refactor: 重构服务层方法命名规范

fix: 修复文件下载和删除功能
fix: 修复Redis哈希获取方法返回值类型
fix: 修复权限检查逻辑

perf: 优化部门和服务详情查询性能
perf: 优化角色数据范围显示

style: 清理无用导入和注释
style: 统一CRUD方法命名

docs: 更新main.py中的命令说明

chore: 移动IP定位工具类位置
chore: 更新.gitignore忽略迁移版本文件
This commit is contained in:
zhangtao
2025-10-03 03:31:45 +08:00
parent 7b9d8d7854
commit 119964e6f9
42 changed files with 326 additions and 545 deletions
+11 -11
View File
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
from fastapi import APIRouter, Depends, Path, Query, Body, WebSocket, Request
from fastapi import APIRouter, Depends, Path, Body, WebSocket
from fastapi.responses import JSONResponse, StreamingResponse
from app.common.response import StreamResponse, SuccessResponse
@@ -41,54 +41,54 @@ async def chat_controller(
@MCPRouter.get("/detail/{id}", summary="获取 MCP 服务器详情", description="获取 MCP 服务器详情")
async def get_mcp_detail_controller(
async def detail_controller(
id: int = Path(..., description="MCP ID"),
auth: AuthSchema = Depends(AuthPermission(permissions=["ai:mcp:query"]))
) -> JSONResponse:
result_dict = await McpService.get_mcp_detail_service(auth=auth, id=id)
result_dict = await McpService.detail_service(auth=auth, id=id)
logger.info(f"获取 MCP 服务器详情成功 {id}")
return SuccessResponse(data=result_dict, msg="获取 MCP 服务器详情成功")
@MCPRouter.get("/list", summary="查询 MCP 服务器列表", description="查询 MCP 服务器列表")
async def get_mcp_list_controller(
async def list_controller(
page: PaginationQueryParam = Depends(),
search: McpQueryParam = Depends(),
auth: AuthSchema = Depends(AuthPermission(permissions=["ai:mcp:query"]))
) -> JSONResponse:
result_dict_list = await McpService.get_mcp_list_service(auth=auth, search=search, order_by=page.order_by)
result_dict_list = await McpService.list_service(auth=auth, search=search, order_by=page.order_by)
result_dict = await PaginationService.paginate(data_list=result_dict_list, page_no=page.page_no, page_size=page.page_size)
logger.info(f"查询 MCP 服务器列表成功")
return SuccessResponse(data=result_dict, msg="查询 MCP 服务器列表成功")
@MCPRouter.post("/create", summary="创建 MCP 服务器", description="创建 MCP 服务器")
async def create_mcp_controller(
async def create_controller(
data: McpCreateSchema,
auth: AuthSchema = Depends(AuthPermission(permissions=["ai:mcp:create"]))
) -> JSONResponse:
result_dict = await McpService.create_mcp_service(auth=auth, data=data)
result_dict = await McpService.create_service(auth=auth, data=data)
logger.info(f"创建 MCP 服务器成功: {result_dict}")
return SuccessResponse(data=result_dict, msg="创建 MCP 服务器成功")
@MCPRouter.put("/update/{id}", summary="修改 MCP 服务器", description="修改 MCP 服务器")
async def update_mcp_controller(
async def update_controller(
data: McpUpdateSchema,
id: int = Path(..., description="MCP ID"),
auth: AuthSchema = Depends(AuthPermission(permissions=["ai:mcp:update"]))
) -> JSONResponse:
result_dict = await McpService.update_mcp_service(auth=auth, id=id, data=data)
result_dict = await McpService.update_service(auth=auth, id=id, data=data)
logger.info(f"修改 MCP 服务器成功: {result_dict}")
return SuccessResponse(data=result_dict, msg="修改 MCP 服务器成功")
@MCPRouter.delete("/delete", summary="删除 MCP 服务器", description="删除 MCP 服务器")
async def delete_mcp_controller(
async def delete_controller(
ids: list[int] = Body(..., description="ID列表"),
auth: AuthSchema = Depends(AuthPermission(permissions=["ai:mcp:delete"]))
) -> JSONResponse:
await McpService.delete_mcp_service(auth=auth, ids=ids)
await McpService.delete_service(auth=auth, ids=ids)
logger.info(f"删除 MCP 服务器成功: {ids}")
return SuccessResponse(msg="删除 MCP 服务器成功")
-205
View File
@@ -1,205 +0,0 @@
# -*- coding: utf-8 -*-
import asyncio
import httpx, aiofiles
import numpy as np
from openai import AsyncOpenAI
from app.core.logger import logger
class AIClient:
def __init__(self, kb_filepath=None, model="qwen3:4b", embedding_model="nomic-embed-text"):
# AI模型配置
self.model = model
self.embedding_model = embedding_model
# 创建HTTP客户端
self.http_client = httpx.AsyncClient(
timeout=30.0,
follow_redirects=True
)
# 初始化OpenAI客户端(用于与Ollama交互)
self.client = AsyncOpenAI(
api_key="ollama",
base_url="http://127.0.0.1:11434/v1",
http_client=self.http_client
)
# 知识库相关属性
self.docs = []
self.embeds = None
# 如果提供了知识库文件路径,则加载知识库
self.kb_loaded = False
self.kb_filepath = kb_filepath
# RAG提示词模板
self.prompt_template = """
基于以下知识回答用户的问题:
1: %s
2: %s
3: %s
4: %s
5: %s
用户的问题: %s
请根据提供的知识,用中文简洁准确地回答问题。如果提供的知识不足以回答,请说明这一点。
"""
# 知识库相关异步方法
async def load_kb(self):
"""异步加载知识库文件"""
if not self.kb_filepath or self.kb_loaded:
return
try:
# 异步读取文件
async with aiofiles.open(self.kb_filepath, 'r', encoding='utf-8') as f:
content = await f.read()
self.docs = self.split_content(content)
self.embeds = await self.encode(self.docs)
self.kb_loaded = True
logger.info(f"成功加载知识库,包含 {len(self.docs)} 个文档片段")
except Exception as e:
logger.error(f"加载知识库失败: {str(e)}")
raise
@staticmethod
def split_content(content):
"""将内容分割成文档块"""
chunks = []
# 按换行符分割成行
lines = content.splitlines()
for line in lines:
stripped_line = line.strip()
if stripped_line:
chunks.append(stripped_line)
return chunks
async def encode(self, texts):
"""异步使用Ollama生成嵌入向量"""
embeds = []
for text in texts:
try:
# 使用AsyncOpenAI客户端异步生成嵌入
response = await self.client.embeddings.create(
model=self.embedding_model,
input=text
)
embeds.append(response.data[0].embedding)
except Exception as e:
logger.error(f"生成嵌入向量失败 for text: {text[:30]}...: {str(e)}")
# 对于失败的嵌入,添加一个零向量
embeds.append([0.0] * 768) # 假设nomic-embed-text生成768维向量
return np.array(embeds)
@staticmethod
def similarity(e1, e2):
"""计算余弦相似度"""
dot_product = np.dot(e1, e2)
norm_e1 = np.linalg.norm(e1)
norm_e2 = np.linalg.norm(e2)
if norm_e1 == 0 or norm_e2 == 0:
return 0.0 # 避免除以零
return dot_product / (norm_e1 * norm_e2)
async def search(self, text, top_k=5):
"""异步在知识库中搜索相似文本"""
# 确保知识库已加载
if not self.kb_loaded:
await self.load_kb()
if not self.embeds.any():
logger.warning("知识库为空,无法进行搜索")
return []
# 生成查询文本的嵌入向量
query_embed = (await self.encode([text]))[0]
# 计算与所有文档的相似度
sims = [(idx, self.similarity(query_embed, doc_embed))
for idx, doc_embed in enumerate(self.embeds)]
# 按相似度排序
sims.sort(key=lambda x: x[1], reverse=True)
# 返回前top_k个匹配结果
top_matches = [self.docs[idx] for idx, _ in sims[:top_k]]
return top_matches
# RAG相关异步方法
async def build_rag_prompt(self, query):
"""异步构建RAG提示词"""
# 搜索知识库获取相关上下文
context = await self.search(query)
# 确保上下文有5个元素,不足的用空字符串填充
context += [""] * (5 - len(context))
# 构建提示词
return self.prompt_template % (
context[0], context[1], context[2], context[3], context[4], query
)
# AI处理相关方法
async def process(self, query: str, use_rag=True):
"""处理查询并返回流式响应,支持RAG模式"""
system_prompt = """你是一个有用的AI助手,可以帮助用户回答问题和提供帮助。请用中文回答用户的问题。"""
# 如果启用RAG,构建增强提示词
if use_rag and self.kb_filepath:
user_query = await self.build_rag_prompt(query)
else:
user_query = query
try:
# 使用 await 调用异步客户端
response = await self.client.chat.completions.create(
model=self.model,
messages=[
{"role": "system", "content": system_prompt},
{"role": "user", "content": user_query}
],
stream=True
)
# 流式返回响应
async for chunk in response:
if chunk.choices and chunk.choices[0].delta.content:
yield chunk.choices[0].delta.content
except Exception as e:
logger.error(f"AI处理查询失败: {str(e)}")
yield f"抱歉,处理您的请求时出现了错误: {str(e)}"
async def close(self):
"""关闭客户端连接"""
if hasattr(self, 'client'):
await self.client.close()
if hasattr(self, 'http_client'):
await self.http_client.aclose()
async def chat_query(message: str, kb_filepath=None):
"""处理聊天查询的异步函数"""
# 创建AI客户端实例,传入知识库文件路径
# message = message + "/no_think"
ai_client = AIClient(kb_filepath=kb_filepath)
try:
# 处理消息,启用RAG
async for response in ai_client.process(message, use_rag=True):
print(response, end='', flush=True)
finally:
# 确保关闭客户端连接
await ai_client.close()
if __name__ == "__main__":
# 在异步事件循环中运行聊天查询
asyncio.run(chat_query("帕金森氏症介绍,怎么治疗", kb_filepath='帕金森氏症en.txt'))
+6 -8
View File
@@ -1,22 +1,20 @@
# -*- coding: utf-8 -*-
from typing import AsyncGenerator, List, Dict, Optional, Any
from typing import List, Dict, Optional, Any
from app.core.exceptions import CustomException
from app.core.logger import logger
from app.api.v1.module_system.auth.schema import AuthSchema
from app.utils.ai_util import AIClient
from .schema import McpCreateSchema, McpUpdateSchema, McpOutSchema, ChatQuerySchema
from .param import McpQueryParam
from .crud import McpCRUD
from .model import McpModel
class McpService:
"""MCP服务层"""
@classmethod
async def get_mcp_detail_service(cls, auth: AuthSchema, id: int) -> Dict[str, Any]:
async def detail_service(cls, auth: AuthSchema, id: int) -> Dict[str, Any]:
"""详情"""
obj = await McpCRUD(auth).get_by_id_crud(id=id)
if not obj:
@@ -24,7 +22,7 @@ class McpService:
return McpOutSchema.model_validate(obj).model_dump()
@classmethod
async def get_mcp_list_service(cls, auth: AuthSchema, search: Optional[McpQueryParam] = None, order_by: Optional[List[Dict[str, str]]] = None) -> List[Dict[str, Any]]:
async def list_service(cls, auth: AuthSchema, search: Optional[McpQueryParam] = None, order_by: Optional[List[Dict[str, str]]] = None) -> List[Dict[str, Any]]:
"""列表查询"""
if order_by:
order_by = eval(str(order_by))
@@ -32,7 +30,7 @@ class McpService:
return [McpOutSchema.model_validate(obj).model_dump() for obj in obj_list]
@classmethod
async def create_mcp_service(cls, auth: AuthSchema, data: McpCreateSchema) -> Dict[str, Any]:
async def create_service(cls, auth: AuthSchema, data: McpCreateSchema) -> Dict[str, Any]:
"""创建"""
obj = await McpCRUD(auth).get_by_name_crud(name=data.name)
if obj:
@@ -41,7 +39,7 @@ class McpService:
return McpOutSchema.model_validate(obj).model_dump()
@classmethod
async def update_mcp_service(cls, auth: AuthSchema, id: int, data: McpUpdateSchema) -> Dict[str, Any]:
async def update_service(cls, auth: AuthSchema, id: int, data: McpUpdateSchema) -> Dict[str, Any]:
"""更新"""
obj = await McpCRUD(auth).get_by_id_crud(id=id)
if not obj:
@@ -53,7 +51,7 @@ class McpService:
return McpOutSchema.model_validate(obj).model_dump()
@classmethod
async def delete_mcp_service(cls, auth: AuthSchema, ids: List[int]) -> None:
async def delete_service(cls, auth: AuthSchema, ids: List[int]) -> None:
"""删除"""
if len(ids) < 1:
raise CustomException(msg='删除失败,删除对象不能为空')