@fanchao8609/agent_brain_sync 1.6.0 → 1.7.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/bin/abs.js +11 -1
- package/package.json +1 -1
- package/skill/SKILL.md +16 -7
- package/src/store.js +95 -13
- package/src/todo.js +22 -6
- package/src/userconfig.js +4 -2
package/bin/abs.js
CHANGED
|
@@ -260,7 +260,17 @@ async function cmdConfig({ sub, value }) {
|
|
|
260
260
|
const name = rest.join(' ');
|
|
261
261
|
if (rest.length > 1) throw new Error(`✗ 姓名不能含空格(收到 "${name}")\n @标记无法解析带空格的姓名`);
|
|
262
262
|
const u = await setUser(name);
|
|
263
|
-
|
|
263
|
+
// 立即建人页(用户说"两者都行":配置时建一份,首次写操作也会兜底建)。
|
|
264
|
+
// 没 .brain/ 就跳过 —— 全局配置不该强绑某个项目。
|
|
265
|
+
let extra = '';
|
|
266
|
+
try {
|
|
267
|
+
const { requireBrain } = await import('../src/index.js');
|
|
268
|
+
const { ensurePersonPage } = await import('../src/store.js');
|
|
269
|
+
const root = await requireBrain(process.cwd());
|
|
270
|
+
const r = await ensurePersonPage(root, u);
|
|
271
|
+
if (r === 'created') extra = `\n 人页: .brain/entities/${u}.md (已登记 index)`;
|
|
272
|
+
} catch { /* 无图谱 / 建页失败都不影响配置生效 */ }
|
|
273
|
+
return `✓ user = ${u}\n → ${p}${extra}`;
|
|
264
274
|
}
|
|
265
275
|
throw new Error(`✗ 未知子命令 "${sub}"\n 用法: abs config [show] / abs config set user <名字>`);
|
|
266
276
|
}
|
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
|
|
|
@@ -232,12 +239,14 @@ monorepo 若多个子包各自独立交付,可各建一份 `.brain/`。`abs st
|
|
|
232
239
|
```markdown
|
|
233
240
|
---
|
|
234
241
|
tags: [concept, 领域] # 首标签 ∈ entity|concept|source|synthesis|session-log
|
|
235
|
-
author: fanchao # 作者(abs note
|
|
242
|
+
author: fanchao # 作者(abs note 自动写;手写页也须填,且与 entities/<name>.md 同名)
|
|
236
243
|
updated: YYYY-MM-DD
|
|
237
244
|
status: draft # 或 reviewed(仅指知识冲突裁决结案)
|
|
238
245
|
---
|
|
239
246
|
```
|
|
240
247
|
|
|
248
|
+
作者名同时是**人页 slug**:`[[fanchao]]` → `entities/fanchao.md`(技术栈 / 特点 / 名下踩过的坑)。
|
|
249
|
+
|
|
241
250
|
- **关联连接区**:每页必须有 `## 关联连接`,用 `[[页面名]]` 链相关页。严禁孤岛页。
|
|
242
251
|
- **知识冲突**:与旧页矛盾不静默覆盖。加 `## 知识冲突` 两版都留、标来源时间,交人工裁决。
|
|
243
252
|
- **命名即链接**:`[[Docker]]` 落 entities/Docker.md;`[[docker-prisma-429]]` 落 concepts/。别建别名层。
|
package/src/store.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
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
|
-
import { requireUser, atTag } from './userconfig.js';
|
|
6
|
+
import { requireUser, atTag, getUser } from './userconfig.js';
|
|
7
7
|
import { addTask, upsertTask, boardText, readTodo, ensureTodo, todoTemplate, today, localStamp, setBreakpoint, moveBlocked, insertDoneGrouped, idOfTaskLine, archiveDoneInText, renderArchivePage, renderArchiveBody, DONE_KINDS, withDoneKind, doneKindOf } from './todo.js';
|
|
8
8
|
import { editFile, SKIP } from './lock.js';
|
|
9
9
|
import { appendWrapup, strandedFor } from './wrapup.js';
|
|
@@ -35,7 +35,11 @@ async function createSkeleton(root, brain) {
|
|
|
35
35
|
await fs.writeFile(join(brain, 'index.md'), indexTemplate(), 'utf8');
|
|
36
36
|
await fs.writeFile(join(brain, 'log.md'), logTemplate(), 'utf8');
|
|
37
37
|
await fs.writeFile(join(brain, 'todo.md'), todoTemplate(), 'utf8');
|
|
38
|
-
|
|
38
|
+
// 建完就提示设姓名 —— 否则用户一路到第一次 todo add 才撞墙,
|
|
39
|
+
// 中间 init/load 都不提,根本不知道该设。
|
|
40
|
+
const who = await getUser();
|
|
41
|
+
const hint = who ? '' : `\n ⚠ 尚未设置使用者姓名,写操作(todo add/log/note)会先报错。\n 请先设置: abs config set user <你的名字>`;
|
|
42
|
+
return `✓ 已建图谱 ${brain}\n (模板见 SKILL.md「文件标准」)${hint}`;
|
|
39
43
|
}
|
|
40
44
|
|
|
41
45
|
/** 结构体检:6 目录 + 3 文件逐一核对。ok=false 时 problems 列出缺失项。 */
|
|
@@ -163,6 +167,17 @@ export async function cmdLoad({ dir }) {
|
|
|
163
167
|
const stranded = await strandedFor(root);
|
|
164
168
|
const sections = [
|
|
165
169
|
`📂 abs → 项目: ${root}`,
|
|
170
|
+
];
|
|
171
|
+
// 未设姓名时开场就提醒 —— load 是开机第一屏,不在这里提,
|
|
172
|
+
// 用户要撞到第一次写操作才知道(init/load 一路沉默)。
|
|
173
|
+
if (!(await getUser())) {
|
|
174
|
+
sections.push(
|
|
175
|
+
'⚠ 尚未设置使用者姓名(写操作会先报错)',
|
|
176
|
+
'→ abs config set user <你的名字> (或临时: ABS_USER=<名字> abs ...)',
|
|
177
|
+
''
|
|
178
|
+
);
|
|
179
|
+
}
|
|
180
|
+
sections.push(
|
|
166
181
|
'--- 当前路线 (index.md) ---',
|
|
167
182
|
index || '(index.md 为空)',
|
|
168
183
|
'',
|
|
@@ -171,15 +186,15 @@ export async function cmdLoad({ dir }) {
|
|
|
171
186
|
'',
|
|
172
187
|
'--- 最近动作 (log.md, 最新 5 条) ---',
|
|
173
188
|
recentLogLines(log, 5) || '(log.md 为空)',
|
|
174
|
-
|
|
189
|
+
);
|
|
175
190
|
if (stranded.length) {
|
|
176
191
|
const rows = stranded.map((t) => {
|
|
177
192
|
const bp = t.bp.length ? `\n ${t.bp.map((b) => `↳ 断点: ${b}`).join('\n ')}` : '';
|
|
178
193
|
return ` - ${t.body}${bp}`;
|
|
179
194
|
});
|
|
195
|
+
// 插在项目行之后、其它内容之前
|
|
180
196
|
sections.splice(
|
|
181
|
-
|
|
182
|
-
`📂 abs → 项目: ${root}`,
|
|
197
|
+
1, 0,
|
|
183
198
|
'⏳ 上会话滞留(未 done,先对账)',
|
|
184
199
|
rows.join('\n'),
|
|
185
200
|
'→ 完成: abs todo done <id>;未完: abs todo note <id> --note 断点',
|
|
@@ -319,7 +334,12 @@ export async function cmdTeardownCheck({ dir, payload }) {
|
|
|
319
334
|
await fs.writeFile(mark, stamp).catch(() => {});
|
|
320
335
|
|
|
321
336
|
const msg = [
|
|
322
|
-
|
|
337
|
+
// 未设姓名时把设置指令插到第0条 —— 否则后续 todo add/log/note 全会被守卫拦下,
|
|
338
|
+
// 而收尾提醒本身不提这事,使用者只会看到一连串报错。
|
|
339
|
+
(await getUser() ? [] : [
|
|
340
|
+
'0) 本机尚未设置使用者姓名 —— 先跑 abs config set user <你的名字>,否则下面 2/3/4 都会被拦下;',
|
|
341
|
+
]),
|
|
342
|
+
'[abs 收尾提醒] 本会话改过文件但 .brain/ 今天还没有记录。请立即走收尾循环:',
|
|
323
343
|
'1) 跑 abs load 看 Today 还有哪些未完成;',
|
|
324
344
|
'2) 实际做完漏登记的 abs todo done <id>,做到一半的 abs todo note <id> --note "断点";',
|
|
325
345
|
'3) 值得留的经验 abs note "..."(宁少勿滥,能从代码 grep 到的不记);',
|
|
@@ -385,6 +405,7 @@ function slugOf(text, n = 24) {
|
|
|
385
405
|
export async function cmdLog({ dir, title, kind = 'dev' }) {
|
|
386
406
|
const root = await requireBrain(dir || process.cwd());
|
|
387
407
|
const who = await requireUser(); // 写操作守卫
|
|
408
|
+
await ensurePersonPage(root, who); // 首次写操作即建人页(已存在不动)
|
|
388
409
|
const p = brainPath(root, 'log.md');
|
|
389
410
|
const stamp = localStamp();
|
|
390
411
|
// 不硬切: log.md 是人类读的成果摘要, 也是 abs load 的开机入口。600 码点够一条完整小结,
|
|
@@ -411,6 +432,7 @@ export async function cmdTask({ dir, action, id, section, note, as }) {
|
|
|
411
432
|
const root = await requireBrain(dir || process.cwd());
|
|
412
433
|
// 写操作守卫:无姓名不落盘(hook 调的 wrapup/teardown-check 不经过这里,不受影响)
|
|
413
434
|
const who = await requireUser();
|
|
435
|
+
await ensurePersonPage(root, who); // 首次写操作即建人页(已存在不动)
|
|
414
436
|
if (action === 'start') {
|
|
415
437
|
const r = await upsertTask(root, {
|
|
416
438
|
section: section || 'Today / In Progress',
|
|
@@ -520,12 +542,22 @@ export async function cmdQuery({ dir, terms }) {
|
|
|
520
542
|
const body = await fs.readFile(full, 'utf8').catch(() => '');
|
|
521
543
|
const matched = words.filter((w) => body.toLowerCase().includes(w.toLowerCase()));
|
|
522
544
|
if (matched.length) {
|
|
523
|
-
|
|
545
|
+
// 作者从本页 frontmatter 读(权威来源)。不在 index 行里重复 ——
|
|
546
|
+
// index 行是覆盖式更新的,作者会从"创建者"漂成"最后改的人"。
|
|
547
|
+
const au = body.match(/^author:\s*(.+)$/m);
|
|
548
|
+
hits.push({
|
|
549
|
+
full, slug: f.replace(/\.md$/, ''), matched,
|
|
550
|
+
author: au ? au[1].trim() : '',
|
|
551
|
+
snippet: firstHitLine(body, words),
|
|
552
|
+
});
|
|
524
553
|
}
|
|
525
554
|
}
|
|
526
555
|
}
|
|
527
556
|
if (!hits.length) return `query [${words.join(', ')}]: 无命中。用 abs lint 看图谱健康;首次使用先 abs init。`;
|
|
528
|
-
const lines = hits.map((h) =>
|
|
557
|
+
const lines = hits.map((h) => {
|
|
558
|
+
const by = h.author ? ` @${h.author}` : '';
|
|
559
|
+
return `📄 ${h.slug}${by} (命中: ${h.matched.join(', ')})\n ${h.snippet}`;
|
|
560
|
+
});
|
|
529
561
|
return [`query [${words.join(', ')}] → ${hits.length} 页:`, '', ...lines].join('\n');
|
|
530
562
|
}
|
|
531
563
|
|
|
@@ -553,6 +585,7 @@ export async function cmdNote({ dir, text, tags }) {
|
|
|
553
585
|
return `未找到 .brain/ 图谱。先在项目根运行: abs init`;
|
|
554
586
|
}
|
|
555
587
|
const who = await requireUser(); // 写操作守卫
|
|
588
|
+
await ensurePersonPage(root, who); // 首次写操作即建人页(已存在不动)
|
|
556
589
|
const srcDir = brainPath(root, 'sources');
|
|
557
590
|
await fs.mkdir(srcDir, { recursive: true });
|
|
558
591
|
// 幂等: 同文本 60s 内只落一份
|
|
@@ -584,6 +617,7 @@ export async function cmdNote({ dir, text, tags }) {
|
|
|
584
617
|
`- ${clean}`,
|
|
585
618
|
'',
|
|
586
619
|
'## 关联连接',
|
|
620
|
+
`- ${atTag(who)} — 本页沉淀者`,
|
|
587
621
|
'(提炼成 concepts 规律页后,在此挂双链到该页)',
|
|
588
622
|
'',
|
|
589
623
|
].join('\n');
|
|
@@ -593,22 +627,70 @@ export async function cmdNote({ dir, text, tags }) {
|
|
|
593
627
|
await fs.writeFile(tmp, body, 'utf8');
|
|
594
628
|
await fs.rename(tmp, srcFile);
|
|
595
629
|
// index Sources 区登记(锁内幂等:别页已登记则跳过,防并发重复) + log 一行
|
|
596
|
-
const iP = brainPath(root, 'index.md');
|
|
597
630
|
const slug = file.replace(/\.md$/, '');
|
|
598
|
-
|
|
631
|
+
await registerInIndex(root, 'Sources', slug, heading);
|
|
632
|
+
await cmdLog({ dir: root, title: clean, kind: 'note' });
|
|
633
|
+
return `✓ 经验暂存 → sources/${file}\n ${clean} ${atTag(who)}`;
|
|
634
|
+
}
|
|
635
|
+
// ---------- person: 使用者实体页(首次需要时创建,已存在则不动) ----------
|
|
636
|
+
/** 确保 entities/<name>.md 存在。已存在一律不动(里面的技术栈/特点是人工沉淀的)。
|
|
637
|
+
* 用 `wx` 独占写:并发下后到者拿到 EEXIST 就静默跳过,不覆盖。
|
|
638
|
+
* 失败不抛:建页是附带动作,不能因为它让 todo/log 写不进去。
|
|
639
|
+
* 返回 'created' | 'exists' | 'skip'。 */
|
|
640
|
+
export async function ensurePersonPage(root, name) {
|
|
641
|
+
const nm = String(name || '').trim();
|
|
642
|
+
if (!nm || !/^[\w\u4e00-\u9fff.-]+$/.test(nm)) return 'skip';
|
|
643
|
+
const dir = brainPath(root, 'entities');
|
|
644
|
+
const file = join(dir, `${nm}.md`);
|
|
645
|
+
const body = [
|
|
646
|
+
'---',
|
|
647
|
+
'tags: [entity, person]',
|
|
648
|
+
`author: ${nm}`,
|
|
649
|
+
`updated: ${today()}`,
|
|
650
|
+
'status: draft',
|
|
651
|
+
'---',
|
|
652
|
+
'',
|
|
653
|
+
`# ${nm}`,
|
|
654
|
+
'',
|
|
655
|
+
'## 技术栈',
|
|
656
|
+
'<!-- 沉淀时填: 主力语言/框架/工具链。例: TypeScript + Node, 熟悉 MCP 协议与 CLI 工具链 -->',
|
|
657
|
+
'',
|
|
658
|
+
'## 特点 / 工作习惯',
|
|
659
|
+
'<!-- 沉淀时填: 决策偏好、沟通习惯、反复出现的判断倾向。例: 先要方案后动手; 质疑"这需求是否需要存在" -->',
|
|
660
|
+
'',
|
|
661
|
+
'## 名下踩过的坑',
|
|
662
|
+
'(本页被 [[todo]] / [[log]] 里的作者标记引用;沉淀经验时在此挂双链)',
|
|
663
|
+
'',
|
|
664
|
+
].join('\n');
|
|
665
|
+
try {
|
|
666
|
+
await fs.mkdir(dir, { recursive: true });
|
|
667
|
+
await fs.writeFile(file, body, { encoding: 'utf8', flag: 'wx' });
|
|
668
|
+
} catch (e) {
|
|
669
|
+
if (e.code === 'EEXIST') return 'exists';
|
|
670
|
+
return 'skip';
|
|
671
|
+
}
|
|
672
|
+
// 只有真建成才登记 index(否则 index 指向不存在的页 → INDEX-DEAD-LINK)。
|
|
673
|
+
// 放这里而非各调用点:todo/log/note 三条写路径都要登记,抄三遍必漂。
|
|
674
|
+
await registerInIndex(root, 'Entities', nm, `${nm} — 使用者;技术栈 / 特点 / 名下踩过的坑`);
|
|
675
|
+
return 'created';
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
/** 把新页登记进 index.md 的指定分区(幂等)。供人页/其它程序建页用。 */
|
|
679
|
+
export async function registerInIndex(root, section, slug, desc) {
|
|
680
|
+
const iP = brainPath(root, 'index.md');
|
|
599
681
|
await editFile(iP, (index) => {
|
|
600
682
|
if (!index || index.includes(`[[${slug}]]`)) return SKIP;
|
|
601
|
-
const sIdx = index.indexOf(
|
|
683
|
+
const sIdx = index.indexOf(`## ${section}`);
|
|
602
684
|
if (sIdx === -1) return SKIP;
|
|
603
685
|
const after = index.indexOf('\n## ', sIdx + 1);
|
|
686
|
+
const line = `- [[${slug}]] — ${desc}`;
|
|
604
687
|
const next = after === -1
|
|
605
688
|
? `${index.replace(/\s*$/, '')}\n${line}\n`
|
|
606
689
|
: index.slice(0, after) + `\n${line}` + index.slice(after);
|
|
607
690
|
return { text: next };
|
|
608
691
|
});
|
|
609
|
-
await cmdLog({ dir: root, title: clean, kind: 'note' });
|
|
610
|
-
return `✓ 经验暂存 → sources/${file}\n ${clean} ${atTag(who)}`;
|
|
611
692
|
}
|
|
693
|
+
|
|
612
694
|
// ---------- lint: 体检(与 scripts/lint.sh 同规则的 Node 版,供 CLI/MCP 直调) ----------
|
|
613
695
|
export async function cmdLint({ dir }) {
|
|
614
696
|
let root;
|
package/src/todo.js
CHANGED
|
@@ -391,10 +391,13 @@ export async function upsertTask(brainRoot, { section, text }) {
|
|
|
391
391
|
const idx = findTaskLine(lines, id);
|
|
392
392
|
if (idx !== -1) {
|
|
393
393
|
// 原位更新:保留断点附属行 + 原@作者,只换任务行本体
|
|
394
|
-
const
|
|
395
|
-
const
|
|
394
|
+
const raw = lines[idx].replace(/^- \[ \] /, '');
|
|
395
|
+
const author = extractAuthor(raw);
|
|
396
|
+
const legacy = isLegacyAuthorTag(raw); // 旧行保持旧形态,不静默改写
|
|
397
|
+
const oldNote = extractNote(raw);
|
|
396
398
|
const merged = note && oldNote && oldNote.startsWith(note) ? `${note}${oldNote.slice(note.length)}` : note || oldNote;
|
|
397
|
-
const at = author ? `
|
|
399
|
+
const at = author ? ` ${legacy ? '@' + author : '[['
|
|
400
|
+
+ author + ']]'}` : '';
|
|
398
401
|
const newLine = `- [ ] ${id}${at}${merged ? ' — ' + merged : ''} (认领 ${today()})`;
|
|
399
402
|
lines[idx] = newLine;
|
|
400
403
|
return { text: lines.join('\n'), updated: true };
|
|
@@ -420,10 +423,23 @@ export function extractNote(text) {
|
|
|
420
423
|
return String(text).slice(i + 3).replace(/\s*\(认领[^)]*\)\s*$/, '').trim();
|
|
421
424
|
}
|
|
422
425
|
|
|
423
|
-
/** 提取 id
|
|
426
|
+
/** 提取 id 后的作者标记(无则空)。供 upsertTask 原位重建行时保留作者。
|
|
427
|
+
* 两种形态都要认:
|
|
428
|
+
* - 新: `- [ ] ID [[fanchao]] — note` (wikilink 到人页)
|
|
429
|
+
* - 旧: `- [ ] ID @fanchao — note` (裸 at 标记, 历史行)
|
|
430
|
+
* 旧行只在原位更新时按原形态保留, 不做批量回填(原文/现场已不在, 回填等于编造)。
|
|
431
|
+
* 返回的是**纯姓名**(不含 [[ ]] 与 @),由调用方决定用什么形态重写。 */
|
|
424
432
|
export function extractAuthor(text) {
|
|
425
|
-
const
|
|
426
|
-
|
|
433
|
+
const s = String(text);
|
|
434
|
+
const link = s.match(/^\S+\s+\[\[([^\]]+)\]\]/);
|
|
435
|
+
if (link) return link[1].trim();
|
|
436
|
+
const at = s.match(/^\S+\s+@([\w\u4e00-\u9fff.-]+)/);
|
|
437
|
+
return at ? at[1] : '';
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
/** 判断行里的作者标记是旧裸 `@name` 形态(保留旧形态,不擅自升级)。 */
|
|
441
|
+
export function isLegacyAuthorTag(text) {
|
|
442
|
+
return !/^\S+\s+\[\[/.test(String(text)) && /^\S+\s+@/.test(String(text));
|
|
427
443
|
}
|
|
428
444
|
|
|
429
445
|
/**
|
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
|
}
|