@ai_mind/dsh-a2a 0.0.0-stage → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +444 -2
- package/client.js +868 -0
- package/cordis.patch.yml +21 -0
- package/icon.svg +15 -0
- package/index.js +59 -0
- package/lib/auth.js +196 -0
- package/lib/bridge.js +518 -0
- package/lib/card.js +214 -0
- package/lib/client-discovery.js +58 -0
- package/lib/config.js +565 -0
- package/lib/executor.js +357 -0
- package/lib/files.js +52 -0
- package/lib/limits.js +133 -0
- package/lib/panel.js +357 -0
- package/lib/prompt.js +161 -0
- package/lib/server.js +456 -0
- package/lib/store.js +288 -0
- package/lib/util.js +221 -0
- package/locale/en.json +6 -0
- package/locale/zh.json +6 -0
- package/package.json +54 -4
package/client.js
ADDED
|
@@ -0,0 +1,868 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @ai_mind/dsh-a2a — browser half (settings panel).
|
|
3
|
+
*
|
|
4
|
+
* Registers one Settings → Plugins tab that renders the live A2A Agent Card,
|
|
5
|
+
* its interface URLs, capabilities, optional runtime counters, an editable
|
|
6
|
+
* configuration form and copy-ready request snippets.
|
|
7
|
+
*
|
|
8
|
+
* The configuration form talks to `<mountPath>/panel/config`, which is
|
|
9
|
+
* authenticated by the Harness Web connection (not the A2A token) and writes
|
|
10
|
+
* through the Host config editor.
|
|
11
|
+
*/
|
|
12
|
+
window.__ModuleLoader__.load({
|
|
13
|
+
id: '@ai_mind/dsh-a2a',
|
|
14
|
+
factory(require) {
|
|
15
|
+
const React = require('react')
|
|
16
|
+
const h = React.createElement
|
|
17
|
+
|
|
18
|
+
const NS = 'a2aPanel'
|
|
19
|
+
const CARD_PATH = '/.well-known/agent-card.json'
|
|
20
|
+
|
|
21
|
+
const zh = {
|
|
22
|
+
tab: 'A2A 服务端',
|
|
23
|
+
heading: 'A2A 服务端',
|
|
24
|
+
intro: '通过 Agent2Agent v1.0 协议把本机智能体开放给其他智能体。',
|
|
25
|
+
refresh: '刷新',
|
|
26
|
+
loading: '正在读取 Agent Card…',
|
|
27
|
+
loadError: '无法读取 Agent Card',
|
|
28
|
+
loadErrorHint: '请确认 a2a-server 插件已启用,并且 /a2a 路径没有被其他插件占用。',
|
|
29
|
+
disabled: '未检测到 A2A 服务端',
|
|
30
|
+
disabledHint: '在 设置 → 插件 中启用 @ai_mind/dsh-a2a,或确认 profile 的 cordis.patch.yml 中存在 a2a-server 行。',
|
|
31
|
+
interfaces: '接口地址',
|
|
32
|
+
jsonrpc: 'JSON-RPC',
|
|
33
|
+
rest: 'HTTP+JSON',
|
|
34
|
+
capabilities: '能力',
|
|
35
|
+
streaming: '流式 SSE',
|
|
36
|
+
push: '推送通知',
|
|
37
|
+
extended: '扩展 Agent Card',
|
|
38
|
+
auth: '鉴权',
|
|
39
|
+
authNone: '未启用(仅允许本机调用)',
|
|
40
|
+
authOn: '已启用',
|
|
41
|
+
version: '协议版本',
|
|
42
|
+
runtime: '运行状态',
|
|
43
|
+
runtimeDenied: '运行状态需要 A2A 凭据,当前页面无权读取;这不影响其他信息。',
|
|
44
|
+
tasks: '任务',
|
|
45
|
+
contexts: '活跃上下文',
|
|
46
|
+
concurrency: '并发占用',
|
|
47
|
+
uptime: '运行时长',
|
|
48
|
+
skills: '技能',
|
|
49
|
+
noSkills: '未声明技能',
|
|
50
|
+
quickstart: '快速请求',
|
|
51
|
+
copy: '复制',
|
|
52
|
+
copied: '已复制',
|
|
53
|
+
jsClient: 'JavaScript 客户端',
|
|
54
|
+
config: '配置',
|
|
55
|
+
configHint: '保存到 profile 的 cordis.patch.yml(a2a-server 行);插件会重载,进行中的 A2A 任务会被中断。',
|
|
56
|
+
configUnavailable: '当前部署未启用配置编辑(缺少 configEditor)。请直接编辑 profile 的 cordis.patch.yml。',
|
|
57
|
+
configUnauthorized: '配置读取需要已登录的 Harness Web 会话,请从 DSH GUI 打开本页。',
|
|
58
|
+
configLoadError: '读取配置失败',
|
|
59
|
+
save: '保存',
|
|
60
|
+
saving: '保存中…',
|
|
61
|
+
saved: '已保存,插件正在重载…',
|
|
62
|
+
saveError: '保存失败',
|
|
63
|
+
reload: '重新读取',
|
|
64
|
+
dirty: '有未保存的修改',
|
|
65
|
+
token: '访问令牌',
|
|
66
|
+
tokenHint: '写回 profile YAML;推荐改用 auth.tokenEnv 从环境变量读取。',
|
|
67
|
+
tokenPlaceholder: '留空保持不变',
|
|
68
|
+
clearToken: '清除已保存的内联令牌',
|
|
69
|
+
tokenConfigured: '已配置内联令牌',
|
|
70
|
+
tokenNotConfigured: '未配置内联令牌',
|
|
71
|
+
tokenEnvConfigured: '环境变量已配置:{name}',
|
|
72
|
+
openCard: '打开 Agent Card',
|
|
73
|
+
seconds: '秒',
|
|
74
|
+
minutes: '分',
|
|
75
|
+
hours: '小时',
|
|
76
|
+
unknown: '—',
|
|
77
|
+
groupCard: '身份与描述',
|
|
78
|
+
groupServer: '服务与能力',
|
|
79
|
+
groupAuth: '鉴权',
|
|
80
|
+
groupAgent: 'Agent 运行时',
|
|
81
|
+
groupLimits: '限额与保留',
|
|
82
|
+
fieldCardName: '名称',
|
|
83
|
+
fieldCardDescription: '描述',
|
|
84
|
+
fieldCardVersion: '版本',
|
|
85
|
+
fieldServerPublicUrl: '对外地址',
|
|
86
|
+
fieldServerMountPath: '挂载路径',
|
|
87
|
+
fieldServerStreaming: '流式 SSE',
|
|
88
|
+
fieldServerPushNotifications: '推送通知',
|
|
89
|
+
fieldServerExtendedCard: '扩展 Agent Card',
|
|
90
|
+
fieldAuthMode: '鉴权模式',
|
|
91
|
+
fieldAuthTokenEnv: '令牌环境变量',
|
|
92
|
+
fieldAuthAllowRemoteUnauthenticated: '允许未鉴权的远程调用',
|
|
93
|
+
fieldAgentCwd: '工作目录',
|
|
94
|
+
fieldAgentPreset: 'Agent 预设',
|
|
95
|
+
fieldAgentProvider: '模型提供方',
|
|
96
|
+
fieldAgentModel: '模型',
|
|
97
|
+
fieldAgentReasoningEffort: '推理强度',
|
|
98
|
+
fieldAgentSandboxMode: '沙箱模式',
|
|
99
|
+
fieldAgentApprovalPolicy: '审批策略',
|
|
100
|
+
fieldAgentMaxConcurrentTasks: '最大并发任务',
|
|
101
|
+
fieldAgentQueueTimeoutMs: '排队超时 (ms)',
|
|
102
|
+
fieldAgentTurnTimeoutMs: '单回合超时 (ms)',
|
|
103
|
+
fieldLimitsMaxRequestBytes: '请求体上限 (bytes)',
|
|
104
|
+
fieldLimitsMaxFileBytes: '单附件上限 (bytes)',
|
|
105
|
+
fieldTasksTtlMs: '任务保留 (ms)',
|
|
106
|
+
fieldTasksMaxTasks: '任务数量上限',
|
|
107
|
+
fieldPushAllowPrivateTargets: '允许私有 Webhook',
|
|
108
|
+
enumNone: '不鉴权',
|
|
109
|
+
enumBearer: 'Bearer',
|
|
110
|
+
enumApiKey: 'API Key',
|
|
111
|
+
enumInherit: '继承部署默认',
|
|
112
|
+
enumReadOnly: '只读',
|
|
113
|
+
enumWorkspaceWrite: '工作区可写',
|
|
114
|
+
enumDangerFullAccess: '完全访问',
|
|
115
|
+
enumAsk: '询问',
|
|
116
|
+
enumNever: '从不',
|
|
117
|
+
restartHint: '修改挂载路径后请刷新页面。',
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const en = {
|
|
121
|
+
tab: 'A2A Server',
|
|
122
|
+
heading: 'A2A Server',
|
|
123
|
+
intro: 'Exposes this Harness agent to other agents over the Agent2Agent v1.0 protocol.',
|
|
124
|
+
refresh: 'Refresh',
|
|
125
|
+
loading: 'Reading the Agent Card…',
|
|
126
|
+
loadError: 'Cannot read the Agent Card',
|
|
127
|
+
loadErrorHint: 'Confirm the a2a-server plugin is enabled and the /a2a path is not owned by another plugin.',
|
|
128
|
+
disabled: 'No A2A server detected',
|
|
129
|
+
disabledHint: 'Enable @ai_mind/dsh-a2a under Settings → Plugins, or add an a2a-server row to the profile cordis.patch.yml.',
|
|
130
|
+
interfaces: 'Endpoints',
|
|
131
|
+
jsonrpc: 'JSON-RPC',
|
|
132
|
+
rest: 'HTTP+JSON',
|
|
133
|
+
capabilities: 'Capabilities',
|
|
134
|
+
streaming: 'Streaming SSE',
|
|
135
|
+
push: 'Push notifications',
|
|
136
|
+
extended: 'Extended Agent Card',
|
|
137
|
+
auth: 'Authentication',
|
|
138
|
+
authNone: 'None (loopback only)',
|
|
139
|
+
authOn: 'Required',
|
|
140
|
+
version: 'Protocol version',
|
|
141
|
+
runtime: 'Runtime',
|
|
142
|
+
runtimeDenied: 'Runtime counters require A2A credentials; the panel cannot read them. Everything else is unaffected.',
|
|
143
|
+
tasks: 'Tasks',
|
|
144
|
+
contexts: 'Active contexts',
|
|
145
|
+
concurrency: 'Concurrency',
|
|
146
|
+
uptime: 'Uptime',
|
|
147
|
+
skills: 'Skills',
|
|
148
|
+
noSkills: 'No skills declared',
|
|
149
|
+
quickstart: 'Quick request',
|
|
150
|
+
copy: 'Copy',
|
|
151
|
+
copied: 'Copied',
|
|
152
|
+
jsClient: 'JavaScript client',
|
|
153
|
+
config: 'Configuration',
|
|
154
|
+
configHint: 'Saved to the a2a-server row in the profile cordis.patch.yml; the plugin reloads and in-flight A2A tasks are interrupted.',
|
|
155
|
+
configUnavailable: 'Configuration editing is unavailable in this deployment (no config editor). Edit the profile cordis.patch.yml instead.',
|
|
156
|
+
configUnauthorized: 'Reading configuration requires an authenticated Harness Web session; open this page from the DSH GUI.',
|
|
157
|
+
configLoadError: 'Cannot read configuration',
|
|
158
|
+
save: 'Save',
|
|
159
|
+
saving: 'Saving…',
|
|
160
|
+
saved: 'Saved. The plugin is reloading…',
|
|
161
|
+
saveError: 'Save failed',
|
|
162
|
+
reload: 'Reload',
|
|
163
|
+
dirty: 'Unsaved changes',
|
|
164
|
+
token: 'Access token',
|
|
165
|
+
tokenHint: 'Stored in the profile YAML. Prefer auth.tokenEnv instead.',
|
|
166
|
+
tokenPlaceholder: 'Leave empty to keep',
|
|
167
|
+
clearToken: 'Clear the saved inline token',
|
|
168
|
+
tokenConfigured: 'Inline token configured',
|
|
169
|
+
tokenNotConfigured: 'No inline token',
|
|
170
|
+
tokenEnvConfigured: 'Environment variable: {name}',
|
|
171
|
+
openCard: 'Open Agent Card',
|
|
172
|
+
seconds: 's',
|
|
173
|
+
minutes: 'm',
|
|
174
|
+
hours: 'h',
|
|
175
|
+
unknown: '—',
|
|
176
|
+
groupCard: 'Identity',
|
|
177
|
+
groupServer: 'Service & capabilities',
|
|
178
|
+
groupAuth: 'Authentication',
|
|
179
|
+
groupAgent: 'Agent runtime',
|
|
180
|
+
groupLimits: 'Limits & retention',
|
|
181
|
+
fieldCardName: 'Name',
|
|
182
|
+
fieldCardDescription: 'Description',
|
|
183
|
+
fieldCardVersion: 'Version',
|
|
184
|
+
fieldServerPublicUrl: 'Public URL',
|
|
185
|
+
fieldServerMountPath: 'Mount path',
|
|
186
|
+
fieldServerStreaming: 'Streaming SSE',
|
|
187
|
+
fieldServerPushNotifications: 'Push notifications',
|
|
188
|
+
fieldServerExtendedCard: 'Extended Agent Card',
|
|
189
|
+
fieldAuthMode: 'Auth mode',
|
|
190
|
+
fieldAuthTokenEnv: 'Token environment variable',
|
|
191
|
+
fieldAuthAllowRemoteUnauthenticated: 'Allow unauthenticated remote callers',
|
|
192
|
+
fieldAgentCwd: 'Working directory',
|
|
193
|
+
fieldAgentPreset: 'Agent preset',
|
|
194
|
+
fieldAgentProvider: 'Model provider',
|
|
195
|
+
fieldAgentModel: 'Model',
|
|
196
|
+
fieldAgentReasoningEffort: 'Reasoning effort',
|
|
197
|
+
fieldAgentSandboxMode: 'Sandbox mode',
|
|
198
|
+
fieldAgentApprovalPolicy: 'Approval policy',
|
|
199
|
+
fieldAgentMaxConcurrentTasks: 'Max concurrent tasks',
|
|
200
|
+
fieldAgentQueueTimeoutMs: 'Queue timeout (ms)',
|
|
201
|
+
fieldAgentTurnTimeoutMs: 'Turn timeout (ms)',
|
|
202
|
+
fieldLimitsMaxRequestBytes: 'Request body limit (bytes)',
|
|
203
|
+
fieldLimitsMaxFileBytes: 'Attachment limit (bytes)',
|
|
204
|
+
fieldTasksTtlMs: 'Task retention (ms)',
|
|
205
|
+
fieldTasksMaxTasks: 'Max tasks',
|
|
206
|
+
fieldPushAllowPrivateTargets: 'Allow private webhooks',
|
|
207
|
+
enumNone: 'None',
|
|
208
|
+
enumBearer: 'Bearer',
|
|
209
|
+
enumApiKey: 'API Key',
|
|
210
|
+
enumInherit: 'Inherit deployment',
|
|
211
|
+
enumReadOnly: 'Read-only',
|
|
212
|
+
enumWorkspaceWrite: 'Workspace write',
|
|
213
|
+
enumDangerFullAccess: 'Full access',
|
|
214
|
+
enumAsk: 'Ask',
|
|
215
|
+
enumNever: 'Never',
|
|
216
|
+
restartHint: 'Refresh the page after changing the mount path.',
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
const CSS = `
|
|
220
|
+
.a2aui-page{display:flex;flex-direction:column;gap:16px;max-width:860px;color:var(--dsw-alias-label-primary);font-size:13px;line-height:20px}
|
|
221
|
+
.a2aui-head{display:flex;align-items:flex-start;justify-content:space-between;gap:16px}
|
|
222
|
+
.a2aui-brand{display:flex;align-items:center;gap:14px;min-width:0}
|
|
223
|
+
.a2aui-logo{width:44px;height:44px;flex:none}
|
|
224
|
+
.a2aui-title{margin:0;font-size:18px;font-weight:600;line-height:26px}
|
|
225
|
+
.a2aui-desc{margin:2px 0 0;color:var(--dsw-alias-label-secondary);font-size:13px;line-height:20px}
|
|
226
|
+
.a2aui-card{border:.5px solid var(--dsw-alias-border-l3);border-radius:var(--dsw-radius-lg);background:var(--dsw-alias-bg-layer-1);padding:14px 16px;display:flex;flex-direction:column;gap:10px;min-width:0}
|
|
227
|
+
.a2aui-section-title{margin:0;font-size:13px;font-weight:600;line-height:20px}
|
|
228
|
+
.a2aui-badges{display:flex;flex-wrap:wrap;gap:6px}
|
|
229
|
+
.a2aui-badge{border:.5px solid var(--dsw-alias-border-l3);border-radius:999px;padding:1px 9px;font-size:12px;line-height:18px;color:var(--dsw-alias-label-secondary);white-space:nowrap}
|
|
230
|
+
.a2aui-badge[data-on=true]{border-color:color-mix(in srgb,var(--dsw-alias-state-success-primary) 45%,transparent);background:color-mix(in srgb,var(--dsw-alias-state-success-primary) 10%,transparent);color:var(--dsw-alias-label-primary)}
|
|
231
|
+
.a2aui-badge[data-tone=warn]{border-color:color-mix(in srgb,var(--dsw-alias-state-warn-primary) 45%,transparent);background:var(--dsw-alias-state-warn-tertiary);color:var(--dsw-alias-label-primary)}
|
|
232
|
+
.a2aui-row{display:flex;align-items:center;gap:10px;min-width:0;border-top:.5px solid var(--dsw-alias-border-l2);padding-top:10px}
|
|
233
|
+
.a2aui-row:first-of-type{border-top:0;padding-top:0}
|
|
234
|
+
.a2aui-row-label{flex:none;width:88px;color:var(--dsw-alias-label-secondary);font-size:12px}
|
|
235
|
+
.a2aui-code{flex:1;min-width:0;overflow-wrap:anywhere;font-family:var(--dsw-font-mono,ui-monospace,SFMono-Regular,Menlo,monospace);font-size:12px;color:var(--dsw-alias-label-primary)}
|
|
236
|
+
.a2aui-btn{flex:none;border:.5px solid var(--dsw-alias-border-l3);background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-label-primary);border-radius:var(--dsw-radius-md);height:26px;padding:0 10px;font:inherit;font-size:12px;cursor:pointer}
|
|
237
|
+
.a2aui-btn:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover)}
|
|
238
|
+
.a2aui-btn:disabled{opacity:.5;cursor:default}
|
|
239
|
+
.a2aui-btn[data-copied=true]{color:var(--dsw-alias-state-success-primary);border-color:color-mix(in srgb,var(--dsw-alias-state-success-primary) 45%,transparent)}
|
|
240
|
+
.a2aui-btnPrimary{background:var(--dsw-alias-state-business-primary);border-color:var(--dsw-alias-state-business-primary);color:var(--dsw-alias-label-primary-foreground)}
|
|
241
|
+
.a2aui-facts{display:grid;grid-template-columns:repeat(auto-fit,minmax(120px,1fr));gap:10px}
|
|
242
|
+
.a2aui-fact{display:flex;flex-direction:column;gap:1px}
|
|
243
|
+
.a2aui-fact-key{color:var(--dsw-alias-label-tertiary);font-size:11.5px}
|
|
244
|
+
.a2aui-fact-value{font-size:15px;font-weight:600;line-height:22px;font-variant-numeric:tabular-nums}
|
|
245
|
+
.a2aui-skills{display:flex;flex-direction:column;gap:8px;margin:0;padding:0;list-style:none}
|
|
246
|
+
.a2aui-skill-name{font-weight:600}
|
|
247
|
+
.a2aui-skill-desc{color:var(--dsw-alias-label-secondary);margin:1px 0 0}
|
|
248
|
+
.a2aui-muted{color:var(--dsw-alias-label-tertiary);margin:0}
|
|
249
|
+
.a2aui-pre{margin:0;border:.5px solid var(--dsw-alias-border-l2);border-radius:var(--dsw-radius-md);background:var(--dsw-alias-bg-layer-2);padding:10px 12px;overflow:auto;max-height:260px;font-family:var(--dsw-font-mono,ui-monospace,SFMono-Regular,Menlo,monospace);font-size:11.5px;line-height:17px;white-space:pre;color:var(--dsw-alias-label-primary)}
|
|
250
|
+
.a2aui-details summary{cursor:pointer;color:var(--dsw-alias-label-secondary);font-size:12px;user-select:none}
|
|
251
|
+
.a2aui-foot{display:flex;align-items:center;gap:8px;flex-wrap:wrap}
|
|
252
|
+
.a2aui-link{color:var(--dsw-alias-state-business-primary);text-decoration:none;font-size:12px}
|
|
253
|
+
.a2aui-link:hover{text-decoration:underline}
|
|
254
|
+
.a2aui-error{border:.5px solid color-mix(in srgb,var(--dsw-alias-state-error-primary) 40%,transparent);border-radius:var(--dsw-radius-lg);background:color-mix(in srgb,var(--dsw-alias-state-error-primary) 6%,transparent);padding:14px 16px;display:flex;flex-direction:column;gap:4px}
|
|
255
|
+
.a2aui-error-title{margin:0;font-weight:600}
|
|
256
|
+
.a2aui-form{display:flex;flex-direction:column;gap:14px}
|
|
257
|
+
.a2aui-group{display:flex;flex-direction:column;gap:8px}
|
|
258
|
+
.a2aui-group-title{margin:0;color:var(--dsw-alias-label-secondary);font-size:12px;font-weight:600;line-height:18px}
|
|
259
|
+
.a2aui-field{display:grid;grid-template-columns:190px minmax(0,1fr);gap:10px;align-items:center;min-width:0}
|
|
260
|
+
.a2aui-field>label{color:var(--dsw-alias-label-secondary);font-size:12.5px;line-height:18px;overflow-wrap:anywhere}
|
|
261
|
+
.a2aui-input,.a2aui-select,.a2aui-textarea{box-sizing:border-box;width:100%;min-width:0;border:.5px solid var(--dsw-alias-border-l4);border-radius:var(--dsw-radius-md);background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-label-primary);font:inherit;font-size:12.5px;padding:5px 9px;outline:none}
|
|
262
|
+
.a2aui-input:focus,.a2aui-select:focus,.a2aui-textarea:focus{border-color:var(--dsw-alias-state-business-primary);box-shadow:inset 0 0 0 .5px var(--dsw-alias-state-business-primary)}
|
|
263
|
+
.a2aui-textarea{resize:vertical;min-height:56px;line-height:18px}
|
|
264
|
+
.a2aui-check{display:inline-flex;align-items:center;gap:8px;color:var(--dsw-alias-label-primary);font-size:12.5px}
|
|
265
|
+
.a2aui-check input{width:15px;height:15px;accent-color:var(--dsw-alias-brand-primary)}
|
|
266
|
+
.a2aui-field-note{color:var(--dsw-alias-label-tertiary);font-size:11.5px;margin:0}
|
|
267
|
+
.a2aui-form-foot{display:flex;align-items:center;gap:10px;flex-wrap:wrap;border-top:.5px solid var(--dsw-alias-border-l2);padding-top:12px}
|
|
268
|
+
.a2aui-status{font-size:12px;color:var(--dsw-alias-label-secondary);margin:0}
|
|
269
|
+
.a2aui-status[data-tone=error]{color:var(--dsw-alias-state-error-primary)}
|
|
270
|
+
.a2aui-status[data-tone=ok]{color:var(--dsw-alias-state-success-primary)}
|
|
271
|
+
.a2aui-secret{display:flex;flex-direction:column;gap:6px}
|
|
272
|
+
`
|
|
273
|
+
|
|
274
|
+
function copyText(text) {
|
|
275
|
+
try {
|
|
276
|
+
if (navigator.clipboard && navigator.clipboard.writeText) {
|
|
277
|
+
return navigator.clipboard.writeText(text).then(
|
|
278
|
+
() => true,
|
|
279
|
+
() => fallbackCopy(text),
|
|
280
|
+
)
|
|
281
|
+
}
|
|
282
|
+
} catch {
|
|
283
|
+
/* fall through */
|
|
284
|
+
}
|
|
285
|
+
return Promise.resolve(fallbackCopy(text))
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
function fallbackCopy(text) {
|
|
289
|
+
try {
|
|
290
|
+
const area = document.createElement('textarea')
|
|
291
|
+
area.value = text
|
|
292
|
+
area.setAttribute('readonly', '')
|
|
293
|
+
area.style.position = 'fixed'
|
|
294
|
+
area.style.opacity = '0'
|
|
295
|
+
document.body.appendChild(area)
|
|
296
|
+
area.select()
|
|
297
|
+
const ok = document.execCommand('copy')
|
|
298
|
+
area.remove()
|
|
299
|
+
return ok
|
|
300
|
+
} catch {
|
|
301
|
+
return false
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
function formatUptime(seconds, t) {
|
|
306
|
+
if (typeof seconds !== 'number' || !Number.isFinite(seconds)) return t('unknown')
|
|
307
|
+
if (seconds < 60) return `${Math.round(seconds)}${t('seconds')}`
|
|
308
|
+
if (seconds < 3600) return `${Math.floor(seconds / 60)}${t('minutes')} ${Math.round(seconds % 60)}${t('seconds')}`
|
|
309
|
+
return `${Math.floor(seconds / 3600)}${t('hours')} ${Math.floor((seconds % 3600) / 60)}${t('minutes')}`
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
const GROUP_LABELS = {
|
|
313
|
+
card: 'groupCard',
|
|
314
|
+
server: 'groupServer',
|
|
315
|
+
auth: 'groupAuth',
|
|
316
|
+
agent: 'groupAgent',
|
|
317
|
+
limits: 'groupLimits',
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
function humanize(path) {
|
|
321
|
+
const tail = path.split('.').pop() ?? path
|
|
322
|
+
return tail.replace(/([A-Z])/g, ' $1').replace(/^./, (value) => value.toUpperCase())
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
function fieldLabel(t, path) {
|
|
326
|
+
const key = `field${path
|
|
327
|
+
.split('.')
|
|
328
|
+
.map((part) => part.charAt(0).toUpperCase() + part.slice(1))
|
|
329
|
+
.join('')}`
|
|
330
|
+
const value = t(key)
|
|
331
|
+
return typeof value === 'string' && value !== key ? value : humanize(path)
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
function groupLabel(t, group) {
|
|
335
|
+
const key = GROUP_LABELS[group] ?? group
|
|
336
|
+
const value = t(key)
|
|
337
|
+
return typeof value === 'string' && value !== key ? value : group
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
function enumLabel(t, value) {
|
|
341
|
+
const key = `enum${value.replace(/[-_]/g, '').replace(/^./, (character) => character.toUpperCase())}`
|
|
342
|
+
const label = t(key)
|
|
343
|
+
return typeof label === 'string' && label !== key ? label : value
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
function draftFrom(panel) {
|
|
347
|
+
const values = {}
|
|
348
|
+
for (const field of panel && Array.isArray(panel.fields) ? panel.fields : []) values[field.path] = field.value
|
|
349
|
+
return values
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
function sameValue(field, value) {
|
|
353
|
+
if (field.type === 'integer') {
|
|
354
|
+
if (value === '' || value === null || value === undefined) return field.value === null
|
|
355
|
+
return Number(value) === Number(field.value)
|
|
356
|
+
}
|
|
357
|
+
if (field.type === 'boolean') return Boolean(value) === Boolean(field.value)
|
|
358
|
+
return String(value ?? '') === String(field.value ?? '')
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
function serializeValue(field, value) {
|
|
362
|
+
if (field.type === 'integer') {
|
|
363
|
+
if (value === '' || value === null || value === undefined) return null
|
|
364
|
+
return Number(value)
|
|
365
|
+
}
|
|
366
|
+
if (field.type === 'boolean') return Boolean(value)
|
|
367
|
+
if (field.nullable && String(value ?? '').trim() === '') return null
|
|
368
|
+
return value
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
function CopyButton({ text, label, copied, onCopy, id }) {
|
|
372
|
+
const done = copied === id
|
|
373
|
+
return h(
|
|
374
|
+
'button',
|
|
375
|
+
{
|
|
376
|
+
type: 'button',
|
|
377
|
+
className: 'a2aui-btn',
|
|
378
|
+
'data-copied': done ? 'true' : 'false',
|
|
379
|
+
onClick: () => onCopy(id, text),
|
|
380
|
+
},
|
|
381
|
+
done ? label.copied : label.copy,
|
|
382
|
+
)
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
function createTab(t) {
|
|
386
|
+
return function A2AServerPanel() {
|
|
387
|
+
const [state, setState] = React.useState({
|
|
388
|
+
status: 'loading',
|
|
389
|
+
card: null,
|
|
390
|
+
health: null,
|
|
391
|
+
denied: false,
|
|
392
|
+
error: null,
|
|
393
|
+
panel: null,
|
|
394
|
+
panelPath: null,
|
|
395
|
+
panelError: null,
|
|
396
|
+
})
|
|
397
|
+
const [draft, setDraft] = React.useState({})
|
|
398
|
+
const [token, setToken] = React.useState('')
|
|
399
|
+
const [clearToken, setClearToken] = React.useState(false)
|
|
400
|
+
const [saving, setSaving] = React.useState(false)
|
|
401
|
+
const [saved, setSaved] = React.useState(false)
|
|
402
|
+
const [saveError, setSaveError] = React.useState(null)
|
|
403
|
+
const [copied, setCopied] = React.useState(null)
|
|
404
|
+
const [reloadKey, setReloadKey] = React.useState(0)
|
|
405
|
+
|
|
406
|
+
React.useEffect(() => {
|
|
407
|
+
const controller = new AbortController()
|
|
408
|
+
setState((previous) => ({ ...previous, status: 'loading', error: null }))
|
|
409
|
+
setSaved(false)
|
|
410
|
+
setSaveError(null)
|
|
411
|
+
;(async () => {
|
|
412
|
+
try {
|
|
413
|
+
const response = await fetch(CARD_PATH, { cache: 'no-store', signal: controller.signal })
|
|
414
|
+
if (!response.ok) {
|
|
415
|
+
setState({ status: response.status === 404 ? 'missing' : 'error', card: null, health: null, denied: false, error: `HTTP ${response.status}`, panel: null, panelPath: null, panelError: null })
|
|
416
|
+
return
|
|
417
|
+
}
|
|
418
|
+
const card = await response.json()
|
|
419
|
+
let health = null
|
|
420
|
+
let denied = false
|
|
421
|
+
let panel = null
|
|
422
|
+
let panelPath = null
|
|
423
|
+
let panelError = null
|
|
424
|
+
const jsonrpc = Array.isArray(card.supportedInterfaces)
|
|
425
|
+
? card.supportedInterfaces.find((entry) => entry.protocolBinding === 'JSONRPC')
|
|
426
|
+
: undefined
|
|
427
|
+
if (jsonrpc && typeof jsonrpc.url === 'string') {
|
|
428
|
+
try {
|
|
429
|
+
const url = new URL(jsonrpc.url, window.location.origin)
|
|
430
|
+
const base = `${window.location.origin}${url.pathname.replace(/\/$/, '')}`
|
|
431
|
+
try {
|
|
432
|
+
const healthResponse = await fetch(`${base}/health`, { cache: 'no-store', signal: controller.signal })
|
|
433
|
+
if (healthResponse.ok) health = await healthResponse.json()
|
|
434
|
+
else denied = healthResponse.status === 401 || healthResponse.status === 403
|
|
435
|
+
} catch {
|
|
436
|
+
denied = true
|
|
437
|
+
}
|
|
438
|
+
panelPath = `${base}/panel/config`
|
|
439
|
+
try {
|
|
440
|
+
const panelResponse = await fetch(panelPath, { cache: 'no-store', signal: controller.signal, credentials: 'same-origin' })
|
|
441
|
+
if (panelResponse.ok) panel = await panelResponse.json()
|
|
442
|
+
else if (panelResponse.status === 401 || panelResponse.status === 403) panelError = 'unauthorized'
|
|
443
|
+
else if (panelResponse.status === 501) panelError = 'unavailable'
|
|
444
|
+
else panelError = 'error'
|
|
445
|
+
} catch {
|
|
446
|
+
panelError = 'error'
|
|
447
|
+
}
|
|
448
|
+
} catch {
|
|
449
|
+
denied = true
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
setState({ status: 'ready', card, health, denied, error: null, panel, panelPath, panelError })
|
|
453
|
+
setDraft(draftFrom(panel))
|
|
454
|
+
} catch (error) {
|
|
455
|
+
if (error && error.name === 'AbortError') return
|
|
456
|
+
setState({ status: 'error', card: null, health: null, denied: false, error: String((error && error.message) || error), panel: null, panelPath: null, panelError: null })
|
|
457
|
+
}
|
|
458
|
+
})()
|
|
459
|
+
return () => controller.abort()
|
|
460
|
+
}, [reloadKey])
|
|
461
|
+
|
|
462
|
+
const writeClipboard = React.useCallback((id, text) => {
|
|
463
|
+
copyText(text).then((ok) => {
|
|
464
|
+
if (!ok) return
|
|
465
|
+
setCopied(id)
|
|
466
|
+
window.setTimeout(() => setCopied((current) => (current === id ? null : current)), 1600)
|
|
467
|
+
})
|
|
468
|
+
}, [])
|
|
469
|
+
|
|
470
|
+
const card = state.card
|
|
471
|
+
const health = state.health
|
|
472
|
+
const panel = state.panel
|
|
473
|
+
const jsonrpc = card && Array.isArray(card.supportedInterfaces)
|
|
474
|
+
? card.supportedInterfaces.find((entry) => entry.protocolBinding === 'JSONRPC')
|
|
475
|
+
: undefined
|
|
476
|
+
const rest = card && Array.isArray(card.supportedInterfaces)
|
|
477
|
+
? card.supportedInterfaces.find((entry) => entry.protocolBinding === 'HTTP+JSON')
|
|
478
|
+
: undefined
|
|
479
|
+
const endpoint = jsonrpc && jsonrpc.url ? jsonrpc.url : ''
|
|
480
|
+
const security = card && card.securitySchemes ? Object.keys(card.securitySchemes) : []
|
|
481
|
+
const authRequired = security.length > 0
|
|
482
|
+
const authLine = authRequired ? " \\\n -H 'authorization: Bearer $A2A_TOKEN'" : ''
|
|
483
|
+
const curl = endpoint
|
|
484
|
+
? `curl -s ${endpoint} \\\n -H 'content-type: application/json' \\\n -H 'a2a-version: 1.0'${authLine} \\\n -d '{"jsonrpc":"2.0","id":1,"method":"SendMessage","params":{"message":{"messageId":"m-1","role":"ROLE_USER","parts":[{"text":"Hello"}]}}}'`
|
|
485
|
+
: ''
|
|
486
|
+
const js = endpoint
|
|
487
|
+
? `import { ClientFactory, Role } from '@a2a-js/sdk/client'\n\nconst client = await new ClientFactory().createFromUrl('${new URL(endpoint, window.location.origin).origin}')\nconst result = await client.sendMessage({\n message: {\n messageId: crypto.randomUUID(),\n role: Role.ROLE_USER,\n parts: [{ content: { $case: 'text', value: 'Hello' } }],\n },\n})\nconsole.log(result)`
|
|
488
|
+
: ''
|
|
489
|
+
|
|
490
|
+
const badge = (label, on, key) => h('span', { key, className: 'a2aui-badge', 'data-on': on ? 'true' : 'false' }, label)
|
|
491
|
+
|
|
492
|
+
const header = h(
|
|
493
|
+
'div',
|
|
494
|
+
{ className: 'a2aui-head' },
|
|
495
|
+
h(
|
|
496
|
+
'div',
|
|
497
|
+
{ className: 'a2aui-brand' },
|
|
498
|
+
h(
|
|
499
|
+
'svg',
|
|
500
|
+
{ className: 'a2aui-logo', viewBox: '0 0 64 64', 'aria-hidden': true },
|
|
501
|
+
h('rect', { x: 2, y: 2, width: 60, height: 60, rx: 14, fill: '#2f6df6' }),
|
|
502
|
+
h('circle', { cx: 18, cy: 32, r: 7, fill: 'none', stroke: '#fff', strokeWidth: 3.5 }),
|
|
503
|
+
h('circle', { cx: 46, cy: 32, r: 7, fill: 'none', stroke: '#fff', strokeWidth: 3.5 }),
|
|
504
|
+
h('path', { d: 'M25.5 32h13', stroke: '#fff', strokeWidth: 3.5, strokeLinecap: 'round' }),
|
|
505
|
+
h('path', { d: 'M32 25.5l6.5 6.5-6.5 6.5', fill: 'none', stroke: '#fff', strokeWidth: 3.5, strokeLinecap: 'round', strokeLinejoin: 'round' }),
|
|
506
|
+
),
|
|
507
|
+
h(
|
|
508
|
+
'div',
|
|
509
|
+
null,
|
|
510
|
+
h('h3', { className: 'a2aui-title' }, card ? card.name : t('heading')),
|
|
511
|
+
h('p', { className: 'a2aui-desc' }, card ? card.description : t('intro')),
|
|
512
|
+
),
|
|
513
|
+
),
|
|
514
|
+
h('button', { type: 'button', className: 'a2aui-btn', onClick: () => setReloadKey((value) => value + 1) }, t('refresh')),
|
|
515
|
+
)
|
|
516
|
+
|
|
517
|
+
if (state.status === 'loading' && !card) {
|
|
518
|
+
return h('div', { className: 'a2aui-page' }, header, h('p', { className: 'a2aui-muted' }, t('loading')))
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
if (!card) {
|
|
522
|
+
const missing = state.status === 'missing'
|
|
523
|
+
return h(
|
|
524
|
+
'div',
|
|
525
|
+
{ className: 'a2aui-page' },
|
|
526
|
+
header,
|
|
527
|
+
h(
|
|
528
|
+
'div',
|
|
529
|
+
{ className: 'a2aui-error' },
|
|
530
|
+
h('p', { className: 'a2aui-error-title' }, missing ? t('disabled') : t('loadError')),
|
|
531
|
+
h('p', { className: 'a2aui-muted' }, missing ? t('disabledHint') : t('loadErrorHint')),
|
|
532
|
+
state.error ? h('p', { className: 'a2aui-muted' }, state.error) : null,
|
|
533
|
+
),
|
|
534
|
+
)
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
const dirty = panel
|
|
538
|
+
? panel.fields.some((field) => !sameValue(field, draft[field.path])) || token !== '' || clearToken
|
|
539
|
+
: false
|
|
540
|
+
|
|
541
|
+
const setField = (path, value) => {
|
|
542
|
+
setDraft((previous) => ({ ...previous, [path]: value }))
|
|
543
|
+
setSaved(false)
|
|
544
|
+
setSaveError(null)
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
const save = async () => {
|
|
548
|
+
if (!panel || !state.panelPath || saving) return
|
|
549
|
+
const values = {}
|
|
550
|
+
for (const field of panel.fields) {
|
|
551
|
+
if (sameValue(field, draft[field.path])) continue
|
|
552
|
+
values[field.path] = serializeValue(field, draft[field.path])
|
|
553
|
+
}
|
|
554
|
+
const body = { values }
|
|
555
|
+
if (clearToken) body.clearToken = true
|
|
556
|
+
else if (token !== '') body.token = token
|
|
557
|
+
setSaving(true)
|
|
558
|
+
setSaved(false)
|
|
559
|
+
setSaveError(null)
|
|
560
|
+
try {
|
|
561
|
+
const response = await fetch(state.panelPath, {
|
|
562
|
+
method: 'PUT',
|
|
563
|
+
credentials: 'same-origin',
|
|
564
|
+
headers: { 'content-type': 'application/json' },
|
|
565
|
+
body: JSON.stringify(body),
|
|
566
|
+
})
|
|
567
|
+
const payload = await response.json().catch(() => ({}))
|
|
568
|
+
if (!response.ok) throw new Error(payload.message || payload.error || `HTTP ${response.status}`)
|
|
569
|
+
setState((previous) => ({ ...previous, panel: payload, panelError: null }))
|
|
570
|
+
setDraft(draftFrom(payload))
|
|
571
|
+
setToken('')
|
|
572
|
+
setClearToken(false)
|
|
573
|
+
setSaved(true)
|
|
574
|
+
window.setTimeout(() => setReloadKey((value) => value + 1), 900)
|
|
575
|
+
} catch (error) {
|
|
576
|
+
setSaveError(String((error && error.message) || error))
|
|
577
|
+
} finally {
|
|
578
|
+
setSaving(false)
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
const renderControl = (field) => {
|
|
583
|
+
const value = draft[field.path]
|
|
584
|
+
const onChange = (event) => setField(field.path, event.target.value)
|
|
585
|
+
if (field.type === 'boolean') {
|
|
586
|
+
return h(
|
|
587
|
+
'label',
|
|
588
|
+
{ className: 'a2aui-check' },
|
|
589
|
+
h('input', { type: 'checkbox', checked: Boolean(value), onChange: (event) => setField(field.path, event.target.checked) }),
|
|
590
|
+
value ? 'true' : 'false',
|
|
591
|
+
)
|
|
592
|
+
}
|
|
593
|
+
if (field.type === 'enum') {
|
|
594
|
+
return h(
|
|
595
|
+
'select',
|
|
596
|
+
{ className: 'a2aui-select', value: value ?? '', onChange },
|
|
597
|
+
(field.options ?? []).map((option) => h('option', { key: option, value: option }, enumLabel(t, option))),
|
|
598
|
+
)
|
|
599
|
+
}
|
|
600
|
+
if (field.type === 'textarea') {
|
|
601
|
+
return h('textarea', { className: 'a2aui-textarea', rows: 2, value: value ?? '', onChange })
|
|
602
|
+
}
|
|
603
|
+
if (field.type === 'integer') {
|
|
604
|
+
return h('input', { className: 'a2aui-input', type: 'number', min: field.min, max: field.max, value: value ?? '', onChange })
|
|
605
|
+
}
|
|
606
|
+
return h('input', {
|
|
607
|
+
className: 'a2aui-input',
|
|
608
|
+
type: 'text',
|
|
609
|
+
value: value ?? '',
|
|
610
|
+
placeholder: field.placeholder || (field.nullable ? t('unknown') : ''),
|
|
611
|
+
onChange,
|
|
612
|
+
})
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
const groups = panel
|
|
616
|
+
? [...new Set(panel.fields.map((field) => field.group))].map((group) => [group, panel.fields.filter((field) => field.group === group)])
|
|
617
|
+
: []
|
|
618
|
+
|
|
619
|
+
const secretBlock = panel
|
|
620
|
+
? h(
|
|
621
|
+
'div',
|
|
622
|
+
{ className: 'a2aui-group' },
|
|
623
|
+
h('h5', { className: 'a2aui-group-title' }, t('token')),
|
|
624
|
+
h(
|
|
625
|
+
'div',
|
|
626
|
+
{ className: 'a2aui-secret' },
|
|
627
|
+
h('input', {
|
|
628
|
+
className: 'a2aui-input',
|
|
629
|
+
type: 'password',
|
|
630
|
+
autoComplete: 'new-password',
|
|
631
|
+
value: token,
|
|
632
|
+
placeholder: t('tokenPlaceholder'),
|
|
633
|
+
onChange: (event) => {
|
|
634
|
+
setToken(event.target.value)
|
|
635
|
+
setSaved(false)
|
|
636
|
+
setSaveError(null)
|
|
637
|
+
},
|
|
638
|
+
}),
|
|
639
|
+
h(
|
|
640
|
+
'label',
|
|
641
|
+
{ className: 'a2aui-check' },
|
|
642
|
+
h('input', {
|
|
643
|
+
type: 'checkbox',
|
|
644
|
+
checked: clearToken,
|
|
645
|
+
onChange: (event) => {
|
|
646
|
+
setClearToken(event.target.checked)
|
|
647
|
+
setSaved(false)
|
|
648
|
+
},
|
|
649
|
+
}),
|
|
650
|
+
t('clearToken'),
|
|
651
|
+
),
|
|
652
|
+
h(
|
|
653
|
+
'p',
|
|
654
|
+
{ className: 'a2aui-field-note' },
|
|
655
|
+
panel.secrets && panel.secrets.inline ? t('tokenConfigured') : t('tokenNotConfigured'),
|
|
656
|
+
panel.secrets && panel.secrets.tokenEnv
|
|
657
|
+
? ` · ${t('tokenEnvConfigured').replace('{name}', panel.secrets.tokenEnv)}`
|
|
658
|
+
: '',
|
|
659
|
+
),
|
|
660
|
+
h('p', { className: 'a2aui-field-note' }, t('tokenHint')),
|
|
661
|
+
),
|
|
662
|
+
)
|
|
663
|
+
: null
|
|
664
|
+
|
|
665
|
+
const configSection = h(
|
|
666
|
+
'section',
|
|
667
|
+
{ className: 'a2aui-card' },
|
|
668
|
+
h('h4', { className: 'a2aui-section-title' }, t('config')),
|
|
669
|
+
panel
|
|
670
|
+
? h(
|
|
671
|
+
'div',
|
|
672
|
+
{ className: 'a2aui-form' },
|
|
673
|
+
...groups.map(([group, fields]) =>
|
|
674
|
+
h(
|
|
675
|
+
'div',
|
|
676
|
+
{ className: 'a2aui-group', key: group },
|
|
677
|
+
h('h5', { className: 'a2aui-group-title' }, groupLabel(t, group)),
|
|
678
|
+
...fields.map((field) =>
|
|
679
|
+
h(
|
|
680
|
+
'div',
|
|
681
|
+
{ className: 'a2aui-field', key: field.path },
|
|
682
|
+
h('label', { htmlFor: `a2aui-${field.path}` }, fieldLabel(t, field.path)),
|
|
683
|
+
h('div', null, renderControl(field)),
|
|
684
|
+
),
|
|
685
|
+
),
|
|
686
|
+
),
|
|
687
|
+
),
|
|
688
|
+
secretBlock,
|
|
689
|
+
h(
|
|
690
|
+
'div',
|
|
691
|
+
{ className: 'a2aui-form-foot' },
|
|
692
|
+
h(
|
|
693
|
+
'button',
|
|
694
|
+
{ type: 'button', className: 'a2aui-btn a2aui-btnPrimary', disabled: !dirty || saving, onClick: save },
|
|
695
|
+
saving ? t('saving') : t('save'),
|
|
696
|
+
),
|
|
697
|
+
h('button', { type: 'button', className: 'a2aui-btn', onClick: () => setReloadKey((value) => value + 1) }, t('reload')),
|
|
698
|
+
dirty ? h('span', { className: 'a2aui-status' }, t('dirty')) : null,
|
|
699
|
+
saved ? h('span', { className: 'a2aui-status', 'data-tone': 'ok' }, t('saved')) : null,
|
|
700
|
+
saveError ? h('span', { className: 'a2aui-status', 'data-tone': 'error' }, `${t('saveError')}: ${saveError}`) : null,
|
|
701
|
+
h('span', { className: 'a2aui-status' }, t('configHint')),
|
|
702
|
+
h('span', { className: 'a2aui-status' }, t('restartHint')),
|
|
703
|
+
),
|
|
704
|
+
)
|
|
705
|
+
: h(
|
|
706
|
+
'p',
|
|
707
|
+
{ className: 'a2aui-muted' },
|
|
708
|
+
state.panelError === 'unauthorized'
|
|
709
|
+
? t('configUnauthorized')
|
|
710
|
+
: state.panelError === 'unavailable'
|
|
711
|
+
? t('configUnavailable')
|
|
712
|
+
: `${t('configLoadError')}${state.panelPath ? `(${state.panelPath})` : ''}`,
|
|
713
|
+
),
|
|
714
|
+
)
|
|
715
|
+
|
|
716
|
+
const statusSection = h(
|
|
717
|
+
'section',
|
|
718
|
+
{ className: 'a2aui-card' },
|
|
719
|
+
h('h4', { className: 'a2aui-section-title' }, t('capabilities')),
|
|
720
|
+
h(
|
|
721
|
+
'div',
|
|
722
|
+
{ className: 'a2aui-badges' },
|
|
723
|
+
badge(`${t('version')} ${jsonrpc ? jsonrpc.protocolVersion || '1.0' : '1.0'}`, true),
|
|
724
|
+
badge(t('streaming'), Boolean(card.capabilities && card.capabilities.streaming)),
|
|
725
|
+
badge(t('push'), Boolean(card.capabilities && card.capabilities.pushNotifications)),
|
|
726
|
+
badge(t('extended'), Boolean(card.capabilities && card.capabilities.extendedAgentCard)),
|
|
727
|
+
h(
|
|
728
|
+
'span',
|
|
729
|
+
{ className: 'a2aui-badge', 'data-tone': authRequired ? 'warn' : undefined },
|
|
730
|
+
`${t('auth')}: ${authRequired ? t('authOn') : t('authNone')}`,
|
|
731
|
+
),
|
|
732
|
+
),
|
|
733
|
+
)
|
|
734
|
+
|
|
735
|
+
const endpointSection = h(
|
|
736
|
+
'section',
|
|
737
|
+
{ className: 'a2aui-card' },
|
|
738
|
+
h('h4', { className: 'a2aui-section-title' }, t('interfaces')),
|
|
739
|
+
jsonrpc
|
|
740
|
+
? h(
|
|
741
|
+
'div',
|
|
742
|
+
{ className: 'a2aui-row' },
|
|
743
|
+
h('span', { className: 'a2aui-row-label' }, t('jsonrpc')),
|
|
744
|
+
h('code', { className: 'a2aui-code' }, jsonrpc.url),
|
|
745
|
+
h(CopyButton, { id: 'jsonrpc', text: jsonrpc.url, label: { copy: t('copy'), copied: t('copied') }, copied, onCopy: writeClipboard }),
|
|
746
|
+
)
|
|
747
|
+
: null,
|
|
748
|
+
rest
|
|
749
|
+
? h(
|
|
750
|
+
'div',
|
|
751
|
+
{ className: 'a2aui-row' },
|
|
752
|
+
h('span', { className: 'a2aui-row-label' }, t('rest')),
|
|
753
|
+
h('code', { className: 'a2aui-code' }, rest.url),
|
|
754
|
+
h(CopyButton, { id: 'rest', text: rest.url, label: { copy: t('copy'), copied: t('copied') }, copied, onCopy: writeClipboard }),
|
|
755
|
+
)
|
|
756
|
+
: null,
|
|
757
|
+
h(
|
|
758
|
+
'div',
|
|
759
|
+
{ className: 'a2aui-foot' },
|
|
760
|
+
h('a', { className: 'a2aui-link', href: CARD_PATH, target: '_blank', rel: 'noreferrer' }, t('openCard')),
|
|
761
|
+
),
|
|
762
|
+
)
|
|
763
|
+
|
|
764
|
+
const runtimeSection = health
|
|
765
|
+
? h(
|
|
766
|
+
'section',
|
|
767
|
+
{ className: 'a2aui-card' },
|
|
768
|
+
h('h4', { className: 'a2aui-section-title' }, t('runtime')),
|
|
769
|
+
h(
|
|
770
|
+
'div',
|
|
771
|
+
{ className: 'a2aui-facts' },
|
|
772
|
+
h('div', { className: 'a2aui-fact' }, h('span', { className: 'a2aui-fact-key' }, t('tasks')), h('span', { className: 'a2aui-fact-value' }, String(health.tasks ? health.tasks.tasks : t('unknown')))),
|
|
773
|
+
h('div', { className: 'a2aui-fact' }, h('span', { className: 'a2aui-fact-key' }, t('contexts')), h('span', { className: 'a2aui-fact-value' }, String(health.agents ? health.agents.contexts : t('unknown')))),
|
|
774
|
+
h('div', { className: 'a2aui-fact' }, h('span', { className: 'a2aui-fact-key' }, t('concurrency')), h('span', { className: 'a2aui-fact-value' }, health.concurrency ? `${health.concurrency.active}/${health.concurrency.max}` : t('unknown'))),
|
|
775
|
+
h('div', { className: 'a2aui-fact' }, h('span', { className: 'a2aui-fact-key' }, t('uptime')), h('span', { className: 'a2aui-fact-value' }, formatUptime(health.uptimeSeconds, t))),
|
|
776
|
+
),
|
|
777
|
+
)
|
|
778
|
+
: null
|
|
779
|
+
|
|
780
|
+
const deniedSection = state.denied
|
|
781
|
+
? h('section', { className: 'a2aui-card' }, h('p', { className: 'a2aui-muted' }, t('runtimeDenied')))
|
|
782
|
+
: null
|
|
783
|
+
|
|
784
|
+
const skills = Array.isArray(card.skills) ? card.skills : []
|
|
785
|
+
const skillsSection = h(
|
|
786
|
+
'section',
|
|
787
|
+
{ className: 'a2aui-card' },
|
|
788
|
+
h('h4', { className: 'a2aui-section-title' }, t('skills')),
|
|
789
|
+
skills.length === 0
|
|
790
|
+
? h('p', { className: 'a2aui-muted' }, t('noSkills'))
|
|
791
|
+
: h(
|
|
792
|
+
'ul',
|
|
793
|
+
{ className: 'a2aui-skills' },
|
|
794
|
+
skills.map((skill) =>
|
|
795
|
+
h(
|
|
796
|
+
'li',
|
|
797
|
+
{ key: skill.id },
|
|
798
|
+
h('div', { className: 'a2aui-skill-name' }, skill.name),
|
|
799
|
+
h('p', { className: 'a2aui-skill-desc' }, skill.description),
|
|
800
|
+
Array.isArray(skill.tags) && skill.tags.length > 0
|
|
801
|
+
? h('div', { className: 'a2aui-badges' }, skill.tags.map((tag) => badge(tag, false, tag)))
|
|
802
|
+
: null,
|
|
803
|
+
),
|
|
804
|
+
),
|
|
805
|
+
),
|
|
806
|
+
)
|
|
807
|
+
|
|
808
|
+
const quickstartSection = h(
|
|
809
|
+
'section',
|
|
810
|
+
{ className: 'a2aui-card' },
|
|
811
|
+
h('h4', { className: 'a2aui-section-title' }, t('quickstart')),
|
|
812
|
+
h('pre', { className: 'a2aui-pre' }, curl),
|
|
813
|
+
h(
|
|
814
|
+
'div',
|
|
815
|
+
{ className: 'a2aui-foot' },
|
|
816
|
+
h(CopyButton, { id: 'curl', text: curl, label: { copy: t('copy'), copied: t('copied') }, copied, onCopy: writeClipboard }),
|
|
817
|
+
h(
|
|
818
|
+
'details',
|
|
819
|
+
{ className: 'a2aui-details' },
|
|
820
|
+
h('summary', null, t('jsClient')),
|
|
821
|
+
h('pre', { className: 'a2aui-pre' }, js),
|
|
822
|
+
),
|
|
823
|
+
h(CopyButton, { id: 'js', text: js, label: { copy: t('copy'), copied: t('copied') }, copied, onCopy: writeClipboard }),
|
|
824
|
+
),
|
|
825
|
+
)
|
|
826
|
+
|
|
827
|
+
return h(
|
|
828
|
+
'div',
|
|
829
|
+
{ className: 'a2aui-page' },
|
|
830
|
+
header,
|
|
831
|
+
statusSection,
|
|
832
|
+
endpointSection,
|
|
833
|
+
runtimeSection,
|
|
834
|
+
deniedSection,
|
|
835
|
+
configSection,
|
|
836
|
+
skillsSection,
|
|
837
|
+
quickstartSection,
|
|
838
|
+
)
|
|
839
|
+
}
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
return {
|
|
843
|
+
inject: ['slots', 'locale'],
|
|
844
|
+
apply(ctx) {
|
|
845
|
+
const t = ctx.locale.bind(NS)
|
|
846
|
+
ctx.effect(() => ctx.locale.register(NS, { zh, en }), 'a2a-ui: dictionaries')
|
|
847
|
+
ctx.effect(() => {
|
|
848
|
+
const tag = document.createElement('style')
|
|
849
|
+
tag.dataset.plugin = '@ai_mind/dsh-a2a'
|
|
850
|
+
tag.textContent = CSS
|
|
851
|
+
document.head.appendChild(tag)
|
|
852
|
+
return () => tag.remove()
|
|
853
|
+
}, 'a2a-ui: styles')
|
|
854
|
+
ctx.slots.inject('settings.plugins.tab', () =>
|
|
855
|
+
ctx.slots.register(
|
|
856
|
+
{
|
|
857
|
+
name: 'settings.plugins.tab',
|
|
858
|
+
id: 'a2a-server',
|
|
859
|
+
order: 20,
|
|
860
|
+
label: () => t('tab'),
|
|
861
|
+
},
|
|
862
|
+
createTab(t),
|
|
863
|
+
),
|
|
864
|
+
)
|
|
865
|
+
},
|
|
866
|
+
}
|
|
867
|
+
},
|
|
868
|
+
})
|