@nelsonlongxiang/dsh-open-a2a-net 0.5.14 → 0.6.24

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 (86) hide show
  1. package/README.md +243 -121
  2. package/README_zh.md +144 -84
  3. package/assets/nexusDist/assets/index-034h1cNc.js +3 -0
  4. package/assets/nexusDist/assets/index-PG9EieGX.css +1 -0
  5. package/assets/nexusDist/index.html +26 -0
  6. package/assets/stageDist/assets/index-Bj7S2pA1.css +1 -0
  7. package/assets/stageDist/assets/index-C-28k_o5.js +974 -0
  8. package/assets/stageDist/assets/interiors-CQGBjffT.png +0 -0
  9. package/assets/stageDist/assets/office-tileset-CHQT-uYL.png +0 -0
  10. package/assets/stageDist/index.html +13 -0
  11. package/lib/a2a-client.js +190 -13
  12. package/lib/canvas-store.js +218 -0
  13. package/lib/card.js +25 -0
  14. package/lib/client/A2aControl.js +45 -137
  15. package/lib/client/locales.js +16 -40
  16. package/lib/client.js +164 -467
  17. package/lib/client.js.map +1 -1
  18. package/lib/group-store.js +6 -0
  19. package/lib/idempotency-store.js +291 -0
  20. package/lib/index.js +4217 -259
  21. package/lib/joined-store.js +11 -0
  22. package/lib/layout-store.js +155 -0
  23. package/lib/peer-store.js +98 -13
  24. package/lib/receipt-ladder.js +35 -0
  25. package/lib/receipt.js +102 -0
  26. package/lib/self-suppress.js +62 -0
  27. package/lib/session-transcript.js +150 -0
  28. package/lib/skill/SKILL.md +77 -0
  29. package/lib/skill/references/discovery.md +38 -0
  30. package/lib/skill/references/membership.md +50 -0
  31. package/lib/skill/references/pitfalls.md +42 -0
  32. package/lib/skill/references/routing.md +43 -0
  33. package/lib/skill-registration.js +52 -0
  34. package/lib/stage-mount.js +32 -0
  35. package/lib/task-ledger.js +540 -0
  36. package/lib/team-store.js +122 -0
  37. package/lib/teams-bridge.js +18 -0
  38. package/lib/transport-caps.js +31 -0
  39. package/lib/types/a2a-client.d.ts +72 -1
  40. package/lib/types/canvas-store.d.ts +102 -0
  41. package/lib/types/client/A2aControl.d.ts +18 -28
  42. package/lib/types/client/locales.d.ts +1 -1
  43. package/lib/types/group-store.d.ts +6 -0
  44. package/lib/types/idempotency-store.d.ts +164 -0
  45. package/lib/types/index.d.ts +210 -6
  46. package/lib/types/joined-store.d.ts +9 -0
  47. package/lib/types/layout-store.d.ts +63 -0
  48. package/lib/types/peer-store.d.ts +45 -2
  49. package/lib/types/receipt-ladder.d.ts +25 -0
  50. package/lib/types/receipt.d.ts +51 -0
  51. package/lib/types/self-suppress.d.ts +41 -0
  52. package/lib/types/session-transcript.d.ts +37 -0
  53. package/lib/types/skill-registration.d.ts +3 -0
  54. package/lib/types/stage-mount.d.ts +29 -0
  55. package/lib/types/task-ledger.d.ts +308 -0
  56. package/lib/types/team-store.d.ts +55 -0
  57. package/lib/types/teams-bridge.d.ts +112 -0
  58. package/lib/types/transport-caps.d.ts +29 -0
  59. package/lib/types/types.d.ts +43 -0
  60. package/nexus-stage/package-lock.json +2262 -0
  61. package/nexus-stage/package.json +21 -0
  62. package/nexus-stage/scripts/__pycache__/mockhost.cpython-312.pyc +0 -0
  63. package/nexus-stage/scripts/mockhost.py +253 -0
  64. package/nexus-stage/src/canvas-ops.ts +175 -0
  65. package/nexus-stage/src/canvas-wire.ts +156 -0
  66. package/nexus-stage/src/edges.ts +72 -0
  67. package/nexus-stage/src/fault.ts +56 -0
  68. package/nexus-stage/src/federation.ts +234 -0
  69. package/nexus-stage/src/layout-doc.ts +143 -0
  70. package/nexus-stage/src/layout-wire.ts +158 -0
  71. package/nexus-stage/src/main.ts +361 -0
  72. package/nexus-stage/src/planning-menus.ts +352 -0
  73. package/nexus-stage/src/planning-nodelist.ts +294 -0
  74. package/nexus-stage/src/planning-types.ts +111 -0
  75. package/nexus-stage/src/planning-view.ts +1313 -0
  76. package/nexus-stage/src/planning.css +280 -0
  77. package/nexus-stage/src/seat.ts +35 -0
  78. package/nexus-stage/src/stage-name.ts +13 -0
  79. package/nexus-stage/src/tokens.ts +32 -0
  80. package/nexus-stage/src/viewport.ts +83 -0
  81. package/nexus-stage/src/workbench.css +267 -0
  82. package/nexus-stage/src/workbench.ts +1530 -0
  83. package/nexus-stage/src/world.ts +509 -0
  84. package/nexus-stage/tsconfig.json +25 -0
  85. package/nexus-stage/vite.config.ts +16 -0
  86. package/package.json +83 -22
