diff --git a/backend/app/api/v1/module_system/log/model.py b/backend/app/api/v1/module_system/log/model.py index e8f74cae..90eda9ef 100644 --- a/backend/app/api/v1/module_system/log/model.py +++ b/backend/app/api/v1/module_system/log/model.py @@ -1,9 +1,22 @@ from sqlalchemy import Integer, String, Text from sqlalchemy.orm import Mapped, mapped_column +from app.config.setting import settings from app.core.base_model import ModelMixin, UserMixin +def get_log_text_column_type(): + db_type = settings.DATABASE_TYPE + if db_type == "mysql": + from sqlalchemy.dialects.mysql import LONGTEXT + return LONGTEXT + elif db_type == "postgres": + from sqlalchemy.dialects.postgresql import TEXT + return TEXT + else: + return Text + + class OperationLogModel(ModelMixin, UserMixin): """ 系统日志模型 @@ -19,11 +32,11 @@ class OperationLogModel(ModelMixin, UserMixin): type: Mapped[int] = mapped_column(Integer, comment="日志类型(1登录日志 2操作日志)") request_path: Mapped[str] = mapped_column(String(255), comment="请求路径") request_method: Mapped[str] = mapped_column(String(10), comment="请求方式") - request_payload: Mapped[str | None] = mapped_column(Text, comment="请求体") + request_payload: Mapped[str | None] = mapped_column(get_log_text_column_type(), comment="请求体") request_ip: Mapped[str | None] = mapped_column(String(50), comment="请求IP地址") login_location: Mapped[str | None] = mapped_column(String(255), comment="登录位置") request_os: Mapped[str | None] = mapped_column(String(64), nullable=True, comment="操作系统") request_browser: Mapped[str | None] = mapped_column(String(64), nullable=True, comment="浏览器") response_code: Mapped[int] = mapped_column(Integer, comment="响应状态码") - response_json: Mapped[str | None] = mapped_column(Text, nullable=True, comment="响应体") + response_json: Mapped[str | None] = mapped_column(get_log_text_column_type(), nullable=True, comment="响应体") process_time: Mapped[str | None] = mapped_column(String(20), nullable=True, comment="处理时间") diff --git a/backend/app/core/ap_scheduler.py b/backend/app/core/ap_scheduler.py index d3f42885..a82a9a43 100644 --- a/backend/app/core/ap_scheduler.py +++ b/backend/app/core/ap_scheduler.py @@ -640,7 +640,7 @@ async def init_scheduler(cls, redis: Redis | None = None) -> None: def _task_wrapper(cls, job_id: str | int, code_block: str | None, *args, **kwargs): """ 任务执行包装器,执行自定义代码块(同步版本,用于 ThreadPoolExecutor) - + 支持完整的 Python 语法,包括 import 语句 """ import types @@ -648,14 +648,14 @@ def _task_wrapper(cls, job_id: str | int, code_block: str | None, *args, **kwarg def run_sync_handler(): if not code_block: return None - + # 创建一个新的模块作为执行环境 module = types.ModuleType(f"node_task_{job_id}") module.__dict__["__builtins__"] = __builtins__ - + # 在模块环境中执行代码 exec(code_block, module.__dict__) - + # 获取 handler 函数 handler = module.__dict__.get("handler") if handler and callable(handler): diff --git a/backend/app/plugin/module_ai/chat/controller.py b/backend/app/plugin/module_ai/chat/controller.py new file mode 100644 index 00000000..6137f313 --- /dev/null +++ b/backend/app/plugin/module_ai/chat/controller.py @@ -0,0 +1,195 @@ +from typing import Annotated, Any + +from fastapi import APIRouter, Depends, Path +from fastapi.responses import JSONResponse + +from app.api.v1.module_system.auth.schema import AuthSchema +from app.common.response import ResponseSchema, SuccessResponse +from app.core.base_params import PaginationQueryParam +from app.core.dependencies import AuthPermission +from app.core.logger import log +from app.core.router_class import OperationLogRoute + +from .schema import ( + AiChatRequestSchema, + AiChatResponseSchema, + ChatSessionCreateSchema, + ChatSessionQueryParam, + ChatSessionUpdateSchema, +) +from .service import ChatService + +ChatRouter = APIRouter(route_class=OperationLogRoute, prefix="/chat", tags=["AI聊天会话管理"]) + + +@ChatRouter.get( + "/detail/{session_id}", + summary="获取会话详情", + description="获取会话详情", + response_model=ResponseSchema[dict[str, Any]], +) +async def get_session_detail_controller( + session_id: Annotated[str, Path(description="会话ID")], + auth: Annotated[AuthSchema, Depends(AuthPermission(["module_ai:chat:detail"]))], +) -> JSONResponse: + """ + 获取会话详情 + + 参数: + - session_id (str): 会话ID + - auth (AuthSchema): 认证信息模型 + + 返回: + - JSONResponse: 包含会话详情的JSON响应 + """ + result = await ChatService.get_session_service(auth=auth, session_id=session_id) + log.info(f"获取会话详情成功 {session_id}") + return SuccessResponse(data=result, msg="获取会话详情成功") + + +@ChatRouter.get( + "/list", + summary="查询会话列表", + description="查询会话列表", + response_model=ResponseSchema[dict], +) +async def get_session_list_controller( + page: Annotated[PaginationQueryParam, Depends()], + search: Annotated[ChatSessionQueryParam, Depends()], + auth: Annotated[AuthSchema, Depends(AuthPermission(["module_ai:chat:query"]))], +) -> JSONResponse: + """ + 查询会话列表 + + 参数: + - page (PaginationQueryParam): 分页查询参数 + - search (ChatSessionQueryParam): 查询参数 + - auth (AuthSchema): 认证信息模型 + + 返回: + - JSONResponse: 包含会话列表分页信息的JSON响应 + """ + result_dict = await ChatService.page_service( + auth=auth, + page_no=page.page_no, + page_size=page.page_size, + search=search, + order_by=page.order_by, + ) + log.info("查询会话列表成功") + return SuccessResponse(data=result_dict, msg="查询会话列表成功") + + +@ChatRouter.post( + "/create", + summary="创建会话", + description="创建会话", + response_model=ResponseSchema[dict[str, Any]], +) +async def create_session_controller( + data: ChatSessionCreateSchema, + auth: Annotated[AuthSchema, Depends(AuthPermission(["module_ai:chat:create"]))], +) -> JSONResponse: + """ + 创建会话 + + 参数: + - data (ChatSessionCreateSchema): 会话创建模型 + - auth (AuthSchema): 认证信息模型 + + 返回: + - JSONResponse: 包含创建会话详情的JSON响应 + """ + result = await ChatService.create_service(auth=auth, data=data) + if result: + log.info(f"创建会话成功 {result.get('session_id')}") + return SuccessResponse(data=result, msg="创建会话成功") + + +@ChatRouter.put( + "/update/{session_id}", + summary="更新会话", + description="更新会话", + response_model=ResponseSchema[None], +) +async def update_session_controller( + session_id: Annotated[str, Path(description="会话ID")], + data: ChatSessionUpdateSchema, + auth: Annotated[AuthSchema, Depends(AuthPermission(["module_ai:chat:update"]))], +) -> JSONResponse: + """ + 更新会话 + + 参数: + - session_id (str): 会话ID + - data (ChatSessionUpdateSchema): 会话更新模型 + - auth (AuthSchema): 认证信息模型 + + 返回: + - JSONResponse: 包含更新会话详情的JSON响应 + """ + await ChatService.update_service(auth=auth, session_id=session_id, data=data) + log.info(f"更新会话成功 {session_id}") + return SuccessResponse(data=None, msg="更新会话成功") + + +@ChatRouter.delete( + "/delete", + summary="删除会话", + description="删除会话", + response_model=ResponseSchema[None], +) +async def delete_session_controller( + session_ids: list[str], + auth: Annotated[AuthSchema, Depends(AuthPermission(["module_ai:chat:delete"]))], +) -> JSONResponse: + """ + 删除会话 + + 参数: + - session_ids (list[str]): 会话ID列表 + - auth (AuthSchema): 认证信息模型 + + 返回: + - JSONResponse: 包含删除结果的JSON响应 + """ + await ChatService.delete_service(auth=auth, session_ids=session_ids) + log.info(f"删除会话成功 {session_ids}") + return SuccessResponse(data=None, msg="删除会话成功") + + +@ChatRouter.post( + "/ai-chat", + summary="AI 对话(非流式)", + description="AI 对话接口,用于 AiAssistant 组件,返回完整响应", + response_model=ResponseSchema[AiChatResponseSchema], +) +async def ai_chat_controller( + data: AiChatRequestSchema, + auth: Annotated[AuthSchema, Depends(AuthPermission(["module_ai:chat:query"]))], +) -> JSONResponse: + """ + AI 对话(非流式) + + 参数: + - data (AiChatRequestSchema): 对话请求数据 + - auth (AuthSchema): 认证信息模型 + + 返回: + - JSONResponse: 包含 AI 回复、会话ID和函数调用信息的JSON响应 + """ + result = await ChatService.chat_non_stream( + message=data.message, + session_id=data.session_id, + auth=auth, + ) + log.info(f"AI 对话成功 {result.get('session_id')}") + return SuccessResponse( + data=AiChatResponseSchema( + response=result["response"], + session_id=result["session_id"], + function_calls=result.get("function_calls"), + action=result.get("action"), + ), + msg="对话成功", + ) diff --git a/backend/app/plugin/module_ai/chat/crud.py b/backend/app/plugin/module_ai/chat/crud.py new file mode 100644 index 00000000..002ac52d --- /dev/null +++ b/backend/app/plugin/module_ai/chat/crud.py @@ -0,0 +1,135 @@ +from typing import Any + +from agno.db.base import SessionType +from agno.db.mysql import MySQLDb +from agno.db.postgres import PostgresDb +from agno.db.sqlite import SqliteDb +from agno.session.team import TeamSession + +from app.api.v1.module_system.auth.schema import AuthSchema +from app.config.setting import settings +from app.core.logger import log + +from .schema import ChatSessionCreateSchema, ChatSessionUpdateSchema + + +class ChatSessionCRUD: + """聊天会话数据层 - 使用 agno 数据库存储""" + + # 会话类型配置 - 使用 TEAM 类型因为创建的是 Team + SESSION_TYPE = SessionType.TEAM + + def __init__(self, auth: AuthSchema) -> None: + """初始化CRUD数据层""" + self.auth = auth + self.user_id = auth.user.username if auth and auth.user else "user" + self.team_id = str(auth.user.dept_id) if auth and auth.user and hasattr(auth.user, 'dept_id') and auth.user.dept_id else None + self.db = self._get_db() + + def _get_db(self) -> Any: + """获取数据库连接""" + db_type = settings.DATABASE_TYPE + db_uri = settings.DB_URI + + db_mapping = { + "mysql": lambda: MySQLDb(db_url=db_uri), + "postgres": lambda: PostgresDb(db_url=db_uri), + "sqlite": lambda: SqliteDb(db_file=db_uri.replace("sqlite:///", "")), + } + + if db_type not in db_mapping: + raise ValueError(f"不支持的数据库类型: {db_type}") + + return db_mapping[db_type]() + + def __del__(self) -> None: + """析构时关闭数据库连接""" + self.db.close() + + async def get_by_id_crud(self, session_id: str) -> TeamSession | None: + """获取会话详情""" + try: + return self.db.get_session( + session_id=session_id, + session_type=self.SESSION_TYPE, + user_id=self.user_id + ) + except Exception as e: + log.error(f"获取会话详情失败: {e}") + return None + + async def list_crud( + self, + search: dict[str, Any] | None = None, + order_by: list[dict[str, str]] | None = None, + ) -> list[TeamSession]: + """列表查询 - 获取所有会话""" + try: + result = self.db.get_sessions( + session_type=self.SESSION_TYPE, + user_id=self.user_id + ) + if isinstance(result, tuple) and len(result) == 2: + return result[0] + return result if isinstance(result, list) else [] + except Exception as e: + log.error(f"获取会话列表失败: {e}") + return [] + + async def create_crud(self, data: ChatSessionCreateSchema) -> TeamSession | None: + """创建会话 - Team 会在运行时自动创建和管理 session""" + import time + import uuid + + try: + session_id = str(uuid.uuid4()) + now = int(time.time()) + + # 创建 session_data,包含 session_name + session_data = {} + if data.title: + session_data["session_name"] = data.title + + # 创建 TeamSession 对象 + session = TeamSession( + session_id=session_id, + user_id=self.user_id, + team_id=self.team_id, + session_data=session_data, + created_at=now, + updated_at=now, + ) + + # 保存会话 + result = self.db.upsert_session(session=session) + return result + except Exception as e: + log.exception(f"创建会话失败: {e}") + return None + + async def update_crud(self, session_id: str, data: ChatSessionUpdateSchema) -> bool: + """更新会话""" + try: + self.db.rename_session( + session_id=session_id, + session_type=self.SESSION_TYPE, + session_name=data.title, + user_id=self.user_id + ) + return True + except Exception as e: + log.error(f"更新会话失败: {e}") + return False + + async def delete_crud(self, session_ids: list[str]) -> bool: + """批量删除会话""" + try: + for session_id in session_ids: + self.db.delete_session( + session_id=session_id, + user_id=self.user_id + ) + return True + except Exception as e: + log.error(f"删除会话失败: {e}") + return False diff --git a/backend/app/plugin/module_ai/chat/schema.py b/backend/app/plugin/module_ai/chat/schema.py index 865839d4..06ee6f44 100644 --- a/backend/app/plugin/module_ai/chat/schema.py +++ b/backend/app/plugin/module_ai/chat/schema.py @@ -1,9 +1,70 @@ -from pydantic import BaseModel, Field +from dataclasses import dataclass +from typing import Any + +from fastapi import Query +from pydantic import BaseModel, ConfigDict, Field + +from app.core.validator import DateTimeStr class ChatQuerySchema(BaseModel): """WebSocket聊天查询模型""" - message: str = Field(..., description="消息内容") - session_id: int | None = Field(None, description="会话ID") - files: list[dict] | None = Field(None, description="文件信息") + session_id: str | None = Field(None, description="会话ID") + files: list[dict[str, Any]] | None = Field(None, description="文件信息") + + +class ChatSessionCreateSchema(BaseModel): + """创建会话模型""" + title: str = Field(..., description="会话标题") + + +class ChatSessionUpdateSchema(BaseModel): + """更新会话模型""" + title: str = Field(..., description="会话标题") + + +class ChatSessionMessageSchema(BaseModel): + """会话消息模型""" + id: str = Field(..., description="消息ID") + role: str = Field(..., description="消息角色") + content: str = Field(..., description="消息内容") + created_at: int | None = Field(None, description="创建时间(Unix时间戳)") + + model_config = ConfigDict(from_attributes=True) + + +@dataclass +class ChatSessionQueryParam: + """会话查询参数""" + def __init__( + self, + title: str | None = Query(None, description="会话标题"), + created_at: list[DateTimeStr] | None = Query( + None, + description="创建时间范围", + examples=["2025-01-01 00:00:00", "2025-12-31 23:59:59"], + ), + updated_at: list[DateTimeStr] | None = Query( + None, + description="更新时间范围", + examples=["2025-01-01 00:00:00", "2025-12-31 23:59:59"], + ), + ) -> None: + self.title = title + self.created_at = created_at + self.updated_at = updated_at + + +class AiChatRequestSchema(BaseModel): + """AI 对话请求模型(非流式)""" + message: str = Field(..., description="用户消息内容") + session_id: str | None = Field(None, description="会话ID,不传则创建新会话") + + +class AiChatResponseSchema(BaseModel): + """AI 对话响应模型(非流式)""" + response: str = Field(..., description="AI 回复内容") + session_id: str = Field(..., description="会话ID") + function_calls: list[dict[str, Any]] | None = Field(None, description="函数调用信息") + action: dict[str, Any] | None = Field(None, description="建议执行的操作") diff --git a/backend/app/plugin/module_ai/chat/service.py b/backend/app/plugin/module_ai/chat/service.py index ea1e46cd..7dcf0a26 100644 --- a/backend/app/plugin/module_ai/chat/service.py +++ b/backend/app/plugin/module_ai/chat/service.py @@ -1,97 +1,357 @@ from collections.abc import AsyncGenerator +from datetime import datetime from typing import Any -from langchain_core.messages import HumanMessage, SystemMessage -from langchain_openai import ChatOpenAI +from agno.run.team import TeamRunOutput +from agno.session.team import TeamSession +from agno.team.team import Team -from app.config.setting import settings +from app.api.v1.module_system.auth.schema import AuthSchema +from app.api.v1.module_system.dept.service import DeptService +from app.common.request import PaginationService from app.core.exceptions import CustomException from app.core.logger import log -from .schema import ChatQuerySchema +from .crud import ChatSessionCRUD +from .schema import ( + ChatQuerySchema, + ChatSessionCreateSchema, + ChatSessionQueryParam, + ChatSessionUpdateSchema, +) +from .utils import AgnoFactory + + +async def _format_session_data(session: TeamSession, auth: AuthSchema | None = None) -> dict[str, Any]: + """格式化会话数据,添加前端需要的字段""" + if hasattr(session, 'to_dict'): + session_dict = session.to_dict() + else: + session_dict = { + 'session_id': getattr(session, 'session_id', ''), + 'agent_id': getattr(session, 'agent_id', None), + 'team_id': getattr(session, 'team_id', None), + 'workflow_id': getattr(session, 'workflow_id', None), + 'user_id': getattr(session, 'user_id', None), + 'session_data': getattr(session, 'session_data', None), + 'agent_data': getattr(session, 'agent_data', None), + 'team_data': getattr(session, 'team_data', None), + 'workflow_data': getattr(session, 'workflow_data', None), + 'metadata': getattr(session, 'metadata', None), + 'runs': getattr(session, 'runs', []), + 'summary': getattr(session, 'summary', None), + 'created_at': getattr(session, 'created_at', None), + 'updated_at': getattr(session, 'updated_at', None), + } + + session_data = session_dict.get("session_data") or {} + runs = session_dict.get("runs") or [] + messages = _extract_messages(runs) + + # 从 session_data 中获取 session_name 作为标题 + session_name = session_data.get("session_name") if session_data else None + + result = { + **session_dict, + "id": session_dict.get("session_id"), + "title": session_name or session_dict.get("session_id", "")[:8] or "未命名会话", + "created_time": _unix_to_datetime(session_dict.get("created_at")), + "updated_time": _unix_to_datetime(session_dict.get("updated_at")), + "message_count": len(messages), + "messages": messages, + } + + # 如果有 auth,查询部门名称 + if auth and session_dict.get("team_id"): + try: + team_id = session_dict.get("team_id") + if isinstance(team_id, str): + dept_name = await DeptService.get_dept_detail_service(auth=auth, id=int(team_id)) + result["team_name"] = dept_name.get("name") + elif isinstance(team_id, int): + dept_name = await DeptService.get_dept_detail_service(auth=auth, id=team_id) + result["team_name"] = dept_name.get("name") + else: + result["team_name"] = None + except Exception: + result["team_name"] = None + else: + result["team_name"] = None + + # 如果 summary 是 SessionSummary 对象,提取 summary 字段 + summary = session_dict.get("summary") + if summary: + if isinstance(summary, dict): + result["summary"] = summary.get("summary") or summary.get("summary") + else: + result["summary"] = str(summary) + + return result + + +def _unix_to_datetime(timestamp: int | None) -> str | None: + """将Unix时间戳转换为日期时间字符串""" + if timestamp is None: + return None + try: + dt = datetime.fromtimestamp(timestamp) + return dt.strftime("%Y-%m-%d %H:%M:%S") + except (ValueError, TypeError, OSError): + return None + + +def _extract_messages(runs: list[dict[str, Any]]) -> list[dict[str, Any]]: + """从 runs 中提取消息""" + messages = [] + if not runs: + return messages + for run in runs: + if not isinstance(run, dict): + continue + run_messages = run.get("messages", []) + if run_messages and isinstance(run_messages, list): + for msg in run_messages: + if isinstance(msg, dict): + role = msg.get("role") + if role in ("user", "assistant"): + messages.append({ + "id": msg.get("id"), + "role": role, + "content": msg.get("content", ""), + "created_at": msg.get("created_at"), + }) + return messages class ChatService: - """ - 聊天会话管理模块服务层 - """ + """聊天会话管理模块服务层""" @classmethod - async def chat_query(cls, query: ChatQuerySchema) -> AsyncGenerator[str, Any]: - """ - 处理聊天查询 - - 参数: - - query (ChatQuerySchema): 聊天查询模型 - - config (AgentConfigSchema | None): 智能体配置模型 - - 返回: - - AsyncGenerator[str, None]: 异步生成器,每次返回一个聊天响应 - """ - - llm = ChatOpenAI( - api_key=lambda: settings.OPENAI_API_KEY, - model=settings.OPENAI_MODEL, - base_url=settings.OPENAI_BASE_URL, - streaming=True, - ) + async def chat_query( + cls, query: ChatQuerySchema, auth: AuthSchema + ) -> AsyncGenerator[str, None]: + """处理聊天查询并返回流式响应""" + try: + # 创建 CRUD 实例获取数据库连接 + crud = ChatSessionCRUD(auth) - system_prompt = ( - """你是一个有用的AI助手,可以帮助用户回答问题和提供帮助。请用中文回答用户的问题。""" - ) + # 获取或创建会话 + session_id = query.session_id + if not session_id: + # 创建新会话 + import uuid + session_id = str(uuid.uuid4()) + session: TeamSession | None = await crud.create_crud( + data=ChatSessionCreateSchema(title="新对话") + ) + if not session: + raise CustomException(msg="创建会话失败") + session_id = session.session_id - messages = [ - SystemMessage(content=system_prompt), - HumanMessage(content=query.message), - ] + # 创建 AgnoFactory 实例并创建 Team,传入数据库连接 + agno_factory = AgnoFactory() + dept_id = str(auth.user.dept_id) if auth and auth.user and hasattr(auth.user, 'dept_id') and auth.user.dept_id else "default" + agent = agno_factory.create_agent( + user_id=auth.user.username if auth and auth.user else "user", + dept_id=dept_id, + session_id=session_id, + db=crud.db + ) - try: - async for chunk in llm.astream(messages): - yield chunk.text + # 执行聊天查询 - 使用流式输出 + async for chunk in agent.arun(input=query.message, stream=True): + if chunk and chunk.content: + yield chunk.content except Exception as e: - log.debug(f"关闭 LLM 客户端时发生异常(预期行为,服务可能正在关闭): {e}") - - status_code = getattr(e, "status_code", None) - body = getattr(e, "body", None) - message = None - error_type = None - error_code = None - try: - if isinstance(body, dict) and "error" in body: - err = body.get("error") or {} - error_type = err.get("type") - error_code = err.get("code") - message = err.get("message") - except Exception: - raise CustomException(f"解析 OpenAI 错误失败: {e!s}") - - text = str(e) - msg = message or text - - if ( - (error_code == "Arrearage") - or (error_type == "Arrearage") - or ("in good standing" in (msg or "")) - ): - raise ValueError( - "账户欠费或结算异常,访问被拒绝。请检查账号状态或更换有效的 API Key。" + log.error(f"聊天查询失败: {e}") + yield f"抱歉,处理您的请求时出现错误:{str(e)}" + + @classmethod + async def chat_non_stream( + cls, message: str, session_id: str | None, auth: AuthSchema + ) -> dict[str, Any]: + """处理聊天查询并返回非流式响应""" + try: + # 创建 CRUD 实例获取数据库连接 + crud = ChatSessionCRUD(auth) + + # 获取或创建会话 + if not session_id: + # 创建新会话 + import uuid + session_id = str(uuid.uuid4()) + session: TeamSession | None = await crud.create_crud( + data=ChatSessionCreateSchema(title="新对话") ) - if status_code == 401 or "invalid api key" in msg.lower(): - raise ValueError("鉴权失败,API Key 无效或已过期。请检查系统配置中的 API Key。") - if status_code == 403 or error_type in { - "PermissionDenied", - "permission_denied", - }: - raise ValueError("访问被拒绝,权限不足或账号受限。请检查账户权限设置。") - if status_code == 429 or error_type in { - "insufficient_quota", - "rate_limit_exceeded", - }: - raise ValueError("请求过于频繁或配额已用尽。请稍后重试或提升账户配额。") - if status_code == 400: - raise ValueError(f"请求参数错误或服务拒绝:{message or '请检查输入内容。'}") - if status_code in {500, 502, 503, 504}: - raise ValueError("服务暂时不可用,请稍后重试。") - - raise CustomException(f"处理您的请求时出现错误:{msg}") + if not session: + raise CustomException(msg="创建会话失败") + session_id = session.session_id + + # 创建 AgnoFactory 实例并创建 Team,传入数据库连接 + agno_factory = AgnoFactory() + dept_id = str(auth.user.dept_id) if auth and auth.user and hasattr(auth.user, 'dept_id') and auth.user.dept_id else "default" + agent: Team = agno_factory.create_agent( + user_id=auth.user.username if auth and auth.user else "user", + dept_id=dept_id, + session_id=session_id, + db=crud.db + ) + + # 执行聊天查询 + response: TeamRunOutput = await agent.arun(input=message) + + # 解析响应内容和操作建议 + response_text = "" + action = None + + if response and response.content: + response_text = response.content + # 尝试从 response 中解析操作建议 + # 如果 AI 返回了 JSON 格式的操作建议 + import json + try: + # 检查响应是否包含 JSON 格式的操作建议 + if response_text.strip().startswith('{') and response_text.strip().endswith('}'): + action = json.loads(response_text) + elif '```json' in response_text: + # 提取 JSON 代码块 + json_start = response_text.find('```json') + 7 + json_end = response_text.find('```', json_start) + if json_end > json_start: + json_str = response_text[json_start:json_end].strip() + action = json.loads(json_str) + except (json.JSONDecodeError, Exception): + pass + + # 如果没有解析到 JSON,尝试从文本中提取操作信息 + if not action: + action = cls._parse_action_from_response(response_text) + + return { + "response": response_text, + "session_id": session_id, + "function_calls": None, + "action": action, + } + + except Exception as e: + log.error(f"聊天查询失败: {e}") + return { + "response": f"抱歉,处理您的请求时出现错误:{str(e)}", + "session_id": session_id if 'session_id' in locals() else None, + "function_calls": None, + "action": None, + } + + @staticmethod + def _parse_action_from_response(response_text: str) -> dict[str, Any] | None: + """从响应文本中解析操作建议""" + + # 定义路由配置 + route_config = { + "用户管理": {"path": "/system/user", "name": "用户管理"}, + "角色管理": {"path": "/system/role", "name": "角色管理"}, + "菜单管理": {"path": "/system/menu", "name": "菜单管理"}, + "部门管理": {"path": "/system/dept", "name": "部门管理"}, + "字典管理": {"path": "/system/dict", "name": "字典管理"}, + "系统日志": {"path": "/system/log", "name": "系统日志"}, + } + + # 检查是否包含导航意图 + navigation_keywords = ["跳转", "打开", "进入", "前往", "去", "浏览", "查看"] + has_navigation = any(keyword in response_text for keyword in navigation_keywords) + + if not has_navigation: + return None + + # 查找页面名称 + for page_name, route_info in route_config.items(): + if page_name in response_text: + return { + "type": "navigate", + "path": route_info["path"], + "name": route_info["name"], + } + + # 尝试从关键词匹配 + keyword_mapping = { + "用户": {"path": "/system/user", "name": "用户管理"}, + "角色": {"path": "/system/role", "name": "角色管理"}, + "菜单": {"path": "/system/menu", "name": "菜单管理"}, + "部门": {"path": "/system/dept", "name": "部门管理"}, + "字典": {"path": "/system/dict", "name": "字典管理"}, + "日志": {"path": "/system/log", "name": "系统日志"}, + } + + for keyword, route_info in keyword_mapping.items(): + if keyword in response_text: + return { + "type": "navigate", + "path": route_info["path"], + "name": route_info["name"], + } + + return None + + @classmethod + async def create_service( + cls, auth: AuthSchema, data: ChatSessionCreateSchema + ) -> dict[str, Any] | None: + """创建会话""" + crud = ChatSessionCRUD(auth) + session = await crud.create_crud(data=data) + if session: + return await _format_session_data(session, auth) + return None + + @classmethod + async def get_session_service( + cls, auth: AuthSchema, session_id: str + ) -> dict[str, Any] | None: + """获取单个会话详情""" + crud = ChatSessionCRUD(auth) + session: TeamSession | None = await crud.get_by_id_crud(session_id=session_id) + if session: + return await _format_session_data(session, auth) + return None + + @classmethod + async def page_service( + cls, + auth: AuthSchema, + page_no: int, + page_size: int, + search: ChatSessionQueryParam, + order_by: list[dict[str, str]] | None = None, + ) -> dict[str, Any]: + """分页获取会话列表 - 使用内存分页""" + crud = ChatSessionCRUD(auth) + # 获取所有会话 + sessions = await crud.list_crud() + + # 转换为响应模型 - 使用 TeamSession 内置的 to_dict 方法并格式化 + items = [await _format_session_data(s, auth) for s in sessions] + + # 使用 PaginationService 进行内存分页 + result = await PaginationService.paginate( + data_list=items, + page_no=page_no, + page_size=page_size, + ) + + return result + + @classmethod + async def update_service( + cls, auth: AuthSchema, session_id: str, data: ChatSessionUpdateSchema + ) -> bool: + """更新会话""" + crud = ChatSessionCRUD(auth) + success = await crud.update_crud(session_id=session_id, data=data) + return success + + @classmethod + async def delete_service(cls, auth: AuthSchema, session_ids: list[str]) -> None: + """删除会话""" + await ChatSessionCRUD(auth).delete_crud(session_ids=session_ids) diff --git a/backend/app/plugin/module_ai/chat/utils.py b/backend/app/plugin/module_ai/chat/utils.py new file mode 100644 index 00000000..cfba6fa0 --- /dev/null +++ b/backend/app/plugin/module_ai/chat/utils.py @@ -0,0 +1,62 @@ +from typing import Any + +from agno.agent import Agent +from agno.models.openai.like import OpenAILike +from agno.team import Team + +from app.config.setting import settings + + +class AgnoFactory: + """Agno 工厂类 - 统一管理 Agent、Team 创建逻辑""" + # 配置常量 + AGENT_DESCRIPTION = "你是一个有用的AI助手,可以帮助用户回答问题和提供帮助。" + AGENT_INSTRUCTIONS = ["保持回答简洁明了", "如果不确定,请说明"] + AGENT_EXPECTED_OUTPUT = "中文回答" + AGENT_TEMPERATURE = 0.7 + NUM_HISTORY_RUNS = 3 + + def create_agent( + self, + user_id: str, + dept_id: str, + session_id: str, + db: Any | None = None + ) -> Team: + """创建 Team 实例""" + + # 创建 Agent + fastapiadmin_agent = Agent( + id=user_id, + name="fastapiadmin_agent", + role="You are a helpful AI assistant", + description=self.AGENT_DESCRIPTION, + tools=[], + ) + + # 创建 Team + fastapiadmin_team = Team( + id=dept_id, + 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, + ), + members=[fastapiadmin_agent], + instructions=self.AGENT_INSTRUCTIONS, + expected_output=self.AGENT_EXPECTED_OUTPUT, + add_datetime_to_context=True, + add_history_to_context=True, + markdown=True, + num_history_runs=self.NUM_HISTORY_RUNS, + input_schema=None, + output_schema=None, + parse_response=True, + read_chat_history=True, + db=db, + ) + + return fastapiadmin_team diff --git a/backend/app/plugin/module_ai/chat/ws.py b/backend/app/plugin/module_ai/chat/ws.py index 0ce16acf..051df973 100644 --- a/backend/app/plugin/module_ai/chat/ws.py +++ b/backend/app/plugin/module_ai/chat/ws.py @@ -1,15 +1,11 @@ import json -import time from fastapi import APIRouter, WebSocket -from app.api.v1.module_system.auth.schema import AuthSchema from app.core.database import async_db_session from app.core.dependencies import _verify_token from app.core.logger import log from app.core.router_class import OperationLogRoute -from app.plugin.module_ai.chat_message.schema import ChatMessageCreateSchema -from app.plugin.module_ai.chat_message.service import ChatMessageService from .schema import ChatQuerySchema from .service import ChatService @@ -38,7 +34,6 @@ async def websocket_chat_controller( # 从查询参数获取token并认证 token = websocket.query_params.get("token") - auth = None if token: try: # 获取数据库和redis连接 @@ -59,55 +54,15 @@ async def websocket_chat_controller( query = ChatQuerySchema(**message_data) log.info(f"收到聊天查询: {query}- 会话ID: {query.session_id}") - # 保存用户消息到数据库(使用独立的事务) - if query.session_id: - async with async_db_session() as msg_db: - async with msg_db.begin(): - msg_auth = AuthSchema(db=msg_db, check_data_scope=False) - msg_auth.user = auth.user - user_message_data = ChatMessageCreateSchema( - session_id=query.session_id, - type="user", - content=query.message, - timestamp=int(time.time()), - files=query.files - ) - log.info(f"准备保存用户消息: session_id={query.session_id}, content={query.message[:50]}...") - await ChatMessageService.create_service(auth=msg_auth, data=user_message_data) - log.info("用户消息保存成功") - else: - log.warning("未提供会话ID,跳过保存用户消息") - - # 处理AI回复并保存 - full_response = "" - chat_result = ChatService.chat_query(query=query) + # 处理AI回复(使用 agno 记忆存储) + chat_result = ChatService.chat_query(query=query, auth=auth) async for chunk in chat_result: if chunk: try: await websocket.send_text(chunk) - full_response += chunk except RuntimeError: log.warning("WebSocket连接已关闭,停止发送消息") break - - # 保存AI回复到数据库(使用独立的事务) - if query.session_id and full_response: - async with async_db_session() as msg_db: - async with msg_db.begin(): - msg_auth = AuthSchema(db=msg_db, check_data_scope=False) - msg_auth.user = auth.user - assistant_message_data = ChatMessageCreateSchema( - session_id=query.session_id, - type="assistant", - content=full_response, - timestamp=int(time.time()), - files=None - ) - log.info(f"准备保存AI回复: session_id={query.session_id}, content={full_response[:50]}...") - await ChatMessageService.create_service(auth=msg_auth, data=assistant_message_data) - log.info("AI回复保存成功") - else: - log.warning(f"未提供会话ID或AI回复为空,跳过保存AI回复: session_id={query.session_id}, full_response_length={len(full_response)}") except json.JSONDecodeError: log.warning(f"收到非JSON消息: {data}") try: diff --git a/backend/app/plugin/module_ai/chat_message/__init__.py b/backend/app/plugin/module_ai/chat_message/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/backend/app/plugin/module_ai/chat_message/controller.py b/backend/app/plugin/module_ai/chat_message/controller.py deleted file mode 100644 index 5345635a..00000000 --- a/backend/app/plugin/module_ai/chat_message/controller.py +++ /dev/null @@ -1,190 +0,0 @@ -from typing import Annotated - -from fastapi import APIRouter, Body, Depends, Path -from fastapi.responses import JSONResponse - -from app.api.v1.module_system.auth.schema import AuthSchema -from app.common.response import ResponseSchema, SuccessResponse -from app.core.base_params import PaginationQueryParam -from app.core.dependencies import AuthPermission -from app.core.logger import log -from app.core.router_class import OperationLogRoute - -from .schema import ( - ChatMessageCreateSchema, - ChatMessageOutSchema, - ChatMessageQueryParam, - ChatMessageUpdateSchema, -) -from .service import ChatMessageService - -ChatMessageRouter = APIRouter(route_class=OperationLogRoute, prefix="/chat_message", tags=["AI模块"]) - - -@ChatMessageRouter.get( - "/detail/{id}", - summary="获取聊天消息详情", - description="获取聊天消息详情", - response_model=ResponseSchema[ChatMessageOutSchema], -) -async def get_message_detail_controller( - id: Annotated[int, Path(description="消息ID")], - auth: Annotated[AuthSchema, Depends(AuthPermission(["module_ai:chat_message:detail"]))], -) -> JSONResponse: - """ - 获取聊天消息详情 - - 参数: - - id (int): 消息ID - - auth (AuthSchema): 认证信息模型 - - 返回: - - JSONResponse: 包含消息详情的JSON响应 - """ - result_dict = await ChatMessageService.detail_service(id=id, auth=auth) - log.info(f"获取聊天消息详情成功 {id}") - return SuccessResponse(data=result_dict, msg="获取聊天消息详情成功") - - -@ChatMessageRouter.get( - "/list", - summary="查询聊天消息列表", - description="查询聊天消息列表", - response_model=ResponseSchema[list[ChatMessageOutSchema]], -) -async def get_message_list_controller( - page: Annotated[PaginationQueryParam, Depends()], - search: Annotated[ChatMessageQueryParam, Depends()], - auth: Annotated[AuthSchema, Depends(AuthPermission(["module_ai:chat_message:query"]))], -) -> JSONResponse: - """ - 查询聊天消息列表 - - 参数: - - page (PaginationQueryParam): 分页查询参数 - - search (ChatMessageQueryParam): 查询参数 - - auth (AuthSchema): 认证信息模型 - - 返回: - - JSONResponse: 包含消息列表分页信息的JSON响应 - """ - result_dict = await ChatMessageService.page_service( - auth=auth, - page_no=page.page_no, - page_size=page.page_size, - search=search, - order_by=page.order_by, - ) - log.info("查询聊天消息列表成功") - return SuccessResponse(data=result_dict, msg="查询聊天消息列表成功") - - -@ChatMessageRouter.get( - "/session/{session_id}", - summary="按会话ID获取聊天消息", - description="按会话ID获取聊天消息", - response_model=ResponseSchema[list[ChatMessageOutSchema]], -) -async def get_message_by_session_id_controller( - session_id: Annotated[int, Path(description="会话ID")], - page: Annotated[PaginationQueryParam, Depends()], - auth: Annotated[AuthSchema, Depends(AuthPermission(["module_ai:chat_message:query"]))], -) -> JSONResponse: - """ - 按会话ID获取聊天消息 - - 参数: - - session_id (int): 会话ID - - page (PaginationQueryParam): 分页查询参数 - - auth (AuthSchema): 认证信息模型 - - 返回: - - JSONResponse: 包含消息列表分页信息的JSON响应 - """ - result_dict = await ChatMessageService.get_by_session_id_service( - auth=auth, - session_id=session_id, - page_no=page.page_no, - page_size=page.page_size, - ) - log.info(f"按会话ID获取聊天消息成功: {session_id}") - return SuccessResponse(data=result_dict, msg="按会话ID获取聊天消息成功") - - -@ChatMessageRouter.post( - "/create", - summary="创建聊天消息", - description="创建聊天消息", - response_model=ResponseSchema[ChatMessageOutSchema], -) -async def create_message_controller( - data: ChatMessageCreateSchema, - auth: Annotated[AuthSchema, Depends(AuthPermission(["module_ai:chat_message:create"]))], -) -> JSONResponse: - """ - 创建聊天消息 - - 参数: - - data (ChatMessageCreateSchema): 消息创建模型 - - auth (AuthSchema): 认证信息模型 - - 返回: - - JSONResponse: 包含创建消息详情的JSON响应 - """ - result_dict = await ChatMessageService.create_service(auth=auth, data=data) - content = result_dict.get('content', '')[:50] if result_dict.get('content') else '' - log.info(f"创建聊天消息成功: {content}...") - return SuccessResponse(data=result_dict, msg="创建聊天消息成功") - - -@ChatMessageRouter.put( - "/update/{id}", - summary="修改聊天消息", - description="修改聊天消息", - response_model=ResponseSchema[ChatMessageOutSchema], -) -async def update_message_controller( - data: ChatMessageUpdateSchema, - id: Annotated[int, Path(description="消息ID")], - auth: Annotated[AuthSchema, Depends(AuthPermission(["module_ai:chat_message:update"]))], -) -> JSONResponse: - """ - 修改聊天消息 - - 参数: - - data (ChatMessageUpdateSchema): 消息更新模型 - - id (int): 消息ID - - auth (AuthSchema): 认证信息模型 - - 返回: - - JSONResponse: 包含修改消息详情的JSON响应 - """ - result_dict = await ChatMessageService.update_service(auth=auth, id=id, data=data) - content = result_dict.get('content', '')[:50] if result_dict.get('content') else '' - log.info(f"修改聊天消息成功: {content}...") - return SuccessResponse(data=result_dict, msg="修改聊天消息成功") - - -@ChatMessageRouter.delete( - "/delete", - summary="删除聊天消息", - description="删除聊天消息", - response_model=ResponseSchema[None], -) -async def delete_message_controller( - ids: Annotated[list[int], Body(description="消息ID列表")], - auth: Annotated[AuthSchema, Depends(AuthPermission(["module_ai:chat_message:delete"]))], -) -> JSONResponse: - """ - 删除聊天消息 - - 参数: - - ids (list[int]): 消息ID列表 - - auth (AuthSchema): 认证信息模型 - - 返回: - - JSONResponse: 包含删除消息详情的JSON响应 - """ - await ChatMessageService.delete_service(auth=auth, ids=ids) - log.info(f"删除聊天消息成功: {ids}") - return SuccessResponse(msg="删除聊天消息成功") diff --git a/backend/app/plugin/module_ai/chat_message/crud.py b/backend/app/plugin/module_ai/chat_message/crud.py deleted file mode 100644 index 1e2a17c9..00000000 --- a/backend/app/plugin/module_ai/chat_message/crud.py +++ /dev/null @@ -1,179 +0,0 @@ -from collections.abc import Sequence - -from app.api.v1.module_system.auth.schema import AuthSchema -from app.core.base_crud import CRUDBase -from app.core.exceptions import CustomException - -from .model import ChatMessageModel -from .schema import ( - ChatMessageCreateSchema, - ChatMessageOutSchema, - ChatMessageUpdateSchema, -) - - -class ChatMessageCRUD(CRUDBase[ChatMessageModel, ChatMessageCreateSchema, ChatMessageUpdateSchema]): - """聊天消息数据层""" - - def __init__(self, auth: AuthSchema) -> None: - """ - 初始化CRUD数据层 - - 参数: - - auth (AuthSchema): 认证信息模型 - """ - super().__init__(model=ChatMessageModel, auth=auth) - - async def get_by_id_crud(self, id: int, preload: list[str] | None = None) -> ChatMessageModel | None: - """ - 详情 - - 参数: - - id (int): 消息ID - - preload (list[str] | None): 预加载关系,未提供时使用模型默认项 - - 返回: - - ChatMessageModel | None: 消息模型实例或None - """ - return await self.get(id=id, preload=preload) - - async def list_crud( - self, - search: dict | None = None, - order_by: list[dict] | None = None, - preload: list[str] | None = None, - ) -> Sequence[ChatMessageModel]: - """ - 列表查询 - - 参数: - - search (dict | None): 查询参数 - - order_by (list[dict] | None): 排序参数 - - preload (list[str] | None): 预加载关系,未提供时使用模型默认项 - - 返回: - - Sequence[ChatMessageModel]: 消息模型实例序列 - """ - return await self.list(search=search, order_by=order_by, preload=preload) - - async def create_crud(self, data: ChatMessageCreateSchema) -> ChatMessageModel: - """ - 创建 - - 参数: - - data (ChatMessageCreateSchema): 消息创建模型 - - 返回: - - ChatMessageModel: 消息模型实例 - """ - return await self.create(data=data) - - async def update_crud(self, id: int, data: ChatMessageUpdateSchema) -> ChatMessageModel: - """ - 更新 - - 参数: - - id (int): 消息ID - - data (ChatMessageUpdateSchema): 消息更新模型 - - 返回: - - ChatMessageModel: 消息模型实例 - """ - obj = await self.get(id=id, preload=[]) - if not obj: - raise CustomException(msg="更新对象不存在") - - obj_dict = data.model_dump(exclude_unset=True) if not isinstance(data, dict) else data - - for key, value in obj_dict.items(): - if hasattr(obj, key): - setattr(obj, key, value) - - await self.auth.db.flush() - await self.auth.db.refresh(obj) - return obj - - async def delete_crud(self, ids: list[int]) -> None: - """ - 批量删除 - - 参数: - - ids (list[int]): 消息ID列表 - - 返回: - - None - """ - from sqlalchemy import delete - - if not ids: - raise CustomException(msg="删除失败,删除对象不能为空") - - sql = delete(self.model).where(self.model.id.in_(ids)) - await self.auth.db.execute(sql) - await self.auth.db.flush() - - async def get_by_session_id_crud( - self, - session_id: int, - offset: int = 0, - limit: int = 50, - order_by: list[dict] | None = None, - preload: list[str] | None = None, - ) -> dict: - """ - 按会话ID获取消息列表 - - 参数: - - session_id (int): 会话ID - - offset (int): 偏移量 - - limit (int): 每页数量 - - order_by (list[dict] | None): 排序参数 - - preload (list[str] | None): 预加载关系,未提供时使用模型默认项 - - 返回: - - dict: 分页数据 - """ - search = {"session_id": session_id} - order_by_list = order_by or [{"timestamp": "asc"}] - - return await self.page( - offset=offset, - limit=limit, - order_by=order_by_list, - search=search, - out_schema=ChatMessageOutSchema, - preload=preload, - ) - - async def page_crud( - self, - offset: int, - limit: int, - order_by: list[dict] | None = None, - search: dict | None = None, - preload: list | None = None, - ) -> dict: - """ - 分页查询 - - 参数: - - offset (int): 偏移量 - - limit (int): 每页数量 - - order_by (list[dict] | None): 排序参数 - - search (dict | None): 查询参数 - - preload (list | None): 预加载关系,未提供时使用模型默认项 - - 返回: - - dict: 分页数据 - """ - order_by_list = order_by or [{"id": "asc"}] - search_dict = search or {} - - return await self.page( - offset=offset, - limit=limit, - order_by=order_by_list, - search=search_dict, - out_schema=ChatMessageOutSchema, - preload=preload, - ) diff --git a/backend/app/plugin/module_ai/chat_message/model.py b/backend/app/plugin/module_ai/chat_message/model.py deleted file mode 100644 index 396eff5a..00000000 --- a/backend/app/plugin/module_ai/chat_message/model.py +++ /dev/null @@ -1,25 +0,0 @@ -from sqlalchemy import JSON, ForeignKey, Integer, String, Text -from sqlalchemy.orm import Mapped, mapped_column - -from app.core.base_model import ModelMixin - - -class ChatMessageModel(ModelMixin): - """ - 聊天消息表 - """ - - __tablename__: str = "ai_chat_message" - __table_args__: dict[str, str] = {"comment": "聊天消息表"} - - session_id: Mapped[int] = mapped_column( - Integer, - ForeignKey("ai_chat_session.id", ondelete="CASCADE", onupdate="CASCADE"), - nullable=False, - index=True, - comment="会话ID" - ) - type: Mapped[str] = mapped_column(String(20), nullable=False, comment="消息类型: user/assistant") - content: Mapped[str] = mapped_column(Text, nullable=False, comment="消息内容") - timestamp: Mapped[int] = mapped_column(Integer, nullable=False, comment="时间戳") - files: Mapped[dict | None] = mapped_column(JSON, nullable=True, comment="关联的文件信息") diff --git a/backend/app/plugin/module_ai/chat_message/schema.py b/backend/app/plugin/module_ai/chat_message/schema.py deleted file mode 100644 index 8a803132..00000000 --- a/backend/app/plugin/module_ai/chat_message/schema.py +++ /dev/null @@ -1,56 +0,0 @@ -from dataclasses import dataclass - -from fastapi import Query -from pydantic import BaseModel, ConfigDict, Field - -from app.common.enums import QueueEnum -from app.core.base_schema import BaseSchema - - -class ChatMessageCreateSchema(BaseModel): - """新增聊天消息""" - - session_id: int = Field(..., description="会话ID") - type: str = Field(..., description="消息类型: user/assistant") - content: str = Field(..., description="消息内容") - timestamp: int = Field(..., description="时间戳") - files: list[dict] | None = Field(None, description="关联的文件信息") - - -class ChatMessageUpdateSchema(BaseModel): - """更新聊天消息""" - - session_id: int | None = Field(None, description="会话ID") - type: str | None = Field(None, description="消息类型: user/assistant") - content: str | None = Field(None, description="消息内容") - timestamp: int | None = Field(None, description="时间戳") - files: list[dict] | None = Field(None, description="关联的文件信息") - - -class ChatMessageOutSchema(ChatMessageCreateSchema, BaseSchema): - """聊天消息响应模型""" - - model_config = ConfigDict(from_attributes=True) - - -@dataclass -class ChatMessageQueryParam: - """聊天消息查询参数""" - - def __init__( - self, - session_id: int | None = Query(None, description="会话ID"), - type: str | None = Query(None, description="消息类型"), - content: str | None = Query(None, description="消息内容"), - created_time: list[str] | None = Query( - None, - description="创建时间范围", - examples=["2025-01-01 00:00:00", "2025-12-31 23:59:59"], - ), - ) -> None: - self.session_id = (QueueEnum.eq.value, session_id) if session_id else None - self.type = (QueueEnum.eq.value, type) if type else None - self.content = (QueueEnum.like.value, f"%{content}%") if content else None - - if created_time and len(created_time) == 2: - self.created_time = (QueueEnum.between.value, (created_time[0], created_time[1])) diff --git a/backend/app/plugin/module_ai/chat_message/service.py b/backend/app/plugin/module_ai/chat_message/service.py deleted file mode 100644 index 90567511..00000000 --- a/backend/app/plugin/module_ai/chat_message/service.py +++ /dev/null @@ -1,182 +0,0 @@ -from app.api.v1.module_system.auth.schema import AuthSchema -from app.core.exceptions import CustomException -from app.core.logger import log - -from .crud import ChatMessageCRUD -from .schema import ( - ChatMessageCreateSchema, - ChatMessageOutSchema, - ChatMessageQueryParam, - ChatMessageUpdateSchema, -) - - -class ChatMessageService: - """ - 聊天消息管理模块服务层 - """ - - @classmethod - async def detail_service(cls, auth: AuthSchema, id: int) -> dict: - """ - 详情 - - 参数: - - auth (AuthSchema): 认证信息模型 - - id (int): 消息ID - - 返回: - - dict: 消息模型实例字典 - """ - obj = await ChatMessageCRUD(auth).get_by_id_crud(id=id) - if not obj: - raise CustomException(msg="该消息不存在") - result = ChatMessageOutSchema.model_validate(obj).model_dump() - return result - - @classmethod - async def list_service( - cls, - auth: AuthSchema, - search: ChatMessageQueryParam | None = None, - order_by: list[dict[str, str]] | None = None, - ) -> list[dict]: - """ - 列表查询 - - 参数: - - auth (AuthSchema): 认证信息模型 - - search (ChatMessageQueryParam | None): 查询参数 - - order_by (list[dict[str, str]] | None): 排序参数 - - 返回: - - list[dict]: 消息模型实例字典列表 - """ - search_dict = search.__dict__ if search else None - obj_list = await ChatMessageCRUD(auth).list_crud(search=search_dict, order_by=order_by) - result_list = [ChatMessageOutSchema.model_validate(obj).model_dump() for obj in obj_list] - return result_list - - @classmethod - async def page_service( - cls, - auth: AuthSchema, - page_no: int, - page_size: int, - search: ChatMessageQueryParam | None = None, - order_by: list[dict[str, str]] | None = None, - ) -> dict: - """ - 分页查询 - - 参数: - - auth (AuthSchema): 认证信息模型 - - page_no (int): 页码 - - page_size (int): 每页数量 - - search (ChatMessageQueryParam | None): 查询参数 - - order_by (list[dict[str, str]] | None): 排序参数 - - 返回: - - dict: 分页数据 - """ - search_dict = search.__dict__ if search else {} - order_by_list = order_by or [{"id": "desc"}] - offset = (page_no - 1) * page_size - - result = await ChatMessageCRUD(auth).page_crud( - offset=offset, - limit=page_size, - order_by=order_by_list, - search=search_dict, - ) - return result - - @classmethod - async def get_by_session_id_service( - cls, - auth: AuthSchema, - session_id: int, - page_no: int = 1, - page_size: int = 50, - ) -> dict: - """ - 按会话ID获取消息列表 - - 参数: - - auth (AuthSchema): 认证信息模型 - - session_id (int): 会话ID - - page_no (int): 页码 - - page_size (int): 每页数量 - - 返回: - - dict: 分页数据 - """ - offset = (page_no - 1) * page_size - result = await ChatMessageCRUD(auth).get_by_session_id_crud( - session_id=session_id, - offset=offset, - limit=page_size, - ) - return result - - @classmethod - async def create_service(cls, auth: AuthSchema, data: ChatMessageCreateSchema) -> dict: - """ - 创建 - - 参数: - - auth (AuthSchema): 认证信息模型 - - data (ChatMessageCreateSchema): 消息创建模型 - - 返回: - - dict: 消息模型实例字典 - """ - obj = await ChatMessageCRUD(auth).create_crud(data=data) - result = ChatMessageOutSchema.model_validate(obj).model_dump() - content = result.get('content') - log.info(f"创建聊天消息成功: {content[:50] if content else ''}...") - return result - - @classmethod - async def update_service(cls, auth: AuthSchema, id: int, data: ChatMessageUpdateSchema) -> dict: - """ - 更新 - - 参数: - - auth (AuthSchema): 认证信息模型 - - id (int): 消息ID - - data (ChatMessageUpdateSchema): 消息更新模型 - - 返回: - - dict: 消息模型实例字典 - """ - obj = await ChatMessageCRUD(auth).update_crud(id=id, data=data) - if not obj: - raise CustomException(msg="更新失败,该消息不存在") - result = ChatMessageOutSchema.model_validate(obj).model_dump() - content = result.get('content') - log.info(f"更新聊天消息成功: {content[:50] if content else ''}...") - return result - - @classmethod - async def delete_service(cls, auth: AuthSchema, ids: list[int]) -> None: - """ - 删除 - - 参数: - - auth (AuthSchema): 认证信息模型 - - ids (list[int]): 消息ID列表 - - 返回: - - None - """ - if len(ids) < 1: - raise CustomException(msg="删除失败,删除对象不能为空") - - for id in ids: - obj = await ChatMessageCRUD(auth).get_by_id_crud(id=id) - if not obj: - raise CustomException(msg=f"删除失败,ID为{id}的消息不存在") - - await ChatMessageCRUD(auth).delete_crud(ids=ids) - log.info(f"删除聊天消息成功: {ids}") diff --git a/backend/app/plugin/module_ai/chat_session/__init__.py b/backend/app/plugin/module_ai/chat_session/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/backend/app/plugin/module_ai/chat_session/controller.py b/backend/app/plugin/module_ai/chat_session/controller.py deleted file mode 100644 index 1b5ea393..00000000 --- a/backend/app/plugin/module_ai/chat_session/controller.py +++ /dev/null @@ -1,156 +0,0 @@ -from typing import Annotated - -from fastapi import APIRouter, Body, Depends, Path -from fastapi.responses import JSONResponse - -from app.api.v1.module_system.auth.schema import AuthSchema -from app.common.response import ResponseSchema, SuccessResponse -from app.core.base_params import PaginationQueryParam -from app.core.dependencies import AuthPermission -from app.core.logger import log -from app.core.router_class import OperationLogRoute - -from .schema import ( - ChatSessionCreateSchema, - ChatSessionOutSchema, - ChatSessionQueryParam, - ChatSessionUpdateSchema, -) -from .service import ChatSessionService - -ChatSessionRouter = APIRouter(route_class=OperationLogRoute, prefix="/chat_session", tags=["AI模块"]) - - -@ChatSessionRouter.get( - "/detail/{id}", - summary="获取聊天会话详情", - description="获取聊天会话详情", - response_model=ResponseSchema[ChatSessionOutSchema], -) -async def get_session_detail_controller( - id: Annotated[int, Path(description="会话ID")], - auth: Annotated[AuthSchema, Depends(AuthPermission(["module_ai:chat_session:detail"]))], -) -> JSONResponse: - """ - 获取聊天会话详情 - - 参数: - - id (str): 会话ID - - auth (AuthSchema): 认证信息模型 - - 返回: - - JSONResponse: 包含会话详情的JSON响应 - """ - result_dict = await ChatSessionService.detail_service(id=id, auth=auth) - log.info(f"获取聊天会话详情成功 {id}") - return SuccessResponse(data=result_dict, msg="获取聊天会话详情成功") - - -@ChatSessionRouter.get( - "/list", - summary="查询聊天会话列表", - description="查询聊天会话列表", - response_model=ResponseSchema[list[ChatSessionOutSchema]], -) -async def get_session_list_controller( - page: Annotated[PaginationQueryParam, Depends()], - search: Annotated[ChatSessionQueryParam, Depends()], - auth: Annotated[AuthSchema, Depends(AuthPermission(["module_ai:chat_session:query"]))], -) -> JSONResponse: - """ - 查询聊天会话列表 - - 参数: - - page (PaginationQueryParam): 分页查询参数 - - search (ChatSessionQueryParam): 查询参数 - - auth (AuthSchema): 认证信息模型 - - 返回: - - JSONResponse: 包含会话列表分页信息的JSON响应 - """ - result_dict = await ChatSessionService.page_service( - auth=auth, - page_no=page.page_no, - page_size=page.page_size, - search=search, - order_by=page.order_by, - ) - log.info("查询聊天会话列表成功") - return SuccessResponse(data=result_dict, msg="查询聊天会话列表成功") - - -@ChatSessionRouter.post( - "/create", - summary="创建聊天会话", - description="创建聊天会话", - response_model=ResponseSchema[ChatSessionOutSchema], -) -async def create_session_controller( - data: ChatSessionCreateSchema, - auth: Annotated[AuthSchema, Depends(AuthPermission(["module_ai:chat_session:create"]))], -) -> JSONResponse: - """ - 创建聊天会话 - - 参数: - - data (ChatSessionCreateSchema): 会话创建模型 - - auth (AuthSchema): 认证信息模型 - - 返回: - - JSONResponse: 包含创建会话详情的JSON响应 - """ - result_dict = await ChatSessionService.create_service(auth=auth, data=data) - log.info(f"创建聊天会话成功: {result_dict.get('title')}") - return SuccessResponse(data=result_dict, msg="创建聊天会话成功") - - -@ChatSessionRouter.put( - "/update/{id}", - summary="修改聊天会话", - description="修改聊天会话", - response_model=ResponseSchema[ChatSessionOutSchema], -) -async def update_session_controller( - data: ChatSessionUpdateSchema, - id: Annotated[int, Path(description="会话ID")], - auth: Annotated[AuthSchema, Depends(AuthPermission(["module_ai:chat_session:update"]))], -) -> JSONResponse: - """ - 修改聊天会话 - - 参数: - - data (ChatSessionUpdateSchema): 会话更新模型 - - id (int): 会话ID - - auth (AuthSchema): 认证信息模型 - - 返回: - - JSONResponse: 包含修改会话详情的JSON响应 - """ - result_dict = await ChatSessionService.update_service(auth=auth, id=id, data=data) - log.info(f"修改聊天会话成功: {result_dict.get('title')}") - return SuccessResponse(data=result_dict, msg="修改聊天会话成功") - - -@ChatSessionRouter.delete( - "/delete", - summary="删除聊天会话", - description="删除聊天会话", - response_model=ResponseSchema[None], -) -async def delete_session_controller( - ids: Annotated[list[int], Body(description="会话ID列表")], - auth: Annotated[AuthSchema, Depends(AuthPermission(["module_ai:chat_session:delete"]))], -) -> JSONResponse: - """ - 删除聊天会话 - - 参数: - - ids (list[int]): 会话ID列表 - - auth (AuthSchema): 认证信息模型 - - 返回: - - JSONResponse: 包含删除会话详情的JSON响应 - """ - await ChatSessionService.delete_service(auth=auth, ids=ids) - log.info(f"删除聊天会话成功: {ids}") - return SuccessResponse(msg="删除聊天会话成功") diff --git a/backend/app/plugin/module_ai/chat_session/crud.py b/backend/app/plugin/module_ai/chat_session/crud.py deleted file mode 100644 index df1c0570..00000000 --- a/backend/app/plugin/module_ai/chat_session/crud.py +++ /dev/null @@ -1,149 +0,0 @@ -from collections.abc import Sequence - -from app.api.v1.module_system.auth.schema import AuthSchema -from app.core.base_crud import CRUDBase -from app.core.exceptions import CustomException - -from .model import ChatSessionModel -from .schema import ( - ChatSessionCreateSchema, - ChatSessionOutSchema, - ChatSessionUpdateSchema, -) - - -class ChatSessionCRUD(CRUDBase[ChatSessionModel, ChatSessionCreateSchema, ChatSessionUpdateSchema]): - """聊天会话数据层""" - - def __init__(self, auth: AuthSchema) -> None: - """ - 初始化CRUD数据层 - - 参数: - - auth (AuthSchema): 认证信息模型 - """ - super().__init__(model=ChatSessionModel, auth=auth) - - async def get_by_id_crud(self, id: int, preload: list[str] | None = None) -> ChatSessionModel | None: - """ - 详情 - - 参数: - - id (int): 会话ID - - preload (list[str] | None): 预加载关系,未提供时使用模型默认项 - - 返回: - - ChatSessionModel | None: 会话模型实例或None - """ - return await self.get(id=id, preload=preload) - - async def list_crud( - self, - search: dict | None = None, - order_by: list[dict] | None = None, - preload: list[str] | None = None, - ) -> Sequence[ChatSessionModel]: - """ - 列表查询 - - 参数: - - search (dict | None): 查询参数 - - order_by (list[dict] | None): 排序参数 - - preload (list[str] | None): 预加载关系,未提供时使用模型默认项 - - 返回: - - Sequence[ChatSessionModel]: 会话模型实例序列 - """ - return await self.list(search=search, order_by=order_by, preload=preload) - - async def create_crud(self, data: ChatSessionCreateSchema) -> ChatSessionModel: - """ - 创建 - - 参数: - - data (ChatSessionCreateSchema): 会话创建模型 - - 返回: - - ChatSessionModel: 会话模型实例 - """ - return await self.create(data=data) - - async def update_crud(self, id: int, data: ChatSessionUpdateSchema) -> ChatSessionModel: - """ - 更新 - - 参数: - - id (int): 会话ID - - data (ChatSessionUpdateSchema): 会话更新模型 - - 返回: - - ChatSessionModel: 会话模型实例 - """ - obj = await self.get(id=id, preload=[]) - if not obj: - raise CustomException(msg="更新对象不存在") - - obj_dict = data.model_dump(exclude_unset=True) if not isinstance(data, dict) else data - - if self.auth.user and hasattr(obj, "updated_id"): - setattr(obj, "updated_id", self.auth.user.id) - - for key, value in obj_dict.items(): - if hasattr(obj, key): - setattr(obj, key, value) - - await self.auth.db.flush() - await self.auth.db.refresh(obj) - return obj - - async def delete_crud(self, ids: list[int]) -> None: - """ - 批量删除 - - 参数: - - ids (list[int]): 会话ID列表 - - 返回: - - None - """ - from sqlalchemy import delete - - if not ids: - raise CustomException(msg="删除失败,删除对象不能为空") - - sql = delete(self.model).where(self.model.id.in_(ids)) - await self.auth.db.execute(sql) - await self.auth.db.flush() - - async def page_crud( - self, - offset: int, - limit: int, - order_by: list[dict] | None = None, - search: dict | None = None, - preload: list | None = None, - ) -> dict: - """ - 分页查询 - - 参数: - - offset (int): 偏移量 - - limit (int): 每页数量 - - order_by (list[dict] | None): 排序参数 - - search (dict | None): 查询参数 - - preload (list | None): 预加载关系,未提供时使用模型默认项 - - 返回: - - dict: 分页数据 - """ - order_by_list = order_by or [{"id": "desc"}] - search_dict = search or {} - - return await self.page( - offset=offset, - limit=limit, - order_by=order_by_list, - search=search_dict, - out_schema=ChatSessionOutSchema, - preload=preload, - ) diff --git a/backend/app/plugin/module_ai/chat_session/model.py b/backend/app/plugin/module_ai/chat_session/model.py deleted file mode 100644 index 11d6ce2d..00000000 --- a/backend/app/plugin/module_ai/chat_session/model.py +++ /dev/null @@ -1,16 +0,0 @@ -from sqlalchemy import String -from sqlalchemy.orm import Mapped, mapped_column - -from app.core.base_model import ModelMixin, UserMixin - - -class ChatSessionModel(ModelMixin, UserMixin): - """ - 聊天会话表 - """ - - __tablename__: str = "ai_chat_session" - __table_args__: dict[str, str] = {"comment": "聊天会话表"} - __loader_options__: list[str] = ["created_by", "updated_by"] - - title: Mapped[str] = mapped_column(String(200), nullable=False, comment="会话标题") diff --git a/backend/app/plugin/module_ai/chat_session/schema.py b/backend/app/plugin/module_ai/chat_session/schema.py deleted file mode 100644 index 2724d1ec..00000000 --- a/backend/app/plugin/module_ai/chat_session/schema.py +++ /dev/null @@ -1,53 +0,0 @@ -from dataclasses import dataclass - -from fastapi import Query -from pydantic import BaseModel, ConfigDict, Field - -from app.common.enums import QueueEnum -from app.core.base_schema import BaseSchema, UserBySchema - - -class ChatSessionCreateSchema(BaseModel): - """新增聊天会话""" - - title: str = Field(..., description="会话标题") - - -class ChatSessionUpdateSchema(BaseModel): - """更新聊天会话""" - - title: str | None = Field(None, description="会话标题") - - -class ChatSessionOutSchema(ChatSessionCreateSchema, BaseSchema, UserBySchema): - """聊天会话响应模型""" - - model_config = ConfigDict(from_attributes=True) - - -@dataclass -class ChatSessionQueryParam: - """聊天会话查询参数""" - - def __init__( - self, - title: str | None = Query(None, description="会话标题"), - status: str | None = Query(None, description="是否启用"), - created_time: list[str] | None = Query( - None, - description="创建时间范围", - examples=["2025-01-01 00:00:00", "2025-12-31 23:59:59"], - ), - updated_time: list[str] | None = Query( - None, - description="更新时间范围", - examples=["2025-01-01 00:00:00", "2025-12-31 23:59:59"], - ), - ) -> None: - self.title = (QueueEnum.like.value, f"%{title}%") if title else None - self.status = (QueueEnum.eq.value, status) if status else None - - if created_time and len(created_time) == 2: - self.created_time = (QueueEnum.between.value, (created_time[0], created_time[1])) - if updated_time and len(updated_time) == 2: - self.updated_time = (QueueEnum.between.value, (updated_time[0], updated_time[1])) diff --git a/backend/app/plugin/module_ai/chat_session/service.py b/backend/app/plugin/module_ai/chat_session/service.py deleted file mode 100644 index 597ee0b8..00000000 --- a/backend/app/plugin/module_ai/chat_session/service.py +++ /dev/null @@ -1,153 +0,0 @@ -from app.api.v1.module_system.auth.schema import AuthSchema -from app.core.exceptions import CustomException -from app.core.logger import log - -from .crud import ChatSessionCRUD -from .model import ChatSessionModel -from .schema import ( - ChatSessionCreateSchema, - ChatSessionOutSchema, - ChatSessionQueryParam, - ChatSessionUpdateSchema, -) - - -class ChatSessionService: - """ - 聊天会话管理模块服务层 - """ - - @classmethod - async def detail_service(cls, auth: AuthSchema, id: int) -> dict: - """ - 详情 - - 参数: - - auth (AuthSchema): 认证信息模型 - - id (str): 会话ID - - 返回: - - dict: 会话模型实例字典 - """ - obj: ChatSessionModel | None = await ChatSessionCRUD(auth).get_by_id_crud(id=id) - if not obj: - raise CustomException(msg="该会话不存在") - result = ChatSessionOutSchema.model_validate(obj).model_dump() - return result - - @classmethod - async def list_service( - cls, - auth: AuthSchema, - search: ChatSessionQueryParam | None = None, - order_by: list[dict[str, str]] | None = None, - ) -> list[dict]: - """ - 列表查询 - - 参数: - - auth (AuthSchema): 认证信息模型 - - search (ChatSessionQueryParam | None): 查询参数 - - order_by (list[dict[str, str]] | None): 排序参数 - - 返回: - - list[dict]: 会话模型实例字典列表 - """ - search_dict = search.__dict__ if search else None - obj_list = await ChatSessionCRUD(auth).list_crud(search=search_dict, order_by=order_by) - result_list = [ChatSessionOutSchema.model_validate(obj).model_dump() for obj in obj_list] - return result_list - - @classmethod - async def page_service( - cls, - auth: AuthSchema, - page_no: int, - page_size: int, - search: ChatSessionQueryParam | None = None, - order_by: list[dict[str, str]] | None = None, - ) -> dict: - """ - 分页查询 - - 参数: - - auth (AuthSchema): 认证信息模型 - - page_no (int): 页码 - - page_size (int): 每页数量 - - search (ChatSessionQueryParam | None): 查询参数 - - order_by (list[dict[str, str]] | None): 排序参数 - - 返回: - - dict: 分页数据 - """ - search_dict = search.__dict__ if search else {} - order_by_list = order_by or [{"id": "desc"}] - offset = (page_no - 1) * page_size - - result = await ChatSessionCRUD(auth).page_crud( - offset=offset, - limit=page_size, - order_by=order_by_list, - search=search_dict, - ) - return result - - @classmethod - async def create_service(cls, auth: AuthSchema, data: ChatSessionCreateSchema) -> dict: - """ - 创建 - - 参数: - - auth (AuthSchema): 认证信息模型 - - data (ChatSessionCreateSchema): 会话创建模型 - - 返回: - - dict: 会话模型实例字典 - """ - obj = await ChatSessionCRUD(auth).create_crud(data=data) - result = ChatSessionOutSchema.model_validate(obj).model_dump() - log.info(f"创建聊天会话成功: {result.get('title')}") - return result - - @classmethod - async def update_service(cls, auth: AuthSchema, id: int, data: ChatSessionUpdateSchema) -> dict: - """ - 更新 - - 参数: - - auth (AuthSchema): 认证信息模型 - - id (int): 会话ID - - data (ChatSessionUpdateSchema): 会话更新模型 - - 返回: - - dict: 会话模型实例字典 - """ - obj = await ChatSessionCRUD(auth).update_crud(id=id, data=data) - if not obj: - raise CustomException(msg="更新失败,该会话不存在") - result = ChatSessionOutSchema.model_validate(obj).model_dump() - log.info(f"更新聊天会话成功: {result.get('title')}") - return result - - @classmethod - async def delete_service(cls, auth: AuthSchema, ids: list[int]) -> None: - """ - 删除 - - 参数: - - auth (AuthSchema): 认证信息模型 - - ids (list[int]): 会话ID列表 - - 返回: - - None - """ - if len(ids) < 1: - raise CustomException(msg="删除失败,删除对象不能为空") - - for id in ids: - obj = await ChatSessionCRUD(auth).get_by_id_crud(id=id) - if not obj: - raise CustomException(msg=f"删除失败,ID为{id}的会话不存在") - - await ChatSessionCRUD(auth).delete_crud(ids=ids) - log.info(f"删除聊天会话成功: {ids}") diff --git a/backend/app/plugin/module_task/node/handlers/__init__.py b/backend/app/plugin/module_task/node/handlers/__init__.py index fb133010..e69de29b 100644 --- a/backend/app/plugin/module_task/node/handlers/__init__.py +++ b/backend/app/plugin/module_task/node/handlers/__init__.py @@ -1,21 +0,0 @@ -""" -节点执行处理器模块 - -提供各种封装好的方法供节点执行函数调用 - -使用方法: - 在节点执行函数中,所有工具函数已经自动加载到全局命名空间, - 可以直接使用,无需导入。 - - 例如: - def handler(*args, **kwargs): - log_info("任务开始") - result = http_get("https://api.example.com") - log_info(f"请求结果: {result}") - return result -""" - -# 这个模块的主要作用是提供文档说明 -# 实际的功能函数在 context.py 中定义,并通过 NodeExecutionContext 注入到执行环境 - -__all__ = [] diff --git a/backend/app/scripts/data/sys_menu.json b/backend/app/scripts/data/sys_menu.json index e6e628e6..54ad1eb2 100644 --- a/backend/app/scripts/data/sys_menu.json +++ b/backend/app/scripts/data/sys_menu.json @@ -2301,30 +2301,30 @@ "description": "AI管理", "children": [ { - "name": "会话标题", + "name": "AI智能助手", "type": 2, - "icon": "el-icon-ChatLineRound", + "icon": "el-icon-ChatDotRound", "order": 1, - "permission": "module_ai:chat_session:query", - "route_name": "ChatSession", - "route_path": "/ai/chat_session", - "component_path": "module_ai/chat_session/index", + "permission": "module_ai:chat:query", + "route_name": "Chat", + "route_path": "/ai/chat", + "component_path": "module_ai/chat/index", "status": "0", "keep_alive": true, "hidden": false, "always_show": false, - "title": "会话标题", + "title": "AI智能助手", "params": null, "affix": false, "redirect": null, - "description": "会话标题", + "description": "AI智能助手", "children": [ { - "name": "会话标题详情", + "name": "AI对话", "type": 3, "icon": null, "order": 1, - "permission": "module_ai:chat_session:detail", + "permission": "module_ai:chat:ws", "route_name": null, "route_path": null, "component_path": null, @@ -2332,18 +2332,18 @@ "keep_alive": true, "hidden": false, "always_show": false, - "title": "会话标题详情", + "title": "AI对话", "params": null, "affix": false, "redirect": null, - "description": "会话标题详情" + "description": "AI对话" }, { - "name": "创建会话标题", + "name": "查询会话", "type": 3, "icon": null, "order": 2, - "permission": "module_ai:chat_session:create", + "permission": "module_ai:chat:query", "route_name": null, "route_path": null, "component_path": null, @@ -2351,18 +2351,18 @@ "keep_alive": true, "hidden": false, "always_show": false, - "title": "创建会话标题", + "title": "查询会话", "params": null, "affix": false, "redirect": null, - "description": "创建会话标题" + "description": "查询会话" }, { - "name": "修改会话标题", + "name": "会话详情", "type": 3, "icon": null, "order": 3, - "permission": "module_ai:chat_session:update", + "permission": "module_ai:chat:detail", "route_name": null, "route_path": null, "component_path": null, @@ -2370,18 +2370,18 @@ "keep_alive": true, "hidden": false, "always_show": false, - "title": "修改会话标题", + "title": "会话详情", "params": null, "affix": false, "redirect": null, - "description": "修改会话标题" + "description": "会话详情" }, { - "name": "删除会话标题", + "name": "创建会话", "type": 3, "icon": null, "order": 4, - "permission": "module_ai:chat_session:delete", + "permission": "module_ai:chat:create", "route_name": null, "route_path": null, "component_path": null, @@ -2389,58 +2389,18 @@ "keep_alive": true, "hidden": false, "always_show": false, - "title": "删除会话标题", + "title": "创建会话", "params": null, "affix": false, "redirect": null, - "description": "删除会话标题" + "description": "创建会话" }, { - "name": "查询会话标题", + "name": "更新会话", "type": 3, "icon": null, "order": 5, - "permission": "module_ai:chat_session:query", - "route_name": null, - "route_path": null, - "component_path": null, - "status": "0", - "keep_alive": true, - "hidden": false, - "always_show": false, - "title": "查询会话标题", - "params": null, - "affix": false, - "redirect": null, - "description": "查询会话标题" - } - ] - }, - { - "name": "会话内容", - "type": 2, - "icon": "el-icon-ChatSquare", - "order": 2, - "permission": "module_ai:chat_message:query", - "route_name": "ChatMessage", - "route_path": "/ai/chat_message", - "component_path": "module_ai/chat_message/index", - "status": "0", - "keep_alive": true, - "hidden": false, - "always_show": false, - "title": "会话内容", - "params": null, - "affix": false, - "redirect": null, - "description": "会话内容", - "children": [ - { - "name": "会话消息详情", - "type": 3, - "icon": null, - "order": 6, - "permission": "module_ai:chat_message:detail", + "permission": "module_ai:chat:update", "route_name": null, "route_path": null, "component_path": null, @@ -2448,18 +2408,18 @@ "keep_alive": true, "hidden": false, "always_show": false, - "title": "会话消息详情", + "title": "更新会话", "params": null, "affix": false, "redirect": null, - "description": "会话消息详情" + "description": "更新会话" }, { - "name": "创建会话消息", + "name": "删除会话", "type": 3, "icon": null, - "order": 7, - "permission": "module_ai:chat_message:create", + "order": 6, + "permission": "module_ai:chat:delete", "route_name": null, "route_path": null, "component_path": null, @@ -2467,18 +2427,39 @@ "keep_alive": true, "hidden": false, "always_show": false, - "title": "创建会话消息", + "title": "删除会话", "params": null, "affix": false, "redirect": null, - "description": "创建会话消息" - }, + "description": "删除会话" + } + ] + }, + { + "name": "会话记忆", + "type": 2, + "icon": "el-icon-ChatLineSquare", + "order": 2, + "permission": "module_ai:chat:query", + "route_name": "Memory", + "route_path": "/ai/memory", + "component_path": "module_ai/memory/index", + "status": "0", + "keep_alive": true, + "hidden": false, + "always_show": false, + "title": "会话记忆", + "params": null, + "affix": false, + "redirect": null, + "description": "会话记忆管理", + "children": [ { - "name": "修改会话消息", + "name": "查询会话记忆", "type": 3, "icon": null, - "order": 8, - "permission": "module_ai:chat_message:update", + "order": 1, + "permission": "module_ai:chat:query", "route_name": null, "route_path": null, "component_path": null, @@ -2486,18 +2467,18 @@ "keep_alive": true, "hidden": false, "always_show": false, - "title": "修改会话消息", + "title": "查询会话记忆", "params": null, "affix": false, "redirect": null, - "description": "修改会话消息" + "description": "查询会话记忆" }, { - "name": "删除会话消息", + "name": "会话记忆详情", "type": 3, "icon": null, - "order": 9, - "permission": "module_ai:chat_message:delete", + "order": 2, + "permission": "module_ai:chat:detail", "route_name": null, "route_path": null, "component_path": null, @@ -2505,58 +2486,18 @@ "keep_alive": true, "hidden": false, "always_show": false, - "title": "删除会话消息", + "title": "会话记忆详情", "params": null, "affix": false, "redirect": null, - "description": "删除会话消息" + "description": "会话记忆详情" }, { - "name": "查询会话消息", + "name": "删除会话记忆", "type": 3, "icon": null, - "order": 10, - "permission": "module_ai:chat_message:query", - "route_name": null, - "route_path": null, - "component_path": null, - "status": "0", - "keep_alive": true, - "hidden": false, - "always_show": false, - "title": "查询会话消息", - "params": null, - "affix": false, - "redirect": null, - "description": "查询会话消息" - } - ] - }, - { - "name": "AI智能助手", - "type": 2, - "icon": "el-icon-ChatDotRound", - "order": 3, - "permission": "module_ai:chat:ws", - "route_name": "Chat", - "route_path": "/application/ai", - "component_path": "module_ai/chat/index", - "status": "0", - "keep_alive": true, - "hidden": false, - "always_show": false, - "title": "AI智能助手", - "params": null, - "affix": false, - "redirect": null, - "description": "AI智能助手", - "children": [ - { - "name": "AI对话", - "type": 3, - "icon": null, - "order": 1, - "permission": "module_ai:chat:ws", + "order": 3, + "permission": "module_ai:chat:delete", "route_name": null, "route_path": null, "component_path": null, @@ -2564,11 +2505,11 @@ "keep_alive": true, "hidden": false, "always_show": false, - "title": "AI对话", + "title": "删除会话记忆", "params": null, "affix": false, "redirect": null, - "description": "AI对话" + "description": "删除会话记忆" } ] } diff --git a/backend/docs/fastapiadmin.md b/backend/docs/fastapiadmin.md new file mode 100644 index 00000000..6d1669b7 --- /dev/null +++ b/backend/docs/fastapiadmin.md @@ -0,0 +1,446 @@ +## 📘 项目介绍 + +**FastApiAdmin** 是一套 **完全开源、高度模块化、技术先进的现代化快速开发平台**,旨在帮助开发者高效搭建高质量的企业级中后台系统。该项目采用 **前后端分离架构**,融合 Python 后端框架 `FastAPI` 和前端主流框架 `Vue3` 实现多端统一开发,提供了一站式开箱即用的开发体验。 + +> **设计初心**: 以模块化、松耦合为核心,追求丰富的功能模块、简洁易用的接口、详尽的开发文档和便捷的维护方式。通过统一框架和组件,降低技术选型成本,遵循开发规范和设计模式,构建强大的代码分层模型,搭配完善的本地中文化支持,专为团队和企业开发场景量身定制。 + +## 🎯 核心优势 + +| 优势 | 描述 | +| ---- | ---- | +| 🔥 **现代化技术栈** | 基于 FastAPI + Vue3 + TypeScript 等前沿技术构建 | +| ⚡ **高性能异步** | 利用 FastAPI 异步特性和 Redis 缓存优化响应速度 | +| 🔐 **安全可靠** | JWT + OAuth2 认证机制,RBAC 权限控制模型 | +| 🧱 **模块化设计** | 高度解耦的系统架构,便于扩展和维护 | +| 🌐 **全栈支持** | Web端 + 移动端(H5) + 后端一体化解决方案 | +| 🚀 **快速部署** | Docker 一键部署,支持生产环境快速上线 | +| 📖 **完善文档** | 详细的开发文档和教程,降低学习成本 | +| 🤖 **智能体框架** | 基于Langchain和Langgraph的开发智能体 | + +## 🍪 演示环境 + +- 💻 网页端:[https://service.fastapiadmin.com/web](https://service.fastapiadmin.com/web) +- 📱 移动端:[https://service.fastapiadmin.com/app](https://service.fastapiadmin.com/app) +- 👤 登录账号:`admin` 密码:`123456` + +## 🔗 源码仓库 + +| 平台 | 仓库地址 | +|------|----------| +| GitHub | [FastapiAdmin主工程](https://github.com/fastapiadmin/FastapiAdmin.git) \| [FastDocs官网](https://github.com/fastapiadmin/FastDocs.git) \| [FastApp移动端](https://github.com/fastapiadmin/FastApp.git) | +| Gitee | [FastapiAdmin主工程](https://gitee.com/fastapiadmin/FastapiAdmin.git) \| [FastDocs官网](https://gitee.com/fastapiadmin/FastDocs.git) \| [FastApp移动端](https://gitee.com/fastapiadmin/FastApp.git) | + +## 📦 工程结构概览 + +```sh +FastapiAdmin +├─ backend # 后端工程 (FastAPI + Python) +├─ frontend # Web前端工程 (Vue3 + Element Plus) +├─ devops # 部署配置 +├─ docker-compose.yaml # Docker编排文件 +├─ deploy.sh # 一键部署脚本 +├─ LICENSE # 开源协议 +|─ README.en.md # 英文文档 +└─ README.md # 中文文档 +``` + +## 🛠️ 技术栈概览 + +| 类型 | 技术选型 | 描述 | +|------|----------|------| +| **后端框架** | FastAPI / Uvicorn / Pydantic 2.0 / Alembic | 现代、高性能的异步框架,强制类型约束,数据迁移 | +| **ORM** | SQLAlchemy 2.0 | 强大的 ORM 库 | +| **定时任务** | APScheduler | 轻松实现定时任务 | +| **权限认证** | PyJWT | 实现 JWT 认证 | +| **前端框架** | Vue3 / Vite5 / Pinia / TypeScript | 快速开发 Vue3 应用 | +| **Web UI** | ElementPlus | 企业级 UI 组件库 | +| **移动端** | UniApp / Wot Design Uni | 跨端移动应用框架 | +| **数据库** | MySQL / PostgreSQL / Sqlite | 关系型和文档型数据库支持 | +| **缓存** | Redis | 高性能缓存数据库 | +| **文档** | Swagger / Redoc | 自动生成 API 文档 | +| **部署** | Docker / Nginx / Docker Compose | 容器化部署方案 | +| **智能体框架** | Langchain / Langgraph | 基于Langchain和Langgraph的智能体框架 | + +## 📌 内置功能模块 + +| 模块 | 功能 | 描述 | +|------|------|------| +| 📊 **仪表盘** | 工作台、分析页 | 系统概览和数据分析 | +| ⚙️ **系统管理** | 用户、角色、菜单、部门、岗位、字典、配置、公告 | 核心系统管理功能 | +| 👀 **监控管理** | 在线用户、服务器监控、缓存监控 | 系统运行状态监控 | +| 📋 **任务管理** | 定时任务 | 异步任务调度管理 | +| 📝 **日志管理** | 操作日志 | 用户行为审计 | +| 🧰 **开发工具** | 代码生成、表单构建、接口文档 | 提升开发效率的工具 | +| 📁 **文件管理** | 文件存储 | 统一文件管理 | + +## 🔧 模块展示 + +### web 端 + +| 模块名
| 截图 | +| ----- | --- | +| 仪表盘 | ![仪表盘](https://gitee.com/fastapiadmin/FastDocs/raw/master/docs/public/dashboard.png) | +| 代码生成 | ![代码生成](https://gitee.com/fastapiadmin/FastDocs/raw/master/docs/public/gencode.png) | +| 智能助手 | ![智能助手](https://gitee.com/fastapiadmin/FastDocs/raw/master/docs/public/ai.png) | + +### 移动端 + +| 登录
| 首页
| 个人中心
| +|----------|----------|----------| +| ![移动端登录](https://gitee.com/fastapiadmin/FastDocs/raw/master/docs/public/app_login.png) | ![移动端首页](https://gitee.com/fastapiadmin/FastDocs/raw/master/docs/public/app_home.png) | ![移动端个人中心](https://gitee.com/fastapiadmin/FastDocs/raw/master/docs/public/app_mine.png) | + +## 🚀 快速开始 + +### 环境要求 + +| 类型 | 技术栈 | 版本 | +|------|--------|------| +| 后端 | Python | 3.12 ≥ 3.10 | +| 后端 | FastAPI | 0.109+ | +| 前端 | Node.js | ≥ 20.0 | +| 前端 | Vue3 | 3.3+ | +| 数据库 | MySQL/PostgreSQL | 8.0+/17+ | +| 缓存 | Redis | 7.0+ | + +### 获取代码 + +```bash +# 克隆代码到本地 +git clone https://gitee.com/fastapiadmin/FastapiAdmin.git +# 或者 +git clone https://github.com/fastapiadmin/FastapiAdmin.git +``` + +> **后端注意**:克隆下的代码需要修改 `backend/env` 目录下的 `.env.dev.example` 文件为 `.env.dev`,修改 `backend/env` 目录下的 `.env.prod.example` 文件为 `.env.prod`,然后根据实际情况修改数据库连接信息、Redis连接信息等。 + +> **前端注意**:克隆下的代码需要修改 `frontend` 目录下的 `.env.development.example` 文件为 `.env.development`,修改 `frontend` 目录下的 `.env.production.example` 文件为 `.env.production`,然后根据实际情况修改接口地址等。 + +### 后端启动 + +#### 使用 uv 管理项目(推荐) + +```bash +# 进入后端工程目录 +cd backend +# 使用 uv 安装依赖 +uv add -r requirements.txt +# 启动后端服务:启动之前保证mysql中创建好了数据库、redis服务 +uv run main.py run +# 或指定环境 +uv run main.py run --env=dev or --env=prod +``` + +#### 使用传统 pip 方式 + +```bash +# 进入后端工程目录 +cd backend +# 安装依赖 +pip3 install -r requirements.txt +# 启动后端服务:启动之前保证mysql中创建好了数据库、redis服务 +python main.py run +# 或指定环境 +python main.py run --env=dev or --env=prod +``` + +### 前端启动 + +```bash +# 进入前端工程目录 +cd frontend +# 安装依赖 +pnpm install +# 启动开发服务器 +pnpm run dev +# 构建生产版本 +pnpm run build +``` + +### 🐳 Docker 部署 + +#### 方式一:脚本放在项目内执行(推荐) + +```bash +# 1. 克隆代码到服务器 +git clone https://gitee.com/fastapiadmin/FastapiAdmin.git +cd FastapiAdmin + +# 2. 赋予执行权限并部署 +chmod +x deploy.sh +./deploy.sh + +# 查看容器日志 +./deploy.sh logs + +# 停止服务 +./deploy.sh stop + +# 重启服务 +./deploy.sh restart + +# 更新代码并重启(不重新构建镜像,适合后端代码热更新) +./deploy.sh update +``` + +#### 方式二:脚本放在项目外执行 + +```bash +# 1. 将部署脚本复制到服务器 +cp deploy.sh /home/ +cd /home +chmod +x deploy.sh + +# 2. 执行一键部署(会自动克隆项目) +./deploy.sh + +# 查看容器日志 +./deploy.sh logs + +# 停止服务 +./deploy.sh stop + +# 重启服务 +./deploy.sh restart + +# 更新代码并重启(不重新构建镜像,适合后端代码热更新) +./deploy.sh update +``` + +> **注意**: +> - 首次部署时会自动拉取代码并构建镜像 +> - 前端使用本地构建的 dist 目录,如需更新前端请先本地构建并提交到仓库 +> - 确保 `devops/nginx/ssl/` 目录包含 SSL 证书文件(如使用 HTTPS) + +## 🛠️ 二开教程 + +### 后端开发 + +项目采用**插件化架构设计**,二次开发建议在 `backend/app/plugin` 目录下进行,系统会**自动发现并注册**所有符合规范的路由,便于模块管理和升级维护。 + +#### 插件化架构特性 + +- **自动路由发现**:系统会自动扫描 `backend/app/plugin/` 目录下所有 `controller.py` 文件 +- **自动路由注册**:所有路由会被自动注册到对应的前缀路径 (module_xxx -> /xxx) +- **模块化管理**:按功能模块组织代码,便于维护和扩展 +- **支持多层级嵌套**:支持模块内部多层级嵌套结构 + +#### 插件目录结构 + +```sh +backend/app/plugin/ +├── module_application/ # 应用模块(自动映射为 /application) +│ └── ai/ # AI子模块 +│ ├── controller.py # 控制器文件 +│ ├── model.py # 数据模型文件 +│ ├── schema.py # 数据验证文件 +│ ├── service.py # 业务逻辑文件 +│ └── crud.py # 数据访问文件 +├── module_example/ # 示例模块(自动映射为 /example) +│ └── demo/ # 子模块 +│ ├── controller.py # 控制器文件 +│ ├── model.py # 数据模型文件 +│ ├── schema.py # 数据验证文件 +│ ├── service.py # 业务逻辑文件 +│ └── crud.py # 数据访问文件 +├── module_generator/ # 代码生成模块(自动映射为 /generator) +└── init_app.py # 插件初始化文件 +``` + +#### 自动路由注册机制 + +系统会**自动发现并注册**所有符合以下条件的路由: +1. 控制器文件必须命名为 `controller.py` +2. 路由会自动映射:`module_xxx` -> `/xxx` +3. 支持多个 `APIRouter` 实例 +4. 自动处理路由去重 + +#### 二次开发步骤 + +1. **创建插件模块**:在 `backend/app/plugin/` 目录下创建新的模块目录,如 `module_yourfeature` +2. **编写数据模型**:在 `model.py` 中定义数据库模型 +3. **编写数据验证**:在 `schema.py` 中定义数据验证模型 +4. **编写数据访问层**:在 `crud.py` 中编写数据库操作逻辑 +5. **编写业务逻辑层**:在 `service.py` 中编写业务逻辑 +6. **编写控制器**:在 `controller.py` 中定义路由和处理函数 +7. **自动注册**:系统会自动扫描并注册所有路由,无需手动配置 + +#### 控制器示例 + +```python +# backend/app/plugin/module_yourfeature/yourcontroller/controller.py +from fastapi import APIRouter, Depends, Path +from fastapi.responses import JSONResponse + +from app.common.response import SuccessResponse +from app.core.router_class import OperationLogRoute +from app.core.dependencies import AuthPermission +from app.api.v1.module_system.auth.schema import AuthSchema +from .service import YourFeatureService + +# 创建路由实例 +YourFeatureRouter = APIRouter( + route_class=OperationLogRoute, + prefix="/yourcontroller", + tags=["你的功能模块"] +) + +@YourFeatureRouter.get("/detail/{id}", summary="获取详情") +async def get_detail( + id: int = Path(..., description="功能ID"), + auth: AuthSchema = Depends(AuthPermission(["module_yourfeature:yourcontroller:detail"])) +) -> JSONResponse: + result = await YourFeatureService.detail_service(id=id, auth=auth) + return SuccessResponse(data=result) + +@YourFeatureRouter.get("/list", summary="获取列表") +async def get_list( + auth: AuthSchema = Depends(AuthPermission(["module_yourfeature:yourcontroller:list"])) +) -> JSONResponse: + result = await YourFeatureService.list_service(auth=auth) + return SuccessResponse(data=result) +``` + +#### 开发规范 + +1. **命名规范**:模块名采用 `module_xxx` 格式,控制器名采用驼峰命名法 +2. **权限控制**:所有API接口必须添加权限控制装饰器 +3. **日志记录**:使用 `OperationLogRoute` 类自动记录操作日志 +4. **返回格式**:统一使用 `SuccessResponse` 或 `ErrorResponse` 返回响应 +5. **代码注释**:为所有API接口添加详细的文档字符串 + +#### 注意事项 + +- 插件模块名必须以 `module_` 开头 +- 控制器文件必须命名为 `controller.py` +- 路由会自动映射到对应的前缀路径 +- 无需手动注册路由,系统会自动发现并注册 + +### 前端部分 + +1. **配置前端API**:在 `frontend/src/api/` 目录下创建对应的API文件 +2. **编写页面组件**:在 `frontend/src/views/` 目录下创建页面组件 +3. **注册路由**:在 `frontend/src/router/index.ts` 中注册路由 + +### 代码生成器使用 + +项目内置代码生成器,可以根据数据库表结构自动生成前后端代码,大幅提升开发效率。 + +#### 生成步骤 + +1. **登录系统**:使用管理员账号登录系统 +2. **进入代码生成模块**:在左侧菜单中点击"代码生成" +3. **导入表结构**:选择要生成代码的数据库表 +4. **配置生成参数**:填写模块名称、功能名称等 +5. **生成代码**:点击"生成代码"按钮 +6. **下载或写入**:选择下载代码包或直接写入项目目录 + +#### 生成文件结构 + +```sh +# 后端文件 +backend/app/plugin/module_yourmodule/ +└── yourfeature/ + ├── controller.py # 控制器文件 + ├── model.py # 数据模型文件 + ├── schema.py # 数据验证文件 + ├── service.py # 业务逻辑文件 + └── crud.py # 数据访问文件 + +# 前端文件 +frontend/src/ +├── api/module_yourmodule/ +│ └── yourfeature.ts # API调用文件 +└── views/module_yourmodule/ + └── yourfeature/ + └── index.vue # 页面组件 +``` + +#### 生成代码示例 + +```python +# 生成的控制器代码示例 +from fastapi import APIRouter, Depends +from fastapi.responses import JSONResponse + +from app.common.response import SuccessResponse +from app.core.router_class import OperationLogRoute +from app.core.dependencies import AuthPermission +from app.api.v1.module_system.auth.schema import AuthSchema +from .service import YourFeatureService +from .schema import ( + YourFeatureCreateSchema, + YourFeatureUpdateSchema, + YourFeatureQueryParam +) + +YourFeatureRouter = APIRouter( + route_class=OperationLogRoute, + prefix="/yourfeature", + tags=["你的功能模块"] +) + +@YourFeatureRouter.get("/detail/{id}") +async def get_detail( + id: int, + auth: AuthSchema = Depends(AuthPermission(["module_yourmodule:yourfeature:detail"])) +) -> JSONResponse: + result = await YourFeatureService.detail_service(id=id, auth=auth) + return SuccessResponse(data=result) +``` + +### 开发工具 + +- **代码生成器**:自动生成前后端CRUD代码 +- **API文档**:自动生成Swagger/Redoc API文档 +- **数据库迁移**:支持Alembic数据库迁移 +- **日志系统**:内置日志记录和查询功能 +- **监控系统**:内置服务器监控和缓存监控功能 + +### 开发流程 + +1. **需求分析**:明确功能需求和业务逻辑 +2. **数据库设计**:设计数据库表结构 +3. **代码生成**:使用代码生成器生成基础代码 +4. **业务逻辑开发**:完善业务逻辑和接口 +5. **前端开发**:开发前端页面和交互 +6. **测试**:进行单元测试和集成测试 +7. **部署**:部署到生产环境 + +### 开发注意事项 + +1. **权限控制**:所有API接口必须添加权限控制 +2. **数据验证**:所有输入数据必须进行验证 +3. **异常处理**:统一处理API异常 +4. **日志记录**:关键操作必须记录日志 +5. **性能优化**:注意API性能优化,避免慢查询 +6. **代码规范**:遵循PEP8和项目代码规范 + +### 常见问题 + +#### Q:如何添加新功能模块? +A:按照二次开发步骤,在 `backend/app/plugin/` 目录下创建新的模块目录,编写相关代码即可。 + +#### Q:如何配置数据库? +A:在 `backend/env/.env.dev` 或 `backend/env/.env.prod` 文件中配置数据库连接信息。 + +#### Q:如何配置Redis? +A:在 `backend/env/.env.dev` 或 `backend/env/.env.prod` 文件中配置Redis连接信息。 + +#### Q:如何生成数据库迁移文件? +A:使用 `python main.py revision --env=dev` 命令生成迁移文件。 + +#### Q:如何应用数据库迁移? +A:使用 `python main.py upgrade --env=dev` 命令应用迁移。 + +#### Q:如何启动开发服务器? +A:使用 `python main.py run --env=dev` 命令启动开发服务器。 + +#### Q:如何构建前端生产版本? +A:使用 `pnpm run build` 命令构建前端生产版本。 + +#### Q:如何部署到生产环境? +A:使用 `./deploy.sh` 脚本一键部署到生产环境。 + +## ℹ️ 帮助 + +更多详情请查看 [官方文档](https://service.fastapiadmin.com) diff --git a/backend/pyproject.toml b/backend/pyproject.toml index 6184a721..04bc1d28 100644 --- a/backend/pyproject.toml +++ b/backend/pyproject.toml @@ -5,6 +5,7 @@ description = "fastapiadmin后端工程" readme = "README.md" requires-python = ">=3.10" dependencies = [ + "agno==2.5.8", "aiofiles==24.1.0", # 文件操作 "aiosqlite==0.17.0", # sqlite 异步操作数据库 "alembic==1.15.1", # 数据库迁移 diff --git a/backend/requirements.txt b/backend/requirements.txt index 3d5b390b..f4b684de 100755 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -37,8 +37,6 @@ asyncpg==0.30.0 # postgresql 异步操作数据库基于 psycopg-binary==3.3.2 # postgresql 异步操作数据库基于 psycopg2:asyncpg 是 psycopg2 的异步版本,psycopg2 是一个 pure-Python PostgreSQL 数据库适配器。 psycopg==3.3.2 # postgresql 同步操作数据库基于 psycopg是psycopg2升级版:psycopg2 是一个 pure-Python PostgreSQL 适配器。 aiosqlite==0.17.0 # sqlite 异步操作数据库,同步是python自带sqlite无需额外库 -langchain==1.2.0 # 大模型 -langchain-openai==1.1.6 # 大模型 openai 适配器 -langchain-chroma==1.1.0 # 向量数据库 +agno==2.5.8 # 大模型开发框架 ruff==0.14.13 # 代码格式化 pytest==9.0.2 # 测试框架 diff --git a/backend/uv.lock b/backend/uv.lock index 94115596..ff3588b9 100644 --- a/backend/uv.lock +++ b/backend/uv.lock @@ -8,6 +8,30 @@ resolution-markers = [ "python_full_version < '3.11'", ] +[[package]] +name = "agno" +version = "2.5.8" +source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" } +dependencies = [ + { name = "docstring-parser" }, + { name = "gitpython" }, + { name = "h11" }, + { name = "httpx", extra = ["http2"] }, + { name = "packaging" }, + { name = "pydantic" }, + { name = "pydantic-settings" }, + { name = "python-dotenv" }, + { name = "python-multipart" }, + { name = "pyyaml" }, + { name = "rich" }, + { name = "typer" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/f0/9c/93bbc5f01b14d3dd1c22ef9ffe88251ba335a53e4ea44f5417167a2477ce/agno-2.5.8.tar.gz", hash = "sha256:52ff13f34b805e484e1614fd7099146c629abd2f3087f761e2c948544731b91a", size = 1760789, upload-time = "2026-03-06T15:09:20.068Z" } +wheels = [ + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/aa/f7/0e5e646ac719a6e20bbcdb7486f5fbf36baa7a47320e7c144c78d23c6ddb/agno-2.5.8-py3-none-any.whl", hash = "sha256:d01c44f2c622651d9735b622e4bc6a584931d18a7711d12ca87cdd06f9d8a578", size = 2100283, upload-time = "2026-03-06T15:09:10.224Z" }, +] + [[package]] name = "aiofiles" version = "24.1.0" @@ -189,6 +213,7 @@ name = "backend" version = "2.0.0" source = { virtual = "." } dependencies = [ + { name = "agno" }, { name = "aiofiles" }, { name = "aiosqlite" }, { name = "alembic" }, @@ -244,6 +269,7 @@ dev = [ [package.metadata] requires-dist = [ + { name = "agno", specifier = "==2.5.8" }, { name = "aiofiles", specifier = "==24.1.0" }, { name = "aiosqlite", specifier = "==0.17.0" }, { name = "alembic", specifier = "==1.15.1" }, @@ -790,6 +816,30 @@ wheels = [ { url = "https://pypi.tuna.tsinghua.edu.cn/packages/e6/ab/fb21f4c939bb440104cc2b396d3be1d9b7a9fd3c6c2a53d98c45b3d7c954/fsspec-2026.2.0-py3-none-any.whl", hash = "sha256:98de475b5cb3bd66bedd5c4679e87b4fdfe1a3bf4d707b151b3c07e58c9a2437", size = 202505, upload-time = "2026-02-05T21:50:51.819Z" }, ] +[[package]] +name = "gitdb" +version = "4.0.12" +source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" } +dependencies = [ + { name = "smmap" }, +] +sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/72/94/63b0fc47eb32792c7ba1fe1b694daec9a63620db1e313033d18140c2320a/gitdb-4.0.12.tar.gz", hash = "sha256:5ef71f855d191a3326fcfbc0d5da835f26b13fbcba60c32c21091c349ffdb571", size = 394684, upload-time = "2025-01-02T07:20:46.413Z" } +wheels = [ + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/a0/61/5c78b91c3143ed5c14207f463aecfc8f9dbb5092fb2869baf37c273b2705/gitdb-4.0.12-py3-none-any.whl", hash = "sha256:67073e15955400952c6565cc3e707c554a4eea2e428946f7a4c162fab9bd9bcf", size = 62794, upload-time = "2025-01-02T07:20:43.624Z" }, +] + +[[package]] +name = "gitpython" +version = "3.1.46" +source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" } +dependencies = [ + { name = "gitdb" }, +] +sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/df/b5/59d16470a1f0dfe8c793f9ef56fd3826093fc52b3bd96d6b9d6c26c7e27b/gitpython-3.1.46.tar.gz", hash = "sha256:400124c7d0ef4ea03f7310ac2fbf7151e09ff97f2a3288d64a440c584a29c37f", size = 215371, upload-time = "2026-01-01T15:37:32.073Z" } +wheels = [ + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/6a/09/e21df6aef1e1ffc0c816f0522ddc3f6dcded766c3261813131c78a704470/gitpython-3.1.46-py3-none-any.whl", hash = "sha256:79812ed143d9d25b6d176a10bb511de0f9c67b1fa641d82097b0ab90398a2058", size = 208620, upload-time = "2026-01-01T15:37:30.574Z" }, +] + [[package]] name = "googleapis-common-protos" version = "1.72.0" @@ -935,6 +985,19 @@ wheels = [ { url = "https://pypi.tuna.tsinghua.edu.cn/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" }, ] +[[package]] +name = "h2" +version = "4.3.0" +source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" } +dependencies = [ + { name = "hpack" }, + { name = "hyperframe" }, +] +sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/1d/17/afa56379f94ad0fe8defd37d6eb3f89a25404ffc71d4d848893d270325fc/h2-4.3.0.tar.gz", hash = "sha256:6c59efe4323fa18b47a632221a1888bd7fde6249819beda254aeca909f221bf1", size = 2152026, upload-time = "2025-08-23T18:12:19.778Z" } +wheels = [ + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/69/b2/119f6e6dcbd96f9069ce9a2665e0146588dc9f88f29549711853645e736a/h2-4.3.0-py3-none-any.whl", hash = "sha256:c438f029a25f7945c69e0ccf0fb951dc3f73a5f6412981daee861431b70e2bdd", size = 61779, upload-time = "2025-08-23T18:12:17.779Z" }, +] + [[package]] name = "hf-xet" version = "1.2.0" @@ -964,6 +1027,15 @@ wheels = [ { url = "https://pypi.tuna.tsinghua.edu.cn/packages/cb/44/870d44b30e1dcfb6a65932e3e1506c103a8a5aea9103c337e7a53180322c/hf_xet-1.2.0-cp37-abi3-win_amd64.whl", hash = "sha256:e6584a52253f72c9f52f9e549d5895ca7a471608495c4ecaa6cc73dba2b24d69", size = 2905735, upload-time = "2025-10-24T19:04:35.928Z" }, ] +[[package]] +name = "hpack" +version = "4.1.0" +source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" } +sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/2c/48/71de9ed269fdae9c8057e5a4c0aa7402e8bb16f2c6e90b3aa53327b113f8/hpack-4.1.0.tar.gz", hash = "sha256:ec5eca154f7056aa06f196a557655c5b009b382873ac8d1e66e79e87535f1dca", size = 51276, upload-time = "2025-01-22T21:44:58.347Z" } +wheels = [ + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/07/c6/80c95b1b2b94682a72cbdbfb85b81ae2daffa4291fbfa1b1464502ede10d/hpack-4.1.0-py3-none-any.whl", hash = "sha256:157ac792668d995c657d93111f46b4535ed114f0c9c8d672271bbec7eae1b496", size = 34357, upload-time = "2025-01-22T21:44:56.92Z" }, +] + [[package]] name = "httpcore" version = "1.0.9" @@ -1036,6 +1108,11 @@ wheels = [ { url = "https://pypi.tuna.tsinghua.edu.cn/packages/56/95/9377bcb415797e44274b51d46e3249eba641711cf3348050f76ee7b15ffc/httpx-0.27.2-py3-none-any.whl", hash = "sha256:7bb2708e112d8fdd7829cd4243970f0c223274051cb35ee80c03301ee29a3df0", size = 76395, upload-time = "2024-08-27T12:53:59.653Z" }, ] +[package.optional-dependencies] +http2 = [ + { name = "h2" }, +] + [[package]] name = "httpx-sse" version = "0.4.3" @@ -1078,6 +1155,15 @@ wheels = [ { url = "https://pypi.tuna.tsinghua.edu.cn/packages/f0/0f/310fb31e39e2d734ccaa2c0fb981ee41f7bd5056ce9bc29b2248bd569169/humanfriendly-10.0-py2.py3-none-any.whl", hash = "sha256:1697e1a8a8f550fd43c2865cd84542fc175a61dcb779b6fee18cf6b6ccba1477", size = 86794, upload-time = "2021-09-17T21:40:39.897Z" }, ] +[[package]] +name = "hyperframe" +version = "6.1.0" +source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" } +sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/02/e7/94f8232d4a74cc99514c13a9f995811485a6903d48e5d952771ef6322e30/hyperframe-6.1.0.tar.gz", hash = "sha256:f630908a00854a7adeabd6382b43923a4c4cd4b821fcb527e6ab9e15382a3b08", size = 26566, upload-time = "2025-01-22T21:41:49.302Z" } +wheels = [ + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/48/30/47d0bf6072f7252e6521f3447ccfa40b421b6824517f82854703d0f5a98b/hyperframe-6.1.0-py3-none-any.whl", hash = "sha256:b03380493a519fce58ea5af42e4a42317bf9bd425596f7a0835ffce80f1a42e5", size = 13007, upload-time = "2025-01-22T21:41:47.295Z" }, +] + [[package]] name = "idna" version = "3.11" @@ -3348,6 +3434,15 @@ wheels = [ { url = "https://pypi.tuna.tsinghua.edu.cn/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" }, ] +[[package]] +name = "smmap" +version = "5.0.3" +source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" } +sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/1f/ea/49c993d6dfdd7338c9b1000a0f36817ed7ec84577ae2e52f890d1a4ff909/smmap-5.0.3.tar.gz", hash = "sha256:4d9debb8b99007ae47165abc08670bd74cb74b5227dda7f643eccc4e9eb5642c", size = 22506, upload-time = "2026-03-09T03:43:26.1Z" } +wheels = [ + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/c1/d4/59e74daffcb57a07668852eeeb6035af9f32cbfd7a1d2511f17d2fe6a738/smmap-5.0.3-py3-none-any.whl", hash = "sha256:c106e05d5a61449cf6ba9a1e650227ecfb141590d2a98412103ff35d89fc7b2f", size = 24390, upload-time = "2026-03-09T03:43:24.361Z" }, +] + [[package]] name = "sniffio" version = "1.3.1" diff --git a/docker-compose.yaml b/docker-compose.yaml index a52ba87e..e6c3a89e 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -10,7 +10,7 @@ services: MYSQL_ROOT_PASSWORD: "FastApi123abc" MYSQL_DATABASE: "fastapiadmin" MYSQL_PASSWORD: "FastApi123abc" - MYSQL_USER: "root" + MYSQL_USER: "fastapiadmin" MYSQL_TZINFO_TO_SYS_TABLES: 1 # 初始化MySQL时区表,解决时差问题 ports: - "3306:3306" diff --git a/frontend/src/api/module_ai/chat.ts b/frontend/src/api/module_ai/chat.ts new file mode 100644 index 00000000..c0c08637 --- /dev/null +++ b/frontend/src/api/module_ai/chat.ts @@ -0,0 +1,140 @@ +import request from "@/utils/request"; + +const API_PATH = "/ai/chat"; + +export const AiChatAPI = { + getSessionList(query: { + page_no: number; + page_size: number; + title?: string; + created_at?: string[]; + updated_at?: string[]; + }) { + return request>>({ + url: `${API_PATH}/list`, + method: "get", + params: query, + }); + }, + + createSession(body: { title: string }) { + return request>({ + url: `${API_PATH}/create`, + method: "post", + data: body, + }); + }, + + updateSession(id: string, body: { title: string }) { + return request>({ + url: `${API_PATH}/update/${id}`, + method: "put", + data: body, + }); + }, + + deleteSession(body: string[]) { + return request({ + url: `${API_PATH}/delete`, + method: "delete", + data: body, + }); + }, + + chat(body: { message: string; session_id?: string | null }) { + return request>({ + url: `${API_PATH}/ai-chat`, + method: "post", + data: body, + }); + }, + + getSessionDetail(sessionId: string) { + return request>({ + url: `${API_PATH}/detail/${sessionId}`, + method: "get", + }); + }, +}; + +export default AiChatAPI; + +export interface ChatSessionMessage { + id: string; + role: string; + content: string; + created_at: number | null; +} + +export interface ChatSession { + session_id: string; + agent_id: string | null; + team_id: string | null; + team_name: string | null; + workflow_id: string | null; + user_id: string | null; + session_data: Record | null; + agent_data: Record | null; + team_data: Record | null; + workflow_data: Record | null; + metadata: Record | null; + runs: Array> | null; + summary: Record | null; + created_at: number | null; + updated_at: number | null; + + id: string; + title: string | null; + created_time: string | null; + updated_time: string | null; + message_count: number; + messages: ChatSessionMessage[]; +} + +export interface SessionGroup { + id: string; + title: string; + sessions: ChatSession[]; +} + +export interface UserInfo { + id: number; + name: string; + username: string; + avatar: string; + email: string; +} + +export interface AiChatResponse { + response: string; + session_id: string; + function_calls: Array<{ + name: string; + arguments: Record; + }> | null; +} + +export interface ChatSessionDetail { + session_id: string; + agent_id: string | null; + team_id: string | null; + team_name: string | null; + workflow_id: string | null; + user_id: string | null; + session_data: Record | null; + agent_data: Record | null; + team_data: Record | null; + workflow_data: Record | null; + metadata: Record | null; + runs: Array> | null; + summary: Record | null; + created_at: number | null; + updated_at: number | null; + + id: string; + title: string | null; + created_time: string | null; + updated_time: string | null; + message_count: number; + messages: ChatSessionMessage[]; +} diff --git a/frontend/src/api/module_ai/chat_message.ts b/frontend/src/api/module_ai/chat_message.ts deleted file mode 100644 index 4d080e7e..00000000 --- a/frontend/src/api/module_ai/chat_message.ts +++ /dev/null @@ -1,97 +0,0 @@ -import request from "@/utils/request"; - -const API_PATH = "/ai/chat_message"; - -const AiChatMessageAPI = { - listMessage(query?: MessagePageQuery) { - return request>>({ - url: `${API_PATH}/list`, - method: "get", - params: query, - }); - }, - - detailMessage(id: number) { - return request>({ - url: `${API_PATH}/detail/${id}`, - method: "get", - }); - }, - - getMessagesBySession(sessionId: number, query?: PageQuery) { - return request>>({ - url: `${API_PATH}/session/${sessionId}`, - method: "get", - params: query, - }); - }, - - createMessage(body: MessageForm) { - return request>({ - url: `${API_PATH}/create`, - method: "post", - data: body, - }); - }, - - updateMessage(id: number, body: MessageForm) { - return request>({ - url: `${API_PATH}/update/${id}`, - method: "put", - data: body, - }); - }, - - deleteMessage(body: number[]) { - return request({ - url: `${API_PATH}/delete`, - method: "delete", - data: body, - }); - }, -}; - -export default AiChatMessageAPI; - -export interface MessagePageQuery extends PageQuery { - session_id?: number; - type?: string; - content?: string; - created_time?: string[]; - updated_time?: string[]; -} - -export interface MessageTable extends BaseType { - session_id: number; - type: "user" | "assistant"; - content: string; - timestamp: number; - files?: Record[]; -} - -export interface MessageForm extends BaseFormType { - session_id?: number; - type?: "user" | "assistant"; - content?: string; - timestamp?: number; - files?: Record[]; -} - -export interface ChatMessage { - id: string; - type: "user" | "assistant"; - content: string; - timestamp: number; - loading?: boolean; - collapsed?: boolean; - files?: UploadedFile[]; -} - -export interface UploadedFile { - id: string; - name: string; - size: number; - type: string; - url?: string; - file?: File; -} diff --git a/frontend/src/api/module_ai/chat_session.ts b/frontend/src/api/module_ai/chat_session.ts deleted file mode 100644 index 0cd62e72..00000000 --- a/frontend/src/api/module_ai/chat_session.ts +++ /dev/null @@ -1,85 +0,0 @@ -import request from "@/utils/request"; - -const API_PATH = "/ai/chat_session"; - -const AiChatSessionAPI = { - listSession(query?: SessionPageQuery) { - return request>>({ - url: `${API_PATH}/list`, - method: "get", - params: query, - }); - }, - - detailSession(id: number) { - return request>({ - url: `${API_PATH}/detail/${id}`, - method: "get", - }); - }, - - createSession(body: SessionForm) { - return request>({ - url: `${API_PATH}/create`, - method: "post", - data: body, - }); - }, - - updateSession(id: number, body: SessionForm) { - return request>({ - url: `${API_PATH}/update/${id}`, - method: "put", - data: body, - }); - }, - - deleteSession(body: number[]) { - return request({ - url: `${API_PATH}/delete`, - method: "delete", - data: body, - }); - }, -}; - -export default AiChatSessionAPI; - -export interface SessionPageQuery extends PageQuery { - title?: string; - status?: string; - created_time?: string[]; - updated_time?: string[]; -} - -export interface SessionTable extends BaseType { - title: string; - created_by?: CommonType; - updated_by?: CommonType; -} - -export interface SessionForm extends BaseFormType { - title?: string; -} - -export interface ChatSession { - id: number; - title: string; - created_time: string; - updated_time: string; - message_count?: number; - created_by?: CommonType; - updated_by?: CommonType; -} - -export interface SessionGroup { - title: string; - sessions: ChatSession[]; -} - -export interface UserInfo { - id: number; - name: string; - username: string; - avatar?: string; -} diff --git a/frontend/src/api/module_application/mcp.ts b/frontend/src/api/module_application/mcp.ts deleted file mode 100644 index 46a409c8..00000000 --- a/frontend/src/api/module_application/mcp.ts +++ /dev/null @@ -1,26 +0,0 @@ -import request from "@/utils/request"; - -const API_PATH = "/application/ai"; - -export const McpAPI = { - /** - * 查询应用列表 - * @param query 查询参数 - */ - chatMcp(query: AiChatQuery) { - return request({ - url: `${API_PATH}/chat`, - method: "post", - params: query, - }); - }, -}; - -export default McpAPI; - -/** - * 应用表单 - */ -export interface AiChatQuery { - message: string; -} diff --git a/frontend/src/components/AiAssistant/index.vue b/frontend/src/components/AiAssistant/index.vue index 4194f891..b03388fb 100644 --- a/frontend/src/components/AiAssistant/index.vue +++ b/frontend/src/components/AiAssistant/index.vue @@ -119,11 +119,11 @@ diff --git a/frontend/src/views/module_ai/chat_session/index.vue b/frontend/src/views/module_ai/memory/index.vue similarity index 56% rename from frontend/src/views/module_ai/chat_session/index.vue rename to frontend/src/views/module_ai/memory/index.vue index a2f5472a..0815e952 100644 --- a/frontend/src/views/module_ai/chat_session/index.vue +++ b/frontend/src/views/module_ai/memory/index.vue @@ -6,7 +6,7 @@