chore(docs): docs/ 目录全面重新编号 + 重组

**重构前**(旧编号 02-11):
- docs/02-产品需求/      → 00 产品规划/PRD
- docs/03-技术架构/      → 01-05 子目录散落
- docs/04-原型设计/      → 01-02 产品设计(HTML 原型)
- docs/05-原型设计/      → screens/
- docs/06-测试素材/      → 02-E2E / 03-功能 / 04-版本测试
- docs/07-项目管理/      → 任务说明书/日报/计划
- docs/08-安全审计/      → 审计报告
- docs/09-堡垒运维/      → toolbox / deploy
- docs/10-项目管理/      → 任务说明书(重复)
- docs/11-历史归档/      → deploy-nas-archived

**重构后**(新编号 00-07,语义化):
- docs/00-产品开发流程与文档管理规范.md
- docs/00-版本迭代总览.md
- docs/01-产品文档/      (PRD/原型/认证/会话/AI 服务/坐席/集成)
- docs/02-技术文档/      (技术方案/架构图/重构记录/前端改造/实现配置)
- docs/03-测试文档/      (E2E/功能用例/版本报告/缺陷单)
- docs/04-运维文档/      (部署运维/运维指南)
- docs/05-运营文档/      (品牌推广/用户手册)
- docs/06-安全审计/      (审计报告)
- docs/07-项目管理/      (任务说明书/日报/计划/看板)

**净收益**:
- 目录编号与产品文档管理规范对齐(按文档阶段 01-07 编号)
- 消除 02-产品需求 与 10-项目管理 的编号重叠
- 子目录按文档类型分组(如 01-产品文档/00-产品规划、01-产品文档/01-认证与登录)
- 把运维/安全/项目管理从 0X 散落改为 04/06/07

