# trade-pro 第 5 主题移植实施计划 > **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:** 把 cms Astro 主题移植为 SiteForge V8_10 第 5 套内置主题 trade-pro(英文外贸 B2B),平台适配 + 骨架跑通 + 完整模板 + 验收。 **Architecture:** 三步走——先做平台适配(主题发现单一源、宏覆盖机制、主题资产优先、6 处前端入口),再以骨架主题包在真实 hesupipe 站点(`483295c29ea0`,193 条内容)跑通构建,最后按 cms 设计源(`D:\project\cms\runtimes\astro-site-runtime`)逐模板移植视觉。设计依据:[spec R3](../specs/2026-10-22-trade-pro-theme-design.md)。 **Tech Stack:** Python 3.12 / FastAPI / Jinja2(FileSystemLoader 多目录)/ pytest / 原生 JS。 **环境事实(执行者必读):** - 工作目录:`C:\Users\30997\Downloads\SiteForge_Factory_V5_Web_Docker\SiteForge_Enterprise_V8_10`(下称 `$ROOT`,bash 里用正斜杠) - Docker 实例已运行:`http://127.0.0.1:8787`(容器 siteforge-v8-10-siteforge-1/-worker-1),admin 密码在 `.env` - 测试命令:`python -m pytest -q`(当前 210 项全绿,任何改动后必须保持);JS 检查 `node --check app/static/.js` - cms 设计源:`D:/project/cms/runtimes/astro-site-runtime/src`(tokens.css 250 行、base.css 445 行、components/*.astro、pages/[locale]/*.astro);像素基准 `dist/hesupipe/en/` - 改动 Python 后需 `docker compose up -d --build` 重建容器再验证 HTTP 层(构建约 1 分钟,有缓存) - 契约清单已固化于 spec §2(F1-F13),本计划直接引用 --- ## Phase 1+2:平台适配与骨架跑通(Task 1-8) ### Task 1: Git 基线 **Files:** 无新文件(初始化仓库) - [ ] **Step 1:** `cd $ROOT && git init && git add -A && git status --short | head -20` - [ ] **Step 2:** 确认 `.env`、`storage/`、`hesupipe_clean2.xml` 未被跟踪(.gitignore 已含 `.env`、`storage/`;`git check-ignore .env` 应有输出;`hesupipe_clean2.xml` 加入 .gitignore 一行) - [ ] **Step 3:** `git commit -m "chore: baseline V8.10.0-dev.1 with v8_workflows type-mapping fix and trade-pro spec"`(含 Co-Authored-By 行,下同) ### Task 2: 主题发现单一源(F1) **Files:** - Modify: `app/core.py:12` - Modify: `app/theme_pro.py:16-18` - Modify: `app/theme_store.py:14` - Test: `tests/test_theme_registry.py`(新建) - [ ] **Step 1: 写失败测试** ```python """Theme discovery must be a single source of truth.""" from app import core, theme_pro, theme_store def test_discover_returns_builtin_themes_with_manifests(): ids = core.discover_themes() for tid in ('automotive', 'industrial', 'corporate', 'minimal'): assert tid in ids import json for tid in ids: manifest = json.loads((core.ROOT / 'themes' / tid / 'manifest.json').read_text(encoding='utf-8')) assert manifest.get('id') == tid def test_three_whitelists_agree(): assert set(core.THEMES) == set(theme_pro.THEMES) == set(theme_store.BUILTIN) assert isinstance(core.THEMES, tuple) ``` 注:`core.ROOT` 若不存在,测试里用 `pathlib.Path(app.__file__).parent.parent`。 - [ ] **Step 2:** `python -m pytest tests/test_theme_registry.py -q` → 预期 FAIL(`discover_themes` 不存在) - [ ] **Step 3: 实现**——`app/core.py:12` 替换为: ```python ROOT_PATH = __import__('pathlib').Path(__file__).resolve().parent.parent def discover_themes(): """Single source of truth: scan themes/*/manifest.json (id must match folder).""" import json themes_dir = ROOT_PATH / 'themes' found = [] for folder in sorted(themes_dir.iterdir()): if not folder.is_dir() or folder.name.startswith('_'): continue manifest = folder / 'manifest.json' if not manifest.is_file(): continue try: if json.loads(manifest.read_text(encoding='utf-8')).get('id') == folder.name: found.append(folder.name) except (ValueError, UnicodeError): continue return tuple(found) THEMES = discover_themes() ``` (core.py 顶部已有 `import json`,可复用;实现时去掉重复 import,风格与文件一致。) `app/theme_pro.py:16-18` 改为: ```python from .core import sanitize, route_path, THEMES ``` (删除原第 18 行的元组定义。) `app/theme_store.py:14` 改为: ```python from .core import THEMES as BUILTIN ``` - [ ] **Step 4:** `python -m pytest tests/test_theme_registry.py -q` → PASS;`python -m pytest -q` 全绿 - [ ] **Step 5:** `git add -A && git commit -m "feat: single-source theme discovery replaces three hardcoded whitelists"` ### Task 3: theme_pro 每主题环境(宏覆盖机制,F4) **Files:** - Modify: `app/theme_pro.py:18-20`(LOADER 旁新增 env_for) - Modify: `app/theme_pro.py:190`(render 用 env_for) - Test: `tests/test_theme_registry.py`(追加) - [ ] **Step 1: 追加失败测试** ```python def test_env_for_theme_prefers_theme_dir_then_shared_root(): from app import theme_pro env = theme_pro.env_for('industrial') paths = [str(p) for p in env.loader.searchpaths] assert paths[0].replace('\\', '/').endswith('themes/industrial') assert paths[1].replace('\\', '/').endswith('themes') # 共享宏库必须仍可解析(回落路径) assert env.get_template('_professional_components.html') is not None def test_env_for_is_cached_per_theme(): from app import theme_pro assert theme_pro.env_for('industrial') is theme_pro.env_for('industrial') ``` - [ ] **Step 2:** 跑测试 → FAIL(env_for 不存在) - [ ] **Step 3: 实现**——`app/theme_pro.py` 在 LOADER 定义后追加: ```python _ENVS = {} def env_for(theme): """Per-theme Jinja env: theme dir first, shared themes/ root as fallback. Lets a theme override shared macros (_professional_components.html) while inheriting everything else. Cached: one env per theme across 5000-page builds. """ env = _ENVS.get(theme) if env is None: theme_dir = ROOT / 'themes' / theme search = [str(theme_dir), str(ROOT / 'themes')] if theme_dir.is_dir() else [str(ROOT / 'themes')] env = Environment(loader=FileSystemLoader(search), autoescape=select_autoescape(['html', 'xml'])) _ENVS[theme] = env return env ``` `render()` 末行(:190)改为: ```python return env_for(theme).get_template('pro.html').render(**ctx) ``` - [ ] **Step 4:** `python -m pytest -q` 全绿(现有 test_theme_pro_v3.py 即回归) - [ ] **Step 5:** commit `feat: per-theme Jinja env enables theme-level macro override` ### Task 4: builder 主题资产优先(F5) **Files:** - Modify: `app/builder.py:191` - Test: `tests/test_theme_registry.py`(追加) - [ ] **Step 1: 追加失败测试** ```python def test_theme_blocks_css_overrides_platform(tmp_path): from app import builder from pathlib import Path dist = tmp_path / 'assets'; dist.mkdir() theme_dir = tmp_path / 'theme'; theme_dir.mkdir() (theme_dir / 'blocks.css').write_text('/* theme version */', encoding='utf-8') builder.copy_blocks_css(theme_dir, dist) assert (dist / 'blocks.css').read_text(encoding='utf-8') == '/* theme version */' # 无主题版时回落平台版 dist2 = tmp_path / 'assets2'; dist2.mkdir() builder.copy_blocks_css(tmp_path / 'nonexistent', dist2) assert (dist2 / 'blocks.css').is_file() ``` - [ ] **Step 2:** 跑 → FAIL - [ ] **Step 3: 实现**——`app/builder.py:191` 的 `shutil.copy2(ROOT/'app'/'static'/'blocks.css',dist/'assets'/'blocks.css')` 替换为调用新函数,并在文件合适位置(output_path 附近)定义: ```python def copy_blocks_css(theme_dir, dist_assets): """Theme-provided blocks.css wins; platform default is the fallback.""" src = theme_dir / 'blocks.css' if (theme_dir / 'blocks.css').is_file() else ROOT / 'app' / 'static' / 'blocks.css' shutil.copy2(src, dist_assets / 'blocks.css') ``` - [ ] **Step 4:** `python -m pytest -q` 全绿;commit `feat: theme-level blocks.css takes priority over platform asset` ### Task 5: trade-pro 骨架主题包(Phase 2 核心) **Files:** - Create: `themes/trade-pro/manifest.json`、`site.html`、`pro.html`、`_professional_components.html`、`theme.css`、`blocks.css`、`pro.css`、`page-presets.json`、`pages/`(14 个 html)、`visual/.gitkeep`、`versions/.gitkeep` - Test: `tests/test_theme_registry.py`(追加) - [ ] **Step 1: 追加失败测试** ```python def test_trade_pro_registered_and_compatible(): assert 'trade-pro' in core.THEMES result = theme_store.compatibility('trade-pro') assert result['valid'], result['errors'] assert not result['errors'] def test_trade_pro_builds_real_site(): # 集成冒烟:真实构建函数对演示站点渲染 home 不抛异常(复用现有测试夹具方式, # 参考 tests/test_theme_pro_v3.py 中的最小站点夹具) ... ``` (第二个测试复用 test_theme_pro_v3.py 已有的最小站点构建夹具写法,theme 传 'trade-pro'。) - [ ] **Step 2:** 跑 → FAIL - [ ] **Step 3: 创建文件** `manifest.json`: ```json { "id": "trade-pro", "name": "Trade Pro — International B2B", "description": "English-first international B2B theme ported from the hesupipe Astro design: SVG icon set, responsive trade catalogue, product specs and inquiry CTA.", "version": "1.0.0", "style": "international-b2b", "entry": "pro.html", "styles": ["theme.css", "blocks.css"], "page_templates": ["home","about","contact","listing","product","article","case","standard","category"], "blocks": ["hero","richtext","features","stats","collection","image_text","gallery","faq","cta","attributes","spacer","layout","carousel","video","tabs","pricing","map","form_link"], "responsive": true, "auto_binding": true, "professional_layout": true, "commercial_ui": true, "accent": "#0b3d91" } ``` `site.html`:以 `themes/industrial/site.html` 为底,改 `lang="zh-CN"` → `lang="en"`,其余变量占位不动(它只包 {{content}})。 `_professional_components.html`:先做**透传骨架**(从共享库复制 card/cover 等宏,Task 10 起逐个换成 cms 设计 + SVG 图标)。 `pro.html`:以 industrial 的 pro.html 为底最小化:`lang="en"`;页眉/页脚消费 site_design chrome 但按 spec 4.2.5 值比对输出英文兜底(中文默认值视为未配置);正文区渲染 page content 与 custom_blocks;按钮类名一律 `sf-tp-*`。 `theme.css`:先放 tokens 骨架(Task 9 全量移植): ```css :root{--sf-tp-primary:#0b3d91;--sf-tp-accent:#f5a623;--sf-tp-bg:#ffffff;--sf-tp-text:#1a2332;--sf-tp-muted:#5a6a7e;--sf-tp-radius:8px;--sf-tp-font:'Inter',system-ui,-apple-system,sans-serif} ``` `blocks.css`:空文件 + 注释 `/* trade-pro component styles: filled in Phase 3 */`。 `pro.css`:空文件(兼容性硬性要求,spec F8)。 `page-presets.json`:**复制 industrial 的同名文件原样**(Phase 4 精修;13 种 key 齐全即可)。 `pages/`:`home.html` 写最小可用骨架(hero + showcase 列表 + cta 引用共享宏);其余 13 个(about/contact/listing/product/brand/article/case/service/faq/category/search/standard/notfound)先各写薄变体(复用 home 的结构、引用同样宏),文件必须存在(spec F7 硬性)。 - [ ] **Step 4:** `python -m pytest -q` 全绿(含新测试) - [ ] **Step 5:** `docker compose up -d --build` 后:`curl -s http://127.0.0.1:8787/api/health` 版本不变、`curl -s "http://127.0.0.1:8787/api/factory/themes/trade-pro/compatibility"`(带登录 cookie)返回 valid=true - [ ] **Step 6:** commit `feat: trade-pro skeleton theme package (Phase 2)` ### Task 6: theme_color 映射 + 6 处前端入口(F3/F11) **Files:** - Modify: `app/theme_pro.py`(theme_color 字典,make_context 内,约 :171) - Modify: `app/templates/admin.html:13`(siteTheme select) - Modify: `app/templates/workspace.html:41`(建站模态框 theme select) - Modify: `app/templates/theme-gallery.html:4`(theme select) - Modify: `app/static/workspace.js:5`(themeName) - Modify: `app/static/admin.js:5`(cfg) - Modify: `app/static/factory-v2.js:30`(schemes) - Test: `tests/test_theme_registry.py`(追加) - [ ] **Step 1: 追加失败测试** ```python def test_frontend_entries_include_trade_pro(): import pathlib base = pathlib.Path(__file__).resolve().parent.parent for rel, needle in [ ('app/templates/admin.html', 'value="trade-pro"'), ('app/templates/workspace.html', 'value="trade-pro"'), ('app/templates/theme-gallery.html', 'value="trade-pro"'), ('app/static/workspace.js', "'trade-pro'"), ('app/static/admin.js', "'trade-pro'"), ('app/static/factory-v2.js', "'trade-pro'"), ]: assert needle in (base / rel).read_text(encoding='utf-8'), rel def test_theme_color_map_covers_trade_pro(): # theme_pro.make_context 的 theme_color 字典含 trade-pro(渲染 smoke 断言 meta theme-color) ... ``` - [ ] **Step 2:** 跑 → FAIL - [ ] **Step 3: 实现**——逐处追加(保持原文风格): - theme_color 字典追加 `'trade-pro':'#0b3d91'` - 三个 HTML select 各追加:``(theme-gallery 用 `Trade Pro · International B2B`) - `workspace.js` themeName 追加 `'trade-pro':'Trade Pro'` - `admin.js` cfg 追加 `trade-pro:{label:'Trade Pro',desc:'英文外贸 · B2B 目录与询盘',prev:'corp',title:'Go global.'}` - `factory-v2.js` schemes 追加 `'trade-pro':['#0b3d91','#e8f1fb','GO GLOBAL']` - [ ] **Step 4:** `python -m pytest -q` 全绿 + `for f in app/static/*.js; do node --check "$f"; done` 通过 - [ ] **Step 5:** commit `feat: register trade-pro in theme color map and all six frontend entry points` ### Task 7: E2E 验收——真实站点切主题构建(Phase 2 门槛) **Files:** 无代码(验证 + 记录) - [ ] **Step 1:** 登录拿 cookie/CSRF(参照已验证流程:POST /api/auth/login → GET /api/auth/me) - [ ] **Step 2:** `PUT /api/factory/sites/483295c29ea0/theme` body `{"theme":"trade-pro"}`(确认返回成功) - [ ] **Step 3:** `POST /api/sites/483295c29ea0/builds`(Idempotency-Key + CSRF)→ 轮询 job 至 done - [ ] **Step 4:** 断言:pages ≥203、errors 空;预览首页 HTTP 200、含 `lang="en"`、不含"首页/联系我们"(平台中文默认,spec F9 验收);`/products/`、`/product/alloy-resin-system-window/` 200 - [ ] **Step 5:** 断言 6 入口:新工作台/`/classic` 建站表单、theme-gallery、factory-studio 主题卡里均出现 trade-pro(浏览器人工点验,截图存 `docs/superpowers/` 或记录于验收文件) - [ ] **Step 6:** 把结果写入 `docs/superpowers/specs/2026-10-22-trade-pro-theme-design.md` 附录"Phase 2 验收记录";commit --- ## Phase 3:完整视觉移植(Task 8-12) > 以下任务的代码无法在计划里全文预写(从 cms 源逐段移植),但每步给出**精确源文件、提取规则与验收断言**。共用纪律:每完成一个 Task 跑全量 pytest + commit。 ### Task 8: 设计令牌与基础样式全量移植 - 源:`D:/project/cms/runtimes/astro-site-runtime/src/styles/tokens.css`(250 行)+ `base.css`(445 行) - [ ] 变量重命名加 `--sf-tp-` 前缀(spec 4.2.6);按钮/交互元素全部 `sf-tp-` 类名(避开平台 `.btn/.primary/.request/.button/.nav-btn`) - [ ] 验收断言:`themes/trade-pro/theme.css` 覆盖 tokens 全集(颜色/字体/圆角/间距);构建产物 `assets/theme.css` 含 `--sf-tp-` 变量;平台 custom.css 的 `.btn{...}` 与主题按钮类无交集(grep 断言) - [ ] pytest(现有快照不受影响)+ commit ### Task 9: 图标宏库(替代箭头) - 源:cms 组件与 dist HTML 中的内联 SVG(`grep -o ']*>' dist/hesupipe/en/index.html` 起逐个提取;参考 `src/components/*.astro` 里的图标用法) - [ ] `themes/trade-pro/_professional_components.html` 定义 `icon(name)` 宏:内联 SVG 集合(≥8 个:arrow-right/check/phone/mail/location/whatsapp/download/quote 等,从 cms 实际用到的集合提取) - [ ] 验收:home/product 页渲染输出含 `