Files
wecom_it_smart_desk/docs/02-技术文档/技术架构/增量设计-知识库迭代-开发任务分解-20260712.md
Simon facc04aa65 chore: docs 结构整改 + compose 双目录对齐(合并重建提交)
本提交为 .git 对象库损坏后的重建提交,内容等价于原先三个本地提交
(5e2fd4c2 / 57a53c98 / 5d7e1873)的累积结果,未做任何额外改动。

一、docs 结构整改(整改 #14)
根因:重构时新结构为 untracked 文件,执行 git stash(未带 -u)未纳入,
随后 git reset 拉回 HEAD 旧 tracked 树,导致旧树复活、新旧两棵目录
树并存于 docs/,共 791 文件、双分类体系冲突。

修复动作:
- b2 同名异主题文件改名迁移保全 9 个
- C 类 39 个孤立文件按主题正确归类
- A/B1 类 222 个重复文件删除(新结构已有内容副本)
- 9 个旧独有空目录删除
- 270 处内部引用按 verified 映射改写
- 整改记录 #14 登记于 04-运维文档/部署运维

结果:docs 791 → 569 文件,顶层仅规范 8 类 + 治理文件,单树恢复。
残留:约 20 处指向从未存在文件的陈旧死链,归入独立文档卫生任务。

二、compose 双目录对齐(消除踩坑 A)
- docker-compose.yml:nginx 前端挂载全部由根目录 frontend-*/dist
  改为 src/frontend-*/dist(h5 / agent / admin / terminal)
- docker-compose.dev.yml:dev 服务 build context 与卷同步改 src/
- 效果:本地 docker compose up 不再把根目录 stale dist 挂回,
  与线上一致,分叉隐患消除(已 docker compose config 校验通过)

防复发铁律:
- 重构须提交;仓库修复须 git stash -u 或先 commit
- 新结构须 git add 并提交,避免再次 untracked 复活
- H5 改动只动 src/frontend-h5/,禁改根目录遗留 frontend-*/
2026-08-07 22:31:32 +08:00

58 KiB
Raw Permalink Blame History

增量设计:知识库迭代 — 开发任务分解

文档版本v1.0 创建日期2026-07-12 文档类型:增量系统设计 + 开发任务分解 前置文档

  • 技术方案:docs/02-技术文档/技术架构/增量设计-知识库迭代与痛点缓解-20260711.md
  • 原型图:docs/01-产品文档/07-知识库/知识库迭代-未实现功能原型设计-20260711.md 技术栈FastAPI + SQLAlchemy + PostgreSQL + Redis + Neo4j / Vue3 + Element Plus + Vant4 + ECharts / Dify

目录

  1. 设计概述
  2. 分诊交互 — 增量设计
  3. 拓扑预览 — 增量设计
  4. 代答排除 — 增量设计
  5. 数据库迁移方案
  6. Dify 分诊应用配置规格
  7. 文件清单
  8. 开发任务列表
  9. 模块依赖关系图
  10. 共享知识(Shared Knowledge
  11. 待明确事项

1. 设计概述

1.1 背景与决策

基于产品经理产出的原型图文档与用户确认的 4 项关键决策 + 9 项次要决策,本文档对已有技术方案文档中三个未实现功能模块(分诊交互、拓扑预览、代答排除)进行增量设计补充,并分解为可执行的开发任务。

4 项关键决策

决策项 确认结果 设计影响
Dify 应用 新建独立分诊应用 需产出 Dify 应用配置规格,不复用 Wingman
坐席看板 本期完整实现 原型图有 2 页坐席端看板,需新增 7 个 API 端点
拓扑预览 只读图谱 + 搜索筛选 不实现节点编辑/添加/删除写操作,复用已有 GET /graph API
代答排除 4 种匹配方式全做 关键词 + 正则 + 意图(Dify) + 分类(依赖分诊)

9 项次要决策

# 决策项 确认结果
1 分诊数据存储 新建 triage_sessions
2 VIP 判断 本期不做,后续接入 eHR
3 紧急度判断 关键词规则("紧急/马上/宕机/无法工作"→高)
4 分诊超时 5 秒,超时自动转人工
5 专家模式 展示所有步骤预览
6 拓扑预览 Relation 节点类型和 DERIVED_FROM 关系类型不新增,筛选中置灰
7 排除检查时机 AI 回复前(消息接收到后、调用 Dify 前)
8 导入规则 本期不实现
9 命中后动作 4 种全实现(转人工/转人工+上下文/仅提示/静默转人工)

1.2 三模块范围对比

维度 分诊交互 拓扑预览 代答排除
涉及端 H5 + 坐席端 + Dify 管理后台 管理后台 + H5(消息流)
后端新增 API 11 个 + 服务 2 个 无新增端点 API 8 个 + 服务 1 个 + 匹配器 4 个
前端新增 H5 组件修改 + 坐席页面 1 个 + 组件 3 个 管理后台页面 1 个 + 组件 3 个 管理后台页面 1 个 + 组件 2 个
DB 新增表 triage_sessions exclusion_rules + exclusion_logs
Dify 依赖 新建独立分诊应用 复用审批意图识别 Dify 链路
可并行性 基础,其他依赖它 完全独立可并行 依赖分诊(分类匹配)

2. 分诊交互 — 增量设计

2.1 范围差异说明

已有技术方案文档(§3.1)仅设计了 H5 端 4 个 APIstart/step/skip/transfer),但用户决策"坐席看板本期完整实现",原型图有 2 页坐席端分诊看板。需补充:

  1. 坐席端分诊看板 API7 个新增端点)
  2. Dify 分诊应用配置规格(新建独立应用,不复用 Wingman)
  3. triage_sessions 表结构设计

2.2 H5 端 API 设计(已有方案,补充 complete 端点)

已有技术方案定义了 4 个 H5 端 API,现补充第 5 个:

方法 路径 请求 响应 说明
POST /api/h5/triage/start {conversation_id, question} {triage_id, steps: TriageStep[], total: int, confidence, urgency, suggested_route} 发起分诊
POST /api/h5/triage/step {triage_id, step_index, selected_label} {next_step: TriageStep, collected_context: string[]} 提交步骤选择
POST /api/h5/triage/skip {triage_id, step_index} {next_step: TriageStep} 跳过步骤
POST /api/h5/triage/transfer {triage_id, context: string[]} {conversation_id, status: "waiting_agent"} 转人工
POST /api/h5/triage/complete {triage_id, context: string[]} {reply: string, confidence: float} 分诊完成生成最终回复

设计变更/start 响应新增 triage_id(分诊会话唯一 ID),后续所有操作通过 triage_id 关联,而非 conversation_id

2.3 坐席端分诊看板 API 设计(新增 7 个端点)

路由前缀:/api/agent/triage

方法 路径 请求 响应 说明
GET /api/agent/triage/pending ?urgency=&problem_type=&page=&page_size= {code:0, data:{total, items: TriageSession[]}} 待分诊列表(按紧急度排序)
GET /api/agent/triage/stats {code:0, data:{pending_total, today_triaged, ai_self_count, human_count, auto_approval_count, avg_duration_sec}} 分诊看板统计概要
GET /api/agent/triage/{triage_id} {code:0, data: TriageSessionDetail} 分诊详情(含用户画像、问题描述、AI分析、已收集上下文)
POST /api/agent/triage/{triage_id}/route {route_action: "ai_self"|"human"|"auto_approval"|"skip", route_note?: string} {code:0, data: TriageSession} 坐席路由操作(覆盖 AI 建议)
GET /api/agent/triage/history ?date_from=&date_to=&route_action=&page=&page_size= {code:0, data:{total, items: TriageSession[]}} 已分诊历史列表
GET /api/agent/triage/export ?date_from=&date_to= 文件流 (xlsx) 导出分诊记录
POST /api/agent/triage/{triage_id}/exclude-options {excluded_labels: string[], recommended_label?: string} {code:0, data:{excluded: true}} 坐席排除/推荐分诊选项(WS 推送到 H5)

