diff --git a/CHANGELOG.md b/CHANGELOG.md index 5f29546..22825eb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -23,6 +23,7 @@ ### 代码重构 1.移除对python3.9的支持 (#67)。 +2.移除AI管理模块。 ### 代码优化 diff --git a/README.md b/README.md index 182656a..bb66a30 100644 --- a/README.md +++ b/README.md @@ -35,8 +35,6 @@ 16. 在线构建器:拖动表单元素生成相应的HTML代码。 17. 系统接口:根据业务代码自动生成相关的api接口文档。 18. 代码生成:配置数据库表信息一键生成前后端代码(python、sql、vue、js),支持下载。 -19. AI管理:提供AI模型管理和AI对话功能。 - ## 在线体验 * *账号:admin* diff --git a/shierfeng-fastapi-backend/common/annotation/rate_limit_annotation.py b/shierfeng-fastapi-backend/common/annotation/rate_limit_annotation.py index 1d7e9b8..c354a1b 100644 --- a/shierfeng-fastapi-backend/common/annotation/rate_limit_annotation.py +++ b/shierfeng-fastapi-backend/common/annotation/rate_limit_annotation.py @@ -63,7 +63,6 @@ class ApiRateLimitPreset: ANON_AUTH_CAPTCHA: 匿名验证码类接口限流预设 ANON_PUBLIC_METADATA: 匿名公开元数据接口限流预设 COMMON_UPLOAD: 通用上传接口限流预设 - USER_INTERACTIVE_HIGH_FREQ: 用户高频交互接口限流预设 USER_RESOURCE_EXECUTION: 用户执行类接口限流预设 USER_COMMON_MUTATION: 用户普通写操作接口限流预设 USER_SECURITY_MUTATION: 用户安全敏感操作接口限流预设 @@ -111,12 +110,6 @@ class ApiRateLimitPreset: scope='user_or_ip', ) - USER_INTERACTIVE_HIGH_FREQ = ApiRateLimitPresetConfig( - name='USER_INTERACTIVE_HIGH_FREQ', - limit=40, - window_seconds=60, - scope='user', - ) USER_RESOURCE_EXECUTION = ApiRateLimitPresetConfig( name='USER_RESOURCE_EXECUTION', limit=12, diff --git a/shierfeng-fastapi-backend/common/constant.py b/shierfeng-fastapi-backend/common/constant.py index 1d8cf23..e212659 100644 --- a/shierfeng-fastapi-backend/common/constant.py +++ b/shierfeng-fastapi-backend/common/constant.py @@ -223,13 +223,6 @@ class ApiNamespace: SYSTEM_USER_IMPORT: 用户导入接口命名空间 SYSTEM_USER_EXPORT: 用户导出接口命名空间 - AI_MODEL_LIST: AI模型列表接口命名空间 - AI_MODEL_ALL: AI模型全量列表接口命名空间 - AI_MODEL_DETAIL: AI模型详情接口命名空间 - AI_CHAT_CONFIG: AI对话配置接口命名空间 - AI_CHAT_SEND: AI对话发送接口命名空间 - AI_CHAT_CANCEL: AI对话取消接口命名空间 - TOOL_GEN_LIST: 代码生成列表接口命名空间 TOOL_GEN_DB_LIST: 代码生成数据源列表接口命名空间 TOOL_GEN_DETAIL: 代码生成详情接口命名空间 @@ -319,13 +312,6 @@ class ApiNamespace: SYSTEM_USER_IMPORT = 'system:user:import' SYSTEM_USER_EXPORT = 'system:user:export' - AI_MODEL_LIST = 'ai:model:list' - AI_MODEL_ALL = 'ai:model:all' - AI_MODEL_DETAIL = 'ai:model:detail' - AI_CHAT_CONFIG = 'ai:chat:config' - AI_CHAT_SEND = 'ai:chat:send' - AI_CHAT_CANCEL = 'ai:chat:cancel' - TOOL_GEN_LIST = 'tool:gen:list' TOOL_GEN_DB_LIST = 'tool:gen:db-list' TOOL_GEN_DETAIL = 'tool:gen:detail' @@ -362,8 +348,6 @@ class ApiGroup: CONFIG_MUTATION: 参数配置写操作关联命名空间分组 DICT_TYPE_MUTATION: 字典类型写操作关联命名空间分组 DICT_DATA_MUTATION: 字典数据写操作关联命名空间分组 - AI_MODEL_MUTATION: AI模型写操作关联命名空间分组 - AI_CHAT_CONFIG_MUTATION: AI对话配置写操作关联命名空间分组 GEN_MUTATION: 代码生成写操作关联命名空间分组 """ @@ -389,9 +373,6 @@ class ApiGroup: ApiNamespace.SYSTEM_USER_LIST, ApiNamespace.SYSTEM_USER_DETAIL, ApiNamespace.SYSTEM_USER_PROFILE, - ApiNamespace.AI_MODEL_LIST, - ApiNamespace.AI_MODEL_ALL, - ApiNamespace.AI_MODEL_DETAIL, ) MENU_MUTATION = ( @@ -488,14 +469,6 @@ class ApiGroup: ApiNamespace.SYSTEM_DICT_DATA_DETAIL, ) - AI_MODEL_MUTATION = ( - ApiNamespace.AI_MODEL_LIST, - ApiNamespace.AI_MODEL_ALL, - ApiNamespace.AI_MODEL_DETAIL, - ) - - AI_CHAT_CONFIG_MUTATION = (ApiNamespace.AI_CHAT_CONFIG,) - GEN_MUTATION = ( ApiNamespace.TOOL_GEN_LIST, ApiNamespace.TOOL_GEN_DB_LIST, diff --git a/shierfeng-fastapi-backend/module_ai/controller/ai_chat_controller.py b/shierfeng-fastapi-backend/module_ai/controller/ai_chat_controller.py deleted file mode 100644 index 4255c7b..0000000 --- a/shierfeng-fastapi-backend/module_ai/controller/ai_chat_controller.py +++ /dev/null @@ -1,164 +0,0 @@ -from typing import Annotated - -from fastapi import Body, Path, Request, Response -from fastapi.responses import StreamingResponse -from sqlalchemy.ext.asyncio import AsyncSession - -from common.annotation.cache_annotation import ApiCache, ApiCacheEvict -from common.annotation.log_annotation import Log -from common.annotation.rate_limit_annotation import ApiRateLimit, ApiRateLimitPreset -from common.aspect.db_seesion import DBSessionDependency -from common.aspect.pre_auth import CurrentUserDependency, PreAuthDependency -from common.constant import ApiGroup, ApiNamespace -from common.enums import BusinessType -from common.router import APIRouterPro -from common.vo import DataResponseModel, ResponseBaseModel -from module_admin.entity.vo.user_vo import CurrentUserModel -from module_ai.entity.vo.ai_chat_vo import ( - AiChatConfigModel, - AiChatRequestModel, - AiChatSessionBaseModel, - AiChatSessionModel, -) -from module_ai.service.ai_chat_service import AiChatService -from utils.log_util import logger -from utils.response_util import ResponseUtil - -ai_chat_controller = APIRouterPro( - prefix='/ai/chat', order_num=19, tags=['AI管理-AI对话'], dependencies=[PreAuthDependency()] -) - - -@ai_chat_controller.post( - '/send', - summary='发送对话消息', - description='流式返回对话结果', - response_class=StreamingResponse, - responses={ - 200: { - 'description': '流式返回对话结果', - 'content': { - 'text/event-stream': {}, - }, - } - }, -) -@ApiRateLimit(namespace=ApiNamespace.AI_CHAT_SEND, preset=ApiRateLimitPreset.USER_INTERACTIVE_HIGH_FREQ) -async def send_chat_message( - request: Request, - chat_req: AiChatRequestModel, - query_db: Annotated[AsyncSession, DBSessionDependency()], - current_user: Annotated[CurrentUserModel, CurrentUserDependency()], -) -> StreamingResponse: - user_id = current_user.user.user_id if current_user and current_user.user else 1 - chat_stream = AiChatService.chat_services(query_db, chat_req, user_id) - logger.info(f'用户{user_id}发送对话消息成功') - - return StreamingResponse(content=chat_stream, media_type='text/event-stream') - - -@ai_chat_controller.get( - '/config', - summary='获取用户对话配置', - description='获取当前用户的AI对话配置', - response_model=DataResponseModel[AiChatConfigModel], -) -@ApiCache(namespace=ApiNamespace.AI_CHAT_CONFIG) -async def get_user_chat_config( - request: Request, - query_db: Annotated[AsyncSession, DBSessionDependency()], - current_user: Annotated[CurrentUserModel, CurrentUserDependency()], -) -> Response: - user_id = current_user.user.user_id - ai_chat_config_detail_result = await AiChatService.ai_chat_config_detail_services(query_db, user_id) - logger.info(f'获取user_id为{user_id}的对话配置成功') - - return ResponseUtil.success(data=ai_chat_config_detail_result) - - -@ai_chat_controller.put( - '/config', - summary='保存用户对话配置', - description='保存当前用户的AI对话配置', - response_model=DataResponseModel[AiChatConfigModel], -) -@ApiCacheEvict(namespaces=ApiGroup.AI_CHAT_CONFIG_MUTATION) -@Log(title='AI对话配置管理', business_type=BusinessType.INSERT) -async def save_user_chat_config( - request: Request, - ai_chat_config: AiChatConfigModel, - query_db: Annotated[AsyncSession, DBSessionDependency()], - current_user: Annotated[CurrentUserModel, CurrentUserDependency()], -) -> Response: - user_id = current_user.user.user_id if current_user and current_user.user else 1 - save_ai_chat_config_result = await AiChatService.save_ai_chat_config_services(query_db, user_id, ai_chat_config) - logger.info(save_ai_chat_config_result.message) - - return ResponseUtil.success(msg=save_ai_chat_config_result.message) - - -@ai_chat_controller.get( - '/session/list', - summary='获取会话列表', - description='获取用户的会话列表', - response_model=DataResponseModel[list[AiChatSessionBaseModel]], -) -async def get_chat_session_list( - request: Request, - current_user: Annotated[CurrentUserModel, CurrentUserDependency()], -) -> Response: - result = await AiChatService.get_chat_session_list_services(current_user.user.user_id) - logger.info('获取成功') - - return ResponseUtil.success(data=result) - - -@ai_chat_controller.delete( - '/session/{session_id}', - summary='删除会话', - description='删除指定会话', - response_model=ResponseBaseModel, -) -@Log(title='AI对话会话管理', business_type=BusinessType.DELETE) -async def delete_chat_session( - request: Request, - session_id: Annotated[str, Path(description='会话ID')], - query_db: Annotated[AsyncSession, DBSessionDependency()], -) -> Response: - delete_chat_session_result = await AiChatService.delete_chat_session_services(session_id) - logger.info(delete_chat_session_result.message) - - return ResponseUtil.success(msg=delete_chat_session_result.message) - - -@ai_chat_controller.get( - '/session/{session_id}', - summary='获取会话消息详情', - description='获取指定会话的消息详情', - response_model=DataResponseModel[AiChatSessionModel], -) -async def get_chat_session_detail( - request: Request, - session_id: Annotated[str, Path(description='会话ID')], -) -> Response: - chat_session_detail_result = await AiChatService.get_chat_session_detail_services(session_id) - logger.info(f'获取session_id为{session_id}的信息成功') - - return ResponseUtil.success(data=chat_session_detail_result) - - -@ai_chat_controller.post( - '/cancel', - summary='取消对话', - description='取消正在进行的对话', - response_model=ResponseBaseModel, -) -@ApiRateLimit(namespace=ApiNamespace.AI_CHAT_CANCEL, preset=ApiRateLimitPreset.USER_INTERACTIVE_HIGH_FREQ) -async def cancel_chat_run( - request: Request, - run_id: Annotated[str, Body(embed=True, description='运行ID', alias='runId')], -) -> Response: - cancel_result = await AiChatService.cancel_run_services(run_id) - logger.info(cancel_result.message) - - return ResponseUtil.success(msg=cancel_result.message) diff --git a/shierfeng-fastapi-backend/module_ai/controller/ai_model_controller.py b/shierfeng-fastapi-backend/module_ai/controller/ai_model_controller.py deleted file mode 100644 index 60292ab..0000000 --- a/shierfeng-fastapi-backend/module_ai/controller/ai_model_controller.py +++ /dev/null @@ -1,188 +0,0 @@ -from datetime import datetime -from typing import Annotated - -from fastapi import Path, Query, Request, Response -from pydantic_validation_decorator import ValidateFields -from sqlalchemy import ColumnElement -from sqlalchemy.ext.asyncio import AsyncSession - -from common.annotation.cache_annotation import ApiCache, ApiCacheEvict -from common.annotation.log_annotation import Log, RequestLogFieldRoot -from common.aspect.data_scope import DataScopeDependency -from common.aspect.db_seesion import DBSessionDependency -from common.aspect.interface_auth import UserInterfaceAuthDependency -from common.aspect.pre_auth import CurrentUserDependency, PreAuthDependency -from common.constant import ApiGroup, ApiNamespace -from common.enums import BusinessType -from common.router import APIRouterPro -from common.vo import DataResponseModel, PageResponseModel, ResponseBaseModel -from module_admin.entity.vo.user_vo import CurrentUserModel -from module_ai.entity.do.ai_model_do import AiModels -from module_ai.entity.vo.ai_model_vo import AiModelModel, AiModelPageQueryModel, DeleteAiModelModel -from module_ai.service.ai_model_service import AiModelService -from utils.log_util import logger -from utils.response_util import ResponseUtil - -ai_model_controller = APIRouterPro( - prefix='/ai/model', order_num=18, tags=['AI管理-模型管理'], dependencies=[PreAuthDependency()] -) - - -@ai_model_controller.get( - '/list', - summary='获取AI模型分页列表接口', - description='用于获取AI模型分页列表', - response_model=PageResponseModel[AiModelModel], - dependencies=[UserInterfaceAuthDependency('ai:model:list')], -) -@ApiCache(namespace=ApiNamespace.AI_MODEL_LIST) -async def get_ai_model_list( - request: Request, - ai_model_page_query: Annotated[AiModelPageQueryModel, Query()], - query_db: Annotated[AsyncSession, DBSessionDependency()], - data_scope_sql: Annotated[ColumnElement, DataScopeDependency(AiModels)], -) -> Response: - # 获取分页数据 - result = await AiModelService.get_ai_model_list_services( - query_db, ai_model_page_query, data_scope_sql, is_page=True - ) - logger.info('获取成功') - - return ResponseUtil.success(model_content=result) - - -@ai_model_controller.get( - '/all', - summary='获取AI模型不分页列表接口', - description='用于获取AI模型不分页列表', - response_model=DataResponseModel[AiModelModel], -) -@ApiCache(namespace=ApiNamespace.AI_MODEL_ALL) -async def get_ai_model_all( - request: Request, - query_db: Annotated[AsyncSession, DBSessionDependency()], - data_scope_sql: Annotated[ColumnElement, DataScopeDependency(AiModels)], -) -> Response: - # 获取不分页数据 - ai_model_page_query = AiModelPageQueryModel(status='0') - result = await AiModelService.get_ai_model_list_services( - query_db, ai_model_page_query, data_scope_sql, is_page=False - ) - logger.info('获取成功') - - return ResponseUtil.success(data=result) - - -@ai_model_controller.post( - '', - summary='新增AI模型接口', - description='用于新增AI模型', - response_model=ResponseBaseModel, - dependencies=[UserInterfaceAuthDependency('ai:model:add')], -) -@ValidateFields(validate_model='add_ai_model') -@ApiCacheEvict(namespaces=ApiGroup.AI_MODEL_MUTATION) -@Log( - title='AI模型管理', - business_type=BusinessType.INSERT, - request_log_mode='exclude', - request_exclude_fields=(RequestLogFieldRoot.JSON_BODY.field('api_key'),), -) -async def add_ai_model( - request: Request, - add_ai_model: AiModelModel, - query_db: Annotated[AsyncSession, DBSessionDependency()], - current_user: Annotated[CurrentUserModel, CurrentUserDependency()], -) -> Response: - add_ai_model.user_id = current_user.user.user_id - add_ai_model.dept_id = current_user.user.dept_id - add_ai_model.create_by = current_user.user.user_name - add_ai_model.create_time = datetime.now() - add_ai_model.update_by = current_user.user.user_name - add_ai_model.update_time = datetime.now() - add_ai_model_result = await AiModelService.add_ai_model_services(query_db, add_ai_model) - logger.info(add_ai_model_result.message) - - return ResponseUtil.success(msg=add_ai_model_result.message) - - -@ai_model_controller.put( - '', - summary='编辑AI模型接口', - description='用于编辑AI模型', - response_model=ResponseBaseModel, - dependencies=[UserInterfaceAuthDependency('ai:model:edit')], -) -@ValidateFields(validate_model='edit_ai_model') -@ApiCacheEvict(namespaces=ApiGroup.AI_MODEL_MUTATION) -@Log( - title='AI模型管理', - business_type=BusinessType.UPDATE, - request_log_mode='exclude', - request_exclude_fields=(RequestLogFieldRoot.JSON_BODY.field('api_key'),), -) -async def edit_ai_model( - request: Request, - edit_ai_model: AiModelModel, - query_db: Annotated[AsyncSession, DBSessionDependency()], - current_user: Annotated[CurrentUserModel, CurrentUserDependency()], - data_scope_sql: Annotated[ColumnElement, DataScopeDependency(AiModels)], -) -> Response: - if not current_user.user.admin: - await AiModelService.check_ai_model_data_scope_services(query_db, edit_ai_model.model_id, data_scope_sql) - edit_ai_model.update_by = current_user.user.user_name - edit_ai_model.update_time = datetime.now() - edit_ai_model_result = await AiModelService.edit_ai_model_services(query_db, edit_ai_model) - logger.info(edit_ai_model_result.message) - - return ResponseUtil.success(msg=edit_ai_model_result.message) - - -@ai_model_controller.delete( - '/{model_ids}', - summary='删除AI模型接口', - description='用于删除AI模型', - response_model=ResponseBaseModel, - dependencies=[UserInterfaceAuthDependency('ai:model:remove')], -) -@ApiCacheEvict(namespaces=ApiGroup.AI_MODEL_MUTATION) -@Log(title='AI模型管理', business_type=BusinessType.DELETE) -async def delete_ai_model( - request: Request, - model_ids: Annotated[str, Path(description='需要删除的模型ID')], - query_db: Annotated[AsyncSession, DBSessionDependency()], - current_user: Annotated[CurrentUserModel, CurrentUserDependency()], - data_scope_sql: Annotated[ColumnElement, DataScopeDependency(AiModels)], -) -> Response: - model_id_list = model_ids.split(',') - for model_id in model_id_list: - if not current_user.user.admin: - await AiModelService.check_ai_model_data_scope_services(query_db, int(model_id), data_scope_sql) - delete_ai_model = DeleteAiModelModel(modelIds=model_ids) - delete_ai_model_result = await AiModelService.delete_ai_model_services(query_db, delete_ai_model) - logger.info(delete_ai_model_result.message) - - return ResponseUtil.success(msg=delete_ai_model_result.message) - - -@ai_model_controller.get( - '/{model_id}', - summary='获取AI模型详情接口', - description='用于获取指定AI模型的详细信息', - response_model=DataResponseModel[AiModelModel], - dependencies=[UserInterfaceAuthDependency('ai:model:query')], -) -@ApiCache(namespace=ApiNamespace.AI_MODEL_DETAIL) -async def get_ai_model_detail( - request: Request, - model_id: Annotated[int, Path(description='模型ID')], - query_db: Annotated[AsyncSession, DBSessionDependency()], - current_user: Annotated[CurrentUserModel, CurrentUserDependency()], - data_scope_sql: Annotated[ColumnElement, DataScopeDependency(AiModels)], -) -> Response: - if not current_user.user.admin: - await AiModelService.check_ai_model_data_scope_services(query_db, model_id, data_scope_sql) - ai_model_detail_result = await AiModelService.ai_model_detail_services(query_db, model_id) - logger.info(f'获取model_id为{model_id}的信息成功') - - return ResponseUtil.success(data=ai_model_detail_result) diff --git a/shierfeng-fastapi-backend/module_ai/dao/ai_chat_dao.py b/shierfeng-fastapi-backend/module_ai/dao/ai_chat_dao.py deleted file mode 100644 index b7677e1..0000000 --- a/shierfeng-fastapi-backend/module_ai/dao/ai_chat_dao.py +++ /dev/null @@ -1,52 +0,0 @@ -from sqlalchemy import select, update -from sqlalchemy.ext.asyncio import AsyncSession - -from module_ai.entity.do.ai_chat_do import AiChatConfig -from module_ai.entity.vo.ai_chat_vo import AiChatConfigModel - - -class AiChatConfigDao: - """ - AI对话配置数据库操作层 - """ - - @classmethod - async def get_chat_config_detail_by_user_id(cls, db: AsyncSession, user_id: int) -> AiChatConfig | None: - """ - 根据用户ID获取配置 - - :param db: orm对象 - :param user_id: 用户ID - :return: 配置对象 - """ - ai_chat_config = ( - (await db.execute(select(AiChatConfig).where(AiChatConfig.user_id == user_id))).scalars().first() - ) - - return ai_chat_config - - @classmethod - async def add_chat_config_dao(cls, db: AsyncSession, chat_config: AiChatConfigModel) -> AiChatConfig: - """ - 新增对话配置数据库操作 - - :param db: orm对象 - :param chat_config: 对话配置对象 - :return: 配置对象 - """ - db_chat_config = AiChatConfig(**chat_config.model_dump(exclude_unset=True)) - db.add(db_chat_config) - await db.flush() - - return db_chat_config - - @classmethod - async def edit_chat_config_dao(cls, db: AsyncSession, chat_config: dict) -> None: - """ - 编辑对话配置数据库操作 - - :param db: orm对象 - :param chat_config: 需要更新的对话配置字典 - :return: - """ - await db.execute(update(AiChatConfig), [chat_config]) diff --git a/shierfeng-fastapi-backend/module_ai/dao/ai_model_dao.py b/shierfeng-fastapi-backend/module_ai/dao/ai_model_dao.py deleted file mode 100644 index 60aa59f..0000000 --- a/shierfeng-fastapi-backend/module_ai/dao/ai_model_dao.py +++ /dev/null @@ -1,96 +0,0 @@ -from typing import Any - -from sqlalchemy import ColumnElement, delete, select, update -from sqlalchemy.ext.asyncio import AsyncSession - -from common.vo import PageModel -from module_ai.entity.do.ai_model_do import AiModels -from module_ai.entity.vo.ai_model_vo import AiModelModel, AiModelPageQueryModel -from utils.page_util import PageUtil - - -class AiModelDao: - """ - AI模型管理数据库操作层 - """ - - @classmethod - async def get_ai_model_detail_by_id(cls, db: AsyncSession, model_id: int) -> AiModels | None: - """ - 根据AI模型id获取AI模型详细信息 - - :param db: orm对象 - :param model_id: AI模型id - :return: AI模型信息对象 - """ - ai_model_info = (await db.execute(select(AiModels).where(AiModels.model_id == model_id))).scalars().first() - - return ai_model_info - - @classmethod - async def get_ai_model_list( - cls, db: AsyncSession, query_object: AiModelPageQueryModel, data_scope_sql: ColumnElement, is_page: bool = False - ) -> PageModel | list[dict[str, Any]]: - """ - 根据查询参数获取AI模型列表信息 - - :param db: orm对象 - :param query_object: 查询参数对象 - :param data_scope_sql: 数据权限对应的查询sql语句 - :param is_page: 是否开启分页 - :return: AI模型列表信息对象 - """ - query = ( - select(AiModels) - .where( - AiModels.model_id == query_object.model_id if query_object.model_id else True, - AiModels.model_name.like(f'%{query_object.model_name}%') if query_object.model_name else True, - AiModels.model_code.like(f'%{query_object.model_code}%') if query_object.model_code else True, - AiModels.provider == query_object.provider if query_object.provider else True, - AiModels.status == query_object.status if query_object.status else True, - data_scope_sql, - ) - .order_by(AiModels.model_sort) - ) - ai_model_list: PageModel | list[dict[str, Any]] = await PageUtil.paginate( - db, query, query_object.page_num, query_object.page_size, is_page - ) - - return ai_model_list - - @classmethod - async def add_ai_model_dao(cls, db: AsyncSession, ai_model: AiModelModel) -> AiModels: - """ - 新增AI模型数据库操作 - - :param db: orm对象 - :param ai_model: AI模型对象 - :return: AI模型信息对象 - """ - db_model = AiModels(**ai_model.model_dump(exclude_unset=True)) - db.add(db_model) - await db.flush() - - return db_model - - @classmethod - async def edit_ai_model_dao(cls, db: AsyncSession, ai_model: dict) -> None: - """ - 编辑AI模型数据库操作 - - :param db: orm对象 - :param ai_model: 需要更新的AI模型字典 - :return: - """ - await db.execute(update(AiModels), [ai_model]) - - @classmethod - async def delete_ai_model_dao(cls, db: AsyncSession, ai_model: AiModelModel) -> None: - """ - 删除AI模型数据库操作 - - :param db: orm对象 - :param ai_model: AI模型对象 - :return: - """ - await db.execute(delete(AiModels).where(AiModels.model_id.in_([ai_model.model_id]))) diff --git a/shierfeng-fastapi-backend/module_ai/entity/do/ai_chat_do.py b/shierfeng-fastapi-backend/module_ai/entity/do/ai_chat_do.py deleted file mode 100644 index a07584a..0000000 --- a/shierfeng-fastapi-backend/module_ai/entity/do/ai_chat_do.py +++ /dev/null @@ -1,26 +0,0 @@ -from datetime import datetime - -from sqlalchemy import CHAR, BigInteger, Column, DateTime, Float, Integer, Text - -from config.database import Base - - -class AiChatConfig(Base): - """ - AI对话配置表 - """ - - __tablename__ = 'ai_chat_config' - __table_args__ = {'comment': 'AI对话配置表'} - - chat_config_id = Column(BigInteger, primary_key=True, nullable=False, autoincrement=True, comment='配置主键') - user_id = Column(BigInteger, nullable=False, unique=True, comment='用户ID') - temperature = Column(Float, nullable=True, comment='默认温度') - add_history_to_context = Column(CHAR(1), server_default='0', comment='是否添加历史记录(0是, 1否)') - num_history_runs = Column(Integer, nullable=True, comment='历史记录条数') - system_prompt = Column(Text, nullable=True, comment='系统提示词') - metrics_default_visible = Column(CHAR(1), server_default='0', comment='默认显示指标(0是, 1否)') - vision_enabled = Column(CHAR(1), server_default='1', comment='是否开启视觉(0是, 1否)') - image_max_size_mb = Column(Integer, nullable=True, comment='图片最大大小(MB)') - create_time = Column(DateTime, nullable=True, default=datetime.now, comment='创建时间') - update_time = Column(DateTime, nullable=True, default=datetime.now, comment='更新时间') diff --git a/shierfeng-fastapi-backend/module_ai/entity/do/ai_model_do.py b/shierfeng-fastapi-backend/module_ai/entity/do/ai_model_do.py deleted file mode 100644 index c13c279..0000000 --- a/shierfeng-fastapi-backend/module_ai/entity/do/ai_model_do.py +++ /dev/null @@ -1,62 +0,0 @@ -from datetime import datetime - -from sqlalchemy import CHAR, BigInteger, Column, DateTime, Float, Integer, String - -from config.database import Base -from config.env import DataBaseConfig -from utils.common_util import SqlalchemyUtil - - -class AiModels(Base): - """ - AI模型表 - """ - - __tablename__ = 'ai_models' - __table_args__ = {'comment': 'AI模型表'} - - model_id = Column(BigInteger, primary_key=True, nullable=False, autoincrement=True, comment='模型主键') - model_code = Column(String(100), nullable=False, comment='模型编码') - model_name = Column( - String(100), - nullable=True, - server_default=SqlalchemyUtil.get_server_default_null(DataBaseConfig.db_type), - comment='模型名称', - ) - provider = Column(String(50), nullable=False, comment='提供商') - model_sort = Column(Integer, nullable=False, comment='显示顺序') - api_key = Column( - String(255), - nullable=True, - server_default=SqlalchemyUtil.get_server_default_null(DataBaseConfig.db_type), - comment='API Key', - ) - base_url = Column( - String(255), - nullable=True, - server_default=SqlalchemyUtil.get_server_default_null(DataBaseConfig.db_type), - comment='Base URL', - ) - model_type = Column( - String(50), - nullable=True, - server_default=SqlalchemyUtil.get_server_default_null(DataBaseConfig.db_type), - comment='模型类型', - ) - max_tokens = Column(Integer, nullable=True, comment='最大输出token') - temperature = Column(Float, nullable=True, comment='默认温度') - support_reasoning = Column(CHAR(1), server_default='N', comment='是否支持推理') - support_images = Column(CHAR(1), server_default='N', comment='是否支持图片') - status = Column(CHAR(1), server_default='0', comment='模型状态') - user_id = Column(BigInteger, nullable=True, comment='用户ID') - dept_id = Column(BigInteger, nullable=True, comment='部门ID') - create_by = Column(String(64), nullable=True, server_default="''", comment='创建者') - create_time = Column(DateTime, nullable=True, default=datetime.now(), comment='创建时间') - update_by = Column(String(64), nullable=True, server_default="''", comment='更新者') - update_time = Column(DateTime, nullable=True, default=datetime.now(), comment='更新时间') - remark = Column( - String(500), - nullable=True, - server_default=SqlalchemyUtil.get_server_default_null(DataBaseConfig.db_type), - comment='备注', - ) diff --git a/shierfeng-fastapi-backend/module_ai/entity/vo/ai_chat_vo.py b/shierfeng-fastapi-backend/module_ai/entity/vo/ai_chat_vo.py deleted file mode 100644 index e9d95c3..0000000 --- a/shierfeng-fastapi-backend/module_ai/entity/vo/ai_chat_vo.py +++ /dev/null @@ -1,159 +0,0 @@ -from datetime import datetime -from typing import Literal - -from pydantic import BaseModel, ConfigDict, Field -from pydantic.alias_generators import to_camel - - -class AiChatRequestModel(BaseModel): - """ - AI对话请求模型 - """ - - model_config = ConfigDict(alias_generator=to_camel) - - session_id: str | None = Field(default=None, description='会话ID') - model_id: int = Field(description='模型ID') - message: str = Field(description='用户消息') - is_reasoning: bool | None = Field(default=None, description='本次是否开启深度思考') - images: list[str] | None = Field(default=None, description='图片URL列表') - - -class AiChatConfigModel(BaseModel): - """ - AI对话配置模型 - """ - - model_config = ConfigDict(alias_generator=to_camel) - - chat_config_id: int | None = Field(default=None, description='配置主键') - user_id: int | None = Field(default=None, description='用户ID') - temperature: float | None = Field(default=None, description='默认温度') - add_history_to_context: Literal['0', '1'] | None = Field(default=None, description='是否添加历史记录') - num_history_runs: int | None = Field(default=3, description='历史记录条数') - system_prompt: str | None = Field(default=None, description='系统提示词') - metrics_default_visible: Literal['0', '1'] | None = Field(default=None, description='默认显示指标') - vision_enabled: Literal['0', '1'] | None = Field(default=None, description='是否开启视觉') - image_max_size_mb: int | None = Field(default=None, description='图片最大大小(MB)') - create_time: datetime | None = Field(default=None, description='创建时间') - update_time: datetime | None = Field(default=None, description='更新时间') - - -class AiChatSessionBaseModel(BaseModel): - """ - AI对话会话基础模型 - """ - - model_config = ConfigDict(alias_generator=to_camel) - - session_id: str = Field(description='会话ID') - session_title: str | None = Field(default=None, description='会话标题') - session_type: str | None = Field(default=None, description='会话类型') - user_id: str | None = Field(default=None, description='用户ID') - created_at: datetime | None = Field(default=None, description='创建时间') - updated_at: datetime | None = Field(default=None, description='更新时间') - - -class ModelInfoModel(BaseModel): - """ - 对话会话数据模型-模型信息模型 - """ - - model_config = ConfigDict(alias_generator=to_camel) - - id: str | None = Field(default=None, description='模型ID') - name: str | None = Field(default=None, description='模型名称') - provider: str | None = Field(default=None, description='模型提供者') - temperature: float | None = Field(default=None, description='模型温度') - - -class AgentDataModel(BaseModel): - """ - 对话会话数据模型-智能体数据模型 - """ - - model_config = ConfigDict(alias_generator=to_camel) - - model: ModelInfoModel | None = Field(default=None, description='模型信息') - agent_id: str | None = Field(default=None, description='智能体ID') - - -class SessionMetricsModel(BaseModel): - """ - 对话会话数据模型-会话指标模型 - """ - - model_config = ConfigDict(alias_generator=to_camel) - - cost: float | None = Field(default=None, description='成本(元)') - timer: float | None = Field(default=None, description='运行时长(秒)') - duration: float | None = Field(default=None, description='运行时长(秒)') - input_tokens: int | None = Field(default=None, description='输入Token数量') - total_tokens: int | None = Field(default=None, description='总Token数量') - output_tokens: int | None = Field(default=None, description='输出Token数量') - provider_metrics: dict | None = Field(default=None, description='提供者指标') - reasoning_tokens: int | None = Field(default=None, description='推理Token数量') - cache_read_tokens: int | None = Field(default=None, description='缓存读取Token数量') - additional_metrics: dict | None = Field(default=None, description='其他指标') - audio_input_tokens: int | None = Field(default=None, description='音频输入Token数量') - audio_total_tokens: int | None = Field(default=None, description='音频总Token数量') - cache_write_tokens: int | None = Field(default=None, description='缓存写入Token数量') - audio_output_tokens: int | None = Field(default=None, description='音频输出Token数量') - time_to_first_token: float | None = Field(default=None, description='到第一个Token的时间(秒)') - - -class SessionDataModel(BaseModel): - """ - 对话会话数据模型 - """ - - model_config = ConfigDict(alias_generator=to_camel) - - session_state: dict | None = Field(default=None, description='会话状态') - session_metrics: SessionMetricsModel | None = Field(default=None, description='会话指标') - - -class MessageMetrics(BaseModel): - """ - 对话消息模型-消息指标模型 - """ - - model_config = ConfigDict(alias_generator=to_camel) - - input_tokens: int | None = Field(default=None, description='输入Token数量') - total_tokens: int | None = Field(default=None, description='总Token数量') - output_tokens: int | None = Field(default=None, description='输出Token数量') - reasoning_tokens: int | None = Field(default=None, description='推理Token数量') - duration: float | None = Field(default=None, description='运行时长(秒)') - time_to_first_token: float | None = Field(default=None, description='到第一个Token的时间(秒)') - - -class ChatMessageModel(BaseModel): - """ - 对话消息模型 - """ - - model_config = ConfigDict(alias_generator=to_camel) - - id: str | None = Field(default=None, description='消息ID') - role: str | None = Field(default=None, description='角色') - content: str | None = Field(default=None, description='内容') - images: list[str] | None = Field(default=None, description='图片列表') - metrics: MessageMetrics | None = Field(default=None, description='Token使用统计') - created_at: datetime | None = Field(default=None, description='创建时间') - from_history: bool | None = Field(default=None, description='是否来自历史记录') - reasoning_content: str | None = Field(default=None, description='推理/思考内容') - stop_after_tool_call: bool | None = Field(default=None, description='是否在工具调用后停止') - - -class AiChatSessionModel(AiChatSessionBaseModel): - """ - AI对话会话模型 - """ - - model_config = ConfigDict(alias_generator=to_camel) - - agent_id: str | None = Field(default=None, description='智能体ID') - session_data: SessionDataModel | None = Field(default=None, description='会话数据') - agent_data: AgentDataModel | None = Field(default=None, description='智能体数据') - messages: list[ChatMessageModel] | None = Field(default=None, description='消息列表') diff --git a/shierfeng-fastapi-backend/module_ai/entity/vo/ai_model_vo.py b/shierfeng-fastapi-backend/module_ai/entity/vo/ai_model_vo.py deleted file mode 100644 index ffc3a19..0000000 --- a/shierfeng-fastapi-backend/module_ai/entity/vo/ai_model_vo.py +++ /dev/null @@ -1,64 +0,0 @@ -from datetime import datetime -from typing import Literal - -from pydantic import BaseModel, ConfigDict, Field -from pydantic.alias_generators import to_camel -from pydantic_validation_decorator import NotBlank, Size - - -class AiModelModel(BaseModel): - """ - AI模型表对应pydantic模型 - """ - - model_config = ConfigDict(alias_generator=to_camel, from_attributes=True) - - model_id: int | None = Field(default=None, description='模型主键') - model_code: str | None = Field(default=None, description='模型编码') - model_name: str | None = Field(default=None, description='模型名称') - provider: str | None = Field(default=None, description='提供商') - model_sort: int | None = Field(default=None, description='显示顺序') - api_key: str | None = Field(default=None, description='API Key') - base_url: str | None = Field(default=None, description='Base URL') - max_tokens: int | None = Field(default=None, description='最大输出token') - temperature: float | None = Field(default=None, description='默认温度') - support_reasoning: Literal['Y', 'N'] | None = Field(default=None, description='是否支持推理(深度思考)') - support_images: Literal['Y', 'N'] | None = Field(default=None, description='是否支持图片') - model_type: str | None = Field(default=None, description='模型类型') - status: Literal['0', '1'] | None = Field(default=None, description='模型状态') - user_id: int | None = Field(default=None, description='用户ID') - dept_id: int | None = Field(default=None, description='部门ID') - create_by: str | None = Field(default=None, description='创建者') - create_time: datetime | None = Field(default=None, description='创建时间') - update_by: str | None = Field(default=None, description='更新者') - update_time: datetime | None = Field(default=None, description='更新时间') - remark: str | None = Field(default=None, description='备注') - - @NotBlank(field_name='model_code', message='模型编码不能为空') - @Size(field_name='model_code', min_length=0, max_length=100, message='模型编码长度不能超过100个字符') - def get_model_code(self) -> str | None: - return self.model_code - - @NotBlank(field_name='base_url', message='Base URL不能为空') - @Size(field_name='base_url', min_length=0, max_length=255, message='Base URL长度不能超过255个字符') - def get_base_url(self) -> str | None: - return self.base_url - - -class AiModelPageQueryModel(AiModelModel): - """ - AI模型管理分页查询模型 - """ - - page_num: int = Field(default=1, description='当前页码') - page_size: int = Field(default=10, description='每页记录数') - - -class DeleteAiModelModel(BaseModel): - """ - 删除AI模型模型 - """ - - model_config = ConfigDict(alias_generator=to_camel) - - model_ids: str = Field(description='需要删除的模型主键') diff --git a/shierfeng-fastapi-backend/module_ai/service/ai_chat_service.py b/shierfeng-fastapi-backend/module_ai/service/ai_chat_service.py deleted file mode 100644 index dfc5406..0000000 --- a/shierfeng-fastapi-backend/module_ai/service/ai_chat_service.py +++ /dev/null @@ -1,483 +0,0 @@ -import json -import os -import uuid -from collections.abc import AsyncGenerator, AsyncIterator -from datetime import datetime -from typing import TYPE_CHECKING, Any - -from agno.agent import Agent -from agno.db.base import SessionType -from agno.media import Image -from agno.run.agent import RunEvent, RunOutput, RunOutputEvent -from agno.run.cancel import acancel_run -from sqlalchemy.ext.asyncio import AsyncSession - -from common.vo import CrudResponseModel -from config.env import UploadConfig -from exceptions.exception import ServiceException -from module_ai.dao.ai_chat_dao import AiChatConfigDao -from module_ai.dao.ai_model_dao import AiModelDao -from module_ai.entity.do.ai_chat_do import AiChatConfig -from module_ai.entity.vo.ai_chat_vo import ( - AgentDataModel, - AiChatConfigModel, - AiChatRequestModel, - AiChatSessionBaseModel, - AiChatSessionModel, - ChatMessageModel, - MessageMetrics, - SessionDataModel, - SessionMetricsModel, -) -from module_ai.entity.vo.ai_model_vo import AiModelModel -from utils.ai_util import AiUtil -from utils.common_util import CamelCaseUtil -from utils.crypto_util import CryptoUtil - -if TYPE_CHECKING: - from agno.models.message import Message - from agno.run.team import TeamRunOutput - from agno.run.workflow import WorkflowRunOutput - from agno.session import Session - - -class AiChatService: - """ - AI对话服务层 - """ - - @classmethod - def _resolve_temperature(cls, user_config: AiChatConfigModel, model_config: AiModelModel) -> float: - """ - 解析温度配置,优先级为 用户配置 > 模型配置 - - :param user_config: 用户配置对象 - :param model_config: 模型配置对象 - :return: 解析后的温度值 - """ - temperature = user_config.temperature or model_config.temperature - return temperature - - @classmethod - def _resolve_is_reasoning(cls, chat_req: AiChatRequestModel, model_config: AiModelModel) -> bool: - """ - 解析深度思考开关,结合请求参数与模型配置确定最终是否开启 - - :param chat_req: 对话请求对象 - :param model_config: 模型配置对象 - :return: 是否开启深度思考 - """ - if model_config.support_reasoning != 'Y': - return False - return bool(chat_req.is_reasoning) - - @classmethod - def _resolve_history_config(cls, user_config: AiChatConfigModel) -> tuple[bool, int]: - """ - 解析历史消息配置,确定是否附带历史以及轮数 - - :param user_config: 用户配置对象 - :return: (是否附带历史, 历史轮数) - """ - add_history = user_config.add_history_to_context == '0' - num_history = user_config.num_history_runs or 3 - - return bool(add_history), int(num_history) - - @classmethod - def _build_agent( - cls, - model_config: AiModelModel, - temperature: float, - system_prompt: str | None, - user_id: int, - session_id: str, - add_history: bool, - num_history: int, - ) -> Agent: - """ - 构建对话Agent对象 - - :param model_config: 模型配置对象 - :param temperature: 对话温度 - :param system_prompt: 系统提示词 - :param user_id: 用户ID - :param session_id: 会话ID - :param add_history: 是否附带历史消息 - :param num_history: 历史消息轮数 - :return: Agent对象 - """ - real_api_key = CryptoUtil.decrypt(model_config.api_key) - - model = AiUtil.get_model_from_factory( - provider=model_config.provider, - model_code=model_config.model_code, - model_name=model_config.model_name, - api_key=real_api_key, - base_url=model_config.base_url, - temperature=temperature, - max_tokens=model_config.max_tokens, - ) - storage = AiUtil.get_storage_engine() - return Agent( - model=model, - id='chat-agent', - description=system_prompt or 'You are a helpful AI assistant.', - db=storage, - user_id=str(user_id), - session_id=session_id, - add_history_to_context=add_history, - num_history_runs=num_history, - markdown=True, - ) - - @classmethod - def _build_run_kwargs( - cls, - chat_req: AiChatRequestModel, - user_config: AiChatConfigModel, - ) -> dict[str, Any]: - """ - 构造Agent运行参数 - - :param chat_req: 对话请求对象 - :param user_config: 用户配置对象 - :return: 运行参数字典 - """ - run_kwargs: dict[str, Any] = {'stream': True, 'stream_events': True} - if not chat_req.images or not user_config.vision_enabled: - return run_kwargs - - processed_images: list[Image] = [] - for img in chat_req.images: - if img and img.startswith(UploadConfig.UPLOAD_PREFIX): - relative_path = img[len(UploadConfig.UPLOAD_PREFIX) :] - if relative_path.startswith('/'): - relative_path = relative_path[1:] - file_path = os.path.join(UploadConfig.UPLOAD_PATH, relative_path) - abs_path = os.path.abspath(file_path) - if os.path.exists(abs_path): - processed_images.append(Image(filepath=abs_path)) - run_kwargs['images'] = processed_images - return run_kwargs - - @classmethod - def _convert_images_to_upload_paths(cls, images: list[Image] | None) -> list[str] | None: - """ - 将Agno Image对象列表转换为前端可访问的上传路径列表 - - :param images: Image对象列表 - :return: 上传路径列表 - """ - if not images: - return None - - result = [] - for img in images: - # 如果是本地文件路径 - if hasattr(img, 'filepath') and img.filepath: - try: - # 使用 abspath 确保路径标准化 - abs_filepath = os.path.abspath(img.filepath) - abs_upload_path = os.path.abspath(UploadConfig.UPLOAD_PATH) - - if abs_filepath.startswith(abs_upload_path): - relative_path = os.path.relpath(abs_filepath, abs_upload_path) - # 转换路径分隔符为URL格式 - url_path = relative_path.replace(os.sep, '/') - # 拼接前缀 - full_url = f'{UploadConfig.UPLOAD_PREFIX}/{url_path}'.replace('//', '/') - result.append(full_url) - else: - result.append(img.filepath) - except Exception: - result.append(img.filepath) - # 如果是URL - elif hasattr(img, 'url') and img.url: - result.append(img.url) - - return result if result else None - - @classmethod - async def _stream_agent( - cls, - agent: Agent, - chat_req: AiChatRequestModel, - run_kwargs: dict[str, Any], - is_reasoning: bool, - session_id: str, - ) -> AsyncGenerator[str, None]: - """ - 将Agent输出流式转换为前端SSE消息 - - :param agent: Agent实例 - :param chat_req: 对话请求对象 - :param run_kwargs: 运行参数字典 - :param is_reasoning: 是否输出推理内容 - :param session_id: 会话ID - :return: SSE消息生成器 - """ - full_response = '' - full_reasoning = '' - try: - yield json.dumps({'session_id': session_id, 'type': 'meta'}) + '\n' - - response_stream: AsyncIterator[RunOutputEvent] = agent.arun(chat_req.message, **run_kwargs) - - async for chunk in response_stream: - content = None - reasoning = None - - if chunk.event == RunEvent.run_started and chunk.run_id: - yield json.dumps({'run_id': chunk.run_id, 'type': 'run_info'}) + '\n' - - if chunk.event == RunEvent.run_content: - content = chunk.content - if hasattr(chunk, 'reasoning_content') and chunk.reasoning_content: - reasoning = chunk.reasoning_content - - if reasoning and is_reasoning: - full_reasoning += reasoning - yield json.dumps({'content': reasoning, 'type': 'reasoning'}) + '\n' - - if chunk.event == RunEvent.run_completed and chunk.metrics: - yield ( - json.dumps( - {'metrics': CamelCaseUtil.transform_result(chunk.metrics.to_dict()), 'type': 'metrics'} - ) - + '\n' - ) - - if content: - full_response += content - yield json.dumps({'content': content, 'type': 'content'}) + '\n' - except Exception as e: - yield json.dumps({'error': str(e), 'type': 'error'}) + '\n' - - @classmethod - async def chat_services( - cls, query_db: AsyncSession, chat_req: AiChatRequestModel, user_id: int - ) -> AsyncGenerator[str, None]: - """ - 流式对话 - - :param query_db: orm对象 - :param chat_req: 对话请求对象 - :param user_id: 用户ID - :return: 对话响应流 - """ - ai_model = await AiModelDao.get_ai_model_detail_by_id(query_db, chat_req.model_id) - model_config = AiModelModel(**CamelCaseUtil.transform_result(ai_model)) if ai_model else AiModelModel() - if not model_config: - raise ServiceException(message='模型不存在') - - user_config = await cls.ai_chat_config_detail_services(query_db, user_id) - - session_id = chat_req.session_id - if not session_id: - session_id = str(uuid.uuid4()) - - temperature = cls._resolve_temperature(user_config, model_config) - is_reasoning = cls._resolve_is_reasoning(chat_req, model_config) - add_history, num_history = cls._resolve_history_config(user_config) - system_prompt = user_config.system_prompt - - agent = cls._build_agent( - model_config=model_config, - temperature=temperature, - system_prompt=system_prompt, - user_id=user_id, - session_id=session_id, - add_history=add_history, - num_history=num_history, - ) - run_kwargs = cls._build_run_kwargs(chat_req, user_config) - async for chunk in cls._stream_agent( - agent=agent, - chat_req=chat_req, - run_kwargs=run_kwargs, - is_reasoning=is_reasoning, - session_id=session_id, - ): - yield chunk - - @classmethod - async def ai_chat_config_detail_services(cls, query_db: AsyncSession, user_id: int) -> AiChatConfigModel: - """ - 获取用户配置 - - :param query_db: orm对象 - :param user_id: 用户ID - :return: 配置模型 - """ - chat_config = await AiChatConfigDao.get_chat_config_detail_by_user_id(query_db, user_id) - result = AiChatConfigModel(**CamelCaseUtil.transform_result(chat_config)) if chat_config else AiChatConfig() - - return result - - @classmethod - async def save_ai_chat_config_services( - cls, query_db: AsyncSession, user_id: int, page_object: AiChatConfigModel - ) -> CrudResponseModel: - """ - 保存用户配置 - - :param query_db: orm对象 - :param user_id: 用户ID - :param page_object: AI对话配置对象 - :return: 更新后的配置模型 - """ - chat_config = await AiChatConfigDao.get_chat_config_detail_by_user_id(query_db, user_id) - if page_object.user_id is None: - page_object.user_id = user_id - - try: - if chat_config: - if chat_config.chat_config_id != page_object.chat_config_id: - raise ServiceException(message='只允许修改当前用户的配置') - page_object.update_time = datetime.now() - edit_ai_chat_config = page_object.model_dump(exclude_unset=True) - await AiChatConfigDao.edit_chat_config_dao(query_db, edit_ai_chat_config) - else: - page_object.create_time = datetime.now() - await AiChatConfigDao.add_chat_config_dao(query_db, page_object) - - await query_db.commit() - except Exception as e: - await query_db.rollback() - raise e - - return CrudResponseModel(is_success=True, message='保存成功') - - @classmethod - async def get_chat_session_list_services(cls, user_id: int) -> list[AiChatSessionBaseModel]: - """ - 获取用户会话列表 - - :param user_id: 用户ID - :return: 用户会话列表 - """ - # 获取Agno会话列表 - storage = AiUtil.get_storage_engine() - sessions: list[Session] = await storage.get_sessions( - user_id=str(user_id), - component_id='chat-agent', - session_type=SessionType.AGENT, - ) - - result = [] - for s in sessions: - created_at = datetime.fromtimestamp(s.created_at) if s.created_at else None - updated_at = datetime.fromtimestamp(s.updated_at) if s.updated_at else None - - title_limit = 20 - session_title = s.runs[0].input.input_content[:title_limit] + '...' if s.runs else '' - - result.append( - AiChatSessionBaseModel( - sessionId=s.session_id, - sessionTitle=session_title if len(session_title) <= title_limit else session_title[:title_limit], - userId=s.user_id, - createdAt=created_at, - updatedAt=updated_at, - ) - ) - return result - - @classmethod - async def delete_chat_session_services(cls, session_id: str) -> CrudResponseModel: - """ - 删除会话 - - :param session_id: 会话ID - :return: 删除结果 - """ - storage = AiUtil.get_storage_engine() - delete_result = await storage.delete_session(session_id=session_id) - if not delete_result: - raise ServiceException(message='删除会话失败') - return CrudResponseModel(is_success=True, message='删除成功') - - @classmethod - async def get_chat_session_detail_services(cls, session_id: str) -> AiChatSessionModel: - """ - 获取会话消息详情 - - :param session_id: 会话ID - :return: 会话消息详情 - """ - storage = AiUtil.get_storage_engine() - session: Session | None = await storage.get_session(session_id=session_id, session_type=SessionType.AGENT) - - if not session: - raise ServiceException(message='会话不存在') - - session_data: dict[str, Any] = session.session_data - agent_data: dict[str, Any] = session.agent_data - runs: list[RunOutput | TeamRunOutput | WorkflowRunOutput] = session.runs - messages: list[Message] = session.get_messages(skip_roles=['system']) - - run_metrics_map = {} - if runs: - for run in runs: - if run.model_provider_data and (provider_id := run.model_provider_data.get('id')): - run_metrics_map[provider_id] = run.metrics - - chat_messages = [] - for m in messages: - if hasattr(m, 'provider_data') and m.provider_data: - provider_id = m.provider_data.get('id') - if provider_id and provider_id in run_metrics_map: - m.metrics = run_metrics_map[provider_id] - - metrics_model = None - if getattr(m, 'metrics', None) and hasattr(m.metrics, 'to_dict'): - metrics_dict = m.metrics.to_dict() - if metrics_dict: - metrics_model = MessageMetrics(**CamelCaseUtil.transform_result(metrics_dict)) - - chat_messages.append( - ChatMessageModel( - id=m.id, - role=m.role, - content=m.content, - images=cls._convert_images_to_upload_paths(m.images), - metrics=metrics_model, - createdAt=datetime.fromtimestamp(m.created_at) if m.created_at else None, - reasoningContent=m.reasoning_content, - fromHistory=m.from_history, - stopAfterToolCall=m.stop_after_tool_call, - ) - ) - - session_detail = AiChatSessionModel( - sessionId=session.session_id, - sessionTitle=session.runs[0].input.input_content[:20] + '...' if session.runs else '', - userId=session.user_id, - createdAt=datetime.fromtimestamp(session.created_at) if session.created_at else None, - updatedAt=datetime.fromtimestamp(session.updated_at) if session.updated_at else None, - agentId=session.agent_id, - sessionData=SessionDataModel( - sessionState=session_data.get('session_state'), - sessionMetrics=SessionMetricsModel( - **CamelCaseUtil.transform_result(session_data.get('session_metrics')) - ), - ), - agentData=AgentDataModel(**CamelCaseUtil.transform_result(agent_data)), - messages=chat_messages, - ) - - return session_detail - - @classmethod - async def cancel_run_services(cls, run_id: str) -> CrudResponseModel: - """ - 取消运行 - - :param run_id: 运行ID - :return: 取消结果 - """ - cancel_result = await acancel_run(run_id) - if not cancel_result: - raise ServiceException(message='取消运行失败') - return CrudResponseModel(is_success=True, message='取消成功') diff --git a/shierfeng-fastapi-backend/module_ai/service/ai_model_service.py b/shierfeng-fastapi-backend/module_ai/service/ai_model_service.py deleted file mode 100644 index 15fc608..0000000 --- a/shierfeng-fastapi-backend/module_ai/service/ai_model_service.py +++ /dev/null @@ -1,155 +0,0 @@ -from typing import Any - -from sqlalchemy import ColumnElement -from sqlalchemy.ext.asyncio import AsyncSession - -from common.vo import CrudResponseModel, PageModel -from exceptions.exception import ServiceException -from module_ai.dao.ai_model_dao import AiModelDao -from module_ai.entity.vo.ai_model_vo import AiModelModel, AiModelPageQueryModel, DeleteAiModelModel -from utils.common_util import CamelCaseUtil -from utils.crypto_util import CryptoUtil - - -class AiModelService: - """ - AI模型管理服务层 - """ - - @classmethod - async def get_ai_model_list_services( - cls, - query_db: AsyncSession, - query_object: AiModelPageQueryModel, - data_scope_sql: ColumnElement, - is_page: bool = False, - ) -> PageModel | list[dict[str, Any]]: - """ - 获取AI模型列表信息service - - :param query_db: orm对象 - :param query_object: 查询参数对象 - :param data_scope_sql: 数据权限对应的查询sql语句 - :param is_page: 是否开启分页 - :return: AI模型列表信息对象 - """ - ai_model_list_result = await AiModelDao.get_ai_model_list(query_db, query_object, data_scope_sql, is_page) - rows = ai_model_list_result.rows if isinstance(ai_model_list_result, PageModel) else ai_model_list_result - - for row in rows: - if 'apiKey' in row: - row['apiKey'] = '********' * 3 - - return ai_model_list_result - - @classmethod - async def check_ai_model_data_scope_services( - cls, - query_db: AsyncSession, - model_id: int, - data_scope_sql: ColumnElement, - ) -> CrudResponseModel: - """ - 校验用户是否有AI模型数据权限service - - :param query_db: orm对象 - :param model_id: 模型主键 - :param data_scope_sql: 数据权限对应的查询sql语句 - :return: 校验结果 - """ - ai_models = await AiModelDao.get_ai_model_list( - query_db, AiModelModel(modelId=model_id), data_scope_sql, is_page=False - ) - if ai_models: - return CrudResponseModel(is_success=True, message='校验通过') - raise ServiceException(message='没有权限访问AI模型数据') - - @classmethod - async def add_ai_model_services(cls, query_db: AsyncSession, page_object: AiModelModel) -> CrudResponseModel: - """ - 新增AI模型信息service - - :param request: Request对象 - :param query_db: orm对象 - :param page_object: 新增AI模型对象 - :return: 新增AI模型校验结果 - """ - try: - if page_object.api_key: - page_object.api_key = CryptoUtil.encrypt(page_object.api_key) - await AiModelDao.add_ai_model_dao(query_db, page_object) - await query_db.commit() - return CrudResponseModel(is_success=True, message='新增成功') - except Exception as e: - await query_db.rollback() - raise e - - @classmethod - async def edit_ai_model_services(cls, query_db: AsyncSession, page_object: AiModelModel) -> CrudResponseModel: - """ - 编辑AI模型信息service - - :param query_db: orm对象 - :param page_object: 编辑AI模型对象 - :return: 编辑AI模型校验结果 - """ - edit_ai_model = page_object.model_dump(exclude_unset=True) - if page_object.api_key: - if page_object.api_key == '********' * 3: - if 'api_key' in edit_ai_model: - del edit_ai_model['api_key'] - else: - edit_ai_model['api_key'] = CryptoUtil.encrypt(page_object.api_key) - - ai_model_info = await cls.ai_model_detail_services(query_db, page_object.model_id) - if ai_model_info.model_id: - try: - await AiModelDao.edit_ai_model_dao(query_db, edit_ai_model) - await query_db.commit() - return CrudResponseModel(is_success=True, message='修改成功') - except Exception as e: - await query_db.rollback() - raise e - else: - raise ServiceException(message='AI模型不存在') - - @classmethod - async def delete_ai_model_services( - cls, query_db: AsyncSession, page_object: DeleteAiModelModel - ) -> CrudResponseModel: - """ - 删除AI模型信息service - - :param query_db: orm对象 - :param page_object: 删除AI模型对象 - :return: 删除AI模型校验结果 - """ - if page_object.model_ids: - model_id_list = page_object.model_ids.split(',') - try: - for model_id in model_id_list: - await AiModelDao.delete_ai_model_dao(query_db, AiModelModel(modelId=model_id)) - await query_db.commit() - return CrudResponseModel(is_success=True, message='删除成功') - except Exception as e: - await query_db.rollback() - raise e - else: - raise ServiceException(message='传入AI模型id为空') - - @classmethod - async def ai_model_detail_services(cls, query_db: AsyncSession, model_id: int) -> AiModelModel: - """ - 获取AI模型详细信息service - - :param query_db: orm对象 - :param model_id: AI模型id - :return: AI模型id对应的信息 - """ - ai_model = await AiModelDao.get_ai_model_detail_by_id(query_db, model_id=model_id) - result = AiModelModel(**CamelCaseUtil.transform_result(ai_model)) if ai_model else AiModelModel() - - if result.api_key: - result.api_key = '********' * 3 - - return result diff --git a/shierfeng-fastapi-backend/requirements-pg.txt b/shierfeng-fastapi-backend/requirements-pg.txt index 43d7532..a5517b3 100644 --- a/shierfeng-fastapi-backend/requirements-pg.txt +++ b/shierfeng-fastapi-backend/requirements-pg.txt @@ -1,31 +1,20 @@ -agno==2.4.8 aiofiles==25.1.0 alembic==1.18.3 -anthropic==0.78.0 APScheduler==3.11.2 async-lru==2.1.0 asyncpg==0.31.0 bcrypt==5.0.0 -cerebras-cloud-sdk==1.67.0 -cohere==5.20.4 fastapi[all]==0.128.2 -google-genai==1.62.0 -groq==1.0.0 -litellm==1.81.8 -llama-api-client==0.6.0 loguru==0.7.3 -mistralai==1.12.0 -ollama==0.6.1 -openai==2.17.0 openpyxl==3.1.5 pandas==2.3.3 Pillow==12.1.1 -portkey-ai==2.1.0 psutil==7.2.2 psycopg2==2.9.11 pydantic>=2.11.4 pydantic-validation-decorator==0.1.5 PyJWT[crypto]==2.11.0 +qrcode==8.2 redis==7.1.0 ruff==0.15.0 SQLAlchemy[asyncio]==2.0.46 diff --git a/shierfeng-fastapi-backend/requirements.txt b/shierfeng-fastapi-backend/requirements.txt index 5c17e51..93940c3 100644 --- a/shierfeng-fastapi-backend/requirements.txt +++ b/shierfeng-fastapi-backend/requirements.txt @@ -1,31 +1,20 @@ -agno==2.4.8 aiofiles==25.1.0 alembic==1.18.3 -anthropic==0.78.0 APScheduler==3.11.2 async-lru==2.1.0 asyncmy==0.2.11 bcrypt==5.0.0 -cerebras-cloud-sdk==1.67.0 -cohere==5.20.4 fastapi[all]==0.128.2 -google-genai==1.62.0 -groq==1.0.0 -litellm==1.81.8 -llama-api-client==0.6.0 loguru==0.7.3 -mistralai==1.12.0 -ollama==0.6.1 -openai==2.17.0 openpyxl==3.1.5 pandas==2.3.3 Pillow==12.1.1 -portkey-ai==2.1.0 psutil==7.2.2 pydantic>=2.11.4 pydantic-validation-decorator==0.1.5 PyJWT[crypto]==2.11.0 PyMySQL==1.1.2 +qrcode==8.2 redis==7.1.0 ruff==0.15.0 SQLAlchemy[asyncio]==2.0.46 diff --git a/shierfeng-fastapi-backend/sql/shierfeng-fastapi-pg.sql b/shierfeng-fastapi-backend/sql/shierfeng-fastapi-pg.sql index 7c59a84..6b3efe4 100644 --- a/shierfeng-fastapi-backend/sql/shierfeng-fastapi-pg.sql +++ b/shierfeng-fastapi-backend/sql/shierfeng-fastapi-pg.sql @@ -245,8 +245,6 @@ comment on table sys_menu is '菜单权限表'; insert into sys_menu values(1, '系统管理', 0, '1', 'system', null, '', '', 1, 0, 'M', '0', '0', '', 'system', 'admin', current_timestamp, '', null, '系统管理目录'); insert into sys_menu values(2, '系统监控', 0, '2', 'monitor', null, '', '', 1, 0, 'M', '0', '0', '', 'monitor', 'admin', current_timestamp, '', null, '系统监控目录'); insert into sys_menu values(3, '系统工具', 0, '3', 'tool', null, '', '', 1, 0, 'M', '0', '0', '', 'tool', 'admin', current_timestamp, '', null, '系统工具目录'); -insert into sys_menu values(4, 'AI 管理', 0, '4', 'ai', null, '', '', 1, 0, 'M', '0', '0', '', 'bug', 'admin', current_timestamp, '', null, 'AI 管理目录'); -insert into sys_menu values(99, '施尔丰仓库', 0, '99', 'https://git.yujiangrubber.cn/niuniu/ShiErFeng', null, '', '', 0, 0, 'M', '0', '0', '', 'guide', 'admin', current_timestamp, '', null, '施尔丰仓库地址'); -- 二级菜单 insert into sys_menu values(100, '用户管理', 1, '1', 'user', 'system/user/index', '', '', 1, 0, 'C', '0', '0', 'system:user:list', 'user', 'admin', current_timestamp, '', null, '用户管理菜单'); insert into sys_menu values(101, '角色管理', 1, '2', 'role', 'system/role/index', '', '', 1, 0, 'C', '0', '0', 'system:role:list', 'peoples', 'admin', current_timestamp, '', null, '角色管理菜单'); @@ -267,8 +265,6 @@ insert into sys_menu values(120, '传输加密', 2, '7', 'transportCrypto', insert into sys_menu values(115, '表单构建', 3, '1', 'build', 'tool/build/index', '', '', 1, 0, 'C', '0', '0', 'tool:build:list', 'build', 'admin', current_timestamp, '', null, '表单构建菜单'); insert into sys_menu values(116, '代码生成', 3, '2', 'gen', 'tool/gen/index', '', '', 1, 0, 'C', '0', '0', 'tool:gen:list', 'code', 'admin', current_timestamp, '', null, '代码生成菜单'); insert into sys_menu values(117, '系统接口', 3, '3', 'swagger', 'tool/swagger/index', '', '', 1, 0, 'C', '0', '0', 'tool:swagger:list', 'swagger', 'admin', current_timestamp, '', null, '系统接口菜单'); -insert into sys_menu values(118, '模型管理', 4, '1', 'model', 'ai/model/index', '', '', 1, 0, 'C', '0', '0', 'ai:model:list', 'form', 'admin', current_timestamp, '', null, '模型管理菜单'); -insert into sys_menu values(119, 'AI 对话', 4, '2', 'chat', 'ai/chat/index', '', '', 1, 0, 'C', '0', '0', 'ai:chat:list', 'wechat', 'admin', current_timestamp, '', null, 'AI 对话菜单'); -- 三级菜单 insert into sys_menu values(500, '操作日志', 108, '1', 'operlog', 'monitor/operlog/index', '', '', 1, 0, 'C', '0', '0', 'monitor:operlog:list', 'form', 'admin', current_timestamp, '', null, '操作日志菜单'); insert into sys_menu values(501, '登录日志', 108, '2', 'logininfor', 'monitor/logininfor/index', '', '', 1, 0, 'C', '0', '0', 'monitor:logininfor:list', 'logininfor', 'admin', current_timestamp, '', null, '登录日志菜单'); @@ -346,11 +342,6 @@ insert into sys_menu values(1057, '生成删除', 116, '3', '#', '', '', '', 1, insert into sys_menu values(1058, '导入代码', 116, '4', '#', '', '', '', 1, 0, 'F', '0', '0', 'tool:gen:import', '#', 'admin', current_timestamp, '', null, ''); insert into sys_menu values(1059, '预览代码', 116, '5', '#', '', '', '', 1, 0, 'F', '0', '0', 'tool:gen:preview', '#', 'admin', current_timestamp, '', null, ''); insert into sys_menu values(1060, '生成代码', 116, '6', '#', '', '', '', 1, 0, 'F', '0', '0', 'tool:gen:code', '#', 'admin', current_timestamp, '', null, ''); --- 模型管理按钮 -insert into sys_menu values(1061, '模型查询', 118, '1', '#', '', '', '', 1, 0, 'F', '0', '0', 'ai:model:query', '#', 'admin', current_timestamp, '', null, ''); -insert into sys_menu values(1062, '模型新增', 118, '2', '#', '', '', '', 1, 0, 'F', '0', '0', 'ai:model:add', '#', 'admin', current_timestamp, '', null, ''); -insert into sys_menu values(1063, '模型修改', 118, '3', '#', '', '', '', 1, 0, 'F', '0', '0', 'ai:model:edit', '#', 'admin', current_timestamp, '', null, ''); -insert into sys_menu values(1064, '模型删除', 118, '4', '#', '', '', '', 1, 0, 'F', '0', '0', 'ai:model:remove', '#', 'admin', current_timestamp, '', null, ''); -- ---------------------------- -- 6、用户和角色关联表 用户N-1角色 @@ -390,7 +381,6 @@ comment on table sys_role_menu is '角色和菜单关联表'; insert into sys_role_menu values (2, 1); insert into sys_role_menu values (2, 2); insert into sys_role_menu values (2, 3); -insert into sys_role_menu values (2, 4); insert into sys_role_menu values (2, 100); insert into sys_role_menu values (2, 101); insert into sys_role_menu values (2, 102); @@ -602,7 +592,6 @@ insert into sys_dict_type values(8, '通知类型', 'sys_notice_type', insert into sys_dict_type values(9, '通知状态', 'sys_notice_status', '0', 'admin', current_timestamp, '', null, '通知状态列表'); insert into sys_dict_type values(10, '操作类型', 'sys_oper_type', '0', 'admin', current_timestamp, '', null, '操作类型列表'); insert into sys_dict_type values(11, '系统状态', 'sys_common_status', '0', 'admin', current_timestamp, '', null, '登录状态列表'); -insert into sys_dict_type values(12, 'AI模型提供商', 'ai_provider_type', '0', 'admin', current_timestamp, '', null, 'AI模型提供商列表'); -- ---------------------------- -- 12、字典数据表 @@ -677,43 +666,6 @@ insert into sys_dict_data values(29, 8, '生成代码', '8', insert into sys_dict_data values(30, 9, '清空数据', '9', 'sys_oper_type', '', 'danger', 'N', '0', 'admin', current_timestamp, '', null, '清空操作'); insert into sys_dict_data values(31, 1, '成功', '0', 'sys_common_status', '', 'primary', 'N', '0', 'admin', current_timestamp, '', null, '正常状态'); insert into sys_dict_data values(32, 2, '失败', '1', 'sys_common_status', '', 'danger', 'N', '0', 'admin', current_timestamp, '', null, '停用状态'); -insert into sys_dict_data values(33, 1, 'AIMLAPI', 'AIMLAPI', 'ai_provider_type', '', 'info', 'N', '0', 'admin', current_timestamp, '', null, 'AIMLAPI'); -insert into sys_dict_data values(34, 2, 'Anthropic', 'Anthropic', 'ai_provider_type', '', 'info', 'N', '0', 'admin', current_timestamp, '', null, 'Anthropic'); -insert into sys_dict_data values(35, 3, 'Cerebras', 'Cerebras', 'ai_provider_type', '', 'info', 'N', '0', 'admin', current_timestamp, '', null, 'Cerebras'); -insert into sys_dict_data values(36, 4, 'CerebrasOpenAI', 'CerebrasOpenAI', 'ai_provider_type', '', 'info', 'N', '0', 'admin', current_timestamp, '', null, 'CerebrasOpenAI'); -insert into sys_dict_data values(37, 5, 'Cohere', 'Cohere', 'ai_provider_type', '', 'info', 'N', '0', 'admin', current_timestamp, '', null, 'Cohere'); -insert into sys_dict_data values(38, 6, 'CometAPI', 'CometAPI', 'ai_provider_type', '', 'info', 'N', '0', 'admin', current_timestamp, '', null, 'CometAPI'); -insert into sys_dict_data values(39, 7, 'DashScope', 'DashScope', 'ai_provider_type', '', 'info', 'N', '0', 'admin', current_timestamp, '', null, 'DashScope'); -insert into sys_dict_data values(40, 8, 'DeepInfra', 'DeepInfra', 'ai_provider_type', '', 'info', 'N', '0', 'admin', current_timestamp, '', null, 'DeepInfra'); -insert into sys_dict_data values(41, 9, 'DeepSeek', 'DeepSeek', 'ai_provider_type', '', 'info', 'N', '0', 'admin', current_timestamp, '', null, 'DeepSeek'); -insert into sys_dict_data values(42, 10, 'Fireworks', 'Fireworks', 'ai_provider_type', '', 'info', 'N', '0', 'admin', current_timestamp, '', null, 'Fireworks'); -insert into sys_dict_data values(43, 11, 'Google', 'Google', 'ai_provider_type', '', 'info', 'N', '0', 'admin', current_timestamp, '', null, 'Google'); -insert into sys_dict_data values(44, 12, 'Groq', 'Groq', 'ai_provider_type', '', 'info', 'N', '0', 'admin', current_timestamp, '', null, 'Groq'); -insert into sys_dict_data values(45, 13, 'HuggingFace', 'HuggingFace', 'ai_provider_type', '', 'info', 'N', '0', 'admin', current_timestamp, '', null, 'HuggingFace'); -insert into sys_dict_data values(46, 14, 'LangDB', 'LangDB', 'ai_provider_type', '', 'info', 'N', '0', 'admin', current_timestamp, '', null, 'LangDB'); -insert into sys_dict_data values(47, 15, 'LiteLLM', 'LiteLLM', 'ai_provider_type', '', 'info', 'N', '0', 'admin', current_timestamp, '', null, 'LiteLLM'); -insert into sys_dict_data values(48, 16, 'LiteLLMOpenAI', 'LiteLLMOpenAI', 'ai_provider_type', '', 'info', 'N', '0', 'admin', current_timestamp, '', null, 'LiteLLMOpenAI'); -insert into sys_dict_data values(49, 17, 'LlamaCpp', 'LlamaCpp', 'ai_provider_type', '', 'info', 'N', '0', 'admin', current_timestamp, '', null, 'LlamaCpp'); -insert into sys_dict_data values(50, 18, 'LMStudio', 'LMStudio', 'ai_provider_type', '', 'info', 'N', '0', 'admin', current_timestamp, '', null, 'LMStudio'); -insert into sys_dict_data values(51, 19, 'Meta', 'Meta', 'ai_provider_type', '', 'info', 'N', '0', 'admin', current_timestamp, '', null, 'Meta'); -insert into sys_dict_data values(52, 20, 'Mistral', 'Mistral', 'ai_provider_type', '', 'info', 'N', '0', 'admin', current_timestamp, '', null, 'Mistral'); -insert into sys_dict_data values(53, 21, 'N1N', 'N1N', 'ai_provider_type', '', 'info', 'N', '0', 'admin', current_timestamp, '', null, 'N1N'); -insert into sys_dict_data values(54, 22, 'Nebius', 'Nebius', 'ai_provider_type', '', 'info', 'N', '0', 'admin', current_timestamp, '', null, 'Nebius'); -insert into sys_dict_data values(55, 23, 'Nexus', 'Nexus', 'ai_provider_type', '', 'info', 'N', '0', 'admin', current_timestamp, '', null, 'Nexus'); -insert into sys_dict_data values(56, 24, 'Nvidia', 'Nvidia', 'ai_provider_type', '', 'info', 'N', '0', 'admin', current_timestamp, '', null, 'Nvidia'); -insert into sys_dict_data values(57, 25, 'Ollama', 'Ollama', 'ai_provider_type', '', 'info', 'N', '0', 'admin', current_timestamp, '', null, 'Ollama'); -insert into sys_dict_data values(58, 26, 'OpenAI', 'OpenAI', 'ai_provider_type', '', 'info', 'N', '0', 'admin', current_timestamp, '', null, 'OpenAI'); -insert into sys_dict_data values(59, 27, 'OpenAIResponses', 'OpenAIResponses', 'ai_provider_type', '', 'info', 'N', '0', 'admin', current_timestamp, '', null, 'OpenAIResponses'); -insert into sys_dict_data values(60, 28, 'OpenRouter', 'OpenRouter', 'ai_provider_type', '', 'info', 'N', '0', 'admin', current_timestamp, '', null, 'OpenRouter'); -insert into sys_dict_data values(61, 29, 'Perplexity', 'Perplexity', 'ai_provider_type', '', 'info', 'N', '0', 'admin', current_timestamp, '', null, 'Perplexity'); -insert into sys_dict_data values(62, 30, 'Portkey', 'Portkey', 'ai_provider_type', '', 'info', 'N', '0', 'admin', current_timestamp, '', null, 'Portkey'); -insert into sys_dict_data values(63, 31, 'Requesty', 'Requesty', 'ai_provider_type', '', 'info', 'N', '0', 'admin', current_timestamp, '', null, 'Requesty'); -insert into sys_dict_data values(64, 32, 'Sambanova', 'Sambanova', 'ai_provider_type', '', 'info', 'N', '0', 'admin', current_timestamp, '', null, 'Sambanova'); -insert into sys_dict_data values(65, 33, 'SiliconFlow', 'SiliconFlow', 'ai_provider_type', '', 'info', 'N', '0', 'admin', current_timestamp, '', null, 'SiliconFlow'); -insert into sys_dict_data values(66, 34, 'Together', 'Together', 'ai_provider_type', '', 'info', 'N', '0', 'admin', current_timestamp, '', null, 'Together'); -insert into sys_dict_data values(67, 35, 'Vercel', 'Vercel', 'ai_provider_type', '', 'info', 'N', '0', 'admin', current_timestamp, '', null, 'Vercel'); -insert into sys_dict_data values(68, 36, 'VLLM', 'VLLM', 'ai_provider_type', '', 'info', 'N', '0', 'admin', current_timestamp, '', null, 'VLLM'); -insert into sys_dict_data values(69, 37, 'xAI', 'xAI', 'ai_provider_type', '', 'info', 'N', '0', 'admin', current_timestamp, '', null, 'xAI'); -- ---------------------------- -- 13、参数配置表 @@ -1011,86 +963,6 @@ comment on column gen_table_column.update_by is '更新者'; comment on column gen_table_column.update_time is '更新时间'; comment on table gen_table_column is '代码生成业务表字段'; --- ---------------------------- --- 20、AI模型表 --- ---------------------------- -drop table if exists ai_models; -create table ai_models ( - model_id bigserial not null, - model_code varchar(100) not null, - model_name varchar(100) default null, - provider varchar(50) not null, - model_sort int4 not null, - api_key varchar(255) default null, - base_url varchar(255) default null, - model_type varchar(50) default null, - max_tokens integer default null, - temperature float default null, - support_reasoning char(1) default 'N', - support_images char(1) default 'N', - status char(1) default '0', - user_id bigint, - dept_id bigint, - create_by varchar(64) default '', - create_time timestamp(0), - update_by varchar(64) default '', - update_time timestamp(0), - remark varchar(500) default null, - primary key (model_id) -); -comment on table ai_models is 'AI模型表'; -comment on column ai_models.model_id is '模型主键'; -comment on column ai_models.model_code is '模型编码'; -comment on column ai_models.model_name is '模型名称'; -comment on column ai_models.provider is '提供商'; -comment on column ai_models.model_sort is '显示顺序'; -comment on column ai_models.api_key is 'API Key'; -comment on column ai_models.base_url is 'Base URL'; -comment on column ai_models.model_type is '模型类型'; -comment on column ai_models.max_tokens is '最大输出token'; -comment on column ai_models.temperature is '默认温度'; -comment on column ai_models.support_reasoning is '是否支持推理'; -comment on column ai_models.support_images is '是否支持图片'; -comment on column ai_models.status is '模型状态'; -comment on column ai_models.user_id is '用户ID'; -comment on column ai_models.dept_id is '部门ID'; -comment on column ai_models.create_by is '创建者'; -comment on column ai_models.create_time is '创建时间'; -comment on column ai_models.update_by is '更新者'; -comment on column ai_models.update_time is '更新时间'; -comment on column ai_models.remark is '备注'; - --- ---------------------------- --- 21、AI对话配置表 --- ---------------------------- -drop table if exists ai_chat_config; -create table ai_chat_config ( - chat_config_id bigserial not null, - user_id bigint not null unique, - temperature float default null, - add_history_to_context char(1) default '0', - num_history_runs int4 default null, - system_prompt text default null, - metrics_default_visible char(1) default '0', - vision_enabled char(1) default '1', - image_max_size_mb int4 default null, - create_time timestamp(0), - update_time timestamp(0), - primary key (chat_config_id) -); -comment on table ai_chat_config is 'AI对话配置表'; -comment on column ai_chat_config.chat_config_id is '配置主键'; -comment on column ai_chat_config.user_id is '用户ID'; -comment on column ai_chat_config.temperature is '默认温度'; -comment on column ai_chat_config.add_history_to_context is '是否添加历史记录(0是, 1否)'; -comment on column ai_chat_config.num_history_runs is '历史记录条数'; -comment on column ai_chat_config.system_prompt is '系统提示词'; -comment on column ai_chat_config.metrics_default_visible is '默认显示指标(0是, 1否)'; -comment on column ai_chat_config.vision_enabled is '是否开启视觉(0是, 1否)'; -comment on column ai_chat_config.image_max_size_mb is '图片最大大小(MB)'; -comment on column ai_chat_config.create_time is '创建时间'; -comment on column ai_chat_config.update_time is '更新时间'; - CREATE OR REPLACE FUNCTION "find_in_set"(int8, varchar) RETURNS "pg_catalog"."bool" AS $BODY$ DECLARE diff --git a/shierfeng-fastapi-backend/sql/shierfeng-fastapi.sql b/shierfeng-fastapi-backend/sql/shierfeng-fastapi.sql index 4295803..d3937ef 100644 --- a/shierfeng-fastapi-backend/sql/shierfeng-fastapi.sql +++ b/shierfeng-fastapi-backend/sql/shierfeng-fastapi.sql @@ -162,8 +162,6 @@ create table sys_menu ( insert into sys_menu values('1', '系统管理', '0', '1', 'system', null, '', '', 1, 0, 'M', '0', '0', '', 'system', 'admin', sysdate(), '', null, '系统管理目录'); insert into sys_menu values('2', '系统监控', '0', '2', 'monitor', null, '', '', 1, 0, 'M', '0', '0', '', 'monitor', 'admin', sysdate(), '', null, '系统监控目录'); insert into sys_menu values('3', '系统工具', '0', '3', 'tool', null, '', '', 1, 0, 'M', '0', '0', '', 'tool', 'admin', sysdate(), '', null, '系统工具目录'); -insert into sys_menu values('4', 'AI 管理', '0', '4', 'ai', null, '', '', 1, 0, 'M', '0', '0', '', 'ai-manage', 'admin', sysdate(), '', null, 'AI 管理目录'); -insert into sys_menu values('99', '施尔丰仓库', '0', '99', 'https://git.yujiangrubber.cn/niuniu/ShiErFeng', null, '', '', 0, 0, 'M', '0', '0', '', 'guide', 'admin', sysdate(), '', null, '施尔丰仓库地址'); -- 二级菜单 insert into sys_menu values('100', '用户管理', '1', '1', 'user', 'system/user/index', '', '', 1, 0, 'C', '0', '0', 'system:user:list', 'user', 'admin', sysdate(), '', null, '用户管理菜单'); insert into sys_menu values('101', '角色管理', '1', '2', 'role', 'system/role/index', '', '', 1, 0, 'C', '0', '0', 'system:role:list', 'peoples', 'admin', sysdate(), '', null, '角色管理菜单'); @@ -184,8 +182,6 @@ insert into sys_menu values('120', '传输加密', '2', '7', 'transportCrypto insert into sys_menu values('115', '表单构建', '3', '1', 'build', 'tool/build/index', '', '', 1, 0, 'C', '0', '0', 'tool:build:list', 'build', 'admin', sysdate(), '', null, '表单构建菜单'); insert into sys_menu values('116', '代码生成', '3', '2', 'gen', 'tool/gen/index', '', '', 1, 0, 'C', '0', '0', 'tool:gen:list', 'code', 'admin', sysdate(), '', null, '代码生成菜单'); insert into sys_menu values('117', '系统接口', '3', '3', 'swagger', 'tool/swagger/index', '', '', 1, 0, 'C', '0', '0', 'tool:swagger:list', 'swagger', 'admin', sysdate(), '', null, '系统接口菜单'); -insert into sys_menu values('118', '模型管理', '4', '1', 'model', 'ai/model/index', '', '', 1, 0, 'C', '0', '0', 'ai:model:list', 'ai-model', 'admin', sysdate(), '', null, '模型管理菜单'); -insert into sys_menu values('119', 'AI 对话', '4', '2', 'chat', 'ai/chat/index', '', '', 1, 0, 'C', '0', '0', 'ai:chat:list', 'ai-chat', 'admin', sysdate(), '', null, 'AI 对话菜单'); -- 三级菜单 insert into sys_menu values('500', '操作日志', '108', '1', 'operlog', 'monitor/operlog/index', '', '', 1, 0, 'C', '0', '0', 'monitor:operlog:list', 'form', 'admin', sysdate(), '', null, '操作日志菜单'); insert into sys_menu values('501', '登录日志', '108', '2', 'logininfor', 'monitor/logininfor/index', '', '', 1, 0, 'C', '0', '0', 'monitor:logininfor:list', 'logininfor', 'admin', sysdate(), '', null, '登录日志菜单'); @@ -263,11 +259,6 @@ insert into sys_menu values('1057', '生成删除', '116', '3', '#', '', '', '', insert into sys_menu values('1058', '导入代码', '116', '4', '#', '', '', '', 1, 0, 'F', '0', '0', 'tool:gen:import', '#', 'admin', sysdate(), '', null, ''); insert into sys_menu values('1059', '预览代码', '116', '5', '#', '', '', '', 1, 0, 'F', '0', '0', 'tool:gen:preview', '#', 'admin', sysdate(), '', null, ''); insert into sys_menu values('1060', '生成代码', '116', '6', '#', '', '', '', 1, 0, 'F', '0', '0', 'tool:gen:code', '#', 'admin', sysdate(), '', null, ''); --- 模型管理按钮 -insert into sys_menu values('1061', '模型查询', '118', '1', '#', '', '', '', 1, 0, 'F', '0', '0', 'ai:model:query', '#', 'admin', sysdate(), '', null, ''); -insert into sys_menu values('1062', '模型新增', '118', '2', '#', '', '', '', 1, 0, 'F', '0', '0', 'ai:model:add', '#', 'admin', sysdate(), '', null, ''); -insert into sys_menu values('1063', '模型修改', '118', '3', '#', '', '', '', 1, 0, 'F', '0', '0', 'ai:model:edit', '#', 'admin', sysdate(), '', null, ''); -insert into sys_menu values('1064', '模型删除', '118', '4', '#', '', '', '', 1, 0, 'F', '0', '0', 'ai:model:remove', '#', 'admin', sysdate(), '', null, ''); -- ---------------------------- @@ -303,7 +294,6 @@ create table sys_role_menu ( insert into sys_role_menu values ('2', '1'); insert into sys_role_menu values ('2', '2'); insert into sys_role_menu values ('2', '3'); -insert into sys_role_menu values ('2', '4'); insert into sys_role_menu values ('2', '100'); insert into sys_role_menu values ('2', '101'); insert into sys_role_menu values ('2', '102'); @@ -482,7 +472,6 @@ insert into sys_dict_type values(8, '通知类型', 'sys_notice_type', insert into sys_dict_type values(9, '通知状态', 'sys_notice_status', '0', 'admin', sysdate(), '', null, '通知状态列表'); insert into sys_dict_type values(10, '操作类型', 'sys_oper_type', '0', 'admin', sysdate(), '', null, '操作类型列表'); insert into sys_dict_type values(11, '系统状态', 'sys_common_status', '0', 'admin', sysdate(), '', null, '登录状态列表'); -insert into sys_dict_type values(12, 'AI模型提供商', 'ai_provider_type', '0', 'admin', sysdate(), '', null, 'AI模型提供商列表'); -- ---------------------------- @@ -540,43 +529,6 @@ insert into sys_dict_data values(29, 8, '生成代码', '8', insert into sys_dict_data values(30, 9, '清空数据', '9', 'sys_oper_type', '', 'danger', 'N', '0', 'admin', sysdate(), '', null, '清空操作'); insert into sys_dict_data values(31, 1, '成功', '0', 'sys_common_status', '', 'primary', 'N', '0', 'admin', sysdate(), '', null, '正常状态'); insert into sys_dict_data values(32, 2, '失败', '1', 'sys_common_status', '', 'danger', 'N', '0', 'admin', sysdate(), '', null, '停用状态'); -insert into sys_dict_data values(33, 1, 'AIMLAPI', 'AIMLAPI', 'ai_provider_type', '', 'info', 'N', '0', 'admin', sysdate(), '', null, 'AIMLAPI'); -insert into sys_dict_data values(34, 2, 'Anthropic', 'Anthropic', 'ai_provider_type', '', 'info', 'N', '0', 'admin', sysdate(), '', null, 'Anthropic'); -insert into sys_dict_data values(35, 3, 'Cerebras', 'Cerebras', 'ai_provider_type', '', 'info', 'N', '0', 'admin', sysdate(), '', null, 'Cerebras'); -insert into sys_dict_data values(36, 4, 'CerebrasOpenAI', 'CerebrasOpenAI', 'ai_provider_type', '', 'info', 'N', '0', 'admin', sysdate(), '', null, 'CerebrasOpenAI'); -insert into sys_dict_data values(37, 5, 'Cohere', 'Cohere', 'ai_provider_type', '', 'info', 'N', '0', 'admin', sysdate(), '', null, 'Cohere'); -insert into sys_dict_data values(38, 6, 'CometAPI', 'CometAPI', 'ai_provider_type', '', 'info', 'N', '0', 'admin', sysdate(), '', null, 'CometAPI'); -insert into sys_dict_data values(39, 7, 'DashScope', 'DashScope', 'ai_provider_type', '', 'info', 'N', '0', 'admin', sysdate(), '', null, 'DashScope'); -insert into sys_dict_data values(40, 8, 'DeepInfra', 'DeepInfra', 'ai_provider_type', '', 'info', 'N', '0', 'admin', sysdate(), '', null, 'DeepInfra'); -insert into sys_dict_data values(41, 9, 'DeepSeek', 'DeepSeek', 'ai_provider_type', '', 'info', 'N', '0', 'admin', sysdate(), '', null, 'DeepSeek'); -insert into sys_dict_data values(42, 10, 'Fireworks', 'Fireworks', 'ai_provider_type', '', 'info', 'N', '0', 'admin', sysdate(), '', null, 'Fireworks'); -insert into sys_dict_data values(43, 11, 'Google', 'Google', 'ai_provider_type', '', 'info', 'N', '0', 'admin', sysdate(), '', null, 'Google'); -insert into sys_dict_data values(44, 12, 'Groq', 'Groq', 'ai_provider_type', '', 'info', 'N', '0', 'admin', sysdate(), '', null, 'Groq'); -insert into sys_dict_data values(45, 13, 'HuggingFace', 'HuggingFace', 'ai_provider_type', '', 'info', 'N', '0', 'admin', sysdate(), '', null, 'HuggingFace'); -insert into sys_dict_data values(46, 14, 'LangDB', 'LangDB', 'ai_provider_type', '', 'info', 'N', '0', 'admin', sysdate(), '', null, 'LangDB'); -insert into sys_dict_data values(47, 15, 'LiteLLM', 'LiteLLM', 'ai_provider_type', '', 'info', 'N', '0', 'admin', sysdate(), '', null, 'LiteLLM'); -insert into sys_dict_data values(48, 16, 'LiteLLMOpenAI', 'LiteLLMOpenAI', 'ai_provider_type', '', 'info', 'N', '0', 'admin', sysdate(), '', null, 'LiteLLMOpenAI'); -insert into sys_dict_data values(49, 17, 'LlamaCpp', 'LlamaCpp', 'ai_provider_type', '', 'info', 'N', '0', 'admin', sysdate(), '', null, 'LlamaCpp'); -insert into sys_dict_data values(50, 18, 'LMStudio', 'LMStudio', 'ai_provider_type', '', 'info', 'N', '0', 'admin', sysdate(), '', null, 'LMStudio'); -insert into sys_dict_data values(51, 19, 'Meta', 'Meta', 'ai_provider_type', '', 'info', 'N', '0', 'admin', sysdate(), '', null, 'Meta'); -insert into sys_dict_data values(52, 20, 'Mistral', 'Mistral', 'ai_provider_type', '', 'info', 'N', '0', 'admin', sysdate(), '', null, 'Mistral'); -insert into sys_dict_data values(53, 21, 'N1N', 'N1N', 'ai_provider_type', '', 'info', 'N', '0', 'admin', sysdate(), '', null, 'N1N'); -insert into sys_dict_data values(54, 22, 'Nebius', 'Nebius', 'ai_provider_type', '', 'info', 'N', '0', 'admin', sysdate(), '', null, 'Nebius'); -insert into sys_dict_data values(55, 23, 'Nexus', 'Nexus', 'ai_provider_type', '', 'info', 'N', '0', 'admin', sysdate(), '', null, 'Nexus'); -insert into sys_dict_data values(56, 24, 'Nvidia', 'Nvidia', 'ai_provider_type', '', 'info', 'N', '0', 'admin', sysdate(), '', null, 'Nvidia'); -insert into sys_dict_data values(57, 25, 'Ollama', 'Ollama', 'ai_provider_type', '', 'info', 'N', '0', 'admin', sysdate(), '', null, 'Ollama'); -insert into sys_dict_data values(58, 26, 'OpenAI', 'OpenAI', 'ai_provider_type', '', 'info', 'N', '0', 'admin', sysdate(), '', null, 'OpenAI'); -insert into sys_dict_data values(59, 27, 'OpenAIResponses', 'OpenAIResponses', 'ai_provider_type', '', 'info', 'N', '0', 'admin', sysdate(), '', null, 'OpenAIResponses'); -insert into sys_dict_data values(60, 28, 'OpenRouter', 'OpenRouter', 'ai_provider_type', '', 'info', 'N', '0', 'admin', sysdate(), '', null, 'OpenRouter'); -insert into sys_dict_data values(61, 29, 'Perplexity', 'Perplexity', 'ai_provider_type', '', 'info', 'N', '0', 'admin', sysdate(), '', null, 'Perplexity'); -insert into sys_dict_data values(62, 30, 'Portkey', 'Portkey', 'ai_provider_type', '', 'info', 'N', '0', 'admin', sysdate(), '', null, 'Portkey'); -insert into sys_dict_data values(63, 31, 'Requesty', 'Requesty', 'ai_provider_type', '', 'info', 'N', '0', 'admin', sysdate(), '', null, 'Requesty'); -insert into sys_dict_data values(64, 32, 'Sambanova', 'Sambanova', 'ai_provider_type', '', 'info', 'N', '0', 'admin', sysdate(), '', null, 'Sambanova'); -insert into sys_dict_data values(65, 33, 'SiliconFlow', 'SiliconFlow', 'ai_provider_type', '', 'info', 'N', '0', 'admin', sysdate(), '', null, 'SiliconFlow'); -insert into sys_dict_data values(66, 34, 'Together', 'Together', 'ai_provider_type', '', 'info', 'N', '0', 'admin', sysdate(), '', null, 'Together'); -insert into sys_dict_data values(67, 35, 'Vercel', 'Vercel', 'ai_provider_type', '', 'info', 'N', '0', 'admin', sysdate(), '', null, 'Vercel'); -insert into sys_dict_data values(68, 36, 'VLLM', 'VLLM', 'ai_provider_type', '', 'info', 'N', '0', 'admin', sysdate(), '', null, 'VLLM'); -insert into sys_dict_data values(69, 37, 'xAI', 'xAI', 'ai_provider_type', '', 'info', 'N', '0', 'admin', sysdate(), '', null, 'xAI'); -- ---------------------------- @@ -762,52 +714,3 @@ create table gen_table_column ( update_time datetime comment '更新时间', primary key (column_id) ) engine=innodb auto_increment=1 comment = '代码生成业务表字段'; - - --- ---------------------------- --- 20、AI模型表 --- ---------------------------- -drop table if exists ai_models; -create table ai_models ( - model_id bigint(20) not null auto_increment comment '模型主键', - model_code varchar(100) not null comment '模型编码', - model_name varchar(100) default null comment '模型名称', - provider varchar(50) not null comment '提供商', - model_sort int(4) not null comment '显示顺序', - api_key varchar(255) default null comment 'API Key', - base_url varchar(255) default null comment 'Base URL', - model_type varchar(50) default null comment '模型类型', - max_tokens int(11) default null comment '最大输出token', - temperature float default null comment '默认温度', - support_reasoning char(1) default 'N' comment '是否支持推理', - support_images char(1) default 'N' comment '是否支持图片', - status char(1) default '0' comment '模型状态', - user_id bigint(20) comment '用户ID', - dept_id bigint(20) comment '部门ID', - create_by varchar(64) default '' comment '创建者', - create_time datetime comment '创建时间', - update_by varchar(64) default '' comment '更新者', - update_time datetime comment '更新时间', - remark varchar(500) default null comment '备注', - primary key (model_id) -) engine=innodb auto_increment=1 comment = 'AI模型表'; - - --- ---------------------------- --- 21、AI对话配置表 --- ---------------------------- -drop table if exists ai_chat_config; -create table ai_chat_config ( - chat_config_id bigint(20) not null auto_increment comment '配置主键', - user_id bigint(20) not null unique comment '用户ID', - temperature float default null comment '默认温度', - add_history_to_context char(1) default '0' comment '是否添加历史记录(0是, 1否)', - num_history_runs int(4) default null comment '历史记录条数', - system_prompt text default null comment '系统提示词', - metrics_default_visible char(1) default '0' comment '默认显示指标(0是, 1否)', - vision_enabled char(1) default '1' comment '是否开启视觉(0是, 1否)', - image_max_size_mb int(4) default null comment '图片最大大小(MB)', - create_time datetime comment '创建时间', - update_time datetime comment '更新时间', - primary key (chat_config_id) -) engine=innodb auto_increment=1 comment = 'AI对话配置表'; diff --git a/shierfeng-fastapi-backend/tests/__init__.py b/shierfeng-fastapi-backend/tests/__init__.py new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/shierfeng-fastapi-backend/tests/__init__.py @@ -0,0 +1 @@ + diff --git a/shierfeng-fastapi-backend/tests/module_business/test_ai_removal_contract.py b/shierfeng-fastapi-backend/tests/module_business/test_ai_removal_contract.py new file mode 100644 index 0000000..0718326 --- /dev/null +++ b/shierfeng-fastapi-backend/tests/module_business/test_ai_removal_contract.py @@ -0,0 +1,92 @@ +import json +import re +import unittest +from pathlib import Path + +PROJECT_ROOT = Path(__file__).resolve().parents[3] +BACKEND_ROOT = PROJECT_ROOT / 'shierfeng-fastapi-backend' +FRONTEND_ROOT = PROJECT_ROOT / 'shierfeng-fastapi-frontend' + +AI_DEPENDENCIES = { + 'agno', + 'anthropic', + 'cerebras-cloud-sdk', + 'cohere', + 'google-genai', + 'groq', + 'litellm', + 'llama-api-client', + 'mistralai', + 'ollama', + 'openai', + 'portkey-ai', +} +AI_FRONTEND_DEPENDENCIES = { + 'katex', + 'markstream-vue', + 'mermaid', + 'shiki', + 'stream-markdown', + 'stream-monaco', + 'uuid', + 'vite-plugin-monaco-editor-esm', +} + + +def _parse_requirement_names(requirements_path: Path) -> set[str]: + names = set() + for raw_line in requirements_path.read_text(encoding='utf-8').splitlines(): + line = raw_line.partition('#')[0].strip() + if not line or line == '.': + continue + match = re.match(r'[A-Za-z0-9_.-]+', line) + if match: + names.add(match.group(0).lower().replace('_', '-')) + return names + + +class AiRemovalContractTest(unittest.TestCase): + def test_ai_source_files_are_removed(self) -> None: + remaining_files = [] + module_ai = BACKEND_ROOT / 'module_ai' + if module_ai.exists(): + remaining_files.extend(module_ai.rglob('*.py')) + + explicit_files = ( + BACKEND_ROOT / 'utils/ai_util.py', + BACKEND_ROOT / 'utils/crypto_util.py', + FRONTEND_ROOT / 'src/api/ai/chat.js', + FRONTEND_ROOT / 'src/api/ai/model.js', + FRONTEND_ROOT / 'src/views/ai/chat/components/AiMessage.vue', + FRONTEND_ROOT / 'src/views/ai/chat/index.vue', + FRONTEND_ROOT / 'src/views/ai/model/index.vue', + FRONTEND_ROOT / 'src/assets/icons/svg/ai-chat.svg', + FRONTEND_ROOT / 'src/assets/icons/svg/ai-manage.svg', + FRONTEND_ROOT / 'src/assets/icons/svg/ai-model.svg', + ) + remaining_files.extend(path for path in explicit_files if path.exists()) + + self.assertEqual([], sorted(str(path.relative_to(PROJECT_ROOT)) for path in remaining_files)) + + def test_ai_dependencies_are_removed(self) -> None: + backend_dependencies = set() + for filename in ('requirements.txt', 'requirements-pg.txt'): + backend_dependencies.update(_parse_requirement_names(BACKEND_ROOT / filename)) + self.assertEqual([], sorted(AI_DEPENDENCIES & backend_dependencies)) + + package_json = json.loads((FRONTEND_ROOT / 'package.json').read_text(encoding='utf-8')) + frontend_dependencies = set(package_json.get('dependencies', {})) + frontend_dependencies.update(package_json.get('devDependencies', {})) + self.assertEqual([], sorted(AI_FRONTEND_DEPENDENCIES & frontend_dependencies)) + + def test_initialization_sql_has_no_ai_or_external_menu(self) -> None: + forbidden_terms = ('ai_models', 'ai_chat_config', 'ai_provider_type', '施尔丰仓库') + violations = [] + for filename in ('shierfeng-fastapi.sql', 'shierfeng-fastapi-pg.sql'): + sql = (BACKEND_ROOT / 'sql' / filename).read_text(encoding='utf-8').lower() + violations.extend(f'{filename}: {term}' for term in forbidden_terms if term.lower() in sql) + self.assertEqual([], violations) + + +if __name__ == '__main__': + unittest.main() diff --git a/shierfeng-fastapi-backend/utils/ai_util.py b/shierfeng-fastapi-backend/utils/ai_util.py deleted file mode 100644 index e9bd4c7..0000000 --- a/shierfeng-fastapi-backend/utils/ai_util.py +++ /dev/null @@ -1,172 +0,0 @@ -from importlib import import_module -from typing import TYPE_CHECKING - -from config.database import async_engine -from config.env import DataBaseConfig - -if TYPE_CHECKING: - from agno.db.base import AsyncBaseDb - from agno.models.base import Model - -# 提供商名称 -> (模块路径, 类名) 的映射,延迟导入避免启动时加载所有AI SDK -_PROVIDER_REGISTRY: dict[str, tuple[str, str]] = { - 'AIMLAPI': ('agno.models.aimlapi', 'AIMLAPI'), - 'Anthropic': ('agno.models.anthropic', 'Claude'), - 'Cerebras': ('agno.models.cerebras', 'Cerebras'), - 'CerebrasOpenAI': ('agno.models.cerebras', 'CerebrasOpenAI'), - 'Cohere': ('agno.models.cohere', 'Cohere'), - 'CometAPI': ('agno.models.cometapi', 'CometAPI'), - 'DashScope': ('agno.models.dashscope', 'DashScope'), - 'DeepInfra': ('agno.models.deepinfra', 'DeepInfra'), - 'DeepSeek': ('agno.models.deepseek', 'DeepSeek'), - 'Fireworks': ('agno.models.fireworks', 'Fireworks'), - 'Google': ('agno.models.google', 'Gemini'), - 'Groq': ('agno.models.groq', 'Groq'), - 'HuggingFace': ('agno.models.huggingface', 'HuggingFace'), - 'LangDB': ('agno.models.langdb', 'LangDB'), - 'LiteLLM': ('agno.models.litellm', 'LiteLLM'), - 'LiteLLMOpenAI': ('agno.models.litellm', 'LiteLLMOpenAI'), - 'LlamaCpp': ('agno.models.llama_cpp', 'LlamaCpp'), - 'LMStudio': ('agno.models.lmstudio', 'LMStudio'), - 'Meta': ('agno.models.meta', 'Llama'), - 'Mistral': ('agno.models.mistral', 'MistralChat'), - 'N1N': ('agno.models.n1n', 'N1N'), - 'Nebius': ('agno.models.nebius', 'Nebius'), - 'Nexus': ('agno.models.nexus', 'Nexus'), - 'Nvidia': ('agno.models.nvidia', 'Nvidia'), - 'Ollama': ('agno.models.ollama', 'Ollama'), - 'OpenAI': ('agno.models.openai', 'OpenAIChat'), - 'OpenAIResponses': ('agno.models.openai.responses', 'OpenAIResponses'), - 'OpenRouter': ('agno.models.openrouter', 'OpenRouter'), - 'Perplexity': ('agno.models.perplexity', 'Perplexity'), - 'Portkey': ('agno.models.portkey', 'Portkey'), - 'Requesty': ('agno.models.requesty', 'Requesty'), - 'Sambanova': ('agno.models.sambanova', 'Sambanova'), - 'SiliconFlow': ('agno.models.siliconflow', 'Siliconflow'), - 'Together': ('agno.models.together', 'Together'), - 'Vercel': ('agno.models.vercel', 'V0'), - 'VLLM': ('agno.models.vllm', 'VLLM'), - 'xAI': ('agno.models.xai', 'xAI'), -} - -# 存储引擎名称 -> (模块路径, 类名) 的映射 -_STORAGE_ENGINE_REGISTRY: dict[str, tuple[str, str]] = { - 'mysql': ('agno.db.mysql', 'AsyncMySQLDb'), - 'postgresql': ('agno.db.postgres', 'AsyncPostgresDb'), -} - -# 已加载的提供商类缓存,避免重复import_module -_provider_class_cache: dict[str, 'type[Model]'] = {} -_storage_class_cache: dict[str, 'type[AsyncBaseDb]'] = {} - - -class AiUtil: - """ - AI工具类 - """ - - @classmethod - def _resolve_provider_class(cls, provider: str) -> 'type[Model] | None': - """ - 按需加载并缓存提供商模型类 - - :param provider: 提供商名称 - :return: 模型类,未找到返回None - """ - if provider in _provider_class_cache: - return _provider_class_cache[provider] - entry = _PROVIDER_REGISTRY.get(provider) - if entry is None: - return None - module_path, class_name = entry - provider_cls = getattr(import_module(module_path), class_name) - _provider_class_cache[provider] = provider_cls - return provider_cls - - @classmethod - def _resolve_storage_class(cls, db_type: str) -> 'type[AsyncBaseDb]': - """ - 按需加载并缓存存储引擎类 - - :param db_type: 数据库类型 - :return: 存储引擎类 - """ - if db_type in _storage_class_cache: - return _storage_class_cache[db_type] - entry = _STORAGE_ENGINE_REGISTRY.get(db_type) - if entry is None: - # 默认使用MySQL - entry = _STORAGE_ENGINE_REGISTRY['mysql'] - module_path, class_name = entry - storage_cls = getattr(import_module(module_path), class_name) - _storage_class_cache[db_type] = storage_cls - return storage_cls - - @classmethod - def get_storage_engine(cls) -> 'AsyncBaseDb': - """ - 获取存储引擎实例 - - :return: 存储引擎实例 - """ - storage_engine_class = cls._resolve_storage_class(DataBaseConfig.db_type) - - return storage_engine_class( - db_engine=async_engine, - db_schema=DataBaseConfig.db_database if DataBaseConfig.db_type == 'mysql' else 'public', - session_table='ai_sessions', - memory_table='ai_memories', - metrics_table='ai_metrics', - eval_table='ai_eval_runs', - knowledge_table='ai_knowledge', - culture_table='ai_culture', - traces_table='ai_traces', - spans_table='ai_spans', - versions_table='ai_schema_versions', - create_schema=False, - ) - - @classmethod - def get_model_from_factory( - cls, - provider: str, - model_code: str, - model_name: str | None = None, - api_key: str | None = None, - base_url: str | None = None, - temperature: float | None = None, - max_tokens: int | None = None, - **kwargs, - ) -> 'Model': - """ - 从工厂获取模型实例 - - :param provider: 提供商 - :param model_code: 模型编码 - :param model_name: 模型名称 - :param api_key: API密钥 - :param base_url: 基础URL - :param temperature: 温度 - :param max_tokens: 最大令牌数 - :return: 模型实例 - """ - params = { - 'id': model_code, - 'name': model_name, - 'base_url': base_url, - 'api_key': api_key, - 'temperature': temperature, - 'max_tokens': max_tokens, - **kwargs, - } - params = {k: v for k, v in params.items() if v is not None} - if provider == 'Ollama': - params['host'] = base_url - if provider == 'DashScope' and not base_url: - params['base_url'] = 'https://dashscope.aliyuncs.com/compatible-mode/v1' - model_class = cls._resolve_provider_class(provider) - if model_class is None: - # 未知提供商,回退到OpenAI - model_class = cls._resolve_provider_class('OpenAI') - - return model_class(**params) diff --git a/shierfeng-fastapi-backend/utils/crypto_util.py b/shierfeng-fastapi-backend/utils/crypto_util.py deleted file mode 100644 index 8dc1229..0000000 --- a/shierfeng-fastapi-backend/utils/crypto_util.py +++ /dev/null @@ -1,58 +0,0 @@ -import base64 -import hashlib - -from cryptography.fernet import Fernet - -from config.env import JwtConfig -from exceptions.exception import ServiceException - - -class CryptoUtil: - """ - 加解密工具类 - """ - - _cipher_suite = None - - @classmethod - def _get_cipher_suite(cls) -> Fernet: - if cls._cipher_suite is None: - # 使用 JWT secret key 派生 Fernet key - # SHA256 hash (32 bytes) -> Base64 encoded (44 bytes) -> Fernet key - key = base64.urlsafe_b64encode(hashlib.sha256(JwtConfig.jwt_secret_key.encode()).digest()) - cls._cipher_suite = Fernet(key) - return cls._cipher_suite - - @classmethod - def encrypt(cls, data: str) -> str: - """ - 加密字符串 - - :param data: 明文 - :return: 密文 - """ - if not data: - return data - try: - cipher_suite = cls._get_cipher_suite() - encrypted_bytes = cipher_suite.encrypt(data.encode('utf-8')) - return encrypted_bytes.decode('utf-8') - except Exception as e: - raise ServiceException('加密失败') from e - - @classmethod - def decrypt(cls, token: str) -> str: - """ - 解密字符串 - - :param token: 密文 - :return: 明文 - """ - if not token: - return token - try: - cipher_suite = cls._get_cipher_suite() - decrypted_bytes = cipher_suite.decrypt(token.encode('utf-8')) - return decrypted_bytes.decode('utf-8') - except Exception as e: - raise ServiceException('解密失败') from e diff --git a/shierfeng-fastapi-backend/utils/log_util.py b/shierfeng-fastapi-backend/utils/log_util.py index 299f2d9..d0a3f36 100644 --- a/shierfeng-fastapi-backend/utils/log_util.py +++ b/shierfeng-fastapi-backend/utils/log_util.py @@ -590,9 +590,6 @@ class LoggerInitializer: for logger_name in ('uvicorn', 'uvicorn.error', 'uvicorn.access', 'fastapi'): logging.getLogger(logger_name).handlers = [InterceptHandler()] logging.getLogger(logger_name).propagate = False - for logger_name in ('LiteLLM', 'litellm'): - logging.getLogger(logger_name).setLevel(logging.WARNING) - def init_log(self) -> Logger: """ 初始化 Loguru 输出与标准 logging 配置 diff --git a/shierfeng-fastapi-frontend/src/api/ai/chat.js b/shierfeng-fastapi-frontend/src/api/ai/chat.js deleted file mode 100644 index ac47b39..0000000 --- a/shierfeng-fastapi-frontend/src/api/ai/chat.js +++ /dev/null @@ -1,53 +0,0 @@ -import request from "@/utils/request"; - -// 获取会话列表 -export function listChatSession() { - return request({ - url: "/ai/chat/session/list", - method: "get", - }); -} - -// 删除会话 -export function delChatSession(sessionId) { - return request({ - url: "/ai/chat/session/" + sessionId, - method: "delete", - }); -} - -// 获取会话详情 -export function getChatSession(sessionId) { - return request({ - url: "/ai/chat/session/" + sessionId, - method: "get", - }); -} - -// 获取用户对话配置 -export function getUserChatConfig() { - return request({ - url: "/ai/chat/config", - method: "get", - }); -} - -// 保存用户对话配置 -export function saveUserChatConfig(data) { - return request({ - url: "/ai/chat/config", - method: "put", - data: data, - }); -} - -// 取消对话 -export function cancelChatRun(runId) { - return request({ - url: "/ai/chat/cancel", - method: "post", - data: { - runId: runId, - }, - }); -} diff --git a/shierfeng-fastapi-frontend/src/api/ai/model.js b/shierfeng-fastapi-frontend/src/api/ai/model.js deleted file mode 100644 index 1d98442..0000000 --- a/shierfeng-fastapi-frontend/src/api/ai/model.js +++ /dev/null @@ -1,52 +0,0 @@ -import request from "@/utils/request"; - -// 查询AI模型列表 -export function listModel(query) { - return request({ - url: "/ai/model/list", - method: "get", - params: query, - }); -} - -// 查询所有AI模型列表 -export function listModelAll() { - return request({ - url: "/ai/model/all", - method: "get", - }); -} - -// 查询AI模型详细 -export function getModel(modelId) { - return request({ - url: "/ai/model/" + modelId, - method: "get", - }); -} - -// 新增AI模型 -export function addModel(data) { - return request({ - url: "/ai/model", - method: "post", - data: data, - }); -} - -// 修改AI模型 -export function updateModel(data) { - return request({ - url: "/ai/model", - method: "put", - data: data, - }); -} - -// 删除AI模型 -export function delModel(modelId) { - return request({ - url: "/ai/model/" + modelId, - method: "delete", - }); -} diff --git a/shierfeng-fastapi-frontend/src/assets/icons/svg/ai-chat.svg b/shierfeng-fastapi-frontend/src/assets/icons/svg/ai-chat.svg deleted file mode 100644 index 1d712a9..0000000 --- a/shierfeng-fastapi-frontend/src/assets/icons/svg/ai-chat.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/shierfeng-fastapi-frontend/src/assets/icons/svg/ai-manage.svg b/shierfeng-fastapi-frontend/src/assets/icons/svg/ai-manage.svg deleted file mode 100644 index e6a6921..0000000 --- a/shierfeng-fastapi-frontend/src/assets/icons/svg/ai-manage.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/shierfeng-fastapi-frontend/src/assets/icons/svg/ai-model.svg b/shierfeng-fastapi-frontend/src/assets/icons/svg/ai-model.svg deleted file mode 100644 index 5d3b2b9..0000000 --- a/shierfeng-fastapi-frontend/src/assets/icons/svg/ai-model.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/shierfeng-fastapi-frontend/src/views/ai/chat/components/AiMessage.vue b/shierfeng-fastapi-frontend/src/views/ai/chat/components/AiMessage.vue deleted file mode 100644 index b233d3b..0000000 --- a/shierfeng-fastapi-frontend/src/views/ai/chat/components/AiMessage.vue +++ /dev/null @@ -1,170 +0,0 @@ - - - - - - 深度思考过程 - 思考中... - - - - - - - - - - - - - - - - - - - diff --git a/shierfeng-fastapi-frontend/src/views/ai/chat/index.vue b/shierfeng-fastapi-frontend/src/views/ai/chat/index.vue deleted file mode 100644 index 7b44d26..0000000 --- a/shierfeng-fastapi-frontend/src/views/ai/chat/index.vue +++ /dev/null @@ -1,1338 +0,0 @@ - - - - - - - 新建对话 - - - - - - - - - {{ session.sessionTitle || "新对话" }} - - - {{ formatTime(session.createdAt) }} - - - - - - 暂无历史对话 - - - - - - - - - AI 智能助手 - - - - - - - - - - - - - - - - - - 你好!我是你的 AI 助手 - 请在下方输入问题开始对话... - - - - - - - - - {{ msg.role === "user" ? "我" : "AI 助手" }} - {{ - formatTime(msg.createdAt) - }} - - - - - - - {{ msg.content }} - - - - - - - - - - - - - - - - - - - - - - - - - 深度思考 - - - - {{ loading ? "停止" : "发送" }} - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - MB - - - - - - - - - - - - - - 取消 - 保存 - - - - - - - - - - diff --git a/shierfeng-fastapi-frontend/src/views/ai/model/index.vue b/shierfeng-fastapi-frontend/src/views/ai/model/index.vue deleted file mode 100644 index 641fbfa..0000000 --- a/shierfeng-fastapi-frontend/src/views/ai/model/index.vue +++ /dev/null @@ -1,473 +0,0 @@ - - - - - - - - - - - - - - - - - - 搜索 - 重置 - - - - - - 新增 - - - 修改 - - - 删除 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - {{ parseTime(scope.row.createTime) }} - - - - - 修改 - 删除 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - {{ dict.label }} - - - - - - - {{ dict.label }} - - - - - - - - - - - - {{ dict.label }} - - - - - - - - - - - - - - - - - - diff --git a/shierfeng-fastapi-frontend/vite/plugins/index.js b/shierfeng-fastapi-frontend/vite/plugins/index.js index eb9e9dd..10e17c3 100644 --- a/shierfeng-fastapi-frontend/vite/plugins/index.js +++ b/shierfeng-fastapi-frontend/vite/plugins/index.js @@ -1,42 +1,14 @@ import vue from '@vitejs/plugin-vue' -import monacoEditorEsmPlugin from 'vite-plugin-monaco-editor-esm' import createAutoImport from './auto-import' import createSvgIcon from './svg-icon' import createCompression from './compression' import createSetupExtend from './setup-extend' -const monacoWorkers = [ - { - label: 'editorWorkerService', - entry: 'monaco-editor/esm/vs/editor/editor.worker.js' - }, - { - label: 'css', - entry: 'monaco-editor/esm/vs/language/css/css.worker.js' - }, - { - label: 'html', - entry: 'monaco-editor/esm/vs/language/html/html.worker.js' - }, - { - label: 'json', - entry: 'monaco-editor/esm/vs/language/json/json.worker.js' - }, - { - label: 'typescript', - entry: 'monaco-editor/esm/vs/language/typescript/ts.worker.js' - } -] - export default function createVitePlugins(viteEnv, isBuild = false) { const vitePlugins = [vue()] vitePlugins.push(createAutoImport()) vitePlugins.push(createSetupExtend()) - vitePlugins.push(monacoEditorEsmPlugin({ - languageWorkers: [], - customWorkers: monacoWorkers - })) vitePlugins.push(createSvgIcon(isBuild)) isBuild && vitePlugins.push(...createCompression(viteEnv)) return vitePlugins
请在下方输入问题开始对话...