@nelsonlongxiang/dsh-open-a2a-net 0.6.28 → 0.6.29
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/README.md +85 -14
- package/README_zh.md +81 -7
- package/assets/nexusDist/assets/index-BqepxJ7E.css +1 -0
- package/assets/nexusDist/assets/index-CTWH6QRh.js +9 -0
- package/assets/nexusDist/index.html +2 -2
- package/cli/a2a.mjs +624 -624
- package/cordis.patch.yml +21 -21
- package/lib/client.js +64 -64
- package/lib/client.js.map +1 -1
- package/lib/faces/admin.js +744 -0
- package/lib/index.js +139 -737
- package/lib/recursion-guard.js +78 -0
- package/lib/runtime.js +1 -0
- package/lib/skill/SKILL.md +34 -9
- package/lib/skill/references/cli.md +24 -7
- package/lib/skill/references/discovery.md +5 -2
- package/lib/skill/references/pitfalls.md +2 -1
- package/lib/skill/references/routing.md +18 -8
- package/lib/skill/references/setup.md +152 -0
- package/lib/task-ledger.js +6 -0
- package/lib/types/faces/admin.d.ts +132 -0
- package/lib/types/index.d.ts +11 -3
- package/lib/types/recursion-guard.d.ts +56 -0
- package/lib/types/runtime.d.ts +34 -0
- package/lib/types/task-ledger.d.ts +6 -0
- package/nexus-stage/package-lock.json +2262 -2262
- package/nexus-stage/src/canvas-ops.ts +175 -175
- package/nexus-stage/src/canvas-wire.ts +156 -156
- package/nexus-stage/src/edges.ts +72 -72
- package/nexus-stage/src/federation-overlay.ts +103 -103
- package/nexus-stage/src/federation.ts +240 -240
- package/nexus-stage/src/layout-doc.ts +143 -143
- package/nexus-stage/src/layout-wire.ts +158 -158
- package/nexus-stage/src/planning-card-text.ts +73 -0
- package/nexus-stage/src/planning-fit.ts +45 -0
- package/nexus-stage/src/planning-legend.ts +38 -0
- package/nexus-stage/src/planning-overlap.ts +134 -0
- package/nexus-stage/src/planning-relayout.ts +121 -0
- package/nexus-stage/src/planning-search.ts +168 -0
- package/nexus-stage/src/planning-view.ts +63 -88
- package/nexus-stage/src/planning.css +39 -8
- package/nexus-stage/src/seat.ts +35 -35
- package/nexus-stage/src/stage-name.ts +13 -13
- package/nexus-stage/src/tokens.ts +31 -31
- package/nexus-stage/src/viewport.ts +24 -1
- package/nexus-stage/src/wb-chat.ts +20 -6
- package/nexus-stage/src/wb-ledger.ts +20 -7
- package/nexus-stage/src/wb-names.ts +43 -0
- package/nexus-stage/src/workbench.css +8 -2
- package/nexus-stage/src/workbench.ts +55 -17
- package/nexus-stage/src/world.ts +5 -1
- package/package.json +216 -214
- package/assets/nexusDist/assets/index-1niOBYxp.js +0 -5
- package/assets/nexusDist/assets/index-uQf7NwjF.css +0 -1
- package/nexus-stage/scripts/__pycache__/mockhost.cpython-312.pyc +0 -0
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
* @module nexus-stage/wb-chat
|
|
20
20
|
*/
|
|
21
21
|
import type { ArchivedRow, DeadRow, PendingRow, RemoteRow, StateBody } from './workbench'
|
|
22
|
-
import { VERDICTS } from './wb-ledger'
|
|
22
|
+
import { overHour, VERDICTS } from './wb-ledger'
|
|
23
23
|
import { displayName } from './stage-name'
|
|
24
24
|
|
|
25
25
|
/** Chat target: a node picked on the board or in the directory. Session and
|
|
@@ -55,8 +55,6 @@ export interface ChatDrawer {
|
|
|
55
55
|
|
|
56
56
|
/** The composer's hard limit (dispatch route contract). */
|
|
57
57
|
const MAX_CHARS = 4000
|
|
58
|
-
/** A pending receipt older than this is 陈旧 (same definition as the ledger). */
|
|
59
|
-
const STALE_MS = 60 * 60 * 1000
|
|
60
58
|
/** Within this many px of the bottom, a repaint keeps the thread pinned there. */
|
|
61
59
|
const STICK_PX = 32
|
|
62
60
|
|
|
@@ -415,9 +413,14 @@ export function createChatDrawer(deps: ChatDrawerDeps): ChatDrawer {
|
|
|
415
413
|
function pendingStatus(row: PendingRow, now: number): SentRow['status'] {
|
|
416
414
|
const inbound = row.direction === 'inbound'
|
|
417
415
|
if (row.receiptExpected === false) return { text: inbound ? '来信 · 无需回执' : '已送达 · 无回执', tone: 'muted', hint: '免回执消息:不等回执,24h 后自动归档' }
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
416
|
+
// One hour boundary with the 台账 tab (the host's settleable verdict,
|
|
417
|
+
// browser clock only on older hosts). Past it, OUR send is 陈旧 (can be
|
|
418
|
+
// nudged); a peer's delivery is a receipt WE owe — never 陈旧 (#30 P3).
|
|
419
|
+
const late = overHour(row, now)
|
|
420
|
+
const text = inbound ? (late ? '待本机回执 · 超 1 小时' : '待本机回执') : late ? '陈旧' : '待回执'
|
|
421
|
+
// The row's own status only when it says more than 'pending'.
|
|
422
|
+
const extra = row.status !== 'pending' ? ` · ${row.status}` : ''
|
|
423
|
+
return { text: `${text}${extra}`, tone: late ? 'warn' : 'wait', hint: `${inbound ? '对方等待本机回执;' : ''}${inbound ? '收到' : '派发'}于 ${ago(row.startedAt, now)}` }
|
|
421
424
|
}
|
|
422
425
|
|
|
423
426
|
/** A send that left the ledger, by how it ended (gh#31). `endedBy` says
|
|
@@ -427,6 +430,17 @@ export function createChatDrawer(deps: ChatDrawerDeps): ChatDrawer {
|
|
|
427
430
|
* ledger's own markers, which are not shown. */
|
|
428
431
|
function settledStatus(row: ArchivedRow, now: number): SentRow['status'] {
|
|
429
432
|
const when = `结算于 ${ago(row.resolvedAt, now)}`
|
|
433
|
+
// gh#68: a fire-forget send this host moved out of the ledger (移出 on
|
|
434
|
+
// the 已过期(免回执) card, an abandon) is bookkeeping, not a failure — it
|
|
435
|
+
// never waited on a receipt, so 已放弃 would contradict the button that
|
|
436
|
+
// did it. A cancel stays 已撤回: it is a deliberate recall that also
|
|
437
|
+
// tells the peer to stop (API / tool path; the workbench offers none).
|
|
438
|
+
// A receipt that arrives after the move keeps its 回执迟到 flag.
|
|
439
|
+
if (row.receiptExpected === false && row.endedBy === 'abandoned') {
|
|
440
|
+
return row.late === true
|
|
441
|
+
? { text: '已移出(免回执) · 回执迟到', tone: 'warn', hint: `${when};免回执消息本机移出后对方回执才到,结局不改写` }
|
|
442
|
+
: { text: '已移出(免回执)', tone: 'muted', hint: `${when};免回执消息,本机移出台账,不是失败` }
|
|
443
|
+
}
|
|
430
444
|
if (row.endedBy === 'abandoned' || row.endedBy === 'cancelled') {
|
|
431
445
|
const act = row.endedBy === 'cancelled' ? '撤回' : '放弃'
|
|
432
446
|
// The peer answered after we ended it: the verdict stays ours (late
|
|
@@ -25,6 +25,7 @@
|
|
|
25
25
|
*/
|
|
26
26
|
import type { DeadRow, PendingRow, StateBody } from './workbench'
|
|
27
27
|
import type { RailTone } from './wb-rail'
|
|
28
|
+
import { nameInto } from './wb-names'
|
|
28
29
|
|
|
29
30
|
export interface LedgerContext {
|
|
30
31
|
readonly state: StateBody
|
|
@@ -54,7 +55,7 @@ function senderOf(row: { peer?: string }): string | undefined {
|
|
|
54
55
|
* settleable verdict (receiptExpected && age < 1h, computed host-side), else
|
|
55
56
|
* by ours — a workbench opened from another machine with a skewed clock then
|
|
56
57
|
* still draws the boundary where the server does (at most one poll late). */
|
|
57
|
-
function overHour(r: PendingRow, now: number): boolean {
|
|
58
|
+
export function overHour(r: PendingRow, now: number): boolean {
|
|
58
59
|
return r.settleable !== undefined ? r.settleable === false : now - r.startedAt >= HOUR
|
|
59
60
|
}
|
|
60
61
|
|
|
@@ -155,7 +156,10 @@ export function renderLedger(pane: HTMLElement, ctx: LedgerContext): { badge: st
|
|
|
155
156
|
const staleCount = facts.stale.length
|
|
156
157
|
|
|
157
158
|
const tiles = el('div', 'wb-led-tiles')
|
|
158
|
-
|
|
159
|
+
// 待回执 counts only rows a receipt is owed on — the same rows as the
|
|
160
|
+
// 陈旧 / 待本机回执 / 等待回执 groups below; receiptless rows are their own
|
|
161
|
+
// 免回执 group and owe nothing.
|
|
162
|
+
tile(tiles, '待回执', expected.length, `要回执、仍未结算的任务(含陈旧与本机欠对方的入站行)${receiptless.length > 0 ? `;另有免回执 ${receiptless.length} 条不计` : ''}`)
|
|
159
163
|
tile(tiles, '陈旧', staleCount, '我方派发、要回执、已等待 ≥1h 仍未回执(可催办);本机欠对方的入站行在「待本机回执」组另计,免回执行不算陈旧', staleCount > 0 ? 'warn' : undefined)
|
|
160
164
|
tile(tiles, '死信', facts.deadLetters.length, '超过 24h TTL 仍未结算(unconsumed);需清账(abandon)才能移出。免回执到期另列,不算死信', facts.deadLetters.length > 0 ? 'danger' : undefined)
|
|
161
165
|
tile(tiles, '已归档', archived, '已离开台账并留档的任务(结算、拒绝或放弃),用于审计')
|
|
@@ -172,8 +176,13 @@ export function renderLedger(pane: HTMLElement, ctx: LedgerContext): { badge: st
|
|
|
172
176
|
head.appendChild(brief)
|
|
173
177
|
node.appendChild(head)
|
|
174
178
|
const meta = el('div', 'wb-led-meta')
|
|
175
|
-
|
|
176
|
-
|
|
179
|
+
// Names, not bare ids (wb-names; the id rides each span's tooltip). An
|
|
180
|
+
// inbound row's sender is the peer handle; an unknown one reads 对端.
|
|
181
|
+
const who = meta.appendChild(el('span', 'wb-led-team'))
|
|
182
|
+
if (!c.inbound) nameInto(who, state, c.team, '发往 ')
|
|
183
|
+
else if (c.sender !== undefined) nameInto(who, state, c.sender, '来自 ')
|
|
184
|
+
else who.textContent = '来自 对端'
|
|
185
|
+
if (c.inbound) nameInto(meta.appendChild(el('span', 'wb-led-to')), state, c.team, '至 ')
|
|
177
186
|
meta.appendChild(el('span')).textContent = c.atText
|
|
178
187
|
meta.appendChild(el('span', 'wb-led-task')).textContent = c.taskId
|
|
179
188
|
for (const chip of c.chips) meta.appendChild(el('span', 'wb-led-chip')).textContent = chip
|
|
@@ -181,10 +190,13 @@ export function renderLedger(pane: HTMLElement, ctx: LedgerContext): { badge: st
|
|
|
181
190
|
node.title = `${c.taskId}\n${c.team}`
|
|
182
191
|
if (c.dead !== undefined) {
|
|
183
192
|
const row = c.dead
|
|
184
|
-
|
|
193
|
+
// A fire-forget expiry is normal bookkeeping, not a failure: moving it
|
|
194
|
+
// out is an optional tidy, so it gets a quiet control, not 清账.
|
|
195
|
+
const expiry = row.reason === 'fire-forget'
|
|
196
|
+
const btn = el('button', `wb-btn wb-led-abandon${expiry ? ' is-quiet' : ''}`)
|
|
185
197
|
btn.type = 'button'
|
|
186
|
-
btn.textContent = '清账'
|
|
187
|
-
btn.title = '从台账移出这条死信(abandon)'
|
|
198
|
+
btn.textContent = expiry ? '移出' : '清账'
|
|
199
|
+
btn.title = expiry ? '可选:把这条已过期的免回执记录移出台账(不是失败,不处理也无妨)' : '从台账移出这条死信(abandon)'
|
|
188
200
|
btn.addEventListener('click', ev => {
|
|
189
201
|
ev.stopPropagation()
|
|
190
202
|
btn.disabled = true
|
|
@@ -209,6 +221,7 @@ export function renderLedger(pane: HTMLElement, ctx: LedgerContext): { badge: st
|
|
|
209
221
|
const section = (label: string, rows: number, tone: string, hint: string): HTMLElement | null => {
|
|
210
222
|
if (rows === 0) return null
|
|
211
223
|
const g = el('div', `wb-led-group is-${tone}`)
|
|
224
|
+
g.dataset.group = label
|
|
212
225
|
const head = el('div', 'wb-led-group-head')
|
|
213
226
|
head.appendChild(el('span')).textContent = `${label} · ${rows}`
|
|
214
227
|
head.appendChild(el('span', 'wb-led-group-hint')).textContent = hint
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Readable names for routing team ids, shared by the activity and plan
|
|
3
|
+
* panes and the 台账 tab (review 2026-10-27: rows read bare `dsh/1111…` ids;
|
|
4
|
+
* the ledger kept doing so after #72 fixed the other two panes).
|
|
5
|
+
* Names are attacker-shaped data: textContent only.
|
|
6
|
+
* @module nexus-stage/wb-names
|
|
7
|
+
*/
|
|
8
|
+
import type { StateBody } from './workbench'
|
|
9
|
+
import { displayName } from './stage-name'
|
|
10
|
+
|
|
11
|
+
/** The name behind a team id — a session, a remote node or a canvas team —
|
|
12
|
+
* or undefined when none matches. An empty name counts as no match:
|
|
13
|
+
* displayName's `??` keeps '', and a peer can publish one. `remote` marks a
|
|
14
|
+
* name the PEER chose — it can collide with a local session's, so callers
|
|
15
|
+
* must show where it is from. */
|
|
16
|
+
export function teamName(state: StateBody, team: string): { name: string; remote: boolean } | undefined {
|
|
17
|
+
const named = (name: string | undefined, remote: boolean) => name !== undefined && name.trim() !== '' ? { name, remote } : undefined
|
|
18
|
+
const s = (state.sessions ?? []).find(r => r.team === team)
|
|
19
|
+
if (s !== undefined) return named(displayName(s), false)
|
|
20
|
+
const r = (state.remote ?? []).find(x => x.team === team)
|
|
21
|
+
if (r !== undefined) return named(displayName(r), true)
|
|
22
|
+
return named((state.canvas?.teams ?? []).find(t => t.team === team)?.name, false)
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** A name for plain text: a peer-published one says it is remote. */
|
|
26
|
+
export function teamLabel(state: StateBody, team: string): string {
|
|
27
|
+
const n = teamName(state, team)
|
|
28
|
+
return n === undefined ? team : n.remote ? `${n.name}(远端)` : n.name
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** Fill `target` with `prefix` + the name (the id rides the tooltip; an
|
|
32
|
+
* unknown team stays its id); a peer-published name gets a visible 远端 chip. */
|
|
33
|
+
export function nameInto(target: HTMLElement, state: StateBody, team: string, prefix = ''): void {
|
|
34
|
+
const n = teamName(state, team)
|
|
35
|
+
target.textContent = `${prefix}${n?.name ?? team}`
|
|
36
|
+
target.title = team
|
|
37
|
+
if (n?.remote === true) {
|
|
38
|
+
const chip = target.appendChild(document.createElement('span'))
|
|
39
|
+
chip.className = 'wb-row-remote'
|
|
40
|
+
chip.textContent = '远端'
|
|
41
|
+
chip.title = '名字由对端自行发布'
|
|
42
|
+
}
|
|
43
|
+
}
|
|
@@ -31,13 +31,17 @@ body{background:var(--n-bg);color:var(--n-ink);font:13px/1.45 system-ui,"Segoe U
|
|
|
31
31
|
.wb-header{display:flex;align-items:center;gap:12px;padding:8px 14px;border-bottom:1px solid var(--n-border);background:var(--n-panel)}
|
|
32
32
|
.wb-title{display:flex;align-items:center;gap:10px;font-weight:600;font-size:14px;white-space:nowrap}
|
|
33
33
|
.wb-badge{font-size:11px;font-weight:400;color:var(--n-ink-muted);border:1px solid var(--n-border);border-radius:99px;padding:1px 8px}
|
|
34
|
+
.wb-badge[hidden]{display:none}
|
|
35
|
+
.wb-row-remote{margin-left:6px;padding:0 6px;border:1px solid currentColor;border-radius:99px;font-size:10.5px;color:var(--n-ink-muted);vertical-align:1px}
|
|
34
36
|
.wb-header-stats{display:flex;gap:16px;font-size:12px;color:var(--n-ink-muted);flex-wrap:wrap;min-width:0}
|
|
35
37
|
.wb-header-stats .wb-kv{display:flex;gap:6px}
|
|
36
38
|
.wb-header-stats .wb-kv-k{color:var(--n-ink-muted)}
|
|
37
39
|
.wb-header-stats .wb-kv-v{color:var(--n-ink)}
|
|
38
40
|
/* Header verdict: exceptions only (results-first). Quiet when healthy. */
|
|
39
41
|
.wb-header-ok{color:var(--n-ink-muted)}
|
|
40
|
-
.wb-alert{border:1px solid currentColor;border-radius:99px;padding:0 9px;line-height:20px;white-space:nowrap;cursor:
|
|
42
|
+
.wb-alert{border:1px solid currentColor;border-radius:99px;padding:0 9px;line-height:20px;white-space:nowrap;cursor:pointer;background:transparent;font:inherit}
|
|
43
|
+
.wb-alert:hover{background:color-mix(in srgb,currentColor 12%,transparent)}
|
|
44
|
+
.wb-alert:focus-visible{outline:2px solid currentColor;outline-offset:2px}
|
|
41
45
|
.wb-alert-danger{color:var(--n-danger)}
|
|
42
46
|
.wb-alert-warn{color:var(--n-act)}
|
|
43
47
|
.wb-forensics{font-family:ui-monospace,Consolas,monospace;font-size:11px}
|
|
@@ -69,7 +73,7 @@ body{background:var(--n-bg);color:var(--n-ink);font:13px/1.45 system-ui,"Segoe U
|
|
|
69
73
|
.wb-search:focus{outline:1px solid var(--n-accent)}
|
|
70
74
|
.wb-filters{display:flex;flex-wrap:wrap;gap:4px}
|
|
71
75
|
.wb-filter{background:none;border:1px solid var(--n-border);color:var(--n-ink-muted);
|
|
72
|
-
border-radius:99px;padding:
|
|
76
|
+
border-radius:99px;padding:0 10px;min-height:24px;font:inherit;font-size:11.5px;cursor:pointer}
|
|
73
77
|
.wb-filter:hover{border-color:var(--n-accent);color:var(--n-ink)}
|
|
74
78
|
.wb-filter.is-active{border-color:var(--n-accent);color:var(--n-ink);
|
|
75
79
|
background:color-mix(in srgb,var(--n-accent) 16%,transparent)}
|
|
@@ -365,6 +369,8 @@ body{background:var(--n-bg);color:var(--n-ink);font:13px/1.45 system-ui,"Segoe U
|
|
|
365
369
|
.wb-led-meta > span{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;max-width:100%}
|
|
366
370
|
.wb-led-chip{border:1px solid var(--n-border);border-radius:99px;padding:0 6px;font-family:inherit}
|
|
367
371
|
.wb-led-abandon{position:absolute;top:6px;right:6px;margin-left:0;padding:1px 9px;font-size:11.5px}
|
|
372
|
+
.wb-led-abandon.is-quiet{border-color:transparent;background:transparent;color:var(--n-ink-muted)}
|
|
373
|
+
.wb-led-abandon.is-quiet:hover{border-color:var(--n-border);color:var(--n-ink)}
|
|
368
374
|
.wb-led-card:has(.wb-led-abandon) .wb-led-card-head{padding-right:52px}
|
|
369
375
|
.wb-led-empty{padding:24px 8px;text-align:center;font-size:12px;color:var(--n-ink-muted)}
|
|
370
376
|
.wb-led-forensics{font-size:11px;color:var(--n-ink-muted);border-top:1px dashed var(--n-border);padding-top:6px;margin-top:4px}
|
|
@@ -22,6 +22,7 @@ import { createChatDrawer, type ChatTarget } from './wb-chat'
|
|
|
22
22
|
import { createRailTabs } from './wb-rail'
|
|
23
23
|
import { DIRECTORY_FILTERS, learnLogTitle, loadLogTitles, renderDirectory, type DirFilter } from './wb-directory'
|
|
24
24
|
import { ledgerFacts, renderLedger as paintLedger } from './wb-ledger'
|
|
25
|
+
import { nameInto, teamLabel } from './wb-names'
|
|
25
26
|
|
|
26
27
|
/** One pending ledger row from /state `tasks`. */
|
|
27
28
|
export interface PendingRow {
|
|
@@ -72,6 +73,9 @@ export interface ArchivedRow {
|
|
|
72
73
|
readonly endedBy?: 'receipt' | 'echo' | 'abandoned' | 'cancelled'
|
|
73
74
|
/** A receipt arrived after the caller had already ended the row. */
|
|
74
75
|
readonly late?: boolean
|
|
76
|
+
/** gh#68: present (false) only when the row never expected a receipt — a
|
|
77
|
+
* fire-forget send; absent on receipt-expected rows and older hosts. */
|
|
78
|
+
readonly receiptExpected?: false
|
|
75
79
|
readonly direction?: 'inbound' | 'outbound'
|
|
76
80
|
readonly brief?: string
|
|
77
81
|
}
|
|
@@ -643,6 +647,11 @@ export function createWorkbench(deps: WorkbenchDeps): Workbench {
|
|
|
643
647
|
}
|
|
644
648
|
|
|
645
649
|
// ── Activity pane ──
|
|
650
|
+
/** A team cell: the name when known (wb-names), else the id. */
|
|
651
|
+
function teamCell(line: HTMLElement, state: StateBody, team: string): void {
|
|
652
|
+
nameInto(line.appendChild(el('span', 'wb-row-team')), state, team)
|
|
653
|
+
}
|
|
654
|
+
|
|
646
655
|
function renderActivity(): void {
|
|
647
656
|
const pane = clear(activityPane)
|
|
648
657
|
if (last === null) return
|
|
@@ -659,7 +668,7 @@ export function createWorkbench(deps: WorkbenchDeps): Workbench {
|
|
|
659
668
|
for (const row of flights) {
|
|
660
669
|
const line = el('div', 'wb-row')
|
|
661
670
|
line.appendChild(el('span', 'wb-row-dir')).textContent = '→'
|
|
662
|
-
line
|
|
671
|
+
teamCell(line, last, row.team)
|
|
663
672
|
line.appendChild(el('span', 'wb-row-meta')).textContent = `${row.peer} · ${ageText(row.startedAt, now)}`
|
|
664
673
|
pane.appendChild(line)
|
|
665
674
|
}
|
|
@@ -678,7 +687,7 @@ export function createWorkbench(deps: WorkbenchDeps): Workbench {
|
|
|
678
687
|
dir.textContent = row.dir === 'out' ? '→' : '←'
|
|
679
688
|
if (row.ok !== true) dir.classList.add('is-fail')
|
|
680
689
|
line.appendChild(dir)
|
|
681
|
-
line
|
|
690
|
+
teamCell(line, last, row.team)
|
|
682
691
|
line.appendChild(el('span', 'wb-row-meta')).textContent = `${row.peer} · ${ageText(row.ts, now)}`
|
|
683
692
|
line.appendChild(el('span', 'wb-row-badges')).textContent = row.ok === true ? '' : '失败'
|
|
684
693
|
pane.appendChild(line)
|
|
@@ -704,18 +713,24 @@ export function createWorkbench(deps: WorkbenchDeps): Workbench {
|
|
|
704
713
|
const subject = row.brief === undefined || row.brief === '' ? '' : `「${row.brief}」`
|
|
705
714
|
actions.push({
|
|
706
715
|
severity: 'normal',
|
|
707
|
-
text: `催办 ${row.taskId} → ${row.team}(等待 ${ageText(row.startedAt, now)} 无回执)${subject}`,
|
|
716
|
+
text: `催办 ${row.taskId} → ${teamLabel(state, row.team)}(等待 ${ageText(row.startedAt, now)} 无回执)${subject}`,
|
|
708
717
|
brief: `a2a_route 催收回执:task ${row.taskId}(team ${row.team})${subject === '' ? '' : `原任务「${row.brief}」`}已派发 ${ageText(row.startedAt, now)},请回报结果或回执。`,
|
|
709
718
|
})
|
|
710
719
|
}
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
720
|
+
// Cold sessions are one action, not one per session: they share the same
|
|
721
|
+
// remedy (open them in the GUI), and a list of identical lines buried the
|
|
722
|
+
// actions that differ.
|
|
723
|
+
const cold = (state.sessions ?? []).filter(row => row.live !== true).map(row => displayName(row))
|
|
724
|
+
if (cold.length > 0) {
|
|
725
|
+
const names = cold.slice(0, 3).join('、') + (cold.length > 3 ? ` 等 ${cold.length} 个` : '')
|
|
726
|
+
actions.push({ severity: 'normal', text: `冷会话 ${cold.length} 个(${names}):在 GUI 打开即可唤醒加入面` })
|
|
715
727
|
}
|
|
716
728
|
for (const row of state.reconcile?.rows ?? []) {
|
|
717
729
|
if (row.needsRepair === true) {
|
|
718
|
-
|
|
730
|
+
// Name the session; the id stays for the operator who greps logs.
|
|
731
|
+
const s = (state.sessions ?? []).find(x => x.id === row.id)
|
|
732
|
+
const who = s !== undefined ? `${displayName(s)}(${row.id})` : row.id
|
|
733
|
+
actions.push({ severity: 'high', text: `reconcile 需修复:${who} ${row.error}` })
|
|
719
734
|
}
|
|
720
735
|
}
|
|
721
736
|
const conflicts = state.idempotency?.conflicts ?? 0
|
|
@@ -1122,35 +1137,58 @@ export function createWorkbench(deps: WorkbenchDeps): Workbench {
|
|
|
1122
1137
|
// forensic counters that used to sit here (idempotency claims/replays,
|
|
1123
1138
|
// reconcile state) read on the ledger's 取证 line instead.
|
|
1124
1139
|
function renderHeader(): void {
|
|
1125
|
-
|
|
1140
|
+
// An unknown version says nothing: hide the badge rather than print 'v?'.
|
|
1141
|
+
versionBadge.textContent = last?.version !== undefined ? `v${last.version}` : ''
|
|
1142
|
+
versionBadge.hidden = last?.version === undefined
|
|
1126
1143
|
const box = clear(headerStats)
|
|
1127
1144
|
if (last === null) return
|
|
1128
|
-
|
|
1145
|
+
// Each pill is a way in: it opens the tab (and ledger group) that holds
|
|
1146
|
+
// the rows behind its count.
|
|
1147
|
+
const alerts: Array<{ text: string; tone: 'warn' | 'danger'; title: string; tab: 'ledger' | 'plan' | 'activity'; group?: string }> = []
|
|
1129
1148
|
// Same definitions as the 台账 tab (wb-ledger ledgerFacts): fire-forget
|
|
1130
1149
|
// expiries are not dead letters, receiptless rows are never stale.
|
|
1131
1150
|
const facts = ledgerFacts(last, deps.now())
|
|
1132
1151
|
const dead = facts.deadLetters.length
|
|
1133
|
-
if (dead > 0) alerts.push({ text: `死信 ${dead}`, tone: 'danger', title: '超过 24h TTL 仍未结算的任务(unconsumed),需在台账清账' })
|
|
1152
|
+
if (dead > 0) alerts.push({ text: `死信 ${dead}`, tone: 'danger', title: '超过 24h TTL 仍未结算的任务(unconsumed),需在台账清账', tab: 'ledger', group: '死信' })
|
|
1134
1153
|
const stale = facts.stale.length
|
|
1135
|
-
if (stale > 0) alerts.push({ text: `陈旧 ${stale}`, tone: 'warn', title: '我方派发、要回执、已等待 ≥1h 仍未回执(可催办;免回执行不算)' })
|
|
1154
|
+
if (stale > 0) alerts.push({ text: `陈旧 ${stale}`, tone: 'warn', title: '我方派发、要回执、已等待 ≥1h 仍未回执(可催办;免回执行不算)', tab: 'ledger', group: '陈旧' })
|
|
1136
1155
|
const owed = facts.owedOverdue.length
|
|
1137
|
-
if (owed > 0) alerts.push({ text: `欠回执 ${owed}`, tone: 'warn', title: '对方投递给本机、已等待本机回执超过 1 小时' })
|
|
1156
|
+
if (owed > 0) alerts.push({ text: `欠回执 ${owed}`, tone: 'warn', title: '对方投递给本机、已等待本机回执超过 1 小时', tab: 'ledger', group: '待本机回执' })
|
|
1138
1157
|
const conflicts = last.idempotency?.conflicts ?? 0
|
|
1139
|
-
if (conflicts > 0) alerts.push({ text: `幂等冲突 ${conflicts}`, tone: 'danger', title: '同一幂等键对应了不同载荷,需人工核查' })
|
|
1158
|
+
if (conflicts > 0) alerts.push({ text: `幂等冲突 ${conflicts}`, tone: 'danger', title: '同一幂等键对应了不同载荷,需人工核查', tab: 'ledger', group: '取证' })
|
|
1140
1159
|
const repair = (last.reconcile?.rows ?? []).filter(row => row.needsRepair === true).length
|
|
1141
|
-
if (repair > 0) alerts.push({ text: `需修复 ${repair}`, tone: 'danger', title: 'reconcile 无法自动恢复的会话,见行动计划' })
|
|
1160
|
+
if (repair > 0) alerts.push({ text: `需修复 ${repair}`, tone: 'danger', title: 'reconcile 无法自动恢复的会话,见行动计划', tab: 'plan' })
|
|
1142
1161
|
if (alerts.length === 0) {
|
|
1143
1162
|
box.appendChild(el('span', 'wb-header-ok')).textContent = '● 无异常'
|
|
1144
1163
|
return
|
|
1145
1164
|
}
|
|
1146
1165
|
for (const a of alerts) {
|
|
1147
|
-
const pill = el('
|
|
1166
|
+
const pill = el('button', `wb-alert wb-alert-${a.tone}`)
|
|
1167
|
+
pill.type = 'button'
|
|
1148
1168
|
pill.textContent = a.text
|
|
1149
|
-
pill.title = a.title
|
|
1169
|
+
pill.title = `${a.title}(点击查看)`
|
|
1170
|
+
pill.addEventListener('click', () => openAlert(a.tab, a.group))
|
|
1150
1171
|
box.appendChild(pill)
|
|
1151
1172
|
}
|
|
1152
1173
|
}
|
|
1153
1174
|
|
|
1175
|
+
/** Open the pane behind a header pill. With the board up the panes are
|
|
1176
|
+
* rail tabs (on a narrow screen the rail slides in first); without it they
|
|
1177
|
+
* sit on the stage. A ledger group (or the 取证 fold) scrolls into view. */
|
|
1178
|
+
function openAlert(tab: 'ledger' | 'plan' | 'activity', group?: string): void {
|
|
1179
|
+
const pane = dataPanes.find(([k]) => k === tab)![1]
|
|
1180
|
+
if (mode === 'canvas') {
|
|
1181
|
+
root.classList.add('is-rail-open')
|
|
1182
|
+
railTabs.select(tab, { focus: false })
|
|
1183
|
+
}
|
|
1184
|
+
const target = group === undefined ? pane
|
|
1185
|
+
: group === '取证' ? pane.querySelector<HTMLDetailsElement>('details.wb-led-forensics')
|
|
1186
|
+
: Array.from(pane.querySelectorAll<HTMLElement>('.wb-led-group')).find(g => g.dataset.group === group) ?? null
|
|
1187
|
+
if (target === null) return
|
|
1188
|
+
if (target instanceof HTMLDetailsElement) target.open = true
|
|
1189
|
+
target.scrollIntoView?.({ block: 'start', behavior: 'smooth' })
|
|
1190
|
+
}
|
|
1191
|
+
|
|
1154
1192
|
function update(state: StateBody | null): void {
|
|
1155
1193
|
last = state
|
|
1156
1194
|
// 0.6.12: refresh the readable-log list opportunistically (single flight;
|
package/nexus-stage/src/world.ts
CHANGED
|
@@ -153,7 +153,7 @@ export function planSeatClear(sid: string, occupied: ReadonlyArray<{ readonly x:
|
|
|
153
153
|
}
|
|
154
154
|
|
|
155
155
|
/** Minimal session row shape the model needs (topology.SessionRow satisfies it). */
|
|
156
|
-
export interface SessionLite { id: string; label: string; team: string; name?: string; joined: boolean; live?: boolean; teams?: readonly string[] }
|
|
156
|
+
export interface SessionLite { id: string; label: string; team: string; name?: string; description?: string; joined: boolean; live?: boolean; teams?: readonly string[] }
|
|
157
157
|
|
|
158
158
|
/** Minimal team shape (topology.CanvasTeam satisfies it). `team` is the
|
|
159
159
|
* mono route alias `<host>/canvas/<name>` when the payload carries it. */
|
|
@@ -172,6 +172,8 @@ export interface WorldNode {
|
|
|
172
172
|
/** The route alias `<team>/<id8>` the state payload provides. */
|
|
173
173
|
team: string
|
|
174
174
|
name?: string
|
|
175
|
+
/** What the session is for (/state `description`) — the card's second line. */
|
|
176
|
+
description?: string
|
|
175
177
|
live: boolean
|
|
176
178
|
/** Peer node (PR D v1 ruling): a remote host entry rendered as a card.
|
|
177
179
|
* Excluded from local content bounds; canvas memberships allowed since
|
|
@@ -338,6 +340,7 @@ export class WorldModel {
|
|
|
338
340
|
}
|
|
339
341
|
existing.label = n.label
|
|
340
342
|
existing.name = n.name
|
|
343
|
+
existing.description = n.description
|
|
341
344
|
existing.live = n.live
|
|
342
345
|
existing.memberships = n.memberships
|
|
343
346
|
this.bump()
|
|
@@ -479,6 +482,7 @@ export class WorldModel {
|
|
|
479
482
|
label: s.label,
|
|
480
483
|
team: s.team,
|
|
481
484
|
name: s.name,
|
|
485
|
+
description: s.description,
|
|
482
486
|
live: s.live !== false,
|
|
483
487
|
memberships: memberships.get(s.id) ?? [],
|
|
484
488
|
})
|