@hanphone/dsh-a2a 0.2.0 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (78) hide show
  1. package/README.md +106 -106
  2. package/README.zh.md +50 -74
  3. package/cordis.patch.yml +4 -7
  4. package/lib/client.js +186 -73
  5. package/lib/index.js +1275 -868
  6. package/lib/types/api.d.ts +30 -23
  7. package/lib/types/api.d.ts.map +1 -1
  8. package/lib/types/api.js +56 -23
  9. package/lib/types/api.js.map +1 -1
  10. package/lib/types/client/index.d.ts +50 -19
  11. package/lib/types/client/index.d.ts.map +1 -1
  12. package/lib/types/commands.d.ts +4 -2
  13. package/lib/types/commands.d.ts.map +1 -1
  14. package/lib/types/commands.js +59 -46
  15. package/lib/types/commands.js.map +1 -1
  16. package/lib/types/events.d.ts +32 -9
  17. package/lib/types/events.d.ts.map +1 -1
  18. package/lib/types/index.d.ts +21 -32
  19. package/lib/types/index.d.ts.map +1 -1
  20. package/lib/types/index.js +177 -272
  21. package/lib/types/index.js.map +1 -1
  22. package/lib/types/outbound/calls.d.ts.map +1 -1
  23. package/lib/types/outbound/calls.js +8 -6
  24. package/lib/types/outbound/calls.js.map +1 -1
  25. package/lib/types/protocol.d.ts +174 -109
  26. package/lib/types/protocol.d.ts.map +1 -1
  27. package/lib/types/protocol.js +58 -34
  28. package/lib/types/protocol.js.map +1 -1
  29. package/lib/types/server/a2a-server.d.ts +4 -0
  30. package/lib/types/server/a2a-server.d.ts.map +1 -1
  31. package/lib/types/server/a2a-server.js +6 -2
  32. package/lib/types/server/a2a-server.js.map +1 -1
  33. package/lib/types/server/card.d.ts +6 -30
  34. package/lib/types/server/card.d.ts.map +1 -1
  35. package/lib/types/server/card.js +13 -49
  36. package/lib/types/server/card.js.map +1 -1
  37. package/lib/types/server/exec/agent-runtime.d.ts +20 -0
  38. package/lib/types/server/exec/agent-runtime.d.ts.map +1 -1
  39. package/lib/types/server/exec/agent-runtime.js +2 -1
  40. package/lib/types/server/exec/agent-runtime.js.map +1 -1
  41. package/lib/types/server/routes.d.ts +5 -1
  42. package/lib/types/server/routes.d.ts.map +1 -1
  43. package/lib/types/server/routes.js +9 -5
  44. package/lib/types/server/routes.js.map +1 -1
  45. package/lib/types/server/store.d.ts +32 -0
  46. package/lib/types/server/store.d.ts.map +1 -1
  47. package/lib/types/server/store.js +10 -0
  48. package/lib/types/server/store.js.map +1 -1
  49. package/lib/types/servers/inbound-manager.d.ts +154 -0
  50. package/lib/types/servers/inbound-manager.d.ts.map +1 -0
  51. package/lib/types/servers/inbound-manager.js +335 -0
  52. package/lib/types/servers/inbound-manager.js.map +1 -0
  53. package/lib/types/servers/outbound-manager.d.ts +129 -0
  54. package/lib/types/servers/outbound-manager.d.ts.map +1 -0
  55. package/lib/types/servers/outbound-manager.js +238 -0
  56. package/lib/types/servers/outbound-manager.js.map +1 -0
  57. package/lib/types/service.d.ts +104 -40
  58. package/lib/types/service.d.ts.map +1 -1
  59. package/lib/types/service.js +36 -27
  60. package/lib/types/service.js.map +1 -1
  61. package/package.json +2 -2
  62. package/src/api.ts +65 -53
  63. package/src/client/index.ts +272 -148
  64. package/src/commands.ts +59 -43
  65. package/src/events.ts +14 -7
  66. package/src/index.ts +203 -310
  67. package/src/outbound/calls.ts +8 -6
  68. package/src/protocol.ts +204 -95
  69. package/src/server/a2a-server.ts +9 -2
  70. package/src/server/card.ts +13 -62
  71. package/src/server/exec/agent-runtime.ts +20 -2
  72. package/src/server/routes.ts +6 -4
  73. package/src/server/store.ts +42 -3
  74. package/src/servers/inbound-manager.ts +416 -0
  75. package/src/servers/outbound-manager.ts +289 -0
  76. package/src/service.ts +125 -40
  77. package/lib/tsconfig.client.tsbuildinfo +0 -1
  78. package/lib/tsconfig.tsbuildinfo +0 -1
