@zzusp/dingtalk-dsh-observer 1.0.0 → 2.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 +3 -2
  2. package/web-client.js +995 -365
package/web-client.js CHANGED
@@ -3,7 +3,7 @@ window.__ModuleLoader__.load({
3
3
  factory: (require) => {
4
4
  const module = { exports: {} }
5
5
  const React = require('react')
6
- const { Button, IconChecklistOutline14, IconChevronDownOutline14, IconChevronUpOutline14, Menu, Pill, StateDot } = require('@deepseek-ai/dsh-client-ui-primitives')
6
+ const { Button, IconChecklistOutlineRegular, IconChevronDownOutlineRegular, IconChevronUpOutlineRegular, Menu, Pill, StateDot } = require('@deepseek-ai/dsh-client-ui-primitives')
7
7
  const { useCallback, useEffect, useState, useRef } = React
8
8
  const ENDPOINT = 'http://127.0.0.1:18998'
9
9
  const colors = {
@@ -50,15 +50,36 @@ window.__ModuleLoader__.load({
50
50
  const fmt = (value) => value ? new Date(value).toLocaleString('zh-CN', { hour12: false }) : '—'
51
51
  const fmtTime = (value) => value ? new Date(value).toLocaleTimeString('zh-CN', { hour12: false }) : '—'
52
52
  const fmtDuration = (value) => { const seconds = Math.max(0, Math.floor(value / 1000)); if (seconds < 60) return `${seconds}秒`; const minutes = Math.floor(seconds / 60); if (minutes < 60) return `${minutes}分${seconds % 60}秒`; const hours = Math.floor(minutes / 60); return `${hours}小时${minutes % 60}分` }
53
- const checkpointDuration = (checkpoint, events, now) => { let startedAt; for (const event of events) { const submittedAt = Date.parse(event.submittedAt); const remainingItems = event.remainingItems || []; if (startedAt === undefined && remainingItems[0] === checkpoint && Number.isFinite(submittedAt)) startedAt = submittedAt; if (startedAt !== undefined && !remainingItems.includes(checkpoint) && Number.isFinite(submittedAt)) return fmtDuration(submittedAt - startedAt) } return startedAt === undefined ? '—' : fmtDuration(now - startedAt) }
54
53
  const short = (value) => value ? String(value).replace(/^session-/, '').slice(0, 14) : '—'
55
54
  const outboxDelivery = (message) => {
56
55
  if (message.recallStatus === 'recalled') return { id: 'recalled', label: '已撤回', state: 'neutral' }
57
- if (message.recallStatus === 'failed') return { id: 'recall-failed', label: '撤回待处理', state: 'warning', detail: message.recallError === 'replacement_delivery_unknown' ? '已停止旧意图发送;历史是否送达仍未知,等待核验' : `旧消息撤回失败${message.recallRetryAt ? ',将有限重试' : ',需核验后处理'}:${message.recallError || '原因未记录'}` }
56
+ if (message.recallStatus === 'failed') return { id: 'recall-failed', label: '撤回受阻', state: 'warning', detail: message.recallError === 'replacement_delivery_unknown' ? '已停止旧意图发送;历史是否送达仍未知,等待核验' : `旧消息撤回失败${message.recallRetryAt ? ',将有限重试' : ',需核验后处理'}:${message.recallError || '原因未记录'}` }
58
57
  if (message.supersededByOutboundId || message.status === 'superseded') return { id: 'superseded', label: '已替代', state: 'neutral', detail: message.deliveredMessageId ? '已记录送达,等待替换通知送达后撤回' : '已停止旧意图发送;未声称历史消息未送达' }
59
- if (message.status === 'sent') return { id: 'confirmed', label: message.deliveredMessageId ? '已回读' : '已发送', state: 'done' }
60
- if (message.deliveryBlockedAt) return { id: 'failed', label: '发送待处理', state: 'error', detail: '服务端已拒绝发送,停止自动重试;需核验失败原因' }
58
+ if (message.status === 'sent') return { id: 'confirmed', label: message.deliveredMessageId ? '已送达' : '已发送', state: 'done', hint: message.deliveredMessageId ? '回读已确认送达' : '历史发送记录,未记录回读消息 ID' }
61
59
  const reason = message.deliveryPendingReason
60
+ const notificationState = message.notificationState || ({
61
+ 'task-notification-prepared': 'prepared',
62
+ 'task-notification-starting': 'starting',
63
+ 'task-notification-unknown': 'unknown',
64
+ 'task-notification-failed': 'failed',
65
+ 'task-notification-pre-send-rejected': 'failed',
66
+ 'task-notification-task-terminal': 'failed',
67
+ 'task-notification-superseded': 'failed',
68
+ 'task-notification-confirmation-reassessment-required': 'failed',
69
+ })[reason]
70
+ if (notificationState === 'unknown') return { id: message.notificationFailurePhase === 'readback' ? 'waiting' : 'checking', label: message.notificationFailurePhase === 'readback' ? '待回读' : '待核对', state: 'warning', detail: message.notificationFailurePhase === 'readback' ? '发送后回读尚未确认送达' : '发送结果未知,等待核验' }
71
+ if (notificationState === 'starting') return { id: 'sending', label: '发送中', state: 'ongoing', hint: '正在执行发送,尚未确认送达' }
72
+ if (notificationState === 'prepared') return { id: 'queued', label: '待发送', state: 'ongoing', hint: '通知已准备,等待发送' }
73
+ if (notificationState === 'failed') {
74
+ if (reason === 'task-notification-task-terminal') return { id: 'stopped', label: '已停止', state: 'neutral', detail: '关联任务已结束,停止旧通知发送;本次未发送', retryDisabled: true }
75
+ if (reason === 'task-notification-superseded') return { id: 'stopped', label: '已停止', state: 'neutral', detail: '通知意图已被替代,停止旧通知发送;本次未发送', retryDisabled: true }
76
+ if (reason === 'task-notification-confirmation-reassessment-required') return { id: 'reassessment', label: '待核验', state: 'warning', detail: '已停止发送,旧确认意图关联不足,需人工核验;不会自动恢复或发送', retryDisabled: true }
77
+ return reason === 'task-notification-pre-send-rejected'
78
+ ? { id: 'failed', label: '发送前受阻', state: 'error', detail: '发送前检查已拒绝,本次未发送' }
79
+ : { id: 'failed', label: '投递异常', state: 'error', detail: message.notificationKnownNotSent === true ? '已确认本次未发送' : '尚未确认送达,需核验失败原因' }
80
+ }
81
+ if (notificationState === 'delivered') return { id: 'confirmed', label: '已送达', state: 'done', hint: '回读已确认送达' }
82
+ if (message.deliveryBlockedAt) return { id: 'failed', label: '发送待处理', state: 'error', detail: '服务端已拒绝发送,停止自动重试;需核验失败原因' }
62
83
  const details = {
63
84
  preflight_history_partial: '发送前检查:历史消息不完整,本次未发送',
64
85
  preflight_failed: '发送前检查失败,本次未发送',
@@ -69,24 +90,80 @@ window.__ModuleLoader__.load({
69
90
  message_not_observed: '已调用发送,回读尚未找到对应消息',
70
91
  }
71
92
  const detail = details[reason] || reason || (message.deliveryError ? '投递异常' : '')
72
- if (['delivery_unknown', 'postflight_history_partial', 'postflight_failed', 'message_not_observed'].includes(reason)) return { id: 'waiting', label: '待回读', state: 'warning', detail }
93
+ if (['delivery_unknown', 'postflight_history_partial', 'postflight_failed', 'message_not_observed'].includes(reason)) return { id: reason === 'delivery_unknown' ? 'checking' : 'waiting', label: reason === 'delivery_unknown' ? '待核对' : '待回读', state: 'warning', detail }
73
94
  if (reason === 'preflight_failed' || reason === 'preflight_history_partial') return { id: 'failed', label: '发送受阻', state: 'error', detail }
74
95
  if (message.deliveryError || reason === 'send_failed') return { id: 'failed', label: '投递异常', state: 'error', detail }
75
- return { id: 'queued', label: '待发送', state: 'ongoing', detail }
96
+ return { id: 'queued', label: '待发送', state: 'ongoing', hint: '通知已准备,等待发送', detail }
76
97
  }
77
98
  const ui = { textSm: 12, textMd: 14, textLg: 16, textXl: 20, space1: 4, space2: 8, space3: 12, space4: 16, space6: 24, radiusSm: 8, radiusMd: 12 }
78
99
  const observerCss = `
100
+ .observer-human-modal { box-sizing: border-box; width: min(1040px, calc(100vw - 48px)); height: min(900px, calc(100dvh - 48px)); max-width: none; max-height: none; margin: auto; padding: 0; border: 1px solid ${colors.border}; border-radius: 12px; background: ${colors.surface}; color: inherit; box-shadow: 0 24px 80px rgba(0,0,0,.22); overflow: hidden; }
101
+ .observer-human-modal[open] { display: flex; flex-direction: column; }
102
+ .observer-human-modal::backdrop { background: rgba(0,0,0,.28); backdrop-filter: blur(2px); }
103
+ .observer-human-modal > header { max-height: 30%; overflow-y: auto; }
104
+ .observer-human-modal > form, .observer-human-modal > p { flex: 0 0 auto; margin: 0; padding: 16px 24px; border-top: 1px solid ${colors.border}; background: ${colors.surface2}; }
105
+ .observer-human-modal > form { gap: 8px !important; max-height: 45%; overflow-y: auto; }
106
+ .observer-human-modal textarea { min-height: 64px; height: 80px; resize: none; }
107
+ .observer-human-modal form > button { min-width: 120px; }
108
+ .observer-human-modal form :is([role=alert], [role=status]) { grid-area: feedback; }
109
+ .observer-human-modal form { grid-template-areas: 'hint' 'field' 'feedback' 'action'; grid-template-rows: auto auto minmax(42px, auto) auto; }
110
+ .observer-human-modal form > p:first-child { grid-area: hint; }
111
+ .observer-human-modal form > label { grid-area: field; }
112
+ .observer-human-modal form > button { grid-area: action; }
113
+ @media (max-width: 600px) { .observer-human-modal { width: calc(100vw - 16px); height: calc(100dvh - 16px); border-radius: 8px; } .observer-human-modal > header { padding: 16px !important; gap: 12px !important; } .observer-human-modal > div { padding: 0 16px 16px !important; } .observer-human-modal > form, .observer-human-modal > p { padding: 12px 16px; } }
114
+
115
+ [data-dingtalk-observer] .observer-card-sessions summary::-webkit-details-marker { display: none; }
116
+ [data-dingtalk-observer] .observer-card-sessions:not([open]) .observer-card-session-chevron { transform: rotate(-90deg); }
117
+ [data-dingtalk-observer] .observer-card-sessions summary:focus-visible { outline: 2px solid ${colors.accent}; outline-offset: 2px; border-radius: 4px; }
118
+ [data-dingtalk-observer] .observer-card-sessions meter { appearance: none; border: 0; border-radius: 3px; background: ${colors.border}; }
119
+ [data-dingtalk-observer] .observer-card-sessions meter::-webkit-meter-bar { height: 5px; border: 0; border-radius: 3px; background: ${colors.border}; }
120
+ [data-dingtalk-observer] .observer-card-sessions meter::-webkit-meter-optimum-value { background: ${colors.accent}; border-radius: 3px; }
121
+ [data-dingtalk-observer] .observer-card-sessions meter::-moz-meter-bar { background: ${colors.accent}; border-radius: 3px; }
122
+ [data-dingtalk-observer] .observer-execution-session:hover:enabled { background: ${colors.surface2} !important; }
123
+ [data-dingtalk-observer] .observer-execution-session:focus-visible { outline: 2px solid ${colors.accent}; outline-offset: 2px; }
124
+ [data-dingtalk-observer] .observer-execution-session:active:enabled { opacity: .8; }
125
+
79
126
  [data-dingtalk-observer] :is(button, input, select, textarea, summary, [tabindex]):focus-visible {
80
127
  outline: 2px solid var(--dsw-alias-brand-primary, #4d6bfe) !important;
81
128
  outline-offset: 2px !important;
82
129
  }
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)); }
130
+ [data-dingtalk-observer] { scrollbar-color: ${colors.muted} ${colors.surface2}; scrollbar-width: thin; }
131
+ [data-dingtalk-observer] ::-webkit-scrollbar { width: 10px; height: 10px; }
132
+ [data-dingtalk-observer] ::-webkit-scrollbar-track { background: ${colors.surface2}; }
133
+ [data-dingtalk-observer] ::-webkit-scrollbar-thumb { background: ${colors.muted}; border: 2px solid ${colors.surface2}; border-radius: 8px; }
134
+ [data-dingtalk-observer] ::-webkit-scrollbar-thumb:hover { background: ${colors.accent}; }
135
+ [data-dingtalk-observer] ::-webkit-scrollbar-thumb:active { background: ${colors.accent}; }
136
+ [data-dingtalk-observer] .observer-task-layout { display: grid; grid-template-columns: 320px minmax(0, 1fr); gap: 32px; align-items: start; }
137
+ [data-dingtalk-observer] .observer-task-navigation { min-width: 0; }
138
+ [data-dingtalk-observer] .observer-task-step { margin-bottom: 4px; }
139
+ [data-dingtalk-observer] .observer-task-step-button { display: grid; grid-template-columns: 28px minmax(0, 1fr); gap: 12px; width: 100%; padding: 12px; border: 1px solid transparent; border-radius: ${ui.radiusSm}px; background: transparent; color: inherit; text-align: left; font: inherit; cursor: pointer; }
140
+ [data-dingtalk-observer] .observer-task-step-button:hover { background: ${colors.surface2}; }
141
+ [data-dingtalk-observer] .observer-task-step-button:active { background: color-mix(in srgb, ${colors.accent} 14%, transparent); }
142
+ [data-dingtalk-observer] .observer-task-step-button[aria-pressed="true"] { background: color-mix(in srgb, ${colors.accent} 8%, ${colors.surface}); border-color: color-mix(in srgb, ${colors.accent} 38%, transparent); box-shadow: inset 3px 0 ${colors.accent}; }
143
+ [data-dingtalk-observer] .observer-task-reading { min-width: 0; position: sticky; top: 16px; border: 1px solid ${colors.border}; border-radius: ${ui.radiusMd}px; background: ${colors.surface}; }
144
+ [data-dingtalk-observer] .observer-task-panel { padding: 24px; min-height: 280px; }
145
+ [data-dingtalk-observer] .observer-task-panel[hidden] { display: none; }
146
+ [data-dingtalk-observer] .observer-task-readout { padding-top: 24px; margin-top: 20px; border-top: 1px solid ${colors.border}; }
147
+ [data-dingtalk-observer] .observer-task-result { padding: 20px 24px; margin: 0 0 28px; background: ${colors.surface2}; border-radius: ${ui.radiusMd}px; }
148
+ [data-dingtalk-observer] .observer-task-result-text { max-height: 220px; overflow: auto; scrollbar-gutter: stable; }
149
+ [data-dingtalk-observer] .observer-task-summary { display: grid; grid-template-columns: minmax(0, 1fr) auto; gap: 24px; align-items: end; }
150
+ [data-dingtalk-observer] .observer-task-statistics { display: grid; gap: 12px; font-size: 12px; font-variant-numeric: tabular-nums; }
85
151
  [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
152
  [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; }
153
+ [data-dingtalk-observer] .observer-task-overview { padding: 28px 0 24px; }
154
+ [data-dingtalk-observer] .observer-task-detail :is(summary, a):hover { color: ${colors.accent}; }
155
+ @media (prefers-reduced-motion: reduce) { [data-dingtalk-observer] article { transition: none !important; } }
156
+ @media (forced-colors: active) {
157
+ [data-dingtalk-observer] { scrollbar-color: auto; }
158
+ [data-dingtalk-observer] .observer-task-step-button[aria-pressed="true"] { outline: 2px solid Highlight; outline-offset: -2px; }
159
+ }
160
+ [data-dingtalk-observer] .observer-config-field { display: grid; gap: 8px; min-width: 0; font-size: 14px; }
161
+ [data-dingtalk-observer] .observer-config-input { width: 100%; min-width: 0; box-sizing: border-box; padding: 10px 12px; border: 1px solid ${colors.border}; border-radius: 8px; background: ${colors.surface}; color: inherit; font: inherit; line-height: 1.6; }
162
+ [data-dingtalk-observer] textarea.observer-config-input { resize: none; }
163
+ [data-dingtalk-observer] .observer-config-input:disabled { opacity: .65; cursor: not-allowed; }
89
164
  @media (max-width: 960px) {
165
+ [data-dingtalk-observer] .observer-task-layout { grid-template-columns: 264px minmax(0, 1fr); gap: 20px; }
166
+ [data-dingtalk-observer] .observer-task-summary { grid-template-columns: minmax(0, 1fr); gap: 16px; }
90
167
  [data-dingtalk-observer] .observer-task-output-row { grid-template-columns: minmax(0, 1fr); gap: 2px; }
91
168
  [data-dingtalk-observer] .observer-task-overview { padding: 20px 0; }
92
169
  [data-dingtalk-observer] .observer-task-result { padding: 18px; }
@@ -96,6 +173,10 @@ window.__ModuleLoader__.load({
96
173
  [data-dingtalk-observer] .observer-task-board { grid-template-columns: repeat(2, minmax(280px, 1fr)) !important; }
97
174
  }
98
175
  @media (max-width: 720px) {
176
+ [data-dingtalk-observer] .observer-task-layout { grid-template-columns: minmax(0, 1fr); }
177
+ [data-dingtalk-observer] .observer-task-navigation { max-height: 360px; overflow: auto; scrollbar-gutter: stable; padding: 2px; }
178
+ [data-dingtalk-observer] .observer-task-reading { position: static; }
179
+ [data-dingtalk-observer] .observer-task-panel { padding: 18px; }
99
180
  [data-dingtalk-observer] .observer-main { padding: 12px !important; }
100
181
  [data-dingtalk-observer] .observer-topics-layout,
101
182
  [data-dingtalk-observer] .observer-task-board { grid-template-columns: minmax(0, 1fr) !important; }
@@ -137,8 +218,8 @@ window.__ModuleLoader__.load({
137
218
  React.createElement('strong', { style: { color: 'inherit', fontSize: ui.textMd, fontWeight: 600 } }, title),
138
219
  detail ? React.createElement('span', null, detail) : null,
139
220
  actionLabel && onAction ? React.createElement(Button, { variant: 'outline', size: 'sm', type: 'button', onClick: onAction }, actionLabel) : null))
140
- async function get(path) {
141
- const response = await fetch(`${ENDPOINT}${path}`, { method: 'GET', headers: { accept: 'application/json' } })
221
+ async function get(path, { signal } = {}) {
222
+ const response = await fetch(`${ENDPOINT}${path}`, { method: 'GET', headers: { accept: 'application/json' }, signal })
142
223
  const value = await response.json()
143
224
  if (!response.ok) throw new Error(value.error || `HTTP ${response.status}`)
144
225
  return value
@@ -155,13 +236,13 @@ window.__ModuleLoader__.load({
155
236
  ])
156
237
  return { health, groups, tasks, alerts, authorizations, topics: topicPage.topics }
157
238
  }
158
- function SidebarAction({ wide }) {
239
+ function SidebarAction({ wide, onOpen }) {
159
240
  const isOpen = useOpen()
160
241
  const icon = React.createElement('svg', { width: 16, height: 16, viewBox: '0 0 24 24', fill: 'none', stroke: 'currentColor', strokeWidth: 1.8, strokeLinecap: 'round', strokeLinejoin: 'round', 'aria-hidden': true },
161
242
  React.createElement('rect', { x: 3, y: 3, width: 18, height: 18, rx: 3 }),
162
243
  React.createElement('path', { d: 'M7 15v2M12 11v6M17 7v10' }))
163
244
  return React.createElement('button', {
164
- type: 'button', title: '钉钉群聊运行看板', 'aria-label': '钉钉群聊运行看板', onClick: () => setOpen(true),
245
+ type: 'button', title: '钉钉群聊运行看板', 'aria-label': '钉钉群聊运行看板', onClick: onOpen,
165
246
  style: wide
166
247
  ? { boxSizing: 'border-box', cursor: 'pointer', width: 'calc(100% + 4px)', height: 42, color: 'var(--dsw-alias-label-primary)', background: isOpen ? 'var(--dingtalk-dsh-observer-selected-bg, var(--dsw-alias-interactive-bg-selected, var(--dsw-alias-interactive-bg-hover)))' : 'transparent', border: 0, outline: 'none', boxShadow: 'none', borderRadius: 12, flex: '0 0 auto', alignItems: 'center', gap: 8, margin: '4px -2px', padding: '0 10px 0 8px', fontFamily: 'inherit', fontSize: 14, lineHeight: '22px', display: 'flex', overflow: 'hidden' }
167
248
  : { boxSizing: 'border-box', cursor: 'pointer', width: 36, height: 36, color: 'var(--dsw-alias-label-primary)', background: isOpen ? 'var(--dingtalk-dsh-observer-selected-bg, var(--dsw-alias-interactive-bg-selected, var(--dsw-alias-interactive-bg-hover)))' : 'transparent', border: 0, outline: 'none', boxShadow: 'none', borderRadius: '50%', display: 'flex', alignItems: 'center', justifyContent: 'center', margin: '8px 0 10px', padding: 0 }
@@ -173,32 +254,8 @@ window.__ModuleLoader__.load({
173
254
  { 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))' },
174
255
  { 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))' }
175
256
  ]
176
- const pages = [{ id: 'groups', label: '群消息' }, { id: 'topics', label: '话题' }, { id: 'tasks', label: '任务看板' }, { id: 'authorizations', label: '人工介入' }, { id: 'archive', label: '归档任务' }, { id: 'alerts', label: '告警' }]
257
+ const pages = [{ id: 'groups', label: '群消息' }, { id: 'topics', label: '话题' }, { id: 'tasks', label: '任务看板' }, { id: 'authorizations', label: '人工介入' }, { id: 'archive', label: '历史任务' }, { id: 'alerts', label: '告警' }]
177
258
  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
259
  const taskDisplayTitle = task => {
203
260
  const text = String(task.title || task.objective || '未命名任务').replace(/\s+/gu, ' ').trim() || '未命名任务'
204
261
  const first = text.split(/[。!?\n]/u)[0]
@@ -206,30 +263,6 @@ window.__ModuleLoader__.load({
206
263
  const characters = Array.from(text)
207
264
  return characters.length <= 32 ? text : `${characters.slice(0, 31).join('')}…`
208
265
  }
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
266
  const traceElapsed = (item, now) => {
234
267
  const start = Date.parse(item.startedAt), end = Date.parse(item.completedAt)
235
268
  const running = ['running', 'claimed'].includes(item.status)
@@ -241,49 +274,25 @@ window.__ModuleLoader__.load({
241
274
  : `${Math.floor(seconds / 3600)} 小时 ${Math.floor(seconds % 3600 / 60)} 分 ${Math.floor(seconds % 60)} 秒`
242
275
  return `${running ? '已用时' : '耗时'} ${value}${item.attempt > 1 ? `(第 ${item.attempt} 次处理)` : ''}`
243
276
  }
277
+ const messageProcessingElapsed = (message) => {
278
+ const parse = value => typeof value === 'number' && Number.isFinite(value) ? value : typeof value === 'string' && value.trim() ? Date.parse(value) : NaN
279
+ const duration = parse(message.processingCompletedAt) - parse(message.processingSentAt)
280
+ if (!Number.isFinite(duration) || duration < 0) return '历时未记录'
281
+ const seconds = duration / 1000
282
+ const value = seconds < 1 ? `${Math.round(duration)} 毫秒` : seconds < 60 ? `${seconds.toFixed(1)} 秒`
283
+ : seconds < 3600 ? `${Math.floor(seconds / 60)} 分 ${Math.floor(seconds % 60)} 秒`
284
+ : `${Math.floor(seconds / 3600)} 小时 ${Math.floor(seconds % 3600 / 60)} 分 ${Math.floor(seconds % 60)} 秒`
285
+ return `历时 ${value}`
286
+ }
244
287
  const traceReason = (reason) => {
245
288
  if (!reason) return null
246
289
  const text = typeof reason === 'string' ? reason : JSON.stringify(reason)
290
+ if (/MESSAGE_MODEL_CONTEXT_WINDOW_EXCEEDED/.test(text)) return '模型报告容量不足,已暂停处理;系统维护人员排查后继续,无需重复提交材料。'
247
291
  if (/MESSAGE_(?:CONTEXT|MATERIAL|REFERENCED_CANDIDATES)_CAPACITY|context_capacity_blocked/.test(text)) return '上下文容量受阻:必要材料未能完整提供,后续判断已停止。'
248
292
  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
293
  }
250
294
  const readableValue = (value) => value === undefined || value === null ? '历史未记录' : typeof value === 'string' ? value : JSON.stringify(value, null, 2)
251
295
  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
296
  function TraceReadout({ runId, resourceRef, label }) {
288
297
  const [expanded, setExpanded] = useState(false)
289
298
  const [page, setPage] = useState()
@@ -333,7 +342,7 @@ window.__ModuleLoader__.load({
333
342
  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
343
  return () => { active = false }
335
344
  }, [runId, cursor, retry])
336
- const labels = { split: '拆分事项', route: '关联话题', intent: '判断下一步', command: '接纳动作', agent: '查询与答复' }
345
+ const labels = { coordinator: '群消息协调', split: '拆分事项', route: '关联话题', intent: '判断下一步', command: '接纳动作', agent: '查询与答复' }
337
346
  const ready = !loading && !error && page
338
347
  return React.createElement('section', { 'aria-label': '消息处理过程', style: { ...card, display: 'grid', gap: 12 } },
339
348
  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)),
@@ -348,7 +357,7 @@ window.__ModuleLoader__.load({
348
357
  const tone = ['failed', 'blocked', 'needs_attention'].includes(item.status) ? colors.warning : colors.accent
349
358
  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
359
  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' } },
360
+ ['intent', 'coordinator'].includes(item.kind) && item.sourceMessages?.length ? React.createElement('section', { 'aria-label': '本次判断的来源消息', style: { background: colors.surface2, padding: 12, borderRadius: ui.radiusSm, margin: '10px 0' } },
352
361
  React.createElement('strong', { style: { fontSize: 13 } }, `${item.topicTitle || '当前话题'} · 本次判断覆盖 ${item.sourceMessages.length} 条消息`),
353
362
  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
363
  React.createElement('div', { style: { fontSize: 12, color: colors.muted } }, `${message.senderName || '发送人未记录'} · ${fmt(message.occurredAt)} · ${message.current ? '当前查看的消息' : '共同参与判断'}`),
@@ -357,7 +366,7 @@ window.__ModuleLoader__.load({
357
366
  React.createElement('p', { style: { margin: '10px 0 0', color: colors.muted, fontSize: 12 } }, '以上消息共享下面这次判断,判断耗时只记录一次。后续重判保留为另一条判断记录。')) : null,
358
367
  React.createElement('p', { style: { margin: '10px 0', fontWeight: 600, lineHeight: 1.6 } }, view.conclusion),
359
368
  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,
369
+ item.clarification ? readout('历史补充请求(只读)', item.clarification.question) : null,
361
370
  item.kind === 'agent' ? React.createElement('div', { style: { display: 'grid', gap: 8, margin: '8px 0' } },
362
371
  item.outputRef ? React.createElement(TraceReadout, { key: item.outputRef, runId, resourceRef: item.outputRef, label: `查看答复与依据${item.evidenceCount ? `(${item.evidenceCount} 条)` : ''}` }) : null,
363
372
  item.sessionId && onOpenSession ? React.createElement(Button, { variant: 'outline', size: 'sm', type: 'button', style: { justifySelf: 'start' }, onClick: () => onOpenSession(item.sessionId) }, '查看会话记录') : null) : null,
@@ -409,6 +418,14 @@ window.__ModuleLoader__.load({
409
418
  get(`/state/topics?${params}`).then((value) => { if (active) { setListing(value); if (value.total > 0 && offset >= value.total) setOffset(Math.floor((value.total - 1) / topicPageSize) * topicPageSize) } }, (error) => { if (active) setListError(error.message) }).finally(() => { if (active) setLoading(false) })
410
419
  return () => { active = false }
411
420
  }, [groupId, offset, retry, updatedAt])
421
+ useEffect(() => {
422
+ const current = listing?.topics.find(item => item.topicId === selection?.topicId && item.groupId === selection?.groupId)
423
+ if (!current || current.revision === selection.revision && current.summaryRevision === selection.summaryRevision && current.updatedAt === selection.updatedAt) return
424
+ setSelection({ groupId: current.groupId, topicId: current.topicId, revision: current.revision, summaryRevision: current.summaryRevision, updatedAt: current.updatedAt })
425
+ setMessageOffset(0); setContextCursor(''); setContextCursorHistory([])
426
+ setIntentCursor(''); setIntentCursorHistory([]); setSelectedIntentRun(''); setContextRevision(undefined)
427
+ setContext(undefined); setTopicContext(undefined)
428
+ }, [listing, selection])
412
429
  useEffect(() => {
413
430
  if (!selection) { setContext(undefined); return }
414
431
  let active = true
@@ -421,11 +438,11 @@ window.__ModuleLoader__.load({
421
438
  if (!selection) { setTopicContext(undefined); return }
422
439
  let active = true
423
440
  setContextLoading(true); setContextError('')
424
- const params = new URLSearchParams()
441
+ const params = new URLSearchParams({ groupId: selection.groupId })
425
442
  if (contextCursor) params.set('cursor', contextCursor)
426
443
  if (intentCursor) params.set('intentCursor', intentCursor)
427
444
  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) })
445
+ get(`/state/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
446
  return () => { active = false }
430
447
  }, [selection, contextCursor, intentCursor, retry])
431
448
  const selectTopic = (topic) => { setSelection({ groupId: topic.groupId, topicId: topic.topicId, revision: topic.revision }); setMessageOffset(0); setContextCursor(''); setContextCursorHistory([]); setIntentCursor(''); setIntentCursorHistory([]); setSelectedIntentRun(''); setContextRevision(undefined); setTopicContext(undefined) }
@@ -461,7 +478,7 @@ window.__ModuleLoader__.load({
461
478
  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 } }, '历史未记录')),
462
479
  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}` } },
463
480
  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 } }, '当前没有待解决问题')),
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))),
481
+ 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', onClick: () => onOpenTask(task), style: { maxWidth: '100%' } }, React.createElement('span', { style: { overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }, title: task.title || task.objective }, taskDisplayTitle(task)))), relatedTasks.length === 0 ? React.createElement('span', { style: { color: colors.muted, fontSize: 12 } }, '暂无关联任务') : null))),
465
482
  React.createElement('section', { 'aria-label': '群聊消息', style: { marginTop: 4 } },
466
483
  React.createElement('strong', { style: { display: 'block', padding: '14px 0', fontSize: 13, fontWeight: 600 } }, `群聊消息 · ${context.total} 条`),
467
484
  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('此版本没有消息')])),
@@ -494,7 +511,7 @@ window.__ModuleLoader__.load({
494
511
  const content = React.createElement('div', { style: { minWidth: 0, fontSize: 13, lineHeight: 1.7 } },
495
512
  text ? React.createElement('div', { style: { display: 'grid', gap: 6, overflowWrap: 'anywhere' } }, ...text.split('\n\n').map((part, index) => {
496
513
  const [heading, ...lines] = part.split('\n')
497
- const titled = ['产出摘要', '正文', '任务要求', '发现', '限制与未确认事项', '执行范围', '相关文件', '已有文件', '新建文件', '材料正文', '已读取文件', '已修改文件', '涉及文件', '文件变更', '修改方案', '检查结果', '文件索引', '基线版本', '可修改文件', '变更文件', '提交说明', '分支', '远端', '提交版本', '执行结果', 'PR 标题', '目标仓库', '来源分支', '目标分支', 'PR 正文', 'PR 地址', 'PR 状态', '方案工件路径', '工作目录', '来源仓库', '隔离方式', '处理内容', '项目', '工作分支', '起点版本'].includes(heading)
514
+ const titled = ['产出摘要', '正文', '任务要求', '发现', '限制与未确认事项', '执行范围', '相关文件', '已有文件', '新建文件', '材料正文', '已读取文件', '已修改文件', '涉及文件', '文件变更', '修改方案', '方案修订意见', '技术方案引用', '检查结果', '文件索引', '基线版本', '可修改文件', '变更文件', '提交说明', '分支', '远端', '提交版本', '执行结果', 'PR 标题', '目标仓库', '来源分支', '目标分支', 'PR 正文', 'PR 地址', 'PR 状态', '方案工件路径', '工作目录', '来源仓库', '隔离方式', '处理内容', '项目', '工作分支', '起点版本'].includes(heading)
498
515
  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
516
  React.createElement('div', { style: { whiteSpace: 'pre-wrap' } }, titled ? lines.join('\n') : part))
500
517
  })) : null,
@@ -519,14 +536,738 @@ window.__ModuleLoader__.load({
519
536
  parts.push(content.slice(offset))
520
537
  return parts
521
538
  }
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)] || ''
539
+ // 仅整理说明文字的转义换行;路径和代码原样显示,完整持久原文另可展开核对。
540
+ const humanRequestText = value => String(value ?? '').replace(/(`[^`]*`|(?:[A-Za-z]:\\|\\\\)[^\r\n]+)|\\r\\n|\\n/g, (match, literal) => literal || '\n')
541
+ function HumanRequestContent({ request }) {
542
+ const question = request.question || request.requestedAction || ''
543
+ const reason = request.reason || (request.waitingReason !== question ? request.waitingReason : '')
544
+ const field = (label, value) => React.createElement('section', { style: { display: 'grid', gap: ui.space2, minWidth: 0 } },
545
+ React.createElement('h3', { style: { margin: 0, fontSize: ui.textSm, color: colors.muted } }, label),
546
+ React.createElement('div', { style: { fontSize: ui.textMd, lineHeight: 1.7, whiteSpace: 'pre-wrap', overflowWrap: 'anywhere' } }, ...linkedTaskText(humanRequestText(value))))
547
+ const supporting = (label, items) => Array.isArray(items) && items.length ? React.createElement('details', { style: { borderTop: `1px solid ${colors.border}`, paddingTop: ui.space3 } },
548
+ React.createElement('summary', { style: { cursor: 'pointer', fontSize: ui.textSm, fontWeight: 600 } }, `${label} · ${items.length}`),
549
+ React.createElement('ol', { style: { paddingLeft: ui.space6, margin: `${ui.space3}px 0 0`, display: 'grid', gap: ui.space2, fontSize: ui.textSm, lineHeight: 1.7 } }, ...items.map((item, index) => React.createElement('li', { key: index, style: { whiteSpace: 'pre-wrap', overflowWrap: 'anywhere' } }, ...linkedTaskText(humanRequestText(readableValue(item))))))) : null
550
+ return React.createElement('div', { className: 'observer-human-request', style: { display: 'grid', gap: ui.space4, minWidth: 0 } },
551
+ field('当前进度与等待原因', reason || '该申请未说明等待原因。'),
552
+ question ? field('需要你决定', question) : null,
553
+ request.risk && request.risk !== '未单独说明' ? field('风险', request.risk) : null,
554
+ supporting('证据与范围明细', request.evidence), supporting('已尝试', request.attemptedActions),
555
+ readout('申请原文', { reason: request.reason, question: request.question, requestedAction: request.requestedAction, waitingReason: request.waitingReason, risk: request.risk, evidence: request.evidence, attemptedActions: request.attemptedActions }))
556
+ }
557
+ const activePageStorageKey = 'dingtalk-dsh-observer.active-page'
558
+ const readActivePage = () => {
559
+ try {
560
+ const page = window.sessionStorage.getItem(activePageStorageKey)
561
+ return pages.some(item => item.id === page) ? page : 'groups'
562
+ } catch { return 'groups' }
563
+ }
564
+ const taskStateLabel = state => ({ running: '执行中', queued: '待处理', waiting: '等待处理', paused: '已暂停', completed: '已结束', succeeded: '已完成', failed: '失败', cancelled: '已取消', blocked: '受阻', pending: '待执行', interrupted: '已中断' })[state] || state || '状态未记录'
565
+ const taskStateTone = state => ['completed', 'succeeded'].includes(state) ? 'done' : ['failed', 'blocked'].includes(state) ? 'error' : ['waiting', 'paused', 'interrupted'].includes(state) ? 'warning' : ['running', 'queued', 'pending'].includes(state) ? 'ongoing' : 'neutral'
566
+ const taskSession = task => task.session || { sessionId: task.sessionId, workspaceDir: task.workspaceDir, logPath: task.logPath }
567
+ function SessionLocation({ session, label, onOpenSession, compact = false, entryOnly = false, actionLabel }) {
568
+ const [busy, setBusy] = useState(false)
569
+ const [error, setError] = useState('')
570
+ const pending = useRef(false)
571
+ const open = async () => {
572
+ if (pending.current || !session?.sessionId || session.sessionBound === false) return
573
+ pending.current = true; setBusy(true); setError('')
574
+ try { await onOpenSession(session.sessionId, session.parentSessionId) }
575
+ catch (cause) { setError(cause.message) }
576
+ finally { pending.current = false; setBusy(false) }
577
+ }
578
+ const entry = session?.sessionId && session.sessionBound !== false ? React.createElement(Button, { type: 'button', variant: 'outline', size: 'sm', disabled: busy, onClick: open, 'aria-label': actionLabel, style: { justifySelf: 'start', minWidth: 110 } }, busy ? '正在打开…' : `打开${label}`) : null
579
+ const failure = error ? React.createElement('p', { role: 'alert', style: { color: colors.danger } }, error) : null
580
+ if (entryOnly) return React.createElement('div', null, entry, failure)
581
+ const location = React.createElement(React.Fragment, null,
582
+ React.createElement('div', null, '会话 ID:', session?.sessionId ? `${session.sessionId}${session.sessionBound === false ? '(尚未绑定)' : ''}` : '尚未建立'),
583
+ React.createElement('div', null, '工作目录:', session?.workspaceDir || '尚未分配'),
584
+ compact && session?.workingDirectory ? React.createElement('div', null, '执行目录:', session.workingDirectory) : null,
585
+ React.createElement('div', null, '日志路径:', session?.logPath || '尚未记录'))
586
+ return React.createElement('section', { className: compact ? 'detail-session' : undefined, 'aria-label': label, style: compact ? undefined : { ...card, display: 'grid', gap: 10, overflowWrap: 'anywhere' } },
587
+ React.createElement('strong', null, label),
588
+ compact ? React.createElement('p', { className: 'detail-muted' }, label === '任务会话' ? '统筹目标、安排独立执行并汇总结果。' : '本次执行的独立会话、工作目录与日志。') : null,
589
+ compact ? React.createElement('details', { className: 'detail-disclosure' }, React.createElement('summary', null, '会话信息与路径'), React.createElement('div', { className: 'detail-location' }, location)) : location,
590
+ compact && (!session?.sessionId || session.sessionBound === false) ? React.createElement('p', { className: 'detail-muted' }, session?.sessionId ? '会话尚未绑定,暂不可打开。' : '会话尚未建立。') : null,
591
+ entry, failure)
592
+ }
593
+ function ExecutionSessionDuration({ timing }) {
594
+ const [clock, setClock] = useState(Date.now)
595
+ useEffect(() => {
596
+ if (!timing?.recorded || !timing.startedAt) return
597
+ setClock(Date.now())
598
+ const timer = setInterval(() => setClock(Date.now()), 1000)
599
+ return () => clearInterval(timer)
600
+ }, [timing?.recorded, timing?.startedAt])
601
+ const delta = timing?.startedAt ? clock - Date.parse(timing.startedAt) : 0
602
+ const recorded = timing?.recorded === true && Number.isFinite(timing.elapsedMs) && timing.elapsedMs >= 0 && Number.isFinite(delta) && delta >= 0
603
+ const value = recorded ? fmtDuration(timing.elapsedMs + delta) : '—'
604
+ const description = recorded ? `累计实际运行时长:${value}` : '时长未记录:缺少完整的实际运行计时证据。'
605
+ return React.createElement('span', { className: 'observer-execution-duration', title: description, 'aria-label': description, style: { color: colors.muted, fontVariantNumeric: 'tabular-nums', display: 'inline-block', minWidth: '8em', textAlign: 'right' } }, value)
606
+ }
607
+ function TaskResult({ result }) {
608
+ const value = typeof result === 'string' ? { summary: result } : result
609
+ const values = items => Array.isArray(items) ? items : items ? [items] : []
610
+ return React.createElement('section', { className: 'observer-task-result', 'aria-label': '当前结果' },
611
+ React.createElement('h2', { style: { margin: '0 0 12px', fontSize: 16 } }, '当前结果'),
612
+ React.createElement('div', { className: 'observer-task-result-text', style: { whiteSpace: 'pre-wrap', overflowWrap: 'anywhere' } }, ...linkedTaskText(value?.summary || '尚未形成结果。')),
613
+ values(value?.evidence).length ? React.createElement('div', null, React.createElement('h3', { style: { fontSize: 14 } }, '证据'), React.createElement('ul', null, ...values(value.evidence).map((item, index) => React.createElement('li', { key: index, style: { overflowWrap: 'anywhere' } }, ...linkedTaskText(typeof item === 'string' ? item : item.description || item.path || item.ref || '证据已记录'))))) : null,
614
+ values(value?.limitations).length ? React.createElement('div', null, React.createElement('h3', { style: { fontSize: 14 } }, '限制与未验证项'), React.createElement('ul', null, ...values(value.limitations).map((item, index) => React.createElement('li', { key: index }, typeof item === 'string' ? item : readableValue(item))))) : null)
615
+ }
616
+ function useUnsavedTaskChanges(dirty, onDirtyChange) {
617
+ useEffect(() => { onDirtyChange?.(dirty); return () => onDirtyChange?.(false) }, [dirty, onDirtyChange])
618
+ useEffect(() => {
619
+ if (!dirty) return
620
+ const warn = event => { event.preventDefault(); event.returnValue = '' }
621
+ window.addEventListener('beforeunload', warn)
622
+ return () => window.removeEventListener('beforeunload', warn)
623
+ }, [dirty])
624
+ }
625
+ function CreateTaskForm({ groups, onCreated, onCancel, onDirtyChange }) {
626
+ const [draft, setDraft] = useState({ groupId: '', title: '', objective: '', context: '', acceptance: '' })
627
+ const [busy, setBusy] = useState(false), [error, setError] = useState('')
628
+ const attempt = useRef(null), pending = useRef(false), fields = useRef({})
629
+ const [errors, setErrors] = useState({}), [leaving, setLeaving] = useState(false)
630
+ const dirty = Object.values(draft).some(Boolean)
631
+ useUnsavedTaskChanges(dirty, onDirtyChange)
632
+ const change = (field, value) => { setDraft(current => ({ ...current, [field]: value })); setErrors(current => ({ ...current, [field]: false })); attempt.current = null }
633
+ const submit = async event => {
634
+ event.preventDefault()
635
+ if (event.nativeEvent?.isComposing || pending.current) return
636
+ const invalid = { groupId: !draft.groupId, title: !draft.title.trim(), objective: !draft.objective.trim() }
637
+ setErrors(invalid)
638
+ if (Object.values(invalid).some(Boolean)) { setError('请选择来源群,并填写任务名称和目标。'); fields.current[Object.keys(invalid).find(name => invalid[name])]?.focus(); return }
639
+ const body = { groupId: draft.groupId, title: draft.title.trim(), objective: draft.objective.trim(), ...(draft.context.trim() ? { context: draft.context.trim() } : {}), acceptanceCriteria: draft.acceptance.split('\n').map(text => text.trim()).filter(Boolean) }
640
+ attempt.current ||= { ...body, requestId: crypto.randomUUID() }
641
+ pending.current = true; setBusy(true); setError('')
642
+ try { const result = await post('/tasks', attempt.current); onDirtyChange?.(false); onCreated(result) }
643
+ catch (cause) { setError(cause.message) }
644
+ finally { pending.current = false; setBusy(false) }
645
+ }
646
+ const field = (label, name, multiline = false, required = false) => React.createElement('label', { className: 'observer-config-field' }, label,
647
+ React.createElement(multiline ? 'textarea' : 'input', { className: 'observer-config-input', ref: element => { fields.current[name] = element }, 'aria-invalid': !!errors[name], 'aria-describedby': errors[name] ? 'task-create-error' : undefined, value: draft[name], required, maxLength: name === 'title' ? 120 : 16000, rows: multiline ? 5 : undefined, disabled: busy, onChange: event => change(name, event.target.value) }))
648
+ return React.createElement('form', { noValidate: true, 'aria-label': '新建任务', onSubmit: submit, style: { ...card, display: 'grid', gap: 16, maxWidth: 800 } },
649
+ React.createElement('h2', { style: { margin: 0, fontSize: 20 } }, '新建任务'),
650
+ React.createElement('p', { style: { margin: 0, color: colors.muted } }, '描述目标与约束。任务会话会按实际需要创建独立执行。'),
651
+ React.createElement('label', { className: 'observer-config-field' }, '来源群', React.createElement('select', { className: 'observer-config-input', ref: element => { fields.current.groupId = element }, 'aria-invalid': !!errors.groupId, 'aria-describedby': errors.groupId ? 'task-create-error' : undefined, required: true, value: draft.groupId, disabled: busy, onChange: event => change('groupId', event.target.value) }, React.createElement('option', { value: '' }, '请选择群聊'), ...groups.map(group => React.createElement('option', { key: group.groupId, value: group.groupId }, group.name || group.groupId)))),
652
+ field('任务名称', 'title', false, true), field('任务目标', 'objective', true, true), field('参考资料(不代表执行授权)', 'context', true), field('验收要求(每行一项,可选)', 'acceptance', true),
653
+ error ? React.createElement('p', { id: 'task-create-error', role: 'alert', style: { margin: 0, color: colors.danger } }, error) : null,
654
+ React.createElement('div', { style: { display: 'flex', gap: 12 } }, React.createElement(Button, { type: 'submit', disabled: busy, style: { minWidth: 100 } }, busy ? '提交中…' : '创建任务'), React.createElement(Button, { type: 'button', variant: 'outline', disabled: busy, onClick: () => dirty ? setLeaving(true) : onCancel() }, '返回看板')),
655
+ leaving ? React.createElement('section', { 'aria-label': '确认放弃草稿', style: { display: 'grid', gap: 8 } }, React.createElement('p', null, '返回看板会丢弃尚未提交的任务草稿。'), React.createElement(Button, { type: 'button', variant: 'outline', onClick: () => setLeaving(false) }, '继续填写'), React.createElement(Button, { type: 'button', onClick: () => { onDirtyChange?.(false); onCancel() } }, '放弃草稿并返回')) : null)
656
+ }
657
+ function HumanRequestModal({ children, onClose }) {
658
+ const dialog = useRef(null)
659
+ useEffect(() => {
660
+ const element = dialog.current
661
+ const previous = document.activeElement
662
+ const overflow = document.documentElement.style.overflow
663
+ element.showModal()
664
+ document.documentElement.style.overflow = 'hidden'
665
+ return () => {
666
+ element.close()
667
+ document.documentElement.style.overflow = overflow
668
+ if (previous?.isConnected) previous.focus({ preventScroll: true })
669
+ }
670
+ }, [])
671
+ return React.createElement('dialog', { ref: dialog, className: 'observer-human-modal', onKeyDown: event => {
672
+ if (event.key !== 'Tab') return
673
+ const controls = [...event.currentTarget.querySelectorAll('button:not(:disabled), a[href], input:not(:disabled), textarea:not(:disabled), select:not(:disabled), summary, [tabindex="0"]')].filter(element => element.getClientRects().length)
674
+ const first = controls[0], last = controls[controls.length - 1]
675
+ if (event.shiftKey && document.activeElement === first) { event.preventDefault(); last?.focus() }
676
+ else if (!event.shiftKey && document.activeElement === last) { event.preventDefault(); first?.focus() }
677
+ }, 'aria-label': '人工介入事项详情', onCancel: event => { event.preventDefault(); onClose() }, onClick: event => { if (event.target === event.currentTarget) { const bounds = event.currentTarget.getBoundingClientRect(); if (event.clientX < bounds.left || event.clientX > bounds.right || event.clientY < bounds.top || event.clientY > bounds.bottom) onClose() } } }, ...children)
678
+ }
679
+ function HumanRequestReply({ request, onChanged }) {
680
+ const [reply, setReply] = useState(''), [busy, setBusy] = useState(false), [error, setError] = useState(''), [notice, setNotice] = useState('')
681
+ const pending = useRef(false), attempt = useRef(null)
682
+ const submit = async event => {
683
+ event.preventDefault()
684
+ if (event.nativeEvent?.isComposing || pending.current || notice || request.status !== 'waiting-reply') return
685
+ if (!reply.trim()) { setError('请填写答复内容。'); return }
686
+ attempt.current ||= { requestId: crypto.randomUUID(), reply: reply.trim(), expectedRequirementRevision: request.requirementRevision, expectedControlRevision: request.controlRevision }
687
+ pending.current = true; setBusy(true); setError('')
688
+ try {
689
+ await post(`/tasks/${encodeURIComponent(request.taskId)}/human-requests/${encodeURIComponent(request.requestId)}/reply`, attempt.current)
690
+ setNotice('答复已记录,原任务将继续处理。'); onChanged()
691
+ } catch (cause) { setError(`答复未提交:${cause.message}。内容已保留,请核对当前问题后重试。`) }
692
+ finally { pending.current = false; setBusy(false) }
693
+ }
694
+ if (!request.taskSessionRequest) return React.createElement('p', { role: 'status' }, '历史介入记录只读。新执行的权限请求请在对应执行会话中处理。')
695
+ if (request.status !== 'waiting-reply') return React.createElement('p', { role: 'status' }, request.status === 'answered' ? `已答复:${request.reply || '答复已记录。'}` : '该问题已失效,请查看当前任务。')
696
+ return React.createElement('form', { noValidate: true, onSubmit: submit, 'aria-label': '答复人工介入问题', style: { display: 'grid', gap: 12, padding: 16 } },
697
+ React.createElement('p', { style: { margin: 0, color: colors.muted } }, '答复将作为当前问题的补充信息交回原任务,不代表批准其他操作。'),
698
+ React.createElement('label', { className: 'observer-config-field' }, '答复内容', React.createElement('textarea', { className: 'observer-config-input', required: true, rows: 5, maxLength: 20000, value: reply, disabled: busy || !!notice, 'aria-invalid': !!error, 'aria-describedby': error ? 'human-request-reply-error' : undefined, onChange: event => { setReply(event.target.value); attempt.current = null; setError('') } })),
699
+ error ? React.createElement('p', { id: 'human-request-reply-error', role: 'alert', style: { margin: 0, color: colors.danger } }, error) : null,
700
+ notice ? React.createElement('p', { role: 'status', style: { margin: 0 } }, notice) : null,
701
+ React.createElement(Button, { type: 'submit', disabled: busy || !!notice || !reply.trim(), style: { justifySelf: 'start' } }, busy ? '提交答复中…' : '提交答复'))
702
+ }
703
+ function TaskExecutions({ taskId, updatedAt, onOpenSession }) {
704
+ const [offset, setOffset] = useState(0), [page, setPage] = useState(), [error, setError] = useState(''), [retry, setRetry] = useState(0)
705
+ const [loading, setLoading] = useState(true)
706
+ useEffect(() => {
707
+ const controller = new AbortController(); let active = true
708
+ setLoading(true); setError('')
709
+ get(`/state/tasks/${encodeURIComponent(taskId)}/executions?offset=${offset}&limit=20`, { signal: controller.signal }).then(value => { if (active) { const items = value.executions || value.items || (Array.isArray(value) ? value : []); const total = value.total ?? items.length; if (offset > 0 && offset >= total) { setOffset(Math.max(0, Math.floor((total - 1) / 20) * 20)); return } setPage({ items, total, offset, refreshedAt: updatedAt }); } }, cause => { if (active) setError(cause.message) }).finally(() => { if (active) setLoading(false) })
710
+ return () => { active = false; controller.abort() }
711
+ }, [taskId, offset, updatedAt, retry])
712
+ const ready = !!page
713
+ const visibleOffset = page?.offset ?? 0
714
+ const refreshedAt = page?.refreshedAt && Number.isFinite(new Date(page.refreshedAt).getTime()) ? page.refreshedAt : null
715
+ return React.createElement('section', { className: 'detail-executions', 'aria-label': '动态执行列表', style: { display: 'grid', gap: 16, minHeight: 160 } },
716
+ React.createElement('div', { className: 'detail-section-heading' }, React.createElement('h2', null, '执行会话'),
717
+ React.createElement('div', { className: 'detail-execution-feedback' },
718
+ React.createElement('span', { role: error ? 'alert' : 'status', className: error ? 'detail-execution-error' : 'detail-muted', tabIndex: error ? 0 : undefined },
719
+ error ? `执行记录读取失败${ready ? ',暂显示已读取记录' : ''}:${error}` : loading ? `${ready && page.offset !== offset ? '正在读取另一页,暂显示已读取记录' : '正在读取执行记录'}…` : ''),
720
+ React.createElement(Button, { type: 'button', variant: 'outline', size: 'sm', 'aria-label': '重试读取执行', disabled: !error || loading, style: { visibility: error ? 'visible' : 'hidden' }, onClick: () => setRetry(value => value + 1) }, '重试'))),
721
+ React.createElement('div', { className: 'detail-pagination' }, React.createElement('span', { className: 'detail-muted detail-execution-meta', style: { marginRight: 'auto' } }, refreshedAt ? React.createElement('span', null, `更新 ${fmt(refreshedAt)}`) : null, React.createElement('span', null, ready ? `${page.total} 条执行记录 · ${page.total ? visibleOffset + 1 : 0}–${visibleOffset + page.items.length}` : '尚未读取执行记录')), React.createElement(Button, { type: 'button', variant: 'outline', disabled: loading || !ready || visibleOffset === 0, onClick: () => setOffset(Math.max(0, visibleOffset - 20)) }, '上一页'), React.createElement(Button, { type: 'button', variant: 'outline', disabled: loading || !ready || visibleOffset + page.items.length >= page.total, onClick: () => setOffset(visibleOffset + 20) }, '下一页')),
722
+ ready ? React.createElement('ol', { className: 'detail-execution-list' }, ...page.items.map(execution => React.createElement('li', { key: execution.executionId, className: 'detail-execution' },
723
+ React.createElement('details', null, React.createElement('summary', { className: 'detail-execution-summary' },
724
+ React.createElement('span', { className: 'detail-execution-title' }, execution.title || execution.objective || execution.executionId),
725
+ statusTag(taskStateLabel(execution.status), taskStateTone(execution.status)),
726
+ React.createElement('span', { className: 'detail-execution-time' }, `创建 ${fmt(execution.createdAt)}`)),
727
+ React.createElement('div', { className: 'detail-execution-body' },
728
+ React.createElement('p', { className: 'detail-objective' }, execution.input?.objective || execution.objective || '尚未记录执行目标。'),
729
+ execution.progressReview ? React.createElement('p', { className: 'detail-muted' }, `最近活动 ${fmt(execution.progressReview.lastActivityAt)} · 进展检查 ${fmt(execution.progressReview.checkedAt)}`, execution.progressReview.readError ? ` · 记录读取失败:${traceReason(execution.progressReview.readError)}` : '') : null,
730
+ React.createElement(SessionLocation, { session: execution, label: '执行会话', onOpenSession, compact: true }),
731
+ execution.error ? React.createElement('p', { role: 'status', style: { color: colors.danger } }, traceReason(execution.error)) : null,
732
+ React.createElement(TaskResult, { result: execution.result }),
733
+ React.createElement('details', { className: 'detail-disclosure' }, React.createElement('summary', null, '执行记录信息'), React.createElement('p', { className: 'detail-muted' }, `执行 ${execution.executionId} · 需求修订 ${execution.requirementRevision ?? '未记录'}`))))))) : null,
734
+ ready && !page.items.length ? emptyState('尚无动态执行', { detail: '任务会话尚未分派执行;任务可直接形成结果。' }) : null,
735
+ !ready ? emptyState(loading ? '正在读取执行记录' : '尚未读取到执行记录', { detail: '读取完成后在此显示独立执行。' }) : null)
736
+ }
737
+ function LegacyTaskOutput({ taskId, node, detailRevision }) {
738
+ const [opened, setOpened] = useState(false)
739
+ return React.createElement('details', { style: card, onToggle: event => { if (event.currentTarget.open) setOpened(true) } },
740
+ React.createElement('summary', { style: { cursor: 'pointer' } }, `${node.title || node.nodeId || '历史执行'} · ${taskStateLabel(node.status)}`),
741
+ opened ? node.outputRef ? React.createElement(TaskStepOutput, { taskId, node, detailRevision }) : React.createElement('p', null, '历史未记录产出') : null)
742
+ }
743
+ function LegacyTaskHistory({ task }) {
744
+ const [page, setPage] = useState(1)
745
+ const items = [...(task.executions || []).map(execution => ({ execution })), ...(task.executionNodes || []).map(node => ({ node }))]
746
+ const pages = Math.max(1, Math.ceil(items.length / 20)), current = Math.min(page, pages)
747
+ return React.createElement('section', { 'aria-label': '只读历史执行', style: { display: 'grid', gap: 12 } },
748
+ React.createElement('p', { role: 'status' }, '历史任务只读,不再接受旧执行操作。'),
749
+ task.session?.sessionId ? React.createElement('p', { style: { color: colors.muted } }, `历史任务会话:${task.session.sessionId}`) : null,
750
+ ...items.slice((current - 1) * 20, current * 20).map(({ execution, node }, index) => node ? React.createElement(LegacyTaskOutput, { key: node.nodeRunId || index, taskId: task.taskId, node, detailRevision: task.detailRevision })
751
+ : React.createElement('details', { key: execution.executionId, style: card }, React.createElement('summary', { style: { cursor: 'pointer' } }, `历史执行 ${execution.executionId} · ${taskStateLabel(execution.status)}`), React.createElement('p', null, `记录时间 ${fmt(execution.createdAt)}`), ...(execution.sessions || []).map(session => React.createElement('p', { key: session.sessionId }, `历史会话 ${session.sessionId} · ${taskStateLabel(session.status)}`)), execution.result ? React.createElement(TaskResult, { result: execution.result }) : null)),
752
+ !items.length ? emptyState('暂无历史执行记录') : null,
753
+ pages > 1 ? React.createElement('div', { style: { display: 'flex', gap: 12 } }, React.createElement(Button, { type: 'button', variant: 'outline', disabled: current <= 1, onClick: () => setPage(value => value - 1) }, '上一页'), React.createElement('span', null, `${current}/${pages}`), React.createElement(Button, { type: 'button', variant: 'outline', disabled: current >= pages, onClick: () => setPage(value => value + 1) }, '下一页')) : null)
754
+ }
755
+ const taskProgressView = task => {
756
+ const terminal = ['completed', 'succeeded', 'failed', 'cancelled', 'stale']
757
+ const ended = task.legacyReadonly || terminal.includes(task.state)
758
+ const paused = (task.controlState || task.control?.state || task.control) === 'paused' || task.state === 'paused'
759
+ const records = new Map()
760
+ for (const item of [...(Array.isArray(task.executions) ? task.executions : []), ...(Array.isArray(task.blockers?.executions) ? task.blockers.executions : [])]) {
761
+ if (item.executionId) records.set(item.executionId, { ...records.get(item.executionId), ...item, sessionBound: item.sessionBound ?? records.get(item.executionId)?.sessionBound ?? false })
762
+ }
763
+ const executions = ended ? [] : [...records.values()].filter(item => !terminal.includes(item.status) || item.drained === false).map(item => {
764
+ const outdated = item.requirementRevision != null && task.requirementRevision != null && item.requirementRevision !== task.requirementRevision
765
+ || item.controlRevision != null && task.controlRevision != null && item.controlRevision !== task.controlRevision
766
+ const stopping = item.cancelRequested || outdated
767
+ const running = !paused && !stopping && !task.recovering && !item.recovering && item.status === 'running' && item.drained !== true
768
+ const label = stopping ? '等待停止 / 收尾' : task.recovering || item.recovering ? '会话恢复中' : paused ? '任务已暂停' : terminal.includes(item.status) ? '执行结束,等待收尾' : taskStateLabel(item.status)
769
+ return { ...item, running, outdated, label, objective: item.input?.objective || item.objective || item.input?.title || item.executionId }
770
+ })
771
+ const running = executions.filter(item => item.running).length
772
+ const queued = executions.filter(item => ['queued', 'pending'].includes(item.status) && !item.cancelRequested && !item.outdated && !paused).length
773
+ const summary = ended ? task.legacyReadonly ? '历史任务只读,不展示实时执行进展。' : `任务${taskStateLabel(task.state)},当前没有正在执行的事项。`
774
+ : paused ? '任务已暂停;以下为尚未结束的执行记录。'
775
+ : task.recovering ? '任务会话恢复中,当前处理步骤尚未记录。'
776
+ : ['waiting', 'blocked', 'interrupted'].includes(task.state) ? `任务${taskStateLabel(task.state)}${executions.length ? ';以下为尚未结束的执行记录。' : ',暂无活跃执行记录。'}`
777
+ : running || queued ? `${running} 项执行中 · ${queued} 项待执行`
778
+ : task.drained === false && taskSession(task)?.sessionBound === true && taskSession(task)?.sessionId ? '统筹会话正在处理任务,具体步骤尚未记录。'
779
+ : '暂无当前执行进展记录。'
780
+ return { ended, paused, executions, summary }
781
+ }
782
+ const taskOverallProgressView = task => {
783
+ const ended = task.legacyReadonly || ['completed', 'succeeded', 'failed', 'cancelled'].includes(task.state)
784
+ const paused = (task.controlState || task.control?.state || task.control) === 'paused' || task.state === 'paused'
785
+ const decision = task.lastDecision
786
+ const hasRecord = typeof decision?.summary === 'string' && !!decision.summary.trim()
787
+ const verified = hasRecord && Number.isSafeInteger(task.requirementRevision) && Number.isSafeInteger(task.controlRevision)
788
+ && decision.requirementRevision === task.requirementRevision && decision.controlRevision === task.controlRevision
789
+ && Number.isFinite(Date.parse(decision.at))
790
+ const currentRecord = ['wait', 'blocked'].includes(decision?.action) && verified && !ended && !paused
791
+ const updating = currentRecord && (task.drained === false || task.recovering === true)
792
+ const waiting = ['waiting', 'blocked', 'interrupted'].includes(task.state)
793
+ return {
794
+ description: ended ? task.legacyReadonly ? '历史任务只读,不展示当前工作。' : `任务${taskStateLabel(task.state)},结果与执行记录见下方。`
795
+ : paused ? '任务已暂停,等待后续指示。'
796
+ : currentRecord ? decision.summary
797
+ : task.recovering ? '任务会话恢复中,当前整体工作尚未记录。' : '当前整体工作尚未记录。',
798
+ heading: ended || paused ? '当前状态' : updating ? '最近已确认进展' : currentRecord ? waiting ? '当前等待 / 阻塞' : '最新统筹进展' : '当前工作',
799
+ recordedAt: currentRecord ? decision.at : null,
800
+ missing: !ended && !paused && !currentRecord,
801
+ updating,
802
+ lastRecord: !ended && hasRecord ? { summary: decision.summary, at: decision.at, verified } : null,
803
+ }
804
+ }
805
+ function TaskProgress({ task, onOpenSession }) {
806
+ const view = taskProgressView(task)
807
+ const confirmed = useRef(null)
808
+ let overall = taskOverallProgressView(task)
809
+ const identity = `${task.taskId}:${task.requirementRevision}:${task.controlRevision}`
810
+ if (confirmed.current?.identity !== identity) confirmed.current = null
811
+ if (overall.recordedAt) confirmed.current = { identity, description: overall.description, recordedAt: overall.recordedAt }
812
+ else if (overall.missing && confirmed.current && (task.drained === false || task.recovering === true)) {
813
+ overall = { ...overall, description: confirmed.current.description, recordedAt: confirmed.current.recordedAt,
814
+ heading: '最近已确认进展', missing: false, updating: true }
815
+ }
816
+ const time = value => value ? fmt(value) : '未记录'
817
+ return React.createElement('section', { className: 'detail-panel detail-progress', 'aria-label': '当前进展' },
818
+ React.createElement('h2', null, '当前进展'),
819
+ React.createElement('div', { className: 'detail-progress-content', tabIndex: 0, 'aria-label': '当前进展记录,可滚动查看' },
820
+ React.createElement('div', { className: 'detail-overall-progress' }, React.createElement('h3', null, overall.heading), React.createElement('p', { className: 'detail-progress-state' }, overall.description), React.createElement('p', { className: 'detail-muted' }, `整体进展记录时间 ${time(overall.recordedAt)}`), overall.updating ? React.createElement('p', { className: 'detail-muted' }, '统筹正在更新;以上为最近已确认进展,不代表实时工作。') : null, React.createElement('p', { className: 'detail-muted' }, `任务状态记录更新 ${time(task.updatedAt)}`), overall.missing ? React.createElement('p', { className: 'detail-muted' }, '当前没有可确认的整体工作说明,请在任务会话中查看。') : null),
821
+ overall.lastRecord && !overall.recordedAt ? React.createElement('details', { className: 'detail-progress-note' }, React.createElement('summary', null, overall.lastRecord.verified ? '最近统筹说明(不代表当前工作)' : '历史统筹说明(当前版本或轮次未核验)'), React.createElement('p', null, traceReason(overall.lastRecord.summary)), React.createElement('p', { className: 'detail-muted' }, `记录时间 ${time(overall.lastRecord.at)}`)) : null,
822
+ view.executions.length ? React.createElement('details', { className: 'detail-progress-note' }, React.createElement('summary', null, `辅助:执行记录(${view.executions.length} 项)`), React.createElement('p', { className: 'detail-muted' }, '以下是执行事项与会话活动记录,不代表任务整体当前步骤。'), React.createElement('ul', { className: 'detail-progress-list' }, ...view.executions.map(item => React.createElement('li', { key: item.executionId },
823
+ React.createElement('div', { className: 'detail-progress-heading' }, React.createElement('strong', null, item.input?.title || item.key || '执行事项'), statusTag(item.label, item.running ? 'ongoing' : view.paused || item.outdated || item.cancelRequested || item.recovering ? 'warning' : taskStateTone(item.status)), React.createElement(SessionLocation, { session: item, label: '执行会话', onOpenSession, entryOnly: true, actionLabel: `打开执行会话 ${item.input?.title || item.key || item.executionId}` })),
824
+ React.createElement('p', { className: 'detail-objective' }, item.objective),
825
+ React.createElement('p', { className: 'detail-muted' }, `最近活动 ${time(item.progressReview?.lastActivityAt)} · 进展检查 ${time(item.progressReview?.checkedAt)}`),
826
+ React.createElement('p', { className: 'detail-muted' }, item.progressReview?.readError ? `进展记录读取失败:${traceReason(item.progressReview.readError)}` : Number.isSafeInteger(item.progressReview?.newEventCount) && item.progressReview.newEventCount >= 0 ? `本次检查新增 ${item.progressReview.newEventCount} 条会话事件。` : '尚无可读的当前步骤摘要。'),
827
+ React.createElement('details', null, React.createElement('summary', null, '会话与记录信息'), React.createElement('p', { className: 'detail-muted' }, `执行记录更新 ${time(item.updatedAt)} · 会话 ${item.sessionId || '尚未建立'}`)))))) : null))
828
+ }
829
+ const taskDetailCss = `
830
+ [data-dingtalk-observer] .observer-session-detail { width: 100%; box-sizing: border-box; max-width: 1100px; margin: 0 auto; padding-bottom: 24px; display: grid; gap: 24px; font-size: 14px; line-height: 1.65; overflow-wrap: anywhere; }
831
+ .observer-session-detail :is(h1, h2, h3, p, ul) { margin: 0; }
832
+ .observer-session-detail h1 { font-size: 24px; line-height: 1.4; font-weight: 650; }
833
+ .observer-session-detail h2 { font-size: 16px; line-height: 1.5; font-weight: 600; }
834
+ .observer-session-detail h3 { font-size: 14px; font-weight: 600; }
835
+ .observer-session-detail ul { padding-left: 20px; }
836
+ .observer-session-detail li + li { margin-top: 4px; }
837
+ .observer-session-detail .detail-header { display: grid; gap: 12px; padding-bottom: 24px; border-bottom: 1px solid ${colors.border}; }
838
+ .observer-session-detail .detail-topline { display: flex; gap: 12px; align-items: center; }
839
+ .observer-session-detail .detail-title-row { display: flex; gap: 12px; align-items: baseline; flex-wrap: wrap; }
840
+ .observer-session-detail .detail-meta { display: flex; gap: 8px 20px; flex-wrap: wrap; color: ${colors.muted}; font-size: 12px; font-variant-numeric: tabular-nums; }
841
+ .observer-session-detail .detail-muted { color: ${colors.muted}; font-size: 12px; }
842
+ .observer-session-detail .detail-columns { display: grid; grid-template-columns: minmax(0, 1fr) 288px; gap: 24px; align-items: start; }
843
+ .observer-session-detail :is(.detail-primary, .detail-sidebar) { display: grid; gap: 16px; min-width: 0; }
844
+ .observer-session-detail :is(.detail-panel, .detail-session, .detail-records) { padding: 16px; border: 1px solid ${colors.border}; border-radius: 12px; background: ${colors.surface}; min-width: 0; }
845
+ .observer-session-detail :is(.detail-panel, .detail-session) { display: grid; gap: 12px; }
846
+ .observer-session-detail .detail-objective { white-space: pre-wrap; }
847
+ .observer-session-detail .detail-requirements { display: grid; gap: 8px; padding-top: 12px; border-top: 1px solid ${colors.border}; }
848
+ .observer-session-detail .observer-task-result { display: grid; gap: 12px; margin: 0; padding: 16px; border: 1px solid ${colors.border}; border-radius: 12px; background: ${colors.surface2}; }
849
+ /* The shared result heading has an inline margin; only this page uses grid gaps. */
850
+ .observer-session-detail .observer-task-result h2 { margin-bottom: 0 !important; }
851
+ .observer-session-detail .observer-task-result h3 { margin: 0 0 8px; }
852
+ .observer-session-detail .observer-task-result-text { max-height: 320px; overflow: auto; scrollbar-gutter: stable; }
853
+ .observer-session-detail .detail-attention { display: grid; gap: 8px; padding: 16px; border: 1px solid color-mix(in srgb, ${colors.warning} 32%, ${colors.border}); border-left: 3px solid ${colors.warning}; border-radius: 8px; background: color-mix(in srgb, ${colors.warning} 6%, ${colors.surface}); }
854
+ .observer-session-detail .detail-attention h2 { color: ${colors.warning}; }
855
+ .observer-session-detail .detail-attention button { justify-self: start; }
856
+ .observer-session-detail :is(summary, button, a) { cursor: pointer; }
857
+ .observer-session-detail button:disabled { cursor: default; }
858
+ .observer-session-detail summary { font-size: 12px; font-weight: 500; }
859
+ .observer-session-detail summary:hover { color: ${colors.accent}; }
860
+ .observer-session-detail summary:active { color: ${colors.accent}; }
861
+ .observer-session-detail .detail-disclosure { padding-top: 12px; border-top: 1px solid ${colors.border}; }
862
+ .observer-session-detail .detail-location { display: grid; gap: 8px; padding-top: 12px; font-size: 12px; color: ${colors.muted}; }
863
+ .observer-session-detail .detail-section-heading { display: flex; align-items: center; justify-content: space-between; gap: 12px; min-width: 0; }
864
+ .observer-session-detail .detail-section-heading h2 { flex-shrink: 0; }
865
+ .observer-session-detail .detail-executions { border-top: 1px solid ${colors.border}; padding-top: 24px; }
866
+ .observer-session-detail .detail-execution-list { list-style: none; padding: 0; margin: 0; display: grid; gap: 8px; }
867
+ .observer-session-detail .detail-execution { border: 1px solid ${colors.border}; border-radius: 8px; background: ${colors.surface}; overflow-wrap: anywhere; }
868
+ .observer-session-detail .detail-execution-summary { padding: 12px 16px; display: grid; grid-template-columns: 12px minmax(0, 1fr) auto; gap: 4px 12px; align-items: baseline; list-style: none; }
869
+ .observer-session-detail .detail-execution-summary::-webkit-details-marker { display: none; }
870
+ .observer-session-detail .detail-execution-summary::before { content: '›'; grid-row: 1 / 3; font-size: 18px; line-height: 24px; color: ${colors.muted}; }
871
+ .observer-session-detail details[open] > .detail-execution-summary::before { content: '⌄'; }
872
+ .observer-session-detail .detail-execution-summary:hover { background: ${colors.surface2}; border-radius: 8px; }
873
+ .observer-session-detail .detail-execution-title { font-size: 14px; font-weight: 500; color: inherit; }
874
+ .observer-session-detail .detail-execution-time { grid-column: 2 / 4; font-size: 12px; color: ${colors.muted}; }
875
+ .observer-session-detail .detail-execution-body { padding: 16px; display: grid; gap: 16px; border-top: 1px solid ${colors.border}; }
876
+ .observer-session-detail .detail-pagination { display: grid; grid-template-columns: minmax(0, 1fr) auto auto; align-items: center; gap: 8px; min-height: 36px; }
877
+ .observer-session-detail .detail-execution-meta { display: flex; flex-wrap: wrap; align-items: center; gap: 4px 12px; min-height: 44px; }
878
+ .observer-session-detail .detail-execution-meta > span { white-space: nowrap; }
879
+ .observer-session-detail .detail-pagination > span { min-width: 0; overflow-wrap: anywhere; font-size: 12px; }
880
+ .observer-session-detail .detail-execution-feedback { height: 36px; flex: 1; min-width: 0; max-width: 520px; display: grid; grid-template-columns: minmax(0, 1fr) auto; gap: 8px; align-items: center; }
881
+ .observer-session-detail .detail-execution-feedback > span { grid-column: 1; grid-row: 1; min-width: 0; max-height: 36px; overflow: auto; font-size: 12px; line-height: 18px; }
882
+ .observer-session-detail .detail-execution-feedback > button { grid-column: 2; grid-row: 1; }
883
+ .observer-session-detail .detail-execution-error { color: ${colors.danger}; }
884
+ .observer-session-detail .detail-progress { height: 320px; grid-template-rows: 24px minmax(0, 1fr); }
885
+ .observer-session-detail .detail-progress-content { min-height: 0; overflow: auto; scrollbar-gutter: stable; display: grid; gap: 8px; align-content: start; }
886
+ .observer-session-detail .detail-progress-state { font-weight: 500; }
887
+ .observer-session-detail .detail-overall-progress { display: grid; gap: 8px; }
888
+ .observer-session-detail .detail-progress-heading { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; }
889
+ .observer-session-detail .detail-progress-heading > strong { margin-right: auto; }
890
+ .observer-session-detail .detail-progress-list { list-style: none; padding: 0; display: grid; gap: 12px; }
891
+ .observer-session-detail .detail-progress-list > li { margin: 0; padding-top: 12px; border-top: 1px solid ${colors.border}; display: grid; gap: 4px; }
892
+ .observer-session-detail .detail-progress-list button { justify-self: start; margin-top: 4px; }
893
+ .observer-session-detail .detail-progress-note { border-top: 1px solid ${colors.border}; padding-top: 8px; }
894
+ .observer-task-detail-route .detail-fetch-error { padding: 16px; border: 1px solid ${colors.danger}; border-radius: 8px; display: flex; gap: 12px; align-items: center; flex-wrap: wrap; color: ${colors.danger}; font-size: 14px; line-height: 1.65; }
895
+ .observer-task-detail-route .detail-fetch-state { min-height: 200px; align-content: start; }
896
+ .observer-session-detail .detail-timeline { min-width: 0; padding: 14px 16px 8px; gap: 8px; font-family: Inter, 'Segoe UI', 'PingFang SC', 'Microsoft YaHei UI', 'Noto Sans CJK SC', system-ui, sans-serif; -webkit-font-smoothing: antialiased; }
897
+ .observer-session-detail .timeline-heading { display: flex; flex-wrap: wrap; gap: 8px; align-items: center; justify-content: space-between; padding-bottom: 4px; }
898
+ .observer-session-detail .timeline-title { display: flex; align-items: center; gap: 8px; }
899
+ .observer-session-detail .timeline-heading h2 { font-size: 17px; font-weight: 600; line-height: 26px; letter-spacing: -.2px; }
900
+ .observer-session-detail .timeline-count { font-size: 11px; line-height: 20px; padding: 0 7px; border-radius: 4px; background: ${colors.surface2}; color: ${colors.muted}; font-weight: 400; }
901
+ .observer-session-detail .timeline-date { font-variant-numeric: tabular-nums; font-size: 11px; color: ${colors.muted}; }
902
+ .observer-session-detail .timeline-heading > span, .observer-session-detail .timeline-note { font-size: 12px; }
903
+ .observer-session-detail .timeline-note { font-size: 11px; line-height: 16px; }
904
+ .observer-session-detail .timeline-navigator { min-width: 0; padding-bottom: 2px; }
905
+ .observer-session-detail .timeline-range-caption { display: flex; align-items: center; justify-content: space-between; gap: 8px; min-height: 26px; color: ${colors.muted}; font-size: 11px; font-variant-numeric: tabular-nums; }
906
+ .observer-session-detail .timeline-range-caption button { flex: none; color: inherit; background: transparent; border: 0; font: inherit; padding: 4px; border-radius: 4px; }
907
+ .observer-session-detail .timeline-range-caption button:disabled { opacity: .4; }
908
+ .observer-session-detail .timeline-range-caption button:not(:disabled):hover { color: ${colors.accent}; background: ${colors.surface2}; }
909
+ .observer-session-detail .timeline-overview { position: relative; height: 32px; margin: 4px 9px 0; background: ${colors.surface2}; border-radius: 4px; }
910
+ .observer-session-detail .timeline-overview-activity { position: absolute; height: 3px; min-width: 1px; opacity: .35; border-radius: 1px; pointer-events: none; }
911
+ .observer-session-detail .timeline-range-window { position: absolute; top: 0; bottom: 0; box-sizing: border-box; background: color-mix(in srgb, #5968cf 12%, transparent); border: 1px solid #7d89df; border-radius: 4px; }
912
+ .observer-session-detail .timeline-range-window button { position: absolute; top: -1px; bottom: -1px; touch-action: none; color: #5968cf; font: inherit; border: 0; border-radius: 3px; padding: 0; }
913
+ .observer-session-detail .timeline-range-pan { left: 0; width: 100%; background: transparent; cursor: grab; font-size: 11px !important; }
914
+ .observer-session-detail .timeline-range-pan:active { cursor: grabbing; }
915
+ .observer-session-detail :is(.timeline-range-left, .timeline-range-right) { width: 18px; background: transparent; cursor: ew-resize; }
916
+ .observer-session-detail :is(.timeline-range-left, .timeline-range-right) > span { position: absolute; left: 50%; top: 50%; transform: translate(-50%, -50%); box-sizing: border-box; width: 5px; height: 18px; border: 1px solid #7d89df; border-radius: 1px; background: ${colors.surface}; }
917
+ .observer-session-detail :is(.timeline-range-left, .timeline-range-right):hover > span { border-color: #5968cf; background: color-mix(in srgb, #5968cf 12%, ${colors.surface}); }
918
+ .observer-session-detail .timeline-range-left { left: -9px; }
919
+ .observer-session-detail .timeline-range-right { right: -9px; }
920
+ .observer-session-detail .timeline-range-window .timeline-range-pan:hover { background: color-mix(in srgb, #5968cf 25%, transparent); }
921
+ .observer-session-detail :is(.timeline-range-window button, .timeline-range-caption button):focus-visible { outline: 2px solid ${colors.accent}; outline-offset: 2px; }
922
+ .observer-session-detail .timeline-overview-labels { display: flex; justify-content: space-between; gap: 8px; margin: 3px 0 0; color: ${colors.muted}; font-size: 10px; font-variant-numeric: tabular-nums; }
923
+ .observer-session-detail .timeline-scroll { overflow-x: auto; padding-bottom: 4px; }
924
+ .observer-session-detail .timeline-scroll:focus-visible { outline: 2px solid ${colors.accent}; outline-offset: 2px; }
925
+ .observer-session-detail .timeline-table { border-collapse: separate; border-spacing: 0; width: 100%; min-width: 820px; table-layout: fixed; font-size: 12px; }
926
+ .observer-session-detail .timeline-table th { text-align: left; font-weight: 500; }
927
+ .observer-session-detail .timeline-table th:first-child { width: 140px; padding: 1px 16px 1px 0; overflow-wrap: anywhere; line-height: 18px; position: sticky; left: 0; z-index: 10; transform: translateZ(0); background: ${colors.surface}; }
928
+ .observer-session-detail .timeline-table thead th, .observer-session-detail .timeline-table tbody tr:first-child th { color: ${colors.muted}; font-size: 11px; font-weight: 400; }
929
+ .observer-session-detail .timeline-table th small { display: none; }
930
+ .observer-session-detail .timeline-name { display: block; position: relative; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; font-size: 13px; font-weight: 500; line-height: 20px; padding-left: 14px; }
931
+ .observer-session-detail .timeline-name::before { content: ''; position: absolute; left: 1px; top: 7px; width: 6px; height: 6px; border-radius: 50%; background: var(--timeline-lane-color); }
932
+ .observer-session-detail .timeline-table tbody tr:last-child .timeline-name::before { border-radius: 2px; }
933
+ .observer-session-detail .timeline-table th:is(:hover, :focus) { z-index: 12; }
934
+ .observer-session-detail .timeline-table th:focus-visible { outline: 2px solid ${colors.accent}; outline-offset: -2px; }
935
+ .observer-session-detail .timeline-table td { padding: 0; }
936
+ .observer-session-detail .timeline-table tbody tr:first-child .timeline-track { margin-bottom: 3px; }
937
+ .observer-session-detail .timeline-table tbody tr:last-child .timeline-track { margin-top: 3px; }
938
+ .observer-session-detail .timeline-axis { position: relative; height: 24px; margin: 0 4px; color: ${colors.muted}; font-weight: 500; font-size: 12px; font-variant-numeric: tabular-nums; }
939
+ .observer-session-detail .timeline-axis span { position: absolute; top: 1px; transform: translateX(-50%); width: 110px; text-align: center; font-variant-numeric: tabular-nums; }
940
+ .observer-session-detail .timeline-axis span:first-child { transform: none; text-align: left; }
941
+ .observer-session-detail .timeline-axis span:last-child { transform: translateX(-100%); text-align: right; }
942
+ .observer-session-detail .timeline-table tbody tr { --timeline-lane-color: #5968cf; }
943
+ .observer-session-detail .timeline-table tbody tr:last-child { --timeline-lane-color: #b48125; }
944
+ .observer-session-detail .timeline-track { height: 34px; position: relative; margin: 0 4px; background: repeating-linear-gradient(to right, color-mix(in srgb, ${colors.border} 30%, transparent) 0, color-mix(in srgb, ${colors.border} 30%, transparent) 1px, transparent 1px, transparent 25%); }
945
+ .observer-session-detail .timeline-bar { position: absolute; top: 6px; height: 22px; min-width: 2px; box-sizing: border-box; border: 0; border-radius: 5px; background: color-mix(in srgb, var(--timeline-lane-color) 20%, ${colors.surface}); color: color-mix(in srgb, var(--timeline-lane-color) 55%, currentColor); overflow: hidden; }
946
+ .observer-session-detail .timeline-bar > span { display: flex; align-items: center; gap: 8px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; padding: 1px 8px; font-size: 12px; font-weight: 500; line-height: 16px; }
947
+ .observer-session-detail .timeline-bar { container-type: inline-size; }
948
+ .observer-session-detail .timeline-activity { overflow: hidden; text-overflow: ellipsis; flex: 1; min-width: 0; }
949
+ .observer-session-detail .timeline-duration { flex: none; font-size: 11px; font-variant-numeric: tabular-nums; }
950
+ @container (max-width: 130px) { .observer-session-detail .timeline-activity { display: none; } .observer-session-detail .timeline-bar > span { gap: 0; padding-inline: 4px; } }
951
+ .observer-session-detail .timeline-bar:focus-visible { outline: 2px solid ${colors.accent}; outline-offset: 2px; }
952
+ .observer-session-detail .timeline-milestone { position: absolute; top: 6px; transform: translateX(-50%); width: 18px; height: 22px; padding: 0; border: 0; background: transparent; color: var(--timeline-lane-color); cursor: help; z-index: 3; }
953
+ .observer-session-detail .timeline-milestone-tick { position: absolute; left: 50%; top: -3px; transform: translateX(-50%); width: 1px; height: 28px; background: currentColor; opacity: .8; }
954
+ .observer-session-detail .timeline-milestone:is(:hover, :focus-visible) { outline: 1px solid currentColor; border-radius: 4px; }
955
+ .observer-session-detail .timeline-total-track { height: 40px; }
956
+ .observer-session-detail .timeline-total { top: 9px; height: 22px; border: 0; border-radius: 5px; background: color-mix(in srgb, ${colors.muted} 12%, ${colors.surface}); color: inherit; }
957
+ .observer-session-detail .timeline-table th small.timeline-total-missing { display: block; font-size: 10px; line-height: 14px; font-weight: 400; color: ${colors.muted}; }
958
+ .observer-session-detail .timeline-muted { border-color: ${colors.muted}; }
959
+ .observer-session-detail .timeline-empty { position: absolute; top: 8px; left: 4px; color: ${colors.muted}; background: ${colors.surface2}; border-radius: 4px; padding: 1px 6px; font-size: 11px; line-height: 16px; }
960
+ .observer-session-detail .timeline-details { font-size: 12px; overflow-wrap: anywhere; padding-top: 6px; }
961
+ .observer-session-detail .timeline-details > summary { min-height: 28px; line-height: 28px; color: ${colors.muted}; }
962
+ .observer-session-detail .timeline-details > summary:hover { color: inherit; }
963
+ .observer-session-detail .timeline-details section { margin-top: 16px; }
964
+ .observer-session-detail .timeline-details li { margin-top: 8px; }
965
+ .observer-session-detail .timeline-sr { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip-path: inset(50%); white-space: nowrap; }
966
+ @media (max-width: 800px) { .observer-session-detail .detail-columns { grid-template-columns: minmax(0, 1fr); gap: 16px; } .observer-session-detail .detail-sidebar { grid-template-columns: repeat(2, minmax(0, 1fr)); align-items: start; } }
967
+ @media (max-width: 520px) { [data-dingtalk-observer] .observer-session-detail { gap: 16px; } .observer-session-detail .detail-sidebar { grid-template-columns: minmax(0, 1fr); } .observer-session-detail .detail-header { padding-bottom: 16px; } .observer-session-detail .detail-title-row { align-items: start; } .observer-session-detail .detail-execution-summary { padding: 12px; gap: 4px 8px; } }
968
+ `
969
+ function TaskExecutionTimeline({ timeline }) {
970
+ const [now, setNow] = useState(Date.now)
971
+ const tooltipRef = useRef(null), tooltipTarget = useRef(null)
972
+ const [windowRange, setWindowRange] = useState(null)
973
+ const dragRange = useRef(null), overviewRef = useRef(null)
974
+ const hideTooltip = () => {
975
+ const node = tooltipRef.current
976
+ if (!node) return
977
+ if (node.matches(':popover-open')) node.hidePopover()
978
+ node.hidden = true
979
+ }
980
+ useEffect(() => {
981
+ const onScroll = event => {
982
+ if (event.target?.nodeType && tooltipRef.current?.contains(event.target)) return
983
+ if (tooltipTarget.current && document.activeElement === tooltipTarget.current) placeTooltip({ currentTarget: tooltipTarget.current })
984
+ else hideTooltip()
985
+ }
986
+ const onKey = event => { if (event.key === 'Escape') hideTooltip() }
987
+ window.addEventListener('scroll', onScroll, true)
988
+ window.addEventListener('resize', hideTooltip)
989
+ window.addEventListener('keydown', onKey)
990
+ return () => {
991
+ window.removeEventListener('scroll', onScroll, true)
992
+ window.removeEventListener('resize', hideTooltip)
993
+ window.removeEventListener('keydown', onKey)
994
+ tooltipRef.current?.remove()
995
+ tooltipRef.current = null
996
+ tooltipTarget.current = null
997
+ }
998
+ }, [])
999
+ const live = timeline?.task?.active || [...(timeline?.executions || []), timeline?.human].some(row => row?.segments?.some(segment => segment.active))
1000
+ useEffect(() => {
1001
+ if (!live) return
1002
+ setNow(Date.now())
1003
+ const timer = window.setInterval(() => setNow(Date.now()), 1000)
1004
+ return () => window.clearInterval(timer)
1005
+ }, [live, timeline])
1006
+ if (!timeline) return React.createElement('section', { className: 'detail-panel detail-timeline', 'aria-label': '执行时间泳道图' }, React.createElement('h2', null, '执行时间'), React.createElement('p', { className: 'detail-muted' }, '历史执行过程未记录。'))
1007
+ const rows = [...timeline.executions, timeline.human]
1008
+ const totalSegments = timeline.task.segments || [{ ...timeline.task, id: 'total', label: timeline.task.active ? '处理中' : timeline.task.status === 'completed' ? '已完成' : '已结束' }]
1009
+ const totalEvents = timeline.task.events || []
1010
+ const dates = [timeline.task.startedAt, timeline.task.endedAt, ...totalSegments.flatMap(segment => [segment.startedAt, segment.endedAt]), ...totalEvents.map(event => event.at), ...rows.flatMap(row => [...row.segments.flatMap(segment => [segment.startedAt, segment.endedAt]), ...row.milestones.map(event => event.at)])].map(value => value ? Date.parse(value) : NaN).filter(Number.isFinite)
1011
+ if (live) dates.push(now)
1012
+ const fullStart = dates.length ? Math.min(...dates) : now
1013
+ const fullEnd = Math.max(fullStart + 1000, ...dates)
1014
+ const minSpan = Math.min(1000, fullEnd - fullStart)
1015
+ const clampRange = (from, to) => {
1016
+ const span = Math.max(minSpan, Math.min(fullEnd - fullStart, to - from))
1017
+ const left = Math.max(fullStart, Math.min(from, fullEnd - span))
1018
+ return [left, left + span]
1019
+ }
1020
+ const [start, end] = windowRange ? clampRange(...windowRange) : [Math.max(fullStart, fullEnd - 3600000), fullEnd]
1021
+ const position = date => Math.max(0, Math.min(100, (Date.parse(date) - start) / (end - start) * 100))
1022
+ const overviewPosition = value => (value - fullStart) / (fullEnd - fullStart) * 100
1023
+ const axisDate = fraction => start + (end - start) * fraction
1024
+ const selectRange = (kind, delta, snapshot = [start, end]) => {
1025
+ const [from, to] = snapshot
1026
+ hideTooltip()
1027
+ setWindowRange(kind === 'left' ? [Math.max(fullStart, Math.min(to - minSpan, from + delta)), to]
1028
+ : kind === 'right' ? [from, Math.min(fullEnd, Math.max(from + minSpan, to + delta))]
1029
+ : clampRange(from + delta, to + delta))
1030
+ }
1031
+ const beginRangeDrag = (event, kind) => {
1032
+ if (event.button !== 0) return
1033
+ event.preventDefault()
1034
+ const width = overviewRef.current.getBoundingClientRect().width
1035
+ dragRange.current = { kind, x: event.clientX, range: [start, end], scale: (fullEnd - fullStart) / width }
1036
+ event.currentTarget.setPointerCapture(event.pointerId)
1037
+ }
1038
+ const moveRangeDrag = event => {
1039
+ const drag = dragRange.current
1040
+ if (drag) selectRange(drag.kind, (event.clientX - drag.x) * drag.scale, drag.range)
1041
+ }
1042
+ const endRangeDrag = () => { dragRange.current = null }
1043
+ const rangeKeyboard = (event, kind) => {
1044
+ const step = Math.max(1000, (fullEnd - fullStart) / (event.shiftKey ? 10 : 100))
1045
+ let delta
1046
+ if (event.key === 'ArrowLeft') delta = -step
1047
+ else if (event.key === 'ArrowRight') delta = step
1048
+ else if (event.key === 'Home') delta = fullStart - (kind === 'right' ? end : start)
1049
+ else if (event.key === 'End') delta = fullEnd - (kind === 'left' ? start : end)
1050
+ else return
1051
+ event.preventDefault()
1052
+ selectRange(kind, delta)
1053
+ }
1054
+ const rangeControl = (kind, label, extra = {}) => ({
1055
+ type: 'button', className: `timeline-range-${kind}`, role: 'slider', 'aria-label': label,
1056
+ 'aria-valuemin': kind === 'right' ? start + minSpan : fullStart,
1057
+ 'aria-valuemax': kind === 'left' ? end - minSpan : kind === 'pan' ? fullEnd - (end - start) : fullEnd,
1058
+ 'aria-valuenow': kind === 'right' ? end : start,
1059
+ 'aria-valuetext': `${fmt(new Date(start).toISOString())} 至 ${fmt(new Date(end).toISOString())}`,
1060
+ onPointerDown: event => beginRangeDrag(event, kind), onPointerMove: moveRangeDrag,
1061
+ onPointerUp: endRangeDrag, onPointerCancel: endRangeDrag, onLostPointerCapture: endRangeDrag,
1062
+ onKeyDown: event => rangeKeyboard(event, kind), ...extra,
1063
+ })
1064
+ const time = value => value ? fmt(value) : '未记录'
1065
+ const sameDay = new Date(start).toDateString() === new Date(end).toDateString()
1066
+ const navTime = value => new Date(value).toLocaleTimeString('zh-CN', { hour12: false })
1067
+ const overviewTime = value => new Date(value).toLocaleString('zh-CN', { month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit', hour12: false })
1068
+ const dateLabel = new Date(start).toLocaleDateString('zh-CN') + (sameDay ? '' : ` — ${new Date(end).toLocaleDateString('zh-CN')}`)
1069
+ const status = value => ({ completed: '已完成', recorded: '已记录', running: '执行中', queued: '排队中', succeeded: '已成功', failed: '失败', stale: '已失效', cancelled: '已取消', 'waiting-reply': '等待处理', answered: '已答复', superseded: '已失效', closed: '已关闭' })[value] || value
1070
+ const range = segment => `${time(segment.startedAt)} — ${segment.active ? '当前' : time(segment.endedAt)}`
1071
+ function placeTooltip(event) {
1072
+ const target = event.currentTarget, bounds = target.getBoundingClientRect()
1073
+ tooltipTarget.current = target
1074
+ let node = tooltipRef.current
1075
+ if (!node) {
1076
+ node = document.createElement('div')
1077
+ node.className = 'timeline-tooltip'
1078
+ node.setAttribute('role', 'tooltip')
1079
+ node.setAttribute('popover', 'manual')
1080
+ node.addEventListener('mouseleave', hideTooltip)
1081
+ document.body.appendChild(node)
1082
+ tooltipRef.current = node
1083
+ }
1084
+ const theme = window.getComputedStyle(target.closest('.detail-timeline'))
1085
+ node.replaceChildren()
1086
+ let content
1087
+ try { content = JSON.parse(target.dataset.tooltip || 'null') } catch {}
1088
+ const addText = (parent, value, className, style) => {
1089
+ if (!value) return
1090
+ const child = document.createElement('div')
1091
+ child.className = className
1092
+ child.textContent = value
1093
+ Object.assign(child.style, style)
1094
+ parent.appendChild(child)
1095
+ }
1096
+ if (content) {
1097
+ const heading = document.createElement('div')
1098
+ heading.style.display = 'flex'
1099
+ heading.style.alignItems = 'baseline'
1100
+ heading.style.gap = '8px'
1101
+ heading.style.flexWrap = 'wrap'
1102
+ node.appendChild(heading)
1103
+ addText(heading, content.title, 'timeline-tooltip-heading', { fontWeight: '600', fontSize: '13px', lineHeight: '20px', flex: '1', minWidth: '0' })
1104
+ if (content.status && content.status !== content.title) addText(heading, content.status, 'timeline-tooltip-status', { fontSize: '11px', opacity: '.8', flex: 'none' })
1105
+ addText(node, content.meta, 'timeline-tooltip-meta', { marginTop: '4px', fontSize: '11px', lineHeight: '17px', fontVariantNumeric: 'tabular-nums', opacity: '.8' })
1106
+ addText(node, content.duration, 'timeline-tooltip-duration', { marginTop: '2px', fontSize: '11px', lineHeight: '17px', fontVariantNumeric: 'tabular-nums', opacity: '.8', whiteSpace: 'nowrap' })
1107
+ addText(node, content.body, 'timeline-tooltip-body', { marginTop: '8px' })
1108
+ for (const item of content.items || []) {
1109
+ const entry = document.createElement('div')
1110
+ entry.className = 'timeline-tooltip-entry'
1111
+ entry.style.marginTop = '8px'
1112
+ addText(entry, item.at, 'timeline-tooltip-meta', { fontSize: '11px', lineHeight: '17px', fontVariantNumeric: 'tabular-nums', opacity: '.8' })
1113
+ addText(entry, item.summary, 'timeline-tooltip-body', { marginTop: '2px' })
1114
+ node.appendChild(entry)
1115
+ }
1116
+ } else node.textContent = target.getAttribute('aria-label')
1117
+ node.hidden = false
1118
+ Object.assign(node.style, {
1119
+ position: 'fixed', inset: 'auto', margin: '0', boxSizing: 'border-box',
1120
+ width: `${Math.min(320, window.innerWidth - 16)}px`,
1121
+ maxHeight: `${window.innerHeight - 16}px`, overflowY: 'auto',
1122
+ padding: '10px 12px', border: '1px solid rgba(127,127,127,.35)',
1123
+ borderRadius: '4px', background: theme.backgroundColor, color: theme.color,
1124
+ fontFamily: theme.fontFamily, fontSize: '12px', lineHeight: '18px',
1125
+ whiteSpace: 'pre-line', overflowWrap: 'anywhere', boxShadow: '0 2px 8px rgba(0,0,0,.08)',
1126
+ zIndex: '1000', left: '8px', top: '8px',
1127
+ })
1128
+ if (typeof node.showPopover === 'function' && !node.matches(':popover-open')) node.showPopover()
1129
+ const width = node.getBoundingClientRect().width, height = node.getBoundingClientRect().height
1130
+ const below = bounds.bottom + 4
1131
+ node.style.left = `${Math.max(8, Math.min(bounds.left, window.innerWidth - width - 8))}px`
1132
+ node.style.top = `${Math.max(8, Math.min(below + height <= window.innerHeight - 8 ? below : bounds.top - height - 4, window.innerHeight - height - 8))}px`
1133
+ }
1134
+ const leaveTooltip = event => {
1135
+ if (!event.relatedTarget?.nodeType || !tooltipRef.current?.contains(event.relatedTarget)) hideTooltip()
1136
+ }
1137
+ const elapsed = segment => fmtDuration(Math.max(0, (segment.active ? now : Date.parse(segment.endedAt)) - Date.parse(segment.startedAt)))
1138
+ const describe = (segment, total, human) => `${segment.label || '任务执行'} · ${range(segment)} · ${total ? '本次处理历时' : human ? '人工处理等待' : '执行区段历时'} ${elapsed(segment)}${segment.active ? '(进行中)' : ''}`
1139
+ const markerLayout = row => {
1140
+ const groups = []
1141
+ for (const event of [...row.milestones].filter(event => Number.isFinite(Date.parse(event.at)) && Date.parse(event.at) >= start && Date.parse(event.at) <= end).sort((a, b) => Date.parse(a.at) - Date.parse(b.at))) {
1142
+ const group = groups.at(-1), at = position(event.at)
1143
+ if (group && Date.parse(event.at) === Date.parse(group.events[0].at)) group.events.push(event)
1144
+ else groups.push({ at, events: [event] })
1145
+ }
1146
+ return groups
1147
+ }
1148
+ const markers = row => markerLayout(row).map(group => {
1149
+ const nearby = row.milestones.filter(event => Number.isFinite(Date.parse(event.at)) && Date.parse(event.at) >= start && Date.parse(event.at) <= end && Math.abs(position(event.at) - group.at) <= 3).sort((a, b) => Date.parse(a.at) - Date.parse(b.at))
1150
+ const content = nearby.map(event => `${time(event.at)} · 阶段成果:${event.summary}`).join('\n')
1151
+ return React.createElement(React.Fragment, { key: group.events[0].id },
1152
+ React.createElement('button', { type: 'button', className: 'timeline-milestone', style: { left: `${group.at}%` }, 'data-recorded-at': group.events[0].at, 'data-tooltip': JSON.stringify({ title: '阶段成果', items: nearby.map(event => ({ at: time(event.at), summary: event.summary })) }), 'aria-label': content, onFocus: placeTooltip, onMouseEnter: placeTooltip, onMouseLeave: leaveTooltip, onBlur: hideTooltip },
1153
+ React.createElement('span', { className: 'timeline-milestone-tick', 'aria-hidden': true })))
1154
+ })
1155
+ const bar = (segment, total = false, human = false) => {
1156
+ if (!segment.startedAt || !segment.active && !segment.endedAt) return null
1157
+ const segmentEnd = segment.active ? Math.max(now, Date.parse(segment.startedAt)) : Date.parse(segment.endedAt)
1158
+ if (segmentEnd < start || Date.parse(segment.startedAt) > end) return null
1159
+ const left = position(segment.startedAt), right = position(segment.active ? new Date(Math.max(now, Date.parse(segment.startedAt))).toISOString() : segment.endedAt)
1160
+ const description = describe(segment, total, human)
1161
+ return React.createElement(React.Fragment, { key: segment.id || 'total' },
1162
+ React.createElement('span', { className: `timeline-bar${total ? ' timeline-total' : ''}${['failed','cancelled','superseded','stale'].includes(segment.status) ? ' timeline-muted' : ''}`,
1163
+ style: { left: `${left}%`, width: `${Math.max(0, right - left)}%` }, tabIndex: 0, role: 'img', 'data-tooltip': JSON.stringify({ title: segment.label || '任务执行', status: segment.status ? status(segment.status) : null, meta: range(segment), duration: `${total ? '本次处理历时' : human ? '等待历时' : '执行历时'} ${elapsed(segment)}`, body: segment.description }), 'aria-label': description, onFocus: placeTooltip, onMouseEnter: placeTooltip, onMouseLeave: leaveTooltip, onBlur: hideTooltip },
1164
+ React.createElement('span', null, React.createElement('span', { className: 'timeline-activity' }, segment.label || '任务执行'), React.createElement('span', { className: 'timeline-duration' }, elapsed(segment)))))
1165
+ }
1166
+ return React.createElement('section', { className: 'detail-panel detail-timeline', 'aria-label': '执行时间泳道图' },
1167
+ React.createElement('div', { className: 'timeline-heading' }, React.createElement('div', { className: 'timeline-title' }, React.createElement('h2', null, '执行时间'), React.createElement('span', { className: 'timeline-count' }, `${timeline.executions.length} 个会话`)), React.createElement('span', { className: 'timeline-date', title: '时间按浏览器本地时区显示' }, dateLabel)),
1168
+ React.createElement('div', { className: 'timeline-navigator', 'aria-label': '时间范围导航器', 'data-full-start': fullStart, 'data-full-end': fullEnd, 'data-start': start, 'data-end': end },
1169
+ React.createElement('div', { className: 'timeline-range-caption' },
1170
+ React.createElement('span', null, `${navTime(start)} — ${navTime(end)} · ${fmtDuration(end - start)}`),
1171
+ React.createElement('button', { type: 'button', disabled: !windowRange, onClick: () => { hideTooltip(); setWindowRange(null) } }, '回到最新')),
1172
+ React.createElement('div', { className: 'timeline-overview', ref: overviewRef },
1173
+ ...rows.flatMap((row, rowIndex) => row.segments.map(part => React.createElement('span', { key: `${row.id}:${part.id}`, className: 'timeline-overview-activity', style: {
1174
+ left: `${overviewPosition(Date.parse(part.startedAt))}%`, width: `${Math.max(0, overviewPosition(part.active ? now : Date.parse(part.endedAt)) - overviewPosition(Date.parse(part.startedAt)))}%`,
1175
+ top: `${7 + rowIndex % 4 * 5}px`, background: row.id === 'human' ? '#b48125' : '#5968cf',
1176
+ }, 'aria-hidden': true }))),
1177
+ React.createElement('div', { className: 'timeline-range-window', style: { left: `${overviewPosition(start)}%`, width: `${overviewPosition(end) - overviewPosition(start)}%` } },
1178
+ React.createElement('button', rangeControl('pan', '平移时间范围'), React.createElement('span', { 'aria-hidden': true }, '⋮⋮')),
1179
+ React.createElement('button', rangeControl('left', '调整开始时间'), React.createElement('span', { 'aria-hidden': true })),
1180
+ React.createElement('button', rangeControl('right', '调整结束时间'), React.createElement('span', { 'aria-hidden': true })))),
1181
+ React.createElement('div', { className: 'timeline-overview-labels', 'aria-hidden': true }, React.createElement('span', null, overviewTime(fullStart)), React.createElement('span', null, overviewTime(fullEnd)))),
1182
+ React.createElement('div', { className: 'timeline-scroll', role: 'region', tabIndex: 0, 'aria-label': '时间泳道图,可横向滚动;完整活动与时间见下方明细' },
1183
+ React.createElement('table', { className: 'timeline-table' }, React.createElement('caption', { className: 'timeline-sr' }, '任务、执行会话与人工介入时间,区段之间保留空档'),
1184
+ React.createElement('thead', null, React.createElement('tr', null, React.createElement('th', { scope: 'col' }, '会话 / 时间'), React.createElement('th', { scope: 'col' }, React.createElement('div', { className: 'timeline-axis' }, ...[0, 1, 2, 3, 4].map(index => React.createElement('span', { key: index, style: { left: `${index * 25}%` } }, new Date(axisDate(index / 4)).toLocaleString('zh-CN', { ...(sameDay ? {} : { month: '2-digit', day: '2-digit' }), hour: '2-digit', minute: '2-digit', second: '2-digit', hour12: false }))))))),
1185
+ React.createElement('tbody', null,
1186
+ React.createElement('tr', null, React.createElement('th', { scope: 'row' }, '任务执行', timeline.task.complete === false ? React.createElement('small', { className: 'timeline-total-missing' }, '部分历史未记录') : null), React.createElement('td', null, React.createElement('div', { className: 'timeline-track timeline-total-track' }, ...totalSegments.map(segment => bar(segment, true)), !totalSegments.length ? React.createElement('span', { className: 'timeline-empty' }, '完成 / 重开时间未完整记录') : null), React.createElement('span', { className: 'timeline-sr' }, ...totalSegments.map(segment => `${segment.label} · ${range(segment)};`)))),
1187
+ ...rows.map(row => React.createElement('tr', { key: row.id }, React.createElement('th', { scope: 'row', tabIndex: 0, onFocus: placeTooltip, onMouseEnter: placeTooltip, onMouseLeave: leaveTooltip, onBlur: hideTooltip, 'data-tooltip': JSON.stringify({ title: row.title, status: row.status ? status(row.status) : null, body: row.complete ? null : '部分区间未记录' }), 'aria-label': `${row.title}${row.status ? ` · ${status(row.status)}` : ''}${row.complete ? '' : ' · 部分区间未记录'}` }, React.createElement('span', { className: 'timeline-name' }, row.title), row.status ? React.createElement('small', null, status(row.status)) : null, !row.complete ? React.createElement('small', null, '部分区间未记录') : null),
1188
+ React.createElement('td', null, React.createElement('div', { className: 'timeline-track' }, ...row.segments.map(segment => bar(segment, false, row.id === 'human')), ...markers(row),
1189
+ !row.segments.length ? React.createElement('span', { className: 'timeline-empty' }, row.complete ? row.id === 'human' ? '暂无人工介入' : '尚未开始执行' : '历史区间未记录') : null),
1190
+ React.createElement('span', { className: 'timeline-sr' }, `${row.segments.length} 个区段${row.complete ? '' : ',部分历史区间未记录'}`))))))),
1191
+ React.createElement('details', { className: 'detail-disclosure timeline-details' }, React.createElement('summary', null, '活动与时间明细'),
1192
+ React.createElement('p', { className: 'detail-muted timeline-note' }, '靛蓝为执行会话,金色为人工介入。贯穿色带的细刻线为阶段成果,悬停或聚焦查看附近成果的准确时间;未记录区间不推算,人工处理不表示批准。'),
1193
+ React.createElement('section', null, React.createElement('h3', null, '任务完成与重开'), timeline.task.complete === false ? React.createElement('p', null, '部分任务历史时间未记录,不推算完成或重开的时间。') : null,
1194
+ totalEvents.length ? React.createElement('ul', null, ...totalEvents.map((event, index) => React.createElement('li', { key: `${event.kind}:${index}` }, `${time(event.at)} · 第 ${event.sequence} 次处理 · ${({ created: '创建任务', completed: '任务完成', reopened: '重新打开', cancelled: '取消任务' })[event.kind] || event.kind}`))) : React.createElement('p', null, '任务历史时间未完整记录。')),
1195
+ ...rows.map(row => React.createElement('section', { key: row.id }, React.createElement('h3', null, `${row.title}${row.status ? ` · ${status(row.status)}` : ''}`), !row.complete ? React.createElement('p', null, '部分历史区间未记录;恢复时未知的结束时间不作推算。') : null,
1196
+ row.segments.length || row.milestones.length ? React.createElement('ul', null, ...row.segments.map(segment => React.createElement('li', { key: segment.id }, `${segment.label} · ${range(segment)} · ${elapsed(segment)} · ${status(segment.status)}`, segment.description ? React.createElement('p', null, segment.description) : null)), ...row.milestones.map(event => React.createElement('li', { key: event.id }, `${time(event.at)} · 阶段成果:${event.summary}`))) : React.createElement('p', null, row.complete ? row.id === 'human' ? '暂无人工介入' : '尚未开始执行' : '历史执行过程未记录。')))))
1197
+ }
1198
+ function TaskSessionDetail({ task, onOpenSession, onBack, updatedAt, onOpenHumanRequest }) {
1199
+ const requirements = task.requirements || task.acceptanceCriteria || []
1200
+ const humanRequest = !task.legacyReadonly && task.humanRequests?.findLast(request => request.status === 'waiting-reply')
1201
+ const question = !task.legacyReadonly && (humanRequest?.question || task.lastDecision?.question)
1202
+ return React.createElement('section', { className: 'observer-session-detail', 'aria-label': '任务会话详情' },
1203
+ React.createElement('style', null, taskDetailCss),
1204
+ React.createElement('header', { className: 'detail-header' },
1205
+ React.createElement('div', { className: 'detail-topline' }, React.createElement(Button, { type: 'button', variant: 'outline', size: 'sm', onClick: onBack }, '返回看板'), React.createElement('span', { className: 'detail-muted' }, '任务详情')),
1206
+ React.createElement('div', { className: 'detail-title-row' }, React.createElement('h1', null, task.title?.trim() || taskDisplayTitle(task)), statusTag(task.legacyReadonly ? '历史只读' : taskStateLabel(task.state), taskStateTone(task.state))),
1207
+ React.createElement('div', { className: 'detail-meta' }, React.createElement('span', null, `创建 ${fmt(task.createdAt)}`), React.createElement('span', null, `更新 ${fmt(task.updatedAt)}`), !task.legacyReadonly ? React.createElement('span', null, `${task.executionCount ?? '未记录'} 次独立执行`) : null)),
1208
+ task.waitingReason || question ? React.createElement('section', { className: 'detail-attention', 'aria-label': '阻塞与待处理' },
1209
+ React.createElement('h2', null, question ? '等待答复' : '当前等待 / 阻塞'),
1210
+ question ? React.createElement(HumanRequestContent, { request: humanRequest || { reason: traceReason(task.waitingReason), question, evidence: task.lastDecision?.evidence } })
1211
+ : React.createElement('p', null, ...linkedTaskText(traceReason(task.waitingReason))),
1212
+ question ? React.createElement(Button, { type: 'button', variant: 'outline', onClick: () => onOpenHumanRequest(task.taskId) }, '查看人工介入问题') : null) : null,
1213
+ React.createElement('div', { className: 'detail-columns' },
1214
+ React.createElement('div', { className: 'detail-primary' },
1215
+ React.createElement('section', { className: 'detail-panel', 'aria-label': '目标与要求' }, React.createElement('h2', null, '目标与要求'), React.createElement('p', { className: 'detail-objective' }, task.objective || '尚未记录任务目标。'),
1216
+ requirements.length ? React.createElement('div', { className: 'detail-requirements' }, React.createElement('h3', null, '验收要求'), React.createElement('ul', null, ...requirements.map((item, index) => React.createElement('li', { key: index }, readableValue(item))))) : null,
1217
+ task.context ? React.createElement('details', { className: 'detail-disclosure' }, React.createElement('summary', null, '参考资料'), React.createElement('p', { className: 'detail-objective' }, readableValue(task.context))) : null),
1218
+ React.createElement(TaskResult, { result: task.result })),
1219
+ React.createElement('aside', { className: 'detail-sidebar', 'aria-label': '统筹会话与记录' },
1220
+ !task.legacyReadonly ? React.createElement(SessionLocation, { session: taskSession(task), label: '任务会话', onOpenSession, compact: true }) : null,
1221
+ React.createElement('details', { className: 'detail-records' }, React.createElement('summary', null, '任务记录与修订'), React.createElement('div', { className: 'detail-location' },
1222
+ React.createElement('div', null, `任务 ID:${task.taskId}`), React.createElement('div', null, `需求修订 ${task.requirementRevision ?? task.inputVersion ?? '未记录'}`), React.createElement('div', null, `控制修订 ${task.controlRevision ?? '未记录'}`), React.createElement('div', null, `控制状态 ${taskStateLabel(task.controlState || task.control?.state || task.state)}`),
1223
+ task.blockers ? readout('待排空记录(原始信息)', task.blockers) : null)))),
1224
+ React.createElement(TaskExecutionTimeline, { key: `timeline:${task.taskId}`, timeline: task.timeline }),
1225
+ task.legacyReadonly ? React.createElement('div', { className: 'detail-panel' }, React.createElement('h2', null, '历史执行'), React.createElement(LegacyTaskHistory, { key: task.taskId, task }))
1226
+ : React.createElement(TaskExecutions, { key: task.taskId, taskId: task.taskId, updatedAt, onOpenSession }))
525
1227
  }
526
1228
  function ObserverContent({ openSession }) {
527
- const [activePage, setActivePage] = useState('groups')
1229
+ const draftDirty = useRef(false)
1230
+ const [pendingNavigation, setPendingNavigation] = useState(null)
1231
+ const reportDirty = useCallback(value => { draftDirty.current = value }, [])
1232
+ const [activePage, updateActivePage] = useState(readActivePage)
1233
+ const setActivePage = useCallback(page => {
1234
+ if (draftDirty.current) { setPendingNavigation({ page }); return }
1235
+ updateActivePage(page)
1236
+ // 导航属于看板,不能随宿主 conversation 插槽重挂载而丢失。
1237
+ try { window.sessionStorage.setItem(activePageStorageKey, page) }
1238
+ catch { /* 浏览器禁用存储时仍允许正常切页。 */ }
1239
+ }, [])
528
1240
  const [topicTarget, setTopicTarget] = useState()
529
1241
  const [data, setData] = useState()
1242
+ const [expandedTaskId, setExpandedTaskId] = useState('')
1243
+ const expansionInitialized = useRef(false)
1244
+ const archivedTasksRef = useRef(new Map())
1245
+ const archivePending = useRef(new Set())
1246
+ const [archivingIds, setArchivingIds] = useState([])
1247
+ const [archiveFeedback, setArchiveFeedback] = useState(null)
1248
+ useEffect(() => {
1249
+ if (!data) return
1250
+ const visible = data.tasks.filter(task => !task.archivedAt && !task.legacyReadonly)
1251
+ if (!expansionInitialized.current) {
1252
+ expansionInitialized.current = true
1253
+ setExpandedTaskId(visible.filter(task => task.state === 'running').sort((a, b) => String(b.updatedAt).localeCompare(String(a.updatedAt)))[0]?.taskId || '')
1254
+ } else setExpandedTaskId(id => visible.some(task => task.taskId === id) ? id : '')
1255
+ }, [data])
1256
+ const archiveTask = async task => {
1257
+ if (archivePending.current.has(task.taskId)) return
1258
+ archivePending.current.add(task.taskId); setArchivingIds([...archivePending.current]); setArchiveFeedback(null)
1259
+ try {
1260
+ const archived = await post(`/tasks/${encodeURIComponent(task.taskId)}/archive`)
1261
+ if (!archived?.archivedAt || archived.taskId !== task.taskId) throw new Error('归档响应无效,请刷新核对')
1262
+ archivedTasksRef.current.set(task.taskId, { archivedAt: archived.archivedAt, archivedBy: archived.archivedBy })
1263
+ setData(current => ({ ...current, tasks: current.tasks.map(item => item.taskId === task.taskId ? { ...item, ...archived } : item) }))
1264
+ setExpandedTaskId(id => id === task.taskId ? '' : id)
1265
+ setArchiveFeedback({ text: `已归档:${task.title || task.taskId}` })
1266
+ } catch (cause) {
1267
+ const message = cause instanceof Error ? cause.message : String(cause)
1268
+ setArchiveFeedback({ error: true, text: /NOT_QUIESCENT/.test(message) ? '归档失败:任务须已完成或已取消,且所有执行已结束;归档不会停止活动执行。' : `归档失败:${message}` })
1269
+ } finally { archivePending.current.delete(task.taskId); setArchivingIds([...archivePending.current]) }
1270
+ }
530
1271
  const [error, setError] = useState()
531
1272
  const [navigationError, setNavigationError] = useState()
532
1273
  const [updatedAt, setUpdatedAt] = useState()
@@ -538,29 +1279,29 @@ window.__ModuleLoader__.load({
538
1279
  const [messageDeliveryFilter, setMessageDeliveryFilter] = useState('all')
539
1280
  const [outboxStatusFilter, setOutboxStatusFilter] = useState('all')
540
1281
  const [hoveredTaskId, setHoveredTaskId] = useState('')
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)
1282
+ const [creatingTask, setCreatingTask] = useState(false)
1283
+ const [taskPages, setTaskPages] = useState({})
1284
+ const [selectedTaskId, setSelectedTaskId] = useState('')
1285
+ const [taskDetail, setTaskDetail] = useState()
1286
+ const [detailError, setDetailError] = useState('')
1287
+ const [detailRetry, setDetailRetry] = useState(0)
548
1288
  const [selectedMessageRunId, setSelectedMessageRunId] = useState('')
549
1289
  const [copiedId, setCopiedId] = useState('')
550
1290
  const [navigatingSessionId, setNavigatingSessionId] = useState('')
551
- const [archivingTaskId, setArchivingTaskId] = useState('')
552
- const [archiveDetailTaskId, setArchiveDetailTaskId] = useState('')
1291
+ const [historyPage, setHistoryPage] = useState(1)
553
1292
  const [alertType, setAlertType] = useState('all')
554
1293
  const [alertView, setAlertView] = useState('active')
555
1294
  const [resolvedAlertPage, setResolvedAlertPage] = useState(1)
556
- const [authorizationComments, setAuthorizationComments] = useState({})
557
- const [decidingAuthorizationId, setDecidingAuthorizationId] = useState('')
558
1295
  const [authorizationPage, setAuthorizationPage] = useState(1)
559
1296
  const [authorizationFilter, setAuthorizationFilter] = useState('all')
560
1297
  const [selectedAuthorizationId, setSelectedAuthorizationId] = useState('')
561
- const refresh = useCallback(async () => {
562
- try { setData(await load()); setUpdatedAt(new Date()); setError(undefined) }
563
- catch (cause) { setError(cause instanceof Error ? cause.message : String(cause)) }
1298
+ const pendingRefresh = useRef(null)
1299
+ const refresh = useCallback(() => {
1300
+ if (pendingRefresh.current) return pendingRefresh.current
1301
+ pendingRefresh.current = load().then(value => { setData({ ...value, tasks: (value.tasks || []).map(task => archivedTasksRef.current.has(task.taskId) ? { ...task, ...archivedTasksRef.current.get(task.taskId) } : task) }); setUpdatedAt(new Date()); setError(undefined) },
1302
+ cause => { setError(cause instanceof Error ? cause.message : String(cause)) })
1303
+ .finally(() => { pendingRefresh.current = null })
1304
+ return pendingRefresh.current
564
1305
  }, [])
565
1306
  const manualRefresh = useCallback(async () => {
566
1307
  if (refreshState === 'refreshing') return
@@ -569,30 +1310,19 @@ window.__ModuleLoader__.load({
569
1310
  setRefreshState('done')
570
1311
  window.setTimeout(() => setRefreshState('idle'), 1200)
571
1312
  }, [refresh, refreshState])
1313
+ const pendingDetail = useRef(null)
572
1314
  useEffect(() => {
573
- if (!selectedWorkflowTaskId) { setWorkflowTaskDetail(undefined); setWorkflowDetailError(''); return }
1315
+ if (!selectedTaskId) { setTaskDetail(undefined); setDetailError(''); return }
574
1316
  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) })
1317
+ setDetailError('')
1318
+ if (pendingDetail.current?.taskId !== selectedTaskId || pendingDetail.current?.retry !== detailRetry) {
1319
+ const request = { taskId: selectedTaskId, retry: detailRetry }
1320
+ request.promise = get(`/state/tasks/${encodeURIComponent(selectedTaskId)}/detail`).finally(() => { if (pendingDetail.current === request) pendingDetail.current = null })
1321
+ pendingDetail.current = request
1322
+ }
1323
+ pendingDetail.current.promise.then(value => { if (active) setTaskDetail(value) }, cause => { if (active) setDetailError(cause.message) })
586
1324
  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])
1325
+ }, [selectedTaskId, updatedAt, detailRetry])
596
1326
  useEffect(() => {
597
1327
  refresh()
598
1328
  const timer = window.setInterval(refresh, 5000)
@@ -606,16 +1336,15 @@ window.__ModuleLoader__.load({
606
1336
  return () => document.removeEventListener('click', closeForSession, true)
607
1337
  }, [])
608
1338
  const groupsById = new Map((data?.groups || []).map((group) => [group.groupId, group]))
1339
+ const tasksById = new Map((data?.tasks || []).map(task => [task.taskId, task]))
609
1340
  const topicsById = new Map((data?.topics || []).map((topic) => [`${topic.groupId}:${topic.topicId}`, topic]))
610
1341
  const navigate = async (sessionId, parentSessionId) => {
611
1342
  try { setNavigationError(undefined); setNavigatingSessionId(sessionId); await openSession(sessionId, parentSessionId); setOpen(false) }
612
1343
  catch (cause) { setNavigationError(cause instanceof Error ? cause.message : String(cause)) }
613
1344
  finally { setNavigatingSessionId('') }
614
1345
  }
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
- }
1346
+ const openTask = task => { setCreatingTask(false); setSelectedTaskId(task.taskId); setActivePage('tasks') }
1347
+ const returnFromTask = () => { if (draftDirty.current) { setPendingNavigation({ back: true }); return } setSelectedTaskId(''); setCreatingTask(false) }
619
1348
  const copyId = async (value) => {
620
1349
  let copied = false
621
1350
  try {
@@ -638,9 +1367,8 @@ window.__ModuleLoader__.load({
638
1367
  }
639
1368
  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}`)
640
1369
  const selectedGroup = groupsById.get(selectedGroupId) || (data?.groups || [])[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))
1370
+ const selectedMessages = [...(selectedGroup?.messages || [])].filter((message) => message.sourceKind === 'dingtalk' || message.sourceKind === 'migration' || message.sourceKind === 'workflow-v2' || message.sourceKind === 'task-session' || !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))
642
1371
  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
644
1372
  if (message.routingStatus === 'failed') return 'failed'
645
1373
  if (message.routingStatus !== 'routed') return 'routing'
646
1374
  if (message.sourceKind === 'workflow-v2') return 'processed'
@@ -658,24 +1386,33 @@ window.__ModuleLoader__.load({
658
1386
  const visibleMessages = filteredMessages.slice((currentMessagePage - 1) * pageSize, currentMessagePage * pageSize)
659
1387
  const delivery = {
660
1388
  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' },
667
1389
  processing: { label: '话题处理中', state: 'ongoing' },
668
1390
  processed: { label: '已处理', state: 'done' },
669
1391
  failed: { label: '归类失败', state: 'error' },
670
1392
  }
671
1393
  const messageRows = visibleMessages.map((message, rowIndex) => {
672
1394
  const status = delivery[messageWorkflowState(message)]
1395
+ const notificationLabels = { prepared: '待发送', sending: '发送中', acknowledged: '已确认发送,待回读', unknown: '发送结果待核对', delivered: '已回读送达', superseded: '已失效' }
1396
+ const responsibilityLabels = { host: '助手读取和核对', requester: '请求人补充', system: '系统维护人员排查', approver: '审批人确认', acceptor: '验收人确认' }
1397
+ const messageFacts = message.waiting?.length || message.blockingSources?.length || message.notifications?.length
1398
+ ? React.createElement('details', { style: { marginTop: 6, fontSize: 12, overflowWrap: 'anywhere' } },
1399
+ React.createElement('summary', { style: { cursor: 'pointer', color: colors.accent } }, '等待与通知'),
1400
+ ...(message.waiting || []).map((wait, index) => React.createElement('p', { key: `wait-${index}`, style: { margin: '6px 0' } },
1401
+ `${wait.goalText ? `事项:${wait.goalText};` : ''}${responsibilityLabels[wait.responsibility] || '责任待确认'}:${traceReason(wait.reason) || '原因待核对'}。恢复条件:${wait.recoveryCondition}`)),
1402
+ ...(message.blockingSources || []).map((source) => React.createElement('p', { key: source.runId, style: { margin: '6px 0' } }, `相关来源:${source.text || source.messageId || '来源正文未记录'}`)),
1403
+ React.createElement('p', { style: { margin: '6px 0' } }, '沟通状态独立于事项处理状态。'),
1404
+ ...(message.notifications || []).map((notice) => React.createElement('p', { key: notice.notificationId, style: { margin: '6px 0' } },
1405
+ `通知:${notificationLabels[notice.status] || '状态未记录'}`)),
1406
+ !message.notifications?.length ? React.createElement('p', null, '尚无通知记录') : null) : null
673
1407
  return React.createElement('tr', { key: message.messageId, style: { background: rowIndex % 2 ? `color-mix(in srgb, ${colors.surface2} 55%, transparent)` : colors.cardSurface } },
674
- React.createElement('td', { style: { ...tableBodyCell, width: 104 }, title: message.workflowStatusDetail || message.agentDeliveryError || '' }, clampTableContent(tableStatusTag(status.label, status.state, { fontWeight: 600 }))),
1408
+ React.createElement('td', { style: { ...tableBodyCell, width: 144 }, title: traceReason(message.workflowStatusDetail || message.agentDeliveryError) || '' }, clampTableContent(tableStatusTag(status.label, status.state, { fontWeight: 600 }))),
675
1409
  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)))),
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' ? '—' : '待归类')]))),
677
- React.createElement('td', { title: message.text, style: tableBodyCell }, clampTableContent(message.text || '(空消息)')),
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' } }, '历史未记录处理过程')))
1410
+ React.createElement('td', { style: { ...tableBodyCell, width: 160 } }, React.createElement('div', { style: { display: 'flex', gap: 4, minWidth: 0, overflow: 'hidden' } }, ...(message.taskSessionRefs || []).map(taskId => React.createElement(Button, { key: `task:${taskId}`, type: 'button', variant: 'outline', size: 'sm', onClick: () => openTask(tasksById.get(taskId) || { taskId }), style: { maxWidth: 140, minWidth: 0, overflow: 'hidden', textOverflow: 'ellipsis' }, 'aria-label': `查看任务 ${tasksById.get(taskId)?.title || taskId}` }, taskDisplayTitle(tasksById.get(taskId) || { title: taskId }))), ...(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)) : message.taskSessionRefs?.length ? [] : [React.createElement('span', { key: 'unrouted', style: { color: colors.muted, fontSize: 12 } }, messageWorkflowState(message) === 'processed' ? '—' : '待归类')]))),
1411
+ React.createElement('td', { title: message.text, style: tableBodyCell }, clampTableContent(message.text || '(空消息)'),
1412
+ React.createElement('div', { 'aria-label': '处理结论与历时', style: { display: 'flex', gap: 12, minWidth: 0, marginTop: 4, height: 20, lineHeight: '20px', fontSize: 12, color: colors.muted } },
1413
+ React.createElement('span', { title: messageWorkflowState(message) === 'processed' ? message.processingConclusion || '结论未记录' : '尚未处理完成', style: { flex: '1 1 auto', minWidth: 0, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' } }, messageWorkflowState(message) === 'processed' ? message.processingConclusion ? `结论:${message.processingConclusion}` : '结论未记录' : '尚未处理完成'),
1414
+ React.createElement('span', { style: { flex: '0 0 auto', fontVariantNumeric: 'tabular-nums', whiteSpace: 'nowrap' } }, messageWorkflowState(message) === 'processed' ? messageProcessingElapsed(message) : '—'))),
1415
+ 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 } }, '处理过程') : null, messageFacts))
679
1416
  })
680
1417
  const selectedOutbox = [...(selectedGroup?.outbox || [])].reverse()
681
1418
  const outboxState = (message) => outboxDelivery(message).id
@@ -687,106 +1424,62 @@ window.__ModuleLoader__.load({
687
1424
  const outboxRows = visibleOutbox.map((message, rowIndex) => {
688
1425
  const status = outboxDelivery(message)
689
1426
  return React.createElement('tr', { key: message.outboundId, style: { background: rowIndex % 2 ? `color-mix(in srgb, ${colors.surface2} 55%, transparent)` : colors.cardSurface } },
690
- React.createElement('td', { style: { ...tableBodyCell, width: 124 } }, clampTableContent(tableStatusTag(status.label, status.state, { fontWeight: 600 }))),
1427
+ React.createElement('td', { title: status.hint || status.detail, style: { ...tableBodyCell, width: 124 } }, clampTableContent(tableStatusTag(status.label, status.state, { fontWeight: 600 }))),
691
1428
  React.createElement('td', { title: message.text, style: tableBodyCell }, clampTableContent(message.text || '(空消息)'),
692
- status.detail ? React.createElement('div', { style: { marginTop: 6, fontSize: 12, color: status.state === 'error' ? colors.danger : colors.warning, overflowWrap: 'anywhere' } }, status.detail, !message.supersededByOutboundId && !message.recallStatus && message.deliveryError ? `:${message.deliveryError}` : '') : null,
1429
+ status.detail || !['confirmed', 'superseded', 'recalled', 'recall-failed'].includes(status.id) && typeof message.deliveryError === 'string' && message.deliveryError ? React.createElement('div', { style: { marginTop: 6, fontSize: 12, color: status.state === 'error' ? colors.danger : colors.warning, overflowWrap: 'anywhere' } }, status.detail || '', !message.supersededByOutboundId && !message.recallStatus && typeof message.deliveryError === 'string' && message.deliveryError ? `${status.detail ? ':' : ''}${message.deliveryError}` : '') : null,
1430
+ !status.retryDisabled && !['confirmed', 'superseded', 'recalled', 'recall-failed'].includes(status.id) && message.notificationRetryAt ? React.createElement('div', { style: { marginTop: 3, fontSize: 11, color: colors.muted } }, `计划${['waiting', 'checking'].includes(status.id) ? '核验' : '重试'} ${fmt(message.notificationRetryAt)}`) : null,
693
1431
  message.deliveryAttemptedAt ? React.createElement('div', { style: { marginTop: 3, fontSize: 11, color: colors.muted } }, `最近尝试 ${fmt(message.deliveryAttemptedAt)} · 共 ${message.deliveryAttemptCount || 1} 次`) : null),
694
1432
  React.createElement('td', { style: { ...tableBodyCell, width: 220 } },
695
1433
  React.createElement('div', { style: { minWidth: 0 } }, singleLineTableContent(React.createElement('code', { title: message.sourceMessageId, style: { fontSize: 14, color: colors.muted, whiteSpace: 'nowrap' } }, short(message.sourceMessageId))), message.replyToMessageId ? React.createElement('div', { style: { marginTop: 3, fontSize: 11, color: colors.muted, whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' } }, '回复 ', React.createElement('code', { title: message.replyToMessageId }, short(message.replyToMessageId))) : null)),
696
1434
  React.createElement('td', { style: { ...tableBodyCell, width: 220 } }, singleLineTableContent(React.createElement('code', { title: message.deliveredMessageId || '', style: { fontSize: 14, color: colors.muted, whiteSpace: 'nowrap' } }, message.deliveredMessageId ? short(message.deliveredMessageId) : message.status === 'sent' ? '历史未记录' : '尚未确认'))),
697
1435
  React.createElement('td', { style: { ...tableBodyCell, width: 220 } }, singleLineTableContent(React.createElement('code', { title: message.outboundId, style: { fontSize: 14, color: colors.muted, whiteSpace: 'nowrap' } }, short(message.outboundId)))))
698
1436
  })
699
- const renderTaskCard = (task) => {
700
- const group = groupsById.get(task.groupId)
1437
+ const renderTaskCard = task => {
701
1438
  const sourceGroup = groupsById.get(task.sourceGroupId || task.groupId)
702
1439
  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 ? '群通知待确认送达' : '群通知尚未生成'
1440
+ const session = taskSession(task)
1441
+ const hovered = hoveredTaskId === task.taskId
708
1442
  const queued = task.state === 'queued'
709
- const checkpointEvents = task.checkpoints || []
710
- const planCheckpointIndex = checkpointEvents.findLastIndex((checkpoint) => checkpoint.kind === 'plan-confirmed')
711
- const planCheckpoint = checkpointEvents[planCheckpointIndex]
712
- const currentCheckpointEvents = checkpointEvents.slice(Math.max(0, planCheckpointIndex))
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) || '执行步骤'
1443
+ const sessions = task.executionSessions || []
1444
+ const completedSessions = sessions.filter(item => ['succeeded', 'completed'].includes(item.status)).length
716
1445
  const displayObjective = String(task.objective || '').startsWith('[TASK_SOURCE_EVIDENCE]') ? (task.title || task.objective) : task.objective
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
719
- const completedCheckpointCount = checkpoints.filter((checkpoint) => completedCheckpointNames.has(checkpoint)).length
720
- const progress = checkpoints.length ? Math.round((completedCheckpointCount / checkpoints.length) * 100) : 0
721
- const showCheckpoints = checkpoints.length > 0 && !task.archivedAt
722
- const checkpointsExpanded = expandedCheckpointTaskId === task.taskId
723
- const hovered = hoveredTaskId === task.taskId
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
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' } },
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)),
1446
+ return React.createElement('article', { key: task.taskId, onClick: event => { if (!event.target.closest('button, summary, details')) openTask(task) }, 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' } },
1447
+ React.createElement('button', { type: 'button', onClick: () => openTask(task), style: { width: '100%', minWidth: 0, boxSizing: 'border-box', border: 0, padding: 0, background: 'transparent', color: 'inherit', cursor: 'pointer', textAlign: 'left', display: 'grid', gap: 6, fontFamily: 'inherit' } },
1448
+ 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))),
770
1449
  React.createElement('div', { style: { display: 'flex', alignItems: 'center', gap: 5, minWidth: 0, overflow: 'hidden' } },
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) })),
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),
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,
778
- showCheckpoints ? React.createElement('section', null,
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 } })),
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,
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 } },
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)),
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)
1450
+ React.createElement('span', { title: groupLabel, style: { ...pill(colors.muted), flex: '0 1 auto', maxWidth: 150, padding: '2px 7px', fontSize: 10.5, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' } }, groupLabel),
1451
+ task.legacyReadonly ? React.createElement('span', { style: { color: colors.muted, fontSize: 10.5, whiteSpace: 'nowrap' } }, '历史只读') : null,
1452
+ ...(task.topicRefs || []).map(ref => {
1453
+ const groupId = ref.groupId || task.sourceGroupId || task.groupId
1454
+ const label = topicsById.get(`${groupId}:${ref.topicId}`)?.title || ref.title || `话题 ${short(ref.topicId)}`
1455
+ return React.createElement('button', { key: ref.topicId, type: 'button', title: label, 'aria-label': `话题 ${label}`, onClick: event => { event.stopPropagation(); setTopicTarget({ groupId, topicId: ref.topicId, revision: ref.revision }); setActivePage('topics') }, style: { ...pill(colors.accent), display: 'inline-block', border: 0, fontFamily: 'inherit', flex: '0 1 auto', minWidth: 0, maxWidth: 96, padding: '2px 7px', fontSize: 10.5, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', cursor: 'pointer' } }, label)
1456
+ })),
1457
+ React.createElement('button', { type: 'button', title: displayObjective, 'aria-label': '查看完整任务目标', onClick: () => openTask(task), style: { border: 0, padding: 0, background: 'transparent', color: 'inherit', textAlign: 'left', fontFamily: 'inherit', cursor: 'pointer', minWidth: 0, display: 'block', maxHeight: '4.5em', overflow: 'hidden', overflowWrap: 'anywhere', fontSize: 11, lineHeight: 1.5 } },
1458
+ React.createElement('svg', { width: 12, height: 12, viewBox: '0 0 16 16', fill: 'none', stroke: 'currentColor', strokeWidth: 1.4, strokeLinecap: 'round', strokeLinejoin: 'round', 'aria-hidden': true, 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),
1459
+ !task.legacyReadonly && sessions.length ? React.createElement('details', { className: 'observer-card-sessions', open: expandedTaskId === task.taskId, 'aria-label': '全部执行会话', style: { minWidth: 0, padding: '8px 9px', border: `1px solid ${colors.border}`, borderRadius: 8, background: colors.surface2 } },
1460
+ React.createElement('summary', { onClick: event => { event.preventDefault(); event.stopPropagation(); setExpandedTaskId(id => id === task.taskId ? '' : task.taskId) }, style: { display: 'flex', alignItems: 'center', gap: 6, minWidth: 0, cursor: 'pointer', fontSize: 11, color: colors.muted, listStyle: 'none' } },
1461
+ React.createElement('svg', { width: 13, height: 13, viewBox: '0 0 16 16', fill: 'none', stroke: 'currentColor', strokeWidth: 1.2, 'aria-hidden': true }, React.createElement('path', { d: 'M2 3h2v2H2zM6 4h8M2 7h2v2H2zM6 8h8M2 11h2v2H2zM6 12h8' })),
1462
+ React.createElement('span', null, '执行会话'),
1463
+ React.createElement('meter', { min: 0, max: sessions.length, value: completedSessions, 'aria-label': '已完成执行会话数量', style: { flex: 1, minWidth: 20, maxWidth: 80, height: 5, marginLeft: 'auto' } }),
1464
+ React.createElement('span', { style: { fontVariantNumeric: 'tabular-nums' } }, `${completedSessions}/${sessions.length}`),
1465
+ React.createElement('svg', { className: 'observer-card-session-chevron', width: 12, height: 12, viewBox: '0 0 16 16', fill: 'none', stroke: 'currentColor', strokeWidth: 1.4, 'aria-hidden': true }, React.createElement('path', { d: 'm4 6 4 4 4-4' }))),
1466
+ React.createElement('div', { style: { display: 'grid', gap: 2, paddingTop: 6 } }, ...sessions.map(execution => {
1467
+ const done = ['succeeded', 'completed'].includes(execution.status)
1468
+ const running = execution.status === 'running'
1469
+ const active = running && !execution.drained && !execution.recovering
1470
+ const failed = execution.status === 'failed'
1471
+ const tone = done ? 'var(--dsw-alias-state-success-primary, #248a3d)' : running ? colors.accent : failed ? colors.danger : colors.muted
1472
+ return React.createElement('button', { key: execution.sessionId || execution.executionId, className: 'observer-execution-session', type: 'button', disabled: !execution.sessionId || navigatingSessionId === execution.sessionId, 'aria-busy': navigatingSessionId === execution.sessionId, onClick: () => navigate(execution.sessionId, session.sessionId), style: { display: 'grid', gridTemplateColumns: '14px minmax(0, 1fr) auto', alignItems: 'start', gap: 7, minWidth: 0, padding: '6px 0', border: 0, borderRadius: 4, background: 'transparent', color: 'inherit', fontFamily: 'inherit', fontSize: 11, lineHeight: 1.5, textAlign: 'left', cursor: execution.sessionId ? 'pointer' : 'default' } },
1473
+ active ? React.createElement('span', { role: 'img', 'aria-label': taskStateLabel(execution.status), title: taskStateLabel(execution.status), style: { display: 'inline-flex', width: 14, height: 14, marginTop: 1 } }, React.createElement(StateDot, { state: 'ongoing', size: 14 })) : React.createElement('svg', { width: 14, height: 14, viewBox: '0 0 16 16', fill: 'none', stroke: tone, strokeWidth: 1.3, role: 'img', 'aria-label': taskStateLabel(execution.status), style: { marginTop: 1 } },
1474
+ React.createElement('title', null, taskStateLabel(execution.status)),
1475
+ React.createElement('circle', { cx: 8, cy: 8, r: 5.6, fill: done ? tone : 'none' }),
1476
+ done ? React.createElement('path', { d: 'm5 8 2 2 4-4', stroke: 'white' }) : running ? React.createElement('circle', { cx: 8, cy: 8, r: 2, fill: tone, stroke: 'none' }) : failed ? React.createElement('path', { d: 'M8 4.5v4M8 10v1' }) : null),
1477
+ React.createElement('span', { title: execution.title, style: { minWidth: 0, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' } }, execution.title),
1478
+ React.createElement('span', { style: { whiteSpace: 'nowrap', color: tone, fontSize: 10.5 } }, React.createElement(ExecutionSessionDuration, { timing: execution.runTiming })))
1479
+ }))) : null,
1480
+ task.waitingReason ? React.createElement('div', { style: { fontSize: 11, lineHeight: 1.5, color: colors.warning, display: '-webkit-box', WebkitLineClamp: 2, WebkitBoxOrient: 'vertical', overflow: 'hidden', overflowWrap: 'anywhere' } }, task.waitingReason) : null,
1481
+ React.createElement('div', { style: { minWidth: 0, paddingTop: 6, borderTop: `1px solid ${colors.border}`, display: 'flex', alignItems: 'center', gap: 4, fontSize: 10.5, color: colors.muted } },
1482
+ 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), React.createElement('span', { style: { marginLeft: 'auto' } }, !task.legacyReadonly && !task.archivedAt ? React.createElement(Button, { type: 'button', variant: 'outline', size: 'sm', title: '归档任务(仅限已完成或已取消且执行已结束的任务)', 'aria-label': `归档任务:${task.title || task.taskId}`, 'aria-busy': archivingIds.includes(task.taskId), disabled: archivingIds.includes(task.taskId), onClick: event => { event.stopPropagation(); archiveTask(task) }, style: { minWidth: 58, fontSize: 10.5, cursor: archivingIds.includes(task.taskId) ? 'wait' : 'pointer' } }, React.createElement('svg', { width: 12, height: 12, viewBox: '0 0 24 24', fill: 'none', stroke: 'currentColor', strokeWidth: 1.8, 'aria-hidden': true, style: { verticalAlign: -2, marginRight: 4 } }, React.createElement('path', { d: 'M3 3h18v5H3zM5 8v13h14V8M9 12h6' })), '归档') : React.createElement('span', { style: { color: colors.muted, fontSize: 10.5 } }, task.legacyReadonly ? '历史只读' : '已归档'))))
790
1483
  }
791
1484
  const alertCategories = [
792
1485
  { id: 'all', label: '全部类型' },
@@ -819,12 +1512,14 @@ window.__ModuleLoader__.load({
819
1512
  const resolvedAlertPageCount = Math.max(1, Math.ceil(filteredResolvedAlerts.length / resolvedAlertPageSize))
820
1513
  const currentResolvedAlertPage = Math.min(resolvedAlertPage, resolvedAlertPageCount)
821
1514
  const visibleResolvedAlerts = filteredResolvedAlerts.slice((currentResolvedAlertPage - 1) * resolvedAlertPageSize, currentResolvedAlertPage * resolvedAlertPageSize)
822
- const bucketColumns = taskBuckets.map((bucket) => {
823
- const tasks = (data?.tasks || []).filter((task) => task.state === bucket.state && !task.archivedAt).sort((left, right) => String(right.updatedAt).localeCompare(String(left.updatedAt)))
824
- const taskCards = tasks.length ? tasks.map(renderTaskCard) : [React.createElement('div', { key: 'empty', style: { border: `1px dashed ${colors.border}`, borderRadius: 10, minHeight: 90, display: 'grid', placeItems: 'center', color: colors.muted, fontSize: 12 } }, '暂无任务')]
825
- return React.createElement('div', { key: bucket.state, style: { boxSizing: 'border-box', borderRadius: 14, background: bucket.background, padding: '8px 4px', display: 'flex', flexDirection: 'column', height: 'calc(100dvh - 116px)', minHeight: 420, maxHeight: 'calc(100dvh - 116px)', overflow: 'hidden' } },
1515
+ const bucketColumns = taskBuckets.map(bucket => {
1516
+ const tasks = (data?.tasks || []).filter(task => (bucket.state === 'completed' ? ['completed', 'cancelled', 'succeeded', 'failed'].includes(task.state) : bucket.state === 'waiting' ? ['waiting', 'paused', 'blocked', 'interrupted'].includes(task.state) : task.state === bucket.state) && !task.archivedAt && !task.legacyReadonly).sort((left, right) => String(right.updatedAt).localeCompare(String(left.updatedAt)))
1517
+ const totalPages = Math.max(1, Math.ceil(tasks.length / 10)), page = Math.min(taskPages[bucket.state] || 1, totalPages)
1518
+ const visible = tasks.slice((page - 1) * 10, page * 10)
1519
+ return React.createElement('section', { key: bucket.state, style: { boxSizing: 'border-box', borderRadius: 14, background: bucket.background, padding: '8px 4px', minWidth: 0, display: 'flex', flexDirection: 'column', height: 'calc(100dvh - 164px)', minHeight: 420, maxHeight: 'calc(100dvh - 164px)', overflow: 'hidden' } },
826
1520
  React.createElement('div', { style: { display: 'flex', alignItems: 'center', gap: 8, padding: '4px 6px 10px' } }, React.createElement('strong', { style: { display: 'inline-flex', alignItems: 'center', borderRadius: 999, padding: '3px 9px', fontSize: 11.5, fontWeight: 650, lineHeight: 1.4, color: 'var(--dsw-alias-label-primary-inverted, #fff)', background: bucket.tone } }, bucket.label), React.createElement('span', { style: { borderRadius: 999, background: colors.cardSurface, padding: '2px 7px', fontSize: 10.5, color: colors.muted } }, tasks.length)),
827
- React.createElement('div', { style: { flex: 1, minHeight: 0, padding: '8px 8px 12px 8px', display: 'flex', flexDirection: 'column', gap: 10, overflowY: 'auto', scrollbarWidth: 'none' } }, ...taskCards))
1521
+ React.createElement('div', { style: { flex: 1, minHeight: 0, padding: '8px 8px 12px 8px', display: 'flex', flexDirection: 'column', gap: 10, overflowY: 'auto', scrollbarWidth: 'none' } }, ...(visible.length ? visible.map(renderTaskCard) : [React.createElement('div', { key: 'empty', style: { border: `1px dashed ${colors.border}`, borderRadius: 10, minHeight: 90, display: 'grid', placeItems: 'center', color: colors.muted, fontSize: 12 } }, '暂无任务')])),
1522
+ totalPages > 1 ? React.createElement('div', { style: { display: 'flex', justifyContent: 'space-between', gap: 8 } }, React.createElement(Button, { type: 'button', variant: 'outline', size: 'sm', disabled: page <= 1, onClick: () => setTaskPages(value => ({ ...value, [bucket.state]: page - 1 })) }, '上一页'), React.createElement('span', null, `${page}/${totalPages}`), React.createElement(Button, { type: 'button', variant: 'outline', size: 'sm', disabled: page >= totalPages, onClick: () => setTaskPages(value => ({ ...value, [bucket.state]: page + 1 })) }, '下一页')) : null)
828
1523
  })
829
1524
  const menu = React.createElement('nav', { 'aria-label': '运行看板视图', style: { minWidth: 0, height: 27, display: 'flex', alignItems: 'stretch', gap: 36, overflowX: 'auto', padding: '0 0 0 8px' } },
830
1525
  ...pages.map((page) => React.createElement('button', { key: page.id, type: 'button', onClick: () => setActivePage(page.id), 'aria-current': activePage === page.id ? 'page' : undefined, style: navigationTabStyle(activePage === page.id) }, page.label)))
@@ -840,19 +1535,22 @@ window.__ModuleLoader__.load({
840
1535
  updatedAt ? React.createElement('span', { 'aria-hidden': true, style: { width: 1, height: 12, background: colors.border } }) : null,
841
1536
  updatedAt ? React.createElement('span', { title: fmt(updatedAt) }, `更新于 ${fmtTime(updatedAt)}`) : null),
842
1537
  React.createElement(Button, { variant: 'outline', size: 'sm', type: 'button', disabled: refreshState === 'refreshing', onClick: manualRefresh, 'aria-live': 'polite', style: { gap: 6, minWidth: 78, fontSize: 13, lineHeight: '20px' } }, refreshState === 'refreshing' ? '刷新中' : refreshState === 'done' ? '已刷新' : '刷新', refreshIcon)),
843
- menu)
1538
+ menu,
1539
+ pendingNavigation ? React.createElement('section', { 'aria-label': '未提交的修改', style: { padding: 12, display: 'flex', gap: 12, flexWrap: 'wrap' } }, React.createElement('span', null, '离开会丢弃尚未提交的修改。'), React.createElement(Button, { type: 'button', variant: 'outline', onClick: () => setPendingNavigation(null) }, '继续编辑'), React.createElement(Button, { type: 'button', onClick: () => { const target = pendingNavigation; draftDirty.current = false; setPendingNavigation(null); if (target.back) returnFromTask(); else setActivePage(target.page) } }, '放弃修改并离开')) : null)
844
1540
  const dataViewTabs = React.createElement('div', { role: 'tablist', 'aria-label': '群聊数据视图', style: { display: 'flex', alignSelf: 'stretch', gap: 20 } },
845
1541
  ...[{ id: 'messages', label: `收信箱 · ${selectedMessages.length}` }, { id: 'outbox', label: `发信箱 · ${selectedOutbox.length}` }].map((item) => React.createElement('button', { key: item.id, role: 'tab', 'aria-selected': groupTableView === item.id, type: 'button', onClick: () => setGroupTableView(item.id), style: navigationTabStyle(groupTableView === item.id, { paddingBottom: 4 }) }, item.label)))
846
1542
  const groupTableToolbar = React.createElement('div', { style: toolbar },
847
1543
  dataViewTabs,
848
1544
  React.createElement('div', { style: { display: 'flex', alignItems: 'center', justifyContent: 'flex-end', gap: 8, flexWrap: 'wrap' } },
849
1545
  (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,
1546
+ selectedGroup?.coordinator?.sessionId ? React.createElement(Button, { variant: 'outline', size: 'sm', type: 'button', disabled: navigatingSessionId === selectedGroup.coordinator.sessionId,
1547
+ onClick: () => navigate(selectedGroup.coordinator.sessionId) }, navigatingSessionId === selectedGroup.coordinator.sessionId ? '正在打开…' : '打开群常驻会话') : null,
850
1548
  groupTableView === 'messages'
851
- ? React.createElement(SelectMenu, { label: '筛选处理状态', value: messageDeliveryFilter, options: [{ id: 'all', label: '全部处理状态' }, { id: 'routing', label: '待归类' }, { id: 'waiting_routing_barrier', label: '等待其他消息关联' }, { id: 'intent_judging', label: '意图判断中' }, { id: 'intent_rejudging', label: '意图重新判断' }, { id: 'waiting_clarification', label: '等待澄清' }, { id: 'waiting_context', label: '等待补充材料' }, { id: 'routing_blocked', label: '关联受阻' }, { id: 'processing', label: '话题处理中' }, { id: 'processed', label: '已处理' }, { id: 'failed', label: '归类失败' }], onChange: (value) => { setMessageDeliveryFilter(value); setMessagePage(1) } })
852
- : React.createElement(SelectMenu, { label: '筛选发件状态', value: outboxStatusFilter, options: [{ id: 'all', label: '全部发件状态' }, { id: 'queued', label: '待发送' }, { id: 'failed', label: '投递异常' }, { id: 'waiting', label: '待回读' }, { id: 'confirmed', label: '已发送' }, { id: 'superseded', label: '已替代' }, { id: 'recall-failed', label: '撤回待处理' }, { id: 'recalled', label: '已撤回' }], onChange: (value) => { setOutboxStatusFilter(value); setOutboxPage(1) } })))
1549
+ ? 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) } })
1550
+ : React.createElement(SelectMenu, { label: '筛选发件状态', value: outboxStatusFilter, options: [{ id: 'all', label: '全部发件状态' }, { id: 'queued', label: '待发送' }, { id: 'sending', label: '发送中' }, { id: 'failed', label: '投递异常' }, { id: 'waiting', label: '待回读' }, { id: 'checking', label: '待核对' }, { id: 'stopped', label: '已停止' }, { id: 'reassessment', label: '待核验' }, { id: 'confirmed', label: '已送达' }, { id: 'superseded', label: '已替代' }, { id: 'recall-failed', label: '撤回受阻' }, { id: 'recalled', label: '已撤回' }], onChange: (value) => { setOutboxStatusFilter(value); setOutboxPage(1) } })))
853
1551
  const messagesTable = React.createElement(React.Fragment, null,
854
- React.createElement('div', { style: { overflowX: 'auto' } }, React.createElement('table', { style: { width: '100%', minWidth: 910, borderCollapse: 'collapse', tableLayout: 'fixed' } },
855
- React.createElement('thead', null, React.createElement('tr', { style: { background: colors.surface2, textAlign: 'left' } }, React.createElement('th', { style: { ...tableHeadCell, width: 104 } }, '话题处理'), React.createElement('th', { style: { ...tableHeadCell, width: 160 } }, '发送人 / 时间'), React.createElement('th', { style: { ...tableHeadCell, width: 160 } }, '话题'), React.createElement('th', { style: tableHeadCell }, '消息内容'), React.createElement('th', { style: { ...tableHeadCell, width: 240 } }, '消息'))),
1552
+ React.createElement('div', { style: { overflowX: 'auto' } }, React.createElement('table', { style: { width: '100%', minWidth: 1040, borderCollapse: 'collapse', tableLayout: 'fixed' } },
1553
+ React.createElement('thead', null, React.createElement('tr', { style: { background: colors.surface2, textAlign: 'left' } }, React.createElement('th', { style: { ...tableHeadCell, width: 144 } }, '处理状态'), React.createElement('th', { style: { ...tableHeadCell, width: 160 } }, '发送人 / 时间'), React.createElement('th', { style: { ...tableHeadCell, width: 160 } }, '关联任务 / 历史话题'), React.createElement('th', { style: tableHeadCell }, '消息内容'), React.createElement('th', { style: { ...tableHeadCell, width: 240 } }, '消息'))),
856
1554
  React.createElement('tbody', null, ...(messageRows.length ? messageRows : [React.createElement('tr', { key: 'empty' }, React.createElement('td', { colSpan: 5, style: { ...tableBodyCell, padding: 36, textAlign: 'center', color: colors.muted } }, '暂无符合条件的群聊消息'))])))),
857
1555
  React.createElement('div', { style: tableFooter }, React.createElement('span', { style: { marginRight: 'auto', fontSize: 11, color: colors.muted } }, `${filteredMessages.length} 条 · 每页 ${pageSize} 条`), React.createElement(Button, { variant: 'outline', size: 'sm', type: 'button', disabled: currentMessagePage <= 1, onClick: () => setMessagePage((page) => Math.max(1, page - 1)) }, '上一页'), React.createElement('span', { style: { fontSize: 11, color: colors.muted } }, `${currentMessagePage} / ${pageCount}`), React.createElement(Button, { variant: 'outline', size: 'sm', type: 'button', disabled: currentMessagePage >= pageCount, onClick: () => setMessagePage((page) => Math.min(pageCount, page + 1)) }, '下一页')))
858
1556
  const outboxTable = React.createElement(React.Fragment, null,
@@ -861,136 +1559,62 @@ window.__ModuleLoader__.load({
861
1559
  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 发件记录'))])))),
862
1560
  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)) }, '下一页')))
863
1561
  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)
1562
+ const selectedTask = taskDetail?.taskId === selectedTaskId ? taskDetail : undefined
1563
+ const tasksPage = creatingTask ? React.createElement(CreateTaskForm, { groups: data?.groups || [], onDirtyChange: reportDirty, onCancel: returnFromTask, onCreated: result => { setCreatingTask(false); const task = result.task || result; if (task.taskId) openTask(task); void refresh() } })
1564
+ : selectedTaskId ? React.createElement('div', { className: 'observer-task-detail-route', style: { maxWidth: 1100, margin: '0 auto', display: 'grid', gap: 16 } },
1565
+ React.createElement('style', null, taskDetailCss),
1566
+ detailError ? React.createElement('div', { className: 'detail-fetch-error', role: 'alert' }, `${selectedTask ? '详情刷新失败,已读取内容可能过期' : '详情读取失败'}:${detailError}`, React.createElement(Button, { type: 'button', variant: 'outline', onClick: () => setDetailRetry(value => value + 1) }, '重试读取详情')) : null,
1567
+ selectedTask ? React.createElement(TaskSessionDetail, { task: selectedTask, onOpenSession: navigate, onBack: returnFromTask, updatedAt, onOpenHumanRequest: taskId => { setSelectedAuthorizationId((data?.authorizations || []).find(item => item.taskId === taskId && item.taskSessionRequest && item.status === 'waiting-reply')?.requestId || ''); setActivePage('authorizations') } })
1568
+ : React.createElement('section', { className: 'observer-session-detail detail-fetch-state', 'aria-label': '任务详情读取状态' }, React.createElement('div', { className: 'detail-topline' }, React.createElement(Button, { type: 'button', variant: 'outline', size: 'sm', onClick: returnFromTask }, '返回看板'), React.createElement('span', { className: 'detail-muted' }, '任务详情')), React.createElement('h1', null, '任务详情'), !detailError ? React.createElement('span', { role: 'status' }, '正在读取任务详情…') : React.createElement('p', { className: 'detail-muted' }, '未读取到任务内容,请重试或返回看板。')))
1569
+ : React.createElement('section', { style: { display: 'grid', gap: 16 } }, React.createElement(Button, { type: 'button', onClick: () => setCreatingTask(true), style: { justifySelf: 'start' } }, '新建任务'), 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))
931
1570
  const authorizationStatus = {
932
1571
  'pending-send': { label: '待发送', state: 'warning' },
1572
+ 'sending-unknown': { label: '投递待确认', state: 'warning' },
933
1573
  'waiting-reply': { label: '等待处理', state: 'warning' },
934
1574
  answered: { label: '已处理', state: 'done' },
935
1575
  superseded: { label: '已失效', state: 'neutral' },
936
1576
  }
937
1577
  const authorizationDecision = { approved: { label: '已继续', state: 'done' }, rejected: { label: '不执行', state: 'error' } }
938
- const isPendingAuthorization = (item) => item.status === 'pending-send' || item.status === 'waiting-reply'
1578
+ const isPendingAuthorization = (item) => item.status === 'pending-send' || item.status === 'sending-unknown' || item.status === 'waiting-reply'
939
1579
  const authorizationPresentation = (item) => item.status === 'answered'
940
- ? authorizationDecision[item.decision] || authorizationStatus.answered
1580
+ ? item.taskSessionRequest ? { label: '已答复', state: 'done' } : authorizationDecision[item.decision] || authorizationStatus.answered
941
1581
  : authorizationStatus[item.status] || { label: '状态异常', state: 'error' }
942
- const decideAuthorization = async (requestId, decision) => {
943
- try {
944
- setNavigationError(undefined); setDecidingAuthorizationId(requestId)
945
- await post(`/authorizations/${encodeURIComponent(requestId)}/decision`, { decision, comment: authorizationComments[requestId] || '' })
946
- await refresh()
947
- } catch (cause) { setNavigationError(cause instanceof Error ? cause.message : String(cause)) }
948
- finally { setDecidingAuthorizationId('') }
949
- }
950
1582
  const authorizationItems = [...(data?.authorizations || [])].sort((left, right) => {
951
1583
  const leftPending = isPendingAuthorization(left) ? 1 : 0; const rightPending = isPendingAuthorization(right) ? 1 : 0
952
1584
  return rightPending - leftPending || String(right.createdAt || '').localeCompare(String(left.createdAt || ''))
953
1585
  })
954
- const filteredAuthorizationItems = authorizationItems.filter((item) => authorizationFilter === 'all' || (authorizationFilter === 'pending' ? isPendingAuthorization(item) : authorizationFilter === 'superseded' ? item.status === 'superseded' : item.decision === authorizationFilter))
1586
+ const filteredAuthorizationItems = authorizationItems.filter((item) => authorizationFilter === 'all' || (authorizationFilter === 'pending' ? isPendingAuthorization(item) : ['superseded', 'answered'].includes(authorizationFilter) ? item.status === authorizationFilter : item.decision === authorizationFilter))
955
1587
  const authorizationPageSize = 10
956
1588
  const authorizationPageCount = Math.max(1, Math.ceil(filteredAuthorizationItems.length / authorizationPageSize))
957
1589
  const currentAuthorizationPage = Math.min(authorizationPage, authorizationPageCount)
958
1590
  const visibleAuthorizationItems = filteredAuthorizationItems.slice((currentAuthorizationPage - 1) * authorizationPageSize, currentAuthorizationPage * authorizationPageSize)
959
1591
  const selectedAuthorization = authorizationItems.find((item) => item.requestId === selectedAuthorizationId)
960
- const selectedAuthorizationPending = selectedAuthorization ? isPendingAuthorization(selectedAuthorization) : false
961
1592
  const authorizationDetailField = (label, value) => React.createElement('div', { style: { display: 'grid', gap: 5 } },
962
1593
  React.createElement('strong', { style: { fontSize: 11, color: colors.muted } }, label),
963
- React.createElement('div', { style: { fontSize: 13, lineHeight: 1.65, whiteSpace: 'pre-wrap', overflowWrap: 'anywhere' } }, value || '—'))
1594
+ React.createElement('div', { style: { fontSize: 13, lineHeight: 1.65, whiteSpace: 'pre-wrap', overflowWrap: 'anywhere' } }, ...linkedTaskText(value || '—')))
964
1595
  const authorizationRows = visibleAuthorizationItems.map((item, rowIndex) => {
965
1596
  const status = authorizationPresentation(item)
966
1597
  const group = groupsById.get(item.groupId)
967
1598
  return React.createElement('div', { key: item.requestId, style: { width: '100%', minWidth: 0, boxSizing: 'border-box', display: 'grid', gridTemplateColumns: '104px minmax(220px, 1.1fr) minmax(280px, 1.5fr) 126px 52px', alignItems: 'center', gap: 16, borderTop: `1px solid ${colors.border}`, background: rowIndex % 2 ? `color-mix(in srgb, ${colors.surface2} 55%, transparent)` : colors.cardSurface, color: 'inherit', padding: '12px 16px', fontSize: 14 } },
968
1599
  React.createElement('div', { style: { justifySelf: 'start' } }, tableStatusTag(status.label, status.state, { fontWeight: 600 })),
969
1600
  React.createElement('span', { style: { minWidth: 0 } }, React.createElement('strong', { title: item.objective, style: { display: 'block', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', fontSize: 14, fontWeight: 600, lineHeight: 1.5 } }, item.objective), React.createElement('span', { style: { display: 'block', marginTop: 3, color: colors.muted, fontSize: 11, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' } }, group?.name || item.groupId)),
970
- React.createElement('span', { style: { minWidth: 0 } }, React.createElement('span', { title: item.requestedAction, style: { display: 'block', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', fontSize: 14, lineHeight: 1.55, color: colors.muted } }, item.requestedAction), item.risk && item.risk !== '未单独说明' ? React.createElement('span', { style: { display: 'block', marginTop: 3, color: colors.danger, fontSize: 11, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' } }, `风险 · ${item.risk}`) : null),
1601
+ React.createElement('span', { style: { minWidth: 0 } }, React.createElement('span', { title: item.requestedAction || item.question || item.reason, style: { display: 'block', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', fontSize: 14, lineHeight: 1.55, color: colors.muted } }, item.requestedAction || item.question || item.reason), item.risk && item.risk !== '未单独说明' ? React.createElement('span', { style: { display: 'block', marginTop: 3, color: colors.danger, fontSize: 11, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' } }, `风险 · ${item.risk}`) : null),
971
1602
  React.createElement('span', { style: { color: colors.muted, fontSize: 11, lineHeight: 1.5 } }, fmt(item.createdAt)),
972
1603
  React.createElement(Button, { variant: 'ghost', size: 'sm', type: 'button', onClick: () => setSelectedAuthorizationId(item.requestId) }, '查看'))
973
1604
  })
974
- const authorizationDetail = selectedAuthorization ? React.createElement('div', { role: 'presentation', onMouseDown: (event) => { if (event.target === event.currentTarget) setSelectedAuthorizationId('') }, style: { position: 'fixed', inset: 0, zIndex: 1100, display: 'flex', justifyContent: 'flex-end', background: 'rgba(0,0,0,.28)', backdropFilter: 'blur(2px)' } },
975
- React.createElement('section', { role: 'dialog', 'aria-modal': true, 'aria-label': '人工介入事项详情', style: { width: 'min(640px, 100%)', height: '100%', display: 'flex', flexDirection: 'column', overflow: 'hidden', borderLeft: `1px solid ${colors.border}`, background: colors.surface, boxShadow: '-20px 0 60px rgba(0,0,0,.2)' } },
1605
+ const authorizationDetail = selectedAuthorization ? React.createElement(HumanRequestModal, { key: selectedAuthorization.requestId, onClose: () => setSelectedAuthorizationId('') },
976
1606
  React.createElement('header', { style: { flex: '0 0 auto', display: 'flex', alignItems: 'flex-start', justifyContent: 'space-between', gap: 24, padding: '24px 24px 20px', borderBottom: `1px solid ${colors.border}`, background: colors.surface } },
977
- React.createElement('div', { style: { minWidth: 0 } }, React.createElement('div', { style: { display: 'flex', alignItems: 'center', gap: 10, marginBottom: 10 } }, statusTag(authorizationPresentation(selectedAuthorization).label, authorizationPresentation(selectedAuthorization).state), React.createElement('span', { style: { color: colors.muted, fontSize: 11.5 } }, fmt(selectedAuthorization.createdAt))), React.createElement('h3', { style: { margin: 0, fontSize: 18, fontWeight: 600, lineHeight: 1.5 } }, selectedAuthorization.objective || '人工介入事项'), React.createElement('code', { style: { display: 'block', marginTop: 10, color: colors.muted, fontSize: 10.5 } }, selectedAuthorization.requestId)),
1607
+ React.createElement('div', { style: { minWidth: 0 } }, React.createElement('div', { style: { display: 'flex', alignItems: 'center', gap: 10, marginBottom: 10 } }, statusTag(authorizationPresentation(selectedAuthorization).label, authorizationPresentation(selectedAuthorization).state), React.createElement('span', { style: { color: colors.muted, fontSize: 11.5 } }, fmt(selectedAuthorization.createdAt))), React.createElement('h3', { style: { margin: 0, fontSize: 18, fontWeight: 600, lineHeight: 1.5, overflowWrap: 'anywhere', maxHeight: '15dvh', overflowY: 'auto' } }, selectedAuthorization.objective || '人工介入事项'), React.createElement('code', { style: { display: 'block', marginTop: 10, color: colors.muted, fontSize: 10.5, overflowWrap: 'anywhere' } }, selectedAuthorization.requestId)),
978
1608
  React.createElement(Button, { variant: 'ghost', size: 'sm', type: 'button', 'aria-label': '关闭人工介入事项', onClick: () => setSelectedAuthorizationId(''), style: { flex: '0 0 auto', minWidth: 52, whiteSpace: 'nowrap' } }, '关闭')),
979
- React.createElement('div', { style: { flex: '1 1 auto', overflowY: 'auto', padding: '0 24px 24px' } },
980
- React.createElement('section', { style: { padding: '24px 0', borderBottom: `1px solid ${colors.border}` } }, authorizationDetailField('需要人工处理', selectedAuthorization.requestedAction)),
981
- React.createElement('section', { style: { padding: '24px 0', borderBottom: `1px solid ${colors.border}` } }, authorizationDetailField('风险', selectedAuthorization.risk || '未单独说明')),
982
- React.createElement('section', { style: { padding: '24px 0', borderBottom: `1px solid ${colors.border}` } }, authorizationDetailField('阻塞原因', selectedAuthorization.waitingReason)),
983
- React.createElement('details', { open: true, style: { padding: '18px 0', borderBottom: `1px solid ${colors.border}` } }, React.createElement('summary', { style: { cursor: 'pointer', fontSize: 12, fontWeight: 600 } }, `现场证据 · ${(selectedAuthorization.evidence || []).length}`), React.createElement('div', { style: { marginTop: 14 } }, authorizationDetailField('', (selectedAuthorization.evidence || []).map((value, index) => `${index + 1}. ${value}`).join('\n')))),
984
- React.createElement('details', { style: { padding: '18px 0', borderBottom: `1px solid ${colors.border}` } }, React.createElement('summary', { style: { cursor: 'pointer', fontSize: 12, fontWeight: 600 } }, `已尝试 · ${(selectedAuthorization.attemptedActions || []).length}`), React.createElement('div', { style: { marginTop: 14 } }, authorizationDetailField('', (selectedAuthorization.attemptedActions || []).map((value, index) => `${index + 1}. ${value}`).join('\n')))),
1609
+ React.createElement('div', { style: { flex: '1 1 auto', minHeight: 0, overflowY: 'auto', scrollbarGutter: 'stable', padding: '0 24px 24px' } },
1610
+ React.createElement('section', { style: { padding: '24px 0', borderBottom: `1px solid ${colors.border}` } }, React.createElement(HumanRequestContent, { request: selectedAuthorization })),
985
1611
  React.createElement('section', { style: { padding: '20px 0', borderBottom: `1px solid ${colors.border}` } }, authorizationDetailField('关联信息', `Task ID:${selectedAuthorization.taskId || '—'}\n群聊:${groupsById.get(selectedAuthorization.groupId)?.name || selectedAuthorization.groupId || '—'}`)),
986
- selectedAuthorization.status === 'answered' ? React.createElement('section', { style: { padding: '20px 0' } }, authorizationDetailField('处理结果', `${selectedAuthorization.decision === 'approved' ? '继续任务' : selectedAuthorization.decision === 'rejected' ? '不执行' : selectedAuthorization.decision || '已处理'}\n处理渠道:${selectedAuthorization.decisionSource === 'web' ? '运行看板' : selectedAuthorization.decisionSource === 'dingtalk' ? '钉钉私聊' : selectedAuthorization.decisionSource || '历史迁移'}\n处理时间:${fmt(selectedAuthorization.decidedAt)}\n处理意见:${selectedAuthorization.reply || '—'}`)) : null),
987
- selectedAuthorizationPending ? React.createElement('footer', { style: { flex: '0 0 auto', display: 'grid', gridTemplateColumns: '1fr auto auto', gap: 10, alignItems: 'center', padding: '16px 24px', borderTop: `1px solid ${colors.border}`, background: colors.surface2 } },
988
- React.createElement('input', { 'aria-label': `执行限制或拒绝原因 ${selectedAuthorization.requestId}`, value: authorizationComments[selectedAuthorization.requestId] || '', onChange: (event) => setAuthorizationComments((current) => ({ ...current, [selectedAuthorization.requestId]: event.target.value })), placeholder: '执行限制或拒绝原因(可选)', style: { minWidth: 0, border: `1px solid ${colors.border}`, borderRadius: 9, background: colors.surface2, color: 'inherit', padding: '9px 11px', fontFamily: 'inherit', fontSize: 12 } }),
989
- React.createElement(Button, { variant: 'outline', size: 'sm', type: 'button', disabled: decidingAuthorizationId === selectedAuthorization.requestId, onClick: () => decideAuthorization(selectedAuthorization.requestId, 'rejected'), style: { color: colors.danger } }, '不执行'),
990
- React.createElement(Button, { variant: 'primary', size: 'sm', type: 'button', disabled: decidingAuthorizationId === selectedAuthorization.requestId, onClick: () => decideAuthorization(selectedAuthorization.requestId, 'approved') }, decidingAuthorizationId === selectedAuthorization.requestId ? '处理中…' : '批准该事项并继续')) : null)) : null
1612
+ selectedAuthorization.status === 'answered' ? React.createElement('section', { style: { padding: '20px 0' } }, authorizationDetailField('处理结果', `${selectedAuthorization.taskSessionRequest ? '已答复' : selectedAuthorization.decision === 'approved' ? '继续任务' : selectedAuthorization.decision === 'rejected' ? '不执行' : selectedAuthorization.decision || '已处理'}\n处理渠道:${selectedAuthorization.decisionSource === 'web' ? '运行看板' : selectedAuthorization.decisionSource === 'dingtalk' ? '钉钉私聊' : selectedAuthorization.decisionSource || '历史迁移'}\n处理时间:${fmt(selectedAuthorization.decidedAt)}\n处理意见:${selectedAuthorization.reply || '—'}`)) : null),
1613
+ selectedAuthorization.notification?.recall ? React.createElement('p', { role: selectedAuthorization.notification.recall.state === 'failed' ? 'alert' : 'status', style: { color: selectedAuthorization.notification.recall.state === 'failed' ? colors.danger : colors.muted } }, selectedAuthorization.notification.recall.state === 'recalled' ? '对应私聊通知已撤回。' : selectedAuthorization.notification.recall.state === 'failed' ? '申请已处理,私聊通知撤回失败,将重试。' : '申请已处理,正在撤回对应私聊通知。') : null,
1614
+ React.createElement(HumanRequestReply, { key: selectedAuthorization.requestId, request: selectedAuthorization, onChanged: () => { setDetailRetry(value => value + 1); void refresh() } })) : null
991
1615
  const authorizationsPage = React.createElement(React.Fragment, null, React.createElement('section', null,
992
1616
  React.createElement('div', { style: tableFrame },
993
- React.createElement('div', { style: { ...toolbar, justifyContent: 'flex-end' } }, React.createElement(SelectMenu, { label: '筛选处理状态', value: authorizationFilter, options: [{ id: 'all', label: '全部处理状态' }, { id: 'pending', label: '等待处理' }, { id: 'approved', label: '已继续' }, { id: 'rejected', label: '不执行' }, { id: 'superseded', label: '已失效' }], onChange: (value) => { setAuthorizationFilter(value); setAuthorizationPage(1) }, fitContent: true })),
1617
+ React.createElement('div', { style: { ...toolbar, justifyContent: 'flex-end' } }, React.createElement(SelectMenu, { label: '筛选处理状态', value: authorizationFilter, options: [{ id: 'all', label: '全部处理状态' }, { id: 'pending', label: '等待处理' }, { id: 'answered', label: '已答复' }, { id: 'approved', label: '已继续' }, { id: 'rejected', label: '不执行' }, { id: 'superseded', label: '已失效' }], onChange: (value) => { setAuthorizationFilter(value); setAuthorizationPage(1) }, fitContent: true })),
994
1618
  React.createElement('div', { style: { height: 42, boxSizing: 'border-box', display: 'grid', gridTemplateColumns: '104px minmax(220px, 1.1fr) minmax(280px, 1.5fr) 126px 52px', alignItems: 'center', gap: 16, padding: '0 16px', background: colors.surface2, color: colors.muted, fontSize: 13, fontWeight: 600 } }, React.createElement('span', null, '状态'), React.createElement('span', null, '任务'), React.createElement('span', null, '阻塞事项'), React.createElement('span', null, '提出时间'), React.createElement('span', null, '操作')),
995
1619
  React.createElement('div', null, ...(authorizationRows.length ? authorizationRows : [React.createElement('div', { key: 'empty', style: { padding: 36, textAlign: 'center', color: colors.muted, fontSize: 12 } }, '暂无人工介入事项')])),
996
1620
  React.createElement('div', { style: tableFooter },
@@ -999,8 +1623,13 @@ window.__ModuleLoader__.load({
999
1623
  React.createElement('span', { style: { fontSize: 11, color: colors.muted } }, `${currentAuthorizationPage} / ${authorizationPageCount}`),
1000
1624
  React.createElement(Button, { variant: 'outline', size: 'sm', type: 'button', disabled: currentAuthorizationPage >= authorizationPageCount, onClick: () => setAuthorizationPage((page) => Math.min(authorizationPageCount, page + 1)) }, '下一页')))
1001
1625
  ), authorizationDetail)
1002
- const archivedTasks = (data?.tasks || []).filter((task) => task.state === 'completed' && task.archivedAt).sort((left, right) => String(right.archivedAt).localeCompare(String(left.archivedAt)))
1003
- const archivePage = React.createElement('section', null, React.createElement('div', { style: { display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(300px, 1fr))', gap: 12 } }, ...(archivedTasks.length ? archivedTasks.map(renderTaskCard) : [React.createElement(React.Fragment, { key: 'empty' }, emptyState('暂无归档任务'))])))
1626
+ const archivedTasks = (data?.tasks || []).filter(task => task.legacyReadonly || task.archivedAt).sort((left, right) => String(right.updatedAt || right.archivedAt).localeCompare(String(left.updatedAt || left.archivedAt)))
1627
+ const historyPageCount = Math.max(1, Math.ceil(archivedTasks.length / 20)), currentHistoryPage = Math.min(historyPage, historyPageCount)
1628
+ const archivePage = React.createElement('section', { style: { display: 'grid', gap: 16 } },
1629
+ React.createElement('p', { role: 'status', style: { margin: 0, color: colors.muted } }, '历史任务只读。旧流程、参数和执行操作已停止。'),
1630
+ React.createElement('div', { style: { display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(min(300px, 100%), 1fr))', gap: 12 } }, ...archivedTasks.slice((currentHistoryPage - 1) * 20, currentHistoryPage * 20).map(renderTaskCard)),
1631
+ !archivedTasks.length ? emptyState('暂无历史任务') : null,
1632
+ React.createElement('div', { style: { display: 'flex', gap: 12, alignItems: 'center' } }, React.createElement('span', { style: { marginRight: 'auto' } }, `${archivedTasks.length} 条 · ${currentHistoryPage}/${historyPageCount}`), React.createElement(Button, { type: 'button', variant: 'outline', disabled: currentHistoryPage <= 1, onClick: () => setHistoryPage(value => value - 1) }, '上一页'), React.createElement(Button, { type: 'button', variant: 'outline', disabled: currentHistoryPage >= historyPageCount, onClick: () => setHistoryPage(value => value + 1) }, '下一页')))
1004
1633
  const alertsPage = React.createElement(React.Fragment, null,
1005
1634
  React.createElement('section', null, React.createElement('div', { style: tableFrame },
1006
1635
  React.createElement('div', { style: toolbar },
@@ -1011,28 +1640,29 @@ window.__ModuleLoader__.load({
1011
1640
  )
1012
1641
  const topicsPage = React.createElement(TopicBrowser, { groups: data?.groups || [], tasks: data?.tasks || [], target: topicTarget, updatedAt, onOpenTask: openTask, onOpenSession: navigate })
1013
1642
  const pageContent = activePage === 'topics' ? topicsPage : activePage === 'tasks' ? tasksPage : activePage === 'authorizations' ? authorizationsPage : activePage === 'archive' ? archivePage : activePage === 'alerts' ? alertsPage : groupsPage
1014
- const pageViewport = React.createElement('div', { style: { width: '100%', maxWidth: 1320, minHeight: 'calc(100dvh - 138px)', boxSizing: 'border-box', margin: '0 auto' } }, pageContent)
1643
+ const pageViewport = React.createElement('div', { style: { width: '100%', maxWidth: 1320, minHeight: 400, boxSizing: 'border-box', margin: '0 auto' } }, pageContent)
1015
1644
  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' } },
1016
1645
  error ? React.createElement('div', { style: { ...card, borderColor: colors.danger, color: colors.danger } }, `无法连接 resident 插件:${error}`) : null,
1646
+ archiveFeedback ? React.createElement('div', { role: archiveFeedback.error ? 'alert' : 'status', style: { ...card, color: archiveFeedback.error ? colors.danger : colors.muted } }, archiveFeedback.text) : null,
1017
1647
  navigationError ? React.createElement('div', { role: 'alert', style: { ...card, borderColor: colors.danger, color: colors.danger } }, `操作失败:${navigationError}`) : null,
1018
1648
  pageViewport
1019
1649
  )
1020
1650
  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)
1021
1651
  }
1022
1652
  function apply(ctx) {
1023
- ctx.slots.inject('sidebar.footer.action', () => ctx.slots.register({ name: 'sidebar.footer.action', id: 'dingtalk-dsh-observer-entry', order: 5, inject: () => ({}) }, SidebarAction))
1024
- ctx.slots.inject('conversation', () => {
1653
+ ctx.slots.inject('sidebar.footer.action', () => ctx.slots.register({ name: 'sidebar.footer.action', id: 'dingtalk-dsh-observer-entry', order: 5, inject: () => ({ onOpen: () => { ctx.layout.selectPanel(null); setOpen(true) } }) }, SidebarAction))
1654
+ ctx.slots.inject('main.conversation', () => {
1025
1655
  let disposeContent
1026
1656
  const syncContent = (visible) => {
1027
1657
  if (visible && !disposeContent) {
1028
- disposeContent = ctx.slots.register({ name: 'conversation', id: 'dingtalk-dsh-observer', priority: -10, inject: () => ({ openSession: async (sessionId, parentSessionId) => {
1658
+ disposeContent = ctx.slots.register({ name: 'main.conversation', priority: -10, inject: () => ({ openSession: async (sessionId, parentSessionId) => {
1029
1659
  if (parentSessionId) {
1030
1660
  let address = ctx.sessions.subagentAddress(sessionId)
1031
- if (!address) { await ctx.sessions.refreshSubagents(parentSessionId); address = ctx.sessions.subagentAddress(sessionId) }
1032
- if (address) { ctx.sessions.openSubagent(address); return }
1661
+ if (!address) { await ctx.sessions.refreshProjections(parentSessionId); address = ctx.sessions.subagentAddress(sessionId) }
1662
+ if (address) { ctx.uiWorkspace.openSession(address); return }
1033
1663
  }
1034
1664
  await ctx.sessions.refresh()
1035
- ctx.sessions.open(sessionId)
1665
+ ctx.uiWorkspace.openSession(sessionId)
1036
1666
  } }) }, ObserverContent)
1037
1667
  } else if (!visible && disposeContent) {
1038
1668
  disposeContent()
@@ -1047,7 +1677,7 @@ window.__ModuleLoader__.load({
1047
1677
  }
1048
1678
  })
1049
1679
  }
1050
- module.exports = { name: 'dingtalk-dsh-observer-client', inject: ['slots', 'sessions'], apply }
1680
+ module.exports = { name: 'dingtalk-dsh-observer-client', inject: ['slots', 'sessions', 'uiWorkspace', 'layout'], apply }
1051
1681
  return module.exports
1052
1682
  }
1053
1683
  })