@hanphone/dsh-a2a 0.1.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 (88) hide show
  1. package/README.md +133 -87
  2. package/README.zh.md +64 -87
  3. package/cordis.patch.yml +4 -7
  4. package/lib/client.js +228 -36
  5. package/lib/index.js +1271 -625
  6. package/lib/types/api.d.ts +43 -13
  7. package/lib/types/api.d.ts.map +1 -1
  8. package/lib/types/api.js +60 -20
  9. package/lib/types/api.js.map +1 -1
  10. package/lib/types/client/index.d.ts +50 -14
  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 +178 -213
  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 +7 -1
  30. package/lib/types/server/a2a-server.d.ts.map +1 -1
  31. package/lib/types/server/a2a-server.js +11 -3
  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/identity.d.ts +38 -0
  42. package/lib/types/server/identity.d.ts.map +1 -0
  43. package/lib/types/server/identity.js +89 -0
  44. package/lib/types/server/identity.js.map +1 -0
  45. package/lib/types/server/inbound-registry.d.ts +60 -0
  46. package/lib/types/server/inbound-registry.d.ts.map +1 -0
  47. package/lib/types/server/inbound-registry.js +86 -0
  48. package/lib/types/server/inbound-registry.js.map +1 -0
  49. package/lib/types/server/routes.d.ts +5 -1
  50. package/lib/types/server/routes.d.ts.map +1 -1
  51. package/lib/types/server/routes.js +9 -5
  52. package/lib/types/server/routes.js.map +1 -1
  53. package/lib/types/server/store.d.ts +37 -1
  54. package/lib/types/server/store.d.ts.map +1 -1
  55. package/lib/types/server/store.js +14 -0
  56. package/lib/types/server/store.js.map +1 -1
  57. package/lib/types/servers/inbound-manager.d.ts +131 -0
  58. package/lib/types/servers/inbound-manager.d.ts.map +1 -0
  59. package/lib/types/servers/inbound-manager.js +312 -0
  60. package/lib/types/servers/inbound-manager.js.map +1 -0
  61. package/lib/types/servers/outbound-manager.d.ts +129 -0
  62. package/lib/types/servers/outbound-manager.d.ts.map +1 -0
  63. package/lib/types/servers/outbound-manager.js +238 -0
  64. package/lib/types/servers/outbound-manager.js.map +1 -0
  65. package/lib/types/service.d.ts +108 -23
  66. package/lib/types/service.d.ts.map +1 -1
  67. package/lib/types/service.js +39 -18
  68. package/lib/types/service.js.map +1 -1
  69. package/package.json +3 -3
  70. package/src/api.ts +70 -31
  71. package/src/client/index.ts +339 -81
  72. package/src/commands.ts +59 -43
  73. package/src/events.ts +14 -7
  74. package/src/index.ts +204 -249
  75. package/src/outbound/calls.ts +8 -6
  76. package/src/protocol.ts +204 -95
  77. package/src/server/a2a-server.ts +16 -4
  78. package/src/server/card.ts +13 -62
  79. package/src/server/exec/agent-runtime.ts +5 -2
  80. package/src/server/identity.ts +109 -0
  81. package/src/server/inbound-registry.ts +129 -0
  82. package/src/server/routes.ts +6 -4
  83. package/src/server/store.ts +50 -4
  84. package/src/servers/inbound-manager.ts +385 -0
  85. package/src/servers/outbound-manager.ts +289 -0
  86. package/src/service.ts +136 -28
  87. package/lib/tsconfig.client.tsbuildinfo +0 -1
  88. 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,24 +71,66 @@ 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: { readonly enabled: boolean; readonly cardUrl?: string; readonly skills: readonly string[] }
73
- readonly tasks: readonly unknown[]
74
- readonly agents: 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
79
+ }
80
+
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[]
75
93
  }
76
94
 
