@zzusp/dingtalk-dsh-observer 0.5.0 → 0.5.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/web-client.js +100 -80
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zzusp/dingtalk-dsh-observer",
3
- "version": "0.5.0",
3
+ "version": "0.5.2",
4
4
  "description": "DingTalk task board client for DeepSeek Harness",
5
5
  "license": "UNLICENSED",
6
6
  "type": "module",
package/web-client.js CHANGED
@@ -3,7 +3,7 @@ window.__ModuleLoader__.load({
3
3
  factory: (require) => {
4
4
  const module = { exports: {} }
5
5
  const React = require('react')
6
- const { Button, Menu, Pill, StateDot } = require('@deepseek-ai/dsh-client-ui-primitives')
6
+ const { Button, IconChecklistOutline14, IconChevronDownOutline14, IconChevronUpOutline14, IconGoalOutline16, Menu, Pill, StateDot } = require('@deepseek-ai/dsh-client-ui-primitives')
7
7
  const { useCallback, useEffect, useState } = React
8
8
  const ENDPOINT = 'http://127.0.0.1:18998'
9
9
  const colors = {
@@ -49,6 +49,8 @@ window.__ModuleLoader__.load({
49
49
  }
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
+ 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) }
52
54
  const short = (value) => value ? String(value).replace(/^session-/, '').slice(0, 14) : '—'
53
55
  const pill = (tone) => ({ display: 'inline-flex', alignItems: 'center', borderRadius: 999, padding: '3px 8px', fontSize: 12, color: tone, background: `color-mix(in srgb, ${tone} 12%, transparent)` })
54
56
  const card = { border: `1px solid ${colors.border}`, borderRadius: 12, background: colors.cardSurface, padding: 16, boxShadow: '0 4px 16px rgba(15,23,42,.07)' }
@@ -67,19 +69,14 @@ window.__ModuleLoader__.load({
67
69
  const tone = statusTone[state] || colors.muted
68
70
  return React.createElement('span', { style: { display: 'inline-flex', alignItems: 'center', gap: 6, minHeight: 24, boxSizing: 'border-box', border: `1px solid color-mix(in srgb, ${tone} 28%, transparent)`, borderRadius: 999, background: `color-mix(in srgb, ${tone} 10%, transparent)`, color: tone, padding: '2px 8px', fontSize: 11, fontWeight: 500, lineHeight: 1.4, whiteSpace: 'nowrap' } }, React.createElement(StateDot, { state, size: 7 }), label)
69
71
  }
70
- const authorizationTag = (label, state) => {
71
- const tone = statusTone[state] || colors.muted
72
- return React.createElement('span', { style: { display: 'inline-flex', alignItems: 'center', justifyContent: 'center', gap: 6, minWidth: 68, minHeight: 26, boxSizing: 'border-box', border: `1px solid color-mix(in srgb, ${tone} 24%, transparent)`, borderRadius: 999, background: `color-mix(in srgb, ${tone} 9%, transparent)`, color: tone, padding: '3px 9px', fontSize: 11, fontWeight: 550, lineHeight: 1.4, whiteSpace: 'nowrap' } }, React.createElement(StateDot, { state, size: 7 }), label)
73
- }
74
72
  function SelectMenu({ label, value, options, onChange, minWidth = 150 }) {
75
73
  const [open, setOpen] = useState(false)
76
74
  const selected = options.find((item) => item.id === value)
77
75
  const anchor = React.createElement(Button, { variant: 'outline', size: 'sm', type: 'button', 'aria-label': label, 'aria-expanded': open, onClick: () => setOpen((current) => !current), style: { minWidth, justifyContent: 'space-between', gap: 16, fontWeight: 400 } }, React.createElement('span', { style: { minWidth: 0, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' } }, selected?.label || value), React.createElement('span', { 'aria-hidden': true, style: { color: colors.muted, fontSize: 9, transform: open ? 'rotate(180deg)' : 'none', transition: 'transform .15s ease' } }, '▼'))
78
76
  return React.createElement(Menu, { open, anchor, items: options, selectedId: value, onSelect: (id) => { onChange(id); setOpen(false) }, onClose: () => setOpen(false), align: 'end', portal: true, dense: true, compact: true })
79
77
  }
80
- const pageHeader = (title, description, count) => React.createElement('div', { style: { minHeight: 44, display: 'flex', alignItems: 'flex-start', justifyContent: 'space-between', gap: 20, marginBottom: 16 } },
81
- React.createElement('div', { style: { minWidth: 0 } }, React.createElement('h1', { style: { margin: 0, fontSize: 18, lineHeight: 1.45, fontWeight: 600 } }, title), React.createElement('p', { style: { margin: '4px 0 0', color: colors.muted, fontSize: 12, lineHeight: 1.5 } }, description)),
82
- count === undefined ? null : React.createElement(Pill, null, String(count)))
78
+ const pageHeader = (title, description) => React.createElement('div', { style: { minHeight: 44, marginBottom: 16 } },
79
+ React.createElement('h1', { style: { margin: 0, fontSize: 18, lineHeight: 1.45, fontWeight: 600 } }, title), React.createElement('p', { style: { margin: '4px 0 0', color: colors.muted, fontSize: 12, lineHeight: 1.5 } }, description))
83
80
  const sectionHeader = (title, meta) => React.createElement('div', { style: { display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 16, marginBottom: 12 } }, React.createElement('h2', { style: { margin: 0, fontSize: 15, lineHeight: 1.45, fontWeight: 600 } }, title), meta ? React.createElement('span', { style: { color: colors.muted, fontSize: 11 } }, meta) : null)
84
81
  const emptyState = (text) => React.createElement('div', { style: { minHeight: 112, display: 'grid', placeItems: 'center', color: colors.muted, fontSize: 12, border: `1px dashed ${colors.border}`, borderRadius: 10, background: colors.surface2 } }, text)
85
82
  async function get(path) {
@@ -95,10 +92,10 @@ window.__ModuleLoader__.load({
95
92
  return value
96
93
  }
97
94
  async function load() {
98
- const [health, groups, tasks, activities, alerts, authorizations] = await Promise.all([
99
- get('/health'), get('/state/groups'), get('/state/tasks'), get('/state/activities'), get('/state/supervisor/alerts'), get('/state/authorizations')
95
+ const [health, groups, tasks, alerts, authorizations] = await Promise.all([
96
+ get('/health'), get('/state/groups'), get('/state/tasks'), get('/state/supervisor/alerts'), get('/state/authorizations')
100
97
  ])
101
- return { health, groups, tasks, activities, alerts, authorizations }
98
+ return { health, groups, tasks, alerts, authorizations }
102
99
  }
103
100
  function SidebarAction({ wide }) {
104
101
  const isOpen = useOpen()
@@ -119,13 +116,13 @@ window.__ModuleLoader__.load({
119
116
  { 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))' }
120
117
  ]
121
118
  const pages = [{ id: 'groups', label: '群聊会话' }, { id: 'tasks', label: '任务看板' }, { id: 'authorizations', label: '授权审批' }, { id: 'archive', label: '归档任务' }, { id: 'alerts', label: '告警' }]
122
- function ObserverOverlay({ openSession }) {
123
- const isOpen = useOpen()
119
+ function ObserverContent({ openSession }) {
124
120
  const [activePage, setActivePage] = useState('groups')
125
121
  const [data, setData] = useState()
126
122
  const [error, setError] = useState()
127
123
  const [navigationError, setNavigationError] = useState()
128
124
  const [updatedAt, setUpdatedAt] = useState()
125
+ const [refreshState, setRefreshState] = useState('idle')
129
126
  const [selectedGroupId, setSelectedGroupId] = useState('')
130
127
  const [messagePage, setMessagePage] = useState(1)
131
128
  const [outboxPage, setOutboxPage] = useState(1)
@@ -133,6 +130,7 @@ window.__ModuleLoader__.load({
133
130
  const [messageDeliveryFilter, setMessageDeliveryFilter] = useState('all')
134
131
  const [outboxStatusFilter, setOutboxStatusFilter] = useState('all')
135
132
  const [hoveredTaskId, setHoveredTaskId] = useState('')
133
+ const [expandedCheckpointTaskId, setExpandedCheckpointTaskId] = useState('')
136
134
  const [copiedId, setCopiedId] = useState('')
137
135
  const [navigatingSessionId, setNavigatingSessionId] = useState('')
138
136
  const [alertType, setAlertType] = useState('all')
@@ -143,36 +141,22 @@ window.__ModuleLoader__.load({
143
141
  const [authorizationPage, setAuthorizationPage] = useState(1)
144
142
  const [authorizationFilter, setAuthorizationFilter] = useState('all')
145
143
  const [selectedAuthorizationId, setSelectedAuthorizationId] = useState('')
146
- const [sidebarWidth, setSidebarWidth] = useState(280)
147
144
  const refresh = useCallback(async () => {
148
145
  try { setData(await load()); setUpdatedAt(new Date()); setError(undefined) }
149
146
  catch (cause) { setError(cause instanceof Error ? cause.message : String(cause)) }
150
147
  }, [])
148
+ const manualRefresh = useCallback(async () => {
149
+ if (refreshState === 'refreshing') return
150
+ setRefreshState('refreshing')
151
+ await refresh()
152
+ setRefreshState('done')
153
+ window.setTimeout(() => setRefreshState('idle'), 1200)
154
+ }, [refresh, refreshState])
151
155
  useEffect(() => {
152
- if (!isOpen) return undefined
153
156
  refresh()
154
157
  const timer = window.setInterval(refresh, 5000)
155
158
  return () => window.clearInterval(timer)
156
- }, [isOpen, refresh])
157
- useEffect(() => {
158
- if (!isOpen) return undefined
159
- const entry = document.querySelector('[aria-label="钉钉群聊运行看板"]')
160
- let sidebar = entry?.parentElement
161
- while (sidebar?.parentElement) {
162
- const rect = sidebar.getBoundingClientRect()
163
- if (rect.left === 0 && rect.width >= 40 && rect.width <= 400 && rect.height >= window.innerHeight * .8) break
164
- sidebar = sidebar.parentElement
165
- }
166
- const update = () => {
167
- const width = sidebar?.getBoundingClientRect().width
168
- if (Number.isFinite(width) && width >= 40 && width <= 400) setSidebarWidth(Math.round(width))
169
- }
170
- update()
171
- const observer = sidebar && typeof ResizeObserver === 'function' ? new ResizeObserver(update) : undefined
172
- if (sidebar && observer) observer.observe(sidebar)
173
- window.addEventListener('resize', update)
174
- return () => { observer?.disconnect(); window.removeEventListener('resize', update) }
175
- }, [isOpen])
159
+ }, [refresh])
176
160
  useEffect(() => {
177
161
  const closeForSession = (event) => {
178
162
  if (event.target instanceof Element && event.target.closest('[role="treeitem"]')) setOpen(false)
@@ -180,8 +164,6 @@ window.__ModuleLoader__.load({
180
164
  document.addEventListener('click', closeForSession, true)
181
165
  return () => document.removeEventListener('click', closeForSession, true)
182
166
  }, [])
183
- if (!isOpen) return null
184
- const activities = new Map((data?.activities || []).map((item) => [item.taskId, item]))
185
167
  const groupsById = new Map((data?.groups || []).map((group) => [group.groupId, group]))
186
168
  const navigate = async (sessionId, parentSessionId) => {
187
169
  try { setNavigationError(undefined); setNavigatingSessionId(sessionId); await openSession(sessionId, parentSessionId); setOpen(false) }
@@ -211,7 +193,7 @@ window.__ModuleLoader__.load({
211
193
  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}`)
212
194
  const selectedGroup = groupsById.get(selectedGroupId) || (data?.groups || [])[0]
213
195
  const selectedGroupSummary = selectedGroup ? React.createElement('div', { style: { minHeight: 64, boxSizing: 'border-box', display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 20, padding: '10px 12px', borderBottom: `1px solid ${colors.border}`, background: colors.surface2 } },
214
- React.createElement('div', { style: { minWidth: 0, display: 'grid', gap: 5 } }, React.createElement('div', { style: { display: 'flex', alignItems: 'baseline', gap: 10 } }, React.createElement('button', { type: 'button', onClick: () => navigate(selectedGroup.residentSessionId), style: { minWidth: 0, border: 0, padding: 0, background: 'transparent', color: 'inherit', cursor: 'pointer', textAlign: 'left', fontFamily: 'inherit', fontSize: 13, fontWeight: 600 } }, selectedGroup.name || '未命名群'), React.createElement('span', { style: { color: colors.muted, fontSize: 10.5 } }, `${selectedGroup.messages?.length || 0} 条消息`), React.createElement('span', { style: { color: colors.muted, fontSize: 10.5 } }, `更新于 ${fmt(selectedGroup.messages?.at?.(-1)?.occurredAt || selectedGroup.updatedAt)}`)), React.createElement('div', { style: { minWidth: 0, display: 'flex', alignItems: 'center', gap: 8, color: colors.muted, fontSize: 10.5 } }, React.createElement('span', null, '常驻 Session'), React.createElement('code', { title: selectedGroup.residentSessionId || '', style: { minWidth: 0, overflow: 'hidden', textOverflow: 'ellipsis', color: 'inherit', fontSize: 10.5 } }, selectedGroup.residentSessionId || '尚未建立'))),
196
+ React.createElement('div', { style: { minWidth: 0, display: 'grid', gap: 5 } }, React.createElement('button', { type: 'button', onClick: () => navigate(selectedGroup.residentSessionId), style: { minWidth: 0, border: 0, padding: 0, background: 'transparent', color: 'inherit', cursor: 'pointer', textAlign: 'left', fontFamily: 'inherit', fontSize: 13, fontWeight: 600 } }, selectedGroup.name || '未命名群'), React.createElement('div', { style: { minWidth: 0, display: 'flex', alignItems: 'center', gap: 8, color: colors.muted, fontSize: 10.5 } }, React.createElement('span', null, '常驻 Session'), React.createElement('code', { title: selectedGroup.residentSessionId || '', style: { minWidth: 0, overflow: 'hidden', textOverflow: 'ellipsis', color: 'inherit', fontSize: 10.5 } }, selectedGroup.residentSessionId || '尚未建立'))),
215
197
  copyButton(selectedGroup.residentSessionId, '会话 ID')) : emptyState('暂无常驻群')
216
198
  const selectedMessages = [...(selectedGroup?.messages || [])].sort((left, right) => Number(right.sequence || 0) - Number(left.sequence || 0))
217
199
  const filteredMessages = selectedMessages.filter((message) => messageDeliveryFilter === 'all' || (message.agentDeliveryStatus || 'unknown') === messageDeliveryFilter)
@@ -235,17 +217,19 @@ window.__ModuleLoader__.load({
235
217
  React.createElement('td', { style: { ...tableBodyCell, width: 150 } }, React.createElement('code', { title: message.messageId, style: { fontSize: 10, color: colors.muted } }, `#${message.sequence ?? '—'} · ${short(message.messageId)}`)))
236
218
  })
237
219
  const selectedOutbox = [...(selectedGroup?.outbox || [])].reverse()
238
- const filteredOutbox = selectedOutbox.filter((message) => outboxStatusFilter === 'all' || (outboxStatusFilter === 'waiting' ? message.status === 'pending' && message.readbackRequired === true : !(message.status === 'pending' && message.readbackRequired === true)))
220
+ const outboxState = (message) => message.recallStatus === 'recalled' ? 'recalled' : message.status === 'pending' && message.readbackRequired === true ? 'waiting' : 'confirmed'
221
+ const filteredOutbox = selectedOutbox.filter((message) => outboxStatusFilter === 'all' || outboxState(message) === outboxStatusFilter)
239
222
  const outboxPageSize = 10
240
223
  const outboxPageCount = Math.max(1, Math.ceil(filteredOutbox.length / outboxPageSize))
241
224
  const currentOutboxPage = Math.min(outboxPage, outboxPageCount)
242
225
  const visibleOutbox = filteredOutbox.slice((currentOutboxPage - 1) * outboxPageSize, currentOutboxPage * outboxPageSize)
243
226
  const outboundStatus = {
244
- sent: { label: '已回读确认', state: 'done' },
245
- pending: { label: '已回读确认', state: 'done' },
227
+ sent: { label: '已回读', state: 'done' },
228
+ pending: { label: '已回读', state: 'done' },
229
+ recalled: { label: '已撤回', state: 'neutral' },
246
230
  }
247
231
  const outboxRows = visibleOutbox.map((message) => {
248
- const status = message.status === 'pending' && message.readbackRequired === true ? { label: '待回读确认', state: 'warning' } : outboundStatus[message.status] || outboundStatus.sent
232
+ const status = message.recallStatus === 'recalled' ? outboundStatus.recalled : message.status === 'pending' && message.readbackRequired === true ? { label: '待回读', state: 'warning' } : outboundStatus[message.status] || outboundStatus.sent
249
233
  return React.createElement('tr', { key: message.outboundId },
250
234
  React.createElement('td', { style: { ...tableBodyCell, width: 130 } }, statusTag(status.label, status.state)),
251
235
  React.createElement('td', { title: message.text, style: { ...tableBodyCell, overflowWrap: 'anywhere' } }, message.text || '(空消息)'),
@@ -257,19 +241,37 @@ window.__ModuleLoader__.load({
257
241
  })
258
242
  const renderTaskCard = (task) => {
259
243
  const group = groupsById.get(task.groupId)
260
- const activity = activities.get(task.taskId)
261
244
  const queued = task.state === 'queued'
262
- const summary = task.waitingReason || task.completion || activity?.type || activity?.eventType
245
+ const checkpointEvents = task.checkpoints || []
246
+ const planCheckpointIndex = checkpointEvents.findLastIndex((checkpoint) => checkpoint.kind === 'plan-confirmed')
247
+ const planCheckpoint = checkpointEvents[planCheckpointIndex]
248
+ const currentCheckpointEvents = checkpointEvents.slice(Math.max(0, planCheckpointIndex))
249
+ const checkpoints = planCheckpoint?.remainingItems?.length ? planCheckpoint.remainingItems : (task.stageTasks?.length ? task.stageTasks : [task.objective])
250
+ const displayObjective = String(task.objective || '').startsWith('[TASK_SOURCE_EVIDENCE]') ? (task.title || task.objective) : task.objective
251
+ const latestCheckpoint = checkpointEvents.at(-1)
252
+ const remainingCheckpointNames = new Set(latestCheckpoint?.remainingItems || checkpoints)
253
+ const completedCheckpointNames = new Set(task.state === 'completed' ? checkpoints : checkpoints.filter((checkpoint) => !remainingCheckpointNames.has(checkpoint)))
254
+ const currentCheckpoint = task.state === 'running' ? checkpoints.find((checkpoint) => remainingCheckpointNames.has(checkpoint)) : undefined
255
+ const completedCheckpointCount = checkpoints.filter((checkpoint) => completedCheckpointNames.has(checkpoint)).length
256
+ const progress = Math.round((completedCheckpointCount / checkpoints.length) * 100)
257
+ const showCheckpoints = task.state !== 'completed' && !task.archivedAt
258
+ const checkpointsExpanded = expandedCheckpointTaskId === task.taskId
259
+ const visibleCheckpoints = checkpointsExpanded ? checkpoints : checkpoints.slice(0, 4)
263
260
  const hovered = hoveredTaskId === task.taskId
264
261
  const navigating = navigatingSessionId === task.childSessionId
265
262
  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' } },
266
- React.createElement('button', { type: 'button', disabled: queued || navigating, onClick: () => 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' } },
263
+ 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' } },
267
264
  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),
268
265
  React.createElement('div', { style: { display: 'flex', alignItems: 'center', minWidth: 0 } }, React.createElement('span', { title: group?.name || task.groupId, style: { ...pill(colors.muted), maxWidth: '100%', padding: '2px 7px', fontSize: 10.5, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' } }, group?.name || task.groupId)),
269
- summary ? React.createElement('div', { title: String(summary), style: { minWidth: 0, fontSize: 11, lineHeight: 1.55, color: task.waitingReason ? colors.warning : colors.muted, display: '-webkit-box', WebkitLineClamp: 4, WebkitBoxOrient: 'vertical', overflow: 'hidden', overflowWrap: 'anywhere', wordBreak: 'break-word' } }, summary) : null,
266
+ React.createElement('div', { style: { display: 'grid', gap: 3 } }, React.createElement('span', { style: { display: 'inline-flex', alignItems: 'center', gap: 4, fontSize: 10.5, color: colors.muted } }, React.createElement(IconGoalOutline16, { size: 12, 'aria-label': '任务目标' }), '任务目标'), React.createElement('div', { title: displayObjective, style: { fontSize: 11, lineHeight: 1.5, display: '-webkit-box', WebkitLineClamp: 3, WebkitBoxOrient: 'vertical', overflow: 'hidden', overflowWrap: 'anywhere' } }, displayObjective)),
267
+ React.createElement('div', { style: { display: 'grid', gap: 6, paddingTop: showCheckpoints ? 3 : 0 } },
268
+ showCheckpoints ? React.createElement(React.Fragment, null,
269
+ React.createElement('div', { style: { display: 'flex', alignItems: 'center', gap: 8, fontSize: 10.5 } }, React.createElement('span', { style: { flex: '0 0 auto', display: 'inline-flex', alignItems: 'center', gap: 4, color: colors.muted } }, React.createElement(IconChecklistOutline14, { size: 12, 'aria-label': '检查点' }), '检查点'), React.createElement('div', { role: 'progressbar', 'aria-label': '检查点进度', 'aria-valuemin': 0, 'aria-valuemax': 100, 'aria-valuenow': progress, style: { flex: '1 1 auto', minWidth: 24, height: 4, borderRadius: 999, overflow: 'hidden', background: `color-mix(in srgb, ${colors.muted} 22%, transparent)` } }, React.createElement('div', { style: { width: `${progress}%`, height: '100%', borderRadius: 'inherit', background: completedCheckpointCount === checkpoints.length ? statusTone.done : colors.accent, transition: 'width 180ms ease' } })), React.createElement('span', { style: { flex: '0 0 auto', color: completedCheckpointCount === checkpoints.length ? statusTone.done : colors.muted } }, `${completedCheckpointCount} / ${checkpoints.length}`)),
270
+ React.createElement('div', { style: { display: 'grid', gap: 4 } }, ...visibleCheckpoints.map((checkpoint, index) => { const completed = completedCheckpointNames.has(checkpoint); const current = !completed && currentCheckpoint === checkpoint; const tone = completed ? statusTone.done : current ? colors.accent : colors.muted; const duration = checkpointDuration(checkpoint, currentCheckpointEvents, Date.now()); return React.createElement('div', { key: `${index}:${checkpoint}`, title: checkpoint, style: { minWidth: 0, display: 'grid', gridTemplateColumns: '12px minmax(0,1fr) 64px', gap: 5, alignItems: 'start', fontSize: 10.5, lineHeight: 1.4 } }, React.createElement('span', { 'aria-hidden': true, style: { width: 12, height: 14, display: 'inline-flex', alignItems: 'center', justifyContent: 'center', color: tone } }, completed ? '✓' : current ? React.createElement(StateDot, { state: 'ongoing', size: 10 }) : '○'), React.createElement('span', { style: { color: 'inherit', display: '-webkit-box', WebkitLineClamp: checkpointsExpanded ? 'unset' : 2, WebkitBoxOrient: 'vertical', overflow: 'hidden', overflowWrap: 'anywhere' } }, checkpoint), React.createElement('span', { title: `执行时长 ${duration}`, style: { width: 64, color: colors.muted, textAlign: 'right', whiteSpace: 'nowrap', fontVariantNumeric: 'tabular-nums' } }, duration)) }), checkpoints.length > 4 ? React.createElement('span', { role: 'button', tabIndex: 0, 'aria-label': checkpointsExpanded ? '收起检查点' : `展开全部 ${checkpoints.length} 个检查点`, title: checkpointsExpanded ? '收起' : `查看全部 ${checkpoints.length} 项`, 'data-task-card-action': 'toggle-checkpoints', onClick: (event) => { event.stopPropagation(); setExpandedCheckpointTaskId(checkpointsExpanded ? '' : task.taskId) }, onKeyDown: (event) => { if (event.key !== 'Enter' && event.key !== ' ') return; event.preventDefault(); event.stopPropagation(); setExpandedCheckpointTaskId(checkpointsExpanded ? '' : task.taskId) }, style: { justifySelf: 'center', width: 28, height: 18, display: 'inline-flex', alignItems: 'center', justifyContent: 'center', color: colors.muted, cursor: 'pointer' } }, React.createElement(checkpointsExpanded ? IconChevronUpOutline14 : IconChevronDownOutline14, { size: 14 })) : null)) : null,
271
+ task.waitingReason ? React.createElement('div', { title: task.waitingReason, style: { fontSize: 10.5, lineHeight: 1.45, color: colors.warning } }, `等待:${task.waitingReason}`) : null),
270
272
  React.createElement('div', { style: { minWidth: 0, marginTop: 3, paddingTop: 9, borderTop: `1px solid ${colors.border}`, display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 8 } },
271
- React.createElement('span', { title: task.updatedAt, style: { minWidth: 0, fontSize: 10.5, color: colors.muted, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' } }, `最后活动 ${fmt(task.updatedAt)}`),
272
- React.createElement('div', { style: { flex: '0 0 auto', display: 'flex', alignItems: 'center', gap: 5 } }, copyButton(task.taskId, ' Task'), task.state === 'completed' && !task.archivedAt ? React.createElement('button', { type: 'button', onClick: async (event) => { event.stopPropagation(); try { await post(`/tasks/${encodeURIComponent(task.taskId)}/archive`); await refresh() } catch (cause) { setNavigationError(cause instanceof Error ? cause.message : String(cause)) } }, style: { flex: '0 0 auto', border: `1px solid ${colors.border}`, borderRadius: 7, background: colors.surface2, color: colors.muted, padding: '3px 7px', cursor: 'pointer', fontFamily: 'inherit', fontSize: 10.5 } }, '归档') : null)),
273
+ React.createElement('span', { title: `最后活动 ${fmt(task.updatedAt)}`, style: { minWidth: 0, display: 'inline-flex', alignItems: 'center', gap: 4, fontSize: 10.5, color: colors.muted, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' } }, React.createElement('svg', { width: 12, height: 12, viewBox: '0 0 24 24', fill: 'none', stroke: 'currentColor', strokeWidth: 1.8, strokeLinecap: 'round', strokeLinejoin: 'round', 'aria-label': '最后活动时间' }, React.createElement('circle', { cx: 12, cy: 12, r: 9 }), React.createElement('path', { d: 'M12 7v5l3 2' })), fmt(task.updatedAt)),
274
+ React.createElement('div', { style: { flex: '0 0 auto', display: 'flex', alignItems: 'center', gap: 5 } }, copyButton(task.taskId, ' Id'), task.state === 'completed' && !task.archivedAt ? React.createElement('button', { type: 'button', onClick: async (event) => { event.stopPropagation(); try { await post(`/tasks/${encodeURIComponent(task.taskId)}/archive`); await refresh() } catch (cause) { setNavigationError(cause instanceof Error ? cause.message : String(cause)) } }, style: { flex: '0 0 auto', border: `1px solid ${colors.border}`, borderRadius: 7, background: colors.surface2, color: colors.muted, padding: '3px 7px', cursor: 'pointer', fontFamily: 'inherit', fontSize: 10.5 } }, '归档') : null)),
273
275
  navigating ? React.createElement('div', { role: 'status', style: { fontSize: 11.5, color: colors.accent } }, '正在打开会话…') : queued ? React.createElement('div', { style: { fontSize: 11.5, color: colors.muted } }, '等待执行,尚无对话和轨迹') : null),
274
276
  null)
275
277
  }
@@ -311,28 +313,30 @@ window.__ModuleLoader__.load({
311
313
  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)),
312
314
  React.createElement('div', { style: { flex: 1, minHeight: 0, padding: '8px 12px 16px 8px', display: 'flex', flexDirection: 'column', gap: 10, overflowY: 'scroll', scrollbarGutter: 'stable' } }, ...taskCards))
313
315
  })
314
- const menu = React.createElement('nav', { 'aria-label': '运行看板视图', style: { minWidth: 0, height: 38, display: 'flex', alignItems: 'stretch', gap: 28, overflowX: 'auto', padding: '0 24px' } },
316
+ const menu = React.createElement('nav', { 'aria-label': '运行看板视图', style: { minWidth: 0, height: 31, display: 'flex', alignItems: 'stretch', gap: 28, overflowX: 'auto', padding: '0 24px' } },
315
317
  ...pages.map((page) => React.createElement('button', { key: page.id, type: 'button', onClick: () => setActivePage(page.id), 'aria-current': activePage === page.id ? 'page' : undefined, style: { position: 'relative', border: 0, borderBottom: activePage === page.id ? '2px solid #4d6bfe' : '2px solid transparent', borderRadius: 0, padding: '0 0 8px', cursor: 'pointer', font: 'inherit', fontSize: 13, fontWeight: 400, color: activePage === page.id ? '#4d6bfe' : colors.muted, background: 'transparent', outline: 'none', whiteSpace: 'nowrap' } }, page.label)))
316
- const header = React.createElement('header', { style: { position: 'sticky', top: 0, zIndex: 2, borderBottom: `1px solid ${colors.border}`, background: colors.surface } },
317
- React.createElement('div', { style: { height: 52, boxSizing: 'border-box', display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 24, padding: '0 24px' } },
318
- React.createElement('div', { style: { minWidth: 0, display: 'flex', alignItems: 'center', gap: 14 } },
318
+ const refreshIcon = React.createElement('svg', { width: 14, height: 14, viewBox: '0 0 24 24', fill: 'none', stroke: 'currentColor', strokeWidth: 1.8, strokeLinecap: 'round', strokeLinejoin: 'round', 'aria-hidden': true, style: refreshState === 'refreshing' ? { animation: 'spin 800ms linear infinite' } : undefined }, React.createElement('path', { d: 'M20 11a8 8 0 1 0-2.34 5.66' }), React.createElement('path', { d: 'M20 4v7h-7' }))
319
+ const header = React.createElement('header', { style: { position: 'sticky', top: 0, zIndex: 2, borderBottom: `1px solid ${colors.border}`, background: colors.surface, pointerEvents: 'auto' } },
320
+ React.createElement('div', { style: { height: 44, boxSizing: 'border-box', display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 24, padding: '0 24px' } },
321
+ React.createElement('div', { 'aria-label': updatedAt ? `运行正常,最近更新 ${fmt(updatedAt)}` : '正在连接运行时', style: { minWidth: 0, display: 'flex', alignItems: 'center', gap: 12, color: colors.muted, fontSize: 11.5, whiteSpace: 'nowrap' } },
319
322
  React.createElement('span', { style: { fontSize: 15, fontWeight: 400, whiteSpace: 'nowrap' } }, '运行看板'),
320
- React.createElement('div', { 'aria-label': updatedAt ? `运行正常,最近更新 ${fmt(updatedAt)}` : '正在连接运行时', style: { display: 'inline-flex', alignItems: 'center', gap: 7, minWidth: 0, color: colors.muted, fontSize: 11.5, whiteSpace: 'nowrap' } },
323
+ React.createElement('span', { 'aria-hidden': true, style: { width: 1, height: 12, background: colors.border } }),
324
+ React.createElement('span', { style: { display: 'inline-flex', alignItems: 'center', gap: 7 } },
321
325
  React.createElement(StateDot, { state: updatedAt ? 'done' : 'ongoing', size: 7 }),
322
- React.createElement('span', { style: { color: updatedAt ? statusTone.done : colors.muted } }, updatedAt ? '运行正常' : '正在连接'),
323
- updatedAt ? React.createElement('span', { 'aria-hidden': true, style: { width: 1, height: 12, background: colors.border } }) : null,
324
- updatedAt ? React.createElement('span', { title: fmt(updatedAt) }, `更新于 ${fmtTime(updatedAt)}`) : null)),
325
- React.createElement(Button, { variant: 'outline', size: 'sm', type: 'button', onClick: refresh }, '刷新')),
326
+ React.createElement('span', { style: { color: updatedAt ? statusTone.done : colors.muted } }, updatedAt ? '运行正常' : '正在连接')),
327
+ updatedAt ? React.createElement('span', { 'aria-hidden': true, style: { width: 1, height: 12, background: colors.border } }) : null,
328
+ updatedAt ? React.createElement('span', { title: fmt(updatedAt) }, `更新于 ${fmtTime(updatedAt)}`) : null),
329
+ React.createElement(Button, { variant: 'outline', size: 'sm', type: 'button', disabled: refreshState === 'refreshing', onClick: manualRefresh, 'aria-live': 'polite', style: { gap: 6, minWidth: 78 } }, refreshState === 'refreshing' ? '刷新中' : refreshState === 'done' ? '已刷新' : '刷新', refreshIcon)),
326
330
  menu)
327
331
  const dataViewTabs = React.createElement('div', { role: 'tablist', 'aria-label': '群聊数据视图', style: { display: 'flex', alignSelf: 'stretch', gap: 20 } },
328
- ...[{ id: 'messages', label: `群消息 ${selectedMessages.length}` }, { id: 'outbox', label: `Outbox ${selectedOutbox.length}` }].map((item) => React.createElement('button', { key: item.id, role: 'tab', 'aria-selected': groupTableView === item.id, type: 'button', onClick: () => setGroupTableView(item.id), style: { border: 0, borderBottom: groupTableView === item.id ? `2px solid ${colors.accent}` : '2px solid transparent', background: 'transparent', color: groupTableView === item.id ? colors.accent : colors.muted, padding: '0 2px', fontFamily: 'inherit', fontSize: 12, cursor: 'pointer' } }, item.label)))
332
+ ...[{ 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: { border: 0, borderBottom: groupTableView === item.id ? `2px solid ${colors.accent}` : '2px solid transparent', background: 'transparent', color: groupTableView === item.id ? colors.accent : colors.muted, padding: '0 2px', fontFamily: 'inherit', fontSize: 12, cursor: 'pointer' } }, item.label)))
329
333
  const groupTableToolbar = React.createElement('div', { style: toolbar },
330
334
  dataViewTabs,
331
335
  React.createElement('div', { style: { display: 'flex', alignItems: 'center', justifyContent: 'flex-end', gap: 8, flexWrap: 'wrap' } },
332
336
  (data?.groups || []).length > 1 ? 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) }, minWidth: 220 }) : null,
333
337
  groupTableView === 'messages'
334
338
  ? React.createElement(SelectMenu, { label: '筛选投递状态', value: messageDeliveryFilter, options: [{ id: 'all', label: '全部投递状态' }, { id: 'delivered', label: '已投递' }, { id: 'failed', label: '投递失败' }, { id: 'pending', label: '投递中' }, { id: 'skipped', label: '历史未投递' }, { id: 'unknown', label: '状态未知' }], onChange: (value) => { setMessageDeliveryFilter(value); setMessagePage(1) } })
335
- : React.createElement(SelectMenu, { label: '筛选发件状态', value: outboxStatusFilter, options: [{ id: 'all', label: '全部发件状态' }, { id: 'confirmed', label: '已回读确认' }, { id: 'waiting', label: '待回读确认' }], onChange: (value) => { setOutboxStatusFilter(value); setOutboxPage(1) } })))
339
+ : React.createElement(SelectMenu, { label: '筛选发件状态', value: outboxStatusFilter, options: [{ id: 'all', label: '全部发件状态' }, { id: 'confirmed', label: '已回读' }, { id: 'waiting', label: '待回读' }, { id: 'recalled', label: '已撤回' }], onChange: (value) => { setOutboxStatusFilter(value); setOutboxPage(1) } })))
336
340
  const messagesTable = React.createElement(React.Fragment, null,
337
341
  React.createElement('div', { style: { overflowX: 'auto' } }, React.createElement('table', { style: { width: '100%', minWidth: 820, borderCollapse: 'collapse', tableLayout: 'fixed' } },
338
342
  React.createElement('thead', null, React.createElement('tr', { style: { background: colors.surface2, textAlign: 'left' } }, React.createElement('th', { style: { ...tableHeadCell, width: 150 } }, '发送人 / 时间'), React.createElement('th', { style: tableHeadCell }, '消息内容'), React.createElement('th', { style: { ...tableHeadCell, width: 150 } }, 'Agent 投递'), React.createElement('th', { style: { ...tableHeadCell, width: 150 } }, '消息'))),
@@ -340,13 +344,13 @@ window.__ModuleLoader__.load({
340
344
  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)) }, '下一页')))
341
345
  const outboxTable = React.createElement(React.Fragment, null,
342
346
  React.createElement('div', { style: { overflowX: 'auto' } }, React.createElement('table', { style: { width: '100%', minWidth: 900, borderCollapse: 'collapse', tableLayout: 'fixed' } },
343
- React.createElement('thead', null, React.createElement('tr', { style: { background: colors.surface2, textAlign: 'left' } }, React.createElement('th', { style: { ...tableHeadCell, width: 130 } }, '发送状态'), React.createElement('th', { style: tableHeadCell }, '消息内容'), React.createElement('th', { style: { ...tableHeadCell, width: 180 } }, '来源 / 回复目标'), React.createElement('th', { style: { ...tableHeadCell, width: 170 } }, '投递消息 ID'), React.createElement('th', { style: { ...tableHeadCell, width: 150 } }, 'Outbox ID'))),
347
+ React.createElement('thead', null, React.createElement('tr', { style: { background: colors.surface2, textAlign: 'left' } }, React.createElement('th', { style: { ...tableHeadCell, width: 130 } }, '发送状态'), React.createElement('th', { style: tableHeadCell }, '消息内容'), React.createElement('th', { style: { ...tableHeadCell, width: 180 } }, '来源 / 回复目标'), React.createElement('th', { style: { ...tableHeadCell, width: 170 } }, '投递消息 ID'), React.createElement('th', { style: { ...tableHeadCell, width: 150 } }, '发信箱 ID'))),
344
348
  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 发件记录'))])))),
345
349
  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)) }, '下一页')))
346
350
  const groupsPage = React.createElement(React.Fragment, null,
347
- pageHeader('群聊会话', '查看常驻会话、群消息接收状态与 Agent 发件记录。', (data?.groups || []).length),
348
- React.createElement('section', null, sectionHeader('消息记录', '群消息与 Agent 发件记录'), React.createElement('div', { style: tableFrame }, selectedGroupSummary, groupTableToolbar, groupTableView === 'messages' ? messagesTable : outboxTable)))
349
- const tasksPage = React.createElement(React.Fragment, null, pageHeader('任务看板', '按执行阶段查看任务、叶子会话和目标进展。', (data?.tasks || []).filter((task) => !task.archivedAt).length), React.createElement('div', { style: { display: 'grid', gridTemplateColumns: 'repeat(4, minmax(320px, 1fr))', gap: 16, overflowX: 'auto', alignItems: 'start', paddingBottom: 8 } }, ...bucketColumns))
351
+ pageHeader('群聊会话', '查看常驻会话、收信箱状态与发信箱记录。'),
352
+ React.createElement('section', null, sectionHeader('消息记录', '收信箱与发信箱记录'), React.createElement('div', { style: tableFrame }, selectedGroupSummary, groupTableToolbar, groupTableView === 'messages' ? messagesTable : outboxTable)))
353
+ const tasksPage = React.createElement(React.Fragment, null, pageHeader('任务看板', '按执行阶段查看任务、叶子会话和目标进展。'), React.createElement('div', { style: { display: 'grid', gridTemplateColumns: 'repeat(4, minmax(300px, 1fr))', gap: 16, overflowX: 'auto', alignItems: 'start', paddingBottom: 8 } }, ...bucketColumns))
350
354
  const authorizationStatus = {
351
355
  'pending-send': { label: '待发送', state: 'warning' },
352
356
  'waiting-reply': { label: '待批复', state: 'warning' },
@@ -378,8 +382,8 @@ window.__ModuleLoader__.load({
378
382
  const pending = item.status !== 'answered'
379
383
  const status = pending ? authorizationStatus[item.status] : authorizationDecision[item.decision] || authorizationStatus.answered
380
384
  const group = groupsById.get(item.groupId)
381
- return React.createElement('div', { key: item.requestId, style: { width: '100%', minWidth: 0, boxSizing: 'border-box', display: 'grid', gridTemplateColumns: '88px minmax(260px, 1.1fr) minmax(300px, 1.5fr) 130px 44px', alignItems: 'center', gap: 18, borderTop: `1px solid ${colors.border}`, background: 'transparent', color: 'inherit', padding: '15px 18px' } },
382
- authorizationTag(status.label, status.state),
385
+ return React.createElement('div', { key: item.requestId, style: { width: '100%', minWidth: 0, boxSizing: 'border-box', display: 'grid', gridTemplateColumns: 'max-content minmax(260px, 1.1fr) minmax(300px, 1.5fr) 130px 44px', alignItems: 'center', gap: 18, borderTop: `1px solid ${colors.border}`, background: 'transparent', color: 'inherit', padding: '15px 18px' } },
386
+ React.createElement('div', { style: { justifySelf: 'start' } }, statusTag(status.label, status.state)),
383
387
  React.createElement('span', { style: { minWidth: 0 } }, React.createElement('strong', { title: item.objective, style: { display: '-webkit-box', WebkitLineClamp: 2, WebkitBoxOrient: 'vertical', overflow: 'hidden', fontSize: 12.5, lineHeight: 1.5 } }, item.objective), React.createElement('span', { style: { display: 'block', marginTop: 5, color: colors.muted, fontSize: 10.5, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' } }, group?.name || item.groupId)),
384
388
  React.createElement('span', { style: { minWidth: 0 } }, React.createElement('span', { title: item.requestedAction, style: { display: '-webkit-box', WebkitLineClamp: 2, WebkitBoxOrient: 'vertical', overflow: 'hidden', fontSize: 11.5, lineHeight: 1.55, color: colors.muted } }, item.requestedAction), item.risk && item.risk !== '未单独说明' ? React.createElement('span', { style: { display: 'block', marginTop: 6, color: colors.danger, fontSize: 10.5, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' } }, `风险 · ${item.risk}`) : null),
385
389
  React.createElement('span', { style: { color: colors.muted, fontSize: 10.5, lineHeight: 1.5 } }, fmt(item.createdAt)),
@@ -388,7 +392,7 @@ window.__ModuleLoader__.load({
388
392
  const authorizationDetail = selectedAuthorization ? React.createElement('div', { role: 'presentation', onMouseDown: (event) => { if (event.target === event.currentTarget) setSelectedAuthorizationId('') }, style: { position: 'fixed', inset: 0, zIndex: 1100, display: 'flex', justifyContent: 'flex-end', background: 'rgba(0,0,0,.28)', backdropFilter: 'blur(2px)' } },
389
393
  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)' } },
390
394
  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 } },
391
- React.createElement('div', { style: { minWidth: 0 } }, React.createElement('div', { style: { display: 'flex', alignItems: 'center', gap: 10, marginBottom: 10 } }, authorizationTag(selectedAuthorization.status === 'answered' ? (authorizationDecision[selectedAuthorization.decision]?.label || '已处理') : (authorizationStatus[selectedAuthorization.status]?.label || '待批复'), selectedAuthorization.status === 'answered' ? (authorizationDecision[selectedAuthorization.decision]?.state || 'done') : 'warning'), 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)),
395
+ React.createElement('div', { style: { minWidth: 0 } }, React.createElement('div', { style: { display: 'flex', alignItems: 'center', gap: 10, marginBottom: 10 } }, statusTag(selectedAuthorization.status === 'answered' ? (authorizationDecision[selectedAuthorization.decision]?.label || '已处理') : (authorizationStatus[selectedAuthorization.status]?.label || '待批复'), selectedAuthorization.status === 'answered' ? (authorizationDecision[selectedAuthorization.decision]?.state || 'done') : 'warning'), 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)),
392
396
  React.createElement(Button, { variant: 'ghost', size: 'sm', type: 'button', 'aria-label': '关闭申请单', onClick: () => setSelectedAuthorizationId(''), style: { flex: '0 0 auto', minWidth: 52, whiteSpace: 'nowrap' } }, '关闭')),
393
397
  React.createElement('div', { style: { flex: '1 1 auto', overflowY: 'auto', padding: '0 24px 24px' } },
394
398
  React.createElement('section', { style: { padding: '24px 0', borderBottom: `1px solid ${colors.border}` } }, authorizationDetailField('申请范围', selectedAuthorization.requestedAction)),
@@ -402,7 +406,7 @@ window.__ModuleLoader__.load({
402
406
  React.createElement('input', { 'aria-label': `审批意见 ${selectedAuthorization.requestId}`, value: authorizationComments[selectedAuthorization.requestId] || '', onChange: (event) => setAuthorizationComments((current) => ({ ...current, [selectedAuthorization.requestId]: event.target.value })), placeholder: '审批意见(可选)', style: { minWidth: 0, border: `1px solid ${colors.border}`, borderRadius: 9, background: colors.surface2, color: 'inherit', padding: '9px 11px', fontFamily: 'inherit', fontSize: 12 } }),
403
407
  React.createElement(Button, { variant: 'outline', size: 'sm', type: 'button', disabled: decidingAuthorizationId === selectedAuthorization.requestId, onClick: () => decideAuthorization(selectedAuthorization.requestId, 'rejected'), style: { color: colors.danger } }, '拒绝'),
404
408
  React.createElement(Button, { variant: 'primary', size: 'sm', type: 'button', disabled: decidingAuthorizationId === selectedAuthorization.requestId, onClick: () => decideAuthorization(selectedAuthorization.requestId, 'approved') }, decidingAuthorizationId === selectedAuthorization.requestId ? '处理中…' : '批准')) : null)) : null
405
- const authorizationsPage = React.createElement(React.Fragment, null, pageHeader('授权审批', '处理需要人工确认的操作范围,并保留风险、证据和批复记录。', authorizationItems.filter((item) => item.status !== 'answered').length), React.createElement('section', null,
409
+ const authorizationsPage = React.createElement(React.Fragment, null, pageHeader('授权审批', '处理需要人工确认的操作范围,并保留风险、证据和批复记录。'), React.createElement('section', null,
406
410
  sectionHeader('审批记录', `${authorizationItems.length} 条 · 每页 ${authorizationPageSize} 条`),
407
411
  React.createElement('div', { style: tableFrame },
408
412
  React.createElement('div', { style: toolbar }, React.createElement('span', { style: { fontSize: 12, color: colors.muted } }, `当前显示 ${filteredAuthorizationItems.length} 条`), React.createElement(SelectMenu, { label: '筛选审批状态', value: authorizationFilter, options: [{ id: 'all', label: '全部审批状态' }, { id: 'pending', label: '待批复' }, { id: 'approved', label: '已批准' }, { id: 'rejected', label: '已拒绝' }], onChange: (value) => { setAuthorizationFilter(value); setAuthorizationPage(1) } })),
@@ -415,11 +419,11 @@ window.__ModuleLoader__.load({
415
419
  ), authorizationDetail)
416
420
  const archivedTasks = (data?.tasks || []).filter((task) => task.state === 'completed' && task.archivedAt).sort((left, right) => String(right.archivedAt).localeCompare(String(left.archivedAt)))
417
421
  const archivePage = React.createElement('section', null,
418
- pageHeader('归档任务', '归档只影响看板展示,Task、Session、Goal 与历史证据仍完整保留。', archivedTasks.length),
422
+ pageHeader('归档任务', '归档只影响看板展示,Task、Session、Goal 与历史证据仍完整保留。'),
419
423
  React.createElement('div', { style: { display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(300px, 1fr))', gap: 12 } }, ...(archivedTasks.length ? archivedTasks.map(renderTaskCard) : [React.createElement(React.Fragment, { key: 'empty' }, emptyState('暂无归档任务'))]))
420
424
  )
421
425
  const alertsPage = React.createElement(React.Fragment, null,
422
- pageHeader('告警', '集中查看消息通道、叶子会话、任务目标和 Runtime 的当前异常与恢复历史。', filteredActiveAlerts.length),
426
+ pageHeader('告警', '集中查看消息通道、叶子会话、任务目标和 Runtime 的当前异常与恢复历史。'),
423
427
  React.createElement('section', null, sectionHeader('告警记录', '按状态和类型筛选'), React.createElement('div', { style: tableFrame },
424
428
  React.createElement('div', { style: toolbar },
425
429
  React.createElement('div', { role: 'tablist', 'aria-label': '告警状态视图', style: { display: 'flex', alignSelf: 'stretch', gap: 20 } }, ...[{ id: 'active', label: `当前异常 ${filteredActiveAlerts.length}` }, { id: 'resolved', label: `恢复历史 ${filteredResolvedAlerts.length}` }].map((item) => React.createElement('button', { key: item.id, role: 'tab', 'aria-selected': alertView === item.id, type: 'button', onClick: () => setAlertView(item.id), style: { border: 0, borderBottom: alertView === item.id ? `2px solid ${colors.accent}` : '2px solid transparent', background: 'transparent', color: alertView === item.id ? colors.accent : colors.muted, padding: '0 2px', fontFamily: 'inherit', fontSize: 12, cursor: 'pointer' } }, item.label))),
@@ -428,25 +432,41 @@ window.__ModuleLoader__.load({
428
432
  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))
429
433
  )
430
434
  const pageContent = activePage === 'tasks' ? tasksPage : activePage === 'authorizations' ? authorizationsPage : activePage === 'archive' ? archivePage : activePage === 'alerts' ? alertsPage : groupsPage
431
- const pageViewport = React.createElement('div', { style: { minHeight: 'calc(100dvh - 138px)', boxSizing: 'border-box' } }, pageContent)
432
- const main = React.createElement('main', { style: { maxWidth: activePage === 'tasks' ? 1480 : 1240, minHeight: 'calc(100dvh - 90px)', boxSizing: 'border-box', margin: '0 auto', padding: '24px 24px 32px', display: 'grid', alignContent: 'start', gap: 20 } },
435
+ const pageViewport = React.createElement('div', { style: { width: '100%', maxWidth: 1320, minHeight: 'calc(100dvh - 138px)', boxSizing: 'border-box', margin: '0 auto' } }, pageContent)
436
+ const main = React.createElement('main', { style: { width: '100%', minHeight: 'calc(100dvh - 90px)', boxSizing: 'border-box', padding: '24px 24px 32px', display: 'grid', alignContent: 'start', gap: 20, pointerEvents: 'auto' } },
433
437
  error ? React.createElement('div', { style: { ...card, borderColor: colors.danger, color: colors.danger } }, `无法连接 resident 插件:${error}`) : null,
434
438
  navigationError ? React.createElement('div', { style: { ...card, borderColor: colors.danger, color: colors.danger } }, `无法打开 DSH Session:${navigationError}`) : null,
435
439
  pageViewport
436
440
  )
437
- return React.createElement('div', { style: { position: 'fixed', top: 0, right: 0, bottom: 0, left: sidebarWidth, zIndex: 900, minWidth: 0, background: colors.surface, color: 'inherit', overflow: 'auto' } }, header, main)
441
+ return React.createElement('div', { style: { width: '100%', height: '100%', minWidth: 0, minHeight: 0, background: colors.surface, color: 'inherit', overflow: 'auto' } }, header, main)
438
442
  }
439
443
  function apply(ctx) {
440
444
  ctx.slots.inject('sidebar.footer.action', () => ctx.slots.register({ name: 'sidebar.footer.action', id: 'dingtalk-dsh-observer-entry', order: 5, inject: () => ({}) }, SidebarAction))
441
- ctx.slots.inject('shell.overlay', () => ctx.slots.register({ name: 'shell.overlay', id: 'dingtalk-dsh-observer', order: 20, inject: () => ({ openSession: async (sessionId, parentSessionId) => {
442
- if (parentSessionId) {
443
- let address = ctx.sessions.subagentAddress(sessionId)
444
- if (!address) { await ctx.sessions.refreshSubagents(parentSessionId); address = ctx.sessions.subagentAddress(sessionId) }
445
- if (address) { ctx.sessions.openSubagent(address); return }
445
+ ctx.slots.inject('conversation', () => {
446
+ let disposeContent
447
+ const syncContent = (visible) => {
448
+ if (visible && !disposeContent) {
449
+ disposeContent = ctx.slots.register({ name: 'conversation', id: 'dingtalk-dsh-observer', priority: -10, inject: () => ({ openSession: async (sessionId, parentSessionId) => {
450
+ if (parentSessionId) {
451
+ let address = ctx.sessions.subagentAddress(sessionId)
452
+ if (!address) { await ctx.sessions.refreshSubagents(parentSessionId); address = ctx.sessions.subagentAddress(sessionId) }
453
+ if (address) { ctx.sessions.openSubagent(address); return }
454
+ }
455
+ await ctx.sessions.refresh()
456
+ ctx.sessions.open(sessionId)
457
+ } }) }, ObserverContent)
458
+ } else if (!visible && disposeContent) {
459
+ disposeContent()
460
+ disposeContent = undefined
461
+ }
446
462
  }
447
- await ctx.sessions.refresh()
448
- ctx.sessions.open(sessionId)
449
- } }) }, ObserverOverlay))
463
+ openListeners.add(syncContent)
464
+ syncContent(open)
465
+ return () => {
466
+ openListeners.delete(syncContent)
467
+ disposeContent?.()
468
+ }
469
+ })
450
470
  }
451
471
  module.exports = { name: 'dingtalk-dsh-observer-client', inject: ['slots', 'sessions'], apply }
452
472
  return module.exports