From 8ba42364025aa8d9842626b17c3bf056efee6e98 Mon Sep 17 00:00:00 2001 From: Ruochen <1051989940@qq.com> Date: Tue, 1 Jul 2025 15:41:30 +0800 Subject: [PATCH 01/54] =?UTF-8?q?refactor:=E5=B0=86=E5=89=8D=E7=AB=AF?= =?UTF-8?q?=E6=B5=8B=E8=AF=95=E4=BE=9B=E5=BA=94=E5=95=86=E9=83=A8=E5=88=86?= =?UTF-8?q?=E4=BF=AE=E6=94=B9=E4=B8=BA=E7=8B=AC=E7=AB=8B=E5=B9=B6=E5=8F=91?= =?UTF-8?q?=E5=BC=82=E6=AD=A5=E8=8E=B7=E5=8F=96=E5=90=84=E4=B8=AA=E6=96=87?= =?UTF-8?q?=E6=9C=AC=E4=BE=9B=E5=BA=94=E5=95=86=E7=9A=84=E7=8A=B6=E6=80=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- astrbot/dashboard/routes/config.py | 43 +++---- .../i18n/locales/en-US/features/provider.json | 1 + .../i18n/locales/zh-CN/features/provider.json | 1 + dashboard/src/views/ProviderPage.vue | 106 +++++++++++++++--- 4 files changed, 114 insertions(+), 37 deletions(-) diff --git a/astrbot/dashboard/routes/config.py b/astrbot/dashboard/routes/config.py index c225c762a..866509605 100644 --- a/astrbot/dashboard/routes/config.py +++ b/astrbot/dashboard/routes/config.py @@ -166,7 +166,7 @@ class ConfigRoute(Route): "/config/provider/update": ("POST", self.post_update_provider), "/config/provider/delete": ("POST", self.post_delete_provider), "/config/llmtools": ("GET", self.get_llm_tools), - "/config/provider/check_status": ("GET", self.check_all_providers_status), + "/config/provider/check_one": ("GET", self.check_one_provider_status), "/config/provider/list": ("GET", self.get_provider_config_list), "/config/provider/get_session_seperate": ( "GET", @@ -256,34 +256,39 @@ class ConfigRoute(Route): ) return status_info - async def check_all_providers_status(self): + async def check_one_provider_status(self): """ - API 接口: 检查所有 LLM Providers 的状态 + API 接口: 检查单个 LLM Provider 的状态 """ - logger.info("API call received: /config/provider/check_status") + provider_id = request.args.get("id") + if not provider_id: + return Response().error("Missing provider_id parameter", status_code=400).__dict__ + + logger.info(f"API call received: /config/provider/check_one for id: {provider_id}") + try: all_providers: typing.List = ( self.core_lifecycle.star_context.get_all_providers() ) - logger.debug(f"Found {len(all_providers)} providers to check.") + + target_provider = None + for p in all_providers: + # provider.provider_config 是 AstrBotConfig 对象,可以直接当字典用 + if p.provider_config.get("id") == provider_id: + target_provider = p + break + + if not target_provider: + logger.warning(f"Provider with id '{provider_id}' not found for status check.") + return Response().error(f"Provider with id '{provider_id}' not found", status_code=404).__dict__ - if not all_providers: - logger.info("No providers found to check.") - return Response().ok([]).__dict__ + result = await self._test_single_provider(target_provider) + return Response().ok(result).__dict__ - tasks = [self._test_single_provider(p) for p in all_providers] - logger.debug(f"Created {len(tasks)} tasks for concurrent provider checks.") - - results = await asyncio.gather(*tasks) - logger.info(f"Provider status check completed. Results: {results}") - - return Response().ok(results).__dict__ except Exception as e: - logger.error(f"Critical error in check_all_providers_status: {str(e)}") + logger.error(f"Critical error in check_one_provider_status for id {provider_id}: {str(e)}") logger.error(traceback.format_exc()) - return ( - Response().error(f"检查 Provider 状态时发生严重错误: {str(e)}").__dict__ - ) + return Response().error(f"检查 Provider 状态时发生严重错误: {str(e)}", status_code=500).__dict__ async def get_configs(self): # plugin_name 为空时返回 AstrBot 配置 diff --git a/dashboard/src/i18n/locales/en-US/features/provider.json b/dashboard/src/i18n/locales/en-US/features/provider.json index c64f23d19..5ecf890ad 100644 --- a/dashboard/src/i18n/locales/en-US/features/provider.json +++ b/dashboard/src/i18n/locales/en-US/features/provider.json @@ -29,6 +29,7 @@ "noData": "Click \"Refresh Status\" button to get service provider availability", "available": "Available", "unavailable": "Unavailable", + "pending": "Pending...", "errorMessage": "Error Message" }, "logs": { diff --git a/dashboard/src/i18n/locales/zh-CN/features/provider.json b/dashboard/src/i18n/locales/zh-CN/features/provider.json index e9bb63c6d..fdf07aefa 100644 --- a/dashboard/src/i18n/locales/zh-CN/features/provider.json +++ b/dashboard/src/i18n/locales/zh-CN/features/provider.json @@ -29,6 +29,7 @@ "noData": "点击\"刷新状态\"按钮获取服务提供商可用性", "available": "可用", "unavailable": "不可用", + "pending": "检查中...", "errorMessage": "错误信息" }, "logs": { diff --git a/dashboard/src/views/ProviderPage.vue b/dashboard/src/views/ProviderPage.vue index 44e7f8206..027c39b55 100644 --- a/dashboard/src/views/ProviderPage.vue +++ b/dashboard/src/views/ProviderPage.vue @@ -115,14 +115,23 @@ - + - - {{ status.status === 'available' ? 'mdi-check-circle' : 'mdi-alert-circle' }} - + mdi-check-circle + mdi-alert-circle + + {{ status.id }} - - {{ status.status === 'available' ? tm('availability.available') : tm('availability.unavailable') }} + + + {{ getStatusText(status.status) }} @@ -470,10 +479,16 @@ export default { sessionSeparation: this.tm('messages.success.sessionSeparation') }, error: { - sessionSeparation: this.tm('messages.error.sessionSeparation') + sessionSeparation: this.tm('messages.error.sessionSeparation'), + fetchStatus: this.tm('messages.error.fetchStatus') }, confirm: { delete: this.tm('messages.confirm.delete') + }, + status: { + available: this.tm('availability.available'), + unavailable: this.tm('availability.unavailable'), + pending: this.tm('availability.pending') } }; }, @@ -763,19 +778,58 @@ export default { }, // 获取供应商状态 - fetchProviderStatus() { + async fetchProviderStatus() { + if (this.loadingStatus) return; + this.loadingStatus = true; - axios.get('/api/config/provider/check_status').then((res) => { - if (res.data && res.data.status === 'ok') { - this.providerStatuses = res.data.data || []; - } else { - this.showError(res.data?.message || this.tm('messages.error.fetchStatus')); - } - this.loadingStatus = false; - }).catch((err) => { - this.loadingStatus = false; - this.showError(err.response?.data?.message || err.message); + + // 1. 立即初始化UI为pending状态 + this.providerStatuses = this.config_data.provider.map(p => ({ + id: p.id, + name: p.id, + status: 'pending', + error: null + })); + + // 2. 为每个provider创建一个并发的测试请求 + const promises = this.config_data.provider.map(p => { + return axios.get(`/api/config/provider/check_one?id=${p.id}`) + .then(res => { + if (res.data && res.data.status === 'ok') { + // 成功,更新对应的provider状态 + const index = this.providerStatuses.findIndex(s => s.id === p.id); + if (index !== -1) { + this.providerStatuses.splice(index, 1, res.data.data); + } + } else { + // 接口返回了业务错误 + throw new Error(res.data?.message || `Failed to check status for ${p.id}`); + } + }) + .catch(err => { + // 网络错误或业务错误 + const errorMessage = err.response?.data?.message || err.message || 'Unknown error'; + const index = this.providerStatuses.findIndex(s => s.id === p.id); + if (index !== -1) { + const failedStatus = { + ...this.providerStatuses[index], + status: 'unavailable', + error: errorMessage + }; + this.providerStatuses.splice(index, 1, failedStatus); + } + // 可以在这里选择性地向上抛出错误,以便Promise.allSettled知道 + return Promise.reject(errorMessage); + }); }); + + // 3. 等待所有请求完成(无论成功或失败) + try { + await Promise.allSettled(promises); + } finally { + // 4. 关闭全局加载状态 + this.loadingStatus = false; + } }, confirmEmptyKey() { @@ -806,6 +860,22 @@ export default { } this.showIdConflictDialog = false; }, + getStatusColor(status) { + switch (status) { + case 'available': + return 'success'; + case 'unavailable': + return 'error'; + case 'pending': + return 'grey'; + default: + return 'default'; + } + }, + + getStatusText(status) { + return this.messages.status[status] || status; + }, } } From 879b7b582ced3e886a9bd5931b43017991506986 Mon Sep 17 00:00:00 2001 From: Ruochen <1051989940@qq.com> Date: Tue, 1 Jul 2025 16:02:56 +0800 Subject: [PATCH 02/54] =?UTF-8?q?perf:=E6=8F=90=E5=8F=96=E9=87=8D=E5=A4=8D?= =?UTF-8?q?=E7=9A=84=E9=94=99=E8=AF=AF=E5=A4=84=E7=90=86=E9=80=BB=E8=BE=91?= =?UTF-8?q?,=E4=BC=98=E5=8C=96=E5=BE=AA=E7=8E=AF=E8=B0=83=E7=94=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- astrbot/dashboard/routes/config.py | 45 +++++++++++++++--------------- 1 file changed, 22 insertions(+), 23 deletions(-) diff --git a/astrbot/dashboard/routes/config.py b/astrbot/dashboard/routes/config.py index 866509605..791228de0 100644 --- a/astrbot/dashboard/routes/config.py +++ b/astrbot/dashboard/routes/config.py @@ -256,39 +256,38 @@ class ConfigRoute(Route): ) return status_info + def _error_response(self, message: str, status_code: int = 500, log_fn=logger.error): + log_fn(message) + # 记录更详细的traceback信息,但只在是严重错误时 + if status_code == 500: + log_fn(traceback.format_exc()) + return Response().error(message, status_code=status_code).__dict__ + async def check_one_provider_status(self): - """ - API 接口: 检查单个 LLM Provider 的状态 - """ + """API: check a single LLM Provider's status by id""" provider_id = request.args.get("id") if not provider_id: - return Response().error("Missing provider_id parameter", status_code=400).__dict__ - - logger.info(f"API call received: /config/provider/check_one for id: {provider_id}") + return self._error_response("Missing provider_id parameter", 400, logger.warning) + logger.info(f"API call: /config/provider/check_one id={provider_id}") try: - all_providers: typing.List = ( - self.core_lifecycle.star_context.get_all_providers() + all_providers = self.core_lifecycle.star_context.get_all_providers() + # replace manual loop with next(filter(...)) + target = next( + (p for p in all_providers if p.provider_config.get("id") == provider_id), + None ) - - target_provider = None - for p in all_providers: - # provider.provider_config 是 AstrBotConfig 对象,可以直接当字典用 - if p.provider_config.get("id") == provider_id: - target_provider = p - break - - if not target_provider: - logger.warning(f"Provider with id '{provider_id}' not found for status check.") - return Response().error(f"Provider with id '{provider_id}' not found", status_code=404).__dict__ + if not target: + return self._error_response(f"Provider with id '{provider_id}' not found", 404, logger.warning) - result = await self._test_single_provider(target_provider) + result = await self._test_single_provider(target) return Response().ok(result).__dict__ except Exception as e: - logger.error(f"Critical error in check_one_provider_status for id {provider_id}: {str(e)}") - logger.error(traceback.format_exc()) - return Response().error(f"检查 Provider 状态时发生严重错误: {str(e)}", status_code=500).__dict__ + return self._error_response( + f"Critical error checking provider {provider_id}: {e}", + 500 + ) async def get_configs(self): # plugin_name 为空时返回 AstrBot 配置 From eb8a1387135f06c606073c0f38e562b456c3f01f Mon Sep 17 00:00:00 2001 From: Soulter <905617992@qq.com> Date: Tue, 1 Jul 2025 21:00:43 +0800 Subject: [PATCH 03/54] feat: enhance conversation actions with delete functionality and improved styling --- dashboard/src/views/ChatPage.vue | 99 +++++++++++--------------------- 1 file changed, 32 insertions(+), 67 deletions(-) diff --git a/dashboard/src/views/ChatPage.vue b/dashboard/src/views/ChatPage.vue index 2954153ae..0dae041b2 100644 --- a/dashboard/src/views/ChatPage.vue +++ b/dashboard/src/views/ChatPage.vue @@ -31,7 +31,7 @@ elevation="0">
- +
@@ -49,8 +49,12 @@ }} --> @@ -65,22 +69,6 @@ -
- -
-
- -
- - mdi-delete - {{ tm('actions.deleteChat') }} - -
-
-
@@ -112,7 +100,7 @@ @@ -1631,6 +1638,7 @@ export default { border-radius: 6px; overflow-x: auto; margin: 12px 0; + position: relative; } .markdown-content code { @@ -1642,6 +1650,144 @@ export default { color: var(--v-theme-code); } +/* 代码块中的code标签样式 */ +.markdown-content pre code { + background-color: transparent; + padding: 0; + border-radius: 0; + font-family: 'Fira Code', 'Consolas', 'Monaco', 'Courier New', monospace; + font-size: 0.85em; + color: inherit; + display: block; + overflow-x: auto; + line-height: 1.5; +} + +/* 自定义代码高亮样式 */ +.markdown-content pre { + border: 1px solid var(--v-theme-border); + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); +} + +/* 确保highlight.js的样式正确应用 */ +.markdown-content pre code.hljs { + background: transparent !important; + color: inherit; +} + +/* 亮色主题下的代码高亮 */ +.v-theme--light .markdown-content pre { + background-color: #f6f8fa; +} + +/* 暗色主题下的代码块样式 */ +.v-theme--dark .markdown-content pre { + background-color: #0d1117 !important; + border-color: rgba(255, 255, 255, 0.1); +} + +.v-theme--dark .markdown-content pre code { + color: #e6edf3 !important; +} + +/* 暗色主题下的highlight.js样式覆盖 */ +.v-theme--dark .hljs { + background: #0d1117 !important; + color: #e6edf3 !important; +} + +.v-theme--dark .hljs-keyword, +.v-theme--dark .hljs-selector-tag, +.v-theme--dark .hljs-built_in, +.v-theme--dark .hljs-name, +.v-theme--dark .hljs-tag { + color: #ff7b72 !important; +} + +.v-theme--dark .hljs-string, +.v-theme--dark .hljs-title, +.v-theme--dark .hljs-section, +.v-theme--dark .hljs-attribute, +.v-theme--dark .hljs-literal, +.v-theme--dark .hljs-template-tag, +.v-theme--dark .hljs-template-variable, +.v-theme--dark .hljs-type, +.v-theme--dark .hljs-addition { + color: #a5d6ff !important; +} + +.v-theme--dark .hljs-comment, +.v-theme--dark .hljs-quote, +.v-theme--dark .hljs-deletion, +.v-theme--dark .hljs-meta { + color: #8b949e !important; +} + +.v-theme--dark .hljs-number, +.v-theme--dark .hljs-regexp, +.v-theme--dark .hljs-symbol, +.v-theme--dark .hljs-variable, +.v-theme--dark .hljs-template-variable, +.v-theme--dark .hljs-link, +.v-theme--dark .hljs-selector-attr, +.v-theme--dark .hljs-selector-pseudo { + color: #79c0ff !important; +} + +.v-theme--dark .hljs-function, +.v-theme--dark .hljs-class, +.v-theme--dark .hljs-title.class_ { + color: #d2a8ff !important; +} + +/* 复制按钮样式 */ +.copy-code-btn { + position: absolute; + top: 8px; + right: 8px; + background: rgba(255, 255, 255, 0.9); + border: 1px solid rgba(0, 0, 0, 0.1); + border-radius: 4px; + padding: 6px; + cursor: pointer; + opacity: 0; + transition: all 0.2s ease; + display: flex; + align-items: center; + justify-content: center; + color: #666; + font-size: 12px; + z-index: 10; + backdrop-filter: blur(4px); +} + +.copy-code-btn:hover { + background: rgba(255, 255, 255, 1); + color: #333; + transform: scale(1.05); + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15); +} + +.copy-code-btn:active { + transform: scale(0.95); +} + +.markdown-content pre:hover .copy-code-btn { + opacity: 1; +} + +.v-theme--dark .copy-code-btn { + background: rgba(45, 45, 45, 0.9); + border-color: rgba(255, 255, 255, 0.15); + color: #ccc; +} + +.v-theme--dark .copy-code-btn:hover { + background: rgba(45, 45, 45, 1); + color: #fff; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3); +} + .markdown-content img { max-width: 100%; border-radius: 8px; From 6a503b82c30d831b09eadd9213ec777b02988fc9 Mon Sep 17 00:00:00 2001 From: Soulter <905617992@qq.com> Date: Tue, 1 Jul 2025 22:34:17 +0800 Subject: [PATCH 05/54] refactor: web chat queue management and streamline chat route handling --- astrbot/core/__init__.py | 2 - .../process_stage/method/llm_request.py | 8 - .../sources/webchat/webchat_adapter.py | 48 +- .../platform/sources/webchat/webchat_event.py | 15 +- .../sources/webchat/webchat_queue_mgr.py | 33 ++ astrbot/dashboard/routes/chat.py | 68 +-- astrbot/dashboard/routes/multi_user_chat.py | 0 dashboard/src/views/ChatPage.vue | 411 ++++++------------ 8 files changed, 245 insertions(+), 340 deletions(-) create mode 100644 astrbot/core/platform/sources/webchat/webchat_queue_mgr.py create mode 100644 astrbot/dashboard/routes/multi_user_chat.py diff --git a/astrbot/core/__init__.py b/astrbot/core/__init__.py index 104a9edb6..16f108ece 100644 --- a/astrbot/core/__init__.py +++ b/astrbot/core/__init__.py @@ -28,5 +28,3 @@ pip_installer = PipInstaller( astrbot_config.get("pip_install_arg", ""), astrbot_config.get("pypi_index_url", None), ) -web_chat_queue = asyncio.Queue(maxsize=32) -web_chat_back_queue = asyncio.Queue(maxsize=32) diff --git a/astrbot/core/pipeline/process_stage/method/llm_request.py b/astrbot/core/pipeline/process_stage/method/llm_request.py index 2ebe4bd42..961463c7a 100644 --- a/astrbot/core/pipeline/process_stage/method/llm_request.py +++ b/astrbot/core/pipeline/process_stage/method/llm_request.py @@ -23,7 +23,6 @@ from astrbot.core.provider.entities import ( LLMResponse, ) from astrbot.core.star.star_handler import EventType -from astrbot.core import web_chat_back_queue from ..agent_runner.tool_loop_agent import ToolLoopAgent @@ -283,13 +282,6 @@ class LLMRequestSubStage(Stage): cid=cid, title=title, ) - web_chat_back_queue.put_nowait( - { - "type": "update_title", - "cid": cid, - "data": title, - } - ) async def _save_to_history( self, diff --git a/astrbot/core/platform/sources/webchat/webchat_adapter.py b/astrbot/core/platform/sources/webchat/webchat_adapter.py index fa384ed99..41d3e9418 100644 --- a/astrbot/core/platform/sources/webchat/webchat_adapter.py +++ b/astrbot/core/platform/sources/webchat/webchat_adapter.py @@ -2,7 +2,7 @@ import time import asyncio import uuid import os -from typing import Awaitable, Any +from typing import Awaitable, Any, Callable from astrbot.core.platform import ( Platform, AstrBotMessage, @@ -13,7 +13,7 @@ from astrbot.core.platform import ( from astrbot.core.message.message_event_result import MessageChain from astrbot.core.message.components import Plain, Image, Record # noqa: F403 from astrbot import logger -from astrbot.core import web_chat_queue +from .webchat_queue_mgr import webchat_queue_mgr, WebChatQueueMgr from .webchat_event import WebChatMessageEvent from astrbot.core.platform.astr_message_event import MessageSesion from ...register import register_platform_adapter @@ -21,14 +21,46 @@ from astrbot.core.utils.astrbot_path import get_astrbot_data_path class QueueListener: - def __init__(self, queue: asyncio.Queue, callback: callable) -> None: - self.queue = queue + def __init__(self, webchat_queue_mgr: WebChatQueueMgr, callback: Callable) -> None: + self.webchat_queue_mgr = webchat_queue_mgr self.callback = callback + self.running_tasks = set() + + async def listen_to_queue(self, conversation_id: str): + """Listen to a specific conversation queue""" + queue = self.webchat_queue_mgr.get_or_create_queue(conversation_id) + while True: + try: + data = await queue.get() + await self.callback(data) + except Exception as e: + logger.error( + f"Error processing message from conversation {conversation_id}: {e}" + ) + break async def run(self): + """Monitor for new conversation queues and start listeners""" + monitored_conversations = set() + while True: - data = await self.queue.get() - await self.callback(data) + # Check for new conversations + current_conversations = set(self.webchat_queue_mgr.queues.keys()) + new_conversations = current_conversations - monitored_conversations + + # Start listeners for new conversations + for conversation_id in new_conversations: + task = asyncio.create_task(self.listen_to_queue(conversation_id)) + self.running_tasks.add(task) + task.add_done_callback(self.running_tasks.discard) + monitored_conversations.add(conversation_id) + logger.debug(f"Started listener for conversation: {conversation_id}") + + # Clean up monitored conversations that no longer exist + removed_conversations = monitored_conversations - current_conversations + monitored_conversations -= removed_conversations + + await asyncio.sleep(1) # Check for new conversations every second @register_platform_adapter("webchat", "webchat") @@ -45,7 +77,7 @@ class WebChatAdapter(Platform): os.makedirs(self.imgs_dir, exist_ok=True) self.metadata = PlatformMetadata( - name="webchat", description="webchat", id=self.config.get("id") + name="webchat", description="webchat", id=self.config.get("id", "") ) async def send_by_session( @@ -105,7 +137,7 @@ class WebChatAdapter(Platform): abm = await self.convert_message(data) await self.handle_msg(abm) - bot = QueueListener(web_chat_queue, callback) + bot = QueueListener(webchat_queue_mgr, callback) return bot.run() def meta(self) -> PlatformMetadata: diff --git a/astrbot/core/platform/sources/webchat/webchat_event.py b/astrbot/core/platform/sources/webchat/webchat_event.py index 111027a5c..c4e5d63c0 100644 --- a/astrbot/core/platform/sources/webchat/webchat_event.py +++ b/astrbot/core/platform/sources/webchat/webchat_event.py @@ -5,8 +5,8 @@ from astrbot.api import logger from astrbot.api.event import AstrMessageEvent, MessageChain 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 from astrbot.core.utils.astrbot_path import get_astrbot_data_path +from .webchat_queue_mgr import webchat_queue_mgr imgs_dir = os.path.join(get_astrbot_data_path(), "webchat", "imgs") @@ -18,13 +18,14 @@ class WebChatMessageEvent(AstrMessageEvent): @staticmethod async def _send(message: MessageChain, session_id: str, streaming: bool = False): + cid = session_id.split("!")[-1] + web_chat_back_queue = webchat_queue_mgr.get_or_create_back_queue(cid) if not message: await web_chat_back_queue.put( {"type": "end", "data": "", "streaming": False} ) return "" - cid = session_id.split("!")[-1] data = "" for comp in message.chain: if isinstance(comp, Plain): @@ -98,18 +99,22 @@ class WebChatMessageEvent(AstrMessageEvent): async def send(self, message: MessageChain): await WebChatMessageEvent._send(message, session_id=self.session_id) + cid = self.session_id.split("!")[-1] + web_chat_back_queue = webchat_queue_mgr.get_or_create_back_queue(cid) await web_chat_back_queue.put( { "type": "end", "data": "", "streaming": False, - "cid": self.session_id.split("!")[-1], + "cid": cid, } ) await super().send(message) async def send_streaming(self, generator, use_fallback: bool = False): final_data = "" + cid = self.session_id.split("!")[-1] + web_chat_back_queue = webchat_queue_mgr.get_or_create_back_queue(cid) async for chain in generator: if chain.type == "break" and final_data: # 分割符 @@ -118,7 +123,7 @@ class WebChatMessageEvent(AstrMessageEvent): "type": "end", "data": final_data, "streaming": True, - "cid": self.session_id.split("!")[-1], + "cid": cid, } ) final_data = "" @@ -132,7 +137,7 @@ class WebChatMessageEvent(AstrMessageEvent): "type": "end", "data": final_data, "streaming": True, - "cid": self.session_id.split("!")[-1], + "cid": cid, } ) await super().send_streaming(generator, use_fallback) diff --git a/astrbot/core/platform/sources/webchat/webchat_queue_mgr.py b/astrbot/core/platform/sources/webchat/webchat_queue_mgr.py new file mode 100644 index 000000000..96e172212 --- /dev/null +++ b/astrbot/core/platform/sources/webchat/webchat_queue_mgr.py @@ -0,0 +1,33 @@ +import asyncio + +class WebChatQueueMgr: + def __init__(self) -> None: + self.queues = {} + """Conversation ID to asyncio.Queue mapping""" + self.back_queues = {} + """Conversation ID to asyncio.Queue mapping for responses""" + + def get_or_create_queue(self, conversation_id: str) -> asyncio.Queue: + """Get or create a queue for the given conversation ID""" + if conversation_id not in self.queues: + self.queues[conversation_id] = asyncio.Queue() + return self.queues[conversation_id] + + def get_or_create_back_queue(self, conversation_id: str) -> asyncio.Queue: + """Get or create a back queue for the given conversation ID""" + if conversation_id not in self.back_queues: + self.back_queues[conversation_id] = asyncio.Queue() + return self.back_queues[conversation_id] + + def remove_queues(self, conversation_id: str): + """Remove queues for the given conversation ID""" + if conversation_id in self.queues: + del self.queues[conversation_id] + if conversation_id in self.back_queues: + del self.back_queues[conversation_id] + + def has_queue(self, conversation_id: str) -> bool: + """Check if a queue exists for the given conversation ID""" + return conversation_id in self.queues + +webchat_queue_mgr = WebChatQueueMgr() diff --git a/astrbot/dashboard/routes/chat.py b/astrbot/dashboard/routes/chat.py index 270c92b44..a273bccdc 100644 --- a/astrbot/dashboard/routes/chat.py +++ b/astrbot/dashboard/routes/chat.py @@ -2,7 +2,7 @@ import uuid import json import os from .route import Route, Response, RouteContext -from astrbot.core import web_chat_queue, web_chat_back_queue +from astrbot.core.platform.sources.webchat.webchat_queue_mgr import webchat_queue_mgr from quart import request, Response as QuartResponse, g, make_response from astrbot.core.db import BaseDatabase import asyncio @@ -21,7 +21,6 @@ class ChatRoute(Route): super().__init__(context) self.routes = { "/chat/send": ("POST", self.chat), - "/chat/listen": ("GET", self.listener), "/chat/new_conversation": ("GET", self.new_conversation), "/chat/conversations": ("GET", self.get_conversations), "/chat/get_conversation": ("GET", self.get_conversation), @@ -40,9 +39,6 @@ class ChatRoute(Route): self.supported_imgs = ["jpg", "jpeg", "png", "gif", "webp"] - self.curr_user_cid = {} - self.curr_chat_sse = {} - async def status(self): has_llm_enabled = ( self.core_lifecycle.provider_manager.curr_provider_inst is not None @@ -133,21 +129,10 @@ class ChatRoute(Route): if not conversation_id: return Response().error("conversation_id is empty").__dict__ - self.curr_user_cid[username] = conversation_id + # Get conversation-specific queues + back_queue = webchat_queue_mgr.get_or_create_back_queue(conversation_id) - await web_chat_queue.put( - ( - username, - conversation_id, - { - "message": message, - "image_url": image_url, # list - "audio_url": audio_url, - }, - ) - ) - - # 持久化 + # append user message conversation = self.db.get_conversation_by_user_id(username, conversation_id) try: history = json.loads(conversation.history) @@ -164,30 +149,12 @@ class ChatRoute(Route): username, conversation_id, history=json.dumps(history) ) - return Response().ok().__dict__ - - async def listener(self): - """一直保持长连接""" - - username = g.get("username", "guest") - - if username in self.curr_chat_sse: - return Response().error("Already connected").__dict__ - - self.curr_chat_sse[username] = None - - heartbeat = json.dumps({"type": "heartbeat", "data": "ping"}) - async def stream(): try: - yield f"data: {heartbeat}\n\n" # 心跳包 while True: try: - result = await asyncio.wait_for( - web_chat_back_queue.get(), timeout=10 - ) # 设置超时时间为5秒 + result = await asyncio.wait_for(back_queue.get(), timeout=10) except asyncio.TimeoutError: - yield f"data: {heartbeat}\n\n" # 心跳包 continue if not result: @@ -197,9 +164,6 @@ class ChatRoute(Route): type = result.get("type") cid = result.get("cid") streaming = result.get("streaming", False) - if cid != self.curr_user_cid.get(username): - # 丢弃 - continue yield f"data: {json.dumps(result, ensure_ascii=False)}\n\n" await asyncio.sleep(0.05) @@ -210,6 +174,7 @@ class ChatRoute(Route): continue if result_text: + # append bot message conversation = self.db.get_conversation_by_user_id( username, cid ) @@ -222,11 +187,25 @@ class ChatRoute(Route): self.db.update_conversation( username, cid, history=json.dumps(history) ) + break except BaseException as _: logger.debug(f"用户 {username} 断开聊天长连接。") - self.curr_chat_sse.pop(username) return + # Put message to conversation-specific queue + chat_queue = webchat_queue_mgr.get_or_create_queue(conversation_id) + await chat_queue.put( + ( + username, + conversation_id, + { + "message": message, + "image_url": image_url, # list + "audio_url": audio_url, + }, + ) + ) + response = await make_response( stream(), { @@ -236,7 +215,6 @@ class ChatRoute(Route): "Connection": "keep-alive", }, ) - response.timeout = None return response async def delete_conversation(self): @@ -245,6 +223,8 @@ class ChatRoute(Route): if not conversation_id: return Response().error("Missing key: conversation_id").__dict__ + # Clean up queues when deleting conversation + webchat_queue_mgr.remove_queues(conversation_id) self.db.delete_conversation(username, conversation_id) return Response().ok().__dict__ @@ -279,6 +259,4 @@ class ChatRoute(Route): conversation = self.db.get_conversation_by_user_id(username, conversation_id) - self.curr_user_cid[username] = conversation_id - return Response().ok(data=conversation).__dict__ diff --git a/astrbot/dashboard/routes/multi_user_chat.py b/astrbot/dashboard/routes/multi_user_chat.py new file mode 100644 index 000000000..e69de29bb diff --git a/dashboard/src/views/ChatPage.vue b/dashboard/src/views/ChatPage.vue index 89262b0d8..8c68f083e 100644 --- a/dashboard/src/views/ChatPage.vue +++ b/dashboard/src/views/ChatPage.vue @@ -187,8 +187,7 @@ style="width: 85%; max-width: 900px; margin: 0 auto; border: 1px solid #e0e0e0; border-radius: 24px; padding: 4px;"> + style="width: 100%; resize: none; outline: none; border: 1px solid var(--v-theme-border); border-radius: 12px; padding: 12px 16px; min-height: 40px; font-family: inherit; font-size: 16px; background-color: var(--v-theme-surface);">
or /chatbox/ pattern @@ -394,7 +390,6 @@ export default { // Theme is now handled globally by the customizer store. // 设置输入框标签 this.inputFieldLabel = this.tm('input.chatPrompt'); - this.startListeningEvent(); this.checkStatus(); this.getConversations(); let inputField = document.getElementById('input-field'); @@ -420,8 +415,6 @@ export default { }, beforeUnmount() { - this.disconnectSSE(); - // 移除keyup事件监听 document.removeEventListener('keyup', this.handleInputKeyUp); @@ -529,246 +522,10 @@ export default { } }, - // 断开SSE连接 - disconnectSSE() { - if (this.eventSourceReader) { - try { - this.eventSourceReader.cancel(); - console.log('SSE Reader cancelled'); - } catch (error) { - console.warn('Error cancelling SSE reader:', error); - } - this.eventSourceReader = null; - } - if (this.eventSource) { - try { - this.eventSource.cancel(); - console.log('SSE连接已断开'); - } catch (error) { - console.warn('Error cancelling SSE:', error); - } - this.eventSource = null; - } - }, - - // 重新连接SSE - async reconnectSSE() { - if (this.sseReconnecting) { - console.log('SSE reconnection already in progress'); - return; - } - - this.sseReconnecting = true; - console.log('Reconnecting SSE...'); - this.disconnectSSE(); - - // 等待更长时间确保后端连接完全清理 - await new Promise(resolve => setTimeout(resolve, 1000)); - - this.startListeningEvent(); - }, - - async startListeningEvent() { - // 确保之前的连接已断开 - this.disconnectSSE(); - - // 如果正在重连过程中,等待一下 - if (this.sseReconnecting) { - await new Promise(resolve => setTimeout(resolve, 500)); - } - - let retryCount = 0; - const maxRetries = 3; - - while (retryCount < maxRetries) { - try { - console.log(`尝试建立SSE连接 (${retryCount + 1}/${maxRetries})`); - - const response = await fetch('/api/chat/listen', { - method: 'GET', - headers: { - 'Content-Type': 'application/json', - 'Authorization': 'Bearer ' + localStorage.getItem('token') - } - }); - - if (!response.ok) { - throw new Error(`SSE连接失败: ${response.statusText}`); - } - - const reader = response.body.getReader(); - const decoder = new TextDecoder(); - this.eventSource = reader; - this.eventSourceReader = reader; - this.sseReconnecting = false; - - let in_streaming = false; - let message_obj = null; - console.log('SSE连接已建立'); - // 显示连接成功状态 - if (retryCount > 0) { - this.showConnectionStatus(this.tm('connection.status.reconnected'), 'success'); - } - - while (true) { - try { - const { done, value } = await reader.read(); - if (done) { - console.log('SSE连接正常关闭'); - break; - } - - const chunk = decoder.decode(value, { stream: true }); - - // 可能有多行 - let lines = chunk.split('\n\n'); - - console.log('SSE数据:', lines); - - for (let i = 0; i < lines.length; i++) { - let line = lines[i].trim(); - - if (!line) { - continue; - } - - console.log(line); // 处理后端错误响应格式 - if (line.startsWith('{"status":"error"')) { - try { - const errorObj = JSON.parse(line); - if (errorObj.message === 'Already connected') { - throw new Error('CONNECTION_CONFLICT'); - } - console.error('后端错误:', errorObj.message); - continue; - } catch (parseError) { - if (parseError.message === 'CONNECTION_CONFLICT') { - throw parseError; - } - console.warn('解析错误响应失败:', line); - continue; - } - } - - // data: {"type": "plain", "data": "helloworld"} - let chunk_json; - try { - chunk_json = JSON.parse(line.replace('data: ', '')); - } catch (parseError) { - console.warn('JSON解析失败:', line, parseError); - continue; - } - - // 检查解析后的数据是否有效 - if (!chunk_json || typeof chunk_json !== 'object') { - console.warn('无效的数据对象:', chunk_json); - continue; - } - - // 检查是否有type字段 - if (!chunk_json.hasOwnProperty('type')) { - console.warn('数据缺少type字段:', chunk_json); - continue; - } - - if (chunk_json.type === 'heartbeat') { - continue; // 心跳包 - } - if (chunk_json.type === 'error') { - console.error('Error received:', chunk_json.data); - continue; - } - - if (chunk_json.type === 'image') { - let img = chunk_json.data.replace('[IMAGE]', ''); - const imageUrl = await this.getMediaFile(img); - let bot_resp = { - type: 'bot', - message: `` - } - this.messages.push(bot_resp); - } else if (chunk_json.type === 'record') { - let audio = chunk_json.data.replace('[RECORD]', ''); - const audioUrl = await this.getMediaFile(audio); - let bot_resp = { - type: 'bot', - message: `` - } - this.messages.push(bot_resp); - } else if (chunk_json.type === 'plain') { - if (!in_streaming) { - message_obj = { - type: 'bot', - message: this.ref(chunk_json.data), - } - this.messages.push(message_obj); - in_streaming = true; - } else { - message_obj.message.value += chunk_json.data; - } - } else if (chunk_json.type === 'end') { - in_streaming = false; - // 在消息流结束后初始化代码复制按钮 - this.initCodeCopyButtons(); - continue; - } else if (chunk_json.type === 'update_title') { - // 更新对话标题 - const conversation = this.conversations.find(c => c.cid === chunk_json.cid); - if (conversation) { - conversation.title = chunk_json.data; - } - } else { - console.warn('未知数据类型:', chunk_json.type); - } - this.scrollToBottom(); - } - } catch (readError) { - if (readError.name === 'AbortError') { - console.log('SSE连接被取消'); - break; - } - if (readError.message === 'CONNECTION_CONFLICT') { - throw readError; - } - console.error('SSE读取错误:', readError); - break; - } - } - - // 如果成功连接并正常结束,跳出重试循环 - break; - - } catch (error) { - console.error(`SSE连接错误 (尝试 ${retryCount + 1}):`, error); - - retryCount++; - if (error.message === 'CONNECTION_CONFLICT' && retryCount < maxRetries) { - console.log(`连接冲突,等待 ${2000 * retryCount}ms 后重试...`); - this.showConnectionStatus(`${this.tm('connection.status.reconnecting')} (${retryCount}/${maxRetries})`, 'warning'); - await new Promise(resolve => setTimeout(resolve, 2000 * retryCount)); - continue; - } - - if (retryCount >= maxRetries) { - console.error('SSE连接重试次数已达上限'); - this.showConnectionStatus(this.tm('connection.status.failed'), 'error'); - this.sseReconnecting = false; - break; - } - - // 等待一段时间后重试 - await new Promise(resolve => setTimeout(resolve, 1000 * retryCount)); - } finally { - this.eventSource = null; - this.eventSourceReader = null; - } - } - - this.sseReconnecting = false; + showConnectionStatus(message, type) { + // You can implement a toast notification here or update UI status + console.log(`Connection status: ${message} (${type})`); }, removeAudio() { @@ -920,7 +677,6 @@ export default { } } this.messages = message; - // 初始化代码复制按钮 this.initCodeCopyButtons(); }).catch(err => { console.error(err); @@ -1032,33 +788,144 @@ export default { this.messages.push(userMessage); this.scrollToBottom(); - this.loadingChat = true; + this.loadingChat = true - fetch('/api/chat/send', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - 'Authorization': 'Bearer ' + localStorage.getItem('token') - }, - body: JSON.stringify({ - message: this.prompt.trim(), // 确保发送的消息已去除前后空格 - conversation_id: this.currCid, - image_url: this.stagedImagesName, - audio_url: this.stagedAudioUrl ? [this.stagedAudioUrl] : [] - }) - }) - .then(response => { - this.prompt = ''; - this.stagedImagesName = []; - this.stagedImagesUrl = []; - this.stagedAudioUrl = ""; - this.loadingChat = false; - }) - .catch(err => { - console.error(err); - this.loadingChat = false; + try { + const response = await fetch('/api/chat/send', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Authorization': 'Bearer ' + localStorage.getItem('token') + }, + body: JSON.stringify({ + message: this.prompt.trim(), // 确保发送的消息已去除前后空格 + conversation_id: this.currCid, + image_url: this.stagedImagesName, + audio_url: this.stagedAudioUrl ? [this.stagedAudioUrl] : [] + }) }); + + this.prompt = ''; // 清空输入框; + + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + + const reader = response.body.getReader(); + const decoder = new TextDecoder(); + let in_streaming = false; + let message_obj = null; + + while (true) { + try { + const { done, value } = await reader.read(); + if (done) { + console.log('SSE stream completed'); + break; + } + + const chunk = decoder.decode(value, { stream: true }); + const lines = chunk.split('\n\n'); + + for (let i = 0; i < lines.length; i++) { + let line = lines[i].trim(); + + if (!line) { + continue; + } + + // Parse SSE data + let chunk_json; + try { + chunk_json = JSON.parse(line.replace('data: ', '')); + } catch (parseError) { + console.warn('JSON解析失败:', line, parseError); + continue; + } + + // 检查解析后的数据是否有效 + if (!chunk_json || typeof chunk_json !== 'object' || !chunk_json.hasOwnProperty('type')) { + console.warn('无效的数据对象:', chunk_json); + continue; + } + + if (chunk_json.type === 'heartbeat') { + continue; // 心跳包 + } + if (chunk_json.type === 'error') { + console.error('Error received:', chunk_json.data); + continue; + } + + if (chunk_json.type === 'image') { + let img = chunk_json.data.replace('[IMAGE]', ''); + const imageUrl = await this.getMediaFile(img); + let bot_resp = { + type: 'bot', + message: `` + } + this.messages.push(bot_resp); + } else if (chunk_json.type === 'record') { + let audio = chunk_json.data.replace('[RECORD]', ''); + const audioUrl = await this.getMediaFile(audio); + let bot_resp = { + type: 'bot', + message: `` + } + this.messages.push(bot_resp); + } else if (chunk_json.type === 'plain') { + if (!in_streaming) { + message_obj = { + type: 'bot', + message: this.ref(chunk_json.data), + } + this.messages.push(message_obj); + in_streaming = true; + } else { + message_obj.message.value += chunk_json.data; + } + } else if (chunk_json.type === 'end') { + in_streaming = false; + // 在消息流结束后初始化代码复制按钮 + this.initCodeCopyButtons(); + continue; + } else if (chunk_json.type === 'update_title') { + // 更新对话标题 + const conversation = this.conversations.find(c => c.cid === chunk_json.cid); + if (conversation) { + conversation.title = chunk_json.data; + } + } else { + console.warn('未知数据类型:', chunk_json.type); + } + this.scrollToBottom(); + } + } catch (readError) { + console.error('SSE读取错误:', readError); + break; + } + } + + // Clear input after successful send + this.prompt = ''; + this.stagedImagesName = []; + this.stagedImagesUrl = []; + this.stagedAudioUrl = ""; + this.loadingChat = false; + + // get the latest conversations + this.getConversations(); + + } catch (err) { + console.error('发送消息失败:', err); + this.loadingChat = false; + this.showConnectionStatus(this.tm('connection.status.failed'), 'error'); + } }, + scrollToBottom() { this.$nextTick(() => { const container = this.$refs.messageContainer; From adb0cbc5ddeff6d4ebd6bb28aaa07401cb435fd5 Mon Sep 17 00:00:00 2001 From: Soulter <905617992@qq.com> Date: Wed, 2 Jul 2025 10:16:44 +0800 Subject: [PATCH 06/54] fix: handle tool_calls_result as list or single object in context query in streaming mode --- .../core/provider/sources/anthropic_source.py | 6 ++- .../core/provider/sources/gemini_source.py | 42 +++++++++++-------- 2 files changed, 30 insertions(+), 18 deletions(-) diff --git a/astrbot/core/provider/sources/anthropic_source.py b/astrbot/core/provider/sources/anthropic_source.py index a53250fb7..4ea4c2e02 100644 --- a/astrbot/core/provider/sources/anthropic_source.py +++ b/astrbot/core/provider/sources/anthropic_source.py @@ -300,7 +300,11 @@ class ProviderAnthropic(Provider): # tool calls result if tool_calls_result: - context_query.extend(tool_calls_result.to_openai_messages()) + if not isinstance(tool_calls_result, list): + context_query.extend(tool_calls_result.to_openai_messages()) + else: + for tcr in tool_calls_result: + context_query.extend(tcr.to_openai_messages()) system_prompt, new_messages = self._prepare_payload(context_query) diff --git a/astrbot/core/provider/sources/gemini_source.py b/astrbot/core/provider/sources/gemini_source.py index e1d1f11bd..d67dd2a94 100644 --- a/astrbot/core/provider/sources/gemini_source.py +++ b/astrbot/core/provider/sources/gemini_source.py @@ -14,7 +14,7 @@ import astrbot.core.message.components as Comp from astrbot import logger from astrbot.api.provider import Provider from astrbot.core.message.message_event_result import MessageChain -from astrbot.core.provider.entities import LLMResponse, ToolCallsResult +from astrbot.core.provider.entities import LLMResponse from astrbot.core.provider.func_tool_manager import FuncCall from astrbot.core.utils.io import download_image_by_url @@ -259,10 +259,12 @@ class ProviderGoogleGenAI(Provider): contents.append(content_cls(parts=part)) gemini_contents: list[types.Content] = [] - native_tool_enabled = any([ - self.provider_config.get("gm_native_coderunner", False), - self.provider_config.get("gm_native_search", False), - ]) + native_tool_enabled = any( + [ + self.provider_config.get("gm_native_coderunner", False), + self.provider_config.get("gm_native_search", False), + ] + ) for message in payloads["messages"]: role, content = message["role"], message.get("content") @@ -544,13 +546,13 @@ class ProviderGoogleGenAI(Provider): async def text_chat_stream( self, - prompt: str, - session_id: str = None, - image_urls: list[str] = None, - func_tool: FuncCall = None, - contexts: str = None, - system_prompt: str = None, - tool_calls_result: ToolCallsResult = None, + prompt, + session_id=None, + image_urls=None, + func_tool=None, + contexts=None, + system_prompt=None, + tool_calls_result=None, **kwargs, ) -> AsyncGenerator[LLMResponse, None]: if contexts is None: @@ -566,7 +568,11 @@ class ProviderGoogleGenAI(Provider): # tool calls result if tool_calls_result: - context_query.extend(tool_calls_result.to_openai_messages()) + if not isinstance(tool_calls_result, list): + context_query.extend(tool_calls_result.to_openai_messages()) + else: + for tcr in tool_calls_result: + context_query.extend(tcr.to_openai_messages()) model_config = self.provider_config.get("model_config", {}) model_config["model"] = self.get_model() @@ -628,10 +634,12 @@ class ProviderGoogleGenAI(Provider): if not image_data: logger.warning(f"图片 {image_url} 得到的结果为空,将忽略。") continue - user_content["content"].append({ - "type": "image_url", - "image_url": {"url": image_data}, - }) + user_content["content"].append( + { + "type": "image_url", + "image_url": {"url": image_data}, + } + ) return user_content else: return {"role": "user", "content": text} From 72702beb0bb6f01a37d832d60267c59c11b44222 Mon Sep 17 00:00:00 2001 From: Raven95676 Date: Wed, 2 Jul 2025 10:29:10 +0800 Subject: [PATCH 07/54] chore: clean code --- .../core/provider/sources/azure_tts_source.py | 48 ++++++++++------- astrbot/core/provider/sources/dify_source.py | 16 +++--- .../core/provider/sources/gemini_source.py | 20 +++---- .../core/provider/sources/openai_source.py | 9 ++-- .../core/provider/sources/volcengine_tts.py | 53 ++++++++++--------- 5 files changed, 76 insertions(+), 70 deletions(-) diff --git a/astrbot/core/provider/sources/azure_tts_source.py b/astrbot/core/provider/sources/azure_tts_source.py index c35c7ec6c..6ddf452d4 100644 --- a/astrbot/core/provider/sources/azure_tts_source.py +++ b/astrbot/core/provider/sources/azure_tts_source.py @@ -19,6 +19,7 @@ from ..register import register_provider_adapter TEMP_DIR = Path("data/temp/azure_tts") TEMP_DIR.mkdir(parents=True, exist_ok=True) + class OTTSProvider: def __init__(self, config: Dict): self.skey = config["OTTS_SKEY"] @@ -70,12 +71,12 @@ class OTTSProvider: "style": voice_params["style"], "role": voice_params["role"], "rate": voice_params["rate"], - "volume": voice_params["volume"] + "volume": voice_params["volume"], }, headers={ "User-Agent": f"AstrBot/{VERSION}", - "UAK": "AstrBot/AzureTTS" - } + "UAK": "AstrBot/AzureTTS", + }, ) response.raise_for_status() file_path.parent.mkdir(parents=True, exist_ok=True) @@ -88,14 +89,19 @@ class OTTSProvider: raise RuntimeError(f"OTTS请求失败: {str(e)}") from e await asyncio.sleep(0.5 * (attempt + 1)) + class AzureNativeProvider(TTSProvider): def __init__(self, provider_config: dict, provider_settings: dict): super().__init__(provider_config, provider_settings) - self.subscription_key = provider_config.get("azure_tts_subscription_key", "").strip() + self.subscription_key = provider_config.get( + "azure_tts_subscription_key", "" + ).strip() if not re.fullmatch(r"^[a-zA-Z0-9]{32}$", self.subscription_key): raise ValueError("无效的Azure订阅密钥") self.region = provider_config.get("azure_tts_region", "eastus").strip() - self.endpoint = f"https://{self.region}.tts.speech.microsoft.com/cognitiveservices/v1" + self.endpoint = ( + f"https://{self.region}.tts.speech.microsoft.com/cognitiveservices/v1" + ) self.client = None self.token = None self.token_expire = 0 @@ -104,15 +110,17 @@ class AzureNativeProvider(TTSProvider): "style": provider_config.get("azure_tts_style", "cheerful"), "role": provider_config.get("azure_tts_role", "Boy"), "rate": provider_config.get("azure_tts_rate", "1"), - "volume": provider_config.get("azure_tts_volume", "100") + "volume": provider_config.get("azure_tts_volume", "100"), } async def __aenter__(self): - self.client = AsyncClient(headers={ - "User-Agent": f"AstrBot/{VERSION}", - "Content-Type": "application/ssml+xml", - "X-Microsoft-OutputFormat": "riff-48khz-16bit-mono-pcm" - }) + self.client = AsyncClient( + headers={ + "User-Agent": f"AstrBot/{VERSION}", + "Content-Type": "application/ssml+xml", + "X-Microsoft-OutputFormat": "riff-48khz-16bit-mono-pcm", + } + ) return self async def __aexit__(self, exc_type, exc_val, exc_tb): @@ -120,10 +128,11 @@ class AzureNativeProvider(TTSProvider): await self.client.aclose() async def _refresh_token(self): - token_url = f"https://{self.region}.api.cognitive.microsoft.com/sts/v1.0/issuetoken" + token_url = ( + f"https://{self.region}.api.cognitive.microsoft.com/sts/v1.0/issuetoken" + ) response = await self.client.post( - token_url, - headers={"Ocp-Apim-Subscription-Key": self.subscription_key} + token_url, headers={"Ocp-Apim-Subscription-Key": self.subscription_key} ) response.raise_for_status() self.token = response.text @@ -150,8 +159,8 @@ class AzureNativeProvider(TTSProvider): content=ssml, headers={ "Authorization": f"Bearer {self.token}", - "User-Agent": f"AstrBot/{VERSION}" - } + "User-Agent": f"AstrBot/{VERSION}", + }, ) response.raise_for_status() file_path.parent.mkdir(parents=True, exist_ok=True) @@ -160,6 +169,7 @@ class AzureNativeProvider(TTSProvider): f.write(chunk) return str(file_path.resolve()) + @register_provider_adapter("azure_tts", "Azure TTS", ProviderType.TEXT_TO_SPEECH) class AzureTTSProvider(TTSProvider): def __init__(self, provider_config: dict, provider_settings: dict): @@ -183,7 +193,7 @@ class AzureTTSProvider(TTSProvider): error_msg = ( f"JSON解析失败,请检查格式(错误位置:行 {e.lineno} 列 {e.colno})\n" f"错误详情: {e.msg}\n" - f"错误上下文: {json_str[max(0, e.pos-30):e.pos+30]}" + f"错误上下文: {json_str[max(0, e.pos - 30) : e.pos + 30]}" ) raise ValueError(error_msg) from e except KeyError as e: @@ -202,8 +212,8 @@ class AzureTTSProvider(TTSProvider): "style": self.provider_config.get("azure_tts_style"), "role": self.provider_config.get("azure_tts_role"), "rate": self.provider_config.get("azure_tts_rate"), - "volume": self.provider_config.get("azure_tts_volume") - } + "volume": self.provider_config.get("azure_tts_volume"), + }, ) else: async with self.provider as provider: diff --git a/astrbot/core/provider/sources/dify_source.py b/astrbot/core/provider/sources/dify_source.py index 81c910d66..b3a0ccccf 100644 --- a/astrbot/core/provider/sources/dify_source.py +++ b/astrbot/core/provider/sources/dify_source.py @@ -18,7 +18,7 @@ class ProviderDify(Provider): self, provider_config, provider_settings, - default_persona = None, + default_persona=None, ) -> None: super().__init__( provider_config, @@ -65,7 +65,7 @@ class ProviderDify(Provider): if image_urls is None: image_urls = [] result = "" - session_id = session_id or kwargs.get("user") # 1734 + session_id = session_id or kwargs.get("user") # 1734 conversation_id = self.conversation_ids.get(session_id, "") files_payload = [] @@ -84,13 +84,11 @@ class ProviderDify(Provider): f"上传图片后得到未知的 Dify 响应:{file_response},图片将忽略。" ) continue - files_payload.append( - { - "type": "image", - "transfer_method": "local_file", - "upload_file_id": file_response["id"], - } - ) + files_payload.append({ + "type": "image", + "transfer_method": "local_file", + "upload_file_id": file_response["id"], + }) # 获得会话变量 payload_vars = self.variables.copy() diff --git a/astrbot/core/provider/sources/gemini_source.py b/astrbot/core/provider/sources/gemini_source.py index d67dd2a94..573fe7684 100644 --- a/astrbot/core/provider/sources/gemini_source.py +++ b/astrbot/core/provider/sources/gemini_source.py @@ -259,12 +259,10 @@ class ProviderGoogleGenAI(Provider): contents.append(content_cls(parts=part)) gemini_contents: list[types.Content] = [] - native_tool_enabled = any( - [ - self.provider_config.get("gm_native_coderunner", False), - self.provider_config.get("gm_native_search", False), - ] - ) + native_tool_enabled = any([ + self.provider_config.get("gm_native_coderunner", False), + self.provider_config.get("gm_native_search", False), + ]) for message in payloads["messages"]: role, content = message["role"], message.get("content") @@ -634,12 +632,10 @@ class ProviderGoogleGenAI(Provider): if not image_data: logger.warning(f"图片 {image_url} 得到的结果为空,将忽略。") continue - user_content["content"].append( - { - "type": "image_url", - "image_url": {"url": image_data}, - } - ) + user_content["content"].append({ + "type": "image_url", + "image_url": {"url": image_data}, + }) return user_content else: return {"role": "user", "content": text} diff --git a/astrbot/core/provider/sources/openai_source.py b/astrbot/core/provider/sources/openai_source.py index ef6131d8c..936fc2e34 100644 --- a/astrbot/core/provider/sources/openai_source.py +++ b/astrbot/core/provider/sources/openai_source.py @@ -30,7 +30,7 @@ class ProviderOpenAIOfficial(Provider): self, provider_config, provider_settings, - default_persona = None, + default_persona=None, ) -> None: super().__init__( provider_config, @@ -525,9 +525,10 @@ class ProviderOpenAIOfficial(Provider): if not image_data: logger.warning(f"图片 {image_url} 得到的结果为空,将忽略。") continue - user_content["content"].append( - {"type": "image_url", "image_url": {"url": image_data}} - ) + user_content["content"].append({ + "type": "image_url", + "image_url": {"url": image_data}, + }) return user_content else: return {"role": "user", "content": text} diff --git a/astrbot/core/provider/sources/volcengine_tts.py b/astrbot/core/provider/sources/volcengine_tts.py index dca0196b1..12e7ed9cd 100644 --- a/astrbot/core/provider/sources/volcengine_tts.py +++ b/astrbot/core/provider/sources/volcengine_tts.py @@ -5,12 +5,12 @@ import os import traceback import asyncio import aiohttp -import requests from ..provider import TTSProvider from ..entities import ProviderType from ..register import register_provider_adapter from astrbot import logger + @register_provider_adapter( "volcengine_tts", "火山引擎 TTS", provider_type=ProviderType.TEXT_TO_SPEECH ) @@ -22,7 +22,9 @@ class ProviderVolcengineTTS(TTSProvider): self.cluster = provider_config.get("volcengine_cluster", "") self.voice_type = provider_config.get("volcengine_voice_type", "") self.speed_ratio = provider_config.get("volcengine_speed_ratio", 1.0) - self.api_base = provider_config.get("api_base", f"https://openspeech.bytedance.com/api/v1/tts") + self.api_base = provider_config.get( + "api_base", "https://openspeech.bytedance.com/api/v1/tts" + ) self.timeout = provider_config.get("timeout", 20) def _build_request_payload(self, text: str) -> dict: @@ -30,11 +32,9 @@ class ProviderVolcengineTTS(TTSProvider): "app": { "appid": self.appid, "token": self.api_key, - "cluster": self.cluster - }, - "user": { - "uid": str(uuid.uuid4()) + "cluster": self.cluster, }, + "user": {"uid": str(uuid.uuid4())}, "audio": { "voice_type": self.voice_type, "encoding": "mp3", @@ -48,60 +48,61 @@ class ProviderVolcengineTTS(TTSProvider): "text_type": "plain", "operation": "query", "with_frontend": 1, - "frontend_type": "unitTson" - } + "frontend_type": "unitTson", + }, } async def get_audio(self, text: str) -> str: """异步方法获取语音文件路径""" headers = { "Content-Type": "application/json", - "Authorization": f"Bearer; {self.api_key}" + "Authorization": f"Bearer; {self.api_key}", } - + payload = self._build_request_payload(text) - + logger.debug(f"请求头: {headers}") logger.debug(f"请求 URL: {self.api_base}") logger.debug(f"请求体: {json.dumps(payload, ensure_ascii=False)[:100]}...") - + try: async with aiohttp.ClientSession() as session: async with session.post( self.api_base, - data=json.dumps(payload), + data=json.dumps(payload), headers=headers, - timeout=self.timeout + timeout=self.timeout, ) as response: logger.debug(f"响应状态码: {response.status}") - + response_text = await response.text() logger.debug(f"响应内容: {response_text[:200]}...") - + if response.status == 200: resp_data = json.loads(response_text) - + if "data" in resp_data: audio_data = base64.b64decode(resp_data["data"]) - + os.makedirs("data/temp", exist_ok=True) - + file_path = f"data/temp/volcengine_tts_{uuid.uuid4()}.mp3" - + loop = asyncio.get_running_loop() await loop.run_in_executor( - None, - lambda: open(file_path, "wb").write(audio_data) + None, lambda: open(file_path, "wb").write(audio_data) ) - + return file_path else: error_msg = resp_data.get("message", "未知错误") raise Exception(f"火山引擎 TTS API 返回错误: {error_msg}") else: - raise Exception(f"火山引擎 TTS API 请求失败: {response.status}, {response_text}") - + raise Exception( + f"火山引擎 TTS API 请求失败: {response.status}, {response_text}" + ) + except Exception as e: error_details = traceback.format_exc() logger.debug(f"火山引擎 TTS 异常详情: {error_details}") - raise Exception(f"火山引擎 TTS 异常: {str(e)}") \ No newline at end of file + raise Exception(f"火山引擎 TTS 异常: {str(e)}") From 1f2de3d3d8d8fd4aefb6cd026faf8e96f2072af4 Mon Sep 17 00:00:00 2001 From: Raven95676 Date: Wed, 2 Jul 2025 10:43:54 +0800 Subject: [PATCH 08/54] =?UTF-8?q?perf:=20=E4=BC=98=E5=8C=96WebChat?= =?UTF-8?q?=E5=AF=B9=E8=AF=9D=E6=A0=87=E9=A2=98=E7=94=9F=E6=88=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- astrbot/core/pipeline/process_stage/method/llm_request.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/astrbot/core/pipeline/process_stage/method/llm_request.py b/astrbot/core/pipeline/process_stage/method/llm_request.py index 2ebe4bd42..ceb1caf0a 100644 --- a/astrbot/core/pipeline/process_stage/method/llm_request.py +++ b/astrbot/core/pipeline/process_stage/method/llm_request.py @@ -259,7 +259,7 @@ class LLMRequestSubStage(Stage): f"{cleaned_text}\n" "Only output the summary within 10 words, DO NOT INCLUDE any other text." "You must use the same language as the user." - "If you think the dialog is too short to summarize, only output a special mark: `None`" + "If you think the dialog is too short to summarize, only output a special mark: ``" ), ) if llm_resp and llm_resp.completion_text: @@ -267,7 +267,7 @@ class LLMRequestSubStage(Stage): f"WebChat 对话标题生成响应: {llm_resp.completion_text.strip()}" ) title = llm_resp.completion_text.strip() - if not title or "None" == title: + if not title or "" in title: return await self.conv_manager.update_conversation_title( event.unified_msg_origin, title=title From f3ad53e949926b2d1ebd9de43eb18fb3f2c4a9cb Mon Sep 17 00:00:00 2001 From: Soulter <905617992@qq.com> Date: Wed, 2 Jul 2025 17:12:30 +0800 Subject: [PATCH 09/54] feat: add supports for selecting provider and models in webchat --- astrbot/dashboard/routes/config.py | 24 ++ astrbot/dashboard/routes/multi_user_chat.py | 0 dashboard/src/views/ChatPage.vue | 314 +++++++++++++++++++- 3 files changed, 324 insertions(+), 14 deletions(-) delete mode 100644 astrbot/dashboard/routes/multi_user_chat.py diff --git a/astrbot/dashboard/routes/config.py b/astrbot/dashboard/routes/config.py index c225c762a..b55f0b21c 100644 --- a/astrbot/dashboard/routes/config.py +++ b/astrbot/dashboard/routes/config.py @@ -9,6 +9,7 @@ from astrbot.core.platform.register import platform_registry from astrbot.core.provider.register import provider_registry from astrbot.core.star.star import star_registry from astrbot.core import logger +from astrbot.core.provider import Provider import asyncio @@ -168,6 +169,7 @@ class ConfigRoute(Route): "/config/llmtools": ("GET", self.get_llm_tools), "/config/provider/check_status": ("GET", self.check_all_providers_status), "/config/provider/list": ("GET", self.get_provider_config_list), + "/config/provider/model_list": ("GET", self.get_provider_model_list), "/config/provider/get_session_seperate": ( "GET", lambda: Response() @@ -319,6 +321,28 @@ class ConfigRoute(Route): provider_list.append(provider) return Response().ok(provider_list).__dict__ + async def get_provider_model_list(self): + """获取指定提供商的模型列表""" + provider_id = request.args.get("provider_id", None) + if not provider_id: + return Response().error("缺少参数 provider_id").__dict__ + + prov_mgr = self.core_lifecycle.provider_manager + provider: Provider | None = prov_mgr.inst_map.get(provider_id, None) + if not provider: + return Response().error(f"未找到 ID 为 {provider_id} 的提供商").__dict__ + + try: + models = await provider.get_models() + ret = { + "models": models, + "provider_id": provider_id, + } + return Response().ok(ret).__dict__ + except Exception as e: + logger.error(traceback.format_exc()) + return Response().error(str(e)).__dict__ + async def post_astrbot_configs(self): post_configs = await request.json try: diff --git a/astrbot/dashboard/routes/multi_user_chat.py b/astrbot/dashboard/routes/multi_user_chat.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/dashboard/src/views/ChatPage.vue b/dashboard/src/views/ChatPage.vue index 8c68f083e..1d849c8cb 100644 --- a/dashboard/src/views/ChatPage.vue +++ b/dashboard/src/views/ChatPage.vue @@ -50,10 +50,12 @@ @@ -78,7 +80,7 @@

{{ getCurrentConversation.title || tm('conversation.newConversation') - }}

+ }}
{{ formatDate(getCurrentConversation.updated_at) }}
@@ -100,8 +102,8 @@ @@ -188,14 +190,29 @@ -
- - +
+
+ + + + {{ selectedProviderId }} / {{ selectedModelName }} + + + 选择模型 + + +
+
+ + +
+
@@ -236,6 +253,89 @@ + + + + + + 选择提供商和模型 + + +
+ +
+
+

