文件历史动画生成
构建一个 GIF(或 MP4)来展示代码仓库中某个文件随时间变化的外观。请求使用自然语言($ARGUMENTS),因此任务如下:找出请求涉及的文件,从 Git 中提取每个版本,渲染每个版本,丢弃与前一个版本视觉相同的版本,然后将剩余版本拼接成动画。
配套脚本负责 Git 遍历、渲染、视觉去重和编码:
scripts/animate-history.sh,路径相对于本技能的基目录,而非用户的仓库。下文中的 $SKILL 表示该目录;使用绝对路径调用脚本。--help 列出所有选项。你的工作是为图片选择合适的文件,并为页面生成屏幕截图。
1. 检查工具
command -v git ffmpeg rsvg-convert; ls "/Applications/Google Chrome.app" 2>/dev/null; command -v chromium google-chrome
ffmpeg 是必需的。SVG 需要 rsvg-convert(brew install librsvg / apt install librsvg2-bin)或 Chrome/Chromium。--label 字幕和 HTML 页面需要 Chrome/Chromium(CHROME=/path 可覆盖检测)。如果缺少某个工具,请说明是什么以及如何安装——不要悄悄地跳过 SVG 版本。
2. 根据请求解析文件
搜索当前分支的整个历史记录,而不仅仅是今天的文件树——Logo 可能会被重命名、移动和重新导出,已删除的文件仍然属于故事的一部分:
git log --format= --name-only -- ':(glob,icase)**/*logo*' | sort -u
搜索的是该对象的名称,而不仅仅是用户的原词。Logo 也可能出现在 favicon*、apple-touch-icon*、*mark*、*lockup*、*wordmark*、*brand* 以及产品名称本身中——早期版本往往只在 favicon 里。相应地扩展 glob 列表,并查看返回的结果。
过滤出可渲染的类型:图片支持 png jpg jpeg gif webp bmp svg;静态页面支持 html。忽略构建输出和 vendored 副本(dist/、build/、node_modules/、.next/)。
然后将匹配结果分组为谱系——每个谱系生成一个动画:
- 同一幅作品经过重命名、移动或格式变化(
logo.png→brand/logo.svg)属于一个谱系。将它们的所有路径传递给一次运行;脚本会根据提交日期将它们合并到一个时间线中。 - 同时存在的变体(
logo-dark.svg、logo-mono.svg、favicon.png、不同尺寸的导出文件如[email protected])属于独立的谱系。默认为每个谱系生成一个动画,并且只动画化请求可能涉及的那些变体(通常是主 Logo,而不是每个 favicon 尺寸)。
如果分组确实存在歧义(几个不相关的“logo”文件,或者没有匹配项),则显示候选列表及其提交次数(git log --follow --oneline -- <path> | wc -l),并在渲染之前请求确认。
3a. 图片:运行脚本
"$SKILL/scripts/animate-history.sh" -o logo-history.gif --label <path> [<older-path> ...]
有用的选项:-s 512 或 -s 1200x800 画布尺寸,-t 0.25 每版停留秒数(图片默认值;页面截图的 --frames 默认值为 0.8;如果用户指定了值则使用用户的值,例如“每帧 0.5 秒” → -t 0.5),--hold 1.5 在最后一帧停留秒数,--bg '#0b0b0b' 适用于为深色背景设计的 Logo,--keep DIR 保留帧文件,-o x.mp4 输出视频,--skip <sha>(可重复)丢弃用户不想要的版本。脚本会在 stderr 上打印每个提交的 keep/skip 以及 kept N of M versions。
路径是相对于当前目录的(或 -C DIR),就像任何 Git pathspec 一样;从仓库根目录运行,并传入上面历史列表所示的路径。脚本本身会处理重命名跟踪(git log --follow),因此通常只给出当前路径就足够了,除非文件在新名称下被重新创建。
3b. 页面:在每个版本截图,然后在文件夹上运行脚本
页面需要在每个提交上进行渲染。找到页面的源文件(index.html、app/page.tsx、src/pages/index.vue 等),并列出修改了该文件或其渲染内容的提交——首页的外观也会随其组件和 CSS 而变化:
git log --format='%H %cs' --reverse -- <page file> <its components/styles dirs>
如果提交数超过约 40 个,则进行采样(每周或每月一个,加上第一个和最后一个),并说明情况。屏幕截图存放在一个文件夹中,命名为 <YYYY-MM-DD>_<short-sha>.png,这样文件按日期排序,名称即成为字幕。
-
静态 HTML:无需构建。使用
git archive <sha> | tar -x -C "$snap"导出提交,然后使用"$CHROME" --headless=new --hide-scrollbars --window-size=1280,800 --screenshot=... "file://$snap/<page>.html"截图该文件。之后删除$snap,并将其保持在仓库之外。 -
框架页面(React、Next.js、Vue、Astro 等):必须运行才能渲染。每个提交使用
snapshot-commit.sh:"$SKILL/scripts/snapshot-commit.sh" -C <repo> -r /<route> -f https://<production site> <sha> "$shots"它会导出提交,安装该提交自己的 lockfile(按 lockfile 缓存),启动开发服务器(如果不是
npm run dev -- --port {port},则使用-d 'cmd with {port}'),并通过 Playwright 截图(scripts/screenshot-page.mjs,需要playwright-core和 Chrome——仓库自带的通过PLAYWRIGHT_DIR查找)。它会等待网络空闲和每个<img>加载,关闭 Cookie 横幅,并输出一行:ok、blank、error(显示错误页面,例如“无法在此浏览器中加载”)或fail(安装或开发服务器失败)。只有ok会写入 PNG。如果有超过几个安装操作,请先请求许可再运行。
在制作动画之前检查结果,不要仅信任 ok:
- 切勿借用当前的
node_modules。 旧提交会导入当前树中已删除的包(例如react-router-dom),导致页面渲染为空白。这就是脚本需要为每个提交单独安装的原因。 blank/error/fail提交:查看开发日志,尝试修复原因(缺少环境变量、错误的-d命令、--ignore-scripts跳过了必要的构建步骤)一次,否则跳过该提交并说明跳过了多少个。- 损坏的图片(
ok行上的broken-images=N,以及 URL):开发服务器无法提供的资源(平台托管的路径如/__l5e/...、被 gitignore 忽略或上传的文件)。-f <production site>会从生产环境获取它们。如果图片仍然损坏,请在报告中说明——不要默默地交付一个满是空框的 GIF。应在发现问题时立即修复,而不是事后报告。 - 在最终运行之前,先查看帧的缩略图(
ffmpeg -pattern_type glob -i '*.png' -vf scale=320:-1,tile=6x6 -frames:v 1 sheet.png)。
然后:
"$SKILL/scripts/animate-history.sh" --frames "$shots" -s 1280x800 --label -o home-page-history.gif
与处理图片时一样,相同的截图会被丢弃,空白(单一纯色)截图也会被丢弃(使用 --keep-blank 保留)。每次截图使用固定的窗口尺寸。对于较长的或细节丰富的页面,建议使用 -o x.mp4——全页面的 GIF 会变得很大。
3c. 跨多个仓库
当 Logo 存在于多个仓库中(例如一个应用、一个仪表板、一个网站)时,在每个仓库中使用 --keep <dir> 运行脚本(不要加 --label),然后将每个保留的帧复制到一个文件夹中,命名为 <YYYY-MM-DD>_<repo>-<short-sha>.png——stderr 上的 keep 行按顺序给出了每个保留帧的日期和 SHA。对于名称冲突(一个提交中修改了多个文件),在名称后添加 -2、-3。然后在合并后的文件夹上运行 --frames <folder> --label -t 0.25,以生成跨所有仓库的统一时间线。
4. 报告
给出输出路径、找到的版本数量以及实际进入动画的版本数量、日期范围,以及任何被跳过的内容(不可渲染、构建失败、采样排除)。如果被要求则打开它(open x.gif)。将输出写入用户希望的任何位置,默认为当前目录——除非明确要求,否则不要提交它。
注意事项
- 去重基于渲染后的像素,而非字节。 如果重新导出的 PNG 或重新格式化的 SVG 渲染结果相同,则会被跳过;一个像素的偏移都算作一次变化。这就是重点——不要再在其上添加字节级别的过滤器。
- 引用外部文件的 SVG(字体、
<image href>)会在没有这些外部文件的情况下渲染,因为只有 SVG 本身是从历史记录中提取的。内嵌内容没有问题。 - 动画 GIF 源仅贡献其第一帧。
- 合并提交不会被遍历——
git log显示的是文件在创建该更改的分支上的变化,这正是时间线所需的信息。 - 浅克隆(CI 环境、
--depth 1)没有历史记录。检查git rev-parse --is-shallow-repository,如果返回true,建议运行git fetch --unshallow。 - 字幕需要 Chrome/Chromium,因为捆绑的 ffmpeg 版本通常缺少
drawtext。
模型与计算量
这里的判断工作量很小(选择文件、分组谱系、确定采样率);繁重的工作由脚本和 Shell 循环完成。默认使用 Sonnet 低计算量。Opus 或更高计算量对此任务没有额外收益,也不需要使用分支或子代理。仅当谱系分组确实存在歧义时才提高计算量。
一次运行的大部分 token 消耗来自会话的继承上下文(CLAUDE.md、技能和工具列表,约 10 万+ token),每次轮换都会从缓存中重新读取,而非来自任务本身。在轻量会话中的运行成本远低于在负载会话中的成本。
基准测试
使用会话记录进行测量(汇总助手轮次中的 usage):
f=$(ls -t ~/.claude/projects/<project-dir>/*.jsonl | head -1)
jq -s '[.[]|select(.message.usage)|.message.usage]|{turns:length,out:(map(.output_tokens)|add),cache_read:(map(.cache_read_input_tokens)|add),cache_create:(map(.cache_creation_input_tokens)|add)}' "$f"
| 日期 | 模型 / 计算量 | 任务 | 轮次 | 输出 token | 缓存读取 | 缓存创建 | 结果 |
|---|---|---|---|---|---|---|---|
| 2026-10-23 | Sonnet 5.5 / 低 | storra 首页,仅发现阶段(渲染前) | 9 | 3.6k | 731k | 134k | 约 7 次调用找到文件和路由历史 |
每次完整运行添加一行(不仅仅是发现阶段);仅当 Sonnet 低计算量运行出现谱系或采样错误时,才与 Opus / 更高计算量进行比较。