dsh-plugin-tool-management 0.10.0 → 0.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (74) hide show
  1. package/CHANGELOG.md +71 -1
  2. package/README.md +64 -49
  3. package/README_EN.md +58 -37
  4. package/docs/images/1-EN.png +0 -0
  5. package/docs/images/1.png +0 -0
  6. package/docs/images/2-EN.png +0 -0
  7. package/docs/images/2.png +0 -0
  8. package/docs/images/3-EN.png +0 -0
  9. package/docs/images/3.png +0 -0
  10. package/docs/images/4-EN.png +0 -0
  11. package/docs/images/4.png +0 -0
  12. package/docs/images/5-EN.png +0 -0
  13. package/docs/images/5.png +0 -0
  14. package/docs/images/6-EN.png +0 -0
  15. package/docs/images/6.png +0 -0
  16. package/docs/images/7-EN.png +0 -0
  17. package/docs/images/7.png +0 -0
  18. package/docs/images/8-EN.png +0 -0
  19. package/docs/images/8.png +0 -0
  20. package/docs/update.md +105 -12
  21. package/lib/client.js +2763 -546
  22. package/lib/compat/preset-reach.js +1 -10
  23. package/lib/compat/probe.js +158 -20
  24. package/lib/context-inject.js +11 -0
  25. package/lib/host-names.js +12 -0
  26. package/lib/http-fence.js +35 -15
  27. package/lib/hub.js +28 -2
  28. package/lib/imports/parsers.js +15 -9
  29. package/lib/imports/upload.js +43 -4
  30. package/lib/index.js +804 -3887
  31. package/lib/mcp/loader-token.js +238 -0
  32. package/lib/mcp/manager.js +1681 -0
  33. package/lib/mcp/override-blocks.js +10 -3
  34. package/lib/mcp/patch-yaml.js +351 -0
  35. package/lib/mcp/secret-guard.js +145 -0
  36. package/lib/{rules → memories}/archive-engine.js +1 -1
  37. package/lib/{rules → memories}/archive.js +1 -1
  38. package/lib/memories/constants.js +128 -0
  39. package/lib/memories/index-io.js +330 -0
  40. package/lib/memories/projection.js +280 -0
  41. package/lib/memories/service.js +686 -0
  42. package/lib/memories/snapshot.js +672 -0
  43. package/lib/ops/candidates.js +64 -0
  44. package/lib/ops/compat.js +136 -0
  45. package/lib/ops/ctx.js +9 -0
  46. package/lib/ops/memory.js +678 -0
  47. package/lib/ops/prompts.js +107 -0
  48. package/lib/ops/scene-records.js +460 -0
  49. package/lib/ops/scene-sync.js +17 -0
  50. package/lib/ops/sessions.js +603 -0
  51. package/lib/ops/trash.js +140 -0
  52. package/lib/paths.js +103 -0
  53. package/lib/prompts/preset-id.js +49 -0
  54. package/lib/{agents-md → prompts}/service.js +1 -1
  55. package/lib/request-gate.js +320 -0
  56. package/lib/scene-prompt-sync.js +4 -4
  57. package/lib/scenes/candidates.js +344 -0
  58. package/lib/{history → sessions}/bridge.js +15 -5
  59. package/lib/sessions/history.js +323 -0
  60. package/lib/{history → sessions}/tombstone.js +1 -1
  61. package/lib/{history → sessions}/workspace.js +92 -24
  62. package/lib/skills/core.js +74 -39
  63. package/lib/skills/readonly-discovery.js +4 -1
  64. package/lib/skills/service.js +98 -13
  65. package/lib/subagents/service.js +199 -55
  66. package/lib/tools/deps.js +8 -0
  67. package/lib/tools/mcp.js +110 -0
  68. package/lib/tools/memory.js +87 -0
  69. package/lib/tools/prompt.js +70 -0
  70. package/lib/tools/skills.js +139 -0
  71. package/lib/tools/subagent.js +40 -0
  72. package/package.json +7 -7
  73. package/lib/agents-md/preset-id.js +0 -49
  74. package/lib/rules/service.js +0 -3078
@@ -3,12 +3,28 @@
3
3
  // 场景记忆(~/.dsh/tool-management/memories/<场景>/<名>.md;场景留空 = 保留场景 global)。
4
4
  // 约定:只认 .md;zip 内任意层级;隐藏项 / 绝对路径 / `..` 穿越 / 超限条目一律跳过并回报;
5
5
  // 重名策略(跳过 or 覆盖)不在这里实现——由调用方按文件系统现状裁决(本项目取「跳过并报告」)。
6
+ //
7
+ // 两条限额的**口径**要分清(2026-09-19 审计 T-09):`MAX_IMPORT_TOTAL_BYTES` 管的是
8
+ // **上传(编码后)**字节,管不住解压后的体积 —— 一个 8 MiB 条目 × 2000 条 ≈ 16 GiB 会在
9
+ // 单次 `unzipSync` 里被实体化。所以另有 `MAX_IMPORT_UNCOMPRESSED_BYTES` 管**解压后**的
10
+ // 累计量,在 filter 里按档案自报的 `info.originalSize` 累加。
6
11
  import { unzipSync } from 'fflate';
12
+ import { isValidSegment } from '../paths.js';
7
13
  export const MAX_IMPORT_FILES = 200;
8
14
  export const MAX_IMPORT_ENTRY_BYTES = 8 * 1024 * 1024;
