Phase 1

数据模型重构:bookmarks → content_items 通用表

数据模型重构:bookmarks → content_items 通用表

Context

MOSS 当前的 bookmarks 表完全绑定 X 书签(author_handle、posted_at 用 X 日期格式、category 是文件名)。MOSS 的目标是 6 域个人上下文中枢,长期要接入 RSS、Pocket、浏览器书签、文章等多种内容源。现在重构为通用 content_items 表,X 特有字段移入 JSON metadata 列。

这是一次数据模型 + 架构计划 + 全部图表的同步更新。


一、新数据模型

1. content_items(替代 bookmarks

CREATE TABLE IF NOT EXISTS content_items (
  id TEXT PRIMARY KEY,              -- MOSS 内部 ID;新增数据统一使用 UUID
  source TEXT NOT NULL,             -- 'x_bookmark' | 'rss' | 'pocket' | ...
  source_id TEXT NOT NULL,          -- 源系统原始 ID;本地内容使用内部 ID
  domain TEXT NOT NULL DEFAULT 'knowledge',  -- 6 域之一
  title TEXT NOT NULL,
  body TEXT NOT NULL DEFAULT '',    -- 原 text 字段
  url TEXT,
  tags TEXT NOT NULL DEFAULT '[]',  -- JSON array
  metadata TEXT NOT NULL DEFAULT '{}', -- 源专属字段(JSON object)
  created_at TEXT NOT NULL,         -- ISO 8601,原始创建时间(原 posted_at)
  imported_at TEXT NOT NULL,        -- ISO 8601,导入时间
  UNIQUE(source, source_id)
);

CREATE INDEX IF NOT EXISTS content_items_source_idx ON content_items(source);
CREATE INDEX IF NOT EXISTS content_items_domain_idx ON content_items(domain);
-- X 书签专用表达式索引:
CREATE INDEX IF NOT EXISTS content_items_author_idx
  ON content_items(json_extract(metadata, '$.author_handle'))
  WHERE source = 'x_bookmark';
CREATE INDEX IF NOT EXISTS content_items_category_idx
  ON content_items(json_extract(metadata, '$.category'))
  WHERE source = 'x_bookmark';

X 书签 metadata 示例:

{
  "author_name": "Tom Huang",
  "author_handle": "tuturetom",
  "category": "01-ai-agents-and-coding",
  "category_label": "AI、Agent 与编程",
  "ordinal": 1
}

连接器必须提供稳定 source_id:优先使用源系统原生 ID;不存在时使用规范化 URL 的确定性哈希;本地创建内容使用内部 id。随机 source_id 不允许进入导入流程。URL fallback 遵循 05-v2-architecture-plan.md 中带版本的规范化与 SHA-256 契约,算法变更必须通过显式数据迁移。

设计决策 结论 原因
PK 策略 内部 ID 与源 ID 分离 迁移时可保留 X snowflake 作为内部 ID;新增数据使用 UUID,幂等性由 (source, source_id) 保证
author/category metadata JSON 非通用字段;json_extract() + 表达式索引保障查询性能
textbody 改名 避免 SQL 保留字,语义更通用
posted_atcreated_at 改名 + ISO 8601 通用:原始创建时间
ordinal metadata 导入序号是源相关概念

2. feedback_events(FK 更新)

CREATE TABLE IF NOT EXISTS feedback_events (
  id INTEGER PRIMARY KEY AUTOINCREMENT,
  content_id TEXT NOT NULL REFERENCES content_items(id) ON DELETE CASCADE,
  value TEXT NOT NULL,
  created_at TEXT NOT NULL
);
CREATE INDEX IF NOT EXISTS feedback_content_idx ON feedback_events(content_id, created_at DESC);

仅改:bookmark_idcontent_id,FK → content_items(id)

3. import_runs(不变)

已通用。

4. 新增表

signals / audit_events 保持原设计。signal_runs 使用 append-and-publish 模型,增加 generation_keybase_keyinput_versionrule_versionstatusactivated_at;任一时刻最多只有一个 active run,刷新失败时继续保留旧 active run。普通访问按 base_key 命中,避免强制刷新 nonce 导致后续请求重复评估。


二、迁移策略(已有 996 条数据 + 2 条反馈)

getDatabase() 初始化时检测并迁移:

  1. 读取 PRAGMA user_version;旧版未标记数据库视为版本 0,v2 目标版本为 2
  2. PRAGMA table_info(bookmarks) → 检测旧表是否存在
  3. 创建 content_items(IF NOT EXISTS)
  4. 严格解析 posted_at,再写入 content_items;格式或日历语义错误必须报告内容 ID 并回滚
  5. 重建 feedback_eventsbookmark_idcontent_id):新表 → 复制 → 删旧 → 改名
  6. 校验迁移前后内容数、反馈数、孤儿外键数和日期转换结果
  7. DROP TABLE bookmarks
  8. 最后在同一事务中设置 PRAGMA user_version = 2
  9. 全部在单个事务中;失败保留旧版本号,未知的更高版本必须拒绝打开

新导入统一按 (source, source_id) upsert,不允许按内部 id 覆盖其他来源的数据。导入事务同时写入 import_runs 与对应 audit_events


三、类型更新 (src/lib/types.ts)

BookmarkRecord ContentItemRecord - {id, source, sourceId, domain, title, body, url?, tags, metadata, createdAt, importedAt}
BookmarkItem ContentItem - 扩展 ContentItemRecord + {feedback, score, reason}
FeedbackEvent.bookmarkId .contentId
FeedbackEvent.bookmarkTitle .contentTitle
ProfileTopic.bookmarkCount .itemCount
ProfileAuthor.bookmarkCount .itemCount

新增:XBookmarkMeta 辅助类型。


四、受影响文件清单

文件 变更
src/lib/db.ts DDL + 所有查询 + 函数名(importBookmarks → importContentItems 等)
src/lib/bookmarks.tssrc/lib/parsers/x-bookmarks.ts 文件重命名,返回 ContentItemRecord
src/lib/parsers/x-date.ts 迁移与新导入共用的严格 X 日期解析
src/lib/types.ts 类型重命名
src/components/moss-workbench.tsx Props/变量/UI 文案
src/app/page.tsx 导入和函数调用
src/app/api/feedback/route.ts 请求体字段 bookmarkId → contentId
scripts/import-bookmarks.tsscripts/import-content.ts 重命名
tests/bookmarks.test.tstests/content-items.test.ts 重命名
package.json 脚本名 import:bookmarks → import:content

五、图表更新

需要更新
data-model-diagram.html 重做 - content_items 替代 bookmarks,展示 metadata JSON 结构
architecture-diagrams.html ① 系统架构 更新解析器名称、数据访问函数名
② 组件架构 更新 Props 类型名
③ 路由结构 无变化
④ 规则引擎 SQL 中的 json_extract 查询
⑤ SSR 数据流 查询函数名
⑥ 实施时间线 Step 1 内容更新
⑦ 安全边界 无变化
05-v2-architecture-plan.md 数据模型和迁移章节

六、验证

  1. npm run import:content 导入 996 条 → content_items
  2. json_extract(metadata, '$.author_handle') 返回正确值
  3. 校准视图反馈/画像/历史正常
  4. npm run build 通过
  5. 已有 feedback_events 的 content_id 正确关联
  6. 两个来源使用相同 source_id 时互不覆盖;同源重复导入保持内部 id 不变
  7. 注入非法日期时迁移整体回滚,不产生伪造的当前时间
  8. 导入批次与 Audit Event 同事务提交