@nelsonlongxiang/dsh-open-a2a-net 0.6.40 → 0.6.42

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 (76) hide show
  1. package/README.md +2 -1
  2. package/README_zh.md +2 -1
  3. package/assets/stageDist/assets/index-Bj7S2pA1.css +1 -1
  4. package/assets/stageDist/assets/index-C-28k_o5.js +974 -974
  5. package/assets/stageDist/index.html +12 -12
  6. package/lib/a2a-client.js +3 -2
  7. package/lib/client.js +64 -64
  8. package/lib/client.js.map +1 -1
  9. package/lib/config.js +1 -0
  10. package/lib/faces/files.js +2 -2
  11. package/lib/faces/session-log.js +3 -3
  12. package/lib/faces/wire.js +11 -16
  13. package/lib/index.js +102 -18
  14. package/lib/link-keys.js +29 -0
  15. package/lib/origin-claim.js +4 -0
  16. package/lib/skill/SKILL.md +117 -117
  17. package/lib/skill/references/collaboration.md +428 -428
  18. package/lib/skill/references/discovery.md +78 -78
  19. package/lib/skill/references/intake.md +107 -107
  20. package/lib/skill/references/membership.md +73 -73
  21. package/lib/skill/references/pitfalls.md +51 -51
  22. package/lib/skill/references/router.md +96 -96
  23. package/lib/skill/references/routing.md +65 -65
  24. package/lib/skill/references/setup.md +5 -1
  25. package/lib/tools/files.js +3 -1
  26. package/lib/types/a2a-client.d.ts +3 -1
  27. package/lib/types/config.d.ts +6 -0
  28. package/lib/types/faces/files.d.ts +2 -1
  29. package/lib/types/faces/session-log.d.ts +2 -0
  30. package/lib/types/faces/wire.d.ts +2 -1
  31. package/lib/types/link-keys.d.ts +13 -0
  32. package/lib/types/origin-claim.d.ts +2 -0
  33. package/lib/types/tools/files.d.ts +1 -1
  34. package/nexus-stage/package.json +21 -21
  35. package/nexus-stage/scripts/mockhost.py +261 -261
  36. package/nexus-stage/src/canvas-ops.ts +191 -191
  37. package/nexus-stage/src/canvas-wire.ts +164 -164
  38. package/nexus-stage/src/cli-home-picker.ts +42 -42
  39. package/nexus-stage/src/fault.ts +56 -56
  40. package/nexus-stage/src/federation-overlay.ts +103 -103
  41. package/nexus-stage/src/federation.ts +241 -241
  42. package/nexus-stage/src/layout-doc.ts +145 -145
  43. package/nexus-stage/src/main.ts +513 -513
  44. package/nexus-stage/src/planning-card-text.ts +73 -73
  45. package/nexus-stage/src/planning-card.ts +63 -63
  46. package/nexus-stage/src/planning-edges.ts +26 -26
  47. package/nexus-stage/src/planning-fit.ts +45 -45
  48. package/nexus-stage/src/planning-frame-hue.ts +13 -13
  49. package/nexus-stage/src/planning-legend.ts +39 -39
  50. package/nexus-stage/src/planning-member-handles.ts +61 -61
  51. package/nexus-stage/src/planning-menus.ts +422 -422
  52. package/nexus-stage/src/planning-mirror.ts +75 -75
  53. package/nexus-stage/src/planning-nodelist.ts +294 -294
  54. package/nexus-stage/src/planning-overlap.ts +159 -159
  55. package/nexus-stage/src/planning-reachability.ts +28 -28
  56. package/nexus-stage/src/planning-relayout.ts +121 -121
  57. package/nexus-stage/src/planning-roster.ts +15 -15
  58. package/nexus-stage/src/planning-search.ts +168 -168
  59. package/nexus-stage/src/planning-types.ts +115 -115
  60. package/nexus-stage/src/planning-undo.ts +68 -68
  61. package/nexus-stage/src/planning-view.ts +1499 -1499
  62. package/nexus-stage/src/planning.css +340 -340
  63. package/nexus-stage/src/seat-name-editor.ts +31 -31
  64. package/nexus-stage/src/session-drag.ts +64 -64
  65. package/nexus-stage/src/viewport.ts +106 -106
  66. package/nexus-stage/src/wb-chat.ts +684 -684
  67. package/nexus-stage/src/wb-directory.ts +237 -237
  68. package/nexus-stage/src/wb-ledger.ts +279 -279
  69. package/nexus-stage/src/wb-names.ts +43 -43
  70. package/nexus-stage/src/wb-rail.ts +147 -147
  71. package/nexus-stage/src/workbench.css +388 -388
  72. package/nexus-stage/src/workbench.ts +1272 -1272
  73. package/nexus-stage/src/world.ts +509 -509
  74. package/nexus-stage/tsconfig.json +24 -24
  75. package/nexus-stage/vite.config.ts +15 -15
  76. package/package.json +1 -1
