@hanphone/dsh-a2a 0.2.0 → 0.3.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.
Files changed (78) hide show
  1. package/README.md +101 -106
  2. package/README.zh.md +50 -74
  3. package/cordis.patch.yml +4 -7
  4. package/lib/client.js +217 -73
  5. package/lib/index.js +1258 -867
  6. package/lib/types/api.d.ts +40 -23
  7. package/lib/types/api.d.ts.map +1 -1
  8. package/lib/types/api.js +58 -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 +173 -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 +3 -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 +35 -1
  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 +131 -0
  50. package/lib/types/servers/inbound-manager.d.ts.map +1 -0
  51. package/lib/types/servers/inbound-manager.js +312 -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 +106 -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 +67 -53
  63. package/src/client/index.ts +309 -148
  64. package/src/commands.ts +59 -43
  65. package/src/events.ts +14 -7
  66. package/src/index.ts +199 -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 +5 -2
  72. package/src/server/routes.ts +6 -4
  73. package/src/server/store.ts +45 -4
  74. package/src/servers/inbound-manager.ts +385 -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',
@@ -128,33 +160,58 @@ async function postControl(payload: Record<string, unknown>): Promise<{ readonly
128
160
  return body
129
161
  }
130
162
 
163
+ /** One skill declaration per line: `id|name|description` (name/description default to id). */
164
+ function parseSkills(text: string): SkillView[] | undefined {
165
+ const lines = text.split('\n').map((l) => l.trim()).filter(Boolean)
166
+ if (lines.length === 0) return undefined
167
+ return lines.map((line) => {
168
+ const [id, name, ...descParts] = line.split('|').map((p) => p.trim())
169
+ const skillId = id ?? ''
170
+ return {
171
+ id: skillId,
172
+ name: name && name.length > 0 ? name : skillId,
173
+ ...(descParts.length > 0 || (descParts[0]?.length ?? 0) > 0 ? { description: descParts.join('|') } : {}),
174
+ }
175
+ })
176
+ }
177
+
178
+ function skillsToText(skills: readonly SkillView[]): string {
179
+ return skills.map((s) => [s.id, s.name, s.description].filter((p) => p !== undefined && p.length > 0).join('|')).join('\n')
180
+ }
181
+
131
182
  type SectionProps = PropsRuntime<'settings.section'>
132
183
 
