重构:移除 AI 管理模块
This commit is contained in:
@@ -23,6 +23,7 @@
|
||||
### 代码重构
|
||||
|
||||
1.移除对python3.9的支持 (#67)。
|
||||
2.移除AI管理模块。
|
||||
|
||||
### 代码优化
|
||||
|
||||
|
||||
@@ -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,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,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()
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user