@zzusp/dingtalk-dsh-observer 0.5.15 → 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 +1213 -120
package/web-client.js CHANGED
@@ -3,8 +3,8 @@ 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')
7
- const { useCallback, useEffect, useState } = React
6
+ const { Button, IconChecklistOutlineRegular, IconChevronDownOutlineRegular, IconChevronUpOutlineRegular, Menu, Pill, StateDot } = require('@deepseek-ai/dsh-client-ui-primitives')
7
+ const { useCallback, useEffect, useState, useRef } = React
8
8
  const ENDPOINT = 'http://127.0.0.1:18998'
9
9
  const colors = {
10
10
  surface: 'var(--dsw-alias-bg-base, #fff)',
@@ -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,93 @@ 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
  }
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; }
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; }
152
+ [data-dingtalk-observer] .observer-task-output-row { display: grid; grid-template-columns: 112px minmax(0, 1fr); gap: 12px; }
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; }
83
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; }
167
+ [data-dingtalk-observer] .observer-task-output-row { grid-template-columns: minmax(0, 1fr); gap: 2px; }
168
+ [data-dingtalk-observer] .observer-task-overview { padding: 20px 0; }
169
+ [data-dingtalk-observer] .observer-task-result { padding: 18px; }
84
170
  [data-dingtalk-observer] .observer-main { padding: 16px !important; }
85
171
  [data-dingtalk-observer] .observer-topics-layout { display: grid !important; grid-template-columns: minmax(280px, 2fr) minmax(0, 3fr); }
86
172
  [data-dingtalk-observer] .observer-topic-list { max-width: none !important; }
87
173
  [data-dingtalk-observer] .observer-task-board { grid-template-columns: repeat(2, minmax(280px, 1fr)) !important; }
88
174
  }
89
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; }
90
180
  [data-dingtalk-observer] .observer-main { padding: 12px !important; }
91
181
  [data-dingtalk-observer] .observer-topics-layout,
92
182
  [data-dingtalk-observer] .observer-task-board { grid-template-columns: minmax(0, 1fr) !important; }
@@ -128,8 +218,8 @@ window.__ModuleLoader__.load({
128
218
  React.createElement('strong', { style: { color: 'inherit', fontSize: ui.textMd, fontWeight: 600 } }, title),
129
219
  detail ? React.createElement('span', null, detail) : null,
130
220
  actionLabel && onAction ? React.createElement(Button, { variant: 'outline', size: 'sm', type: 'button', onClick: onAction }, actionLabel) : null))
131
- async function get(path) {
132
- 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 })
133
223
  const value = await response.json()
134
224
  if (!response.ok) throw new Error(value.error || `HTTP ${response.status}`)
135
225
  return value
@@ -146,13 +236,13 @@ window.__ModuleLoader__.load({
146
236
  ])
147
237
  return { health, groups, tasks, alerts, authorizations, topics: topicPage.topics }
148
238
  }
149
- function SidebarAction({ wide }) {
239
+ function SidebarAction({ wide, onOpen }) {
150
240
  const isOpen = useOpen()
151
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 },
152
242
  React.createElement('rect', { x: 3, y: 3, width: 18, height: 18, rx: 3 }),
153
243
  React.createElement('path', { d: 'M7 15v2M12 11v6M17 7v10' }))
154
244
  return React.createElement('button', {
155
- type: 'button', title: '钉钉群聊运行看板', 'aria-label': '钉钉群聊运行看板', onClick: () => setOpen(true),
245
+ type: 'button', title: '钉钉群聊运行看板', 'aria-label': '钉钉群聊运行看板', onClick: onOpen,
156
246
  style: wide
157
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' }
158
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 }
@@ -162,16 +252,148 @@ window.__ModuleLoader__.load({
162
252
  { state: 'queued', label: '待执行', tone: 'var(--dsw-alias-label-secondary, #64748b)', background: 'color-mix(in srgb, var(--dsw-alias-label-secondary, #64748b) 8%, var(--dsw-alias-bg-layer-1, #fff))' },
163
253
  { state: 'running', label: '执行中', tone: colors.accent, background: 'color-mix(in srgb, var(--dsw-alias-brand-primary, #4d6bfe) 8%, var(--dsw-alias-bg-layer-1, #fff))' },
164
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))' },
165
- { state: 'completed', label: '已完成', tone: 'var(--dsw-alias-state-success-primary, #248a3d)', background: 'color-mix(in srgb, var(--dsw-alias-state-success-primary, #248a3d) 8%, var(--dsw-alias-bg-layer-1, #fff))' }
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))' }
166
256
  ]