15
+ /** 上传(编码后)总量:与传输层的 88 MiB 体限是同一层口径,防止一次请求塞进几百 MiB。 */
9
16
  export const MAX_IMPORT_TOTAL_BYTES = 32 * 1024 * 1024;
10
17
  export const MAX_IMPORT_ENTRIES = 2000;
11
18
  export const MAX_IMPORT_NAME_LENGTH = 64;
19
+ /**
20
+ * 解压后累计上限(与技能上传器 `MAX_UPLOAD_TOTAL_BYTES = 64 MiB` 同层口径)。
21
+ *
22
+ * 为什么必须单列:单条目 8 MiB × 2000 条 ≈ 16 GiB 会在一次 `unzipSync` 里全部展开进内存 ——
23
+ * 传输层与 `MAX_IMPORT_TOTAL_BYTES` 都只数**压缩后**的字节,一个数不到。
24
+ * 已知前提:信任档案自报的 `info.originalSize`(fflate 不独立约束输出长度);谎报只能让
25
+ * 解压产物比申报的大,**不会**绕过这个上限之前的条目数门禁。
26
+ */
27
+ export const MAX_IMPORT_UNCOMPRESSED_BYTES = 64 * 1024 * 1024;
12
28
  const message = (e) => String((e && e.message) || e);
13
29
  /** base64 → 字节;容忍 `data:...;base64,` 前缀。 */
14
30
  export function decodeBase64(data) {
@@ -57,6 +73,8 @@ export function expandUploads(files) {
57
73
  problems.push({ name: `(其余 ${all.length - MAX_IMPORT_FILES} 个文件)`, reason: `一次最多导入 ${MAX_IMPORT_FILES} 个文件` });
58
74
  }
59
75
  let total = 0;
76
+ /** 解压后超限只报一次(与"超条目数"同口径,避免几千条问题刷屏)。 */
77
+ let uncompressedReported = false;
60
78
  for (const raw of list) {
61
79
  const file = (raw || {});
62
80
  const name = String(file.name || '').trim() || '(未命名)';
@@ -76,6 +94,7 @@ export function expandUploads(files) {
76
94
  }
77
95
  if (isZipBytes(bytes)) {
78
96
  let unzipped;
97
+ let uncompressed = 0;
79
98
  try {
80
99
  let count = 0;
81
100
  unzipped = unzipSync(bytes, {
@@ -95,6 +114,18 @@ export function expandUploads(files) {
95
114
  problems.push({ name: info.name, reason: `zip 内单条目超过 ${MAX_IMPORT_ENTRY_BYTES >> 20} MiB,已跳过` });
96
115
  return false;
97
116
  }
117
+ // 解压后累计:单条目与条目数都挡不住「2000 × 8 MiB」这种组合,只有累计量挡得住。
118
+ // 目录条目不占解压预算(originalSize 为 0 且不产出内容)。
119
+ if (!info.name.endsWith('/')) {
120
+ uncompressed += info.originalSize;
121
+ if (uncompressed > MAX_IMPORT_UNCOMPRESSED_BYTES) {
122
+ if (!uncompressedReported) {
123
+ uncompressedReported = true;
124
+ problems.push({ name: info.name, reason: `zip 解压后合计超过 ${MAX_IMPORT_UNCOMPRESSED_BYTES >> 20} MiB,该条目及其后条目已忽略` });
125
+ }
126
+ return false;
127
+ }
128
+ }
98
129
  return true;
99
130
  },
100
131
  });
@@ -120,14 +151,22 @@ export function expandUploads(files) {
120
151
  problems.push({ name, reason: '只支持 .md 或 .zip' });
121
152
  continue;
122
153
  }
123
- entries.push({ path: name, bytes });
154
+ // 非 zip 分支同样要走 `normalizeEntryPath`:客户端给的文件名是**外部输入**,
155
+ // `"../../x.md"` / `".hidden/x.md"` 直接进 `RawEntry[]` 就把"隐藏项/穿越一律跳过"
156
+ // 这条承诺交给下游 planner 兜着 —— 本模块的文件头正是这么承诺的(审计 T-10)。
157
+ // 不变量该由承诺方强制:新增的第三个消费者不该靠"运气好下游也查了"才安全。
158
+ const normalized = normalizeEntryPath(name);
159
+ if (!normalized) {
160
+ problems.push({ name, reason: '路径非法或隐藏项,已跳过' });
161
+ continue;
162
+ }
163
+ entries.push({ path: normalized, bytes });
124
164
  }
125
165
  return { entries, problems };
126
166
  }
127
- /** 单个名字段(人设名 / 记忆名 / 场景路径的一段)合法性:与宿主侧校验同口径。 */
167
+ /** 单个名字段(人设名 / 记忆名 / 场景路径的一段)合法性:谓词收敛到 `../paths.ts`。 */
128
168
  export function isValidImportName(name) {
129
- const s = String(name || '');
130
- return s.length > 0 && s.length <= MAX_IMPORT_NAME_LENGTH && s === s.trim() && !s.startsWith('.') && !/[\\/<>:"|?*]/.test(s);
169
+ return isValidSegment(String(name || ''), MAX_IMPORT_NAME_LENGTH);
131
170
  }
132
171
  /** 场景/分组路径合法性:逐段校验(空串 = 根/全局,合法)。 */
133
172
  export function isValidImportGroup(group) {