2.4 triage_sessions 表结构

CREATE TABLE triage_sessions (
    id                  VARCHAR(36)   PRIMARY KEY DEFAULT gen_random_uuid()::text,
    conversation_id     VARCHAR(36)   NOT NULL,
    user_id             VARCHAR(100)  NOT NULL,
    user_name           VARCHAR(100),
    user_dept           VARCHAR(100),
    user_level          VARCHAR(20),
    device_info         VARCHAR(200),
    request_title       VARCHAR(200)  NOT NULL,
    request_content     TEXT          NOT NULL,
    source              VARCHAR(50)   DEFAULT 'wecom_h5',
    -- AI 分诊分析结果
    problem_type        VARCHAR(50),
    problem_category    VARCHAR(100),
    confidence          FLOAT,
    urgency             VARCHAR(20)   DEFAULT 'medium',   -- high/medium/low
    suggested_route     VARCHAR(50),                      -- ai_self/human/auto_approval
    matched_knowledge   VARCHAR(500),
    match_score         FLOAT,
    context_tags        JSON          DEFAULT '[]',
    -- 分诊步骤数据
    triage_steps        JSON          DEFAULT '[]',       -- [{question, options:[{label,probability}]}]
    collected_context   JSON          DEFAULT '[]',       -- ["Outlook 2019", "Windows 10"]
    -- 状态与路由
    status              VARCHAR(30)   DEFAULT 'pending',  -- pending/triaging/routed/skipped/timeout
    route_action        VARCHAR(50),                      -- ai_self/human/auto_approval/skip
    route_note          TEXT,
    operator_id         VARCHAR(100),
    operated_at         TIMESTAMP,
    -- 时间戳
    created_at          TIMESTAMP     DEFAULT NOW(),
    updated_at          TIMESTAMP     DEFAULT NOW()
);

-- 索引
CREATE INDEX idx_triage_status ON triage_sessions(status);
CREATE INDEX idx_triage_urgency ON triage_sessions(urgency);
CREATE INDEX idx_triage_conversation ON triage_sessions(conversation_id);
CREATE INDEX idx_triage_created ON triage_sessions(created_at);
CREATE INDEX idx_triage_user ON triage_sessions(user_id);

2.5 分诊主流程时序图

sequenceDiagram
    participant H5 as H5 员工端
    participant API as FastAPI 后端
    participant TriSvc as TriageService
    participant DifyTri as Dify 分诊应用
    participant DB as PostgreSQL
    participant AG as 坐席端看板
    participant WS as WebSocket

    rect rgb(255, 243, 224)
        Note over H5,DB: 阶段1:发起分诊
        H5->>API: POST /api/h5/triage/start {conversation_id, question}
        API->>TriSvc: start_triage(conversation_id, question)
        TriSvc->>DB: INSERT triage_sessions (status=triaging)
        TriSvc->>DifyTri: 调用分诊 prompt(拆分问题为分步选择题)
        alt Dify 5秒内响应
            DifyTri-->>TriSvc: {steps[], confidence, urgency, suggested_route, problem_type}
            TriSvc->>DB: UPDATE triage_sessions SET triage_steps, confidence, urgency, suggested_route
            TriSvc-->>API: {triage_id, steps, total, confidence, urgency, suggested_route}
            API-->>H5: {triage_id, steps, total, ...}
        else Dify 超时(>5s)
            TriSvc->>DB: UPDATE triage_sessions SET status=timeout
            TriSvc-->>API: 超时,自动转人工
            API-->>H5: 分诊超时,已转人工
        end
    end

    rect rgb(227, 242, 253)
        Note over H5,DB: 阶段2:分步选择 + 坐席协同
        loop 每一步
            H5->>API: POST /api/h5/triage/step {triage_id, step_index, selected_label}
            API->>TriSvc: submit_step(triage_id, step_index, selected_label)
            TriSvc->>DB: 记录 collected_context
            TriSvc->>DifyTri: 根据选择动态调整后续步骤
            DifyTri-->>TriSvc: next_step
            TriSvc-->>API: {next_step, collected_context}
            API-->>H5: {next_step, collected_context}
        end

        Note over AG: 坐席看板实时查看分诊进度
        AG->>API: GET /api/agent/triage/pending
        API-->>AG: 待分诊列表
        AG->>API: GET /api/agent/triage/{triage_id}
        API-->>AG: 分诊详情

        opt 坐席排除选项
            AG->>API: POST /api/agent/triage/{triage_id}/exclude-options {excluded_labels}
            API->>WS: WS 推送 excluded_labels 到 H5
            WS-->>H5: {type: "triage_exclude", excluded_labels}
            H5->>H5: TriageCard.setExcludedOptions(labels)
        end
    end

    rect rgb(232, 245, 233)
        Note over H5,DB: 阶段3:分诊完成 / 转人工
        alt 所有步骤完成
            H5->>API: POST /api/h5/triage/complete {triage_id, context}
            API->>TriSvc: complete_triage(triage_id, context)
            TriSvc->>DifyTri: 根据收集的上下文生成最终回复
            DifyTri-->>TriSvc: {reply, confidence}
            TriSvc->>DB: UPDATE triage_sessions SET status=routed, route_action=ai_self
            TriSvc-->>API: {reply, confidence}
            API-->>H5: AI 回复
        else 转人工
            H5->>API: POST /api/h5/triage/transfer {triage_id, context}
            API->>TriSvc: transfer_to_human(triage_id, context)
            TriSvc->>DB: UPDATE triage_sessions SET status=routed, route_action=human
            TriSvc-->>API: 转人工成功
            API-->>H5: 已转人工
        end
    end

2.6 坐席端看板组件设计

组件 文件路径 说明
分诊看板主页面 frontend-agent/src/views/TriageDashboard.vue 左列表+右详情双栏布局
统计概要栏 frontend-agent/src/components/triage/TriageStatsBar.vue 6 项统计卡片
待分诊列表 frontend-agent/src/components/triage/TriagePendingList.vue 紧急度排序+筛选
分诊详情面板 frontend-agent/src/components/triage/TriageDetailPanel.vue 用户画像+问题描述+AI分析+上下文+路由操作
路由操作栏 内嵌于 TriageDetailPanel 4 个路由按钮 + 备注 + 排除选项