167
- const pages = [{ id: 'groups', label: '群聊会话' }, { id: 'topics', label: '话题' }, { id: 'tasks', label: '任务看板' }, { id: 'authorizations', label: '人工介入' }, { id: 'archive', label: '归档任务' }, { id: 'alerts', label: '告警' }]
168
- function TopicBrowser({ groups, tasks, target, updatedAt, onOpenTask }) {
257
+ const pages = [{ id: 'groups', label: '群消息' }, { id: 'topics', label: '话题' }, { id: 'tasks', label: '任务看板' }, { id: 'authorizations', label: '人工介入' }, { id: 'archive', label: '历史任务' }, { id: 'alerts', label: '告警' }]
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] || '状态未记录')
259
+ const taskDisplayTitle = task => {
260
+ const text = String(task.title || task.objective || '未命名任务').replace(/\s+/gu, ' ').trim() || '未命名任务'
261
+ const first = text.split(/[。!?\n]/u)[0]
262
+ if (Array.from(first).length >= 8 && Array.from(first).length <= 32) return first
263
+ const characters = Array.from(text)
264
+ return characters.length <= 32 ? text : `${characters.slice(0, 31).join('')}…`
265
+ }
266
+ const traceElapsed = (item, now) => {
267
+ const start = Date.parse(item.startedAt), end = Date.parse(item.completedAt)
268
+ const running = ['running', 'claimed'].includes(item.status)
269
+ const duration = (running ? now : end) - start
270
+ if (!Number.isFinite(duration) || duration < 0) return '耗时未记录'
271
+ const seconds = duration / 1000
272
+ const value = seconds < 1 ? `${Math.round(duration)} 毫秒` : seconds < 60 ? `${seconds.toFixed(1)} 秒`
273
+ : seconds < 3600 ? `${Math.floor(seconds / 60)} 分 ${Math.floor(seconds % 60)} 秒`
274
+ : `${Math.floor(seconds / 3600)} 小时 ${Math.floor(seconds % 3600 / 60)} 分 ${Math.floor(seconds % 60)} 秒`
275
+ return `${running ? '已用时' : '耗时'} ${value}${item.attempt > 1 ? `(第 ${item.attempt} 次处理)` : ''}`
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
+ }
287
+ const traceReason = (reason) => {
288
+ if (!reason) return null
289
+ const text = typeof reason === 'string' ? reason : JSON.stringify(reason)
290
+ if (/MESSAGE_MODEL_CONTEXT_WINDOW_EXCEEDED/.test(text)) return '模型报告容量不足,已暂停处理;系统维护人员排查后继续,无需重复提交材料。'
291
+ if (/MESSAGE_(?:CONTEXT|MATERIAL|REFERENCED_CANDIDATES)_CAPACITY|context_capacity_blocked/.test(text)) return '上下文容量受阻:必要材料未能完整提供,后续判断已停止。'
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
293
+ }
294
+ const readableValue = (value) => value === undefined || value === null ? '历史未记录' : typeof value === 'string' ? value : JSON.stringify(value, null, 2)
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)))
296
+ function TraceReadout({ runId, resourceRef, label }) {
297
+ const [expanded, setExpanded] = useState(false)
298
+ const [page, setPage] = useState()
299
+ const [cursor, setCursor] = useState(0)
300
+ const [hash, setHash] = useState('')
301
+ const [text, setText] = useState('')
302
+ const [loading, setLoading] = useState(false)
303
+ const [error, setError] = useState('')
304
+ const [retry, setRetry] = useState(0)
305
+ useEffect(() => {
306
+ if (!expanded) return
307
+ let active = true
308
+ setLoading(true); setError('')
309
+ get(`/state/workflows/${encodeURIComponent(runId)}/evidence/${encodeURIComponent(resourceRef)}?cursor=${cursor}${cursor && hash ? `&hash=${encodeURIComponent(hash)}` : ''}`).then(value => {
310
+ if (active) { setPage(value); setHash(value.hash); setText(previous => cursor ? previous + value.text : value.text) }
311
+ }, () => { if (active) setError('产出暂时无法读取,请重试。') }).finally(() => { if (active) setLoading(false) })
312
+ return () => { active = false }
313
+ }, [runId, resourceRef, expanded, cursor, retry])
314
+ return React.createElement('section', { style: { minWidth: 0 } },
315
+ React.createElement(Button, { variant: 'outline', size: 'sm', type: 'button', 'aria-expanded': expanded, onClick: () => { setExpanded(value => !value); setCursor(0); setHash(''); setText(''); setPage(undefined) } }, expanded ? '收起产出' : label),
316
+ expanded ? React.createElement('div', { style: { marginTop: 10, padding: 12, background: colors.surface2, borderRadius: ui.radiusSm, lineHeight: 1.7 } },
317
+ text ? React.createElement('div', { style: { whiteSpace: 'pre-wrap', overflowWrap: 'anywhere' } }, text) : null,
318
+ loading ? React.createElement('p', { role: 'status', style: { minHeight: 24 } }, '正在读取产出…') : null,
319
+ error ? React.createElement('div', { role: 'alert' }, error, React.createElement(Button, { variant: 'outline', size: 'sm', type: 'button', onClick: () => setRetry(value => value + 1) }, '重试')) : null,
320
+ !loading && !error && page && !text ? React.createElement('p', null, '未记录正文。') : null,
321
+ page?.nextCursor !== null && page?.nextCursor !== undefined ? React.createElement(Button, { variant: 'outline', size: 'sm', type: 'button', disabled: loading, onClick: () => setCursor(page.nextCursor) }, '继续阅读产出') : null,
322
+ page?.evidenceRefs?.length ? React.createElement('div', { style: { display: 'grid', gap: 8, marginTop: 12 } }, ...page.evidenceRefs.map((ref, index) => React.createElement(TraceReadout, { key: ref, runId, resourceRef: ref, label: `查看依据 ${index + 1}` }))) : null) : null)
323
+ }
324
+ function TraceDetail({ runId: initialRunId, onBack, onOpenSession, backLabel = '返回收信箱' }) {
325
+ const [runId, setRunId] = useState(initialRunId)
326
+ const [page, setPage] = useState()
327
+ const [cursor, setCursor] = useState('')
328
+ const [cursorHistory, setCursorHistory] = useState([])
329
+ const [retry, setRetry] = useState(0)
330
+ const [error, setError] = useState('')
331
+ const [loading, setLoading] = useState(true)
332
+ const [clock, setClock] = useState(Date.now)
333
+ const hasRunningStep = page?.items?.some(item => ['running', 'claimed'].includes(item.status))
334
+ useEffect(() => {
335
+ if (!hasRunningStep) return
336
+ const timer = setInterval(() => setClock(Date.now()), 1000)
337
+ return () => clearInterval(timer)
338
+ }, [hasRunningStep])
339
+ useEffect(() => {
340
+ let active = true
341
+ setLoading(true); setError('')
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) })
343
+ return () => { active = false }
344
+ }, [runId, cursor, retry])
345
+ const labels = { coordinator: '群消息协调', split: '拆分事项', route: '关联话题', intent: '判断下一步', command: '接纳动作', agent: '查询与答复' }
346
+ const ready = !loading && !error && page
347
+ return React.createElement('section', { 'aria-label': '消息处理过程', style: { ...card, display: 'grid', gap: 12 } },
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)),
349
+ ready ? React.createElement('div', { style: { background: colors.surface2, padding: 16, borderRadius: ui.radiusSm, display: 'grid', gap: 10 } },
350
+ React.createElement('div', { style: { display: 'flex', justifyContent: 'space-between', alignItems: 'center', gap: 12, flexWrap: 'wrap' } }, React.createElement('strong', { style: { fontSize: 16 } }, traceStatus(page.status)), React.createElement(Button, { variant: 'outline', size: 'sm', type: 'button', onClick: () => setRetry(value => value + 1) }, '刷新处理记录')),
351
+ React.createElement('blockquote', { style: { margin: 0, paddingLeft: 12, borderLeft: `3px solid ${colors.accent}`, fontSize: 14, lineHeight: 1.7, whiteSpace: 'pre-wrap', overflowWrap: 'anywhere' } }, page.message?.text ?? '原消息未记录'),
352
+ page.reason ? React.createElement('p', { role: 'status', style: { color: colors.warning, margin: 0, whiteSpace: 'pre-wrap', overflowWrap: 'anywhere' } }, traceReason(page.reason)) : null,
353
+ React.createElement('span', { style: { color: colors.muted, fontSize: 12 } }, '这里展示消息判断、查询与答复;需要持续交付的任务见任务看板,回复送达情况见发信箱。')) : null,
354
+ ready && page.items?.length ? React.createElement('div', { 'aria-label': '本页处理步骤', style: { display: 'flex', flexWrap: 'wrap', gap: 8 } }, ...Object.entries(labels).map(([kind, label]) => { const count = page.items.filter(item => item.kind === kind).length; return count ? React.createElement('span', { key: kind, style: pill(colors.accent) }, `${label} · ${count} 条记录`) : null })) : null,
355
+ loading ? React.createElement('div', { role: 'status', style: { minHeight: 160 } }, '正在读取处理记录…') : error ? React.createElement('div', { role: 'alert' }, `处理记录读取失败:${error}`, React.createElement(Button, { variant: 'outline', size: 'sm', type: 'button', onClick: () => setRetry((value) => value + 1) }, '重试')) : !page?.items?.length ? emptyState('此页没有处理记录', { detail: '历史消息可能尚未记录完整处理轨迹。' }) : React.createElement('ol', { 'aria-label': '逐步判断记录', start: Number(cursor || 0) + 1, style: { margin: 0, paddingLeft: 28, display: 'grid', gap: 16 } }, ...page.items.map((item, index) => {
356
+ const view = item.summary ?? { title: '处理记录', conclusion: '尚未记录判断结论', rows: [] }
357
+ const tone = ['failed', 'blocked', 'needs_attention'].includes(item.status) ? colors.warning : colors.accent
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' } },
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))),
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' } },
361
+ React.createElement('strong', { style: { fontSize: 13 } }, `${item.topicTitle || '当前话题'} · 本次判断覆盖 ${item.sourceMessages.length} 条消息`),
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 } },
363
+ React.createElement('div', { style: { fontSize: 12, color: colors.muted } }, `${message.senderName || '发送人未记录'} · ${fmt(message.occurredAt)} · ${message.current ? '当前查看的消息' : '共同参与判断'}`),
364
+ React.createElement('p', { style: { margin: '6px 0', whiteSpace: 'pre-wrap', lineHeight: 1.65 } }, message.text),
365
+ !message.current ? React.createElement(Button, { variant: 'outline', size: 'sm', type: 'button', onClick: () => { setPage(undefined); setCursor(''); setCursorHistory([]); setRunId(message.runId) } }, '查看这条消息的过程') : null))),
366
+ React.createElement('p', { style: { margin: '10px 0 0', color: colors.muted, fontSize: 12 } }, '以上消息共享下面这次判断,判断耗时只记录一次。后续重判保留为另一条判断记录。')) : null,
367
+ React.createElement('p', { style: { margin: '10px 0', fontWeight: 600, lineHeight: 1.6 } }, view.conclusion),
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,
369
+ item.clarification ? readout('历史补充请求(只读)', item.clarification.question) : null,
370
+ item.kind === 'agent' ? React.createElement('div', { style: { display: 'grid', gap: 8, margin: '8px 0' } },
371
+ item.outputRef ? React.createElement(TraceReadout, { key: item.outputRef, runId, resourceRef: item.outputRef, label: `查看答复与依据${item.evidenceCount ? `(${item.evidenceCount} 条)` : ''}` }) : null,
372
+ item.sessionId && onOpenSession ? React.createElement(Button, { variant: 'outline', size: 'sm', type: 'button', style: { justifySelf: 'start' }, onClick: () => onOpenSession(item.sessionId) }, '查看会话记录') : null) : null,
373
+ item.reason ? React.createElement('p', { style: { color: colors.warning, whiteSpace: 'pre-wrap' } }, traceReason(item.reason)) : null,
374
+ item.sourceRunIds?.length > 1 ? React.createElement('p', { style: { color: colors.muted, fontSize: 12 } }, `同一话题的 ${new Set(item.sourceRunIds).size} 条消息共同判断,各事项的决定分别列出。`) : null,
375
+ item.gaps?.length ? React.createElement('p', { style: { color: colors.warning } }, `依据缺口:${item.gaps.join(';')}`) : null)
376
+ })),
377
+ ready ? React.createElement('span', { style: { color: colors.muted, fontSize: 12 } }, `本页 ${page.items?.length ?? 0} 条${Number.isFinite(page.total) ? `,共 ${page.total} 条处理记录` : ''};按记录时间排列,多事项可能分别判断。`) : null,
378
+ ready ? React.createElement('p', { style: { color: colors.muted, fontSize: 12, margin: 0 } }, '耗时按本次开始到完成计算,包含工具等待;重试显示最近一次处理,不代表纯模型耗时。进行中记录可刷新查看最新状态。') : null,
379
+ React.createElement('div', { style: { display: 'flex', gap: 8 } }, cursorHistory.length ? React.createElement(Button, { variant: 'outline', size: 'sm', type: 'button', disabled: loading, onClick: () => { setCursor(cursorHistory.at(-1)); setCursorHistory((items) => items.slice(0, -1)) } }, '上一页') : null, page?.nextCursor ? React.createElement(Button, { variant: 'outline', size: 'sm', type: 'button', disabled: loading, onClick: () => { setCursorHistory((items) => [...items, cursor]); setCursor(page.nextCursor) } }, '下一页') : null))
380
+ }
381
+ function TopicBrowser({ groups, tasks, target, updatedAt, onOpenTask, onOpenSession }) {
169
382
  const [groupId, setGroupId] = useState(target?.groupId || '')
170
383
  const [offset, setOffset] = useState(0)
171
384
  const [selection, setSelection] = useState(target)
172
385
  const [messageOffset, setMessageOffset] = useState(0)
173
386
  const [listing, setListing] = useState()
174
387
  const [context, setContext] = useState()
388
+ const [topicContext, setTopicContext] = useState()
389
+ const [contextCursor, setContextCursor] = useState('')
390
+ const [contextCursorHistory, setContextCursorHistory] = useState([])
391
+ const [contextRevision, setContextRevision] = useState()
392
+ const [intentCursor, setIntentCursor] = useState('')
393
+ const [intentCursorHistory, setIntentCursorHistory] = useState([])
394
+ const [selectedIntentRun, setSelectedIntentRun] = useState('')
395
+ const [contextError, setContextError] = useState('')
396
+ const [contextLoading, setContextLoading] = useState(false)
175
397
  const [listError, setListError] = useState('')
176
398
  const [detailError, setDetailError] = useState('')
177
399
  const [loading, setLoading] = useState(true)
@@ -179,6 +401,15 @@ window.__ModuleLoader__.load({
179
401
  const [retry, setRetry] = useState(0)
180
402
  const topicPageSize = 10
181
403
  const messagePageSize = 25
404
+ useEffect(() => {
405
+ if (!target?.topicId) return
406
+ setGroupId(target.groupId || '')
407
+ setSelection(target)
408
+ setMessageOffset(0)
409
+ setContextCursor('')
410
+ setContextCursorHistory([])
411
+ setIntentCursor(''); setIntentCursorHistory([]); setSelectedIntentRun(''); setContextRevision(undefined)
412
+ }, [target?.groupId, target?.topicId, target?.revision])
182
413
  useEffect(() => {
183
414
  let active = true
184
415
  setLoading(true); setListError('')
@@ -187,6 +418,14 @@ window.__ModuleLoader__.load({
187
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) })
188
419
  return () => { active = false }
189
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])
190
429
  useEffect(() => {
191
430
  if (!selection) { setContext(undefined); return }
192
431
  let active = true
@@ -195,10 +434,22 @@ window.__ModuleLoader__.load({
195
434
  get(`/state/topics/${encodeURIComponent(selection.topicId)}?${params}`).then((value) => { if (active) setContext(value) }, (error) => { if (active) setDetailError(error.message) }).finally(() => { if (active) setDetailLoading(false) })
196
435
  return () => { active = false }
197
436
  }, [selection, messageOffset, retry])
198
- const selectTopic = (topic) => { setSelection({ groupId: topic.groupId, topicId: topic.topicId, revision: topic.revision }); setMessageOffset(0) }
437
+ useEffect(() => {
438
+ if (!selection) { setTopicContext(undefined); return }
439
+ let active = true
440
+ setContextLoading(true); setContextError('')
441
+ const params = new URLSearchParams({ groupId: selection.groupId })
442
+ if (contextCursor) params.set('cursor', contextCursor)
443
+ if (intentCursor) params.set('intentCursor', intentCursor)
444
+ if (contextRevision !== undefined) params.set('revision', String(contextRevision))
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) })
446
+ return () => { active = false }
447
+ }, [selection, contextCursor, intentCursor, retry])
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) }
449
+ if (selectedIntentRun) return React.createElement(TraceDetail, { key: selectedIntentRun, runId: selectedIntentRun, onOpenSession, onBack: () => setSelectedIntentRun(''), backLabel: '返回话题判断列表' })
199
450
  const topic = context?.topic
