255b166513
- V2 规格取代 V1(V1 文件头标注作废与原因) - 修订①:§1.2 列明资产移植白名单与重写边界 - 修订②:Tag/Product Tag 默认 301 主列表(不生成 Tag 页) - 修订③:ZIP 输入移出切片(媒体只接受已解包目录) - 修订④:?p= 查询路由降级为 excluded 登记,SP3 再实现 Co-Authored-By: Claude Code <noreply@anthropic.com>
1540 lines
44 KiB
Markdown
1540 lines
44 KiB
Markdown
# 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 和媒体副本打通一条可重复、可审计、可继续演进的正式主链路:
|
||
|
||
```text
|
||
授权与输入登记
|
||
→ 原始快照
|
||
→ WXR 流式解析
|
||
→ 内容/分类/媒体标准化
|
||
→ 草稿修订
|
||
→ URL 注册表
|
||
→ 显式发布修订
|
||
→ 冻结构建输入
|
||
→ 极简主题静态构建
|
||
→ 全量门禁
|
||
→ 基于同一 Manifest 的本地预览
|
||
```
|
||
|
||
本切片的目标不是尽快拼出一个演示站,而是建立后续后台、主题、差量迁移、部署和回滚都能继续使用的领域模型与契约。
|
||
|
||
### 1.1 权威关系
|
||
|
||
1. `SiteForge_完整项目计划方案_V3.0.md` 是产品范围和总体架构的唯一基线。
|
||
2. 本文是第一个纵向切片的设计与实施基线。
|
||
3. `cms现状与SiteForge差距分析_V2.0.md` 说明旧代码的真实成熟度和风险。
|
||
4. 旧 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_article`
|
||
- `page` → `basic_page`
|
||
- `product` CPT → `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 基线只来自:
|
||
|
||
```text
|
||
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. 仓库结构
|
||
|
||
```text
|
||
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
|
||
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
|
||
|
||
```text
|
||
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
|
||
|
||
```text
|
||
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
|
||
|
||
```text
|
||
id uuid PK
|
||
site_id FK
|
||
kind enum(wxr)
|
||
name text
|
||
configuration jsonb
|
||
created_at timestamptz
|
||
```
|
||
|
||
Connection 只描述来源,不保存某次文件路径和 Hash。
|
||
|
||
#### source_snapshots
|
||
|
||
```text
|
||
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
|
||
|
||
```text
|
||
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
|
||
|
||
```text
|
||
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 的稳定映射键为:
|
||
|
||
```text
|
||
site_id + source_connection_id + source_type + source_id + locale
|
||
```
|
||
|
||
### 5.3 内容与修订
|
||
|
||
#### content_entries
|
||
|
||
```text
|
||
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
|
||
|
||
```text
|
||
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
|
||
|
||
```text
|
||
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
|
||
|
||
```text
|
||
id uuid PK
|
||
site_id FK
|
||
key text
|
||
object_type text
|
||
public_base_path text
|
||
UNIQUE(site_id, key, object_type)
|
||
```
|
||
|
||
#### terms
|
||
|
||
```text
|
||
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
|
||
|
||
```text
|
||
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
|
||
|
||
```text
|
||
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
|
||
|
||
```text
|
||
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)
|
||
```
|
||
|
||
媒体匹配顺序:
|
||
|
||
1. 完整旧 URL。
|
||
2. `wp-content/uploads` 后的完整相对路径。
|
||
3. 编码修复后的完整相对路径。
|
||
4. 仅文件名模糊匹配只能生成候选,不得自动选中;多个候选进入冲突报告。
|
||
|
||
### 5.6 路由注册表
|
||
|
||
#### route_registry
|
||
|
||
```text
|
||
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)
|
||
```
|
||
|
||
规则:
|
||
|
||
- `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
|
||
|
||
```text
|
||
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
|
||
|
||
```text
|
||
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
|
||
|
||
```text
|
||
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/WXR 状态决定 HTML、Redirect 或 Gone | html/redirect/gone |
|
||
| 上传媒体路径 | 按旧 `wp-content/uploads/...` 输出 | asset |
|
||
| 无法保真的内容路径 | 人工映射;不得默认跳首页 | redirect/unresolved |
|
||
| 已确认永久删除 | 410 | gone |
|
||
| `?p=ID`、`?page_id=ID` | 登记为 `excluded_with_reason`(报告单列记录在案);查询路由能力留待 SP3 实现 | —(登记不实现) |
|
||
|
||
### 6.2 当前已知 URL 基线
|
||
|
||
每条来源 URL 必须归入:
|
||
|
||
```text
|
||
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 静态文件映射
|
||
|
||
示例:
|
||
|
||
```text
|
||
/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 输入登记
|
||
|
||
```bash
|
||
pnpm sf source:register \
|
||
--site hesupipe \
|
||
--wxr storage/incoming/hesupipe.xml \
|
||
--media storage/incoming/uploads \
|
||
--sitemap storage/incoming/sitemap.xml
|
||
```
|
||
|
||
登记过程:
|
||
|
||
1. 验证文件位于允许目录。
|
||
2. 计算 SHA-256、字节数和媒体清单。
|
||
3. 将输入复制到 Content-Addressed Snapshot 目录。
|
||
4. 创建 `source_snapshots`。
|
||
5. 原输入之后不再被任务直接读取。
|
||
|
||
本切片媒体输入**只接受已解包目录**(自 cms 只读复制 `storage/media/` 下的站点目录)。ZIP 包输入及其防护(Zip Slip、绝对路径、符号链接逃逸、压缩炸弹、数量/单文件/总解压限额、文件名碰撞)**移出本切片**,待后续支持 ZIP 上传的阶段实现。
|
||
|
||
### 7.2 流式解析
|
||
|
||
解析器限制通过配置明确:
|
||
|
||
- 最大 WXR 字节数。
|
||
- 最大 Item 数。
|
||
- 最大 XML 深度。
|
||
- 最大单字段和 CDATA 长度。
|
||
- 最大 Term、Meta、Attachment 数。
|
||
- 总处理时限。
|
||
- PHP Serialized 数据最大深度和元素数。
|
||
|
||
解析器不得解析外部实体或访问网络。
|
||
|
||
### 7.3 UniversalRow
|
||
|
||
最低契约:
|
||
|
||
```ts
|
||
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 清洗
|
||
|
||
处理顺序:
|
||
|
||
```text
|
||
原始 HTML 保留在 Source Snapshot
|
||
→ WordPress Shortcode/主题字段适配
|
||
→ URL 和媒体引用提取
|
||
→ HTML 白名单清洗
|
||
→ 链接/媒体路径重写
|
||
→ 生成 Revision Data
|
||
```
|
||
|
||
默认删除或限制:
|
||
|
||
- script、object、embed。
|
||
- 事件属性。
|
||
- `javascript:`、危险 Data URL。
|
||
- 未批准 iframe。
|
||
- 外部追踪像素。
|
||
- 未知 Form。
|
||
|
||
清洗差异必须按内容记录到 Import Report。
|
||
|
||
### 7.5 Checksum 与幂等
|
||
|
||
Normalized Checksum 输入:
|
||
|
||
```text
|
||
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 继续对构建可见。
|
||
|
||
发布只执行:
|
||
|
||
```text
|
||
published_revision_id = current_revision_id
|
||
```
|
||
|
||
不得 UPDATE `content_revisions`。
|
||
|
||
### 8.2 CLI 安全要求
|
||
|
||
```bash
|
||
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` 至少声明:
|
||
|
||
```ts
|
||
interface ThemeManifest {
|
||
key: 'pilot-minimal'
|
||
version: string
|
||
renderContractVersion: string
|
||
supportedContentTypes: string[]
|
||
requiredSiteConfig: string[]
|
||
templates: Array<{
|
||
key: string
|
||
appliesTo: string
|
||
}>
|
||
}
|
||
```
|
||
|
||
### 9.2 Render Snapshot
|
||
|
||
主题只接收冻结文件:
|
||
|
||
```text
|
||
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 状态
|
||
|
||
```text
|
||
queued
|
||
→ snapshotting
|
||
→ building
|
||
→ validating
|
||
→ succeeded
|
||
```
|
||
|
||
异常状态:`failed`、`cancelled`、`stale`。
|
||
|
||
进程启动时将超过阈值且无心跳的 Running Job 标记为 stale,不自动宣告成功。
|
||
|
||
### 10.2 冻结输入
|
||
|
||
Snapshot 物化过程在数据库一致性读取中完成:
|
||
|
||
1. 读取站点配置 Revision。
|
||
2. 读取所有 Published Revision 指针及内容。
|
||
3. 读取 Taxonomy、Term 和关系。
|
||
4. 读取 Route Registry 当前版本。
|
||
5. 读取 Media Manifest 和 Usage。
|
||
6. 写临时目录。
|
||
7. 生成 Canonical JSON 和 Hash。
|
||
8. 原子重命名为最终 Input 目录。
|
||
9. 创建 `build_input_snapshots` 记录。
|
||
|
||
### 10.3 Artifact 统一结构
|
||
|
||
```text
|
||
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 的不可变派生产物,至少包含:
|
||
|
||
```text
|
||
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
|
||
|
||
```bash
|
||
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 与响应
|
||
|
||
- 当前已知 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 天)
|
||
|
||
任务:
|
||
|
||
1. 记录当前 Git Commit。
|
||
2. 确认试点数据使用授权和敏感信息范围。
|
||
3. 备份旧 cms PostgreSQL、WXR、媒体和关键 Artifact 到第二存储。
|
||
4. 验证 `storage/`、`artifacts/`、`.env*` 被 Git Ignore。
|
||
5. 创建脱敏最小 WXR/媒体/Sitemap 夹具。
|
||
6. 记录真实 WXR 的输入基数。
|
||
|
||
验收:
|
||
|
||
- 原项目未被修改。
|
||
- 备份有 Hash 和位置记录。
|
||
- `git status` 不出现真实试点数据。
|
||
|
||
### 阶段 1:Workspace、数据库和契约(3–5 天)
|
||
|
||
任务:
|
||
|
||
1. 建立 pnpm Workspace、TypeScript、Vitest 和 CI。
|
||
2. 建立 PostgreSQL Compose。
|
||
3. 建立 Drizzle Migration。
|
||
4. 实现 §5 核心表和数据库约束。
|
||
5. 冻结 UniversalRow、Route Manifest、Render Snapshot、Theme Manifest。
|
||
6. 写契约测试和空库迁移测试。
|
||
|
||
验收:
|
||
|
||
- 空环境一条命令启动数据库并迁移。
|
||
- 所有核心约束有自动测试。
|
||
- Revision/Snapshot 不可变测试通过。
|
||
|
||
### 阶段 2:Source Snapshot 与 WXR Adapter(4–7 天)
|
||
|
||
任务:
|
||
|
||
1. `source:register`。
|
||
2. Content-Addressed Snapshot。
|
||
3. 流式 WXR 解析。
|
||
4. UniversalRow 映射。
|
||
5. Parser 限额。
|
||
6. HTML 清洗。
|
||
7. 未支持对象报告。
|
||
|
||
验收:
|
||
|
||
- 脱敏夹具全绿。
|
||
- 大文件不整文件读入内存。
|
||
- 恶意/畸形输入安全失败。
|
||
- 解析器不访问网络。
|
||
|
||
### 阶段 3:内容、分类、媒体和路由导入(5–8 天)
|
||
|
||
任务:
|
||
|
||
1. Stable Source Identity。
|
||
2. Content Entry/Revision 和 Current Pointer。
|
||
3. Taxonomy/Term/Content Term。
|
||
4. Media Registry/Usage 和安全校验。
|
||
5. Route Registry Exact/Collision 模型。
|
||
6. Import Job 和报告。
|
||
7. 幂等重跑。
|
||
|
||
验收:
|
||
|
||
- 同 Snapshot 二次导入 0 新 Revision。
|
||
- Permalink 变化不创建重复 Content Entry。
|
||
- 媒体歧义不自动匹配。
|
||
- URL 冲突和不支持对象全部进入报告。
|
||
|
||
### 阶段 4:显式发布、Render Snapshot 和主题(5–8 天)
|
||
|
||
任务:
|
||
|
||
1. Publish Dry Run/Confirm。
|
||
2. Current/Published Pointer 状态测试。
|
||
3. Build Input Snapshot。
|
||
4. pilot-minimal Theme Manifest。
|
||
5. 公共组件。
|
||
6. Astro 内容、分类、分页、媒体和 SEO 页面。
|
||
7. Sitemap/Robots。
|
||
|
||
验收:
|
||
|
||
- 导入不改变 Published 页面。
|
||
- 显式发布后新修订进入下次 Build。
|
||
- Theme 无数据库/API/Seed 依赖。
|
||
|
||
### 阶段 5:Gate、Preview 与可复现性(4–7 天)
|
||
|
||
任务:
|
||
|
||
1. Route/Asset/Build Manifest。
|
||
2. 全量 Validation Engine。
|
||
3. Preview Server。
|
||
4. 全路由 HTTP 测试。
|
||
5. 相同输入双构建测试。
|
||
6. JSON/HTML 报告。
|
||
|
||
验收:
|
||
|
||
- 全部自动 Gate 通过。
|
||
- Preview 只读 Artifact。
|
||
- 路径穿越和远程脚本测试通过。
|
||
- 双构建业务 Hash 一致。
|
||
|
||
### 阶段 6:真实试点 L3 验收(2–5 天)
|
||
|
||
任务:
|
||
|
||
1. 注册真实 Snapshot。
|
||
2. 全量 Import。
|
||
3. 修复或显式排除问题。
|
||
4. 再次 Import 验证幂等。
|
||
5. 显式 Publish。
|
||
6. Build + Gate + Preview。
|
||
7. 全量路由自动验证和 20 条人工抽检。
|
||
8. 输出脱敏验收摘要。
|
||
|
||
验收:
|
||
|
||
- §16 全部满足。
|
||
|
||
预计总周期:约 4–6 周。真实数据异常、未知插件字段和媒体命名问题可能增加时间。
|
||
|
||
---
|
||
|
||
## 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. 切片后的实施顺序
|
||
|
||
```text
|
||
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. 实施前最终确认项
|
||
|
||
1. hesupipe WXR、媒体和 Sitemap 的准确位置及使用授权。
|
||
2. 是否存在需要保留的 PDF、字体、视频或非图片附件。
|
||
3. 试点 WXR 中实际 CPT、Taxonomy 和菜单类型清单。
|
||
4. hesupipe 的生产 Origin 和旧 URL 是否区分大小写。
|
||
5. 首页、产品列表和新闻列表的权威旧路径。
|
||
6. ~~Tag、日期、作者、Attachment 页面使用生成、301 还是 410~~(已定默认:Tag/日期/作者 → 301 主列表;Attachment 页按 Sitemap/WXR 状态在 html/redirect/gone 中逐条处置——如需偏离默认请提出)。
|
||
7. 未支持 CPT 是否全部进入 Excluded,还是有必须纳入切片的类型。
|
||
8. L3 Evidence 完整报告保存位置。
|
||
|
||
以上信息缺失时可以先完成框架、契约和脱敏夹具,但不得宣告真实试点 L3 验收完成。
|
||
|
||
---
|
||
|
||
## 20. 最终结论
|
||
|
||
本修订版将第一切片控制在可实施范围内,同时避免以下旧 cms 技术债再次进入 SiteForge:
|
||
|
||
- Permalink 充当内容身份。
|
||
- 导入后自动发布。
|
||
- 路由塞进 Settings JSON。
|
||
- 大小写和尾斜杠粗暴合并。
|
||
- 实时查 API 构建和 Seed 兜底。
|
||
- Preview 读取变化中的数据库。
|
||
- 媒体按文件名静默猜测。
|
||
- WordPress HTML 未清洗。
|
||
- 主题页面继续硬编码站点数据。
|
||
- Build/Import 状态只存在内存。
|
||
|
||
完成该切片后,项目将得到一个真正可复用的核心:
|
||
|
||
```text
|
||
不可变来源快照
|
||
+ 幂等导入
|
||
+ 不可变内容修订
|
||
+ 显式发布指针
|
||
+ 精确 URL 注册表
|
||
+ 安全媒体清单
|
||
+ 冻结构建输入
|
||
+ 最小主题契约
|
||
+ 可复现 Astro 构建
|
||
+ 全量自动门禁
|
||
+ Artifact 自包含预览
|
||
```
|
||
|
||
这条主链达到 L3 后,再建设后台、差量同步、完整 SEO 扫描、第二主题和生产部署,不需要推翻第一切片的数据与契约。
|