@@ -0,0 +1,361 @@
1
+ /**
2
+ * The A2A workbench boot: fetches /__dsh_a2a/state on a poll, feeds the
3
+ * merged workbench (the 2D planning board as the stage + directory/ledger/
4
+ * activity/plan rail, 0.6.2) and keeps the fault discipline
5
+ * (a fetch failure must never masquerade as an empty fleet — the badge is
6
+ * the honesty surface).
7
+ * @module nexus-stage/main
8
+ */
9
+ import { createFaultReporter } from './fault'
10
+ import { createWorkbench, type StateBody } from './workbench'
11
+ import { createPlanningView } from './planning-view'
12
+ import { createSaveLoop, type LampState } from './layout-wire'
13
+ import { createCanvasWire } from './canvas-wire'
14
+ import type { CanvasAction } from './canvas-ops'
15
+ import './workbench.css'
16
+
17
+ const app = document.getElementById('app')!
18
+
19
+ // gh#9 §10: the host injects the control-plane key into the shell when
20
+ // apiKey is configured — stamp it on every /__dsh_a2a write so the GUI
21
+ // survives a keyed host (same-origin disclosure only, see the serve site).
22
+ const CONTROL_KEY = (window as unknown as { __DSH_A2A_KEY__?: string }).__DSH_A2A_KEY__ ?? ''
23
+ const controlFetch = (path: string, init: RequestInit = {}): Promise<Response> =>
24
+ fetch(path, { ...init, headers: { ...(init.headers as Record<string, string> | undefined ?? {}), ...(CONTROL_KEY !== '' ? { 'X-API-Key': CONTROL_KEY } : {}) } })
25
+
26
+ const faultBadge = document.createElement('div')
27
+ faultBadge.className = 'nexus-fault'
28
+ app.appendChild(faultBadge)
29
+ const { fault, clear: clearFault } = createFaultReporter(
30
+ {
31
+ show(message) {
32
+ faultBadge.textContent = message
33
+ faultBadge.style.display = 'block'
34
+ },
35
+ hide() {
36
+ faultBadge.style.display = 'none'
37
+ },
38
+ },
39
+ message => console.error(message),
40
+ )
41
+
42
+ // ─── Spatial planning board (restored 0.6.1; wiring from the pre-0.6.0 stage) ───
43
+ // 0.6.13: board node picks forward to the workbench chat drawer. The bench
44
+ // is created later (it embeds the board), so the dep is a late-bound slot.
45
+ let openNodeChat: ((id: string) => void) | undefined
46
+ // Per-device viewport memory: browser-local, one slot per screen bucket.
47
+ // Storage can be absent or throw (private windows, blocked site data) —
48
+ // every access is guarded and the board falls back to the host/fit path.
49
+ const VIEWPORT_KEY = 'dsh-a2a.viewport.'
50
+ const viewportStore = {
51
+ get(bucket: string): unknown {
52
+ try {
53
+ const raw = window.localStorage.getItem(VIEWPORT_KEY + bucket)
54
+ return raw === null ? undefined : JSON.parse(raw)
55
+ } catch {
56
+ return undefined
57
+ }
58
+ },
59
+ set(bucket: string, vp: { x: number; y: number; scale: number }): void {
60
+ try {
61
+ window.localStorage.setItem(VIEWPORT_KEY + bucket, JSON.stringify({ x: vp.x, y: vp.y, scale: vp.scale }))
62
+ } catch { /* presentation memory only */ }
63
+ },
64
+ }
65
+
66
+ const planning = createPlanningView({
67
+ viewportStore,
68
+ onDirty: () => saveLoop.markDirty(),
69
+ onLampClick: () => saveLoop.retry(),
70
+ onNodePick: (id) => { openNodeChat?.(id) },
71
+ onCanvasAction: async (a) => {
72
+ const ok = await runCanvasAction(a)
73
+ // Settled: converge on truth either way — success absorbs the host's
74
+ // rich state; failure re-pulls so the rollback matches reality.
75
+ void poll()
76
+ return ok
77
+ },
78
+ })
79
+
80
+ const saveLoop = createSaveLoop({
81
+ snapshot: () => planning.snapshotDoc(),
82
+ send: (doc, opts) => controlFetch('/__dsh_a2a/canvas-layout', {
83
+ method: 'POST',
84
+ headers: { 'Content-Type': 'application/json' },
85
+ body: JSON.stringify({ action: 'save', layout: doc }),
86
+ cache: 'no-store',
87
+ keepalive: opts.keepalive,
88
+ }).then(async (r) => await r.json() as { ok: boolean; layout?: unknown; error?: string }),
89
+ schedule: (fn, ms) => { const t = setTimeout(fn, ms); return () => { clearTimeout(t) } },
90
+ now: Date.now,
91
+ onLamp: (state) => {
92
+ lampState = state
93
+ const now = new Date()
94
+ const hhmm = String(now.getHours()).padStart(2, '0') + ':' + String(now.getMinutes()).padStart(2, '0')
95
+ planning.setLamp(state, hhmm)
96
+ },
97
+ onAdopt: (normalized) => { planning.adoptExternalLayout(normalized) },
98
+ onHttpError: (message) => fault(message),
99
+ })
100
+ window.addEventListener('pagehide', () => saveLoop.flush())
101
+
102
+ const canvasWire = createCanvasWire({
103
+ send: async (body) => {
104
+ const r = await controlFetch('/__dsh_a2a/canvas', {
105
+ method: 'POST',
106
+ headers: { 'Content-Type': 'application/json' },
107
+ body: JSON.stringify(body),
108
+ cache: 'no-store',
109
+ })
110
+ return { status: r.status, body: await r.json() as { ok?: boolean; error?: string } }
111
+ },
112
+ onNotice: (kind, text) => planning.notice(kind, text),
113
+ })
114
+
115
+ function runCanvasAction(a: CanvasAction): Promise<boolean> {
116
+ switch (a.type) {
117
+ case 'create-team': return canvasWire.createTeam(a.name, a.ids, a.created)
118
+ case 'add-member': return canvasWire.addMembers(a.team, a.ids)
119
+ case 'remove-member': return canvasWire.removeMembers(a.team, a.ids)
120
+ case 'remove-team': return canvasWire.removeTeam(a.name)
121
+ case 'reorder': return canvasWire.runRosterOps(a.team, a.ops)
122
+ case 'join-network':
123
+ case 'leave-network': return postNetworkJoin(a.type === 'join-network' ? 'join' : 'leave', a.id)
124
+ }
125
+ }
126
+
127
+ /** Network join/leave for one session (the panel-slim migration: this
128
+ * affordance lives on the planning view's node list). */
129
+ async function postNetworkJoin(action: 'join' | 'leave', id: string): Promise<boolean> {
130
+ const r = await controlFetch(`/__dsh_a2a/${action}`, {
131
+ method: 'POST',
132
+ headers: { 'Content-Type': 'application/json' },
133
+ body: JSON.stringify({ id }),
134
+ cache: 'no-store',
135
+ }).catch(() => undefined)
136
+ return r !== undefined && r.ok
137
+ }
138
+
139
+ let lampState: LampState = 'idle'
140
+ let canvasFace = false
141
+ let lastLayoutJson: string | undefined
142
+ /** True once any layout answer (saved doc or an explicit null) has been
143
+ * seen; the board build waits for that answer on the very first poll. */
144
+ let layoutSeen = false
145
+
146
+ /** Fetch the persisted layout; throws on transport failure so the poll can
147
+ * hold the board build instead of silently building on ring seats. */
148
+ async function fetchLayout(): Promise<unknown> {
149
+ const res = await controlFetch('/__dsh_a2a/canvas-layout', { cache: 'no-store' })
150
+ if (!res.ok) throw new Error(`/__dsh_a2a/canvas-layout ${res.status}`)
151
+ return ((await res.json()) as { layout?: unknown }).layout ?? null
152
+ }
153
+
154
+ // ─── The workbench ───
155
+ const bench = createWorkbench({
156
+ now: () => Date.now(),
157
+ async abandon(taskId, reason) {
158
+ try {
159
+ const res = await controlFetch('/__dsh_a2a/tasks/abandon', {
160
+ method: 'POST',
161
+ headers: { 'content-type': 'application/json' },
162
+ body: JSON.stringify({ task_id: taskId, reason }),
163
+ })
164
+ return res.ok
165
+ } catch {
166
+ return false
167
+ }
168
+ },
169
+ copy(text) {
170
+ void navigator.clipboard?.writeText(text).catch(() => {})
171
+ },
172
+ // 0.6.10: the ↗ jump affordances (0.6.7 L1 broadcast / 0.6.8 L2 deep
173
+ // link) are REMOVED by ruling — 禁止任何跳转,全部使用响应式布局.
174
+ // The only reach primitive is the inline 快派 form: it works for local
175
+ // session teams, canvas teams and remote rows alike, and never leaves
176
+ // this page.
177
+ async canvasOp(action, name, id) {
178
+ // Same control route the planning board's wire uses; the inspector's
179
+ // member writes are one id at a time, so no serial fan-out needed.
180
+ try {
181
+ const res = await controlFetch('/__dsh_a2a/canvas', {
182
+ method: 'POST',
183
+ headers: { 'content-type': 'application/json' },
184
+ body: JSON.stringify({ action, name, id }),
185
+ })
186
+ const body = (await res.json().catch(() => ({}))) as { ok?: boolean; error?: string }
187
+ if (res.ok && body.ok !== false) return true
188
+ console.warn(`canvas ${action} ${name}: ${body.error ?? `HTTP ${res.status}`}`)
189
+ return false
190
+ } catch {
191
+ return false
192
+ }
193
+ },
194
+ async dispatchQuick(team, message) {
195
+ // Quick dispatch (0.6.9): host-face POST; the trunk answers with the
196
+ // canonical route shape ({ok, task_id, task_status, team, ...} on
197
+ // delivery) or {ok:false, error}.
198
+ try {
199
+ const res = await controlFetch('/__dsh_a2a/dispatch', {
200
+ method: 'POST',
201
+ headers: { 'content-type': 'application/json' },
202
+ body: JSON.stringify({ team, message }),
203
+ })
204
+ const body = (await res.json().catch(() => ({}))) as { ok?: boolean; task_id?: string; reply?: string; error?: string }
205
+ return {
206
+ ok: res.ok && body.ok === true,
207
+ task_id: body.task_id,
208
+ reply: body.reply,
209
+ error: body.error ?? `HTTP ${res.status}`,
210
+ }
211
+ } catch (error) {
212
+ return { ok: false, error: error instanceof Error ? error.message : String(error) }
213
+ }
214
+ },
215
+ async sessionLog(id) {
216
+ // Inline transcript (0.6.11): read-only tail of a local session's own
217
+ // log from the host control face. Caps live server-side.
218
+ try {
219
+ const res = await controlFetch(`/__dsh_a2a/session-log?id=${encodeURIComponent(id)}`)
220
+ const body = (await res.json().catch(() => ({}))) as { ok?: boolean; messages?: Array<{ role: string; text: string }>; error?: string }
221
+ // 0.6.16: error stays undefined on success — an unconditional
222
+ // `body.error ?? HTTP ${status}` painted "HTTP 200" on listing-shaped
223
+ // successes that lacked a messages field.
224
+ return { ok: res.ok && body.ok === true, messages: body.messages, error: body.error ?? (res.ok ? undefined : `HTTP ${res.status}`) }
225
+ } catch (error) {
226
+ return { ok: false, error: error instanceof Error ? error.message : String(error) }
227
+ }
228
+ },
229
+ async sessionLogList() {
230
+ // 0.6.12: host-scope readable-log listing — archived and never-joined
231
+ // sessions included; the network directory is not the reading gate.
232
+ try {
233
+ const res = await controlFetch('/__dsh_a2a/session-log')
234
+ const body = (await res.json().catch(() => ({}))) as { ok?: boolean; sessions?: Array<{ id: string; mtimeMs: number; sizeBytes: number }>; error?: string }
235
+ return { ok: res.ok && body.ok === true, sessions: body.sessions, error: body.error ?? (res.ok ? undefined : `HTTP ${res.status}`) }
236
+ } catch (error) {
237
+ return { ok: false, error: error instanceof Error ? error.message : String(error) }
238
+ }
239
+ },
240
+ async remoteSessionLog(query) {
241
+ // 0.6.15: proxy read of a remote host's /a2a/log — by tracked peer url or
242
+ // by team handle (directory-resolved). The proxy is the only reader; the
243
+ // browser never dials the peer directly.
244
+ try {
245
+ const params = new URLSearchParams()
246
+ if (query.team !== undefined) params.set('team', query.team)
247
+ if (query.peer !== undefined) params.set('peer', query.peer)
248
+ if (query.id !== undefined) params.set('id', query.id)
249
+ if (query.limit !== undefined) params.set('limit', String(query.limit))
250
+ const res = await controlFetch(`/__dsh_a2a/remote-session-log?${params.toString()}`)
251
+ const body = (await res.json().catch(() => ({}))) as {
252
+ ok?: boolean
253
+ messages?: Array<{ role: string; text: string }>
254
+ sessions?: Array<{ id: string; mtimeMs: number; sizeBytes: number }>
255
+ error?: string
256
+ via?: string
257
+ }
258
+ return { ok: res.ok && body.ok === true, messages: body.messages, sessions: body.sessions, error: body.error ?? (res.ok ? undefined : `HTTP ${res.status}`), via: body.via }
259
+ } catch (error) {
260
+ return { ok: false, error: error instanceof Error ? error.message : String(error) }
261
+ }
262
+ },
263
+ board: {
264
+ root: planning.root,
265
+ activate: () => planning.activate(),
266
+ deactivate: () => planning.deactivate(),
267
+ available: () => canvasFace,
268
+ },
269
+ })
270
+ app.appendChild(bench.root)
271
+ // 0.6.13: board node picks now resolve against the live bench state.
272
+ openNodeChat = (id) => { bench.openNodeChat(id) }
273
+
274
+ // Legacy entry compatibility: the old stage link ?mode=plan converges on the
275
+ // board stage — in the merged shell canvas mode is already the default while
276
+ // the host serves the canvas face, so this only guards the first poll.
277
+ if (new URLSearchParams(window.location.search).get('mode') === 'plan') {
278
+ const tryBoard = window.setInterval(() => {
279
+ if (canvasFace) {
280
+ bench.selectTab('board')
281
+ window.clearInterval(tryBoard)
282
+ }
283
+ }, 300)
284
+ window.setTimeout(() => window.clearInterval(tryBoard), 30_000)
285
+ }
286
+
287
+ async function poll(): Promise<void> {
288
+ try {
289
+ const res = await controlFetch('/__dsh_a2a/state', { cache: 'no-store' })
290
+ if (!res.ok) throw new Error(`/__dsh_a2a/state ${res.status}`)
291
+ const body = (await res.json()) as StateBody
292
+ clearFault()
293
+ // canvasFace first: bench.update reads board.available() to pick the
294
+ // mode, so assigning it afterwards held the very first paint in data
295
+ // mode (board display:none, 0px wide) until the next 3s poll — and the
296
+ // board's first reconcile measured a zero-width view.
297
+ canvasFace = body.canvas !== undefined
298
+ bench.update(body)
299
+
300
+ // ── The layout answer precedes the first reconcile on purpose ──
301
+ // The board's first build reads `layoutDoc` for saved positions and
302
+ // frame rects; every later pass keeps live geometry ("live rect wins").
303
+ // A layout landing AFTER the build therefore never displaces the ring
304
+ // seats — the refresh-reset-to-middle bug. Until any answer (doc or
305
+ // null) has been seen, a failed GET holds the board one poll rather
306
+ // than building on seats; after that, failures leave the last doc in
307
+ // place and the poll continues.
308
+ if (canvasFace) {
309
+ try {
310
+ const layout = await fetchLayout()
311
+ const layoutJson = JSON.stringify(layout ?? null)
312
+ if (layoutJson !== lastLayoutJson) {
313
+ lastLayoutJson = layoutJson
314
+ // Adopt poll-sourced layouts only while nothing is unsaved/in
315
+ // flight — otherwise a slow GET would revert positions the user
316
+ // is still dragging.
317
+ if ((lampState === 'idle' || lampState === 'saved') && !saveLoop.isBusy()) {
318
+ planning.adoptExternalLayout(layout)
319
+ }
320
+ }
321
+ layoutSeen = true
322
+ } catch (err) {
323
+ if (!layoutSeen) {
324
+ fault(`canvas layout 获取失败:${String(err)}`)
325
+ return
326
+ }
327
+ }
328
+ } else {
329
+ layoutSeen = true
330
+ }
331
+
332
+ // ── Planning board shares this poll: same state payload. ──
333
+ // SessionLite needs `joined: boolean` (workbench rows carry `joined?`):
334
+ // tri-state → boolean without losing the unjoined node-list population.
335
+ const allSessions = (body.sessions ?? []).map(s => ({ ...s, joined: s.joined === true }))
336
+ const teams = body.canvas?.teams ?? []
337
+ const peers = (body.peers ?? []).filter(p => p !== null && typeof p === 'object' && typeof (p as { url?: unknown }).url === 'string')
338
+ const inFlight = (body.inFlight ?? []).filter(r => r !== null && typeof r === 'object' && typeof (r as { team?: unknown }).team === 'string' && typeof (r as { peer?: unknown }).peer === 'string' && Number.isFinite((r as { startedAt?: unknown }).startedAt))
339
+ const remoteTeams = (body.remote ?? [])
340
+ .filter(r => r !== null && typeof r === 'object' && typeof (r as { team?: unknown }).team === 'string' && typeof (r as { via?: unknown }).via === 'string')
341
+ .map(r => ({
342
+ team: r.team,
343
+ name: r.name,
344
+ via: r.via,
345
+ workspace: r.workspace,
346
+ ...(typeof r.origin === 'string' && r.origin !== '' ? { origin: r.origin } : {}),
347
+ ...(Array.isArray(r.teams) && r.teams.length > 0 ? { teams: r.teams.filter((t): t is string => typeof t === 'string' && t !== '') } : {}),
348
+ }))
349
+ planning.reconcile({ sessions: allSessions, teams, peerCount: peers.length, peers, inFlight, remoteTeams })
350
+ } catch (err) {
351
+ fault(`state 轮询失败:${String(err)}`)
352
+ }
353
+ }
354
+
355
+ void poll()
356
+ window.setInterval(() => {
357
+ if (!document.hidden) void poll()
358
+ }, 3000)
359
+ document.addEventListener('visibilitychange', () => {
360
+ if (!document.hidden) void poll()
361
+ })