diff --git a/README.md b/README.md index cdff01aec..9791c037b 100644 --- a/README.md +++ b/README.md @@ -41,7 +41,7 @@ AstrBot 是一个松耦合、异步、支持多消息平台部署、具有易用 1. **大语言模型对话**。支持各种大语言模型,包括 OpenAI API、Google Gemini、Llama、Deepseek、ChatGLM 等,支持接入本地部署的大模型,通过 Ollama、LLMTuner。具有多轮对话、人格情境、多模态能力,支持图片理解、语音转文字(Whisper)。 2. **多消息平台接入**。支持接入 QQ(OneBot)、QQ 频道、微信(Gewechat)、飞书、Telegram。后续将支持钉钉、Discord、WhatsApp、小爱音响。支持速率限制、白名单、关键词过滤、百度内容审核。 -3. **Agent**。原生支持部分 Agent 能力,如代码执行器、自然语言待办、网页搜索。对接 [Dify 平台](https://astrbot.app/others/dify.html),便捷接入 Dify 智能助手、知识库和 Dify 工作流。 +3. **Agent**。原生支持部分 Agent 能力,如代码执行器、自然语言待办、网页搜索。对接 [Dify 平台](https://dify.ai/),便捷接入 Dify 智能助手、知识库和 Dify 工作流。 4. **插件扩展**。深度优化的插件机制,支持[开发插件](https://astrbot.app/dev/plugin.html)扩展功能,极简开发。已支持安装多个插件。 5. **可视化管理面板**。支持可视化修改配置、插件管理、日志查看等功能,降低配置难度。集成 WebChat,可在面板上与大模型对话。 6. **高稳定性、高模块化**。基于事件总线和流水线的架构设计,高度模块化,低耦合。 diff --git a/README_en.md b/README_en.md index b77fe7344..249dd5e79 100644 --- a/README_en.md +++ b/README_en.md @@ -28,7 +28,7 @@ AstrBot is a loosely coupled, asynchronous chatbot and development framework tha 1. **LLM Conversations** - Supports various LLMs including OpenAI API, Google Gemini, Llama, Deepseek, ChatGLM, etc. Enables local model deployment via Ollama/LLMTuner. Features multi-turn dialogues, personality contexts, multimodal capabilities (image understanding), and speech-to-text (Whisper). 2. **Multi-platform Integration** - Supports QQ (OneBot), QQ Channels, WeChat (Gewechat), Feishu, and Telegram. Planned support for DingTalk, Discord, WhatsApp, and Xiaomi Smart Speakers. Includes rate limiting, whitelisting, keyword filtering, and Baidu content moderation. -3. **Agent Capabilities** - Native support for code execution, natural language TODO lists, web search. Integrates with [Dify Platform](https://astrbot.app/others/dify.html) for easy access to Dify assistants/knowledge bases/workflows. +3. **Agent Capabilities** - Native support for code execution, natural language TODO lists, web search. Integrates with [Dify Platform](https://dify.ai/) for easy access to Dify assistants/knowledge bases/workflows. 4. **Plugin System** - Optimized plugin mechanism with minimal development effort. Supports multiple installed plugins. 5. **Web Dashboard** - Visual configuration management, plugin controls, logging, and WebChat interface for direct LLM interaction. 6. **High Stability & Modularity** - Event bus and pipeline architecture ensures high modularization and loose coupling. diff --git a/README_ja.md b/README_ja.md index 03092818d..325857d9a 100644 --- a/README_ja.md +++ b/README_ja.md @@ -28,7 +28,7 @@ AstrBot は、疎結合、非同期、複数のメッセージプラットフォ 1. **大規模言語モデルの対話**。OpenAI API、Google Gemini、Llama、Deepseek、ChatGLM など、さまざまな大規模言語モデルをサポートし、Ollama、LLMTuner を介してローカルにデプロイされた大規模モデルをサポートします。多輪対話、人格シナリオ、多モーダル機能を備え、画像理解、音声からテキストへの変換(Whisper)をサポートします。 2. **複数のメッセージプラットフォームの接続**。QQ(OneBot)、QQ チャンネル、WeChat(Gewechat)、Feishu、Telegram への接続をサポートします。今後、DingTalk、Discord、WhatsApp、Xiaoai 音響をサポートする予定です。レート制限、ホワイトリスト、キーワードフィルタリング、Baidu コンテンツ監査をサポートします。 -3. **エージェント**。一部のエージェント機能をネイティブにサポートし、コードエグゼキューター、自然言語タスク、ウェブ検索などを提供します。[Dify プラットフォーム](https://astrbot.app/others/dify.html)と連携し、Dify スマートアシスタント、ナレッジベース、Dify ワークフローを簡単に接続できます。 +3. **エージェント**。一部のエージェント機能をネイティブにサポートし、コードエグゼキューター、自然言語タスク、ウェブ検索などを提供します。[Dify プラットフォーム](https://dify.ai/)と連携し、Dify スマートアシスタント、ナレッジベース、Dify ワークフローを簡単に接続できます。 4. **プラグインの拡張**。深く最適化されたプラグインメカニズムを備え、[プラグインの開発](https://astrbot.app/dev/plugin.html)をサポートし、機能を拡張できます。複数のプラグインのインストールをサポートします。 5. **ビジュアル管理パネル**。設定の視覚的な変更、プラグイン管理、ログの表示などをサポートし、設定の難易度を低減します。WebChat を統合し、パネル上で大規模モデルと対話できます。 6. **高い安定性と高いモジュール性**。イベントバスとパイプラインに基づくアーキテクチャ設計により、高度にモジュール化され、低結合です。 diff --git a/astrbot/api/star/__init__.py b/astrbot/api/star/__init__.py index 630786de3..1b33923fe 100644 --- a/astrbot/api/star/__init__.py +++ b/astrbot/api/star/__init__.py @@ -2,11 +2,7 @@ from astrbot.core.star.register import ( register_star as register, # 注册插件(Star) ) -from astrbot.core.star import Context, Star +from astrbot.core.star import Context, Star, StarTools from astrbot.core.star.config import * -__all__ = [ - "register", - "Context", - "Star", -] +__all__ = ["register", "Context", "Star", "StarTools"] diff --git a/astrbot/core/config/default.py b/astrbot/core/config/default.py index 879193f43..00e27c15c 100644 --- a/astrbot/core/config/default.py +++ b/astrbot/core/config/default.py @@ -2,7 +2,7 @@ 如需修改配置,请在 `data/cmd_config.json` 中修改或者在管理面板中可视化修改。 """ -VERSION = "3.5.1" +VERSION = "3.5.2" DB_PATH = "data/data_v3.db" # 默认配置 @@ -98,6 +98,7 @@ DEFAULT_CONFIG = { "plugin_repo_mirror": "", "knowledge_db": {}, "persona": [], + "timezone": "", } @@ -522,6 +523,12 @@ CONFIG_METADATA_2 = { "model": "gemini-2.0-flash-exp", }, "gm_resp_image_modal": False, + "gm_safety_settings": { + "harassment": "BLOCK_MEDIUM_AND_ABOVE", + "hate_speech": "BLOCK_MEDIUM_AND_ABOVE", + "sexually_explicit": "BLOCK_MEDIUM_AND_ABOVE", + "dangerous_content": "BLOCK_MEDIUM_AND_ABOVE", + }, }, "DeepSeek": { "id": "deepseek_default", @@ -671,17 +678,82 @@ CONFIG_METADATA_2 = { "fishaudio-tts-character": "可莉", "timeout": "20", }, + "阿里云百炼_TTS(API)": { + "id": "dashscope_tts", + "type": "dashscope_tts", + "enable": False, + "api_key": "", + "model": "cosyvoice-v1", + "dashscope_tts_voice": "loongstella", + "timeout": "20", + }, }, "items": { + "dashscope_tts_voice": { + "description": "语音合成模型", + "type": "string", + "hint": "阿里云百炼语音合成模型名称。具体可参考 https://help.aliyun.com/zh/model-studio/developer-reference/cosyvoice-python-api 等内容", + }, "gm_resp_image_modal": { "description": "启用图片模态", "type": "bool", "hint": "启用后,将支持返回图片内容。需要模型支持,否则会报错。具体支持模型请查看 Google Gemini 官方网站。温馨提示,如果您需要生成图片,请关闭 `启用群员识别` 配置获得更好的效果。", }, + "gm_safety_settings": { + "description": "安全过滤器", + "type": "object", + "hint": "设置模型输入的内容安全过滤级别。过滤级别分类为NONE(不屏蔽)、HIGH(高风险时屏蔽)、MEDIUM_AND_ABOVE(中等风险及以上屏蔽)、LOW_AND_ABOVE(低风险及以上时屏蔽),具体参见Gemini API文档。", + "items": { + "harassment": { + "description": "骚扰内容", + "type": "string", + "hint": "负面或有害评论", + "options": [ + "BLOCK_NONE", + "BLOCK_ONLY_HIGH", + "BLOCK_MEDIUM_AND_ABOVE", + "BLOCK_LOW_AND_ABOVE", + ], + }, + "hate_speech": { + "description": "仇恨言论", + "type": "string", + "hint": "粗鲁、无礼或亵渎性质内容", + "options": [ + "BLOCK_NONE", + "BLOCK_ONLY_HIGH", + "BLOCK_MEDIUM_AND_ABOVE", + "BLOCK_LOW_AND_ABOVE", + ], + }, + "sexually_explicit": { + "description": "露骨色情内容", + "type": "string", + "hint": "包含性行为或其他淫秽内容的引用", + "options": [ + "BLOCK_NONE", + "BLOCK_ONLY_HIGH", + "BLOCK_MEDIUM_AND_ABOVE", + "BLOCK_LOW_AND_ABOVE", + ], + }, + "dangerous_content": { + "description": "危险内容", + "type": "string", + "hint": "宣扬、助长或鼓励有害行为的信息", + "options": [ + "BLOCK_NONE", + "BLOCK_ONLY_HIGH", + "BLOCK_MEDIUM_AND_ABOVE", + "BLOCK_LOW_AND_ABOVE", + ], + }, + }, + }, "rag_options": { "description": "RAG 选项", "type": "object", - "hint": "检索知识库设置, 非必填。仅 Agent 应用类型支持(智能体应用, 包括 RAG 应用)", + "hint": "检索知识库设置, 非必填。仅 Agent 应用类型支持(智能体应用, 包括 RAG 应用)。阿里云百炼应用开启此功能后将无法多轮对话。", "items": { "pipeline_ids": { "description": "知识库 ID 列表", @@ -1101,6 +1173,12 @@ CONFIG_METADATA_2 = { "type": "string", "hint": "启用后,会以添加环境变量的方式设置代理。格式为 `http://ip:port`", }, + "timezone": { + "description": "时区", + "type": "string", + "obvious_hint": True, + "hint": "时区设置。请填写 IANA 时区名称, 如 Asia/Shanghai, 为空时使用系统默认时区。所有时区请查看: https://data.iana.org/time-zones/tzdb-2021a/zone1970.tab", + }, "log_level": { "description": "控制台日志级别", "type": "string", diff --git a/astrbot/core/core_lifecycle.py b/astrbot/core/core_lifecycle.py index f0e8e2144..ee40187f5 100644 --- a/astrbot/core/core_lifecycle.py +++ b/astrbot/core/core_lifecycle.py @@ -190,7 +190,6 @@ class AstrBotCoreLifecycle: task.cancel() for plugin in self.plugin_manager.context.get_all_stars(): - logger.info(f"正在终止插件 {plugin.name} ...") try: await self.plugin_manager._terminate_plugin(plugin) except Exception as e: diff --git a/astrbot/core/db/plugin/sqlite_impl.py b/astrbot/core/db/plugin/sqlite_impl.py new file mode 100644 index 000000000..5440362af --- /dev/null +++ b/astrbot/core/db/plugin/sqlite_impl.py @@ -0,0 +1,112 @@ +import json +import aiosqlite +import os +from typing import Any +from .plugin_storage import PluginStorage + +DBPATH = "data/plugin_data/sqlite/plugin_data.db" + + +class SQLitePluginStorage(PluginStorage): + """插件数据的 SQLite 存储实现类。 + + 该类提供异步方式将插件数据存储到 SQLite 数据库中,支持数据的增删改查操作。 + 所有数据以 (plugin, key) 作为复合主键进行索引。 + """ + + _instance = None # Standalone instance of the class + _db_conn = None + db_path = None + + def __new__(cls): + """ + 创建或获取 SQLitePluginStorage 的单例实例。 + 如果实例已存在,则返回现有实例;否则创建一个新实例。 + 数据在 `data/plugin_data/sqlite/plugin_data.db` 下。 + """ + os.makedirs(os.path.dirname(DBPATH), exist_ok=True) + if cls._instance is None: + cls._instance = super(SQLitePluginStorage, cls).__new__(cls) + cls._instance.db_path = DBPATH + return cls._instance + + async def _init_db(self): + """初始化数据库连接(只执行一次)""" + if SQLitePluginStorage._db_conn is None: + SQLitePluginStorage._db_conn = await aiosqlite.connect(self.db_path) + await self._setup_db() + + async def _setup_db(self): + """ + 异步初始化数据库。 + + 创建插件数据表,如果表不存在则创建,表结构包含 plugin、key 和 value 字段, + 其中 plugin 和 key 组合作为主键。 + """ + await self._db_conn.execute(""" + CREATE TABLE IF NOT EXISTS plugin_data ( + plugin TEXT, + key TEXT, + value TEXT, + PRIMARY KEY (plugin, key) + ) + """) + await self._db_conn.commit() + + async def set(self, plugin: str, key: str, value: Any): + """ + 异步存储数据。 + + 将指定插件的键值对存入数据库,如果键已存在则更新值。 + 值会被序列化为 JSON 字符串后存储。 + + Args: + plugin: 插件标识符 + key: 数据键名 + value: 要存储的数据值(任意类型,将被 JSON 序列化) + """ + await self._init_db() + await self._db_conn.execute( + "INSERT INTO plugin_data (plugin, key, value) VALUES (?, ?, ?) " + "ON CONFLICT(plugin, key) DO UPDATE SET value = excluded.value", + (plugin, key, json.dumps(value)), + ) + await self._db_conn.commit() + + async def get(self, plugin: str, key: str) -> Any: + """ + 异步获取数据。 + + 从数据库中获取指定插件和键名对应的值, + 返回的值会从 JSON 字符串反序列化为原始数据类型。 + + Args: + plugin: 插件标识符 + key: 数据键名 + + Returns: + Any: 存储的数据值,如果未找到则返回 None + """ + await self._init_db() + async with self._db_conn.execute( + "SELECT value FROM plugin_data WHERE plugin = ? AND key = ?", + (plugin, key), + ) as cursor: + row = await cursor.fetchone() + return json.loads(row[0]) if row else None + + async def delete(self, plugin: str, key: str): + """ + 异步删除数据。 + + 从数据库中删除指定插件和键名对应的数据项。 + + Args: + plugin: 插件标识符 + key: 要删除的数据键名 + """ + await self._init_db() + await self._db_conn.execute( + "DELETE FROM plugin_data WHERE plugin = ? AND key = ?", (plugin, key) + ) + await self._db_conn.commit() diff --git a/astrbot/core/log.py b/astrbot/core/log.py index 501f0012e..e1e2cde2e 100644 --- a/astrbot/core/log.py +++ b/astrbot/core/log.py @@ -108,11 +108,12 @@ class LogBroker: """ self.subscribers.remove(q) - def publish(self, log_entry: str): + def publish(self, log_entry: dict): """发布新日志到所有订阅者, 使用非阻塞方式投递, 避免一个订阅者阻塞整个系统 Args: - log_entry (str): 日志消息, 可以是字符串或字典 + log_entry (dict): 日志消息, 包含日志级别和日志内容. + example: {"level": "INFO", "data": "This is a log message.", "time": "2023-10-01 12:00:00"} """ self.log_cache.append(log_entry) for q in self.subscribers: @@ -140,7 +141,11 @@ class LogQueueHandler(logging.Handler): record (logging.LogRecord): 日志记录对象, 包含日志信息 """ log_entry = self.format(record) - self.log_broker.publish(log_entry) + self.log_broker.publish({ + "level": record.levelname, + "time": record.asctime, + "data": log_entry, + }) class LogManager: diff --git a/astrbot/core/pipeline/process_stage/method/llm_request.py b/astrbot/core/pipeline/process_stage/method/llm_request.py index 353d9d3df..674a7fd79 100644 --- a/astrbot/core/pipeline/process_stage/method/llm_request.py +++ b/astrbot/core/pipeline/process_stage/method/llm_request.py @@ -58,9 +58,9 @@ class LLMRequestSubStage(Stage): if event.get_extra("provider_request"): req = event.get_extra("provider_request") - assert isinstance( - req, ProviderRequest - ), "provider_request 必须是 ProviderRequest 类型。" + assert isinstance(req, ProviderRequest), ( + "provider_request 必须是 ProviderRequest 类型。" + ) if req.conversation: req.contexts = json.loads(req.conversation.history) @@ -80,7 +80,6 @@ class LLMRequestSubStage(Stage): conversation_id = await self.conv_manager.get_curr_conversation_id( event.unified_msg_origin ) - req.session_id = event.unified_msg_origin if not conversation_id: conversation_id = await self.conv_manager.new_conversation( event.unified_msg_origin @@ -134,6 +133,10 @@ class LLMRequestSubStage(Stage): logger.debug("上下文长度超过限制,将截断。") req.contexts = req.contexts[-self.max_context_length * 2 :] + # session_id + if not req.session_id: + req.session_id = event.unified_msg_origin + try: need_loop = True while need_loop: diff --git a/astrbot/core/pipeline/respond/stage.py b/astrbot/core/pipeline/respond/stage.py index a43f0b32d..86c165945 100644 --- a/astrbot/core/pipeline/respond/stage.py +++ b/astrbot/core/pipeline/respond/stage.py @@ -2,6 +2,7 @@ import random import asyncio import math import traceback +import astrbot.core.message.components as Comp from typing import Union, AsyncGenerator from ..stage import register_stage, Stage from ..context import PipelineContext @@ -11,11 +12,42 @@ from astrbot.core import logger from astrbot.core.message.message_event_result import BaseMessageComponent from astrbot.core.star.star_handler import star_handlers_registry, EventType from astrbot.core.star.star import star_map -from astrbot.core.message.components import Plain, Reply, At @register_stage class RespondStage(Stage): + # 组件类型到其非空判断函数的映射 + _component_validators = { + Comp.Plain: lambda comp: bool(comp.text and comp.text.strip()), # 纯文本消息需要strip + Comp.Face: lambda comp: comp.id is not None, # QQ表情 + Comp.Record: lambda comp: bool(comp.file), # 语音 + Comp.Video: lambda comp: bool(comp.file), # 视频 + Comp.At: lambda comp: bool(comp.qq) or bool(comp.name), # @ + Comp.AtAll: lambda comp: True, # @所有人 + Comp.RPS: lambda comp: True, # 不知道是啥(未完成) + Comp.Dice: lambda comp: True, # 骰子(未完成) + Comp.Shake: lambda comp: True, # 摇一摇(未完成) + Comp.Anonymous: lambda comp: True, # 匿名(未完成) + Comp.Share: lambda comp: bool(comp.url) and bool(comp.title), # 分享 + Comp.Contact: lambda comp: True, # 联系人(未完成) + Comp.Location: lambda comp: bool(comp.lat and comp.lon), # 位置 + Comp.Music: lambda comp: bool(comp._type) and bool(comp.url) and bool(comp.audio), # 音乐 + Comp.Image: lambda comp: bool(comp.file), # 图片 + Comp.Reply: lambda comp: bool(comp.id) and comp.sender_id is not None, # 回复 + Comp.RedBag: lambda comp: bool(comp.title), # 红包 + Comp.Poke: lambda comp: comp.id != 0 and comp.qq != 0, # 戳一戳 + Comp.Forward: lambda comp: bool(comp.id and comp.id.strip()), # 转发 + Comp.Node: lambda comp: bool(comp.name) and comp.uin != 0 and bool(comp.content), # 一个转发节点 + Comp.Nodes: lambda comp: bool(comp.nodes), # 多个转发节点 + Comp.Xml: lambda comp: bool(comp.data and comp.data.strip()), # XML + Comp.Json: lambda comp: bool(comp.data), # JSON + Comp.CardImage: lambda comp: bool(comp.file), # 卡片图片 + Comp.TTS: lambda comp: bool(comp.text and comp.text.strip()), # 语音合成 + Comp.Unknown: lambda comp: bool(comp.text and comp.text.strip()), # 未知消息 + Comp.File: lambda comp: bool(comp.file), # 文件 + Comp.WechatEmoji: lambda comp: bool(comp.md5), # 微信表情 + } + async def initialize(self, ctx: PipelineContext): self.ctx = ctx @@ -62,7 +94,7 @@ class RespondStage(Stage): async def _calc_comp_interval(self, comp: BaseMessageComponent) -> float: """分段回复 计算间隔时间""" if self.interval_method == "log": - if isinstance(comp, Plain): + if isinstance(comp, Comp.Plain): wc = await self._word_cnt(comp.text) i = math.log(wc + 1, self.log_base) return random.uniform(i, i + 0.5) @@ -72,6 +104,28 @@ class RespondStage(Stage): # random return random.uniform(self.interval[0], self.interval[1]) + async def _is_empty_message_chain(self, chain: list[BaseMessageComponent]): + """检查消息链是否为空 + + Args: + chain (list[BaseMessageComponent]): 包含消息对象的列表 + """ + if not chain: + return True + + for comp in chain: + comp_type = type(comp) + + # 检查组件类型是否在字典中 + if comp_type in self._component_validators: + if self._component_validators[comp_type](comp): + return False + else: + logger.info(f"空内容检查: 无法识别的组件类型: {comp_type.__name__}") + + # 如果所有组件都为空 + return True + async def process( self, event: AstrMessageEvent ) -> Union[None, AsyncGenerator[None, None]]: @@ -82,6 +136,16 @@ class RespondStage(Stage): if len(result.chain) > 0: await event._pre_send() + # 检查消息链是否为空 + try: + if await self._is_empty_message_chain(result.chain): + logger.info("消息为空,跳过发送阶段") + event.clear_result() + event.stop_event() + return + except Exception as e: + logger.warning(f"空内容检查异常: {e}") + if self.enable_seg and ( (self.only_llm_result and result.is_llm_result()) or not self.only_llm_result @@ -89,13 +153,13 @@ class RespondStage(Stage): decorated_comps = [] if self.reply_with_mention: for comp in result.chain: - if isinstance(comp, At): + if isinstance(comp, Comp.At): decorated_comps.append(comp) result.chain.remove(comp) break if self.reply_with_quote: for comp in result.chain: - if isinstance(comp, Reply): + if isinstance(comp, Comp.Reply): decorated_comps.append(comp) result.chain.remove(comp) break diff --git a/astrbot/core/pipeline/result_decorate/stage.py b/astrbot/core/pipeline/result_decorate/stage.py index 4894b2e03..d7bb9583c 100644 --- a/astrbot/core/pipeline/result_decorate/stage.py +++ b/astrbot/core/pipeline/result_decorate/stage.py @@ -156,9 +156,7 @@ class ResultDecorateStage(Stage): self.ctx.astrbot_config["provider_tts_settings"]["enable"] and result.is_llm_result() ): - tts_provider = ( - self.ctx.plugin_manager.context.provider_manager.curr_tts_provider_inst - ) + tts_provider = self.ctx.plugin_manager.context.provider_manager.curr_tts_provider_inst new_chain = [] for comp in result.chain: if isinstance(comp, Plain) and len(comp.text) > 1: diff --git a/astrbot/core/pipeline/whitelist_check/stage.py b/astrbot/core/pipeline/whitelist_check/stage.py index 85f911a60..b140d23ba 100644 --- a/astrbot/core/pipeline/whitelist_check/stage.py +++ b/astrbot/core/pipeline/whitelist_check/stage.py @@ -15,6 +15,9 @@ class WhitelistCheckStage(Stage): "enable_id_white_list" ] self.whitelist = ctx.astrbot_config["platform_settings"]["id_whitelist"] + self.whitelist = [ + str(i).strip() for i in self.whitelist if str(i).strip() != "" + ] self.wl_ignore_admin_on_group = ctx.astrbot_config["platform_settings"][ "wl_ignore_admin_on_group" ] @@ -53,7 +56,7 @@ class WhitelistCheckStage(Stage): return if ( event.unified_msg_origin not in self.whitelist - and event.get_group_id() not in self.whitelist + and str(event.get_group_id()).strip() not in self.whitelist ): if self.wl_log: logger.info( diff --git a/astrbot/core/platform/sources/dingtalk/dingtalk_event.py b/astrbot/core/platform/sources/dingtalk/dingtalk_event.py index e865913e1..7980ecd55 100644 --- a/astrbot/core/platform/sources/dingtalk/dingtalk_event.py +++ b/astrbot/core/platform/sources/dingtalk/dingtalk_event.py @@ -24,7 +24,7 @@ class DingtalkMessageEvent(AstrMessageEvent): if isinstance(segment, Comp.Plain): segment.text = segment.text.strip() await asyncio.get_event_loop().run_in_executor( - None, client.reply_text, segment.text, self.message_obj.raw_message + None, client.reply_markdown, "AstrBot", segment.text, self.message_obj.raw_message ) elif isinstance(segment, Comp.Image): markdown_str = "" diff --git a/astrbot/core/platform/sources/telegram/tg_event.py b/astrbot/core/platform/sources/telegram/tg_event.py index 87fea26c6..eab41ad84 100644 --- a/astrbot/core/platform/sources/telegram/tg_event.py +++ b/astrbot/core/platform/sources/telegram/tg_event.py @@ -1,8 +1,10 @@ +import telegramify_markdown from astrbot.api.event import AstrMessageEvent, MessageChain from astrbot.api.platform import AstrBotMessage, PlatformMetadata, MessageType from astrbot.api.message_components import Plain, Image, Reply, At, File, Record from telegram.ext import ExtBot from astrbot.core.utils.io import download_file +from astrbot import logger class TelegramPlatformEvent(AstrMessageEvent): @@ -49,7 +51,17 @@ class TelegramPlatformEvent(AstrMessageEvent): if at_user_id and not at_flag: i.text = f"@{at_user_id} " + i.text at_flag = True - await client.send_message(text=i.text, **payload) + text = i.text + try: + text = telegramify_markdown.markdownify( + i.text, max_line_length=None, normalize_whitespace=False + ) + except Exception as e: + logger.warning( + f"MarkdownV2 conversion failed: {e}. Using plain text instead." + ) + return + await client.send_message(text=text, parse_mode="MarkdownV2", **payload) elif isinstance(i, Image): image_path = await i.convert_to_file_path() await client.send_photo(photo=image_path, **payload) diff --git a/astrbot/core/platform/sources/webchat/webchat_event.py b/astrbot/core/platform/sources/webchat/webchat_event.py index fb42080db..ef82dbfed 100644 --- a/astrbot/core/platform/sources/webchat/webchat_event.py +++ b/astrbot/core/platform/sources/webchat/webchat_event.py @@ -3,7 +3,7 @@ import uuid import base64 from astrbot.api import logger from astrbot.api.event import AstrMessageEvent, MessageChain -from astrbot.api.message_components import Plain, Image +from astrbot.api.message_components import Plain, Image, Record from astrbot.core.utils.io import download_image_by_url from astrbot.core import web_chat_back_queue @@ -47,6 +47,22 @@ class WebChatMessageEvent(AstrMessageEvent): with open(comp.file, "rb") as f2: f.write(f2.read()) web_chat_back_queue.put_nowait((f"[IMAGE]{filename}", cid)) + elif isinstance(comp, Record): + # save record to local + filename = str(uuid.uuid4()) + ".wav" + path = os.path.join(imgs_dir, filename) + if comp.file and comp.file.startswith("file:///"): + ph = comp.file[8:] + with open(path, "wb") as f: + with open(ph, "rb") as f2: + f.write(f2.read()) + elif comp.file and comp.file.startswith("http"): + await download_image_by_url(comp.file, path=path) + else: + with open(path, "wb") as f: + with open(comp.file, "rb") as f2: + f.write(f2.read()) + web_chat_back_queue.put_nowait((f"[RECORD]{filename}", cid)) else: logger.debug(f"webchat 忽略: {comp.type}") web_chat_back_queue.put_nowait(None) diff --git a/astrbot/core/provider/manager.py b/astrbot/core/provider/manager.py index 71647a5ac..a3fa65e86 100644 --- a/astrbot/core/provider/manager.py +++ b/astrbot/core/provider/manager.py @@ -198,6 +198,10 @@ class ProviderManager: from .sources.fishaudio_tts_api_source import ( ProviderFishAudioTTSAPI as ProviderFishAudioTTSAPI, ) + case "dashscope_tts": + from .sources.dashscope_tts import ( + ProviderDashscopeTTSAPI as ProviderDashscopeTTSAPI, + ) except (ImportError, ModuleNotFoundError) as e: logger.critical( f"加载 {provider_config['type']}({provider_config['id']}) 提供商适配器失败:{e}。可能是因为有未安装的依赖。" diff --git a/astrbot/core/provider/sources/dashscope_source.py b/astrbot/core/provider/sources/dashscope_source.py index 7158d57b9..14aefceef 100644 --- a/astrbot/core/provider/sources/dashscope_source.py +++ b/astrbot/core/provider/sources/dashscope_source.py @@ -51,10 +51,14 @@ class ProviderDashscope(ProviderOpenAIOfficial): self.timeout = int(self.timeout) def has_rag_options(self): - if ( - self.rag_options - and self.rag_options.get("pipeline_ids", None) - and self.rag_options.get("file_ids", None) + """判断是否有 RAG 选项 + + Returns: + bool: 是否有 RAG 选项 + """ + if self.rag_options and ( + len(self.rag_options.get("pipeline_ids", [])) > 0 + or len(self.rag_options.get("file_ids", [])) > 0 ): return True return False @@ -78,7 +82,7 @@ class ProviderDashscope(ProviderOpenAIOfficial): if ( self.dashscope_app_type in ["agent", "dialog-workflow"] - and self.has_rag_options() + and not self.has_rag_options() ): # 支持多轮对话的 new_record = {"role": "user", "content": prompt} @@ -92,12 +96,15 @@ class ProviderDashscope(ProviderOpenAIOfficial): if "_no_save" in part: del part["_no_save"] # 调用阿里云百炼 API + payload = { + "app_id": self.app_id, + "api_key": self.api_key, + "messages": context_query, + "biz_params": payload_vars or None, + } partial = functools.partial( Application.call, - app_id=self.app_id, - api_key=self.api_key, - messages=context_query, - biz_params=payload_vars or None, + **payload, ) response = await asyncio.get_event_loop().run_in_executor(None, partial) else: @@ -134,7 +141,8 @@ class ProviderDashscope(ProviderOpenAIOfficial): if self.output_reference and response.output.get("doc_references", None): ref_str = "" for ref in response.output.get("doc_references", []): - ref_str += f"{ref['index_id']}. {ref['title']}\n" + ref_title = ref.get("title", "") if ref.get("title") else ref.get("doc_name", "") + ref_str += f"{ref['index_id']}. {ref_title}\n" output_text += f"\n\n回答来源:\n{ref_str}" return LLMResponse(role="assistant", completion_text=output_text) diff --git a/astrbot/core/provider/sources/dashscope_tts.py b/astrbot/core/provider/sources/dashscope_tts.py new file mode 100644 index 000000000..06b390fcd --- /dev/null +++ b/astrbot/core/provider/sources/dashscope_tts.py @@ -0,0 +1,39 @@ +import dashscope +import uuid +import asyncio +from dashscope.audio.tts_v2 import * +from ..provider import TTSProvider +from ..entites import ProviderType +from ..register import register_provider_adapter + + +@register_provider_adapter( + "dashscope_tts", "Dashscope TTS API", provider_type=ProviderType.TEXT_TO_SPEECH +) +class ProviderDashscopeTTSAPI(TTSProvider): + def __init__( + self, + provider_config: dict, + provider_settings: dict, + ) -> None: + super().__init__(provider_config, provider_settings) + self.chosen_api_key: str = provider_config.get("api_key", "") + self.voice: str = provider_config.get("dashscope_tts_voice", "loongstella") + self.set_model(provider_config.get("model", None)) + self.timeout_ms = float(provider_config.get("timeout", 20))*1000 + + dashscope.api_key = self.chosen_api_key + self.synthesizer = SpeechSynthesizer( + model=self.get_model(), + voice=self.voice, + format=AudioFormat.WAV_24000HZ_MONO_16BIT, + ) + + async def get_audio(self, text: str) -> str: + path = f"data/temp/dashscope_tts_{uuid.uuid4()}.wav" + audio = await asyncio.get_event_loop().run_in_executor( + None, self.synthesizer.call, text, self.timeout_ms + ) + with open(path, "wb") as f: + f.write(audio) + return path diff --git a/astrbot/core/provider/sources/edge_tts_source.py b/astrbot/core/provider/sources/edge_tts_source.py index b6b758e29..0eadb2190 100644 --- a/astrbot/core/provider/sources/edge_tts_source.py +++ b/astrbot/core/provider/sources/edge_tts_source.py @@ -63,9 +63,7 @@ class ProviderEdgeTTS(TTSProvider): ff = FFmpeg() ff.convert(input=mp3_path, output=wav_path) except Exception as e: - logger.debug( - f"pyffmpeg 转换失败: {e}, 尝试使用 ffmpeg 命令行进行转换" - ) + logger.debug(f"pyffmpeg 转换失败: {e}, 尝试使用 ffmpeg 命令行进行转换") # use ffmpeg command line # 使用ffmpeg将MP3转换为标准WAV格式 diff --git a/astrbot/core/provider/sources/gemini_source.py b/astrbot/core/provider/sources/gemini_source.py index 3233b3453..9f5f7c3c1 100644 --- a/astrbot/core/provider/sources/gemini_source.py +++ b/astrbot/core/provider/sources/gemini_source.py @@ -43,6 +43,7 @@ class SimpleGoogleGenAIClient: system_instruction: str = "", tools: dict = None, modalities: List[str] = ["Text"], + safety_settings: List[dict] = [], ): payload = {} if system_instruction: @@ -53,6 +54,10 @@ class SimpleGoogleGenAIClient: payload["generationConfig"] = { "responseModalities": modalities, } + payload["safetySettings"] = [ + {"category": s["category"], "threshold": s["threshold"]} + for s in safety_settings + ] logger.debug(f"payload: {payload}") request_url = ( f"{self.api_base}/v1beta/models/{model}:generateContent?key={self.api_key}" @@ -106,6 +111,21 @@ class ProviderGoogleGenAI(Provider): ) self.set_model(provider_config["model_config"]["model"]) + safety_mapping = { + "harassment": "HARM_CATEGORY_HARASSMENT", + "hate_speech": "HARM_CATEGORY_HATE_SPEECH", + "sexually_explicit": "HARM_CATEGORY_SEXUALLY_EXPLICIT", + "dangerous_content": "HARM_CATEGORY_DANGEROUS_CONTENT", + } + + self.safety_settings = [] + user_safety_config = self.provider_config.get("gm_safety_settings", {}) + for config_key, harm_category in safety_mapping.items(): + if threshold := user_safety_config.get(config_key): + self.safety_settings.append( + {"category": harm_category, "threshold": threshold} + ) + async def get_models(self): return await self.client.models_list() @@ -127,7 +147,7 @@ class ProviderGoogleGenAI(Provider): if message["role"] == "user": if isinstance(message["content"], str): if not message["content"]: - message["content"] = "" + message["content"] = "" google_genai_conversation.append( {"role": "user", "parts": [{"text": message["content"]}]} @@ -138,7 +158,7 @@ class ProviderGoogleGenAI(Provider): for part in message["content"]: if part["type"] == "text": if not part["text"]: - part["text"] = "" + part["text"] = "" parts.append({"text": part["text"]}) elif part["type"] == "image_url": parts.append( @@ -156,7 +176,7 @@ class ProviderGoogleGenAI(Provider): elif message["role"] == "assistant": if "content" in message: if not message["content"]: - message["content"] = "" + message["content"] = "" google_genai_conversation.append( {"role": "model", "parts": [{"text": message["content"]}]} ) @@ -205,6 +225,7 @@ class ProviderGoogleGenAI(Provider): system_instruction=system_instruction, tools=tool, modalities=modalites, + safety_settings=self.safety_settings, ) logger.debug(f"result: {result}") diff --git a/astrbot/core/provider/sources/openai_source.py b/astrbot/core/provider/sources/openai_source.py index 766835719..f8d392404 100644 --- a/astrbot/core/provider/sources/openai_source.py +++ b/astrbot/core/provider/sources/openai_source.py @@ -221,8 +221,8 @@ class ProviderOpenAIOfficial(Provider): payloads["messages"] = new_contexts elif ( "Function calling is not enabled" in str(e) - or ("tool" in str(e) and "support" in str(e).lower()) - or ("function" in str(e) and "support" in str(e).lower()) + or ("tool" in str(e).lower() and "support" in str(e).lower()) + or ("function" in str(e).lower() and "support" in str(e).lower()) ): # openai, ollama, gemini openai, siliconcloud 的错误提示与 code 不统一,只能通过字符串匹配 logger.info( diff --git a/astrbot/core/star/__init__.py b/astrbot/core/star/__init__.py index b1bd5de81..ec1ee655b 100644 --- a/astrbot/core/star/__init__.py +++ b/astrbot/core/star/__init__.py @@ -4,12 +4,14 @@ from .context import Context from astrbot.core.provider import Provider from astrbot.core.utils.command_parser import CommandParserMixin from astrbot.core import html_renderer +from astrbot.core.star.star_tools import StarTools class Star(CommandParserMixin): """所有插件(Star)的父类,所有插件都应该继承于这个类""" def __init__(self, context: Context): + StarTools.initialize(context) self.context = context async def text_to_image(self, text: str, return_url=True) -> str: @@ -27,4 +29,4 @@ class Star(CommandParserMixin): pass -__all__ = ["Star", "StarMetadata", "PluginManager", "Context", "Provider"] +__all__ = ["Star", "StarMetadata", "PluginManager", "Context", "Provider", "StarTools"] diff --git a/astrbot/core/star/star_manager.py b/astrbot/core/star/star_manager.py index 75a18317c..2d610f15c 100644 --- a/astrbot/core/star/star_manager.py +++ b/astrbot/core/star/star_manager.py @@ -596,7 +596,7 @@ class PluginManager: asyncio.get_event_loop().run_in_executor( None, star_metadata.star_cls.__del__ ) - else: + elif hasattr(star_metadata.star_cls, "terminate"): await star_metadata.star_cls.terminate() async def turn_on_plugin(self, plugin_name: str): diff --git a/astrbot/core/star/star_tools.py b/astrbot/core/star/star_tools.py new file mode 100644 index 000000000..68468e353 --- /dev/null +++ b/astrbot/core/star/star_tools.py @@ -0,0 +1,144 @@ +from typing import Union, Awaitable, List, Optional, ClassVar +from astrbot.core.message.components import BaseMessageComponent +from astrbot.core.message.message_event_result import MessageChain +from astrbot.api.platform import MessageMember, AstrBotMessage +from astrbot.core.platform.astr_message_event import MessageSesion +from astrbot.core.star.context import Context + + +class StarTools: + """ + 提供给插件使用的便捷工具函数集合 + 这些方法封装了一些常用操作,使插件开发更加简单便捷! + """ + + _context: ClassVar[Optional[Context]] = None + + @classmethod + def initialize(cls, context: Context) -> None: + """ + 初始化StarTools,设置context引用 + + Args: + context: 暴露给插件的上下文 + """ + cls._context = context + + @classmethod + async def send_message( + cls, session: Union[str, MessageSesion], message_chain: MessageChain + ) -> bool: + """ + 根据session(unified_msg_origin)主动发送消息 + + Args: + session: 消息会话。通过event.session或者event.unified_msg_origin获取 + message_chain: 消息链 + + Returns: + bool: 是否找到匹配的平台 + + Raises: + ValueError: 当session为字符串且解析失败时抛出 + + Note: + qq_official(QQ官方API平台)不支持此方法 + """ + return await cls._context.send_message(session, message_chain) + + @classmethod + async def create_message( + cls, + type: str, + self_id: str, + session_id: str, + message_id: str, + sender: MessageMember, + message: List[BaseMessageComponent], + message_str: str, + raw_message: object, + group_id: str = "", + ): + """ + 创建一个AstrBot消息对象 + + Args: + type (str): 消息类型 + self_id (str): 机器人自身ID + session_id (str): 会话ID(通常为用户ID)(QQ号, 群号等) + message_id (str): 消息ID + sender (MessageMember): 发送者信息 + message (List[BaseMessageComponent]): 消息组件列表 + message_str (str): 消息字符串 + raw_message (object): 原始消息对象 + group_id (str, optional): 群组ID, 如果为私聊则为空. Defaults to "". + + Returns: + AstrBotMessage: 创建的消息对象 + """ + abm = AstrBotMessage() + abm.type = type + abm.self_id = self_id + abm.session_id = session_id + abm.message_id = message_id + abm.sender = sender + abm.message = message + abm.message_str = message_str + abm.raw_message = raw_message + abm.group_id = group_id + return abm + + # todo: 添加构造事件的方法 + # async def create_event( + # self, platform: str, umo: str, sender_id: str, session_id: str + # ): + # platform = self._context.get_platform(platform) + + # todo: 添加找到对应平台并提交对应事件的方法 + + @classmethod + def activate_llm_tool(cls, name: str) -> bool: + """ + 激活一个已经注册的函数调用工具 + 注册的工具默认是激活状态 + + Args: + name (str): 工具名称 + """ + return cls._context.activate_llm_tool(name) + + @classmethod + def deactivate_llm_tool(cls, name: str) -> bool: + """ + 停用一个已经注册的函数调用工具 + + Args: + name (str): 工具名称 + """ + return cls._context.deactivate_llm_tool(name) + + @classmethod + def register_llm_tool( + cls, name: str, func_args: list, desc: str, func_obj: Awaitable + ) -> None: + """ + 为函数调用(function-calling/tools-use)添加工具 + + Args: + name (str): 工具名称 + func_args (list): 函数参数列表 + desc (str): 工具描述 + func_obj (Awaitable): 函数对象,必须是异步函数 + """ + cls._context.register_llm_tool(name, func_args, desc, func_obj) + + @classmethod + def unregister_llm_tool(cls, name: str) -> None: + """ + 删除一个函数调用工具 + 如果再要启用,需要重新注册 + + Args: + name (str): 工具名称 + """ + cls._context.unregister_llm_tool(name) diff --git a/astrbot/core/utils/io.py b/astrbot/core/utils/io.py index c996cadbd..5bd5d7c77 100644 --- a/astrbot/core/utils/io.py +++ b/astrbot/core/utils/io.py @@ -103,7 +103,7 @@ async def download_image_by_url( with open(path, "wb") as f: f.write(await resp.read()) return path - except aiohttp.client.ClientConnectorSSLError: + except (aiohttp.ClientConnectorSSLError, aiohttp.ClientConnectorCertificateError): # 关闭SSL验证 ssl_context = ssl.create_default_context() ssl_context.set_ciphers("DEFAULT") @@ -152,7 +152,7 @@ async def download_file(url: str, path: str, show_progress: bool = False): f"\r下载进度: {downloaded_size / total_size:.2%} 速度: {speed:.2f} KB/s", end="", ) - except aiohttp.client.ClientConnectorSSLError: + except (aiohttp.ClientConnectorSSLError, aiohttp.ClientConnectorCertificateError): # 关闭SSL验证 ssl_context = ssl.create_default_context() ssl_context.set_ciphers("DEFAULT") diff --git a/astrbot/dashboard/routes/log.py b/astrbot/dashboard/routes/log.py index b1c68722c..6f3940c0a 100644 --- a/astrbot/dashboard/routes/log.py +++ b/astrbot/dashboard/routes/log.py @@ -20,7 +20,7 @@ class LogRoute(Route): message = await queue.get() payload = { "type": "log", - "data": message, + **message # see astrbot/core/log.py } yield f"data: {json.dumps(payload, ensure_ascii=False)}\n\n" except asyncio.CancelledError: diff --git a/astrbot/dashboard/routes/static_file.py b/astrbot/dashboard/routes/static_file.py index 5d4c05c6b..4503a28e5 100644 --- a/astrbot/dashboard/routes/static_file.py +++ b/astrbot/dashboard/routes/static_file.py @@ -21,7 +21,7 @@ class StaticFileRoute(Route): "/about", "/extension-marketplace", "/conversation", - "/tool-use" + "/tool-use", ] for i in index_: self.app.add_url_rule(i, view_func=self.index) diff --git a/changelogs/v3.5.2.md b/changelogs/v3.5.2.md new file mode 100644 index 000000000..cbc12af9f --- /dev/null +++ b/changelogs/v3.5.2.md @@ -0,0 +1,31 @@ +# What's Changed + +> 📢 在升级前,请完整阅读本次更新日志。 + +## ✨ 新增的功能 + +1. 安装完插件后自动弹出插件仓库 README 对话框 @zhx8702 +4. 支持阿里云百炼 TTS@Soulter +5. 支持 Telegram MarkdownV2 渲染 @Soulter +6. 支持 钉钉 Markdown 渲染 @Soulter +6. 增加对 Gemini 系列模型的输入安全设置参数支持 @AliveGh0st +7. 支持手动设置时区以应对容器、国外用户的时区问题 @anka-afk @Raven95676 @Soulter +8. 插件市场显示帮助按钮 @Soulter + +## 🎈 功能性优化 + +1. WebUI 的日志通信使用 SSE 替代 Websockets @Soulter +2. 在发送消息之前统一检查消息内容是否为空, 不允许发送空消息, 以解决该消息内容不支持查看以及 Gemini 返回 `` 问题 @anka-afk +3. 更新 Dify 平台链接为官方域名 by @Captain-Slacker-OwO +4. 人格 prompt 输入框支持调节高度 @Soulter + +## 🐛 修复的 Bug + +1. 将最多携带对话数量修改回 `-1` 时出现报错 #1074 @anka-afk +2. 修复无法识别到函数调用异常的问题 by @Soulter +3. 修复 aiocqhttp 适配器下空白 plain 导致的 `the object is not a proper segment chain` 报错问题 @Soulter +4. 修复阿里百炼应用无法多轮会话的问题 @Soulter + +## 🧩 新增的插件 + +待补充 diff --git a/dashboard/src/components/shared/AstrBotConfig.vue b/dashboard/src/components/shared/AstrBotConfig.vue index 2e24af80a..0117fd2f3 100644 --- a/dashboard/src/components/shared/AstrBotConfig.vue +++ b/dashboard/src/components/shared/AstrBotConfig.vue @@ -94,7 +94,6 @@ v-else-if="metadata[metadataKey].items[key]?.type === 'text' && !metadata[metadataKey].items[key]?.invisible" v-model="iterable[key]" variant="outlined" - auto-grow rows="3" class="config-field" hide-details diff --git a/dashboard/src/components/shared/ConsoleDisplayer.vue b/dashboard/src/components/shared/ConsoleDisplayer.vue index dfee44ce9..e780d6569 100644 --- a/dashboard/src/components/shared/ConsoleDisplayer.vue +++ b/dashboard/src/components/shared/ConsoleDisplayer.vue @@ -3,9 +3,20 @@ import { useCommonStore } from '@/stores/common'; \ No newline at end of file + + + \ No newline at end of file diff --git a/dashboard/src/components/shared/ListConfigItem.vue b/dashboard/src/components/shared/ListConfigItem.vue index 9105b5724..c166b2496 100644 --- a/dashboard/src/components/shared/ListConfigItem.vue +++ b/dashboard/src/components/shared/ListConfigItem.vue @@ -3,12 +3,36 @@ - + {{ item }} - - mdi-close - + +
+ + mdi-pencil + + + mdi-close + +
+
+ + mdi-check + + + mdi-close + +
@@ -41,6 +65,8 @@ export default { return { newItem: '', items: this.value, + editIndex: -1, + editItem: '', }; }, watch: { @@ -58,6 +84,20 @@ export default { removeItem(index) { this.items.splice(index, 1); }, + startEdit(index, item) { + this.editIndex = index; + this.editItem = item; + }, + saveEdit() { + if (this.editItem.trim() !== '') { + this.items[this.editIndex] = this.editItem.trim(); + this.cancelEdit(); + } + }, + cancelEdit() { + this.editIndex = -1; + this.editItem = ''; + }, }, }; @@ -82,4 +122,8 @@ export default { .v-btn { margin-left: 8px; } + +.edit-btn { + margin-right: -8px; +} \ No newline at end of file diff --git a/dashboard/src/stores/common.js b/dashboard/src/stores/common.js index f983115ca..49f55f5db 100644 --- a/dashboard/src/stores/common.js +++ b/dashboard/src/stores/common.js @@ -20,7 +20,7 @@ export const useCommonStore = defineStore({ "gewechat": "https://astrbot.app/deploy/platform/gewechat.html", "lark": "https://astrbot.app/deploy/platform/lark.html", "telegram": "https://astrbot.app/deploy/platform/telegram.html", - "dingtalk": "https: //astrbot.app/deploy/platform/dingtalk.html", + "dingtalk": "https://astrbot.app/deploy/platform/dingtalk.html", }, pluginMarketData: [], @@ -62,17 +62,26 @@ export const useCommonStore = defineStore({ } const text = decoder.decode(value); - const lines = text.split('\n'); + const lines = text.split('\n\n'); lines.forEach(line => { if (line.startsWith('data:')) { const data = line.substring(5).trim(); - // {"type":"log","data":"[2021-08-01 00:00:00] INFO: Hello, world!"} - - let data_json = JSON.parse(data) + let data_json = {} + try { + data_json = JSON.parse(data); + } catch (e) { + console.error('Invalid JSON:', data); + data_json = { + type: 'log', + data: data, + level: 'INFO', + time: new Date().toISOString(), + } + } if (data_json.type === 'log') { - let log = data_json.data - this.log_cache.push(log); + // let log = data_json.data + this.log_cache.push(data_json); if (this.log_cache.length > this.log_cache_max_len) { this.log_cache.shift(); } diff --git a/dashboard/src/views/ChatPage.vue b/dashboard/src/views/ChatPage.vue index 346dde075..81c02374b 100644 --- a/dashboard/src/views/ChatPage.vue +++ b/dashboard/src/views/ChatPage.vue @@ -289,6 +289,16 @@ export default { message: `` } this.messages.push(bot_resp); + } else if (chunk.startsWith('[RECORD]')) { + let audio = chunk.replace('[RECORD]', ''); + let bot_resp = { + type: 'bot', + message: `` + } + this.messages.push(bot_resp); } else { let bot_resp = { type: 'bot', @@ -407,6 +417,13 @@ export default { let img = message[i].message.replace('[IMAGE]', ''); message[i].message = `` } + if (message[i].message.startsWith('[RECORD]')) { + let audio = message[i].message.replace('[RECORD]', ''); + message[i].message = `` + } if (message[i].image_url && message[i].image_url.length > 0) { for (let j = 0; j < message[i].image_url.length; j++) { message[i].image_url[j] = `/api/chat/get_file?filename=${message[i].image_url[j]}`; @@ -846,7 +863,6 @@ export default { } .audio-player { - width: 100%; height: 36px; border-radius: 18px; } diff --git a/dashboard/src/views/ConsolePage.vue b/dashboard/src/views/ConsolePage.vue index e638df47c..2ac9b3a73 100644 --- a/dashboard/src/views/ConsolePage.vue +++ b/dashboard/src/views/ConsolePage.vue @@ -44,7 +44,7 @@ import axios from 'axios'; - +