@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/src/store.js
CHANGED
|
@@ -7,6 +7,7 @@ import { requireUser, atTag, getUser } from './userconfig.js';
|
|
|
7
7
|
import { stripStateMark, ensureStateMark, normalizeTodo, addTask, upsertTask, boardText, readTodo, ensureTodo, todoTemplate, today, localStamp, setBreakpoint, setStateMark, TASK_STATES, insertDoneGrouped, idOfTaskLine, archiveDoneInText, upsertArchiveSection, DONE_KINDS, withDoneKind, doneKindOf, doneDateOf, collapseDone, SEC, rebuildStructure } from './todo.js';
|
|
8
8
|
import { editFile, SKIP } from './lock.js';
|
|
9
9
|
import { appendWrapup, strandedFor } from './wrapup.js';
|
|
10
|
+
import { keywords, pickRelevant, renderRelevant, recentFiles, rankPage } from './relevant.js';
|
|
10
11
|
|
|
11
12
|
// ---------- init: 建 .brain/ 骨架 ----------
|
|
12
13
|
const BRAIN_DIRS = ['entities', 'concepts', 'sources', 'syntheses', 'sessions'];
|
|
@@ -332,6 +333,27 @@ export async function cmdLoad({ dir }) {
|
|
|
332
333
|
''
|
|
333
334
|
);
|
|
334
335
|
}
|
|
336
|
+
// 卡点提取(2026-09-16,定位 = 看板可见性):
|
|
337
|
+
// 从 todo 里抽出带 `阻塞:` / `等待:` 的断点行,集中展示。
|
|
338
|
+
// 为何需要:断点行在任务下方,一屏扫过去看不出"哪些事在等人/等外部"。
|
|
339
|
+
// 只提取不求全 —— 只有明确写了前缀的才被抽出,没写的不猜。
|
|
340
|
+
const blocks = extractBlocks(todo);
|
|
341
|
+
if (blocks.length) {
|
|
342
|
+
const at = sections.findIndex((s) => String(s).startsWith('--- Rules'));
|
|
343
|
+
sections.splice(at === -1 ? 1 : at, 0,
|
|
344
|
+
`⛔ 卡点 ${blocks.length} 条(在等外部条件)`,
|
|
345
|
+
...blocks.map((b) => ` - ${b.id}: ${b.why}`),
|
|
346
|
+
'');
|
|
347
|
+
}
|
|
348
|
+
// 相关页提示(2026-09-16):实测 abs_query 几乎不被调用(MCP 日志 task 234 / note 54 / load 53 vs query 2)。
|
|
349
|
+
// 根因:load 只列目录 → 模型觉得"我记过了" → 真需要时靠印象。模型不知道自己不知道什么。
|
|
350
|
+
// 解法(用户定):不替它查 —— 而是【把该查的词直接给出来】,降低发起查询的成本。
|
|
351
|
+
// 自动带出正文是错的方向:那会让 query 更没人用,且无法按需求变化调整词。
|
|
352
|
+
// 失败静默:这只是 additive 提示,出任何错都不应弄坏 load。
|
|
353
|
+
try {
|
|
354
|
+
const rel = await queryHint(root, todo);
|
|
355
|
+
if (rel) sections.push('', rel);
|
|
356
|
+
} catch { /* 提示失败不影响 load */ }
|
|
335
357
|
// sources 堆积提醒:与「滞留」同构 —— 放在每次开工必经的顶部,而不是等人跑 lint。
|
|
336
358
|
// 只数目录条目(不读文件),零成本。提炼仍手工:这里只负责送达,不替判断。
|
|
337
359
|
const nsrc = await countSources(root);
|
|
@@ -346,6 +368,61 @@ export async function cmdLoad({ dir }) {
|
|
|
346
368
|
return sections.join('\n');
|
|
347
369
|
}
|
|
348
370
|
|
|
371
|
+
/**
|
|
372
|
+
* 从 todo 正文抽出"卡点"行,供 load 集中展示。
|
|
373
|
+
*
|
|
374
|
+
* 识别的前缀:`阻塞:` / `等待:` / `等 `(在断点/附属行里)。
|
|
375
|
+
* 为何只认前缀(不靠语义猜):没写就是没写,猜出来的卡点比不报更坑。
|
|
376
|
+
*
|
|
377
|
+
* @returns {{id:string, why:string}[]}
|
|
378
|
+
*/
|
|
379
|
+
export function extractBlocks(todoText) {
|
|
380
|
+
const out = [];
|
|
381
|
+
let curId = null;
|
|
382
|
+
for (const line of String(todoText || '').split('\n')) {
|
|
383
|
+
// 任务行(未完成):`- [ ] [状态] ID [[who]] — 描述`
|
|
384
|
+
const t = line.match(/^\s*-\s*\[\s*\]\s*(?:\[[^\]]*\]\s*)?(\S+)/);
|
|
385
|
+
if (t) { curId = t[1]; continue; }
|
|
386
|
+
// 完成行:不再跟踪
|
|
387
|
+
if (/^\s*-\s*\[x\]/.test(line)) { curId = null; continue; }
|
|
388
|
+
if (!curId) continue;
|
|
389
|
+
// 附属行(↳ 开头或缩进行)里找卡点前缀
|
|
390
|
+
const m = line.match(/^\s*(?:↳\s*断点:\s*)?(?:阻塞|等待|等)[::]\s*(.+)$/);
|
|
391
|
+
if (m && m[1].trim()) out.push({ id: curId, why: m[1].trim() });
|
|
392
|
+
}
|
|
393
|
+
return out;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
/**
|
|
397
|
+
* 开工提示:直接给出该查的几个词,降低发起 query 的成本。
|
|
398
|
+
*
|
|
399
|
+
* 为何不是"替它查"(2026-09-16 用户定):自动带出正文会让 query 更没人用,
|
|
400
|
+
* 且无法按需求变化调整词。正确做法是把词准备好,查询仍由调用方发。
|
|
401
|
+
*
|
|
402
|
+
* 词从两个环境事实推:活跃 todo 的关键词 + 最近改过的文件名。
|
|
403
|
+
* 输出保持极短(两行)—— 它只是提示,不是报告。
|
|
404
|
+
*/
|
|
405
|
+
async function queryHint(root, todoText) {
|
|
406
|
+
const active = String(todoText || '')
|
|
407
|
+
.split('\n')
|
|
408
|
+
.filter((l) => /^\s*-\s*\[\s*\]/.test(l))
|
|
409
|
+
.join(' ');
|
|
410
|
+
const files = await recentFiles(root, { n: 5 }).catch(() => []);
|
|
411
|
+
const src = [active, files.map((f) => f.name).join(' ')].filter(Boolean).join(' ');
|
|
412
|
+
const kws = keywords(src);
|
|
413
|
+
// 只取 3 个:英文词优先(中文 2-gram 单看无意义)
|
|
414
|
+
const en = kws.filter((k) => /^[a-z][a-z0-9_-]+$/.test(k));
|
|
415
|
+
const zh = kws.filter((k) => !/^[a-z][a-z0-9_-]+$/.test(k));
|
|
416
|
+
const pick = [...en.slice(0, 2), ...zh.slice(0, 1)].slice(0, 3);
|
|
417
|
+
if (!pick.length) return '';
|
|
418
|
+
const rows = [
|
|
419
|
+
'--- 开工前建议查一次(图谱里有相关经验,别重踩)---',
|
|
420
|
+
` abs query ${pick.join(' ')}`,
|
|
421
|
+
];
|
|
422
|
+
if (files.length) rows.push(` (据最近改动: ${files.slice(0, 3).map((f) => f.name).join(', ')})`);
|
|
423
|
+
return rows.join('\n');
|
|
424
|
+
}
|
|
425
|
+
|
|
349
426
|
/** 数 sources/ 下的 .md 文件数(只读目录,不解析内容)—— load 顶部提醒用。
|
|
350
427
|
* 坑: root 是【项目根】,图谱在 root/.brain/ 下 —— 必须走 brainPath,
|
|
351
428
|
* 直接用 join(root,'sources') 会 ENOENT 被 catch 吞成 0,成为又一个静默失效。 */
|
|
@@ -768,7 +845,12 @@ export async function cmdTeardownCheck({ dir, payload }) {
|
|
|
768
845
|
// ② 己方节流: 以 session_id 落 mark。缺 id 时退化为按项目+日期节流,
|
|
769
846
|
// 绝不"无节流"——否则一旦宿主张不到 id, decision:block 就会无限自激。
|
|
770
847
|
const sid = String(ev.session_id || ev.sessionId || '').replace(/[^\w-]/g, '');
|
|
771
|
-
|
|
848
|
+
// 兼底 key 只用【项目名】而非 full path。
|
|
849
|
+
// 坑(2026-09-16 实测): 原用 root.replace(/[^\w]/g,'_') 会把 cwd 的 tmp 路径
|
|
850
|
+
// (/var/folders/..._tmpXXXX) 也编进去 —— 而每个会话的 tmp 目录都不同,
|
|
851
|
+
// 于是"按项目+日期节流"根本没生效, 每次都新 key → ~/.abs/log/ 堆了 361 个 mark。
|
|
852
|
+
const projKey = root.split('/').filter(Boolean).pop() || 'unknown';
|
|
853
|
+
const key = sid || 'nosession-' + day + '-' + projKey;
|
|
772
854
|
const mark = join(absLogDir(), `teardown-${key}.mark`);
|
|
773
855
|
try {
|
|
774
856
|
await fs.access(mark);
|
|
@@ -776,19 +858,24 @@ export async function cmdTeardownCheck({ dir, payload }) {
|
|
|
776
858
|
} catch { /* 未推过 */ }
|
|
777
859
|
await fs.mkdir(dirname(mark), { recursive: true });
|
|
778
860
|
await fs.writeFile(mark, stamp).catch(() => {});
|
|
861
|
+
// mark 只增不减会堆成垃圾(实测 361 个)。只清【带旧日期】的 mark ——
|
|
862
|
+
// 节流只在当天有意义(判据是 day), 旧日期 mark 不可能再命中;
|
|
863
|
+
// 无日期的会话 mark(teardown-<sid>.mark)保留, 它靠自身存在与否判重。
|
|
864
|
+
try {
|
|
865
|
+
for (const f of await fs.readdir(absLogDir())) {
|
|
866
|
+
const m = f.match(/^teardown-.*-(\d{4}-\d{2}-\d{2})-.*\.mark$/);
|
|
867
|
+
if (m && m[1] !== day) await fs.unlink(join(absLogDir(), f)).catch(() => {});
|
|
868
|
+
}
|
|
869
|
+
} catch { /* 清理失败不影响主流程 */ }
|
|
779
870
|
|
|
780
871
|
const msg = [
|
|
781
|
-
// 未设姓名时把设置指令插到第0条 —— 否则后续 todo add/log/note
|
|
782
|
-
// 而收尾提醒本身不提这事,使用者只会看到一连串报错。
|
|
872
|
+
// 未设姓名时把设置指令插到第0条 —— 否则后续 todo add/log/note 全会被守卫拦下。
|
|
783
873
|
(await getUser() ? [] : [
|
|
784
|
-
'0) 本机尚未设置使用者姓名 —— 先跑 abs config set user
|
|
874
|
+
'0) 本机尚未设置使用者姓名 —— 先跑 abs config set user <你的名字>,否则 todo/log/note 都会被拦下;',
|
|
785
875
|
]),
|
|
786
|
-
'[abs
|
|
787
|
-
'
|
|
788
|
-
'
|
|
789
|
-
'3) 值得留的经验 abs note "..."(宁少勿滥,能从代码 grep 到的不记);',
|
|
790
|
-
'4) abs log "完成 X:..." 记一行工作成果,新页同步进 index。',
|
|
791
|
-
'简洁执行,不要复述本条提醒。若本次确实没有可沉淀产出,直接回一句"无可沉淀"即可。',
|
|
876
|
+
'[abs] 本会话改过文件,.brain/ 今日无记录。',
|
|
877
|
+
'这条是信息不是命令:该沉淀就沉淀,没有可沉淀的直接回一句「无可沉淀」,不用凑。',
|
|
878
|
+
'需要时:abs todo / abs todo done <id> / abs note "..." / abs log "..."',
|
|
792
879
|
].join('\n');
|
|
793
880
|
|
|
794
881
|
// Claude Code Stop hook 契约: {"decision":"block","reason":"..."} = 阻止结束并把 reason 回灌给 agent
|
|
@@ -903,10 +990,10 @@ export async function cmdTask({ dir, action, id, section, note, as }) {
|
|
|
903
990
|
cc = r.changed;
|
|
904
991
|
return r.changed.length ? { text: r.text } : SKIP;
|
|
905
992
|
});
|
|
906
|
-
return cc.length ? `✓ ${id} 状态 → [${note}]` : `
|
|
993
|
+
return cc.length ? `✓ ${id} 状态 → [${note}]` : `[NO_MATCH] 未找到含 "${id}" 的未完成任务行,或状态未变`;
|
|
907
994
|
}
|
|
908
995
|
if (action === 'note') {
|
|
909
|
-
if (!note) return '用法: abs todo note <id> --note "
|
|
996
|
+
if (!note) return '用法: abs todo note <id> --note "进度"\n 建议前缀(load 会把它们单独抽出来,让人一眼看到):\n 验证: <跑过的命令/结果>\n 边界: <这方案治不了什么>\n 阻塞: <在等什么,卡在哪>';
|
|
910
997
|
const r = await setBreakpoint(root, { id, text: note });
|
|
911
998
|
return r.msg;
|
|
912
999
|
}
|
|
@@ -949,7 +1036,7 @@ async function markDone(file, id, kind = '落地') {
|
|
|
949
1036
|
return { text: insertDoneGrouped(kept.join('\n'), moved) };
|
|
950
1037
|
});
|
|
951
1038
|
return res === SKIP
|
|
952
|
-
? `
|
|
1039
|
+
? `[NO_MATCH] 未找到含 "${id}" 的未完成任务行`
|
|
953
1040
|
: `✓ 已完成并归位 Done: ${id} 【${kind}】`;
|
|
954
1041
|
}
|
|
955
1042
|
|
|
@@ -1006,8 +1093,9 @@ export async function cmdQuery({ dir, terms, includeSuperseded }) {
|
|
|
1006
1093
|
if (!f.endsWith('.md') || f.startsWith('_')) continue;
|
|
1007
1094
|
const full = join(p, f);
|
|
1008
1095
|
const body = await fs.readFile(full, 'utf8').catch(() => '');
|
|
1009
|
-
const
|
|
1010
|
-
|
|
1096
|
+
const slug = f.replace(/\.md$/, '');
|
|
1097
|
+
const rank = rankPage(body, slug, words);
|
|
1098
|
+
if (rank) {
|
|
1011
1099
|
// 作者从本页 frontmatter 读(权威来源)。不在 index 行里重复 ——
|
|
1012
1100
|
// index 行是覆盖式更新的,作者会从"创建者"漂成"最后改的人"。
|
|
1013
1101
|
const au = body.match(/^author:\s*(.+)$/m);
|
|
@@ -1016,28 +1104,34 @@ export async function cmdQuery({ dir, terms, includeSuperseded }) {
|
|
|
1016
1104
|
// 而不是回答"我上次怎么解決 X"(那会拿到一个已知错误的答案)。
|
|
1017
1105
|
// 但它不是静默消失:计数里告知还有几条被隐藏(带 --all 能看)。
|
|
1018
1106
|
if (st === 'superseded' && !includeSuperseded) {
|
|
1019
|
-
hits.push({ hidden: true, slug
|
|
1107
|
+
hits.push({ hidden: true, slug });
|
|
1020
1108
|
continue;
|
|
1021
1109
|
}
|
|
1022
1110
|
hits.push({
|
|
1023
|
-
full, slug:
|
|
1111
|
+
full, slug, matched: rank.matched, kind: rank.kind, fuzzyScore: rank.score, via: rank.via,
|
|
1024
1112
|
author: au ? au[1].trim() : '',
|
|
1025
|
-
id: idOfPage(body,
|
|
1113
|
+
id: idOfPage(body, slug),
|
|
1026
1114
|
status: st,
|
|
1027
1115
|
supersededBy: supersededByOf(body),
|
|
1028
|
-
snippet: firstHitLine(body, words),
|
|
1116
|
+
snippet: firstHitLine(body, rank.matched.length ? rank.matched : words),
|
|
1029
1117
|
});
|
|
1030
1118
|
}
|
|
1031
1119
|
}
|
|
1032
1120
|
}
|
|
1033
1121
|
const hidden = hits.filter((h) => h.hidden).length;
|
|
1034
|
-
//
|
|
1035
|
-
//
|
|
1036
|
-
//
|
|
1037
|
-
//
|
|
1038
|
-
const
|
|
1039
|
-
.map((h) => ({ ...h, score: h.
|
|
1122
|
+
// 分两区:精确命中优先,模糊只作补充。
|
|
1123
|
+
// 坑(2026-09-16 实测): 不加区分时查 file-write-locking 返回 26 页(几乎全图)——
|
|
1124
|
+
// 连字符词的 2-gram 太通用,模糊命中把无关页也拉进来。
|
|
1125
|
+
// 规则:只要有任何精确命中,模糊命中的就不排在前面(但也不丢弃,单列一段)。
|
|
1126
|
+
const exactHits = hits.filter((h) => !h.hidden && h.kind === 'exact')
|
|
1127
|
+
.map((h) => ({ ...h, score: h.fuzzyScore }))
|
|
1128
|
+
.sort((a, b) => b.score - a.score);
|
|
1129
|
+
const fuzzyHits = hits.filter((h) => !h.hidden && h.kind === 'fuzzy')
|
|
1130
|
+
.map((h) => ({ ...h, score: h.fuzzyScore }))
|
|
1040
1131
|
.sort((a, b) => b.score - a.score);
|
|
1132
|
+
// 有精确命中 → 模糊全藏起来(但不静默:告知数量 + 怎么看)
|
|
1133
|
+
const fuzzySuppressed = exactHits.length > 0 && fuzzyHits.length > 0;
|
|
1134
|
+
const shown = exactHits.length ? exactHits : fuzzyHits;
|
|
1041
1135
|
if (!shown.length) {
|
|
1042
1136
|
const extra = hidden ? `(另外 ${hidden} 页已标记 superseded,用 abs query ${words.join(' ')} --all 查看)` : '';
|
|
1043
1137
|
return `query [${words.join(', ')}]: 无命中。${extra}用 abs lint 看图谱健康;首次使用先 abs init。`;
|
|
@@ -1049,13 +1143,19 @@ export async function cmdQuery({ dir, terms, includeSuperseded }) {
|
|
|
1049
1143
|
const id = h.id && h.id !== h.slug ? ` [id: ${h.id}]` : '';
|
|
1050
1144
|
// draft = 未经核实。不拦使用,但必须让 AI 知道这是它自己没验证过的。
|
|
1051
1145
|
const st = h.status === 'draft' ? ' [draft 未核实]' : '';
|
|
1052
|
-
const full = words.length > 1 && h.
|
|
1053
|
-
|
|
1146
|
+
const full = h.kind === 'exact' && words.length > 1 && h.matched.length === words.length ? ' ★全命中' : '';
|
|
1147
|
+
// 模糊命中必须显式标出 —— 否则用户会把"语义相近"当成"真的是这条"。
|
|
1148
|
+
const fuzzy = h.kind === 'fuzzy' ? ' [模糊命中: 字面未出现,仅字形相近]' : '';
|
|
1149
|
+
const hitTxt = h.matched.length ? h.matched.join(', ') : '(无字面命中)';
|
|
1150
|
+
// 命中渠道:tag 最有价值(人工提炼的关键词),显式标出便于判断可信度
|
|
1151
|
+
const viaTag = h.via && h.via.tag.length ? ` [tag: ${h.via.tag.join(', ')}]` : '';
|
|
1152
|
+
return `📄 ${h.slug}${by}${id}${st}${fuzzy} (命中: ${hitTxt}${full})${viaTag}\n ${h.snippet}`;
|
|
1054
1153
|
});
|
|
1055
1154
|
// 多词且无全命中时告知降级了 —— 不静默给一堆弱相关结果。
|
|
1056
|
-
const anyFull = shown.some((h) => h.
|
|
1155
|
+
const anyFull = shown.some((h) => h.kind === 'exact' && h.matched.length === words.length);
|
|
1057
1156
|
const tail = [];
|
|
1058
1157
|
if (words.length > 1 && !anyFull) tail.push('', `(无页同时命中全部 ${words.length} 个词,以下按命中数排序)`);
|
|
1158
|
+
if (fuzzySuppressed) tail.push('', `(另有 ${fuzzyHits.length} 页字形相近但字面未命中,已隐藏 —— 它们通常不相关)`);
|
|
1059
1159
|
if (hidden) tail.push(``, `(${hidden} 页 superseded 已隐藏;--all 可看)`);
|
|
1060
1160
|
return [`query [${words.join(', ')}] → ${shown.length} 页:`, '', ...lines, ...tail].join('\n');
|
|
1061
1161
|
}
|
|
@@ -1096,9 +1196,9 @@ function firstHitLine(body, words) {
|
|
|
1096
1196
|
// ---------- note: 经验实时暂存(source 页,一念一落,防流失) ----------
|
|
1097
1197
|
const NOTE_DEDUP_MS = 60 * 1000;
|
|
1098
1198
|
|
|
1099
|
-
export async function cmdNote({ dir, text, tags }) {
|
|
1199
|
+
export async function cmdNote({ dir, text, tags, when }) {
|
|
1100
1200
|
const clean = String(text || '').trim();
|
|
1101
|
-
if (!clean) return '用法: abs note "经验/坑/技巧一句话"(落 sources/ 暂存页,实时不流失)';
|
|
1201
|
+
if (!clean) return '用法: abs note "经验/坑/技巧一句话" [--when "何时该读它"](落 sources/ 暂存页,实时不流失)';
|
|
1102
1202
|
let root;
|
|
1103
1203
|
try {
|
|
1104
1204
|
root = await requireBrain(dir || process.cwd());
|
|
@@ -1122,6 +1222,9 @@ export async function cmdNote({ dir, text, tags }) {
|
|
|
1122
1222
|
const slugSrc = slugOf(clean);
|
|
1123
1223
|
const file = `${today()}-${slugSrc || 'note'}.md`;
|
|
1124
1224
|
const heading = clip(clean, 80); // 页面标题: 完整优先, 超长才收口
|
|
1225
|
+
// 触发条件(2026-09-16):经验"写入多读得少"的根因之一是存的是结论、不是"何时该看"。
|
|
1226
|
+
// 带上 --when 后,load 的相关页推荐能按当前在做的事匹配,而不是按主题词。
|
|
1227
|
+
const whenText = String(when || '').trim();
|
|
1125
1228
|
const body = [
|
|
1126
1229
|
'---',
|
|
1127
1230
|
`tags: [${fmTags}]`,
|
|
@@ -1134,9 +1237,11 @@ export async function cmdNote({ dir, text, tags }) {
|
|
|
1134
1237
|
`# 来源:${heading}`,
|
|
1135
1238
|
'',
|
|
1136
1239
|
`TITLE: ${clean}`,
|
|
1240
|
+
...(whenText ? ['', `WHEN: ${whenText}`] : []),
|
|
1137
1241
|
'',
|
|
1138
1242
|
`## 记录(实时暂存,Teardown 时提炼进 concepts/ 后本页可删)`,
|
|
1139
1243
|
`- ${clean}`,
|
|
1244
|
+
...(whenText ? [`- 何时读:${whenText}`] : []),
|
|
1140
1245
|
'',
|
|
1141
1246
|
'## 关联连接',
|
|
1142
1247
|
`- ${atTag(who)} — 本页沉淀者`,
|
package/src/todo.js
CHANGED
|
@@ -724,7 +724,7 @@ export async function setBreakpoint(brainRoot, { id, text }) {
|
|
|
724
724
|
return { text: lines.join('\n'), ok: true };
|
|
725
725
|
});
|
|
726
726
|
return res === SKIP
|
|
727
|
-
? { ok: false, msg: `
|
|
727
|
+
? { ok: false, msg: `[NO_MATCH] 未找到含 "${id}" 的未完成任务行` }
|
|
728
728
|
: { ok: true, msg: `✓ 断点已落 → ${id}\n ${bp.trim()}` };
|
|
729
729
|
}
|
|
730
730
|
|
package/src/userconfig.js
CHANGED
|
@@ -54,12 +54,16 @@ export async function setUser(name) {
|
|
|
54
54
|
export async function requireUser() {
|
|
55
55
|
const u = await getUser();
|
|
56
56
|
if (u) return u;
|
|
57
|
-
|
|
58
|
-
'✗ 尚未设置使用者姓名 ——
|
|
59
|
-
' 请任选其一设置后重试:\n' +
|
|
60
|
-
' abs config set user <你的名字> (写入 ~/.abs/config.json, 一次即可)\n' +
|
|
61
|
-
' ABS_USER=<你的名字> abs ... (仅本次生效)'
|
|
57
|
+
const e = new Error(
|
|
58
|
+
'✗ 尚未设置使用者姓名 —— 图谱需要标记每条记录的作者。'
|
|
62
59
|
);
|
|
60
|
+
// 错误码:hook/脚本靠它区分「需先配置」与真故障(借 Anneal templateRefusal)。
|
|
61
|
+
e.code = 'NO_USER';
|
|
62
|
+
e.fallback =
|
|
63
|
+
'任选其一:\n' +
|
|
64
|
+
' abs config set user <你的名字> (写入 ~/.abs/config.json, 一次即可)\n' +
|
|
65
|
+
' ABS_USER=<你的名字> abs ... (仅本次生效)';
|
|
66
|
+
throw e;
|
|
63
67
|
}
|
|
64
68
|
|
|
65
69
|
/** 标记串:`[[name]]`(wikilink 到人页 entities/<name>.md)。
|