dsh-plugin-tool-management 0.20.2 → 0.21.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/CHANGELOG.md +60 -1
- package/README.md +4 -1
- package/README_EN.md +4 -1
- package/docs/update.md +3 -1
- package/lib/client.js +225 -54
- package/lib/compat/capability-specs.js +60 -1
- package/lib/compat/host-contracts.js +72 -0
- package/lib/compat/host-probe.js +59 -0
- package/lib/compat/host-roots.js +1 -1
- package/lib/compat/probe.js +84 -13
- package/lib/context-inject-frame.js +134 -26
- package/lib/context-inject.js +12 -9
- package/lib/http-api.js +3 -2
- package/lib/http-fence.js +26 -1
- package/lib/index.js +5 -2
- package/lib/mcp/manager.js +6 -1
- package/lib/mcp/state-section.js +75 -34
- package/lib/memories/constants.js +39 -7
- package/lib/memories/projection.js +18 -18
- package/lib/memories/service.js +8 -7
- package/lib/memories/snapshot.js +27 -17
- package/lib/ops/compat.js +64 -2
- package/lib/ops/memory.js +2 -2
- package/lib/ops/prompts.js +30 -2
- package/lib/request-gate.js +5 -4
- package/lib/skills/catalog.js +2 -2
- package/lib/subagents/catalog.js +4 -4
- package/lib/subagents/child-preset.js +120 -0
- package/lib/subagents/persona-parse.js +9 -9
- package/lib/subagents/run-provider.js +218 -62
- package/lib/subagents/tool-filter.js +12 -12
- package/lib/subagents/tools.js +29 -22
- package/lib/tools/deps.js +4 -4
- package/lib/tools/mcp.js +34 -34
- package/lib/tools/memory.js +34 -34
- package/lib/tools/prompt.js +5 -5
- package/lib/tools/remove.js +17 -17
- package/lib/tools/scene.js +66 -66
- package/lib/tools/skills.js +21 -21
- package/lib/tools/subagent.js +13 -13
- package/lib/tools/table.js +2 -2
- package/package.json +25 -24
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
// 这张表就是与宿主的契约本体 —— 纯字面量,判定逻辑在 probe.ts 的 inspectCapability,
|
|
4
4
|
// 取数原语在 host-probe.ts。缺一个成员时界面给出的是这一行点名的降级,而不是删除序列
|
|
5
5
|
// 中途抛出的一次异常。
|
|
6
|
-
import { callableWithSentinel, isFn, PROBE_SENTINEL } from './host-probe.js';
|
|
6
|
+
import { callableWithSentinel, isFn, PROBE_SENTINEL, resolvableFromHost } from './host-probe.js';
|
|
7
7
|
/**
|
|
8
8
|
* The capability table. This IS the contract with the host: everything the
|
|
9
9
|
* adapters reach for is listed here with what happens when it is absent, so a
|
|
@@ -318,4 +318,63 @@ export const CAPABILITY_SPECS = [
|
|
|
318
318
|
return undefined;
|
|
319
319
|
},
|
|
320
320
|
},
|
|
321
|
+
// ---- 0.2.1 的四处新契约面 ------------------------------------------------
|
|
322
|
+
// 这四条都是**本版适配真实依赖**的宿主成员。它们与上面那些的区别是:上面那些从 0.1.x
|
|
323
|
+
// 起就在表里,而这四个是 0.2.1 才出现的 —— 下一代宿主改掉其中任何一个,对应的功能会
|
|
324
|
+
// **静默失效**(不抛错、只少一半),所以必须在升级后第一眼可见。
|
|
325
|
+
//
|
|
326
|
+
// 逐条的后果:
|
|
327
|
+
// ① register 没了 → 带人设的子代理委派失败(临时预设注册不上去);
|
|
328
|
+
// ② restrict 没了 → 人设的 tools/toolsDeny 不生效,子代理继承父级全部工具;
|
|
329
|
+
// ③ agent/created 不串行 → 工具限制慢一步(首轮看到的还是继承来的工具集);
|
|
330
|
+
// ④ admit 没了 → Web 鉴权退回本地回环判定,本机任意进程可调写 op。
|
|
331
|
+
{
|
|
332
|
+
id: 'agentPreset.register',
|
|
333
|
+
label: '临时子预设注册',
|
|
334
|
+
kind: 'write',
|
|
335
|
+
owner: 'presetRoster',
|
|
336
|
+
fallback: 'refuse-operation',
|
|
337
|
+
methods: ['register'],
|
|
338
|
+
// 只查存在性 + 与参考副本的文本比对,**不真注册**:register 会真的树上挂一棵插件树。
|
|
339
|
+
probe: (t) => isFn(t?.register) ? undefined : '宿主预设名册缺少 register():带人设的子代理委派无法创建独立预设',
|
|
340
|
+
},
|
|
341
|
+
{
|
|
342
|
+
id: 'tools.restrict-method',
|
|
343
|
+
label: '工具限制施加(restrict)',
|
|
344
|
+
kind: 'write',
|
|
345
|
+
owner: 'tools',
|
|
346
|
+
fallback: 'disable-destructive',
|
|
347
|
+
methods: ['restrict'],
|
|
348
|
+
probe: (t) => isFn(t?.restrict) ? undefined : '宿主工具服务缺少 restrict():人设的工具限制不生效,子代理将继承父级全部工具',
|
|
349
|
+
},
|
|
350
|
+
{
|
|
351
|
+
// 探测的是 **agent 作用域**上 `on` 在场这一事实。真正的时序保证(announce 等待
|
|
352
|
+
// 串行 agent/created 监听全部完成后才放行收件箱)藏在宿主源码里,探针看不见 ——
|
|
353
|
+
// 这一条能抓的是"ctx.on 整个没了 / 改名",那时监听器挂不上、限制退化成事后施加。
|
|
354
|
+
id: 'agent.created-event',
|
|
355
|
+
label: '子代理创建事件(agent/created)',
|
|
356
|
+
kind: 'read',
|
|
357
|
+
owner: 'agentContext',
|
|
358
|
+
fallback: 'disable-destructive',
|
|
359
|
+
methods: ['on'],
|
|
360
|
+
probe: (t) => isFn(t?.on) ? undefined : 'agent 作用域缺少 on():无法在子代理创建时施加工具限制',
|
|
361
|
+
},
|
|
362
|
+
{
|
|
363
|
+
// 0.2.1 起宿主把鉴权栅栏拆成 admit()/requestRejection() 两层(见 http-fence.ts)。
|
|
364
|
+
// admit 不在场时插件退回 requestRejection,再缺则退回本地回环判定 —— 每一级都少一层,
|
|
365
|
+
// 但都不抛错,所以必须单列一条让人看得见现在停在哪一级。
|
|
366
|
+
id: 'connection.admit',
|
|
367
|
+
label: '连接准入(admit)',
|
|
368
|
+
kind: 'read',
|
|
369
|
+
owner: 'connection',
|
|
370
|
+
fallback: 'refuse-operation',
|
|
371
|
+
methods: ['admit'],
|
|
372
|
+
probe: (t) => isFn(t?.admit) ? undefined : '宿主 connection 缺少 admit():Web 鉴权降级(退到 requestRejection / 本地回环判定)',
|
|
373
|
+
// 静态检查的门槛:`dsh-client-connection` **不是**本插件的 peer(宿主不要求插件直接依赖
|
|
374
|
+
// 它),doctor 从插件锚点解析不到它。那时不能报 `missing` —— 那等于断言"宿主没有这个
|
|
375
|
+
// 能力",而运行时探针看到的是 Web 组合里真实的 connection 服务(0.2.1 起一定带 admit)。
|
|
376
|
+
// 门槛取宿主侧:至少一个宿主锚点解析得到才查;解析不到退回 `runtime-only`(如实标注
|
|
377
|
+
// "只能对活的宿主查"),而不是编一个"缺失"。
|
|
378
|
+
staticWhen: () => resolvableFromHost('@deepseek-ai/dsh-client-connection'),
|
|
379
|
+
},
|
|
321
380
|
];
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
// 宿主**非 peer** 契约的共享清单:运行时探测(compat/probe.ts 的 runtime-only 上报)与
|
|
2
|
+
// `scripts/doctor.mjs` 都要读它们,所以只有这一份。
|
|
3
|
+
//
|
|
4
|
+
// 为什么单独一层:这些包**不在**本插件的 `peerDependencies` 里 —— 宿主不要求插件依赖它们,
|
|
5
|
+
// 但插件在运行时按**裸模块名**直接 require(MCP 客户端、人设、全局指令、技能目录)。它们有
|
|
6
|
+
// 两个共同点让它们必须被看见:
|
|
7
|
+
// (1) 官方改名或撤掉其中一个,对应功能**静默降级**(不抛错,只是少一半);
|
|
8
|
+
// (2) 它们不参与模块身份比对(不是硬依赖),所以"两份不同拷贝"那一栏根本看不到它们。
|
|
9
|
+
//
|
|
10
|
+
// 此前这份清单只活在 doctor 里:命令行看得到,兼容页看不到。而兼容页正是用户升级 DSH 后
|
|
11
|
+
// 第一眼看的界面 —— 于是同一个"官方把某个包撤了"的事实,CLI 报得出来、界面报不出来。
|
|
12
|
+
// 提升到这里之后两边读同一份(doctor 从 lib/ import,不再自己维护一份)。
|
|
13
|
+
/**
|
|
14
|
+
* `~/.dsh/AGENTS.md` 的**增量刷新**契约 —— 本插件"应用提示词预设"依赖它。
|
|
15
|
+
*
|
|
16
|
+
* 插件把预设正文写进全局 `AGENTS.md`,然后靠官方**每一步**重新读盘(对比 stat 的 version 与
|
|
17
|
+
* 内容 SHA-1)决定要不要把新正文送进模型。官方一旦改成事件驱动、加 touchedPaths 白名单、或改
|
|
18
|
+
* digest 口径,"应用预设"就会**静默失效**(旧正文继续注入),零报错。
|
|
19
|
+
*/
|
|
20
|
+
export const AGENTS_MD_REFRESH_PACKAGE = '@deepseek-ai/dsh-agent-instructions';
|
|
21
|
+
export const AGENTS_MD_REFRESH_MARKERS = [
|
|
22
|
+
{ needle: 'agentInstructionsHook', why: '每步重读全局 AGENTS.md 的钩子入口' },
|
|
23
|
+
{ needle: 'trimmedInstructionDigest', why: '内容同一性 = 去掉首尾空白后的 SHA-1' },
|
|
24
|
+
{ needle: 'instructionContentSha1', why: 'SHA-1 摘要口径' },
|
|
25
|
+
{ needle: 'sameInstructionChange', why: '按 {action, scope, path, digest} 判「有没有变化」' },
|
|
26
|
+
];
|
|
27
|
+
/**
|
|
28
|
+
* 运行时按**裸模块名** require 的宿主包,以及每个包守着的契约。
|
|
29
|
+
*
|
|
30
|
+
* 与 `AGENTS_MD_REFRESH_MARKERS` 的分工:那一份是"同一个包内的若干标识符",这一份是
|
|
31
|
+
* "若干不同的包各要求什么"。合起来才是"官方撤掉了什么"的完整清单。
|
|
32
|
+
*/
|
|
33
|
+
export const BARE_HOST_MODULE_CONTRACTS = [
|
|
34
|
+
{
|
|
35
|
+
package: '@deepseek-ai/dsh-mcp-client',
|
|
36
|
+
feature: 'MCP 客户端(MCP 服务器装配的宿主出口)',
|
|
37
|
+
markers: [],
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
package: '@deepseek-ai/dsh-persona',
|
|
41
|
+
feature: '人设(子代理人格的读取与解析)',
|
|
42
|
+
markers: [],
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
package: AGENTS_MD_REFRESH_PACKAGE,
|
|
46
|
+
feature: 'AGENTS.md 注入(应用提示词预设)',
|
|
47
|
+
markers: AGENTS_MD_REFRESH_MARKERS,
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
package: '@deepseek-ai/dsh-tool-skill',
|
|
51
|
+
feature: '技能目录(官方技能挂载的宿主出口)',
|
|
52
|
+
markers: [],
|
|
53
|
+
},
|
|
54
|
+
];
|
|
55
|
+
/** doctor 的 CLI 用:裸模块名清单(顺序与上面一致)。 */
|
|
56
|
+
export const BARE_HOST_MODULE_NAMES = BARE_HOST_MODULE_CONTRACTS.map((item) => item.package);
|
|
57
|
+
/**
|
|
58
|
+
* 官方 JSONL 会话目录布局的 canary —— 历史适配器(`src/sessions/workspace.ts` →
|
|
59
|
+
* `jsonlSessionDirectory`)硬编码了它。
|
|
60
|
+
*
|
|
61
|
+
* 这些是**行为常量**,不是装饰:任何一个变了,适配器就再也不能确认某个转写目录属于它正要删的
|
|
62
|
+
* 那个会话,删除会退化成"只删工件、保留父目录",而唯一征兆只是插件侧一行日志。那种退化被刻意
|
|
63
|
+
* 做成非致命(工件仍然删掉,且删后经 `persistence.stat` 复核),所以对着一台没活着的宿主做静态
|
|
64
|
+
* 检查,是用户在自己翻出孤儿目录之前唯一能知道漂移的办法。
|
|
65
|
+
*/
|
|
66
|
+
export const JSONL_LAYOUT_PACKAGE = '@deepseek-ai/dsh-session-persistence-jsonl';
|
|
67
|
+
export const JSONL_LAYOUT_MARKERS = [
|
|
68
|
+
{ needle: 'session-persistence-jsonl', why: '适配器的后端名门槛' },
|
|
69
|
+
{ needle: '_no-cwd', why: '会话头没有 cwd 时落进的桶' },
|
|
70
|
+
{ needle: '251', why: '项目段截断长度' },
|
|
71
|
+
{ needle: 'padStart(4', why: '~XXXX UTF-16 转义编码' },
|
|
72
|
+
];
|
package/lib/compat/host-probe.js
CHANGED
|
@@ -177,12 +177,18 @@ const SESSIONS_CLASS = { pkg: '@deepseek-ai/dsh-session', target: 'SessionStore'
|
|
|
177
177
|
// (dsh-settings 导出 SettingsForms;dsh-agent-preset-registry 导出 AgentPresetRegistry)。
|
|
178
178
|
const SETTINGS_CLASS = { pkg: '@deepseek-ai/dsh-settings', target: 'SettingsForms' };
|
|
179
179
|
const ROSTER_CLASS = { pkg: '@deepseek-ai/dsh-agent-preset-registry', target: 'AgentPresetRegistry' };
|
|
180
|
+
// 0.2.1 的两块契约面(tools.restrict / connection.admit)也都声明在官方类上,所以同样接入
|
|
181
|
+
// 「参考副本 + 文本比对」:官方改了实现,比对的结论从"一致"变成"漂移",界面上看得见。
|
|
182
|
+
const TOOLS_CLASS = { pkg: '@deepseek-ai/dsh-tools', target: 'ToolRuntime' };
|
|
183
|
+
const CONNECTION_CLASS = { pkg: '@deepseek-ai/dsh-client-connection', target: 'HostConnectionService' };
|
|
180
184
|
export const OWNER_CLASSES = {
|
|
181
185
|
workspace: WORKSPACE_CLASS,
|
|
182
186
|
projectionCache: PROJECTION_CLASS,
|
|
183
187
|
sessions: SESSIONS_CLASS,
|
|
184
188
|
settings: SETTINGS_CLASS,
|
|
185
189
|
presetRoster: ROSTER_CLASS,
|
|
190
|
+
tools: TOOLS_CLASS,
|
|
191
|
+
connection: CONNECTION_CLASS,
|
|
186
192
|
};
|
|
187
193
|
/** The named export's prototype, or undefined when the package is absent/trimmed. */
|
|
188
194
|
function classPrototypeOf(ref) {
|
|
@@ -257,5 +263,58 @@ export function referencePrototypes() {
|
|
|
257
263
|
const roster = classPrototypeOf(ROSTER_CLASS);
|
|
258
264
|
if (roster !== undefined)
|
|
259
265
|
out.roster = roster;
|
|
266
|
+
// 0.2.1 的两块:`dsh-tools` 是本插件的 peerDependency(参考副本拿得到);
|
|
267
|
+
// `dsh-client-connection` **不在** peerDependencies 里(宿主不要求插件直接依赖它),
|
|
268
|
+
// 所以参考副本常态取不到 —— textMatch 记 undefined,那条能力退化成纯存在性检查。
|
|
269
|
+
// 这是如实的能力边界:能验"admit 在不在",验不了"实现漂没漂"。
|
|
270
|
+
const tools = classPrototypeOf(TOOLS_CLASS);
|
|
271
|
+
if (tools !== undefined)
|
|
272
|
+
out.tools = tools;
|
|
273
|
+
const connection = classPrototypeOf(CONNECTION_CLASS);
|
|
274
|
+
if (connection !== undefined)
|
|
275
|
+
out.connection = connection;
|
|
260
276
|
return out;
|
|
261
277
|
}
|
|
278
|
+
/**
|
|
279
|
+
* 从**宿主侧**能不能解析到一个包(不 import,只看解析)。
|
|
280
|
+
*
|
|
281
|
+
* 为什么需要它:有一部分官方包**不是**本插件的 peer(宿主不要求插件依赖它们),插件自己的
|
|
282
|
+
* `node_modules` 里就没有 —— `safeResolve`/`classPrototypeOf` 一律取不到。运行时探测不看
|
|
283
|
+
* 插件侧(它看的是 ctx 上的活服务),所以只有 doctor 这种"没有活宿主"的静态视图会被误导:
|
|
284
|
+
* 它从插件锚点解析,解析不到就把一条**运行时确实存在**的能力记成 `missing`
|
|
285
|
+
* (2026-10-17 `connection.admit` 就这么误报过:`dsh-client-connection` 不在插件依赖图里,
|
|
286
|
+
* 而它承载的 `admit()` 在 0.2.1 的 Web 组合里一定有)。
|
|
287
|
+
*
|
|
288
|
+
* 判据必须是宿主侧:逐个宿主安装锚点试着解析(纯 `require.resolve`,不 import),只要有
|
|
289
|
+
* **一个**能解析到就算能查。与 `safeResolve` 的分工:那个回答"本插件能不能 import 它",
|
|
290
|
+
* 这个回答"宿主装不装它" —— 后者才是静态能力表该用的口径。
|
|
291
|
+
*/
|
|
292
|
+
export function resolvableFromHost(specifier) {
|
|
293
|
+
for (const anchor of hostResolveAnchors()) {
|
|
294
|
+
try {
|
|
295
|
+
createRequire(anchor).resolve(specifier);
|
|
296
|
+
return true;
|
|
297
|
+
}
|
|
298
|
+
catch {
|
|
299
|
+
/* try the next anchor */
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
return false;
|
|
303
|
+
}
|
|
304
|
+
/**
|
|
305
|
+
* 宿主安装的解析锚点(`package.json` 路径,按候选顺序)。
|
|
306
|
+
*
|
|
307
|
+
* 与 probe.ts 的 `hostPackageRoot()` 共用同一份 `hostRootCandidates()`,但这里**不要求**
|
|
308
|
+
* `dsh/package.json` 在场:即使那一层只剩空壳(0.1.5 时代的悬空 junction 树,本机正是如此),
|
|
309
|
+
* 它下面的 `node_modules/@deepseek-ai/<pkg>` 仍可能解析得到。判据各取所需 —— probe.ts 要
|
|
310
|
+
* "像不像一份完整安装",这里只问"这个包从这里能不能解析到"。
|
|
311
|
+
*/
|
|
312
|
+
function hostResolveAnchors() {
|
|
313
|
+
return hostRootCandidatesFn().map((dir) => join(dirname(dir), 'package.json'));
|
|
314
|
+
}
|
|
315
|
+
// `hostRootCandidates()` 的注入点:由 probe.ts 在模块加载时填进来。不反向 import 是为了
|
|
316
|
+
// 不成环(probe.ts → host-probe.ts → probe.ts),而"宿主在哪"的口径仍然只有一份。
|
|
317
|
+
let hostRootCandidatesFn = () => [];
|
|
318
|
+
export function provideHostRootCandidates(fn) {
|
|
319
|
+
hostRootCandidatesFn = fn;
|
|
320
|
+
}
|
package/lib/compat/host-roots.js
CHANGED
|
@@ -20,7 +20,7 @@ export const IDENTITY_PACKAGES = [
|
|
|
20
20
|
'@deepseek-ai/dsh-storage-domain',
|
|
21
21
|
'@deepseek-ai/dsh-spill-local',
|
|
22
22
|
];
|
|
23
|
-
export const VERIFIED_HOST_VERSION = '0.2.
|
|
23
|
+
export const VERIFIED_HOST_VERSION = '0.2.1-rc.2';
|
|
24
24
|
export const OPERATION_ROUTES = {
|
|
25
25
|
archive: { native: ['workspace.archive-native'], adapter: ['workspace.enqueue', 'workspace.set-state'] },
|
|
26
26
|
unarchive: { native: ['workspace.unarchive-native'], adapter: ['workspace.enqueue', 'workspace.set-state'] },
|
package/lib/compat/probe.js
CHANGED
|
@@ -30,17 +30,23 @@
|
|
|
30
30
|
* leave the rest working.
|
|
31
31
|
*/
|
|
32
32
|
import { createRequire } from 'node:module';
|
|
33
|
-
import { existsSync } from 'node:fs';
|
|
33
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
34
34
|
import { dirname, join } from 'node:path';
|
|
35
35
|
import { fileURLToPath } from 'node:url';
|
|
36
36
|
import { CAPABILITY_SPECS } from './capability-specs.js';
|
|
37
|
-
import {
|
|
37
|
+
import { BARE_HOST_MODULE_CONTRACTS } from './host-contracts.js';
|
|
38
|
+
import { isAsarPath, isFn, OWNER_CLASSES, realPathWithReason, referencePrototypes, provideHostRootCandidates, safeResolve, textMatchOf, versionOf, versionOfPackageAt, } from './host-probe.js';
|
|
38
39
|
// 宿主安装根的发现与「该走哪条恢复路」的判定 2026-10-01 整段搬到 ./host-roots.js,正文一字
|
|
39
40
|
// 未改(moved-verify 逐字对拍)。probe.js 的公开面照旧:搬走的那 12 件全部从这里再出口 ——
|
|
40
41
|
// index.ts / ops/compat.ts / sessions/bridge.ts 都按 ./compat/probe.js 引它们,少一件 equiv
|
|
41
42
|
// 对拍就报「导出面丢失」。
|
|
42
43
|
export { asarHostPackageRoot, findingsById, hostRootCandidates, IDENTITY_PACKAGES, npxCacheHostRoots, OPERATION_ROUTES, recoveryFor, refusalsFor, routeFor, VERIFIED_HOST_VERSION, } from './host-roots.js';
|
|
43
44
|
import { hostRootCandidates, IDENTITY_PACKAGES, routeFor, VERIFIED_HOST_VERSION } from './host-roots.js';
|
|
45
|
+
// 「宿主在哪」这份口径只有一份(host-roots.ts 的 hostRootCandidates)。静态能力表里有条目要
|
|
46
|
+
// 按宿主侧解析结果决定**这一轮能不能查**(`staticWhen`,见 connection.admit 那条),而
|
|
47
|
+
// capability-specs.ts 不能反向 import 本文件(probe → specs → probe 成环)。所以在这里把
|
|
48
|
+
// 函数本体注入 host-probe.ts:它只调用、不 import,加载顺序天然正确(本文件先被求值)。
|
|
49
|
+
provideHostRootCandidates(hostRootCandidates);
|
|
44
50
|
/** Packages whose physical module identity matters to this plugin. */
|
|
45
51
|
/**
|
|
46
52
|
* 本插件是不是从**源码检出**跑起来的(仓库里那份,`scripts/host-deps.mjs` 就躺在旁边)。
|
|
@@ -63,9 +69,15 @@ export function isSourceInstall() {
|
|
|
63
69
|
* `0.1.8-rc.1` / `0.2.1-rc.1` / `0.3.0-rc.1` 全部 false)—— 范围只能**逐代枚举并集**,
|
|
64
70
|
* 每适配一代新 rc 就补一段(见 README 的升级清单)。
|
|
65
71
|
*
|
|
66
|
-
*
|
|
67
|
-
*
|
|
68
|
-
*
|
|
72
|
+
* 每一代各自**带上界**(0.1.7 → `<0.2.0-rc.1`、0.2.0 → `<0.2.1-rc.2`、
|
|
73
|
+
* 0.2.1 → `<0.2.2`):宿主跨 minor 升级时不给"装进去再说",而是**主动发一版**。新的 minor
|
|
74
|
+
* 已经动过 SubagentStartRequest、身份比对这些会静默改行为的契约,旧适配继续跑在一份新宿主上,
|
|
75
|
+
* 失败会以"功能莫名不工作"的形式出现 —— 那时用户无法判断是插件问题还是版本问题。装上被拒 +
|
|
76
|
+
* 一版新适配,优于装上然后部分功能悄悄失效。
|
|
77
|
+
* 上界**必须逐段写**:只在最后一段挂 `<0.2.2` 是无效的 —— `>=0.2.0-rc.1` 那一支没有上界,
|
|
78
|
+
* 并集仍会匹配 0.3.0 / 1.0.0(实测:那种写法下 0.3.0 satisfies 为 true)。`^` 更不能用:
|
|
79
|
+
* `^0.2.0` 的隐含上界是 `<0.3.0`,而 0.2.1-rc.2 这个已适配版本自己都满足不了它(prerelease
|
|
80
|
+
* 只在同元组比较器带 prerelease 时才匹配)。
|
|
69
81
|
*
|
|
70
82
|
* 2026-09-30 审查 §5 F9 + §5.4:这一段此前与 `package.json` 的 peerDependencies **漂移**成
|
|
71
83
|
* 两种写法(这里是 `^` 三段、那里是 `>=` 两段,且下界不同)。现在两处是**同一个字符串**,
|
|
@@ -80,16 +92,13 @@ export function isSourceInstall() {
|
|
|
80
92
|
* 0.2.0-rc.2(桌面版首发)已实测通过:`assessHost` 全通过,宿主版本读得出来,
|
|
81
93
|
* 归档宿主的身份判定见 `hostPackageRoot` / `isAsarPath`。所以它同时进范围与「已验证」。
|
|
82
94
|
*/
|
|
83
|
-
export const EXPECTED_PEER_RANGE = '>=0.1.7-rc.2 || >=0.2.0-rc.1';
|
|
95
|
+
export const EXPECTED_PEER_RANGE = '>=0.1.7-rc.2 <0.2.0-rc.1 || >=0.2.0-rc.1 <0.2.1-rc.2 || >=0.2.1-rc.2 <0.2.2';
|
|
84
96
|
/** The release this plugin's adapters were last verified against. */
|
|
85
97
|
/**
|
|
86
|
-
* peer range
|
|
98
|
+
* peer range 的下界。仍从 EXPECTED_PEER_RANGE 派生,避免两处手写漂移。
|
|
87
99
|
*
|
|
88
|
-
*
|
|
89
|
-
*
|
|
90
|
-
* 宿主版本高于已验证范围时,`assessHost` 会逐项探测并把缺失能力降级/禁用,
|
|
91
|
-
* 而不是在安装期拒绝整包。
|
|
92
|
-
* 从 EXPECTED_PEER_RANGE 派生,避免两处手写漂移。
|
|
100
|
+
* 注意它取的是范围里的**最低**那个版本号(`>=0.1.7-rc.2`),不是已验证版本 —— 与上界收敛
|
|
101
|
+
* 不冲突:这条讲的是"最早支持哪一代",上界讲的是"当前适配覆盖到哪一代"。
|
|
93
102
|
*
|
|
94
103
|
* 2026-09-29 起**不再出现在界面上**(用户裁定:删掉「最低要求 ≥ …」那一行 —— 它是安装期
|
|
95
104
|
* 门槛,摆在"当前宿主"卡片下面会被读成对当前宿主的断言)。保留此导出是因为它进了
|
|
@@ -223,6 +232,11 @@ export const SUBSTITUTED_CAPABILITIES = CAPABILITY_SPECS
|
|
|
223
232
|
export const CAPABILITY_STATIC = CAPABILITY_SPECS.map((spec) => {
|
|
224
233
|
const ref = OWNER_CLASSES[spec.owner];
|
|
225
234
|
const methods = spec.methods;
|
|
235
|
+
// `staticWhen` 是"这一轮能不能静态检查"的门槛。它说不的时候,`check` 必须是 null ——
|
|
236
|
+
// 否则 doctor 会从插件锚点解析一个本插件不依赖的包,然后把"解析不到"写成"宿主缺失"
|
|
237
|
+
// (2026-10-17 `connection.admit` 的误报正是这条路径)。null 让它落到 `runtime-only`:
|
|
238
|
+
// 「只能对活的宿主查」是事实,"missing" 不是。
|
|
239
|
+
const checkableByStaticView = spec.staticWhen === undefined || spec.staticWhen();
|
|
226
240
|
return {
|
|
227
241
|
id: spec.id,
|
|
228
242
|
label: spec.label,
|
|
@@ -230,7 +244,7 @@ export const CAPABILITY_STATIC = CAPABILITY_SPECS.map((spec) => {
|
|
|
230
244
|
optional: spec.optional === true,
|
|
231
245
|
// A `fields`-only spec (instance Maps) and a spec whose judgement lives in
|
|
232
246
|
// its `probe` have nothing a prototype can answer — they stay runtime-only.
|
|
233
|
-
check: ref !== undefined && methods !== undefined && methods.length > 0
|
|
247
|
+
check: checkableByStaticView && ref !== undefined && methods !== undefined && methods.length > 0
|
|
234
248
|
? { pkg: ref.pkg, target: ref.target, members: methods }
|
|
235
249
|
: null,
|
|
236
250
|
};
|
|
@@ -272,6 +286,15 @@ export function assessHost(ctx) {
|
|
|
272
286
|
const registry = unwrap(get('workspaceRegistry'));
|
|
273
287
|
const cache = unwrap(get('sessionProjectionCache'));
|
|
274
288
|
const sessions = unwrap(ctx.sessions !== undefined ? ctx.sessions : get('sessions'));
|
|
289
|
+
// tools 走 ctx 属性优先(它在插件 inject 清单里)。它有两层取法:宿主平面的 ctx.tools
|
|
290
|
+
// 与**子代理 scope 里**的那个(restrict 在子 scope 上才作用于该子代理,run-provider.ts
|
|
291
|
+
// 就是 `childAgent.ctx.get('tools')` 取的)。探测只能看宿主平面那一层 —— 子 scope 还没建。
|
|
292
|
+
const tools = unwrap(ctx.tools !== undefined ? ctx.tools : get('tools'));
|
|
293
|
+
// agent 作用域:探测对象是 agent 自己的 ctx(`on` 挂在它上面)。这里没有活的 agent 可看,
|
|
294
|
+
// 所以拿宿主平面的 ctx 代答 —— 同一个类实例链上的成员存在性,`on` 改名时两边一起改。
|
|
295
|
+
// 局限如实标注:探得到"on 在不在",探不到"agent/created 是否仍串行"。
|
|
296
|
+
const agentContext = ctx;
|
|
297
|
+
const connection = unwrap(get('connection'));
|
|
275
298
|
// settings 走 ctx 属性优先(插件 inject 清单里的正式服务),roster 走服务名查找
|
|
276
299
|
// ('agentPresets' 与 preset-reach.ts 的 presetRosterOf 同名 —— 注入矩阵实际用的就是它)。
|
|
277
300
|
const settings = unwrap(ctx.settings !== undefined ? ctx.settings : get('settings'));
|
|
@@ -287,6 +310,9 @@ export function assessHost(ctx) {
|
|
|
287
310
|
settings: { target: settings, reference: references.settings },
|
|
288
311
|
presetRoster: { target: roster, reference: references.roster },
|
|
289
312
|
fs: { target: fsService, reference: undefined },
|
|
313
|
+
tools: { target: tools, reference: references.tools },
|
|
314
|
+
agentContext: { target: agentContext, reference: undefined },
|
|
315
|
+
connection: { target: connection, reference: references.connection },
|
|
290
316
|
};
|
|
291
317
|
const findings = CAPABILITY_SPECS.map((spec) => {
|
|
292
318
|
const slot = targets[spec.owner];
|
|
@@ -308,6 +334,8 @@ export function assessHost(ctx) {
|
|
|
308
334
|
let pluginRealpathNote = null;
|
|
309
335
|
let asarHostVersion = null;
|
|
310
336
|
let asarPluginVersion = null;
|
|
337
|
+
// 宿主非 peer 契约只跟"宿主装在哪"有关,与下面那个逐包 identity 循环无关,先量一次。
|
|
338
|
+
const contracts = hostContractStatus(hostRoot);
|
|
311
339
|
for (const name of IDENTITY_PACKAGES) {
|
|
312
340
|
const resolved = safeResolve(name);
|
|
313
341
|
modules[name] = resolved;
|
|
@@ -424,6 +452,49 @@ export function assessHost(ctx) {
|
|
|
424
452
|
degraded,
|
|
425
453
|
mayDelete: delta,
|
|
426
454
|
generatedAt: Date.now(),
|
|
455
|
+
...(contracts === null ? {} : { contracts }),
|
|
456
|
+
};
|
|
457
|
+
}
|
|
458
|
+
/**
|
|
459
|
+
* 宿主**非 peer** 契约的实况:插件按裸模块名 require 的那几个官方包,宿主还暴不暴露、
|
|
460
|
+
* 入口里那几条 canary 还在不在。
|
|
461
|
+
*
|
|
462
|
+
* 为什么在探测里量而不是只在 doctor 里量:doctor 是"没有活宿主"的静态视图,而兼容页看到的是
|
|
463
|
+
* 正在跑的宿主。升级 DSH 后第一眼该看见的正是这一块 —— 官方撤掉某个包时**不抛错**,功能只是
|
|
464
|
+
* 少一半,除此之外没有任何信号。
|
|
465
|
+
*
|
|
466
|
+
* 口径从宿主安装锚点解析(不是插件侧):这些包**不是**本插件的 peer,插件 `node_modules`
|
|
467
|
+
* 里通常没有,从插件侧解析只会得到一律"解析不到",那不是一个结论。
|
|
468
|
+
*
|
|
469
|
+
* @param hostRoot - `.../node_modules/@deepseek-ai` 或 `null`(找不到宿主)。
|
|
470
|
+
*/
|
|
471
|
+
function hostContractStatus(hostRoot) {
|
|
472
|
+
if (hostRoot === null)
|
|
473
|
+
return null;
|
|
474
|
+
const packages = BARE_HOST_MODULE_CONTRACTS.map((contract) => {
|
|
475
|
+
let resolved = null;
|
|
476
|
+
try {
|
|
477
|
+
resolved = createRequire(join(dirname(hostRoot), 'package.json')).resolve(contract.package);
|
|
478
|
+
}
|
|
479
|
+
catch {
|
|
480
|
+
resolved = null;
|
|
481
|
+
}
|
|
482
|
+
let missingMarkers = [];
|
|
483
|
+
if (resolved !== null && contract.markers.length > 0) {
|
|
484
|
+
try {
|
|
485
|
+
const source = readFileSync(resolved, 'utf8');
|
|
486
|
+
missingMarkers = contract.markers.filter((marker) => !source.includes(marker.needle)).map((marker) => marker.needle);
|
|
487
|
+
}
|
|
488
|
+
catch { /* 入口读不到时只报"解析不到",不额外造一条 canary 结论 */ }
|
|
489
|
+
}
|
|
490
|
+
return { package: contract.package, feature: contract.feature, resolved, missingMarkers };
|
|
491
|
+
});
|
|
492
|
+
return {
|
|
493
|
+
packages,
|
|
494
|
+
missing: packages.filter((item) => item.resolved === null).map((item) => item.package),
|
|
495
|
+
drifted: packages
|
|
496
|
+
.filter((item) => item.missingMarkers.length > 0)
|
|
497
|
+
.map((item) => ({ package: item.package, missingMarkers: item.missingMarkers })),
|
|
427
498
|
};
|
|
428
499
|
}
|
|
429
500
|
/**
|