Repository Wiki
Brandon030722/ark-ui-skill

宣传素材与生成管线

Ark UI Skill 内置一组由真实样例界面编排而成的社交宣传图(横版 1600×900 / 竖版 1080×1350 共 8 张 PNG),以及一条用 Node 驱动无头 Edge 浏览器把它们重新渲染出来的生成管线。本页覆盖这条管线的完整链路:可编辑源文件、参数化页面、无头截图脚本与产物清单。

Purpose and Scope

本页是宣传素材子系统的完整参考,覆盖:

  • assets/promo/ 下的三个可编辑源文件(promo.html / promo.js / promo.css)如何组成一个"按查询参数切换画面"的单页宣传画布;
  • scripts/capture-promos.mjs 如何用无头 Microsoft Edge 逐张导出 8 张 PNG;
  • 生成产物的命名、尺寸、对应场景,以及环境变量 ARK_UI_EDGE_BIN 等运行配置。

以下内容有意留给兄弟页面:

  • 样例界面本身的实现(五个风格族样例、四档深度控制),见 5-assets-and-examples 下的 showcase 相关页面;
  • 样例截图是如何被采集出来的(桌面/移动视口、横向溢出校验),由 scripts/capture-showcases.mjs 负责,见 showcase 截图采集相关页面;
  • README/SKILL 层面的整体资产目录说明。

Overview

宣传素材解决的问题是:用同一套真实证据(showcase 截图)讲清楚 Ark UI Skill 的三个核心卖点——五种风格族、四档应用深度、经过验证的响应式重构。因此它不是一个静态图片目录,而是一条可重复执行的管线:

  1. 素材来源:五个复杂档(complex)样例的桌面截图与移动截图,位于 assets/showcases/screenshots/;
  2. 编排层:assets/promo/promo.html 定义四个宣传场景卡片(cover / families / depth / responsive),把上述截图作为"产品证据"嵌入版面;
  3. 参数路由层:assets/promo/promo.js 读取 URL 查询参数 scene 与 format,白名单校验后写入 body.dataset;
  4. 样式层:assets/promo/promo.css 通过 body[data-scene="..."] 选择器只显示一张卡片,并承载全部品牌视觉 token;
  5. 生成脚本:scripts/capture-promos.mjs 串行 spawn 无头 Edge,注入 8 组"文件名 + 场景 + 比例 + 视口"任务,用 --screenshot 直接落盘。

设计意图(WHY):

  • 单一可编辑源:文案与版式改动只需编辑 HTML/CSS,再跑一次脚本即可全量重生成,不需要打开图形软件手工改图;
  • 证据真实性:宣传图里的界面全部来自真实渲染的样例截图,而非示意图——这与仓库"不复制受保护素材"的合规边界一致(SKILL.md 明确禁止复制官方 logo、角色立绘、宣传图);
  • 无 JS 依赖的降级:promo.html 的 <body> 带有默认 data-scene="cover" data-format="landscape",即使脚本尚未执行,封面卡片也已按 CSS 默认显示。

Architecture

Loading diagram...

架构说明:

  • capture-promos.mjs 是唯一的编排者。它不解析页面内容,也不做图像后处理——所有视觉决策都在 HTML/CSS 里,脚本只负责"给定 URL + 视口 → 落一张 PNG"。这使管线没有除 Edge 之外的任何原生依赖(无 puppeteer、无 sharp)。
  • promo.html 是数据源 + 模板的合体。四个 <section class="promo-card scene-*"> 同时存在于 DOM 中,靠 CSS display 切换,而不是靠 JS 重建 DOM。因此同一份 HTML 能服务 4 个场景 × 2 种比例 = 8 张图。
  • promo.js 只做路由,不做渲染。14 行脚本完成参数白名单校验、dataset 写入与字体就绪标记,渲染逻辑全部下沉到 CSS,避免截图时序与 JS 执行时序耦合。
  • 产物是构建结果。assets/promo/output/ 中的 PNG 与源文件并列存放,README 直接引用其中的封面图作为项目门面。

Core Flow:一次生成任务的端到端执行

Loading diagram...

