Files
Simon bea288e414 feat: 2026-07-11 全量更新 - 代办集成+会议室预定+知识迭代修复+UI统一+Bug修复
== 已部署上线 (9项) ==
- 代办事项真实数据源集成 (企微审批API 8bug修复链)
- H5/坐席端 Logo样式统一+绿色背景
- 视频引导页修复 (localStorage key v2)
- 坐席端 v9 Vue版本修复 (ElMessage._context)
- 截图按钮 v10 修复 (getDisplayMedia user gesture)
- 扫码样式恢复+H5扫码登录跳转修复
- H5截图快捷键提示

== 代码完成待部署 (3项) ==
- 知识迭代3Bug修复 (#8 POST端点/#7 MERGE幂等/#6 过期检查)
- 会议室预定-小鱼易联终端 (40文件, 40/40测试通过)
- IT资产升级审批推送 (asset_service.py)

== 需求文档 (2项) ==
- 坐席端AI辅助消息框-PRD (4项新功能确认)
- 坐席端布局优化建议 v2.0 (7天计划)

== 新增文档 ==
- 日报-2026-07-11.md
- 知识迭代Bug修复报告-20260711.md
- 会议室预定-部署指南.md
- CHANGELOG.md 更新

== 测试 ==
- test_todo_integration.py: 40/40
- test_meetingroom.py: 40/40
- test_bugfix_ki_suggestions.py: 21/21
2026-07-11 23:13:10 +08:00

639 lines
23 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# =============================================================================
# 企微IT智能服务台 — 知识库自动迭代 API(Tier1 扩展)
# =============================================================================
# 说明:知识库自动迭代相关接口(扩展版)。
# 1. POST /api/admin/knowledge-iteration/analyze - 触发分析并生成建议
# 2. GET /api/admin/knowledge-iteration/suggestions - 获取建议列表(支持 audience/confidence 筛选)
# 2.1 POST /api/admin/knowledge-iteration/suggestions - 创建知识建议(手动录入,通道 B)
# 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.enums import SuggestionStatusEnum, GraphSyncStatusEnum
from app.schemas.knowledge_suggestion import (
KnowledgeSuggestionListResponse,
KnowledgeSuggestionResponse,
KnowledgeSuggestionStatsResponse,
KnowledgeSuggestionApprove,
KnowledgeSuggestionReject,
KnowledgeSuggestionRewrite,
KnowledgeSuggestionMerge,
KnowledgeSuggestionCreate,
)
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
],
},
}
# -----------------------------------------------------------------------------
# 创建知识建议(手动录入 — 通道 B)
# -----------------------------------------------------------------------------
# POST /api/admin/knowledge-iteration/suggestions
@router.post("/suggestions")
@require_admin
async def create_suggestion(
body: KnowledgeSuggestionCreate,
current_user: UserInfo = Depends(get_current_user),
db: AsyncSession = Depends(get_db),
):
"""创建知识库优化建议(手动录入,通道 B)。
训练师/管理员手动创建知识建议,状态初始为 pending,等待审核。
- **suggestion_type**: 建议类型(new_faq/update/outdated
- **title**: 建议标题(必填)
- **content**: 答案内容(必填)
- **source_type**: 来源类型(手动录入为 manual)
**需要管理员权限。**
"""
# 字段验证:必填字段不能为空
if not body.title or not body.title.strip():
return {"code": 400, "message": "标题不能为空", "data": None}
if not body.content or not body.content.strip():
return {"code": 400, "message": "内容不能为空", "data": None}
if not body.source_type or not body.source_type.strip():
return {"code": 400, "message": "来源类型不能为空", "data": None}
logger.info(
f"管理员 {current_user.name} 手动创建知识建议: "
f"type={body.suggestion_type}, title={body.title}"
)
# 构建 KnowledgeSuggestion 对象,status 初始为 pending
suggestion = KnowledgeSuggestion(
suggestion_type=body.suggestion_type,
status=SuggestionStatusEnum.pending.value,
title=body.title.strip(),
content=body.content.strip(),
category=body.category or "其他",
tags=body.tags or [],
source_type=body.source_type,
source_data=body.source_data,
reason=body.reason,
confidence=body.confidence,
audience=body.audience.value if body.audience else None,
issue=body.issue,
action=body.action,
relation_type=body.relation_type.value if body.relation_type else None,
parent_issue=body.parent_issue,
graph_meta=body.graph_meta,
graph_sync_status=GraphSyncStatusEnum.pending.value,
source_failed=False,
)
db.add(suggestion)
await db.commit()
await db.refresh(suggestion)
return {
"code": 0,
"message": "知识建议创建成功,等待审核",
"data": KnowledgeSuggestionResponse.model_validate(suggestion),
}
# -----------------------------------------------------------------------------
# 获取建议详情
# -----------------------------------------------------------------------------
# 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),
}
# -----------------------------------------------------------------------------
# 知识图谱可视化(任务2P2
# -----------------------------------------------------------------------------
# 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),
}