dsh-subagent-profile 0.3.2 → 0.3.3

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.
@@ -19,6 +19,49 @@ import { TOOL_ZH, TOOL_CATEGORY } from './catalog.mjs';
19
19
 
20
20
  const DEFAULT_TTL_MS = 60000;
21
21
 
22
+ // --- 工具分组/中文名的薄覆盖层(schema 声明优先,缺失回退)------------------------
23
+ //
24
+ // 工具目录里每一条的 group(分类)与 zh(中文名)优先取工具 schema 自带声明字段,
25
+ // 其次才回退数据表/前缀/预设名。宿主 tools.schemas() 当前只投影 name/description/
26
+ // parameters(无 category/group/zh 字段);此层在宿主未来为 schema 补上
27
+ // 分组/中文声明时自动优先采用,缺失时按既有回退链兜底——不新增任何硬编码条目。
28
+
29
+ // 读取 schema 声明的分组候选(可接受的字段名集合内部固定,非外部注入)。
30
+ function schemaDeclaredGroup(schema) {
31
+ if (schema === null || typeof schema !== 'object') return undefined;
32
+ if (typeof schema.category === 'string' && schema.category.length > 0) return schema.category;
33
+ if (typeof schema.group === 'string' && schema.group.length > 0) return schema.group;
34
+ return undefined;
35
+ }
36
+
37
+ // 读取 schema 声明的中文名候选(宿主有 zh/label 声明时优先)。
38
+ function schemaDeclaredZh(schema) {
39
+ if (schema === null || typeof schema !== 'object') return undefined;
40
+ if (typeof schema.zh === 'string' && schema.zh.length > 0) return schema.zh;
41
+ if (typeof schema.label === 'string' && schema.label.length > 0) return schema.label;
42
+ return undefined;
43
+ }
44
+
45
+ // core 层无数据表条目时的缺省分组。
46
+ const DEFAULT_CORE_GROUP = '其他';
47
+
48
+ // 工具的分组解析链:schema 声明 → 数据表(core)/ 前缀(plugin)/ 预设名(custom)。
49
+ // `source` 是工具来源('global' 或 preset id),`layer` 为 layerOf(source)。
50
+ export function resolveToolCategory(schema, name, source, layer) {
51
+ const declared = schemaDeclaredGroup(schema);
52
+ if (declared !== undefined) return declared;
53
+ if (layer === 'core') return TOOL_CATEGORY[name] ?? DEFAULT_CORE_GROUP;
54
+ if (layer === 'plugin') return name.includes('_') ? name.split('_')[0] : name;
55
+ return source;
56
+ }
57
+
58
+ // 工具中文名解析链:schema 声明 → 数据表 → 空串(沿用原缺省)。
59
+ export function resolveToolZh(schema, name) {
60
+ const declared = schemaDeclaredZh(schema);
61
+ if (declared !== undefined) return declared;
62
+ return TOOL_ZH[name] ?? '';
63
+ }
64
+
22
65
  // --- 目录构建(无缓存;行为与 http-routes 原 collector 逐字一致)----------------
23
66
 
24
67
  // llm 目录:providers + 每 provider models + 每模型 efforts。listProviders 失败
@@ -96,17 +139,12 @@ async function collectToolsDirectory(getTools, getAgentPresets, logger) {
96
139
  if (OFFICIAL_PRESETS.includes(source)) return 'core';
97
140
  return 'custom';
98
141
  };
