```
feat(api): 添加万川平台模型配置获取和同步功能 - 新增 getWanchuanModelConfig 函数,按模型编码获取平台模型配置 - 新增 syncWanchuanModelToSettings 函数,从万川平台拉取模型配置并写入后端 AI 设置 - 支持按用途分多个模型编码(generic/vision/voice)分别同步配置 - 配置失败时跳过对应字段,不影响其他模型同步 feat(settings): 重构AI模型配置界面支持多模块分组 - 将AI配置按话题分析、报告生成、视觉、语音四个模块分组展示 - 每个模块独立配置接口地址、密钥和模型名称 - 添加从万川平台获取配置的按钮和同步功能 - 优化配置状态指示和错误提示信息 refactor(config): 扩展AI配置支持独立的语音视觉报告网关 - 新增 voice_base_url/voice_api_key 配置项 - 新增 vision_base_url/vision_api_key 配置项 - 新增 summary_base_url/summary_api_key 配置项 - 留空时回退到 ai_base_url/ai_api_key 兼容单网关场景 refactor(http): 统一使用共享HTTP客户端减少连接开销 - 替换各处 httpx.AsyncClient 为 shared_client - 在 lifespan 中正确关闭共享客户端资源 - 优化 get_current_wxid 和 health 检查中的HTTP请求 refactor(ai): 按用途缓存AI客户端支持不同网关配置 - 重构 get_openai_client 支持按(base_url, api_key)缓存 - 新增 get_client_for 函数按用途获取对应客户端 - 支持语音、视觉、报告等不同用途使用独立网关和密钥 ```
This commit is contained in:
@@ -2,6 +2,7 @@ import httpx
|
||||
import asyncio
|
||||
from typing import List
|
||||
from config import settings
|
||||
from services.http_client import shared_client
|
||||
|
||||
|
||||
class ChatlogHTTPError(RuntimeError):
|
||||
@@ -21,13 +22,21 @@ class ChatlogClient:
|
||||
def __init__(self):
|
||||
self.base = settings.chatlog_base_url
|
||||
self._contact_db_file = None
|
||||
# 进程级头像缓存:wxid -> url。同一账号下同一 wxid 只查一次 chatlog SQL,
|
||||
# 避免打开群聊时几十个发言人各打一次 /api/v1/db/query 头像查询。
|
||||
self._avatar_cache: dict[str, str] = {}
|
||||
|
||||
def reset_account_cache(self):
|
||||
"""账号切换时调用:清掉 contact 库路径与头像缓存,避免显示上一个账号的数据。"""
|
||||
self._contact_db_file = None
|
||||
self._avatar_cache.clear()
|
||||
|
||||
async def _get(self, path: str, params: dict, timeout: float = 30.0) -> dict:
|
||||
try:
|
||||
async with httpx.AsyncClient(timeout=timeout, trust_env=False) as client:
|
||||
r = await client.get(f"{self.base}{path}", params=params)
|
||||
r.raise_for_status()
|
||||
return r.json()
|
||||
client = shared_client()
|
||||
r = await client.get(f"{self.base}{path}", params=params, timeout=timeout)
|
||||
r.raise_for_status()
|
||||
return r.json()
|
||||
except httpx.TimeoutException:
|
||||
raise RuntimeError(f"chatlog timeout: GET {path}")
|
||||
except httpx.HTTPStatusError as e:
|
||||
@@ -38,10 +47,10 @@ class ChatlogClient:
|
||||
|
||||
async def _post(self, path: str, body: dict, timeout: float = 30.0) -> dict:
|
||||
try:
|
||||
async with httpx.AsyncClient(timeout=timeout, trust_env=False) as client:
|
||||
r = await client.post(f"{self.base}{path}", json=body)
|
||||
r.raise_for_status()
|
||||
return r.json()
|
||||
client = shared_client()
|
||||
r = await client.post(f"{self.base}{path}", json=body, timeout=timeout)
|
||||
r.raise_for_status()
|
||||
return r.json()
|
||||
except httpx.TimeoutException:
|
||||
raise RuntimeError(f"chatlog timeout: POST {path}")
|
||||
except httpx.HTTPStatusError as e:
|
||||
@@ -128,15 +137,16 @@ class ChatlogClient:
|
||||
|
||||
async def get_message(self, talker: str, seq: int) -> dict | None:
|
||||
try:
|
||||
async with httpx.AsyncClient(timeout=10.0, trust_env=False) as client:
|
||||
r = await client.get(
|
||||
f"{self.base}/api/v1/chatlog/message",
|
||||
params={"talker": talker, "seq": seq},
|
||||
)
|
||||
if r.status_code == 404:
|
||||
return None
|
||||
r.raise_for_status()
|
||||
return r.json()
|
||||
client = shared_client()
|
||||
r = await client.get(
|
||||
f"{self.base}/api/v1/chatlog/message",
|
||||
params={"talker": talker, "seq": seq},
|
||||
timeout=10.0,
|
||||
)
|
||||
if r.status_code == 404:
|
||||
return None
|
||||
r.raise_for_status()
|
||||
return r.json()
|
||||
except httpx.TimeoutException:
|
||||
raise RuntimeError("chatlog timeout: get_message")
|
||||
except Exception as e:
|
||||
@@ -174,6 +184,11 @@ class ChatlogClient:
|
||||
|
||||
|
||||
async def get_avatar_url(self, wxid: str) -> str:
|
||||
if not wxid:
|
||||
return ""
|
||||
cached = self._avatar_cache.get(wxid)
|
||||
if cached is not None:
|
||||
return cached
|
||||
if self._contact_db_file is None:
|
||||
try:
|
||||
db_list = await self._get("/api/v1/db", {})
|
||||
@@ -185,15 +200,17 @@ class ChatlogClient:
|
||||
safe_wxid = wxid.replace("'", "''")
|
||||
sql = f"SELECT small_head_url, big_head_url FROM contact WHERE username='{safe_wxid}' LIMIT 1"
|
||||
params = {"group": "contact", "file": self._contact_db_file, "sql": sql}
|
||||
url = ""
|
||||
try:
|
||||
rows = await self._get("/api/v1/db/query", params, timeout=5.0)
|
||||
if rows:
|
||||
url = rows[0].get("small_head_url") or rows[0].get("big_head_url") or ""
|
||||
if url:
|
||||
return url
|
||||
except Exception:
|
||||
pass
|
||||
return ""
|
||||
# 查询失败不写缓存,下次仍可重试
|
||||
return ""
|
||||
# 命中(含确定无头像的空串)都缓存,避免重复查询
|
||||
self._avatar_cache[wxid] = url
|
||||
return url
|
||||
|
||||
async def get_db_paths(self) -> dict:
|
||||
data = await self._get("/api/v1/db", {}, timeout=10.0)
|
||||
|
||||
Reference in New Issue
Block a user