合计 494 文件 + 78495 行 / - 14076 行
This commit is contained in:
Simon
2026-08-03 18:46:55 +08:00
parent 3a44141eac
commit 44e77dcb0e
494 changed files with 78494 additions and 14075 deletions
@@ -0,0 +1,345 @@
# PRD: OpenClaw 安全合规检查与通知系统
> **版本**: v1.0
> **日期**: 2026-07-20
> **状态**: [草稿]
> **作者**: Simon
> **需求编号**: REQ-集成-001
> **关联文档**:
> - 原型: `01-产品文档/08-集成生态/原型-REQ-集成-001-OpenClaw合规检查-v1.0.html`
> - 技术: `02-技术文档/实现配置/技术方案-REQ-集成-001-OpenClaw合规检查-v1.0.md`
> - 测试: `03-测试文档/03-功能测试用例/TC-REQ-集成-001-OpenClaw合规检查.md`
---
## 1. 问题陈述 (Problem Statement)
### 背景
在重大安全活动期间(如国家网络安全周、重大会议保障期),公司需要对终端进行合规审计。
> 💡 **本期目标**:以 OpenClaw 检测为 **一期试点**,验证安全策略检查与通知框架的可行性。
>
> **扩展性设计**:本系统设计为 **通用安全策略检查平台**,未来可低成本扩展其他策略和推送渠道。
### 问题描述
- **现状**:目前依赖手工导出联软数据或通过火绒终端管理查看,缺乏自动化的定期检查和通知机制
- **痛点**
- 无法及时发现违规软件安装
- 依赖人工通知,效率低、易遗漏
- 无法形成定期的合规审计报告
### 影响
- **安全风险**:未经授权的远程软件可能成为攻击入口
- **合规风险**:无法满足重大活动期间的安全审计要求
- **业务影响**:IT 运维人员需要投入大量人工排查
---
## 2. 目标 (Goals)
| # | 目标 | 成功衡量标准 |
|---|------|-------------|
| G1 | **一期试点**:自动检测 OpenClaw 安装 | 系统可自动发现所有已安装 OpenClaw 的终端,覆盖率 ≥ 95% |
| G2 | 及时通知当事人 | 检测到违规软件后,24 小时内通过企微发送通知给终端用户 |
| G3 | 支持定时+手动执行 | 支持每日定时执行 + 管理员手动触发两种方式 |
| G4 | 合规审计留痕 | 支持导出 Excel 审计报告,保留检测记录至少 6 个月 |
| G5 | 活动期间管控 | 支持设置活动期间,自动开启检查 |
| G6 | **框架可扩展** | 新增其他安全策略和推送渠道的成本 ≤ 1人天 |
---
## 3. 非目标 (Non-Goals)
| # | 非目标 | 原因 |
|---|--------|------|
| N1 | **自动封禁/断网** | 仅做通知提醒,不做自动阻断,避免影响业务连续性 |
| N2 | ~~**支持其他软件检查**~~ | ⚠️ 已调整为扩展性设计,详见下方框架说明 |
| N3 | **实时监控** | 采用定时检查(非实时推送),5 分钟级延迟可接受 |
| N4 | **移动端管理** | 本期仅支持 PC Web 端管理 |
| N5 | **火绒+联软自动同步** | 联软数据本期采用手动导入 Excel,不做自动 API 对接 |
---
## 3.1 扩展性框架设计(重要)
> 📐 **架构理念**:将"检测策略"与"推送渠道"解耦,实现可扩展的安全合规平台
### 3.1.1 策略类型(Strategy
| 策略ID | 策略名称 | 检测规则 | 数据源 | 状态 |
|--------|----------|----------|--------|------|
| openclaw_remote | OpenClaw/向日葵远程软件 | 软件名包含 OpenClaw/向日葵/sunflower | 火绒API | 🔵 一期 |
| unknown_usb | 未知USB设备 | 未登记的USB设备接入 | 联软/火绒 | ⚪ 二期 |
| illegal_proxy | 非法代理软件 | 软件名包含 proxy/vpn | 火绒API | ⚪ 二期 |
| unverified_software | 未授权软件 | 不在白名单内的软件 | 火绒API | ⚪ 二期 |
### 3.1.2 推送渠道(Channel
| 渠道ID | 渠道名称 | 适用场景 | 状态 |
|--------|----------|----------|------|
| wecom_app | 企微应用消息 | 终端用户通知 | 🔵 一期 |
| wecom_group | 企微群通知 | 部门汇总、管理员 | 🔵 一期 |
| email | 邮件通知 | 正式留痕、邮件通知 | ⚪ 二期 |
| sms | 短信通知 | 紧急告警、无企微用户 | ⚪ 二期 |
| dingtalk | 钉钉通知 | 跨平台通知 | ⚪ 二期 |
### 3.1.3 策略-渠道映射
```
策略配置:
{
"strategy_id": "openclaw_remote",
"name": "OpenClaw远程软件检测",
"channels": [
{"id": "wecom_app", "template": "终端用户通知"},
{"id": "wecom_group", "template": "部门汇总"}
],
"schedule": "0 8 * * *", # 每天8点
"enabled": true
}
```
### 3.1.4 扩展成本
| 扩展项 | 工作量预估 | 说明 |
|--------|------------|------|
| 新增检测策略 | 0.5人天 | 仅需配置检测规则和数据源 |
| 新增推送渠道 | 1人天 | 需开发渠道适配器 |
| 新策略+新渠道 | 1人天 | 配置映射关系即可 |
---
## 4. 用户故事 (User Stories)
### 角色定义
- **IT 管理员**:负责配置检查策略、查看检测结果、触发手动检查
- **终端用户**:收到通知的被检查对象
- **部门负责人**:(可选)收到本部门汇总通知
### 用户故事列表
| # | 角色 | 用户故事 | 优先级 |
|---|------|----------|--------|
| US1 | IT 管理员 | 我希望系统能自动从火绒 API 获取终端软件列表,以便及时发现 OpenClaw 安装 | P0 |
| US2 | IT 管理员 | 我希望能够导入联软导出的用户-终端关联 Excel,以便知道哪台设备属于哪个用户 | P0 |
| US3 | IT 管理员 | 我希望系统能自动匹配"终端→用户",输出"谁在哪台设备安装了 OpenClaw",以便精准确认责任人 | P0 |
| US4 | IT 管理员 | 我希望能够设置定时任务(如每天 8:00 自动检查),以便在活动期间自动执行 | P0 |
| US5 | IT 管理员 | 我希望能够手动点击"立即检查"按钮,以便随时执行检查 | P0 |
| US6 | IT 管理员 | 我希望能够编辑通知模板(包含变量),以便自定义通知内容 | P1 |
| US7 | IT 管理员 | 我希望能够设置"是否通知部门负责人"开关,以便灵活配置通知范围 | P1 |
| US8 | IT 管理员 | 我希望能够导出 Excel 报告,以便留存审计记录 | P0 |
| US9 | 终端用户 | 我希望收到企微应用消息通知,告诉我哪台设备安装了违规软件,以便及时卸载 | P0 |
| US10 | 部门负责人 | 我希望收到本部门的汇总通知(可选),以便了解本部门合规情况 | P1 |
| US11 | IT 管理员 | 我希望能够新增其他安全策略(如未知USB、非法代理),以便扩展检测范围 | P0 |
| US12 | IT 管理员 | 我希望能够配置不同策略使用不同推送渠道(如邮件、短信),以便灵活通知 | P1 |
---
## 5. 需求规格 (Requirements)
### 5.1 功能需求
> 📌 **需求说明**:一期以 OpenClaw 为试点,但架构设计需支持策略和渠道的扩展。
#### T0: 策略与渠道管理(框架层)
| 需求ID | 描述 | 优先级 | 验收标准 |
|--------|------|--------|----------|
| REQ-001-00 | 策略配置:支持创建/编辑/启停安全策略 | P0 | 可配置策略名称、检测规则、数据源、推送渠道 |
| REQ-001-00a | 预置策略:一期预置"OpenClaw远程软件"检测策略 | P0 | 内置关键词规则(OpenClaw/向日葵/sunflower |
| REQ-001-00b | 渠道配置:支持配置推送渠道参数 | P0 | 企微渠道:应用ID/Secret;邮件渠道:SMTP配置 |
| REQ-001-00c | 渠道适配器:企微应用消息发送 | P0 | 复用现有企微消息能力 |
| REQ-001-00d | 渠道适配器:(扩展)邮件发送 | P2 | 可延后至二期 |
| REQ-001-00e | 渠道适配器:(扩展)短信发送 | P2 | 可延后至二期 |
#### T1: 数据采集层
> ⚠️ **注意**:火绒 API 集成功能已存在于系统中(`app/integrations/huorong`),无需重复开发。
> - 现有 API`GET /api/admin/integrations/huorong/terminals` - 获取终端列表
> - 现有 API`GET /api/admin/integrations/huorong/terminals/{client_id}` - 获取终端详情(含软件列表)
> - 本期仅需确认现有 API 满足 OpenClaw 检测需求(获取终端软件列表)。
| 需求ID | 描述 | 优先级 | 验收标准 |
|--------|------|--------|----------|
| REQ-001-01 | ✅ 已存在:火绒 API 集成(`/api/clnts/_list` 获取终端列表) | - | 复用现有功能 |
| REQ-001-02 | ✅ 已存在:火绒 API 集成(`/api/clnts/_info2` 获取软件列表) | - | 复用现有功能,确认支持 OpenClaw 检测 |
| REQ-001-03 | 联软 Excel 导入:支持上传 .xlsx/.xls 文件 | P0 | 能解析联软导出的 Excel,提取用户-终端关联字段 |
| REQ-001-04 | 联软字段映射:自动识别 `用户名``设备名称``MAC地址``设备IP``部门名称` | P0 | 上传文件后自动匹配字段,无需手动配置 |
| REQ-001-05 | 联软数据存储:将导入的关联数据存入数据库 | P0 | 导入后数据持久化,支持后续匹配查询 |
#### T2: 数据融合引擎
| 需求ID | 描述 | 优先级 | 验收标准 |
|--------|------|--------|----------|
| REQ-001-06 | IP 地址匹配:优先通过 IP 完全匹配关联终端和用户 | P0 | 火绒终端 IP 与联软数据完全一致时,成功匹配 |
| REQ-001-07 | 主机名匹配:IP 未匹配时,通过主机名包含匹配 | P0 | 火绒主机名包含联软设备名称时,成功匹配 |
| REQ-001-08 | MAC 地址匹配:前两者未匹配时,通过 MAC 地址匹配 | P0 | 火绒 MAC 与联软 MAC 一致时,成功匹配 |
| REQ-001-09 | 未匹配记录标记:无法关联到用户的终端需标记为"未匹配" | P0 | 输出结果中明确标注哪些终端无法确定责任人 |
| REQ-001-10 | OpenClaw 命中过滤:仅输出安装了 OpenClaw 的终端 | P0 | 匹配关键词:`OpenClaw`, `向日葵`, `sunflower`(不区分大小写) |
#### T3: 策略规则与命中管理
| 需求ID | 描述 | 优先级 | 验收标准 |
|--------|------|--------|----------|
| REQ-001-11 | 白名单规则:支持配置排除的用户/终端 | P1 | 配置白名单后,命中用户不在通知范围内 |
| REQ-001-12 | 部门汇总:支持按部门聚合命中结果 | P1 | 统计每个部门的违规终端数量 |
| REQ-001-12a | 命中状态管理:支持手动修改命中记录状态 | P0 | 状态:待处理→已通知→已解决/误报 |
| REQ-001-12b | 自动状态更新:再次检测时自动判断状态变化 | P0 | 历史命中用户再次检测未命中,自动标记"已解决" |
| REQ-001-12c | 状态变更记录:记录状态变更时间和操作人 | P1 | 可查看每条记录的状态变更历史 |
#### T4: 通知模板管理
| 需求ID | 描述 | 优先级 | 验收标准 |
|--------|------|--------|----------|
| REQ-001-13 | 模板变量:支持变量替换({username}, {device}, {dept}, {software}, {detect_time} | P0 | 通知内容中变量被正确替换 |
| REQ-001-14 | 默认模板:内置默认通知模板 | P0 | 未配置模板时使用默认模板发送 |
| REQ-001-15 | 模板编辑:支持富文本编辑通知模板 | P1 | 可编辑标题和正文内容 |
| REQ-001-16 | 企微消息发送:通过企微应用消息接口发送通知 | P0 | 成功发送至终端用户企微 |
#### T5: 调度与执行
| 需求ID | 描述 | 优先级 | 验收标准 |
|--------|------|--------|----------|
| REQ-001-17 | 定时任务:支持配置 cron 表达式执行检查 | P0 | 每天 8:00 自动执行检查任务 |
| REQ-001-18 | 手动触发:提供"立即检查"按钮 | P0 | 点击后立即执行检查流程 |
| REQ-001-19 | 活动期间开关:支持设置活动起止时间 | P0 | 活动期间自动开启检查,非活动期间可手动开启 |
| REQ-001-20 | 执行日志:记录每次执行的起止时间、命中数量、发送结果 | P0 | 可查看历史执行记录 |
| REQ-001-21 | Excel 导出:支持导出检测结果为 Excel | P0 | 导出字段:用户名、用户全名、部门、设备名称、IP、MAC、软件名称、安装日期 |
### 5.2 边缘情况处理
| 场景 | 处理方式 |
|------|----------|
| 火绒 API 调用失败 | 记录错误日志,发送告警给 IT 管理员,继续使用上次导入的联软数据 |
| 联软 Excel 未导入 | 仅使用火绒数据,未匹配用户的终端标记为"用户未知" |
| 用户无企微账户 | 跳过发送,标记为"发送失败",记录日志 |
| 重复检测 | 同一用户同一天多次检测,仅发送首次通知(去重) |
| 空结果(无违规) | 发送"本次检查无违规"的汇总通知给管理员 |
---
## 6. 成功指标 (Success Metrics)
### 6.1 领先指标(Leading Indicators
| 指标 | 定义 | 目标 | 测量方式 |
|------|------|------|----------|
| 检测覆盖率 | 成功关联到用户的终端数 / 火绒总终端数 | ≥ 85% | 数据库统计 |
| 通知成功率 | 成功发送通知数 / 需通知总数 | ≥ 95% | 企微 API 返回 |
| 执行完成率 | 定时任务成功执行次数 / 计划执行次数 | ≥ 99% | 调度日志 |
### 6.2 滞后指标(Lagging Indicators
| 指标 | 定义 | 目标 | 测量方式 |
|------|------|------|----------|
| 闭环率 | 当事人确认处理(卸载)的比例 | ≥ 80% | 用户反馈或二次检测 |
| 投诉率 | 通知后用户投诉误报的比例 | < 5% | 反馈记录 |
| 合规达标率 | 活动期间最后一天违规终端清理比例 | 100% | 最终检测报告 |
---
## 7. 开放问题 (Open Questions)
| # | 问题 | 负责人 | 状态 |
|---|------|--------|------|
| Q1 | 联软 Excel 导入频率?建议每日导入一次还是每次检查前导入? | 产品 | 待确认 |
| Q2 | 部门负责人通知是否需要汇总(多条合并为1条)? | 产品 | 待确认 |
| Q3 | 活动期间结束后,是否需要自动关闭定时任务? | 产品 | 待确认 |
---
## 7.1 任务拆分说明(重要)
> ⚠️ **系统现有火绒 API 集成,无需重复开发**
| 任务 | 说明 | 状态 |
|------|------|------|
| **T0: 策略与渠道管理** | 需开发:策略配置、渠道适配器框架 | 🔲 待开发 |
| **T1a: 火绒API集成** | 现有功能已支持(`/api/admin/integrations/huorong/terminals` | ✅ 已存在 |
| **T1b: 联软Excel导入** | 需开发:从联软导出Excel导入用户-终端关联 | 🔲 待开发 |
| T2: 数据融合引擎 | 需开发:IP/主机名/MAC三级匹配 | 🔲 待开发 |
| T3: 策略规则引擎 | 需开发:白名单、命中过滤 | 🔲 待开发 |
| T4: 通知模板管理 | 需开发:模板编辑、企微消息发送 | 🔲 待开发 |
| T5: 调度与执行 | 需开发:定时任务、手动触发、活动开关 | 🔲 待开发 |
**实际开发任务**T0 + T1b + T2 + T3 + T4 + T5(共6个任务)
---
## 8. 时间计划 (Timeline)
### 阶段划分
> ⚠️ **注**:T1a(火绒API)已存在,无需开发。
| 阶段 | 内容 | 预估工期 |
|------|------|----------|
| Phase 0 | T0(策略与渠道框架)+ T1b(联软导入) | 2 天 |
| Phase 1 | T2(融合引擎)+ T3(策略规则) | 2 天 |
| Phase 2 | T4(通知模板)+ T5(调度执行+导出) | 2 天 |
| Phase 3 | 联调测试 + 验收 | 2 天 |
| **合计** | 实际开发 | **8 天** |
### 里程碑
| 日期 | 里程碑 |
|------|--------|
| 2026-07-25 | Phase 1-2 完成,进入联调 |
| 2026-07-28 | Phase 3-4 完成,进入测试 |
| 2026-07-30 | UAT 通过,具备上线条件 |
---
## 9. 风险与依赖 (Risks & Dependencies)
| 风险/依赖 | 影响 | 缓解措施 |
|-----------|------|----------|
| 火绒 API 性能 | 终端数量多时可能超时 | 分页获取,增加重试机制 |
| 联软数据更新 | 用户-终端关联可能变更 | 建议每次检查前重新导入最新 Excel |
| 企微消息频率限制 | 大批量发送可能触发限制 | 增加发送间隔,批量改单发 |
---
## 10. 附录
### 数据字段映射
#### 火绒终端字段(API 返回)
- `client_id`: 终端唯一标识
- `hostname`: 主机名
- `ip`: IP 地址
- `mac`: MAC 地址
- `software`: 已安装软件列表
#### 联软 Excel 字段
- `用户名`: 企微/AD 用户名
- `用户全名`: 真实姓名
- `部门名称`: 组织架构
- `设备名称`: 终端名称
- `设备IP`: IP 地址
- `MAC地址`: MAC 地址
- `软件名称`: 已安装软件
- `安装日期`: 软件安装时间
### 匹配优先级
1. IP 地址完全匹配
2. 主机名包含匹配(联软设备名称 in 火绒主机名)
3. MAC 地址匹配
---
## 变更记录
| 日期 | 版本 | 变更内容 | 变更人 | 变更原因 |
|------|------|----------|--------|----------|
| 2026-07-20 | v1.0 | 初始版本 | Simon | 新建需求 |
---
**审批记录**
| 角色 | 姓名 | 日期 | 签字 |
|------|------|------|------|
| 产品负责人 | | | |
| 技术负责人 | | | |
| 业务负责人 | | | |
@@ -0,0 +1,578 @@
# 会议室预定 — 小鱼易联终端 PRD
> **版本**: v1.0
> **日期**: 2026-07-15
> **作者**: 许清楚 (Xu) · 产品经理
> **状态**: 待评审
> **所属项目**: `wecom_it_smart_desk` — IT智能服务台
> **子系统**: 08-集成生态
> **模块**: 会议室预定
---
## 1. 项目信息
| 字段 | 值 |
|------|-----|
| **项目名称** | `meetingroom_booking` |
| **技术栈** | 终端页面: Vue3 + Tailwind CSS(横屏大屏适配)/ 后端: FastAPI + SQLAlchemy + PostgreSQL + Redis / 企微API: 会议室预定管理 + 会议室管理 / 小鱼易联: 终端管理API + Web SDK |
| **语言** | 中文 |
| **集成范围** | 小鱼易联终端定制页面 + 企微会议室预定API + 小鱼易联终端管理API |
| **UI风格** | 深色主题(适配会议室大屏),大字号触控交互,accent = #07C160(企微绿) |
### 原始需求复述
税友集团IT支持组希望通过小鱼易联终端(安装在会议室的物理大屏设备)实现会议室预定功能。核心诉求:
1. 在小鱼易联终端上定制页面,集成企微自定义应用中的会议室预定能力
2. 员工可通过H5/企微/终端页面多入口自助预定,预定结果同步到小鱼易联终端显示
3. 有小鱼易联API文档+开发者账号,企微侧也有会议室API可对接
### 架构决策(初步方案)
采用**企微会议室API为数据源 + 小鱼易联终端为展示端 + 现有后端为中间层**的架构:
```
┌─────────────────────────────────────────────────────────────────────┐
│ 系统架构概览 │
│ │
│ ┌──────────┐ ┌──────────────┐ ┌──────────────────────┐ │
│ │ 企微H5端 │ │ 企微客户端App │ │ 小鱼易联终端(大屏) │ │
│ │ (Vant4) │ │ (会议室入口) │ │ (Vue3+Tailwind) │ │
│ └─────┬────┘ └──────┬───────┘ └──────────┬───────────┘ │
│ │ │ │ │
│ └────────┬────────┘ │ │
│ │ │ │
│ ┌──────▼───────────────────────────────▼──────┐ │
│ │ 现有后端 (FastAPI + Redis) │ │
│ │ │ │
│ │ ┌─────────────┐ ┌──────────────────┐ │ │
│ │ │ 会议室API │ │ 终端管理API │ │ │
│ │ │ (企微对接) │ │ (小鱼易联对接) │ │ │
│ │ └──────┬──────┘ └────────┬─────────┘ │ │
│ └─────────┼─────────────────┼─────────────┘ │
│ │ │ │
│ ┌─────────▼─────┐ ┌────────▼──────────┐ │
│ │ 企微会议室API │ │ 小鱼易联开放平台 │ │
│ │ (get_booking │ │ (终端管理/SDK) │ │
│ │ book/cancel) │ │ │ │
│ └───────────────┘ └──────────────────┘ │
└─────────────────────────────────────────────────────────────────────┘
```
**关键设计决策**
- 企微会议室API为**唯一数据源**single source of truth),不在本地维护独立的预定数据
- 后端作为中间层:代理企微API调用(处理鉴权/限流/缓存),同时对接小鱼易联终端管理
- 终端页面通过浏览器/WebView运行,与后端通过REST API + WebSocket通信
---
## 2. 产品定义
### 2.1 产品目标
| # | 目标 | 衡量标准 |
|---|------|---------|
| G1 | **终端可视化会议室状态**:在会议室物理大屏上实时显示当前会议室的预定状态、即将到来的会议、空闲时段,让路过的人一眼看到是否可用 | 终端页面打开后3秒内显示完整状态;状态更新延迟不超过30秒 |
| G2 | **多入口自助预定闭环**:员工可通过企微H5、企微客户端、终端大屏三个入口完成「查看空闲→预定→取消」全流程,预定结果三端同步 | 任意入口预定后,其他入口在30秒内显示更新;预定操作完成时间不超过15秒 |
| G3 | **与现有IT智能服务台无缝集成**:会议室预定作为IT智能服务台的新功能模块复用现有后端基础设施(鉴权/Redis/企微对接),不引入新的独立服务 | 复用现有Settings配置体系和数据库连接;新增代码以模块化方式集成到`backend/app/`,不破坏现有功能 |
### 2.2 用户故事
| # | 角色 | 用户故事 |
|---|------|---------|
| US-1 | 普通员工 | **As a** 员工, **I want** 在会议室门口的大屏上看到当前会议室是否空闲、下一个会议什么时候开始, **so that** 我不用打开手机就能判断能否临时使用这个会议室 |
| US-2 | 普通员工 | **As a** 员工, **I want** 在终端大屏上直接点击空闲时段完成预定, **so that** 临时开会时不用掏手机操作,30秒内就能预定成功 |
| US-3 | 普通员工 | **As a** 员工, **I want** 在企微H5端或企微客户端查看所有会议室的空闲状态并预定, **so that** 在回到会议室之前就提前预定好,到场直接使用 |
| US-4 | 普通员工 | **As a** 员工, **I want** 预定后能随时取消(通过终端或企微), **so that** 会议取消后及时释放会议室资源 |
| US-5 | IT管理员 | **As a** IT管理员, **I want** 在管理后台查看所有会议室的预定情况和使用率统计, **so that** 能识别热门/冷门会议室,优化资源配置 |
| US-6 | IT管理员 | **As a** IT管理员, **I want** 将会议室与小鱼易联终端设备绑定(SN映射), **so that** 终端自动显示对应会议室的状态 |
| US-7 | 访客 | **As a** 访客, **I want** 在终端大屏上看到会议室名称和当前状态(无需登录即可查看), **so that** 找会议室时能快速确认位置 |
---
## 3. 需求池(P0/P1/P2
### P0 — 必须实现(Must Have
#### P0-1 企微会议室API对接 — access_token管理
| 项 | 说明 |
|------|------|
| **需求** | 在现有`WecomService`中新增会议室专用access_token管理。企微会议室API需要独立的"会议室"secret获取access_token(不同于普通应用secret和通讯录secret |
| **实现** | 在`config.py`中新增`wecom_meetingroom_secret`配置项;在`WecomService`中新增`get_meetingroom_access_token()`方法,复用Redis缓存模式(key: `wecom:meetingroom_access_token`TTL: 6900秒) |
| **验收** | 能成功获取会议室access_token;token缓存命中时不重复请求企微接口;token过期前自动刷新 |
#### P0-2 会议室列表查询接口
| 项 | 说明 |
|------|------|
| **需求** | 后端新增API`GET /api/meetingroom/list`,代理调用企微会议室列表接口`POST /cgi-bin/oa/meetingroom/list`,返回会议室列表(ID、名称、容量、位置、设备、是否需要审批) |
| **缓存** | 会议室列表变化频率低,Redis缓存10分钟 |
| **验收** | 返回完整的会议室信息;支持按城市/楼宇/楼层过滤;缓存失效后自动刷新 |
#### P0-3 会议室预定状态查询接口
| 项 | 说明 |
|------|------|
| **需求** | 后端新增API`GET /api/meetingroom/{meetingroom_id}/booking?date=YYYY-MM-DD`,代理调用企微`POST /cgi-bin/oa/meetingroom/get_booking_info`,返回指定会议室在指定日期的预定情况列表 |
| **限制** | 企微API不支持跨天查询,后端按日期维度封装 |
| **缓存** | 缓存30秒(保证实时性同时减少API调用) |
| **验收** | 返回预定列表(booking_id、start_time、end_time、booker、status);时间戳转为可读时间格式 |
#### P0-4 预定会议室接口
| 项 | 说明 |
|------|------|
| **需求** | 后端新增API`POST /api/meetingroom/book`,代理调用企微`POST /cgi-bin/oa/meetingroom/book` |
| **参数** | meetingroom_id、subject(会议主题)、start_time、end_time、booker(预定人userid)、attendees(参与人列表,可选) |
| **限制** | 时间自动按30分钟取整;仅可预定无需审批的会议室 |
| **验收** | 预定成功返回booking_id;时间冲突时返回明确错误信息;预定成功后立即清除该会议室的状态缓存 |
#### P0-5 取消预定接口
| 项 | 说明 |
|------|------|
| **需求** | 后端新增API`DELETE /api/meetingroom/booking/{booking_id}`,代理调用企微`POST /cgi-bin/oa/meetingroom/cancel_book` |
| **参数** | booking_id、meetingroom_id(企微取消接口需要booking_id即可,但查询需要meetingroom_id+booking_id |
| **验收** | 取消成功返回确认;取消后立即清除缓存;非预定人取消时返回权限错误 |
#### P0-6 终端页面 — 会议室状态展示
| 项 | 说明 |
|------|------|
| **需求** | 新建前端项目(或模块)`frontend-terminal`,使用Vue3 + Tailwind CSS,为小鱼易联终端大屏定制横屏页面 |
| **页面** | 终端状态页:全屏显示当前绑定的会议室名称、当前状态(空闲/使用中)、当前进行中的会议信息、时间轴展示当日预定情况 |
| **适配** | 1920×1080横屏适配;大字号(标题48px+,正文24px+);深色主题减少大屏眩光 |
| **验收** | 页面在终端浏览器/WebView中正常渲染;3秒内加载完毕;空闲/使用中状态视觉区分明显 |
#### P0-7 终端页面 — 快速预定
| 项 | 说明 |
|------|------|
| **需求** | 终端页面支持点击时间轴上的空闲时段进行快速预定 |
| **交互** | 点击空闲时段→弹出预定面板(输入会议主题、选择时长)→确认预定→显示预定结果→3秒后返回状态页 |
| **鉴权** | 终端预定时需要身份识别:扫码登录(企微扫码)或输入工号+姓名(访客模式仅查看,不可预定) |
| **验收** | 30分钟以内的空闲时段可一键预定;预定结果实时显示在终端页面上 |
#### P0-8 终端-会议室绑定关系管理
| 项 | 说明 |
|------|------|
| **需求** | 后端新增`terminal_room_binding`表,存储小鱼易联终端SN与企微会议室ID的映射关系 |
| **字段** | terminal_sn(终端序列号)、meetingroom_id(企微会议室ID)、terminal_name(终端名称)、location(位置描述)、created_at、updated_at |
| **管理** | 管理后台CRUD接口:`GET/POST/PUT/DELETE /api/admin/terminal-bindings` |
| **验收** | 支持一个终端绑定一个会议室;绑定后终端页面自动加载对应会议室的状态 |
### P1 — 应该实现(Should Have
#### P1-1 实时状态同步 — WebSocket推送
| 项 | 说明 |
|------|------|
| **需求** | 当会议室预定状态发生变化时(预定/取消),通过WebSocket实时推送到绑定的终端页面 |
| **方案** | 复用现有WebSocket基础设施(`ws.py`),新增终端WS端点`/ws/terminal/{terminal_sn}`。状态变更触发时:①清除Redis缓存 ②向已连接的终端推送状态更新消息 |
| **降级** | WebSocket不可用时,终端页面降级为30秒轮询 |
| **验收** | 一个入口预定后,其他入口的终端在5秒内显示更新;WS断线后自动降级为轮询 |
#### P1-2 企微回调 — 预定变更通知
| 项 | 说明 |
|------|------|
| **需求** | 配置企微事件回调,当会议室预定状态发生变化(企微客户端预定/取消/审批通过)时接收通知,主动刷新缓存并推送WS消息 |
| **回调** | 企微回调URL配置在管理后台,接收事件后调用`get_booking_info`刷新缓存 |
| **验收** | 企微客户端预定后,终端在10秒内收到更新(含回调延迟) |
#### P1-3 多会议室切换
| 项 | 说明 |
|------|------|
| **需求** | 终端页面支持查看其他会议室的状态(点击侧边栏/下拉选择) |
| **交互** | 顶部会议室名称可点击→弹出会议室选择列表(显示名称+位置+实时状态)→选择后切换显示 |
| **验收** | 切换后3秒内加载目标会议室状态;支持按楼宇/楼层筛选 |
#### P1-4 终端页面 — 会议详情展示
| 项 | 说明 |
|------|------|
| **需求** | 点击时间轴上的已预定时段,展示会议详情(主题、预定人、参与人、时间) |
| **限制** | 终端仅展示预定人姓名(不展示userid),参与人列表最多显示5人 |
| **验收** | 点击后弹出详情面板;信息来源为企微`bookinfo/get`接口 |
#### P1-5 H5端会议室预定页面
| 项 | 说明 |
|------|------|
| **需求** | 在现有H5端(Vue3 + Vant4)新增会议室预定页面,员工可在手机上查看所有会议室状态并预定 |
| **入口** | H5端首页新增「会议室」入口卡片 |
| **功能** | 会议室列表→选择会议室→查看时间轴→选择空闲时段→预定/取消 |
| **验收** | 移动端交互流畅;预定结果同步到终端和企微客户端 |
#### P1-6 小鱼易联终端管理API集成
| 项 | 说明 |
|------|------|
| **需求** | 通过小鱼易联开放平台API实现终端设备管理:根据终端SN查询设备状态、推送自定义页面URL到终端 |
| **接口** | 小鱼易联API基础URL`https://sdk.xylink.com/api/rest/external/v1/`,使用enterpriseId + token鉴权(支持签名1.0/2.0 |
| **功能** | ① 终端注册时自动上报SN ② 管理后台可远程推送页面URL到指定终端 ③ 查询终端在线状态 |
| **验收** | 能通过API查询终端在线状态;能远程推送页面URL |
### P2 — 可以实现(Nice to Have
#### P2-1 会议室使用统计报表
| 项 | 说明 |
|------|------|
| **需求** | 管理后台提供会议室使用率统计:按会议室/日期/时段统计预定时长、实际使用时长、空闲率 |
| **数据来源** | 每日定时拉取预定记录存入本地数据库(作为统计快照,非实时数据源) |
| **验收** | 支持按周/月查看统计图表;支持导出Excel |
#### P2-2 会议室维护模式
| 项 | 说明 |
|------|------|
| **需求** | 管理员可将会议室设为"维护中"状态,终端页面显示维护提示,禁止预定 |
| **实现** | 本地维护状态表,终端查询时叠加本地状态判断 |
| **验收** | 维护模式下终端显示维护提示;恢复后自动恢复预定功能 |
#### P2-3 终端页面 — 会议提醒
| 项 | 说明 |
|------|------|
| **需求** | 会议开始前5分钟,终端页面弹出提醒(如有下一个会议即将开始) |
| **验收** | 提醒动画醒目但不影响当前操作;5分钟后自动消失 |
#### P2-4 终端页面 — 二维码预定
| 项 | 说明 |
|------|------|
| **需求** | 终端页面显示企微预定二维码,员工可扫码后在手机上完成预定 |
| **验收** | 二维码指向H5端预定页面(带会议室ID参数);扫码后自动选中当前会议室 |
#### P2-5 小鱼易联Web SDK — 视频会议集成
| 项 | 说明 |
|------|------|
| **需求** | 预定会议后,可在终端上一键发起小鱼易联视频会议,将会议链接关联到企微预定记录 |
| **SDK** | `@xylink/xy-rtc-sdk`,初始化需要clientId + clientSecret + extId(企业ID |
| **验收** | 终端可发起视频会议;会议信息关联到企微日程 |
---
## 4. UI设计稿(文字描述)
### 4.1 终端状态页(主页面)
```
┌─────────────────────────────────────────────────────────────────────────────┐
│ 📍 18F-会议室A 2026-07-15 周三 14:32 │
│ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ │
│ │
│ ┌──────────────┐ │
│ │ │ │
│ │ 空闲中 │ ← 当前状态(大字 96px) │
│ │ │ │
│ └──────────────┘ │
│ │
│ 距下一个会议:28分钟 │
│ │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ 今日日程 14:32 ▮ │ │
│ │ ┌─────────┐ ┌──────────────┐ ┌──────────────────┐ ┌──────────┐ │ │
│ │ │ 09:00 │ │ 10:00 │ │ 14:00 │ │ 15:00 │ │ │
│ │ │ ~ 10:00 │ │ ~ 11:30 │ │ ~ 15:00 ← 进行中 │ │ ~ 16:00 │ │ │
│ │ │ 周度例会 │ │ 产品评审 │ │ 技术方案讨论 │ │ 空闲可预定│ │ │
│ │ │ 张三 │ │ 李四 │ │ 王五 │ │ 点击预定 │ │ │
│ │ └─────────┘ └──────────────┘ └──────────────────┘ └──────────┘ │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ [ 📱 扫码预定 ] [ 🔄 切换会议室 ] [ ❓ 使用帮助 ] │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────────────────┘
```
**设计要点**
- **深色主题**:背景 `#1a1a2e`,卡片 `#16213e`,减少大屏眩光
- **状态色**:空闲 = `#07C160`(企微绿),使用中 = `#FF6B6B`(红色),即将开始 = `#FFA502`(橙色)
- **字体**:当前状态 96px,会议室名称 36px,时间轴 20px,操作按钮 24px
- **时间轴**:横向滚动,每个时段卡片宽度固定,当前时间用竖线标记
- **空闲时段**:显示「点击预定」提示,可点击
- **已预定时段**:显示会议主题和预定人,不可点击(仅查看)
### 4.2 快速预定弹窗
```
┌───────────────────────────────────┐
│ 📅 预定会议室 │
│ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ │
│ │
│ 会议室:18F-会议室A │
│ 时段:15:00 ~ 15:30 │
│ │
│ 会议主题: │
│ ┌─────────────────────────────┐ │
│ │ 请输入会议主题 │ │
│ └─────────────────────────────┘ │
│ │
│ 时长: │
│ [ 30分钟 ] [ 60分钟 ] [ 90分钟 ] │
│ [ 自定义 ] │
│ │
│ 预定人:张三(扫码登录) │
│ │
│ ┌─────────────┐ ┌──────────────┐ │
│ │ 取消 │ │ 确认预定 │ │
│ └─────────────┘ └──────────────┘ │
└───────────────────────────────────┘
```
### 4.3 交互流程
```
终端页面加载
├── 读取终端SN → 查询绑定关系 → 获取meetingroom_id
│ └── 无绑定 → 显示「请联系IT管理员绑定会议室」提示页
├── 获取会议室信息 + 当日预定状态
│ └── 加载失败 → 显示错误页面 + 重试按钮
├── 渲染状态页
│ ├── 空闲 → 显示「空闲中」+ 时间轴空闲时段可点击
│ └── 使用中 → 显示「使用中」+ 当前会议信息
├── 用户点击空闲时段
│ ├── 未登录 → 弹出企微扫码登录二维码
│ │ └── 扫码成功 → 获取userid → 进入预定弹窗
│ └── 已登录 → 直接进入预定弹窗
├── 确认预定
│ ├── 调用后端API → 企微book接口
│ ├── 成功 → 显示「预定成功」动画 → 3秒后返回状态页(状态已更新)
│ └── 失败 → 显示错误原因(冲突/权限/网络) → 返回预定弹窗
└── 实时更新
├── WebSocket连接 → 收到状态变更 → 刷新页面
└── WebSocket断线 → 降级为30秒轮询
```
### 4.4 H5端预定页面
```
┌─────────────────────────┐
│ ← 会议室预定 │
│ ━━━━━━━━━━━━━━━━━━━━━ │
│ │
│ 📍 选择会议室 │
│ ┌───────────────────┐ │
│ │ 18F-会议室A 10人 │ │
│ │ ● 空闲 │ │
│ └───────────────────┘ │
│ ┌───────────────────┐ │
│ │ 18F-会议室B 20人 │ │
│ │ ● 使用中 │ │
│ └───────────────────┘ │
│ │
│ 📅 2026-07-15 │
│ ┌───────────────────┐ │
│ │ 时间轴(纵向) │ │
│ │ 09:00 ████████ │ │
│ │ 10:00 ████████ │ │
│ │ 11:00 ░░░░░░░░ │ │
│ │ 14:00 ████████ │ │
│ │ 15:00 ░░░░░░░░ │ │
│ │ 16:00 ░░░░░░░░ │ │
│ └───────────────────┘ │
│ │
│ [ + 预定空闲时段 ] │
└─────────────────────────┘
```
### 4.5 管理后台 — 终端绑定管理
```
┌─────────────────────────────────────────────────────────────┐
│ 会议室管理 > 终端绑定 │
│ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ │
│ │
│ [+ 新增绑定] [搜索: ___________] │
│ │
│ ┌────────┬──────────┬──────────┬──────────┬────────────┐ │
│ │ 终端SN │ 终端名称 │ 会议室 │ 位置 │ 操作 │ │
│ ├────────┼──────────┼──────────┼──────────┼────────────┤ │
│ │ XY001 │ 大屏A │ 18F-会议室A│ 18F东区 │ 编辑 删除 │ │
│ │ XY002 │ 大屏B │ 18F-会议室B│ 18F西区 │ 编辑 删除 │ │
│ │ XY003 │ 大屏C │ 19F-会议室C│ 19F东区 │ 编辑 删除 │ │
│ └────────┴──────────┴──────────┴──────────┴────────────┘ │
└─────────────────────────────────────────────────────────────┘
```
---
## 5. 技术规范
### 5.1 企微会议室API对接
| API | 企微接口 | 方法 | 说明 |
|-----|---------|------|------|
| 会议室列表 | `POST /cgi-bin/oa/meetingroom/list` | `WecomService.get_meetingroom_list()` | 缓存10分钟 |
| 预定状态查询 | `POST /cgi-bin/oa/meetingroom/get_booking_info` | `WecomService.get_booking_info()` | 缓存30秒 |
| 预定会议室 | `POST /cgi-bin/oa/meetingroom/book` | `WecomService.book_meetingroom()` | 成功后清缓存 |
| 取消预定 | `POST /cgi-bin/oa/meetingroom/cancel_book` | `WecomService.cancel_booking()` | 成功后清缓存 |
| 预定详情 | `POST /cgi-bin/oa/meetingroom/bookinfo/get` | `WecomService.get_booking_detail()` | 缓存5分钟 |
**access_token管理**
- 企微会议室API需要独立的"会议室"secret获取access_token
-`config.py`新增`wecom_meetingroom_secret`配置项
-`WecomService`中新增`get_meetingroom_access_token()`方法
- Redis缓存key: `wecom:meetingroom_access_token`TTL: 6900秒(提前300秒刷新)
- 参考现有`get_access_token()``_contact_token_cache`的实现模式
**企微API限制**
- 预定时间自动按30分钟取整(15:15→15:0015:45→16:00
- 仅可预定无需审批的会议室(`need_approval=0`
- `get_booking_info`不支持跨天查询,后端按日期维度封装
- 当前时间超过预定开始时间15分钟后不允许预定
- 会议室设备类型:1=电视, 2=电话, 3=投影, 4=白板, 5=视频
**官方文档**
- 会议室管理:https://developer.work.weixin.qq.com/document/path/93619
- 会议室预定管理:https://developer.work.weixin.qq.com/document/path/93620
### 5.2 小鱼易联终端管理API对接
| 能力 | 接口 | 说明 |
|------|------|------|
| 鉴权 | enterpriseId + token | 支持1.0signature)和2.0两种签名方式 |
| 终端管理 | `https://sdk.xylink.com/api/rest/external/v1/` | 根据SN或终端号管理设备配置 |
| 终端状态查询 | 终端管理API | 查询终端在线状态 |
| Web SDK | `@xylink/xy-rtc-sdk` | 初始化需要clientId + clientSecret + extId(企业ID |
**配置项**(新增到`config.py`):
- `xylink_enterprise_id`:小鱼易联企业ID
- `xylink_client_id`SDK客户端ID
- `xylink_client_secret`SDK客户端密钥
- `xylink_api_base`API基础URL(默认`https://sdk.xylink.com/api/rest/external/v1/`
**官方文档**https://openapi.xylink.com/
### 5.3 后端新增文件结构
```
backend/app/
├── api/
│ ├── meetingroom.py # 会议室预定API路由
│ └── admin/
│ └── terminal_binding.py # 终端绑定管理(管理后台)
├── services/
│ ├── meetingroom_service.py # 会议室预定业务逻辑
│ └── xylink_service.py # 小鱼易联终端管理服务
├── models/
│ └── terminal_room_binding.py # 终端-会议室绑定模型
└── config.py # 新增配置项
```
### 5.4 前端新增项目
```
frontend-terminal/ # 小鱼易联终端页面(新建)
├── src/
│ ├── views/
│ │ ├── StatusView.vue # 终端状态页(主页面)
│ │ └── BookingView.vue # 快速预定弹窗
│ ├── components/
│ │ ├── Timeline.vue # 时间轴组件
│ │ ├── StatusBadge.vue # 状态标识
│ │ └── QrLogin.vue # 扫码登录组件
│ ├── api/
│ │ └── meetingroom.ts # API调用
│ └── App.vue
├── tailwind.config.js # 横屏大屏适配配置
└── vite.config.ts
```
### 5.5 实时同步方案
| 方案 | 机制 | 延迟 | 适用场景 |
|------|------|------|---------|
| WebSocketP1) | 终端建立WS连接,后端状态变更时推送 | <5秒 | 终端在线时的实时同步 |
| 轮询(P0降级) | 终端每30秒请求一次状态 | <30秒 | WS不可用时的降级方案 |
| 企微回调(P1) | 企微预定变更时回调后端,触发缓存刷新+WS推送 | <10秒 | 企微客户端预定的同步 |
**P0阶段**:终端页面使用30秒轮询保证基本实时性。
**P1阶段**:升级为WebSocket + 企微回调,实现5秒内同步。
### 5.6 终端页面部署方案
| 方案 | 说明 | 优劣 |
|------|------|------|
| **方案A(推荐)** | 终端通过浏览器/WebView访问后端提供的URL | 灵活度高,更新无需触达终端;但依赖网络稳定 |
| 方案B | 通过小鱼易联终端管理API推送自定义页面URL | 更深度集成;但需要小鱼易联API支持自定义页面推送 |
| 方案C | 使用小鱼易联Web SDK在终端原生渲染 | 最深度集成;但开发成本高,P2阶段考虑 |
**P0推荐方案A**:终端浏览器全屏访问 `https://itsupport.servyou.com.cn/terminal/{sn}`,后端根据SN返回对应会议室的状态页面。
---
## 6. 数据模型
### 6.1 终端-会议室绑定表 `terminal_room_binding`
| 字段 | 类型 | 说明 |
|------|------|------|
| id | Integer (PK) | 自增主键 |
| terminal_sn | String(64) | 小鱼易联终端序列号(唯一) |
| terminal_name | String(100) | 终端名称(如"18F东区大屏" |
| meetingroom_id | Integer | 企微会议室ID |
| meetingroom_name | String(100) | 会议室名称(冗余字段,便于展示) |
| location | String(200) | 位置描述(如"18F东区" |
| is_active | Boolean | 是否启用(默认True |
| created_at | DateTime | 创建时间 |
| updated_at | DateTime | 更新时间 |
### 6.2 会议室预定快照表 `meetingroom_booking_snapshot`P2
| 字段 | 类型 | 说明 |
|------|------|------|
| id | Integer (PK) | 自增主键 |
| meetingroom_id | Integer | 企微会议室ID |
| booking_id | String(64) | 企微预定ID |
| subject | String(200) | 会议主题 |
| booker | String(64) | 预定人userid |
| start_time | DateTime | 开始时间 |
| end_time | DateTime | 结束时间 |
| status | Integer | 预定状态(0=已预定, 1=已取消) |
| snapshot_date | Date | 快照日期 |
| created_at | DateTime | 记录创建时间 |
---
## 7. 待确认问题
| # | 问题 | 影响范围 | 建议 |
|---|------|---------|------|
| Q1 | **企微"会议室"secret是否已申请?** 会议室API需要独立的secret获取access_token,不同于现有应用secret和通讯录secret。需要在企微管理后台「应用管理 > 会议室 > 可调用接口的应用」中配置。 | P0核心前置条件 | 请IT支持组确认是否已有会议室secret,或协助申请 |
| Q2 | **企微会议室是否已在管理后台创建?** 需要先在企微管理后台创建会议室(设置名称、容量、位置、设备等),API只能查询和预定已创建的会议室。 | P0核心前置条件 | 请确认企微后台已创建公司所有会议室 |
| Q3 | **小鱼易联终端的具体型号和运行环境?** 需要确认终端是通过内置浏览器/WebView运行页面,还是有其他自定义页面机制。不同型号可能支持的页面渲染能力不同。 | 影响前端技术选型 | 请提供终端型号,或确认是否支持浏览器全屏访问 |
| Q4 | **小鱼易联开发者账号的具体权限?** 需要确认开发者账号是否有终端管理API的调用权限,以及Web SDK的使用授权。 | P1小鱼易联API集成 | 请确认开发者账号的API权限范围 |
| Q5 | **终端身份识别方案?** 终端是公共设备,员工预定时需要身份识别。方案一:企微扫码登录(推荐,与现有系统一致);方案二:输入工号+短信验证码;方案三:企微NFC碰一碰。 | P0预定功能 | 建议方案一(企微扫码),与现有H5端登录方式一致 |
| Q6 | **是否需要支持访客预定?** 访客通常没有企微账号,无法通过企微API预定。如需支持,需要额外的访客登记流程。 | P0-P1 | 建议P0阶段仅支持企微员工预定,访客仅查看状态 |
| Q7 | **会议室是否需要与小鱼易联视频会议联动?** 即预定会议室后,是否需要自动在终端上发起小鱼易联视频会议? | P2功能 | 建议P0阶段仅做预定,P2阶段考虑视频会议联动 |
| Q8 | **终端页面部署在哪个域名下?** 是复用现有`itsupport.servyou.com.cn`域名(新增`/terminal/`路径),还是使用独立域名? | 影响部署和Nginx配置 | 建议复用现有域名,新增`/terminal/`路由 |
| Q9 | **与现有IT智能服务台的关系?** 会议室预定是作为独立功能模块(仅在终端和H5端提供入口),还是也需要集成到坐席端(如坐席帮员工预定)? | 影响功能范围 | 建议P0阶段聚焦终端+H5端,坐席端集成放到P1 |
| Q10 | **企微回调URL是否已配置?** 企微会议室预定状态变更回调需要配置回调URL,且需要企微后台配置可信域名。 | P1企微回调 | 请确认企微后台是否支持会议室事件回调配置 |
---
## 8. 里程碑建议
| 阶段 | 内容 | 预估工期 | 依赖 |
|------|------|---------|------|
| **M1** | 企微会议室API对接(token管理+CRUD接口)+ 终端状态页 | 1周 | Q1/Q2确认 |
| **M2** | 终端快速预定功能 + 终端-会议室绑定管理 | 1周 | M1完成 |
| **M3** | WebSocket实时同步 + 企微回调 + H5端预定页面 | 1.5周 | M2完成 |
| **M4** | 小鱼易联终端管理API集成 + 多会议室切换 | 1周 | Q3/Q4确认 |
| **M5** | 统计报表 + 维护模式 + P2功能 | 按需 | M4完成 |
**总预估**:4-5周(P0+P1),P2按需排期。
@@ -0,0 +1,658 @@
# 智能IT支持服务台 — 管理后台增量 PRD
> **文档版本**: v1.2
> **创建日期**: 2026-06-16 · **更新日期**: 2026-07-28
> **产品经理**: 许清楚 (Xu) · 宋献
> **状态**: 阶段一 1B — 已迭代(v1.2 分配模式收编坐席管理 Tab)
> **关联文档**: `docs/PRD.md` (主 PRD §18-20)、`原型-REQ-集成-000-管理后台-v1.0.html`(原型,与本文档同步更新)
---
## 目录
1. [项目信息](#1-项目信息)
2. [产品定义与目标](#2-产品定义与目标)
3. [用户故事](#3-用户故事)
4. [功能需求池](#4-功能需求池)
5. [页面清单与导航结构](#5-页面清单与导航结构)
6. [已实现功能映射](#6-已实现功能映射)
7. [数据模型扩展方案](#7-数据模型扩展方案)
8. [API 设计概要](#8-api-设计概要)
9. [占位模块规格](#9-占位模块规格)
10. [技术约束与约定](#10-技术约束与约定)
11. [待确认问题](#11-待确认问题)
---
## 1. 项目信息
| 字段 | 值 |
|------|------|
| 产品名称 | 智能IT支持服务台 — 管理后台 |
| 项目代号 | `wecom_it_smart_desk`(第三端:admin |
| 编程语言 | 前端: Vue 3 + TypeScript + Element Plus + Pinia · 后端: FastAPI + SQLAlchemy + PostgreSQL + Redis |
| 部署路径 | `/itadmin/`(与 H5 `/itdesk/`、坐席 `/itagent/` 并列) |
| 文档语言 | 中文 |
| 原型参考 | `docs/prototypes/admin-dashboard-v1.html`(深色科技风,8页面) |
| 所属阶段 | 阶段一 1B — 管理后台骨架 |
---
## 2. 产品定义与目标
### 2.1 产品定位
管理后台是 IT 智能服务台的**第三端产品**,与员工端 H5(`/itdesk/`)和坐席工作台(`/itagent/`)并列,面向**坐席组长/IT运维负责人**(以下简称"组长"),提供系统配置、人员管理、内容运营三项核心能力。
### 2.2 阶段一 1B 目标
> 先把已实现功能相关的管理后台功能实现,后续项目功能开发时同步完成管理功能开发,未进行的管理功能预留占位。
**1B 交付范围**
1. **功能开关/参数管理**P0)— 可视化编辑 `system_configs` 表中的配置项
2. **坐席人员管理**(P0)— 坐席列表 + 角色/技能标签编辑 + 状态查看
3. **快速回复管理**(P1)— 分类列表 + 审核发布流程(坐席提交→组长审核→全员可见)
4. **外部系统集成配置**(P0 占位)— 6个外部系统卡片展示 + 配置入口(仅 Dify 可用)
5. **消息分配模式**(P1 占位,v1.2 起收编为坐席管理页第 2 Tab「分配策略」)— 手动接单为当前模式,其他模式灰化锁定
6. **排查模板管理**(P1 占位)— 模板列表查看 + JSON 导入导出入口(阶段三启用)
7. **运营总览仪表盘**(P0)— 关键指标统计卡片 + 待处理事项 + 系统健康状态
### 2.3 产品目标(3个正交目标)
| # | 目标 | 衡量标准 |
|---|------|---------|
| G1 | **运维自助化** — 组长无需改代码即可调整系统参数、管理人员、审核内容 | 100% 配置项可通过管理后台修改,无需重启 |
| G2 | **操作可追溯** — 配置变更、审核操作记录版本历史 | 每次变更记录操作人和时间戳 |
| G3 | **页面可扩展** — 已规划的 10 个模块在导航中均有对应位置,未实现页面以占位方式呈现 | 所有已规划模块在菜单可见,未来阶段功能有明确入口 |
---
## 3. 用户故事
| ID | 用户故事 | 涉及模块 | 优先级 |
|----|---------|---------|--------|
| US1 | 作为**坐席组长**,我希望能通过可视化界面开关功能模块(如应急模式),而不需要登录服务器修改数据库 | 功能开关 | P0 |
| US2 | 作为**坐席组长**,我希望能查看所有坐席的在线状态、技能标签和当前负载,以便合理分配工作 | 坐席管理 | P0 |
| US3 | 作为**坐席组长**,我希望能编辑坐席的角色(组长/坐席)和技能标签(电脑/网络/软件等),以匹配实际分工 | 坐席管理 | P0 |
| US4 | 作为**坐席组长**,我希望能审核坐席提交的快速回复模板,通过后全员可见,驳回后仅提交人可见 | 快速回复管理 | P1 |
| US5 | 作为**坐席组长**,我希望能查看已连接的外部系统状态,并配置 API Key/URL | 系统集成 | P0 |
| US6 | 作为**坐席组长**,我希望在运营总览页面一目了然地看到在线坐席数、今日会话量、AI命中率等关键指标 | 运营总览 | P0 |
| US7 | 作为**坐席组长**,我希望能通过搜索快速找到某个配置项或坐席,而不需要逐页翻找 | 全局搜索 | P1 |
---
## 4. 功能需求池
### 4.1 P0 功能(阶段一 1B 必须交付)
#### P0-01 运营总览仪表盘
- **功能描述**:管理后台首页,展示4个统计卡片(在线坐席、今日会话、平均响应时间、AI命中率)+ 待处理事项列表 + 系统健康状态
- **对应原型**`page-dashboard`
- **数据来源**
- 在线坐席:`agents``status='online'` 计数
- 今日会话:`conversations` 表当日创建数
- 平均响应时间:`conversations` 表计算(若无则显示占位符)
- AI命中率:`conversations``ai_hit` 字段统计
- 待处理事项:快速回复待审核 + 系统告警
- 系统健康:各集成系统连接状态
- **后端模型**`Agent``Conversation``SystemConfig`
- **API**:需新建 `GET /api/admin/dashboard/overview`
- **复杂度**:S(纯查询+聚合,无写入操作)
#### P0-02 功能开关/参数管理
- **功能描述**:以卡片网格展示按功能模块分组的配置开关,每组包含若干 toggle 开关。配置变更即时生效(更新 `system_configs` 表)。支持回滚(记录旧值)
- **对应原型**`page-features`6张功能卡片)
- **功能卡片清单**
| 卡片 | 配置键 | 当前值来源 | 开关数 |
|------|--------|-----------|--------|
| AI 对话引擎 | `ai_auto_reply``hand_raise_keywords``intervene_round_threshold` | `system_configs` | 3 |
| 人工服务 | `manual_pickup_enabled``invite_employee_enabled` | 新建 | 2 |
| 排队系统 | 阶段二功能,开关灰化 | 无 | 2(灰化) |
| 满意度评价 | 阶段二功能,开关灰化 | 无 | 2(灰化) |
| 应急模式 | `emergency_mode` | `system_configs` | 1 |
| 关键词管理 | `hand_raise_keywords``emotion_keywords_*` | `system_configs` | 2组关键词编辑 |
- **编辑交互**:点击关键词旁的"编辑"按钮弹出对话框,支持 JSON 数组编辑;普通开关直接 toggle
- **后端模型**`SystemConfig`
- **API**:需新建 `GET /api/admin/configs``PUT /api/admin/configs/{key}``GET /api/admin/configs/{key}/history`
- **复杂度**:M(涉及分组展示、JSON 编辑验证、变更历史)
#### P0-03 坐席人员管理
- **功能描述**:坐席列表(表格形式),支持按状态筛选(全部/在线/忙碌/离线),展示:头像、姓名、工号、状态、技能标签、角色、当前/最大负载、今日结单数。组长可编辑坐席的角色和技能标签
- **对应原型**`page-agents`
- **扩展需求**(Agent 模型需新增字段):
- `role`VARCHAR(20),取值 `admin`(组长)/ `agent`(坐席),默认 `agent`
- `skill_tags`:JSON 数组,取值从 7 大类中选择:`["电脑","软件","外设","网络","安全","资产","其他"]`
- **后端模型**`Agent`(需扩展)
- **已有 API**`GET /api/agents`(坐席端用,可复用获取列表)
- **需新建 API**`PUT /api/admin/agents/{id}`(编辑角色/技能标签)、`POST /api/admin/agents`(添加坐席)、`DELETE /api/admin/agents/{id}`(移除坐席)
- **复杂度**:M(涉及模型扩展 + CRUD + 状态筛选)
#### P0-04 外部系统集成配置(占位)
- **功能描述**:展示 6 个外部系统的集成状态卡片(3×2 网格)。阶段一仅 Dify 和 RAGFlow 可配置(已有后端集成),其余 4 个系统仅展示"未连接"/"待确认"状态
- **对应原型**`page-integrations`
- **6个系统**
| 系统 | 阶段一状态 | 可操作 |
|------|-----------|--------|
| Dify AI | 已连接 | 配置(URL/Key)、测试连接 |
| RAGFlow | 部分集成 | 配置(URL/Key |
| 数据平台 | 未连接 | 仅展示状态 |
| 北森 eHR | 未连接 | 仅展示状态 |
| 火绒安全 | 未连接 | 仅展示状态 |
| 联软安全 | 待确认 | 仅展示状态 + "申请"按钮(无实际功能) |
- **后端模型**:需新建 `IntegrationConfig` 模型(或复用 `SystemConfig` 存 JSON
- **API**:需新建 `GET /api/admin/integrations``PUT /api/admin/integrations/{id}`
- **复杂度**:S(大部分为静态展示 + 2个配置表单)
### 4.2 P1 功能(阶段一 1B 应交付)
#### P1-01 快速回复管理(审核流程)
- **功能描述**:卡片式列表展示快速回复模板,按 7 大分类筛选(电脑/软件/外设/网络/安全/资产/其他)。支持审核流程:坐席提交→状态"待审核"(仅提交人可用)→组长审核通过→"已审核"(全员可见)/ 驳回→返回修改。每个模板展示版本号、变量列表、最后更新时间
- **对应原型**`page-quickreply`
- **审核状态机**
```
draft(草稿)→ pending_review(待审核,仅提交人可用)
├─→ approved(已审核,全员可见)
└─→ rejected(驳回,返回修改)
```
- **后端模型**`QuickReplyTemplate`(需扩展 `status`、`version`、`submitted_by` 字段)
- **已有 API**`GET/POST/PUT/DELETE /api/quick-replies`(坐席端用,需扩展审核逻辑)
- **需新建 API**`PUT /api/admin/quick-replies/{id}/review`(审核通过/驳回)、`GET /api/admin/quick-replies/pending`(待审核列表)
- **复杂度**:L(涉及审核状态机 + 版本管理 + 权限可见性逻辑)
#### P1-02 消息分配模式(占位)
- **功能描述**:展示 6 种分配模式卡片,阶段一仅「手动接单」可选(当前启用),其余 5 种模式灰化锁定并显示解锁条件
- **对应原型**`page-agents` 第 2 Tab「分配策略」(v1.2 起收编,原 `page-assignment` 独立页已删除)
- **6 种模式**:手动接单(✅启用)、轮询分配(P2锁定)、最少活跃优先(P2锁定)、加权比例分配(P3锁定)、技能匹配分配(P3锁定)、优先队列(P3锁定)
- **后端模型**:可复用 `SystemConfig`(键 `assignment_mode`
- **API**`GET /api/admin/assignment-mode`、`PUT /api/admin/assignment-mode`
- **复杂度**:S(静态展示为主,仅1个配置读写)
#### P1-03 排查模板管理(占位)
- **功能描述**:展示排查模板列表(表格形式:名称、分类、节点数、版本、状态),提供 JSON 导入/导出按钮。阶段一仅展示已有模板数据,导入导出功能灰化标注"阶段三启用"
- **对应原型**`page-flowchart`
- **后端模型**`TroubleshootingTemplate`(已有,可直接查询)
- **已有 API**`GET /api/troubleshooting-templates`(可复用)
- **复杂度**:S(数据展示 + 按钮占位)
#### P1-04 会话监控(占位,Demo预览)
- **功能描述**:展示会话统计卡片(进行中/等待中/今日已结单/异常告警)+ 实时会话表格。阶段一从数据库查询真实数据展示,标注"Demo 预览"
- **对应原型**`page-monitor`
- **数据来源**`conversations` 表实时查询
- **API**:需新建 `GET /api/admin/monitor/sessions`
- **复杂度**:S(只读查询展示)
### 4.3 P2 功能(阶段一预留占位)
以下模块仅需在导航菜单中预留入口(灰化 + "开发中"标识),页面内容为空白占位页:
| 模块 | 对应阶段 | 导航分组 |
|------|---------|---------|
| 主题模板 | 阶段二 | P2 高级功能 |
| 数据看板 | 阶段四 | P2 高级功能 |
| 知识库管理 | 阶段四 | P2 高级功能 |
### 4.4 全局功能
#### P0-05 导航布局框架
- 深色科技风侧边栏(220px宽)+ 顶部面包屑 + 内容区
- 导航分组:概览 → P0 核心配置 → P1 运营管理 → P2 高级功能
- 每个导航项显示优先级标签(P0红/P1黄/P2绿)
- 灰化菜单项:不可点击,显示 tooltip "阶段X 开发中"
#### P0-06 RBAC 权限控制(最小实现)
- 认证方式:复用坐席端 Redis token 机制,`Agent` 模型新增 `role` 字段
- 权限校验:`role='admin'` 可访问管理后台,`role='agent'` 返回 403
- 后端中间件:`/api/admin/*` 路由组统一校验 token + role
- 前端路由守卫:无 admin 角色跳转 403 页面
#### P1-05 全局搜索
- 顶部搜索框输入关键词,搜索范围:配置项名称、坐席姓名、快速回复标题
- 搜索结果以下拉菜单展示,点击跳转到对应页面
---
## 5. 页面清单与导航结构
### 5.1 页面树(v1.1 重构 · 5 组 27 项)
> v1.1 调整背景:v1.0 采用的 4 组(概览 / P0 / P1 / P2)按优先级分组,但实际运营中按"用户任务流"分组更友好(运营主管看运营中心、合规审计员看安全审计、运维看系统集成、知识管理员看知识与 AI)。本次重构调整为**5 组按用户场景**的结构。
```
管理后台 (/itadmin/)
├── 🟦 运营中心(7 项 · 实时作战 + 实时监控)
│ ├── 运营总览 /admin/dashboard P0 ✅ (含会话监控 widget)
│ ├── 会话监控 /admin/monitor P1 ⚡ (Dashboard 详情页跳转)
│ ├── 坐席管理 /admin/agents P0 ✅ (含「分配策略」Tab,v1.2 收编原独立页)
│ ├── 角色管理 /admin/roles P0 ✅ 🆕
│ ├── 权限矩阵 /admin/permissions-matrix P1 ⚡ 🆕 补漏
│ ├── OTP 管理 /admin/mfa-manage P1 ✅
│ └── 功能开关 /admin/configs P0 ✅
├── 🟪 数据与监控(4 项 · 离线分析 + AI 指标)
│ ├── 会话审计 /admin/session-audit P0 ✅
│ ├── 坐席绩效 /admin/agent-performance P0 ✅
│ ├── 满意度评价 /admin/evaluation-stats P1 ✅
│ └── 自动化指标 /admin/automation-metrics P1 ✅
├── 🟥 安全审计(5 项 🆕 · 合规追溯 + 系统安全)
│ ├── 配置变更历史 /admin/system-logs P0 ✅ 🆕
│ ├── 运行期日志 /admin/runtime-logs P1 ✅ 🆕
│ ├── 安全审计日志 /admin/audit-logs P0 ✅ 🆕
│ ├── 终端安全 /admin/terminal-security P0 ✅ 🆕 移入
│ └── 快速回复审计日志 /admin/quick-rules/audit P1 ✅ 🆕 移入(v1.1 调整:从快速回复规则组移至安全审计)
├── 🟩 系统与集成(4 项 · 底层能力配置)
│ ├── 系统集成 /admin/integrations P0 ⚡
│ ├── 欢迎与引导 /admin/welcome-config P1 ✅
│ ├── 自动化场景 /admin/automation-scenarios P1 ✅
│ └── 规则版本 /admin/automation-versions P1 ✅
└── 🟪 知识与 AI(7 项 · 配置规则 + 知识沉淀 + 智能建议)
├── 配置规则(3 项)
│ ├── 快速回复规则(5 个子页:index/greeting/contacts/routing/targets
│ │ /admin/quick-rules P1 ✅ 🆕 包含模板 tab 收编
│ ├── 排查流程图 /admin/flowcharts P1 ⚡ 🆕 移入
│ └── 代答排除 /admin/exclusion-rules P1 ✅ 🆕
├── 知识沉淀(3 项)
│ ├── 知识迭代管理 /admin/knowledge-iteration P1 ✅ 🆕
│ ├── RAGFlow 文档导入 /admin/ragflow-ingestion P1 ✅ 🆕
│ └── 拓扑预览 /admin/topology-preview P1 ✅ 🆕
└── 智能建议(1 项)
└── 知识库建议 /admin/knowledge-suggestions P1 ✅ 🆕 移入
```
> 图例:✅ 1B 实现 | ⚡ 部分实现/占位 | 🚧 仅占位页
> 🆕 = v1.1 新增/移入/调整归属
> 折叠区(不计入主导航):主题模板 / 数据看板 / 知识库管理(3 个 P2 占位,灰化)
### 5.2 导航分组(v1.1 重构)
| 导航分组 | 菜单数 | 包含菜单项 | 用户角色 |
|---------|--------|-----------|---------|
| 🟦 运营中心 | 7 | 运营总览 / 会话监控 / 坐席管理 / 角色管理 / 权限矩阵 / OTP 管理 / 功能开关 | 运营主管 / 坐席组长 |
| 🟪 数据与监控 | 4 | 会话审计 / 坐席绩效 / 满意度评价 / 自动化指标 | 数据分析师 / 主管 |
| 🟥 安全审计 | 5 | 配置变更历史 / 运行期日志 / 安全审计日志 / 终端安全 / 快速回复审计日志 | 合规审计员 / 安全管理员 |
| 🟩 系统与集成 | 4 | 系统集成 / 欢迎与引导 / 自动化场景 / 规则版本 | 系统管理员 |
| 🟪 知识与 AI | 7 | 快速回复规则 / 排查流程图 / 代答排除 / 知识迭代 / RAGFlow 导入 / 拓扑预览 / 知识库建议 | AI 运营 / 知识管理员 |
| **合计** | **26** | + 折叠区占位 3 项;分配模式作为坐席管理第 2 Tab 收编 | — |
### 5.3 对应原型页面映射(v1.1 更新)
| 原型页面 `id` | PRD 模块 | 实现方式 | 所属分组 |
|--------------|---------|---------|---------|
| `page-dashboard` | 运营总览(含会话监控 widget) | Vue 组件实现 | 运营中心 |
| `page-monitor` | 会话监控(详情页) | Vue 组件实现 | 运营中心 |
| `page-agents` | 坐席管理(含「分配策略」Tabv1.2 收编原 page-assignment | Vue 组件实现(`el-tabs` 双 Tab | 运营中心 🆕 v1.2 |
| `page-assignment` | 分配模式(已收编为 `page-agents` 第 2 Tab,v1.2 起不再独立展示) | — | — |
| `page-roles` | 角色管理 | Vue 组件实现 | 运营中心 🆕 |
| `page-permissions-matrix` | 权限矩阵 | Vue 组件实现(RBAC 可视化) | 运营中心 🆕 |
| `page-mfa-manage` | OTP 管理 | Vue 组件实现 | 运营中心 🆕 |
| `page-features` | 功能开关 | Vue 组件实现 | 运营中心 |
| `page-session-audit` | 会话审计 | Vue 组件实现 | 数据与监控 |
| `page-agent-performance` | 坐席绩效 | Vue 组件实现 | 数据与监控 |
| `page-evaluation-stats` | 满意度评价 | Vue 组件实现 | 数据与监控 |
| `page-automation-metrics` | 自动化指标 | Vue 组件实现 | 数据与监控 🆕 |
| `page-system-logs` | 配置变更历史 | Vue 组件实现 | 安全审计 🆕 |
| `page-runtime-logs` | 运行期日志 | Vue 组件实现 | 安全审计 🆕 |
| `page-audit-logs` | 安全审计日志 | Vue 组件实现 | 安全审计 🆕 |
| `page-terminal-security` | 终端安全 | Vue 组件实现 | 安全审计 🆕 移入 |
| `page-quick-rules-audit` | 快速回复审计日志 | Vue 组件实现 | 安全审计 🆕 移入(v1.1 调整:从快速回复规则组移出) |
| `page-integrations` | 系统集成 | Vue 组件实现(部分占位) | 系统与集成 |
| `page-welcome-config` | 欢迎与引导 | Vue 组件实现 | 系统与集成 🆕 |
| `page-automation-scenarios` | 自动化场景 | Vue 组件实现 | 系统与集成 🆕 |
| `page-automation-versions` | 规则版本 | Vue 组件实现 | 系统与集成 🆕 |
| `page-quick-rules` | 快速回复规则(含 5 子页 + 模板 tab 收编) | Vue 组件实现 | 知识与 AI 🆕 |
| `page-flowchart` | 排查流程图 | Vue 组件实现(大部分占位) | 知识与 AI 🆕 移入 |
| `page-exclusion-rules` | 代答排除 | Vue 组件实现 | 知识与 AI 🆕 |
| `page-knowledge-iteration` | 知识迭代管理 | Vue 组件实现 | 知识与 AI 🆕 |
| `page-ragflow-ingestion` | RAGFlow 文档导入 | Vue 组件实现 | 知识与 AI 🆕 |
| `page-topology-preview` | 拓扑预览 | Vue 组件实现 | 知识与 AI 🆕 |
| `page-knowledge-suggestions` | 知识库建议 | Vue 组件实现 | 知识与 AI 🆕 移入 |
---
## 6. 已实现功能映射
### 6.1 可直接复用的后端模型
| 模型 | 表名 | 复用方式 | 备注 |
|------|------|---------|------|
| `SystemConfig` | `system_configs` | 直接读写 | 已有 12 个配置键,功能开关页面直接映射 |
| `Agent` | `agents` | 查询列表 + 扩展字段 | 需要新增 role/skill_tags 列 |
| `QuickReplyTemplate` | `quick_reply_templates` | 查询列表 + 扩展字段 | 需要新增 status/version/submitted_by 列 |
| `TroubleshootingTemplate` | `troubleshooting_templates` | 只读查询 | 排查流程图页面展示 |
| `Conversation` | `conversations` | 只读聚合查询 | 仪表盘统计 + 会话监控 |
| `Employee` | `employees` | 只读查询 | 关联坐席信息 |
### 6.2 可直接复用的后端 API
| 现有 API | 复用场景 | 是否需要修改 |
|---------|---------|------------|
| `GET /api/agents` | 坐席列表查询 | 是,增加 role 和 skill_tags 返回 |
| `GET /api/quick-replies` | 快速回复列表 | 是,增加审核状态筛选 |
| `GET /api/troubleshooting-templates` | 排查模板列表 | 否,直接复用 |
| `GET /api/system/emergency-mode` | 应急模式状态读取 | 否,直接复用 |
| `PUT /api/system/emergency-mode` | 应急模式开关切换 | 否,直接复用 |
### 6.3 需要新建的 API 路由组
所有管理后台 API 统一挂载到 `/api/admin/` 路由组下:
| API | 方法 | 用途 | 优先级 |
|-----|------|------|--------|
| `/api/admin/dashboard/overview` | GET | 仪表盘统计数据 | P0 |
| `/api/admin/configs` | GET | 获取全部配置项(分组) | P0 |
| `/api/admin/configs/{key}` | PUT | 更新单个配置项 | P0 |
| `/api/admin/configs/{key}/history` | GET | 配置变更历史 | P0 |
| `/api/admin/agents` | GET | 坐席列表(管理视图,含角色/标签) | P0 |
| `/api/admin/agents` | POST | 添加坐席 | P0 |
| `/api/admin/agents/{id}` | PUT | 编辑坐席(角色/技能标签/负载上限) | P0 |
| `/api/admin/agents/{id}` | DELETE | 移除坐席 | P0 |
| `/api/admin/integrations` | GET | 集成系统列表+状态 | P0 |
| `/api/admin/integrations/{id}` | PUT | 更新集成配置 | P0 |
| `/api/admin/integrations/{id}/test` | POST | 测试连接 | P1 |
| `/api/admin/quick-replies/pending` | GET | 待审核模板列表 | P1 |
| `/api/admin/quick-replies/{id}/review` | PUT | 审核通过/驳回 | P1 |
| `/api/admin/assignment-mode` | GET/PUT | 分配模式读写 | P1 |
| `/api/admin/monitor/sessions` | GET | 实时会话列表 | P1 |
| `/api/admin/search` | GET | 全局搜索 | P1 |
### 6.4 需要扩展的已有模型
| 模型 | 新增字段 | 类型 | 默认值 | 说明 |
|------|---------|------|--------|------|
| `Agent` | `role` | VARCHAR(20) | `'agent'` | admin=组长, agent=坐席 |
| `Agent` | `skill_tags` | JSON | `[]` | 如 `["电脑","网络"]` |
| `QuickReplyTemplate` | `status` | VARCHAR(20) | `'approved'` | draft/pending_review/approved/rejected |
| `QuickReplyTemplate` | `version` | INTEGER | `1` | 版本号,每次审核通过后 +1 |
| `QuickReplyTemplate` | `submitted_by` | VARCHAR(36) | NULL | 提交人 agent_id(外键关联 agents |
---
## 7. 数据模型扩展方案
### 7.1 Agent 模型扩展
```python
# 在 Agent 模型中新增:
role: Mapped[str] = mapped_column(
String(20), nullable=False, default="agent",
comment="角色:admin=组长, agent=坐席"
)
skill_tags: Mapped[List[str]] = mapped_column(
JSON, nullable=False, default=list,
comment="技能标签列表(电脑/软件/外设/网络/安全/资产/其他)"
)
```
已有种子数据:当前坐席(宋献 → 组长角色 + 电脑/网络/软件标签,王丽 → 坐席 + 外设/安全,张伟 → 坐席 + 资产/其他)。
### 7.2 QuickReplyTemplate 模型扩展
```python
# 在 QuickReplyTemplate 模型中新增:
status: Mapped[str] = mapped_column(
String(20), nullable=False, default="approved",
comment="状态:draft/pending_review/approved/rejected"
)
version: Mapped[int] = mapped_column(
Integer, nullable=False, default=1,
comment="版本号"
)
submitted_by: Mapped[str] = mapped_column(
String(36), nullable=True, default=None,
comment="提交人 agent_id"
)
```
已有种子数据默认 `status='approved'`(无需审核)。
### 7.3 新建 IntegrationConfig 模型(可选)
若需要持久化集成系统的配置(API URL、Key 等),建议新建:
```python
class IntegrationConfig(Base):
__tablename__ = "integration_configs"
id: str (UUID PK)
system: str # dify/ragflow/data_platform/beisen/huorong/liansoft
name: str # 显示名称
api_url: str # API 地址
api_key: str # API Key(加密存储)
status: str # connected/partial/disconnected/pending
updated_at: datetime
```
阶段一可暂不复用此模型,直接硬编码 6 个系统的状态展示,Dify 和 RAGFlow 的配置暂时存 `system_configs`。
---
## 8. API 设计概要
### 8.1 路由注册
```python
# 新建 backend/app/api/admin.py
# 在 backend/app/api/router.py 中注册:
from app.api.admin import router as admin_router
api_router.include_router(admin_router, prefix="/admin", tags=["管理后台"])
```
### 8.2 权限中间件
```python
# 所有 /api/admin/* 路由需校验:
# 1. token 有效性(复用坐席端 Redis token
# 2. Agent.role == 'admin'
# 不满足条件返回统一错误响应(code=1003, message="无管理权限"
```
### 8.3 响应格式
沿用项目统一的 `success_response` / `error_response` 格式:
```json
{"code": 0, "message": "success", "data": {...}}
{"code": 1003, "message": "无管理权限"}
```
### 8.4 配置变更历史
`PUT /api/admin/configs/{key}` 时:
1. 读取当前值存入日志(`config_change_logs` 表或 JSON 字段)
2. 写入新值
3. 返回变更前后对比
日志结构:`{config_key, old_value, new_value, changed_by, changed_at}`
---
## 9. 占位模块规格
### 9.1 占位页面交互规范
所有未来阶段的占位页面遵循统一的展示方式:
**页面内容**
```
┌────────────────────────────────────────┐
│ │
│ 🚧 开发中 │
│ │
│ 该功能将在阶段 X 上线 │
│ │
│ 预计功能:{简短描述} │
│ │
│ [返回首页] │
│ │
└────────────────────────────────────────┘
```
**导航菜单**
- 灰化样式:`opacity: 0.4; pointer-events: none;`
- 不响应点击
- Tooltip 悬停提示:"阶段X 开发中,敬请期待"
### 9.2 各占位页面规格
| 页面 | 占位类型 | 占位内容 |
|------|---------|---------|
| 主题模板 | 🚧 空白占位 | 居中显示"阶段二上线",描述:支持全局/坐席端/H5端三层主题配置 |
| 数据看板 | 🚧 空白占位 | 居中显示"阶段四上线",描述:坐席绩效、满意度趋势、热点问题排行 |
| 知识库管理 | 🚧 空白占位 | 居中显示"阶段四上线",描述:标注→知识条目→RAGFlow同步迭代闭环 |
| 系统集成(部分) | ⚡ 功能占位 | 展示系统卡片但"配置""测试"按钮灰化,tooltip 说明"阶段二启用" |
| 排查流程图 | ⚡ 功能占位 | 展示已有模板数据,导入导出按钮灰化标注"阶段三启用" |
---
## 10. 技术约束与约定
### 10.1 前端技术栈
| 项目 | 值 |
|------|------|
| 框架 | Vue 3 + Composition API |
| 语言 | TypeScript |
| UI 库 | Element Plus |
| 状态管理 | Pinia |
| 构建工具 | Vite |
| 样式方案 | Tailwind CSS(与坐席端一致) |
| UI 风格 | 深色科技风(CSS 变量与原型一致) |
| 部署路径 | `/itadmin/` |
### 10.2 CSS 变量(与原型对齐)
```css
--bg-primary: #0f172a; /* 主背景 */
--bg-secondary: #1e293b; /* 侧边栏/卡片 */
--bg-tertiary: #334155; /* 表格表头 */
--accent: #3b82f6; /* 主题色 */
--success: #10b981; /* 成功/在线 */
--warning: #f59e0b; /* 警告 */
--danger: #ef4444; /* 危险/错误 */
--text-primary: #f1f5f9; /* 主文字 */
--text-secondary: #94a3b8; /* 辅助文字 */
--text-muted: #64748b; /* 弱化文字 */
```
### 10.3 后端技术栈
| 项目 | 值 |
|------|------|
| 框架 | FastAPI |
| ORM | SQLAlchemy 2.0 (async) |
| 数据库 | PostgreSQL(生产)/ SQLite(开发) |
| 缓存 | Redistoken + 配置热更新) |
| 认证 | Redis token + Agent.role 权限校验 |
| API 前缀 | `/api/admin/` |
### 10.4 部署
```nginx
# nginx 配置示例(新增 /itadmin/ 路由)
location /itadmin/ {
alias /path/to/frontend-admin/dist/;
try_files $uri $uri/ /itadmin/index.html;
}
location /api/admin/ {
proxy_pass http://backend:8000/api/admin/;
}
```
### 10.5 其他约定
- 项目名称:`frontend-admin`,目录与 `frontend-agent/`、`frontend-h5/` 并列
- 后端新建文件:`backend/app/api/admin.py`、`backend/app/services/admin_service.py`(可选)
- 数据库迁移:新增列使用 Alembic 迁移脚本
- 中文界面,所有文案使用中文
- 配置项命名遵循现有 `snake_case` 规范
---
## 11. 待确认问题
| # | 问题 | 影响范围 | 建议方案 | 确认人 |
|---|------|---------|---------|--------|
| Q1 | 坐席组长是否只有 1 人(宋献)?其他坐席是否需要管理后台访问权限? | RBAC 设计 | 阶段一仅宋献(role=admin),后续需新增组长时可扩展 | 宋献 |
| Q2 | IntegrationConfig 模型是新建独立表还是复用 SystemConfig 存 JSON | 数据模型 | 建议阶段一先用 SystemConfig,等集成系统配置复杂度上升后再建独立表 | 开发 |
| Q3 | 快速回复的版本历史是存一张新表(quick_reply_versions)还是在主表用 JSON 存历史版本? | 数据模型 | 建议阶段一先用主表 version 字段递增 + JSON 字段存 diff,阶段二按需建版本表 | 开发 |
| Q4 | 配置变更历史的存储粒度:每键独立日志表 vs 通用 JSON 日志? | 数据模型 | 建议阶段一在 config_change_logs 表中存 {key, old, new, who, when},简单够用 | 开发 |
| Q5 | 仪表盘"平均响应时间"和"AI命中率"的计算口径需要确认(从坐席接单到首条回复?还是从员工发消息到坐席回复?) | 运营总览 | 建议阶段一先展示"今日会话数"和"在线坐席数"两个有把握的指标,其余用占位符 | 宋献 |
| Q6 | 快速回复"仅提交人可用"(待审核期间)的权限粒度:坐席端 API 是否需要改? | 快速回复 | 是,坐席端 `GET /api/quick-replies` 返回需增加 status 筛选(全员可见的 approved + 自己的 pending_review | 开发 |
| Q7 | 应急模式开启时,H5用户端展示引导文案的内容是否需要管理后台可配? | 功能开关 | 建议阶段一固定文案(硬编码在 system.py 中),阶段二增加可配置 | 宋献 |
---
> **文档结束** — 本 PRD 覆盖管理后台阶段一 1B 的全部需求,与主 PRD §18-20 和原型 `admin-dashboard-v1.html` 对齐。后续阶段的功能将在迭代中增量补充。
---
## 12. 变更日志
| 版本 | 日期 | 变更内容 | 变更人 | 变更原因 | 影响范围 |
|------|------|---------|--------|---------|---------|
| v1.0 | 2026-06-16 | 初版 PRD4 组导航:概览/P0 核心/P1 运营/P2 高级) | 许清楚 | 阶段一 1B 立项 | — |
| v1.1 | 2026-07-28 | **导航重构为 5 组按用户场景**(运营中心/数据与监控/安全审计/系统与集成/知识与 AI);新增 12 项菜单(含 v1.0 未列出的角色管理/权限矩阵/OTP 管理/自动化指标/配置变更历史/运行期日志/安全审计日志/终端安全/快速回复审计/欢迎与引导/自动化场景/规则版本/知识迭代/RAGFlow 导入/拓扑预览/知识库建议/代答排除等);终端安全从系统与集成移入安全审计组;快速回复审计从快速回复规则组移入安全审计组;快速回复模板作为快速回复规则页内"模板"tab 收编;会话监控 widget 嵌入运营总览 | 宋献 | v1.0 按优先级分组的导航不符合运营实际使用场景;安全审计类功能分散;快速回复模板与规则功能重复;用户进入运营总览看不到实时会话 | §5 全部;§4.1/4.2/4.3 部分功能项;§9.2 占位规格微调;新增 27 项页面映射到原型 |
| v1.2 | 2026-07-28 | **分配模式 Tab 收编到坐席管理**:原独立页 `page-assignment` 不再独立展示,整体内嵌为 `/admin/agents` 的第 2 个 Tab"分配策略";路由 `/admin/assignment-mode` 移除;菜单导航「知识与 AI」8 项 → 7 项(合计 26 项);坐席管理原型增加 `el-tabs` 双 Tab 切换 | 宋献 | 阶段一分配模式 6 个里 5 个灰化,占独立菜单过重;解锁条件强依赖坐席人数,Tab 合并后上下文最连贯;未来膨胀仍可拆回独立页 | §5.1/5.2/5.3 树状图与分组表;§9.2 占位表;§12 变更日志 |
### v1.2 关键变更说明
**1. 分配模式 Tab 收编到坐席管理**
- 原 `/admin/assignment-mode` 独立菜单/独立页 → 收编为 `/admin/agents` 的第 2 个 Tab「分配策略」
- 路由表删除 `/admin/assignment-mode` 项;菜单导航「知识与 AI」8 项 → 7 项,总菜单数 27 → 26
- 坐席管理页实现方式:`<el-tabs v-model="activeTab">`,Tab1 = 坐席列表(原内容),Tab2 = 分配策略(迁移 `AssignmentMode.vue` 的 6 张模式卡片)
- 后端零改动:分配模式的读写仍走 `system_configs.assignment_mode` + `GET/PUT /api/admin/assignment-mode`
- `views/AssignmentMode.vue` 保留文件但停用导入(防止后续回滚时再拆出)
**2. 决策依据**
- 阶段一分配模式 6 个里 5 个灰化占位,独立菜单过重
- 解锁条件强依赖坐席人数(轮询 ≥3、加权 ≥5),Tab 内可见当前在岗数,无需跨页比对
- 若阶段二/三分配模式膨胀(权重配置、技能匹配规则),可逆撤销为独立页
### v1.1 关键变更说明
**1. 5 组按用户场景分组**(替代 v1.0 的 4 组按优先级分组)
- 运营中心:日常运营主入口(运营主管/组长)
- 数据与监控:离线数据分析(数据分析师)
- 安全审计:合规追溯(合规审计员/安全管理员)🆕
- 系统与集成:底层能力配置(系统管理员)
- 知识与 AI:智能引擎 + 知识沉淀(AI 运营/知识管理员)
**2. 快速回复模板收编为 Tab**(节省 1 个菜单项)
- 原 `/admin/quick-replies` 独立菜单页面 → 整合为 `/admin/quick-rules` 内的"模板"tab
- 原 5 个子路由(greeting/contacts/routing/targets/audit+ 新增 template tab = 6 个 tab
- 复用 quickReplyStore 与现有 API,无需新建后端
**3. 会话监控 widget 嵌入运营总览**(双视图模式)
- Dashboard 顶部新增"实时会话 Top 5" widget
- 点"查看更多"跳 `/admin/monitor` 详情页
- 优势:用户进入 Dashboard 即可见实时状态,无需跳转
**4. 快速回复审计日志移出知识与 AI 组**v1.1 关键调整)
- 原规划:作为"快速回复规则"页的 6 个 tab 之一(与 greeting/contacts/routing/targets/template 并列)
- 调整后:独立菜单项,归入"安全审计"组(与配置变更历史/运行期日志/安全审计日志/终端安全 并列)
- 理由:审计类功能在性质上属于安全合规范畴,不应混杂在业务规则配置组
**5. 终端安全从系统与集成移入安全审计**
- 原位置:与系统集成、自动化场景、规则版本同组(系统集成 + 终端安全 = 4 项)
- 调整后:归入安全审计组(合规追溯 + 系统安全 = 5 项)
- 理由:终端安全本质是合规管控(联软/火绒管控),与底层引擎集成(系统集成/自动化场景)性质不同
**6. 新增 12 项菜单覆盖完整功能域**
- 运营中心:角色管理、权限矩阵、OTP 管理(v1.0 漏列)
- 数据与监控:自动化指标(v1.0 漏列)
- 安全审计:配置变更历史、运行期日志、安全审计日志(v1.0 漏列)
- 系统与集成:欢迎与引导、自动化场景、规则版本(v1.0 漏列)
- 知识与 AI:知识迭代管理、RAGFlow 文档导入、拓扑预览、知识库建议、代答排除、排查流程图(v1.0 部分漏列)
@@ -0,0 +1,119 @@
# PRD - SLA 仪表盘
> **REQ编号**: REQ-集成-003
> **版本**: v1.0
> **优先级**: P2
> **阶段**: 远期(5-8个月)
> **作者**: 宋献
> **日期**: 2026-07-19
---
## 一、问题陈述
**用户问题**
- 管理层无法实时了解 SLA 达成情况
- 需要人工统计报表,滞后
- 缺乏预警机制,问题发现不及时
**业务目标**
- 管理层实时看到 SLA 达成率
- 支持超时预警
- 支持趋势分析
---
## 二、需求范围
### 2.1 核心功能
| 功能 | 描述 |
|------|------|
| SLA 看板 | 展示 SLA 达成率、响应时间等核心指标 |
| 实时刷新 | 数据实时更新 |
| 超时预警 | 即将超时时自动预警 |
| 趋势分析 | 支持时间维度分析 |
### 2.2 非目标
- 不支持自定义 SLA 规则配置
- 不支持多租户
---
## 三、用户故事
| 角色 | 用户故事 | 验收标准 |
|------|----------|---------|
| 管理员 | 我可以查看 SLA 达成率 | 仪表盘展示 SLA 达成率 |
| 管理员 | 我可以看到超时预警 | 即将超时时显示预警 |
| 管理员 | 我可以看到趋势图 | 支持查看近7天/30天趋势 |
---
## 四、功能详情
### 4.1 核心指标
| 指标 | 描述 | 计算方式 |
|------|------|---------|
| SLA 达成率 | 按时解决的会话比例 | 按时解决数/总会话数 |
| 平均响应时间 | 首次响应平均耗时 | 总响应时间/响应次数 |
| 平均解决时间 | 问题解决平均耗时 | 总解决时间/解决次数 |
| 转人工率 | 需人工处理的比例 | 转人工数/总会话数 |
### 4.2 看板布局
```
┌─────────────────────────────────────────────┐
│ IT智能服务台 - SLA仪表盘 │
├─────────────────────────────────────────────┤
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ SLA达成率│ │平均响应 │ │平均解决 │ │
│ │ 92% │ │ 1.2min │ │ 5.6min │ │
│ └──────────┘ └──────────┘ └──────────┘ │
├─────────────────────────────────────────────┤
│ 趋势图 │
│ [折线图:近7天SLA达成率趋势] │
├─────────────────────────────────────────────┤
│ 超时预警 │
│ · 会话 #1234 即将超时(剩余2分钟) │
│ · 会话 #1235 即将超时(剩余5分钟) │
├─────────────────────────────────────────────┤
│ 坐席负载 │
│ [柱状图:各坐席处理中的会话数] │
└─────────────────────────────────────────────┘
```
### 4.3 预警规则
| 规则 | 触发条件 |
|------|---------|
| 即将超时 | 剩余时间 < 10分钟 |
| 已超时 | 已超过 SLA 设定时间 |
| 坐席繁忙 | 处理中会话 > 5个 |
---
## 五、指标设计
| 指标 | 目标 | 测量方式 |
|------|------|---------|
| SLA 达成率 | ≥ 90% | 按时解决数/总会话数 |
| 数据刷新间隔 | ≤ 30s | 实时数据更新频率 |
| 预警准确率 | 100% | 预警后确实超时的比例 |
---
## 六、技术方案
- 管理后台新增 SLA 仪表盘页面
- 后端定时计算 SLA 指标
- WebSocket 推送预警信息
---
## 七、关联文档
- 前置需求: REQ-集成-001 会议室预定(基础集成能力)
- 后续需求: 无
@@ -0,0 +1,139 @@
# PRD - 自动化闭环
> **REQ编号**: REQ-集成-004
> **版本**: v1.0
> **优先级**: P2
> **阶段**: 远期(5-8个月)
> **作者**: 宋献
> **日期**: 2026-07-19
---
## 一、问题陈述
**用户问题**
- 大量简单重复问题占用人工坐席时间
- 问题分诊、升级、回访均需人工操作
- 整体服务效率有提升空间
**业务目标**
- 实现问题自动分诊 → 自动升级 → 自动回访的闭环
- 减少人工干预,提升服务效率
- 释放坐席精力处理复杂问题
---
## 二、需求范围
### 2.1 核心功能
| 功能 | 描述 |
|------|------|
| 自动分诊 | AI 判断问题类型,自动分配 |
| 自动升级 | 符合条件时自动转人工 |
| 自动回访 | 问题解决后自动回访确认 |
| 规则引擎 | 支持配置自动化规则 |
### 2.2 非目标
- 不支持复杂规则配置
- 不支持与外部系统集成(如 ITSM)
---
## 三、用户故事
| 角色 | 用户故事 | 验收标准 |
|------|----------|---------|
| 员工 | 我的问题被自动分到正确的队列 | AI 根据问题类型自动分配 |
| 员工 | 我的问题被自动升级 | 符合条件时自动转人工 |
| 员工 | 问题解决后有人回访 | 自动发送回访消息 |
| 管理员 | 我可以配置自动化规则 | 支持配置触发条件和动作 |
---
## 四、功能详情
### 4.1 自动分诊
**触发时机**:员工首次提问
**分诊逻辑**
```
员工提问 → AI分析问题类型 → 匹配队列规则 → 分配到对应队列
```
**分诊依据**
- 问题类型(技术咨询、故障申报、咨询投诉)
- 紧急程度(高/中/低)
- 关键词匹配
### 4.2 自动升级
**触发时机**:满足升级条件
**升级条件**
| 条件 | 描述 |
|------|------|
| AI 无法解决 | 置信度 < 0.3 |
| 多次未解决 | AI 回复 3 次后未解决 |
| 用户要求 | 用户明确要求转人工 |
| 特定关键词 | 包含"紧急"、"故障"等 |
**升级流程**
1. 触发升级条件
2. 将会话状态标记为"待升级"
3. 分配给空闲坐席
4. 通知坐席
### 4.3 自动回访
**触发时机**:会话标记为"已解决"
**回访内容**
- "您的问题是否已解决?"
- 满意/不满意选项
- 反馈输入框
**回访时机**
- 解决后 30 分钟发送
- 仅对 AI 解决的会话
### 4.4 规则引擎
**配置项**
| 配置 | 描述 |
|------|------|
| 触发条件 | 满足什么条件时触发 |
| 执行动作 | 触发什么动作 |
| 启用/禁用 | 是否启用 |
**示例规则**
- 触发条件:问题包含"VPN" → 执行动作:分配到"网络支持"队列
- 触发条件:用户等待 > 5分钟 → 执行动作:提升优先级
---
## 五、指标设计
| 指标 | 目标 | 测量方式 |
|------|------|---------|
| 自动分诊准确率 | ≥ 85% | 分诊正确的比例 |
| 自动升级准确率 | ≥ 90% | 升级合理的比例 |
| 回访率 | ≥ 60% | 收到回访的用户比例 |
| 人工干预率 | ≤ 30% | 需人工处理的比例 |
---
## 六、技术方案
- 后端规则引擎服务
- 定时任务处理回访
- 消息队列解耦各环节
---
## 七、关联文档
- 前置需求: REQ-AI-002 置信度门控
- 后续需求: 无
@@ -0,0 +1,806 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>安全策略检查平台 - 原型设计</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
background: #f5f5f5;
padding: 20px;
}
.page {
background: #fff;
border-radius: 8px;
padding: 24px;
margin-bottom: 20px;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
max-width: 1200px;
margin: 0 auto 20px;
}
.page-title {
font-size: 18px;
font-weight: 600;
margin-bottom: 20px;
padding-bottom: 12px;
border-bottom: 1px solid #eee;
color: #333;
}
.nav {
background: #fff;
border-radius: 8px;
padding: 12px 24px;
margin-bottom: 20px;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
max-width: 1200px;
margin: 0 auto 20px;
overflow-x: auto;
}
.nav ul {
list-style: none;
display: flex;
gap: 16px;
white-space: nowrap;
}
.nav li {
cursor: pointer;
padding: 8px 16px;
border-radius: 4px;
color: #666;
font-size: 14px;
}
.nav li.active {
background: #07C160;
color: #fff;
}
.nav li:hover:not(.active) {
background: #f5f5f5;
}
.card {
background: #fafafa;
border-radius: 8px;
padding: 16px;
margin-bottom: 16px;
}
.card-title {
font-weight: 500;
margin-bottom: 12px;
color: #333;
display: flex;
justify-content: space-between;
align-items: center;
}
.stat-grid {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 16px;
margin-bottom: 20px;
}
.stat-card {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
border-radius: 8px;
padding: 20px;
color: #fff;
}
.stat-card.success {
background: linear-gradient(135deg, #11998e 0%, #38ef7d 100%);
}
.stat-card.warning {
background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
}
.stat-card.info {
background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%);
}
.stat-value {
font-size: 28px;
font-weight: 600;
margin-bottom: 4px;
}
.stat-label {
font-size: 14px;
opacity: 0.9;
}
.form-group {
margin-bottom: 16px;
}
.form-label {
display: block;
margin-bottom: 6px;
font-weight: 500;
color: #333;
}
.form-input {
width: 100%;
padding: 10px 12px;
border: 1px solid #ddd;
border-radius: 6px;
font-size: 14px;
}
.form-input:focus {
outline: none;
border-color: #07C160;
}
.btn {
padding: 8px 16px;
border-radius: 6px;
border: none;
cursor: pointer;
font-size: 13px;
font-weight: 500;
}
.btn-sm {
padding: 4px 10px;
font-size: 12px;
}
.btn-primary {
background: #07C160;
color: #fff;
}
.btn-primary:hover {
background: #06ad56;
}
.btn-default {
background: #fff;
border: 1px solid #ddd;
color: #333;
}
.btn-default:hover {
background: #f5f5f5;
}
.btn-danger {
background: #fff;
border: 1px solid #f56c6c;
color: #f56c6c;
}
.table {
width: 100%;
border-collapse: collapse;
}
.table th,
.table td {
padding: 10px 12px;
text-align: left;
border-bottom: 1px solid #eee;
font-size: 13px;
}
.table th {
background: #fafafa;
font-weight: 500;
color: #666;
}
.table tr:hover {
background: #fafafa;
}
.tag {
display: inline-block;
padding: 2px 8px;
border-radius: 4px;
font-size: 12px;
}
.tag-danger {
background: #fee;
color: #f56c6c;
}
.tag-success {
background: #e8f8f0;
color: #07C160;
}
.tag-warning {
background: #fdf6ec;
color: #e6a23c;
}
.tag-info {
background: #e6f7ff;
color: #1890ff;
}
.section-title {
font-size: 16px;
font-weight: 600;
margin: 20px 0 12px;
color: #333;
}
.flex {
display: flex;
gap: 12px;
align-items: center;
}
.flex-between {
display: flex;
justify-content: space-between;
align-items: center;
}
.text-muted {
color: #999;
font-size: 13px;
}
.badge {
display: inline-flex;
align-items: center;
padding: 2px 8px;
border-radius: 10px;
font-size: 11px;
font-weight: 500;
}
.badge-primary {
background: #e6f7ff;
color: #1890ff;
}
.grid-2 {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 16px;
}
</style>
</head>
<body>
<div class="nav">
<ul>
<li class="active" onclick="switchPage(this, 'overview')">检查概览</li>
<li onclick="switchPage(this, 'strategy')">策略管理</li>
<li onclick="switchPage(this, 'channel')">渠道管理</li>
<li onclick="switchPage(this, 'datasource')">数据源</li>
<li onclick="switchPage(this, 'records')">命中记录</li>
<li onclick="switchPage(this, 'template')">通知模板</li>
<li onclick="switchPage(this, 'schedule')">计划任务</li>
</ul>
</div>
<!-- 页面1: 检查概览 -->
<div class="page" id="page-overview">
<div class="page-title">安全策略检查平台 - 检查概览</div>
<div class="stat-grid">
<div class="stat-card">
<div class="stat-value">128</div>
<div class="stat-label">总终端数</div>
</div>
<div class="stat-card warning">
<div class="stat-value">3</div>
<div class="stat-label">今日命中</div>
</div>
<div class="stat-card success">
<div class="stat-value">125</div>
<div class="stat-label">合规终端</div>
</div>
<div class="stat-card info">
<div class="stat-value">97.6%</div>
<div class="stat-label">覆盖率</div>
</div>
</div>
<div class="flex-between" style="margin-bottom: 16px;">
<div>
<button class="btn btn-primary">立即检查</button>
<button class="btn btn-default">导出报告</button>
</div>
<div class="text-muted">最后检查: 2026-07-20 08:00:00</div>
</div>
<div class="card">
<div class="card-title">最近执行记录</div>
<table class="table">
<thead>
<tr>
<th>策略</th>
<th>执行时间</th>
<th>总终端</th>
<th>命中数</th>
<th>通知成功</th>
<th>状态</th>
</tr>
</thead>
<tbody>
<tr>
<td><span class="badge badge-primary">OpenClaw远程软件</span></td>
<td>2026-07-20 08:00</td>
<td>128</td>
<td>3</td>
<td>3</td>
<td><span class="tag tag-success">成功</span></td>
</tr>
<tr>
<td><span class="badge badge-primary">OpenClaw远程软件</span></td>
<td>2026-07-19 08:00</td>
<td>127</td>
<td>5</td>
<td>5</td>
<td><span class="tag tag-success">成功</span></td>
</tr>
</tbody>
</table>
</div>
</div>
<!-- 页面2: 策略管理 -->
<div class="page" id="page-strategy" style="display: none;">
<div class="page-title">策略管理</div>
<div class="card">
<div class="card-title">
<span>安全策略列表</span>
<button class="btn btn-primary btn-sm">+ 新增策略</button>
</div>
<table class="table">
<thead>
<tr>
<th>策略ID</th>
<th>策略名称</th>
<th>检测规则</th>
<th>数据源</th>
<th>推送渠道</th>
<th>状态</th>
<th>操作</th>
</tr>
</thead>
<tbody>
<tr>
<td>openclaw_remote</td>
<td>OpenClaw远程软件检测</td>
<td>OpenClaw/向日葵/sunflower</td>
<td>火绒API</td>
<td>企微应用 + 企微群</td>
<td><span class="tag tag-success">已启用</span></td>
<td>
<button class="btn btn-default btn-sm">编辑</button>
<button class="btn btn-danger btn-sm">停用</button>
</td>
</tr>
<tr>
<td>unknown_usb</td>
<td>未知USB设备检测</td>
<td>未登记的USB设备</td>
<td>联软/火绒</td>
<td>企微应用</td>
<td><span class="tag tag-warning">未启用</span></td>
<td>
<button class="btn btn-default btn-sm">编辑</button>
<button class="btn btn-primary btn-sm">启用</button>
</td>
</tr>
<tr>
<td>illegal_proxy</td>
<td>非法代理软件检测</td>
<td>proxy/vpn类软件</td>
<td>火绒API</td>
<td>企微应用</td>
<td><span class="tag tag-warning">未启用</span></td>
<td>
<button class="btn btn-default btn-sm">编辑</button>
<button class="btn btn-primary btn-sm">启用</button>
</td>
</tr>
</tbody>
</table>
</div>
<div class="card">
<div class="card-title">新增策略</div>
<div class="grid-2">
<div class="form-group">
<label class="form-label">策略ID</label>
<input type="text" class="form-input" placeholder="如: unknown_usb">
</div>
<div class="form-group">
<label class="form-label">策略名称</label>
<input type="text" class="form-input" placeholder="如: 未知USB设备检测">
</div>
</div>
<div class="form-group">
<label class="form-label">检测规则(关键词,多个用逗号分隔)</label>
<input type="text" class="form-input" placeholder="如: proxy, vpn, 代理">
</div>
<div class="grid-2">
<div class="form-group">
<label class="form-label">数据源</label>
<select class="form-input">
<option>火绒API</option>
<option>联软Excel</option>
<option>火绒API + 联软</option>
</select>
</div>
<div class="form-group">
<label class="form-label">推送渠道</label>
<select class="form-input">
<option>企微应用</option>
<option>企微应用 + 企微群</option>
<option>企微 + 邮件</option>
</select>
</div>
</div>
<button class="btn btn-primary">保存策略</button>
</div>
</div>
<!-- 页面3: 渠道管理 -->
<div class="page" id="page-channel" style="display: none;">
<div class="page-title">渠道管理</div>
<div class="card">
<div class="card-title">
<span>推送渠道列表</span>
<button class="btn btn-primary btn-sm">+ 新增渠道</button>
</div>
<table class="table">
<thead>
<tr>
<th>渠道ID</th>
<th>渠道名称</th>
<th>类型</th>
<th>配置参数</th>
<th>状态</th>
<th>操作</th>
</tr>
</thead>
<tbody>
<tr>
<td>wecom_app</td>
<td>企微应用消息</td>
<td><span class="badge badge-primary">企微</span></td>
<td class="text-muted">应用ID: wx123... Secret: ****</td>
<td><span class="tag tag-success">已启用</span></td>
<td>
<button class="btn btn-default btn-sm">配置</button>
</td>
</tr>
<tr>
<td>wecom_group</td>
<td>企微群通知</td>
<td><span class="badge badge-primary">企微</span></td>
<td class="text-muted">群ID: 123456789</td>
<td><span class="tag tag-success">已启用</span></td>
<td>
<button class="btn btn-default btn-sm">配置</button>
</td>
</tr>
<tr>
<td>email</td>
<td>邮件通知</td>
<td><span class="tag tag-info">邮件</span></td>
<td class="text-muted">SMTP: smtp.example.com</td>
<td><span class="tag tag-warning">未启用</span></td>
<td>
<button class="btn btn-default btn-sm">配置</button>
<button class="btn btn-primary btn-sm">启用</button>
</td>
</tr>
<tr>
<td>sms</td>
<td>短信通知</td>
<td><span class="tag tag-info">短信</span></td>
<td class="text-muted">未配置</td>
<td><span class="tag tag-warning">未启用</span></td>
<td>
<button class="btn btn-default btn-sm">配置</button>
</td>
</tr>
</tbody>
</table>
</div>
<div class="card">
<div class="card-title">新增渠道配置</div>
<div class="grid-2">
<div class="form-group">
<label class="form-label">渠道ID</label>
<input type="text" class="form-input" placeholder="如: email">
</div>
<div class="form-group">
<label class="form-label">渠道名称</label>
<input type="text" class="form-input" placeholder="如: 邮件通知">
</div>
</div>
<div class="form-group">
<label class="form-label">渠道类型</label>
<select class="form-input">
<option value="wecom">企微应用</option>
<option value="wecom_group">企微群</option>
<option value="email">邮件</option>
<option value="sms">短信</option>
<option value="dingtalk">钉钉</option>
</select>
</div>
<div class="form-group">
<label class="form-label">配置参数</label>
<textarea class="form-input" rows="3" placeholder="JSON格式配置"></textarea>
</div>
<button class="btn btn-primary">保存渠道</button>
<button class="btn btn-default">测试连接</button>
</div>
</div>
<!-- 页面4: 数据源管理 -->
<div class="page" id="page-datasource" style="display: none;">
<div class="page-title">数据源管理</div>
<div class="card">
<div class="card-title">
<span>火绒API配置</span>
<span class="text-muted" style="font-weight: normal; font-size: 12px;">(已存在,复用现有配置)</span>
</div>
<div class="grid-2">
<div class="form-group">
<label class="form-label">服务器地址</label>
<input type="text" class="form-input" value="https://huorong.company.com" disabled>
</div>
<div class="form-group">
<label class="form-label">连接状态</label>
<div style="padding: 10px;">
<span class="tag tag-success">已连接</span>
<span class="text-muted" style="margin-left: 8px;">最后同步: 2026-07-20 08:00</span>
</div>
</div>
</div>
</div>
<div class="card">
<div class="card-title">联软数据导入</div>
<div class="form-group">
<label class="form-label">上传Excel文件</label>
<input type="file" class="form-input" accept=".xlsx,.xls">
</div>
<div class="text-muted" style="margin-bottom: 12px;">
已导入: 2026-07-19 15:30 (94条记录, 88用户, 91设备)
</div>
<button class="btn btn-primary">导入Excel</button>
<button class="btn btn-default">查看历史</button>
</div>
</div>
<!-- 页面5: 命中记录 -->
<div class="page" id="page-records" style="display: none;">
<div class="page-title">命中记录</div>
<div class="flex" style="margin-bottom: 16px;">
<input type="text" class="form-input" placeholder="搜索用户名/设备名" style="max-width: 300px;">
<select class="form-input" style="width: auto;">
<option>全部策略</option>
<option>OpenClaw远程软件</option>
<option>未知USB设备</option>
</select>
<select class="form-input" style="width: auto;">
<option>全部状态</option>
<option>待处理</option>
<option>已通知</option>
<option>已解决</option>
<option>误报</option>
</select>
<button class="btn btn-default">筛选</button>
</div>
<div class="card">
<div class="card-title">
<span>命中记录列表</span>
<span class="text-muted" style="font-weight: normal; font-size: 12px;">共 3 条记录</span>
</div>
<table class="table">
<thead>
<tr>
<th>策略</th>
<th>用户名</th>
<th>部门</th>
<th>设备名称</th>
<th>IP地址</th>
<th>命中内容</th>
<th>首次检测</th>
<th>最近检测</th>
<th>状态</th>
<th>操作</th>
</tr>
</thead>
<tbody>
<tr>
<td><span class="badge badge-primary">OpenClaw</span></td>
<td>zhangsan</td>
<td>研发部</td>
<td>DESKTOP-ABC123</td>
<td>10.90.5.101</td>
<td>OpenClaw</td>
<td>2026-07-18</td>
<td>2026-07-20</td>
<td><span class="tag tag-danger">待处理</span></td>
<td>
<select class="form-input" style="width: auto; padding: 4px 8px; font-size: 12px;">
<option>标记为...</option>
<option>已通知</option>
<option>已解决</option>
<option>误报</option>
</select>
</td>
</tr>
<tr>
<td><span class="badge badge-primary">OpenClaw</span></td>
<td>lisi</td>
<td>销售部</td>
<td>DESKTOP-DEF456</td>
<td>10.90.5.102</td>
<td>向日葵</td>
<td>2026-07-15</td>
<td>2026-07-20</td>
<td><span class="tag tag-warning">已通知</span></td>
<td>
<select class="form-input" style="width: auto; padding: 4px 8px; font-size: 12px;">
<option>标记为...</option>
<option selected>已解决</option>
<option>误报</option>
</select>
</td>
</tr>
<tr>
<td><span class="badge badge-primary">OpenClaw</span></td>
<td>wangwu</td>
<td>市场部</td>
<td>DESKTOP-GHI789</td>
<td>10.90.5.103</td>
<td>OpenClaw</td>
<td>2026-07-10</td>
<td>2026-07-19</td>
<td><span class="tag tag-success">已解决</span></td>
<td>
<span class="text-muted" style="font-size: 12px;">2026-07-19 IT管理员</span>
</td>
</tr>
</tbody>
</table>
</div>
<div class="card">
<div class="card-title">状态说明</div>
<div class="text-muted" style="font-size: 13px;">
<span class="tag tag-danger" style="margin-right: 8px;">待处理</span> 新检测到,未处理<br><br>
<span class="tag tag-warning" style="margin-right: 8px;">已通知</span> 已发送企微通知,等待用户处理<br><br>
<span class="tag tag-success" style="margin-right: 8px;">已解决</span> 再次检测未命中(用户已卸载)或管理员手动标记<br><br>
<span class="tag tag-info" style="margin-right: 8px;">误报</span> 管理员确认为误报
</div>
</div>
</div>
<!-- 页面6: 通知模板 -->
<div class="page" id="page-template" style="display: none;">
<div class="page-title">通知模板管理</div>
<div class="card">
<div class="card-title">模板变量说明</div>
<div class="text-muted">
<code>{username}</code> 用户名 &nbsp;
<code>{fullname}</code> 用户全名 &nbsp;
<code>{device}</code> 设备名称 &nbsp;
<code>{dept}</code> 部门 &nbsp;
<code>{software}</code> 命中内容 &nbsp;
<code>{strategy}</code> 策略名称 &nbsp;
<code>{detect_time}</code> 检测时间
</div>
</div>
<div class="card">
<div class="card-title">企微应用消息模板</div>
<div class="form-group">
<label class="form-label">策略选择</label>
<select class="form-input" style="max-width: 300px;">
<option>OpenClaw远程软件检测</option>
<option>未知USB设备检测</option>
</select>
</div>
<div class="form-group">
<label class="form-label">标题</label>
<input type="text" class="form-input" value="【安全通知】您有设备需要处理">
</div>
<div class="form-group">
<label class="form-label">正文内容</label>
<textarea class="form-input" rows="6">尊敬的 {fullname} 您好:
经检测,您使用的设备 {device}(IP: {ip})安装了非授权软件 {software},请及时卸载。
策略:{strategy}
检测时间:{detect_time}
如有疑问,请联系IT部门。</textarea>
</div>
<button class="btn btn-primary">保存模板</button>
<button class="btn btn-default">预览效果</button>
</div>
<div class="card">
<div class="card-title">企微群通知模板(部门汇总)</div>
<div class="form-group">
<label class="form-label">标题</label>
<input type="text" class="form-input" value="【{dept}部门安全合规周报】">
</div>
<div class="form-group">
<label class="form-label">正文内容</label>
<textarea class="form-input" rows="4">部门:{dept}
命中数量:{count}台
详情:
{detail}
请部门负责人督促相关人员及时处理。</textarea>
</div>
<div class="flex">
<input type="checkbox" id="notifyDept" checked>
<label for="notifyDept">启用部门负责人汇总通知</label>
</div>
</div>
</div>
<!-- 页面7: 计划任务 -->
<div class="page" id="page-schedule" style="display: none;">
<div class="page-title">计划任务管理</div>
<div class="card">
<div class="card-title">活动期间配置</div>
<div class="grid-2">
<div class="form-group">
<label class="form-label">活动开始日期</label>
<input type="date" class="form-input" value="2026-07-20">
</div>
<div class="form-group">
<label class="form-label">活动结束日期</label>
<input type="date" class="form-input" value="2026-09-30">
</div>
</div>
<div class="flex">
<input type="checkbox" id="activityEnabled" checked>
<label for="activityEnabled">启用活动期间自动检查</label>
</div>
</div>
<div class="card">
<div class="card-title">策略定时配置</div>
<table class="table">
<thead>
<tr>
<th>策略</th>
<th>执行时间</th>
<th>频率</th>
<th>状态</th>
<th>操作</th>
</tr>
</thead>
<tbody>
<tr>
<td>OpenClaw远程软件检测</td>
<td><input type="time" class="form-input" value="08:00" style="width: 120px;"></td>
<td>每天</td>
<td><span class="tag tag-success">已启用</span></td>
<td><button class="btn btn-default btn-sm">编辑</button></td>
</tr>
<tr>
<td>未知USB设备检测</td>
<td><input type="time" class="form-input" value="09:00" style="width: 120px;"></td>
<td>每周一</td>
<td><span class="tag tag-warning">已暂停</span></td>
<td><button class="btn btn-default btn-sm">编辑</button></td>
</tr>
</tbody>
</table>
</div>
<div class="card">
<div class="card-title">白名单管理</div>
<div class="form-group">
<label class="form-label">添加白名单用户/终端</label>
<input type="text" class="form-input" placeholder="输入用户名或设备名,多个用逗号分隔">
</div>
<button class="btn btn-default">添加</button>
<div class="text-muted" style="margin-top: 12px;">
当前白名单: 2项 (IT管理员, 测试设备)
</div>
</div>
</div>
<script>
function switchPage(el, pageId) {
document.querySelectorAll('.nav li').forEach(li => li.classList.remove('active'));
el.classList.add('active');
document.querySelectorAll('.page').forEach(page => page.style.display = 'none');
document.getElementById('page-' + pageId).style.display = 'block';
}
</script>
</body>
</html>
@@ -0,0 +1,746 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>方案三:邀请功能原型 — 交互流程</title>
<style>
:root {
--bg: #0f172a;
--bg2: #1e293b;
--bg3: #334155;
--border: #475569;
--text: #f1f5f9;
--dim: #94a3b8;
--accent: #3b82f6;
--accent-l: #60a5fa;
--green: #22c55e;
--amber: #f59e0b;
--red: #ef4444;
--purple: #a855f7;
--cyan: #06b6d4;
}
*{margin:0;padding:0;box-sizing:border-box}
body{font-family:-apple-system,BlinkMacSystemFont,'Segoe UI','PingFang SC','Microsoft YaHei',sans-serif;background:var(--bg);color:var(--text);line-height:1.6}
.app{max-width:1200px;margin:0 auto;padding:20px}
h1{font-size:1.8em;font-weight:700;background:linear-gradient(135deg,var(--accent-l),var(--cyan));-webkit-background-clip:text;-webkit-text-fill-color:transparent;text-align:center;margin-bottom:8px}
.subtitle{text-align:center;color:var(--dim);margin-bottom:32px;font-size:0.95em}
h2{font-size:1.2em;font-weight:600;color:var(--accent-l);margin:24px 0 12px;padding-left:12px;border-left:3px solid var(--accent)}
.flow-steps{display:flex;gap:8px;margin-bottom:32px;overflow-x:auto;padding:8px 0}
.step{flex-shrink:0;padding:10px 16px;border-radius:10px;background:var(--bg2);border:1px solid var(--border);cursor:pointer;transition:all .2s;font-size:0.88em;white-space:nowrap}
.step:hover{border-color:var(--accent);background:#1e3a5f}
.step.active{background:var(--accent);border-color:var(--accent);color:#fff;font-weight:600}
.step .step-num{display:inline-block;width:20px;height:20px;border-radius:50%;background:rgba(255,255,255,0.15);text-align:center;line-height:20px;font-size:0.75em;margin-right:6px}
.step.active .step-num{background:rgba(255,255,255,0.3)}
.scene{display:none}
.scene.show{display:block}
.card{background:var(--bg2);border:1px solid var(--border);border-radius:12px;padding:20px;margin-bottom:16px}
.card h3{color:var(--accent-l);margin-bottom:10px;font-size:1em}
.row2{display:grid;grid-template-columns:1fr 1fr;gap:16px;margin-bottom:16px}
@media(max-width:768px){.row2{grid-template-columns:1fr}}
.mockup{background:#111827;border-radius:12px;border:1px solid var(--border);overflow:hidden;position:relative}
.mockup-bar{height:32px;background:var(--bg3);display:flex;align-items:center;padding:0 12px;gap:6px;border-bottom:1px solid var(--border)}
.mockup-dot{width:10px;height:10px;border-radius:50%}
.mockup-body{padding:16px;min-height:400px;position:relative}
.tag{display:inline-block;padding:2px 8px;border-radius:4px;font-size:0.78em;font-weight:600}
.tag-blue{background:rgba(59,130,246,0.2);color:var(--accent-l)}
.tag-green{background:rgba(34,197,94,0.2);color:var(--green)}
.tag-amber{background:rgba(245,158,11,0.2);color:var(--amber)}
table{width:100%;border-collapse:collapse;font-size:0.88em}
th,td{padding:8px 12px;text-align:left;border-bottom:1px solid var(--border)}
th{background:var(--bg3);color:var(--accent-l);font-weight:600;font-size:0.85em}
.avatar{display:inline-flex;width:32px;height:32px;border-radius:50%;align-items:center;justify-content:center;font-size:0.8em;font-weight:600;flex-shrink:0}
.av-user{background:#3b82f6;color:#fff}
.av-agent{background:#22c55e;color:#fff}
.av-invited{background:#f59e0b;color:#fff}
.msg{display:flex;gap:8px;margin-bottom:10px;align-items:flex-start}
.msg-left{flex-direction:row}
.msg-right{flex-direction:row-reverse}
.msg-bubble{max-width:70%;padding:10px 14px;border-radius:12px;font-size:0.88em;line-height:1.5}
.bubble-user{background:#1e3a5f;color:var(--text)}
.bubble-agent{background:#1a3d2e;color:var(--text)}
.bubble-sys{background:var(--bg3);color:var(--dim);font-size:0.82em;max-width:100%;text-align:center;padding:6px 12px;border-radius:8px}
.bubble-invited{background:#3d2e1a;color:var(--text)}
.btn{padding:8px 16px;border-radius:8px;border:1px solid var(--border);background:var(--bg3);color:var(--text);cursor:pointer;font-size:0.85em;transition:all .15s;display:inline-flex;align-items:center;gap:6px}
.btn:hover{border-color:var(--accent);background:#1e3a5f}
.btn-primary{background:var(--accent);border-color:var(--accent);color:#fff}
.btn-primary:hover{background:#2563eb}
.tree{padding:4px 0}
.tree-item{display:flex;align-items:center;gap:8px;padding:6px 12px;border-radius:6px;cursor:pointer;font-size:0.85em;transition:background .15s}
.tree-item:hover{background:var(--bg3)}
.tree-item.selected{background:rgba(59,130,246,0.15);outline:1px solid var(--accent)}
.tree-expand{width:16px;text-align:center;color:var(--dim);font-size:0.8em;cursor:pointer}
.tree-ck{width:16px;height:16px;border:1px solid var(--border);border-radius:3px;display:inline-flex;align-items:center;justify-content:center;font-size:0.7em;cursor:pointer;flex-shrink:0}
.tree-ck.checked{background:var(--accent);border-color:var(--accent);color:#fff}
.desc{color:var(--dim);font-size:0.9em;margin:8px 0;line-height:1.7}
.desc strong{color:var(--text)}
.callout{background:rgba(59,130,246,0.1);border-left:3px solid var(--accent);border-radius:0 8px 8px 0;padding:12px 16px;margin:12px 0;font-size:0.9em}
.callout.warn{background:rgba(245,158,11,0.1);border-left-color:var(--amber)}
.callout.ok{background:rgba(34,197,94,0.1);border-left-color:var(--green)}
.arrow{color:var(--accent);font-size:1.2em;font-weight:700;margin:0 6px}
.seq{display:flex;flex-direction:column;gap:0;margin:16px 0}
.seq-row{display:flex;align-items:center;gap:12px;padding:8px 0}
.seq-line{width:2px;height:20px;background:var(--accent);margin-left:15px}
.seq-dot{width:32px;height:32px;border-radius:50%;display:flex;align-items:center;justify-content:center;font-size:0.8em;font-weight:700;flex-shrink:0}
.seq-dot-blue{background:var(--accent);color:#fff}
.seq-dot-green{background:var(--green);color:#fff}
.seq-dot-amber{background:var(--amber);color:#fff}
.seq-dot-purple{background:var(--purple);color:#fff}
.seq-text{flex:1;font-size:0.88em;color:var(--dim)}
.seq-text strong{color:var(--text)}
.seq-actor{font-size:0.75em;color:var(--accent-l);font-weight:600}
.wecom-notif{background:var(--bg3);border-radius:10px;padding:14px;border:1px solid var(--border);max-width:340px;margin:12px auto}
.wecom-notif-header{display:flex;align-items:center;gap:8px;margin-bottom:8px}
.wecom-notif-app{font-size:0.8em;color:var(--accent-l);font-weight:600}
.wecom-notif-time{font-size:0.7em;color:var(--dim)}
.wecom-notif-title{font-weight:600;font-size:0.92em;margin-bottom:4px}
.wecom-notif-body{font-size:0.85em;color:var(--dim);line-height:1.5}
.wecom-notif-btn{display:inline-block;margin-top:8px;padding:6px 14px;border-radius:6px;background:var(--accent);color:#fff;font-size:0.82em;cursor:pointer}
.compare-grid{display:grid;grid-template-columns:1fr 1fr;gap:16px}
@media(max-width:768px){.compare-grid{grid-template-columns:1fr}}
.compare-card{background:var(--bg2);border:1px solid var(--border);border-radius:12px;padding:16px}
.compare-card.recommend{border-color:var(--accent);box-shadow:0 0 0 1px var(--accent)}
.compare-card h3{text-align:center;margin-bottom:12px;font-size:1em}
.compare-card ul{padding-left:16px;font-size:0.88em}
.compare-card li{margin-bottom:4px;color:var(--dim)}
.badge{display:inline-block;padding:2px 10px;border-radius:12px;font-size:0.75em;font-weight:600}
.badge-blue{background:rgba(59,130,246,0.2);color:var(--accent-l)}
.badge-green{background:rgba(34,197,94,0.2);color:var(--green)}
.badge-red{background:rgba(239,68,68,0.2);color:var(--red)}
</style>
</head>
<body>
<div class="app">
<h1>邀请功能 — 方案三交互原型</h1>
<div class="subtitle">WebSocket + 应用消息双通道架构 | 在现有一对一对话上扩展多人参与</div>
<div class="flow-steps" id="flowSteps">
<div class="step active" data-scene="s0">0. 架构概览</div>
<div class="step" data-scene="s1">1. 一对一会话中</div>
<div class="step" data-scene="s2">2. 点击邀请</div>
<div class="step" data-scene="s3">3. 选人界面</div>
<div class="step" data-scene="s4">4. 历史共享</div>
<div class="step" data-scene="s5">5. 确认邀请</div>
<div class="step" data-scene="s6">6. 被邀请方通知</div>
<div class="step" data-scene="s7">7. 加入会话</div>
<div class="step" data-scene="s8">8. 多人会话</div>
</div>
<!-- ===== Scene 0: 架构概览 ===== -->
<div class="scene show" id="s0">
<div class="card">
<h3>方案三核心架构</h3>
<p class="desc">在现有 <strong>WebSocket 双通道</strong> 架构上扩展,不引入群聊概念。后端维护 <strong>conversation.participants</strong> 数组,所有参与者共享同一个 WebSocket 会话通道。</p>
</div>
<div class="mockup">
<div class="mockup-bar"><div class="mockup-dot" style="background:#ef4444"></div><div class="mockup-dot" style="background:#f59e0b"></div><div class="mockup-dot" style="background:#22c55e"></div><span style="color:var(--dim);font-size:0.8em;margin-left:8px">架构流程图</span></div>
<div class="mockup-body" style="display:flex;align-items:center;justify-content:center;min-height:300px">
<svg viewBox="0 0 700 260" style="max-width:100%">
<defs>
<marker id="arrowBlue" markerWidth="8" markerHeight="6" refX="8" refY="3" orient="auto"><polygon points="0 0, 8 3, 0 6" fill="#3b82f6"/></marker>
<marker id="arrowGreen" markerWidth="8" markerHeight="6" refX="8" refY="3" orient="auto"><polygon points="0 0, 8 3, 0 6" fill="#22c55e"/></marker>
<marker id="arrowAmber" markerWidth="8" markerHeight="6" refX="8" refY="3" orient="auto"><polygon points="0 0, 8 3, 0 6" fill="#f59e0b"/></marker>
</defs>
<rect x="20" y="80" width="120" height="100" rx="12" fill="#1e3a5f" stroke="#3b82f6" stroke-width="1"/>
<text x="80" y="118" text-anchor="middle" fill="#60a5fa" font-size="13" font-weight="600">H5 员工端</text>
<text x="80" y="138" text-anchor="middle" fill="#94a3b8" font-size="10">发起者</text>
<text x="80" y="155" text-anchor="middle" fill="#94a3b8" font-size="10">WebSocket</text>
<rect x="290" y="40" width="120" height="80" rx="12" fill="#1a3d2e" stroke="#22c55e" stroke-width="1"/>
<text x="350" y="72" text-anchor="middle" fill="#4ade80" font-size="13" font-weight="600">后端</text>
<text x="350" y="92" text-anchor="middle" fill="#94a3b8" font-size="10">FastAPI + Redis</text>
<text x="350" y="107" text-anchor="middle" fill="#94a3b8" font-size="10">participants[]</text>
<rect x="290" y="160" width="120" height="80" rx="12" fill="#3d2e1a" stroke="#f59e0b" stroke-width="1"/>
<text x="350" y="192" text-anchor="middle" fill="#fbbf24" font-size="13" font-weight="600">企微 API</text>
<text x="350" y="210" text-anchor="middle" fill="#94a3b8" font-size="10">应用消息推送</text>
<text x="350" y="225" text-anchor="middle" fill="#94a3b8" font-size="10">通讯录</text>
<rect x="560" y="30" width="120" height="80" rx="12" fill="#1a3d2e" stroke="#22c55e" stroke-width="1"/>
<text x="620" y="62" text-anchor="middle" fill="#4ade80" font-size="13" font-weight="600">坐席工作台</text>
<text x="620" y="82" text-anchor="middle" fill="#94a3b8" font-size="10">WebSocket</text>
<text x="620" y="97" text-anchor="middle" fill="#94a3b8" font-size="10">接单+邀请</text>
<rect x="560" y="150" width="120" height="80" rx="12" fill="#1e3a5f" stroke="#3b82f6" stroke-width="1"/>
<text x="620" y="182" text-anchor="middle" fill="#60a5fa" font-size="13" font-weight="600">H5 被邀请人</text>
<text x="620" y="202" text-anchor="middle" fill="#94a3b8" font-size="10">企微消息→H5</text>
<text x="620" y="217" text-anchor="middle" fill="#94a3b8" font-size="10">WebSocket</text>
<line x1="140" y1="110" x2="290" y2="80" stroke="#3b82f6" stroke-width="1.5" marker-end="url(#arrowBlue)"/>
<line x1="410" y1="80" x2="560" y2="70" stroke="#22c55e" stroke-width="1.5" marker-end="url(#arrowGreen)"/>
<line x1="140" y1="140" x2="290" y2="80" stroke="#3b82f6" stroke-width="1.5" stroke-dasharray="4 3"/>
<line x1="350" y1="120" x2="350" y2="160" stroke="#f59e0b" stroke-width="1.5" marker-end="url(#arrowAmber)"/>
<line x1="410" y1="200" x2="560" y2="190" stroke="#3b82f6" stroke-width="1.5" marker-end="url(#arrowBlue)"/>
<text x="200" y="80" fill="#94a3b8" font-size="9">WS 双向</text>
<text x="470" y="65" fill="#94a3b8" font-size="9">WS 双向</text>
<text x="370" y="148" fill="#94a3b8" font-size="9">邀请通知</text>
<text x="470" y="185" fill="#94a3b8" font-size="9">应用消息→H5→WS</text>
</svg>
</div>
</div>
<div class="callout ok"><strong>关键区别</strong>:不创建企微群聊,所有参与者通过 H5+WebSocket 接入同一个后端 conversation。企微应用消息仅用于「通知被邀请人」和「断连降级推送」。</div>
</div>
<!-- ===== Scene 1: 一对一会话中 ===== -->
<div class="scene" id="s1">
<div class="card">
<h3>场景:坐席正在与用户一对一对话</h3>
<p class="desc">用户「张三」遇到 VPN 问题,坐席「小李」正在处理。对话过程中发现需要网络安全组同事「王工」协助排查。</p>
</div>
<div class="row2">
<div class="mockup">
<div class="mockup-bar"><div class="mockup-dot" style="background:#ef4444"></div><div class="mockup-dot" style="background:#f59e0b"></div><div class="mockup-dot" style="background:#22c55e"></div><span style="color:var(--dim);font-size:0.78em;margin-left:8px">坐席工作台 — 会话区</span></div>
<div class="mockup-body">
<div style="display:flex;align-items:center;gap:8px;padding-bottom:10px;border-bottom:1px solid var(--border);margin-bottom:12px">
<div class="avatar av-user"></div>
<div style="flex:1">
<div style="font-weight:600;font-size:0.92em">张三</div>
<div style="font-size:0.78em;color:var(--dim)">IT研发部 · 高级开发工程师</div>
</div>
<span class="tag tag-green">会话中</span>
<button class="btn" style="font-size:0.8em" onclick="goScene('s2')">+ 邀请</button>
</div>
<div class="msg msg-left">
<div class="avatar av-user" style="width:28px;height:28px;font-size:0.7em"></div>
<div class="msg-bubble bubble-user">VPN 连接不上,提示证书错误</div>
</div>
<div class="msg msg-right">
<div class="avatar av-agent" style="width:28px;height:28px;font-size:0.7em"></div>
<div class="msg-bubble bubble-agent">您好,请问您使用的是 ATrust 还是零信任客户端?</div>
</div>
<div class="msg msg-left">
<div class="avatar av-user" style="width:28px;height:28px;font-size:0.7em"></div>
<div class="msg-bubble bubble-user">ATrust,之前一直正常的,今天突然不行了</div>
</div>
<div class="msg msg-right">
<div class="avatar av-agent" style="width:28px;height:28px;font-size:0.7em"></div>
<div class="msg-bubble bubble-agent">这个问题可能涉及网络策略变更,我邀请网络安全组的同事一起排查</div>
</div>
<div style="text-align:center;margin:16px 0">
<button class="btn-primary btn" onclick="goScene('s2')">点击「+ 邀请」按钮</button>
</div>
</div>
</div>
<div>
<div class="card">
<h3>交互说明</h3>
<div class="seq">
<div class="seq-row"><div class="seq-dot seq-dot-green">1</div><div class="seq-text"><strong>坐席点击「+ 邀请」</strong><br><span class="seq-actor">坐席工作台</span></div></div>
<div class="seq-line"></div>
<div class="seq-row"><div class="seq-dot seq-dot-blue">2</div><div class="seq-text"><strong>弹出选人弹窗</strong>(组织架构树/搜索)<br><span class="seq-actor">坐席工作台</span></div></div>
<div class="seq-line"></div>
<div class="seq-row"><div class="seq-dot seq-dot-amber">3</div><div class="seq-text"><strong>选择历史消息共享范围</strong><br><span class="seq-actor">选人弹窗</span></div></div>
<div class="seq-line"></div>
<div class="seq-row"><div class="seq-dot seq-dot-blue">4</div><div class="seq-text"><strong>确认邀请 → 后端处理</strong><br><span class="seq-actor">后端 API</span></div></div>
<div class="seq-line"></div>
<div class="seq-row"><div class="seq-dot seq-dot-amber">5</div><div class="seq-text"><strong>企微应用消息通知被邀请人</strong><br><span class="seq-actor">企微 API</span></div></div>
<div class="seq-line"></div>
<div class="seq-row"><div class="seq-dot seq-dot-green">6</div><div class="seq-text"><strong>被邀请人打开 H5 加入会话</strong><br><span class="seq-actor">H5 员工端</span></div></div>
</div>
</div>
</div>
</div>
</div>
<!-- ===== Scene 2: 点击邀请 ===== -->
<div class="scene" id="s2">
<div class="card">
<h3>步骤 2:点击「+ 邀请」→ 弹出选人弹窗</h3>
<p class="desc">弹窗包含三个区域:<strong>组织架构树</strong>(左侧)、<strong>已选人员</strong>(右侧上方)、<strong>历史消息共享设置</strong>(右侧下方)。</p>
</div>
<div class="mockup">
<div class="mockup-bar"><div class="mockup-dot" style="background:#ef4444"></div><div class="mockup-dot" style="background:#f59e0b"></div><div class="mockup-dot" style="background:#22c55e"></div><span style="color:var(--dim);font-size:0.78em;margin-left:8px">坐席工作台 — 邀请弹窗</span></div>
<div class="mockup-body" style="min-height:440px">
<div style="display:flex;gap:16px;height:100%">
<!-- 左侧:组织架构 -->
<div style="flex:1;border-right:1px solid var(--border);padding-right:12px">
<div style="font-weight:600;font-size:0.9em;margin-bottom:8px;color:var(--accent-l)">选择邀请对象</div>
<input type="text" placeholder="搜索姓名/部门..." style="width:100%;padding:8px 12px;border-radius:8px;border:1px solid var(--border);background:var(--bg3);color:var(--text);font-size:0.85em;margin-bottom:10px;outline:none">
<div class="tree">
<div class="tree-item" onclick="toggleTree(this)">
<div class="tree-expand"></div>
<div class="tree-ck" onclick="event.stopPropagation();toggleCk(this)"></div>
<span>信息技术部</span>
</div>
<div style="padding-left:20px">
<div class="tree-item" onclick="toggleTree(this)">
<div class="tree-expand"></div>
<div class="tree-ck" onclick="event.stopPropagation();toggleCk(this)"></div>
<span>IT支持组</span>
</div>
<div class="tree-item selected">
<div class="tree-expand" style="visibility:hidden"></div>
<div class="tree-ck checked" onclick="event.stopPropagation();toggleCk(this)"></div>
<span>王工</span>
<span style="font-size:0.75em;color:var(--dim);margin-left:4px">网络安全</span>
</div>
<div class="tree-item">
<div class="tree-expand" style="visibility:hidden"></div>
<div class="tree-ck" onclick="event.stopPropagation();toggleCk(this)"></div>
<span>赵磊</span>
<span style="font-size:0.75em;color:var(--dim);margin-left:4px">系统运维</span>
</div>
</div>
<div style="padding-left:20px">
<div class="tree-item">
<div class="tree-expand" style="visibility:hidden"></div>
<div class="tree-ck" onclick="event.stopPropagation();toggleCk(this)"></div>
<span>软件开发组</span>
<span style="font-size:0.75em;color:var(--dim);margin-left:4px">8人</span>
</div>
</div>
<div class="tree-item" onclick="toggleTree(this)">
<div class="tree-expand"></div>
<div class="tree-ck" onclick="event.stopPropagation();toggleCk(this)"></div>
<span>信息安全部</span>
</div>
</div>
</div>
<!-- 右侧:已选+设置 -->
<div style="flex:1;padding-left:12px">
<div style="font-weight:600;font-size:0.9em;margin-bottom:8px;color:var(--accent-l)">已选择 (1)</div>
<div style="background:var(--bg3);border-radius:8px;padding:10px;margin-bottom:16px">
<div style="display:flex;align-items:center;gap:8px;margin-bottom:6px">
<div class="avatar av-invited" style="width:28px;height:28px;font-size:0.7em"></div>
<div style="flex:1;font-size:0.88em">王工</div>
<span style="font-size:0.75em;color:var(--dim)">信息技术部·网络安全</span>
<span style="cursor:pointer;color:var(--red);font-size:0.85em"></span>
</div>
</div>
<div style="font-weight:600;font-size:0.9em;margin-bottom:8px;color:var(--accent-l)">历史消息共享</div>
<div style="background:var(--bg3);border-radius:8px;padding:12px;margin-bottom:16px">
<div style="font-size:0.85em;color:var(--dim);margin-bottom:8px">被邀请人加入后能看到哪些历史消息?</div>
<label style="display:flex;align-items:center;gap:8px;font-size:0.88em;margin-bottom:6px;cursor:pointer">
<input type="radio" name="share" checked style="accent-color:var(--accent)"> 共享全部历史消息
</label>
<label style="display:flex;align-items:center;gap:8px;font-size:0.88em;margin-bottom:6px;cursor:pointer">
<input type="radio" name="share" style="accent-color:var(--accent)"> 仅共享最近10条消息
</label>
<label style="display:flex;align-items:center;gap:8px;font-size:0.88em;cursor:pointer">
<input type="radio" name="share" style="accent-color:var(--accent)"> 不共享历史消息
</label>
</div>
<div style="font-weight:600;font-size:0.9em;margin-bottom:8px;color:var(--accent-l)">邀请说明(可选)</div>
<textarea placeholder="例如:需要你协助排查VPN证书问题..." style="width:100%;height:56px;padding:8px 12px;border-radius:8px;border:1px solid var(--border);background:var(--bg3);color:var(--text);font-size:0.85em;resize:none;outline:none"></textarea>
<div style="display:flex;gap:8px;justify-content:flex-end;margin-top:16px">
<button class="btn" onclick="goScene('s1')">取消</button>
<button class="btn btn-primary" onclick="goScene('s5')">确认邀请</button>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- ===== Scene 3: 选人界面(同Scene2,此处展示另一种交互:选部门) ===== -->
<div class="scene" id="s3">
<div class="card">
<h3>步骤 3:选择人员 — 支持按部门批量邀请</h3>
<p class="desc">勾选部门节点 = 邀请该部门<strong>所有人</strong>。每个被邀请人都会收到独立的应用消息通知。</p>
</div>
<div class="row2">
<div class="mockup">
<div class="mockup-bar"><div class="mockup-dot" style="background:#ef4444"></div><div class="mockup-dot" style="background:#f59e0b"></div><div class="mockup-dot" style="background:#22c55e"></div><span style="color:var(--dim);font-size:0.78em;margin-left:8px">选人 — 勾选整个部门</span></div>
<div class="mockup-body" style="min-height:280px">
<div class="tree">
<div class="tree-item" style="background:rgba(59,130,246,0.1)">
<div class="tree-expand"></div>
<div class="tree-ck checked"></div>
<span style="font-weight:600">信息安全部</span>
<span class="tag tag-blue" style="margin-left:8px">全部邀请</span>
</div>
<div style="padding-left:20px">
<div class="tree-item">
<div class="tree-expand" style="visibility:hidden"></div>
<div class="tree-ck checked"></div>
<span>陈安全</span>
<span style="font-size:0.75em;color:var(--dim);margin-left:4px">安全主管</span>
</div>
<div class="tree-item">
<div class="tree-expand" style="visibility:hidden"></div>
<div class="tree-ck checked"></div>
<span>刘防护</span>
<span style="font-size:0.75em;color:var(--dim);margin-left:4px">安全工程师</span>
</div>
</div>
<div class="tree-item">
<div class="tree-expand"></div>
<div class="tree-ck"></div>
<span>IT支持组</span>
<span style="font-size:0.75em;color:var(--dim);margin-left:4px">3人</span>
</div>
</div>
</div>
</div>
<div>
<div class="card">
<h3>选人交互规则</h3>
<table>
<tr><th>操作</th><th>效果</th></tr>
<tr><td>勾选人员</td><td>该人员加入已选列表</td></tr>
<tr><td>勾选部门</td><td>该部门下所有人员加入已选列表</td></tr>
<tr><td>搜索姓名</td><td>模糊匹配,点击结果直接加入已选</td></tr>
<tr><td>取消勾选</td><td>从已选列表移除</td></tr>
</table>
</div>
<div class="callout warn"><strong>注意</strong>:邀请人数建议上限 10 人。超过 10 人时应提示「当前邀请人数较多,建议优先邀请关键人员」。这不是硬限制,而是用户体验优化。</div>
</div>
</div>
</div>
<!-- ===== Scene 4: 历史消息共享 ===== -->
<div class="scene" id="s4">
<div class="card">
<h3>步骤 4:历史消息共享设置</h3>
<p class="desc">邀请时坐席可以选择被邀请人能看到哪些历史消息。这是<strong>隐私保护</strong>的重要设计——避免敏感信息(如员工个人账号问题)被无关人员看到。</p>
</div>
<div class="row2">
<div class="card">
<h3>三种共享模式对比</h3>
<table>
<tr><th>模式</th><th>适用场景</th><th>隐私风险</th></tr>
<tr><td><span class="badge badge-green">共享全部</span></td><td>通用IT问题(VPN/网络/打印),无敏感信息</td><td style="color:var(--green)"></td></tr>
<tr><td><span class="badge badge-blue">最近10条</span></td><td>对话较长,仅需上下文即可理解当前问题</td><td style="color:var(--amber)"></td></tr>
<tr><td><span class="badge badge-red">不共享</span></td><td>涉及员工个人账号/权限等敏感信息</td><td style="color:var(--green)"></td></tr>
</table>
</div>
<div class="card">
<h3>被邀请人视角 — 共享 vs 不共享</h3>
<div style="display:flex;gap:12px">
<div style="flex:1;background:var(--bg3);border-radius:8px;padding:10px">
<div style="font-size:0.82em;font-weight:600;color:var(--green);margin-bottom:6px">共享全部</div>
<div style="font-size:0.82em;color:var(--dim)">
[张三] VPN连不上...<br>
[小李] 请问用的是...<br>
[张三] ATrust...<br>
<span style="color:var(--accent-l)">↑ 可看到完整上下文</span>
</div>
</div>
<div style="flex:1;background:var(--bg3);border-radius:8px;padding:10px">
<div style="font-size:0.82em;font-weight:600;color:var(--red);margin-bottom:6px">不共享</div>
<div style="font-size:0.82em;color:var(--dim)">
<span style="opacity:0.4">[历史消息不可见]</span><br>
── 加入会话 ──<br>
[王工] 我来看看VPN...<br>
<span style="color:var(--amber)">↑ 仅能看到加入后的消息</span>
</div>
</div>
</div>
</div>
</div>
<div class="callout"><strong>默认值</strong>:建议默认选中「最近10条」,在可见性和隐私之间取得平衡。坐席可根据实际情况调整。</div>
</div>
<!-- ===== Scene 5: 确认邀请 ===== -->
<div class="scene" id="s5">
<div class="card">
<h3>步骤 5:确认邀请 → 后端处理流程</h3>
</div>
<div class="mockup">
<div class="mockup-bar"><div class="mockup-dot" style="background:#ef4444"></div><div class="mockup-dot" style="background:#f59e0b"></div><div class="mockup-dot" style="background:#22c55e"></div><span style="color:var(--dim);font-size:0.78em;margin-left:8px">确认邀请 — 后端处理时序</span></div>
<div class="mockup-body" style="min-height:340px">
<div class="seq">
<div class="seq-row">
<div class="seq-dot seq-dot-green">1</div>
<div class="seq-text"><strong>坐席点击「确认邀请」</strong><div class="seq-actor">坐席工作台 → POST /api/conversations/:id/invite</div></div>
</div>
<div class="seq-line"></div>
<div class="seq-row">
<div class="seq-dot seq-dot-blue">2</div>
<div class="seq-text"><strong>后端更新 participants 数组</strong>,将王工 userid 加入 conversation.participants<div class="seq-actor">后端 → PostgreSQL</div></div>
</div>
<div class="seq-line"></div>
<div class="seq-row">
<div class="seq-dot seq-dot-blue">3</div>
<div class="seq-text"><strong>后端生成邀请卡片消息</strong>(含会话ID + H5入口链接 + 邀请说明 + 共享的历史消息摘要)<div class="seq-actor">后端 → 企微应用消息 API</div></div>
</div>
<div class="seq-line"></div>
<div class="seq-row">
<div class="seq-dot seq-dot-amber">4</div>
<div class="seq-text"><strong>企微推送应用消息给王工</strong>(单聊卡片消息,非群聊消息)<div class="seq-actor">企微 → 王工企微客户端</div></div>
</div>
<div class="seq-line"></div>
<div class="seq-row">
<div class="seq-dot seq-dot-green">5</div>
<div class="seq-text"><strong>坐席工作台显示「已邀请王工」系统消息</strong>,会话参与者列表更新<div class="seq-actor">WebSocket → 坐席工作台</div></div>
</div>
<div class="seq-line"></div>
<div class="seq-row">
<div class="seq-dot seq-dot-blue">6</div>
<div class="seq-text"><strong>坐席工作台显示「已邀请王工」系统消息</strong>,会话参与者列表更新<div class="seq-actor">WebSocket → H5 发起者</div></div>
</div>
</div>
</div>
</div>
<div class="card" style="margin-top:16px">
<h3>API 接口设计</h3>
<table>
<tr><th>接口</th><th>方法</th><th>说明</th></tr>
<tr><td style="font-family:monospace;color:var(--cyan)">/api/conversations/:id/invite</td><td>POST</td><td>邀请人员加入会话</td></tr>
<tr><td style="font-family:monospace;color:var(--cyan)">/api/conversations/:id/participants</td><td>GET</td><td>获取会话参与者列表</td></tr>
<tr><td style="font-family:monospace;color:var(--cyan)">/api/conversations/:id/participants/:uid</td><td>DELETE</td><td>移除参与者(坐席/被邀请人退出)</td></tr>
<tr><td style="font-family:monospace;color:var(--cyan)">/api/contacts/departments</td><td>GET</td><td>获取组织架构树(缓存自eHR/企微通讯录)</td></tr>
<tr><td style="font-family:monospace;color:var(--cyan)">/api/contacts/search</td><td>GET</td><td>搜索人员(姓名/工号模糊匹配)</td></tr>
</table>
</div>
</div>
<!-- ===== Scene 6: 被邀请方通知 ===== -->
<div class="scene" id="s6">
<div class="card">
<h3>步骤 6:被邀请人收到企微应用消息</h3>
<p class="desc">王工在企微中收到一条<strong>应用消息卡片</strong>,点击卡片中的按钮即可跳转到 H5 会话页面。</p>
</div>
<div class="row2">
<div class="mockup">
<div class="mockup-bar"><div class="mockup-dot" style="background:#ef4444"></div><div class="mockup-dot" style="background:#f59e0b"></div><div class="mockup-dot" style="background:#22c55e"></div><span style="color:var(--dim);font-size:0.78em;margin-left:8px">企微消息卡片 — 通知样式</span></div>
<div class="mockup-body" style="display:flex;align-items:center;justify-content:center;min-height:340px">
<div class="wecom-notif">
<div class="wecom-notif-header">
<div class="avatar av-invited" style="width:24px;height:24px;font-size:0.65em">IT</div>
<div class="wecom-notif-app">IT智能服务台</div>
<div class="wecom-notif-time">刚刚</div>
</div>
<div class="wecom-notif-title">邀请你加入 IT 支持会话</div>
<div class="wecom-notif-body">
<div style="margin-bottom:6px"><strong>邀请人:</strong>小李(IT支持组)</div>
<div style="margin-bottom:6px"><strong>发起人:</strong>张三(IT研发部)</div>
<div style="margin-bottom:6px"><strong>问题摘要:</strong>VPN证书错误,ATrust客户端无法连接</div>
<div style="margin-bottom:6px"><strong>邀请说明:</strong>需要你协助排查VPN证书问题</div>
</div>
<div style="display:flex;gap:8px;margin-top:10px">
<div class="wecom-notif-btn" onclick="goScene('s7')">加入会话</div>
</div>
</div>
</div>
</div>
<div>
<div class="card">
<h3>消息卡片设计要点</h3>
<ul style="color:var(--dim);font-size:0.88em">
<li><strong style="color:var(--text)">使用交互式卡片消息</strong>textcard 类型),而非纯文本</li>
<li><strong style="color:var(--text)">包含问题摘要</strong>,让被邀请人判断是否需要立即加入</li>
<li><strong style="color:var(--text)">「加入会话」按钮</strong>直接跳转 H5 页面(企微内置浏览器)</li>
<li><strong style="color:var(--text)">如果共享了历史消息</strong>,摘要中自动包含最近 2-3 条关键消息</li>
</ul>
</div>
<div class="card">
<h3>降级方案</h3>
<p class="desc">如果被邀请人<strong>未安装企微</strong><strong>处于离线</strong>:后端记录邀请状态为「待加入」。被邀请人下次登录企微时会收到消息。同时,坐席工作台显示邀请状态:<span class="tag tag-amber">待加入</span> / <span class="tag tag-green">已加入</span></p>
</div>
</div>
</div>
</div>
<!-- ===== Scene 7: 加入会话 ===== -->
<div class="scene" id="s7">
<div class="card">
<h3>步骤 7:被邀请人点击「加入会话」→ H5 页面</h3>
<p class="desc">王工点击消息卡片按钮 → 企微内置浏览器打开 H5 页面 → 自动加入 WebSocket 会话 → 看到历史消息(如果被共享)→ 可直接发消息</p>
</div>
<div class="row2">
<div class="mockup">
<div class="mockup-bar"><div class="mockup-dot" style="background:#ef4444"></div><div class="mockup-dot" style="background:#f59e0b"></div><div class="mockup-dot" style="background:#22c55e"></div><span style="color:var(--dim);font-size:0.78em;margin-left:8px">H5 被邀请人视角 — 加入后</span></div>
<div class="mockup-body">
<div style="display:flex;align-items:center;gap:8px;padding-bottom:10px;border-bottom:1px solid var(--border);margin-bottom:12px">
<div style="font-weight:600;font-size:0.92em;flex:1">IT支持会话</div>
<span style="font-size:0.78em;color:var(--dim)">3人参与</span>
</div>
<div class="msg" style="justify-content:center">
<div class="msg-bubble bubble-sys">── 小李 邀请了 王工 加入会话 ──</div>
</div>
<div class="msg msg-left">
<div class="avatar av-user" style="width:28px;height:28px;font-size:0.7em"></div>
<div class="msg-bubble bubble-user">VPN 连接不上,提示证书错误</div>
</div>
<div class="msg msg-right">
<div class="avatar av-agent" style="width:28px;height:28px;font-size:0.7em"></div>
<div class="msg-bubble bubble-agent">请问您使用的是 ATrust 还是零信任客户端?</div>
</div>
<div class="msg msg-left">
<div class="avatar av-user" style="width:28px;height:28px;font-size:0.7em"></div>
<div class="msg-bubble bubble-user">ATrust,之前一直正常的,今天突然不行了</div>
</div>
<div class="msg" style="justify-content:center">
<div class="msg-bubble bubble-sys">── 王工 已加入会话 ──</div>
</div>
<div class="msg msg-right">
<div class="avatar av-invited" style="width:28px;height:28px;font-size:0.7em"></div>
<div class="msg-bubble bubble-invited">我来看看,先确认下证书服务状态</div>
</div>
</div>
</div>
<div>
<div class="card">
<h3>加入流程细节</h3>
<div class="seq">
<div class="seq-row"><div class="seq-dot seq-dot-amber">1</div><div class="seq-text"><strong>点击「加入会话」</strong><br>企微内置浏览器打开 H5 URL(含 conversation_id + token</div></div>
<div class="seq-line"></div>
<div class="seq-row"><div class="seq-dot seq-dot-blue">2</div><div class="seq-text"><strong>H5 自动认证</strong><br>企微环境 → OAuth2 自动登录;非企微 → Mock 登录</div></div>
<div class="seq-line"></div>
<div class="seq-row"><div class="seq-dot seq-dot-blue">3</div><div class="seq-text"><strong>建立 WebSocket 连接</strong><br>加入 conversation 的消息通道</div></div>
<div class="seq-line"></div>
<div class="seq-row"><div class="seq-dot seq-dot-green">4</div><div class="seq-text"><strong>拉取历史消息</strong><br>根据共享设置,显示可查看的历史消息</div></div>
<div class="seq-line"></div>
<div class="seq-row"><div class="seq-dot seq-dot-green">5</div><div class="seq-text"><strong>广播「XX 已加入」</strong><br>所有参与者(坐席+用户+其他被邀请人)都能看到</div></div>
</div>
</div>
</div>
</div>
</div>
<!-- ===== Scene 8: 多人会话 ===== -->
<div class="scene" id="s8">
<div class="card">
<h3>步骤 8:多人会话持续进行</h3>
<p class="desc">所有人通过 <strong>WebSocket</strong> 实时通信。坐席工作台和所有 H5 参与者共享同一个消息通道。坐席拥有<strong>管理权限</strong>(可移除参与者、结束会话)。</p>
</div>
<div class="row2">
<!-- 坐席工作台视角 -->
<div class="mockup">
<div class="mockup-bar"><div class="mockup-dot" style="background:#ef4444"></div><div class="mockup-dot" style="background:#f59e0b"></div><div class="mockup-dot" style="background:#22c55e"></div><span style="color:var(--dim);font-size:0.78em;margin-left:8px">坐席工作台 — 多人会话</span></div>
<div class="mockup-body" style="min-height:400px">
<!-- 参与者栏 -->
<div style="display:flex;align-items:center;gap:8px;padding-bottom:10px;border-bottom:1px solid var(--border);margin-bottom:12px;flex-wrap:wrap">
<div class="avatar av-user" style="width:32px;height:32px;font-size:0.7em"></div>
<div style="font-size:0.8em">张三<span style="color:var(--dim);margin-left:4px">发起</span></div>
<div class="avatar av-agent" style="width:32px;height:32px;font-size:0.7em"></div>
<div style="font-size:0.8em">小李<span style="color:var(--dim);margin-left:4px">坐席</span></div>
<div class="avatar av-invited" style="width:32px;height:32px;font-size:0.7em"></div>
<div style="font-size:0.8em">王工<span style="color:var(--dim);margin-left:4px">邀请</span></div>
<button class="btn" style="font-size:0.75em;margin-left:auto" onclick="goScene('s2')">+ 邀请</button>
</div>
<!-- 消息 -->
<div class="msg" style="justify-content:center"><div class="msg-bubble bubble-sys">── 小李 邀请了 王工 加入会话 ──</div></div>
<div class="msg msg-left">
<div class="avatar av-user" style="width:28px;height:28px;font-size:0.7em"></div>
<div style="font-size:0.72em;color:var(--dim);margin-bottom:2px">张三</div>
<div class="msg-bubble bubble-user">VPN 连接不上,提示证书错误</div>
</div>
<div class="msg msg-right">
<div class="avatar av-agent" style="width:28px;height:28px;font-size:0.7em"></div>
<div style="font-size:0.72em;color:var(--dim);margin-bottom:2px">小李(坐席)</div>
<div class="msg-bubble bubble-agent">请问您使用的是 ATrust 还是零信任客户端?</div>
</div>
<div class="msg msg-left">
<div class="avatar av-user" style="width:28px;height:28px;font-size:0.7em"></div>
<div style="font-size:0.72em;color:var(--dim);margin-bottom:2px">张三</div>
<div class="msg-bubble bubble-user">ATrust,今天突然不行了</div>
</div>
<div class="msg" style="justify-content:center"><div class="msg-bubble bubble-sys">── 王工 已加入会话 ──</div></div>
<div class="msg msg-right">
<div class="avatar av-invited" style="width:28px;height:28px;font-size:0.7em"></div>
<div style="font-size:0.72em;color:var(--dim);margin-bottom:2px">王工(受邀)</div>
<div class="msg-bubble bubble-invited">证书服务器刚做了更新,我来检查下白名单配置</div>
</div>
<div class="msg msg-left">
<div class="avatar av-user" style="width:28px;height:28px;font-size:0.7em"></div>
<div style="font-size:0.72em;color:var(--dim);margin-bottom:2px">张三</div>
<div class="msg-bubble bubble-user">好的,麻烦了</div>
</div>
</div>
</div>
<!-- H5 被邀请人视角 -->
<div class="mockup">
<div class="mockup-bar"><div class="mockup-dot" style="background:#ef4444"></div><div class="mockup-dot" style="background:#f59e0b"></div><div class="mockup-dot" style="background:#22c55e"></div><span style="color:var(--dim);font-size:0.78em;margin-left:8px">H5 被邀请人视角 — 王工</span></div>
<div class="mockup-body" style="min-height:400px">
<div style="display:flex;align-items:center;gap:8px;padding-bottom:10px;border-bottom:1px solid var(--border);margin-bottom:12px">
<div style="font-weight:600;font-size:0.92em;flex:1">IT支持会话</div>
<span style="font-size:0.78em;color:var(--dim)">3人</span>
<span class="tag tag-green">已加入</span>
</div>
<!-- 王工只能看到共享后的消息 -->
<div style="font-size:0.78em;color:var(--dim);text-align:center;margin-bottom:8px;padding:6px;background:var(--bg3);border-radius:6px">你可以查看最近10条历史消息</div>
<div class="msg msg-left">
<div class="avatar av-user" style="width:28px;height:28px;font-size:0.7em"></div>
<div style="font-size:0.72em;color:var(--dim);margin-bottom:2px">张三</div>
<div class="msg-bubble bubble-user">ATrust,今天突然不行了</div>
</div>
<div class="msg" style="justify-content:center"><div class="msg-bubble bubble-sys">── 王工 已加入会话 ──</div></div>
<div class="msg msg-right">
<div class="avatar av-invited" style="width:28px;height:28px;font-size:0.7em"></div>
<div class="msg-bubble bubble-invited">证书服务器刚做了更新,我来检查下白名单配置</div>
</div>
<div class="msg msg-left">
<div class="avatar av-user" style="width:28px;height:28px;font-size:0.7em"></div>
<div style="font-size:0.72em;color:var(--dim);margin-bottom:2px">张三</div>
<div class="msg-bubble bubble-user">好的,麻烦了</div>
</div>
<!-- 输入区 -->
<div style="margin-top:auto;padding-top:12px;border-top:1px solid var(--border)">
<div style="display:flex;gap:8px">
<input type="text" placeholder="输入消息..." style="flex:1;padding:8px 12px;border-radius:8px;border:1px solid var(--border);background:var(--bg3);color:var(--text);font-size:0.88em;outline:none">
<button class="btn btn-primary" style="font-size:0.85em">发送</button>
</div>
</div>
</div>
</div>
</div>
<div class="card" style="margin-top:16px">
<h3>坐席管理能力</h3>
<div class="row2" style="margin-bottom:0">
<div>
<table>
<tr><th>操作</th><th>权限</th><th>说明</th></tr>
<tr><td>邀请人员</td><td>坐席</td><td>选择人员加入会话</td></tr>
<tr><td>移除参与者</td><td>坐席</td><td>将某人移出会话(不通知)</td></tr>
<tr><td>主动退出</td><td>被邀请人</td><td>被邀请人可自行退出</td></tr>
<tr><td>结束会话</td><td>坐席</td><td>关闭会话,所有人断开</td></tr>
<tr><td>转让坐席</td><td>坐席</td><td>将坐席角色转给其他参与者</td></tr>
</table>
</div>
<div>
<div class="callout warn"><strong>与企微群聊的核心区别</strong><br>1. 会话数据完全在后端控制,不依赖企微群聊 API<br>2. 被邀请人通过 H5 WebSocket 通信,无需加入企微群<br>3. 坐席拥有管理权限,可控制参与者和历史可见性<br>4. 外部联系人(供应商等)也可通过 H5 链接加入</div>
</div>
</div>
</div>
</div>
</div>
<script>
const steps = document.querySelectorAll('.step');
const scenes = document.querySelectorAll('.scene');
function goScene(id) {
steps.forEach(s => s.classList.remove('active'));
scenes.forEach(s => s.classList.remove('show'));
const idx = parseInt(id.replace('s',''));
steps[idx].classList.add('active');
document.getElementById(id).classList.add('show');
window.scrollTo({top: 0, behavior: 'smooth'});
}
steps.forEach(s => {
s.addEventListener('click', () => goScene(s.dataset.scene));
});
</script>
</body>
</html>
@@ -0,0 +1,472 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<title>员工名片卡片原型</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'PingFang SC', 'Hiragino Sans GB', sans-serif;
background: #ededed;
display: flex;
justify-content: center;
min-height: 100vh;
}
.phone-frame {
width: 375px;
background: #ededed;
min-height: 100vh;
display: flex;
flex-direction: column;
position: relative;
}
/* 顶部导航栏 */
.nav-bar {
background: #f7f7f7;
height: 44px;
display: flex;
align-items: center;
justify-content: center;
border-bottom: 1px solid #e0e0e0;
position: sticky;
top: 0;
z-index: 10;
}
.nav-bar .title {
font-size: 17px;
font-weight: 600;
color: #1a1a1a;
}
.nav-bar .back {
position: absolute;
left: 12px;
font-size: 15px;
color: #07C160;
}
/* 聊天区域 */
.chat-area {
flex: 1;
padding: 12px 16px;
display: flex;
flex-direction: column;
gap: 16px;
overflow-y: auto;
padding-bottom: 80px;
}
/* 时间戳 */
.time-stamp {
text-align: center;
font-size: 12px;
color: #999;
margin: 8px 0;
}
/* 消息行 */
.msg-row {
display: flex;
gap: 8px;
max-width: 100%;
}
.msg-row.agent { flex-direction: row; }
.msg-row.user { flex-direction: row-reverse; }
.avatar {
width: 40px;
height: 40px;
border-radius: 6px;
flex-shrink: 0;
display: flex;
align-items: center;
justify-content: center;
font-size: 16px;
color: #fff;
font-weight: 600;
}
.avatar.agent { background: #07C160; }
.avatar.user { background: #5d9cec; }
.msg-content {
max-width: 260px;
display: flex;
flex-direction: column;
}
.msg-row.user .msg-content { align-items: flex-end; }
.sender-name {
font-size: 12px;
color: #999;
margin-bottom: 2px;
margin-left: 4px;
}
.bubble {
padding: 10px 14px;
border-radius: 8px;
font-size: 15px;
line-height: 1.5;
word-break: break-word;
position: relative;
}
.agent .bubble {
background: #fff;
color: #1a1a1a;
border-top-left-radius: 2px;
}
.user .bubble {
background: #95EC69;
color: #1a1a1a;
border-top-right-radius: 2px;
}
/* === 员工名片卡片 === */
.contact-card {
width: 260px;
background: #fff;
border-radius: 8px;
overflow: hidden;
border-top-left-radius: 2px;
box-shadow: 0 1px 3px rgba(0,0,0,0.06);
}
.card-header {
padding: 14px 16px 10px;
display: flex;
align-items: center;
gap: 10px;
border-bottom: 1px solid #f0f0f0;
}
.card-avatar {
width: 44px;
height: 44px;
border-radius: 6px;
flex-shrink: 0;
display: flex;
align-items: center;
justify-content: center;
font-size: 18px;
color: #fff;
font-weight: 600;
}
.card-avatar.wang { background: linear-gradient(135deg, #667eea, #764ba2); }
.card-avatar.li { background: linear-gradient(135deg, #f093fb, #f5576c); }
.card-avatar.zhao { background: linear-gradient(135deg, #4facfe, #00f2fe); }
.card-name-row {
flex: 1;
display: flex;
flex-direction: column;
gap: 2px;
}
.card-name {
font-size: 16px;
font-weight: 600;
color: #1a1a1a;
display: flex;
align-items: center;
gap: 6px;
}
.card-name .gender {
font-size: 12px;
color: #4facfe;
}
.card-dept {
font-size: 13px;
color: #888;
}
.card-body {
padding: 10px 16px;
}
.card-info-item {
display: flex;
align-items: center;
gap: 8px;
padding: 4px 0;
font-size: 13px;
color: #555;
}
.card-info-item .label {
color: #aaa;
width: 56px;
flex-shrink: 0;
}
.card-info-item .value {
color: #333;
}
.card-info-item .value.responsibility {
color: #07C160;
font-weight: 500;
}
.card-footer {
padding: 0 16px 12px;
}
.card-action-btn {
display: flex;
align-items: center;
justify-content: center;
gap: 4px;
width: 100%;
height: 36px;
background: #07C160;
color: #fff;
border: none;
border-radius: 6px;
font-size: 14px;
font-weight: 500;
cursor: pointer;
transition: opacity 0.2s;
}
.card-action-btn:active { opacity: 0.85; }
.card-action-btn .icon {
font-size: 16px;
}
/* 名片类型标识 */
.card-type-badge {
display: inline-block;
font-size: 11px;
padding: 1px 6px;
border-radius: 3px;
background: #f0f0f0;
color: #888;
margin-left: auto;
}
/* 底部输入栏 */
.input-bar {
position: fixed;
bottom: 0;
width: 375px;
background: #f7f7f7;
border-top: 1px solid #dcdcdc;
padding: 8px 12px;
display: flex;
align-items: center;
gap: 8px;
z-index: 10;
}
.input-bar input {
flex: 1;
height: 36px;
border: none;
border-radius: 4px;
background: #fff;
padding: 0 12px;
font-size: 15px;
outline: none;
}
.input-bar .send-btn {
background: #07C160;
color: #fff;
border: none;
padding: 0 16px;
height: 36px;
border-radius: 4px;
font-size: 14px;
cursor: pointer;
}
/* Toast 提示 */
.toast {
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background: rgba(0,0,0,0.75);
color: #fff;
padding: 12px 20px;
border-radius: 8px;
font-size: 14px;
z-index: 999;
opacity: 0;
transition: opacity 0.3s;
pointer-events: none;
max-width: 280px;
text-align: center;
}
.toast.show { opacity: 1; }
/* 系统消息 */
.system-msg {
text-align: center;
font-size: 12px;
color: #b0b0b0;
margin: 8px 0;
}
.system-msg span {
background: #e8e8e8;
padding: 2px 8px;
border-radius: 4px;
}
</style>
</head>
<body>
<div class="phone-frame">
<!-- 顶部导航 -->
<div class="nav-bar">
<span class="back">← 返回</span>
<span class="title">IT智能服务台</span>
</div>
<!-- 聊天区域 -->
<div class="chat-area">
<!-- 时间戳 -->
<div class="time-stamp">今天 01:08</div>
<!-- 用户消息 -->
<div class="msg-row user">
<div class="avatar user"></div>
<div class="msg-content">
<div class="bubble">我的电脑连不上打印机了</div>
</div>
</div>
<!-- 坐席/AI回复 -->
<div class="msg-row agent">
<div class="avatar agent">AI</div>
<div class="msg-content">
<div class="sender-name">IT智能助手</div>
<div class="bubble">
您好,打印机问题属于行政设备范畴,不在IT服务台服务范围内 😊
<br><br>
为您推荐行政服务相关联系人,您可以直接点击名片联系TA:
</div>
</div>
</div>
<!-- 名片卡片1 -->
<div class="msg-row agent">
<div class="avatar agent">AI</div>
<div class="msg-content">
<div class="contact-card" onclick="showToast('正在打开与 王芳 的企微聊天...')">
<div class="card-header">
<div class="card-avatar wang"></div>
<div class="card-name-row">
<div class="card-name">
王芳
<span class="gender"></span>
</div>
<div class="card-dept">行政部 · 设备管理岗</div>
</div>
<span class="card-type-badge">员工名片</span>
</div>
<div class="card-body">
<div class="card-info-item">
<span class="label">负责业务</span>
<span class="value responsibility">打印机/复印机/扫描仪</span>
</div>
<div class="card-info-item">
<span class="label">分机号</span>
<span class="value">8002</span>
</div>
<div class="card-info-item">
<span class="label">服务区域</span>
<span class="value">滨江园区 3-5楼</span>
</div>
</div>
<div class="card-footer">
<button class="card-action-btn" onclick="event.stopPropagation(); showToast('调用 wx.invoke(\"openEnterpriseChat\", {userids: \"WangFang\"}) 打开与王芳的企微单聊')">
<span class="icon">💬</span>
<span>联系TA</span>
</button>
</div>
</div>
</div>
</div>
<!-- 系统提示 -->
<div class="system-msg">
<span>以上为AI自动推荐,点击名片可直接发起企微聊天</span>
</div>
<!-- 名片卡片2(多推荐场景) -->
<div class="msg-row user">
<div class="avatar user"></div>
<div class="msg-content">
<div class="bubble">我的工牌丢了,补办找谁?</div>
</div>
</div>
<div class="msg-row agent">
<div class="avatar agent">AI</div>
<div class="msg-content">
<div class="sender-name">IT智能助手</div>
<div class="bubble">
工牌补办属于人力资源服务,为您推荐HR联系人:
</div>
</div>
</div>
<!-- HR名片 -->
<div class="msg-row agent">
<div class="avatar agent">AI</div>
<div class="msg-content">
<div class="contact-card" onclick="showToast('正在打开与 李娜 的企微聊天...')">
<div class="card-header">
<div class="card-avatar li"></div>
<div class="card-name-row">
<div class="card-name">
李娜
<span class="gender" style="color:#f5576c"></span>
</div>
<div class="card-dept">人力资源部 · 员工服务岗</div>
</div>
<span class="card-type-badge">员工名片</span>
</div>
<div class="card-body">
<div class="card-info-item">
<span class="label">负责业务</span>
<span class="value responsibility">工牌补办/考勤异常/入职手续</span>
</div>
<div class="card-info-item">
<span class="label">分机号</span>
<span class="value">8005</span>
</div>
<div class="card-info-item">
<span class="label">办公地点</span>
<span class="value">滨江园区 A栋3楼</span>
</div>
</div>
<div class="card-footer">
<button class="card-action-btn" onclick="event.stopPropagation(); showToast('调用 wx.invoke(\"openEnterpriseChat\", {userids: \"LiNa\"}) 打开与李娜的企微单聊')">
<span class="icon">💬</span>
<span>联系TA</span>
</button>
</div>
</div>
</div>
</div>
</div>
<!-- 底部输入栏 -->
<div class="input-bar">
<input type="text" placeholder="输入消息..." disabled>
<button class="send-btn">发送</button>
</div>
</div>
<!-- Toast -->
<div class="toast" id="toast"></div>
<script>
function showToast(msg) {
const toast = document.getElementById('toast');
toast.textContent = msg;
toast.classList.add('show');
setTimeout(() => {
toast.classList.remove('show');
}, 2500);
}
</script>
</body>
</html>