Merge branch 'Soulter:master' into master

This commit is contained in:
IGCrystal
2025-03-15 12:05:27 +08:00
committed by GitHub
12 changed files with 107 additions and 25 deletions
+2
View File
@@ -26,3 +26,5 @@ venv/*
packages/python_interpreter/workplace
.venv/*
.conda/
.idea/
pytest.ini
+4 -1
View File
@@ -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",
@@ -148,11 +148,18 @@ 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(
@@ -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:///"):
@@ -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,11 @@ 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,
)
@@ -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,11 @@ 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,
)
+4 -1
View File
@@ -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)
+1 -1
View File
@@ -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:
"""
+55 -6
View File
@@ -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] = ""
+3 -1
View File
@@ -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, md.handler)
llm_tools.add_func(
llm_tool_name, args, docstring.description.strip(), md.handler
)
return awaitable
return decorator
+1 -1
View File
@@ -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}",
}
+14 -6
View File
@@ -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,16 @@ 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,
)