你的 MacBook 应该为你工作,而不是反过来。所以尽早利用手头的本地能力,把重复的任务和工作流自动化,非常重要。
本文要自动化的是:随简历一起发给公司的定制求职信。求职信会针对具体公司、职位和日期定制,复制一封求职信、改这几项、再另存为 PDF,在投几十份简历时就会非常耗时。
概述
我们会写一个 Bash 脚本和一个 JavaScript 脚本,用来定制并生成 PDF 版求职信,然后把这个脚本加到 Raycast 里,通过其图形界面调用。
说明: 必须用 JS 脚本,是因为仅用系统自带工具更新 .docx 时,生成的 PDF 要么失败,要么会丢失文档原有样式。
环境要求
安装步骤
为便于日后维护,在用户主目录下建一个文件夹 raycast-scripts/cover-letter,并进入该目录。
cd ~
mkdir raycast-scripts/cover-letter
cd raycast-scripts/cover-letter
接着安装下一步操作 docx 所需的依赖。
npm i pizzip docxtemplater
创建脚本
创建两个文件:cover-letter-local.sh 和 generate-cover-letter.mjs。给 shell 脚本加上可执行权限:
chmod +x cover-letter-local.sh
1. cover-letter-local.sh
这是 Raycast 调用的入口脚本。接收 Company(公司)和 Role(职位)两个参数,找到你的 DOCX 模板,运行 Node 脚本填充占位符,再通过 Pages(AppleScript)把填好的文档导出为 PDF。
#!/bin/bash
# @raycast.schemaVersion 1
# @raycast.title Generate Cover Letter
# @raycast.mode compact
# @raycast.packageName cover-letter
# @raycast.icon 📄
# @raycast.argument1 { "type": "text", "placeholder": "Company (e.g. Acme)" }
# @raycast.argument2 { "type": "text", "placeholder": "Role (e.g. Backend Engineer)" }
set -euo pipefail
COMPANY_NAME="$1"
ROLE_NAME="$2"
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
TEMPLATE_DOCX="$SCRIPT_DIR/JobSearch/YOUR_COVER_LETTER_FILE_NAME.docx"
OUTDIR="$HOME/Downloads/Cover Letters"
mkdir -p "$OUTDIR"
NEW_DATE="$(LC_TIME=en_US_POSIX date '+%b %e, %Y' | sed 's/ / /g')"
sanitize() {
echo "$1" \
| tr '\n' ' ' \
| sed -E 's/[\/:*?"<>|]/-/g; s/[[:space:]]+/ /g; s/^[[:space:]]+//; s/[[:space:]]+$//'
}
SAFE_COMPANY="$(sanitize "$COMPANY_NAME")"
BASENAME="YOUR_NAME_${SAFE_COMPANY}_Cover Letter"
TMPDOCX="/tmp/${BASENAME}.docx"
PDFPATH="${OUTDIR}/${BASENAME}.pdf"
if [[ ! -f "$TEMPLATE_DOCX" ]]; then
echo "DOCX template not found: $TEMPLATE_DOCX"
exit 1
fi
if [[ ! -d "$SCRIPT_DIR/node_modules" ]]; then
echo "Run once in this folder: npm install"
exit 1
fi
NODE=""
if command -v node &>/dev/null; then
NODE="node"
elif [[ -x "$HOME/.nvm/current/bin/node" ]]; then
NODE="$HOME/.nvm/current/bin/node"
elif [[ -x /opt/homebrew/bin/node ]]; then
NODE="/opt/homebrew/bin/node"
elif [[ -x /usr/local/bin/node ]]; then
NODE="/usr/local/bin/node"
fi
if [[ -z "$NODE" ]]; then
echo "node not found. Install Node.js or add it to PATH."
exit 1
fi
"$NODE" "$SCRIPT_DIR/generate-cover-letter.mjs" \
"$TEMPLATE_DOCX" "$TMPDOCX" "$COMPANY_NAME" "$ROLE_NAME" "$NEW_DATE"
osascript - "$TMPDOCX" "$PDFPATH" <<'APPLESCRIPT'
on run argv
set docxPosix to item 1 of argv
set pdfPosix to item 2 of argv
set docxAlias to (POSIX file docxPosix) as alias
set pdfFile to POSIX file pdfPosix
tell application "Pages"
activate
open docxAlias
delay 0.8
tell front document
export it to pdfFile as PDF
close saving no
end tell
quit
end tell
end run
APPLESCRIPT
rm -f "$TMPDOCX"
echo "Saved: $PDFPATH"
脚本说明:
- Raycast 元数据(
@raycast.*注释)定义命令名、图标和两个文本参数,Raycast 会显示「Company」和「Role」输入框。 - 路径:
TEMPLATE_DOCX指向子目录JobSearch/下的YOUR_COVER_LETTER_FILE_NAME.docx。请把YOUR_COVER_LETTER_FILE_NAME换成你的实际 DOCX 文件名(不含路径)。PDF 会输出到~/Downloads/Cover Letters。 - 日期: 使用当前日期,格式类似
Feb 11, 2025。 - Sanitize: 清理公司名用于文件名(去掉或替换文件名非法字符)。
- 命名:
BASENAME使用YOUR_NAME_,生成的文件类似YOUR_NAME_Acme_Cover Letter.pdf。请把YOUR_NAME_换成你的名字或缩写。 - Node 查找: 先试 PATH 里的
node,再试常见安装位置(nvm、Homebrew),这样从 Raycast 启动时(PATH 可能很精简)脚本也能运行。 - AppleScript: 在 Pages 中打开填好的 DOCX,导出为 PDF,不保存关闭文档并退出 Pages,最后删除临时 DOCX。
2. generate-cover-letter.mjs
这个 Node 脚本通过替换文档 XML 里的纯文占位符来填充 DOCX 模板。不使用 {COMPANY_NAME} 这种语法,只在 Word 里写 COMPANY_NAME、ROLE_NAME、NEW_DATE 这三个词即可。
#!/usr/bin/env node
/**
* Usage: node generate-cover-letter.mjs <template.docx> <output.docx> <company> <role> <date>
*/
import PizZip from "pizzip";
import fs from "fs";
import path from "path";
const [templatePath, outputPath, companyName, roleName, newDate] =
process.argv.slice(2);
const TEMPLATE = templatePath || process.env.TEMPLATE_PATH;
const OUTPUT = outputPath || process.env.OUTPUT_PATH;
const COMPANY = companyName || process.env.COMPANY_NAME || "";
const ROLE = roleName || process.env.ROLE_NAME || "";
const DATE = newDate || process.env.NEW_DATE || "";
if (!TEMPLATE || !OUTPUT) {
console.error(
"Usage: node generate-cover-letter.mjs <template.docx> <output.docx> <company> <role> <date>",
);
process.exit(1);
}
const templateAbs = path.resolve(TEMPLATE);
const outputAbs = path.resolve(OUTPUT);
if (!fs.existsSync(templateAbs)) {
console.error("Template not found:", templateAbs);
process.exit(1);
}
const content = fs.readFileSync(templateAbs, "binary");
const zip = new PizZip(content);
const docPath = "word/document.xml";
const docFile = zip.files[docPath];
if (!docFile) {
console.error("DOCX invalid: missing word/document.xml");
process.exit(1);
}
let xml = docFile.asText();
const replacements = [
["COMPANY_NAME", COMPANY],
["ROLE_NAME", ROLE],
["NEW_DATE", DATE],
];
for (const [placeholder, value] of replacements) {
const escaped = String(value)
.replace(/&/g, "&")
.replace(/</g, "<")
.replace(/>/g, ">")
.replace(/"/g, """)
.replace(/'/g, "'");
xml = xml.split(placeholder).join(escaped);
}
zip.file(docPath, xml);
const buf = zip.generate({ type: "nodebuffer" });
fs.writeFileSync(outputAbs, buf);
console.log(outputAbs);
说明:
- 把 DOCX 当 ZIP 读入,修改
word/document.xml后写出新 DOCX。只改文本节点,所以样式、字体、版式都会保留。 - 将 COMPANY_NAME、ROLE_NAME、NEW_DATE 替换为传入值(或环境变量)。会对值做 XML 转义,避免
&、<等字符破坏文件。
3. 准备 DOCX 模板
- 在 Word 中写好求职信(或用 Google Docs,再 文件 → 下载 → Microsoft Word (.docx))。
- 要显示公司名的地方输入 COMPANY_NAME(不要花括号)。
- 要显示职位的地方输入 ROLE_NAME。
- 要显示日期的地方输入 NEW_DATE。
- 保存文件并放到
cover-letter-local.sh期望的位置,例如raycast-scripts/cover-letter/JobSearch/YOUR_COVER_LETTER_FILE_NAME.docx。在脚本里把YOUR_COVER_LETTER_FILE_NAME换成你的实际 DOCX 文件名,YOUR_NAME_换成你的名字或缩写,这样导出的文件名才符合你的需求。
4. 在 Raycast 中添加脚本
- 打开 Raycast → Settings → Extensions。
- 点击 Add Script Directory,选择
~/raycast-scripts/cover-letter(或存放cover-letter-local.sh和generate-cover-letter.mjs的目录)。 - Raycast 会根据
@raycast.*注释发现脚本,你应能在 cover-letter 包下看到 Generate Cover Letter。 - 运行:输入公司和职位,回车。脚本会生成填好的 DOCX,经 Pages 导出为 PDF,并保存到
~/Downloads/Cover Letters。
这样你就有了一步到位的流程:在 Raycast 里输入公司 + 职位 → 得到定制好的求职信 PDF,无需手动查找替换或导出。
祝你拿到心仪的 offer。