重构:移除 AI 管理模块

This commit is contained in:
焦龙言
2026-07-24 19:06:55 +08:00
parent a509dada79
commit 5209911dd8
32 changed files with 96 additions and 4084 deletions
+1
View File
@@ -23,6 +23,7 @@
### 代码重构
1.移除对python3.9的支持 (#67)。
2.移除AI管理模块。
### 代码优化
-2
View File
@@ -35,8 +35,6 @@
16. 在线构建器:拖动表单元素生成相应的HTML代码。
17. 系统接口:根据业务代码自动生成相关的api接口文档。
18. 代码生成:配置数据库表信息一键生成前后端代码(python、sql、vue、js),支持下载。
19. AI管理:提供AI模型管理和AI对话功能。
## 在线体验
* *账号:admin*
@@ -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,
@@ -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,
@@ -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)
@@ -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)
@@ -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])
@@ -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])))
@@ -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='更新时间')
@@ -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='备注',
)
@@ -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='消息列表')
@@ -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='需要删除的模型主键')
@@ -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='取消成功')
@@ -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
+1 -12
View File
@@ -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
+1 -12
View File
@@ -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
@@ -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
@@ -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对话配置表';
@@ -0,0 +1 @@
@@ -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()
-172
View File
@@ -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)
@@ -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
@@ -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 配置
@@ -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,
},
});
}
@@ -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",
});
}
@@ -1 +0,0 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1768874965762" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="9150" xmlns:xlink="http://www.w3.org/1999/xlink" width="100" height="100"><path d="M568.32 183.978667q4.266667 0 8.362667-0.796445t7.964444-2.446222q3.925333-1.592889 7.395556-3.925333 3.527111-2.332444 6.485333-5.290667 2.958222-3.015111 5.290667-6.485333 2.332444-3.470222 3.982222-7.395556 1.592889-3.868444 2.389333-7.964444 0.853333-4.152889 0.853334-8.362667 0-4.152889-0.853334-8.305778-0.796444-4.096-2.389333-7.964444-1.649778-3.925333-3.982222-7.395556-2.275556-3.527111-5.290667-6.485333-2.958222-2.958222-6.485333-5.290667-3.470222-2.332444-7.395556-3.982222-3.868444-1.592889-7.964444-2.389333-4.096-0.853333-8.305778-0.853334H301.169778q-111.786667 0-190.862222 79.075556T31.288889 368.64v291.157333q0 111.843556 79.075555 190.919111t190.862223 79.075556h598.414222q28.672 0 48.981333-20.309333t20.309334-49.038223V489.244444q0-4.209778-0.853334-8.305777-0.796444-4.152889-2.389333-8.021334-1.649778-3.868444-3.982222-7.395555-2.275556-3.470222-5.290667-6.428445-2.958222-3.015111-6.485333-5.347555-3.413333-2.275556-7.395556-3.982222-3.811556-1.536-7.964444-2.389334-4.096-0.796444-8.305778-0.796444t-8.362667 0.796444q-4.096 0.853333-7.964444 2.446222-3.868444 1.592889-7.395556 3.982223-3.470222 2.275556-6.485333 5.290666-2.958222 2.958222-5.290667 6.428445-2.275556 3.527111-3.982222 7.395555-1.536 3.868444-2.389333 7.964445-0.796444 4.152889-0.796445 8.362666v355.157334H301.226667q-76.458667 0-130.56-54.044445-54.044444-54.044444-54.044445-130.56V368.64q0-76.515556 54.044445-130.56 54.101333-54.044444 130.56-54.044444h267.150222z" p-id="9151"></path><path d="M300.999111 398.222222m51.2 0l0 0q51.2 0 51.2 51.2l0 125.155556q0 51.2-51.2 51.2l0 0q-51.2 0-51.2-51.2l0-125.155556q0-51.2 51.2-51.2Z" p-id="9152"></path><path d="M585.443556 398.222222m51.2 0l0 0q51.2 0 51.2 51.2l0 125.155556q0 51.2-51.2 51.2l0 0q-51.2 0-51.2-51.2l0-125.155556q0-51.2 51.2-51.2Z" p-id="9153"></path><path d="M682.723556 200.704c-16.156444 3.982222-16.156444 26.965333 0 30.947556l66.503111 16.384a91.022222 91.022222 0 0 1 66.56 66.56l16.384 66.503111c3.982222 16.156444 26.965333 16.156444 30.947555 0l16.384-66.503111a91.022222 91.022222 0 0 1 66.56-66.56l66.503111-16.384c16.156444-3.982222 16.156444-26.965333 0-30.947556l-66.503111-16.440889a91.022222 91.022222 0 0 1-66.56-66.56L863.118222 51.313778c-3.982222-16.156444-26.965333-16.156444-30.947555 0L815.786667 117.76a91.022222 91.022222 0 0 1-66.56 66.56L682.666667 200.647111z" p-id="9154"></path></svg>

