dsh-recall-plugin 2.3.1 → 2.3.3

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/lib/config.js CHANGED
@@ -1,96 +1,53 @@
1
- /**
2
- * dsh-recall-plugin — 配置域(ctx 绑定的工厂,无模块级副作用)
3
- *
4
- * 三层配置解析(官方 settings 模型,见 dsh-settings README):
5
- * schema 默认值(Config)→ 组合 base(cordis.patch.yml insert 行 config
6
- * 键)→ 用户文档(设置页「插件配置」卡片写入,dsh-settings 持久化)。
7
- * 环境变量 DSH_RECALL_GC_SNAPS / DSH_RECALL_GC_HOURS 保留为最高优先级
8
- * 覆盖:已用它们调档的用户(含冒烟测试脚本)升级后行为不漂移;设了 env
9
- * 的字段在设置卡片里锁定不可编辑。
10
- *
11
- * Config 同时承担两个角色:cordis 入口配置校验(index.js re-export
12
- * 加载器,非法配置在插件加载时响亮失败)与 settings namespace
13
- * 「dsh-recall」的注册 schema(installSettingsSection,见 index.js)。
14
- */
15
-
16
- import Schema from '@deepseek-ai/schemastery'
17
-
18
- export const Config = Schema.object({
19
- gcSnaps: Schema.number().default(50).description('每积累多少条快照触发一次 git gc'),
20
- gcHours: Schema.number().default(24).description('距上次 gc 超过多少小时触发(与条数先到先触发)'),
21
- maxFileBytes: Schema.number().default(104857600).description('超过该字节数的文件不进快照、不被回退触碰'),
22
- maxSnapshotsPerWorkspace: Schema.number().default(500).description('每个工作区保留的最大快照数,超限删除最旧的'),
23
- // 排除表必须同时覆盖两种存储目录名:降级存储是项目内 .dsh-recall-snapshots/,
24
- // 而 home 存储目录名是 dsh-recall-snapshots/(无点)——工作区 root 恰为
25
- // HOME 时(容器 root=/root 等)它落在工作区内,漏排除会让 git add -A
26
- // 把影子仓库自己吞进去、快照全部失败(issue #6)
27
- baseExcludes: Schema.array(Schema.string()).default(['.git', 'node_modules/', '.dsh-recall-snapshots/', 'dsh-recall-snapshots/']).description('基础排除表(gitignore 语法,优先级低于 exclude.txt)'),
28
- refillDraft: Schema.boolean().default(true).description('撤回后把被撤回的消息文本回填到输入框'),
29
- snapshotEnabled: Schema.boolean().default(true).description('启用消息快照(关闭后不再新建,已有快照仍可撤回)'),
30
- archiveOriginal: Schema.boolean().default(true).description('撤回后归档原会话(关闭后原会话保留在列表中)'),
31
- retentionDays: Schema.number().default(0).description('按天数保留快照,超期自动删除;0 表示不启用'),
32
- })
33
-
34
- // schema 默认值的运行时镜像:settings 服务未组装时 createConfig 直接以
35
- // 入口 config 解析,这组兜底与 Config 保持一致(改默认值两处同步改)。
36
- // DEFAULTS 同时供 config-reset 降级路径(settings.replace 不可用时的兜底,
37
- // index.js config-reset 端点)——默认值只此一份,避免重置与 schema 漂移。
38
- const BASE_EXCLUDES = ['.git', 'node_modules/', '.dsh-recall-snapshots/', 'dsh-recall-snapshots/']
39
-
40
- export const DEFAULTS = {
41
- gcSnaps: 50,
42
- gcHours: 24,
43
- maxFileBytes: 104857600,
44
- maxSnapshotsPerWorkspace: 500,
45
- baseExcludes: BASE_EXCLUDES,
46
- refillDraft: true,
47
- snapshotEnabled: true,
48
- archiveOriginal: true,
49
- retentionDays: 0,
50
- }
51
-
52
- export function createConfig(raw) {
53
- const cfg = raw && typeof raw === 'object' ? raw : {}
54
-
55
- function pickNumber(value, fallback, min) {
56
- const n = typeof value === 'number' ? value : parseInt(String(value == null ? '' : value), 10)
57
- if (!Number.isFinite(n) || n < min) return fallback
58
- return n
59
- }
60
-
61
- // 环境变量优先(向后兼容),其次 config,最后默认值
62
- const gcSnaps = pickNumber(process.env.DSH_RECALL_GC_SNAPS, pickNumber(cfg.gcSnaps, 50, 1), 1)
63
- const gcHours = pickNumber(process.env.DSH_RECALL_GC_HOURS, pickNumber(cfg.gcHours, 24, 1), 1)
64
- const maxFileBytes = pickNumber(cfg.maxFileBytes, 104857600, 1024)
65
- // 每工作区快照上限:0 或负值语义 = 不限制(给想全保留的用户出口);
66
- // 非数值回退默认 500。默认 500 ≈ 重度使用一周量级,太小会静默丢历史
67
- // 撤回点,太大失去防膨胀意义。
68
- const rawMax = typeof cfg.maxSnapshotsPerWorkspace === 'number'
69
- ? cfg.maxSnapshotsPerWorkspace
70
- : parseInt(String(cfg.maxSnapshotsPerWorkspace == null ? '' : cfg.maxSnapshotsPerWorkspace), 10)
71
- const maxSnapshotsPerWorkspace = Number.isFinite(rawMax) ? Math.max(0, rawMax) : 500
72
-
73
- const baseExcludes = Array.isArray(cfg.baseExcludes) && cfg.baseExcludes.length
74
- ? cfg.baseExcludes.filter((p) => typeof p === 'string' && p.trim())
75
- : BASE_EXCLUDES
76
-
77
- const refillDraft = typeof cfg.refillDraft === 'boolean' ? cfg.refillDraft : true
78
-
79
- // 快照总开关:false 冻结「新建」(session/event 短路,见 index.js),
80
- // 已有快照的撤回链路不受影响——关闭只停增量,不销毁存量。
81
- const snapshotEnabled = typeof cfg.snapshotEnabled === 'boolean' ? cfg.snapshotEnabled : true
82
-
83
- // 撤回后是否归档原会话:关闭时原会话保留在侧栏(fork 新会话仍打开),
84
- // 供用户对照回退前后上下文;默认开(归档只是隐藏、可恢复)。
85
- const archiveOriginal = typeof cfg.archiveOriginal === 'boolean' ? cfg.archiveOriginal : true
86
-
87
- // 按时间保留(S2-3):0 或负值 = 不启用(静默删历史撤回点必须显式
88
- // opt-in);非数值回退 0。与 maxSnapshotsPerWorkspace(条数维度)并存,
89
- // 各自独立触发——见 maintenance.enforceRetention。
90
- const rawDays = typeof cfg.retentionDays === 'number'
91
- ? cfg.retentionDays
92
- : parseInt(String(cfg.retentionDays == null ? '' : cfg.retentionDays), 10)
93
- const retentionDays = Number.isFinite(rawDays) ? Math.max(0, rawDays) : 0
94
-
95
- return { gcSnaps, gcHours, maxFileBytes, maxSnapshotsPerWorkspace, baseExcludes, refillDraft, snapshotEnabled, archiveOriginal, retentionDays }
96
- }
1
+ import Schema from "@deepseek-ai/schemastery";
2
+ const Config = Schema.object({
3
+ gcSnaps: Schema.number().default(50).description("\u6BCF\u79EF\u7D2F\u591A\u5C11\u6761\u5FEB\u7167\u89E6\u53D1\u4E00\u6B21 git gc"),
4
+ gcHours: Schema.number().default(24).description("\u8DDD\u4E0A\u6B21 gc \u8D85\u8FC7\u591A\u5C11\u5C0F\u65F6\u89E6\u53D1\uFF08\u4E0E\u6761\u6570\u5148\u5230\u5148\u89E6\u53D1\uFF09"),
5
+ maxFileBytes: Schema.number().default(104857600).description("\u8D85\u8FC7\u8BE5\u5B57\u8282\u6570\u7684\u6587\u4EF6\u4E0D\u8FDB\u5FEB\u7167\u3001\u4E0D\u88AB\u56DE\u9000\u89E6\u78B0"),
6
+ maxSnapshotsPerWorkspace: Schema.number().default(500).description("\u6BCF\u4E2A\u5DE5\u4F5C\u533A\u4FDD\u7559\u7684\u6700\u5927\u5FEB\u7167\u6570\uFF0C\u8D85\u9650\u5220\u9664\u6700\u65E7\u7684"),
7
+ // 排除表必须同时覆盖两种存储目录名:降级存储是项目内 .dsh-recall-snapshots/,
8
+ // home 存储目录名是 dsh-recall-snapshots/(无点)——工作区 root 恰为
9
+ // HOME 时(容器 root=/root 等)它落在工作区内,漏排除会让 git add -A
10
+ // 把影子仓库自己吞进去、快照全部失败(issue #6)
11
+ baseExcludes: Schema.array(Schema.string()).default([".git", "node_modules/", ".dsh-recall-snapshots/", "dsh-recall-snapshots/"]).description("\u57FA\u7840\u6392\u9664\u8868\uFF08gitignore \u8BED\u6CD5\uFF0C\u4F18\u5148\u7EA7\u4F4E\u4E8E exclude.txt\uFF09"),
12
+ refillDraft: Schema.boolean().default(true).description("\u64A4\u56DE\u540E\u628A\u88AB\u64A4\u56DE\u7684\u6D88\u606F\u6587\u672C\u56DE\u586B\u5230\u8F93\u5165\u6846"),
13
+ snapshotEnabled: Schema.boolean().default(true).description("\u542F\u7528\u6D88\u606F\u5FEB\u7167\uFF08\u5173\u95ED\u540E\u4E0D\u518D\u65B0\u5EFA\uFF0C\u5DF2\u6709\u5FEB\u7167\u4ECD\u53EF\u64A4\u56DE\uFF09"),
14
+ archiveOriginal: Schema.boolean().default(true).description("\u64A4\u56DE\u540E\u5F52\u6863\u539F\u4F1A\u8BDD\uFF08\u5173\u95ED\u540E\u539F\u4F1A\u8BDD\u4FDD\u7559\u5728\u5217\u8868\u4E2D\uFF09"),
15
+ retentionDays: Schema.number().default(0).description("\u6309\u5929\u6570\u4FDD\u7559\u5FEB\u7167\uFF0C\u8D85\u671F\u81EA\u52A8\u5220\u9664\uFF1B0 \u8868\u793A\u4E0D\u542F\u7528")
16
+ });
17
+ const BASE_EXCLUDES = [".git", "node_modules/", ".dsh-recall-snapshots/", "dsh-recall-snapshots/"];
18
+ const DEFAULTS = {
19
+ gcSnaps: 50,
20
+ gcHours: 24,
21
+ maxFileBytes: 104857600,
22
+ maxSnapshotsPerWorkspace: 500,
23
+ baseExcludes: BASE_EXCLUDES,
24
+ refillDraft: true,
25
+ snapshotEnabled: true,
26
+ archiveOriginal: true,
27
+ retentionDays: 0
28
+ };
29
+ function createConfig(raw) {
30
+ const cfg = raw && typeof raw === "object" ? raw : {};
31
+ function pickNumber(value, fallback, min) {
32
+ const n = typeof value === "number" ? value : parseInt(String(value == null ? "" : value), 10);
33
+ if (!Number.isFinite(n) || n < min) return fallback;
34
+ return n;
35
+ }
36
+ const gcSnaps = pickNumber(process.env.DSH_RECALL_GC_SNAPS, pickNumber(cfg.gcSnaps, 50, 1), 1);
37
+ const gcHours = pickNumber(process.env.DSH_RECALL_GC_HOURS, pickNumber(cfg.gcHours, 24, 1), 1);
38
+ const maxFileBytes = pickNumber(cfg.maxFileBytes, 104857600, 1024);
39
+ const rawMax = typeof cfg.maxSnapshotsPerWorkspace === "number" ? cfg.maxSnapshotsPerWorkspace : parseInt(String(cfg.maxSnapshotsPerWorkspace == null ? "" : cfg.maxSnapshotsPerWorkspace), 10);
40
+ const maxSnapshotsPerWorkspace = Number.isFinite(rawMax) ? Math.max(0, rawMax) : 500;
41
+ const baseExcludes = Array.isArray(cfg.baseExcludes) && cfg.baseExcludes.length ? cfg.baseExcludes.filter((p) => typeof p === "string" && p.trim()) : BASE_EXCLUDES;
42
+ const refillDraft = typeof cfg.refillDraft === "boolean" ? cfg.refillDraft : true;
43
+ const snapshotEnabled = typeof cfg.snapshotEnabled === "boolean" ? cfg.snapshotEnabled : true;
44
+ const archiveOriginal = typeof cfg.archiveOriginal === "boolean" ? cfg.archiveOriginal : true;
45
+ const rawDays = typeof cfg.retentionDays === "number" ? cfg.retentionDays : parseInt(String(cfg.retentionDays == null ? "" : cfg.retentionDays), 10);
46
+ const retentionDays = Number.isFinite(rawDays) ? Math.max(0, rawDays) : 0;
47
+ return { gcSnaps, gcHours, maxFileBytes, maxSnapshotsPerWorkspace, baseExcludes, refillDraft, snapshotEnabled, archiveOriginal, retentionDays };
48
+ }
49
+ export {
50
+ Config,
51
+ DEFAULTS,
52
+ createConfig
53
+ };
@@ -1,59 +1,34 @@
1
- /**
2
- * dsh-recall-plugin — 环境错误诊断(纯函数模块,无 ctx 依赖)
3
- *
4
- * 环境类失败(锁冲突/磁盘满/权限等)的「识别 → 可行动提示」分层,仿
5
- * errors.js 的机器码与人文案分层:kind 供机器分流(recordError 富集、
6
- * status API、未来设置页过滤),提示文本在 Host 侧生成——client 的 toast
7
- * 运行时读 res.error('快照失败:' + String(res.error).slice(0, 140)),
8
- * Host 换 error 字段文本即生效,client 零改动。
9
- *
10
- * 文案硬约束( motivated by issue #11:锁路径就 100+ 字符,嵌进提示必被
11
- * 140 截断出残句):提示不嵌原始路径,目标 ≤120 字符、硬上限 140;完整
12
- * 原文(含路径)由设置页「最近错误」承载。
13
- */
14
-
15
- // 分类模式表,按根因优先级排列(同一文本命中多类时先命中者胜——
16
- // 如 `Unable to create '…lock': No space left on device` 同时命中 lock 与
17
- // space,磁盘满是根因,space 必须排在 lock 前面)。模式为不区分大小写的
18
- // 正则,覆盖 git 两平台措辞(POSIX `command not found` / win32 `not
19
- // recognized`)与常见 errno 文本。
20
1
  const ENV_PATTERNS = [
21
- ['git', [/command not found/i, /not recognized/i, /git: not found/i, /is not a git command/i]],
22
- ['space', [/no space left on device/i, /disk quota exceeded/i, /enospc/i]],
23
- ['permission', [/permission denied/i, /operation not permitted/i, /not permitted/i, /access is denied/i]],
24
- ['lock', [/could not lock .*file exists/i, /unable to create .*\.lock/i, /fatal: cannot lock/i]],
25
- ['mkdir', [/fatal: cannot mkdir .*file exists/i, /mkdir: cannot create directory/i]],
26
- ]
27
-
28
- // kind 可行动中文提示(buildFeedbackError status 端点 hint 共用同一
29
- // 张表,保证 toast 与设置页看到同一套文案)。值都是静态短句,不带路径。
30
- export const ENV_HINTS = {
31
- git: '未检测到 git CLI 或版本过旧:请安装或升级 git,完成后自动恢复',
32
- space: '磁盘空间已满,快照写入失败:清理磁盘空间后自动恢复',
33
- permission: '快照目录无写入权限:请检查目录权限后重试',
34
- lock: '疑似多个 DSH 实例并发使用同一快照库:请确认只启动了一个;确认后仍失败时,按「设置 · 插件配置 · 最近错误」中的路径删除锁文件',
35
- mkdir: '快照存储目录被同名文件占用:处理后自动恢复',
36
- }
37
-
38
- // 环境错误分类:命中返回 kind,未命中返回 null(未识别错误保现状回落
39
- // 原文,误判只影响 toast 文案不影响功能)。git > space > permission >
40
- // lock > mkdir 的表序即根因优先级,勿按字母序重排。
41
- export function classifyEnvError(text) {
42
- const s = String(text || '')
2
+ ["git", [/command not found/i, /not recognized/i, /git: not found/i, /is not a git command/i]],
3
+ ["space", [/no space left on device/i, /disk quota exceeded/i, /enospc/i]],
4
+ ["permission", [/permission denied/i, /operation not permitted/i, /not permitted/i, /access is denied/i]],
5
+ ["lock", [/could not lock .*file exists/i, /unable to create .*\.lock/i, /fatal: cannot lock/i]],
6
+ ["mkdir", [/fatal: cannot mkdir .*file exists/i, /mkdir: cannot create directory/i]]
7
+ ];
8
+ const ENV_HINTS = {
9
+ git: "\u672A\u68C0\u6D4B\u5230 git CLI \u6216\u7248\u672C\u8FC7\u65E7\uFF1A\u8BF7\u5B89\u88C5\u6216\u5347\u7EA7 git\uFF0C\u5B8C\u6210\u540E\u81EA\u52A8\u6062\u590D",
10
+ space: "\u78C1\u76D8\u7A7A\u95F4\u5DF2\u6EE1\uFF0C\u5FEB\u7167\u5199\u5165\u5931\u8D25\uFF1A\u6E05\u7406\u78C1\u76D8\u7A7A\u95F4\u540E\u81EA\u52A8\u6062\u590D",
11
+ permission: "\u5FEB\u7167\u76EE\u5F55\u65E0\u5199\u5165\u6743\u9650\uFF1A\u8BF7\u68C0\u67E5\u76EE\u5F55\u6743\u9650\u540E\u91CD\u8BD5",
12
+ lock: "\u7591\u4F3C\u591A\u4E2A DSH \u5B9E\u4F8B\u5E76\u53D1\u4F7F\u7528\u540C\u4E00\u5FEB\u7167\u5E93\uFF1A\u8BF7\u786E\u8BA4\u53EA\u542F\u52A8\u4E86\u4E00\u4E2A\uFF1B\u786E\u8BA4\u540E\u4ECD\u5931\u8D25\u65F6\uFF0C\u6309\u300C\u8BBE\u7F6E \xB7 \u63D2\u4EF6\u914D\u7F6E \xB7 \u6700\u8FD1\u9519\u8BEF\u300D\u4E2D\u7684\u8DEF\u5F84\u5220\u9664\u9501\u6587\u4EF6",
13
+ mkdir: "\u5FEB\u7167\u5B58\u50A8\u76EE\u5F55\u88AB\u540C\u540D\u6587\u4EF6\u5360\u7528\uFF1A\u5904\u7406\u540E\u81EA\u52A8\u6062\u590D"
14
+ };
15
+ function classifyEnvError(text) {
16
+ const s = String(text || "");
43
17
  for (const [kind, patterns] of ENV_PATTERNS) {
44
18
  for (const p of patterns) {
45
- if (p.test(s)) return kind
19
+ if (p.test(s)) return kind;
46
20
  }
47
21
  }
48
- return null
22
+ return null;
49
23
  }
