dsh-plugin-tool-management 0.9.1 → 0.11.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 +119 -1
- package/README.md +227 -201
- package/README_EN.md +227 -199
- package/docs/images/1-EN.png +0 -0
- package/docs/images/1.png +0 -0
- package/docs/images/2-EN.png +0 -0
- package/docs/images/2.png +0 -0
- package/docs/images/3-EN.png +0 -0
- package/docs/images/3.png +0 -0
- package/docs/images/4-EN.png +0 -0
- package/docs/images/4.png +0 -0
- package/docs/images/5-EN.png +0 -0
- package/docs/images/5.png +0 -0
- package/docs/images/6-EN.png +0 -0
- package/docs/images/6.png +0 -0
- package/docs/images/7-EN.png +0 -0
- package/docs/images/7.png +0 -0
- package/docs/images/8-EN.png +0 -0
- package/docs/images/8.png +0 -0
- package/docs/update.md +144 -12
- package/lib/client.js +8437 -5929
- package/lib/compat/preset-reach.js +1 -10
- package/lib/compat/probe.js +158 -25
- package/lib/context-inject.js +396 -53
- package/lib/host-names.js +12 -0
- package/lib/http-fence.js +35 -15
- package/lib/hub.js +31 -3
- package/lib/imports/parsers.js +15 -9
- package/lib/imports/upload.js +43 -4
- package/lib/index.js +940 -3765
- package/lib/mcp/loader-token.js +238 -0
- package/lib/mcp/manager.js +1681 -0
- package/lib/mcp/override-blocks.js +20 -9
- package/lib/mcp/patch-yaml.js +351 -0
- package/lib/mcp/secret-guard.js +145 -0
- package/lib/mcp/state-section.js +64 -21
- package/lib/{rules → memories}/archive-engine.js +65 -10
- package/lib/{rules → memories}/archive.js +6 -7
- package/lib/memories/constants.js +128 -0
- package/lib/memories/index-io.js +330 -0
- package/lib/memories/projection.js +280 -0
- package/lib/memories/service.js +686 -0
- package/lib/memories/snapshot.js +672 -0
- package/lib/ops/candidates.js +64 -0
- package/lib/ops/compat.js +136 -0
- package/lib/ops/ctx.js +9 -0
- package/lib/ops/memory.js +678 -0
- package/lib/ops/prompts.js +107 -0
- package/lib/ops/scene-records.js +460 -0
- package/lib/ops/scene-sync.js +17 -0
- package/lib/ops/sessions.js +603 -0
- package/lib/ops/trash.js +140 -0
- package/lib/paths.js +103 -0
- package/lib/prompts/preset-id.js +49 -0
- package/lib/{agents-md → prompts}/service.js +1 -1
- package/lib/request-gate.js +320 -0
- package/lib/scene-prompt-sync.js +4 -4
- package/lib/scenes/candidates.js +344 -0
- package/lib/{history → sessions}/bridge.js +22 -5
- package/lib/sessions/history.js +323 -0
- package/lib/{history → sessions}/tombstone.js +1 -2
- package/lib/{history → sessions}/workspace.js +92 -24
- package/lib/skills/catalog.js +6 -9
- package/lib/skills/core.js +79 -43
- package/lib/skills/readonly-discovery.js +4 -1
- package/lib/skills/service.js +98 -13
- package/lib/subagents/catalog.js +31 -15
- package/lib/subagents/service.js +506 -89
- package/lib/subagents/tools.js +29 -4
- package/lib/tools/deps.js +8 -0
- package/lib/tools/mcp.js +110 -0
- package/lib/tools/memory.js +87 -0
- package/lib/tools/prompt.js +70 -0
- package/lib/tools/skills.js +139 -0
- package/lib/tools/subagent.js +40 -0
- package/package.json +105 -102
- package/lib/agents-md/preset-id.js +0 -49
- package/lib/history/projcache.js +0 -335
- package/lib/rules/service.js +0 -2971
|
@@ -56,6 +56,7 @@
|
|
|
56
56
|
* exactly what its author asked for; the plugin's job is to say so out loud and
|
|
57
57
|
* point at the switch that overrides it.
|
|
58
58
|
*/
|
|
59
|
+
import { MCP_CLIENT_MODULE } from '../host-names.js';
|
|
59
60
|
/** Whether a preset declares "no extra text": persona `complete: true` or runtime context off. */
|
|
60
61
|
export function isSuppressingPreset(facts) {
|
|
61
62
|
return facts.personaComplete === true || facts.includeRuntimeContext === false;
|
|
@@ -74,16 +75,6 @@ export function injectionFactsOf(facts) {
|
|
|
74
75
|
const PERSONA_MODULE = '@deepseek-ai/dsh-persona';
|
|
75
76
|
const AGENT_INSTRUCTIONS_MODULE = '@deepseek-ai/dsh-agent-instructions';
|
|
76
77
|
const TOOL_SKILL_MODULE = '@deepseek-ai/dsh-tool-skill';
|
|
77
|
-
/**
|
|
78
|
-
* An MCP client mounted INSIDE a composition. The shipped presets mount none,
|
|
79
|
-
* so MCP tools normally come from the host plane (the `$DSH_HOME/cordis.patch.yml`
|
|
80
|
-
* layer) and are callable under every preset; a user-authored preset that mounts
|
|
81
|
-
* its own client only carries those tools under itself, and this scan is how the
|
|
82
|
-
* page can tell the two apart. Tool reachability is not the column's answer,
|
|
83
|
-
* though — the server list and the user's notes are a prompt section, which a
|
|
84
|
-
* complete persona suppresses whichever client serves the tools.
|
|
85
|
-
*/
|
|
86
|
-
const MCP_CLIENT_MODULE = '@deepseek-ai/dsh-mcp-client';
|
|
87
78
|
const NAME_LINE = /^(\s*)name:\s*(['"]?)([^'"\s#]+)\2\s*(?:#.*)?$/;
|
|
88
79
|
const ENTRY_LINE = /^(\s*)-\s/;
|
|
89
80
|
const DISABLED_LINE = /^\s*disabled:\s*(.+?)\s*(?:#.*)?$/;
|
package/lib/compat/probe.js
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
*
|
|
5
5
|
* WHY THIS REPLACES THE TEXT-COMPARISON GATE
|
|
6
6
|
* ------------------------------------------
|
|
7
|
-
* The previous gate in `
|
|
7
|
+
* The previous gate in `sessions/bridge.ts` compared each host method against
|
|
8
8
|
* this plugin's own copy of the official prototype with
|
|
9
9
|
* `Function.prototype.toString()`. That only ever answered one question —
|
|
10
10
|
* "is the host running the same release I was written against?" — and it
|
|
@@ -139,22 +139,88 @@ function prototypeOf(instance) {
|
|
|
139
139
|
const proto = Object.getPrototypeOf(instance);
|
|
140
140
|
return proto !== null && typeof proto === 'object' ? proto : undefined;
|
|
141
141
|
}
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
142
|
+
// The official package + class each capability owner's members live on. This is
|
|
143
|
+
// the single answer to "which class defines this owner" — both the live
|
|
144
|
+
// reference-prototype lookup below and the exported static table read it, so a
|
|
145
|
+
// host drift is described in exactly one place.
|
|
146
|
+
const WORKSPACE_CLASS = { pkg: '@deepseek-ai/dsh-workspace', target: 'WorkspaceRegistry' };
|
|
147
|
+
const PROJECTION_CLASS = { pkg: '@deepseek-ai/dsh-session-projection-cache', target: 'SessionProjectionCache' };
|
|
148
|
+
// `sessions.*` members are host-private members, but they are still declared on
|
|
149
|
+
// an official class: `@deepseek-ai/dsh-session` is a peer this plugin declares,
|
|
150
|
+
// and `SessionStore` is where `flush`/`liveEntryFor`/`detachEntered`/`enter`/
|
|
151
|
+
// `announce` live. Without this entry the doctor could not see the two
|
|
152
|
+
// capabilities the delete route depends on.
|
|
153
|
+
const SESSIONS_CLASS = { pkg: '@deepseek-ai/dsh-session', target: 'SessionStore' };
|
|
154
|
+
const OWNER_CLASSES = {
|
|
155
|
+
workspace: WORKSPACE_CLASS,
|
|
156
|
+
projectionCache: PROJECTION_CLASS,
|
|
157
|
+
sessions: SESSIONS_CLASS,
|
|
158
|
+
};
|
|
159
|
+
/** The named export's prototype, or undefined when the package is absent/trimmed. */
|
|
160
|
+
function classPrototypeOf(ref) {
|
|
145
161
|
try {
|
|
146
|
-
|
|
147
|
-
const
|
|
148
|
-
|
|
149
|
-
out.workspace = ws.WorkspaceRegistry.prototype;
|
|
162
|
+
const mod = createRequire(import.meta.url)(ref.pkg);
|
|
163
|
+
const klass = mod[ref.target];
|
|
164
|
+
return klass?.prototype;
|
|
150
165
|
}
|
|
151
|
-
catch {
|
|
166
|
+
catch {
|
|
167
|
+
return undefined; // optional in tests and in trimmed deployments
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
/**
|
|
171
|
+
* 哨兵值:不可能命中任何真实会话/工作区(会话 id 由宿主生成,不含此串)。
|
|
172
|
+
* 行为探针真调宿主方法时一律传它 —— 最坏情形是宿主受控拒绝(not found),
|
|
173
|
+
* 而「哨兵输入都能产生意外效果」本身就是要探出来的「不该路由信任」。
|
|
174
|
+
*/
|
|
175
|
+
const PROBE_SENTINEL = '__dshm_probe_never_exists__';
|
|
176
|
+
/**
|
|
177
|
+
* 用哨兵实参**真调**一个宿主方法(2026-09-19 分层探针第二层),验证「存在且可调用」:
|
|
178
|
+
* - 同步抛 `TypeError` → 实现坏了/签名漂移,返回失败详情(shape-mismatch);
|
|
179
|
+
* - 受控同步拒绝(普通 `Error`,如 not found)或返回值(含 rejected Promise)→ 通过。
|
|
180
|
+
*
|
|
181
|
+
* 只判同步段:探针框架是同步的(`assessHost` 不能 await),异步结果分类留待
|
|
182
|
+
* 异步化改造(见审查 07 跟进项)。返回的 Promise 一律挂 `.catch` 吞掉,探针
|
|
183
|
+
* 不制造 unhandled rejection。调用必须以 `target` 为 receiver —— 私有方法全靠
|
|
184
|
+
* `this.*` 拿内部状态,脱离 receiver 调用会把好实现误判成 TypeError。
|
|
185
|
+
*
|
|
186
|
+
* 副作用口径(按官方源码逐个核过):liveEntryFor/announce/flush 哨兵输入在
|
|
187
|
+
* 查表处受控抛错,零副作用;detachEntered 传普通对象在未知 id 处早退;
|
|
188
|
+
* deleteSession/archiveSession 等原生入口对未知 id 是读路径校验后受控拒绝。
|
|
189
|
+
* `enter` 不在此列(会往 store 里写哨兵条目),它只做存在性 + 文本比对。
|
|
190
|
+
*/
|
|
191
|
+
function callableWithSentinel(target, name, ...args) {
|
|
152
192
|
try {
|
|
153
|
-
const
|
|
154
|
-
if (
|
|
155
|
-
|
|
193
|
+
const method = target[name];
|
|
194
|
+
if (!isFn(method))
|
|
195
|
+
return `${name} 不是函数(methods 检查应已拦下,此处兜底)`;
|
|
196
|
+
const returned = method.call(target, ...args);
|
|
197
|
+
if (returned !== null && typeof returned === 'object' && isFn(returned.then)) {
|
|
198
|
+
const settled = returned;
|
|
199
|
+
if (isFn(settled.catch))
|
|
200
|
+
settled.catch(() => { });
|
|
201
|
+
}
|
|
202
|
+
return undefined;
|
|
203
|
+
}
|
|
204
|
+
catch (error) {
|
|
205
|
+
if (error instanceof TypeError)
|
|
206
|
+
return `${name} 哨兵真调抛 TypeError:${String(error?.message ?? error)}`;
|
|
207
|
+
return undefined;
|
|
156
208
|
}
|
|
157
|
-
|
|
209
|
+
}
|
|
210
|
+
/** Official prototypes this plugin's adapters mirror, when importable. */
|
|
211
|
+
function referencePrototypes() {
|
|
212
|
+
const out = {};
|
|
213
|
+
const workspace = classPrototypeOf(WORKSPACE_CLASS);
|
|
214
|
+
if (workspace !== undefined)
|
|
215
|
+
out.workspace = workspace;
|
|
216
|
+
const cache = classPrototypeOf(PROJECTION_CLASS);
|
|
217
|
+
if (cache !== undefined)
|
|
218
|
+
out.cache = cache;
|
|
219
|
+
// sessions 的五个私有方法全声明在官方 SessionStore.prototype 上(本插件 peer 依赖
|
|
220
|
+
// 同包同版本),与 workspace/cache 同机制接入文本比对 —— 此前该域只有存在性检查。
|
|
221
|
+
const sessions = classPrototypeOf(SESSIONS_CLASS);
|
|
222
|
+
if (sessions !== undefined)
|
|
223
|
+
out.sessions = sessions;
|
|
158
224
|
return out;
|
|
159
225
|
}
|
|
160
226
|
/**
|
|
@@ -170,7 +236,7 @@ const CAPABILITY_SPECS = [
|
|
|
170
236
|
label: '读取工作区状态',
|
|
171
237
|
kind: 'read',
|
|
172
238
|
owner: 'workspace',
|
|
173
|
-
fallback: '
|
|
239
|
+
fallback: 'refuse-operation',
|
|
174
240
|
methods: ['requireState'],
|
|
175
241
|
probe: (t) => {
|
|
176
242
|
try {
|
|
@@ -194,7 +260,7 @@ const CAPABILITY_SPECS = [
|
|
|
194
260
|
label: '读取工作区表',
|
|
195
261
|
kind: 'read',
|
|
196
262
|
owner: 'workspace',
|
|
197
|
-
fallback: '
|
|
263
|
+
fallback: 'refuse-operation',
|
|
198
264
|
methods: ['requireTable'],
|
|
199
265
|
probe: (t) => {
|
|
200
266
|
try {
|
|
@@ -213,7 +279,7 @@ const CAPABILITY_SPECS = [
|
|
|
213
279
|
label: '工作区索引结构',
|
|
214
280
|
kind: 'read',
|
|
215
281
|
owner: 'workspace',
|
|
216
|
-
fallback: '
|
|
282
|
+
fallback: 'refuse-operation',
|
|
217
283
|
fields: [
|
|
218
284
|
{ name: 'headers', instanceOf: 'Map' },
|
|
219
285
|
{ name: 'sessionPaths', instanceOf: 'Map' },
|
|
@@ -226,7 +292,7 @@ const CAPABILITY_SPECS = [
|
|
|
226
292
|
label: '读取会话头部',
|
|
227
293
|
kind: 'read',
|
|
228
294
|
owner: 'workspace',
|
|
229
|
-
fallback: '
|
|
295
|
+
fallback: 'refuse-operation',
|
|
230
296
|
methods: ['readSessionHeader'],
|
|
231
297
|
},
|
|
232
298
|
// ---- workspace registry: write side ------------------------------------
|
|
@@ -262,6 +328,9 @@ const CAPABILITY_SPECS = [
|
|
|
262
328
|
fallback: 'native-entry',
|
|
263
329
|
methods: ['archiveSession'],
|
|
264
330
|
optional: true,
|
|
331
|
+
// 分层探针第二层(哨兵真调):官方实现对未知 id 走读路径校验后受控拒绝
|
|
332
|
+
//(WorkspaceUnknownSessionError);同步 TypeError 才是「不该路由信任」的信号。
|
|
333
|
+
probe: (t) => callableWithSentinel(t, 'archiveSession', PROBE_SENTINEL),
|
|
265
334
|
},
|
|
266
335
|
{
|
|
267
336
|
id: 'workspace.unarchive-native',
|
|
@@ -271,6 +340,7 @@ const CAPABILITY_SPECS = [
|
|
|
271
340
|
fallback: 'native-entry',
|
|
272
341
|
methods: ['unarchiveSession'],
|
|
273
342
|
optional: true,
|
|
343
|
+
probe: (t) => callableWithSentinel(t, 'unarchiveSession', PROBE_SENTINEL),
|
|
274
344
|
},
|
|
275
345
|
{
|
|
276
346
|
id: 'workspace.batch-native',
|
|
@@ -280,6 +350,7 @@ const CAPABILITY_SPECS = [
|
|
|
280
350
|
fallback: 'native-entry',
|
|
281
351
|
methods: ['archiveWorkspaceSessions'],
|
|
282
352
|
optional: true,
|
|
353
|
+
probe: (t) => callableWithSentinel(t, 'archiveWorkspaceSessions', [PROBE_SENTINEL]),
|
|
283
354
|
},
|
|
284
355
|
{
|
|
285
356
|
id: 'workspace.delete-native',
|
|
@@ -292,6 +363,10 @@ const CAPABILITY_SPECS = [
|
|
|
292
363
|
fallback: 'native-entry',
|
|
293
364
|
methods: ['deleteSession'],
|
|
294
365
|
optional: true,
|
|
366
|
+
// 删除路由的 native 判定此前只看方法存在(07 审查五档问题 2):宿主升级把同名
|
|
367
|
+
// 方法换成别的签名,路由仍会走 native 裸调。哨兵真调把口径提到「可调用」,
|
|
368
|
+
// 文本比对收紧(见 inspectCapability 的 delete 类分支)负责「实现漂移」那一层。
|
|
369
|
+
probe: (t) => callableWithSentinel(t, 'deleteSession', PROBE_SENTINEL),
|
|
295
370
|
},
|
|
296
371
|
// ---- sessions runtime (private members, used only on the live branch) ---
|
|
297
372
|
{
|
|
@@ -301,6 +376,11 @@ const CAPABILITY_SPECS = [
|
|
|
301
376
|
owner: 'sessions',
|
|
302
377
|
fallback: 'disable-destructive',
|
|
303
378
|
methods: ['flush', 'liveEntryFor', 'detachEntered'],
|
|
379
|
+
// 零副作用真调(按官方源码核对):liveEntryFor/flush 对哨兵在查表处受控抛错;
|
|
380
|
+
// detachEntered 传普通对象在未知 id 处早退(entry 形状 {id} 即可)。
|
|
381
|
+
probe: (t) => callableWithSentinel(t, 'liveEntryFor', PROBE_SENTINEL)
|
|
382
|
+
?? callableWithSentinel(t, 'flush', PROBE_SENTINEL)
|
|
383
|
+
?? callableWithSentinel(t, 'detachEntered', { id: PROBE_SENTINEL }),
|
|
304
384
|
},
|
|
305
385
|
{
|
|
306
386
|
id: 'sessions.cold-announce',
|
|
@@ -309,6 +389,9 @@ const CAPABILITY_SPECS = [
|
|
|
309
389
|
owner: 'sessions',
|
|
310
390
|
fallback: 'disable-destructive',
|
|
311
391
|
methods: ['enter', 'announce'],
|
|
392
|
+
// announce 哨兵真调:内部先 liveEntryFor 查表,哨兵输入在查表处受控抛错。
|
|
393
|
+
// **enter 不真调**(官方实现对任意输入都会往 store 写入条目),它只有存在性 + 文本比对。
|
|
394
|
+
probe: (t) => callableWithSentinel(t, 'announce', PROBE_SENTINEL),
|
|
312
395
|
},
|
|
313
396
|
// ---- projection cache ---------------------------------------------------
|
|
314
397
|
{
|
|
@@ -324,7 +407,7 @@ const CAPABILITY_SPECS = [
|
|
|
324
407
|
label: '投影缓存删除屏障',
|
|
325
408
|
kind: 'delete',
|
|
326
409
|
owner: 'projectionCache',
|
|
327
|
-
// Absent on rc.2: `
|
|
410
|
+
// Absent on rc.2: `sessions/bridge.ts` installs a checked write barrier
|
|
328
411
|
// instead, so absence is a routing fact, not a failure. Only a cache whose
|
|
329
412
|
// write path cannot be wrapped at all is a real problem.
|
|
330
413
|
fallback: 'native-entry',
|
|
@@ -365,7 +448,7 @@ export const OPERATION_ROUTES = {
|
|
|
365
448
|
native: ['workspace.delete-native'],
|
|
366
449
|
// NOTE: `projection.delete-native` is deliberately NOT a route requirement.
|
|
367
450
|
// A host cache without its own delete barrier is expected on rc.2; the
|
|
368
|
-
// bridge wraps it (`workspace.js` / `
|
|
451
|
+
// bridge wraps it (`workspace.js` / `sessions/bridge.ts`) and reports a
|
|
369
452
|
// refusal itself when even that is impossible. Requiring the native barrier
|
|
370
453
|
// here would disable deletion on exactly the host this plugin was verified
|
|
371
454
|
// against.
|
|
@@ -437,6 +520,21 @@ function inspectCapability(spec, target, reference) {
|
|
|
437
520
|
return { ...base, state: 'shape-mismatch', detail: failure, missing: [], ...(textMatch === undefined ? {} : { textMatch }) };
|
|
438
521
|
}
|
|
439
522
|
}
|
|
523
|
+
// 删除类收紧(2026-09-19,07 审查五档问题 2):textMatch === false 意味着宿主运行的
|
|
524
|
+
// 不是本插件适配并验证过的实现(参考副本与宿主同源时恒真 —— junction 同物理文件;
|
|
525
|
+
// 只有宿主升级/漂移才会 false)。读/写类维持「按能力使用」的宽口径,但删除不可逆:
|
|
526
|
+
// 漂移的 flush/detachEntered/announce/deleteSession 一律不盲调,路由降级 adapter 或
|
|
527
|
+
// 拒绝,恢复文案引导更新插件。bridge.js 曾因无害重构误报而移除过文本比对 —— 本次
|
|
528
|
+
// 只收紧 delete 类,且参考副本不可解析时 textMatch 为 undefined,不拦截(优雅回退)。
|
|
529
|
+
if (textMatch === false && spec.kind === 'delete') {
|
|
530
|
+
return {
|
|
531
|
+
...base,
|
|
532
|
+
state: 'shape-mismatch',
|
|
533
|
+
detail: '成员齐备但实现文本与本插件适配的版本不同:删除类能力不盲调漂移实现',
|
|
534
|
+
missing: [],
|
|
535
|
+
textMatch,
|
|
536
|
+
};
|
|
537
|
+
}
|
|
440
538
|
return {
|
|
441
539
|
...base,
|
|
442
540
|
state: 'ok',
|
|
@@ -458,6 +556,31 @@ function inspectCapability(spec, target, reference) {
|
|
|
458
556
|
export const SUBSTITUTED_CAPABILITIES = CAPABILITY_SPECS
|
|
459
557
|
.filter((spec) => spec.optional === true)
|
|
460
558
|
.map((spec) => spec.id);
|
|
559
|
+
/**
|
|
560
|
+
* The capability table as a *static* view: for each spec, the official class
|
|
561
|
+
* whose prototype must carry the required members. This exists so a CLI without
|
|
562
|
+
* a running host (the doctor) can still answer "can this build archive / delete /
|
|
563
|
+
* list?".
|
|
564
|
+
*
|
|
565
|
+
* Derived from {@link CAPABILITY_SPECS} — the previous hand-written second table
|
|
566
|
+
* drifted silently: it carried 9 of the 15 ids and missed every `sessions.*` and
|
|
567
|
+
* native-slot capability the routing actually needs.
|
|
568
|
+
*/
|
|
569
|
+
export const CAPABILITY_STATIC = CAPABILITY_SPECS.map((spec) => {
|
|
570
|
+
const ref = OWNER_CLASSES[spec.owner];
|
|
571
|
+
const methods = spec.methods;
|
|
572
|
+
return {
|
|
573
|
+
id: spec.id,
|
|
574
|
+
label: spec.label,
|
|
575
|
+
kind: spec.kind,
|
|
576
|
+
optional: spec.optional === true,
|
|
577
|
+
// A `fields`-only spec (instance Maps) and a spec whose judgement lives in
|
|
578
|
+
// its `probe` have nothing a prototype can answer — they stay runtime-only.
|
|
579
|
+
check: ref !== undefined && methods !== undefined && methods.length > 0
|
|
580
|
+
? { pkg: ref.pkg, target: ref.target, members: methods }
|
|
581
|
+
: null,
|
|
582
|
+
};
|
|
583
|
+
});
|
|
461
584
|
/**
|
|
462
585
|
* Inspect the live host behind one plugin context.
|
|
463
586
|
*
|
|
@@ -478,6 +601,9 @@ export function assessHost(ctx) {
|
|
|
478
601
|
}
|
|
479
602
|
};
|
|
480
603
|
// Cordis exposes traceable proxies; compare and inspect the original objects.
|
|
604
|
+
// `Symbol.for('cordis.original')` 与 cordis 导出的 `symbols.original` 是**同一个符号**
|
|
605
|
+
// (该包内即 `original: Symbol.for("cordis.original")`)。这里不 import cordis,是为了让
|
|
606
|
+
// 兼容探测在宿主包加载失败时仍能工作 —— 本文件对宿主零硬依赖(见文件头的 createRequire)。
|
|
481
607
|
const unwrap = (value) => {
|
|
482
608
|
if (value === null || typeof value !== 'object')
|
|
483
609
|
return value;
|
|
@@ -496,7 +622,7 @@ export function assessHost(ctx) {
|
|
|
496
622
|
const targets = {
|
|
497
623
|
workspace: { target: registry, reference: references.workspace },
|
|
498
624
|
projectionCache: { target: cache, reference: references.cache },
|
|
499
|
-
sessions: { target: sessions, reference:
|
|
625
|
+
sessions: { target: sessions, reference: references.sessions },
|
|
500
626
|
persistence: { target: unwrap(get('sessionPersistence')), reference: undefined },
|
|
501
627
|
};
|
|
502
628
|
const findings = CAPABILITY_SPECS.map((spec) => {
|
|
@@ -507,6 +633,7 @@ export function assessHost(ctx) {
|
|
|
507
633
|
const modules = {};
|
|
508
634
|
const sameAsHost = {};
|
|
509
635
|
const blockers = [];
|
|
636
|
+
const unverified = [];
|
|
510
637
|
const hostRoot = hostPackageRoot();
|
|
511
638
|
for (const name of IDENTITY_PACKAGES) {
|
|
512
639
|
const resolved = safeResolve(name);
|
|
@@ -518,6 +645,7 @@ export function assessHost(ctx) {
|
|
|
518
645
|
}
|
|
519
646
|
if (hostRoot === null) {
|
|
520
647
|
sameAsHost[name] = null;
|
|
648
|
+
unverified.push(name);
|
|
521
649
|
continue;
|
|
522
650
|
}
|
|
523
651
|
// Resolve the same name from the host installation's own anchor, then
|
|
@@ -534,6 +662,10 @@ export function assessHost(ctx) {
|
|
|
534
662
|
sameAsHost[name] = same;
|
|
535
663
|
if (same === false)
|
|
536
664
|
blockers.push(`${name}:插件与宿主加载的是两份不同拷贝(运行 node scripts/host-deps.mjs --fix)`);
|
|
665
|
+
// 解析得到、却比不了:宿主锚点里找不到它。不能静默 —— 否则整块身份校验等于没做,
|
|
666
|
+
// 而页头仍报「全部可用」(pnpm 的 .pnpm 隔离目录就是这种情形,见 hostPackageRoot)。
|
|
667
|
+
if (same === null)
|
|
668
|
+
unverified.push(name);
|
|
537
669
|
}
|
|
538
670
|
// Degraded = something is genuinely unavailable. Optional slots are excluded:
|
|
539
671
|
// their absence selects the adapter route and leaves the feature fully
|
|
@@ -552,6 +684,7 @@ export function assessHost(ctx) {
|
|
|
552
684
|
modules,
|
|
553
685
|
sameAsHost,
|
|
554
686
|
blockers,
|
|
687
|
+
unverified,
|
|
555
688
|
},
|
|
556
689
|
findings,
|
|
557
690
|
degraded,
|
|
@@ -559,11 +692,6 @@ export function assessHost(ctx) {
|
|
|
559
692
|
generatedAt: Date.now(),
|
|
560
693
|
};
|
|
561
694
|
}
|
|
562
|
-
/** True when two paths denote the same file; falls back to string equality. */
|
|
563
|
-
function samePath(a, b) {
|
|
564
|
-
const normalize = (value) => value.replace(/\\/g, '/').toLowerCase();
|
|
565
|
-
return normalize(realPathOf(a) ?? a) === normalize(realPathOf(b) ?? b);
|
|
566
|
-
}
|
|
567
695
|
/**
|
|
568
696
|
* The `@deepseek-ai` directory of the DSH installation this plugin is attached
|
|
569
697
|
* to, derived from where a shared package physically lives.
|
|
@@ -571,6 +699,11 @@ function samePath(a, b) {
|
|
|
571
699
|
* Anchoring on the resolved entry rather than on `require.resolve('@deepseek-ai/dsh')`
|
|
572
700
|
* matters: the plugin never imports the `dsh` app package, so it need not be
|
|
573
701
|
* resolvable from the plugin at all — only the shared libraries are.
|
|
702
|
+
*
|
|
703
|
+
* 注意 pnpm:这里在 `.pnpm/<name>@<ver>/node_modules/` 布局下会返回**该包的隔离目录**
|
|
704
|
+
* (那也是一个 `node_modules/@deepseek-ai`)。它本身不是问题 —— node 的解析会继续向上走到
|
|
705
|
+
* 顶层 `node_modules`,所以能解析出的包集合与顶层锚点相同(实测确认)。真正的风险在
|
|
706
|
+
* 调用方:从锚点解析不到的包会被判成 `same = null`,见 assessHost 里的 `unverified`。
|
|
574
707
|
*/
|
|
575
708
|
function hostPackageRoot() {
|
|
576
709
|
for (const anchor of IDENTITY_PACKAGES) {
|