关于 after-effects
通过 ExtendScript 自动化 After Effects 项目的技能。它会读取项目状态、按任务查阅规则,并对可能有破坏性的操作设置确认步骤。
主要功能
- 01查询项目与合成状态
- 02生成并运行 ExtendScript 脚本
- 03按任务读取规则文件
- 04将修改操作纳入撤销组
- 05提供内置批量操作
- 06对破坏性操作先行确认
适用场景
- 01检查项目中的常见问题
- 02批量重命名或整理项目项
- 03调整图层、关键帧或合成
- 04替换字体或表达式
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ aedev-tools/ adobe-agent-skills --skill after-effects安装后在 Claude Code 中输入/after-effects确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
references/ae-api-versions.mdrules/assets-footage.mdbatch-operations.mdcomposition-management.mdeffects.mdexpressions.mdextendscript-fundamentals.mdkeyframes-animation.mdlayer-manipulation.mdrendering.mdscripts/active-state.jsxanchor-point-mover.jsxbatch-comp-settings.jsxbatch-expression.jsxbatch-rename.jsxblend-mode-set.jsxcomp-detail.jsxcomp-from-csv.jsxcopy-ease.jsxeasing-presets.jsxexplode-shape-layer.jsxexpression-errors.jsxexpression-replace.jsxfit-to-comp.jsxfont-inventory.jsxfont-replace.jsxincremental-save.jsxlabel-layers.jsxlayer-detail.jsxlayer-sort.jsx详细介绍
概述
此技能通过生成 ExtendScript(.jsx)并使用 osascript 执行,实现 After Effects 自动化。它通过查询脚本读取项目状态,使用规则文件获取领域知识,并将所有修改封装在撤销组中。
首次设置
- 使用任意查询脚本运行
scripts/runner.sh,检测 AE 版本 - 如果安装了多个 AE 版本,用户必须选择一个——runner.sh 会提示用户
- 确保已启用 AE Preferences > Scripting & Expressions > "Allow Scripts to Write Files and Access Network"
工作流
对于每个用户请求:
步骤 1:收集上下文(自动运行,无需确认)
对所有查询脚本使用 --background——这样会跳过 ae.activate(),避免 AE 窗口抢占焦点:
bash scripts/runner.sh --background scripts/active-state.jsx
然后读取 /tmp/ae-assistant-result.json,获取活动合成、选中图层和 CTI。
如果这是首次交互,或项目上下文未知,还要运行:
bash scripts/runner.sh --background scripts/project-overview.jsx
默认情况下,这会返回摘要:包含数量统计的文件夹树、所有合成列表,以及按文件类型分组的素材。不会列出每个单独的文件。
若要深入查看特定文件夹:
bash scripts/runner.sh --background scripts/project-overview.jsx '{"mode": "folder", "folderName": "Images"}'
只有在确实需要列出每个项目时,才使用完整模式:
bash scripts/runner.sh --background scripts/project-overview.jsx '{"mode": "full"}'
步骤 2:按需深入查看(自动运行)
如果任务针对特定合成:
bash scripts/runner.sh --background scripts/comp-detail.jsx '{"compName": "Comp Name"}'
如果任务针对特定图层:
bash scripts/runner.sh --background scripts/layer-detail.jsx '{"layerNames": ["Layer 1", "Layer 2"]}'
省略 compName 可使用活动合成。省略 layerNames 可使用选中图层。
其他查询脚本
表达式错误——扫描失效表达式:
bash scripts/runner.sh --background scripts/expression-errors.jsx
bash scripts/runner.sh --background scripts/expression-errors.jsx '{"compName": "Main Comp"}'
字体清单——列出项目中使用的所有字体:
bash scripts/runner.sh --background scripts/font-inventory.jsx
项目审计——全面健康检查(未使用的素材、缺失文件、表达式错误、重复纯色图层、字体问题、空文件夹):
bash scripts/runner.sh --background scripts/project-audit.jsx
bash scripts/runner.sh --background scripts/project-audit.jsx '{"checks": ["unused", "missing", "expressions"]}'
步骤 3:加载领域知识
从 rules/ 中读取相关规则文件。始终读取 rules/extendscript-fundamentals.md——其中包含适用于每个生成脚本的 ES3 限制。
| 任务涉及 | 加载规则文件 |
|---|---|
| 图层(创建、移动、设为父级、复制) | rules/layer-manipulation.md |
| 关键帧、动画、缓动 | rules/keyframes-animation.md |
| 表达式 | rules/expressions.md |
| 合成(创建、预合成、嵌套) | rules/composition-management.md |
| 效果和参数 | rules/effects.md |
| 导入、素材、资源 | rules/assets-footage.md |
| 渲染队列、导出 | rules/rendering.md |
| 批量操作 | rules/batch-operations.md |
| 特定版本功能 | references/ae-api-versions.md |
步骤 4:生成操作脚本
重要:先解析技能的实际路径
编写或执行任何操作脚本之前,先解析技能的实际路径(非符号链接路径)。ExtendScript 的 #include 无法跟随符号链接,因此必须使用真实文件系统路径。
在每个会话开始时运行一次:
SKILL_SCRIPTS="$(readlink -f ~/.claude/skills/after-effects-assistant/scripts 2>/dev/null || readlink ~/.claude/skills/after-effects-assistant/scripts)"
echo "$SKILL_SCRIPTS"
在本会话后续所有 Write 和 Bash 命令中,使用解析出的路径($SKILL_SCRIPTS)。
原因:
~/.claude/skills/通常是符号链接——ExtendScript 的#include无法通过符号链接工作- 写入真实的
scripts/目录后,#include "lib/json2.jsx"才能正确解析 - 解析出的路径因机器而异,因此切勿将其硬编码
每个生成的脚本都必须遵循此模板:
#include "lib/json2.jsx"
#include "lib/utils.jsx"
(function() {
app.beginUndoGroup("AE Assistant: <action description>");
try {
var args = readArgs();
var comp = getActiveComp();
if (!comp) return;
// ... action code ...
writeResult({ success: true, message: "<what was done>" });
} catch (e) {
try { writeResult({ error: e.toString(), line: e.line, fileName: e.fileName }); }
catch(e2) { writeError(e.toString(), "line:" + e.line); }
} finally {
app.endUndoGroup();
}
})();
可供生成脚本使用的 utils.jsx 辅助函数
| 函数 | 用途 |
|---|---|
writeResult(obj) | 将 JSON 结果写入 /tmp/ae-assistant-result.json |
writeError(msg, detail) | JSON.stringify 失败时的最后手段,用于捕获错误 |
readArgs() | 从 /tmp/ae-assistant-args.json 读取参数 |
getLayerType(layer) | 返回类型字符串:"shape"、"text"、"null"、"precomp" 等 |
getBlendModeName(mode) | 将 BlendingMode 枚举转换为字符串名称 |
getActiveComp() | 返回活动合成,若无则写入错误并返回 null |
getSelectedOrAllLayers(comp) | 返回选中的图层数组;若未选中任何图层,则返回全部图层 |
hexToRGB(hex) | "#ff0000" → [1, 0, 0] |
getCompByName(name) | 在项目项中按名称查找合成 |
walkProperties(group, leafFn, path) | 递归遍历属性树——对叶节点调用 leafFn(prop, path) |
bubbleSort(arr, compareFn) | 兼容 ES3 的原地排序 |
framesToSeconds(frames, comp) | 根据 comp.frameDuration 将帧数转换为秒数 |
appendLog(message) | 将日志追加到 ~/.ae-assistant-extendscript.log |
使用 Write 工具编写脚本,然后通过一条简短的 bash 命令执行:
- 使用 Write 工具将脚本写入
$SKILL_SCRIPTS/ae-action.jsx(解析后的实际路径) - 使用 bash 执行:
bash "$SKILL_SCRIPTS/runner.sh" "$SKILL_SCRIPTS/ae-action.jsx"
**重要:**请勿使用 cat > file << 'SCRIPT' heredoc——这会将整个脚本放进 bash 命令中,让权限提示变得杂乱。始终使用 Write 工具写入脚本内容,然后通过一条简短的 bash 命令运行。
步骤 5:执行或确认
自动运行(无需确认):
- 所有只读查询(active-state、project-overview、comp-detail、layer-detail、expression-errors、font-inventory、project-audit)
- 非破坏性添加:添加关键帧、添加效果、创建图层、创建合成
运行前确认(向用户展示脚本并征求确认):
- 删除图层或合成
- 移除关键帧
- 替换素材
- 清除表达式
- 渲染队列操作
- 项目清理(移除未使用项、合并纯色图层)
- 用户可能意料之外的任何操作
内置操作脚本
在生成自定义的 ae-action.jsx 之前,请先检查是否已有内置脚本可以处理该任务。这些是稳定、经过测试且基于参数运行的脚本:
True Comp Duplicator — 深度克隆合成,并为其子合成创建独立副本(运行前确认):
bash "$SKILL_SCRIPTS/runner.sh" "$SKILL_SCRIPTS/true-comp-duplicator.jsx" '{"compName": "Main Comp", "suffix": " COPY"}'
Font Replace — 查找并替换整个项目中的字体。始终先使用 dryRun:
bash "$SKILL_SCRIPTS/runner.sh" "$SKILL_SCRIPTS/font-replace.jsx" '{"find": "Helvetica", "replace": "Inter-Regular", "dryRun": true}'
bash "$SKILL_SCRIPTS/runner.sh" "$SKILL_SCRIPTS/font-replace.jsx" '{"find": "Helvetica", "replace": "Inter-Regular"}'
Project Cleanup — 移除未使用的素材,合并重复的纯色图层,移除空文件夹。始终先使用 dryRun:
bash "$SKILL_SCRIPTS/runner.sh" "$SKILL_SCRIPTS/project-cleanup.jsx" '{"dryRun": true}'
bash "$SKILL_SCRIPTS/runner.sh" "$SKILL_SCRIPTS/project-cleanup.jsx"
Batch Rename — 批量重命名图层、合成或项目项:
bash "$SKILL_SCRIPTS/runner.sh" "$SKILL_SCRIPTS/batch-rename.jsx" '{"target": "layers", "mode": "find-replace", "find": "Layer", "replace": "Element"}'
bash "$SKILL_SCRIPTS/runner.sh" "$SKILL_SCRIPTS/batch-rename.jsx" '{"target": "layers", "mode": "prefix", "prefix": "BG_"}'
bash "$SKILL_SCRIPTS/runner.sh" "$SKILL_SCRIPTS/batch-rename.jsx" '{"target": "layers", "mode": "sequence", "base": "Card", "start": 1}'
Layer Stagger — 在时间上错开选中的图层,制作级联动画:
bash "$SKILL_SCRIPTS/runner.sh" "$SKILL_SCRIPTS/layer-stagger.jsx" '{"offset": 0.1, "unit": "seconds", "direction": "forward"}'
bash "$SKILL_SCRIPTS/runner.sh" "$SKILL_SCRIPTS/layer-stagger.jsx" '{"offset": 2, "unit": "frames"}'
Expression Replace — 查找并替换整个项目表达式中的文本。始终先使用 dryRun:
bash "$SKILL_SCRIPTS/runner.sh" "$SKILL_SCRIPTS/expression-replace.jsx" '{"find": "comp(\"Old\")", "replace": "comp(\"New\")", "dryRun": true}'
bash "$SKILL_SCRIPTS/runner.sh" "$SKILL_SCRIPTS/expression-replace.jsx" '{"find": "comp(\"Old\")", "replace": "comp(\"New\")"}'
Organize Project — 按类型将项目面板中的项自动归类到文件夹:
bash "$SKILL_SCRIPTS/runner.sh" "$SKILL_SCRIPTS/organize-project.jsx" '{"structure": "by-type"}'
bash "$SKILL_SCRIPTS/runner.sh" "$SKILL_SCRIPTS/organize-project.jsx" '{"structure": "by-extension"}'
Batch Comp Settings — 批量更改多个合成的帧速率、分辨率和时长:
bash "$SKILL_SCRIPTS/runner.sh" "$SKILL_SCRIPTS/batch-comp-settings.jsx" '{"scope": "all", "fps": 25}'
bash "$SKILL_SCRIPTS/runner.sh" "$SKILL_SCRIPTS/batch-comp-settings.jsx" '{"compNames": ["Comp 1", "Comp 2"], "width": 3840, "height": 2160}'
bash "$SKILL_SCRIPTS/runner.sh" "$SKILL_SCRIPTS/batch-comp-settings.jsx" '{"scope": "nested", "fps": 30, "duration": 10}'
Easing Presets — 应用专业缓动或弹跳/弹性表达式:
bash "$SKILL_SCRIPTS/runner.sh" "$SKILL_SCRIPTS/easing-presets.jsx" '{"preset": "smooth"}'
bash "$SKILL_SCRIPTS/runner.sh" "$SKILL_SCRIPTS/easing-presets.jsx" '{"preset": "snappy"}'
bash "$SKILL_SCRIPTS/runner.sh" "$SKILL_SCRIPTS/easing-presets.jsx" '{"preset": "bounce"}'
Anchor Point Mover — 重新定位锚点,同时通过视觉位置补偿保持图层位置不变:
bash "$SKILL_SCRIPTS/runner.sh" "$SKILL_SCRIPTS/anchor-point-mover.jsx" '{"position": "center"}'
bash "$SKILL_SCRIPTS/runner.sh" "$SKILL_SCRIPTS/anchor-point-mover.jsx" '{"position": "bottom-left"}'
Reverse Keyframes — 反转选中属性上的动画:
bash "$SKILL_SCRIPTS/runner.sh" "$SKILL_SCRIPTS/reverse-keyframes.jsx"
Select Layers — 按类型、标签、属性或名称选择图层:
bash "$SKILL_SCRIPTS/runner.sh" "$SKILL_SCRIPTS/select-layers.jsx" '{"type": "text"}'
bash "$SKILL_SCRIPTS/runner.sh" "$SKILL_SCRIPTS/select-layers.jsx" '{"hasExpressions": true}'
bash "$SKILL_SCRIPTS/runner.sh" "$SKILL_SCRIPTS/select-layers.jsx" '{"nameContains": "BG"}'
Layer Sort — 在时间轴中按名称、位置、入点、类型或标签重新排列图层:
bash "$SKILL_SCRIPTS/runner.sh" "$SKILL_SCRIPTS/layer-sort.jsx" '{"sortBy": "name", "order": "ascending"}'
bash "$SKILL_SCRIPTS/runner.sh" "$SKILL_SCRIPTS/layer-sort.jsx" '{"sortBy": "position-y"}'
Smart Precompose — 预合成并自动修剪时长:
bash "$SKILL_SCRIPTS/runner.sh" "$SKILL_SCRIPTS/smart-precompose.jsx" '{"name": "My Precomp", "trimToContent": true}'
Copy Ease — 复制一个关键帧的缓动并粘贴到其他关键帧:
bash "$SKILL_SCRIPTS/runner.sh" "$SKILL_SCRIPTS/copy-ease.jsx" '{"mode": "both"}'
bash "$SKILL_SCRIPTS/runner.sh" "$SKILL_SCRIPTS/copy-ease.jsx" '{"sourceLayer": "Logo", "sourceProperty": "Position", "sourceKeyIndex": 2}'
Relink Footage — 从搜索目录批量重新链接缺失的素材。始终先使用 dryRun:
bash "$SKILL_SCRIPTS/runner.sh" "$SKILL_SCRIPTS/relink-footage.jsx" '{"searchPaths": ["/Volumes/Projects/footage"], "dryRun": true}'
bash "$SKILL_SCRIPTS/runner.sh" "$SKILL_SCRIPTS/relink-footage.jsx" '{"searchPaths": ["/Volumes/Projects/footage"]}'
SRT Import — 根据 SRT 文件创建定时字幕文本图层:
bash "$SKILL_SCRIPTS/runner.sh" "$SKILL_SCRIPTS/srt-import.jsx" '{"srtPath": "/path/to/subs.srt", "fontSize": 48}'
Text Export/Import — 将所有文本导出为 CSV,在外部编辑后重新导入:
bash "$SKILL_SCRIPTS/runner.sh" "$SKILL_SCRIPTS/text-export-import.jsx" '{"mode": "export", "csvPath": "/tmp/text.csv"}'
bash "$SKILL_SCRIPTS/runner.sh" "$SKILL_SCRIPTS/text-export-import.jsx" '{"mode": "import", "csvPath": "/tmp/text.csv"}'
Batch Expression — 批量应用、移除、启用或禁用表达式:
bash "$SKILL_SCRIPTS/runner.sh" "$SKILL_SCRIPTS/batch-expression.jsx" '{"property": "opacity", "expression": "wiggle(2, 10)", "mode": "apply"}'
bash "$SKILL_SCRIPTS/runner.sh" "$SKILL_SCRIPTS/batch-expression.jsx" '{"mode": "remove"}'
bash "$SKILL_SCRIPTS/runner.sh" "$SKILL_SCRIPTS/batch-expression.jsx" '{"mode": "disable"}'
Randomize Properties — 为变换属性应用随机值:
bash "$SKILL_SCRIPTS/runner.sh" "$SKILL_SCRIPTS/randomize-properties.jsx" '{"property": "rotation", "min": -15, "max": 15}'
bash "$SKILL_SCRIPTS/runner.sh" "$SKILL_SCRIPTS/randomize-properties.jsx" '{"property": "position", "minX": 0, "maxX": 1920, "minY": 0, "maxY": 1080}'
Un-PreCompose — 将图层从预合成中提取回父合成(运行前确认):
bash "$SKILL_SCRIPTS/runner.sh" "$SKILL_SCRIPTS/un-precompose.jsx" '{"precompLayerName": "Precomp 1"}'
Comp from CSV — 根据电子表格数据生成多个合成变体(运行前确认):
bash "$SKILL_SCRIPTS/runner.sh" "$SKILL_SCRIPTS/comp-from-csv.jsx" '{"templateComp": "Lower Third", "csvPath": "/path/data.csv"}'
Render Queue Batch — 将多个合成添加到渲染队列(运行前确认):
bash "$SKILL_SCRIPTS/runner.sh" "$SKILL_SCRIPTS/render-queue-batch.jsx" '{"compNames": ["Final_16x9", "Final_9x16"], "outputPath": "~/Desktop/renders/"}'
bash "$SKILL_SCRIPTS/runner.sh" "$SKILL_SCRIPTS/render-queue-batch.jsx" '{"scope": "folder", "folderName": "Finals"}'
Explode Shape Layer — 将形状组拆分为独立图层:
bash "$SKILL_SCRIPTS/runner.sh" "$SKILL_SCRIPTS/explode-shape-layer.jsx" '{"layerName": "AI Import"}'
Incremental Save — 保存项目并自动递增版本号:
bash "$SKILL_SCRIPTS/runner.sh" "$SKILL_SCRIPTS/incremental-save.jsx" '{"comment": "before revisions"}'
Purge Cache — 清除内存缓存和磁盘缓存,并释放资源。先使用 dryRun 检查大小:
bash "$SKILL_SCRIPTS/runner.sh" "$SKILL_SCRIPTS/purge-cache.jsx" '{"dryRun": true}'
bash "$SKILL_SCRIPTS/runner.sh" "$SKILL_SCRIPTS/purge-cache.jsx"
bash "$SKILL_SCRIPTS/runner.sh" "$SKILL_SCRIPTS/purge-cache.jsx" '{"memory": true, "disk": false}'
Trim Comp to Content — 修剪/延长合成时长,使其恰好适配图层内容:
bash "$SKILL_SCRIPTS/runner.sh" "$SKILL_SCRIPTS/trim-comp-to-content.jsx"
bash "$SKILL_SCRIPTS/runner.sh" "$SKILL_SCRIPTS/trim-comp-to-content.jsx" '{"padding": 0.5}'
bash "$SKILL_SCRIPTS/runner.sh" "$SKILL_SCRIPTS/trim-comp-to-content.jsx" '{"recursive": true}'
Null from Layers — 在每个选中图层的位置创建空对象,并自动设为父级:
bash "$SKILL_SCRIPTS/runner.sh" "$SKILL_SCRIPTS/null-from-layers.jsx"
bash "$SKILL_SCRIPTS/runner.sh" "$SKILL_SCRIPTS/null-from-layers.jsx" '{"naming": "custom", "prefix": "Driver"}'
bash "$SKILL_SCRIPTS/runner.sh" "$SKILL_SCRIPTS/null-from-layers.jsx" '{"position": "comp-center"}'
Fit to Comp — 缩放选中图层,使其适配/填满合成尺寸:
bash "$SKILL_SCRIPTS/runner.sh" "$SKILL_SCRIPTS/fit-to-comp.jsx" '{"mode": "fit"}'
bash "$SKILL_SCRIPTS/runner.sh" "$SKILL_SCRIPTS/fit-to-comp.jsx" '{"mode": "fill"}'
bash "$SKILL_SCRIPTS/runner.sh" "$SKILL_SCRIPTS/fit-to-comp.jsx" '{"mode": "stretch"}'
Label Layers — 按类型、名称或选择批量设置图层标签颜色:
bash "$SKILL_SCRIPTS/runner.sh" "$SKILL_SCRIPTS/label-layers.jsx" '{"label": 3, "target": "selected"}'
bash "$SKILL_SCRIPTS/runner.sh" "$SKILL_SCRIPTS/label-layers.jsx" '{"label": 1, "target": "text"}'
bash "$SKILL_SCRIPTS/runner.sh" "$SKILL_SCRIPTS/label-layers.jsx" '{"label": 5, "target": "all", "nameContains": "BG"}'
Blend Mode Set — 为选中图层设置混合模式:
bash "$SKILL_SCRIPTS/runner.sh" "$SKILL_SCRIPTS/blend-mode-set.jsx" '{"mode": "SCREEN"}'
bash "$SKILL_SCRIPTS/runner.sh" "$SKILL_SCRIPTS/blend-mode-set.jsx" '{"mode": "MULTIPLY"}'
bash "$SKILL_SCRIPTS/runner.sh" "$SKILL_SCRIPTS/blend-mode-set.jsx" '{"mode": "ADD"}'
Split Layer — 在 CTI 处拆分选中图层(运行前确认 — 会修改图层时间):
bash "$SKILL_SCRIPTS/runner.sh" "$SKILL_SCRIPTS/split-layer.jsx"
bash "$SKILL_SCRIPTS/runner.sh" "$SKILL_SCRIPTS/split-layer.jsx" '{"time": 2.5}'
第 6 步:执行并读取结果
# Execute the action script (already written to $SKILL_SCRIPTS/ae-action.jsx in Step 4)
bash "$SKILL_SCRIPTS/runner.sh" "$SKILL_SCRIPTS/ae-action.jsx" '{"arg1": "value1"}'
读取 /tmp/ae-assistant-result.json 获取结果。
故障排查
如果脚本运行失败,请按以下顺序检查:
~/.ae-assistant-log— runner.sh 会在此记录每次执行、参数、结果和错误/tmp/ae-assistant-error.txt— JXA 级错误(AE 无响应、DoScriptFile 失败)~/.ae-assistant-extendscript.log—utils.jsx中appendLog()写入的 ExtendScript 级日志- AE Preferences — 确保已启用“允许脚本写入文件并访问网络”
发生错误时,请读取 ~/.ae-assistant-log 了解情况,修复脚本后重试。
必须
- 始终将修改操作包裹在
app.beginUndoGroup()/app.endUndoGroup()中 - 始终使用 matchNames 访问属性,不要使用显示名称(显示名称会本地化)
- 始终对图层和项目项使用从 1 开始的索引
- 始终将操作脚本写入
$SKILL_SCRIPTS/ae-action.jsx(解析后的实际路径,而非/tmp/) - 始终使用相对路径
#include "lib/json2.jsx"和#include "lib/utils.jsx"(不要使用绝对路径) - 始终用 IIFE 包裹代码,以避免污染全局作用域
- 始终使用
var,绝不使用let或const(ES3) - 始终通过
writeResult()将结果写入 /tmp/ae-assistant-result.json - 始终使用
getActiveComp()获取活动合成(它会处理 null/CompItem 检查并写入错误) - 查询脚本始终使用
--background标志,以避免抢占焦点
禁止
- 绝不使用 ES5 及更高版本的语法:
let、const、箭头函数、模板字面量、解构 - 绝不使用
Array.map、Array.filter、Array.reduce、Array.forEach(ES3 不支持) - 未引入
json2.jsx时,绝不使用JSON.parse或JSON.stringify - 绝不将操作脚本写入
/tmp/——ExtendScript 的#include无法从那里解析路径 - 绝不在
#include中使用绝对路径——它们会因符号链接而失效 - 绝不硬编码图层索引——使用名称、选择项或迭代
- 未经用户确认,绝不执行破坏性操作
- 绝不假定某个合成处于活动状态,必须先检查
- 绝不使用
cat > file << 'SCRIPT'heredoc 编写脚本——改用 Write 工具,然后通过简短的 bash 命令执行