提供商

+
+ + + {{ provider.id }} + {{ provider.api_base }} + + +
+ +
暂无可用提供商
+
+
+ + +
+
+

模型

+ + +
+ + + {{ model }} + {{ model.description }} + + +
+ +
请先选择提供商
+
+
+ +
该提供商暂无可用模型
+
+
+
+
+ + + 取消 + + 确认选择 + + +
+
@@ -1709,4 +1917,82 @@ export default { flex-shrink: 0; /* 防止header被压缩 */ } + +/* 提供商和模型选择对话框样式 */ +.provider-model-container { + display: flex; + height: 500px; + border: 1px solid var(--v-theme-border); + border-radius: 8px; + overflow: hidden; +} + +.provider-list-panel, +.model-list-panel { + flex: 1; + display: flex; + flex-direction: column; + background-color: var(--v-theme-surface); +} + +.provider-list-panel { + border-right: 1px solid var(--v-theme-border); +} + +.panel-header { + display: flex; + align-items: center; + justify-content: space-between; + padding: 16px; + border-bottom: 1px solid var(--v-theme-border); + background-color: var(--v-theme-containerBg); +} + +.panel-header h4 { + margin: 0; + font-size: 16px; + font-weight: 500; + color: var(--v-theme-primaryText); +} + +.provider-list, +.model-list { + flex: 1; + overflow-y: auto; + padding: 8px; +} + +.provider-item, +.model-item { + margin-bottom: 4px; + border-radius: 8px !important; + transition: all 0.2s ease; + cursor: pointer; +} + +.provider-item:hover, +.model-item:hover { + background-color: rgba(103, 58, 183, 0.05); +} + +.provider-item.v-list-item--active, +.model-item.v-list-item--active { + background-color: rgba(103, 58, 183, 0.1); + color: var(--v-theme-secondary); +} + +.empty-state { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + height: 200px; + opacity: 0.6; + gap: 12px; +} + +.empty-text { + font-size: 14px; + color: var(--v-theme-secondaryText); +} \ No newline at end of file From ab677ea100981f65a8d102ea3b59f133fef28193 Mon Sep 17 00:00:00 2001 From: chenpeng <115522593@qq.com> Date: Wed, 2 Jul 2025 17:30:37 +0800 Subject: [PATCH 10/54] =?UTF-8?q?=E4=BF=AE=E6=AD=A3pilk=E4=BE=9D=E8=B5=96?= =?UTF-8?q?=E6=8F=90=E7=A4=BA=E6=96=87=E6=A1=88=20=E5=A2=9E=E5=8A=A0?= =?UTF-8?q?=E7=9B=91=E5=90=ACwechatpadpro=E6=B6=88=E6=81=AF=E5=B9=B3?= =?UTF-8?q?=E5=8F=B0=E7=9A=84=E4=BA=8B=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- astrbot/core/star/filter/platform_adapter_type.py | 4 +++- astrbot/core/utils/tencent_record_helper.py | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/astrbot/core/star/filter/platform_adapter_type.py b/astrbot/core/star/filter/platform_adapter_type.py index 0926cc337..4252aa1af 100644 --- a/astrbot/core/star/filter/platform_adapter_type.py +++ b/astrbot/core/star/filter/platform_adapter_type.py @@ -13,7 +13,8 @@ class PlatformAdapterType(enum.Flag): TELEGRAM = enum.auto() WECOM = enum.auto() LARK = enum.auto() - ALL = AIOCQHTTP | QQOFFICIAL | VCHAT | GEWECHAT | TELEGRAM | WECOM | LARK + WECHATPADPRO = enum.auto() + ALL = AIOCQHTTP | QQOFFICIAL | VCHAT | GEWECHAT | TELEGRAM | WECOM | LARK | WECHATPADPRO ADAPTER_NAME_2_TYPE = { @@ -24,6 +25,7 @@ ADAPTER_NAME_2_TYPE = { "telegram": PlatformAdapterType.TELEGRAM, "wecom": PlatformAdapterType.WECOM, "lark": PlatformAdapterType.LARK, + "wechatpadpro": PlatformAdapterType.WECHATPADPRO, } diff --git a/astrbot/core/utils/tencent_record_helper.py b/astrbot/core/utils/tencent_record_helper.py index 9d0552c1e..2c97a01ed 100644 --- a/astrbot/core/utils/tencent_record_helper.py +++ b/astrbot/core/utils/tencent_record_helper.py @@ -117,7 +117,7 @@ async def audio_to_tencent_silk_base64(audio_path: str) -> tuple[str, float]: try: import pilk except ImportError as e: - raise Exception("未安装 pysilk,请执行: pip install pysilk") from e + raise Exception("未安装 pilk: pip install pilk") from e temp_dir = os.path.join(get_astrbot_data_path(), "temp") os.makedirs(temp_dir, exist_ok=True) From f234a5bcc2d67decc97a417009f8c21e808ef6a5 Mon Sep 17 00:00:00 2001 From: Soulter <905617992@qq.com> Date: Thu, 3 Jul 2025 00:23:56 +0800 Subject: [PATCH 11/54] fix: enhance event hook handling to return status and prevent propagation --- astrbot/core/pipeline/context.py | 10 ++++++++-- .../process_stage/agent_runner/tool_loop_agent.py | 5 +++-- .../core/pipeline/process_stage/method/llm_request.py | 5 ++++- 3 files changed, 15 insertions(+), 5 deletions(-) diff --git a/astrbot/core/pipeline/context.py b/astrbot/core/pipeline/context.py index d98f7c341..0b9d9e533 100644 --- a/astrbot/core/pipeline/context.py +++ b/astrbot/core/pipeline/context.py @@ -23,7 +23,12 @@ class PipelineContext: event: AstrMessageEvent, hook_type: EventType, *args, - ): + ) -> bool: + """调用事件钩子函数 + + Returns: + bool: 如果事件被终止,返回 True + """ platform_id = event.get_platform_id() handlers = star_handlers_registry.get_handlers_by_event_type( hook_type, platform_id=platform_id @@ -41,7 +46,8 @@ class PipelineContext: logger.info( f"{star_map[handler.handler_module_path].name} - {handler.handler_name} 终止了事件传播。" ) - return + + return event.is_stopped() async def call_handler( self, diff --git a/astrbot/core/pipeline/process_stage/agent_runner/tool_loop_agent.py b/astrbot/core/pipeline/process_stage/agent_runner/tool_loop_agent.py index 3163e02e4..dcb390b2f 100644 --- a/astrbot/core/pipeline/process_stage/agent_runner/tool_loop_agent.py +++ b/astrbot/core/pipeline/process_stage/agent_runner/tool_loop_agent.py @@ -127,9 +127,10 @@ class ToolLoopAgent(BaseAgentRunner): self._transition_state(AgentState.DONE) # 执行事件钩子 - await self.pipeline_ctx.call_event_hook( + if await self.pipeline_ctx.call_event_hook( self.event, EventType.OnLLMResponseEvent, llm_resp - ) + ): + return # 返回 LLM 结果 if llm_resp.result_chain: diff --git a/astrbot/core/pipeline/process_stage/method/llm_request.py b/astrbot/core/pipeline/process_stage/method/llm_request.py index 961463c7a..bd5005ee3 100644 --- a/astrbot/core/pipeline/process_stage/method/llm_request.py +++ b/astrbot/core/pipeline/process_stage/method/llm_request.py @@ -112,7 +112,8 @@ class LLMRequestSubStage(Stage): return # 执行请求 LLM 前事件钩子。 - await self.ctx.call_event_hook(event, EventType.OnLLMRequestEvent, req) + if await self.ctx.call_event_hook(event, EventType.OnLLMRequestEvent, req): + return if isinstance(req.contexts, str): req.contexts = json.loads(req.contexts) @@ -159,6 +160,8 @@ class LLMRequestSubStage(Stage): step_idx += 1 try: async for resp in tool_loop_agent.step(): + if event.is_stopped(): + return if resp.type == "tool_call_result": continue # 跳过工具调用结果 if resp.type == "tool_call": From 5f0d601baa449edb3b28837150ecc4259be098db Mon Sep 17 00:00:00 2001 From: Soulter <905617992@qq.com> Date: Thu, 3 Jul 2025 09:59:27 +0800 Subject: [PATCH 12/54] feat: add support for selecting provider and models in webchat --- .../process_stage/method/llm_request.py | 23 +- astrbot/core/pipeline/waking_check/stage.py | 2 +- .../sources/webchat/webchat_adapter.py | 4 + astrbot/core/provider/entities.py | 3 + astrbot/core/provider/provider.py | 2 + .../core/provider/sources/anthropic_source.py | 6 +- .../core/provider/sources/dashscope_source.py | 2 + astrbot/core/provider/sources/dify_source.py | 15 +- .../core/provider/sources/gemini_source.py | 26 +- .../core/provider/sources/openai_source.py | 19 +- astrbot/core/provider/sources/zhipu_source.py | 3 +- astrbot/dashboard/routes/chat.py | 4 + .../components/chat/ProviderModelSelector.vue | 353 ++++++++++++++++++ dashboard/src/views/ChatPage.vue | 296 +-------------- uv.lock | 2 +- 15 files changed, 450 insertions(+), 310 deletions(-) create mode 100644 dashboard/src/components/chat/ProviderModelSelector.vue diff --git a/astrbot/core/pipeline/process_stage/method/llm_request.py b/astrbot/core/pipeline/process_stage/method/llm_request.py index 961463c7a..a6c772c4f 100644 --- a/astrbot/core/pipeline/process_stage/method/llm_request.py +++ b/astrbot/core/pipeline/process_stage/method/llm_request.py @@ -24,6 +24,7 @@ from astrbot.core.provider.entities import ( ) from astrbot.core.star.star_handler import EventType from ..agent_runner.tool_loop_agent import ToolLoopAgent +from astrbot.core.provider import Provider class LLMRequestSubStage(Stage): @@ -51,16 +52,25 @@ class LLMRequestSubStage(Stage): self.conv_manager = ctx.plugin_manager.context.conversation_manager + def _select_provider(self, event: AstrMessageEvent) -> Provider | None: + """选择使用的 LLM 提供商""" + sel_provider = event.get_extra("selected_provider") + _ctx = self.ctx.plugin_manager.context + if sel_provider and isinstance(sel_provider, str): + provider = _ctx.get_provider_by_id(sel_provider) + return provider + + return _ctx.get_using_provider(umo=event.unified_msg_origin) + async def process( self, event: AstrMessageEvent, _nested: bool = False ) -> Union[None, AsyncGenerator[None, None]]: - req: ProviderRequest = None + req: ProviderRequest | None = None if not self.ctx.astrbot_config["provider_settings"]["enable"]: logger.debug("未启用 LLM 能力,跳过处理。") return - umo = event.unified_msg_origin - provider = self.ctx.plugin_manager.context.get_using_provider(umo=umo) + provider = self._select_provider(event) if provider is None: return @@ -75,6 +85,8 @@ class LLMRequestSubStage(Stage): else: req = ProviderRequest(prompt="", image_urls=[]) + if sel_model := event.get_extra("selected_model"): + req.model = sel_model if self.provider_wake_prefix: if not event.message_str.startswith(self.provider_wake_prefix): return @@ -165,7 +177,10 @@ class LLMRequestSubStage(Stage): if self.streaming_response: # 用来标记流式响应需要分节 yield MessageChain(chain=[], type="break") - if self.show_tool_use or event.get_platform_name() == "webchat": + if ( + self.show_tool_use + or event.get_platform_name() == "webchat" + ): resp.data["chain"].type = "tool_call" await event.send(resp.data["chain"]) continue diff --git a/astrbot/core/pipeline/waking_check/stage.py b/astrbot/core/pipeline/waking_check/stage.py index 82e36ca2d..0354260e9 100644 --- a/astrbot/core/pipeline/waking_check/stage.py +++ b/astrbot/core/pipeline/waking_check/stage.py @@ -164,7 +164,7 @@ class WakingCheckStage(Stage): "parsed_params" ) - event.clear_extra() + event._extras.pop("parsed_params", None) event.set_extra("activated_handlers", activated_handlers) event.set_extra("handlers_parsed_params", handlers_parsed_params) diff --git a/astrbot/core/platform/sources/webchat/webchat_adapter.py b/astrbot/core/platform/sources/webchat/webchat_adapter.py index 41d3e9418..aaac8e289 100644 --- a/astrbot/core/platform/sources/webchat/webchat_adapter.py +++ b/astrbot/core/platform/sources/webchat/webchat_adapter.py @@ -151,6 +151,10 @@ class WebChatAdapter(Platform): session_id=message.session_id, ) + _, _, payload = message.raw_message # type: ignore + message_event.set_extra("selected_provider", payload.get("selected_provider")) + message_event.set_extra("selected_model", payload.get("selected_model")) + self.commit_event(message_event) async def terminate(self): diff --git a/astrbot/core/provider/entities.py b/astrbot/core/provider/entities.py index abb01960c..2d120d7f6 100644 --- a/astrbot/core/provider/entities.py +++ b/astrbot/core/provider/entities.py @@ -110,6 +110,9 @@ class ProviderRequest: tool_calls_result: list[ToolCallsResult] | ToolCallsResult | None = None """附加的上次请求后工具调用的结果。参考: https://platform.openai.com/docs/guides/function-calling#handling-function-calls""" + model: str | None = None + """模型名称,为 None 时使用提供商的默认模型""" + def __repr__(self): return f"ProviderRequest(prompt={self.prompt}, session_id={self.session_id}, image_urls={self.image_urls}, func_tool={self.func_tool}, contexts={self._print_friendly_context()}, system_prompt={self.system_prompt.strip()}, tool_calls_result={self.tool_calls_result})" diff --git a/astrbot/core/provider/provider.py b/astrbot/core/provider/provider.py index 1ecca3537..98e8fab85 100644 --- a/astrbot/core/provider/provider.py +++ b/astrbot/core/provider/provider.py @@ -88,6 +88,7 @@ class Provider(AbstractProvider): contexts: list = None, system_prompt: str = None, tool_calls_result: ToolCallsResult | list[ToolCallsResult] = None, + model: str | None = None, **kwargs, ) -> LLMResponse: """获得 LLM 的文本对话结果。会使用当前的模型进行对话。 @@ -116,6 +117,7 @@ class Provider(AbstractProvider): contexts: list = None, system_prompt: str = None, tool_calls_result: ToolCallsResult | list[ToolCallsResult] = None, + model: str | None = None, **kwargs, ) -> AsyncGenerator[LLMResponse, None]: """获得 LLM 的流式文本对话结果。会使用当前的模型进行对话。在生成的最后会返回一次完整的结果。 diff --git a/astrbot/core/provider/sources/anthropic_source.py b/astrbot/core/provider/sources/anthropic_source.py index 4ea4c2e02..aaff177e5 100644 --- a/astrbot/core/provider/sources/anthropic_source.py +++ b/astrbot/core/provider/sources/anthropic_source.py @@ -235,6 +235,7 @@ class ProviderAnthropic(Provider): contexts=None, system_prompt=None, tool_calls_result=None, + model=None, **kwargs, ) -> LLMResponse: if contexts is None: @@ -259,7 +260,7 @@ class ProviderAnthropic(Provider): system_prompt, new_messages = self._prepare_payload(context_query) model_config = self.provider_config.get("model_config", {}) - model_config["model"] = self.get_model() + model_config["model"] = model or self.get_model() payloads = {"messages": new_messages, **model_config} @@ -285,6 +286,7 @@ class ProviderAnthropic(Provider): contexts=..., system_prompt=None, tool_calls_result=None, + model=None, **kwargs, ): if contexts is None: @@ -309,7 +311,7 @@ class ProviderAnthropic(Provider): system_prompt, new_messages = self._prepare_payload(context_query) model_config = self.provider_config.get("model_config", {}) - model_config["model"] = self.get_model() + model_config["model"] = model or self.get_model() payloads = {"messages": new_messages, **model_config} diff --git a/astrbot/core/provider/sources/dashscope_source.py b/astrbot/core/provider/sources/dashscope_source.py index 3498f8346..46b12726b 100644 --- a/astrbot/core/provider/sources/dashscope_source.py +++ b/astrbot/core/provider/sources/dashscope_source.py @@ -67,6 +67,7 @@ class ProviderDashscope(ProviderOpenAIOfficial): func_tool: FuncCall = None, contexts: List = None, system_prompt: str = None, + model=None, **kwargs, ) -> LLMResponse: if contexts is None: @@ -163,6 +164,7 @@ class ProviderDashscope(ProviderOpenAIOfficial): contexts=..., system_prompt=None, tool_calls_result=None, + model=None, **kwargs, ): # raise NotImplementedError("This method is not implemented yet.") diff --git a/astrbot/core/provider/sources/dify_source.py b/astrbot/core/provider/sources/dify_source.py index b3a0ccccf..cc3e8062e 100644 --- a/astrbot/core/provider/sources/dify_source.py +++ b/astrbot/core/provider/sources/dify_source.py @@ -60,6 +60,8 @@ class ProviderDify(Provider): func_tool: FuncCall = None, contexts: List = None, system_prompt: str = None, + tool_calls_result=None, + model=None, **kwargs, ) -> LLMResponse: if image_urls is None: @@ -84,11 +86,13 @@ class ProviderDify(Provider): f"上传图片后得到未知的 Dify 响应:{file_response},图片将忽略。" ) continue - files_payload.append({ - "type": "image", - "transfer_method": "local_file", - "upload_file_id": file_response["id"], - }) + files_payload.append( + { + "type": "image", + "transfer_method": "local_file", + "upload_file_id": file_response["id"], + } + ) # 获得会话变量 payload_vars = self.variables.copy() @@ -195,6 +199,7 @@ class ProviderDify(Provider): contexts=..., system_prompt=None, tool_calls_result=None, + model=None, **kwargs, ): # raise NotImplementedError("This method is not implemented yet.") diff --git a/astrbot/core/provider/sources/gemini_source.py b/astrbot/core/provider/sources/gemini_source.py index 573fe7684..56526c121 100644 --- a/astrbot/core/provider/sources/gemini_source.py +++ b/astrbot/core/provider/sources/gemini_source.py @@ -259,10 +259,12 @@ class ProviderGoogleGenAI(Provider): contents.append(content_cls(parts=part)) gemini_contents: list[types.Content] = [] - native_tool_enabled = any([ - self.provider_config.get("gm_native_coderunner", False), - self.provider_config.get("gm_native_search", False), - ]) + native_tool_enabled = any( + [ + self.provider_config.get("gm_native_coderunner", False), + self.provider_config.get("gm_native_search", False), + ] + ) for message in payloads["messages"]: role, content = message["role"], message.get("content") @@ -505,6 +507,7 @@ class ProviderGoogleGenAI(Provider): contexts=None, system_prompt=None, tool_calls_result=None, + model=None, **kwargs, ) -> LLMResponse: if contexts is None: @@ -527,7 +530,7 @@ class ProviderGoogleGenAI(Provider): context_query.extend(tcr.to_openai_messages()) model_config = self.provider_config.get("model_config", {}) - model_config["model"] = self.get_model() + model_config["model"] = model or self.get_model() payloads = {"messages": context_query, **model_config} @@ -551,6 +554,7 @@ class ProviderGoogleGenAI(Provider): contexts=None, system_prompt=None, tool_calls_result=None, + model=None, **kwargs, ) -> AsyncGenerator[LLMResponse, None]: if contexts is None: @@ -573,7 +577,7 @@ class ProviderGoogleGenAI(Provider): context_query.extend(tcr.to_openai_messages()) model_config = self.provider_config.get("model_config", {}) - model_config["model"] = self.get_model() + model_config["model"] = model or self.get_model() payloads = {"messages": context_query, **model_config} @@ -632,10 +636,12 @@ class ProviderGoogleGenAI(Provider): if not image_data: logger.warning(f"图片 {image_url} 得到的结果为空,将忽略。") continue - user_content["content"].append({ - "type": "image_url", - "image_url": {"url": image_data}, - }) + user_content["content"].append( + { + "type": "image_url", + "image_url": {"url": image_data}, + } + ) return user_content else: return {"role": "user", "content": text} diff --git a/astrbot/core/provider/sources/openai_source.py b/astrbot/core/provider/sources/openai_source.py index 936fc2e34..f4c4987f4 100644 --- a/astrbot/core/provider/sources/openai_source.py +++ b/astrbot/core/provider/sources/openai_source.py @@ -99,6 +99,8 @@ class ProviderOpenAIOfficial(Provider): for key in to_del: del payloads[key] + logger.info(f"payloads: {payloads}") + completion = await self.client.chat.completions.create( **payloads, stream=False, extra_body=extra_body ) @@ -222,6 +224,7 @@ class ProviderOpenAIOfficial(Provider): contexts: list | None = None, system_prompt: str | None = None, tool_calls_result: ToolCallsResult | list[ToolCallsResult] | None = None, + model: str | None = None, **kwargs, ) -> tuple: """准备聊天所需的有效载荷和上下文""" @@ -245,7 +248,7 @@ class ProviderOpenAIOfficial(Provider): context_query.extend(tcr.to_openai_messages()) model_config = self.provider_config.get("model_config", {}) - model_config["model"] = self.get_model() + model_config["model"] = model or self.get_model() payloads = {"messages": context_query, **model_config} @@ -346,6 +349,7 @@ class ProviderOpenAIOfficial(Provider): contexts=None, system_prompt=None, tool_calls_result=None, + model=None, **kwargs, ) -> LLMResponse: payloads, context_query = await self._prepare_chat_payload( @@ -354,6 +358,7 @@ class ProviderOpenAIOfficial(Provider): contexts, system_prompt, tool_calls_result, + model=model, **kwargs, ) @@ -413,6 +418,7 @@ class ProviderOpenAIOfficial(Provider): contexts=[], system_prompt=None, tool_calls_result=None, + model=None, **kwargs, ) -> AsyncGenerator[LLMResponse, None]: """流式对话,与服务商交互并逐步返回结果""" @@ -422,6 +428,7 @@ class ProviderOpenAIOfficial(Provider): contexts, system_prompt, tool_calls_result, + model=model, **kwargs, ) @@ -525,10 +532,12 @@ class ProviderOpenAIOfficial(Provider): if not image_data: logger.warning(f"图片 {image_url} 得到的结果为空,将忽略。") continue - user_content["content"].append({ - "type": "image_url", - "image_url": {"url": image_data}, - }) + user_content["content"].append( + { + "type": "image_url", + "image_url": {"url": image_data}, + } + ) return user_content else: return {"role": "user", "content": text} diff --git a/astrbot/core/provider/sources/zhipu_source.py b/astrbot/core/provider/sources/zhipu_source.py index 428dee8f4..cf52e95fc 100644 --- a/astrbot/core/provider/sources/zhipu_source.py +++ b/astrbot/core/provider/sources/zhipu_source.py @@ -28,6 +28,7 @@ class ProviderZhipu(ProviderOpenAIOfficial): func_tool: FuncCall = None, contexts=None, system_prompt=None, + model=None, **kwargs, ) -> LLMResponse: if contexts is None: @@ -38,7 +39,7 @@ class ProviderZhipu(ProviderOpenAIOfficial): context_query = [*contexts, new_record] model_cfgs: dict = self.provider_config.get("model_config", {}) - model = self.get_model() + model = model or self.get_model() # glm-4v-flash 只支持一张图片 if model.lower() == "glm-4v-flash" and image_urls and len(context_query) > 1: logger.debug("glm-4v-flash 只支持一张图片,将只保留最后一张图片") diff --git a/astrbot/dashboard/routes/chat.py b/astrbot/dashboard/routes/chat.py index a273bccdc..e7b086cd1 100644 --- a/astrbot/dashboard/routes/chat.py +++ b/astrbot/dashboard/routes/chat.py @@ -120,6 +120,8 @@ class ChatRoute(Route): conversation_id = post_data["conversation_id"] image_url = post_data.get("image_url") audio_url = post_data.get("audio_url") + selected_provider = post_data.get("selected_provider") + selected_model = post_data.get("selected_model") if not message and not image_url and not audio_url: return ( Response() @@ -202,6 +204,8 @@ class ChatRoute(Route): "message": message, "image_url": image_url, # list "audio_url": audio_url, + "selected_provider": selected_provider, + "selected_model": selected_model, }, ) ) diff --git a/dashboard/src/components/chat/ProviderModelSelector.vue b/dashboard/src/components/chat/ProviderModelSelector.vue new file mode 100644 index 000000000..7509b5295 --- /dev/null +++ b/dashboard/src/components/chat/ProviderModelSelector.vue @@ -0,0 +1,353 @@ + + + + + diff --git a/dashboard/src/views/ChatPage.vue b/dashboard/src/views/ChatPage.vue index 1d849c8cb..4995babfc 100644 --- a/dashboard/src/views/ChatPage.vue +++ b/dashboard/src/views/ChatPage.vue @@ -80,7 +80,7 @@

{{ getCurrentConversation.title || tm('conversation.newConversation') - }}

+ }}
{{ formatDate(getCurrentConversation.updated_at) }}
@@ -190,17 +190,11 @@ -
+
- - - {{ selectedProviderId }} / {{ selectedModelName }} - - - 选择模型 - - +
- - - - - - 选择提供商和模型 - - -
- -
-
-

