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