dsh-plugin-tool-management 0.14.0 → 0.15.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 +560 -519
- 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 +391 -365
- package/lib/audit-log.js +157 -0
- package/lib/client.js +3800 -2504
- package/lib/compat/patch-dialect.js +3 -1
- package/lib/compat/preset-reach.js +37 -6
- package/lib/compat/probe.js +149 -7
- package/lib/context-inject.js +5 -3
- package/lib/index.js +236 -30
- package/lib/mcp/loader-token.js +53 -28
- package/lib/mcp/manager.js +146 -7
- package/lib/memories/archive-engine.js +70 -2
- package/lib/memories/archive.js +8 -1
- package/lib/memories/index-io.js +6 -0
- package/lib/op-registry.js +18 -4
- package/lib/ops/compat.js +109 -35
- package/lib/ops/scene-records.js +34 -0
- package/lib/ops/scene-sync.js +12 -1
- package/lib/ops/sessions.js +23 -8
- package/lib/ops/snapshot.js +596 -0
- package/lib/scenes/candidates.js +10 -5
- package/lib/sessions/bridge.js +5 -0
- package/lib/sessions/workspace.js +17 -5
- package/lib/skills/core.js +12 -3
- package/lib/tools/deps.js +1 -1
- package/lib/tools/table.js +5 -1
- package/package.json +112 -108
package/lib/index.js
CHANGED
|
@@ -22,6 +22,8 @@ import { isInsideRootResolved } from './paths.js';
|
|
|
22
22
|
import { DEFAULT_PROFILE_NAME, PROFILE_CANDIDATES } from './host-names.js';
|
|
23
23
|
import { createMemoriesService } from './memories/service.js';
|
|
24
24
|
import { createArchiveEngine } from './memories/archive-engine.js';
|
|
25
|
+
import { createSnapshotOps } from './ops/snapshot.js';
|
|
26
|
+
import { AUDIT_TARGET_CONFIG, clearAudit, readAudit, recordAudit, recordOpAudit } from './audit-log.js';
|
|
25
27
|
import { readIndexSync } from './memories/index-io.js';
|
|
26
28
|
import { runStateDoctor } from './ops/state-doctor.js';
|
|
27
29
|
import { createSubagentService, decideToolFilter, defaultPersonasDir } from './subagents/service.js';
|
|
@@ -29,7 +31,7 @@ import { createSubagentCatalog } from './subagents/catalog.js';
|
|
|
29
31
|
import { createSkillCatalog } from './skills/catalog.js';
|
|
30
32
|
import { DEFAULT_INJECT_SETTINGS, createContextInjector, normalizeInjectSettings, subagentDepthOf, } from './context-inject.js';
|
|
31
33
|
import { isApprovalNever } from './approval-policy.js';
|
|
32
|
-
import { addPreset, buildToolTableReport, DEFAULT_HIDDEN_TOOLS, dropPreset, migrateLegacyToolNames, normalizeToolTableSettings, PRESET_MAX_COUNT, PRESET_NAME_MAX_LENGTH, toolTableSettingsFrom, } from './tools/table.js';
|
|
34
|
+
import { addPreset, buildToolTableReport, DEFAULT_HIDDEN_TOOLS, dropPreset, migrateLegacyToolNames, normalizeToolTableSettings, FACTORY_TOOL_TABLE_PRESET, PRESET_MAX_COUNT, PRESET_NAME_MAX_LENGTH, toolTableSettingsFrom, } from './tools/table.js';
|
|
33
35
|
import { normalizeSceneSettings } from './scene-settings.js';
|
|
34
36
|
import { checkPatchWrite, takePatchGuardWarnings } from './compat/patch-dialect.js';
|
|
35
37
|
import { clearRuntimeNote, noteRuntime } from './compat/runtime-notes.js';
|
|
@@ -42,7 +44,7 @@ import { createCandidates } from './scenes/candidates.js';
|
|
|
42
44
|
// 「已知工具」归一化与「当前可用工具数」原本定义在本文件(导出仅为测试接缝),
|
|
43
45
|
// 2026-09-19 随 MCP 域搬进 ./mcp/manager.ts。这里保持原路径可导出,契约不变。
|
|
44
46
|
export { normalizeKnownTools, countEnabledTools } from './mcp/manager.js';
|
|
45
|
-
import { applyLoaderToken, applyLoaderTokenDisabled, readLoaderToken } from './mcp/loader-token.js';
|
|
47
|
+
import { applyLoaderToken, applyLoaderTokenDisabled, buildLoaderOverrideEntry, readLoaderToken } from './mcp/loader-token.js';
|
|
46
48
|
import { hubBackupDir, hubPath, migrateHubLayoutSync, relocateEntries } from './hub.js';
|
|
47
49
|
// 分域的 HTTP ops(2026-09-19 从本文件 handlers 表抽出,依赖显式传参)。
|
|
48
50
|
import { buildCompatOps } from './ops/compat.js';
|
|
@@ -163,6 +165,8 @@ export default {
|
|
|
163
165
|
kind: 'read',
|
|
164
166
|
fallback: 'inform-only',
|
|
165
167
|
detail: '技能 provider 装配失败(' + message(e) + '):技能启用/停用与 agent 技能目录这一轮不生效。',
|
|
168
|
+
detailKey: 'skills-provider',
|
|
169
|
+
params: { reason: message(e) },
|
|
166
170
|
});
|
|
167
171
|
}
|
|
168
172
|
// ---------- 提示词预设库(hub/prompts/)+ 切换 ----------
|
|
@@ -296,7 +300,41 @@ export default {
|
|
|
296
300
|
// 用生效集去校验会把合法勾选判成 stale 丢掉。
|
|
297
301
|
return (await skillScan()).all;
|
|
298
302
|
}
|
|
303
|
+
// 整体替换关停名单的那一步抽成具名函数:引擎的 apply 依赖与「修改场景」表单的
|
|
304
|
+
// 即时生效(scene-sync 包装)共用同一条写路径,不能各抄一份。
|
|
305
|
+
const applyToolTableHiddenNow = async (hidden) => {
|
|
306
|
+
// 与 `tool-table` op 写分支同一条路径、同一顺序:落盘 → 重排可见性 → 两个目录重算。
|
|
307
|
+
// 绕开这套顺序就会留下「名单已经改了、工具下一轮还发给模型」这种最难查的状态。
|
|
308
|
+
const current = await readToolTableSettings();
|
|
309
|
+
const next = {
|
|
310
|
+
hidden: normalizeToolTableSettings({ hidden }).hidden.filter((name) => toolTableSizes.has(name)),
|
|
311
|
+
presets: current.presets,
|
|
312
|
+
};
|
|
313
|
+
const saved = await writeToolTableSettings(next);
|
|
314
|
+
if (!saved.ok)
|
|
315
|
+
throw new Error(saved.error);
|
|
316
|
+
mcp.scheduleToolRestrictions();
|
|
317
|
+
await Promise.all([
|
|
318
|
+
skillCatalog.refresh().catch(() => { }),
|
|
319
|
+
subagentCatalog.refresh().catch(() => { }),
|
|
320
|
+
]);
|
|
321
|
+
};
|
|
299
322
|
const archiveService = createArchiveEngine({
|
|
323
|
+
// 引擎每应用/还原一次整套档案记一条流水(source='engine',见 ./audit-log.ts)。
|
|
324
|
+
// 记在边界这一层而不是往五个 apply* 里散着写:一条流水说的是「引擎动了整个环境」,
|
|
325
|
+
// 与同一次进入里面板/模型那条记录天然按秒分组显示。
|
|
326
|
+
audit: (op, target) => recordAudit({ ts: Date.now(), op, target: target || AUDIT_TARGET_CONFIG, source: 'engine' }),
|
|
327
|
+
// ── 模型工具表(0.15.0 C1):场景可以绑一份方案,进场景整体换成那一份 ──
|
|
328
|
+
currentToolTableHidden: async () => [...(await readToolTableSettings()).hidden],
|
|
329
|
+
toolTablePresetHidden: async (name) => {
|
|
330
|
+
const s = await readToolTableSettings();
|
|
331
|
+
// 用户存的方案**优先**:万一有人把方案起名叫 'factory-default',他要的是自己那份。
|
|
332
|
+
const hit = s.presets.filter((p) => p.name === name)[0];
|
|
333
|
+
if (hit)
|
|
334
|
+
return [...hit.hidden];
|
|
335
|
+
return name === FACTORY_TOOL_TABLE_PRESET ? [...DEFAULT_HIDDEN_TOOLS] : null;
|
|
336
|
+
},
|
|
337
|
+
applyToolTableHidden: applyToolTableHiddenNow,
|
|
300
338
|
loadSlice: async () => ({ ...(await memoriesService.readArchiveSlice()) }),
|
|
301
339
|
saveSlice: (slice) => memoriesService.patchIndex(slice),
|
|
302
340
|
configuredServers: async () => {
|
|
@@ -803,7 +841,7 @@ export default {
|
|
|
803
841
|
return { ok: true, settings: next };
|
|
804
842
|
});
|
|
805
843
|
}
|
|
806
|
-
// 注入实况(只读诊断):注入器的 live() 读回"最近活跃会话"
|
|
844
|
+
// 注入实况(只读诊断):注入器的 live() 读回"最近活跃会话"可见表面上的各域文本。
|
|
807
845
|
// 挂在 apply 作用域:`injection-live` op 与注入器不在同一层(effect 内部)。
|
|
808
846
|
let contextInjectorLive = null;
|
|
809
847
|
// 采纳遥测的入口(同样是 apply 作用域的中转):工具注册在 effect 之外,
|
|
@@ -876,7 +914,9 @@ export default {
|
|
|
876
914
|
label: '上下文注入通道',
|
|
877
915
|
kind: 'read',
|
|
878
916
|
fallback: 'inform-only',
|
|
879
|
-
detail: '注入通道装配失败(' + message(e) + '
|
|
917
|
+
detail: '注入通道装配失败(' + message(e) + '):全部注入域的内容这一轮不会被送进模型。',
|
|
918
|
+
detailKey: 'context-injection',
|
|
919
|
+
params: { reason: message(e) },
|
|
880
920
|
});
|
|
881
921
|
}
|
|
882
922
|
void readInjectSettings().catch(() => { });
|
|
@@ -903,6 +943,7 @@ export default {
|
|
|
903
943
|
kind: 'read',
|
|
904
944
|
fallback: 'inform-only',
|
|
905
945
|
detail: 'cordis 导出的 symbols.original 与 Symbol.for("cordis.original") 不是同一个符号:能力探测可能把代理当原始对象,身份判定会失真。',
|
|
946
|
+
detailKey: 'cordis-original-symbol',
|
|
906
947
|
});
|
|
907
948
|
}
|
|
908
949
|
}
|
|
@@ -1190,23 +1231,87 @@ export default {
|
|
|
1190
1231
|
if (cached)
|
|
1191
1232
|
return cached;
|
|
1192
1233
|
let home = null;
|
|
1234
|
+
let derivedProfileDir = null;
|
|
1235
|
+
let derivedProfileName = null;
|
|
1236
|
+
let docObserved;
|
|
1237
|
+
let docError;
|
|
1193
1238
|
try {
|
|
1194
1239
|
// 注意:这里的 home 来自宿主文档路径的切片,与 `skills/core.js` 的
|
|
1195
1240
|
// `resolveDshHome()`(`$DSH_HOME` ‖ `~/.dsh`)是**两条独立来源**,本插件同时用着两者。
|
|
1196
1241
|
// 不合并是有意的:合并会改变所有 profile 补丁的落点,属于行为变更,留给结构性重构那一档。
|
|
1197
1242
|
const doc = await settings.prepareDocument();
|
|
1198
1243
|
if (typeof doc === 'string' && doc) {
|
|
1244
|
+
docObserved = doc;
|
|
1199
1245
|
const i = Math.max(doc.lastIndexOf('\\'), doc.lastIndexOf('/'));
|
|
1200
|
-
|
|
1246
|
+
const docDir = i > 0 ? doc.slice(0, i) : doc;
|
|
1247
|
+
// 0.1.7 起 `prepareDocument()` 返回的是 **profile 补丁**(`configEditor.documentPath`,
|
|
1248
|
+
// 形如 `<home>\profiles\web\cordis.patch.yml`);0.1.5 返回的才是主目录下的设置文档。
|
|
1249
|
+
// 直接切片会把 profile 目录当成主目录 —— globalPatch 随之指到 profile 补丁,
|
|
1250
|
+
// 全局补丁里那些 MCP 行就全部读不到了(0.15.0 实测)。所以先认这个形状:
|
|
1251
|
+
// 目录以 `profiles\<name>` 结尾 ⇒ 它自己就是 profileDir,主目录上溯两级。
|
|
1252
|
+
const profileShaped = /^(.+)[\\/]profiles[\\/]([^\\/]+)$/.exec(docDir);
|
|
1253
|
+
if (profileShaped) {
|
|
1254
|
+
home = profileShaped[1];
|
|
1255
|
+
derivedProfileDir = docDir;
|
|
1256
|
+
derivedProfileName = profileShaped[2];
|
|
1257
|
+
}
|
|
1258
|
+
else {
|
|
1259
|
+
home = docDir;
|
|
1260
|
+
}
|
|
1201
1261
|
}
|
|
1202
1262
|
}
|
|
1203
|
-
catch (e) {
|
|
1204
|
-
|
|
1263
|
+
catch (e) {
|
|
1264
|
+
docError = String(e?.message ?? e);
|
|
1265
|
+
}
|
|
1266
|
+
if (!home) {
|
|
1267
|
+
// 推导失败从「静默抛错」升级为「具名上报 + 抛错」:这条路径正是 0.1.7 升级事故的
|
|
1268
|
+
// 源头(prepareDocument 返回语义一变这里就指错目录,MCP 页与补丁读写全部失明),
|
|
1269
|
+
// 下次契约再变,兼容页必须第一眼看到推导挂了,而不是等某个页面空白再倒查。
|
|
1270
|
+
noteRuntime({
|
|
1271
|
+
id: 'paths.home-derivation',
|
|
1272
|
+
label: '主目录推导',
|
|
1273
|
+
kind: 'read',
|
|
1274
|
+
fallback: 'refuse-operation',
|
|
1275
|
+
detail: docError !== undefined
|
|
1276
|
+
? `settings.prepareDocument() 抛错:${docError}。主目录推导失败,MCP 页与补丁读写不可用。`
|
|
1277
|
+
: 'settings.prepareDocument() 未返回可用路径(宿主路径契约可能又变了)。主目录推导失败,MCP 页与补丁读写不可用。',
|
|
1278
|
+
});
|
|
1205
1279
|
throw new Error('无法确定 DSH 主目录(settings.prepareDocument 未返回路径)');
|
|
1280
|
+
}
|
|
1206
1281
|
const sep = home.indexOf('\\') >= 0 ? '\\' : '/';
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1282
|
+
// prepareDocument 给出的就是**现存**的 profile 补丁路径(官方注释如此),所以
|
|
1283
|
+
// 推导出的 profileDir 直接可信;只有它没给出 profile 形状时才走下面的候选探测。
|
|
1284
|
+
const profileShapedPatchExists = derivedProfileDir !== null
|
|
1285
|
+
&& (await exists(derivedProfileDir + sep + 'cordis.patch.yml'));
|
|
1286
|
+
// —— 推导自检(0.15.0):健康时不留 note(运行时上报一律渲染成兼容页的问题行,
|
|
1287
|
+
// 常驻健康行会违反「琥珀只留给故障」的口径),只在推导结果反常时记,恢复时收掉。
|
|
1288
|
+
// 两条不变量照着 0.1.7 事故的形状挑:那次推出的「主目录」其实存在(是 profile 目录),
|
|
1289
|
+
// 存在性检查抓不住,**profiles 子目录**检查才抓得住 —— 主目录下永远有 profiles\
|
|
1290
|
+
//(本插件自己的 profile 就住在里面)。官方再改一次语义,两道检查至少一道当场报警。
|
|
1291
|
+
const derivationNote = (detail) => {
|
|
1292
|
+
noteRuntime({
|
|
1293
|
+
id: 'paths.home-derivation',
|
|
1294
|
+
label: '主目录推导',
|
|
1295
|
+
kind: 'read',
|
|
1296
|
+
fallback: 'refuse-operation',
|
|
1297
|
+
detail,
|
|
1298
|
+
});
|
|
1299
|
+
};
|
|
1300
|
+
if (!(await exists(home))) {
|
|
1301
|
+
derivationNote(`推导出的主目录不存在:${home}(设置文档路径:${docObserved ?? '未知'})。推导结果不可信,MCP 页与补丁读写会落空。`);
|
|
1302
|
+
}
|
|
1303
|
+
else if (!(await exists(home + sep + 'profiles'))) {
|
|
1304
|
+
derivationNote(`推导出的主目录下没有 profiles 目录:${home}(设置文档路径:${docObserved ?? '未知'})。主目录很可能指错了 —— 直接切片一个 profile 补丁路径就是这个形状;MCP 页与补丁读写会读错文件。`);
|
|
1305
|
+
}
|
|
1306
|
+
else if (derivedProfileDir !== null && !profileShapedPatchExists) {
|
|
1307
|
+
derivationNote(`设置文档路径呈 profile 目录形状,但其中没有 cordis.patch.yml:${docObserved}。官方承诺返回「现存」的 profile 补丁路径,形状对不上说明契约又变了;已回退候选探测。`);
|
|
1308
|
+
}
|
|
1309
|
+
else {
|
|
1310
|
+
clearRuntimeNote('paths.home-derivation');
|
|
1311
|
+
}
|
|
1312
|
+
let profileDir = profileShapedPatchExists ? derivedProfileDir : null;
|
|
1313
|
+
let profileName = derivedProfileName ?? DEFAULT_PROFILE_NAME;
|
|
1314
|
+
for (const name of profileDir === null ? PROFILE_CANDIDATES : []) {
|
|
1210
1315
|
if (await exists(home + sep + 'profiles' + sep + name + sep + 'cordis.patch.yml')) {
|
|
1211
1316
|
profileDir = home + sep + 'profiles' + sep + name;
|
|
1212
1317
|
profileName = name;
|
|
@@ -1237,6 +1342,10 @@ export default {
|
|
|
1237
1342
|
profileName,
|
|
1238
1343
|
projectPatch: profileDir + sep + 'cordis.patch.yml',
|
|
1239
1344
|
globalPatch: home + sep + 'cordis.patch.yml',
|
|
1345
|
+
// 0.1.7 bundle 层:插件包内自己的 cordis.patch.yml(`dsh.bundle.patch`)定义 loader
|
|
1346
|
+
// 条目。它只是「覆盖目标存在吗」的凭证与读取参照,**绝不是令牌的写点** —— 那份文件
|
|
1347
|
+
// 是仓库镜像(sync 每次构建覆盖),把令牌写进去等于把明文密钥提交进仓库。
|
|
1348
|
+
bundlePatch: profileDir + sep + 'node_modules' + sep + 'dsh-plugin-tool-management' + sep + 'cordis.patch.yml',
|
|
1240
1349
|
};
|
|
1241
1350
|
return cached;
|
|
1242
1351
|
}
|
|
@@ -1475,7 +1584,17 @@ export default {
|
|
|
1475
1584
|
infoOf.set(abs, { token: read.token, disabled: read.disabled });
|
|
1476
1585
|
}
|
|
1477
1586
|
}
|
|
1478
|
-
|
|
1587
|
+
// 0.1.7 bundle 挂载:loader 条目由插件包内的 cordis.patch.yml(bundle 层)提供,两份
|
|
1588
|
+
// 补丁里都没有可就地改写的行。bundleBase 只回答「覆盖目标存在吗」—— 写点是 profile
|
|
1589
|
+
// 补丁里的覆盖条目(见 tokenConfigure),bundle 文件本身永不落密钥。
|
|
1590
|
+
let bundleBase = false;
|
|
1591
|
+
try {
|
|
1592
|
+
bundleBase = readLoaderToken(await readPatch(p.bundlePatch)).found;
|
|
1593
|
+
}
|
|
1594
|
+
catch {
|
|
1595
|
+
bundleBase = false;
|
|
1596
|
+
}
|
|
1597
|
+
return { files, infoOf, bundleBase };
|
|
1479
1598
|
}
|
|
1480
1599
|
/**
|
|
1481
1600
|
* 配置**文件里**写了令牌,但当前进程还没生效(或反过来)—— 也就是"改了配置还没重启"。
|
|
@@ -1565,13 +1684,39 @@ export default {
|
|
|
1565
1684
|
if (value === CONFIG_TOKEN)
|
|
1566
1685
|
return { ok: true, changed: false, note: '和当前令牌相同,未改动。', restartRequired: false };
|
|
1567
1686
|
}
|
|
1568
|
-
const
|
|
1569
|
-
|
|
1570
|
-
|
|
1687
|
+
const p = await ensurePaths();
|
|
1688
|
+
const { files, bundleBase } = await loaderTokenFiles();
|
|
1689
|
+
if (!files.length && !(mode === 'set' && bundleBase)) {
|
|
1690
|
+
return { ok: false, error: '没找到本插件的 loader 行,也无法确认 bundle 层的挂载条目,无法自动改写配置:请在 profile 的 cordis.patch.yml 里手动加一条覆盖条目(- id: dsh-plugin-tool-management → config: → token: "…"),或改用环境变量 DSH_PLUGIN_TOOL_MANAGEMENT_TOKEN。' };
|
|
1571
1691
|
}
|
|
1572
1692
|
if (files.length > 1) {
|
|
1573
1693
|
return { ok: false, error: '本插件的 loader 行同时出现在两份补丁文件里(会导致 DSH 无法启动):请先清掉重复那条,再回来设置令牌。' };
|
|
1574
1694
|
}
|
|
1695
|
+
if (!files.length) {
|
|
1696
|
+
// 0.1.7 bundle 挂载:两份补丁里都没有本插件的 loader 行(条目由 bundle 层提供,那份
|
|
1697
|
+
// 文件是仓库镜像、不能落密钥)—— 令牌写进 **profile 补丁的覆盖条目**:宿主在所有
|
|
1698
|
+
// bundle 层之后应用它,整体替换那条 loader 的 config(bundle 条目没有 config,替换
|
|
1699
|
+
// 零丢失);同 id 的 insert 条目才会重复挂载导致启动失败,覆盖条目是官方支持的改法。
|
|
1700
|
+
// `off` / `clear` / `on` 走不到这里:有 site 时就地改写,没 site 且有令牌 = 令牌来自
|
|
1701
|
+
// 环境变量,那两条路在前面已按各自口径返回。
|
|
1702
|
+
return withWriteLock(async () => {
|
|
1703
|
+
let content = '';
|
|
1704
|
+
try {
|
|
1705
|
+
content = await readPatch(p.projectPatch);
|
|
1706
|
+
}
|
|
1707
|
+
catch (e) {
|
|
1708
|
+
return { ok: false, error: '读取补丁失败: ' + message(e) };
|
|
1709
|
+
}
|
|
1710
|
+
const next = content === '' ? buildLoaderOverrideEntry(value) + '\n' : (content.endsWith('\n') ? content : content + '\n') + '\n' + buildLoaderOverrideEntry(value);
|
|
1711
|
+
try {
|
|
1712
|
+
await writePatch(p.projectPatch, next);
|
|
1713
|
+
}
|
|
1714
|
+
catch (e) {
|
|
1715
|
+
return { ok: false, error: '写入补丁失败: ' + message(e) };
|
|
1716
|
+
}
|
|
1717
|
+
return { ok: true, changed: true, mode, restartRequired: true, path: p.projectPatch };
|
|
1718
|
+
});
|
|
1719
|
+
}
|
|
1575
1720
|
const abs = files[0];
|
|
1576
1721
|
return withWriteLock(async () => {
|
|
1577
1722
|
let content = '';
|
|
@@ -1774,6 +1919,10 @@ export default {
|
|
|
1774
1919
|
});
|
|
1775
1920
|
const handlers = {
|
|
1776
1921
|
'plugin-version': pluginVersion,
|
|
1922
|
+
// 「最近改动」流水的读侧(0.15.0 B2):只读 hub 里的 `audit.jsonl`,倒序、可按域过滤。
|
|
1923
|
+
'audit-list': async (args) => ({ ok: true, entries: await readAudit(args || {}) }),
|
|
1924
|
+
// 清空流水。清完这一条 op 自己会被下面的包装层记进新流水("谁清过"也留痕)。
|
|
1925
|
+
'audit-clear': async () => await clearAudit(),
|
|
1777
1926
|
'skill-open': skillOpen,
|
|
1778
1927
|
// MCP 域 16 个 op(实现在 ./mcp/manager.ts)
|
|
1779
1928
|
...mcp.ops,
|
|
@@ -1812,10 +1961,11 @@ export default {
|
|
|
1812
1961
|
cleanPatchBackups,
|
|
1813
1962
|
getContextInjectorLive: () => contextInjectorLive,
|
|
1814
1963
|
// 功能总览(B6)的三层合成:令牌实况与兼容页「访问令牌」同源;场景锁定与写门禁同源;
|
|
1815
|
-
//
|
|
1964
|
+
// 停用工具的两个口径读 mcp 的 TTL 缓存(无 I/O)。
|
|
1816
1965
|
readTokenState: async () => ({ active: TOKEN !== '', accepted: TOKEN !== '' && acceptedThisBoot() }),
|
|
1817
1966
|
lockedSceneNames: () => lockedSceneNames(),
|
|
1818
|
-
|
|
1967
|
+
disabledToolSummary: () => mcp.disabledToolSummary(),
|
|
1968
|
+
injectServiceCount: INJECT_SERVICES.length,
|
|
1819
1969
|
message,
|
|
1820
1970
|
compatLog,
|
|
1821
1971
|
}),
|
|
@@ -1849,6 +1999,20 @@ export default {
|
|
|
1849
1999
|
// 必须排在 ...memoriesService.ops 之后 —— 这里是显式覆盖同名 op,不是新增。
|
|
1850
2000
|
...buildSceneSyncOps({
|
|
1851
2001
|
withAgentsMdSync,
|
|
2002
|
+
onToolTablePresetChanged: async (scene, value) => {
|
|
2003
|
+
// 「修改场景」里改绑工具表方案且改的是**当前启用场景** → 立即把关停名单整体换成
|
|
2004
|
+
// 那一份(与「改档案 = 立刻生效」同口径)。解绑('')按下拉里的承诺 =「保持现状」,
|
|
2005
|
+
// 运行时不动;悬空绑定(方案已删)也不在这里硬失败,交给引擎进出场景时的 stale 处理。
|
|
2006
|
+
const slice = await memoriesService.readArchiveSlice();
|
|
2007
|
+
if (!slice.mode || slice.mode.scene !== scene || !value)
|
|
2008
|
+
return;
|
|
2009
|
+
const s = await readToolTableSettings();
|
|
2010
|
+
const hit = s.presets.filter((p) => p.name === value)[0];
|
|
2011
|
+
const hidden = hit ? [...hit.hidden] : (value === FACTORY_TOOL_TABLE_PRESET ? [...DEFAULT_HIDDEN_TOOLS] : null);
|
|
2012
|
+
if (!hidden)
|
|
2013
|
+
return;
|
|
2014
|
+
await applyToolTableHiddenNow(hidden);
|
|
2015
|
+
},
|
|
1852
2016
|
rulesOps: {
|
|
1853
2017
|
'rules-set-active': (args) => memoriesService.ops['rules-set-active'](args),
|
|
1854
2018
|
'rules-update-scene': (args) => memoriesService.ops['rules-update-scene'](args),
|
|
@@ -2252,6 +2416,22 @@ export default {
|
|
|
2252
2416
|
// handlers 后处理(2026-09-19 抽到 ./request-gate.ts):场景锁定守卫、开关同步档案、
|
|
2253
2417
|
// 读 op 的 anyLocked/activeScene 注解、子智能体失败清单 attach。
|
|
2254
2418
|
// 顺序即语义(先装的在内层),整段搬运未改。
|
|
2419
|
+
//
|
|
2420
|
+
// 整机迁移快照(0.15.0 C2)在门禁装好**之前**并入 handlers:它自己不写任何域的数据,
|
|
2421
|
+
// 而是逐域调 handlers 表里那些 op —— 装好后取到的就是带门禁的那一份,场景锁定期间各域
|
|
2422
|
+
// 自己拒绝,迁移这边不必再判一套锁。
|
|
2423
|
+
const snapshotOps = createSnapshotOps({
|
|
2424
|
+
invokeOp: (name, args) => {
|
|
2425
|
+
const fn = handlers[name];
|
|
2426
|
+
return fn ? fn(args || {}) : Promise.resolve({ ok: false, error: '未知操作: ' + name });
|
|
2427
|
+
},
|
|
2428
|
+
hubPath,
|
|
2429
|
+
ensurePaths,
|
|
2430
|
+
pluginVersion: () => PKG_VERSION,
|
|
2431
|
+
promptsDir: () => promptsDir,
|
|
2432
|
+
message,
|
|
2433
|
+
});
|
|
2434
|
+
Object.assign(handlers, snapshotOps);
|
|
2255
2435
|
installHandlerGuards({
|
|
2256
2436
|
handlers,
|
|
2257
2437
|
lockedSceneNames,
|
|
@@ -2329,8 +2509,28 @@ export default {
|
|
|
2329
2509
|
*/
|
|
2330
2510
|
const hostDefineToolAny = hostDefineTool;
|
|
2331
2511
|
const defineTool = ((options) => trackAdoption(hostDefineToolAny(options)));
|
|
2332
|
-
//
|
|
2512
|
+
// 各域共用的那一份依赖(形状见 tools/deps.ts)。defineTool 带着遥测一起传下去 ——
|
|
2333
2513
|
// 各域必须用它,直接 import 宿主的那个会丢掉采纳统计。
|
|
2514
|
+
/**
|
|
2515
|
+
* 「最近改动」流水的模型侧挂点(0.15.0 B2)。
|
|
2516
|
+
*
|
|
2517
|
+
* 为什么包在**交给工具的 op 表**上而不是散进每个 op:op 执行有多条入口路径(HTTP 路由、
|
|
2518
|
+
* 模型工具的 execute、场景引擎),而判断「谁干的」这件事只有调用点知道 —— 在 op 里自己猜
|
|
2519
|
+
* 调用方就是猜。这里给工具看到的表包一层,界面上 handlers 表那份原样不动,两条路各记各的。
|
|
2520
|
+
* 只多记一条流水:不改行为、不改返回值、也不吞异常(写类与成败的判据在 ./audit-log.ts)。
|
|
2521
|
+
*/
|
|
2522
|
+
const auditFn = (opName, fn) => async (args) => {
|
|
2523
|
+
const result = await fn(args || {});
|
|
2524
|
+
recordOpAudit(opName, args || {}, 'model', result);
|
|
2525
|
+
return result;
|
|
2526
|
+
};
|
|
2527
|
+
const auditOps = (ops) => {
|
|
2528
|
+
const out = {};
|
|
2529
|
+
for (const [name, fn] of Object.entries(ops)) {
|
|
2530
|
+
out[name] = typeof fn === 'function' ? auditFn(name, fn) : fn;
|
|
2531
|
+
}
|
|
2532
|
+
return out;
|
|
2533
|
+
};
|
|
2334
2534
|
const toolDeps = {
|
|
2335
2535
|
defineTool,
|
|
2336
2536
|
// 量体积只认**注册成功**的那些(注册失败的域工具本来就不在模型工具表里)——
|
|
@@ -2345,26 +2545,27 @@ export default {
|
|
|
2345
2545
|
injectNoticeOptions,
|
|
2346
2546
|
message,
|
|
2347
2547
|
};
|
|
2548
|
+
const mcpToolOps = auditOps(mcp.ops);
|
|
2348
2549
|
buildMcpTools({
|
|
2349
2550
|
...toolDeps,
|
|
2350
2551
|
mcpmListView: mcp.mcpmListView,
|
|
2351
2552
|
mcpmTools: mcp.ops['mcpm-tools'],
|
|
2352
|
-
mcpmSetEnabled:
|
|
2353
|
-
mcpmRestart:
|
|
2354
|
-
mcpmToolEnabled:
|
|
2355
|
-
mcpmAdd:
|
|
2356
|
-
mcpmEdit:
|
|
2357
|
-
mcpmNote:
|
|
2553
|
+
mcpmSetEnabled: mcpToolOps['mcpm-set-enabled'],
|
|
2554
|
+
mcpmRestart: mcpToolOps['mcpm-restart'],
|
|
2555
|
+
mcpmToolEnabled: mcpToolOps['mcpm-tool-enabled'],
|
|
2556
|
+
mcpmAdd: mcpToolOps['mcpm-add'],
|
|
2557
|
+
mcpmEdit: mcpToolOps['mcpm-edit'],
|
|
2558
|
+
mcpmNote: mcpToolOps['mcpm-note'],
|
|
2358
2559
|
// **故意不接 `mcpm-reveal`**:`mcp_manager_save` 的改分支用 `mcpmListView()` 的
|
|
2359
2560
|
// 打码视图填回省略字段,由 `mcpm-edit` 的 `resolveMaskedKv` / `resolveMaskedUrl`
|
|
2360
2561
|
// 还原真值 —— 那条路本来就是给"表单里出现打码值"设计的(界面编辑框预填的就是它)。
|
|
2361
2562
|
// 让模型驱动的工具在进程内读明文凭据,是另一条没人设计过、也没有测试覆盖的路径。
|
|
2362
2563
|
});
|
|
2363
|
-
buildSkillTools({ ...toolDeps, skillsOps: skillsService.ops });
|
|
2564
|
+
buildSkillTools({ ...toolDeps, skillsOps: auditOps(skillsService.ops) });
|
|
2364
2565
|
// 传 op 表而不是 promptsService:「生效中」的判定只有 `agentsmd-list` 里有(场景绑定
|
|
2365
2566
|
// 的那份才算),直调服务会得到文件比对口径 —— 场景驱动时工具会报一个与界面不同的答案。
|
|
2366
|
-
buildPromptTools({ ...toolDeps, promptOps, applyPresetGuarded, promptsDir });
|
|
2367
|
-
buildMemoryTools({ ...toolDeps, rulesOps: memoriesService.ops });
|
|
2567
|
+
buildPromptTools({ ...toolDeps, promptOps: auditOps(promptOps), applyPresetGuarded, promptsDir });
|
|
2568
|
+
buildMemoryTools({ ...toolDeps, rulesOps: auditOps(memoriesService.ops) });
|
|
2368
2569
|
// 场景族(tools/scene.ts):列场景与档案(`_list`)+ 建场景 / 写档案 / 绑提示词(`_save`)
|
|
2369
2570
|
// + 进入与退出(`_switch`)。
|
|
2370
2571
|
// 传**包装后**的 `archiveService.ops`:`scene-archive-save` 与 `scene-mode-set` 都在本文件
|
|
@@ -2376,13 +2577,14 @@ export default {
|
|
|
2376
2577
|
// 再包一次,是为了让工具与 HTTP API 共用同一个实现(不会各自漂移)。
|
|
2377
2578
|
buildSceneTools({
|
|
2378
2579
|
...toolDeps,
|
|
2379
|
-
rulesOps: memoriesService.ops,
|
|
2380
|
-
archiveOps: archiveService.ops,
|
|
2381
|
-
sceneActivate: (args) => handlers['rules-set-active'](args),
|
|
2580
|
+
rulesOps: auditOps(memoriesService.ops),
|
|
2581
|
+
archiveOps: auditOps(archiveService.ops),
|
|
2582
|
+
sceneActivate: auditFn('rules-set-active', (args) => handlers['rules-set-active'](args)),
|
|
2382
2583
|
});
|
|
2383
2584
|
buildSubagentTools({
|
|
2384
2585
|
...toolDeps,
|
|
2385
|
-
|
|
2586
|
+
// 只换掉工具看到的 `ops` 那一层(原型链回真实服务,其余读法一字不动)。
|
|
2587
|
+
subagentService: Object.assign(Object.create(subagentService), { ops: auditOps(subagentService.ops) }),
|
|
2386
2588
|
sceneLists: subagentSceneLists,
|
|
2387
2589
|
toolFilterFor: subagentToolFilterFor,
|
|
2388
2590
|
failures: subagentFailures,
|
|
@@ -2829,6 +3031,10 @@ export default {
|
|
|
2829
3031
|
return;
|
|
2830
3032
|
}
|
|
2831
3033
|
const result = await fn(payload.args || {});
|
|
3034
|
+
// 「最近改动」流水:来源 = 面板(这条 HTTP 路由是界面唯一的入口)。
|
|
3035
|
+
// `readOnlyCall` 是上面那四个「读改写两态 op」的纯读分支 —— 它们没改任何状态,不记。
|
|
3036
|
+
if (!readOnlyCall)
|
|
3037
|
+
recordOpAudit(op, payload.args || {}, 'panel', result);
|
|
2832
3038
|
res.end(JSON.stringify(withPatchWarnings(result === undefined ? { ok: true } : result)));
|
|
2833
3039
|
}
|
|
2834
3040
|
catch (e) {
|
package/lib/mcp/loader-token.js
CHANGED
|
@@ -9,8 +9,10 @@
|
|
|
9
9
|
// 全部抹掉、把 `!!js` 引号风格改掉、把不认识的键重排 —— 一次"设置令牌"不该顺带重写整个
|
|
10
10
|
// 文件。(这与 `override-blocks.ts` 选择"只算删除区间"是同一条理由。)
|
|
11
11
|
//
|
|
12
|
-
// 边界:只动**本插件 id
|
|
13
|
-
//
|
|
12
|
+
// 边界:只动**本插件 id 那条条目**内的 `config:` / `token:` 两行,别的一概不碰。条目有两种
|
|
13
|
+
// 形状(见 ROW_ID_RE 的注释):0.1.5 补丁里嵌套的 insert 子条目、0.1.7 profile 补丁里顶层
|
|
14
|
+
// 的覆盖条目 —— 同一套相对缩进逻辑都认。找不到那条行时如实回 `found: false`,由调用方
|
|
15
|
+
// 决定怎么报错;唯一的"新建"路径是 0.1.7 的覆盖条目(buildLoaderOverrideEntry,见下)。
|
|
14
16
|
//
|
|
15
17
|
// 两个**不改写、只拒绝**的形状(2026-09-19 审计 T-28):
|
|
16
18
|
// - `config:` 写成 flow style(`config: {token: x}`)或缩进不是 6 空格 ⇒ 本模块的
|
|
@@ -22,17 +24,15 @@
|
|
|
22
24
|
const yq = (value) => JSON.stringify(value);
|
|
23
25
|
/** 本插件 loader 行的 id(与 cordis.patch.yml 里那条 insert 子条目一致)。 */
|
|
24
26
|
export const PLUGIN_LOADER_ID = 'dsh-plugin-tool-management';
|
|
25
|
-
|
|
26
|
-
|
|
27
|
+
/**
|
|
28
|
+
* 行首 `- id:` 的缩进就是条目的几何原点:0.1.5 的补丁把条目嵌在映射里(4 空格,config
|
|
29
|
+
* 在 6、键在 8);0.1.7 的 profile 覆盖条目在顶层(0 空格,config 在 2、键在 4)。两类形状
|
|
30
|
+
* 用同一套「相对缩进」逻辑定位,readLoaderToken / rewriteConfigKey 都按条目自己的缩进算。
|
|
31
|
+
*/
|
|
32
|
+
const ROW_ID_RE = /^( *)- id:\s*(\S+)\s*$/;
|
|
27
33
|
/** `config:` 的任何别的写法(flow style `config: {…}`、非空值、别的缩进)—— 一律不改写。 */
|
|
28
34
|
const CONFIG_OTHER_RE = /^\s*config:\s*\S/;
|
|
29
|
-
const keyRe = (key) => new RegExp('^ {
|
|
30
|
-
const TOKEN_RE = keyRe('token');
|
|
31
|
-
const DISABLED_RE = keyRe('tokenDisabled');
|
|
32
|
-
/** 该子条目块内的行:缩进 ≥ 6(键),或空行/注释(块间空隙,归前一块)。 */
|
|
33
|
-
const inRowBlock = (line) => line.trim() === '' || /^\s*#/.test(line) || /^ {6,}\S/.test(line);
|
|
34
|
-
/** config 块内的行:缩进 ≥ 8,或空行/注释。 */
|
|
35
|
-
const inConfigBlock = (line) => line.trim() === '' || /^\s*#/.test(line) || /^ {8,}\S/.test(line);
|
|
35
|
+
const keyRe = (indent, key) => new RegExp('^ {' + indent + '}' + key + ':\\s*(.*)$');
|
|
36
36
|
const unquote = (raw) => {
|
|
37
37
|
const text = raw.trim();
|
|
38
38
|
if (text.length >= 2 && ((text.startsWith('"') && text.endsWith('"')) || (text.startsWith("'") && text.endsWith("'")))) {
|
|
@@ -76,12 +76,23 @@ function parseFlag(raw) {
|
|
|
76
76
|
/** 定位插件 loader 行及其 `config:` 块(找不到行时 `start < 0`)。 */
|
|
77
77
|
function locate(content, pluginId) {
|
|
78
78
|
const lines = content.split(/\r?\n/);
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
79
|
+
let start = -1;
|
|
80
|
+
let indent = 0;
|
|
81
|
+
for (let i = 0; i < lines.length; i++) {
|
|
82
|
+
const m = ROW_ID_RE.exec(lines[i]);
|
|
83
|
+
if (m && m[2] === pluginId) {
|
|
84
|
+
start = i;
|
|
85
|
+
indent = m[1].length;
|
|
86
|
+
break;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
83
89
|
if (start < 0)
|
|
84
|
-
return { lines, start: -1, end: -1, configAt: -1, configEnd: -1, configUnrecognised: false, configDuplicated: false };
|
|
90
|
+
return { lines, start: -1, end: -1, indent: 0, configAt: -1, configEnd: -1, configUnrecognised: false, configDuplicated: false };
|
|
91
|
+
// 该子条目块内的行:缩进 ≥ 条目+2(键),或空行/注释(块间空隙,归前一块)。
|
|
92
|
+
const inRowBlock = (line) => line.trim() === '' || /^\s*#/.test(line) || new RegExp('^\\s{' + (indent + 2) + ',}\\S').test(line);
|
|
93
|
+
// config 块内的行:缩进 ≥ 条目+4,或空行/注释。
|
|
94
|
+
const inConfigBlock = (line) => line.trim() === '' || /^\s*#/.test(line) || new RegExp('^\\s{' + (indent + 4) + ',}\\S').test(line);
|
|
95
|
+
const configRe = new RegExp('^ {' + (indent + 2) + '}config:\\s*$');
|
|
85
96
|
let end = start + 1;
|
|
86
97
|
while (end < lines.length && inRowBlock(lines[end]))
|
|
87
98
|
end++;
|
|
@@ -89,7 +100,7 @@ function locate(content, pluginId) {
|
|
|
89
100
|
let configCount = 0;
|
|
90
101
|
let configUnrecognised = false;
|
|
91
102
|
for (let i = start + 1; i < end; i++) {
|
|
92
|
-
if (
|
|
103
|
+
if (configRe.test(lines[i])) {
|
|
93
104
|
configCount += 1;
|
|
94
105
|
if (configAt < 0)
|
|
95
106
|
configAt = i;
|
|
@@ -106,24 +117,26 @@ function locate(content, pluginId) {
|
|
|
106
117
|
while (configEnd < end && inConfigBlock(lines[configEnd]))
|
|
107
118
|
configEnd++;
|
|
108
119
|
}
|
|
109
|
-
return { lines, start, end, configAt, configEnd, configUnrecognised, configDuplicated: configCount > 1 };
|
|
120
|
+
return { lines, start, end, indent, configAt, configEnd, configUnrecognised, configDuplicated: configCount > 1 };
|
|
110
121
|
}
|
|
111
122
|
/** 读插件 loader 行里的 `config.token` / `config.tokenDisabled`(没写 / 没这条行都返回空值)。 */
|
|
112
123
|
export function readLoaderToken(content, pluginId = PLUGIN_LOADER_ID) {
|
|
113
|
-
const { lines, start,
|
|
124
|
+
const { lines, start, configAt, configEnd, indent } = locate(content, pluginId);
|
|
114
125
|
if (start < 0)
|
|
115
126
|
return { found: false, token: '', disabled: false };
|
|
116
127
|
if (configAt < 0)
|
|
117
128
|
return { found: true, token: '', disabled: false };
|
|
129
|
+
const tokenRe = keyRe(indent + 4, 'token');
|
|
130
|
+
const disabledRe = keyRe(indent + 4, 'tokenDisabled');
|
|
118
131
|
let token = '';
|
|
119
132
|
let disabled = false;
|
|
120
133
|
for (let i = configAt + 1; i < configEnd; i++) {
|
|
121
|
-
const t =
|
|
134
|
+
const t = tokenRe.exec(lines[i]);
|
|
122
135
|
if (t) {
|
|
123
136
|
token = unquote(t[1]);
|
|
124
137
|
continue;
|
|
125
138
|
}
|
|
126
|
-
const d =
|
|
139
|
+
const d = disabledRe.exec(lines[i]);
|
|
127
140
|
// `null` = 值既不是 true 也不是 false(宿主同样不当成"已关闭"),按未关闭处理。
|
|
128
141
|
if (d)
|
|
129
142
|
disabled = parseFlag(d[1]) === true;
|
|
@@ -143,8 +156,8 @@ export function readLoaderToken(content, pluginId = PLUGIN_LOADER_ID) {
|
|
|
143
156
|
*
|
|
144
157
|
* @param line - 要写入的整行(含缩进);`null` = 删除该键。
|
|
145
158
|
*/
|
|
146
|
-
function rewriteConfigKey(content, key,
|
|
147
|
-
const { lines, start, end, configAt, configEnd, configUnrecognised, configDuplicated } = locate(content, pluginId);
|
|
159
|
+
function rewriteConfigKey(content, key, value, pluginId) {
|
|
160
|
+
const { lines, start, end, indent, configAt, configEnd, configUnrecognised, configDuplicated } = locate(content, pluginId);
|
|
148
161
|
if (start < 0)
|
|
149
162
|
return { found: false };
|
|
150
163
|
// 写之前先自检:这两种形状一旦落盘就是"DSH 下次起不来"(官方 js-yaml 对重复映射键 throw),
|
|
@@ -165,7 +178,7 @@ function rewriteConfigKey(content, key, line, pluginId) {
|
|
|
165
178
|
refused: '本插件那条 loader 行里的 `config:` 不是插件认识的写法(flow style 或缩进不是 6 空格),自动改写会插入重复键并导致 DSH 启动失败:请手工把它改回 ` config:` + 缩进 8 空格的键值行,或在配置里手工写 token。',
|
|
166
179
|
};
|
|
167
180
|
}
|
|
168
|
-
const re = keyRe(key);
|
|
181
|
+
const re = keyRe(indent + 4, key);
|
|
169
182
|
const keyAt = (() => {
|
|
170
183
|
if (configAt < 0)
|
|
171
184
|
return -1;
|
|
@@ -174,6 +187,8 @@ function rewriteConfigKey(content, key, line, pluginId) {
|
|
|
174
187
|
return i;
|
|
175
188
|
return -1;
|
|
176
189
|
})();
|
|
190
|
+
// 要写的整行(含缩进);`value === null` = 删除该键。缩进按条目自己的原点算。
|
|
191
|
+
const line = value === null ? null : ' '.repeat(indent + 4) + key + ': ' + value;
|
|
177
192
|
// 写之后的第二道自检:不管走哪条分支,产物里 `config:` 只能有一个。宁可原样退回,
|
|
178
193
|
// 也不让一次界面点击产出一个"DSH 下次起不来"的补丁文件。
|
|
179
194
|
const commit = (nextContent) => {
|
|
@@ -194,7 +209,7 @@ function rewriteConfigKey(content, key, line, pluginId) {
|
|
|
194
209
|
// 这个键之外还有别的键吗?(在原行上判断,避免 splice 之后的下标漂移算错)
|
|
195
210
|
const otherKeys = lines
|
|
196
211
|
.slice(configAt + 1, configEnd)
|
|
197
|
-
.some((row, index) => configAt + 1 + index !== keyAt &&
|
|
212
|
+
.some((row, index) => configAt + 1 + index !== keyAt && new RegExp('^\\s{' + (indent + 4) + ',}\\S').test(row));
|
|
198
213
|
next.splice(keyAt, 1);
|
|
199
214
|
if (!otherKeys)
|
|
200
215
|
next.splice(configAt, 1);
|
|
@@ -211,7 +226,7 @@ function rewriteConfigKey(content, key, line, pluginId) {
|
|
|
211
226
|
return commit(next.join('\n'));
|
|
212
227
|
}
|
|
213
228
|
// 没有 config:紧跟 `- id:` 之后插入(映射键的顺序不影响 YAML 语义,位置固定才好断言)。
|
|
214
|
-
next.splice(start + 1, 0, '
|
|
229
|
+
next.splice(start + 1, 0, ' '.repeat(indent + 2) + 'config:', line);
|
|
215
230
|
return commit(next.join('\n'));
|
|
216
231
|
}
|
|
217
232
|
/**
|
|
@@ -223,7 +238,7 @@ function rewriteConfigKey(content, key, line, pluginId) {
|
|
|
223
238
|
* 否则给出新全文与"是否真的改了"。
|
|
224
239
|
*/
|
|
225
240
|
export function applyLoaderToken(content, token, pluginId = PLUGIN_LOADER_ID) {
|
|
226
|
-
return rewriteConfigKey(content, 'token', token === null ? null :
|
|
241
|
+
return rewriteConfigKey(content, 'token', token === null ? null : yq(token), pluginId);
|
|
227
242
|
}
|
|
228
243
|
/**
|
|
229
244
|
* 打开(`true`)或关掉(`false`)令牌功能 —— **不动 `config.token` 本身**。
|
|
@@ -234,5 +249,15 @@ export function applyLoaderToken(content, token, pluginId = PLUGIN_LOADER_ID) {
|
|
|
234
249
|
* @param disabled - `true` 写开关;`false` / `null` 删掉该键。
|
|
235
250
|
*/
|
|
236
251
|
export function applyLoaderTokenDisabled(content, disabled, pluginId = PLUGIN_LOADER_ID) {
|
|
237
|
-
return rewriteConfigKey(content, 'tokenDisabled', disabled === true ? '
|
|
252
|
+
return rewriteConfigKey(content, 'tokenDisabled', disabled === true ? 'true' : null, pluginId);
|
|
253
|
+
}
|
|
254
|
+
/**
|
|
255
|
+
* 0.1.7 bundle 挂载下的令牌落点:profile 补丁里的**覆盖条目**(官方 `applyEntryPatches`
|
|
256
|
+
* 的 id-targeted override —— 不带 name / insert、只带 id + config,宿主在 bundle 层之后
|
|
257
|
+
* 应用它,整体替换那条 loader 的 `config`;bundle 条目没有 config,替换零丢失)。
|
|
258
|
+
* 注意:同 id 的 **insert** 条目才会重复挂载、启动失败;覆盖条目是官方支持的改法,
|
|
259
|
+
* 目标不存在的树只是逐条警告跳过(一份 overlay 打多个表面,不必每棵树都命中)。
|
|
260
|
+
*/
|
|
261
|
+
export function buildLoaderOverrideEntry(token, pluginId = PLUGIN_LOADER_ID) {
|
|
262
|
+
return '- id: ' + pluginId + '\n config:\n token: ' + yq(token);
|
|
238
263
|
}
|