99
- const groupOf = (name, source) => {
100
- const layer = layerOf(source);
101
- if (layer === 'core') return TOOL_CATEGORY[name] ?? '其他';
102
- if (layer === 'plugin') return name.includes('_') ? name.split('_')[0] : name;
103
- return source;
104
- };
142
+ const groupOf = (schema, name, source) => resolveToolCategory(schema, name, source, layerOf(source));
105
143
  const push = (schemas, source) => {
106
144
  for (const s of (Array.isArray(schemas) ? schemas : [])) {
107
145
  if (!s || typeof s.name !== 'string' || s.name === 'run_code' || seen.has(s.name)) continue;
108
146
  seen.add(s.name);
109
- tools.push({ name: s.name, description: typeof s.description === 'string' ? s.description : '', zh: TOOL_ZH[s.name] ?? '', source, layer: layerOf(source), group: groupOf(s.name, source) });
147
+ tools.push({ name: s.name, description: typeof s.description === 'string' ? s.description : '', zh: resolveToolZh(s, s.name), source, layer: layerOf(source), group: groupOf(s, s.name, source) });
110
148
  }
111
149
  };
112
150
  const toolsService = typeof getTools === 'function' ? getTools() : undefined;
@@ -1,10 +1,10 @@
1
- // lib/core/catalog.mjs — settings-page data tables moved verbatim from index.mjs
2
- // (import-free, no @deepseek-ai dependency — node builtins only, none used
3
- // here). The /options tool-directory builder itself lives in lib/core/catalog-cache.mjs:
4
- // it reads ctx.tools/agentPresets (non-pure), so it is not part of this module.
1
+ // lib/core/catalog.mjs — 设置页数据表,从 index.mjs 逐字拆出
2
+ // import-free,无 @deepseek-ai 依赖)。/options 工具目录构建器在
3
+ // lib/core/catalog-cache.mjs:它读取 ctx.tools/agentPresets(非纯函数),
4
+ // 不属于本模块。
5
5
 
