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() + 表达式索引保障查询性能 |
text → body |
改名 | 避免 SQL 保留字,语义更通用 |
posted_at → created_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_id → content_id,FK → content_items(id)。
3. import_runs(不变)
已通用。
4. 新增表
signals / audit_events 保持原设计。signal_runs 使用 append-and-publish 模型,增加 generation_key、base_key、input_version、rule_version、status 和 activated_at;任一时刻最多只有一个 active run,刷新失败时继续保留旧 active run。普通访问按 base_key 命中,避免强制刷新 nonce 导致后续请求重复评估。
二、迁移策略(已有 996 条数据 + 2 条反馈)
在 getDatabase() 初始化时检测并迁移:
- 读取
PRAGMA user_version;旧版未标记数据库视为版本 0,v2 目标版本为 2 PRAGMA table_info(bookmarks)→ 检测旧表是否存在- 创建
content_items(IF NOT EXISTS) - 严格解析
posted_at,再写入content_items;格式或日历语义错误必须报告内容 ID 并回滚 - 重建
feedback_events(bookmark_id→content_id):新表 → 复制 → 删旧 → 改名 - 校验迁移前后内容数、反馈数、孤儿外键数和日期转换结果
DROP TABLE bookmarks- 最后在同一事务中设置
PRAGMA user_version = 2 - 全部在单个事务中;失败保留旧版本号,未知的更高版本必须拒绝打开
新导入统一按 (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.ts → src/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.ts → scripts/import-content.ts |
重命名 |
tests/bookmarks.test.ts → tests/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 |
数据模型和迁移章节 |
六、验证
npm run import:content导入 996 条 → content_itemsjson_extract(metadata, '$.author_handle')返回正确值- 校准视图反馈/画像/历史正常
npm run build通过- 已有 feedback_events 的 content_id 正确关联
- 两个来源使用相同 source_id 时互不覆盖;同源重复导入保持内部 id 不变
- 注入非法日期时迁移整体回滚,不产生伪造的当前时间
- 导入批次与 Audit Event 同事务提交