refactor: centralize llm backend configuration
This commit is contained in:
@@ -4,14 +4,8 @@
|
||||
enabled = true
|
||||
|
||||
[api]
|
||||
api_key = "app-shCA6bo5l2VDmnvhg2BtuJbk"
|
||||
api_base_url = "http://192.168.2.240/v1"
|
||||
mode = "workflow"
|
||||
endpoint = "workflows/run"
|
||||
workflow_output_key = "text"
|
||||
response_mode = "streaming"
|
||||
backend = "dify_workflow_message_summary"
|
||||
connect_timeout_seconds = 10
|
||||
request_timeout_seconds = 180
|
||||
retry_delays_seconds = [10, 20]
|
||||
|
||||
[output]
|
||||
|
||||
@@ -6,8 +6,6 @@ from datetime import datetime, timedelta
|
||||
from pathlib import Path
|
||||
from typing import Dict, Any, Tuple, Optional, List
|
||||
|
||||
import aiohttp
|
||||
from aiohttp import ClientTimeout
|
||||
from loguru import logger
|
||||
|
||||
from base.plugin_common.message_plugin_interface import MessagePluginInterface
|
||||
@@ -22,6 +20,7 @@ from utils.markdown_to_image import convert_md_str_to_image
|
||||
from utils.revoke.message_auto_revoke import MessageAutoRevoke
|
||||
from utils.robot_cmd.robot_command import GroupBotManager, PermissionStatus
|
||||
from utils.string_utils import remove_reasoning_content, remove_trailing_content
|
||||
from utils.ai.unified_llm import UnifiedLLMClient
|
||||
from utils.wechat.contact_manager import ContactManager
|
||||
from utils.wechat.message_to_db import MessageStorage
|
||||
from wechat_ipad import WechatAPIClient
|
||||
@@ -93,6 +92,10 @@ class MessageSummaryPlugin(MessagePluginInterface):
|
||||
self._connect_timeout_seconds = int(api_config.get("connect_timeout_seconds", 10))
|
||||
self._request_timeout_seconds = int(api_config.get("request_timeout_seconds", 180))
|
||||
self._retry_delays_seconds = api_config.get("retry_delays_seconds", [10, 20])
|
||||
self.llm_client = UnifiedLLMClient(api_config)
|
||||
self._api_mode = self.llm_client.mode or self._api_mode
|
||||
self._response_mode = self.llm_client.response_mode or self._response_mode
|
||||
self._workflow_output_key = self.llm_client.workflow_output_key or self._workflow_output_key
|
||||
self.message_storage = MessageStorage()
|
||||
db_manager = context.get("db_manager")
|
||||
if db_manager:
|
||||
@@ -221,81 +224,6 @@ class MessageSummaryPlugin(MessagePluginInterface):
|
||||
sanitized_name = "群聊"
|
||||
return sanitized_name
|
||||
|
||||
async def _parse_streaming_response(self, response: aiohttp.ClientResponse) -> Dict[str, Any]:
|
||||
"""解析 Dify 的 SSE 流式响应"""
|
||||
answer_parts: List[str] = []
|
||||
metadata: Dict[str, Any] = {}
|
||||
final_payload: Dict[str, Any] = {}
|
||||
buffer = ""
|
||||
|
||||
async for chunk in response.content.iter_any():
|
||||
if not chunk:
|
||||
continue
|
||||
|
||||
buffer += chunk.decode("utf-8", errors="ignore")
|
||||
|
||||
while "\n\n" in buffer:
|
||||
raw_event, buffer = buffer.split("\n\n", 1)
|
||||
raw_event = raw_event.strip()
|
||||
if not raw_event:
|
||||
continue
|
||||
|
||||
data_lines = []
|
||||
for line in raw_event.splitlines():
|
||||
line = line.strip()
|
||||
if line.startswith("data:"):
|
||||
data_lines.append(line[5:].strip())
|
||||
|
||||
if not data_lines:
|
||||
continue
|
||||
|
||||
payload_text = "\n".join(data_lines).strip()
|
||||
if not payload_text or payload_text == "[DONE]":
|
||||
continue
|
||||
|
||||
try:
|
||||
payload = json.loads(payload_text)
|
||||
except json.JSONDecodeError:
|
||||
self.LOG.warning(f"无法解析流式响应片段: {payload_text[:200]}")
|
||||
continue
|
||||
|
||||
event_name = str(payload.get("event", "")).strip()
|
||||
if event_name in {"message", "agent_message"}:
|
||||
chunk_text = payload.get("answer", "")
|
||||
if chunk_text:
|
||||
answer_parts.append(chunk_text)
|
||||
elif event_name in {"message_end", "workflow_finished"}:
|
||||
final_payload = payload
|
||||
if self._api_mode == "workflow":
|
||||
payload_data = payload.get("data", {}) if isinstance(payload.get("data"), dict) else {}
|
||||
outputs = payload_data.get("outputs", {}) if isinstance(payload_data.get("outputs"), dict) else {}
|
||||
if outputs:
|
||||
for key in filter(None, [self._workflow_output_key, "text", "answer", "result_json", "result"]):
|
||||
if outputs.get(key) is not None:
|
||||
answer_parts = [self._stringify_output(outputs.get(key))]
|
||||
break
|
||||
metadata = payload.get("metadata", {}) or payload.get("data", {}).get("metadata", {}) or metadata
|
||||
elif event_name == "error":
|
||||
raise RuntimeError(payload.get("message") or payload.get("error") or "流式总结生成失败")
|
||||
else:
|
||||
if self._api_mode == "workflow":
|
||||
payload_data = payload.get("data", {}) if isinstance(payload.get("data"), dict) else {}
|
||||
outputs = payload_data.get("outputs", {}) if isinstance(payload_data.get("outputs"), dict) else {}
|
||||
for key in filter(None, [self._workflow_output_key, "text", "answer", "result_json", "result"]):
|
||||
if outputs.get(key) is not None:
|
||||
chunk_text = self._stringify_output(outputs.get(key))
|
||||
if chunk_text:
|
||||
answer_parts.append(chunk_text)
|
||||
break
|
||||
|
||||
answer = "".join(answer_parts)
|
||||
return {
|
||||
"answer": answer,
|
||||
"metadata": metadata,
|
||||
"data": final_payload.get("data", {}) if isinstance(final_payload, dict) else {},
|
||||
"event": final_payload.get("event", "") if isinstance(final_payload, dict) else "",
|
||||
}
|
||||
|
||||
def _append_usage_info(self, answer: str, metadata: Dict[str, Any]) -> str:
|
||||
"""把 token 统计追加到总结文本末尾"""
|
||||
if not answer or not answer.strip():
|
||||
@@ -440,7 +368,6 @@ class MessageSummaryPlugin(MessagePluginInterface):
|
||||
|
||||
async def _generate_summary(self, chat_content: str, group_name: str) -> Tuple[str, Optional[str]]:
|
||||
"""生成总结"""
|
||||
# Dify API配置
|
||||
content_compress = chat_content
|
||||
try:
|
||||
content_compress = compress_chat_data(chat_content)
|
||||
@@ -449,96 +376,50 @@ class MessageSummaryPlugin(MessagePluginInterface):
|
||||
self.LOG.error(f"压缩内容失败:{e}")
|
||||
|
||||
prompt = f"请根据[{group_name}]群的群聊记录生成一份总结:\n\n{content_compress}"
|
||||
if self._api_mode == "workflow":
|
||||
data = {
|
||||
"inputs": {
|
||||
"query": prompt,
|
||||
"group_name": group_name,
|
||||
"chat_content": content_compress,
|
||||
},
|
||||
"response_mode": self._response_mode,
|
||||
"user": group_name if group_name is not None else "message_summary_bot",
|
||||
}
|
||||
else:
|
||||
data = {
|
||||
"inputs": {},
|
||||
"query": prompt,
|
||||
"response_mode": self._response_mode,
|
||||
"conversation_id": "",
|
||||
"user": group_name if group_name is not None else "message_summary_bot",
|
||||
"files": []
|
||||
}
|
||||
|
||||
self.LOG.info(f"群聊总结内容:{data}")
|
||||
# 设置请求头
|
||||
headers = {
|
||||
"Authorization": f"Bearer {self._api_key}",
|
||||
"Content-Type": "application/json",
|
||||
"Accept": "text/event-stream" if self._response_mode == "streaming" else "application/json"
|
||||
inputs = {
|
||||
"query": prompt,
|
||||
"group_name": group_name,
|
||||
"chat_content": content_compress,
|
||||
}
|
||||
self.LOG.info(f"群聊总结请求准备: group={group_name}, mode={self._api_mode}, response_mode={self._response_mode}")
|
||||
|
||||
max_retries = len(self._retry_delays_seconds) + 1
|
||||
|
||||
for attempt in range(1, max_retries + 1):
|
||||
try:
|
||||
custom_timeout = ClientTimeout(
|
||||
total=None,
|
||||
connect=self._connect_timeout_seconds,
|
||||
sock_read=self._request_timeout_seconds
|
||||
response = await asyncio.to_thread(
|
||||
self.llm_client.run,
|
||||
prompt,
|
||||
group_name if group_name is not None else "message_summary_bot",
|
||||
inputs,
|
||||
f"message_summary:{group_name}",
|
||||
)
|
||||
conn = aiohttp.TCPConnector(keepalive_timeout=60) # 保持连接活跃
|
||||
async with aiohttp.ClientSession(connector=conn, timeout=custom_timeout) as session:
|
||||
async with session.post(self._api_url, headers=headers, json=data) as response:
|
||||
response.raise_for_status() # 检查请求是否成功
|
||||
if self._response_mode == "streaming":
|
||||
response_data = await self._parse_streaming_response(response)
|
||||
else:
|
||||
response_data = await response.json()
|
||||
if not response or not response.get("text"):
|
||||
raise RuntimeError(self.llm_client.last_error or "LLM 未返回有效总结内容")
|
||||
|
||||
self.LOG.info(f"Dify API响应状态码: {response.status}, attempt={attempt}")
|
||||
self.LOG.debug(f"响应数据: {json.dumps(response_data, ensure_ascii=False, indent=2)}")
|
||||
answer = self._clean_summary_output(response.get("text", ""))
|
||||
metadata = {"usage": response.get("usage", {}) or {}}
|
||||
spath = ""
|
||||
answer = self._append_usage_info(answer, metadata)
|
||||
|
||||
if self._api_mode == "workflow":
|
||||
answer, metadata = self._parse_workflow_response(response_data)
|
||||
else:
|
||||
answer = response_data.get("answer", "")
|
||||
metadata = response_data.get("metadata", {})
|
||||
if answer and len(answer.strip()) > 0:
|
||||
try:
|
||||
timestamp = int(time.time())
|
||||
output_path = f"summary_{timestamp}.png"
|
||||
self.LOG.info(f"开始生成图片: {output_path}")
|
||||
spath = await convert_md_str_to_image(answer, output_path)
|
||||
self.LOG.info(f"成功生成图片: {spath}")
|
||||
except Exception as e:
|
||||
self.LOG.error(f"生成图片失败: {e}", exc_info=True)
|
||||
max_length = 2000
|
||||
if len(answer) > max_length:
|
||||
answer = answer[:max_length] + "\n\n... (内容过长,已截断)"
|
||||
self.LOG.info("图片生成失败,将发送文本消息作为备选方案")
|
||||
spath = None
|
||||
else:
|
||||
spath = None
|
||||
return answer, spath
|
||||
|
||||
if not answer or not answer.strip():
|
||||
raise RuntimeError("Dify 未返回有效总结内容")
|
||||
|
||||
answer = self._clean_summary_output(answer)
|
||||
spath = ""
|
||||
answer = self._append_usage_info(answer, metadata)
|
||||
|
||||
if answer and len(answer.strip()) > 0:
|
||||
try:
|
||||
# 使用唯一文件名并指定完整路径
|
||||
timestamp = int(time.time())
|
||||
output_path = f"summary_{timestamp}.png"
|
||||
self.LOG.info(f"开始生成图片: {output_path}")
|
||||
spath = await convert_md_str_to_image(answer, output_path)
|
||||
self.LOG.info(f"成功生成图片: {spath}")
|
||||
except Exception as e:
|
||||
self.LOG.error(f"生成图片失败: {e}", exc_info=True)
|
||||
try:
|
||||
max_length = 2000
|
||||
if len(answer) > max_length:
|
||||
answer = answer[:max_length] + "\n\n... (内容过长,已截断)"
|
||||
self.LOG.info("图片生成失败,将发送文本消息作为备选方案")
|
||||
spath = None
|
||||
except Exception as fallback_error:
|
||||
self.LOG.error(f"备选文本发送也失败: {fallback_error}")
|
||||
spath = None
|
||||
else:
|
||||
spath = None
|
||||
# 返回文本内容和图片路径
|
||||
return answer, spath
|
||||
|
||||
except aiohttp.ClientError as e:
|
||||
self.LOG.error(f"请求Dify API时出错: attempt={attempt}/{max_retries}, error={e}")
|
||||
except json.JSONDecodeError as e:
|
||||
self.LOG.error(f"解析Dify API响应时出错: attempt={attempt}/{max_retries}, error={e}")
|
||||
except Exception as e:
|
||||
self.LOG.error(f"处理总结时出现未知错误: attempt={attempt}/{max_retries}, error={e}")
|
||||
|
||||
|
||||
Reference in New Issue
Block a user