6
- // Tool-name → 中文说明 map, shown beside the raw tool name in the toolFilter
7
- // picker. Tools absent here fall back to their raw name.
6
+ // 工具名 → 中文说明 map,toolFilter 选择器里显示在原始工具名旁。
7
+ // 未收录的工具回退显示原始名。
8
8
  export const TOOL_ZH = {
9
9
  'bash': '终端命令',
10
10
  'pwsh': 'PowerShell 命令',
@@ -2,30 +2,28 @@
2
2
  // 仅引用 lib/core/pure.mjs 的 assertHardLimits;无 @deepseek-ai 依赖。
3
3
  //
4
4
  // 运行时推导的 cost guard,两部分:
5
- // always-on hard caps (assertHardLimits, in lib/core/pure.mjs) — maxTokens /
6
- // maxDepth are hard delegation caps, independent of the `llm` service, so
7
- // they must NOT stop applying when `llm` is absent(历史教训:早期版本在
8
- // llm 缺失时直接 return,连带跳过了硬上限,属缺陷——硬上限永远前置)。
9
- // llm capability validates provider / model / reasoningEffort against the
10
- // live provider directory. When the `llm` service is absent OR its provider
11
- // directory is empty (an adapter without discovery), capability cannot be
12
- // verified: per `allowFailOpen`(迁移开关)either
13
- // fail-open compat (warn + skip; v1 数据迁移中) or fail-loud reject. A
14
- // profile that requests none of provider/model/reasoningEffort has nothing
15
- // to verify and always passes (valid in a headless deployment).
5
+ // * always-on hard capsassertHardLimits,在 lib/core/pure.mjs)——maxTokens /
6
+ // maxDepth 是硬性委派上限,与 `llm` 服务无关,llm 缺失时绝不能停止生效
7
+ // (历史教训:早期版本在 llm 缺失时直接 return,连带跳过硬上限,属缺陷——
8
+ // 硬上限永远前置)。
9
+ // * llm 能力校验——对 live provider 目录校验 provider / model /
10
+ // reasoningEffort。`llm` 服务缺失或其 provider 目录为空(无发现能力的
11
+ // adapter)时能力无法核验:按 `allowFailOpen`(迁移开关)二选一——
12
+ // fail-open 兼容(warn + 跳过;v1 数据迁移中)或 fail-loud 拒绝。profile
13
+ // provider/model/reasoningEffort 都未请求,无校验项,恒通过(无头部署
14
+ // 下合法)。
16
15
  //
17
16
  // 目录读取改走共享 catalog 快照(lib/core/catalog-cache.mjs):assertCostGuard 的
18
17
  // `catalog` 参数是缓存实例,`catalog.getSnapshot(llm)` 返回缓存过的 providers /
19
18
  // modelsByProvider。缓存只提速目录解析、不复检安全门——provider/model/effort 的
20
19
  // 校验逻辑与 fail-loud 文案逐字不变;reasoningEffort 走直连 resolveCallConfig
21
20
  // (校验调用,非目录读取,不入缓存)。
22
- // Used by both the provider's authoritative check and the dispatch tool's
23
- // pre-check. `allowFailOpen`/`logger` are injected because this function is
24
- // module-scoped and cannot reach the apply closure's `allowFailOpen`/`ctx.logger`.
21
+ // 本函数同时供 provider 权威校验与 dispatch 工具预检使用。`allowFailOpen`/`logger`
22
+ // 经注入传入:函数是模块级作用域,够不到 apply 闭包里的 `allowFailOpen`/`ctx.logger`。
25
23
 
26
24
  import { assertHardLimits } from './pure.mjs';
27
25
 
28
- // 仅当 profile 请求了需核验能力面的字段时才进入 llm 校验(无头/headless 部署下
26
+ // 仅当 profile 请求了需核验能力面的字段时才进入 llm 校验(无头/headless 部署下
29
27
  // persona-only / toolFilter-only 的 profile 合法,不应被 fail-loud 拒绝)。
30
28
  function needsLlmCheck(profile) {
31
29
  return ['provider', 'model', 'reasoningEffort'].some(
@@ -42,23 +40,22 @@ function handleUnverifiable(allowFailOpen, logger) {
42
40
  throw new Error('dispatch: 模型能力不可验证:fail-loud 拒绝(可在配置中显式开启兼容模式)');
43
41
  }
44
42
 
45
- // provider 注册校验(目录非空时才能判定「不在目录」)。providers 来自 catalog 快照。
43
+ // provider 注册校验(目录非空时才能判定「不在目录」)。providers 来自 catalog 快照。
46
44
  function assertProviderRegistered(dir, profile) {
47
45
  if (typeof profile.provider !== 'string' || profile.provider.length === 0) return;
48
- if (!(dir.providers ?? []).some((provider) => provider && provider.id === profile.provider)) {
49
- throw new Error(`dispatch: provider "${profile.provider}" is not a registered provider`);
46
+ const providers = Array.isArray(dir.providers) ? dir.providers : [];
47
+ if (!providers.some((provider) => provider && provider.id === profile.provider)) {
48
+ throw new Error(`dispatch: provider "${profile.provider}" 未注册(不在当前 provider 目录中,可在设置页改用已注册的 provider)`);
50
49
  }
51
50
  }
52
51
 
53
- // model 校验。resolveModelInfo does not reject unknown models (catalog
54
- // membership is advisory), so validate against the advertised catalog
55
- // instead. An EMPTY catalog (adapter without discovery) cannot be verified
56
- // and is skipped — 目录级空集已在 ③ 走 allowFailOpen 分支,此处仅兜底
57
- // per-provider 空目录。A non-empty catalog that does not advertise the model
58
- // fails loud. An unverifiable lookup (listModels(undefined) when no provider
59
- // is known) becomes a clean fail-loud error instead of leaking "undefined".
60
- // modelsByProvider 来自快照;快照未覆盖的 provider(undefined / 不在名册)
61
- // 走直连 listModels 兜底,保持原 fail-loud 文案逐字。
52
+ // model 校验。resolveModelInfo 不拒绝未知模型(目录成员关系只是建议性的),
53
+ // 所以改为对照已发布的目录校验。空目录(无发现能力的 adapter)无法核验,
54
+ // 跳过——目录级空集已在 llm 门走 allowFailOpen 分支,此处仅兜底
55
+ // per-provider 空目录。非空目录未发布该模型则 fail-loud。不可核验的查询
56
+ //provider 未知时 listModels(undefined))变成干净的 fail-loud 错误,而不是
57
+ // 泄漏 "undefined"。modelsByProvider 来自快照;快照未覆盖的 provider
58
+ // (undefined / 不在名册)走直连 listModels 兜底,保持原 fail-loud 文案逐字。
62
59
  async function assertModelAdvertised(llm, profile, effectiveProvider, modelsByProvider) {
63
60
  if (typeof profile.model !== 'string' || profile.model.length === 0) return;
64
61
  const cached = modelsByProvider !== undefined ? modelsByProvider[String(effectiveProvider)] : undefined;
@@ -75,48 +72,78 @@ async function assertModelAdvertised(llm, profile, effectiveProvider, modelsByPr
75
72
  }
76
73
  }
77
74
  if (modelsError !== undefined) {
78
- throw new Error(`dispatch: cannot validate model "${profile.model}" without a provider: ${modelsError instanceof Error ? modelsError.message : String(modelsError)}`, { cause: modelsError });
75
+ throw new Error(`dispatch: 无法校验模型 "${profile.model}"provider 目录查询失败(${modelsError instanceof Error ? modelsError.message : String(modelsError)}),可稍后重试或在设置页关闭模型能力校验`, { cause: modelsError });
79
76
  }
80
77
  const listed = models ?? [];
81
78
  const known = listed.length > 0 && listed.some((model) => model && (model.id === profile.model || model.name === profile.model));
82
79
  if (listed.length > 0 && !known) {
83
- throw new Error(`dispatch: model "${profile.model}" is not advertised by provider "${String(effectiveProvider)}"`);
80
+ throw new Error(`dispatch: 模型 "${profile.model}" 不在 provider "${String(effectiveProvider)}" 的目录中(可换用该 provider 已发布的模型)`);
84
81
  }
85
82
  }
86
83
 
87
- // reasoningEffort 校验。
84
+ // reasoningEffort 校验。
88
85
  async function assertReasoningSupported(llm, profile, effectiveProvider, effectiveModel) {
89
86
  if (typeof profile.reasoningEffort !== 'string' || profile.reasoningEffort.length === 0) return;
90
87
  try {
91
88
  await llm.resolveCallConfig({ provider: effectiveProvider, model: effectiveModel, reasoningEffort: profile.reasoningEffort });
92
89
  } catch (error) {
93
- throw new Error(`dispatch: reasoningEffort "${profile.reasoningEffort}" is not supported by provider "${String(effectiveProvider)}" model "${String(effectiveModel)}": ${error instanceof Error ? error.message : String(error)}`, { cause: error });
90
+ throw new Error(`dispatch: reasoningEffort "${profile.reasoningEffort}" 不受 provider "${String(effectiveProvider)}" 的模型 "${String(effectiveModel)}" 支持(${error instanceof Error ? error.message : String(error)}),可改用一个该模型支持的 effort 值`, { cause: error });
94
91
  }
95
92
  }
96
93
 
97
- export async function assertCostGuard(parent, profile, allowFailOpen, logger, catalog) {
98
- // 硬上限 always-on(不依赖 llm)。
94
+ // 能力面逐项校验的统一接线:判定通过回调 verdict:'pass';失败先回调 verdict:'fail'
95
+ // (含 error.message detail)再 rethrow 原错误(文案不变)。
96
+ async function runChecked(check, field, checkedAgainst, fn) {
97
+ try {
98
+ await fn();
99
+ check({ field, checkedAgainst, verdict: 'pass' });
100
+ } catch (error) {
101
+ check({ field, checkedAgainst, verdict: 'fail', detail: error.message });
102
+ throw error;
103
+ }
104
+ }
105
+
106
+ // llm 门的逐项记录:verdict 对应实际走向,detail 注明 fail-open/fail-loud。
107
+ function checkLlmGate(check, allowFailOpen, detailFailLoud, detailFailOpen) {
108
+ check({ field: 'llm', verdict: allowFailOpen === true ? 'pass' : 'fail', detail: allowFailOpen === true ? detailFailOpen : detailFailLoud });
109
+ }
110
+
111
+ export async function assertCostGuard(parent, profile, allowFailOpen, logger, catalog, onCheck) {
112
+ // 可选逐项回调;onCheck 缺省时归一为 no-op,行为逐字不变(现有调用点不加参数,零漂移)。
113
+ const check = typeof onCheck === 'function' ? onCheck : () => {};
114
+
115
+ // 硬上限 always-on(不依赖 llm)。
99
116
  assertHardLimits(profile.maxTokens, profile.maxDepth);
100
117
 
101
- // 无 llm 能力面字段 → 无可核验,直接通过(headless 部署合法)。
102
- if (!needsLlmCheck(profile)) return;
118
+ // 无 llm 能力面字段 → 无可核验,直接通过(headless 部署合法)。
119
+ if (!needsLlmCheck(profile)) {
120
+ check({ field: 'llm', verdict: 'pass', detail: '无需核验(未请求 provider/model/reasoningEffort)' });
121
+ return;
122
+ }
103
123
 
104
124
  const llm = parent.ctx.get('llm');
105
- // llm 缺失 → 无法核验:按 allowFailOpen 决定 fail-open / fail-loud。
106
- if (llm === undefined) return handleUnverifiable(allowFailOpen, logger);
125
+ // llm 缺失 → 无法核验:按 allowFailOpen 决定 fail-open / fail-loud。
126
+ if (llm === undefined) {
127
+ checkLlmGate(check, allowFailOpen, 'llm 缺失:fail-loud 拒绝', 'llm 缺失:fail-open 跳过核验');
128
+ return handleUnverifiable(allowFailOpen, logger);
129
+ }
107
130
 
108
131
  // llm 存在:目录读取走共享 catalog 快照(缓存只提速目录解析,校验逻辑不变)。
109
132
  const snapshot = await catalog.getSnapshot(llm);
110
- const dir = snapshot.llm;
111
- if (dir.providersError !== undefined || dir.providers.length === 0) {
133
+ // M10:快照形状守卫——畸形快照(llm 缺失 / providers 非数组)不得被静默放行,
134
+ // 归一为「目录为空」走同一 fail-loud / fail-open 门。
135
+ const dir = (snapshot !== null && typeof snapshot === 'object' && snapshot.llm !== null && typeof snapshot.llm === 'object') ? snapshot.llm : {};
136
+ const providers = Array.isArray(dir.providers) ? dir.providers : [];
137
+ if (dir.providersError !== undefined || providers.length === 0) {
138
+ checkLlmGate(check, allowFailOpen, 'provider 目录为空:fail-loud 拒绝', 'provider 目录为空:fail-open 跳过核验');
112
139
  return handleUnverifiable(allowFailOpen, logger);
113
140
  }
114
141
 
115
- // provider 注册校验。
116
- assertProviderRegistered(dir, profile);
142
+ // provider 注册校验。
143
+ await runChecked(check, 'provider', 'catalogProviders', () => assertProviderRegistered(dir, profile));
117
144
  const effectiveProvider = profile.provider !== undefined ? profile.provider : parent.options.provider;
118
145
  const effectiveModel = profile.model !== undefined ? profile.model : parent.options.model;
119
- // model 校验 + reasoningEffort 校验。
120
- await assertModelAdvertised(llm, profile, effectiveProvider, dir.modelsByProvider);
121
- await assertReasoningSupported(llm, profile, effectiveProvider, effectiveModel);
146
+ // model 校验 + reasoningEffort 校验。
147
+ await runChecked(check, 'model', 'providerModelList', () => assertModelAdvertised(llm, profile, effectiveProvider, dir.modelsByProvider));
148
+ await runChecked(check, 'reasoningEffort', 'resolveCallConfig', () => assertReasoningSupported(llm, profile, effectiveProvider, effectiveModel));
122
149
  }