50
-
51
- // 把原始错误文本转成 snapFeedback 的失败条目字段:命中 error 为提示
52
- // 文案(client toast 直显);未命中 → error 为原文截断(保 issue #7 现状)、
53
- // kind 标记 unknown。截断统一收在这里,调用方不再各写一遍 slice。
54
- export function buildFeedbackError(raw) {
55
- const text = String(raw || '')
56
- const kind = classifyEnvError(text)
57
- if (!kind) return { error: text.slice(0, 300), kind: 'unknown' }
58
- return { error: ENV_HINTS[kind], kind }
24
+ function buildFeedbackError(raw) {
25
+ const text = String(raw || "");
26
+ const kind = classifyEnvError(text);
27
+ if (!kind) return { error: text.slice(0, 300), kind: "unknown" };
28
+ return { error: ENV_HINTS[kind], kind };
59
29
  }
30
+ export {
31
+ ENV_HINTS,
32
+ buildFeedbackError,
33
+ classifyEnvError
34
+ };
package/lib/dump-parse.js CHANGED
@@ -1,78 +1,96 @@
1
- /**
2
- * dsh-recall-plugin dump 输出解析纯函数(PF-4 / PF-8)
3
- *
4
- * storesDumpScript / excludeDumpScript(两平台脚本模板)定界输出的解析器。
5
- * 放独立模块而非 index.js:routes-manage 也要用 parseExcludeDump,放 index.js
6
- * 会形成 index → routes-manage → index 的循环依赖;纯函数无依赖,独立成
7
- * 文件最干净。模块级导出供单测(tests/unit/stores-dump.test.js 等),index.js
8
- * re-export 保持既有 import 路径稳定。
9
- */
10
-
11
- // 解析 storesDumpScript 的定界输出:dir → { root, entries, lineage }。逐行
12
- // 状态机(==DIR / ROOT / INDEXBEGIN..INDEXEND / LINEAGEBEGIN..LINEAGEEND),
13
- // 单个 store 的 JSON 损坏只丢它自己。
14
- // PF-4:LINEAGE 段承载 lineage.json 原文(与 INDEX 段同构)——manage lineage
15
- // 原实现对每个 root 串行 loadLineage(每 root 一条进程,20 工作区 ≈ 10s),
16
- // 并入 dump 后零新增进程。无 LINEAGE 段(脚本/Host 版本错位的理论场景)按
17
- // 无 lineage 处理,解析容错;lineage.json 损坏按空处理(与 loadLineage 的
18
- // 既有语义一致:损坏不致命,树退化为普通分组)。
19
- export function parseStoresDump(text) {
20
- const map = new Map()
21
- let cur = null
22
- let inIndex = false
23
- let indexLines = []
24
- let inLineage = false
25
- let lineageLines = []
1
+ function parseStoresDump(text) {
2
+ const map = /* @__PURE__ */ new Map();
3
+ let cur = null;
4
+ let inIndex = false;
5
+ let indexLines = [];
6
+ let inLineage = false;
7
+ let lineageLines = [];
26
8
  function flush() {
27
- if (!cur) return
28
- const raw = indexLines.join('\n').trim()
9
+ if (!cur) return;
10
+ const raw = indexLines.join("\n").trim();
29
11
  if (raw) {
30
12
  try {
31
- const arr = JSON.parse(raw)
32
- if (Array.isArray(arr)) cur.entries = arr
33
- } catch (error) { /* index 损坏按无索引处理 */ }
13
+ const arr = JSON.parse(raw);
14
+ if (Array.isArray(arr)) cur.entries = arr;
15
+ } catch (error) {
16
+ }
34
17
  }
35
- const lraw = lineageLines.join('\n').trim()
18
+ const lraw = lineageLines.join("\n").trim();
36
19
  if (lraw) {
37
20
  try {
38
- const larr = JSON.parse(lraw)
21
+ const larr = JSON.parse(lraw);
39
22
  if (Array.isArray(larr)) {
40
- cur.lineage = larr.filter((e) => e && typeof e.childId === 'string' && typeof e.parentId === 'string')
23
+ cur.lineage = larr.filter((e) => e && typeof e.childId === "string" && typeof e.parentId === "string");
41
24
  }
42
- } catch (error) { /* lineage 损坏按无处理(不隔离),与 loadLineage 一致 */ }
25
+ } catch (error) {
26
+ }
43
27
  }
44
- map.set(cur.dir, cur)
45
- cur = null
28
+ map.set(cur.dir, cur);
29
+ cur = null;
46
30
  }
47
31
  for (const line of String(text).split(/\r?\n/)) {
48
- if (line.indexOf('==DIR ') === 0) { flush(); cur = { dir: line.slice(6).trim(), root: null, entries: null, lineage: null }; inIndex = false; inLineage = false; indexLines = []; lineageLines = []; continue }
49
- if (!cur) continue
50
- if (line.indexOf('ROOT ') === 0) { const v = line.slice(5).trim(); cur.root = v || null; continue }
51
- if (line === 'INDEXBEGIN') { inIndex = true; indexLines = []; continue }
52
- if (line === 'INDEXEND') { inIndex = false; continue }
53
- if (line === 'LINEAGEBEGIN') { inLineage = true; lineageLines = []; continue }
54
- if (line === 'LINEAGEEND') { inLineage = false; continue }
55
- if (inIndex) indexLines.push(line)
56
- else if (inLineage) lineageLines.push(line)
32
+ if (line.indexOf("==DIR ") === 0) {
33
+ flush();
34
+ cur = { dir: line.slice(6).trim(), root: null, entries: null, lineage: null };
35
+ inIndex = false;
36
+ inLineage = false;
37
+ indexLines = [];
38
+ lineageLines = [];
39
+ continue;
40
+ }
41
+ if (!cur) continue;
42
+ if (line.indexOf("ROOT ") === 0) {
43
+ const v = line.slice(5).trim();
44
+ cur.root = v || null;
45
+ continue;
46
+ }
47
+ if (line === "INDEXBEGIN") {
48
+ inIndex = true;
49
+ indexLines = [];
50
+ continue;
51
+ }
52
+ if (line === "INDEXEND") {
53
+ inIndex = false;
54
+ continue;
55
+ }
56
+ if (line === "LINEAGEBEGIN") {
57
+ inLineage = true;
58
+ lineageLines = [];
59
+ continue;
60
+ }
61
+ if (line === "LINEAGEEND") {
62
+ inLineage = false;
63
+ continue;
64
+ }
65
+ if (inIndex) indexLines.push(line);
66
+ else if (inLineage) lineageLines.push(line);
57
67
  }
58
- flush()
59
- return map
68
+ flush();
69
+ return map;
60
70
  }