关键步骤逐条解读(均对应 scripts/capture-promos.mjs 实际代码):

  1. 目录自举(第 41 行):mkdir(outputDirectory, { recursive: true }) 保证 assets/promo/output/ 存在,因此脚本从任意 CWD 运行都不会因目录缺失而失败;outputDirectory 是从 import.meta.url 反推出来的(第 8–11 行),与 CWD 完全解耦。
  2. 任务表驱动(第 15–24 行):jobs 数组把"产物文件名 ↔ 场景 ↔ 比例 ↔ 视口尺寸"四元组固化。新增一张宣传图只需在此追加一行,不需要改任何渲染代码。
  3. URL 参数注入(第 43–45 行):对 file:// 形式的 promo.html 追加 scene 与 format 查询参数。用 pathToFileURL 而非手工拼字符串,正确处理了路径中的空格等字符(例如默认 Edge 安装路径)。
  4. 串行执行(for...of + await runEdge):8 次截图严格串行。这是有意为之——避免多个 Edge 实例同时争抢 --virtual-time-budget 计时与字体缓存,也让日志按文件名顺序输出,便于肉眼核对。
  5. 退出码即校验(第 33–37 行):runEdge 监听 stderr 与 exit,非 0 退出码直接 reject,最终由 main().catch 打印堆栈并置 process.exitCode = 1。管线没有像素级 diff 校验,失败语义是"进程级失败"(例如 Edge 路径错误、找不到页面)。

数据模型与产物清单

管线没有数据库或持久化实体,"数据模型"体现为两个层面的静态结构:

产物表(assets/promo/output/)

文件名sceneformat视口尺寸内容主题
01-cover-landscape.pngcoverlandscape1600×900主封面:五风格族 × 四深度 × 响应式卖点,三张样例屏透视叠放
02-families-landscape.pngfamilieslandscape1600×900五个风格族磁贴,逐一列出配色关键词
03-depth-landscape.pngdepthlandscape1600×900四档深度卡 + 复杂档 Endfield 参考
04-responsive-landscape.pngresponsivelandscape1600×900桌面 1440×900 与三台移动设备对照
05-cover-portrait.pngcoverportrait1080×1350同 01,竖版构图
06-families-portrait.pngfamiliesportrait1080×1350同 02,竖版构图
07-depth-portrait.pngdepthportrait1080×1350同 03,竖版构图
08-responsive-portrait.pngresponsiveportrait1080×1350同 04,竖版构图

场景与参数的合法取值(promo.js 中的白名单)

参数合法值非法/缺省时的回退
scenecover / families / depth / responsive回退为 cover
formatlandscape / portrait回退为 landscape

这张白名单表就是 promo.js 的全部业务逻辑——它同时是 URL 校验器和 CSS 场景选择器之间的契约。

Usage Examples

场景路由脚本(promo.js 全文)

参数白名单 + dataset 写入 + 字体就绪标记,是整条管线的"路由核心":

javascript
1const params = new URLSearchParams(location.search); 2const allowedScenes = ['cover', 'families', 'depth', 'responsive']; 3const allowedFormats = ['landscape', 'portrait']; 4 5const scene = allowedScenes.includes(params.get('scene')) ? params.get('scene') : 'cover'; 6const format = allowedFormats.includes(params.get('format')) ? params.get('format') : 'landscape'; 7 8document.body.dataset.scene = scene; 9document.body.dataset.format = format; 10document.title = `Ark UI Promo / ${scene} / ${format}`; 11document.fonts.ready.then(() => { 12 document.body.dataset.ready = 'true'; 13});

Source: promo.js

要点:

  • 防御式解析:includes(...) ? ... : 'cover' 保证任何非法查询串都得到可渲染的画面,截图永远不会得到空白页;
  • document.title 被同步改写,便于在浏览器调试时快速确认当前 scene/format 组合;
  • document.fonts.ready 之后才置 dataset.ready = 'true'——配合截图脚本的 --virtual-time-budget=1200,确保字体加载完成后再定格。

场景切换的 CSS 机制

css
1.promo-card { position: relative; display: none; isolation: isolate; overflow: hidden; } 2body[data-scene="cover"] .scene-cover, 3body[data-scene="families"] .scene-families, 4body[data-scene="depth"] .scene-depth, 5body[data-scene="responsive"] .scene-responsive { display: block; }

Source: promo.css

这是"一个 HTML 出八张图"的关键:所有场景卡片常驻 DOM,display: none 默认隐藏,只有匹配 body[data-scene] 的那张变成 display: block。isolation: isolate 建立层叠上下文,保证每个场景的 ::before 装饰层(如封面右侧的黄色同心圆)不会互相串染。

任务表与无头截图调用

javascript
1const jobs = [ 2 ['01-cover-landscape.png', 'cover', 'landscape', 1600, 900], 3 ['02-families-landscape.png', 'families', 'landscape', 1600, 900], 4 ['03-depth-landscape.png', 'depth', 'landscape', 1600, 900], 5 ['04-responsive-landscape.png', 'responsive', 'landscape', 1600, 900], 6 ['05-cover-portrait.png', 'cover', 'portrait', 1080, 1350], 7 ['06-families-portrait.png', 'families', 'portrait', 1080, 1350], 8 ['07-depth-portrait.png', 'depth', 'portrait', 1080, 1350], 9 ['08-responsive-portrait.png', 'responsive', 'portrait', 1080, 1350], 10];

Source: capture-promos.mjs

