Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 15 additions & 2 deletions backend/app/api/v1/module_system/log/model.py
Original file line number Diff line number Diff line change
@@ -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):
"""
系统日志模型
Expand All @@ -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="处理时间")
8 changes: 4 additions & 4 deletions backend/app/core/ap_scheduler.py
Original file line number Diff line number Diff line change
Expand Up @@ -640,22 +640,22 @@ 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

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):
Expand Down
195 changes: 195 additions & 0 deletions backend/app/plugin/module_ai/chat/controller.py
Original file line number Diff line number Diff line change
@@ -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="对话成功",
)
135 changes: 135 additions & 0 deletions backend/app/plugin/module_ai/chat/crud.py
Original file line number Diff line number Diff line change
@@ -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
Loading