2.7 紧急度判断规则(决策 #3

# 关键词规则,在 TriageService 中实现
URGENCY_HIGH_KEYWORDS = ["紧急", "马上", "宕机", "无法工作", "崩溃", "死机", "蓝屏"]
URGENCY_MEDIUM_KEYWORDS = ["报错", "失败", "连不上", "打不开", "不能用"]

def determine_urgency(question: str, confidence: float) -> str:
    """根据关键词 + 置信度判断紧急度。"""
    if any(kw in question for kw in URGENCY_HIGH_KEYWORDS):
        return "high"
    if confidence < 0.5:
        return "high"  # 低置信也视为紧急
    if any(kw in question for kw in URGENCY_MEDIUM_KEYWORDS):
        return "medium"
    return "low"

2.8 分诊超时处理(决策 #4

# TriageService.start_triage 中
import asyncio

try:
    result = await asyncio.wait_for(
        dify_triage_service.analyze(question, context),
        timeout=5.0  # 5秒超时
    )
except asyncio.TimeoutError:
    # 超时自动转人工
    await self._transfer_to_human_on_timeout(triage_id)
    return {"status": "timeout", "message": "分诊超时,已自动转人工"}

3. 拓扑预览 — 增量设计

3.1 范围确认

维度 确认结果
交互模式 只读 — 不实现节点编辑/添加/删除写操作
后端端点 不新增 — 复用已有 GET /api/admin/knowledge-iteration/graph?issue_name=xxx
节点类型 Issue + ActionRelation 节点类型和 DERIVED_FROM 关系类型不新增,筛选中置灰)
布局 力导向图(ECharts graph),层次树布局预留但不实现
搜索 节点名称模糊搜索 + 高亮定位
筛选 节点类型(Issue/Action+ 关系类型(LEADS_TO/RELATES_TO/CAN_JUMP_TO

3.2 前端组件清单

组件 文件路径 说明
拓扑预览主页面 frontend-admin/src/views/TopologyPreview.vue 页面容器,统计栏+工具栏+画布+详情面板
图谱画布 frontend-admin/src/components/topology/GraphCanvas.vue ECharts 力导向图渲染 + 搜索高亮 + 缩放控制
节点详情面板 frontend-admin/src/components/topology/NodeDetailPanel.vue 节点属性 + 关联关系 + 高亮路径(只读)
工具栏 frontend-admin/src/components/topology/GraphToolbar.vue 搜索框 + 节点类型筛选 + 关系类型筛选 + 缩放控制
API 封装 frontend-admin/src/api/topology.ts 封装 GET /graph 调用

3.3 数据流

sequenceDiagram
    participant Page as TopologyPreview.vue
    participant API as topology.ts
    participant Backend as FastAPI
    participant Neo4j as Neo4jClient

    Page->>API: fetchGraph({issue_name?, limit?})
    API->>Backend: GET /api/admin/knowledge-iteration/graph?issue_name=xxx&limit=200
    Backend->>Neo4j: query_full_graph() 或 query_issue_subgraph()
    Neo4j-->>Backend: {nodes:[], links:[]}
    Backend-->>API: {code:0, data:{nodes, links}}
    API-->>Page: graphData

    Page->>Page: ECharts setOption(graphData)
    Note over Page: 力导向渲染,节点按类型着色

    opt 用户搜索节点
        Page->>Page: 前端过滤 nodes.filter(n => n.name.includes(keyword))
        Page->>Page: 匹配节点高亮脉冲,非匹配变暗
        Page->>Page: 右侧浮层展示搜索结果
    end

    opt 用户点击节点
        Page->>Page: 选中节点高亮 + 路径高亮
        Page->>Page: NodeDetailPanel 显示详情(从已有数据取,不发请求)
    end

    opt 用户筛选节点类型
        Page->>Page: ECharts legend 过滤 Issue/Action
        Note over Page: Relation 类型 checkbox 置灰(disabled
    end

3.4 ECharts 配置要点

// GraphCanvas.vue 核心配置
const chartOption = {
  tooltip: {
    formatter: (params) => `${params.data.name} (${params.data.type})`
  },
  legend: {
    data: [
      { name: 'Issue', itemStyle: { color: '#07C160' } },
      { name: 'Action', itemStyle: { color: '#f59e0b' } },
      { name: 'Relation', itemStyle: { color: '#9ca3af' }, itemStyle: { opacity: 0.3 } }  // 置灰
    ]
  },
  series: [{
    type: 'graph',
    layout: 'force',
    roam: true,                    // 支持缩放平移
    focusNodeAdjacency: true,      // 悬停高亮相邻
    force: {
      repulsion: 300,
      gravity: 0.1,
      edgeLength: [100, 250]
    },
    categories: [
      { name: 'Issue' },           // 绿色 #07C160, symbolSize=28
      { name: 'Action' },          // 橙色 #f59e0b, symbolSize=22
    ],
    data: nodes.map(n => ({
      id: n.id,
      name: n.name,
      category: n.type === 'issue' ? 0 : 1,
      symbolSize: n.type === 'issue' ? 28 : 22,
      itemStyle: { color: n.type === 'issue' ? '#07C160' : '#f59e0b' }
    })),
    links: links.map(l => ({
      source: l.source,
      target: l.target,
      lineStyle: {
        color: '#c0c4cc',
        width: Math.max(0.5, (l.weight || 1) * 1.5),
        curveness: 0.3
      }
    }))
  }]
}

3.5 搜索高亮实现

搜索在前端完成(不发后端请求),对已加载的节点数据做名称模糊匹配:

function highlightSearch(keyword: string) {
  if (!keyword) {
    // 恢复所有节点正常样式
    restoreAllNodes()
    return
  }
  const matched = nodes.filter(n => n.name.includes(keyword))
  // 匹配节点:蓝色描边 + 脉冲动画
  // 非匹配节点:降低透明度
  chart.setOption({
    series: [{
      data: nodes.map(n => ({
        ...n,
        itemStyle: matched.includes(n)
          ? { borderColor: '#3b82f6', borderWidth: 3, shadowBlur: 10, shadowColor: '#3b82f6' }
          : { opacity: 0.15 }
      }))
    }]
  })
  // 右侧浮层展示搜索结果
  searchResults.value = matched
}

4. 代答排除 — 增量设计

4.1 架构设计:策略模式 + 责任链

代答排除采用 策略模式 定义 4 种匹配器,责任链模式 按优先级依次检查规则。

graph TB
    subgraph "代答排除匹配引擎"
        Engine[ExclusionService<br/>匹配引擎入口]
        Chain[ExclusionChain<br/>责任链调度]
        
        subgraph "策略模式 - 4种匹配器"
            M1[KeywordMatcher<br/>关键词匹配]
            M2[RegexMatcher<br/>正则匹配]
            M3[IntentMatcher<br/>意图匹配]
            M4[CategoryMatcher<br/>分类排除]
        end
        
        Engine --> Chain
        Chain --> M1
        Chain --> M2
        Chain --> M3
        Chain --> M4
    end
    
    subgraph "外部依赖"
        Dify[Dify 意图识别<br/>复用审批意图链路]
        TriageDB[triage_sessions<br/>分诊结果]
        RuleDB[(exclusion_rules)]
        LogDB[(exclusion_logs)]
    end
    
    M3 -->|调用| Dify
    M4 -->|查询| TriageDB
    Engine -->|读取规则| RuleDB
    Engine -->|记录命中| LogDB
    
    style M1 fill:#dbeafe,stroke:#3b82f6
    style M2 fill:#fce7f3,stroke:#ec4899
    style M3 fill:#ede9fe,stroke:#8b5cf6
    style M4 fill:#dcfce7,stroke:#07C160

4.2 匹配器接口设计

# backend/app/services/matchers/base.py
from abc import ABC, abstractmethod
from dataclasses import dataclass
from typing import Optional

@dataclass
class MatchResult:
    """匹配结果。"""
    matched: bool
    matched_detail: str = ""          # 命中的关键词/正则/意图/分类
    match_position: str = ""          # 匹配位置(用于测试展示)

class BaseMatcher(ABC):
    """匹配器基类 — 策略模式接口。"""
    
    @abstractmethod
    async def match(self, message: str, condition: str, context: dict = None) -> MatchResult:
        """检查消息是否匹配规则条件。
        
        Args:
            message: 用户消息文本
            condition: 匹配条件(关键词列表/正则表达式/意图ID列表/分类名称列表)
            context: 上下文(含 conversation_id, triage_result 等)
        
        Returns:
            MatchResult
        """
        ...

4.3 四种匹配器实现要点

匹配器 文件 匹配逻辑 外部依赖
KeywordMatcher matchers/keyword_matcher.py 逗号分隔关键词列表,消息包含任一即命中
RegexMatcher matchers/regex_matcher.py Python re.search(pattern, message),正则编译缓存
IntentMatcher matchers/intent_matcher.py 调用 Dify 意图识别 API,检查返回意图是否在排除列表中 Dify(复用审批意图链路 approval_dify_base_url
CategoryMatcher matchers/category_matcher.py 查询 triage_sessions 表获取分诊结果的 problem_category,检查是否在排除分类列表中 triage_sessions 表(依赖分诊模块)

4.4 意图匹配与 Dify 对接方案

复用现有审批意图识别 Dify 链路,不新建 Dify 应用:

# backend/app/services/matchers/intent_matcher.py
from app.config import settings
import httpx

class IntentMatcher(BaseMatcher):
    async def match(self, message: str, condition: str, context: dict = None) -> MatchResult:
        # 1. 调用 Dify 意图识别(复用 approval_dify_base_url
        intent = await self._recognize_intent(message)
        
        # 2. 检查意图是否在排除列表中
        excluded_intents = [s.strip() for s in condition.split(",")]
        if intent in excluded_intents:
            return MatchResult(matched=True, matched_detail=f"意图: {intent}")
        return MatchResult(matched=False)
    
    async def _recognize_intent(self, message: str) -> str:
        """调用 Dify 意图识别 API。"""
        async with httpx.AsyncClient(timeout=settings.approval_dify_timeout) as client:
            resp = await client.post(
                f"{settings.approval_dify_base_url}/v1/chat/completions",
                headers={"Authorization": f"Bearer {settings.approval_dify_api_key}"},
                json={
                    "model": "intent-recognition",
                    "messages": [
                        {"role": "system", "content": "识别用户消息的意图类别,输出意图ID。"},
                        {"role": "user", "content": message}
                    ],
                    "temperature": 0.1
                }
            )
            return resp.json()["choices"][0]["message"]["content"].strip()

4.5 分类排除与分诊的依赖关系

CategoryMatcher 依赖分诊结果:

# backend/app/services/matchers/category_matcher.py
class CategoryMatcher(BaseMatcher):
    async def match(self, message: str, condition: str, context: dict = None) -> MatchResult:
        conversation_id = context.get("conversation_id") if context else None
        if not conversation_id:
            return MatchResult(matched=False)  # 无会话上下文,跳过分类匹配
        
        # 查询 triage_sessions 获取分诊结果
        from app.models.triage_session import TriageSession
        result = await db.execute(
            select(TriageSession).where(
                TriageSession.conversation_id == conversation_id
            ).order_by(TriageSession.created_at.desc()).limit(1)
        )
        triage = result.scalar_one_or_none()
        if not triage or not triage.problem_category:
            return MatchResult(matched=False)  # 无分诊结果,跳过
        
        excluded_categories = [s.strip() for s in condition.split(",")]
        if triage.problem_category in excluded_categories:
            return MatchResult(
                matched=True,
                matched_detail=f"分类: {triage.problem_category}"
            )
        return MatchResult(matched=False)

设计说明:分类排除是软依赖——如果分诊模块尚未运行或无分诊结果,CategoryMatcher 返回未命中,不影响其他匹配器执行。

4.6 排除检查时机与消息流集成(决策 #7)

排除检查发生在 AI 回复前(消息接收到后、调用 Dify 前),嵌入 AIHandler 处理链:

flowchart TD
    A[用户发送消息] --> B[AIHandler.handle_message]
    B --> C{打招呼/呼叫人工?}
    C -->|是| D[引导话术]
    C -->|否| E[代答排除检查]
    
    E --> F[ExclusionService.check_exclusions]
    F --> G{命中排除规则?}
    G -->|是| H[执行命中后动作]
    G -->|否| I[正常 AI 回复流程]
    
    H --> J{action_type}
    J -->|transfer_human| K[转人工坐席]
    J -->|transfer_human_with_context| L[携带上下文转人工]
    J -->|prompt_transfer| M[展示提示语 等待确认]
    J -->|silent_transfer| N[静默转人工]
    
    K --> O[记录 exclusion_logs]
    L --> O
    M --> O
    N --> O
    O --> P[更新 exclusion_rules.hit_count]
    
    I --> Q[调用 Dify AI 生成回复]
    Q --> R[返回 AI 回复]

4.7 命中后动作实现(决策 #9 — 4 种全实现)

动作类型 标识 实现方式 员工体验
转人工坐席 transfer_human 会话状态→waiting_agent,推送给坐席队列 看到"已转接人工"提示
转人工+上下文 transfer_human_with_context 同上 + 附带 collected_context 快照 看到"已转接人工"提示
仅提示转人工 prompt_transfer 返回 transfer_message,等待用户确认 看到提示语,可选择确认转人工
静默转人工 silent_transfer 会话状态→waiting_agent,不提示用户 无感知,直接进入人工队列

4.8 DB 表设计

exclusion_rules 表

CREATE TABLE exclusion_rules (
    id                  VARCHAR(36)   PRIMARY KEY DEFAULT gen_random_uuid()::text,
    rule_name           VARCHAR(200)  NOT NULL UNIQUE,
    rule_description    TEXT,
    priority            VARCHAR(5)    NOT NULL DEFAULT 'P2',   -- P0/P1/P2/P3
    match_type          VARCHAR(20)   NOT NULL,                 -- keyword/regex/intent/category
    match_condition     TEXT          NOT NULL,                  -- 关键词列表/正则/意图ID列表/分类名称列表
    match_scope         JSON          NOT NULL DEFAULT '["ai_auto_reply"]',
    action_type         VARCHAR(50)   NOT NULL DEFAULT 'transfer_human',
    transfer_message    TEXT,
    status              VARCHAR(10)   NOT NULL DEFAULT 'enabled', -- enabled/disabled
    hit_count           INTEGER       DEFAULT 0,
    created_by          VARCHAR(100)  NOT NULL,
    created_at          TIMESTAMP     DEFAULT NOW(),
    updated_at          TIMESTAMP     DEFAULT NOW()
);

CREATE INDEX idx_exclusion_rules_status ON exclusion_rules(status);
CREATE INDEX idx_exclusion_rules_priority ON exclusion_rules(priority);
CREATE INDEX idx_exclusion_rules_match_type ON exclusion_rules(match_type);

exclusion_logs 表

CREATE TABLE exclusion_logs (
    id                  VARCHAR(36)   PRIMARY KEY DEFAULT gen_random_uuid()::text,
    rule_id             VARCHAR(36)   NOT NULL,
    rule_name           VARCHAR(200),
    conversation_id     VARCHAR(36),
    user_id             VARCHAR(100),
    message_content     TEXT,
    match_type          VARCHAR(20),
    matched_detail      TEXT,
    action_type         VARCHAR(50),
    action_result       VARCHAR(50)   DEFAULT 'success',  -- success/failed
    created_at          TIMESTAMP     DEFAULT NOW()
);

CREATE INDEX idx_exclusion_logs_rule ON exclusion_logs(rule_id);
CREATE INDEX idx_exclusion_logs_created ON exclusion_logs(created_at);
CREATE INDEX idx_exclusion_logs_conversation ON exclusion_logs(conversation_id);

4.9 管理后台 API 设计

路由前缀:/api/admin/exclusion-rules

方法 路径 请求 响应 说明
GET /api/admin/exclusion-rules ?status=&match_type=&priority=&keyword=&page=&page_size= {code:0, data:{total, items}} 规则列表(分页+筛选)
POST /api/admin/exclusion-rules ExclusionRuleCreate {code:0, data: ExclusionRule} 新建规则
GET /api/admin/exclusion-rules/{id} {code:0, data: ExclusionRule} 规则详情
PUT /api/admin/exclusion-rules/{id} ExclusionRuleUpdate {code:0, data: ExclusionRule} 编辑规则
DELETE /api/admin/exclusion-rules/{id} {code:0, message:"删除成功"} 删除规则
POST /api/admin/exclusion-rules/{id}/toggle {status: "enabled"|"disabled"} {code:0, data: ExclusionRule} 启用/停用
POST /api/admin/exclusion-rules/test {message: string, rule_id?: string} {code:0, data:{matched, matched_detail, rule_name, action_type}} 测试匹配
GET /api/admin/exclusion-rules/stats {code:0, data:{enabled_count, disabled_count, monthly_hits, monthly_transfers}} 统计概要

5. 数据库迁移方案

5.1 迁移文件

新增迁移版本:045_triage_exclusion_tables.py

"""triage_sessions + exclusion_rules + exclusion_logs

Revision ID: 045_triage_exclusion
Revises: 044_automation
Create Date: 2026-07-12
"""
from alembic import op
import sqlalchemy as sa

revision = "045_triage_exclusion"
down_revision = "044_automation"

def upgrade():
    # 1. triage_sessions 表
    op.create_table(
        "triage_sessions",
        sa.Column("id", sa.String(36), primary_key=True),
        sa.Column("conversation_id", sa.String(36), nullable=False),
        sa.Column("user_id", sa.String(100), nullable=False),
        sa.Column("user_name", sa.String(100)),
        sa.Column("user_dept", sa.String(100)),
        sa.Column("user_level", sa.String(20)),
        sa.Column("device_info", sa.String(200)),
        sa.Column("request_title", sa.String(200), nullable=False),
        sa.Column("request_content", sa.Text, nullable=False),
        sa.Column("source", sa.String(50), server_default="wecom_h5"),
        sa.Column("problem_type", sa.String(50)),
        sa.Column("problem_category", sa.String(100)),
        sa.Column("confidence", sa.Float),
        sa.Column("urgency", sa.String(20), server_default="medium"),
        sa.Column("suggested_route", sa.String(50)),
        sa.Column("matched_knowledge", sa.String(500)),
        sa.Column("match_score", sa.Float),
        sa.Column("context_tags", sa.JSON, server_default="[]"),
        sa.Column("triage_steps", sa.JSON, server_default="[]"),
        sa.Column("collected_context", sa.JSON, server_default="[]"),
        sa.Column("status", sa.String(30), server_default="pending"),
        sa.Column("route_action", sa.String(50)),
        sa.Column("route_note", sa.Text),
        sa.Column("operator_id", sa.String(100)),
        sa.Column("operated_at", sa.DateTime),
        sa.Column("created_at", sa.DateTime, server_default=sa.func.now()),
        sa.Column("updated_at", sa.DateTime, server_default=sa.func.now()),
    )
    op.create_index("idx_triage_status", "triage_sessions", ["status"])
    op.create_index("idx_triage_urgency", "triage_sessions", ["urgency"])
    op.create_index("idx_triage_conversation", "triage_sessions", ["conversation_id"])
    op.create_index("idx_triage_created", "triage_sessions", ["created_at"])
    op.create_index("idx_triage_user", "triage_sessions", ["user_id"])

    # 2. exclusion_rules 表
    op.create_table(
        "exclusion_rules",
        sa.Column("id", sa.String(36), primary_key=True),
        sa.Column("rule_name", sa.String(200), nullable=False, unique=True),
        sa.Column("rule_description", sa.Text),
        sa.Column("priority", sa.String(5), nullable=False, server_default="P2"),
        sa.Column("match_type", sa.String(20), nullable=False),
        sa.Column("match_condition", sa.Text, nullable=False),
        sa.Column("match_scope", sa.JSON, nullable=False, server_default='["ai_auto_reply"]'),
        sa.Column("action_type", sa.String(50), nullable=False, server_default="transfer_human"),
        sa.Column("transfer_message", sa.Text),
        sa.Column("status", sa.String(10), nullable=False, server_default="enabled"),
        sa.Column("hit_count", sa.Integer, server_default="0"),
        sa.Column("created_by", sa.String(100), nullable=False),
        sa.Column("created_at", sa.DateTime, server_default=sa.func.now()),
        sa.Column("updated_at", sa.DateTime, server_default=sa.func.now()),
    )
    op.create_index("idx_exclusion_rules_status", "exclusion_rules", ["status"])
    op.create_index("idx_exclusion_rules_priority", "exclusion_rules", ["priority"])
    op.create_index("idx_exclusion_rules_match_type", "exclusion_rules", ["match_type"])

    # 3. exclusion_logs 表
    op.create_table(
        "exclusion_logs",
        sa.Column("id", sa.String(36), primary_key=True),
        sa.Column("rule_id", sa.String(36), nullable=False),
        sa.Column("rule_name", sa.String(200)),
        sa.Column("conversation_id", sa.String(36)),
        sa.Column("user_id", sa.String(100)),
        sa.Column("message_content", sa.Text),
        sa.Column("match_type", sa.String(20)),
        sa.Column("matched_detail", sa.Text),
        sa.Column("action_type", sa.String(50)),
        sa.Column("action_result", sa.String(50), server_default="success"),
        sa.Column("created_at", sa.DateTime, server_default=sa.func.now()),
    )
    op.create_index("idx_exclusion_logs_rule", "exclusion_logs", ["rule_id"])
    op.create_index("idx_exclusion_logs_created", "exclusion_logs", ["created_at"])
    op.create_index("idx_exclusion_logs_conversation", "exclusion_logs", ["conversation_id"])

def downgrade():
    op.drop_table("exclusion_logs")
    op.drop_table("exclusion_rules")
    op.drop_table("triage_sessions")

5.2 迁移顺序

044_automation (已有)
    ↓
045_triage_exclusion (本次新增 — 3 张表一次性创建)

三张表在一个迁移中创建,因为 exclusion_logs 外键引用 exclusion_rulestriage_sessions 被 exclusion 的 CategoryMatcher 引用,三者紧密关联。


6. Dify 分诊应用配置规格

6.1 应用基本信息

配置项
应用名称 IT-智能分诊引擎
应用类型 聊天助手 (Chat Assistant)
模型 与 Wingman 相同的 LLM(如 Qwen2.5-72B
温度 0.3(偏确定性输出)
最大 token 2000
超时 5 秒(后端 asyncio.wait_for 控制)

6.2 输入参数 Schema

{
  "conversation_id": "string — 会话ID",
  "question": "string — 员工问题文本(必填)",
  "collected_context": "array — 已收集的上下文标签(分步选择中累积)",
  "excluded_options": "array — 坐席已排除的选项标签(避免AI后续推荐)",
  "step_index": "int — 当前步骤序号(0=首次分诊)"
}

6.3 输出格式(JSON

{
  "triage_type": "confirm|transfer|approval",
  "confidence": 0.85,
  "urgency": "high|medium|low",
  "problem_type": "硬件|软件|网络|安全|账号|其他",
  "problem_category": "Outlook",
  "suggested_route": "ai_self|human|auto_approval",
  "matched_knowledge": "Outlook登录失败 → 密码过期检查",
  "match_score": 0.89,
  "context_tags": ["Outlook 2019", "Windows 10", "AD域用户"],
  "triage_steps": [
    {
      "question": "Outlook是否弹出了具体的错误提示?",
      "options": [
        {"label": "弹出了,提示无法连接到服务器", "probability": 0.68},
        {"label": "弹出了,提示密码错误", "probability": 0.22},
        {"label": "没有弹出任何提示,直接闪退", "probability": 0.08},
        {"label": "其他情况", "probability": 0.02}
      ]
    }
  ],
  "total_steps": 3,
  "reply": "AI回复文本(当triage_type=confirm时,引导员工选择)"
}

6.4 Prompt 设计要点

要点 说明
角色定义 你是IT服务台智能分诊引擎,负责分析员工IT问题并拆分为分步选择题
任务目标 1. 识别问题类型和分类 2. 评估置信度和紧急度 3. 拆分复杂问题为分步选择题 4. 推荐路由渠道
复杂度自适应 简单问题1-2步,复杂问题3-5步,每步最多4个选项
概率分配 每个选项分配概率(0-1),所有选项概率之和为1,最高概率项可标记推荐
紧急度规则 消息含"紧急/马上/宕机/无法工作/崩溃/死机/蓝屏"→high;含"报错/失败/连不上"→medium;其余→low
排除选项 excluded_options 中的选项不出现在后续步骤中
输出约束 必须输出合法JSON,不要输出解释性文字
置信度评估 基于知识库匹配度、问题清晰度、上下文完整度综合评估

6.5 后端对接方式

配置项 环境变量 默认值 说明
API URL DIFY_TRIAGE_API_URL "" Dify OpenAI 兼容接口地址(如 http://yw-dify/dc.servyou-it.com/dify2openai
API Key DIFY_TRIAGE_API_KEY "" 格式:base_url|app_id|app_name
超时 DIFY_TRIAGE_TIMEOUT 5 秒,超时自动转人工
# backend/app/config.py 新增配置项
# ----------------------------------------------------------------------
# AI 分诊服务配置(Dify Agent 3 — 独立分诊应用)
# ----------------------------------------------------------------------
dify_triage_api_url: str = ""
dify_triage_api_key: str = ""
dify_triage_timeout: int = 5
# backend/app/services/dify_triage_service.py 对接方式
class DifyTriageService:
    async def analyze(self, question: str, context: list = None, 
                      excluded_options: list = None, step_index: int = 0) -> dict:
        """调用 Dify 分诊应用。"""
        async with httpx.AsyncClient(timeout=settings.dify_triage_timeout) as client:
            resp = await client.post(
                f"{settings.dify_triage_api_url}/v1/chat/completions",
                headers={"Authorization": f"Bearer {settings.dify_triage_api_key}"},
                json={
                    "model": "triage-engine",
                    "messages": [
                        {"role": "system", "content": TRIAGE_SYSTEM_PROMPT},
                        {"role": "user", "content": json.dumps({
                            "question": question,
                            "collected_context": context or [],
                            "excluded_options": excluded_options or [],
                            "step_index": step_index
                        })}
                    ],
                    "temperature": 0.3
                }
            )
            content = resp.json()["choices"][0]["message"]["content"]
            return json.loads(content)  # 解析 JSON 输出

7. 文件清单

7.1 后端文件

# 文件路径 操作 模块 说明
B01 backend/migrations/versions/045_triage_exclusion_tables.py [新建] DB 迁移:3 张新表
B02 backend/app/config.py [修改] 配置 新增 dify_triage_* 配置项
B03 backend/app/models/triage_session.py [新建] 分诊 TriageSession ORM 模型
B04 backend/app/models/exclusion_rule.py [新建] 排除 ExclusionRule ORM 模型
B05 backend/app/models/exclusion_log.py [新建] 排除 ExclusionLog ORM 模型
B06 backend/app/schemas/triage.py [新建] 分诊 请求/响应 Schema(H5端+坐席端)
B07 backend/app/schemas/exclusion.py [新建] 排除 请求/响应 Schema
B08 backend/app/api/triage.py [新建] 分诊 分诊 API(H5端 5 个 + 坐席端 7 个)
B09 backend/app/api/exclusion_rules.py [新建] 排除 代答排除管理 API8 个端点)
B10 backend/app/services/triage_service.py [新建] 分诊 分诊业务逻辑服务
B11 backend/app/services/dify_triage_service.py [新建] 分诊 Dify 分诊应用对接
B12 backend/app/services/exclusion_service.py [新建] 排除 代答排除匹配引擎
B13 backend/app/services/matchers/__init__.py [新建] 排除 匹配器包初始化
B14 backend/app/services/matchers/base.py [新建] 排除 匹配器基类 + MatchResult
B15 backend/app/services/matchers/keyword_matcher.py [新建] 排除 关键词匹配器
B16 backend/app/services/matchers/regex_matcher.py [新建] 排除 正则匹配器
B17 backend/app/services/matchers/intent_matcher.py [新建] 排除 意图匹配器(Dify
B18 backend/app/services/matchers/category_matcher.py [新建] 排除 分类匹配器(依赖分诊)
B19 backend/app/api/router.py [修改] 路由 注册 triage + exclusion_rules 路由
B20 backend/app/services/ai_handler.py [修改] 排除 消息流集成排除检查(AI回复前)
B21 backend/app/models/__init__.py [修改] DB 导出新模型

7.2 前端文件

# 文件路径 操作 模块 说明
F01 frontend-h5/src/api/triage.ts [新建] 分诊-H5 H5 端分诊 API 封装
F02 frontend-h5/src/components/TriageCard.vue [修改] 分诊-H5 对接后端 API(已有组件 530 行)
F03 frontend-h5/src/composables/useTriage.ts [新建] 分诊-H5 分诊状态管理 composable
F04 frontend-agent/src/views/TriageDashboard.vue [新建] 分诊-坐席 分诊看板主页面
F05 frontend-agent/src/api/triage.ts [新建] 分诊-坐席 坐席端分诊 API 封装
F06 frontend-agent/src/components/triage/TriageStatsBar.vue [新建] 分诊-坐席 统计概要栏
F07 frontend-agent/src/components/triage/TriagePendingList.vue [新建] 分诊-坐席 待分诊列表
F08 frontend-agent/src/components/triage/TriageDetailPanel.vue [新建] 分诊-坐席 分诊详情+路由操作
F09 frontend-agent/src/router/index.ts [修改] 分诊-坐席 新增分诊看板路由
F10 frontend-admin/src/views/TopologyPreview.vue [新建] 拓扑 拓扑预览主页面
F11 frontend-admin/src/components/topology/GraphCanvas.vue [新建] 拓扑 ECharts 力导向图画布
F12 frontend-admin/src/components/topology/NodeDetailPanel.vue [新建] 拓扑 节点详情面板(只读)
F13 frontend-admin/src/components/topology/GraphToolbar.vue [新建] 拓扑 搜索+筛选+缩放工具栏
F14 frontend-admin/src/api/topology.ts [新建] 拓扑 封装 GET /graph 调用
F15 frontend-admin/src/router/index.ts [修改] 拓扑 新增拓扑预览路由
F16 frontend-admin/src/components/Sidebar.vue [修改] 拓扑 侧边栏新增"拓扑预览"菜单项
F17 frontend-admin/src/views/ExclusionRules.vue [新建] 排除 代答排除规则列表页
F18 frontend-admin/src/components/exclusion/ExclusionRuleForm.vue [新建] 排除 新建/编辑规则弹窗
F19 frontend-admin/src/components/exclusion/ExclusionTestDialog.vue [新建] 排除 测试匹配弹窗
F20 frontend-admin/src/api/exclusion.ts [新建] 排除 代答排除 API 封装
F21 frontend-admin/src/router/index.ts [修改] 排除 新增代答排除路由

7.3 Dify / 配置文件

# 文件路径 操作 模块 说明
D01 dify/triage-app-prompt.md [新建] Dify 分诊应用 Prompt 完整文本
D02 .env.example [修改] 配置 新增 DIFY_TRIAGE_* 环境变量示例

7.4 文件统计

类别 新建 修改 合计
后端 16 5 21
前端 16 5 21
Dify/配置 1 1 2
合计 33 11 44

8. 开发任务列表

T01:项目基础设施(数据库迁移 + 配置 + 数据模型 + 路由注册)

维度 内容
任务 ID T01
任务名称 项目基础设施:数据库迁移 + 配置 + 数据模型 + 路由注册
涉及文件 backend/migrations/versions/045_triage_exclusion_tables.py [新建], backend/app/config.py [修改], backend/app/models/triage_session.py [新建], backend/app/models/exclusion_rule.py [新建], backend/app/models/exclusion_log.py [新建], backend/app/models/__init__.py [修改], backend/app/schemas/triage.py [新建], backend/app/schemas/exclusion.py [新建], backend/app/api/router.py [修改], .env.example [修改]
依赖
优先级 P0
预估工作量 M
关键实现要点 1. 创建 3 张新表的 Alembic 迁移(triage_sessions + exclusion_rules + exclusion_logs
2. config.py 新增 dify_triage_api_urldify_triage_api_keydify_triage_timeout 三个配置项
3. 定义 3 个 ORM 模型,字段与 DDL 对齐
4. 定义 Pydantic Schema(请求/响应模型),含 TriageStep、TriageOption、ExclusionRuleCreate/Update 等
5. router.py 注册 triage_routerprefix 无,内部定义)和 exclusion_rules_routerprefix /admin/exclusion-rules
6. .env.example 补充环境变量示例

T02:分诊交互模块(后端 API + 服务 + Dify 分诊 + H5 前端 + 坐席端前端)

维度 内容
任务 ID T02
任务名称 分诊交互模块:H5 端分诊 API + 坐席端看板 API + Dify 分诊服务 + H5/坐席前端
涉及文件 backend/app/api/triage.py [新建], backend/app/services/triage_service.py [新建], backend/app/services/dify_triage_service.py [新建], frontend-h5/src/api/triage.ts [新建], frontend-h5/src/components/TriageCard.vue [修改], frontend-h5/src/composables/useTriage.ts [新建], frontend-agent/src/views/TriageDashboard.vue [新建], frontend-agent/src/api/triage.ts [新建], frontend-agent/src/components/triage/TriageStatsBar.vue [新建], frontend-agent/src/components/triage/TriagePendingList.vue [新建], frontend-agent/src/components/triage/TriageDetailPanel.vue [新建], frontend-agent/src/router/index.ts [修改], dify/triage-app-prompt.md [新建]
依赖 T01
优先级 P0
预估工作量 L
关键实现要点 1. TriageService:实现 start_triage(含 5 秒超时自动转人工)、submit_step、skip_step、complete_triage、transfer_to_human、determine_urgency关键词规则)、坐席端 list_pending/get_detail/route/override
2. DifyTriageService:对接 Dify OpenAI 兼容接口,解析 JSON 输出,降级处理(Dify 不可用时返回友好错误)
3. triage.py APIH5 端 5 个端点(start/step/skip/transfer/complete+ 坐席端 7 个端点(pending/stats/detail/route/history/export/exclude-options
4. H5 TriageCard.vue:已有 530 行组件,修改为对接后端 API,通过 useTriage composable 管理状态
5. 坐席端 TriageDashboard.vue:左列表+右详情双栏布局,复用原型图设计
6. WS 推送:坐席排除选项后通过 WS 推送到 H5 端,H5 调用 TriageCard.setExcludedOptions()
7. Dify Prompt:按 §6.4 要点编写完整 prompt,保存到 dify/triage-app-prompt.md

T03:拓扑预览模块(纯前端页面 + 组件,复用已有 API)

维度 内容
任务 ID T03
任务名称 拓扑预览模块:管理后台只读图谱页面 + 搜索筛选 + 详情面板
涉及文件 frontend-admin/src/views/TopologyPreview.vue [新建], frontend-admin/src/components/topology/GraphCanvas.vue [新建], frontend-admin/src/components/topology/NodeDetailPanel.vue [新建], frontend-admin/src/components/topology/GraphToolbar.vue [新建], frontend-admin/src/api/topology.ts [新建], frontend-admin/src/router/index.ts [修改], frontend-admin/src/components/Sidebar.vue [修改]
依赖 T01(仅需确认路由注册格式,无 DB 依赖)
优先级 P1
预估工作量 M
关键实现要点 1. 复用已有 APIGET /api/admin/knowledge-iteration/graph?issue_name=xxx&limit=200,不新增后端端点
2. GraphCanvas.vueECharts graph 类型力导向布局,Issue 绿色 #07C160 / Action 橙色 #f59e0broam=true 支持缩放平移,focusNodeAdjacency 悬停高亮
3. 搜索高亮:前端过滤节点名称,匹配节点蓝色描边+脉冲,非匹配降低透明度,右侧浮层展示结果
4. 筛选器:节点类型 checkboxIssue/Action),Relation 类型置灰 disabled;关系类型下拉(LEADS_TO/RELATES_TO/CAN_JUMP_TO),DERIVED_FROM 置灰
5. NodeDetailPanel.vue:只读展示节点属性+关联关系+高亮路径,不显示编辑/添加/删除按钮
6. GraphToolbar.vue:搜索框+筛选+缩放控制(放大/缩小/重置)
7. 降级处理:Neo4j 不可用时 API 返回空数组,前端展示空状态引导
8. Sidebar.vue:新增"拓扑预览"菜单项,图标 🔮

T04:代答排除模块(后端匹配引擎 + API + 管理后台前端 + 消息流集成)

维度 内容
任务 ID T04
任务名称 代答排除模块:4 种匹配引擎 + 管理 API + 管理后台前端 + 消息流集成
涉及文件 backend/app/api/exclusion_rules.py [新建], backend/app/services/exclusion_service.py [新建], backend/app/services/matchers/__init__.py [新建], backend/app/services/matchers/base.py [新建], backend/app/services/matchers/keyword_matcher.py [新建], backend/app/services/matchers/regex_matcher.py [新建], backend/app/services/matchers/intent_matcher.py [新建], backend/app/services/matchers/category_matcher.py [新建], backend/app/services/ai_handler.py [修改], frontend-admin/src/views/ExclusionRules.vue [新建], frontend-admin/src/components/exclusion/ExclusionRuleForm.vue [新建], frontend-admin/src/components/exclusion/ExclusionTestDialog.vue [新建], frontend-admin/src/api/exclusion.ts [新建], frontend-admin/src/router/index.ts [修改]
依赖 T01DB 表 + 模型 + Schema),T02CategoryMatcher 软依赖 triage_sessions 数据)
优先级 P0
预估工作量 L
关键实现要点 1. 策略模式BaseMatcher 抽象基类 + 4 种实现(KeywordMatcher/RegexMatcher/IntentMatcher/CategoryMatcher
2. 责任链调度ExclusionService 按优先级排序规则,依次调用对应 Matcher,命中即停止并记录日志
3. IntentMatcher:复用 approval_dify_base_url + approval_dify_api_key 调用 Dify 意图识别
4. CategoryMatcher:查询 triage_sessions 获取分诊结果的 problem_category,软依赖(无分诊结果时跳过)
5. 消息流集成:修改 ai_handler.py,在 AI 回复前插入排除检查,命中后执行 4 种动作(transfer_human/transfer_human_with_context/prompt_transfer/silent_transfer
6. 管理 API8 个端点(CRUD + toggle + test + stats
7. 前端:规则列表页(表格+筛选+分页+批量操作)+ 新建/编辑弹窗(匹配方式切换表单)+ 测试匹配弹窗
8. 命中日志:每次命中记录 exclusion_logs,更新 hit_count

任务汇总表

任务 ID 任务名称 文件数 依赖 优先级 工作量
T01 项目基础设施 10 P0 M
T02 分诊交互模块 13 T01 P0 L
T03 拓扑预览模块 7 T01 P1 M
T04 代答排除模块 14 T01, T02(软) P0 L

9. 模块依赖关系图

graph TB
    subgraph "T01: 项目基础设施"
        DB[数据库迁移<br/>3张新表]
        CFG[config.py<br/>新增配置项]
        MODEL[ORM 模型<br/>3个]
        SCHEMA[Pydantic Schema<br/>2个文件]
        ROUTER[router.py<br/>路由注册]
    end

    subgraph "T02: 分诊交互模块"
        TRI_API[分诊 API<br/>12个端点]
        TRI_SVC[TriageService<br/>+ DifyTriageService]
        H5_FE[H5 前端<br/>TriageCard + useTriage]
        AG_FE[坐席端前端<br/>TriageDashboard + 3组件]
        DIFY_APP[Dify 分诊应用<br/>Prompt + 配置]
    end

    subgraph "T03: 拓扑预览模块"
        TOPO_FE[管理后台前端<br/>TopologyPreview + 3组件]
        TOPO_API[复用已有<br/>GET /graph]
    end

    subgraph "T04: 代答排除模块"
        EXC_API[排除管理 API<br/>8个端点]
        EXC_SVC[ExclusionService<br/>+ 4个Matcher]
        EXC_FE[管理后台前端<br/>ExclusionRules + 2组件]
        MSG_INT[消息流集成<br/>ai_handler.py]
    end

    %% 依赖关系
    DB --> TRI_API
    DB --> EXC_API
    CFG --> TRI_SVC
    MODEL --> TRI_API
    MODEL --> EXC_API
    SCHEMA --> TRI_API
    SCHEMA --> EXC_API
    ROUTER --> TRI_API
    ROUTER --> EXC_API

    TRI_API --> TRI_SVC
    TRI_SVC --> H5_FE
    TRI_SVC --> AG_FE
    TRI_SVC --> DIFY_APP

    EXC_API --> EXC_SVC
    EXC_SVC --> EXC_FE
    EXC_SVC --> MSG_INT

    %% 软依赖
    TRI_SVC -.->|"CategoryMatcher<br/>软依赖分诊结果"| EXC_SVC

    %% 拓扑预览独立
    TOPO_FE --> TOPO_API

    %% 并行标注
    style T03 fill:#e8f5e9,stroke:#4caf50,stroke-dasharray: 5 5
    style TOPO_FE fill:#e8f5e9,stroke:#4caf50,stroke-dasharray: 5 5
    style TOPO_API fill:#e8f5e9,stroke:#4caf50,stroke-dasharray: 5 5

    style DB fill:#e3f2fd,stroke:#2196f3
    style CFG fill:#e3f2fd,stroke:#2196f3
    style MODEL fill:#e3f2fd,stroke:#2196f3
    style SCHEMA fill:#e3f2fd,stroke:#2196f3
    style ROUTER fill:#e3f2fd,stroke:#2196f3

并行开发说明

并行组 可并行任务 前置条件
组 1 T02(分诊)+ T03(拓扑) T01 完成
组 2 T04(排除)可与 T02 后半段并行 T01 完成;T04 的 CategoryMatcher 部分可先实现框架,分诊结果接入后联调

T03(拓扑预览)完全独立,与 T02、T04 无交叉依赖,可全程并行开发。


10. 共享知识(Shared Knowledge

10.1 API 响应格式

所有 API 响应统一使用 {code, message, data} 格式:

{
  "code": 0,
  "message": "success",
  "data": { ... }
}
  • code: 0 表示成功,非 0 表示错误
  • 错误时 data 为 nullmessage 包含错误描述

10.2 路由前缀规范

前缀 说明
H5 员工端 /api/h5/triage/* 分诊交互(员工发起分诊)
坐席端 /api/agent/triage/* 分诊看板(坐席查看/路由)
管理后台-知识迭代 /api/admin/knowledge-iteration/* 已有,拓扑预览复用
管理后台-代答排除 /api/admin/exclusion-rules/* 新增

10.3 认证方式

认证方式 装饰器
H5 端 H5 用户 token(企微 OAuth Depends(get_current_user)
坐席端 坐席 token Depends(get_current_agent)
管理后台 管理员 token + IP 白名单 @require_admin

10.4 Dify 对接统一模式

所有 Dify 调用统一使用 OpenAI Chat Completions 兼容接口:

POST {dify_base_url}/v1/chat/completions
Authorization: Bearer {api_key}
Content-Type: application/json

{
  "model": "{app_identifier}",
  "messages": [
    {"role": "system", "content": "{system_prompt}"},
    {"role": "user", "content": "{user_input}"}
  ],
  "temperature": 0.3
}
Dify 应用 配置项前缀 用途
Agent 1(员工端 AI dify_api_* AI 对话回复
Agent 2Wingman dify_wingman_* 坐席草稿/摘要/标签
Agent 3(分诊引擎) dify_triage_* 本次新增 — 分诊分析
审批意图识别 approval_dify_* 审批路由 + 排除意图匹配

10.5 数据库模型规范

  • 主键:id VARCHAR(36),默认 gen_random_uuid()::text(与现有 KnowledgeSuggestion 一致)
  • 时间戳:created_at / updated_at,默认 NOW()
  • JSON 字段:使用 SQLAlchemy JSON 类型,PostgreSQL 原生 JSON
  • 软删除:本期不实现软删除,直接物理删除(排除规则)

10.6 前端 API 封装规范

所有前端 API 封装统一使用 axios 实例(已有),响应拦截器统一处理 {code, data} 解包:

// 示例:frontend-admin/src/api/exclusion.ts
import request from './index'

export function getExclusionRules(params: ExclusionRuleQuery) {
  return request.get('/admin/exclusion-rules', { params })
}

10.7 WebSocket 消息格式

坐席排除选项推送到 H5 端的 WS 消息:

{
  "type": "triage_exclude",
  "data": {
    "triage_id": "xxx",
    "excluded_labels": ["弹出了,提示密码错误"],
    "recommended_label": "弹出了,提示无法连接到服务器"
  }
}

11. 待明确事项

# 问题 影响范围 建议处理方式
1 Dify 分诊应用的 dify2openai 代理地址是否与 Wingman 共用?还是需要独立部署? T02 DifyTriageService 建议共用同一个 dify2openai 代理,通过不同 API Key 区分应用。需运维确认代理是否支持多应用路由。
2 意图匹配复用审批意图识别 Dify 应用,但审批意图的意图分类体系(12种/18流程)是否覆盖代答排除需要的意图(如 data_deletion, data_modification)? T04 IntentMatcher 建议在审批意图识别应用中扩展意图分类,或确认现有意图体系已覆盖。需与 Dify 管理员确认。
3 坐席端分诊看板的 WS 实时推送是否复用现有 ws_manager.py 的 WebSocket 连接?还是需要新建独立 WS 通道? T02 坐席端排除选项推送 建议复用现有 WS 连接,通过 type 字段区分消息类型。需确认现有 WS 消息协议是否支持扩展。
4 分诊看板"导出"功能导出的 xlsx 包含哪些字段?是否需要包含分诊步骤详情? T02 导出 API 建议导出基础字段(标题/用户/部门/问题类型/置信度/紧急度/路由/时间),分诊步骤详情可选。
5 代答排除的"仅提示转人工"动作,用户确认转人工后是否需要再次调用后端 API?还是前端直接调用已有的转人工接口? T04 消息流集成 建议前端收到 prompt_transfer 类型的排除命中后,展示提示语 + 确认按钮,用户确认后调用已有 POST /api/conversations/{id}/transfer
6 拓扑预览页面是否需要权限控制?所有管理员都能查看,还是需要新增 RBAC 权限项? T03 路由配置 建议复用 @require_admin 装饰器,所有管理员可查看。如需细粒度控制,后续新增 topology:read 权限。

文档结束 本文档基于已有技术方案文档和原型图文档,结合用户确认的 4 项关键决策 + 9 项次要决策,产出增量系统设计和开发任务分解。 工程师拿到后可直接按 T01→T02/T03(并行)→T04 的顺序开始开发。