200
451
  const group = groups.find((item) => item.groupId === groupId)
201
- const relatedTasks = tasks.filter((task) => task.groupId === selection?.groupId && task.topicRefs?.some((ref) => ref.topicId === selection?.topicId))
452
+ const relatedTasks = tasks.filter((task) => task.topicRefs?.some((ref) => (ref.groupId || task.sourceGroupId || task.groupId) === selection?.groupId && ref.topicId === selection?.topicId))
202
453
  const failure = (message) => React.createElement('div', { role: 'alert', style: { padding: 16, color: colors.danger } }, message, ' ', React.createElement(Button, { variant: 'outline', size: 'sm', type: 'button', onClick: () => setRetry((value) => value + 1) }, '重试'))
203
454
  const pager = (pageOffset, total, pageSize, onChange, busy) => React.createElement('div', { style: { ...tableFooter, flexWrap: 'wrap' } }, React.createElement('span', { style: { marginRight: 'auto', color: colors.muted, fontSize: 12 } }, `${total} 条 · 第 ${Math.floor(pageOffset / pageSize) + 1} 页`), React.createElement(Button, { variant: 'outline', size: 'sm', type: 'button', disabled: busy || pageOffset === 0, onClick: () => onChange(Math.max(0, pageOffset - pageSize)) }, '上一页'), React.createElement(Button, { variant: 'outline', size: 'sm', type: 'button', disabled: busy || pageOffset + pageSize >= total, onClick: () => onChange(pageOffset + pageSize) }, '下一页'))