以及真正把页面变成 PNG 的那段:

javascript
1await runEdge([ 2 '--headless=new', 3 '--disable-gpu', 4 '--hide-scrollbars', 5 '--run-all-compositor-stages-before-draw', 6 '--virtual-time-budget=1200', 7 `--window-size=${width},${height}`, 8 `--screenshot=${outputPath}`, 9 url.href, 10]);

Source: capture-promos.mjs

每个开关都有明确目的:

参数作用为何需要
--headless=new新版无头模式与有头渲染引擎一致,避免旧无头模式的排版差异
--disable-gpu关闭 GPU 加速无头环境下的稳定性考虑,避免驱动差异
--hide-scrollbars隐藏滚动条html, body { overflow: hidden } 已限制溢出,但保险起见仍关闭
--run-all-compositor-stages-before-draw先完成合成阶段再绘制确保层叠/透明度/滤镜全部就绪
--virtual-time-budget=1200虚拟时间预算 1200ms给 document.fonts.ready 与 <img> 加载留时间,替代 sleep
--window-size=W,H视口尺寸横版 1600×900、竖版 1080×1350 的来源
--screenshot=path直接写文件免去 CDP 截图协议,无需任何 Node 端图像库

产物被 README 直接引用

markdown
1![Ark UI 横版宣传封面](assets/promo/output/01-cover-landscape.png) 2 3![Ark UI 竖版宣传封面](assets/promo/output/05-cover-portrait.png) 4 5完整素材位于 `assets/promo/output/`,可编辑源文件位于 `assets/promo/`。重新生成全部八张图片: 6 7node "$CODEX_HOME/skills/ark-ui/scripts/capture-promos.mjs"

Source: README.md

注意这里的调用路径是 $CODEX_HOME/skills/ark-ui/scripts/capture-promos.mjs——脚本按"skill 安装位置"编写,但在本仓库内直接以 node scripts/capture-promos.mjs 运行同样有效,因为所有路径都从 import.meta.url 推导。

Configuration Options

选项类型默认值说明
ARK_UI_EDGE_BIN环境变量/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge指定 Microsoft Edge 可执行文件路径;非 macOS 或非默认安装位置必须显式覆盖
scene(URL 查询参数)stringcover(回退值)决定显示哪张 promo-card,取值见场景白名单
format(URL 查询参数)stringlandscape(回退值)画幅比例标记,写入 body.dataset.format
--virtual-time-budget内置常量1200(毫秒)截图前等待字体与图片的虚拟时间上限,写死在脚本中
视口尺寸内置常量横版 1600×900,竖版 1080×1350与 jobs 表一一对应,改尺寸需同步改任务表

设计意图:唯一的运行时可变项是 ARK_UI_EDGE_BIN,其余全部固化为常量——这是一条"确定性管线",目标是每次运行产出字节级一致(或视觉一致)的 8 张图,而不是可配置的通用截图工具。通用样例截图采集由 capture-showcases.mjs 承担(不在本页范围)。

API Reference

本子系统没有导出的公共 API;下面列出两个"事实上被外部调用"的入口。

node scripts/capture-promos.mjs

行为:串行调用无头 Edge,按 jobs 表生成 8 张 PNG 到 assets/promo/output/。

参数:无命令行参数。所有输入来自脚本内常量与环境变量 ARK_UI_EDGE_BIN。

输出:stdout 打印每张图的 文件名 + 尺寸(fileName.padEnd(32) 对齐),stderr 仅在失败时由 Edge 写入并被脚本收集。

退出码:成功 0;任一 Edge 进程非 0 退出或 spawn 失败时 1(process.exitCode = 1),并打印 error.stack || error.message。

runEdge(args): Promise<string>

脚本内部唯一的可复用函数(未导出)。

参数:args(string[])——传给 Edge 的完整参数数组。

返回:Promise,resolve 值为收集到的 stderr 字符串(即使成功也可能非空)。

抛出:child.on('error', reject)——spawn 本身失败(典型情况:ARK_UI_EDGE_BIN 指向不存在的路径);退出码非 0 时 reject Edge exited with code N: <stderr>。

promo.js 的隐式接口

输入输出(副作用)
?scene=X&format=Y(X/Y 均在白名单内)body.dataset.scene=X、body.dataset.format=Y、标题同步更新
任意非法或缺失参数回退为 cover / landscape,页面仍可渲染
字体加载完成body.dataset.ready = 'true'

Failure Modes, Edge Cases & Concurrency

