Co-Authored-By: Claude Code <noreply@anthropic.com>
9.6 KiB
SiteForge 纵向切片 · Plan 3:内容导入管道(阶段 3)实施计划
For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (
- [ ]) syntax for tracking.
Goal: 把阶段 2 的 UniversalRow 流落库:稳定身份幂等导入(内容修订 + current 指针)、term 物化(含自建缺失 taxonomy)、媒体登记(魔数/SVG 隔离/uploads 对拍)、route_registry 生成(§6.1 全表处置);终点 = 真实 hesupipe 全量导入成功且二导幂等(0 新修订)。
Architecture: 依据 spec §15 阶段 3、§5 数据模型(Plan 1 已建 21 表)、§6.1 路径处置、§7.5 checksum;消费 Plan 2 产出(runWxrAdapter 四事件流 / registerSource 快照 / 版本块)。新增 apps/api/src/modules/import/(编排服务)+ sf import 命令。导入不发布(published 指针不动——ADR-010)。
Tech Stack: 既有栈零新增依赖(file-type 类魔数检测手写常用签名表,避免新依赖——见 Task 4)。
边界(不做):显式 publish 命令(阶段 4)、构建/主题(阶段 4)、SEO diff(后续)、REST 通道、差量删除检测。
前置事实(执行者必读)
- Plan 2 移交清单 9 项是本计划必读输入(Plan 2 文末);尤其第 1 项(term 表缺 category/post_tag,按 (domain,nicename) 自建)、第 2 项(uploads 路径对拍)、第 5 项(消费接口契约)。
- 表结构已冻结(Plan 1):source_records 六列稳定身份 UNIQUE、content_revisions 不可变触发器、entry_locales 双指针、route_registry exact/collision 双键 + NULLS NOT DISTINCT。
- 既有纪律全套沿用:TDD、临时库测试、身份断言、无包级 test 脚本、typecheck 四段链、cms 只读。
- 导入语义核心(spec §7.5/§8.1):同一 (site, connection, source_type, source_id, locale) 的 row → 已有 source_record 且 normalized_checksum 相同 → skip(0 新修订);不同 → 新修订 + entry_locales.current_revision_id 前移(published 不动)。permalink 变化只影响 route,不影响条目身份。
- wppop/extras 等未知字段已在 UniversalRow.extras——落 revision.data.extras 原样保留。
仓库结构增量
apps/api/src/modules/import/
import.service.ts # 编排:事件流→各写入器→报告
term-writer.ts # wp:term 事件 + 自建缺失 term
content-writer.ts # row→entry/locale/revision/source_record(checksum skip)
media-writer.ts # attachment row + media_bundle 对拍→media_assets/usages
route-writer.ts # §6.1 处置表→route_registry
magic-bytes.ts # 常用文件签名检测(png/jpg/gif/webp/pdf/svg 判别 + svg 文本特征)
scripts/sf.ts # + import 命令
tests/integration/import-*.test.ts
Task 1: import 命令骨架 + import_jobs 生命周期
Files: apps/api/src/modules/import/{import.service.ts,import.module.ts};sf.ts + import;Test tests/integration/import-job.test.ts
- 失败测试:
sf import --site <key> --snapshot <id>(或按 connection+kind 选最新快照)→ import_jobs 行 created→running→succeeded(或 succeeded_with_issues/failed);idempotency_key 唯一(重复提交同 key 复用既有 job 不重跑);job 记录 mapping/adapter/sanitizer 版本块(来自 versions.ts) - 实现:job 状态机 + 报告落 artifacts/import//;空流(无 snapshot)→ 明确报错
- 绿 → commit
feat(import): job 骨架与生命周期
Task 2: term 物化(含自建缺失)
Files: term-writer.ts;Test tests/integration/import-terms.test.ts
- 失败测试(用 mini.xml 全链 + 手构缺表场景):
- wp:term 事件 → taxonomies(按 term_taxonomy 映射 object_type:category→post/product_cat→product/post_tag→post…)+ terms(source_id=term_id)+ 幂等重跑不重复
- 自建缺失 term(移交项①):row 的 taxonomyRefs 中 (domain, nicename) 无对应 term → 自动建(source_id=
auto:<nicename>、slug=nicename、name=引用 value)+ 报告计数 autoCreatedTerms;同一 nicename 多次引用只建一条(唯一 (site, taxonomy, source_id) 兜底) - content_terms 写入(is_primary:首个为 true 余 false——WP 单主分类语义近似,JSDoc 注明)
- 实现 + 绿 → commit
feat(import): term 物化与缺失自建
Task 3: 内容写入器(稳定身份 + checksum skip)
Files: content-writer.ts;Test tests/integration/import-content.test.ts
- 失败测试:
- 首导:每 row → source_records + content_entries + entry_locales(current_revision_id 设、published NULL)+ content_revisions(source=import、checksum=normalized checksum、data 含 title/knownFields/extras/清洗后 bodyHtml/seo)
- 二导同快照:0 新修订(checksum skip)、0 新 entry
- 内容变化(改 checksum 手构):新修订 + current 前移 + published 仍指旧值(ADR-010 断言)
- permalink 变化(改 sourceUrl 手构):不建新 entry(身份键不含 URL)+ route 更新走 Task 5;source_records.source_url 更新
- locale 语义:row.locale 必须匹配站点 default_locale 或显式支持列表——未知 locale 记错误跳过(Plan 1 纪律)
- 实现 + 绿 → commit
feat(import): 内容稳定身份写入
Task 4: 媒体登记(魔数/隔离/对拍)
Files: media-writer.ts + magic-bytes.ts;Test tests/integration/import-media.test.ts
- 失败测试:
- attachment row + media_bundle 清单对拍:attachment_url 的
/wp-content/uploads/<yyyy>/<mm>/<file>相对路径在清单中精确命中(移交项② uploads 约定)→ media_assets(public_path=旧路径、sha256 从清单取)+ media_usages(正文/featured/gallery 引用关系从 row.attachmentRefs+清洗后 HTML 提取) - 魔数校验:png/jpg/gif/webp/pdf 签名表手写(~40 行);声明 MIME 与检测不符 → 报告 issue;SVG 检出(文本
<svg特征)→ status=quarantined 不进产物(spec §11.1) - 失配三级(Plan 1 复审口径):清单无此文件 → missing + 报告;URL 命中但清单 sha 与实体不符 → 报告 issue;多候选(同名不同目录)→ 歧义不自动选,全部列报告(spec §5.5 匹配顺序 4 的纪律)
- 855 attachment 真实规模冒烟(Task 7 一并)
- attachment row + media_bundle 清单对拍:attachment_url 的
- 实现 + 绿 → commit
feat(import): 媒体登记与安全校验
Task 5: route_registry 生成(§6.1 处置全表)
Files: route-writer.ts;Test tests/integration/import-routes.test.ts
- 失败测试(spec §6.1 每行一用例,mini 数据手构补充):
- 内容 permalink → html(路径保真含 .html;target_content_id 关联);主列表页
/products/、/news/(站点配置声明)→ html;产品/文章分类 → html + 分页占位规则(page 1 即列表本体);tag/product_tag/日期/作者 → redirect 301 主列表;attachment 页 → 默认规则三态(sitemap 有→html/仅 WXR→301 媒体文件/确认删→410);?p=→ excluded_with_reason 登记 - exact/collision 双键:
/About与/about两条并存;同 exact_key 冲突(两内容同路径)→ 报告 conflict 不静默覆盖 - 重导幂等:同路径同目标 → 0 新行;目标变 → 更新(route 表可变,非不可变表)
- 内容 permalink → html(路径保真含 .html;target_content_id 关联);主列表页
- 实现 + 绿 → commit
feat(import): 路由注册表生成
Task 6: 编排集成 + 导入报告
Files: import.service.ts 补全;Test tests/integration/import-full.test.ts
- 失败测试:mini.xml register→import 全链:site/connection/snapshot→job→terms(2+自建0)→entries(5)→media(2+对拍)→routes(首页+2列表+5内容+2分类+2 attachment+tags…)→报告(各计数+版本块+冲突/失配清单);published_revision_id 全 NULL(导入不发布断言)
- 报告落 artifacts/import//reports/import.json+html(复用 Task 6 模板纪律:转义/封顶)
- 绿 → commit
feat(import): 全链编排与导入报告
Task 7: 真实 hesupipe 全量导入(阶段 3 验收)
Files: scripts/import-real.mjs(或直接 sf import 命令 + 采样);docs/evidence/phase3-import.md
- register 真实 WXR + 媒体目录(
D:/project/cms/storage/media/54f91c49-*只读副本到 storage/incoming——先 robocopy 约 500MB,耗时记录)→ sf import - 验收断言:entries=1047(855 attachment + 129+48+15);terms=451+自建数(报告对照冒烟证据的 807 引用覆盖);routes 计数与处置分布报告;二导 0 新修订;数据库行数与导入报告逐项一致
- 内存/耗时记录;证据文档;commit
test(evidence): 阶段3真实全量导入验收 - 任何计数不符 → 如实报告停下分析(同 Task 8 纪律)
Task 8: 收尾
- ADR-017(导入幂等语义:稳定身份键选择/checksum 输入域/自建 term 策略);README 状态更新;typecheck 四段链/lint/test 三绿;阶段 4 移交清单(publish 命令消费的接口:current 指针集合/审计动作形状;site_config_revisions 首版生成时机建议)
- commit + push 由用户执行
验收(对照 spec §15 阶段 3)
- 同 Snapshot 二次导入 0 新 Revision
- Permalink 变化不创建重复 Content Entry
- 媒体歧义不自动匹配(多候选全列报告)
- URL 冲突和不支持对象全部进入报告
- 真实全量导入成功 + 幂等重跑 + 报告对照
- 三绿;published 指针全程未动