204
455
  return React.createElement('section', { 'aria-label': '话题与上下文', style: { display: 'grid', gap: ui.space4 } },
@@ -215,19 +466,808 @@ window.__ModuleLoader__.load({
215
466
  React.createElement('header', { style: { padding: '17px 18px 15px', borderBottom: `1px solid ${colors.border}`, background: colors.cardSurface } }, React.createElement('h2', { title: topic?.title, style: { minWidth: 0, margin: 0, overflowWrap: 'anywhere', fontSize: 18, fontWeight: 650, lineHeight: 1.5, letterSpacing: '-.01em' } }, topic?.title || '话题详情')),
216
467
  detailError ? failure(`上下文加载失败:${detailError}`) : detailLoading ? React.createElement('div', { role: 'status' }, emptyState('正在读取固定版本上下文…')) : !context ? emptyState('选择一个话题查看消息和关联任务') : React.createElement('div', { role: 'region', 'aria-label': '话题详情内容', style: { minWidth: 0, maxHeight: 'calc(100vh - 260px)', padding: '0 18px 18px', display: 'grid', gap: 0, overflowY: 'auto', overflowX: 'hidden', overflowWrap: 'anywhere' } },
217
468
  React.createElement('section', { 'aria-label': '话题摘要', style: { marginTop: 16, padding: '13px 14px', borderLeft: `3px solid ${colors.accent}`, borderRadius: '0 9px 9px 0', background: `color-mix(in srgb, ${colors.accent} 6%, ${colors.surface2})` } }, React.createElement('strong', { style: { display: 'block', fontSize: 12 } }, '话题摘要'), React.createElement('p', { style: { margin: '6px 0 0', whiteSpace: 'pre-wrap', color: topic?.summary ? 'inherit' : colors.muted, fontSize: 13, lineHeight: 1.65 } }, topic?.summary || '暂无摘要')),
469
+ React.createElement('section', { 'aria-label': '当前上下文与判断记录', style: { padding: '16px 0', borderBottom: `1px solid ${colors.border}`, display: 'grid', gap: 10, fontSize: 12 } },
470
+ React.createElement('strong', { style: { fontSize: 13 } }, '当前上下文与判断记录'),
471
+ contextLoading ? React.createElement('span', { role: 'status' }, '正在读取话题上下文…') : contextError ? React.createElement('div', { role: 'alert', style: { color: colors.danger } }, `上下文读取失败:${contextError}`, React.createElement(Button, { variant: 'outline', size: 'sm', type: 'button', onClick: () => { setContextCursor(''); setContextCursorHistory([]); setIntentCursor(''); setIntentCursorHistory([]); setContextRevision(undefined); setRetry((value) => value + 1) } }, '重新读取当前上下文')) : topicContext ? React.createElement(React.Fragment, null,
472
+ readout('当前有效状态', topicContext.current),
473
+ React.createElement('strong', null, `事实 · ${(topicContext.facts || []).length} 条`),
474
+ ...(topicContext.facts || []).map((fact, index) => React.createElement('details', { key: fact.factId || index }, React.createElement('summary', { style: { cursor: 'pointer' } }, `${fact.kind || '事实'} · ${fact.text || '内容未记录'} · ${fact.status || '状态未记录'}`), readout('原文来源', fact.sourceRefs))),
475
+ React.createElement('strong', null, `意图判断批次 · ${(topicContext.intentRuns || []).length} 条`),
476
+ ...(topicContext.intentRuns || []).map((item, index) => React.createElement('details', { key: item.nodeRunId || item.intentRunId || index }, React.createElement('summary', { style: { cursor: 'pointer' } }, `${fmt(item.createdAt)} · ${item.status || '状态未记录'} · ${short(item.intentRunId)}`), readout('本批来源', item.sourceRunIds), readout('判断依据', item.reason), item.carrierRunId ? React.createElement(Button, { variant: 'outline', size: 'sm', type: 'button', onClick: () => setSelectedIntentRun(item.carrierRunId) }, '查看本次判断') : React.createElement('span', { style: { color: colors.muted } }, '历史未记录判断来源'))),
477
+ React.createElement('nav', { 'aria-label': '判断批次分页', style: { display: 'flex', gap: 8 } }, React.createElement(Button, { variant: 'outline', size: 'sm', type: 'button', disabled: !intentCursorHistory.length, onClick: () => { setIntentCursor(intentCursorHistory.at(-1)); setIntentCursorHistory((items) => items.slice(0, -1)) } }, '上一页判断'), React.createElement(Button, { variant: 'outline', size: 'sm', type: 'button', disabled: !topicContext.intentNextCursor, onClick: () => { setIntentCursorHistory((items) => [...items, intentCursor]); setIntentCursor(topicContext.intentNextCursor) } }, '下一页判断')),
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 } }, '历史未记录')),
218
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}` } },
219
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 } }, '当前没有待解决问题')),
220
- React.createElement('section', { 'aria-label': '关联任务', style: { minWidth: 0 } }, React.createElement('strong', { style: { display: 'block', marginBottom: 8, fontSize: 12 } }, `关联任务 · ${relatedTasks.length}`), React.createElement('div', { style: { display: 'flex', flexWrap: 'wrap', gap: 7 } }, ...relatedTasks.map((task) => React.createElement(Button, { key: task.taskId, variant: 'outline', size: 'sm', type: 'button', disabled: !task.childSessionId || task.state === 'queued', onClick: () => onOpenTask(task), style: { maxWidth: '100%' } }, React.createElement('span', { style: { overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' } }, task.title || task.objective))), relatedTasks.length === 0 ? React.createElement('span', { style: { color: colors.muted, fontSize: 12 } }, '暂无关联任务') : null))),
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))),
221
482
  React.createElement('section', { 'aria-label': '群聊消息', style: { marginTop: 4 } },
222
483
  React.createElement('strong', { style: { display: 'block', padding: '14px 0', fontSize: 13, fontWeight: 600 } }, `群聊消息 · ${context.total} 条`),
223
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('此版本没有消息')])),
224
485
  pager(messageOffset, context.total, messagePageSize, setMessageOffset, detailLoading))
225
486
  ))))
226
487
  }
488
+ function TaskStepOutput({ taskId, node, detailRevision }) {
489
+ return React.createElement(TaskStepOutputContent, { key: `${taskId}:${node.runId}:${node.nodeRunId}:${node.outputRef}`, taskId, node, detailRevision })
490
+ }
491
+ function TaskStepOutputContent({ taskId, node, detailRevision }) {
492
+ const [text, setText] = useState('')
493
+ const [overview, setOverview] = useState('')
494
+ const [documentName, setDocumentName] = useState('')
495
+ const [expanded, setExpanded] = useState(false)
496
+ const [cursor, setCursor] = useState(0)
497
+ const [nextCursor, setNextCursor] = useState(null)
498
+ const [loading, setLoading] = useState(true)
499
+ const [error, setError] = useState('')
500
+ const [retry, setRetry] = useState(0)
501
+ const loadedCursor = useRef(null)
502
+ useEffect(() => {
503
+ if (loadedCursor.current === cursor) { setLoading(false); return }
504
+ let active = true
505
+ setLoading(true); setError('')
506
+ get(`/state/tasks/${encodeURIComponent(taskId)}/runs/${encodeURIComponent(node.runId)}/nodes/${encodeURIComponent(node.nodeRunId)}/output?ref=${encodeURIComponent(node.outputRef)}&cursor=${cursor}&detailRevision=${encodeURIComponent(detailRevision)}`).then(value => {
507
+ if (active) { loadedCursor.current = cursor; setText(previous => cursor ? previous + value.text : value.text); setNextCursor(value.nextCursor); setOverview(value.overview || ''); setDocumentName(value.documentName || '') }
508
+ }, () => { if (active) setError('产出暂时无法读取,请重试。') }).finally(() => { if (active) setLoading(false) })
509
+ return () => { active = false }
510
+ }, [taskId, node.runId, node.nodeRunId, node.outputRef, detailRevision, cursor, retry])
511
+ const content = React.createElement('div', { style: { minWidth: 0, fontSize: 13, lineHeight: 1.7 } },
512
+ text ? React.createElement('div', { style: { display: 'grid', gap: 6, overflowWrap: 'anywhere' } }, ...text.split('\n\n').map((part, index) => {
513
+ const [heading, ...lines] = part.split('\n')
514
+ const titled = ['产出摘要', '正文', '任务要求', '发现', '限制与未确认事项', '执行范围', '相关文件', '已有文件', '新建文件', '材料正文', '已读取文件', '已修改文件', '涉及文件', '文件变更', '修改方案', '方案修订意见', '技术方案引用', '检查结果', '文件索引', '基线版本', '可修改文件', '变更文件', '提交说明', '分支', '远端', '提交版本', '执行结果', 'PR 标题', '目标仓库', '来源分支', '目标分支', 'PR 正文', 'PR 地址', 'PR 状态', '方案工件路径', '工作目录', '来源仓库', '隔离方式', '处理内容', '项目', '工作分支', '起点版本'].includes(heading)
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,
516
+ React.createElement('div', { style: { whiteSpace: 'pre-wrap' } }, titled ? lines.join('\n') : part))
517
+ })) : null,
518
+ loading ? React.createElement('span', { role: 'status', style: { color: colors.muted } }, '正在读取产出…') : null,
519
+ error ? React.createElement('div', { role: 'alert', style: { color: colors.danger } }, error, React.createElement(Button, { type: 'button', variant: 'ghost', size: 'sm', onClick: () => setRetry(value => value + 1) }, '重试读取产出')) : null,
520
+ !loading && !error && !text ? React.createElement('span', { style: { color: colors.muted } }, '已保存节点产出,暂未提供可读展示') : null,
521
+ !loading && !error && nextCursor !== null ? React.createElement(Button, { type: 'button', variant: 'ghost', size: 'sm', onClick: () => setCursor(nextCursor) }, '继续阅读产出') : null)
522
+ const documentLink = documentName ? React.createElement('a', { href: `${ENDPOINT}/state/tasks/${encodeURIComponent(taskId)}/runs/${encodeURIComponent(node.runId)}/nodes/${encodeURIComponent(node.nodeRunId)}/document?ref=${encodeURIComponent(node.outputRef)}&detailRevision=${encodeURIComponent(detailRevision)}`, style: { display: 'inline-block', marginTop: 6, color: colors.accent, fontSize: 12 } }, `下载 ${documentName}`) : null
523
+ return React.createElement('div', null, overview ? React.createElement('details', { onToggle: event => setExpanded(event.currentTarget.open), style: { fontSize: 13, lineHeight: 1.7 } },
524
+ React.createElement('summary', { style: { cursor: 'pointer', color: colors.muted } }, overview, React.createElement('span', { style: { marginLeft: 12, color: colors.accent, fontSize: 12 } }, expanded ? '收起产出' : '查看产出')),
525
+ expanded ? React.createElement('div', { style: { marginTop: 8 } }, content) : null) : content, documentLink)
526
+ }
527
+ const linkedTaskText = text => {
528
+ const content = String(text), parts = []
529
+ let offset = 0
530
+ for (const match of content.matchAll(/\[([^\]\n]+)\]\((https?:\/\/[^\s)]+)\)|(https?:\/\/[^\s<>")。,;]+)/g)) {
531
+ const href = (match[2] || match[3]).replace(/[.,;!?]+$/, '')
532
+ parts.push(content.slice(offset, match.index), React.createElement('a', { key: match.index, href, target: '_blank', rel: 'noopener noreferrer', style: { color: colors.accent } }, match[1] || href))
533
+ if (!match[2]) parts.push(match[3].slice(href.length))
534
+ offset = match.index + match[0].length
535
+ }
536
+ parts.push(content.slice(offset))
537
+ return parts
538
+ }
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 }))
1227
+ }
227
1228
  function ObserverContent({ openSession }) {
228
- 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
+ }, [])
229
1240
  const [topicTarget, setTopicTarget] = useState()
230
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
+ }
231
1271
  const [error, setError] = useState()
232
1272
  const [navigationError, setNavigationError] = useState()
233
1273
  const [updatedAt, setUpdatedAt] = useState()
@@ -239,20 +1279,29 @@ window.__ModuleLoader__.load({
239
1279
  const [messageDeliveryFilter, setMessageDeliveryFilter] = useState('all')
240
1280
  const [outboxStatusFilter, setOutboxStatusFilter] = useState('all')
241
1281
  const [hoveredTaskId, setHoveredTaskId] = useState('')
242
- const [expandedCheckpointTaskId, setExpandedCheckpointTaskId] = useState('')
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)
1288
+ const [selectedMessageRunId, setSelectedMessageRunId] = useState('')
243
1289
  const [copiedId, setCopiedId] = useState('')
244
1290
  const [navigatingSessionId, setNavigatingSessionId] = useState('')
1291
+ const [historyPage, setHistoryPage] = useState(1)
245
1292
  const [alertType, setAlertType] = useState('all')
246
1293
  const [alertView, setAlertView] = useState('active')
247
1294
  const [resolvedAlertPage, setResolvedAlertPage] = useState(1)
248
- const [authorizationComments, setAuthorizationComments] = useState({})
249
- const [decidingAuthorizationId, setDecidingAuthorizationId] = useState('')
250
1295
  const [authorizationPage, setAuthorizationPage] = useState(1)
251
1296
  const [authorizationFilter, setAuthorizationFilter] = useState('all')
252
1297
  const [selectedAuthorizationId, setSelectedAuthorizationId] = useState('')
253
- const refresh = useCallback(async () => {
254
- try { setData(await load()); setUpdatedAt(new Date()); setError(undefined) }
255
- 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
256
1305
  }, [])
257
1306
  const manualRefresh = useCallback(async () => {
258
1307
  if (refreshState === 'refreshing') return
@@ -261,6 +1310,19 @@ window.__ModuleLoader__.load({
261
1310
  setRefreshState('done')
262
1311
  window.setTimeout(() => setRefreshState('idle'), 1200)
263
1312
  }, [refresh, refreshState])
1313
+ const pendingDetail = useRef(null)
1314
+ useEffect(() => {
1315
+ if (!selectedTaskId) { setTaskDetail(undefined); setDetailError(''); return }
1316
+ let active = true
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) })
1324
+ return () => { active = false }
1325
+ }, [selectedTaskId, updatedAt, detailRetry])
264
1326
  useEffect(() => {
265
1327
  refresh()
266
1328
  const timer = window.setInterval(refresh, 5000)
@@ -274,12 +1336,15 @@ window.__ModuleLoader__.load({
274
1336
  return () => document.removeEventListener('click', closeForSession, true)
275
1337
  }, [])
276
1338
  const groupsById = new Map((data?.groups || []).map((group) => [group.groupId, group]))
1339
+ const tasksById = new Map((data?.tasks || []).map(task => [task.taskId, task]))
277
1340
  const topicsById = new Map((data?.topics || []).map((topic) => [`${topic.groupId}:${topic.topicId}`, topic]))
278
1341
  const navigate = async (sessionId, parentSessionId) => {
279
1342
  try { setNavigationError(undefined); setNavigatingSessionId(sessionId); await openSession(sessionId, parentSessionId); setOpen(false) }
280
1343
  catch (cause) { setNavigationError(cause instanceof Error ? cause.message : String(cause)) }
281
1344
  finally { setNavigatingSessionId('') }
282
1345
  }
1346
+ const openTask = task => { setCreatingTask(false); setSelectedTaskId(task.taskId); setActivePage('tasks') }
1347
+ const returnFromTask = () => { if (draftDirty.current) { setPendingNavigation({ back: true }); return } setSelectedTaskId(''); setCreatingTask(false) }
283
1348
  const copyId = async (value) => {
284
1349
  let copied = false
285
1350
  try {
@@ -302,10 +1367,11 @@ window.__ModuleLoader__.load({
302
1367
  }
303
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}`)
304
1369
  const selectedGroup = groupsById.get(selectedGroupId) || (data?.groups || [])[0]
