dsh-plugin-tool-management 0.19.1 → 0.20.1
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 +915 -829
- package/README.md +237 -254
- package/README_EN.md +234 -223
- 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/images/logo.gif +0 -0
- package/docs/update.md +32 -30
- package/lib/client.js +9480 -6970
- package/lib/compat/preset-reach.js +5 -2
- package/lib/context-inject-frame.js +13 -8
- package/lib/hub.js +104 -11
- package/lib/index.js +72 -24
- package/lib/mcp/manager.js +45 -13
- package/lib/mcp/secret-guard.js +2 -2
- package/lib/mcp/state-section.js +1 -1
- package/lib/memories/archive-engine.js +4 -5
- package/lib/memories/constants.js +11 -4
- package/lib/memories/service.js +6 -1
- package/lib/op-registry.js +13 -11
- package/lib/ops/memory.js +42 -4
- package/lib/ops/scene-records.js +43 -9
- package/lib/ops/sessions.js +67 -46
- package/lib/ops/trash.js +107 -8
- package/lib/request-gate.js +9 -2
- package/lib/scene-prompt-sync.js +48 -23
- package/lib/sessions/history.js +32 -22
- package/lib/sessions/workspace.js +0 -10
- package/lib/skills/catalog.js +17 -5
- package/lib/skills/core.js +189 -7
- package/lib/skills/frontmatter.js +31 -0
- package/lib/skills/manager-state.js +73 -3
- package/lib/skills/service.js +53 -13
- package/lib/skills/skill-roots.js +54 -4
- package/lib/skills/skill-trash.js +118 -12
- package/lib/skills/upload-pipeline.js +110 -26
- package/lib/subagents/catalog.js +9 -5
- package/lib/subagents/service.js +44 -6
- package/lib/subagents/tools.js +45 -15
- package/lib/tool-table-switch.js +6 -6
- package/lib/tools/deps.js +43 -0
- package/lib/tools/mcp.js +74 -27
- package/lib/tools/memory.js +49 -9
- package/lib/tools/prompt.js +38 -9
- package/lib/tools/remove.js +206 -0
- package/lib/tools/scene.js +155 -32
- package/lib/tools/skills.js +162 -55
- package/lib/tools/subagent.js +65 -31
- package/lib/tools/table.js +39 -12
- package/lib/trash-id.js +16 -0
- package/lib/trash-retention.js +3 -3
- package/package.json +115 -114
- package/lib/mcp/inspect-probe.js +0 -199
|
@@ -468,10 +468,13 @@ export function reachNoticeFor(presetId, facts, inject) {
|
|
|
468
468
|
const parts = [];
|
|
469
469
|
const domainOff = (key) => inject?.domains?.[key] === false;
|
|
470
470
|
if (isSuppressingPreset(facts) && inject?.underSuppressingPresets !== true) {
|
|
471
|
+
// 末尾那句「(想让它在这类预设下也注入:插件的「兼容」页 → 注入。)」已删(2026-10-15 用户裁定):
|
|
472
|
+
// 它是**给人看的操作路径**(界面怎么点),而这段文字进的是**工具回执** —— 回执留在 transcript 里,
|
|
473
|
+
// 比工具 schema 还贵(每次调用都会带一份)。判据与上一轮 P1-5 从场景锁定守卫里清掉
|
|
474
|
+
// 「去界面解锁」逐字同一条:守卫/提示只**陈述事实**,"怎么改"由模型自己向用户解释。
|
|
471
475
|
parts.push(`预设「${presetId}」声明只要它自己的文本(persona complete / 关闭运行时上下文):` +
|
|
472
476
|
'本插件注入的 —— 场景、记忆、MCP、技能、子智能体、提示词 —— 默认不注入,' +
|
|
473
|
-
'需要时用对应的 list / read 工具按需读取;不要假设你已经看到它们。'
|
|
474
|
-
'(想让它在这类预设下也注入:插件的「兼容」页 → 注入。)');
|
|
477
|
+
'需要时用对应的 list / read 工具按需读取;不要假设你已经看到它们。');
|
|
475
478
|
}
|
|
476
479
|
else {
|
|
477
480
|
if (facts.agentInstructions !== 'mounted' && domainOff('prompt')) {
|
|
@@ -26,22 +26,22 @@ export const DOMAIN_FRAME = {
|
|
|
26
26
|
// 现在这一段只回答一个问题:**当前处在哪个场景、它是什么**(`**「代码」—— 写代码**`)。
|
|
27
27
|
// 它还比别的段少一层:因果句也删了 —— 它解释的是"另外四段是怎么产生的"(机制),
|
|
28
28
|
// 不是当前情况本身,而且"清单里没有 ≠ 本机没有"这层反推被用户明确判为浪费。
|
|
29
|
-
title: '
|
|
29
|
+
title: '当前场景',
|
|
30
30
|
supersede: SUPERSEDE_NOTE,
|
|
31
31
|
},
|
|
32
32
|
memory: {
|
|
33
|
-
title: '
|
|
33
|
+
title: '当前记忆',
|
|
34
34
|
supersede: SUPERSEDE_NOTE,
|
|
35
35
|
},
|
|
36
36
|
mcp: {
|
|
37
|
-
title: '
|
|
37
|
+
title: '当前 MCP 服务器',
|
|
38
38
|
// how 只剩"怎么用备注"这半句:前半个分句「工具名是 `mcp__<服务器>__<工具>`」删掉了
|
|
39
39
|
// —— 模型自己的工具表里就是这个命名(`mcp__context7__xxx`),告诉它格式是零信息量。
|
|
40
40
|
how: '带「用户提示:」的行是用户写给这台服务器的决策提示,选服务器之前先看一眼。',
|
|
41
41
|
supersede: SUPERSEDE_NOTE,
|
|
42
42
|
},
|
|
43
43
|
skills: {
|
|
44
|
-
title: '
|
|
44
|
+
title: '当前技能目录',
|
|
45
45
|
// 两句都只在预设没挂官方 `skill` 工具时出现,差别只在点名不点名那个取正文的工具
|
|
46
46
|
// (工具被用户在兼容页关掉时不点名 —— 点名一个模型手里没有的工具只会让它去猜名字)。
|
|
47
47
|
how: (toolHidden) => toolHidden('skill_manager_read')
|
|
@@ -50,20 +50,25 @@ export const DOMAIN_FRAME = {
|
|
|
50
50
|
supersede: `${SUPERSEDE_NOTE.slice(0, -1)};只列当前可调用的技能。`,
|
|
51
51
|
},
|
|
52
52
|
subagents: {
|
|
53
|
-
title: '
|
|
53
|
+
title: '当前子智能体',
|
|
54
54
|
// 分界规则(2026-09-17 方案 C,本机实测 session-ee722e23 逼出来的):官方那两个
|
|
55
55
|
// 委派工具(`subagent` / `subagent_fork`)不带人设,而此前没有任何一句话说明何时该
|
|
56
56
|
// 用谁 —— 模型在"审查刚读过的 README"时选了 `subagent_fork`(fork 能继承已读内容、
|
|
57
57
|
// 省一次复述)。现在人设通道也有 `inherit`(同一套 fork 机制),分界只剩"要不要后台跑"。
|
|
58
58
|
// 带人设的委派工具被关掉时改说"本会话没有这条通道":不说的话,模型看到有人设目录却
|
|
59
59
|
// 找不到对应的工具,会去拿官方那两个凑(它们不接受人设,等于白跑一趟)。
|
|
60
|
+
//
|
|
61
|
+
// `inherit` 那半句 2026-10-14 删掉了(用户裁定):它在 `subagent_manager_run` 的
|
|
62
|
+
// Modes 段与 `inherit` 参数说明里**各有一份**,三份都在每轮上下文里 = 同一件事付三次
|
|
63
|
+
// token。留这两份(它们就在"要传这个参数"的那一刻),这里只留 how 行独有的那半句 ——
|
|
64
|
+
// 与官方两个委派工具的分界规则。
|
|
60
65
|
how: (toolHidden) => toolHidden('subagent_manager_run')
|
|
61
66
|
? '本会话没有带人设的委派工具;官方 `subagent` / `subagent_fork` 不带人设,只在没有人设贴合、或要后台跑时用。'
|
|
62
|
-
: '贴合人设的任务一律用 `subagent_manager_run
|
|
67
|
+
: '贴合人设的任务一律用 `subagent_manager_run`;官方 `subagent` / `subagent_fork` 不带人设,只在没有人设贴合、或要后台跑时用。',
|
|
63
68
|
supersede: SUPERSEDE_NOTE,
|
|
64
69
|
},
|
|
65
70
|
prompt: {
|
|
66
|
-
title: '
|
|
71
|
+
title: '当前提示词',
|
|
67
72
|
supersede: SUPERSEDE_NOTE,
|
|
68
73
|
},
|
|
69
74
|
};
|
|
@@ -82,7 +87,7 @@ export const DOMAIN_FRAME = {
|
|
|
82
87
|
export const HOW_NAMED_TOOLS = ['skill_manager_read', 'subagent_manager_run'];
|
|
83
88
|
/** 域声明里没登记的 key(理论上到不了这里):给一个不出错的通用框架。 */
|
|
84
89
|
export const fallbackFrame = (label) => ({
|
|
85
|
-
title:
|
|
90
|
+
title: `当前的${label}`,
|
|
86
91
|
supersede: SUPERSEDE_NOTE,
|
|
87
92
|
});
|
|
88
93
|
export const domainFrame = (key, label) => DOMAIN_FRAME[key] ?? fallbackFrame(label);
|
package/lib/hub.js
CHANGED
|
@@ -24,13 +24,14 @@
|
|
|
24
24
|
// ├─ mcp-disabled-tools.json | mcp-known-tools.json | mcp-notes.json | mcp-settings.json | mcp-export.json
|
|
25
25
|
// ├─ tool-management.log 插件日志(滚动 .1)
|
|
26
26
|
// ├─ backups/cordis.patch.yml.bak-<时间戳> 改宿主 patch 前的备份
|
|
27
|
-
// ├─ trash/{skills,subagents,prompts,scenes}-trash/<id>/
|
|
28
|
-
// └─ memories-trash/<id>/ 记忆回收站(**hub 根下独立目录**)
|
|
27
|
+
// ├─ trash/{skills,subagents,prompts,scenes,memories}-trash/<id>/ 回收站(六类)
|
|
29
28
|
//
|
|
30
|
-
//
|
|
31
|
-
//
|
|
32
|
-
//
|
|
33
|
-
//
|
|
29
|
+
// 六类回收站的落点已统一在 `trash/` 下(2026-10-10 起;记忆此前在 hub 根的 `memories-trash/`,
|
|
30
|
+
// 由启动迁移并入 —— 见 `HUB_RENAMES`)。
|
|
31
|
+
// 但**记忆那一类仍不经本模块的 `moveToTrash`**:它走 `memories/service.ts` 自己的路径
|
|
32
|
+
// (`join(stateDir, 'trash/memories-trash', id)`,stateDir = hub 根),manifest 形状也由记忆域
|
|
33
|
+
// 自己定。本模块只保证**目录位置**与其余几类一致;把它的读写抄到这里来只会得到第二份会漂移的
|
|
34
|
+
// 真相(本条注释的前身就是这么写错的:它把记忆画进 `trash/{…}` 里,而当时那里根本没有它)。
|
|
34
35
|
//
|
|
35
36
|
// 留在 `$DSH_HOME` 根下的两个文件**不是**插件的:`AGENTS.md`(宿主每轮读取的全局基线,
|
|
36
37
|
// 插件只是按场景/预设写它)与 `cordis.patch.yml`(宿主加载插件的配置入口)。
|
|
@@ -42,8 +43,8 @@
|
|
|
42
43
|
// 3. **每进程一次**——迁移是启动期的一次性动作,失败也不阻断服务(旧目录仍可读)。
|
|
43
44
|
import { cp, lstat, mkdir, readdir, readFile, rename, rm, writeFile } from 'node:fs/promises';
|
|
44
45
|
import { cpSync, existsSync, mkdirSync, readdirSync, renameSync, rmSync } from 'node:fs';
|
|
45
|
-
import { randomUUID } from 'node:crypto';
|
|
46
46
|
import { basename, dirname, join } from 'node:path';
|
|
47
|
+
import { newTrashId } from './trash-id.js';
|
|
47
48
|
import { resolveDshHome } from './skills/core.js';
|
|
48
49
|
import { isSameOrDescendant } from './paths.js';
|
|
49
50
|
/** hub 根目录名(`$DSH_HOME` 下)。 */
|
|
@@ -68,6 +69,11 @@ const HUB_RENAMES = [
|
|
|
68
69
|
['agents-md', 'prompts'],
|
|
69
70
|
['rules-index.json', 'memories-index.json'],
|
|
70
71
|
['rules-trash', 'memories-trash'],
|
|
72
|
+
// 记忆回收站并入 `trash/`,与其余五类同处一处(2026-10-10)。
|
|
73
|
+
// **必须排在上一行之后**:极老的 `rules-trash/` 要先改名成 `memories-trash/`,这一行才能
|
|
74
|
+
// 把它一并搬进 `trash/`。顺序颠倒的话,老用户那份会留在 hub 根下不动 —— 而它恰恰是条目数
|
|
75
|
+
// 最多的一类数据。
|
|
76
|
+
['memories-trash', 'trash/memories-trash'],
|
|
71
77
|
['agents', 'subagents'],
|
|
72
78
|
['agents-index.json', 'subagents-index.json'],
|
|
73
79
|
['state.json', 'skills-state.json'],
|
|
@@ -252,10 +258,13 @@ const LEGACY_TRASH_KINDS = {
|
|
|
252
258
|
export function trashKindDir(kind) {
|
|
253
259
|
return hubPath('trash', `${kind}-trash`);
|
|
254
260
|
}
|
|
255
|
-
/**
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
261
|
+
/**
|
|
262
|
+
* 新条目 id(时间戳 base36 + uuid 前 8 位,与记忆回收站同形)。
|
|
263
|
+
*
|
|
264
|
+
* 实现挪到 `./trash-id.ts`(2026-10-15):本模块引了 `skills/core.js`,技能域要共用这个生成器
|
|
265
|
+
* 就不能再反过来引这里。这里保留同一条导出路径 —— `memories/service.ts` 等既有引方不动。
|
|
266
|
+
*/
|
|
267
|
+
export { newTrashId };
|
|
259
268
|
/** id 只由本模块生成:严格白名单,杜绝路径穿越。 */
|
|
260
269
|
export function isValidTrashId(id) {
|
|
261
270
|
return /^[a-z0-9]+-[a-z0-9]{1,32}$/i.test(id);
|
|
@@ -266,6 +275,90 @@ export function trashEntryPath(kind, id) {
|
|
|
266
275
|
return null;
|
|
267
276
|
return join(trashKindDir(kind), id);
|
|
268
277
|
}
|
|
278
|
+
/** 清单类文件名:它们是插件自己的元数据,不是"删掉的那份东西",不进详情。 */
|
|
279
|
+
const TRASH_META_NAMES = new Set(['manifest.json', 'metadata.json']);
|
|
280
|
+
/** 详情上限:单个文件、总字节、文件数、递归深度。超了就如实标 `truncated`,绝不悄悄截。 */
|
|
281
|
+
const PREVIEW_MAX_FILE = 64 * 1024;
|
|
282
|
+
const PREVIEW_MAX_TOTAL = 256 * 1024;
|
|
283
|
+
const PREVIEW_MAX_FILES = 40;
|
|
284
|
+
const PREVIEW_MAX_DEPTH = 4;
|
|
285
|
+
/** 前 8 KiB 里出现 NUL 就按二进制处理 —— 二进制内容转成字符串只会是一屏乱码。 */
|
|
286
|
+
function looksBinary(buf) {
|
|
287
|
+
const n = Math.min(buf.length, 8192);
|
|
288
|
+
for (let i = 0; i < n; i += 1)
|
|
289
|
+
if (buf[i] === 0)
|
|
290
|
+
return true;
|
|
291
|
+
return false;
|
|
292
|
+
}
|
|
293
|
+
/**
|
|
294
|
+
* 把一条回收站条目目录里的负载**只读**地读出来,供界面「详情」显示。
|
|
295
|
+
*
|
|
296
|
+
* 为什么放在这里而不是各域各写一遍:三个域的回收站落在三套不同的目录布局里
|
|
297
|
+
* (hub 的四类 `<kind>-trash`、记忆的 `memories-trash`、技能的 `skills-trash`),
|
|
298
|
+
* 但"把一个目录里的文件读成能看的文本"这件事完全一样。上限、二进制判据、哪些名字算清单
|
|
299
|
+
* 只写一遍 —— 否则迟早出现"某一域的详情能读到别的东西"。
|
|
300
|
+
*
|
|
301
|
+
* 只读、不落盘、不跟随符号链接(条目里出现链接一律跳过,别把根外的文件读出来给界面看)。
|
|
302
|
+
* 目录不存在 / 读不动 → 返回空数组,调用方据此报"内容已不可读",不抛。
|
|
303
|
+
*/
|
|
304
|
+
export async function readTrashPayloadFiles(dir, options = {}) {
|
|
305
|
+
const out = [];
|
|
306
|
+
let total = 0;
|
|
307
|
+
const maxDepth = Number.isFinite(Number(options.maxDepth)) ? Number(options.maxDepth) : PREVIEW_MAX_DEPTH;
|
|
308
|
+
const walk = async (current, rel, depth) => {
|
|
309
|
+
if (out.length >= PREVIEW_MAX_FILES || depth > maxDepth)
|
|
310
|
+
return;
|
|
311
|
+
let entries;
|
|
312
|
+
try {
|
|
313
|
+
entries = await readdir(current, { withFileTypes: true });
|
|
314
|
+
}
|
|
315
|
+
catch {
|
|
316
|
+
return;
|
|
317
|
+
}
|
|
318
|
+
// 目录在前、文件在后;同层按名字排序 —— 让详情里的顺序稳定(不随文件系统枚举顺序变)。
|
|
319
|
+
const sorted = [...entries].sort((a, b) => {
|
|
320
|
+
if (a.isDirectory() !== b.isDirectory())
|
|
321
|
+
return a.isDirectory() ? -1 : 1;
|
|
322
|
+
return a.name.localeCompare(b.name);
|
|
323
|
+
});
|
|
324
|
+
for (const entry of sorted) {
|
|
325
|
+
if (out.length >= PREVIEW_MAX_FILES)
|
|
326
|
+
return;
|
|
327
|
+
const abs = join(current, entry.name);
|
|
328
|
+
const nextRel = rel ? `${rel}/${entry.name}` : entry.name;
|
|
329
|
+
if (entry.isSymbolicLink())
|
|
330
|
+
continue;
|
|
331
|
+
if (entry.isDirectory()) {
|
|
332
|
+
await walk(abs, nextRel, depth + 1);
|
|
333
|
+
continue;
|
|
334
|
+
}
|
|
335
|
+
if (!entry.isFile() || TRASH_META_NAMES.has(entry.name))
|
|
336
|
+
continue;
|
|
337
|
+
let buf;
|
|
338
|
+
try {
|
|
339
|
+
buf = await readFile(abs);
|
|
340
|
+
}
|
|
341
|
+
catch {
|
|
342
|
+
continue;
|
|
343
|
+
}
|
|
344
|
+
const file = { path: nextRel, size: buf.length };
|
|
345
|
+
if (looksBinary(buf)) {
|
|
346
|
+
file.binary = true;
|
|
347
|
+
}
|
|
348
|
+
else {
|
|
349
|
+
const room = Math.max(0, Math.min(PREVIEW_MAX_FILE, PREVIEW_MAX_TOTAL - total));
|
|
350
|
+
const slice = buf.subarray(0, room);
|
|
351
|
+
file.text = slice.toString('utf8');
|
|
352
|
+
if (slice.length < buf.length)
|
|
353
|
+
file.truncated = true;
|
|
354
|
+
total += slice.length;
|
|
355
|
+
}
|
|
356
|
+
out.push(file);
|
|
357
|
+
}
|
|
358
|
+
};
|
|
359
|
+
await walk(dir, '', 0);
|
|
360
|
+
return out;
|
|
361
|
+
}
|
|
269
362
|
async function readManifest(kind, id) {
|
|
270
363
|
const dir = trashEntryPath(kind, id);
|
|
271
364
|
if (dir === null)
|
package/lib/index.js
CHANGED
|
@@ -16,7 +16,7 @@ import { createRequire } from 'node:module';
|
|
|
16
16
|
import { randomUUID } from 'node:crypto';
|
|
17
17
|
import { createHistoryDomain } from './sessions/history.js';
|
|
18
18
|
import { createSkillsService, pluginLog } from './skills/service.js';
|
|
19
|
-
import { renameWithRetry } from './skills/core.js';
|
|
19
|
+
import { renameWithRetry, trashRootPath } from './skills/core.js';
|
|
20
20
|
import { createPromptsService } from './prompts/service.js';
|
|
21
21
|
import { createQuickPromptsService } from './prompts/quick-service.js';
|
|
22
22
|
import { isInsideRootResolved } from './paths.js';
|
|
@@ -70,6 +70,7 @@ import { buildMcpTools } from './tools/mcp.js';
|
|
|
70
70
|
import { buildSkillTools } from './tools/skills.js';
|
|
71
71
|
import { buildPromptTools } from './tools/prompt.js';
|
|
72
72
|
import { buildMemoryTools } from './tools/memory.js';
|
|
73
|
+
import { buildRemoveTools } from './tools/remove.js';
|
|
73
74
|
import { buildSceneTools } from './tools/scene.js';
|
|
74
75
|
import { buildSubagentTools } from './tools/subagent.js';
|
|
75
76
|
import { createScenePromptSync } from './scene-prompt-sync.js';
|
|
@@ -206,6 +207,10 @@ export default {
|
|
|
206
207
|
memoriesRoot: String(config?.memoriesRoot || ''),
|
|
207
208
|
stateDir: String(config?.memoriesStateDir || ''),
|
|
208
209
|
scenesDir: String(config?.memoriesScenesDir || ''),
|
|
210
|
+
// 技能回收站根目录:只给回收站「详情」(`trash-preview`)读技能条目用。路径的唯一定义
|
|
211
|
+
// 在 skills/skill-roots.ts(`trashRootPath()`),这里原样转手,不在记忆域重算一遍 ——
|
|
212
|
+
// 重算就是第二个真相源,改一处忘一处时详情会指向一个空目录。
|
|
213
|
+
skillsTrashDir: trashRootPath(),
|
|
209
214
|
// 场景记忆段预算(字节),默认 131072(= DEFAULT_MAX_BYTES,128 KiB);仅用于测试与特殊部署调优。
|
|
210
215
|
...(Number.isFinite(Number(config?.rulesMaxBytes))
|
|
211
216
|
? { maxBytes: Number(config.rulesMaxBytes) }
|
|
@@ -628,7 +633,7 @@ export default {
|
|
|
628
633
|
return out;
|
|
629
634
|
};
|
|
630
635
|
/**
|
|
631
|
-
*
|
|
636
|
+
* 这条工具**在这个 agent 手里吗** —— 注入侧(`how` 行、两个目录的尾句)与工具回执共用的一份。
|
|
632
637
|
*
|
|
633
638
|
* 两条路并起来,缺一条框架文本就会说谎:
|
|
634
639
|
* ① `toolTableHidden()` —— 兼容页「模型工具表」的全局开关。**必须留着**:它读的是设置
|
|
@@ -639,21 +644,33 @@ export default {
|
|
|
639
644
|
* scope 上(`dsh-subagent/lib/index.js:522`),全局开关表里看不到它 —— 于是人设砍掉
|
|
640
645
|
* `skill_manager_read` 之后,技能目录照注、`how` 行照样点名一个模型手里没有的工具。
|
|
641
646
|
* 拿不到 agent 时跳过:`get(name, undefined)` 问的是**全局视图**,会得出相反结论。
|
|
647
|
+
*
|
|
648
|
+
* 为什么要有这一份(2026-10-14 对抗性审查 P0-1/P1-8):此前只有全局开关一条路,于是
|
|
649
|
+
* `mcp_manager_switch` 的 `listHint`、技能目录与子智能体目录的尾句都会点名一条**这个模型
|
|
650
|
+
* 手里没有的**工具。它是这两处的唯一判据 —— 工具侧经 `toolVisibleFor` 依赖拿,注入侧经
|
|
651
|
+
* `injectHiddenToolsFor` 拿,同一个函数,不定义第二份。
|
|
642
652
|
*/
|
|
643
|
-
const
|
|
644
|
-
|
|
653
|
+
const toolVisibleFor = (name, agent) => {
|
|
654
|
+
if (toolTableHidden().has(name))
|
|
655
|
+
return false;
|
|
645
656
|
if (agent === null || (typeof agent !== 'object' && typeof agent !== 'function'))
|
|
646
|
-
return
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
if (hidden.has(name))
|
|
650
|
-
continue;
|
|
651
|
-
try {
|
|
652
|
-
if (tools.get(name, scope) === undefined)
|
|
653
|
-
hidden.add(name);
|
|
654
|
-
}
|
|
655
|
-
catch { /* 读不到 = 按还在手里算 */ }
|
|
657
|
+
return true;
|
|
658
|
+
try {
|
|
659
|
+
return tools.get(name, agent) !== undefined;
|
|
656
660
|
}
|
|
661
|
+
catch {
|
|
662
|
+
return true; /* 读不到 = 按还在手里算 */
|
|
663
|
+
}
|
|
664
|
+
};
|
|
665
|
+
/**
|
|
666
|
+
* `how` 行点名过的工具里,**这个 agent 手里没有**的那些(注入器每步同步问一次)。
|
|
667
|
+
* 判据就是上面那份 `toolVisibleFor`,这里只把它收成一份集合交给注入框架。
|
|
668
|
+
*/
|
|
669
|
+
const injectHiddenToolsFor = (agent) => {
|
|
670
|
+
const hidden = new Set(toolTableHidden());
|
|
671
|
+
for (const name of HOW_NAMED_TOOLS)
|
|
672
|
+
if (!toolVisibleFor(name, agent))
|
|
673
|
+
hidden.add(name);
|
|
657
674
|
return hidden;
|
|
658
675
|
};
|
|
659
676
|
// ---------- 注入通道(场景 / 记忆 / MCP / 技能 / 子智能体 / 提示词,各一条消息)----------
|
|
@@ -670,8 +687,10 @@ export default {
|
|
|
670
687
|
const subagentCatalog = createSubagentCatalog({
|
|
671
688
|
list: () => subagentService.list(),
|
|
672
689
|
sceneLists: subagentSceneLists,
|
|
673
|
-
// 人在「兼容」页把 `subagent_manager_list`
|
|
674
|
-
|
|
690
|
+
// 人在「兼容」页把 `subagent_manager_list` 关掉、或某个人设用 `toolsDeny` 砍掉它之后,
|
|
691
|
+
// 截断提示里那句「用 X 查」就是假的。判据按 **agent**(2026-10-14 对抗性审查 P1-8):
|
|
692
|
+
// 与 `how` 行、MCP 回执同一份 `toolVisibleFor`,不再只看全局开关。
|
|
693
|
+
listToolVisible: (agent) => toolVisibleFor('subagent_manager_list', agent),
|
|
675
694
|
});
|
|
676
695
|
// ⚠️ 取数必须用 mcpmRowsWithNotes(false):true 会带上**未打码**的 url/headers/env(含明文密钥)。
|
|
677
696
|
// 技能目录与提示词两个域是**官方载体的兜底**:预设挂得到官方
|
|
@@ -679,7 +698,9 @@ export default {
|
|
|
679
698
|
// CARRIER_FACT_OF),挂不到(极简)时才由这里送 —— 所以极简下也能按开关决定要不要。
|
|
680
699
|
const skillCatalog = createSkillCatalog({
|
|
681
700
|
state: () => skillsService.ops['skill-state']({}),
|
|
682
|
-
|
|
701
|
+
// 与子智能体目录同一份判据(2026-10-14 对抗性审查 P1-8):只看全局开关时,人设用
|
|
702
|
+
// `toolsDeny` 砍掉 `skill_manager_list` 之后,技能目录尾句照样点名一条它手里没有的工具。
|
|
703
|
+
listToolVisible: (agent) => toolVisibleFor('skill_manager_list', agent),
|
|
683
704
|
});
|
|
684
705
|
// 三份界面设置侧车(inject / scene / slash)拆到 ./settings-sidecars.ts:读走 3 秒 TTL 缓存,
|
|
685
706
|
// 写共用上面那条串行写锁。工厂只在这里调用一次,三份缓存仍是「每次 apply 一份」。
|
|
@@ -729,7 +750,7 @@ export default {
|
|
|
729
750
|
// 记忆(**内容**):各场景下的条目。与场景段同源、同一条 `applicableTo`。
|
|
730
751
|
{ key: 'memory', name: 'tool-management:memory', label: '记忆', form: 'snapshot', text: () => memoriesService.memoryText(), applicableTo: (agent) => subagentDepthOf(agent) === 0 },
|
|
731
752
|
{ key: 'mcp', name: 'tool-management:mcp-state', label: 'MCP 服务器', form: 'catalog', text: () => mcp.stateCatalog.text() },
|
|
732
|
-
{ key: 'skills', name: 'tool-management:skill-catalog', label: '技能目录', form: 'catalog', text: () => skillCatalog.text() },
|
|
753
|
+
{ key: 'skills', name: 'tool-management:skill-catalog', label: '技能目录', form: 'catalog', text: (agent) => skillCatalog.text(agent) },
|
|
733
754
|
{
|
|
734
755
|
key: 'subagents',
|
|
735
756
|
name: 'tool-management:subagents',
|
|
@@ -742,7 +763,7 @@ export default {
|
|
|
742
763
|
],
|
|
743
764
|
settings: () => injectSettingsSync(),
|
|
744
765
|
// 工具表开关:`how` 行里点名工具的那几句要跟着它换话术(关掉的不点名)。
|
|
745
|
-
hiddenTools:
|
|
766
|
+
hiddenTools: injectHiddenToolsFor,
|
|
746
767
|
factsFor: (agent) => candidates.presetFactsForAgent(agent),
|
|
747
768
|
// 令牌门禁(2026-09-19,用户裁定「宿主侧硬拦截」):令牌**在生效**(`TOKEN !== ''`,
|
|
748
769
|
// 关掉或没配都是空串)而本次启动还没有人验过 ⇒ 这一步不放行,宿主把 turn 收成
|
|
@@ -882,8 +903,8 @@ export default {
|
|
|
882
903
|
// 启动那次清扫读完侧车就返回了,一次磁盘遍历都不做。
|
|
883
904
|
//
|
|
884
905
|
// 记忆回收站那两条**走 memories 域的 op**,不在这里重抄路径与 manifest 形状:它在
|
|
885
|
-
// `hub/memories-trash/`(
|
|
886
|
-
//
|
|
906
|
+
// `hub/trash/memories-trash/`(2026-10-10 起与其余五类同处一处,之前是 hub 根下的独立
|
|
907
|
+
// 目录),形状由 memories 域自己定 —— 重抄一遍只会得到第二份会漂移的真相。
|
|
887
908
|
const trashRetention = createTrashRetentionDomain({
|
|
888
909
|
ctx,
|
|
889
910
|
config,
|
|
@@ -1502,7 +1523,7 @@ export default {
|
|
|
1502
1523
|
// 清空流水。清完这一条 op 自己会被下面的包装层记进新流水("谁清过"也留痕)。
|
|
1503
1524
|
'audit-clear': async () => await clearAudit(),
|
|
1504
1525
|
'skill-open': skillOpen,
|
|
1505
|
-
// MCP 域
|
|
1526
|
+
// MCP 域 17 个 op(实现在 ./mcp/manager.ts)
|
|
1506
1527
|
...mcp.ops,
|
|
1507
1528
|
// 技能管理 ops(由 ./skills/service.js 提供):skill-state / skill-detail /
|
|
1508
1529
|
// skill-browse / skill-enable / skill-disable / skill-source-enable /
|
|
@@ -1554,7 +1575,7 @@ export default {
|
|
|
1554
1575
|
// -toggle / -remove 与回收站三条。它**不**碰 AGENTS.md、不进场景、不进注入。
|
|
1555
1576
|
...quickPromptOps,
|
|
1556
1577
|
// 归档会话域(ops/history.ts 提供):history-list / history-sessions /
|
|
1557
|
-
// history-export-defaults / dir-list /
|
|
1578
|
+
// history-export-defaults / dir-list /
|
|
1558
1579
|
// history-unarchive / history-delete / history-unarchive-batch / history-delete-batch /
|
|
1559
1580
|
// history-workspace-register / history-import / bundle-export / history-export /
|
|
1560
1581
|
// history-retention-get / history-retention-set。
|
|
@@ -2175,6 +2196,10 @@ export default {
|
|
|
2175
2196
|
syncSwitchToScene,
|
|
2176
2197
|
// 错误句里点名工具前要先问一句(判据与两个目录的 `listToolVisible` 同一份缓存)。
|
|
2177
2198
|
toolVisible: (name) => !toolTableHidden().has(name),
|
|
2199
|
+
// 同上一句,但把**该 agent 的工具限制**也算进去(2026-10-14 对抗性审查 P0-1/P1-8):
|
|
2200
|
+
// 人设用 `toolsDeny` 砍掉 `mcp_manager_list` 之后,只看全局开关就会让回执点名一条
|
|
2201
|
+
// 这个模型手里没有的工具。与注入侧 `injectHiddenToolsFor` 同一个函数。
|
|
2202
|
+
toolVisibleFor: (name, agent) => toolVisibleFor(name, agent),
|
|
2178
2203
|
reachNoticeForAgent,
|
|
2179
2204
|
presetRoster: candidates.presetRoster,
|
|
2180
2205
|
injectNoticeOptions,
|
|
@@ -2208,7 +2233,15 @@ export default {
|
|
|
2208
2233
|
const denied = await frozenGate('agentsmd-apply', { id });
|
|
2209
2234
|
return denied ? { ok: false, error: denied } : applyPresetGuarded(id);
|
|
2210
2235
|
};
|
|
2211
|
-
|
|
2236
|
+
// 取消应用同理:`agentsmd-unapply` 在冻结清单里,模型侧不经 op 表直调就要在这里显式过
|
|
2237
|
+
// 冻结闸(与 apply 同一形状)—— 少了这一层,锁着场景时模型仍能撤掉当前基线。
|
|
2238
|
+
const unapplyPresetForModel = async () => {
|
|
2239
|
+
const denied = await frozenGate('agentsmd-unapply', {});
|
|
2240
|
+
return denied
|
|
2241
|
+
? { ok: false, error: denied }
|
|
2242
|
+
: promptOps['agentsmd-unapply']();
|
|
2243
|
+
};
|
|
2244
|
+
buildPromptTools({ ...toolDeps, promptOps: auditOps(promptOps), applyPresetGuarded: applyPresetForModel, unapplyPreset: unapplyPresetForModel, promptsDir });
|
|
2212
2245
|
buildMemoryTools({ ...toolDeps, rulesOps: auditOps(memoriesService.ops) });
|
|
2213
2246
|
// 场景族(tools/scene.ts):列场景与档案(`_list`)+ 建场景 / 写档案 / 绑提示词(`_save`)
|
|
2214
2247
|
// + 进入与退出(`_switch`)。
|
|
@@ -2233,6 +2266,21 @@ export default {
|
|
|
2233
2266
|
toolFilterFor: subagentToolFilterFor,
|
|
2234
2267
|
failures: subagentFailures,
|
|
2235
2268
|
});
|
|
2269
|
+
// 删除 / 回收站族(tools/remove.ts,2026-10-15):五个域各一条 `*_manager_remove`。
|
|
2270
|
+
// 此前模型只能停用、不能删除。删除一律进回收站(可恢复);**不做永久删除**(低频且
|
|
2271
|
+
// 不可恢复,留给面板)。`sceneRemove` 传 handlers 里那份(`withAgentsMdSync` 包过),
|
|
2272
|
+
// 与 `sceneActivate` 同一条纪律:直调 `memoriesService.ops` 会静默漏掉 AGENTS.md 那一步。
|
|
2273
|
+
buildRemoveTools({
|
|
2274
|
+
...toolDeps,
|
|
2275
|
+
memoryOps: auditOps(memoriesService.ops),
|
|
2276
|
+
skillOps: auditOps(skillsService.ops),
|
|
2277
|
+
subagentOps: auditOps(subagentService.ops),
|
|
2278
|
+
promptOps: auditOps(promptOps),
|
|
2279
|
+
// 场景回收站(`scene-trash-list` / `scene-trash-restore`)在 **memoriesService** 的 op 表里
|
|
2280
|
+
// (`archiveService` 只有 scene-mode-get / -set / scene-archive-save 三条)。
|
|
2281
|
+
sceneOps: auditOps(memoriesService.ops),
|
|
2282
|
+
sceneRemove: auditFn('rules-remove-scene', (args) => handlers['rules-remove-scene'](args)),
|
|
2283
|
+
});
|
|
2236
2284
|
if (typeof ctx.on === 'function') {
|
|
2237
2285
|
// 三个确认门的统一前裁决。口径(用户裁定,方案 B):会话审批策略为 never(「完全权限」预设)
|
|
2238
2286
|
// = 用户已在预设层面预先批准一切确认门,**直接放行**并写审计日志留痕。
|
package/lib/mcp/manager.js
CHANGED
|
@@ -18,11 +18,11 @@
|
|
|
18
18
|
// 其余 14 项(ensurePaths / withWriteLock / readPatch / pluginInventory / memoriesService 等)
|
|
19
19
|
// 由 deps 显式传入。
|
|
20
20
|
import { clearRuntimeNote, noteRuntime } from '../compat/runtime-notes.js';
|
|
21
|
+
import { mkdir } from 'node:fs/promises';
|
|
22
|
+
import { join } from 'node:path';
|
|
21
23
|
import { MCP_CLIENT_MODULE, MCP_TOOL_PREFIX, ambiguousServerNames, looksLikeMcpPrefixDrift, serverNameCandidates, splitMcpToolName } from '../host-names.js';
|
|
22
24
|
import { hubPath } from '../hub.js';
|
|
23
25
|
import { planOverrideCompaction } from './override-blocks.js';
|
|
24
|
-
// 配置体检的探测(PATH 查询 / 端点连通性 / 检查清单 + mcpm-inspect op),2026-10-01 剥出。
|
|
25
|
-
import { createInspectProbe } from './inspect-probe.js';
|
|
26
26
|
import { createMcpStateCatalog } from './state-section.js';
|
|
27
27
|
import { createToolVisibility } from './tool-visibility.js';
|
|
28
28
|
import { appendBlock, buildDisableBlock, buildInsertBlock, insertEntryIds, parseRows, removeEntryAll, removeMarked, spliceRanges, splitLines, } from './patch-yaml.js';
|
|
@@ -106,6 +106,8 @@ export function isToolDisabledIn(map, fullName) {
|
|
|
106
106
|
return true;
|
|
107
107
|
return list.indexOf(parts.tool) >= 0;
|
|
108
108
|
}
|
|
109
|
+
/** `mcpm-parse-patch` 的输入上限:粘贴框不是文件上传口,几十 KB 之外基本都是误贴。 */
|
|
110
|
+
const PATCH_PARSE_MAX_CHARS = 200_000;
|
|
109
111
|
export function createMcpManager(deps) {
|
|
110
112
|
// 外部能力一次解构成局部名:块内代码是逐字搬来的,保持原样最不容易出错。
|
|
111
113
|
const { readJsonFile, writeJsonFile, ensurePaths, withWriteLock, message, tools, pluginInventory, settings, readPatch, writePatch, memoriesService, wait, } = deps;
|
|
@@ -1519,15 +1521,19 @@ export function createMcpManager(deps) {
|
|
|
1519
1521
|
return { ok: true, removed: total, ids: removed, files };
|
|
1520
1522
|
});
|
|
1521
1523
|
}
|
|
1522
|
-
// ---------- 配置体检的探测(0.15.0 B1;0.16.5 起含端点探测)----------------------
|
|
1523
|
-
// 正文 2026-10-01 整段搬到 ./inspect-probe.js(一字未改,只把闭包作用域的几个名字改走
|
|
1524
|
-
// deps)。那段「对补丁只读、`where`/`which` 只查命令名绝不执行配置里的命令、复用列表结果
|
|
1525
|
-
// 但不回写任何缓存」的口径连同理由一起搬过去了,见那边的文件头。
|
|
1526
|
-
const { mcpmInspect } = createInspectProbe({
|
|
1527
|
-
ensurePaths, readPatch, message, normalizeRow, mcpmList,
|
|
1528
|
-
});
|
|
1529
1524
|
// ---------- 配置的导出 / 导入 ----------
|
|
1530
|
-
|
|
1525
|
+
/**
|
|
1526
|
+
* `args.outDir`(可选):给了就写到该目录下的 `mcp-export.json`,不给则照旧写 hub 侧车。
|
|
1527
|
+
*
|
|
1528
|
+
* 判据与 `bundle-export` / `snapshot-export` 完全一致(必须绝对路径):相对路径会落到
|
|
1529
|
+
* 进程 CWD,那是猜。用户 2026-10-12:「mcp 的导出缺少选择导出目录」—— 此前这一档只能
|
|
1530
|
+
* 落到 `~/.dsh/tool-management/mcp-export.json`,界面上没有任何可改的出口。
|
|
1531
|
+
*
|
|
1532
|
+
* `args.dryRun`(可选):只算 JSON、**不落盘**。界面打开弹窗时先用它取预览,真正落盘
|
|
1533
|
+
* 由用户点「导出到目录」触发 —— 与其余四个导出弹窗同一套交互(不开弹窗就写文件是副作用)。
|
|
1534
|
+
* `snapshot-export` 走的是不带 dryRun 的老路径,行为不变。
|
|
1535
|
+
*/
|
|
1536
|
+
async function mcpmExport(args) {
|
|
1531
1537
|
const p = await ensurePaths();
|
|
1532
1538
|
const list = await mcpmList();
|
|
1533
1539
|
const rows = (list.rows || []).filter((r) => r.level !== 'loader').map((r) => ({
|
|
@@ -1544,16 +1550,24 @@ export function createMcpManager(deps) {
|
|
|
1544
1550
|
}));
|
|
1545
1551
|
const payload = { exportedAt: new Date().toISOString(), rows };
|
|
1546
1552
|
const json = JSON.stringify(payload, null, 2);
|
|
1553
|
+
if (args && args.dryRun)
|
|
1554
|
+
return { ok: true, json, savedTo: null };
|
|
1555
|
+
const outDir = String((args && args.outDir) || '').trim();
|
|
1556
|
+
if (outDir && !/^([A-Za-z]:[\\/]|\\\\|\/)/.test(outDir))
|
|
1557
|
+
return { ok: false, error: '导出目录需为绝对路径' };
|
|
1547
1558
|
let savedTo = null;
|
|
1548
1559
|
try {
|
|
1549
|
-
const abs = mcpSidecar(MCP_EXPORT_FILE);
|
|
1550
|
-
// 用 `writeJsonFile`(直写
|
|
1560
|
+
const abs = outDir ? join(outDir, MCP_EXPORT_FILE) : mcpSidecar(MCP_EXPORT_FILE);
|
|
1561
|
+
// 用 `writeJsonFile`(直写 JSON)而**不是** `writePatch`(2026-09-30 审查 P1-3):
|
|
1551
1562
|
// `writePatch` 是宿主**补丁文件**的写口,写前会跑 `checkPatchWrite` 的官方方言校验
|
|
1552
1563
|
// (顶层必须是 YAML 数组),而这里写的是 hub 里一份 JSON **对象** —— 校验必然失败、
|
|
1553
1564
|
// 异常被下面的 catch 吞掉,于是 `savedTo` 恒为 null,界面显示"没能落盘(hub 目录写入
|
|
1554
1565
|
// 失败)",**诊断是错的**(真实原因是方言校验器拒收 JSON 对象)。而且 `writePatch`
|
|
1555
1566
|
// 自己的调用面纪律(`index.ts`:「不写宿主配置的动作也不该调用它」)本就排除了它 ——
|
|
1556
1567
|
// 本处是唯一一个传 hub 路径的调用点。顺带省掉一次 `danger-full-access` 策略升级。
|
|
1568
|
+
// `writeJsonFile` 不建父目录(hub 路径总是已存在),所以自定义目录要自己 mkdir。
|
|
1569
|
+
if (outDir)
|
|
1570
|
+
await mkdir(outDir, { recursive: true });
|
|
1557
1571
|
await writeJsonFile(abs, payload);
|
|
1558
1572
|
savedTo = abs;
|
|
1559
1573
|
}
|
|
@@ -1722,6 +1736,24 @@ export function createMcpManager(deps) {
|
|
|
1722
1736
|
const notes = importNotes.length ? { warning: importNotes.join(';') } : {};
|
|
1723
1737
|
return overwrite ? { ok: true, added, overwritten, skipped, ...notes } : { ok: true, added, skipped, ...notes };
|
|
1724
1738
|
}
|
|
1739
|
+
/**
|
|
1740
|
+
* 补丁片段解析(0.20.1 导入弹窗):把用户粘贴的 dsh 补丁文本读成条目 —— 不读盘、不写盘。
|
|
1741
|
+
*
|
|
1742
|
+
* 为什么这件事在宿主侧:客户端那一半跑在浏览器里(`window.__ModuleLoader__`),那里的
|
|
1743
|
+
* `require` 只解析宿主注册过的浏览器模块,`js-yaml` 永远取不到 —— 界面自己解析 YAML 补丁
|
|
1744
|
+
* 是死路。这里走 `parseRows`:纯字符串运算,且正是本插件读真身补丁用的同一份口径(只收
|
|
1745
|
+
* `name === MCP_CLIENT_MODULE` 的 insert 子条目、覆盖块按文件顺序生效)。
|
|
1746
|
+
*/
|
|
1747
|
+
async function mcpmParsePatch(args) {
|
|
1748
|
+
const text = String((args && args.text) || '');
|
|
1749
|
+
if (!text.trim())
|
|
1750
|
+
return { ok: false, error: '没有内容' };
|
|
1751
|
+
if (text.length > PATCH_PARSE_MAX_CHARS) {
|
|
1752
|
+
return { ok: false, error: `内容过长(${text.length} 字符,上限 ${PATCH_PARSE_MAX_CHARS}):请只粘贴要导入的那几块` };
|
|
1753
|
+
}
|
|
1754
|
+
const rows = parseRows(text).rows.map((r) => ({ id: r.id, disabled: r.disabled, config: r.config }));
|
|
1755
|
+
return { ok: true, rows };
|
|
1756
|
+
}
|
|
1725
1757
|
function parseKv(text) {
|
|
1726
1758
|
const out = {};
|
|
1727
1759
|
String(text || '').split(/\r?\n/).forEach((line) => {
|
|
@@ -1792,7 +1824,7 @@ export function createMcpManager(deps) {
|
|
|
1792
1824
|
'mcpm-compact': mcpmCompact,
|
|
1793
1825
|
'mcpm-export': mcpmExport,
|
|
1794
1826
|
'mcpm-import': mcpmImport,
|
|
1795
|
-
'mcpm-
|
|
1827
|
+
'mcpm-parse-patch': mcpmParsePatch,
|
|
1796
1828
|
},
|
|
1797
1829
|
readDisabledTools,
|
|
1798
1830
|
readKnownMcpTools,
|
package/lib/mcp/secret-guard.js
CHANGED
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
// 「那几段」= **userinfo / 查询串 / 片段**三处(2026-09-30 审查 F5):`new URL()` 把 userinfo
|
|
16
16
|
// 与 fragment 存在 `username/password` 与 `hash` 里,`toString()` 原样带出。只换 `search`
|
|
17
17
|
// 的话,`https://user:pass@host/mcp`(Basic-Auth 形态)与 `https://host/mcp#access_token=…`
|
|
18
|
-
// 会整条原样返回 —— 而这两条路正是免令牌出口(`mcpm-list` /
|
|
18
|
+
// 会整条原样返回 —— 而这两条路正是免令牌出口(`mcpm-list` / 模型工具 /
|
|
19
19
|
// 快照导出)。路径与主机**不动**:它们是该服务的身份,算进来会把真 URL 误判成打码值。
|
|
20
20
|
//
|
|
21
21
|
// 判据为什么可以这么硬:打码值只可能表示"用户没改这一项",绝不可能表示"用户想把它设成这个"。
|
|
@@ -170,7 +170,7 @@ function maskUrlPartsByString(url) {
|
|
|
170
170
|
* fragment 存在 `username/password` 与 `hash` 里,`toString()` 会原样带出 ——
|
|
171
171
|
* 只替换 `search` 的话,`https://user:pass@host/mcp` 与 `https://host/mcp#access_token=…`
|
|
172
172
|
* 会**整条原样返回**(后者连 `?` 都没有),而这两条路正是免令牌出口
|
|
173
|
-
* (`mcpm-list` /
|
|
173
|
+
* (`mcpm-list` / 模型工具 / 快照导出)。Basic-Auth 形态的凭据就此明文可见。
|
|
174
174
|
*/
|
|
175
175
|
export function maskUrlQuery(url) {
|
|
176
176
|
if (!url)
|
package/lib/mcp/state-section.js
CHANGED
|
@@ -122,7 +122,7 @@ export function renderMcpStateSection(rows, opts = {}) {
|
|
|
122
122
|
...shownUsable.map((r) => lineOf(r, liveEnabledOf(r), false)),
|
|
123
123
|
...shownOffline.map((r) => lineOf(r, knownOf(r), true)),
|
|
124
124
|
];
|
|
125
|
-
// 只有清单:标题(`#
|
|
125
|
+
// 只有清单:标题(`# 当前 MCP 服务器`)由注入通道的框架承担
|
|
126
126
|
// (2026-09-18 起,见 context-inject.ts 的 DOMAIN_FRAME)—— 同一件事只有一个出处,
|
|
127
127
|
// 框架已经写了标题,正文再写一遍就是每步多付一行 token。
|
|
128
128
|
const out = [...lines];
|
|
@@ -449,10 +449,10 @@ export function createArchiveEngine(deps) {
|
|
|
449
449
|
catch (e) {
|
|
450
450
|
return { ok: false, error: `读取技能启停现状失败:${msg(e)}` };
|
|
451
451
|
}
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
452
|
+
// 来源级切换(skillsPlan.sourceSwitches)只做不说:它是技能启停的实现细节 ——
|
|
453
|
+
// 勾了技能的来源才启用、一条都没勾就停用。项目来源的 key 是路径哈希
|
|
454
|
+
// (`project-dsh:47d48f408c5edc5e`),目录对用户可能根本不存在、也不是新建技能的
|
|
455
|
+
// 落点,报上预览卡只会被当成故障(用户 2026-10-15 裁定:提醒不显示技能目录)。
|
|
456
456
|
const stale = [...mcpPlan.stale, ...skillsPlan.stale];
|
|
457
457
|
// 工具表方案(C1):绑了就说清「会切成哪一份、关几条」。方案在别处被删掉 / 换机器之后
|
|
458
458
|
// 查不到时 hiddenCount 给 null,界面那句会改成"方案已不存在,这次不会切换"——
|
|
@@ -498,7 +498,6 @@ export function createArchiveEngine(deps) {
|
|
|
498
498
|
enter: { servers: clip(enterServers) },
|
|
499
499
|
skills: {
|
|
500
500
|
on: clip(skillsOn), off: clip(skillsOff),
|
|
501
|
-
sourcesOn: clip(sourcesOn), sourcesOff: clip(sourcesOff),
|
|
502
501
|
},
|
|
503
502
|
subagents: { on: clip(personas.on), off: clip(personas.off) },
|
|
504
503
|
quickPrompts,
|
|
@@ -63,7 +63,7 @@ export const ATTACHMENT_LIST_MAX = 10;
|
|
|
63
63
|
*
|
|
64
64
|
* 用词:不用「常驻」「注入」这类内部行话(模型没有先验)。
|
|
65
65
|
* ⚠️ 「用『信息』而不是『记忆』」那条**已被 2026-09-23 第四次裁定推翻**(见下):用户
|
|
66
|
-
*
|
|
66
|
+
* 给定的定稿文案里就是「记忆」,而板块标题本来就是「当前记忆」—— 两者同名反而更好,
|
|
67
67
|
* 当时那条理由(「记忆」在系统提示词里指代不明)在板块标题定名之后也不成立了。
|
|
68
68
|
*
|
|
69
69
|
* 2026-09-17 重写(用户:「当前模式会不重视这些提示词」)。上一版的三个毛病都在**授权**
|
|
@@ -110,7 +110,7 @@ export const ATTACHMENT_LIST_MAX = 10;
|
|
|
110
110
|
* 时以实际情况为准、④ 与用户当场说的冲突时以用户为准、⑤ 无关不必提 + 完整性声明(防探测,
|
|
111
111
|
* 见上,那半句是整句里唯一不能删的)。77 → 48 字符(219 → 144 字节),而它**每个场景块各来
|
|
112
112
|
* 一次**,所以省的是 N 倍。删掉的只有修饰与重复:「以下是用户为本机写的记录」→「本机记录」
|
|
113
|
-
* (板块标题 `#
|
|
113
|
+
* (板块标题 `# 当前记忆` 已经把"这是本机的记忆"说过了),两句"冲突时以…为准"
|
|
114
114
|
* 合成一句。
|
|
115
115
|
*
|
|
116
116
|
* 2026-09-23 第四次裁定(用户直接给定稿文案,逐字):
|
|
@@ -188,5 +188,12 @@ export const fail = (code, error, params) => (params ? { ok: false, error, code,
|
|
|
188
188
|
* 旧名 `rules-index.json` / `rules-trash/` 由 hub 的启动迁移搬过来(见 hub.ts)。
|
|
189
189
|
*/
|
|
190
190
|
export const MEMORIES_INDEX_FILE = 'memories-index.json';
|
|
191
|
-
/**
|
|
192
|
-
|
|
191
|
+
/**
|
|
192
|
+
* 记忆回收站目录(**hub 内相对路径**,故带 `trash/` 段)。
|
|
193
|
+
*
|
|
194
|
+
* 2026-10-10 起与其余五类同处一处:`hub/trash/memories-trash/`。此前它是 hub 根下的
|
|
195
|
+
* `memories-trash/`,独立于 `trash/` —— 界面上六类早就统一成一个「回收站」了,落点却分两处,
|
|
196
|
+
* 按 `trash/` 写备份 / 迁移脚本会漏掉这一类(条目数最多的那类)。
|
|
197
|
+
* 老位置由 hub 的启动迁移搬过来(见 `hub.ts` 的 `HUB_RENAMES`),只搬不删。
|
|
198
|
+
*/
|
|
199
|
+
export const MEMORIES_TRASH_DIR = 'trash/memories-trash';
|