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:
2026-06-24 20:34:10 +08:00
parent eecbe4172e
commit 646efa132e
18 changed files with 839 additions and 238 deletions

View File

@@ -3,29 +3,44 @@ from openai import AsyncOpenAI
from services.runtime_settings import get_ai_settings
# 按 (base_url, api_key) 缓存客户端:聊天/视觉/语音可能指向不同网关与密钥,
# 各自一个 pair最多累积 3 个有界。配置变更settings PUT 会 invalidate
# runtime_settings 缓存)后,新的 pair 会自然生成新客户端;旧的留存无副作用。
_client_cache: dict[tuple[str, str], AsyncOpenAI] = {}
_http_client_cache: dict[tuple[str, str], httpx.AsyncClient] = {}
async def get_openai_client() -> tuple[AsyncOpenAI, dict]:
settings = await get_ai_settings()
cache_key = (
settings.get("ai_base_url") or "",
settings.get("ai_api_key") or "",
)
def _get_client(base_url: str, api_key: str) -> AsyncOpenAI:
cache_key = (base_url or "", api_key or "")
if cache_key not in _client_cache:
for http_client in _http_client_cache.values():
await http_client.aclose()
_client_cache.clear()
_http_client_cache.clear()
http_client = httpx.AsyncClient(timeout=httpx.Timeout(600.0, connect=30.0))
_http_client_cache[cache_key] = http_client
_client_cache[cache_key] = AsyncOpenAI(
api_key=settings.get("ai_api_key") or "missing",
base_url=settings.get("ai_base_url"),
api_key=api_key or "missing",
base_url=base_url or None,
http_client=http_client,
)
return _client_cache[cache_key]
return _client_cache[cache_key], settings
async def get_openai_client() -> tuple[AsyncOpenAI, dict]:
"""聊天调用方(话题/报告/总结/对话)复用:用全局 ai_base_url / ai_api_key。"""
settings = await get_ai_settings()
client = _get_client(
settings.get("ai_base_url") or "",
settings.get("ai_api_key") or "",
)
return client, settings
async def get_client_for(purpose: str) -> tuple[AsyncOpenAI, dict]:
"""按用途取客户端purpose 为 'voice' / 'vision'
优先用 {purpose}_base_url / {purpose}_api_key为空则回退到全局
ai_base_url / ai_api_key单网关场景无需重复配置
"""
settings = await get_ai_settings()
base_url = settings.get(f"{purpose}_base_url") or settings.get("ai_base_url") or ""
api_key = settings.get(f"{purpose}_api_key") or settings.get("ai_api_key") or ""
client = _get_client(base_url, api_key)
return client, settings