@fanchao8609/agent_brain_sync 1.20.1 → 1.21.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 +6 -0
- package/bin/abs.js +39 -0
- package/package.json +1 -1
- package/skill/abs-agent-brain-sync/SKILL.md +21 -2
- package/src/brain/capture.js +170 -0
- package/src/brain/db-cmd.js +244 -0
- package/src/brain/db.js +406 -0
- package/src/brain/gate.js +19 -0
- package/src/brain/init.js +135 -0
- package/src/brain/layers.js +112 -0
- package/src/brain/load.js +273 -0
- package/src/brain/maintain.js +199 -0
- package/src/brain/rules.js +98 -0
- package/src/brain/shape.js +102 -0
- package/src/brain/summon-cmd.js +62 -0
- package/src/brain/summon.js +236 -0
- package/src/note.js +15 -0
- package/src/store.js +36 -1104
- package/src/text.js +21 -1
- package/src/todo/task-cmd.js +274 -0
package/README.md
CHANGED
|
@@ -117,6 +117,12 @@ abs todo done TASK-1
|
|
|
117
117
|
abs note "一句话经验" --tags 坑,docker # 经验暂存 → sources/
|
|
118
118
|
abs log "完成 X" # 记一行流水;abs log 无参 = 查看
|
|
119
119
|
abs query <词> # 检索图谱(多词 OR);已被推翻的经验默认隐藏(--all 可看)
|
|
120
|
+
abs capture [--layer 1-7] "一句话" # 按七层判层后直接落到位(不传 = L4 认知)
|
|
121
|
+
abs db rebuild # 扫所有登记项目 → 建图数据库(~/.abs/graph.db)
|
|
122
|
+
abs db search <词> # 走库检索:FTS 拿种子 + 亮度扩散,L1 铁律恒在最前
|
|
123
|
+
abs db status # 库体检:七层分布 / 悬挂链接 / 已入库项目
|
|
124
|
+
abs db add # 把当前项目登记进 ~/.abs/projects.json
|
|
125
|
+
abs summon [--mark|--status] # 会话总结托底:列出未总结 / 有更新的会话(不写盘)
|
|
120
126
|
abs resolve <页名或id> # 反查页面路径(页改名后 id 不变)
|
|
121
127
|
abs supersede <页名> [--by <新页>] # 标记经验已失效(不删文件,保留历史;query 默认不再返回)
|
|
122
128
|
abs status # 当前项目 + 图谱概要
|
package/bin/abs.js
CHANGED
|
@@ -3,6 +3,9 @@
|
|
|
3
3
|
// abs <cmd> [args]
|
|
4
4
|
// 命令: init / board / status / load / task / install / uninstall / help
|
|
5
5
|
import { cmdInit, cmdStatus, cmdLoad, cmdTask, cmdLog, cmdQuery, cmdLint, cmdNote, cmdConcept, cmdShow, cmdRepair, cmdWrapup, cmdRule, cmdTeardownCheck, cmdTodoArchive, cmdResolve, cmdSupersede, cmdReview } from '../src/store.js';
|
|
6
|
+
import { cmdDbRebuild, cmdDbSearch, cmdDbStatus, addProject } from '../src/brain/db-cmd.js';
|
|
7
|
+
import { cmdCapture } from '../src/brain/capture.js';
|
|
8
|
+
import { cmdSummon } from '../src/brain/summon-cmd.js';
|
|
6
9
|
import { setUser, getUser, userConfigPath } from '../src/userconfig.js';
|
|
7
10
|
import { runInstall, runUninstall } from '../src/install.js';
|
|
8
11
|
import { readFileSync } from 'node:fs';
|
|
@@ -101,6 +104,13 @@ const FLAG_SPEC = {
|
|
|
101
104
|
'session': { type: 'string' },
|
|
102
105
|
'payload': { type: 'string' },
|
|
103
106
|
'tags': { type: 'string' },
|
|
107
|
+
// db search 用:最多返回几页(亮度排序后的截断)。未声明会被静默吞成 true。
|
|
108
|
+
'limit': { type: 'string' },
|
|
109
|
+
// capture 用:1-7 的层号。未声明会被静默吞成 true。
|
|
110
|
+
'layer': { type: 'string' },
|
|
111
|
+
// summon 用:布尔开关。未声明会被静默吞成 true(--mark 会变成真值即生效,但 --status 同理)。
|
|
112
|
+
'mark': { type: 'boolean' },
|
|
113
|
+
'status': { type: 'boolean' },
|
|
104
114
|
'port': { type: 'string' },
|
|
105
115
|
// note 的触发条件(何时该读这条经验)—— 供 load 的相关页推荐匹配。
|
|
106
116
|
'when': { type: 'string' },
|
|
@@ -198,6 +208,10 @@ function parseArgv(args) {
|
|
|
198
208
|
all: !!values.all,
|
|
199
209
|
payload: values.payload,
|
|
200
210
|
tags: values.tags,
|
|
211
|
+
limit: values.limit === undefined ? undefined : Number(values.limit),
|
|
212
|
+
layer: values.layer,
|
|
213
|
+
mark: !!values.mark,
|
|
214
|
+
status: !!values.status,
|
|
201
215
|
title: values.title,
|
|
202
216
|
desc: values.desc,
|
|
203
217
|
help: !!values.help,
|
|
@@ -231,8 +245,11 @@ const usage = `abs — 跨会话记忆工具(.brain/ 图谱)
|
|
|
231
245
|
todo 任务看板 (add/note/state/done/archive)
|
|
232
246
|
index | log | status 索引 / 流水 / 概要
|
|
233
247
|
note <经验> [--tags a,b] 经验暂存 → sources/
|
|
248
|
+
capture [--layer 1-7] "…" 按七层判定落点后直接写入 (不传=L4)
|
|
249
|
+
summon [--mark|--status] 会话总结托底: 列出未总结/有更新的会话 (不写盘)
|
|
234
250
|
concept <slug> --title … 建概念页骨架
|
|
235
251
|
query <词…> [--all] 检索知识页 (多词 OR)
|
|
252
|
+
db rebuild|search|status 图数据库: 建库 / 亮度搜索 / 体检
|
|
236
253
|
resolve <页名…> 反查页面路径
|
|
237
254
|
supersede <页名> [--by 页] 标经验已失效
|
|
238
255
|
review [--accept 页…|--reject 页…] 待确认页队列 (draft 升 active/否决)
|
|
@@ -512,6 +529,14 @@ async function main() {
|
|
|
512
529
|
console.log(await cmdTask({ dir: opts.dir, action, id, section: opts.section, note: opts.note, as: opts.as, session: opts.session }));
|
|
513
530
|
break;
|
|
514
531
|
}
|
|
532
|
+
case 'summon': {
|
|
533
|
+
console.log(await cmdSummon({ dir: opts.dir, mark: !!opts.mark, full: !!opts.full, status: !!opts.status, limit: Number(opts.limit) || 15 }));
|
|
534
|
+
break;
|
|
535
|
+
}
|
|
536
|
+
case 'capture': {
|
|
537
|
+
console.log(await cmdCapture({ dir: opts.dir, text: opts._.join(' '), layer: opts.layer, tags: opts.tags, title: opts.title }));
|
|
538
|
+
break;
|
|
539
|
+
}
|
|
515
540
|
case 'note': {
|
|
516
541
|
console.log(await cmdNote({ dir: opts.dir, text: opts._.join(' '), tags: opts.tags, when: opts.when, impact: opts.impact, type: opts.type }));
|
|
517
542
|
break;
|
|
@@ -550,6 +575,20 @@ async function main() {
|
|
|
550
575
|
console.log(await cmdRule({ dir: opts.dir, action: isAdd ? 'add' : 'list', text: rest3.join(' ') }));
|
|
551
576
|
break;
|
|
552
577
|
}
|
|
578
|
+
case 'db': {
|
|
579
|
+
const sub = opts._[0];
|
|
580
|
+
const rest = opts._.slice(1);
|
|
581
|
+
if (sub === 'rebuild') { console.log(await cmdDbRebuild({ dir: opts.dir })); break; }
|
|
582
|
+
if (sub === 'search') { console.log(await cmdDbSearch({ query: rest.join(' '), limit: Number(opts.limit) || 8 })); break; }
|
|
583
|
+
if (sub === 'status') { console.log(await cmdDbStatus()); break; }
|
|
584
|
+
if (sub === 'add') {
|
|
585
|
+
const r = await addProject(opts.dir || process.cwd());
|
|
586
|
+
console.log(`✓ 已登记项目: ${r}\n 建库: abs db rebuild`);
|
|
587
|
+
break;
|
|
588
|
+
}
|
|
589
|
+
console.log('用法: abs db rebuild | search <词> | status | add [--dir <项目根>]');
|
|
590
|
+
break;
|
|
591
|
+
}
|
|
553
592
|
case 'query': {
|
|
554
593
|
console.log(await cmdQuery({ dir: opts.dir, terms: opts._, includeSuperseded: opts.all }));
|
|
555
594
|
break;
|
package/package.json
CHANGED
|
@@ -19,7 +19,8 @@ description: "USE FOR abs / agent-brain-sync 跨会话记忆与任务续接。
|
|
|
19
19
|
abs load # 开机读状态
|
|
20
20
|
abs query <词> # 检索;abs resolve <页名> 反查路径
|
|
21
21
|
abs todo add <id> --note "做什么" # 登记;note/memo/state/done 见「进行中」
|
|
22
|
-
abs
|
|
22
|
+
abs capture [--layer 1-7] "…" # 按七层判定落点后直接写入(不传=L4)
|
|
23
|
+
abs note "经验" [--tags a,b] # 暂存经验(一律 L7 混沌层)
|
|
23
24
|
abs concept <slug> --title "…" # 建概念页骨架
|
|
24
25
|
abs rule add "一句话" # 加 Rules;abs lint 体检;abs init 建图谱
|
|
25
26
|
```
|
|
@@ -93,6 +94,24 @@ abs rule add "一句话" # 加 Rules;abs lint 体检;abs init
|
|
|
93
94
|
3. **读命中页**:`abs query <词>` → `abs resolve <页名或id>` 拿路径,**只读命中那几页**,绝不通读 `.brain/`;多文件汇总交 `ctx_execute` 在沙箱处理,只打印结论。
|
|
94
95
|
4. **续 todo**:把顶部未完成项当当前任务开做;有新任务而 todo 没有 → `abs todo add <id> --note 做什么` 再动工。
|
|
95
96
|
|
|
97
|
+
## 按七层总结(总结前必读)
|
|
98
|
+
|
|
99
|
+
七层不是分类好看,是**检索权重不同**:L1 永不衰减,L7 几乎不参与。所以总结时必须先判层,判完直接落到位 —— 一律塞 sources/ 会让 44% 的知识按错误的权重参与检索。
|
|
100
|
+
|
|
101
|
+
| 层 | 判据 | 落点 | 怎么落 |
|
|
102
|
+
|---|---|---|---|
|
|
103
|
+
| L1 铁律 | 违反就必须停下 | index 的 `## Rules` | `abs capture --layer 1`(≤42 字、禁链接) |
|
|
104
|
+
| L2 规则 | 这个项目要遵守的做法 | `concepts/` + `layer: 2` | `abs capture --layer 2` |
|
|
105
|
+
| L3 工具 | 某工具怎么用、有什么坑 | `concepts/` + `layer: 3` | `abs capture --layer 3` |
|
|
106
|
+
| L4 认知 | 踩坑的根因与解法 | `concepts/` + `layer: 4` | `abs capture "…"`(默认) |
|
|
107
|
+
| L5 会话 | 这次做了什么 | `log.md` / `sessions/` | `abs log` / `abs todo done` |
|
|
108
|
+
| L6 断点 | 双链词 | 不立页,机器自动抽 | 不接手写 |
|
|
109
|
+
| L7 混沌 | 还没想清的碎片 | `sources/` | `abs capture --layer 7` |
|
|
110
|
+
|
|
111
|
+
判据不明时用 L4 —— 但**知道是工具经验就写 L3**:实测算过,本仓 52 页 concepts 里 23 页其实是 L3。
|
|
112
|
+
|
|
113
|
+
`abs capture` 之后跑 `abs db rebuild` 让图数据库收录,否则检索拿不到它。
|
|
114
|
+
|
|
96
115
|
## 进行中:什么时候动它
|
|
97
116
|
|
|
98
117
|
todo 是随改随写的活看板,和 git commit 同反射。
|
|
@@ -104,7 +123,7 @@ todo 是随改随写的活看板,和 git commit 同反射。
|
|
|
104
123
|
| 被打断 / 干到一半 | `note` + 改到哪个文件哪一步 | 任务下追加一条子项(**一句话**) |
|
|
105
124
|
| 干活中随手记多句发现 | `memo` + 原文 | `sessions/` 当日笔记,**不上看板** |
|
|
106
125
|
| 子任务做完 | `done` + note 结语 + as 落地\|否决\|仅方案 | Done |
|
|
107
|
-
| 总结出经验/坑/规律 | `
|
|
126
|
+
| 总结出经验/坑/规律 | `abs capture --layer N`(先按上表判层) | 按层落到位 |
|
|
108
127
|
|
|
109
128
|
**四个落点的区别**:
|
|
110
129
|
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
// src/brain/capture.js — 按七层判定落点的统一总结入口。
|
|
2
|
+
//
|
|
3
|
+
// 为何需要它(2026-11-01 实测):七层建好了,但总结这个动作还是老的一根管道 ——
|
|
4
|
+
// `abs note` 一律进 sources/(L7),`abs concept` 一律进 concepts/(L4),
|
|
5
|
+
// `abs rule` 一律进 ## Rules(L1)。落点由**命令**写死,不由**内容**决定。
|
|
6
|
+
// 结果是 52 页 concepts 里 23 页(44%)其实是工具经验,全按 L4 的 0.6 衰减算 ——
|
|
7
|
+
// 分层在检索权重上的作用被抹平了。
|
|
8
|
+
//
|
|
9
|
+
// 本命令让判层发生在「总结时」,判完直接落到位,不留二次分拣。
|
|
10
|
+
// 沿用 note.js 的两条既有约定:整条相等才算重复(不是子串)、tmp+rename 原子落盘。
|
|
11
|
+
|
|
12
|
+
import { promises as fs } from 'node:fs';
|
|
13
|
+
import { join } from 'node:path';
|
|
14
|
+
import { requireBrain, brainPath } from '../index.js';
|
|
15
|
+
import { requireUser, atTag } from '../userconfig.js';
|
|
16
|
+
import { today } from '../todo.js';
|
|
17
|
+
import { registerInIndex } from '../note.js';
|
|
18
|
+
import { cmdLog } from '../note.js';
|
|
19
|
+
import { clip, slugOf } from '../text.js';
|
|
20
|
+
import { LAYERS, layerOf, DEFAULT_LAYER } from './layers.js';
|
|
21
|
+
import { autoIndex } from './db-cmd.js';
|
|
22
|
+
|
|
23
|
+
/** 层 → 落点目录。L1/L6 不立页,由下面的分支单独处理。 */
|
|
24
|
+
const LAYER_DIR = {
|
|
25
|
+
2: 'concepts',
|
|
26
|
+
3: 'concepts',
|
|
27
|
+
4: 'concepts',
|
|
28
|
+
5: 'sessions',
|
|
29
|
+
7: 'sources',
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
/** 判据表:输入时就看得到,不用记。 */
|
|
33
|
+
export function layerGuide() {
|
|
34
|
+
const rows = [
|
|
35
|
+
['1', '铁律', '违反就必须停下的事', 'abs capture --layer 1 "…"'],
|
|
36
|
+
['2', '规则', '这个项目要遵守的做法', 'abs capture --layer 2 "…"'],
|
|
37
|
+
['3', '工具', '某工具怎么用、有什么坑', 'abs capture --layer 3 "…"'],
|
|
38
|
+
['4', '认知', '踩坑的根因与解法(默认)', 'abs capture "…"'],
|
|
39
|
+
['5', '会话', '这次会话做了什么', 'abs log / abs todo done'],
|
|
40
|
+
['6', '断点', '双链词,机器自动抽,不用手写', '(无)'],
|
|
41
|
+
['7', '混沌', '还没想清的碎片', 'abs capture --layer 7 "…"'],
|
|
42
|
+
];
|
|
43
|
+
return [
|
|
44
|
+
'按七层判定这一句该落在哪:',
|
|
45
|
+
'',
|
|
46
|
+
'层 名称 判据 命令',
|
|
47
|
+
...rows.map(([id, name, when, cmd]) => `L${id} ${name} ${when.padEnd(26)} ${cmd}`),
|
|
48
|
+
'',
|
|
49
|
+
'不传 --layer 时默认 L4(认知)。',
|
|
50
|
+
'L1 走铁律门槛(≤42 字、禁链接);L6 是机器自动抽的双链词,不接手动。',
|
|
51
|
+
].join('\n');
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export async function cmdCapture({ dir, text, layer, tags, title }) {
|
|
55
|
+
const clean = String(text || '').trim();
|
|
56
|
+
const layerArg = String(layer || '').trim();
|
|
57
|
+
|
|
58
|
+
if (!clean) {
|
|
59
|
+
return [
|
|
60
|
+
'用法: abs capture [--layer 1-7] "一句话" [--tags a,b] [--title "页标题"]',
|
|
61
|
+
'',
|
|
62
|
+
layerGuide(),
|
|
63
|
+
].join('\n');
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// 层号校验:非法直接拒,不静默当默认层(否则记错层没人知道)
|
|
67
|
+
let L = DEFAULT_LAYER;
|
|
68
|
+
if (layerArg) {
|
|
69
|
+
const n = Number(layerArg);
|
|
70
|
+
if (!Number.isInteger(n) || !layerOf(n)) {
|
|
71
|
+
return `✗ --layer 非法(读到 "${layerArg}")。只能是 1-7 的整数。\n\n${layerGuide()}`;
|
|
72
|
+
}
|
|
73
|
+
L = n;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// L6 是机器自动抽的双链词,不接受手动写入 —— 手写会让断点词表失真
|
|
77
|
+
if (L === 6) {
|
|
78
|
+
return '✗ L6 断点层是机器从双链里自动抽的词,不接手写。\n 想加索引词:在页面正文写 [[词]],abs db rebuild 时会自动收录。';
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// L1 铁律:复用 cmdRule 的门槛(≤42 字、禁链接),不另起一套
|
|
82
|
+
if (L === 1) {
|
|
83
|
+
const { cmdRule } = await import('./rules.js');
|
|
84
|
+
const r = await cmdRule({ dir, action: 'add', text: clean });
|
|
85
|
+
return r;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
let root;
|
|
89
|
+
try {
|
|
90
|
+
root = await requireBrain(dir || process.cwd());
|
|
91
|
+
} catch {
|
|
92
|
+
return '未找到 .brain/ 图谱。先在项目根运行: abs init';
|
|
93
|
+
}
|
|
94
|
+
const who = await requireUser();
|
|
95
|
+
const { ensurePersonPage } = await import('../note.js');
|
|
96
|
+
await ensurePersonPage(root, who);
|
|
97
|
+
|
|
98
|
+
const dirName = LAYER_DIR[L];
|
|
99
|
+
if (!dirName) return `✗ L${L} 没有对应目录(这层不立页)。`;
|
|
100
|
+
|
|
101
|
+
const targetDir = brainPath(root, dirName);
|
|
102
|
+
await fs.mkdir(targetDir, { recursive: true });
|
|
103
|
+
|
|
104
|
+
// 幂等:整条相等才算重复(子串判据会把「新经验恰好是旧经验的子串」丢掉)
|
|
105
|
+
const titleLine = `TITLE: ${clean}`;
|
|
106
|
+
for (const f of (await fs.readdir(targetDir).catch(() => [])).filter((x) => x.endsWith('.md'))) {
|
|
107
|
+
const body = await fs.readFile(join(targetDir, f), 'utf8').catch(() => '');
|
|
108
|
+
if (body.split('\n').some((l) => l.trim() === titleLine)) {
|
|
109
|
+
return `• 已落过同文本 → ${dirName}/${f} (跳过重复)`;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const head = String(title || '').trim() || clip(clean, 80);
|
|
114
|
+
const tagList = String(tags || '').split(',').map((t) => t.trim()).filter(Boolean);
|
|
115
|
+
const meta = layerOf(L);
|
|
116
|
+
const slugSrc = slugOf(clean);
|
|
117
|
+
const file = `${today()}-${slugSrc || 'capture'}.md`;
|
|
118
|
+
|
|
119
|
+
const body = [
|
|
120
|
+
'---',
|
|
121
|
+
`tags: [${['concept', ...tagList].join(', ')}]`,
|
|
122
|
+
`layer: ${L}`,
|
|
123
|
+
`author: ${who}`,
|
|
124
|
+
`updated: ${today()}`,
|
|
125
|
+
'status: draft',
|
|
126
|
+
'---',
|
|
127
|
+
'',
|
|
128
|
+
`# ${meta.name}:${head}`,
|
|
129
|
+
'',
|
|
130
|
+
`TITLE: ${clean}`,
|
|
131
|
+
'',
|
|
132
|
+
'## 记录',
|
|
133
|
+
`- ${clean}`,
|
|
134
|
+
'',
|
|
135
|
+
// 「验证」是本仓硬要求(lint 的 NO-TAIL):没尾巴的经验只能被相信、不能被验证。
|
|
136
|
+
// 占位注释不算内容(hasTail 明确跳过 <!-- -->),所以这里给的是**可填的实际位置**,
|
|
137
|
+
// 而不是一句注释就交差 —— 那会让新页天生就带 NO-TAIL。
|
|
138
|
+
'## 验证',
|
|
139
|
+
'- 做完怎么确认:跑什么命令 / 看什么信号 / 按什么判据',
|
|
140
|
+
'',
|
|
141
|
+
'## 关联连接',
|
|
142
|
+
`- ${atTag(who)} — 本页沉淀者`,
|
|
143
|
+
'(挂相关页双链,别留孤岛;本页无人链入时 lint 会报 NO-INBOUND)',
|
|
144
|
+
'',
|
|
145
|
+
].join('\n');
|
|
146
|
+
|
|
147
|
+
// tmp + rename 原子落盘(并发读不会读到半写文件)
|
|
148
|
+
const dst = join(targetDir, file);
|
|
149
|
+
const tmp = join(targetDir, `.${file}.tmp-${Date.now()}-${Math.random().toString(36).slice(2, 6)}`);
|
|
150
|
+
await fs.writeFile(tmp, body, 'utf8');
|
|
151
|
+
await fs.rename(tmp, dst);
|
|
152
|
+
|
|
153
|
+
const slug = file.replace(/\.md$/, '');
|
|
154
|
+
const section = dirName === 'concepts' ? 'Concepts' : dirName === 'sessions' ? 'Sessions' : 'Sources';
|
|
155
|
+
await registerInIndex(root, section, slug, head);
|
|
156
|
+
await cmdLog({ dir: root, title: clean, kind: 'note' });
|
|
157
|
+
|
|
158
|
+
// 落盘即入库 —— 否则用户以为记下了,检索却空手而归(静默失效)。
|
|
159
|
+
// autoIndex 永不抛错也不阻塞:库坏了只是检索旧一点,不能让记笔记失败。
|
|
160
|
+
const idx = await autoIndex();
|
|
161
|
+
|
|
162
|
+
return [
|
|
163
|
+
`✓ 已落入 L${L} ${meta.name}层 → ${dirName}/${file}`,
|
|
164
|
+
` ${clip(clean, 60)} ${atTag(who)}`,
|
|
165
|
+
` 该层衰减 ${meta.decay}(${meta.note})`,
|
|
166
|
+
idx.ok ? ' 已同步入库,abs db search 可查到' : ` (未入库:${idx.reason})`,
|
|
167
|
+
].join('\n');
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
export { LAYERS };
|
|
@@ -0,0 +1,244 @@
|
|
|
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 } 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
|
+
/** 扫一个项目的 .brain/ → 页面数组(含 index/todo/log 三份平铺文件)。 */
|
|
16
|
+
async function scanProject(root, project) {
|
|
17
|
+
const out = [];
|
|
18
|
+
for (const d of PAGE_DIRS) {
|
|
19
|
+
const p = brainPath(root, d);
|
|
20
|
+
let files;
|
|
21
|
+
try { files = await fs.readdir(p); } catch { continue; }
|
|
22
|
+
for (const f of files) {
|
|
23
|
+
if (!f.endsWith('.md') || f.startsWith('_')) continue;
|
|
24
|
+
const full = join(p, f);
|
|
25
|
+
try {
|
|
26
|
+
const [body, st] = await Promise.all([fs.readFile(full, 'utf8'), fs.stat(full)]);
|
|
27
|
+
out.push({ project, dir: d, slug: f.replace(/\.md$/, ''), body, mtime: st.mtimeMs });
|
|
28
|
+
} catch { /* 读不到就跳过,不阻断整库 */ }
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
// 用户要求:todo / index / log 也进库。dir 为空串表示"根目录平铺文件"。
|
|
32
|
+
for (const f of FLAT_FILES) {
|
|
33
|
+
try {
|
|
34
|
+
const full = brainPath(root, f);
|
|
35
|
+
const [body, st] = await Promise.all([fs.readFile(full, 'utf8'), fs.stat(full)]);
|
|
36
|
+
out.push({ project, dir: '', slug: f.replace(/\.md$/, ''), body, mtime: st.mtimeMs });
|
|
37
|
+
} catch { /* 不存在 */ }
|
|
38
|
+
}
|
|
39
|
+
return out;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** 项目登记表。~/.abs/projects.json:一行一个项目根路径。
|
|
43
|
+
* 用登记表而不是自动发现 —— 自动扫根目录会误收无关项目,且用户项目未必在一处。 */
|
|
44
|
+
function projectsFile() {
|
|
45
|
+
// 坑(2026-11-01 实测):曾写 join(dbPath(), '../../projects.json') —— 那是把
|
|
46
|
+
// graph.db 当**目录**往上退两级,实际落到 ~/projects.json(家目录),不在 ~/.abs/。
|
|
47
|
+
// 判据:配置与库同目录,用户找得到、备份得走一起。
|
|
48
|
+
return process.env.ABS_PROJECTS || join(dirname(dbPath()), 'projects.json');
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
async function readProjects() {
|
|
52
|
+
try {
|
|
53
|
+
const t = await fs.readFile(projectsFile(), 'utf8');
|
|
54
|
+
const j = JSON.parse(t);
|
|
55
|
+
return Array.isArray(j.projects) ? j.projects : [];
|
|
56
|
+
} catch {
|
|
57
|
+
return [];
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** 登记一个项目(幂等)。 */
|
|
62
|
+
export async function addProject(dir) {
|
|
63
|
+
const root = resolve(dir);
|
|
64
|
+
const p = projectsFile();
|
|
65
|
+
const list = await readProjects();
|
|
66
|
+
if (!list.includes(root)) list.push(root);
|
|
67
|
+
await fs.mkdir(join(p, '..'), { recursive: true });
|
|
68
|
+
await fs.writeFile(p, JSON.stringify({ projects: list }, null, 2) + '\n');
|
|
69
|
+
return root;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** `abs db rebuild` —— 扫所有登记项目,全量重建库。 */
|
|
73
|
+
export async function cmdDbRebuild({ dir } = {}) {
|
|
74
|
+
const registered = await readProjects();
|
|
75
|
+
// 当前目录即使没登记也纳入(用户就地跑 rebuild 应生效)
|
|
76
|
+
const here = dir ? resolve(dir) : await findBrainRoot(process.cwd());
|
|
77
|
+
const roots = [...new Set([...registered, ...(here ? [here] : [])])];
|
|
78
|
+
|
|
79
|
+
if (!roots.length) {
|
|
80
|
+
return '✗ 没有可入的项目。先在项目根跑: abs db add 或 abs db rebuild';
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const pages = [];
|
|
84
|
+
const used = [];
|
|
85
|
+
for (const root of roots) {
|
|
86
|
+
const found = await findBrainRoot(root);
|
|
87
|
+
if (!found) continue; // 登记了但目录没了 → 跳过,不报错
|
|
88
|
+
const project = found.split('/').filter(Boolean).pop() || 'unknown';
|
|
89
|
+
pages.push(...await scanProject(found, project));
|
|
90
|
+
used.push(`${project} (${found})`);
|
|
91
|
+
}
|
|
92
|
+
if (!pages.length) return '✗ 登记的项目都没有 .brain/ 图谱,无可入库内容。';
|
|
93
|
+
|
|
94
|
+
const path = dbPath();
|
|
95
|
+
await ensureDbDir(path);
|
|
96
|
+
const h = openDb(path);
|
|
97
|
+
try {
|
|
98
|
+
const st = rebuild(h, pages);
|
|
99
|
+
const lines = [
|
|
100
|
+
`✓ 已重建 ${path}`,
|
|
101
|
+
` 项目 ${used.length}: ${used.join(', ')}`,
|
|
102
|
+
` 节点 ${st.nodes} | 边 ${st.edges} | 悬挂 ${st.dangling} | 断点词 ${st.anchors} | 铁律 ${st.rules}`,
|
|
103
|
+
];
|
|
104
|
+
if (st.dangling) {
|
|
105
|
+
lines.push('', `⚠ ${st.dangling} 条链接指向不存在的页(abs db status 可查)`);
|
|
106
|
+
}
|
|
107
|
+
return lines.join('\n');
|
|
108
|
+
} finally {
|
|
109
|
+
h.close();
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** `abs db search <词>` —— 走库搜索:FTS 种子 → 亮度扩散。 */
|
|
114
|
+
export async function cmdDbSearch({ query, limit = 8 } = {}) {
|
|
115
|
+
const q = String(query || '').trim();
|
|
116
|
+
if (!q) return '用法: abs db search <词>';
|
|
117
|
+
|
|
118
|
+
const path = dbPath();
|
|
119
|
+
const exists = await fs.stat(path).then(() => true).catch(() => false);
|
|
120
|
+
if (!exists) return `✗ 库还没建(${path})。先跑: abs db rebuild`;
|
|
121
|
+
|
|
122
|
+
const h = openDb(path);
|
|
123
|
+
try {
|
|
124
|
+
// L1 铁律永远先出 —— 「不可逾越」的代码保证,不靠读到文本才遵守
|
|
125
|
+
const project = process.env.ABS_PROJECT
|
|
126
|
+
|| (await findBrainRoot(process.cwd()) || '').split('/').filter(Boolean).pop()
|
|
127
|
+
|| '';
|
|
128
|
+
const rules = rulesOf(h, project);
|
|
129
|
+
|
|
130
|
+
const seeds = search(h, q, { limit: 12 });
|
|
131
|
+
const lit = illuminate(h, seeds, { limit });
|
|
132
|
+
|
|
133
|
+
const out = [`db search [${q}] → 种子 ${seeds.length} 条,亮起 ${lit.length} 页`];
|
|
134
|
+
if (rules.length) {
|
|
135
|
+
out.push('', `--- L1 铁律 ${rules.length} 条(任何检索先出)---`);
|
|
136
|
+
for (const r of rules) out.push(`- ${r}`);
|
|
137
|
+
}
|
|
138
|
+
if (!lit.length) {
|
|
139
|
+
out.push('', '(无命中。库可能是旧的:abs db rebuild 重扫)');
|
|
140
|
+
return out.join('\n');
|
|
141
|
+
}
|
|
142
|
+
out.push('', '--- 按亮度排序 ---');
|
|
143
|
+
for (const p of lit) {
|
|
144
|
+
const bar = '█'.repeat(Math.max(1, Math.round(Math.min(p.bright, 3) / 3 * 12)));
|
|
145
|
+
const where = [p.project, p.dir, p.slug].filter(Boolean).join('/');
|
|
146
|
+
const tag = p.from === 'spread' ? ' ↝' : '';
|
|
147
|
+
out.push(`${bar} ${p.bright.toFixed(2)} L${p.layer} ${where}${tag}`);
|
|
148
|
+
}
|
|
149
|
+
out.push('', '(L1 铁律恒在最前;↝ 表示由邻居扩散点亮,非直接命中)');
|
|
150
|
+
return out.join('\n');
|
|
151
|
+
} finally {
|
|
152
|
+
h.close();
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/** `abs db status` —— 库的体检:计数、按层分布、项目列表、悬挂链接。 */
|
|
157
|
+
export async function cmdDbStatus() {
|
|
158
|
+
const path = dbPath();
|
|
159
|
+
const exists = await fs.stat(path).then(() => true).catch(() => false);
|
|
160
|
+
if (!exists) return `库不存在: ${path}\n 先跑: abs db rebuild`;
|
|
161
|
+
|
|
162
|
+
const h = openDb(path);
|
|
163
|
+
try {
|
|
164
|
+
const s = stats(h);
|
|
165
|
+
const out = [
|
|
166
|
+
`📀 库: ${path}`,
|
|
167
|
+
`节点 ${s.nodes} | 边 ${s.edges} | 悬挂链接 ${s.dangling} | 断点词 ${s.anchors} | 铁律 ${s.rules}`,
|
|
168
|
+
'',
|
|
169
|
+
'--- 七层分布 ---',
|
|
170
|
+
];
|
|
171
|
+
const byId = new Map(s.byLayer.map((r) => [r.layer, r.c]));
|
|
172
|
+
for (const l of LAYERS) {
|
|
173
|
+
const c = byId.get(l.id) ?? 0;
|
|
174
|
+
if (l.id === 1) {
|
|
175
|
+
out.push(`L1 ${l.name.padEnd(4)} ${String(s.rules).padStart(4)} 条(Rules 区,不立页)`);
|
|
176
|
+
continue;
|
|
177
|
+
}
|
|
178
|
+
if (l.id === 6) {
|
|
179
|
+
out.push(`L6 ${l.name.padEnd(4)} ${String(s.anchors).padStart(4)} 词(双链词,不立页)`);
|
|
180
|
+
continue;
|
|
181
|
+
}
|
|
182
|
+
out.push(`L${l.id} ${l.name.padEnd(4)} ${String(c).padStart(4)} 页`);
|
|
183
|
+
}
|
|
184
|
+
const dangling = h.db.prepare('SELECT raw FROM edges WHERE dst = 0 ORDER BY raw').all();
|
|
185
|
+
if (dangling.length) {
|
|
186
|
+
out.push('', `--- 悬挂链接 ${dangling.length} 条(目标页不存在)---`);
|
|
187
|
+
for (const d of dangling.slice(0, 10)) out.push(` [[${d.raw}]]`);
|
|
188
|
+
if (dangling.length > 10) out.push(` …另有 ${dangling.length - 10} 条`);
|
|
189
|
+
}
|
|
190
|
+
const projects = h.db.prepare("SELECT DISTINCT project FROM nodes WHERE project != '' ORDER BY project").all();
|
|
191
|
+
if (projects.length) {
|
|
192
|
+
out.push('', `--- 已入库项目 ${projects.length} 个 ---`);
|
|
193
|
+
for (const p of projects) out.push(` ${p.project}`);
|
|
194
|
+
}
|
|
195
|
+
return out.join('\n');
|
|
196
|
+
} finally {
|
|
197
|
+
h.close();
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
export { scanProject };
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* 静默自动入库:写入类命令(capture / note / concept)收尾时调。
|
|
205
|
+
*
|
|
206
|
+
* 为何要它(2026-11-01 实测):capture 落盘后不重建,检索就查不到刚写的东西 ——
|
|
207
|
+
* 用户以为记下了,检索却空手而归,是静默失效。实测全量重建只要 0.07 秒(93 节点
|
|
208
|
+
* 2 个项目),比做增量同步的复杂度便宜得多,故一律全量。
|
|
209
|
+
*
|
|
210
|
+
* 边界(关键):
|
|
211
|
+
* · 永不抛错、永不阻塞主流程 —— 库坏了只是检索旧一点,不能让记笔记失败。
|
|
212
|
+
* · 库从没建过时不自动创建:那是用户没打算用库,静默建一个 ~/.abs/graph.db
|
|
213
|
+
* 属于「没让做的事自己加戏」。只有库已存在才刷新。
|
|
214
|
+
* @returns {Promise<{ok:boolean, reason?:string}>}
|
|
215
|
+
*/
|
|
216
|
+
export async function autoIndex() {
|
|
217
|
+
try {
|
|
218
|
+
const path = dbPath();
|
|
219
|
+
const exists = await fs.stat(path).then(() => true).catch(() => false);
|
|
220
|
+
if (!exists) return { ok: false, reason: '库未建(跑 abs db rebuild 建)' };
|
|
221
|
+
await cmdDbRebuildQuiet();
|
|
222
|
+
return { ok: true };
|
|
223
|
+
} catch (e) {
|
|
224
|
+
return { ok: false, reason: e?.message || '重建失败' };
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/** cmdDbRebuild 的静默版:只重建,不返回给人看的文案。 */
|
|
229
|
+
async function cmdDbRebuildQuiet() {
|
|
230
|
+
const registered = await readProjects();
|
|
231
|
+
const here = await findBrainRoot(process.cwd());
|
|
232
|
+
const roots = [...new Set([...registered, ...(here ? [here] : [])])];
|
|
233
|
+
if (!roots.length) return;
|
|
234
|
+
const pages = [];
|
|
235
|
+
for (const root of roots) {
|
|
236
|
+
const found = await findBrainRoot(root);
|
|
237
|
+
if (!found) continue;
|
|
238
|
+
const project = found.split('/').filter(Boolean).pop() || 'unknown';
|
|
239
|
+
pages.push(...await scanProject(found, project));
|
|
240
|
+
}
|
|
241
|
+
if (!pages.length) return;
|
|
242
|
+
const h = openDb(dbPath());
|
|
243
|
+
try { rebuild(h, pages); } finally { h.close(); }
|
|
244
|
+
}
|