# 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 incoming/ # 待登记的临时暂存输入(source:register 登记后复制进 snapshots/,可清理) snapshots/ media/ quarantine/ sitemap/ artifacts/ # 全部 Git Ignore;本地完整证据 jobs// 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) ``` 规则: - `UNIQUE(site_id, host, exact_route_key, query_key)` 中 `query_key` 为 NULL 的纯路径路由,需以 PostgreSQL `NULLS 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 ```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 → 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 必须归入: ```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 knownFields: Record extras: Record 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 \ --actor