@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.
- package/README.md +243 -121
- package/README_zh.md +144 -84
- package/assets/nexusDist/assets/index-034h1cNc.js +3 -0
- package/assets/nexusDist/assets/index-PG9EieGX.css +1 -0
- package/assets/nexusDist/index.html +26 -0
- package/assets/stageDist/assets/index-Bj7S2pA1.css +1 -0
- package/assets/stageDist/assets/index-C-28k_o5.js +974 -0
- package/assets/stageDist/assets/interiors-CQGBjffT.png +0 -0
- package/assets/stageDist/assets/office-tileset-CHQT-uYL.png +0 -0
- package/assets/stageDist/index.html +13 -0
- package/lib/a2a-client.js +190 -13
- package/lib/canvas-store.js +218 -0
- package/lib/card.js +25 -0
- package/lib/client/A2aControl.js +45 -137
- package/lib/client/locales.js +16 -40
- package/lib/client.js +164 -467
- package/lib/client.js.map +1 -1
- package/lib/group-store.js +6 -0
- package/lib/idempotency-store.js +291 -0
- package/lib/index.js +4217 -259
- package/lib/joined-store.js +11 -0
- package/lib/layout-store.js +155 -0
- package/lib/peer-store.js +98 -13
- package/lib/receipt-ladder.js +35 -0
- package/lib/receipt.js +102 -0
- package/lib/self-suppress.js +62 -0
- package/lib/session-transcript.js +150 -0
- package/lib/skill/SKILL.md +77 -0
- package/lib/skill/references/discovery.md +38 -0
- package/lib/skill/references/membership.md +50 -0
- package/lib/skill/references/pitfalls.md +42 -0
- package/lib/skill/references/routing.md +43 -0
- package/lib/skill-registration.js +52 -0
- package/lib/stage-mount.js +32 -0
- package/lib/task-ledger.js +540 -0
- package/lib/team-store.js +122 -0
- package/lib/teams-bridge.js +18 -0
- package/lib/transport-caps.js +31 -0
- package/lib/types/a2a-client.d.ts +72 -1
- package/lib/types/canvas-store.d.ts +102 -0
- package/lib/types/client/A2aControl.d.ts +18 -28
- package/lib/types/client/locales.d.ts +1 -1
- package/lib/types/group-store.d.ts +6 -0
- package/lib/types/idempotency-store.d.ts +164 -0
- package/lib/types/index.d.ts +210 -6
- package/lib/types/joined-store.d.ts +9 -0
- package/lib/types/layout-store.d.ts +63 -0
- package/lib/types/peer-store.d.ts +45 -2
- package/lib/types/receipt-ladder.d.ts +25 -0
- package/lib/types/receipt.d.ts +51 -0
- package/lib/types/self-suppress.d.ts +41 -0
- package/lib/types/session-transcript.d.ts +37 -0
- package/lib/types/skill-registration.d.ts +3 -0
- package/lib/types/stage-mount.d.ts +29 -0
- package/lib/types/task-ledger.d.ts +308 -0
- package/lib/types/team-store.d.ts +55 -0
- package/lib/types/teams-bridge.d.ts +112 -0
- package/lib/types/transport-caps.d.ts +29 -0
- package/lib/types/types.d.ts +43 -0
- package/nexus-stage/package-lock.json +2262 -0
- package/nexus-stage/package.json +21 -0
- package/nexus-stage/scripts/__pycache__/mockhost.cpython-312.pyc +0 -0
- package/nexus-stage/scripts/mockhost.py +253 -0
- package/nexus-stage/src/canvas-ops.ts +175 -0
- package/nexus-stage/src/canvas-wire.ts +156 -0
- package/nexus-stage/src/edges.ts +72 -0
- package/nexus-stage/src/fault.ts +56 -0
- package/nexus-stage/src/federation.ts +234 -0
- package/nexus-stage/src/layout-doc.ts +143 -0
- package/nexus-stage/src/layout-wire.ts +158 -0
- package/nexus-stage/src/main.ts +361 -0
- package/nexus-stage/src/planning-menus.ts +352 -0
- package/nexus-stage/src/planning-nodelist.ts +294 -0
- package/nexus-stage/src/planning-types.ts +111 -0
- package/nexus-stage/src/planning-view.ts +1313 -0
- package/nexus-stage/src/planning.css +280 -0
- package/nexus-stage/src/seat.ts +35 -0
- package/nexus-stage/src/stage-name.ts +13 -0
- package/nexus-stage/src/tokens.ts +32 -0
- package/nexus-stage/src/viewport.ts +83 -0
- package/nexus-stage/src/workbench.css +267 -0
- package/nexus-stage/src/workbench.ts +1530 -0
- package/nexus-stage/src/world.ts +509 -0
- package/nexus-stage/tsconfig.json +25 -0
- package/nexus-stage/vite.config.ts +16 -0
- 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
|
+
})
|