@@ -1,16 +1,18 @@
1
1
  /**
2
2
  * A2A settings dashboard — browser half.
3
3
  *
4
- * Registers one `settings.section` ("A2A 连接") listing who is connected TO
5
- * this DSH (inbound server state + tasks) and who this DSH is connected TO
6
- * (outbound agents), with per-agent controls (enable/disable/remove/refresh)
7
- * and the inbound-server toggle. All data and controls live behind the host's
8
- * loopback-only `/a2a/api` route (src/api.ts); this half is a thin read/write
9
- * client with no protocol knowledge and no direct storage access.
4
+ * Registers one `settings.section` ("A2A 连接") managing the multi-instance
5
+ * A2A composition: inbound server instances (create/start/stop/remove/edit,
6
+ * each with its own agent preset, creator-declared skill declarations, and
7
+ * auth env) and outbound server connections (create/start/stop/remove/refresh,
8
+ * each with a remote URL and optional preset). All data and controls live
9
+ * behind the host's loopback-only `/a2a/api` route (src/api.ts); this half is
10
+ * a thin read/write client with no protocol knowledge and no direct storage
11
+ * access.
10
12
  *
11
13
  * Styling: one idempotent `<style>` injector built on the product's
12
14
  * `--dsw-alias-*` design tokens so light/dark themes follow the DSH shell.
13
- * No inline styles.
15
+ * No inline styles beyond fixed-width inputs.
14
16
  * @module dsh-a2a/client
15
17
  */
16
18
 
