@zzusp/dingtalk-dsh-observer 0.5.15 → 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 +498 -35
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zzusp/dingtalk-dsh-observer",
3
- "version": "0.5.15",
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)',
@@ -80,7 +80,16 @@ window.__ModuleLoader__.load({
80
80
  outline: 2px solid var(--dsw-alias-brand-primary, #4d6bfe) !important;
81
81
  outline-offset: 2px !important;
82
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; }
83
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; }
84
93
  [data-dingtalk-observer] .observer-main { padding: 16px !important; }
85
94
  [data-dingtalk-observer] .observer-topics-layout { display: grid !important; grid-template-columns: minmax(280px, 2fr) minmax(0, 3fr); }
86
95
  [data-dingtalk-observer] .observer-topic-list { max-width: none !important; }
@@ -162,16 +171,220 @@ window.__ModuleLoader__.load({
162
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))' },
163
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))' },
164
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))' },
165
- { 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))' }
166
175
  ]
167
- const pages = [{ id: 'groups', label: '群聊会话' }, { id: 'topics', label: '话题' }, { id: 'tasks', label: '任务看板' }, { id: 'authorizations', label: '人工介入' }, { id: 'archive', label: '归档任务' }, { id: 'alerts', label: '告警' }]
168
- 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 }) {
169
373
  const [groupId, setGroupId] = useState(target?.groupId || '')
170
374
  const [offset, setOffset] = useState(0)
171
375
  const [selection, setSelection] = useState(target)
172
376
  const [messageOffset, setMessageOffset] = useState(0)
173
377
  const [listing, setListing] = useState()
174
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)
175
388
  const [listError, setListError] = useState('')
176
389
  const [detailError, setDetailError] = useState('')
177
390
  const [loading, setLoading] = useState(true)
@@ -179,6 +392,15 @@ window.__ModuleLoader__.load({
179
392
  const [retry, setRetry] = useState(0)
180
393
  const topicPageSize = 10
181
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])
182
404
  useEffect(() => {
183
405
  let active = true
184
406
  setLoading(true); setListError('')
@@ -195,10 +417,22 @@ window.__ModuleLoader__.load({
195
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) })
196
418
  return () => { active = false }
197
419
  }, [selection, messageOffset, retry])
198
- 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: '返回话题判断列表' })
199
433
  const topic = context?.topic
200
434
  const group = groups.find((item) => item.groupId === groupId)
201
- 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))
202
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) }, '重试'))
203
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) }, '下一页'))
204
438
  return React.createElement('section', { 'aria-label': '话题与上下文', style: { display: 'grid', gap: ui.space4 } },
@@ -215,15 +449,80 @@ window.__ModuleLoader__.load({
215
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 || '话题详情')),
216
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' } },
217
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 } }, '历史未记录')),
218
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}` } },
219
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 } }, '当前没有待解决问题')),
220
- 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))),
221
465
  React.createElement('section', { 'aria-label': '群聊消息', style: { marginTop: 4 } },
222
466
  React.createElement('strong', { style: { display: 'block', padding: '14px 0', fontSize: 13, fontWeight: 600 } }, `群聊消息 · ${context.total} 条`),
223
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('此版本没有消息')])),
224
468
  pager(messageOffset, context.total, messagePageSize, setMessageOffset, detailLoading))
225
469
  ))))
226
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
+ }
227
526
  function ObserverContent({ openSession }) {
228
527
  const [activePage, setActivePage] = useState('groups')
229
528
  const [topicTarget, setTopicTarget] = useState()
@@ -240,8 +539,17 @@ window.__ModuleLoader__.load({
240
539
  const [outboxStatusFilter, setOutboxStatusFilter] = useState('all')
241
540
  const [hoveredTaskId, setHoveredTaskId] = useState('')
242
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('')
243
549
  const [copiedId, setCopiedId] = useState('')
244
550
  const [navigatingSessionId, setNavigatingSessionId] = useState('')
551
+ const [archivingTaskId, setArchivingTaskId] = useState('')
552
+ const [archiveDetailTaskId, setArchiveDetailTaskId] = useState('')
245
553
  const [alertType, setAlertType] = useState('all')
246
554
  const [alertView, setAlertView] = useState('active')
247
555
  const [resolvedAlertPage, setResolvedAlertPage] = useState(1)
@@ -261,6 +569,30 @@ window.__ModuleLoader__.load({
261
569
  setRefreshState('done')
262
570
  window.setTimeout(() => setRefreshState('idle'), 1200)
263
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])
264
596
  useEffect(() => {
265
597
  refresh()
266
598
  const timer = window.setInterval(refresh, 5000)
@@ -280,6 +612,10 @@ window.__ModuleLoader__.load({
280
612
  catch (cause) { setNavigationError(cause instanceof Error ? cause.message : String(cause)) }
281
613
  finally { setNavigatingSessionId('') }
282
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
+ }
283
619
  const copyId = async (value) => {
284
620
  let copied = false
285
621
  try {
@@ -302,10 +638,12 @@ window.__ModuleLoader__.load({
302
638
  }
303
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}`)
304
640
  const selectedGroup = groupsById.get(selectedGroupId) || (data?.groups || [])[0]
