@fanchao8609/agent_brain_sync 1.8.7 → 1.8.9
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/bin/abs.js +54 -17
- package/hooks/abs.opencode.ts +3 -6
- package/hooks/abs.pi.ts +4 -7
- package/hooks/event.sh +8 -0
- package/package.json +2 -2
- package/skill/abs-agent-brain-sync/SKILL.md +27 -4
- package/skill/abs-bug-hunter/SKILL.md +109 -158
- package/src/index.js +22 -3
- package/src/relevant.js +244 -0
- package/src/serve.js +505 -0
- package/src/store.js +135 -30
- package/src/todo.js +1 -1
- package/src/userconfig.js +9 -5
package/bin/abs.js
CHANGED
|
@@ -73,6 +73,9 @@ const FLAG_SPEC = {
|
|
|
73
73
|
'as': { type: 'string' },
|
|
74
74
|
'payload': { type: 'string' },
|
|
75
75
|
'tags': { type: 'string' },
|
|
76
|
+
'port': { type: 'string' },
|
|
77
|
+
// note 的触发条件(何时该读这条经验)—— 供 load 的相关页推荐匹配。
|
|
78
|
+
'when': { type: 'string' },
|
|
76
79
|
// concept 骨架用: 不在 FLAG_SPEC 里的 `--title` 会被静默当布尔 true(见 parseArgv 注释),
|
|
77
80
|
// 于是 `--title "一句话"` 的正文会进位置参数 → 必须在这声明。
|
|
78
81
|
'title': { type: 'string' },
|
|
@@ -175,46 +178,53 @@ function parseArgv(args) {
|
|
|
175
178
|
}
|
|
176
179
|
const usage = `abs — agent-brain-sync 记忆工具
|
|
177
180
|
|
|
178
|
-
|
|
181
|
+
快速开始:
|
|
182
|
+
abs init 在项目里建 .brain/ 图谱
|
|
183
|
+
abs load 看当前状态 (每次开工先跑这个)
|
|
184
|
+
abs todo add X --note "做什么" 登记一个任务
|
|
185
|
+
abs note "经验一句话" 随手记一条经验 → sources/
|
|
186
|
+
|
|
187
|
+
以上四条覆盖日常使用的 90%。
|
|
188
|
+
|
|
189
|
+
读写:
|
|
179
190
|
abs load 开机读状态 (index/todo/log)
|
|
180
191
|
abs todo 任务看板 Todo / Done(未完成的都在这,行首带状态)
|
|
181
192
|
abs index 图谱索引 index.md
|
|
182
193
|
abs log 流水 log.md
|
|
183
194
|
abs status 当前项目 + 图谱概要
|
|
184
|
-
|
|
185
|
-
写:
|
|
195
|
+
abs serve [--port 7777] 在浏览器里浏览 .brain/(只读,仅本机)
|
|
186
196
|
abs todo add <id> [--note ..] [--section ..] 登记任务 (start 同义)
|
|
187
|
-
abs todo note <id> --note "断点/进度" 实时落 ↳ 断点
|
|
197
|
+
abs todo note <id> --note "断点/进度" 实时落 ↳ 断点 行(建议前缀: 验证: / 边界: / 阻塞:)
|
|
188
198
|
abs todo state <id> --note "进行中|讨论中|滞留中" 改状态标记(原地,不搬区)
|
|
189
199
|
abs todo done <id> [--as 落地|否决|仅方案] 完成;结语标明到底"做成了没有"
|
|
190
200
|
默认 落地。否决=评估后不做(含做了又撤);仅方案=只设计过
|
|
191
201
|
不加结语或结语失真会让下一个会话把"想过"当成"做完了"。
|
|
192
202
|
abs log "完成 X:…" 记一行工作成果 (无参=查看)
|
|
193
|
-
abs note "
|
|
203
|
+
abs note "经验" [--tags 坑,docker] [--when "何时该读"] 经验实时暂存 → sources/
|
|
194
204
|
abs concept <slug> --title "标题" [--tags a,b] [--desc "index 描述"]
|
|
195
205
|
建概念页骨架(头/中/尾四段位置)。只给结构不给内容
|
|
196
206
|
|
|
197
|
-
|
|
207
|
+
检索与维护:
|
|
198
208
|
abs query <词1> [词2 …] [--all]
|
|
199
209
|
检索 .brain/ 知识页 (多词 OR);superseded 默认隐藏
|
|
200
210
|
abs resolve <id-or-slug> [更多…]
|
|
201
211
|
按 id/页面名反查路径 (页改名后 id 不变,仍能找回)
|
|
202
212
|
abs supersede <页名> [--by <取代它的页>]
|
|
203
213
|
标记一条经验已失效 (不删文件;query/load 默认不再展示)
|
|
204
|
-
abs todo archive [--keep-days N] [--dry-run]
|
|
205
|
-
归档 Done 区旧日期组 → sessions/<日期>-todo归档.md
|
|
206
|
-
(默认保留近 3 天; 任一天有未完成则整天不归档)
|
|
207
214
|
abs lint 图谱体检 (死链/孤岛/超尺寸/堆积)
|
|
208
215
|
abs rule 列出 index.md 的 ## Rules 硬规则
|
|
209
|
-
abs rule add "一句话" 追加一条硬规则 (
|
|
216
|
+
abs rule add "一句话" 追加一条硬规则 (只放违反会丢数据/静默失效级的)
|
|
217
|
+
abs todo archive [--keep-days N] [--dry-run]
|
|
218
|
+
归档 Done 区旧日期组 → sessions/<日期>-todo归档.md
|
|
219
|
+
|
|
220
|
+
安装与配置 (一次性):
|
|
221
|
+
abs install [--agent <宿主>] 安装 MCP+hook+skill (宿主: claude-code/codex/opencode/pi)
|
|
222
|
+
abs uninstall [--agent <...>] 卸载
|
|
223
|
+
abs update 升级到最新版并刷新四宿主 hook/skill
|
|
210
224
|
abs config [show] 查看使用者姓名 (标记作者用)
|
|
211
225
|
abs config set user <名字> 设置使用者姓名 → ~/.abs/config.json
|
|
212
|
-
|
|
213
|
-
临时覆盖: ABS_USER=<名字> abs ...
|
|
226
|
+
未设置时写操作会报错要求先设置 (临时: ABS_USER=<名字> abs ...)
|
|
214
227
|
abs init [--repair] 建 .brain/ 图谱; 结构不完整时报明细, --repair 只补缺不覆盖
|
|
215
|
-
abs install [--agent <宿主>] 安装 MCP+hook+skill (宿主: claude-code/codex/opencode/pi)
|
|
216
|
-
abs uninstall [--agent <...>] 卸载
|
|
217
|
-
abs update 升级到最新版并刷新四宿主 hook/skill
|
|
218
228
|
abs --version 显示当前版本
|
|
219
229
|
abs help 本帮助
|
|
220
230
|
|
|
@@ -356,6 +366,25 @@ async function main() {
|
|
|
356
366
|
rejectExtra(opts._, 'abs status');
|
|
357
367
|
console.log(await cmdStatus({ dir: opts.dir }));
|
|
358
368
|
break;
|
|
369
|
+
// abs serve —— 把 .brain/ 挂成只读网页(浏览器无法自己列目录,所以必须有服务端)
|
|
370
|
+
case 'serve': {
|
|
371
|
+
rejectExtra(opts._, 'abs serve');
|
|
372
|
+
const { serve } = await import('../src/serve.js');
|
|
373
|
+
const { requireBrain, brainPath } = await import('../src/index.js');
|
|
374
|
+
// 坑(2026-09-16 实测): requireBrain 返回的是【项目根】, .brain/ 在它下面 ——
|
|
375
|
+
// 直接把项目根当服务根会扫到 node_modules。必须走 brainPath()。
|
|
376
|
+
const root = brainPath(await requireBrain(opts.dir || process.cwd()));
|
|
377
|
+
const port = opts.port ? Number(opts.port) : 7777;
|
|
378
|
+
const { url, server } = await serve({ root, port });
|
|
379
|
+
console.log(`📖 abs serve → ${url}`);
|
|
380
|
+
console.log(` 根: ${root}`);
|
|
381
|
+
console.log(' (只读,仅本机可访;Ctrl+C 停止)');
|
|
382
|
+
if (opts.open !== false) runCmd('open', [url]);
|
|
383
|
+
// 不断开进程:服务要活着才有用
|
|
384
|
+
await new Promise(() => {});
|
|
385
|
+
server.unref();
|
|
386
|
+
break;
|
|
387
|
+
}
|
|
359
388
|
// abs todo —— 无子命令=看板;带子命令=任务写操作
|
|
360
389
|
case 'todo': {
|
|
361
390
|
const [sub, id, ...rest2] = opts._;
|
|
@@ -386,7 +415,7 @@ async function main() {
|
|
|
386
415
|
break;
|
|
387
416
|
}
|
|
388
417
|
case 'note': {
|
|
389
|
-
console.log(await cmdNote({ dir: opts.dir, text: opts._.join(' '), tags: opts.tags }));
|
|
418
|
+
console.log(await cmdNote({ dir: opts.dir, text: opts._.join(' '), tags: opts.tags, when: opts.when }));
|
|
390
419
|
break;
|
|
391
420
|
}
|
|
392
421
|
case 'concept': {
|
|
@@ -461,7 +490,15 @@ async function main() {
|
|
|
461
490
|
default: throw new Error(`未知命令: ${cmd}\n\n${usage}`);
|
|
462
491
|
}
|
|
463
492
|
} catch (e) {
|
|
464
|
-
|
|
493
|
+
// 带码的错(AbsError):首行印 [CODE],fallback 另起一行。
|
|
494
|
+
// 为何:hook/脚本需要机器可读的分支依据(借 Anneal 的 templateRefusal 惯例)。
|
|
495
|
+
// 无码的错照旧只印 message —— 不能把内部堆栈当错误码泄给使用人。
|
|
496
|
+
if (e && e.code) {
|
|
497
|
+
console.error(`[${e.code}] ${e.message}`);
|
|
498
|
+
if (e.fallback) console.error(` → ${e.fallback}`);
|
|
499
|
+
} else {
|
|
500
|
+
console.error(String(e && e.message ? e.message : e));
|
|
501
|
+
}
|
|
465
502
|
process.exit(1);
|
|
466
503
|
}
|
|
467
504
|
}
|
package/hooks/abs.opencode.ts
CHANGED
|
@@ -54,12 +54,9 @@ const server = async ({ client, directory }) => {
|
|
|
54
54
|
|
|
55
55
|
|
|
56
56
|
const TEARDOWN_MSG =
|
|
57
|
-
"[abs
|
|
58
|
-
"
|
|
59
|
-
"
|
|
60
|
-
"3) 值得留的经验 abs note \"...\"(宁少勿滥,能从代码 grep 到的不记);\n" +
|
|
61
|
-
"4) abs log \"完成 X:...\" 记一行工作成果,新页同步进 index。\n" +
|
|
62
|
-
"简洁执行,不要复述本条提醒。若本次确实没有可沉淀产出,直接回一句\"无可沉淀\"即可。"
|
|
57
|
+
"[abs] 本会话改过文件,.brain/ 今日无记录。\n" +
|
|
58
|
+
"这条是信息不是命令:该沉淀就沉淀,没有可沉淀的直接回一句「无可沉淀」,不用凑。\n" +
|
|
59
|
+
"需要时:abs todo / abs todo done <id> / abs note \"...\" / abs log \"...\""
|
|
63
60
|
|
|
64
61
|
// bash 里只跑查询类命令不算改文件 (与 pi 侧 READONLY_CMD 同义, 但生成代码里要写进模板串)
|
|
65
62
|
const READONLY_CMD = /^\s*(ls|cat|grep|rg|find|head|tail|wc|git\s+(status|log|diff|show|branch)|pwd|which|echo|node\s+-v|npm\s+(ls|view)|curl)\b/
|
package/hooks/abs.pi.ts
CHANGED
|
@@ -176,13 +176,10 @@ export default function absPiHook(pi: ExtensionAPI): void {
|
|
|
176
176
|
const notes = sessionNotes.splice(0, sessionNotes.length)
|
|
177
177
|
try {
|
|
178
178
|
pi.sendUserMessage(
|
|
179
|
-
"[abs
|
|
180
|
-
"
|
|
181
|
-
"
|
|
182
|
-
|
|
183
|
-
"4) abs log \"完成 X:...\" 记一行工作成果,新页同步进 index。\n" +
|
|
184
|
-
notesBlock(notes) +
|
|
185
|
-
"简洁执行,不要复述本条提醒。若本次确实没有可沉淀产出,直接回一句\"无可沉淀\"即可。",
|
|
179
|
+
"[abs] 本会话改过文件,.brain/ 今日无记录。\n" +
|
|
180
|
+
"这条是信息不是命令:该沉淀就沉淀,没有可沉淀的直接回一句「无可沉淀」,不用凑。\n" +
|
|
181
|
+
"需要时:abs todo / abs todo done <id> / abs note \"...\" / abs log \"...\"" +
|
|
182
|
+
notesBlock(notes),
|
|
186
183
|
{ deliverAs: "followUp" },
|
|
187
184
|
)
|
|
188
185
|
} catch {}
|
package/hooks/event.sh
CHANGED
|
@@ -38,6 +38,14 @@ fi
|
|
|
38
38
|
MARK="$MARK_DIR/abs-hook-${FINGER}-${STAMP}.mark"
|
|
39
39
|
[ -e "$MARK" ] && exit 0
|
|
40
40
|
: > "$MARK" 2>/dev/null
|
|
41
|
+
# mark 只增不减: STAMP 是分钟级 → 每分钟一批, 永不回收。实测堆了 361 个。
|
|
42
|
+
# 幂等窗口只 60s, 非本分钟的 mark 不可能再命中 → 清掉。
|
|
43
|
+
# 注意: 同一分钟内不同 payload 有不同 FINGER, 它们各自合法 —— 只按 STAMP 清, 不按 FINGER。
|
|
44
|
+
for old in "$MARK_DIR"/abs-hook-*.mark; do
|
|
45
|
+
[ -e "$old" ] || continue
|
|
46
|
+
case "$old" in *-"$STAMP".mark) continue ;; esac
|
|
47
|
+
rm -f "$old" 2>/dev/null
|
|
48
|
+
done
|
|
41
49
|
|
|
42
50
|
# 定位项目由 CLI 完成 (向上找 .brain/, 代码写死); 找不到图谱则静默放弃。
|
|
43
51
|
# 纪律: hook 事件只进技术日志 (~/.abs/log/), 不进图谱 log.md (那是活动流水, 不收琐碎请求)。
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fanchao8609/agent_brain_sync",
|
|
3
|
-
"version": "1.8.
|
|
3
|
+
"version": "1.8.9",
|
|
4
4
|
"description": "agent-brain-sync: 跨会话 AI 编码记忆 — hook 纯触发 + CLI/MCP 读写 .brain markdown 图谱, 防并发写保护。",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -34,7 +34,7 @@
|
|
|
34
34
|
"scripts": {
|
|
35
35
|
"abs": "node bin/abs.js",
|
|
36
36
|
"mcp": "node bin/mcp.js",
|
|
37
|
-
"test": "node --test 'test/*.test.js'",
|
|
37
|
+
"test": "node --test 'test/*.test.js' 'skill/**/*.test.js'",
|
|
38
38
|
"prepack": "node -e \"require('fs').chmodSync('bin/abs.js',0o755);require('fs').chmodSync('bin/mcp.js',0o755)\"",
|
|
39
39
|
"pack:check": "npm pack --dry-run"
|
|
40
40
|
}
|
|
@@ -1,10 +1,32 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: abs-agent-brain-sync
|
|
3
|
-
description: abs (agent-brain-sync) 跨会话 AI 编码记忆与任务续接。开场续接状态(abs load/MCP abs_load),干活中任务/经验实时落盘(abs_task/abs_note)
|
|
3
|
+
description: abs (agent-brain-sync) 跨会话 AI 编码记忆与任务续接。开场续接状态(abs load/MCP abs_load),干活中任务/经验实时落盘(abs_task/abs_note),需要收尾时才走收尾循环。解决会话无状态:经验/进度/坑碎片化、重开失忆。遇 bug 排查时配合挂载 bug-hunter skill。
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# abs — 跨会话记忆 (agent-brain-sync)
|
|
7
7
|
|
|
8
|
+
## 最高优先:触发总则(凌驾本文所有流程)
|
|
9
|
+
|
|
10
|
+
**关键词不是触发器,意图才是。**
|
|
11
|
+
|
|
12
|
+
句子里出现 abs 词(收尾/todo/沉淀/提示/load/log/note)**不等于**要执行 abs 动作。
|
|
13
|
+
|
|
14
|
+
| 用户的意思 | 做什么 |
|
|
15
|
+
|---|---|
|
|
16
|
+
| 谈论、提问、吐槽、定规则("太啰嗦""要简短""这个设计怎样") | **只回应,不执行动作** |
|
|
17
|
+
| 明确让我做事("收尾""记一下""去做") | 执行 |
|
|
18
|
+
|
|
19
|
+
**判断依据是整段话的意图,不是里面出现过哪个词。**
|
|
20
|
+
|
|
21
|
+
反例(真实发生):用户说"尽量简短的汇报" —— 那是在**定规则**,不是在**下命令**;
|
|
22
|
+
被误当成指令跑了一次收尾。
|
|
23
|
+
|
|
24
|
+
推论:
|
|
25
|
+
- 拿不准时**先问**,不要靠关键词直接动手。
|
|
26
|
+
- 规则变更("以后简短点")写入本文,**不执行动作**。
|
|
27
|
+
- 输出简短:收尾/提示/todo 汇报尽量一两句,不写小作文。
|
|
28
|
+
- 本总则适用于所有工具,不限 abs。
|
|
29
|
+
|
|
8
30
|
把 AI 编码经验从会话沙盒里救出来。每个会话都是无状态的——Claude、OpenCode、Cursor
|
|
9
31
|
各开一堆会话,经验/进度/踩坑全碎片化,重开像失忆。本技能用一个放**项目根目录**、
|
|
10
32
|
Obsidian 可直接打开的 Markdown 图谱(`.brain/`)做统一落点。
|
|
@@ -256,7 +278,7 @@ abs todo archive # 一条命令搞定,无需手工搬
|
|
|
256
278
|
> OpenCode todowrite / pi `/list`)—— 那些多是会话内临时,不写 `.brain/todo.md`,
|
|
257
279
|
> 下会话接不上、收尾没影。原生 todo 顶多记“本会话不跨断点的临时拆解”。
|
|
258
280
|
|
|
259
|
-
##
|
|
281
|
+
## 收尾循环(用户明确要求收尾时才走)
|
|
260
282
|
|
|
261
283
|
**每个任务边界、被 Stop/打断、告一段落时,别停半空。** 这是“开场接上状态、结束落回状态”的闭环。
|
|
262
284
|
|
|
@@ -264,10 +286,11 @@ abs todo archive # 一条命令搞定,无需手工搬
|
|
|
264
286
|
> 下会话 `abs load` 会自动把滞留顶到顶部(`⏳ 上会话滞留`)—— 所以收尾不靠自觉,是开场被强制接上。
|
|
265
287
|
>
|
|
266
288
|
> **主动注入**:pi 扩展在 `agent_end` 检测「本会话真改过文件」且「log.md 今日无记录」时注入
|
|
267
|
-
> `[abs 收尾提醒]
|
|
289
|
+
> `[abs 收尾提醒]`(每会话最多一次)。**它是一条信息,不是命令** ——
|
|
290
|
+
> 自己判断该不该沉淀;没有可沉淀的就回一句「无可沉淀」,不用强行凑。
|
|
268
291
|
> Claude/Codex 靠 `Stop` 事件达成同样效果。
|
|
269
292
|
|
|
270
|
-
|
|
293
|
+
当用户**明确说要收尾/结束/切别的事**时,按下面走(不是每个词都触发,见开头总则):
|
|
271
294
|
|
|
272
295
|
1. **读 todo** → `abs load`,看 Todo 还有哪些没完成。
|
|
273
296
|
2. **判有没有做完没登记** → 实际完成了漏登记的 `abs todo done <id>`;做到一半补
|
|
@@ -1,228 +1,179 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: abs-bug-hunter
|
|
3
|
-
description:
|
|
3
|
+
description: 排查 bug 的流程纪律 —— 先列假设再动手,用数据定位,修根因。排查完把踩坑写进 .brain/。
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Bug 排查方法论
|
|
7
7
|
|
|
8
8
|
## 核心原则
|
|
9
9
|
|
|
10
|
-
|
|
10
|
+
**不凭空猜测,用数据说话。但数据不是终点 —— 先复现,再定位,再理解根因。**
|
|
11
11
|
|
|
12
|
-
|
|
12
|
+
最常见的失败:把"能复现"当成"已定位"。定位到出错的**位置** ≠ 找到**根因**。
|
|
13
|
+
改症状不改根因,换一批数据就复发。
|
|
13
14
|
|
|
14
|
-
##
|
|
15
|
+
## 第 -1 步:列假设(不能跳过)
|
|
15
16
|
|
|
16
|
-
|
|
17
|
+
**触发**:用户报了症状("打不开""报错了""不对"),但没给一手错误信息。
|
|
17
18
|
|
|
18
|
-
|
|
19
|
+
**这一步在"复现"之前**,因为拿不到报错时,"先复现"会逼你造假。
|
|
19
20
|
|
|
20
|
-
|
|
21
|
-
编了个接口名 `app.init` → curl 测出 404 → “我测出来的,是事实” → 推出“nginx 没配 PATH_INFO”
|
|
22
|
-
→ 用户纠正后**重跑同一实验、得到同一 404** → 坚信自己没错。
|
|
21
|
+
### 自造证据 —— 最毒的一类错误
|
|
23
22
|
|
|
24
|
-
|
|
25
|
-
> (输入名)在最上游,且已消失在过程里 → 所以“再验证一次”救不了,
|
|
26
|
-
> 他会重跑同假输入得同真输出。
|
|
27
|
-
> 下面第 0 步“先复现”在拿不到报错时反而会逼出造假 —— 所以本步在它之前。
|
|
23
|
+
**真实案例**:只有一个假设时,它要么被证实要么思路断掉 —— 于是自己造一个输入去测:
|
|
28
24
|
|
|
29
|
-
|
|
25
|
+
```
|
|
26
|
+
编了个接口名 app.init → curl 测出 404 → "我测出来的,是事实"
|
|
27
|
+
→ 推出"nginx 没配 PATH_INFO"
|
|
28
|
+
→ 用户纠正后,重跑同一实验、得到同一 404 → 坚信自己没错
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
**为什么最毒**:下游全程都是真的(404 真、命令真跑过)。唯一假的那环在**最上游**,
|
|
32
|
+
且已消失在过程里 → 所以"再验证一次"救不了,重跑同假输入得同真输出。
|
|
33
|
+
|
|
34
|
+
### 所以
|
|
35
|
+
|
|
36
|
+
1. **开局列 ≥2 个假设**(代码 / 配置 / 构建产物 / 缓存 / 第三方)。
|
|
30
37
|
只列一个 = 逼自己去证实它 = 造证据的动力。
|
|
31
38
|
2. **每个假设配反证条件**:什么现象出现就认它错。
|
|
32
|
-
|
|
33
|
-
> 测出也是 404 → 接口本身不存在,与 nginx 无关 → 弃此假设。”
|
|
34
|
-
> 反证条件里写明“真实接口名”,`app.init` 这种编造的就自动不能用了。
|
|
39
|
+
反证条件里要写明**能从代码里读到的真实名字**,编造的自动不能用。
|
|
35
40
|
3. **卡住两轮就换假设**,不是在同一假设上加细节。
|
|
36
|
-
4. **领域外的先声明无取证能力**(构建产物在不在 / 缓存 /
|
|
41
|
+
4. **领域外的先声明无取证能力**(构建产物在不在 / 缓存 / 第三方后台行为),
|
|
37
42
|
要用户提供事实,**不要脑补**。
|
|
38
43
|
|
|
39
44
|
**结论所依据的每个具体名字(接口/字段/报错文本/路径)必须指得出处(文件:行)。**
|
|
40
|
-
|
|
45
|
+
指不出的只能标"推测",**不得当证据用**。
|
|
41
46
|
|
|
42
47
|
> **为什么多假设能防造假**:单假设必须被证实,否则思路断 → 有动机造。
|
|
43
|
-
>
|
|
48
|
+
> 多假设下,"支持了 A" ≠ "就是 A"(B/C/D 还没排除)→ 假证据推不出结论,造它就没意义。
|
|
49
|
+
|
|
50
|
+
## 第 0 步:复现
|
|
44
51
|
|
|
45
|
-
|
|
52
|
+
没有稳定复现,后面全是猜。
|
|
46
53
|
|
|
47
|
-
没有稳定复现,后面全是猜。先确定:
|
|
48
54
|
- 复现的必要条件是什么?哪个输入/操作/环境触发?
|
|
49
|
-
-
|
|
50
|
-
-
|
|
55
|
+
- 100% 复现还是偶发?偶发先找触发模式(特定数据/时机/顺序)。
|
|
56
|
+
- 有现成错误栈吗?**错误信息 + 行号是最便宜的定位入口。**
|
|
51
57
|
|
|
52
|
-
**给 Bug 起个名字**(一句话描述 +
|
|
58
|
+
**给 Bug 起个名字**(一句话描述 + 触发条件)。卡住时回来核对是否还在同一个 Bug 上。
|
|
53
59
|
|
|
54
|
-
|
|
60
|
+
## 第一步:定位范围 —— 改动了什么
|
|
55
61
|
|
|
56
|
-
Bug
|
|
62
|
+
Bug 不会凭空出现。先查最近的改动:
|
|
57
63
|
|
|
58
64
|
```bash
|
|
59
|
-
git diff HEAD~3 --stat
|
|
60
|
-
git log
|
|
61
|
-
git
|
|
62
|
-
git log -p -S '可疑字段/函数名' # 谁改过这段代码
|
|
63
|
-
git blame <file> -L <行号,行号> # 定位到具体某行是谁写的
|
|
65
|
+
git diff HEAD~3 --stat # 改了哪些文件
|
|
66
|
+
git log -p -S '可疑字段/函数名' # 谁改过这段
|
|
67
|
+
git blame <file> -L <行号,行号> # 定位到具体某行
|
|
64
68
|
```
|
|
65
69
|
|
|
66
|
-
|
|
67
|
-
- 这次新增/修改了什么功能?
|
|
68
|
-
- 改动涉及哪些文件?
|
|
69
|
-
- 哪个改动最可能影响到出问题的地方?
|
|
70
|
-
|
|
71
|
-
**不要排查无关代码,把精力集中在改动范围。**
|
|
70
|
+
问自己:这次新增/修改了什么?涉及哪些文件?哪个最可能影响出问题的地方?
|
|
72
71
|
|
|
73
|
-
|
|
72
|
+
**别排查无关代码。**
|
|
74
73
|
|
|
75
|
-
|
|
74
|
+
## 第二步:让数据说话
|
|
76
75
|
|
|
77
|
-
|
|
78
|
-
|--------|----------|----------|
|
|
79
|
-
| PHP (ThinkPHP) | `\think\Log::error($data)` 或 `trace($data)` | `runtime/log/` |
|
|
80
|
-
| PHP (Laravel) | `Log::info($data)` 或 `logger($data)` | `storage/logs/` |
|
|
81
|
-
| Vue / JS | `console.log(data)` | 浏览器控制台 |
|
|
82
|
-
| Node.js | `console.log(data)` 或 `logger.debug(data)` | 终端 / 日志文件 |
|
|
83
|
-
| SQL | `\think\Db::getLastSql()` 或开启 SQL 日志 | runtime/log 或控制台 |
|
|
76
|
+
不猜,打印出来看。
|
|
84
77
|
|
|
85
|
-
|
|
78
|
+
- **打印关键数据,不要只打印"到了这里"** —— 要打印实际的变量值、类型、入参出参。
|
|
79
|
+
- **二分法**:在可疑链路上隔几层插日志,先跑一遍看哪段有数据、哪段没有,
|
|
80
|
+
范围砍半再往里加。比一次打满所有层更快收敛。
|
|
81
|
+
- **带唯一前缀**(如 `[BUG-01]`):日志里 grep 一次看到全链路顺序。
|
|
82
|
+
- 怀疑并发/时序时,打时间戳与调用来源。
|
|
86
83
|
|
|
87
|
-
|
|
88
|
-
// ❌ 没用
|
|
89
|
-
\Log::error('进入方法');
|
|
84
|
+
## 第三步:顺藤摸瓜
|
|
90
85
|
|
|
91
|
-
|
|
92
|
-
\Log::error('goods detail', ['id' => $id, 'goods' => $goods, 'type' => gettype($goods)]);
|
|
93
|
-
```
|
|
94
|
-
|
|
95
|
-
**二分法打印**:在可疑链路上隔几层就插一个日志点,先跑一遍看哪段有数据、哪段没数据,把范围砍半,再往里面加日志。比一次打满所有层更快收敛。
|
|
96
|
-
|
|
97
|
-
**加分技巧**:
|
|
98
|
-
- 给每个日志点带唯一前缀(如 `[BUG-01]`、`[BUG-02]`),日志里 grep 一次就能看到全链路顺序。
|
|
99
|
-
- 打印耗时和调用来源:怀疑并发/时序时,打上时间戳与 `debug_backtrace()` 或请求 ID。
|
|
100
|
-
|
|
101
|
-
### 第三步:顺藤摸瓜 — 按调用链逐层排查
|
|
86
|
+
从用户操作出发,沿调用链一层层往下:入口 → 路由 → 中间件 → 业务层 → 数据层 → 存储。
|
|
102
87
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
```
|
|
106
|
-
用户操作(点击/请求)
|
|
107
|
-
→ 路由(routes / pages.json / url)
|
|
108
|
-
→ 门面/中间件(facade / middleware)
|
|
109
|
-
→ 控制器(controller)
|
|
110
|
-
→ 服务层(service)
|
|
111
|
-
→ 模型(model)
|
|
112
|
-
→ 数据库(SQL)
|
|
113
|
-
```
|
|
88
|
+
**每一层打印关键数据,找到数据从正确变为错误的那一层 —— bug 就在那一层。**
|
|
114
89
|
|
|
115
|
-
|
|
90
|
+
## 第四步:找根因,不只修症状
|
|
116
91
|
|
|
117
|
-
|
|
118
|
-
// 控制器 — 打印接收到的参数
|
|
119
|
-
\Log::error('controller input', ['params' => $params]);
|
|
92
|
+
修复前回答:
|
|
120
93
|
|
|
121
|
-
|
|
122
|
-
|
|
94
|
+
- 为什么数据在这一层变错了?**逻辑错误**(判断写反/取错字段)、
|
|
95
|
+
**类型错误**(null/数组/对象混用)、还是**数据本身脏**(上游写入时就错)?
|
|
96
|
+
- 根因若在上游,这里 patch 只是挡一下 —— 应该去修上游,或在入口统一兜底。
|
|
123
97
|
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
```
|
|
98
|
+
**修一层,不改所有调用点**:如果多个地方调用同一函数,只在出问题的调用点打补丁,
|
|
99
|
+
其他调用点照样坏。**在共享函数里修一次,是所有调用点的最小修复。**
|
|
127
100
|
|
|
128
|
-
|
|
101
|
+
## 第五步:验证修复
|
|
129
102
|
|
|
130
|
-
|
|
103
|
+
不要凭空验证。**先拿第 0 步的复现条件重跑,必须看到它失败** ——
|
|
104
|
+
没失败就说明你修好了但没复现过,或者复现条件记错了。
|
|
105
|
+
不先看到 fail,就无法区分"修好了"和"根本没坏过"。
|
|
106
|
+
改完再看它变 pass,三层都要过:
|
|
131
107
|
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
108
|
+
| 层 | 查什么 |
|
|
109
|
+
|---|---|
|
|
110
|
+
| **修好了** | 复现路径不再报错,输出正确 |
|
|
111
|
+
| **没修坏** | 相关正常路径仍正常(回归) |
|
|
112
|
+
| **边界还在** | 边缘输入(空值/超大值/并发/重复提交)没引入新洞 |
|
|
135
113
|
|
|
136
|
-
|
|
114
|
+
确认无误后再删调试日志。
|
|
137
115
|
|
|
138
|
-
|
|
116
|
+
## 排查完:写进 .brain/
|
|
139
117
|
|
|
140
|
-
|
|
118
|
+
**这一步是 `abs-` 前缀的意义 —— 排查的结论不写下来,下个会话会重踩同一个坑。**
|
|
141
119
|
|
|
142
|
-
```
|
|
143
|
-
|
|
144
|
-
\Log::error('step1: fetch', ['data' => $data]);
|
|
145
|
-
$data = transform($data);
|
|
146
|
-
\Log::error('step2: transform', ['data' => $data]);
|
|
147
|
-
$result = save($data);
|
|
148
|
-
\Log::error('step3: save', ['result' => $result]);
|
|
120
|
+
```bash
|
|
121
|
+
abs note "一句话结论" --when "什么时候该看这条"
|
|
149
122
|
```
|
|
150
123
|
|
|
151
|
-
|
|
152
|
-
- **修好了**:复现路径不再报错,输出正确。
|
|
153
|
-
- **没修坏**:相关正常路径仍正常(回归)。
|
|
154
|
-
- **边界还在**:尝试边缘输入(空值、超大值、并发、重复提交),确认修复没引入新洞。
|
|
155
|
-
|
|
156
|
-
确认无误后,再删除调试日志。
|
|
157
|
-
|
|
158
|
-
## 实战示例
|
|
159
|
-
|
|
160
|
-
**场景:** 商品详情页报 `Call to a member function toArray() on array`
|
|
161
|
-
|
|
162
|
-
**第 0 步:复现**
|
|
163
|
-
- 访问 `/shopro/goods/goods/detail/id/31` 稳定复现
|
|
164
|
-
- 日志:`[error] 致命错误: Call to a member function toArray() on array [/var/www/html/.../GoodsMemberPrice.php:50]`
|
|
165
|
-
|
|
166
|
-
**第一步:定位范围**
|
|
167
|
-
- 最近新增了会员系统,改了 Goods 控制器和新增了 GoodsMemberPrice 模型
|
|
168
|
-
- 怀疑新增代码有问题
|
|
124
|
+
值得写的(**能从代码 grep 到的不写**):
|
|
169
125
|
|
|
170
|
-
|
|
171
|
-
|
|
126
|
+
| 写什么 | 例 |
|
|
127
|
+
|---|---|
|
|
128
|
+
| **假象的根因** | "报错位置是 A,真因在 B 的写入侧" |
|
|
129
|
+
| **判据** | "统计硬切率 >30% 即确诊,别去调 prompt" |
|
|
130
|
+
| **反直觉处** | "慢的不是读写,是 node 启动" |
|
|
131
|
+
| **排查路径** | "先查写入侧,别先怪模型" |
|
|
172
132
|
|
|
173
|
-
|
|
174
|
-
- 路由:`/shopro/goods/goods/detail/id/31`
|
|
175
|
-
- 控制器:`Goods::detail()` → 调用了 `GoodsMemberPriceModel::getByGoods()`
|
|
176
|
-
- 模型:`getByGoods()` 第50行 `select()->toArray()` → `select()` 返回数组,不是 Collection
|
|
133
|
+
不值得写的:报错原文(日志里有)、修好的代码(git 里有)、通用常识。
|
|
177
134
|
|
|
178
|
-
|
|
179
|
-
- 为什么 id=31 出错而别的正常?查数据:31 这件商品有会员价记录 → 触发 `select()` 返回多行场景。可能是 `find()`/`select()` 返回值混用的历史问题。若只是个别记录脏,应清理数据或给方法加统一返回类型。
|
|
135
|
+
**如果是反复踩的坑**,排查结束后提成硬规则:
|
|
180
136
|
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
// 修复前
|
|
184
|
-
return self::where('goods_id', $goodsId)->select()->toArray();
|
|
185
|
-
|
|
186
|
-
// 修复后
|
|
187
|
-
$result = self::where('goods_id', $goodsId)->select();
|
|
188
|
-
return $result instanceof \think\Collection ? $result->toArray() : (array)$result;
|
|
137
|
+
```bash
|
|
138
|
+
abs rule add "靠提醒才能工作的功能,该删不该补"
|
|
189
139
|
```
|
|
190
|
-
- 重跑 id=31(不再报错)+ 跑一个无会员价记录的 id(确认没回归)
|
|
191
140
|
|
|
192
141
|
## 常见陷阱
|
|
193
142
|
|
|
194
143
|
| 陷阱 | 正确做法 |
|
|
195
144
|
|------|----------|
|
|
196
|
-
|
|
|
197
|
-
| 只看代码不运行 |
|
|
198
|
-
|
|
|
199
|
-
| 修症状不修根因 | 追问"为什么这层数据变了"
|
|
200
|
-
|
|
|
201
|
-
|
|
|
202
|
-
| 只在本地验证 |
|
|
203
|
-
|
|
|
204
|
-
|
|
|
205
|
-
|
|
206
|
-
##
|
|
207
|
-
|
|
208
|
-
排查 >20
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
145
|
+
| 凭经验猜位置 | 先复现 + 看日志/打印 |
|
|
146
|
+
| 只看代码不运行 | 打印运行时的实际数据 |
|
|
147
|
+
| 一次改很多地方再测 | 改一处、验证一处 |
|
|
148
|
+
| 修症状不修根因 | 追问"为什么这层数据变了" |
|
|
149
|
+
| 只验证出错路径 | 复现 + 回归 + 边界 |
|
|
150
|
+
| 忘记删调试代码 | 确认后清理所有打印 |
|
|
151
|
+
| 只在本地验证 | 确认部署的代码/数据一致 |
|
|
152
|
+
| 偶发当成必然 | 先找触发模式,别用单样本下结论 |
|
|
153
|
+
| 排查完不落盘 | 提一条 `abs note`,反复踩的提 `abs rule` |
|
|
154
|
+
|
|
155
|
+
## 兜底:卡住时
|
|
156
|
+
|
|
157
|
+
排查 >20 分钟没进展,不硬扛,回流程检查:
|
|
158
|
+
|
|
159
|
+
1. **Bug 描述还准吗?** 新观察是否改变了问题定义。
|
|
160
|
+
2. **复现还稳定吗?** 换个触发样本还出现吗。
|
|
161
|
+
3. **漏看日志了吗?** grep 全量(不只最近的),可能早期就报过错。
|
|
162
|
+
4. **改动范围查全了吗?** 只看 HEAD~3 会漏分支合并、配置、部署差异。
|
|
163
|
+
5. **要不要问人?** 这条功能最近谁改的、意图是什么,可能一句话点醒。
|
|
164
|
+
6. **向上游看一层。** 来源方是否也变了,不只是消费方的问题。
|
|
165
|
+
|
|
166
|
+
## 收尾:结论要明确
|
|
217
167
|
|
|
218
168
|
**不许**:"可能是 X,也可能是 Y,建议排查一下" —— 这是把判断推回给用户。
|
|
219
169
|
|
|
220
|
-
|
|
221
|
-
|
|
170
|
+
**必须**给出:
|
|
171
|
+
|
|
172
|
+
1. **当前结论**(是什么 / 或"未定位")
|
|
222
173
|
2. **置信度与依据**(哪来的证据 / 还是只是推测)
|
|
223
|
-
3.
|
|
174
|
+
3. **下一步具体动作**(跑什么、看什么、要用户提供什么)
|
|
224
175
|
|
|
225
|
-
"未定位"是合法输出,但必须配一句"**需要你提供 X**"
|
|
176
|
+
"未定位"是合法输出,但必须配一句"**需要你提供 X**"(具体到要什么),
|
|
226
177
|
不能只说"无法确定"就结束。
|
|
227
178
|
|
|
228
|
-
> 与第 -1
|
|
179
|
+
> 与第 -1 步同一根因:**模糊化是逃避判断。** 要么给结论 + 依据,要么明确索取证据。
|
package/src/index.js
CHANGED
|
@@ -26,13 +26,32 @@ export function brainPath(brainRoot, ...rel) {
|
|
|
26
26
|
return join(brainRoot, BRAIN_DIR, ...rel);
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
+
/**
|
|
30
|
+
* 带错误码的错(CLI/MCP 同源)。
|
|
31
|
+
*
|
|
32
|
+
* 为何要码(2026-09-16 借 Anneal 的 templateRefusal):
|
|
33
|
+
* MCP 侧早就有 code(err(msg,{code,fallback}),5 种),CLI 侧全是自然语言句子。
|
|
34
|
+
* 于是 hook/脚本无法区分「该静默」与「该报警」——只能靠抓字符串,改文案就碎。
|
|
35
|
+
* 约定:所有可预期的失败都带 code;调用方按码分支,不看文案。
|
|
36
|
+
* 命名:大写下划线(与 MCP 侧一致)。
|
|
37
|
+
*/
|
|
38
|
+
export class AbsError extends Error {
|
|
39
|
+
constructor(code, message, fallback) {
|
|
40
|
+
super(message);
|
|
41
|
+
this.name = 'AbsError';
|
|
42
|
+
this.code = code;
|
|
43
|
+
this.fallback = fallback || null;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
29
47
|
/** 断言 .brain/ 存在,否则抛错(宁可失败不落错项目)。 */
|
|
30
48
|
export async function requireBrain(startDir) {
|
|
31
49
|
const root = await findBrainRoot(startDir);
|
|
32
50
|
if (!root) {
|
|
33
|
-
throw new
|
|
34
|
-
|
|
35
|
-
`
|
|
51
|
+
throw new AbsError(
|
|
52
|
+
'NO_BRAIN',
|
|
53
|
+
`abs: 目录 ${resolve(startDir)} 下没有 .brain/ 图谱(不向上搜索)。`,
|
|
54
|
+
'请在该目录运行: abs init'
|
|
36
55
|
);
|
|
37
56
|
}
|
|
38
57
|
return root;
|