@fanchao8609/agent_brain_sync 1.8.0 → 1.8.2
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/hooks/abs.opencode.ts +3 -43
- package/hooks/abs.pi.ts +7 -70
- package/package.json +1 -1
- package/skill/SKILL.md +4 -25
- package/skill/bug-hunter/SKILL.md +228 -0
- package/src/install.js +28 -13
package/hooks/abs.opencode.ts
CHANGED
|
@@ -15,17 +15,13 @@
|
|
|
15
15
|
import { appendFile, mkdir, readFile, stat } from "node:fs/promises"
|
|
16
16
|
import { homedir } from "node:os"
|
|
17
17
|
import { join, dirname } from "node:path"
|
|
18
|
-
|
|
19
18
|
const server = async ({ client, directory }) => {
|
|
20
19
|
// 本会话是否真改过文件 (由 tool.execute.after 观测)
|
|
21
20
|
let wroteFiles = false
|
|
22
21
|
let nudged = false
|
|
23
22
|
let idleSeen = false
|
|
24
|
-
//
|
|
25
|
-
let registerNudged = 0
|
|
26
|
-
// 当前 session id(由事件里拿),tool.execute.after 没带 sessionID 时靠它补。
|
|
23
|
+
// 当前 session id(由事件里拿)—— 收尾注入需要它找到会话。
|
|
27
24
|
let currentSessionID = ""
|
|
28
|
-
|
|
29
25
|
async function logHook(evt) {
|
|
30
26
|
try {
|
|
31
27
|
const dir = process.env.ABS_LOG_DIR || join(homedir(), ".abs", "log")
|
|
@@ -56,21 +52,6 @@ const server = async ({ client, directory }) => {
|
|
|
56
52
|
} catch { return false }
|
|
57
53
|
}
|
|
58
54
|
|
|
59
|
-
// 登记提醒:注入失败不阻塞宿主。每次写文件都提醒(用户明确要「频繁一点」)。
|
|
60
|
-
async function registerNudge(files) {
|
|
61
|
-
registerNudged++
|
|
62
|
-
await logHook(`tool.execute.after:register-nudge #${registerNudged} files=${files.length}`)
|
|
63
|
-
if (!currentSessionID) return // 拿不到 session 就无法注入,静默跳过(不抛)
|
|
64
|
-
const cwd = directory || process.cwd()
|
|
65
|
-
if (!(await findBrain(cwd))) return // 无图谱 = 不在这项目沉淀,不打扰
|
|
66
|
-
try {
|
|
67
|
-
await client.session.promptAsync({
|
|
68
|
-
path: { id: currentSessionID },
|
|
69
|
-
query: { directory: cwd },
|
|
70
|
-
body: { parts: [{ type: "text", text: REGISTER_MSG(files) }] },
|
|
71
|
-
})
|
|
72
|
-
} catch {}
|
|
73
|
-
}
|
|
74
55
|
|
|
75
56
|
const TEARDOWN_MSG =
|
|
76
57
|
"[abs 收尾提醒] 本会话改过文件但 .brain/ 今天还没有记录。请立即走收尾循环:\n" +
|
|
@@ -83,23 +64,8 @@ const server = async ({ client, directory }) => {
|
|
|
83
64
|
// bash 里只跑查询类命令不算改文件 (与 pi 侧 READONLY_CMD 同义, 但生成代码里要写进模板串)
|
|
84
65
|
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/
|
|
85
66
|
|
|
86
|
-
// 写文件 = 任务开始的机械信号。排除 .brain/ 自身(`abs note`/`abs log` 也写文件,
|
|
87
|
-
// 但那是记录行为不是任务 —— 不排除则每落一条经验都弹登记提醒,纯噪音)。
|
|
88
|
-
const BRAIN_PATH = /\.brain[\\/]/
|
|
89
|
-
function isProjectWrite(p, cmd) {
|
|
90
|
-
if (cmd !== undefined) return !!cmd && !READONLY_CMD.test(cmd)
|
|
91
|
-
if (!p) return false
|
|
92
|
-
return !BRAIN_PATH.test(String(p))
|
|
93
|
-
}
|
|
94
67
|
|
|
95
|
-
|
|
96
|
-
"[abs 登记提醒] 本轮改了项目文件,说明有任务在进行。\n" +
|
|
97
|
-
"改了:" + files.slice(0, 6).join(", ") + (files.length > 6 ? ` 等 ${files.length} 个` : "") + "\n" +
|
|
98
|
-
"若这属于某个任务,立刻登记(别等到会话结束才回忆——那时必漏):\n" +
|
|
99
|
-
" · 新任务: abs todo add <id> --note \"做什么\"\n" +
|
|
100
|
-
" · 已有任务: abs todo note <id> --note \"改到哪/下一步\"\n" +
|
|
101
|
-
" · 纯讨论/调研/改图谱自身 → 无需登记,回「跳过」即可。\n" +
|
|
102
|
-
"简洁执行,不要复述本条提醒。"
|
|
68
|
+
|
|
103
69
|
|
|
104
70
|
return {
|
|
105
71
|
// 观测真实写操作: write/edit/patch 类工具成功即标记。
|
|
@@ -108,20 +74,14 @@ const server = async ({ client, directory }) => {
|
|
|
108
74
|
"tool.execute.after": async ({ tool, args }) => {
|
|
109
75
|
const t = String(tool || "").toLowerCase()
|
|
110
76
|
const a = args || {}
|
|
111
|
-
const fp = a.file_path || a.filePath || a.path || a.filename
|
|
112
77
|
if (["write", "edit", "patch", "multiedit", "apply_patch"].includes(t)) {
|
|
113
78
|
wroteFiles = true
|
|
114
|
-
// 登记提醒:项目文件被写 = 任务已开始,立刻推(不等 idle)
|
|
115
|
-
if (isProjectWrite(fp)) await registerNudge([String(fp)])
|
|
116
79
|
return
|
|
117
80
|
}
|
|
118
81
|
// bash 里只有非只读命令算改文件(ls/cat/git status 之类不算)
|
|
119
82
|
if (t === "bash") {
|
|
120
83
|
const cmd = String(a.command || a.cmd || "")
|
|
121
|
-
if (
|
|
122
|
-
wroteFiles = true
|
|
123
|
-
await registerNudge(["bash: " + cmd.slice(0, 60)])
|
|
124
|
-
}
|
|
84
|
+
if (cmd && !READONLY_CMD.test(cmd)) wroteFiles = true
|
|
125
85
|
}
|
|
126
86
|
},
|
|
127
87
|
|
package/hooks/abs.pi.ts
CHANGED
|
@@ -13,13 +13,10 @@
|
|
|
13
13
|
* - turn_end (每轮结束) → 记改过哪些文件
|
|
14
14
|
* 累积在**内存**(下方 sessionNotes), 收尾时拼进注入提示 —— **不落盘**。
|
|
15
15
|
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
* · 频繁没关系 —— 每轮改文件都提醒(用户明确要求), 用 nudgeCount 防同一轮重复
|
|
21
|
-
* · 判据 = 整个项目, 但**排除 .brain/ 自身**(写图谱不是任务, 是记录行为)
|
|
22
|
-
* · 文案明说「纯讨论可跳过」—— 不逼 AI 造任务(否则会长出假条目污染看板)
|
|
16
|
+
* 已删除(2026-09-15):曾加过一个「写文件即任务开始 → 每轮注入登记提醒」的功能。
|
|
17
|
+
* 已发布到 1.8.0 后用户实报「pi 里一直报 Follow-up」,判 C 整个删掉。
|
|
18
|
+
* 教训:每次改文件就往对话里插一句话,本身就是设计错误,不是频率问题 ——
|
|
19
|
+
* 改守卫(从每轮一次改成每会话一次)治不了它。别再加回来。
|
|
23
20
|
*/
|
|
24
21
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent"
|
|
25
22
|
import { appendFile, mkdir, readFile, stat } from "node:fs/promises"
|
|
@@ -117,37 +114,6 @@ async function loggedToday(brain: string): Promise<boolean> {
|
|
|
117
114
|
} catch { return false }
|
|
118
115
|
}
|
|
119
116
|
|
|
120
|
-
// 写文件 = 任务开始的机械信号。
|
|
121
|
-
// 注: `abs note` / `abs log` 也会写文件 —— 但那是**记录行为**不是任务,
|
|
122
|
-
// 所以要排除 .brain/ 自身(否则每落一条经验都弹一次登记提醒,纯噪音)。
|
|
123
|
-
// ponytail: 用路径前缀判,不解析项目根(子目录/软链场景可能漏判;
|
|
124
|
-
// 漏判的代价只是少提醒一次,不丢数据)。
|
|
125
|
-
const BRAIN_PATH = /\.brain[\/]/;
|
|
126
|
-
|
|
127
|
-
function isProjectWrite(p: string | undefined, cmd?: string): boolean {
|
|
128
|
-
if (cmd !== undefined) return !READONLY_CMD.test(cmd); // bash: 非只读就算
|
|
129
|
-
if (!p) return false;
|
|
130
|
-
return !BRAIN_PATH.test(String(p));
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
/** 从一轮的 toolResults 里抽出「改过的项目文件」。排除 .brain/ 自身。 */
|
|
134
|
-
function projectWrites(toolResults: any[]): string[] {
|
|
135
|
-
const out = new Set<string>();
|
|
136
|
-
for (const r of toolResults || []) {
|
|
137
|
-
const name = String(r?.toolName || "");
|
|
138
|
-
if (!WRITE_TOOLS.has(name)) continue;
|
|
139
|
-
if (r?.isError) continue;
|
|
140
|
-
if (name === "bash") {
|
|
141
|
-
const cmd = String(r?.input?.command ?? r?.args?.command ?? "");
|
|
142
|
-
if (!isProjectWrite(undefined, cmd)) continue;
|
|
143
|
-
out.add("bash: " + cmd.slice(0, 60));
|
|
144
|
-
continue;
|
|
145
|
-
}
|
|
146
|
-
const f = r?.input?.file_path ?? r?.args?.file_path ?? r?.input?.path ?? r?.args?.path;
|
|
147
|
-
if (isProjectWrite(f ? String(f) : undefined)) out.add(String(f));
|
|
148
|
-
}
|
|
149
|
-
return [...out];
|
|
150
|
-
}
|
|
151
117
|
|
|
152
118
|
export default function absPiHook(pi: ExtensionAPI): void {
|
|
153
119
|
// 收尾注入的节流状态。声明在**外层** + 在 session_start 里重置:
|
|
@@ -155,17 +121,10 @@ export default function absPiHook(pi: ExtensionAPI): void {
|
|
|
155
121
|
// (2026-09-13 实测:同一天多个会话时后半场全部静默)。
|
|
156
122
|
let teardownNudged = false
|
|
157
123
|
let agentEndSeen = false
|
|
158
|
-
// 登记提醒节流:只需防**同一轮内**重复注入(一个 turn_end 只提醒一次)。
|
|
159
|
-
// 跨轮不去重 —— 用户明确要「频繁一点」:同一文件改第二回仍是新任务进展,
|
|
160
|
-
// 该提醒。曾经用「文件列表」当去重键 → 同一文件再改就永不提醒(实测 step②失败)。
|
|
161
|
-
let lastNudgedTurn = -1
|
|
162
|
-
let turnSeq = 0
|
|
163
124
|
|
|
164
|
-
pi.on("session_start", () => {
|
|
125
|
+
pi.on("session_start", (event: any, _ctx: any) => {
|
|
165
126
|
teardownNudged = false
|
|
166
127
|
agentEndSeen = false
|
|
167
|
-
lastNudgedTurn = -1
|
|
168
|
-
turnSeq = 0
|
|
169
128
|
return logHook("session_start").catch(() => {})
|
|
170
129
|
})
|
|
171
130
|
|
|
@@ -176,8 +135,7 @@ export default function absPiHook(pi: ExtensionAPI): void {
|
|
|
176
135
|
})
|
|
177
136
|
|
|
178
137
|
// 每轮结束 = 这轮干了什么(改了哪些文件)。机械事实,供收尾时回忆。
|
|
179
|
-
|
|
180
|
-
pi.on("turn_end", async (event: any, ctx: any) => {
|
|
138
|
+
pi.on("turn_end", async (event: any, _ctx: any) => {
|
|
181
139
|
const files = new Set<string>()
|
|
182
140
|
for (const r of event?.toolResults || []) {
|
|
183
141
|
const name = String(r?.toolName || "")
|
|
@@ -191,29 +149,8 @@ export default function absPiHook(pi: ExtensionAPI): void {
|
|
|
191
149
|
}
|
|
192
150
|
}
|
|
193
151
|
if (files.size) addNote("tool", [...files].join(", "))
|
|
194
|
-
|
|
195
|
-
// ---- 登记提醒:项目文件被写 = 任务已经开始 ----
|
|
196
|
-
const touched = projectWrites(event?.toolResults || [])
|
|
197
|
-
if (!touched.length) return
|
|
198
|
-
const me = ++turnSeq
|
|
199
|
-
if (me === lastNudgedTurn) return // 同一轮不重复(turn_end 理论上只来一次,防御)
|
|
200
|
-
lastNudgedTurn = me
|
|
201
|
-
const cwd = (ctx && ctx.cwd) || process.cwd()
|
|
202
|
-
if (!(await findBrain(cwd))) return // 无图谱 = 不在这项目沉淀,不打扰
|
|
203
|
-
await logHook(`turn_end:register-nudge files=${touched.length}`).catch(() => {})
|
|
204
|
-
try {
|
|
205
|
-
pi.sendUserMessage(
|
|
206
|
-
"[abs 登记提醒] 本轮改了项目文件,说明有任务在进行。\n" +
|
|
207
|
-
"改了:" + touched.slice(0, 6).join(", ") + (touched.length > 6 ? ` 等 ${touched.length} 个` : "") + "\n" +
|
|
208
|
-
"若这属于某个任务,立刻登记(别等到会话结束才回忆——那时必漏):\n" +
|
|
209
|
-
" · 新任务: abs todo add <id> --note \"做什么\"\n" +
|
|
210
|
-
" · 已有任务: abs todo note <id> --note \"改到哪/下一步\"\n" +
|
|
211
|
-
" · 纯讨论/调研/改图谱自身 → 无需登记,回「跳过」即可。\n" +
|
|
212
|
-
"简洁执行,不要复述本条提醒。",
|
|
213
|
-
{ deliverAs: "followUp" },
|
|
214
|
-
)
|
|
215
|
-
} catch {}
|
|
216
152
|
})
|
|
153
|
+
|
|
217
154
|
// 收尾注入: 每个会话最多一次, 避免反复打扰。
|
|
218
155
|
pi.on("agent_end", async (event: any, ctx: any) => {
|
|
219
156
|
if (teardownNudged) return
|
package/package.json
CHANGED
package/skill/SKILL.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
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),每轮结束走收尾循环(读todo→判未登记→沉淀→更新index/log)
|
|
3
|
+
description: abs (agent-brain-sync) 跨会话 AI 编码记忆与任务续接。开场续接状态(abs load/MCP abs_load),干活中任务/经验实时落盘(abs_task/abs_note),每轮结束走收尾循环(读todo→判未登记→沉淀→更新index/log)。解决会话无状态:经验/进度/坑碎片化、重开失忆。遇 bug 排查时配合挂载 bug-hunter skill。
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# abs — 跨会话记忆 (agent-brain-sync)
|
|
@@ -214,30 +214,6 @@ Done 区由 `abs wrapup` 在会话结束时自动把「超 3 天且整天都已
|
|
|
214
214
|
> OpenCode todowrite / pi `/list`)—— 那些多是会话内临时,不写 `.brain/todo.md`,
|
|
215
215
|
> 下会话接不上、收尾没影。原生 todo 顶多记“本会话不跨断点的临时拆解”。
|
|
216
216
|
|
|
217
|
-
## 写文件 = 任务开始(收到登记提醒就立即做)
|
|
218
|
-
|
|
219
|
-
**改了项目文件,说明有任务在进行。** hook 观测到写入会就地注入一条
|
|
220
|
-
`[abs 登记提醒]`(pi 在 `turn_end`、OpenCode 在 `tool.execute.after`),**不等会话结束**。
|
|
221
|
-
|
|
222
|
-
**为什么不等收尾**(实测教训):旧设计只在会话末尾提醒 —— 而那时用户已想结束、
|
|
223
|
-
AI 只想收尾不想登记。且「任务何时开始」只有写文件那一刻清楚:等收尾回忆必漏。
|
|
224
|
-
|
|
225
|
-
收到登记提醒,当场判断(**三选一,不猜**):
|
|
226
|
-
|
|
227
|
-
| 情况 | 动作 |
|
|
228
|
-
|---|---|
|
|
229
|
-
| 属于某个任务,todo 里没有 | `abs todo add <id> --note "做什么"` |
|
|
230
|
-
| 属于已在登记的任务 | `abs todo note <id> --note "改到哪/下一步"` |
|
|
231
|
-
| 纯讨论/调研/只改 `.brain/` 自身 | **无需登记**,回「跳过」即可 |
|
|
232
|
-
|
|
233
|
-
> **「跳过」是合法选项** —— 不逼你造任务。假条目比不登记更坏(污染看板,下会话当真)。
|
|
234
|
-
>
|
|
235
|
-
> **判据是「整个项目」,但排除 `.brain/` 自身**:`abs note` / `abs log` 也写文件,
|
|
236
|
-
> 但那是**记录行为**不是任务 —— 不排除则每落一条经验都弹一次提醒,纯噪音。
|
|
237
|
-
>
|
|
238
|
-
> **提醒很频繁是故意的**:每轮写文件都提醒(同一文件改第二回也提)。嫌吵就当场登记,
|
|
239
|
-
> 登记完下一轮仍有写入还会提 —— 它盯的是「有活干就有登记」,不是「提醒过一次就算了」。
|
|
240
|
-
|
|
241
217
|
## 每轮结束:收尾循环(Stop / 告一段落后必做)
|
|
242
218
|
|
|
243
219
|
**每个任务边界、被 Stop/打断、告一段落时,别停半空。** 这是“开场接上状态、结束落回状态”的闭环。
|
|
@@ -337,3 +313,6 @@ abs supersede <页名> --by <取代它的新页> # 不写 --by 也行 = 单纯
|
|
|
337
313
|
- 只读写 `.brain/` 与目标代码,不动全局配置(一次性接入除外)。
|
|
338
314
|
- 只写真实发生的事实;遵守容量纪律,宁缺毋滥。
|
|
339
315
|
- 双链/frontmatter/index 必须自洽 —— 坏链 = 掰断接力棒。
|
|
316
|
+
- **排查 bug 时挂载 `bug-hunter` skill**(随 abs 一并安装,在 `skills/bug-hunter/`):
|
|
317
|
+
完整排查流程(列假设 → 复现 → 定位改动 → 打印 → 顺藤摸瓜 → 根因 → 验证)
|
|
318
|
+
与「给结论要明确」都在那份里。**排查方法不写在本文件**(两份维护必漂)。
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: bug-hunter
|
|
3
|
+
description: 按流程论排查 bug,先定位改动 → 日志打印 → 顺藤摸瓜逐层排查
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Bug 排查方法论
|
|
7
|
+
|
|
8
|
+
## 核心原则
|
|
9
|
+
|
|
10
|
+
**Bug 多数由新增或修改的功能引起。不凭空猜测,用数据说话。但数据不是终点——先复现,再定位,再理解根因。**
|
|
11
|
+
|
|
12
|
+
常见的失败模式是把"能复现的问题"当成"已定位的 bug",跳过理解直接下结论。定位到错误位置 ≠ 找到根因。改对症状而不改根因,换一批数据又会复发。
|
|
13
|
+
|
|
14
|
+
## 排查流程
|
|
15
|
+
|
|
16
|
+
### 第 -1 步:列假设 —— 方向错了怎么办(不能跳过)
|
|
17
|
+
|
|
18
|
+
**触发**:用户报了症状(“打不开”“报错了”“不对”),但没给一手错误信息。
|
|
19
|
+
|
|
20
|
+
**最大坑(真实案例)**:只有一个假设时,它要么被证实要么思路断掉 —— 于是**自己造一个输入**去测:
|
|
21
|
+
编了个接口名 `app.init` → curl 测出 404 → “我测出来的,是事实” → 推出“nginx 没配 PATH_INFO”
|
|
22
|
+
→ 用户纠正后**重跑同一实验、得到同一 404** → 坚信自己没错。
|
|
23
|
+
|
|
24
|
+
> 假证据最毒之处:**下游全程都是真的**(404 真、命令真跑过)。唯一假的那环
|
|
25
|
+
> (输入名)在最上游,且已消失在过程里 → 所以“再验证一次”救不了,
|
|
26
|
+
> 他会重跑同假输入得同真输出。
|
|
27
|
+
> 下面第 0 步“先复现”在拿不到报错时反而会逼出造假 —— 所以本步在它之前。
|
|
28
|
+
|
|
29
|
+
1. **开局列 ≥2 个假设**(代码层 / 配置层 / 构建产物 / 缓存 / 第三方后台)。
|
|
30
|
+
只列一个 = 逼自己去证实它 = 造证据的动力。
|
|
31
|
+
2. **每个假设配反证条件**:什么现象出现就认它错。
|
|
32
|
+
> 例:“假设 nginx 没配 PATH_INFO;反证条件:用**从代码里读到的真实接口名**
|
|
33
|
+
> 测出也是 404 → 接口本身不存在,与 nginx 无关 → 弃此假设。”
|
|
34
|
+
> 反证条件里写明“真实接口名”,`app.init` 这种编造的就自动不能用了。
|
|
35
|
+
3. **卡住两轮就换假设**,不是在同一假设上加细节。
|
|
36
|
+
4. **领域外的先声明无取证能力**(构建产物在不在 / 缓存 / 微信等第三方后台行为),
|
|
37
|
+
要用户提供事实,**不要脑补**。
|
|
38
|
+
|
|
39
|
+
**结论所依据的每个具体名字(接口/字段/报错文本/路径)必须指得出处(文件:行)。**
|
|
40
|
+
指不出的,只能标“推测”二字,**不得当证据用**。
|
|
41
|
+
|
|
42
|
+
> **为什么多假设能防造假**:单假设必须被证实,否则思路断 → 有动机造。
|
|
43
|
+
> 多假设下,“支持了 A” 不等于 “就是 A”(B/C/D 还没排除)→ **假证据推不出结论,造它就没意义了**。
|
|
44
|
+
|
|
45
|
+
### 第 0 步:复现 — 先让 Bug 稳定出现
|
|
46
|
+
|
|
47
|
+
没有稳定复现,后面全是猜。先确定:
|
|
48
|
+
- 复现的必要条件是什么?哪个输入/操作/环境触发?
|
|
49
|
+
- 是 100% 复现,还是偶发?偶发问题先找触发模式(特定数据、特定时机、特定顺序)。
|
|
50
|
+
- 日志里有没有现成的错误栈?错误信息 + 行号是最便宜的定位入口。
|
|
51
|
+
|
|
52
|
+
**给 Bug 起个名字**(一句话描述 + 触发条件)。排查卡住时,回去核对是否还在同一个 Bug 上。
|
|
53
|
+
|
|
54
|
+
### 第一步:定位范围 — 改动了什么
|
|
55
|
+
|
|
56
|
+
Bug 不会凭空出现。先查最近的代码改动:
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
git diff HEAD~3 --stat # 看改了哪些文件
|
|
60
|
+
git log --oneline -5 # 看最近提交
|
|
61
|
+
git diff <commit> -- <file> # 看具体改了什么
|
|
62
|
+
git log -p -S '可疑字段/函数名' # 谁改过这段代码
|
|
63
|
+
git blame <file> -L <行号,行号> # 定位到具体某行是谁写的
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
问自己:
|
|
67
|
+
- 这次新增/修改了什么功能?
|
|
68
|
+
- 改动涉及哪些文件?
|
|
69
|
+
- 哪个改动最可能影响到出问题的地方?
|
|
70
|
+
|
|
71
|
+
**不要排查无关代码,把精力集中在改动范围。**
|
|
72
|
+
|
|
73
|
+
### 第二步:日志打印 — 让数据说话
|
|
74
|
+
|
|
75
|
+
不要猜,打印出来看。根据技术栈选择打印方式:
|
|
76
|
+
|
|
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 或控制台 |
|
|
84
|
+
|
|
85
|
+
**打印要打印关键数据,不要只打印 "到了这里",要打印实际的变量值。**
|
|
86
|
+
|
|
87
|
+
```php
|
|
88
|
+
// ❌ 没用
|
|
89
|
+
\Log::error('进入方法');
|
|
90
|
+
|
|
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
|
+
### 第三步:顺藤摸瓜 — 按调用链逐层排查
|
|
102
|
+
|
|
103
|
+
从用户操作出发,沿着调用链一层层往下查:
|
|
104
|
+
|
|
105
|
+
```
|
|
106
|
+
用户操作(点击/请求)
|
|
107
|
+
→ 路由(routes / pages.json / url)
|
|
108
|
+
→ 门面/中间件(facade / middleware)
|
|
109
|
+
→ 控制器(controller)
|
|
110
|
+
→ 服务层(service)
|
|
111
|
+
→ 模型(model)
|
|
112
|
+
→ 数据库(SQL)
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
每一层都打印关键数据,确认数据在哪一层开始出错:
|
|
116
|
+
|
|
117
|
+
```php
|
|
118
|
+
// 控制器 — 打印接收到的参数
|
|
119
|
+
\Log::error('controller input', ['params' => $params]);
|
|
120
|
+
|
|
121
|
+
// 服务层 — 打印处理中的数据
|
|
122
|
+
\Log::error('service process', ['data' => $data, 'result' => $result]);
|
|
123
|
+
|
|
124
|
+
// 模型 — 打印查询结果
|
|
125
|
+
\Log::error('model query', ['sql' => $this->getLastSql(), 'result' => $result]);
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
**找到数据从正确变为错误的那一层,bug 就在那一层。**
|
|
129
|
+
|
|
130
|
+
### 第四步:找到根因,不只修症状
|
|
131
|
+
|
|
132
|
+
改对症状 = 换一批数据又复发。修复前回答:
|
|
133
|
+
- 为什么数据在这一层变错了?是**逻辑错误**(判断写反/字段取错),**类型错误**(null/数组/对象混用),还是**数据本身脏**(上游写入时就错了)?
|
|
134
|
+
- 根因若在上游(写入方/历史数据),这里 patch 只是挡一下,应该去修上游或在入口统一兜底。
|
|
135
|
+
|
|
136
|
+
**修一层,不改所有调用点**:如果多个地方都调用同一函数,只在出问题的那个调用点打补丁,其他调用点照样坏。在共享函数里修一次,是所有调用点的最小修复。
|
|
137
|
+
|
|
138
|
+
### 第五步:验证修复 — 逐行打印确认
|
|
139
|
+
|
|
140
|
+
修复 bug 后,不要凭空验证。**先拿第 0 步的复现条件重跑**,确认错误消失、结果正确,再用打印验证每一步符合预期:
|
|
141
|
+
|
|
142
|
+
```php
|
|
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]);
|
|
149
|
+
```
|
|
150
|
+
|
|
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
|
+
- 怀疑新增代码有问题
|
|
169
|
+
|
|
170
|
+
**第二步:查看日志**
|
|
171
|
+
- 日志直接告诉出错文件和行号
|
|
172
|
+
|
|
173
|
+
**第三步:顺藤摸瓜**
|
|
174
|
+
- 路由:`/shopro/goods/goods/detail/id/31`
|
|
175
|
+
- 控制器:`Goods::detail()` → 调用了 `GoodsMemberPriceModel::getByGoods()`
|
|
176
|
+
- 模型:`getByGoods()` 第50行 `select()->toArray()` → `select()` 返回数组,不是 Collection
|
|
177
|
+
|
|
178
|
+
**第四步:找根因**
|
|
179
|
+
- 为什么 id=31 出错而别的正常?查数据:31 这件商品有会员价记录 → 触发 `select()` 返回多行场景。可能是 `find()`/`select()` 返回值混用的历史问题。若只是个别记录脏,应清理数据或给方法加统一返回类型。
|
|
180
|
+
|
|
181
|
+
**第五步:修复验证**
|
|
182
|
+
```php
|
|
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;
|
|
189
|
+
```
|
|
190
|
+
- 重跑 id=31(不再报错)+ 跑一个无会员价记录的 id(确认没回归)
|
|
191
|
+
|
|
192
|
+
## 常见陷阱
|
|
193
|
+
|
|
194
|
+
| 陷阱 | 正确做法 |
|
|
195
|
+
|------|----------|
|
|
196
|
+
| 凭经验猜测 bug 位置 | 先复现 + 看日志/打印,用数据定位 |
|
|
197
|
+
| 只看代码不运行 | 打印实际运行时的数据 |
|
|
198
|
+
| 一次改很多地方再测试 | 改一处、验证一处 |
|
|
199
|
+
| 修症状不修根因 | 追问"为什么这层数据变了",确认根因 |
|
|
200
|
+
| 只验证出错的路径 | 重跑复现 + 回归正常路径 + 边界输入 |
|
|
201
|
+
| 忘记删除调试代码 | 修复确认后清理所有 `console.log` / `\Log::error` |
|
|
202
|
+
| 只在本地验证 | 确认服务器代码是否同步部署,数据是否一致 |
|
|
203
|
+
| 排查卡住还硬扛 | 回去核对第 0 步的 Bug 描述,确认是否在同一个问题上;必要时换数据/换触发角度重看 |
|
|
204
|
+
| 偶发当成必然 | 偶发先找触发模式,别用单个样本下结论 |
|
|
205
|
+
|
|
206
|
+
## 兜底:卡住时做什么
|
|
207
|
+
|
|
208
|
+
排查 >20 分钟没进展,不硬扛,回到流程检查:
|
|
209
|
+
1. **Bug 描述还准吗?** 是否有新的观察改变了问题定义。
|
|
210
|
+
2. **复现还稳定吗?** 换一个触发样本还出现吗。
|
|
211
|
+
3. **有没有漏看的日志?** grep 全量日志(不只最近的),可能早期就报过错。
|
|
212
|
+
4. **改动范围查全了吗?** 只看 HEAD~3 可能漏了分支合并、配置文件、部署差异。
|
|
213
|
+
5. **要不要跟同事确认?** 这条功能最近谁改的、改动意图是什么,可能一句话点醒。
|
|
214
|
+
6. **向上游看一层。** 数据/配置来源方是否也变了,不只是消费方的问题。
|
|
215
|
+
|
|
216
|
+
## 收尾:给结论要明确,不要模糊
|
|
217
|
+
|
|
218
|
+
**不许**:"可能是 X,也可能是 Y,建议排查一下" —— 这是把判断推回给用户。
|
|
219
|
+
|
|
220
|
+
**必须**:明确给出
|
|
221
|
+
1. **当前结论**(是什么 / 或者"未定位")
|
|
222
|
+
2. **置信度与依据**(哪来的证据 / 还是只是推测)
|
|
223
|
+
3. **下一步具体动作**(跑什么命令、看什么输出、要用户提供什么)
|
|
224
|
+
|
|
225
|
+
"未定位"是合法输出,但必须配一句"**需要你提供 X**"(具体到要什么都),
|
|
226
|
+
不能只说"无法确定"就结束。
|
|
227
|
+
|
|
228
|
+
> 与第 -1 步同一根因:模糊化是逃避判断。要么给结论 + 依据,要么明确索取证据。
|
package/src/install.js
CHANGED
|
@@ -16,6 +16,28 @@ const ABS_DIR = resolve(dirname(fileURLToPath(import.meta.url)), '..');
|
|
|
16
16
|
const HOOK_TEMPLATE = join(ABS_DIR, 'hooks', 'event.sh');
|
|
17
17
|
const SKILL_SOURCE = join(ABS_DIR, 'skill', 'SKILL.md');
|
|
18
18
|
|
|
19
|
+
// 随 abs 一并安装的附带 skill(agent 排查 bug 时挂载)。
|
|
20
|
+
// 必须放在本包内随包发布 —— 不能假设宿主已装:实测 codex 只有 abs 自己装的 skill,
|
|
21
|
+
// 若引用外部 skill 会成悬空。ponytail: 只加这一个,需要更多时改成扫 skill/ 子目录。
|
|
22
|
+
const BUNDLED_SKILLS = [
|
|
23
|
+
{ name: 'bug-hunter', src: join(ABS_DIR, 'skill', 'bug-hunter', 'SKILL.md') },
|
|
24
|
+
];
|
|
25
|
+
|
|
26
|
+
/** 安装主 skill + 全部附带 skill 到该宿主。收口在此:四处安装点共用,
|
|
27
|
+
* 新增附带 skill 时只改本函数(避免「改一份不算改」)。返回步骤行。 */
|
|
28
|
+
async function installSkills(agentKey) {
|
|
29
|
+
const steps = [];
|
|
30
|
+
const base = hostSkillDir(agentKey);
|
|
31
|
+
await atomicWrite(join(base, 'SKILL.md'), await fs.readFile(SKILL_SOURCE, 'utf8'));
|
|
32
|
+
steps.push(`✓ skill → ${join(base, 'SKILL.md')}`);
|
|
33
|
+
for (const b of BUNDLED_SKILLS) {
|
|
34
|
+
const t = join(base, '..', b.name, 'SKILL.md');
|
|
35
|
+
await atomicWrite(t, await fs.readFile(b.src, 'utf8'));
|
|
36
|
+
steps.push(`✓ skill → ${t}`);
|
|
37
|
+
}
|
|
38
|
+
return steps;
|
|
39
|
+
}
|
|
40
|
+
|
|
19
41
|
/**
|
|
20
42
|
* 本包的稳定入口路径解析(mcp.js / abs.js 通用)。
|
|
21
43
|
*
|
|
@@ -455,9 +477,7 @@ async function installClaudeCode({ withMcp, withSkill, log }) {
|
|
|
455
477
|
|
|
456
478
|
// 3) skill → <configRoot>/skills/abs-agent-brain-sync/SKILL.md (config 根 = CLAUDE_CONFIG_DIR)
|
|
457
479
|
if (withSkill) {
|
|
458
|
-
|
|
459
|
-
await atomicWrite(target, await fs.readFile(SKILL_SOURCE, 'utf8'));
|
|
460
|
-
steps.push(`✓ skill → ${target}`);
|
|
480
|
+
steps.push(...await installSkills('claude-code'));
|
|
461
481
|
}
|
|
462
482
|
return steps;
|
|
463
483
|
}
|
|
@@ -494,7 +514,8 @@ async function uninstallClaudeCode() {
|
|
|
494
514
|
// skill
|
|
495
515
|
const skillDir = hostSkillDir('claude-code');
|
|
496
516
|
await fs.rm(skillDir, { recursive: true, force: true });
|
|
497
|
-
|
|
517
|
+
for (const b of BUNDLED_SKILLS) await fs.rm(join(skillDir, '..', b.name), { recursive: true, force: true });
|
|
518
|
+
steps.push(`✓ skill 已删除(含附带: ${BUNDLED_SKILLS.map((x) => x.name).join(", ")})`);
|
|
498
519
|
return steps;
|
|
499
520
|
}
|
|
500
521
|
|
|
@@ -574,9 +595,7 @@ async function installCodex({ withMcp, withSkill, log }) {
|
|
|
574
595
|
}
|
|
575
596
|
}
|
|
576
597
|
if (withSkill) {
|
|
577
|
-
|
|
578
|
-
await atomicWrite(target, await fs.readFile(SKILL_SOURCE, 'utf8'));
|
|
579
|
-
steps.push(`✓ skill → ${target}`);
|
|
598
|
+
steps.push(...await installSkills('codex'));
|
|
580
599
|
}
|
|
581
600
|
return steps;
|
|
582
601
|
}
|
|
@@ -726,9 +745,7 @@ async function installOpenCode({ withMcp, withSkill, log }) {
|
|
|
726
745
|
steps.push(`✓ MCP → ${mcpP} (mcp.abs local)`);
|
|
727
746
|
}
|
|
728
747
|
if (withSkill) {
|
|
729
|
-
|
|
730
|
-
await atomicWrite(target, await fs.readFile(SKILL_SOURCE, 'utf8'));
|
|
731
|
-
steps.push(`✓ skill → ${target}`);
|
|
748
|
+
steps.push(...await installSkills('opencode'));
|
|
732
749
|
}
|
|
733
750
|
return steps;
|
|
734
751
|
}
|
|
@@ -775,9 +792,7 @@ async function installPi({ withMcp, withSkill, log }) {
|
|
|
775
792
|
steps.push(`✓ MCP → ${mcpP} (mcpServers.abs, stdio)`);
|
|
776
793
|
}
|
|
777
794
|
if (withSkill) {
|
|
778
|
-
|
|
779
|
-
await atomicWrite(target, await fs.readFile(SKILL_SOURCE, 'utf8'));
|
|
780
|
-
steps.push(`✓ skill → ${target}`);
|
|
795
|
+
steps.push(...await installSkills('pi'));
|
|
781
796
|
}
|
|
782
797
|
return steps;
|
|
783
798
|
}
|