来源:
notes/documents/渠道开关取代_sensitivity_方案与改造清单.md渠道开关取代 sensitivity:方案与改造清单
追踪副本:
.scratch/channel-switches/spec.md(tickets 与后续修订以那份为准,本笔记是同步快照)。 配套决策记录:docs/adr/0011-channel-switches-replace-sensitivity.md。 当前状态:本方案尚未过审。只有 ticket 01(schema 加开关 + 按实测回填)已落地, 且它是零行为变化的——新列全部回填成与今天实际到达一致,没有任何读点被切换。 ticket 02~08 仍处于挂起,等方案确认后放行。
Status: needs-triage(方案待用户过审;02~08 已挂起)
Problem Statement
sensitivity(private / internal / public)名义上是内容敏感度分级,实际是「这条记录允许被哪些目的地看见」的开关,
而且只是一个半成品:全库 9 个目的地里它只覆盖 2 个,public 与 internal 行为完全一致,且它在两张表里都是常量——
事件表 298 条 100% internal(唯一能算出 private 的算式在真实路径上到不了),笔记表 129 篇里 127 篇 private
(register_note 的默认值刷出来的,不是判断出来的)。详见
notes/documents/sensitivity_字段梳理与移除方案.md(note_41fb5cfa22d34bed)。
要表达「这条内容去哪些渠道」需要的是每个渠道一个布尔,不是三个互不相关的等级值。
本轮核对还发现一个更严重的事实:另外两个「渠道开关」里有一个是装饰品。
| 渠道 | 今天真正的闸门 | 结论 |
|---|---|---|
| 飞书日历 | calendar_enabled(scripts/calendar_entries.py 的 ELIGIBLE_DAILY_SQL / ELIGIBLE_NOTE_SQL) |
真闸门,工作正常 |
| 飞书 Base(事件) | sensitivity != 'private'(scripts/sync_to_feishu_base.py:559/702/806) |
待换成渠道开关 |
| 飞书 Base(笔记) | 没有闸门(scripts/note_base_projection.py:333 全量输出) |
待补 |
| 飞书文档 | status = 'active'(scripts/note_docs_projection.py) |
待独立成开关 |
| 博客 | 没有闸门。scripts/publish_private_blog_data.js:204 全量读 logs/daily_log.md 加密; |
|
andywu1998.github.io/scripts/import_personal_assistant_notes.py:76 rglob("*.md") 全量导入 |
待补(见下「回填」) | |
| 网页日程(Neon) | sensitivity(web/scripts/lib/daily-projection.mjs:64) |
待换成渠道开关 |
| 网页笔记面 | 构建产物全量(web/scripts/generate-notes.mjs) |
不在本轮范围 |
publish_enabled 目前唯一的可见效果是飞书 Base 里的一个展示列(「博客发布」)。
374 条内容标着 publish_enabled=0,却全部躺在博客上(129 篇笔记里 128 篇是 0;298 条事件里 246 条是 0)。
所以「给博客加上闸门」这一步如果照字段现值回填,等于一次性静默撤回几百条内容 —— 这是本 spec 最关键的一条约束。
Solution
目标模型:四个渠道开关,一个渠道一个布尔
| 开关 | 管辖 | 取值来源 |
|---|---|---|
feishu_docs_enabled(新增) |
飞书文档(笔记正文) | 每篇笔记独立 |
feishu_base_enabled(新增) |
飞书 Base:日常事件表 + 内容资产表 | 每条记录独立 |
calendar_enabled(已有) |
飞书日历 + 网页日程 | 每条记录独立 |
publish_enabled(已有,本轮接上) |
博客:加密 payload + 笔记导入 | 每条记录独立 |
外加一条规则:本地日视图不做任何闸门,logs/daily_log.md 永远渲染全文
(它是本机文件,脱敏只会让人误判自己的记录)。删掉 scripts/daily_log_view.py:75 的 [敏感记录已隐藏] 分支。
notes.status 保留既有含义(active / archived / withdrawn,生命周期),不再兼任 Docs 闸门;
withdrawn 仍然对所有渠道生效(撤回语义不变)。
关键约束:闸门上线必须按「当前实际到达了什么」回填
新开关的初值 = 该条记录今天是否真的到达了那个目的地,而不是照旧字段的值抄。 否则一次上线就等于静默撤回。实测回填表(2026-09-26):
| 表 | 渠道 | 今天实际到达 | 旧字段现值 | 新开关回填 |
|---|---|---|---|---|
| events(298) | Base | 298 条全部 | —— | feishu_base_enabled = 1 |
| events(298) | 日历 | 52 条 | calendar_enabled=1 52 条 |
不变(已一致) |
| events(298) | 博客 | 298 条全部 | publish_enabled=1 42 条 |
publish_enabled = 1(全量) |
| notes(129) | 文档 | 除 archived 外全部 | status |
feishu_docs_enabled = 1(全量) |
| notes(129) | Base | 129 条全部 | —— | feishu_base_enabled = 1 |
| notes(129) | 日历 | 129 条全部 | calendar_enabled=1 129 条 |
不变 |
| notes(129) | 博客 | 129 条全部 | publish_enabled=1 1 条 |
publish_enabled = 1(全量) |
飞书 Base 的列
按用户裁决「尽量和 SQLite 一样」:不做组合值列(不要「渠道 = Base/日历/博客」), 而是照 SQLite 的模型,一个渠道一个独立布尔列:
- 删「敏感级别」。
- 「日历同步」「博客发布」保留(已经是布尔列,模型正确)。
- 新增「飞书文档」(对应
feishu_docs_enabled)与「Base 同步」(对应feishu_base_enabled)。 - 「Base 同步」看着自指,但它有真实用途:Base 的行是从 SQLite 投影出来的,直接删行会被下一次投影重建;
只有
feishu_base_enabled=0才能表达「本地留着、但别放进 Base」。因此投影必须实现退出语义: 开关翻成 0 时把已投影的行删除(或置为撤回),而不是像今天sensitivity == 'private'那样只是continue。
实施步骤
阶段一:expand(新旧并存,可独立验证)
scripts/event_store.py:events/event_revisions加feishu_base_enabled;notes/note_revisions加feishu_docs_enabled与feishu_base_enabled(INTEGER NOT NULL DEFAULT 1)。 建表语句与增量迁移两处都加;dataclass、create_event、update_event、register_note、update_note_operational、_insert_note_revision同步带上。- 按上面的回填表写入初值(events 与 notes 的
publish_enabled一律置 1;feishu_*一律 1)。 - 读点切换:
scripts/sync_to_feishu_base.py:559/702/806:sensitivity != 'private'→feishu_base_enabledscripts/note_base_projection.py:371:同上;_note_rows()(:333)补上feishu_base_enabled过滤web/scripts/lib/daily-projection.mjs:64:日线改读feishu_base_enabled(笔记线保持不看)scripts/daily_log_view.py:75:删除脱敏分支scripts/note_docs_projection.py:Docs 闸门从status改成status='active' AND feishu_docs_enabled
- 写点带上参数:
scripts/note_cli.py加--feishu-docs/--no-feishu-docs、--feishu-base/--no-feishu-base、--publish-enabled/--no-publish-enabled(今天只有打开没有关闭)、--no-calendar;scripts/capture_event.py的推导改为写feishu_base_enabled; Go worker(main.go:219-232)透传。 - 博客闸门接上:
scripts/publish_private_blog_data.js与andywu1998.github.io/scripts/import_personal_assistant_notes.py按publish_enabled过滤。 因为第 2 步已全量回填为 1,这一步的行为变化必须为零(用博客 diff 验证)。
阶段二:观察
完整跑一次 python3 scripts/sync_daily_log_outputs.py + scripts/sync_all_outputs.sh,再等一个 5 分钟 web 投影周期。
对照 Base 行数、飞书日历条数、logs/daily_log.md 行数、博客生成物 diff,必须与改动前一致。
阶段三:contract(删除 sensitivity)
- SQLite:
DROP VIEW calendar_entries→ALTER TABLE events/event_revisions/notes/note_revisions DROP COLUMN sensitivity→ 重建视图。注意:本机 SQLite 3.46.1 实测可删带 CHECK 约束的列,但列被视图引用时会失败,所以顺序不能反。 - 代码:删
SENSITIVITY_LEVELS、_validate_content的校验、dataclass 字段、--sensitivity、calendar_entries.py的ENTRY_COLUMNS列、note_docs_projection.py:109、note_inbound_reconciliation.py:57。 - 飞书 Base:删「敏感级别」列,加「飞书文档」与「Base 同步」。
- Web:drizzle 迁移删
calendar_entries.sensitivity(ALTER TABLE ... DROP COLUMN), 同步web/backend/src/lib/schema.ts:75、web/scripts/lib/local-daily-events.mjs、web/scripts/project-calendar-entries.mjs;npm run db:check必须过。 需要web/.env.local里的DATABASE_URL(本机当前缺失,需先neon env pull)。 - 文档:
personal_assistant/skills/personal-assistant-workflow/references/note-cli.md:73、docs/adr/0006:26、web/docs/adr/0011:11、.scratch/web-multimodule-shell/spec.md:135。 - 测试:14 个文件 53 处断言改写;
tests/test_daily_log_view.py:118与tests/test_sqlite_projections.py:305两个「private 被隐藏」的用例改成「渠道开关关闭才被隐藏」。
验收
python3 -m unittest discover -s tests与cd web && npm test全绿。- Base 行数、日历条数、日视图行数、博客 payload diff 在 expand 前后完全一致。
grep -rn sensitivity scripts/ web/ tests/只剩历史快照(exports/、migration-artifacts/)。- 反向链路仍可用:在 Base 里改「状态 / 主题 / 日历同步 / 博客发布 / 飞书文档 / Base 同步 / 用户备注」,
note_inbound_reconciliation.py能回写;非法值仍报错。
非目标
- 不改
notes/status的生命周期语义。 - 不给网页笔记面加闸门(它服务的是构建产物,ADR-0008)。
- 不动
exports/、migration-artifacts/里的历史快照。 - 事件线不做反向编辑(只有笔记线有 Base 回写链路)。
回滚
data/daily_log.sqlite3 与代码都在 git 里,按提交逐个 revert 即可;
唯二不可自动回滚的是远端飞书 Base 的列与 Neon 的表结构,两者都只涉及这一个字段,重跑迁移可恢复。