@fanchao8609/agent_brain_sync 1.6.1 → 1.7.1
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 +14 -3
- package/package.json +1 -1
- package/skill/SKILL.md +21 -7
- package/src/store.js +79 -13
- package/src/todo.js +72 -8
- package/src/userconfig.js +4 -2
package/bin/abs.js
CHANGED
|
@@ -76,6 +76,7 @@ const FLAG_SPEC = {
|
|
|
76
76
|
'keep-days': { type: 'string' },
|
|
77
77
|
'help': { type: 'boolean' },
|
|
78
78
|
'dry-run': { type: 'boolean' },
|
|
79
|
+
'full': { type: 'boolean' },
|
|
79
80
|
'yes': { type: 'boolean' },
|
|
80
81
|
'repair': { type: 'boolean' },
|
|
81
82
|
'no-mcp': { type: 'boolean' },
|
|
@@ -260,7 +261,17 @@ async function cmdConfig({ sub, value }) {
|
|
|
260
261
|
const name = rest.join(' ');
|
|
261
262
|
if (rest.length > 1) throw new Error(`✗ 姓名不能含空格(收到 "${name}")\n @标记无法解析带空格的姓名`);
|
|
262
263
|
const u = await setUser(name);
|
|
263
|
-
|
|
264
|
+
// 立即建人页(用户说"两者都行":配置时建一份,首次写操作也会兜底建)。
|
|
265
|
+
// 没 .brain/ 就跳过 —— 全局配置不该强绑某个项目。
|
|
266
|
+
let extra = '';
|
|
267
|
+
try {
|
|
268
|
+
const { requireBrain } = await import('../src/index.js');
|
|
269
|
+
const { ensurePersonPage } = await import('../src/store.js');
|
|
270
|
+
const root = await requireBrain(process.cwd());
|
|
271
|
+
const r = await ensurePersonPage(root, u);
|
|
272
|
+
if (r === 'created') extra = `\n 人页: .brain/entities/${u}.md (已登记 index)`;
|
|
273
|
+
} catch { /* 无图谱 / 建页失败都不影响配置生效 */ }
|
|
274
|
+
return `✓ user = ${u}\n → ${p}${extra}`;
|
|
264
275
|
}
|
|
265
276
|
throw new Error(`✗ 未知子命令 "${sub}"\n 用法: abs config [show] / abs config set user <名字>`);
|
|
266
277
|
}
|
|
@@ -303,8 +314,8 @@ async function main() {
|
|
|
303
314
|
case 'todo': {
|
|
304
315
|
const [sub, id, ...rest2] = opts._;
|
|
305
316
|
if (!sub) {
|
|
306
|
-
rejectExtra([id, ...rest2].filter(Boolean), 'abs todo');
|
|
307
|
-
console.log(await cmdShow({ dir: opts.dir, view: 'todo' }));
|
|
317
|
+
rejectExtra([id, ...rest2].filter(Boolean), 'abs todo [--full]');
|
|
318
|
+
console.log(await cmdShow({ dir: opts.dir, view: 'todo', full: opts.full }));
|
|
308
319
|
break;
|
|
309
320
|
}
|
|
310
321
|
const action = TODO_ACTIONS[sub];
|
package/package.json
CHANGED
package/skill/SKILL.md
CHANGED
|
@@ -69,17 +69,24 @@ abs config # 查看当前姓名
|
|
|
69
69
|
ABS_USER=<名字> abs todo add ... # 临时覆盖(CI/多身份),不改落盘配置
|
|
70
70
|
```
|
|
71
71
|
|
|
72
|
-
|
|
73
|
-
- `todo.md`:`- [ ] TASK-ID
|
|
74
|
-
- `log.md`:`## [2026-09-12 13:17]
|
|
72
|
+
设置后自动标记(**作者是人页的 wikilink,点得进去看技术栈/特点**):
|
|
73
|
+
- `todo.md`:`- [ ] TASK-ID [[fanchao]] — 说明 (认领 2026-09-12)`(作者紧跟 id,扫板先看到人)
|
|
74
|
+
- `log.md`:`## [2026-09-12 13:17] [[fanchao]] dev | 完成 X`(作者前置于 kind)
|
|
75
75
|
- `sources/`:frontmatter `author: fanchao`
|
|
76
76
|
|
|
77
|
+
同时自动建人页 `.brain/entities/<name>.md`(含「技术栈 / 特点·工作习惯 / 名下踩过的坑」三个空槽),
|
|
78
|
+
并登记进 `index.md` 的 Entities 区。**已存在则一律不动** —— 里面的内容是人工沉淀的,机器不许覆盖。
|
|
79
|
+
|
|
80
|
+
> **沉淀时顺手填人页**:经验提炼进 concepts/ 时,若观察到工程师的技术栈或判断倾向,
|
|
81
|
+
> 写进 `entities/<name>.md`。这页是可积累的画像,不是一次性标签。
|
|
82
|
+
|
|
77
83
|
**只读命令不检查**(`load`/`todo`/`status`/`lint`/`query`/`index`/`log` 无参)——
|
|
78
84
|
hook 在会话结束时非交互调 `abs wrapup`/`abs teardown-check`,那儿拦人会卡断收尾。
|
|
79
85
|
|
|
80
|
-
> `index.md`
|
|
81
|
-
> 语义不固定。要查谁写的,看该页自己的 `author
|
|
82
|
-
>
|
|
86
|
+
> `index.md` 的**经验行不加作者**:index 行是覆盖式更新的,作者会从"创建者"漂成"最后改的人",
|
|
87
|
+
> 语义不固定。要查谁写的,看该页自己的 `author`,或 `abs query` 输出(带每页 author)。
|
|
88
|
+
> 历史条目**不回填**:旧行的裸 `@name` 只在原位更新时按原形态保留,不批量改写(原文/现场已不在,
|
|
89
|
+
> 回填等于编造)。
|
|
83
90
|
|
|
84
91
|
## 图谱定位(一个项目一个 `.brain/`,abs 自动定位不用手工指定路径)
|
|
85
92
|
|
|
@@ -138,6 +145,11 @@ monorepo 若多个子包各自独立交付,可各建一份 `.brain/`。`abs st
|
|
|
138
145
|
图谱已存在;收到第一个核心开发指令**之前**走这条链载入上下文:
|
|
139
146
|
|
|
140
147
|
1. **读状态**:`abs load`(或 MCP `abs_load`)读 index 路线 + todo 看板 + 最近 log。
|
|
148
|
+
> **Done 区是折叠的(按日期计数)**,不是全量 —— Done 是无上限增长的归档区,
|
|
149
|
+
> 全量打印会随项目历史线性膨胀,把上下文塞满(曾占 load 输出 68.8%,
|
|
150
|
+
> 长历史项目上单次 load 吃掉 40% 上下文)。
|
|
151
|
+
> 要 Done 明细:`abs todo --full`,或看 `.brain/sessions/<日期>-todo归档.md`。
|
|
152
|
+
> 「最近动作」每条也按语义边界收口 —— 续接只需线索,不需要日志全文。
|
|
141
153
|
2. **对账滞留(强制,别跳过)**:若 `abs load` 顶部出现 `⏳ 上会话滞留`,说明上会话有任务做完/做到一半就断了。**先收尾再开工**:
|
|
142
154
|
- 快照里的任务现在真做完了 → `abs todo done <id>`(done 后下次 load 滞留自动消失);
|
|
143
155
|
- 还没做完 → `abs todo note <id> --note "接到哪/改到哪个文件"` 补断点(别空手续接)。
|
|
@@ -232,12 +244,14 @@ monorepo 若多个子包各自独立交付,可各建一份 `.brain/`。`abs st
|
|
|
232
244
|
```markdown
|
|
233
245
|
---
|
|
234
246
|
tags: [concept, 领域] # 首标签 ∈ entity|concept|source|synthesis|session-log
|
|
235
|
-
author: fanchao # 作者(abs note
|
|
247
|
+
author: fanchao # 作者(abs note 自动写;手写页也须填,且与 entities/<name>.md 同名)
|
|
236
248
|
updated: YYYY-MM-DD
|
|
237
249
|
status: draft # 或 reviewed(仅指知识冲突裁决结案)
|
|
238
250
|
---
|
|
239
251
|
```
|
|
240
252
|
|
|
253
|
+
作者名同时是**人页 slug**:`[[fanchao]]` → `entities/fanchao.md`(技术栈 / 特点 / 名下踩过的坑)。
|
|
254
|
+
|
|
241
255
|
- **关联连接区**:每页必须有 `## 关联连接`,用 `[[页面名]]` 链相关页。严禁孤岛页。
|
|
242
256
|
- **知识冲突**:与旧页矛盾不静默覆盖。加 `## 知识冲突` 两版都留、标来源时间,交人工裁决。
|
|
243
257
|
- **命名即链接**:`[[Docker]]` 落 entities/Docker.md;`[[docker-prisma-429]]` 落 concepts/。别建别名层。
|
package/src/store.js
CHANGED
|
@@ -4,7 +4,7 @@ import { promises as fs } from 'node:fs';
|
|
|
4
4
|
import { join, resolve, dirname } from 'node:path';
|
|
5
5
|
import { requireBrain, brainPath, absLogDir, BRAIN_DIR } from './index.js';
|
|
6
6
|
import { requireUser, atTag, getUser } from './userconfig.js';
|
|
7
|
-
import { addTask, upsertTask, boardText, readTodo, ensureTodo, todoTemplate, today, localStamp, setBreakpoint, moveBlocked, insertDoneGrouped, idOfTaskLine, archiveDoneInText, renderArchivePage, renderArchiveBody, DONE_KINDS, withDoneKind, doneKindOf } from './todo.js';
|
|
7
|
+
import { addTask, upsertTask, boardText, readTodo, ensureTodo, todoTemplate, today, localStamp, setBreakpoint, moveBlocked, insertDoneGrouped, idOfTaskLine, archiveDoneInText, renderArchivePage, renderArchiveBody, DONE_KINDS, withDoneKind, doneKindOf, collapseDone } from './todo.js';
|
|
8
8
|
import { editFile, SKIP } from './lock.js';
|
|
9
9
|
import { appendWrapup, strandedFor } from './wrapup.js';
|
|
10
10
|
|
|
@@ -128,9 +128,9 @@ export function logTemplate() {
|
|
|
128
128
|
}
|
|
129
129
|
|
|
130
130
|
// ---------- board: 看板 ----------
|
|
131
|
-
export async function cmdBoard({ dir }) {
|
|
131
|
+
export async function cmdBoard({ dir, full }) {
|
|
132
132
|
const root = await requireBrain(dir || process.cwd());
|
|
133
|
-
return boardText(root);
|
|
133
|
+
return boardText(root, undefined, { full: !!full });
|
|
134
134
|
}
|
|
135
135
|
|
|
136
136
|
// ---------- status: 定位报告 + 图谱概要 ----------
|
|
@@ -182,7 +182,8 @@ export async function cmdLoad({ dir }) {
|
|
|
182
182
|
index || '(index.md 为空)',
|
|
183
183
|
'',
|
|
184
184
|
'--- Todo 看板 (todo.md) ---',
|
|
185
|
-
todo.
|
|
185
|
+
collapseDone(todo).text || '(todo.md 为空)',
|
|
186
|
+
'\n(Done 已按日期折叠计数;明细: abs todo --full)',
|
|
186
187
|
'',
|
|
187
188
|
'--- 最近动作 (log.md, 最新 5 条) ---',
|
|
188
189
|
recentLogLines(log, 5) || '(log.md 为空)',
|
|
@@ -366,6 +367,9 @@ function recentLogLines(text, n) {
|
|
|
366
367
|
.split('\n')
|
|
367
368
|
.filter((l) => /^##\s*\[/.test(l))
|
|
368
369
|
.slice(0, n)
|
|
370
|
+
// 每条按语义边界收口:load 是开机读状态,只求"上会话做到哪"的线索,不须全文。
|
|
371
|
+
// 坑: 不收口时 5 条能占 2.9KB(单条 800B),又是一处随日志变长而膨胀的上下文。
|
|
372
|
+
.map((l) => clip(l, 220))
|
|
369
373
|
.join('\n');
|
|
370
374
|
}
|
|
371
375
|
|
|
@@ -405,6 +409,7 @@ function slugOf(text, n = 24) {
|
|
|
405
409
|
export async function cmdLog({ dir, title, kind = 'dev' }) {
|
|
406
410
|
const root = await requireBrain(dir || process.cwd());
|
|
407
411
|
const who = await requireUser(); // 写操作守卫
|
|
412
|
+
await ensurePersonPage(root, who); // 首次写操作即建人页(已存在不动)
|
|
408
413
|
const p = brainPath(root, 'log.md');
|
|
409
414
|
const stamp = localStamp();
|
|
410
415
|
// 不硬切: log.md 是人类读的成果摘要, 也是 abs load 的开机入口。600 码点够一条完整小结,
|
|
@@ -431,6 +436,7 @@ export async function cmdTask({ dir, action, id, section, note, as }) {
|
|
|
431
436
|
const root = await requireBrain(dir || process.cwd());
|
|
432
437
|
// 写操作守卫:无姓名不落盘(hook 调的 wrapup/teardown-check 不经过这里,不受影响)
|
|
433
438
|
const who = await requireUser();
|
|
439
|
+
await ensurePersonPage(root, who); // 首次写操作即建人页(已存在不动)
|
|
434
440
|
if (action === 'start') {
|
|
435
441
|
const r = await upsertTask(root, {
|
|
436
442
|
section: section || 'Today / In Progress',
|
|
@@ -491,7 +497,7 @@ async function markDone(file, id, kind = '落地') {
|
|
|
491
497
|
}
|
|
492
498
|
|
|
493
499
|
// ---------- show: 查看 index/todo/log(只读面) ----------
|
|
494
|
-
export async function cmdShow({ dir, view }) {
|
|
500
|
+
export async function cmdShow({ dir, view, full }) {
|
|
495
501
|
const v = String(view || '').toLowerCase();
|
|
496
502
|
if (!['todo', 'index', 'log'].includes(v)) {
|
|
497
503
|
return '用法: abs <todo|index|log> — todo=看板(原 board), index=图谱索引, log=操作流水';
|
|
@@ -508,7 +514,7 @@ export async function cmdShow({ dir, view }) {
|
|
|
508
514
|
return `${v}.md 不存在于 ${brainPath(root)}。初始化/补齐: abs init --repair`;
|
|
509
515
|
}
|
|
510
516
|
if (!text) return `(${v}.md 为空)`;
|
|
511
|
-
return v === 'todo' ? boardText(root, text) : text;
|
|
517
|
+
return v === 'todo' ? boardText(root, text, { full: !!full }) : text;
|
|
512
518
|
}
|
|
513
519
|
// ---------- query: 检索知识图谱(多词 OR,扫全 .md 页) ----------
|
|
514
520
|
const KNOWN_SLUG_HINT = /模板残留|\[\[slug\]\]/;
|
|
@@ -540,12 +546,22 @@ export async function cmdQuery({ dir, terms }) {
|
|
|
540
546
|
const body = await fs.readFile(full, 'utf8').catch(() => '');
|
|
541
547
|
const matched = words.filter((w) => body.toLowerCase().includes(w.toLowerCase()));
|
|
542
548
|
if (matched.length) {
|
|
543
|
-
|
|
549
|
+
// 作者从本页 frontmatter 读(权威来源)。不在 index 行里重复 ——
|
|
550
|
+
// index 行是覆盖式更新的,作者会从"创建者"漂成"最后改的人"。
|
|
551
|
+
const au = body.match(/^author:\s*(.+)$/m);
|
|
552
|
+
hits.push({
|
|
553
|
+
full, slug: f.replace(/\.md$/, ''), matched,
|
|
554
|
+
author: au ? au[1].trim() : '',
|
|
555
|
+
snippet: firstHitLine(body, words),
|
|
556
|
+
});
|
|
544
557
|
}
|
|
545
558
|
}
|
|
546
559
|
}
|
|
547
560
|
if (!hits.length) return `query [${words.join(', ')}]: 无命中。用 abs lint 看图谱健康;首次使用先 abs init。`;
|
|
548
|
-
const lines = hits.map((h) =>
|
|
561
|
+
const lines = hits.map((h) => {
|
|
562
|
+
const by = h.author ? ` @${h.author}` : '';
|
|
563
|
+
return `📄 ${h.slug}${by} (命中: ${h.matched.join(', ')})\n ${h.snippet}`;
|
|
564
|
+
});
|
|
549
565
|
return [`query [${words.join(', ')}] → ${hits.length} 页:`, '', ...lines].join('\n');
|
|
550
566
|
}
|
|
551
567
|
|
|
@@ -573,6 +589,7 @@ export async function cmdNote({ dir, text, tags }) {
|
|
|
573
589
|
return `未找到 .brain/ 图谱。先在项目根运行: abs init`;
|
|
574
590
|
}
|
|
575
591
|
const who = await requireUser(); // 写操作守卫
|
|
592
|
+
await ensurePersonPage(root, who); // 首次写操作即建人页(已存在不动)
|
|
576
593
|
const srcDir = brainPath(root, 'sources');
|
|
577
594
|
await fs.mkdir(srcDir, { recursive: true });
|
|
578
595
|
// 幂等: 同文本 60s 内只落一份
|
|
@@ -604,6 +621,7 @@ export async function cmdNote({ dir, text, tags }) {
|
|
|
604
621
|
`- ${clean}`,
|
|
605
622
|
'',
|
|
606
623
|
'## 关联连接',
|
|
624
|
+
`- ${atTag(who)} — 本页沉淀者`,
|
|
607
625
|
'(提炼成 concepts 规律页后,在此挂双链到该页)',
|
|
608
626
|
'',
|
|
609
627
|
].join('\n');
|
|
@@ -613,22 +631,70 @@ export async function cmdNote({ dir, text, tags }) {
|
|
|
613
631
|
await fs.writeFile(tmp, body, 'utf8');
|
|
614
632
|
await fs.rename(tmp, srcFile);
|
|
615
633
|
// index Sources 区登记(锁内幂等:别页已登记则跳过,防并发重复) + log 一行
|
|
616
|
-
const iP = brainPath(root, 'index.md');
|
|
617
634
|
const slug = file.replace(/\.md$/, '');
|
|
618
|
-
|
|
635
|
+
await registerInIndex(root, 'Sources', slug, heading);
|
|
636
|
+
await cmdLog({ dir: root, title: clean, kind: 'note' });
|
|
637
|
+
return `✓ 经验暂存 → sources/${file}\n ${clean} ${atTag(who)}`;
|
|
638
|
+
}
|
|
639
|
+
// ---------- person: 使用者实体页(首次需要时创建,已存在则不动) ----------
|
|
640
|
+
/** 确保 entities/<name>.md 存在。已存在一律不动(里面的技术栈/特点是人工沉淀的)。
|
|
641
|
+
* 用 `wx` 独占写:并发下后到者拿到 EEXIST 就静默跳过,不覆盖。
|
|
642
|
+
* 失败不抛:建页是附带动作,不能因为它让 todo/log 写不进去。
|
|
643
|
+
* 返回 'created' | 'exists' | 'skip'。 */
|
|
644
|
+
export async function ensurePersonPage(root, name) {
|
|
645
|
+
const nm = String(name || '').trim();
|
|
646
|
+
if (!nm || !/^[\w\u4e00-\u9fff.-]+$/.test(nm)) return 'skip';
|
|
647
|
+
const dir = brainPath(root, 'entities');
|
|
648
|
+
const file = join(dir, `${nm}.md`);
|
|
649
|
+
const body = [
|
|
650
|
+
'---',
|
|
651
|
+
'tags: [entity, person]',
|
|
652
|
+
`author: ${nm}`,
|
|
653
|
+
`updated: ${today()}`,
|
|
654
|
+
'status: draft',
|
|
655
|
+
'---',
|
|
656
|
+
'',
|
|
657
|
+
`# ${nm}`,
|
|
658
|
+
'',
|
|
659
|
+
'## 技术栈',
|
|
660
|
+
'<!-- 沉淀时填: 主力语言/框架/工具链。例: TypeScript + Node, 熟悉 MCP 协议与 CLI 工具链 -->',
|
|
661
|
+
'',
|
|
662
|
+
'## 特点 / 工作习惯',
|
|
663
|
+
'<!-- 沉淀时填: 决策偏好、沟通习惯、反复出现的判断倾向。例: 先要方案后动手; 质疑"这需求是否需要存在" -->',
|
|
664
|
+
'',
|
|
665
|
+
'## 名下踩过的坑',
|
|
666
|
+
'(本页被 [[todo]] / [[log]] 里的作者标记引用;沉淀经验时在此挂双链)',
|
|
667
|
+
'',
|
|
668
|
+
].join('\n');
|
|
669
|
+
try {
|
|
670
|
+
await fs.mkdir(dir, { recursive: true });
|
|
671
|
+
await fs.writeFile(file, body, { encoding: 'utf8', flag: 'wx' });
|
|
672
|
+
} catch (e) {
|
|
673
|
+
if (e.code === 'EEXIST') return 'exists';
|
|
674
|
+
return 'skip';
|
|
675
|
+
}
|
|
676
|
+
// 只有真建成才登记 index(否则 index 指向不存在的页 → INDEX-DEAD-LINK)。
|
|
677
|
+
// 放这里而非各调用点:todo/log/note 三条写路径都要登记,抄三遍必漂。
|
|
678
|
+
await registerInIndex(root, 'Entities', nm, `${nm} — 使用者;技术栈 / 特点 / 名下踩过的坑`);
|
|
679
|
+
return 'created';
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
/** 把新页登记进 index.md 的指定分区(幂等)。供人页/其它程序建页用。 */
|
|
683
|
+
export async function registerInIndex(root, section, slug, desc) {
|
|
684
|
+
const iP = brainPath(root, 'index.md');
|
|
619
685
|
await editFile(iP, (index) => {
|
|
620
686
|
if (!index || index.includes(`[[${slug}]]`)) return SKIP;
|
|
621
|
-
const sIdx = index.indexOf(
|
|
687
|
+
const sIdx = index.indexOf(`## ${section}`);
|
|
622
688
|
if (sIdx === -1) return SKIP;
|
|
623
689
|
const after = index.indexOf('\n## ', sIdx + 1);
|
|
690
|
+
const line = `- [[${slug}]] — ${desc}`;
|
|
624
691
|
const next = after === -1
|
|
625
692
|
? `${index.replace(/\s*$/, '')}\n${line}\n`
|
|
626
693
|
: index.slice(0, after) + `\n${line}` + index.slice(after);
|
|
627
694
|
return { text: next };
|
|
628
695
|
});
|
|
629
|
-
await cmdLog({ dir: root, title: clean, kind: 'note' });
|
|
630
|
-
return `✓ 经验暂存 → sources/${file}\n ${clean} ${atTag(who)}`;
|
|
631
696
|
}
|
|
697
|
+
|
|
632
698
|
// ---------- lint: 体检(与 scripts/lint.sh 同规则的 Node 版,供 CLI/MCP 直调) ----------
|
|
633
699
|
export async function cmdLint({ dir }) {
|
|
634
700
|
let root;
|
package/src/todo.js
CHANGED
|
@@ -160,6 +160,51 @@ function renderDoneGroups(units) {
|
|
|
160
160
|
return out;
|
|
161
161
|
}
|
|
162
162
|
|
|
163
|
+
/** 把 Done 区从「全量行」折叠为「按日期计数」,供 `abs load` / `abs todo` 这类
|
|
164
|
+
* 给人(和 AI 上下文)看的视图用。
|
|
165
|
+
*
|
|
166
|
+
* 坑: 这两个命令此前直接 `todo.trim()` 全量打印,而 Done 区**无上限增长** ——
|
|
167
|
+
* 本仓库一处就占 load 输出的 68.8%(19.6KB/28.4KB)。长历史机器上直接把
|
|
168
|
+
* 上下文塞满(实报:「另一台机器 abs load 塞了 40%」)。
|
|
169
|
+
* 这与 hooks.log / wrapup.log 同类问题(那两处有轮转),故这里只给计数,
|
|
170
|
+
* 要看明细用 `abs todo` 加 `--full`,或直接看 todo.md / 归档页。
|
|
171
|
+
*
|
|
172
|
+
* `### 归档` 区(历史的「归档 N 条」标记行)体积小且是长期引用,原样保留。
|
|
173
|
+
* 返回 { text, doneCount };text 不含 `## Done` 标题行。 */
|
|
174
|
+
export function collapseDone(text) {
|
|
175
|
+
const lines = String(text).split('\n');
|
|
176
|
+
const di = lines.findIndex((l) => l.startsWith('## Done'));
|
|
177
|
+
if (di === -1) return { text: String(text).trim(), doneCount: 0 };
|
|
178
|
+
const head = lines.slice(0, di);
|
|
179
|
+
const rest = lines.slice(di + 1);
|
|
180
|
+
const { groupLines, archiveLines } = splitDoneBody(rest);
|
|
181
|
+
const units = parseDoneUnits(groupLines);
|
|
182
|
+
// 按日期归组计数(新日期在前),未标日期的归尾
|
|
183
|
+
const byDate = new Map();
|
|
184
|
+
let undated = 0;
|
|
185
|
+
for (const u of units) {
|
|
186
|
+
if (!u.date) undated++;
|
|
187
|
+
else byDate.set(u.date, (byDate.get(u.date) || 0) + 1);
|
|
188
|
+
}
|
|
189
|
+
const counts = [...byDate.entries()]
|
|
190
|
+
.sort((a, b) => (a[0] < b[0] ? 1 : -1))
|
|
191
|
+
.map(([d, n]) => `${d} ${n} 条`);
|
|
192
|
+
if (undated) counts.push(`未标日期 ${undated} 条`);
|
|
193
|
+
// 只列最近 MAX_DONE_DATE_LINES 个日期组:日期组本身也随历史增长(实测 300 条跨 28 天
|
|
194
|
+
// 会把 load 输出从 660B 拉到 1203B)。载入视图只需"最近做了多少",早期历史看归档页。
|
|
195
|
+
const MAX_DONE_DATE_LINES = 7;
|
|
196
|
+
const shown = counts.slice(0, MAX_DONE_DATE_LINES);
|
|
197
|
+
if (counts.length > MAX_DONE_DATE_LINES) {
|
|
198
|
+
const rest = counts.length - MAX_DONE_DATE_LINES;
|
|
199
|
+
shown.push(`… 另有 ${rest} 个更早日期组(abs todo --full 看全量)`);
|
|
200
|
+
}
|
|
201
|
+
const doneLines = units.length
|
|
202
|
+
? [`## Done(${units.length} 条,按日期折叠)`, ...shown.map((c) => `- ${c}`)]
|
|
203
|
+
: ['## Done(0 条)'];
|
|
204
|
+
const out = [...head, ...doneLines, '', ...archiveLines].join('\n').replace(/\n{3,}/g, '\n\n').trim();
|
|
205
|
+
return { text: out, doneCount: units.length };
|
|
206
|
+
}
|
|
207
|
+
|
|
163
208
|
/** 归档标记区标题。它在 Done 区内部、日期分组之后,形如:
|
|
164
209
|
* ### 归档
|
|
165
210
|
* - [[2026-09-10-todo归档]] 完成任务 10 条
|
|
@@ -391,10 +436,13 @@ export async function upsertTask(brainRoot, { section, text }) {
|
|
|
391
436
|
const idx = findTaskLine(lines, id);
|
|
392
437
|
if (idx !== -1) {
|
|
393
438
|
// 原位更新:保留断点附属行 + 原@作者,只换任务行本体
|
|
394
|
-
const
|
|
395
|
-
const
|
|
439
|
+
const raw = lines[idx].replace(/^- \[ \] /, '');
|
|
440
|
+
const author = extractAuthor(raw);
|
|
441
|
+
const legacy = isLegacyAuthorTag(raw); // 旧行保持旧形态,不静默改写
|
|
442
|
+
const oldNote = extractNote(raw);
|
|
396
443
|
const merged = note && oldNote && oldNote.startsWith(note) ? `${note}${oldNote.slice(note.length)}` : note || oldNote;
|
|
397
|
-
const at = author ? `
|
|
444
|
+
const at = author ? ` ${legacy ? '@' + author : '[['
|
|
445
|
+
+ author + ']]'}` : '';
|
|
398
446
|
const newLine = `- [ ] ${id}${at}${merged ? ' — ' + merged : ''} (认领 ${today()})`;
|
|
399
447
|
lines[idx] = newLine;
|
|
400
448
|
return { text: lines.join('\n'), updated: true };
|
|
@@ -420,10 +468,23 @@ export function extractNote(text) {
|
|
|
420
468
|
return String(text).slice(i + 3).replace(/\s*\(认领[^)]*\)\s*$/, '').trim();
|
|
421
469
|
}
|
|
422
470
|
|
|
423
|
-
/** 提取 id
|
|
471
|
+
/** 提取 id 后的作者标记(无则空)。供 upsertTask 原位重建行时保留作者。
|
|
472
|
+
* 两种形态都要认:
|
|
473
|
+
* - 新: `- [ ] ID [[fanchao]] — note` (wikilink 到人页)
|
|
474
|
+
* - 旧: `- [ ] ID @fanchao — note` (裸 at 标记, 历史行)
|
|
475
|
+
* 旧行只在原位更新时按原形态保留, 不做批量回填(原文/现场已不在, 回填等于编造)。
|
|
476
|
+
* 返回的是**纯姓名**(不含 [[ ]] 与 @),由调用方决定用什么形态重写。 */
|
|
424
477
|
export function extractAuthor(text) {
|
|
425
|
-
const
|
|
426
|
-
|
|
478
|
+
const s = String(text);
|
|
479
|
+
const link = s.match(/^\S+\s+\[\[([^\]]+)\]\]/);
|
|
480
|
+
if (link) return link[1].trim();
|
|
481
|
+
const at = s.match(/^\S+\s+@([\w\u4e00-\u9fff.-]+)/);
|
|
482
|
+
return at ? at[1] : '';
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
/** 判断行里的作者标记是旧裸 `@name` 形态(保留旧形态,不擅自升级)。 */
|
|
486
|
+
export function isLegacyAuthorTag(text) {
|
|
487
|
+
return !/^\S+\s+\[\[/.test(String(text)) && /^\S+\s+@/.test(String(text));
|
|
427
488
|
}
|
|
428
489
|
|
|
429
490
|
/**
|
|
@@ -497,9 +558,12 @@ export async function moveBlocked(brainRoot, { id, reason }) {
|
|
|
497
558
|
}
|
|
498
559
|
|
|
499
560
|
// ---------- 看板输出 ----------
|
|
500
|
-
export async function boardText(brainRoot, textOverride) {
|
|
561
|
+
export async function boardText(brainRoot, textOverride, { full = false } = {}) {
|
|
501
562
|
const text = textOverride !== undefined ? textOverride : await readTodo(brainRoot);
|
|
502
563
|
const head = `📂 abs → 项目: ${brainRoot}`;
|
|
503
564
|
if (!text.trim()) return `${head}\n\n(todo.md 为空,先 abs todo add 登记任务)`;
|
|
504
|
-
|
|
565
|
+
// Done 区折叠:它无上限增长,全量打印会把上下文塞满(见 collapseDone 注释)
|
|
566
|
+
const body = full ? text.trim() : collapseDone(text).text;
|
|
567
|
+
const hint = full ? '' : '\n\n(Done 只给计数;看明细: abs todo --full)';
|
|
568
|
+
return `${head}\n\n${body}${hint}`;
|
|
505
569
|
}
|
package/src/userconfig.js
CHANGED
|
@@ -62,7 +62,9 @@ export async function requireUser() {
|
|
|
62
62
|
);
|
|
63
63
|
}
|
|
64
64
|
|
|
65
|
-
/**
|
|
65
|
+
/** 标记串:`[[name]]`(wikilink 到人页 entities/<name>.md)。
|
|
66
|
+
* 用 wiki 链接而非裸 `@name`:人是图谱实体,点得进去看技术栈/特点。
|
|
67
|
+
* 旧数据里的裸 `@name` 仍可解析(见 todo.js extractAuthor),但不回填。 */
|
|
66
68
|
export function atTag(name) {
|
|
67
|
-
return
|
|
69
|
+
return `[[${name}]]`;
|
|
68
70
|
}
|