@gehennawu/dsh-service 2.0.7 → 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/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)
@@ -3993,6 +4035,7 @@ async function listSubagentModels(llm, agentDefaultModel) {
3993
4035
 
3994
4036
  /** 子代理路由配置的空档(inherit):与未安装本功能的原生行为完全一致。 */
3995
4037
  function createEmptySubagentRoute() {
4038
+ // 不含 preset 键:空预设不落盘,磁盘形状与 v2.0.8 及之前逐字节兼容(注入端按 absent = 跟随父代)。
3996
4039
  return { version: SUBAGENT_ROUTE_VERSION, mode: 'inherit' }
3997
4040
  }
3998
4041
 
@@ -4031,12 +4074,16 @@ function parseSubagentRouteText(text) {
4031
4074
  if (parsed.mode === 'custom' && (provider === '' || model === '')) return fallback
4032
4075
  // reasoningEffort 是可选的 adapter 自有等级 ID:只有字符串、trim 后非空才保留(空串视为默认)。
4033
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) : ''
4034
4080
  return {
4035
4081
  version: SUBAGENT_ROUTE_VERSION,
4036
4082
  mode: parsed.mode,
4037
4083
  ...(provider !== '' && model !== '' ? { provider, model } : {}),
4038
4084
  ...(reasoningEffort !== '' ? { reasoningEffort } : {}),
4039
4085
  ...(fallbacks.length > 0 ? { fallbacks } : {}),
4086
+ ...(preset !== '' ? { preset } : {}),
4040
4087
  }
4041
4088
  }
4042
4089
 
