@ai_mind/dsh-a2a 1.1.3 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -1
- package/client.js +107 -2
- package/lib/panel.js +106 -6
- package/lib/server.js +7 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -115,7 +115,7 @@
|
|
|
115
115
|
面板分三部分:
|
|
116
116
|
|
|
117
117
|
1. **概览**:实时 Agent Card、JSON-RPC / HTTP+JSON 地址(一键复制)、流式/推送/扩展卡片能力、鉴权状态、运行状态与可复制的 `curl` / 官方 JS 客户端示例。
|
|
118
|
-
2. **配置表单**:直接编辑名称、描述、对外地址、挂载路径、流式/推送/扩展卡片、鉴权模式与令牌环境变量、访问令牌(写入后不再回显)、Agent
|
|
118
|
+
2. **配置表单**:直接编辑名称、描述、对外地址、挂载路径、流式/推送/扩展卡片、鉴权模式与令牌环境变量、访问令牌(写入后不再回显)、Agent 预设/模型/工作目录/沙箱/审批/并发/超时,以及请求体与任务保留限额。预设、模型提供方、模型和推理强度会从当前 Host 的 `agentPresets` / `llm` 服务动态列出选项;留空即使用部署默认,无法列出的自定义值仍可手动输入。
|
|
119
119
|
3. **保存**:写入 profile 的 `cordis.patch.yml`(`a2a-server` 行),校验通过后插件自动重载;修改挂载路径后刷新页面即可。
|
|
120
120
|
|
|
121
121
|
配置接口 `GET/PUT <mountPath>/panel/config` 始终位于 DSH Web 的 origin(独立端口模式下也不在 A2A 端口上),使用 **Harness Web 连接凭据**(不是 A2A token)鉴权:只有能打开 Settings 的已登录浏览器可以读写,远程 A2A 客户端即使知道路径也会收到 401。令牌字段是只写字段,GET 只返回“是否已配置”。
|
|
@@ -322,8 +322,11 @@ A2A 前面的 Nginx/Caddy 等反代需要保留原始 `Host` 和协议头:
|
|
|
322
322
|
proxy_set_header Host $host;
|
|
323
323
|
proxy_set_header X-Forwarded-Proto $scheme;
|
|
324
324
|
proxy_set_header X-Forwarded-Host $host;
|
|
325
|
+
proxy_set_header X-Forwarded-Port $server_port;
|
|
325
326
|
```
|
|
326
327
|
|
|
328
|
+
自定义外部端口时,代理除了 `Host` 还要传 `X-Forwarded-Port`(或让 `Host` 直接包含端口),卡片才会带上正确端口。
|
|
329
|
+
|
|
327
330
|
并设置:
|
|
328
331
|
|
|
329
332
|
```yaml
|
package/client.js
CHANGED
|
@@ -108,6 +108,9 @@ window.__ModuleLoader__.load({
|
|
|
108
108
|
fieldAgentProvider: '模型提供方',
|
|
109
109
|
fieldAgentModel: '模型',
|
|
110
110
|
fieldAgentReasoningEffort: '推理强度',
|
|
111
|
+
optionDefault: '(使用默认)',
|
|
112
|
+
defaultModel: '默认模型(留空时)',
|
|
113
|
+
modelPairHint: '模型提供方和模型必须同时选择,或都留空使用默认。',
|
|
111
114
|
fieldAgentSandboxMode: '沙箱模式',
|
|
112
115
|
fieldAgentApprovalPolicy: '审批策略',
|
|
113
116
|
fieldAgentMaxConcurrentTasks: '最大并发任务',
|
|
@@ -220,6 +223,9 @@ window.__ModuleLoader__.load({
|
|
|
220
223
|
fieldAgentProvider: 'Model provider',
|
|
221
224
|
fieldAgentModel: 'Model',
|
|
222
225
|
fieldAgentReasoningEffort: 'Reasoning effort',
|
|
226
|
+
optionDefault: '(use default)',
|
|
227
|
+
defaultModel: 'Default model (when empty)',
|
|
228
|
+
modelPairHint: 'Choose both a provider and a model, or leave both empty for the default.',
|
|
223
229
|
fieldAgentSandboxMode: 'Sandbox mode',
|
|
224
230
|
fieldAgentApprovalPolicy: 'Approval policy',
|
|
225
231
|
fieldAgentMaxConcurrentTasks: 'Max concurrent tasks',
|
|
@@ -369,6 +375,15 @@ window.__ModuleLoader__.load({
|
|
|
369
375
|
return typeof label === 'string' && label !== key ? label : value
|
|
370
376
|
}
|
|
371
377
|
|
|
378
|
+
function panelOptionsPath(panelPath, provider, model) {
|
|
379
|
+
const base = panelPath.replace(/\/config$/, '/options')
|
|
380
|
+
const params = new URLSearchParams()
|
|
381
|
+
if (provider) params.set('provider', provider)
|
|
382
|
+
if (model) params.set('model', model)
|
|
383
|
+
const query = params.toString()
|
|
384
|
+
return query ? `${base}?${query}` : base
|
|
385
|
+
}
|
|
386
|
+
|
|
372
387
|
function draftFrom(panel) {
|
|
373
388
|
const values = {}
|
|
374
389
|
for (const field of panel && Array.isArray(panel.fields) ? panel.fields : []) values[field.path] = field.value
|
|
@@ -428,6 +443,37 @@ window.__ModuleLoader__.load({
|
|
|
428
443
|
const [saveError, setSaveError] = React.useState(null)
|
|
429
444
|
const [copied, setCopied] = React.useState(null)
|
|
430
445
|
const [reloadKey, setReloadKey] = React.useState(0)
|
|
446
|
+
const [optionSets, setOptionSets] = React.useState({})
|
|
447
|
+
const optionsRequest = React.useRef(0)
|
|
448
|
+
|
|
449
|
+
const loadOptions = async (panelPath, provider, model, signal) => {
|
|
450
|
+
if (!panelPath) return
|
|
451
|
+
const requestId = ++optionsRequest.current
|
|
452
|
+
try {
|
|
453
|
+
const response = await fetch(panelOptionsPath(panelPath, provider, model), {
|
|
454
|
+
cache: 'no-store',
|
|
455
|
+
credentials: 'same-origin',
|
|
456
|
+
signal,
|
|
457
|
+
})
|
|
458
|
+
if (!response.ok) return
|
|
459
|
+
const data = await response.json()
|
|
460
|
+
if (requestId !== optionsRequest.current) return
|
|
461
|
+
setOptionSets(data)
|
|
462
|
+
if (provider && !model && Array.isArray(data.models) && data.models.length > 0) {
|
|
463
|
+
const preferred =
|
|
464
|
+
data.default && data.default.provider === provider && data.models.some((entry) => entry.value === data.default.model)
|
|
465
|
+
? data.default.model
|
|
466
|
+
: data.models[0].value
|
|
467
|
+
setDraft((previous) => {
|
|
468
|
+
if (previous['agent.provider'] !== provider || previous['agent.model']) return previous
|
|
469
|
+
return { ...previous, 'agent.model': preferred, 'agent.reasoningEffort': '' }
|
|
470
|
+
})
|
|
471
|
+
void loadOptions(panelPath, provider, preferred)
|
|
472
|
+
}
|
|
473
|
+
} catch {
|
|
474
|
+
/* options are optional; the text inputs remain usable */
|
|
475
|
+
}
|
|
476
|
+
}
|
|
431
477
|
|
|
432
478
|
React.useEffect(() => {
|
|
433
479
|
const controller = new AbortController()
|
|
@@ -487,6 +533,10 @@ window.__ModuleLoader__.load({
|
|
|
487
533
|
}
|
|
488
534
|
setState({ status: 'ready', card, health, denied, error: null, panel, panelPath, panelError })
|
|
489
535
|
setDraft(draftFrom(panel))
|
|
536
|
+
if (panel && panelPath) {
|
|
537
|
+
const fieldValue = (path) => panel.fields.find((field) => field.path === path)?.value
|
|
538
|
+
void loadOptions(panelPath, fieldValue('agent.provider'), fieldValue('agent.model'), controller.signal)
|
|
539
|
+
}
|
|
490
540
|
} catch (error) {
|
|
491
541
|
if (error && error.name === 'AbortError') return
|
|
492
542
|
setState({ status: 'error', card: null, health: null, denied: false, error: String((error && error.message) || error), panel: null, panelPath: null, panelError: null })
|
|
@@ -573,11 +623,35 @@ window.__ModuleLoader__.load({
|
|
|
573
623
|
const dirty = panel
|
|
574
624
|
? panel.fields.some((field) => !sameValue(field, draft[field.path])) || token !== '' || clearToken
|
|
575
625
|
: false
|
|
626
|
+
const invalidModelPair = Boolean(draft['agent.provider']) !== Boolean(draft['agent.model'])
|
|
576
627
|
|
|
577
628
|
const setField = (path, value) => {
|
|
578
|
-
setDraft((previous) =>
|
|
629
|
+
setDraft((previous) => {
|
|
630
|
+
const next = { ...previous, [path]: value }
|
|
631
|
+
if (path === 'agent.provider') {
|
|
632
|
+
next['agent.model'] = ''
|
|
633
|
+
next['agent.reasoningEffort'] = ''
|
|
634
|
+
} else if (path === 'agent.model') {
|
|
635
|
+
if (value === '') next['agent.provider'] = ''
|
|
636
|
+
else if (!previous['agent.provider']) {
|
|
637
|
+
next['agent.provider'] = optionSets.provider ?? optionSets.default?.provider ?? ''
|
|
638
|
+
}
|
|
639
|
+
next['agent.reasoningEffort'] = ''
|
|
640
|
+
}
|
|
641
|
+
return next
|
|
642
|
+
})
|
|
579
643
|
setSaved(false)
|
|
580
644
|
setSaveError(null)
|
|
645
|
+
if (path === 'agent.provider') {
|
|
646
|
+
void loadOptions(state.panelPath, value ?? '', '')
|
|
647
|
+
} else if (path === 'agent.model') {
|
|
648
|
+
if (value === '') {
|
|
649
|
+
void loadOptions(state.panelPath, '', '')
|
|
650
|
+
} else {
|
|
651
|
+
const provider = draft['agent.provider'] || optionSets.provider || optionSets.default?.provider || ''
|
|
652
|
+
void loadOptions(state.panelPath, provider, value)
|
|
653
|
+
}
|
|
654
|
+
}
|
|
581
655
|
}
|
|
582
656
|
|
|
583
657
|
const save = async () => {
|
|
@@ -618,6 +692,26 @@ window.__ModuleLoader__.load({
|
|
|
618
692
|
const renderControl = (field) => {
|
|
619
693
|
const value = draft[field.path]
|
|
620
694
|
const onChange = (event) => setField(field.path, event.target.value)
|
|
695
|
+
const dynamicOptions = field.optionsKey ? optionSets[field.optionsKey] : undefined
|
|
696
|
+
if (field.optionsKey && Array.isArray(dynamicOptions) && dynamicOptions.length > 0) {
|
|
697
|
+
const current = value == null ? '' : String(value)
|
|
698
|
+
const options =
|
|
699
|
+
!current || dynamicOptions.some((option) => String(option.value) === current)
|
|
700
|
+
? dynamicOptions
|
|
701
|
+
: [{ value: current, label: `${current} (current)` }, ...dynamicOptions]
|
|
702
|
+
return h(
|
|
703
|
+
'select',
|
|
704
|
+
{ className: 'a2aui-select', value: current, onChange },
|
|
705
|
+
h('option', { value: '' }, t('optionDefault')),
|
|
706
|
+
options.map((option) =>
|
|
707
|
+
h(
|
|
708
|
+
'option',
|
|
709
|
+
{ key: option.value, value: option.value },
|
|
710
|
+
option.description ? `${option.label} — ${option.description}` : option.label,
|
|
711
|
+
),
|
|
712
|
+
),
|
|
713
|
+
)
|
|
714
|
+
}
|
|
621
715
|
if (field.type === 'boolean') {
|
|
622
716
|
return h(
|
|
623
717
|
'label',
|
|
@@ -719,6 +813,14 @@ window.__ModuleLoader__.load({
|
|
|
719
813
|
h('div', null, renderControl(field)),
|
|
720
814
|
),
|
|
721
815
|
),
|
|
816
|
+
group === 'agent' && optionSets.default
|
|
817
|
+
? h(
|
|
818
|
+
'p',
|
|
819
|
+
{ className: 'a2aui-field-note' },
|
|
820
|
+
`${t('defaultModel')}: ${optionSets.default.provider ?? '?'} / ${optionSets.default.model ?? '?'}` +
|
|
821
|
+
(optionSets.default.reasoningEffort ? ` · ${optionSets.default.reasoningEffort}` : ''),
|
|
822
|
+
)
|
|
823
|
+
: null,
|
|
722
824
|
)
|
|
723
825
|
return group === 'auth' ? [node, secretBlock] : [node]
|
|
724
826
|
}),
|
|
@@ -727,11 +829,14 @@ window.__ModuleLoader__.load({
|
|
|
727
829
|
{ className: 'a2aui-form-foot' },
|
|
728
830
|
h(
|
|
729
831
|
'button',
|
|
730
|
-
{ type: 'button', className: 'a2aui-btn a2aui-btnPrimary', disabled: !dirty || saving, onClick: save },
|
|
832
|
+
{ type: 'button', className: 'a2aui-btn a2aui-btnPrimary', disabled: !dirty || saving || invalidModelPair, onClick: save },
|
|
731
833
|
saving ? t('saving') : t('save'),
|
|
732
834
|
),
|
|
733
835
|
h('button', { type: 'button', className: 'a2aui-btn', onClick: () => setReloadKey((value) => value + 1) }, t('reload')),
|
|
734
836
|
dirty ? h('span', { className: 'a2aui-status' }, t('dirty')) : null,
|
|
837
|
+
invalidModelPair
|
|
838
|
+
? h('span', { className: 'a2aui-status', 'data-tone': 'warn' }, t('modelPairHint'))
|
|
839
|
+
: null,
|
|
735
840
|
saved ? h('span', { className: 'a2aui-status', 'data-tone': 'ok' }, t('saved')) : null,
|
|
736
841
|
saveError ? h('span', { className: 'a2aui-status', 'data-tone': 'error' }, `${t('saveError')}: ${saveError}`) : null,
|
|
737
842
|
h('span', { className: 'a2aui-status' }, t('configHint')),
|
package/lib/panel.js
CHANGED
|
@@ -49,10 +49,10 @@ const FIELDS = [
|
|
|
49
49
|
{ path: 'auth.tokenEnv', type: 'string', group: 'auth', nullable: true, placeholder: 'A2A_AUTH_TOKEN' },
|
|
50
50
|
{ path: 'auth.allowRemoteUnauthenticated', type: 'boolean', group: 'auth' },
|
|
51
51
|
{ path: 'agent.cwd', type: 'string', group: 'agent', nullable: true, placeholder: '/srv/a2a-workspace' },
|
|
52
|
-
{ path: 'agent.preset', type: 'string', group: 'agent', nullable: true },
|
|
53
|
-
{ path: 'agent.provider', type: 'string', group: 'agent', nullable: true },
|
|
54
|
-
{ path: 'agent.model', type: 'string', group: 'agent', nullable: true },
|
|
55
|
-
{ path: 'agent.reasoningEffort', type: 'string', group: 'agent', nullable: true },
|
|
52
|
+
{ path: 'agent.preset', type: 'string', group: 'agent', nullable: true, optionsKey: 'presets' },
|
|
53
|
+
{ path: 'agent.provider', type: 'string', group: 'agent', nullable: true, optionsKey: 'providers' },
|
|
54
|
+
{ path: 'agent.model', type: 'string', group: 'agent', nullable: true, optionsKey: 'models' },
|
|
55
|
+
{ path: 'agent.reasoningEffort', type: 'string', group: 'agent', nullable: true, optionsKey: 'reasoning' },
|
|
56
56
|
{
|
|
57
57
|
path: 'agent.sandboxMode',
|
|
58
58
|
type: 'enum',
|
|
@@ -175,6 +175,7 @@ function describe(config, entry, warnings) {
|
|
|
175
175
|
...(field.min === undefined ? {} : { min: field.min }),
|
|
176
176
|
...(field.max === undefined ? {} : { max: field.max }),
|
|
177
177
|
...(field.placeholder === undefined ? {} : { placeholder: field.placeholder }),
|
|
178
|
+
...(field.optionsKey === undefined ? {} : { optionsKey: field.optionsKey }),
|
|
178
179
|
value: value === undefined ? null : value,
|
|
179
180
|
}
|
|
180
181
|
})
|
|
@@ -202,6 +203,23 @@ function describe(config, entry, warnings) {
|
|
|
202
203
|
* @param {() => object} [options.health] live health snapshot
|
|
203
204
|
* @returns {import('express').Router}
|
|
204
205
|
*/
|
|
206
|
+
/** First value of an express query parameter. */
|
|
207
|
+
function firstQuery(value) {
|
|
208
|
+
if (Array.isArray(value)) return value[0]
|
|
209
|
+
return typeof value === 'string' ? value : undefined
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/** Resolve an optional Host call without letting a slow model catalog block the panel. */
|
|
213
|
+
function withTimeout(promise, fallback, ms) {
|
|
214
|
+
return Promise.race([
|
|
215
|
+
Promise.resolve(promise).catch(() => fallback),
|
|
216
|
+
new Promise((resolve) => {
|
|
217
|
+
const timer = setTimeout(() => resolve(fallback), ms)
|
|
218
|
+
timer.unref?.()
|
|
219
|
+
}),
|
|
220
|
+
])
|
|
221
|
+
}
|
|
222
|
+
|
|
205
223
|
function safeHealth(health, logger) {
|
|
206
224
|
try {
|
|
207
225
|
return health()
|
|
@@ -287,7 +305,7 @@ export function createPanelRouter({ ctx, config, mountPath, logger, status, heal
|
|
|
287
305
|
res.setHeader('Cache-Control', 'no-store')
|
|
288
306
|
res.json({
|
|
289
307
|
ok: true,
|
|
290
|
-
plugin: { name: PLUGIN_NAME, version: '1.
|
|
308
|
+
plugin: { name: PLUGIN_NAME, version: '1.2.0' },
|
|
291
309
|
mountPath,
|
|
292
310
|
...payload,
|
|
293
311
|
...(typeof status === 'function' ? { server: status() } : {}),
|
|
@@ -295,6 +313,88 @@ export function createPanelRouter({ ctx, config, mountPath, logger, status, heal
|
|
|
295
313
|
})
|
|
296
314
|
})
|
|
297
315
|
|
|
316
|
+
router.get('/options', async (req, res) => {
|
|
317
|
+
if (authorize(req, res)) return
|
|
318
|
+
const llm = ctx.get?.('llm')
|
|
319
|
+
const presetsService = ctx.get?.('agentPresets')
|
|
320
|
+
const defaults = ctx.get?.('agentDefaultModel')
|
|
321
|
+
let selection = null
|
|
322
|
+
try {
|
|
323
|
+
selection = defaults?.currentSelection?.() ?? null
|
|
324
|
+
} catch (error) {
|
|
325
|
+
logger.warn(`[a2a] cannot read the default model selection: ${errorMessage(error)}`)
|
|
326
|
+
}
|
|
327
|
+
const requestedProvider = firstQuery(req.query.provider)?.trim() || null
|
|
328
|
+
const requestedModel = firstQuery(req.query.model)?.trim() || null
|
|
329
|
+
const provider = requestedProvider ?? selection?.provider ?? null
|
|
330
|
+
const model = requestedModel ?? (provider === selection?.provider ? selection?.model : null) ?? null
|
|
331
|
+
|
|
332
|
+
let presets = []
|
|
333
|
+
try {
|
|
334
|
+
const listed = await withTimeout(presetsService?.list?.(), [], 3000)
|
|
335
|
+
presets = (listed ?? []).map((preset) => ({
|
|
336
|
+
value: preset.id,
|
|
337
|
+
label: preset.name ?? preset.id,
|
|
338
|
+
description: preset.broken ? `unavailable: ${preset.broken}` : (preset.description ?? null),
|
|
339
|
+
}))
|
|
340
|
+
} catch (error) {
|
|
341
|
+
logger.warn(`[a2a] cannot list agent presets: ${errorMessage(error)}`)
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
let providers = []
|
|
345
|
+
try {
|
|
346
|
+
providers = (llm?.listProviders?.() ?? []).map((entry) => ({
|
|
347
|
+
value: entry.id,
|
|
348
|
+
label: entry.name ?? entry.id,
|
|
349
|
+
}))
|
|
350
|
+
} catch (error) {
|
|
351
|
+
logger.warn(`[a2a] cannot list model providers: ${errorMessage(error)}`)
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
let models = []
|
|
355
|
+
if (provider !== null && typeof llm?.listModels === 'function') {
|
|
356
|
+
try {
|
|
357
|
+
const listed = await withTimeout(llm.listModels(provider), [], 4000)
|
|
358
|
+
models = (listed ?? []).map((entry) => ({
|
|
359
|
+
value: entry.id,
|
|
360
|
+
label: entry.name && entry.name !== entry.id ? `${entry.name} (${entry.id})` : entry.id,
|
|
361
|
+
description: entry.description ?? null,
|
|
362
|
+
}))
|
|
363
|
+
} catch (error) {
|
|
364
|
+
logger.warn(`[a2a] cannot list models for ${provider}: ${errorMessage(error)}`)
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
let reasoning = []
|
|
369
|
+
let reasoningDefault = null
|
|
370
|
+
if (provider !== null && model !== null && typeof llm?.resolveModelInfo === 'function') {
|
|
371
|
+
try {
|
|
372
|
+
const info = await withTimeout(llm.resolveModelInfo(provider, model), null, 4000)
|
|
373
|
+
reasoning = (info?.reasoning?.efforts ?? []).map((effort) => ({
|
|
374
|
+
value: effort.id,
|
|
375
|
+
label: effort.name ?? effort.id,
|
|
376
|
+
description: effort.description ?? null,
|
|
377
|
+
}))
|
|
378
|
+
reasoningDefault = info?.reasoning?.defaultEffort ?? null
|
|
379
|
+
} catch (error) {
|
|
380
|
+
logger.warn(`[a2a] cannot resolve reasoning options for ${provider}/${model}: ${errorMessage(error)}`)
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
res.setHeader('Cache-Control', 'no-store')
|
|
385
|
+
res.json({
|
|
386
|
+
ok: true,
|
|
387
|
+
presets,
|
|
388
|
+
providers,
|
|
389
|
+
models,
|
|
390
|
+
reasoning,
|
|
391
|
+
reasoningDefault,
|
|
392
|
+
default: selection,
|
|
393
|
+
provider,
|
|
394
|
+
model,
|
|
395
|
+
})
|
|
396
|
+
})
|
|
397
|
+
|
|
298
398
|
router.put('/config', json, async (req, res) => {
|
|
299
399
|
if (authorize(req, res)) return
|
|
300
400
|
const configEditor = editor()
|
|
@@ -384,7 +484,7 @@ export function createPanelRouter({ ctx, config, mountPath, logger, status, heal
|
|
|
384
484
|
logger.info('[a2a] configuration updated from the settings panel')
|
|
385
485
|
res.json({
|
|
386
486
|
ok: true,
|
|
387
|
-
plugin: { name: PLUGIN_NAME, version: '1.
|
|
487
|
+
plugin: { name: PLUGIN_NAME, version: '1.2.0' },
|
|
388
488
|
mountPath,
|
|
389
489
|
...describe(live, liveEntry, []),
|
|
390
490
|
...(typeof status === 'function' ? { server: status() } : {}),
|
package/lib/server.js
CHANGED
|
@@ -94,6 +94,13 @@ function baseFromHeaders(headers, config, fallbackProtocol = 'http') {
|
|
|
94
94
|
try {
|
|
95
95
|
const url = new URL(`${protocol}://${host}`)
|
|
96
96
|
if (url.protocol !== 'http:' && url.protocol !== 'https:') return null
|
|
97
|
+
// A proxy may expose the service on a non-standard port while forwarding
|
|
98
|
+
// only the hostname; X-Forwarded-Port carries the client-facing port.
|
|
99
|
+
if (trusted && url.port === '') {
|
|
100
|
+
const forwardedPort = firstHeader(headers?.['x-forwarded-port'])
|
|
101
|
+
const port = Number(forwardedPort?.split(',')[0].trim())
|
|
102
|
+
if (Number.isInteger(port) && port >= 1 && port <= 65535) url.port = String(port)
|
|
103
|
+
}
|
|
97
104
|
return trimTrailingSlash(url.origin)
|
|
98
105
|
} catch {
|
|
99
106
|
return null
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ai_mind/dsh-a2a",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.2.0",
|
|
4
4
|
"description": "Production A2A (Agent2Agent) v1.0 server for DeepSeek Harness agents: JSON-RPC + HTTP+JSON bindings, streaming, tasks, push notifications and signed Agent Cards.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|