chore: 整理项目结构,清理归档文件,更新部署配置

This commit is contained in:
Simon
2026-07-04 21:01:39 +08:00
parent 8bd4ab0366
commit 64ff1bf7d5
508 changed files with 43575 additions and 14129 deletions
@@ -0,0 +1,499 @@
# IT智能服务台 — 系统架构设计文档
> **文档版本**: v1.4
> **创建日期**: 2025-07-11
> **最近更新**: 2026-07-04
> **架构师**: 高见远 (Bob)
> **状态**: 正式版
---
## 目录
1. [技术文档索引](#1-技术文档索引)
2. [系统概述](#2-系统概述)
3. [整体架构](#3-整体架构)
4. [技术选型](#4-技术选型)
5. [部署架构](#5-部署架构)
6. [统一入口设计](#6-统一入口设计)
7. [模块架构](#7-模块架构)
8. [AI Wingman 设计](#8-ai-wingman-设计)
9. [外部系统集成](#9-外部系统集成)
10. [安全设计](#10-安全设计)
11. [复杂对话场景设计](#11-复杂对话场景设计)
12. [知识图谱数据模型设计](#12-知识图谱数据模型设计)
13. [数据库设计](#13-数据库设计)
14. [API设计规范](#14-api设计规范)
---
## 1. 技术文档索引
本文档为技术架构主文档,相关详细设计文档如下:
| 文档 | 说明 | 状态 |
|------|------|------|
| **本文档** | 系统架构总览 v1.3 | ✅ 正式 |
| **01-ADRs-架构决策/** | 4项架构决策 | ✅ 已完成 |
| **02-技术方案/** | 具体功能技术方案 | |
| ├── [技术方案-消息功能详细设计.md](./02-技术方案/技术方案-消息功能详细设计.md) | 消息功能详细设计 | ✅ 已实现 |
| ├── [技术方案-摇人协作.md](./02-技术方案/技术方案-摇人协作.md) | 多坐席协作方案 | ✅ 已实现 |
| ├── [技术方案-邀请功能.md](./02-技术方案/技术方案-邀请功能.md) | 邀请功能方案 | ✅ 已实现 |
| ├── [技术方案-复杂场景重构.md](./02-技术方案/技术方案-复杂场景重构.md) | 复杂对话场景技术方案 | ✅ 设计完成 |
| └── [技术方案-ExternalSystemAdapter抽象层.md](./02-技术方案/技术方案-ExternalSystemAdapter抽象层.md) | 外部系统适配层设计 | ✅ 设计完成 |
| **03-技术分析/** | 技术研究分析 | |
| ├── [技术分析-H5右侧栏动态推送评估.md](./03-技术分析/技术分析-H5右侧栏动态推送评估.md) | H5右侧栏动态推送评估 | ✅ 已完成 |
| └── [技术分析-架构消息知识库迭代.md](./03-技术分析/技术分析-架构消息知识库迭代.md) | 架构消息知识库迭代 | ✅ 已完成 |
| **04-数据库设计/** | 数据库设计 | |
| └── [数据库设计-ER图与环境变量清点.md](./04-数据库设计/数据库设计-ER图与环境变量清点.md) | 数据库ER图与环境变量 | ✅ 已完成 |
| **05-架构图/** | Mermaid图表 | |
| └── [架构图集](./05-架构图/) | 各类时序图/类图 | ✅ 已完成 |
| [D-T19-知识图谱数据模型设计.md](../02-产品需求/小组任务书/D-T19-知识图谱数据模型设计.md) | 知识图谱数据模型设计 | ✅ 设计完成 |
---
## 2. 系统概述
### 2.1 项目背景
IT智能服务台是为企业提供 IT support 的智能化服务平台,核心目标:
- **员工侧**:通过 H5 页面提交 IT 问题、AI 自助解答、人工坐席服务
- **坐席侧**:通过自研工作台处理会话、AI 辅助( Wingman )、知识推荐
- **管理侧**:通过管理后台配置系统、管理坐席、查看数据
### 2.2 核心能力
| 能力 | 说明 |
|------|------|
| 消息路由 | AI 与人工无缝切换 |
| 实时会话 | 坐席工作台实时消息 |
| AI 辅助 | Wingman 草稿/摘要/知识推荐 |
| 外部集成 | 联软/火绒/aTrust/eHR 终端数据 |
| 角色管理 | 统一入口 + RBAC 权限 |
| 知识图谱 | Neo4j 图数据库支撑智能对话 |
---
## 3. 整体架构
### 3.1 系统架构图
```
┌─────────────────────────────────────────────────────────────────────┐
│ Linux 服务器 Docker │
│ │
│ ┌──────────┐ ┌──────────────┐ ┌──────────────┐ ┌────────────┐ │
│ │ Nginx │ │ Frontend │ │ Frontend │ │ Frontend │ │
│ │ (反代) │──│ Agent │ │ H5 User │ │ Portal │ │
│ │ :80/:443│ │ (Vue3+EP) │ │ (Vue3+Vant4)│ │ (Vue3) │ │
│ └────┬─────┘ └──────────────┘ └──────────────┘ └────────────┘ │
│ │ :5173 :5174 :5176 │
│ ▼ │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────────┐ │
│ │ FastAPI │ │ Redis │ │PostgreSQL│ │ Neo4j │ │
│ │ Backend │──│ (缓存/Session)│──│ (持久化) │──│ (知识图谱) │ │
│ │ :8000 │ │ :6379 │ │ :5432 │ │ :7687 │ │
│ └────┬─────┘ └──────────┘ └──────────┘ └──────┬───────┘ │
│ │ │ │
│ │ ▼ │
│ │ ┌──────────────┐ │
│ └──────────────────────────────────────▶│ Dify AI │ │
│ │ (外部依赖) │ │
│ └──────────────┘ │
└───────────────────────────────────────────────────────────────────────┘
│ HTTPS
┌───────────────┐
│ 企微服务器 │
│ (消息回调API) │
└───────────────┘
```
### 3.2 前端架构
| 端 | 路径 | 技术栈 | 端口 |
|----|------|--------|------|
| Portal | /itportal/ | Vue3 + Element Plus | 5176 |
| 坐席端 | /itagent/ | Vue3 + Element Plus | 5173 |
| H5用户端 | /itdesk/ | Vue3 + Vant 4 | 5174 |
| 管理后台 | /itadmin/ | Vue3 + Element Plus + Tailwind | 5175 |
---
## 4. 技术选型
### 4.1 核心技术栈
| 层级 | 技术 | 版本 | 说明 |
|------|------|------|------|
| 前端框架 | Vue 3 | ^3.4.0 | Composition API |
| 前端路由 | Vue Router | ^4.3.0 | SPA 路由 |
| 状态管理 | Pinia | ^2.1.0 | 轻量级状态管理 |
| UI 组件 | Element Plus | ^2.7.0 | 坐席端/Portal |
| UI 组件 | Vant 4 | ^4.0 | H5 移动端 |
| 样式 | Tailwind CSS | ^3.4.0 | 管理后台 |
| 构建工具 | Vite | ^5.3.0 | 快速构建 |
| 后端框架 | FastAPI | 0.110+ | 异步高性能 |
| ORM | SQLAlchemy | 2.0+ | async ORM |
| 数据库 | PostgreSQL | 16 | 主数据存储 |
| 图数据库 | Neo4j Community | 5.x | 知识图谱存储 |
| 缓存 | Redis | 7 | Session/Token/缓存 |
| AI 引擎 | Dify | — | 外部依赖 |
---
## 5. 部署架构
### 5.1 部署拓扑
```
┌────────────────────────────┐
│ 企微服务器(外部) │
│ qyapi.weixin.qq.com │
└──────────┬─────────────────┘
│ HTTPS :443
┌──────────────── 办公网络 ────────────────────────────────┐
│ │
│ ┌──────────┐ ┌──────────────────────────┐ │
│ │ 坐席浏览器 │────────▶│ https://itsupport. │ │
│ │ (内网) │ HTTPS │ servyou.com.cn │ │
│ └──────────┘ └──────────┬───────────────┘ │
│ │ │
├────────────────── OA 服务器网络 ──┼───────────────────────┤
│ │ │
│ ┌───────▼──────────────┐ │
│ │ 服务器 (Docker) │ │
│ │ ┌────────────────┐ │ │
│ │ │ Nginx :80/443 │ │ │
│ │ └───────┬────────┘ │ │
│ │ │ │ │
│ │ ┌───────▼────────┐ │ │
│ │ │ FastAPI :8000 │ │ │
│ │ └───────┬────────┘ │ │
│ │ │ │ │
│ │ ┌───────▼────────┐ │ │
│ │ │ PostgreSQL │ │ │
│ │ └───────┬────────┘ │ │
│ │ │ │ │
│ │ ┌───────▼────────┐ │ │
│ │ │ Redis │ │ │
│ │ └───────┬────────┘ │ │
│ └──────────┴───────────┘ │
│ │ │
│ ┌────────▼──────────────┐ │
│ │ 现有 AI 服务(外部依赖)│ │
│ └───────────────────────┘ │
└──────────────────────────────────────────────────────────┘
```
---
## 6. 统一入口设计
### 6.1 概述
统一入口(Portal)是系统的认证入口,按角色分发到对应端。
**核心功能**
- 企微 OAuth2 静默授权(用户端)
- 账号密码+OTP 认证(坐席/管理端)
- 角色检测与路由选择
- 统一 Token 管理
### 6.2 登录方式(v1.5 变更)
> **更新日期**: 2026-07-04 | **核心变更**: 坐席/管理端浏览器直接打开,**无需经过企微工作台**
| 端 | 访问方式 | 登录方式 | 说明 |
|----|----------|----------|------|
| **用户端 (H5)** | 企微工作台 → 应用内嵌打开 | OAuth2 静默授权 | 强制内嵌,保证安全、入口统一、用户粘性 |
| **坐席端** | 浏览器直接打开 | 账号密码+OTP | **无需经过企微**,灵活办公,支持多设备 |
| **管理后台** | 浏览器直接打开 | 账号密码+OTP | **无需经过企微**,安全可控 |
### 6.3 登录流程
```
用户端 (H5) 坐席端 / 管理后台
│ │
▼ ▼
企微工作台打开 浏览器直接访问
│ │
▼ ▼
OAuth2 静默授权 账号密码+OTP验证
(用户无感知) │
│ ▼
▼ 登录成功
跳转 /itdesk/ 跳转 /itagent/ 或 /itadmin/
```
### 6.4 角色路由逻辑
```
OAuth2 授权完成(用户端) / 登录成功(坐席/管理端)
查询角色列表: GET /api/portal/roles
├── 仅 user 角色 → 直接跳转 /itdesk/
├── user + agent → 显示选择页(2张卡片)
├── user + admin → 显示选择页(2张卡片)
└── user + agent + admin → 显示选择页(3张卡片)
```
### 6.5 URL 路径规划
| 端 | 路径 | 说明 |
|---|------|------|
| 统一入口 | /itportal/ | 路由选择页 |
| 用户端 | /itdesk/ | 员工提交工单 |
| 坐席端 | /itagent/ | IT坐席处理会话 |
| 管理端 | /itadmin/ | 系统配置 |
| API | /api/ | 后端接口 |
### 6.6 企微环境检测
| 组件 | 说明 |
|------|------|
| **用户端检测** | `navigator.userAgent.includes('wxwork')`,非企微跳转拦截页 |
| **坐席/管理端** | 无需检测,浏览器直接访问 |
### 6.7 OTP 双因素认证
| 组件 | 说明 |
|------|------|
| **OTP 绑定** | 首次登录引导绑定,支持 TOTPGoogle Authenticator/微信扫码) |
| **OTP API** | `/api/agents/otp-bind``/api/agents/otp-verify` |
| **验证场景** | 坐席/管理员登录时需 OTP 验证 |
| **绑定入口** | 坐席端 TopBar 下拉菜单"OTP二次验证"选项 |
### 6.8 测试账号
| 角色 | 用户名 | 初始密码 | OTP | 说明 |
|------|--------|----------|-----|------|
| 坐席 | `sxn` | `admin123` | 需绑定 | IT 支持组组长 |
| 管理 | `sxn` | `admin123` | 需绑定 | 同上,具有 admin 权限 |
> **注意**:生产环境需修改默认密码;坐席/管理员需先绑定OTP才能登录
---
## 7. 模块架构
### 7.1 管理后台模块
**技术栈**Vue 3 + TypeScript + Element Plus + Tailwind CSS + Pinia
**核心功能**
- 运营仪表盘
- 功能开关配置
- 坐席管理(角色/技能标签)
- 外部系统集成配置
- 快速回复审核
- 会话监控
- 知识图谱管理
### 7.2 坐席工作台模块
**技术栈**Vue 3 + TypeScript + Element Plus + Pinia
**核心功能**
- 会话列表(排队/进行中/已解决)
- 实时聊天
- 快速回复
- AI Wingman 右侧栏
- 消息标记(VIP/招手/情绪)
### 7.3 H5 用户端模块
**技术栈**Vue 3 + Vant 4 + TypeScript
**核心功能**
- 消息发送/接收
- 排查步骤引导
- 会话状态查看
- 满意度评价
---
## 8. AI Wingman 设计
详见 [02-技术方案/技术方案-Wingman设计.md](./02-技术方案/技术方案-Wingman设计.md)
### 8.1 什么是 Wingman
Wingman 是坐席工作台的 AI 辅助系统:
- **草稿回复**:坐席打字 → AI 实时生成 3 条草稿
- **自动摘要**:会话结束 → AI 200 字摘要
- **知识推荐**:对话中识别关键字 → 推 FAQ
- **排查步骤**:员工描述问题 → AI 给 step-by-step
---
## 9. 外部系统集成
### 9.1 系统角色与优先级
| 系统 | 角色 | 核心能力 | 认证方式 |
|------|------|---------|---------|
| 联软LV7000 | 主映射源(P0) | 终端查询、硬件详情、在线状态 | IP白名单+账号密码 |
| 火绒企业版 | 安全源(P0) | 终端列表、漏洞/病毒事件 | HMAC-SHA1 AccessKey |
| aTrust | VPN源(P1) | 在线用户+VPN IP、终端查询 | HMAC-SHA256签名 |
| 北森eHR | 辅助静态数据(P2) | 员工基础信息、任职信息 | OAuth2.0 |
| 企微审批 | 待办同步(P1) | 审批工单同步到坐席待办 | 企微审批应用API |
### 9.2 企微审批工单同步
> **新增日期**: 2026-07-04
| 项目 | 说明 |
|------|------|
| **功能** | 将企微审批工单同步到坐席待办事项面板 |
| **企微API** | `GET /cgi-bin/oa/approvallist` |
| **同步频率** | 每5分钟定时拉取,或 Webhook 实时推送 |
| **数据映射** | 审批标题 → TodoItem.title / 审批状态 → TodoItem.status |
| **依赖** | 需企微管理后台创建审批应用并授权 API |
---
## 10. 安全设计
### 10.1 认证安全
| 安全措施 | 说明 |
|----------|------|
| OAuth2 静默授权 | scope=snsapi_base,用户无感知(用户端) |
| 账号密码+OTP | 坐席/管理端认证方式 |
| state 参数防 CSRF | 随机 state,回调时验证 |
| Token 密码学安全 | secrets.token_urlsafe(32) |
| Token TTL 8小时 | Redis 自动过期 |
| redirect_uri 白名单 | 生产环境仅允许正式域名 |
| 企微 UA 检测 | 用户端非企微环境跳转拦截页 |
| OTP 双因素认证 | 坐席/管理员登录需 OTP 验证码 |
### 10.2 角色安全
| 安全措施 | 说明 |
|----------|------|
| 角色最小权限 | 默认仅 user 角色 |
| 角色来源追溯 | user_roles 表记录 source 和 assigned_by |
| 管理端 IP 白名单 | 仅内网/VPN 可访问 |
---
## 11. 复杂对话场景设计
### 11.1 设计理念:TeliChat 三重约束
| 约束 | 作用 | 实现方式 |
|------|------|---------|
| 拓扑结构限制 | 限制对话可以走到哪里 | Neo4j DAG 边定义 |
| 信息状态约束 | 决定当前已经知道什么 | 信息项组合状态 |
| Python 代码约束 | 负责真正的业务判断 | FastAPI 业务逻辑 |
### 11.2 全局意图类型
| 意图 | 用户表达示例 | 处理策略 |
|------|-------------|---------|
| SKIP | "这个问题先不管了" | 跳过当前节点 |
| INSERT | "对了,我的打印机也有问题" | 插入新任务到队列 |
| RESUME | "还是说回刚才那个网络问题" | 恢复之前话题 |
| ESCALATE | "叫个人工来" | 转接坐席 |
---
## 12. 知识图谱数据模型设计
详见 [../02-产品需求/小组任务书/D-T19-知识图谱数据模型设计.md](../02-产品需求/小组任务书/D-T19-知识图谱数据模型设计.md)
### 12.1 实体类型
| 实体类型 | 说明 | 示例 |
|----------|------|------|
| Domain | 业务域 | 网络域、安全域、设备域 |
| Issue | 问题 | VPN连不上、打印机故障 |
| Solution | 解决方案 | 密码重置、重启服务 |
| FAQ | 常见问题 | 如何连接VPN |
### 12.2 关系类型
| 关系类型 | 方向 | 含义 | 核心属性 |
|----------|------|------|----------|
| BELONGS_TO | Issue→Domain | 属于 | weight |
| RECOMMENDS | Issue→Solution | 推荐 | priority, confidence |
| CAN_RESOLVE | Solution→Issue | 解决 | success_rate |
---
## 13. 数据库设计
### 13.1 核心表结构
| 表名 | 说明 |
|------|------|
| agents | 坐席信息 |
| conversations | 会话表 |
| messages | 消息表 |
| quick_reply_templates | 快速回复模板 |
| system_configs | 系统配置 |
| roles | 角色表 |
| user_roles | 用户角色关联 |
---
## 14. API设计规范
### 14.1 响应格式
```json
// 成功
{"code": 0, "data": {...}, "message": "success"}
// 失败
{"code": 1001, "data": null, "message": "参数错误"}
```
### 14.2 认证方式
| 端 | localStorage 键 | 说明 |
|----|-----------------|------|
| 坐席端 | `agent_token` | 坐席工作台 |
| 管理后台 | `admin_token` | 管理后台 |
| 统一入口 | `user_token` | Portal |
---
## 附录
### A. 项目阶段规划
| 阶段 | 内容 |
|------|------|
| 阶段一 | 转人工改H5+坐席MVP+邀请+管理后台 |
| 阶段二 | H5全流程+WS+排队+满意度+OAuth2 |
| 阶段三 | AI Wingman+排查流程图+标注+知识图谱 |
| 阶段四 | 迭代闭环+数据看板+知识库 |
| 阶段五 | 自动/辅助审核、开单、结单 |
### B. 部署信息
| 环境 | 域名 | IP |
|------|------|-----|
| 生产 | itsupport.servyou.com.cn | 10.90.5.110 |
| 测试 | itdesk.amanzac.com | NAS |
### C. 技术文档更新日志
| 版本 | 日期 | 修改内容 |
|------|------|---------|
| v1.0 | 2026-07-04 | 整合技术架构文档 |
| v1.1 | 2026-07-04 | 新增知识图谱章节 |
| v1.2 | 2026-07-04 | 新增登录设计章节(用户端强制企微内嵌) |
| v1.3 | 2026-07-04 | 坐席/管理端无需经过企微,浏览器直接登录 |
| v1.4 | 2026-07-04 | 新增企微审批工单同步到待办事项 |
---
> **文档结束** — 本文档为 IT 智能服务台系统架构设计主文档
@@ -0,0 +1,61 @@
# ADR-001: Gitea 自托管 + Tailscale Funnel 暴露
**状态**: ✅ 已采纳
**日期**: 2026-06-14
**决策者**: 宋献 + Claude 评审
**关联**: [[Gitea部署指南]] / [[风险跟踪表]] 第十二节
---
## 1. 背景
项目主仓 `D:\资料\03-项目开发\wecom_it_smart_desk\` 需要:
- 跨设备协作(simon's 电脑 + workbuddy 沙箱)
- 推送评审 + 分支保护
- 异地可访问(workbuddy 沙箱无 Tailscale 内网)
## 2. 评估方案
| 方案 | 优势 | 劣势 | 结论 |
|---|---|---|---|
| **A. GitHub 私有仓** | 零运维 + 全球 CDN + 完善 Actions | 代码在境外(企业合规风险)+ 付费 | ❌ 否决 |
| **B. GitLab.com 私有仓** | 免费私有 + 完善 CI | 代码在境外 + workbuddy 沙箱访问延迟 | ❌ 否决 |
| **C. Gitea 自托管(NAS)+ Tailscale Funnel** | 数据本地 + workbuddy 可访问 + 免费 | NAS 单点故障 + Funnel 稳定性依赖 Tailscale | ✅ **采纳** |
| **D. Gitea 自托管 + 公网 IP 暴露** | 不依赖 Tailscale | 需配 SSL + DDOS 风险 + 国内带宽限制 | ❌ 否决 |
## 3. 决策
**采纳 C 方案**: Gitea 套件(DS923+ NAS)+ Tailscale Funnel 暴露公网。
## 4. 关键参数
| 项 | 值 | 备注 |
|---|---|---|
| Gitea 版本 | 1.22+ | 套件中心固定 |
| 端口 | 8418 (HTTP) | 避开被占端口 |
| 数据库 | SQLite3 | 单机够用,简化部署 |
| Tailscale 私网 | `tail58d872.ts.net` | DSM 已配 |
| Funnel 域名 | `https://ds923plus.tail58d872.ts.net` | 沙箱访问 |
| 备份 | `scripts/backup-gitea.sh` cron 3 点 | 见 [[Gitea部署指南]] §6 |
| 异地备份 | OSS / COS 推 | M-1 风险项待解决 |
## 5. 风险与缓解
| 风险 | 等级 | 缓解 |
|---|---|---|
| NAS 硬盘故障 | 🟠 高 | 异地 OSS 备份(待配) |
| Tailscale Funnel 稳定性 | 🟡 中 | Funnel 故障时降级 LAN(`http://100.85.152.112:8418`) |
| 卸载误操作数据丢失 | 🟡 中 | 备份脚本 + 卸载前 checklist |
| token 泄露 | 🟠 高 | token 不入文件,走 wincred |
## 6. 决策影响
- ✅ 团队协作无需 VPN(workbuddy 沙箱直连 Funnel)
- ✅ 推送评审 + 分支保护(PR + 1 reviewer)
- ⚠️ NAS 单点是隐患,需异地备份
- ⚠️ 卸载/迁移需严格按 [[Gitea部署指南]] §8 走
## 7. 后续评审
- 3 个月后(2026-09-14)评审:Funnel 稳定性 + 备份完整度
- 6 个月后(2026-12-14)评审:是否切到企业 GitLab(如果合规要求)
@@ -0,0 +1,80 @@
# ADR-002: WebSocket Token 鉴权(走 Sec-WebSocket-Protocol)
**状态**: ✅ 已采纳
**日期**: 2026-06-14
**决策者**: 宋献 + Claude 评审
**关联**: [[风险跟踪表]] 第十节 / 评审报告 `workbuddy-2026-06-14-P0安全.md`
---
## 1. 背景
WebSocket 鉴权原方案:`ws://server/ws/?token=<JWT>` —— **token 在 URL 里**:
- ❌ 被 nginx access_log 记录
- ❌ 被 CDN / 反代记录
- ❌ 被浏览器历史记录
**P0 漏洞**(H-11 风险项),已修复。
## 2. 评估方案
| 方案 | 浏览器支持 | token 泄露 | 实施难度 | 结论 |
|---|---|---|---|---|
| **A. Authorization: Bearer header** | ❌ 浏览器 WS API 不支持自定义 header | ✅ 不泄 | 中 | ❌ 否决(浏览器限制) |
| **B. Sec-WebSocket-Protocol: bearer.<token>** | ✅ 现代浏览器都支持 | ✅ 不在 URL | 低 | ✅ **采纳** |
| **C. 第一条消息传 token** | ✅ 全支持 | ⚠️ 需先开 WS 接受任意连接(无法鉴权) | 低 | ❌ 否决 |
| **D. Cookie 自动带** | ✅ 全支持 | ⚠️ CSRF 风险 | 中 | ❌ 否决 |
## 3. 决策
**采纳 B 方案**: `Sec-WebSocket-Protocol: bearer.<token>`
服务端协商 subprotocol,客户端用第二个 subprotocol 传 token(浏览器 API `new WebSocket(url, [subprotocols])`)。
## 4. 实现
### 4.1 前端
```ts
// frontend-agent/src/composables/useWebSocket.ts
const ws = new WebSocket(wsUrl, [`bearer.${agentStore.token}`])
```
### 4.2 后端
```python
# backend/app/api/ws.py
subprotocol = request.headers.get("sec-websocket-protocol", "")
if subprotocol.startswith("bearer."):
token = subprotocol[7:]
else:
# 降级:Authorization header
auth = request.headers.get("Authorization", "")
if auth.startswith("Bearer "):
token = auth[7:]
else:
# 降级:query param(已废,只用于兼容旧前端)
token = request.query_params.get("token", "")
```
## 5. 降级路径
| 优先级 | 来源 | 用途 |
|---|---|---|
| 1 | Sec-WebSocket-Protocol | 标准(主) |
| 2 | Authorization: Bearer | Postman / 测试工具 |
| 3 | query `?token=` | 已废(留兼容) |
## 6. 风险与缓解
| 风险 | 缓解 |
|---|---|
| 浏览器 API 不支持 subprotocol | 现代浏览器(2020+)都支持,无问题 |
| 旧客户端不更新 | query param 降级仍可用,但提示更新 |
| nginx 仍记录 subprotocol | `location /ws/ { access_log off; }` 配合 |
## 7. 决策影响
- ✅ WS 鉴权修复,token 不再泄
- ✅ nginx access_log 关闭,旧 token 不留痕
- ⚠️ 旧客户端需更新(发版通知)
@@ -0,0 +1,106 @@
# ADR-003: nginx 敏感路径 access_log 关闭
**状态**: ✅ 已采纳
**日期**: 2026-06-14
**决策者**: 宋献 + Claude 评审
**关联**: [[风险跟踪表]] 第十节 / 评审报告 `workbuddy-2026-06-14-P0安全.md`
---
## 1. 背景
nginx `access_log` 默认记录所有请求,含敏感信息:
- `Authorization: Bearer <token>`
- `?token=<JWT>`
- `Cookie: session=<sid>`
敏感路径必须关闭 access_log,避免 token 永久落盘。
## 2. 决策
**敏感路径一律 `access_log off`**,具体见下表。
## 3. 关闭清单
| 路径 | 原因 | access_log |
|---|---|---|
| `/ws/` | WebSocket token 鉴权 | `off` |
| `/api/v1/auth/login` | 密码登录 | `off` |
| `/api/v1/auth/refresh` | token 刷新 | `off` |
| `/api/v1/h5/oauth/callback` | OAuth2 回调 | `off` |
| `/api/v1/wecom/callback` | 企微回调(验证 URL 含 echostr) | `off` |
| `/api/v1/agents/login` | 坐席登录 | `off` |
| `/api/v1/upload*` | 文件上传(可能含敏感文件名) | `off` |
| `/health` `/healthz` `/readyz` | 健康检查(高频) | `off` |
## 4. 实现
```nginx
server {
# 全局
access_log /var/log/nginx/access.log;
error_log /var/log/nginx/error.log;
# WS(敏感)
location /ws/ {
access_log off;
proxy_pass http://backend;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
}
# 登录(敏感)
location ~ ^/api/v1/(auth|agents)/login$ {
access_log off;
proxy_pass http://backend;
}
# 健康检查(高频)
location ~ ^/(health|healthz|readyz)$ {
access_log off;
proxy_pass http://backend;
}
# 其它
location / {
proxy_pass http://backend;
}
}
```
## 5. error_log 仍开启
⚠️ **error_log 仍开** —— 4xx/5xx 错误需要留痕(token 在 error log 里出现频率低,且 error log 有 TTL 自动切割)。
## 6. 日志清理脚本
`/etc/logrotate.d/nginx` 配:
```
/var/log/nginx/*.log {
daily
rotate 7
compress
delaycompress
missingok
notifempty
create 0640 www-data adm
sharedscripts
prerotate
if [ -d /etc/logrotate.d/httpd-prerotate ]; then \
run-parts /etc/logrotate.d/httpd-prerotate; \
fi
endscript
postrotate
invoke-rc.d nginx rotate >/dev/null 2>&1
endscript
}
```
## 7. 风险与缓解
| 风险 | 缓解 |
|---|---|
| 漏关某个敏感路径 | 定期审计(任务 W-5,workbuddy 跑) |
| 调试时无 access_log 难定位 | debug 时临时开 `access_log /tmp/debug.log;` |
| 攻击者利用关闭日志 | error_log 仍开,异常请求有记录 |
@@ -0,0 +1,101 @@
# ADR-004: Token 不入文件,走 wincred 缓存
**状态**: ✅ 已采纳
**日期**: 2026-06-14
**决策者**: 宋献 + Claude 评审
**关联**: [[风险跟踪表]] 第十二节 12.6 / 推送约定
---
## 1. 背景
之前 Gitea 推送 token 直接嵌入 `.git/config``origin.url`:
```
url = https://ae236991c3d5...@ds923plus.tail58d872.ts.net/...
```
**风险**:
- ❌ token 明文落盘
- ❌ token 失效后难更新(URL 整体换)
- ❌ 误 `git add .git/` 可能入仓(虽然 .git/config 本身不入仓,但 .git/ 目录其他文件可能)
- ❌ auto-classifier 拒绝重写 URL(防误操作)
**事故**: 2026-06-14 workbuddy-claude token 失效后,`origin.url` 残留死凭据。
## 2. 决策
**`.git/config``origin.url` 只写用户名,token 走 git credential helper(wincred)缓存**。
## 3. 实现
### 3.1 配 remote URL(无 token)
```bash
git remote add origin https://simon@ds923plus.tail58d872.ts.net/simon/wecom_it_smart_desk.git
# 或修复现有:
git remote set-url origin https://simon@ds923plus.tail58d872.ts.net/simon/wecom_it_smart_desk.git
```
### 3.2 配 credential helper
`.git/config`:
```ini
[credential]
helper = manager # Windows = wincred / Linux = git-credential-manager
```
### 3.3 首次推(输一次 token)
```bash
git push -u origin main
# 弹窗 → username 留空,password = token
# wincred 自动缓存
```
### 3.4 换 token(必走)
```bash
# 清旧缓存
printf "protocol=https\nhost=ds923plus.tail58d872.ts.net\nusername=simon\n" | git credential reject
# 存新缓存(一次性,token 在 heredoc 不入文件)
printf "protocol=https\nhost=ds923plus.tail58d872.ts.net\nusername=simon\npassword=NEW_TOKEN\n" | git credential approve
# 验证
git push origin main
# 应不弹窗
```
## 4. workbuddy 推送同理
`.workbuddy/config.json` 是 workbuddy 自己的凭据存储(类比 .git/config),**入仓** ❌。
**正确做法**:
- `.workbuddy/config.json` 写用户名/URL/其他配置,**不写 token**
- workbuddy 启动时读 `gitea.token` 字段(从环境变量 / 启动参数传入)
- 或者 workbuddy 自己也用 git credential helper
**已加 .gitignore**:
```gitignore
.workbuddy/config.json
.workbuddy/config.local.json
.workbuddy/*.token
.workbuddy/credentials*
.workbuddy/.env*
```
## 5. 优势
- ✅ token 不入文件(只入 wincred 系统密钥环)
- ✅ 换 token 简单(`credential reject` + `approve`)
- ✅ 不会误入仓
- ✅ auto-classifier 不拒绝(无 token 写文件)
## 6. 风险与缓解
| 风险 | 缓解 |
|---|---|
| wincred 缓存被读(本机攻击) | 操作系统级防护 + 强密码 + BitLocker |
| 跨设备不能用 wincred | Linux 用 `git-credential-manager`,Mac 用 `git-credential-osxkeychain` |
| 换电脑忘缓存 | `git credential approve` 一次性配置 |
| token 在环境变量 | 仍比文件安全 + CI 用 secret store |
@@ -0,0 +1,199 @@
# 技术方案-企微审批工单同步
> **文档编号**: 02-技术方案-企微审批工单同步
> **版本**: v1.1
> **创建日期**: 2026-07-04
> **状态**: 待评审
---
## 1. 需求概述
### 1.1 背景
将企微审批工单同步到坐席待办事项,实现IT服务台与企微审批系统的集成。
### 1.2 需求描述
| 需求项 | 说明 |
|--------|------|
| 需求来源 | PRD v0.7.2 Backlog #74 |
| 需求类型 | P1(待办事项集成) |
| 目标 | 将企微审批工单同步到坐席待办事项 |
---
## 2. 技术方案
### 2.1 整体架构
```
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
│ 企微审批系统 │ ───▶ │ IT服务台后端 │ ───▶ │ 坐席待办事项 │
│ (外部API) │ │ (同步服务) │ │ (todo_items) │
└─────────────────┘ └─────────────────┘ └─────────────────┘
│ │ │
▼ ▼ ▼
审批工单数据 定时轮询/回调 待办面板展示
(get_approval_list) 转换+存储
```
### 2.2 数据同步方式
| 方式 | 说明 | 优点 | 缺点 |
|------|------|------|------|
| **定时轮询** | 每5分钟调用企微API拉取审批列表 | 实现简单,稳定可靠 | 有延迟(最大5分钟) |
| **企微回调** | 审批通过时企微主动推送 | 实时性高 | 需要企微开通回调权限 |
| **混合模式** | 回调为主 + 定时兜底 | 实时+兜底 | 实现复杂 |
**推荐方案**:定时轮询(简单可靠) + 回调模式(实时性)
### 2.3 企微API接口
使用企微 OA 审批接口:
| 接口 | 说明 |
|------|------|
| `GET /cgi-bin/oa/approvallist` | 获取审批列表 |
| `GET /cgi-bin/oa/approvalinfo` | 获取审批详情 |
### 2.4 数据映射
| 企微审批字段 | todo_item 字段 | 说明 |
|-------------|----------------|------|
| `sp_no` | `id` | 审批单号 |
| `sp_name` | `title` | 审批名称 |
| `apply_name` | `description.applicant` | 申请人 |
| `apply_time` | `created_at` | 申请时间 |
| `status` | `status` | 审批状态 |
| `sp_status` | `priority` | 审批类型 |
### 2.5 待办事项字段扩展
```python
class TodoItem(Base):
# 现有字段...
type: Mapped[str] = "approval" # 固定为 approval
# 新增字段(审批专用)
approval_id: Mapped[str] = mapped_column(String(64)) # 企微审批单ID
approval_type: Mapped[str] = mapped_column(String(64)) # 审批模板名称
applicant: Mapped[str] = mapped_column(String(100)) # 申请人
applicant_dept: Mapped[str] = mapped_column(String(100)) # 申请人部门
apply_time: Mapped[datetime] # 申请时间
approval_url: Mapped[str] = mapped_column(String(512)) # 审批详情链接
```
---
## 3. 审批模板(IT相关)
根据需求,明确需要同步的审批模板:
| 序号 | 审批模板名称 | 说明 |
|------|-------------|------|
| 1 | IT资产升级申请 | 硬件升级审批 |
| 2 | IT资产报废申请 | 资产报废审批 |
| 3 | 商业软件服务申请 | 软件采购审批 |
| 4 | 企微外联权限申请 | 外网权限审批 |
| 5 | 离职人员企微微盘&文档空间异常移交 | 离职资产移交 |
| 6 | 会议室故障报修 | 设备报修审批 |
---
## 4. 企微API权限确认
### 4.1 如何确认是否已开通权限
**方法一:企微管理后台查看**
1. 登录企微管理后台:https://work.weixin.qq.com/
2. 进入「应用管理」→「自建应用」→ 选择IT服务台应用
3. 查看「API权限」中是否包含:
- 通讯录同步
- 审批相关接口
**方法二:调用接口测试**
使用已有 access_token 调用以下接口测试:
```bash
curl "https://qyapi.weixin.qq.com/cgi-bin/oa/approvallist?access_token=xxx&start_time=0&end_time=9999999999"
```
如果返回 `{"errcode":0, ...}` 表示已开通权限。
**方法三:联系企微管理员**
确认是否在「审批」应用中开通了API调用权限。
### 4.2 回调模式说明
| 对比项 | 定时轮询 | 企微回调 |
|--------|----------|----------|
| 实时性 | 5分钟延迟 | 秒级实时 |
| 实现复杂度 | 简单 | 稍复杂 |
| 可靠性 | 稳定 | 依赖回调通道 |
| 资源消耗 | 每次全量/增量拉取 | 按需推送 |
**回调模式优势**
1. 实时性高 - 审批提交/通过后立即同步
2. 节省资源 - 只需处理变更,无需轮询
3. 体验更好 - 坐席几乎实时看到新待办
**回调模式限制**
1. 需要企微开通审批回调权限
2. 回调地址需公网可访问(可通过nginx反向代理)
3. 需要处理回调签名验证
---
## 5. 实施计划
### 5.1 任务拆分
| 任务 | 说明 | 优先级 |
|------|------|--------|
| T1 | 扩展 todo_item 模型,新增审批专用字段 | P0 |
| T2 | 创建企微审批同步服务 `ApprovalSyncService` | P0 |
| T3 | 实现定时轮询逻辑(每5分钟) | P0 |
| T4 | 坐席端待办面板接入审批数据 | P1 |
| T5 | 审批状态变更同步 | P1 |
| T6 | 企微审批回调接入(可选) | P2 |
### 5.2 数据库变更
```sql
ALTER TABLE todo_items
ADD COLUMN approval_id VARCHAR(64),
ADD COLUMN approval_type VARCHAR(64),
ADD COLUMN applicant VARCHAR(100),
ADD COLUMN applicant_dept VARCHAR(100),
ADD COLUMN apply_time TIMESTAMP,
ADD COLUMN approval_url VARCHAR(512);
```
---
## 6. 待确认事项
| 事项 | 状态 | 备注 |
|------|------|------|
| 企微API权限 | 待确认 | 需按4.1方法确认 |
| 审批模板 | ✅ 已明确 | 6个IT相关模板 |
| 同步频率 | ✅ 已确认 | 5分钟可接受 |
| 回调模式 | ✅ 确认开通 | 实时性优先 |
---
## 7. 相关文档
| 文档 | 说明 |
|------|------|
| [PRD v0.7.2 Backlog](./02-产品需求/product-产品/v0.7.2-backlog-candidate-2026-06-24.md) | 需求来源 |
| [todo_item 模型](../../backend/app/models/todo_item.py) | 现有模型定义 |
| [企微API文档](https://developer.work.weixin.qq.com/document/14567) | 审批接口文档 |
---
> **文档状态**: 待评审
@@ -0,0 +1,305 @@
# ExternalSystemAdapter 抽象层设计文档
> 版本:V1.0 | 日期:2026-06-11 | 作者:智能IT支持服务台项目组
---
## 一、设计目标
为联软、火绒、aTrust、eHR 四个外部系统提供**统一适配层**,实现:
1. **接口统一**:上层业务代码只依赖抽象接口,不感知底层系统差异
2. **可替换性**:Mock数据开发 → 真实API无缝切换,只需改配置
3. **缓存透明**:外部数据自动缓存+定时刷新,业务层无感
4. **降级安全**:外部系统不可用时自动降级,不阻断主流程
5. **横向扩展**:新增系统只需实现一个 Adapter,零改动业务层
---
## 二、系统角色与优先级
| 系统 | 角色 | 核心能力 | 认证方式 | 凭证状态 |
|------|------|---------|---------|---------|
| 联软LV7000 | 主映射源(P0) | 终端查询(含strusername)、硬件详情、在线状态 | IP白名单+账号密码+Token | 明天可拿 |
| 火绒企业版 | 安全源(P0) | 终端列表、漏洞/病毒事件、一键隔离 | HMAC-SHA1 AccessKey | 现在可拿 |
| aTrust | VPN源(P1) | 在线用户+VPN IP、终端查询、踢出用户 | HMAC-SHA256签名 | 约一周 |
| 北森eHR | 辅助静态数据(P2) | 员工基础信息、任职信息 | OAuth2.0 | 待对接HR |
---
## 三、架构分层
```
┌─────────────────────────────────────────────────┐
│ 上层业务代码(AI Wingman等) │
├─────────────────────────────────────────────────┤
│ ExternalSystemService(统一门面) │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ 缓存层 │ │ 降级策略 │ │ 配置管理 │ │
│ └──────────┘ └──────────┘ └──────────┘ │
├─────────────────────────────────────────────────┤
│ ExternalSystemAdapter(抽象基类) │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌────┐│
│ │ LianRuan │ │ HuoRong │ │ aTrust │ │eHR ││
│ │ Adapter │ │ Adapter │ │ Adapter │ │ 适配││
│ └──────────┘ └──────────┘ └──────────┘ └────┘│
├─────────────────────────────────────────────────┤
│ MockAdapter(开发期) │
└─────────────────────────────────────────────────┘
```
---
## 四、核心抽象接口
### 4.1 数据模型(统一DTO
```python
class TerminalInfo(BaseModel):
"""统一终端信息模型 — 所有Adapter返回同一结构"""
source_system: str # 数据来源系统标识
computer_name: str # 计算机名
ip_addresses: List[str] # IP地址列表(含VPN虚拟IP
mac_addresses: List[str] # MAC地址列表
os_version: Optional[str] # 操作系统版本
is_online: bool # 是否在线
logged_in_user: Optional[str] # 当前登录用户账号(映射核心字段)
logged_in_user_name: Optional[str] # 用户姓名
department: Optional[str] # 所属部门
hardware_summary: Optional[Dict] # 硬件摘要(CPU/内存/磁盘)
last_seen: Optional[datetime] # 最后在线时间
raw_data: Optional[Dict] # 原始响应(调试用,生产可关闭)
class SecurityStatus(BaseModel):
"""统一安全状态模型"""
source_system: str
terminal_id: str
virus_events: Optional[Dict] # 病毒事件统计
vulnerabilities: Optional[List] # 高危漏洞列表
is_isolated: bool # 是否被隔离
isolation_source: Optional[str] # 隔离来源系统
class VpnSession(BaseModel):
"""VPN会话模型(仅aTrust"""
source_system: str = "atrust"
username: str
display_name: Optional[str]
remote_ip: str
vpn_ip: Optional[str] # 虚拟内网IP
is_trusted: bool
last_login: Optional[datetime]
```
### 4.2 Adapter抽象基类
```python
from abc import ABC, abstractmethod
from typing import Optional, List
class ExternalSystemAdapter(ABC):
"""外部系统适配器抽象基类
每个外部系统实现此接口,上层业务只依赖此接口。
"""
@property
@abstractmethod
def system_name(self) -> str:
"""系统标识名称,如 'lianruan' / 'huorong' / 'atrust' / 'ehr'"""
...
@property
@abstractmethod
def is_available(self) -> bool:
"""当前系统是否可用(凭证已配置+网络可达)"""
...
@abstractmethod
async def health_check(self) -> bool:
"""健康检查 — 验证凭证和网络连通性"""
...
# ── 终端查询能力 ──
async def get_terminal_by_user(self, username: str) -> Optional[TerminalInfo]:
"""通过员工账号查询终端信息(映射核心方法)
联软:queryDevByParams(strusername=xxx)
火绒:_list(ip=xxx) 需配合联软IP交叉匹配
aTrustqueryAll(bindUserList) 终端绑定用户
eHR:不提供终端数据,返回None
"""
return None # 默认不支持,子类按需覆写
async def get_terminal_by_computer(self, computer_name: str) -> Optional[TerminalInfo]:
"""通过计算机名查询终端信息"""
return None
async def get_terminal_detail(self, terminal_id: str) -> Optional[TerminalInfo]:
"""查询终端详细信息(硬件/软件/网络配置)"""
return None
# ── 安全能力 ──
async def get_security_status(self, terminal_id: str) -> Optional[SecurityStatus]:
"""获取终端安全状态(病毒/漏洞/隔离状态)"""
return None
async def isolate_terminal(self, terminal_id: str, reason: str) -> bool:
"""隔离终端(仅火绒支持,需admin角色二次确认)"""
raise NotImplementedError(f"{self.system_name} 不支持终端隔离")
async def unisolate_terminal(self, terminal_id: str) -> bool:
"""解除终端隔离"""
raise NotImplementedError(f"{self.system_name} 不支持解除隔离")
# ── VPN/在线状态 ──
async def get_vpn_sessions(self, username: Optional[str] = None) -> List[VpnSession]:
"""查询VPN在线会话(仅aTrust支持)"""
return []
async def get_online_status(self, username: str) -> bool:
"""查询用户是否在线"""
return False
```
### 4.3 统一门面服务
```python
class ExternalSystemService:
"""外部系统统一门面 — 上层业务只调用此类"""
def __init__(self, adapters: Dict[str, ExternalSystemAdapter], cache: CacheService):
self._adapters = adapters # {"lianruan": LianRuanAdapter, ...}
self._cache = cache
async def find_user_terminal(self, username: str) -> Optional[TerminalInfo]:
"""查找用户终端 — 优先联软,降级aTrust,最后eHR
做什么:按映射优先级依次查询,任一系统返回即停止
为什么:联软strusername精确匹配最可靠,aTrust次之
"""
# 1. 联软(主源,strusername精确匹配)
result = await self._query_with_cache("lianruan", "get_terminal_by_user", username)
if result:
return result
# 2. aTrustVPN源,bindUserList匹配)
result = await self._query_with_cache("atrust", "get_terminal_by_user", username)
if result:
return result
# 3. eHR(静态辅助,无终端数据)
return None
async def get_terminal_security(self, terminal_id: str) -> Optional[SecurityStatus]:
"""获取终端安全状态 — 仅火绒"""
return await self._query_with_cache("huorong", "get_security_status", terminal_id)
async def isolate_terminal(self, terminal_id: str, reason: str, operator: str) -> bool:
"""隔离终端 — 仅火绒,需operator记录审计日志"""
logger.warning(f"终端隔离操作: terminal={terminal_id}, operator={operator}, reason={reason}")
return await self._adapters["huorong"].isolate_terminal(terminal_id, reason)
```
---
## 五、缓存策略
| 数据类型 | 缓存TTL | 刷新策略 | 说明 |
|---------|---------|---------|------|
| 终端映射(员工→终端) | 30分钟 | 定时刷新+访问时检查 | 映射关系不常变 |
| 终端详情(硬件/软件) | 60分钟 | 懒加载 | 硬件配置极少变 |
| 安全状态(漏洞/病毒) | 5分钟 | 短TTL+事件驱动 | 安全状态需近实时 |
| VPN在线状态 | 1分钟 | 短TTL | 在线状态变化快 |
| eHR员工信息 | 24小时 | 每日凌晨全量同步 | 静态数据 |
缓存key格式:`ext:{system}:{method}:{param_hash}`
---
## 六、降级策略
| 故障场景 | 处理方式 | 用户影响 |
|---------|---------|---------|
| 单个系统不可用 | 跳过该系统,尝试下一优先级 | 部分数据缺失,不阻断 |
| 所有外部系统不可用 | 返回缓存数据(如有)+ 明确标注"数据可能过时" | 信息可能过时 |
| 缓存+外部系统均不可用 | 返回空结果+告警通知坐席 | 无法获取外部数据 |
| 火绒隔离操作失败 | 重试1次 → 失败则记录待执行队列 → 告警坐席 | 安全操作不静默失败 |
---
## 七、配置管理
```python
class ExternalSystemConfig(BaseModel):
"""外部系统连接配置 — 从环境变量或配置中心读取"""
# 联软
lianruan_base_url: str = "http://192.168.x.x:30098"
lianruan_api_account: Optional[str] = None
lianruan_api_password: Optional[str] = None
# 火绒
huorong_base_url: str = "http://huorong.oa.servyou-it.com:8080"
huorong_access_key_id: Optional[str] = None
huorong_access_key_secret: Optional[str] = None
# aTrust
atrust_base_url: str = "https://atrust.servyou-it.com:4433"
atrust_api_id: Optional[str] = None
atrust_api_secret: Optional[str] = None
atrust_directory_domain: Optional[str] = None
# eHR
ehr_base_url: Optional[str] = None
ehr_client_id: Optional[str] = None
ehr_client_secret: Optional[str] = None
# 全局
cache_enabled: bool = True
mock_mode: bool = False # True时所有请求走MockAdapter
```
---
## 八、目录结构
```
backend/app/services/external/
├── __init__.py # 模块导出
├── base.py # 抽象基类 ExternalSystemAdapter + 数据模型
├── config.py # 配置管理 ExternalSystemConfig
├── cache.py # 缓存装饰器和策略
├── mock.py # MockAdapter(开发期使用)
├── lianruan_adapter.py # 联软适配器
├── huorong_adapter.py # 火绒适配器
├── atrust_adapter.py # aTrust适配器
├── ehr_adapter.py # eHR适配器
└── service.py # ExternalSystemService 统一门面
```
---
## 九、实施路径
| 阶段 | 内容 | 依赖 |
|------|------|------|
| Step 1 | base.py + config.py + mock.py + service.py + cache.py | 无,立即可做 |
| Step 2 | huorong_adapter.py | 凭证现在可拿 |
| Step 3 | lianruan_adapter.py | 凭证明天可拿 |
| Step 4 | atrust_adapter.py | 凭证约一周 |
| Step 5 | ehr_adapter.py | 待对接HR团队 |
---
## 十、与项目阶段的对应关系
| 项目阶段 | Adapter用途 | 对接系统 |
|---------|------------|---------|
| 阶段一(1C) | 不使用 — MVP只跑会话管理 | 无 |
| 阶段二(2B) | 联软终端查询 + 火绒安全状态 | 联软+火绒 |
| 阶段二(2C) | 火绒漏洞/病毒/隔离 | 火绒 |
| 阶段三(3B) | aTrust VPN数据 + AI混合排查 | aTrust |
| 阶段三(3C) | eHR员工信息 + 标注体系 | eHR |
@@ -0,0 +1,900 @@
# 重构方案 - 复杂场景技术实现方案
> 文档版本:v1.1
> 日期:2026-07-03
> **设计理念**:借鉴 TeliChat "让代码负责业务逻辑,让模型负责语言理解"
---
## 零、设计理念:TeliChat 三重约束
> **核心理念**:借鉴 TeliChat 白盒架构,确保复杂对话场景的可靠性
### 0.1 三重约束机制
| 约束 | 作用 | 实现方式 |
|------|------|---------|
| **拓扑结构限制** | 限制对话可以走到哪里 | Neo4j DAG 边定义 |
| **信息状态约束** | 决定当前已经知道什么 | 信息项组合状态 |
| **Python 代码约束** | 负责真正的业务判断 | FastAPI 业务逻辑 |
### 0.2 信息项修饰机制
| 修饰 | 含义 | 在复杂场景中的应用 |
|------|------|------------------|
| `固定` | 用户回答后不再重复询问 | 已通过系统获取的信息(操作系统、用户名) |
| `增量` | 允许用户补充新信息 | 故障描述、错误信息 — **非线性跳转核心** |
| `明确` | 必须明确回答 | 紧急程度确认 — **信息更正核心** |
| `隐含` | 可以从上下文推断 | AI 推断的问题类型 |
| `复述` | 要求用户确认信息正确性 | 重要操作确认 — **信息更正核心** |
| `必需` | 必须填写才能进入下一节点 | 必填字段 — **任务中断恢复核心** |
### 0.3 全局意图类型
| 意图 | 用户表达示例 | 处理策略 | 对应场景 |
|------|-------------|---------|---------|
| `SKIP` | "这个问题先不管了" | 跳过当前节点,记录未完成 | 非线性跳转 |
| `INSERT` | "对了,我的打印机也有问题" | 插入新任务到队列 | 多意图并行 |
| `RESUME` | "还是说回刚才那个网络问题" | 恢复之前话题 | 任务中断恢复 |
| `SWITCH` | "先帮我看看VPN吧" | 切换到指定话题 | 非线性跳转 |
| `CORRECT` | "刚才说错了,是win10" | 更新信息项值 | 信息更正 |
| `SUPPLEMENT` | "再补充一下,是财务部的电脑" | 增量补充信息 | 信息更正 |
| `PAUSE` | "我先去开会,等会继续" | 保存状态,等待恢复 | 任务中断恢复 |
| `RESUME_TASK` | "好了,继续吧" | 恢复中断的任务 | 任务中断恢复 |
| `ESCALATE` | "叫个人工来" | 转接坐席 | 所有场景 |
### 0.4 状态驱动流程
```python
def determine_next_node(topology, information_items, user_intent):
"""
根据三重因素确定下一个节点
"""
# 1. 拓扑约束:检查意图是否在允许的路径上
allowed_paths = topology.get_allowed_paths(current_node)
if user_intent not in allowed_paths:
return handle_off_path_intent(user_intent)
# 2. 信息项检查:是否满足必填信息要求
required_items = topology.get_required_items(next_node)
for item in required_items:
if not information_items[item].is_filled:
return PromptForItem(item)
# 3. 业务逻辑:Python 代码执行判断
if should_escalate(information_items):
return TransferToAgent()
return ExecuteNode(next_node)
```
---
## 一、非线性跳转
### 1.1 场景描述
用户在对话过程中不按线性路径跳转,而是随时切换话题或返回上一步。
**示例**
```
用户:我想开VPN
AI:请问是个人用途还是团队用途?
用户:先说说团队 VPN 是什么(跳转到知识了解)
AI:(介绍团队VPN
用户:算了,我还是开个人的吧(返回原话题)
AI:好的,个人VPN开通需要...
```
### 1.2 技术架构
```
┌─────────────────────────────────────────────────────────────────┐
│ 用户对话 │
└─────────────────────────────┬───────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────┐
│ Dify LLM 推理层 │
│ ┌─────────────────┐ ┌─────────────────┐ ┌───────────────┐ │
│ │ 意图理解 │ │ 上下文追踪 │ │ 路径规划 │ │
│ │ Intent Parser │ │ Context Track │ │ Path Planner │ │
│ └─────────────────┘ └─────────────────┘ └───────────────┘ │
└─────────────────────────────┬───────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────┐
│ Neo4j 知识图谱 │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ IT_SUPPORT_GRAPH │ │
│ │ │ │
│ │ [VPN问题] ──[可选]──> [个人VPN] │ │
│ │ │ │ │
│ │ [可选] │ │
│ │ │ │ │
│ │ └───[可选]──> [团队VPN] ──[子节点]──> [使用场景] │ │
│ │ │ │
│ └─────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────┘
```
### 1.3 知识图谱设计
```cypher
// 节点设计
CREATE (vpn:Issue {name: "VPN开通", category: "网络"})
CREATE (personal:Action {name: "个人VPN开通"})
CREATE (team:Action {name: "团队VPN开通"})
CREATE (usage:Info {name: "使用场景说明"})
// 关系设计 - 支持非线性跳转
CREATE (vpn)-[:LEADS_TO {type: "可选", order: 1}]->(personal)
CREATE (vpn)-[:LEADS_TO {type: "可选", order: 2}]->(team)
CREATE (team)-[:LINKS_TO {type: "子节点"}]->(usage)
// 跳转关系 - 支持任意跳转
CREATE (personal)-[:CAN_JUMP_TO {type: "跳转"}]->(team)
CREATE (team)-[:CAN_JUMP_TO {type: "返回"}]->(vpn)
CREATE (usage)-[:CAN_JUMP_TO {type: "返回"}]->(team)
```
### 1.4 信息项修饰机制(借鉴 TeliChat)
**核心设计**:使用信息项的 `增量` 修饰符支持非线性跳转
```python
# 信息项定义
class InformationItem:
name: str # 信息项名称,如 "{故障描述}"
value: Any # 当前值
modifiers: List[str] # 修饰符: ["增量"]
# 状态追踪
is_filled: bool # 是否已填写
is_incremental: bool # 是否允许增量(补充)
# 非线性跳转示例
用户我想开VPN
AI请问是个人用途还是团队用途
用户先说说团队 VPN 是什么用户切换到"了解"意图
# 信息项状态变化
information_items = {
"VPN类型": {"value": None, "modifiers": ["增量"], "is_filled": False},
}
# 用户切换话题时,信息项"VPN类型"保留(因为是增量修饰)
# 用户返回时,可以继续之前的流程
```
### 1.5 全局意图识别支持
```python
# 非线性跳转意图识别
def detect_jump_intent(user_input: str) -> JumpIntent:
"""检测跳转意图"""
# RESUME - 返回之前话题
if any(kw in user_input for kw in ["还是说回", "继续刚才", "回到"]):
return JumpIntent.RESUME
# SWITCH - 切换到新话题
if any(kw in user_input for kw in ["先看", "先帮我看看", "算了"]):
return JumpIntent.SWITCH
# SKIP - 跳过当前问题
if any(kw in user_input for kw in ["先不管", "跳过", "算了"]):
return JumpIntent.SKIP
return JumpIntent.NONE
```
### 1.6 关键设计点
| 设计点 | 方案 | 说明 |
|--------|------|------|
| 上下文栈 | 使用栈结构维护对话路径 | 支持"返回上一步" |
| 节点状态 | 每个节点记录 visited/focused 状态 | 区分已访问和当前节点 |
| 跳转权限 | 边设计 CAN_JUMP_TO 关系 | 控制哪些节点可以互相跳转 |
| **信息项修饰** | 使用"增量"修饰符 | **借鉴 TeliChat,支持乱序输入** |
| **全局意图** | 识别 RESUME/SWITCH/SKIP | **借鉴 TeliChat,控制跳转** |
---
## 二、多意图并行
### 2.1 场景描述
用户一次输入包含多个意图,系统需要并行处理后再合并结果。
**示例**
```
用户:我电脑开不了机,VPN也连不上
→ 同时处理2个问题:
1. 电脑开机问题 → 引导检查电源/硬件
2. VPN连接问题 → 引导检查网络/账号
→ 合并输出:两个问题的处理指引
```
### 2.2 技术架构
```
用户输入: "我电脑开不了机,VPN也连不上"
┌─────────────────────────────────────────────────────────────────┐
│ Dify 多意图识别节点 │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ Input: "我电脑开不了机,VPN也连不上" │ │
│ │ Output: │ │
│ │ [ │ │
│ │ {intent: "电脑开机故障", entities: []}, │ │
│ │ {intent: "VPN连接失败", entities: []} │ │
│ │ ] │ │
│ └─────────────────────────────────────────────────────────┘ │
└─────────────────────────────┬───────────────────────────────────┘
┌───────────────────┼───────────────────┐
▼ ▼ ▼
┌───────────┐ ┌───────────┐ ┌───────────┐
│ 意图1分支 │ │ 意图2分支 │ │ 意图N分支 │
│ 电脑开机 │ │ VPN连接 │ │ ... │
│ 路径推理 │ │ 路径推理 │ │ │
└─────┬─────┘ └─────┬─────┘ └─────┬─────┘
│ │ │
└───────────────────┼───────────────────┘
┌─────────────────────────────────────────────────────────────────┐
│ 结果合并节点 │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ 将多个分支的结果合并为统一回复 │ │
│ └─────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────┘
```
### 2.3 Dify 工作流设计
```yaml
# Dify 工作流配置(简化版)
workflow:
nodes:
- id: multi_intent_parser
type: LLM
prompt: |
用户输入: {{input}}
识别所有意图,以JSON数组返回:
[{"intent": "意图1", "entities": [...]}, {"intent": "意图2", "entities": [...]}]
- id: parallel_branches
type: parallel
branches:
- target: intent_1_handler
- target: intent_2_handler
- id: result_merger
type: LLM
prompt: |
合并以下处理结果为统一回复:
{{intent_1_result}}
{{intent_2_result}}
```
### 2.4 知识图谱辅助
```cypher
// 为多意图场景设计聚合节点
CREATE (multi:IntentGroup {name: "多问题聚合", type: "parallel"})
// 并行意图关系
CREATE (multi)-[:CONTAINS {parallel: true}]->(vpn_issue)
CREATE (multi)-[:CONTAINS {parallel: true}]->(hardware_issue)
// 并行度标记
MATCH (n)-[r:LEADS_TO]->(m)
SET r.is_parallel = false // 默认串行
```
### 2.5 信息项聚合管理(借鉴 TeliChat)
**核心设计**:多意图对应多个独立的信息项集合
```python
# 多意图场景的信息项设计
class MultiIntentSession:
"""多意图会话管理"""
# 每个意图对应独立的信息项集合
intent_items: Dict[str, List[InformationItem]] = {
"电脑开机": [
{"name": "故障现象", "modifiers": ["增量", "必需"]},
{"name": "错误信息", "modifiers": ["增量"]},
],
"VPN连接": [
{"name": "错误代码", "modifiers": ["明确"]},
{"name": "网络环境", "modifiers": ["隐含"]},
]
}
def add_intent(self, intent: str):
"""添加新意图,创建独立信息项集合"""
if intent not in self.intent_items:
self.intent_items[intent] = []
def get_all_items(self) -> List[InformationItem]:
"""获取所有意图的信息项"""
items = []
for intent_items in self.intent_items.values():
items.extend(intent_items)
return items
```
### 2.6 全局意图 INSERT 支持
```python
# INSERT 意图处理
def handle_insert_intent(user_input: str, session: MultiIntentSession):
"""处理插入新意图"""
# 检测 INSERT 意图
insert_keywords = ["对了", "还有", "另外", "顺便"]
if any(kw in user_input for kw in insert_keywords):
# 识别新意图
new_intent = llm_recognize_intent(user_input)
session.add_intent(new_intent)
# 并行处理新旧意图
return process_parallel_intents(session)
return None
```
### 2.7 关键设计点
| 设计点 | 方案 | 说明 |
|--------|------|------|
| 意图识别 | Dify LLM 并行识别 | 使用 Few-shot 提示词模板 |
| 分支并行 | Dify Parallel Branch | 同时触发多个处理分支 |
| 结果合并 | Dify LLM 合并 | 智能合并多分支输出 |
| 冲突检测 | 边设计 CONFLICTS_WITH | 检测意图间冲突 |
| **信息项聚合** | 每个意图独立信息项集合 | **借鉴 TeliChat,管理多意图状态** |
| **INSERT 意图** | 检测"对了/还有"等插入语 | **借鉴 TeliChat 全局意图** |
---
## 三、信息更正
### 3.1 场景描述
用户在对话过程中更正之前提供的信息,系统需要理解更正并更新上下文。
**示例**
```
用户:帮我重置密码,用户名是 zhangsan
AI:好的,正在为 zhangsan 重置密码...
用户:不好意思,用户名是 lisi,不是 zhangsan
AI:好的,已更正,为 lisi 重置密码
```
### 3.2 技术架构
```
用户输入: "不好意思,用户名是 lisi,不是 zhangsan"
┌─────────────────────────────────────────────────────────────────┐
│ Dify 意图理解层 │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ 识别更正意图: │ │
│ │ { │ │
│ │ "type": "correction", │ │
│ │ "field": "username", │ │
│ │ "old_value": "zhangsan", │ │
│ │ "new_value": "lisi" │ │
│ │ } │ │
│ └─────────────────────────────────────────────────────────┘ │
└─────────────────────────────┬───────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────┐
│ Neo4j 会话状态图谱 │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ Session(id: xxx) │ │
│ │ │ │ │
│ │ ├── [:PROVIDED]─> Field(name: "username", value: "zhangsan") │ │
│ │ │ │ │
│ │ └── [:CORRECTED]─> (标记旧值为已更正) │ │
│ │ │ │ │
│ │ └──> Field(name: "username", value: "lisi") │ │
│ └─────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────┘
```
### 3.3 知识图谱设计
```cypher
// 会话状态节点
CREATE (session:Session {
id: "session_123",
user_id: "user_001",
created_at: datetime(),
current_node: "password_reset"
})
// 用户提供的字段(可更正)
CREATE (session)-[:PROVIDED]->(field1:Field {
name: "username",
value: "zhangsan",
timestamp: datetime(),
status: "corrected" // 标记为已更正
})
// 更正后的字段
CREATE (session)-[:PROVIDED]->(field2:Field {
name: "username",
value: "lisi",
timestamp: datetime(),
status: "active" // 当前有效值
})
// 更正历史关系
CREATE (field1)-[:CORRECTED_TO {new_value: "lisi", timestamp: datetime()}]->(field2)
```
### 3.4 Dify 工作流设计
```yaml
# 更正处理节点
nodes:
- id: correction_detector
type: LLM
prompt: |
检测用户输入是否为信息更正:
用户输入: {{input}}
当前已知信息: {{known_fields}}
输出JSON:
{
"is_correction": true/false,
"corrected_field": "字段名",
"old_value": "旧值",
"new_value": "新值",
"confidence": 0.0-1.0
}
- id: field_updater
type: code
action: |
# 更新 Neo4j 中的字段状态
# 1. 标记旧值为 corrected
# 2. 创建新值节点
# 3. 建立更正关系
```
### 3.5 信息项修饰机制(借鉴 TeliChat)
**核心设计**:使用"增量"+"复述"双修饰实现智能信息更正
```python
# 信息项修饰与更正策略
class InformationItem:
modifiers: List[str] # 修饰符组合
def handle_update(self, new_value: str, is_correction: bool = False):
"""处理信息更新"""
if "增量" in self.modifiers and not is_correction:
# 增量模式:追加新值,不覆盖旧值
self.value = f"{self.value}; {new_value}"
elif "复述" in self.modifiers:
# 复述模式:要求用户确认
self.pending_confirmation = new_value
return ConfirmationRequest(new_value)
else:
# 默认模式:直接覆盖
self.value = new_value
self.is_filled = True
self.last_updated = datetime.now()
return None
# 更正示例
# 用户:不好意思,用户名是 lisi,不是 zhangsan
# 系统识别 CORRECT 意图,更新信息项
information_items["用户名"] = {
"value": "lisi",
"modifiers": ["明确"], # 原来是"明确"修饰
"is_filled": True,
"update_history": ["zhangsan"] # 保留更正历史
}
```
### 3.6 全局意图 CORRECT/SUPPLEMENT 支持
```python
# 更正意图识别
def detect_correction_intent(user_input: str) -> CorrectionInfo:
"""检测更正意图"""
correction_patterns = [
(r"不是(.+),是(.+)", "swap"), # 不是A,是B
(r"应该是(.+)", "replace"), # 应该是A
(r"更正.*?为(.+)", "replace"), # 更正为A
(r"说错了.*?是(.+)", "replace"), # 说错了是A
]
for pattern, correction_type in correction_patterns:
match = re.search(pattern, user_input)
if match:
return CorrectionInfo(
type=correction_type,
old_value=match.group(1) if match.lastindex >= 1 else None,
new_value=match.group(2) if match.lastindex >= 2 else match.group(1),
is_correction=True
)
return None
```
### 3.7 关键设计点
| 设计点 | 方案 | 说明 |
|--------|------|------|
| 更正识别 | Dify LLM | 检测"不是/应该是/更正为"等模式 |
| 字段版本 | Neo4j 节点版本 | 维护字段历史,支持回溯 |
| 状态同步 | WS 实时推送 | 更正后立即更新前端状态 |
| **增量修饰** | 追加而非覆盖 | **借鉴 TeliChat,支持补充** |
| **复述修饰** | 要求用户确认 | **借鉴 TeliChat,关键信息确认** |
| **CORRECT 意图** | 识别更正表达 | **借鉴 TeliChat 全局意图** |
---
## 四、任务中断与恢复
### 4.1 场景描述
用户在任务进行过程中中断(离开/超时),后续可以恢复继续。
**示例**
```
用户:我要开VPN
AI:请问是个人还是团队用途?
用户:(离开/超时/未回复)
--- 2小时后 ---
用户:继续刚才的VPN申请
AI:好的,您刚才选择的是VPN开通,请问是个人还是团队用途?
(恢复上下文,继续流程)
```
### 4.2 技术架构
```
┌─────────────────────────────────────────────────────────────────┐
│ 任务状态机设计 │
│ │
│ ┌─────────┐ 用户输入 ┌─────────┐ 选择个人 ┌─────┐ │
│ │ START │ ───────────> │ ASK_TYPE│ ──────────> │INPUT│ │
│ └─────────┘ └─────────┘ └──┬──┘ │
│ ^ │ │ │
│ │ │ 恢复 │ │
│ │ ▼ ▼ │
│ │ ┌─────────┐ ┌────────┐ │
│ └─────────────── │ PAUSED │ <──────────── │ RESUME │ │
│ 恢复 └─────────┘ 用户恢复 └────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘
```
### 4.3 知识图谱设计
```cypher
// 任务节点
CREATE (task:Task {
id: "task_vpn_001",
type: "VPN开通",
status: "paused", // paused / active / completed / cancelled
created_at: datetime(),
updated_at: datetime(),
current_node: "ASK_TYPE",
user_id: "user_001"
})
// 任务路径历史
CREATE (task)-[:HAS_HISTORY]->(step1:TaskStep {
node: "START",
status: "completed",
timestamp: datetime()
})
CREATE (task)-[:HAS_HISTORY]->(step2:TaskStep {
node: "ASK_TYPE",
status: "active",
timestamp: datetime()
})
// 恢复点
CREATE (task)-[:CAN_RESUME_FROM {node: "ASK_TYPE"}]->(resume_point:ResumePoint {
prompt: "请问是个人还是团队用途?",
options: ["个人", "团队"],
timestamp: datetime()
})
```
### 4.4 状态管理
```python
# 任务状态机
class TaskState:
STATES = {
"created": ["active", "cancelled"],
"active": ["paused", "completed", "cancelled"],
"paused": ["active", "cancelled", "expired"],
"completed": [],
"cancelled": [],
"expired": ["active"]
}
def pause(self):
"""任务中断"""
self.status = "paused"
self.paused_at = datetime.now()
self._save_to_neo4j()
def resume(self):
"""任务恢复"""
if self.status != "paused":
raise InvalidStateError("只有暂停的任务可以恢复")
self.status = "active"
self.resumed_at = datetime.now()
self._save_to_neo4j()
```
### 4.5 恢复触发
| 触发方式 | 说明 |
|----------|------|
| 关键字恢复 | 用户输入"继续/恢复/接着刚才" |
| 菜单恢复 | 提供"我的任务"入口 |
| 超时恢复 | 定时任务检测暂停任务,恢复后通知用户 |
| 坐席恢复 | 坐席手动恢复用户任务 |
```cypher
// 恢复点查询
MATCH (task:Task {user_id: $user_id, status: "paused"})
MATCH (task)-[:CAN_RESUME_FROM]->(rp)
RETURN task, rp.prompt as resume_prompt
ORDER BY rp.timestamp DESC
LIMIT 1
```
### 4.6 信息项与任务状态(借鉴 TeliChat)
**核心设计**:任务状态 = 信息项组合,使用结构化状态空间
```python
# 任务状态 - 结构化信息项组合
class TaskState:
"""借鉴 TeliChat 的结构化状态空间"""
# 任务元信息
task_id: str
status: str # created/active/paused/completed/cancelled/expired
# 信息项组合 - 决定任务能否继续
information_items: Dict[str, InformationItem] = {}
# 当前节点
current_node: str
visited_nodes: List[str] = []
def can_proceed_to(self, next_node: str) -> bool:
"""检查是否可以进入下一节点"""
# 检查必需信息项是否已填写
required_items = get_required_items(next_node)
for item_name in required_items:
if item_name not in self.information_items:
return False
if not self.information_items[item_name].is_filled:
return False
return True
def get_pending_items(self) -> List[str]:
"""获取未完成的必需信息项"""
pending = []
# 检查所有节点的必需信息项
all_required = get_all_required_items(self.current_node)
for item_name in all_required:
if item_name not in self.information_items:
pending.append(item_name)
elif not self.information_items[item_name].is_filled:
pending.append(item_name)
return pending
```
### 4.7 全局意图 PAUSE/RESUME 支持
```python
# 任务中断与恢复意图
class TaskIntent(Enum):
PAUSE = "暂停" # 用户主动暂停
RESUME_TASK = "继续" # 用户恢复任务
EXPIRED = "过期" # 任务超时过期
def handle_task_intent(user_input: str, task_state: TaskState) -> Action:
"""处理任务控制意图"""
# PAUSE - 用户离开
pause_keywords = ["先去开会", "等会继续", "先处理别的"]
if any(kw in user_input for kw in pause_keywords):
task_state.status = "paused"
task_state.paused_at = datetime.now()
save_to_redis(task_state) # 持久化
return Action(message="好的,您先忙,需要时 say一声继续")
# RESUME_TASK - 用户返回
resume_keywords = ["继续", "好了", "继续刚才", "接着来"]
if any(kw in user_input for kw in resume_keywords):
task_state = load_from_redis(task_state.task_id)
task_state.status = "active"
pending = task_state.get_pending_items()
if pending:
return Action(message=f"好的,您刚才说到{pending[0]},请继续")
else:
return Action(message="继续刚才的流程...")
return None
```
### 4.8 关键设计点
| 设计点 | 方案 | 说明 |
|--------|------|------|
| 状态持久化 | Neo4j 节点 | 保存任务完整上下文 |
| 恢复点 | ResumePoint 节点 | 保存每个步骤的恢复信息 |
| 超时处理 | 定时任务 | 24小时未恢复则标记 expired |
| 坐席可见 | 状态同步 | 坐席工作台可查看用户任务状态 |
| **结构化状态** | 信息项组合决定状态 | **借鉴 TeliChat,可靠的状态管理** |
| **必需修饰** | 缺失必填项则阻塞 | **借鉴 TeliChat,保证任务完整性** |
| **PAUSE/RESUME 意图** | 任务控制意图 | **借鉴 TeliChat 全局意图** |
---
## 五、综合架构
### 5.1 完整技术栈
```
┌─────────────────────────────────────────────────────────────────┐
│ 用户层 (H5端) │
│ 用户发起对话,接收AI/坐席回复 │
└─────────────────────────────┬───────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────┐
│ Dify AI 推理层 │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────────────┐ │
│ │ 意图理解 │ │ 多意图并行 │ │ 信息更正检测 │ │
│ │ Intent │ │ Parallel │ │ Correction Detector │ │
│ └──────────────┘ └──────────────┘ └──────────────────────┘ │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────────────┐ │
│ │ 结果合并 │ │ 路径规划 │ │ 任务状态机 │ │
│ │ Merger │ │ Path Plan │ │ Task FSM │ │
│ └──────────────┘ └──────────────┘ └──────────────────────┘ │
└─────────────────────────────┬───────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────┐
│ Neo4j 知识图谱层 │
│ ┌───────────────────────────────────────────────────────────┐ │
│ │ │ │
│ │ [Issue] ──[LEADS_TO]──> [Action] │ │
│ │ │ │ │
│ │ [:CAN_JUMP_TO] ←──→ [:CAN_JUMP_TO] │ │
│ │ │ │ │
│ │ [Session] ──[PROVIDED]──> [Field] │ │
│ │ │ │ │
│ │ [Task] ──[HAS_HISTORY]──> [TaskStep] │ │
│ │ │ │ │
│ │ [:CAN_RESUME_FROM] ──> [ResumePoint] │ │
│ │ │ │
│ └───────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────┘
```
### 5.2 TeliChat 风格架构
```
┌─────────────────────────────────────────────────────────────────┐
│ TeliChat 风格架构 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ 信息项状态管理层 │ │
│ │ (InformationItem: name/value/modifiers/is_filled) │ │
│ └─────────────────────────────┬───────────────────────────┘ │
│ │ │
│ ┌─────────────────────────────▼───────────────────────────┐ │
│ │ 全局意图识别层 │ │
│ │ (SKIP/INSERT/RESUME/SWITCH/CORRECT/SUPPLEMENT/ │ │
│ │ PAUSE/RESUME_TASK/CANCEL/ESCALATE) │ │
│ └─────────────────────────────┬───────────────────────────┘ │
│ │ │
│ ┌─────────────────────────────▼───────────────────────────┐ │
│ │ 状态驱动引擎 │ │
│ │ f(拓扑结构, 信息项组合, 用户意图) = 下一节点 │ │
│ └─────────────────────────────┬───────────────────────────┘ │
│ │ │
│ ┌─────────────────────────────▼───────────────────────────┐ │
│ │ Dify AI 执行层 │ │
│ │ (意图理解 + 路径推理 + 结果生成) │ │
│ └─────────────────────────────┬───────────────────────────┘ │
│ │ │
│ ┌─────────────────────────────▼───────────────────────────┐ │
│ │ Neo4j 图数据库层 │ │
│ │ (知识图谱 + 会话状态 + 任务状态) │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘
```
### 5.3 核心能力矩阵
| 场景 | TeliChat 设计 | Dify 能力 | Neo4j 能力 | 综合支持 |
|------|--------------|-----------|-------------|---------|
| 非线性跳转 | 增量修饰 + SWITCH/SKIP/RESUME 意图 | 路径规划 | 图谱遍历 + 跳转关系 | ✅ 完全支持 |
| 多意图并行 | INSERT 意图 + 信息项聚合 | 并行分支 + 结果合并 | 聚合节点 | ✅ 完全支持 |
| 信息更正 | 增量+复述修饰 + CORRECT/SUPPLEMENT 意图 | 更正检测 | 字段版本管理 | ✅ 完全支持 |
| 中断恢复 | 必需修饰 + PAUSE/RESUME_TASK 意图 | 状态触发 | 任务状态机 + 恢复点 | ✅ 完全支持 |
---
## 六、实施建议
### 6.1 TeliChat 架构落地计划
#### 短期(1-2周)
1. **信息项数据模型设计**
- 设计 InformationItem 数据结构
- 定义 6 种修饰符的交互策略
- 开发 CRUD 接口
2. **全局意图识别 Agent**
- 在 Dify 中创建意图识别工作流
- 支持 10 种全局意图类型
#### 中期(1个月)
3. **状态驱动引擎**
- 开发对话状态管理服务
- 实现"拓扑+信息项+意图"三因素路由
4. **Neo4j 融合**
- 图谱节点携带信息项定义
- 支持信息项状态查询
### 6.2 实施优先级
| 优先级 | 场景 | TeliChat 核心 | 工作量 | 建议 |
|--------|------|--------------|--------|------|
| P0 | 任务中断恢复 | 必需修饰 + PAUSE/RESUME | 中 | 核心场景,优先实现 |
| P1 | 信息更正 | 增量+复述 + CORRECT | 小 | 用户体验关键 |
| P1 | 非线性跳转 | 增量修饰 + SWITCH/SKIP | 大 | 知识图谱扩展 |
| P2 | 多意图并行 | INSERT + 信息项聚合 | 中 | 高级场景,后续迭代 |
### 6.3 技术债务
| 项 | 说明 | 规避方案 |
|----|------|----------|
| 图谱复杂度 | 跳转关系过多导致图谱复杂 | 设计跳转权限控制 |
| 状态一致性 | 中断恢复可能产生状态不一致 | 使用事务保证 |
| 性能 | 多意图并行增加响应时间 | 添加缓存层 |
| **LLM 幻觉** | **TeliChat 解决的核心问题** | **代码约束 + 拓扑限制** |
---
*本文档为技术实现方案详细设计 v1.1*
*新增 TeliChat 风格设计理念*
@@ -0,0 +1,389 @@
# 摇人(多坐席协作)— 技术方案
> **场景**:坐席A在处理会话时发现需要坐席B的专业知识,点击「摇人」→ 坐席B收到通知 → 进入同一会话协助。
>
> **与现有 Grab 的区别**:Grab 是「移交」(所有权转移),摇人是「协作」(所有权不变,B 加入共同处理)。
---
## 一、数据模型改动
### 1.1 Conversation 模型新增字段
```python
# backend/app/models/conversation.py
# 协作坐席ID列表(JSON 数组,存储所有被邀请来协作的坐席ID)
# 和 assigned_agent_id 的区别:
# - assigned_agent_id:会话的"主责"坐席(接单人),只有他才能结单/转接
# - collaborating_agent_ids:被邀请来协助的坐席,可以查看和回复,但不能结单
collaborating_agent_ids: Mapped[list] = mapped_column(
JSON,
nullable=False,
default=list,
comment="协作坐席ID列表",
)
```
### 1.2 数据库迁移 SQL
```sql
-- 开发环境 SQLite / 生产环境 PostgreSQL 通用
ALTER TABLE conversations ADD COLUMN collaborating_agent_ids JSON NOT NULL DEFAULT '[]';
```
### 1.3 数据关系示意
```
Conversation
├── assigned_agent_id = "agent_A" ← 主责坐席(接单人)
├── collaborating_agent_ids = ["agent_B", "agent_C"] ← 协作坐席
└── status = "serving"
权限矩阵:
主责坐席(A) 协作坐席(B/C) 其他坐席
查看会话 ✅ ✅ ✅(只读)
发送回复 ✅ ✅ ❌
结单 ✅ ❌ ❌
转接 ✅ ❌ ❌
摇人(邀请其他人) ✅ ✅ ❌
退出协作 ❌(不能) ✅ -
标记(置顶/代办) ✅ ❌ ❌
```
---
## 二、后端实现
### 2.1 新增 Schema
```python
# backend/app/schemas/conversation.py
class ConversationInvite(BaseModel):
"""摇人邀请请求"""
agent_id: str = Field(..., description="被邀请的坐席ID")
class ConversationLeave(BaseModel):
"""退出协作请求(可选,也可以从当前坐席推断)"""
pass
```
### 2.2 ConversationResponse 扩展字段
```python
# 在现有基础上新增
class ConversationResponse(BaseModel):
# ... 现有字段 ...
# ----- 多坐席协作扩展字段 -----
# 协作坐席列表
collaborating_agent_ids: list[str] = Field(default_factory=list)
# 协作坐席姓名映射(agent_id → name
collaborating_agent_names: dict[str, str] = Field(default_factory=dict)
# 当前坐席是否为协作坐席(非主责)
is_collaborator: bool = Field(default=False)
```
### 2.3 新增 API 端点
```python
# backend/app/api/conversations.py
# POST /api/conversations/{id}/invite
# 坐席A邀请坐席B加入协作
@router.post("/conversations/{conversation_id}/invite")
async def invite_collaborator(
conversation_id: UUID,
body: ConversationInvite,
db: AsyncSession = Depends(get_db),
current_agent: Agent = Depends(get_current_agent),
):
"""
邀请另一个坐席加入会话协作。
校验规则:
1. 当前坐席必须是主责坐席或已加入的协作坐席
2. 被邀请坐席存在且在线
3. 被邀请坐席不是主责坐席,也不在协作列表中(防止重复邀请)
4. 会话状态必须为 serving(已结单的不能摇人)
副作用:
- WebSocket 推送给被邀请坐席
- 企微消息通知被邀请坐席
"""
pass
# POST /api/conversations/{id}/leave
# 坐席B退出协作
@router.post("/conversations/{conversation_id}/leave")
async def leave_collaboration(
conversation_id: UUID,
db: AsyncSession = Depends(get_db),
current_agent: Agent = Depends(get_current_agent),
):
"""
坐席退出协作。
校验规则:
1. 当前坐席必须在协作列表中
2. 当前坐席不能是主责坐席(主责坐席不能"退出",只能转接或结单)
副作用:
- WebSocket 广播会话更新
"""
pass
```
### 2.4 SessionService 新增方法
```python
# backend/app/services/session_service.py
async def invite_collaborator(
self,
conversation_id: UUID,
inviter_agent_id: str,
invitee_agent_id: str,
) -> Conversation:
"""邀请坐席加入协作。
流程:
1. 校验:会话存在且为 serving
2. 校验:邀请人在主责或协作列表中
3. 校验:被邀请人不在主责和协作列表中
4. 校验:被邀请人在线
5. 将被邀请人加入 collaborating_agent_ids
6. (可选)企微通知被邀请人
7. WS 广播 + 定向推送
"""
async def leave_collaboration(
self,
conversation_id: UUID,
agent_id: str,
) -> Conversation:
"""退出协作。
流程:
1. 校验:坐席在协作列表中
2. 从 collaborating_agent_ids 中移除
3. WS 广播
"""
```
### 2.5 会话列表接口改动
```python
# GET /api/conversations — 增加 is_collaborator 和 collaborating_agent_names
# 原来:
conv_data["is_mine"] = conv.assigned_agent_id == current_agent.user_id
conv_data["can_grab"] = (...)
# 新增:
conv_data["is_collaborator"] = (
current_agent.user_id in conv.collaborating_agent_ids
and conv.assigned_agent_id != current_agent.user_id
)
# 协作坐席姓名映射(需要批量查询坐席表)
collab_agent_ids = conv.collaborating_agent_ids or []
conv_data["collaborating_agent_ids"] = collab_agent_ids
conv_data["collaborating_agent_names"] = {
aid: agent_name_map.get(aid, "未知") for aid in collab_agent_ids
}
```
### 2.6 WebSocket 事件定义
| 事件类型 | 推送范围 | 数据 |
|---------|---------|------|
| `collaborator_invited` | 被邀请人(定向)+ 所有在线坐席(广播) | `{ conversation_id, inviter_id, invitee_id, inviter_name }` |
| `collaborator_joined` | 所有在线坐席(广播) | `{ conversation_id, agent_id, agent_name }` |
| `collaborator_left` | 所有在线坐席(广播) | `{ conversation_id, agent_id, agent_name }` |
### 2.7 企微通知(可选增强)
被邀请时发送企微卡片消息:
```
┌─────────────────────────────┐
│ 🔔 摇人邀请 │
│ │
│ 坐席A 邀请你协助处理会话 │
│ 员工:张三 │
│ 问题:打印机连接失败 │
│ │
│ [点击查看] │
└─────────────────────────────┘
```
---
## 三、前端实现(坐席工作台)
### 3.1 API 层新增
```typescript
// frontend-agent/src/api/conversation.ts
/** 邀请坐席协作 */
export function inviteCollaborator(
conversationId: string,
agentId: string
): Promise<Conversation>
/** 退出协作 */
export function leaveCollaboration(
conversationId: string
): Promise<Conversation>
```
### 3.2 Store 改动
```typescript
// frontend-agent/src/stores/conversation.ts
// 新增计算属性:协作会话(我是协作者但不是主责的会话)
const collaboratingConversations = computed(() => {
return sortedConversations.value.filter(
c => c.is_collaborator && c.status === 'serving'
)
})
// 新增方法
async function inviteCollaborator(convId: string, agentId: string): Promise<void>
async function leaveCollaboration(convId: string): Promise<void>
// WS 事件处理
function handleCollaboratorInvited(data: {...}): void // 弹出通知
function handleCollaboratorJoined(data: {...}): void // 刷新列表
function handleCollaboratorLeft(data: {...}): void // 刷新列表
```
### 3.3 ConversationList.vue 改动
```vue
<!-- 新增协作会话排在我的会话其他坐席会话之间 -->
<template v-if="filteredCollaborating.length > 0">
<div class="section-title">
<span>🤝 协作会话 ({{ filteredCollaborating.length }})</span>
</div>
<ConversationItem
v-for="conv in filteredCollaborating"
:key="conv.id"
:conversation="conv"
:active="conv.id === conversationStore.currentConversationId"
:show-leave="true" <!-- 新增退出按钮 -->
@click="conversationStore.selectConversation(conv.id)"
@leave="handleLeave(conv)"
/>
</template>
```
### 3.4 新增:摇人弹窗组件
```
┌──────────────────────────────────┐
│ 摇人 — 邀请坐席协作 │
│ │
│ 🔍 [搜索坐席姓名...] │
│ │
│ ┌──────────────────────────────┐│
│ │ ○ 张三 在线 负载 2/5 ││
│ │ ○ 李四 在线 负载 1/5 (推荐)││
│ │ ○ 王五 忙碌 负载 5/5 ││
│ └──────────────────────────────┘│
│ │
│ 已选:李四 │
│ │
│ [取消] [确认邀请] │
└──────────────────────────────────┘
```
### 3.5 会话详情区域改动
在会话详情的头部工具栏(自己的会话或协作的会话)增加「摇人」按钮:
```
┌──────────────────────────────────────────┐
│ 👤 张三 · 技术部 [摇人] [⋮] │ ← 工具栏
│ 状态:服务中 | 主责:坐席A | 协作:坐席B │ ← 协作信息
└──────────────────────────────────────────┘
```
### 3.6 WebSocket 事件处理改动
```typescript
// frontend-agent/src/composables/useWebSocket.ts
case 'collaborator_invited':
// 如果被邀请的是当前坐席,弹出通知
if (msg.data?.invitee_id === agentStore.userId) {
ElNotification({
title: '摇人邀请',
message: `${msg.data.inviter_name} 邀请你协助处理会话`,
type: 'info',
duration: 0, // 不自动关闭
onClick: () => {
conversationStore.selectConversation(msg.data.conversation_id)
}
})
}
conversationStore.fetchConversations()
break
case 'collaborator_joined':
case 'collaborator_left':
conversationStore.fetchConversations()
break
```
---
## 四、改动清单汇总
| 文件 | 改动类型 | 说明 |
|------|---------|------|
| `backend/app/models/conversation.py` | 修改 | +`collaborating_agent_ids` 字段 |
| `backend/app/schemas/conversation.py` | 修改 | +`ConversationInvite`、响应扩展字段 |
| `backend/app/api/conversations.py` | 修改 | +`invite`/`leave` 两个端点,列表接口扩展 |
| `backend/app/services/session_service.py` | 修改 | +`invite_collaborator`/`leave_collaboration` |
| `frontend-agent/src/api/conversation.ts` | 修改 | +2 个 API 函数 |
| `frontend-agent/src/stores/conversation.ts` | 修改 | +计算属性、方法、WS 处理 |
| `frontend-agent/src/components/conversation/ConversationList.vue` | 修改 | +协作会话区 |
| `frontend-agent/src/components/conversation/ConversationItem.vue` | 修改 | +退出按钮 |
| `frontend-agent/src/components/conversation/InviteDialog.vue` | **新建** | 摇人选人弹窗 |
| `frontend-agent/src/composables/useWebSocket.ts` | 修改 | +3 个 WS 事件处理 |
| 数据库迁移 SQL | **新建** | `ALTER TABLE` 加列 |
---
## 五、开发顺序
| 步骤 | 内容 | 依赖 |
|------|------|------|
| 1 | 模型 + 迁移 SQL | 无 |
| 2 | Schema + SessionService | 1 |
| 3 | API 端点(invite/leave/列表扩展) | 2 |
| 4 | 后端测试 | 3 |
| 5 | 前端 API 层 + Store | 无(可并行) |
| 6 | 摇人弹窗组件 | 5 |
| 7 | ConversationList 改动 | 5, 6 |
| 8 | WebSocket 事件处理 | 3 |
| 9 | 端到端集成测试 | 全部 |
---
## 六、设计决策
| 决策 | 理由 |
|------|------|
| 协作坐席不增加 `current_load` | 协作是轻量参与,不影响坐席接单能力 |
| 协作坐席不能结单/转接 | 避免多人操作冲突,只有主责坐席有权关闭会话 |
| 使用 JSON 数组而非关联表 | 协作人数少(1-3人),JSON 查询足够;参考现有 `tags` 字段设计 |
| WS 广播 + 定向推送双通道 | 广播让其他人看到协作关系变化,定向推送确保被邀请人收到通知 |
@@ -0,0 +1,535 @@
# 消息功能详细方案
> **版本**: v1.0
> **日期**: 2026-06-14
> **优先级**: P0 - 最高优先级
---
## 一、现状与目标
### 1.1 当前问题
| 问题 | 影响 | 优先级 |
|------|------|--------|
| 3秒轮询,实时性差 | 用户体验差 | P0 |
| 无消息状态 | 不知道是否送达 | P0 |
| 无表情回应 | 交互单调 | P1 |
| 无截图功能 | 无法快速上报问题 | P1 |
| 媒体处理耦合企微 | 3天失效风险 | P0 |
### 1.2 目标
```
┌─────────────────────────────────────────────────────────┐
│ 消息功能 V2 目标 │
├─────────────────────────────────────────────────────────┤
│ ✅ 实时性: WebSocket 推送,毫秒级响应 │
│ ✅ 消息状态: sent→delivered→read │
│ ✅ 表情回应: emoji reactions │
│ ✅ 截图上传: 屏幕截图快速上报 │
│ ✅ 媒体独立: 本地存储,解耦企微 │
│ ✅ 已读回执: 双向可见 │
└─────────────────────────────────────────────────────────┘
```
---
## 二、架构设计
### 2.1 技术选型
| 组件 | 选型 | 说明 |
|------|------|------|
| 实时通信 | WebSocket | 已有基础(ws_manager |
| 消息状态 | Redis Key-Event | 轻量实现 |
| 媒体存储 | 本地文件系统 + NAS | 解耦企微 |
| 截图工具 | html2canvas + 粘贴 | 浏览器原生 |
### 2.2 系统架构
```
┌─────────────────┐
│ WebSocket │
│ 实时推送 │
└───────┬─────────┘
┌───────────────────┼───────────────────┐
▼ ▼ ▼
┌─────────┐ ┌─────────┐ ┌─────────┐
│H5用户端 │ │坐席工作台│ │管理后台 │
└────┬────┘ └────┬────┘ └────┬────┘
│ │ │
└───────────────────┼───────────────────┘
┌───────────────────────┐
│ 后端 WebSocket │
│ ws_manager │
└───────────┬───────────┘
┌────────────────┼────────────────┐
▼ ▼ ▼
┌──────────┐ ┌──────────┐ ┌──────────┐
│消息状态 │ │媒体存储 │ │事件广播 │
│Redis │ │本地/NAS │ │Channel │
└──────────┘ └─────────┘ └──────────┘
```
### 2.3 数据流
```
用户A发送消息
POST /messages (创建消息,status=sent)
WebSocket 广播 new_message 给用户B
├── 用户B收到 → status=delivered
├── 用户B读取 → status=read + 已读回执
└── 用户A收到回执 → 更新消息状态
```
---
## 三、消息模型扩展
### 3.1 新增字段
```python
# backend/app/models/message.py 新增
class Message(Base):
# ... 现有字段 ...
# --------------------------------------------------------------------------
# V2 新增字段
# --------------------------------------------------------------------------
# 消息状态(V2新增)
# sent: 已发送
# delivered: 已送达(对方收到)
# read: 已读
message_status: Mapped[str] = mapped_column(
String(20),
nullable=False,
default="sent",
comment="消息状态: sent/delivered/read",
)
# 表情回应(V2新增)
# 存储格式: {"👍": "user_id", "👎": "user_id", "😊": "user_id"}
# 每个用户只能对同一消息添加一个表情
reactions: Mapped[Optional[Dict[str, str]]] = mapped_column(
JSON,
nullable=True,
default=None,
comment="表情回应: {emoji: user_id}",
)
# 消息来源设备(V2新增)
# mobile: 手机端发送
# desktop: 桌面端发送
device_type: Mapped[str] = mapped_column(
String(20),
nullable=False,
default="desktop",
comment="设备类型: mobile/desktop",
)
# 已读用户列表(V2新增)
# 存储已读该消息的用户ID列表
read_by: Mapped[Optional[List[str]]] = mapped_column(
JSON,
nullable=True,
default=None,
comment="已读用户列表",
)
```
### 3.2 DDL
```sql
-- 消息模型 V2 DDL
ALTER TABLE messages
ADD COLUMN message_status VARCHAR(20) NOT NULL DEFAULT 'sent',
ADD COLUMN reactions JSON,
ADD COLUMN device_type VARCHAR(20) NOT NULL DEFAULT 'desktop',
ADD COLUMN read_by JSON;
-- 新增索引
CREATE INDEX idx_messages_status ON messages(message_status);
CREATE INDEX idx_messages_conversation_status ON messages(conversation_id, message_status);
```
---
## 四、API 设计
### 4.1 现有 API(保持兼容)
| 端点 | 方法 | 状态 |
|------|------|------|
| `/messages` | GET | ✅ 兼容 |
| `/messages` | POST | ✅ 兼容 |
### 4.2 新增 API
| 端点 | 方法 | 功能 |
|------|------|------|
| `/messages/{id}/status` | PATCH | 更新消息状态 |
| `/messages/{id}/reactions` | POST | 添加表情回应 |
| `/messages/{id}/reactions` | DELETE | 移除表情回应 |
| `/messages/poll` | GET | 轮询(保留兼容) |
### 4.3 API 详情
#### 4.3.1 更新消息状态
```http
PATCH /api/messages/{id}/status
Content-Type: application/json
Request:
{
"status": "delivered" | "read"
}
Response:
{
"id": "uuid",
"message_status": "delivered" | "read",
"read_by": ["user_id_1", "user_id_2"]
}
```
#### 4.3.2 添加表情回应
```http
POST /api/messages/{id}/reactions
Content-Type: application/json
Request:
{
"emoji": "👍" // emoji Unicode
}
Response:
{
"id": "uuid",
"reactions": {
"👍": "user_id_1",
"😊": "user_id_2"
}
}
```
#### 4.3.3 移除表情回应
```http
DELETE /api/messages/{id}/reactions
Response:
{
"id": "uuid",
"reactions": {
"😊": "user_id_2"
}
}
```
---
## 五、WebSocket 事件
### 5.1 现有事件(保持)
| 事件名 | 方向 | 说明 |
|--------|------|------|
| `new_message` | Server→Client | 新消息 |
| `conversation_updated` | Server→Client | 会话更新 |
### 5.2 新增事件
| 事件名 | 方向 | 说明 |
|--------|------|------|
| `message_status_changed` | Server→Client | 消息状态变更 |
| `reaction_added` | Server→Client | 表情回应添加 |
| `reaction_removed` | Server→Client | 表情回应移除 |
| `typing` | Client→Server | 对方正在输入 |
| `typing` | Server→Client | 对方正在输入通知 |
### 5.3 事件格式
#### 5.3.1 消息状态变更
```json
{
"event": "message_status_changed",
"data": {
"message_id": "uuid",
"status": "delivered",
"changed_by": "user_id",
"timestamp": "2026-06-14T11:30:00Z"
}
}
```
#### 5.3.2 表情回应
```json
{
"event": "reaction_added",
"data": {
"message_id": "uuid",
"emoji": "👍",
"user_id": "user_id",
"user_name": "张三"
}
}
```
#### 5.3.3 Typing 通知
```json
{
"event": "typing",
"data": {
"conversation_id": "uuid",
"user_id": "user_id",
"user_name": "张三",
"is_typing": true
}
}
```
---
## 六、媒体处理(截图/图片/文件)
### 6.1 架构
```
┌─────────────────────────────────────────────────────────────┐
│ 媒体处理架构 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 用户上传 ──▶ 前端压缩/裁剪 ──▶ 上传API ──▶ 本地存储 │
│ │ │ │
│ ▼ ▼ │
│ 生成缩略图 返回 media_url │
│ │ │ │
│ ▼ ▼ │
│ 消息内容引用 media_url │
│ │
└─────────────────────────────────────────────────────────────┘
```
### 6.2 上传流程
```python
# backend/app/api/upload.py
@router.post("/upload", dependencies=[require_auth])
async def upload_media(
file: UploadFile,
file_type: str = Form(...), # image/file/screenshot
):
"""媒体文件上传"""
# 1. 验证文件类型
allowed_types = {
"image": ["image/jpeg", "image/png", "image/gif", "image/webp"],
"file": ["application/pdf", "application/msword",
"application/vnd.openxmlformats-officedocument.wordprocessingml.document"],
"screenshot": ["image/png", "image/webp"],
}
if file.content_type not in allowed_types.get(file_type, []):
raise HTTPException(400, "不支持的文件类型")
# 2. 验证文件大小(10MB
if file.size > 10 * 1024 * 1024:
raise HTTPException(400, "文件大小不能超过10MB")
# 3. 生成存储路径
date_str = datetime.now().strftime("%Y/%m/%d")
file_ext = Path(file.filename).suffix
unique_name = f"{uuid.uuid4()}{file_ext}"
relative_path = f"/media/{date_str}/{unique_name}"
# 4. 保存到本地
upload_dir = Path(settings.MEDIA_UPLOAD_DIR) / date_str
upload_dir.mkdir(parents=True, exist_ok=True)
file_path = upload_dir / unique_name
content = await file.read()
file_path.write_bytes(content)
# 5. 生成缩略图(图片)
thumbnail_url = None
if file_type == "image" or file_type == "screenshot":
thumbnail_url = await generate_thumbnail(file_path, unique_name)
return {
"media_url": relative_path,
"thumbnail_url": thumbnail_url,
"file_size": len(content),
"file_name": file.filename,
}
```
### 6.3 截图功能
```javascript
// frontend-h5/src/components/ChatInput.vue
<script setup>
import { ref } from 'vue'
const handlePaste = async (event) => {
const items = event.clipboardData?.items
if (!items) return
for (const item of items) {
if (item.type.startsWith('image/')) {
const blob = item.getAsFile()
if (blob) {
await uploadScreenshot(blob)
}
}
}
}
const uploadScreenshot = async (blob) => {
const formData = new FormData()
formData.append('file', blob, 'screenshot.png')
formData.append('file_type', 'screenshot')
const response = await fetch('/api/upload', {
method: 'POST',
body: formData,
})
const data = await response.json()
emit('image-uploaded', data.media_url)
}
</script>
<template>
<div @paste="handlePaste">
<!-- 输入框区域 -->
</div>
</template>
```
---
## 七、前端交互设计
### 7.1 消息卡片(V2
```
┌────────────────────────────────────────────────────┐
│ 👤 张三 11:30 ✓✓ 已读 │
│ │
│ 这是消息内容... │
│ │
│ ┌─────┐ │
│ │图片 │ ← 点击可预览 │
│ └─────┘ │
│ │
│ 👍👎😊 ← 表情回应(点击选择) │
└────────────────────────────────────────────────────┘
```
### 7.2 表情选择器
```
┌──────────────────────────┐
│ 👍 👎 😊 😂 😢 😡 ❤️ 🔥 │
│ │
│ [自定义表情...] │
└──────────────────────────┘
```
### 7.3 截图快捷键
| 平台 | 快捷键 |
|------|--------|
| Windows | `Win + Shift + S` / `Ctrl + V` 粘贴 |
| macOS | `Cmd + Shift + 4` / `Cmd + V` 粘贴 |
---
## 八、实施计划
### 8.1 任务拆分
| 序号 | 任务 | 工作量 | 依赖 |
|------|------|--------|------|
| T1 | 消息模型扩展 | 1d | - |
| T2 | 媒体上传API | 2d | T1 |
| T3 | WebSocket事件 | 1d | - |
| T4 | 消息状态API | 1d | T1 |
| T5 | 表情回应API | 1d | T1 |
| T6 | 坐席端V2 | 2d | T3,T4,T5 |
| T7 | H5端V2 | 2d | T2,T3,T4,T5 |
| T8 | 截图功能 | 1d | T2 |
| T9 | 联调测试 | 2d | T6,T7,T8 |
### 8.2 时间估算
```
总工期: 12 工作日
Week 1: ████████░░░░░░░░░░
模型+API+WS (5d)
Week 2: ░░░░░░░████████░░░
前端+截图 (5d)
Week 3: ░░░░░░░░░░░░████
联调测试 (2d)
```
---
## 九、兼容性
### 9.1 向后兼容
| 场景 | 处理 |
|------|------|
| 旧客户端连接 | 消息状态字段有默认值,不影响 |
| 轮询仍然工作 | 保留 `/messages/poll` 兼容 |
| 媒体未迁移 | 企微MediaID仍然可用 |
### 9.2 降级策略
| 故障场景 | 降级方案 |
|----------|----------|
| WS连接失败 | 降级到轮询 |
| 媒体上传失败 | 提示用户重试 |
| 表情功能不可用 | 隐藏表情按钮 |
---
## 十、待确认事项
- [ ] 媒体存储路径(本地 vs NAS
- [ ] 文件大小限制(当前10MB
- [ ] 支持的截图快捷键
- [ ] 表情包自定义权限
---
## 附录
### A. Emoji 列表(默认支持)
```
常用: 👍 👎 😊 😂 😢 😡 ❤️ 🔥 👏 🎉 😎
```
@@ -0,0 +1,407 @@
# 邀请功能 — 技术方案
> **场景**:坐席在处理会话时需要拉入其他员工/部门协助,通过邀请功能将新人加入同一会话。
>
> **与"摇人"的区别**:摇人是坐席→坐席的协作(`collaborating_agent_ids`),邀请是坐席→任意员工/部门的协作(`participants`)。
---
## 一、方案决策记录
### 1.1 方案选型(2026-06-10 确认)
| 方案 | 核心思路 | 可行性 | 结论 |
|------|---------|--------|------|
| 方案一:一对一+邀请 | 在现有会话扩展参与者,企微应用消息通知 | ✅ 可行 | 备选 |
| 方案二:应用群聊 | 企微 `appchat` 创建群,群内沟通 | ❌ 应用无法接收群内消息 | 不可行 |
| **方案三:WebSocket+应用消息双通道** | 后端维护 `participants`,WebSocket 通信,企微消息仅通知 | ✅ 零新增基础设施 | **采纳** |
**方案二不可行原因**:企微 `appchat` 是「应用推送消息群」,群成员在群内发言**不会回调给应用**。应用只能单向推送消息到群,无法看到用户回复,坐席工作台无法获取群内对话。
---
## 二、数据模型改动
### 2.1 Conversation 模型新增字段
```python
# backend/app/models/conversation.py
# 会话参与者列表(JSON 数组)
# 与 collaborating_agent_ids 的区别:
# - collaborating_agent_ids:被邀请来协助的坐席ID(坐席间协作)
# - participants:被邀请加入会话的员工/部门成员(跨端协作)
# 每个 participant 包含:userid, name, department, joined_at, role, invited_by
participants: Mapped[list] = mapped_column(
JSON,
nullable=False,
default=list,
comment="会话参与者列表",
)
```
### 2.2 participants 字段结构
```json
[
{
"userid": "zhangsan",
"name": "张三",
"department": "技术部/网络组",
"role": "invited", // "invited"=被邀请人, "owner"=原始员工
"invited_by": "agent_001", // 邀请人的坐席ID
"joined_at": "2026-06-10T14:30:00Z",
"history_shared": "last_10", // "all" / "last_10" / "none"
"status": "active" // "active" / "left"
}
]
```
### 2.3 数据库迁移 SQL
```sql
-- 开发环境 SQLite / 生产环境 PostgreSQL 通用
ALTER TABLE conversations ADD COLUMN participants JSON NOT NULL DEFAULT '[]';
```
### 2.4 权限矩阵
```
原始员工 主责坐席 协作坐席 被邀请人
查看消息 ✅ ✅ ✅ ✅
发送消息 ✅ ✅ ✅ ✅
邀请他人 ❌ ✅ ✅ ❌
结单 ❌ ✅ ❌ ❌
转接 ❌ ✅ ❌ ❌
退出会话 关闭页面 ❌(主责不可) ✅(退出协作) ✅
移除参与者 ❌ ✅ ❌ ❌
```
---
## 三、后端实现
### 3.1 新增 Schema
```python
# backend/app/schemas/conversation.py
class ConversationInviteRequest(BaseModel):
"""邀请请求"""
user_ids: list[str] = Field(..., description="被邀请人ID列表")
department_ids: list[str] = Field(default_factory=list, description="部门ID列表(整部门邀请)")
history_shared: str = Field("last_10", description="历史消息共享模式: all/last_10/none")
class ConversationInviteResponse(BaseModel):
"""邀请响应"""
conversation_id: UUID
invited_count: int = Field(..., description="成功邀请人数")
failed: list[dict] = Field(default_factory=list, description="邀请失败的用户及原因")
participants: list[dict] = Field(default_factory=list, description="更新后的参与者列表")
class ConversationLeaveRequest(BaseModel):
"""退出会话请求"""
pass
```
### 3.2 ConversationResponse 扩展字段
```python
class ConversationResponse(BaseModel):
# ... 现有字段 ...
# ----- 邀请功能扩展字段 -----
participants: list[dict] = Field(default_factory=list, description="参与者列表")
participant_count: int = Field(0, description="参与者人数")
```
### 3.3 新增 API 端点
```python
# backend/app/api/conversations.py
# POST /api/conversations/{id}/invite
# 坐席邀请员工/部门加入会话
@router.post("/conversations/{conversation_id}/invite")
async def invite_participants(
conversation_id: UUID,
body: ConversationInviteRequest,
db: AsyncSession = Depends(get_db),
current_agent: Agent = Depends(get_current_agent),
):
"""
邀请员工/部门加入会话。
校验规则:
1. 当前用户必须是主责坐席或协作坐席
2. 会话状态必须为 serving
3. 被邀请人不能已在 participants 中
4. 被邀请人不能是主责坐席
副作用:
1. 更新 conversation.participants
2. 企微应用消息通知被邀请人
3. WebSocket 广播系统消息
"""
# POST /api/conversations/{id}/leave
# 被邀请人退出会话
@router.post("/conversations/{conversation_id}/leave")
async def leave_conversation(
conversation_id: UUID,
db: AsyncSession = Depends(get_db),
current_user = Depends(get_current_user), # 可以是坐席或H5用户
):
"""
参与者退出会话。
校验规则:
1. 当前用户必须在 participants 中(role=invited
2. 主责坐席不能退出
3. 原始员工不能退出(关闭页面即视为离开)
副作用:
1. 更新 participant.status = "left"
2. WebSocket 广播系统消息
"""
# DELETE /api/conversations/{id}/participants/{userid}
# 坐席移除参与者
@router.delete("/conversations/{conversation_id}/participants/{userid}")
async def remove_participant(
conversation_id: UUID,
userid: str,
db: AsyncSession = Depends(get_db),
current_agent: Agent = Depends(get_current_agent),
):
"""
主责坐席移除会话参与者。
校验规则:
1. 当前用户必须是主责坐席
2. 被移除人必须在 participants 中且 status=active
副作用:
1. 更新 participant.status = "left"
2. WebSocket 广播系统消息
"""
```
### 3.4 企微通知卡片消息
邀请时发送企微 template_card 卡片消息:
```python
# backend/app/services/wecom_service.py
async def send_invite_card(
self,
invitee_userid: str,
inviter_name: str,
employee_name: str,
problem_summary: str,
conversation_id: str,
) -> None:
"""发送邀请卡片消息给被邀请人"""
card = {
"msgtype": "template_card",
"template_card": {
"card_type": "button_interaction",
"source": {
"desc": "智能IT支持服务台"
},
"main_title": {
"title": "🔔 会话邀请"
},
"emphasis_content": {
"title": f"{inviter_name} 邀请你协助处理",
"desc": f"员工:{employee_name}"
},
"sub_title_text": f"问题:{problem_summary[:50]}",
"button_list": [
{
"text": "加入会话",
"style": 1, # 蓝色主按钮
"key": f"join_{conversation_id}"
},
{
"text": "稍后查看",
"style": 2, # 灰色次按钮
"key": "later"
}
]
}
}
```
### 3.5 WebSocket 事件定义
| 事件类型 | 推送范围 | 数据 |
|---------|---------|------|
| `participant_invited` | 所有在线坐席 + 会话内H5用户 | `{ conversation_id, invited_by, participants: [{userid, name, role}] }` |
| `participant_joined` | 所有在线坐席 + 会话内H5用户 | `{ conversation_id, userid, name }` |
| `participant_left` | 所有在线坐席 + 会话内H5用户 | `{ conversation_id, userid, name, reason: "self_left"/"removed" }` |
| `participant_removed` | 所有在线坐席 + 被移除人 | `{ conversation_id, userid, name, removed_by }` |
### 3.6 历史消息共享逻辑
```python
# backend/app/services/session_service.py
async def get_shared_messages(
self,
conversation_id: UUID,
history_shared: str, # "all" / "last_10" / "none"
) -> list[dict]:
"""根据共享模式返回历史消息"""
if history_shared == "none":
return []
messages = await self._get_conversation_messages(conversation_id)
if history_shared == "last_10":
# 取最近10条,优先包含人工消息
return messages[-10:]
# "all" — 返回全部
return messages
```
---
## 四、前端实现
### 4.1 坐席工作台(Agent
#### 4.1.1 API 层新增
```typescript
// frontend-agent/src/api/conversation.ts
/** 邀请员工/部门加入会话 */
export function inviteParticipants(
conversationId: string,
data: { user_ids: string[]; department_ids?: string[]; history_shared?: string }
): Promise<ConversationInviteResponse>
/** 退出会话 */
export function leaveConversation(conversationId: string): Promise<void>
/** 移除参与者 */
export function removeParticipant(conversationId: string, userid: string): Promise<void>
```
#### 4.1.2 邀请弹窗组件
```
┌──────────────────────────────────────────┐
│ 邀请加入会话 │
│ │
│ 🔍 [搜索姓名/工号...] │
│ │
│ ┌── 组织架构 ──┐ ┌── 已选 (3人) ──────┐│
│ │ ▼ 技术部 │ │ × 张三 / 网络组 ││
│ │ ☑ 网络组 │ │ × 李四 / 运维组 ││
│ │ ○ 运维组 │ │ × 王五 / 安全组 ││
│ │ ▼ 行政部 │ └────────────────────┘│
│ │ ○ 前台 │ │
│ └──────────────┘ │
│ │
│ 历史消息共享: │
│ ○ 全部 ● 最近10条 ○ 不共享 │
│ │
│ [取消] [确认邀请] │
└──────────────────────────────────────────┘
```
#### 4.1.3 参与者面板(聊天区头部)
```
┌──────────────────────────────────────────────────┐
│ 👤 张三 · 网络问题 [+ 邀请] [⋮] │
│ 👥 3人参与: 我(坐席) · 张三(员工) · 李四(受邀) │ ← 可点击展开
└──────────────────────────────────────────────────┘
```
### 4.2 H5用户端(被邀请人视角)
#### 4.2.1 加入会话流程
```
点击企微通知 → H5加载 → Mock登录/自动登录 → 加载会话 → 拉取历史 → WebSocket连接 → 可发送消息
```
#### 4.2.2 参与者标识
```
┌──────────────────────────────────────────┐
│ IT支持会话 [退出] │
│ 👥 参与者: 坐席(小宋) · 你 · 张三(网络组) │
├──────────────────────────────────────────┤
│ │
│ [系统] 李四(坐席)邀请你加入会话 │
│ [系统] 你已加入会话 │
│ [坐席] 网络组的同事来看下这个VPN问题 │
│ [张三] 我看下,是零信任客户端连不上对吧 │
│ │
├──────────────────────────────────────────┤
│ [输入消息...] [发送] │
└──────────────────────────────────────────┘
```
---
## 五、改动清单汇总
| 文件 | 改动类型 | 说明 |
|------|---------|------|
| `backend/app/models/conversation.py` | 修改 | +`participants` 字段 |
| `backend/app/schemas/conversation.py` | 修改 | +邀请/退出/移除Schema + 响应扩展字段 |
| `backend/app/api/conversations.py` | 修改 | +`invite`/`leave`/`remove_participant` 三个端点 |
| `backend/app/services/session_service.py` | 修改 | +邀请/退出/历史共享逻辑 |
| `backend/app/services/wecom_service.py` | 修改 | +`send_invite_card` 卡片消息 |
| `frontend-agent/src/api/conversation.ts` | 修改 | +3个API函数 |
| `frontend-agent/src/stores/conversation.ts` | 修改 | +participants相关计算属性和方法 |
| `frontend-agent/src/components/conversation/InviteDialog.vue` | **新建** | 邀请弹窗组件 |
| `frontend-agent/src/components/conversation/ParticipantBar.vue` | **新建** | 参与者面板组件 |
| `frontend-h5/src/components/chat/ParticipantList.vue` | **新建** | H5参与者列表组件 |
| `frontend-h5/src/views/ChatView.vue` | 修改 | +退出按钮 + 参与者展示 |
| `frontend-h5/src/api/conversation.ts` | 修改 | +退出会话API |
| `frontend-agent/src/composables/useWebSocket.ts` | 修改 | +4个WS事件处理 |
| `frontend-h5/src/composables/useWebSocket.ts` | 修改 | +4个WS事件处理 |
| 数据库迁移 SQL | **新建** | `ALTER TABLE` 加列 |
---
## 六、开发顺序
| 步骤 | 内容 | 依赖 | 预计工时 |
|------|------|------|---------|
| 1 | 模型 + 迁移 SQL + participants字段 | 无 | 0.5天 |
| 2 | Schema + SessionService邀请逻辑 | 1 | 1天 |
| 3 | API端点(invite/leave/remove + 历史共享 | 2 | 1天 |
| 4 | 企微template_card消息发送 | 2 | 0.5天 |
| 5 | 后端测试 | 3 | 0.5天 |
| 6 | 坐席端 InviteDialog + ParticipantBar 组件 | 无(可并行) | 1.5天 |
| 7 | H5端 ChatView改动 + ParticipantList | 无(可并行) | 1天 |
| 8 | WebSocket 事件处理(双端) | 3 | 0.5天 |
| 9 | 端到端集成测试 | 全部 | 1天 |
| **合计** | | | **7-8天** |
---
## 七、设计决策
| 决策 | 理由 |
|------|------|
| 使用 `participants` JSON 数组而非关联表 | 参与者数量少(1-5人),JSON 查询足够;与 `collaborating_agent_ids` 设计一致 |
| 历史消息默认共享最近10条 | 避免被邀请人被大量无关历史淹没,同时保留足够上下文理解问题 |
| 被邀请人不能二次邀请 | 防止邀请链失控,只有坐席有权管理参与者 |
| 不使用企微 appchat 群聊 | appchat 群内消息不会回调给应用,坐席无法获取群内对话 |
| 企微通知用 template_card 而非 text | 卡片消息提供「加入会话」按钮,体验优于纯文本+手动复制链接 |
| 不设邀请人数硬上限 | 低频场景(1-3人常见),>10人弹窗提醒而非阻断 |
@@ -0,0 +1,127 @@
# H5用户端右侧栏动态推送评估
评估日期:2026-06-11
## 评估对象
用户端右侧栏不再提供静态标签导航和资源列表,而是基于会话上下文和第三方集成数据触发阈值,动态推送相关问题答疑、流程审批、软件下载等资源卡片。
本质:从"人找资源"到"资源找人"的范式转换。
---
## 一、正面价值
### 1. 符合系统定位——"AI驱动"
系统全名是"智能IT支持服务台 — AI驱动",但当前右侧栏本质是传统信息架构(标签页+列表),AI只在左侧会话区参与。动态推送让右侧也变成AI能力的延伸,整个产品才能名副其实。
### 2. 降低用户认知负荷
传统模式下,用户遇到VPN问题,需要自己点"常用资源"→翻到VPN分类→找对应文档。动态推送模式下,系统识别到会话提到VPN,右侧自动出现VPN连接指南、aTrust下载链接——用户零步触达。
### 3. 提升首问解决率
很多IT支持请求本质是"信息差"——用户不知道流程怎么走、软件去哪下、密码怎么改。主动推送填补信息差,用户可能都不需要和坐席对话就解决了。
### 4. 数据闭环价值
推送了什么、用户点了什么、是否解决问题——这些行为数据反过来可以优化推送准确度和知识库质量,形成"推送→反馈→优化"的飞轮。
### 5. 与第三方集成天然契合
联软查到某员工终端有高危漏洞→右侧推送修复指南;aTrust检测到VPN异常→右侧推送重连步骤。这种场景下,动态推送比静态列表的体验差距是量级性的。
---
## 二、风险与挑战
### 1. 冷启动问题——首条消息前,右侧是空的
用户刚进入会话,还没说话,系统没有任何上下文。此时右侧要么空白(体验差),要么需要兜底策略(猜用户可能需要什么)。静态列表没有这个问题,任何时候都有内容可看。
### 2. 准确性依赖——推错了比不推更糟
用户问"VPN连不上",系统推了aTrust下载链接,但实际是密码过期问题——错误推送不仅没帮到忙,还可能误导用户走弯路,增加后续排查成本。传统列表虽然效率低,但至少不会误导。
### 3. 可发现性丧失——用户无法主动探索
有些资源用户不知道存在,只有浏览列表时才会发现"原来还有这个工具"。动态推送只推系统认为相关的,存在"你不知道你不知道"的信息茧房风险。
### 4. 技术实现成本高
需要:意图识别引擎→规则引擎/阈值系统→第三方数据实时对接→推送排序算法→用户反馈收集。这套体系远比静态列表复杂,且需要持续调优。
### 5. 用户信任建立周期长
如果早期推送不准,用户会养成"忽略右侧栏"的习惯,一旦习惯形成,后续推送再准也没用了。第一印象决定成败。
### 6. 多意图场景处理难
"我VPN连不上,另外帮我看看电脑上有没有装火绒"——一个会话包含两个问题,右侧推什么?两套资源都推会显得杂乱,推一套又遗漏另一个。
---
## 三、对比分析
| 维度 | 静态列表(当前方案) | 动态推送(提议方案) |
|------|-------------------|-------------------|
| 认知负荷 | 高(需自行查找) | 低(自动呈现) |
| 准确性 | 无(用户自己判断) | 依赖AI准确度 |
| 冷启动 | 有内容 | 需兜底策略 |
| 可发现性 | 好(可浏览) | 差(只能看推的) |
| 实现成本 | 低 | 高 |
| 与第三方集成契合度 | 低(无法感知外部数据) | 高(实时响应) |
| 用户信任 | 高(所见即所得) | 需积累 |
| 个性化程度 | 无 | 高 |
---
## 四、专业建议:混合架构
两条路线单独走都有明显短板。建议采用"动态为主、静态兜底"的混合模式。
### 具体方案
右侧栏分两个区域,上下排列:
上方(占70%):AI动态推送区
- 基于会话上下文+第三方数据触发,显示资源卡片
- 卡片类型:问题答疑卡、流程指引卡、软件下载卡、状态通知卡
- 有推送时显示,没有推送时不占空间
- 用户可点"不相关"反馈,用于优化推送
下方(占30%):常用资源兜底区
- 收起式面板,默认折叠,点击展开
- 保留最常用的3-5个入口(密码重置、软件下载、VPN指南、IT制度)
- 满足冷启动和主动探索需求
### 分阶段实施路径
| 阶段 | 右侧栏形态 | 说明 |
|------|-----------|------|
| 阶段一(当前) | 静态列表 + 少量AI提示 | 低成本上线,右侧保留3个标签页,AI推送区仅做关键词匹配 |
| 阶段二 | 混合模式 | 上方AI推送区基于规则引擎+第三方数据触发,下方常用资源折叠 |
| 阶段三 | AI为主、静态兜底 | 引入意图识别,推送准确度达标后,动态推送成主体,静态降级为兜底 |
### 关键验证指标
动态推送是否值得扩大投入,看两个数据:
1. 点击率——推送卡片的点击/曝光比,>15%说明推送有价值
2. 自助解决率——用户点了推送资源后未发起坐席对话的比例,>10%说明推送有效减少了人工负担
两个指标不达标,就别急着扩大动态推送的比例。
---
## 五、结论
| 维度 | 判断 |
|------|------|
| 方向 | 正确,符合AI驱动定位和用户期望 |
| 时机 | 阶段一不急,阶段二再实质性投入 |
| 风险 | 全盘替代有冷启动和准确性风险 |
| 建议 | 混合架构,数据验证后再扩大动态推送占比 |
核心观点:想法是对的,但别一步到位。先让静态列表跑起来,用数据证明动态推送真的比用户自己找更高效,再逐步替代。
@@ -0,0 +1,312 @@
# JP-webcli 自动化部署工具 — 能力、限制与优化空间分析
> 分析时间:2026-06-25 | 基于 v7/v8/v8b/v9_cdp/v10_persistent 全版本代码审查
---
## 一、现有能力全景
### 1.1 核心自动化能力(已验证可用)
| 能力 | 实现方式 | 状态 | 版本 |
|------|---------|------|------|
| 自动登录 | Playwright 填写用户名/密码 | ✅ 稳定 | v6+ |
| OTP 双因素认证 | pyotp 本地生成 TOTP | ✅ 自动填充 | v4+ |
| 资产导航 | DOM 选择器定位目标行 | ✅ 支持模糊匹配 | v6+ |
| Web CLI 连接 | 点击连接按钮 → 处理 Luna dialog | ✅ 含轮询重试 | v6+ |
| 命令执行 | `keyboard.type()` 逐字输入 | ✅ 含 delay 防丢字 | v6+ |
| SFTP 文件上传 | `set_input_files()` 文件管理器 | ✅ 分块(8 chunk | v7+ |
| base64 大文件传输 | 分段 echo + base64 -d 还原 | ✅ 250 字符/段 | v8b+ |
| 截图留存 | `page.screenshot()` | ✅ PNG 格式 | v3+ |
| 会话复用 | `persistent_context` / CDP connect | ✅ v10 | v10 |
| 轮询等待(替代死等) | 检测 prompt 字符/dialog/terminal | ✅ 40s → 待命 | v10.15 |
| 分阶段部署 | Part A(传输) + B(执行) + C(验证) | ✅ 脚本编排 | v5+ |
### 1.2 已验证的部署场景
| 场景 | 日期 | 成果 |
|------|------|------|
| Hotfix #116 扫码获取 | 06-22 | `auth_qrcode.py` 部署成功 |
| Hotfix #120 扫码自动确认 | 06-23 | 29KB `qrcode_service.py` + 8 chunk SFTP |
| Hotfix #48 Nginx Upstream | 06-24 | 配置修复 + HTTPS 验证 |
| Hotfix SSO 单点登录 | 06-25 | config.py + 环境变量注入 |
---
## 二、当前限制(按严重程度排序)
### 🔴 P0 — 阻塞性问题
#### 2.1 xterm.js 输出无法结构化捕获(**当前最紧急**)
**现状**:所有版本(v6v10)均依赖 **截图** 作为命令执行结果的唯一验证手段。
**根因**xterm.js 将终端内容渲染到 `<canvas>` 元素,终端缓冲区存储在 JavaScript 内部对象中:
```
xterm.js 架构:
┌─────────────────────────────┐
│ DOM: <div class="xterm"> │
│ ├── .xterm-viewport │ ← 可滚动视口
│ ├── .xterm-screen │ ← 屏幕容器
│ │ └── <canvas> │ ← ★ 内容渲染到这里
│ └── .xterm-accessibility │ ← 无障碍层(可能为空)
└─────────────────────────────┘
terminal.buffer.active ← ★ 真正的文本在 JS 内存中
├── .baseY / .viewportY ← 滚动位置
├── .length ← 总行数
└── .getLine(y) ← 按行获取
└── .translateToString() ← 转为可见文本
```
当前所有版本的 "输出检测" 方法:
| 方法 | 代码 | 捕获到的是什么 |
|------|------|-------------|
| `page.evaluate("document.body.innerText")` | v10.15 | ⚠️ DOM 文本(不含 canvas 内容) |
| `page.screenshot()` | 所有版本 | ⚠️ 二进制图片,不可解析 |
| prompt 正则匹配 | v10.15 | ⚠️ 只能判定"命令结束了",不能拿到输出 |
**导致的失败模式**
1. **Hotfix #120 的 18 个脚本分裂**`part1 → part2 → diag → verify → sub → tail → view` 中,大量 "查看日志" 脚本 (diag/log/tail/view) 的唯一目的是在终端执行 `cat`/`tail`/`curl`,然后**人工查看截图**确认结果。如果能结构化捕获输出,这些脚本可以合并为带条件判断的单一流程。
2. **无法自动判断部署成功与否**`run_v7_v5.py``proc.returncode` 只能判断 Playwright 进程是否正常退出,无法判断远程服务器上的 `docker restart` 是否成功、`curl` 是否返回预期响应。
3. **无退出码捕获**:执行 `docker ps && echo $?` 的结果在 canvas 里,脚本只能"相信"它成功了。
4. **日志无法结构化存储**:每次部署的输出是截图(PNG),而非文本。无法 grep、无法 diff、无法自动对比两次部署的差异。
5. **截图驱动的不稳定性**
- 命令输出超过一屏时,截图只看到最后一段
- 颜色/样式变化可能被误读为错误
- 网络延迟导致截图时机窗口不可控
#### 2.2 无 headless 模式(依赖 Chrome 可见窗口)
**现状**v10 明文注释"默认非 headless 模式(JumpServer 可能检测 headless 并拒绝)"。
**影响**
- 无法在无 GUI 的服务器上运行
- 必须保持 Chrome 窗口可见(占用桌面空间)
- 无法与用户其他 Chrome 使用并行
#### 2.3 单实例限制
- 同一时间只能运行一个 Playwright 实例
- 多个部署任务必须串行
- `persistent_context` 和 CDP connect 互斥
### 🟡 P1 — 重要限制
| 限制 | 详情 | 影响 |
|------|------|------|
| **长命令输入易截断** | 单次 `keyboard.type` ~20KB 安全上限,超过需分块 | 29KB 文件需 8 chunk |
| **依赖 Windows Chrome** | Playwright `channel="chrome"` | 无法在 Linux 服务器运行 |
| **磁盘占用大** | Playwright + Chromium ~500MB | 多实例部署开销大 |
| **无命令队列机制** | 没有批量执行 + 结果收集 | 每次部署需人工编排 |
| **OTP 需本地密钥文件** | 依赖 `otp_secret.key` | 换机器需重新配置 |
| **错误恢复依赖人工** | 失败后无自动重试策略 | 夜间部署需人工值守 |
| **硬编码路径** | v7 中 V7/V7_DIR 路径硬编码 | 换环境需修改代码 |
### 🟢 P2 — 改进空间
| 限制 | 详情 |
|------|------|
| **部署包 >500MB 未迁移** | `deploy-server/` 目录 |
| **无企微通知集成** | 部署结果需手动确认 |
| **无 CLI 参数标准** | v7/v8/v9/v10 参数不统一 |
| **无部署历史数据库** | 每次部署成功/失败无结构化记录 |
---
## 三、优化空间(按 ROI 排序)
### 🔴 P0 — 紧急优化
#### 3.1 xterm.js 结构化输出捕获 ✅ 最高优先级
这是从 **"截图验证" → "程序化验证"** 的关键跃迁。
**实现方案**:通过 `page.evaluate()` 直接访问 xterm.js 的内部 buffer
```python
# 方案 A:读取 xterm.js 内部 buffer(推荐)
def capture_terminal_output(page):
"""从 xterm.js 的 terminal.buffer 中提取全部可见文本"""
return page.evaluate("""() => {
// JumpServer Luna 将 xterm 实例挂载在全局或 DOM 属性上
// 方式1: 通过 xterm 的全局引用
if (window.term) {
const term = window.term;
const buffer = term.buffer.active;
const lines = [];
for (let i = 0; i < buffer.length; i++) {
const line = buffer.getLine(i);
if (line) {
lines.push(line.translateToString());
}
}
return lines.join('\\n');
}
// 方式2: 通过 xterm 的 DOM 属性(如 data-xterm 或 __vue__
// 方式3: 遍历 window 对象找 xterm 实例
for (const key of Object.keys(window)) {
try {
const obj = window[key];
if (obj && obj.buffer && obj.buffer.active) {
const buffer = obj.buffer.active;
const lines = [];
for (let i = 0; i < buffer.length; i++) {
const line = buffer.getLine(i);
if (line) lines.push(line.translateToString());
}
return lines.join('\\n');
}
} catch(e) {}
}
return null;
}""")
# 方案 B:利用 xterm.js 的无障碍 DOM 层(如果开启)
def capture_via_a11y(page):
"""xterm.js 可选地把内容同步到 .xterm-accessibility 层"""
return page.evaluate("""() => {
const a11y = document.querySelector('.xterm-accessibility');
if (a11y) {
const children = a11y.querySelectorAll('[role="presentation"]');
return Array.from(children).map(el => el.textContent).join('\\n');
}
return null;
}""")
```
**预期收益**
| 改进项 | 当前状态 | 优化后 |
|--------|---------|--------|
| 命令输出获取 | 截图(PNG 二进制) | 结构化文本 |
| 部署成功率判断 | 人工看截图 | 自动 grep exit code / 关键字 |
| 日志存储 | PNG 图片 | 文本(可 diff、可搜索) |
| 脚本数量 | Hotfix #120 需 18 个 | 可合并为 3-5 个带条件分支 |
| 错误诊断速度 | 人工翻截图 | 自动提取错误行 |
| CI/CD 集成 | 不可行 | 可行(输出可解析) |
**实施步骤**
1. **探测阶段**:写一个 `probe_xterm_buffer.py`,在连上终端后执行 `page.evaluate()`,尝试多种方式找到 xterm 实例引用
2. **验证阶段**:对比 `capture_terminal_output()` 的输出和截图内容是否一致
3. **集成阶段**:修改 `send_terminal_command()``send_and_capture()`,返回结构化文本
4. **高级阶段**:利用 buffer 实现增量读取(只读新增行),避免重复传输全部内容
**风险**JumpServer Luna 可能对 xterm 实例做了封装/私有化。需要先探测具体引用路径。
#### 3.2 命令执行结果自动判定
依赖 3.1 的输出,实现:
```python
def execute_and_verify(terminal_page, command, expected_pattern=None, timeout=60):
"""执行命令并自动判定结果"""
send_command(terminal_page, command)
output = wait_for_prompt_and_capture(terminal_page, timeout)
# 自动提取退出码
exit_code = extract_exit_code(output) # 从 "$?=0" 或显式 echo $?
# 自动匹配预期模式
if expected_pattern:
matched = re.search(expected_pattern, output)
return CommandResult(
output=output,
exit_code=exit_code,
matched=matched,
screenshot=screenshot(terminal_page)
)
```
---
### 🟡 P1 — 高价值优化
#### 3.3 部署流水线引擎
将当前的手动编排的 Phase 1/2/3 流程抽象为通用引擎:
```python
class DeployPipeline:
stages = [
Stage("prepare", commands=[...], verify="docker ps | grep wecom"),
Stage("upload", sftp_files=[...], verify="ls -la /tmp/"),
Stage("deploy", commands=[...], verify="curl -s localhost/api/health"),
Stage("verify", commands=[...], verify="grep 'started' /var/log/app.log"),
]
def run(self):
for stage in self.stages:
result = self.execute_stage(stage)
if not result.ok:
self.rollback()
self.notify_failure(stage, result)
return
self.notify_success()
```
#### 3.4 headless 模式适配
探索 JumpServer 反 headless 检测的具体方式,针对性绕过:
- 添加 `--disable-blink-features=AutomationControlled`
- 注入 JS 覆盖 `navigator.webdriver`
- 使用 `stealth` 模式的 Playwright 配置
#### 3.5 企微通知集成
部署完成后自动发送企微消息:
```
✅ Hotfix #XXX 部署成功
目标: 10.90.5.110
耗时: 8m 23s
验证: curl 返回 200 OK
日志: webcli_output/v10_result.txt
```
---
### 🟢 P2 — 长期优化
| 优化项 | 说明 | 依赖 |
|--------|------|------|
| **SSH Key 认证(v17** | 绕过 OTP 流程,实现无人值守 | JumpServer 管理员配置 |
| **API Token 方式** | 直接通过 Koko WebSocket 通信 | JumpServer API 权限 |
| **部署包 pip 化** | `pip install jumpserver-webcli` | 路径去硬编码 |
| **Docker 化执行环境** | Chrome + Playwright 容器化 | headless 模式 |
| **部署历史数据库** | SQLite 记录每次部署 | 输出结构化捕获 |
| **回滚自动化** | 失败自动回滚到上一个版本 | 部署流水线引擎 |
---
## 四、技术路线优先级矩阵
```
紧急度
高 ●●● │ 低 ●○○
┌──────────┼──────────┐
高 ●●●│ xterm.js │ 部署流水 │
│ 输出捕获 │ 线引擎 │
影响面 ├──────────┼──────────┤
│ headless │ SSH Key │
低 ●○○│ 适配 │ 认证 │
└──────────┴──────────┘
```
## 五、总结
JP-webcli 自动化工具已经通过了 4 次生产部署验证,核心链路(登录 → 导航 → 命令执行 → 文件传输)稳定可用。当前最大的瓶颈是 **xterm.js 输出无法结构化捕获**,导致:
1. 所有验证依赖截图(不可解析)
2. 部署成功/失败无法自动判定
3. 脚本过多(单个 hotfix 分裂为 18 个)
4. 无法集成 CI/CD
**建议立即启动 xterm.js buffer 探测**(估计 1-2 小时),确认 JumpServer Luna 中 xterm 实例的引用路径后,一周内完成 `send_and_capture()` 改造。这将使自动化覆盖率从当前的 ~40% 提升到 ~90%。
@@ -0,0 +1,655 @@
# 企微智能IT支持服务台 — 系统架构、消息收发、知识库迭代说明
> **版本**: v1.1 | **日期**: 2026-06-02 | **负责人**: 宋献(IT支持组组长)
> **目标读者**: 运维团队 / 架构团队 / 开发团队
---
## 一、项目概述
### 1.1 背景
公司约 **6000 人**,全国设分子机构,使用企业微信作为内部 IM。当前 IT 服务存在三大痛点:
| 痛点 | 现状 | 影响 |
|------|------|------|
| 员工绕过 AI 直接找人工 | 可通过关键词直通人工坐席,首次后永久记忆 | AI 筛选率极低,人工成本高 |
| AI 转人工需另开窗口 | 跳转到企微"员工服务"模块,与 AI 对话割裂 | 体验差,员工困惑 |
| 无法跨主体共享 | 企微"员工服务"不支持互联企业应用共享 | 跨企业服务不可达 |
### 1.2 核心方案
**自研 IT 服务坐席系统**,替代企微内置的"员工服务"模块:
- 基于企微自建应用消息 API,所有消息由自己的服务器接管
- 分三步渐进式构建:M1 消息接管 → M2 AI 接入 → M3 知识库闭环
- 当前处于 **M1(消息接管 + 极简坐席)开发阶段**
---
## 二、系统架构
### 2.1 部署架构总览
> 详见附件:**系统部署架构图**(图1)
系统采用 **Docker Compose 单体容器化部署**,部署于公司内部独立服务器(预生产环境,与数据平台不同主机。正式环境将迁移到 K8s 集群):
```
┌────────────────────────────────────────────────────┐
│ Docker Engine │
│ │
│ Nginx (:80/:443) — 反向代理 + HTTPS + 静态文件 │
│ │ │
│ ├──→ 前端静态资源 (Vue3) │
│ │ ├─ 坐席工作台 (ElementPlus) │
│ │ └─ 员工 H5 端 (Vant4) │
│ │ │
│ └──→ FastAPI 后端 (:8000) │
│ ├─ 消息路由层 │
│ ├─ 紧急度评分引擎 │
│ └─ 会话/消息/坐席 管理 │
│ │ │ │
│ ▼ ▼ │
│ PostgreSQL 16 Redis 7 │
│ (:5432) (:6379) │
│ 持久化存储 Token 缓存 │
└────────────────────────────────────────────────────┘
↕ HTTPS
┌─────────────────┐
│ 企微服务器 │
│ (回调推送 + API) │
└─────────────────┘
```
### 2.2 技术栈
| 层级 | 技术选型 | 说明 |
|------|---------|------|
| 反向代理 | Nginx | HTTPS 终止、静态文件、路由分发 |
| 后端框架 | FastAPI (Python 3.12) | 异步、自动 OpenAPI 文档、类型安全 |
| 数据库 | PostgreSQL 16 | 会话/消息/坐席/配置 持久化(9 张表) |
| 缓存 | Redis 7 | access_token 缓存(TTL 7200s)、JWT 会话 |
| ORM | SQLAlchemy 2.0 (async) | 异步 session、声明式模型 |
| 数据库迁移 | Alembic | 所有表结构变更通过迁移脚本管理 |
| 坐席前端 | Vue3 + ElementPlus + Pinia | 企业级组件库,三栏工作台布局 |
| 员工 H5 | Vue3 + Vant4 + Pinia | 移动端组件库,企微 WebView 兼容 |
| 加解密 | cryptography (Python) | AES-CBC-256 企微消息加解密 |
| 容器化 | Docker + Docker Compose | 一键启停,5 个容器 |
### 2.3 数据库设计(9 张核心表)
| 表名 | 用途 | 关键字段 |
|------|------|---------|
| `conversations` | 会话主表 | employee_id, status(queued/serving/resolved), urgency_score(1-5), tags(JSON), is_vip |
| `messages` | 消息记录 | sender_type(employee/agent/ai/system), content, msg_type(text/image/file) |
| `agents` | 坐席信息 | user_id, status(online/offline/busy), current_load, max_load |
| `quick_reply_templates` | 快速回复模板 | category, title, content(支持 {变量}), variables(JSON) |
| `system_configs` | 系统配置 | config_key, config_value(关键词/阈值/话术等业务规则) |
| `funny_phrases` | 趣味话术 | scene(6 种场景), content, tone, is_active |
| `approval_links` | 审批流程链接 | category(IT/HR/行政/财务), title, url |
| `software_downloads` | 软件下载入口 | category, name, version, platform, download_url |
| `agent_notes` | 坐席备注 | conversation_id, agent_id, content |
### 2.4 API 接口分组(7 组,约 25 个端点)
| 分组 | 路径前缀 | 核心接口 |
|------|---------|---------|
| 企微回调 | `/api/wecom/callback` | GET 验证 URL、POST 接收消息 |
| 会话管理 | `/api/conversations` | 列表/详情/状态/置顶/代办/接单 |
| 消息管理 | `/api/conversations/{id}/messages` | 消息列表/发送 |
| 坐席管理 | `/api/agents` | 列表/登录/状态切换 |
| 快速回复 | `/api/quick-replies` | CRUD |
| H5 用户端 | `/api/h5/*` | 会话/摇人/审批链接/软件下载/OAuth |
| 系统健康 | `/api/health` | Docker 健康检查 |
统一响应格式:
```json
{ "code": 0, "data": {}, "message": "success" }
```
错误码:0=成功,1000+=通用错误,2000+=企微 API 错误,3000+=业务逻辑错误。
---
## 三、消息收发全链路
### 3.1 流程总览
> 详见附件:**消息收发全链路流程图**(图2)
完整链路(6 步闭环):
```
员工发消息 → 企微回调解密 → 消息路由 → 评分标记 → 入库 → 坐席轮询 → 坐席回复 → 企微主动推送 → 员工同一窗口收到
```
### 3.2 详细步骤
| 步骤 | 触发方 | 操作 | 技术要点 |
|------|--------|------|---------|
| ① 接收 | 员工 | 在企微应用中发消息 | 企微应用内消息,非微信客服 |
| ② 回调 | 企微服务器 | POST 加密 XML 到 `/api/wecom/callback` | AES-CBC-256 加密,SHA1 签名验证 |
| ③ 解密 | FastAPI | `wecom_crypto.decrypt_message()` | 使用 `cryptography` 库,兼容企微官方加解密 |
| ④ 路由 | MessageRouter | `route_message()` 核心编排 | 按序调用:创建会话→VIP检测→标记检测→评分→入库 |
| ⑤ 评分 | ScoringService | 5 步评分 + 标记:VIP→情绪→举手→需介入→紧急度 | 纯规则引擎(M1 不用 AI) |
| ⑥ 入库 | PostgreSQL | INSERT conversations + messages | 会话和消息原子写入 |
| ⑦ 轮询 | 坐席浏览器 | 每 3 秒 GET `/api/conversations` | 按紧急度 DESC 排序,列表 + 未读标记 |
| ⑧ 回复 | 坐席 | POST 消息内容 → 后端调用企微 API | 同时写 DB 和调用企微 `message/send` 接口 |
| ⑨ 推送 | 企微服务器 | 推送坐席回复到员工 | 同一对话窗口显示,无跳转 |
### 3.3 紧急度评分公式
```
紧急度 = 基础分(关键词) + 情绪加成 + VIP加成 + 重复追问加成
```
- 分值范围:1-5,限幅 `clamp(1, 5)`
- 映射:1=低, 2=中, 3=高, 4=紧急, 5=最高
- 所有关键词和阈值存 `system_configs` 表,支持动态修改无需重启
### 3.4 会话排序规则
```
紧急 → 举手 → 需介入 → 活跃 → AI处理中 → 已结单
(同级按 last_message_at 倒序)
```
### 3.5 坐席端通信
M1 已升级为 **WebSocket 实时推送**2026-06-03 完成),坐席浏览器通过 `ws://host/ws/{agent_id}` 保持长连接:
- 新消息/会话状态变更通过 WS 实时推送,无需轮询
- 心跳保活:前端每 30 秒发 ping,后端回 pong
- 断线重连:指数退避(1s→2s→4s→...→30s 上限)
- 降级策略:WS 断连时自动降级为 3 秒轮询,WS 重连后自动停止轮询
> 旧方案(已废弃):M1 原计划使用短轮询(3-5秒),已替换为 WebSocket。
### 3.6 员工端架构双方案(2026-06-03 评估)
> **评估结论**: 企微原生1对1方案技术完全可行,已纳入项目选型文档和运维应急预案
当前 H5 WebView 方案(方案A)与企微原生1对1方案(方案B)为双轨可选架构:
| 维度 | 方案A: H5 WebView | 方案B: 原生1对1 + 外援群聊 |
|------|-------------------|---------------------------|
| 员工入口 | 点击应用 → H5 页面 | 直接在企微与应用1对1聊天 |
| 交互体验 | H5 内输入框 | **原生聊天窗口,体验最优** |
| 前端开发 | 大(Vue3+Vant4 | **零** |
| 跨平台 | **✅ 可挂载钉钉/飞书/浏览器** | ❌ 绑定企微 |
| 跨主体 | **✅ 可(其他认证方式)** | ❌ 仅同一企微主体 |
| OAuth2 | 必须 | **不需要** |
| AI/人工区分 | H5 可做丰富标识 | 需内容前缀区分 |
| 外援协作 | 需额外设计 | **原生群聊(appchat)** |
| 消息存档 | 全量经后端 | **全量经后端(无需会话存档权限)** |
| 切换成本 | — | **核心链路已实现,零代码改动可切换** |
**方案B 交互路径**(关键纠正:不是每次都创建群聊):
1. **AI 自助**:员工发消息 → 企微回调 → Dify → `/message/send` → 同一窗口
2. **坐席介入**:坐席工作台 → `/message/send` → 同一窗口(员工无感知切换)
3. **外援场景**:坐席发起 → `/appchat/create`**新群聊窗口**(非常态,低频)
**选型决策**(详见 PRD §3.3):
- 企微主体内服务 → 方案B 体验更优
- 需跨平台/跨主体 → 方案A 不可替代
- **推荐混合策略**:原生1对1做日常入口,H5 保留为扩展层
**运维应急**(详见 `01-项目总览与部署手册.md` §7.5):
- H5 不可用时,零代码切换至方案B
- 需外援协作时,按需启用 appchat 群聊
---
## 四、三步演进路径与知识库迭代
### 4.1 三步总览
> 详见附件:**三步演进路径与知识库迭代闭环图**(图3)
| 里程碑 | 周期 | 核心交付 |
|--------|------|---------|
| **M1: 消息接管 + 极简坐席** | 6 周(进行中) | 企微 API 链路验证 · 坐席三栏工作台 · 员工 H5 双栏 |
| **M2: AI 机器人接入** | M1 后 4-6 周 | 千问/Dify/RAGFlow 接入 · AI 前置筛选 · 排队系统 |
| **M3: 知识库闭环迭代** | M2 后 4-6 周 | 坐席标注系统 · 千问自动分析 · 知识库自优化 |
### 4.2 M1 — 当前阶段详情
| 模块 | 内容 | 状态 |
|------|------|------|
| 企微消息对接 | 自建应用回调 + AES 加解密 + token 管理 | ✅ 代码完成 |
| 消息路由 | 所有消息进路由层,新会话→坐席队列 | ✅ 代码完成 |
| 紧急度评分 | 5 步评分引擎(VIP/情绪/举手/需介入/紧急度) | ✅ 代码完成 |
| 坐席工作台 | Vue3 三栏布局(会话列表/对话区/AI助手面板) | ✅ 代码完成 |
| 员工 H5 | Vue3+Vant4 双栏布局(对话区/助手面板+摇人按钮) | ✅ 代码完成 |
| 测试用例 | 116 条 pytest 全部通过 | ✅ 完成 |
| 环境搭建 | Docker Compose + PostgreSQL + Redis | 🔧 配置中 |
M1 **不接入 AI**——先验证企微基础链路(消息收发、会话管理、坐席工作台)完整可用。
### 4.3 M2 — AI 接入计划
核心改动:**只改路由层逻辑**,其余不动。
```
M1: 新会话 → 坐席队列
M2: 新会话 → AI 先回答 → AI判断/用户触发 → 坐席队列
```
新增能力:
- 千问(通义千问)作为对话模型
- RAGFlow 作为知识库语义检索引擎
- Dify 作为 AI 应用编排平台
- 转人工触发:关键词 / AI 连续 2 轮追问 / AI 调用超时 3 秒
- AI 回复末尾自动追加 "以上为 AI 自动回复,如需人工帮助请回复'转人工'"
- 排队系统:等待人数、预计时间
目标:AI 首答率 ≥ 80%
### 4.4 M3 — 知识库迭代闭环(核心创新)
这是整个系统从"工具"进化为"智能平台"的关键一步。
```
┌────────────────────────────────────────────────────────┐
│ 知识库迭代正向循环 │
│ │
│ 坐席日常标注 ──→ 千问分析 ──→ 自动处理 ──→ 知识库增强 │
│ (正确/错误) (缺文档/过时) │ │
│ ↑ │ │
│ └──────── 持续循环 ─────────┘ │
└────────────────────────────────────────────────────────┘
```
**标注融入日常工作流**,不另开标注页面:
- 对话区每条 AI 回复旁有 👍 / 👎 按钮
- 坐席在正常服务过程中顺手标注
**千问自动分析**两类问题:
| 分析结果 | 自动处理 |
|---------|---------|
| **缺文档**:知识库没有覆盖此问题 | 千问生成标准 FAQ → 推送 RAGFlow |
| **信息过时**:知识库有文档但内容不对 | 标记原文档需更新 → 通知管理员审核 |
### 4.5 并行协作模式(设计理念)
传统"串行排队"改为**"并行协作"**
| 角色 | 工作方式 |
|------|---------|
| AI | 全程在线,所有对话可见 |
| 坐席 | 随时介入,AI 始终在旁辅助 |
| 员工 | 同一窗口,AI 和人工无缝切换 |
### 4.6 AI Wingman — 坐席端智能辅助(2026-06-04 新增)
> **设计理念**:AI不仅服务员工,更要解放坐席——从"坐席=打字员"升级为"坐席=审核员+专家"
**三层渐进式架构**
| 层 | 目标 | 核心功能 | 行业验证 | 实施时间 |
|----|------|---------|---------|---------|
| 效率层 | 消灭重复劳动 | AI草稿回复 + 自动摘要 + 自动标签 | 打字量 -80%,填单 1分钟→10秒 | Phase 1(已确认) |
| 认知层 | 降低认知负荷 | 知识推荐 + SOP导航 + 相似工单 + 客户画像 | 新人上手 -50% | Phase 2 |
| 情感层 | 减少情绪消耗 | 情绪识别 + 安抚话术 + 语气润色 + 疲劳检测 | 情绪耗竭 -45% | Phase 3 |
**Phase 1 双区布局**(已确认实现方案):
| 区域 | 位置 | 功能 |
|------|------|------|
| 内嵌区 | 对话流中(每条员工消息下方) | AI草稿回复 — [采纳]/[编辑]/[忽略] 三选一 |
| 侧栏区 | 右侧 AI 助手面板 | 会话自动摘要、自动标签、知识推荐、快捷回复库 |
**底层实现**:扩展现有 Dify,新增坐席端 Wingman Agent
- Agent 1(已有):员工端 AI,直接回答员工问题
- Agent 2(新增):坐席端 Wingman,辅助坐席生成草稿/摘要/知识推荐
- 两个 Agent 共用知识库,但 system prompt 和上下文不同
**为什么这样做**
1. 坐席每天大量重复回复(密码重置、VPN指引等),AI草稿让坐席从"打字员"变"审核员"
2. 结单文书消耗 70% 非服务时间,自动摘要压缩到 10%
3. 员工情绪会传染坐席,情绪预警+安抚话术帮助保持专业冷静
4. 参考:NiCE Copilot、Helpshift AI Copilot、天润融通、循环智能等产品验证
---
## 五、运维相关信息
### 5.1 资源配置需求
| 资源 | 最低配置 | 建议配置 |
|------|---------|---------|
| 服务器 | 4 核 / 8 GB / 100 GB SSD | Docker Engine 环境 |
| 公网 HTTPS 域名 | 1 个 | 用于企微回调 URL |
| 企微自建应用 | 1 个(已创建) | CorpID/AgentID/Secret/Token/EncodingAESKey |
> **待办**:企微回调 URL 需要公司服务器+公网 HTTPS 域名,目前暂缓,先在本机测试环境完成其他部分。
### 5.2 Docker Compose 服务清单
| 服务 | 镜像 | 端口 | 健康检查 |
|------|------|------|---------|
| postgres | postgres:16 | 5432 | pg_isready |
| redis | redis:7 | 6379 | redis-cli ping |
| backend | 自构建 Dockerfile | 8000 | /api/health |
| frontend-agent | 自构建 + Nginx 静态 | 80/443 | — |
| frontend-h5 | 同 Nginx 容器 | — | — |
### 5.3 关键配置项(.env
```env
# 企微
WECOM_CORP_ID=ww...
WECOM_AGENT_ID=1000002
WECOM_SECRET=...
WECOM_TOKEN=... # 回调验证 Token
WECOM_ENCODING_AES_KEY=... # 43 位随机字符串
# 数据库
DATABASE_URL=postgresql://user:pass@postgres:5432/wecom_it_desk
# Redis
REDIS_URL=redis://redis:6379/0
# 服务
BACKEND_PORT=8000
CORS_ORIGINS=http://localhost:5173
```
### 5.4 启动命令
```bash
# 一键启动所有服务
docker compose up -d
# 数据库迁移
docker compose exec backend alembic upgrade head
# 查看日志
docker compose logs -f backend
# 停止
docker compose down
```
---
## 六、当前进展与待办
### 6.1 当前进度
| 模块 | 状态 | 备注 |
|------|------|------|
| PRD | ✅ 完成 | 31 项需求,7 个用户故事 |
| 架构设计 | ✅ 完成 | 9 张表 DDL,7 组 API4 个时序图 |
| 后端代码 | ✅ 完成 | 45 个文件,7 个服务层 + 7 个 API 路由 |
| 坐席前端 | ✅ 完成 | 25 个文件,Vue3+ElementPlus 三栏布局 |
| 员工 H5 | ✅ 完成 | 12 个文件,Vue3+Vant4 双栏+摇人 |
| 测试用例 | ✅ 完成 | 116 条 pytest 全部通过 |
| 环境搭建 | 🔧 进行中 | PostgreSQL + Redis 安装配置中 |
| 企微回调 | ⏳ 待办 | 需要公司服务器 + 公网 HTTPS 域名 |
### 6.2 需要团队协助的事项
| # | 事项 | 需要谁 | 紧急度 |
|---|------|--------|--------|
| 1 | **服务器资源**:分配一台 Linux 服务器用于 Docker 部署 | 运维 | 中 |
| 2 | **公网域名**:一个 HTTPS 域名用于企微回调 URL | 运维/架构 | 中(M1 联调前) |
| 3 | **SSL 证书**:通配符证书或 Let's Encrypt | 运维 | 中(同上) |
| 4 | **企微通讯录权限**:确认 API 权限(VIP 功能依赖) | 运维/企微管理员 | 低(M1 可暂用 mock) |
| 5 | **千问/Dify/RAGFlow 环境**(M2 阶段) | 架构/开发 | 低(M2 前准备) |
### 6.3 下一步计划
1. **本周**:完成本机 PostgreSQL + Redis 安装,后端本地启动验证
2. **下周**:坐席前端启动联调,Swagger 接口测试
3. **服务器就绪后**Docker Compose 部署 + 企微回调配置 + 完整链路联调
---
## 七、现有系统复用评估
> 基于交接文档 + db_query_project_v8 源码分析,评估现有企微AI客服系统可复用的服务能力和资源
### 7.1 现有系统全景
```
┌──────────────────────────────────────────────────────────────┐
│ 员工(企微App) │
│ │ 发消息/收回复 │
│ ▼ │
│ ┌─────────────────┐ │
│ │ 企业微信自建应用 │ │
│ └────────┬────────┘ │
│ │ 回调/主动消息 │
│ ▼ │
│ ┌─────────────────┐ ┌──────────────────┐ │
│ │ B端生产智能体 │───▶│ dify2openai 桥接 │ │
│ │ agent.dc.servyou │ │ yw-dify.dc │ │
│ └────────┬────────┘ └────────┬─────────┘ │
│ │ │ │
│ ▼ ▼ │
│ ┌─────────────────┐ ┌──────────────────┐ │
│ │ Dify Workflow │ │ RAGFlow 知识库 │ │
│ │ yw-dify.dc │ │ 10.80.0.85:8080 │ │
│ └─────────────────┘ └──────────────────┘ │
│ │ │ │
│ ▼ ▼ │
│ ┌─────────────────┐ ┌──────────────────┐ │
│ │ Qwen3-30B │ │ bge-m3 向量模型 │ │
│ │ 10.80.0.49 │ │ │ │
│ └─────────────────┘ └──────────────────┘ │
│ │
│ ┌──────────────────────────────────────────┐ │
│ │ 智能IT数据平台 (Django 3.2) │ │
│ │ it-dataquery.dc.servyou-it.com │ │
│ │ 10.80.0.86 │ │
│ │ ┌────────────┬────────────┬───────────┐ │ │
│ │ │ 数据查询统计 │ 人工介入标注 │ 人工录入 │ │ │
│ │ └────────────┴────────────┴───────────┘ │ │
│ │ DB: dify(只读) + intervention_db(读写) │ │
│ └──────────────────────────────────────────┘ │
└──────────────────────────────────────────────────────────────┘
```
### 7.2 可复用资源清单
#### 🔴 核心复用(直接影响新系统架构)
| # | 资源 | 现有位置/信息 | 复用方式 | 新系统对应 |
|---|------|-------------|---------|-----------|
| 1 | **Dify Workflow** | `yw-dify.dc.servyou-it.com/apps` | 直接复用,M2阶段接入AI回复 | 坐席助手AI面板 + 自动回复 |
| 2 | **dify2openai 桥接** | `yw-dify.dc/dify2openai/v1/chat/completions` | 直接复用API | 后端调用AI的入口 |
| 3 | **RAGFlow 知识库** | `10.80.0.85:8080` | 直接复用,M3阶段混合标注迭代 | 知识库管理 + 标注闭环 |
| 4 | **Qwen3-30B 大模型** | `10.80.0.49:5000/api/llm/servyou/v1` | 直接复用 | AI对话底层模型 |
| 5 | **bge-m3 向量模型** | RAGFlow 内置 | 直接复用 | 知识库检索向量化 |
| 6 | **Dify 数据库(只读)** | `10.80.128.40:5432` DB=dify User=difyro | 读取messages表获取AI对话记录 | 历史会话数据迁移 + 统计 |
| 7 | **企微自建应用** | 已创建,有CorpID/AgentID/Secret | 直接复用应用凭证 | 消息收发的企微入口 |
#### 🟡 业务逻辑复用(需适配改造)
| # | 现有能力 | 现有实现 | 适配到新系统 | 改造量 |
|---|---------|---------|-------------|-------|
| 8 | **会话定义** | 15分钟无交互=一个会话结束 | 改为新系统会话状态机(queued→serving→resolved | 中 |
| 9 | **自助解决判定** | 15分钟内未转人工 | 复用判定逻辑,改为新系统统计口径 | 小 |
| 10 | **知识库命中判定** | 回答含"您的问题可能不在服务业务范围内"=未命中 | 复用关键词,增加AI置信度评分 | 小 |
| 11 | **系统转人工判定** | 用户输入"IT"即转人工 | 改为摇人按钮 + 智能评分触发 | 中 |
| 12 | **人工介入标注** | ManualIntervention模型(命中/待处理/已处理) | 适配为新系统会话标记体系(VIP/举手/需介入/情绪) | 中 |
| 13 | **人工录入** | ManualEntry模型(补录线下咨询) | 复用,接入新系统数据库 | 小 |
| 14 | **月度报表查询** | MonthlyReportQueryView | 改造为新系统运营报表 | 中 |
| 15 | **坐席登录认证** | Session + Redis + 12小时超时 | 改为JWT + Redis,复用登录逻辑 | 小 |
| 16 | **密码修改** | ChangePasswordView | 复用,加哈希加密(现在明文存储) | 小 |
#### 🟢 基础设施复用(直接复用或微调)
| # | 资源 | 现有配置 | 复用方式 |
|---|------|---------|---------|
| 17 | **服务器 10.80.0.86** | 现有Django+PostgreSQL+Redis | 新系统后端部署目标 |
| 18 | **域名** | `it-dataquery.dc.servyou-it.com` | 新系统用子域名如 `itdesk.dc.servyou-it.com` |
| 19 | **SSL证书** | ssl/目录 | 复用或申请新证书 |
| 20 | **Nginx** | `nginx/nginx.conf` | 改反代配置指向新系统 |
| 21 | **Docker Compose 部署模式** | 现有5套 compose 编排 | 复用部署模式,新系统加自己的 compose |
| 22 | **Redis** | `10.90.5.8` Redis 7 + 密码(见运维密码管理) | 直接连接使用 |
| 23 | **Docker网络** | dbquery_net 桥接模式 | 新建 itdesk_net |
| 24 | **SearXNG 搜索** | `10.90.5.8:8080` | M2阶段AI联网搜索能力 |
| 25 | **LangBot** | `10.90.5.8:30030` | 可选,多模型接入 |
### 7.3 数据模型映射(旧→新)
#### 用户模型
| 现有 system_users | 新系统 Agent(坐席) | 复用方式 |
|-------------------|---------------------|---------|
| username | username | ✅ 直接复用 |
| password(明文⚠️ | password_hash | 🔄 改为bcrypt哈希 |
| is_active | is_online | 🔄 语义变更 |
| created_at | created_at | ✅ 直接复用 |
| — | display_name | 🆕 新增 |
| — | role | 🆕 新增(admin/agent |
#### 会话/消息模型
| 现有 messagesDify只读) | 新系统 Conversation + Message | 说明 |
|--------------------------|-------------------------------|------|
| id | — | Dify消息ID,迁移时关联 |
| session_id → user_name | Conversation.user_id | 会话归属用户 |
| query | Message.contenttype=text | 用户问题 |
| answer | Message.contenttype=ai_reply | AI回复 |
| created_at | Message.created_at | 时间戳 |
| — | Conversation.status | 🆕 状态机 |
| — | Conversation.urgency_score | 🆕 紧急度 |
| — | Conversation.tags | 🆕 标记体系 |
#### 人工介入模型
| 现有 ManualIntervention | 新系统 Conversation.tags | 说明 |
|------------------------|--------------------------|------|
| message_id | conversation_id | 关联对象变更 |
| manual_intervention(是/否) | tags中的"需介入" | 逻辑迁移 |
| operation_user | agent_id | 操作人关联 |
| knowledge_status(命中/待处理/已处理) | tags中的知识库状态 | 逻辑迁移 |
| query | — | 已在Message中 |
#### 人工录入模型
| 现有 ManualEntry | 新系统 | 说明 |
|-----------------|--------|------|
| 整个模型 | ✅ 可直接复用 | 补录线下咨询记录 |
### 7.4 技术栈对比与迁移路径
| 维度 | 现有系统 | 新系统 | 迁移策略 |
|------|---------|--------|---------|
| 后端框架 | Django 3.2(同步) | FastAPI(异步) | **完全重写后端**Django不适配实时消息 |
| 数据库ORM | Django ORM | SQLAlchemy 2.0(异步) | 模型定义迁移,逻辑重写 |
| 前端坐席台 | Bootstrap + jQuery | Vue3 + ElementPlus | **完全重写前端** |
| 前端用户端 | 无(企微原生聊天) | Vue3 + Vant4 (H5) | 新建 |
| 数据库 | PG 11.8Dify只读)+ PG 13(本地) | PG 16(统一) | 升级,合并为单库 |
| 缓存 | Redis 7django-redis | Redis 7aioredis | 复用Redis实例 |
| 部署 | Docker Compose | Docker Compose | 复用模式 |
| 认证 | Django Session | JWT + Redis | 重写认证层 |
> **关键结论**:代码层面复用率约 15%(主要是业务逻辑和SQL查询),基础设施和AI能力复用率约 70%。
### 7.5 对接点梳理(新系统与现有系统交互)
#### M1阶段(当前)— 纯坐席台
```
新系统独立运行,不与现有系统交互
├── 企微消息 → 新后端(FastAPI) → 坐席台(Vue3) → 回复
├── 数据库:本地 PostgreSQL 16
└── 缓存:本地 Redis
```
#### M2阶段 — 接入AI
```
新系统 + 现有AI能力
├── 企微消息 → 新后端 → Dify(dify2openai) → AI回复
│ └─ 同时 → 坐席台(AI助手面板展示AI回复)
├── AI无法解决 → 转坐席(摇人)
├── 读取Dify数据库 → 同步历史对话到新系统
└── RAGFlow → 知识库检索
```
#### M3阶段 — 知识库迭代
```
新系统 + AI + 知识库闭环
├── 坐席标注 → RAGFlow 知识库更新
├── AI回复 + 坐席校正 → 知识库质量提升
├── 统计面板 → 月度运营报告(复用现有报表逻辑)
└── 数据平台 → 合并到新系统或并行运行
```
### 7.6 关键对接参数汇总
| 参数 | 值 | 用途 |
|------|-----|------|
| dify2openai API | `http://yw-dify.dc.servyou-it.com/dify2openai/v1/chat/completions` | AI对话 |
| dify2openai Key | `http://yw-dify.dc.servyou-it.com/v1\|app-***\|Chat` | 认证 |
| RAGFlow | `http://10.80.0.85:8080` | 知识库管理 |
| Qwen3-30B | `http://10.80.0.49:5000/api/llm/servyou/v1/chat/completions` | 大模型 |
| Dify DB(生产只读) | `10.80.128.40:5432` DB=dify User=difyro Pwd=*** | 历史数据 |
| Dify DB(测试) | `10.199.16.9:5432` DB=dify User=dify_ro | 测试环境 |
| Redis | `10.90.5.8:6379` Pwd=*** | 缓存共享 |
| 数据平台 | `http://it-dataquery.dc.servyou-it.com` (10.80.0.86) | 部署服务器 |
| B端智能体 | `https://agent.dc.servyou-it.com` | 智能体管理 |
| SearXNG | `http://10.90.5.8:8080` | 联网搜索 |
| Dify App ID | `a57543f3-de66-47cc-ad89-d0540c08159f` | 消息查询过滤 |
### 7.7 对运维/架构/开发的具体建议
**给运维:**
1. **10.80.0.86 服务器**已跑Django+PG+Redis,新系统FastAPI可同机部署(不同端口),后续迁容器
2. **域名**申请 `itdesk.dc.servyou-it.com`Nginx反代到新系统8001端口
3. **Redis**可复用现有实例(db=0给旧系统,db=1给新系统)
4. **SSL**复用现有通配符或新申请
**给架构:**
1. **M1独立运行**,不依赖现有系统,降低风险
2. **M2通过dify2openai API对接**,不需要改Dify Workflow代码
3. **Dify数据库只读**,新系统通过SQL同步历史数据,不影响现有AI服务
4. **RAGFlow API**直接调用,M3阶段做双向同步
**给开发:**
1. 新系统用 **FastAPI + SQLAlchemy 2.0**,不要沿用Django代码
2. 现有系统的 **业务逻辑(会话判定、命中判定、报表SQL)** 可参考移植
3. **数据模型映射**见7.3节,需要做数据迁移脚本
4. **API对接参数**见7.6节,M2阶段需要配置
### 7.8 风险与注意事项
| 风险 | 级别 | 应对 |
|------|------|------|
| Dify数据库是只读的,不能写入 | ⚠️ 中 | 新系统自建库,只从Dify同步数据 |
| 现有系统密码明文存储 | 🔴 高 | 新系统必须用bcrypt,迁移时做哈希转换 |
| dify2openai桥接由CF维护 | ⚠️ 中 | 提前沟通M2对接需求,预留联调时间 |
| RAGFlow知识库由宋献IT组维护 | ℹ️ 低 | M3阶段需协调知识库写入权限 |
| 现有Django 3.2 + PG 11.8版本老旧 | ⚠️ 中 | 新系统独立部署,不升级旧系统 |
---
## 附录:代码仓库
```
C:\Users\simon\wecom_it_smart_desk\
├── PRD.md # 产品需求文档
├── ARCHITECTURE.md # 系统架构设计(含类图/时序图/API/DDL)
├── docker-compose.yml # Docker 编排
├── .env # 环境变量
├── backend/ # FastAPI 后端 (45 个文件)
│ ├── app/
│ │ ├── api/ # 7 个路由模块
│ │ ├── services/ # 6 个服务层
│ │ ├── models/ # 9 个数据模型
│ │ ├── schemas/ # 6 个 Pydantic Schema
│ │ └── utils/ # 加解密/token/响应工具
│ └── alembic/ # 数据库迁移
├── frontend-agent/ # 坐席工作台前端 (25 个文件)
├── frontend-h5/ # 员工 H5 前端 (12 个文件)
├── tests/ # 测试用例 (116 条)
└── docs/ # 文档
```
---
> 本文档面向运维/架构/开发三团队沟通使用。详细技术规格见 `ARCHITECTURE.md`,产品需求见 `PRD.md`。
@@ -0,0 +1,462 @@
# 数据库 ER 图 + 环境变量清点
**日期**: 2026-06-15
**审计人**: Claude(满载跑批)
**关联**: [[技术架构]] / [[风险跟踪表]] / [[前端审计报告]]
---
## 📌 1. ER 图(ASCII + Mermaid)
### 1.1 实体清单(16 张表)
| # | 表名 | 中文 | 模型文件 | 用途 |
|---|---|---|---|---|
| 1 | `conversations` | 会话 | `conversation.py` | 核心:员工-坐席咨询会话 |
| 2 | `messages` | 消息 | `message.py` | 会话内的所有消息 |
| 3 | `agents` | 坐席 | `agent.py` | IT 服务人员 |
| 4 | `employees` | 员工 | `employee.py` | 通过 OAuth2 认证的员工 |
| 5 | `agent_notes` | 坐席备注 | `agent_note.py` | 坐席对会话的备注 |
| 6 | `system_configs` | 系统配置 | `system_config.py` | 动态配置(关键词/阈值) |
| 7 | `config_change_logs` | 配置变更日志 | `config_change_log.py` | 配置项的审计 |
| 8 | `quick_reply_templates` | 快速回复模板 | `quick_reply_template.py` | 坐席常用回复 |
| 9 | `funny_phrases` | 趣味话术 | `funny_phrase.py` | 等候中的趣味话 |
| 10 | `approval_links` | 审批流程链接 | `approval_link.py` | 各类审批入口 |
| 11 | `software_downloads` | 软件下载 | `software_download.py` | 常用软件清单 |
| 12 | `troubleshooting_templates` | 排障模板 | `troubleshooting_template.py` | 标准化排障路径 |
| 13 | `todo_items` | 待办事项 | `todo_item.py` | 工单/审批/设备 |
| 14 | `roles` | 角色 | `role.py` | RBAC 角色定义 |
| 15 | `user_roles` | 用户-角色关联 | `user_role.py` | 多对多关联 |
| 16 | `role_mapping_rules` | 角色映射规则 | `role_mapping_rule.py` | 自动分配规则 |
### 1.2 ER 图(Mermaid)
```mermaid
erDiagram
EMPLOYEES ||--o{ CONVERSATIONS : "创建(通过 corp_id+employee_id)"
EMPLOYEES ||--o{ USER_ROLES : "拥有角色"
CONVERSATIONS ||--o{ MESSAGES : "包含"
CONVERSATIONS ||--o{ AGENT_NOTES : "有备注"
CONVERSATIONS }o--|| AGENTS : "被分配给"
AGENTS ||--o{ AGENT_NOTES : "写"
AGENTS ||--o{ QUICK_REPLY_TEMPLATES : "提交"
AGENTS ||--o{ CONFIG_CHANGE_LOGS : "改配置"
ROLES ||--o{ USER_ROLES : "分配给"
ROLES ||--o{ ROLE_MAPPING_RULES : "按规则映射"
CONVERSATIONS ||--o| TODO_ITEMS : "关联待办"
SYSTEM_CONFIGS ||--o{ CONFIG_CHANGE_LOGS : "被改"
EMPLOYEES {
string id PK "UUID"
string corp_id "企业ID"
string employee_id "企微UserID"
string name "姓名"
string department "部门(IDs)"
string position "岗位"
string mobile
string email
string avatar
int status "1激活 2禁用 4未激活"
string it_level "技能等级"
string it_level_source
json notes "坐席备注"
datetime last_login_at
datetime created_at
datetime updated_at
}
AGENTS {
string id PK
string user_id UK "企微UserID"
string name
string status "online/offline/busy"
int current_load
int max_load
string role "admin/agent"
json skill_tags
string otp_secret "TOTP密钥"
boolean otp_enabled
string password_hash "bcrypt"
datetime created_at
datetime updated_at
}
CONVERSATIONS {
string id PK
string corp_id
string employee_id
string employee_name
string department
string position
string level
string status "ai/queued/serving/resolved"
boolean is_vip
boolean is_pinned
boolean is_todo
int urgency_score "1-5"
json tags
string assigned_agent_id FK
json collaborating_agent_ids
json participants
int ai_substantive_reply_count
int impact_scope
boolean is_blocking
string emotion_state
string dify_conversation_id
datetime last_message_at
string last_message_summary
datetime created_at
datetime updated_at
}
MESSAGES {
string id PK
string conversation_id FK
string sender_type "employee/agent/ai/system"
string sender_id
string sender_name
text content
string msg_type "text/image/file/voice/system"
string reply_to_id "引用"
string media_id
string media_url
string file_name
int file_size
json extra_data
boolean ai_suggestion
string status "sending/sent/delivered/read"
datetime recallable_until
boolean is_read
string suggestion_action "accepted/edited/ignored"
datetime created_at
}
AGENT_NOTES {
string id PK
string conversation_id FK
string agent_id
text content
datetime created_at
datetime updated_at
}
SYSTEM_CONFIGS {
string id PK
string config_key UK
text config_value
string description
datetime updated_at
}
CONFIG_CHANGE_LOGS {
string id PK
string config_key
text old_value
text new_value
string changed_by
datetime changed_at
}
QUICK_REPLY_TEMPLATES {
string id PK
string category
string title
text content
json variables
int sort_order
string status "draft/pending/approved/rejected"
int version
string submitted_by
datetime created_at
datetime updated_at
}
FUNNY_PHRASES {
string id PK
string scene "shake/keyword/waiting/connected/timeout/vip"
text content
string tone
int sort_order
boolean is_active
datetime created_at
datetime updated_at
}
APPROVAL_LINKS {
string id PK
string category "IT/HR/行政/财务"
string title
text url
int sort_order
datetime created_at
datetime updated_at
}
SOFTWARE_DOWNLOADS {
string id PK
string category "办公/开发/安全/工具"
string name
string version
string platform
text download_url
int sort_order
datetime created_at
datetime updated_at
}
TROUBLESHOOTING_TEMPLATES {
string id PK
string name
string category "vpn/email/system/account"
json path_steps
json flowchart
boolean is_active
datetime created_at
datetime updated_at
}
TODO_ITEMS {
string id PK
string type "ticket/approval/device"
string title
string priority "urgent/high/normal"
json description
string status "pending/processing/resolved"
string assigned_agent_id
string corp_id
datetime created_at
datetime updated_at
}
ROLES {
string id PK
string name UK "user/agent/admin"
string display_name
text description
json permissions
boolean is_default
datetime created_at
datetime updated_at
}
USER_ROLES {
string id PK
string employee_id
string role_id FK
string source "auto/tag/ehr/manual"
string assigned_by
datetime assigned_at
datetime expires_at
}
ROLE_MAPPING_RULES {
string id PK
string role_id FK
string source_type "wecom_tag/ehr_position"
string source_value
int priority
boolean is_active
datetime created_at
}
```
### 1.3 ER 关系总结
```
┌────────────┐
│ EMPLOYEES │
│ (员工) │
└─────┬──────┘
│ corp_id + employee_id
┌─────────────┼─────────────┐
│ │ │
v v v
┌────────┐ ┌──────────────┐ ┌──────────────┐
│CONVER- │ │ USER_ROLES │ │TODO_ITEMS │
│SATIONS │ │ ↕ │ │(企业内待办) │
└──┬─────┘ │ ROLES │ └──────────────┘
│ │↕ │
│ │ROLE_MAPPING │
│ │_RULES │
│ └──────────────┘
│ 1:N
v
┌────────┐ 1:N ┌────────┐
│MESSAGES│◄─────│AGENT_ │
└────────┘ │NOTES │
└────┬───┘
│ 写
v
┌────────┐
┌──────────┐ │AGENTS │
│CONFIGS │ └───┬────┘
│ ↕ │ │ 改
│CHANGE │◄───────┘
│LOGS │
└──────────┘
```
**关系数**: 13 个外键关联 + 3 个 JSON 数组(协作/参与者/技能)
**外键关系**:
1. `conversations.employee_id` → 企微 ID(无 DB FK,跨企业灵活)
2. `conversations.assigned_agent_id``agents.id`(可空)
3. `messages.conversation_id``conversations.id` (CASCADE)
4. `agent_notes.conversation_id``conversations.id` (CASCADE)
5. `agent_notes.agent_id``agents.id`(无 CASCADE)
6. `user_roles.role_id``roles.id` (CASCADE)
7. `role_mapping_rules.role_id``roles.id` (CASCADE)
8. `config_change_logs.changed_by``agents.id`(无 FK)
9. `quick_reply_templates.submitted_by``agents.id`(可空)
---
## 📌 2. 字段-模块映射
| 业务模块 | 主要表 | 关键字段 |
|---|---|---|
| 鉴权登录 | `agents`, `employees`, `roles`, `user_roles` | `user_id`, `password_hash`, `otp_secret`, `role` |
| 会话管理 | `conversations` | `status`, `urgency_score`, `assigned_agent_id`, `is_vip` |
| 消息 | `messages` | `sender_type`, `content`, `msg_type`, `reply_to_id` |
| AI 助手 | `conversations`, `system_configs` | `dify_conversation_id`, `ai_substantive_reply_count` |
| 排障 | `troubleshooting_templates` | `path_steps`, `flowchart` |
| 快速回复 | `quick_reply_templates` | `category`, `content`, `variables` |
| 待办 | `todo_items` | `type`, `status`, `priority` |
| 工具面板 | `approval_links`, `software_downloads`, `funny_phrases` | `category`, `scene` |
| 动态配置 | `system_configs`, `config_change_logs` | `config_key`, `config_value` |
| 审计 | `config_change_logs` | `changed_by`, `changed_at`, `old_value`, `new_value` |
---
## 📌 3. 数据规模评估(生产估算)
| 表 | 日增(估) | 总量/年 | 备注 |
|---|---|---|---|
| `conversations` | 100-500 | 50K-100K | 视企业规模 |
| `messages` | 1K-10K | 1M-3M | 高频 |
| `employees` | 10-30 | 5K-20K | 增长慢 |
| `agents` | 0-1 | 20-50 | 增长极慢 |
| `agent_notes` | 50-200 | 30K-70K | 每会话 1-2 条 |
| `quick_reply_templates` | 1-3 | 50-200 | 缓慢增长 |
| `system_configs` | 0-1 | 50-100 | 极慢 |
| `config_change_logs` | 5-20 | 5K-10K | 审计 |
| `todo_items` | 50-200 | 30K-70K | 流转快 |
| `troubleshooting_templates` | 0-1 | 30-50 | 缓慢 |
| `funny_phrases` | 0 | 30-50 | 几乎不变 |
| `approval_links` | 0-1 | 20-50 | 缓慢 |
| `software_downloads` | 0-1 | 30-80 | 缓慢 |
| `roles` | 0 | 3-10 | 几乎不变 |
| `user_roles` | 5-15 | 5K-20K | 跟员工同步 |
| `role_mapping_rules` | 0 | 5-15 | 几乎不变 |
**总数据量估算**: 第 1 年 ~5-10 MB(纯数据), 含索引 ~20-50 MB
**建议**: PostgreSQL 起步 10 GB 足够,3-5 年无需扩容
---
## 📌 4. 环境变量清点(15 个 + 4 个文档化待补)
### 4.1 后端核心(`backend/app/config.py`)
| # | 变量 | 类型 | 默认 | 必填 | 敏感 | 用途 |
|---|---|---|---|---|---|---|
| 1 | `WECOM_CORP_ID` | str | ww1234... | ✅ | ❌ | 企微企业 ID |
| 2 | `WECOM_AGENT_ID` | str | 1000002 | ✅ | ❌ | 企微应用 ID |
| 3 | `WECOM_SECRET` | str | your-agent-secret | ✅ | 🔴 高 | 企微应用 Secret |
| 4 | `WECOM_TOKEN` | str | your-callback-token | ✅ | 🟠 中 | 回调 Token |
| 5 | `WECOM_ENCODING_AES_KEY` | str | your-aes-key-43-... | ✅ | 🟠 中 | 回调 AES Key |
| 6 | `DATABASE_URL` | str | postgresql://wecom:... | ✅ | 🔴 高(密码部分) | DB 连接 |
| 7 | `REDIS_URL` | str | redis://localhost:6379/0 | ✅ | 🟠 中(密码) | Redis 连接 |
| 8 | `BACKEND_HOST` | str | 0.0.0.0 | ❌ | ❌ | 监听地址 |
| 9 | `BACKEND_PORT` | int | 8000 | ❌ | ❌ | 监听端口 |
| 10 | `CORS_ORIGINS` | str(逗号分隔) | localhost:5173,5174 | 🟡 生产必填 | ❌ | CORS 白名单 |
| 11 | `DIFY_API_URL` | str | "" | 🟡 启用 AI 必填 | ❌ | Dify Chat 端点 |
| 12 | `DIFY_API_KEY` | str | "" | 🟡 启用 AI 必填 | 🔴 高 | Dify API Key |
| 13 | `DIFY_TIMEOUT` | int | 30 | ❌ | ❌ | Dify 超时 |
| 14 | `DIFY_WINGMAN_API_URL` | str | "" | ❌ | ❌ | Wingman 端点 |
| 15 | `DIFY_WINGMAN_API_KEY` | str | "" | ❌ | 🔴 高 | Wingman Key |
| 16 | `DIFY_WINGMAN_TIMEOUT` | int | 30 | ❌ | ❌ | Wingman 超时 |
| 17 | `MOCK_LOGIN_ENABLED` | bool | false | ❌ | ❌ | Mock 登录开关 |
**合计 17 个**(`Settings` 字段),**5 个敏感**(3 个 P0-高,2 个 P0-中)
### 4.2 部署相关(`deploy-server/.env` / `deploy-nas/.env.nas`)
| # | 变量 | 用途 |
|---|---|---|
| 18 | `POSTGRES_USER` | DB 用户名 |
| 19 | `POSTGRES_PASSWORD` | DB 密码(🔴) |
| 20 | `POSTGRES_DB` | DB 名 |
| 21 | `REDIS_PASSWORD` | Redis 密码(🔴) |
### 4.3 前端(Vue 4 个端)
| 前端 | 变量 | 用途 |
|---|---|---|
| admin | `VITE_API_BASE_URL` | 后端地址 |
| agent | `VITE_API_BASE_URL`, `VITE_WS_URL` | 后端 + WebSocket |
| h5 | `VITE_API_BASE_URL`, `VITE_WS_URL` | 同上 |
| portal | `VITE_API_BASE_URL`, `VITE_PORTAL_REDIRECT` | 入口跳转 |
### 4.4 漏配/待补
| # | 变量 | 状态 | 影响 |
|---|---|---|---|
| A | `LOG_LEVEL` | ❌ 缺失 | 日志粒度无法控制 |
| B | `JWT_SECRET` / `SESSION_SECRET` | ❌ 缺失 | token 加密用,但还没用 JWT |
| C | `WS_TOKEN_SECRET` | ❌ 缺失 | WS token 签名用 |
| D | `DIFY_PROXY_URL` | ❌ 文档化但未用 | 公司有内部 Dify,本项目直连 |
---
## 📌 5. 敏感凭据安全审计
### 5.1 现状
| # | 凭据 | 存储位置 | 风险 |
|---|---|---|---|
| 1 | WECOM_SECRET | `.env.production`(git?) | 🟠 中(看是否加 .gitignore) |
| 2 | POSTGRES_PASSWORD | `.env.production` | 🟠 中 |
| 3 | REDIS_PASSWORD | `.env.production` | 🟠 中 |
| 4 | DIFY_API_KEY | `.env.production` | 🟠 中 |
| 5 | 内部 Gitea tokens | wincred(✅) | 🟢 已修 |
### 5.2 待办(风险跟踪表 M-11)
- [ ] `.env.production` 是否在 .gitignore?(需确认)
- [ ] `.env.nas` 是否入仓?(文档明确说不入)
- [ ] 公司有内部 Vault?目前直连 Dify
- [ ] WECOM_TOKEN / AES_KEY 走 vault(下一轮)
### 5.3 短期方案(本周)
```bash
# 1. 验证 .gitignore 覆盖
git check-ignore -v .env.production .env.nas backend/.env
# 2. 验证仓里无 secret
git log --all -p --source -- .env.production 2>/dev/null | head -20
# 3. 跑 gitleaks 扫描
bash scripts/security-audit.sh --secrets
```
### 5.4 长期方案(下季度)
> ⚠️ NAS 部署方案已下线
1. **Server Keyring**:用 systemd-creds / HashiCorp Vault(当前推荐)
2. **环境变量注入**:容器启动时从 vault 拉,不入镜像
---
## 📌 6. 关联文档
- [[技术架构]] §3 数据层
- [[风险跟踪表]] M-11(凭据管理)/ D-3(DB 密码)
- [[外部系统集成]] §1-4(火绒/联软/aTrust/eHR 凭据)
- [[SOP-001-Gitea部署]] - token 走 wincred
- [[Gitea部署指南]] - Gitea app.ini 凭据
---
*本清点是 2026-06-15 Claude 满载跑批产出,待评审*
@@ -0,0 +1,59 @@
classDiagram
class Agent {
+str id
+str user_id
+str name
+str status
+str role
+list skill_tags
+int current_load
+int max_load
+datetime created_at
+datetime updated_at
}
class SystemConfig {
+str id
+str config_key
+str config_value
+str description
+datetime updated_at
}
class ConfigChangeLog {
+str id
+str config_key
+str old_value
+str new_value
+str changed_by
+datetime changed_at
}
class QuickReplyTemplate {
+str id
+str category
+str title
+str content
+list variables
+int sort_order
+str status
+int version
+str submitted_by
+datetime created_at
+datetime updated_at
}
class Conversation {
+str id
+str employee_id
+str employee_name
+str status
+int urgency_score
+str assigned_agent_id
+datetime created_at
}
ConfigChangeLog --> SystemConfig : tracks changes to
ConfigChangeLog --> Agent : changed_by
QuickReplyTemplate --> Agent : submitted_by
Conversation --> Agent : assigned_agent_id
@@ -0,0 +1,19 @@
sequenceDiagram
participant U as 管理员
participant FE as frontend-admin
participant API as /api/admin/configs/{key}
participant SVC as admin_service
participant DB as PostgreSQL
U->>FE: 切换应急模式开关
FE->>API: PUT /api/admin/configs/emergency_mode
API->>API: require_admin 校验权限
API->>SVC: update_config(key, value, agent_id)
SVC->>DB: SELECT SystemConfig WHERE key=emergency_mode
DB-->>SVC: 当前值 "false"
SVC->>DB: INSERT ConfigChangeLog(old="false", new="true", by=agent_id)
SVC->>DB: UPDATE SystemConfig SET value="true"
DB-->>SVC: 更新成功
SVC-->>API: {key, old_value, new_value, changed_at}
API-->>FE: 返回变更结果
FE->>FE: 显示变更成功提示
@@ -0,0 +1,16 @@
sequenceDiagram
participant U as 管理员(组长)
participant FE as frontend-admin
participant API as /api/agents/login
participant Redis as Redis
participant DB as PostgreSQL
U->>FE: 输入 user_id + name 登录
FE->>API: POST /api/agents/login
API->>DB: 查询 Agent (user_id)
DB-->>API: Agent 记录(含 role 字段)
API->>Redis: 存储 token → user_id 映射
API-->>FE: {agent_info, token, role: "admin"}
FE->>FE: 检查 role === "admin"
FE->>FE: 存储 admin_token 到 localStorage
FE->>FE: 跳转到 /admin/dashboard
@@ -0,0 +1,23 @@
sequenceDiagram
participant A as 坐席(王丽)
participant AG as /api/quick-replies
participant U as 管理员(宋献)
participant ADM as /api/admin/quick-replies
participant DB as PostgreSQL
A->>AG: POST /api/quick-replies (创建模板, status=draft)
A->>AG: PUT /api/quick-replies/{id} (提交审核, status→pending_review)
AG->>DB: UPDATE QuickReplyTemplate SET status='pending_review', submitted_by='wang_li'
U->>ADM: GET /api/admin/quick-replies/pending
ADM->>DB: SELECT WHERE status='pending_review'
DB-->>ADM: 待审核列表
ADM-->>U: 显示待审核模板
U->>ADM: PUT /api/admin/quick-replies/{id}/review (action=approve)
ADM->>DB: UPDATE SET status='approved', version=version+1
DB-->>ADM: 更新成功
A->>AG: GET /api/quick-replies (获取可见模板)
AG->>DB: SELECT WHERE status='approved' OR (status='pending_review' AND submitted_by=自己)
DB-->>AG: 全员可见(approved) + 仅自己(pending_review)
@@ -0,0 +1,77 @@
classDiagram
direction TB
class Employee {
+str id
+str corp_id
+str employee_id
+str name
+str department
+str position
+str mobile
+str email
+str avatar
+int status
+str it_level ★NEW
+str it_level_source ★NEW
+dict notes ★NEW
+datetime last_login_at
+datetime created_at
+datetime updated_at
}
class Conversation {
+str id
+str corp_id
+str employee_id
+str employee_name
+str department
+str status
+bool is_vip
+bool is_pinned
+bool is_todo
+int urgency_score
+dict tags
+str assigned_agent_id
+list collaborating_agent_ids
+int impact_scope ★NEW
+bool is_blocking ★NEW
+str emotion_state ★NEW
+datetime last_message_at
+str last_message_summary
+datetime created_at
+datetime updated_at
}
class TodoItem {
+str id
+str type
+str title
+str priority
+dict description
+str status
+str assigned_agent_id
+str corp_id
+datetime created_at
+datetime updated_at
}
class TroubleshootingTemplate {
+str id
+str name
+str category
+list path_steps
+dict flowchart
+bool is_active
+datetime created_at
+datetime updated_at
}
Employee "1" --> "*" Conversation : has
Conversation "1" --> "*" TodoItem : may generate
TroubleshootingTemplate "1" --> "0..1" Conversation : applied to
note for Employee "it_level: bronze|silver|gold|platinum|diamond|star|king\nit_level_source: system|manual"
note for Conversation "impact_scope: 受影响人数\nis_blocking: 是否阻断性\nemotion_state: normal|anxious|angry|urgent"
note for TodoItem "type: ticket|approval|device\npriority: urgent|high|normal\nstatus: pending|processing|resolved"
note for TroubleshootingTemplate "category: vpn|email|system|account\npath_steps: [{label, status}]\nflowchart: 递归树结构"
@@ -0,0 +1,22 @@
sequenceDiagram
participant U as 坐席
participant TB as TopBar.vue
participant TS as useThemeStore
participant UT as useTheme.ts
participant DOM as document.documentElement
participant LS as localStorage
U->>TB: 点击 ☀️/🌙 切换开关
TB->>TS: toggleTheme()
TS->>TS: currentTheme = currentTheme === 'light' ? 'dark' : 'light'
TS->>UT: applyTheme(currentTheme)
UT->>DOM: setAttribute('data-theme', theme)
UT->>LS: setItem('theme', theme)
DOM-->>DOM: CSS 变量自动切换(:root / [data-theme="dark"]
DOM-->>U: 300ms 过渡动画,界面变色
Note over U,LS: 页面加载时
U->>UT: 首次进入页面
UT->>LS: getItem('theme')
LS-->>UT: 'dark' | 'light' | null
UT->>DOM: setAttribute('data-theme', theme || 'light')
@@ -0,0 +1,62 @@
sequenceDiagram
participant Browser as 坐席浏览器
participant App as Vue3 App
participant Store as Pinia Store
participant API as Backend API
participant DB as PostgreSQL
Note over Browser,App: 页面加载
Browser->>App: 挂载 Workspace.vue
App->>Store: 初始化 conversationStore
Store->>API: GET /api/conversations
API->>DB: SELECT * FROM conversations WHERE status IN ('queued','serving') ORDER BY ...
DB-->>API: 会话列表
API-->>Store: 会话数据
Store-->>App: 渲染会话列表
loop 每3秒 setInterval
App->>Store: pollConversations()
Store->>API: GET /api/conversations?page=1&page_size=50
API->>DB: SELECT ... (同上)
DB-->>API: 最新会话列表
API-->>Store: 最新数据
alt 数据有变化
Store->>Store: diff 比较,更新变化的会话
Store-->>App: 触发响应式更新
App->>App: 更新列表项标签/排序/未读数
else 数据无变化
Store-->>App: 无需更新
end
end
Note over App: 用户点击某个会话
App->>Store: selectConversation(id)
Store->>API: GET /api/conversations/{id}/messages?limit=50
API->>DB: SELECT * FROM messages WHERE conversation_id=... ORDER BY created_at
DB-->>API: 消息列表
API-->>Store: messages
Store-->>App: 渲染对话区
loop 选中会话的消息轮询
App->>Store: pollMessages(conv_id)
Store->>API: GET /api/conversations/{id}/messages?limit=20&before=latest_id
API->>DB: SELECT ... WHERE created_at > latest
DB-->>API: 新消息
API-->>Store: 新消息列表
alt 有新消息
Store->>Store: 追加消息到列表
Store-->>App: 滚动到底部,显示新消息
end
end
Note over App: 坐席发送回复
App->>Store: sendMessage(conv_id, content)
Store->>API: POST /api/conversations/{id}/messages {content}
API->>DB: INSERT INTO messages ...
API-->>Store: 发送的消息对象
Store->>Store: 追加到消息列表
Store-->>App: 显示在对话区
@@ -0,0 +1,69 @@
sequenceDiagram
participant WX as 企微消息
participant API as FastAPI
participant Router as MessageRouter
participant Score as ScoringService
participant Vip as VipService
participant DB as PostgreSQL
participant Redis as Redis
WX->>API: 员工消息回调
API->>Router: route_message(msg)
rect rgb(255, 240, 240)
Note over Router,Score: Step 1: VIP检测
Router->>Vip: is_vip(employee_id)
Vip->>Redis: GET vip_cache:{employee_id}
alt 缓存命中
Redis-->>Vip: is_vip=True/False
else 缓存未命中
Vip->>Vip: get_user_info(employee_id)
Vip->>Vip: _check_vip_rules(user_info)
Note over Vip: 规则: 总监及以上 或 关键部门
Vip->>Redis: SET vip_cache:{employee_id} EX 3600
end
Vip-->>Router: is_vip=True/False
end
rect rgb(255, 255, 220)
Note over Router,Score: Step 2: 情绪关键词检测
Router->>Score: detect_emotion(msg)
Score->>DB: SELECT FROM system_configs WHERE key LIKE 'emotion_keywords_%'
DB-->>Score: 关键词列表
Score->>Score: 遍历关键词匹配消息内容
Score-->>Router: emotion="urgent"
end
rect rgb(220, 255, 220)
Note over Router,Score: Step 3: 举手检测
Router->>Score: detect_hand_raise(msg)
Score->>DB: SELECT FROM system_configs WHERE key='hand_raise_keywords'
DB-->>Score: ["转人工","人工",...]
Score->>Score: 遍历关键词匹配
Score-->>Router: hand_raise=True
end
rect rgb(220, 220, 255)
Note over Router,Score: Step 4: 需介入检测
Router->>Score: detect_need_intervene(conv)
Score->>DB: SELECT COUNT FROM messages WHERE conversation_id=... AND sender_type='employee'
DB-->>Score: 员工消息数
Score->>DB: SELECT FROM system_configs WHERE key='intervene_round_threshold'
DB-->>Score: 3
Score->>Score: 员工连续追问 > 3轮?
Score-->>Router: need_intervene=True
end
rect rgb(255, 220, 255)
Note over Router,Score: Step 5: 紧急度计算
Router->>Score: calculate_urgency(conv, msg)
Score->>Score: base = keyword_score(1)
Score->>Score: + emotion_bonus(1)
Score->>Score: + vip_bonus(1)
Score->>Score: + repeat_bonus(1)
Score->>Score: total = min(5, base+emotion+vip+repeat)
Score-->>Router: urgency_score=4
end
Router->>DB: UPDATE conversations SET tags=..., urgency_score=4, is_vip=...
Router-->>API: 更新后的会话
@@ -0,0 +1,43 @@
sequenceDiagram
participant Emp as 员工(H5页面)
participant API as FastAPI
participant ConvSvc as ConversationService
participant Score as ScoringService
participant WXSvc as WecomService
participant Agent as 坐席工作台
participant DB as PostgreSQL
Emp->>API: POST /api/h5/conversation/shake
API->>ConvSvc: find_or_create_conversation(employee_id)
ConvSvc->>DB: 查询/创建 conversation
DB-->>ConvSvc: conversation
API->>Score: detect_hand_raise("摇人")
Score-->>API: hand_raise=True
API->>ConvSvc: update_conversation(tags={hand_raise:true})
ConvSvc->>DB: UPDATE conversations SET tags=...
API->>ConvSvc: get_funny_phrase(scene='shake')
ConvSvc->>DB: SELECT FROM funny_phrases WHERE scene='shake'
DB-->>ConvSvc: "大哥,俺这就去摇人,稍等..."
ConvSvc->>ConvSvc: send_message(conv_id, 'system', 趣味话术)
ConvSvc->>WXSvc: send_text_message(employee_id, 趣味话术)
API-->>Emp: {conversation, funny_phrase}
Note over Emp: H5显示摇人动画 + 趣味话术
loop 坐席轮询
Agent->>API: GET /api/conversations
API->>ConvSvc: get_conversations()
ConvSvc->>DB: SELECT ... ORDER BY urgency DESC
DB-->>ConvSvc: 列表(举手会话靠前)
API-->>Agent: 举手标记的会话(黄色标签)
end
Agent->>API: POST /api/conversations/{id}/assign {agent_id}
API->>ConvSvc: update_conversation(status='serving', assigned_agent_id)
ConvSvc->>DB: UPDATE conversations SET status='serving'
ConvSvc->>ConvSvc: send_message(conv_id, 'system', '人摇来了!IT坐席为您服务')
ConvSvc->>WXSvc: send_text_message(employee_id, 接入话术)
ConvSvc-->>API: updated conversation
API-->>Agent: 接单成功
WXSvc-->>Emp: 企微推送"人摇来了!IT坐席为您服务"
+64
View File
@@ -0,0 +1,64 @@
# 03-技术架构 目录说明
> 本目录存放 IT 智能服务台项目的技术架构相关文档。
## 目录结构
```
03-技术架构/
├── 00-系统架构设计文档-v1.3.md # 主文档:系统架构总览(v1.3)
├── 01-ADRs-架构决策/ # 架构决策记录 (ADR)
├── 02-技术方案/ # 技术方案文档
├── 03-技术分析/ # 技术分析报告
├── 04-数据库设计/ # 数据库设计
├── 05-架构图/ # Mermaid 图表
└── archive/ # 归档目录:已废弃/旧版文档
```
## 文档清单
### 01-ADRs-架构决策/
| 文档 | 说明 |
|------|------|
| ADR-001-Gitea自托管-Funnel暴露.md | 架构决策 #001 |
| ADR-002-WS-Token-Subprotocol鉴权.md | 架构决策 #002 |
| ADR-003-nginx-access_log关闭.md | 架构决策 #003 |
| ADR-004-Token不入文件-走wincred.md | 架构决策 #004 |
### 02-技术方案/
| 文档 | 说明 |
|------|------|
| 技术方案-ExternalSystemAdapter抽象层.md | 外部系统适配器设计 |
| 技术方案-消息功能详细设计.md | 消息功能详细设计 |
| 技术方案-摇人协作.md | 摇人功能技术方案 |
| 技术方案-邀请功能.md | 邀请功能技术方案 |
| 技术方案-复杂场景重构.md | 复杂场景重构方案 |
### 03-技术分析/
| 文档 | 说明 |
|------|------|
| 技术分析-架构消息知识库迭代.md | 架构消息知识库迭代分析 |
| 技术分析-H5右侧栏动态推送评估.md | H5右侧栏动态推送评估 |
| 技术分析-jp-webcli自动化部署能力分析.md | JumpServer自动化部署分析 |
### 04-数据库设计/
| 文档 | 说明 |
|------|------|
| 数据库设计-ER图与环境变量清点.md | 数据库ER图与环境变量 |
### 05-架构图/
| 文档 | 说明 |
|------|------|
| class-diagram.mermaid | 核心类图 |
| sequence-diagram.mermaid | 核心序列图 |
| sequence-scoring.mermaid | 评分序列图 |
| sequence-polling.mermaid | 轮询序列图 |
| sequence-shake.mermaid | 摇人序列图 |
| admin-class-diagram.mermaid | 管理端类图 |
| admin-login-sequence.mermaid | 管理端登录序列图 |
| admin-config-change-sequence.mermaid | 配置变更序列图 |
| admin-quick-reply-review-sequence.mermaid | 快速回复审核序列图 |
---
*最后更新: 2026-07-04*