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