@@ -1,164 +1,164 @@
1
- /**
2
- * The canvas write face's transport layer (POST /__dsh_a2a/canvas): action
3
- * sequencing, per-team serialization, and the notice classification that
4
- * design.md §3.4 mandates (host error text surfaces VERBATIM - the sidebar
5
- * reference's swallowed `.catch(() => {})` is the anti-pattern here).
6
- *
7
- * Classification per request:
8
- * - `ok:true` → success (silent)
9
- * - `ok:false` WITH an `error` string → error notice, verbatim
10
- * - `ok:false` without `error` → success only when returned members/teams
11
- * prove the requested idempotent effect; cap/unknown results are failures
12
- * - HTTP >= 400 → body error verbatim, else `HTTP <status>`
13
- * - send() throws → `画布不可达:<message>`
14
- * - missing success acknowledgement (including non-JSON transport errors)
15
- * → visible failure, never a confirmed join
16
- *
17
- * Ops on the same team run through a serial queue (a later gesture waits
18
- * instead of racing; cross-team ops run in parallel) - mirrors the
19
- * sidebar's per-team busy keys, minus the rejection hostility. Pure
20
- * control flow: the transport is injected.
21
- * @module nexus-stage/canvas-wire
22
- */
23
-
24
- /** One send outcome: HTTP status plus the parsed JSON body (whatever it is). */
25
- export interface CanvasWireResult {
26
- status: number
27
- body: { ok?: boolean; error?: string; teams?: readonly string[]; members?: readonly string[]; name?: string }
28
- }
29
-
30
- /** Transport injection: POST the body, resolve with parsed outcome, throw on network failure. */
31
- export type CanvasSend = (body: unknown) => Promise<CanvasWireResult>
32
-
33
- export interface CanvasWireDeps {
34
- send: CanvasSend
35
- onNotice(kind: 'error' | 'info', text: string): void
36
- }
37
-
38
- export interface CanvasWire {
39
- /** create → add-member per id, serial, first failure stops. `created` is
40
- * a model-side hint ONLY, never server creation provenance. A failed add
41
- * cannot authorize deleting a same-name team; preserve it for retry. */
42
- createTeam(name: string, ids: readonly string[], created?: boolean): Promise<boolean>
43
- addMembers(team: string, ids: readonly string[]): Promise<boolean>
44
- removeMembers(team: string, ids: readonly string[]): Promise<boolean>
45
- /** Pre-computed remove/add roster ops (from canvas-ops.reorderOps). */
46
- runRosterOps(team: string, ops: ReadonlyArray<{ op: 'remove' | 'add'; id: string }>): Promise<boolean>
47
- removeTeam(name: string): Promise<boolean>
48
- hasPending(): boolean
49
- }
50
-
51
- type RosterOp = { op: 'remove' | 'add'; id: string }
52
-
53
- function errorText(body: CanvasWireResult['body']): string | undefined {
54
- return body != null && typeof body.error === 'string' ? body.error : undefined
55
- }
56
-
57
- export function createCanvasWire(deps: CanvasWireDeps): CanvasWire {
58
- const queues = new Map<string, Promise<unknown>>()
59
- let pending = 0
60
-
61
- /** Serial per-team queue: same key waits, other keys run in parallel. */
62
- function enqueue<T>(key: string, task: () => Promise<T>): Promise<T> {
63
- pending += 1
64
- const prev = queues.get(key) ?? Promise.resolve()
65
- const run = async (): Promise<T> => {
66
- try {
67
- return await task()
68
- } finally {
69
- pending -= 1 // settles BEFORE the returned promise resolves
70
- }
71
- }
72
- const next = prev.then(run, run) // run regardless of how the predecessor settled
73
- queues.set(
74
- key,
75
- next.then(
76
- () => undefined,
77
- () => undefined,
78
- ),
79
- )
80
- return next
81
- }
82
-
83
- /** One classified request — NEVER enqueues (public ops own the queue). */
84
- async function raw(body: Record<string, unknown>): Promise<boolean> {
85
- let res: CanvasWireResult
86
- try {
87
- res = await deps.send(body)
88
- } catch (error) {
89
- const message = String((error as Error | undefined)?.message ?? error).slice(0, 80)
90
- deps.onNotice('error', `画布不可达:${message}`)
91
- return false
92
- }
93
- if (res.status >= 400) {
94
- deps.onNotice('error', errorText(res.body) ?? `HTTP ${res.status}`)
95
- return false
96
- }
97
- if (res.body?.ok === true) return true
98
- const text = res.body != null && typeof res.body === 'object' ? errorText(res.body) : undefined
99
- if (text !== undefined) {
100
- deps.onNotice('error', text)
101
- return false
102
- }
103
- // A bare false can mean a capacity refusal, not just an idempotent no-op.
104
- // Accept a no-op only when the host's returned state proves the effect.
105
- const name = String(body.name ?? ''), id = String(body.id ?? '')
106
- const members = res.body?.members, teams = res.body?.teams
107
- const memberProof = Array.isArray(members) && members.every(v => typeof v === 'string')
108
- const teamProof = Array.isArray(teams) && teams.every(v => typeof v === 'string')
109
- if (res.body?.ok === false && (
110
- (body.action === 'add-member' && memberProof && members.includes(id)) ||
111
- (body.action === 'remove-member' && memberProof && !members.includes(id)) ||
112
- (body.action === 'remove' && teamProof && !teams.includes(name))
113
- )) return true
114
- deps.onNotice('error', '画布操作未确认:' + String(body.action) + '(可能队伍/成员已达上限,请重试)')
115
- return false
116
- }
117
-
118
- async function serial(body: Record<string, unknown>, ids: readonly string[]): Promise<boolean> {
119
- for (const id of ids) {
120
- if (!(await raw({ ...body, id }))) return false
121
- }
122
- return true
123
- }
124
-
125
- return {
126
- createTeam(name, ids) {
127
- return enqueue(name, async () => {
128
- if (!(await raw({ action: 'create', name }))) return false
129
- for (let i = 0; i < ids.length; i++) {
130
- if (!(await raw({ action: 'add-member', name, id: ids[i]! }))) {
131
- // Local absence does not prove server-side creation. Never issue
132
- // an unconditional compensating remove: an idempotent create may
133
- // have reused an existing team (including one already at capacity).
134
- return false
135
- }
136
- }
137
- return true
138
- })
139
- },
140
- addMembers(team, ids) {
141
- return enqueue(team, () => serial({ action: 'add-member', name: team }, ids))
142
- },
143
- removeMembers(team, ids) {
144
- return enqueue(team, () => serial({ action: 'remove-member', name: team }, ids))
145
- },
146
- runRosterOps(team, ops) {
147
- return enqueue(team, async () => {
148
- for (const op of ops) {
149
- const body = op.op === 'remove'
150
- ? { action: 'remove-member', name: team, id: op.id }
151
- : { action: 'add-member', name: team, id: op.id }
152
- if (!(await raw(body))) return false
153
- }
154
- return true
155
- })
156
- },
157
- removeTeam(name) {
158
- return enqueue(name, () => raw({ action: 'remove', name }))
159
- },
160
- hasPending() {
161
- return pending > 0
162
- },
163
- }
164
- }
1
+ /**
2
+ * The canvas write face's transport layer (POST /__dsh_a2a/canvas): action
3
+ * sequencing, per-team serialization, and the notice classification that
4
+ * design.md §3.4 mandates (host error text surfaces VERBATIM - the sidebar
5
+ * reference's swallowed `.catch(() => {})` is the anti-pattern here).
6
+ *
7
+ * Classification per request:
8
+ * - `ok:true` → success (silent)
9
+ * - `ok:false` WITH an `error` string → error notice, verbatim
10
+ * - `ok:false` without `error` → success only when returned members/teams
11
+ * prove the requested idempotent effect; cap/unknown results are failures
12
+ * - HTTP >= 400 → body error verbatim, else `HTTP <status>`
13
+ * - send() throws → `画布不可达:<message>`
14
+ * - missing success acknowledgement (including non-JSON transport errors)
15
+ * → visible failure, never a confirmed join
16
+ *
17
+ * Ops on the same team run through a serial queue (a later gesture waits
18
+ * instead of racing; cross-team ops run in parallel) - mirrors the
19
+ * sidebar's per-team busy keys, minus the rejection hostility. Pure
20
+ * control flow: the transport is injected.
21
+ * @module nexus-stage/canvas-wire
22
+ */
23
+
24
+ /** One send outcome: HTTP status plus the parsed JSON body (whatever it is). */
25
+ export interface CanvasWireResult {
26
+ status: number
27
+ body: { ok?: boolean; error?: string; teams?: readonly string[]; members?: readonly string[]; name?: string }
28
+ }
29
+
30
+ /** Transport injection: POST the body, resolve with parsed outcome, throw on network failure. */
31
+ export type CanvasSend = (body: unknown) => Promise<CanvasWireResult>
32
+
33
+ export interface CanvasWireDeps {
34
+ send: CanvasSend
35
+ onNotice(kind: 'error' | 'info', text: string): void
36
+ }
37
+
38
+ export interface CanvasWire {
39
+ /** create → add-member per id, serial, first failure stops. `created` is
40
+ * a model-side hint ONLY, never server creation provenance. A failed add
41
+ * cannot authorize deleting a same-name team; preserve it for retry. */
42
+ createTeam(name: string, ids: readonly string[], created?: boolean): Promise<boolean>
43
+ addMembers(team: string, ids: readonly string[]): Promise<boolean>
44
+ removeMembers(team: string, ids: readonly string[]): Promise<boolean>
45
+ /** Pre-computed remove/add roster ops (from canvas-ops.reorderOps). */
46
+ runRosterOps(team: string, ops: ReadonlyArray<{ op: 'remove' | 'add'; id: string }>): Promise<boolean>
47
+ removeTeam(name: string): Promise<boolean>
48
+ hasPending(): boolean
49
+ }
50
+
51
+ type RosterOp = { op: 'remove' | 'add'; id: string }
52
+
53
+ function errorText(body: CanvasWireResult['body']): string | undefined {
54
+ return body != null && typeof body.error === 'string' ? body.error : undefined
55
+ }
56
+
57
+ export function createCanvasWire(deps: CanvasWireDeps): CanvasWire {
58
+ const queues = new Map<string, Promise<unknown>>()
59
+ let pending = 0
60
+
61
+ /** Serial per-team queue: same key waits, other keys run in parallel. */
62
+ function enqueue<T>(key: string, task: () => Promise<T>): Promise<T> {
63
+ pending += 1
64
+ const prev = queues.get(key) ?? Promise.resolve()
65
+ const run = async (): Promise<T> => {
66
+ try {
67
+ return await task()
68
+ } finally {
69
+ pending -= 1 // settles BEFORE the returned promise resolves
70
+ }
71
+ }
72
+ const next = prev.then(run, run) // run regardless of how the predecessor settled
73
+ queues.set(
74
+ key,
75
+ next.then(
76
+ () => undefined,
77
+ () => undefined,
78
+ ),
79
+ )
80
+ return next
81
+ }
82
+
83
+ /** One classified request — NEVER enqueues (public ops own the queue). */
84
+ async function raw(body: Record<string, unknown>): Promise<boolean> {
85
+ let res: CanvasWireResult
86
+ try {
87
+ res = await deps.send(body)
88
+ } catch (error) {
89
+ const message = String((error as Error | undefined)?.message ?? error).slice(0, 80)
90
+ deps.onNotice('error', `画布不可达:${message}`)
91
+ return false
92
+ }
93
+ if (res.status >= 400) {
94
+ deps.onNotice('error', errorText(res.body) ?? `HTTP ${res.status}`)
95
+ return false
96
+ }
97
+ if (res.body?.ok === true) return true
98
+ const text = res.body != null && typeof res.body === 'object' ? errorText(res.body) : undefined
99
+ if (text !== undefined) {
100
+ deps.onNotice('error', text)
101
+ return false
102
+ }
103
+ // A bare false can mean a capacity refusal, not just an idempotent no-op.
104
+ // Accept a no-op only when the host's returned state proves the effect.
105
+ const name = String(body.name ?? ''), id = String(body.id ?? '')
106
+ const members = res.body?.members, teams = res.body?.teams
107
+ const memberProof = Array.isArray(members) && members.every(v => typeof v === 'string')
108
+ const teamProof = Array.isArray(teams) && teams.every(v => typeof v === 'string')
109
+ if (res.body?.ok === false && (
110
+ (body.action === 'add-member' && memberProof && members.includes(id)) ||
111
+ (body.action === 'remove-member' && memberProof && !members.includes(id)) ||
112
+ (body.action === 'remove' && teamProof && !teams.includes(name))
113
+ )) return true
114
+ deps.onNotice('error', '画布操作未确认:' + String(body.action) + '(可能队伍/成员已达上限,请重试)')
115
+ return false
116
+ }
117
+
118
+ async function serial(body: Record<string, unknown>, ids: readonly string[]): Promise<boolean> {
119
+ for (const id of ids) {
120
+ if (!(await raw({ ...body, id }))) return false
121
+ }
122
+ return true
123
+ }
124
+
125
+ return {
126
+ createTeam(name, ids) {
127
+ return enqueue(name, async () => {
128
+ if (!(await raw({ action: 'create', name }))) return false
129
+ for (let i = 0; i < ids.length; i++) {
130
+ if (!(await raw({ action: 'add-member', name, id: ids[i]! }))) {
131
+ // Local absence does not prove server-side creation. Never issue
132
+ // an unconditional compensating remove: an idempotent create may
133
+ // have reused an existing team (including one already at capacity).
134
+ return false
135
+ }
136
+ }
137
+ return true
138
+ })
139
+ },
140
+ addMembers(team, ids) {
141
+ return enqueue(team, () => serial({ action: 'add-member', name: team }, ids))
142
+ },
143
+ removeMembers(team, ids) {
144
+ return enqueue(team, () => serial({ action: 'remove-member', name: team }, ids))
145
+ },
146
+ runRosterOps(team, ops) {
147
+ return enqueue(team, async () => {
148
+ for (const op of ops) {
149
+ const body = op.op === 'remove'
150
+ ? { action: 'remove-member', name: team, id: op.id }
151
+ : { action: 'add-member', name: team, id: op.id }
152
+ if (!(await raw(body))) return false
153
+ }
154
+ return true
155
+ })
156
+ },
157
+ removeTeam(name) {
158
+ return enqueue(name, () => raw({ action: 'remove', name }))
159
+ },
160
+ hasPending() {
161
+ return pending > 0
162
+ },
163
+ }
164
+ }
@@ -1,42 +1,42 @@
1
- /** Explicit operator choice only; this form never auto-selects or relaxes home admission. */
2
- export interface CliHomeOptions { ok: boolean; homes?: string[]; unreadable?: number; held?: boolean; error?: string }
3
- export function createCliHomePicker(read: () => Promise<CliHomeOptions>, save: (home: string) => Promise<boolean>): HTMLFormElement {
4
- const form = document.createElement('form')
5
- form.className = 'wb-cli-home'
6
- const readButton = document.createElement('button')
7
- readButton.type = 'button'; readButton.textContent = '指定 home'; readButton.className = 'wb-member-submit'
8
- const select = document.createElement('select')
9
- select.className = 'wb-member-pick'; select.setAttribute('aria-label', '选择 CLI home 主机'); select.hidden = true
10
- const submit = document.createElement('button')
11
- submit.type = 'submit'; submit.textContent = '确认 home'; submit.className = 'wb-member-submit'; submit.hidden = true; submit.disabled = true
12
- const note = document.createElement('div')
13
- note.className = 'wb-hint'; note.setAttribute('role', 'status'); note.setAttribute('aria-live', 'polite')
14
- note.textContent = 'home 未确定;点击指定 home 现读已链接主机名片。'
15
- let busy = false
16
- readButton.addEventListener('click', async () => {
17
- if (busy) return
18
- busy = true; readButton.disabled = true; submit.disabled = true; note.textContent = '正在现读主机名片…'
19
- try {
20
- const result = await read()
21
- if (!result.ok) throw Error(result.error ?? '候选读取失败')
22
- if (result.held) { readButton.hidden = true; select.hidden = true; submit.hidden = true; select.replaceChildren(); note.textContent = '本机持有,无需指定'; return }
23
- select.replaceChildren(new Option('请选择 home 主机…', '', true, true))
24
- for (const home of result.homes ?? []) select.appendChild(new Option(home, home))
25
- select.hidden = false; submit.hidden = false; readButton.textContent = '刷新候选'
26
- const count = result.homes?.length ?? 0
27
- note.textContent = (result.unreadable ? '有 ' + result.unreadable + ' 台对端名片读不到;' : '') + (count > 1 ? '多台已链接主机列出该句柄,请显式选择。' : count === 1 ? '找到 1 台已链接主机;仍需你显式确认。' : '当前没有已链接且列出该句柄的可选主机。')
28
- } catch (error) { note.textContent = '读取失败:' + String(error); select.hidden = true; submit.hidden = true }
29
- finally { busy = false; readButton.disabled = false }
30
- })
31
- select.addEventListener('change', () => { submit.disabled = busy || select.value === '' })
32
- form.addEventListener('submit', async event => {
33
- event.preventDefault()
34
- if (busy || select.value === '') return
35
- busy = true; readButton.disabled = true; select.disabled = true; submit.disabled = true
36
- try { note.textContent = await save(select.value) ? 'home 已指定:' + select.value : '指定失败:主机可能已离线或不再列出该句柄,请刷新后重选。' }
37
- catch (error) { note.textContent = '指定失败:' + String(error) }
38
- finally { busy = false; readButton.disabled = false; select.disabled = false; submit.disabled = select.value === '' }
39
- })
40
- form.append(readButton, select, submit, note)
41
- return form
42
- }
1
+ /** Explicit operator choice only; this form never auto-selects or relaxes home admission. */
2
+ export interface CliHomeOptions { ok: boolean; homes?: string[]; unreadable?: number; held?: boolean; error?: string }
3
+ export function createCliHomePicker(read: () => Promise<CliHomeOptions>, save: (home: string) => Promise<boolean>): HTMLFormElement {
4
+ const form = document.createElement('form')
5
+ form.className = 'wb-cli-home'
6
+ const readButton = document.createElement('button')
7
+ readButton.type = 'button'; readButton.textContent = '指定 home'; readButton.className = 'wb-member-submit'
8
+ const select = document.createElement('select')
9
+ select.className = 'wb-member-pick'; select.setAttribute('aria-label', '选择 CLI home 主机'); select.hidden = true
10
+ const submit = document.createElement('button')
11
+ submit.type = 'submit'; submit.textContent = '确认 home'; submit.className = 'wb-member-submit'; submit.hidden = true; submit.disabled = true
12
+ const note = document.createElement('div')
13
+ note.className = 'wb-hint'; note.setAttribute('role', 'status'); note.setAttribute('aria-live', 'polite')
14
+ note.textContent = 'home 未确定;点击指定 home 现读已链接主机名片。'
15
+ let busy = false
16
+ readButton.addEventListener('click', async () => {
17
+ if (busy) return
18
+ busy = true; readButton.disabled = true; submit.disabled = true; note.textContent = '正在现读主机名片…'
19
+ try {
20
+ const result = await read()
21
+ if (!result.ok) throw Error(result.error ?? '候选读取失败')
22
+ if (result.held) { readButton.hidden = true; select.hidden = true; submit.hidden = true; select.replaceChildren(); note.textContent = '本机持有,无需指定'; return }
23
+ select.replaceChildren(new Option('请选择 home 主机…', '', true, true))
24
+ for (const home of result.homes ?? []) select.appendChild(new Option(home, home))
25
+ select.hidden = false; submit.hidden = false; readButton.textContent = '刷新候选'
26
+ const count = result.homes?.length ?? 0
27
+ note.textContent = (result.unreadable ? '有 ' + result.unreadable + ' 台对端名片读不到;' : '') + (count > 1 ? '多台已链接主机列出该句柄,请显式选择。' : count === 1 ? '找到 1 台已链接主机;仍需你显式确认。' : '当前没有已链接且列出该句柄的可选主机。')
28
+ } catch (error) { note.textContent = '读取失败:' + String(error); select.hidden = true; submit.hidden = true }
29
+ finally { busy = false; readButton.disabled = false }
30
+ })
31
+ select.addEventListener('change', () => { submit.disabled = busy || select.value === '' })
32
+ form.addEventListener('submit', async event => {
33
+ event.preventDefault()
34
+ if (busy || select.value === '') return
35
+ busy = true; readButton.disabled = true; select.disabled = true; submit.disabled = true
36
+ try { note.textContent = await save(select.value) ? 'home 已指定:' + select.value : '指定失败:主机可能已离线或不再列出该句柄,请刷新后重选。' }
37
+ catch (error) { note.textContent = '指定失败:' + String(error) }
38
+ finally { busy = false; readButton.disabled = false; select.disabled = false; submit.disabled = select.value === '' }
39
+ })
40
+ form.append(readButton, select, submit, note)
41
+ return form
42
+ }
@@ -1,56 +1,56 @@
1
- /**
2
- * The stage's fault surface as pure logic: whoever calls {@link fault}
3
- * gets the newest message on the sink immediately, while the diagnostic
4
- * log line is throttled to one per window (the N2 breadcrumb cadence).
5
- * {@link clear} hides the sink and resets the throttle clock, so the first
6
- * fault after a healthy stretch logs promptly again.
7
- *
8
- * DOM-free on purpose: the browser shell in main.ts supplies the sink, and
9
- * the unit test supplies fakes — the lifecycle (sink-always-current,
10
- * log-throttled, clear-resets) is what the regression net owns. This module
11
- * exists because the incident class it guards against (a catch burying a
12
- * ReferenceError behind a healthy render loop) shipped once already.
13
- * @module nexus-stage/fault
14
- */
15
-
16
- /** How long one diagnostic log line suppresses its successors. */
17
- export const FAULT_LOG_THROTTLE_MS = 60_000
18
-
19
- /** Anything the fault reporter can render onto. */
20
- export interface FaultSink {
21
- /** Surface the newest fault; the sink becomes visible with this text. */
22
- show(message: string): void
23
- /** Retract the sink after a healthy response. */
24
- hide(): void
25
- }
26
-
27
- export interface FaultReporter {
28
- /** Surface a fault: the sink always updates, the log line throttles. */
29
- fault(message: string): void
30
- /** A healthy response arrived: hide the sink and reset the throttle. */
31
- clear(): void
32
- }
33
-
34
- /**
35
- * @param sink - render target (DOM badge in the shell, recording fake in tests).
36
- * @param log - diagnostic channel (console.error in the shell, spy in tests).
37
- * @param now - injectable clock for deterministic throttle tests. A fault
38
- * exactly one window after the previous logs again (window is inclusive).
39
- */
40
- export function createFaultReporter(sink: FaultSink, log: (message: string) => void, now: () => number = Date.now): FaultReporter {
41
- let lastLoggedAt = -FAULT_LOG_THROTTLE_MS
42
- return {
43
- fault(message) {
44
- const at = now()
45
- if (at - lastLoggedAt >= FAULT_LOG_THROTTLE_MS) {
46
- log(`[nexus] ${message}`)
47
- lastLoggedAt = at
48
- }
49
- sink.show(message)
50
- },
51
- clear() {
52
- sink.hide()
53
- lastLoggedAt = -FAULT_LOG_THROTTLE_MS
54
- },
55
- }
56
- }
1
+ /**
2
+ * The stage's fault surface as pure logic: whoever calls {@link fault}
3
+ * gets the newest message on the sink immediately, while the diagnostic
4
+ * log line is throttled to one per window (the N2 breadcrumb cadence).
5
+ * {@link clear} hides the sink and resets the throttle clock, so the first
6
+ * fault after a healthy stretch logs promptly again.
7
+ *
8
+ * DOM-free on purpose: the browser shell in main.ts supplies the sink, and
9
+ * the unit test supplies fakes — the lifecycle (sink-always-current,
10
+ * log-throttled, clear-resets) is what the regression net owns. This module
11
+ * exists because the incident class it guards against (a catch burying a
12
+ * ReferenceError behind a healthy render loop) shipped once already.
13
+ * @module nexus-stage/fault
14
+ */
15
+
16
+ /** How long one diagnostic log line suppresses its successors. */
17
+ export const FAULT_LOG_THROTTLE_MS = 60_000
18
+
19
+ /** Anything the fault reporter can render onto. */
20
+ export interface FaultSink {
21
+ /** Surface the newest fault; the sink becomes visible with this text. */
22
+ show(message: string): void
23
+ /** Retract the sink after a healthy response. */
24
+ hide(): void
25
+ }
26
+
27
+ export interface FaultReporter {
28
+ /** Surface a fault: the sink always updates, the log line throttles. */
29
+ fault(message: string): void
30
+ /** A healthy response arrived: hide the sink and reset the throttle. */
31
+ clear(): void
32
+ }
33
+
34
+ /**
35
+ * @param sink - render target (DOM badge in the shell, recording fake in tests).
36
+ * @param log - diagnostic channel (console.error in the shell, spy in tests).
37
+ * @param now - injectable clock for deterministic throttle tests. A fault
38
+ * exactly one window after the previous logs again (window is inclusive).
39
+ */
40
+ export function createFaultReporter(sink: FaultSink, log: (message: string) => void, now: () => number = Date.now): FaultReporter {
41
+ let lastLoggedAt = -FAULT_LOG_THROTTLE_MS
42
+ return {
43
+ fault(message) {
44
+ const at = now()
45
+ if (at - lastLoggedAt >= FAULT_LOG_THROTTLE_MS) {
46
+ log(`[nexus] ${message}`)
47
+ lastLoggedAt = at
48
+ }
49
+ sink.show(message)
50
+ },
51
+ clear() {
52
+ sink.hide()
53
+ lastLoggedAt = -FAULT_LOG_THROTTLE_MS
54
+ },
55
+ }
56
+ }