dsh-recall-plugin 1.7.1 → 2.1.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 +55 -12
- package/README.en.md +23 -19
- package/README.md +30 -20
- package/lib/client.js +1459 -1266
- package/lib/config.js +43 -2
- package/lib/diagnostics.js +59 -0
- package/lib/errors.js +73 -0
- package/lib/index.js +426 -1000
- package/lib/maintenance.js +263 -145
- package/lib/routes-core.js +162 -0
- package/lib/routes-manage.js +541 -0
- package/lib/scripts.posix.js +165 -27
- package/lib/scripts.pwsh.js +148 -33
- package/lib/session-info.js +71 -0
- package/lib/snapshots.js +305 -78
- package/lib/store.js +157 -19
- package/package.json +65 -54
package/lib/config.js
CHANGED
|
@@ -19,18 +19,36 @@ export const Config = Schema.object({
|
|
|
19
19
|
gcSnaps: Schema.number().default(50).description('每积累多少条快照触发一次 git gc'),
|
|
20
20
|
gcHours: Schema.number().default(24).description('距上次 gc 超过多少小时触发(与条数先到先触发)'),
|
|
21
21
|
maxFileBytes: Schema.number().default(104857600).description('超过该字节数的文件不进快照、不被回退触碰'),
|
|
22
|
+
maxSnapshotsPerWorkspace: Schema.number().default(500).description('每个工作区保留的最大快照数,超限删除最旧的'),
|
|
22
23
|
// 排除表必须同时覆盖两种存储目录名:降级存储是项目内 .dsh-recall-snapshots/,
|
|
23
24
|
// 而 home 存储目录名是 dsh-recall-snapshots/(无点)——工作区 root 恰为
|
|
24
25
|
// HOME 时(容器 root=/root 等)它落在工作区内,漏排除会让 git add -A
|
|
25
26
|
// 把影子仓库自己吞进去、快照全部失败(issue #6)
|
|
26
27
|
baseExcludes: Schema.array(Schema.string()).default(['.git', 'node_modules/', '.dsh-recall-snapshots/', 'dsh-recall-snapshots/']).description('基础排除表(gitignore 语法,优先级低于 exclude.txt)'),
|
|
27
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 表示不启用'),
|
|
28
32
|
})
|
|
29
33
|
|
|
30
34
|
// schema 默认值的运行时镜像:settings 服务未组装时 createConfig 直接以
|
|
31
|
-
// 入口 config 解析,这组兜底与 Config
|
|
35
|
+
// 入口 config 解析,这组兜底与 Config 保持一致(改默认值两处同步改)。
|
|
36
|
+
// DEFAULTS 同时供 config-reset 降级路径(settings.replace 不可用时的兜底,
|
|
37
|
+
// 见 index.js config-reset 端点)——默认值只此一份,避免重置与 schema 漂移。
|
|
32
38
|
const BASE_EXCLUDES = ['.git', 'node_modules/', '.dsh-recall-snapshots/', 'dsh-recall-snapshots/']
|
|
33
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
|
+
|
|
34
52
|
export function createConfig(raw) {
|
|
35
53
|
const cfg = raw && typeof raw === 'object' ? raw : {}
|
|
36
54
|
|
|
@@ -44,6 +62,13 @@ export function createConfig(raw) {
|
|
|
44
62
|
const gcSnaps = pickNumber(process.env.DSH_RECALL_GC_SNAPS, pickNumber(cfg.gcSnaps, 50, 1), 1)
|
|
45
63
|
const gcHours = pickNumber(process.env.DSH_RECALL_GC_HOURS, pickNumber(cfg.gcHours, 24, 1), 1)
|
|
46
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
|
|
47
72
|
|
|
48
73
|
const baseExcludes = Array.isArray(cfg.baseExcludes) && cfg.baseExcludes.length
|
|
49
74
|
? cfg.baseExcludes.filter((p) => typeof p === 'string' && p.trim())
|
|
@@ -51,5 +76,21 @@ export function createConfig(raw) {
|
|
|
51
76
|
|
|
52
77
|
const refillDraft = typeof cfg.refillDraft === 'boolean' ? cfg.refillDraft : true
|
|
53
78
|
|
|
54
|
-
|
|
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 }
|
|
55
96
|
}
|
|
@@ -0,0 +1,59 @@
|
|
|
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
|
+
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 || '')
|
|
43
|
+
for (const [kind, patterns] of ENV_PATTERNS) {
|
|
44
|
+
for (const p of patterns) {
|
|
45
|
+
if (p.test(s)) return kind
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
return null
|
|
49
|
+
}
|
|
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 }
|
|
59
|
+
}
|
package/lib/errors.js
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
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([
|
|
55
|
+
RECALL_STALE,
|
|
56
|
+
RECALL_NO_SNAPSHOT,
|
|
57
|
+
RECALL_NO_STORE,
|
|
58
|
+
RECALL_AGENT_BUSY,
|
|
59
|
+
RECALL_ROLLBACK_FAILED,
|
|
60
|
+
RECALL_UNKNOWN_PATH,
|
|
61
|
+
RECALL_BAD_TYPE,
|
|
62
|
+
RECALL_EMPTY_PATCH,
|
|
63
|
+
RECALL_SETTINGS_UNAVAILABLE,
|
|
64
|
+
RECALL_SETTINGS_WRITE_FAILED,
|
|
65
|
+
RECALL_BODY_TOO_LARGE,
|
|
66
|
+
RECALL_ERROR,
|
|
67
|
+
RECALL_NO_ROOT,
|
|
68
|
+
RECALL_NO_SESSION,
|
|
69
|
+
RECALL_PARTIAL_DELETE,
|
|
70
|
+
RECALL_UNKNOWN_OP,
|
|
71
|
+
RECALL_UNKNOWN_ENDPOINT,
|
|
72
|
+
RECALL_INDEX_CORRUPT,
|
|
73
|
+
])
|