Add chat app adapters for QQ, Feishu, WeCom, and DingTalk

This commit is contained in:
Vivi 8n24
2026-03-13 15:55:49 +08:00
parent 3f66368689
commit 6fe93e9cb4
10 changed files with 1752 additions and 79 deletions

View File

@@ -1,4 +1,4 @@
import os, json, re, time, requests, sys, threading, urllib3
import os, json, re, time, requests, sys, threading, urllib3, base64, mimetypes
from datetime import datetime
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
@@ -34,6 +34,25 @@ def auto_make_url(base, path):
if b.endswith('$'): return b[:-1].rstrip('/')
return b if b.endswith(p) else f"{b}/{p}" if re.search(r'/v\d+$', b) else f"{b}/v1/{p}"
def build_multimodal_content(prompt_text, image_paths):
parts = []
text = prompt_text if isinstance(prompt_text, str) else str(prompt_text or "")
if text.strip():
parts.append({"type": "text", "text": text})
else:
parts.append({"type": "text", "text": "请查看图片并理解用户意图。"})
for path in image_paths or []:
if not path or not os.path.isfile(path): continue
try:
mime = mimetypes.guess_type(path)[0] or "image/png"
if not mime.startswith("image/"): mime = "image/png"
with open(path, "rb") as f:
data_url = f"data:{mime};base64,{base64.b64encode(f.read()).decode('ascii')}"
parts.append({"type": "image_url", "image_url": {"url": data_url}})
except Exception as e:
print(f"[WARN] encode image failed {path}: {e}")
return parts
class SiderLLMSession:
def __init__(self, sider_cookie, default_model="gemini-3.0-flash"):
from sider_ai_api import Session # 不使用sider的话没必要安装这个包
@@ -392,6 +411,8 @@ class ToolClient:
self.total_cd_tokens = 0
def chat(self, messages, tools=None):
if self._should_use_structured_messages(messages):
return (yield from self._chat_structured(messages, tools))
full_prompt = self._build_protocol_prompt(messages, tools)
print("Full prompt length:", len(full_prompt), 'chars')
script_dir = os.path.dirname(os.path.abspath(__file__))
@@ -410,14 +431,27 @@ class ToolClient:
f.write(f"=== Response === {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n{raw_text}\n\n")
return self._parse_mixed_response(raw_text)
def _build_protocol_prompt(self, messages, tools):
system_content = next((m['content'] for m in messages if m['role'].lower() == 'system'), "")
history_msgs = [m for m in messages if m['role'].lower() != 'system']
# 构造工具描述
def _should_use_structured_messages(self, messages):
return isinstance(self.backend, LLMSession) and any(isinstance(m.get("content"), list) for m in messages)
def _estimate_content_len(self, content):
if isinstance(content, str): return len(content)
if isinstance(content, list):
total = 0
for part in content:
if not isinstance(part, dict): continue
if part.get("type") == "text":
total += len(part.get("text", ""))
elif part.get("type") == "image_url":
total += 1000
return total
return len(str(content))
def _prepare_tool_instruction(self, tools):
tool_instruction = ""
if tools:
tools_json = json.dumps(tools, ensure_ascii=False, separators=(',', ':'))
tool_instruction = f"""
if not tools: return tool_instruction
tools_json = json.dumps(tools, ensure_ascii=False, separators=(',', ':'))
tool_instruction = f"""
### 交互协议 (必须严格遵守,持续有效)
请按照以下步骤思考并行动,标签之间需要回车换行:
1. **思考**: 在 `<thinking>` 标签中先进行思考,分析现状和策略。
@@ -428,11 +462,70 @@ class ToolClient:
### 可用工具库(已挂载,持续有效)
{tools_json}
"""
if self.auto_save_tokens and self.last_tools == tools_json:
tool_instruction = "\n### 工具库状态持续有效code_run/file_read等**可正常调用**。调用协议沿用。\n"
if self.auto_save_tokens and self.last_tools == tools_json:
tool_instruction = "\n### 工具库状态持续有效code_run/file_read等**可正常调用**。调用协议沿用。\n"
else:
self.total_cd_tokens = 0
self.last_tools = tools_json
return tool_instruction
def _build_backend_messages(self, messages, tools):
system_content = next((m['content'] for m in messages if m['role'].lower() == 'system'), "")
history_msgs = [m for m in messages if m['role'].lower() != 'system']
tool_instruction = self._prepare_tool_instruction(tools)
backend_messages = []
merged_system = f"{system_content}\n{tool_instruction}".strip() if tool_instruction else system_content
if merged_system:
backend_messages.append({"role": "system", "content": merged_system})
for m in history_msgs:
backend_messages.append({"role": m['role'], "content": m['content']})
self.total_cd_tokens += self._estimate_content_len(m['content'])
if self.total_cd_tokens > 6000: self.last_tools = ''
return backend_messages
def _serialize_messages_for_log(self, messages):
logged = []
for msg in messages:
content = msg.get("content")
if isinstance(content, list):
parts = []
for part in content:
if not isinstance(part, dict): continue
if part.get("type") == "text":
parts.append({"type": "text", "text": part.get("text", "")})
elif part.get("type") == "image_url":
url = (part.get("image_url") or {}).get("url", "")
prefix = url.split(",", 1)[0] if url else "data:image/unknown;base64"
parts.append({"type": "image_url", "image_url": {"url": prefix + ",<omitted>"}})
else:
parts.append(part)
logged.append({"role": msg.get("role"), "content": parts})
else:
self.total_cd_tokens = 0
self.last_tools = tools_json
logged.append(msg)
return json.dumps(logged, ensure_ascii=False, indent=2)
def _chat_structured(self, messages, tools):
backend_messages = self._build_backend_messages(messages, tools)
print("Structured prompt length:", sum(self._estimate_content_len(m.get("content")) for m in backend_messages), 'chars')
script_dir = os.path.dirname(os.path.abspath(__file__))
with open(os.path.join(script_dir, f'./temp/model_responses_{os.getpid()}.txt'), 'a', encoding='utf-8', errors="replace") as f:
f.write(f"=== Prompt === {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n{self._serialize_messages_for_log(backend_messages)}\n")
gen = self.backend.raw_ask(backend_messages)
raw_text = ''; summarytag = '[NextWillSummary]'
for chunk in gen:
raw_text += chunk
if chunk != summarytag: yield chunk
print('Complete response received.')
if raw_text.endswith(summarytag):
self.last_tools = ''; raw_text = raw_text[:-len(summarytag)]
with open(os.path.join(script_dir, f'./temp/model_responses_{os.getpid()}.txt'), 'a', encoding='utf-8', errors="replace") as f:
f.write(f"=== Response === {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n{raw_text}\n\n")
return self._parse_mixed_response(raw_text)
def _build_protocol_prompt(self, messages, tools):
system_content = next((m['content'] for m in messages if m['role'].lower() == 'system'), "")
history_msgs = [m for m in messages if m['role'].lower() != 'system']
tool_instruction = self._prepare_tool_instruction(tools)
prompt = ""
if system_content: prompt += f"=== SYSTEM ===\n{system_content}\n"
@@ -440,7 +533,7 @@ class ToolClient:
for m in history_msgs:
role = "USER" if m['role'] == 'user' else "ASSISTANT"
prompt += f"=== {role} ===\n{m['content']}\n\n"
self.total_cd_tokens += len(m['content'])
self.total_cd_tokens += self._estimate_content_len(m['content'])
if self.total_cd_tokens > 6000: self.last_tools = ''
@@ -538,4 +631,4 @@ if __name__ == "__main__":
response = get_final(llmclient.chat(
messages=[{"role": "user", "content": "<tool_result>10.176.45.12</tool_result>"}]
))
print(response.content)
print(response.content)