@nelsonlongxiang/dsh-open-a2a-net 0.6.33 → 0.6.35
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 +5 -2
- package/README_zh.md +5 -2
- package/assets/nexusDist/assets/index-Du2E6DJL.js +9 -0
- package/assets/nexusDist/assets/index-z33lHtLw.css +1 -0
- package/assets/nexusDist/index.html +2 -2
- package/cli/a2a.mjs +3 -1
- package/lib/a2a-client.js +9 -2
- package/lib/auth-observer.js +1 -1
- package/lib/canvas-store.js +64 -6
- package/lib/card.js +2 -1
- package/lib/cli-home.js +44 -0
- package/lib/client/A2aControl.js +1 -1
- package/lib/client.js +65 -65
- package/lib/client.js.map +1 -1
- package/lib/direct-forward.js +61 -0
- package/lib/faces/admin.js +43 -6
- package/lib/faces/wire.js +57 -40
- package/lib/flood-guard.js +7 -1
- package/lib/index.js +117 -65
- package/lib/outbound-scope.js +25 -0
- package/lib/queue-guard.js +37 -0
- package/lib/response-fields.js +27 -0
- package/lib/skill/references/cli.md +11 -4
- package/lib/skill/references/pitfalls.md +5 -4
- package/lib/skill/references/routing.md +4 -0
- package/lib/task-auth.js +52 -3
- package/lib/task-ledger.js +4 -1
- package/lib/tools/info.js +19 -3
- package/lib/types/a2a-client.d.ts +1 -1
- package/lib/types/auth-observer.d.ts +2 -2
- package/lib/types/canvas-store.d.ts +15 -1
- package/lib/types/cli-home.d.ts +24 -0
- package/lib/types/client/A2aControl.d.ts +1 -1
- package/lib/types/direct-forward.d.ts +32 -0
- package/lib/types/faces/admin.d.ts +8 -1
- package/lib/types/faces/wire.d.ts +12 -4
- package/lib/types/flood-guard.d.ts +5 -0
- package/lib/types/index.d.ts +4 -0
- package/lib/types/outbound-scope.d.ts +21 -0
- package/lib/types/queue-guard.d.ts +32 -0
- package/lib/types/response-fields.d.ts +3 -0
- package/lib/types/runtime.d.ts +1 -1
- package/lib/types/task-auth.d.ts +17 -2
- package/lib/types/task-ledger.d.ts +6 -1
- package/lib/types/types.d.ts +8 -0
- package/nexus-stage/src/cli-home-picker.ts +42 -0
- package/nexus-stage/src/main.ts +9 -2
- package/nexus-stage/src/planning-card.ts +61 -0
- package/nexus-stage/src/planning-menus.ts +42 -7
- package/nexus-stage/src/planning-view.ts +14 -32
- package/nexus-stage/src/planning.css +17 -0
- package/nexus-stage/src/workbench.css +4 -0
- package/nexus-stage/src/workbench.ts +28 -5
- package/nexus-stage/src/world.ts +5 -1
- package/package.json +2 -1
- package/assets/nexusDist/assets/index-BACVZe0j.css +0 -1
- package/assets/nexusDist/assets/index-Bbxnpf2p.js +0 -9
|
@@ -63,7 +63,7 @@ export type ArchiveEnd = 'receipt' | 'echo' | 'abandoned' | 'cancelled' | 'evict
|
|
|
63
63
|
* same-host seats could not be told apart.
|
|
64
64
|
*/
|
|
65
65
|
export interface Sender {
|
|
66
|
-
readonly kind: 'session' | 'host' | 'bridge';
|
|
66
|
+
readonly kind: 'session' | 'host' | 'bridge' | 'cli';
|
|
67
67
|
/**
|
|
68
68
|
* A session's `<zone>/<id8>` node team, the host process label, or the
|
|
69
69
|
* bridge submitter. Group senders by `kind:id`, never by `id` alone: a host
|
|
@@ -89,6 +89,7 @@ export declare const SENDER_ID_CAP = 256;
|
|
|
89
89
|
export declare function parseSender(raw: unknown): Sender | undefined;
|
|
90
90
|
/** #139: what an outbound row is born with besides its sender. */
|
|
91
91
|
export interface OutboundBirth {
|
|
92
|
+
readonly forwarded?: true;
|
|
92
93
|
/** The local-candidate admission (ingress local); kept only on a row whose peer is 'local'. */
|
|
93
94
|
readonly auth?: TaskAuthorization;
|
|
94
95
|
readonly peerKey?: string;
|
|
@@ -124,6 +125,8 @@ export interface TaskRecord {
|
|
|
124
125
|
readonly brief?: string;
|
|
125
126
|
/** #114 S3: the real sender of an outbound row (absent on inbound rows and rows persisted before the field). */
|
|
126
127
|
readonly sender?: Sender;
|
|
128
|
+
/** Home host forwarded an online CLI request to a peer. */
|
|
129
|
+
readonly forwarded?: true;
|
|
127
130
|
/**
|
|
128
131
|
* #114 S2: the immutable authorization the row was born with — an inbound
|
|
129
132
|
* row's wire admission, or (#139) an outbound row's local-candidate
|
|
@@ -183,6 +186,8 @@ export interface ArchivedRecord {
|
|
|
183
186
|
readonly receiptExpected?: false;
|
|
184
187
|
/** #114 S3: copied from the owed row (absent on inbound rows and rows archived before the field). */
|
|
185
188
|
readonly sender?: Sender;
|
|
189
|
+
/** Home host forwarded an online CLI request to a peer. */
|
|
190
|
+
readonly forwarded?: true;
|
|
186
191
|
/** #114 S2: carried over from the live row unchanged. */
|
|
187
192
|
readonly auth?: TaskAuthorization;
|
|
188
193
|
/** #139: carried over from the live row unchanged (a late receipt is judged against them). */
|
package/lib/types/types.d.ts
CHANGED
|
@@ -76,6 +76,8 @@ export interface A2aSessionTeamInfo {
|
|
|
76
76
|
readonly name: string;
|
|
77
77
|
/** One-line excerpt of the session's newest user or assistant text. */
|
|
78
78
|
readonly description: string;
|
|
79
|
+
/** CLI deposit status, explicitly online/offline on full host cards; absent on ordinary sessions. */
|
|
80
|
+
readonly offline?: boolean;
|
|
79
81
|
/** The session's working directory, when the publishing node shares it. */
|
|
80
82
|
readonly workspace?: string;
|
|
81
83
|
}
|
|
@@ -105,6 +107,8 @@ export interface ZoneRecord {
|
|
|
105
107
|
}
|
|
106
108
|
/** Canonical `a2a_route` success value. */
|
|
107
109
|
export interface A2aRouteOk {
|
|
110
|
+
/** Actual member face returned by a forwarded peer delivery. */
|
|
111
|
+
readonly deliveredTo?: string;
|
|
108
112
|
readonly ok: true;
|
|
109
113
|
readonly team: string;
|
|
110
114
|
/** Reply text the remote team produced. */
|
|
@@ -129,6 +133,8 @@ export interface A2aRouteOk {
|
|
|
129
133
|
* receipt-owed (`trackOwedTask` skips it).
|
|
130
134
|
*/
|
|
131
135
|
readonly bridge?: 'native-teams';
|
|
136
|
+
/** The peer's explicit receipt contract, when provided. */
|
|
137
|
+
readonly receipt?: 'none' | 'routed' | 'mailbox';
|
|
132
138
|
/**
|
|
133
139
|
* gh#141: the peer refused the settlement the delivered message carried
|
|
134
140
|
* (its enforce verdict, `settlement: {status: 'refused'}` on the wire,
|
|
@@ -138,6 +144,8 @@ export interface A2aRouteOk {
|
|
|
138
144
|
}
|
|
139
145
|
/** Canonical `a2a_route` failure value. */
|
|
140
146
|
export interface A2aRouteError {
|
|
147
|
+
/** A forwarded receipt never promises another receipt, including uncertain waits. */
|
|
148
|
+
readonly receipt?: 'none';
|
|
141
149
|
readonly ok: false;
|
|
142
150
|
/** Peer or transport failure text. */
|
|
143
151
|
readonly error: string;
|
|
@@ -0,0 +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
|
+
}
|
package/nexus-stage/src/main.ts
CHANGED
|
@@ -292,14 +292,21 @@ const bench = createWorkbench({
|
|
|
292
292
|
// The only reach primitive is the inline 快派 form: it works for local
|
|
293
293
|
// session teams, canvas teams and remote rows alike, and never leaves
|
|
294
294
|
// this page.
|
|
295
|
-
async
|
|
295
|
+
async cliHomeOptions(name, id) {
|
|
296
|
+
try {
|
|
297
|
+
const res = await controlFetch('/__dsh_a2a/canvas', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ action: 'cli-home-options', name, id }) })
|
|
298
|
+
const body = await res.json() as { ok: boolean; homes?: string[]; unreadable?: number; held?: boolean; error?: string }
|
|
299
|
+
return { ...body, ok: res.ok && body.ok === true }
|
|
300
|
+
} catch (error) { return { ok: false, error: String(error) } }
|
|
301
|
+
},
|
|
302
|
+
async canvasOp(action, name, id, home) {
|
|
296
303
|
// Same control route the planning board's wire uses; the inspector's
|
|
297
304
|
// member writes are one id at a time, so no serial fan-out needed.
|
|
298
305
|
try {
|
|
299
306
|
const res = await controlFetch('/__dsh_a2a/canvas', {
|
|
300
307
|
method: 'POST',
|
|
301
308
|
headers: { 'content-type': 'application/json' },
|
|
302
|
-
body: JSON.stringify({ action, name, id }),
|
|
309
|
+
body: JSON.stringify({ action, name, id, ...(home === undefined ? {} : { home }) }),
|
|
303
310
|
})
|
|
304
311
|
const body = (await res.json().catch(() => ({}))) as { ok?: boolean; error?: string }
|
|
305
312
|
if (res.ok && body.ok !== false) return true
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Board node card: the DOM skeleton and the right-hand chip. Extracted from planning-view so the card's static
|
|
3
|
+
* structure reads in one place and the view stays under its M1 module-size cap (1507 lines; it reached 1529 with
|
|
4
|
+
* the 工作中/空闲 chip). Pure DOM helpers — no view state; renderNodes still decides what each card shows.
|
|
5
|
+
* @module nexus-stage/planning-card
|
|
6
|
+
*/
|
|
7
|
+
import type { PlanningSession } from './planning-types'
|
|
8
|
+
import type { WorldNode } from './world'
|
|
9
|
+
|
|
10
|
+
/** The card's 工作中/空闲 chip text; empty when the host reported no status (older host, cold or remote node). */
|
|
11
|
+
export function workLabel(work: Pick<PlanningSession, 'status' | 'queued'> | undefined): string {
|
|
12
|
+
if (work?.status === undefined) return ''
|
|
13
|
+
const base = work.status === 'running' ? '工作中' : '空闲'
|
|
14
|
+
return typeof work.queued === 'number' && work.queued > 0 ? `${base} · 排队 ${String(work.queued)}` : base
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** A fresh `.p-node` card: title row (dot + text), second row (description + 工作中/空闲 chip), and the right-hand chip. */
|
|
18
|
+
export function createCardElement(id: string): HTMLDivElement {
|
|
19
|
+
const el = document.createElement('div')
|
|
20
|
+
el.className = 'p-node'
|
|
21
|
+
el.dataset.id = id
|
|
22
|
+
el.tabIndex = 0
|
|
23
|
+
const nm = document.createElement('div')
|
|
24
|
+
nm.className = 'nm'
|
|
25
|
+
const dot = document.createElement('span')
|
|
26
|
+
dot.className = 'dot'
|
|
27
|
+
const name = document.createElement('span')
|
|
28
|
+
name.className = 'nm-text'
|
|
29
|
+
nm.append(dot, name)
|
|
30
|
+
const sub = document.createElement('div')
|
|
31
|
+
sub.className = 'sub mono'
|
|
32
|
+
const prio = document.createElement('span')
|
|
33
|
+
prio.className = 'prio mono'
|
|
34
|
+
// Second line: the description, with the 工作中/空闲 chip at its right end — the title keeps its full width.
|
|
35
|
+
const work = document.createElement('span')
|
|
36
|
+
work.className = 'work'
|
|
37
|
+
const subRow = document.createElement('div')
|
|
38
|
+
subRow.className = 'sub-row'
|
|
39
|
+
subRow.append(sub, work)
|
|
40
|
+
el.append(nm, subRow, prio)
|
|
41
|
+
return el
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* The right-hand chip. Peer cards show the link badge (gh#20 W-A: linked green / stranger gray, then the
|
|
46
|
+
* verified tick/cross; an absent link field from an older host renders the neutral 'peer'); session cards show
|
|
47
|
+
* their first membership priority `P<n>`. Writes only on change.
|
|
48
|
+
*/
|
|
49
|
+
export function paintPrio(prioEl: HTMLElement, n: WorldNode, info: { link?: 'linked' | 'stranger'; verified?: boolean } | undefined): void {
|
|
50
|
+
if (n.remote === true) {
|
|
51
|
+
const cls = info?.link === 'linked' ? 'lnk-linked' : info?.link === 'stranger' ? 'lnk-stranger' : ''
|
|
52
|
+
if (prioEl.className !== `prio mono ${cls}`.trim()) prioEl.className = `prio mono ${cls}`.trim()
|
|
53
|
+
const mark = info === undefined ? '' : info.verified === true ? '✓' : '✗'
|
|
54
|
+
const badge = (info?.link ?? 'peer') + mark
|
|
55
|
+
if (prioEl.textContent !== badge) prioEl.textContent = badge
|
|
56
|
+
} else {
|
|
57
|
+
if (prioEl.className !== 'prio mono') prioEl.className = 'prio mono'
|
|
58
|
+
const prioText = n.memberships[0] !== undefined ? `P${n.memberships[0]!.index}` : ''
|
|
59
|
+
if (prioEl.textContent !== prioText) prioEl.textContent = prioText
|
|
60
|
+
}
|
|
61
|
+
}
|
|
@@ -91,12 +91,46 @@ export function createPlanningMenus(d: MenusDeps): PlanningMenus {
|
|
|
91
91
|
title.id = 'p-dialog-title'
|
|
92
92
|
title.className = 'p-dialog-title'
|
|
93
93
|
title.textContent = `组建团队(${ids.length} 个节点,自上而下即优先级)`
|
|
94
|
-
|
|
95
|
-
kind
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
94
|
+
// Team type: a visible two-way toggle (a bare <select> read as a label and roster went unnoticed).
|
|
95
|
+
const kind = { value: 'canvas' as 'canvas' | 'roster' }
|
|
96
|
+
const kindGroup = document.createElement('div')
|
|
97
|
+
kindGroup.className = 'p-dialog-kind'
|
|
98
|
+
kindGroup.setAttribute('role', 'radiogroup')
|
|
99
|
+
kindGroup.setAttribute('aria-label', '团队类型')
|
|
100
|
+
const kindHint = document.createElement('div')
|
|
101
|
+
kindHint.className = 'p-dialog-hint'
|
|
102
|
+
const KIND_HINT = {
|
|
103
|
+
canvas: 'canvas 队:画布上的协作组,可以加入远端成员。',
|
|
104
|
+
roster: 'roster 网络队:只收本机已入网的会话;队名会写进属主的精确允许名单。',
|
|
105
|
+
} as const
|
|
106
|
+
const kindButtons = (['canvas', 'roster'] as const).map(value => {
|
|
107
|
+
const b = document.createElement('button')
|
|
108
|
+
b.type = 'button'
|
|
109
|
+
b.dataset.kind = value
|
|
110
|
+
b.setAttribute('role', 'radio')
|
|
111
|
+
b.textContent = value === 'canvas' ? 'canvas 队' : 'roster 网络队'
|
|
112
|
+
b.addEventListener('click', () => setKind(value))
|
|
113
|
+
kindGroup.appendChild(b)
|
|
114
|
+
return b
|
|
115
|
+
})
|
|
116
|
+
function setKind(value: 'canvas' | 'roster'): void {
|
|
117
|
+
kind.value = value
|
|
118
|
+
for (const b of kindButtons) {
|
|
119
|
+
const on = b.dataset.kind === value
|
|
120
|
+
b.setAttribute('aria-checked', String(on))
|
|
121
|
+
b.tabIndex = on ? 0 : -1
|
|
122
|
+
}
|
|
123
|
+
kindHint.textContent = KIND_HINT[value]
|
|
124
|
+
err.textContent = ''
|
|
99
125
|
}
|
|
126
|
+
// Arrow keys move between the two options (radiogroup pattern).
|
|
127
|
+
kindGroup.addEventListener('keydown', (ev) => {
|
|
128
|
+
const e = ev as KeyboardEvent
|
|
129
|
+
if (!['ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown'].includes(e.key)) return
|
|
130
|
+
e.preventDefault()
|
|
131
|
+
setKind(kind.value === 'canvas' ? 'roster' : 'canvas')
|
|
132
|
+
kindButtons.find(b => b.dataset.kind === kind.value)?.focus()
|
|
133
|
+
})
|
|
100
134
|
const input = document.createElement('input')
|
|
101
135
|
input.className = 'p-dialog-input'
|
|
102
136
|
input.maxLength = 40
|
|
@@ -113,7 +147,8 @@ export function createPlanningMenus(d: MenusDeps): PlanningMenus {
|
|
|
113
147
|
cancelBtn.type = 'button'
|
|
114
148
|
cancelBtn.textContent = '取消'
|
|
115
149
|
row.append(okBtn, cancelBtn)
|
|
116
|
-
|
|
150
|
+
setKind('canvas')
|
|
151
|
+
panel.append(title, kindGroup, kindHint, input, err, row)
|
|
117
152
|
wrap.appendChild(panel)
|
|
118
153
|
root.appendChild(wrap)
|
|
119
154
|
|
|
@@ -161,7 +196,7 @@ export function createPlanningMenus(d: MenusDeps): PlanningMenus {
|
|
|
161
196
|
if (e.key === 'Escape') { e.preventDefault(); e.stopPropagation(); close(); return }
|
|
162
197
|
if (e.key !== 'Tab') return
|
|
163
198
|
e.preventDefault()
|
|
164
|
-
const focusables = Array.from(panel.querySelectorAll<HTMLElement>('input, select, button'))
|
|
199
|
+
const focusables = Array.from(panel.querySelectorAll<HTMLElement>('input, select, button:not([tabindex="-1"])'))
|
|
165
200
|
const idx = focusables.indexOf(document.activeElement as HTMLElement)
|
|
166
201
|
const next = e.shiftKey
|
|
167
202
|
? focusables[(idx - 1 + focusables.length) % focusables.length]
|
|
@@ -51,6 +51,7 @@ import { settleBoard, tidyMembers } from './planning-overlap'
|
|
|
51
51
|
import { createLayoutUndo, type LayoutSnap } from './planning-undo'
|
|
52
52
|
import { createNodeList } from './planning-nodelist'
|
|
53
53
|
import { cardText } from './planning-card-text'
|
|
54
|
+
import { createCardElement, paintPrio, workLabel } from './planning-card'
|
|
54
55
|
import { createLegend } from './planning-legend'
|
|
55
56
|
import { UNREACHABLE_HINT, showFrameReachability, unreachableRefs } from './planning-reachability'
|
|
56
57
|
import { viewportBucket, type PlanningDeps, type PlanningInput, type PlanningSession, type PlanningTeam, type PlanningView, type SeamKey, type SeamPointer, type ViewportBucket } from './planning-types'
|
|
@@ -68,6 +69,9 @@ type Gesture =
|
|
|
68
69
|
| { readonly kind: 'pan'; last: { x: number; y: number }; moved: boolean }
|
|
69
70
|
| { readonly kind: 'marquee'; origin: { x: number; y: number }; last: { x: number; y: number }; additive: boolean }
|
|
70
71
|
|
|
72
|
+
// The card skeleton, its 工作中/空闲 chip and the right-hand chip live in planning-card (module-size cap).
|
|
73
|
+
export { workLabel } from './planning-card'
|
|
74
|
+
|
|
71
75
|
export function createPlanningView(deps: PlanningDeps): PlanningView {
|
|
72
76
|
const model = new WorldModel()
|
|
73
77
|
let layoutDoc: LayoutDoc | undefined
|
|
@@ -398,22 +402,7 @@ export function createPlanningView(deps: PlanningDeps): PlanningView {
|
|
|
398
402
|
seen.add(n.id)
|
|
399
403
|
let el = nodeEls.get(n.id)
|
|
400
404
|
if (el === undefined) {
|
|
401
|
-
el =
|
|
402
|
-
el.className = 'p-node'
|
|
403
|
-
el.dataset.id = n.id
|
|
404
|
-
el.tabIndex = 0
|
|
405
|
-
const nm = document.createElement('div')
|
|
406
|
-
nm.className = 'nm'
|
|
407
|
-
const dot = document.createElement('span')
|
|
408
|
-
dot.className = 'dot'
|
|
409
|
-
const name = document.createElement('span')
|
|
410
|
-
name.className = 'nm-text'
|
|
411
|
-
nm.append(dot, name)
|
|
412
|
-
const sub = document.createElement('div')
|
|
413
|
-
sub.className = 'sub mono'
|
|
414
|
-
const prio = document.createElement('span')
|
|
415
|
-
prio.className = 'prio mono'
|
|
416
|
-
el.append(nm, sub, prio)
|
|
405
|
+
el = createCardElement(n.id)
|
|
417
406
|
nodeLayer.appendChild(el)
|
|
418
407
|
nodeEls.set(n.id, el)
|
|
419
408
|
}
|
|
@@ -422,6 +411,12 @@ export function createPlanningView(deps: PlanningDeps): PlanningView {
|
|
|
422
411
|
el.style.top = `${r.y}px`
|
|
423
412
|
el.classList.toggle('cold', !n.live)
|
|
424
413
|
el.classList.toggle('remote', n.remote === true)
|
|
414
|
+
// 工作中/空闲: live local sessions only; the host reports nothing for cold or remote nodes.
|
|
415
|
+
const work = n.live && n.remote !== true ? lastWork.get(n.id) : undefined
|
|
416
|
+
el.classList.toggle('is-working', work?.status === 'running')
|
|
417
|
+
const workText = workLabel(work)
|
|
418
|
+
const workEl = el.querySelector<HTMLElement>('.work')!
|
|
419
|
+
if (workEl.textContent !== workText) workEl.textContent = workText
|
|
425
420
|
el.classList.toggle('selected', model.isSelected(n.id))
|
|
426
421
|
el.setAttribute('aria-selected', String(model.isSelected(n.id)))
|
|
427
422
|
const host = n.id.slice('peer-'.length)
|
|
@@ -436,22 +431,7 @@ export function createPlanningView(deps: PlanningDeps): PlanningView {
|
|
|
436
431
|
if (subEl.textContent !== subText) subEl.textContent = subText
|
|
437
432
|
const tip = unavailable ? `${txt.tip} · ${UNREACHABLE_HINT}` : txt.tip
|
|
438
433
|
if (el.title !== tip) el.title = tip
|
|
439
|
-
|
|
440
|
-
if (n.remote === true) {
|
|
441
|
-
// gh#20 W-A: the peer badge — link class colors the chip (linked
|
|
442
|
-
// green / stranger gray), the verified tick/cross rides after it.
|
|
443
|
-
// An absent link field (older host) renders the neutral 'peer'.
|
|
444
|
-
const info = peerInfoByHost.get(host)
|
|
445
|
-
const cls = info?.link === 'linked' ? 'lnk-linked' : info?.link === 'stranger' ? 'lnk-stranger' : ''
|
|
446
|
-
if (prioEl.className !== `prio mono ${cls}`.trim()) prioEl.className = `prio mono ${cls}`.trim()
|
|
447
|
-
const mark = info === undefined ? '' : info.verified === true ? '✓' : '✗'
|
|
448
|
-
const badge = (info?.link ?? 'peer') + mark
|
|
449
|
-
if (prioEl.textContent !== badge) prioEl.textContent = badge
|
|
450
|
-
} else {
|
|
451
|
-
if (prioEl.className !== 'prio mono') prioEl.className = 'prio mono'
|
|
452
|
-
const prioText = n.memberships[0] !== undefined ? `P${n.memberships[0]!.index}` : ''
|
|
453
|
-
if (prioEl.textContent !== prioText) prioEl.textContent = prioText
|
|
454
|
-
}
|
|
434
|
+
paintPrio(el.querySelector<HTMLElement>('.prio')!, n, n.remote === true ? peerInfoByHost.get(host) : undefined)
|
|
455
435
|
}
|
|
456
436
|
for (const [id, el] of [...nodeEls]) {
|
|
457
437
|
if (!seen.has(id)) { el.remove(); nodeEls.delete(id) }
|
|
@@ -470,6 +450,7 @@ export function createPlanningView(deps: PlanningDeps): PlanningView {
|
|
|
470
450
|
const federation = createFederationOverlay(svg, model)
|
|
471
451
|
let lastPeers: ReadonlyArray<PeerRow> = []
|
|
472
452
|
let lastRemoteTeams: ReadonlyMap<string, RemoteTeamRow[]> = new Map()
|
|
453
|
+
let lastWork: ReadonlyMap<string, Pick<PlanningSession, 'status' | 'queued'>> = new Map()
|
|
473
454
|
const peerScoreByText = new Map<string, number | undefined>()
|
|
474
455
|
/** gh#20 W-A: per-host link/verified info for the peer badges. */
|
|
475
456
|
const peerInfoByHost = new Map<string, { link?: 'linked' | 'stranger'; verified?: boolean }>()
|
|
@@ -1221,6 +1202,7 @@ export function createPlanningView(deps: PlanningDeps): PlanningView {
|
|
|
1221
1202
|
// excluded from bounds, so the column cannot feed back into itself).
|
|
1222
1203
|
const peerSeen = new Set<string>()
|
|
1223
1204
|
lastRemoteTeams = groupRemoteTeams(input.remoteTeams ?? [])
|
|
1205
|
+
lastWork = new Map(input.sessions.map(s => [s.id, { status: s.status, queued: s.queued }]))
|
|
1224
1206
|
reachability = unreachableRefs(input)
|
|
1225
1207
|
peerScoreByText.clear()
|
|
1226
1208
|
for (const p of input.peers ?? []) {
|
|
@@ -116,6 +116,14 @@
|
|
|
116
116
|
#dsh-plan .p-node.remote .nm-text{overflow-wrap:anywhere}
|
|
117
117
|
#dsh-plan .p-node .dot{width:8px; height:8px; border-radius:50%; background:var(--edge-live); flex:none}
|
|
118
118
|
#dsh-plan .p-node.cold .dot{background:transparent; border:1.5px solid var(--edge-cold)}
|
|
119
|
+
#dsh-plan .p-node .sub-row{display:flex; align-items:baseline; gap:6px; margin-top:2px; min-width:0}
|
|
120
|
+
#dsh-plan .p-node .sub-row .sub{flex:1 1 auto; min-width:0; margin-top:0}
|
|
121
|
+
#dsh-plan .p-node .work{flex:none; font-size:10.5px; font-weight:500; color:var(--ink-muted); white-space:nowrap}
|
|
122
|
+
#dsh-plan .p-node .work:empty{display:none}
|
|
123
|
+
#dsh-plan .p-node.is-working .work{color:var(--edge-live)}
|
|
124
|
+
#dsh-plan .p-node.is-working .dot{animation:p-working 1.4s ease-in-out infinite}
|
|
125
|
+
@keyframes p-working{0%,100%{box-shadow:0 0 0 0 color-mix(in srgb,var(--edge-live) 55%,transparent)}50%{box-shadow:0 0 0 4px transparent}}
|
|
126
|
+
@media (prefers-reduced-motion: reduce){#dsh-plan .p-node.is-working .dot{animation:none}}
|
|
119
127
|
#dsh-plan .p-node .sub{font-size:11px; color:var(--ink-muted); margin-top:2px;
|
|
120
128
|
overflow:hidden; text-overflow:ellipsis; white-space:nowrap}
|
|
121
129
|
/* W1: a picked search hit pulses its card for 1.2s (class removed on timer). */
|
|
@@ -252,6 +260,15 @@
|
|
|
252
260
|
#dsh-plan .p-dialog-input:hover{border-color:color-mix(in srgb,var(--uupm-ink-1) 55%,transparent)}
|
|
253
261
|
#dsh-plan .p-dialog-input:focus-visible{outline:2px solid var(--accent); outline-offset:1px; border-color:var(--accent)}
|
|
254
262
|
#dsh-plan .p-dialog-err{min-height:1.2em; font-size:11px; color:var(--uupm-error)}
|
|
263
|
+
#dsh-plan .p-dialog-kind{display:grid; grid-template-columns:1fr 1fr; gap:4px; padding:3px;
|
|
264
|
+
background:var(--uupm-bg-0); border:1px solid var(--card-line); border-radius:8px}
|
|
265
|
+
#dsh-plan .p-dialog-kind button{font:inherit; color:var(--ink-muted); background:transparent; border:1px solid transparent;
|
|
266
|
+
border-radius:6px; padding:6px 10px; cursor:pointer}
|
|
267
|
+
#dsh-plan .p-dialog-kind button:hover{color:var(--ink); background:var(--uupm-wash-8)}
|
|
268
|
+
#dsh-plan .p-dialog-kind button[aria-checked=true]{color:var(--ink); font-weight:600; background:var(--uupm-wash-15);
|
|
269
|
+
border-color:var(--accent)}
|
|
270
|
+
#dsh-plan .p-dialog-kind button:focus-visible{outline:2px solid var(--accent); outline-offset:1px}
|
|
271
|
+
#dsh-plan .p-dialog-hint{font-size:11px; color:var(--ink-muted); max-width:340px}
|
|
255
272
|
#dsh-plan .p-dialog-row{display:flex; gap:8px; justify-content:flex-end}
|
|
256
273
|
#dsh-plan .p-dialog-row button{background:transparent; color:var(--ink); border:1px solid var(--card-line);
|
|
257
274
|
border-radius:6px; padding:5px 12px; font:inherit; cursor:pointer}
|
|
@@ -93,6 +93,10 @@ body{background:var(--n-bg);color:var(--n-ink);font:13px/1.45 system-ui,"Segoe U
|
|
|
93
93
|
.wb-member-remove:hover:not(:disabled),.wb-member-submit:hover:not(:disabled){border-color:var(--n-danger)}
|
|
94
94
|
.wb-member-submit:hover:not(:disabled){border-color:var(--n-accent)}
|
|
95
95
|
.wb-member-add{display:flex;flex-wrap:wrap;gap:6px;margin-top:6px}
|
|
96
|
+
.wb-cli-home{display:flex;flex-wrap:wrap;gap:6px;padding:8px;margin:4px 0 10px;border:1px solid var(--n-border);border-radius:6px}
|
|
97
|
+
.wb-cli-home [hidden]{display:none}
|
|
98
|
+
.wb-cli-home .wb-hint{flex-basis:100%;margin-top:0}
|
|
99
|
+
.wb-cli-home .wb-member-pick{min-width:0;max-width:100%}
|
|
96
100
|
.wb-member-pick,.wb-member-manual{background:var(--n-panel);border:1px solid var(--n-border);color:var(--n-ink);
|
|
97
101
|
border-radius:6px;padding:3px 8px;font:inherit;font-size:12px;min-width:0}
|
|
98
102
|
.wb-member-pick{flex:1 1 150px}
|
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
* @module nexus-stage/workbench
|
|
20
20
|
*/
|
|
21
21
|
import { createChatDrawer, type ChatTarget } from './wb-chat'
|
|
22
|
+
import { createCliHomePicker, type CliHomeOptions } from './cli-home-picker'
|
|
22
23
|
import { createRailTabs } from './wb-rail'
|
|
23
24
|
import { DIRECTORY_FILTERS, learnLogTitle, loadLogTitles, renderDirectory, type DirFilter } from './wb-directory'
|
|
24
25
|
import { ledgerFacts, renderLedger as paintLedger } from './wb-ledger'
|
|
@@ -130,6 +131,10 @@ export interface CanvasMemberRow {
|
|
|
130
131
|
readonly joined?: boolean
|
|
131
132
|
readonly live?: boolean
|
|
132
133
|
readonly remote?: boolean
|
|
134
|
+
/** Present only for CLI refs: null means the linked home is ambiguous or unknown. */
|
|
135
|
+
readonly cliHome?: string | null
|
|
136
|
+
readonly cliHeld?: boolean
|
|
137
|
+
readonly cliHomeCleared?: boolean
|
|
133
138
|
}
|
|
134
139
|
|
|
135
140
|
/** One canvas team from /state `canvas.teams`. */
|
|
@@ -142,7 +147,7 @@ export interface CanvasTeamRow {
|
|
|
142
147
|
/** One activity-ring entry. */
|
|
143
148
|
export interface ActivityRow {
|
|
144
149
|
readonly ts: number
|
|
145
|
-
readonly dir: 'in' | 'out'
|
|
150
|
+
readonly dir: 'in' | 'out' | 'fwd'
|
|
146
151
|
readonly team: string
|
|
147
152
|
readonly peer: string
|
|
148
153
|
readonly ok: boolean
|
|
@@ -212,7 +217,8 @@ export interface WorkbenchDeps {
|
|
|
212
217
|
abandon(taskId: string, reason: string): Promise<boolean>
|
|
213
218
|
/** Canvas team member change via the control route (0.6.6): `id` is a
|
|
214
219
|
* local session id or a zone-qualified remote handle. True on success. */
|
|
215
|
-
canvasOp(action: 'add-member' | 'remove-member', name: string, id: string): Promise<boolean>
|
|
220
|
+
canvasOp(action: 'add-member' | 'remove-member', name: string, id: string, home?: string): Promise<boolean>
|
|
221
|
+
cliHomeOptions?(name: string, id: string): Promise<CliHomeOptions>
|
|
216
222
|
/** Quick dispatch (0.6.9): POST /__dsh_a2a/dispatch as a human gesture —
|
|
217
223
|
* one message to a routing team, async delivery, receipt lands in the
|
|
218
224
|
* ledger. Optional — without it the 快派 form hides. */
|
|
@@ -333,6 +339,7 @@ export function createWorkbench(deps: WorkbenchDeps): Workbench {
|
|
|
333
339
|
// Canvas add-member form cache (static DOM re-mounted across polls; keyed
|
|
334
340
|
// by team name — same trick as draftForm so inputs survive the 3s poll).
|
|
335
341
|
let canvasForm: { name: string; form: HTMLFormElement; pick: HTMLSelectElement; manual: HTMLInputElement } | null = null
|
|
342
|
+
const cliHomeForms = new Map<string, HTMLFormElement>()
|
|
336
343
|
// Quick-dispatch form (0.6.9), cached per target routing team the same
|
|
337
344
|
// draftForm way: a textarea's draft must survive the 3s poll remount.
|
|
338
345
|
let dispatchForm: { team: string; form: HTMLFormElement; text: HTMLTextAreaElement; note: HTMLElement } | null = null
|
|
@@ -690,7 +697,7 @@ export function createWorkbench(deps: WorkbenchDeps): Workbench {
|
|
|
690
697
|
for (const row of ring) {
|
|
691
698
|
const line = el('div', 'wb-row')
|
|
692
699
|
const dir = el('span', 'wb-row-dir')
|
|
693
|
-
dir.textContent = row.dir === 'out' ? '→' : '←'
|
|
700
|
+
dir.textContent = row.dir === 'fwd' ? '⇢' : row.dir === 'out' ? '→' : '←'
|
|
694
701
|
if (row.ok !== true) dir.classList.add('is-fail')
|
|
695
702
|
line.appendChild(dir)
|
|
696
703
|
teamCell(line, last, row.team)
|
|
@@ -962,12 +969,12 @@ export function createWorkbench(deps: WorkbenchDeps): Workbench {
|
|
|
962
969
|
}
|
|
963
970
|
for (const member of fresh.members) {
|
|
964
971
|
const row = el('div', 'wb-member-row')
|
|
965
|
-
const dot = member.remote === true ? '◇ 远端' : member.live === true ? '● 现场' : member.joined === true ? '○ 冷启' : '· 未入网'
|
|
972
|
+
const dot = member.cliHeld === true ? '本机持有,无需指定' : member.remote === true ? (member.cliHome === null ? '◇ 远端 · home 未确定' : '◇ 远端') : member.live === true ? '● 现场' : member.joined === true ? '○ 冷启' : '· 未入网'
|
|
966
973
|
row.appendChild(el('span', 'wb-member-dot')).textContent = dot
|
|
967
974
|
const label = el('span', 'wb-member-label')
|
|
968
975
|
const session = (last?.sessions ?? []).find(s => s.id === member.id)
|
|
969
976
|
label.textContent = member.remote === true ? member.id : (session !== undefined ? displayName(session) : member.id)
|
|
970
|
-
label.title = member.team ?? member.id
|
|
977
|
+
label.title = member.cliHeld === true ? '本机持有,无需指定' : member.cliHome !== undefined ? (member.cliHome ?? 'home 未确定') : (member.team ?? member.id)
|
|
971
978
|
row.appendChild(label)
|
|
972
979
|
const remove = el('button', 'wb-member-remove')
|
|
973
980
|
remove.type = 'button'
|
|
@@ -978,6 +985,20 @@ export function createWorkbench(deps: WorkbenchDeps): Workbench {
|
|
|
978
985
|
})
|
|
979
986
|
row.appendChild(remove)
|
|
980
987
|
card.appendChild(row)
|
|
988
|
+
if (member.cliHomeCleared === true) {
|
|
989
|
+
const notice = el('div', 'wb-hint')
|
|
990
|
+
notice.textContent = '本机落户已清除旧 home 钉;户口离线后也不会恢复旧钉。'
|
|
991
|
+
card.appendChild(notice)
|
|
992
|
+
}
|
|
993
|
+
if (member.cliHome === null && member.cliHeld !== true && deps.cliHomeOptions !== undefined) {
|
|
994
|
+
const key = JSON.stringify([fresh.name, member.id])
|
|
995
|
+
let form = cliHomeForms.get(key)
|
|
996
|
+
if (form === undefined) {
|
|
997
|
+
form = createCliHomePicker(() => deps.cliHomeOptions!(fresh.name, member.id), home => deps.canvasOp('add-member', fresh.name, member.id, home))
|
|
998
|
+
cliHomeForms.set(key, form)
|
|
999
|
+
}
|
|
1000
|
+
card.appendChild(form)
|
|
1001
|
+
}
|
|
981
1002
|
}
|
|
982
1003
|
// Add-member form: static DOM re-mounted per poll (draftForm trick) —
|
|
983
1004
|
// rebuilt only when the inspected team changes.
|
|
@@ -1196,6 +1217,8 @@ export function createWorkbench(deps: WorkbenchDeps): Workbench {
|
|
|
1196
1217
|
}
|
|
1197
1218
|
|
|
1198
1219
|
function update(state: StateBody | null): void {
|
|
1220
|
+
const unknown = new Set((state?.canvas?.teams ?? []).flatMap(team => team.members.filter(member => member.cliHome === null && member.cliHeld !== true).map(member => JSON.stringify([team.name, member.id]))))
|
|
1221
|
+
for (const key of cliHomeForms.keys()) if (!unknown.has(key)) cliHomeForms.delete(key)
|
|
1199
1222
|
last = state
|
|
1200
1223
|
// 0.6.12: refresh the readable-log list opportunistically (single flight;
|
|
1201
1224
|
// the sidebar re-renders when rows land, mid-poll remount is idempotent).
|
package/nexus-stage/src/world.ts
CHANGED
|
@@ -153,7 +153,11 @@ 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 {
|
|
156
|
+
export interface SessionLite {
|
|
157
|
+
id: string; label: string; team: string; name?: string; description?: string; joined: boolean; live?: boolean; teams?: readonly string[]
|
|
158
|
+
/** Live local sessions only: mid-turn ('running') or idle, and how many receipt-owing tasks wait on it. */
|
|
159
|
+
status?: 'running' | 'idle'; queued?: number
|
|
160
|
+
}
|
|
157
161
|
|
|
158
162
|
/** Minimal team shape (topology.CanvasTeam satisfies it). `team` is the
|
|
159
163
|
* mono route alias `<host>/canvas/<name>` when the payload carries it. */
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nelsonlongxiang/dsh-open-a2a-net",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.35",
|
|
4
4
|
"description": "Open A2A network plugin for DeepSeek Harness: signed agent cards, decentralized peer/zone discovery, direct routing model tools, and joinable session nodes with a sidebar control",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|
|
@@ -85,6 +85,7 @@
|
|
|
85
85
|
"test:live": "vitest run tests/live",
|
|
86
86
|
"test:build-layout": "vitest run tests/gates/build-layout.spec.ts",
|
|
87
87
|
"verify:docs": "node scripts/doc-drift.mjs",
|
|
88
|
+
"reanchor:arch": "node scripts/reanchor-architecture.mjs",
|
|
88
89
|
"verify:nexus": "node scripts/verify-nexus-ci.mjs && npm --prefix nexus-stage run build && npm --prefix nexus-stage run test && git diff --exit-code -- assets/nexusDist/assets && git diff --ignore-cr-at-eol --exit-code -- assets/nexusDist/index.html",
|
|
89
90
|
"verify": "pnpm typecheck && pnpm verify:docs && pnpm build && pnpm test && pnpm test:build-layout && pnpm verify:nexus"
|
|
90
91
|
},
|