来源:notes/documents/网页版笔记身份改用_Note_ID.md

网页版笔记身份改用 Note ID

Status: needs-triage Type: spec

上游依据:本轮对话结论(2026-09-22);web/CONTEXT.mdNote Path 词条; web/docs/adr/0005(v1 只读笔记)、0008(构建期产物)、0010(查询参数路由); .scratch/notes-sqlite-feishu-rearchitecture/spec.md 的 user story 4(「路径不作为业务身份」)。

Problem Statement

工作区已经有一等身份,网页版还在按路径认人。

事实 数字(2026-09-22 实测,含本 spec 自身)
notes/ 下的 Markdown 161 篇
构建产物 web/generated/notes.json 158 篇(上次构建的快照,缺今天新增的 3 篇)
SQLite notes 表已注册 112 条,note_id 与 front matter 逐条一致
front matter 有 note_id 的文件 114 篇
note_id 但没进注册表 2 篇(06_dg板卡X5_BPU推理服务_开放词表检测YOLO-World接入.md02_dg板卡X5_BPU推理服务_可视化HTML.md
完全没有 note_id 47 篇:sources/ 25、books/ 11、topics/ 6、projects/ 4、knowledge_base.md 1
note_path_aliases(旧路径 → note_id) 71 条
飞书日历的笔记线 事件描述里写的已经是「笔记ID」,即 note_id

缺口是整齐的:只要有 front matter,就一定带 note_id(0 例外);那 47 篇是「整个文件没有 front matter」,全部落在 sources/books/topics/projects/ 这些迁移前历史目录与来源全文里, 其中 18 篇本身就是 README.md 换句话说,缺口不是「注册漏了」,而是这些文件本来就没进过正式 笔记这条线——Q1 要决定的正是它们算不算「笔记」。

已注册的 112 条里有 5 个不是 note_<16hex> 形态(迁移期手写): note_2026aug_review_7f3a91c2note_dg_bpu_html_viznote_monocloud_cli_20260920note_cc_connect_feishu_video_delivery_20260905note_dg_smolvlm_vlm_20260919

路径在这条链上被当身份用的地方: 产物里只有 path 字段(parseNoteDocument 已解析出 front matter,finalizeNote 把它丢了);单篇接口 GET /api/notes/detail?path=;路由 ?n=<path>; 索引与搜索的列表 key、选中态比较;日程模块的笔记深链 notePath

后果与工作区 spec 早就点到的一样:重命名/移动一篇笔记等于删一条再加一条,身份丢失;网页侧的 身份与 SQLite registry(revisionentity_projection_state)、飞书投影对不上号。

日程是唯一已经把身份准备好的一面:calendar_entries 视图里笔记行的 id 就是 note_idnote_path 只是给深链用的。所以这次改造不是发明概念,是把网页版对齐到既有身份。

Solution

Note 的身份是 note_id,Note Path 降级为「兜底查找键 + 档号显示值」。

  1. 产物带上 id。 note-artifact.mjs 从 front matter 取 note_id,产物条目新增 idpath 保留。构建期不读 SQLite:ADR-0008 定的是产物只从 notes/ 派生,所以 id 只能来自文件自己的 front matter。
  2. 未注册的笔记不伪造 id。 没有 note_id 的(当前 47 篇),idpath:<path>。这样产物里 id 永远唯一且稳定,同时把「这不是一条注册笔记」这件事显式带出来。
  3. 接口只有一个身份入口。 GET /api/notes/detail?id=<note_id>不再接受 ?path=。服务端只认 id,就不存在「两个参数谁优先」的规则。旧链接的兼容放在前端:GET /api/notes 本来就一次性把全部 笔记元数据交给前端(158 条约 63 KB),路由解析时若 ?n= 不是已知 id,就在这份索引里按 path 查 一次,命中后 canonical 成 id;查不到仍是「产物里没有这篇笔记」。兼容逻辑因此只有前端一处、 删起来是一个 commit,服务端身份保持单一。id 是单段字符串,ADR-0005 里「斜杠路径匹配不上」的约束 顺势消失,但仍走查询参数,避免动 api/ 的路由约定。
  4. 路由切 id,显示不动。 ?n= 的语义改为 Note ID;页边 gutter 的档号继续显示 pathnote_ebcc838e0cde496a 放进 72px 页边既读不出也放不下)。身份与标签分离。解析顺序:?n= 已是产物里的 id 就直接用;否则在已加载的索引里按 path 解析;都不是才报「产物里没有这篇笔记」。 详情请求只在解析出 id 之后发出——冷启动走旧 path 链接会多等一次索引加载,这个代价只有旧链接付。
  5. 日程深链改吃 id。 Daily.tsxevent.notePath 改成 event.idnotePath 字段保留做显示, calendar_entries 视图不用改。

