状态:已批准(用户授权全自主执行,2026-08-22) 依据:v4 路线图(已裁决采纳)· 需求池 v0.11 区段(REQ-195~198)· pain-points-v4 §7.4/§7.5/§11/附录B 范围:v0.11.0(组基建·容器侧)→ v0.11.1(feed 进料)→ v0.11.2(学习循环统一)→ v0.11.3(组结算)
统一产物层「笔记组」:一切采集产物归入组,组是唯一容器;容器地形(课程)与 feed 地形(碎片)只是组的两种形成方式(v4 §7.4 融合点)。自底向上四层:
原子层:note_groups/fragments/flashcards schema + 路由/调度/结算纯函数(TDD)
业务层:db_note_groups.rs / group_route.rs / spacy scheduler / settlement
系统层:commands_groups.rs / commands_fragments.rs / commands_flashcards.rs
UI 层:NotesPage 组侧栏 + 组卡片(路由可见可改)+ 复习面(最小化)+ 结算向导
硬约束(写入每层实现):
- 提取优先:复习"先回忆再看",无被动重读模式
- 弹性承诺:周契约,断签不清零(review_logs 无 streak 字段)
- 动机App化防御:UI 最小化,引擎先有真牙
- 碎片身份诚实:fragments 独立表,不与 notes 混装
- feed 功能开关默认关;路由误判可见可改
CREATE TABLE IF NOT EXISTS note_groups (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
terrain TEXT NOT NULL DEFAULT 'container', -- container/feed(管线分叉前提)
kind TEXT NOT NULL DEFAULT 'standalone', -- course 课程组/topic 主题组/standalone 独立组
domain_tag TEXT, -- DomainKind kebab-case(主题组必填)
source TEXT NOT NULL DEFAULT 'route', -- route/series/manual
series_key TEXT, -- series_detect 系列名(课程组幂等键)
route_reason TEXT, -- 路由理由 JSON(命中信号明细,REQ-198)
route_overridden INTEGER NOT NULL DEFAULT 0, -- 用户改判标记(修改即记忆)
created_at INTEGER NOT NULL,
updated_at INTEGER NOT NULL
);
CREATE UNIQUE INDEX IF NOT EXISTS idx_groups_series
ON note_groups(series_key) WHERE series_key IS NOT NULL;
CREATE INDEX IF NOT EXISTS idx_groups_domain ON note_groups(domain_tag);
-- notes 补列(ensure_column 幂等;ON DELETE SET NULL——删组不断笔记资产)
ALTER TABLE notes ADD COLUMN group_id INTEGER REFERENCES note_groups(id) ON DELETE SET NULL;
CREATE INDEX IF NOT EXISTS idx_notes_group ON notes(group_id);复用 vote_detect/vote_tier 加权投票范式(每信号一票 → 计票 → 置信):
pub struct GroupRouteSignals {
pub has_series: bool, // series_detect 命中 → 直接课程组(不进投票)
pub chapter_density: f32, // 章节数/小时(chapter_detect 产出)
pub glossary_terms: usize, // 术语表条目数(glossary 产出)
pub ocr_text_density: f32, // OCR 文本块密度 0..1
pub profile_unknown: bool, // 形态 unknown(四维解耦信号)
pub tier_rich: bool, // 画面档 rich/mid(video_tier_detect)
pub domain_kind: Option<DomainKind>, // 领域命中(归主题组的前提)
}
pub enum RouteAction { OwnGroup, TopicGroup, NeedConfirm }
pub struct GroupRouteDecision {
pub action: RouteAction,
pub reasons: Vec<String>, // 命中信号明细(前端路由理由展示)
}
pub fn route_group(signals: &GroupRouteSignals) -> GroupRouteDecision;投票规则(附录B 判据:高密度自成一组、低密度归主题组):
- 高密度票:chapter_density ≥ 2.0/h、glossary_terms ≥ 5、ocr_text_density ≥ 0.5、tier_rich
- 低密度票:profile_unknown、chapter_density < 0.5、glossary_terms == 0
- 高密度 ≥2 票 → OwnGroup;低密度 ≥2 票且 domain 命中 → TopicGroup; 票数不足/高低冲突 → NeedConfirm(诚实不乱判,vote_tier 同口径)
- 零信号 → OwnGroup 兜底(会话产物默认独立成组,不埋进主题组——埋没比空洞更伤)
笔记落库后(单一落库出口):
extract_series(session.title)命中 → 课程组(series_key 幂等查找/创建, kind=course,source=series)——兑现 REQ-078 语义升级- 无系列 → 收集信号跑
route_group:- OwnGroup → 独立组(name=笔记标题)
- TopicGroup → 按 domain_tag 查/建主题组(kind=topic,terrain=container)
- NeedConfirm → 独立组 + route_reason 标记待确认(UI 高亮)
notes.group_id回写;route_reason 落组(REQ-198 可见)
list_note_groups(含组内笔记数)/get_note_group/create_topic_groupoverride_group_route(group_id, kind, domain_tag)——改判即记忆 (route_overridden=1,改判记录进 route_reason,REQ-198)move_note_to_group(note_id, group_id)/rename_note_group- 前端:NotesPage 左侧栏新增「组」区段(按 v4 §7.5 容器组在每日/会话面语义内); 组卡片显示路由理由明细 + 待确认徽标 + 一键改判(v0.9.0 检测卡交互哲学延续); 笔记列表支持按组过滤
CREATE TABLE IF NOT EXISTS fragments (
id INTEGER PRIMARY KEY AUTOINCREMENT,
text TEXT NOT NULL,
image_path TEXT, -- 剪贴板图片(存 data_dir/fragments/)
domain_tag TEXT, -- detect_domain 自动归组依据
group_id INTEGER REFERENCES note_groups(id) ON DELETE SET NULL,
source TEXT NOT NULL DEFAULT 'manual', -- manual/clipboard
created_at INTEGER NOT NULL
);feature_flags.json(应用数据目录):{ "feed_capture": false }——默认关, SettingsPage 开关;关闭时捕获 command 拒绝执行(不信任前端隐藏)capture_fragment(text, image_b64?):文本 ≤2000 字;图片解码白名单格式 落 data_dir/fragments/;detect_domain命中标题词 → 查/建 terrain=feed 主题组并挂 group_id;未命中 → 无组(结算面兜底归组)- DomainTag 归组粒度对齐领域(契约一:拒绝大类)
SettingsPage 开关 + NotesPage 组侧栏显示 feed 主题组(仅开关开启时); 快速捕获输入框(组侧栏顶部,文本粘贴即捕获;图片走剪贴板)。
- 优先引入
fsrs = "6"crate(纯算法零网络,FSRS-6;补 ADR-018); 若构建环境受限 → 手写sm2.rs纯函数兜底(EF 系数版,调度接口一致) - 统一接口(scheduler.rs):
pub enum Rating { Again, Hard, Good, Easy }
pub struct CardState { pub stability: f32, pub difficulty: f32,
pub reps: u32, pub lapses: u32, pub last_review_ms: u64 }
pub struct ScheduleOutcome { pub next_state: CardState, pub due_at_ms: u64 }
pub fn schedule(state: &CardState, rating: Rating, now_ms: u64) -> ScheduleOutcome;- 弹性承诺:无 streak 概念;欠账卡仅按 due_at 排序呈现,不追债不清零
CREATE TABLE IF NOT EXISTS flashcards (
id INTEGER PRIMARY KEY AUTOINCREMENT,
group_id INTEGER NOT NULL REFERENCES note_groups(id) ON DELETE CASCADE,
note_id INTEGER REFERENCES notes(id) ON DELETE SET NULL,
fragment_id INTEGER REFERENCES fragments(id) ON DELETE SET NULL,
front TEXT NOT NULL, back TEXT NOT NULL,
kind TEXT NOT NULL DEFAULT 'fact', -- 内容分型预埋:fact 先做
state_json TEXT NOT NULL, -- CardState 序列化
due_at INTEGER NOT NULL,
created_at INTEGER NOT NULL
);
CREATE INDEX IF NOT EXISTS idx_cards_due ON flashcards(due_at);
CREATE TABLE IF NOT EXISTS review_logs (
id INTEGER PRIMARY KEY AUTOINCREMENT,
card_id INTEGER NOT NULL REFERENCES flashcards(id) ON DELETE CASCADE,
rating TEXT NOT NULL, -- again/hard/good/easy
reviewed_at INTEGER NOT NULL
);
CREATE TABLE IF NOT EXISTS metrics_events (
id INTEGER PRIMARY KEY AUTOINCREMENT,
kind TEXT NOT NULL, -- card_reviewed/fragment_upgraded/
-- group_settled/self_test_done
payload_json TEXT NOT NULL DEFAULT '{}',
created_at INTEGER NOT NULL
);generate_group_cards(group_id):组内笔记的词汇表块术语(structure_note (锚点回链);fragment 文本 >2 句时首句为 front 其余为 back(提取优先)- AI 生成为可选增强:复用 ai_client 授权/成本/审计全套,默认关
- 生成幂等:同 group+front 查重跳过
- NotesPage 组卡片「复习」入口 → 复习队列(due_at ≤ now,按组过滤): 先显示 front → 点击「回忆完成」→ 展开 back → 四档评分(Again/Hard/Good/Easy)
- 主题自测最轻形态:组内随机 5 卡快答(仅评分不展示对错叙事——N14 防御)
- 每次评分落 review_logs + metrics_events(card_reviewed); fragment→card 落 metrics_events(fragment_upgraded)——Phase 4 门控数据从第一天记
pub struct SettlementSignals { pub item_count: usize, pub oldest_at_ms: u64,
pub last_settled_at_ms: Option<u64>, pub now_ms: u64 }
pub fn settlement_due(s: &SettlementSignals) -> bool;
// 阈值:item_count ≥ 50;或 周期:≥90 天未结算且 item_count ≥ 20- 核心提炼:组内词汇表/高频术语汇总为「组核心」笔记(本地规则; AI 精修可选,复用 AiRefine 协议)
- 重复合并:碎片/卡文本 Jaccard 去重(复用 asr_dedupe 归一化口径), 展示合并对由用户确认(不静默删)
- 低价值归档:无卡绑定且 180 天未访问的碎片标记 archived (fragments 补 status 列;不删除——可恢复)
CREATE TABLE IF NOT EXISTS settlements (
id INTEGER PRIMARY KEY AUTOINCREMENT,
group_id INTEGER NOT NULL REFERENCES note_groups(id) ON DELETE CASCADE,
stats_json TEXT NOT NULL, -- 提炼条数/合并对数/归档数
created_at INTEGER NOT NULL
);
-- fragments 补列(ensure_column):status TEXT NOT NULL DEFAULT 'active'结算完成落 metrics_events(group_settled)——北极星组成之一。
- 纯函数全 TDD(AAA + golden 用例):group_route(高/低/冲突/零信号/系列直判)、 schedule(四档评分间隔单调性、Again 重置)、settlement_due(阈值/周期边界)、 卡片生成幂等、去重合并
- 数据层用
:memory:库(db_notes_tests 同款 mem_db 范式) - command 层仅参数校验与编排,无业务逻辑(AGENTS.md §6)
- 模型文件零依赖(调度器纯算法;FSRS crate 无模型资产)
每个小版本独立交付闭环(代码 + 测试 + 需求池状态同步 + 版本文档 + 提交):
- v0.11.0:
feat(groups): ...3-5 个原子提交(原子层/接线/commands/UI/文档) - v0.11.1 同上;v0.11.2 启动时补 ADR-018(fsrs 依赖引入)
- v0.11.3 收尾全系列;需求池 REQ-195~198 状态逐版推进
- 单文件 ≤300 行(超限即拆 + 豁免登记);所有新模块含 @ai-context 注释