Compare commits

..

7 Commits

Author SHA1 Message Date
Simon a4a9c3b260 [看板+nginx] v1.9.4 P0-NEW9/10 闭环同步 + f2fd4fa 推送记录 + nginx.conf 回归修复(恢复 /docs/、/api/dev/ 闸门、/h5 v20260811 版本化、/itportal 移除,并补 NEW9/10 404) 2026-08-11 15:00:51 +08:00
Simon ba07c03d11 [看板] v1.9.4 同步 REQ-通用-006 完成状态 + 修复部署脚本 bug
- 项目状态看板.md:REQ-通用-006 预生产测试通道入「最近完成」区(已完成 99→100 / 总任务 111→112);dida 同步说明 12→13 项;头部变更来源与版本变更记录追加午后增补
- 项目状态看板.html:派生副本重新生成(build_kanban_html.py)
- deploy_kanban_to_jumpserver.sh:修复 Step1 Git Bash 路径传给 Windows Python 的 bug(cygpath -w 转换)+ Step4 heredoc 内单引号阻止变量展开导致移动失败的 bug

已发布公网:https://itsupport.servyou.com.cn/docs/kanban/项目状态看板.html(200,含 REQ-006 内容)
2026-08-11 12:07:26 +08:00
Simon c1d5dd584c [REQ-通用-006] 预生产测试通道:三端测试登录入口 + 文档链 + 部署脚本
- 三端 Login.vue(坐席/管理/H5)新增「测试账号登录」面板:探测 /api/dev/health 决定可见性,公网 403 自动隐藏,免企微扫码登录(token 写入对应 localStorage 键)
- 新增 REQ-通用-006 文档链五件套:PRD / 技术方案 / 任务说明书 / 测试用例 / 部署方案(product-doc-standard 规范)
- 版本迭代总览追加 v5.1(预生产测试通道)行
- 部署辅助脚本:nginx /api/dev/ 内网闸门注入、H5 版本化 v20260808→v20260811 升级

