1实现原理 · 为什么它能做到
核心把戏是把「一堆图摆上白板」做成一个独立 scene 文件(pick tray 挑选托盘),而不是去改用户那块正在用的板子。整个 Job 1 就是把 N 张图等比缩放、居中排进网格、base64 内嵌进 files 映射。
The scene is a *pick tray*: open it in a second Excalidraw tab, select what you want, and copy-paste it onto your real board. It is never written into the board file itself — see references/paste_workflow.md for why that matters.
图片尺寸不靠外部库,直接从文件头解析:PNG 读 IHDR 的 8 字节宽高,JPEG 走段扫描到 SOF 标记再取宽高。
"""(width, height) in pixels for PNG or JPEG, read from the file's own header."""
每次写入后强制从磁盘 read-back 自证:元素数、fileId 与 base64 内容的 sha1 是否一致、绘制长宽比与原图是否一致、两两包围盒是否重叠,任一不符即 SystemExit。
Deliberately re-reads from disk rather than checking the in-memory objects: the failure this catches is a bad write, and an in-memory assert cannot see one.
去重与排除都按内容哈希(sha256)而不是文件名,改过名的副本同样被识别。
skip = {hashlib.sha256(p.read_bytes()).hexdigest() for p in exclude if p.exists()}
--template-from 解决的是「官方 JSON schema 没写 image element 自己的字段」这个真空:从用户自己的板子里深拷贝一个真实 image element 来拿字段集。
for el in scene.get("elements", []): if el.get("type") == "image" and not el.get("isDeleted"): return copy.deepcopy(el)
交付路线刻意选剪贴板而非 Open / 拖拽:后两者会整块替换用户当前场景。
The clipboard carries elements *and* their embedded image payloads, so copy-paste merges instead of replacing:
Job 2 的截图之所以不丢内容,靠三件事:从源码正则抽 slide id 与 fragmentCount、每页全量 page.goto(而不是改 hash)、以及用 computed opacity 而不是 innerText 判断 fragment 是否真的显示出来。
// Fragments that never revealed stay in the DOM at opacity 0, so any check // based on text content reports them as present while the picture is blank. // Measure what actually renders.
两种 fragment 推进模式被判定为互斥,脚本层直接禁止混用:--advance 只接受 pdf 或 keys。
console.error(`--advance must be "pdf" or "keys", got ${ADVANCE}`)
Job 3 的 inspect_scene.py 是只读体检:类型计数、内嵌 payload 体积、fileId 与内容 sha1 是否仍然一致、以及已占用 extent。extent 是给用户「往哪滚再粘贴」用的。
# Excalidraw keys files by a content hash; when it holds, a # mismatch means the payload was swapped without rekeying.
split_scene.py 先整除再按实际分片数命名(避免出现「1-of-4 却没有 4-of-4」),并且每个分片只携带自己引用到的 files,否则分片体积毫无收益。
"files": {e["fileId"]: files[e["fileId"]] for e in part},
2核心能力
3外部依赖
| 类型 | 依赖 |
|---|---|
| package | puppeteer(Job 2 无头浏览器,第三方 npm 包,需可被解析,skill 不负责安装) |
| cli | node(执行 shoot_deck.mjs) |
| cli | npx vite build(把 deck 从源码构建到 /tmp/deck-dist) |
| cli | python3 / python3 -m http.server(本地静态服务器) |
| network | http://127.0.0.1:8080(无头浏览器取页地址;注意 http.server 默认监听 0.0.0.0) |
| network | https://excalidraw.com(不是调用点:只作为生成 scene 的 source 字段字符串,以及文档里指向站点的手工操作说明) |
| network | https://docs.excalidraw.com/docs/codebase/json-schema(参考文献里引用的官方文档链接,非运行时代码) |
4风险提醒 风险提醒:蓝色 · 知晓即可
- Job 2 依赖外部 npm 包与构建工具,且 skill 不负责安装。 — puppeteer 必须从脚本所在位置可解析(SKILL.md 明说 npx puppeteer 不提供可 import 的模块);npx vite build 在缺 vite 时可能走 npm registry,属第三方供应链面。
- 文档建议的本地静态服务器默认对外网卡开放。 — python3 -m http.server 8080 未指定 --bind,默认 0.0.0.0;/tmp/deck-dist 的内容与目录列表在同网段可达。改为 --bind 127.0.0.1 即可消除。
- Job 2 会在无头浏览器中执行被截页面的 JavaScript。 — deck 页面可能是第三方模板;页面脚本运行在 Chromium 沙箱内,脚本不回落 Node,但仍应只对可信 deck 运行。
- PNG/JPEG 之外的格式会被直接拒绝。 — image_size 只处理 PNG 与 JPEG,其他格式 raise ValueError(『not a PNG or JPEG (only these two carry readable dimensions here)』),build_scene.py 随即 SystemExit——是硬失败而非降级,批量任务里可能出现部分图片不可用。
- 剪贴板路线需要用户手工操作,且验证边界只能靠人。 — 最终一步(选中→复制→滚到空白→粘贴)无法自动化;skill 明确要求把这一步交给用户,agent 不得自行打开 excalidraw.com 去验证。
5第二遍独立确认
- [ok] external_deps 每条的调用点是否真实存在 — puppeteer(shoot_deck.mjs 第 1 行 import)、node(shebang)、npx vite build 与 python3 -m http.server(SKILL.md Job 2 命令行)、127.0.0.1:8080(--url 实参)全部实读命中,非推测。
- [discrepancy] excalidraw.com 是否被当作网络端点误报 — 〔第二遍标注:第一遍易误报,已修正〕build_scene.py 与 split_scene.py 里的 https://excalidraw.com 只是写入 scene 的 "source" 字段值,脚本从不请求该域名;README/文档里的链接同理。已在 external_deps 中显式标注『不是调用点』,避免把字符串常量当成外发。
- [discrepancy] exec( 命中是否意味着命令执行 — 〔第二遍标注:扫描器误报,已澄清〕shoot_deck.mjs 的 const frag = /fragmentCount:\s*(\d+)/.exec(m[2]) 是 JavaScript RegExp.prototype.exec,属正则匹配,不执行进程。全目录无 child_process / spawn / execSync / subprocess。
- [ok] 『不写用户现场板子』是否只是口号 — 代码层:build_scene.py 只写 --out 指定路径,从不读入并回写用户的板子文件;文档层:SKILL.md 有独立小节『## Never write into their live board file』并给出两条独立理由(标签页不监听文件、磁盘文件通常旧于浏览器状态)。
- [ok] 写入后校验是否真做(不是文档承诺) — build_scene.py 的 verify() 在 build() 末尾被调用且从磁盘重读;split_scene.py 对每个分片做同样校验。两道都是可执行代码,失败即 SystemExit。
- [unlocatable] python3 -m http.server 绑定范围 — 〔第二遍标注:文档未声明,属解释器默认行为〕SKILL.md 只写 python3 -m http.server 8080,未写 --bind 127.0.0.1;http.server 的默认绑定是 0.0.0.0。这是 CPython 的既定行为而非本 skill 的代码,故标注为「文档未声明」并在 security.reason 中提示,不写成 skill 缺陷。
- [ok] Job 2 的 --advance 互斥是否代码强制 — shoot_deck.mjs 中 if (!['pdf','keys'].includes(ADVANCE)) 报错退出,且 query 只在 ADVANCE === 'pdf' 时拼 ?pdf=1;SKILL.md 有对应警告段,代码与文档一致。
- [ok] 元数据(license/stars/last_push) — 取自同 repo 同 commit 的池内既有值(MIT、1385★、2026-09-09T12:33:29Z);本次未联网复核 gh。
6结论
0c4ae44eb4c051d3…d5c4678cb5