305
- const selectedMessages = [...(selectedGroup?.messages || [])].filter((message) => message.sourceKind === 'dingtalk' || message.sourceKind === 'migration' || !message.sourceKind).sort((left, right) => Number(right.sequence || 0) - Number(left.sequence || 0))
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))
306
1371
  const messageWorkflowState = (message) => {
307
1372
  if (message.routingStatus === 'failed') return 'failed'
308
1373
  if (message.routingStatus !== 'routed') return 'routing'
1374
+ if (message.sourceKind === 'workflow-v2') return 'processed'
309
1375
  const related = (selectedGroup?.topics || []).flatMap((topic) => {
310
1376
  const entry = [...(topic.entries || [])].reverse().find((item) => item.messageId === message.messageId)
311
1377
  return entry?.action === 'add' && entry.messageVersion === message.messageVersion ? [{ topic, entry }] : []
@@ -326,12 +1392,27 @@ window.__ModuleLoader__.load({
326
1392
  }
327
1393
  const messageRows = visibleMessages.map((message, rowIndex) => {
328
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
329
1407
  return React.createElement('tr', { key: message.messageId, style: { background: rowIndex % 2 ? `color-mix(in srgb, ${colors.surface2} 55%, transparent)` : colors.cardSurface } },
330
- React.createElement('td', { style: { ...tableBodyCell, width: 104 }, title: message.agentDeliveryError || '' }, clampTableContent(tableStatusTag(status.label, status.state, { fontWeight: 600 }))),
1408
+ React.createElement('td', { style: { ...tableBodyCell, width: 144 }, title: traceReason(message.workflowStatusDetail || message.agentDeliveryError) || '' }, clampTableContent(tableStatusTag(status.label, status.state, { fontWeight: 600 }))),
331
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)))),
332
- React.createElement('td', { style: { ...tableBodyCell, width: 160 } }, React.createElement('div', { style: { display: 'flex', gap: 4, minWidth: 0, overflow: 'hidden' } }, ...(message.topicRefs?.length ? message.topicRefs.map((ref) => React.createElement('button', { key: ref.topicId, type: 'button', title: ref.title, 'aria-label': `话题 ${ref.title}`, onClick: () => { setTopicTarget({ groupId: selectedGroup.groupId, topicId: ref.topicId, revision: ref.revision }); setActivePage('topics') }, style: { ...pill(colors.accent), display: 'inline-block', flex: '0 1 auto', minWidth: 0, maxWidth: 120, border: 0, padding: '2px 7px', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', cursor: 'pointer', fontFamily: 'inherit' } }, ref.title)) : [React.createElement('span', { key: 'unrouted', style: { color: colors.muted, fontSize: 12 } }, '待归类')]))),
333
- React.createElement('td', { title: message.text, style: tableBodyCell }, clampTableContent(message.text || '(空消息)')),
334
- React.createElement('td', { style: { ...tableBodyCell, width: 240 } }, singleLineTableContent(React.createElement('code', { title: message.messageId, style: { fontSize: 14, color: colors.muted, whiteSpace: 'nowrap' } }, `#${message.sequence ?? '—'} · ${short(message.messageId)}`))))
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))
335
1416
  })
336
1417
  const selectedOutbox = [...(selectedGroup?.outbox || [])].reverse()
337
1418
  const outboxState = (message) => outboxDelivery(message).id
@@ -343,53 +1424,62 @@ window.__ModuleLoader__.load({
343
1424
  const outboxRows = visibleOutbox.map((message, rowIndex) => {
344
1425
  const status = outboxDelivery(message)
345
1426
  return React.createElement('tr', { key: message.outboundId, style: { background: rowIndex % 2 ? `color-mix(in srgb, ${colors.surface2} 55%, transparent)` : colors.cardSurface } },
346
- 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 }))),
347
1428
  React.createElement('td', { title: message.text, style: tableBodyCell }, clampTableContent(message.text || '(空消息)'),
348
- 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,
349
1431
  message.deliveryAttemptedAt ? React.createElement('div', { style: { marginTop: 3, fontSize: 11, color: colors.muted } }, `最近尝试 ${fmt(message.deliveryAttemptedAt)} · 共 ${message.deliveryAttemptCount || 1} 次`) : null),
350
1432
  React.createElement('td', { style: { ...tableBodyCell, width: 220 } },
351
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)),
352
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' ? '历史未记录' : '尚未确认'))),
353
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)))))
354
1436
  })
