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

@@ -5,6 +5,7 @@ from fastapi import APIRouter, Request
from fastapi.responses import Response, StreamingResponse
from config import settings
from services.http_client import shared_client
router = APIRouter(tags=["chatlog-proxy"])
@@ -41,13 +42,14 @@ async def _proxy_chatlog(request: Request, upstream_path: str) -> Response:
if key.lower() not in HOP_BY_HOP_HEADERS and key.lower() != "host"
}
async with httpx.AsyncClient(timeout=None, trust_env=False, follow_redirects=True) as client:
upstream = await client.request(
request.method,
target,
content=body if body else None,
headers=headers,
)
client = shared_client()
upstream = await client.request(
request.method,
target,
content=body if body else None,
headers=headers,
timeout=None,
)
response_headers = _copy_headers(upstream.headers)
return StreamingResponse(

View File

@@ -7,12 +7,12 @@ import tempfile
from pathlib import Path
from urllib.parse import quote
import httpx
from fastapi import APIRouter, HTTPException, Query
from fastapi.responses import FileResponse, StreamingResponse
from config import settings
from services.chatlog_client import chatlog_client
from services.http_client import shared_client
router = APIRouter(prefix="/api/files", tags=["files"])
@@ -61,8 +61,8 @@ def _guess_media_type(filename: str, fallback: str = "") -> str:
async def _proxy_chatlog_file(md5: str, filename: str = ""):
url = f"{settings.chatlog_base_url}/file/{quote(md5, safe='')}"
try:
async with httpx.AsyncClient(timeout=30, trust_env=False, follow_redirects=True) as client:
resp = await client.get(url)
client = shared_client()
resp = await client.get(url, timeout=30)
except Exception:
return None

View File

@@ -10,8 +10,13 @@ router = APIRouter(prefix="/api/settings", tags=["settings"])
EDITABLE_KEYS = [
"ai_base_url", "ai_api_key", "ai_model", "summary_model",
"vision_model", "voice_model", "topic_analysis_prompt",
"voice_base_url", "voice_api_key", "vision_base_url", "vision_api_key",
"summary_base_url", "summary_api_key",
]
# 需要脱敏GET 返回打码)并在 PUT 时跳过含 `*` 占位值的密钥字段。
SECRET_KEYS = {"ai_api_key", "voice_api_key", "vision_api_key", "summary_api_key"}
# 万川 AI 平台对接配置整体作为一条 JSON 存储,独立于上面的 AI 模型配置。
# 存到后端 SQLiteapp_settings 表)后,配置不再依赖前端 localStorage 的 origin
# 桌面应用exe即便后端端口/origin 变化也能跨次启动恢复平台地址、账号、密码与已选知识库。
@@ -35,7 +40,7 @@ async def get_settings(db: aiosqlite.Connection = Depends(get_db)):
rows = await cur.fetchall()
for row in rows:
k, v = row["key"], row["value"]
result[k] = _mask_key(v) if k == "ai_api_key" else v
result[k] = _mask_key(v) if k in SECRET_KEYS else v
for k in EDITABLE_KEYS:
if k not in result:
result[k] = ""
@@ -50,6 +55,12 @@ class SettingsUpdate(BaseModel):
vision_model: Optional[str] = None
voice_model: Optional[str] = None
topic_analysis_prompt: Optional[str] = None
voice_base_url: Optional[str] = None
voice_api_key: Optional[str] = None
vision_base_url: Optional[str] = None
vision_api_key: Optional[str] = None
summary_base_url: Optional[str] = None
summary_api_key: Optional[str] = None
@router.put("")
@@ -58,7 +69,8 @@ async def update_settings(body: SettingsUpdate, db: aiosqlite.Connection = Depen
for k, v in updates.items():
if k not in EDITABLE_KEYS:
continue
if k == "ai_api_key" and "*" in v:
# 密钥字段含 `*` 说明是 GET 返回的打码值,未被用户真正修改,跳过避免覆盖真实值
if k in SECRET_KEYS and "*" in v:
continue
await db.execute(
"INSERT INTO app_settings (key, value) VALUES (?, ?) ON CONFLICT(key) DO UPDATE SET value = ?",