@kira905/dsh-memory-toolkit 0.1.3 → 0.2.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 +92 -0
- package/README.en.md +225 -2
- package/README.md +180 -2
- package/lib/deploy-guard.mjs +463 -275
- package/lib/guard-http.mjs +59 -62
- package/lib/pitfall-form-map.mjs +94 -84
- package/lib/pitfall-score.mjs +262 -185
- package/lib/session-file-name.mjs +116 -100
- package/lib/zstd-frames.mjs +88 -67
- package/memory-db-tools/README.md +29 -1
- package/memory-db-tools/audit-analyze.mjs +1 -1
- package/memory-db-tools/audit-export.mjs +60 -16
- package/memory-db-tools/audit-struct-probe.mjs +29 -8
- package/memory-db-tools/lib/known-devices.mjs +137 -0
- package/memory-engine/README.md +58 -4
- package/memory-engine/bench/bench.mjs +1 -1
- package/memory-engine/cli/band-sentinel.mjs +164 -0
- package/memory-engine/cli/distill-rollback.mjs +112 -44
- package/memory-engine/cli/distill-sessions.mjs +237 -0
- package/memory-engine/cli/distill.mjs +196 -95
- package/memory-engine/cli/doc-index-rebuild.mjs +142 -108
- package/memory-engine/cli/doc-index.mjs +47 -38
- package/memory-engine/cli/domain-tags.mjs +246 -162
- package/memory-engine/cli/layer-migrate.mjs +176 -0
- package/memory-engine/cli/layer-move.mjs +189 -0
- package/memory-engine/cli/pitfall-hook.mjs +136 -70
- package/memory-engine/cli/recall.mjs +243 -110
- package/memory-engine/cli/selfcheck.mjs +93 -44
- package/memory-engine/cli/serve-start.mjs +221 -140
- package/memory-engine/cli/sweep.mjs +83 -44
- package/memory-engine/cli/sync.mjs +93 -57
- package/memory-engine/config/audit-devices.json +6 -0
- package/memory-engine/config/bands.json +7 -0
- package/memory-engine/config/layers.json +7 -0
- package/memory-engine/dict/LICENSE-/350/257/215/345/205/270.md +53 -0
- package/memory-engine/dict/lexicon-config.json +11 -0
- package/memory-engine/dict//350/257/215/345/205/270.txt +109750 -0
- package/memory-engine/dict//350/257/215/351/242/221.txt +20000 -0
- package/memory-engine/lib/band-sentinel.mjs +417 -0
- package/memory-engine/lib/cli-strict.mjs +128 -0
- package/memory-engine/lib/cli-util.mjs +147 -125
- package/memory-engine/lib/config.mjs +83 -96
- package/memory-engine/lib/dirty-ledger.mjs +127 -94
- package/memory-engine/lib/distill-rollback.mjs +173 -133
- package/memory-engine/lib/distill.mjs +878 -585
- package/memory-engine/lib/doc-index-store.mjs +208 -235
- package/memory-engine/lib/doc-index.mjs +351 -350
- package/memory-engine/lib/doc-scan.mjs +178 -156
- package/memory-engine/lib/doc-summarize.mjs +220 -205
- package/memory-engine/lib/domain-tags.mjs +358 -277
- package/memory-engine/lib/embedder.mjs +175 -194
- package/memory-engine/lib/hashes.mjs +22 -7
- package/memory-engine/lib/layer-migrate.mjs +330 -0
- package/memory-engine/lib/layer-move.mjs +307 -0
- package/memory-engine/lib/layers.mjs +191 -0
- package/memory-engine/lib/lexicon-recall.mjs +473 -0
- package/memory-engine/lib/memory-source.mjs +78 -80
- package/memory-engine/lib/pitfall-source.mjs +115 -114
- package/memory-engine/lib/policy-source.mjs +279 -261
- package/memory-engine/lib/recall-log.mjs +285 -0
- package/memory-engine/lib/recall.mjs +565 -240
- package/memory-engine/lib/selfcheck.mjs +142 -106
- package/memory-engine/lib/session-distill.mjs +603 -0
- package/memory-engine/lib/session-source.mjs +421 -0
- package/memory-engine/lib/sweep.mjs +107 -130
- package/memory-engine/lib/sync.mjs +110 -140
- package/memory-engine/lib/tick-hook.mjs +123 -148
- package/memory-engine/lib/vectors-store.mjs +213 -188
- package/memory-engine/test/12-layering.test.mjs +539 -0
- package/memory-engine/test/13-session-distill.test.mjs +616 -0
- package/memory-engine/tools/bridge-smoke.mjs +94 -55
- package/memory-engine/tools/fetch-model.mjs +167 -87
- package/memory-engine/tools/smoke-recall.mjs +94 -38
- package/memory-engine/tools/verify-recall-chain.mjs +413 -0
- package/package.json +1 -1
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
//
|
|
1
|
+
// 提炼产物回滚(方案 §3.5 的回滚行)
|
|
2
2
|
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
3
|
+
// 口径:按 `tags LIKE 'distilled%'` 批量列、批量删;默认 dry-run;真删之前先用 VACUUM INTO 拍快照。
|
|
4
|
+
// 为什么认 tag 而不认 id 清单:提炼条本身是**推断产物**,主人可能一句话否掉一整批,
|
|
5
|
+
// 也可能只否掉其中某几组 ⇒ 两种粒度都得给:`--key <srcKey>` 整组、`--id <行 id>` 单条。
|
|
6
|
+
// ⚠ 删除语句**恒带 `AND tags LIKE 'distilled%'`**:调用方就算把 id 集合算错,也误伤不到真实记忆。
|
|
7
7
|
import fs from 'node:fs';
|
|
8
8
|
import path from 'node:path';
|
|
9
9
|
import crypto from 'node:crypto';
|
|
@@ -11,83 +11,103 @@ import { DatabaseSync } from 'node:sqlite';
|
|
|
11
11
|
import { vacuumInto } from './domain-tags.mjs';
|
|
12
12
|
import { DISTILL_PREFIX } from './distill.mjs';
|
|
13
13
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
const key = t.match(/(?:^|\s)src=([0-9a-f]+)(?:\s|$)/);
|
|
19
|
-
const ids = t.match(/(?:^|\s)srcids=([0-9,\s]*)(?:\s|$)/);
|
|
20
|
-
const date = t.match(/(?:^|\s)date=(\d{8})(?:\s|$)/);
|
|
21
|
-
return {
|
|
22
|
-
srcKey: key ? key[1] : null,
|
|
23
|
-
srcIds: ids && ids[1] ? ids[1].split(',').map((x) => Number(x.trim())).filter((x) => Number.isFinite(x)) : [],
|
|
24
|
-
date: date ? date[1] : null,
|
|
25
|
-
};
|
|
26
|
-
}
|
|
14
|
+
// 标签里的三个属性位:来源键 / 来源 id 串 / 提炼日期
|
|
15
|
+
const TAG_SRCKEY_RE = /(?:^|\s)src=([0-9a-f]+)(?:\s|$)/;
|
|
16
|
+
const TAG_SRCIDS_RE = /(?:^|\s)srcids=([0-9,\s]*)(?:\s|$)/;
|
|
17
|
+
const TAG_DATE_RE = /(?:^|\s)date=(\d{8})(?:\s|$)/;
|
|
27
18
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
19
|
+
const SQL_LIST = `SELECT id, text, tags, updated_at, created_at%EXTRA% FROM memories WHERE tags LIKE 'distilled%' ORDER BY id`;
|
|
20
|
+
const SQL_TRIGGERS = "SELECT name FROM sqlite_master WHERE type='trigger' AND tbl_name='memories'";
|
|
21
|
+
const SQL_DELETE = "DELETE FROM memories WHERE id = ? AND tags LIKE 'distilled%'";
|
|
22
|
+
|
|
23
|
+
// 库里有没有某一列(老库没有 owner_user / visibility 这两个多人口径字段)
|
|
24
|
+
function columnExists(db, name) {
|
|
25
|
+
return db.prepare('PRAGMA table_info(memories)').all().some((row) => row.name === name);
|
|
32
26
|
}
|
|
33
27
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
*/
|
|
38
|
-
export function listDistilled({ dbPath, keys = null, ids = null, limit = 0, headChars = 100 }) {
|
|
39
|
-
const db = open(dbPath, { readOnly: true });
|
|
28
|
+
// 统一开关库:只读走 readOnly 连接,写连接才设 busy_timeout;关库一律放 finally
|
|
29
|
+
function withDb(dbPath, { readOnly = false } = {}, body) {
|
|
30
|
+
const conn = readOnly ? new DatabaseSync(dbPath, { readOnly: true }) : new DatabaseSync(dbPath);
|
|
40
31
|
try {
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
.all();
|
|
44
|
-
let items = rows.map((r) => {
|
|
45
|
-
const meta = parseDistilledTags(r.tags) ?? { srcKey: null, srcIds: [], date: null };
|
|
46
|
-
return {
|
|
47
|
-
id: Number(r.id), srcKey: meta.srcKey, srcIds: meta.srcIds, date: meta.date,
|
|
48
|
-
chars: String(r.text ?? '').length,
|
|
49
|
-
head: String(r.text ?? '').slice(0, headChars).replace(/\s+/g, ' '),
|
|
50
|
-
tags: r.tags, createdAt: Number(r.created_at), updatedAt: Number(r.updated_at),
|
|
51
|
-
ownerUser: r.owner_user ?? null, visibility: r.visibility ?? null,
|
|
52
|
-
};
|
|
53
|
-
});
|
|
54
|
-
if (Array.isArray(keys) && keys.length) {
|
|
55
|
-
const want = new Set(keys.map((k) => String(k)));
|
|
56
|
-
items = items.filter((x) => want.has(String(x.srcKey)));
|
|
57
|
-
}
|
|
58
|
-
if (Array.isArray(ids) && ids.length) {
|
|
59
|
-
const want = new Set(ids.map((x) => Number(x)));
|
|
60
|
-
items = items.filter((x) => want.has(x.id));
|
|
61
|
-
}
|
|
62
|
-
if (limit > 0) items = items.slice(0, Number(limit));
|
|
63
|
-
return items;
|
|
32
|
+
if (!readOnly) conn.exec('PRAGMA busy_timeout = 5000;');
|
|
33
|
+
return body(conn);
|
|
64
34
|
} finally {
|
|
65
|
-
try {
|
|
35
|
+
try { conn.close(); } catch { /* 关不掉也不拖累结果 */ }
|
|
66
36
|
}
|
|
67
37
|
}
|
|
68
38
|
|
|
69
|
-
|
|
70
|
-
|
|
39
|
+
// 解析提炼条标签 → {srcKey, srcIds, date};不是提炼条就给 null
|
|
40
|
+
export function parseDistilledTags(tags) {
|
|
41
|
+
const raw = String(tags ?? '');
|
|
42
|
+
if (!raw.startsWith(DISTILL_PREFIX)) return null;
|
|
43
|
+
|
|
44
|
+
const pick = (pattern) => {
|
|
45
|
+
const hit = pattern.exec(raw);
|
|
46
|
+
return hit ? hit[1] : '';
|
|
47
|
+
};
|
|
48
|
+
const idText = pick(TAG_SRCIDS_RE);
|
|
49
|
+
const ids = idText
|
|
50
|
+
? idText.split(',').map((piece) => Number(piece.trim())).filter((n) => Number.isFinite(n))
|
|
51
|
+
: [];
|
|
52
|
+
return {
|
|
53
|
+
srcKey: pick(TAG_SRCKEY_RE) || null,
|
|
54
|
+
srcIds: ids,
|
|
55
|
+
date: pick(TAG_DATE_RE) || null,
|
|
56
|
+
};
|
|
71
57
|
}
|
|
72
58
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
59
|
+
// 列出提炼条(只读);keys / ids 是两组独立过滤,limit > 0 才截断
|
|
60
|
+
export function listDistilled({ dbPath, keys = null, ids = null, limit = 0, headChars = 100 }) {
|
|
61
|
+
return withDb(dbPath, { readOnly: true }, (conn) => {
|
|
62
|
+
const extra = columnExists(conn, 'owner_user') ? ', owner_user, visibility' : '';
|
|
63
|
+
const rows = conn.prepare(SQL_LIST.replace('%EXTRA%', extra)).all();
|
|
64
|
+
const keySet = new Set((Array.isArray(keys) ? keys : []).map((k) => String(k)));
|
|
65
|
+
const idSet = new Set((Array.isArray(ids) ? ids : []).map((x) => Number(x)));
|
|
66
|
+
|
|
67
|
+
const listed = [];
|
|
68
|
+
for (const row of rows) {
|
|
69
|
+
const meta = parseDistilledTags(row.tags) ?? { srcKey: null, srcIds: [], date: null };
|
|
70
|
+
if (keySet.size > 0 && !keySet.has(String(meta.srcKey))) continue;
|
|
71
|
+
if (idSet.size > 0 && !idSet.has(Number(row.id))) continue;
|
|
72
|
+
const text = String(row.text ?? '');
|
|
73
|
+
listed.push({
|
|
74
|
+
id: Number(row.id),
|
|
75
|
+
srcKey: meta.srcKey,
|
|
76
|
+
srcIds: meta.srcIds,
|
|
77
|
+
date: meta.date,
|
|
78
|
+
chars: text.length,
|
|
79
|
+
head: text.slice(0, headChars).replace(/\s+/g, ' '),
|
|
80
|
+
tags: row.tags,
|
|
81
|
+
createdAt: Number(row.created_at),
|
|
82
|
+
updatedAt: Number(row.updated_at),
|
|
83
|
+
ownerUser: row.owner_user ?? null,
|
|
84
|
+
visibility: row.visibility ?? null,
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
return limit > 0 ? listed.slice(0, Number(limit)) : listed;
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// **内容级摘要**:按 id 排序取 (id, text, tags, pinned, updated_at),逐行拼再整体哈希。
|
|
92
|
+
// 为什么不看文件 sha256:SQLite 写回后页布局会变(内容一样也可能字节不同),
|
|
93
|
+
// 文件 sha 回答不了"回滚后库是否回到原状",内容摘要能。
|
|
78
94
|
export function contentDigest(dbPath, { excludeTagsLike = null } = {}) {
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
const
|
|
82
|
-
const
|
|
83
|
-
const
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
95
|
+
return withDb(dbPath, { readOnly: true }, (conn) => {
|
|
96
|
+
const filter = excludeTagsLike ? ` WHERE tags NOT LIKE '${excludeTagsLike}'` : '';
|
|
97
|
+
const rows = conn.prepare(`SELECT id, text, tags, pinned, updated_at FROM memories${filter} ORDER BY id`).all();
|
|
98
|
+
const hash = crypto.createHash('sha256');
|
|
99
|
+
const FIELD_SEP = '\u0001';
|
|
100
|
+
const ROW_END = '\u0002';
|
|
101
|
+
for (const row of rows) {
|
|
102
|
+
const fields = [row.id, row.text, row.tags, row.pinned, row.updated_at].map((value) => String(value));
|
|
103
|
+
hash.update(`${fields.join(FIELD_SEP)}${ROW_END}`, 'utf8');
|
|
104
|
+
}
|
|
105
|
+
const allCount = Number(conn.prepare('SELECT COUNT(*) n FROM memories').get().n);
|
|
106
|
+
return { count: rows.length, digest: hash.digest('hex'), allCount };
|
|
107
|
+
});
|
|
89
108
|
}
|
|
90
109
|
|
|
110
|
+
// 文件 sha256(读不动就给 null,怎么记由调用方决定)
|
|
91
111
|
export function fileSha256(file) {
|
|
92
112
|
try {
|
|
93
113
|
return crypto.createHash('sha256').update(fs.readFileSync(file)).digest('hex');
|
|
@@ -96,83 +116,103 @@ export function fileSha256(file) {
|
|
|
96
116
|
}
|
|
97
117
|
}
|
|
98
118
|
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
* @param {{dbPath:string, apply?:boolean, keys?:string[], ids?:number[], snapshotDir?:string,
|
|
102
|
-
* now?:number, keepSnapshot?:boolean}} p
|
|
103
|
-
* @returns {{ok:boolean, matched:number, deleted:number, deletedIds:number[], snapshot:string|null,
|
|
104
|
-
* before:object, after:object, dryRun:boolean, rollbackFile:string|null}}
|
|
105
|
-
*/
|
|
106
|
-
export function rollbackDistilled(p = {}) {
|
|
107
|
-
const { dbPath, keys = null, ids = null, snapshotDir = null, now = Date.now(), apply = false } = p;
|
|
108
|
-
const before = contentDigest(dbPath);
|
|
109
|
-
const items = listDistilled({ dbPath, keys, ids });
|
|
110
|
-
const out = {
|
|
111
|
-
ok: true, dryRun: !apply, matched: items.length, deleted: 0, deletedIds: [],
|
|
112
|
-
snapshot: null, before, after: before, items: items.map((x) => ({ id: x.id, srcKey: x.srcKey, head: x.head })),
|
|
113
|
-
rollbackFile: null, warnings: [],
|
|
114
|
-
};
|
|
115
|
-
if (!items.length) return out;
|
|
116
|
-
|
|
117
|
-
if (!apply) return out;
|
|
118
|
-
|
|
119
|
-
// 删除前快照(**唯一回滚点**)
|
|
119
|
+
// 删除前快照:这是整条链上唯一的回滚点
|
|
120
|
+
function writeSnapshot(dbPath, snapshotDir, now) {
|
|
120
121
|
const dir = snapshotDir ?? path.join(path.dirname(dbPath), 'snapshots');
|
|
121
122
|
fs.mkdirSync(dir, { recursive: true });
|
|
122
123
|
const stamp = new Date(Number(now)).toISOString().replace(/[:.]/g, '-');
|
|
123
124
|
const snap = path.join(dir, `memory-before-distill-rollback-${stamp}.db`);
|
|
124
125
|
vacuumInto(dbPath, snap);
|
|
125
|
-
|
|
126
|
-
|
|
126
|
+
return { dir, stamp, snap };
|
|
127
|
+
}
|
|
127
128
|
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
const triggers =
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
const targetIds = items.map((x) => x.id);
|
|
136
|
-
const stmt = db.prepare(`DELETE FROM memories WHERE id = ? AND tags LIKE 'distilled%'`);
|
|
137
|
-
db.exec('BEGIN IMMEDIATE');
|
|
129
|
+
// 真删:先记触发器现场(FTS 影子表靠 memories_ad 同步),再在一个事务里逐条删
|
|
130
|
+
function deleteDistilledRows(dbPath, items) {
|
|
131
|
+
return withDb(dbPath, {}, (conn) => {
|
|
132
|
+
const triggers = conn.prepare(SQL_TRIGGERS).all().map((row) => row.name);
|
|
133
|
+
const wipe = conn.prepare(SQL_DELETE);
|
|
134
|
+
const removed = [];
|
|
135
|
+
conn.exec('BEGIN IMMEDIATE');
|
|
138
136
|
try {
|
|
139
|
-
for (const
|
|
140
|
-
const
|
|
141
|
-
if (Number(
|
|
137
|
+
for (const item of items) {
|
|
138
|
+
const res = wipe.run(item.id);
|
|
139
|
+
if (Number(res.changes || 0) > 0) removed.push(item.id);
|
|
142
140
|
}
|
|
143
|
-
|
|
144
|
-
} catch (
|
|
145
|
-
try {
|
|
146
|
-
throw
|
|
141
|
+
conn.exec('COMMIT');
|
|
142
|
+
} catch (err) {
|
|
143
|
+
try { conn.exec('ROLLBACK'); } catch { /* 忽略 */ }
|
|
144
|
+
throw err;
|
|
147
145
|
}
|
|
148
|
-
|
|
149
|
-
}
|
|
150
|
-
|
|
146
|
+
return { triggers, removed };
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// 生成"把快照写回目标库"的一键恢复脚本(覆盖前先把现有库另存一份)
|
|
151
|
+
function restoreScriptText({ snapshot, target }) {
|
|
152
|
+
const lines = [
|
|
153
|
+
'// 自动生成:把提炼回滚前的快照写回目标记忆库(覆盖式,覆盖前先另存现有库)',
|
|
154
|
+
'// 用法:node <本文件路径>',
|
|
155
|
+
"import fs from 'node:fs';",
|
|
156
|
+
`const SNAPSHOT = ${JSON.stringify(snapshot)};`,
|
|
157
|
+
`const DB_TARGET = ${JSON.stringify(target)};`,
|
|
158
|
+
"const CURRENT_SUFFIXES = ['', '-wal', '-shm'];",
|
|
159
|
+
'if (!fs.existsSync(SNAPSHOT)) {',
|
|
160
|
+
" console.error('快照不存在:' + SNAPSHOT);",
|
|
161
|
+
' process.exit(3);',
|
|
162
|
+
'}',
|
|
163
|
+
'for (const suffix of CURRENT_SUFFIXES) {',
|
|
164
|
+
' const file = DB_TARGET + suffix;',
|
|
165
|
+
" try { if (fs.existsSync(file)) fs.copyFileSync(file, file + '.before-restore'); } catch { /* 忽略 */ }",
|
|
166
|
+
'}',
|
|
167
|
+
'fs.copyFileSync(SNAPSHOT, DB_TARGET);',
|
|
168
|
+
"for (const suffix of ['-wal', '-shm']) { try { fs.rmSync(DB_TARGET + suffix, { force: true }); } catch { /* 忽略 */ } }",
|
|
169
|
+
"console.log('已恢复:' + DB_TARGET + ' <- ' + SNAPSHOT);",
|
|
170
|
+
'',
|
|
171
|
+
];
|
|
172
|
+
return lines.join('\n');
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// 批量回滚主入口:先摘要 + 列表;没命中或 dry-run 都早退;真删才拍快照
|
|
176
|
+
export function rollbackDistilled(p = {}) {
|
|
177
|
+
const { dbPath, keys = null, ids = null, snapshotDir = null, now = Date.now(), apply = false } = p;
|
|
178
|
+
const before = contentDigest(dbPath);
|
|
179
|
+
const items = listDistilled({ dbPath, keys, ids });
|
|
180
|
+
|
|
181
|
+
const out = {
|
|
182
|
+
ok: true,
|
|
183
|
+
dryRun: !apply,
|
|
184
|
+
matched: items.length,
|
|
185
|
+
deleted: 0,
|
|
186
|
+
deletedIds: [],
|
|
187
|
+
snapshot: null,
|
|
188
|
+
before,
|
|
189
|
+
after: before,
|
|
190
|
+
items: items.map((item) => ({ id: item.id, srcKey: item.srcKey, head: item.head })),
|
|
191
|
+
rollbackFile: null,
|
|
192
|
+
warnings: [],
|
|
193
|
+
};
|
|
194
|
+
if (items.length === 0) return out;
|
|
195
|
+
if (!apply) return out;
|
|
196
|
+
|
|
197
|
+
const shot = writeSnapshot(dbPath, snapshotDir, now);
|
|
198
|
+
out.snapshot = shot.snap;
|
|
199
|
+
out.snapshotSha256 = fileSha256(shot.snap);
|
|
200
|
+
|
|
201
|
+
const wiped = deleteDistilledRows(dbPath, items);
|
|
202
|
+
out.triggers = wiped.triggers;
|
|
203
|
+
if (!wiped.triggers.some((name) => /_ad$|delete/i.test(name))) {
|
|
204
|
+
out.warnings.push('memories 上没有 AFTER DELETE 触发器 ⇒ FTS 影子表可能残留脏行(建议核对 memories_fts)');
|
|
151
205
|
}
|
|
206
|
+
out.deletedIds = wiped.removed;
|
|
207
|
+
out.deleted = wiped.removed.length;
|
|
208
|
+
|
|
152
209
|
out.after = contentDigest(dbPath);
|
|
153
|
-
|
|
210
|
+
|
|
211
|
+
// 反向回滚脚本:把快照拷回去 = 一键恢复
|
|
154
212
|
if (out.snapshot) {
|
|
155
|
-
const rbFile = path.join(dir, `restore-memory-${stamp}.mjs`);
|
|
156
|
-
fs.writeFileSync(rbFile,
|
|
213
|
+
const rbFile = path.join(shot.dir, `restore-memory-${shot.stamp}.mjs`);
|
|
214
|
+
fs.writeFileSync(rbFile, restoreScriptText({ snapshot: shot.snap, target: dbPath }), 'utf8');
|
|
157
215
|
out.rollbackFile = rbFile;
|
|
158
216
|
}
|
|
159
217
|
return out;
|
|
160
218
|
}
|
|
161
|
-
|
|
162
|
-
/** 生成"把快照写回目标库"的一键恢复脚本(纯 ASCII 头 + 覆盖前校验存在性)。 */
|
|
163
|
-
function restoreScript({ snapshot, target }) {
|
|
164
|
-
return `// 自动生成:把提炼回滚前的快照写回目标记忆库(覆盖式)
|
|
165
|
-
// 用法:node ${path.basename('restore-memory.mjs')} 或 node "本文件路径"
|
|
166
|
-
import fs from 'node:fs';
|
|
167
|
-
const SNAP = ${JSON.stringify(snapshot)};
|
|
168
|
-
const TARGET = ${JSON.stringify(target)};
|
|
169
|
-
if (!fs.existsSync(SNAP)) { console.error('快照不存在:' + SNAP); process.exit(3); }
|
|
170
|
-
for (const suffix of ['', '-wal', '-shm']) {
|
|
171
|
-
const f = TARGET + suffix;
|
|
172
|
-
try { if (fs.existsSync(f)) fs.copyFileSync(f, f + '.before-restore'); } catch { /* ignore */ }
|
|
173
|
-
}
|
|
174
|
-
fs.copyFileSync(SNAP, TARGET);
|
|
175
|
-
for (const suffix of ['-wal', '-shm']) { try { fs.rmSync(TARGET + suffix, { force: true }); } catch { /* ignore */ } }
|
|
176
|
-
console.log('已恢复:' + TARGET + ' <- ' + SNAP);
|
|
177
|
-
`;
|
|
178
|
-
}
|