Before

Width:  |  Height:  |  Size: 2.7 KiB

@@ -1 +0,0 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1768875270205" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="12326" xmlns:xlink="http://www.w3.org/1999/xlink" width="100" height="100"><path d="M950.857143 515.657143a163.693714 163.693714 0 0 0-62.390857-128 163.876571 163.876571 0 0 0-102.180572-292.571429h-548.571428a163.876571 163.876571 0 0 0-102.180572 292.571429 162.486857 162.486857 0 0 0 0 256 163.876571 163.876571 0 0 0 102.180572 292.571428h548.571428a163.876571 163.876571 0 0 0 102.180572-292.571428A163.693714 163.693714 0 0 0 950.857143 515.657143z m-804.571429-256a91.428571 91.428571 0 0 1 91.428572-91.428572H256v36.571429a36.571429 36.571429 0 0 0 73.142857 0v-36.571429h73.142857v36.571429a36.571429 36.571429 0 0 0 73.142857 0v-36.571429h310.857143a91.428571 91.428571 0 1 1 0 182.857143h-548.571428A91.428571 91.428571 0 0 1 146.285714 259.657143z m731.428572 512a91.428571 91.428571 0 0 1-91.428572 91.428571h-548.571428a91.428571 91.428571 0 0 1 0-182.857143H256v36.571429a36.571429 36.571429 0 0 0 73.142857 0v-36.571429h73.142857v36.571429a36.571429 36.571429 0 0 0 73.142857 0v-36.571429h310.857143a91.428571 91.428571 0 0 1 91.428572 91.428572zM237.714286 607.085714a91.428571 91.428571 0 0 1 0-182.857143H256v36.571429a36.571429 36.571429 0 0 0 73.142857 0v-36.571429h73.142857v36.571429a36.571429 36.571429 0 0 0 73.142857 0v-36.571429h310.857143a91.428571 91.428571 0 1 1 0 182.857143h-548.571428z" p-id="12327"></path></svg>

Before

Width:  |  Height:  |  Size: 1.6 KiB

@@ -1 +0,0 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1768874837359" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="4895" xmlns:xlink="http://www.w3.org/1999/xlink" width="100" height="100"><path d="M418.133333 356.266667L298.666667 661.333333h66.133333l25.6-70.4H512l25.6 70.4h66.133333l-121.6-305.066666h-64z m-10.666666 183.466666L448 426.666667l42.666667 113.066666h-83.2zM635.733333 356.266667h61.866667V661.333333h-61.866667z" p-id="4896"></path><path d="M512 85.333333L142.933333 298.666667v426.666666L512 938.666667l369.066667-213.333334V298.666667L512 85.333333z m292.266667 595.2L512 849.066667l-292.266667-168.533334V343.466667L512 174.933333l292.266667 168.533334v337.066666z" p-id="4897"></path></svg>

Before

Width:  |  Height:  |  Size: 848 B

