mirror of
https://github.com/fastapiadmin/FastapiAdmin.git
synced 2026-09-23 13:13:09 +00:00
feat: 新增AI模型配置功能,优化前端组件与代码规范
1. 新增Redis AI_MODEL_CONFIG枚举与后端AI模型配置CRUD接口 2. 升级vue-img-cutter到3.1.1版本,更新前端依赖 3. 重构前端多处ElMessage提示逻辑,统一由拦截器处理 4. 替换ElDrawer为FaDrawer组件,统一弹窗组件库 5. 重构文章详情、评论组件,新增租户切换器与AI配置面板 6. 优化代码生成模板、菜单树表格逻辑与代码高亮样式 7. 修复前端路由与组件命名问题,更新快速入口配置
This commit is contained in:
@@ -2,21 +2,25 @@ from typing import Annotated, Any
|
||||
|
||||
from fastapi import APIRouter, Depends, Path
|
||||
from fastapi.responses import JSONResponse
|
||||
from redis.asyncio import Redis
|
||||
|
||||
from app.common.response import ResponseSchema, SuccessResponse
|
||||
from app.core.base_params import PaginationQueryParam
|
||||
from app.core.base_schema import AuthSchema
|
||||
from app.core.dependencies import AuthPermission
|
||||
from app.core.dependencies import AuthPermission, redis_getter
|
||||
from app.core.router_class import OperationLogRoute
|
||||
|
||||
from .schema import (
|
||||
AiChatRequestSchema,
|
||||
AiChatResponseSchema,
|
||||
AiModelConfigListResponse,
|
||||
AiModelConfigSchema,
|
||||
AiModelConfigUpdateSchema,
|
||||
ChatSessionCreateSchema,
|
||||
ChatSessionQueryParam,
|
||||
ChatSessionUpdateSchema,
|
||||
)
|
||||
from .service import ChatService
|
||||
from .service import AiModelConfigService, ChatService
|
||||
|
||||
ChatRouter = APIRouter(route_class=OperationLogRoute, prefix="/chat", tags=["AI管理", "AI对话"])
|
||||
|
||||
@@ -121,3 +125,83 @@ async def ai_chat_controller(
|
||||
),
|
||||
msg="对话成功",
|
||||
)
|
||||
|
||||
|
||||
# ============ AI 模型配置 ============ #
|
||||
|
||||
|
||||
@ChatRouter.get(
|
||||
"/model",
|
||||
summary="获取当前用户的 AI 模型配置列表(含当前激活 ID)",
|
||||
response_model=ResponseSchema[AiModelConfigListResponse],
|
||||
)
|
||||
async def list_model_config_controller(
|
||||
redis: Annotated[Redis, Depends(redis_getter)],
|
||||
auth: Annotated[AuthSchema, Depends(AuthPermission(["module_ai:chat:query"]))],
|
||||
) -> JSONResponse:
|
||||
service = AiModelConfigService(auth, redis)
|
||||
result = await service.list()
|
||||
return SuccessResponse(data=result, msg="获取模型配置列表成功")
|
||||
|
||||
|
||||
@ChatRouter.post(
|
||||
"/model",
|
||||
summary="新增一个 AI 模型配置",
|
||||
response_model=ResponseSchema[dict[str, Any]],
|
||||
)
|
||||
async def create_model_config_controller(
|
||||
data: AiModelConfigUpdateSchema,
|
||||
redis: Annotated[Redis, Depends(redis_getter)],
|
||||
auth: Annotated[AuthSchema, Depends(AuthPermission(["module_ai:chat:update"]))],
|
||||
) -> JSONResponse:
|
||||
service = AiModelConfigService(auth, redis)
|
||||
payload = AiModelConfigSchema(**data.model_dump())
|
||||
result = await service.create(payload)
|
||||
return SuccessResponse(data=result, msg="模型配置已新增")
|
||||
|
||||
|
||||
@ChatRouter.put(
|
||||
"/model/{config_id}",
|
||||
summary="更新指定 ID 的 AI 模型配置",
|
||||
response_model=ResponseSchema[dict[str, Any]],
|
||||
)
|
||||
async def update_model_config_controller(
|
||||
config_id: Annotated[str, Path(description="配置项 ID")],
|
||||
data: AiModelConfigUpdateSchema,
|
||||
redis: Annotated[Redis, Depends(redis_getter)],
|
||||
auth: Annotated[AuthSchema, Depends(AuthPermission(["module_ai:chat:update"]))],
|
||||
) -> JSONResponse:
|
||||
service = AiModelConfigService(auth, redis)
|
||||
payload = AiModelConfigSchema(**data.model_dump())
|
||||
result = await service.update(config_id, payload)
|
||||
return SuccessResponse(data=result, msg="模型配置已更新")
|
||||
|
||||
|
||||
@ChatRouter.delete(
|
||||
"/model/{config_id}",
|
||||
summary="删除指定 ID 的 AI 模型配置",
|
||||
response_model=ResponseSchema[None],
|
||||
)
|
||||
async def delete_model_config_controller(
|
||||
config_id: Annotated[str, Path(description="配置项 ID")],
|
||||
redis: Annotated[Redis, Depends(redis_getter)],
|
||||
auth: Annotated[AuthSchema, Depends(AuthPermission(["module_ai:chat:update"]))],
|
||||
) -> JSONResponse:
|
||||
service = AiModelConfigService(auth, redis)
|
||||
await service.delete(config_id)
|
||||
return SuccessResponse(data=None, msg="模型配置已删除")
|
||||
|
||||
|
||||
@ChatRouter.post(
|
||||
"/model/{config_id}/activate",
|
||||
summary="切换当前激活的 AI 模型配置(空 ID 表示使用系统默认)",
|
||||
response_model=ResponseSchema[None],
|
||||
)
|
||||
async def activate_model_config_controller(
|
||||
config_id: Annotated[str, Path(description="配置项 ID;传 __default__ 使用系统默认")],
|
||||
redis: Annotated[Redis, Depends(redis_getter)],
|
||||
auth: Annotated[AuthSchema, Depends(AuthPermission(["module_ai:chat:update"]))],
|
||||
) -> JSONResponse:
|
||||
service = AiModelConfigService(auth, redis)
|
||||
await service.set_active(config_id)
|
||||
return SuccessResponse(data=None, msg="已切换模型")
|
||||
|
||||
@@ -10,9 +10,10 @@ from app.core.base_params import BaseQueryParam, TenantByQueryParam, UserByQuery
|
||||
class ChatQuerySchema(BaseModel):
|
||||
"""WebSocket聊天查询模型"""
|
||||
|
||||
message: str = Field(..., min_length=1, description="消息内容")
|
||||
message: str | None = Field("", description="消息内容(停止时可为空)")
|
||||
session_id: str | None = Field(None, description="会话ID")
|
||||
files: list[dict[str, Any]] | None = Field(None, description="文件信息")
|
||||
action: str | None = Field(None, description="动作类型:stop=停止生成 | None=对话")
|
||||
|
||||
|
||||
class ChatSessionCreateSchema(BaseModel):
|
||||
@@ -83,3 +84,47 @@ class AiChatResponseSchema(BaseModel):
|
||||
session_id: str = Field(..., description="会话ID")
|
||||
function_calls: list[dict[str, Any]] | None = Field(None, description="函数调用信息")
|
||||
action: dict[str, Any] | None = Field(None, description="建议执行的操作")
|
||||
|
||||
|
||||
class AiModelConfigSchema(BaseModel):
|
||||
"""AI 模型配置项"""
|
||||
|
||||
name: str = Field(..., min_length=1, max_length=50, description="配置名称(用户可读)")
|
||||
base_url: str = Field(..., min_length=1, max_length=500, description="API Base URL,如 https://api.openai.com/v1")
|
||||
api_key: str = Field(..., min_length=1, max_length=500, description="API 密钥")
|
||||
model_id: str = Field(..., min_length=1, max_length=100, description="模型 ID")
|
||||
temperature: float = Field(0.7, ge=0.0, le=2.0, description="温度参数")
|
||||
|
||||
@field_validator("base_url")
|
||||
@classmethod
|
||||
def validate_base_url(cls, v: str) -> str:
|
||||
v = v.strip().rstrip("/")
|
||||
if not v.startswith(("http://", "https://")):
|
||||
raise ValueError("Base URL 必须以 http:// 或 https:// 开头")
|
||||
return v
|
||||
|
||||
@field_validator("model_id")
|
||||
@classmethod
|
||||
def validate_model_id(cls, v: str) -> str:
|
||||
v = v.strip()
|
||||
if not v:
|
||||
raise ValueError("模型 ID 不能为空")
|
||||
return v
|
||||
|
||||
|
||||
class AiModelConfigItemSchema(AiModelConfigSchema):
|
||||
"""带 ID 的模型配置项(存储与返回)"""
|
||||
|
||||
id: str = Field(..., min_length=1, max_length=64, description="配置项唯一 ID")
|
||||
created_time: str | None = Field(None, description="创建时间(ISO 字符串)")
|
||||
|
||||
|
||||
class AiModelConfigUpdateSchema(AiModelConfigSchema):
|
||||
"""更新 AI 模型配置(与创建结构相同,不含 id)"""
|
||||
|
||||
|
||||
class AiModelConfigListResponse(BaseModel):
|
||||
"""模型配置列表响应"""
|
||||
|
||||
items: list[AiModelConfigItemSchema] = Field(default_factory=list, description="配置项列表")
|
||||
active_id: str | None = Field(None, description="当前激活的配置项 ID;为空表示使用系统默认")
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
|
||||
import asyncio
|
||||
import json
|
||||
from collections.abc import AsyncGenerator
|
||||
from datetime import datetime
|
||||
from typing import Any
|
||||
@@ -6,15 +8,19 @@ from typing import Any
|
||||
from agno.run.team import TeamRunOutput
|
||||
from agno.session.team import TeamSession
|
||||
from agno.team.team import Team
|
||||
from redis.asyncio import Redis
|
||||
|
||||
from app.api.v1.module_system.dept.service import DeptService
|
||||
from app.common.enums import RedisInitKeyConfig
|
||||
from app.common.request import PaginationService
|
||||
from app.core.base_schema import AuthSchema
|
||||
from app.core.exceptions import CustomException
|
||||
from app.core.logger import logger
|
||||
from app.core.redis_crud import RedisCURD
|
||||
|
||||
from .crud import ChatSessionCRUD
|
||||
from .schema import (
|
||||
AiModelConfigSchema,
|
||||
ChatQuerySchema,
|
||||
ChatSessionCreateSchema,
|
||||
ChatSessionQueryParam,
|
||||
@@ -132,7 +138,12 @@ class ChatService:
|
||||
def __init__(self, auth: AuthSchema) -> None:
|
||||
self.auth = auth
|
||||
|
||||
async def chat_query(self, query: ChatQuerySchema) -> AsyncGenerator[str, None]:
|
||||
async def chat_query(
|
||||
self,
|
||||
query: ChatQuerySchema,
|
||||
stop_event: asyncio.Event | None = None,
|
||||
model_config: dict[str, Any] | None = None,
|
||||
) -> AsyncGenerator[str, None]:
|
||||
"""流式 AI 对话"""
|
||||
try:
|
||||
crud = ChatSessionCRUD(self.auth)
|
||||
@@ -154,14 +165,43 @@ class ChatService:
|
||||
dept_id=dept_id,
|
||||
session_id=session_id,
|
||||
db=crud.db,
|
||||
model_config=model_config,
|
||||
)
|
||||
|
||||
async for chunk in agent.arun(input=query.message, stream=True):
|
||||
if chunk and chunk.content:
|
||||
yield chunk.content
|
||||
message = (query.message or "").strip()
|
||||
if not message:
|
||||
yield "请输入消息内容"
|
||||
return
|
||||
|
||||
logger.info("开始流式生成: session_id={} message={!r}", session_id, message[:80])
|
||||
chunk_count = 0
|
||||
try:
|
||||
stream = agent.arun(input=message, stream=True)
|
||||
logger.info("agent.arun 返回对象类型: {}", type(stream).__name__)
|
||||
if hasattr(stream, "__aiter__"):
|
||||
async for chunk in stream:
|
||||
if stop_event is not None and stop_event.is_set():
|
||||
logger.info("用户主动停止生成: session_id={}", session_id)
|
||||
return
|
||||
if chunk and getattr(chunk, "content", None):
|
||||
chunk_count += 1
|
||||
yield chunk.content
|
||||
else:
|
||||
logger.debug("空 chunk 跳过: {}", type(chunk).__name__ if chunk else None)
|
||||
else:
|
||||
# 兼容非流式直接返回结果的场景
|
||||
logger.warning("agent.arun 未返回异步迭代器,尝试按单次结果处理")
|
||||
if stream and getattr(stream, "content", None):
|
||||
chunk_count += 1
|
||||
yield stream.content
|
||||
except asyncio.CancelledError:
|
||||
logger.info("生成任务被取消: session_id={}", session_id)
|
||||
return
|
||||
|
||||
logger.info("流式生成结束: session_id={} chunk_count={}", session_id, chunk_count)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"聊天查询失败: {e}")
|
||||
logger.error(f"聊天查询失败: {e}", exc_info=True)
|
||||
yield f"抱歉,处理您的请求时出现错误:{str(e)}"
|
||||
|
||||
async def chat_non_stream(self, message: str, session_id: str | None) -> dict[str, Any]:
|
||||
@@ -194,8 +234,6 @@ class ChatService:
|
||||
|
||||
if response and response.content:
|
||||
response_text = response.content
|
||||
import json
|
||||
|
||||
try:
|
||||
if response_text.strip().startswith("{") and response_text.strip().endswith("}"):
|
||||
action = json.loads(response_text)
|
||||
@@ -309,3 +347,170 @@ class ChatService:
|
||||
|
||||
async def delete(self, session_ids: list[str]) -> None:
|
||||
await ChatSessionCRUD(self.auth).delete_crud(session_ids=session_ids)
|
||||
|
||||
|
||||
# ================================================= #
|
||||
# ******************* AI 模型配置 ****************** #
|
||||
# ================================================= #
|
||||
|
||||
|
||||
def _ai_model_items_key(user_id: int) -> str:
|
||||
return f"{RedisInitKeyConfig.AI_MODEL_CONFIG.key}:items:{user_id}"
|
||||
|
||||
|
||||
def _ai_model_active_key(user_id: int) -> str:
|
||||
return f"{RedisInitKeyConfig.AI_MODEL_CONFIG.key}:active:{user_id}"
|
||||
|
||||
|
||||
async def get_user_model_config(redis: Redis, user_id: int) -> dict[str, Any] | None:
|
||||
"""读取当前激活的 AI 模型配置;不存在或未激活返回 None。"""
|
||||
active_id = await RedisCURD(redis).get(_ai_model_active_key(user_id))
|
||||
if not active_id:
|
||||
return None
|
||||
items = await list_user_model_configs(redis, user_id)
|
||||
for item in items:
|
||||
if item.get("id") == active_id:
|
||||
return item
|
||||
return None
|
||||
|
||||
|
||||
async def list_user_model_configs(redis: Redis, user_id: int) -> list[dict[str, Any]]:
|
||||
"""列出用户的所有模型配置项。"""
|
||||
raw = await RedisCURD(redis).get(_ai_model_items_key(user_id))
|
||||
if not raw:
|
||||
return []
|
||||
try:
|
||||
data = json.loads(raw)
|
||||
if isinstance(data, list):
|
||||
return data
|
||||
return []
|
||||
except (json.JSONDecodeError, TypeError):
|
||||
logger.warning("AI 模型配置列表 JSON 解析失败: user_id={}", user_id)
|
||||
return []
|
||||
|
||||
|
||||
async def get_active_model_id(redis: Redis, user_id: int) -> str | None:
|
||||
"""读取当前激活的模型配置 ID;为空表示使用系统默认。"""
|
||||
return await RedisCURD(redis).get(_ai_model_active_key(user_id))
|
||||
|
||||
|
||||
async def create_user_model_config(
|
||||
redis: Redis,
|
||||
user_id: int,
|
||||
config: AiModelConfigSchema,
|
||||
) -> dict[str, Any]:
|
||||
"""新增一个模型配置项。"""
|
||||
import uuid
|
||||
from datetime import datetime
|
||||
|
||||
items = await list_user_model_configs(redis, user_id)
|
||||
item = {
|
||||
**config.model_dump(),
|
||||
"id": uuid.uuid4().hex,
|
||||
"created_time": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
|
||||
}
|
||||
items.append(item)
|
||||
await RedisCURD(redis).set(
|
||||
_ai_model_items_key(user_id),
|
||||
json.dumps(items, ensure_ascii=False),
|
||||
)
|
||||
|
||||
# 若用户尚未激活任何配置,自动激活新增的
|
||||
if not await get_active_model_id(redis, user_id):
|
||||
await RedisCURD(redis).set(_ai_model_active_key(user_id), item["id"])
|
||||
|
||||
logger.info("已新增 AI 模型配置: user_id={} name={} id={}", user_id, config.name, item["id"])
|
||||
return item
|
||||
|
||||
|
||||
async def update_user_model_config(
|
||||
redis: Redis,
|
||||
user_id: int,
|
||||
config_id: str,
|
||||
config: AiModelConfigSchema,
|
||||
) -> dict[str, Any] | None:
|
||||
"""更新指定 ID 的模型配置项;不存在返回 None。"""
|
||||
items = await list_user_model_configs(redis, user_id)
|
||||
target = next((it for it in items if it.get("id") == config_id), None)
|
||||
if not target:
|
||||
return None
|
||||
target.update(config.model_dump())
|
||||
await RedisCURD(redis).set(
|
||||
_ai_model_items_key(user_id),
|
||||
json.dumps(items, ensure_ascii=False),
|
||||
)
|
||||
logger.info("已更新 AI 模型配置: user_id={} id={}", user_id, config_id)
|
||||
return target
|
||||
|
||||
|
||||
async def delete_user_model_config(redis: Redis, user_id: int, config_id: str) -> bool:
|
||||
"""删除指定 ID 的模型配置项;若该 ID 是当前激活则清空激活。"""
|
||||
items = await list_user_model_configs(redis, user_id)
|
||||
new_items = [it for it in items if it.get("id") != config_id]
|
||||
if len(new_items) == len(items):
|
||||
return False
|
||||
await RedisCURD(redis).set(
|
||||
_ai_model_items_key(user_id),
|
||||
json.dumps(new_items, ensure_ascii=False),
|
||||
)
|
||||
active_id = await get_active_model_id(redis, user_id)
|
||||
if active_id == config_id:
|
||||
await RedisCURD(redis).delete(_ai_model_active_key(user_id))
|
||||
logger.info("已删除 AI 模型配置: user_id={} id={}", user_id, config_id)
|
||||
return True
|
||||
|
||||
|
||||
async def set_active_model_config(redis: Redis, user_id: int, config_id: str) -> bool:
|
||||
"""设置当前激活的模型配置项;id 为空字符串或 "__default__" 表示使用系统默认。"""
|
||||
if config_id in ("", "__default__"):
|
||||
await RedisCURD(redis).delete(_ai_model_active_key(user_id))
|
||||
logger.info("已切换到系统默认模型: user_id={}", user_id)
|
||||
return True
|
||||
items = await list_user_model_configs(redis, user_id)
|
||||
if not any(it.get("id") == config_id for it in items):
|
||||
return False
|
||||
await RedisCURD(redis).set(_ai_model_active_key(user_id), config_id)
|
||||
logger.info("已切换 AI 模型: user_id={} id={}", user_id, config_id)
|
||||
return True
|
||||
|
||||
|
||||
class AiModelConfigService:
|
||||
"""AI 模型配置业务服务(多配置 + 激活切换)"""
|
||||
|
||||
def __init__(self, auth: AuthSchema, redis: Redis) -> None:
|
||||
self.auth = auth
|
||||
self.redis = redis
|
||||
|
||||
@property
|
||||
def _user_id(self) -> int:
|
||||
if not self.auth or not self.auth.user:
|
||||
raise CustomException(msg="未登录", code=10401, status_code=401)
|
||||
return self.auth.user.id
|
||||
|
||||
async def list(self) -> dict[str, Any]:
|
||||
"""获取配置列表 + 当前激活 ID。"""
|
||||
items = await list_user_model_configs(self.redis, self._user_id)
|
||||
active_id = await get_active_model_id(self.redis, self._user_id)
|
||||
return {"items": items, "active_id": active_id}
|
||||
|
||||
async def get_active(self) -> dict[str, Any] | None:
|
||||
return await get_user_model_config(self.redis, self._user_id)
|
||||
|
||||
async def create(self, config: AiModelConfigSchema) -> dict[str, Any]:
|
||||
return await create_user_model_config(self.redis, self._user_id, config)
|
||||
|
||||
async def update(self, config_id: str, config: AiModelConfigSchema) -> dict[str, Any] | None:
|
||||
result = await update_user_model_config(self.redis, self._user_id, config_id, config)
|
||||
if result is None:
|
||||
raise CustomException(msg="模型配置不存在", code=10404, status_code=404)
|
||||
return result
|
||||
|
||||
async def delete(self, config_id: str) -> None:
|
||||
ok = await delete_user_model_config(self.redis, self._user_id, config_id)
|
||||
if not ok:
|
||||
raise CustomException(msg="模型配置不存在", code=10404, status_code=404)
|
||||
|
||||
async def set_active(self, config_id: str) -> None:
|
||||
ok = await set_active_model_config(self.redis, self._user_id, config_id)
|
||||
if not ok:
|
||||
raise CustomException(msg="模型配置不存在", code=10404, status_code=404)
|
||||
|
||||
@@ -16,8 +16,17 @@ class AgnoFactory:
|
||||
AGENT_EXPECTED_OUTPUT = "中文回答"
|
||||
AGENT_TEMPERATURE = 0.7
|
||||
NUM_HISTORY_RUNS = 3
|
||||
REQUEST_TIMEOUT = 60.0 # LLM 请求总超时(秒),流式响应需放长
|
||||
CONNECT_TIMEOUT = 10.0 # TCP 连接超时(秒)
|
||||
|
||||
def create_agent(self, user_id: str, dept_id: str, session_id: str, db: Any | None = None) -> Team:
|
||||
def create_agent(
|
||||
self,
|
||||
user_id: str,
|
||||
dept_id: str,
|
||||
session_id: str,
|
||||
db: Any | None = None,
|
||||
model_config: dict[str, Any] | None = None,
|
||||
) -> Team:
|
||||
"""
|
||||
创建带 Agent 的 Team 实例。
|
||||
|
||||
@@ -26,10 +35,24 @@ class AgnoFactory:
|
||||
- dept_id (str): 部门/团队标识。
|
||||
- session_id (str): 会话 ID。
|
||||
- db (Any | None): Agno 持久化数据库实例,可选。
|
||||
- model_config (dict | None): 运行时模型配置,覆盖系统默认。
|
||||
支持字段:base_url, api_key, model_id, temperature。
|
||||
|
||||
返回:
|
||||
- Team: 配置好的 Team。
|
||||
"""
|
||||
# 优先使用运行时配置,否则 fallback 到系统 settings
|
||||
base_url = settings.OPENAI_BASE_URL
|
||||
api_key = settings.OPENAI_API_KEY
|
||||
model_id = settings.OPENAI_MODEL
|
||||
temperature = self.AGENT_TEMPERATURE
|
||||
|
||||
if model_config:
|
||||
base_url = model_config.get("base_url") or base_url
|
||||
api_key = model_config.get("api_key") or api_key
|
||||
model_id = model_config.get("model_id") or model_id
|
||||
if isinstance(model_config.get("temperature"), (int, float)):
|
||||
temperature = float(model_config["temperature"])
|
||||
|
||||
# 创建 Agent
|
||||
fastapiadmin_agent = Agent(
|
||||
@@ -46,10 +69,11 @@ class AgnoFactory:
|
||||
user_id=user_id,
|
||||
session_id=session_id,
|
||||
model=OpenAILike(
|
||||
id=settings.OPENAI_MODEL,
|
||||
api_key=settings.OPENAI_API_KEY,
|
||||
base_url=settings.OPENAI_BASE_URL,
|
||||
temperature=self.AGENT_TEMPERATURE,
|
||||
id=model_id,
|
||||
api_key=api_key,
|
||||
base_url=base_url,
|
||||
temperature=temperature,
|
||||
timeout=self.REQUEST_TIMEOUT,
|
||||
),
|
||||
members=[fastapiadmin_agent],
|
||||
instructions=self.AGENT_INSTRUCTIONS,
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import asyncio
|
||||
import json
|
||||
|
||||
from fastapi import APIRouter, WebSocket, WebSocketDisconnect
|
||||
@@ -9,7 +10,7 @@ from app.core.logger import logger
|
||||
from app.core.router_class import OperationLogRoute
|
||||
|
||||
from .schema import ChatQuerySchema
|
||||
from .service import ChatService
|
||||
from .service import ChatService, get_user_model_config
|
||||
|
||||
WS_AI = APIRouter(
|
||||
route_class=OperationLogRoute,
|
||||
@@ -36,9 +37,9 @@ async def websocket_chat_controller(websocket: WebSocket) -> None:
|
||||
"""
|
||||
WebSocket 聊天接口。
|
||||
|
||||
支持两种消息格式:
|
||||
1. 纯文本:直接发送消息内容
|
||||
2. JSON 格式:{"message": "消息内容", "session_id": "会话ID", "files": [...]}
|
||||
支持的消息格式(JSON):
|
||||
- 对话:{"message": "...", "session_id": "...", "files": [...]}
|
||||
- 停止:{"action": "stop", "session_id": "..."}
|
||||
|
||||
ws://127.0.0.1:8001/api/v1/ai/chat/ws?token=xxx
|
||||
"""
|
||||
@@ -49,8 +50,12 @@ async def websocket_chat_controller(websocket: WebSocket) -> None:
|
||||
await _send_error_and_close(websocket, "未提供认证token,请重新登录")
|
||||
return
|
||||
|
||||
# 跨消息循环共享的停止信号:客户端发送 stop 时 set,生成器检测到后退出
|
||||
stop_event = asyncio.Event()
|
||||
# 标记当前是否在生成中,便于 stop 校验
|
||||
is_generating = asyncio.Event()
|
||||
|
||||
try:
|
||||
# 认证:db 会话需在整个连接生命周期内保持打开(auth.db 供 ChatService 使用)
|
||||
redis = websocket.app.state.redis
|
||||
async with async_db_session() as db:
|
||||
auth = await _authenticate(token, db, redis)
|
||||
@@ -58,6 +63,8 @@ async def websocket_chat_controller(websocket: WebSocket) -> None:
|
||||
user = auth.user
|
||||
logger.info("WebSocket连接已建立: {} - 用户: {}", websocket.client, user.username if user else "未认证")
|
||||
|
||||
chat_service = ChatService(auth)
|
||||
|
||||
# 消息循环
|
||||
while True:
|
||||
try:
|
||||
@@ -65,22 +72,54 @@ async def websocket_chat_controller(websocket: WebSocket) -> None:
|
||||
try:
|
||||
message_data = json.loads(data)
|
||||
query = ChatQuerySchema(**message_data)
|
||||
logger.info("收到聊天查询: {} - 会话ID: {}", query, query.session_id)
|
||||
|
||||
chat_result = ChatService.chat_query(query=query, auth=auth)
|
||||
async for chunk in chat_result:
|
||||
if chunk:
|
||||
try:
|
||||
await websocket.send_text(chunk)
|
||||
except RuntimeError:
|
||||
logger.warning("WebSocket连接已关闭,停止发送消息")
|
||||
return
|
||||
except json.JSONDecodeError:
|
||||
logger.warning("收到非JSON消息: {}", data)
|
||||
await websocket.send_text("消息格式错误,请发送JSON格式的消息")
|
||||
continue
|
||||
except Exception as e:
|
||||
logger.error("处理消息时出错: {}", e)
|
||||
await websocket.send_text(f"处理消息时出错: {e}")
|
||||
logger.warning("消息校验失败: {}", e)
|
||||
await websocket.send_text(f"消息格式错误: {e}")
|
||||
continue
|
||||
|
||||
# 处理停止指令
|
||||
if query.action == "stop":
|
||||
if is_generating.is_set():
|
||||
stop_event.set()
|
||||
logger.info("收到停止指令: session={}", query.session_id)
|
||||
await websocket.send_text("[STOPPED]")
|
||||
else:
|
||||
await websocket.send_text("当前没有正在进行的生成任务")
|
||||
continue
|
||||
|
||||
# 对话指令
|
||||
logger.info("收到聊天查询: session_id={}", query.session_id)
|
||||
|
||||
is_generating.set()
|
||||
stop_event.clear()
|
||||
# 读取用户的 AI 模型配置(每次可动态切换)
|
||||
model_config = await get_user_model_config(redis, user.id)
|
||||
try:
|
||||
async for chunk in chat_service.chat_query(
|
||||
query=query,
|
||||
stop_event=stop_event,
|
||||
model_config=model_config,
|
||||
):
|
||||
if not chunk:
|
||||
continue
|
||||
try:
|
||||
await websocket.send_text(chunk)
|
||||
except RuntimeError:
|
||||
logger.warning("WebSocket连接已关闭,停止发送消息")
|
||||
return
|
||||
finally:
|
||||
is_generating.clear()
|
||||
stop_event.clear()
|
||||
|
||||
# 告知前端生成结束
|
||||
try:
|
||||
await websocket.send_text("[DONE]")
|
||||
except RuntimeError:
|
||||
return
|
||||
|
||||
except WebSocketDisconnect:
|
||||
logger.info("WebSocket连接已断开: {}", websocket.client)
|
||||
|
||||
Reference in New Issue
Block a user