355
- const renderTaskCard = (task) => {
356
- const group = groupsById.get(task.groupId)
1437
+ const renderTaskCard = task => {
1438
+ const sourceGroup = groupsById.get(task.sourceGroupId || task.groupId)
1439
+ const groupLabel = sourceGroup?.name || (task.groupId?.startsWith('web:') ? 'Web 任务' : task.groupId) || '未关联群聊'
1440
+ const session = taskSession(task)
1441
+ const hovered = hoveredTaskId === task.taskId
357
1442
  const queued = task.state === 'queued'
358
- const checkpointEvents = task.checkpoints || []
359
- const planCheckpointIndex = checkpointEvents.findLastIndex((checkpoint) => checkpoint.kind === 'plan-confirmed')
360
- const planCheckpoint = checkpointEvents[planCheckpointIndex]
361
- const currentCheckpointEvents = checkpointEvents.slice(Math.max(0, planCheckpointIndex))
362
- const checkpoints = planCheckpoint?.remainingItems?.length ? planCheckpoint.remainingItems : (task.stageTasks?.length ? task.stageTasks : [task.objective])
1443
+ const sessions = task.executionSessions || []
1444
+ const completedSessions = sessions.filter(item => ['succeeded', 'completed'].includes(item.status)).length
363
1445
  const displayObjective = String(task.objective || '').startsWith('[TASK_SOURCE_EVIDENCE]') ? (task.title || task.objective) : task.objective
364
- const latestCheckpoint = checkpointEvents.at(-1)
365
- const latestRemainingItems = latestCheckpoint?.remainingItems || checkpoints
366
- const remainingItemsMatchPlan = latestRemainingItems.every((checkpoint) => checkpoints.includes(checkpoint))
367
- const normalizedRemainingItems = remainingItemsMatchPlan ? latestRemainingItems : checkpoints.slice(-Math.min(checkpoints.length, latestRemainingItems.length))
368
- const remainingCheckpointNames = new Set(normalizedRemainingItems)
369
- const completedCheckpointNames = new Set(task.state === 'completed' ? checkpoints : checkpoints.filter((checkpoint) => !remainingCheckpointNames.has(checkpoint)))
370
- const currentCheckpoint = task.state === 'running' ? checkpoints.find((checkpoint) => remainingCheckpointNames.has(checkpoint)) : undefined
371
- const completedCheckpointCount = checkpoints.filter((checkpoint) => completedCheckpointNames.has(checkpoint)).length
372
- const progress = Math.round((completedCheckpointCount / checkpoints.length) * 100)
373
- const showCheckpoints = task.state !== 'completed' && !task.archivedAt
374
- const checkpointsExpanded = expandedCheckpointTaskId === task.taskId
375
- const hovered = hoveredTaskId === task.taskId
376
- const navigating = navigatingSessionId === task.childSessionId
377
- return React.createElement('div', { key: task.taskId, onMouseEnter: () => setHoveredTaskId(task.taskId), onMouseLeave: () => setHoveredTaskId(''), style: { width: '100%', minWidth: 0, boxSizing: 'border-box', border: `1px solid ${colors.border}`, borderRadius: 12, background: colors.cardSurface, boxShadow: hovered ? '0 8px 20px rgba(15,23,42,.08), 0 2px 6px rgba(15,23,42,.05)' : 'var(--dsw-shadow-card, 0 1px 2px rgba(0,0,0,.08))', padding: '11px 13px', color: 'inherit', display: 'grid', gap: 6, opacity: queued ? 0.72 : 1, transition: 'box-shadow 180ms ease' } },
378
- React.createElement('button', { type: 'button', disabled: queued || navigating, onClick: (event) => { if (event.target.closest('[data-task-card-action]')) return; navigate(task.childSessionId, group?.residentSessionId) }, style: { width: '100%', minWidth: 0, boxSizing: 'border-box', border: 0, padding: 0, background: 'transparent', color: 'inherit', cursor: queued || navigating ? 'default' : 'pointer', textAlign: 'left', display: 'grid', gap: 6, fontFamily: 'inherit' } },
379
- React.createElement('strong', { title: task.title || task.objective, style: { fontSize: 13, lineHeight: 1.45, display: '-webkit-box', WebkitLineClamp: 2, WebkitBoxOrient: 'vertical', overflow: 'hidden', overflowWrap: 'anywhere' } }, task.title || task.objective),
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))),
380
1449
  React.createElement('div', { style: { display: 'flex', alignItems: 'center', gap: 5, minWidth: 0, overflow: 'hidden' } },
381
- React.createElement('span', { title: group?.name || task.groupId, style: { ...pill(colors.muted), flex: '0 1 auto', maxWidth: 150, padding: '2px 7px', fontSize: 10.5, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' } }, group?.name || task.groupId),
382
- ...(task.topicRefs || []).map((ref) => { const topic = topicsById.get(`${task.groupId}:${ref.topicId}`); const label = topic?.title || `话题 ${short(ref.topicId)}`; return React.createElement('span', { key: ref.topicId, role: 'button', tabIndex: 0, title: label, 'aria-label': `话题 ${label}`, 'data-task-card-action': 'open-topic', onClick: (event) => { event.stopPropagation(); setTopicTarget({ groupId: task.groupId, topicId: ref.topicId, revision: ref.revision }); setActivePage('topics') }, onKeyDown: (event) => { if (event.key !== 'Enter' && event.key !== ' ') return; event.preventDefault(); event.stopPropagation(); setTopicTarget({ groupId: task.groupId, topicId: ref.topicId, revision: ref.revision }); setActivePage('topics') }, style: { ...pill(colors.accent), display: 'inline-block', flex: '0 1 auto', minWidth: 0, maxWidth: 96, padding: '2px 7px', fontSize: 10.5, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', cursor: 'pointer' } }, label) })),
383
- React.createElement('div', { title: displayObjective, style: { minWidth: 0, display: 'block', maxHeight: '4.5em', overflow: 'hidden', overflowWrap: 'anywhere', fontSize: 11, lineHeight: 1.5 } }, React.createElement('svg', { width: 12, height: 12, viewBox: '0 0 16 16', fill: 'none', stroke: 'currentColor', strokeWidth: 1.4, strokeLinecap: 'round', strokeLinejoin: 'round', role: 'img', 'aria-label': '任务目标', style: { display: 'inline-block', verticalAlign: -2, marginRight: 5, color: colors.muted } }, React.createElement('circle', { cx: 8, cy: 8, r: 5.5 }), React.createElement('circle', { cx: 8, cy: 8, r: 2 }), React.createElement('path', { d: 'M8 1v2M15 8h-2' })), displayObjective),
384
- React.createElement('div', null,
385
- showCheckpoints ? React.createElement('section', null,
386
- React.createElement('div', { role: 'button', tabIndex: 0, 'aria-expanded': checkpointsExpanded, 'aria-label': checkpointsExpanded ? '收起检查点' : `展开全部 ${checkpoints.length} 个检查点`, 'data-task-card-action': 'toggle-checkpoints', onClick: (event) => { event.stopPropagation(); setExpandedCheckpointTaskId(checkpointsExpanded ? '' : task.taskId) }, onKeyDown: (event) => { if (event.key !== 'Enter' && event.key !== ' ') return; event.preventDefault(); event.stopPropagation(); setExpandedCheckpointTaskId(checkpointsExpanded ? '' : task.taskId) }, style: { minHeight: 24, boxSizing: 'border-box', display: 'flex', alignItems: 'center', gap: 8, fontSize: 10.5, cursor: 'pointer' } }, React.createElement('span', { style: { flex: '0 0 auto', display: 'inline-flex', alignItems: 'center', gap: 4, color: colors.muted } }, React.createElement(IconChecklistOutline14, { size: 12, 'aria-label': '任务' }), '任务'), React.createElement('div', { role: 'progressbar', 'aria-label': '检查点进度', 'aria-valuemin': 0, 'aria-valuemax': 100, 'aria-valuenow': progress, style: { flex: '1 1 auto', minWidth: 24, height: 4, borderRadius: 999, overflow: 'hidden', background: `color-mix(in srgb, ${colors.muted} 22%, transparent)` } }, React.createElement('div', { style: { width: `${progress}%`, height: '100%', borderRadius: 'inherit', background: completedCheckpointCount === checkpoints.length ? statusTone.done : colors.accent, transition: 'width 180ms ease' } })), React.createElement('span', { style: { flex: '0 0 auto', color: completedCheckpointCount === checkpoints.length ? statusTone.done : colors.muted } }, `${completedCheckpointCount} / ${checkpoints.length}`), React.createElement(checkpointsExpanded ? IconChevronUpOutline14 : IconChevronDownOutline14, { size: 14, 'aria-hidden': true, style: { flex: '0 0 auto', color: colors.muted } })),
387
- checkpointsExpanded ? React.createElement('div', { style: { display: 'grid', gap: 4, padding: '2px 9px 9px' } }, ...checkpoints.map((checkpoint, index) => { const completed = completedCheckpointNames.has(checkpoint); const current = !completed && currentCheckpoint === checkpoint; const tone = completed ? statusTone.done : current ? colors.accent : colors.muted; const duration = checkpointDuration(checkpoint, currentCheckpointEvents, Date.now()); return React.createElement('div', { key: `${index}:${checkpoint}`, title: checkpoint, style: { minWidth: 0, display: 'grid', gridTemplateColumns: '12px minmax(0,1fr) 64px', gap: 5, alignItems: 'start', fontSize: 10.5, lineHeight: 1.4 } }, React.createElement('span', { 'aria-hidden': true, style: { width: 12, height: 14, display: 'inline-flex', alignItems: 'center', justifyContent: 'center', color: tone } }, completed ? React.createElement(CheckpointDoneIcon, { size: 12 }) : current ? React.createElement(StateDot, { state: 'ongoing', size: 10 }) : '○'), React.createElement('span', { style: { color: 'inherit', display: '-webkit-box', WebkitLineClamp: 'unset', WebkitBoxOrient: 'vertical', overflow: 'hidden', overflowWrap: 'anywhere' } }, checkpoint), React.createElement('span', { title: `执行时长 ${duration}`, style: { width: 64, color: colors.muted, textAlign: 'right', whiteSpace: 'nowrap', fontVariantNumeric: 'tabular-nums' } }, duration)) })) : null) : null,
388
- task.waitingReason ? React.createElement('div', { title: task.waitingReason, style: { fontSize: 10.5, lineHeight: 1.45, color: colors.warning } }, `等待:${task.waitingReason}`) : null),
389
- React.createElement('div', { style: { minWidth: 0, marginTop: 0, paddingTop: 6, borderTop: `1px solid ${colors.border}`, display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 8 } },
390
- React.createElement('span', { title: `最后活动 ${fmt(task.updatedAt)}`, style: { minWidth: 0, display: 'inline-flex', alignItems: 'center', gap: 4, fontSize: 10.5, color: colors.muted, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' } }, React.createElement('svg', { width: 12, height: 12, viewBox: '0 0 24 24', fill: 'none', stroke: 'currentColor', strokeWidth: 1.8, strokeLinecap: 'round', strokeLinejoin: 'round', 'aria-label': '最后活动时间' }, React.createElement('circle', { cx: 12, cy: 12, r: 9 }), React.createElement('path', { d: 'M12 7v5l3 2' })), fmt(task.updatedAt)),
391
- task.state === 'completed' && !task.archivedAt ? React.createElement('button', { type: 'button', onClick: async (event) => { event.stopPropagation(); try { await post(`/tasks/${encodeURIComponent(task.taskId)}/archive`); await refresh() } catch (cause) { setNavigationError(cause instanceof Error ? cause.message : String(cause)) } }, style: { flex: '0 0 auto', border: `1px solid ${colors.border}`, borderRadius: 7, background: colors.surface2, color: colors.muted, padding: '3px 7px', cursor: 'pointer', fontFamily: 'inherit', fontSize: 10.5 } }, '归档') : null),
392
- navigating ? React.createElement('div', { role: 'status', style: { fontSize: 11.5, color: colors.accent } }, '正在打开会话…') : queued ? React.createElement('div', { style: { fontSize: 11.5, color: colors.muted } }, '等待执行,尚无对话和轨迹') : null))
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 ? '历史只读' : '已归档'))))
393
1483
  }
394
1484
  const alertCategories = [
395
1485
  { id: 'all', label: '全部类型' },
@@ -422,12 +1512,14 @@ window.__ModuleLoader__.load({
422
1512
  const resolvedAlertPageCount = Math.max(1, Math.ceil(filteredResolvedAlerts.length / resolvedAlertPageSize))
423
1513
  const currentResolvedAlertPage = Math.min(resolvedAlertPage, resolvedAlertPageCount)
424
1514
  const visibleResolvedAlerts = filteredResolvedAlerts.slice((currentResolvedAlertPage - 1) * resolvedAlertPageSize, currentResolvedAlertPage * resolvedAlertPageSize)
425
- const bucketColumns = taskBuckets.map((bucket) => {
426
- const tasks = (data?.tasks || []).filter((task) => task.state === bucket.state && !task.archivedAt).sort((left, right) => String(right.updatedAt).localeCompare(String(left.updatedAt)))
427
- 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 } }, '暂无任务')]
428
- 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' } },
429
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)),
430
- 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)
431
1523
  })
432
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' } },
433
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)))
@@ -443,19 +1535,22 @@ window.__ModuleLoader__.load({
443
1535
  updatedAt ? React.createElement('span', { 'aria-hidden': true, style: { width: 1, height: 12, background: colors.border } }) : null,
444
1536
  updatedAt ? React.createElement('span', { title: fmt(updatedAt) }, `更新于 ${fmtTime(updatedAt)}`) : null),
445
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)),
446
- 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)
447
1540
  const dataViewTabs = React.createElement('div', { role: 'tablist', 'aria-label': '群聊数据视图', style: { display: 'flex', alignSelf: 'stretch', gap: 20 } },
448
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)))
449
1542
  const groupTableToolbar = React.createElement('div', { style: toolbar },
450
1543
  dataViewTabs,
451
1544
  React.createElement('div', { style: { display: 'flex', alignItems: 'center', justifyContent: 'flex-end', gap: 8, flexWrap: 'wrap' } },
452
- (data?.groups || []).length ? React.createElement(SelectMenu, { label: '选择群聊会话', value: selectedGroup?.groupId || '', options: (data?.groups || []).map((group) => ({ id: group.groupId, label: group.name || group.groupId })), onChange: (value) => { setSelectedGroupId(value); setMessagePage(1); setOutboxPage(1) }, fitContent: true }) : null,
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,
453
1548
  groupTableView === 'messages'
454
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) } })
455
- : 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) } })))
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) } })))
456
1551
  const messagesTable = React.createElement(React.Fragment, null,
457
- React.createElement('div', { style: { overflowX: 'auto' } }, React.createElement('table', { style: { width: '100%', minWidth: 910, borderCollapse: 'collapse', tableLayout: 'fixed' } },
458
- 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 } }, '消息'))),
459
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 } }, '暂无符合条件的群聊消息'))])))),
460
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)) }, '下一页')))
461
1556
  const outboxTable = React.createElement(React.Fragment, null,
@@ -463,71 +1558,63 @@ window.__ModuleLoader__.load({
463
1558
  React.createElement('thead', null, React.createElement('tr', { style: { background: colors.surface2, textAlign: 'left' } }, React.createElement('th', { style: { ...tableHeadCell, width: 124 } }, '发送状态'), React.createElement('th', { style: tableHeadCell }, '消息内容'), React.createElement('th', { style: { ...tableHeadCell, width: 220 } }, '来源 / 回复目标'), React.createElement('th', { style: { ...tableHeadCell, width: 220 } }, '投递消息 ID'), React.createElement('th', { style: { ...tableHeadCell, width: 220 } }, '发信箱 ID'))),
464
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 发件记录'))])))),
465
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)) }, '下一页')))
466
- const groupsPage = React.createElement('section', null, React.createElement('div', { style: tableFrame }, groupTableToolbar, groupTableView === 'messages' ? messagesTable : outboxTable))
467
- const tasksPage = React.createElement('div', { className: 'observer-task-board', style: { display: 'grid', gridTemplateColumns: 'repeat(4, minmax(300px, 1fr))', gap: ui.space3, overflowX: 'auto', alignItems: 'start', paddingBottom: ui.space2 } }, ...bucketColumns)
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))
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))
468
1570
  const authorizationStatus = {
469
1571
  'pending-send': { label: '待发送', state: 'warning' },
1572
+ 'sending-unknown': { label: '投递待确认', state: 'warning' },
470
1573
  'waiting-reply': { label: '等待处理', state: 'warning' },
471
1574
  answered: { label: '已处理', state: 'done' },
472
1575
  superseded: { label: '已失效', state: 'neutral' },
473
1576
  }
