dsh-memory-palace 1.7.2 → 1.8.0-alpha.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.
@@ -4,7 +4,8 @@
4
4
  // 设计约束(v1.6.0 定案):
5
5
  // - 全部返回 { ok, reason, ... },永不抛错——LLM 产出的 ops 不可信,机器侧兜底;
6
6
  // - 只拆两级标题(`## `),`#` 文件标题与前言归 head,`###` 及更深归当前章节内容;
7
- // - 不改动 text.mjs 既有函数(normLine/budgetClip 有 v1.4.1 测试锚定),本文件自持归一化。
7
+ // - text.mjs 的 normLine 不动;budgetClip 已在 v1.8.0-alpha.1 按 D2 修复重写(兑现「保留全部
8
+ // 结构行」声明),v1.4.1 锚点同步更新——本文件自持归一化,不依赖其内部实现。
8
9
 
9
10
  // 章节标题归一化:剥 `#` 前缀、合空白、转小写。标题匹配全部走它,"带不带 ## 前缀"都鲁棒。
10
11
  const normTitle = (s) =>
@@ -80,34 +81,49 @@ export function locateSection(md, section) {
80
81
  return { start, end, lines };
81
82
  }
82
83
 
84
+ // 条目扁平化(v1.8.0-alpha.1 修复 D1):LLM 传来的 entry 可能内嵌换行——含 `\n## x` / `\n# x`
85
+ // 时会原样落盘生成真实章节标题、把原章节从中间劈开(实测结构损坏且无插件路径可复原)。
86
+ // 处理:按行拆开,逐行剥行首列表符(- / *)与井号(#),去空行后以「;」连接为单行——
87
+ // 内容零丢失、结构损坏向量消除。返回 { text, lines }(lines = 非空行数),调用方可在
88
+ // message 里明示「已合并单行」。memory_write 与子代理 log_write_ops 共用,一处修两条路径闭环。
89
+ export function flattenEntry(entry) {
90
+ const parts = String(entry ?? "")
91
+ .split(/\r?\n/)
92
+ .map((l) => l.trim().replace(/^[-*]\s+/, "").replace(/^#+\s*/, "").trim())
93
+ .filter(Boolean);
94
+ return { text: parts.join(";"), lines: parts.length };
95
+ }
96
+
83
97
  // 章节内追加条目。章节存在 → 内容最后一个非空行后插入 `- entry`(全空行则插在标题行后);
84
98
  // 章节不存在 → { ok:false, reason:"section-miss" }(调用方决定是否走新增)。
85
- // entry 写入前剥除行首列表符(`- `/`* `/`-`/`*`,含缩进)——LLM 常按描述示例自带 `- ` 前缀,
86
- // 不剥会导致 `- - xxx` 双列表符脏数据(v1.6.0 实装踩坑)。
99
+ // entry 先经 flattenEntry 扁平化为单行(防多行/标题注入破坏结构),行首列表符在此一并剥除。
87
100
  export function appendToSectionText(md, section, entry) {
88
- const clean = String(entry ?? "").trim().replace(/^[-*]\s+/, "").trim();
89
- const entryLine = `- ${clean}`;
90
- if (!entryLine.slice(2).trim()) return { ok: false, reason: "empty-entry" };
101
+ const f = flattenEntry(entry);
102
+ const entryLine = `- ${f.text}`;
103
+ if (!f.text) return { ok: false, reason: "empty-entry" };
91
104
  const loc = locateSection(md, section);
92
105
  if (!loc) return { ok: false, reason: "section-miss" };
93
106
  const { start, end, lines } = loc;
94
107
  let insertAt = end;
95
108
  while (insertAt > start + 1 && !lines[insertAt - 1].trim()) insertAt--;
96
109
  lines.splice(insertAt, 0, entryLine);
97
- return { ok: true, reason: "appended", text: lines.join("\n") };
110
+ return { ok: true, reason: "appended", text: lines.join("\n"), flattened: f.lines > 1 };
98
111
  }
99
112
 
100
113
  // 章节追加(upsert 语义):存在则追加,不存在则文件尾新增章节。日志 ops 的 "append" 与
101
114
  // memory_write 工具共用——"不要强行匹配现有章节,不符合章节标题就走新增"。
102
115
  export function upsertSectionText(md, section, entry) {
103
- const r = appendToSectionText(md, section, entry);
104
- if (r.ok) return r;
116
+ const f = flattenEntry(entry);
117
+ const r = appendToSectionText(md, section, f.text);
118
+ // appendToSectionText 会把已扁平化的文本再走一次 flattenEntry(幂等)——此时其 flattened
119
+ // 恒为 false,故这里必须用**本函数**的 f.lines 覆盖标记,否则多行 entry 的标记被吃掉。
120
+ if (r.ok) return { ...r, flattened: f.lines > 1 };
105
121
  if (r.reason === "empty-entry") return r;
106
122
  const title = `## ${String(section ?? "").trim()}`;
107
123
  const base = (md || "").replace(/\s+$/, "");
108
- const entryLine = `- ${String(entry ?? "").trim().replace(/^[-*]\s+/, "").trim()}`;
124
+ const entryLine = `- ${f.text}`;
109
125
  const text = base ? `${base}\n\n${title}\n${entryLine}\n` : `${title}\n${entryLine}\n`;
110
- return { ok: true, reason: "section-created", text };
126
+ return { ok: true, reason: "section-created", text, flattened: f.lines > 1 };
111
127
  }
112
128
 
113
129
  // 显式新建章节(与 upsert 的区别:章节已存在时拒绝而非追加)——子 agent 日志 ops 的
@@ -130,6 +146,14 @@ export function replaceSectionText(md, section, oldSectionText, newSectionText)
130
146
  if (normBlock(current) !== normBlock(String(oldSectionText ?? ""))) {
131
147
  return { ok: false, reason: "conflict", actual: current };
132
148
  }
149
+ // v1.8.0-alpha.1 修复(D3):newText 的标题行必须与 section 参数一致——否则会把章节静默
150
+ // 改名(实测标题改写成功而 message 仍回报旧名,掩盖误操作),且 newText 缺标题行时会把
151
+ // 章节标题整行吃掉(内容并入上一章节)。归一化比对(大小写/空白/`##` 前缀差异均容忍)。
152
+ const firstLine = newText.split("\n").find((l) => l.trim())?.trim() ?? "";
153
+ const heading = /^##\s+(.+)$/.exec(firstLine);
154
+ if (!heading || normTitle(heading[1]) !== normTitle(section)) {
155
+ return { ok: false, reason: "heading-mismatch", actual: current };
156
+ }
133
157
  // 保留章节末尾的分隔空行(章节与下一章节之间通常有空行,整块替换时不能吞掉,
134
158
  // 否则下一章节标题会紧贴上一章节最后一条目)。
135
159
  const hasTrailingBlank = end > start && !lines[end - 1].trim();
@@ -90,8 +90,13 @@ export function budgetClip(text, budget) {
90
90
  j--;
91
91
  }
92
92
  if (j < i) return head.join("\n") + (head.length ? "\n" : "") + tail.join("\n");
93
- const marker = `…(已截断,原 ${text.length} 字符,仅注入结构行与尾部最近条目)`;
94
- return [...head, marker, ...tail].join("\n");
93
+ // v1.8.0-alpha.1 修复(D2):中部结构行不再随内容一起丢弃——旧实现把中部的 `## 章节标题`
94
+ // 当普通内容行处理,落在尾部窗口外即消失,而截断标记却自称「仅注入结构行与尾部最近条目」
95
+ // (声明与行为不符,实测中部 3 个章节标题整体丢失)。现把窗口外幸存的结构行按原序
96
+ // 列在标记之后,保证章节大纲始终完整可见。结构行不计预算(大纲优先于条目)。
97
+ const middleStructural = lines.slice(i, j + 1).filter((l) => isStructural(l.trim()));
98
+ const marker = [`…(已截断,原 ${text.length} 字符,仅注入结构行与尾部最近条目)`, ...middleStructural];
99
+ return [...head, ...marker, ...tail].join("\n");
95
100
  }
96
101
 
97
102
  // v1.4.1:剥除行首 `[smart]` 标记(含重复),保留列表符与其余内容。用于注入与回喂的读侧——
@@ -164,7 +169,7 @@ export function extractText(event) {
164
169
 
165
170
  // 从工具执行结果中识别错误。in-session 错误未必以 turn/end 的 reason.kind==='error' 暴露——
166
171
  // 代码运行报错、工具执行失败常以 tool/result 形式呈现,agent 看到错误后正常收尾(reason.kind 为 completed),
167
- // 故需单独扫描工具结果文本。命中即视为错误信号(受 autoCaptureErrors 门控)。
172
+ // 故需单独扫描工具结果文本。命中即视为错误信号(作为 isError 传给记忆子代理)。
168
173
  export function extractToolErrorText(turn) {
169
174
  for (const b of turn) {
170
175
  if (b.role !== "tool") continue;
package/lib/distill.mjs CHANGED
@@ -1,5 +1,6 @@
1
- // memory-palace 蒸馏业务:会话蒸馏核心(自动智能模式 + 按钮「蒸馏会话」共用)+ 项目记忆蒸馏。
2
- // 经 createDistill 工厂注入 ctx/config/paths/records/运行时状态;返回句柄供 index.mjs 装配与 api.mjs 调用。
1
+ // memory-palace 蒸馏业务:会话蒸馏核心(按钮「蒸馏会话」)+ 项目记忆蒸馏。
2
+ // 经 createDistill 工厂注入 ctx/config/paths/records;返回句柄供 index.mjs 装配与 api.mjs 调用。
3
+ // v1.8.0:原 smart 模式自动摘要入口 summarizeTurn 已删除,distillSessionCore 唯一调用方 = 手动按钮。
3
4
  import { mkdir, unlink, writeFile, rename } from "node:fs/promises";
4
5
  import { join, dirname } from "node:path";
5
6
  import { BlockAssembler, createUserMessage } from "@deepseek-ai/dsh-llm";
@@ -10,9 +11,9 @@ import { appendLineDedup, readNumberedMemory, applyMemoryOp } from "./common/rec
10
11
  import { runWithRetry, RETRY_CONSTANTS } from "./common/retry.mjs";
11
12
 
12
13
  /**
13
- * @param {{ ctx: object, getConfig: () => object, paths: object, records: object, state: object }} deps
14
+ * @param {{ ctx: object, getConfig: () => object, paths: object, records: object }} deps
14
15
  */
15
- export function createDistill({ ctx, getConfig, paths, records, state }) {
16
+ export function createDistill({ ctx, getConfig, paths, records }) {
16
17
  const cfg = () => getConfig();
17
18
 
18
19
  // 调试日志(v1.3.0):dbgFail=失败/跳过留痕(无条件输出,不受开关控制,保证排障可观测);
@@ -93,13 +94,12 @@ export function createDistill({ ctx, getConfig, paths, records, state }) {
93
94
  }
94
95
 
95
96
  // 蒸馏核心(v1.2.0 抽取):对指定 session 的 [fromSeq, ∞) surface 事件做 LLM 智能摘要并写盘。
96
- // 自动智能模式(summarizeTurn,增量断点)与按钮「蒸馏会话」(全量 fromSeq=0)共用本核心。
97
+ // v1.8.0:唯一调用方 = 按钮「蒸馏会话」(全量 fromSeq=0);原 smart 模式自动摘要链路已删除。
97
98
  // 返回 { ok, summary, durableCount };失败 { ok:false }(调用方各自决定降级策略)。
98
99
  async function distillSessionCore(session, dirs, fromSeq, opts) {
99
100
  const allowDelete = !!(opts && opts.allowDelete);
100
- // 回喂存量记忆(v1.4.0 特性3):智能模式级能力——feedbackEnabled 开启即在【自动智能模式(turn/end)
101
- // 与手动蒸馏按钮】两条链路都回喂项目级 + 用户级 MEMORY.md 全文(逐行编号、无截断)。
102
- // delete 仍仅手动蒸馏(allowDelete=true)开放;自动模式 memoryOps 中的 delete 在 §5.5 被 !allowDelete 跳过。
101
+ // 回喂存量记忆(v1.4.0 特性3):feedbackEnabled 开启即回喂项目级 + 用户级 MEMORY.md 全文
102
+ // (逐行编号、无截断)。delete 由 allowDelete 决定(手动按钮传 true)。
103
103
  // 回喂目标严格限定为 MEMORY.md;每日日志(.deepseek-harness/memory/YYYY-MM-DD.md)不读取、不改写、不删旧行。
104
104
  const useFeedback = !!cfg().feedbackEnabled;
105
105
  if (!session) {
@@ -275,17 +275,6 @@ export function createDistill({ ctx, getConfig, paths, records, state }) {
275
275
  return { ok: true, summary, durableCount: durable.length };
276
276
  }
277
277
 
278
- // 智能模式核心:LLM 智能会话摘要(增量范围,产物带 [smart] 标记)。
279
- // 输入:capturedTurn(本轮缓冲,仅用于失败降级)、dirs(写盘目标)、isError。
280
- // 核心逻辑在 distillSessionCore;此处只负责增量断点(state.lastSummarizedSeq)的推进。
281
- // 失败返回 false 由调用方降级轻量条目;handler 永不 reject(调用方包 catch)。
282
- async function summarizeTurn(capturedTurn, dirs, isError) {
283
- if (!state.activeSession) return false;
284
- const r = await distillSessionCore(state.activeSession, dirs, state.lastSummarizedSeq);
285
- if (r.ok) state.lastSummarizedSeq = state.activeSession.seq;
286
- return r.ok;
287
- }
288
-
289
278
  // ---------- v1.2.0 项目记忆蒸馏(按钮「蒸馏项目记忆」) ----------
290
279
  // 流程:读主目标 MEMORY.md → DISTILL_PROMPT 蒸馏 → 写 memory-cover.md(同目录,保证 rename 原子)
291
280
  // → 完整性检查 → 备份 MEMORY.md.{时间戳} → rename 覆盖 → cover 随 rename 消失。
@@ -399,5 +388,5 @@ export function createDistill({ ctx, getConfig, paths, records, state }) {
399
388
  }
400
389
  }
401
390
 
402
- return { distillSessionCore, summarizeTurn, distillProjectMemory, distillLocks };
391
+ return { distillSessionCore, distillProjectMemory, distillLocks };
403
392
  }
@@ -1,7 +1,6 @@
1
- // memory-palace v1.6.0 hybrid 模式:模块入口(装配)。
2
- // 仅在 memoryMode === "hybrid" 时由 index.mjs 调用 registerHybrid(memoryMode 切换需重启
3
- // dsh——注册时机安全)。不注册任何 DSH 全局资源以外的东西;runMemorySubagent 由 index.mjs
4
- // 的 _settle hybrid 分支每轮调用。
1
+ // memory-palace 记忆子代理模块:模块入口(装配)。
2
+ // v1.8.0:原 hybrid 模式已成唯一写入路径(plugin / smart 已删除),本模块由 index.mjs 无条件装配。
3
+ // 不注册任何 DSH 全局资源以外的东西;runMemorySubagent 由 index.mjs 的 _settle 每轮调用。
5
4
  import { runMemorySubagent } from "./subagent.mjs";
6
5
  import { registerHybridTools, attachHybridGuards } from "./tools.mjs";
7
6
 
@@ -235,7 +235,7 @@ function applyLogOps(logFiles, dateStr, ops) {
235
235
 
236
236
  // 记忆子 agent 主入口。每轮必跑(定案:不走闸门);plan 模式拦截由调用方(index.mjs _settle)负责。
237
237
  // 返回 { ok, mode }:mode = "noop"(无重点收尾)| "written"(有落盘)| "no-tool-support";
238
- // ok=false 表示整体失败(调用方降级 writeLightEntry,不推进断点)。
238
+ // ok=false 表示整体失败(不推进断点、下一次 turn/end 自动补蒸;不降级为任何轻量写入)。
239
239
  export async function runMemorySubagent({ ctx, getConfig, paths, state, session, dirs, isError }) {
240
240
  const cfg = getConfig();
241
241
  // 调试日志(v1.6.0):与 distill.mjs 同款门控。dbgFail=失败/跳过留痕(无条件输出,排障可观测);
@@ -379,7 +379,7 @@ export async function runMemorySubagent({ ctx, getConfig, paths, state, session,
379
379
  }
380
380
  if (finish.kind !== "tool-calls") {
381
381
  // max-tokens 等异常收尾:返回失败,断点不推进——下一次 turn/end 子代理自动补蒸。
382
- // v1.6.0 定案:不降级 writeLightEntry(无格式原文会破坏日志章节化结构),宁缺勿滥。
382
+ // v1.6.0 定案:不降级为轻量原文写入(无格式原文会破坏日志章节化结构),宁缺勿滥。
383
383
  dbgFail(`finish ${finish.kind}`, { round });
384
384
  return { ok: false, mode: `finish-${finish.kind}` };
385
385
  }
@@ -35,7 +35,7 @@ export function readLastReorg(md) {
35
35
  export function checkReorgGate(memFile, cfg) {
36
36
  const md = readMdSync(memFile);
37
37
  const size = md.length;
38
- const overBudget = size > (cfg.workspaceBudgetChars || 3000);
38
+ const overBudget = size > (cfg.workspaceBudgetChars || 6000);
39
39
  const last = readLastReorg(md);
40
40
  const cooldownMs = (cfg.reorgCooldownDays ?? 7) * 86400000;
41
41
  const cooled = Date.now() - last >= cooldownMs;
@@ -49,7 +49,7 @@ export function checkReorgGate(memFile, cfg) {
49
49
  ? cooled
50
50
  ? "ok"
51
51
  : `重整冷却期内(上次重整 ${new Date(last).toLocaleString()},冷却 ${cfg.reorgCooldownDays ?? 7} 天),请用 memory_update_section 做章节级修正。`
52
- : `MEMORY.md 当前 ${size} 字符未超出注入预算(${cfg.workspaceBudgetChars || 3000}),无需重整;请用 memory_write / memory_update_section 维护。`,
52
+ : `MEMORY.md 当前 ${size} 字符未超出注入预算(${cfg.workspaceBudgetChars || 6000}),无需重整;请用 memory_write / memory_update_section 维护。`,
53
53
  };
54
54
  }
55
55
 
@@ -65,8 +65,8 @@ function sectionHasEntry(md, section, entry) {
65
65
  }
66
66
 
67
67
  /**
68
- * 注册 hybrid 工具 + pre-execute 闸门。仅在 memoryMode === "hybrid" 时由 index.mjs 调用
69
- * (memoryMode 切换需重启 dsh——注册时机安全,不存在运行中换挡错配)。
68
+ * 注册记忆工具(MEMORY.md 章节化写入 / 整章节替换 / 全量重整)+ pre-execute 闸门。
69
+ * v1.8.0:由 index.mjs 无条件装配(原 hybrid 已成唯一写入路径)。
70
70
  * @param {{ ctx: object, getConfig: () => object, paths: object, records: object, state: object }} deps
71
71
  */
72
72
  export function registerHybridTools({ ctx, getConfig, paths, records, state }) {
@@ -118,7 +118,6 @@ export function registerHybridTools({ ctx, getConfig, paths, records, state }) {
118
118
  render: (_args, value) => [{ type: "text", text: value?.message ?? "Saved." }],
119
119
  },
120
120
  async execute(args, exec) {
121
- state.recentAgentWrote = true;
122
121
  const c = cfg();
123
122
  if (!c.enabled) return { ok: false, message: "memory-palace is currently disabled in settings." };
124
123
  const section = String(args.section ?? "").trim();
@@ -127,28 +126,33 @@ export function registerHybridTools({ ctx, getConfig, paths, records, state }) {
127
126
  const cwd = sessionCwd(exec);
128
127
  const target = memFileOf(args.scope === "user" ? "user" : "project", cwd);
129
128
  if (!target) return { ok: false, message: "No active workspace." };
130
- try {
131
- let wrote = 0;
132
- let skipped = 0;
133
- const files = target.dirs.length ? target.dirs.map((d) => paths.memoryFileOf(d, cwd)) : [target.file];
134
- for (const file of files) {
135
- const md = readMdSync(file);
136
- if (sectionHasEntry(md, section, entry)) {
137
- skipped++;
138
- continue;
139
- }
140
- const r = upsertSectionText(md, section, entry);
141
- if (!r.ok) {
142
- skipped++;
143
- continue;
129
+ try {
130
+ let wrote = 0;
131
+ let skipped = 0;
132
+ let flattened = false;
133
+ const files = target.dirs.length ? target.dirs.map((d) => paths.memoryFileOf(d, cwd)) : [target.file];
134
+ for (const file of files) {
135
+ const md = readMdSync(file);
136
+ if (sectionHasEntry(md, section, entry)) {
137
+ skipped++;
138
+ continue;
139
+ }
140
+ const r = upsertSectionText(md, section, entry);
141
+ if (!r.ok) {
142
+ skipped++;
143
+ continue;
144
+ }
145
+ await writeFile(file, r.text, "utf8");
146
+ wrote++;
147
+ flattened = flattened || r.flattened === true;
144
148
  }
145
- await writeFile(file, r.text, "utf8");
146
- wrote++;
147
- }
149
+ // v1.8.0-alpha.1 修复(D1):entry 经 flattenEntry 扁平化为单行——含内嵌换行/标题行
150
+ // 时不再原样落盘破坏结构,message 明示合并,不静默撒谎。
151
+ const flatNote = flattened ? "(entry 含多行,已合并为单行)" : "";
148
152
  return {
149
153
  ok: wrote > 0,
150
154
  message: wrote > 0
151
- ? `Saved to ${wrote} MEMORY.md file(s) under 「${section}」${skipped ? ` (${skipped} duplicate skipped)` : ""}.`
155
+ ? `Saved to ${wrote} MEMORY.md file(s) under 「${section}」${flatNote}${skipped ? ` (${skipped} duplicate skipped)` : ""}.`
152
156
  : `Duplicate entry under 「${section}」; nothing written.`,
153
157
  };
154
158
  } catch (e) {
@@ -202,7 +206,6 @@ export function registerHybridTools({ ctx, getConfig, paths, records, state }) {
202
206
  [{ type: "text", text: value?.actual ? `${value.message}\n\n当前实际内容:\n${value.actual}` : value?.message ?? "Done." }],
203
207
  },
204
208
  async execute(args, exec) {
205
- state.recentAgentWrote = true;
206
209
  const c = cfg();
207
210
  if (!c.enabled) return { ok: false, message: "memory-palace is currently disabled in settings.", actual: "" };
208
211
  const section = String(args.section ?? "").trim();
@@ -217,7 +220,10 @@ export function registerHybridTools({ ctx, getConfig, paths, records, state }) {
217
220
  ? markEntryDeletedText(md, section, oldText)
218
221
  : replaceSectionText(md, section, oldText, args.newText);
219
222
  if (!r.ok) {
220
- return { ok: false, message: `更新被拒绝(${r.reason}):请先 memory_read 重读后再试。`, actual: r.actual ?? "" };
223
+ const msg = r.reason === "heading-mismatch"
224
+ ? `更新被拒绝(heading-mismatch):newText 的标题行与 section 参数「${section}」不一致(或缺少 ## 标题行)。请保持两者一致后重试;确需重命名章节,请同步修改标题行与 section 参数。`
225
+ : `更新被拒绝(${r.reason}):请先 memory_read 重读后再试。`;
226
+ return { ok: false, message: msg, actual: r.actual ?? "" };
221
227
  }
222
228
  await writeFile(file, r.text, "utf8");
223
229
  return { ok: true, message: `Updated 「${section}」 in ${toHomeShort(file)} (${r.reason}).`, actual: "" };
@@ -258,7 +264,6 @@ export function registerHybridTools({ ctx, getConfig, paths, records, state }) {
258
264
  render: (_args, value) => [{ type: "text", text: value?.message ?? "Done." }],
259
265
  },
260
266
  async execute(args, exec) {
261
- state.recentAgentWrote = true;
262
267
  const c = cfg();
263
268
  if (!c.enabled) return { ok: false, message: "memory-palace is currently disabled in settings." };
264
269
  const cwd = sessionCwd(exec);
@@ -327,7 +332,7 @@ export function attachHybridGuards(ctx, getConfig, paths, state) {
327
332
  kind: "ask",
328
333
  reason:
329
334
  `⚠️ 项目级 MEMORY.md 全量重整确认\n` +
330
- `当前 ${gate.size} 字符(已超预算 ${c.workspaceBudgetChars || 3000}),冷却已满足(上次重整 ${gate.last ? new Date(gate.last).toLocaleString() : "从未"})。\n` +
335
+ `当前 ${gate.size} 字符(已超预算 ${c.workspaceBudgetChars || 6000}),冷却已满足(上次重整 ${gate.last ? new Date(gate.last).toLocaleString() : "从未"})。\n` +
331
336
  `即将用 ${size} 字符的新全文覆盖 ${file}(原文件会自动备份)。\n` +
332
337
  `请确认是否执行重整?`,
333
338
  };