Skill 已被调用,却报 ENOENT,不应该先重写 description。本例接续任务状态汇总 Skill,新增 Node.js 20+ 脚本,把资源与输入路径的来源分开。错误与数据都是教学案例。
固定目录与失败证据
在原练习项目添加:
skill-demo/
.agents/skills/task-status-summary/
SKILL.md
references/statuses.json
scripts/run.mjs
input/tasks.json
references/statuses.json 的完整内容:
["done","todo"]
如果 scripts/run.mjs 用以下写法读取资源:
fs.readFileSync('references/statuses.json', 'utf8');
从项目根目录执行时,它会找项目根下的 references,而不是 Skill 内的 references。记录实际工作目录、执行命令和错误路径,就能验证这个假设。文件确实存在但路径算错,与 Skill 没触发是不同问题。
完整修复脚本
将下面保存为该 Skill 的 scripts/run.mjs。资源相对于脚本定位,用户输入则相对于调用时的工作目录解析;两者不能混用。
import fs from 'node:fs';
import path from 'node:path';
try {
if (process.argv.length !== 3) throw new Error('用法:node run.mjs <输入JSON>');
const allowed = JSON.parse(fs.readFileSync(
new URL('../references/statuses.json', import.meta.url), 'utf8'));
if (!Array.isArray(allowed) || allowed.join(',') !== 'done,todo') {
throw new Error('资源必须声明 done,todo');
}
const inputPath = path.resolve(process.argv[2]);
const rows = JSON.parse(fs.readFileSync(inputPath, 'utf8'));
if (!Array.isArray(rows)) throw new Error('输入顶层必须为数组');
const ids = new Set();
const result = { total: rows.length, done: 0, todo: 0, todoIds: [] };
for (const [index, row] of rows.entries()) {
if (!row || typeof row.id !== 'string' || !row.id.trim() ||
typeof row.title !== 'string' || !row.title.trim()) {
throw new Error('记录 ' + (index + 1) + ' 缺少有效 id/title');
}
if (ids.has(row.id)) throw new Error('重复 id: ' + row.id);
if (!allowed.includes(row.status)) throw new Error('未知 status: ' + row.id);
ids.add(row.id);
result[row.status]++;
if (row.status === 'todo') result.todoIds.push(row.id);
}
console.log(JSON.stringify(result));
} catch (error) {
console.error(error.message);
process.exitCode = 2;
}
SKILL.md 中增加脚本调用要求:从用户指定的项目目录执行该 Skill 脚本,传入用户指定输入文件;脚本退出码非零时报告错误,不继续生成统计。保留第一篇的输入和用途边界,不改宽触发描述来掩盖路径问题。
用同一样本从两个目录运行
input/tasks.json 使用第一篇三条任务样本。先在项目根运行:
node .agents/skills/task-status-summary/scripts/run.mjs input/tasks.json
再进入 input 目录运行:
Set-Location input
node ../.agents/skills/task-status-summary/scripts/run.mjs tasks.json
Set-Location ..
两次预期输出相同:
{"total":3,"done":1,"todo":2,"todoIds":["T-02","T-03"]}
同一个脚本、同一资源没有随当前目录变化,输入路径则由调用者正确给出。不要把输入硬编码为个人电脑绝对路径,否则“在我这里能跑”仍可能掩盖可移植性问题。
失败分支也要回归
| 输入或环境 | 预期 |
|---|---|
| 三条正常任务 | 输出 3/1/2,退出码 0 |
| [] | 输出 0/0/0 和空 todoIds |
| T-02 的 status 为 doing | 标准错误提示未知 status,退出码 2,无成功统计 |
| 重复 id | 指出重复 id,退出码 2 |
| 输入文件不存在 | 文件读取错误,退出码 2 |
| statuses.json 被移走 | 资源路径错误,退出码 2;不能默认放行所有状态 |
PowerShell 在运行后用 $LASTEXITCODE 查看 Node 退出码。先确认脚本层满足这些预期,再回到 Codex 显式调用 Skill,检查它真的用了正确脚本并保留错误信息。
如果显式调用成功、普通表达不触发,再单独检查 name/description 与任务是否匹配;如果输入未指定,应先索取路径。将每次修改限制在证据指向的一层,才能知道到底修好了什么。
资料来源
- OpenAI Skills
核对 Skill 的说明、脚本和资源组织。
- Node.js URL
使用 import.meta.url 解析脚本相邻资源。
下一步
继续阅读与你当前问题相关的教程:


