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 函数按用途获取对应客户端 - 支持语音、视觉、报告等不同用途使用独立网关和密钥 ```
156 lines
5.0 KiB
Python
156 lines
5.0 KiB
Python
from fastapi import FastAPI, Request
|
|
from fastapi.middleware.cors import CORSMiddleware
|
|
from fastapi.responses import FileResponse, JSONResponse
|
|
from fastapi.staticfiles import StaticFiles
|
|
from pydantic import BaseModel
|
|
from contextlib import asynccontextmanager
|
|
import asyncio
|
|
import logging
|
|
from pathlib import Path
|
|
from database import get_active_db_path, get_current_wxid, init_db, reset_wxid_cache, update_db_path
|
|
from scheduler import start_scheduler
|
|
from config import settings
|
|
from routers import search, groups, topics, knowledge, ai, sse, files, chatlog_proxy
|
|
from routers import settings as settings_router
|
|
from services.chatlog_context import get_chatlog_context, update_chatlog_context
|
|
from services.media_resolver import diagnose_media
|
|
from services.http_client import shared_client, close_shared_client
|
|
|
|
log = logging.getLogger(__name__)
|
|
|
|
|
|
class ChatlogContextRequest(BaseModel):
|
|
account: str = ""
|
|
workDir: str = ""
|
|
dataDir: str = ""
|
|
platform: str = "windows"
|
|
version: int = 4
|
|
chatlogExe: str = ""
|
|
chatlogVersion: str = ""
|
|
|
|
async def _account_watch_loop():
|
|
"""每 60 秒检测一次当前微信账号,如账号切换则自动切换数据库。"""
|
|
while True:
|
|
await asyncio.sleep(60)
|
|
try:
|
|
await update_db_path()
|
|
except Exception as e:
|
|
log.warning(f"[account_watch] update_db_path error: {e}")
|
|
|
|
@asynccontextmanager
|
|
async def lifespan(app: FastAPI):
|
|
await init_db()
|
|
await start_scheduler()
|
|
# 启动后台账号监控任务
|
|
task = asyncio.create_task(_account_watch_loop())
|
|
yield
|
|
task.cancel()
|
|
try:
|
|
await task
|
|
except asyncio.CancelledError:
|
|
pass
|
|
await close_shared_client()
|
|
|
|
app = FastAPI(lifespan=lifespan)
|
|
|
|
@app.exception_handler(RuntimeError)
|
|
async def runtime_error_handler(request: Request, exc: RuntimeError):
|
|
return JSONResponse(status_code=500, content={"detail": str(exc)})
|
|
|
|
app.add_middleware(
|
|
CORSMiddleware,
|
|
allow_origins=settings.cors_origins,
|
|
allow_methods=["*"],
|
|
allow_headers=["*"],
|
|
)
|
|
|
|
app.include_router(search.router)
|
|
app.include_router(groups.router)
|
|
app.include_router(topics.router)
|
|
app.include_router(knowledge.router)
|
|
app.include_router(ai.router)
|
|
app.include_router(sse.router)
|
|
app.include_router(files.router)
|
|
app.include_router(settings_router.router)
|
|
app.include_router(chatlog_proxy.router)
|
|
|
|
|
|
@app.get("/health")
|
|
async def health():
|
|
chatlog_ok = False
|
|
chatlog_error = ""
|
|
try:
|
|
client = shared_client()
|
|
resp = await client.get(f"{settings.chatlog_base_url}/api/v1/session", params={"limit": 1, "format": "json"}, timeout=3.0)
|
|
chatlog_ok = resp.status_code == 200
|
|
if not chatlog_ok:
|
|
chatlog_error = f"HTTP {resp.status_code}"
|
|
except Exception as e:
|
|
chatlog_error = str(e)
|
|
|
|
wxid = await get_current_wxid() if chatlog_ok else "default"
|
|
return {
|
|
"ok": True,
|
|
"chatlog_ok": chatlog_ok,
|
|
"chatlog_error": chatlog_error,
|
|
"wxid": wxid,
|
|
"db_path": get_active_db_path(),
|
|
"data_dir": settings.data_dir,
|
|
}
|
|
|
|
|
|
@app.post("/api/system/refresh-account")
|
|
async def refresh_account():
|
|
reset_wxid_cache()
|
|
# 用户主动点“重新识别账号”=要最新数据,无条件清掉头像/contact 库缓存
|
|
from services.chatlog_client import chatlog_client
|
|
chatlog_client.reset_account_cache()
|
|
db_path = await update_db_path(force=True)
|
|
wxid = await get_current_wxid()
|
|
return {"ok": True, "wxid": wxid, "db_path": db_path}
|
|
|
|
|
|
@app.post("/api/system/chatlog-context")
|
|
async def set_chatlog_context(body: ChatlogContextRequest):
|
|
return {"ok": True, "context": update_chatlog_context(body.model_dump())}
|
|
|
|
|
|
@app.get("/api/system/chatlog-context")
|
|
async def read_chatlog_context():
|
|
return {"ok": True, "context": get_chatlog_context()}
|
|
|
|
|
|
@app.get("/api/system/media-diagnostics")
|
|
async def media_diagnostics(kind: str = "voice", key: str = ""):
|
|
return await diagnose_media(kind, key)
|
|
|
|
|
|
static_dir = Path(settings.static_dir)
|
|
if static_dir.exists():
|
|
assets_dir = static_dir / "assets"
|
|
if assets_dir.exists():
|
|
app.mount("/assets", StaticFiles(directory=str(assets_dir)), name="assets")
|
|
for static_name in ("favicon.svg", "icons.svg"):
|
|
static_file = static_dir / static_name
|
|
|
|
if static_file.exists():
|
|
@app.get(f"/{static_name}", include_in_schema=False)
|
|
async def _serve_static_file(name=static_name):
|
|
return FileResponse(static_dir / name)
|
|
|
|
@app.get("/", include_in_schema=False)
|
|
async def spa_index():
|
|
return FileResponse(static_dir / "index.html")
|
|
|
|
@app.get("/{full_path:path}", include_in_schema=False)
|
|
async def spa_fallback(full_path: str):
|
|
path = static_dir / full_path
|
|
if path.exists() and path.is_file():
|
|
return FileResponse(path)
|
|
return FileResponse(static_dir / "index.html")
|
|
|
|
if __name__ == "__main__":
|
|
import uvicorn
|
|
# 为了在使用 PyInstaller 打包时也能正常运行
|
|
uvicorn.run(app, host="127.0.0.1", port=8000, reload=False)
|