305
- 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))
306
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
307
644
  if (message.routingStatus === 'failed') return 'failed'
308
645
  if (message.routingStatus !== 'routed') return 'routing'
646
+ if (message.sourceKind === 'workflow-v2') return 'processed'
309
647
  const related = (selectedGroup?.topics || []).flatMap((topic) => {
310
648
  const entry = [...(topic.entries || [])].reverse().find((item) => item.messageId === message.messageId)
311
649
  return entry?.action === 'add' && entry.messageVersion === message.messageVersion ? [{ topic, entry }] : []
@@ -320,6 +658,12 @@ window.__ModuleLoader__.load({
320
658
  const visibleMessages = filteredMessages.slice((currentMessagePage - 1) * pageSize, currentMessagePage * pageSize)
321
659
  const delivery = {
322
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' },
323
667
  processing: { label: '话题处理中', state: 'ongoing' },
324
668
  processed: { label: '已处理', state: 'done' },
325
669
  failed: { label: '归类失败', state: 'error' },
@@ -327,11 +671,11 @@ window.__ModuleLoader__.load({
327
671
  const messageRows = visibleMessages.map((message, rowIndex) => {
328
672
  const status = delivery[messageWorkflowState(message)]
329
673
  return React.createElement('tr', { key: message.messageId, style: { background: rowIndex % 2 ? `color-mix(in srgb, ${colors.surface2} 55%, transparent)` : colors.cardSurface } },
330
- 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 }))),
331
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)))),
332
- 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' ? '—' : '待归类')]))),
333
677
  React.createElement('td', { title: message.text, style: tableBodyCell }, clampTableContent(message.text || '(空消息)')),
334
- 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' } }, '历史未记录处理过程')))
335
679
  })
336
680
  const selectedOutbox = [...(selectedGroup?.outbox || [])].reverse()
337
681
  const outboxState = (message) => outboxDelivery(message).id
@@ -354,42 +698,95 @@ window.__ModuleLoader__.load({
354
698
  })
355
699
  const renderTaskCard = (task) => {
356
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 ? '群通知待确认送达' : '群通知尚未生成'
357
708
  const queued = task.state === 'queued'
358
709
  const checkpointEvents = task.checkpoints || []
359
710
  const planCheckpointIndex = checkpointEvents.findLastIndex((checkpoint) => checkpoint.kind === 'plan-confirmed')
360
711
  const planCheckpoint = checkpointEvents[planCheckpointIndex]
361
712
  const currentCheckpointEvents = checkpointEvents.slice(Math.max(0, planCheckpointIndex))
362
- 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) || '执行步骤'
363
716
  const displayObjective = String(task.objective || '').startsWith('[TASK_SOURCE_EVIDENCE]') ? (task.title || task.objective) : task.objective
