dsh-plugin-tool-management 0.10.0 → 0.12.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 +100 -1
- package/README.md +67 -50
- package/README_EN.md +61 -38
- package/cordis.patch.yml +10 -1
- 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 +132 -12
- package/lib/client.js +2826 -547
- package/lib/compat/patch-dialect.js +173 -0
- package/lib/compat/preset-reach.js +1 -10
- package/lib/compat/probe.js +205 -24
- package/lib/compat/runtime-notes.js +25 -0
- package/lib/context-inject.js +83 -9
- package/lib/host-names.js +12 -0
- package/lib/http-fence.js +35 -15
- package/lib/hub.js +28 -2
- package/lib/imports/parsers.js +15 -9
- package/lib/imports/upload.js +43 -4
- package/lib/index.js +927 -3897
- package/lib/mcp/loader-token.js +238 -0
- package/lib/mcp/manager.js +1769 -0
- package/lib/mcp/override-blocks.js +10 -3
- package/lib/mcp/patch-yaml.js +351 -0
- package/lib/mcp/secret-guard.js +145 -0
- package/lib/{rules → memories}/archive-engine.js +1 -1
- package/lib/{rules → memories}/archive.js +1 -1
- 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 +226 -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 +124 -36
- package/lib/sessions/history.js +323 -0
- package/lib/{history → sessions}/tombstone.js +1 -1
- package/lib/{history → sessions}/workspace.js +151 -50
- package/lib/skills/core.js +74 -39
- package/lib/skills/readonly-discovery.js +4 -1
- package/lib/skills/service.js +98 -13
- package/lib/subagents/service.js +199 -55
- 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 +13 -10
- package/lib/agents-md/preset-id.js +0 -49
- package/lib/rules/service.js +0 -3078
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
// 官方补丁方言(`cordis.patch.yml`)的**只读复刻** —— 写入宿主配置前的最后一道校验。
|
|
2
|
+
//
|
|
3
|
+
// 为什么需要它:本插件 19 个调用面都在**文本级**改宿主的补丁文件(插块 / 删块 / 换行),
|
|
4
|
+
// 而官方解析这份文件用的是 js-yaml + 自定义 `!!js` 标签(`dsh-app-boot/lib/index.js:17-31`),
|
|
5
|
+
// 并且把「顶层必须是数组」「每项必须是映射」写成显式断言(同文件 `parsePatchList`,:1196-1200)。
|
|
6
|
+
// 解析失败 = DSH **下次启动直接起不来**(`CHANGELOG.md` 里记过这次事故)。三条写入路径
|
|
7
|
+
// (`profiles/<名>/cordis.patch.yml`、`~/.dsh/cordis.patch.yml`、bundle 的 `cordis.patch.yml`)
|
|
8
|
+
// 走的是同一份方言(前者 `loadOverlayPatches`、后者 `loadOptionalPatches` → `parsePatchList`;
|
|
9
|
+
// `userPatchesSchema === entryListSchema`,实测于 dsh-app-boot 0.1.5-rc.2)。
|
|
10
|
+
//
|
|
11
|
+
// 官方**没有**导出这份 schema(导出清单见同文件 :1575,无 `entryListSchema`),所以只能复刻。
|
|
12
|
+
// 复刻**必然会过期**(官方加方言 / 换 js-yaml 大版本),因此判定策略是**非对称**的:
|
|
13
|
+
// · 改前能解析、改后被我们改成不能解析 → 是我们的改动弄坏了它 → 拒绝写入(原文件与备份不动);
|
|
14
|
+
// · 改前本来就解析不过 → 说明复刻过期,不是我们的错 → 照写 + 上报,绝不拦;
|
|
15
|
+
// · 依赖(js-yaml)不在 → 跳过校验 + 上报。
|
|
16
|
+
// 一律拒绝会把「校验器过期」变成「MCP 写路径整体停摆」(含重启恢复写,见 request-gate.ts
|
|
17
|
+
// 里那处「恢复写失败会把服务器永久留在停用态」),那是拿能力换保守。
|
|
18
|
+
//
|
|
19
|
+
// `judgePatchText` / `decidePatchWrite` 是纯函数,契约测试直接钉上面几条分支;IO 只有
|
|
20
|
+
// `checkPatchWrite` 一处,它同时把结论记进运行时上报通道(兼容页据此出一行)与回执队列。
|
|
21
|
+
import { clearRuntimeNote, noteRuntime } from './runtime-notes.js';
|
|
22
|
+
/** 官方 `.js` 标签名(逐字取自 dsh-app-boot/lib/index.js:17)。 */
|
|
23
|
+
const JS_EXPR_TAG = 'tag:yaml.org,2002:js';
|
|
24
|
+
/** 官方 `isJsExpr`(cordis-plugin-loader/lib/index.js:302-304)的等价物。 */
|
|
25
|
+
const isJsExpr = (value) => value instanceof Object && '__jsExpr' in value;
|
|
26
|
+
function messageOf(error) {
|
|
27
|
+
return error instanceof Error ? error.message : String(error);
|
|
28
|
+
}
|
|
29
|
+
// 同一份 yaml 模块只 extend 一次(官方是在模块顶层 extend 一次的等价位)。
|
|
30
|
+
const schemas = new WeakMap();
|
|
31
|
+
function schemaOf(yaml) {
|
|
32
|
+
const cached = schemas.get(yaml);
|
|
33
|
+
if (cached !== undefined)
|
|
34
|
+
return cached;
|
|
35
|
+
// 与官方逐字等价的选项对象(dsh-app-boot/lib/index.js:17-23):load 只用到 resolve /
|
|
36
|
+
// construct;predicate / represent 属于 dump 方向,复刻它们是让这份定义与官方对齐。
|
|
37
|
+
const exprType = new yaml.Type(JS_EXPR_TAG, {
|
|
38
|
+
kind: 'scalar',
|
|
39
|
+
resolve: (data) => typeof data === 'string',
|
|
40
|
+
construct: (data) => ({ __jsExpr: data }),
|
|
41
|
+
predicate: isJsExpr,
|
|
42
|
+
represent: (data) => data.__jsExpr,
|
|
43
|
+
});
|
|
44
|
+
const schema = yaml.JSON_SCHEMA.extend(exprType);
|
|
45
|
+
schemas.set(yaml, schema);
|
|
46
|
+
return schema;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* 判一份补丁文本官方解析得动吗(纯函数:不碰 IO、不记状态)。
|
|
50
|
+
*
|
|
51
|
+
* @param yaml - 注入的 yaml 模块(测试可传真模块;运行时是惰性 import 来的那份)。
|
|
52
|
+
* @param content - 补丁文件全文。
|
|
53
|
+
* @returns 判定结果;`unparseable` 的 detail 带官方会抛的那句错,直接可展示。
|
|
54
|
+
*/
|
|
55
|
+
export function judgePatchText(yaml, content) {
|
|
56
|
+
let parsed;
|
|
57
|
+
try {
|
|
58
|
+
parsed = yaml.load(content, { schema: schemaOf(yaml) });
|
|
59
|
+
}
|
|
60
|
+
catch (error) {
|
|
61
|
+
return { status: 'unparseable', detail: messageOf(error) };
|
|
62
|
+
}
|
|
63
|
+
// 官方 parsePatchList 的两条显式断言:顶层数组(:1199)、每项是映射(:1200)。
|
|
64
|
+
// 空文件(含只有空白的文件)在这里是 undefined → 判「解析不过」—— 与官方一致:
|
|
65
|
+
// 文件**不存在**官方容忍(`ENOENT` → 没有这一层),文件**在但空**官方是抛错的。
|
|
66
|
+
if (!Array.isArray(parsed)) {
|
|
67
|
+
return { status: 'unparseable', detail: '顶层不是 YAML 数组(官方 parsePatchList 的显式断言)' };
|
|
68
|
+
}
|
|
69
|
+
const bad = parsed.findIndex((entry) => typeof entry !== 'object' || entry === null || Array.isArray(entry));
|
|
70
|
+
if (bad >= 0) {
|
|
71
|
+
return { status: 'unparseable', detail: `第 ${bad + 1} 项不是映射(官方 parsePatchList 的显式断言)` };
|
|
72
|
+
}
|
|
73
|
+
return { status: 'ok' };
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* 非对称策略本体(纯函数)。三个分支就是本模块存在的理由,契约测试逐个钉住。
|
|
77
|
+
*
|
|
78
|
+
* @param before - 改前内容的判定;`null` 表示**没有可用基线**(文件不存在或读不到内容)。
|
|
79
|
+
* @param after - 本次要写入内容的判定。
|
|
80
|
+
* @returns 是否放行 + 要不要上报 + 拒绝时的说明。
|
|
81
|
+
*/
|
|
82
|
+
export function decidePatchWrite(before, after) {
|
|
83
|
+
if (after.status === 'ok')
|
|
84
|
+
return { allow: true, report: null };
|
|
85
|
+
if (after.status === 'no-dep')
|
|
86
|
+
return { allow: true, report: { kind: 'no-dep', detail: after.detail } };
|
|
87
|
+
// 改后解析不过:只有「本来就没有基线」或「改前是好的」才拦 —— 这两种都是我们引入的坏内容。
|
|
88
|
+
if (before !== null && before.status === 'unparseable') {
|
|
89
|
+
return {
|
|
90
|
+
allow: true,
|
|
91
|
+
report: {
|
|
92
|
+
kind: 'replica-outdated',
|
|
93
|
+
detail: `改前内容本来就解析不过(${before.detail}),本次写入按「复刻过期」放行`,
|
|
94
|
+
},
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
return {
|
|
98
|
+
allow: false,
|
|
99
|
+
report: null,
|
|
100
|
+
error: `写入被拒绝:新内容不是合法的补丁列表(${after.detail})。官方解析器会因此让 DSH 起不来,`
|
|
101
|
+
+ '已保留原文件与备份。',
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
// ---------- 运行时依赖:惰性加载 + 结果缓存(`undefined` 未试过 / `null` 加载不到) ----------
|
|
105
|
+
let dialect;
|
|
106
|
+
let depError = '';
|
|
107
|
+
async function loadDialect() {
|
|
108
|
+
if (dialect !== undefined)
|
|
109
|
+
return dialect;
|
|
110
|
+
try {
|
|
111
|
+
// 与宿主实装同一份 js-yaml(本机实测 4.3.2)。只在写补丁时加载:插件加载期不碰它,
|
|
112
|
+
// 装不上也绝不因此让插件挂掉。
|
|
113
|
+
dialect = (await import('js-yaml'));
|
|
114
|
+
}
|
|
115
|
+
catch (error) {
|
|
116
|
+
dialect = null;
|
|
117
|
+
depError = messageOf(error);
|
|
118
|
+
}
|
|
119
|
+
return dialect;
|
|
120
|
+
}
|
|
121
|
+
// ---------- 状态:兼容页那一行(走运行时上报通道)+ 写入回执的 warning(消费式) ----------
|
|
122
|
+
/** 回执队列状态:`pendingWarnings` 只在「本次请求的写入没做成交验」时有货。 */
|
|
123
|
+
const pendingWarnings = [];
|
|
124
|
+
// 正常情况下每请求都会被取空;上限只是防止某个不取回的调用路径把它撑大。
|
|
125
|
+
const WARN_QUEUE_MAX = 8;
|
|
126
|
+
function record(report) {
|
|
127
|
+
if (report === null) {
|
|
128
|
+
clearRuntimeNote('patch-write-guard');
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
noteRuntime({
|
|
132
|
+
id: 'patch-write-guard',
|
|
133
|
+
label: '补丁写入校验',
|
|
134
|
+
kind: 'write',
|
|
135
|
+
fallback: 'inform-only',
|
|
136
|
+
detail: (report.kind === 'no-dep' ? '校验依赖不可用' : '复刻可能已过期')
|
|
137
|
+
+ `:${report.detail}(写入照常进行,仅少一道「把启动配置写坏」的拦截)`,
|
|
138
|
+
});
|
|
139
|
+
pendingWarnings.push(report.kind === 'no-dep'
|
|
140
|
+
? '补丁写入未做解析校验(依赖不可用):' + report.detail
|
|
141
|
+
: '补丁写入的解析校验放行(可能是复刻过期):' + report.detail);
|
|
142
|
+
if (pendingWarnings.length > WARN_QUEUE_MAX)
|
|
143
|
+
pendingWarnings.splice(0, pendingWarnings.length - WARN_QUEUE_MAX);
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* 写入前的完整判定(唯一一处 IO/依赖入口)。
|
|
147
|
+
*
|
|
148
|
+
* 判完把结论记进 note / 回执队列:兼容页据此出「补丁校验不可用」一行,写入回执据此带 warning。
|
|
149
|
+
*
|
|
150
|
+
* @param previous - 改前全文(`''` 视为没有基线:文件不存在,或读不到内容)。
|
|
151
|
+
* @param next - 本次要写入的全文。
|
|
152
|
+
* @returns 判定;`allow=false` 时调用方必须中止写入。
|
|
153
|
+
*/
|
|
154
|
+
export async function checkPatchWrite(previous, next) {
|
|
155
|
+
const yaml = await loadDialect();
|
|
156
|
+
if (yaml === null) {
|
|
157
|
+
const decision = {
|
|
158
|
+
allow: true,
|
|
159
|
+
report: { kind: 'no-dep', detail: `无法加载 js-yaml:${depError}` },
|
|
160
|
+
};
|
|
161
|
+
record(decision.report);
|
|
162
|
+
return decision;
|
|
163
|
+
}
|
|
164
|
+
const before = previous === '' ? null : judgePatchText(yaml, previous);
|
|
165
|
+
const after = judgePatchText(yaml, next);
|
|
166
|
+
const decision = decidePatchWrite(before, after);
|
|
167
|
+
record(decision.report);
|
|
168
|
+
return decision;
|
|
169
|
+
}
|
|
170
|
+
/** 写入回执取用:取走并清空本次累积的 warning(每次请求取一次)。 */
|
|
171
|
+
export function takePatchGuardWarnings() {
|
|
172
|
+
return pendingWarnings.splice(0, pendingWarnings.length);
|
|
173
|
+
}
|
|
@@ -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
|
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
* leave the rest working.
|
|
31
31
|
*/
|
|
32
32
|
import { createRequire } from 'node:module';
|
|
33
|
-
import { readFileSync, realpathSync } from 'node:fs';
|
|
33
|
+
import { existsSync, readFileSync, realpathSync } from 'node:fs';
|
|
34
34
|
import { dirname, join } from 'node:path';
|
|
35
35
|
/** Packages whose physical module identity matters to this plugin. */
|
|
36
36
|
export const IDENTITY_PACKAGES = [
|
|
@@ -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
|
{
|
|
@@ -319,12 +402,37 @@ const CAPABILITY_SPECS = [
|
|
|
319
402
|
fallback: 'disable-destructive',
|
|
320
403
|
methods: ['write', 'put', 'requireTable'],
|
|
321
404
|
},
|
|
405
|
+
{
|
|
406
|
+
// B2:此前它只是 bridge 运行时那句拒绝里的**临时 id**(`acquireCacheGuard` 里现场拼的),
|
|
407
|
+
// 于是客户端压根不知道它 —— 宿主表不可删时,"路由说可用、点下去必拒"(V13)。提升为
|
|
408
|
+
// 能力表的正式条目后,路由判定与运行时前提同一份依据。
|
|
409
|
+
id: 'projection.table-delete',
|
|
410
|
+
label: '投影缓存行删除',
|
|
411
|
+
kind: 'delete',
|
|
412
|
+
owner: 'projectionCache',
|
|
413
|
+
fallback: 'disable-destructive',
|
|
414
|
+
// 探测内容就是运行时那句硬前提:`requireTable().delete` 在不在。
|
|
415
|
+
probe: (target) => {
|
|
416
|
+
if (typeof target?.requireTable !== 'function')
|
|
417
|
+
return '宿主投影缓存缺少 requireTable';
|
|
418
|
+
let table;
|
|
419
|
+
try {
|
|
420
|
+
table = target.requireTable();
|
|
421
|
+
}
|
|
422
|
+
catch (error) {
|
|
423
|
+
return `宿主投影缓存 requireTable() 抛错:${String(error)}`;
|
|
424
|
+
}
|
|
425
|
+
if (typeof table?.delete !== 'function')
|
|
426
|
+
return '宿主投影缓存存储不支持安全删除(table.delete 缺失)';
|
|
427
|
+
return undefined;
|
|
428
|
+
},
|
|
429
|
+
},
|
|
322
430
|
{
|
|
323
431
|
id: 'projection.delete-native',
|
|
324
432
|
label: '投影缓存删除屏障',
|
|
325
433
|
kind: 'delete',
|
|
326
434
|
owner: 'projectionCache',
|
|
327
|
-
// Absent on rc.2: `
|
|
435
|
+
// Absent on rc.2: `sessions/bridge.ts` installs a checked write barrier
|
|
328
436
|
// instead, so absence is a routing fact, not a failure. Only a cache whose
|
|
329
437
|
// write path cannot be wrapped at all is a real problem.
|
|
330
438
|
fallback: 'native-entry',
|
|
@@ -365,13 +473,17 @@ export const OPERATION_ROUTES = {
|
|
|
365
473
|
native: ['workspace.delete-native'],
|
|
366
474
|
// NOTE: `projection.delete-native` is deliberately NOT a route requirement.
|
|
367
475
|
// A host cache without its own delete barrier is expected on rc.2; the
|
|
368
|
-
// bridge wraps it (`workspace.js` / `
|
|
476
|
+
// bridge wraps it (`workspace.js` / `sessions/bridge.ts`) and reports a
|
|
369
477
|
// refusal itself when even that is impossible. Requiring the native barrier
|
|
370
478
|
// here would disable deletion on exactly the host this plugin was verified
|
|
371
479
|
// against.
|
|
372
480
|
adapter: [
|
|
373
481
|
'workspace.enqueue', 'workspace.set-state', 'workspace.index-header',
|
|
374
482
|
'sessions.detach-live', 'sessions.cold-announce', 'projection.write',
|
|
483
|
+
// B2:运行时硬前提(`table.delete`)也算一条路由要求 —— 不算进来的话,宿主表不可删时
|
|
484
|
+
// 路由说可用、点下去必拒。batch 的 adapter 路由**有意**保持只有 enqueue/set-state:
|
|
485
|
+
// 它的逐条失败会在结果里按 sessionId 报出来,此处不放宽也不收紧。
|
|
486
|
+
'projection.table-delete',
|
|
375
487
|
],
|
|
376
488
|
},
|
|
377
489
|
list: { native: [], adapter: ['workspace.read-state', 'workspace.read-table', 'workspace.index-shape'] },
|
|
@@ -437,6 +549,21 @@ function inspectCapability(spec, target, reference) {
|
|
|
437
549
|
return { ...base, state: 'shape-mismatch', detail: failure, missing: [], ...(textMatch === undefined ? {} : { textMatch }) };
|
|
438
550
|
}
|
|
439
551
|
}
|
|
552
|
+
// 删除类收紧(2026-09-19,07 审查五档问题 2):textMatch === false 意味着宿主运行的
|
|
553
|
+
// 不是本插件适配并验证过的实现(参考副本与宿主同源时恒真 —— junction 同物理文件;
|
|
554
|
+
// 只有宿主升级/漂移才会 false)。读/写类维持「按能力使用」的宽口径,但删除不可逆:
|
|
555
|
+
// 漂移的 flush/detachEntered/announce/deleteSession 一律不盲调,路由降级 adapter 或
|
|
556
|
+
// 拒绝,恢复文案引导更新插件。bridge.js 曾因无害重构误报而移除过文本比对 —— 本次
|
|
557
|
+
// 只收紧 delete 类,且参考副本不可解析时 textMatch 为 undefined,不拦截(优雅回退)。
|
|
558
|
+
if (textMatch === false && spec.kind === 'delete') {
|
|
559
|
+
return {
|
|
560
|
+
...base,
|
|
561
|
+
state: 'shape-mismatch',
|
|
562
|
+
detail: '成员齐备但实现文本与本插件适配的版本不同:删除类能力不盲调漂移实现',
|
|
563
|
+
missing: [],
|
|
564
|
+
textMatch,
|
|
565
|
+
};
|
|
566
|
+
}
|
|
440
567
|
return {
|
|
441
568
|
...base,
|
|
442
569
|
state: 'ok',
|
|
@@ -458,6 +585,31 @@ function inspectCapability(spec, target, reference) {
|
|
|
458
585
|
export const SUBSTITUTED_CAPABILITIES = CAPABILITY_SPECS
|
|
459
586
|
.filter((spec) => spec.optional === true)
|
|
460
587
|
.map((spec) => spec.id);
|
|
588
|
+
/**
|
|
589
|
+
* The capability table as a *static* view: for each spec, the official class
|
|
590
|
+
* whose prototype must carry the required members. This exists so a CLI without
|
|
591
|
+
* a running host (the doctor) can still answer "can this build archive / delete /
|
|
592
|
+
* list?".
|
|
593
|
+
*
|
|
594
|
+
* Derived from {@link CAPABILITY_SPECS} — the previous hand-written second table
|
|
595
|
+
* drifted silently: it carried 9 of the 15 ids and missed every `sessions.*` and
|
|
596
|
+
* native-slot capability the routing actually needs.
|
|
597
|
+
*/
|
|
598
|
+
export const CAPABILITY_STATIC = CAPABILITY_SPECS.map((spec) => {
|
|
599
|
+
const ref = OWNER_CLASSES[spec.owner];
|
|
600
|
+
const methods = spec.methods;
|
|
601
|
+
return {
|
|
602
|
+
id: spec.id,
|
|
603
|
+
label: spec.label,
|
|
604
|
+
kind: spec.kind,
|
|
605
|
+
optional: spec.optional === true,
|
|
606
|
+
// A `fields`-only spec (instance Maps) and a spec whose judgement lives in
|
|
607
|
+
// its `probe` have nothing a prototype can answer — they stay runtime-only.
|
|
608
|
+
check: ref !== undefined && methods !== undefined && methods.length > 0
|
|
609
|
+
? { pkg: ref.pkg, target: ref.target, members: methods }
|
|
610
|
+
: null,
|
|
611
|
+
};
|
|
612
|
+
});
|
|
461
613
|
/**
|
|
462
614
|
* Inspect the live host behind one plugin context.
|
|
463
615
|
*
|
|
@@ -478,6 +630,9 @@ export function assessHost(ctx) {
|
|
|
478
630
|
}
|
|
479
631
|
};
|
|
480
632
|
// Cordis exposes traceable proxies; compare and inspect the original objects.
|
|
633
|
+
// `Symbol.for('cordis.original')` 与 cordis 导出的 `symbols.original` 是**同一个符号**
|
|
634
|
+
// (该包内即 `original: Symbol.for("cordis.original")`)。这里不 import cordis,是为了让
|
|
635
|
+
// 兼容探测在宿主包加载失败时仍能工作 —— 本文件对宿主零硬依赖(见文件头的 createRequire)。
|
|
481
636
|
const unwrap = (value) => {
|
|
482
637
|
if (value === null || typeof value !== 'object')
|
|
483
638
|
return value;
|
|
@@ -496,7 +651,7 @@ export function assessHost(ctx) {
|
|
|
496
651
|
const targets = {
|
|
497
652
|
workspace: { target: registry, reference: references.workspace },
|
|
498
653
|
projectionCache: { target: cache, reference: references.cache },
|
|
499
|
-
sessions: { target: sessions, reference:
|
|
654
|
+
sessions: { target: sessions, reference: references.sessions },
|
|
500
655
|
persistence: { target: unwrap(get('sessionPersistence')), reference: undefined },
|
|
501
656
|
};
|
|
502
657
|
const findings = CAPABILITY_SPECS.map((spec) => {
|
|
@@ -507,6 +662,7 @@ export function assessHost(ctx) {
|
|
|
507
662
|
const modules = {};
|
|
508
663
|
const sameAsHost = {};
|
|
509
664
|
const blockers = [];
|
|
665
|
+
const unverified = [];
|
|
510
666
|
const hostRoot = hostPackageRoot();
|
|
511
667
|
for (const name of IDENTITY_PACKAGES) {
|
|
512
668
|
const resolved = safeResolve(name);
|
|
@@ -518,6 +674,7 @@ export function assessHost(ctx) {
|
|
|
518
674
|
}
|
|
519
675
|
if (hostRoot === null) {
|
|
520
676
|
sameAsHost[name] = null;
|
|
677
|
+
unverified.push(name);
|
|
521
678
|
continue;
|
|
522
679
|
}
|
|
523
680
|
// Resolve the same name from the host installation's own anchor, then
|
|
@@ -534,6 +691,10 @@ export function assessHost(ctx) {
|
|
|
534
691
|
sameAsHost[name] = same;
|
|
535
692
|
if (same === false)
|
|
536
693
|
blockers.push(`${name}:插件与宿主加载的是两份不同拷贝(运行 node scripts/host-deps.mjs --fix)`);
|
|
694
|
+
// 解析得到、却比不了:宿主锚点里找不到它。不能静默 —— 否则整块身份校验等于没做,
|
|
695
|
+
// 而页头仍报「全部可用」(pnpm 的 .pnpm 隔离目录就是这种情形,见 hostPackageRoot)。
|
|
696
|
+
if (same === null)
|
|
697
|
+
unverified.push(name);
|
|
537
698
|
}
|
|
538
699
|
// Degraded = something is genuinely unavailable. Optional slots are excluded:
|
|
539
700
|
// their absence selects the adapter route and leaves the feature fully
|
|
@@ -552,6 +713,7 @@ export function assessHost(ctx) {
|
|
|
552
713
|
modules,
|
|
553
714
|
sameAsHost,
|
|
554
715
|
blockers,
|
|
716
|
+
unverified,
|
|
555
717
|
},
|
|
556
718
|
findings,
|
|
557
719
|
degraded,
|
|
@@ -566,22 +728,41 @@ export function assessHost(ctx) {
|
|
|
566
728
|
* Anchoring on the resolved entry rather than on `require.resolve('@deepseek-ai/dsh')`
|
|
567
729
|
* matters: the plugin never imports the `dsh` app package, so it need not be
|
|
568
730
|
* resolvable from the plugin at all — only the shared libraries are.
|
|
731
|
+
*
|
|
732
|
+
* 注意 pnpm:这里在 `.pnpm/<name>@<ver>/node_modules/` 布局下会返回**该包的隔离目录**
|
|
733
|
+
* (那也是一个 `node_modules/@deepseek-ai`)。它本身不是问题 —— node 的解析会继续向上走到
|
|
734
|
+
* 顶层 `node_modules`,所以能解析出的包集合与顶层锚点相同(实测确认)。真正的风险在
|
|
735
|
+
* 调用方:从锚点解析不到的包会被判成 `same = null`,见 assessHost 里的 `unverified`。
|
|
569
736
|
*/
|
|
570
737
|
function hostPackageRoot() {
|
|
738
|
+
// 与 doctor 的 `findHost` **同一套策略**(先看 `$DSH_HOME/profiles/node_modules/@deepseek-ai`,
|
|
739
|
+
// 再从插件自身位置逐级上溯,并确认那一层里真有 `dsh/package.json`)。此前运行时只按"插件
|
|
740
|
+
// 自己解析到的包"上溯,dev 布局下会把仓库里的副本当成宿主锚点、比出假的 `true` —— 于是
|
|
741
|
+
// 界面说 ok、doctor 说 SEPARATE COPY(V8)。两处口径分裂本身就是缺陷。
|
|
742
|
+
const home = process.env.DSH_HOME || join(process.env.USERPROFILE || process.env.HOME || '', '.dsh');
|
|
743
|
+
const candidates = [];
|
|
744
|
+
if (home !== '')
|
|
745
|
+
candidates.push(join(home, 'profiles', 'node_modules', '@deepseek-ai'));
|
|
571
746
|
for (const anchor of IDENTITY_PACKAGES) {
|
|
572
747
|
const resolved = realPathOf(safeResolve(anchor));
|
|
573
748
|
if (resolved === null)
|
|
574
749
|
continue;
|
|
575
750
|
let dir = dirname(resolved);
|
|
576
|
-
for (let i = 0; i <
|
|
577
|
-
if (dir.endsWith(join('node_modules', '@deepseek-ai')))
|
|
578
|
-
|
|
751
|
+
for (let i = 0; i < 4; i += 1) {
|
|
752
|
+
if (dir.endsWith(join('node_modules', '@deepseek-ai'))) {
|
|
753
|
+
candidates.push(dir);
|
|
754
|
+
break;
|
|
755
|
+
}
|
|
579
756
|
const parent = dirname(dir);
|
|
580
757
|
if (parent === dir)
|
|
581
758
|
break;
|
|
582
759
|
dir = parent;
|
|
583
760
|
}
|
|
584
761
|
}
|
|
762
|
+
for (const candidate of candidates) {
|
|
763
|
+
if (existsSync(join(candidate, 'dsh', 'package.json')))
|
|
764
|
+
return candidate;
|
|
765
|
+
}
|
|
585
766
|
return null;
|
|
586
767
|
}
|
|
587
768
|
/** Human-readable summary line for logs and the settings page header. */
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
// 运行时降级上报通道 —— 「插件自己发现自己降级了」的统一出口。
|
|
2
|
+
//
|
|
3
|
+
// 为什么需要它:有一批降级**没有抛错可挂**:它们发生在装配期或探测期(第三方包装接管了本插件的
|
|
4
|
+
// 运行时适配、注入域装配失败、启动期表达式或服务名解析出问题),失败当下的表现只是"某个功能
|
|
5
|
+
// 少了一半"或者"静默失效"。此前这些点只落 console / logger,用户在界面上看不到,要等事故之后
|
|
6
|
+
// 才倒查(审查报告 §3 记了多处)。这里给它们一个共用出口:`noteRuntime` 记一条,
|
|
7
|
+
// `compat-status` 把全部条目并进 findings —— 兼容页因此成为"降级总账"。
|
|
8
|
+
//
|
|
9
|
+
// 与 `probe.ts` 的 CAPABILITY_SPECS 分工:那张表是**写死的宿主能力清单**(每次探测重算),
|
|
10
|
+
// 这里是**运行时事件**(谁在什么时候发现了什么),两者在 compat-status 里合并展示。
|
|
11
|
+
//
|
|
12
|
+
// 键是 `id`:同一主题重复上报只覆盖不堆积(它表达的是"现在的状态",不是日志)。
|
|
13
|
+
const notes = new Map();
|
|
14
|
+
/** 记一条(同 id 覆盖)。 */
|
|
15
|
+
export function noteRuntime(note) {
|
|
16
|
+
notes.set(note.id, { ...note, at: Date.now() });
|
|
17
|
+
}
|
|
18
|
+
/** 该主题已恢复正常 → 收掉这一行。 */
|
|
19
|
+
export function clearRuntimeNote(id) {
|
|
20
|
+
notes.delete(id);
|
|
21
|
+
}
|
|
22
|
+
/** 当前全部运行时降级(`compat-status` 取用)。 */
|
|
23
|
+
export function runtimeNotes() {
|
|
24
|
+
return [...notes.values()];
|
|
25
|
+
}
|