133
184
  export function A2aSection(_props: SectionProps): ReactElement {
134
185
  const [snap, setSnap] = useState<ApiSnapshot | undefined>(undefined)
186
+ const [presets, setPresets] = useState<readonly PresetView[]>([])
135
187
  const [error, setError] = useState<string | undefined>(undefined)
136
- const [agentName, setAgentName] = useState('')
137
- const [agentUrl, setAgentUrl] = useState('')
138
188
  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('')
189
+
190
+ // Inbound create/edit form state.
191
+ const [editingIn, setEditingIn] = useState<string | undefined>(undefined)
192
+ const [inName, setInName] = useState('')
193
+ const [inDesc, setInDesc] = useState('')
194
+ const [inVersion, setInVersion] = useState('1.0.0')
195
+ const [inPreset, setInPreset] = useState('')
196
+ const [inAuthEnv, setInAuthEnv] = useState('')
197
+ const [inSkills, setInSkills] = useState('')
198
+
199
+ // Outbound create form state.
200
+ const [outName, setOutName] = useState('')
201
+ const [outUrl, setOutUrl] = useState('')
202
+ const [outPreset, setOutPreset] = useState('')
203
+ const [outTokenEnv, setOutTokenEnv] = useState('')
204
+ const [outTimeout, setOutTimeout] = useState('60000')
143
205
 
144
206
  useEffect(() => {
145
207
  let alive = true
146
208
  const tick = async (): Promise<void> => {
147
209
  try {
148
- const next = await fetchSnapshot()
210
+ const [next, presetRows] = await Promise.all([fetchSnapshot(), fetchPresets()])
149
211
  if (alive) {
150
212
  setSnap(next)
213
+ setPresets(presetRows)
151
214
  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
215
  }
159
216
  } catch (err) {
160
217
  if (alive) setError((err as Error).message)
@@ -181,9 +238,74 @@ export function A2aSection(_props: SectionProps): ReactElement {
181
238
  }
182
239
  }
183
240
 
184
- const server = snap?.server
185
- const agents = (snap?.agents ?? []) as readonly AgentView[]
186
- const inbounds = (snap?.inbounds ?? []) as readonly InboundPeerView[]
241
+ const startCreateInbound = (): void => {
242
+ setEditingIn(undefined)
243
+ setInName('')
244
+ setInDesc('')
245
+ setInVersion('1.0.0')
246
+ setInPreset('')
247
+ setInAuthEnv('')
248
+ setInSkills('')
249
+ }
250
+
251
+ const submitInbound = (): void => {
252
+ if (!inName.trim()) return
253
+ const skills = parseSkills(inSkills)
254
+ if (editingIn !== undefined) {
255
+ void control({
256
+ action: 'inbound.update',
257
+ id: editingIn,
258
+ name: inName.trim(),
259
+ description: inDesc.trim(),
260
+ version: inVersion.trim() || '1.0.0',
261
+ ...(inPreset !== '' ? { preset: inPreset } : {}),
262
+ ...(inAuthEnv.trim() !== '' ? { authTokenEnv: inAuthEnv.trim() } : {}),
263
+ ...(skills !== undefined ? { skills } : {}),
264
+ }).then(() => setEditingIn(undefined))
265
+ return
266
+ }
267
+ void control({
268
+ action: 'inbound.create',
269
+ name: inName.trim(),
270
+ description: inDesc.trim() || 'An A2A inbound server',
271
+ version: inVersion.trim() || '1.0.0',
272
+ ...(inPreset !== '' ? { preset: inPreset } : {}),
273
+ ...(inAuthEnv.trim() !== '' ? { authTokenEnv: inAuthEnv.trim() } : {}),
274
+ ...(skills !== undefined ? { skills } : {}),
275
+ }).then(() => startCreateInbound())
276
+ }
277
+
278
+ const beginEditInbound = (v: InboundServerView): void => {
279
+ setEditingIn(v.id)
280
+ setInName(v.name)
281
+ setInDesc(v.description)
282
+ setInVersion(v.version)
283
+ setInPreset(v.preset ?? '')
284
+ setInAuthEnv(v.authTokenEnv ?? '')
285
+ setInSkills(skillsToText(v.skills))
286
+ }
287
+
288
+ const submitOutbound = (): void => {
289
+ if (!outName.trim() || !outUrl.trim()) return
290
+ const timeout = Number.parseInt(outTimeout, 10)
291
+ void control({
292
+ action: 'outbound.create',
293
+ name: outName.trim(),
294
+ agentCardUrl: outUrl.trim(),
295
+ ...(outPreset !== '' ? { preset: outPreset } : {}),
296
+ ...(outTokenEnv.trim() !== '' ? { bearerTokenEnv: outTokenEnv.trim() } : {}),
297
+ ...(Number.isFinite(timeout) && timeout > 0 ? { timeoutMs: timeout } : {}),
298
+ }).then(() => {
299
+ setOutName('')
300
+ setOutUrl('')
301
+ setOutPreset('')
302
+ setOutTokenEnv('')
303
+ })
304
+ }
305
+
306
+ const inbounds = snap?.inbounds ?? []
307
+ const outbounds = snap?.outbounds ?? []
308
+ const peers = (snap?.peers ?? []) as readonly InboundPeerView[]
187
309
  const tasks = (snap?.tasks ?? []) as readonly TaskView[]
188
310
 
189
311
  return createElement(
@@ -191,170 +313,209 @@ export function A2aSection(_props: SectionProps): ReactElement {
191
313
  { className: 'dsh-a2a-panel' },
192
314
  error !== undefined ? createElement('div', { className: 'dsh-a2a-error' }, error) : null,
193
315
 
194
- // ── inbound server ───────────────────────────────────────────────────
316
+ // ── inbound server instances ────────────────────────────────────────
195
317
  createElement(
196
318
  'div',
197
319
  { className: 'dsh-a2a-card' },
198
- createElement('h3', null, '入站 Server'),
199
- server === undefined
200
- ? createElement('div', { className: 'muted' }, '加载中…')
320
+ createElement('h3', null, `入站 Servers(${inbounds.length})`),
321
+ inbounds.length === 0
322
+ ? createElement('div', { className: 'muted' }, '还没有入站 server。创建一个即可对外发布 A2A 端点。')
201
323
  : createElement(
202
- 'div',
203
- { className: 'dsh-a2a-row' },
204
- createElement(
205
- 'span',
206
- { className: `dsh-a2a-state ${server.enabled ? 'ok' : 'bad'}` },
207
- server.enabled ? '已启用' : '已停用',
324
+ 'table',
325
+ { className: 'dsh-a2a-table' },
326
+ createElement('thead', null,
327
+ createElement('tr', null,
328
+ createElement('th', null, '名称 / 端点'),
329
+ createElement('th', null, 'Preset'),
330
+ createElement('th', null, '技能宣告'),
331
+ createElement('th', null, '状态'),
332
+ createElement('th', null, '操作'),
333
+ ),
208
334
  ),
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 ? '停用' : '启用',
335
+ createElement('tbody', null,
336
+ ...inbounds.map((v) =>
337
+ createElement('tr', { key: v.id },
338
+ createElement('td', null,
339
+ createElement('div', null, v.name),
340
+ createElement('div', { className: 'muted' }, `${v.endpointPath} · ${v.cardUrl ?? v.cardPath}`),
341
+ v.authTokenEnv !== undefined && v.authTokenEnv !== ''
342
+ ? createElement('div', { className: 'muted' }, `鉴权: $${v.authTokenEnv}`)
343
+ : null,
344
+ ),
345
+ createElement('td', null, v.preset ?? createElement('span', { className: 'muted' }, '默认')),
346
+ createElement('td', null,
347
+ createElement('div', null,
348
+ ...v.skills.map((s) =>
349
+ createElement('span', { className: 'dsh-a2a-skill', key: s.id, title: s.description ?? '' }, s.name),
350
+ ),
351
+ ),
352
+ ),
353
+ createElement('td', null,
354
+ createElement('span', { className: `dsh-a2a-state ${v.enabled ? 'ok' : 'bad'}` }, v.enabled ? '运行中' : '已停用'),
355
+ ),
356
+ createElement('td', null,
357
+ createElement('button', { disabled: busy, onClick: () => void control({ action: v.enabled ? 'inbound.disable' : 'inbound.enable', id: v.id }) }, v.enabled ? '停用' : '启用'),
358
+ ' ',
359
+ createElement('button', { disabled: busy, onClick: () => beginEditInbound(v) }, '编辑'),
360
+ ' ',
361
+ createElement('button', { disabled: busy, onClick: () => void control({ action: 'inbound.remove', id: v.id }) }, '删除'),
362
+ ),
363
+ ),
364
+ ),
217
365
  ),
218
366
  ),
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, '服务身份'),
367
+ createElement('h3', { style: { marginTop: '12px' } }, editingIn !== undefined ? '编辑入站 Server' : '新建入站 Server'),
235
368
  createElement(
236
369
  'form',
237
370
  {
238
371
  className: 'dsh-a2a-form',
239
372
  onSubmit: (ev: { preventDefault(): void }) => {
240
373
  ev.preventDefault()
241
- void control({ action: 'identity.update', name: idName, description: idDescription, version: idVersion }).then(() => {
242
- setEditing(false)
243
- })
374
+ submitInbound()
244
375
  },
245
376
  },
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 }, '保存身份'),
377
+ createElement('input', { value: inName, placeholder: '名称', onChange: (e: { target: { value: string } }) => setInName(e.target.value) }),
378
+ createElement('input', { value: inVersion, placeholder: '版本', onChange: (e: { target: { value: string } }) => setInVersion(e.target.value), style: { width: '70px' } }),
379
+ createElement('select', { value: inPreset, onChange: (e: { target: { value: string } }) => setInPreset(e.target.value) },
380
+ createElement('option', { value: '' }, 'Preset(默认)'),
381
+ ...presets.map((p) => createElement('option', { value: p.id, key: p.id }, `${p.name ?? p.id}${p.isDefault ? '(默认)' : ''}`)),
382
+ ),
383
+ createElement('input', { value: inAuthEnv, placeholder: '鉴权 env 变量名(可选)', onChange: (e: { target: { value: string } }) => setInAuthEnv(e.target.value) }),
250
384
  ),
251
- createElement('div', { className: 'dsh-a2a-row muted' }, '名称、描述、版本即刻重建 AgentCard;技能始终从工具注册表派生。'),
385
+ createElement(
386
+ 'form',
387
+ {
388
+ className: 'dsh-a2a-form',
389
+ onSubmit: (ev: { preventDefault(): void }) => {
390
+ ev.preventDefault()
391
+ submitInbound()
392
+ },
393
+ },
394
+ createElement('textarea', {
395
+ value: inSkills,
396
+ placeholder: '技能宣告(每行 id|名称|描述,留空默认取 preset 名)',
397
+ onChange: (e: { target: { value: string } }) => setInSkills(e.target.value),
398
+ }),
399
+ ),
400
+ createElement(
401
+ 'form',
402
+ {
403
+ className: 'dsh-a2a-form',
404
+ onSubmit: (ev: { preventDefault(): void }) => {
405
+ ev.preventDefault()
406
+ submitInbound()
407
+ },
408
+ },
409
+ createElement('input', { value: inDesc, placeholder: '描述', onChange: (e: { target: { value: string } }) => setInDesc(e.target.value), style: { flex: 1, minWidth: '260px' } }),
410
+ createElement('button', { type: 'submit', className: 'primary', disabled: busy || !inName.trim() }, editingIn !== undefined ? '保存修改' : '创建 Server'),
411
+ editingIn !== undefined ? createElement('button', { type: 'button', disabled: busy, onClick: () => startCreateInbound() }, '取消') : null,
412
+ ),
413
+ createElement('div', { className: 'dsh-a2a-hint' }, '技能宣告由创建者输入,默认取所绑定的 preset 展示名(无 preset 时为内置 chat)。'),
252
414
  ),
253
415
 
254
- // ── inbound peers monitoring ─────────────────────────────────────────
416
+ // ── outbound server instances ───────────────────────────────────────
255
417
  createElement(
256
418
  'div',
257
419
  { className: 'dsh-a2a-card' },
258
- createElement('h3', null, `入站连接(${inbounds.length})`),
259
- inbounds.length === 0
260
- ? createElement('div', { className: 'muted' }, '当前无远程对端正在调用本服务。')
420
+ createElement('h3', null, `出站 Servers(${outbounds.length})`),
421
+ outbounds.length === 0
422
+ ? createElement('div', { className: 'muted' }, '还没有出站 server。添加一个远端 A2A 连接即可把其技能注册为模型工具。')
261
423
  : createElement(
262
424
  'table',
263
425
  { className: 'dsh-a2a-table' },
264
426
  createElement('thead', null,
265
427
  createElement('tr', null,
266
- createElement('th', null, '来源'),
267
- createElement('th', null, '任务'),
268
- createElement('th', null, '流式'),
269
- createElement('th', null, '首次 / 最近'),
428
+ createElement('th', null, '名称 / 远端'),
429
+ createElement('th', null, 'Preset'),
430
+ createElement('th', null, '状态'),
431
+ createElement('th', null, '工具'),
270
432
  createElement('th', null, '操作'),
271
433
  ),
272
434
  ),
273
435
  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 ? '●' : '—'),
436
+ ...outbounds.map((v) =>
437
+ createElement('tr', { key: v.id },
279
438
  createElement('td', null,
280
- createElement('span', { className: 'muted' }, `${p.firstSeen ?? ''} / ${p.lastSeen ?? ''}`),
439
+ createElement('div', null, v.name),
440
+ createElement('div', { className: 'muted' }, v.agentCardUrl),
441
+ v.lastError !== undefined ? createElement('div', { className: 'dsh-a2a-error' }, v.lastError) : null,
281
442
  ),
443
+ createElement('td', null, v.preset ?? createElement('span', { className: 'muted' }, '默认')),
282
444
  createElement('td', null,
283
- createElement('button', { disabled: busy, onClick: () => void control({ action: 'inbound.close', id: p.id }) }, '关闭'),
445
+ createElement('span', { className: `dsh-a2a-state ${v.state === 'connected' ? 'ok' : 'bad'}` }, v.state === 'connected' ? '已连接' : v.state),
446
+ ),
447
+ createElement('td', null, `${v.toolCount} / ${v.skillCount}`),
448
+ createElement('td', null,
449
+ createElement('button', { disabled: busy, onClick: () => void control({ action: v.enabled ? 'outbound.disable' : 'outbound.enable', id: v.id }) }, v.enabled ? '停用' : '启用'),
450
+ ' ',
451
+ createElement('button', { disabled: busy, onClick: () => void control({ action: 'outbound.refresh', id: v.id }) }, '刷新'),
452
+ ' ',
453
+ createElement('button', { disabled: busy, onClick: () => void control({ action: 'outbound.remove', id: v.id }) }, '删除'),
284
454
  ),
285
455
  ),
286
456
  ),
287
457
  ),
288
458
  ),
459
+ createElement('h3', { style: { marginTop: '12px' } }, '新建出站 Server'),
460
+ createElement(
461
+ 'form',
462
+ {
463
+ className: 'dsh-a2a-form',
464
+ onSubmit: (ev: { preventDefault(): void }) => {
465
+ ev.preventDefault()
466
+ submitOutbound()
467
+ },
468
+ },
469
+ createElement('input', { value: outName, placeholder: '名称', onChange: (e: { target: { value: string } }) => setOutName(e.target.value) }),
470
+ createElement('input', { value: outUrl, placeholder: '远端 AgentCard URL', onChange: (e: { target: { value: string } }) => setOutUrl(e.target.value), style: { flex: 1, minWidth: '240px' } }),
471
+ createElement('select', { value: outPreset, onChange: (e: { target: { value: string } }) => setOutPreset(e.target.value) },
472
+ createElement('option', { value: '' }, 'Preset(默认)'),
473
+ ...presets.map((p) => createElement('option', { value: p.id, key: p.id }, `${p.name ?? p.id}${p.isDefault ? '(默认)' : ''}`)),
474
+ ),
475
+ createElement('input', { value: outTokenEnv, placeholder: 'Bearer env 变量名(可选)', onChange: (e: { target: { value: string } }) => setOutTokenEnv(e.target.value) }),
476
+ createElement('input', { value: outTimeout, placeholder: '超时 ms', onChange: (e: { target: { value: string } }) => setOutTimeout(e.target.value), style: { width: '90px' } }),
477
+ createElement('button', { type: 'submit', className: 'primary', disabled: busy || !outName.trim() || !outUrl.trim() }, '添加出站 Server'),
478
+ ),
289
479
  ),
290
480
 
291
- // ── outbound agents ──────────────────────────────────────────────────
481
+ // ── inbound peer monitoring ─────────────────────────────────────────
292
482
  createElement(
293
483
  'div',
294
484
  { className: 'dsh-a2a-card' },
295
- createElement('h3', null, `出站 Agents(${agents.length})`),
296
- agents.length === 0
297
- ? createElement('div', { className: 'muted' }, '未配置远程 agent。')
485
+ createElement('h3', null, `入站连接(${peers.length})`),
486
+ peers.length === 0
487
+ ? createElement('div', { className: 'muted' }, '当前无远程对端正在调用本服务。')
298
488
  : createElement(
299
489
  'table',
300
490
  { className: 'dsh-a2a-table' },
301
- createElement(
302
- 'thead',
303
- null,
491
+ createElement('thead', null,
304
492
  createElement('tr', null,
305
- createElement('th', null, '名称'),
306
- createElement('th', null, '状态'),
307
- createElement('th', null, '工具'),
493
+ createElement('th', null, '来源'),
494
+ createElement('th', null, '任务'),
495
+ createElement('th', null, '流式'),
496
+ createElement('th', null, '首次 / 最近'),
308
497
  createElement('th', null, '操作'),
309
498
  ),
310
499
  ),
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
- ),
500
+ createElement('tbody', null,
501
+ ...peers.map((p) =>
502
+ createElement('tr', { key: p.id },
503
+ createElement('td', null, p.source ?? p.label),
504
+ createElement('td', null, `${p.taskCount}(活跃 ${p.activeTaskIds.length})`),
505
+ createElement('td', null, p.streaming ? '●' : '—'),
323
506
  createElement('td', null,
324
- createElement('span', { className: `dsh-a2a-state ${a.state === 'connected' ? 'ok' : 'bad'}` }, a.state),
507
+ createElement('span', { className: 'muted' }, `${p.firstSeen ?? ''} / ${p.lastSeen ?? ''}`),
325
508
  ),
326
- createElement('td', null, `${a.toolCount} / ${a.skillCount}`),
327
509
  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 }) }, '删除'),
510
+ createElement('button', { disabled: busy, onClick: () => void control({ action: 'inbound.close', id: p.id }) }, '关闭'),
333
511
  ),
334
512
  ),
335
513
  ),
336
514
  ),
337
515
  ),
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
516
  ),
356
517
 
357
- // ── tasks ────────────────────────────────────────────────────────────
518
+ // ── tasks ───────────────────────────────────────────────────────────
358
519
  createElement(
359
520
  'div',
360
521
  { className: 'dsh-a2a-card' },