Files
cms-website/docs/superpowers/plans/2026-10-21-slice-import-pipeline-phase3.md
T
2026-10-21 14:00:09 +08:00

120 lines
9.6 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 通道、差量删除检测。
---
## 前置事实(执行者必读)
1. **Plan 2 移交清单 9 项**是本计划必读输入(Plan 2 文末);尤其第 1 项(term 表缺 category/post_tag,按 (domain,nicename) 自建)、第 2 项(uploads 路径对拍)、第 5 项(消费接口契约)。
2. 表结构已冻结(Plan 1):source_records 六列稳定身份 UNIQUE、content_revisions 不可变触发器、entry_locales 双指针、route_registry exact/collision 双键 + NULLS NOT DISTINCT。
3. 既有纪律全套沿用:TDD、临时库测试、身份断言、无包级 test 脚本、typecheck 四段链、cms 只读。
4. **导入语义核心**(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,不影响条目身份。
5. wppop/extras 等未知字段已在 UniversalRow.extras——落 revision.data.extras 原样保留。
## 仓库结构增量
```text
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/<jobId>/;空流(无 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 一并)
- [ ] 实现 + 绿 → 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 表可变,非不可变表)
- [ ] 实现 + 绿 → 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/<jobId>/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 指针全程未动