364
- const latestCheckpoint = checkpointEvents.at(-1)
365
- const latestRemainingItems = latestCheckpoint?.remainingItems || checkpoints
366
- const remainingItemsMatchPlan = latestRemainingItems.every((checkpoint) => checkpoints.includes(checkpoint))
367
- const normalizedRemainingItems = remainingItemsMatchPlan ? latestRemainingItems : checkpoints.slice(-Math.min(checkpoints.length, latestRemainingItems.length))
368
- const remainingCheckpointNames = new Set(normalizedRemainingItems)
369
- const completedCheckpointNames = new Set(task.state === 'completed' ? checkpoints : checkpoints.filter((checkpoint) => !remainingCheckpointNames.has(checkpoint)))
370
- 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
371
719
  const completedCheckpointCount = checkpoints.filter((checkpoint) => completedCheckpointNames.has(checkpoint)).length
372
- const progress = Math.round((completedCheckpointCount / checkpoints.length) * 100)
373
- 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
374
722
  const checkpointsExpanded = expandedCheckpointTaskId === task.taskId
375
723
  const hovered = hoveredTaskId === task.taskId
376
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
377
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' } },
378
- 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' } },
379
- 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)),
380
770
  React.createElement('div', { style: { display: 'flex', alignItems: 'center', gap: 5, minWidth: 0, overflow: 'hidden' } },
381
- 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),
382
- ...(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) })),
383
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),
384
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,
385
778
  showCheckpoints ? React.createElement('section', null,
386
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 } })),
387
- 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,
388
- 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,
389
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 } },
390
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)),
391
- 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),
392
- 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)
393
790
  }
394
791
  const alertCategories = [
395
792
  { id: 'all', label: '全部类型' },
@@ -449,9 +846,9 @@ window.__ModuleLoader__.load({
449
846
  const groupTableToolbar = React.createElement('div', { style: toolbar },
450
847
  dataViewTabs,
451
848
  React.createElement('div', { style: { display: 'flex', alignItems: 'center', justifyContent: 'flex-end', gap: 8, flexWrap: 'wrap' } },
452
- (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,
453
850
  groupTableView === 'messages'
454
- ? 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) } })
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) } })
455
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) } })))
456
853
  const messagesTable = React.createElement(React.Fragment, null,
457
854
  React.createElement('div', { style: { overflowX: 'auto' } }, React.createElement('table', { style: { width: '100%', minWidth: 910, borderCollapse: 'collapse', tableLayout: 'fixed' } },
@@ -463,8 +860,74 @@ window.__ModuleLoader__.load({
463
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'))),
464
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 发件记录'))])))),
465
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)) }, '下一页')))
466
- const groupsPage = React.createElement('section', null, React.createElement('div', { style: tableFrame }, groupTableToolbar, groupTableView === 'messages' ? messagesTable : outboxTable))
467
- const tasksPage = 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)
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)
468
931
  const authorizationStatus = {
469
932
  'pending-send': { label: '待发送', state: 'warning' },
470
933
  'waiting-reply': { label: '等待处理', state: 'warning' },
@@ -546,12 +1009,12 @@ window.__ModuleLoader__.load({
546
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 } }, '暂无此类型恢复记录')]))),
547
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))
548
1011
  )
549
- 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 })
550
1013
  const pageContent = activePage === 'topics' ? topicsPage : activePage === 'tasks' ? tasksPage : activePage === 'authorizations' ? authorizationsPage : activePage === 'archive' ? archivePage : activePage === 'alerts' ? alertsPage : groupsPage
551
1014
  const pageViewport = React.createElement('div', { style: { width: '100%', maxWidth: 1320, minHeight: 'calc(100dvh - 138px)', boxSizing: 'border-box', margin: '0 auto' } }, pageContent)
552
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' } },
553
1016
  error ? React.createElement('div', { style: { ...card, borderColor: colors.danger, color: colors.danger } }, `无法连接 resident 插件:${error}`) : null,
554
- 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,
555
1018
  pageViewport
556
1019
  )
557
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)