@@ -4236,7 +4283,7 @@ function subagentRouteCandidates(request, config, options = {}) {
4236
4283
 
4237
4284
  function resolveSubagentInjection(request, config, options = {}) {
4238
4285
  for (const candidate of subagentRouteCandidates(request, config, options)) {
4239
- if (options.isRoutable !== undefined && !options.isRoutable(candidate.provider)) {
4286
+ if (options.isRoutable !== undefined && !options.isRoutable(candidate.provider, candidate.model)) {
4240
4287
  options.note?.(`${candidate.provider}/${candidate.model} skipped (not routable)`)
4241
4288
  continue
4242
4289
  }
@@ -4301,6 +4348,8 @@ function buildSubagentDispatchRecord(agent, parent, dispatch, options = {}) {
4301
4348
  }
4302
4349
  if (typeof turn === 'number' && Number.isFinite(turn)) record.turn = turn
4303
4350
  if (reasoningEffort !== undefined) record.reasoningEffort = reasoningEffort
4351
+ // 独立 Agent 预设(v2.0.9):仅记录插件注入的值;调用方显式携带时不记(来源不是本插件)。
4352
+ if (typeof dispatch?.preset === 'string' && dispatch.preset !== '') record.preset = dispatch.preset
4304
4353
  return record
4305
4354
  }
4306
4355
 
@@ -5120,7 +5169,9 @@ async function quotaCredentialEndpoint(ctx, refreshQuotaConfig, throttle, payloa
5120
5169
  try {
5121
5170
  await credentials.set(name, value)
5122
5171
  } catch (error) {
5123
- // 典型拒绝:进程环境层正在遮蔽该名字(describe().writable=false)——seam 契约的显式报错。
5172
+ // 可写性由宿主凭据服务裁决后回传:0.2.1-alpha.3 起托管存储优先、env 层不再只读,
5173
+ // 旧版「进程环境层遮蔽 ⇒ describe().writable=false 且 set 必拒」的成因已不适用;
5174
+ // 这里不预判成因,宿主报什么错就透传什么(seam 契约的显式报错)。
5124
5175
  return { ok: false, error: 'credential-write-failed', detail: sanitizeQuotaErrorDetail(error?.message) }
5125
5176
  }
5126
5177
  // 新凭据落库即清掉该 provider 的退避/冷却闸门:旧失败是旧凭据造成的,
@@ -5348,6 +5399,18 @@ function apply(ctx, featureConfig) {
5348
5399
  // 版本一变即自然失效,不必手工清理。
5349
5400
  const releaseNotesCache = new Map()
5350
5401
  const quotaThrottle = createQuotaThrottle()
5402
+ // 模型配置工作台(v1.10):侧车仓库持有写前快照/绑定/验证摘要/人工恢复态。
5403
+ // 写串行化在仓库内部;目录随 DSH 升级才变,进程内缓存一次。
5404
+ const modelProfileDir = ctx.get('profileContext')?.dir ?? process.env.DSH_PROFILE_DIR
5405
+ const workbenchStore = createWorkbenchStore(typeof modelProfileDir === 'string' && modelProfileDir !== '' ? modelProfileDir : join(dshHome, 'dsh-service-model-config', 'legacy'))
5406
+ const providerAvailability = createProviderAvailability(workbenchStore)
5407
+ const notifyProviderAvailability = () => ctx.emit('llm/adapters-updated')
5408
+ ctx.inject(['llm'], (scope) => {
5409
+ const llm = scope.get('llm') ?? ctx.get('llm')
5410
+ if (!llm) return
5411
+ const dispose = installProviderAvailability(llm, providerAvailability, notifyProviderAvailability)
5412
+ scope.effect(() => dispose)
5413
+ })
5351
5414
  // 技能管理(v0.22):侧车索引缓存 + 批量补全状态。批量随 Fiber 销毁中止。
5352
5415
  // 可重赋值的 let 一律以 { current } 箱体跨模块传递:端点工厂解构是值快照,
5353
5416
  // let 重赋值不会穿过解构边界;箱体靠对象身份保持活绑定。
@@ -5432,7 +5495,8 @@ function apply(ctx, featureConfig) {
5432
5495
  const hasActivation = typeof subagents?.startActivation === 'function'
5433
5496
  const hasLegacyStarts = typeof subagents?.start === 'function' && typeof subagents?.startContinuable === 'function'
5434
5497
  if (!hasActivation && !hasLegacyStarts) return
5435
- const isRoutable = (provider) => {
5498
+ const isRoutable = (provider, model) => {
5499
+ if (model !== undefined && !providerAvailability.modelEnabled(provider, model)) return false
5436
5500
  const llm = ctx.get('llm')
5437
5501
  if (llm === undefined || typeof llm.listProviders !== 'function') return false
5438
5502
  try {
@@ -5483,6 +5547,8 @@ function apply(ctx, featureConfig) {
5483
5547
  return quotaProviderUnusable(view) === false
5484
5548
  }
5485
5549
  const applyInjection = (request) => {
5550
+ const explicit = request?.agentOptions?.provider
5551
+ if (typeof explicit === 'string' && explicit) providerAvailability.assertEnabled(explicit, request?.agentOptions?.model)
5486
5552
  // 功能关闭:零记录(完全静默,与原生行为一致)——记录是 subagentRoute 功能的一部分。
5487
5553
  if (!featureEnabled('subagentRoute')) return { request, dispatch: undefined }
5488
5554
  const routeOptions = {
@@ -5493,7 +5559,7 @@ function apply(ctx, featureConfig) {
5493
5559
  }
5494
5560
  const injected = resolveSubagentInjection(request, subagentRouteRef.current, routeOptions)
5495
5561
  const routeCandidates = injected === undefined ? [] : subagentRouteCandidates(request, subagentRouteRef.current, routeOptions)
5496
- .filter((candidate) => isRoutable(candidate.provider) && isQuotaHealthy(candidate.provider) !== false)
5562
+ .filter((candidate) => isRoutable(candidate.provider, candidate.model) && isQuotaHealthy(candidate.provider) !== false)
5497
5563
  .filter((candidate, index, candidates) => candidates.findIndex((item) => item.provider === candidate.provider && item.model === candidate.model) === index)
5498
5564
  const injectedIndex = routeCandidates.findIndex((candidate) => candidate.provider === injected?.provider && candidate.model === injected?.model)
5499
5565
  const eligibleRoutes = injectedIndex < 0 ? [] : routeCandidates.slice(injectedIndex)
@@ -5517,6 +5583,45 @@ function apply(ctx, featureConfig) {
5517
5583
  const effort = typeof reasoningEffort === 'string' && reasoningEffort !== '' ? reasoningEffort : undefined
5518
5584
  return { request: decorated, dispatch: { parent: request?.parent, provider: injected.provider, model: injected.model, ...(effort !== undefined ? { reasoningEffort: effort } : {}), routeCandidates: eligibleRoutes } }
5519
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
+ }
5520
5625
  const runWithDispatch = (dispatch, work) => {
5521
5626
  if (dispatch === undefined) return work()
5522
5627
  return pendingDispatchStorage.run(dispatch, work)
@@ -5525,17 +5630,23 @@ function apply(ctx, featureConfig) {
5525
5630
  const originalStart = subagents.start
5526
5631
  const originalStartContinuable = subagents.startContinuable
5527
5632
  if (hasActivation) {
5528
- subagents.startActivation = (spec) => {
5633
+ subagents.startActivation = async (spec) => {
5634
+ await providerAvailability.loading
5529
5635
  const { request: decorated, dispatch } = applyInjection(spec.request)
5530
5636
  if (dispatch === undefined) return originalStartActivation.call(subagents, spec)
5531
- return runWithDispatch(dispatch, () => originalStartActivation.call(subagents, { ...spec, request: decorated }))
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 }))
5532
5641
  }
5533
5642
  } else {
5534
- subagents.start = (name, request) => {
5643
+ subagents.start = async (name, request) => {
5644
+ await providerAvailability.loading
5535
5645
  const { request: decorated, dispatch } = applyInjection(request)
5536
5646
  return runWithDispatch(dispatch, () => originalStart.call(subagents, name, decorated))
5537
5647
  }
5538
- subagents.startContinuable = (spec) => {
5648
+ subagents.startContinuable = async (spec) => {
5649
+ await providerAvailability.loading
5539
5650
  const { request: decorated, dispatch } = applyInjection(spec.request)
5540
5651
  return runWithDispatch(dispatch, () => originalStartContinuable.call(subagents, { ...spec, request: decorated }))
5541
5652
  }
@@ -6201,8 +6312,29 @@ function apply(ctx, featureConfig) {
6201
6312
  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 }),
6202
6313
  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 }),
6203
6314
  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 }),
6204
- createSubagentRoutes({ ctx, dispatchRing, serializeSubagentRouteWrite, subagentRouteRef, subagentRouteLoadPromise, subagentSeamRef, MAX_SUBAGENT_ROUTE_FIELD, SUBAGENT_ROUTE_FALLBACK_MAX, SUBAGENT_ROUTE_MODES, listSubagentDispatches, listSubagentModels, rpcTechnicalFailure }),
6315
+ createSubagentRoutes({ ctx, providerAvailability, dispatchRing, serializeSubagentRouteWrite, subagentRouteRef, subagentRouteLoadPromise, subagentSeamRef, MAX_SUBAGENT_ROUTE_FIELD, SUBAGENT_ROUTE_FALLBACK_MAX, SUBAGENT_ROUTE_MODES, listSubagentDispatches, listSubagentModels, rpcTechnicalFailure }),
6205
6316
  createBackupRoutes({ ctx, backupIntegrity, backupProgress, clearBackupProgress, downloadTokens, dshHome, dshVersion, setBackupProgress, withBackupLock, backupNameVersionSuffix, createBackup, deleteBackup, exportBackup, formatBackupTimestamp, importBackup, listBackups, listLocalProfiles, name, rpcFailure }),
6317
+ createModelConfigRoutes({
6318
+ ctx,
6319
+ loadPiCatalog: loadPiAiCatalog,
6320
+ workbenchStore,
6321
+ providerAvailability,
6322
+ notifyProviderAvailability,
6323
+ // 移除守卫的只读引用面:子代理路由配置 + 官方默认模型选择(都是宿主侧事实,浏览器不参与)。
6324
+ loadSubagentRoute: async () => {
6325
+ await subagentRouteLoadPromise
6326
+ return subagentRouteRef.current
6327
+ },
6328
+ loadAgentDefaultModelSelection: () => {
6329
+ const service = ctx.get('agentDefaultModel')
6330
+ if (service === undefined || typeof service.currentSelection !== 'function') return undefined
6331
+ return service.currentSelection()
6332
+ },
6333
+ writeSubagentRoute: serializeSubagentRouteWrite,
6334
+ listSubagentModels,
6335
+ dshHome,
6336
+ pluginVersion,
6337
+ }),
6206
6338
  ])
6207
6339
 
6208
6340
  const dispatchEndpoint = createRpcDispatcher({ endpoints: rpcEndpoints, featureEnabled, logger: ctx.logger })
@@ -6312,6 +6444,45 @@ export {
6312
6444
  saveUnifiedConfig,
6313
6445
  updateUnifiedConfigSection,
6314
6446
  sanitizeSettingsNavConfig,
6447
+ // 模型配置工作台(v1.10):领域纯函数经 index.js 再导出,测试从 '../index.js' 导入的
6448
+ // 既有约定保持不变(旧分支 test/workbench.test.js 即此形状)。
6449
+ WORKBENCH_SIDECAR_VERSION,
6450
+ applySuggestions,
6451
+ applyUndo,
6452
+ arbitrateModelSuggestions,
6453
+ buildPiAiIndex,
6454
+ createWorkbenchStore,
6455
+ deriveKeyRefFor,
6456
+ diffModelLists,
6457
+ emptyWorkbenchSidecar,
6458
+ fetchModelsProbe,
6459
+ hostnameOf,
6460
+ isPrivateIpAddress,
6461
+ loadWorkbenchSidecar,
6462
+ modelPatchFromPiAi,
6463
+ modelsProbeLookup,
6464
+ normalizeBaseURLForMatch,
6465
+ parseModelListing,
6466
+ probeBaseURLCheck,
6467
+ profileFingerprint,
6468
+ readLlmProfiles,
6469
+ saveWorkbenchSidecar,
6470
+ selfCheckModelsPatch,
6471
+ suggestRouteId,
6472
+ suggestionId,
6473
+ undoPreviewModels,
6474
+ // P3.5 补充源(modelparams.dev)与多源共识:同样经 index.js 再导出供测试直达。
6475
+ abilityPatch,
6476
+ consensusFields,
6477
+ createFreshnessManager,
6478
+ indexModelParamsDev,
6479
+ loadModelParamsSnapshot,
6480
+ modelParamsCandidates,
6481
+ modelParamsDevEfforts,
6482
+ openRouterModelFacts,
6483
+ openRouterVerifySource,
6484
+ pickOpenRouterMatch,
6485
+ saveModelParamsSnapshot,
6315
6486
  }
6316
6487
  // 插件 Config:0.1.7-alpha.1 起宿主配置面由 Profile 的插件 Config 托管;声明它即可让
6317
6488
  // 功能开关出现在插件管理页,并让宿主经 `loader/volatile-update` 热更新(无需重启)。