Compare commits
7 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 9fcee9df0e | |||
| 6be361fb63 | |||
| f1b12b7871 | |||
| b4e21e3150 | |||
| 5db3079d41 | |||
| d8e7dbe998 | |||
| 5311a526af |
@@ -3,9 +3,14 @@
|
||||
> **版本**: v1.0
|
||||
> **日期**: 2026-07-14
|
||||
> **作者**: 许清楚(产品经理)
|
||||
> **状态**: 待评审
|
||||
> **状态**: 已实现(双端能力已落地;员工端 H5 工具栏「群聊」入口于 2026-08-08 完成接线)
|
||||
> **子系统**: 05-用户端H5
|
||||
> **模块**: 群聊
|
||||
> **关联文档**:
|
||||
> - 技术方案(双模式): `docs/02-技术文档/技术架构/技术方案-REQ-用户-001-群聊双模式-v1.0.md`
|
||||
> - 架构设计: `docs/02-技术文档/01-架构设计/群聊参与者展开缩略双模式-架构设计.md`
|
||||
> - 技术方案(入口接线): `docs/02-技术文档/技术方案-REQ-用户-001-群聊入口接线-v1.0.md`
|
||||
> - 任务说明书(入口接线): `docs/07-项目管理/任务说明书/任务说明书-REQ-用户-001-群聊入口接线.md`
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -0,0 +1,287 @@
|
||||
# 技术方案 — 员工端 H5 群聊入口接线
|
||||
|
||||
> **需求编号**: REQ-用户-001-群聊入口接线
|
||||
> **版本**: v1.0
|
||||
> **日期**: 2026-08-08
|
||||
> **作者**: 高见远(架构师)
|
||||
> **状态**: 已实现
|
||||
> **子系统**: 用户端H5
|
||||
> **模块**: 群聊
|
||||
> **关联文档**:
|
||||
> - PRD: `docs/01-产品文档/05-用户端H5/PRD-REQ-用户-001-群聊双模式-v1.0.md`
|
||||
> - 既有技术方案: `docs/02-技术文档/技术架构/技术方案-REQ-用户-001-群聊双模式-v1.0.md`
|
||||
> - 既有架构设计: `docs/02-技术文档/01-架构设计/群聊参与者展开缩略双模式-架构设计.md`
|
||||
> - 任务说明书: `docs/07-项目管理/任务说明书/任务说明书-REQ-用户-001-群聊入口接线.md`
|
||||
|
||||
> **补充参考**(非三件套,仅供实现比对,路径均已核验存在):
|
||||
> - 原型(用户端): `docs/01-产品文档/05-用户端H5/原型-REQ-用户-001-群聊双模式-v1.0.html`
|
||||
> - 原型(坐席端): `docs/01-产品文档/04-坐席工作台/原型-REQ-坐席-003-群聊双模式-v1.0.html`
|
||||
> - 工具栏基线交付清单: `docs/01-产品文档/02-会话管理/交付-REQ-会话-001-工具栏统一设计v1.9-开发交付清单.md`
|
||||
|
||||
---
|
||||
|
||||
## 一、背景与目标
|
||||
|
||||
### 1.1 需求触发原点
|
||||
|
||||
使用者(IT 支持组组长)提出的原始诉求是两句话:
|
||||
|
||||
1. **确认坐席端的群聊功能到底做没做?**
|
||||
2. **如果做了,把员工端工具栏上那颗「群聊」按钮接上。**
|
||||
|
||||
### 1.2 核查结论:坐席端早已实现
|
||||
|
||||
代码核查确认,**坐席端群聊能力完整可用,本次无需任何改动**。其入口是「邀请」按钮,而非名为「群聊」的按钮:
|
||||
|
||||
| 能力 | 文件 · 行号 | 状态 |
|
||||
|------|------------|------|
|
||||
| 参与者横条 | `src/frontend-agent/src/components/chat/ChatArea.vue` L38 `<ParticipantBar>` | ✅ 已实现 |
|
||||
| 邀请事件出口 | `ChatArea.vue` L50 `@invite="showInviteParticipantDialog = true"` | ✅ 已实现 |
|
||||
| 邀请参与者弹窗 | `ChatArea.vue` L221-222 `<InviteParticipantDialog v-model="showInviteParticipantDialog">` | ✅ 已实现 |
|
||||
| 摇人邀请弹窗 | `ChatArea.vue` L214 `<InviteDialog>` | ✅ 已实现 |
|
||||
| 就地展开面板 | `src/frontend-agent/src/components/conversation/ParticipantBar.vue` L78 `<ParticipantExpandedPanel>` | ✅ 已实现 |
|
||||
| 组件注册 | `ChatArea.vue` L277-279 三处 import | ✅ 已实现 |
|
||||
|
||||
**"功能没做"是误判,误判的根因是文档不可发现**:PRD `PRD-REQ-用户-001-群聊双模式-v1.0.md` 头部**缺失 `关联文档` 字段**,从 PRD 无法反向索引到技术方案与架构设计;而实现层的入口叫「邀请」、需求层的名字叫「群聊」,术语不对齐,检索时对不上。本次一并回写 PRD 关联链路(见 §六)。
|
||||
|
||||
### 1.3 本方案的目标
|
||||
|
||||
**本方案不新增任何群聊能力,只记录并固化一件事:员工端 H5 工具栏「群聊」按钮的入口接线。**
|
||||
|
||||
REQ-用户-001 的群聊双模式(缩略头像条 + 展开参与者面板 + 邀请 + 退出)在双端均已完整实现,后端接口与 WebSocket 双池推送全部打通。唯一缺口是:员工端 H5 那颗 `title="群聊"` 的按钮,点击后只弹一句 `showToast('群聊功能开发中')` —— 这句占位 toast 正是使用者判定"功能未开发"的直接来源。
|
||||
|
||||
本次改动即:把这句占位 toast 换成对已有 store action 的调用。
|
||||
|
||||
| 维度 | 决策 |
|
||||
|------|------|
|
||||
| 后端改动 | **无** |
|
||||
| store 改动 | **无**(`toggleParticipantPanel()` 已存在并已导出) |
|
||||
| 新增组件 | **无** |
|
||||
| 前端改动文件 | `InputBar.vue` ×1 + 测试 ×2 |
|
||||
| 本次范围 | **仅员工端 H5**,坐席端零改动(用户已拍板) |
|
||||
|
||||
---
|
||||
|
||||
## 二、范围与边界
|
||||
|
||||
### 2.1 在范围内
|
||||
|
||||
- ✅ 仅**员工端 H5**(`src/frontend-h5/**`)
|
||||
- ✅ `InputBar.vue` 中 `handleGroupChat()` 的入口接线
|
||||
- ✅ 两个测试文件的断言同步
|
||||
- ✅ PRD ↔ 技术方案 ↔ 任务说明书 三件套关联回写
|
||||
|
||||
### 2.2 明确不做(Out of Scope)
|
||||
|
||||
- ❌ **0 后端改动**:不改接口、不改模型、不改推送逻辑
|
||||
- ❌ **不部署**:仅留工作区改动,部署另行安排
|
||||
- ❌ **坐席端不动**:`src/frontend-agent/**` 零改动(已有「邀请」入口,用户已拍板)
|
||||
- ❌ 不新增坐席端第 6 个工具栏按钮;将来若做,采用「邀请」**改名**为「群聊」的方式,保持 5 键基线(方向已定,本次不执行)
|
||||
- ❌ 不做需求编号收敛、不处理其他文档治理项
|
||||
- ❌ 不改 `ChatPanel.vue` L69 的 `ParticipantStrip` 渲染条件
|
||||
|
||||
---
|
||||
|
||||
## 三、实现细节
|
||||
|
||||
> 以下文件与行号均以当前工作区代码为准,逐条 Read 核验。
|
||||
|
||||
### 3.1 入口断点(改造前)
|
||||
|
||||
| 位置 | 内容 |
|
||||
|------|------|
|
||||
| `src/frontend-h5/src/components/chat/InputBar.vue` **L345-360** | 拱形工具栏第 5 个按钮:L348 `title="群聊"`、L349 `aria-label="群聊"`、L350 `@click="handleGroupChat"` |
|
||||
| `src/frontend-h5/src/components/chat/InputBar.vue` **L872-874**(v1.9 时期) | 占位实现 `showToast('群聊功能开发中')` |
|
||||
|
||||
改造前的函数体只有一行 toast,且注释写着「store 暂无 group chat action……后续接入真实群聊会话时,把 toast 替换为 `store.startGroupChat()` 即可」。该注释在写下之后即已过期 —— `toggleParticipantPanel()` 早已在 store 落地,而 `startGroupChat` 是一个**从未存在过的虚构 action**。注释与代码事实脱节,是按钮长期停留在占位态的直接原因。
|
||||
|
||||
### 3.2 目标实现(改造后 · 已落地)
|
||||
|
||||
`InputBar.vue` 现状 **L867-899**(常量 L868 / L871,函数 L886-899):
|
||||
|
||||
```ts
|
||||
/** 无会话时点击群聊的提示文案。 */
|
||||
const GROUP_CHAT_NO_CONVERSATION_TIP = '请先发起会话'
|
||||
|
||||
/** 会话内暂无其他同事时的邀请引导文案。 */
|
||||
const GROUP_CHAT_EMPTY_TIP = '还没有其他同事,点击「邀请参与者」拉同事进群'
|
||||
|
||||
function handleGroupChat(): void {
|
||||
if (!currentConversation.value) {
|
||||
showToast(GROUP_CHAT_NO_CONVERSATION_TIP)
|
||||
return
|
||||
}
|
||||
|
||||
const hasParticipants = store.participants.length > 0
|
||||
store.toggleParticipantPanel()
|
||||
|
||||
// 仅在「由收起切换为展开」且暂无其他同事时补引导,避免收起面板时也弹提示
|
||||
if (!hasParticipants && store.participantPanelVisible) {
|
||||
showToast(GROUP_CHAT_EMPTY_TIP)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
> **与原始设计的差异说明**:契约描述为「先算 `nextVisible = !participantPanelVisible`,若 `!hasParticipants && nextVisible` 则提示」。落地实现改为**先 `toggle()` 再读 `store.participantPanelVisible`**,两者语义完全等价(toggle 后的实际值即 `nextVisible`),且读实际状态比预测值更稳健 —— 若将来 action 内部新增条件分支,实现不会与预测值脱节。文案常量与三分支行为与契约一致。
|
||||
|
||||
### 3.3 handleGroupChat 行为契约(三分支)
|
||||
|
||||
| # | 前置条件 | toast | `toggleParticipantPanel()` |
|
||||
|---|---------|-------|---------------------------|
|
||||
| 1 | 无当前会话 | `'请先发起会话'` | **不调用**(提前 return) |
|
||||
| 2 | 有会话 + `participants.length > 0` | 无 | 调用 1 次 |
|
||||
| 3 | 有会话 + `participants.length === 0` 且切换为展开 | `'还没有其他同事,点击「邀请参与者」拉同事进群'` | 调用 1 次 |
|
||||
|
||||
**分支 3 的设计依据**:零参与者时 `ChatPanel.vue` L69 的 `ParticipantStrip` 不渲染,用户界面上没有任何群聊线索,因此必须由 toast 补一句引导。而面板本身照常打开 —— `ParticipantList` 在零被邀请人时仍会渲染「坐席 + 我(发起人)」,且会话发起人的 `canInvite` 恒为 `true`,「+ 邀请参与者」按钮天然可达,不会出现空白面板。
|
||||
|
||||
### 3.4 依赖的 store 契约(只读引用,零修改)
|
||||
|
||||
`src/frontend-h5/src/stores/conversation.ts`:
|
||||
|
||||
| 契约项 | 行号 | 说明 |
|
||||
|--------|------|------|
|
||||
| `currentConversation` | **L135** | `ref<ConversationInfo \| null>(null)` — 分支 1 的判据 |
|
||||
| `participants` | **L191** | `ref<ParticipantItem[]>([])` — 分支 2/3 的判据 |
|
||||
| `participantPanelVisible` | **L194** | `ref<boolean>(false)` — 面板显隐单一真值源 |
|
||||
| `toggleParticipantPanel()` | **L1296** | `participantPanelVisible.value = !participantPanelVisible.value`;无网络请求、无持久化 |
|
||||
| 导出 | L2185 / L2214 | 状态与 action 均已在 return 块内 |
|
||||
| 重置 | L1367 | 会话清理时置回 `false` |
|
||||
|
||||
> ⚠️ 语义提示:这是 **toggle** 而非 `open`。当前按钮为唯一触发点,用户在面板内通过关闭按钮直接置 `false`,不会与 toggle 产生状态错位。若后续新增第二个触发入口,需评估是否补一个 `openParticipantPanel()` 语义化 action。
|
||||
|
||||
### 3.5 承载面板的容器(零修改)
|
||||
|
||||
`src/frontend-h5/src/components/chat/ChatPanel.vue`:
|
||||
|
||||
| 位置 | 内容 |
|
||||
|------|------|
|
||||
| **L69** | `<ParticipantStrip v-if="store.participants.length > 0" />` — 缩略头像条,零参与者时不渲染 |
|
||||
| **L133-142** | 底部弹层 `<van-popup :show="store.participantPanelVisible" position="bottom" round teleport="body" :style="{ maxHeight: '60vh' }">`,其中 **L141** 挂载 `<ParticipantList />` |
|
||||
|
||||
> 原始描述记为 L135-142,实测弹层完整块为 **L133-142**(`van-popup` 起始标签在 L133,L132 为注释行),`<ParticipantList />` 位于 L141。以实测为准。
|
||||
|
||||
### 3.6 为什么不在 InputBar 里挂 InviteParticipantSheet
|
||||
|
||||
`InviteParticipantSheet.vue` **由面板内部自行挂载**,`handleGroupChat` 不直接调用它:
|
||||
|
||||
- 挂载点一:`ParticipantStrip.vue` L91
|
||||
- 挂载点二:`ParticipantList.vue` L115
|
||||
|
||||
若 InputBar 再挂一次,全应用将出现**第三份**同名弹层实例,带来状态互相覆盖与 `v-model` 归属混乱的风险。分支 3 的引导文案明确指向面板内的「邀请参与者」按钮,用户路径为:**群聊按钮 → 面板打开 → 点面板内「+ 邀请参与者」→ 已挂载的 Sheet 弹出**,全程复用既有实例。
|
||||
|
||||
### 3.7 架构一致性:为何选 store 驱动而非 emit
|
||||
|
||||
`InputBar.vue` L903 注释白纸黑字写明:**「InputBar 是 v1.3 唯一坐席入口,不向 ChatPanel emit。」** 全文件 `defineEmits` 零匹配,InputBar 至今保持「零 emit、纯 store 驱动」的单向数据流。为一个按钮破例会留下两套并存的通信范式,故定案 store 驱动 —— 这也让改动收敛到 1 个业务文件。
|
||||
|
||||
---
|
||||
|
||||
## 四、测试影响
|
||||
|
||||
### 4.1 受影响的测试文件
|
||||
|
||||
| 文件 | 改动性质 |
|
||||
|------|---------|
|
||||
| `src/frontend-h5/src/components/chat/InputBar.test.ts` | 同步断言 |
|
||||
| `src/frontend-h5/src/components/chat/__tests__/InputBar.vitest.test.ts` | 同步断言 |
|
||||
|
||||
### 4.2 契约常量对齐
|
||||
|
||||
两个测试文件**必须复刻 `InputBar.vue` 的文案常量**,不得内联裸字符串:
|
||||
|
||||
| 常量 | 值 |
|
||||
|------|-----|
|
||||
| `GROUP_CHAT_NO_CONVERSATION_TIP` | `'请先发起会话'` |
|
||||
| `GROUP_CHAT_EMPTY_TIP` | `'还没有其他同事,点击「邀请参与者」拉同事进群'` |
|
||||
|
||||
现状:`InputBar.test.ts` L177 / L180 已对齐(含注释标注「与 InputBar.vue 对齐」)。
|
||||
|
||||
### 4.3 弃用串必须 0 命中
|
||||
|
||||
| 弃用串 | 断言位置 | 说明 |
|
||||
|--------|---------|------|
|
||||
| `'群聊功能开发中'` | `InputBar.test.ts` L848 · `InputBar.vitest.test.ts` L867 | 含注释在内,`InputBar.vue` 全文不得再出现 |
|
||||
| `'startGroupChat'` | `InputBar.test.ts` L861 · `InputBar.vitest.test.ts` L871 | 虚构 action,含注释在内不得再被引用 |
|
||||
|
||||
> **R1 风险(预期内的红)**:存量测试原本断言 `showToast('群聊功能开发中')`,接线后必然变红。这是预期结果,**不得靠删除测试绕过**,必须改为断言新契约。
|
||||
|
||||
### 4.4 测试覆盖现状
|
||||
|
||||
| 用例 | 位置 |
|
||||
|------|------|
|
||||
| 三分支行为契约 | `InputBar.test.ts` L797 `describe('InputBar v2.1 — handleGroupChat 三分支行为契约')` |
|
||||
| 按钮可访问性属性不变 | `InputBar.vitest.test.ts` L625(`title` / `aria-label` / `@click` 三属性正则) |
|
||||
| 接线目标为 `toggleParticipantPanel` | `InputBar.test.ts` L861 · `InputBar.vitest.test.ts` L847 |
|
||||
|
||||
---
|
||||
|
||||
## 五、验收标准
|
||||
|
||||
### 5.1 功能验收
|
||||
|
||||
| # | 场景 | 预期 |
|
||||
|---|------|------|
|
||||
| A1 | 未发起会话 → 点「群聊」 | toast「请先发起会话」;**不**打开参与者面板 |
|
||||
| A2 | 有会话(含参与者)→ 点「群聊」 | 参与者面板打开;再点一次关闭(开/关可逆) |
|
||||
| A3 | 有会话 + 零参与者 → 点「群聊」 | 面板打开 **且** toast「还没有其他同事,点击「邀请参与者」拉同事进群」 |
|
||||
| A4 | A3 状态下再点「群聊」收起面板 | 面板关闭,**不**重复弹邀请引导 |
|
||||
| A5 | 全代码库 | 无任何 `startGroupChat` 调用;`InputBar.vue` 无 `'群聊功能开发中'` |
|
||||
| A6 | A3 中点面板内「+ 邀请参与者」 | 复用既有 `InviteParticipantSheet`,无第三处挂载 |
|
||||
| A7 | 工具栏基线 | 5 按钮顺序 emoji / 文件 / 坐席 / 语音 / 群聊 不变,拱形轨道无形变,L345-360 模板未被改动 |
|
||||
|
||||
### 5.2 质量门禁
|
||||
|
||||
- `pnpm test` 全量通过,重点 `InputBar` 相关 2 个测试文件
|
||||
- ESLint 无新增告警(`showToast` import 在分支 1/3 仍被使用,不会变成 unused)
|
||||
- 坐席端零改动,不参与本次回归
|
||||
|
||||
### 5.3 文档验收
|
||||
|
||||
- PRD 头部 `关联文档` 字段存在,且指向本方案 + 既有技术方案 + 既有架构设计
|
||||
- 本方案 `关联文档` 反向指向 PRD,双向一致
|
||||
- 任务说明书已建立并被三件套互相引用
|
||||
|
||||
---
|
||||
|
||||
## 六、关联文档回写(本轮同步执行)
|
||||
|
||||
本轮同时修复「PRD 不可发现」这一根因:
|
||||
|
||||
| 文档 | 回写内容 |
|
||||
|------|---------|
|
||||
| `PRD-REQ-用户-001-群聊双模式-v1.0.md` | 头部新增 `关联文档` 字段(指向既有技术方案 + 既有架构设计 + 本方案);`状态` 由 `待评审` → `已实现` |
|
||||
| 本方案 | `关联文档` 指向 PRD + 既有技术方案 + 既有架构设计 + 任务说明书 |
|
||||
| `任务说明书-REQ-用户-001-群聊入口接线.md` | 新建,引用上述四份文档 |
|
||||
|
||||
> **遗留观察(不在本轮范围)**:`docs/02-技术文档/01-架构设计/群聊参与者展开缩略双模式-架构设计.md` 头部的 PRD 指向 `docs/01-产品文档/02-会话管理/群聊参与者展开缩略双模式-PRD.md`,与 `05-用户端H5/PRD-REQ-用户-001-群聊双模式-v1.0.md` 构成同一需求的两份 PRD(两份文件均真实存在,非死链)。属需求编号收敛范畴,本轮按用户要求不处理,登记备查。
|
||||
|
||||
---
|
||||
|
||||
## 七、影响面与风险
|
||||
|
||||
### 7.1 影响面
|
||||
|
||||
| 文件 | 改动性质 | 风险 |
|
||||
|------|---------|------|
|
||||
| `src/frontend-h5/src/components/chat/InputBar.vue` | 替换单个函数体 + 新增 2 个文案常量 | 🟢 极低 |
|
||||
| `src/frontend-h5/src/components/chat/InputBar.test.ts` | 同步断言 | 🟢 极低 |
|
||||
| `src/frontend-h5/src/components/chat/__tests__/InputBar.vitest.test.ts` | 同步断言 | 🟢 极低 |
|
||||
|
||||
**零影响面清单**:后端 · store · 路由 · 网络请求 · 数据库 · `ChatPanel.vue` · `ParticipantList.vue` · `ParticipantStrip.vue` · `InviteParticipantSheet.vue` · 坐席端全部文件 —— 均不改动。
|
||||
|
||||
### 7.2 风险登记
|
||||
|
||||
| # | 风险 | 等级 | 缓解 |
|
||||
|---|------|------|------|
|
||||
| R1 | 存量测试断言旧占位文案,改动后必红 | 🟠 中 | 已纳入交付清单,两个测试文件同步更新;不得删测试绕过 |
|
||||
| R2 | `showToast` import 变为未使用 | 🟡 低 | 分支 1/3 仍在使用,import 保持有效 |
|
||||
| R3 | toggle 语义在未来多入口场景下状态错位 | 🟡 低 | 当前单入口;后续新增入口时引入 `openParticipantPanel()` |
|
||||
| R4 | 坐席未接入且零被邀请人时,面板仅一行「我」 | 🟡 低 | 可接受;分支 3 的 toast 已给出下一步引导 |
|
||||
| R5 | 误改工具栏 DOM,破坏 v1.9 拱形轨道基线 | 🟠 中 | **严禁改动 L345-360 模板**,只改 script;验收项 A7 比对 5 按钮基线 |
|
||||
|
||||
---
|
||||
|
||||
## 八、变更记录
|
||||
|
||||
| 日期 | 版本 | 变更 | 作者 |
|
||||
|------|------|------|------|
|
||||
| 2026-08-08 | v1.0 | 初版;定案 store 驱动方案,落地三分支契约;补齐 `需求编号` / `子系统` / `模块` / `关联文档` 头部字段;状态置为「已实现」;同步 PRD 关联回写 | 高见远 |
|
||||
@@ -0,0 +1,295 @@
|
||||
# 任务说明书 - 员工端 H5 群聊入口接线
|
||||
|
||||
> **REQ 编号**: REQ-用户-001-群聊入口接线
|
||||
> **版本**: v1.0
|
||||
> **日期**: 2026-08-08
|
||||
> **作者**: 高见远(架构师)
|
||||
> **状态**: ✅ 已完成(代码已落地,未部署)
|
||||
> **子系统**: 05-用户端H5
|
||||
> **模块**: 群聊
|
||||
> **关联文档**:
|
||||
> - **PRD**:`docs/01-产品文档/05-用户端H5/PRD-REQ-用户-001-群聊双模式-v1.0.md`
|
||||
> - **本次技术方案**:`docs/02-技术文档/技术方案-REQ-用户-001-群聊入口接线-v1.0.md`
|
||||
> - **既有技术方案**:`docs/02-技术文档/技术架构/技术方案-REQ-用户-001-群聊双模式-v1.0.md`
|
||||
> - **既有架构设计**:`docs/02-技术文档/01-架构设计/群聊参与者展开缩略双模式-架构设计.md`
|
||||
|
||||
---
|
||||
|
||||
## 📋 一、基本信息
|
||||
|
||||
| 项目 | 内容 |
|
||||
|------|------|
|
||||
| **任务名称** | 员工端 H5 工具栏「群聊」按钮入口接线 |
|
||||
| **任务ID** | REQ-用户-001-群聊入口接线 |
|
||||
| **优先级** | 🟠P1(功能可见性缺陷:能力已具备但入口断路,导致用户误判功能未开发) |
|
||||
| **类型** | 功能开发(入口接线)+ 文档完善(关联回写) |
|
||||
| **状态** | 已完成(代码 + 文档已落地,**未部署**) |
|
||||
| **负责人** | 前端工程师(实现)/ 高见远(方案与验收) |
|
||||
| **创建日期** | 2026-08-08 |
|
||||
| **计划完成日期** | 2026-08-08 |
|
||||
| **预估工期** | 0.5 天(含测试同步与文档回写) |
|
||||
| **风险等级** | 低(仅 1 个前端业务文件,0 后端 / 0 store / 0 新组件) |
|
||||
| **回滚预案** | 单函数体回退即可;无数据与接口副作用 |
|
||||
|
||||
---
|
||||
|
||||
## 📥 二、输入项来源
|
||||
|
||||
### 2.1 产品需求
|
||||
|
||||
| 来源文档 | 相关章节 | 说明 |
|
||||
|----------|----------|------|
|
||||
| `docs/01-产品文档/05-用户端H5/PRD-REQ-用户-001-群聊双模式-v1.0.md` | §3 FE-H5-01 / FE-H5-02 | 缩略头像条 + 底部弹出展开面板的需求定义 |
|
||||
|
||||
### 2.2 技术方案
|
||||
|
||||
| 来源文档 | 相关章节 | 说明 |
|
||||
|----------|----------|------|
|
||||
| `docs/02-技术文档/技术方案-REQ-用户-001-群聊入口接线-v1.0.md` | §三 实现细节 / §五 验收标准 | 本次接线的实现契约与验收口径 |
|
||||
| `docs/02-技术文档/技术架构/技术方案-REQ-用户-001-群聊双模式-v1.0.md` | 全文 | 群聊双模式既有实现(本次仅复用,不改动) |
|
||||
| `docs/02-技术文档/01-架构设计/群聊参与者展开缩略双模式-架构设计.md` | Part A §1 | `useParticipantDisplay` 数据规范化层与双模式组件架构 |
|
||||
|
||||
### 2.3 原型设计
|
||||
|
||||
| 来源文档 | 页面 | 说明 |
|
||||
|----------|------|------|
|
||||
| `docs/01-产品文档/05-用户端H5/原型-REQ-用户-001-群聊双模式-v1.0.html` | H5 缩略 / 展开 | 参与者面板交互基准 |
|
||||
| `docs/01-产品文档/02-会话管理/原型-REQ-会话-001-工具栏统一设计v1.9-员工端落地版.html` | 工具栏 | **5 按钮基线**,本次严禁改动其 DOM 结构 |
|
||||
|
||||
### 2.4 需了解的现有代码(历史现状)
|
||||
|
||||
| 模块/文件 | 说明 | 需了解的内容 |
|
||||
|-----------|------|-------------|
|
||||
| `src/frontend-h5/src/components/chat/InputBar.vue` | 工具栏 + 输入区 | L345-360 群聊按钮模板;改造前 L872-874 占位 toast;L903 「不向 ChatPanel emit」约定 |
|
||||
| `src/frontend-h5/src/stores/conversation.ts` | 会话 store | L135 / L191 / L194 状态,L1296 `toggleParticipantPanel()` |
|
||||
| `src/frontend-h5/src/components/chat/ChatPanel.vue` | 聊天页容器 | L69 缩略条渲染条件;L133-142 底部弹层 |
|
||||
| `src/frontend-h5/src/components/chat/ParticipantList.vue` | 展开面板 | L115 已挂载 `InviteParticipantSheet`;发起人 `canInvite` 恒真 |
|
||||
| `src/frontend-h5/src/components/chat/ParticipantStrip.vue` | 缩略头像条 | L91 已挂载 `InviteParticipantSheet`(第二处) |
|
||||
|
||||
### 2.5 上游依赖
|
||||
|
||||
- 无 Alembic 迁移需求
|
||||
- 无后端 API 变更
|
||||
- 无新增 npm 依赖
|
||||
- 坐席端 `src/frontend-agent/**` 零改动
|
||||
|
||||
---
|
||||
|
||||
## 🎯 三、范围
|
||||
|
||||
### 3.1 在范围内
|
||||
|
||||
- ✅ 仅**员工端 H5**(`src/frontend-h5/**`)
|
||||
- ✅ `InputBar.vue` 中 `handleGroupChat()` 接线(占位 toast → 真实能力)
|
||||
- ✅ 两个测试文件断言同步(契约常量化 + 弃用串清零)
|
||||
- ✅ PRD ↔ 技术方案 ↔ 任务说明书 三件套关联回写
|
||||
|
||||
### 3.2 明确不做
|
||||
|
||||
- ❌ **0 后端改动**(接口 / 模型 / 推送均不动)
|
||||
- ❌ **不部署**(仅留工作区改动,部署另行安排)
|
||||
- ❌ **坐席端不动**(已有「邀请」入口,用户已拍板)
|
||||
- ❌ 不在 InputBar 挂第三个 `InviteParticipantSheet`
|
||||
- ❌ 不改 `ChatPanel.vue` L69 渲染条件
|
||||
- ❌ 不做需求编号收敛及其他文档治理
|
||||
|
||||
---
|
||||
|
||||
## 📤 四、输出成果要求(交付物)
|
||||
|
||||
### 4.1 代码交付物
|
||||
|
||||
| # | 文件 | 类型 | 改动说明 | 状态 |
|
||||
|---|------|------|---------|------|
|
||||
| 1 | `src/frontend-h5/src/components/chat/InputBar.vue` | 代码 | 新增 2 个文案常量 + 重写 `handleGroupChat()` 三分支 | ✅ 已落地 |
|
||||
| 2 | `src/frontend-h5/src/components/chat/InputBar.test.ts` | 测试 | 三分支契约用例 + 弃用串断言 | ✅ 已落地 |
|
||||
| 3 | `src/frontend-h5/src/components/chat/__tests__/InputBar.vitest.test.ts` | 测试 | 按钮属性正则 + 接线目标断言 | ✅ 已落地 |
|
||||
|
||||
### 4.2 文档交付物
|
||||
|
||||
| # | 文档 | 类型 | 状态 |
|
||||
|---|------|------|------|
|
||||
| 1 | `docs/02-技术文档/技术方案-REQ-用户-001-群聊入口接线-v1.0.md` | 文档 | ✅ 已建 |
|
||||
| 2 | `docs/07-项目管理/任务说明书/任务说明书-REQ-用户-001-群聊入口接线.md`(本文档) | 文档 | ✅ 当前 |
|
||||
| 3 | `docs/01-产品文档/05-用户端H5/PRD-REQ-用户-001-群聊双模式-v1.0.md` | 文档 | ✅ 已回写(新增 `关联文档`,状态 → 已实现) |
|
||||
|
||||
### 4.3 代码要求
|
||||
|
||||
- 遵循项目代码规范;ESLint 零新增告警
|
||||
- 文案**必须常量化**(`GROUP_CHAT_NO_CONVERSATION_TIP` / `GROUP_CHAT_EMPTY_TIP`),测试侧复刻同名常量,禁止两侧各写裸字符串
|
||||
- 严禁改动 `InputBar.vue` L345-360 模板(v1.9 工具栏基线)
|
||||
- 严禁通过删除/跳过测试来消红
|
||||
|
||||
### 4.4 部署交付物
|
||||
|
||||
- **本次不部署**。工作区改动保留,交由后续统一发布窗口处理。
|
||||
|
||||
---
|
||||
|
||||
## 📁 五、涉及文件清单(含行号)
|
||||
|
||||
### 5.1 需修改
|
||||
|
||||
| 文件 | 行号 | 改动 |
|
||||
|------|------|------|
|
||||
| `src/frontend-h5/src/components/chat/InputBar.vue` | **L872-874**(改造前占位) | `showToast('群聊功能开发中')` 整段替换 |
|
||||
| 同上 | **L867-899**(改造后现状) | L868 `GROUP_CHAT_NO_CONVERSATION_TIP`、L871 `GROUP_CHAT_EMPTY_TIP`、L886-899 `handleGroupChat()` |
|
||||
| `src/frontend-h5/src/components/chat/InputBar.test.ts` | L177 / L180 / L186-187 / L797 / L848 / L861 | 契约常量、弃用串常量、三分支 describe、清零断言 |
|
||||
| `src/frontend-h5/src/components/chat/__tests__/InputBar.vitest.test.ts` | L625 / L847 / L867 / L871 | 按钮属性正则、接线目标、两处弃用串断言 |
|
||||
|
||||
### 5.2 只读引用(严禁修改)
|
||||
|
||||
| 文件 | 行号 | 内容 |
|
||||
|------|------|------|
|
||||
| `src/frontend-h5/src/components/chat/InputBar.vue` | L345-360 | 群聊按钮模板(L348 `title="群聊"`、L349 `aria-label="群聊"`、L350 `@click="handleGroupChat"`) |
|
||||
| `src/frontend-h5/src/stores/conversation.ts` | L135 | `currentConversation` |
|
||||
| 同上 | L191 | `participants` |
|
||||
| 同上 | L194 | `participantPanelVisible` |
|
||||
| 同上 | L1296 | `toggleParticipantPanel()` |
|
||||
| 同上 | L2185 / L2214 | 状态与 action 导出 |
|
||||
| 同上 | L1367 | 会话清理时面板状态复位 |
|
||||
| `src/frontend-h5/src/components/chat/ChatPanel.vue` | L69 | `<ParticipantStrip v-if="store.participants.length > 0" />` |
|
||||
| 同上 | L133-142 | 底部弹层 `van-popup`,L141 `<ParticipantList />` |
|
||||
| `src/frontend-h5/src/components/chat/ParticipantList.vue` | L115 | `InviteParticipantSheet` 挂载点一 |
|
||||
| `src/frontend-h5/src/components/chat/ParticipantStrip.vue` | L91 | `InviteParticipantSheet` 挂载点二 |
|
||||
|
||||
> `InviteParticipantSheet.vue` 由面板内部挂载,`handleGroupChat` **不直接调用**,避免出现第三次挂载。
|
||||
|
||||
### 5.3 坐席端(本次零改动,仅登记已实现事实)
|
||||
|
||||
| 文件 | 行号 | 内容 |
|
||||
|------|------|------|
|
||||
| `src/frontend-agent/src/components/chat/ChatArea.vue` | L38 / L50 | `<ParticipantBar>` + `@invite` |
|
||||
| 同上 | L214 / L221-222 | `<InviteDialog>` / `<InviteParticipantDialog>` |
|
||||
| 同上 | L277-279 | 三处组件 import |
|
||||
| `src/frontend-agent/src/components/conversation/ParticipantBar.vue` | L78 | `<ParticipantExpandedPanel>` |
|
||||
|
||||
---
|
||||
|
||||
## 📊 六、工作项拆分
|
||||
|
||||
| # | 子任务 | 涉及文件 | 负责人 | 预估工时 | 状态 |
|
||||
|---|--------|---------|--------|---------|------|
|
||||
| 1 | **接线 `handleGroupChat`** —— 占位 toast 替换为三分支实现,文案常量化 | `InputBar.vue` L867-899 | 前端 | 1h | ✅ 已完成 |
|
||||
| 2 | **同步两测试** —— 复刻契约常量,新增三分支用例,弃用串 `'群聊功能开发中'` / `'startGroupChat'` 断言 0 命中 | `InputBar.test.ts`、`__tests__/InputBar.vitest.test.ts` | 前端 | 1.5h | ✅ 已完成 |
|
||||
| 3 | **文档关联回写** —— 新建技术方案与本说明书;PRD 头部补 `关联文档`、状态改 `已实现` | 3 份文档 | 架构 | 1h | ✅ 已完成 |
|
||||
|
||||
### 子任务验收要点
|
||||
|
||||
- **子任务 1**:分支 1 提前 return 不得调用 toggle;分支 3 的 toast 仅在「收起 → 展开」方向触发
|
||||
- **子任务 2**:不得以删除或 skip 测试的方式消红(R1 为预期内的红)
|
||||
- **子任务 3**:PRD 与技术方案的 `关联文档` 必须双向一致
|
||||
|
||||
---
|
||||
|
||||
## 🔧 七、验证方式
|
||||
|
||||
### 7.1 单元测试
|
||||
|
||||
| 用例 | 断言 | 位置 |
|
||||
|------|------|------|
|
||||
| 无会话点击 | toast = `'请先发起会话'`,**未**调用 `toggleParticipantPanel` | `InputBar.test.ts` L797 起 |
|
||||
| 有会话 + 有参与者 | `toggleParticipantPanel()` 恰好 1 次,无 toast | 同上 |
|
||||
| 有会话 + 零参与者(展开方向) | `toggleParticipantPanel()` 1 次 + toast = `GROUP_CHAT_EMPTY_TIP` | 同上 |
|
||||
| 收起方向不重复提示 | 零参与者收起面板时无 toast | 同上 |
|
||||
| 弃用串清零 | `'群聊功能开发中'` / `'startGroupChat'` 在 `InputBar.vue` 中 0 命中 | `InputBar.test.ts` L848 / L861;`InputBar.vitest.test.ts` L867 / L871 |
|
||||
| 按钮属性不变 | `title="群聊"` + `aria-label="群聊"` + `@click="handleGroupChat"` | `InputBar.vitest.test.ts` L625 |
|
||||
|
||||
### 7.2 功能验证(手工)
|
||||
|
||||
| # | 步骤 | 预期结果 |
|
||||
|---|------|---------|
|
||||
| M1 | 未发起会话 → 点群聊 | toast「请先发起会话」,无面板 |
|
||||
| M2 | 有会话(0 被邀请人)→ 点群聊 | 面板弹出(显示坐席 + 我),并 toast 邀请引导 |
|
||||
| M3 | M2 中点「+ 邀请参与者」→ 选人确认 | 复用既有 `InviteParticipantSheet`,提交成功 |
|
||||
| M4 | M3 成功后观察聊天页 | WS 推送到达,`ParticipantStrip` 自动出现(无需刷新) |
|
||||
| M5 | 反复点群聊 | 面板开合正常,收起时不重复弹提示,无状态卡死 |
|
||||
| M6 | 对照工具栏基线 | 5 按钮顺序 emoji / 文件 / 坐席 / 语音 / 群聊 不变,拱形轨道无形变 |
|
||||
| M7 | 被邀请人身份进入会话 → 点群聊 | 面板显示「退出会话」,二次确认可用 |
|
||||
|
||||
### 7.3 安全验证
|
||||
|
||||
| 验证项 | 验证方法 | 预期结果 |
|
||||
|--------|----------|---------|
|
||||
| 权限控制 | 非发起人 / 非参与者身份点击群聊 | 沿用 `ParticipantList` 既有 `canInvite` 判定,无越权邀请入口 |
|
||||
| 输入验证 | 本次无新增用户输入 | N/A(纯状态切换,无网络请求) |
|
||||
|
||||
### 7.4 性能验证
|
||||
|
||||
| 验证项 | 验证方法 | 预期结果 |
|
||||
|--------|----------|---------|
|
||||
| 响应时间 | 点击到面板弹出 | 纯本地状态切换,无网络往返,肉眼无延迟 |
|
||||
| 并发能力 | N/A | 本次不涉及后端 |
|
||||
|
||||
### 7.5 回归范围
|
||||
|
||||
- `pnpm test` 全量通过(重点 `InputBar` 相关 2 文件)
|
||||
- 手工回归工具栏其余 4 键(emoji / 文件 / 坐席 / 语音)
|
||||
- 坐席端零改动,不参与本次回归
|
||||
|
||||
---
|
||||
|
||||
## ✅ 八、完成标准
|
||||
|
||||
### 验收条件
|
||||
|
||||
- [x] `handleGroupChat` 三分支行为与技术方案 §3.3 契约一致
|
||||
- [x] 文案常量化,测试侧复刻同名常量
|
||||
- [x] `'群聊功能开发中'` / `'startGroupChat'` 在 `InputBar.vue` 中 0 命中
|
||||
- [x] 未在 InputBar 新增 `InviteParticipantSheet` 挂载
|
||||
- [x] 工具栏 5 按钮基线未被破坏(L345-360 模板未改)
|
||||
- [x] 后端 / store / 坐席端 零改动
|
||||
- [x] 文档已更新(技术方案 + 任务说明书 + PRD 回写)
|
||||
- [ ] 所有测试通过(CI/CD 绿灯)—— 待工程侧执行 `pnpm test` 确认
|
||||
- [ ] 代码合入主干分支 —— **本次不提交、不部署**,工作区改动待发布窗口
|
||||
|
||||
### 产出确认
|
||||
|
||||
- [x] 单元测试新增/修复完成
|
||||
- [x] 文档更新已完成(三件套关联双向一致)
|
||||
- [ ] Code Review
|
||||
- [ ] 部署验证(本次明确不部署)
|
||||
|
||||
---
|
||||
|
||||
## 📞 九、依赖与阻塞
|
||||
|
||||
### 前置依赖
|
||||
|
||||
| 依赖任务 | 依赖说明 | 状态 |
|
||||
|----------|----------|------|
|
||||
| REQ-用户-001 群聊双模式 | 缩略条 / 展开面板 / 邀请 / 退出能力,本次直接复用 | ✅ 已完成 |
|
||||
| REQ-会话-001 工具栏统一设计 v1.9 | 提供群聊按钮的 DOM 位(第 5 键) | ✅ 已完成 |
|
||||
| store `toggleParticipantPanel()` | 面板显隐 action,已存在并导出 | ✅ 已完成 |
|
||||
|
||||
### 阻塞因素
|
||||
|
||||
| 阻塞项 | 影响范围 | 解决方案 |
|
||||
|--------|----------|---------|
|
||||
| 无 | — | 本任务无阻塞项 |
|
||||
|
||||
### 遗留登记(不在本轮范围)
|
||||
|
||||
| 项 | 说明 | 处置 |
|
||||
|----|------|------|
|
||||
| 同需求双 PRD | `02-会话管理/群聊参与者展开缩略双模式-PRD.md` 与 `05-用户端H5/PRD-REQ-用户-001-群聊双模式-v1.0.md` 描述同一需求(两文件均存在,非死链) | 归入需求编号收敛,本轮按用户要求不处理 |
|
||||
| 坐席端按钮命名 | 坐席端入口叫「邀请」,需求语义是「群聊」,术语不对齐 | 将来若统一,采用「邀请」改名方案,保持 5 键基线 |
|
||||
|
||||
---
|
||||
|
||||
## 📈 十、变更记录
|
||||
|
||||
| 日期 | 变更内容 | 变更人 | 说明 |
|
||||
|------|----------|--------|------|
|
||||
| 2026-08-08 | 创建任务 | 高见远 | 初始版本;记录员工端 H5 群聊入口接线与三件套关联回写 |
|
||||
|
||||
---
|
||||
|
||||
## 📎 十一、附件
|
||||
|
||||
- PRD:`docs/01-产品文档/05-用户端H5/PRD-REQ-用户-001-群聊双模式-v1.0.md`
|
||||
- 本次技术方案:`docs/02-技术文档/技术方案-REQ-用户-001-群聊入口接线-v1.0.md`
|
||||
- 既有技术方案:`docs/02-技术文档/技术架构/技术方案-REQ-用户-001-群聊双模式-v1.0.md`
|
||||
- 既有架构设计:`docs/02-技术文档/01-架构设计/群聊参与者展开缩略双模式-架构设计.md`
|
||||
- 原型(用户端):`docs/01-产品文档/05-用户端H5/原型-REQ-用户-001-群聊双模式-v1.0.html`
|
||||
- 工具栏基线交付清单:`docs/01-产品文档/02-会话管理/交付-REQ-会话-001-工具栏统一设计v1.9-开发交付清单.md`
|
||||
+228
-37
@@ -180,21 +180,71 @@ 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>: Duckula(AI)· <strong>审核人</strong>: Simon(2026-08-06 11:36)</p>
|
||||
<p><strong>版本</strong>: v1.9.4-DRAFT | <strong>更新日期</strong>: 2026-08-11(早班巡检同步增补,未冻结)<br/>
|
||||
<strong>变更来源</strong>: 2026-08-11 09:00 早班巡检:jumpserver-V2 + 公网实测发现 <strong>公网版本停滞 3 天</strong>(/h5/go 仍 v20260808,08-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 待审)<br/>
|
||||
<strong>执行人</strong>: Duckula(AI)· <strong>审核人</strong>: 待 Simon 审核<br/>
|
||||
<strong>基础版本</strong>: v1.9.3-DRAFT(2026-08-10 早班巡检)</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.3(08-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=0,due 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 45683B(08-10 09:26 = v1.9.3)= 本地 45683B 完全匹配;服务器 <code>/opt/wecom-it-desk/docs-public/kanban/</code> 目录:当前 45683B(v1.9.3 派生)+ v1.9.0-FROZEN 30765B + v1.9.1-FROZEN 32078B 三份归档。</li>
|
||||
<li><strong>🟢 容器与资源全绿</strong>(jumpserver-V2 09:00 实测):</li>
|
||||
<li>5 容器 all healthy:nginx 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 二次治理作为新治理项。</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/>
|
||||
- <strong>人看副本</strong> = <code>项目状态看板.html</code>(由 <code>scripts/build_kanban_html.py</code> 自动派生,<strong>不手维护</strong>)<br/>
|
||||
- <strong>归档副本</strong> = <code>项目状态看板-v1.X.Y-FROZEN.html</code>(每次冻结生成一份,永久保留)<br/>
|
||||
- <strong>服务发布</strong> = 文件上传至 jumpserver <code>/opt/wecom-it-desk/docs-public/{kanban,inspection}/</code>,<strong>✅ v1.9.1 起已可对外访问</strong>:nginx.conf 加 <code>location /docs/</code> 路由 + docker-compose.yml 加 docs-public 挂载 + reload 已生效。外部 URL 实测:<br/>
|
||||
- 看板:https://itsupport.servyou.com.cn/docs/kanban/项目状态看板.html<br/>
|
||||
- 巡检:https://itsupport.servyou.com.cn/docs/inspection/2026-08/2026-08-06-早班.html<br/>
|
||||
- 修改流程:改 .md → 跑脚本生成 .html → 归档副本 → 跑部署脚本上传 → commit & 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→20(useScreenCapture/useWebSocket 新增 3 处)/ terminal 7→8(useWebSocket.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→2,completedTime 2026-08-10 01:25 UTC)</li>
|
||||
<li>看板-滴答清单双向同步铁律扩展:dida→看板反向同步同样纳入铁律</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/>
|
||||
@@ -218,18 +268,18 @@ footer {
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>🔴 P0 必做</td>
|
||||
<td>5</td>
|
||||
<td>🆕 -1(P0-NEW7 已修复)、P0-NEW8 新增(host 文件结构差异)</td>
|
||||
<td>7</td>
|
||||
<td>+1(P0-NEW11 /api/openapi.json 312 端点公开治理)</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 +288,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 +303,13 @@ footer {
|
||||
</tr>
|
||||
<tr>
|
||||
<td>✅ 已完成</td>
|
||||
<td>94</td>
|
||||
<td>+2(P1 Nginx 安全响应头关闭 + #104 运行期日志页结案)</td>
|
||||
<td>98</td>
|
||||
<td>持平</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>⚠️ 风险项</td>
|
||||
<td>2</td>
|
||||
<td>+1(公网版本停滞 3 天,/h5/go 仍 v20260808,08-08 09:30 last deploy)</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
@@ -277,20 +332,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 实测仍 500,nginx 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 +379,107 @@ 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)</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 外部</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 端点)。P0-NEW10 文字已覆盖但无独立 task,建议单列以便追踪进度。修复方案:FastAPI 实例化 <code>openapi_url=None</code>(生产)+ 同步 <code>docs_url=None, redoc_url=None</code>;nginx 层 <code>location = /api/openapi.json { return 404; }</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>:9 项 P0 已在滴答清单 <code>wecom_it_smart_desk</code> 项目下建任务(标签 <code>work</code>)。<strong>v1.9.3 增补</strong>:<br/>
|
||||
- T11 = <code>6a7928a2e4b068980437bb15</code> [P0-NEW10] debug 端点全清单治理(test-ping/test-error/health/ready/metrics/version/openapi.json),due 2026-08-13 17:00<br/>
|
||||
- <code>6a6bfc2be</code> [P1 sensitive_words 13 端点补 auth] 看板已完成 → dida 同步 close(completedTime 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→404(rewrite 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→20(useScreenCapture/useWebSocket 新增 3 处,源自 5311a52 合并)+ terminal 7→8(useWebSocket.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_active(H5 列表 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;本次同步 close(completedTime 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 +495,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 +622,13 @@ footer {
|
||||
</thead>
|
||||
<tbody>
|
||||
<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 +637,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 +944,16 @@ footer {
|
||||
<hr/>
|
||||
<h2 id="_4">📈 任务统计 <a class="back-to-toc" href="#toc" title="回到目录">📑</a></h2>
|
||||
<ul>
|
||||
<li><strong>总任务数</strong>: 101(v1.9 = 100 + P0-NEW8 host 文件结构差异)</li>
|
||||
<li><strong>已完成</strong>: 94(v1.9 = 91 + nginx /docs/ 路由上线 + P1 安全响应头关闭 + #104 运行期日志页结案)</li>
|
||||
<li><strong>🔴 P0 必做</strong>: 5(P0-1 / 3 / 4 / 5 / 6 / NEW8;NEW7 已修复)</li>
|
||||
<li><strong>🟡 P1 重要</strong>: 0</li>
|
||||
<li><strong>🟢 等用户决策</strong>: 2(BLK-A/B,<strong>均超 26 天阈值</strong>)</li>
|
||||
<li><strong>总任务数</strong>: 110(v1.9.4 = v1.9.3 109 + P0-NEW11 /api/openapi.json 独立 task)</li>
|
||||
<li><strong>已完成</strong>: 98(持平)</li>
|
||||
<li><strong>🔴 P0 必做</strong>: 7(P0-3 / 4 / 5 / 6 / NEW8 / NEW9 / NEW10 / <strong>NEW11</strong>;NEW7 已修复;P0-1 已闭环)</li>
|
||||
<li><strong>🟡 P1 重要</strong>: 6(P1-1.1 / 治理-1 / 治理-2 / 治理-3 / P1-Alembic / P1-Idx;P1-治理-sens 是 dida close 同步不入待办列)</li>
|
||||
<li><strong>🟢 等用户决策</strong>: 2(BLK-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>双向同步铁律</strong>:看板→dida 与 dida→看板 均纳入巡检必做项</li>
|
||||
</ul>
|
||||
<hr/>
|
||||
<h2 id="_5">🔗 相关文档 <a class="back-to-toc" href="#toc" title="回到目录">📑</a></h2>
|
||||
@@ -803,6 +976,24 @@ footer {
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><strong>v1.9.4-DRAFT</strong></td>
|
||||
<td>2026-08-11</td>
|
||||
<td>🆕 早班巡检触发的治理小版本(<strong>5 项关键发现</strong>):① <strong>🔴 公网版本停滞 3 天</strong>(NEW):jumpserver-V2 + 公网 <code>curl</code> 实测 <code>/h5/go</code> 仍 <code>302 → /h5/v20260808/</code>(08-08 09:30 last deploy,3 天无新发版);/itservice/go 同样 ② <strong>🔴 P0-NEW9/NEW10 仍 200 暴露</strong>:容器内 <code>curl /health</code> 200 + 公网 <code>/api/test-ping</code> 200 含 pong + <code>/api/test-error</code> 200 + <code>/api/openapi.json</code> 200 OK 424122B(312 端点全公开) ③ <strong>🔴 风险 /h5/ 今日到期</strong>:dida <code>6a752de4</code> due 2026-08-11 16:00,今晚不修即逾期 ④ <strong>🔴 P0-3 closing_service 时区错位 5 → 8 天</strong>:dida <code>6a72c892</code> 仍 status=0,due 08-06 已逾期 5 天 ⑤ <strong>🟡 BLK-A/B 30 → 31 天阈值校正</strong>(2026-07-11 → 2026-08-11)。<strong>新增</strong>:P0-NEW11(/api/openapi.json 独立 task,从 P0-NEW10 拆分)+ 风险-2(公网版本停滞)。看板:🔴 P0 6→7 / 🟡 P1 6(持平)/ 🟢 等决策 31 天阈值(30→31)/ ⚠️ 风险项 1→2 / 总任务 109→110。<strong>v1.9.3-DRAFT 1 天未冻结</strong>(08-10 09:00 → 08-11 09:00),本次合并入 v1.9.4 待审</td>
|
||||
<td>Duckula (AI)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>v1.9.3-DRAFT</strong></td>
|
||||
<td>2026-08-10</td>
|
||||
<td>🆕 早班巡检触发的治理小版本(<strong>4 项关键发现</strong>):① <strong>P0-1 /itportal/ 500 修复闭环</strong>:nginx.conf line 139-142 已 08-03 部署生效,今日公网实测 500→404(rewrite cycle 消失),P0-1 从"待部署"迁入"已完成"区(2026-08-03 完成日) ② <strong>dida365 双向校验发现 4 处反向脱节</strong>:dida <code>6a6bfc2be</code> sensitive_words 13 端点补 auth 看板已完成但 dida 仍 status=0(本次同步 close)+ dida <code>6a705109</code> Alembic 053-057 迁移脱节 / dida <code>6a70510f</code> troubleshooting_templates 索引 / dida <code>6a752de4</code> Nginx /h5/ alias+try_files 潜伏500 看板缺失(补登) ③ <strong>console 残留 128 → 129</strong>(h5 不变 / agent 17→20 新增 3 处 / terminal 7→8 新增 1 处) ④ <strong>BLK-A/B 26 → 30 天阈值校正</strong>(2026-07-11 → 2026-08-10)。新增 P0-NEW10(debug 端点全清单治理)+ P1 治理-2/3(看板-滴答双向同步铁律升级)。看板:🔴 P0 6(持平,P0-1 闭环 + P0-NEW10)/ 🟡 P1 2→5(Alembic/Idx/治理-2/治理-3 补登)/ ✅ 已完成 97→98 / ⚠️ 风险项 0→1</td>
|
||||
<td>Duckula (AI)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>v1.9.2-DRAFT</td>
|
||||
<td>2026-08-08</td>
|
||||
<td>🆕 早班巡检触发的治理小版本:<strong>dida365 自动同步 6 项</strong>(close 3 / create 3)——close <code>6a6bfc29</code> [P0→P3] /h5/ 404、close <code>6a72c88d</code> [P0-2] sensitive_words 13 端点、close <code>6a72c897</code> [P1-1] console 208 行(但发现仍残留 128 行,二次治理由 P1-1.1 承担);create <code>6a76a80d</code> [P0-NEW9] /api/test-ping 生产暴露、create <code>6a76a802</code> [P1-1.1] console 二次清理、create <code>6a76a809</code> [P1 治理-看板-滴答双源同步]。看板:🔴 P0 5→6 / 🟡 P1 0→2 / ✅ 已完成 94→97(基础计数 v1.9.1 100 + P0-NEW9 + P1-1.1 + P1 治理-1 = 104)。同步时间戳:2026-08-08 03:52 UTC</td>
|
||||
<td>Duckula (AI)</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>
|
||||
@@ -867,7 +1058,7 @@ footer {
|
||||
</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 09:26 (GMT+8) · 数据源:docs/07-项目管理/项目状态看板.md<br>
|
||||
修改流程:编辑 .md → 跑脚本生成 .html → (可选)部署到 jumpserver → commit & push
|
||||
</footer>
|
||||
</div>
|
||||
|
||||
+95
-22
@@ -1,8 +1,54 @@
|
||||
# 项目状态看板
|
||||
|
||||
> **版本**: v1.9.1-FROZEN | **更新日期**: 2026-08-06(已审核冻结,对外可引用)
|
||||
> **变更来源**: 2026-08-06 11:46 看板-部署脱节修复(jumpserver-V2 实测 + docker compose 重启 + nginx reload)
|
||||
> **执行人**: Duckula(AI)· **审核人**: Simon(2026-08-06 11:36)
|
||||
> **版本**: v1.9.4-DRAFT | **更新日期**: 2026-08-11(早班巡检同步增补,未冻结)
|
||||
> **变更来源**: 2026-08-11 09:00 早班巡检:jumpserver-V2 + 公网实测发现 **公网版本停滞 3 天**(/h5/go 仍 v20260808,08-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 待审)
|
||||
> **执行人**: Duckula(AI)· **审核人**: 待 Simon 审核
|
||||
> **基础版本**: v1.9.3-DRAFT(2026-08-10 早班巡检)
|
||||
|
||||
## 📌 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.3(08-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=0,due 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 45683B(08-10 09:26 = v1.9.3)= 本地 45683B 完全匹配;服务器 `/opt/wecom-it-desk/docs-public/kanban/` 目录:当前 45683B(v1.9.3 派生)+ v1.9.0-FROZEN 30765B + v1.9.1-FROZEN 32078B 三份归档。
|
||||
- **🟢 容器与资源全绿**(jumpserver-V2 09:00 实测):
|
||||
- 5 容器 all healthy:nginx 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 二次治理作为新治理项。
|
||||
|
||||
> 📐 **文档管理策略(2026-08-05 锁定 / 2026-08-06 修订 / v1.9.1 生效)**
|
||||
> 本看板遵循 **Markdown 单一源原则**:
|
||||
> - **权威源** = 本 `项目状态看板.md`(Git 版本控制 / 程序解析 / PR review 均基于此)
|
||||
> - **人看副本** = `项目状态看板.html`(由 `scripts/build_kanban_html.py` 自动派生,**不手维护**)
|
||||
> - **归档副本** = `项目状态看板-v1.X.Y-FROZEN.html`(每次冻结生成一份,永久保留)
|
||||
> - **服务发布** = 文件上传至 jumpserver `/opt/wecom-it-desk/docs-public/{kanban,inspection}/`,**✅ v1.9.1 起已可对外访问**:nginx.conf 加 `location /docs/` 路由 + docker-compose.yml 加 docs-public 挂载 + reload 已生效。外部 URL 实测:
|
||||
> - 看板:https://itsupport.servyou.com.cn/docs/kanban/项目状态看板.html
|
||||
> - 巡检:https://itsupport.servyou.com.cn/docs/inspection/2026-08/2026-08-06-早班.html
|
||||
> - 修改流程:改 .md → 跑脚本生成 .html → 归档副本 → 跑部署脚本上传 → commit & push
|
||||
|
||||
---
|
||||
|
||||
## 📌 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→20(useScreenCapture/useWebSocket 新增 3 处)/ terminal 7→8(useWebSocket.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→2,completedTime 2026-08-10 01:25 UTC)
|
||||
- 看板-滴答清单双向同步铁律扩展:dida→看板反向同步同样纳入铁律
|
||||
|
||||
> 📐 **文档管理策略(2026-08-05 锁定 / 2026-08-06 修订 / v1.9.1 生效)**
|
||||
> 本看板遵循 **Markdown 单一源原则**:
|
||||
@@ -20,14 +66,15 @@
|
||||
|
||||
| 状态 | 数量 | 变化 |
|
||||
|------|------|------|
|
||||
| 🔴 P0 必做 | 5 | 🆕 -1(P0-NEW7 已修复)、P0-NEW8 新增(host 文件结构差异) |
|
||||
| 🟡 P1 重要 | 0 | 持平 |
|
||||
| 🟢 等用户决策 | 2 | +1 自然日(25→26 天阈值) |
|
||||
| 🔴 P0 必做 | 7 | +1(P0-NEW11 /api/openapi.json 312 端点公开治理) |
|
||||
| 🟡 P1 重要 | 6 | 持平 |
|
||||
| 🟢 等用户决策 | 2 | 持平(**31 天阈值**,从 30 天修正) |
|
||||
| 🟠 进行中 | 1 | 持平 |
|
||||
| 🏷️ 看板治理 | 1 | ✅ P0-NEW7 已修复(nginx /docs/ 路由上线) |
|
||||
| 🏷️ 看板治理 | 3 | 持平 |
|
||||
| 🟡 待开始 | 0 | 持平 |
|
||||
| ⏸️ 暂停 | 5 | 持平 |
|
||||
| ✅ 已完成 | 94 | +2(P1 Nginx 安全响应头关闭 + #104 运行期日志页结案) |
|
||||
| ✅ 已完成 | 98 | 持平 |
|
||||
| ⚠️ 风险项 | 2 | +1(公网版本停滞 3 天,/h5/go 仍 v20260808,08-08 09:30 last deploy) |
|
||||
|
||||
---
|
||||
|
||||
@@ -38,34 +85,50 @@
|
||||
|
||||
| 任务ID | 任务名称 | 类别 | 阻塞时长 | 状态 | 说明 |
|
||||
|--------|----------|------|----------|------|------|
|
||||
| **P0-1** | `/itportal/` 入口 500 错误 | ♻️ 历史遗留 | **29 天**(自 2026-07-08) | 🔴 待部署 | 2026-08-06 06:00 jumpserver-V2 实测仍 500,nginx 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-4(SessionLocal 绑定失败)。建议:① 临时 `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**(30765B,Cache-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) |
|
||||
| **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 外部 |
|
||||
| **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 端点)。P0-NEW10 文字已覆盖但无独立 task,建议单列以便追踪进度。修复方案:FastAPI 实例化 `openapi_url=None`(生产)+ 同步 `docs_url=None, redoc_url=None`;nginx 层 `location = /api/openapi.json { return 404; }` 兜底 |
|
||||
|
||||
**滴答清单跟踪**:6 项 P0 已在滴答清单 `wecom_it_smart_desk` 项目下建任务(标签 `work`)。新增项 T5 (P0-5 打包错误) + T6 (P0-4 实际位置修正) + T7 (P0-NEW7 看板-部署脱节),due 2026-08-06 17:00。
|
||||
**滴答清单跟踪**:9 项 P0 已在滴答清单 `wecom_it_smart_desk` 项目下建任务(标签 `work`)。**v1.9.3 增补**:
|
||||
- T11 = `6a7928a2e4b068980437bb15` [P0-NEW10] debug 端点全清单治理(test-ping/test-error/health/ready/metrics/version/openapi.json),due 2026-08-13 17:00
|
||||
- `6a6bfc2be` [P1 sensitive_words 13 端点补 auth] 看板已完成 → dida 同步 close(completedTime 2026-08-10 01:25 UTC)
|
||||
|
||||
**P0-1 闭环**:看板 P0-1 `/itportal/` 500 修复闭环 — nginx.conf line 139-142 已显式注释"/itportal/ 静态前端块已移除 (2026-08-03 fix)",今日公网实测 500→404(rewrite 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→20(useScreenCapture/useWebSocket 新增 3 处,源自 5311a52 合并)+ terminal 7→8(useWebSocket.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_active(H5 列表 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;本次同步 close(completedTime 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 +171,12 @@
|
||||
|
||||
| 任务ID | 任务名称 | 优先级 | 负责人 | 完成日期 |
|
||||
|--------|----------|--------|--------|----------|
|
||||
| **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` 自动 close,completedTime 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→2,completedTime 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` 自动 close,completedTime 2026-08-08 03:52 UTC。看板 08-04 已确认线上 13 端点全部带 `Depends(require_admin)` 部署生效 |
|
||||
| - | [P1-1] 清理前端 console.log 残留 208 行 | 🟡 P1 | Duckula | 2026-08-08 | dida `6a72c897e4b08d42854aa8f6` 自动 close,completedTime 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_nginx;origin(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 +233,16 @@
|
||||
|
||||
## 📈 任务统计
|
||||
|
||||
- **总任务数**: 101(v1.9 = 100 + P0-NEW8 host 文件结构差异)
|
||||
- **已完成**: 94(v1.9 = 91 + nginx /docs/ 路由上线 + P1 安全响应头关闭 + #104 运行期日志页结案)
|
||||
- **🔴 P0 必做**: 5(P0-1 / 3 / 4 / 5 / 6 / NEW8;NEW7 已修复)
|
||||
- **🟡 P1 重要**: 0
|
||||
- **🟢 等用户决策**: 2(BLK-A/B,**均超 26 天阈值**)
|
||||
- **总任务数**: 110(v1.9.4 = v1.9.3 109 + P0-NEW11 /api/openapi.json 独立 task)
|
||||
- **已完成**: 98(持平)
|
||||
- **🔴 P0 必做**: 7(P0-3 / 4 / 5 / 6 / NEW8 / NEW9 / NEW10 / **NEW11**;NEW7 已修复;P0-1 已闭环)
|
||||
- **🟡 P1 重要**: 6(P1-1.1 / 治理-1 / 治理-2 / 治理-3 / P1-Alembic / P1-Idx;P1-治理-sens 是 dida close 同步不入待办列)
|
||||
- **🟢 等用户决策**: 2(BLK-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 项。**双向同步铁律**:看板→dida 与 dida→看板 均纳入巡检必做项
|
||||
|
||||
---
|
||||
|
||||
@@ -189,6 +259,9 @@
|
||||
|
||||
| 版本 | 日期 | 变更内容 | 变更人 |
|
||||
|------|------|----------|--------|
|
||||
| **v1.9.4-DRAFT** | 2026-08-11 | 🆕 早班巡检触发的治理小版本(**5 项关键发现**):① **🔴 公网版本停滞 3 天**(NEW):jumpserver-V2 + 公网 `curl` 实测 `/h5/go` 仍 `302 → /h5/v20260808/`(08-08 09:30 last deploy,3 天无新发版);/itservice/go 同样 ② **🔴 P0-NEW9/NEW10 仍 200 暴露**:容器内 `curl /health` 200 + 公网 `/api/test-ping` 200 含 pong + `/api/test-error` 200 + `/api/openapi.json` 200 OK 424122B(312 端点全公开) ③ **🔴 风险 /h5/ 今日到期**:dida `6a752de4` due 2026-08-11 16:00,今晚不修即逾期 ④ **🔴 P0-3 closing_service 时区错位 5 → 8 天**:dida `6a72c892` 仍 status=0,due 08-06 已逾期 5 天 ⑤ **🟡 BLK-A/B 30 → 31 天阈值校正**(2026-07-11 → 2026-08-11)。**新增**:P0-NEW11(/api/openapi.json 独立 task,从 P0-NEW10 拆分)+ 风险-2(公网版本停滞)。看板:🔴 P0 6→7 / 🟡 P1 6(持平)/ 🟢 等决策 31 天阈值(30→31)/ ⚠️ 风险项 1→2 / 总任务 109→110。**v1.9.3-DRAFT 1 天未冻结**(08-10 09:00 → 08-11 09:00),本次合并入 v1.9.4 待审 | Duckula (AI) |
|
||||
| **v1.9.3-DRAFT** | 2026-08-10 | 🆕 早班巡检触发的治理小版本(**4 项关键发现**):① **P0-1 /itportal/ 500 修复闭环**:nginx.conf line 139-142 已 08-03 部署生效,今日公网实测 500→404(rewrite cycle 消失),P0-1 从"待部署"迁入"已完成"区(2026-08-03 完成日) ② **dida365 双向校验发现 4 处反向脱节**:dida `6a6bfc2be` sensitive_words 13 端点补 auth 看板已完成但 dida 仍 status=0(本次同步 close)+ dida `6a705109` Alembic 053-057 迁移脱节 / dida `6a70510f` troubleshooting_templates 索引 / dida `6a752de4` Nginx /h5/ alias+try_files 潜伏500 看板缺失(补登) ③ **console 残留 128 → 129**(h5 不变 / agent 17→20 新增 3 处 / terminal 7→8 新增 1 处) ④ **BLK-A/B 26 → 30 天阈值校正**(2026-07-11 → 2026-08-10)。新增 P0-NEW10(debug 端点全清单治理)+ P1 治理-2/3(看板-滴答双向同步铁律升级)。看板:🔴 P0 6(持平,P0-1 闭环 + P0-NEW10)/ 🟡 P1 2→5(Alembic/Idx/治理-2/治理-3 补登)/ ✅ 已完成 97→98 / ⚠️ 风险项 0→1 | Duckula (AI) |
|
||||
| v1.9.2-DRAFT | 2026-08-08 | 🆕 早班巡检触发的治理小版本:**dida365 自动同步 6 项**(close 3 / create 3)——close `6a6bfc29` [P0→P3] /h5/ 404、close `6a72c88d` [P0-2] sensitive_words 13 端点、close `6a72c897` [P1-1] console 208 行(但发现仍残留 128 行,二次治理由 P1-1.1 承担);create `6a76a80d` [P0-NEW9] /api/test-ping 生产暴露、create `6a76a802` [P1-1.1] console 二次清理、create `6a76a809` [P1 治理-看板-滴答双源同步]。看板:🔴 P0 5→6 / 🟡 P1 0→2 / ✅ 已完成 94→97(基础计数 v1.9.1 100 + P0-NEW9 + P1-1.1 + P1 治理-1 = 104)。同步时间戳:2026-08-08 03:52 UTC | Duckula (AI) |
|
||||
| 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_nginx,origin 与公网(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-NEW8(host 文件结构 vs git 仓库结构差异)。归档副本 `项目状态看板-v1.9.1-FROZEN.html` | Duckula (AI) |
|
||||
|
||||
@@ -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>
|
||||
|
||||
<!-- ⋯ 次要动作收纳按钮(仅 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
|
||||
class="tic-action-btn tic-action-primary"
|
||||
:href="wecomApprovalUrl"
|
||||
v-if="item.href && !item.disabled"
|
||||
:href="item.href"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
@click="handleApprovalAction('open')"
|
||||
@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)
|
||||
|
||||
/**
|
||||
* 切换 ⋯ 菜单开关
|
||||
* - 关闭用点击外部/Esc:document 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 中打开」enabled(fallback)
|
||||
*
|
||||
* 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 → 主「接单」disabled(U-1.1 阻塞)
|
||||
* - B-2: ticket processing → 主「结单」disabled(U-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 + primary(fallback)', () => {
|
||||
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'
|
||||
/** 跳转 URL(href 模式);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
|
||||
/** 跳转 URL(href 模式) */
|
||||
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 → 「📥 接单」+ 跳转 ITSM(disabled 占位)
|
||||
* - processing→ 「✅ 结单」+ 跳转 ITSM(disabled 占位)
|
||||
* - 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 }
|
||||
}
|
||||
Reference in New Issue
Block a user