@fanchao8609/agent_brain_sync 1.29.0 → 1.31.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/README.md +4 -9
- package/bin/abs.js +9 -64
- package/bin/mcp.js +2 -34
- package/hooks/abs-rounded-editor/format.ts +26 -37
- package/hooks/abs-rounded-editor/index.ts +34 -55
- package/hooks/abs.omp.ts +8 -50
- package/hooks/abs.pi.ts +67 -106
- package/package.json +1 -1
- package/skill/abs-agent-brain-sync/SKILL.md +11 -9
- package/src/brain/capture.js +0 -6
- package/src/brain/load.js +11 -244
- package/src/note.js +0 -14
- package/src/store.js +1 -1
- package/src/todo/common.js +19 -4
- package/src/todo/spec.js +3 -1
- package/src/todo/task-cmd.js +11 -5
- package/src/todo/tasks.js +32 -2
- package/src/todo.js +1 -1
- package/src/brain/db-cmd.js +0 -387
- package/src/brain/db.js +0 -667
- package/src/brain/evolve.js +0 -135
package/src/brain/db-cmd.js
DELETED
|
@@ -1,387 +0,0 @@
|
|
|
1
|
-
// src/brain/db-cmd.js — `abs db` 命令族:建库 / 搜索 / 状态。
|
|
2
|
-
//
|
|
3
|
-
// 这是「触发搜索先搜 SQLite」的入口。分工:
|
|
4
|
-
// abs query <词> —— 旧的 markdown 直扫,保留不动(唯一被验证过的路径)
|
|
5
|
-
// abs db search <词> —— 走库:FTS 拿种子 → 亮度扩散,带 L1 铁律
|
|
6
|
-
import { promises as fs } from 'node:fs';
|
|
7
|
-
import { join, resolve, dirname } from 'node:path';
|
|
8
|
-
import { findBrainRoot, brainPath } from '../index.js';
|
|
9
|
-
import { openDb, rebuild, search, illuminate, rulesOf, stats, ensureDbDir, dbPath, lastOpenRebuilt } from './db.js';
|
|
10
|
-
import { LAYERS } from './layers.js';
|
|
11
|
-
|
|
12
|
-
const PAGE_DIRS = ['concepts', 'entities', 'sources', 'syntheses', 'sessions'];
|
|
13
|
-
const FLAT_FILES = ['index.md', 'todo.md', 'log.md'];
|
|
14
|
-
|
|
15
|
-
/** 项目根路径 → 项目名(入库时的 project 字段)。
|
|
16
|
-
*
|
|
17
|
-
* 取路径末段。为何不引入 UUID(2026-11-02 评估):项目名是给人看的标识,
|
|
18
|
-
* 出现在每条检索结果里;UUID 会让输出不可读。末段同名的风险用 `db status` 暴露,
|
|
19
|
-
* 不做静默处理。
|
|
20
|
-
*/
|
|
21
|
-
function projectNameOf(root) {
|
|
22
|
-
return String(root).split('/').filter(Boolean).pop() || 'unknown';
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
/** 给一批项目根算出**互不重名**的项目名。
|
|
26
|
-
*
|
|
27
|
-
* 为何需要(2026-11-02 实测):项目名默认取路径末段,而机器上同时存在
|
|
28
|
-
* ~/.agents 与 ~/Documents/软件/.agents,两者末段都是 `.agents` →
|
|
29
|
-
* 入库时撞 UNIQUE(project, dir, slug) 约束,整个 rebuild 失败。
|
|
30
|
-
*
|
|
31
|
-
* 消歧规则:重名的那些改用「父目录-末段」,仍重名就继续往上加,直到唯一。
|
|
32
|
-
* 只给重名的项目加前缀 —— 没重名的保持短名(检索结果里读起来更清楚)。
|
|
33
|
-
*/
|
|
34
|
-
export function uniqueProjectNames(roots) {
|
|
35
|
-
const seg = (p) => String(p).split('/').filter(Boolean);
|
|
36
|
-
const base = new Map(roots.map((r) => [r, seg(r).pop() || 'unknown']));
|
|
37
|
-
const groups = new Map();
|
|
38
|
-
for (const [r, n] of base) {
|
|
39
|
-
if (!groups.has(n)) groups.set(n, []);
|
|
40
|
-
groups.get(n).push(r);
|
|
41
|
-
}
|
|
42
|
-
const out = new Map();
|
|
43
|
-
for (const [name, list] of groups) {
|
|
44
|
-
if (list.length === 1) { out.set(list[0], name); continue; }
|
|
45
|
-
// 重名 → 逐层加父目录前缀,直到组内唯一
|
|
46
|
-
for (let depth = 2; depth <= 4; depth++) {
|
|
47
|
-
const cand = new Map();
|
|
48
|
-
for (const r of list) {
|
|
49
|
-
const s = seg(r);
|
|
50
|
-
cand.set(r, s.slice(Math.max(0, s.length - depth)).join('-'));
|
|
51
|
-
}
|
|
52
|
-
const vals = [...cand.values()];
|
|
53
|
-
if (new Set(vals).size === vals.length) {
|
|
54
|
-
for (const [r, v] of cand) out.set(r, v);
|
|
55
|
-
break;
|
|
56
|
-
}
|
|
57
|
-
if (depth === 4) {
|
|
58
|
-
// 深度到 4 仍重名(如 ~/a/x/软件/.agents 与 ~/b/y/软件/.agents 共享 4 层尾路径):
|
|
59
|
-
// 不能「认了」直接塞 —— 那会让两个项目拿到同一个项目名,rebuild 撞
|
|
60
|
-
// UNIQUE(project,dir,slug) 约束整体崩溃(正是本函数要避免的场景)。
|
|
61
|
-
// 给仍碰撞的条目追加序号(-2/-3…),保证互不重名且可读。
|
|
62
|
-
const byVal = new Map();
|
|
63
|
-
for (const [r, v] of cand) {
|
|
64
|
-
if (!byVal.has(v)) { byVal.set(v, []); }
|
|
65
|
-
byVal.get(v).push(r);
|
|
66
|
-
}
|
|
67
|
-
for (const [v, rs] of byVal) {
|
|
68
|
-
if (rs.length === 1) { out.set(rs[0], v); continue; }
|
|
69
|
-
rs.forEach((r, i) => out.set(r, `${v}-${i + 2}`));
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
return out;
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
/** 当前所在项目名。查询时用它决定「读哪几条铁律」。
|
|
78
|
-
*
|
|
79
|
-
* 为何要单独一个函数(2026-11-02):原来只在 cmdDbSearch 里内联算,
|
|
80
|
-
* 与 rebuild 用的算法是两份代码 —— 任一边改了项目名规则,铁律就会读错项目的。
|
|
81
|
-
* 判据:项目名的算法只能有一处。 */
|
|
82
|
-
async function currentProjectName(dir) {
|
|
83
|
-
if (process.env.ABS_PROJECT) return process.env.ABS_PROJECT;
|
|
84
|
-
const from = dir || process.cwd();
|
|
85
|
-
const root = await findBrainRoot(from);
|
|
86
|
-
return root ? projectNameOf(root) : '';
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
/** 扫一个项目的 .brain/ → 页面数组(含 index/todo/log 三份平铺文件)。 */
|
|
90
|
-
async function scanProject(root, project) {
|
|
91
|
-
const out = [];
|
|
92
|
-
for (const d of PAGE_DIRS) {
|
|
93
|
-
const p = brainPath(root, d);
|
|
94
|
-
let files;
|
|
95
|
-
try { files = await fs.readdir(p); } catch { continue; }
|
|
96
|
-
for (const f of files) {
|
|
97
|
-
if (!f.endsWith('.md') || f.startsWith('_')) continue;
|
|
98
|
-
const full = join(p, f);
|
|
99
|
-
try {
|
|
100
|
-
const [body, st] = await Promise.all([fs.readFile(full, 'utf8'), fs.stat(full)]);
|
|
101
|
-
out.push({ project, dir: d, slug: f.replace(/\.md$/, ''), body, mtime: st.mtimeMs });
|
|
102
|
-
} catch { /* 读不到就跳过,不阻断整库 */ }
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
// 用户要求:todo / index / log 也进库。dir 为空串表示"根目录平铺文件"。
|
|
106
|
-
for (const f of FLAT_FILES) {
|
|
107
|
-
try {
|
|
108
|
-
const full = brainPath(root, f);
|
|
109
|
-
const [body, st] = await Promise.all([fs.readFile(full, 'utf8'), fs.stat(full)]);
|
|
110
|
-
out.push({ project, dir: '', slug: f.replace(/\.md$/, ''), body, mtime: st.mtimeMs });
|
|
111
|
-
} catch { /* 不存在 */ }
|
|
112
|
-
}
|
|
113
|
-
return out;
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
/** 项目登记表。~/.abs/projects.json:一行一个项目根路径。
|
|
117
|
-
* 用登记表而不是自动发现 —— 自动扫根目录会误收无关项目,且用户项目未必在一处。 */
|
|
118
|
-
function projectsFile() {
|
|
119
|
-
// 坑(2026-11-01 实测):曾写 join(dbPath(), '../../projects.json') —— 那是把
|
|
120
|
-
// graph.db 当**目录**往上退两级,实际落到 ~/projects.json(家目录),不在 ~/.abs/。
|
|
121
|
-
// 判据:配置与库同目录,用户找得到、备份得走一起。
|
|
122
|
-
return process.env.ABS_PROJECTS || join(dirname(dbPath()), 'projects.json');
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
async function readProjects() {
|
|
126
|
-
try {
|
|
127
|
-
const t = await fs.readFile(projectsFile(), 'utf8');
|
|
128
|
-
const j = JSON.parse(t);
|
|
129
|
-
return Array.isArray(j.projects) ? j.projects : [];
|
|
130
|
-
} catch {
|
|
131
|
-
return [];
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
/** 登记一个项目(幂等)。 */
|
|
136
|
-
export async function addProject(dir) {
|
|
137
|
-
const root = resolve(dir);
|
|
138
|
-
const p = projectsFile();
|
|
139
|
-
const list = await readProjects();
|
|
140
|
-
if (!list.includes(root)) list.push(root);
|
|
141
|
-
await fs.mkdir(join(p, '..'), { recursive: true });
|
|
142
|
-
await fs.writeFile(p, JSON.stringify({ projects: list }, null, 2) + '\n');
|
|
143
|
-
return root;
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
/** `abs db rebuild` —— 扫所有登记项目,全量重建库。 */
|
|
147
|
-
export async function cmdDbRebuild({ dir } = {}) {
|
|
148
|
-
const registered = await readProjects();
|
|
149
|
-
// 当前目录即使没登记也纳入(用户就地跑 rebuild 应生效)
|
|
150
|
-
const here = dir ? resolve(dir) : await findBrainRoot(process.cwd());
|
|
151
|
-
const roots = [...new Set([...registered, ...(here ? [here] : [])])];
|
|
152
|
-
|
|
153
|
-
if (!roots.length) {
|
|
154
|
-
return '✗ 没有可入的项目。先在项目根跑: abs db add 或 abs db rebuild';
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
// 先收集所有有效根,再统一算项目名 —— 名字要去重,必须看到全局才知道谁重名
|
|
158
|
-
const found = [];
|
|
159
|
-
for (const root of roots) {
|
|
160
|
-
const r = await findBrainRoot(root);
|
|
161
|
-
if (r) found.push(r); // 登记了但目录没了 → 跳过,不报错
|
|
162
|
-
}
|
|
163
|
-
const names = uniqueProjectNames([...new Set(found)]);
|
|
164
|
-
const pages = [];
|
|
165
|
-
const used = [];
|
|
166
|
-
for (const f of [...new Set(found)]) {
|
|
167
|
-
const project = names.get(f);
|
|
168
|
-
pages.push(...await scanProject(f, project));
|
|
169
|
-
used.push(`${project} (${f})`);
|
|
170
|
-
}
|
|
171
|
-
if (!pages.length) return '✗ 登记的项目都没有 .brain/ 图谱,无可入库内容。';
|
|
172
|
-
|
|
173
|
-
const path = dbPath();
|
|
174
|
-
await ensureDbDir(path);
|
|
175
|
-
const h = openDb(path);
|
|
176
|
-
try {
|
|
177
|
-
const st = rebuild(h, pages);
|
|
178
|
-
const lines = [
|
|
179
|
-
`✓ 已重建 ${path}`,
|
|
180
|
-
` 项目 ${used.length}: ${used.join(', ')}`,
|
|
181
|
-
` 节点 ${st.nodes} | 边 ${st.edges} | 悬挂 ${st.dangling} | 断点词 ${st.anchors} | 铁律 ${st.rules}`,
|
|
182
|
-
];
|
|
183
|
-
if (st.dangling) {
|
|
184
|
-
lines.push('', `⚠ ${st.dangling} 条链接指向不存在的页(abs db status 可查)`);
|
|
185
|
-
}
|
|
186
|
-
return lines.join('\n');
|
|
187
|
-
} finally {
|
|
188
|
-
h.close();
|
|
189
|
-
}
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
/** `abs db search <词>` —— 走库搜索:FTS 种子 → 亮度扩散。 */
|
|
193
|
-
export async function cmdDbSearch({ query, limit = 8, full = false, noRules = false, cmdName = 'db search' } = {}) {
|
|
194
|
-
const q = String(query || '').trim();
|
|
195
|
-
if (!q) return '用法: abs db search <词>';
|
|
196
|
-
|
|
197
|
-
const path = dbPath();
|
|
198
|
-
const exists = await fs.stat(path).then(() => true).catch(() => false);
|
|
199
|
-
if (!exists) return `✗ 库还没建(${path})。先跑: abs db rebuild`;
|
|
200
|
-
|
|
201
|
-
const h = openDb(path);
|
|
202
|
-
try {
|
|
203
|
-
// L1 铁律永远先出 —— 「不可逾越」的代码保证,不靠读到文本才遵守
|
|
204
|
-
const project = await currentProjectName();
|
|
205
|
-
const rules = rulesOf(h, project);
|
|
206
|
-
|
|
207
|
-
const seeds = search(h, q, { limit: 12 });
|
|
208
|
-
const lit = illuminate(h, seeds, { limit });
|
|
209
|
-
|
|
210
|
-
// 命令名随调用方式变(2026-11-06):顶层叫 search,别名仍走 db search。
|
|
211
|
-
// 直接印死 'db search' 会让新命令的输出自相矛盾。
|
|
212
|
-
const out = [`${cmdName} [${q}] → 种子 ${seeds.length} 条,亮起 ${lit.length} 页`];
|
|
213
|
-
// --no-rules:不输出 L1 铁律(2026-11-05)。
|
|
214
|
-
// 为何要它:铁律 18 条,占输出近一半。开工时(abs load)确实该先看,
|
|
215
|
-
// 但「查一个主题」时它们是噪音 —— 实测同一查询 43 行里有 4 行是铁律区,
|
|
216
|
-
// 而 MCP 侧每次调用都受这个成本影响。省略不等于不遵守:铁律由 load 负责送达。
|
|
217
|
-
if (rules.length && !noRules) {
|
|
218
|
-
out.push('', `--- L1 铁律 ${rules.length} 条(任何检索先出)---`);
|
|
219
|
-
for (const r of rules) out.push(`- ${r}`);
|
|
220
|
-
}
|
|
221
|
-
if (!lit.length) {
|
|
222
|
-
out.push('', lastOpenRebuilt()
|
|
223
|
-
? '(库文件曾损坏被清空,现在没有内容:跑 abs db rebuild 重建)'
|
|
224
|
-
: '(无命中。库可能是旧的:abs db rebuild 重扫)');
|
|
225
|
-
return out.join('\n');
|
|
226
|
-
}
|
|
227
|
-
// --full:显式要求时才读正文。默认不给 —— 这是索引优先的核心。
|
|
228
|
-
// 实测(2026-11-02):ABS 单页平均 824 token,8 页全带就是 6600 token,
|
|
229
|
-
// 而真正被用上的往往只有 1-2 页。让「看全文」成为一个显式动作。
|
|
230
|
-
const bodies = new Map();
|
|
231
|
-
if (full) {
|
|
232
|
-
const root = await findBrainRoot(process.cwd());
|
|
233
|
-
if (root) {
|
|
234
|
-
for (const p of lit) {
|
|
235
|
-
const body = await fs.readFile(brainPath(root, p.dir, `${p.slug}.md`), 'utf8').catch(() => '');
|
|
236
|
-
if (body) bodies.set(p.id, body);
|
|
237
|
-
}
|
|
238
|
-
}
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
out.push('', '--- 按亮度排序 ---');
|
|
242
|
-
for (const p of lit) {
|
|
243
|
-
// 亮度现在是 0.3~1.0(不再是早期可能到 165 万的异常值),
|
|
244
|
-
// 条长按 1.0 满格算:0.3 → 4 格,1.0 → 12 格。
|
|
245
|
-
const bar = '█'.repeat(Math.max(1, Math.round(Math.min(p.bright, 1) * 12)));
|
|
246
|
-
const where = [p.project, p.dir, p.slug].filter(Boolean).join('/');
|
|
247
|
-
const tag = p.from === 'spread' ? ' ↝' : '';
|
|
248
|
-
// 跨项目的结果要显式标出来 —— 用户得知道「这条是别的项目的经验」,
|
|
249
|
-
// 否则会以为是自己项目的结论(判据、上下文都可能不同)。
|
|
250
|
-
const other = project && p.project && p.project !== project ? ' [跨项目]' : '';
|
|
251
|
-
// 索引优先:给摘要 + 取全文成本,让「要不要看全文」成为一个可算的决定。
|
|
252
|
-
const about = p.digest ? `\n ${p.digest}` : '';
|
|
253
|
-
const cost = p.tokens ? ` ~${p.tokens}tok` : '';
|
|
254
|
-
// 作用分档(gotcha/decision/tradeoff/howto)—— 同层里「哪种更该先看」的线索
|
|
255
|
-
const KIND_ICON = { gotcha: '🔴坑', decision: '🟤决策', tradeoff: '⚖️权衡', howto: '🔵做法' };
|
|
256
|
-
const kind = p.kind && KIND_ICON[p.kind] ? ` ${KIND_ICON[p.kind]}` : '';
|
|
257
|
-
out.push(`${bar} ${p.bright.toFixed(2)} L${p.layer} ${where}${tag}${other}${kind}${cost}${about}`);
|
|
258
|
-
if (full && bodies.has(p.id)) {
|
|
259
|
-
// 正文缩进显示,便于与索引行区分
|
|
260
|
-
const body = bodies.get(p.id).replace(/^---[\s\S]*?---\n/, ''); // 去掉 frontmatter
|
|
261
|
-
out.push(...body.split('\n').slice(0, 60).map((l) => ` │ ${l}`));
|
|
262
|
-
if (body.split('\n').length > 60) out.push(' │ …(已截断,用 abs resolve 看全文)');
|
|
263
|
-
}
|
|
264
|
-
}
|
|
265
|
-
if (noRules) {
|
|
266
|
-
out.push('', '(↝ 表示由邻居扩散点亮,非直接命中;铁律已省略,开工时 abs load 会给)');
|
|
267
|
-
} else {
|
|
268
|
-
out.push('', '(L1 铁律恒在最前;↝ 表示由邻居扩散点亮,非直接命中)');
|
|
269
|
-
}
|
|
270
|
-
out.push(`(只给摘要与成本;要看全文: abs ${cmdName} <词> --full 或 abs resolve <页名>)`);
|
|
271
|
-
if (project && !noRules) {
|
|
272
|
-
out.push(`(铁律只显示本项目 "${project}" 的 —— 别的项目的铁律不外借)`);
|
|
273
|
-
}
|
|
274
|
-
return out.join('\n');
|
|
275
|
-
} finally {
|
|
276
|
-
h.close();
|
|
277
|
-
}
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
/** `abs db status` —— 库的体检:计数、按层分布、项目列表、悬挂链接。 */
|
|
281
|
-
export async function cmdDbStatus() {
|
|
282
|
-
const path = dbPath();
|
|
283
|
-
const exists = await fs.stat(path).then(() => true).catch(() => false);
|
|
284
|
-
if (!exists) return `库不存在: ${path}\n 先跑: abs db rebuild`;
|
|
285
|
-
|
|
286
|
-
const h = openDb(path);
|
|
287
|
-
try {
|
|
288
|
-
const rebuilt = lastOpenRebuilt();
|
|
289
|
-
const s = stats(h);
|
|
290
|
-
const out = [
|
|
291
|
-
`📀 库: ${path}`,
|
|
292
|
-
...(rebuilt ? ['⚠ 库文件曾损坏,已自动清空重建 —— 跑 abs db rebuild 恢复内容'] : []),
|
|
293
|
-
`节点 ${s.nodes} | 边 ${s.edges} | 悬挂链接 ${s.dangling} | 断点词 ${s.anchors} | 铁律 ${s.rules}`,
|
|
294
|
-
'',
|
|
295
|
-
'--- 七层分布 ---',
|
|
296
|
-
];
|
|
297
|
-
const byId = new Map(s.byLayer.map((r) => [r.layer, r.c]));
|
|
298
|
-
for (const l of LAYERS) {
|
|
299
|
-
const c = byId.get(l.id) ?? 0;
|
|
300
|
-
if (l.id === 1) {
|
|
301
|
-
out.push(`L1 ${l.name.padEnd(4)} ${String(s.rules).padStart(4)} 条(Rules 区,不立页)`);
|
|
302
|
-
continue;
|
|
303
|
-
}
|
|
304
|
-
if (l.id === 6) {
|
|
305
|
-
out.push(`L6 ${l.name.padEnd(4)} ${String(s.anchors).padStart(4)} 词(双链词,不立页)`);
|
|
306
|
-
continue;
|
|
307
|
-
}
|
|
308
|
-
out.push(`L${l.id} ${l.name.padEnd(4)} ${String(c).padStart(4)} 页`);
|
|
309
|
-
}
|
|
310
|
-
const dangling = h.db.prepare('SELECT raw FROM edges WHERE dst = 0 ORDER BY raw').all();
|
|
311
|
-
if (dangling.length) {
|
|
312
|
-
out.push('', `--- 悬挂链接 ${dangling.length} 条(目标页不存在)---`);
|
|
313
|
-
for (const d of dangling.slice(0, 10)) out.push(` [[${d.raw}]]`);
|
|
314
|
-
if (dangling.length > 10) out.push(` …另有 ${dangling.length - 10} 条`);
|
|
315
|
-
}
|
|
316
|
-
const projects = h.db.prepare("SELECT DISTINCT project FROM nodes WHERE project != '' ORDER BY project").all();
|
|
317
|
-
if (projects.length) {
|
|
318
|
-
out.push('', `--- 已入库项目 ${projects.length} 个 ---`);
|
|
319
|
-
for (const p of projects) out.push(` ${p.project}`);
|
|
320
|
-
}
|
|
321
|
-
return out.join('\n');
|
|
322
|
-
} finally {
|
|
323
|
-
h.close();
|
|
324
|
-
}
|
|
325
|
-
}
|
|
326
|
-
|
|
327
|
-
export { scanProject };
|
|
328
|
-
|
|
329
|
-
/**
|
|
330
|
-
* 静默自动入库:写入类命令(capture / note / concept)收尾时调。
|
|
331
|
-
*
|
|
332
|
-
* 为何要它(2026-11-01 实测):capture 落盘后不重建,检索就查不到刚写的东西 ——
|
|
333
|
-
* 用户以为记下了,检索却空手而归,是静默失效。实测全量重建只要 0.07 秒(93 节点
|
|
334
|
-
* 2 个项目),比做增量同步的复杂度便宜得多,故一律全量。
|
|
335
|
-
*
|
|
336
|
-
* 边界(关键):
|
|
337
|
-
* · 永不抛错、永不阻塞主流程 —— 库坏了只是检索旧一点,不能让记笔记失败。
|
|
338
|
-
* · 库从没建过时不自动创建:那是用户没打算用库,静默建一个 ~/.abs/graph.db
|
|
339
|
-
* 属于「没让做的事自己加戏」。只有库已存在才刷新。
|
|
340
|
-
* @returns {Promise<{ok:boolean, reason?:string}>}
|
|
341
|
-
*/
|
|
342
|
-
export async function autoIndex() {
|
|
343
|
-
try {
|
|
344
|
-
const path = dbPath();
|
|
345
|
-
const exists = await fs.stat(path).then(() => true).catch(() => false);
|
|
346
|
-
if (!exists) return { ok: false, reason: '库未建(跑 abs db rebuild 建)' };
|
|
347
|
-
await cmdDbRebuildQuiet();
|
|
348
|
-
return { ok: true };
|
|
349
|
-
} catch (e) {
|
|
350
|
-
return { ok: false, reason: e?.message || '重建失败' };
|
|
351
|
-
}
|
|
352
|
-
}
|
|
353
|
-
|
|
354
|
-
/** cmdDbRebuild 的静默版:只重建,不返回给人看的文案。 */
|
|
355
|
-
async function cmdDbRebuildQuiet() {
|
|
356
|
-
const registered = await readProjects();
|
|
357
|
-
const here = await findBrainRoot(process.cwd());
|
|
358
|
-
const roots = [...new Set([...registered, ...(here ? [here] : [])])];
|
|
359
|
-
if (!roots.length) return;
|
|
360
|
-
const found = [];
|
|
361
|
-
for (const root of roots) {
|
|
362
|
-
const r = await findBrainRoot(root);
|
|
363
|
-
if (r) found.push(r);
|
|
364
|
-
}
|
|
365
|
-
const uniq = [...new Set(found)];
|
|
366
|
-
const names = uniqueProjectNames(uniq);
|
|
367
|
-
const pages = [];
|
|
368
|
-
for (const f of uniq) pages.push(...await scanProject(f, names.get(f)));
|
|
369
|
-
if (!pages.length) return;
|
|
370
|
-
const h = openDb(dbPath());
|
|
371
|
-
try { rebuild(h, pages); } finally { h.close(); }
|
|
372
|
-
}
|
|
373
|
-
|
|
374
|
-
/** `abs db evolve` — 知识自我进化的体检:哪些被推翻、哪些堆积、哪些该合并。 */
|
|
375
|
-
export async function cmdDbEvolve({ staleDays = 90 } = {}) {
|
|
376
|
-
const path = dbPath();
|
|
377
|
-
const exists = await fs.stat(path).then(() => true).catch(() => false);
|
|
378
|
-
if (!exists) return `库不存在: ${path}\n 先跑: abs db rebuild`;
|
|
379
|
-
|
|
380
|
-
const h = openDb(path);
|
|
381
|
-
try {
|
|
382
|
-
const { evolveSignals, renderEvolve } = await import('./evolve.js');
|
|
383
|
-
return renderEvolve(evolveSignals(h, { staleDays }), { staleDays });
|
|
384
|
-
} finally {
|
|
385
|
-
h.close();
|
|
386
|
-
}
|
|
387
|
-
}
|