1. 深夜自动巡检这件事,为什么值得用 Codex Skills 来做
很多项目的代码检查、依赖审计、备份归档,白天根本没空跑。CI 只在 push 时触发,覆盖不了“代码没变但环境变了”的场景;手动跑一遍又容易忘。我试过把这几件事串成一个 Codex Skill,交给 Cron 在每天凌晨两点触发,跑完把报告和备份落到本地目录,第二天上班直接看结果。
Codex Skills 的本质是把一段可复用的操作流程封装成模型能调用的能力单元。它和普通脚本的区别在于:脚本只执行固定命令,Skill 可以让模型根据当前仓库状态决定先检查什么、报告怎么写、异常怎么归类。Cron 负责“什么时候跑”,Skill 负责“跑的时候做什么决策”,两者组合起来就是一套无人值守的巡检链路。
这套方案适合谁:手里有 1 到 N 个本地或内网 Git 仓库、希望每天有一份代码质量快照、又不想引入重型 CI 的独立开发者和小团队。它不替代 CI,而是补上 CI 覆盖不到的“定时体检”这一环。整条链路里模型调用统一走 TaoToken 的 API 通道,Key 和 Base URL 只配一次,后面所有 Skill 复用同一个入口,省得每个脚本各配一套。
下面按“先接通道、再写 Skill、再挂 Cron、最后手动验证”的顺序展开,每一步都给可复制的配置和命令。
2. 用 TaoToken 统一模型通道,给 Codex Skills 备好 Key 与 Base URL
Codex Skills 在执行代码检查时,需要调用模型来生成报告摘要、归类 lint 错误、判断哪些问题值得优先修。如果每个 Skill 各自维护一套模型配置,换模型或换 Key 时就要改多处。TaoToken 的作用是把模型调用收敛到一个 API 入口,Codex 侧只认一个 Base URL 和一个 Key。
先到控制台创建 API Key。打开 https://taotoken.net/console ,登录后在 API Keys 页面新建一个 Key,复制出来保存好。这个 Key 后面会写进 Codex 的配置文件,不要提交到 Git 仓库。
模型对话入口可以用来先验证 Key 是否可用: https://taotoken.net/models 。在里面发一条测试消息,能正常返回就说明 Key 和通道没问题。
Codex 侧的配置分两种常见形态。如果你用的是 Codex CLI 的 auth.json 方式,配置长这样:
{ "base_url": "https://taotoken.net/api", "api_key": "sk-你的TaoTokenKey", "model": "gpt-4o" }如果你用的是 settings 风格的 TOML 配置,对应片段是:
[model] provider = "taotoken" base_url = "https://taotoken.net/api" api_key = "sk-你的TaoTokenKey" model_id = "gpt-4o"三件套必须齐全:Base URL 填https://taotoken.net/api,Key 填控制台生成的,Model ID 填你要用的模型名。少任何一个,Skill 在调用模型时都会报 401 或 model not found。
如果你同时用 Cline 或 Claude Code 这类工具,它们的 MCP 配置里也是同样的三件套。以 Cline 的 MCP 配置为例:
{ "mcpServers": { "taotoken": { "command": "npx", "args": ["-y", "@taotoken/mcp-server"], "env": { "TAOTOKEN_BASE_URL": "https://taotoken.net/api", "TAOTOKEN_API_KEY": "sk-你的TaoTokenKey", "TAOTOKEN_MODEL": "gpt-4o" } } } }配好之后,Codex Skills 里所有模型调用都走这个通道。接入文档在 https://taotoken.net/doc ,里面有各客户端的详细字段说明,遇到字段对不上时以文档为准。
这一步做完,你手里应该有一个可用的 Key、一个确认能通的 Base URL、一个确定的 Model ID。接下来写 Skill 时直接引用这三个值。
3. 可复制的 Codex Skill 配置与 Cron 表达式
Codex Skill 的落地形态通常是一个目录加一个描述文件。我们在项目根目录下建.codex/skills/nightly-inspect/,里面放skill.toml和run.py。
skill.toml定义这个 Skill 的名字、触发方式和它要执行的入口:
[skill] name = "nightly-inspect" description = "每天深夜执行代码检查、生成报告并备份仓库" entry = "run.py" timeout_seconds = 1800 [model] base_url = "https://taotoken.net/api" api_key_env = "TAOTOKEN_API_KEY" model_id = "gpt-4o" [schedule] cron = "0 2 * * *" timezone = "Asia/Shanghai"注意api_key_env这一项:Key 不直接写进文件,而是从环境变量读。这样 Skill 配置可以进版本库,Key 留在机器上。启动前 export 一下:
export TAOTOKEN_API_KEY="sk-你的TaoTokenKey"run.py是 Skill 的实际逻辑,负责串起检查、报告、备份三步:
import os import subprocess import datetime import json import urllib.request REPO_DIR = os.path.expanduser("~/my_project") BACKUP_DIR = os.path.expanduser("~/backups/my_project") REPORT_DIR = os.path.expanduser("~/reports/nightly") BASE_URL = "https://taotoken.net/api" API_KEY = os.environ["TAOTOKEN_API_KEY"] MODEL_ID = "gpt-4o" def run_lint(): result = subprocess.run( ["flake8", ".", "--max-line-length=120", "--exclude=__pycache__,.git,venv", "--statistics"], cwd=REPO_DIR, capture_output=True, text=True ) return result.returncode, result.stdout def summarize_with_model(lint_output): payload = json.dumps({ "model": MODEL_ID, "messages": [ {"role": "system", "content": "你是代码审查助手,把 lint 结果归纳成三条以内的重点。"}, {"role": "user", "content": lint_output[:4000]} ] }).encode() req = urllib.request.Request( f"{BASE_URL}/v1/chat/completions", data=payload, headers={ "Content-Type": "application/json", "Authorization": f"Bearer {API_KEY}" } ) with urllib.request.urlopen(req, timeout=120) as resp: data = json.loads(resp.read()) return data["choices"][0]["message"]["content"] def backup(): os.makedirs(BACKUP_DIR, exist_ok=True) ts = datetime.datetime.now().strftime("%Y%m%d_%H%M%S") target = os.path.join(BACKUP_DIR, f"repo_{ts}.tar.gz") subprocess.run( ["tar", "czf", target, "--exclude=.git", "--exclude=venv", "-C", os.path.dirname(REPO_DIR), os.path.basename(REPO_DIR)], check=True ) return target def main(): os.makedirs(REPORT_DIR, exist_ok=True) code, lint_out = run_lint() summary = summarize_with_model(lint_out) if lint_out else "无 lint 输出" backup_path = backup() ts = datetime.datetime.now().strftime("%Y%m%d_%H%M%S") report = os.path.join(REPORT_DIR, f"report_{ts}.md") with open(report, "w", encoding="utf-8") as f: f.write(f"# 夜间巡检报告 {ts}\n\n") f.write(f"lint 返回码: {code}\n\n") f.write(f"## 模型归纳\n\n{summary}\n\n") f.write(f"## 备份文件\n\n{backup_path}\n") print(f"报告已生成: {report}") if __name__ == "__main__": main()Cron 表达式0 2 * * *表示每天凌晨两点整触发。五个字段依次是分、时、日、月、周。如果你想改成凌晨三点半,写成30 3 * * *。想只在工作日跑,把第五个字段改成1-5。
挂到系统 Crontab 里:
crontab -e加入一行:
0 2 * * * cd /path/to/project && /usr/bin/python3 .codex/skills/nightly-inspect/run.py >> ~/logs/nightly.log 2>&1这里用cd切到项目目录再执行,保证 Skill 里的相对路径能解析。日志重定向到~/logs/nightly.log,方便第二天排查。
如果你不想用系统 Crontab,也可以用 Codex 自带的调度能力,把skill.toml里的[schedule]段交给 Codex 运行时处理。两种方式选一种即可,不要同时挂,否则同一时间会跑两次。
4. 手动触发一次,验证整条链路是否跑通
配置写完不要直接等凌晨两点。先手动跑一次,确认 Skill 能调通模型、能生成报告、能落备份。
第一步,确认环境变量已导出:
echo $TAOTOKEN_API_KEY有输出说明 Key 在环境里。如果为空,重新 export 一次。
第二步,手动执行 Skill:
cd /path/to/project python3 .codex/skills/nightly-inspect/run.py预期输出类似:
报告已生成: /home/user/reports/nightly/report_20250610_143022.md第三步,检查报告内容:
cat ~/reports/nightly/report_20250610_143022.md报告里应该有三块:lint 返回码、模型归纳的要点、备份文件路径。如果模型归纳那一段是空的,说明模型调用没成功,往下看排障部分。
第四步,确认备份文件存在且大小合理:
ls -lh ~/backups/my_project/应该能看到一个repo_时间戳.tar.gz,大小和你的项目体积相当。如果只有几 KB,可能是 tar 的-C路径写错了,打包了个空目录。
第五步,验证 Cron 是否真的会触发。不用等到凌晨,把表达式临时改成下一分钟,比如当前是 14:35,就改成36 14 * * *,等一分钟看日志:
tail -f ~/logs/nightly.log看到报告生成的那行输出,说明 Cron 链路也通了。验证完记得把表达式改回0 2 * * *。
这一步跑通后,整条链路就是:Cron 到点触发 → Skill 执行 → 调 TaoToken 通道让模型归纳 → 生成报告 → 打包备份。四个环节任何一个断了,报告都不会完整生成。
5. 常见报错排查:401、local proxy failed、reading choices、OAuth
实际跑的时候,报错基本集中在这几类。逐个说现象和修法。
401 Unauthorized。模型调用返回 401,说明 Key 没被正确带上或已失效。先确认TAOTOKEN_API_KEY环境变量在 Cron 执行时也存在——Cron 的环境变量和你的 shell 不是同一套,经常是这里丢的。修法是在 Crontab 行里显式带上:
0 2 * * * export TAOTOKEN_API_KEY="sk-你的Key" && cd /path/to/project && python3 .codex/skills/nightly-inspect/run.py >> ~/logs/nightly.log 2>&1或者把 Key 写进一个env文件,Cron 里 source 一下。确认 Key 本身有效,可以去 https://taotoken.net/api-keys 重新生成一个再试。
local proxy failed。这个报错通常出现在请求根本没发出去的时候,比如 Base URL 写成了https://taotoken.net/api/带了尾斜杠,或者网络层有额外的转发配置干扰。先检查skill.toml和run.py里的 Base URL 是否严格是https://taotoken.net/api,不带尾斜杠。再确认机器能直接访问这个地址:
curl -I https://taotoken.net/api能返回 HTTP 状态码说明网络通。如果 curl 也失败,检查本机的 DNS 和出站规则。
reading choices 相关报错。这类报错说明请求发出去了、也返回了,但返回体里没有choices字段,代码在解析data["choices"][0]时崩了。常见原因是模型名写错,服务端返回了一个错误对象而不是正常响应。修法是打印原始返回体看看到底返回了什么:
print(resp.read().decode())把model_id改成控制台里确认可用的模型名,再跑一次。另外注意run.py里请求路径是/v1/chat/completions,如果 Base URL 已经带了/v1,这里就不要重复。
OAuth 相关报错。如果你用的是 Codex CLI 且之前登录过某个账号,它可能优先走 OAuth 而不是 API Key。现象是报错里出现 token 过期或授权失败。修法是显式指定用 API Key 模式,在 auth.json 里确保api_key字段有值,并且没有残留的 OAuth token 字段。或者干脆清掉旧的认证缓存,重新用 Key 配置一次。
排查时记住一个顺序:先确认 Key 在环境里,再确认 Base URL 没写错,再确认模型名对,最后看返回体原文。四步走完,绝大多数报错都能定位。
6. 把巡检链路固定下来,后续怎么扩展
链路跑通之后,日常维护其实很轻。每天上班看一眼~/reports/nightly/下最新的报告,重点看模型归纳的那三条。备份文件按时间戳堆积,可以再加一个清理 Skill,只保留最近 30 天的备份。
扩展方向有几个。一是把报告推送到你的协作工具,在run.py末尾加一个 webhook 调用即可。二是把单仓库扩展成多仓库,把REPO_DIR改成列表循环。三是让 Skill 在 lint 错误超过阈值时自动开一个 issue,这需要你的代码托管平台有对应的 API。
模型调用这块,长期跑建议用 Coding Plan,额度更稳,适合这种每天固定触发的场景: https://taotoken.net/coding-plan 。如果只是偶尔跑跑,按量走 API 也够用。
最后提醒一句:Cron 任务失败时默认是静默的,所以日志重定向那一步别省。有了日志,第二天花十秒就能确认昨晚跑没跑、跑到哪一步断的。这套东西的价值不在于多复杂,而在于它每天准时出现,你不用记着它。