474
1577
  const authorizationDecision = { approved: { label: '已继续', state: 'done' }, rejected: { label: '不执行', state: 'error' } }
475
- 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'
476
1579
  const authorizationPresentation = (item) => item.status === 'answered'
477
- ? authorizationDecision[item.decision] || authorizationStatus.answered
1580
+ ? item.taskSessionRequest ? { label: '已答复', state: 'done' } : authorizationDecision[item.decision] || authorizationStatus.answered
478
1581
  : authorizationStatus[item.status] || { label: '状态异常', state: 'error' }
479
- const decideAuthorization = async (requestId, decision) => {
480
- try {
481
- setNavigationError(undefined); setDecidingAuthorizationId(requestId)
482
- await post(`/authorizations/${encodeURIComponent(requestId)}/decision`, { decision, comment: authorizationComments[requestId] || '' })
483
- await refresh()
484
- } catch (cause) { setNavigationError(cause instanceof Error ? cause.message : String(cause)) }
485
- finally { setDecidingAuthorizationId('') }
486
- }
487
1582
  const authorizationItems = [...(data?.authorizations || [])].sort((left, right) => {
488
1583
  const leftPending = isPendingAuthorization(left) ? 1 : 0; const rightPending = isPendingAuthorization(right) ? 1 : 0
489
1584
  return rightPending - leftPending || String(right.createdAt || '').localeCompare(String(left.createdAt || ''))
490
1585
  })
491
- 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))
492
1587
  const authorizationPageSize = 10
493
1588
  const authorizationPageCount = Math.max(1, Math.ceil(filteredAuthorizationItems.length / authorizationPageSize))
494
1589
  const currentAuthorizationPage = Math.min(authorizationPage, authorizationPageCount)
495
1590
  const visibleAuthorizationItems = filteredAuthorizationItems.slice((currentAuthorizationPage - 1) * authorizationPageSize, currentAuthorizationPage * authorizationPageSize)