提供商

-
- - - {{ provider.id }} - {{ provider.api_base }} - - -
- -
暂无可用提供商
-
-
- - -
-
-

模型

- - -
- - - {{ model }} - {{ model.description }} - - -
- -
请先选择提供商
-
-
- -
该提供商暂无可用模型
-
-
-
-
- - - 取消 - - 确认选择 - - -
-
@@ -1917,82 +1729,4 @@ export default { flex-shrink: 0; /* 防止header被压缩 */ } - -/* 提供商和模型选择对话框样式 */ -.provider-model-container { - display: flex; - height: 500px; - border: 1px solid var(--v-theme-border); - border-radius: 8px; - overflow: hidden; -} - -.provider-list-panel, -.model-list-panel { - flex: 1; - display: flex; - flex-direction: column; - background-color: var(--v-theme-surface); -} - -.provider-list-panel { - border-right: 1px solid var(--v-theme-border); -} - -.panel-header { - display: flex; - align-items: center; - justify-content: space-between; - padding: 16px; - border-bottom: 1px solid var(--v-theme-border); - background-color: var(--v-theme-containerBg); -} - -.panel-header h4 { - margin: 0; - font-size: 16px; - font-weight: 500; - color: var(--v-theme-primaryText); -} - -.provider-list, -.model-list { - flex: 1; - overflow-y: auto; - padding: 8px; -} - -.provider-item, -.model-item { - margin-bottom: 4px; - border-radius: 8px !important; - transition: all 0.2s ease; - cursor: pointer; -} - -.provider-item:hover, -.model-item:hover { - background-color: rgba(103, 58, 183, 0.05); -} - -.provider-item.v-list-item--active, -.model-item.v-list-item--active { - background-color: rgba(103, 58, 183, 0.1); - color: var(--v-theme-secondary); -} - -.empty-state { - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - height: 200px; - opacity: 0.6; - gap: 12px; -} - -.empty-text { - font-size: 14px; - color: var(--v-theme-secondaryText); -} \ No newline at end of file diff --git a/uv.lock b/uv.lock index 6279381bf..7a245997f 100644 --- a/uv.lock +++ b/uv.lock @@ -204,7 +204,7 @@ wheels = [ [[package]] name = "astrbot" -version = "3.5.17" +version = "3.5.18" source = { editable = "." } dependencies = [ { name = "aiocqhttp" }, From 6f71301aaf8360b488da0a3d08bd8b154663c2b2 Mon Sep 17 00:00:00 2001 From: Soulter <905617992@qq.com> Date: Thu, 3 Jul 2025 10:49:12 +0800 Subject: [PATCH 13/54] fix: log error when selected provider is not found --- astrbot/core/pipeline/process_stage/method/llm_request.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/astrbot/core/pipeline/process_stage/method/llm_request.py b/astrbot/core/pipeline/process_stage/method/llm_request.py index a6c772c4f..58b76a387 100644 --- a/astrbot/core/pipeline/process_stage/method/llm_request.py +++ b/astrbot/core/pipeline/process_stage/method/llm_request.py @@ -58,6 +58,8 @@ class LLMRequestSubStage(Stage): _ctx = self.ctx.plugin_manager.context if sel_provider and isinstance(sel_provider, str): provider = _ctx.get_provider_by_id(sel_provider) + if not provider: + logger.error(f"未找到指定的提供商: {sel_provider}。") return provider return _ctx.get_using_provider(umo=event.unified_msg_origin) From ad14e9bf4018c15481874e76ca190c9a9f1dfbe1 Mon Sep 17 00:00:00 2001 From: Soulter <905617992@qq.com> Date: Thu, 3 Jul 2025 10:50:03 +0800 Subject: [PATCH 14/54] chore: remove unnecessary logging of payloads in chat completion --- astrbot/core/provider/sources/openai_source.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/astrbot/core/provider/sources/openai_source.py b/astrbot/core/provider/sources/openai_source.py index f4c4987f4..36a8579a6 100644 --- a/astrbot/core/provider/sources/openai_source.py +++ b/astrbot/core/provider/sources/openai_source.py @@ -99,8 +99,6 @@ class ProviderOpenAIOfficial(Provider): for key in to_del: del payloads[key] - logger.info(f"payloads: {payloads}") - completion = await self.client.chat.completions.create( **payloads, stream=False, extra_body=extra_body ) From 1a8a171f8b692961f63f9638b676bcce1fe9a10e Mon Sep 17 00:00:00 2001 From: Christian Clauss Date: Thu, 3 Jul 2025 08:46:42 +0200 Subject: [PATCH 15/54] Keep GitHub Actions up to date with GitHub's Dependabot * [Keeping your software supply chain secure with Dependabot](https://docs.github.com/en/code-security/dependabot) * [Keeping your actions up to date with Dependabot](https://docs.github.com/en/code-security/dependabot/working-with-dependabot/keeping-your-actions-up-to-date-with-dependabot) * [Configuration options for the `dependabot.yml` file - package-ecosystem](https://docs.github.com/en/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file#package-ecosystem) --- .github/dependabot.yml | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 .github/dependabot.yml diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 000000000..be006de9a --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,13 @@ +# Keep GitHub Actions up to date with GitHub's Dependabot... +# https://docs.github.com/en/code-security/dependabot/working-with-dependabot/keeping-your-actions-up-to-date-with-dependabot +# https://docs.github.com/en/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file#package-ecosystem +version: 2 +updates: + - package-ecosystem: github-actions + directory: / + groups: + github-actions: + patterns: + - "*" # Group all Actions updates into a single larger pull request + schedule: + interval: weekly From 14e0aa3ec59611693d8fde6c38ee553799e52fdb Mon Sep 17 00:00:00 2001 From: Soulter <905617992@qq.com> Date: Thu, 3 Jul 2025 15:39:42 +0800 Subject: [PATCH 16/54] =?UTF-8?q?perf:=20history=20=E5=92=8C=20persona=20?= =?UTF-8?q?=E6=8C=87=E4=BB=A4=E5=BD=93=E5=AF=B9=E8=AF=9D=E4=B8=8D=E5=AD=98?= =?UTF-8?q?=E5=9C=A8=E7=9A=84=E6=97=B6=E5=80=99=E8=87=AA=E5=8A=A8=E5=88=9B?= =?UTF-8?q?=E5=BB=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit fixes: #1997 --- astrbot/core/conversation_mgr.py | 12 ++++++++- packages/astrbot/main.py | 45 ++++++++++++-------------------- 2 files changed, 27 insertions(+), 30 deletions(-) diff --git a/astrbot/core/conversation_mgr.py b/astrbot/core/conversation_mgr.py index b0f5c136d..b665488e4 100644 --- a/astrbot/core/conversation_mgr.py +++ b/astrbot/core/conversation_mgr.py @@ -88,7 +88,10 @@ class ConversationManager: return self.session_conversations.get(unified_msg_origin, None) async def get_conversation( - self, unified_msg_origin: str, conversation_id: str + self, + unified_msg_origin: str, + conversation_id: str, + create_if_not_exists: bool = False, ) -> Conversation: """获取会话的对话 @@ -98,6 +101,13 @@ class ConversationManager: Returns: conversation (Conversation): 对话对象 """ + conv = self.db.get_conversation_by_user_id(unified_msg_origin, conversation_id) + if not conv and create_if_not_exists: + # 如果对话不存在且需要创建,则新建一个对话 + conversation_id = await self.new_conversation(unified_msg_origin) + return self.db.get_conversation_by_user_id( + unified_msg_origin, conversation_id + ) return self.db.get_conversation_by_user_id(unified_msg_origin, conversation_id) async def get_conversations(self, unified_msg_origin: str) -> List[Conversation]: diff --git a/packages/astrbot/main.py b/packages/astrbot/main.py index 0790326c1..0a3f8ba6c 100644 --- a/packages/astrbot/main.py +++ b/packages/astrbot/main.py @@ -655,25 +655,16 @@ UID: {user_id} 此 ID 可用于设置管理员。 return size_per_page = 6 - session_curr_cid = ( - await self.context.conversation_manager.get_curr_conversation_id( - message.unified_msg_origin - ) - ) + + conv_mgr = self.context.conversation_manager + umo = message.unified_msg_origin + session_curr_cid = await conv_mgr.get_curr_conversation_id(umo) if not session_curr_cid: - message.set_result( - MessageEventResult().message( - "当前未处于对话状态,请 /switch 序号 切换或者 /new 创建。" - ) - ) - return + session_curr_cid = await conv_mgr.new_conversation(umo) - ( - contexts, - total_pages, - ) = await self.context.conversation_manager.get_human_readable_context( - message.unified_msg_origin, session_curr_cid, page, size_per_page + contexts, total_pages = await conv_mgr.get_human_readable_context( + umo, session_curr_cid, page, size_per_page ) history = "" @@ -682,12 +673,12 @@ UID: {user_id} 此 ID 可用于设置管理员。 context = context[:150] + "..." history += f"{context}\n" - ret = f"""当前对话历史记录: -{history} -第 {page} 页 | 共 {total_pages} 页 - -*输入 /history 2 跳转到第 2 页 -""" + ret = ( + f"当前对话历史记录:" + f"{history if history else '无历史记录'}\n\n" + f"第 {page} 页 | 共 {total_pages} 页\n" + f"*输入 /history 2 跳转到第 2 页" + ) message.set_result(MessageEventResult().message(ret).use_t2i(False)) @@ -1022,14 +1013,10 @@ UID: {user_id} 此 ID 可用于设置管理员。 curr_cid_title = "无" if cid: conversation = await self.context.conversation_manager.get_conversation( - message.unified_msg_origin, cid + unified_msg_origin=message.unified_msg_origin, + conversation_id=cid, + create_if_not_exists=True, ) - if not conversation: - message.set_result( - MessageEventResult().message( - "请先进入一个对话。可以使用 /new 创建。" - ) - ) if not conversation.persona_id and not conversation.persona_id == "[%None]": curr_persona_name = ( self.context.provider_manager.selected_default_persona["name"] From 543e01c3011ede4b790012df01267bf61b6598c2 Mon Sep 17 00:00:00 2001 From: Soulter <905617992@qq.com> Date: Thu, 3 Jul 2025 15:44:45 +0800 Subject: [PATCH 17/54] =?UTF-8?q?perf:=20webui=20=E5=88=A0=E9=99=A4?= =?UTF-8?q?=E5=AF=B9=E8=AF=9D=E4=BD=BF=E7=94=A8=20conversation=5Fmgr?= =?UTF-8?q?=EF=BC=8C=E4=BB=A5=E4=BF=9D=E6=8C=81=E7=8A=B6=E6=80=81=E5=90=8C?= =?UTF-8?q?=E6=AD=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- astrbot/dashboard/routes/conversation.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/astrbot/dashboard/routes/conversation.py b/astrbot/dashboard/routes/conversation.py index aa8b0af36..d73e6186a 100644 --- a/astrbot/dashboard/routes/conversation.py +++ b/astrbot/dashboard/routes/conversation.py @@ -29,6 +29,7 @@ class ConversationRoute(Route): ), } self.db_helper = db_helper + self.core_lifecycle = core_lifecycle self.register_routes() async def list_conversations(self): @@ -165,11 +166,9 @@ class ConversationRoute(Route): if not user_id or not cid: return Response().error("缺少必要参数: user_id 和 cid").__dict__ - conversation = self.db_helper.get_conversation_by_user_id(user_id, cid) - if not conversation: - return Response().error("对话不存在").__dict__ - self.db_helper.delete_conversation(user_id, cid) - + self.core_lifecycle.conversation_manager.delete_conversation( + unified_msg_origin=user_id, conversation_id=cid + ) return Response().ok({"message": "对话删除成功"}).__dict__ except Exception as e: From a4e999c47f9eeec3732aa91f4a53161080ee2273 Mon Sep 17 00:00:00 2001 From: Raven95676 Date: Thu, 3 Jul 2025 22:13:13 +0800 Subject: [PATCH 18/54] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0=E9=A3=8E?= =?UTF-8?q?=E9=99=A9=E6=8F=92=E4=BB=B6=E5=AE=89=E8=A3=85=E7=A1=AE=E8=AE=A4?= =?UTF-8?q?=E5=AF=B9=E8=AF=9D=E6=A1=86=E4=BB=A5=E5=8F=8A=E9=A3=8E=E9=99=A9?= =?UTF-8?q?=E6=8F=92=E4=BB=B6=E6=A0=87=E7=AD=BE=E7=89=B9=E6=AE=8A=E5=A4=84?= =?UTF-8?q?=E7=90=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/components/shared/ExtensionCard.vue | 12 +++- .../locales/en-US/features/extension.json | 11 +++- .../locales/zh-CN/features/extension.json | 11 +++- dashboard/src/views/ExtensionPage.vue | 65 +++++++++++++++++-- 4 files changed, 91 insertions(+), 8 deletions(-) diff --git a/dashboard/src/components/shared/ExtensionCard.vue b/dashboard/src/components/shared/ExtensionCard.vue index 35666b740..8a0075173 100644 --- a/dashboard/src/components/shared/ExtensionCard.vue +++ b/dashboard/src/components/shared/ExtensionCard.vue @@ -49,6 +49,11 @@ const reloadExtension = () => { }; const $confirm = inject("$confirm"); + +const installExtension = async () => { + emit('install', props.extension); +}; + const uninstallExtension = async () => { if (typeof $confirm !== "function") { console.error(tm("card.errors.confirmNotRegistered")); @@ -117,6 +122,10 @@ const viewReadme = () => { {{ extension.handlers?.length }}{{ tm("card.status.handlersCount") }} + + {{ tag === 'danger' ? tm('tags.danger') : tag }} +
@@ -139,7 +148,7 @@ const viewReadme = () => { + @click="installExtension"> @@ -200,6 +209,7 @@ const viewReadme = () => { + \ No newline at end of file From 225db6673823a6ca82cb8368a8d6c7ec32d796c8 Mon Sep 17 00:00:00 2001 From: Soulter <905617992@qq.com> Date: Fri, 4 Jul 2025 16:59:49 +0800 Subject: [PATCH 26/54] fix: refine streaming logic in chat response handling --- astrbot/dashboard/routes/chat.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/astrbot/dashboard/routes/chat.py b/astrbot/dashboard/routes/chat.py index e7b086cd1..b704a8888 100644 --- a/astrbot/dashboard/routes/chat.py +++ b/astrbot/dashboard/routes/chat.py @@ -166,13 +166,12 @@ class ChatRoute(Route): type = result.get("type") cid = result.get("cid") streaming = result.get("streaming", False) + chain_type = result.get("chain_type") yield f"data: {json.dumps(result, ensure_ascii=False)}\n\n" await asyncio.sleep(0.05) if streaming and type != "end": - continue - - if type == "update_title": + # If the result is still streaming, we continue to wait for more data continue if result_text: @@ -189,7 +188,11 @@ class ChatRoute(Route): self.db.update_conversation( username, cid, history=json.dumps(history) ) - break + if chain_type not in ["tool_call", "tool_call_result"]: + # If the result is not a tool call or tool call result, + # we can break the loop and end the stream + break + except BaseException as _: logger.debug(f"用户 {username} 断开聊天长连接。") return From 3df5e7b9b901225b6a864a51f7d405a981780d6c Mon Sep 17 00:00:00 2001 From: IGCrystal Date: Fri, 4 Jul 2025 17:28:39 +0800 Subject: [PATCH 27/54] =?UTF-8?q?=F0=9F=90=9E=20fix:=20=E6=B7=BB=E5=8A=A0t?= =?UTF-8?q?ags.danger=E7=9A=84=E7=BF=BB=E8=AF=91=E9=94=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- dashboard/src/i18n/locales/en-US/features/extension.json | 3 +++ dashboard/src/i18n/locales/zh-CN/features/extension.json | 3 +++ 2 files changed, 6 insertions(+) diff --git a/dashboard/src/i18n/locales/en-US/features/extension.json b/dashboard/src/i18n/locales/en-US/features/extension.json index 40929054e..a9d0d1b2f 100644 --- a/dashboard/src/i18n/locales/en-US/features/extension.json +++ b/dashboard/src/i18n/locales/en-US/features/extension.json @@ -79,6 +79,9 @@ "devDocs": "Extension Development Docs", "submitRepo": "Submit Extension Repository" }, + "tags": { + "danger": "Danger" + }, "dialogs": { "error": { "title": "Error Information", diff --git a/dashboard/src/i18n/locales/zh-CN/features/extension.json b/dashboard/src/i18n/locales/zh-CN/features/extension.json index 1e8eb849a..a8e4559eb 100644 --- a/dashboard/src/i18n/locales/zh-CN/features/extension.json +++ b/dashboard/src/i18n/locales/zh-CN/features/extension.json @@ -79,6 +79,9 @@ "devDocs": "插件开发文档", "submitRepo": "提交插件仓库" }, + "tags": { + "danger": "危险" + }, "dialogs": { "error": { "title": "错误信息", From 3f5210c587927e48a980d591c70b1ba0ebca85de Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=B8=A6=E7=BE=BD?= Date: Fri, 4 Jul 2025 22:28:00 +0800 Subject: [PATCH 28/54] chore: update plugin publish template --- .github/ISSUE_TEMPLATE/PLUGIN_PUBLISH.md | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/ISSUE_TEMPLATE/PLUGIN_PUBLISH.md b/.github/ISSUE_TEMPLATE/PLUGIN_PUBLISH.md index 73f5009ca..0358a5b27 100644 --- a/.github/ISSUE_TEMPLATE/PLUGIN_PUBLISH.md +++ b/.github/ISSUE_TEMPLATE/PLUGIN_PUBLISH.md @@ -17,6 +17,7 @@ assignees: '' { "name": "插件名", "desc": "插件介绍", + "author": "作者名", "repo": "插件仓库链接", "tags": [], "social_link": "" From ff63134c147dde33f378cea74d172e8eee574dab Mon Sep 17 00:00:00 2001 From: JOJO <41473909@qq.com> Date: Sat, 5 Jul 2025 12:30:50 +0800 Subject: [PATCH 29/54] =?UTF-8?q?fix:=20=E5=A2=9E=E5=8A=A0=E6=BC=94?= =?UTF-8?q?=E7=A4=BA=E6=A8=A1=E5=BC=8F=E4=B8=8B=E6=A0=A1=E9=AA=8C=E6=8F=92?= =?UTF-8?q?=E4=BB=B6=E5=BC=80=E5=90=AF/=E5=85=B3=E9=97=AD/=E5=AE=89?= =?UTF-8?q?=E8=A3=85=E6=8C=87=E4=BB=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/astrbot/main.py | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/packages/astrbot/main.py b/packages/astrbot/main.py index 0a3f8ba6c..d2a78d609 100644 --- a/packages/astrbot/main.py +++ b/packages/astrbot/main.py @@ -10,6 +10,7 @@ import astrbot.api.event.filter as filter from astrbot.api.event import AstrMessageEvent, MessageEventResult from astrbot.api import sp from astrbot.api.provider import ProviderRequest +from astrbot.core import DEMO_MODE from astrbot.core.platform.astr_message_event import MessageSesion from astrbot.core.platform.message_type import MessageType from astrbot.core.provider.entities import ProviderType @@ -59,7 +60,7 @@ class RstScene(Enum): name="astrbot", desc="AstrBot 基础指令结合 + 拓展功能", author="Soulter", - version="4.0.0", + version="4.0.1", ) class Main(star.Star): def __init__(self, context: star.Context) -> None: @@ -233,6 +234,11 @@ class Main(star.Star): @plugin.command("off") async def plugin_off(self, event: AstrMessageEvent, plugin_name: str = None): """禁用插件""" + if DEMO_MODE: + event.set_result( + MessageEventResult().message("演示模式下无法禁用插件。") + ) + return if not plugin_name: event.set_result( MessageEventResult().message("/plugin off <插件名> 禁用插件。") @@ -245,6 +251,11 @@ class Main(star.Star): @plugin.command("on") async def plugin_on(self, event: AstrMessageEvent, plugin_name: str = None): """启用插件""" + if DEMO_MODE: + event.set_result( + MessageEventResult().message("演示模式下无法启用插件。") + ) + return if not plugin_name: event.set_result( MessageEventResult().message("/plugin on <插件名> 启用插件。") @@ -257,6 +268,11 @@ class Main(star.Star): @plugin.command("get") async def plugin_get(self, event: AstrMessageEvent, plugin_repo: str = None): """安装插件""" + if DEMO_MODE: + event.set_result( + MessageEventResult().message("演示模式下无法安装插件。") + ) + return if not plugin_repo: event.set_result( MessageEventResult().message("/plugin get <插件仓库地址> 安装插件") From 24958377913b41d9da33cd9a6c192312807ac937 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 5 Jul 2025 11:20:25 +0000 Subject: [PATCH 30/54] chore(deps): bump the github-actions group with 4 updates Bumps the github-actions group with 4 updates: [actions/checkout](https://github.com/actions/checkout), [actions/setup-python](https://github.com/actions/setup-python), [codecov/codecov-action](https://github.com/codecov/codecov-action) and [actions/stale](https://github.com/actions/stale). Updates `actions/checkout` from 3 to 4 - [Release notes](https://github.com/actions/checkout/releases) - [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md) - [Commits](https://github.com/actions/checkout/compare/v3...v4) Updates `actions/setup-python` from 4 to 5 - [Release notes](https://github.com/actions/setup-python/releases) - [Commits](https://github.com/actions/setup-python/compare/v4...v5) Updates `codecov/codecov-action` from 4 to 5 - [Release notes](https://github.com/codecov/codecov-action/releases) - [Changelog](https://github.com/codecov/codecov-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/codecov/codecov-action/compare/v4...v5) Updates `actions/stale` from 5 to 9 - [Release notes](https://github.com/actions/stale/releases) - [Changelog](https://github.com/actions/stale/blob/main/CHANGELOG.md) - [Commits](https://github.com/actions/stale/compare/v5...v9) --- updated-dependencies: - dependency-name: actions/checkout dependency-version: '4' dependency-type: direct:production update-type: version-update:semver-major dependency-group: github-actions - dependency-name: actions/setup-python dependency-version: '5' dependency-type: direct:production update-type: version-update:semver-major dependency-group: github-actions - dependency-name: codecov/codecov-action dependency-version: '5' dependency-type: direct:production update-type: version-update:semver-major dependency-group: github-actions - dependency-name: actions/stale dependency-version: '9' dependency-type: direct:production update-type: version-update:semver-major dependency-group: github-actions ... Signed-off-by: dependabot[bot] --- .github/workflows/auto_release.yml | 2 +- .github/workflows/coverage_test.yml | 4 ++-- .github/workflows/docker-image.yml | 2 +- .github/workflows/stale.yml | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/auto_release.yml b/.github/workflows/auto_release.yml index 854a69f16..46d914a9a 100644 --- a/.github/workflows/auto_release.yml +++ b/.github/workflows/auto_release.yml @@ -73,7 +73,7 @@ jobs: uses: actions/checkout@v4 - name: Set up Python - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: '3.10' diff --git a/.github/workflows/coverage_test.yml b/.github/workflows/coverage_test.yml index 30e9237ed..a62efa5d2 100644 --- a/.github/workflows/coverage_test.yml +++ b/.github/workflows/coverage_test.yml @@ -21,7 +21,7 @@ jobs: fetch-depth: 0 - name: Set up Python - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 - name: Install dependencies run: | @@ -40,6 +40,6 @@ jobs: PYTHONPATH=./ pytest --cov=. tests/ -v -o log_cli=true -o log_level=DEBUG - name: Upload results to Codecov - uses: codecov/codecov-action@v4 + uses: codecov/codecov-action@v5 with: token: ${{ secrets.CODECOV_TOKEN }} \ No newline at end of file diff --git a/.github/workflows/docker-image.yml b/.github/workflows/docker-image.yml index e0e235c09..c0610a3c1 100644 --- a/.github/workflows/docker-image.yml +++ b/.github/workflows/docker-image.yml @@ -12,7 +12,7 @@ jobs: steps: - name: Pull The Codes - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: fetch-depth: 0 # Must be 0 so we can fetch tags diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml index 310e250b6..283e99989 100644 --- a/.github/workflows/stale.yml +++ b/.github/workflows/stale.yml @@ -18,7 +18,7 @@ jobs: pull-requests: write steps: - - uses: actions/stale@v5 + - uses: actions/stale@v9 with: repo-token: ${{ secrets.GITHUB_TOKEN }} stale-issue-message: 'Stale issue message' From 9f31df7f3a9dac47a7e6a5b567cde4e1065c5fab Mon Sep 17 00:00:00 2001 From: Christian Clauss Date: Sat, 5 Jul 2025 13:52:28 +0200 Subject: [PATCH 31/54] pytest recommendation: `pip install --editable .` https://docs.pytest.org/en/stable/how-to/existingtestsuite.html This makes setting `PYTHONPATH` unnecessary and will pull requirements from `pyproject.toml` instead of `requirements.txt`, so it is similar to end-user installations. `makedir -p data/plugins` will do both `mkdir data` and `mkdir data/plugins`. The `$CI` environment variable might be better to use than `$TESTING` because it is preset to `true` in GitHub Actions. * https://docs.github.com/en/actions/reference/variables-reference#default-environment-variables * https://docs.pytest.org/en/stable/explanation/ci.html --- .github/workflows/coverage_test.yml | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/.github/workflows/coverage_test.yml b/.github/workflows/coverage_test.yml index a62efa5d2..3f219121c 100644 --- a/.github/workflows/coverage_test.yml +++ b/.github/workflows/coverage_test.yml @@ -26,20 +26,19 @@ jobs: - name: Install dependencies run: | python -m pip install --upgrade pip - pip install -r requirements.txt - pip install pytest pytest-cov pytest-asyncio + pip install pytest pytest-asyncio pytest-cov + pip install --editable . - name: Run tests run: | - mkdir data - mkdir data/plugins - mkdir data/config - mkdir data/temp + mkdir -p data/plugins + mkdir -p data/config + mkdir -p data/temp export TESTING=true export ZHIPU_API_KEY=${{ secrets.OPENAI_API_KEY }} - PYTHONPATH=./ pytest --cov=. tests/ -v -o log_cli=true -o log_level=DEBUG + pytest --cov=. tests/ -v -o log_cli=true -o log_level=DEBUG - name: Upload results to Codecov uses: codecov/codecov-action@v5 with: - token: ${{ secrets.CODECOV_TOKEN }} \ No newline at end of file + token: ${{ secrets.CODECOV_TOKEN }} From d2f7e55bf5bd34c20ab6d06dc76d922b0d37e6ea Mon Sep 17 00:00:00 2001 From: Christian Clauss Date: Sat, 5 Jul 2025 13:57:58 +0200 Subject: [PATCH 32/54] Run the tests on pull requests --- .github/workflows/coverage_test.yml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/coverage_test.yml b/.github/workflows/coverage_test.yml index 3f219121c..e9c94d679 100644 --- a/.github/workflows/coverage_test.yml +++ b/.github/workflows/coverage_test.yml @@ -1,6 +1,6 @@ name: Run tests and upload coverage -on: +on: push: branches: - master @@ -8,6 +8,7 @@ on: - 'README.md' - 'changelogs/**' - 'dashboard/**' + pull_request: workflow_dispatch: jobs: @@ -36,7 +37,7 @@ jobs: mkdir -p data/temp export TESTING=true export ZHIPU_API_KEY=${{ secrets.OPENAI_API_KEY }} - pytest --cov=. tests/ -v -o log_cli=true -o log_level=DEBUG + pytest --cov=. -v -o log_cli=true -o log_level=DEBUG - name: Upload results to Codecov uses: codecov/codecov-action@v5 From 1674653a42f9c56ef58d404bcd3aa5b19feee6d4 Mon Sep 17 00:00:00 2001 From: Zhenyi Wang Date: Sun, 6 Jul 2025 16:18:31 +0800 Subject: [PATCH 33/54] =?UTF-8?q?fix(wechatpadpro):=20=E4=BF=AE=E5=A4=8D?= =?UTF-8?q?=E6=8E=88=E6=9D=83=E7=A0=81=E6=8F=90=E5=8F=96=E9=80=BB=E8=BE=91?= =?UTF-8?q?=E4=BB=A5=E5=85=BC=E5=AE=B9=E6=96=B0=E6=97=A7=E6=8E=A5=E5=8F=A3?= =?UTF-8?q?=E6=A0=BC=E5=BC=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 新接口返回多了一层authKeys字段,同时兼容二者 --- .../wechatpadpro/wechatpadpro_adapter.py | 28 ++++++++++--------- 1 file changed, 15 insertions(+), 13 deletions(-) diff --git a/astrbot/core/platform/sources/wechatpadpro/wechatpadpro_adapter.py b/astrbot/core/platform/sources/wechatpadpro/wechatpadpro_adapter.py index 58e3c9b19..786c769bf 100644 --- a/astrbot/core/platform/sources/wechatpadpro/wechatpadpro_adapter.py +++ b/astrbot/core/platform/sources/wechatpadpro/wechatpadpro_adapter.py @@ -225,21 +225,23 @@ class WeChatPadProAdapter(Platform): # 修正成功判断条件和授权码提取路径 if response.status == 200 and response_data.get("Code") == 200: # 授权码在 Data 字段的列表中 - if ( - response_data.get("Data") - and isinstance(response_data["Data"], list) - and len(response_data["Data"]) > 0 - ): - self.auth_key = response_data["Data"][0] - logger.info(f"成功获取授权码 {self.auth_key[:8]}...") + data = response_data.get("Data") + if data: + # 新返回格式 + if isinstance(data.get("authKeys"), list) and data["authKeys"]: + self.auth_key = data["authKeys"][0] + # 兼容旧版接口 + elif isinstance(data, list) and data: + self.auth_key = data[0] + + if self.auth_key: + logger.info(f"成功获取授权码 {self.auth_key[:8]}...") + else: + logger.error(f"生成授权码成功但未找到授权码: {response_data}") else: - logger.error( - f"生成授权码成功但未找到授权码: {response_data}" - ) + logger.error(f"生成授权码成功但未找到授权码: {response_data}") else: - logger.error( - f"生成授权码失败: {response.status}, {response_data}" - ) + logger.error(f"生成授权码失败: {response.status}, {response_data}") except aiohttp.ClientConnectorError as e: logger.error(f"连接到 WeChatPadPro 服务失败: {e}") except Exception as e: From 3e4917f0a10ba6e2556573e6a9fa0fd31eaa9364 Mon Sep 17 00:00:00 2001 From: Zhenyi Wang Date: Sun, 6 Jul 2025 16:34:55 +0800 Subject: [PATCH 34/54] =?UTF-8?q?refactor:=20=E9=87=8D=E6=9E=84=20wechatpa?= =?UTF-8?q?dpro=20=E6=8E=88=E6=9D=83=E7=A0=81=E7=94=9F=E6=88=90=E5=B9=B6?= =?UTF-8?q?=E5=A2=9E=E5=BC=BA=E5=AE=89=E5=85=A8=E6=80=A7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 将 generate_auth_key 方法中的授权码提取逻辑重构为新的辅助方法 _extract_auth_key ,以提高代码的可读性和可测试性。 - 在访问 data.get('authKeys') 之前添加 isinstance(data, dict) 检查,以防止潜在的 AttributeError 。 - 移除了 auth_key 的明文日志记录,以避免敏感信息泄露。 - 在生成新密钥之前,将 self.auth_key 初始化为 None ,以避免在失败时保留旧值。 --- .../wechatpadpro/wechatpadpro_adapter.py | 40 +++++++++++-------- 1 file changed, 23 insertions(+), 17 deletions(-) diff --git a/astrbot/core/platform/sources/wechatpadpro/wechatpadpro_adapter.py b/astrbot/core/platform/sources/wechatpadpro/wechatpadpro_adapter.py index 786c769bf..7d5984416 100644 --- a/astrbot/core/platform/sources/wechatpadpro/wechatpadpro_adapter.py +++ b/astrbot/core/platform/sources/wechatpadpro/wechatpadpro_adapter.py @@ -210,6 +210,16 @@ class WeChatPadProAdapter(Platform): logger.error(traceback.format_exc()) return False + def _extract_auth_key(self, data): + """Helper method to extract auth_key from response data.""" + if isinstance(data, dict): + auth_keys = data.get("authKeys") # 新接口 + if isinstance(auth_keys, list) and auth_keys: + return auth_keys[0] + elif isinstance(data, list) and data: # 旧接口 + return data[0] + return None + async def generate_auth_key(self): """ 生成授权码。 @@ -218,30 +228,26 @@ class WeChatPadProAdapter(Platform): params = {"key": self.admin_key} payload = {"Count": 1, "Days": 365} # 生成一个有效期365天的授权码 + self.auth_key = None # Reset auth_key before generating a new one + async with aiohttp.ClientSession() as session: try: async with session.post(url, params=params, json=payload) as response: + if response.status != 200: + logger.error(f"生成授权码失败: {response.status}, {await response.text()}") + return + response_data = await response.json() - # 修正成功判断条件和授权码提取路径 - if response.status == 200 and response_data.get("Code") == 200: - # 授权码在 Data 字段的列表中 - data = response_data.get("Data") - if data: - # 新返回格式 - if isinstance(data.get("authKeys"), list) and data["authKeys"]: - self.auth_key = data["authKeys"][0] - # 兼容旧版接口 - elif isinstance(data, list) and data: - self.auth_key = data[0] - - if self.auth_key: - logger.info(f"成功获取授权码 {self.auth_key[:8]}...") - else: - logger.error(f"生成授权码成功但未找到授权码: {response_data}") + if response_data.get("Code") == 200: + if data := response_data.get("Data"): + self.auth_key = self._extract_auth_key(data) + + if self.auth_key: + logger.info("成功获取授权码") else: logger.error(f"生成授权码成功但未找到授权码: {response_data}") else: - logger.error(f"生成授权码失败: {response.status}, {response_data}") + logger.error(f"生成授权码失败: {response_data}") except aiohttp.ClientConnectorError as e: logger.error(f"连接到 WeChatPadPro 服务失败: {e}") except Exception as e: From 20f49890ad6d7a615db2acda8e6adb95ebb6f07f Mon Sep 17 00:00:00 2001 From: Soulter <905617992@qq.com> Date: Fri, 4 Jul 2025 16:59:49 +0800 Subject: [PATCH 35/54] fix: provider selection for updating webchat title --- astrbot/core/pipeline/process_stage/method/llm_request.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/astrbot/core/pipeline/process_stage/method/llm_request.py b/astrbot/core/pipeline/process_stage/method/llm_request.py index 5923e8246..4027dcd7e 100644 --- a/astrbot/core/pipeline/process_stage/method/llm_request.py +++ b/astrbot/core/pipeline/process_stage/method/llm_request.py @@ -254,11 +254,11 @@ class LLMRequestSubStage(Stage): # 异步处理 WebChat 特殊情况 if event.get_platform_name() == "webchat": - asyncio.create_task(self._handle_webchat(event, req)) + asyncio.create_task(self._handle_webchat(event, req, provider)) await self._save_to_history(event, req, tool_loop_agent.get_final_llm_resp()) - async def _handle_webchat(self, event: AstrMessageEvent, req: ProviderRequest): + async def _handle_webchat(self, event: AstrMessageEvent, req: ProviderRequest, prov: Provider): """处理 WebChat 平台的特殊情况,包括第一次 LLM 对话时总结对话内容生成 title""" conversation = await self.conv_manager.get_conversation( event.unified_msg_origin, req.conversation.cid @@ -268,10 +268,9 @@ class LLMRequestSubStage(Stage): latest_pair = messages[-2:] if not latest_pair: return - provider = self.ctx.plugin_manager.context.get_using_provider() cleaned_text = "User: " + latest_pair[0].get("content", "").strip() logger.debug(f"WebChat 对话标题生成请求,清理后的文本: {cleaned_text}") - llm_resp = await provider.text_chat( + llm_resp = await prov.text_chat( system_prompt="You are expert in summarizing user's query.", prompt=( f"Please summarize the following query of user:\n" From 67ef993d617162a0c660bebf87858b033494ad4e Mon Sep 17 00:00:00 2001 From: Soulter <905617992@qq.com> Date: Sun, 6 Jul 2025 17:21:57 +0800 Subject: [PATCH 36/54] fix: webchat message bubble style --- dashboard/src/views/ChatPage.vue | 1 - 1 file changed, 1 deletion(-) diff --git a/dashboard/src/views/ChatPage.vue b/dashboard/src/views/ChatPage.vue index 1aa8bb9e0..fedb86b3e 100644 --- a/dashboard/src/views/ChatPage.vue +++ b/dashboard/src/views/ChatPage.vue @@ -1473,7 +1473,6 @@ export default { .message-bubble { padding: 8px 16px; border-radius: 12px; - max-width: 80%; } .user-bubble { From 69d3ae709c23771f16212ef3e25463cc12aed0cb Mon Sep 17 00:00:00 2001 From: Raven95676 Date: Sat, 5 Jul 2025 22:08:17 +0800 Subject: [PATCH 37/54] fix: direct send tool_call_result --- .../agent_runner/tool_loop_agent.py | 1 - .../process_stage/method/llm_request.py | 13 +++++++------ dashboard/src/views/ChatPage.vue | 1 - packages/astrbot/main.py | 18 +++++++++++++++++- 4 files changed, 24 insertions(+), 9 deletions(-) diff --git a/astrbot/core/pipeline/process_stage/agent_runner/tool_loop_agent.py b/astrbot/core/pipeline/process_stage/agent_runner/tool_loop_agent.py index dcb390b2f..3f97c189c 100644 --- a/astrbot/core/pipeline/process_stage/agent_runner/tool_loop_agent.py +++ b/astrbot/core/pipeline/process_stage/agent_runner/tool_loop_agent.py @@ -106,7 +106,6 @@ class ToolLoopAgent(BaseAgentRunner): # 处理 LLM 响应 llm_resp = llm_resp_result - logger.debug(f"LLMResp: {llm_resp}") if llm_resp.role == "err": # 如果 LLM 响应错误,转换到错误状态 diff --git a/astrbot/core/pipeline/process_stage/method/llm_request.py b/astrbot/core/pipeline/process_stage/method/llm_request.py index 5923e8246..000369326 100644 --- a/astrbot/core/pipeline/process_stage/method/llm_request.py +++ b/astrbot/core/pipeline/process_stage/method/llm_request.py @@ -177,7 +177,10 @@ class LLMRequestSubStage(Stage): if event.is_stopped(): return if resp.type == "tool_call_result": - continue # 跳过工具调用结果 + # 处理工具调用结果,直接发送给用户 + resp.data["chain"].type = "tool_call_result" + await event.send(resp.data["chain"]) + continue if resp.type == "tool_call": if self.streaming_response: # 用来标记流式响应需要分节 @@ -254,11 +257,11 @@ class LLMRequestSubStage(Stage): # 异步处理 WebChat 特殊情况 if event.get_platform_name() == "webchat": - asyncio.create_task(self._handle_webchat(event, req)) + asyncio.create_task(self._handle_webchat(event, req, provider)) await self._save_to_history(event, req, tool_loop_agent.get_final_llm_resp()) - async def _handle_webchat(self, event: AstrMessageEvent, req: ProviderRequest): + async def _handle_webchat(self, event: AstrMessageEvent, req: ProviderRequest, prov: Provider): """处理 WebChat 平台的特殊情况,包括第一次 LLM 对话时总结对话内容生成 title""" conversation = await self.conv_manager.get_conversation( event.unified_msg_origin, req.conversation.cid @@ -268,10 +271,9 @@ class LLMRequestSubStage(Stage): latest_pair = messages[-2:] if not latest_pair: return - provider = self.ctx.plugin_manager.context.get_using_provider() cleaned_text = "User: " + latest_pair[0].get("content", "").strip() logger.debug(f"WebChat 对话标题生成请求,清理后的文本: {cleaned_text}") - llm_resp = await provider.text_chat( + llm_resp = await prov.text_chat( system_prompt="You are expert in summarizing user's query.", prompt=( f"Please summarize the following query of user:\n" @@ -333,7 +335,6 @@ class LLMRequestSubStage(Stage): await self.conv_manager.update_conversation( event.unified_msg_origin, req.conversation.cid, history=messages ) - logger.debug(f"messages persisted: {messages}") def fix_messages(self, messages: list[dict]) -> list[dict]: """验证并且修复上下文""" diff --git a/dashboard/src/views/ChatPage.vue b/dashboard/src/views/ChatPage.vue index 1aa8bb9e0..fedb86b3e 100644 --- a/dashboard/src/views/ChatPage.vue +++ b/dashboard/src/views/ChatPage.vue @@ -1473,7 +1473,6 @@ export default { .message-bubble { padding: 8px 16px; border-radius: 12px; - max-width: 80%; } .user-bubble { diff --git a/packages/astrbot/main.py b/packages/astrbot/main.py index 0a3f8ba6c..d2a78d609 100644 --- a/packages/astrbot/main.py +++ b/packages/astrbot/main.py @@ -10,6 +10,7 @@ import astrbot.api.event.filter as filter from astrbot.api.event import AstrMessageEvent, MessageEventResult from astrbot.api import sp from astrbot.api.provider import ProviderRequest +from astrbot.core import DEMO_MODE from astrbot.core.platform.astr_message_event import MessageSesion from astrbot.core.platform.message_type import MessageType from astrbot.core.provider.entities import ProviderType @@ -59,7 +60,7 @@ class RstScene(Enum): name="astrbot", desc="AstrBot 基础指令结合 + 拓展功能", author="Soulter", - version="4.0.0", + version="4.0.1", ) class Main(star.Star): def __init__(self, context: star.Context) -> None: @@ -233,6 +234,11 @@ class Main(star.Star): @plugin.command("off") async def plugin_off(self, event: AstrMessageEvent, plugin_name: str = None): """禁用插件""" + if DEMO_MODE: + event.set_result( + MessageEventResult().message("演示模式下无法禁用插件。") + ) + return if not plugin_name: event.set_result( MessageEventResult().message("/plugin off <插件名> 禁用插件。") @@ -245,6 +251,11 @@ class Main(star.Star): @plugin.command("on") async def plugin_on(self, event: AstrMessageEvent, plugin_name: str = None): """启用插件""" + if DEMO_MODE: + event.set_result( + MessageEventResult().message("演示模式下无法启用插件。") + ) + return if not plugin_name: event.set_result( MessageEventResult().message("/plugin on <插件名> 启用插件。") @@ -257,6 +268,11 @@ class Main(star.Star): @plugin.command("get") async def plugin_get(self, event: AstrMessageEvent, plugin_repo: str = None): """安装插件""" + if DEMO_MODE: + event.set_result( + MessageEventResult().message("演示模式下无法安装插件。") + ) + return if not plugin_repo: event.set_result( MessageEventResult().message("/plugin get <插件仓库地址> 安装插件") From cd722a0e39b4ca2aa83332f79dc986f7d213f616 Mon Sep 17 00:00:00 2001 From: Soulter <905617992@qq.com> Date: Sun, 6 Jul 2025 18:04:46 +0800 Subject: [PATCH 38/54] fix: handle direct tool call results --- .../process_stage/agent_runner/tool_loop_agent.py | 12 +++++++++--- .../pipeline/process_stage/method/llm_request.py | 15 ++++++++++----- 2 files changed, 19 insertions(+), 8 deletions(-) diff --git a/astrbot/core/pipeline/process_stage/agent_runner/tool_loop_agent.py b/astrbot/core/pipeline/process_stage/agent_runner/tool_loop_agent.py index 3f97c189c..c2961ded5 100644 --- a/astrbot/core/pipeline/process_stage/agent_runner/tool_loop_agent.py +++ b/astrbot/core/pipeline/process_stage/agent_runner/tool_loop_agent.py @@ -218,7 +218,9 @@ class ToolLoopAgent(BaseAgentRunner): content="返回了图片(已直接发送给用户)", ) ) - yield MessageChain().base64_image(res.content[0].data) + yield MessageChain(type="tool_direct_result").base64_image( + res.content[0].data + ) elif isinstance(res.content[0], EmbeddedResource): resource = res.content[0].resource if isinstance(resource, TextResourceContents): @@ -242,7 +244,9 @@ class ToolLoopAgent(BaseAgentRunner): content="返回了图片(已直接发送给用户)", ) ) - yield MessageChain().base64_image(res.content[0].data) + yield MessageChain(type="tool_direct_result").base64_image( + res.content[0].data + ) else: tool_call_result_blocks.append( ToolCallMessageSegment( @@ -275,7 +279,9 @@ class ToolLoopAgent(BaseAgentRunner): self._transition_state(AgentState.DONE) if res := self.event.get_result(): if res.chain: - yield MessageChain(chain=res.chain) + yield MessageChain( + chain=res.chain, type="tool_direct_result" + ) self.event.clear_result() except Exception as e: diff --git a/astrbot/core/pipeline/process_stage/method/llm_request.py b/astrbot/core/pipeline/process_stage/method/llm_request.py index 000369326..b1d9310c6 100644 --- a/astrbot/core/pipeline/process_stage/method/llm_request.py +++ b/astrbot/core/pipeline/process_stage/method/llm_request.py @@ -177,10 +177,13 @@ class LLMRequestSubStage(Stage): if event.is_stopped(): return if resp.type == "tool_call_result": - # 处理工具调用结果,直接发送给用户 - resp.data["chain"].type = "tool_call_result" - await event.send(resp.data["chain"]) - continue + msg_chain = resp.data["chain"] + if msg_chain.type == "tool_direct_result": + # tool_direct_result 用于标记 llm tool 需要直接发送给用户的内容 + resp.data["chain"].type = "tool_call_result" + await event.send(resp.data["chain"]) + continue + # 对于其他情况,暂时先不处理 if resp.type == "tool_call": if self.streaming_response: # 用来标记流式响应需要分节 @@ -261,7 +264,9 @@ class LLMRequestSubStage(Stage): await self._save_to_history(event, req, tool_loop_agent.get_final_llm_resp()) - async def _handle_webchat(self, event: AstrMessageEvent, req: ProviderRequest, prov: Provider): + async def _handle_webchat( + self, event: AstrMessageEvent, req: ProviderRequest, prov: Provider + ): """处理 WebChat 平台的特殊情况,包括第一次 LLM 对话时总结对话内容生成 title""" conversation = await self.conv_manager.get_conversation( event.unified_msg_origin, req.conversation.cid From 7512bfc7107f3e60b6d62c88bbf7214990f3ded4 Mon Sep 17 00:00:00 2001 From: Soulter <905617992@qq.com> Date: Sun, 6 Jul 2025 18:06:25 +0800 Subject: [PATCH 39/54] fix: update user message bubble styling for improved appearance --- dashboard/src/views/ChatPage.vue | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/dashboard/src/views/ChatPage.vue b/dashboard/src/views/ChatPage.vue index fedb86b3e..2a17473f1 100644 --- a/dashboard/src/views/ChatPage.vue +++ b/dashboard/src/views/ChatPage.vue @@ -1477,8 +1477,10 @@ export default { .user-bubble { color: var(--v-theme-primaryText); - padding: 12px 16px; + padding: 18px 20px; font-size: 16px; + max-width: 60%; + border-radius: 1.5rem; } .bot-bubble { From 14a8bb57df5e7085489a3f8939515534b595fec0 Mon Sep 17 00:00:00 2001 From: IGCrystal Date: Fri, 4 Jul 2025 22:39:32 +0800 Subject: [PATCH 40/54] =?UTF-8?q?=F0=9F=90=9E=20fix(WebUI):=20=E8=A7=A3?= =?UTF-8?q?=E5=86=B3XSS=E6=B3=A8=E5=85=A5=E7=9A=84=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- dashboard/package.json | 1 + .../src/components/shared/ReadmeDialog.vue | 41 +++++++++---------- .../full/vertical-header/VerticalHeader.vue | 12 +++++- dashboard/src/views/ChatPage.vue | 13 +++--- dashboard/src/views/ConversationPage.vue | 15 ++++--- 5 files changed, 49 insertions(+), 33 deletions(-) diff --git a/dashboard/package.json b/dashboard/package.json index 7a5dd44a5..4f7ca6753 100644 --- a/dashboard/package.json +++ b/dashboard/package.json @@ -26,6 +26,7 @@ "js-md5": "^0.8.3", "lodash": "4.17.21", "marked": "^15.0.7", + "markdown-it": "^14.1.0", "pinia": "2.1.6", "remixicon": "3.5.0", "vee-validate": "4.11.3", diff --git a/dashboard/src/components/shared/ReadmeDialog.vue b/dashboard/src/components/shared/ReadmeDialog.vue index 739b9a4dd..4d1a7a258 100644 --- a/dashboard/src/components/shared/ReadmeDialog.vue +++ b/dashboard/src/components/shared/ReadmeDialog.vue @@ -1,7 +1,7 @@ + + diff --git a/dashboard/src/components/shared/ItemCardGrid.vue b/dashboard/src/components/shared/ItemCardGrid.vue index 71841d2ab..5176c186b 100644 --- a/dashboard/src/components/shared/ItemCardGrid.vue +++ b/dashboard/src/components/shared/ItemCardGrid.vue @@ -9,10 +9,10 @@ - +
- {{ getItemTitle(item) }} + {{ getItemTitle(item) }}