场景现象处理方式源码依据
Edge 未安装 / 路径错误spawn 立即失败child.on('error', reject) → main().catch 打印堆栈,退出码 1runEdge 第 32 行
Edge 进程崩溃退出码非 0收集 stderr 后 reject Edge exited with code N: ...第 33–37 行
非法查询参数可能渲染空白/错误场景promo.js 白名单回退到 cover/landscape,截图永不空白promo.js 第 5–6 行
字体未加载完就截图文字使用回退字体document.fonts.ready + --virtual-time-budget=1200 双保险promo.js 第 11 行、脚本第 52 行
showcase 截图缺失图片破图管线不校验——HTML 直接引用 ../showcases/screenshots/*-complex.png,需先运行 capture-showcases.mjspromo.html 第 29–31 行等
并发运行多个实例字体缓存/虚拟时间计时竞争脚本内串行 for...of + await,无并行;也未提供并发开关(有意保守)第 42–58 行
产物目录缺失写文件失败mkdir(..., { recursive: true }) 预先自举第 41 行
内容溢出视口被裁切html, body { overflow: hidden } + --hide-scrollbars;宣传画布按精确视口设计,无溢出检测逻辑(与 capture-showcases.mjs 的"fail on horizontal overflow"不同)promo.css 第 18 行

边界条件要点:

  • 顺序依赖:宣传管线依赖 showcase 截图先存在。这是一个隐式前置条件,管线本身不校验、不自动触发——若需要从零重建,应先跑 capture-showcases.mjs 再跑本脚本。
  • 横向溢出不校验:本脚本与 showcase 采集脚本的职责差异是刻意的——宣传画布是固定视口的"海报",其布局在 CSS 中绝对定位(如 .cover-copy { left: 4.5rem; top: 10.8rem; width: 43% }),不存在需要验证的流式溢出。
  • 可重入:脚本每次全量覆盖 output/ 下的同名文件,无增量/缓存机制,因此总是幂等的。

Performance & Operational Notes

  • 成本模型:8 次串行 Edge 冷启动 ≈ 8 × (进程启动 + 虚拟时间 1200ms + 图片解码)。对一次性再生成任务完全可接受;若未来扩展到几十张图,可考虑复用单个 Edge 实例(--remote-debugging-port + CDP),但这与当前"零外部依赖"的设计取舍相悖。
  • 确定性:--disable-gpu、--run-all-compositor-stages-before-draw、--virtual-time-budget 三者共同把渲染时间线压平,降低机器间差异;这是把"截图"当作"构建产物"而非"随手抓图"的关键工程化决策。
  • 跨平台:默认 ARK_UI_EDGE_BIN 指向 macOS 安装路径。Linux/CI 环境需显式设置(例如指向 chromium 二进制或 Edge 的 Linux 包)。脚本未对 Windows 的 .exe 做任何特判。
  • 产物体积:8 张 1600×900 / 1080×1350 的 PNG 全部入库,README 引用其中两张作为门面。重生成会改变 PNG 的字节内容(压缩时间戳等元数据可能变化),review 时应注意只提交必要的重生成。

Extension Points

  • 新增一张宣传图:三步即可——(1) 在 promo.html 增加 <section class="promo-card scene-<name>">;(2) 在 promo.css 增加 body[data-scene="<name>"] .scene-<name> { display: block } 与该场景样式;(3) 在 capture-promos.mjs 的 jobs 表追加一行。promo.js 只需把新场景名加进 allowedScenes。
  • 新增一种画幅:format 已被参数化(allowedFormats + body.dataset.format),CSS 中可按 body[data-format="portrait"] 写差异规则;脚本侧仅需在 jobs 中增加对应视口尺寸行。
  • 更换浏览器:runEdge(args) 是一个薄封装,任何支持 --headless --screenshot 标志集的 Chromium 系浏览器都可通过 ARK_UI_EDGE_BIN 接入,无需改代码。
  • 文案本地化:promo.html 中标题/副标题为中文(不只是一张皮肤、五种风格,不是五套配色等),装饰性英文短语作为排版元素保留;本地化只需改 HTML 文本节点,不影响管线。

Tests

仓库中没有针对宣传管线的自动化测试(无单测、无像素 diff)。质量保障依赖三条间接机制,均可在源码中找到证据:

  1. 渲染白名单回退(promo.js)保证参数化入口永远产出可渲染页面;
  2. --virtual-time-budget + fonts.ready 保证截取时机稳定;
  3. README 中的预览引用(第 28–30 行)让每次浏览 README 即成为对产物存在性的冒烟检查。
  • README.md 宣传素材章节 — 产物用途与再生成命令
  • SKILL.md 资产目录 — assets/promo/ 与 scripts/capture-promos.mjs 的官方定位
  • promo.html — 四个场景卡片的完整结构与文案
  • promo.css — 品牌 token、场景切换与封面/风格族样式
  • capture-promos.mjs — 生成管线完整实现
  • 样例界面与截图采集(showcase、capture-showcases.mjs)属于兄弟页面主题,本页仅引用其产物路径 assets/showcases/screenshots/

Sources

(4 files)