From 3a1578b3c6a4b5b7791af7d28e9c6b5139b128b2 Mon Sep 17 00:00:00 2001 From: Soulter <905617992@qq.com> Date: Sat, 15 Mar 2025 00:51:32 +0800 Subject: [PATCH 1/5] =?UTF-8?q?=E2=9C=A8feat:=20=E6=94=AF=E6=8C=81=20Dify?= =?UTF-8?q?=20=E6=96=87=E4=BB=B6=E3=80=81=E5=9B=BE=E7=89=87=E3=80=81?= =?UTF-8?q?=E8=A7=86=E9=A2=91=E3=80=81=E9=9F=B3=E9=A2=91=E8=BE=93=E5=87=BA?= =?UTF-8?q?=E3=80=82#819?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../process_stage/method/llm_request.py | 16 +++-- astrbot/core/provider/entites.py | 5 +- astrbot/core/provider/sources/dify_source.py | 61 +++++++++++++++++-- astrbot/core/utils/dify_api_client.py | 2 +- 4 files changed, 71 insertions(+), 13 deletions(-) diff --git a/astrbot/core/pipeline/process_stage/method/llm_request.py b/astrbot/core/pipeline/process_stage/method/llm_request.py index 22d67d322..d168d1fb0 100644 --- a/astrbot/core/pipeline/process_stage/method/llm_request.py +++ b/astrbot/core/pipeline/process_stage/method/llm_request.py @@ -148,11 +148,17 @@ class LLMRequestSubStage(Stage): if llm_response.role == "assistant": # text completion - event.set_result( - MessageEventResult() - .message(llm_response.completion_text) - .set_result_content_type(ResultContentType.LLM_RESULT) - ) + if llm_response.result_chain: + event.set_result( + MessageEventResult(chain=llm_response.result_chain.chain) + .set_result_content_type(ResultContentType.LLM_RESULT) + ) + else: + event.set_result( + MessageEventResult() + .message(llm_response.completion_text) + .set_result_content_type(ResultContentType.LLM_RESULT) + ) elif llm_response.role == "err": event.set_result( MessageEventResult().message( diff --git a/astrbot/core/provider/entites.py b/astrbot/core/provider/entites.py index 3180b4955..17b6edd46 100644 --- a/astrbot/core/provider/entites.py +++ b/astrbot/core/provider/entites.py @@ -4,6 +4,7 @@ from typing import List, Dict, Type from .func_tool_manager import FuncCall from openai.types.chat.chat_completion import ChatCompletion from astrbot.core.db.po import Conversation +from astrbot.core.message.message_event_result import MessageChain class ProviderType(enum.Enum): @@ -56,8 +57,10 @@ class ProviderRequest: class LLMResponse: role: str """角色, assistant, tool, err""" + result_chain: MessageChain = None + """返回的消息链""" completion_text: str = "" - """LLM 返回的文本""" + """LLM 返回的文本, 已经废弃但仍然兼容。使用 result_chain 替代""" tools_call_args: List[Dict[str, any]] = field(default_factory=list) """工具调用参数""" tools_call_name: List[str] = field(default_factory=list) diff --git a/astrbot/core/provider/sources/dify_source.py b/astrbot/core/provider/sources/dify_source.py index 37f575f21..9af198aa7 100644 --- a/astrbot/core/provider/sources/dify_source.py +++ b/astrbot/core/provider/sources/dify_source.py @@ -1,3 +1,5 @@ +import astrbot.core.message.components as Comp + from typing import List from .. import Provider, Personality from ..entites import LLMResponse @@ -5,8 +7,9 @@ from ..func_tool_manager import FuncCall from astrbot.core.db import BaseDatabase from ..register import register_provider_adapter from astrbot.core.utils.dify_api_client import DifyAPIClient -from astrbot.core.utils.io import download_image_by_url +from astrbot.core.utils.io import download_image_by_url, download_file from astrbot.core import logger, sp +from astrbot.core.message.message_event_result import MessageChain @register_provider_adapter("dify", "Dify APP 适配器。") @@ -148,8 +151,9 @@ class ProviderDify(Provider): ) case "workflow_finished": logger.info( - f"Dify 工作流(ID: {chunk['workflow_run_id']})运行结束。" + f"Dify 工作流(ID: {chunk['workflow_run_id']})运行结束" ) + logger.debug(f"Dify 工作流结果:{chunk}") if chunk["data"]["error"]: logger.error( f"Dify 工作流出现错误:{chunk['data']['error']}" @@ -164,9 +168,7 @@ class ProviderDify(Provider): raise Exception( f"Dify 工作流的输出不包含指定的键名:{self.workflow_output_key}" ) - result = chunk["data"]["outputs"][ - self.workflow_output_key - ] + result = chunk case _: raise Exception(f"未知的 Dify API 类型:{self.api_type}") except Exception as e: @@ -176,7 +178,54 @@ class ProviderDify(Provider): if not result: logger.warning("Dify 请求结果为空,请查看 Debug 日志。") - return LLMResponse(role="assistant", completion_text=result) + chain = await self.parse_dify_result(result) + + return LLMResponse(role="assistant", result_chain=chain) + + async def parse_dify_result(self, chunk: dict | str) -> MessageChain: + if isinstance(chunk, str): + # Chat + return MessageChain(chain=[Comp.Plain(chunk)]) + + async def parse_file(item: dict) -> Comp: + match item["type"]: + case "image": + return Comp.Image(file=item["url"], url=item["url"]) + case "audio": + # 仅支持 wav + path = f"data/temp/{item['filename']}.wav" + await download_file(item["url"], path) + return Comp.Image(file=item["url"], url=item["url"]) + case "video": + return Comp.Video(file=item["url"]) + case _: + return Comp.File(name=item["filename"], file=item["url"]) + + output = chunk["data"]["outputs"][self.workflow_output_key] + chains = [] + if isinstance(output, str): + # 纯文本输出 + chains.append(Comp.Plain(output)) + elif isinstance(output, list): + # 主要适配 Dify 的 HTTP 请求结点的多模态输出 + for item in output: + # handle Array[File] + if ( + not isinstance(item, dict) + or item.get("dify_model_identity", "") != "__dify__file__" + ): + chains.append(Comp.Plain(str(output))) + break + else: + chains.append(Comp.Plain(str(output))) + + # scan file + files = chunk["data"].get("files", []) + for item in files: + comp = await parse_file(item) + chains.append(comp) + + return MessageChain(chain=chains) async def forget(self, session_id): self.conversation_ids[session_id] = "" diff --git a/astrbot/core/utils/dify_api_client.py b/astrbot/core/utils/dify_api_client.py index 80be3fff7..badf5d62b 100644 --- a/astrbot/core/utils/dify_api_client.py +++ b/astrbot/core/utils/dify_api_client.py @@ -8,7 +8,7 @@ class DifyAPIClient: def __init__(self, api_key: str, api_base: str = "https://api.dify.ai/v1"): self.api_key = api_key self.api_base = api_base - self.session = ClientSession() + self.session = ClientSession(trust_env=True) self.headers = { "Authorization": f"Bearer {self.api_key}", } From 26e229867da613d0095e939c9763dad7668ed658 Mon Sep 17 00:00:00 2001 From: Soulter <905617992@qq.com> Date: Sat, 15 Mar 2025 00:57:17 +0800 Subject: [PATCH 2/5] =?UTF-8?q?=F0=9F=90=9Bfix:=20=E5=8F=AF=E8=83=BD?= =?UTF-8?q?=E7=9A=84QQ=E5=B9=B3=E5=8F=B0=E5=9B=9E=E5=A4=8D=E6=B6=88?= =?UTF-8?q?=E6=81=AF=E5=B8=A6=E6=9C=89=E6=9C=AB=E5=B0=BE=E7=A9=BA=E7=99=BD?= =?UTF-8?q?=E7=9A=84=E9=97=AE=E9=A2=98=20#822?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../core/platform/sources/aiocqhttp/aiocqhttp_message_event.py | 1 + 1 file changed, 1 insertion(+) diff --git a/astrbot/core/platform/sources/aiocqhttp/aiocqhttp_message_event.py b/astrbot/core/platform/sources/aiocqhttp/aiocqhttp_message_event.py index ce38296e6..dbe9a3ce0 100644 --- a/astrbot/core/platform/sources/aiocqhttp/aiocqhttp_message_event.py +++ b/astrbot/core/platform/sources/aiocqhttp/aiocqhttp_message_event.py @@ -21,6 +21,7 @@ class AiocqhttpMessageEvent(AstrMessageEvent): d = segment.toDict() if isinstance(segment, Plain): d["type"] = "text" + d["data"]["text"] = segment.text.strip() elif isinstance(segment, (Image, Record)): # convert to base64 if segment.file and segment.file.startswith("file:///"): From 35468233f8b1ef86b1a92908ec619e3c2d23d053 Mon Sep 17 00:00:00 2001 From: Soulter <905617992@qq.com> Date: Sat, 15 Mar 2025 01:21:36 +0800 Subject: [PATCH 3/5] =?UTF-8?q?=F0=9F=8E=88=20perf:=20supports=20for=20cus?= =?UTF-8?q?tomizing=20webui=20host,=20wecom=20webhook=20server=20host,=20q?= =?UTF-8?q?q=20official=20webhook=20server=20host=20#821?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- astrbot/core/config/default.py | 5 ++++- .../qqofficial_webhook/qo_webhook_server.py | 4 +++- .../platform/sources/wecom/wecom_adapter.py | 5 +++-- astrbot/dashboard/server.py | 18 ++++++++++++------ 4 files changed, 22 insertions(+), 10 deletions(-) diff --git a/astrbot/core/config/default.py b/astrbot/core/config/default.py index ba87ba528..280c841d0 100644 --- a/astrbot/core/config/default.py +++ b/astrbot/core/config/default.py @@ -85,6 +85,7 @@ DEFAULT_CONFIG = { "enable": True, "username": "astrbot", "password": "77b90590a8945a7d36c963981a307dc9", + "host": "127.0.0.1", "port": 6185, }, "platform": [], @@ -122,6 +123,7 @@ CONFIG_METADATA_2 = { "enable": False, "appid": "", "secret": "", + "callback_server_host": "0.0.0.0", "port": 6196, }, "aiocqhttp(OneBotv11)": { @@ -146,10 +148,11 @@ CONFIG_METADATA_2 = { "enable": False, "corpid": "", "secret": "", - "port": 6195, "token": "", "encoding_aes_key": "", "api_base_url": "https://qyapi.weixin.qq.com/cgi-bin/", + "callback_server_host": "0.0.0.0", + "port": 6195, }, "lark(飞书)": { "id": "lark", diff --git a/astrbot/core/platform/sources/qqofficial_webhook/qo_webhook_server.py b/astrbot/core/platform/sources/qqofficial_webhook/qo_webhook_server.py index 562574204..dcd160d27 100644 --- a/astrbot/core/platform/sources/qqofficial_webhook/qo_webhook_server.py +++ b/astrbot/core/platform/sources/qqofficial_webhook/qo_webhook_server.py @@ -15,6 +15,7 @@ class QQOfficialWebhook: self.appid = config["appid"] self.secret = config["secret"] self.port = config.get("port", 6196) + self.callback_server_host = config.get("callback_server_host", "0.0.0.0") if isinstance(self.port, str): self.port = int(self.port) @@ -95,8 +96,9 @@ class QQOfficialWebhook: return {"opcode": 12} async def start_polling(self): + logger.info(f"将在 {self.callback_server_host}:{self.port} 端口启动 QQ 官方机器人 webhook 适配器。") await self.server.run_task( - host="0.0.0.0", + host=self.callback_server_host, port=self.port, shutdown_trigger=self.shutdown_trigger_placeholder, ) diff --git a/astrbot/core/platform/sources/wecom/wecom_adapter.py b/astrbot/core/platform/sources/wecom/wecom_adapter.py index 77eae03d6..4237c2ebd 100644 --- a/astrbot/core/platform/sources/wecom/wecom_adapter.py +++ b/astrbot/core/platform/sources/wecom/wecom_adapter.py @@ -34,6 +34,7 @@ class WecomServer: def __init__(self, event_queue: asyncio.Queue, config: dict): self.server = quart.Quart(__name__) self.port = int(config.get("port")) + self.callback_server_host = config.get("callback_server_host", "0.0.0.0") self.server.add_url_rule( "/callback/command", view_func=self.verify, methods=["GET"] ) @@ -86,9 +87,9 @@ class WecomServer: return "success" async def start_polling(self): - logger.info(f"将在 0.0.0.0:{self.port} 端口启动 企业微信 适配器。") + logger.info(f"将在 {self.callback_server_host}:{self.port} 端口启动 企业微信 适配器。") await self.server.run_task( - host="0.0.0.0", + host=self.callback_server_host, port=self.port, shutdown_trigger=self.shutdown_trigger_placeholder, ) diff --git a/astrbot/dashboard/server.py b/astrbot/dashboard/server.py index 42bbad318..c836ea576 100644 --- a/astrbot/dashboard/server.py +++ b/astrbot/dashboard/server.py @@ -120,12 +120,14 @@ class AstrBotDashboard: return f"获取进程信息失败: {str(e)}" def run(self): - try: - ip_addr = get_local_ip_addresses() - except Exception as _: - ip_addr = [] - + ip_addr = [] port = self.core_lifecycle.astrbot_config["dashboard"].get("port", 6185) + host = self.core_lifecycle.astrbot_config["dashboard"].get("host", "127.0.0.1") + if host not in ["localhost", "127.0.0.1"]: + try: + ip_addr = get_local_ip_addresses() + except Exception as _: + pass if isinstance(port, str): port = int(port) @@ -147,10 +149,14 @@ class AstrBotDashboard: for ip in ip_addr: display += f" ➜ 网络: http://{ip}:{port}\n" display += " ➜ 默认用户名和密码: astrbot\n ✨✨✨\n" + + if not ip_addr: + display += "可在 data/cmd_config.json 中配置 dashboard.host 以便远程访问。\n" + logger.info(display) return self.app.run_task( - host="0.0.0.0", + host=host, port=port, shutdown_trigger=self.shutdown_trigger_placeholder, ) From ef86838f628de8541f809fef70c5b8af2b062792 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=82=B9=E6=B0=B8=E8=B5=AB?= <1259085392@qq.com> Date: Sat, 15 Mar 2025 12:15:05 +0900 Subject: [PATCH 4/5] =?UTF-8?q?=E4=BF=AE=E6=94=B9=E6=B3=A8=E5=86=8C?= =?UTF-8?q?=E5=87=BD=E6=95=B0=E5=B7=A5=E5=85=B7=E6=97=B6=E7=9A=84=E6=89=93?= =?UTF-8?q?=E5=8D=B0=E4=BF=A1=E6=81=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 2 ++ astrbot/core/provider/func_tool_manager.py | 2 +- astrbot/core/star/register/star_handler.py | 2 +- 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/.gitignore b/.gitignore index a863e36ec..865b0596d 100644 --- a/.gitignore +++ b/.gitignore @@ -26,3 +26,5 @@ venv/* packages/python_interpreter/workplace .venv/* .conda/ +.idea/ +pytest.ini diff --git a/astrbot/core/provider/func_tool_manager.py b/astrbot/core/provider/func_tool_manager.py index cdb1b3d6d..0f04628f7 100644 --- a/astrbot/core/provider/func_tool_manager.py +++ b/astrbot/core/provider/func_tool_manager.py @@ -73,7 +73,7 @@ class FuncCall: handler=handler, ) self.func_list.append(_func) - logger.info(f"添加了函数调用工具({len(self.func_list)}): {name} - {desc}") + logger.info(f"添加函数调用工具: {name}") def remove_func(self, name: str) -> None: """ diff --git a/astrbot/core/star/register/star_handler.py b/astrbot/core/star/register/star_handler.py index 77d7fb482..39e50c63d 100644 --- a/astrbot/core/star/register/star_handler.py +++ b/astrbot/core/star/register/star_handler.py @@ -358,7 +358,7 @@ def register_llm_tool(name: str = None): } ) md = get_handler_or_create(awaitable, EventType.OnCallingFuncToolEvent) - llm_tools.add_func(llm_tool_name, args, docstring.description, md.handler) + llm_tools.add_func(llm_tool_name, args, docstring.description.strip(), md.handler) return awaitable return decorator From 9d870092162cf888d1be9cc44616b70ac11d5182 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Sat, 15 Mar 2025 03:16:51 +0000 Subject: [PATCH 5/5] :balloon: auto fixes by pre-commit hooks --- astrbot/core/pipeline/process_stage/method/llm_request.py | 5 +++-- .../platform/sources/qqofficial_webhook/qo_webhook_server.py | 4 +++- astrbot/core/platform/sources/wecom/wecom_adapter.py | 4 +++- astrbot/core/star/register/star_handler.py | 4 +++- astrbot/dashboard/server.py | 4 +++- 5 files changed, 15 insertions(+), 6 deletions(-) diff --git a/astrbot/core/pipeline/process_stage/method/llm_request.py b/astrbot/core/pipeline/process_stage/method/llm_request.py index d168d1fb0..e8246805e 100644 --- a/astrbot/core/pipeline/process_stage/method/llm_request.py +++ b/astrbot/core/pipeline/process_stage/method/llm_request.py @@ -150,8 +150,9 @@ class LLMRequestSubStage(Stage): # text completion if llm_response.result_chain: event.set_result( - MessageEventResult(chain=llm_response.result_chain.chain) - .set_result_content_type(ResultContentType.LLM_RESULT) + MessageEventResult( + chain=llm_response.result_chain.chain + ).set_result_content_type(ResultContentType.LLM_RESULT) ) else: event.set_result( diff --git a/astrbot/core/platform/sources/qqofficial_webhook/qo_webhook_server.py b/astrbot/core/platform/sources/qqofficial_webhook/qo_webhook_server.py index dcd160d27..a219e2492 100644 --- a/astrbot/core/platform/sources/qqofficial_webhook/qo_webhook_server.py +++ b/astrbot/core/platform/sources/qqofficial_webhook/qo_webhook_server.py @@ -96,7 +96,9 @@ class QQOfficialWebhook: return {"opcode": 12} async def start_polling(self): - logger.info(f"将在 {self.callback_server_host}:{self.port} 端口启动 QQ 官方机器人 webhook 适配器。") + logger.info( + f"将在 {self.callback_server_host}:{self.port} 端口启动 QQ 官方机器人 webhook 适配器。" + ) await self.server.run_task( host=self.callback_server_host, port=self.port, diff --git a/astrbot/core/platform/sources/wecom/wecom_adapter.py b/astrbot/core/platform/sources/wecom/wecom_adapter.py index 4237c2ebd..cef83b030 100644 --- a/astrbot/core/platform/sources/wecom/wecom_adapter.py +++ b/astrbot/core/platform/sources/wecom/wecom_adapter.py @@ -87,7 +87,9 @@ class WecomServer: return "success" async def start_polling(self): - logger.info(f"将在 {self.callback_server_host}:{self.port} 端口启动 企业微信 适配器。") + logger.info( + f"将在 {self.callback_server_host}:{self.port} 端口启动 企业微信 适配器。" + ) await self.server.run_task( host=self.callback_server_host, port=self.port, diff --git a/astrbot/core/star/register/star_handler.py b/astrbot/core/star/register/star_handler.py index 39e50c63d..0b9f7ad09 100644 --- a/astrbot/core/star/register/star_handler.py +++ b/astrbot/core/star/register/star_handler.py @@ -358,7 +358,9 @@ def register_llm_tool(name: str = None): } ) md = get_handler_or_create(awaitable, EventType.OnCallingFuncToolEvent) - llm_tools.add_func(llm_tool_name, args, docstring.description.strip(), md.handler) + llm_tools.add_func( + llm_tool_name, args, docstring.description.strip(), md.handler + ) return awaitable return decorator diff --git a/astrbot/dashboard/server.py b/astrbot/dashboard/server.py index c836ea576..072ded4ae 100644 --- a/astrbot/dashboard/server.py +++ b/astrbot/dashboard/server.py @@ -151,7 +151,9 @@ class AstrBotDashboard: display += " ➜ 默认用户名和密码: astrbot\n ✨✨✨\n" if not ip_addr: - display += "可在 data/cmd_config.json 中配置 dashboard.host 以便远程访问。\n" + display += ( + "可在 data/cmd_config.json 中配置 dashboard.host 以便远程访问。\n" + ) logger.info(display)