Co-Authored-By: Claude Code <noreply@anthropic.com>
44 KiB
SiteForge 最小纵向切片设计与实施计划 V2.0
日期:2026-10-20
状态:已确认(2026-10-20 用户采纳为设计基线,附四项修订:①资产移植白名单列明 ②Tag/归档页默认 301 ③ZIP 输入移出切片 ④?p= 查询路由降级为登记不实现)
上位文档:SiteForge_完整项目计划方案_V3.0.md
现状依据:cms现状与SiteForge差距分析_V2.0.md
复用依据:SiteForge_现有CMS项目复用评估_V1.0.md
被替代文档:2026-10-20-siteforge-vertical-slice-design.md
验收目标:L3——使用真实试点数据完成本地全链路验证;不把本切片认定为生产上线完成。
1. 文档定位
SiteForge 是多站点静态迁移、内容维护、静态构建和发布平台。本计划定义第一个最小纵向切片,用 hesupipe.com 的 WordPress WXR 和媒体副本打通一条可重复、可审计、可继续演进的正式主链路:
授权与输入登记
→ 原始快照
→ WXR 流式解析
→ 内容/分类/媒体标准化
→ 草稿修订
→ URL 注册表
→ 显式发布修订
→ 冻结构建输入
→ 极简主题静态构建
→ 全量门禁
→ 基于同一 Manifest 的本地预览
本切片的目标不是尽快拼出一个演示站,而是建立后续后台、主题、差量迁移、部署和回滚都能继续使用的领域模型与契约。
1.1 权威关系
SiteForge_完整项目计划方案_V3.0.md是产品范围和总体架构的唯一基线。- 本文是第一个纵向切片的设计与实施基线。
cms现状与SiteForge差距分析_V2.0.md说明旧代码的真实成熟度和风险。- 旧 cms 文档和旧切片 V1 只作为历史参考,不覆盖本文。
1.2 前提约束
D:\project\cms保持只读,不在其中实施新架构。- 资产移植白名单(路线 C,列明如下)——可移植:WXR 解析规则与映射经验(重写到流式解析器之上)、测试夹具(脱敏后)、门禁规则逻辑、docker-compose/CI 配置;旧 cms 的 Astro 页面仅作视觉参考,不移植代码。不继承、按本设计重写:认证、BuildService、release/rollback、MigrationService 编排(迁移即发布)、settings JSONB 路由、Lead 服务、后台业务页面、整页硬编码的前台架构。
- 真实试点数据必须获得使用授权,只复制完成切片所需的 WXR、媒体和 Sitemap。
- 真实数据、媒体、数据库、凭据、完整报告不进入 Git。
- 测试夹具必须脱敏、最小化并可公开给项目开发者。
- 本切片只启动本地 CLI 和绑定回环地址的 Preview Server,不对公网开放控制 API。
- 所有新 Schema、契约和目录均按可长期演进的方式设计,不使用明确会被下一阶段推翻的临时结构。
2. 切片目标、范围与成功标准
2.1 目标
| 编号 | 目标 | 验收口径 |
|---|---|---|
| G1 | WXR 范围内全量导入 | 纳入范围的 post、page、product、taxonomy、term、attachment 全部被创建、更新、跳过或明确报错;不支持对象有统计和原因 |
| G2 | 幂等与不可变修订 | 第二次导入同一 Source Snapshot 产生 0 条新修订;内容变化产生新修订,不 UPDATE 历史修订 |
| G3 | 导入不发布 | 首次导入只更新 current revision;只有显式 publish 才更新 published revision |
| G4 | URL 注册表 | WXR + 本地 Sitemap 构成的“当前已知 URL 基线”全部有明确处置;未处理 🔴=0 |
| G5 | 路径和媒体保真 | 可保真的内容、分类、分页和媒体按旧路径输出;不能保真的路径使用明确 Redirect/Gone;登记为 unresolved 的路径必须在门禁前清零 |
| G6 | 可复现静态构建 | 构建只读取冻结快照;相同输入连续构建的业务文件 Hash 一致;禁止 Seed 和在线数据库读取 |
| G7 | 最小主题契约 | pilot-minimal 使用正式 Theme Manifest、Render Snapshot 和公共基础组件,不复制旧 cms 的整页硬编码架构 |
| G8 | 自动门禁 | 全量检查路由、SEO、死链、媒体、Sitemap、重定向图、路径冲突和远程依赖 |
| G9 | 本地 HTTP 预览 | Preview 只读取该 Build 的 Route Manifest 和 Dist,正确响应 HTML、Asset、301、410 和 404 |
| G10 | 可审计证据 | Import、Publish、Build、Validation 全部持久化状态;生成 JSON/HTML 报告和脱敏验收摘要 |
2.2 本切片支持的 WordPress 范围
必须支持:
post→news_articlepage→basic_pageproductCPT →product- 分类、标签、产品分类、产品标签及内容关系
- Attachment 元数据、特色图片、正文图片、产品图库
- Yoast 常见 Title、Description 和主分类字段
_thumbnail_id_product_image_gallery- 试点已知
wppop_sections等主题字段 .html、.php、多级目录等原 permalink- Sitemap 文件中的额外已知 URL
必须识别但允许进入“不支持清单”:
- 未配置映射的 CPT
- Elementor/WPBakery 等复杂布局数据
- 未知短代码
- 插件私有表中、WXR 不包含的数据
- 复杂 WooCommerce 变体、库存、订单等非展示数据
- 评论、用户和敏感个人数据
“全量导入”是指本切片范围内的所有对象均有明确结果,不代表已经支持 WordPress 的全部插件生态。
2.3 非目标
以下内容不在本切片交付范围:
- Art Design Pro 管理后台。
- ZIP 媒体包解包输入(本切片仅接受已解包目录);查询参数路由(
?p=等仅登记不实现)。 - 公网认证、MFA 和会话体系。
- WordPress REST、SQL 直连和在线爬虫。
- 日志、Search Console、GA4 等完整 URL 发现来源。
- 差量同步、删除检测和人工冲突队列;本切片只建立未来所需的数据位。
- 第二主题和可视化 Block Editor。
- Redis/BullMQ;但 Job 状态必须先持久化。
- Nginx/SSH、OSS/CDN 正式部署。
- 生产发布锁、CDN 刷新和生产回滚。
- 公网表单服务。
- 完整多语言工作流;模型支持 Locale,本切片只验收
en。 - ClickHouse、Event Gateway、客户画像和增长分析。
2.4 能力边界声明
本切片的 URL 基线只来自:
WXR 可推导 URL
∪ WXR Attachment URL
∪ 本地提供的 Sitemap/Sitemap Index
∪ 人工补充 URL 清单(可选)
报告必须显示“当前已知基线”,不得使用“发现了全部历史 URL”的表述。日志、GSC、GA4、在线爬虫未接入的事实必须显示在报告首页。
3. 技术决策
| ADR | 决策 | 本切片选择 | 约束 |
|---|---|---|---|
| ADR-001 | ORM | Drizzle + PostgreSQL 16 | 所有 Schema 通过迁移文件管理;关键约束落数据库 |
| ADR-002 | 服务形态 | NestJS 模块化单体 + Standalone App Context + CLI | 本切片不监听公网端口;服务层未来由 HTTP Controller 复用 |
| ADR-003 | Job 执行 | 进程内 Runner + 数据库 Job 状态 | 不要求 BullMQ,但不能只存内存;进程中断后可识别 stale Job |
| ADR-004 | 原始输入 | Source Connection 与 Source Snapshot 分离 | 每个 WXR/Sitemap/已解包媒体目录形成不可变 Snapshot 和 Hash |
| ADR-005 | WXR 解析 | 流式 SAX 解析 | 禁止整文件正则解析;限制字节、节点、深度、字段长度和处理时间 |
| ADR-006 | 内容身份 | Source Record 稳定键 | WordPress ID 是来源身份;Permalink 只是 URL 证据,不作为稳定主键 |
| ADR-007 | URL 模型 | Exact Route Key 与 Collision Key 分离 | 实际路由不大小写折叠、不去尾斜杠;碰撞键只用于风险检查 |
| ADR-008 | 媒体 | 本地 Content-Addressed Storage | 本切片不用 MinIO;对象存储阶段可更换 Storage Adapter |
| ADR-009 | HTML 安全 | 白名单清洗,原文保留在 Source Snapshot | 清洗后内容进入 Revision;差异写报告 |
| ADR-010 | 发布模型 | current/published 双指针 | 导入不改变 published pointer;发布不修改 Revision |
| ADR-011 | 构建 | 冻结 Render Snapshot | Astro 禁止查数据库、禁止在线拉内容、禁止 Seed fallback |
| ADR-012 | 主题 | 最小 Theme Manifest + Render Contract | 完整 Block 协议后置,但第一主题不得依赖站点专属数据导入代码 |
| ADR-013 | 预览 | Artifact 自包含 | Preview 读取同一 Build 的 Route Manifest,不读取实时数据库 |
运行环境:
- Node.js 22 LTS
- pnpm Workspace
- TypeScript Strict
- PostgreSQL 16
- Astro
- NestJS
- Drizzle ORM
- Vitest
- HTML Parser/Sanitizer
- SAX XML Parser
依赖版本必须写入 Lockfile。使用新第三方库前检查许可证和维护状态。
4. 仓库结构
cms-one/
apps/
api/ # NestJS 模块化单体;本切片由 CLI 创建 standalone context
packages/
contracts/ # Source/Content/Route/Render/Report DTO
adapter-wordpress/ # WXR 流式解析与映射
route-engine/ # URL 建模、冲突检测、Route Manifest
validation-engine/ # Gate 规则与报告
theme-runtime/ # Render Snapshot 读取和公共渲染约定
ui-primitives/ # 最小无业务耦合 Astro 组件
themes/
pilot-minimal/
theme.manifest.ts
src/layouts/
src/components/
src/pages/
src/styles/
scripts/
register-source.ts
import.ts
publish.ts
build.ts
gate.ts
preview.ts
verify.ts
infra/
docker-compose.yml # 本切片仅 PostgreSQL
database/
storage/ # 全部 Git Ignore
incoming/ # 待登记的临时暂存输入(source:register 登记后复制进 snapshots/,可清理)
snapshots/
media/
quarantine/
sitemap/
artifacts/ # 全部 Git Ignore;本地完整证据
jobs/<job-id>/
input/
dist/
manifests/
reports/
logs/
tests/
fixtures/ # 脱敏最小夹具,可提交
contract/
unit/
integration/
acceptance/
docs/
adr/
specs/
runbooks/
evidence/ # 可提交的脱敏验收摘要
暂不移动根目录现有规划文档,避免在实现启动时同时制造链接和历史迁移噪音。文档整理另开独立提交。
.gitignore 已经存在,实施开始时只需验证以下路径确实被忽略:
storage/artifacts/.env*node_modules/dist/coverage/- 数据库本地卷
5. 核心领域模型
5.1 站点与配置
sites
id uuid PK
key text UNIQUE
name text
default_locale text
production_origin text
created_at timestamptz
updated_at timestamptz
production_origin 必须是明确的 https://host[:port],用于 Canonical、OG 和 Sitemap。路径不写入该字段。
site_config_revisions
id uuid PK
site_id FK
version integer
data jsonb
checksum text
created_at timestamptz
UNIQUE(site_id, version)
第一版配置至少包含:
- 公司名称和站点名称。
- 首页路径和主要列表路径。
- Logo、联系方式和 Footer 信息。
- 默认 SEO 信息。
- 主导航配置;如果 WXR 中存在可用菜单,可以导入后生成配置修订。
5.2 来源、快照与导入
source_connections
id uuid PK
site_id FK
kind enum(wxr)
name text
configuration jsonb
created_at timestamptz
Connection 只描述来源,不保存某次文件路径和 Hash。
source_snapshots
id uuid PK
site_id FK
source_connection_id FK
kind enum(wxr, sitemap, media_bundle, manual_url_list)
storage_path text
sha256 text
byte_size bigint
adapter_version text
captured_at timestamptz
metadata jsonb
created_at timestamptz
UNIQUE(site_id, kind, sha256)
Snapshot 创建后不可变。替换文件必须创建新 Snapshot。
import_jobs
id uuid PK
site_id FK
source_snapshot_id FK
status enum(queued, running, succeeded, succeeded_with_issues, failed, cancelled, stale)
mapping_version text
idempotency_key text UNIQUE
attempt integer
started_at / finished_at
error_code / error_summary
report_path text
created_at timestamptz
source_records
id uuid PK
site_id FK
source_connection_id FK
source_snapshot_id FK
import_job_id FK
source_type text
source_id text
locale text
source_url text
raw_location text
source_checksum text
normalized_checksum text
parse_status text
content_entry_id uuid nullable
UNIQUE(site_id, source_connection_id, source_type, source_id, locale, source_snapshot_id)
跨 Snapshot 的稳定映射键为:
site_id + source_connection_id + source_type + source_id + locale
5.3 内容与修订
content_entries
id uuid PK
site_id FK
type_key text
lifecycle_status enum(active, archived)
created_at / updated_at
INDEX(site_id, type_key)
不使用 Permalink 作为 Content Entry 的唯一身份。
entry_locales
id uuid PK
site_id FK
content_entry_id FK
locale text
slug text
current_revision_id uuid nullable
published_revision_id uuid nullable
created_at / updated_at
UNIQUE(site_id, content_entry_id, locale)
Slug 是否唯一由 Route Registry 决定,不单独假设所有内容类型共享同一 Slug 空间。
content_revisions
id uuid PK
site_id FK
content_entry_id FK
locale text
schema_version integer
data jsonb
checksum text
source_kind enum(import, manual, restore)
source_record_id uuid nullable
import_job_id uuid nullable
overridden_fields jsonb default []
created_by text
created_at timestamptz
约束:
- 只 INSERT,不提供 UPDATE/DELETE 业务接口。
- 数据库权限或 Trigger 阻止对既有 Revision 的更新。
current_revision_id指向最新工作修订。published_revision_id指向公开修订。- 两个指针不同表示“已发布但有未发布修改”。
- 构建只读取
published_revision_id。
5.4 分类和关系
taxonomies
id uuid PK
site_id FK
key text
object_type text
public_base_path text
UNIQUE(site_id, key, object_type)
terms
id uuid PK
site_id FK
taxonomy_id FK
source_id text
slug text
name text
parent_term_id uuid nullable
data jsonb
UNIQUE(site_id, taxonomy_id, source_id)
content_terms
site_id FK
content_entry_id FK
term_id FK
is_primary boolean
PRIMARY KEY(site_id, content_entry_id, term_id)
这些表支撑分类页、标签页、产品分类、分页和 Yoast 主分类,不允许临时塞进内容 JSON 后再重构。
5.5 媒体与使用关系
media_assets
id uuid PK
site_id FK
source_connection_id FK
source_id text nullable
source_url text
source_path text
storage_path text
public_path text
sha256 text
detected_mime text
declared_mime text nullable
byte_size bigint
width / height integer nullable
alt_text text nullable
status enum(ready, missing, quarantined, unsupported)
created_at timestamptz
UNIQUE(site_id, source_url)
UNIQUE(site_id, public_path)
media_usages
site_id FK
media_asset_id FK
content_revision_id FK
field_path text
source_url text
PRIMARY KEY(media_asset_id, content_revision_id, field_path)
媒体匹配顺序:
- 完整旧 URL。
wp-content/uploads后的完整相对路径。- 编码修复后的完整相对路径。
- 仅文件名模糊匹配只能生成候选,不得自动选中;多个候选进入冲突报告。
5.6 路由注册表
route_registry
id uuid PK
site_id FK
host text
source_path_raw text
route_path text
exact_route_key text
collision_key text
query_policy enum(ignore, exact, preserve, dynamic)
query_key text nullable
source_status text
target_status text
route_kind enum(html, asset, redirect, gone, dynamic, unresolved)
target_content_id uuid nullable
target_file text nullable
redirect_status integer nullable
redirect_location text nullable
indexable boolean
priority integer
source_snapshot_id uuid nullable
verification_status text
created_at / updated_at
UNIQUE(site_id, host, exact_route_key, query_key)
规则:
UNIQUE(site_id, host, exact_route_key, query_key)中query_key为 NULL 的纯路径路由,需以 PostgreSQLNULLS NOT DISTINCT(或哨兵值)实现数据库级唯一——写入 Schema 约束,避免迁移编写时才发现。source_path_raw永远保存来源原文。route_path是经过安全校验的实际公开路径。exact_route_key用于请求匹配,保留大小写和尾斜杠语义。collision_key仅用于检测 Windows/Linux、大小写、Unicode NFC 和尾斜杠风险,不用于实际路由。- Percent Encoding 只解码 RFC 非保留字符;
%2F、%5C等保留编码不改写。 - 拒绝 NUL、反斜杠、路径穿越、控制字符、Windows 保留设备名、尾部空格/点和超长路径。
- Fragment 不属于服务端路由,不写入 Route Key。
- 查询路由单独建模,不与 Path 静默合并。
5.7 Job、构建和验收
build_jobs
id uuid PK
site_id FK
status enum(queued, snapshotting, building, validating, succeeded, failed, cancelled, stale)
idempotency_key text UNIQUE
attempt integer
worker_id text nullable
started_at / heartbeat_at / finished_at
error_code / error_summary
created_at timestamptz
build_input_snapshots
id uuid PK
build_job_id FK UNIQUE
site_id FK
content_snapshot_hash text
route_snapshot_hash text
asset_manifest_hash text
site_config_revision_id FK
theme_key text
theme_version text
theme_source_hash text
dependency_lock_hash text
source_commit text
node_version text
pnpm_version text
astro_version text
locale text
timezone text
build_timestamp timestamptz
manifest_path text
created_at timestamptz
build_artifacts
id uuid PK
build_job_id FK
kind enum(dist, manifest, report, log)
storage_path text
sha256 text
byte_size bigint
created_at timestamptz
UNIQUE(build_job_id, kind, storage_path)
validation_runs / validation_issues
保存 Gate 的规则版本、开始/结束时间、结果和每条问题的严重级别、URL、证据、修复状态。
5.8 审计日志
audit_logs 为追加式,记录:
- site_id
- actor_type:system、cli_user
- actor_id/actor_label
- command_id
- action
- object_type/object_id
- before/after 摘要
- occurred_at
CLI 必须接受或生成明确 Actor,不使用模糊的固定 system/importer 代替所有操作。
6. URL 与页面生成策略
6.1 路径处置
| 来源 URL | 切片处置 | Route Kind |
|---|---|---|
| 内容 Permalink | 尽量按原路径输出 HTML | html |
| 首页 | 使用站点配置指定路径 / |
html |
| 产品/新闻主列表 | 使用站点配置的旧路径,生成分页 | html |
| 产品分类/文章分类 | 根据 Taxonomy/Term 生成列表和分页 | html |
| Tag/Product Tag | 默认 301 到对应主列表(本切片不生成 Tag 页;生成能力留待 SP3 后按需评估) | redirect |
| 日期/作者归档 | 默认 301 到新闻主列表,逐条登记 | redirect |
| Attachment 页面 | 确定性默认规则:出现在老站 Sitemap → html;仅 WXR 证据 → 301 到对应媒体文件(或父列表页);确认已删除 → 410。偏离默认的条目在报告中逐条列出 | html/redirect/gone |
| 上传媒体路径 | 按旧 wp-content/uploads/... 输出 |
asset |
| 无法保真的内容路径 | 人工映射;不得默认跳首页 | redirect/unresolved |
| 已确认永久删除 | 410 | gone |
?p=ID、?page_id=ID |
登记为 excluded_with_reason(报告单列记录在案);查询路由能力留待 SP3 实现 |
—(登记不实现) |
6.2 当前已知 URL 基线
每条来源 URL 必须归入:
verified_html
verified_asset
verified_redirect
verified_gone
unresolved
excluded_with_reason
unknown_source_gap
Gate 要求 unresolved=0。unknown_source_gap 不伪装成成功,必须在报告首页显示,但因本切片明确未接日志/GSC/爬虫,不单独阻止 L3 验收。
6.3 Redirect 约束
- Redirect 状态码只能是批准的 301/302;本切片默认 301。
- Location 必须可解析。
- 目标必须落到 HTML、Asset、Gone 或批准的外部 URL。
- Redirect 环直接失败。
- 任意多跳大于 1 均告警;超过配置上限直接失败。
- 禁止把无法映射的路径批量跳转到首页。
6.4 静态文件映射
示例:
/about-us/ → dist/about-us/index.html
/news/a.html → dist/news/a.html
/legacy/page.php → dist/legacy/page.php 或由 Route Manifest 映射
/wp-content/uploads/2024/a.jpg
→ dist/wp-content/uploads/2024/a.jpg
所有文件映射必须经过安全 Join,最终绝对路径必须位于该 Build 的 Dist 根目录内。
7. WXR 导入管道
7.1 输入登记
pnpm sf source:register \
--site hesupipe \
--wxr storage/incoming/hesupipe.xml \
--media storage/incoming/uploads \
--sitemap storage/incoming/sitemap.xml
登记过程:
- 验证文件位于允许目录。
- 计算 SHA-256、字节数和媒体清单。
- 将输入复制到 Content-Addressed Snapshot 目录。
- 创建
source_snapshots。 - 原输入之后不再被任务直接读取。
本切片媒体输入只接受已解包目录(自 cms 只读复制 storage/media/ 下的站点目录)。ZIP 包输入及其防护(Zip Slip、绝对路径、符号链接逃逸、压缩炸弹、数量/单文件/总解压限额、文件名碰撞)移出本切片,待后续支持 ZIP 上传的阶段实现。
7.2 流式解析
解析器限制通过配置明确:
- 最大 WXR 字节数。
- 最大 Item 数。
- 最大 XML 深度。
- 最大单字段和 CDATA 长度。
- 最大 Term、Meta、Attachment 数。
- 总处理时限。
- PHP Serialized 数据最大深度和元素数。
解析器不得解析外部实体或访问网络。
7.3 UniversalRow
最低契约:
interface UniversalRow {
sourceType: string
sourceId: string
status: string
locale: string
sourceUrl?: string
slug?: string
title?: string
excerpt?: string
bodyHtml?: string
publishedAt?: string
modifiedAt?: string
taxonomyRefs: Array<{ taxonomy: string; sourceTermId: string }>
attachmentRefs: Array<{ role: string; sourceId?: string; sourceUrl?: string }>
seo: Record<string, unknown>
knownFields: Record<string, unknown>
extras: Record<string, unknown>
rawLocation: string
}
契约版本变更必须提升 Schema Version 并保留转换测试。
7.4 HTML 清洗
处理顺序:
原始 HTML 保留在 Source Snapshot
→ WordPress Shortcode/主题字段适配
→ URL 和媒体引用提取
→ HTML 白名单清洗
→ 链接/媒体路径重写
→ 生成 Revision Data
默认删除或限制:
- script、object、embed。
- 事件属性。
javascript:、危险 Data URL。- 未批准 iframe。
- 外部追踪像素。
- 未知 Form。
清洗差异必须按内容记录到 Import Report。
7.5 Checksum 与幂等
Normalized Checksum 输入:
canonical_json({
universal_row_version,
adapter_version,
mapping_version,
sanitizer_version,
locale,
mapped_content,
taxonomy_relations,
media_reference_identities,
route_source_values
})
规则:
- JSON Key 稳定排序。
- 时间统一 UTC ISO 8601。
- 不包含 Import Job ID、处理时间等非业务字段。
- 相同 Snapshot 重跑不得产生新 Revision。
- 相同 Source Record 在新 Snapshot 中无业务变化时同样 Skip。
- 适配器、映射或清洗版本变化可显式触发重新标准化。
7.6 事务和失败
- Job 创建和 Snapshot 绑定在事务中完成。
- 每个 Source Record 的内容、Revision、分类关系和路由写入使用数据库事务。
- 单条失败不回滚整个 Import,但 Job 最终为
succeeded_with_issues。 - 结构损坏、Snapshot Hash 不符或数据库约束错误可使整个 Job 失败。
- 重跑使用新的 Import Job,但复用已有 Snapshot。
- 不支持的对象必须计数并输出原因,不得静默跳过。
7.7 导入报告
报告至少包含:
- Snapshot 和 Adapter/Mapping/Sanitizer 版本。
- 按 WP 类型和状态统计的输入基数。
- Created/Updated/Skipped/Failed/Unsupported。
- 内容修订数量。
- Taxonomy/Term/关系数量。
- 媒体 Ready/Missing/Quarantined/Unsupported/Ambiguous。
- HTML 清洗统计。
- URL Created/Conflict/Unresolved。
- 行级错误和 Raw Location。
- 未覆盖来源声明。
8. 显式发布模型
8.1 发布语义
导入只更新 entry_locales.current_revision_id。如果内容从未发布,published_revision_id 保持 NULL;如果已经发布过,旧 Published Revision 继续对构建可见。
发布只执行:
published_revision_id = current_revision_id
不得 UPDATE content_revisions。
8.2 CLI 安全要求
pnpm sf publish \
--site hesupipe \
--import-job <id> \
--actor <label> \
--dry-run
pnpm sf publish \
--site hesupipe \
--import-job <id> \
--actor <label> \
--confirm <preview-token>
发布前必须:
- 展示条目数、类型和现有/目标修订差异。
- 拒绝包含未解决 P0 Import Issue 的批次。
- 拒绝跨 Site 对象。
- 写 Audit Log。
- 重复发布同一 Revision 幂等。
本切片中的“发布”是内容修订批准,不是生产部署。
9. 最小主题与组件契约
9.1 Theme Manifest
themes/pilot-minimal/theme.manifest.ts 至少声明:
interface ThemeManifest {
key: 'pilot-minimal'
version: string
renderContractVersion: string
supportedContentTypes: string[]
requiredSiteConfig: string[]
templates: Array<{
key: string
appliesTo: string
}>
}
9.2 Render Snapshot
主题只接收冻结文件:
site.json
content.json
taxonomies.json
routes.json
assets.json
render-manifest.json
主题不得:
- 查询 PostgreSQL。
- 调用 Control API。
- 读取
storage/原始输入。 - 使用站点专属 Seed 数据。
- 在构建时访问网络。
9.3 第一批公共组件
- BaseLayout
- SeoHead
- SiteHeader
- SiteFooter
- Breadcrumbs
- RichText
- ResponsiveMedia
- ProductCard/ProductGrid
- NewsCard/NewsList
- TaxonomyList
- Pagination
- NotFoundPage
业务内容只能通过 Render Snapshot 进入组件,不允许组件内部硬编码 hesupipe 内容。
9.4 暂不实现的主题能力
- 可视化 Block Editor。
- 多主题切换。
- Slot 管理后台。
- Block Schema UI。
- Theme Token 在线编辑。
但 Theme Manifest、Render Contract 和公共组件边界必须在本切片建立,以避免第二主题时重写第一主题。
10. 构建输入和静态构建
10.1 Build Job 状态
queued
→ snapshotting
→ building
→ validating
→ succeeded
异常状态:failed、cancelled、stale。
进程启动时将超过阈值且无心跳的 Running Job 标记为 stale,不自动宣告成功。
10.2 冻结输入
Snapshot 物化过程在数据库一致性读取中完成:
- 读取站点配置 Revision。
- 读取所有 Published Revision 指针及内容。
- 读取 Taxonomy、Term 和关系。
- 读取 Route Registry 当前版本。
- 读取 Media Manifest 和 Usage。
- 写临时目录。
- 生成 Canonical JSON 和 Hash。
- 原子重命名为最终 Input 目录。
- 创建
build_input_snapshots记录。
10.3 Artifact 统一结构
artifacts/jobs/<build-job-id>/
input/
site.json
content.json
taxonomies.json
routes.json
assets.json
build-input.json
dist/
manifests/
build-manifest.json
route-manifest.json
asset-manifest.json
checksums.txt
reports/
validation-summary.json
validation-report.html
url-coverage.json
logs/
build.log
不再同时使用 artifacts/<buildId> 和 artifacts/jobs/<jobId> 两套路径。
10.4 构建要求
- 只读 Input 目录。
- 默认断网;至少通过测试证明构建不需要网络。
- 无 Seed fallback。
- 输入为空、Hash 不符或 Schema 不兼容立即失败。
- SITE Origin 来自冻结 Site Config。
- Sitemap 只包含
route_kind=html && indexable=true。 - Route/Asset/Build Manifest 与 Dist 同步生成。
- 构建日志不得包含敏感原始数据和凭据。
10.5 可复现性
对同一 Build Input 连续构建两次,必须满足:
- HTML、CSS、JS、媒体和 Route Manifest Hash 相同。
- 允许 Build Metadata 中时间、Job ID 不同,但不得进入业务文件内容。
- 排序、时区、随机数和生成时间固定或显式注入。
11. 媒体处理
11.1 安全校验
- 文件路径安全 Join。
- 魔数和实际解码结果校验。
- 声明 MIME 与检测 MIME 比对。
- 图片像素、字节和尺寸限制。
- SVG 默认 Quarantine,不进入 Dist。
- 可执行文件拒绝。
- 未支持附件类型登记为 Unsupported。
11.2 缺失分级
| 情况 | Import | Build | Gate |
|---|---|---|---|
| 内容引用没有 Media Asset 记录 | 允许完成并产生 P0 Issue | 允许生成候选报告,但 Build Job 最终失败 | 🔴 |
| Media Asset 已登记但本地文件缺失 | 允许完成并标 Missing | 允许生成候选报告,但不得标记 succeeded | 🔴 |
| SVG 被隔离且仍被公开内容引用 | 允许完成并标 Quarantined | 不复制 | 🔴 |
| 不影响公开内容的孤立缺失媒体 | 告警 | 可继续 | 🟡 |
为了获得完整诊断,可以生成候选 Dist;但只要存在公开引用的 P0 媒体问题,Build Job 最终状态必须是 failed,不能进入可发布状态。
11.3 旧路径输出
public_path 使用旧 URL Path。写文件前必须验证:
- 路径位于 Dist 根下。
- 与 HTML 路由无冲突。
- 大小写、NFC 和 Windows 文件名无冲突。
- 同一路径不同 Hash 不允许静默覆盖。
12. Route Manifest 与 Preview Server
12.1 Route Manifest
Route Manifest 是某个 Build Input 的不可变派生产物,至少包含:
manifest_version
site_id
build_job_id
origin
generated_from_route_snapshot_hash
routes[]
每个 Route 包含 Exact Key、Query Policy、Kind、目标文件、状态码、Location、Content-Type 和 Indexable。
12.2 Preview Server
pnpm sf preview --build <build-job-id> --host 127.0.0.1 --port 4173
约束:
- 只绑定
127.0.0.1,除非用户显式指定其他地址。 - 只读取指定 Job 的 Dist 和 Route Manifest。
- 不读取实时数据库。
- 防止
..、编码路径穿越、反斜杠和符号链接逃逸。 - 禁用目录列表。
- HTML/Asset 使用正确 Content-Type。
- Redirect 返回 Manifest 声明的状态和 Location。
- Gone 返回 410。
- 未命中返回自定义 404。
- 所有响应添加
X-Robots-Tag: noindex, nofollow。 - HTML 添加严格的预览 CSP,阻止未批准远程脚本。
- 自动测试完成后可以无交互停止。
13. 自动门禁
13.1 URL 与响应
门禁 CLI 分工:gate.ts 对 Artifacts 做静态检查(清单/文件/Hash/路径安全);verify.ts 启动 Preview 后对 Route Manifest 全部路由做真实 HTTP 验证(本节规则与 §14.5)。
- 当前已知 URL 基线
unresolved=0。 - 自动请求 Route Manifest 中全部路由,不以 20 条抽样代替全量检查。
- HTML、Asset、Redirect、Gone 状态符合 Manifest。
- Redirect Location 有效。
- Redirect 无环。
- 多跳大于 1 告警,超过配置上限失败。
- 404 测试路径返回 404 页面而不是 200。
13.2 页面 SEO
对所有 Indexable HTML:
- 恰好一个非空 Title。
- 一个 Meta Description;缺失为错误或按批准策略降级。
- 恰好一个 Canonical。
- Canonical Host 和 Path 与 Route 策略一致。
- 恰好一个 H1;明确豁免模板除外。
- 页面不是 Noindex。
对 Preview 响应:
- 必须有
X-Robots-Tag: noindex,但该 Header 不写入正式 Dist 的 SEO 内容判断。
13.3 Sitemap
- Sitemap URL 全部对应 Indexable HTML Route。
- 所有 Indexable HTML Route 全部进入 Sitemap。
- 无 Redirect、Gone、404 和 Asset。
- Origin 与冻结 Site Config 相同。
- URL 编码规则一致。
13.4 链接和资源
- 使用 HTML Parser,不使用单纯正则。
- 检查
href、src、srcset、Poster 和批准的 CSS URL。 - 正确处理 Query、Fragment、Base URL 和 HTML Entity。
- 内部链接必须命中 Route Manifest。
- 外部脚本、iframe、字体和图片列入远程依赖报告。
- 未批准远程脚本直接失败。
13.5 文件系统兼容
- Lowercase Collision。
- Unicode NFC Collision。
- 尾斜杠映射冲突。
- Windows 保留设备名。
- 尾部点和空格。
- 路径过长。
- 文件与目录同名冲突。
- 编码分隔符和路径穿越。
13.6 数据与构建完整性
- Build Input Hash 正确。
- Dist 文件都有 Checksums。
- Route Target File 存在。
- Asset Manifest 和实际文件一致。
- 公开引用媒体无 Missing/Quarantined。
- 相同 Input 的重复构建业务 Hash 一致。
- 损坏输入必须失败,禁止兜底。
13.7 报告
每次 Validation 输出:
- JSON 机器报告。
- HTML 人工报告。
- P0/P1/P2/P3 汇总。
- URL 和文件证据。
- 规则版本。
- Build Input/Dist/Manifest Hash。
完整报告保存在忽略目录或 CI Artifact;docs/evidence/ 只保存不含真实客户内容的验收摘要。
14. 测试计划
14.1 单元测试
WXR
- CDATA、Entity、Namespace。
- 超大字段、深度、节点数和时限。
_thumbnail_id。_product_image_gallery。wppop_sections。- Yoast Title/Description/主分类。
- Post/Page/Product/Attachment/Term/Nav Item。
- 未知 CPT 和未知 Meta。
.html、.php和多级 Permalink。- PHP Serialized 数据深度和长度限制。
URL
- 大小写不同路径。
- 有无尾斜杠。
- Unicode NFC/NFD。
- Percent Encoding。
%2F、%5C不被错误解码。- Query Policy。
- Windows 保留名称。
- 路径穿越和超长路径。
- Redirect 环、链和不存在目标。
媒体
- 魔数不匹配。
- SVG Quarantine。
- 相同文件不同路径。
- 相同路径不同 Hash。
- 同名文件多候选。
- Missing Asset 两个等级。
- (ZIP 防护测试随 ZIP 支持阶段补充;本切片媒体输入为目录。)
HTML 安全
- Script。
- 事件属性。
javascript:URL。- Data URL。
- iframe 白名单。
- 外部追踪像素。
- 清洗差异记录。
14.2 契约测试
- UniversalRow。
- Source Snapshot Manifest。
- Render Snapshot。
- Route Manifest。
- Theme Manifest。
- Import/Validation Report。
- 契约版本不兼容时构建失败。
14.3 数据库集成测试
- 从空数据库运行全部 Migration。
- Revision 不可更新。
- Source Snapshot 不可替换。
- Stable Source Identity 幂等。
- 第二次导入 0 新 Revision。
- Publish Pointer 幂等。
- 已发布 A、导入 B 后仍构建 A。
- 发布 B 后构建 B。
- Site A 不可读写 Site B。
- Route Exact Key 唯一。
- Media Public Path 唯一。
- Job 崩溃后标记 Stale。
14.4 构建集成测试
- Snapshot 物化。
- Astro 仅从 Snapshot 构建。
- 数据库/API 停止时仍可从已冻结 Snapshot 构建。
- 构建断网仍成功。
- 输入损坏立即失败。
- 无 Seed fallback。
- 相同 Snapshot 双构建 Hash 相同。
14.5 Preview 与验收测试
- 自动遍历 Route Manifest 全部路由。
- HTML 200。
- Redirect 301 和 Location。
- Gone 410。
- 不存在路径 404。
- Asset MIME 和 Hash。
- 查询路由未实现(
?p=类 URL 已登记为 excluded,预览不响应)。 - Noindex Header。
- 路径穿越返回 400/404。
人工抽检 20 条作为补充:
- 10 条内容页。
- 5 条分类/分页。
- 3 条 Redirect。
- 2 条 404/Gone。
14.6 真实数据验收
- 完整 hesupipe WXR。
- 完整授权媒体副本。
- 本地 Sitemap。
- 输入基数与 Import Report 对照。
- 运行时间、峰值内存和产物大小记录。
15. 实施阶段
以下周期按单开发者配合 AI、旧 cms 只读复用估算。以阶段验收为准,不以日期强行结束。
阶段 0:输入授权、备份和基线(1–2 天)
任务:
- 记录当前 Git Commit。
- 确认试点数据使用授权和敏感信息范围。
- 备份旧 cms PostgreSQL、WXR、媒体和关键 Artifact 到第二存储。
- 验证
storage/、artifacts/、.env*被 Git Ignore。 - 创建脱敏最小 WXR/媒体/Sitemap 夹具。
- 记录真实 WXR 的输入基数。
验收:
- 原项目未被修改。
- 备份有 Hash 和位置记录。
git status不出现真实试点数据。
阶段 1:Workspace、数据库和契约(3–5 天)
任务:
- 建立 pnpm Workspace、TypeScript、Vitest 和 CI。
- 建立 PostgreSQL Compose。
- 建立 Drizzle Migration。
- 实现 §5 核心表和数据库约束。
- 冻结 UniversalRow、Route Manifest、Render Snapshot、Theme Manifest。
- 写契约测试和空库迁移测试。
验收:
- 空环境一条命令启动数据库并迁移。
- 所有核心约束有自动测试。
- Revision/Snapshot 不可变测试通过。
阶段 2:Source Snapshot 与 WXR Adapter(4–7 天)
任务:
source:register。- Content-Addressed Snapshot。
- 流式 WXR 解析。
- UniversalRow 映射。
- Parser 限额。
- HTML 清洗。
- 未支持对象报告。
验收:
- 脱敏夹具全绿。
- 大文件不整文件读入内存。
- 恶意/畸形输入安全失败。
- 解析器不访问网络。
阶段 3:内容、分类、媒体和路由导入(5–8 天)
任务:
- Stable Source Identity。
- Content Entry/Revision 和 Current Pointer。
- Taxonomy/Term/Content Term。
- Media Registry/Usage 和安全校验。
- Route Registry Exact/Collision 模型。
- Import Job 和报告。
- 幂等重跑。
验收:
- 同 Snapshot 二次导入 0 新 Revision。
- Permalink 变化不创建重复 Content Entry。
- 媒体歧义不自动匹配。
- URL 冲突和不支持对象全部进入报告。
阶段 4:显式发布、Render Snapshot 和主题(5–8 天)
任务:
- Publish Dry Run/Confirm。
- Current/Published Pointer 状态测试。
- Build Input Snapshot。
- pilot-minimal Theme Manifest。
- 公共组件。
- Astro 内容、分类、分页、媒体和 SEO 页面。
- Sitemap/Robots。
验收:
- 导入不改变 Published 页面。
- 显式发布后新修订进入下次 Build。
- Theme 无数据库/API/Seed 依赖。
阶段 5:Gate、Preview 与可复现性(4–7 天)
任务:
- Route/Asset/Build Manifest。
- 全量 Validation Engine。
- Preview Server。
- 全路由 HTTP 测试。
- 相同输入双构建测试。
- JSON/HTML 报告。
验收:
- 全部自动 Gate 通过。
- Preview 只读 Artifact。
- 路径穿越和远程脚本测试通过。
- 双构建业务 Hash 一致。
阶段 6:真实试点 L3 验收(2–5 天)
任务:
- 注册真实 Snapshot。
- 全量 Import。
- 修复或显式排除问题。
- 再次 Import 验证幂等。
- 显式 Publish。
- Build + Gate + Preview。
- 全量路由自动验证和 20 条人工抽检。
- 输出脱敏验收摘要。
验收:
- §16 全部满足。
预计总周期:约 5–8 周(各阶段区间合计 24–42 个工作日)。真实数据异常、未知插件字段和媒体命名问题可能增加时间。
16. Definition of Done
切片只有同时满足以下条件才算完成。
16.1 输入与导入
- Source Connection、Source Snapshot、Import Job 语义分离。
- 真实输入有 Hash、大小、版本和授权记录。
- 范围内所有对象均有明确结果。
- 不支持对象有数量、类型和原因。
- 第二次相同输入导入 0 新 Revision。
- 导入不发布。
- HTML 清洗和媒体安全报告可审计。
16.2 内容与发布
- Revision 数据库级不可变。
- Current/Published 双指针正确。
- 已发布 A、导入 B 后公开仍为 A。
- Publish B 后构建读取 B。
- 发布操作可 Dry Run、显式确认并写审计。
16.3 URL 与媒体
- Exact Route Key 不粗暴合并大小写、尾斜杠和编码语义。
- Collision Key 正确报告跨平台风险。
- 当前已知 URL 基线
unresolved=0。 - 不允许无法映射路径默认跳首页。
- 公开引用媒体无 Missing/Quarantined。
- 查询路由未实现的事实(
?p=已登记 excluded)与未覆盖来源在报告中明确显示。
16.4 构建
- Build Input 不可变且有完整 Hash。
- Theme 只读 Render Snapshot。
- 构建无网络、无数据库、无 API、无 Seed。
- 相同 Input 两次构建业务 Hash 相同。
- Artifact 结构统一并包含完整 Manifest。
16.5 门禁和预览
- Route Manifest 全量 HTTP 自动验证通过。
- HTML/Redirect/Asset/Gone/404 行为正确。
- Dead Link=0。
- Sitemap 双向一致。
- Indexable HTML 的 Title、Description、Canonical、H1 通过。
- Redirect 无环,目标存在。
- 文件系统和路径安全检查通过。
- Preview 使用同一 Build 的 Manifest,绑定回环地址并携带 Noindex Header。
16.6 工程质量
pnpm lint、typecheck、test、build全绿。- 空数据库 Migration 测试通过。
- 没有真实数据、Secret、Storage、Artifact 被提交。
- 关键决策已有 ADR。
- 生成脱敏 L3 验收摘要。
17. 风险与控制
| 风险 | 等级 | 控制 |
|---|---|---|
| WXR 类型或字段超出范围 | P1 | Unsupported 清单、Extras、Adapter 夹具 |
| Permalink 变化造成重复内容 | P0 | Stable Source ID 与 Source Record 映射 |
| URL 规范化合并不同路径 | P0 | Exact Key 与 Collision Key 分离 |
| WXR HTML 执行恶意脚本 | P0 | 白名单清洗、CSP、负向测试 |
| 媒体模糊匹配错误 | P0 | 完整路径优先、多候选不自动选 |
| Windows 与 Linux 路径冲突 | P0 | Collision Gate、保留名称和 Unicode 检查 |
| 构建读到变化中数据 | P0 | 不可变 Build Input Snapshot |
| Seed/默认数据进入产物 | P0 | 无兜底代码、负向回归测试 |
| 进程中断造成假成功 | P1 | 持久化 Job、Heartbeat、Stale 状态 |
| 真实数据误提交 Git | P0 | Ignore、Pre-commit/CI Secret 和大文件检查 |
| 报告只存在本机后丢失 | P1 | CI Artifact/受控存储 + 脱敏 Evidence 摘要 |
| 主题再次硬编码站点数据 | P1 | Theme Manifest、Render Contract、组件边界 |
| L3 被误认为生产完成 | P0 | 报告显式标记未部署、未完成 L4 |
18. 切片后的实施顺序
SP1 管理后台 + 平台级 super_admin + MFA
SP2 差量同步、删除检测、人工冲突队列
SP3 HTML 爬虫/Playwright + 日志/GSC URL 补全 + SEO 三方 Diff
SP4 正式 Block/Template/Theme 协议 + 第二主题
SP5 Build Worker/BullMQ + Release/发布锁/原子部署/回滚
SP6 Form API + Lead + 通知重试 + 隐私
SP7 备份自动化、监控、告警和生产 L4 验收
说明:
- 手工备份、Source Snapshot 和本地 Evidence 保留从本切片开始执行。
- SP7 是生产级自动备份和监控,不代表此前允许无备份开发。
- 第一试点正式上线前,SP1、SP2、SP3、SP5 和 SP7 的上线阻断项必须完成。
19. 实施前最终确认项
- hesupipe WXR、媒体和 Sitemap 的准确位置及使用授权。
- 是否存在需要保留的 PDF、字体、视频或非图片附件。
- 试点 WXR 中实际 CPT、Taxonomy 和菜单类型清单。
- hesupipe 的生产 Origin 和旧 URL 是否区分大小写。
- 首页、产品列表和新闻列表的权威旧路径。
Tag、日期、作者、Attachment 页面使用生成、301 还是 410(已定默认:Tag/日期/作者 → 301 主列表;Attachment 页按 Sitemap/WXR 状态在 html/redirect/gone 中逐条处置——如需偏离默认请提出)。- 未支持 CPT 是否全部进入 Excluded,还是有必须纳入切片的类型。
- L3 Evidence 完整报告保存位置。
以上信息缺失时可以先完成框架、契约和脱敏夹具,但不得宣告真实试点 L3 验收完成。
20. 最终结论
本修订版将第一切片控制在可实施范围内,同时避免以下旧 cms 技术债再次进入 SiteForge:
- Permalink 充当内容身份。
- 导入后自动发布。
- 路由塞进 Settings JSON。
- 大小写和尾斜杠粗暴合并。
- 实时查 API 构建和 Seed 兜底。
- Preview 读取变化中的数据库。
- 媒体按文件名静默猜测。
- WordPress HTML 未清洗。
- 主题页面继续硬编码站点数据。
- Build/Import 状态只存在内存。
完成该切片后,项目将得到一个真正可复用的核心:
不可变来源快照
+ 幂等导入
+ 不可变内容修订
+ 显式发布指针
+ 精确 URL 注册表
+ 安全媒体清单
+ 冻结构建输入
+ 最小主题契约
+ 可复现 Astro 构建
+ 全量自动门禁
+ Artifact 自包含预览
这条主链达到 L3 后,再建设后台、差量同步、完整 SEO 扫描、第二主题和生产部署,不需要推翻第一切片的数据与契约。