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": "" 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 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..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: @@ -21,25 +22,24 @@ jobs: fetch-depth: 0 - name: Set up Python - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 - 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=. -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 + token: ${{ secrets.CODECOV_TOKEN }} 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' diff --git a/astrbot/core/config/default.py b/astrbot/core/config/default.py index 4a75a11bc..b3982cb13 100644 --- a/astrbot/core/config/default.py +++ b/astrbot/core/config/default.py @@ -6,7 +6,7 @@ import os from astrbot.core.utils.astrbot_path import get_astrbot_data_path -VERSION = "3.5.18" +VERSION = "3.5.19" DB_PATH = os.path.join(get_astrbot_data_path(), "data_v3.db") # 默认配置 @@ -64,7 +64,7 @@ DEFAULT_CONFIG = { "streaming_response": False, "show_tool_use_status": False, "streaming_segmented": False, - "separate_provider": False, + "separate_provider": True, }, "provider_stt_settings": { "enable": False, @@ -724,16 +724,16 @@ CONFIG_METADATA_2 = { "model": "deepseek-chat", }, }, - "智谱 AI": { - "id": "zhipu_default", - "type": "zhipu_chat_completion", + "302.AI": { + "id": "302ai", + "type": "openai_chat_completion", "provider_type": "chat_completion", "enable": True, "key": [], + "api_base": "https://api.302.ai/v1", "timeout": 120, - "api_base": "https://open.bigmodel.cn/api/paas/v4/", "model_config": { - "model": "glm-4-flash", + "model": "gpt-4.1-mini", }, }, "硅基流动": { @@ -748,6 +748,18 @@ CONFIG_METADATA_2 = { "model": "deepseek-ai/DeepSeek-V3", }, }, + "PPIO派欧云": { + "id": "ppio", + "type": "openai_chat_completion", + "provider_type": "chat_completion", + "enable": True, + "key": [], + "api_base": "https://api.ppinfra.com/v3/openai", + "timeout": 120, + "model_config": { + "model": "deepseek/deepseek-r1", + }, + }, "Kimi": { "id": "moonshot", "type": "openai_chat_completion", @@ -760,16 +772,16 @@ CONFIG_METADATA_2 = { "model": "moonshot-v1-8k", }, }, - "PPIO派欧云": { - "id": "ppio", - "type": "openai_chat_completion", + "智谱 AI": { + "id": "zhipu_default", + "type": "zhipu_chat_completion", "provider_type": "chat_completion", "enable": True, "key": [], - "api_base": "https://api.ppinfra.com/v3/openai", "timeout": 120, + "api_base": "https://open.bigmodel.cn/api/paas/v4/", "model_config": { - "model": "deepseek/deepseek-r1", + "model": "glm-4-flash", }, }, "Dify": { 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..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 @@ -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 响应错误,转换到错误状态 @@ -219,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): @@ -243,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( @@ -276,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 5923e8246..b1d9310c6 100644 --- a/astrbot/core/pipeline/process_stage/method/llm_request.py +++ b/astrbot/core/pipeline/process_stage/method/llm_request.py @@ -177,7 +177,13 @@ class LLMRequestSubStage(Stage): if event.is_stopped(): return if resp.type == "tool_call_result": - 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: # 用来标记流式响应需要分节 @@ -254,11 +260,13 @@ 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 +276,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 +340,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/astrbot/core/platform/sources/wechatpadpro/wechatpadpro_adapter.py b/astrbot/core/platform/sources/wechatpadpro/wechatpadpro_adapter.py index 58e3c9b19..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,28 +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 字段的列表中 - 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]}...") + 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}" - ) + 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: diff --git a/astrbot/core/provider/manager.py b/astrbot/core/provider/manager.py index 2abe59d65..05747c3ff 100644 --- a/astrbot/core/provider/manager.py +++ b/astrbot/core/provider/manager.py @@ -40,11 +40,13 @@ class ProviderManager: begin_dialogs = [] user_turn = True for dialog in begin_dialogs: - bd_processed.append({ - "role": "user" if user_turn else "assistant", - "content": dialog, - "_no_save": None, # 不持久化到 db - }) + bd_processed.append( + { + "role": "user" if user_turn else "assistant", + "content": dialog, + "_no_save": None, # 不持久化到 db + } + ) user_turn = not user_turn if mood_imitation_dialogs: if len(mood_imitation_dialogs) % 2 != 0: @@ -93,15 +95,15 @@ class ProviderManager: """加载的 Text To Speech Provider 的实例""" self.embedding_provider_insts: List[Provider] = [] """加载的 Embedding Provider 的实例""" - self.inst_map = {} + self.inst_map: dict[str, Provider] = {} """Provider 实例映射. key: provider_id, value: Provider 实例""" self.llm_tools = llm_tools - self.curr_provider_inst: Provider = None + self.curr_provider_inst: Provider | None = None """默认的 Provider 实例""" - self.curr_stt_provider_inst: STTProvider = None + self.curr_stt_provider_inst: STTProvider | None = None """默认的 Speech To Text Provider 实例""" - self.curr_tts_provider_inst: TTSProvider = None + self.curr_tts_provider_inst: TTSProvider | None = None """默认的 Text To Speech Provider 实例""" self.db_helper = db_helper @@ -145,21 +147,24 @@ class ProviderManager: await self.load_provider(provider_config) # 设置默认提供商 - self.curr_provider_inst = self.inst_map.get( - self.provider_settings.get("default_provider_id") + selected_provider_id = sp.get( + "curr_provider", self.provider_settings.get("default_provider_id") ) + selected_stt_provider_id = sp.get( + "curr_provider_stt", self.provider_stt_settings.get("provider_id") + ) + selected_tts_provider_id = sp.get( + "curr_provider_tts", self.provider_tts_settings.get("provider_id") + ) + self.curr_provider_inst = self.inst_map.get(selected_provider_id) if not self.curr_provider_inst and self.provider_insts: self.curr_provider_inst = self.provider_insts[0] - self.curr_stt_provider_inst = self.inst_map.get( - self.provider_stt_settings.get("provider_id") - ) + self.curr_stt_provider_inst = self.inst_map.get(selected_stt_provider_id) if not self.curr_stt_provider_inst and self.stt_provider_insts: self.curr_stt_provider_inst = self.stt_provider_insts[0] - self.curr_tts_provider_inst = self.inst_map.get( - self.provider_tts_settings.get("provider_id") - ) + self.curr_tts_provider_inst = self.inst_map.get(selected_tts_provider_id) if not self.curr_tts_provider_inst and self.tts_provider_insts: self.curr_tts_provider_inst = self.tts_provider_insts[0] @@ -417,7 +422,7 @@ class ProviderManager: self.curr_tts_provider_inst = None if getattr(self.inst_map[provider_id], "terminate", None): - await self.inst_map[provider_id].terminate() + await self.inst_map[provider_id].terminate() # type: ignore logger.info( f"{provider_id} 提供商适配器已终止({len(self.provider_insts)}, {len(self.stt_provider_insts)}, {len(self.tts_provider_insts)})" @@ -427,6 +432,6 @@ class ProviderManager: async def terminate(self): for provider_inst in self.provider_insts: if hasattr(provider_inst, "terminate"): - await provider_inst.terminate() + await provider_inst.terminate() # type: ignore # 清理 MCP Client 连接 await self.llm_tools.mcp_service_queue.put({"type": "terminate"}) diff --git a/astrbot/core/provider/sources/openai_source.py b/astrbot/core/provider/sources/openai_source.py index 36a8579a6..ec1624776 100644 --- a/astrbot/core/provider/sources/openai_source.py +++ b/astrbot/core/provider/sources/openai_source.py @@ -487,7 +487,7 @@ class ProviderOpenAIOfficial(Provider): if flag: flag = False # 删除 image 后,下一条(LLM 响应)也要删除 continue - if isinstance(context["content"], list): + if "content" in context and isinstance(context["content"], list): flag = True # continue new_content = [] diff --git a/astrbot/core/star/filter/platform_adapter_type.py b/astrbot/core/star/filter/platform_adapter_type.py index 0926cc337..fffaf8553 100644 --- a/astrbot/core/star/filter/platform_adapter_type.py +++ b/astrbot/core/star/filter/platform_adapter_type.py @@ -8,22 +8,48 @@ from typing import Union class PlatformAdapterType(enum.Flag): AIOCQHTTP = enum.auto() QQOFFICIAL = enum.auto() - VCHAT = enum.auto() GEWECHAT = enum.auto() TELEGRAM = enum.auto() WECOM = enum.auto() LARK = enum.auto() - ALL = AIOCQHTTP | QQOFFICIAL | VCHAT | GEWECHAT | TELEGRAM | WECOM | LARK + WECHATPADPRO = enum.auto() + DINGTALK = enum.auto() + DISCORD = enum.auto() + SLACK = enum.auto() + KOOK = enum.auto() + VOCECHAT = enum.auto() + WEIXIN_OFFICIAL_ACCOUNT = enum.auto() + ALL = ( + AIOCQHTTP + | QQOFFICIAL + | GEWECHAT + | TELEGRAM + | WECOM + | LARK + | WECHATPADPRO + | DINGTALK + | DISCORD + | SLACK + | KOOK + | VOCECHAT + | WEIXIN_OFFICIAL_ACCOUNT + ) ADAPTER_NAME_2_TYPE = { "aiocqhttp": PlatformAdapterType.AIOCQHTTP, "qq_official": PlatformAdapterType.QQOFFICIAL, - "vchat": PlatformAdapterType.VCHAT, "gewechat": PlatformAdapterType.GEWECHAT, "telegram": PlatformAdapterType.TELEGRAM, "wecom": PlatformAdapterType.WECOM, "lark": PlatformAdapterType.LARK, + "dingtalk": PlatformAdapterType.DINGTALK, + "discord": PlatformAdapterType.DISCORD, + "slack": PlatformAdapterType.SLACK, + "kook": PlatformAdapterType.KOOK, + "wechatpadpro": PlatformAdapterType.WECHATPADPRO, + "vocechat": PlatformAdapterType.VOCECHAT, + "weixin_official_account": PlatformAdapterType.WEIXIN_OFFICIAL_ACCOUNT, } 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) 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 diff --git a/changelogs/v3.5.19.md b/changelogs/v3.5.19.md new file mode 100644 index 000000000..cb821cef6 --- /dev/null +++ b/changelogs/v3.5.19.md @@ -0,0 +1,10 @@ +# What's Changed + +1. 修复: 通过 provider 指令设置提供商,重启后失效 +2. 新增: WebChat 支持直接选择提供商和模型 +3. 优化: WebUI 视觉效果、WebChat 视觉效果 +4. 优化: WebUI 测试提供商功能 +5. 优化: 修复潜在的 README XSS 注入问题 +6. 修复: WechatPadPro 授权码提取逻辑以适配上游新版本,并提高安全性 +7. 修复: Gemini 下,多轮工具调用时可能报错的问题 +8. 其他修复与优化 \ No newline at end of file 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/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 = () => { + 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) }}