@@ -33,7 +35,7 @@ function injectStyles(): () => void {
33
35
  .dsh-a2a-panel { display: flex; flex-direction: column; gap: 16px; padding: 12px 0; }
34
36
  .dsh-a2a-card { border: 1px solid var(--dsw-alias-border, #d0d7de); border-radius: 8px; padding: 12px 14px; }
35
37
  .dsh-a2a-card h3 { margin: 0 0 8px; font-size: 14px; }
36
- .dsh-a2a-row { display: flex; align-items: center; gap: 8px; margin: 4px 0; }
38
+ .dsh-a2a-row { display: flex; align-items: center; gap: 8px; margin: 4px 0; flex-wrap: wrap; }
37
39
  .dsh-a2a-row .muted { color: var(--dsw-alias-text-secondary, #59636e); font-size: 12px; }
38
40
  .dsh-a2a-row button { padding: 2px 10px; border-radius: 6px; border: 1px solid var(--dsw-alias-border, #d0d7de); background: transparent; cursor: pointer; }
39
41
  .dsh-a2a-row button.primary { background: var(--dsw-alias-accent, #0f6); color: var(--dsw-alias-bg, #fff); border-color: transparent; }
@@ -41,10 +43,13 @@ function injectStyles(): () => void {
41
43
  .dsh-a2a-state.ok { background: #1a7f3722; color: #1a7f37; }
42
44
  .dsh-a2a-state.bad { background: #d1242f22; color: #d1242f; }
43
45
  .dsh-a2a-form { display: flex; gap: 6px; margin: 8px 0; flex-wrap: wrap; }
44
- .dsh-a2a-form input { padding: 4px 8px; border-radius: 6px; border: 1px solid var(--dsw-alias-border, #d0d7de); background: transparent; color: inherit; }
46
+ .dsh-a2a-form input, .dsh-a2a-form select, .dsh-a2a-form textarea { padding: 4px 8px; border-radius: 6px; border: 1px solid var(--dsw-alias-border, #d0d7de); background: transparent; color: inherit; }
47
+ .dsh-a2a-form textarea { font-family: inherit; width: 100%; min-height: 64px; resize: vertical; }
45
48
  .dsh-a2a-table { width: 100%; border-collapse: collapse; font-size: 13px; }
46
- .dsh-a2a-table th, .dsh-a2a-table td { text-align: left; padding: 4px 8px; border-bottom: 1px solid var(--dsw-alias-border, #d0d7de); }
49
+ .dsh-a2a-table th, .dsh-a2a-table td { text-align: left; padding: 4px 8px; border-bottom: 1px solid var(--dsw-alias-border, #d0d7de); vertical-align: top; }
50
+ .dsh-a2a-skill { display: inline-block; margin: 1px 4px 1px 0; padding: 0 6px; border-radius: 10px; background: #6e778122; font-size: 12px; }
47
51
  .dsh-a2a-error { color: #d1242f; font-size: 12px; }
52
+ .dsh-a2a-hint { color: var(--dsw-alias-text-secondary, #59636e); font-size: 12px; margin: 2px 0 6px; }
48
53
  `
49
54
  document.head.appendChild(tag)
50
55
  return () => tag.remove()
@@ -53,8 +58,7 @@ function injectStyles(): () => void {
53
58
  export function apply(ctx: ClientContext): void {
54
59
  ctx.effect(() => injectStyles(), `${NS}: dashboard styles`)
55
60
  // settings.section is declared at runtime by ui-settings-general; slots.inject
56
- // defers registration until that declaration exists and follows its lifetime
57
- // (a bare register before the declaration can fail or land invisible).
61
+ // defers registration until that declaration exists and follows its lifetime.
58
62
  ctx.slots.inject('settings.section', () => ctx.slots.register(
59
63
  {
60
64
  name: 'settings.section',
@@ -67,37 +71,59 @@ export function apply(ctx: ClientContext): void {
67
71
  ))
68
72
  }
69
73
 
70
- /** Wire types — mirror the host's src/api.ts snapshot. */
71
- export interface ApiSnapshot {
72
- readonly server: {
73
- readonly enabled: boolean
74
- readonly cardUrl?: string
75
- readonly skills: readonly string[]
76
- readonly name?: string
77
- readonly description?: string
78
- readonly version?: string
79
- readonly configured: boolean
80
- }
81
- readonly tasks: readonly unknown[]
82
- readonly agents: readonly unknown[]
83
- readonly inbounds: readonly unknown[]
74
+ /** Wire types — mirror the host's src/api.ts + src/service.ts views. */
75
+ export interface SkillView {
76
+ readonly id: string
77
+ readonly name: string
78
+ readonly description?: string
84
79
  }
85
80
 
86
- interface AgentView {
81
+ export interface InboundServerView {
82
+ readonly id: string
83
+ readonly name: string
84
+ readonly description: string
85
+ readonly version: string
86
+ readonly endpointPath: string
87
+ readonly preset?: string
88
+ readonly authTokenEnv?: string
89
+ readonly cardPath: string
90
+ readonly cardUrl?: string
91
+ readonly enabled: boolean
92
+ readonly skills: readonly SkillView[]
93
+ }
94
+
95
+ export interface OutboundServerView {
87
96
  readonly id: string
88
97
  readonly name: string
89
98
  readonly agentCardUrl: string
99
+ readonly preset?: string
90
100
  readonly enabled: boolean
101
+ readonly timeoutMs: number
91
102
  readonly state: string
92
103
  readonly skillCount: number
93
104
  readonly toolCount: number
94
105
  readonly lastError?: string
95
106
  }
96
107
 
108
+ export interface PresetView {
109
+ readonly id: string
110
+ readonly name?: string
111
+ readonly description?: string
112
+ /** Whether this preset is the deployment default when none is named. */
113
+ readonly isDefault?: boolean
114
+ }
115
+
116
+ export interface ApiSnapshot {
117
+ readonly inbounds: readonly InboundServerView[]
118
+ readonly outbounds: readonly OutboundServerView[]
119
+ readonly tasks: readonly unknown[]
120
+ readonly peers: readonly unknown[]
121
+ }
122
+
97
123
  interface InboundPeerView {
98
124
  readonly id: string
99
125
  readonly label: string
100
- readonly source?: string
126
+ readonly source?: string | null
101
127
  readonly firstSeen?: string
102
128
  readonly lastSeen?: string
103
129
  readonly taskCount: number
@@ -118,6 +144,12 @@ async function fetchSnapshot(): Promise<ApiSnapshot> {
118
144
  return (await res.json()) as ApiSnapshot
119
145
  }
120
146
 
147
+ async function fetchPresets(): Promise<PresetView[]> {
148
+ const res = await fetch('/a2a/api/presets', { headers: { accept: 'application/json' } })
149
+ if (!res.ok) return []
150
+ return (await res.json()) as PresetView[]
151
+ }
152
+
121
153
  async function postControl(payload: Record<string, unknown>): Promise<{ readonly ok: boolean; readonly message: string }> {
122
154
  const res = await fetch('/a2a/api', {
123
155
  method: 'POST',
@@ -132,29 +164,38 @@ type SectionProps = PropsRuntime<'settings.section'>
132
164
 
133
165
  export function A2aSection(_props: SectionProps): ReactElement {
134
166
  const [snap, setSnap] = useState<ApiSnapshot | undefined>(undefined)
167
+ const [presets, setPresets] = useState<readonly PresetView[]>([])
135
168
  const [error, setError] = useState<string | undefined>(undefined)
136
- const [agentName, setAgentName] = useState('')
137
- const [agentUrl, setAgentUrl] = useState('')
138
169
  const [busy, setBusy] = useState(false)
139
- const [editing, setEditing] = useState(false)
140
- const [idName, setIdName] = useState('')
141
- const [idDescription, setIdDescription] = useState('')
142
- const [idVersion, setIdVersion] = useState('')
170
+
171
+ // Inbound create/edit form state.
172
+ const [editingIn, setEditingIn] = useState<string | undefined>(undefined)
173
+ const [inName, setInName] = useState('')
174
+ const [inDesc, setInDesc] = useState('')
175
+ const [inVersion, setInVersion] = useState('1.0.0')
176
+ const [inPreset, setInPreset] = useState('')
177
+ const [inAuthEnv, setInAuthEnv] = useState('')
178
+
179
+ // Outbound create form state.
180
+ const [outName, setOutName] = useState('')
181
+ const [outUrl, setOutUrl] = useState('')
182
+ const [outPreset, setOutPreset] = useState('')
183
+ const [outTokenEnv, setOutTokenEnv] = useState('')
184
+ const [outTimeout, setOutTimeout] = useState('60000')
143
185
 
144
186
  useEffect(() => {
145
187
  let alive = true
146
188
  const tick = async (): Promise<void> => {
147
189
  try {
148
- const next = await fetchSnapshot()
190
+ const [next, presetRows] = await Promise.all([fetchSnapshot(), fetchPresets()])
149
191
  if (alive) {
150
192
  setSnap(next)
193
+ setPresets(presetRows)
194
+ // Default an unset preset picker to the deployment default (if any).
195
+ const def = presetRows.find((p) => p.isDefault)?.id
196
+ setInPreset((current) => current === '' && def !== undefined ? def : current)
197
+ setOutPreset((current) => current === '' && def !== undefined ? def : current)
151
198
  setError(undefined)
152
- // Seed the identity form from the live card when not editing.
153
- if (!editing) {
154
- setIdName(next.server.name ?? '')
155
- setIdDescription(next.server.description ?? '')
156
- setIdVersion(next.server.version ?? '')
157
- }
158
199
  }
159
200
  } catch (err) {
160
201
  if (alive) setError((err as Error).message)
@@ -181,9 +222,69 @@ export function A2aSection(_props: SectionProps): ReactElement {
181
222
  }
182
223
  }
183
224
 
184
- const server = snap?.server
185
- const agents = (snap?.agents ?? []) as readonly AgentView[]
186
- const inbounds = (snap?.inbounds ?? []) as readonly InboundPeerView[]
225
+ const startCreateInbound = (): void => {
226
+ setEditingIn(undefined)
227
+ setInName('')
228
+ setInDesc('')
229
+ setInVersion('1.0.0')
230
+ setInPreset(presets.find((p) => p.isDefault)?.id ?? '')
231
+ setInAuthEnv('')
232
+ }
233
+
234
+ const submitInbound = (): void => {
235
+ if (!inName.trim()) return
236
+ if (editingIn !== undefined) {
237
+ void control({
238
+ action: 'inbound.update',
239
+ id: editingIn,
240
+ name: inName.trim(),
241
+ description: inDesc.trim(),
242
+ version: inVersion.trim() || '1.0.0',
243
+ ...(inPreset !== '' ? { preset: inPreset } : {}),
244
+ ...(inAuthEnv.trim() !== '' ? { authTokenEnv: inAuthEnv.trim() } : {}),
245
+ }).then(() => setEditingIn(undefined))
246
+ return
247
+ }
248
+ void control({
249
+ action: 'inbound.create',
250
+ name: inName.trim(),
251
+ description: inDesc.trim() || 'An A2A inbound server',
252
+ version: inVersion.trim() || '1.0.0',
253
+ ...(inPreset !== '' ? { preset: inPreset } : {}),
254
+ ...(inAuthEnv.trim() !== '' ? { authTokenEnv: inAuthEnv.trim() } : {}),
255
+ }).then(() => startCreateInbound())
256
+ }
257
+
258
+ const beginEditInbound = (v: InboundServerView): void => {
259
+ setEditingIn(v.id)
260
+ setInName(v.name)
261
+ setInDesc(v.description)
262
+ setInVersion(v.version)
263
+ setInPreset(v.preset ?? presets.find((p) => p.isDefault)?.id ?? '')
264
+ setInAuthEnv(v.authTokenEnv ?? '')
265
+ }
266
+
267
+ const submitOutbound = (): void => {
268
+ if (!outName.trim() || !outUrl.trim()) return
269
+ const timeout = Number.parseInt(outTimeout, 10)
270
+ void control({
271
+ action: 'outbound.create',
272
+ name: outName.trim(),
273
+ agentCardUrl: outUrl.trim(),
274
+ ...(outPreset !== '' ? { preset: outPreset } : {}),
275
+ ...(outTokenEnv.trim() !== '' ? { bearerTokenEnv: outTokenEnv.trim() } : {}),
276
+ ...(Number.isFinite(timeout) && timeout > 0 ? { timeoutMs: timeout } : {}),
277
+ }).then(() => {
278
+ setOutName('')
279
+ setOutUrl('')
280
+ setOutPreset(presets.find((p) => p.isDefault)?.id ?? '')
281
+ setOutTokenEnv('')
282
+ })
283
+ }
284
+
285
+ const inbounds = snap?.inbounds ?? []
286
+ const outbounds = snap?.outbounds ?? []
287
+ const peers = (snap?.peers ?? []) as readonly InboundPeerView[]
187
288
  const tasks = (snap?.tasks ?? []) as readonly TaskView[]
188
289
 
189
290
  return createElement(
@@ -191,170 +292,193 @@ export function A2aSection(_props: SectionProps): ReactElement {
191
292
  { className: 'dsh-a2a-panel' },
192
293
  error !== undefined ? createElement('div', { className: 'dsh-a2a-error' }, error) : null,
193
294
 
194
- // ── inbound server ───────────────────────────────────────────────────
295
+ // ── inbound server instances ────────────────────────────────────────
195
296
  createElement(
196
297
  'div',
197
298
  { className: 'dsh-a2a-card' },
198
- createElement('h3', null, '入站 Server'),
199
- server === undefined
200
- ? createElement('div', { className: 'muted' }, '加载中…')
299
+ createElement('h3', null, `入站 Servers(${inbounds.length})`),
300
+ inbounds.length === 0
301
+ ? createElement('div', { className: 'muted' }, '还没有入站 server。创建一个即可对外发布 A2A 端点。')
201
302
  : createElement(
202
- 'div',
203
- { className: 'dsh-a2a-row' },
204
- createElement(
205
- 'span',
206
- { className: `dsh-a2a-state ${server.enabled ? 'ok' : 'bad'}` },
207
- server.enabled ? '已启用' : '已停用',
303
+ 'table',
304
+ { className: 'dsh-a2a-table' },
305
+ createElement('thead', null,
306
+ createElement('tr', null,
307
+ createElement('th', null, '名称 / 端点'),
308
+ createElement('th', null, 'Preset'),
309
+ createElement('th', null, '技能宣告'),
310
+ createElement('th', null, '状态'),
311
+ createElement('th', null, '操作'),
312
+ ),
208
313
  ),
209
- createElement(
210
- 'button',
211
- {
212
- className: server.enabled ? '' : 'primary',
213
- disabled: busy,
214
- onClick: () => void control({ action: server.enabled ? 'server.disable' : 'server.enable' }),
215
- },
216
- server.enabled ? '停用' : '启用',
314
+ createElement('tbody', null,
315
+ ...inbounds.map((v) =>
316
+ createElement('tr', { key: v.id },
317
+ createElement('td', null,
318
+ createElement('div', null, v.name),
319
+ createElement('div', { className: 'muted' }, `${v.endpointPath} · ${v.cardUrl ?? v.cardPath}`),
320
+ v.authTokenEnv !== undefined && v.authTokenEnv !== ''
321
+ ? createElement('div', { className: 'muted' }, `鉴权: $${v.authTokenEnv}`)
322
+ : null,
323
+ ),
324
+ createElement('td', null, v.preset ?? createElement('span', { className: 'muted' }, '默认')),
325
+ createElement('td', null,
326
+ createElement('div', null,
327
+ ...v.skills.map((s) =>
328
+ createElement('span', { className: 'dsh-a2a-skill', key: s.id, title: s.description ?? '' }, s.name),
329
+ ),
330
+ ),
331
+ ),
332
+ createElement('td', null,
333
+ createElement('span', { className: `dsh-a2a-state ${v.enabled ? 'ok' : 'bad'}` }, v.enabled ? '运行中' : '已停用'),
334
+ ),
335
+ createElement('td', null,
336
+ createElement('button', { disabled: busy, onClick: () => void control({ action: v.enabled ? 'inbound.disable' : 'inbound.enable', id: v.id }) }, v.enabled ? '停用' : '启用'),
337
+ ' ',
338
+ createElement('button', { disabled: busy, onClick: () => beginEditInbound(v) }, '编辑'),
339
+ ' ',
340
+ createElement('button', { disabled: busy, onClick: () => void control({ action: 'inbound.remove', id: v.id }) }, '删除'),
341
+ ),
342
+ ),
343
+ ),
217
344
  ),
218
345
  ),
219
- server !== undefined && server.enabled
220
- ? createElement(
221
- 'div',
222
- { className: 'dsh-a2a-row muted' },
223
- `AgentCard: ${server.cardUrl ?? '(n/a)'} · 技能: ${server.skills.join(', ') || '(chat)'}`,
224
- )
225
- : null,
226
- ),
227
-
228
- // ── service identity (guided first-run + edit) ───────────────────────
229
- createElement(
230
- 'div',
231
- { className: 'dsh-a2a-card' },
232
- server === undefined || (server as { configured?: boolean }).configured === false
233
- ? createElement('h3', null, '服务身份(首次配置)')
234
- : createElement('h3', null, '服务身份'),
346
+ createElement('h3', { style: { marginTop: '12px' } }, editingIn !== undefined ? '编辑入站 Server' : '新建入站 Server'),
347
+ createElement(
348
+ 'form',
349
+ {
350
+ className: 'dsh-a2a-form',
351
+ onSubmit: (ev: { preventDefault(): void }) => {
352
+ ev.preventDefault()
353
+ submitInbound()
354
+ },
355
+ },
356
+ createElement('input', { value: inName, placeholder: '名称', onChange: (e: { target: { value: string } }) => setInName(e.target.value) }),
357
+ createElement('input', { value: inVersion, placeholder: '版本', onChange: (e: { target: { value: string } }) => setInVersion(e.target.value), style: { width: '70px' } }),
358
+ createElement('select', { value: inPreset, onChange: (e: { target: { value: string } }) => setInPreset(e.target.value) },
359
+ ...presets.map((p) => createElement('option', { value: p.id, key: p.id, title: p.description ?? p.id }, p.name ?? p.id)),
360
+ ),
361
+ createElement('input', { value: inAuthEnv, placeholder: '鉴权 env 变量名(可选)', onChange: (e: { target: { value: string } }) => setInAuthEnv(e.target.value) }),
362
+ ),
235
363
  createElement(
236
364
  'form',
237
365
  {
238
366
  className: 'dsh-a2a-form',
239
367
  onSubmit: (ev: { preventDefault(): void }) => {
240
368
  ev.preventDefault()
241
- void control({ action: 'identity.update', name: idName, description: idDescription, version: idVersion }).then(() => {
242
- setEditing(false)
243
- })
369
+ submitInbound()
244
370
  },
371
+ style: { alignItems: 'flex-end' },
245
372
  },
246
- createElement('input', { value: idVersion, placeholder: '版本', onChange: (e: { target: { value: string } }) => setIdVersion(e.target.value), style: { width: '60px' } }),
247
- createElement('input', { value: idName, placeholder: '服务名称', onChange: (e: { target: { value: string } }) => setIdName(e.target.value), style: { width: '160px' } }),
248
- createElement('input', { value: idDescription, placeholder: '服务描述', onChange: (e: { target: { value: string } }) => setIdDescription(e.target.value), style: { width: '260px' } }),
249
- createElement('button', { type: 'submit', className: 'primary', disabled: busy }, '保存身份'),
373
+ createElement('input', { value: inDesc, placeholder: '描述', onChange: (e: { target: { value: string } }) => setInDesc(e.target.value), style: { flex: 1, minWidth: '260px' } }),
374
+ createElement('button', { type: 'submit', className: 'primary', disabled: busy || !inName.trim() }, editingIn !== undefined ? '保存修改' : '创建 Server'),
375
+ editingIn !== undefined ? createElement('button', { type: 'button', disabled: busy, onClick: () => startCreateInbound() }, '取消') : null,
250
376
  ),
251
- createElement('div', { className: 'dsh-a2a-row muted' }, '名称、描述、版本即刻重建 AgentCard;技能始终从工具注册表派生。'),
377
+ createElement('div', { className: 'dsh-a2a-hint' }, ' server 绑定的 preset 决定其技能(一切皆插件):AgentCard 技能 = 该 preset 可用的模型技能,创建后自动派生。'),
252
378
  ),
253
379
 
254
- // ── inbound peers monitoring ─────────────────────────────────────────
380
+ // ── outbound server instances ───────────────────────────────────────
255
381
  createElement(
256
382
  'div',
257
383
  { className: 'dsh-a2a-card' },
258
- createElement('h3', null, `入站连接(${inbounds.length})`),
259
- inbounds.length === 0
260
- ? createElement('div', { className: 'muted' }, '当前无远程对端正在调用本服务。')
384
+ createElement('h3', null, `出站 Servers(${outbounds.length})`),
385
+ outbounds.length === 0
386
+ ? createElement('div', { className: 'muted' }, '还没有出站 server。添加一个远端 A2A 连接即可把其技能注册为模型工具。')
261
387
  : createElement(
262
388
  'table',
263
389
  { className: 'dsh-a2a-table' },
264
390
  createElement('thead', null,
265
391
  createElement('tr', null,
266
- createElement('th', null, '来源'),
267
- createElement('th', null, '任务'),
268
- createElement('th', null, '流式'),
269
- createElement('th', null, '首次 / 最近'),
392
+ createElement('th', null, '名称 / 远端'),
393
+ createElement('th', null, 'Preset'),
394
+ createElement('th', null, '状态'),
395
+ createElement('th', null, '工具'),
270
396
  createElement('th', null, '操作'),
271
397
  ),
272
398
  ),
273
399
  createElement('tbody', null,
274
- ...inbounds.map((p) =>
275
- createElement('tr', { key: p.id },
276
- createElement('td', null, p.source ?? p.label),
277
- createElement('td', null, `${p.taskCount}(活跃 ${p.activeTaskIds.length})`),
278
- createElement('td', null, p.streaming ? '●' : '—'),
400
+ ...outbounds.map((v) =>
401
+ createElement('tr', { key: v.id },
279
402
  createElement('td', null,
280
- createElement('span', { className: 'muted' }, `${p.firstSeen ?? ''} / ${p.lastSeen ?? ''}`),
403
+ createElement('div', null, v.name),
404
+ createElement('div', { className: 'muted' }, v.agentCardUrl),
405
+ v.lastError !== undefined ? createElement('div', { className: 'dsh-a2a-error' }, v.lastError) : null,
281
406
  ),
407
+ createElement('td', null, v.preset ?? createElement('span', { className: 'muted' }, '默认')),
282
408
  createElement('td', null,
283
- createElement('button', { disabled: busy, onClick: () => void control({ action: 'inbound.close', id: p.id }) }, '关闭'),
409
+ createElement('span', { className: `dsh-a2a-state ${v.state === 'connected' ? 'ok' : 'bad'}` }, v.state === 'connected' ? '已连接' : v.state),
410
+ ),
411
+ createElement('td', null, `${v.toolCount} / ${v.skillCount}`),
412
+ createElement('td', null,
413
+ createElement('button', { disabled: busy, onClick: () => void control({ action: v.enabled ? 'outbound.disable' : 'outbound.enable', id: v.id }) }, v.enabled ? '停用' : '启用'),
414
+ ' ',
415
+ createElement('button', { disabled: busy, onClick: () => void control({ action: 'outbound.refresh', id: v.id }) }, '刷新'),
416
+ ' ',
417
+ createElement('button', { disabled: busy, onClick: () => void control({ action: 'outbound.remove', id: v.id }) }, '删除'),
284
418
  ),
285
419
  ),
286
420
  ),
287
421
  ),
288
422
  ),
423
+ createElement('h3', { style: { marginTop: '12px' } }, '新建出站 Server'),
424
+ createElement(
425
+ 'form',
426
+ {
427
+ className: 'dsh-a2a-form',
428
+ onSubmit: (ev: { preventDefault(): void }) => {
429
+ ev.preventDefault()
430
+ submitOutbound()
431
+ },
432
+ },
433
+ createElement('input', { value: outName, placeholder: '名称', onChange: (e: { target: { value: string } }) => setOutName(e.target.value) }),
434
+ createElement('input', { value: outUrl, placeholder: '远端 AgentCard URL', onChange: (e: { target: { value: string } }) => setOutUrl(e.target.value), style: { flex: 1, minWidth: '240px' } }),
435
+ createElement('select', { value: outPreset, onChange: (e: { target: { value: string } }) => setOutPreset(e.target.value) },
436
+ ...presets.map((p) => createElement('option', { value: p.id, key: p.id, title: p.description ?? p.id }, p.name ?? p.id)),
437
+ ),
438
+ createElement('input', { value: outTokenEnv, placeholder: 'Bearer env 变量名(可选)', onChange: (e: { target: { value: string } }) => setOutTokenEnv(e.target.value) }),
439
+ createElement('input', { value: outTimeout, placeholder: '超时 ms', onChange: (e: { target: { value: string } }) => setOutTimeout(e.target.value), style: { width: '90px' } }),
440
+ createElement('button', { type: 'submit', className: 'primary', disabled: busy || !outName.trim() || !outUrl.trim() }, '添加出站 Server'),
441
+ ),
289
442
  ),
290
443
 
291
- // ── outbound agents ──────────────────────────────────────────────────
444
+ // ── inbound peer monitoring ─────────────────────────────────────────
292
445
  createElement(
293
446
  'div',
294
447
  { className: 'dsh-a2a-card' },
295
- createElement('h3', null, `出站 Agents(${agents.length})`),
296
- agents.length === 0
297
- ? createElement('div', { className: 'muted' }, '未配置远程 agent。')
448
+ createElement('h3', null, `入站连接(${peers.length})`),
449
+ peers.length === 0
450
+ ? createElement('div', { className: 'muted' }, '当前无远程对端正在调用本服务。')
298
451
  : createElement(
299
452
  'table',
300
453
  { className: 'dsh-a2a-table' },
301
- createElement(
302
- 'thead',
303
- null,
454
+ createElement('thead', null,
304
455
  createElement('tr', null,
305
- createElement('th', null, '名称'),
306
- createElement('th', null, '状态'),
307
- createElement('th', null, '工具'),
456
+ createElement('th', null, '来源'),
457
+ createElement('th', null, '任务'),
458
+ createElement('th', null, '流式'),
459
+ createElement('th', null, '首次 / 最近'),
308
460
  createElement('th', null, '操作'),
309
461
  ),
310
462
  ),
311
- createElement(
312
- 'tbody',
313
- null,
314
- ...agents.map((a) =>
315
- createElement(
316
- 'tr',
317
- { key: a.id },
318
- createElement('td', null,
319
- createElement('div', null, a.name),
320
- createElement('div', { className: 'muted' }, a.agentCardUrl),
321
- a.lastError !== undefined ? createElement('div', { className: 'dsh-a2a-error' }, a.lastError) : null,
322
- ),
463
+ createElement('tbody', null,
464
+ ...peers.map((p) =>
465
+ createElement('tr', { key: p.id },
466
+ createElement('td', null, p.source ?? p.label),
467
+ createElement('td', null, `${p.taskCount}(活跃 ${p.activeTaskIds.length})`),
468
+ createElement('td', null, p.streaming ? '●' : '—'),
323
469
  createElement('td', null,
324
- createElement('span', { className: `dsh-a2a-state ${a.state === 'connected' ? 'ok' : 'bad'}` }, a.state),
470
+ createElement('span', { className: 'muted' }, `${p.firstSeen ?? ''} / ${p.lastSeen ?? ''}`),
325
471
  ),
326
- createElement('td', null, `${a.toolCount} / ${a.skillCount}`),
327
472
  createElement('td', null,
328
- createElement('button', { disabled: busy, onClick: () => void control({ action: a.enabled ? 'agent.disable' : 'agent.enable', id: a.id }) }, a.enabled ? '停用' : '启用'),
329
- ' ',
330
- createElement('button', { disabled: busy, onClick: () => void control({ action: 'agent.refresh', id: a.id }) }, '刷新'),
331
- ' ',
332
- createElement('button', { disabled: busy, onClick: () => void control({ action: 'agent.remove', id: a.id }) }, '删除'),
473
+ createElement('button', { disabled: busy, onClick: () => void control({ action: 'inbound.close', id: p.id }) }, '关闭'),
333
474
  ),
334
475
  ),
335
476
  ),
336
477
  ),
337
478
  ),
338
- createElement(
339
- 'form',
340
- {
341
- className: 'dsh-a2a-form',
342
- onSubmit: (ev: { preventDefault(): void }) => {
343
- ev.preventDefault()
344
- if (!agentName.trim() || !agentUrl.trim()) return
345
- void control({ action: 'agent.add', name: agentName.trim(), agentCardUrl: agentUrl.trim() }).then(() => {
346
- setAgentName('')
347
- setAgentUrl('')
348
- })
349
- },
350
- },
351
- createElement('input', { value: agentName, placeholder: '名称', onChange: (e: { target: { value: string } }) => setAgentName(e.target.value) }),
352
- createElement('input', { value: agentUrl, placeholder: 'AgentCard URL', onChange: (e: { target: { value: string } }) => setAgentUrl(e.target.value) }),
353
- createElement('button', { type: 'submit', className: 'primary', disabled: busy }, '添加 Agent'),
354
- ),
355
479
  ),
356
480
 
357
- // ── tasks ────────────────────────────────────────────────────────────
481
+ // ── tasks ───────────────────────────────────────────────────────────
358
482
  createElement(
359
483
  'div',
360
484
  { className: 'dsh-a2a-card' },