61
-
62
- // 解析 excludeDumpScript 的定界输出(PF-8):EXCLBEGIN <path> / base64 单行
63
- // / EXCLEND → Map<路径, 原文>。内容行是 base64(ASCII 单行),exclude.txt
64
- // 里的任意文本(空行/注释/恰好像标记的行)都不会打乱状态机;文件不存在的
65
- // 段内容为空串(按「尚未配置」处理)。
66
- export function parseExcludeDump(text) {
67
- const map = new Map()
68
- let cur = null
69
- for (const line of String(text || '').split(/\r?\n/)) {
70
- if (line.indexOf('EXCLBEGIN ') === 0) { cur = line.slice('EXCLBEGIN '.length).trim(); map.set(cur, ''); continue }
71
- if (line === 'EXCLEND') { cur = null; continue }
71
+ function parseExcludeDump(text) {
72
+ const map = /* @__PURE__ */ new Map();
73
+ let cur = null;
74
+ for (const line of String(text || "").split(/\r?\n/)) {
75
+ if (line.indexOf("EXCLBEGIN ") === 0) {
76
+ cur = line.slice("EXCLBEGIN ".length).trim();
77
+ map.set(cur, "");
78
+ continue;
79
+ }
80
+ if (line === "EXCLEND") {
81
+ cur = null;
82
+ continue;
83
+ }
72
84
  if (cur !== null && line) {
73
- // base64 损坏按空处理(与「文件不存在」同语义,不致命)
74
- try { map.set(cur, Buffer.from(line, 'base64').toString('utf8')) } catch (error) { /* 保持空串 */ }
85
+ try {
86
+ map.set(cur, Buffer.from(line, "base64").toString("utf8"));
87
+ } catch (error) {
88
+ }
75
89
  }
76
90
  }
77
- return map
91
+ return map;
78
92
  }
93
+ export {
94
+ parseExcludeDump,
95
+ parseStoresDump
96
+ };
package/lib/errors.js CHANGED
@@ -1,57 +1,22 @@
1
- /**
2
- * dsh-recall-plugin — 错误码单一事实源(H3)
3
- *
4
- * 端点响应 code 字段的常量表:此前 code 字符串(STALE/NO_SNAPSHOT/...)
5
- * 散布各 handler 内联,改一处漏一处。这里集中导出、值保持不变(code 是
6
- * client 已消费的线上契约,不改值只收拢),每条注释说明触发条件与 client
7
- * 预期行为。client 侧按 code 映射展示文案(见 client.js CODE_TEXT),未
8
- * 命中回退 host 返回的 message——机器码与人文案分层,为将来 locale 留口
9
- * (当前插件单语,不预建 i18n 抽象)。
10
- */
11
-
12
- // 预览后项目文件变化(P0-3 STALE total 比对失败)——client 自动重新预览
13
- export const RECALL_STALE = 'STALE'
14
- // 目标消息没有可用项目快照(未捕获/已删除)
15
- export const RECALL_NO_SNAPSHOT = 'NO_SNAPSHOT'
16
- // 快照存储不可用(store 未建/已失配)
17
- export const RECALL_NO_STORE = 'NO_STORE'
18
- // 目标工作区 agent 运行中(P0-1 保护,preview/execute 双处拒绝)
19
- export const RECALL_AGENT_BUSY = 'AGENT_BUSY'
20
- // 回退失败(H1)——可能已自动救援恢复;message 携带救援结果
21
- export const RECALL_ROLLBACK_FAILED = 'ROLLBACK_FAILED'
22
- // 排除配置写入路径不在已知白名单内(防「借 API 写任意文件」)
23
- export const RECALL_UNKNOWN_PATH = 'UNKNOWN_PATH'
24
- // 配置字段类型/取值非法
25
- export const RECALL_BAD_TYPE = 'BAD_TYPE'
26
- // 配置补丁为空(无可写字段)
27
- export const RECALL_EMPTY_PATCH = 'EMPTY_PATCH'
28
- // settings 服务未组装(非 web 部署 / 未挂载)
29
- export const RECALL_SETTINGS_UNAVAILABLE = 'SETTINGS_UNAVAILABLE'
30
- // settings.update 写入被拒
31
- export const RECALL_SETTINGS_WRITE_FAILED = 'SETTINGS_WRITE_FAILED'
32
- // 请求体超过 1MB 上限(errBody 统一映射)
33
- export const RECALL_BODY_TOO_LARGE = 'BODY_TOO_LARGE'
34
- // 系统异常/未分类错误兜底(errBody 统一映射)
35
- export const RECALL_ERROR = 'ERROR'
36
- // 无法解析当前工作区(manage usage/delete)
37
- export const RECALL_NO_ROOT = 'NO_ROOT'
38
- // 缺少会话 ID(manage delete scope=session)
39
- export const RECALL_NO_SESSION = 'NO_SESSION'
40
- // 管理操作部分完成(deleteAll 有 store 失败)
41
- export const RECALL_PARTIAL_DELETE = 'PARTIAL_DELETE'
42
- // 未知管理操作(manage 端点 op 未识别)
43
- export const RECALL_UNKNOWN_OP = 'UNKNOWN_OP'
44
- // 未知 API 端点(webServer 路由 404)
45
- export const RECALL_UNKNOWN_ENDPOINT = 'UNKNOWN_ENDPOINT'
46
-
47
- // 语义锚点:H2 的索引损坏经 status 端点 errors 通道暴露(recordError 文本
48
- // 前缀 'recall index corrupt'),不作为端点 code 返回。保留单一命名供
49
- // 一致性扫描与未来若要升级为端点 code 时复用,避免「损坏」这一事实在
50
- // 代码里无归属。
51
- export const RECALL_INDEX_CORRUPT = 'INDEX_CORRUPT'
52
-
53
- // 全量常量集合:供单测做「端点返回的 code 都在表内」的一致性扫描
54
- export const ALL_CODES = Object.freeze([
1
+ const RECALL_STALE = "STALE";
2
+ const RECALL_NO_SNAPSHOT = "NO_SNAPSHOT";
3
+ const RECALL_NO_STORE = "NO_STORE";
4
+ const RECALL_AGENT_BUSY = "AGENT_BUSY";
5
+ const RECALL_ROLLBACK_FAILED = "ROLLBACK_FAILED";
6
+ const RECALL_UNKNOWN_PATH = "UNKNOWN_PATH";
7
+ const RECALL_BAD_TYPE = "BAD_TYPE";
8
+ const RECALL_EMPTY_PATCH = "EMPTY_PATCH";
9
+ const RECALL_SETTINGS_UNAVAILABLE = "SETTINGS_UNAVAILABLE";
10
+ const RECALL_SETTINGS_WRITE_FAILED = "SETTINGS_WRITE_FAILED";
11
+ const RECALL_BODY_TOO_LARGE = "BODY_TOO_LARGE";
12
+ const RECALL_ERROR = "ERROR";
13
+ const RECALL_NO_ROOT = "NO_ROOT";
14
+ const RECALL_NO_SESSION = "NO_SESSION";
15
+ const RECALL_PARTIAL_DELETE = "PARTIAL_DELETE";
16
+ const RECALL_UNKNOWN_OP = "UNKNOWN_OP";
17
+ const RECALL_UNKNOWN_ENDPOINT = "UNKNOWN_ENDPOINT";
18
+ const RECALL_INDEX_CORRUPT = "INDEX_CORRUPT";
19
+ const ALL_CODES = Object.freeze([
55
20
  RECALL_STALE,
56
21
  RECALL_NO_SNAPSHOT,
57
22
  RECALL_NO_STORE,
@@ -69,5 +34,26 @@ export const ALL_CODES = Object.freeze([
69
34
  RECALL_PARTIAL_DELETE,
70
35
  RECALL_UNKNOWN_OP,
71
36
  RECALL_UNKNOWN_ENDPOINT,
37
+ RECALL_INDEX_CORRUPT
38
+ ]);
39
+ export {
40
+ ALL_CODES,
41
+ RECALL_AGENT_BUSY,
42
+ RECALL_BAD_TYPE,
43
+ RECALL_BODY_TOO_LARGE,
44
+ RECALL_EMPTY_PATCH,
45
+ RECALL_ERROR,
72
46
  RECALL_INDEX_CORRUPT,
73
- ])
47
+ RECALL_NO_ROOT,
48
+ RECALL_NO_SESSION,
49
+ RECALL_NO_SNAPSHOT,
50
+ RECALL_NO_STORE,
51
+ RECALL_PARTIAL_DELETE,
52
+ RECALL_ROLLBACK_FAILED,
53
+ RECALL_SETTINGS_UNAVAILABLE,
54
+ RECALL_SETTINGS_WRITE_FAILED,
55
+ RECALL_STALE,
56
+ RECALL_UNKNOWN_ENDPOINT,
57
+ RECALL_UNKNOWN_OP,
58
+ RECALL_UNKNOWN_PATH
59
+ };