from fastapi import APIRouter, Depends, HTTPException from fastapi.responses import StreamingResponse from pydantic import BaseModel from typing import Optional, Literal import aiosqlite, json, logging import httpx from database import get_db from config import settings from services.ai_client import get_openai_client from services.runtime_settings import get_ai_settings from services.media_parser import parse_media router = APIRouter(prefix="/api", tags=["ai"]) log = logging.getLogger(__name__) async def _get_ai_client(): return await get_openai_client() class SummarizeRequest(BaseModel): context: str # 已组装好的对话文本(含媒体描述) room_name: Optional[str] = "" messages: Optional[list] = None # 兼容旧调用,忽略 class ParseRequest(BaseModel): type: Literal["voice", "image", "video"] key: str # voice: ServerID string; image/video: md5 @router.post("/ai/parse") async def ai_parse(body: ParseRequest): """ 通过 FastAPI 代理 AI 媒体解析: - voice: 从 chatlog 下载音频 → DashScope Paraformer ASR 转文字 - image/video: 从 chatlog 下载媒体 → base64 → 视觉模型描述 """ try: return await parse_media(body.type, body.key) except HTTPException: raise except Exception as e: log.error(f"[ai/parse] 媒体解析失败: {e}", exc_info=True) raise HTTPException(500, f"媒体解析失败: {e}") @router.post("/ai/summarize/stream") async def summarize_stream(body: SummarizeRequest): """ 接收前端已处理好的对话上下文,调用 AI 模型流式输出总结。 前端负责先把媒体(图片/语音/视频)解析成文字再拼进 context。 """ _ai = await get_ai_settings() if not _ai.get("ai_api_key"): async def err_gen(): yield 'data: {"error": "AI 服务未配置,请在「设置」页面填入 AI API Key"}\n\n' return StreamingResponse(err_gen(), media_type="text/event-stream") if not _ai.get("ai_model"): async def err_gen(): yield 'data: {"error": "知识总结模型未配置,请在「设置」页面填入模型名称(如 qwen-max)"}\n\n' return StreamingResponse(err_gen(), media_type="text/event-stream") context = body.context.strip() if not context: async def err_gen(): yield 'data: {"error": "对话内容为空"}\n\n' return StreamingResponse(err_gen(), media_type="text/event-stream") room = body.room_name or "会话" system_prompt = ( "你是一位专业的对话分析助手。" "请根据提供的聊天记录(可能包含图片描述、语音转文字、视频描述等多媒体内容)" "生成一份结构清晰的 Markdown 总结。" "总结应包含:主要话题、关键信息点、媒体内容要点、待办事项(如有)。" "只输出 Markdown 格式内容,不要有任何额外说明。" ) user_prompt = ( f"群聊:{room}\n\n" f"以下是聊天记录(含多媒体内容描述):\n\n" f"{context[:12000]}\n\n" # 限制 token 数 f"请生成总结:" ) async def generate(): try: _client, _ai = await _get_ai_client() stream = await _client.chat.completions.create( model=_ai["ai_model"], messages=[ {"role": "system", "content": system_prompt}, {"role": "user", "content": user_prompt}, ], stream=True, temperature=0.3, ) async for chunk in stream: delta = chunk.choices[0].delta.content if chunk.choices else None if delta: yield f"data: {json.dumps({'delta': delta}, ensure_ascii=False)}\n\n" yield 'data: {"done": true}\n\n' except Exception as e: log.error(f"[summarize/stream] LLM 调用失败: {e}", exc_info=True) yield f"data: {json.dumps({'error': str(e)}, ensure_ascii=False)}\n\n" return StreamingResponse(generate(), media_type="text/event-stream") @router.get("/tasks/{task_id}") async def get_task(task_id: int, db: aiosqlite.Connection = Depends(get_db)): async with db.execute("SELECT * FROM ai_tasks WHERE id=?", (task_id,)) as cur: row = await cur.fetchone() if not row: raise HTTPException(404, "not found") return dict(row)