Initial upload for secondary development

This commit is contained in:
2026-06-08 19:00:03 +08:00
commit b913b8c78c
81 changed files with 27139 additions and 0 deletions

View File

@@ -0,0 +1,116 @@
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)