77
- interface AgentView {
95
+ export interface OutboundServerView {
78
96
  readonly id: string
79
97
  readonly name: string
80
98
  readonly agentCardUrl: string
99
+ readonly preset?: string
81
100
  readonly enabled: boolean
101
+ readonly timeoutMs: number
82
102
  readonly state: string
83
103
  readonly skillCount: number
84
104
  readonly toolCount: number
85
105
  readonly lastError?: string
86
106
  }
87
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
+
123
+ interface InboundPeerView {
124
+ readonly id: string
125
+ readonly label: string
126
+ readonly source?: string | null
127
+ readonly firstSeen?: string
128
+ readonly lastSeen?: string
129
+ readonly taskCount: number
130
+ readonly activeTaskIds: readonly string[]
131
+ readonly streaming: boolean
132
+ }
133
+
88
134
  interface TaskView {
89
135
  readonly id: string
90
136
  readonly contextId?: string
@@ -98,6 +144,12 @@ async function fetchSnapshot(): Promise<ApiSnapshot> {
98
144
  return (await res.json()) as ApiSnapshot
99
145
  }
100
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
+
101
153
  async function postControl(payload: Record<string, unknown>): Promise<{ readonly ok: boolean; readonly message: string }> {
102
154
  const res = await fetch('/a2a/api', {
103
155
  method: 'POST',
@@ -108,21 +160,59 @@ async function postControl(payload: Record<string, unknown>): Promise<{ readonly
108
160
  return body
109
161
  }
110
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
+
111
182
  type SectionProps = PropsRuntime<'settings.section'>
112
183
 
113
184
  export function A2aSection(_props: SectionProps): ReactElement {
114
185
  const [snap, setSnap] = useState<ApiSnapshot | undefined>(undefined)
186
+ const [presets, setPresets] = useState<readonly PresetView[]>([])
115
187
  const [error, setError] = useState<string | undefined>(undefined)
116
- const [agentName, setAgentName] = useState('')
117
- const [agentUrl, setAgentUrl] = useState('')
118
188
  const [busy, setBusy] = useState(false)
119
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')
205
+
120
206
  useEffect(() => {
121
207
  let alive = true
122
208
  const tick = async (): Promise<void> => {
123
209
  try {
124
- const next = await fetchSnapshot()
125
- if (alive) { setSnap(next); setError(undefined) }
210
+ const [next, presetRows] = await Promise.all([fetchSnapshot(), fetchPresets()])
211
+ if (alive) {
212
+ setSnap(next)
213
+ setPresets(presetRows)
214
+ setError(undefined)
215
+ }
126
216
  } catch (err) {
127
217
  if (alive) setError((err as Error).message)
128
218
  }
@@ -148,8 +238,74 @@ export function A2aSection(_props: SectionProps): ReactElement {
148
238
  }
149
239
  }
150
240
 
151
- const server = snap?.server
152
- const agents = (snap?.agents ?? []) as readonly AgentView[]
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[]
153
309
  const tasks = (snap?.tasks ?? []) as readonly TaskView[]
154
310
 
155
311
  return createElement(
@@ -157,107 +313,209 @@ export function A2aSection(_props: SectionProps): ReactElement {
157
313
  { className: 'dsh-a2a-panel' },
158
314
  error !== undefined ? createElement('div', { className: 'dsh-a2a-error' }, error) : null,
159
315
 
160
- // ── inbound server ───────────────────────────────────────────────────
316
+ // ── inbound server instances ────────────────────────────────────────
161
317
  createElement(
162
318
  'div',
163
319
  { className: 'dsh-a2a-card' },
164
- createElement('h3', null, '入站 Server'),
165
- server === undefined
166
- ? createElement('div', { className: 'muted' }, '加载中…')
320
+ createElement('h3', null, `入站 Servers(${inbounds.length})`),
321
+ inbounds.length === 0
322
+ ? createElement('div', { className: 'muted' }, '还没有入站 server。创建一个即可对外发布 A2A 端点。')
167
323
  : createElement(
168
- 'div',
169
- { className: 'dsh-a2a-row' },
170
- createElement(
171
- 'span',
172
- { className: `dsh-a2a-state ${server.enabled ? 'ok' : 'bad'}` },
173
- 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
+ ),
174
334
  ),
175
- createElement(
176
- 'button',
177
- {
178
- className: server.enabled ? '' : 'primary',
179
- disabled: busy,
180
- onClick: () => void control({ action: server.enabled ? 'server.disable' : 'server.enable' }),
181
- },
182
- 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
+ ),
183
365
  ),
184
366
  ),
185
- server !== undefined && server.enabled
186
- ? createElement(
187
- 'div',
188
- { className: 'dsh-a2a-row muted' },
189
- `AgentCard: ${server.cardUrl ?? '(n/a)'} · 技能: ${server.skills.join(', ') || '(chat)'}`,
190
- )
191
- : null,
367
+ createElement('h3', { style: { marginTop: '12px' } }, editingIn !== undefined ? '编辑入站 Server' : '新建入站 Server'),
368
+ createElement(
369
+ 'form',
370
+ {
371
+ className: 'dsh-a2a-form',
372
+ onSubmit: (ev: { preventDefault(): void }) => {
373
+ ev.preventDefault()
374
+ submitInbound()
375
+ },
376
+ },
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) }),
384
+ ),
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)。'),
192
414
  ),
193
415
 
194
- // ── outbound agents ──────────────────────────────────────────────────
416
+ // ── outbound server instances ───────────────────────────────────────
195
417
  createElement(
196
418
  'div',
197
419
  { className: 'dsh-a2a-card' },
198
- createElement('h3', null, `出站 Agents(${agents.length})`),
199
- agents.length === 0
200
- ? createElement('div', { className: 'muted' }, '未配置远程 agent。')
420
+ createElement('h3', null, `出站 Servers(${outbounds.length})`),
421
+ outbounds.length === 0
422
+ ? createElement('div', { className: 'muted' }, '还没有出站 server。添加一个远端 A2A 连接即可把其技能注册为模型工具。')
201
423
  : createElement(
202
424
  'table',
203
425
  { className: 'dsh-a2a-table' },
204
- createElement(
205
- 'thead',
206
- null,
426
+ createElement('thead', null,
207
427
  createElement('tr', null,
208
- createElement('th', null, '名称'),
428
+ createElement('th', null, '名称 / 远端'),
429
+ createElement('th', null, 'Preset'),
209
430
  createElement('th', null, '状态'),
210
431
  createElement('th', null, '工具'),
211
432
  createElement('th', null, '操作'),
212
433
  ),
213
434
  ),
214
- createElement(
215
- 'tbody',
216
- null,
217
- ...agents.map((a) =>
218
- createElement(
219
- 'tr',
220
- { key: a.id },
435
+ createElement('tbody', null,
436
+ ...outbounds.map((v) =>
437
+ createElement('tr', { key: v.id },
221
438
  createElement('td', null,
222
- createElement('div', null, a.name),
223
- createElement('div', { className: 'muted' }, a.agentCardUrl),
224
- a.lastError !== undefined ? createElement('div', { className: 'dsh-a2a-error' }, a.lastError) : null,
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,
225
442
  ),
443
+ createElement('td', null, v.preset ?? createElement('span', { className: 'muted' }, '默认')),
226
444
  createElement('td', null,
227
- createElement('span', { className: `dsh-a2a-state ${a.state === 'connected' ? 'ok' : 'bad'}` }, a.state),
445
+ createElement('span', { className: `dsh-a2a-state ${v.state === 'connected' ? 'ok' : 'bad'}` }, v.state === 'connected' ? '已连接' : v.state),
228
446
  ),
229
- createElement('td', null, `${a.toolCount} / ${a.skillCount}`),
447
+ createElement('td', null, `${v.toolCount} / ${v.skillCount}`),
230
448
  createElement('td', null,
231
- createElement('button', { disabled: busy, onClick: () => void control({ action: a.enabled ? 'agent.disable' : 'agent.enable', id: a.id }) }, a.enabled ? '停用' : '启用'),
449
+ createElement('button', { disabled: busy, onClick: () => void control({ action: v.enabled ? 'outbound.disable' : 'outbound.enable', id: v.id }) }, v.enabled ? '停用' : '启用'),
232
450
  ' ',
233
- createElement('button', { disabled: busy, onClick: () => void control({ action: 'agent.refresh', id: a.id }) }, '刷新'),
451
+ createElement('button', { disabled: busy, onClick: () => void control({ action: 'outbound.refresh', id: v.id }) }, '刷新'),
234
452
  ' ',
235
- createElement('button', { disabled: busy, onClick: () => void control({ action: 'agent.remove', id: a.id }) }, '删除'),
453
+ createElement('button', { disabled: busy, onClick: () => void control({ action: 'outbound.remove', id: v.id }) }, '删除'),
236
454
  ),
237
455
  ),
238
456
  ),
239
457
  ),
240
458
  ),
459
+ createElement('h3', { style: { marginTop: '12px' } }, '新建出站 Server'),
241
460
  createElement(
242
461
  'form',
243
462
  {
244
463
  className: 'dsh-a2a-form',
245
464
  onSubmit: (ev: { preventDefault(): void }) => {
246
465
  ev.preventDefault()
247
- if (!agentName.trim() || !agentUrl.trim()) return
248
- void control({ action: 'agent.add', name: agentName.trim(), agentCardUrl: agentUrl.trim() }).then(() => {
249
- setAgentName('')
250
- setAgentUrl('')
251
- })
466
+ submitOutbound()
252
467
  },
253
468
  },
254
- createElement('input', { value: agentName, placeholder: '名称', onChange: (e: { target: { value: string } }) => setAgentName(e.target.value) }),
255
- createElement('input', { value: agentUrl, placeholder: 'AgentCard URL', onChange: (e: { target: { value: string } }) => setAgentUrl(e.target.value) }),
256
- createElement('button', { type: 'submit', className: 'primary', disabled: busy }, '添加 Agent'),
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'),
257
478
  ),
258
479
  ),
259
480
 
260
- // ── tasks ────────────────────────────────────────────────────────────
481
+ // ── inbound peer monitoring ─────────────────────────────────────────
482
+ createElement(
483
+ 'div',
484
+ { className: 'dsh-a2a-card' },
485
+ createElement('h3', null, `入站连接(${peers.length})`),
486
+ peers.length === 0
487
+ ? createElement('div', { className: 'muted' }, '当前无远程对端正在调用本服务。')
488
+ : createElement(
489
+ 'table',
490
+ { className: 'dsh-a2a-table' },
491
+ createElement('thead', null,
492
+ createElement('tr', null,
493
+ createElement('th', null, '来源'),
494
+ createElement('th', null, '任务'),
495
+ createElement('th', null, '流式'),
496
+ createElement('th', null, '首次 / 最近'),
497
+ createElement('th', null, '操作'),
498
+ ),
499
+ ),
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 ? '●' : '—'),
506
+ createElement('td', null,
507
+ createElement('span', { className: 'muted' }, `${p.firstSeen ?? ''} / ${p.lastSeen ?? ''}`),
508
+ ),
509
+ createElement('td', null,
510
+ createElement('button', { disabled: busy, onClick: () => void control({ action: 'inbound.close', id: p.id }) }, '关闭'),
511
+ ),
512
+ ),
513
+ ),
514
+ ),
515
+ ),
516
+ ),
517
+
518
+ // ── tasks ───────────────────────────────────────────────────────────
261
519
  createElement(
262
520
  'div',
263
521
  { className: 'dsh-a2a-card' },