@@ -1,170 +0,0 @@
<template>
<div class="ai-message-container">
<div v-if="reasoningContent" class="reasoning-section">
<div class="reasoning-header" @click="toggleReasoning">
<el-icon :class="{ 'is-expanded': isReasoningExpanded }"
><ArrowRight
/></el-icon>
<span>深度思考过程</span>
<span class="reasoning-status" v-if="!isThinkingComplete"
>思考中...</span
>
</div>
<div v-show="isReasoningExpanded" class="reasoning-content">
<MarkdownRender :content="reasoningContent" :is-dark="isDark" />
</div>
</div>
<div class="ai-message-content">
<MarkdownRender :content="content" :is-dark="isDark" />
</div>
<div
v-if="loading && !content && !reasoningContent"
class="typing-indicator"
>
<span></span>
<span></span>
<span></span>
</div>
</div>
</template>
<script setup>
import { computed, ref } from "vue";
import { MarkdownRender } from "markstream-vue";
import { useDark } from "@vueuse/core";
import { enableKatex, enableMermaid } from "markstream-vue";
import "markstream-vue/index.css";
import "katex/dist/katex.min.css";
enableMermaid();
enableKatex();
const isDark = useDark();
const props = defineProps({
content: {
type: String,
default: "",
},
reasoningContent: {
type: String,
default: "",
},
loading: {
type: Boolean,
default: false,
},
});
const isReasoningExpanded = ref(true);
const isThinkingComplete = computed(() => {
return !!props.content;
});
function toggleReasoning() {
isReasoningExpanded.value = !isReasoningExpanded.value;
}
</script>
<style lang="scss">
.ai-message-container {
width: 100%;
.ai-message-content {
font-size: 14px;
line-height: 1.6;
color: var(--el-text-color-primary);
overflow-wrap: break-word;
word-break: break-word;
}
}
.reasoning-section {
margin-bottom: 15px;
border-left: 2px solid var(--el-border-color);
padding-left: 10px;
.reasoning-header {
display: flex;
align-items: center;
cursor: pointer;
color: var(--el-text-color-secondary);
font-size: 13px;
user-select: none;
margin-bottom: 5px;
.el-icon {
margin-right: 4px;
transition: transform 0.3s;
&.is-expanded {
transform: rotate(90deg);
}
}
.reasoning-status {
margin-left: 10px;
font-size: 12px;
color: #e6a23c;
animation: blink 1.5s infinite;
}
}
.reasoning-content {
font-size: 13px;
color: var(--el-text-color-regular);
padding: 8px;
background-color: var(--el-fill-color-light);
border-radius: 4px;
overflow-wrap: break-word;
word-break: break-word;
}
}
@keyframes blink {
0% {
opacity: 1;
}
50% {
opacity: 0.5;
}
100% {
opacity: 1;
}
}
.typing-indicator {
display: flex;
align-items: center;
padding: 8px 0;
span {
display: inline-block;
width: 6px;
height: 6px;
background-color: var(--el-text-color-secondary);
border-radius: 50%;
animation: typing 1.4s infinite ease-in-out both;
margin-right: 4px;
&:nth-child(1) {
animation-delay: -0.32s;
}
&:nth-child(2) {
animation-delay: -0.16s;
}
}
}
@keyframes typing {
0%,
80%,
100% {
transform: scale(0);
}
40% {
transform: scale(1);
}
}
</style>
File diff suppressed because it is too large Load Diff
@@ -1,473 +0,0 @@
<template>
<div class="app-container">
<el-form
:model="queryParams"
ref="queryRef"
:inline="true"
v-show="showSearch"
>
<el-form-item label="模型编码" prop="modelCode">
<el-input
v-model="queryParams.modelCode"
placeholder="请输入模型编码"
clearable
style="width: 200px"
@keyup.enter="handleQuery"
/>
</el-form-item>
<el-form-item label="提供商" prop="provider">
<el-select
v-model="queryParams.provider"
placeholder="请选择提供商"
clearable
style="width: 200px"
@keyup.enter="handleQuery"
>
<el-option
v-for="dict in ai_provider_type"
:key="dict.value"
:label="dict.label"
:value="dict.value"
/>
</el-select>
</el-form-item>
<el-form-item label="状态" prop="status">
<el-select
v-model="queryParams.status"
placeholder="模型状态"
clearable
style="width: 240px"
>
<el-option
v-for="dict in sys_normal_disable"
:key="dict.value"
:label="dict.label"
:value="dict.value"
/>
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" icon="Search" @click="handleQuery"
>搜索</el-button
>
<el-button icon="Refresh" @click="resetQuery">重置</el-button>
</el-form-item>
</el-form>
<el-row :gutter="10" class="mb8">
<el-col :span="1.5">
<el-button
type="primary"
plain
icon="Plus"
@click="handleAdd"
v-hasPermi="['ai:model:add']"
>新增</el-button
>
</el-col>
<el-col :span="1.5">
<el-button
type="success"
plain
icon="Edit"
:disabled="single"
@click="handleUpdate"
v-hasPermi="['ai:model:edit']"
>修改</el-button
>
</el-col>
<el-col :span="1.5">
<el-button
type="danger"
plain
icon="Delete"
:disabled="multiple"
@click="handleDelete"
v-hasPermi="['ai:model:remove']"
>删除</el-button
>
</el-col>
<right-toolbar
v-model:showSearch="showSearch"
@queryTable="getList"
></right-toolbar>
</el-row>
<el-table
v-loading="loading"
:data="modelList"
@selection-change="handleSelectionChange"
>
<el-table-column type="selection" width="55" align="center" />
<el-table-column label="模型ID" align="center" prop="modelId" />
<el-table-column label="模型编码" align="center" prop="modelCode" />
<el-table-column label="提供商" align="center" prop="provider">
<template #default="scope">
<dict-tag :options="ai_provider_type" :value="scope.row.provider" />
</template>
</el-table-column>
<el-table-column label="支持推理" align="center" prop="supportReasoning">
<template #default="scope">
<dict-tag :options="sys_yes_no" :value="scope.row.supportReasoning" />
</template>
</el-table-column>
<el-table-column label="支持图片" align="center" prop="supportImages">
<template #default="scope">
<dict-tag :options="sys_yes_no" :value="scope.row.supportImages" />
</template>
</el-table-column>
<el-table-column label="状态" align="center" prop="status">
<template #default="scope">
<dict-tag :options="sys_normal_disable" :value="scope.row.status" />
</template>
</el-table-column>
<el-table-column
label="创建时间"
align="center"
prop="createTime"
width="180"
>
<template #default="scope">
<span>{{ parseTime(scope.row.createTime) }}</span>
</template>
</el-table-column>
<el-table-column
label="操作"
width="180"
align="center"
class-name="small-padding fixed-width"
>
<template #default="scope">
<el-button
link
type="primary"
icon="Edit"
@click="handleUpdate(scope.row)"
v-hasPermi="['ai:model:edit']"
>修改</el-button
>
<el-button
link
type="primary"
icon="Delete"
@click="handleDelete(scope.row)"
v-hasPermi="['ai:model:remove']"
>删除</el-button
>
</template>
</el-table-column>
</el-table>
<pagination
v-show="total > 0"
:total="total"
v-model:page="queryParams.pageNum"
v-model:limit="queryParams.pageSize"
@pagination="getList"
/>
<!-- 添加或修改对话框 -->
<el-dialog :title="title" v-model="open" width="700px" append-to-body>
<el-form ref="modelRef" :model="form" :rules="rules" label-width="100px">
<el-row :gutter="10">
<el-col :span="12">
<el-form-item label="模型编码" prop="modelCode">
<el-input
v-model="form.modelCode"
placeholder="请输入模型编码 (如 deepseek-r1)"
/>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="模型名称" prop="modelName">
<el-input v-model="form.modelName" placeholder="请输入模型名称" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="提供商" prop="provider">
<el-select
v-model="form.provider"
placeholder="请选择提供商"
style="width: 100%"
>
<el-option
v-for="dict in ai_provider_type"
:key="dict.value"
:label="dict.label"
:value="dict.value"
/>
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="模型排序" prop="modelSort">
<el-input-number
v-model="form.modelSort"
:min="0"
style="width: 100%"
/>
</el-form-item>
</el-col>
<el-col :span="24">
<el-form-item label="API Key" prop="apiKey">
<el-input
v-model="form.apiKey"
placeholder="请输入API Key"
type="password"
/>
</el-form-item>
</el-col>
<el-col :span="24">
<el-form-item label="Base URL" prop="baseUrl">
<el-input v-model="form.baseUrl" placeholder="请输入Base URL" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="最大输出" prop="maxTokens">
<el-input-number
v-model="form.maxTokens"
:min="0"
style="width: 100%"
placeholder="最大输出"
/>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="默认温度" prop="temperature">
<el-input-number
v-model="form.temperature"
:min="0"
:max="2"
:step="0.1"
placeholder="默认温度"
style="width: 100%"
/>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="支持推理" prop="supportReasoning">
<el-radio-group v-model="form.supportReasoning">
<el-radio
v-for="dict in sys_yes_no"
:key="dict.value"
:value="dict.value"
>{{ dict.label }}</el-radio
>
</el-radio-group>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="支持图片" prop="supportImages">
<el-radio-group v-model="form.supportImages">
<el-radio
v-for="dict in sys_yes_no"
:key="dict.value"
:value="dict.value"
>{{ dict.label }}</el-radio
>
</el-radio-group>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="模型类型" prop="modelType">
<el-input
v-model="form.modelType"
placeholder="请输入模型类型 (可选)"
/>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="状态" prop="status">
<el-radio-group v-model="form.status">
<el-radio
v-for="dict in sys_normal_disable"
:key="dict.value"
:value="dict.value"
>{{ dict.label }}</el-radio
>
</el-radio-group>
</el-form-item>
</el-col>
<el-col :span="24">
<el-form-item label="备注" prop="remark">
<el-input
v-model="form.remark"
type="textarea"
placeholder="请输入内容"
/>
</el-form-item>
</el-col>
</el-row>
</el-form>
<template #footer>
<div class="dialog-footer">
<el-button type="primary" @click="submitForm">确 定</el-button>
<el-button @click="cancel">取 消</el-button>
</div>
</template>
</el-dialog>
</div>
</template>
<script setup name="AiModel">
import {
listModel,
addModel,
delModel,
getModel,
updateModel,
} from "@/api/ai/model";
const { proxy } = getCurrentInstance();
const { ai_provider_type, sys_normal_disable, sys_yes_no } = proxy.useDict(
"ai_provider_type",
"sys_normal_disable",
"sys_yes_no",
);
const modelList = ref([]);
const open = ref(false);
const loading = ref(true);
const showSearch = ref(true);
const ids = ref([]);
const single = ref(true);
const multiple = ref(true);
const total = ref(0);
const title = ref("");
const data = reactive({
form: {},
queryParams: {
pageNum: 1,
pageSize: 10,
modelCode: undefined,
provider: undefined,
status: undefined,
},
rules: {
modelCode: [
{ required: true, message: "模型编码不能为空", trigger: "blur" },
],
provider: [
{ required: true, message: "提供商不能为空", trigger: "change" },
],
modelSort: [
{ required: true, message: "模型排序不能为空", trigger: "blur" },
],
},
});
const { queryParams, form, rules } = toRefs(data);
/** 查询列表 */
function getList() {
loading.value = true;
listModel(queryParams.value).then((response) => {
modelList.value = response.rows;
total.value = response.total;
loading.value = false;
});
}
/** 取消按钮 */
function cancel() {
open.value = false;
reset();
}
/** 表单重置 */
function reset() {
form.value = {
modelId: undefined,
modelCode: undefined,
modelName: undefined,
provider: undefined,
modelSort: 0,
apiKey: undefined,
baseUrl: undefined,
maxTokens: undefined,
temperature: undefined,
supportReasoning: "N",
supportImages: "N",
modelType: undefined,
status: "0",
remark: undefined,
};
proxy.resetForm("modelRef");
}
/** 搜索按钮操作 */
function handleQuery() {
queryParams.value.pageNum = 1;
getList();
}
/** 重置按钮操作 */
function resetQuery() {
proxy.resetForm("queryRef");
handleQuery();
}
/** 多选框选中数据 */
function handleSelectionChange(selection) {
ids.value = selection.map((item) => item.modelId);
single.value = selection.length != 1;
multiple.value = !selection.length;
}
/** 新增按钮操作 */
function handleAdd() {
reset();
open.value = true;
title.value = "添加模型";
}
/** 修改按钮操作 */
function handleUpdate(row) {
reset();
const modelId = row.modelId || ids.value;
getModel(modelId).then((response) => {
form.value = response.data;
open.value = true;
title.value = "修改模型";
});
}
/** 提交按钮 */
function submitForm() {
proxy.$refs["modelRef"].validate((valid) => {
if (valid) {
if (form.value.modelId != undefined) {
updateModel(form.value).then((response) => {
proxy.$modal.msgSuccess("修改成功");
open.value = false;
getList();
});
} else {
addModel(form.value).then((response) => {
proxy.$modal.msgSuccess("新增成功");
open.value = false;
getList();
});
}
}
});
}
/** 删除按钮操作 */
function handleDelete(row) {
const modelIds = row.modelId || ids.value;
proxy.$modal
.confirm('是否确认删除模型编号为"' + modelIds + '"的数据项?')
.then(function () {
return delModel(modelIds);
})
.then(() => {
getList();
proxy.$modal.msgSuccess("删除成功");
})
.catch(() => {});
}
getList();
</script>
@@ -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