@zzusp/dingtalk-dsh-observer 0.5.14 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/web-client.js +557 -62
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zzusp/dingtalk-dsh-observer",
3
- "version": "0.5.14",
3
+ "version": "1.0.0",
4
4
  "description": "DingTalk task board client for DeepSeek Harness",
5
5
  "license": "UNLICENSED",
6
6
  "type": "module",
package/web-client.js CHANGED
@@ -4,7 +4,7 @@ window.__ModuleLoader__.load({
4
4
  const module = { exports: {} }
5
5
  const React = require('react')
6
6
  const { Button, IconChecklistOutline14, IconChevronDownOutline14, IconChevronUpOutline14, Menu, Pill, StateDot } = require('@deepseek-ai/dsh-client-ui-primitives')
7
- const { useCallback, useEffect, useState } = React
7
+ const { useCallback, useEffect, useState, useRef } = React
8
8
  const ENDPOINT = 'http://127.0.0.1:18998'
9
9
  const colors = {
10
10
  surface: 'var(--dsw-alias-bg-base, #fff)',
@@ -54,7 +54,10 @@ window.__ModuleLoader__.load({
54
54
  const short = (value) => value ? String(value).replace(/^session-/, '').slice(0, 14) : '—'
55
55
  const outboxDelivery = (message) => {
56
56
  if (message.recallStatus === 'recalled') return { id: 'recalled', label: '已撤回', state: 'neutral' }
57
+ if (message.recallStatus === 'failed') return { id: 'recall-failed', label: '撤回待处理', state: 'warning', detail: message.recallError === 'replacement_delivery_unknown' ? '已停止旧意图发送;历史是否送达仍未知,等待核验' : `旧消息撤回失败${message.recallRetryAt ? ',将有限重试' : ',需核验后处理'}:${message.recallError || '原因未记录'}` }
58
+ if (message.supersededByOutboundId || message.status === 'superseded') return { id: 'superseded', label: '已替代', state: 'neutral', detail: message.deliveredMessageId ? '已记录送达,等待替换通知送达后撤回' : '已停止旧意图发送;未声称历史消息未送达' }
57
59
  if (message.status === 'sent') return { id: 'confirmed', label: message.deliveredMessageId ? '已回读' : '已发送', state: 'done' }
60
+ if (message.deliveryBlockedAt) return { id: 'failed', label: '发送待处理', state: 'error', detail: '服务端已拒绝发送,停止自动重试;需核验失败原因' }
58
61
  const reason = message.deliveryPendingReason
59
62
  const details = {
60
63
  preflight_history_partial: '发送前检查:历史消息不完整,本次未发送',
@@ -71,17 +74,46 @@ window.__ModuleLoader__.load({
71
74
  if (message.deliveryError || reason === 'send_failed') return { id: 'failed', label: '投递异常', state: 'error', detail }
72
75
  return { id: 'queued', label: '待发送', state: 'ongoing', detail }
73
76
  }
74
- const pill = (tone) => ({ display: 'inline-flex', alignItems: 'center', borderRadius: 999, padding: '3px 8px', fontSize: 12, color: tone, background: `color-mix(in srgb, ${tone} 12%, transparent)` })
75
- const card = { border: `1px solid ${colors.border}`, borderRadius: 12, background: colors.cardSurface, padding: 16, boxShadow: '0 4px 16px rgba(15,23,42,.07)' }
77
+ const ui = { textSm: 12, textMd: 14, textLg: 16, textXl: 20, space1: 4, space2: 8, space3: 12, space4: 16, space6: 24, radiusSm: 8, radiusMd: 12 }
78
+ const observerCss = `
79
+ [data-dingtalk-observer] :is(button, input, select, textarea, summary, [tabindex]):focus-visible {
80
+ outline: 2px solid var(--dsw-alias-brand-primary, #4d6bfe) !important;
81
+ outline-offset: 2px !important;
82
+ }
83
+ [data-dingtalk-observer] .observer-task-step { position: relative; display: grid; grid-template-columns: 28px minmax(0, 1fr); gap: 12px; padding-bottom: 24px; }
84
+ [data-dingtalk-observer] .observer-task-step:not(:last-child)::before { content: ''; position: absolute; left: 13px; top: 28px; bottom: 0; border-left: 1px solid var(--dsw-alias-border-primary, rgba(127,127,127,.25)); }
85
+ [data-dingtalk-observer] .observer-task-step-heading { display: flex; align-items: baseline; justify-content: space-between; gap: 8px 16px; flex-wrap: wrap; min-height: 28px; }
86
+ [data-dingtalk-observer] .observer-task-output-row { display: grid; grid-template-columns: 112px minmax(0, 1fr); gap: 12px; }
87
+ [data-dingtalk-observer] .observer-task-overview { padding: 20px 0; }
88
+ [data-dingtalk-observer] .observer-task-result { padding: 24px; }
89
+ @media (max-width: 960px) {
90
+ [data-dingtalk-observer] .observer-task-output-row { grid-template-columns: minmax(0, 1fr); gap: 2px; }
91
+ [data-dingtalk-observer] .observer-task-overview { padding: 20px 0; }
92
+ [data-dingtalk-observer] .observer-task-result { padding: 18px; }
93
+ [data-dingtalk-observer] .observer-main { padding: 16px !important; }
94
+ [data-dingtalk-observer] .observer-topics-layout { display: grid !important; grid-template-columns: minmax(280px, 2fr) minmax(0, 3fr); }
95
+ [data-dingtalk-observer] .observer-topic-list { max-width: none !important; }
96
+ [data-dingtalk-observer] .observer-task-board { grid-template-columns: repeat(2, minmax(280px, 1fr)) !important; }
97
+ }
98
+ @media (max-width: 720px) {
99
+ [data-dingtalk-observer] .observer-main { padding: 12px !important; }
100
+ [data-dingtalk-observer] .observer-topics-layout,
101
+ [data-dingtalk-observer] .observer-task-board { grid-template-columns: minmax(0, 1fr) !important; }
102
+ [data-dingtalk-observer] .observer-topic-detail { min-height: 320px !important; }
103
+ [data-dingtalk-observer] .observer-toolbar { align-items: stretch !important; }
104
+ }
105
+ `
106
+ const pill = (tone) => ({ display: 'inline-flex', alignItems: 'center', borderRadius: 999, padding: `${ui.space1}px ${ui.space2}px`, fontSize: ui.textSm, color: tone, background: `color-mix(in srgb, ${tone} 12%, transparent)` })
107
+ const card = { border: `1px solid ${colors.border}`, borderRadius: ui.radiusMd, background: colors.cardSurface, padding: ui.space4 }
76
108
  const tableFrame = { border: `1px solid ${colors.border}`, borderRadius: 12, background: colors.cardSurface, overflow: 'hidden' }
77
- const tableHeadCell = { height: 42, boxSizing: 'border-box', padding: '0 16px', color: colors.muted, fontSize: 13, fontWeight: 600, verticalAlign: 'middle' }
78
- const tableBodyCell = { padding: '12px 16px', borderTop: `1px solid ${colors.border}`, fontSize: 14, lineHeight: 1.55, verticalAlign: 'middle' }
109
+ const tableHeadCell = { height: 42, boxSizing: 'border-box', padding: `0 ${ui.space4}px`, color: colors.muted, fontSize: ui.textSm, fontWeight: 600, verticalAlign: 'middle' }
110
+ const tableBodyCell = { padding: `${ui.space3}px ${ui.space4}px`, borderTop: `1px solid ${colors.border}`, fontSize: ui.textMd, lineHeight: 1.55, verticalAlign: 'middle' }
79
111
  const tableBodyContent = { display: '-webkit-box', WebkitLineClamp: 2, WebkitBoxOrient: 'vertical', overflow: 'hidden', overflowWrap: 'anywhere' }
80
112
  const clampTableContent = (...children) => React.createElement('div', { style: tableBodyContent }, ...children)
81
113
  const singleLineTableContent = (...children) => React.createElement('div', { style: { overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' } }, ...children)
82
114
  const tableFooter = { minHeight: 48, boxSizing: 'border-box', display: 'flex', justifyContent: 'flex-end', alignItems: 'center', gap: 8, padding: '8px 12px', borderTop: `1px solid ${colors.border}`, background: colors.surface2 }
83
115
  const toolbar = { minHeight: 48, boxSizing: 'border-box', display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 12, padding: '8px 12px', borderBottom: `1px solid ${colors.border}`, background: colors.cardSurface }
84
- const navigationTabStyle = (selected, { paddingBottom = 11 } = {}) => ({ position: 'relative', boxSizing: 'border-box', border: 0, borderBottom: selected ? '2px solid #4d6bfe' : '2px solid transparent', borderRadius: 0, padding: `0 0 ${paddingBottom}px`, cursor: 'pointer', font: 'inherit', fontSize: 13, fontWeight: 500, lineHeight: '16px', color: selected ? '#4d6bfe' : colors.muted, background: 'transparent', outline: 'none', whiteSpace: 'nowrap' })
116
+ const navigationTabStyle = (selected, { paddingBottom = 12 } = {}) => ({ position: 'relative', boxSizing: 'border-box', border: 0, borderBottom: selected ? `2px solid ${colors.accent}` : '2px solid transparent', borderRadius: 0, padding: `0 0 ${paddingBottom}px`, cursor: 'pointer', font: 'inherit', fontSize: ui.textMd, fontWeight: 500, lineHeight: '20px', color: selected ? colors.accent : colors.muted, background: 'transparent', whiteSpace: 'nowrap' })
85
117
  const statusTone = {
86
118
  done: 'var(--dsw-alias-state-success-primary, #248a3d)',
87
119
  warning: 'var(--dsw-alias-state-warn-primary, #a56500)',
@@ -100,7 +132,11 @@ window.__ModuleLoader__.load({
100
132
  const anchor = React.createElement(Button, { variant: 'outline', size: 'sm', type: 'button', 'aria-label': label, 'aria-expanded': open, onClick: () => setOpen((current) => !current), style: { minWidth: fitContent ? 0 : minWidth, width: fitContent ? 'fit-content' : undefined, maxWidth: fitContent ? 220 : undefined, justifyContent: fitContent ? 'flex-start' : 'space-between', gap: fitContent ? 8 : 16, fontWeight: 400 } }, React.createElement('span', { style: { minWidth: 0, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' } }, selected?.label || value), React.createElement('span', { 'aria-hidden': true, style: { flex: '0 0 auto', color: colors.muted, fontSize: 9, transform: open ? 'rotate(180deg)' : 'none', transition: 'transform .15s ease' } }, '▼'))
101
133
  return React.createElement(Menu, { open, anchor, items: options, selectedId: value, onSelect: (id) => { onChange(id); setOpen(false) }, onClose: () => setOpen(false), align: 'end', portal: true, dense: true, compact: true })
102
134
  }
103
- const emptyState = (text) => React.createElement('div', { style: { minHeight: 112, display: 'grid', placeItems: 'center', color: colors.muted, fontSize: 12, border: `1px dashed ${colors.border}`, borderRadius: 10, background: colors.surface2 } }, text)
135
+ const emptyState = (title, { detail, actionLabel, onAction } = {}) => React.createElement('div', { style: { minHeight: 112, display: 'grid', placeItems: 'center', padding: ui.space4, textAlign: 'center', color: colors.muted, fontSize: ui.textSm, border: `1px dashed ${colors.border}`, borderRadius: ui.radiusSm, background: colors.surface2 } },
136
+ React.createElement('div', { style: { display: 'grid', justifyItems: 'center', gap: ui.space2 } },
137
+ React.createElement('strong', { style: { color: 'inherit', fontSize: ui.textMd, fontWeight: 600 } }, title),
138
+ detail ? React.createElement('span', null, detail) : null,
139
+ actionLabel && onAction ? React.createElement(Button, { variant: 'outline', size: 'sm', type: 'button', onClick: onAction }, actionLabel) : null))
104
140
  async function get(path) {
105
141
  const response = await fetch(`${ENDPOINT}${path}`, { method: 'GET', headers: { accept: 'application/json' } })
106
142
  const value = await response.json()
@@ -135,16 +171,220 @@ window.__ModuleLoader__.load({
135
171
  { state: 'queued', label: '待执行', tone: 'var(--dsw-alias-label-secondary, #64748b)', background: 'color-mix(in srgb, var(--dsw-alias-label-secondary, #64748b) 8%, var(--dsw-alias-bg-layer-1, #fff))' },
136
172
  { state: 'running', label: '执行中', tone: colors.accent, background: 'color-mix(in srgb, var(--dsw-alias-brand-primary, #4d6bfe) 8%, var(--dsw-alias-bg-layer-1, #fff))' },
137
173
  { state: 'waiting', label: '等待中', tone: colors.warning, background: 'color-mix(in srgb, var(--dsw-alias-state-warn-primary, #a56500) 8%, var(--dsw-alias-bg-layer-1, #fff))' },
138
- { state: 'completed', label: '已完成', tone: 'var(--dsw-alias-state-success-primary, #248a3d)', background: 'color-mix(in srgb, var(--dsw-alias-state-success-primary, #248a3d) 8%, var(--dsw-alias-bg-layer-1, #fff))' }
174
+ { state: 'completed', label: '已结束', tone: 'var(--dsw-alias-state-success-primary, #248a3d)', background: 'color-mix(in srgb, var(--dsw-alias-state-success-primary, #248a3d) 8%, var(--dsw-alias-bg-layer-1, #fff))' }
139
175
  ]
140
- const pages = [{ id: 'groups', label: '群聊会话' }, { id: 'topics', label: '话题' }, { id: 'tasks', label: '任务看板' }, { id: 'authorizations', label: '人工介入' }, { id: 'archive', label: '归档任务' }, { id: 'alerts', label: '告警' }]
141
- function TopicBrowser({ groups, tasks, target, updatedAt, onOpenTask }) {
176
+ const pages = [{ id: 'groups', label: '群消息' }, { id: 'topics', label: '话题' }, { id: 'tasks', label: '任务看板' }, { id: 'authorizations', label: '人工介入' }, { id: 'archive', label: '归档任务' }, { id: 'alerts', label: '告警' }]
177
+ const traceStatus = (status) => ({ succeeded: '已完成', applied: '已接纳', settled: '处理已结束', processed: '处理已结束', running: '处理中', claimed: '处理中', pending: '等待处理', queued: '等待处理', waiting: '等待继续', waiting_user: '待补充', interrupted: '待恢复', cancelling: '正在停止', ready: '等待执行', needs_attention: '需要处理', failed: '失败', blocked: '已阻塞', superseded: '已被新消息替代', cancelled: '已取消', rejected: '未接纳', accepted: '已接纳' }[status] || '状态未记录')
178
+ function TaskTotalElapsed({ task }) {
179
+ const [now, setNow] = useState(Date.now)
180
+ const ended = task.state === 'completed'
181
+ useEffect(() => {
182
+ if (ended || !task.createdAt) return
183
+ setNow(Date.now())
184
+ const timer = setInterval(() => setNow(Date.now()), 1000)
185
+ return () => clearInterval(timer)
186
+ }, [task.taskId, task.createdAt, ended])
187
+ const elapsed = traceElapsed({ startedAt: task.createdAt, completedAt: task.updatedAt,
188
+ status: ended ? 'succeeded' : 'running' }, now)
189
+ const timing = task.executionTiming
190
+ const executionMs = timing?.elapsedMs + (timing?.running && !ended ? Math.max(0, now - Date.parse(timing.sampledAt)) : 0)
191
+ const execution = Number.isFinite(executionMs) ? traceElapsed({ startedAt: new Date(0).toISOString(),
192
+ completedAt: new Date(executionMs).toISOString(), status: 'succeeded' }, now).replace(/^耗时 /, '') : '未记录'
193
+ return React.createElement('div', { style: { marginLeft: 'auto', display: 'flex', flexWrap: 'wrap', justifyContent: 'flex-end', gap: '4px 16px', color: colors.muted, fontVariantNumeric: 'tabular-nums' } },
194
+ React.createElement('span', { title: '累计业务步骤实际执行时间,包含重试;排除排队及等待,并行区间只计一次,不含消息判断和任务负责会话' },
195
+ `累计执行时长 ${execution}${timing?.complete === false ? '(记录不完整)' : ''}`),
196
+ React.createElement('span', {
197
+ title: ended ? '从任务创建至终态记录更新时间,包含排队、等待与重试' : '从任务创建至现在,包含排队、等待与重试',
198
+ style: { marginLeft: 'auto', textAlign: 'right', color: colors.muted, fontVariantNumeric: 'tabular-nums' },
199
+ }, elapsed === '耗时未记录' ? '累计总耗时未记录' : `累计总耗时 ${elapsed.replace(/^(?:已用时|耗时) /, '')}`))
200
+ }
201
+ const nodeTitle = { 'prepare-merge-main': '准备合并主分支', 'execute-merge-main': '合并主分支', 'prepare-verify-main-merge': '准备核验主分支合并', 'execute-verify-main-merge': '核验主分支合并', 'inspect-merged': '回读合并结果', 'prepare-approval-gate': '准备审批', 'execute-approval-gate': '等待真人审批', 'inspect-approved': '回读审批结果', 'prepare-tag': '准备发布标签', 'execute-tag': '创建发布标签', 'inspect-tagged': '回读标签结果', 'prepare-build': '准备构建', 'execute-build': '执行构建', 'inspect-built': '核对构建产物', 'inspect-runtime': '核对运行版本', investigate: '调查与分析', 'accept-result': '校验调查结果', execute: '执行任务', 'prepare-rebuild': '准备重新构建', 'execute-rebuild': '执行重新构建', 'prepare-workspace': '创建独立工作目录', 'read-files': '读取相关文件', 'propose-changes': '编写修改方案', 'inspect-and-propose': '编写修改方案', 'validate-proposal': '检查修改方案', 'apply-changes': '按方案修改文件', 'verify-candidate': '构建检查', 'business-acceptance': '业务验收', 'define-local-acceptance': '核对本地验收条件', 'plan-local-acceptance': '编写本地验收用例', 'prepare-local-acceptance': '准备本地验收环境', 'run-local-acceptance': '启动服务并执行本地验收', 'finalize-local-acceptance': '核对验收与清理结果', 'index-files': '索引可修改文件', 'select-files': '选择改动文件', 'validate-selection': '检查文件范围', 'prepare-commit': '检查提交条件', commit: '提交代码', 'prepare-push': '检查推送条件', push: '推送代码', 'prepare-pr': '编写合并请求', 'create-pr': '创建合并请求', 'prepare-generation': '确认项目与修改起点', prepare: '校验输入', assess: '审查材料', analyze: '分析材料', 'validate-result': '校验结果', 'inspect-preflight': '核对合并条件', 'prepare-merge': '准备合并', 'execute-merge': '执行合并', 'verify-source': '回读合并结果', 'freeze-target': '冻结目标', 'freeze-input': '冻结输入', 'propose-sql': '编写 SQL 候选', 'validate-package': '校验变更包', 'prepare-rehearsal': '核对 UAT 演练条件', 'run-rehearsal': '在 UAT 数据库演练', 'readback-rehearsal': '回读 UAT 演练', 'prepare-issue': '准备工单', 'create-issue': '提交工单', 'readback-issue': '回读工单', 'approval-gate': '等待真人审批', 'prepare-execute': '准备执行', 'execute-task': '执行受控任务', 'readback-production': '生产只读回查', finalize: '核对交付结果' }
202
+ const taskDisplayTitle = task => {
203
+ const text = String(task.title || task.objective || '未命名任务').replace(/\s+/gu, ' ').trim() || '未命名任务'
204
+ const first = text.split(/[。!?\n]/u)[0]
205
+ if (Array.from(first).length >= 8 && Array.from(first).length <= 32) return first
206
+ const characters = Array.from(text)
207
+ return characters.length <= 32 ? text : `${characters.slice(0, 31).join('')}…`
208
+ }
209
+ const taskStepStart = node => Number.isFinite(Date.parse(node.startedAt)) ? `开始 ${fmt(node.startedAt)}`
210
+ : !node.startedAt && ['pending', 'ready', 'blocked'].includes(node.status) ? '未开始' : '开始时间未记录'
211
+ const groupTaskSteps = nodes => {
212
+ const groups = new Map()
213
+ nodes.forEach((node, index) => {
214
+ const key = node.stageId || node.runId || 'current'
215
+ if (!groups.has(key)) groups.set(key, { key, title: node.stageTitle || '当前工作流', steps: [] })
216
+ groups.get(key).steps.push({ node, index })
217
+ })
218
+ return [...groups.values()]
219
+ }
220
+ function TaskStepElapsed({ node, fontSize = 12 }) {
221
+ const [now, setNow] = useState(Date.now)
222
+ useEffect(() => {
223
+ if (node.status !== 'running' || !node.startedAt) return
224
+ setNow(Date.now())
225
+ const timer = setInterval(() => setNow(Date.now()), 1000)
226
+ return () => clearInterval(timer)
227
+ }, [node.status, node.startedAt])
228
+ const notStarted = !node.startedAt && ['pending', 'ready', 'blocked'].includes(node.status)
229
+ const label = notStarted ? '尚未开始' : traceElapsed({ ...node, attempt: node.leaseEpoch }, now)
230
+ return React.createElement('span', { style: { color: colors.muted, fontSize, fontVariantNumeric: 'tabular-nums' } },
231
+ node.status === 'waiting' && node.completedAt ? `本次执行${label}` : label)
232
+ }
233
+ const traceElapsed = (item, now) => {
234
+ const start = Date.parse(item.startedAt), end = Date.parse(item.completedAt)
235
+ const running = ['running', 'claimed'].includes(item.status)
236
+ const duration = (running ? now : end) - start
237
+ if (!Number.isFinite(duration) || duration < 0) return '耗时未记录'
238
+ const seconds = duration / 1000
239
+ const value = seconds < 1 ? `${Math.round(duration)} 毫秒` : seconds < 60 ? `${seconds.toFixed(1)} 秒`
240
+ : seconds < 3600 ? `${Math.floor(seconds / 60)} 分 ${Math.floor(seconds % 60)} 秒`
241
+ : `${Math.floor(seconds / 3600)} 小时 ${Math.floor(seconds % 3600 / 60)} 分 ${Math.floor(seconds % 60)} 秒`
242
+ return `${running ? '已用时' : '耗时'} ${value}${item.attempt > 1 ? `(第 ${item.attempt} 次处理)` : ''}`
243
+ }
244
+ const traceReason = (reason) => {
245
+ if (!reason) return null
246
+ const text = typeof reason === 'string' ? reason : JSON.stringify(reason)
247
+ if (/MESSAGE_(?:CONTEXT|MATERIAL|REFERENCED_CANDIDATES)_CAPACITY|context_capacity_blocked/.test(text)) return '上下文容量受阻:必要材料未能完整提供,后续判断已停止。'
248
+ return ({ LOCAL_ACCEPTANCE_CONFIG_REQUIRED: '尚未配置本地验收环境与固定验收命令,请补齐后继续', LOCAL_ACCEPTANCE_CRITERIA_REQUIRED: '缺少明确的业务验收条件,请补充预期结果', LOCAL_ACCEPTANCE_PLAN_INVALID: '验收方案未覆盖任务要求或包含无效用例,请修订方案', LOCAL_ACCEPTANCE_PENDING_RECONCILIATION: '上次本地验收结果待核对,暂停重试以避免重复写入共享 UAT 数据', LOCAL_ACCEPTANCE_FAILED: '本地业务验收未通过,请查看实际结果并修复', LOCAL_ACCEPTANCE_CLEANUP_UNCONFIRMED: '测试数据清理或本地服务停止尚未确认,后续提交已停止', LOCAL_ACCEPTANCE_RECEIPT_INVALID: '验收回执与当前代码或方案不一致,后续提交已停止', ENGINEERING_UAT_ENVIRONMENT_REQUIRED: '请明确指定 uat1~uat9 中的一个环境,不能默认选择', ENGINEERING_UAT_BRANCH_REQUIRED: '开发 PR 只能提交到明确指定的 UAT 分支,main 合并需独立上线任务', ENGINEERING_UAT_BRANCH_NOT_FOUND: '指定 UAT 环境对应的分支不存在,请核对', ENGINEERING_ACCEPTANCE_REQUIRED: '缺少业务验收用例与预期结果,后续提交已停止', ENGINEERING_ACCEPTANCE_FAILED: '业务验收未通过或未取得实际结果,后续提交已停止', ENGINEERING_VERIFICATION_FAILED: '构建检查未通过,后续步骤已停止' })[text] ?? text
249
+ }
250
+ const readableValue = (value) => value === undefined || value === null ? '历史未记录' : typeof value === 'string' ? value : JSON.stringify(value, null, 2)
251
+ const readout = (label, value) => React.createElement('details', { style: { borderTop: `1px solid ${colors.border}`, padding: '8px 0', overflowWrap: 'anywhere' } }, React.createElement('summary', { style: { cursor: 'pointer', fontWeight: 600 } }, label), React.createElement('pre', { style: { margin: '8px 0 0', padding: 10, maxHeight: 300, overflow: 'auto', whiteSpace: 'pre-wrap', wordBreak: 'break-word', background: colors.surface2, borderRadius: ui.radiusSm, font: 'inherit', fontSize: 12 } }, readableValue(value)))
252
+ function WorkflowClarification({ request, onSubmitted }) {
253
+ const [answer, setAnswer] = useState('')
254
+ const [busy, setBusy] = useState(false)
255
+ const [submitted, setSubmitted] = useState(false)
256
+ const [error, setError] = useState('')
257
+ const field = useRef(null), pending = useRef(false), attempt = useRef(null)
258
+ const fieldId = `clarification-${request.requestId}`, errorId = `${fieldId}-error`
259
+ const submit = async event => {
260
+ event.preventDefault()
261
+ if (pending.current || submitted) return
262
+ const value = answer.trim()
263
+ if (!value) { setError('请填写需要补充的信息。'); field.current?.focus(); return }
264
+ if (!attempt.current || attempt.current.answer !== value) attempt.current = { answer: value, eventId: `web-answer:${crypto.randomUUID()}` }
265
+ pending.current = true; setBusy(true); setError('')
266
+ try {
267
+ await post(`/workflows/${encodeURIComponent(request.runId)}/requests/${encodeURIComponent(request.requestId)}/answer`, attempt.current)
268
+ setSubmitted(true)
269
+ onSubmitted?.()
270
+ } catch { setError('补充信息提交未确认,内容已保留。请重试或刷新核对处理状态。') }
271
+ finally { pending.current = false; setBusy(false) }
272
+ }
273
+ return React.createElement('section', { 'aria-label': '补充调查信息', style: { margin: '12px 0 16px', padding: 16, background: colors.surface2, borderLeft: `3px solid ${colors.warning}`, borderRadius: ui.radiusSm } },
274
+ React.createElement('strong', { style: { display: 'block', marginBottom: 8 } }, '需要你补充'),
275
+ React.createElement('p', { style: { margin: '0 0 12px', whiteSpace: 'pre-wrap', overflowWrap: 'anywhere' } }, request.question),
276
+ submitted ? React.createElement('p', { role: 'status', style: { margin: 0 } }, '补充已提交,后续执行情况请查看处理记录。')
277
+ : request.canAnswer !== true ? React.createElement('p', { style: { margin: 0, color: colors.muted } }, '请由任务发起人或负责人在原群回复补充信息。')
278
+ : React.createElement('form', { noValidate: true, onSubmit: submit },
279
+ React.createElement('label', { htmlFor: fieldId, style: { display: 'block', marginBottom: 6, fontSize: 13 } }, '补充信息'),
280
+ React.createElement('textarea', { id: fieldId, ref: field, value: answer, rows: 4, maxLength: 16000, disabled: busy,
281
+ 'aria-invalid': !!error, 'aria-describedby': error ? errorId : undefined,
282
+ onChange: event => { setAnswer(event.target.value); setError('') },
283
+ style: { width: '100%', boxSizing: 'border-box', minHeight: 96, resize: 'none', padding: 10, color: 'inherit', background: colors.surface, border: `1px solid ${colors.border}`, borderRadius: ui.radiusSm, font: 'inherit', lineHeight: 1.6 } }),
284
+ error ? React.createElement('p', { id: errorId, role: 'alert', style: { color: colors.danger, margin: '8px 0' } }, error) : null,
285
+ React.createElement(Button, { variant: 'outline', size: 'sm', type: 'submit', disabled: busy, 'aria-busy': busy, style: { marginTop: 8, minWidth: 120 } }, busy ? '正在提交…' : '提交补充并继续')))
286
+ }
287
+ function TraceReadout({ runId, resourceRef, label }) {
288
+ const [expanded, setExpanded] = useState(false)
289
+ const [page, setPage] = useState()
290
+ const [cursor, setCursor] = useState(0)
291
+ const [hash, setHash] = useState('')
292
+ const [text, setText] = useState('')
293
+ const [loading, setLoading] = useState(false)
294
+ const [error, setError] = useState('')
295
+ const [retry, setRetry] = useState(0)
296
+ useEffect(() => {
297
+ if (!expanded) return
298
+ let active = true
299
+ setLoading(true); setError('')
300
+ get(`/state/workflows/${encodeURIComponent(runId)}/evidence/${encodeURIComponent(resourceRef)}?cursor=${cursor}${cursor && hash ? `&hash=${encodeURIComponent(hash)}` : ''}`).then(value => {
301
+ if (active) { setPage(value); setHash(value.hash); setText(previous => cursor ? previous + value.text : value.text) }
302
+ }, () => { if (active) setError('产出暂时无法读取,请重试。') }).finally(() => { if (active) setLoading(false) })
303
+ return () => { active = false }
304
+ }, [runId, resourceRef, expanded, cursor, retry])
305
+ return React.createElement('section', { style: { minWidth: 0 } },
306
+ React.createElement(Button, { variant: 'outline', size: 'sm', type: 'button', 'aria-expanded': expanded, onClick: () => { setExpanded(value => !value); setCursor(0); setHash(''); setText(''); setPage(undefined) } }, expanded ? '收起产出' : label),
307
+ expanded ? React.createElement('div', { style: { marginTop: 10, padding: 12, background: colors.surface2, borderRadius: ui.radiusSm, lineHeight: 1.7 } },
308
+ text ? React.createElement('div', { style: { whiteSpace: 'pre-wrap', overflowWrap: 'anywhere' } }, text) : null,
309
+ loading ? React.createElement('p', { role: 'status', style: { minHeight: 24 } }, '正在读取产出…') : null,
310
+ error ? React.createElement('div', { role: 'alert' }, error, React.createElement(Button, { variant: 'outline', size: 'sm', type: 'button', onClick: () => setRetry(value => value + 1) }, '重试')) : null,
311
+ !loading && !error && page && !text ? React.createElement('p', null, '未记录正文。') : null,
312
+ page?.nextCursor !== null && page?.nextCursor !== undefined ? React.createElement(Button, { variant: 'outline', size: 'sm', type: 'button', disabled: loading, onClick: () => setCursor(page.nextCursor) }, '继续阅读产出') : null,
313
+ page?.evidenceRefs?.length ? React.createElement('div', { style: { display: 'grid', gap: 8, marginTop: 12 } }, ...page.evidenceRefs.map((ref, index) => React.createElement(TraceReadout, { key: ref, runId, resourceRef: ref, label: `查看依据 ${index + 1}` }))) : null) : null)
314
+ }
315
+ function TraceDetail({ runId: initialRunId, onBack, onOpenSession, backLabel = '返回收信箱' }) {
316
+ const [runId, setRunId] = useState(initialRunId)
317
+ const [page, setPage] = useState()
318
+ const [cursor, setCursor] = useState('')
319
+ const [cursorHistory, setCursorHistory] = useState([])
320
+ const [retry, setRetry] = useState(0)
321
+ const [error, setError] = useState('')
322
+ const [loading, setLoading] = useState(true)
323
+ const [clock, setClock] = useState(Date.now)
324
+ const hasRunningStep = page?.items?.some(item => ['running', 'claimed'].includes(item.status))
325
+ useEffect(() => {
326
+ if (!hasRunningStep) return
327
+ const timer = setInterval(() => setClock(Date.now()), 1000)
328
+ return () => clearInterval(timer)
329
+ }, [hasRunningStep])
330
+ useEffect(() => {
331
+ let active = true
332
+ setLoading(true); setError('')
333
+ get(`/state/workflows/${encodeURIComponent(runId)}/trace${cursor ? `?cursor=${encodeURIComponent(cursor)}` : ''}`).then((value) => { if (active) setPage(value) }, (cause) => { if (active) setError(cause.message) }).finally(() => { if (active) setLoading(false) })
334
+ return () => { active = false }
335
+ }, [runId, cursor, retry])
336
+ const labels = { split: '拆分事项', route: '关联话题', intent: '判断下一步', command: '接纳动作', agent: '查询与答复' }
337
+ const ready = !loading && !error && page
338
+ return React.createElement('section', { 'aria-label': '消息处理过程', style: { ...card, display: 'grid', gap: 12 } },
339
+ React.createElement('div', { style: { display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 12 } }, React.createElement('strong', { style: { fontSize: 16 } }, '消息处理过程'), React.createElement(Button, { variant: 'outline', size: 'sm', type: 'button', onClick: onBack }, backLabel)),
340
+ ready ? React.createElement('div', { style: { background: colors.surface2, padding: 16, borderRadius: ui.radiusSm, display: 'grid', gap: 10 } },
341
+ React.createElement('div', { style: { display: 'flex', justifyContent: 'space-between', alignItems: 'center', gap: 12, flexWrap: 'wrap' } }, React.createElement('strong', { style: { fontSize: 16 } }, traceStatus(page.status)), React.createElement(Button, { variant: 'outline', size: 'sm', type: 'button', onClick: () => setRetry(value => value + 1) }, '刷新处理记录')),
342
+ React.createElement('blockquote', { style: { margin: 0, paddingLeft: 12, borderLeft: `3px solid ${colors.accent}`, fontSize: 14, lineHeight: 1.7, whiteSpace: 'pre-wrap', overflowWrap: 'anywhere' } }, page.message?.text ?? '原消息未记录'),
343
+ page.reason ? React.createElement('p', { role: 'status', style: { color: colors.warning, margin: 0, whiteSpace: 'pre-wrap', overflowWrap: 'anywhere' } }, traceReason(page.reason)) : null,
344
+ React.createElement('span', { style: { color: colors.muted, fontSize: 12 } }, '这里展示消息判断、查询与答复;需要持续交付的任务见任务看板,回复送达情况见发信箱。')) : null,
345
+ ready && page.items?.length ? React.createElement('div', { 'aria-label': '本页处理步骤', style: { display: 'flex', flexWrap: 'wrap', gap: 8 } }, ...Object.entries(labels).map(([kind, label]) => { const count = page.items.filter(item => item.kind === kind).length; return count ? React.createElement('span', { key: kind, style: pill(colors.accent) }, `${label} · ${count} 条记录`) : null })) : null,
346
+ loading ? React.createElement('div', { role: 'status', style: { minHeight: 160 } }, '正在读取处理记录…') : error ? React.createElement('div', { role: 'alert' }, `处理记录读取失败:${error}`, React.createElement(Button, { variant: 'outline', size: 'sm', type: 'button', onClick: () => setRetry((value) => value + 1) }, '重试')) : !page?.items?.length ? emptyState('此页没有处理记录', { detail: '历史消息可能尚未记录完整处理轨迹。' }) : React.createElement('ol', { 'aria-label': '逐步判断记录', start: Number(cursor || 0) + 1, style: { margin: 0, paddingLeft: 28, display: 'grid', gap: 16 } }, ...page.items.map((item, index) => {
347
+ const view = item.summary ?? { title: '处理记录', conclusion: '尚未记录判断结论', rows: [] }
348
+ const tone = ['failed', 'blocked', 'needs_attention'].includes(item.status) ? colors.warning : colors.accent
349
+ return React.createElement('li', { key: item.id || index, style: { borderLeft: `2px solid ${colors.border}`, padding: '0 0 12px 16px', minWidth: 0, fontSize: 14, overflowWrap: 'anywhere' } },
350
+ React.createElement('div', { style: { display: 'flex', alignItems: 'center', gap: 8, flexWrap: 'wrap' } }, React.createElement('strong', null, view.title), React.createElement('span', { style: pill(tone) }, traceStatus(item.status)), React.createElement('span', { style: { marginLeft: 'auto', color: colors.muted, fontSize: 12, textAlign: 'right', fontVariantNumeric: 'tabular-nums' } }, traceElapsed(item, clock))),
351
+ item.kind === 'intent' && item.sourceMessages?.length ? React.createElement('section', { 'aria-label': '本次判断的来源消息', style: { background: colors.surface2, padding: 12, borderRadius: ui.radiusSm, margin: '10px 0' } },
352
+ React.createElement('strong', { style: { fontSize: 13 } }, `${item.topicTitle || '当前话题'} · 本次判断覆盖 ${item.sourceMessages.length} 条消息`),
353
+ React.createElement('ol', { style: { margin: '12px 0 0', paddingLeft: 22, display: 'grid', gap: 12 } }, ...item.sourceMessages.map(message => React.createElement('li', { key: message.runId, style: { borderLeft: message.current ? `3px solid ${colors.accent}` : `3px solid ${colors.border}`, paddingLeft: 10 } },
354
+ React.createElement('div', { style: { fontSize: 12, color: colors.muted } }, `${message.senderName || '发送人未记录'} · ${fmt(message.occurredAt)} · ${message.current ? '当前查看的消息' : '共同参与判断'}`),
355
+ React.createElement('p', { style: { margin: '6px 0', whiteSpace: 'pre-wrap', lineHeight: 1.65 } }, message.text),
356
+ !message.current ? React.createElement(Button, { variant: 'outline', size: 'sm', type: 'button', onClick: () => { setPage(undefined); setCursor(''); setCursorHistory([]); setRunId(message.runId) } }, '查看这条消息的过程') : null))),
357
+ React.createElement('p', { style: { margin: '10px 0 0', color: colors.muted, fontSize: 12 } }, '以上消息共享下面这次判断,判断耗时只记录一次。后续重判保留为另一条判断记录。')) : null,
358
+ React.createElement('p', { style: { margin: '10px 0', fontWeight: 600, lineHeight: 1.6 } }, view.conclusion),
359
+ view.rows.length ? React.createElement('dl', { style: { margin: '0 0 12px', display: 'grid', gap: 10 } }, ...view.rows.map((row, i) => React.createElement('div', { key: i }, React.createElement('dt', { style: { color: colors.muted, fontSize: 12, marginBottom: 4 } }, row.label), React.createElement('dd', { style: { margin: 0, whiteSpace: 'pre-wrap', lineHeight: 1.65 } }, row.value)))) : null,
360
+ item.clarification ? React.createElement(WorkflowClarification, { key: item.clarification.requestId, request: item.clarification, onSubmitted: () => setRetry(value => value + 1) }) : null,
361
+ item.kind === 'agent' ? React.createElement('div', { style: { display: 'grid', gap: 8, margin: '8px 0' } },
362
+ item.outputRef ? React.createElement(TraceReadout, { key: item.outputRef, runId, resourceRef: item.outputRef, label: `查看答复与依据${item.evidenceCount ? `(${item.evidenceCount} 条)` : ''}` }) : null,
363
+ item.sessionId && onOpenSession ? React.createElement(Button, { variant: 'outline', size: 'sm', type: 'button', style: { justifySelf: 'start' }, onClick: () => onOpenSession(item.sessionId) }, '查看会话记录') : null) : null,
364
+ item.reason ? React.createElement('p', { style: { color: colors.warning, whiteSpace: 'pre-wrap' } }, traceReason(item.reason)) : null,
365
+ item.sourceRunIds?.length > 1 ? React.createElement('p', { style: { color: colors.muted, fontSize: 12 } }, `同一话题的 ${new Set(item.sourceRunIds).size} 条消息共同判断,各事项的决定分别列出。`) : null,
366
+ item.gaps?.length ? React.createElement('p', { style: { color: colors.warning } }, `依据缺口:${item.gaps.join(';')}`) : null)
367
+ })),
368
+ ready ? React.createElement('span', { style: { color: colors.muted, fontSize: 12 } }, `本页 ${page.items?.length ?? 0} 条${Number.isFinite(page.total) ? `,共 ${page.total} 条处理记录` : ''};按记录时间排列,多事项可能分别判断。`) : null,
369
+ ready ? React.createElement('p', { style: { color: colors.muted, fontSize: 12, margin: 0 } }, '耗时按本次开始到完成计算,包含工具等待;重试显示最近一次处理,不代表纯模型耗时。进行中记录可刷新查看最新状态。') : null,
370
+ React.createElement('div', { style: { display: 'flex', gap: 8 } }, cursorHistory.length ? React.createElement(Button, { variant: 'outline', size: 'sm', type: 'button', disabled: loading, onClick: () => { setCursor(cursorHistory.at(-1)); setCursorHistory((items) => items.slice(0, -1)) } }, '上一页') : null, page?.nextCursor ? React.createElement(Button, { variant: 'outline', size: 'sm', type: 'button', disabled: loading, onClick: () => { setCursorHistory((items) => [...items, cursor]); setCursor(page.nextCursor) } }, '下一页') : null))
371
+ }
372
+ function TopicBrowser({ groups, tasks, target, updatedAt, onOpenTask, onOpenSession }) {
142
373
  const [groupId, setGroupId] = useState(target?.groupId || '')
143
374
  const [offset, setOffset] = useState(0)
144
375
  const [selection, setSelection] = useState(target)
145
376
  const [messageOffset, setMessageOffset] = useState(0)
146
377
  const [listing, setListing] = useState()
147
378
  const [context, setContext] = useState()
379
+ const [topicContext, setTopicContext] = useState()
380
+ const [contextCursor, setContextCursor] = useState('')
381
+ const [contextCursorHistory, setContextCursorHistory] = useState([])
382
+ const [contextRevision, setContextRevision] = useState()
383
+ const [intentCursor, setIntentCursor] = useState('')
384
+ const [intentCursorHistory, setIntentCursorHistory] = useState([])
385
+ const [selectedIntentRun, setSelectedIntentRun] = useState('')
386
+ const [contextError, setContextError] = useState('')
387
+ const [contextLoading, setContextLoading] = useState(false)
148
388
  const [listError, setListError] = useState('')
149
389
  const [detailError, setDetailError] = useState('')
150
390
  const [loading, setLoading] = useState(true)
@@ -152,6 +392,15 @@ window.__ModuleLoader__.load({
152
392
  const [retry, setRetry] = useState(0)
153
393
  const topicPageSize = 10
154
394
  const messagePageSize = 25
395
+ useEffect(() => {
396
+ if (!target?.topicId) return
397
+ setGroupId(target.groupId || '')
398
+ setSelection(target)
399
+ setMessageOffset(0)
400
+ setContextCursor('')
401
+ setContextCursorHistory([])
402
+ setIntentCursor(''); setIntentCursorHistory([]); setSelectedIntentRun(''); setContextRevision(undefined)
403
+ }, [target?.groupId, target?.topicId, target?.revision])
155
404
  useEffect(() => {
156
405
  let active = true
157
406
  setLoading(true); setListError('')
@@ -168,35 +417,112 @@ window.__ModuleLoader__.load({
168
417
  get(`/state/topics/${encodeURIComponent(selection.topicId)}?${params}`).then((value) => { if (active) setContext(value) }, (error) => { if (active) setDetailError(error.message) }).finally(() => { if (active) setDetailLoading(false) })
169
418
  return () => { active = false }
170
419
  }, [selection, messageOffset, retry])
171
- const selectTopic = (topic) => { setSelection({ groupId: topic.groupId, topicId: topic.topicId, revision: topic.revision }); setMessageOffset(0) }
420
+ useEffect(() => {
421
+ if (!selection) { setTopicContext(undefined); return }
422
+ let active = true
423
+ setContextLoading(true); setContextError('')
424
+ const params = new URLSearchParams()
425
+ if (contextCursor) params.set('cursor', contextCursor)
426
+ if (intentCursor) params.set('intentCursor', intentCursor)
427
+ if (contextRevision !== undefined) params.set('revision', String(contextRevision))
428
+ get(`/state/workflows/topics/${encodeURIComponent(selection.topicId)}/context?${params}`).then((value) => { if (active) { setTopicContext(value); setContextRevision(value.revision) } }, (cause) => { if (active) setContextError(cause.message) }).finally(() => { if (active) setContextLoading(false) })
429
+ return () => { active = false }
430
+ }, [selection, contextCursor, intentCursor, retry])
431
+ const selectTopic = (topic) => { setSelection({ groupId: topic.groupId, topicId: topic.topicId, revision: topic.revision }); setMessageOffset(0); setContextCursor(''); setContextCursorHistory([]); setIntentCursor(''); setIntentCursorHistory([]); setSelectedIntentRun(''); setContextRevision(undefined); setTopicContext(undefined) }
432
+ if (selectedIntentRun) return React.createElement(TraceDetail, { key: selectedIntentRun, runId: selectedIntentRun, onOpenSession, onBack: () => setSelectedIntentRun(''), backLabel: '返回话题判断列表' })
172
433
  const topic = context?.topic
173
434
  const group = groups.find((item) => item.groupId === groupId)
174
- const relatedTasks = tasks.filter((task) => task.groupId === selection?.groupId && task.topicRefs?.some((ref) => ref.topicId === selection?.topicId))
435
+ const relatedTasks = tasks.filter((task) => task.topicRefs?.some((ref) => (ref.groupId || task.sourceGroupId || task.groupId) === selection?.groupId && ref.topicId === selection?.topicId))
175
436
  const failure = (message) => React.createElement('div', { role: 'alert', style: { padding: 16, color: colors.danger } }, message, ' ', React.createElement(Button, { variant: 'outline', size: 'sm', type: 'button', onClick: () => setRetry((value) => value + 1) }, '重试'))
176
437
  const pager = (pageOffset, total, pageSize, onChange, busy) => React.createElement('div', { style: { ...tableFooter, flexWrap: 'wrap' } }, React.createElement('span', { style: { marginRight: 'auto', color: colors.muted, fontSize: 12 } }, `${total} 条 · 第 ${Math.floor(pageOffset / pageSize) + 1} 页`), React.createElement(Button, { variant: 'outline', size: 'sm', type: 'button', disabled: busy || pageOffset === 0, onClick: () => onChange(Math.max(0, pageOffset - pageSize)) }, '上一页'), React.createElement(Button, { variant: 'outline', size: 'sm', type: 'button', disabled: busy || pageOffset + pageSize >= total, onClick: () => onChange(pageOffset + pageSize) }, '下一页'))
177
- return React.createElement('section', { 'aria-label': '话题与上下文', style: { display: 'grid', gap: 16 } },
178
- React.createElement('div', { style: { ...toolbar, flexWrap: 'wrap' } }, React.createElement(SelectMenu, { label: '筛选话题群聊', value: groupId, options: [{ id: '', label: '全部群聊' }, ...groups.map((item) => ({ id: item.groupId, label: item.name || item.groupId }))], onChange: (value) => { setGroupId(value); setOffset(0); setListing(undefined); setSelection(undefined) } }), React.createElement('span', { style: { fontSize: 12, color: colors.muted } }, group?.topicProgress ? `待归类 ${group.topicProgress.unroutedMessages} 条 · 待处理 ${group.topicProgress.pending} 个话题` : '接收、话题处理与任务完成分别计量')),
179
- React.createElement('div', { style: { display: 'flex', flexWrap: 'wrap', alignItems: 'flex-start', gap: 16 } },
180
- React.createElement('section', { 'aria-label': '话题列表', 'aria-busy': loading, style: { ...tableFrame, flex: '1 1 340px', width: '100%', minWidth: 0, maxWidth: 420, minHeight: 200 } },
438
+ return React.createElement('section', { 'aria-label': '话题与上下文', style: { display: 'grid', gap: ui.space4 } },
439
+ React.createElement('div', { className: 'observer-toolbar', style: { ...toolbar, flexWrap: 'wrap' } }, React.createElement(SelectMenu, { label: '筛选话题群聊', value: groupId, options: [{ id: '', label: '全部群聊' }, ...groups.map((item) => ({ id: item.groupId, label: item.name || item.groupId }))], onChange: (value) => { setGroupId(value); setOffset(0); setListing(undefined); setSelection(undefined) } }), React.createElement('span', { style: { fontSize: ui.textSm, color: colors.muted } }, group?.topicProgress ? `待归类 ${group.topicProgress.unroutedMessages} 条 · 待处理 ${group.topicProgress.pending} 个话题` : '接收、话题处理与任务完成分别计量')),
440
+ React.createElement('div', { className: 'observer-topics-layout', style: { display: 'flex', flexWrap: 'wrap', alignItems: 'flex-start', gap: ui.space4 } },
441
+ React.createElement('section', { className: 'observer-topic-list', 'aria-label': '话题列表', 'aria-busy': loading, style: { ...tableFrame, flex: '1 1 340px', width: '100%', minWidth: 0, maxWidth: 420, minHeight: 200 } },
181
442
  React.createElement('div', { role: 'status', style: { ...toolbar, minHeight: 52 } }, React.createElement('strong', { style: { fontSize: 14 } }, loading ? '正在刷新话题…' : '话题'), listing ? React.createElement('span', { style: { color: colors.muted, fontSize: 12, fontVariantNumeric: 'tabular-nums' } }, `${listing.total} 个`) : null),
182
- listError ? failure(`话题加载失败:${listError}`) : !listing ? emptyState('正在读取话题…') : listing.topics.length === 0 ? emptyState('暂无话题,消息归类后会显示在这里') : React.createElement('ul', { style: { listStyle: 'none', margin: 0, padding: 0 } }, ...listing.topics.map((item) => { const selected = selection?.topicId === item.topicId && selection?.groupId === item.groupId; return React.createElement('li', { key: `${item.groupId}:${item.topicId}`, style: { borderBottom: `1px solid ${colors.border}` } }, React.createElement('button', { type: 'button', 'aria-pressed': selected, 'aria-label': item.title || item.topicId, onClick: () => selectTopic(item), style: { position: 'relative', width: '100%', minWidth: 0, border: 0, borderLeft: `3px solid ${selected ? colors.accent : 'transparent'}`, padding: '15px 16px 14px 13px', background: selected ? `color-mix(in srgb, ${colors.accent} 8%, ${colors.cardSurface})` : colors.cardSurface, color: 'inherit', cursor: 'pointer', textAlign: 'left', fontFamily: 'inherit', outlineOffset: -2 } },
183
- React.createElement('strong', { title: item.title || item.topicId, style: { display: 'block', minWidth: 0, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', overflowWrap: 'anywhere', fontSize: 13, fontWeight: 650, lineHeight: 1.55 } }, item.title || item.topicId),
184
- React.createElement('p', { title: item.summary || '暂无摘要', style: { margin: '5px 0 0', display: '-webkit-box', WebkitLineClamp: 2, WebkitBoxOrient: 'vertical', overflow: 'hidden', overflowWrap: 'anywhere', whiteSpace: 'pre-wrap', color: colors.muted, fontSize: 11.5, lineHeight: 1.55 } }, item.summary || '暂无摘要')
443
+ listError ? failure(`话题加载失败:${listError}`) : !listing ? emptyState('正在读取话题…') : listing.topics.length === 0 ? emptyState('暂无话题', { detail: '新消息完成归类后会显示在这里。', actionLabel: '重新读取', onAction: () => setRetry((value) => value + 1) }) : React.createElement('ul', { style: { listStyle: 'none', margin: 0, padding: 0 } }, ...listing.topics.map((item) => { const selected = selection?.topicId === item.topicId && selection?.groupId === item.groupId; return React.createElement('li', { key: `${item.groupId}:${item.topicId}`, style: { borderBottom: `1px solid ${colors.border}` } }, React.createElement('button', { type: 'button', 'aria-pressed': selected, 'aria-label': item.title || item.topicId, onClick: () => selectTopic(item), style: { position: 'relative', width: '100%', minWidth: 0, border: 0, borderLeft: `3px solid ${selected ? colors.accent : 'transparent'}`, padding: `${ui.space4}px ${ui.space4}px ${ui.space4}px ${ui.space3}px`, background: selected ? `color-mix(in srgb, ${colors.accent} 8%, ${colors.cardSurface})` : colors.cardSurface, color: 'inherit', cursor: 'pointer', textAlign: 'left', fontFamily: 'inherit', outlineOffset: -2 } },
444
+ React.createElement('strong', { title: item.title || item.topicId, style: { display: 'block', minWidth: 0, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', overflowWrap: 'anywhere', fontSize: ui.textMd, fontWeight: 650, lineHeight: 1.5 } }, item.title || item.topicId),
445
+ React.createElement('p', { title: item.summary || '暂无摘要', style: { margin: `${ui.space1}px 0 0`, display: '-webkit-box', WebkitLineClamp: 2, WebkitBoxOrient: 'vertical', overflow: 'hidden', overflowWrap: 'anywhere', whiteSpace: 'pre-wrap', color: colors.muted, fontSize: ui.textSm, lineHeight: 1.6 } }, item.summary || '暂无摘要')
185
446
  )) })),
186
447
  listing ? pager(offset, listing.total, topicPageSize, setOffset, loading) : null),
187
- React.createElement('section', { 'aria-label': '话题详情', 'aria-busy': detailLoading, style: { ...tableFrame, flex: '2 1 560px', width: '100%', minWidth: 0, minHeight: 200 } },
448
+ React.createElement('section', { className: 'observer-topic-detail', 'aria-label': '话题详情', 'aria-busy': detailLoading, style: { ...tableFrame, flex: '2 1 560px', width: '100%', minWidth: 0, minHeight: 200 } },
188
449
  React.createElement('header', { style: { padding: '17px 18px 15px', borderBottom: `1px solid ${colors.border}`, background: colors.cardSurface } }, React.createElement('h2', { title: topic?.title, style: { minWidth: 0, margin: 0, overflowWrap: 'anywhere', fontSize: 18, fontWeight: 650, lineHeight: 1.5, letterSpacing: '-.01em' } }, topic?.title || '话题详情')),
189
450
  detailError ? failure(`上下文加载失败:${detailError}`) : detailLoading ? React.createElement('div', { role: 'status' }, emptyState('正在读取固定版本上下文…')) : !context ? emptyState('选择一个话题查看消息和关联任务') : React.createElement('div', { role: 'region', 'aria-label': '话题详情内容', style: { minWidth: 0, maxHeight: 'calc(100vh - 260px)', padding: '0 18px 18px', display: 'grid', gap: 0, overflowY: 'auto', overflowX: 'hidden', overflowWrap: 'anywhere' } },
190
451
  React.createElement('section', { 'aria-label': '话题摘要', style: { marginTop: 16, padding: '13px 14px', borderLeft: `3px solid ${colors.accent}`, borderRadius: '0 9px 9px 0', background: `color-mix(in srgb, ${colors.accent} 6%, ${colors.surface2})` } }, React.createElement('strong', { style: { display: 'block', fontSize: 12 } }, '话题摘要'), React.createElement('p', { style: { margin: '6px 0 0', whiteSpace: 'pre-wrap', color: topic?.summary ? 'inherit' : colors.muted, fontSize: 13, lineHeight: 1.65 } }, topic?.summary || '暂无摘要')),
452
+ React.createElement('section', { 'aria-label': '当前上下文与判断记录', style: { padding: '16px 0', borderBottom: `1px solid ${colors.border}`, display: 'grid', gap: 10, fontSize: 12 } },
453
+ React.createElement('strong', { style: { fontSize: 13 } }, '当前上下文与判断记录'),
454
+ contextLoading ? React.createElement('span', { role: 'status' }, '正在读取话题上下文…') : contextError ? React.createElement('div', { role: 'alert', style: { color: colors.danger } }, `上下文读取失败:${contextError}`, React.createElement(Button, { variant: 'outline', size: 'sm', type: 'button', onClick: () => { setContextCursor(''); setContextCursorHistory([]); setIntentCursor(''); setIntentCursorHistory([]); setContextRevision(undefined); setRetry((value) => value + 1) } }, '重新读取当前上下文')) : topicContext ? React.createElement(React.Fragment, null,
455
+ readout('当前有效状态', topicContext.current),
456
+ React.createElement('strong', null, `事实 · ${(topicContext.facts || []).length} 条`),
457
+ ...(topicContext.facts || []).map((fact, index) => React.createElement('details', { key: fact.factId || index }, React.createElement('summary', { style: { cursor: 'pointer' } }, `${fact.kind || '事实'} · ${fact.text || '内容未记录'} · ${fact.status || '状态未记录'}`), readout('原文来源', fact.sourceRefs))),
458
+ React.createElement('strong', null, `意图判断批次 · ${(topicContext.intentRuns || []).length} 条`),
459
+ ...(topicContext.intentRuns || []).map((item, index) => React.createElement('details', { key: item.nodeRunId || item.intentRunId || index }, React.createElement('summary', { style: { cursor: 'pointer' } }, `${fmt(item.createdAt)} · ${item.status || '状态未记录'} · ${short(item.intentRunId)}`), readout('本批来源', item.sourceRunIds), readout('判断依据', item.reason), item.carrierRunId ? React.createElement(Button, { variant: 'outline', size: 'sm', type: 'button', onClick: () => setSelectedIntentRun(item.carrierRunId) }, '查看本次判断') : React.createElement('span', { style: { color: colors.muted } }, '历史未记录判断来源'))),
460
+ React.createElement('nav', { 'aria-label': '判断批次分页', style: { display: 'flex', gap: 8 } }, React.createElement(Button, { variant: 'outline', size: 'sm', type: 'button', disabled: !intentCursorHistory.length, onClick: () => { setIntentCursor(intentCursorHistory.at(-1)); setIntentCursorHistory((items) => items.slice(0, -1)) } }, '上一页判断'), React.createElement(Button, { variant: 'outline', size: 'sm', type: 'button', disabled: !topicContext.intentNextCursor, onClick: () => { setIntentCursorHistory((items) => [...items, intentCursor]); setIntentCursor(topicContext.intentNextCursor) } }, '下一页判断')),
461
+ React.createElement('div', { style: { display: 'flex', gap: 8 } }, contextCursorHistory.length ? React.createElement(Button, { variant: 'outline', size: 'sm', type: 'button', onClick: () => { setContextCursor(contextCursorHistory.at(-1)); setContextCursorHistory((items) => items.slice(0, -1)) } }, '上一页') : null, topicContext.nextCursor ? React.createElement(Button, { variant: 'outline', size: 'sm', type: 'button', onClick: () => { setContextCursorHistory((items) => [...items, contextCursor]); setContextCursor(topicContext.nextCursor) } }, '下一页') : null)) : React.createElement('span', { style: { color: colors.muted } }, '历史未记录')),
191
462
  React.createElement('div', { style: { display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(min(100%, 240px), 1fr))', gap: 12, padding: '16px 0', borderBottom: `1px solid ${colors.border}` } },
192
463
  React.createElement('section', { 'aria-label': '当前待解决问题', style: { minWidth: 0 } }, React.createElement('strong', { style: { display: 'block', marginBottom: 8, fontSize: 12 } }, `待解决问题 · ${topic?.openQuestions?.length || 0}`), topic?.openQuestions?.length ? React.createElement('ul', { style: { margin: 0, paddingLeft: 18, display: 'grid', gap: 6, fontSize: 12, lineHeight: 1.6 } }, ...topic.openQuestions.map((question, index) => React.createElement('li', { key: index }, question))) : React.createElement('span', { style: { color: colors.muted, fontSize: 12 } }, '当前没有待解决问题')),
193
- React.createElement('section', { 'aria-label': '关联任务', style: { minWidth: 0 } }, React.createElement('strong', { style: { display: 'block', marginBottom: 8, fontSize: 12 } }, `关联任务 · ${relatedTasks.length}`), React.createElement('div', { style: { display: 'flex', flexWrap: 'wrap', gap: 7 } }, ...relatedTasks.map((task) => React.createElement(Button, { key: task.taskId, variant: 'outline', size: 'sm', type: 'button', disabled: !task.childSessionId || task.state === 'queued', onClick: () => onOpenTask(task), style: { maxWidth: '100%' } }, React.createElement('span', { style: { overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' } }, task.title || task.objective))), relatedTasks.length === 0 ? React.createElement('span', { style: { color: colors.muted, fontSize: 12 } }, '暂无关联任务') : null))),
464
+ React.createElement('section', { 'aria-label': '关联任务', style: { minWidth: 0 } }, React.createElement('strong', { style: { display: 'block', marginBottom: 8, fontSize: 12 } }, `关联任务 · ${relatedTasks.length}`), React.createElement('div', { style: { display: 'flex', flexWrap: 'wrap', gap: 7 } }, ...relatedTasks.map((task) => React.createElement(Button, { key: task.taskId, variant: 'outline', size: 'sm', type: 'button', disabled: task.engine !== 'workflow-v2' && (!task.childSessionId || task.state === 'queued'), onClick: () => onOpenTask(task), style: { maxWidth: '100%' } }, React.createElement('span', { style: { overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' } }, task.title || task.objective))), relatedTasks.length === 0 ? React.createElement('span', { style: { color: colors.muted, fontSize: 12 } }, '暂无关联任务') : null))),
194
465
  React.createElement('section', { 'aria-label': '群聊消息', style: { marginTop: 4 } },
195
466
  React.createElement('strong', { style: { display: 'block', padding: '14px 0', fontSize: 13, fontWeight: 600 } }, `群聊消息 · ${context.total} 条`),
196
467
  React.createElement('div', { style: { display: 'grid' } }, ...(context.messages?.length ? context.messages.map((message, index) => React.createElement('article', { key: `${message.messageId}:${index}`, style: { position: 'relative', minWidth: 0, padding: '10px 0 15px 22px', borderLeft: `1px solid ${colors.border}` } }, React.createElement('span', { 'aria-hidden': true, style: { position: 'absolute', top: 15, left: -4, width: 7, height: 7, borderRadius: '50%', background: index === 0 ? colors.accent : colors.cardSurface, border: `1px solid ${index === 0 ? colors.accent : colors.border}` } }), React.createElement('div', { style: { display: 'flex', flexWrap: 'wrap', alignItems: 'baseline', gap: '2px 8px', color: colors.muted, fontSize: 11 } }, React.createElement('strong', { style: { color: 'inherit', fontWeight: 600 } }, message.senderName || message.sourceKind || '未知发送人'), React.createElement('time', { dateTime: message.occurredAt || undefined, style: { fontVariantNumeric: 'tabular-nums' } }, fmt(message.occurredAt))), React.createElement('p', { style: { margin: '6px 0 0', whiteSpace: 'pre-wrap', overflowWrap: 'anywhere', fontSize: 13, lineHeight: 1.7 } }, message.text || '(空消息)'), message.quotedMessage?.content ? React.createElement('details', { style: { marginTop: 9, color: colors.muted, fontSize: 12 } }, React.createElement('summary', { style: { cursor: 'pointer', fontWeight: 600 } }, '查看引用消息'), React.createElement('blockquote', { style: { margin: '7px 0 0', padding: '8px 10px', borderLeft: `2px solid ${colors.border}`, background: colors.surface2, whiteSpace: 'pre-wrap', overflowWrap: 'anywhere', lineHeight: 1.6 } }, message.quotedMessage.content)) : null)) : [emptyState('此版本没有消息')])),
197
468
  pager(messageOffset, context.total, messagePageSize, setMessageOffset, detailLoading))
198
469
  ))))
199
470
  }
471
+ function TaskStepOutput({ taskId, node, detailRevision }) {
472
+ return React.createElement(TaskStepOutputContent, { key: `${taskId}:${node.runId}:${node.nodeRunId}:${node.outputRef}`, taskId, node, detailRevision })
473
+ }
474
+ function TaskStepOutputContent({ taskId, node, detailRevision }) {
475
+ const [text, setText] = useState('')
476
+ const [overview, setOverview] = useState('')
477
+ const [documentName, setDocumentName] = useState('')
478
+ const [expanded, setExpanded] = useState(false)
479
+ const [cursor, setCursor] = useState(0)
480
+ const [nextCursor, setNextCursor] = useState(null)
481
+ const [loading, setLoading] = useState(true)
482
+ const [error, setError] = useState('')
483
+ const [retry, setRetry] = useState(0)
484
+ const loadedCursor = useRef(null)
485
+ useEffect(() => {
486
+ if (loadedCursor.current === cursor) { setLoading(false); return }
487
+ let active = true
488
+ setLoading(true); setError('')
489
+ get(`/state/tasks/${encodeURIComponent(taskId)}/runs/${encodeURIComponent(node.runId)}/nodes/${encodeURIComponent(node.nodeRunId)}/output?ref=${encodeURIComponent(node.outputRef)}&cursor=${cursor}&detailRevision=${encodeURIComponent(detailRevision)}`).then(value => {
490
+ if (active) { loadedCursor.current = cursor; setText(previous => cursor ? previous + value.text : value.text); setNextCursor(value.nextCursor); setOverview(value.overview || ''); setDocumentName(value.documentName || '') }
491
+ }, () => { if (active) setError('产出暂时无法读取,请重试。') }).finally(() => { if (active) setLoading(false) })
492
+ return () => { active = false }
493
+ }, [taskId, node.runId, node.nodeRunId, node.outputRef, detailRevision, cursor, retry])
494
+ const content = React.createElement('div', { style: { minWidth: 0, fontSize: 13, lineHeight: 1.7 } },
495
+ text ? React.createElement('div', { style: { display: 'grid', gap: 6, overflowWrap: 'anywhere' } }, ...text.split('\n\n').map((part, index) => {
496
+ const [heading, ...lines] = part.split('\n')
497
+ const titled = ['产出摘要', '正文', '任务要求', '发现', '限制与未确认事项', '执行范围', '相关文件', '已有文件', '新建文件', '材料正文', '已读取文件', '已修改文件', '涉及文件', '文件变更', '修改方案', '检查结果', '文件索引', '基线版本', '可修改文件', '变更文件', '提交说明', '分支', '远端', '提交版本', '执行结果', 'PR 标题', '目标仓库', '来源分支', '目标分支', 'PR 正文', 'PR 地址', 'PR 状态', '方案工件路径', '工作目录', '来源仓库', '隔离方式', '处理内容', '项目', '工作分支', '起点版本'].includes(heading)
498
+ return React.createElement('div', { key: index, className: titled ? 'observer-task-output-row' : undefined }, titled ? React.createElement('strong', { style: { fontSize: 12, fontWeight: 500, color: colors.muted } }, heading) : null,
499
+ React.createElement('div', { style: { whiteSpace: 'pre-wrap' } }, titled ? lines.join('\n') : part))
500
+ })) : null,
501
+ loading ? React.createElement('span', { role: 'status', style: { color: colors.muted } }, '正在读取产出…') : null,
502
+ error ? React.createElement('div', { role: 'alert', style: { color: colors.danger } }, error, React.createElement(Button, { type: 'button', variant: 'ghost', size: 'sm', onClick: () => setRetry(value => value + 1) }, '重试读取产出')) : null,
503
+ !loading && !error && !text ? React.createElement('span', { style: { color: colors.muted } }, '已保存节点产出,暂未提供可读展示') : null,
504
+ !loading && !error && nextCursor !== null ? React.createElement(Button, { type: 'button', variant: 'ghost', size: 'sm', onClick: () => setCursor(nextCursor) }, '继续阅读产出') : null)
505
+ const documentLink = documentName ? React.createElement('a', { href: `${ENDPOINT}/state/tasks/${encodeURIComponent(taskId)}/runs/${encodeURIComponent(node.runId)}/nodes/${encodeURIComponent(node.nodeRunId)}/document?ref=${encodeURIComponent(node.outputRef)}&detailRevision=${encodeURIComponent(detailRevision)}`, style: { display: 'inline-block', marginTop: 6, color: colors.accent, fontSize: 12 } }, `下载 ${documentName}`) : null
506
+ return React.createElement('div', null, overview ? React.createElement('details', { onToggle: event => setExpanded(event.currentTarget.open), style: { fontSize: 13, lineHeight: 1.7 } },
507
+ React.createElement('summary', { style: { cursor: 'pointer', color: colors.muted } }, overview, React.createElement('span', { style: { marginLeft: 12, color: colors.accent, fontSize: 12 } }, expanded ? '收起产出' : '查看产出')),
508
+ expanded ? React.createElement('div', { style: { marginTop: 8 } }, content) : null) : content, documentLink)
509
+ }
510
+ const linkedTaskText = text => {
511
+ const content = String(text), parts = []
512
+ let offset = 0
513
+ for (const match of content.matchAll(/\[([^\]\n]+)\]\((https?:\/\/[^\s)]+)\)|(https?:\/\/[^\s<>")。,;]+)/g)) {
514
+ const href = (match[2] || match[3]).replace(/[.,;!?]+$/, '')
515
+ parts.push(content.slice(offset, match.index), React.createElement('a', { key: match.index, href, target: '_blank', rel: 'noopener noreferrer', style: { color: colors.accent } }, match[1] || href))
516
+ if (!match[2]) parts.push(match[3].slice(href.length))
517
+ offset = match.index + match[0].length
518
+ }
519
+ parts.push(content.slice(offset))
520
+ return parts
521
+ }
522
+ const adjacentCurrentStep = (previousKeys, currentKeys, readingKey) => {
523
+ if (!readingKey || currentKeys.includes(readingKey) || !previousKeys.includes(readingKey)) return null
524
+ return currentKeys[Math.min(previousKeys.indexOf(readingKey), currentKeys.length - 1)] || ''
525
+ }
200
526
  function ObserverContent({ openSession }) {
201
527
  const [activePage, setActivePage] = useState('groups')
202
528
  const [topicTarget, setTopicTarget] = useState()
@@ -213,8 +539,17 @@ window.__ModuleLoader__.load({
213
539
  const [outboxStatusFilter, setOutboxStatusFilter] = useState('all')
214
540
  const [hoveredTaskId, setHoveredTaskId] = useState('')
215
541
  const [expandedCheckpointTaskId, setExpandedCheckpointTaskId] = useState('')
542
+ const [selectedWorkflowTaskId, setSelectedWorkflowTaskId] = useState('')
543
+ const [workflowTaskDetail, setWorkflowTaskDetail] = useState()
544
+ const [workflowDetailError, setWorkflowDetailError] = useState('')
545
+ const [workflowDetailRetry, setWorkflowDetailRetry] = useState(0)
546
+ const [stepReadingNotice, setStepReadingNotice] = useState('')
547
+ const detailFocusTarget = useRef(null)
548
+ const [selectedMessageRunId, setSelectedMessageRunId] = useState('')
216
549
  const [copiedId, setCopiedId] = useState('')
217
550
  const [navigatingSessionId, setNavigatingSessionId] = useState('')
551
+ const [archivingTaskId, setArchivingTaskId] = useState('')
552
+ const [archiveDetailTaskId, setArchiveDetailTaskId] = useState('')
218
553
  const [alertType, setAlertType] = useState('all')
219
554
  const [alertView, setAlertView] = useState('active')
220
555
  const [resolvedAlertPage, setResolvedAlertPage] = useState(1)
@@ -234,6 +569,30 @@ window.__ModuleLoader__.load({
234
569
  setRefreshState('done')
235
570
  window.setTimeout(() => setRefreshState('idle'), 1200)
236
571
  }, [refresh, refreshState])
572
+ useEffect(() => {
573
+ if (!selectedWorkflowTaskId) { setWorkflowTaskDetail(undefined); setWorkflowDetailError(''); return }
574
+ let active = true
575
+ setWorkflowDetailError('')
576
+ get(`/state/tasks/${encodeURIComponent(selectedWorkflowTaskId)}/detail`).then(value => {
577
+ if (!active) return
578
+ const previous = [...document.querySelectorAll('[aria-label="任务执行详情"] li.observer-task-step')]
579
+ const focused = document.activeElement?.closest?.('li.observer-task-step')
580
+ const reading = focused || previous.find(element => { const bounds = element.getBoundingClientRect(); return bounds.top < window.innerHeight && bounds.bottom > 0 })
581
+ const adjacentKey = adjacentCurrentStep(previous.map(element => element.dataset.stepKey), (value.executionNodes || []).map(node => node.stepKey), reading?.dataset.stepKey)
582
+ if (adjacentKey !== null) { setStepReadingNotice('正在阅读的步骤已从当前计划移除,已转到相邻步骤。'); detailFocusTarget.current = { taskId: selectedWorkflowTaskId, stepKey: adjacentKey } }
583
+ else setStepReadingNotice('')
584
+ setWorkflowTaskDetail(value)
585
+ }, cause => { if (active) setWorkflowDetailError(cause.message) })
586
+ return () => { active = false }
587
+ }, [selectedWorkflowTaskId, updatedAt, workflowDetailRetry])
588
+ useEffect(() => {
589
+ const target = detailFocusTarget.current
590
+ if (!target || target.taskId !== selectedWorkflowTaskId) return
591
+ detailFocusTarget.current = null
592
+ const steps = [...document.querySelectorAll('[aria-label="任务执行详情"] li.observer-task-step')]
593
+ const adjacent = steps.find(element => element.dataset.stepKey === target.stepKey)
594
+ ;(adjacent || document.querySelector('[aria-label="任务执行详情"] h2'))?.focus()
595
+ }, [workflowTaskDetail, selectedWorkflowTaskId])
237
596
  useEffect(() => {
238
597
  refresh()
239
598
  const timer = window.setInterval(refresh, 5000)
@@ -253,6 +612,10 @@ window.__ModuleLoader__.load({
253
612
  catch (cause) { setNavigationError(cause instanceof Error ? cause.message : String(cause)) }
254
613
  finally { setNavigatingSessionId('') }
255
614
  }
615
+ const openTask = (task) => {
616
+ if (task.engine === 'workflow-v2') { setSelectedWorkflowTaskId(task.taskId); setActivePage('tasks'); return }
617
+ if (task.childSessionId) navigate(task.childSessionId, groupsById.get(task.groupId)?.residentSessionId)
618
+ }
256
619
  const copyId = async (value) => {
257
620
  let copied = false
258
621
  try {
@@ -275,10 +638,12 @@ window.__ModuleLoader__.load({
275
638
  }
276
639
  const copyButton = (value, label) => React.createElement(Button, { variant: 'outline', size: 'sm', type: 'button', disabled: !value, onClick: (event) => { event.stopPropagation(); copyId(value).catch((cause) => setNavigationError(cause instanceof Error ? cause.message : String(cause))) }, style: { color: copiedId === value ? 'var(--dsw-alias-state-success-primary, #248a3d)' : undefined, cursor: value ? 'pointer' : 'default', fontSize: 10.5 } }, copiedId === value ? '已复制' : `复制${label}`)
277
640
  const selectedGroup = groupsById.get(selectedGroupId) || (data?.groups || [])[0]
278
- const selectedMessages = [...(selectedGroup?.messages || [])].filter((message) => message.sourceKind === 'dingtalk' || message.sourceKind === 'migration' || !message.sourceKind).sort((left, right) => Number(right.sequence || 0) - Number(left.sequence || 0))
641
+ const selectedMessages = [...(selectedGroup?.messages || [])].filter((message) => message.sourceKind === 'dingtalk' || message.sourceKind === 'migration' || message.sourceKind === 'workflow-v2' || !message.sourceKind).sort((left, right) => (new Date(right.occurredAt).getTime() || 0) - (new Date(left.occurredAt).getTime() || 0) || Number(right.sequence || 0) - Number(left.sequence || 0))
279
642
  const messageWorkflowState = (message) => {
643
+ if (['routing', 'waiting_clarification', 'waiting_context', 'routing_blocked', 'waiting_routing_barrier', 'intent_judging', 'intent_rejudging', 'processed'].includes(message.workflowStatus)) return message.workflowStatus
280
644
  if (message.routingStatus === 'failed') return 'failed'
281
645
  if (message.routingStatus !== 'routed') return 'routing'
646
+ if (message.sourceKind === 'workflow-v2') return 'processed'
282
647
  const related = (selectedGroup?.topics || []).flatMap((topic) => {
283
648
  const entry = [...(topic.entries || [])].reverse().find((item) => item.messageId === message.messageId)
284
649
  return entry?.action === 'add' && entry.messageVersion === message.messageVersion ? [{ topic, entry }] : []
@@ -293,6 +658,12 @@ window.__ModuleLoader__.load({
293
658
  const visibleMessages = filteredMessages.slice((currentMessagePage - 1) * pageSize, currentMessagePage * pageSize)
294
659
  const delivery = {
295
660
  routing: { label: '待归类', state: 'ongoing' },
661
+ waiting_clarification: { label: '等待澄清', state: 'ongoing' },
662
+ waiting_context: { label: '等待补充材料', state: 'ongoing' },
663
+ routing_blocked: { label: '关联受阻', state: 'error' },
664
+ waiting_routing_barrier: { label: '已关联 · 等待其他消息', state: 'ongoing' },
665
+ intent_judging: { label: '话题意图判断中', state: 'ongoing' },
666
+ intent_rejudging: { label: '新消息加入 · 重新判断', state: 'ongoing' },
296
667
  processing: { label: '话题处理中', state: 'ongoing' },
297
668
  processed: { label: '已处理', state: 'done' },
298
669
  failed: { label: '归类失败', state: 'error' },
@@ -300,11 +671,11 @@ window.__ModuleLoader__.load({
300
671
  const messageRows = visibleMessages.map((message, rowIndex) => {
301
672
  const status = delivery[messageWorkflowState(message)]
302
673
  return React.createElement('tr', { key: message.messageId, style: { background: rowIndex % 2 ? `color-mix(in srgb, ${colors.surface2} 55%, transparent)` : colors.cardSurface } },
303
- React.createElement('td', { style: { ...tableBodyCell, width: 104 }, title: message.agentDeliveryError || '' }, clampTableContent(tableStatusTag(status.label, status.state, { fontWeight: 600 }))),
674
+ React.createElement('td', { style: { ...tableBodyCell, width: 104 }, title: message.workflowStatusDetail || message.agentDeliveryError || '' }, clampTableContent(tableStatusTag(status.label, status.state, { fontWeight: 600 }))),
304
675
  React.createElement('td', { style: { ...tableBodyCell, width: 160 } }, clampTableContent(React.createElement('strong', { style: { fontSize: 14, fontWeight: 600 } }, message.senderName || message.senderOpenDingTalkId || '发送人未记录'), React.createElement('div', { style: { marginTop: 3, fontSize: 11, color: colors.muted } }, fmt(message.occurredAt)))),
305
- React.createElement('td', { style: { ...tableBodyCell, width: 160 } }, React.createElement('div', { style: { display: 'flex', gap: 4, minWidth: 0, overflow: 'hidden' } }, ...(message.topicRefs?.length ? message.topicRefs.map((ref) => React.createElement('button', { key: ref.topicId, type: 'button', title: ref.title, 'aria-label': `话题 ${ref.title}`, onClick: () => { setTopicTarget({ groupId: selectedGroup.groupId, topicId: ref.topicId, revision: ref.revision }); setActivePage('topics') }, style: { ...pill(colors.accent), display: 'inline-block', flex: '0 1 auto', minWidth: 0, maxWidth: 120, border: 0, padding: '2px 7px', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', cursor: 'pointer', fontFamily: 'inherit' } }, ref.title)) : [React.createElement('span', { key: 'unrouted', style: { color: colors.muted, fontSize: 12 } }, '待归类')]))),
676
+ React.createElement('td', { style: { ...tableBodyCell, width: 160 } }, React.createElement('div', { style: { display: 'flex', gap: 4, minWidth: 0, overflow: 'hidden' } }, ...(message.topicRefs?.length ? message.topicRefs.map((ref) => React.createElement('button', { key: ref.topicId, type: 'button', title: ref.title, 'aria-label': `话题 ${ref.title}`, onClick: () => { setTopicTarget({ groupId: selectedGroup.groupId, topicId: ref.topicId, revision: ref.revision }); setActivePage('topics') }, style: { ...pill(colors.accent), display: 'inline-block', flex: '0 1 auto', minWidth: 0, maxWidth: 120, border: 0, padding: '2px 7px', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', cursor: 'pointer', fontFamily: 'inherit' } }, ref.title)) : [React.createElement('span', { key: 'unrouted', style: { color: colors.muted, fontSize: 12 } }, messageWorkflowState(message) === 'processed' ? '—' : '待归类')]))),
306
677
  React.createElement('td', { title: message.text, style: tableBodyCell }, clampTableContent(message.text || '(空消息)')),
307
- React.createElement('td', { style: { ...tableBodyCell, width: 240 } }, singleLineTableContent(React.createElement('code', { title: message.messageId, style: { fontSize: 14, color: colors.muted, whiteSpace: 'nowrap' } }, `#${message.sequence ?? '—'} · ${short(message.messageId)}`))))
678
+ React.createElement('td', { style: { ...tableBodyCell, width: 240 } }, singleLineTableContent(React.createElement('code', { title: message.messageId, style: { fontSize: 14, color: colors.muted, whiteSpace: 'nowrap' } }, `#${message.sequence ?? '—'} · ${short(message.messageId)}`)), message.runId ? React.createElement(Button, { variant: 'outline', size: 'sm', type: 'button', onClick: () => setSelectedMessageRunId(message.runId), style: { marginTop: 5 } }, '处理过程') : React.createElement('span', { style: { color: colors.muted, fontSize: 11, display: 'block' } }, '历史未记录处理过程')))
308
679
  })
309
680
  const selectedOutbox = [...(selectedGroup?.outbox || [])].reverse()
310
681
  const outboxState = (message) => outboxDelivery(message).id
@@ -318,7 +689,7 @@ window.__ModuleLoader__.load({
318
689
  return React.createElement('tr', { key: message.outboundId, style: { background: rowIndex % 2 ? `color-mix(in srgb, ${colors.surface2} 55%, transparent)` : colors.cardSurface } },
319
690
  React.createElement('td', { style: { ...tableBodyCell, width: 124 } }, clampTableContent(tableStatusTag(status.label, status.state, { fontWeight: 600 }))),
320
691
  React.createElement('td', { title: message.text, style: tableBodyCell }, clampTableContent(message.text || '(空消息)'),
321
- status.detail ? React.createElement('div', { style: { marginTop: 6, fontSize: 12, color: status.state === 'error' ? colors.danger : colors.warning, overflowWrap: 'anywhere' } }, status.detail, message.deliveryError ? `:${message.deliveryError}` : '') : null,
692
+ status.detail ? React.createElement('div', { style: { marginTop: 6, fontSize: 12, color: status.state === 'error' ? colors.danger : colors.warning, overflowWrap: 'anywhere' } }, status.detail, !message.supersededByOutboundId && !message.recallStatus && message.deliveryError ? `:${message.deliveryError}` : '') : null,
322
693
  message.deliveryAttemptedAt ? React.createElement('div', { style: { marginTop: 3, fontSize: 11, color: colors.muted } }, `最近尝试 ${fmt(message.deliveryAttemptedAt)} · 共 ${message.deliveryAttemptCount || 1} 次`) : null),
323
694
  React.createElement('td', { style: { ...tableBodyCell, width: 220 } },
324
695
  React.createElement('div', { style: { minWidth: 0 } }, singleLineTableContent(React.createElement('code', { title: message.sourceMessageId, style: { fontSize: 14, color: colors.muted, whiteSpace: 'nowrap' } }, short(message.sourceMessageId))), message.replyToMessageId ? React.createElement('div', { style: { marginTop: 3, fontSize: 11, color: colors.muted, whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' } }, '回复 ', React.createElement('code', { title: message.replyToMessageId }, short(message.replyToMessageId))) : null)),
@@ -327,42 +698,95 @@ window.__ModuleLoader__.load({
327
698
  })
328
699
  const renderTaskCard = (task) => {
329
700
  const group = groupsById.get(task.groupId)
701
+ const sourceGroup = groupsById.get(task.sourceGroupId || task.groupId)
702
+ const groupLabel = sourceGroup?.name || (task.groupId?.startsWith('web:') ? 'Web 任务' : task.groupId) || '未关联群聊'
703
+ const waitingNotice = task.state === 'waiting' && task.waitingKind === 'information'
704
+ ? group?.outbox?.findLast(message => message.sourceMessageId?.startsWith(`task-result:${task.taskId}:waiting:`) && !message.sourceMessageId.includes(':followup:')
705
+ && message.status !== 'superseded' && message.taskInputVersion === task.inputVersion && message.taskRunSequence === task.runSequence) : undefined
706
+ const waitingNoticeLabel = waitingNotice?.status === 'sent' && waitingNotice.deliveredMessageId ? `群通知已送达 · ${fmt(waitingNotice.deliveredAt)}`
707
+ : waitingNotice?.deliveryBlockedAt || waitingNotice?.deliveryError ? '群通知发送失败,请检查' : waitingNotice ? '群通知待确认送达' : '群通知尚未生成'
330
708
  const queued = task.state === 'queued'
331
709
  const checkpointEvents = task.checkpoints || []
332
710
  const planCheckpointIndex = checkpointEvents.findLastIndex((checkpoint) => checkpoint.kind === 'plan-confirmed')
333
711
  const planCheckpoint = checkpointEvents[planCheckpointIndex]
334
712
  const currentCheckpointEvents = checkpointEvents.slice(Math.max(0, planCheckpointIndex))
335
- const checkpoints = planCheckpoint?.remainingItems?.length ? planCheckpoint.remainingItems : (task.stageTasks?.length ? task.stageTasks : [task.objective])
713
+ const boardStages = task.workflowProgress?.stages || []
714
+ const checkpoints = boardStages.map(stage => stage.stageId)
715
+ const checkpointLabel = id => nodeTitle[id] || (boardStages.find(stage => stage.stageId === id)?.title !== id ? boardStages.find(stage => stage.stageId === id)?.title : null) || '执行步骤'
336
716
  const displayObjective = String(task.objective || '').startsWith('[TASK_SOURCE_EVIDENCE]') ? (task.title || task.objective) : task.objective
337
- const latestCheckpoint = checkpointEvents.at(-1)
338
- const latestRemainingItems = latestCheckpoint?.remainingItems || checkpoints
339
- const remainingItemsMatchPlan = latestRemainingItems.every((checkpoint) => checkpoints.includes(checkpoint))
340
- const normalizedRemainingItems = remainingItemsMatchPlan ? latestRemainingItems : checkpoints.slice(-Math.min(checkpoints.length, latestRemainingItems.length))
341
- const remainingCheckpointNames = new Set(normalizedRemainingItems)
342
- const completedCheckpointNames = new Set(task.state === 'completed' ? checkpoints : checkpoints.filter((checkpoint) => !remainingCheckpointNames.has(checkpoint)))
343
- const currentCheckpoint = task.state === 'running' ? checkpoints.find((checkpoint) => remainingCheckpointNames.has(checkpoint)) : undefined
717
+ const completedCheckpointNames = new Set(boardStages.filter(stage => stage.completed).map(stage => stage.stageId))
718
+ const currentCheckpoint = task.state === 'running' ? checkpoints.find((checkpoint) => !completedCheckpointNames.has(checkpoint)) : undefined
344
719
  const completedCheckpointCount = checkpoints.filter((checkpoint) => completedCheckpointNames.has(checkpoint)).length
345
- const progress = Math.round((completedCheckpointCount / checkpoints.length) * 100)
346
- const showCheckpoints = task.state !== 'completed' && !task.archivedAt
720
+ const progress = checkpoints.length ? Math.round((completedCheckpointCount / checkpoints.length) * 100) : 0
721
+ const showCheckpoints = checkpoints.length > 0 && !task.archivedAt
347
722
  const checkpointsExpanded = expandedCheckpointTaskId === task.taskId
348
723
  const hovered = hoveredTaskId === task.taskId
349
724
  const navigating = navigatingSessionId === task.childSessionId
725
+ const worktreesByPath = new Map()
726
+ for (const item of Array.isArray(task.localWorktrees) ? task.localWorktrees : []) {
727
+ const previous = worktreesByPath.get(item.path)
728
+ worktreesByPath.set(item.path, previous ? { ...item, documents: [...(previous.documents || []), ...(item.documents || [])] } : item)
729
+ }
730
+ const worktrees = [...worktreesByPath.values()]
731
+ const ownedWorktrees = worktrees.filter(item => item.createdByTask && item.status === 'registered')
732
+ const borrowedWorktrees = worktrees.filter(item => !item.createdByTask)
733
+ const ownedDocumentCount = ownedWorktrees.reduce((count, item) => count + new Set((item.documents || []).map(document => document.source)).size, 0)
734
+ const notifications = (task.notificationIntents || []).filter(intent => intent.inputVersion === task.inputVersion && intent.runSequence === task.runSequence && intent.status !== 'superseded')
735
+ const notificationCounts = new Map()
736
+ for (const intent of notifications.filter(item => item.status !== 'delivered')) notificationCounts.set(intent.status, (notificationCounts.get(intent.status) || 0) + 1)
737
+ const retryExhausted = task.waitingReason?.startsWith('topic_request_retry_exhausted:')
738
+ const waitingLabel = retryExhausted ? '系统协调受阻 · 待恢复' : `等待 · ${traceReason(task.waitingReason)}`
739
+ const cleanup = task.archiveCleanup
740
+ const cleanupRunning = cleanup?.status === 'running' || archivingTaskId === task.taskId
741
+ const archiveDetailOpen = archiveDetailTaskId === task.taskId
742
+ const archiveNeedsReview = worktrees.length > 0 || cleanup?.status === 'failed'
743
+ const archiveTask = async () => {
744
+ if (cleanupRunning) return
745
+ setArchivingTaskId(task.taskId)
746
+ setNavigationError('')
747
+ try { await post(`/tasks/${encodeURIComponent(task.taskId)}/archive`); setArchiveDetailTaskId(''); await refresh() }
748
+ catch (cause) { setNavigationError(cause instanceof Error ? cause.message : String(cause)); await refresh().catch(() => {}) }
749
+ finally { setArchivingTaskId('') }
750
+ }
751
+ const archiveDetail = archiveDetailOpen ? React.createElement('section', { 'aria-label': task.archivedAt ? '归档记录' : '归档前核对', style: { display: 'grid', gap: 10, minWidth: 0, padding: 10, border: `1px solid ${colors.border}`, borderRadius: 8, background: colors.surface2, fontSize: 11, lineHeight: 1.5 } },
752
+ React.createElement('strong', { style: { fontSize: 12 } }, task.archivedAt ? '归档记录' : '归档前核对'),
753
+ !task.archivedAt ? React.createElement('span', null, ownedWorktrees.length ? `将迁出 ${ownedDocumentCount} 份文档并清理 ${ownedWorktrees.length} 个任务自建目录。` : '本次仅归档任务记录。', borrowedWorktrees.length ? ` ${borrowedWorktrees.length} 个借用目录及其登记文档会保留原处。` : '') : null,
754
+ cleanup?.error ? React.createElement('span', { role: 'alert', style: { color: colors.danger, overflowWrap: 'anywhere' } }, cleanup.error) : null,
755
+ ...worktrees.map(worktree => {
756
+ const documents = [...new Map((worktree.documents || []).map(document => [document.source, document])).values()]
757
+ const label = !worktree.createdByTask ? '借用 · 保留原处' : worktree.status === 'cleaned' ? '已清理' : '任务自建 · 归档时清理'
758
+ return React.createElement('div', { key: worktree.path, style: { display: 'grid', gap: 4, minWidth: 0, paddingTop: 8, borderTop: `1px solid ${colors.border}` } },
759
+ React.createElement('strong', { style: { fontSize: 11, fontWeight: 600 } }, label, ` · ${documents.length} 份登记文档`),
760
+ worktree.branch ? React.createElement('span', { style: { color: colors.muted } }, `分支 ${worktree.branch}`) : null,
761
+ React.createElement('code', { style: { overflowWrap: 'anywhere' } }, worktree.path),
762
+ documents.length ? React.createElement('div', { style: { display: 'grid', gap: 3, maxHeight: 160, overflowY: 'auto', paddingLeft: 8, borderLeft: `2px solid ${colors.border}` } }, ...documents.map(document => React.createElement('div', { key: document.source, style: { overflowWrap: 'anywhere', color: colors.muted } }, document.source, document.archivePath ? React.createElement('div', { style: { fontSize: 10.5 } }, `归档至 ${document.archivePath}`) : null))) : null)
763
+ }),
764
+ !task.archivedAt ? React.createElement('div', { style: { display: 'flex', justifyContent: 'flex-end', gap: 8 } },
765
+ React.createElement('button', { type: 'button', onClick: () => setArchiveDetailTaskId(''), style: { border: `1px solid ${colors.border}`, borderRadius: 7, background: colors.cardSurface, color: 'inherit', padding: '5px 9px', cursor: 'pointer', fontFamily: 'inherit', fontSize: 11 } }, '取消'),
766
+ React.createElement('button', { type: 'button', disabled: cleanupRunning, onClick: archiveTask, style: { border: `1px solid ${colors.accent}`, borderRadius: 7, background: colors.accent, color: '#fff', padding: '5px 9px', cursor: cleanupRunning ? 'default' : 'pointer', fontFamily: 'inherit', fontSize: 11 } }, cleanupRunning ? '归档中…' : '确认归档')) : null) : null
350
767
  return React.createElement('div', { key: task.taskId, onMouseEnter: () => setHoveredTaskId(task.taskId), onMouseLeave: () => setHoveredTaskId(''), style: { width: '100%', minWidth: 0, boxSizing: 'border-box', border: `1px solid ${colors.border}`, borderRadius: 12, background: colors.cardSurface, boxShadow: hovered ? '0 8px 20px rgba(15,23,42,.08), 0 2px 6px rgba(15,23,42,.05)' : 'var(--dsw-shadow-card, 0 1px 2px rgba(0,0,0,.08))', padding: '11px 13px', color: 'inherit', display: 'grid', gap: 6, opacity: queued ? 0.72 : 1, transition: 'box-shadow 180ms ease' } },
351
- React.createElement('button', { type: 'button', disabled: queued || navigating, onClick: (event) => { if (event.target.closest('[data-task-card-action]')) return; navigate(task.childSessionId, group?.residentSessionId) }, style: { width: '100%', minWidth: 0, boxSizing: 'border-box', border: 0, padding: 0, background: 'transparent', color: 'inherit', cursor: queued || navigating ? 'default' : 'pointer', textAlign: 'left', display: 'grid', gap: 6, fontFamily: 'inherit' } },
352
- React.createElement('strong', { title: task.title || task.objective, style: { fontSize: 13, lineHeight: 1.45, display: '-webkit-box', WebkitLineClamp: 2, WebkitBoxOrient: 'vertical', overflow: 'hidden', overflowWrap: 'anywhere' } }, task.title || task.objective),
768
+ React.createElement('button', { type: 'button', disabled: task.engine !== 'workflow-v2' && (queued || navigating), onClick: (event) => { if (event.target.closest('[data-task-card-action]')) return; openTask(task) }, style: { width: '100%', minWidth: 0, boxSizing: 'border-box', border: 0, padding: 0, background: 'transparent', color: 'inherit', cursor: task.engine === 'workflow-v2' || !queued && !navigating ? 'pointer' : 'default', textAlign: 'left', display: 'grid', gap: 6, fontFamily: 'inherit' } },
769
+ React.createElement('strong', { title: task.title || task.objective, style: { fontSize: 13, lineHeight: 1.45, display: '-webkit-box', WebkitLineClamp: 2, WebkitBoxOrient: 'vertical', overflow: 'hidden', overflowWrap: 'anywhere' } }, taskDisplayTitle(task)),
353
770
  React.createElement('div', { style: { display: 'flex', alignItems: 'center', gap: 5, minWidth: 0, overflow: 'hidden' } },
354
- React.createElement('span', { title: group?.name || task.groupId, style: { ...pill(colors.muted), flex: '0 1 auto', maxWidth: 150, padding: '2px 7px', fontSize: 10.5, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' } }, group?.name || task.groupId),
355
- ...(task.topicRefs || []).map((ref) => { const topic = topicsById.get(`${task.groupId}:${ref.topicId}`); const label = topic?.title || `话题 ${short(ref.topicId)}`; return React.createElement('span', { key: ref.topicId, role: 'button', tabIndex: 0, title: label, 'aria-label': `话题 ${label}`, 'data-task-card-action': 'open-topic', onClick: (event) => { event.stopPropagation(); setTopicTarget({ groupId: task.groupId, topicId: ref.topicId, revision: ref.revision }); setActivePage('topics') }, onKeyDown: (event) => { if (event.key !== 'Enter' && event.key !== ' ') return; event.preventDefault(); event.stopPropagation(); setTopicTarget({ groupId: task.groupId, topicId: ref.topicId, revision: ref.revision }); setActivePage('topics') }, style: { ...pill(colors.accent), display: 'inline-block', flex: '0 1 auto', minWidth: 0, maxWidth: 96, padding: '2px 7px', fontSize: 10.5, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', cursor: 'pointer' } }, label) })),
771
+ React.createElement('span', { title: task.sourceChannel === 'web' ? `${groupLabel} · Web 重新执行` : groupLabel, style: { ...pill(colors.muted), flex: '0 1 auto', maxWidth: 150, padding: '2px 7px', fontSize: 10.5, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' } }, groupLabel),
772
+ ...(task.topicRefs || []).map((ref) => { const topicGroupId = ref.groupId || task.sourceGroupId || task.groupId; const topic = topicsById.get(`${topicGroupId}:${ref.topicId}`); const label = topic?.title || ref.title || `话题 ${short(ref.topicId)}`; return React.createElement('span', { key: ref.topicId, role: 'button', tabIndex: 0, title: label, 'aria-label': `话题 ${label}`, 'data-task-card-action': 'open-topic', onClick: (event) => { event.stopPropagation(); setTopicTarget({ groupId: topicGroupId, topicId: ref.topicId, revision: ref.revision }); setActivePage('topics') }, onKeyDown: (event) => { if (event.key !== 'Enter' && event.key !== ' ') return; event.preventDefault(); event.stopPropagation(); setTopicTarget({ groupId: topicGroupId, topicId: ref.topicId, revision: ref.revision }); setActivePage('topics') }, style: { ...pill(colors.accent), display: 'inline-block', flex: '0 1 auto', minWidth: 0, maxWidth: 96, padding: '2px 7px', fontSize: 10.5, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', cursor: 'pointer' } }, label) })),
356
773
  React.createElement('div', { title: displayObjective, style: { minWidth: 0, display: 'block', maxHeight: '4.5em', overflow: 'hidden', overflowWrap: 'anywhere', fontSize: 11, lineHeight: 1.5 } }, React.createElement('svg', { width: 12, height: 12, viewBox: '0 0 16 16', fill: 'none', stroke: 'currentColor', strokeWidth: 1.4, strokeLinecap: 'round', strokeLinejoin: 'round', role: 'img', 'aria-label': '任务目标', style: { display: 'inline-block', verticalAlign: -2, marginRight: 5, color: colors.muted } }, React.createElement('circle', { cx: 8, cy: 8, r: 5.5 }), React.createElement('circle', { cx: 8, cy: 8, r: 2 }), React.createElement('path', { d: 'M8 1v2M15 8h-2' })), displayObjective),
357
774
  React.createElement('div', null,
775
+ task.workflowProgress?.outcomeLabel || notificationCounts.size ? React.createElement('div', { role: 'status', style: { display: 'flex', flexWrap: 'wrap', alignItems: 'center', gap: 5, fontSize: 11, lineHeight: 1.5 } },
776
+ task.workflowProgress?.outcomeLabel ? React.createElement('strong', { style: { color: task.workflowProgress.outcome === 'succeeded' ? statusTone.done : colors.muted, fontWeight: 600 } }, task.workflowProgress.outcomeLabel) : null,
777
+ ...[...notificationCounts].map(([status, count], index) => React.createElement('span', { key: status, style: { color: status === 'blocked' ? colors.warning : colors.muted } }, `${task.workflowProgress?.outcomeLabel || index ? '· ' : ''}${count > 1 ? `${count} 条` : ''}${({ pending: '通知待生成', enqueued: '通知待送达', delivered: '通知已送达', blocked: '通知受阻' })[status] || status}`))) : null,
358
778
  showCheckpoints ? React.createElement('section', null,
359
779
  React.createElement('div', { role: 'button', tabIndex: 0, 'aria-expanded': checkpointsExpanded, 'aria-label': checkpointsExpanded ? '收起检查点' : `展开全部 ${checkpoints.length} 个检查点`, 'data-task-card-action': 'toggle-checkpoints', onClick: (event) => { event.stopPropagation(); setExpandedCheckpointTaskId(checkpointsExpanded ? '' : task.taskId) }, onKeyDown: (event) => { if (event.key !== 'Enter' && event.key !== ' ') return; event.preventDefault(); event.stopPropagation(); setExpandedCheckpointTaskId(checkpointsExpanded ? '' : task.taskId) }, style: { minHeight: 24, boxSizing: 'border-box', display: 'flex', alignItems: 'center', gap: 8, fontSize: 10.5, cursor: 'pointer' } }, React.createElement('span', { style: { flex: '0 0 auto', display: 'inline-flex', alignItems: 'center', gap: 4, color: colors.muted } }, React.createElement(IconChecklistOutline14, { size: 12, 'aria-label': '任务' }), '任务'), React.createElement('div', { role: 'progressbar', 'aria-label': '检查点进度', 'aria-valuemin': 0, 'aria-valuemax': 100, 'aria-valuenow': progress, style: { flex: '1 1 auto', minWidth: 24, height: 4, borderRadius: 999, overflow: 'hidden', background: `color-mix(in srgb, ${colors.muted} 22%, transparent)` } }, React.createElement('div', { style: { width: `${progress}%`, height: '100%', borderRadius: 'inherit', background: completedCheckpointCount === checkpoints.length ? statusTone.done : colors.accent, transition: 'width 180ms ease' } })), React.createElement('span', { style: { flex: '0 0 auto', color: completedCheckpointCount === checkpoints.length ? statusTone.done : colors.muted } }, `${completedCheckpointCount} / ${checkpoints.length}`), React.createElement(checkpointsExpanded ? IconChevronUpOutline14 : IconChevronDownOutline14, { size: 14, 'aria-hidden': true, style: { flex: '0 0 auto', color: colors.muted } })),
360
- checkpointsExpanded ? React.createElement('div', { style: { display: 'grid', gap: 4, padding: '2px 9px 9px' } }, ...checkpoints.map((checkpoint, index) => { const completed = completedCheckpointNames.has(checkpoint); const current = !completed && currentCheckpoint === checkpoint; const tone = completed ? statusTone.done : current ? colors.accent : colors.muted; const duration = checkpointDuration(checkpoint, currentCheckpointEvents, Date.now()); return React.createElement('div', { key: `${index}:${checkpoint}`, title: checkpoint, style: { minWidth: 0, display: 'grid', gridTemplateColumns: '12px minmax(0,1fr) 64px', gap: 5, alignItems: 'start', fontSize: 10.5, lineHeight: 1.4 } }, React.createElement('span', { 'aria-hidden': true, style: { width: 12, height: 14, display: 'inline-flex', alignItems: 'center', justifyContent: 'center', color: tone } }, completed ? React.createElement(CheckpointDoneIcon, { size: 12 }) : current ? React.createElement(StateDot, { state: 'ongoing', size: 10 }) : '○'), React.createElement('span', { style: { color: 'inherit', display: '-webkit-box', WebkitLineClamp: 'unset', WebkitBoxOrient: 'vertical', overflow: 'hidden', overflowWrap: 'anywhere' } }, checkpoint), React.createElement('span', { title: `执行时长 ${duration}`, style: { width: 64, color: colors.muted, textAlign: 'right', whiteSpace: 'nowrap', fontVariantNumeric: 'tabular-nums' } }, duration)) })) : null) : null,
361
- task.waitingReason ? React.createElement('div', { title: task.waitingReason, style: { fontSize: 10.5, lineHeight: 1.45, color: colors.warning } }, `等待:${task.waitingReason}`) : null),
780
+ checkpointsExpanded ? React.createElement('div', { style: { display: 'grid', gap: 4, padding: '2px 9px 9px' } }, ...checkpoints.map((checkpoint, index) => { const completed = completedCheckpointNames.has(checkpoint); const current = !completed && currentCheckpoint === checkpoint; const tone = completed ? statusTone.done : current ? colors.accent : colors.muted; const node = (task.executionNodes || []).find(node => node.nodeId === checkpoint); const duration = node ? React.createElement(TaskStepElapsed, { node, fontSize: 10.5 }) : checkpointDuration(checkpointLabel(checkpoint), currentCheckpointEvents, Date.now()); return React.createElement('div', { key: `${index}:${checkpoint}`, title: checkpointLabel(checkpoint), style: { minWidth: 0, display: 'grid', gridTemplateColumns: '12px minmax(0,1fr) max-content', gap: 5, alignItems: 'start', fontSize: 10.5, lineHeight: 1.4 } }, React.createElement('span', { 'aria-hidden': true, style: { width: 12, height: 14, display: 'inline-flex', alignItems: 'center', justifyContent: 'center', color: tone } }, completed ? React.createElement(CheckpointDoneIcon, { size: 12 }) : current ? React.createElement(StateDot, { state: 'ongoing', size: 10 }) : '○'), React.createElement('span', { style: { color: 'inherit', display: '-webkit-box', WebkitLineClamp: 'unset', WebkitBoxOrient: 'vertical', overflow: 'hidden', overflowWrap: 'anywhere' } }, checkpointLabel(checkpoint)), React.createElement('span', { title: node ? '本次执行耗时' : `执行时长 ${duration}`, style: { color: colors.muted, textAlign: 'right', whiteSpace: 'nowrap', fontVariantNumeric: 'tabular-nums' } }, duration)) })) : null) : null,
781
+ task.waitingReason ? React.createElement('div', { title: task.waitingReason, style: { fontSize: 11, lineHeight: 1.5, color: colors.warning, display: '-webkit-box', WebkitLineClamp: 2, WebkitBoxOrient: 'vertical', overflow: 'hidden', overflowWrap: 'anywhere' } }, waitingLabel) : null,
782
+ task.waitingReason && task.waitingKind === 'information' ? React.createElement('div', { style: { fontSize: 10.5, color: colors.muted } }, waitingNoticeLabel, task.requesterName ? ` · 发起人 ${task.requesterName}` : '') : null),
783
+ ),
784
+ cleanup?.status === 'failed' && !task.archivedAt ? React.createElement('div', { role: 'alert', style: { fontSize: 11, color: colors.danger } }, '归档失败 · 查看原因后重试') : null,
362
785
  React.createElement('div', { style: { minWidth: 0, marginTop: 0, paddingTop: 6, borderTop: `1px solid ${colors.border}`, display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 8 } },
363
786
  React.createElement('span', { title: `最后活动 ${fmt(task.updatedAt)}`, style: { minWidth: 0, display: 'inline-flex', alignItems: 'center', gap: 4, fontSize: 10.5, color: colors.muted, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' } }, React.createElement('svg', { width: 12, height: 12, viewBox: '0 0 24 24', fill: 'none', stroke: 'currentColor', strokeWidth: 1.8, strokeLinecap: 'round', strokeLinejoin: 'round', 'aria-label': '最后活动时间' }, React.createElement('circle', { cx: 12, cy: 12, r: 9 }), React.createElement('path', { d: 'M12 7v5l3 2' })), fmt(task.updatedAt)),
364
- task.state === 'completed' && !task.archivedAt ? React.createElement('button', { type: 'button', onClick: async (event) => { event.stopPropagation(); try { await post(`/tasks/${encodeURIComponent(task.taskId)}/archive`); await refresh() } catch (cause) { setNavigationError(cause instanceof Error ? cause.message : String(cause)) } }, style: { flex: '0 0 auto', border: `1px solid ${colors.border}`, borderRadius: 7, background: colors.surface2, color: colors.muted, padding: '3px 7px', cursor: 'pointer', fontFamily: 'inherit', fontSize: 10.5 } }, '归档') : null),
365
- navigating ? React.createElement('div', { role: 'status', style: { fontSize: 11.5, color: colors.accent } }, '正在打开会话…') : queued ? React.createElement('div', { style: { fontSize: 11.5, color: colors.muted } }, '等待执行,尚无对话和轨迹') : null))
787
+ task.state === 'completed' && (!task.archivedAt || worktrees.length) ? React.createElement('button', { type: 'button', disabled: cleanupRunning, 'aria-busy': cleanupRunning, 'aria-expanded': archiveNeedsReview || task.archivedAt ? archiveDetailOpen : undefined, onClick: (event) => { event.stopPropagation(); archiveNeedsReview || task.archivedAt ? setArchiveDetailTaskId(archiveDetailOpen ? '' : task.taskId) : archiveTask() }, style: { flex: '0 0 auto', border: `1px solid ${colors.border}`, borderRadius: 7, background: colors.surface2, color: cleanupRunning ? colors.muted : colors.accent, padding: '3px 7px', cursor: cleanupRunning ? 'default' : 'pointer', fontFamily: 'inherit', fontSize: 10.5 } }, cleanupRunning ? '归档中…' : task.archivedAt ? archiveDetailOpen ? '收起记录' : '归档记录' : archiveDetailOpen ? '收起预览' : cleanup?.status === 'failed' ? '查看并重试' : archiveNeedsReview ? '检查并归档' : '归档') : null),
788
+ archiveDetail,
789
+ navigating && task.engine !== 'workflow-v2' ? React.createElement('div', { role: 'status', style: { fontSize: 11.5, color: colors.accent } }, '正在打开会话…') : queued && task.engine !== 'workflow-v2' ? React.createElement('div', { style: { fontSize: 11.5, color: colors.muted } }, '等待执行,尚无对话和轨迹') : null)
366
790
  }
367
791
  const alertCategories = [
368
792
  { id: 'all', label: '全部类型' },
@@ -422,10 +846,10 @@ window.__ModuleLoader__.load({
422
846
  const groupTableToolbar = React.createElement('div', { style: toolbar },
423
847
  dataViewTabs,
424
848
  React.createElement('div', { style: { display: 'flex', alignItems: 'center', justifyContent: 'flex-end', gap: 8, flexWrap: 'wrap' } },
425
- (data?.groups || []).length ? React.createElement(SelectMenu, { label: '选择群聊会话', value: selectedGroup?.groupId || '', options: (data?.groups || []).map((group) => ({ id: group.groupId, label: group.name || group.groupId })), onChange: (value) => { setSelectedGroupId(value); setMessagePage(1); setOutboxPage(1) }, fitContent: true }) : null,
849
+ (data?.groups || []).length ? React.createElement(SelectMenu, { label: '选择群聊', value: selectedGroup?.groupId || '', options: (data?.groups || []).map((group) => ({ id: group.groupId, label: group.name || group.groupId })), onChange: (value) => { setSelectedGroupId(value); setMessagePage(1); setOutboxPage(1); setSelectedMessageRunId('') }, fitContent: true }) : null,
426
850
  groupTableView === 'messages'
427
- ? React.createElement(SelectMenu, { label: '筛选处理状态', value: messageDeliveryFilter, options: [{ id: 'all', label: '全部处理状态' }, { id: 'routing', label: '待归类' }, { id: 'processing', label: '话题处理中' }, { id: 'processed', label: '已处理' }, { id: 'failed', label: '归类失败' }], onChange: (value) => { setMessageDeliveryFilter(value); setMessagePage(1) } })
428
- : React.createElement(SelectMenu, { label: '筛选发件状态', value: outboxStatusFilter, options: [{ id: 'all', label: '全部发件状态' }, { id: 'queued', label: '待发送' }, { id: 'failed', label: '投递异常' }, { id: 'waiting', label: '待回读' }, { id: 'confirmed', label: '已发送' }, { id: 'recalled', label: '已撤回' }], onChange: (value) => { setOutboxStatusFilter(value); setOutboxPage(1) } })))
851
+ ? React.createElement(SelectMenu, { label: '筛选处理状态', value: messageDeliveryFilter, options: [{ id: 'all', label: '全部处理状态' }, { id: 'routing', label: '待归类' }, { id: 'waiting_routing_barrier', label: '等待其他消息关联' }, { id: 'intent_judging', label: '意图判断中' }, { id: 'intent_rejudging', label: '意图重新判断' }, { id: 'waiting_clarification', label: '等待澄清' }, { id: 'waiting_context', label: '等待补充材料' }, { id: 'routing_blocked', label: '关联受阻' }, { id: 'processing', label: '话题处理中' }, { id: 'processed', label: '已处理' }, { id: 'failed', label: '归类失败' }], onChange: (value) => { setMessageDeliveryFilter(value); setMessagePage(1) } })
852
+ : React.createElement(SelectMenu, { label: '筛选发件状态', value: outboxStatusFilter, options: [{ id: 'all', label: '全部发件状态' }, { id: 'queued', label: '待发送' }, { id: 'failed', label: '投递异常' }, { id: 'waiting', label: '待回读' }, { id: 'confirmed', label: '已发送' }, { id: 'superseded', label: '已替代' }, { id: 'recall-failed', label: '撤回待处理' }, { id: 'recalled', label: '已撤回' }], onChange: (value) => { setOutboxStatusFilter(value); setOutboxPage(1) } })))
429
853
  const messagesTable = React.createElement(React.Fragment, null,
430
854
  React.createElement('div', { style: { overflowX: 'auto' } }, React.createElement('table', { style: { width: '100%', minWidth: 910, borderCollapse: 'collapse', tableLayout: 'fixed' } },
431
855
  React.createElement('thead', null, React.createElement('tr', { style: { background: colors.surface2, textAlign: 'left' } }, React.createElement('th', { style: { ...tableHeadCell, width: 104 } }, '话题处理'), React.createElement('th', { style: { ...tableHeadCell, width: 160 } }, '发送人 / 时间'), React.createElement('th', { style: { ...tableHeadCell, width: 160 } }, '话题'), React.createElement('th', { style: tableHeadCell }, '消息内容'), React.createElement('th', { style: { ...tableHeadCell, width: 240 } }, '消息'))),
@@ -436,14 +860,85 @@ window.__ModuleLoader__.load({
436
860
  React.createElement('thead', null, React.createElement('tr', { style: { background: colors.surface2, textAlign: 'left' } }, React.createElement('th', { style: { ...tableHeadCell, width: 124 } }, '发送状态'), React.createElement('th', { style: tableHeadCell }, '消息内容'), React.createElement('th', { style: { ...tableHeadCell, width: 220 } }, '来源 / 回复目标'), React.createElement('th', { style: { ...tableHeadCell, width: 220 } }, '投递消息 ID'), React.createElement('th', { style: { ...tableHeadCell, width: 220 } }, '发信箱 ID'))),
437
861
  React.createElement('tbody', null, ...(outboxRows.length ? outboxRows : [React.createElement('tr', { key: 'empty' }, React.createElement('td', { colSpan: 5, style: { ...tableBodyCell, padding: 36, textAlign: 'center', color: colors.muted } }, '暂无符合条件的 Agent 发件记录'))])))),
438
862
  React.createElement('div', { style: tableFooter }, React.createElement('span', { style: { marginRight: 'auto', fontSize: 11, color: colors.muted } }, `${filteredOutbox.length} 条 · 每页 ${outboxPageSize} 条`), React.createElement(Button, { variant: 'outline', size: 'sm', type: 'button', disabled: currentOutboxPage <= 1, onClick: () => setOutboxPage((page) => Math.max(1, page - 1)) }, '上一页'), React.createElement('span', { style: { fontSize: 11, color: colors.muted } }, `${currentOutboxPage} / ${outboxPageCount}`), React.createElement(Button, { variant: 'outline', size: 'sm', type: 'button', disabled: currentOutboxPage >= outboxPageCount, onClick: () => setOutboxPage((page) => Math.min(outboxPageCount, page + 1)) }, '下一页')))
439
- const groupsPage = React.createElement('section', null, React.createElement('div', { style: tableFrame }, groupTableToolbar, groupTableView === 'messages' ? messagesTable : outboxTable))
440
- const tasksPage = React.createElement('div', { style: { display: 'grid', gridTemplateColumns: 'repeat(4, minmax(300px, 1fr))', gap: 10, overflowX: 'auto', alignItems: 'start', paddingBottom: 8 } }, ...bucketColumns)
863
+ const groupsPage = selectedMessageRunId ? React.createElement(TraceDetail, { runId: selectedMessageRunId, onOpenSession: navigate, onBack: () => setSelectedMessageRunId('') }) : React.createElement('section', null, React.createElement('div', { style: tableFrame }, groupTableToolbar, groupTableView === 'messages' ? messagesTable : outboxTable))
864
+ const selectedWorkflowTask = (workflowTaskDetail?.taskId === selectedWorkflowTaskId || workflowTaskDetail?.requestedTaskId === selectedWorkflowTaskId) ? workflowTaskDetail : undefined
865
+ const nodeState = { succeeded: '已完成', running: '执行中', ready: '待执行', waiting: '等待处理', failed: '失败', pending: '未开始', cancelled: '已取消', skipped: '已跳过' }
866
+ const planStageState = { ready: '待执行', waiting_confirmation: '等待人工确认', running: '执行中', succeeded: '已完成', invalidated: '需重新执行', blocked: '受阻' }
867
+
868
+ const taskNodes = selectedWorkflowTask?.executionNodes || []
869
+ const completedSteps = taskNodes.filter(node => !node.definitionPending && node.status === 'succeeded').length
870
+ const stepsResolved = selectedWorkflowTask?.plan?.stepsResolved !== false && !taskNodes.some(node => node.definitionPending)
871
+ const stepProgress = taskNodes.length ? Math.min(stepsResolved ? 100 : 99, Math.round(completedSteps / taskNodes.length * 100)) : 0
872
+ const taskTone = selectedWorkflowTask?.outcome === 'failed' ? colors.danger : selectedWorkflowTask?.state === 'waiting' ? colors.warning : selectedWorkflowTask?.outcome === 'succeeded' ? 'var(--dsw-alias-state-success-primary, #248a3d)' : colors.accent
873
+ const taskStatus = selectedWorkflowTask?.workflowProgress?.outcomeLabel || ({ succeeded: '已完成', failed: '执行失败', cancelled: '已取消' }[selectedWorkflowTask?.outcome]) || ({ completed: '已结束', running: '执行中', waiting: '等待处理', queued: '待执行' }[selectedWorkflowTask?.state]) || '状态未记录'
874
+ const activeStep = taskNodes.find(node => ['running', 'waiting', 'failed'].includes(node.status))
875
+ const stepColor = (status) => status === 'succeeded' ? 'var(--dsw-alias-state-success-primary, #248a3d)' : status === 'failed' ? colors.danger : status === 'waiting' ? colors.warning : status === 'running' ? colors.accent : colors.muted
876
+ const workflowDetail = selectedWorkflowTask ? React.createElement('section', { 'aria-label': '任务执行详情', style: { maxWidth: 1120, margin: '0 auto', minWidth: 0, overflowWrap: 'anywhere' } },
877
+ React.createElement('div', { style: { display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 16, fontSize: 12, color: colors.muted } },
878
+ React.createElement('span', null, '任务看板 / 任务详情'),
879
+ React.createElement(Button, { variant: 'outline', size: 'sm', type: 'button', onClick: () => setSelectedWorkflowTaskId('') }, '返回看板')),
880
+ stepReadingNotice ? React.createElement('p', { role: 'status', 'aria-live': 'polite', style: { color: colors.muted, fontSize: 12 } }, stepReadingNotice) : null,
881
+ workflowDetailError ? React.createElement('div', { role: 'alert', style: { marginTop: 12, color: colors.danger, fontSize: 12 } }, `当前详情刷新失败,以下内容尚未刷新:${workflowDetailError}`, React.createElement(Button, { variant: 'outline', size: 'sm', type: 'button', onClick: () => setWorkflowDetailRetry(value => value + 1) }, '重试读取详情')) : null,
882
+ React.createElement('header', { className: 'observer-task-overview', style: { borderBottom: `1px solid ${colors.border}`, marginBottom: 16 } },
883
+ React.createElement('div', { style: { display: 'flex', alignItems: 'center', flexWrap: 'wrap', gap: 12, marginBottom: 12, fontSize: 12 } },
884
+ React.createElement('span', { style: { ...pill(taskTone), fontWeight: 600 } }, taskStatus),
885
+ React.createElement('span', { style: { color: colors.muted } }, `最近更新 ${fmt(selectedWorkflowTask.updatedAt)}`)),
886
+ React.createElement('h1', { title: selectedWorkflowTask.title || selectedWorkflowTask.objective, style: { margin: 0, fontSize: 22, fontWeight: 650, lineHeight: 1.45, letterSpacing: '-.02em', maxWidth: 900 } }, taskDisplayTitle(selectedWorkflowTask)),
887
+ React.createElement('div', { style: { marginTop: 16, display: 'flex', alignItems: 'center', flexWrap: 'wrap', gap: '12px 24px', fontSize: 12 } },
888
+ React.createElement('span', { style: { color: colors.muted, fontVariantNumeric: 'tabular-nums' } }, taskNodes.length ? `已完成 ${completedSteps} / ${taskNodes.length} 个步骤` : stepsResolved ? '暂无步骤记录' : '步骤待确定'),
889
+ activeStep ? React.createElement('span', { style: { color: stepColor(activeStep.status) } }, `当前 · ${activeStep.title || nodeTitle[activeStep.nodeId] || '执行步骤'}`) : null,
890
+ React.createElement(TaskTotalElapsed, { key: selectedWorkflowTask.taskId, task: selectedWorkflowTask }))),
891
+ selectedWorkflowTask.objective ? React.createElement('details', { style: { margin: '0 0 16px', color: colors.muted, fontSize: 14, lineHeight: 1.7 } },
892
+ React.createElement('summary', { style: { cursor: 'pointer' } }, '任务目标'),
893
+ React.createElement('p', { style: { margin: '8px 0 0', whiteSpace: 'pre-wrap' } }, selectedWorkflowTask.objective)) : null,
894
+ selectedWorkflowTask.waitingReason ? React.createElement('div', { role: 'status', style: { marginBottom: 16, padding: '14px 18px', borderLeft: `3px solid ${colors.warning}`, background: colors.surface2, fontSize: 14, lineHeight: 1.7 } }, traceReason(selectedWorkflowTask.waitingReason)) : null,
895
+ selectedWorkflowTask.investigationRequest ? React.createElement(WorkflowClarification, { key: selectedWorkflowTask.investigationRequest.requestId, request: selectedWorkflowTask.investigationRequest, onSubmitted: manualRefresh }) : null,
896
+ selectedWorkflowTask.plan?.stages?.length ? React.createElement('div', { 'aria-label': '任务阶段', style: { display: 'flex', flexWrap: 'wrap', gap: '8px 24px', marginBottom: 16, fontSize: 12, color: colors.muted } }, ...selectedWorkflowTask.plan.stages.map((stage, index) => React.createElement('span', { key: stage.stageId }, `${String(index + 1).padStart(2, '0')} · ${stage.title || '任务阶段'} · ${planStageState[stage.status] || '状态未记录'}`))) : null,
897
+ React.createElement('section', { 'aria-label': '执行步骤' },
898
+ React.createElement('div', { style: { display: 'flex', justifyContent: 'space-between', alignItems: 'baseline', gap: 16, marginBottom: 12 } },
899
+ React.createElement('h2', { style: { margin: 0, fontSize: 16, fontWeight: 650 }, tabIndex: -1 }, '执行步骤'),
900
+ taskNodes.length || !stepsResolved ? React.createElement('span', { style: { fontSize: 12, color: colors.muted, fontVariantNumeric: 'tabular-nums' } }, stepsResolved ? `步骤完成 ${stepProgress}%` : '步骤待确定') : null),
901
+ taskNodes.length ? React.createElement('div', { role: 'progressbar', 'aria-label': '步骤完成情况', 'aria-valuemin': 0, 'aria-valuemax': 100, 'aria-valuenow': stepProgress, 'aria-valuetext': stepsResolved ? `步骤完成 ${stepProgress}%` : '步骤待确定', style: { height: 3, background: colors.border, marginBottom: 24 } }, React.createElement('div', { style: { width: `${stepProgress}%`, height: '100%', background: taskTone } })) : null,
902
+ !taskNodes.length ? React.createElement('p', { style: { padding: '20px 0', color: colors.muted, fontSize: 14 } }, stepsResolved ? '暂无执行步骤记录' : '该阶段步骤待确定') : null,
903
+ ...groupTaskSteps(taskNodes).map((group, groupIndex) => React.createElement('section', {
904
+ key: group.key, className: 'observer-task-workflow', 'aria-label': `工作流 ${group.title}`,
905
+ style: { marginTop: 16, minWidth: 0 } },
906
+ React.createElement('h3', { style: { margin: '0 0 14px', padding: '10px 0', borderBottom: `1px solid ${colors.border}`, fontSize: 14, fontWeight: 600 } },
907
+ `工作流 ${groupIndex + 1} · ${group.title}`, React.createElement('span', { style: { marginLeft: 12, color: colors.muted, fontSize: 12, fontWeight: 400 } }, `${group.steps.length} 个步骤`)),
908
+ React.createElement('ol', { style: { margin: 0, padding: 0, listStyle: 'none' } }, ...group.steps.map(({ node, index }) => {
909
+ const tone = stepColor(node.status)
910
+ return React.createElement('li', { key: node.stepKey, 'data-step-key': node.stepKey, tabIndex: -1, className: 'observer-task-step', 'aria-current': node.status === 'running' ? 'step' : undefined },
911
+ React.createElement('span', { 'aria-label': `步骤 ${index + 1}`, style: { display: 'grid', placeItems: 'center', width: 28, height: 28, boxSizing: 'border-box', borderRadius: '50%', border: `${node.status === 'running' ? 2 : 1}px solid ${tone}`, background: colors.surface, color: tone, fontSize: 12, fontWeight: 600, fontVariantNumeric: 'tabular-nums' } }, String(index + 1).padStart(2, '0')),
912
+ React.createElement('div', { style: { minWidth: 0 } },
913
+ React.createElement('div', { className: 'observer-task-step-heading' },
914
+ React.createElement('div', { style: { display: 'flex', alignItems: 'baseline', flexWrap: 'wrap', gap: '4px 10px' } },
915
+ React.createElement('strong', { style: { fontSize: 14, lineHeight: 1.6 } }, node.title || nodeTitle[node.nodeId] || '执行步骤'),
916
+ React.createElement('span', { style: { color: tone, fontSize: 12 } }, nodeState[node.status] || '状态未记录')),
917
+ React.createElement('div', { style: { marginLeft: 'auto', textAlign: 'right', fontVariantNumeric: 'tabular-nums' } }, React.createElement(TaskStepElapsed, { node }))),
918
+ React.createElement('span', { style: { display: 'block', marginBottom: 6, color: colors.muted, fontSize: 12, fontVariantNumeric: 'tabular-nums' } },
919
+ Number.isFinite(Date.parse(node.startedAt)) ? React.createElement('time', { dateTime: node.startedAt }, taskStepStart(node)) : taskStepStart(node)),
920
+ node.waitReason?.reference ? React.createElement('p', { style: { margin: '0 0 6px', fontSize: 13, lineHeight: 1.7, color: colors.warning } }, traceReason(node.waitReason.reference)) : null,
921
+ node.outputRef ? React.createElement(TaskStepOutput, { key: `${node.nodeRunId}:${node.outputRef}`, taskId: selectedWorkflowTask.taskId, node, detailRevision: selectedWorkflowTask.detailRevision }) : React.createElement('span', { style: { color: colors.muted, fontSize: 13 } }, node.definitionPending ? '该阶段步骤待确定' : ['pending', 'ready', 'blocked'].includes(node.status) ? '执行后将在这里展示产出' : '暂未记录产出'),
922
+ node.sessionId ? React.createElement('button', { type: 'button', onClick: () => navigate(node.sessionId), style: { display: 'block', marginTop: 6, padding: 0, border: 0, color: colors.accent, background: 'transparent', font: 'inherit', fontSize: 12, cursor: 'pointer' } }, '查看会话记录') : null))
923
+ }))))),
924
+ React.createElement('section', { 'aria-label': '当前结果', style: { marginTop: 24, padding: '24px 0', borderTop: `1px solid ${colors.border}` } },
925
+ React.createElement('h2', { style: { margin: '0 0 12px', fontSize: 16, fontWeight: 650 } }, '当前结果'),
926
+ React.createElement('div', { style: { fontSize: 14, lineHeight: 1.8, whiteSpace: 'pre-wrap', overflowWrap: 'anywhere' } }, ...linkedTaskText(selectedWorkflowTask.result || '暂未记录产出')),
927
+ selectedWorkflowTask.result ? React.createElement('p', { style: { margin: '16px 0 0', color: colors.muted, fontSize: 12 } }, '送达情况请查看发件记录。') : null))
928
+ : null
929
+ const workflowDetailPending = selectedWorkflowTaskId && !selectedWorkflowTask ? React.createElement('section', { 'aria-label': '任务详情读取', style: { display: 'grid', gap: 12 } }, React.createElement(Button, { variant: 'outline', size: 'sm', type: 'button', onClick: () => setSelectedWorkflowTaskId('') }, '返回看板'), workflowDetailError ? React.createElement('div', { role: 'alert', style: { color: colors.danger } }, `任务详情读取失败:${workflowDetailError}`, React.createElement(Button, { variant: 'outline', size: 'sm', type: 'button', onClick: () => setWorkflowDetailRetry(value => value + 1) }, '重试读取详情')) : React.createElement('span', { role: 'status' }, '正在读取当前任务详情…')) : null
930
+ const tasksPage = workflowDetailPending || workflowDetail || React.createElement('div', { className: 'observer-task-board', style: { display: 'grid', gridTemplateColumns: 'repeat(4, minmax(300px, 1fr))', gap: ui.space3, overflowX: 'auto', alignItems: 'start', paddingBottom: ui.space2 } }, ...bucketColumns)
441
931
  const authorizationStatus = {
442
932
  'pending-send': { label: '待发送', state: 'warning' },
443
933
  'waiting-reply': { label: '等待处理', state: 'warning' },
444
934
  answered: { label: '已处理', state: 'done' },
935
+ superseded: { label: '已失效', state: 'neutral' },
445
936
  }
446
937
  const authorizationDecision = { approved: { label: '已继续', state: 'done' }, rejected: { label: '不执行', state: 'error' } }
938
+ const isPendingAuthorization = (item) => item.status === 'pending-send' || item.status === 'waiting-reply'
939
+ const authorizationPresentation = (item) => item.status === 'answered'
940
+ ? authorizationDecision[item.decision] || authorizationStatus.answered
941
+ : authorizationStatus[item.status] || { label: '状态异常', state: 'error' }
447
942
  const decideAuthorization = async (requestId, decision) => {
448
943
  try {
449
944
  setNavigationError(undefined); setDecidingAuthorizationId(requestId)
@@ -453,21 +948,21 @@ window.__ModuleLoader__.load({
453
948
  finally { setDecidingAuthorizationId('') }
454
949
  }
455
950
  const authorizationItems = [...(data?.authorizations || [])].sort((left, right) => {
456
- const leftPending = left.status === 'answered' ? 0 : 1; const rightPending = right.status === 'answered' ? 0 : 1
951
+ const leftPending = isPendingAuthorization(left) ? 1 : 0; const rightPending = isPendingAuthorization(right) ? 1 : 0
457
952
  return rightPending - leftPending || String(right.createdAt || '').localeCompare(String(left.createdAt || ''))
458
953
  })
459
- const filteredAuthorizationItems = authorizationItems.filter((item) => authorizationFilter === 'all' || (authorizationFilter === 'pending' ? item.status !== 'answered' : item.decision === authorizationFilter))
954
+ const filteredAuthorizationItems = authorizationItems.filter((item) => authorizationFilter === 'all' || (authorizationFilter === 'pending' ? isPendingAuthorization(item) : authorizationFilter === 'superseded' ? item.status === 'superseded' : item.decision === authorizationFilter))
460
955
  const authorizationPageSize = 10
461
956
  const authorizationPageCount = Math.max(1, Math.ceil(filteredAuthorizationItems.length / authorizationPageSize))
462
957
  const currentAuthorizationPage = Math.min(authorizationPage, authorizationPageCount)
463
958
  const visibleAuthorizationItems = filteredAuthorizationItems.slice((currentAuthorizationPage - 1) * authorizationPageSize, currentAuthorizationPage * authorizationPageSize)
464
959
  const selectedAuthorization = authorizationItems.find((item) => item.requestId === selectedAuthorizationId)
960
+ const selectedAuthorizationPending = selectedAuthorization ? isPendingAuthorization(selectedAuthorization) : false
465
961
  const authorizationDetailField = (label, value) => React.createElement('div', { style: { display: 'grid', gap: 5 } },
466
962
  React.createElement('strong', { style: { fontSize: 11, color: colors.muted } }, label),
467
963
  React.createElement('div', { style: { fontSize: 13, lineHeight: 1.65, whiteSpace: 'pre-wrap', overflowWrap: 'anywhere' } }, value || '—'))
468
964
  const authorizationRows = visibleAuthorizationItems.map((item, rowIndex) => {
469
- const pending = item.status !== 'answered'
470
- const status = pending ? authorizationStatus[item.status] : authorizationDecision[item.decision] || authorizationStatus.answered
965
+ const status = authorizationPresentation(item)
471
966
  const group = groupsById.get(item.groupId)
472
967
  return React.createElement('div', { key: item.requestId, style: { width: '100%', minWidth: 0, boxSizing: 'border-box', display: 'grid', gridTemplateColumns: '104px minmax(220px, 1.1fr) minmax(280px, 1.5fr) 126px 52px', alignItems: 'center', gap: 16, borderTop: `1px solid ${colors.border}`, background: rowIndex % 2 ? `color-mix(in srgb, ${colors.surface2} 55%, transparent)` : colors.cardSurface, color: 'inherit', padding: '12px 16px', fontSize: 14 } },
473
968
  React.createElement('div', { style: { justifySelf: 'start' } }, tableStatusTag(status.label, status.state, { fontWeight: 600 })),
@@ -479,7 +974,7 @@ window.__ModuleLoader__.load({
479
974
  const authorizationDetail = selectedAuthorization ? React.createElement('div', { role: 'presentation', onMouseDown: (event) => { if (event.target === event.currentTarget) setSelectedAuthorizationId('') }, style: { position: 'fixed', inset: 0, zIndex: 1100, display: 'flex', justifyContent: 'flex-end', background: 'rgba(0,0,0,.28)', backdropFilter: 'blur(2px)' } },
480
975
  React.createElement('section', { role: 'dialog', 'aria-modal': true, 'aria-label': '人工介入事项详情', style: { width: 'min(640px, 100%)', height: '100%', display: 'flex', flexDirection: 'column', overflow: 'hidden', borderLeft: `1px solid ${colors.border}`, background: colors.surface, boxShadow: '-20px 0 60px rgba(0,0,0,.2)' } },
481
976
  React.createElement('header', { style: { flex: '0 0 auto', display: 'flex', alignItems: 'flex-start', justifyContent: 'space-between', gap: 24, padding: '24px 24px 20px', borderBottom: `1px solid ${colors.border}`, background: colors.surface } },
482
- React.createElement('div', { style: { minWidth: 0 } }, React.createElement('div', { style: { display: 'flex', alignItems: 'center', gap: 10, marginBottom: 10 } }, statusTag(selectedAuthorization.status === 'answered' ? (authorizationDecision[selectedAuthorization.decision]?.label || '已处理') : (authorizationStatus[selectedAuthorization.status]?.label || '等待处理'), selectedAuthorization.status === 'answered' ? (authorizationDecision[selectedAuthorization.decision]?.state || 'done') : 'warning'), React.createElement('span', { style: { color: colors.muted, fontSize: 11.5 } }, fmt(selectedAuthorization.createdAt))), React.createElement('h3', { style: { margin: 0, fontSize: 18, fontWeight: 600, lineHeight: 1.5 } }, selectedAuthorization.objective || '人工介入事项'), React.createElement('code', { style: { display: 'block', marginTop: 10, color: colors.muted, fontSize: 10.5 } }, selectedAuthorization.requestId)),
977
+ React.createElement('div', { style: { minWidth: 0 } }, React.createElement('div', { style: { display: 'flex', alignItems: 'center', gap: 10, marginBottom: 10 } }, statusTag(authorizationPresentation(selectedAuthorization).label, authorizationPresentation(selectedAuthorization).state), React.createElement('span', { style: { color: colors.muted, fontSize: 11.5 } }, fmt(selectedAuthorization.createdAt))), React.createElement('h3', { style: { margin: 0, fontSize: 18, fontWeight: 600, lineHeight: 1.5 } }, selectedAuthorization.objective || '人工介入事项'), React.createElement('code', { style: { display: 'block', marginTop: 10, color: colors.muted, fontSize: 10.5 } }, selectedAuthorization.requestId)),
483
978
  React.createElement(Button, { variant: 'ghost', size: 'sm', type: 'button', 'aria-label': '关闭人工介入事项', onClick: () => setSelectedAuthorizationId(''), style: { flex: '0 0 auto', minWidth: 52, whiteSpace: 'nowrap' } }, '关闭')),
484
979
  React.createElement('div', { style: { flex: '1 1 auto', overflowY: 'auto', padding: '0 24px 24px' } },
485
980
  React.createElement('section', { style: { padding: '24px 0', borderBottom: `1px solid ${colors.border}` } }, authorizationDetailField('需要人工处理', selectedAuthorization.requestedAction)),
@@ -489,13 +984,13 @@ window.__ModuleLoader__.load({
489
984
  React.createElement('details', { style: { padding: '18px 0', borderBottom: `1px solid ${colors.border}` } }, React.createElement('summary', { style: { cursor: 'pointer', fontSize: 12, fontWeight: 600 } }, `已尝试 · ${(selectedAuthorization.attemptedActions || []).length}`), React.createElement('div', { style: { marginTop: 14 } }, authorizationDetailField('', (selectedAuthorization.attemptedActions || []).map((value, index) => `${index + 1}. ${value}`).join('\n')))),
490
985
  React.createElement('section', { style: { padding: '20px 0', borderBottom: `1px solid ${colors.border}` } }, authorizationDetailField('关联信息', `Task ID:${selectedAuthorization.taskId || '—'}\n群聊:${groupsById.get(selectedAuthorization.groupId)?.name || selectedAuthorization.groupId || '—'}`)),
491
986
  selectedAuthorization.status === 'answered' ? React.createElement('section', { style: { padding: '20px 0' } }, authorizationDetailField('处理结果', `${selectedAuthorization.decision === 'approved' ? '继续任务' : selectedAuthorization.decision === 'rejected' ? '不执行' : selectedAuthorization.decision || '已处理'}\n处理渠道:${selectedAuthorization.decisionSource === 'web' ? '运行看板' : selectedAuthorization.decisionSource === 'dingtalk' ? '钉钉私聊' : selectedAuthorization.decisionSource || '历史迁移'}\n处理时间:${fmt(selectedAuthorization.decidedAt)}\n处理意见:${selectedAuthorization.reply || '—'}`)) : null),
492
- selectedAuthorization.status !== 'answered' ? React.createElement('footer', { style: { flex: '0 0 auto', display: 'grid', gridTemplateColumns: '1fr auto auto', gap: 10, alignItems: 'center', padding: '16px 24px', borderTop: `1px solid ${colors.border}`, background: colors.surface2 } },
493
- React.createElement('input', { 'aria-label': `处理意见 ${selectedAuthorization.requestId}`, value: authorizationComments[selectedAuthorization.requestId] || '', onChange: (event) => setAuthorizationComments((current) => ({ ...current, [selectedAuthorization.requestId]: event.target.value })), placeholder: '处理意见(可选)', style: { minWidth: 0, border: `1px solid ${colors.border}`, borderRadius: 9, background: colors.surface2, color: 'inherit', padding: '9px 11px', fontFamily: 'inherit', fontSize: 12 } }),
987
+ selectedAuthorizationPending ? React.createElement('footer', { style: { flex: '0 0 auto', display: 'grid', gridTemplateColumns: '1fr auto auto', gap: 10, alignItems: 'center', padding: '16px 24px', borderTop: `1px solid ${colors.border}`, background: colors.surface2 } },
988
+ React.createElement('input', { 'aria-label': `执行限制或拒绝原因 ${selectedAuthorization.requestId}`, value: authorizationComments[selectedAuthorization.requestId] || '', onChange: (event) => setAuthorizationComments((current) => ({ ...current, [selectedAuthorization.requestId]: event.target.value })), placeholder: '执行限制或拒绝原因(可选)', style: { minWidth: 0, border: `1px solid ${colors.border}`, borderRadius: 9, background: colors.surface2, color: 'inherit', padding: '9px 11px', fontFamily: 'inherit', fontSize: 12 } }),
494
989
  React.createElement(Button, { variant: 'outline', size: 'sm', type: 'button', disabled: decidingAuthorizationId === selectedAuthorization.requestId, onClick: () => decideAuthorization(selectedAuthorization.requestId, 'rejected'), style: { color: colors.danger } }, '不执行'),
495
- React.createElement(Button, { variant: 'primary', size: 'sm', type: 'button', disabled: decidingAuthorizationId === selectedAuthorization.requestId, onClick: () => decideAuthorization(selectedAuthorization.requestId, 'approved') }, decidingAuthorizationId === selectedAuthorization.requestId ? '处理中…' : '继续任务')) : null)) : null
990
+ React.createElement(Button, { variant: 'primary', size: 'sm', type: 'button', disabled: decidingAuthorizationId === selectedAuthorization.requestId, onClick: () => decideAuthorization(selectedAuthorization.requestId, 'approved') }, decidingAuthorizationId === selectedAuthorization.requestId ? '处理中…' : '批准该事项并继续')) : null)) : null
496
991
  const authorizationsPage = React.createElement(React.Fragment, null, React.createElement('section', null,
497
992
  React.createElement('div', { style: tableFrame },
498
- React.createElement('div', { style: { ...toolbar, justifyContent: 'flex-end' } }, React.createElement(SelectMenu, { label: '筛选处理状态', value: authorizationFilter, options: [{ id: 'all', label: '全部处理状态' }, { id: 'pending', label: '等待处理' }, { id: 'approved', label: '已继续' }, { id: 'rejected', label: '不执行' }], onChange: (value) => { setAuthorizationFilter(value); setAuthorizationPage(1) }, fitContent: true })),
993
+ React.createElement('div', { style: { ...toolbar, justifyContent: 'flex-end' } }, React.createElement(SelectMenu, { label: '筛选处理状态', value: authorizationFilter, options: [{ id: 'all', label: '全部处理状态' }, { id: 'pending', label: '等待处理' }, { id: 'approved', label: '已继续' }, { id: 'rejected', label: '不执行' }, { id: 'superseded', label: '已失效' }], onChange: (value) => { setAuthorizationFilter(value); setAuthorizationPage(1) }, fitContent: true })),
499
994
  React.createElement('div', { style: { height: 42, boxSizing: 'border-box', display: 'grid', gridTemplateColumns: '104px minmax(220px, 1.1fr) minmax(280px, 1.5fr) 126px 52px', alignItems: 'center', gap: 16, padding: '0 16px', background: colors.surface2, color: colors.muted, fontSize: 13, fontWeight: 600 } }, React.createElement('span', null, '状态'), React.createElement('span', null, '任务'), React.createElement('span', null, '阻塞事项'), React.createElement('span', null, '提出时间'), React.createElement('span', null, '操作')),
500
995
  React.createElement('div', null, ...(authorizationRows.length ? authorizationRows : [React.createElement('div', { key: 'empty', style: { padding: 36, textAlign: 'center', color: colors.muted, fontSize: 12 } }, '暂无人工介入事项')])),
501
996
  React.createElement('div', { style: tableFooter },
@@ -514,15 +1009,15 @@ window.__ModuleLoader__.load({
514
1009
  React.createElement('div', { style: { padding: '0 16px' } }, ...(alertView === 'active' ? (filteredActiveAlerts.length ? filteredActiveAlerts.map(renderAlert) : [React.createElement('div', { key: 'empty', style: { padding: '36px 0', textAlign: 'center', color: colors.muted, fontSize: 12 } }, '当前没有此类型异常')]) : (visibleResolvedAlerts.length ? visibleResolvedAlerts.map(renderAlert) : [React.createElement('div', { key: 'empty', style: { padding: '36px 0', textAlign: 'center', color: colors.muted, fontSize: 12 } }, '暂无此类型恢复记录')]))),
515
1010
  alertView === 'resolved' ? React.createElement('div', { style: tableFooter }, React.createElement('span', { style: { marginRight: 'auto', fontSize: 11, color: colors.muted } }, `${filteredResolvedAlerts.length} 条 · 每页 ${resolvedAlertPageSize} 条`), React.createElement(Button, { variant: 'outline', size: 'sm', type: 'button', disabled: currentResolvedAlertPage <= 1, onClick: () => setResolvedAlertPage((page) => Math.max(1, page - 1)) }, '上一页'), React.createElement('span', { style: { fontSize: 11, color: colors.muted } }, `${currentResolvedAlertPage} / ${resolvedAlertPageCount}`), React.createElement(Button, { variant: 'outline', size: 'sm', type: 'button', disabled: currentResolvedAlertPage >= resolvedAlertPageCount, onClick: () => setResolvedAlertPage((page) => Math.min(resolvedAlertPageCount, page + 1)) }, '下一页')) : null))
516
1011
  )
517
- const topicsPage = React.createElement(TopicBrowser, { groups: data?.groups || [], tasks: data?.tasks || [], target: topicTarget, updatedAt, onOpenTask: (task) => navigate(task.childSessionId, groupsById.get(task.groupId)?.residentSessionId) })
1012
+ const topicsPage = React.createElement(TopicBrowser, { groups: data?.groups || [], tasks: data?.tasks || [], target: topicTarget, updatedAt, onOpenTask: openTask, onOpenSession: navigate })
518
1013
  const pageContent = activePage === 'topics' ? topicsPage : activePage === 'tasks' ? tasksPage : activePage === 'authorizations' ? authorizationsPage : activePage === 'archive' ? archivePage : activePage === 'alerts' ? alertsPage : groupsPage
519
1014
  const pageViewport = React.createElement('div', { style: { width: '100%', maxWidth: 1320, minHeight: 'calc(100dvh - 138px)', boxSizing: 'border-box', margin: '0 auto' } }, pageContent)
520
- const main = React.createElement('main', { style: { width: '100%', minHeight: 'calc(100dvh - 90px)', boxSizing: 'border-box', padding: activePage === 'tasks' ? '24px 24px 0' : '24px 24px 32px', display: 'grid', alignContent: 'start', gap: 20, pointerEvents: 'auto' } },
1015
+ const main = React.createElement('main', { className: 'observer-main', style: { width: '100%', minHeight: 'calc(100dvh - 90px)', boxSizing: 'border-box', padding: activePage === 'tasks' ? `${ui.space6}px ${ui.space6}px 0` : `${ui.space6}px ${ui.space6}px ${ui.space6 + ui.space2}px`, display: 'grid', alignContent: 'start', gap: ui.space6, pointerEvents: 'auto' } },
521
1016
  error ? React.createElement('div', { style: { ...card, borderColor: colors.danger, color: colors.danger } }, `无法连接 resident 插件:${error}`) : null,
522
- navigationError ? React.createElement('div', { style: { ...card, borderColor: colors.danger, color: colors.danger } }, `无法打开 DSH Session:${navigationError}`) : null,
1017
+ navigationError ? React.createElement('div', { role: 'alert', style: { ...card, borderColor: colors.danger, color: colors.danger } }, `操作失败:${navigationError}`) : null,
523
1018
  pageViewport
524
1019
  )
525
- return React.createElement('div', { style: { width: '100%', height: '100%', minWidth: 0, minHeight: 0, background: colors.surface, color: 'inherit', overflow: 'auto' } }, header, main)
1020
+ return React.createElement('div', { 'data-dingtalk-observer': '', style: { width: '100%', height: '100%', minWidth: 0, minHeight: 0, background: colors.surface, color: 'inherit', overflow: 'auto' } }, React.createElement('style', null, observerCss), header, main)
526
1021
  }
527
1022
  function apply(ctx) {
528
1023
  ctx.slots.inject('sidebar.footer.action', () => ctx.slots.register({ name: 'sidebar.footer.action', id: 'dingtalk-dsh-observer-entry', order: 5, inject: () => ({}) }, SidebarAction))