@gehennawu/dsh-service 2.0.8 → 2.0.9
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.en.md +80 -113
- package/README.md +80 -113
- package/client.js +1 -1
- package/index.js +53 -2
- package/model-config-routes.js +8 -0
- package/package.json +2 -2
- package/quota-adapters.js +55 -0
- package/screenshots/README.md +4 -3
- package/screenshots/configuration.png +0 -0
- package/screenshots/configuration_en.png +0 -0
- package/screenshots/health-diagnostics.png +0 -0
- package/screenshots/health-diagnostics_en.png +0 -0
- package/screenshots/model-configuration.png +0 -0
- package/screenshots/model-configuration_en.png +0 -0
- package/screenshots/overview.png +0 -0
- package/screenshots/overview_en.png +0 -0
- package/subagent-routes.js +59 -1
package/index.js
CHANGED
|
@@ -4035,6 +4035,7 @@ async function listSubagentModels(llm, agentDefaultModel) {
|
|
|
4035
4035
|
|
|
4036
4036
|
/** 子代理路由配置的空档(inherit):与未安装本功能的原生行为完全一致。 */
|
|
4037
4037
|
function createEmptySubagentRoute() {
|
|
4038
|
+
// 不含 preset 键:空预设不落盘,磁盘形状与 v2.0.8 及之前逐字节兼容(注入端按 absent = 跟随父代)。
|
|
4038
4039
|
return { version: SUBAGENT_ROUTE_VERSION, mode: 'inherit' }
|
|
4039
4040
|
}
|
|
4040
4041
|
|
|
@@ -4073,12 +4074,16 @@ function parseSubagentRouteText(text) {
|
|
|
4073
4074
|
if (parsed.mode === 'custom' && (provider === '' || model === '')) return fallback
|
|
4074
4075
|
// reasoningEffort 是可选的 adapter 自有等级 ID:只有字符串、trim 后非空才保留(空串视为默认)。
|
|
4075
4076
|
const reasoningEffort = provider !== '' && model !== '' && typeof parsed.reasoningEffort === 'string' ? parsed.reasoningEffort.trim().slice(0, MAX_SUBAGENT_ROUTE_FIELD) : ''
|
|
4077
|
+
// 独立 Agent 预设(v2.0.9):与模式无关的全局字段;空串 = 跟随父代。存在性校验在保存端做
|
|
4078
|
+
// (磁盘配置容忍失效 id——旧宿主/预设被删时注入端按 provider 能力门跳过,不阻塞派生)。
|
|
4079
|
+
const preset = typeof parsed.preset === 'string' ? parsed.preset.trim().slice(0, MAX_SUBAGENT_ROUTE_FIELD) : ''
|
|
4076
4080
|
return {
|
|
4077
4081
|
version: SUBAGENT_ROUTE_VERSION,
|
|
4078
4082
|
mode: parsed.mode,
|
|
4079
4083
|
...(provider !== '' && model !== '' ? { provider, model } : {}),
|
|
4080
4084
|
...(reasoningEffort !== '' ? { reasoningEffort } : {}),
|
|
4081
4085
|
...(fallbacks.length > 0 ? { fallbacks } : {}),
|
|
4086
|
+
...(preset !== '' ? { preset } : {}),
|
|
4082
4087
|
}
|
|
4083
4088
|
}
|
|
4084
4089
|
|
|
@@ -4343,6 +4348,8 @@ function buildSubagentDispatchRecord(agent, parent, dispatch, options = {}) {
|
|
|
4343
4348
|
}
|
|
4344
4349
|
if (typeof turn === 'number' && Number.isFinite(turn)) record.turn = turn
|
|
4345
4350
|
if (reasoningEffort !== undefined) record.reasoningEffort = reasoningEffort
|
|
4351
|
+
// 独立 Agent 预设(v2.0.9):仅记录插件注入的值;调用方显式携带时不记(来源不是本插件)。
|
|
4352
|
+
if (typeof dispatch?.preset === 'string' && dispatch.preset !== '') record.preset = dispatch.preset
|
|
4346
4353
|
return record
|
|
4347
4354
|
}
|
|
4348
4355
|
|
|
@@ -5162,7 +5169,9 @@ async function quotaCredentialEndpoint(ctx, refreshQuotaConfig, throttle, payloa
|
|
|
5162
5169
|
try {
|
|
5163
5170
|
await credentials.set(name, value)
|
|
5164
5171
|
} catch (error) {
|
|
5165
|
-
//
|
|
5172
|
+
// 可写性由宿主凭据服务裁决后回传:0.2.1-alpha.3 起托管存储优先、env 层不再只读,
|
|
5173
|
+
// 旧版「进程环境层遮蔽 ⇒ describe().writable=false 且 set 必拒」的成因已不适用;
|
|
5174
|
+
// 这里不预判成因,宿主报什么错就透传什么(seam 契约的显式报错)。
|
|
5166
5175
|
return { ok: false, error: 'credential-write-failed', detail: sanitizeQuotaErrorDetail(error?.message) }
|
|
5167
5176
|
}
|
|
5168
5177
|
// 新凭据落库即清掉该 provider 的退避/冷却闸门:旧失败是旧凭据造成的,
|
|
@@ -5574,6 +5583,45 @@ function apply(ctx, featureConfig) {
|
|
|
5574
5583
|
const effort = typeof reasoningEffort === 'string' && reasoningEffort !== '' ? reasoningEffort : undefined
|
|
5575
5584
|
return { request: decorated, dispatch: { parent: request?.parent, provider: injected.provider, model: injected.model, ...(effort !== undefined ? { reasoningEffort: effort } : {}), routeCandidates: eligibleRoutes } }
|
|
5576
5585
|
}
|
|
5586
|
+
// 独立 Agent 预设(v2.0.9):官方 0.2.1-alpha.3 起 startActivation 的请求面新增
|
|
5587
|
+
// request.agentPreset(统一设置子代理角色与工具;省略 = 跟随父代当前预设)。注入门完全
|
|
5588
|
+
// 对齐官方 tool-subagent 的 assertProvider:provider.capabilities.agentPreset === true 且
|
|
5589
|
+
// !provider.inheritsParentContext(fork 类继承父代上下文的 provider 不支持)。调用方显式
|
|
5590
|
+
// 携带的 agentPreset 永远赢。其余一切不明朗情形(服务缺席、清单读不到、预设已删/broken、
|
|
5591
|
+
// 能力不明)一律 fail-closed 不注入——给不支持的 provider 注入该字段会被宿主
|
|
5592
|
+
// assertCapabilities 拒绝、派生直接失败,「不生效」永远好过「失败」。每次派发现查清单
|
|
5593
|
+
// (预设集极小、变更低频),保证预设被删后自动回落跟随父代。仅 startActivation 路径装饰:
|
|
5594
|
+
// 旧版 start/startContinuable 的宿主请求类型没有该字段,其 provider 能力面也不含 agentPreset。
|
|
5595
|
+
const resolvePresetForSpec = async (spec, request) => {
|
|
5596
|
+
const configured = typeof subagentRouteRef.current.preset === 'string' ? subagentRouteRef.current.preset : ''
|
|
5597
|
+
if (configured === '') return undefined
|
|
5598
|
+
if (typeof request?.agentPreset === 'string' && request.agentPreset !== '') return undefined
|
|
5599
|
+
let declared
|
|
5600
|
+
try {
|
|
5601
|
+
const registry = ctx.get('agentPresets')
|
|
5602
|
+
if (registry === undefined || registry === null || typeof registry.list !== 'function') return undefined
|
|
5603
|
+
declared = await registry.list()
|
|
5604
|
+
} catch (_) {
|
|
5605
|
+
return undefined
|
|
5606
|
+
}
|
|
5607
|
+
const hit = (Array.isArray(declared) ? declared : []).find((entry) => entry !== null && typeof entry === 'object' && entry.id === configured)
|
|
5608
|
+
if (hit === undefined || (typeof hit.broken === 'string' && hit.broken !== '')) {
|
|
5609
|
+
ctx.logger?.info?.(`dsh-service: subagent preset "${configured}" skipped: not in the host preset list`)
|
|
5610
|
+
return undefined
|
|
5611
|
+
}
|
|
5612
|
+
let provider
|
|
5613
|
+
try {
|
|
5614
|
+
provider = typeof subagents.getProvider === 'function' ? subagents.getProvider(spec?.provider) : undefined
|
|
5615
|
+
} catch (_) {
|
|
5616
|
+
provider = undefined
|
|
5617
|
+
}
|
|
5618
|
+
if (provider === null || typeof provider !== 'object') return undefined
|
|
5619
|
+
if (provider.capabilities?.agentPreset !== true || provider.inheritsParentContext === true) {
|
|
5620
|
+
ctx.logger?.info?.(`dsh-service: subagent preset "${configured}" skipped: provider "${spec?.provider ?? ''}" does not support independent presets`)
|
|
5621
|
+
return undefined
|
|
5622
|
+
}
|
|
5623
|
+
return configured
|
|
5624
|
+
}
|
|
5577
5625
|
const runWithDispatch = (dispatch, work) => {
|
|
5578
5626
|
if (dispatch === undefined) return work()
|
|
5579
5627
|
return pendingDispatchStorage.run(dispatch, work)
|
|
@@ -5586,7 +5634,10 @@ function apply(ctx, featureConfig) {
|
|
|
5586
5634
|
await providerAvailability.loading
|
|
5587
5635
|
const { request: decorated, dispatch } = applyInjection(spec.request)
|
|
5588
5636
|
if (dispatch === undefined) return originalStartActivation.call(subagents, spec)
|
|
5589
|
-
|
|
5637
|
+
const preset = await resolvePresetForSpec(spec, decorated)
|
|
5638
|
+
const finalRequest = preset !== undefined ? { ...decorated, agentPreset: preset } : decorated
|
|
5639
|
+
if (preset !== undefined) dispatch.preset = preset
|
|
5640
|
+
return runWithDispatch(dispatch, () => originalStartActivation.call(subagents, { ...spec, request: finalRequest }))
|
|
5590
5641
|
}
|
|
5591
5642
|
} else {
|
|
5592
5643
|
subagents.start = async (name, request) => {
|
package/model-config-routes.js
CHANGED
|
@@ -279,6 +279,9 @@ export function createModelConfigRoutes({ ctx, workbenchStore, loadPiCatalog = l
|
|
|
279
279
|
name: defaultEntry?.name ?? id,
|
|
280
280
|
api: defaultEntry?.api ?? 'openai-completions',
|
|
281
281
|
baseURL: defaultEntry?.baseUrl ?? '',
|
|
282
|
+
// 混合协议目录(v2.0.9):pi-ai 1.1.0 起 azure 等渠道单目录含多种 api。路由级 `api`
|
|
283
|
+
// 会覆盖该路由全部模型(官方语义),客户端据此提示「钉协议前先确认全部模型同协议」。
|
|
284
|
+
apis: [...new Set(records.map((x) => x.api).filter(Boolean))],
|
|
282
285
|
models: records,
|
|
283
286
|
}
|
|
284
287
|
})
|
|
@@ -317,6 +320,11 @@ export function createModelConfigRoutes({ ctx, workbenchStore, loadPiCatalog = l
|
|
|
317
320
|
}
|
|
318
321
|
} catch (_) { /* Missing status must not prevent configuration editing. */ }
|
|
319
322
|
if (builtin(catalog, id)) dto.catalogModels = [...catalog.byModelId.entries()].flatMap(([modelId, records]) => records.filter((x) => x.channel === id).map((x) => ({ id: modelId, api: x.api, ...displayProfile({ models: [{ id: modelId, ...x }] }).models[0] })))
|
|
323
|
+
// 混合协议目录(v2.0.9):≥2 种 api 才下发,客户端据它在路由编辑器里提示路由级 api 的覆盖语义。
|
|
324
|
+
if (Array.isArray(dto.catalogModels)) {
|
|
325
|
+
const apis = [...new Set(dto.catalogModels.map((entry) => entry.api).filter(Boolean))]
|
|
326
|
+
if (apis.length > 1) dto.catalogApis = apis
|
|
327
|
+
}
|
|
320
328
|
dto.resolved = {
|
|
321
329
|
displayName: effectiveRaw.displayName ?? dto.effective.displayName ?? defaultEntry?.name ?? id,
|
|
322
330
|
api: dto.resolvedApi,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gehennawu/dsh-service",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.9",
|
|
4
4
|
"description": "DSH Web 运维面板:安全重启、健康监控、备份和 Linux 权限维护。",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"scripts": {
|
|
@@ -59,7 +59,7 @@
|
|
|
59
59
|
"homepage": "https://github.com/gehennawu/dsh-service#readme",
|
|
60
60
|
"engines": {
|
|
61
61
|
"node": ">=22",
|
|
62
|
-
"dsh": ">=0.1.1-rc.2 <0.2.1-alpha.
|
|
62
|
+
"dsh": ">=0.1.1-rc.2 <0.2.1-alpha.4"
|
|
63
63
|
},
|
|
64
64
|
"dependencies": {
|
|
65
65
|
"@deepseek-ai/schemastery": "^3.18.3"
|
package/quota-adapters.js
CHANGED
|
@@ -1270,6 +1270,37 @@ async function fetchCliproxyUsage({ profile, config, credential, signal, request
|
|
|
1270
1270
|
return windows
|
|
1271
1271
|
}
|
|
1272
1272
|
|
|
1273
|
+
// Non-monetary points retain decimal strings instead of implying a currency or quota denominator.
|
|
1274
|
+
function normalizePoePoints(payload) {
|
|
1275
|
+
const raw = payload?.current_point_balance
|
|
1276
|
+
const text = typeof raw === 'string' ? raw : typeof raw === 'number' && Number.isFinite(raw) ? String(raw) : ''
|
|
1277
|
+
if (text.length > 64 || !/^-?\d+(?:\.\d+)?$/.test(text)) return { windows: [] }
|
|
1278
|
+
return { windows: [{ id: 'points', kindKey: 'points', text: `${text} points`, unit: 'points' }] }
|
|
1279
|
+
}
|
|
1280
|
+
|
|
1281
|
+
function normalizeVercelCredits(payload) {
|
|
1282
|
+
// The API declares USD decimal strings. Lifetime spend is not a quota denominator.
|
|
1283
|
+
const balance = payload?.balance
|
|
1284
|
+
if (typeof balance !== 'string' || balance.length > 64 || !/^\d+(?:\.\d+)?$/.test(balance)
|
|
1285
|
+
|| !Number.isFinite(Number(balance)) || Number(balance) > Number.MAX_SAFE_INTEGER) return { windows: [] }
|
|
1286
|
+
return { windows: [{ id: 'balance', kindKey: 'balance', text: `$${balance}`, unit: 'usd' }] }
|
|
1287
|
+
}
|
|
1288
|
+
|
|
1289
|
+
function normalizeClinePassUsage(payload) {
|
|
1290
|
+
if (payload?.success !== true || !Array.isArray(payload?.data?.limits)) return { windows: [] }
|
|
1291
|
+
const windows = []
|
|
1292
|
+
for (const [type, id] of [['five_hour', 'five-hour'], ['weekly', 'weekly'], ['monthly', 'monthly']]) {
|
|
1293
|
+
const matches = payload.data.limits.filter((entry) => entry?.type === type)
|
|
1294
|
+
if (matches.length > 1) return { windows: [] }
|
|
1295
|
+
const entry = matches[0]
|
|
1296
|
+
if (typeof entry?.percentUsed !== 'number' || !Number.isFinite(entry.percentUsed) || entry.percentUsed < 0) continue
|
|
1297
|
+
const resetsAt = typeof entry.resetsAt === 'string' && /^\d{4}-\d{2}-\d{2}T.*(?:Z|[+-]\d{2}:\d{2})$/.test(entry.resetsAt)
|
|
1298
|
+
? normalizeResetTimestamp(entry.resetsAt) : undefined
|
|
1299
|
+
windows.push({ id, percent: Math.min(100, Math.round(entry.percentUsed)), ...(resetsAt !== undefined ? { resetsAt } : {}) })
|
|
1300
|
+
}
|
|
1301
|
+
return { windows }
|
|
1302
|
+
}
|
|
1303
|
+
|
|
1273
1304
|
function createQuotaAdapterCatalog() {
|
|
1274
1305
|
return Object.freeze([
|
|
1275
1306
|
createEndpointAdapter({
|
|
@@ -1330,6 +1361,30 @@ function createQuotaAdapterCatalog() {
|
|
|
1330
1361
|
keyHints: ['SILICONFLOW_API_KEY'],
|
|
1331
1362
|
hosts: ['siliconflow.cn'],
|
|
1332
1363
|
}),
|
|
1364
|
+
createEndpointAdapter({
|
|
1365
|
+
kind: 'vercel-ai-gateway',
|
|
1366
|
+
normalize: normalizeVercelCredits,
|
|
1367
|
+
endpoints: ['https://ai-gateway.vercel.sh/v1/credits'],
|
|
1368
|
+
keyHints: ['AI_GATEWAY_API_KEY'],
|
|
1369
|
+
hosts: ['ai-gateway.vercel.sh'],
|
|
1370
|
+
routeIds: ['vercel-ai-gateway'],
|
|
1371
|
+
}),
|
|
1372
|
+
createEndpointAdapter({
|
|
1373
|
+
kind: 'poe',
|
|
1374
|
+
normalize: normalizePoePoints,
|
|
1375
|
+
endpoints: ['https://api.poe.com/usage/current_balance'],
|
|
1376
|
+
keyHints: ['POE_API_KEY'],
|
|
1377
|
+
hosts: ['api.poe.com'],
|
|
1378
|
+
routeIds: ['poe'],
|
|
1379
|
+
}),
|
|
1380
|
+
createEndpointAdapter({
|
|
1381
|
+
kind: 'clinepass',
|
|
1382
|
+
normalize: normalizeClinePassUsage,
|
|
1383
|
+
endpoints: ['https://api.cline.bot/api/v1/users/me/plan/usage-limits'],
|
|
1384
|
+
keyHints: ['CLINEPASS_API_KEY'],
|
|
1385
|
+
hosts: ['api.cline.bot'],
|
|
1386
|
+
routeIds: ['clinepass'],
|
|
1387
|
+
}),
|
|
1333
1388
|
createEndpointAdapter({
|
|
1334
1389
|
kind: 'deepseek',
|
|
1335
1390
|
normalize: normalizeDeepseekBalance,
|
package/screenshots/README.md
CHANGED
|
@@ -16,9 +16,10 @@ README 使用的中英文界面截图。文件名与对应功能如下:
|
|
|
16
16
|
| `session-manager.png` | `session-manager_en.png` | 会话管理 |
|
|
17
17
|
| `mobile-adaptation.png` | `mobile-adaptation_en.png` | 移动端适配 |
|
|
18
18
|
| `plugin-config.png` | `plugin-config_en.png` | 插件配置 |
|
|
19
|
-
| `maintenance.png` | `maintenance_en.png` |
|
|
20
|
-
| `configuration.png` | `configuration_en.png` |
|
|
19
|
+
| `maintenance.png` | `maintenance_en.png` | 维护聚合页 |
|
|
20
|
+
| `configuration.png` | `configuration_en.png` | 配置聚合页 |
|
|
21
|
+
| `model-configuration.png` | `model-configuration_en.png` | 模型配置:渠道与模型 |
|
|
21
22
|
|
|
22
23
|
截图宽度建议 800–1200px,PNG 格式。README 新增截图引用时,必须同时加入本目录并通过 `test/docs-static.test.js`;本目录由 `package.json#files` 打入 npm 包。
|
|
23
24
|
|
|
24
|
-
|
|
25
|
+
后续替换保持现有文件名,并确保截图不暴露 API key、Cookie、Oasis-Token、管理密钥、会话正文或其他凭据/隐私数据。
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/screenshots/overview.png
CHANGED
|
Binary file
|
|
Binary file
|
package/subagent-routes.js
CHANGED
|
@@ -36,6 +36,24 @@ export function createSubagentRoutes({
|
|
|
36
36
|
current = catalog.current
|
|
37
37
|
}
|
|
38
38
|
const config = subagentRouteRef.current
|
|
39
|
+
// 独立 Agent 预设(v2.0.9):清单来自宿主 agentPresets 服务(0.2.1-alpha.3+;旧宿主缺席
|
|
40
|
+
// ⇒ 空清单 + presetSupported:false,客户端禁用选择器)。broken 的声明照常下发但带标记,
|
|
41
|
+
// 保存端拒绝选中它。
|
|
42
|
+
let presets = []
|
|
43
|
+
let presetSupported = false
|
|
44
|
+
const presetRegistry = ctx.get('agentPresets')
|
|
45
|
+
if (presetRegistry !== undefined && presetRegistry !== null && typeof presetRegistry.list === 'function') {
|
|
46
|
+
presetSupported = true
|
|
47
|
+
try {
|
|
48
|
+
const declared = await presetRegistry.list()
|
|
49
|
+
presets = (Array.isArray(declared) ? declared : [])
|
|
50
|
+
.filter((entry) => entry !== null && typeof entry === 'object' && typeof entry.id === 'string' && entry.id !== '')
|
|
51
|
+
.map((entry) => ({ id: entry.id, ...(typeof entry.name === 'string' && entry.name !== '' ? { name: entry.name } : {}), ...(typeof entry.broken === 'string' && entry.broken !== '' ? { broken: entry.broken } : {}) }))
|
|
52
|
+
.sort((a, b) => a.id < b.id ? -1 : a.id > b.id ? 1 : 0)
|
|
53
|
+
} catch (_) {
|
|
54
|
+
presets = []
|
|
55
|
+
}
|
|
56
|
+
}
|
|
39
57
|
// 自定义路由草稿(v1.4.12):三模式统一下发——客户端据此回填表单,切换模式不丢配置。
|
|
40
58
|
const routePresent = typeof config.provider === 'string' && config.provider !== '' && typeof config.model === 'string' && config.model !== ''
|
|
41
59
|
return {
|
|
@@ -49,6 +67,9 @@ export function createSubagentRoutes({
|
|
|
49
67
|
...(typeof config.reasoningEffort === 'string' && config.reasoningEffort !== '' ? { reasoningEffort: config.reasoningEffort } : {}),
|
|
50
68
|
} : {}),
|
|
51
69
|
...(Array.isArray(config.fallbacks) && config.fallbacks.length > 0 ? { fallbacks: config.fallbacks } : {}),
|
|
70
|
+
...(typeof config.preset === 'string' && config.preset !== '' ? { preset: config.preset } : {}),
|
|
71
|
+
presetSupported,
|
|
72
|
+
presets,
|
|
52
73
|
models,
|
|
53
74
|
...(current !== undefined ? { current } : {}),
|
|
54
75
|
},
|
|
@@ -129,6 +150,36 @@ export function createSubagentRoutes({
|
|
|
129
150
|
fallbacks.push(normalized)
|
|
130
151
|
}
|
|
131
152
|
}
|
|
153
|
+
// 独立 Agent 预设(v2.0.9):与模式无关,payload 携带 `preset` 键才生效(缺省 = 保留原值,
|
|
154
|
+
// 与 fallbacks 的草稿语义一致);空串 = 清除(跟随父代)。非空值必须在 agentPresets 清单内
|
|
155
|
+
// 且未 broken——宿主在派生端还会按 provider 能力二次把关,这里只挡「明显无效」的引用。
|
|
156
|
+
let preset
|
|
157
|
+
if (Object.prototype.hasOwnProperty.call(payload ?? {}, 'preset')) {
|
|
158
|
+
const rawPreset = payload.preset
|
|
159
|
+
if (rawPreset === undefined || rawPreset === null || rawPreset === '') {
|
|
160
|
+
preset = ''
|
|
161
|
+
} else if (typeof rawPreset !== 'string') {
|
|
162
|
+
return { ok: false, error: 'invalid-preset' }
|
|
163
|
+
} else {
|
|
164
|
+
const candidate = rawPreset.trim().slice(0, MAX_SUBAGENT_ROUTE_FIELD)
|
|
165
|
+
if (candidate === '') {
|
|
166
|
+
preset = ''
|
|
167
|
+
} else {
|
|
168
|
+
const registry = ctx.get('agentPresets')
|
|
169
|
+
if (registry === undefined || registry === null || typeof registry.list !== 'function') return { ok: false, error: 'preset-unavailable' }
|
|
170
|
+
let declared = []
|
|
171
|
+
try {
|
|
172
|
+
declared = await registry.list()
|
|
173
|
+
} catch (_) {
|
|
174
|
+
declared = []
|
|
175
|
+
}
|
|
176
|
+
const hit = (Array.isArray(declared) ? declared : []).find((entry) => entry !== null && typeof entry === 'object' && entry.id === candidate)
|
|
177
|
+
if (hit === undefined) return { ok: false, error: 'invalid-preset' }
|
|
178
|
+
if (typeof hit.broken === 'string' && hit.broken !== '') return { ok: false, error: 'invalid-preset' }
|
|
179
|
+
preset = candidate
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
}
|
|
132
183
|
return await serializeSubagentRouteWrite(async (config) => {
|
|
133
184
|
if (providerAvailability) {
|
|
134
185
|
await providerAvailability.loading
|
|
@@ -152,12 +203,19 @@ export function createSubagentRoutes({
|
|
|
152
203
|
if (fallbacks.length > 0) config.fallbacks = fallbacks
|
|
153
204
|
else delete config.fallbacks
|
|
154
205
|
}
|
|
206
|
+
// 预设:与模式无关;只有本轮 payload 携带了 `preset` 键才写/清。空值一律不落盘
|
|
207
|
+
// (磁盘形状与不认识该字段的旧版本逐字节兼容;注入端 absent = 跟随父代)。
|
|
208
|
+
if (preset !== undefined) {
|
|
209
|
+
if (preset === '') delete config.preset
|
|
210
|
+
else config.preset = preset
|
|
211
|
+
}
|
|
212
|
+
if (config.preset === '') delete config.preset
|
|
155
213
|
const routePresent = typeof config.provider === 'string' && config.provider !== '' && typeof config.model === 'string' && config.model !== ''
|
|
156
214
|
return { value: { ok: true, mode: config.mode, ...(routePresent ? {
|
|
157
215
|
provider: config.provider,
|
|
158
216
|
model: config.model,
|
|
159
217
|
...(typeof config.reasoningEffort === 'string' && config.reasoningEffort !== '' ? { reasoningEffort: config.reasoningEffort } : {}),
|
|
160
|
-
} : {}), ...(Array.isArray(config.fallbacks) && config.fallbacks.length > 0 ? { fallbacks: config.fallbacks } : {}) } }
|
|
218
|
+
} : {}), ...(Array.isArray(config.fallbacks) && config.fallbacks.length > 0 ? { fallbacks: config.fallbacks } : {}), ...(typeof config.preset === 'string' && config.preset !== '' ? { preset: config.preset } : {}) } }
|
|
161
219
|
})
|
|
162
220
|
} catch (error) {
|
|
163
221
|
return rpcTechnicalFailure(error)
|