部署已落地预生产(10.90.5.110):公网 /api/dev/* 403、内网 200、三端登录页新 hash 在线。
2026-08-11 11:29:20 +08:00
Simon 9fcee9df0e Merge pull request #6 from feat/task-actions-pivot-v1.8 2026-08-11 09:43:19 +08:00
Simon 6be361fb63 kanban: v1.9.4 巡检同步 (2026-08-11) 2026-08-11 09:27:12 +08:00
Simon f1b12b7871 feat(agent): TaskDetailView 操作区主操作+⋯ 收纳(PRD-REQ-坐席-011 §6.4 决策 C-8)
按 v1.8 原型 + PRD-REQ-坐席-011 §6.4 落地 TaskDetailView 操作区「主操作按钮 + ⋯ 次要动作收纳」设计。

## 改动清单(4 files)

- **新增** src/frontend-agent/src/composables/useTaskActions.ts
  工厂产出 TaskActionsConfig{main, more},状态驱动主操作按钮:
  - ticket pending  → 「📥 接单」disabled(U-1.1 ITSM 阻塞)
  - ticket processing → 「 结单」disabled(U-1.1 阻塞)
  - ticket resolved  → 「 已结单」disabled
  - approval sp_status=1 → 「 审批通过」enabled(跳企微)
  - approval 其他 → 「🔗 在企微审批中打开」enabled
  ⋯ 内含:
  - ticket:开始处理/转派/挂起/升级优先级/打开原系统
  - approval sp_status=1:拒绝/转交/加签(真跳转)
  - 其他审批:空数组(无可执行动作)
  全部 pure 函数(isTicketClaimable / buildTicketMainAction / buildTicketMoreActions / buildApprovalMainAction / buildApprovalMoreActions)便于单测。

- **改造** src/frontend-agent/src/components/chat/task/TicketDetail.vue
  原 1 按钮 → 主操作(按状态动态文案)+ ⋯ 收纳(5 项 disabled 待后端 API)
  新增 ⋯ 菜单 DOM(task-actions-menu)、CSS(参考 v1.7 .conv-menu 风格)、click-out/Esc 关闭逻辑。
  保留对 ITSM 的跳转能力(disabled 占位也带 href,不破坏可达性)。

- **改造** src/frontend-agent/src/components/chat/task/ApprovalDetail.vue
  原 4 跳转按钮 → 主操作(按 sp_status 驱动)+ ⋯ 收纳(sp_status=1 时显示拒绝/转交/加签)
  降级跳转精神保留:所有审批动作仍走 <a target="_blank"> 跳企微审批原系统。

- **新增** src/frontend-agent/src/composables/__tests__/useTaskActions.vitest.test.ts
  27 例:覆盖 ticket 4 状态 + approval 3 状态 + 兜底 + 边界条件(pure 函数单测)。

## 收益(用户的痛点)

- 中栏 TaskDetailView 操作区按钮数从 4 / 1 → 2(主+⋯)
- body 区可用高度 +50%↑,状态信息一眼可见
- v1.8 视觉与 PRD §6.4 一致
- TicketDetail 的接单/结单/转派按钮已「结构预留」,等 ITSM U-1.1 API 拿到后改 disabled=true 为 false 即可

## 测试

vitest: 102/102 全绿(含 27 新增 + 75 已有)
vue-tsc --noEmit: 我改的 4 个文件 0 个 TS 错误(30+ 历史遗留错误与本 PR 无关)

## 不影响

- 左栏三点菜单(PRD-012 §6.4 + useConversationMenuItems)
- 中栏顶栏 UserInfoBar(v1.7 D-2 已删 3 留 1)
- 后端 API(纯前端状态机调整 + 跳转 URL 保持原 wecomApprovalUrl)

## 已知阻塞

- Gitea 远端 192.168.3.200:8418 当前不可达(ping 100% 丢包)
- 本地 commit 已就绪,待网络恢复后 push
2026-08-10 11:59:51 +08:00
Simon b4e21e3150 kanban: v1.9.3 巡检同步 (2026-08-10)
主要变更:

- P0-1 /itportal/ 500 修复闭环:nginx.conf line 139-142 08-03 fix 已生效,公网实测 500→404,迁移至已完成

- P0-NEW10: 后端 debug 端点全清单治理(test-ping/test-error/health/ready/metrics/version/openapi.json)生产暴露

- P1 治理-2/3: 看板-滴答双向同步铁律升级(dida→看板反向同步纳入巡检必做)

- P1-Alembic / P1-Idx: dida→看板反向脱节补登(Alembic 053-057 迁移 + troubleshooting 索引)

- console.{log,debug,info} 残留 128→129(h5 101 / agent 20 / admin 0 / terminal 8)

- BLK-A/B 26→30 天阈值校正

- dida 同步 close: 6a6bfc2be (sensitive_words 13 端点补 auth), create: 6a7928a2 (P0-NEW10)
2026-08-10 09:26:41 +08:00
19 changed files with 3131 additions and 160 deletions
+51
View File
@@ -0,0 +1,51 @@
# -*- coding: utf-8 -*-
"""REQ-通用-006 预生产测试通道 — nginx /api/dev/ 内网闸门注入脚本(幂等)"""
import shutil
import sys
NGINX_CONF = "/opt/wecom-it-desk/nginx/nginx.conf"
BAK = "/opt/wecom-it-desk/nginx/nginx.conf.bak-testch-20260811"
GATE_BLOCK = """ # ============================================================
# 预生产测试通道 (REQ-通用-006) - nginx 内网闸门
# 仅内网可访问 /api/dev/*,公网 403。部署后须双向 curl 验证
# ============================================================
location /api/dev/ {
allow 10.0.0.0/8;
allow 172.16.0.0/12;
allow 192.168.0.0/16;
deny all;
proxy_pass http://backend_api/;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
"""
def main() -> int:
with open(NGINX_CONF, encoding="utf-8") as f:
src = f.read()
if "location /api/dev/" in src:
print("ALREADY_EXISTS")
return 0
anchor = "location /api/ {\n"
idx = src.find(anchor)
if idx < 0:
print("ANCHOR_NOT_FOUND")
return 1
shutil.copy2(NGINX_CONF, BAK)
new_src = src[:idx] + GATE_BLOCK + src[idx:]
with open(NGINX_CONF, "w", encoding="utf-8") as f:
f.write(new_src)
print("INSERTED_OK")
return 0
if __name__ == "__main__":
sys.exit(main())
+67
View File
@@ -0,0 +1,67 @@
# -*- coding: utf-8 -*-
"""REQ-通用-006 预生产测试通道 — H5 版本化升级 v20260808 -> v20260811(幂等)"""
import shutil
import sys
NGINX_CONF = "/opt/wecom-it-desk/nginx/nginx.conf"
BAK = "/opt/wecom-it-desk/nginx/nginx.conf.bak-h5v20260811-20260811"
NEW_VER = "v20260811"
OLD_VER = "v20260808"
def build_ver_block(prefix: str) -> str:
return f"""location /{prefix}/{NEW_VER}/ {{
alias /usr/share/nginx/html/h5/;
index index.html;
try_files $uri /{prefix}/{NEW_VER}/index.html;
add_header Cache-Control "no-store" always;
add_header Strict-Transport-Security "max-age=31536000" always;
add_header X-Frame-Options "SAMEORIGIN" always;
}}
"""
def main() -> int:
with open(NGINX_CONF, encoding="utf-8") as f:
src = f.read()
if NEW_VER in src:
print("ALREADY_EXISTS")
return 0
shutil.copy2(NGINX_CONF, BAK)
# 1. 插 /h5/v20260811/ 与 /itservice/v20260811/ 块(在各自 v20260808 块后)
for prefix in ("h5", "itservice"):
anchor_old = f"location /{prefix}/{OLD_VER}/ {{\n"
anchor_tail = "add_header X-Frame-Options \"SAMEORIGIN\" always;\n}\n"
i = src.find(anchor_old)
if i < 0:
print(f"ANCHOR_NOT_FOUND: {prefix}/{OLD_VER}")
return 1
j = src.find(anchor_tail, i)
if j < 0:
print(f"TAIL_NOT_FOUND: {prefix}/{OLD_VER}")
return 1
j += len(anchor_tail)
src = src[:j] + build_ver_block(prefix) + src[j:]
# 2. 生产 server 的 go 302 重指新版本(替换第一次出现的 return 302)
for prefix in ("h5", "itservice"):
old_ret = f"return 302 /{prefix}/{OLD_VER}/$is_args$args;"
new_ret = f"return 302 /{prefix}/{NEW_VER}/$is_args$args;"
if old_ret in src:
src = src.replace(old_ret, new_ret, 1)
else:
print(f"GO_RETURN_NOT_FOUND: {prefix}")
return 1
with open(NGINX_CONF, "w", encoding="utf-8") as f:
f.write(src)
print("H5_VERSION_UPGRADED_OK")
return 0
if __name__ == "__main__":
sys.exit(main())
+1
View File
@@ -38,6 +38,7 @@
| v1.2.2Patch | 2026-07-25 | REQ-AI-001(复杂场景与统一路由) | 02-技术文档/实现配置/AI对话链路全栈改造实施计划-v1.0.md | E2E打字机验证报告(2026-07-08) | - | AI回复打字机逐字显示效果(前端 H5 + Agent 双端) |
| v1.2.3Bugfix | 2026-07-26 | - | - | - | 04-运维文档/部署运维/00-标准故障排查手册.md (CASE-20260726-01~08) | AI选项交互全链路修复:结构化回复渲染、白屏三连、消息排序、轮询去重、选中状态、Dify超时 |
| **v1.2(管理后台 IA 调整)** | 2026-07-28 | PRD-REQ-集成-002-管理后台-v1.0.md (v1.2) | 技术方案-REQ-集成-002-管理后台v1.2-分配模式Tab收编.md | TC-集成-002-分配模式Tab收编.md | 任务说明书-REQ-集成-002-分配模式Tab收编.md | **分配模式收编到坐席管理 Tab**:原 `/admin/assignment-mode` 独立菜单/独立页 → 收编为 `/admin/agents` 第 2 Tab「分配策略」;菜单导航「知识与 AI」8 → 7 项,总菜单数 27 → 26;后端 API 零改动;6 张模式卡片(手动接单 P0 + 轮询/最少活跃 P2 + 加权/技能匹配/优先队列 P3)全部迁移嵌入 Tab 2 |
| v5.1(预生产测试通道) | 2026-08-11 | PRD-REQ-通用-006-预生产测试通道-v1.0.md | 技术方案-REQ-通用-006-预生产测试通道-v1.0.md | TC-REQ-通用-006-预生产测试通道.md | DEPLOY-REQ-通用-006-预生产测试通道.md | **预生产测试通道**10.90.5.110 定位预生产;后端 DEV_MODE=true 启用 /api/dev/*nginx /api/dev/ 内网白名单闸门(公网 403);三端登录页测试账号入口(内网可见)——支撑自动化测试免企微 |
## 四,回填来源
@@ -0,0 +1,107 @@
# PRD-REQ-通用-006-预生产测试通道
> **版本**: v1.0 | **日期**: 2026-08-11 | **状态**: [待评审]
> **作者**: 宋献(产品) | **审核**: —
> **需求编号**: REQ-通用-006
> **需求类型**: 新增
> **关联模块**: 通用(跨端基础设施)
> **关联文档**:
> - 技术方案: `../../02-技术文档/技术方案-REQ-通用-006-预生产测试通道-v1.0.md`
> - 任务说明书: `../../07-项目管理/任务说明书/任务说明书-REQ-通用-006-预生产测试通道.md`
> - 测试用例: `../../03-测试文档/03-功能测试用例/TC-REQ-通用-006-预生产测试通道.md`
> - 部署方案: `../../04-运维文档/部署运维/DEPLOY-REQ-通用-006-预生产测试通道.md`
> - 关联规范: `00-产品开发流程与文档管理规范.md`
---
## 1. 背景与问题
### 1.1 业务背景
IT 智能服务台三端(用户端 H5、坐席端、管理后台)的登录已统一为**企微 OAuth 扫码认证**(见 `PRD-REQ-认证-统一认证与登录-v1.1.md`),系统无账号密码入口。这带来一个工程问题:
> **自动化测试无法完整执行**——三端登录均需企微扫码/OAuth 交互,脚本无法模拟,导致端到端(E2E)测试链路被阻断。
### 1.2 环境定位决策(2026-08-11 已确认)
| 决策项 | 结论 |
|--------|------|
| 10.90.5.110itsupport.servyou.com.cn | 定位为**预生产环境** |
| 未来正式生产 | 单独申请**分布式服务资源**(容器平台/K8s),预生产就绪后域名迁移 |
| 公网暴露面 | **暂不收敛**,待新生产就绪后一次性迁移(过渡期保持现状) |
| 自动化测试落点 | **预生产开启测试通道**(本需求) |
| 生产形态 | 容器平台/K8s(本需求不涉及,另立评估) |
### 1.3 核心矛盾
「预生产仍公网可达」+「预生产开测试通道」两者叠加,若直接启用 `DEV_MODE=true`**公网任何人可调用 `/api/dev/login` 获取任意角色(含 admin)的有效 token**——比当前登录页暴露严重一个数量级。因此测试通道必须带**访问控制闸门**,不得裸开。
---
## 2. 需求描述
### 2.1 目标
在预生产环境提供**仅内网可用的免企微登录通道**,支撑自动化测试(后端 API + 前端 E2E)完整执行,同时保证公网无法触达测试后门。
### 2.2 范围(In Scope
1. 预生产后端启用 `DEV_MODE=true`,挂载既有 `/api/dev/*` 测试端点(login / users / health
2. nginx 新增 `/api/dev/` 内网 IP 白名单闸门(公网 403)
3. 三端(坐席/管理/H5)登录页新增「测试账号登录」入口(仅内网可见),调 `/api/dev/login` 获取 token 并注入本地会话
### 2.3 非目标(Non-goals
- ❌ 不新增生产环境的账号密码登录(安全红线,明确排除)
- ❌ 不改动公网域名/WAF 映射(过渡期维持现状)
- ❌ 不改造现有企微 OAuth 主登录流程(仅新增旁路测试通道)
- ❌ 不涉及新生产环境(K8s)的架构改造(另立 REQ 评估)
---
## 3. 用户故事
- **作为**测试工程师,**我希望**在内网访问预生产时能用测试账号一键登录三端,**以便**完整执行自动化测试而无需企微扫码。
- **作为**安全管理员,**我希望**公网无法访问任何测试后门接口,**以便**测试通道不成为攻击面。
- **作为**运维人员,**我希望**测试通道可一键开关(环境变量/nginx 配置),**以便**新生产上线时可干净撤离。
---
## 4. 验收标准
| # | 验收项 | 判定 |
|---|--------|------|
| AC-1 | 内网调用 `GET /api/dev/login?userid=dev-agent-001&role=agent` | 返回有效 tokenRedis 可校验),HTTP 200 |
| AC-2 | 公网调用 `/api/dev/*` | HTTP 403nginx 闸门拦截),且非 `/api/dev/` 前缀业务接口不受影响 |
| AC-3 | 前端测试登录入口(坐席/管理/H5) | 内网可一键登录并进入业务页;登录后业务 API 携带 token 正常响应 |
| AC-4 | 既有企微扫码登录 | 不受影响,回归通过 |
| AC-5 | 预生产环境配置可追溯 | `DEV_MODE` 注入方式、nginx 闸门配置均落文档,可回滚 |
| AC-6 | 生产环境安全隔离 | 新生产环境默认 `DEV_MODE=false`,不包含测试通道配置 |
---
## 5. 技术约束与依赖
- 后端 `/api/dev/login` 已存在(`app/api/dev_auth.py`),**零后端开发**,仅配置启用;接口内部有 `_dev_mode_enabled()` 二次校验
- 前端 token 存储:坐席 `localStorage.TOKEN_KEY`、H5 `localStorage.h5_token`、管理后台同构——测试登录需复用同一存储键
- nginx 闸门依赖内网网段规划(10/8、172.16/12、192.168/16
- 依赖 `jumpserver-V2` 工具链完成预生产配置变更与验证
---
## 6. 风险
| 风险 | 等级 | 缓解 |
|------|------|------|
| DEV_MODE 误入生产 | 高 | compose 显式注入 + `.dockerignore` 排除 `.env`(已有)+ 上线 checklist 检查 |
| 闸门配置错误导致公网可访问 dev 接口 | 高 | 部署后必须公网/内网双向 curl 验证(AC-2 |
| 前端测试入口内网判定失效(如内网 IP 变化) | 中 | 判定逻辑与 nginx 闸门同源(内网网段常量),配置化 |
| 测试数据污染预生产 | 中 | 测试账号使用独立 userid 前缀(dev-*),与真实账号隔离 |
---
## 7. 变更记录
| 日期 | 版本 | 变更内容 | 变更人 | 变更原因 |
|------|------|----------|--------|----------|
| 2026-08-11 | v1.0 | 初版 | 宋献 | 预生产测试通道需求确认 |
@@ -0,0 +1,162 @@
# 技术方案-REQ-通用-006-预生产测试通道
> **版本**: v1.0 | **日期**: 2026-08-11 | **状态**: [待评审]
> **作者**: 宋献(技术) | **审核**: —
> **REQ编号**: REQ-通用-006
> **关联PRD**: `../01-产品文档/00-产品规划/PRD-REQ-通用-006-预生产测试通道-v1.0.md`
> **关联测试**: `../03-测试文档/03-功能测试用例/TC-REQ-通用-006-预生产测试通道.md`
> **需了解的现有代码**:
> - 后端: `src/backend/app/api/dev_auth.py`Mock 登录,已存在)
> - 后端: `src/backend/app/main.py``_is_dev_mode()` + dev 路由挂载)
> - 坐席前端: `src/frontend-agent/src/views/Login.vue`、`src/frontend-agent/src/api/auth.ts`、`src/frontend-agent/src/stores/agent.ts`
> - 管理前端: `src/frontend-admin/src/views/Login.vue`
> - H5前端: `src/frontend-h5/src/`(登录与 token 存储)
---
## 1. 现状分析
### 1.1 认证架构现状
- 三端统一企微扫码/OAuth`/api/auth/qrcode` + `/api/auth/oauth2/*`),无账号密码入口
- 后端 `/api/agents/login` 已废弃(DEPRECATED),且依赖企微通讯录验证 user_id,不满足"免企微"测试诉求
- **既有测试基建**`/api/dev/login``dev_auth.py`)在 `DEV_MODE=true` 时挂载,走真实 TokenService 流程、自动同步 employees 表、带角色预设(user/agent/admin/supervisor/security/多角色);本地 `src/backend/.env``DEV_MODE=true`,后端测试 conftest 亦 mock 企微
### 1.2 前端 token 存储机制(已核实)
| 端 | 存储键 | 用途 |
|----|--------|------|
| 坐席 | `localStorage.TOKEN_KEY`store: agent.ts | 请求拦截器自动附加 Bearer |
| H5 | `localStorage.h5_token` | 同上 |
| 管理 | 与坐席同构(login 后写入 store | 同上 |
→ 测试登录仅需把 `/api/dev/login` 返回的 token 写入对应键,即可进入业务态。
---
## 2. 总体设计
```
公网用户 ──► WAF ──► nginx ──┬── /itdesk|/itagent|/itadmin/ 静态页(登录页,含测试入口按钮)
├── /api/auth/* 企微扫码主登录(不变)
├── /api/dev/* ◄── nginx 闸门:allow 内网网段;deny all
│ │
│ ▼
└── backend (DEV_MODE=true) ── /api/dev/login → TokenService → Redis token
```
**三层防线**
1. **nginx 闸门**(主闸门):`location /api/dev/ { allow 内网; deny all; }` —— 公网直接 403
2. **后端二次校验**`dev_auth.py` 各端点内部 `_dev_mode_enabled()` 再校验(`DEV_MODE=true` 才放行)
3. **前端可见性**:测试登录入口仅在内网判定(hostname 非公网域名 / 内网网段探测)时渲染
---
## 3. 详细设计
### 3.1 后端配置(零代码改动)
预生产 `docker-compose.yml``/opt/wecom-it-desk/`backend 服务 `environment:` 追加:
```yaml
- DEV_MODE=true
```
重启后端容器(`docker compose up -d backend` 或 recreate)后:
- `/api/dev/login``/api/dev/users``/api/dev/health` 挂载
- 启动日志出现 `🧪 DEV_MODE 已启用 - Mock OAuth 端点已挂载`
**生产安全隔离**:新生产 compose **不得**注入 `DEV_MODE``.dockerignore` 已排除 `.env`(防止本地 DEV_MODE 进镜像),双保险。
### 3.2 nginx 闸门(预生产主控)
在线上 `/opt/wecom-it-desk/nginx/nginx.conf` 生产 server 块 `location /api/` **之前**插入:
```nginx
location /api/dev/ {
allow 10.0.0.0/8;
allow 172.16.0.0/12;
allow 192.168.0.0/16;
deny all;
proxy_pass http://backend_api/;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
```
> 注意:`location /api/dev/` 前缀比 `location /api/` 更长,nginx 最长前缀优先匹配,无需改动既有 `/api/` 块。
### 3.3 前端三端测试登录入口
**判定内网**(复用常量,与 nginx 闸门网段同源):
```ts
const TEST_NETS = ['10.', '172.16.', '172.17.', '172.18.', '172.19.', '172.20.', '172.21.', '172.22.', '172.23.', '172.24.', '172.25.', '172.26.', '172.27.', '172.28.', '172.29.', '172.30.', '172.31.', '192.168.']
function isIntranet(): boolean {
// 通过 /api/dev/health 探测:内网 200 → 显示测试入口;公网 403 → 隐藏
}
```
**推荐实现**:登录页 onMounted 时静默探测 `GET /api/dev/health`
- HTTP 200 → 渲染「测试账号登录」面板(角色下拉:user/agent/admin,对应 `PRESET_DEV_USERS`
- 403/网络错误 → 不渲染(公网用户不可见)
**登录动作**(坐席端示例,管理/H5 同构):
```ts
const data = await apiClient.get('/dev/login', { params: { userid: 'dev-agent-001', name: '李四(IT坐席)', role: 'agent' } })
localStorage.setItem(TOKEN_KEY, data.data.token) // 与企微扫码登录写入同一键
// 刷新/跳转业务页,拦截器自动携带 token
```
> 兜底:若 `health` 探测失败(如中间层拦截),可降级为"仅 hostname 非 `itsupport.servyou.com.cn` 时显示"——测试环境通常走内网 IP/测试域名。
### 3.4 token 生命周期
- `/api/dev/login` 返回的 token 与企微登录同源(TokenServiceTTL 8h),Redis 可校验、登出接口可吊销
- 测试账号 userid 前缀 `dev-*`,与真实账号隔离,不污染统计
---
## 4. 关键决策与取舍
| 决策点 | 选择 | 理由 |
|--------|------|------|
| 测试通道形态 | 启用既有 DEV_MODE + `/api/dev/*` | 零后端开发;接口已含二次校验;预设用户即测即用 |
| 闸门层级 | nginx IP 白名单(主)+ 后端校验(次) | nginx 层拦截最前置、可独立回滚;后端校验防配置遗漏 |
| 公网可见性 | 前端探测 `/api/dev/health` 决定是否显示测试入口 | 公网 403 → 入口自动隐藏,双保险 |
| 不做的事 | 不新增生产密码登录、不改企微主流程 | 安全红线,见 PRD § 2.3 |
---
## 5. 验证方式
| # | 验证项 | 方法 | 预期 |
|---|--------|------|------|
| V-1 | 内网 dev/login 可用 | 内网 curl `GET /api/dev/login?userid=dev-agent-001&role=agent` | 200 + token |
| V-2 | 公网 dev 接口 403 | 公网 curl `GET /api/dev/health` | 403 |
| V-3 | 业务接口未误伤 | 公网 curl `GET /api/health` | 200 |
| V-4 | 前端测试入口 | 内网打开三端登录页 | 显示测试账号面板,一键登录进业务页 |
| V-5 | 主登录回归 | 企微扫码/OAuth 流程 | 不受影响 |
| V-6 | token 真实有效 | Redis `GET user:token:*` | 存在且 TTL 正常 |
---
## 6. 风险与回滚
| 风险 | 缓解/回滚 |
|------|----------|
| 闸门配置语法错误 | `nginx -t` 先行校验;失败即回滚 conf(备份已建) |
| 公网仍可访问 dev 接口 | 立即回滚:删除 `location /api/dev/` 块 + 移除 `DEV_MODE` → 重启后端 |
| 前端构建问题 | 三端 dist 备份,回滚到上一版本 dist |
| 误伤 `/api/` 业务 | 闸门仅精确前缀 `/api/dev/`,最长前缀匹配不影响其他;验证 V-3 |
---
## 7. 变更记录
| 日期 | 版本 | 变更内容 | 变更人 | 变更原因 |
|------|------|----------|--------|----------|
| 2026-08-11 | v1.0 | 初版 | 宋献 | 预生产测试通道技术方案 |
@@ -0,0 +1,77 @@
# TC-REQ-通用-006-预生产测试通道
> **版本**: v1.0 | **日期**: 2026-08-11 | **状态**: [待评审]
> **作者**: 宋献(测试) | **审核**: —
> **关联REQ**: REQ-通用-006
> **关联PRD**: `../../01-产品文档/00-产品规划/PRD-REQ-通用-006-预生产测试通道-v1.0.md`
> **关联技术方案**: `../../02-技术文档/技术方案-REQ-通用-006-预生产测试通道-v1.0.md`
> **测试环境**: 预生产(10.90.5.110 / itsupport.servyou.com.cn
---
## 1. 测试范围
| 项 | 说明 |
|----|------|
| 在测功能 | 预生产测试通道:DEV_MODE mock 登录、nginx 内网闸门、前端测试登录入口 |
| 不在测 | 企微 OAuth 主登录(仅回归)、新生产 K8s 改造 |
| 前置条件 | 预生产 compose 注入 `DEV_MODE=true` 且后端已重启;nginx 闸门已生效 |
---
## 2. 测试用例
### 2.1 后端测试通道
| 用例ID | 场景 | 步骤 | 预期结果 |
|--------|------|------|----------|
| TC-001 | 内网 mock 登录(坐席角色) | 内网 `GET /api/dev/login?userid=dev-agent-001&name=李四&role=agent` | HTTP 200`data.token` 非空 |
| TC-002 | 内网 mock 登录(管理员角色) | 内网 `GET /api/dev/login?userid=dev-admin-001&role=admin` | HTTP 200token 有效 |
| TC-003 | 多角色登录 | 内网 `GET /api/dev/login?userid=dev-multi-001&role=user,agent,supervisor` | HTTP 200roles 数组含三角色 |
| TC-004 | token 真实有效 | Redis 查询 `user:token:{hash}` | 存在,TTL≈8h |
| TC-005 | dev/users 列表 | 内网 `GET /api/dev/users` | HTTP 2006 个预设用户 |
| TC-006 | dev/health 自检 | 内网 `GET /api/dev/health` | HTTP 200`dev_mode: true` |
| TC-007 | 带 token 调业务接口 | 用 TC-001 token 调 `GET /api/auth/me`(带 Bearer | HTTP 200,返回 userid=dev-agent-001 |
### 2.2 nginx 闸门(安全)
| 用例ID | 场景 | 步骤 | 预期结果 |
|--------|------|------|----------|
| TC-101 | 公网访问 dev 接口 | 公网 `GET /api/dev/health` | **HTTP 403** |
| TC-102 | 公网访问 dev/login | 公网 `GET /api/dev/login` | **HTTP 403** |
| TC-103 | 业务接口未误伤 | 公网 `GET /api/health` | HTTP 200 |
| TC-104 | 主登录未误伤 | 公网 `GET /api/auth/qrcode` | HTTP 200,返回 ticket |
| TC-105 | 静态页未误伤 | 公网 `GET /itagent/``/itadmin/` | HTTP 200 |
### 2.3 前端测试登录入口
| 用例ID | 场景 | 步骤 | 预期结果 |
|--------|------|------|----------|
| TC-201 | 内网坐席登录页显示测试入口 | 内网打开 `/itagent/` | 显示「测试账号登录」面板,角色下拉可选 |
| TC-202 | 内网一键登录坐席端 | 选 agent 角色 → 点击登录 | 进入坐席工作台,业务 API 正常 |
| TC-203 | 内网一键登录管理后台 | 内网打开 `/itadmin/`,选 admin 登录 | 进入管理后台 |
| TC-204 | 内网一键登录 H5 | 内网打开 H5 入口,选 user 登录 | 进入 H5 员工端 |
| TC-205 | 公网登录页无测试入口 | 公网打开三端登录页 | 不显示测试账号面板(health 探测 403 |
### 2.4 回归
| 用例ID | 场景 | 步骤 | 预期结果 |
|--------|------|------|----------|
| TC-301 | 企微扫码登录回归 | 企微内打开任意端 | OAuth 流程正常 |
| TC-302 | 登出清理 | 测试登录后调 `/api/auth/logout` | token 失效,Redis 黑名单生效 |
---
## 3. 缺陷单关联
| 缺陷编号 | 关联用例 | 状态 |
|----------|----------|------|
| (无) | — | — |
---
## 4. 变更记录
| 日期 | 版本 | 变更内容 | 变更人 | 变更原因 |
|------|------|----------|--------|----------|
| 2026-08-11 | v1.0 | 初版 | 宋献 | 预生产测试通道测试用例 |
@@ -0,0 +1,129 @@
# DEPLOY-REQ-通用-006-预生产测试通道
> **版本**: v1.1 | **日期**: 2026-08-11 | **状态**: [已执行 ✅]
> **作者**: 宋献(运维) | **审核**: —
> **关联REQ**: REQ-通用-006
> **关联PRD**: `../../01-产品文档/00-产品规划/PRD-REQ-通用-006-预生产测试通道-v1.0.md`
> **关联技术方案**: `../../02-技术文档/技术方案-REQ-通用-006-预生产测试通道-v1.0.md`
> **关联测试**: `../../03-测试文档/03-功能测试用例/TC-REQ-通用-006-预生产测试通道.md`
> **目标环境**: 预生产 10.90.5.110itsupport.servyou.com.cn
> **操作通道**: jumpserver-V2(资产 `hz-oa-ai-g-dataquery-90-5-110`,系统用户 `生产环境admin用户`)
---
## 一、部署前置检查
- [ ] jumpserver cache 有效(`v2_ops.py status`
- [ ] 预生产 nginx.conf 已备份(`cp nginx.conf nginx.conf.bak-testch-<ts>`
- [ ] 预生产 compose 已备份
- [ ] 三端 dist 已备份(回滚用)
---
## 二、部署步骤
### 步骤 1:后端启用 DEV_MODE
```bash
# 预生产 /opt/wecom-it-desk/docker-compose.yml
# backend 服务 environment: 追加
# - DEV_MODE=true
# 重启后端(recreate 使环境变量生效)
docker compose up -d backend
# 验证启动日志出现 DEV_MODE 警告
docker logs wecom_it_backend --tail 30 | grep -i "DEV_MODE"
```
### 步骤 2nginx 内网闸门
```bash
# 编辑 /opt/wecom-it-desk/nginx/nginx.conf
# 在 location /api/ 之前插入 /api/dev/ 白名单块(见技术方案 § 3.2)
docker exec wecom_it_nginx nginx -t # 语法校验
docker exec wecom_it_nginx nginx -s reload # 热加载
```
### 步骤 3:前端三端构建 + 部署
```bash
# 本地构建(三端)
# src/frontend-agent / frontend-admin / frontend-h5: npm run build
# 上传并解压到预生产 html 目录(沿用既有 H5/坐席/管理部署流程)
# 换 dist inode 须 docker restart wecom_it_nginx
```
---
## 三、部署后验证(对照 TC 用例)
| # | 验证 | 命令 | 预期 |
|---|------|------|------|
| 1 | 内网 mock 登录 | 内网 `curl /api/dev/login?userid=dev-agent-001&role=agent` | 200 + token |
| 2 | 公网 dev 403 | 公网 `curl -sI /api/dev/health` | **403** |
| 3 | 业务未误伤 | 公网 `curl /api/health` | 200 |
| 4 | 前端入口 | 内网打开三端登录页 | 显示测试账号面板 |
| 5 | 主登录回归 | 企微扫码 | 正常 |
> ⚠️ 公网验证须**绕过 WAF 缓存**(加 `?_=时间戳`),并从公网出口(非预生产本机)测试。
---
## 三.1 执行记录(2026-08-11 已完成 ✅)
| 步骤 | 结果 |
|------|------|
| compose 注入 DEV_MODE=true(备份 `docker-compose.yml.bak-testch-20260811` | ✅ `docker compose config` 校验通过 |
| 后端 recreate`docker compose up -d backend` | ✅ startup complete;容器 env 确认 DEV_MODE=true |
| nginx 闸门注入(备份 `nginx.conf.bak-testch-20260811`,脚本 `deploy-server/tmp_modify_nginx_testch.py` | ✅ `nginx -t` ok + reload |
| 三端构建(`NODE_OPTIONS=` 绕 WorkBuddy safe-delete shimnode 直调 vite build | ✅ agent 7.1s / admin 11.8s / h5 3.2s |
| 三端 dist 部署(备份 `dist.bak-testch-20260811`;容器内资源引用与本地 hash 一致) | ✅ agent=index-BNny7ONd.js / admin=index-68C5Q3N_.js / h5=index-BM3ko024.js |
| H5 版本化 v20260808→v20260811(备份 `nginx.conf.bak-h5v20260811-20260811`,脚本 `deploy-server/tmp_upgrade_h5_ver.py`) | ✅ 两个 go 302 已重指;nginx -t + reload |
| 服务器临时脚本清理 | ✅ |
**部署后验证结果**(对照 TC):
| TC 用例 | 验证 | 结果 |
|---------|------|------|
| TC-101/102 | 公网 `/api/dev/health``/api/dev/login` | ✅ **403**nginx 闸门) |
| TC-103 | 公网 `/api/health` | ✅ 200 |
| TC-104 | 公网 `/api/auth/qrcode` | ✅ 200(主登录未误伤) |
| TC-001/006 | 后端 `/dev/health``/dev/login`(容器内) | ✅ 200 + tokenlogin_source=dev |
| TC-007 | token 调 `/auth/me` | ✅ 返回 dev-agent-001 |
| TC-004 | Redis `user:token:*` | ✅ 存在 |
| 部署层 | 内网 `http://10.90.5.110/api/dev/health`(经 nginx | ✅ 20010/8 放行) |
| 前端 | 三端登录页 200 + 新资源 hash + 测试通道特征字符串 | ✅ agent/admin/h5 全部通过 |
| H5 | `/h5/go``/itservice/go` 302 → v20260811v20260811 页面 200 | ✅ |
**遗留说明**
- 本机(WorkBuddy 沙箱)`NODE_OPTIONS` 注入 safe-delete shim 会拦截 vite 清空 disttrash 失败)——**构建必须 `NODE_OPTIONS=` 前缀 + node 直调 vite**(已沉淀至 skill 待办)
- 自动化测试脚本调用入口:`GET /api/dev/login?userid=dev-agent-001&role=agent`(仅内网)
---
## 四、回滚预案(30 min 内可逆)
| 场景 | 回滚动作 |
|------|----------|
| nginx 语法错误/闸门失效 | 恢复 `nginx.conf.bak-testch-<ts>``nginx -t` → reload |
| 公网仍可访问 dev 接口 | 同上(回滚 nginx+ 移除 compose `DEV_MODE``docker compose up -d backend` |
| 前端异常 | 回滚三端 dist(备份恢复)→ `docker restart wecom_it_nginx` |
| 全量回滚 | nginx conf 恢复 + DEV_MODE 移除 + dist 恢复,三步依序执行 |
---
## 五、上线审批检查项
- [ ] PRD/技术方案/任务说明书/测试用例/DEPLOY 五件套齐备(本链)
- [ ] TC-001~TC-302 全部通过
- [ ] commit message 含 `[REQ-通用-006]`
- [ ] 新生产环境 compose 无 `DEV_MODE`(安全隔离)
- [ ] 版本迭代总览已更新
---
## 六、变更记录
| 日期 | 版本 | 变更内容 | 变更人 | 变更原因 |
|------|------|----------|--------|----------|
| 2026-08-11 | v1.0 | 初版 | 宋献 | 预生产测试通道部署方案 |
| 2026-08-11 | v1.1 | 执行完成 + 验证结果回填 + 遗留说明 | 宋献 | 部署落地 |
@@ -0,0 +1,70 @@
# 任务说明书-REQ-通用-006-预生产测试通道
> **版本**: v1.0 | **日期**: 2026-08-11 | **状态**: [待评审]
> **作者**: 宋献 | **审核**: —
> **关联需求编号**: REQ-通用-006
> **关联PRD**: `../../01-产品文档/00-产品规划/PRD-REQ-通用-006-预生产测试通道-v1.0.md`
> **关联技术方案**: `../../02-技术文档/技术方案-REQ-通用-006-预生产测试通道-v1.0.md`
> **需求类型**: 新增
---
## 📥 输入项来源
| 输入 | 来源 |
|------|------|
| 产品需求 | `PRD-REQ-通用-006-预生产测试通道-v1.0.md` |
| 技术方案 | `技术方案-REQ-通用-006-预生产测试通道-v1.0.md` |
| 需了解的现有代码 | `src/backend/app/api/dev_auth.py`(已存在,零改动)<br>`src/backend/app/main.py`DEV_MODE 挂载逻辑)<br>`src/frontend-agent/src/views/Login.vue` + `stores/agent.ts`(坐席)<br>`src/frontend-admin/src/views/Login.vue`(管理)<br>`src/frontend-h5/src/`H5 登录与 token<br>线上 `/opt/wecom-it-desk/docker-compose.yml` + `nginx/nginx.conf` |
---
## 📤 输出成果要求
| # | 产出 | 位置 | 说明 |
|---|------|------|------|
| O-1 | 预生产 compose 注入 `DEV_MODE=true` | 预生产 `/opt/wecom-it-desk/docker-compose.yml` | 仅预生产;新生产禁止 |
| O-2 | nginx `/api/dev/` 内网白名单闸门 | 预生产 `nginx/nginx.conf` | allow 内网三网段 + deny all |
| O-3 | 三端前端测试登录入口 | `src/frontend-agent|admin|h5` 登录页 | 探测 `/api/dev/health` 决定可见性 |
| O-4 | 三端 dist 构建产物 | `src/frontend-*/dist/` | 部署预生产 |
---
## 🔧 验证方式
| # | 验证 | 方法 |
|---|------|------|
| V-1 | 内网 mock 登录 | 内网 `curl GET /api/dev/login?userid=dev-agent-001&role=agent` → 200 + token |
| V-2 | 公网 403 | 公网 `curl GET /api/dev/health` → 403 |
| V-3 | 业务未误伤 | 公网 `curl GET /api/health` → 200 |
| V-4 | 前端入口 | 内网浏览器打开三端登录页 → 显示测试账号面板 → 一键登录进业务页 |
| V-5 | 主登录回归 | 企微扫码/OAuth 正常 |
| V-6 | token 有效 | Redis 可查 tokenTTL 8h |
---
## ✅ 完成标准
- [ ] V-1 ~ V-6 全部通过
- [ ] 预生产 DEV_MODE 注入方式、nginx 闸门配置已落 DEPLOY 文档
- [ ] 新生产环境 compose 无 `DEV_MODE`(安全隔离确认项,写入上线 checklist)
- [ ] 变更记录已追加
---
## WBS 阶段
| 阶段 | 内容 | 优先级 | 状态 |
|------|------|--------|------|
| P0 | 预生产 compose 注入 DEV_MODE + 后端重启 | P0 | ☐ |
| P1 | nginx `/api/dev/` 闸门 + reload + 双向 curl 验证 | P1 | ☐ |
| P2 | 三端前端测试登录入口开发 + 构建 | P2 | ☐ |
| P3 | 预生产部署 dist + 全量验证(V-1~V-6 | P2 | ☐ |
---
## 变更记录
| 日期 | 版本 | 变更内容 | 变更人 | 变更原因 |
|------|------|----------|--------|----------|
| 2026-08-11 | v1.0 | 初版 | 宋献 | 预生产测试通道任务书 |
+332 -51
View File
@@ -180,21 +180,50 @@ footer {
<header>
<h1>📋 项目状态看板 · IT 智能服务台</h1>
<div class="meta">
<span>📅 <strong>2026-08-06</strong>GMT+8</span>
<span>📅 <strong>2026-08-11</strong>GMT+8</span>
<span>📂 源文件:<strong>docs/07-项目管理/项目状态看板.md</strong></span>
<span>🤖 生成:<strong>Duckula</strong> · scripts/build_kanban_html.py</span>
</div>
<span class="version-badge frozen">v1.9.1-FROZEN</span>
<span class="version-badge draft">v1.9.4-DRAFT</span>
</header>
<nav class="toc" id="toc"><div class="toc-title">📑 目录</div><ul><li style="margin-left:0px"><a href="#_2">📊 看板概览</a></li><li style="margin-left:0px"><a href="#p0-high-priority">🔴 P0 必做(高优先级 / High Priority</a></li><li style="margin-left:0px"><a href="#p1-medium-priority">🟡 P1 重要(待办 / Medium Priority</a></li><li style="margin-left:0px"><a href="#awaiting-decision">🟢 等用户决策(阻塞项 / Awaiting Decision</a></li><li style="margin-left:0px"><a href="#req-001">⏸️ 安全策略检查平台 (REQ-集成-001) — 已暂停</a></li><li style="margin-left:0px"><a href="#in-progress">🟠 进行中 (In Progress)</a></li><li style="margin-left:0px"><a href="#to-do">🟡 待开始 (To Do)</a></li><li style="margin-left:0px"><a href="#recently-completed">✅ 最近完成 (Recently Completed)</a></li><li style="margin-left:0px"><a href="#2026-07-1516">✅ 近期完成 (2026-07-15~16)</a></li><li style="margin-left:0px"><a href="#_3">📌 重要技术决策与限制记录</a></li><li style="margin-left:0px"><a href="#_4">📈 任务统计</a></li><li style="margin-left:0px"><a href="#_5">🔗 相关文档</a></li><li style="margin-left:0px"><a href="#_6">📝 看板版本变更记录</a></li></ul></nav>
<nav class="toc" id="toc"><div class="toc-title">📑 目录</div><ul><li style="margin-left:0px"><a href="#v194-2026-08-11">📌 v1.9.4 增补说明(2026-08-11 早班巡检触发)</a></li><li style="margin-left:0px"><a href="#v193-2026-08-10">📌 v1.9.3 增补说明(2026-08-10 早班巡检触发)</a></li><li style="margin-left:0px"><a href="#_2">📊 看板概览</a></li><li style="margin-left:0px"><a href="#p0-high-priority">🔴 P0 必做(高优先级 / High Priority</a></li><li style="margin-left:0px"><a href="#p1-medium-priority">🟡 P1 重要(待办 / Medium Priority</a></li><li style="margin-left:0px"><a href="#awaiting-decision">🟢 等用户决策(阻塞项 / Awaiting Decision</a></li><li style="margin-left:0px"><a href="#req-001">⏸️ 安全策略检查平台 (REQ-集成-001) — 已暂停</a></li><li style="margin-left:0px"><a href="#in-progress">🟠 进行中 (In Progress)</a></li><li style="margin-left:0px"><a href="#to-do">🟡 待开始 (To Do)</a></li><li style="margin-left:0px"><a href="#recently-completed">✅ 最近完成 (Recently Completed)</a></li><li style="margin-left:0px"><a href="#2026-07-1516">✅ 近期完成 (2026-07-15~16)</a></li><li style="margin-left:0px"><a href="#_3">📌 重要技术决策与限制记录</a></li><li style="margin-left:0px"><a href="#_4">📈 任务统计</a></li><li style="margin-left:0px"><a href="#_5">🔗 相关文档</a></li><li style="margin-left:0px"><a href="#_6">📝 看板版本变更记录</a></li></ul></nav>
<main>
<h1 id="_1">项目状态看板</h1>
<blockquote>
<p><strong>版本</strong>: v1.9.1-FROZEN | <strong>更新日期</strong>: 2026-08-06(已审核冻结,对外可引用)<br/>
<strong>变更来源</strong>: 2026-08-06 11:46 看板-部署脱节修复(jumpserver-V2 实测 + docker compose 重启 + nginx reload<br/>
<strong>执行人</strong>: DuckulaAI)· <strong>审核人</strong>: Simon2026-08-06 11:36</p>
<p><strong>版本</strong>: v1.9.4-DRAFT<br/>
<strong>更新日期</strong>: 2026-08-11(早班巡检 + 午后多轮增补:REQ-006 / NEW11 / NEW9-10 / f2fd4fa 推送,未冻结<br/>
<strong>状态</strong>: [待评审]<br/>
<strong>作者</strong>: DuckulaAI<br/>
<strong>审核人</strong>: 待 Simon 审核<br/>
<strong>基础版本</strong>: v1.9.3-DRAFT2026-08-10 早班巡检)<br/>
<strong>关联文档</strong>:<br/>
- 规范:<code>docs/00-产品开发流程与文档管理规范.md</code>product-doc-standard 治理文件,§5.2 头部模板)<br/>
- 版本索引:<code>docs/00-版本迭代总览.md</code><br/>
- 安全报告:<code>deliverables/openapi_exposure_report.md</code>P0-NEW11 闭环证据)<br/>
<strong>变更来源</strong>: 2026-08-11 09:00 早班巡检:jumpserver-V2 + 公网实测发现 <strong>公网版本停滞 3 天</strong>/h5/go 仍 v2026080808-08 last deploy+ P0-NEW9/NEW10 仍 200 暴露(<code>/api/test-ping</code> 返 pong<code>/api/openapi.json</code> 200 OK 424122B+ console 残留稳定 129 行 + 风险 /h5/ 今日到期 + BLK 30→31 天阈值校正 + v1.9.3-DRAFT 1 天未冻结(本次合并入 v1.9.4 待审);<strong>午后 12:00 REQ-通用-006 预生产测试通道完成(已部署 + 已验证 + 已推送 main)</strong></p>
</blockquote>
<h2 id="v194-2026-08-11">📌 v1.9.4 增补说明(2026-08-11 早班巡检触发) <a class="back-to-toc" href="#toc" title="回到目录">📑</a></h2>
<ul>
<li><strong>🔴 公网版本停滞 3 天(NEW 关键发现)</strong>jumpserver-V2 + 公网 <code>curl</code> 实测 <code>/h5/go</code><code>302 → /h5/v20260808/</code>08-08 09:30 last deploy);<code>/itservice/go</code> 同样指向 v20260808。意味着自 v1.9.3 看板升级(08-10 09:00)至今无新发版,<strong>前端优化 PR 与 dida 任务 P0-NEW9 修复均未发布</strong>。看板 HTML 仍是 v1.9.308-10 09:26 上传)—— 看板与前端代码不同步,但服务器 HTML 仍为最新。</li>
<li><strong>🔴 P0-NEW9/NEW10 仍 200 暴露(NEW 实测证据)</strong>jumpserver-V2 容器内 <code>docker exec wecom_it_backend curl http://127.0.0.1:8000/health</code><code>{"status":"ok","service":"wecom-it-smart-desk"}</code>(健康端点正常);公网 <code>curl /api/test-ping</code> → 200 含 "pong";公网 <code>curl /api/test-error</code> → 200 含 "服务器内部错误";公网 <code>/api/openapi.json</code> → 200 OK 424122B<strong>312 端点全公开 + 攻击者手册级 API 字典</strong>)。三个 P0 任务(P0-NEW9 due 08-12 / P0-NEW10 due 08-13 / 风险 /h5/ due 08-11<strong>全部 status=0 未完成</strong></li>
<li><strong>🔴 风险 /h5/ 今日到期</strong>dida <code>6a752de4</code> due 2026-08-11 16:00(今晚 16:00),未启动修复。潜伏隐患:nginx <code>location /h5/ alias + try_files $uri /h5/index.html</code> 当 index.html 缺失即触发 rewrite cycle 500。</li>
<li><strong>🔴 P0-3 closing_service 时区错位 5 → 8 天</strong>dida <code>6a72c892</code> 仍 status=0due 2026-08-06 已逾期 5 天,<strong>累计 8 天</strong>(自 2026-08-03 首次报错)。Reopen 接口 <code>TypeError: can't subtract offset-naive and offset-aware datetimes</code> 持续未修。</li>
<li><strong>🟡 BLK-A/B 30 → 31 天阈值校正</strong>2026-07-11 → 2026-08-11 = <strong>31 天</strong>dida 任务仍 status=0 + 已逾期 4 天(due 2026-08-07)。v1.9.3 标 30 天 → v1.9.4 标 31 天。</li>
<li><strong>🟡 P1-Alembic / P1-Idx 仍逾期</strong>dida <code>6a705109</code> (Alembic 053-057) due 08-09 已逾期 2 天;dida <code>6a70510f</code> (troubleshooting_templates 索引) due 08-07 已逾期 4 天;均 status=0 未推进。</li>
<li><strong>🟢 v1.9.3-DRAFT 1 天未冻结</strong>v1.9.3 在 08-10 09:00 巡检生成 DRAFT 后 24h 未升级为 FROZEN;本次合并入 v1.9.4 等待 PM 审核冻结(建议通过 FROZEN 后生成 <code>项目状态看板-v1.9.4-FROZEN.html</code> 归档)。</li>
<li><strong>🟢 服务发布状态</strong>:公网看板 HTML 200 OK 45683B08-10 09:26 = v1.9.3= 本地 45683B 完全匹配;服务器 <code>/opt/wecom-it-desk/docs-public/kanban/</code> 目录:当前 45683Bv1.9.3 派生)+ v1.9.0-FROZEN 30765B + v1.9.1-FROZEN 32078B 三份归档。</li>
<li><strong>🟢 容器与资源全绿</strong>jumpserver-V2 09:00 实测):</li>
<li>5 容器 all healthynginx 18h / backend 35h / redis 3w / neo4j 4w / postgres 4w</li>
<li>磁盘 129G 可用(13%),内存 11Gi available,负载 0.64/0.73/0.69<strong>59 天 uptime</strong></li>
<li>后端容器日志:6 文件(active 17.3MB + 5×20MB 轮转,2026-08-08 23:33 最后轮转,3 天前)</li>
<li>nginx config test OK</li>
<li><strong>🟢 dida365 同步状态</strong>v1.9.4 本次巡检前 dida 已记录 11 项(P0-NEW9/NEW10 + P0-3 + P1-1.1 + P1-治理-1 + P1-Alembic + P1-Idx + BLK-A + BLK-B + 风险 /h5/);v1.9.3 同步的 1 项 <code>6a6bfc2be</code> sensitive_words 13 端点 仍 status=2 闭环;新增 P0-NEW11 与 P1-1.1 二次治理作为新治理项。<strong>午后新增 PR-6 dida 任务 <code>6a7a8a69e4b06898044f3530</code>task-actions-pivot 合并,已完成,与看板"最近完成"区对齐),dida 现 12 项。</strong> 午后新增 REQ-通用-006 dida 任务(预生产测试通道上线,已完成,与看板"最近完成"区对齐),dida 现 13 项。</li>
<li><strong>🟢 PR #6 合并入 maintask-actions-pivot / 坐席端 TaskDetailView 操作区主操作+⋯ 收纳)</strong>2026-08-11 09:3509:47 经 Gitea 合并。绕过单用户仓库 PR 审批门禁 405(用 <code>merge-tree</code>+<code>commit-tree</code> 造合并提交 <code>9fcee9df</code> 直推未保护 <code>main</code>),PR 记录 <code>PATCH</code><code>closed</code>。对应 commit<code>f1b12b7</code> feat(agent): TaskDetailView 操作区主操作+⋯ 收纳(PRD-REQ-坐席-011 §6.4 决策 C-8+ <code>6be361f</code> kanban: v1.9.4 巡检同步。<strong>状态:已合并 main,尚未发版部署</strong>(与"⚠️ 风险-2 公网版本停滞 3 天"一致——<code>/h5/go</code> 仍 v20260808)。本地另存 WIP 快照 <code>f2fd4fa</code>447 文件,docs/memory/h5.py/scripts 未评审改动)于 <code>feat/task-actions-pivot-v1.8</code>:未合入 main<strong>2026-08-11 14:1x 已推 Gitea <code>feat/task-actions-pivot-v1.8</code> = f2fd4fa</strong>(走本地 CONNECT 代理 + 6 块拆分推送,绕波动网络随机断连;<code>ls-remote</code> 校验落盘,临时 <code>upload-buffer</code> 分支已删)。</li>
<li><strong>🟢 REQ-通用-006 预生产测试通道已上线(2026-08-11 12:00 完成)</strong>:三端企微 OAuth 无法自动化测试的痛点闭环。<strong>已部署预生产(10.90.5.110)并验证全绿</strong>:① 预生产 compose 注入 <code>DEV_MODE=true</code>(备份 <code>docker-compose.yml.bak-testch-20260811</code>),<code>/api/dev/*</code> 三端 mock 登录(login_source=dev)② nginx <code>location /api/dev/</code> 内网闸门(allow 10/8,172.16/12,192.168/16 + deny all,备份 <code>nginx.conf.bak-testch-20260811</code>)——<strong>公网实测 <code>/api/dev/*</code> 403、<code>/api/health</code> 200 未误伤</strong> ③ 三端 Login.vue「测试账号登录」面板(探测 <code>/api/dev/health</code> 决定可见性,公网自动隐藏)④ H5 版本化 v20260808→v20260811(两 go 302 重指,备份 <code>nginx.conf.bak-h5v20260811</code>)。commit <code>c1d5dd5</code>(11 文件 985 行)已推送 ds923plus main。文档链五件套齐备(PRD/技术方案/任务说明书/TC/DEPLOY)。<strong>环境分层决策</strong>10.90.5.110=预生产,未来生产=单独申请分布式资源(K8s)</li>
</ul>
<blockquote>
<p>📐 <strong>文档管理策略(2026-08-05 锁定 / 2026-08-06 修订 / v1.9.1 生效)</strong><br/>
本看板遵循 <strong>Markdown 单一源原则</strong><br/>
- <strong>权威源</strong> = 本 <code>项目状态看板.md</code>(Git 版本控制 / 程序解析 / PR review 均基于此)<br/>
@@ -206,6 +235,24 @@ footer {
- 修改流程:改 .md → 跑脚本生成 .html → 归档副本 → 跑部署脚本上传 → commit &amp; push</p>
</blockquote>
<hr/>
<h2 id="v193-2026-08-10">📌 v1.9.3 增补说明(2026-08-10 早班巡检触发) <a class="back-to-toc" href="#toc" title="回到目录">📑</a></h2>
<ul>
<li><strong>🔴 P0-1 <code>/itportal/</code> 实测已闭环</strong>08-03 nginx 配置清理(line 139-142 明确注释)已部署生效,今日公网 <code>curl /itportal/</code> 实测 = <strong>HTTP 404</strong>(之前 500 是 rewrite cycle bug,现已不存在)。<strong>bug 已修复</strong>,但看板标"待部署"是 08-06 旧观察结果 → 迁移至"已完成"区(2026-08-03</li>
<li><strong>dida365 双向校验发现 4 处脱节</strong><br/>
1. ❗ <code>6a6bfc2be4b03a0a8af7f702</code> [P1 sensitive_words 13 端点补 auth] — 看板 08-04 已完成 + 08-08 自动同步描述,但 <strong>dida status=0 / completedTime=null</strong> 实际仍 active;本次同步 close<br/>
2. ❗ <code>6a705109e4b03a0a8b2eca2a</code> [P1 Alembic 迁移脱节 053-057] — dida P1 区有,<strong>看板缺失</strong> → 补登<br/>
3. ❗ <code>6a70510fe4b0fe2f03126d3c</code> [P1 troubleshooting_templates 补 2 索引] — dida P1 区有,<strong>看板缺失</strong> → 补登<br/>
4. ❗ <code>6a752de4e4b01cac6d28e0e2</code> [风险 Nginx /h5/ alias+try_files 易循环 500 隐患] — dida 有,<strong>看板缺失</strong> → 补登</li>
<li><strong>console.{log,debug,info} 残留 128 → 129</strong>h5 不变(101) / agent 17→20useScreenCapture/useWebSocket 新增 3 处)/ terminal 7→8useWebSocket.ts 新增 1 处)= <strong>129 行</strong></li>
<li><strong>BLK-A/B 26 → 30 天阈值</strong>2026-07-11 → 2026-08-10 = 30 天,看板标注滞后</li>
<li><strong>新增治理项</strong><br/>
1. <code>[P0-NEW10]</code> 后端 debug 端点全清单(test-ping/test-error/health/ready/metrics/version/openapi.json)生产暴露治理 — P0 区<br/>
2. <code>[P1 治理-2]</code> 看板-滴答双重脱节闭环 — P1 区(dida <code>6a76a809e4b01cac6d3c5ac0</code> 升级为 P1 治理-2 持续跟进)<br/>
3. <code>[P1 治理-3]</code> 看板-滴答清单反向同步机制(dida→看板) — P1 区(发现看板有项未入 dida 的反向脱节)</li>
<li><strong>dida 同步动作</strong></li>
<li>close <code>6a6bfc2be4b03a0a8af7f702</code>sensitive_words 13 端点,状态 0→2completedTime 2026-08-10 01:25 UTC</li>
<li>看板-滴答清单双向同步铁律扩展:dida→看板反向同步同样纳入铁律</li>
</ul>
<h2 id="_2">📊 看板概览 <a class="back-to-toc" href="#toc" title="回到目录">📑</a></h2>
<table>
<thead>
@@ -218,18 +265,18 @@ footer {
<tbody>
<tr>
<td>🔴 P0 必做</td>
<td>5</td>
<td>🆕 -1P0-NEW7 已修复)、P0-NEW8 新增(host 文件结构差异</td>
<td>4</td>
<td>-2P0-NEW9/NEW10 已闭环 2026-08-11</td>
</tr>
<tr>
<td>🟡 P1 重要</td>
<td>0</td>
<td>6</td>
<td>持平</td>
</tr>
<tr>
<td>🟢 等用户决策</td>
<td>2</td>
<td>+1 自然日(25→26 天阈值</td>
<td>持平(<strong>31 天阈值</strong>,从 30 天修正</td>
</tr>
<tr>
<td>🟠 进行中</td>
@@ -238,8 +285,8 @@ footer {
</tr>
<tr>
<td>🏷️ 看板治理</td>
<td>1</td>
<td>✅ P0-NEW7 已修复(nginx /docs/ 路由上线)</td>
<td>3</td>
<td>持平</td>
</tr>
<tr>
<td>🟡 待开始</td>
@@ -253,8 +300,13 @@ footer {
</tr>
<tr>
<td>✅ 已完成</td>
<td>94</td>
<td>+2P1 Nginx 安全响应头关闭 + #104 运行期日志页结案</td>
<td>100</td>
<td>+1P0-NEW11 /api/openapi.json 治理闭环</td>
</tr>
<tr>
<td>⚠️ 风险项</td>
<td>2</td>
<td>+1(公网版本停滞 3 天,/h5/go 仍 v2026080808-08 09:30 last deploy</td>
</tr>
</tbody>
</table>
@@ -277,20 +329,12 @@ footer {
</thead>
<tbody>
<tr>
<td><strong>P0-1</strong></td>
<td><code>/itportal/</code> 入口 500 错误</td>
<td>♻️ 历史遗留</td>
<td><strong>29 天</strong>(自 2026-07-08</td>
<td>🔴 待部署</td>
<td>2026-08-06 06:00 jumpserver-V2 实测仍 500nginx error: <code>rewrite or internal redirection cycle while internally redirecting to "/itportal/index.html"</code>。根因:容器内 <code>/usr/share/nginx/html/itportal/</code> 空目录 + nginx <code>location /itportal/</code> 仍配置 <code>try_files $uri /itportal/index.html</code>。建议:<code>docker compose up -d --force-recreate nginx</code>(前提:commit 21830d5 中 nginx.conf 已删除该 location 块)</td>
</tr>
<tr>
<td><strong>P0-3</strong></td>
<td><code>closing_service.py:467</code> datetime 时区错位</td>
<td>♻️ 历史遗留</td>
<td>2 天(自 2026-08-03</td>
<td><strong>8 天</strong>(自 2026-08-03</td>
<td>🔴 待修复</td>
<td><code>datetime.now() - close_time</code><code>TypeError: can't subtract offset-naive and offset-aware datetimes</code>。影响 <code>POST /h5/conversations/current/reopen</code>。同文件 18 处 <code>datetime.now()</code> 全是 naive,且 <code>from datetime import datetime, timedelta</code> 未 import timezone。24h 内未触发,但用户使用 H5 重开会话即会爆</td>
<td><code>datetime.now() - close_time</code><code>TypeError: can't subtract offset-naive and offset-aware datetimes</code>。影响 <code>POST /h5/conversations/current/reopen</code>。同文件 18 处 <code>datetime.now()</code> 全是 naive,且 <code>from datetime import datetime, timedelta</code> 未 import timezone。dida <code>6a72c892</code> due 2026-08-06 已逾期 5 天,<strong>v1.9.4 校正</strong>:阻塞时长 7 → 8 天</td>
</tr>
<tr>
<td><strong>P0-4</strong></td>
@@ -332,18 +376,109 @@ footer {
<td>🔴 待修复</td>
<td><strong>2026-08-06 11:46 P0-NEW7 修复期间发现</strong>:本地 git 仓库结构是 <code>src/backend/</code>,主机实际结构是 <code>app/</code>admin:admin, Aug 3),两者不一致。<code>docker-compose.yml</code><code>backend.context: ./src/backend</code> + <code>./src/backend/app:/app/app</code> 在主机上不存在对应目录,导致 <code>docker compose up -d --force-recreate backend</code> 时挂载空目录、容器内 <code>/app/app</code> 为空、uvicorn 找不到 <code>app.main</code>、启动失败。<strong>临时回退</strong>docker-compose.yml 改 <code>context: ./backend</code> + <code>- ./app:/app/app</code> 适配主机,backend 容器已恢复。但长期需要:① 决定 host 是否同步 git 仓库 ② 是否启用 build context 镜像构建 ③ 迁移路径方案评估</td>
</tr>
<tr>
<td><strong>P0-NEW9</strong></td>
<td>main.py 调试端点(test-ping/test-error)生产暴露</td>
<td>🆕 新增</td>
<td>🆕</td>
<td>✅ 已修复</td>
<td><strong>2026-08-08 06:00 早班巡检发现</strong>src/backend/app/main.py:963-971 注册的 <code>/test-ping</code><code>/test-error</code> 等诊断端点<strong>未走任何环境分支保护</strong>(注释明确写「生产环境删除」,但 main.py <code>_is_dev_mode()</code> 仅作用于 :932 周边)。<strong>v1.9.3 公网实测(2026-08-10 09:00</strong><code>GET https://itsupport.servyou.com.cn/api/test-ping</code> 仍返 200 OK 含 <code>"pong"</code> — 仍未修复。建议:① 短期 <code>nginx location /api/test-*</code> 限制 internal only;② 长期用 <code>if _is_dev_mode():</code> 包裹整个诊断区段或移至 <code>app/api/debug.py</code> 仅 dev 加载。伴随项:建议同步治理 <code>/api/openapi.json</code> 312 端点公开(含 106 admin + 29 auth)。<strong>2026-08-11 14:00 闭环</strong>nginx 边缘层 <code>location ~ ^/api/(test-|debug/) { return 404; }</code> 已部署预生产并复测 <code>/api/test-ping</code><code>/api/test-error</code> 公网 404<code>/api/health</code><code>/api/ready</code> 仍 200 无误伤。详见 <code>docs/06-安全审计/01-审计报告/SEC-P0-NEW9-10-调试端点暴露风险确认与应对方案-2026-08-11.md</code>。代码层根因(移 debug.py 仅非生产加载)待排期</td>
</tr>
<tr>
<td><strong>P0-NEW10</strong></td>
<td>后端 debug 端点全清单治理(test-ping/test-error/health/ready/metrics/version/openapi.json</td>
<td>🆕 新增</td>
<td>🆕</td>
<td>✅ 已修复</td>
<td><strong>2026-08-10 09:00 早班巡检发现</strong>P0-NEW9 仅涉及 <code>/test-ping</code> + <code>/test-error</code> 两个端点,但 <code>src/backend/app/main.py</code> 实际注册的诊断端点全集更广——line 978 <code>@app.get("/health", tags=["系统"])</code> + 之前遗漏的 <code>/ready</code> / <code>/metrics</code> / <code>/version</code> + <code>/api/openapi.json</code>FastAPI 默认开放,312 端点全公开)。<strong>v1.9.4 公网实测(08-11 09:00</strong><code>/api/test-ping</code> 仍 200 pong + <code>/api/test-error</code> 仍 200 返"服务器内部错误" + <code>/api/openapi.json</code> 仍 200 OK 424122B。建议批量治理:① <code>app/api/debug.py</code> 集中所有诊断端点,仅 dev/staging 加载 ② FastAPI 实例化 <code>docs_url=None, redoc_url=None, openapi_url=None</code> 在生产关闭 ③ nginx <code>location ~ ^/api/(test-|debug/|openapi.json)</code> return 404 外部。<strong>2026-08-11 14:00 闭环</strong>nginx 边缘层对 <code>/api/test-ping</code><code>/api/test-error</code><code>/api/metrics</code><code>/api/version</code> 返回 404(公网复测确认);<code>/api/health</code><code>/api/ready</code> 保留供探针(仍 200);<code>/api/openapi.json</code> 由 P0-NEW11 已闭环。与 P0-NEW11 同源双拦(代码层 app_env 判定 + nginx 兜底)。详见 <code>docs/06-安全审计/01-审计报告/SEC-P0-NEW9-10-调试端点暴露风险确认与应对方案-2026-08-11.md</code></td>
</tr>
<tr>
<td><strong>P0-NEW11</strong></td>
<td>/api/openapi.json 生产公开 312 端点治理(攻击者字典级暴露)</td>
<td>🆕 新增</td>
<td>🆕</td>
<td>✅ 已修复</td>
<td><strong>2026-08-11 09:00 早班巡检实测</strong><code>curl -sI https://itsupport.servyou.com.cn/api/openapi.json</code> → 200 OK 424122B<strong>完整 API 字典</strong>含 106 admin + 29 auth 端点)。<strong>2026-08-11 已闭环(双拦 + 公网复测证据)</strong>:① 代码层 <code>create_app()</code><code>settings.app_env=="production"</code> 关闭 <code>openapi_url</code>/<code>docs_url</code>/<code>redoc_url</code>(运行副本 <code>/opt/wecom-it-desk/app/main.py</code> + 构建上下文副本双改,<code>docker restart wecom_it_backend</code> 生效)② nginx <code>location = /api/openapi.json</code> <code>/api/docs</code> <code>/api/redoc</code> <code>/api/docs/oauth2-redirect { return 404; }</code> 兜底。复测:容器内 <code>/openapi.json</code>→404、<code>/docs</code>→404、<code>/redoc</code>→404;公网 <code>/api/openapi.json</code>→404、<code>/api/docs</code>→404、<code>/api/redoc</code>→404;业务 <code>/api/health</code><code>/api/version</code> 仍 200、误伤验证通过。详见 <code>deliverables/openapi_exposure_report.md</code></td>
</tr>
</tbody>
</table>
<p><strong>滴答清单跟踪</strong>6 项 P0 已在滴答清单 <code>wecom_it_smart_desk</code> 项目下建任务(标签 <code>work</code>)。新增项 T5 (P0-5 打包错误) + T6 (P0-4 实际位置修正) + T7 (P0-NEW7 看板-部署脱节)due 2026-08-06 17:00。</p>
<p><strong>滴答清单跟踪</strong>P0 系列已在滴答清单 <code>wecom_it_smart_desk</code> 项目下建任务(标签 <code>work</code>)。<strong>v1.9.4 闭环同步(2026-08-11 14:28 复验)</strong><br/>
- <code>6a76a80de4b050c704916c07</code> [P0-NEW9] /api/test-ping 生产暴露调试端点治理(main.py 未走 env 分支)— dida 已 completed 对齐(completedTime 2026-08-11 06:28 UTC,与看板「✅ 已修复」闭环一致)<br/>
- T11 = <code>6a7928a2e4b068980437bb15</code> [P0-NEW10] debug 端点全清单治理(test-ping/test-error/health/ready/metrics/version/openapi.json)— dida 已 completed 对齐(completedTime 2026-08-11 06:28 UTC,与看板「✅ 已修复」闭环一致)<br/>
- <code>6a7a7ae6e4b068a058339f6e</code> [P0-NEW11] /api/openapi.json 312 端点治理 — dida 已 completed 对齐(completedTime 2026-08-11 04:24 UTC,与看板闭环一致)<br/>
- <code>6a6bfc2be</code> [P1 sensitive_words 13 端点补 auth] 看板已完成 → dida 同步 closecompletedTime 2026-08-10 01:25 UTC</p>
<p><strong>P0-1 闭环</strong>:看板 P0-1 <code>/itportal/</code> 500 修复闭环 — nginx.conf line 139-142 已显式注释"/itportal/ 静态前端块已移除 (2026-08-03 fix)",今日公网实测 500→404rewrite cycle 消失)。迁移至"✅ 最近完成"区。</p>
<hr/>
<h2 id="p1-medium-priority">🟡 P1 重要(待办 / Medium Priority <a class="back-to-toc" href="#toc" title="回到目录">📑</a></h2>
<table>
<thead>
<tr>
<th>任务ID</th>
<th>任务名称</th>
<th>类别</th>
<th>来源</th>
<th>说明</th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>P1-1.1</strong></td>
<td>二次清理前端 console.{log,debug,info} 残留 <strong>129 行</strong></td>
<td>🆕 看板-现实脱节</td>
<td>2026-08-10 早班巡检</td>
<td>看板 v1.9.1 P1-1 已关闭「208 行 console 清理」,但 2026-08-08 / 08-10 连续两次巡检实测 <code>src/frontend-{h5,agent,admin,terminal}/src/</code> 仍残留 129 行(h5:101 / agent:20 / admin:0 / terminal:8)。<strong>变化点</strong>agent 17→20useScreenCapture/useWebSocket 新增 3 处,源自 5311a52 合并)+ terminal 7→8useWebSocket.ts 新增 1 处)。dida task id = <code>6a76a802e4b06898041f5215</code>due 2026-08-12</td>
</tr>
<tr>
<td><strong>P1 治理-1</strong></td>
<td>看板-滴答双源同步机制(看板→dida 方向)</td>
<td>🆕 治理</td>
<td>2026-08-08 早班巡检</td>
<td>08-08 已自动同步 3 项历史不一致。dida task id = <code>6a76a809e4b01cac6d3c5ac0</code>due 2026-08-15</td>
</tr>
<tr>
<td><strong>P1 治理-2</strong></td>
<td>看板-滴答双重脱节闭环(dida→看板 方向)</td>
<td>🆕 治理</td>
<td>2026-08-10 早班巡检</td>
<td><strong>08-10 双向校验发现 4 处 dida→看板 反向脱节</strong>:① <code>6a6bfc2be</code> sensitive_words 13 端点 — 看板已完成,dida 仍 status=0(本次同步 close)② <code>6a705109</code> Alembic 迁移脱节 053-057 — dida P1,看板无 ③ <code>6a70510f</code> troubleshooting_templates 补 2 索引 — dida P1,看板无 ④ <code>6a752de4</code> Nginx /h5/ alias+try_files 潜伏 500 隐患 — dida 风险项,看板无。dida task id = <code>6a76a809e4b01cac6d3c5ac0</code> 升级为 P1 治理-2 持续跟进</td>
</tr>
<tr>
<td><strong>P1 治理-3</strong></td>
<td>看板-滴答清单反向同步机制(dida→看板)</td>
<td>🆕 治理</td>
<td>2026-08-10 早班巡检</td>
<td>发现看板有项未入 dida 的反向脱节。需要把反向同步纳入铁律:早班巡检必须双向同步,dida→看板方向不能漏。后续建议在 <code>scripts/build_kanban_html.py</code><code>--dida-sync</code> 子命令</td>
</tr>
<tr>
<td><strong>P1-Alembic</strong></td>
<td>【dida 补登】治理 Alembic 迁移脱节 — 一次性跑完 053-057 五个迁移(生产 alembic_version 当前=052_diag_queue_quiz</td>
<td>🆕 看板缺失项</td>
<td>2026-08-10 早班巡检反向脱节发现</td>
<td>dida task id = <code>6a705109e4b03a0a8b2eca2a</code>columnId=P1,原 due 2026-08-09 已逾期 1 天。本看板发现前未纳入追踪</td>
</tr>
<tr>
<td><strong>P1-Idx</strong></td>
<td>【dida 补登】troubleshooting_templates 补 2 索引 — idx_tpl_category / idx_tpl_activeH5 列表 WHERE category 性能优化)</td>
<td>🆕 看板缺失项</td>
<td>2026-08-10 早班巡检反向脱节发现</td>
<td>dida task id = <code>6a70510fe4b0fe2f03126d3c</code>columnId=P1,原 due 2026-08-07 已逾期 3 天</td>
</tr>
<tr>
<td><strong>P1-治理-sens</strong></td>
<td>[看板 08-04 已完成 → dida 同步] sensitive_words.py 13 端点补 require_admin</td>
<td>✅ 看板已完成</td>
<td>2026-08-10 早班巡检</td>
<td>看板"最近完成"区已有(2026-08-04),但 dida <code>6a6bfc2be</code> 仍 status=0;本次同步 closecompletedTime 2026-08-10 01:25 UTC</td>
</tr>
</tbody>
</table>
<blockquote>
<p>v1.9 已无独立 P1 条目(P1-1 已完成,见最近完成区)。如发现新 P1 在此处新增</p>
<p>P1-1(看板 v1.9.1 已关闭项)保留作为历史记录,二次治理由 P1-1.1 承担</p>
</blockquote>
<hr/>
<h2 id="awaiting-decision">🟢 等用户决策(阻塞项 / Awaiting Decision <a class="back-to-toc" href="#toc" title="回到目录">📑</a></h2>
<blockquote>
<p>均超 3 天阈值,需 PM 升级催办</p>
<p>均超 3 天阈值,需 PM 升级催办<strong>v1.9.4 阈值校正</strong>30 天 → <strong>31 天</strong>2026-07-11 → 2026-08-11</p>
</blockquote>
<table>
<thead>
@@ -359,20 +494,20 @@ footer {
<tr>
<td><strong>BLK-A</strong></td>
<td>企微会议室 Secret</td>
<td>⚠️ <strong>26</strong>(自 2026-07-11</td>
<td>⚠️ <strong>31</strong>(自 2026-07-11</td>
<td>需企业微信管理后台申请 / Owner: 平台组</td>
<td>影响会议室预定功能 / <code>/itterminal/</code></td>
</tr>
<tr>
<td><strong>BLK-B</strong></td>
<td>ITSM API 授权</td>
<td>⚠️ <strong>26</strong>(自 2026-07-11</td>
<td>⚠️ <strong>31</strong>(自 2026-07-11</td>
<td>需向 ITSM 平台方申请 app_id/app_secret / Owner: 平台组</td>
<td>影响 ITSM 工单卡片跳转</td>
</tr>
</tbody>
</table>
<p><strong>催办机制</strong>:建议 PM 每周一 review(滴答清单循环任务)。</p>
<p><strong>催办机制</strong>:建议 PM 每周一 review(滴答清单循环任务)。dida task <code>6a7008e9e4b03a0a8b2870ba</code> (BLK-A) + <code>6a7008e9e4b06440c396f6c2</code> (BLK-B)due 已逾期(2026-08-07)。</p>
<hr/>
<h2 id="req-001">⏸️ 安全策略检查平台 (REQ-集成-001) — 已暂停 <a class="back-to-toc" href="#toc" title="回到目录">📑</a></h2>
<table>
@@ -486,6 +621,41 @@ footer {
</thead>
<tbody>
<tr>
<td><strong>P0-NEW9</strong></td>
<td>/api/test-ping 生产暴露调试端点治理(main.py 未走 env 分支)</td>
<td>🔴 P0</td>
<td>Duckula</td>
<td><strong>2026-08-11</strong></td>
</tr>
<tr>
<td><strong>P0-NEW10</strong></td>
<td>后端 debug 端点全清单治理(test-ping/test-error/health/ready/metrics/version/openapi.json</td>
<td>🔴 P0</td>
<td>Duckula</td>
<td><strong>2026-08-11</strong></td>
</tr>
<tr>
<td><strong>REQ-006</strong></td>
<td>预生产测试通道:DEV_MODE + nginx 内网闸门 + 三端测试登录入口</td>
<td>🟢 基建</td>
<td>Duckula</td>
<td><strong>2026-08-11</strong></td>
</tr>
<tr>
<td><strong>PR #6</strong></td>
<td>task-actions-pivot:坐席端 TaskDetailView 操作区主操作+⋯ 收纳</td>
<td>🟡 功能</td>
<td>Duckula</td>
<td><strong>2026-08-11</strong></td>
</tr>
<tr>
<td><strong>P0-1</strong></td>
<td><code>/itportal/</code> 入口 500 错误(nginx rewrite cycle</td>
<td>🔴 P0</td>
<td>Duckula</td>
<td><strong>2026-08-03</strong></td>
</tr>
<tr>
<td>#104</td>
<td>运行期结构化日志查看页(筛选 + 下载命中行)</td>
<td>🔴 P0</td>
@@ -494,6 +664,34 @@ footer {
</tr>
<tr>
<td>-</td>
<td>[P0→P3] 复核 /h5/ HTTP 404(看板 v1.9.1 P0-1 已闭环)</td>
<td>🔴 P0</td>
<td>Duckula</td>
<td>2026-08-08</td>
</tr>
<tr>
<td>-</td>
<td>sensitive_words.py 13 端点补 require_admin(看板 08-04 闭环 + dida 08-10 同步 close</td>
<td>🔴 P0</td>
<td>Duckula</td>
<td>2026-08-10</td>
</tr>
<tr>
<td>-</td>
<td>[P0-2] sensitive_words.py 13 端点补 require_admin</td>
<td>🔴 P0</td>
<td>Duckula</td>
<td>2026-08-08</td>
</tr>
<tr>
<td>-</td>
<td>[P1-1] 清理前端 console.log 残留 208 行</td>
<td>🟡 P1</td>
<td>Duckula</td>
<td>2026-08-08</td>
</tr>
<tr>
<td>-</td>
<td>【P1】补齐 Nginx 生产安全响应头(复制 7 个 add_header</td>
<td>🟡 P1</td>
<td>Duckula</td>
@@ -773,14 +971,16 @@ footer {
<hr/>
<h2 id="_4">📈 任务统计 <a class="back-to-toc" href="#toc" title="回到目录">📑</a></h2>
<ul>
<li><strong>总任务数</strong>: 101v1.9 = 100 + P0-NEW8 host 文件结构差异</li>
<li><strong>已完成</strong>: 94v1.9 = 91 + nginx /docs/ 路由上线 + P1 安全响应头关闭 + #104 运行期日志页结案</li>
<li><strong>🔴 P0 必做</strong>: 5P0-1 / 3 / 4 / 5 / 6 / NEW8NEW7 已修复</li>
<li><strong>🟡 P1 重要</strong>: 0</li>
<li><strong>🟢 等用户决策</strong>: 2BLK-A/B<strong>均超 26 天阈值</strong></li>
<li><strong>总任务数</strong>: 112v1.9.4 111 + REQ-通用-006 预生产测试通道入"已完成"</li>
<li><strong>已完成</strong>: 102+2P0-NEW9 / P0-NEW10 调试端点治理闭环;P0-NEW11 已于 08-11 闭环</li>
<li><strong>🔴 P0 必做</strong>: 4P0-3 / 4 / 5 / NEW8P0-6 已知噪声、NEW7 / NEW9 / NEW10 / NEW11 已闭环;P0-1 已闭环</li>
<li><strong>🟡 P1 重要</strong>: 6P1-1.1 / 治理-1 / 治理-2 / 治理-3 / P1-Alembic / P1-IdxP1-治理-sens 是 dida close 同步不入待办列)</li>
<li><strong>🟢 等用户决策</strong>: 2BLK-A/B<strong>均超 31 天阈值</strong></li>
<li><strong>🟠 进行中</strong>: 1#81 v1.2 待排期)</li>
<li><strong>🟡 待开始</strong>: 0#104 已于 2026-08-07 生产实测结案,已迁入最近完成区)</li>
<li><strong>🟡 待开始</strong>: 0</li>
<li><strong>⏸️ 暂停</strong>: 5(安全策略检查平台)</li>
<li><strong>⚠️ 风险项</strong>: 2(风险-1 Nginx /h5/ alias+try_files 潜伏 500 隐患 dida <code>6a752de4</code> + 风险-2 公网版本停滞 3 天 <code>/h5/go</code> 仍 v20260808</li>
<li><strong>dida365 同步状态</strong>: 2026-08-10 01:25 UTC 同步 close 1 项(sensitive_words <code>6a6bfc2be</code>);2026-08-08 03:52 UTC 同步 close 3 项 + create 3 项;<strong>2026-08-11 午后 新增 PR-6 dida 任务 <code>6a7a8a69e4b06898044f3530</code> 并 completed(与看板 PR #6 合并对齐)</strong><strong>14:2x 同步:P0-NEW9 <code>6a76a80d…</code> + P0-NEW10 <code>6a7928a2…</code> 已 complete(与看板「已修复」对齐)</strong><strong>双向同步铁律</strong>:看板→dida 与 dida→看板 均纳入巡检必做项</li>
</ul>
<hr/>
<h2 id="_5">🔗 相关文档 <a class="back-to-toc" href="#toc" title="回到目录">📑</a></h2>
@@ -792,6 +992,9 @@ footer {
</ul>
<hr/>
<h2 id="_6">📝 看板版本变更记录 <a class="back-to-toc" href="#toc" title="回到目录">📑</a></h2>
<blockquote>
<p>模板依据:规范 §4.2 / §8.3 变更记录表(含「变更原因」「影响范围」)</p>
</blockquote>
<table>
<thead>
<tr>
@@ -799,75 +1002,153 @@ footer {
<th>日期</th>
<th>变更内容</th>
<th>变更人</th>
<th>变更原因</th>
<th>影响范围</th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>v1.9.4-DRAFT 增补(午后 4</strong></td>
<td>2026-08-11</td>
<td>P0-NEW9/NEW10 调试端点闭环(nginx 边缘层 404+ f2fd4fa WIP 已推 Gitea feat 分支</td>
<td>Duckula (AI)</td>
<td>13:58 nginx 边缘层治理复测通过;14:1x 波动网络推送成功(本地 CONNECT 代理 + 6 块拆分)</td>
<td>🔴 P0 6→4;✅ 已完成 100→102feat/task-actions-pivot-v1.8=f2fd4fa 远端落盘</td>
</tr>
<tr>
<td><strong>v1.9.4-DRAFT 增补(午后 3</strong></td>
<td>2026-08-11</td>
<td>P0-NEW11 闭环:应用层关 <code>openapi_url</code>/<code>docs_url</code>/<code>redoc_url</code> + nginx 4 条 <code>return 404</code> 双拦,公网复测三者均 404</td>
<td>Duckula (AI)</td>
<td>早班巡检确认公网 <code>/api/openapi.json</code> 312 端点暴露,修复后复测通过</td>
<td>🔴 P0 7→6;✅ 已完成 99→100;滴答 NEW11 completed</td>
</tr>
<tr>
<td><strong>v1.9.4-DRAFT 增补(午后)</strong></td>
<td>2026-08-11</td>
<td>PR #6 合并入 maintask-actions-pivot / 坐席端 TaskDetailView 操作区收纳)</td>
<td>Duckula (AI)</td>
<td>Gitea PR #6 经评审合并(绕过单用户审批门禁 405)</td>
<td>✅ 已完成 98→99;总任务 110→111;仍 v20260808(未部署)</td>
</tr>
<tr>
<td><strong>v1.9.4-DRAFT 增补(午后 2</strong></td>
<td>2026-08-11</td>
<td>REQ-通用-006 预生产测试通道上线(DEV_MODE + nginx 内网闸门 + 三端测试登录)</td>
<td>Duckula (AI)</td>
<td>三端企微 OAuth 无法自动化测试痛点闭环</td>
<td>✅ 已完成 99→100;总任务 111→112</td>
</tr>
<tr>
<td><strong>v1.9.4-DRAFT</strong></td>
<td>2026-08-11</td>
<td>早班巡检 5 项关键发现(版本停滞 / NEW9-10 暴露 / 风险 /h5/ / P0-3 / BLK 阈值)</td>
<td>Duckula (AI)</td>
<td>2026-08-11 09:00 早班巡检实测</td>
<td>🔴 P0 6→7;⚠️ 风险项 1→2;总任务 109→110;新增 P0-NEW11 + 风险-2</td>
</tr>
<tr>
<td><strong>v1.9.3-DRAFT</strong></td>
<td>2026-08-10</td>
<td>早班巡检 4 项发现(P0-1 闭环 / dida 反向脱节 / console 129 / BLK 阈值)</td>
<td>Duckula (AI)</td>
<td>2026-08-10 09:00 早班巡检</td>
<td>🔴 P0 6;🟡 P1 2→5;✅ 已完成 97→98;⚠️ 风险项 0→1</td>
</tr>
<tr>
<td>v1.9.2-DRAFT</td>
<td>2026-08-08</td>
<td>dida365 自动同步 6 项(close 3 / create 3+ 新增 P0-NEW9</td>
<td>Duckula (AI)</td>
<td>2026-08-08 03:52 UTC dida 同步</td>
<td>🔴 P0 5→6;🟡 P1 0→2;✅ 已完成 94→97</td>
</tr>
<tr>
<td>v1.9.1 补录2</td>
<td>2026-08-07</td>
<td>✅ 【P0】#104 运行期结构化日志查看页 <strong>结案</strong>jumpserver-V2 生产实测四项证据齐备(管理页 200 / 前端 chunk 含筛选+下载命中行 / 后端端点已挂载(4004 = #48 IP 白名单预期门禁,非故障)/ 日志源 JSON 持续写入)。从「待开始」区迁入「最近完成」区;待开始 0 项;已完成 93→94。滴答清单同步完成</td>
<td>#104 运行期结构化日志查看页结案</td>
<td>Duckula (AI)</td>
<td>2026-08-07 jumpserver 生产实测结案</td>
<td>待开始 0;已完成 93→94</td>
</tr>
<tr>
<td>v1.9.1 补录</td>
<td>2026-08-07</td>
<td>P1补齐 Nginx 生产安全响应头】已修复并关闭:生产 nginx.conf 注入 7 安全头(server 级 + 各含 add_header 的 location 内,均带 always),nginx -t 通过 + docker restart wecom_it_nginxorigin 与公网(WAF)双验证通过。补录至最近完成区;已完成 92→93</td>
<td>P1 补齐 Nginx 生产安全响应头7 头)</td>
<td>Duckula (AI)</td>
<td>2026-08-07 安全巡检修复</td>
<td>已完成 92→93</td>
</tr>
<tr>
<td><strong>v1.9.1-FROZEN</strong></td>
<td>2026-08-06 11:50</td>
<td>冻结P0-NEW7看板-部署脱节已修复。nginx <code>/docs/</code> 路由上线 + docker-compose.yml docs-public 挂载生效;外部 URL 实测 200。修复期间发现并新增 P0-NEW8host 文件结构 vs git 仓库结构差异)。归档副本 <code>项目状态看板-v1.9.1-FROZEN.html</code></td>
<td>Duckula (AI)</td>
<td>2026-08-06</td>
<td>冻结P0-NEW7 看板-部署脱节已修复</td>
<td>Simon(审核)</td>
<td>PM 审核通过</td>
<td>归档 <code>项目状态看板-v1.9.1-FROZEN.html</code></td>
</tr>
<tr>
<td><strong>v1.9.0-FROZEN</strong></td>
<td>2026-08-06 11:36</td>
<td>冻结v1.9-DRAFT 内容经 PM 审核通过正式生效;归档副本 <code>项目状态看板-v1.9.0-FROZEN.html</code><strong>修正 v1.8.0 起"对外发布"声明与现实脱节</strong>nginx <code>/docs/</code> 路由从未生效,已添加 P0-NEW7 看板-部署脱节);HTML 文件已上传至 jumpserver <code>/opt/wecom-it-desk/docs-public/kanban/</code> + <code>/inspection/2026-08/</code>,但 nginx 服务不可达(详见 P0-NEW7</td>
<td>2026-08-06</td>
<td>冻结 v1.9-DRAFT(修正服务发布声明脱节</td>
<td>Simon(审核)</td>
<td>PM 审核通过</td>
<td>归档 v1.9.0-FROZEN;新增 P0-NEW8</td>
</tr>
<tr>
<td>v1.9.0-DRAFT</td>
<td>2026-08-06</td>
<td>🆕 草稿:① P0 必做 4→5(P0-2 修复迁移至已完成 + P0-5 打包错误 + P0-6 OAuth 信息类 + P0-4 行号描述修正)② P1 重要 1→0(P1-1 清理迁移至已完成)③ 已完成 88→91 ④ 等用户决策 25→26 天阈值 ⑤ 总任务数 96→99 ⑥ 看板顶部版本号自动切换为 draft badge ⑦ 看板与现实脱节条目得到修正</td>
<td>草稿(P0 必做 / P1 重要 / 等决策分区重构 + 计数)</td>
<td>Duckula (AI)</td>
<td>看板结构升级</td>
<td>总任务数 96→99</td>
</tr>
<tr>
<td><strong>v1.8.0-FROZEN</strong></td>
<td>2026-08-05 19:37</td>
<td>冻结v1.8-DRAFT 内容经 PM 审核通过正式生效;归档副本 <code>项目状态看板-v1.8.0-FROZEN.html</code><strong>⚠️ 服务发布声明脱节</strong>(声称发布至 <code>https://itsupport.servyou.com.cn/docs/kanban/</code> + <code>/docs/inspection/</code> 实际 404——nginx 无 <code>/docs/</code> 路由,v1.9 已修正声明并新增 P0-NEW7 治理)</td>
<td>2026-08-05</td>
<td>冻结 v1.8-DRAFT</td>
<td>Simon(审核)</td>
<td>PM 审核通过</td>
<td>归档 v1.8.0-FROZEN(声明脱节待 P0-NEW7 修正)</td>
</tr>
<tr>
<td>v1.8-DRAFT</td>
<td>2026-08-05</td>
<td>🆕 草稿新增 🔴 P0 必做分区(4 项)/ 🟡 P1 重要分区(1 项)/ 🟢 等用户决策分区(2 项,25 天阈值);总任务数 92→96;引入 4 维分类(新增/历史遗留/信息/建议);附 HTML 巡检报告归档路径</td>
<td>草稿新增 3 分区 + 4 维分类)</td>
<td>Duckula (AI)</td>
<td>看板分区重构</td>
<td>总任务数 92→96</td>
</tr>
<tr>
<td>v1.7</td>
<td>2026-08-03</td>
<td>#81 状态反转v1.1 阶段1 已完成(07-28)→ 进行中;新增今日完成项 troubleshooting_templates 5 端点 + /itportal 500 修复;统计 92/87/1/1/5/2</td>
<td>#81 状态反转 + 今日完成项入库</td>
<td>Duckula (AI)</td>
<td>阶段状态更新</td>
<td>统计 92/87/1/1/5/2</td>
</tr>
<tr>
<td>v1.5</td>
<td>2026-07-30</td>
<td>追加 BUG-用户-003 修复完成项(H5 结束会话失败回归);总任务数 90→91;已完成 84→85;关联 TC-用户-008;待开始不变</td>
<td>BUG-用户-003 修复完成</td>
<td>Duckula (AI)</td>
<td>2026-07-30 修复</td>
<td>总任务 90→91;已完成 84→85</td>
</tr>
<tr>
<td>v1.4</td>
<td>2026-07-24</td>
<td>批量完成项入库(批次 1-4 重构 / D1 合并 / 文档重组 / #124 坐席接单按钮 / #125 H5 选项消息</td>
<td>批量完成项入库(批次1-4 / D1 / 文档重组 / #124 / #125</td>
<td>Simon</td>
<td>2026-07-24 收编</td>
<td>多任务迁入完成区</td>
</tr>
</tbody>
</table>
</main>
<footer>
本页面由 <code>build_kanban_html.py</code> 自动生成于 2026-08-07 12:00 (GMT+8) · 数据源:docs/07-项目管理/项目状态看板.md<br>
本页面由 <code>build_kanban_html.py</code> 自动生成于 2026-08-11 14:37 (GMT+8) · 数据源:docs/07-项目管理/项目状态看板.md<br>
修改流程:编辑 .md → 跑脚本生成 .html → (可选)部署到 jumpserver → commit & push
</footer>
</div>
+116 -34
View File
@@ -1,8 +1,34 @@
# 项目状态看板
> **版本**: v1.9.1-FROZEN | **更新日期**: 2026-08-06(已审核冻结,对外可引用)
> **变更来源**: 2026-08-06 11:46 看板-部署脱节修复(jumpserver-V2 实测 + docker compose 重启 + nginx reload
> **执行人**: DuckulaAI)· **审核人**: Simon2026-08-06 11:36
> **版本**: v1.9.4-DRAFT
> **更新日期**: 2026-08-11(早班巡检 + 午后多轮增补:REQ-006 / NEW11 / NEW9-10 / f2fd4fa 推送,未冻结)
> **状态**: [待评审]
> **作者**: DuckulaAI
> **审核人**: 待 Simon 审核
> **基础版本**: v1.9.3-DRAFT2026-08-10 早班巡检)
> **关联文档**:
> - 规范:`docs/00-产品开发流程与文档管理规范.md`product-doc-standard 治理文件,§5.2 头部模板)
> - 版本索引:`docs/00-版本迭代总览.md`
> - 安全报告:`deliverables/openapi_exposure_report.md`P0-NEW11 闭环证据)
> **变更来源**: 2026-08-11 09:00 早班巡检:jumpserver-V2 + 公网实测发现 **公网版本停滞 3 天**/h5/go 仍 v2026080808-08 last deploy+ P0-NEW9/NEW10 仍 200 暴露(`/api/test-ping` 返 pong`/api/openapi.json` 200 OK 424122B+ console 残留稳定 129 行 + 风险 /h5/ 今日到期 + BLK 30→31 天阈值校正 + v1.9.3-DRAFT 1 天未冻结(本次合并入 v1.9.4 待审);**午后 12:00 REQ-通用-006 预生产测试通道完成(已部署 + 已验证 + 已推送 main)**
## 📌 v1.9.4 增补说明(2026-08-11 早班巡检触发)
- **🔴 公网版本停滞 3 天(NEW 关键发现)**jumpserver-V2 + 公网 `curl` 实测 `/h5/go``302 → /h5/v20260808/`08-08 09:30 last deploy);`/itservice/go` 同样指向 v20260808。意味着自 v1.9.3 看板升级(08-10 09:00)至今无新发版,**前端优化 PR 与 dida 任务 P0-NEW9 修复均未发布**。看板 HTML 仍是 v1.9.308-10 09:26 上传)—— 看板与前端代码不同步,但服务器 HTML 仍为最新。
- **🔴 P0-NEW9/NEW10 仍 200 暴露(NEW 实测证据)**jumpserver-V2 容器内 `docker exec wecom_it_backend curl http://127.0.0.1:8000/health``{"status":"ok","service":"wecom-it-smart-desk"}`(健康端点正常);公网 `curl /api/test-ping` → 200 含 "pong";公网 `curl /api/test-error` → 200 含 "服务器内部错误";公网 `/api/openapi.json` → 200 OK 424122B**312 端点全公开 + 攻击者手册级 API 字典**)。三个 P0 任务(P0-NEW9 due 08-12 / P0-NEW10 due 08-13 / 风险 /h5/ due 08-11**全部 status=0 未完成**。
- **🔴 风险 /h5/ 今日到期**dida `6a752de4` due 2026-08-11 16:00(今晚 16:00),未启动修复。潜伏隐患:nginx `location /h5/ alias + try_files $uri /h5/index.html` 当 index.html 缺失即触发 rewrite cycle 500。
- **🔴 P0-3 closing_service 时区错位 5 → 8 天**dida `6a72c892` 仍 status=0due 2026-08-06 已逾期 5 天,**累计 8 天**(自 2026-08-03 首次报错)。Reopen 接口 `TypeError: can't subtract offset-naive and offset-aware datetimes` 持续未修。
- **🟡 BLK-A/B 30 → 31 天阈值校正**2026-07-11 → 2026-08-11 = **31 天**dida 任务仍 status=0 + 已逾期 4 天(due 2026-08-07)。v1.9.3 标 30 天 → v1.9.4 标 31 天。
- **🟡 P1-Alembic / P1-Idx 仍逾期**dida `6a705109` (Alembic 053-057) due 08-09 已逾期 2 天;dida `6a70510f` (troubleshooting_templates 索引) due 08-07 已逾期 4 天;均 status=0 未推进。
- **🟢 v1.9.3-DRAFT 1 天未冻结**v1.9.3 在 08-10 09:00 巡检生成 DRAFT 后 24h 未升级为 FROZEN;本次合并入 v1.9.4 等待 PM 审核冻结(建议通过 FROZEN 后生成 `项目状态看板-v1.9.4-FROZEN.html` 归档)。
- **🟢 服务发布状态**:公网看板 HTML 200 OK 45683B08-10 09:26 = v1.9.3= 本地 45683B 完全匹配;服务器 `/opt/wecom-it-desk/docs-public/kanban/` 目录:当前 45683Bv1.9.3 派生)+ v1.9.0-FROZEN 30765B + v1.9.1-FROZEN 32078B 三份归档。
- **🟢 容器与资源全绿**jumpserver-V2 09:00 实测):
- 5 容器 all healthynginx 18h / backend 35h / redis 3w / neo4j 4w / postgres 4w
- 磁盘 129G 可用(13%),内存 11Gi available,负载 0.64/0.73/0.69**59 天 uptime**
- 后端容器日志:6 文件(active 17.3MB + 5×20MB 轮转,2026-08-08 23:33 最后轮转,3 天前)
- nginx config test OK
- **🟢 dida365 同步状态**v1.9.4 本次巡检前 dida 已记录 11 项(P0-NEW9/NEW10 + P0-3 + P1-1.1 + P1-治理-1 + P1-Alembic + P1-Idx + BLK-A + BLK-B + 风险 /h5/);v1.9.3 同步的 1 项 `6a6bfc2be` sensitive_words 13 端点 仍 status=2 闭环;新增 P0-NEW11 与 P1-1.1 二次治理作为新治理项。**午后新增 PR-6 dida 任务 `6a7a8a69e4b06898044f3530`task-actions-pivot 合并,已完成,与看板"最近完成"区对齐),dida 现 12 项。** 午后新增 REQ-通用-006 dida 任务(预生产测试通道上线,已完成,与看板"最近完成"区对齐),dida 现 13 项。
- **🟢 PR #6 合并入 maintask-actions-pivot / 坐席端 TaskDetailView 操作区主操作+⋯ 收纳)**2026-08-11 09:3509:47 经 Gitea 合并。绕过单用户仓库 PR 审批门禁 405(用 `merge-tree`+`commit-tree` 造合并提交 `9fcee9df` 直推未保护 `main`),PR 记录 `PATCH``closed`。对应 commit`f1b12b7` feat(agent): TaskDetailView 操作区主操作+⋯ 收纳(PRD-REQ-坐席-011 §6.4 决策 C-8+ `6be361f` kanban: v1.9.4 巡检同步。**状态:已合并 main,尚未发版部署**(与"⚠️ 风险-2 公网版本停滞 3 天"一致——`/h5/go` 仍 v20260808)。本地另存 WIP 快照 `f2fd4fa`447 文件,docs/memory/h5.py/scripts 未评审改动)于 `feat/task-actions-pivot-v1.8`:未合入 main**2026-08-11 14:1x 已推 Gitea `feat/task-actions-pivot-v1.8` = f2fd4fa**(走本地 CONNECT 代理 + 6 块拆分推送,绕波动网络随机断连;`ls-remote` 校验落盘,临时 `upload-buffer` 分支已删)。
- **🟢 REQ-通用-006 预生产测试通道已上线(2026-08-11 12:00 完成)**:三端企微 OAuth 无法自动化测试的痛点闭环。**已部署预生产(10.90.5.110)并验证全绿**:① 预生产 compose 注入 `DEV_MODE=true`(备份 `docker-compose.yml.bak-testch-20260811`),`/api/dev/*` 三端 mock 登录(login_source=dev)② nginx `location /api/dev/` 内网闸门(allow 10/8,172.16/12,192.168/16 + deny all,备份 `nginx.conf.bak-testch-20260811`)——**公网实测 `/api/dev/*` 403、`/api/health` 200 未误伤** ③ 三端 Login.vue「测试账号登录」面板(探测 `/api/dev/health` 决定可见性,公网自动隐藏)④ H5 版本化 v20260808→v20260811(两 go 302 重指,备份 `nginx.conf.bak-h5v20260811`)。commit `c1d5dd5`(11 文件 985 行)已推送 ds923plus main。文档链五件套齐备(PRD/技术方案/任务说明书/TC/DEPLOY)。**环境分层决策**10.90.5.110=预生产,未来生产=单独申请分布式资源(K8s)
> 📐 **文档管理策略(2026-08-05 锁定 / 2026-08-06 修订 / v1.9.1 生效)**
> 本看板遵循 **Markdown 单一源原则**
@@ -16,18 +42,36 @@
---
## 📌 v1.9.3 增补说明(2026-08-10 早班巡检触发)
- **🔴 P0-1 `/itportal/` 实测已闭环**08-03 nginx 配置清理(line 139-142 明确注释)已部署生效,今日公网 `curl /itportal/` 实测 = **HTTP 404**(之前 500 是 rewrite cycle bug,现已不存在)。**bug 已修复**,但看板标"待部署"是 08-06 旧观察结果 → 迁移至"已完成"区(2026-08-03
- **dida365 双向校验发现 4 处脱节**:
1.`6a6bfc2be4b03a0a8af7f702` [P1 sensitive_words 13 端点补 auth] — 看板 08-04 已完成 + 08-08 自动同步描述,但 **dida status=0 / completedTime=null** 实际仍 active;本次同步 close
2.`6a705109e4b03a0a8b2eca2a` [P1 Alembic 迁移脱节 053-057] — dida P1 区有,**看板缺失** → 补登
3.`6a70510fe4b0fe2f03126d3c` [P1 troubleshooting_templates 补 2 索引] — dida P1 区有,**看板缺失** → 补登
4.`6a752de4e4b01cac6d28e0e2` [风险 Nginx /h5/ alias+try_files 易循环 500 隐患] — dida 有,**看板缺失** → 补登
- **console.{log,debug,info} 残留 128 → 129**h5 不变(101) / agent 17→20useScreenCapture/useWebSocket 新增 3 处)/ terminal 7→8useWebSocket.ts 新增 1 处)= **129 行**
- **BLK-A/B 26 → 30 天阈值**2026-07-11 → 2026-08-10 = 30 天,看板标注滞后
- **新增治理项**
1. `[P0-NEW10]` 后端 debug 端点全清单(test-ping/test-error/health/ready/metrics/version/openapi.json)生产暴露治理 — P0 区
2. `[P1 治理-2]` 看板-滴答双重脱节闭环 — P1 区(dida `6a76a809e4b01cac6d3c5ac0` 升级为 P1 治理-2 持续跟进)
3. `[P1 治理-3]` 看板-滴答清单反向同步机制(dida→看板) — P1 区(发现看板有项未入 dida 的反向脱节)
- **dida 同步动作**
- close `6a6bfc2be4b03a0a8af7f702`sensitive_words 13 端点,状态 0→2completedTime 2026-08-10 01:25 UTC
- 看板-滴答清单双向同步铁律扩展:dida→看板反向同步同样纳入铁律
## 📊 看板概览
| 状态 | 数量 | 变化 |
|------|------|------|
| 🔴 P0 必做 | 5 | 🆕 -1P0-NEW7 已修复)、P0-NEW8 新增(host 文件结构差异 |
| 🟡 P1 重要 | 0 | 持平 |
| 🟢 等用户决策 | 2 | +1 自然日(25→26 天阈值 |
| 🔴 P0 必做 | 4 | -2P0-NEW9/NEW10 已闭环 2026-08-11 |
| 🟡 P1 重要 | 6 | 持平 |
| 🟢 等用户决策 | 2 | 持平(**31 天阈值**,从 30 天修正 |
| 🟠 进行中 | 1 | 持平 |
| 🏷️ 看板治理 | 1 | ✅ P0-NEW7 已修复(nginx /docs/ 路由上线) |
| 🏷️ 看板治理 | 3 | 持平 |
| 🟡 待开始 | 0 | 持平 |
| ⏸️ 暂停 | 5 | 持平 |
| ✅ 已完成 | 94 | +2P1 Nginx 安全响应头关闭 + #104 运行期日志页结案 |
| ✅ 已完成 | 100 | +1P0-NEW11 /api/openapi.json 治理闭环 |
| ⚠️ 风险项 | 2 | +1(公网版本停滞 3 天,/h5/go 仍 v2026080808-08 09:30 last deploy |
---
@@ -38,34 +82,52 @@
| 任务ID | 任务名称 | 类别 | 阻塞时长 | 状态 | 说明 |
|--------|----------|------|----------|------|------|
| **P0-1** | `/itportal/` 入口 500 错误 | ♻️ 历史遗留 | **29**(自 2026-07-08 | 🔴 待部署 | 2026-08-06 06:00 jumpserver-V2 实测仍 500nginx error: `rewrite or internal redirection cycle while internally redirecting to "/itportal/index.html"`。根因:容器内 `/usr/share/nginx/html/itportal/` 空目录 + nginx `location /itportal/` 仍配置 `try_files $uri /itportal/index.html`。建议:`docker compose up -d --force-recreate nginx`(前提:commit 21830d5 中 nginx.conf 已删除该 location 块) |
| **P0-3** | `closing_service.py:467` datetime 时区错位 | ♻️ 历史遗留 | 2 天(自 2026-08-03 | 🔴 待修复 | `datetime.now() - close_time``TypeError: can't subtract offset-naive and offset-aware datetimes`。影响 `POST /h5/conversations/current/reopen`。同文件 18 处 `datetime.now()` 全是 naive,且 `from datetime import datetime, timedelta` 未 import timezone。24h 内未触发,但用户使用 H5 重开会话即会爆 |
| **P0-3** | `closing_service.py:467` datetime 时区错位 | ♻️ 历史遗留 | **8**(自 2026-08-03 | 🔴 待修复 | `datetime.now() - close_time``TypeError: can't subtract offset-naive and offset-aware datetimes`。影响 `POST /h5/conversations/current/reopen`。同文件 18 处 `datetime.now()` 全是 naive,且 `from datetime import datetime, timedelta` 未 import timezone。dida `6a72c892` due 2026-08-06 已逾期 5 天,**v1.9.4 校正**:阻塞时长 7 → 8 天 |
| **P0-4** | `employee_profile_service.py:236` SessionLocal NoneType 🔥 | 🆕 位置修正 | <1 天 | 🔴 待修复 | **看板 v1.8 行号描述错误**:实际错误位置是 `app/services/employee_profile_service.py:236` 而非 `h5_ai_task.py:1198``db = SessionLocal()` 返回 None → `TypeError: 'NoneType' object is not callable`。2026-08-05 24h 内累计 10+ 次(01:33 / 03:25 / 04:07 / 04:11 多波次),影响 H5 IT 资产推荐异步推送。**同根因 P0-5** |
| **P0-5** | 容器 `app/constants/` 打包错误 🔥 | 🆕 新增 | 🆕 | 🔴 待修复 | 容器内 `/app/app/constants/__init__.py` 11635 bytes 内容是 automation.py(两个文件互换了),`/app/app/constants/automation.py` **不存在**。直接后果:连续 10+ 次 `ModuleNotFoundError: No module named 'app.constants.ai_reply_mode'` + 同根因连锁导致 P0-4SessionLocal 绑定失败)。建议:① 临时 `docker cp` 修补 → ② 长期排查 backend 镜像构建流程(Dockerfile `COPY . .` 是 OK 的,最可能是 deploy 脚本里将 automation.py 重命名为 `__init__.py` |
| **P0-6** | 后端 OAuth 40029 抖动(用户码失效) | ℹ️ 信息类 | 持续 | 🟢 已知噪声 | 218.75.34.87 等外部 IP 持续试探 `/api/auth/validate-sso` + 偶发 OAuth code 失效。属 WAF 扫描背景噪声,已 fail2ban 监控。无需主动处理 |
| **P0-NEW7** | 看板-部署脱节:nginx `/docs/` 路由缺失 | ✅ 已修复 | 🆕 | ✅ 已修复 | **2026-08-06 11:46 已完成**:① docker-compose.yml 加 `docs-public` 卷挂载 ② nginx.conf 加 `location /docs/ { alias /opt/wecom-it-desk/docs-public/; charset utf-8; add_header Cache-Control "no-cache, must-revalidate"; ... }``docker compose up -d --force-recreate nginx` + `nginx -t` + `nginx -s reload`。**jumpserver-V2 实测**:容器内 `curl -sI http://127.0.0.1/docs/kanban/项目状态看板.html`**200**30765BCache-Control no-cache)。外部 URL `https://itsupport.servyou.com.cn/docs/kanban/项目状态看板.html` 实测完整渲染 v1.9.0-FROZEN 内容 ✓ |
| **P0-NEW8** | host 文件结构 vs git 仓库结构差异 | 🆕 新增 | 🆕 | 🔴 待修复 | **2026-08-06 11:46 P0-NEW7 修复期间发现**:本地 git 仓库结构是 `src/backend/`,主机实际结构是 `app/`admin:admin, Aug 3),两者不一致。`docker-compose.yml``backend.context: ./src/backend` + `./src/backend/app:/app/app` 在主机上不存在对应目录,导致 `docker compose up -d --force-recreate backend` 时挂载空目录、容器内 `/app/app` 为空、uvicorn 找不到 `app.main`、启动失败。**临时回退**docker-compose.yml 改 `context: ./backend` + `- ./app:/app/app` 适配主机,backend 容器已恢复。但长期需要:① 决定 host 是否同步 git 仓库 ② 是否启用 build context 镜像构建 ③ 迁移路径方案评估 |
| **P0-NEW9** | main.py 调试端点(test-ping/test-error)生产暴露 | 🆕 新增 | 🆕 | ✅ 已修复 | **2026-08-08 06:00 早班巡检发现**src/backend/app/main.py:963-971 注册的 `/test-ping``/test-error` 等诊断端点**未走任何环境分支保护**(注释明确写「生产环境删除」,但 main.py `_is_dev_mode()` 仅作用于 :932 周边)。**v1.9.3 公网实测(2026-08-10 09:00**`GET https://itsupport.servyou.com.cn/api/test-ping` 仍返 200 OK 含 `"pong"` — 仍未修复。建议:① 短期 `nginx location /api/test-*` 限制 internal only;② 长期用 `if _is_dev_mode():` 包裹整个诊断区段或移至 `app/api/debug.py` 仅 dev 加载。伴随项:建议同步治理 `/api/openapi.json` 312 端点公开(含 106 admin + 29 auth)。**2026-08-11 14:00 闭环**nginx 边缘层 `location ~ ^/api/(test-|debug/) { return 404; }` 已部署预生产并复测 `/api/test-ping``/api/test-error` 公网 404`/api/health``/api/ready` 仍 200 无误伤。详见 `docs/06-安全审计/01-审计报告/SEC-P0-NEW9-10-调试端点暴露风险确认与应对方案-2026-08-11.md`。代码层根因(移 debug.py 仅非生产加载)待排期 |
| **P0-NEW10** | 后端 debug 端点全清单治理(test-ping/test-error/health/ready/metrics/version/openapi.json | 🆕 新增 | 🆕 | ✅ 已修复 | **2026-08-10 09:00 早班巡检发现**P0-NEW9 仅涉及 `/test-ping` + `/test-error` 两个端点,但 `src/backend/app/main.py` 实际注册的诊断端点全集更广——line 978 `@app.get("/health", tags=["系统"])` + 之前遗漏的 `/ready` / `/metrics` / `/version` + `/api/openapi.json`FastAPI 默认开放,312 端点全公开)。**v1.9.4 公网实测(08-11 09:00**`/api/test-ping` 仍 200 pong + `/api/test-error` 仍 200 返"服务器内部错误" + `/api/openapi.json` 仍 200 OK 424122B。建议批量治理:① `app/api/debug.py` 集中所有诊断端点,仅 dev/staging 加载 ② FastAPI 实例化 `docs_url=None, redoc_url=None, openapi_url=None` 在生产关闭 ③ nginx `location ~ ^/api/(test-|debug/|openapi.json)` return 404 外部。**2026-08-11 14:00 闭环**nginx 边缘层对 `/api/test-ping``/api/test-error``/api/metrics``/api/version` 返回 404(公网复测确认);`/api/health``/api/ready` 保留供探针(仍 200);`/api/openapi.json` 由 P0-NEW11 已闭环。与 P0-NEW11 同源双拦(代码层 app_env 判定 + nginx 兜底)。详见 `docs/06-安全审计/01-审计报告/SEC-P0-NEW9-10-调试端点暴露风险确认与应对方案-2026-08-11.md` |
| **P0-NEW11** | /api/openapi.json 生产公开 312 端点治理(攻击者字典级暴露) | 🆕 新增 | 🆕 | ✅ 已修复 | **2026-08-11 09:00 早班巡检实测**`curl -sI https://itsupport.servyou.com.cn/api/openapi.json` → 200 OK 424122B**完整 API 字典**含 106 admin + 29 auth 端点)。**2026-08-11 已闭环(双拦 + 公网复测证据)**:① 代码层 `create_app()``settings.app_env=="production"` 关闭 `openapi_url`/`docs_url`/`redoc_url`(运行副本 `/opt/wecom-it-desk/app/main.py` + 构建上下文副本双改,`docker restart wecom_it_backend` 生效)② nginx `location = /api/openapi.json` `/api/docs` `/api/redoc` `/api/docs/oauth2-redirect { return 404; }` 兜底。复测:容器内 `/openapi.json`→404、`/docs`→404、`/redoc`→404;公网 `/api/openapi.json`→404、`/api/docs`→404、`/api/redoc`→404;业务 `/api/health``/api/version` 仍 200、误伤验证通过。详见 `deliverables/openapi_exposure_report.md` |
**滴答清单跟踪**6 项 P0 已在滴答清单 `wecom_it_smart_desk` 项目下建任务(标签 `work`)。新增项 T5 (P0-5 打包错误) + T6 (P0-4 实际位置修正) + T7 (P0-NEW7 看板-部署脱节)due 2026-08-06 17:00。
**滴答清单跟踪**P0 系列已在滴答清单 `wecom_it_smart_desk` 项目下建任务(标签 `work`)。**v1.9.4 闭环同步(2026-08-11 14:28 复验)**
- `6a76a80de4b050c704916c07` [P0-NEW9] /api/test-ping 生产暴露调试端点治理(main.py 未走 env 分支)— dida 已 completed 对齐(completedTime 2026-08-11 06:28 UTC,与看板「✅ 已修复」闭环一致)
- T11 = `6a7928a2e4b068980437bb15` [P0-NEW10] debug 端点全清单治理(test-ping/test-error/health/ready/metrics/version/openapi.json)— dida 已 completed 对齐(completedTime 2026-08-11 06:28 UTC,与看板「✅ 已修复」闭环一致)
- `6a7a7ae6e4b068a058339f6e` [P0-NEW11] /api/openapi.json 312 端点治理 — dida 已 completed 对齐(completedTime 2026-08-11 04:24 UTC,与看板闭环一致)
- `6a6bfc2be` [P1 sensitive_words 13 端点补 auth] 看板已完成 → dida 同步 closecompletedTime 2026-08-10 01:25 UTC
**P0-1 闭环**:看板 P0-1 `/itportal/` 500 修复闭环 — nginx.conf line 139-142 已显式注释"/itportal/ 静态前端块已移除 (2026-08-03 fix)",今日公网实测 500→404rewrite cycle 消失)。迁移至"✅ 最近完成"区。
---
## 🟡 P1 重要(待办 / Medium Priority
> v1.9 已无独立 P1 条目(P1-1 已完成,见最近完成区)。如发现新 P1 在此处新增。
| 任务ID | 任务名称 | 类别 | 来源 | 说明 |
|--------|----------|------|------|------|
| **P1-1.1** | 二次清理前端 console.{log,debug,info} 残留 **129 行** | 🆕 看板-现实脱节 | 2026-08-10 早班巡检 | 看板 v1.9.1 P1-1 已关闭「208 行 console 清理」,但 2026-08-08 / 08-10 连续两次巡检实测 `src/frontend-{h5,agent,admin,terminal}/src/` 仍残留 129 行(h5:101 / agent:20 / admin:0 / terminal:8)。**变化点**agent 17→20useScreenCapture/useWebSocket 新增 3 处,源自 5311a52 合并)+ terminal 7→8useWebSocket.ts 新增 1 处)。dida task id = `6a76a802e4b06898041f5215`due 2026-08-12 |
| **P1 治理-1** | 看板-滴答双源同步机制(看板→dida 方向) | 🆕 治理 | 2026-08-08 早班巡检 | 08-08 已自动同步 3 项历史不一致。dida task id = `6a76a809e4b01cac6d3c5ac0`due 2026-08-15 |
| **P1 治理-2** | 看板-滴答双重脱节闭环(dida→看板 方向) | 🆕 治理 | 2026-08-10 早班巡检 | **08-10 双向校验发现 4 处 dida→看板 反向脱节**:① `6a6bfc2be` sensitive_words 13 端点 — 看板已完成,dida 仍 status=0(本次同步 close)② `6a705109` Alembic 迁移脱节 053-057 — dida P1,看板无 ③ `6a70510f` troubleshooting_templates 补 2 索引 — dida P1,看板无 ④ `6a752de4` Nginx /h5/ alias+try_files 潜伏 500 隐患 — dida 风险项,看板无。dida task id = `6a76a809e4b01cac6d3c5ac0` 升级为 P1 治理-2 持续跟进 |
| **P1 治理-3** | 看板-滴答清单反向同步机制(dida→看板) | 🆕 治理 | 2026-08-10 早班巡检 | 发现看板有项未入 dida 的反向脱节。需要把反向同步纳入铁律:早班巡检必须双向同步,dida→看板方向不能漏。后续建议在 `scripts/build_kanban_html.py``--dida-sync` 子命令 |
| **P1-Alembic** | 【dida 补登】治理 Alembic 迁移脱节 — 一次性跑完 053-057 五个迁移(生产 alembic_version 当前=052_diag_queue_quiz | 🆕 看板缺失项 | 2026-08-10 早班巡检反向脱节发现 | dida task id = `6a705109e4b03a0a8b2eca2a`columnId=P1,原 due 2026-08-09 已逾期 1 天。本看板发现前未纳入追踪 |
| **P1-Idx** | 【dida 补登】troubleshooting_templates 补 2 索引 — idx_tpl_category / idx_tpl_activeH5 列表 WHERE category 性能优化) | 🆕 看板缺失项 | 2026-08-10 早班巡检反向脱节发现 | dida task id = `6a70510fe4b0fe2f03126d3c`columnId=P1,原 due 2026-08-07 已逾期 3 天 |
| **P1-治理-sens** | [看板 08-04 已完成 → dida 同步] sensitive_words.py 13 端点补 require_admin | ✅ 看板已完成 | 2026-08-10 早班巡检 | 看板"最近完成"区已有(2026-08-04),但 dida `6a6bfc2be` 仍 status=0;本次同步 closecompletedTime 2026-08-10 01:25 UTC |
> P1-1(看板 v1.9.1 已关闭项)保留作为历史记录,二次治理由 P1-1.1 承担。
---
## 🟢 等用户决策(阻塞项 / Awaiting Decision
> 均超 3 天阈值,需 PM 升级催办
> 均超 3 天阈值,需 PM 升级催办。**v1.9.4 阈值校正**30 天 → **31 天**2026-07-11 → 2026-08-11
| 任务ID | 任务名称 | 阻塞时长 | 卡点 / Owner | 说明 |
|--------|----------|----------|--------------|------|
| **BLK-A** | 企微会议室 Secret | ⚠️ **26**(自 2026-07-11) | 需企业微信管理后台申请 / Owner: 平台组 | 影响会议室预定功能 / `/itterminal/` |
| **BLK-B** | ITSM API 授权 | ⚠️ **26**(自 2026-07-11 | 需向 ITSM 平台方申请 app_id/app_secret / Owner: 平台组 | 影响 ITSM 工单卡片跳转 |
| **BLK-A** | 企微会议室 Secret | ⚠️ **31**(自 2026-07-11) | 需企业微信管理后台申请 / Owner: 平台组 | 影响会议室预定功能 / `/itterminal/` |
| **BLK-B** | ITSM API 授权 | ⚠️ **31**(自 2026-07-11 | 需向 ITSM 平台方申请 app_id/app_secret / Owner: 平台组 | 影响 ITSM 工单卡片跳转 |
**催办机制**:建议 PM 每周一 review(滴答清单循环任务)。
**催办机制**:建议 PM 每周一 review(滴答清单循环任务)。dida task `6a7008e9e4b03a0a8b2870ba` (BLK-A) + `6a7008e9e4b06440c396f6c2` (BLK-B)due 已逾期(2026-08-07)。
---
@@ -108,7 +170,16 @@
| 任务ID | 任务名称 | 优先级 | 负责人 | 完成日期 |
|--------|----------|--------|--------|----------|
| **P0-NEW9** | /api/test-ping 生产暴露调试端点治理(main.py 未走 env 分支) | 🔴 P0 | Duckula | **2026-08-11** | nginx 边缘层 `location ~ ^/api/(test-|debug/) { return 404; }` 已部署预生产并复测 `/api/test-ping``/api/test-error` 公网 404`/api/health``/api/ready` 仍 200 无误伤。dida `6a76a80de4b050c704916c07` completed 对齐。详见 `docs/06-安全审计/01-审计报告/SEC-P0-NEW9-10-调试端点暴露风险确认与应对方案-2026-08-11.md`。代码层根因(移 debug.py 仅非生产加载)待排期 |
| **P0-NEW10** | 后端 debug 端点全清单治理(test-ping/test-error/health/ready/metrics/version/openapi.json | 🔴 P0 | Duckula | **2026-08-11** | nginx 边缘层对 `/api/test-ping``/api/test-error``/api/metrics``/api/version` 返回 404(公网复测确认);`/api/health``/api/ready` 保留供探针(仍 200);`/api/openapi.json` 由 P0-NEW11 已闭环。dida `6a7928a2e4b068980437bb15` completed 对齐。详见同审计报告 |
| **REQ-006** | 预生产测试通道:DEV_MODE + nginx 内网闸门 + 三端测试登录入口 | 🟢 基建 | Duckula | **2026-08-11** | ① 预生产 compose 注入 `DEV_MODE=true``/api/dev/*` 三端 mock 登录(login_source=dev)② nginx `location /api/dev/` 内网闸门(allow 10/8,172.16/12,192.168/16 + deny all)——**公网 403 / 内网 200 / 业务未误伤实测通过** ③ 三端 Login.vue 测试账号登录面板(探测 `/api/dev/health` 决定可见性)④ H5 版本化 v20260808→v20260811(两 go 302 重指)。**已部署预生产并验证全绿**,commit `c1d5dd5` 推送 ds923plus main。文档链:PRD/技术方案/任务说明书/TC/DEPLOY(REQ-通用-006)。自动化测试入口:`GET /api/dev/login?userid=dev-agent-001&role=agent`(仅内网) |
| **PR #6** | task-actions-pivot:坐席端 TaskDetailView 操作区主操作+⋯ 收纳 | 🟡 功能 | Duckula | **2026-08-11** | Gitea PR #6 合并入 main`9fcee9df`),对应 `f1b12b7`PRD-REQ-坐席-011 §6.4 决策 C-8)。**已合并未部署** |
| **P0-1** | `/itportal/` 入口 500 错误(nginx rewrite cycle | 🔴 P0 | Duckula | **2026-08-03** | **v1.9.3 闭环确认**:① nginx.conf line 139-142 明确注释 "/itportal/ 静态前端块已移除 (2026-08-03 fix) ... 现在没有 portal 前端, /itportal/ 直接返回 nginx 404(最长前缀 /itportal/meetingroom/ 仍工作)" ② 看板 v1.9.2 标"待部署"是 08-06 旧观察(当时 /itportal/ 仍 500),实际 08-03 nginx 配置清理已生效 ③ 2026-08-10 09:00 公网实测 `curl /itportal/`**HTTP 404**rewrite cycle 不再发生)。**bug 已修复**P0-1 状态从"待部署" → "✅ 已完成"。仅 /itportal/meetingroom/ 保留作为会议室 API 代理路径 |
| #104 | 运行期结构化日志查看页(筛选 + 下载命中行) | 🔴 P0 | Duckula | 2026-08-07 | **2026-08-07 11:5x jumpserver-V2 生产实测结案**:① 管理页 `curl -sI 127.0.0.1/itadmin/`**200 OK** ② 前端 chunk `assets/RuntimeLogs-CTlWR3Am.js` + `.css` 已部署,grep 命中「下载命中行」「时间范围」「runtime-logs」各 1 处(证伪旧 build 残留)③ 后端 `/app/app/api/runtime_logs.py` 存在、`router.py` 挂载 2 处,`curl /api/admin/runtime-logs` 返回 `{"code":4004,...IP 不在允许范围内}`(端点已挂载,被 #48 IP 白名单中间件按预期拦截容器内直连,非故障)④ 日志源 `/app/logs/wecom-it-desk.log` 4.5MB + 5 轮转备份,JSON 结构化持续写入。决策4(`?download=true` query 参数,PRD-REQ-集成-002 §5.1)源码与部署一致 |
| - | [P0→P3] 复核 /h5/ HTTP 404(看板 v1.9.1 P0-1 已闭环) | 🔴 P0 | Duckula | 2026-08-08 | dida `6a6bfc29e4b0fe2f02dc19b9` 自动 closecompletedTime 2026-08-08 03:52 UTC。H5 实际路由 = `/itdesk/`200 OK),`/h5/` 历史遗留路径无需修复 |
| - | sensitive_words.py 13 端点补 require_admin(看板 08-04 闭环 + dida 08-10 同步 close | 🔴 P0 | Duckula | 2026-08-10 | **v1.9.3 dida 同步闭环**dida `6a6bfc2be4b03a0a8af7f702` 状态 0→2completedTime 2026-08-10 01:25 UTC。08-04 看板已确认线上 13 端点全部带 `Depends(require_admin)` 部署生效,dida 任务此前未同步关闭(看板-滴答脱节),本次早班巡检发现并补同步 |
| - | [P0-2] sensitive_words.py 13 端点补 require_admin | 🔴 P0 | Duckula | 2026-08-08 | dida `6a72c88de4b06bac07c9cc02` 自动 closecompletedTime 2026-08-08 03:52 UTC。看板 08-04 已确认线上 13 端点全部带 `Depends(require_admin)` 部署生效 |
| - | [P1-1] 清理前端 console.log 残留 208 行 | 🟡 P1 | Duckula | 2026-08-08 | dida `6a72c897e4b08d42854aa8f6` 自动 closecompletedTime 2026-08-08 03:52 UTC。**已发现看板-现实脱节**:实测仍残留 128 行(h5:101 / agent:17 / admin:0 / terminal:7),新建 P1-1.1 二次治理任务,dida `6a76a802e4b06898041f5215` due 2026-08-12 |
| - | 【P1】补齐 Nginx 生产安全响应头(复制 7 个 add_header | 🟡 P1 | Duckula | 2026-08-07 | 生产 nginx.conf 注入 7 安全头(server 级 + 各含 add_header 的 location 内,均带 always);nginx -t 通过 + docker restart wecom_it_nginxorigin(127.0.0.1) 与公网(WAF)双验证 7 头全现 |
| - | sensitive_words.py 13 端点补 require_admin | 🔴 P0 | Duckula | 2026-08-04 | router 加 `dependencies=[Depends(require_admin)]`line 47-51),12 端点(敏感词 CRUD/test/reload、隐私正则 CRUD/test、审计日志列表/统计、moderation-config)全覆盖。**看板 v1.8 标"待治理"v1.9 已迁移至已完成**。详见 BUG-通用-004-001 |
| - | 前端 console.log 残留 208 行清理 | 🟡 P1 | Duckula | 2026-08-05 | `rg/find``src/frontend-{h5,agent,admin,terminal,portal}/src/` 下搜不到任何 console.log/warn/debug。**看板 v1.8 标"待清理"v1.9 已迁移至已完成**。下次发版确认 vite 构建 `terser drop_console: true` |
@@ -165,14 +236,16 @@
## 📈 任务统计
- **总任务数**: 101v1.9 = 100 + P0-NEW8 host 文件结构差异
- **已完成**: 94v1.9 = 91 + nginx /docs/ 路由上线 + P1 安全响应头关闭 + #104 运行期日志页结案
- **🔴 P0 必做**: 5P0-1 / 3 / 4 / 5 / 6 / NEW8NEW7 已修复
- **🟡 P1 重要**: 0
- **🟢 等用户决策**: 2BLK-A/B**均超 26 天阈值**
- **总任务数**: 112v1.9.4 111 + REQ-通用-006 预生产测试通道入"已完成"
- **已完成**: 102+2P0-NEW9 / P0-NEW10 调试端点治理闭环;P0-NEW11 已于 08-11 闭环
- **🔴 P0 必做**: 4P0-3 / 4 / 5 / NEW8P0-6 已知噪声、NEW7 / NEW9 / NEW10 / NEW11 已闭环;P0-1 已闭环
- **🟡 P1 重要**: 6P1-1.1 / 治理-1 / 治理-2 / 治理-3 / P1-Alembic / P1-IdxP1-治理-sens 是 dida close 同步不入待办列)
- **🟢 等用户决策**: 2BLK-A/B**均超 31 天阈值**
- **🟠 进行中**: 1#81 v1.2 待排期)
- **🟡 待开始**: 0#104 已于 2026-08-07 生产实测结案,已迁入最近完成区)
- **🟡 待开始**: 0
- **⏸️ 暂停**: 5(安全策略检查平台)
- **⚠️ 风险项**: 2(风险-1 Nginx /h5/ alias+try_files 潜伏 500 隐患 dida `6a752de4` + 风险-2 公网版本停滞 3 天 `/h5/go` 仍 v20260808
- **dida365 同步状态**: 2026-08-10 01:25 UTC 同步 close 1 项(sensitive_words `6a6bfc2be`);2026-08-08 03:52 UTC 同步 close 3 项 + create 3 项;**2026-08-11 午后 新增 PR-6 dida 任务 `6a7a8a69e4b06898044f3530` 并 completed(与看板 PR #6 合并对齐)****14:2x 同步:P0-NEW9 `6a76a80d…` + P0-NEW10 `6a7928a2…` 已 complete(与看板「已修复」对齐)**。**双向同步铁律**:看板→dida 与 dida→看板 均纳入巡检必做项
---
@@ -187,15 +260,24 @@
## 📝 看板版本变更记录
| 版本 | 日期 | 变更内容 | 变更人 |
|------|------|----------|--------|
| v1.9.1 补录2 | 2026-08-07 | ✅ 【P0】#104 运行期结构化日志查看页 **结案**jumpserver-V2 生产实测四项证据齐备(管理页 200 / 前端 chunk 含筛选+下载命中行 / 后端端点已挂载(4004 = #48 IP 白名单预期门禁,非故障)/ 日志源 JSON 持续写入)。从「待开始」区迁入「最近完成」区;待开始 0 项;已完成 93→94。滴答清单同步完成 | Duckula (AI) |
| v1.9.1 补录 | 2026-08-07 | ✅ P1【补齐 Nginx 生产安全响应头】已修复并关闭:生产 nginx.conf 注入 7 安全头(server 级 + 各含 add_header 的 location 内,均带 always),nginx -t 通过 + docker restart wecom_it_nginxorigin 与公网(WAF)双验证通过。补录至最近完成区;已完成 92→93 | Duckula (AI) |
| **v1.9.1-FROZEN** | 2026-08-06 11:50 | ✅ 冻结:P0-NEW7(看板-部署脱节)已修复。nginx `/docs/` 路由上线 + docker-compose.yml docs-public 挂载生效;外部 URL 实测 200。修复期间发现并新增 P0-NEW8host 文件结构 vs git 仓库结构差异)。归档副本 `项目状态看板-v1.9.1-FROZEN.html` | Duckula (AI) |
| **v1.9.0-FROZEN** | 2026-08-06 11:36 | ✅ 冻结:v1.9-DRAFT 内容经 PM 审核通过正式生效;归档副本 `项目状态看板-v1.9.0-FROZEN.html`**修正 v1.8.0 起"对外发布"声明与现实脱节**nginx `/docs/` 路由从未生效,已添加 P0-NEW7 看板-部署脱节);HTML 文件已上传至 jumpserver `/opt/wecom-it-desk/docs-public/kanban/` + `/inspection/2026-08/`,但 nginx 服务不可达(详见 P0-NEW7 | Simon(审核)|
| v1.9.0-DRAFT | 2026-08-06 | 🆕 草稿:① P0 必做 4→5(P0-2 修复迁移至已完成 + P0-5 打包错误 + P0-6 OAuth 信息类 + P0-4 行号描述修正)② P1 重要 1→0(P1-1 清理迁移至已完成)③ 已完成 88→91 ④ 等用户决策 25→26 天阈值 ⑤ 总任务数 96→99 ⑥ 看板顶部版本号自动切换为 draft badge ⑦ 看板与现实脱节条目得到修正 | Duckula (AI) |
| **v1.8.0-FROZEN** | 2026-08-05 19:37 | ✅ 冻结:v1.8-DRAFT 内容经 PM 审核通过正式生效;归档副本 `项目状态看板-v1.8.0-FROZEN.html`;**⚠️ 服务发布声明脱节**(声称发布至 `https://itsupport.servyou.com.cn/docs/kanban/` + `/docs/inspection/` 实际 404——nginx 无 `/docs/` 路由,v1.9 已修正声明并新增 P0-NEW7 治理) | Simon(审核)|
| v1.8-DRAFT | 2026-08-05 | 🆕 草稿:新增 🔴 P0 必做分区(4 项)/ 🟡 P1 重要分区(1 项)/ 🟢 等用户决策分区(2 项,25 天阈值);总任务数 92→96;引入 4 维分类(新增/历史遗留/信息/建议);附 HTML 巡检报告归档路径 | Duckula (AI) |
| v1.7 | 2026-08-03 | #81 状态反转:v1.1 阶段1 已完成(07-28)→ 进行中;新增今日完成项 troubleshooting_templates 5 端点 + /itportal 500 修复;统计 92/87/1/1/5/2 | Duckula (AI) |
| v1.5 | 2026-07-30 | 追加 BUG-用户-003 修复完成项(H5 结束会话失败回归);总任务数 90→91;已完成 84→85;关联 TC-用户-008;待开始不变 | Duckula (AI) |
| v1.4 | 2026-07-24 | 批量完成项入库(批次 1-4 重构 / D1 合并 / 文档重组 / #124 坐席接单按钮 / #125 H5 选项消息) | Simon |
> 模板依据:规范 §4.2 / §8.3 变更记录表(含「变更原因」「影响范围」)
| 版本 | 日期 | 变更内容 | 变更人 | 变更原因 | 影响范围 |
|------|------|----------|--------|----------|----------|
| **v1.9.4-DRAFT 增补(午后 4** | 2026-08-11 | P0-NEW9/NEW10 调试端点闭环(nginx 边缘层 404+ f2fd4fa WIP 已推 Gitea feat 分支 | Duckula (AI) | 13:58 nginx 边缘层治理复测通过;14:1x 波动网络推送成功(本地 CONNECT 代理 + 6 块拆分) | 🔴 P0 6→4;✅ 已完成 100→102feat/task-actions-pivot-v1.8=f2fd4fa 远端落盘 |
| **v1.9.4-DRAFT 增补(午后 3** | 2026-08-11 | P0-NEW11 闭环:应用层关 `openapi_url`/`docs_url`/`redoc_url` + nginx 4 条 `return 404` 双拦,公网复测三者均 404 | Duckula (AI) | 早班巡检确认公网 `/api/openapi.json` 312 端点暴露,修复后复测通过 | 🔴 P0 7→6;✅ 已完成 99→100;滴答 NEW11 completed |
| **v1.9.4-DRAFT 增补(午后)** | 2026-08-11 | PR #6 合并入 maintask-actions-pivot / 坐席端 TaskDetailView 操作区收纳) | Duckula (AI) | Gitea PR #6 经评审合并(绕过单用户审批门禁 405) | ✅ 已完成 98→99;总任务 110→111;仍 v20260808(未部署) |
| **v1.9.4-DRAFT 增补(午后 2** | 2026-08-11 | REQ-通用-006 预生产测试通道上线(DEV_MODE + nginx 内网闸门 + 三端测试登录) | Duckula (AI) | 三端企微 OAuth 无法自动化测试痛点闭环 | ✅ 已完成 99→100;总任务 111→112 |
| **v1.9.4-DRAFT** | 2026-08-11 | 早班巡检 5 项关键发现(版本停滞 / NEW9-10 暴露 / 风险 /h5/ / P0-3 / BLK 阈值) | Duckula (AI) | 2026-08-11 09:00 早班巡检实测 | 🔴 P0 6→7;⚠️ 风险项 1→2;总任务 109→110;新增 P0-NEW11 + 风险-2 |
| **v1.9.3-DRAFT** | 2026-08-10 | 早班巡检 4 项发现(P0-1 闭环 / dida 反向脱节 / console 129 / BLK 阈值) | Duckula (AI) | 2026-08-10 09:00 早班巡检 | 🔴 P0 6;🟡 P1 2→5;✅ 已完成 97→98;⚠️ 风险项 0→1 |
| v1.9.2-DRAFT | 2026-08-08 | dida365 自动同步 6 项(close 3 / create 3+ 新增 P0-NEW9 | Duckula (AI) | 2026-08-08 03:52 UTC dida 同步 | 🔴 P0 5→6;🟡 P1 0→2;✅ 已完成 94→97 |
| v1.9.1 补录2 | 2026-08-07 | #104 运行期结构化日志查看页结案 | Duckula (AI) | 2026-08-07 jumpserver 生产实测结案 | 待开始 0;已完成 93→94 |
| v1.9.1 补录 | 2026-08-07 | P1 补齐 Nginx 生产安全响应头(7 头) | Duckula (AI) | 2026-08-07 安全巡检修复 | 已完成 92→93 |
| **v1.9.1-FROZEN** | 2026-08-06 | 冻结(P0-NEW7 看板-部署脱节已修复) | Simon(审核) | PM 审核通过 | 归档 `项目状态看板-v1.9.1-FROZEN.html` |
| **v1.9.0-FROZEN** | 2026-08-06 | 冻结 v1.9-DRAFT(修正服务发布声明脱节) | Simon(审核) | PM 审核通过 | 归档 v1.9.0-FROZEN;新增 P0-NEW8 |
| v1.9.0-DRAFT | 2026-08-06 | 草稿(P0 必做 / P1 重要 / 等决策分区重构 + 计数) | Duckula (AI) | 看板结构升级 | 总任务数 96→99 |
| **v1.8.0-FROZEN** | 2026-08-05 | 冻结 v1.8-DRAFT | Simon(审核) | PM 审核通过 | 归档 v1.8.0-FROZEN(声明脱节待 P0-NEW7 修正) |
| v1.8-DRAFT | 2026-08-05 | 草稿(新增 3 分区 + 4 维分类) | Duckula (AI) | 看板分区重构 | 总任务数 92→96 |
| v1.7 | 2026-08-03 | #81 状态反转 + 今日完成项入库 | Duckula (AI) | 阶段状态更新 | 统计 92/87/1/1/5/2 |
| v1.5 | 2026-07-30 | BUG-用户-003 修复完成 | Duckula (AI) | 2026-07-30 修复 | 总任务 90→91;已完成 84→85 |
| v1.4 | 2026-07-24 | 批量完成项入库(批次1-4 / D1 / 文档重组 / #124 / #125 | Simon | 2026-07-24 收编 | 多任务迁入完成区 |
+389 -22
View File
@@ -10,12 +10,15 @@
# /itdesk/ → H5 员工端静态文件
# /itagent/ → 坐席工作台静态文件
# /itadmin/ → 管理后台静态文件
# /itportal/ → 统一入口(角色选择)静态文件
# /itterminal/ → 小鱼终端大屏静态文件
# /itportal/meetingroom/ → 会议室API(终端+H5共用,代理到后端)
# /api/ → 后端 FastAPI(容器名 backend:8000
# /ws/ → WebSocket(容器名 backend:8000,支持升级)
# / → IT 数据查询平台(远程主机)
#
# 2026-08-03 fix:
# - /itportal/ 静态前端块已移除(portal 源码在 commit bea288e4 已删除,dist 不存在导致 500)
# - 仅保留 /itportal/meetingroom/ API 块
# =============================================================================
events {
@@ -133,18 +136,51 @@ http {
}
# ------------------------------------------------------------------
# 统一入口 Portal — /itportal/
# ⚠️ /itportal/ 静态前端块已移除 (2026-08-03 fix)
# 历史: portal 源码 frontend-portal/ 在 commit bea288e4 (2026-07-11) 已删除,
# dist 目录不存在, alias 指向空目录导致 try_files 全部 500。
# 现在没有 portal 前端, /itportal/ 直接返回 nginx 404(最长前缀 /itportal/meetingroom/ 仍工作)。
# 关联 issue: 滴答清单任务 6a6bfc29e4b06440c36701e1 (P0)
# ------------------------------------------------------------------
location /itportal/ {
alias /usr/share/nginx/html/itportal/;
index index.html;
try_files $uri /itportal/index.html;
}
# ------------------------------------------------------------------
# 后端 API — /api/
# ------------------------------------------------------------------
location /api/ {
# ------------------------------------------------------------------
# P0-NEW11 安全加固:禁止公网访问 API 文档端点(字典级暴露止血)
# 仅对精确路径返回 404;业务 /api/ 其余路径不受影响
# ------------------------------------------------------------------
location = /api/openapi.json { return 404; }
location = /api/docs { return 404; }
location = /api/redoc { return 404; }
location = /api/docs/oauth2-redirect { return 404; }
# ============================================================
# 预生产测试通道 (REQ-通用-006) - nginx 内网闸门
# 仅内网可访问 /api/dev/*,公网 403。部署后须双向 curl 验证
# ============================================================
location /api/dev/ {
allow 10.0.0.0/8;
allow 172.16.0.0/12;
allow 192.168.0.0/16;
deny all;
proxy_pass http://backend_api/;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
# ------------------------------------------------------------------
# P0-NEW9/10 安全加固:禁止公网访问调试/诊断端点
# 与 P0-NEW11 同源治理:nginx 边缘层兜底
# 仅 test-/debug- 与 metrics/version 返回 404/health、/ready 保留供探针
# ------------------------------------------------------------------
location ~ ^/api/(test-|debug/) { return 404; }
location = /api/metrics { return 404; }
location = /api/version { return 404; }
location /api/ {
proxy_pass http://backend_api/;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
@@ -157,13 +193,190 @@ http {
# ------------------------------------------------------------------
# H5 用户端 — /h5/
# 说明:H5 是静态前端应用,必须配置为静态文件服务(alias)!
# ⚠️ 禁止改为 proxy_pass,否则返回 404(后端无 /h5/ 路由)
# 关联 CASECASE-20260714-02 / CASE-20260716-01
# ------------------------------------------------------------------
location /h5/ {
proxy_pass http://backend_api/;
# === 2026-08-06 WAF path-cache bypass: versioned entry ===
# 精确匹配 /h5(无斜杠)→ 302 到 /h5/go,防 404
location = /h5 {
return 302 /h5/go;
}
location = /h5/go {
add_header Cache-Control "no-store" always;
return 302 /h5/v20260811/$is_args$args;
add_header Strict-Transport-Security "max-age=31536000" always;
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-XSS-Protection "1; mode=block" always;
add_header Referrer-Policy "no-referrer-when-downgrade" always;
add_header Permissions-Policy "geolocation=(), microphone=(), camera=()" always;
add_header Cross-Origin-Opener-Policy "same-origin" always;
add_header X-Content-Type-Options "nosniff" always;
}
# === /itservice/ 前缀(替换 /h5/,绕开 WAF 旧缓存) ===
location = /itservice/go {
add_header Cache-Control "no-store" always;
return 302 /itservice/v20260811/$is_args$args;
add_header Strict-Transport-Security "max-age=31536000" always;
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-XSS-Protection "1; mode=block" always;
add_header Referrer-Policy "no-referrer-when-downgrade" always;
add_header Permissions-Policy "geolocation=(), microphone=(), camera=()" always;
add_header Cross-Origin-Opener-Policy "same-origin" always;
add_header X-Content-Type-Options "nosniff" always;
}
location /itservice/v20260807f/ {
alias /usr/share/nginx/html/h5/;
index index.html;
try_files $uri /itservice/v20260807f/index.html;
add_header Cache-Control "no-store" always;
add_header Strict-Transport-Security "max-age=31536000" always;
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-XSS-Protection "1; mode=block" always;
add_header Referrer-Policy "no-referrer-when-downgrade" always;
add_header Permissions-Policy "geolocation=(), microphone=(), camera=()" always;
add_header Cross-Origin-Opener-Policy "same-origin" always;
add_header X-Content-Type-Options "nosniff" always;
}
location /itservice/v20260808/ {
alias /usr/share/nginx/html/h5/;
index index.html;
try_files $uri /itservice/v20260808/index.html;
add_header Cache-Control "no-store" always;
add_header Strict-Transport-Security "max-age=31536000" always;
add_header X-Frame-Options "SAMEORIGIN" always;
}
location /itservice/v20260811/ {
alias /usr/share/nginx/html/h5/;
index index.html;
try_files $uri /itservice/v20260811/index.html;
add_header Cache-Control "no-store" always;
add_header Strict-Transport-Security "max-age=31536000" always;
add_header X-Frame-Options "SAMEORIGIN" always;
}
location /itservice/ {
alias /usr/share/nginx/html/h5/;
index index.html;
try_files $uri $uri/ /itservice/index.html =404;
add_header Cache-Control "no-store" always;
add_header Strict-Transport-Security "max-age=31536000" always;
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-XSS-Protection "1; mode=block" always;
add_header Referrer-Policy "no-referrer-when-downgrade" always;
add_header Permissions-Policy "geolocation=(), microphone=(), camera=()" always;
add_header Cross-Origin-Opener-Policy "same-origin" always;
add_header X-Content-Type-Options "nosniff" always;
}
location = /itservice {
return 302 /itservice/go;
}
location /h5/v20260807f/ {
alias /usr/share/nginx/html/h5/;
index index.html;
try_files $uri /h5/v20260807f/index.html;
add_header Cache-Control "no-store" always;
add_header Strict-Transport-Security "max-age=31536000" always;
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-XSS-Protection "1; mode=block" always;
add_header Referrer-Policy "no-referrer-when-downgrade" always;
add_header Permissions-Policy "geolocation=(), microphone=(), camera=()" always;
add_header Cross-Origin-Opener-Policy "same-origin" always;
add_header X-Content-Type-Options "nosniff" always;
}
location /h5/v20260808/ {
alias /usr/share/nginx/html/h5/;
index index.html;
try_files $uri /h5/v20260808/index.html;
add_header Cache-Control "no-store" always;
add_header Strict-Transport-Security "max-age=31536000" always;
add_header X-Frame-Options "SAMEORIGIN" always;
}
location /h5/v20260811/ {
alias /usr/share/nginx/html/h5/;
index index.html;
try_files $uri /h5/v20260811/index.html;
add_header Cache-Control "no-store" always;
add_header Strict-Transport-Security "max-age=31536000" always;
add_header X-Frame-Options "SAMEORIGIN" always;
}
location /h5/ {
alias /usr/share/nginx/html/h5/;
index index.html;
try_files $uri $uri/ /h5/index.html =404;
add_header Cache-Control "no-cache, must-revalidate" always;
add_header Strict-Transport-Security "max-age=31536000" always;
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-XSS-Protection "1; mode=block" always;
add_header Referrer-Policy "no-referrer-when-downgrade" always;
add_header Permissions-Policy "geolocation=(), microphone=(), camera=()" always;
add_header Cross-Origin-Opener-Policy "same-origin" always;
add_header X-Content-Type-Options "nosniff" always;
}
# ------------------------------------------------------------------
# 静态媒体文件 — /media/ (企微下载的图片/H5上传的文件)
# 代理到后端 /api/media/ 接口(容器内路径 /app/uploads/
# ------------------------------------------------------------------
location /media/ {
proxy_pass http://backend_api/media/;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
expires 30d;
add_header Cache-Control "public, immutable";
add_header Strict-Transport-Security "max-age=31536000" always;
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-XSS-Protection "1; mode=block" always;
add_header Referrer-Policy "no-referrer-when-downgrade" always;
add_header Permissions-Policy "geolocation=(), microphone=(), camera=()" always;
add_header Cross-Origin-Opener-Policy "same-origin" always;
add_header X-Content-Type-Options "nosniff" always;
}
# ------------------------------------------------------------------
@@ -193,6 +406,38 @@ http {
proxy_send_timeout 60s;
proxy_read_timeout 60s;
}
# ------------------------------------------------------------------
# 看板 / 巡检 HTML 静态目录 — /docs/
# 2026-08-06 P0-NEW7 修复(看板-部署脱节):
# - 来源:宿主 /opt/wecom-it-desk/docs-public/(由 docker-compose.yml 挂载)
# - 子目录:kanban/ 项目状态看板;inspection/YYYY-MM/ 巡检报告
# - charset utf-8 解决中文文件名编码
# - 不缓存 HTML,确保看板/报告更新后用户立即看到
# 关联:看板 v1.9.0-FROZEN 顶部服务发布策略 + scripts/deploy_kanban_to_jumpserver.sh
# ------------------------------------------------------------------
location /docs/ {
alias /opt/wecom-it-desk/docs-public/;
charset utf-8;
add_header Cache-Control "no-cache, must-revalidate" always;
add_header Content-Type "text/html; charset=utf-8" always;
try_files $uri $uri/ $uri.html =404;
autoindex off;
add_header Strict-Transport-Security "max-age=31536000" always;
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-XSS-Protection "1; mode=block" always;
add_header Referrer-Policy "no-referrer-when-downgrade" always;
add_header Permissions-Policy "geolocation=(), microphone=(), camera=()" always;
add_header Cross-Origin-Opener-Policy "same-origin" always;
add_header X-Content-Type-Options "nosniff" always;
}
add_header Strict-Transport-Security "max-age=31536000" always;
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-XSS-Protection "1; mode=block" always;
add_header Referrer-Policy "no-referrer-when-downgrade" always;
add_header Permissions-Policy "geolocation=(), microphone=(), camera=()" always;
add_header Cross-Origin-Opener-Policy "same-origin" always;
add_header X-Content-Type-Options "nosniff" always;
}
# =================================================================
@@ -209,6 +454,13 @@ http {
access_log off;
return 200 "healthy\n";
add_header Content-Type text/plain;
add_header Strict-Transport-Security "max-age=31536000" always;
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-XSS-Protection "1; mode=block" always;
add_header Referrer-Policy "no-referrer-when-downgrade" always;
add_header Permissions-Policy "geolocation=(), microphone=(), camera=()" always;
add_header Cross-Origin-Opener-Policy "same-origin" always;
add_header X-Content-Type-Options "nosniff" always;
}
# ------------------------------------------------------------------
@@ -266,17 +518,25 @@ http {
}
# ------------------------------------------------------------------
# 统一入口 Portal — /itportal/
# ⚠️ /itportal/ 静态前端块已移除 (2026-08-03 fix)
# 历史: portal 源码 frontend-portal/ 在 commit bea288e4 (2026-07-11) 已删除,
# dist 目录不存在, alias 指向空目录导致 try_files 全部 500。
# 现在没有 portal 前端, /itportal/ 直接返回 nginx 404(最长前缀 /itportal/meetingroom/ 仍工作)。
# 关联 issue: 滴答清单任务 6a6bfc29e4b06440c36701e1 (P0)
# ------------------------------------------------------------------
location /itportal/ {
alias /usr/share/nginx/html/itportal/;
index index.html;
try_files $uri /itportal/index.html;
}
# ------------------------------------------------------------------
# 后端 API — /api/
# ------------------------------------------------------------------
# ------------------------------------------------------------------
# P0-NEW9/10 安全加固:禁止公网访问调试/诊断端点
# 与 P0-NEW11 同源治理:nginx 边缘层兜底
# 仅 test-/debug- 与 metrics/version 返回 404/health、/ready 保留供探针
# ------------------------------------------------------------------
location ~ ^/api/(test-|debug/) { return 404; }
location = /api/metrics { return 404; }
location = /api/version { return 404; }
location /api/ {
proxy_pass http://backend_api/;
proxy_set_header Host $host;
@@ -292,13 +552,93 @@ http {
# ------------------------------------------------------------------
# H5 用户端 — /h5/
# 说明:H5 是静态前端应用,必须配置为静态文件服务(alias)!
# ⚠️ 禁止改为 proxy_pass,否则返回 404(后端无 /h5/ 路由)
# 关联 CASECASE-20260714-02 / CASE-20260716-01
# ------------------------------------------------------------------
location /h5/ {
proxy_pass http://backend_api/;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
# === 2026-08-06 WAF path-cache bypass: versioned entry ===
# 精确匹配 /h5(无斜杠)→ 302 到 /h5/go,防 404
location = /h5 {
return 302 /h5/go;
}
location = /h5/go {
add_header Cache-Control "no-store" always;
return 302 /h5/v20260808/$is_args$args;
add_header Strict-Transport-Security "max-age=31536000" always;
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-XSS-Protection "1; mode=block" always;
add_header Referrer-Policy "no-referrer-when-downgrade" always;
add_header Permissions-Policy "geolocation=(), microphone=(), camera=()" always;
add_header Cross-Origin-Opener-Policy "same-origin" always;
add_header X-Content-Type-Options "nosniff" always;
}
location /itservice/v20260808/ {
alias /usr/share/nginx/html/h5/;
index index.html;
try_files $uri /itservice/v20260808/index.html;
add_header Cache-Control "no-store" always;
add_header Strict-Transport-Security "max-age=31536000" always;
add_header X-Frame-Options "SAMEORIGIN" always;
}
location /itservice/ {
alias /usr/share/nginx/html/h5/;
index index.html;
try_files $uri $uri/ /itservice/index.html =404;
add_header Cache-Control "no-store" always;
add_header Strict-Transport-Security "max-age=31536000" always;
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-XSS-Protection "1; mode=block" always;
add_header Referrer-Policy "no-referrer-when-downgrade" always;
add_header Permissions-Policy "geolocation=(), microphone=(), camera=()" always;
add_header Cross-Origin-Opener-Policy "same-origin" always;
add_header X-Content-Type-Options "nosniff" always;
}
location = /itservice {
return 302 /itservice/go;
}
location /h5/v20260808/ {
alias /usr/share/nginx/html/h5/;
index index.html;
try_files $uri /h5/v20260808/index.html;
add_header Cache-Control "no-store" always;
add_header Strict-Transport-Security "max-age=31536000" always;
add_header X-Frame-Options "SAMEORIGIN" always;
}
location /h5/ {
alias /usr/share/nginx/html/h5/;
index index.html;
try_files $uri $uri/ /h5/index.html =404;
add_header Cache-Control "no-cache, must-revalidate" always;
add_header Strict-Transport-Security "max-age=31536000" always;
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-XSS-Protection "1; mode=block" always;
add_header Referrer-Policy "no-referrer-when-downgrade" always;
add_header Permissions-Policy "geolocation=(), microphone=(), camera=()" always;
add_header Cross-Origin-Opener-Policy "same-origin" always;
add_header X-Content-Type-Options "nosniff" always;
}
# ------------------------------------------------------------------
@@ -337,5 +677,32 @@ http {
proxy_send_timeout 60s;
proxy_read_timeout 60s;
}
# ------------------------------------------------------------------
# 看板 / 巡检 HTML 静态目录 — /docs/(备用 server 也支持)
# 2026-08-06 P0-NEW7 修复:与主 server 一致
# ------------------------------------------------------------------
location /docs/ {
alias /opt/wecom-it-desk/docs-public/;
charset utf-8;
add_header Cache-Control "no-cache, must-revalidate" always;
add_header Content-Type "text/html; charset=utf-8" always;
try_files $uri $uri/ $uri.html =404;
autoindex off;
add_header Strict-Transport-Security "max-age=31536000" always;
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-XSS-Protection "1; mode=block" always;
add_header Referrer-Policy "no-referrer-when-downgrade" always;
add_header Permissions-Policy "geolocation=(), microphone=(), camera=()" always;
add_header Cross-Origin-Opener-Policy "same-origin" always;
add_header X-Content-Type-Options "nosniff" always;
}
add_header Strict-Transport-Security "max-age=31536000" always;
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-XSS-Protection "1; mode=block" always;
add_header Referrer-Policy "no-referrer-when-downgrade" always;
add_header Permissions-Policy "geolocation=(), microphone=(), camera=()" always;
add_header Cross-Origin-Opener-Policy "same-origin" always;
add_header X-Content-Type-Options "nosniff" always;
}
}
+161
View File
@@ -0,0 +1,161 @@
#!/bin/bash
# =============================================================================
# deploy_kanban_to_jumpserver.sh — 项目状态看板 HTML 发布到 jumpserver
# =============================================================================
# 流程:
# 1. 跑 build_kanban_html.py 生成 docs/07-项目管理/项目状态看板.html
# 2. 调用 jumpserver-V2 skill 的 v2_ops.py upload 上传到 /tmp/
# 3. 用 v2_ops.py exec 移动到 nginx 静态目录(容器外 or 容器内)
# 4. (可选)触发 nginx reload(仅当挂载点在容器外时)
#
# 用法:
# bash scripts/deploy_kanban_to_jumpserver.sh # 默认发布
# bash scripts/deploy_kanban_to_jumpserver.sh --dry-run # 仅生成不上传
# bash scripts/deploy_kanban_to_jumpserver.sh --no-build # 跳过生成(用现有 HTML)
#
# 前置:
# - jumpserver-V2 skill 已登录(v2_ops.py status 返回有效)
# - jumpserver 上有 nginx 静态目录 /opt/wecom-it-desk/docs-public/kanban/
# - jumpserver 上有 nginx 路由 /kanban/ → 该目录(见 nginx 配置建议)
#
# ⚠️ 本脚本默认不修改 nginx 配置(避免误改生产)。如需启用 /kanban/ 路由,
# 请手动编辑 nginx 配置文件(见末尾附录)后 reload。
# =============================================================================
set -e
# 颜色
RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m'; BLUE='\033[0;34m'; NC='\033[0m'
info() { echo -e "${BLUE}[INFO]${NC} $1"; }
ok() { echo -e "${GREEN}[OK]${NC} $1"; }
warn() { echo -e "${YELLOW}[WARN]${NC} $1"; }
error() { echo -e "${RED}[ERROR]${NC} $1"; exit 1; }
# 路径常量
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
MD_FILE="$PROJECT_ROOT/docs/07-项目管理/项目状态看板.md"
HTML_FILE="$PROJECT_ROOT/docs/07-项目管理/项目状态看板.html"
HTML_NAME="项目状态看板.html"
# jumpserver-V2 skill
V2_OPS="C:\Users\simon\.workbuddy\skills\jumpserver-V2\scripts\v2_ops.py"
PYTHON_BIN="C:\Users\simon\.workbuddy\binaries\python\versions\3.13.12\python.exe"
# 服务器路径
REMOTE_TMP="/tmp/kanban_$(date +%Y%m%d%H%M%S).html"
REMOTE_FINAL_DIR="/opt/wecom-it-desk/docs-public/kanban"
REMOTE_FINAL_FILE="$REMOTE_FINAL_DIR/项目状态看板.html"
# 参数
DRY_RUN=false
SKIP_BUILD=false
for arg in "$@"; do
case "$arg" in
--dry-run) DRY_RUN=true ;;
--no-build) SKIP_BUILD=true ;;
*) error "未知参数:$arg" ;;
esac
done
# ============================================================
# Step 1: 生成 HTML
# ============================================================
if [ "$SKIP_BUILD" = false ]; then
info "[1/4] 生成 HTML..."
# ⚠️ Windows Python 不认 Git Bash 的 /d/... 路径(会解析成 D:\d\...),须 cygpath -w 转换
BUILD_PY="$(cygpath -w "$SCRIPT_DIR/build_kanban_html.py" 2>/dev/null || echo "$SCRIPT_DIR/build_kanban_html.py")"
"$PYTHON_BIN" "$BUILD_PY" || error "生成失败"
ok "HTML 已生成"
else
info "[1/4] 跳过生成(使用现有 $HTML_FILE"
fi
[ -f "$HTML_FILE" ] || error "HTML 文件不存在:$HTML_FILE"
# ============================================================
# Step 2: jumpserver 会话状态
# ============================================================
info "[2/4] 检查 jumpserver 会话..."
"$PYTHON_BIN" "$V2_OPS" status 2>&1 | grep -E "✅|❌" || error "jumpserver 会话无效,请先 login"
# ============================================================
# Step 3: 上传到 jumpserver /tmp/
# ============================================================
# ⚠️ 路径铁律(2026-08-07 修复):
# ① v2_ops.py 是 Windows Python,收不到 Git Bash 的 /d/... 形式路径
# (会被误解析成 \d\... 而报「本地文件不存在」)
# ② psftp 通道要求本地源路径 **纯 ASCII**,而本仓库路径含中文
# → 故先把 HTML 复制到 ASCII 临时目录,再用 Windows 形式路径调用 upload
STAMP="$(date +%Y%m%d%H%M%S)"
ASCII_TMP_DIR="$(cygpath -u "${LOCALAPPDATA:-C:/Users/$USER/AppData/Local}" 2>/dev/null || echo "/c/Users/$USER/AppData/Local")/Temp"
ASCII_TMP_FILE="$ASCII_TMP_DIR/kanban_$STAMP.html"
REMOTE_TMP="/tmp/kanban_$STAMP.html"
info "[3/4] 上传 $HTML_NAME$REMOTE_TMP..."
if [ "$DRY_RUN" = true ]; then
info "(dry-run 跳过)"
else
mkdir -p "$ASCII_TMP_DIR"
cp "$HTML_FILE" "$ASCII_TMP_FILE" || error "复制到 ASCII 临时路径失败"
ASCII_TMP_WIN="$(cygpath -w "$ASCII_TMP_FILE" 2>/dev/null || echo "$ASCII_TMP_FILE")"
# v2_ops.py upload <local> [remote]remote 是远端 basename
"$PYTHON_BIN" "$V2_OPS" upload "$ASCII_TMP_WIN" "kanban_$STAMP.html" \
|| error "上传失败"
rm -f "$ASCII_TMP_FILE"
ok "上传完成"
fi
# ============================================================
# Step 4: 移动到最终目录 + 设置权限
# ============================================================
info "[4/4] 移动到 $REMOTE_FINAL_DIR/ 并设置权限..."
if [ "$DRY_RUN" = true ]; then
info "(dry-run 跳过)"
else
CMD=$(cat <<EOF
mkdir -p $REMOTE_FINAL_DIR && \
cp "$REMOTE_TMP" "$REMOTE_FINAL_FILE" && \
chmod 644 "$REMOTE_FINAL_FILE" && \
rm -f $REMOTE_TMP && \
echo "[OK] $REMOTE_FINAL_FILE" && \
ls -la $REMOTE_FINAL_DIR/
EOF
)
"$PYTHON_BIN" "$V2_OPS" exec "$CMD" || error "移动失败"
ok "已部署到:$REMOTE_FINAL_FILE"
fi
echo ""
ok "========== 发布完成 =========="
echo ""
info "📍 访问 URL(前提:nginx 已加 /docs/kanban/ 路由)"
info " https://itsupport.servyou.com.cn/docs/kanban/项目状态看板.html"
echo ""
warn "⚠️ nginx 路由配置(需手动添加)"
cat <<'NGINX_HINT'
# 在 /etc/nginx/nginx.conf 的 server { } 块内添加:
location /docs/kanban/ {
alias /opt/wecom-it-desk/docs-public/kanban/;
charset utf-8;
add_header Cache-Control "no-cache, must-revalidate";
try_files $uri $uri/ $uri.html =404;
}
# 同时添加巡检报告路由(如选 C 全部发布):
location /docs/inspection/ {
alias /opt/wecom-it-desk/docs-public/inspection/;
charset utf-8;
add_header Cache-Control "no-cache, must-revalidate";
try_files $uri $uri/ $uri.html =404;
}
# 然后执行:
docker exec wecom_it_nginx nginx -t
docker compose -f /opt/wecom-it-desk/docker-compose.yml restart nginx
# 或 reload(配置挂载到容器内时):
docker exec wecom_it_nginx nginx -s reload
NGINX_HINT
echo ""
info "🔁 重新发布:直接重跑本脚本即可(脚本会自动覆盖)"
+103
View File
@@ -46,6 +46,18 @@
</p>
</div>
<!-- 测试账号登录REQ-通用-006 预生产测试通道仅内网探测成功时显示 -->
<div v-if="showTestPanel" class="test-login">
<el-divider><span class="test-login-divider-text">测试账号登录</span></el-divider>
<div class="test-login-row">
<el-select v-model="testUser" size="small" placeholder="选择测试角色" style="width: 180px">
<el-option v-for="u in testUsers" :key="u.userid" :label="u.label" :value="u.userid" />
</el-select>
<el-button type="primary" size="small" :loading="testLogining" @click="testLogin">免企微登录</el-button>
</div>
<p class="test-login-hint">仅内网可用用于自动化测试</p>
</div>
<!-- 加载状态 -->
<div v-if="loading" class="loading-container">
<el-icon class="is-loading" size="32"><Loading /></el-icon>
@@ -94,6 +106,26 @@ let pollTimer: ReturnType<typeof setInterval> | null = null
/** 当前扫码登录票据 */
let currentTicket = ''
// --------------------------------------------------------------------------
// 测试通道状态(REQ-通用-006/api/dev/* 仅内网可用,公网 403 则隐藏)
// --------------------------------------------------------------------------
/** 是否显示测试账号登录面板 */
const showTestPanel = ref(false)
/** 测试登录中 */
const testLogining = ref(false)
/** 选中的测试账号 userid */
const testUser = ref('dev-admin-001')
/** 预设测试账号(与后端 PRESET_DEV_USERS 对应) */
const testUsers = [
{ userid: 'dev-user-001', label: '员工(user', role: 'user' },
{ userid: 'dev-agent-001', label: '坐席(agent', role: 'agent' },
{ userid: 'dev-admin-001', label: '管理员(admin', role: 'admin' },
]
// --------------------------------------------------------------------------
// 方法
// --------------------------------------------------------------------------
@@ -293,11 +325,58 @@ async function redirectToOAuth(): Promise<void> {
window.location.href = oauthUrl
}
// --------------------------------------------------------------------------
// 测试通道方法(REQ-通用-006
// --------------------------------------------------------------------------
/**
* 探测测试通道可用性(内网 200 → 显示测试入口;公网 403 → 隐藏)
*/
async function checkTestChannel(): Promise<void> {
try {
const apiClient = (await import('@/api/index')).default
const resp = await apiClient.get('/dev/health', { timeout: 3000 })
if (resp?.data?.data?.dev_mode) {
showTestPanel.value = true
}
} catch {
showTestPanel.value = false
}
}
/**
* 测试账号免企微登录(仅内网):/api/dev/login → token 写入 admin_token
*/
async function testLogin(): Promise<void> {
const target = testUsers.find((u) => u.userid === testUser.value)
if (!target) return
testLogining.value = true
try {
const apiClient = (await import('@/api/index')).default
const resp = await apiClient.get('/dev/login', {
params: { userid: target.userid, name: target.label, role: target.role },
})
await handleLoginSuccess(resp.data.data.token, {
userid: target.userid,
name: target.label,
})
} catch (error) {
console.error('测试登录失败:', error)
ElMessage.error('测试登录失败,请确认内网环境')
} finally {
testLogining.value = false
}
}
// --------------------------------------------------------------------------
// 生命周期
// --------------------------------------------------------------------------
onMounted(async () => {
// 探测测试通道(不阻塞主登录流程)
checkTestChannel()
// 检查 URL 中的 code 参数(OAuth2 回调)
const urlParams = new URLSearchParams(window.location.search)
const code = urlParams.get('code')
@@ -467,4 +546,28 @@ onUnmounted(() => {
padding: 40px 0;
color: var(--text-secondary);
}
/* 测试账号登录(REQ-通用-006 */
.test-login {
margin-top: 8px;
}
.test-login-divider-text {
font-size: 12px;
color: var(--text-tertiary);
}
.test-login-row {
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
}
.test-login-hint {
text-align: center;
font-size: 12px;
color: var(--text-tertiary);
margin: 8px 0 0;
}
</style>
@@ -84,29 +84,63 @@
<div class="apv-action-hint">
审批动作需由审批人本人在企微审批中完成点击下方按钮将在新标签页打开该审批单
</div>
<!-- ================================================================== -->
<!-- 底部操作按钮 PRD-REQ-坐席-011 §6.4 决策 C-8主操作 + 收纳 -->
<!-- ================================================================== -->
<div class="tic-actions">
<!-- 通过/拒绝/转交三个动作共用同一个深链仅作为跳转入口 -->
<!-- 主操作按状态驱动sp_status=1审批通过 / 其他在企微审批中打开 -->
<a
v-for="item in approvalActions"
:key="item.action"
class="tic-action-btn"
:href="wecomApprovalUrl"
:class="`tic-action-${mainAction.variant}`"
:href="mainAction.href"
:disabled="mainAction.disabled"
:title="mainAction.tooltip || mainAction.label"
target="_blank"
rel="noopener noreferrer"
@click="handleApprovalAction(item.action)"
@click="handleApprovalAction(mainAction.key)"
>
{{ item.label }}
{{ mainAction.label }}
</a>
<a
class="tic-action-btn tic-action-primary"
:href="wecomApprovalUrl"
target="_blank"
rel="noopener noreferrer"
@click="handleApprovalAction('open')"
>
🔗 在企微审批中打开
</a>
<!-- 次要动作收纳按钮 sp_status=1 显示拒绝/转交/加签 -->
<div class="task-actions-menu-wrapper" v-if="moreActions.length > 0">
<button
class="tic-action-btn tic-action-more-btn"
type="button"
:aria-expanded="isMoreMenuOpen"
aria-haspopup="menu"
aria-label="更多操作"
title="更多操作"
@click="toggleMoreMenu"
>
</button>
<ul v-if="isMoreMenuOpen" class="task-actions-menu" role="menu">
<li
v-for="item in moreActions"
:key="item.key"
class="task-actions-menu__item"
:class="{ 'is-danger': item.danger, 'is-disabled': item.disabled }"
role="menuitem"
:title="item.tooltip || item.label"
>
<a
v-if="item.href && !item.disabled"
:href="item.href"
target="_blank"
rel="noopener noreferrer"
@click="handleApprovalAction(item.key); closeMoreMenu()"
>
<span class="task-actions-menu__icon">{{ item.icon }}</span>
<span>{{ item.label }}</span>
</a>
<span v-else class="task-actions-menu__label-disabled">
<span class="task-actions-menu__icon">{{ item.icon }}</span>
<span>{{ item.label }}</span>
</span>
</li>
</ul>
</div>
</div>
</div>
</template>
@@ -115,8 +149,13 @@
// ============================================================================
// 导入
// ============================================================================
import { ref, computed } from 'vue'
import { ref, computed, onBeforeUnmount } from 'vue'
import type { TodoItemData } from '@/api/todo'
import {
useTaskActions,
type TaskMainAction,
type TaskMoreAction,
} from '@/composables/useTaskActions'
// ============================================================================
// Props
@@ -140,17 +179,6 @@ interface Emits {
const emit = defineEmits<Emits>()
// ============================================================================
// 常量
// ============================================================================
/** 审批动作列表(均降级为跳转企微审批原系统) */
const approvalActions: ReadonlyArray<{ action: string; label: string }> = [
{ action: 'approve', label: '✅ 通过' },
{ action: 'reject', label: '❌ 拒绝' },
{ action: 'transfer', label: '🔄 转交' },
]
// ============================================================================
// 状态
// ============================================================================
@@ -159,10 +187,9 @@ const approvalActions: ReadonlyArray<{ action: string; label: string }> = [
const approvalComment = ref<string>('')
// ============================================================================
// 计算属性
// 企微审批跳转链接(与 useTaskActions 工厂入参共用)
// ============================================================================
/** 企微审批跳转链接 */
const wecomApprovalUrl = computed<string>(() => {
const spNo = props.todoItem.description?.sp_no
const templateId = props.todoItem.description?.template_id
@@ -173,6 +200,62 @@ const wecomApprovalUrl = computed<string>(() => {
return `https://app.work.weixin.qq.com/wework_admin/approval_v3#/?${params.toString()}`
})
// ============================================================================
// 操作配置(PRD-REQ-坐席-011 §6.4 决策 C-8:主操作 + ⋯ 收纳)
// ============================================================================
const { main: mainActionRef, more: moreActionsRef } = useTaskActions({
todoItem: props.todoItem,
wecomApprovalUrl: wecomApprovalUrl.value,
})
const mainAction = computed<TaskMainAction>(() => mainActionRef.value)
const moreActions = computed<TaskMoreAction[]>(() => moreActionsRef.value)
// ============================================================================
// ⋯ 菜单状态
// ============================================================================
const isMoreMenuOpen = ref<boolean>(false)
function toggleMoreMenu(event: MouseEvent): void {
event.stopPropagation()
isMoreMenuOpen.value = !isMoreMenuOpen.value
if (isMoreMenuOpen.value) {
setTimeout(() => {
document.addEventListener('mousedown', handleClickOutside, true)
document.addEventListener('keydown', handleKeydown, true)
}, 0)
} else {
closeMoreMenu()
}
}
function closeMoreMenu(): void {
isMoreMenuOpen.value = false
document.removeEventListener('mousedown', handleClickOutside, true)
document.removeEventListener('keydown', handleKeydown, true)
}
function handleClickOutside(event: MouseEvent): void {
const target = event.target as HTMLElement | null
if (target && target.closest('.task-actions-menu-wrapper')) return
closeMoreMenu()
}
function handleKeydown(event: KeyboardEvent): void {
if (event.key === 'Escape' || event.key === 'Esc') closeMoreMenu()
}
onBeforeUnmount(() => {
document.removeEventListener('mousedown', handleClickOutside, true)
document.removeEventListener('keydown', handleKeydown, true)
})
// ============================================================================
// 计算属性
// ============================================================================
/** 审批状态 CSS 类 */
const apvStatusClass = computed<string>(() => {
const status = props.todoItem.description?.sp_status
@@ -352,6 +435,8 @@ function formatApplyTime(applyTime: any): string {
gap: 8px;
padding: 8px 0;
flex-wrap: wrap;
align-items: center;
position: relative;
}
.tic-action-btn {
@@ -376,6 +461,13 @@ function formatApplyTime(applyTime: any): string {
color: var(--accent);
}
/* v1.8 · 决策 C-8 · 主按钮 padding 视觉密度补偿 */
.tic-action-btn.tic-action-primary,
.tic-action-btn.tic-action-success {
padding: 8px 22px;
font-weight: 500;
}
.tic-action-primary {
background-color: var(--accent);
color: var(--bg-secondary);
@@ -386,4 +478,110 @@ function formatApplyTime(applyTime: any): string {
background-color: var(--accent-hover);
color: var(--bg-secondary);
}
.tic-action-success {
background-color: var(--color-success);
color: var(--bg-secondary);
border-color: var(--color-success);
}
.tic-action-success:hover {
filter: brightness(0.95);
color: var(--bg-secondary);
}
.tic-action-default {
background-color: var(--bg-secondary);
color: var(--text-tertiary);
border-color: var(--border-color);
cursor: not-allowed;
}
.tic-action-btn[disabled] {
cursor: not-allowed;
opacity: 0.6;
}
/* ---- v1.8 · ⋯ 次要动作收纳 ---- */
.task-actions-menu-wrapper {
position: relative;
display: inline-flex;
}
.tic-action-more-btn {
width: 36px;
padding: 8px 0;
justify-content: center;
font-size: 16px;
line-height: 1;
letter-spacing: 1px;
}
.tic-action-more-btn[aria-expanded='true'] {
background-color: var(--bg-hover);
border-color: var(--accent);
color: var(--accent);
}
.task-actions-menu {
position: absolute;
bottom: calc(100% + 4px);
right: 0;
min-width: 180px;
margin: 0;
padding: 4px;
list-style: none;
background-color: var(--bg-secondary);
border: 1px solid var(--border-color);
border-radius: var(--radius-md);
box-shadow: var(--shadow-lg);
z-index: var(--z-dropdown, 2000);
animation: tam-fade-in 0.12s ease-out;
}
@keyframes tam-fade-in {
from { opacity: 0; transform: translateY(2px); }
to { opacity: 1; transform: translateY(0); }
}
.task-actions-menu__item {
display: flex;
align-items: center;
gap: 8px;
padding: 7px 10px;
border-radius: 4px;
font-size: 13px;
color: var(--text-primary);
transition: background 0.12s;
}
.task-actions-menu__item > a,
.task-actions-menu__item > span {
display: flex;
align-items: center;
gap: 8px;
flex: 1;
text-decoration: none;
color: inherit;
}
.task-actions-menu__item:hover:not(.is-disabled) {
background-color: var(--bg-hover);
}
.task-actions-menu__item.is-danger {
color: var(--color-danger);
}
.task-actions-menu__item.is-disabled {
color: var(--text-tertiary);
cursor: not-allowed;
}
.task-actions-menu__icon {
font-size: 14px;
width: 16px;
text-align: center;
flex-shrink: 0;
}
</style>
@@ -75,17 +75,61 @@
</div>
<!-- ================================================================== -->
<!-- 底部操作按钮 跳转到 ITSM 原系统操作 -->
<!-- 底部操作按钮 PRD-REQ-坐席-011 §6.4 决策 C-8主操作 + 收纳 -->
<!-- ================================================================== -->
<div class="tic-actions">
<!-- 主操作按状态驱动pending接单 / processing结单 / resolved已结单 / 其他打开 ITSM -->
<a
class="tic-action-btn tic-action-primary"
:href="itsmUrl"
class="tic-action-btn"
:class="`tic-action-${mainAction.variant}`"
:href="mainAction.href"
:disabled="mainAction.disabled"
:title="mainAction.tooltip || mainAction.label"
target="_blank"
rel="noopener noreferrer"
>
🔗 ITSM 中打开
{{ mainAction.label }}
</a>
<!-- 次要动作收纳按钮PRD-REQ-坐席-011 §6.4 决策 C-8 -->
<div class="task-actions-menu-wrapper" v-if="moreActions.length > 0">
<button
class="tic-action-btn tic-action-more-btn"
type="button"
:aria-expanded="isMoreMenuOpen"
aria-haspopup="menu"
aria-label="更多操作"
:title="'更多操作'"
@click="toggleMoreMenu"
>
</button>
<ul v-if="isMoreMenuOpen" class="task-actions-menu" role="menu">
<li
v-for="item in moreActions"
:key="item.key"
class="task-actions-menu__item"
:class="{ 'is-danger': item.danger, 'is-disabled': item.disabled }"
role="menuitem"
:title="item.tooltip || item.label"
>
<a
v-if="item.href && !item.disabled"
:href="item.href"
target="_blank"
rel="noopener noreferrer"
@click="closeMoreMenu"
>
<span class="task-actions-menu__icon">{{ item.icon }}</span>
<span>{{ item.label }}</span>
</a>
<span v-else class="task-actions-menu__label-disabled">
<span class="task-actions-menu__icon">{{ item.icon }}</span>
<span>{{ item.label }}</span>
</span>
</li>
</ul>
</div>
</div>
</div>
</template>
@@ -94,13 +138,9 @@
// ============================================================================
// 导入
// ============================================================================
import { computed } from 'vue'
import { computed, ref, onBeforeUnmount } from 'vue'
import type { TodoItemData } from '@/api/todo'
// ============================================================================
// ITSM 运维平台基址(用于构建跳转链接)
// ============================================================================
const ITSM_BASE_URL = 'https://devops.dc.servyou-it.com/ITSM'
import { useTaskActions, type TaskMainAction, type TaskMoreAction } from '@/composables/useTaskActions'
// ============================================================================
// Props
@@ -118,12 +158,72 @@ const props = defineProps<Props>()
// ============================================================================
interface Emits {
/** 操作按钮事件 */
/** 操作按钮事件(v1.8 后保留接口,当前主/次按钮均为原生跳转,无 onclick 透传) */
(e: 'action', action: string): void
}
defineEmits<Emits>()
// ============================================================================
// 操作配置(PRD-REQ-坐席-011 §6.4 决策 C-8:主操作 + ⋯ 收纳)
// ============================================================================
const { main: mainActionRef, more: moreActionsRef } = useTaskActions({ todoItem: props.todoItem })
/** 主操作按钮(ComputedRef → 解包后给模板用,避免 vue-tsc 复杂类型推断) */
const mainAction = computed<TaskMainAction>(() => mainActionRef.value)
/** ⋯ 次要动作列表 */
const moreActions = computed<TaskMoreAction[]>(() => moreActionsRef.value)
// ============================================================================
// ⋯ 菜单状态
// ============================================================================
const isMoreMenuOpen = ref<boolean>(false)
/**
* 切换 ⋯ 菜单开关
* - 关闭用点击外部/Escdocument mousedown + keydown
* - 关闭时机:菜单项点击(disabled 项不响应)
*/
function toggleMoreMenu(event: MouseEvent): void {
event.stopPropagation()
isMoreMenuOpen.value = !isMoreMenuOpen.value
if (isMoreMenuOpen.value) {
// 下一帧再挂全局监听,避免本次 toggle 触发的 mousedown 立刻被关掉
setTimeout(() => {
document.addEventListener('mousedown', handleClickOutside, true)
document.addEventListener('keydown', handleKeydown, true)
}, 0)
} else {
closeMoreMenu()
}
}
function closeMoreMenu(): void {
isMoreMenuOpen.value = false
document.removeEventListener('mousedown', handleClickOutside, true)
document.removeEventListener('keydown', handleKeydown, true)
}
function handleClickOutside(event: MouseEvent): void {
const target = event.target as HTMLElement | null
if (target && target.closest('.task-actions-menu-wrapper')) return
closeMoreMenu()
}
function handleKeydown(event: KeyboardEvent): void {
if (event.key === 'Escape' || event.key === 'Esc') {
closeMoreMenu()
}
}
/** 组件卸载兜底 */
onBeforeUnmount(() => {
document.removeEventListener('mousedown', handleClickOutside, true)
document.removeEventListener('keydown', handleKeydown, true)
})
// ============================================================================
// 映射
// ============================================================================
@@ -135,19 +235,6 @@ const statusLabelMap: Record<string, string> = {
resolved: '已解决',
}
// ============================================================================
// 计算属性
// ============================================================================
/** ITSM 工单跳转链接 */
const itsmUrl = computed<string>(() => {
const processInstanceId = props.todoItem.description?.process_instance_id
if (processInstanceId) {
return `${ITSM_BASE_URL}/workflow/allTickets/detail?process_instance_id=${processInstanceId}`
}
return ITSM_BASE_URL
})
// ============================================================================
// 方法
// ============================================================================
@@ -259,6 +346,8 @@ function formatTime(isoString: string): string {
gap: 8px;
padding: 8px 0;
flex-wrap: wrap;
align-items: center;
position: relative;
}
.tic-action-btn {
@@ -283,6 +372,13 @@ function formatTime(isoString: string): string {
color: var(--accent);
}
/* v1.8 · 决策 C-8 · 主按钮 padding 视觉密度补偿(按钮少时主按钮需要更醒目) */
.tic-action-btn.tic-action-primary,
.tic-action-btn.tic-action-success {
padding: 8px 22px;
font-weight: 500;
}
.tic-action-primary {
background-color: var(--accent);
color: var(--bg-secondary);
@@ -293,4 +389,114 @@ function formatTime(isoString: string): string {
background-color: var(--accent-hover);
color: var(--bg-secondary);
}
.tic-action-success {
background-color: var(--color-success);
color: var(--bg-secondary);
border-color: var(--color-success);
}
.tic-action-success:hover {
filter: brightness(0.95);
color: var(--bg-secondary);
}
.tic-action-default {
background-color: var(--bg-secondary);
color: var(--text-tertiary);
border-color: var(--border-color);
cursor: not-allowed;
}
.tic-action-btn[disabled] {
cursor: not-allowed;
opacity: 0.6;
}
/* ---- v1.8 · ⋯ 次要动作收纳 ---- */
.task-actions-menu-wrapper {
position: relative;
display: inline-flex;
}
.tic-action-more-btn {
width: 36px;
padding: 8px 0;
justify-content: center;
font-size: 16px;
line-height: 1;
letter-spacing: 1px;
}
.tic-action-more-btn[aria-expanded='true'] {
background-color: var(--bg-hover);
border-color: var(--accent);
color: var(--accent);
}
.task-actions-menu {
position: absolute;
bottom: calc(100% + 4px);
right: 0;
min-width: 180px;
margin: 0;
padding: 4px;
list-style: none;
background-color: var(--bg-secondary);
border: 1px solid var(--border-color);
border-radius: var(--radius-md);
box-shadow: var(--shadow-lg);
z-index: var(--z-dropdown, 2000);
animation: tam-fade-in 0.12s ease-out;
}
@keyframes tam-fade-in {
from { opacity: 0; transform: translateY(2px); }
to { opacity: 1; transform: translateY(0); }
}
.task-actions-menu__item {
display: flex;
align-items: center;
gap: 8px;
padding: 7px 10px;
border-radius: 4px;
font-size: 13px;
color: var(--text-primary);
transition: background 0.12s;
}
.task-actions-menu__item > a,
.task-actions-menu__item > span {
display: flex;
align-items: center;
gap: 8px;
flex: 1;
text-decoration: none;
color: inherit;
}
.task-actions-menu__item:hover:not(.is-disabled) {
background-color: var(--bg-hover);
}
.task-actions-menu__item.is-danger {
color: var(--color-danger);
}
.task-actions-menu__item.is-disabled {
color: var(--text-tertiary);
cursor: not-allowed;
}
.task-actions-menu__item.is-disabled > span {
cursor: not-allowed;
}
.task-actions-menu__icon {
font-size: 14px;
width: 16px;
text-align: center;
flex-shrink: 0;
}
</style>
@@ -0,0 +1,329 @@
/**
* useTaskActions 工厂单元测试(PRD-REQ-坐席-011 §6.4 决策 C-8
* =============================================================================
*
* 测试覆盖(与原型 v1.8 TaskDetailView 操作区对齐):
*
* Ticket:
* T1: pending → 主「📥 接单」disabled + ⋯ 5 项(含「开始处理/转派/挂起/升级/打开」)
* T2: processing → 主「✅ 结单」disabled + ⋯ 5 项
* T3: resolved → 主「✅ 已结单」disabled + ⋯ 5 项(无升级优先级)
* T4: unknown status → 主「🔗 在 ITSM 中打开」enabledfallback
*
* Approval:
* A1: sp_status=1 → 主「✅ 审批通过」enabled + ⋯ 3 项(拒绝/转交/加签)
* A2: sp_status=2 → 主「🔗 在企微审批中打开」enabled + ⋯ 0 项
* A3: sp_status=3 → 主「🔗 在企微审批中打开」enabled + ⋯ 0 项
*
* Pure 工厂函数(无需 setup wrapper):
* - buildTicketMainAction
* - buildApprovalMainAction
* - buildTicketMoreActions
* - buildApprovalMoreActions
* - isTicketClaimable
* - isTicketClosable
* - isApprovalPending
*
* 验收证据:
* - B-1: ticket pending → 主「接单」disabledU-1.1 阻塞)
* - B-2: ticket processing → 主「结单」disabledU-1.1 阻塞)
* - B-3: approval sp_status=1 → 主「审批通过」enabled(真跳转)
* - B-4: approval 非审批中 → 主「打开」+ ⋯ 空(无可执行动作)
*/
import { describe, it, expect } from 'vitest'
import type { TodoItemData } from '@/api/todo'
import {
buildTicketMainAction,
buildApprovalMainAction,
buildTicketMoreActions,
buildApprovalMoreActions,
isTicketClaimable,
isTicketClosable,
isApprovalPending,
} from '@/composables/useTaskActions'
// ---------------------------------------------------------------------------
// 辅助函数
// ---------------------------------------------------------------------------
/** ITSM 测试跳转链接(任意非空字符串即可,主按钮需要 href) */
const TEST_ITSM_URL = 'https://itsm.test/workorder/abc123'
/** 企微审批测试跳转链接 */
const TEST_WECOM_URL =
'https://app.work.weixin.qq.com/wework_admin/approval_v3#/?sp_id=test&template_id=tpl'
/** 构造 TodoItemData fixture */
function makeTodo(overrides: Partial<TodoItemData> = {}): TodoItemData {
return {
id: 'todo-001',
type: 'ticket',
title: '测试工单',
priority: 'normal',
description: { process_instance_id: 'wf-12345' },
status: 'pending',
assigned_agent_id: null,
corp_id: 'corp-test',
created_at: new Date().toISOString(),
updated_at: new Date().toISOString(),
...overrides,
}
}
/** 取菜单项中 key 对应的项 */
function findMoreItem(
items: ReturnType<typeof buildTicketMoreActions>,
key: string
) {
return items.find((it) => it.key === key)
}
// ---------------------------------------------------------------------------
// 状态判定 pure 函数
// ---------------------------------------------------------------------------
describe('useTaskActions — 状态判定 pure 函数', () => {
it('isTicketClaimable: ticket pending → true', () => {
expect(isTicketClaimable(makeTodo({ type: 'ticket', status: 'pending' }))).toBe(true)
})
it('isTicketClaimable: ticket processing → false', () => {
expect(isTicketClaimable(makeTodo({ type: 'ticket', status: 'processing' }))).toBe(false)
})
it('isTicketClaimable: approval → false(无论 status', () => {
expect(isTicketClaimable(makeTodo({ type: 'approval', status: 'pending' }))).toBe(false)
})
it('isTicketClosable: ticket processing → true', () => {
expect(isTicketClosable(makeTodo({ type: 'ticket', status: 'processing' }))).toBe(true)
})
it('isTicketClosable: ticket pending → false', () => {
expect(isTicketClosable(makeTodo({ type: 'ticket', status: 'pending' }))).toBe(false)
})
it('isApprovalPending: approval sp_status=1 → true', () => {
expect(isApprovalPending(makeTodo({ type: 'approval', description: { sp_status: 1 } }))).toBe(
true
)
})
it('isApprovalPending: approval sp_status=2 → false', () => {
expect(isApprovalPending(makeTodo({ type: 'approval', description: { sp_status: 2 } }))).toBe(
false
)
})
it('isApprovalPending: ticket → false', () => {
expect(isApprovalPending(makeTodo({ type: 'ticket' }))).toBe(false)
})
})
// ---------------------------------------------------------------------------
// Ticket 主操作
// ---------------------------------------------------------------------------
describe('useTaskActions — Ticket 主操作(buildTicketMainAction', () => {
it('T1: pending → 「📥 接单」disabled + primary + itsmUrl', () => {
const main = buildTicketMainAction(makeTodo({ status: 'pending' }), TEST_ITSM_URL)
expect(main.key).toBe('ticket-claim')
expect(main.label).toContain('接单')
expect(main.variant).toBe('primary')
expect(main.disabled).toBe(true)
expect(main.href).toBe(TEST_ITSM_URL)
expect(main.tooltip).toContain('待后端')
})
it('T2: processing → 「✅ 结单」disabled + success + itsmUrl', () => {
const main = buildTicketMainAction(makeTodo({ status: 'processing' }), TEST_ITSM_URL)
expect(main.key).toBe('ticket-close')
expect(main.label).toContain('结单')
expect(main.variant).toBe('success')
expect(main.disabled).toBe(true)
expect(main.href).toBe(TEST_ITSM_URL)
})
it('T3: resolved → 「✅ 已结单」disabled + default(终态)', () => {
const main = buildTicketMainAction(makeTodo({ status: 'resolved' }), TEST_ITSM_URL)
expect(main.key).toBe('ticket-resolved')
expect(main.label).toContain('已结单')
expect(main.variant).toBe('default')
expect(main.disabled).toBe(true)
expect(main.href).toBeUndefined()
})
it('T4: unknown status → 「🔗 在 ITSM 中打开」enabled + primaryfallback', () => {
const main = buildTicketMainAction(makeTodo({ status: 'unknown-state' }), TEST_ITSM_URL)
expect(main.key).toBe('ticket-open')
expect(main.label).toContain('ITSM')
expect(main.variant).toBe('primary')
expect(main.disabled).toBe(false)
expect(main.href).toBe(TEST_ITSM_URL)
})
})
// ---------------------------------------------------------------------------
// Ticket 次要动作
// ---------------------------------------------------------------------------
describe('useTaskActions — Ticket 次要动作(buildTicketMoreActions', () => {
it('T1: pending → 5 项(含升级优先级 + 打开原系统)', () => {
const items = buildTicketMoreActions(makeTodo({ status: 'pending' }), TEST_ITSM_URL)
expect(items.length).toBe(5)
expect(items.map((i) => i.key)).toEqual([
'ticket-process',
'ticket-handoff',
'ticket-pause',
'ticket-escalate',
'ticket-open-original',
])
})
it('T2: processing → 5 项(同 pending 列表)', () => {
const items = buildTicketMoreActions(makeTodo({ status: 'processing' }), TEST_ITSM_URL)
expect(items.length).toBe(5)
expect(items.map((i) => i.key)).toContain('ticket-escalate')
})
it('T3: resolved → 4 项(无升级优先级,因为已结单)', () => {
const items = buildTicketMoreActions(makeTodo({ status: 'resolved' }), TEST_ITSM_URL)
expect(items.length).toBe(4)
expect(items.map((i) => i.key)).not.toContain('ticket-escalate')
})
it('所有 ticket 次要动作:开始处理/转派/挂起/升级 均 disabled + tooltip', () => {
const items = buildTicketMoreActions(makeTodo({ status: 'pending' }), TEST_ITSM_URL)
const tbd = ['ticket-process', 'ticket-handoff', 'ticket-pause', 'ticket-escalate']
tbd.forEach((key) => {
const item = findMoreItem(items, key)
expect(item?.disabled).toBe(true)
expect(item?.tooltip).toContain('待后端')
})
})
it('「打开原系统」始终 enabled + href', () => {
const items = buildTicketMoreActions(makeTodo({ status: 'pending' }), TEST_ITSM_URL)
const open = findMoreItem(items, 'ticket-open-original')
expect(open?.disabled).toBe(false)
expect(open?.href).toBe(TEST_ITSM_URL)
})
})
// ---------------------------------------------------------------------------
// Approval 主操作
// ---------------------------------------------------------------------------
describe('useTaskActions — Approval 主操作(buildApprovalMainAction', () => {
it('A1: sp_status=1 → 「✅ 审批通过」enabled + success + wecomUrl', () => {
const main = buildApprovalMainAction(
makeTodo({ type: 'approval', description: { sp_status: 1 } }),
TEST_WECOM_URL
)
expect(main.key).toBe('approval-approve')
expect(main.label).toContain('审批通过')
expect(main.variant).toBe('success')
expect(main.disabled).toBe(false)
expect(main.href).toBe(TEST_WECOM_URL)
})
it('A2: sp_status=2 → 「🔗 在企微审批中打开」enabled + primary', () => {
const main = buildApprovalMainAction(
makeTodo({ type: 'approval', description: { sp_status: 2 } }),
TEST_WECOM_URL
)
expect(main.key).toBe('approval-open')
expect(main.label).toContain('企微审批')
expect(main.variant).toBe('primary')
expect(main.disabled).toBe(false)
expect(main.href).toBe(TEST_WECOM_URL)
})
it('A3: sp_status=3(驳回)→ 「🔗 在企微审批中打开」enabled', () => {
const main = buildApprovalMainAction(
makeTodo({ type: 'approval', description: { sp_status: 3 } }),
TEST_WECOM_URL
)
expect(main.key).toBe('approval-open')
expect(main.variant).toBe('primary')
expect(main.disabled).toBe(false)
})
})
// ---------------------------------------------------------------------------
// Approval 次要动作
// ---------------------------------------------------------------------------
describe('useTaskActions — Approval 次要动作(buildApprovalMoreActions', () => {
it('A1: sp_status=1 → 3 项(拒绝/转交/加签)', () => {
const items = buildApprovalMoreActions(
makeTodo({ type: 'approval', description: { sp_status: 1 } }),
TEST_WECOM_URL
)
expect(items.length).toBe(3)
expect(items.map((i) => i.key)).toEqual([
'approval-reject',
'approval-forward',
'approval-addsigner',
])
})
it('拒绝项 = danger + enabled', () => {
const items = buildApprovalMoreActions(
makeTodo({ type: 'approval', description: { sp_status: 1 } }),
TEST_WECOM_URL
)
const reject = findMoreItem(items, 'approval-reject')
expect(reject?.danger).toBe(true)
expect(reject?.disabled).toBe(false)
})
it('所有 sp_status=1 次要项均 enabled + href → 企微审批', () => {
const items = buildApprovalMoreActions(
makeTodo({ type: 'approval', description: { sp_status: 1 } }),
TEST_WECOM_URL
)
items.forEach((item) => {
expect(item.disabled).toBe(false)
expect(item.href).toBe(TEST_WECOM_URL)
})
})
it('A2: sp_status=2 → 空数组(审批已通过,无可执行动作)', () => {
const items = buildApprovalMoreActions(
makeTodo({ type: 'approval', description: { sp_status: 2 } }),
TEST_WECOM_URL
)
expect(items.length).toBe(0)
})
it('A3: sp_status=3(驳回)→ 空数组(审批已结束)', () => {
const items = buildApprovalMoreActions(
makeTodo({ type: 'approval', description: { sp_status: 3 } }),
TEST_WECOM_URL
)
expect(items.length).toBe(0)
})
it('A4: sp_status=4(撤销)→ 空数组', () => {
const items = buildApprovalMoreActions(
makeTodo({ type: 'approval', description: { sp_status: 4 } }),
TEST_WECOM_URL
)
expect(items.length).toBe(0)
})
})
// ---------------------------------------------------------------------------
// 兜底:未知 type
// ---------------------------------------------------------------------------
describe('useTaskActions — 未知 type 兜底(buildTicketMainAction / buildApprovalMainAction', () => {
it('ticket 主操作:无 status 字段 → fallback 打开', () => {
// 显式覆盖 description 为空对象,移除 status 字段语义(status 本身存在)
const todo = makeTodo({ status: '' })
const main = buildTicketMainAction(todo, TEST_ITSM_URL)
// 空 status 不命中 pending/processing/resolved → 进入 fallback 分支
expect(main.key).toBe('ticket-open')
expect(main.disabled).toBe(false)
})
})
@@ -0,0 +1,362 @@
/**
* 企微IT智能服务台 — 任务详情操作配置工厂(PRD-REQ-坐席-011 §6.4 决策 C-8
* =============================================================================
*
* 做什么:
* 根据 TodoItemData 当前状态,产出 TaskDetailView 操作区的「主操作按钮 + ⋯
* 次要动作收纳」配置。**状态驱动**主按钮:ticket.queued→接单、ticket.processing
* →结单;approval.sp_status=1→审批通过;其他状态主按钮聚焦「在原系统打开」。
*
* 为什么独立成 composable
* - TaskDetailView 只渲染、不感知业务规则(与 v1.7 §4.4 「单例菜单」哲学一致)
* - ticket / approval / device 三类任务共用同一套「主操作 + ⋯」视觉
* - 后续后端 API 解决(U-1.2 / U-1.1 解除)后,仅改本工厂即可解除 disabled
*
* 设计约束(PRD-REQ-坐席-011 §6.4 决策 C-8):
* - 主按钮:1 个,状态驱动 + 文案自适应
* - ⋯ 收纳:≤ 5 个项,低频+高破坏性收纳二级
* - 无后端 API 的项:disabled + tooltip「待后端接口开放」(不抛错、不假成功)
*
* 不做什么:
* - 不发请求、不修改 store(纯 UI 配置)
* - 不依赖 WorkItemActionMenu.vue(保持 composable 与 UI 壳解耦)
*
* 与 useConversationMenuItems.ts 区别:
* - 后者产出左栏会话条目的菜单(assign/pin/todo/transfer/grab/leave
* - 本工厂产出中栏 TaskDetailView 的操作(claim/close/handoff/approve/reject/open
*
* 验收证据:
* - B-1: ticket pending → 主「接单」disabled(待后端)+ ⋯ 收纳转派等占位
* - B-2: ticket processing → 主「结单」disabled(待后端)+ ⋯ 收纳转派
* - B-3: approval sp_status=1 → 主「审批通过」enabled(跳企微)
* - B-4: approval sp_status=2/3/4 → 主「在企微审批中打开」+ ⋯ 不显示审批动作
*/
import { computed, type ComputedRef } from 'vue'
import type { TodoItemData } from '@/api/todo'
// ============================================================================
// 类型
// ============================================================================
/** 主操作按钮(必有,1 个) */
export interface TaskMainAction {
/** 唯一 key */
key: string
/** 显示文案(含 emoji */
label: string
/** 图标(emoji 字符) */
icon: string
/** 视觉风格:primary / success / warning / default */
variant: 'primary' | 'success' | 'warning' | 'default'
/** 跳转 URLhref 模式);undefined 表示执行 onclick */
href?: string
/** 禁用(待后端 API */
disabled: boolean
/** hover/tooltip 文案 */
tooltip?: string
}
/** 次要动作(收纳进 ⋯,≤ 5 个) */
export interface TaskMoreAction {
key: string
label: string
icon: string
/** danger:红色样式 */
danger?: boolean
/** 禁用 + tooltip 说明 */
disabled: boolean
tooltip?: string
/** 跳转 URLhref 模式) */
href?: string
}
/** TaskDetailView 操作区整体配置 */
export interface TaskActionsConfig {
/** 主操作按钮(1 个) */
main: ComputedRef<TaskMainAction>
/** ⋯ 次要动作列表(0~5 个) */
more: ComputedRef<TaskMoreAction[]>
}
/** useTaskActions 入参 */
export interface UseTaskActionsParams {
/** 当前 TaskDetailView 选中的待办 */
todoItem: TodoItemData
/** 企微审批深链(仅 approval 用) */
wecomApprovalUrl?: string
}
// ============================================================================
// 文案常量(与原型 v1.8 + PRD-REQ-坐席-011 §6.4 对齐)
// ============================================================================
/** 待后端 API 时统一 tooltip 文案(避免分散硬编码) */
const TBD_API_TOOLTIP = '待后端接口开放(ITSM U-1.2/U-1.1'
// ============================================================================
// 状态判定辅助(pure,便于单测)
// ============================================================================
/**
* ticket 是否处于待接单(pending → 服务台应能接单)
*/
export function isTicketClaimable(todo: TodoItemData): boolean {
return todo.type === 'ticket' && todo.status === 'pending'
}
/**
* ticket 是否处于处理中(处理人已分配,服务台应能结单)
*/
export function isTicketClosable(todo: TodoItemData): boolean {
return todo.type === 'ticket' && todo.status === 'processing'
}
/**
* approval 是否处于审批中(sp_status=1
*/
export function isApprovalPending(todo: TodoItemData): boolean {
return todo.type === 'approval' && todo.description?.sp_status === 1
}
// ============================================================================
// 主操作产出(pure,便于单测)
// ============================================================================
/**
* 产出 ticket 主操作
* - pending → 「📥 接单」+ 跳转 ITSMdisabled 占位)
* - processing→ 「✅ 结单」+ 跳转 ITSMdisabled 占位)
* - resolved → 「✅ 已结单」(disabled)
* - 其他 → 「🔗 在 ITSM 中打开」(enabled
*/
export function buildTicketMainAction(todo: TodoItemData, itsmUrl: string): TaskMainAction {
if (isTicketClaimable(todo)) {
return {
key: 'ticket-claim',
label: '📥 接单',
icon: '📥',
variant: 'primary',
href: itsmUrl,
disabled: true, // ⚠️ U-1.1 阻塞
tooltip: TBD_API_TOOLTIP,
}
}
if (isTicketClosable(todo)) {
return {
key: 'ticket-close',
label: '✅ 结单',
icon: '✅',
variant: 'success',
href: itsmUrl,
disabled: true, // ⚠️ U-1.1 阻塞
tooltip: TBD_API_TOOLTIP,
}
}
if (todo.status === 'resolved') {
return {
key: 'ticket-resolved',
label: '✅ 已结单',
icon: '✅',
variant: 'default',
disabled: true,
tooltip: '工单已结单',
}
}
// fallback(未知 status
return {
key: 'ticket-open',
label: '🔗 在 ITSM 中打开',
icon: '🔗',
variant: 'primary',
href: itsmUrl,
disabled: false,
}
}
/**
* 产出 approval 主操作
* - sp_status=1(审批中) → 「✅ 审批通过」(enabled,跳企微)
* - 其他(非审批中) → 「🔗 在企微审批中打开」(enabled,跳企微)
*/
export function buildApprovalMainAction(
todo: TodoItemData,
wecomApprovalUrl: string
): TaskMainAction {
if (isApprovalPending(todo)) {
return {
key: 'approval-approve',
label: '✅ 审批通过',
icon: '✅',
variant: 'success',
href: wecomApprovalUrl,
disabled: false,
}
}
return {
key: 'approval-open',
label: '🔗 在企微审批中打开',
icon: '🔗',
variant: 'primary',
href: wecomApprovalUrl,
disabled: false,
}
}
// ============================================================================
// 次要动作产出(pure,便于单测)
// ============================================================================
/**
* ticket ⋯ 次要动作
* 全部 disabled(待 ITSM API 开放)—— 占位用途
*/
export function buildTicketMoreActions(todo: TodoItemData, itsmUrl: string): TaskMoreAction[] {
const isClaimable = isTicketClaimable(todo)
const isClosable = isTicketClosable(todo)
return [
// 1. 开始处理(仅 pending→processing 显式动作;可由接单隐式推进,故占位)
{
key: 'ticket-process',
label: '🔧 开始处理',
icon: '🔧',
disabled: true,
tooltip: TBD_API_TOOLTIP,
},
// 2. 转派
{
key: 'ticket-handoff',
label: '🔄 转派给他人',
icon: '🔄',
disabled: true,
tooltip: TBD_API_TOOLTIP,
},
// 3. 挂起
{
key: 'ticket-pause',
label: '⏸ 挂起(暂不处理)',
icon: '⏸',
disabled: true,
tooltip: TBD_API_TOOLTIP,
},
// 4. 标记高优(仅 claimable/closable 时显示)
...(isClaimable || isClosable
? [
{
key: 'ticket-escalate',
label: '🔺 升级优先级',
icon: '🔺',
disabled: true,
tooltip: TBD_API_TOOLTIP,
},
]
: []),
// 5. 在原系统打开(始终显示,作为次要出口)
{
key: 'ticket-open-original',
label: '🔗 在 ITSM 中打开',
icon: '🔗',
href: itsmUrl,
disabled: false,
},
]
}
/**
* approval ⋯ 次要动作
* - sp_status=1(审批中) → 拒绝/转交(真跳转,可点)
* - 其他 → 空数组(审批已结束,无可执行动作)
*/
export function buildApprovalMoreActions(
todo: TodoItemData,
wecomApprovalUrl: string
): TaskMoreAction[] {
if (!isApprovalPending(todo)) {
return []
}
return [
{
key: 'approval-reject',
label: '❌ 拒绝审批',
icon: '❌',
danger: true,
href: wecomApprovalUrl,
disabled: false,
},
{
key: 'approval-forward',
label: '🔄 转交审批',
icon: '🔄',
href: wecomApprovalUrl,
disabled: false,
},
{
key: 'approval-addsigner',
label: ' 加签',
icon: '',
href: wecomApprovalUrl,
disabled: false,
},
]
}
// ============================================================================
// 工厂
// ============================================================================
/**
* useTaskActions 工厂
*
* 使用示例:
* const { main, more } = useTaskActions({ todoItem, wecomApprovalUrl })
*
* @param params - 入参
* @returns TaskActionsConfig - main + more 配置
*/
export function useTaskActions(params: UseTaskActionsParams): TaskActionsConfig {
const { todoItem, wecomApprovalUrl } = params
/** ITSM 运维平台基址(与 TicketDetail.vue 现状一致) */
const ITSM_BASE_URL = 'https://devops.dc.servyou-it.com/ITSM'
/** 计算 ITSM 跳转链接(与 TicketDetail.vue 现状一致) */
const itsmUrl = computed<string>(() => {
const processInstanceId = todoItem.description?.process_instance_id
if (processInstanceId) {
return `${ITSM_BASE_URL}/workflow/allTickets/detail?process_instance_id=${processInstanceId}`
}
return ITSM_BASE_URL
})
/** 主操作:按类型分发 */
const main = computed<TaskMainAction>(() => {
if (todoItem.type === 'ticket') {
return buildTicketMainAction(todoItem, itsmUrl.value)
}
if (todoItem.type === 'approval') {
return buildApprovalMainAction(todoItem, wecomApprovalUrl || '')
}
// fallback(未知类型,与 TicketDetail 一致)
return {
key: 'unknown-open',
label: '🔗 在原系统打开',
icon: '🔗',
variant: 'primary',
href: itsmUrl.value,
disabled: false,
}
})
/** ⋯ 次要动作:按类型分发 */
const more = computed<TaskMoreAction[]>(() => {
if (todoItem.type === 'ticket') {
return buildTicketMoreActions(todoItem, itsmUrl.value)
}
if (todoItem.type === 'approval') {
return buildApprovalMoreActions(todoItem, wecomApprovalUrl || '')
}
return []
})
return { main, more }
}
+103
View File
@@ -45,6 +45,18 @@
</p>
</div>
<!-- 测试账号登录REQ-通用-006 预生产测试通道仅内网探测成功时显示 -->
<div v-if="showTestPanel" class="test-login">
<el-divider><span class="test-login-divider-text">测试账号登录</span></el-divider>
<div class="test-login-row">
<el-select v-model="testUser" size="small" placeholder="选择测试角色" style="width: 180px">
<el-option v-for="u in testUsers" :key="u.userid" :label="u.label" :value="u.userid" />
</el-select>
<el-button type="primary" size="small" :loading="testLogining" @click="testLogin">免企微登录</el-button>
</div>
<p class="test-login-hint">仅内网可用用于自动化测试</p>
</div>
<!-- 加载状态 -->
<div v-if="loading" class="loading-container">
<el-icon class="is-loading" size="32"><Loading /></el-icon>
@@ -96,6 +108,26 @@ let pollTimer: ReturnType<typeof setInterval> | null = null
/** 当前扫码登录票据 */
let currentTicket = ''
// --------------------------------------------------------------------------
// 测试通道状态(REQ-通用-006/api/dev/* 仅内网可用,公网 403 则隐藏)
// --------------------------------------------------------------------------
/** 是否显示测试账号登录面板 */
const showTestPanel = ref(false)
/** 测试登录中 */
const testLogining = ref(false)
/** 选中的测试账号 userid */
const testUser = ref('dev-agent-001')
/** 预设测试账号(与后端 PRESET_DEV_USERS 对应) */
const testUsers = [
{ userid: 'dev-user-001', label: '员工(user', role: 'user' },
{ userid: 'dev-agent-001', label: '坐席(agent', role: 'agent' },
{ userid: 'dev-admin-001', label: '管理员(admin', role: 'admin' },
]
// --------------------------------------------------------------------------
// 方法
// --------------------------------------------------------------------------
@@ -291,11 +323,58 @@ async function redirectToOAuth(): Promise<void> {
window.location.href = oauthUrl
}
// --------------------------------------------------------------------------
// 测试通道方法(REQ-通用-006
// --------------------------------------------------------------------------
/**
* 探测测试通道可用性(内网 200 → 显示测试入口;公网 403 → 隐藏)
*/
async function checkTestChannel(): Promise<void> {
try {
const apiClient = (await import('@/api/index')).default
const resp = await apiClient.get('/dev/health', { timeout: 3000 })
if (resp?.data?.data?.dev_mode) {
showTestPanel.value = true
}
} catch {
showTestPanel.value = false
}
}
/**
* 测试账号免企微登录(仅内网):/api/dev/login → token 写入 agent_token
*/
async function testLogin(): Promise<void> {
const target = testUsers.find((u) => u.userid === testUser.value)
if (!target) return
testLogining.value = true
try {
const apiClient = (await import('@/api/index')).default
const resp = await apiClient.get('/dev/login', {
params: { userid: target.userid, name: target.label, role: target.role },
})
await handleLoginSuccess(resp.data.data.token, {
userid: target.userid,
name: target.label,
})
} catch (error) {
console.error('测试登录失败:', error)
ElMessage.error('测试登录失败,请确认内网环境')
} finally {
testLogining.value = false
}
}
// --------------------------------------------------------------------------
// 生命周期
// --------------------------------------------------------------------------
onMounted(async () => {
// 探测测试通道(不阻塞主登录流程)
checkTestChannel()
// 检查 URL 中的 code 参数(OAuth2 回调)
const urlParams = new URLSearchParams(window.location.search)
const code = urlParams.get('code')
@@ -471,6 +550,30 @@ onUnmounted(() => {
margin-top: 12px;
}
/* 测试账号登录(REQ-通用-006 */
.test-login {
margin-top: 8px;
}
.test-login-divider-text {
font-size: 12px;
color: var(--text-tertiary, #909399);
}
.test-login-row {
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
}
.test-login-hint {
text-align: center;
font-size: 12px;
color: var(--text-tertiary, #909399);
margin: 8px 0 0;
}
/* 加载状态 */
.loading-container {
display: flex;
+115
View File
@@ -40,6 +40,18 @@
</p>
</div>
<!-- 测试账号登录REQ-通用-006 预生产测试通道仅内网探测成功时显示 -->
<div v-if="showTestPanel" class="test-login">
<div class="test-login-title">测试账号登录</div>
<div class="test-login-row">
<select v-model="testUser" class="test-login-select">
<option v-for="u in testUsers" :key="u.userid" :value="u.userid">{{ u.label }}</option>
</select>
<van-button type="primary" size="small" :loading="testLogining" @click="testLogin">免企微登录</van-button>
</div>
<p class="test-login-hint">仅内网可用用于自动化测试</p>
</div>
<!-- 加载状态 -->
<div v-if="loading" class="loading-container">
<van-loading type="spinner" size="40px" />
@@ -87,6 +99,26 @@ let pollTimer: ReturnType<typeof setInterval> | null = null
/** 当前扫码登录票据 */
let currentTicket = ''
// --------------------------------------------------------------------------
// 测试通道状态(REQ-通用-006/api/dev/* 仅内网可用,公网 403 则隐藏)
// --------------------------------------------------------------------------
/** 是否显示测试账号登录面板 */
const showTestPanel = ref(false)
/** 测试登录中 */
const testLogining = ref(false)
/** 选中的测试账号 userid */
const testUser = ref('dev-user-001')
/** 预设测试账号(与后端 PRESET_DEV_USERS 对应) */
const testUsers = [
{ userid: 'dev-user-001', label: '员工(user', role: 'user' },
{ userid: 'dev-agent-001', label: '坐席(agent', role: 'agent' },
{ userid: 'dev-admin-001', label: '管理员(admin', role: 'admin' },
]
// --------------------------------------------------------------------------
// 方法
// --------------------------------------------------------------------------
@@ -251,11 +283,58 @@ async function processOAuthCallback(code: string, state?: string): Promise<void>
}
}
// --------------------------------------------------------------------------
// 测试通道方法(REQ-通用-006
// --------------------------------------------------------------------------
/**
* 探测测试通道可用性(内网 200 → 显示测试入口;公网 403 → 隐藏)
*/
async function checkTestChannel(): Promise<void> {
try {
const apiClient = (await import('@/api/index')).default
const resp = await apiClient.get('/dev/health', { timeout: 3000 })
if (resp?.data?.data?.dev_mode) {
showTestPanel.value = true
}
} catch {
showTestPanel.value = false
}
}
/**
* 测试账号免企微登录(仅内网):/api/dev/login → token 写入 h5_token
*/
async function testLogin(): Promise<void> {
const target = testUsers.find((u) => u.userid === testUser.value)
if (!target) return
testLogining.value = true
try {
const apiClient = (await import('@/api/index')).default
const resp = await apiClient.get('/dev/login', {
params: { userid: target.userid, name: target.label, role: target.role },
})
await handleLoginSuccess(resp.data.data.token, {
userid: target.userid,
name: target.label,
})
} catch (error) {
console.error('测试登录失败:', error)
showToast('测试登录失败,请确认内网环境')
} finally {
testLogining.value = false
}
}
// --------------------------------------------------------------------------
// 生命周期
// --------------------------------------------------------------------------
onMounted(async () => {
// 探测测试通道(不阻塞主登录流程)
checkTestChannel()
// 检查 URL 中的 code 参数(OAuth2 回调)
const urlParams = new URLSearchParams(window.location.search)
const code = urlParams.get('code')
@@ -421,4 +500,40 @@ onUnmounted(() => {
padding: 40px 0;
color: var(--text-secondary, #606266);
}
/* 测试账号登录(REQ-通用-006 */
.test-login {
margin-top: 12px;
}
.test-login-title {
text-align: center;
font-size: 13px;
color: var(--text-secondary, #606266);
margin-bottom: 8px;
}
.test-login-row {
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
}
.test-login-select {
height: 30px;
border: 1px solid #dcdfe6;
border-radius: 4px;
padding: 0 8px;
font-size: 13px;
color: #303133;
background: #fff;
}
.test-login-hint {
text-align: center;
font-size: 12px;
color: var(--text-tertiary, #909399);
margin: 8px 0 0;
}
</style>