# ============================================================================= # 企微IT智能服务台 — 知识库自动迭代 API(Tier1 扩展) # ============================================================================= # 说明:知识库自动迭代相关接口(扩展版)。 # 1. POST /api/admin/knowledge-iteration/analyze - 触发分析并生成建议 # 2. GET /api/admin/knowledge-iteration/suggestions - 获取建议列表(支持 audience/confidence 筛选) # 3. GET /api/admin/knowledge-iteration/suggestions/{id} - 获取建议详情 # 4. POST /api/admin/knowledge-iteration/suggestions/{id}/approve - 审核通过(触发Neo4j写图) # 5. POST /api/admin/knowledge-iteration/suggestions/{id}/reject - 审核拒绝 # 6. POST /api/admin/knowledge-iteration/suggestions/{id}/rewrite - 改写提案(Tier1新增) # 7. POST /api/admin/knowledge-iteration/suggestions/{id}/queue - 放入独立队列(Tier1新增) # 8. POST /api/admin/knowledge-iteration/suggestions/{id}/dequeue-approve - 队列中审批(Tier1新增) # 9. GET /api/admin/knowledge-iteration/stats - 获取统计 # ============================================================================= import logging from typing import Optional from fastapi import APIRouter, Depends, Query from sqlalchemy.ext.asyncio import AsyncSession from app.database import get_db from app.dependencies import get_current_user, require_admin, UserInfo from app.models.knowledge_suggestion import KnowledgeSuggestion from app.schemas.knowledge_suggestion import ( KnowledgeSuggestionListResponse, KnowledgeSuggestionResponse, KnowledgeSuggestionStatsResponse, KnowledgeSuggestionApprove, KnowledgeSuggestionReject, KnowledgeSuggestionRewrite, KnowledgeSuggestionMerge, ) from app.services.knowledge_iteration_service import ( KnowledgeIterationService, dep_knowledge_iteration_service, ) from app.services.neo4j_client import get_neo4j_client logger = logging.getLogger(__name__) router = APIRouter() # ----------------------------------------------------------------------------- # 触发分析 # ----------------------------------------------------------------------------- # POST /api/admin/knowledge-iteration/analyze @router.post("/analyze") @require_admin async def trigger_analysis( days: int = Query(default=7, ge=1, le=90, description="分析过去N天的数据"), current_user: UserInfo = Depends(get_current_user), db: AsyncSession = Depends(get_db), service: KnowledgeIterationService = Depends(dep_knowledge_iteration_service), ): """触发知识库迭代分析。 分析过去N天的标注数据和会话数据,调用 Dify AI 自动生成优化建议。 - **days**: 分析过去N天的数据(默认7天,最大90天) **需要管理员权限。** """ logger.info(f"管理员 {current_user.name} 触发了知识库迭代分析, days={days}") result = await service.analyze_and_generate_suggestions(db, days=days) return { "code": 0, "message": "分析完成", "data": result, } # ----------------------------------------------------------------------------- # 获取建议列表(Tier1 扩展:audience/confidence 筛选) # ----------------------------------------------------------------------------- # GET /api/admin/knowledge-iteration/suggestions @router.get("/suggestions") @require_admin async def list_suggestions( status: Optional[str] = Query(default=None, description="筛选状态:pending/queued/approved/rejected/applied/graph_synced/expired"), suggestion_type: Optional[str] = Query(default=None, description="筛选类型:new_faq/update/outdated"), audience: Optional[str] = Query(default=None, description="筛选受众:employee_quick_reply/engineer_workguide"), confidence_min: Optional[float] = Query(default=None, ge=0.0, le=1.0, description="置信度下限"), confidence_max: Optional[float] = Query(default=None, ge=0.0, le=1.0, description="置信度上限"), page: int = Query(default=1, ge=1, description="页码"), page_size: int = Query(default=20, ge=1, le=100, description="每页数量"), current_user: UserInfo = Depends(get_current_user), db: AsyncSession = Depends(get_db), service: KnowledgeIterationService = Depends(dep_knowledge_iteration_service), ): """获取知识库优化建议列表(Tier1 扩展:支持 audience/confidence 筛选)。 - **status**: 筛选状态 - **suggestion_type**: 筛选类型 - **audience**: 按受众类型筛选(Tier1 新增) - **confidence_min**: 置信度下限(Tier1 新增) - **confidence_max**: 置信度上限(Tier1 新增) - **page**: 页码 - **page_size**: 每页数量 **需要管理员权限。** """ from sqlalchemy import select, func # 构建查询 stmt = select(KnowledgeSuggestion).order_by( KnowledgeSuggestion.created_at.desc() ) if status: stmt = stmt.where(KnowledgeSuggestion.status == status) if suggestion_type: stmt = stmt.where(KnowledgeSuggestion.suggestion_type == suggestion_type) if audience: stmt = stmt.where(KnowledgeSuggestion.audience == audience) if confidence_min is not None: stmt = stmt.where(KnowledgeSuggestion.confidence >= confidence_min) if confidence_max is not None: stmt = stmt.where(KnowledgeSuggestion.confidence <= confidence_max) # 分页 offset = (page - 1) * page_size stmt = stmt.offset(offset).limit(page_size) result = await db.execute(stmt) suggestions = result.scalars().all() # 统计总数 count_stmt = select(func.count()).select_from(KnowledgeSuggestion) if status: count_stmt = count_stmt.where(KnowledgeSuggestion.status == status) if suggestion_type: count_stmt = count_stmt.where( KnowledgeSuggestion.suggestion_type == suggestion_type ) if audience: count_stmt = count_stmt.where(KnowledgeSuggestion.audience == audience) if confidence_min is not None: count_stmt = count_stmt.where(KnowledgeSuggestion.confidence >= confidence_min) if confidence_max is not None: count_stmt = count_stmt.where(KnowledgeSuggestion.confidence <= confidence_max) total_result = await db.execute(count_stmt) total = total_result.scalar() return { "code": 0, "message": "success", "data": { "total": total, "items": [ KnowledgeSuggestionResponse.model_validate(s) for s in suggestions ], }, } # ----------------------------------------------------------------------------- # 获取建议详情 # ----------------------------------------------------------------------------- # GET /api/admin/knowledge-iteration/suggestions/{id} @router.get("/suggestions/{suggestion_id}") @require_admin async def get_suggestion( suggestion_id: str, current_user: UserInfo = Depends(get_current_user), db: AsyncSession = Depends(get_db), ): """获取知识库优化建议详情。 - **suggestion_id**: 建议ID **需要管理员权限。** """ from sqlalchemy import select stmt = select(KnowledgeSuggestion).where( KnowledgeSuggestion.id == suggestion_id ) result = await db.execute(stmt) suggestion = result.scalar_one_or_none() if not suggestion: return {"code": 404, "message": "建议不存在", "data": None} return { "code": 0, "message": "success", "data": KnowledgeSuggestionResponse.model_validate(suggestion), } # ----------------------------------------------------------------------------- # 审核通过(Tier1 扩展:串联 Neo4j 写图) # ----------------------------------------------------------------------------- # POST /api/admin/knowledge-iteration/suggestions/{id}/approve @router.post("/suggestions/{suggestion_id}/approve") @require_admin async def approve_suggestion( suggestion_id: str, body: KnowledgeSuggestionApprove, current_user: UserInfo = Depends(get_current_user), db: AsyncSession = Depends(get_db), service: KnowledgeIterationService = Depends(dep_knowledge_iteration_service), ): """审核通过知识库优化建议(Tier1:串联 Neo4j 写图 + 五态流转)。 审核通过后: 1. 状态 pending/queued → approved → applied → graph_synced 2. 自动创建 KnowledgeBase 条目(派生视图) 3. 触发 Neo4j 图写入(D1 解读2 合一) - **suggestion_id**: 建议ID **需要管理员权限。** """ logger.info( f"管理员 {current_user.name} 审核通过建议: {suggestion_id}" ) # 尝试获取 Neo4j 客户端(可选,不影响审批主流程) neo4j_client = await get_neo4j_client() suggestion = await service.approve_suggestion( db, suggestion_id, current_user.employee_id, neo4j_client=neo4j_client, ) if not suggestion: return {"code": 404, "message": "建议不存在或状态转换无效", "data": None} return { "code": 0, "message": "审核通过,建议已应用到知识库并同步至知识图谱", "data": KnowledgeSuggestionResponse.model_validate(suggestion), } # ----------------------------------------------------------------------------- # 审核拒绝 # ----------------------------------------------------------------------------- # POST /api/admin/knowledge-iteration/suggestions/{id}/reject @router.post("/suggestions/{suggestion_id}/reject") @require_admin async def reject_suggestion( suggestion_id: str, body: KnowledgeSuggestionReject, current_user: UserInfo = Depends(get_current_user), db: AsyncSession = Depends(get_db), service: KnowledgeIterationService = Depends(dep_knowledge_iteration_service), ): """拒绝知识库优化建议。 - **suggestion_id**: 建议ID **需要管理员权限。** """ logger.info( f"管理员 {current_user.name} 拒绝建议: {suggestion_id}, " f"理由: {body.reject_reason}" ) suggestion = await service.reject_suggestion( db, suggestion_id, current_user.employee_id, body.reject_reason ) if not suggestion: return {"code": 404, "message": "建议不存在", "data": None} return { "code": 0, "message": "已拒绝该建议", "data": KnowledgeSuggestionResponse.model_validate(suggestion), } # ----------------------------------------------------------------------------- # 改写提案(Tier1 新增 — D7 内联审批改写) # ----------------------------------------------------------------------------- # POST /api/admin/knowledge-iteration/suggestions/{id}/rewrite @router.post("/suggestions/{suggestion_id}/rewrite") @require_admin async def rewrite_suggestion( suggestion_id: str, body: KnowledgeSuggestionRewrite, current_user: UserInfo = Depends(get_current_user), db: AsyncSession = Depends(get_db), service: KnowledgeIterationService = Depends(dep_knowledge_iteration_service), ): """训练师改写知识库优化建议(Tier1 新增)。 改写后提案状态重置为 pending,重新走审批流程。 可修改字段:title、content、category、tags、confidence、audience、 issue、action、relation_type、parent_issue。 - **suggestion_id**: 建议ID **需要管理员权限。** """ logger.info( f"管理员 {current_user.name} 改写建议: {suggestion_id}" ) # 将非 None 的字段收集为改写数据 rewrite_data = body.model_dump(exclude_none=True, exclude_unset=True) suggestion = await service.rewrite_suggestion( db, suggestion_id, current_user.employee_id, rewrite_data ) if not suggestion: return {"code": 404, "message": "建议不存在", "data": None} return { "code": 0, "message": "提案已改写,等待重新审批", "data": KnowledgeSuggestionResponse.model_validate(suggestion), } # ----------------------------------------------------------------------------- # 放入独立队列(Tier1 新增 — D7 独立队列) # ----------------------------------------------------------------------------- # POST /api/admin/knowledge-iteration/suggestions/{id}/queue @router.post("/suggestions/{suggestion_id}/queue") @require_admin async def queue_suggestion( suggestion_id: str, current_user: UserInfo = Depends(get_current_user), db: AsyncSession = Depends(get_db), service: KnowledgeIterationService = Depends(dep_knowledge_iteration_service), ): """将建议放入独立审批队列(Tier1 新增)。 当会话关闭且提案仍处于 pending 时调用,将提案状态改为 queued。 - **suggestion_id**: 建议ID **需要管理员权限。** """ logger.info( f"管理员 {current_user.name} 将建议放入独立队列: {suggestion_id}" ) suggestion = await service.queue_suggestion(db, suggestion_id) if not suggestion: return {"code": 404, "message": "建议不存在或状态转换无效", "data": None} return { "code": 0, "message": "建议已放入独立审批队列", "data": KnowledgeSuggestionResponse.model_validate(suggestion), } # ----------------------------------------------------------------------------- # 队列中审批通过(Tier1 新增 — D7 独立队列审批) # ----------------------------------------------------------------------------- # POST /api/admin/knowledge-iteration/suggestions/{id}/dequeue-approve @router.post("/suggestions/{suggestion_id}/dequeue-approve") @require_admin async def dequeue_approve_suggestion( suggestion_id: str, current_user: UserInfo = Depends(get_current_user), db: AsyncSession = Depends(get_db), service: KnowledgeIterationService = Depends(dep_knowledge_iteration_service), ): """从独立队列中审批通过建议(Tier1 新增)。 流程与 approve 一致:状态流转 + KB 落库 + Neo4j 写图。 - **suggestion_id**: 建议ID **需要管理员权限。** """ logger.info( f"管理员 {current_user.name} 从队列中审批通过建议: {suggestion_id}" ) neo4j_client = await get_neo4j_client() suggestion = await service.dequeue_approve( db, suggestion_id, current_user.employee_id, neo4j_client=neo4j_client, ) if not suggestion: return {"code": 404, "message": "建议不存在或状态转换无效", "data": None} return { "code": 0, "message": "队列审批通过,建议已应用到知识库并同步至知识图谱", "data": KnowledgeSuggestionResponse.model_validate(suggestion), } # ----------------------------------------------------------------------------- # 获取统计 # ----------------------------------------------------------------------------- # GET /api/admin/knowledge-iteration/stats @router.get("/stats") @require_admin async def get_stats( current_user: UserInfo = Depends(get_current_user), db: AsyncSession = Depends(get_db), service: KnowledgeIterationService = Depends(dep_knowledge_iteration_service), ): """获取知识库优化建议统计。 返回各状态的建议数量统计(含 queued/graph_synced/expired)。 **需要管理员权限。** """ stats = await service.get_suggestion_stats(db) return { "code": 0, "message": "success", "data": KnowledgeSuggestionStatsResponse(**stats), } # ----------------------------------------------------------------------------- # 知识图谱可视化(任务2:P2) # ----------------------------------------------------------------------------- # GET /api/admin/knowledge-iteration/graph @router.get("/graph") @require_admin async def get_knowledge_graph( limit: int = Query(default=100, ge=10, le=500, description="节点数量上限"), issue_name: Optional[str] = Query(default=None, description="指定Issue名称查询子图"), current_user: UserInfo = Depends(get_current_user), service: KnowledgeIterationService = Depends(dep_knowledge_iteration_service), ): """获取知识图谱数据(Neo4j 节点+关系 JSON 格式)。 返回 ECharts 力导向图兼容的节点和关系数据。 支持全图查询(默认)和指定 Issue 的子图查询。 - **limit**: 节点数量上限(10-500) - **issue_name**: 指定 Issue 名称时查询子图(用于审批卡片预览) **需要管理员权限。** """ neo4j_client = await get_neo4j_client() if not neo4j_client: return { "code": 0, "message": "Neo4j 不可用,图数据为空", "data": {"nodes": [], "links": []}, } if issue_name: graph_data = await neo4j_client.query_issue_subgraph( issue_name=issue_name, depth=1 ) else: graph_data = await neo4j_client.query_full_graph(limit=limit) return { "code": 0, "message": "success", "data": graph_data, } # ----------------------------------------------------------------------------- # 检查重复建议(任务3:P2 知识去重) # ----------------------------------------------------------------------------- # GET /api/admin/knowledge-iteration/suggestions/{id}/duplicates @router.get("/suggestions/{suggestion_id}/duplicates") @require_admin async def check_duplicates( suggestion_id: str, current_user: UserInfo = Depends(get_current_user), db: AsyncSession = Depends(get_db), service: KnowledgeIterationService = Depends(dep_knowledge_iteration_service), ): """检查指定建议是否存在重复(利用 Neo4j 图结构 + SQL 文本相似)。 在采纳建议前调用,检测是否有同名 Issue 或相似标题的已有条目。 返回重复项列表供训练师参考。 - **suggestion_id**: 建议ID **需要管理员权限。** """ from sqlalchemy import select stmt = select(KnowledgeSuggestion).where( KnowledgeSuggestion.id == suggestion_id ) result = await db.execute(stmt) suggestion = result.scalar_one_or_none() if not suggestion: return {"code": 404, "message": "建议不存在", "data": None} neo4j_client = await get_neo4j_client() duplicates = await service.find_duplicates( db=db, issue_name=suggestion.issue, title=suggestion.title, suggestion_id=suggestion_id, neo4j_client=neo4j_client, ) return { "code": 0, "message": "success", "data": { "suggestion_id": suggestion_id, "has_duplicates": len(duplicates) > 0, "duplicates": duplicates, }, } # ----------------------------------------------------------------------------- # 合并重复建议(任务3:P2 知识去重) # ----------------------------------------------------------------------------- # POST /api/admin/knowledge-iteration/suggestions/{id}/merge @router.post("/suggestions/{suggestion_id}/merge") @require_admin async def merge_suggestions( suggestion_id: str, body: KnowledgeSuggestionMerge, current_user: UserInfo = Depends(get_current_user), db: AsyncSession = Depends(get_db), service: KnowledgeIterationService = Depends(dep_knowledge_iteration_service), ): """合并重复建议(去重操作)。 将 duplicate_id 的建议合并到当前建议(primary), 标签和元数据合并,重复建议标记为 rejected(合并归入)。 - **suggestion_id**: 主建议ID(保留) - **duplicate_id**: 重复建议ID(将被合并) **需要管理员权限。** """ logger.info( f"管理员 {current_user.name} 合并建议: " f"primary={suggestion_id}, duplicate={body.duplicate_id}" ) merged = await service.merge_suggestions( db=db, primary_id=suggestion_id, duplicate_id=body.duplicate_id, reviewer_id=current_user.employee_id, ) if not merged: return {"code": 404, "message": "主建议不存在", "data": None} return { "code": 0, "message": "建议合并完成,重复建议已标记为已驳回(合并归入)", "data": KnowledgeSuggestionResponse.model_validate(merged), }