496
1591
  const selectedAuthorization = authorizationItems.find((item) => item.requestId === selectedAuthorizationId)
497
- const selectedAuthorizationPending = selectedAuthorization ? isPendingAuthorization(selectedAuthorization) : false
498
1592
  const authorizationDetailField = (label, value) => React.createElement('div', { style: { display: 'grid', gap: 5 } },
499
1593
  React.createElement('strong', { style: { fontSize: 11, color: colors.muted } }, label),
500
- 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 || '—')))
501
1595
  const authorizationRows = visibleAuthorizationItems.map((item, rowIndex) => {
502
1596
  const status = authorizationPresentation(item)
503
1597
  const group = groupsById.get(item.groupId)
504
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 } },
505
1599
  React.createElement('div', { style: { justifySelf: 'start' } }, tableStatusTag(status.label, status.state, { fontWeight: 600 })),
506
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)),
507
- 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),
508
1602
  React.createElement('span', { style: { color: colors.muted, fontSize: 11, lineHeight: 1.5 } }, fmt(item.createdAt)),
509
1603
  React.createElement(Button, { variant: 'ghost', size: 'sm', type: 'button', onClick: () => setSelectedAuthorizationId(item.requestId) }, '查看'))
510
1604
  })
511
- 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)' } },
512
- 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('') },
513
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 } },
514
- 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)),
515
1608
  React.createElement(Button, { variant: 'ghost', size: 'sm', type: 'button', 'aria-label': '关闭人工介入事项', onClick: () => setSelectedAuthorizationId(''), style: { flex: '0 0 auto', minWidth: 52, whiteSpace: 'nowrap' } }, '关闭')),
516
- React.createElement('div', { style: { flex: '1 1 auto', overflowY: 'auto', padding: '0 24px 24px' } },
517
- React.createElement('section', { style: { padding: '24px 0', borderBottom: `1px solid ${colors.border}` } }, authorizationDetailField('需要人工处理', selectedAuthorization.requestedAction)),
518
- React.createElement('section', { style: { padding: '24px 0', borderBottom: `1px solid ${colors.border}` } }, authorizationDetailField('风险', selectedAuthorization.risk || '未单独说明')),
519
- React.createElement('section', { style: { padding: '24px 0', borderBottom: `1px solid ${colors.border}` } }, authorizationDetailField('阻塞原因', selectedAuthorization.waitingReason)),
520
- 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')))),
521
- 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 })),
522
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 || '—'}`)),
523
- 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),
524
- 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 } },
525
- 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 } }),
526
- React.createElement(Button, { variant: 'outline', size: 'sm', type: 'button', disabled: decidingAuthorizationId === selectedAuthorization.requestId, onClick: () => decideAuthorization(selectedAuthorization.requestId, 'rejected'), style: { color: colors.danger } }, '不执行'),
527
- 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
528
1615
  const authorizationsPage = React.createElement(React.Fragment, null, React.createElement('section', null,
529
1616
  React.createElement('div', { style: tableFrame },
530
- 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 })),
531
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, '操作')),
532
1619
  React.createElement('div', null, ...(authorizationRows.length ? authorizationRows : [React.createElement('div', { key: 'empty', style: { padding: 36, textAlign: 'center', color: colors.muted, fontSize: 12 } }, '暂无人工介入事项')])),
533
1620
  React.createElement('div', { style: tableFooter },
@@ -536,8 +1623,13 @@ window.__ModuleLoader__.load({
536
1623
  React.createElement('span', { style: { fontSize: 11, color: colors.muted } }, `${currentAuthorizationPage} / ${authorizationPageCount}`),
537
1624
  React.createElement(Button, { variant: 'outline', size: 'sm', type: 'button', disabled: currentAuthorizationPage >= authorizationPageCount, onClick: () => setAuthorizationPage((page) => Math.min(authorizationPageCount, page + 1)) }, '下一页')))
538
1625
  ), authorizationDetail)
539
- const archivedTasks = (data?.tasks || []).filter((task) => task.state === 'completed' && task.archivedAt).sort((left, right) => String(right.archivedAt).localeCompare(String(left.archivedAt)))
540
- 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) }, '下一页')))
541
1633
  const alertsPage = React.createElement(React.Fragment, null,
542
1634
  React.createElement('section', null, React.createElement('div', { style: tableFrame },
543
1635
  React.createElement('div', { style: toolbar },
@@ -546,30 +1638,31 @@ window.__ModuleLoader__.load({
546
1638
  React.createElement('div', { style: { padding: '0 16px' } }, ...(alertView === 'active' ? (filteredActiveAlerts.length ? filteredActiveAlerts.map(renderAlert) : [React.createElement('div', { key: 'empty', style: { padding: '36px 0', textAlign: 'center', color: colors.muted, fontSize: 12 } }, '当前没有此类型异常')]) : (visibleResolvedAlerts.length ? visibleResolvedAlerts.map(renderAlert) : [React.createElement('div', { key: 'empty', style: { padding: '36px 0', textAlign: 'center', color: colors.muted, fontSize: 12 } }, '暂无此类型恢复记录')]))),
547
1639
  alertView === 'resolved' ? React.createElement('div', { style: tableFooter }, React.createElement('span', { style: { marginRight: 'auto', fontSize: 11, color: colors.muted } }, `${filteredResolvedAlerts.length} 条 · 每页 ${resolvedAlertPageSize} 条`), React.createElement(Button, { variant: 'outline', size: 'sm', type: 'button', disabled: currentResolvedAlertPage <= 1, onClick: () => setResolvedAlertPage((page) => Math.max(1, page - 1)) }, '上一页'), React.createElement('span', { style: { fontSize: 11, color: colors.muted } }, `${currentResolvedAlertPage} / ${resolvedAlertPageCount}`), React.createElement(Button, { variant: 'outline', size: 'sm', type: 'button', disabled: currentResolvedAlertPage >= resolvedAlertPageCount, onClick: () => setResolvedAlertPage((page) => Math.min(resolvedAlertPageCount, page + 1)) }, '下一页')) : null))
548
1640
  )
549
- const topicsPage = React.createElement(TopicBrowser, { groups: data?.groups || [], tasks: data?.tasks || [], target: topicTarget, updatedAt, onOpenTask: (task) => navigate(task.childSessionId, groupsById.get(task.groupId)?.residentSessionId) })
1641
+ const topicsPage = React.createElement(TopicBrowser, { groups: data?.groups || [], tasks: data?.tasks || [], target: topicTarget, updatedAt, onOpenTask: openTask, onOpenSession: navigate })
550
1642
  const pageContent = activePage === 'topics' ? topicsPage : activePage === 'tasks' ? tasksPage : activePage === 'authorizations' ? authorizationsPage : activePage === 'archive' ? archivePage : activePage === 'alerts' ? alertsPage : groupsPage
551
- 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)
552
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' } },
553
1645
  error ? React.createElement('div', { style: { ...card, borderColor: colors.danger, color: colors.danger } }, `无法连接 resident 插件:${error}`) : null,
554
- navigationError ? React.createElement('div', { style: { ...card, borderColor: colors.danger, color: colors.danger } }, `无法打开 DSH Session:${navigationError}`) : null,
1646
+ archiveFeedback ? React.createElement('div', { role: archiveFeedback.error ? 'alert' : 'status', style: { ...card, color: archiveFeedback.error ? colors.danger : colors.muted } }, archiveFeedback.text) : null,
1647
+ navigationError ? React.createElement('div', { role: 'alert', style: { ...card, borderColor: colors.danger, color: colors.danger } }, `操作失败:${navigationError}`) : null,
555
1648
  pageViewport
556
1649
  )
557
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)
558
1651
  }
559
1652
  function apply(ctx) {
560
- ctx.slots.inject('sidebar.footer.action', () => ctx.slots.register({ name: 'sidebar.footer.action', id: 'dingtalk-dsh-observer-entry', order: 5, inject: () => ({}) }, SidebarAction))
561
- 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', () => {
562
1655
  let disposeContent
563
1656
  const syncContent = (visible) => {
564
1657
  if (visible && !disposeContent) {
565
- 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) => {
566
1659
  if (parentSessionId) {
567
1660
  let address = ctx.sessions.subagentAddress(sessionId)
568
- if (!address) { await ctx.sessions.refreshSubagents(parentSessionId); address = ctx.sessions.subagentAddress(sessionId) }
569
- 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 }
570
1663
  }
571
1664
  await ctx.sessions.refresh()
572
- ctx.sessions.open(sessionId)
1665
+ ctx.uiWorkspace.openSession(sessionId)
573
1666
  } }) }, ObserverContent)
574
1667
  } else if (!visible && disposeContent) {
575
1668
  disposeContent()
@@ -584,7 +1677,7 @@ window.__ModuleLoader__.load({
584
1677
  }
585
1678
  })
586
1679
  }
587
- 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 }
588
1681
  return module.exports
589
1682
  }
590
1683
  })