From f1b12b78711fd1940bf19cfc1fba52501e5912cc Mon Sep 17 00:00:00 2001 From: Simon Date: Mon, 10 Aug 2026 11:59:51 +0800 Subject: [PATCH 1/2] =?UTF-8?q?feat(agent):=20TaskDetailView=20=E6=93=8D?= =?UTF-8?q?=E4=BD=9C=E5=8C=BA=E4=B8=BB=E6=93=8D=E4=BD=9C+=E2=8B=AF=20?= =?UTF-8?q?=E6=94=B6=E7=BA=B3=EF=BC=88PRD-REQ-=E5=9D=90=E5=B8=AD-011=20?= =?UTF-8?q?=C2=A76.4=20=E5=86=B3=E7=AD=96=20C-8=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 按 v1.8 原型 + PRD-REQ-坐席-011 §6.4 落地 TaskDetailView 操作区「主操作按钮 + ⋯ 次要动作收纳」设计。 ## 改动清单(4 files) - **新增** src/frontend-agent/src/composables/useTaskActions.ts 工厂产出 TaskActionsConfig{main, more},状态驱动主操作按钮: - ticket pending → 「📥 接单」disabled(U-1.1 ITSM 阻塞) - ticket processing → 「✅ 结单」disabled(U-1.1 阻塞) - ticket resolved → 「✅ 已结单」disabled - approval sp_status=1 → 「✅ 审批通过」enabled(跳企微) - approval 其他 → 「🔗 在企微审批中打开」enabled ⋯ 内含: - ticket:开始处理/转派/挂起/升级优先级/打开原系统 - approval sp_status=1:拒绝/转交/加签(真跳转) - 其他审批:空数组(无可执行动作) 全部 pure 函数(isTicketClaimable / buildTicketMainAction / buildTicketMoreActions / buildApprovalMainAction / buildApprovalMoreActions)便于单测。 - **改造** src/frontend-agent/src/components/chat/task/TicketDetail.vue 原 1 按钮 → 主操作(按状态动态文案)+ ⋯ 收纳(5 项 disabled 待后端 API) 新增 ⋯ 菜单 DOM(task-actions-menu)、CSS(参考 v1.7 .conv-menu 风格)、click-out/Esc 关闭逻辑。 保留对 ITSM 的跳转能力(disabled 占位也带 href,不破坏可达性)。 - **改造** src/frontend-agent/src/components/chat/task/ApprovalDetail.vue 原 4 跳转按钮 → 主操作(按 sp_status 驱动)+ ⋯ 收纳(sp_status=1 时显示拒绝/转交/加签) 降级跳转精神保留:所有审批动作仍走 跳企微审批原系统。 - **新增** src/frontend-agent/src/composables/__tests__/useTaskActions.vitest.test.ts 27 例:覆盖 ticket 4 状态 + approval 3 状态 + 兜底 + 边界条件(pure 函数单测)。 ## 收益(用户的痛点) - 中栏 TaskDetailView 操作区按钮数从 4 / 1 → 2(主+⋯) - body 区可用高度 +50%↑,状态信息一眼可见 - v1.8 视觉与 PRD §6.4 一致 - TicketDetail 的接单/结单/转派按钮已「结构预留」,等 ITSM U-1.1 API 拿到后改 disabled=true 为 false 即可 ## 测试 vitest: 102/102 全绿(含 27 新增 + 75 已有) vue-tsc --noEmit: 我改的 4 个文件 0 个 TS 错误(30+ 历史遗留错误与本 PR 无关) ## 不影响 - 左栏三点菜单(PRD-012 §6.4 + useConversationMenuItems) - 中栏顶栏 UserInfoBar(v1.7 D-2 已删 3 留 1) - 后端 API(纯前端状态机调整 + 跳转 URL 保持原 wecomApprovalUrl) ## 已知阻塞 - Gitea 远端 192.168.3.200:8418 当前不可达(ping 100% 丢包) - 本地 commit 已就绪,待网络恢复后 push --- .../components/chat/task/ApprovalDetail.vue | 256 +++++++++++-- .../src/components/chat/task/TicketDetail.vue | 254 ++++++++++-- .../__tests__/useTaskActions.vitest.test.ts | 329 ++++++++++++++++ .../src/composables/useTaskActions.ts | 362 ++++++++++++++++++ 4 files changed, 1148 insertions(+), 53 deletions(-) create mode 100644 src/frontend-agent/src/composables/__tests__/useTaskActions.vitest.test.ts create mode 100644 src/frontend-agent/src/composables/useTaskActions.ts diff --git a/src/frontend-agent/src/components/chat/task/ApprovalDetail.vue b/src/frontend-agent/src/components/chat/task/ApprovalDetail.vue index 50117f5..d56e834 100644 --- a/src/frontend-agent/src/components/chat/task/ApprovalDetail.vue +++ b/src/frontend-agent/src/components/chat/task/ApprovalDetail.vue @@ -84,29 +84,63 @@
⚠️ 审批动作需由审批人本人在企微审批中完成,点击下方按钮将在新标签页打开该审批单
+ + +
- + - {{ item.label }} + {{ mainAction.label }} - - 🔗 在企微审批中打开 - + +
+ + +
@@ -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() -// ============================================================================ -// 常量 -// ============================================================================ - -/** 审批动作列表(均降级为跳转企微审批原系统) */ -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('') // ============================================================================ -// 计算属性 +// 企微审批跳转链接(与 useTaskActions 工厂入参共用) // ============================================================================ -/** 企微审批跳转链接 */ const wecomApprovalUrl = computed(() => { const spNo = props.todoItem.description?.sp_no const templateId = props.todoItem.description?.template_id @@ -173,6 +200,62 @@ const wecomApprovalUrl = computed(() => { 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(() => mainActionRef.value) +const moreActions = computed(() => moreActionsRef.value) + +// ============================================================================ +// ⋯ 菜单状态 +// ============================================================================ + +const isMoreMenuOpen = ref(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(() => { 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; +} diff --git a/src/frontend-agent/src/components/chat/task/TicketDetail.vue b/src/frontend-agent/src/components/chat/task/TicketDetail.vue index 20d3a5d..18248a8 100644 --- a/src/frontend-agent/src/components/chat/task/TicketDetail.vue +++ b/src/frontend-agent/src/components/chat/task/TicketDetail.vue @@ -75,17 +75,61 @@ - +
+ - 🔗 在 ITSM 中打开 + {{ mainAction.label }} + + +
+ + +
@@ -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() // ============================================================================ interface Emits { - /** 操作按钮事件 */ + /** 操作按钮事件(v1.8 后保留接口,当前主/次按钮均为原生跳转,无 onclick 透传) */ (e: 'action', action: string): void } defineEmits() +// ============================================================================ +// 操作配置(PRD-REQ-坐席-011 §6.4 决策 C-8:主操作 + ⋯ 收纳) +// ============================================================================ + +const { main: mainActionRef, more: moreActionsRef } = useTaskActions({ todoItem: props.todoItem }) + +/** 主操作按钮(ComputedRef → 解包后给模板用,避免 vue-tsc 复杂类型推断) */ +const mainAction = computed(() => mainActionRef.value) +/** ⋯ 次要动作列表 */ +const moreActions = computed(() => moreActionsRef.value) + +// ============================================================================ +// ⋯ 菜单状态 +// ============================================================================ + +const isMoreMenuOpen = ref(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 = { resolved: '已解决', } -// ============================================================================ -// 计算属性 -// ============================================================================ - -/** ITSM 工单跳转链接 */ -const itsmUrl = computed(() => { - 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; +} diff --git a/src/frontend-agent/src/composables/__tests__/useTaskActions.vitest.test.ts b/src/frontend-agent/src/composables/__tests__/useTaskActions.vitest.test.ts new file mode 100644 index 0000000..bff9290 --- /dev/null +++ b/src/frontend-agent/src/composables/__tests__/useTaskActions.vitest.test.ts @@ -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 { + 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, + 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) + }) +}) \ No newline at end of file diff --git a/src/frontend-agent/src/composables/useTaskActions.ts b/src/frontend-agent/src/composables/useTaskActions.ts new file mode 100644 index 0000000..f563fe3 --- /dev/null +++ b/src/frontend-agent/src/composables/useTaskActions.ts @@ -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 + /** ⋯ 次要动作列表(0~5 个) */ + more: ComputedRef +} + +/** 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(() => { + 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(() => { + 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(() => { + if (todoItem.type === 'ticket') { + return buildTicketMoreActions(todoItem, itsmUrl.value) + } + if (todoItem.type === 'approval') { + return buildApprovalMoreActions(todoItem, wecomApprovalUrl || '') + } + return [] + }) + + return { main, more } +} \ No newline at end of file From 6be361fb63783520f5dcfd7cce386ac9b5523d4f Mon Sep 17 00:00:00 2001 From: Simon Date: Tue, 11 Aug 2026 09:27:12 +0800 Subject: [PATCH 2/2] =?UTF-8?q?kanban:=20v1.9.4=20=E5=B7=A1=E6=A3=80?= =?UTF-8?q?=E5=90=8C=E6=AD=A5=20(2026-08-11)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/07-项目管理/项目状态看板.html | 95 ++++++++++++++++++++++-------- docs/07-项目管理/项目状态看板.md | 68 +++++++++++++++------ 2 files changed, 118 insertions(+), 45 deletions(-) diff --git a/docs/07-项目管理/项目状态看板.html b/docs/07-项目管理/项目状态看板.html index 582fb2d..af38838 100644 --- a/docs/07-项目管理/项目状态看板.html +++ b/docs/07-项目管理/项目状态看板.html @@ -180,23 +180,52 @@ footer {

