@gehennawu/dsh-service 2.0.6 → 2.0.8
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 +77 -17
- package/README.md +77 -17
- package/backup-integrity.js +215 -24
- package/backup-routes.js +17 -1
- package/client.js +1 -1
- package/index.js +229 -38
- package/model-config-routes.js +633 -0
- package/model-config-schema.js +132 -0
- package/model-smart-fill.js +2206 -0
- package/model-workbench-routes.js +1740 -0
- package/model-workbench-settings.js +389 -0
- package/model-workbench.js +1618 -0
- package/package.json +11 -3
- package/provider-availability.js +84 -0
- package/quota-adapters.js +39 -3
- package/quota-routes.js +13 -7
- package/subagent-routes.js +7 -0
package/index.js
CHANGED
|
@@ -20,6 +20,46 @@ import { createSessionsRoutes } from './session-routes.js'
|
|
|
20
20
|
import { createQuotaRoutes } from './quota-routes.js'
|
|
21
21
|
import { createSubagentRoutes } from './subagent-routes.js'
|
|
22
22
|
import { createBackupRoutes } from './backup-routes.js'
|
|
23
|
+
import { createModelConfigRoutes } from './model-config-routes.js'
|
|
24
|
+
import { createProviderAvailability, installProviderAvailability } from './provider-availability.js'
|
|
25
|
+
import { createWorkbenchStore, loadPiAiCatalog } from './model-workbench.js'
|
|
26
|
+
import {
|
|
27
|
+
WORKBENCH_SIDECAR_VERSION,
|
|
28
|
+
applySuggestions,
|
|
29
|
+
applyUndo,
|
|
30
|
+
arbitrateModelSuggestions,
|
|
31
|
+
buildPiAiIndex,
|
|
32
|
+
abilityPatch,
|
|
33
|
+
consensusFields,
|
|
34
|
+
createFreshnessManager,
|
|
35
|
+
deriveKeyRefFor,
|
|
36
|
+
diffModelLists,
|
|
37
|
+
emptyWorkbenchSidecar,
|
|
38
|
+
fetchModelsProbe,
|
|
39
|
+
hostnameOf,
|
|
40
|
+
indexModelParamsDev,
|
|
41
|
+
isPrivateIpAddress,
|
|
42
|
+
loadModelParamsSnapshot,
|
|
43
|
+
loadWorkbenchSidecar,
|
|
44
|
+
modelParamsCandidates,
|
|
45
|
+
modelParamsDevEfforts,
|
|
46
|
+
modelPatchFromPiAi,
|
|
47
|
+
openRouterModelFacts,
|
|
48
|
+
openRouterVerifySource,
|
|
49
|
+
pickOpenRouterMatch,
|
|
50
|
+
modelsProbeLookup,
|
|
51
|
+
normalizeBaseURLForMatch,
|
|
52
|
+
parseModelListing,
|
|
53
|
+
probeBaseURLCheck,
|
|
54
|
+
profileFingerprint,
|
|
55
|
+
readLlmProfiles,
|
|
56
|
+
saveModelParamsSnapshot,
|
|
57
|
+
saveWorkbenchSidecar,
|
|
58
|
+
selfCheckModelsPatch,
|
|
59
|
+
suggestRouteId,
|
|
60
|
+
suggestionId,
|
|
61
|
+
undoPreviewModels,
|
|
62
|
+
} from './model-workbench.js'
|
|
23
63
|
import { collectPluginHealth, pluginCheckItem, restartPluginEntry } from './plugin-health.js'
|
|
24
64
|
import {
|
|
25
65
|
buildCliproxyAccountPlan as buildCliproxyAccountPlanAdapter,
|
|
@@ -88,6 +128,8 @@ const DEFAULT_FEATURE_SETTINGS = Object.freeze({
|
|
|
88
128
|
// v1.8 新增:composer 模型按钮显示厂家/渠道图标(宽态加在模型名前,
|
|
89
129
|
// 窄态替换官方默认图标;未适配渠道保留官方默认图标)。
|
|
90
130
|
modelProviderIcons: true,
|
|
131
|
+
// v1.10 新增:模型配置工作台(供应商与模型 / 子代理路由),写路径经两段式确认。
|
|
132
|
+
modelCatalog: true,
|
|
91
133
|
})
|
|
92
134
|
/** 标记为「可热更新」的 Config 字段(0.1.7-alpha.1 的表单与原地热更新以此为准)。 */
|
|
93
135
|
const volatileField = (schema) => (typeof schema?.extra === 'function' ? schema.extra('volatile', true) : schema)
|
|
@@ -238,6 +280,11 @@ const QUOTA_MIN_INTERVAL_MS = 15000
|
|
|
238
280
|
const QUOTA_MANUAL_COOLDOWN_MS = 5000
|
|
239
281
|
const QUOTA_BACKOFF_BASE_MS = 30000
|
|
240
282
|
const QUOTA_BACKOFF_MAX_MS = 15 * 60 * 1000
|
|
283
|
+
// 确定性失败矩阵:自动重试不会改变结果,只会白打上游(CPA 还会累计 fail2ban 失败次数、把 IP 打进 30 分钟封禁)。
|
|
284
|
+
// 命中即把该 provider 转入「手动模式」——此后只认手动重试这一条路,直到一次成功落定或用户改配置/凭据。
|
|
285
|
+
// 表外(network / network-transient / timeout / 5xx / upstream-error / cancelled)是可能自愈的瞬时失败,
|
|
286
|
+
// 保留指数退避自动重试。与 QUOTA_UNUSABLE_ERROR_RE 无关:这里只决定「还要不要自动打上游」。
|
|
287
|
+
const QUOTA_MANUAL_ONLY_ERROR_RE = /^(credential-missing|credential-rejected|credentials-unavailable|no-base-url|no-subscription|host-not-pinned|mgmt-disabled|mgmt-banned|transport-unavailable|bad-payload|http-status:4\d\d|upstream-status:4\d\d)/i
|
|
241
288
|
const QUOTA_CONFIG_STAT_TTL_MS = 5000
|
|
242
289
|
const QUOTA_MAX_CONCURRENCY = 4
|
|
243
290
|
const MAX_QUOTA_RESPONSE_BYTES = 1024 * 1024
|
|
@@ -601,9 +648,42 @@ function backupMetadataPayload(version, createdAt) {
|
|
|
601
648
|
dshVersion: typeof version === 'string' ? version : '',
|
|
602
649
|
pluginVersion: typeof pluginVersion === 'string' ? pluginVersion : '',
|
|
603
650
|
createdAt: createdAt instanceof Date ? createdAt.toISOString() : new Date().toISOString(),
|
|
651
|
+
// 来源平台/架构(v0.47):跨设备恢复时用来提示「这份快照出自哪个系统」。
|
|
652
|
+
// 与其它字段一样全部来自宿主常量,浏览器不参与;旧版插件读新归档时忽略这两个键。
|
|
653
|
+
platform: process.platform,
|
|
654
|
+
arch: process.arch,
|
|
604
655
|
}, null, 2)}\n`, 'utf8')
|
|
605
656
|
}
|
|
606
657
|
|
|
658
|
+
// 目标平台的事实来源:执行恢复的那台机器的进程平台。声明式覆盖只服务测试与验证
|
|
659
|
+
// (与 DSH_SERVICE_RUNTIME_ENV 同规),它只能影响只读提示与错误措辞,**不放宽**任何
|
|
660
|
+
// 归档校验——否则「声明成 Linux」就成了把 Windows 非法文件名塞进写入路径的后门。
|
|
661
|
+
function readBackupPlatform() {
|
|
662
|
+
const declared = process.env.DSH_SERVICE_BACKUP_PLATFORM
|
|
663
|
+
return typeof declared === 'string' && /^[a-z][a-z0-9_-]{0,15}$/.test(declared) ? declared : process.platform
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
// 目标机可用于承载恢复内容的 profile 名清单(宿主侧 readdir 事实,浏览器只在该清单内
|
|
667
|
+
// 选名字)。名字口径与备份侧白名单同规:单一路径段、长度封顶。
|
|
668
|
+
async function listLocalProfiles(dshHome) {
|
|
669
|
+
const root = join(dshHome, 'profiles')
|
|
670
|
+
let entries
|
|
671
|
+
try { entries = await readdir(root, { withFileTypes: true }) } catch (error) {
|
|
672
|
+
if (error?.code === 'ENOENT') return []
|
|
673
|
+
throw error
|
|
674
|
+
}
|
|
675
|
+
const profiles = []
|
|
676
|
+
for (const entry of entries) {
|
|
677
|
+
if (!entry.isDirectory() || !/^[A-Za-z0-9._-]{1,64}$/.test(entry.name) || entry.name === '.' || entry.name === '..') continue
|
|
678
|
+
profiles.push({
|
|
679
|
+
name: entry.name,
|
|
680
|
+
hasManifest: await pathExists(join(root, entry.name, 'package.json')),
|
|
681
|
+
hasPatch: await pathExists(join(root, entry.name, 'cordis.patch.yml')),
|
|
682
|
+
})
|
|
683
|
+
}
|
|
684
|
+
return profiles.sort((a, b) => a.name.localeCompare(b.name))
|
|
685
|
+
}
|
|
686
|
+
|
|
607
687
|
// 从归档名解析版本段(列表展示用,零 I/O)。只有 Host 自己签发的名字会走到这里,
|
|
608
688
|
// 但正则本身也保证不会把任意文本当版本号回给浏览器。
|
|
609
689
|
function backupVersionFromName(name) {
|
|
@@ -2241,8 +2321,9 @@ async function requestQuotaJson(endpoint, options = {}) {
|
|
|
2241
2321
|
}
|
|
2242
2322
|
|
|
2243
2323
|
// 每 provider 节流状态机(内存态,重启清零)。一切来源共用同一判定,优先序:
|
|
2244
|
-
// 单飞去重 > 失败指数退避(30s×2 封顶 15min)> 成功 TTL 60s >
|
|
2245
|
-
//
|
|
2324
|
+
// 单飞去重 > 确定性失败后的手动模式 > 失败指数退避(30s×2 封顶 15min)> 成功 TTL 60s >
|
|
2325
|
+
// 最小上游间隔 15s(与 attempt() 判定序一致);手动模式只由「一次成功落定」或用户写操作
|
|
2326
|
+
// (resetGates)解除,中途不会因时间流逝自动恢复。now 由调用方注入,测试可推进假时钟。
|
|
2246
2327
|
function createQuotaThrottle(options = {}) {
|
|
2247
2328
|
const successTtlMs = options.successTtlMs ?? QUOTA_SUCCESS_TTL_MS
|
|
2248
2329
|
const minIntervalMs = options.minIntervalMs ?? QUOTA_MIN_INTERVAL_MS
|
|
@@ -2253,7 +2334,7 @@ function createQuotaThrottle(options = {}) {
|
|
|
2253
2334
|
const entryOf = (provider) => {
|
|
2254
2335
|
let entry = entries.get(provider)
|
|
2255
2336
|
if (entry === undefined) {
|
|
2256
|
-
entry = { lastSuccessAt: 0, lastUpstreamAt: 0, lastManualAt: 0, backoffUntil: 0, failures: 0, inflight: false, windows: undefined, fetchedAt: 0, lastError: undefined, lastErrorDetail: undefined, lastErrorEndpoint: undefined, lastErrorAccount: undefined }
|
|
2337
|
+
entry = { lastSuccessAt: 0, lastUpstreamAt: 0, lastManualAt: 0, backoffUntil: 0, failures: 0, inflight: false, manualOnly: false, windows: undefined, fetchedAt: 0, lastError: undefined, lastErrorDetail: undefined, lastErrorEndpoint: undefined, lastErrorAccount: undefined }
|
|
2257
2338
|
entries.set(provider, entry)
|
|
2258
2339
|
}
|
|
2259
2340
|
return entry
|
|
@@ -2273,7 +2354,7 @@ function createQuotaThrottle(options = {}) {
|
|
|
2273
2354
|
lastErrorAccount: entry.lastErrorAccount,
|
|
2274
2355
|
}
|
|
2275
2356
|
},
|
|
2276
|
-
/**
|
|
2357
|
+
/** 只读快照:缓存窗口、是否刷新中、是否已转手动、下次允许自动发起上游的时间(null=进行中/手动模式)。 */
|
|
2277
2358
|
view(provider, now = Date.now()) {
|
|
2278
2359
|
const entry = entryOf(provider)
|
|
2279
2360
|
return {
|
|
@@ -2284,21 +2365,28 @@ function createQuotaThrottle(options = {}) {
|
|
|
2284
2365
|
lastErrorDetail: entry.lastErrorDetail,
|
|
2285
2366
|
lastErrorEndpoint: entry.lastErrorEndpoint,
|
|
2286
2367
|
lastErrorAccount: entry.lastErrorAccount,
|
|
2287
|
-
|
|
2368
|
+
// 手动模式:自动路径一律拒发,故没有「下次自动重试时刻」可报(客户端据此换文案)。
|
|
2369
|
+
manualOnly: entry.manualOnly === true,
|
|
2370
|
+
nextAllowedAt: entry.inflight || entry.manualOnly
|
|
2288
2371
|
? null
|
|
2289
2372
|
: Math.max(entry.backoffUntil, entry.lastUpstreamAt + minIntervalMs, entry.lastSuccessAt + successTtlMs),
|
|
2290
2373
|
}
|
|
2291
2374
|
},
|
|
2292
|
-
/** 申请一次上游调用。允许则置单飞并返回 ok;拒绝时给出稳定原因与 nextAllowedAt
|
|
2293
|
-
|
|
2375
|
+
/** 申请一次上游调用。允许则置单飞并返回 ok;拒绝时给出稳定原因与 nextAllowedAt
|
|
2376
|
+
* (优先序:单飞 > 手动模式 > 退避 > 成功 TTL > 最小间隔)。
|
|
2377
|
+
* options.manual=true 是「用户手动连接」:跳过手动模式与三道时间闸,只受 force() 的冷却约束。 */
|
|
2378
|
+
attempt(provider, now = Date.now(), options = {}) {
|
|
2294
2379
|
const entry = entryOf(provider)
|
|
2295
2380
|
if (entry.inflight) return { ok: false, reason: 'inflight', nextAllowedAt: null }
|
|
2296
|
-
if (
|
|
2297
|
-
|
|
2298
|
-
return { ok: false, reason: '
|
|
2299
|
-
|
|
2300
|
-
|
|
2301
|
-
|
|
2381
|
+
if (options.manual !== true) {
|
|
2382
|
+
if (entry.manualOnly) return { ok: false, reason: 'manual-only', nextAllowedAt: null }
|
|
2383
|
+
if (now < entry.backoffUntil) return { ok: false, reason: 'backoff', nextAllowedAt: entry.backoffUntil }
|
|
2384
|
+
if (now - entry.lastSuccessAt < successTtlMs) {
|
|
2385
|
+
return { ok: false, reason: 'fresh', nextAllowedAt: entry.lastSuccessAt + successTtlMs }
|
|
2386
|
+
}
|
|
2387
|
+
if (entry.lastUpstreamAt > 0 && now - entry.lastUpstreamAt < minIntervalMs) {
|
|
2388
|
+
return { ok: false, reason: 'interval', nextAllowedAt: entry.lastUpstreamAt + minIntervalMs }
|
|
2389
|
+
}
|
|
2302
2390
|
}
|
|
2303
2391
|
entry.inflight = true
|
|
2304
2392
|
entry.lastUpstreamAt = now
|
|
@@ -2312,6 +2400,8 @@ function createQuotaThrottle(options = {}) {
|
|
|
2312
2400
|
entry.lastSuccessAt = now
|
|
2313
2401
|
entry.failures = 0
|
|
2314
2402
|
entry.backoffUntil = 0
|
|
2403
|
+
// 唯一解锁点(除 resetGates 的用户写操作外):一次成功落定即回归自动模式。
|
|
2404
|
+
entry.manualOnly = false
|
|
2315
2405
|
entry.windows = outcome.windows ?? []
|
|
2316
2406
|
entry.fetchedAt = now
|
|
2317
2407
|
entry.lastError = undefined
|
|
@@ -2324,27 +2414,29 @@ function createQuotaThrottle(options = {}) {
|
|
|
2324
2414
|
const delay = Math.min(backoffBaseMs * 2 ** (entry.failures - 1), backoffMaxMs)
|
|
2325
2415
|
entry.backoffUntil = now + delay
|
|
2326
2416
|
entry.lastError = typeof outcome.code === 'string' ? outcome.code : 'unknown'
|
|
2417
|
+
// 确定性失败(换凭据/改配置才可能恢复)→ 转手动模式:自动快照不再重打上游。
|
|
2418
|
+
// 瞬时失败保持自动退避,等下一轮快照接续。
|
|
2419
|
+
if (QUOTA_MANUAL_ONLY_ERROR_RE.test(entry.lastError)) entry.manualOnly = true
|
|
2327
2420
|
entry.lastErrorDetail = typeof outcome.detail === 'string' && outcome.detail !== '' ? outcome.detail : undefined
|
|
2328
2421
|
// 渠道特有事实(多候选链的失败端点 / CPA 的失败账号):只作附注,与错误码、上游原话分开存。
|
|
2329
2422
|
entry.lastErrorEndpoint = sanitizeQuotaErrorDetail(outcome.endpoint)
|
|
2330
2423
|
entry.lastErrorAccount = sanitizeQuotaErrorDetail(outcome.account)
|
|
2331
2424
|
},
|
|
2332
|
-
/**
|
|
2425
|
+
/** 手动重试的准入判定:只受单飞与不可绕过的硬冷却约束——手动模式与失败退避都由调用方的
|
|
2426
|
+
* attempt({ manual: true }) 跳过(用户明确要求「仅可手动连接报错的渠道」,手动不该比自动更受限)。 */
|
|
2333
2427
|
force(provider, now = Date.now()) {
|
|
2334
2428
|
const entry = entryOf(provider)
|
|
2335
2429
|
if (entry.inflight) return { ok: false, reason: 'inflight', nextAllowedAt: null }
|
|
2336
|
-
if (now < entry.backoffUntil) return { ok: false, reason: 'backoff', nextAllowedAt: entry.backoffUntil }
|
|
2337
2430
|
if (entry.lastManualAt > 0 && now - entry.lastManualAt < manualCooldownMs) {
|
|
2338
2431
|
return { ok: false, reason: 'cooldown', nextAllowedAt: entry.lastManualAt + manualCooldownMs }
|
|
2339
2432
|
}
|
|
2340
2433
|
entry.lastManualAt = now
|
|
2341
|
-
entry.lastSuccessAt = 0
|
|
2342
|
-
entry.lastUpstreamAt = 0
|
|
2343
2434
|
return { ok: true }
|
|
2344
2435
|
},
|
|
2345
2436
|
// 凭据/适配变更后的闸门重置(保存 key/Cookie、改适配类型时调用):旧失败是旧配置造成的,
|
|
2346
2437
|
// 填完就该立刻重试,不傻等最长 15min 的指数退避(GUI 反馈点名);单飞在途不抢占。
|
|
2347
|
-
//
|
|
2438
|
+
// 这也是手动模式唯一的「非成功解锁」路径——保存凭据/改适配本身就是用户的手动动作,
|
|
2439
|
+
// 且凭据类错误的「手动连接」正是保存后立刻重试。与 force() 的区别:连硬冷却一并清掉。
|
|
2348
2440
|
resetGates(provider) {
|
|
2349
2441
|
const entry = entryOf(provider)
|
|
2350
2442
|
if (entry.inflight) return { ok: false, reason: 'inflight', nextAllowedAt: null }
|
|
@@ -2353,6 +2445,7 @@ function createQuotaThrottle(options = {}) {
|
|
|
2353
2445
|
entry.lastManualAt = 0
|
|
2354
2446
|
entry.lastUpstreamAt = 0
|
|
2355
2447
|
entry.lastSuccessAt = 0
|
|
2448
|
+
entry.manualOnly = false
|
|
2356
2449
|
return { ok: true }
|
|
2357
2450
|
},
|
|
2358
2451
|
prune(activeProviders) {
|
|
@@ -4185,7 +4278,7 @@ function subagentRouteCandidates(request, config, options = {}) {
|
|
|
4185
4278
|
|
|
4186
4279
|
function resolveSubagentInjection(request, config, options = {}) {
|
|
4187
4280
|
for (const candidate of subagentRouteCandidates(request, config, options)) {
|
|
4188
|
-
if (options.isRoutable !== undefined && !options.isRoutable(candidate.provider)) {
|
|
4281
|
+
if (options.isRoutable !== undefined && !options.isRoutable(candidate.provider, candidate.model)) {
|
|
4189
4282
|
options.note?.(`${candidate.provider}/${candidate.model} skipped (not routable)`)
|
|
4190
4283
|
continue
|
|
4191
4284
|
}
|
|
@@ -5270,6 +5363,11 @@ function apply(ctx, featureConfig) {
|
|
|
5270
5363
|
runtimeEnv,
|
|
5271
5364
|
previousInstanceId: instanceId,
|
|
5272
5365
|
scheduleRestart: () => scheduleRestart(ctx, runtimeEnv),
|
|
5366
|
+
// 目标平台与目标 profile 清单都由宿主侧事实提供:恢复发生在哪台机器上,
|
|
5367
|
+
// 就以哪台机器的进程平台与 profiles 目录为准,浏览器不参与判定。
|
|
5368
|
+
platform: readBackupPlatform(),
|
|
5369
|
+
// 工厂只要名字清单(映射校验用);端点另取带 hasManifest/hasPatch 的完整形态。
|
|
5370
|
+
listProfiles: async () => (await listLocalProfiles(dshHome)).map((profile) => profile.name),
|
|
5273
5371
|
})
|
|
5274
5372
|
// 会话管理(v0.35):删除两段式计划(planId → {id, path, bytes}),TTL 过期自动驱逐。
|
|
5275
5373
|
const sessionDeletePlans = new Map()
|
|
@@ -5292,6 +5390,18 @@ function apply(ctx, featureConfig) {
|
|
|
5292
5390
|
// 版本一变即自然失效,不必手工清理。
|
|
5293
5391
|
const releaseNotesCache = new Map()
|
|
5294
5392
|
const quotaThrottle = createQuotaThrottle()
|
|
5393
|
+
// 模型配置工作台(v1.10):侧车仓库持有写前快照/绑定/验证摘要/人工恢复态。
|
|
5394
|
+
// 写串行化在仓库内部;目录随 DSH 升级才变,进程内缓存一次。
|
|
5395
|
+
const modelProfileDir = ctx.get('profileContext')?.dir ?? process.env.DSH_PROFILE_DIR
|
|
5396
|
+
const workbenchStore = createWorkbenchStore(typeof modelProfileDir === 'string' && modelProfileDir !== '' ? modelProfileDir : join(dshHome, 'dsh-service-model-config', 'legacy'))
|
|
5397
|
+
const providerAvailability = createProviderAvailability(workbenchStore)
|
|
5398
|
+
const notifyProviderAvailability = () => ctx.emit('llm/adapters-updated')
|
|
5399
|
+
ctx.inject(['llm'], (scope) => {
|
|
5400
|
+
const llm = scope.get('llm') ?? ctx.get('llm')
|
|
5401
|
+
if (!llm) return
|
|
5402
|
+
const dispose = installProviderAvailability(llm, providerAvailability, notifyProviderAvailability)
|
|
5403
|
+
scope.effect(() => dispose)
|
|
5404
|
+
})
|
|
5295
5405
|
// 技能管理(v0.22):侧车索引缓存 + 批量补全状态。批量随 Fiber 销毁中止。
|
|
5296
5406
|
// 可重赋值的 let 一律以 { current } 箱体跨模块传递:端点工厂解构是值快照,
|
|
5297
5407
|
// let 重赋值不会穿过解构边界;箱体靠对象身份保持活绑定。
|
|
@@ -5369,13 +5479,15 @@ function apply(ctx, featureConfig) {
|
|
|
5369
5479
|
const subagentSeamRef = { current: false }
|
|
5370
5480
|
// 子代理派发记录环(v1.2):apply 级持有,RPC 端点与 seam 共同读写(seam 写入、端点只读)。
|
|
5371
5481
|
const dispatchRing = { order: [], byChild: new Map() }
|
|
5372
|
-
//
|
|
5373
|
-
//
|
|
5374
|
-
// 原方法——包装挂在服务实例上,disposer 期间新派生恢复原生行为。
|
|
5482
|
+
// 按宿主能力包装子代理启动入口:新版 startActivation,旧版 start/startContinuable。
|
|
5483
|
+
// 未显式指定模型的派生按配置注入 agentOptions,其余原样透传;Fiber 销毁时还原原方法。
|
|
5375
5484
|
ctx.inject(['subagents'], (scope) => {
|
|
5376
5485
|
const subagents = scope.subagents
|
|
5377
|
-
|
|
5378
|
-
const
|
|
5486
|
+
const hasActivation = typeof subagents?.startActivation === 'function'
|
|
5487
|
+
const hasLegacyStarts = typeof subagents?.start === 'function' && typeof subagents?.startContinuable === 'function'
|
|
5488
|
+
if (!hasActivation && !hasLegacyStarts) return
|
|
5489
|
+
const isRoutable = (provider, model) => {
|
|
5490
|
+
if (model !== undefined && !providerAvailability.modelEnabled(provider, model)) return false
|
|
5379
5491
|
const llm = ctx.get('llm')
|
|
5380
5492
|
if (llm === undefined || typeof llm.listProviders !== 'function') return false
|
|
5381
5493
|
try {
|
|
@@ -5426,6 +5538,8 @@ function apply(ctx, featureConfig) {
|
|
|
5426
5538
|
return quotaProviderUnusable(view) === false
|
|
5427
5539
|
}
|
|
5428
5540
|
const applyInjection = (request) => {
|
|
5541
|
+
const explicit = request?.agentOptions?.provider
|
|
5542
|
+
if (typeof explicit === 'string' && explicit) providerAvailability.assertEnabled(explicit, request?.agentOptions?.model)
|
|
5429
5543
|
// 功能关闭:零记录(完全静默,与原生行为一致)——记录是 subagentRoute 功能的一部分。
|
|
5430
5544
|
if (!featureEnabled('subagentRoute')) return { request, dispatch: undefined }
|
|
5431
5545
|
const routeOptions = {
|
|
@@ -5436,7 +5550,7 @@ function apply(ctx, featureConfig) {
|
|
|
5436
5550
|
}
|
|
5437
5551
|
const injected = resolveSubagentInjection(request, subagentRouteRef.current, routeOptions)
|
|
5438
5552
|
const routeCandidates = injected === undefined ? [] : subagentRouteCandidates(request, subagentRouteRef.current, routeOptions)
|
|
5439
|
-
.filter((candidate) => isRoutable(candidate.provider) && isQuotaHealthy(candidate.provider) !== false)
|
|
5553
|
+
.filter((candidate) => isRoutable(candidate.provider, candidate.model) && isQuotaHealthy(candidate.provider) !== false)
|
|
5440
5554
|
.filter((candidate, index, candidates) => candidates.findIndex((item) => item.provider === candidate.provider && item.model === candidate.model) === index)
|
|
5441
5555
|
const injectedIndex = routeCandidates.findIndex((candidate) => candidate.provider === injected?.provider && candidate.model === injected?.model)
|
|
5442
5556
|
const eligibleRoutes = injectedIndex < 0 ? [] : routeCandidates.slice(injectedIndex)
|
|
@@ -5464,15 +5578,27 @@ function apply(ctx, featureConfig) {
|
|
|
5464
5578
|
if (dispatch === undefined) return work()
|
|
5465
5579
|
return pendingDispatchStorage.run(dispatch, work)
|
|
5466
5580
|
}
|
|
5581
|
+
const originalStartActivation = subagents.startActivation
|
|
5467
5582
|
const originalStart = subagents.start
|
|
5468
5583
|
const originalStartContinuable = subagents.startContinuable
|
|
5469
|
-
|
|
5470
|
-
|
|
5471
|
-
|
|
5472
|
-
|
|
5473
|
-
|
|
5474
|
-
|
|
5475
|
-
|
|
5584
|
+
if (hasActivation) {
|
|
5585
|
+
subagents.startActivation = async (spec) => {
|
|
5586
|
+
await providerAvailability.loading
|
|
5587
|
+
const { request: decorated, dispatch } = applyInjection(spec.request)
|
|
5588
|
+
if (dispatch === undefined) return originalStartActivation.call(subagents, spec)
|
|
5589
|
+
return runWithDispatch(dispatch, () => originalStartActivation.call(subagents, { ...spec, request: decorated }))
|
|
5590
|
+
}
|
|
5591
|
+
} else {
|
|
5592
|
+
subagents.start = async (name, request) => {
|
|
5593
|
+
await providerAvailability.loading
|
|
5594
|
+
const { request: decorated, dispatch } = applyInjection(request)
|
|
5595
|
+
return runWithDispatch(dispatch, () => originalStart.call(subagents, name, decorated))
|
|
5596
|
+
}
|
|
5597
|
+
subagents.startContinuable = async (spec) => {
|
|
5598
|
+
await providerAvailability.loading
|
|
5599
|
+
const { request: decorated, dispatch } = applyInjection(spec.request)
|
|
5600
|
+
return runWithDispatch(dispatch, () => originalStartContinuable.call(subagents, { ...spec, request: decorated }))
|
|
5601
|
+
}
|
|
5476
5602
|
}
|
|
5477
5603
|
// agent/created(0.1.6 起由 announce await ctx.serial 异步串行派发;0.1.5 及之前同步派发在创建栈内):
|
|
5478
5604
|
// 记录派发路由 + 绑定等级(仅当确有非空等级)。监听器内部全 try/catch 严格保证绝不抛错,
|
|
@@ -5552,8 +5678,11 @@ function apply(ctx, featureConfig) {
|
|
|
5552
5678
|
}, { prepend: true }) : null
|
|
5553
5679
|
subagentSeamRef.current = true
|
|
5554
5680
|
scope.effect(() => () => {
|
|
5555
|
-
subagents.
|
|
5556
|
-
|
|
5681
|
+
if (hasActivation) subagents.startActivation = originalStartActivation
|
|
5682
|
+
else {
|
|
5683
|
+
subagents.start = originalStart
|
|
5684
|
+
subagents.startContinuable = originalStartContinuable
|
|
5685
|
+
}
|
|
5557
5686
|
if (typeof disposeCreated === 'function') disposeCreated()
|
|
5558
5687
|
if (typeof disposeRequest === 'function') disposeRequest()
|
|
5559
5688
|
if (typeof disposeRequestError === 'function') disposeRequestError()
|
|
@@ -5665,8 +5794,10 @@ function apply(ctx, featureConfig) {
|
|
|
5665
5794
|
}
|
|
5666
5795
|
// 远端额度:后台补拉一次。index.js 只提供节流/生命周期/受控传输 context;所有方言、
|
|
5667
5796
|
// 端点、凭据策略与编排都通过同一 Adapter interface 执行,不再区分 parser/fetcher 形状。
|
|
5668
|
-
|
|
5669
|
-
|
|
5797
|
+
// options.manual=true 只由 quota-refresh(手动重试/连接)传入:跳过手动模式与三道时间闸,
|
|
5798
|
+
// 是「确定性失败转手动」之后唯一还能打上游的路径。
|
|
5799
|
+
const kickQuotaRefresh = (profile, adapter, config, options = {}) => {
|
|
5800
|
+
const decision = quotaThrottle.attempt(profile.name, Date.now(), { manual: options.manual === true })
|
|
5670
5801
|
if (!decision.ok) return
|
|
5671
5802
|
const queued = enqueueQuotaWork(async () => {
|
|
5672
5803
|
const controller = new AbortController()
|
|
@@ -6130,8 +6261,29 @@ function apply(ctx, featureConfig) {
|
|
|
6130
6261
|
createSkillsRoutes({ ctx, describeJobs, dshHome, makeDescribeJobLogger, registerSkillCall, serializeSkillsIndexWrite, skillFileCache, skillsActiveControllers, skillsBatchRef, skillsIndexRef, SKILL_DESCRIPTION_MAX_CHARS, SKILL_USAGE_MAX_CHARS, bodyHashOf, describeSkillDraft, evaluateSkillFile, fixLegacySkillInvocationKeys, listSkillModels, locateSkillFrontmatter, mutateSkillEntryById, name, normalizeSkillDescribeLang, publicSkillEntry, rpcTechnicalFailure, sanitizeSkillDraftText, scanSkillEntries, selectSkillBatchCandidates, setSkillInvocationKey }),
|
|
6131
6262
|
createSessionsRoutes({ ctx, dshHome, sessionBytesCache, sessionDeletePlans, sessionTitleCache, sessionTitlesReady, sessionViewCache, SESSIONS_BYTES_MAX_IDS, SESSIONS_DELETE_PLAN_TTL_MS, SESSIONS_VIEW_PAGE_SIZE, listSessionsForManage, loadDeletedSessions, name, resolveSessionBytesForIds, resolveSessionForDelete, rpcFailure, rpcTechnicalFailure, saveDeletedSessions, searchSessionsContent, sessionExists, sessionIsLive, viewSessionPage }),
|
|
6132
6263
|
createQuotaRoutes({ ctx, kickQuotaRefresh, quotaThrottle, refreshQuotaConfigCache, serializeQuotaConfigWrite, MAX_QUOTA_PROVIDER_NAME, MAX_QUOTA_RESET_CARDS, MAX_QUOTA_RESET_CARDS_PER_PROVIDER, MAX_QUOTA_RESET_CARD_ACCOUNT, canonicalResetCardExpiresAt, QUOTA_ADAPTER_BY_KIND, name, quotaCredentialConfigured, quotaCredentialEndpoint, quotaCredentialHintNames, readQuotaProfiles, resolveQuotaKind, rpcTechnicalFailure }),
|
|
6133
|
-
createSubagentRoutes({ ctx, dispatchRing, serializeSubagentRouteWrite, subagentRouteRef, subagentRouteLoadPromise, subagentSeamRef, MAX_SUBAGENT_ROUTE_FIELD, SUBAGENT_ROUTE_FALLBACK_MAX, SUBAGENT_ROUTE_MODES, listSubagentDispatches, listSubagentModels, rpcTechnicalFailure }),
|
|
6134
|
-
createBackupRoutes({ ctx, backupIntegrity, backupProgress, clearBackupProgress, downloadTokens, dshHome, dshVersion, setBackupProgress, withBackupLock, backupNameVersionSuffix, createBackup, deleteBackup, exportBackup, formatBackupTimestamp, importBackup, listBackups, name, rpcFailure }),
|
|
6264
|
+
createSubagentRoutes({ ctx, providerAvailability, dispatchRing, serializeSubagentRouteWrite, subagentRouteRef, subagentRouteLoadPromise, subagentSeamRef, MAX_SUBAGENT_ROUTE_FIELD, SUBAGENT_ROUTE_FALLBACK_MAX, SUBAGENT_ROUTE_MODES, listSubagentDispatches, listSubagentModels, rpcTechnicalFailure }),
|
|
6265
|
+
createBackupRoutes({ ctx, backupIntegrity, backupProgress, clearBackupProgress, downloadTokens, dshHome, dshVersion, setBackupProgress, withBackupLock, backupNameVersionSuffix, createBackup, deleteBackup, exportBackup, formatBackupTimestamp, importBackup, listBackups, listLocalProfiles, name, rpcFailure }),
|
|
6266
|
+
createModelConfigRoutes({
|
|
6267
|
+
ctx,
|
|
6268
|
+
loadPiCatalog: loadPiAiCatalog,
|
|
6269
|
+
workbenchStore,
|
|
6270
|
+
providerAvailability,
|
|
6271
|
+
notifyProviderAvailability,
|
|
6272
|
+
// 移除守卫的只读引用面:子代理路由配置 + 官方默认模型选择(都是宿主侧事实,浏览器不参与)。
|
|
6273
|
+
loadSubagentRoute: async () => {
|
|
6274
|
+
await subagentRouteLoadPromise
|
|
6275
|
+
return subagentRouteRef.current
|
|
6276
|
+
},
|
|
6277
|
+
loadAgentDefaultModelSelection: () => {
|
|
6278
|
+
const service = ctx.get('agentDefaultModel')
|
|
6279
|
+
if (service === undefined || typeof service.currentSelection !== 'function') return undefined
|
|
6280
|
+
return service.currentSelection()
|
|
6281
|
+
},
|
|
6282
|
+
writeSubagentRoute: serializeSubagentRouteWrite,
|
|
6283
|
+
listSubagentModels,
|
|
6284
|
+
dshHome,
|
|
6285
|
+
pluginVersion,
|
|
6286
|
+
}),
|
|
6135
6287
|
])
|
|
6136
6288
|
|
|
6137
6289
|
const dispatchEndpoint = createRpcDispatcher({ endpoints: rpcEndpoints, featureEnabled, logger: ctx.logger })
|
|
@@ -6241,6 +6393,45 @@ export {
|
|
|
6241
6393
|
saveUnifiedConfig,
|
|
6242
6394
|
updateUnifiedConfigSection,
|
|
6243
6395
|
sanitizeSettingsNavConfig,
|
|
6396
|
+
// 模型配置工作台(v1.10):领域纯函数经 index.js 再导出,测试从 '../index.js' 导入的
|
|
6397
|
+
// 既有约定保持不变(旧分支 test/workbench.test.js 即此形状)。
|
|
6398
|
+
WORKBENCH_SIDECAR_VERSION,
|
|
6399
|
+
applySuggestions,
|
|
6400
|
+
applyUndo,
|
|
6401
|
+
arbitrateModelSuggestions,
|
|
6402
|
+
buildPiAiIndex,
|
|
6403
|
+
createWorkbenchStore,
|
|
6404
|
+
deriveKeyRefFor,
|
|
6405
|
+
diffModelLists,
|
|
6406
|
+
emptyWorkbenchSidecar,
|
|
6407
|
+
fetchModelsProbe,
|
|
6408
|
+
hostnameOf,
|
|
6409
|
+
isPrivateIpAddress,
|
|
6410
|
+
loadWorkbenchSidecar,
|
|
6411
|
+
modelPatchFromPiAi,
|
|
6412
|
+
modelsProbeLookup,
|
|
6413
|
+
normalizeBaseURLForMatch,
|
|
6414
|
+
parseModelListing,
|
|
6415
|
+
probeBaseURLCheck,
|
|
6416
|
+
profileFingerprint,
|
|
6417
|
+
readLlmProfiles,
|
|
6418
|
+
saveWorkbenchSidecar,
|
|
6419
|
+
selfCheckModelsPatch,
|
|
6420
|
+
suggestRouteId,
|
|
6421
|
+
suggestionId,
|
|
6422
|
+
undoPreviewModels,
|
|
6423
|
+
// P3.5 补充源(modelparams.dev)与多源共识:同样经 index.js 再导出供测试直达。
|
|
6424
|
+
abilityPatch,
|
|
6425
|
+
consensusFields,
|
|
6426
|
+
createFreshnessManager,
|
|
6427
|
+
indexModelParamsDev,
|
|
6428
|
+
loadModelParamsSnapshot,
|
|
6429
|
+
modelParamsCandidates,
|
|
6430
|
+
modelParamsDevEfforts,
|
|
6431
|
+
openRouterModelFacts,
|
|
6432
|
+
openRouterVerifySource,
|
|
6433
|
+
pickOpenRouterMatch,
|
|
6434
|
+
saveModelParamsSnapshot,
|
|
6244
6435
|
}
|
|
6245
6436
|
// 插件 Config:0.1.7-alpha.1 起宿主配置面由 Profile 的插件 Config 托管;声明它即可让
|
|
6246
6437
|
// 功能开关出现在插件管理页,并让宿主经 `loader/volatile-update` 热更新(无需重启)。
|