@hanphone/dsh-a2a 0.1.0 → 0.2.0
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 +98 -47
- package/README.zh.md +43 -42
- package/lib/client.js +49 -1
- package/lib/index.js +272 -17
- package/lib/tsconfig.client.tsbuildinfo +1 -1
- package/lib/tsconfig.tsbuildinfo +1 -1
- package/lib/types/api.d.ts +13 -0
- package/lib/types/api.d.ts.map +1 -1
- package/lib/types/api.js +5 -0
- package/lib/types/api.js.map +1 -1
- package/lib/types/client/index.d.ts +5 -0
- package/lib/types/client/index.d.ts.map +1 -1
- package/lib/types/index.d.ts.map +1 -1
- package/lib/types/index.js +74 -10
- package/lib/types/index.js.map +1 -1
- package/lib/types/server/a2a-server.d.ts +3 -1
- package/lib/types/server/a2a-server.d.ts.map +1 -1
- package/lib/types/server/a2a-server.js +5 -1
- package/lib/types/server/a2a-server.js.map +1 -1
- package/lib/types/server/identity.d.ts +38 -0
- package/lib/types/server/identity.d.ts.map +1 -0
- package/lib/types/server/identity.js +89 -0
- package/lib/types/server/identity.js.map +1 -0
- package/lib/types/server/inbound-registry.d.ts +60 -0
- package/lib/types/server/inbound-registry.d.ts.map +1 -0
- package/lib/types/server/inbound-registry.js +86 -0
- package/lib/types/server/inbound-registry.js.map +1 -0
- package/lib/types/server/store.d.ts +2 -0
- package/lib/types/server/store.d.ts.map +1 -1
- package/lib/types/server/store.js +4 -0
- package/lib/types/server/store.js.map +1 -1
- package/lib/types/service.d.ts +19 -0
- package/lib/types/service.d.ts.map +1 -1
- package/lib/types/service.js +12 -0
- package/lib/types/service.js.map +1 -1
- package/package.json +2 -2
- package/src/api.ts +27 -2
- package/src/client/index.ts +99 -2
- package/src/index.ts +76 -10
- package/src/server/a2a-server.ts +7 -2
- package/src/server/identity.ts +109 -0
- package/src/server/inbound-registry.ts +129 -0
- package/src/server/store.ts +5 -0
- package/src/service.ts +23 -0
package/README.md
CHANGED
|
@@ -1,36 +1,52 @@
|
|
|
1
1
|
# dsh-a2a
|
|
2
2
|
|
|
3
|
-
Agent2Agent (A2A) Protocol v1.0 dual-end plugin for [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness).
|
|
3
|
+
Agent2Agent (A2A) Protocol v1.0 dual-end plugin for [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness) — **English** · [中文](README.zh.md)
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
`@hanphone/dsh-a2a` is an independent, open-source A2A plugin that turns a
|
|
6
|
+
DeepSeek Harness profile into a first-class agent-to-agent citizen: an inbound
|
|
7
|
+
server with a durable governed task store and a live-derived AgentCard, an
|
|
8
|
+
outbound client whose remote skills appear as model tools, and a GUI
|
|
9
|
+
dashboard that manages both halves without touching any file.
|
|
6
10
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
- **Inbound server** — AgentCard derived from the live tool registry, JSON-RPC + SSE, durable task store, pluggable session/subagent executors, and a policy gate (`a2a/inbound-task`) with audit.
|
|
10
|
-
- **Outbound client** — a persisted multi-agent AgentCard registry, remote skills mapped to model tools (`a2a__<name>__<skill>`), sync calls with per-agent timeout.
|
|
11
|
-
- **GUI dashboard** — an **A2A 连接** settings page in the Harness Web UI: toggle the inbound server, manage outbound agents, view and cancel tasks — no config files required.
|
|
12
|
-
|
|
13
|
-
Design decisions are recorded in [docs/architecture.md](docs/architecture.md). Current scope is P0 of that document.
|
|
11
|
+
Architecture and design decisions: [docs/architecture.md](docs/architecture.md).
|
|
14
12
|
|
|
15
13
|
## Features
|
|
16
14
|
|
|
17
|
-
- **A2A v1.0 protocol surface** — `SendMessage`, `SendStreamingMessage`,
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
- **
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
- **
|
|
15
|
+
- **A2A v1.0 protocol surface** — `SendMessage`, `SendStreamingMessage`,
|
|
16
|
+
`GetTask`, `ListTasks`, `CancelTask`, `GetExtendedAgentCard`,
|
|
17
|
+
`SubscribeToTask` over JSON-RPC; SSE streaming with catch-up frames.
|
|
18
|
+
- **Dynamic AgentCard** — skills derived from the live `ctx.tools` registry
|
|
19
|
+
(explicit id list, loud failure on missing referents) plus a built-in
|
|
20
|
+
`chat` skill; a fresh install answers end-to-end immediately.
|
|
21
|
+
- **Durable task store** — tasks live in the `a2a` storage domain (JSON
|
|
22
|
+
backend by default, SQLite per deployment choice); server-generated ids
|
|
23
|
+
survive restarts.
|
|
24
|
+
- **Executors** — `session` (one DSH session per `contextId`) and `subagent`
|
|
25
|
+
(delegates to `ctx.subagents`, streams tool-call artifacts back).
|
|
26
|
+
- **Governed inbound** — every inbound task passes through the
|
|
27
|
+
`a2a/inbound-task` waterfall so policy plugins can veto or audit.
|
|
28
|
+
- **Inbound connection monitoring** — the dashboard shows which remote peers
|
|
29
|
+
are talking to this DSH (source, first/last seen, task counts, active
|
|
30
|
+
streams) and can close a peer.
|
|
31
|
+
- **Runtime service identity** — edit the AgentCard's name/description/version
|
|
32
|
+
from the dashboard; the card rebuilds immediately and the identity persists
|
|
33
|
+
across restarts.
|
|
34
|
+
- **First-run onboarding** — a fresh install shows a pre-filled identity form
|
|
35
|
+
("service identity") so publishing the service is one click.
|
|
36
|
+
- **Auth by environment variable** — inbound bearer token is referenced by
|
|
37
|
+
env-var name (`authTokenEnv`), never stored in config as plaintext.
|
|
38
|
+
- **Install-and-use** — both halves are enabled by default after
|
|
39
|
+
`dsh plugin add`; no manual patch required to start.
|
|
24
40
|
|
|
25
41
|
## Installation
|
|
26
42
|
|
|
27
|
-
### From npm
|
|
43
|
+
### From npm
|
|
28
44
|
|
|
29
45
|
```sh
|
|
30
46
|
dsh plugin --profile web add @hanphone/dsh-a2a
|
|
31
47
|
```
|
|
32
48
|
|
|
33
|
-
|
|
49
|
+
Any profile name works (`web`, custom profiles, headless etc.):
|
|
34
50
|
|
|
35
51
|
```sh
|
|
36
52
|
dsh plugin --profile <name> add @hanphone/dsh-a2a
|
|
@@ -42,16 +58,20 @@ dsh plugin --profile <name> add @hanphone/dsh-a2a
|
|
|
42
58
|
cd dsh-a2a
|
|
43
59
|
pnpm build
|
|
44
60
|
npm pack
|
|
45
|
-
dsh plugin --profile <name> add <path-to>/hanphone-dsh-a2a-0.
|
|
61
|
+
dsh plugin --profile <name> add <path-to>/hanphone-dsh-a2a-0.2.0.tgz
|
|
46
62
|
```
|
|
47
63
|
|
|
48
64
|
## Quick start
|
|
49
65
|
|
|
50
66
|
1. **Install** — `dsh plugin --profile web add @hanphone/dsh-a2a`.
|
|
51
|
-
2. **Restart the GUI** — the browser half is scanned at host startup, so
|
|
52
|
-
|
|
67
|
+
2. **Restart the GUI** — the browser half is scanned at host startup, so
|
|
68
|
+
restart once after installing (`pnpm dsh web` or your profile launcher).
|
|
69
|
+
3. **Open Settings → A2A 连接** — the dashboard shows the inbound server
|
|
70
|
+
status, the service identity (with first-run onboarding pre-filled), the
|
|
71
|
+
outbound agent list, inbound peer connections, and the task list.
|
|
53
72
|
|
|
54
|
-
The inbound server listens on the profile's webServer (default
|
|
73
|
+
The inbound server listens on the profile's webServer (default
|
|
74
|
+
`http://127.0.0.1:3080`):
|
|
55
75
|
|
|
56
76
|
```sh
|
|
57
77
|
curl http://127.0.0.1:3080/.well-known/agent-card.json
|
|
@@ -67,17 +87,26 @@ curl -X POST http://127.0.0.1:3080/a2a \
|
|
|
67
87
|
|
|
68
88
|
## GUI dashboard
|
|
69
89
|
|
|
70
|
-
The browser half registers an **A2A 连接** page under Settings. From it you
|
|
90
|
+
The browser half registers an **A2A 连接** page under Settings. From it you
|
|
91
|
+
can, without touching any file:
|
|
71
92
|
|
|
72
|
-
- toggle the inbound server
|
|
73
|
-
-
|
|
93
|
+
- toggle the inbound server;
|
|
94
|
+
- view and edit the service identity (name/description/version) — the
|
|
95
|
+
AgentCard rebuilds immediately and the change persists;
|
|
96
|
+
- list, add, enable/disable, refresh, and remove outbound agents;
|
|
97
|
+
- see which inbound peers are connected (source, activity, tasks) and close
|
|
98
|
+
one;
|
|
74
99
|
- view and cancel inbound tasks.
|
|
75
100
|
|
|
76
|
-
All dashboard traffic goes through the **loopback-only** `/a2a/api` route
|
|
101
|
+
All dashboard traffic goes through the **loopback-only** `/a2a/api` route —
|
|
102
|
+
remote peers can never drive it.
|
|
77
103
|
|
|
78
104
|
## Configuration
|
|
79
105
|
|
|
80
|
-
The dashboard covers
|
|
106
|
+
The dashboard covers day-to-day operations. Values the dashboard does not
|
|
107
|
+
edit (baseUrl, `authTokenEnv`, skills, executors, toolPrefix) are configured
|
|
108
|
+
through the profile's user patch layer (`$DSH_HOME/profiles/<name>/cordis.patch.yml`)
|
|
109
|
+
— the reserve path:
|
|
81
110
|
|
|
82
111
|
```yaml
|
|
83
112
|
- id: a2a
|
|
@@ -103,11 +132,17 @@ The dashboard covers the day-to-day operations. Values the dashboard does not ed
|
|
|
103
132
|
|
|
104
133
|
### Required host services
|
|
105
134
|
|
|
106
|
-
Base-backed profiles mount them all: `webServer` (`@deepseek-ai/dsh-host-webserver`),
|
|
135
|
+
Base-backed profiles mount them all: `webServer` (`@deepseek-ai/dsh-host-webserver`),
|
|
136
|
+
the storage stack (`@deepseek-ai/dsh-storage` + `@deepseek-ai/dsh-storage-domain`),
|
|
137
|
+
the tools registry (`@deepseek-ai/dsh-tools`), and an agent loop
|
|
138
|
+
(`@deepseek-ai/dsh-agent` + `@deepseek-ai/dsh-agent-loop`; the subagent
|
|
139
|
+
executor additionally needs `@deepseek-ai/dsh-subagent`).
|
|
107
140
|
|
|
108
141
|
### Storage backend
|
|
109
142
|
|
|
110
|
-
The task store lives in the `a2a` storage domain. The base composition routes
|
|
143
|
+
The task store lives in the `a2a` storage domain. The base composition routes
|
|
144
|
+
storage through the `json` backend; to use SQLite, route the domain and add
|
|
145
|
+
the backend in the same patch layer:
|
|
111
146
|
|
|
112
147
|
```yaml
|
|
113
148
|
- id: storage-domain
|
|
@@ -137,7 +172,9 @@ The task store lives in the `a2a` storage domain. The base composition routes st
|
|
|
137
172
|
timeoutMs: 60000
|
|
138
173
|
```
|
|
139
174
|
|
|
140
|
-
Each enabled remote agent's skills become model tools named
|
|
175
|
+
Each enabled remote agent's skills become model tools named
|
|
176
|
+
`a2a__<name>__<skill>` (normalized, collision-hashed). The registry persists
|
|
177
|
+
across restarts.
|
|
141
178
|
|
|
142
179
|
## CLI
|
|
143
180
|
|
|
@@ -151,9 +188,15 @@ a2a status | enable | disable | card | agents |
|
|
|
151
188
|
|
|
152
189
|
## How it works
|
|
153
190
|
|
|
154
|
-
- **Inbound** — `POST /a2a` (JSON-RPC) and `GET /.well-known/agent-card.json`;
|
|
155
|
-
|
|
156
|
-
|
|
191
|
+
- **Inbound** — `POST /a2a` (JSON-RPC) and `GET /.well-known/agent-card.json`;
|
|
192
|
+
the AgentCard derives from the live tool registry and carries the persisted
|
|
193
|
+
service identity. Tasks flow through `a2a/inbound-task` → executor → task
|
|
194
|
+
store, with SSE frames streamed to subscribers.
|
|
195
|
+
- **Outbound** — a persisted `agents` table in the `a2a` domain; `A2AClient`
|
|
196
|
+
discovers an AgentCard, and each skill registers as a tool.
|
|
197
|
+
- **Dashboard** — the browser half (React, `settings.section`) reads/writes
|
|
198
|
+
the loopback-only `/a2a/api` route; the host half feeds it snapshots of the
|
|
199
|
+
server, tasks, agents, inbound peers, and the service identity.
|
|
157
200
|
|
|
158
201
|
See [docs/architecture.md](docs/architecture.md) for the full design.
|
|
159
202
|
|
|
@@ -161,19 +204,21 @@ See [docs/architecture.md](docs/architecture.md) for the full design.
|
|
|
161
204
|
|
|
162
205
|
```
|
|
163
206
|
src/
|
|
164
|
-
api.ts
|
|
165
|
-
index.ts
|
|
166
|
-
protocol.ts
|
|
167
|
-
jsonrpc.ts
|
|
168
|
-
server/
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
207
|
+
api.ts # loopback dashboard API (/a2a/api)
|
|
208
|
+
index.ts # Cordis plugin entry (apply)
|
|
209
|
+
protocol.ts # A2A v1.0 protocol constants + types
|
|
210
|
+
jsonrpc.ts # JSON-RPC framing
|
|
211
|
+
server/ # inbound half: store, card, a2a-server, routes,
|
|
212
|
+
# executors, inbound-registry, identity
|
|
213
|
+
outbound/ # outbound half: A2AClient, registry, tools
|
|
214
|
+
client/ # browser half: settings dashboard (React)
|
|
215
|
+
service.ts # ctx.a2a service facade
|
|
216
|
+
commands.ts # /a2a chat command
|
|
173
217
|
tests/
|
|
174
|
-
unit/
|
|
175
|
-
|
|
176
|
-
|
|
218
|
+
unit/ # protocol, framing, card, store, registry, server,
|
|
219
|
+
# client, api, inbound-registry, identity
|
|
220
|
+
composition/ # apply() on a real Cordis Context with stub host services
|
|
221
|
+
cordis.patch.yml # bundle patch (mounts the plugin, enabled by default)
|
|
177
222
|
```
|
|
178
223
|
|
|
179
224
|
## Development
|
|
@@ -187,9 +232,15 @@ pnpm test # vitest run (unit + composition suites)
|
|
|
187
232
|
pnpm build # tsc + tsdown → lib/index.js (host) + lib/client.js (browser)
|
|
188
233
|
```
|
|
189
234
|
|
|
190
|
-
##
|
|
235
|
+
## Acknowledgements
|
|
191
236
|
|
|
192
|
-
|
|
237
|
+
This plugin was inspired by and developed alongside
|
|
238
|
+
[ryubyte/dsh-a2a](https://github.com/ryubyte/dsh-a2a), an earlier A2A plugin
|
|
239
|
+
for DeepSeek Harness. That project's design — dual-end scope, AgentCard
|
|
240
|
+
derivation from the tool registry, and the settings-dashboard pattern — set
|
|
241
|
+
the direction this implementation follows. Our protocol layer, task store,
|
|
242
|
+
and executor seam are independent implementations; the GUI management model
|
|
243
|
+
owes a direct debt to ryubyte's connection dashboard.
|
|
193
244
|
|
|
194
245
|
## License
|
|
195
246
|
|
package/README.zh.md
CHANGED
|
@@ -1,36 +1,33 @@
|
|
|
1
1
|
# dsh-a2a
|
|
2
2
|
|
|
3
|
-
Agent2Agent(A2A)v1.0 双端插件,用于 [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness)
|
|
3
|
+
Agent2Agent(A2A)v1.0 双端插件,用于 [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness) — [English](README.md) · **中文**
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
`@hanphone/dsh-a2a` 是一个独立开源的 A2A 插件,把 DeepSeek Harness profile 变成 A2A 一等公民:带持久化、受治理任务存储与实时派生 AgentCard 的入站服务端;远程技能映射为模型工具的出站客户端;以及无需改任何文件即可管理两端的 GUI 面板。
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
- **入站服务端** —— 从实时工具注册表派生的 AgentCard、JSON-RPC + SSE、持久化任务存储、可插拔的会话/子代理执行器,以及带审计的策略门禁(`a2a/inbound-task`)。
|
|
10
|
-
- **出站客户端** —— 持久化的多 Agent 注册表,远程技能映射为模型工具(`a2a__<name>__<skill>`),带每 agent 超时的同步调用。
|
|
11
|
-
- **GUI 面板** —— Harness Web UI 设置中的 **A2A 连接** 页:开关入站服务端、管理出站 agent、查看与取消任务——无需改任何配置文件。
|
|
12
|
-
|
|
13
|
-
设计决策见 [docs/architecture.md](docs/architecture.md)。当前范围为该文档中的 P0。
|
|
7
|
+
架构与设计决策:[docs/architecture.md](docs/architecture.md)。
|
|
14
8
|
|
|
15
9
|
## 功能
|
|
16
10
|
|
|
17
|
-
- **A2A v1.0 协议面**
|
|
18
|
-
- **动态 AgentCard**
|
|
19
|
-
- **持久化任务存储**
|
|
20
|
-
- **执行器**
|
|
21
|
-
-
|
|
22
|
-
-
|
|
23
|
-
-
|
|
11
|
+
- **A2A v1.0 协议面** — JSON-RPC 上的 `SendMessage`、`SendStreamingMessage`、`GetTask`、`ListTasks`、`CancelTask`、`GetExtendedAgentCard`、`SubscribeToTask`;SSE 流式带补发帧。
|
|
12
|
+
- **动态 AgentCard** — 技能从实时 `ctx.tools` 注册表派生(显式 id 清单,缺失引用大声失败),外加内置 `chat` 技能;全新安装即可端到端验证。
|
|
13
|
+
- **持久化任务存储** — 任务存于 `a2a` 存储域(默认 json 后端,可按部署切 SQLite);服务端生成 id,跨重启存活。
|
|
14
|
+
- **执行器** — `session`(每个 `contextId` 一个 DSH 会话)与 `subagent`(委托 `ctx.subagents`,把工具调用过程流式回传)。
|
|
15
|
+
- **受治理入站** — 每个入站任务都经过 `a2a/inbound-task` waterfall,策略插件可否决或审计。
|
|
16
|
+
- **入站连接监控** — 面板展示谁在调用本 DSH(来源、首/末次、任务数、活跃流),可关闭某个对端。
|
|
17
|
+
- **运行时服务身份** — 在面板中编辑 AgentCard 的 name/description/version;卡片立即重建,身份跨重启持久化。
|
|
18
|
+
- **引导式首次配置** — 全新安装显示预填身份表单("服务身份"),一步发布服务。
|
|
19
|
+
- **环境变量鉴权** — 入站 Bearer token 只以环境变量名(`authTokenEnv`)引用,不以明文落配置。
|
|
20
|
+
- **装完即用** — `dsh plugin add` 后两端默认启用,无需手动 patch。
|
|
24
21
|
|
|
25
22
|
## 安装
|
|
26
23
|
|
|
27
|
-
### 从 npm
|
|
24
|
+
### 从 npm
|
|
28
25
|
|
|
29
26
|
```sh
|
|
30
27
|
dsh plugin --profile web add @hanphone/dsh-a2a
|
|
31
28
|
```
|
|
32
29
|
|
|
33
|
-
任意 profile
|
|
30
|
+
任意 profile 名均可(`web`、自定义 profile、headless 等):
|
|
34
31
|
|
|
35
32
|
```sh
|
|
36
33
|
dsh plugin --profile <name> add @hanphone/dsh-a2a
|
|
@@ -42,14 +39,14 @@ dsh plugin --profile <name> add @hanphone/dsh-a2a
|
|
|
42
39
|
cd dsh-a2a
|
|
43
40
|
pnpm build
|
|
44
41
|
npm pack
|
|
45
|
-
dsh plugin --profile <name> add <path-to>/hanphone-dsh-a2a-0.
|
|
42
|
+
dsh plugin --profile <name> add <path-to>/hanphone-dsh-a2a-0.2.0.tgz
|
|
46
43
|
```
|
|
47
44
|
|
|
48
45
|
## 快速开始
|
|
49
46
|
|
|
50
|
-
1. **安装**
|
|
51
|
-
2. **重启 GUI**
|
|
52
|
-
3. **打开 设置 → A2A 连接**
|
|
47
|
+
1. **安装** — `dsh plugin --profile web add @hanphone/dsh-a2a`。
|
|
48
|
+
2. **重启 GUI** — 浏览器端插件表在 host 启动时扫描,装完请重启一次(`pnpm dsh web` 或对应 profile 启动命令)。
|
|
49
|
+
3. **打开 设置 → A2A 连接** — 面板展示入站服务端状态、服务身份(全新安装预填引导表单)、出站 agent 列表、入站对端连接、任务列表。
|
|
53
50
|
|
|
54
51
|
入站服务端监听在 profile 的 webServer 上(默认 `http://127.0.0.1:3080`):
|
|
55
52
|
|
|
@@ -69,15 +66,17 @@ curl -X POST http://127.0.0.1:3080/a2a \
|
|
|
69
66
|
|
|
70
67
|
浏览器端在设置中注册 **A2A 连接** 页。无需改文件即可:
|
|
71
68
|
|
|
72
|
-
-
|
|
73
|
-
-
|
|
69
|
+
- 开关入站服务端;
|
|
70
|
+
- 查看与编辑服务身份(name/description/version)——AgentCard 立即重建,变更持久化;
|
|
71
|
+
- 列出、添加、启用/停用、刷新、删除出站 agent;
|
|
72
|
+
- 查看入站对端连接(来源、活动、任务)并关闭某个对端;
|
|
74
73
|
- 查看与取消入站任务。
|
|
75
74
|
|
|
76
75
|
所有面板流量都走 profile webServer 上的**仅回环** `/a2a/api` 路由——远程对端永远无法驱动它。
|
|
77
76
|
|
|
78
77
|
## 配置
|
|
79
78
|
|
|
80
|
-
面板覆盖日常操作。面板不编辑的项(
|
|
79
|
+
面板覆盖日常操作。面板不编辑的项(baseUrl、`authTokenEnv`、skills、executors、toolPrefix)通过 profile 用户 patch 层(`$DSH_HOME/profiles/<name>/cordis.patch.yml`)配置——保留方式:
|
|
81
80
|
|
|
82
81
|
```yaml
|
|
83
82
|
- id: a2a
|
|
@@ -151,9 +150,9 @@ a2a status | enable | disable | card | agents |
|
|
|
151
150
|
|
|
152
151
|
## 工作原理
|
|
153
152
|
|
|
154
|
-
- **入站**
|
|
155
|
-
- **出站**
|
|
156
|
-
- **面板**
|
|
153
|
+
- **入站** — `POST /a2a`(JSON-RPC)与 `GET /.well-known/agent-card.json`;AgentCard 从实时工具注册表派生并携带持久化服务身份。任务流经 `a2a/inbound-task` → 执行器 → 任务存储,SSE 帧推送给订阅者。
|
|
154
|
+
- **出站** — `a2a` 域中的持久化 `agents` 表;`A2AClient` 发现 AgentCard,每个技能注册为一个工具。
|
|
155
|
+
- **面板** — 浏览器端(React,`settings.section`)经仅回环 `/a2a/api` 路由读写;host 端喂给它 server/tasks/agents/入站对端/服务身份的快照。
|
|
157
156
|
|
|
158
157
|
完整设计见 [docs/architecture.md](docs/architecture.md)。
|
|
159
158
|
|
|
@@ -161,19 +160,21 @@ a2a status | enable | disable | card | agents |
|
|
|
161
160
|
|
|
162
161
|
```
|
|
163
162
|
src/
|
|
164
|
-
api.ts
|
|
165
|
-
index.ts
|
|
166
|
-
protocol.ts
|
|
167
|
-
jsonrpc.ts
|
|
168
|
-
server/
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
163
|
+
api.ts # 回环面板 API (/a2a/api)
|
|
164
|
+
index.ts # Cordis 插件入口 (apply)
|
|
165
|
+
protocol.ts # A2A v1.0 协议常量与类型
|
|
166
|
+
jsonrpc.ts # JSON-RPC 帧
|
|
167
|
+
server/ # 入站半区:store、card、a2a-server、routes、
|
|
168
|
+
# executors、inbound-registry、identity
|
|
169
|
+
outbound/ # 出站半区:A2AClient、registry、tools
|
|
170
|
+
client/ # 浏览器半区:设置面板 (React)
|
|
171
|
+
service.ts # ctx.a2a 服务 facade
|
|
172
|
+
commands.ts # /a2a 聊天命令
|
|
173
173
|
tests/
|
|
174
|
-
unit/
|
|
175
|
-
|
|
176
|
-
|
|
174
|
+
unit/ # protocol、framing、card、store、registry、server、
|
|
175
|
+
# client、api、inbound-registry、identity
|
|
176
|
+
composition/ # 在真实 Cordis Context 上以 stub 宿主服务跑 apply()
|
|
177
|
+
cordis.patch.yml # bundle patch(挂载插件,默认启用)
|
|
177
178
|
```
|
|
178
179
|
|
|
179
180
|
## 开发
|
|
@@ -186,9 +187,9 @@ pnpm test # vitest run(单元 + 组合套件)
|
|
|
186
187
|
pnpm build # tsc + tsdown → lib/index.js(host)+ lib/client.js(浏览器)
|
|
187
188
|
```
|
|
188
189
|
|
|
189
|
-
##
|
|
190
|
+
## 致谢
|
|
190
191
|
|
|
191
|
-
|
|
192
|
+
本项目受 [ryubyte/dsh-a2a](https://github.com/ryubyte/dsh-a2a) 启发并与其并行开发——那是 DeepSeek Harness 上更早的 A2A 插件。其设计——双端范围、从工具注册表派生 AgentCard、设置面板模式——为本文实现指明了方向。我们的协议层、任务存储与执行器接缝均为独立实现;GUI 管理模型则直接承袭了 ryubyte 的连接面板。
|
|
192
193
|
|
|
193
194
|
## 许可证
|
|
194
195
|
|
package/lib/client.js
CHANGED
|
@@ -65,6 +65,10 @@ window.__ModuleLoader__.load({
|
|
|
65
65
|
const [agentName, setAgentName] = (0, react.useState)("");
|
|
66
66
|
const [agentUrl, setAgentUrl] = (0, react.useState)("");
|
|
67
67
|
const [busy, setBusy] = (0, react.useState)(false);
|
|
68
|
+
const [editing, setEditing] = (0, react.useState)(false);
|
|
69
|
+
const [idName, setIdName] = (0, react.useState)("");
|
|
70
|
+
const [idDescription, setIdDescription] = (0, react.useState)("");
|
|
71
|
+
const [idVersion, setIdVersion] = (0, react.useState)("");
|
|
68
72
|
(0, react.useEffect)(() => {
|
|
69
73
|
let alive = true;
|
|
70
74
|
const tick = async () => {
|
|
@@ -73,6 +77,11 @@ window.__ModuleLoader__.load({
|
|
|
73
77
|
if (alive) {
|
|
74
78
|
setSnap(next);
|
|
75
79
|
setError(void 0);
|
|
80
|
+
if (!editing) {
|
|
81
|
+
setIdName(next.server.name ?? "");
|
|
82
|
+
setIdDescription(next.server.description ?? "");
|
|
83
|
+
setIdVersion(next.server.version ?? "");
|
|
84
|
+
}
|
|
76
85
|
}
|
|
77
86
|
} catch (err) {
|
|
78
87
|
if (alive) setError(err.message);
|
|
@@ -99,12 +108,51 @@ window.__ModuleLoader__.load({
|
|
|
99
108
|
};
|
|
100
109
|
const server = snap?.server;
|
|
101
110
|
const agents = snap?.agents ?? [];
|
|
111
|
+
const inbounds = snap?.inbounds ?? [];
|
|
102
112
|
const tasks = snap?.tasks ?? [];
|
|
103
113
|
return (0, react.createElement)("div", { className: "dsh-a2a-panel" }, error !== void 0 ? (0, react.createElement)("div", { className: "dsh-a2a-error" }, error) : null, (0, react.createElement)("div", { className: "dsh-a2a-card" }, (0, react.createElement)("h3", null, "入站 Server"), server === void 0 ? (0, react.createElement)("div", { className: "muted" }, "加载中…") : (0, react.createElement)("div", { className: "dsh-a2a-row" }, (0, react.createElement)("span", { className: `dsh-a2a-state ${server.enabled ? "ok" : "bad"}` }, server.enabled ? "已启用" : "已停用"), (0, react.createElement)("button", {
|
|
104
114
|
className: server.enabled ? "" : "primary",
|
|
105
115
|
disabled: busy,
|
|
106
116
|
onClick: () => void control({ action: server.enabled ? "server.disable" : "server.enable" })
|
|
107
|
-
}, server.enabled ? "停用" : "启用")), server !== void 0 && server.enabled ? (0, react.createElement)("div", { className: "dsh-a2a-row muted" }, `AgentCard: ${server.cardUrl ?? "(n/a)"} · 技能: ${server.skills.join(", ") || "(chat)"}`) : null), (0, react.createElement)("div", { className: "dsh-a2a-card" },
|
|
117
|
+
}, server.enabled ? "停用" : "启用")), server !== void 0 && server.enabled ? (0, react.createElement)("div", { className: "dsh-a2a-row muted" }, `AgentCard: ${server.cardUrl ?? "(n/a)"} · 技能: ${server.skills.join(", ") || "(chat)"}`) : null), (0, react.createElement)("div", { className: "dsh-a2a-card" }, server === void 0 || server.configured === false ? (0, react.createElement)("h3", null, "服务身份(首次配置)") : (0, react.createElement)("h3", null, "服务身份"), (0, react.createElement)("form", {
|
|
118
|
+
className: "dsh-a2a-form",
|
|
119
|
+
onSubmit: (ev) => {
|
|
120
|
+
ev.preventDefault();
|
|
121
|
+
control({
|
|
122
|
+
action: "identity.update",
|
|
123
|
+
name: idName,
|
|
124
|
+
description: idDescription,
|
|
125
|
+
version: idVersion
|
|
126
|
+
}).then(() => {
|
|
127
|
+
setEditing(false);
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
}, (0, react.createElement)("input", {
|
|
131
|
+
value: idVersion,
|
|
132
|
+
placeholder: "版本",
|
|
133
|
+
onChange: (e) => setIdVersion(e.target.value),
|
|
134
|
+
style: { width: "60px" }
|
|
135
|
+
}), (0, react.createElement)("input", {
|
|
136
|
+
value: idName,
|
|
137
|
+
placeholder: "服务名称",
|
|
138
|
+
onChange: (e) => setIdName(e.target.value),
|
|
139
|
+
style: { width: "160px" }
|
|
140
|
+
}), (0, react.createElement)("input", {
|
|
141
|
+
value: idDescription,
|
|
142
|
+
placeholder: "服务描述",
|
|
143
|
+
onChange: (e) => setIdDescription(e.target.value),
|
|
144
|
+
style: { width: "260px" }
|
|
145
|
+
}), (0, react.createElement)("button", {
|
|
146
|
+
type: "submit",
|
|
147
|
+
className: "primary",
|
|
148
|
+
disabled: busy
|
|
149
|
+
}, "保存身份")), (0, react.createElement)("div", { className: "dsh-a2a-row muted" }, "名称、描述、版本即刻重建 AgentCard;技能始终从工具注册表派生。")), (0, react.createElement)("div", { className: "dsh-a2a-card" }, (0, react.createElement)("h3", null, `入站连接(${inbounds.length})`), inbounds.length === 0 ? (0, react.createElement)("div", { className: "muted" }, "当前无远程对端正在调用本服务。") : (0, react.createElement)("table", { className: "dsh-a2a-table" }, (0, react.createElement)("thead", null, (0, react.createElement)("tr", null, (0, react.createElement)("th", null, "来源"), (0, react.createElement)("th", null, "任务"), (0, react.createElement)("th", null, "流式"), (0, react.createElement)("th", null, "首次 / 最近"), (0, react.createElement)("th", null, "操作"))), (0, react.createElement)("tbody", null, ...inbounds.map((p) => (0, react.createElement)("tr", { key: p.id }, (0, react.createElement)("td", null, p.source ?? p.label), (0, react.createElement)("td", null, `${p.taskCount}(活跃 ${p.activeTaskIds.length})`), (0, react.createElement)("td", null, p.streaming ? "●" : "—"), (0, react.createElement)("td", null, (0, react.createElement)("span", { className: "muted" }, `${p.firstSeen ?? ""} / ${p.lastSeen ?? ""}`)), (0, react.createElement)("td", null, (0, react.createElement)("button", {
|
|
150
|
+
disabled: busy,
|
|
151
|
+
onClick: () => void control({
|
|
152
|
+
action: "inbound.close",
|
|
153
|
+
id: p.id
|
|
154
|
+
})
|
|
155
|
+
}, "关闭"))))))), (0, react.createElement)("div", { className: "dsh-a2a-card" }, (0, react.createElement)("h3", null, `出站 Agents(${agents.length})`), agents.length === 0 ? (0, react.createElement)("div", { className: "muted" }, "未配置远程 agent。") : (0, react.createElement)("table", { className: "dsh-a2a-table" }, (0, react.createElement)("thead", null, (0, react.createElement)("tr", null, (0, react.createElement)("th", null, "名称"), (0, react.createElement)("th", null, "状态"), (0, react.createElement)("th", null, "工具"), (0, react.createElement)("th", null, "操作"))), (0, react.createElement)("tbody", null, ...agents.map((a) => (0, react.createElement)("tr", { key: a.id }, (0, react.createElement)("td", null, (0, react.createElement)("div", null, a.name), (0, react.createElement)("div", { className: "muted" }, a.agentCardUrl), a.lastError !== void 0 ? (0, react.createElement)("div", { className: "dsh-a2a-error" }, a.lastError) : null), (0, react.createElement)("td", null, (0, react.createElement)("span", { className: `dsh-a2a-state ${a.state === "connected" ? "ok" : "bad"}` }, a.state)), (0, react.createElement)("td", null, `${a.toolCount} / ${a.skillCount}`), (0, react.createElement)("td", null, (0, react.createElement)("button", {
|
|
108
156
|
disabled: busy,
|
|
109
157
|
onClick: () => void control({
|
|
110
158
|
action: a.enabled ? "agent.disable" : "agent.enable",
|