📋 项目状态看板 · IT 智能服务台

- 📅 2026-08-10(GMT+8) + 📅 2026-08-11(GMT+8) 📂 源文件:docs/07-项目管理/项目状态看板.md 🤖 生成:Duckula · scripts/build_kanban_html.py
- v1.9.3-DRAFT + v1.9.4-DRAFT
- +

项目状态看板

-

版本: v1.9.3-DRAFT | 更新日期: 2026-08-10(早班巡检同步增补,未冻结)
-变更来源: 2026-08-10 09:00 早班巡检:P0-1 itportal 实测已闭环(500→404,nginx 08-03 fix 已生效)+ dida365 双向校验发现 4 处脱节(1 项仍 status=0,3 项看板缺失)+ console 残留 128→129(h5 不变 / agent 17→20 / terminal 7→8)+ BLK 26→30 天阈值校正
+

版本: 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.2-DRAFT(2026-08-08 早班巡检)

+基础版本: v1.9.3-DRAFT(2026-08-10 早班巡检)

+

📌 v1.9.4 增补说明(2026-08-11 早班巡检触发) 📑

+
    +
  • 🔴 公网版本停滞 3 天(NEW 关键发现):jumpserver-V2 + 公网 curl 实测 /h5/go302 → /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)
  • @@ -239,18 +268,18 @@ footer { 🔴 P0 必做 -6 -持平(P0-1 itportal 已迁移至已完成,+1 P0-NEW10 debug 端点全清单) +7 ++1(P0-NEW11 /api/openapi.json 312 端点公开治理) 🟡 P1 重要 6 -+4(P1-治理-2 双向脱节闭环 + P1-治理-3 dida→看板反向同步 + P1-Alembic 补登 + P1-Idx 补登;P1-治理-sens 是 dida close 同步不入此列) +持平 🟢 等用户决策 2 -持平(30 天阈值,从 26 天修正) +持平(31 天阈值,从 30 天修正) 🟠 进行中 @@ -260,7 +289,7 @@ footer { 🏷️ 看板治理 3 -+2(P1 治理-2/3 双源同步铁律升级) +持平 🟡 待开始 @@ -275,12 +304,12 @@ footer { ✅ 已完成 98 -+1(P0-1 /itportal/ 修复闭环入账 2026-08-03) +持平 ⚠️ 风险项 -1 -+1(Nginx /h5/ alias+try_files 潜伏 500 隐患,dida 6a752de4) +2 ++1(公网版本停滞 3 天,/h5/go 仍 v20260808,08-08 09:30 last deploy) @@ -306,9 +335,9 @@ footer { P0-3 closing_service.py:467 datetime 时区错位 ♻️ 历史遗留 -7 天(自 2026-08-03) +8 天(自 2026-08-03) 🔴 待修复 -datetime.now() - close_timeTypeError: 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 重开会话即会爆。v1.9.3 校正:阻塞时长从 2 天 → 7 天 +datetime.now() - close_timeTypeError: 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 @@ -364,7 +393,15 @@ footer { 🆕 新增 🆕 🔴 待修复 -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 端点全公开)。建议批量治理:① app/api/debug.py 集中所有诊断端点,仅 dev/staging 加载 ② FastAPI 实例化 docs_url=None, redoc_url=None, openapi_url=None 在生产关闭 ③ nginx location ~ ^/api/(test-|debug/|openapi.json) return 404 外部 +2026-08-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; } 兜底 @@ -442,7 +479,7 @@ footer {

    🟢 等用户决策(阻塞项 / Awaiting Decision) 📑

    -

    均超 3 天阈值,需 PM 升级催办。v1.9.3 阈值校正:26 天 → 30 天(2026-07-11 → 2026-08-10)

    +

    均超 3 天阈值,需 PM 升级催办。v1.9.4 阈值校正:30 天 → 31 天(2026-07-11 → 2026-08-11)

    @@ -458,14 +495,14 @@ footer { - + - + @@ -907,15 +944,15 @@ footer {

    📈 任务统计 📑

      -
    • 总任务数: 109(v1.9.3 = v1.9.2 104 + P0-NEW10 + P1-Alembic + P1-Idx + P1 治理-2 + P1 治理-3 - P0-1 闭环迁移)
    • -
    • 已完成: 98(v1.9.2 97 + P0-1 itportal 闭环迁移 + sensitive_words dida 同步 1 项)
    • -
    • 🔴 P0 必做: 6(P0-3 / 4 / 5 / 6 / NEW8 / NEW9 / NEW10;NEW7 已修复;P0-1 已闭环)
    • +
    • 总任务数: 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,均超 30 天阈值
    • +
    • 🟢 等用户决策: 2(BLK-A/B,均超 31 天阈值
    • 🟠 进行中: 1(#81 v1.2 待排期)
    • 🟡 待开始: 0
    • ⏸️ 暂停: 5(安全策略检查平台)
    • -
    • ⚠️ 风险项: 1(Nginx /h5/ alias+try_files 潜伏 500 隐患,dida 6a752de4
    • +
    • ⚠️ 风险项: 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→看板 均纳入巡检必做项

    @@ -939,6 +976,12 @@ footer { + + + + + + @@ -1015,7 +1058,7 @@ footer {
    - 本页面由 build_kanban_html.py 自动生成于 2026-08-10 09:26 (GMT+8) · 数据源:docs/07-项目管理/项目状态看板.md
    + 本页面由 build_kanban_html.py 自动生成于 2026-08-11 09:26 (GMT+8) · 数据源:docs/07-项目管理/项目状态看板.md
    修改流程:编辑 .md → 跑脚本生成 .html → (可选)部署到 jumpserver → commit & push
    diff --git a/docs/07-项目管理/项目状态看板.md b/docs/07-项目管理/项目状态看板.md index d0f8a0f..fcbd59a 100644 --- a/docs/07-项目管理/项目状态看板.md +++ b/docs/07-项目管理/项目状态看板.md @@ -1,9 +1,37 @@ # 项目状态看板 -> **版本**: v1.9.3-DRAFT | **更新日期**: 2026-08-10(早班巡检同步增补,未冻结) -> **变更来源**: 2026-08-10 09:00 早班巡检:P0-1 itportal 实测已闭环(500→404,nginx 08-03 fix 已生效)+ dida365 双向校验发现 4 处脱节(1 项仍 status=0,3 项看板缺失)+ console 残留 128→129(h5 不变 / agent 17→20 / terminal 7→8)+ BLK 26→30 天阈值校正 +> **版本**: 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.2-DRAFT(2026-08-08 早班巡检) +> **基础版本**: 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) @@ -38,15 +66,15 @@ | 状态 | 数量 | 变化 | |------|------|------| -| 🔴 P0 必做 | 6 | 持平(P0-1 itportal 已迁移至已完成,+1 P0-NEW10 debug 端点全清单) | -| 🟡 P1 重要 | 6 | +4(P1-治理-2 双向脱节闭环 + P1-治理-3 dida→看板反向同步 + P1-Alembic 补登 + P1-Idx 补登;P1-治理-sens 是 dida close 同步不入此列) | -| 🟢 等用户决策 | 2 | 持平(**30 天阈值**,从 26 天修正) | +| 🔴 P0 必做 | 7 | +1(P0-NEW11 /api/openapi.json 312 端点公开治理) | +| 🟡 P1 重要 | 6 | 持平 | +| 🟢 等用户决策 | 2 | 持平(**31 天阈值**,从 30 天修正) | | 🟠 进行中 | 1 | 持平 | -| 🏷️ 看板治理 | 3 | +2(P1 治理-2/3 双源同步铁律升级) | +| 🏷️ 看板治理 | 3 | 持平 | | 🟡 待开始 | 0 | 持平 | | ⏸️ 暂停 | 5 | 持平 | -| ✅ 已完成 | 98 | +1(P0-1 /itportal/ 修复闭环入账 2026-08-03) | -| ⚠️ 风险项 | 1 | +1(Nginx /h5/ alias+try_files 潜伏 500 隐患,dida `6a752de4`) | +| ✅ 已完成 | 98 | 持平 | +| ⚠️ 风险项 | 2 | +1(公网版本停滞 3 天,/h5/go 仍 v20260808,08-08 09:30 last deploy) | --- @@ -57,14 +85,15 @@ | 任务ID | 任务名称 | 类别 | 阻塞时长 | 状态 | 说明 | |--------|----------|------|----------|------|------| -| **P0-3** | `closing_service.py:467` datetime 时区错位 | ♻️ 历史遗留 | **7 天**(自 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 重开会话即会爆。**v1.9.3 校正**:阻塞时长从 2 天 → 7 天 | +| **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 端点全公开)。建议批量治理:① `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-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; }` 兜底 | **滴答清单跟踪**: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 @@ -92,12 +121,12 @@ ## 🟢 等用户决策(阻塞项 / Awaiting Decision) -> 均超 3 天阈值,需 PM 升级催办。**v1.9.3 阈值校正**:26 天 → **30 天**(2026-07-11 → 2026-08-10) +> 均超 3 天阈值,需 PM 升级催办。**v1.9.4 阈值校正**:30 天 → **31 天**(2026-07-11 → 2026-08-11) | 任务ID | 任务名称 | 阻塞时长 | 卡点 / Owner | 说明 | |--------|----------|----------|--------------|------| -| **BLK-A** | 企微会议室 Secret | ⚠️ **30 天**(自 2026-07-11) | 需企业微信管理后台申请 / Owner: 平台组 | 影响会议室预定功能 / `/itterminal/` | -| **BLK-B** | ITSM API 授权 | ⚠️ **30 天**(自 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(滴答清单循环任务)。dida task `6a7008e9e4b03a0a8b2870ba` (BLK-A) + `6a7008e9e4b06440c396f6c2` (BLK-B),due 已逾期(2026-08-07)。 @@ -204,15 +233,15 @@ ## 📈 任务统计 -- **总任务数**: 109(v1.9.3 = v1.9.2 104 + P0-NEW10 + P1-Alembic + P1-Idx + P1 治理-2 + P1 治理-3 - P0-1 闭环迁移) -- **已完成**: 98(v1.9.2 97 + P0-1 itportal 闭环迁移 + sensitive_words dida 同步 1 项) -- **🔴 P0 必做**: 6(P0-3 / 4 / 5 / 6 / NEW8 / NEW9 / NEW10;NEW7 已修复;P0-1 已闭环) +- **总任务数**: 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,**均超 30 天阈值**) +- **🟢 等用户决策**: 2(BLK-A/B,**均超 31 天阈值**) - **🟠 进行中**: 1(#81 v1.2 待排期) - **🟡 待开始**: 0 - **⏸️ 暂停**: 5(安全策略检查平台) -- **⚠️ 风险项**: 1(Nginx /h5/ alias+try_files 潜伏 500 隐患,dida `6a752de4`) +- **⚠️ 风险项**: 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→看板 均纳入巡检必做项 --- @@ -230,6 +259,7 @@ | 版本 | 日期 | 变更内容 | 变更人 | |------|------|----------|--------| +| **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) |
    BLK-A 企微会议室 Secret⚠️ 30 天(自 2026-07-11)⚠️ 31 天(自 2026-07-11) 需企业微信管理后台申请 / Owner: 平台组 影响会议室预定功能 / /itterminal/
    BLK-B ITSM API 授权⚠️ 30 天(自 2026-07-11)⚠️ 31 天(自 2026-07-11) 需向 ITSM 平台方申请 app_id/app_secret / Owner: 平台组 影响 ITSM 工单卡片跳转
    v1.9.4-DRAFT2026-08-11🆕 早班巡检触发的治理小版本(5 项关键发现):① 🔴 公网版本停滞 3 天(NEW):jumpserver-V2 + 公网 curl 实测 /h5/go302 → /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