@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,77 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: a2a-network
|
|
3
|
+
description: Use when a task needs cross-session or cross-node collaboration over the DSH A2A network — joining and teaming, routing work to remote teams and agents, reconciling receipts and the task ledger, and reading the A2A workbench (/__dsh_a2a_nexus/). Covers admission rules, the receipt contract, and the failure shapes that look like bugs but are contract semantics.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# DSH A2A Network
|
|
7
|
+
|
|
8
|
+
Drive the decentralized A2A network: sessions publish agent cards, join a
|
|
9
|
+
membership layer, form canvas teams, and route work to each other through
|
|
10
|
+
`a2a_route`. One plugin (`@nelsonlongxiang/dsh-open-a2a-net`) owns the whole
|
|
11
|
+
surface on every node.
|
|
12
|
+
|
|
13
|
+
## Objects
|
|
14
|
+
|
|
15
|
+
- **Node** — one DSH host process exposing `/.well-known/agent-card.json` and
|
|
16
|
+
the `__dsh_a2a/state` face. Peers discover each other through cards.
|
|
17
|
+
- **Session node** — a live DSH session on a node. Visibility follows
|
|
18
|
+
membership: an unjoined session is invisible to peer discovery AND refused
|
|
19
|
+
at the routing admission gate.
|
|
20
|
+
- **Team** — a routable identity. Two kinds: the node's process team
|
|
21
|
+
(`<zone>/<id8>` from the session id) and canvas teams
|
|
22
|
+
(`<zone>/canvas/<name>`, user-composed multi-member groups).
|
|
23
|
+
- **Task** — one routed round. Carries a task id, an idempotency fingerprint,
|
|
24
|
+
and a receipt contract.
|
|
25
|
+
|
|
26
|
+
## Routing (a2a_route)
|
|
27
|
+
|
|
28
|
+
```text
|
|
29
|
+
a2a_route { team: "<zone>/<id8 or canvas name>", message, context_id?, async? }
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
- `async: true` delivers without waiting (`delivered:true` + task id); the
|
|
33
|
+
target routes a receipt — a message starting `[A2A receipt] task <id>` —
|
|
34
|
+
back to your team.
|
|
35
|
+
- Sync (default) waits for the round's final answer. If the 15s transport
|
|
36
|
+
budget fires first, the result is **-32005 DELIVERED-UNSETTLED**: the
|
|
37
|
+
delivery landed and the peer keeps executing. **Never re-dispatch blindly**
|
|
38
|
+
— reconcile via the task id (the receipt or `a2a_teams` coordinates) instead.
|
|
39
|
+
- `context_id` from a previous reply continues that conversation.
|
|
40
|
+
|
|
41
|
+
## Membership rules
|
|
42
|
+
|
|
43
|
+
- An unjoined session has NO network: `a2a_route` and `a2a_teams` refuse it
|
|
44
|
+
(join gate), and discovery does not list it. The session that originates a
|
|
45
|
+
task is exempt from the gate.
|
|
46
|
+
- Join through the GUI sidebar (A2A 网络) or the join gate prompt; leave
|
|
47
|
+
through the same surface. Membership is visible; leaving removes you from
|
|
48
|
+
discovery too.
|
|
49
|
+
- Inbound deliveries from a caller with no `caller_session` are refused
|
|
50
|
+
(anonymous injection is a closed hole).
|
|
51
|
+
|
|
52
|
+
## Reading the state
|
|
53
|
+
|
|
54
|
+
`__dsh_a2a/state` (per node) carries `sessions` (live/joined), `sessionTeams`,
|
|
55
|
+
`remote` (peer session rows with `origin` = node label + LAN IP and `via` =
|
|
56
|
+
the peer URL), `activity` (routing ring), `tasks` (the ledger: pending /
|
|
57
|
+
stale / archived). The A2A workbench (`/__dsh_a2a_nexus/`, merged responsive
|
|
58
|
+
shell since 0.6.2 — the 2D planning board is the stage, directory / ledger /
|
|
59
|
+
activity / plan ride a collapsible rail, narrow screens get a drawer)
|
|
60
|
+
renders the same data for humans.
|
|
61
|
+
|
|
62
|
+
## Failure shapes that are contract semantics
|
|
63
|
+
|
|
64
|
+
- `-32000` with "declares no team membership" — the admission gate working
|
|
65
|
+
as designed; join (or route through a teamed member).
|
|
66
|
+
- `-32000` with "caller_session is required" — the delivery had no caller
|
|
67
|
+
identity; the protocol contract requires it.
|
|
68
|
+
- `-32005` — sync wait budget fired; delivery landed, reconcile the task id.
|
|
69
|
+
- "No live DSH session node accepts team X" — the team resolved but no
|
|
70
|
+
session node accepted the round; check whether the target session is live.
|
|
71
|
+
|
|
72
|
+
## Details
|
|
73
|
+
|
|
74
|
+
- [routing.md](references/routing.md) — receipts, idempotency, ledger anatomy
|
|
75
|
+
- [discovery.md](references/discovery.md) — cards, peer boundary model, invisibility
|
|
76
|
+
- [membership.md](references/membership.md) — join/leave, allowlist, canvas teams
|
|
77
|
+
- [pitfalls.md](references/pitfalls.md) — operational pitfalls observed in production
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# Discovery and the peer boundary model
|
|
2
|
+
|
|
3
|
+
## Cards
|
|
4
|
+
|
|
5
|
+
Each node serves `/.well-known/agent-card.json` (unsigned serve-fresh fields:
|
|
6
|
+
`peers`, `sessionTeams`, `teamMemberships`, `version`, `lanIp`; signed core:
|
|
7
|
+
identity + capability flags). Peers poll cards and aggregate `sessionTeams`
|
|
8
|
+
into their `remote` rows — this is how the planning canvas shows other nodes.
|
|
9
|
+
|
|
10
|
+
## Visibility follows membership
|
|
11
|
+
|
|
12
|
+
An unjoined session node is NOT advertised: it is absent from the served
|
|
13
|
+
card's `sessionTeams` and invisible to peer discovery. Join to appear; leave
|
|
14
|
+
to vanish. The node's own state face keeps every session (management surface
|
|
15
|
+
is local-only).
|
|
16
|
+
|
|
17
|
+
## The peer boundary model (connection layer)
|
|
18
|
+
|
|
19
|
+
Referrals — peer URLs learned from other peers' cards — pass two gates:
|
|
20
|
+
|
|
21
|
+
1. `referralLearning: false` freezes the tracked set to the configured seed
|
|
22
|
+
(`peers` config) for that config's lifetime: referrals are neither learned
|
|
23
|
+
nor offered.
|
|
24
|
+
2. A non-empty `peerAllowlist` admits a referral only when its host matches
|
|
25
|
+
an entry — exact `host[:port]`, bare domain suffix (`.example.com`), or
|
|
26
|
+
the full URL. An empty list keeps the historical open-referral posture.
|
|
27
|
+
|
|
28
|
+
Seeds from the `peers` config are owner-configured and never filtered.
|
|
29
|
+
|
|
30
|
+
Self-referrals (a peer listing this node's own URL back) are dropped and
|
|
31
|
+
remembered as aliases so mirrored lists stop re-offering them.
|
|
32
|
+
|
|
33
|
+
## Where to look
|
|
34
|
+
|
|
35
|
+
- Node state: `GET /__dsh_a2a/state`
|
|
36
|
+
- Card: `GET /.well-known/agent-card.json`
|
|
37
|
+
- Direct delivery: `POST /a2a/direct` (`{team, message, caller_session, wait}`)
|
|
38
|
+
- Task query: `POST /a2a/query` (`{task_id, fingerprint}`)
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# Membership: join, leave, teaming
|
|
2
|
+
|
|
3
|
+
## Joining
|
|
4
|
+
|
|
5
|
+
- GUI: sidebar → A2A 网络 → join toggle. The join writes the session into
|
|
6
|
+
`a2a/joined.json` (node home) — this is the discovery + admission
|
|
7
|
+
membership layer.
|
|
8
|
+
- API-created sessions do NOT materialize a consumable agent; open the
|
|
9
|
+
session in the GUI once (a queued prompt is then consumed) or create the
|
|
10
|
+
session through the GUI.
|
|
11
|
+
|
|
12
|
+
## Teaming
|
|
13
|
+
|
|
14
|
+
- `a2a_team_join { team }` — declare a membership in `a2a/teams.json`
|
|
15
|
+
(roster store, restored at construction). Subject to the owner's
|
|
16
|
+
`teamJoinAllowlist` (empty list denies every join; entries are exact names
|
|
17
|
+
or trailing-`*` prefixes).
|
|
18
|
+
- `a2a_team_leave { team }` — retract one declaration.
|
|
19
|
+
- Canvas teams (`<zone>/canvas/<name>`) are user-composed groups created in
|
|
20
|
+
the planning view (建队) — any joined node can be a member; a node may sit
|
|
21
|
+
in multiple teams.
|
|
22
|
+
|
|
23
|
+
## Admission (S3, default on)
|
|
24
|
+
|
|
25
|
+
- Outbound: a session with no declared team has no network
|
|
26
|
+
(`teamScopeRouting`, default true; explicit false restores the open seam).
|
|
27
|
+
- Inbound: `/a2a/direct` requires a SHARED team between caller and target —
|
|
28
|
+
the caller's declared teams (roster store) must intersect the target's
|
|
29
|
+
routable set. Refusal text names the missing membership.
|
|
30
|
+
- Anonymous deliveries (no `caller_session`) are refused outright.
|
|
31
|
+
|
|
32
|
+
## Information-plane scoping (S5, default on)
|
|
33
|
+
|
|
34
|
+
- The read-only fleet tools answer from the joined-team scope only
|
|
35
|
+
(`teamScopeInfo`, default true; the union of this host's roster
|
|
36
|
+
declarations): `a2a_teams` lists `[this host]` rows plus peer rows
|
|
37
|
+
serving a joined team (own name or roster declaration — the `note`
|
|
38
|
+
field explains the scope), `a2a_status` narrows peers/routes/activity
|
|
39
|
+
to that scope (local traffic stays), `a2a_probe` sweeps serving peers
|
|
40
|
+
and refuses a single URL whose card cannot prove service.
|
|
41
|
+
- `a2a_tasks` (this host's own dispatch ledger) stays unscoped — it is
|
|
42
|
+
the receipt-reconciliation surface for this host's own dispatches.
|
|
43
|
+
- A host whose nodes declare no teams keeps host-internal facts only;
|
|
44
|
+
explicit `teamScopeInfo: false` restores the open plane.
|
|
45
|
+
|
|
46
|
+
## Leaving
|
|
47
|
+
|
|
48
|
+
- `a2a_team_leave` per team; the network-level leave (退网) removes the
|
|
49
|
+
session from discovery and the join gate grants. Rejoining later is cheap —
|
|
50
|
+
there is no penalty for cycling membership around task boundaries.
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# Operational pitfalls (production-observed)
|
|
2
|
+
|
|
3
|
+
## Sessions
|
|
4
|
+
|
|
5
|
+
- **API-created sessions do not materialize.** `session/create` registers an
|
|
6
|
+
in-memory record; the sessions directory never appears and queued prompts
|
|
7
|
+
are never consumed. Create sessions through the GUI, or open the session in
|
|
8
|
+
the GUI once after creating via API.
|
|
9
|
+
- **adopt-style revive only works for previously materialized sessions** —
|
|
10
|
+
it is an idempotent revive, not a creation path.
|
|
11
|
+
|
|
12
|
+
## Routing
|
|
13
|
+
|
|
14
|
+
- **Do not blindly re-dispatch after a sync timeout.** -32005 means the
|
|
15
|
+
delivery landed; the peer may still be executing. Reconcile the task id
|
|
16
|
+
first (receipt or query), or you will double-execute.
|
|
17
|
+
- **Unroutable caller labels** (e.g. CLI-initiated `cli-manager/...`) cannot
|
|
18
|
+
receive receipts — use the initiating session's own node team, or expect
|
|
19
|
+
the receipt to be dropped. Deliveries still land.
|
|
20
|
+
- A sync route's 15s transport budget is shorter than most model turns. For
|
|
21
|
+
anything that runs long, prefer `async: true` and the receipt contract.
|
|
22
|
+
|
|
23
|
+
## Names and prefixes
|
|
24
|
+
|
|
25
|
+
- Python-tool registrations come in two families: shared `py__<name>` and
|
|
26
|
+
dispatcher-exclusive `py__dispatch__<name>`. A route resolving a member
|
|
27
|
+
tool against the wrong family fails with "cannot read properties of
|
|
28
|
+
undefined (reading 'find')" — check which wave registered the tool before
|
|
29
|
+
blaming the route.
|
|
30
|
+
- alpha.4 harness sessions have no `.events` accessor — integrations must
|
|
31
|
+
use `session.snapshotEvents()` (or guard), or they crash new-session turns
|
|
32
|
+
with "Cannot read properties of undefined (reading 'length')".
|
|
33
|
+
|
|
34
|
+
## Configuration
|
|
35
|
+
|
|
36
|
+
- `teams-scan` roots are resolved against the process CWD when relative. A
|
|
37
|
+
bare `disabled: true` row hides whole team mounts; pointing `roots` at
|
|
38
|
+
per-team `.agents/teams` directories is the reliable form. Do not add a
|
|
39
|
+
second scan source alongside packaged team bundles — double registration
|
|
40
|
+
crashes alpha.4's strict NamedEntries loader.
|
|
41
|
+
- The "Set TEAMS_ROOT" hint in the dir-degraded error is misleading: there is
|
|
42
|
+
no such env var; the scan reads its `roots` from config.
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# Routing deep-dive
|
|
2
|
+
|
|
3
|
+
## The receipt contract
|
|
4
|
+
|
|
5
|
+
Every async route (`wait:false`) registers the task on the peer and returns
|
|
6
|
+
immediately. The peer's target session steers the message into its turn; when
|
|
7
|
+
the turn settles, the peer routes a receipt envelope back to the caller's
|
|
8
|
+
team:
|
|
9
|
+
|
|
10
|
+
```text
|
|
11
|
+
[A2A receipt] task <task_id> <one-line outcome>
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
Correlated receipts also carry `{"outcome":"completed","idempotencyKey":"..."}`
|
|
15
|
+
on a following line. Receipts are the ONLY completion signal for async work —
|
|
16
|
+
polling `a2a_teams` cannot see another host's ledger.
|
|
17
|
+
|
|
18
|
+
## The ledger
|
|
19
|
+
|
|
20
|
+
`__dsh_a2a/state` → `tasks`:
|
|
21
|
+
|
|
22
|
+
- **pending** — routed, receipt still owed. An owed task with a delivered
|
|
23
|
+
marker is waiting on the target's turn, not lost.
|
|
24
|
+
- **stale/dead** — past the stale TTL, auto-dead-lettered. A dead task that
|
|
25
|
+
actually completed at the peer is a reconciliation candidate, not a rerun.
|
|
26
|
+
- **archived** — settled with a correlated receipt. The archive is bounded
|
|
27
|
+
(oldest-first eviction).
|
|
28
|
+
|
|
29
|
+
## Idempotency
|
|
30
|
+
|
|
31
|
+
A route carries a fingerprint (caller + message + team + wait-mode). Re-
|
|
32
|
+
submitting the same fingerprint inside the idempotency window returns the
|
|
33
|
+
original verdict (-32002/-32003 shapes) instead of executing twice. Never
|
|
34
|
+
work around a duplicate verdict by mutating the message — reconcile instead.
|
|
35
|
+
|
|
36
|
+
## -32005 (DELIVERED-UNSETTLED) procedure
|
|
37
|
+
|
|
38
|
+
1. Note the task id from the result.
|
|
39
|
+
2. Wait for the receipt envelope; or
|
|
40
|
+
3. Re-check via the coordinating surface (`a2a_status` activity ring, the
|
|
41
|
+
peer's state `tasks` face) for the task id's settled outcome.
|
|
42
|
+
4. Only re-dispatch when BOTH the receipt is absent AND the peer's ledger
|
|
43
|
+
shows the task unknown — otherwise you risk duplicate execution.
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Plugin-owned agent skill registration (obelisk pattern): the packaged
|
|
3
|
+
* `src/skill/SKILL.md` (+ references/) rides the standard DSH skill catalog.
|
|
4
|
+
* Install = apply registers the skill into ctx.skills; uninstall = the
|
|
5
|
+
* registration disposer unwinds with the fiber (no files copied, no residue).
|
|
6
|
+
*/
|
|
7
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
8
|
+
import { fileURLToPath } from 'node:url';
|
|
9
|
+
const PACKAGED_SKILL_ROOT = new URL('./skill/', import.meta.url);
|
|
10
|
+
/** Locate and validate the packaged SKILL.md; throw loudly when missing. */
|
|
11
|
+
function readSkill() {
|
|
12
|
+
const path = fileURLToPath(new URL('SKILL.md', PACKAGED_SKILL_ROOT));
|
|
13
|
+
if (!existsSync(path)) {
|
|
14
|
+
throw new Error(`a2a skill bundle is missing; expected SKILL.md at ${path}`);
|
|
15
|
+
}
|
|
16
|
+
const raw = readFileSync(path, 'utf8');
|
|
17
|
+
const firstLineEnd = raw.indexOf('\n');
|
|
18
|
+
if (firstLineEnd < 0 || raw.slice(0, firstLineEnd).replace(/\r$/, '') !== '---') {
|
|
19
|
+
throw new Error('a2a skill bundle is missing YAML frontmatter');
|
|
20
|
+
}
|
|
21
|
+
const closingStart = raw.indexOf('\n---', firstLineEnd);
|
|
22
|
+
if (closingStart < 0) {
|
|
23
|
+
throw new Error('a2a skill bundle has unterminated YAML frontmatter');
|
|
24
|
+
}
|
|
25
|
+
const frontmatter = raw.slice(firstLineEnd + 1, closingStart);
|
|
26
|
+
const nameMatch = frontmatter.match(/^name:\s*(.+)$/m);
|
|
27
|
+
const descMatch = frontmatter.match(/^description:\s*(.+)$/m);
|
|
28
|
+
const skillName = nameMatch?.[1]?.trim() ?? '';
|
|
29
|
+
const description = descMatch?.[1]?.trim() ?? '';
|
|
30
|
+
if (skillName !== 'a2a-network' || description === '') {
|
|
31
|
+
throw new Error('a2a skill frontmatter must define the a2a-network name and a non-empty description');
|
|
32
|
+
}
|
|
33
|
+
const bodyStart = raw.indexOf('\n', closingStart + 1);
|
|
34
|
+
return { name: skillName, description, content: raw.slice(bodyStart + 1).trim() };
|
|
35
|
+
}
|
|
36
|
+
/** Register the a2a-network skill (runtime contribution). */
|
|
37
|
+
export function registerA2aNetworkSkill(ctx) {
|
|
38
|
+
const skills = ctx.skills;
|
|
39
|
+
if (skills === undefined)
|
|
40
|
+
return;
|
|
41
|
+
const skill = readSkill();
|
|
42
|
+
const registration = {
|
|
43
|
+
name: skill.name,
|
|
44
|
+
description: skill.description,
|
|
45
|
+
invocation: { modelInvocable: true, userInvocable: true },
|
|
46
|
+
provider: 'a2a',
|
|
47
|
+
source: 'runtime',
|
|
48
|
+
resourceBase: { kind: 'directory', path: fileURLToPath(PACKAGED_SKILL_ROOT) },
|
|
49
|
+
content: skill.content,
|
|
50
|
+
};
|
|
51
|
+
skills.register(registration);
|
|
52
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Stage-mount pathname resolution: both static stages (canvas office floor,
|
|
3
|
+
* Three.js nexus viewer) serve one built Vite tree behind a mount prefix,
|
|
4
|
+
* and both used to answer the bare `/mount` form directly with index.html.
|
|
5
|
+
* That shell then resolved its `./assets/…` script reference against `/`,
|
|
6
|
+
* fetched `/assets/index-*.js` from the web root — a 404 — and rendered a
|
|
7
|
+
* silent black shell (`main.ts` returns early when state fetches fail). The
|
|
8
|
+
* fix is the directory-classic one: a bare mount request redirects to
|
|
9
|
+
* `mount/`, every other relative resolution then lands inside the tree.
|
|
10
|
+
* @module @nelsonlongxiang/dsh-open-a2a-net/stage-mount
|
|
11
|
+
*/
|
|
12
|
+
/**
|
|
13
|
+
* Resolve one request pathname against a stage mount prefix.
|
|
14
|
+
* @param pathname - `req.url` verbatim (query string allowed, split here).
|
|
15
|
+
* @param mount - the registered mount prefix, e.g. `/__dsh_a2a_nexus`.
|
|
16
|
+
* @returns the redirect target plus the within-tree relative path.
|
|
17
|
+
*/
|
|
18
|
+
export function resolveStageMount(pathname, mount) {
|
|
19
|
+
const raw = pathname.split('?')[0] ?? '/';
|
|
20
|
+
if (raw === mount)
|
|
21
|
+
return { redirectTo: `${mount}/`, rel: '' };
|
|
22
|
+
if (raw === `${mount}/`)
|
|
23
|
+
return { rel: '/index.html' };
|
|
24
|
+
let rel;
|
|
25
|
+
if (raw.startsWith(`${mount}/`))
|
|
26
|
+
rel = raw.slice(mount.length);
|
|
27
|
+
else if (raw.startsWith('/'))
|
|
28
|
+
rel = raw;
|
|
29
|
+
else
|
|
30
|
+
rel = `/${raw}`;
|
|
31
|
+
return { rel };
|
|
32
|
+
}
|