决定点(需要用户拍板)

  • Q1 · 非正式笔记去哪: sources/ 的来源全文、README.mdknowledge_base.md 这 47 篇, 是继续留在笔记模块(id 用 path: 兜底),还是从笔记模块移出、只出现在「材料与来源」模块? (web-multimodule-shell 的 spec 已经说过要分清「谁写的」,这次是顺势的机会。)
  • Q2 · 旧链接要不要保: 现有 ?n=documents/xx.md 书签、以及文件已改名的情况,只有拿到 note_path_aliases 才能恢复。别名在 SQLite 里,构建期读不到,要保就得给产物引入一份别名数据 (新 ADR,会碰 ADR-0008 的边界)。不保的话,重命名只保证链接不断。
  • Q3 · 档号: 维持「显示 path、身份用 id」(本 spec 的建议),还是干脆显示 id?
  • Q4 · 补注册: 47 篇无 id + 2 篇有 id 未注册,是否这一轮补齐(note_cli.py create --apply --force 会沿用已有 id)?还是先只改网页、缺口记账。若补齐,顺带决定 5 个手写 id 是否规范成 note_<16hex> (改 id 会让飞书投影按新身份重建,代价要单列)。

非目标

  • 不改飞书 Docs / Base / Calendar 三层投影链路,不改 note_cli.py
  • 不让构建期依赖 data/daily_log.sqlite3
  • 不加笔记编辑能力;网页仍是只读视图。
  • 不统一 note_dg_bpu_html_viz 这种手写 id 的形态(除非 Q4 选「补齐」)。

验收口径

cd codex_personal_assistant/web
npm run build            # 产物每条带 id,脚本断言 key 唯一
node --test tests/       # note-artifact / route 测试更新后全绿
npm run preview:local    # 本地只读预览
  • 产物里每条笔记都有 id;重复 id 让构建失败(而不是线上静默丢一条)。
  • ?n=note_xxx 能打开正文,且 /api/notes/detail 只接受 id=;旧形态 ?n=documents/xx.md 也能 打开(前端在索引里解析 path → id),并把地址栏 canonical 成 id。
  • 刷新、收藏、浏览器回退仍停在同一篇;从日程模块的笔记线深链进笔记模块是同一篇。
  • web/CONTEXT.mdNote Path 词条改写:path 是档号与兜底键,身份是 Note ID。

Tickets

# 标题 一句话
01 产物带 Note ID note-artifact.mjs 保留 front matter 的 note_id,无 id 用 path: 兜底,唯一性断言
02 详情接口只认 ID backend/src/lib/notes.tsapi/notes/detail.ts:唯一入口 ?id=,去掉 ?path=
03 前端路由切 ID + 旧 path 兜底 route.tsApp.tsxNotes.tsx:选中态与详情请求按 id;?n= 是旧 path 时先用索引解析成 id;档号仍显示 path
04 日程深链改用 Note ID Daily.tsxevent.idnotePath 降为显示字段
05 术语与 ADR web/CONTEXT.md 词条、web/docs/adr/0005/0008 的修订;是否新增「身份是 ID」的 ADR
06 补齐注册与来源分家 按 Q1/Q4 的结论处理 47 篇无 id 与 2 篇未注册(可能拆成两票)

Comments

  • 2026-09-22:spec 新建,状态 needs-triage,等用户对 Q1–Q4 拍板后再开 ticket。
  • 2026-09-22:详情接口不引入第二个参数。服务端只认 ?id=,旧 path 的兼容由前端拿已加载的索引 解析(这一条不扩大 Q2 的别名问题:别名仍缺,重命名过的旧 path 依旧打不开)。