[
 {
  "id": "T01",
  "target": "ai_session_search_archive",
  "model": "deepseek-v4.1-flash",
  "condition": "full_all",
  "answer": "不能直接拿几周前的旧文件列表跑，这正是会漏结果的最典型场景。按 `skill_semantic_search` 的缓存契约，文件列表同时是**结果白名单**：列表里没有的文件，即使缓存里有它的向量，也永远不会被返回。\n\n具体要做的：\n\n**1. 查询时重新生成文件列表，不要复用旧的**\n\n```bash\nFILELIST=\"$(mktemp)\"\ntrap 'rm -f \"$FILELIST\"' EXIT\nrg --files contexts/ai_sessions/{opencode,claude_code,codex,antigravity,second_mind}/ \\\n  -g '*.md' > \"$FILELIST\"\n```\n\n几周里新增的会话文件不在旧列表里，用旧列表就等于把它们排除在检索范围外。\n\n**2. 路径解析要对齐**\n\nCLI 会把列表里的每一项 canonicalize 成绝对路径再查缓存。相对路径按 `--source-root` 解析，没给就按 CLI 当前工作目录解析——**不是**生成列表时所在的目录。如果列表是几周前在别的目录用 `find` 生成的相对路径，现在从另一个目录跑，解析出的路径和缓存里的身份对不上，查询会**静默返回空结果，不报错**。\n\n所以要么列表里直接写绝对路径，要么显式传 `--source-root` 指向当初生成列表的目录。\n\n**3. 查询用 `--no-refresh` 做纯读**\n\n```bash\nsemantic-search query \\\n  --file-list \"$FILELIST\" \\\n  --cache-dir .knowledge_cache \\\n  --query '你记得的大概内容' \\\n  --top-k 10 \\\n  --no-refresh\n```\n\n`--no-refresh` 只读现有缓存，不会触发重建。缓存里已有的向量会被复用，缺向量的新文件这次查不到——但至少不会因为旧列表而漏掉本来能命中的。\n\n**4. 如果确实需要覆盖新文件**\n\n先重建索引再查：\n\n```bash\nsemantic-search rebuild --file-list \"$FILELIST\" --cache-dir .knowledge_cache --workers 64\n```\n\n**5. 顺手体检一下缓存**\n\n```bash\nsemantic-search doctor --cache-dir .knowledge_cache\nsemantic-search stats --cache-dir .knowledge_cache\n```\n\n`doctor` 会报 inactive chunks 和 orphan segment 文件。注意：小更新是追加新 segment、把旧 chunk 标记为 inactive，查询会忽略它们，但物理上还在磁盘上——所以\"缓存里有向量\"和\"这次查询能命中\"是两回事。\n\n**关于\"会不会漏结果\"的直接回答**：会，而且有两种漏法。一是旧列表把新会话排除在白名单外；二是相对路径解析错位导致整个查询静默返回空。两者都不报错，所以别用\"没报错\"当\"没漏\"。判断依据只能是：列表是本次查询时新生成的、路径是绝对路径或 `--source-root` 对齐的。\n\n另外，如果这个归档有每日导出管线（比如 cron 每天早上重导并重建语义索引），先查一下归档目录里有没有现成的、比手头这份更新的导出，直接读它，别自己再跑一遍 `export` 造重复副本。",
  "error": null,
  "n_skills": 139,
  "target_pos": 29,
  "prompt_tokens": 336208,
  "completion_tokens": 694,
  "sec": 11.62732481956482,
  "cost": 0.0508476,
  "web_tools": [],
  "n_calls": 1,
  "finish_reason": "stop"
 },
 {
  "id": "T02",
  "target": "compressor",
  "model": "deepseek-v4.1-flash",
  "condition": "full_all",
  "answer": "`-format json` 必须紧跟在 `-monitor` 后面，不能放在命令末尾。\n\n正确写法：\n\n```bash\n\"/Applications/Compressor.app/Contents/MacOS/Compressor\" \\\n  -monitor \\\n  -format json \\\n  -batchid \"<batch_id>\" \\\n  -once\n```\n\n放在末尾（比如 `-monitor -batchid ... -once -format json`）会报 `Invalid parameter: -format`。\n\n另外提醒两点：\n\n- 拿到 `batchID` 只说明批次已入队，不代表真的在编码。用 `-monitor` 确认 `percentComplete` / `timeElapsedSeconds` 在增长，并对照 `~/Library/Logs/Compressor/servicecontroller:com.apple.stomp.transcoder.stomp.log` 里的 `starting _processRequest` / `Done _processRequest` 记录。\n- 如果状态停在 `Processing 0%` 或 `Waiting` 且 `timeElapsed` 一直是 0，通常是后台转码服务没有空闲实例（GUI 里可能显示 `No service available for processing this job`），不是源文件的问题。",
  "error": null,
  "n_skills": 139,
  "target_pos": 55,
  "prompt_tokens": 336202,
  "completion_tokens": 244,
  "sec": 4.665614128112793,
  "cost": 0.050576699999999995,
  "web_tools": [],
  "n_calls": 1,
  "finish_reason": "stop"
 },
 {
  "id": "T03",
  "target": "deployment_github_actions_koyeb",
  "model": "deepseek-v4.1-flash",
  "condition": "full_all",
  "answer": "在 `koyeb/action-git-deploy@v1` 那一步里加 `git-builder` 入参，值填 `docker`：\n\n```yaml\n- name: Deploy\n  uses: koyeb/action-git-deploy@v1\n  with:\n    app-name: \"<app-name>\"\n    service-name: \"<service-name>\"\n    git-branch: \"master\"\n    git-builder: \"docker\"                  # ← 强制按容器构建\n    git-docker-dockerfile: \"Dockerfile\"    # ← 同时显式指定 Dockerfile 路径\n    service-regions: \"na\"\n    service-ports: \"8000:http\"\n    service-routes: \"/:8000\"\n```\n\n两个要点：\n\n1. **`git-builder: \"docker\"`** 是解决你这个问题的关键。不填时 Koyeb 会检测到仓库里的 Python/Node 文件而回退到 buildpack，日志里会出现 `Installing Python 3.x` 之类的行，而不是 Docker 构建。\n2. **`git-docker-dockerfile: \"Dockerfile\"`** 一并显式给出（相对仓库根目录的路径），避免它去找默认位置。\n\n另外确认 Koyeb 控制台里该服务的 **Autodeploy 已关闭**，部署时机交给 GitHub Actions 控制；否则可能出现重复部署。",
  "error": null,
  "n_skills": 139,
  "target_pos": 100,
  "prompt_tokens": 336214,
  "completion_tokens": 300,
  "sec": 4.878364086151123,
  "cost": 0.0506121,
  "web_tools": [],
  "n_calls": 1,
  "finish_reason": "stop"
 },
 {
  "id": "T04",
  "target": "mac_universal_clipboard",
  "model": "deepseek-v4.1-flash",
  "condition": "full_all",
  "answer": "根据 `mac_universal_clipboard` skill，直接给你完整命令。\n\n## 完整命令\n\n```bash\ndefaults delete ~/Library/Preferences/com.apple.coreservices.useractivityd.plist ClipboardSharingEnabled 2>/dev/null; \\\ndefaults write ~/Library/Preferences/com.apple.coreservices.useractivityd.plist ClipboardSharingEnabled 1; \\\nkillall useractivityd 2>/dev/null; \\\nkillall sharingd 2>/dev/null; \\\nkillall pboard 2>/dev/null; \\\nsleep 2; \\\npgrep -fl 'useractivityd|sharingd|pboard'\n```\n\n## 命令里各部分的含义\n\n**要修改的具体偏好项位置：**\n- 文件：`~/Library/Preferences/com.apple.coreservices.useractivityd.plist`\n- 键名：`ClipboardSharingEnabled`\n- 操作：先 `delete` 清掉可能损坏的旧值，再 `write ... 1` 重写为开启\n\n**需要重启的所有相关后台进程（三个）：**\n- `useractivityd` — 管 Handoff / Continuity activity 状态，包括 Universal Clipboard 偏好和共享剪贴板 blob\n- `sharingd` — 管附近设备发现和 sharing transport\n- `pboard` — 管本地 pasteboard 状态\n\n这三个被 `killall` 后会被 launchd 自动拉起。最后 `sleep 2` 等它们重启，`pgrep -fl` 确认它们重新出现在进程列表里。\n\n## 执行后\n\n让用户测试两个方向：iPhone/iPad → Mac，以及 Mac → iPhone/iPad。\n\n## 如果这条命令无效\n\nskill 里给的升级顺序（按成本从低到高）：\n\n1. 在 Mac 的 System Settings → General → AirDrop & Handoff，以及 iOS/iPadOS 的 Settings → General → AirPlay & Handoff 里关开 Handoff。\n2. 让 Mac 进一次 Safe Mode 再正常重启（会清理一批系统缓存）。\n3. 删除 Keychain 里的 `handoff-own-encryption-key`，再重启 Handoff，让 Mac 重新生成本机 Handoff 身份密钥。\n4. 退出 iCloud 再登录（最后手段，会触发 iCloud Drive、Photos、Keychain 重新同步）。\n\n另外：如果这台 Mac 跑的是 macOS beta，快速修复失败后优先按系统回归判断，不要反复叠加更重的本地改动。\n\n## 可选诊断（只在快速修复失败或想确认根因时做）\n\n```bash\nsw_vers\ndefaults read ~/Library/Preferences/com.apple.coreservices.useractivityd.plist ClipboardSharingEnabled 2>/dev/null\npgrep -fl 'pboard|useractivityd|sharingd|bluetoothd'\nls -la ~/Library/Group\\ Containers/group.com.apple.coreservices.useractivityd/shared-pasteboard/ 2>/dev/null\n/usr/bin/log show --predicate 'process == \"useractivityd\"' --last 30m --style compact 2>/dev/null | tail -30\n/usr/bin/log show --predicate 'process == \"sharingd\"' --last 30m --style compact 2>/dev/null | tail -30\n```\n\n注意在 zsh 里用 `/usr/bin/log` 而不是裸 `log`，因为某些环境里 `log` 可能被解析成 shell builtin 或 function。",
  "error": null,
  "n_skills": 139,
  "target_pos": 82,
  "prompt_tokens": 336214,
  "completion_tokens": 744,
  "sec": 5.8745527267456055,
  "cost": 0.0508785,
  "web_tools": [],
  "n_calls": 1,
  "finish_reason": "stop"
 },
 {
  "id": "T05",
  "target": "openreview",
  "model": "deepseek-v4.1-flash",
  "condition": "full_all",
  "answer": "你描述的是 OpenReview 的登录 rate limit（每个时间窗口最多 3 次登录），不是账号问题。恢复方法：\n\n**删掉本地 token 缓存，然后重试一次：**\n\n```bash\nrm tmp/openreview_token.json\n```\n\n然后重新执行原来的命令即可。CLI 会自动重新登录并缓存新 token。\n\n**关键约束：**\n\n- **只重试一次**。如果删掉 token 后第一次重试仍然 403，说明当前时间窗口的登录配额还没恢复，继续重试只会加深限流。等一个窗口过去再试。\n- 不要反复删 token 反复重试——每次删除都会触发一次新的 login，正好撞在限流上。\n- 正常使用不会触发这个限制，因为 CLI 已经做了 token 缓存；只有 token 过期导致 403 时才会走到重新登录路径。\n\n**如果删 token 后仍持续 403：**\n\n- 确认 `.env` 里的 `openreview_email` / `openreview_password`（或环境变量 `OPENREVIEW_EMAIL` / `OPENREVIEW_PASSWORD`）没有变化或失效。\n- 确认没有其他进程/脚本在并发登录同一个账号——并发登录会快速耗尽 3 次/窗口的配额。\n- 等待一个完整的限流窗口后再试，不要在此期间做任何登录操作。\n\n**预防：**\n\n- 批量任务（如 `papers` → 提取 authorids → `profiles`）尽量在一次会话内完成，复用同一个 token。\n- 不要为每个子任务单独启动一次登录。",
  "error": null,
  "n_skills": 139,
  "target_pos": 64,
  "prompt_tokens": 336200,
  "completion_tokens": 330,
  "sec": 4.6933581829071045,
  "cost": 0.05062799999999999,
  "web_tools": [],
  "n_calls": 1,
  "finish_reason": "stop"
 },
 {
  "id": "T06",
  "target": "workflow_bilibili_whisper_transcription",
  "model": "deepseek-v4.1-flash",
  "condition": "full_all",
  "answer": "根据你提供的 skill 文档，B 站批量下载的稳妥做法是**单线程 + 每次下载之间间隔 2-3 秒**，并配合 User-Agent / Referer headers。\n\n具体依据（来自 `workflow_bilibili_whisper_transcription.md`）：\n\n- **下载并发**：单线程（不要并行），原因是避免触发 B 站反爬机制。\n- **下载间隔**：逐个下载音频，**每次间隔 2-3 秒**。\n- **请求头**：添加 User-Agent / Referer headers。\n- **只下载音频**，并记录失败 ID。\n\n另外，`download_and_transcribe.md` 里还记录了一个更顽固的情况：即使间隔和 cookie 都正常，仍可能遇到 **HTTP 412**。这时文档给出的判断是——412 往往不是 cookie 坏了，而是 B 站风控在拦客户端指纹（yt-dlp 的请求指纹被拒），而同样的匿名 cookie 用 curl 却能拿到 200。对应的恢复路线是绕开 yt-dlp，直接用 curl 走 HTML5 playurl 通道（`platform=html5&high_quality=1&fnval=0`），并带上浏览器 UA 和 `Referer: https://www.bilibili.com/`。这条路线画质上限 720p，适合做 ASR 输入，不适合存档级音频。\n\n所以稳妥的默认值是 **2-3 秒间隔 + 单线程 + 正确 headers**；如果这样还被 412 拦，就说明是指纹层面的风控，需要换 curl 路线，而不是继续加大间隔。",
  "error": null,
  "n_skills": 139,
  "target_pos": 23,
  "prompt_tokens": 336193,
  "completion_tokens": 345,
  "sec": 5.055094003677368,
  "cost": 0.05063595,
  "web_tools": [],
  "n_calls": 1,
  "finish_reason": "stop"
 },
 {
  "id": "T07",
  "target": "workflow_watchdog",
  "model": "deepseek-v4.1-flash",
  "condition": "full_all",
  "answer": "默认隔 **约 30 分钟（1800 秒）** 醒一次。\n\n调度动作按 harness 分：\n\n- **Claude Code harness**：用 `ScheduleWakeup` 设这个巡检唤醒。\n- **其他 harness**：用对应的定时机制，比如 Process Launcher 的延时执行（`POST /run` 带 `delay_seconds: 1800`）。\n\n醒来后自己判断两种情况，不用问用户：\n\n1. **真在忙**（有新输出、agent 在持续产出）→ 不管它，继续等或再设一次 wake-up。\n2. **鬼打墙卡住**（长时间无进展、同一步骤反复重试）→ kill 掉（Claude Code 用 `TaskStop`），用已有的部分结果推进，或换方法重做。\n\n这套做法来自 `workflow_watchdog`：派出长时间运行的 workflow / 后台 agent / 批量 sub-agent 之后，把\"派出后设巡检\"当成默认动作，而不是出问题后的补救。巡检和 kill 属于技术编排决策，自己推到底即可。",
  "error": null,
  "n_skills": 139,
  "target_pos": 81,
  "prompt_tokens": 336205,
  "completion_tokens": 230,
  "sec": 5.254735946655273,
  "cost": 0.050568749999999996,
  "web_tools": [],
  "n_calls": 1,
  "finish_reason": "stop"
 },
 {
  "id": "T08",
  "target": "workflow_parallel_subagents",
  "model": "deepseek-v4.1-flash",
  "condition": "full_all",
  "answer": "不行。`subagent_type` 必须是**已注册的 agent 名**，不是模型名。`deepseek/deepseek-v4-pro` 是 `provider/model` 形式的模型入口，直接传给 `functions.task` 会报 `Unknown agent type`。\n\n## 为什么\n\nOpenCode 执行的是 `agent.get(subagent_type)`，找不到同名 agent 就失败。模型和 agent 是两个命名空间：\n\n- `provider/model`（如 `deepseek/deepseek-v4-pro`）只是底层模型入口\n- 要能被 `task` 调用，必须**包成 agent**，例如 `writer_deepseek -> deepseek/deepseek-v4-pro`\n\n## 正确做法\n\n### 1. 用已注册的 agent 名\n\n当前可用的 `subagent_type` 里，DeepSeek Pro 路线对应的是：\n\n```\nwriter_deepseek\n```\n\n它适合中文写作、改稿、风格润色、最终 prose polishing；避免高隐私材料。\n\n调用形态：\n\n```json\n{\n  \"recipient_name\": \"functions.task\",\n  \"parameters\": {\n    \"description\": \"中文改稿\",\n    \"subagent_type\": \"writer_deepseek\",\n    \"prompt\": \"……\",\n    \"task_id\": \"\",\n    \"command\": \"\"\n  }\n}\n```\n\n### 2. 如果确实需要一个新的模型入口，先注册成 agent\n\n在以下任一位置定义 agent，把模型绑上去：\n\n- `~/.config/opencode/opencode.json`\n- 项目 `opencode.json`\n- `.opencode/agent(s)/*.md`\n\n注册后，用**你给这个 agent 起的名字**作为 `subagent_type`，而不是模型字符串。\n\n### 3. 并行调用方式\n\n多个 subagent 必须在**同一条 assistant 消息**里用 `multi_tool_use.parallel` 一次性发出，否则即使文字上说\"并行\"，实际也是串行：\n\n```json\n{\n  \"tool_uses\": [\n    {\"recipient_name\": \"functions.task\", \"parameters\": {\"description\": \"任务A\", \"subagent_type\": \"writer_deepseek\", \"prompt\": \"……\", \"task_id\": \"\", \"command\": \"\"}},\n    {\"recipient_name\": \"functions.task\", \"parameters\": {\"description\": \"任务B\", \"subagent_type\": \"reasoning_gpt\", \"prompt\": \"……\", \"task_id\": \"\", \"command\": \"\"}}\n  ]\n}\n```\n\n## 顺带提醒\n\n- 不要用旧的 `category=\"deep\"` / `category=\"writing\"` / `librarian` 这类路由名，除非它们已在当前 `opencode.json` 里注册成同名 agent。\n- 不要用 `mcp_task(run_in_background=True)`，当前工具没有这个参数。\n- 如果任务涉及高隐私材料，`writer_deepseek` 不合适，改用 `private_ds4`（本地）或 `ollama_deepseek_pro`（zero-data-retention 云路线）。",
  "error": null,
  "n_skills": 139,
  "target_pos": 124,
  "prompt_tokens": 336203,
  "completion_tokens": 641,
  "sec": 5.529134035110474,
  "cost": 0.050815049999999994,
  "web_tools": [],
  "n_calls": 1,
  "finish_reason": "stop"
 },
 {
  "id": "T09",
  "target": "codex_cli",
  "model": "deepseek-v4.1-flash",
  "condition": "full_all",
  "answer": "在 Codex 配置里彻底关掉 `web_search`，写法是：\n\n```toml\nweb_search = \"disabled\"\n```\n\n**关键点：**\n\n1. **值必须是字符串 `\"disabled\"`，不能用布尔值。** 在 Codex CLI 0.156.1 上，`false`、`on`、`off`、`enabled` 全都会被拒绝，报错 `expected string only in 'web_search'`。这是 Rust 的 string unit variant，只认 `\"disabled\"` 这一个字面量。\n\n2. **层级：必须是裸的顶层 key，放在所有 `[section]` 表头之前。** TOML 会把裸 key 绑定到它前面最近的那个 table，所以如果你把它写在某个 `[profile.xxx]` 之类的 section 下面，它就会变成那个 section 的字段，而不是顶层配置，起不到作用。\n\n```toml\n# 正确：顶层，在所有 [section] 之前\nweb_search = \"disabled\"\n\n[profile.example]\n# ...\n```\n\n3. **放哪个文件：** 放在基础配置文件（`$CODEX_HOME/config.toml`）里，或者放在这次运行实际加载的 profile 文件里（profile 文件会叠加在基础配置之上）。两者都可以，选一个即可。\n\n**背景：** 当前 Codex 构建默认每一轮都会发送一个 `web_search` 工具定义。OpenAI 兼容后端如果不实现这个工具，整个 run 会以 HTTP 400 `the web_search tool is not supported` 失败。所以关掉它是让第三方后端能跑通的前提。\n\n**验证：** 改完后跑一次 `codex exec --help` 加一个便宜的探测调用，确认配置被接受、不再报 400。注意 Codex CLI 的 flag 和配置项在版本间变动频繁，升级后建议重新确认这个 key 仍然有效。",
  "error": null,
  "n_skills": 139,
  "target_pos": 12,
  "prompt_tokens": 336215,
  "completion_tokens": 407,
  "sec": 5.0855653285980225,
  "cost": 0.05067645,
  "web_tools": [],
  "n_calls": 1,
  "finish_reason": "stop"
 },
 {
  "id": "T10",
  "target": "ai_session_profanity_rate",
  "model": "deepseek-v4.1-flash",
  "condition": "full_all",
  "answer": "不能混着喂，会重复计数。这是 skill 里明确写过的坑。\n\n## 为什么不能混\n\n`ai_session_profanity_rate` 的 workflow contract 里有一条硬规则：\n\n> Do not combine an archive with its corresponding native stores in one run; that counts the same messages twice.\n\n原因很直接：Markdown 归档（`ai_session_export` 产出的那种）本身就是从本机原始会话库导出的。同一批消息在两边都存在，一次 run 里同时读 archive 和 native store，同一条消息会被统计两次——分母虚高，分子也虚高，比率虽然可能碰巧接近，但 incidence rate 和 per-100-messages 都会被污染，而且你无法事后分辨哪些是重复。\n\n## 该怎么选\n\n**优先用 native store。** skill 里写得很清楚：\n\n> Prefer native stores when available because they preserve exact timestamps and stronger per-message model attribution.\n\nnative store 保留精确时间戳和更强的逐消息模型归属。archive 是便携性 fallback，Markdown 契约里只存 session 日期加本地 `HH:MM`，跨午夜要靠 turn 顺序推断，模型归属也弱（新归档可能有 index 对齐的 `turn_models` 数组，旧归档和没有 turn models 的来源只能是 `archive_unavailable` / `Unknown`）。\n\n所以：\n\n- 本机能直接读原始库 → 用 native store，不要碰 archive。\n- 只有在拿不到 native store（换机器、只有归档副本）时，才用 archive 模式：\n\n```bash\nai-session-profanity-rate prepare \\\n  --source archive \\\n  --archive-dir ~/.local/share/ai-session-export \\\n  --timezone <导出机器用的时区> \\\n  --classifier-profile <approved-classifier-profile>\n```\n\n## 如果你确实想覆盖两边\n\n不要在一次 run 里合并。正确做法是分别跑、分别看：\n\n1. 先跑 native store 的 run，得到 `results.json` / `summary.json`。\n2. 再单独跑 archive 的 run。\n3. 在报告层面比较两者的覆盖差异（比如 archive 里有没有 native 库里已经删掉的旧 session），而不是把两份结果相加。\n\n另外注意 archive 模式必须显式传 `--timezone`，用导出那台机器的时区，否则 `HH:MM` 的午夜翻转会算错。\n\n## 一个附带提醒\n\n`--assume-source-model SOURCE=MODEL` 只在某个来源缺少逐 turn 模型元数据、且用户明确确认该来源整个窗口只用了一个模型时才用。它只填缺失值，不会覆盖已有归属，也绝不能用来\"猜\"模型——猜出来的归属会污染按模型分组的图表。",
  "error": null,
  "n_skills": 139,
  "target_pos": 31,
  "prompt_tokens": 336209,
  "completion_tokens": 585,
  "sec": 5.9481201171875,
  "cost": 0.05078234999999999,
  "web_tools": [],
  "n_calls": 1,
  "finish_reason": "stop"
 },
 {
  "id": "T11",
  "target": "apple_photos",
  "model": "deepseek-v4.1-flash",
  "condition": "full_all",
  "answer": "不能一次性发起 120 对删除，也不能复用授权或对不确定的批次重试。按 `apple_photos` skill 的硬边界：\n\n**每批上限 50 对。** 120 对必须拆成至少 3 个独立计划（例如 50 + 50 + 20）。每个计划都要单独走一遍完整流程：冻结证据 → 生成 manifest → 人工审阅 → 交互式确认 → 单独授权 token。不能把 120 对塞进一个 manifest。\n\n**授权不能复用。** 删除 token 是短期的、单次使用的、绑定到具体 manifest 的（绑定 action、manifest、snapshot、count、expiry、nonce 和 CLI major version），而且 nonce 在 helper 执行前就被消耗掉。所以：\n\n- 每个批次需要一份新的、独立的授权。\n- 不能拿第一批的 token 去跑第二批。\n- 不能拿一个 token 覆盖多个 manifest。\n\n**不确定的批次不能重试。** 这是明确禁止的：\n\n- `partial` 或 `outcome_unknown` 状态一律按事务失败处理。\n- 处理方式是**只读对账**（reconcile read-only），不是自动重试。\n- 绝不能重试一个不确定的批次，也不能复用 token。\n- 如果进程被外部中断或崩溃，结果就是 unknown，此时要重新通过 PhotoKit 和库读取来对账每一个冻结的候选标识符，而不是重跑。\n\n另外几个相关约束，避免你踩坑：\n\n- 删除只把像素验证过的候选移到\"最近删除\"（Recently Deleted），不做永久擦除。\n- 只支持字节不同、单资源的静态图像对；Live Photo、RAW+JPEG、编辑过的复合资产、不可用资源、非照片资源一律拒绝。\n- 阈值要求：尺寸相等、RGB MAE ≤ 1%、luma MAE ≤ 1%、RGB P99 ≤ 5%（策略可以更严，不能更松）。\n- 字节完全相同的重复项不支持删除，用 Photos 自带的重复项处理。\n- 删除 dispatch 故意没有客户端超时，因为 macOS 会弹删除确认窗口，进程会一直持有库锁直到操作者应答。所以执行时人必须在电脑前，别走开。\n- 这是 live-unverified alpha，别在生产库上跑 mutation 命令。\n\n实操建议：把 120 对按 50/50/20 拆成三个 manifest，逐个走\"审阅 → dry-run → 交互确认 → 授权 → 执行 → 只读对账\"的闭环，每批之间确认上一批已经 terminal 完成再开下一批。",
  "error": null,
  "n_skills": 139,
  "target_pos": 105,
  "prompt_tokens": 336213,
  "completion_tokens": 565,
  "sec": 6.259488105773926,
  "cost": 0.050770949999999995,
  "web_tools": [],
  "n_calls": 1,
  "finish_reason": "stop"
 },
 {
  "id": "T12",
  "target": "asr",
  "model": "deepseek-v4.1-flash",
  "condition": "full_all",
  "answer": "不能直接用。按 `asr` skill 的规则，**单个文件超过 7200 秒（2 小时）会在加载模型之前就被拒绝**——也就是说，拒绝发生在 `run` / `diarize` 的输入校验阶段，模型根本不会加载，不会浪费推理时间。\n\n你的录音约 2 小时 10 分 = 7800 秒，超过 7200 秒上限，所以会被拒。\n\n**切成多段能解决吗？能，但有两个必须知道的代价：**\n\n1. **切分后是各自独立的文件**，不是一次连续处理。skill 明确写了：\"Splitting a file makes separate files; a speaker letter does not continue across slices.\" 也就是说，说话人标签（speaker A/B/C…）不会跨切片延续——第 1 段里的 \"A\" 和第 2 段里的 \"A\" 不保证是同一个人，需要你自己在合并时重新对齐。\n\n2. **\"多个文件加起来很长\"不等于\"一个文件超限\"**。skill 里写得很清楚：\"Several files that add up to a long day are not one file over the limit.\" 所以你把 7800 秒切成 4 段各约 1950 秒，每段都远低于 7200 秒，可以正常处理。\n\n**建议做法：**\n\n- 按自然停顿点（比如话题切换、静音段）切成若干段，每段控制在 1 小时以内留余量。\n- 每段单独跑 `asr-skill run`（或 `diarize` + `align`），得到各自的 `transcript.timed.csv`。\n- 合并时注意：每段的 speaker 字母是独立编号的，需要人工或按声纹/上下文重新映射到统一的说话人身份；时间戳也要按各段在原始录音中的偏移量加上去。\n- 如果这段录音里说话人很多、需要跨段保持一致的说话人归属，切分方案会明显增加后处理工作量——这种情况下可以考虑先确认是否真的必须一次处理完整 2 小时 10 分，或者接受分段后手工对齐。\n\n另外提醒：`asr-skill doctor` 会检查环境和模型缓存，但不会替你判断文件时长；时长拒绝是在实际处理命令里触发的。",
  "error": null,
  "n_skills": 139,
  "target_pos": 67,
  "prompt_tokens": 336206,
  "completion_tokens": 488,
  "sec": 6.330307722091675,
  "cost": 0.050723700000000004,
  "web_tools": [],
  "n_calls": 1,
  "finish_reason": "stop"
 },
 {
  "id": "T13",
  "target": "ct_education",
  "model": "deepseek-v4.1-flash",
  "condition": "full_all",
  "answer": "不能填绝对路径。`ct-edu serve` 的 `--video-file` / `--rendered-video-file` 只接受**已完成 workspace 内的相对路径**，指向一个存在的普通 `.mp4` 文件。绝对路径、路径穿越、符号链接、不存在的文件、非 `.mp4` 后缀都会在启动时直接失败并报 `E_VIDEO_FILE`。\n\n具体规则和做法：\n\n**1. 参数形态**\n\n```bash\nct-edu serve --workspace /path/to/external/workspace \\\n  --video-file demo.mp4 \\\n  --rendered-video-file film.mp4\n```\n\n- 两个 flag 都可以单独使用，也可以都不用（默认视频功能关闭）。\n- 每个值都是相对于 `--workspace` 的相对路径，且文件必须已经存在于该 workspace 内。\n- `--video-file` 对应 demo（默认播放项），`--rendered-video-file` 对应渲染成片；只有 rendered 时也能回退播放。\n- HTTP 请求无法再选别的文件，服务端只认启动时指定的这两个。\n\n**2. 所以你要先把 MP4 放进 workspace**\n\n`/tmp/xxx.mp4` 不能直接用。把它复制进那个已完成的 workspace 目录，例如：\n\n```bash\ncp /tmp/your_video.mp4 /path/to/external/workspace/demo.mp4\nct-edu serve --workspace /path/to/external/workspace --video-file demo.mp4\n```\n\n**3. 注意 cinematic 输出的额外约束**\n\n如果你这个 MP4 是 `render-cinematic` 的产物，它的 run 目录和 viewer workspace 是**不相交**的（disjoint）。要把它放进 workspace 供播放，需要**显式批准**后再复制最终成片进去；不要把中间产物或整个 run 目录暴露出去。复制后同样用相对路径指定。\n\n**4. 播放/下载的固定行为**\n\n- `/api/video-info` 默认返回 `{\"available\": false}`；配置后返回播放/下载 URL 和固定的 `versions` 条目。\n- 流式接口是 `/api/video`（demo）和 `/api/video/rendered`（film），下载文件名固定为 `ct-education-tour.mp4` 和 `ct-education-film.mp4`。\n- 元数据可用不代表编码一定能播；仍要实际验证 codec 可播放性。\n\n**5. 隐私边界**\n\nworkspace 内的 MP4 属于私有运行时数据。只服务显式选中的 demo/film 两个文件，不要静态托管整个 workspace，也不要把含患者衍生内容的成片公开或上传。\n\n一句话：**不能填 `/tmp` 绝对路径；先把文件复制进已完成的 workspace，再用相对路径传给 `--video-file` 或 `--rendered-video-file`。**",
  "error": null,
  "n_skills": 139,
  "target_pos": 46,
  "prompt_tokens": 336209,
  "completion_tokens": 611,
  "sec": 6.81689715385437,
  "cost": 0.050797949999999994,
  "web_tools": [],
  "n_calls": 1,
  "finish_reason": "stop"
 },
 {
  "id": "T14",
  "target": "dataforseo",
  "model": "deepseek-v4.1-flash",
  "condition": "full_all",
  "answer": "根据 dataforseo skill 文档，`40104` 这个错误码有明确的解释和处置方式：\n\n## 原因\n\n`40104` 表示**账号已认证但未通过验证（not verified）**。这正是你观察到的现象——`balance` 能正常工作（账号/余额端点可用），但关键词数据端点（包括 sandbox）在用户完成 DataForSEO 面板里的验证之前会被拦截。\n\n## 解决步骤\n\n1. **登录 DataForSEO 面板完成账号验证**。这是唯一的根治办法，CLI 侧没有任何绕过方式。文档明确写了：在用户于 DataForSEO 面板完成验证之前，keyword data 端点（含 sandbox）会一直处于 blocked 状态。\n\n2. **验证完成后重试**。先跑一次余额确认账号状态正常：\n\n   ```bash\n   .venv/bin/dataforseo-skill balance\n   ```\n\n   再跑关键词命令：\n\n   ```bash\n   .venv/bin/dataforseo-skill keyword-suggestions \"agent harness\" --limit 10 --location-code 2840 --language-code en\n   ```\n\n## 不要做的事\n\n- **不要切 sandbox 试图绕过**。文档明确说明 sandbox 同样会被 `40104` 拦截，你实测也验证了这一点。sandbox 不是验证状态的替代路径。\n- **不要反复重试或换参数**。这不是参数问题，重试不会改变结果。\n\n## 顺带确认（如果验证后仍报错）\n\n如果完成验证后依然返回 `40104`，再检查凭证本身：\n\n- 确认 `DATAFORSEO_AUTH_BASE64` 是 `login:password` 的 Base64 编码（不是加密，只是编码）。\n- 或者改用 `DATAFORSEO_LOGIN` + `DATAFORSEO_PASSWORD` 两个环境变量。\n- 注意 Basic Auth token 是 Base64 编码而非加密，如果它曾出现在日志或提交文件里，应轮换。\n\n但按你描述的症状（balance 正常、keyword 报 40104），根因几乎可以确定是**账号未验证**，去面板完成验证即可。",
  "error": null,
  "n_skills": 139,
  "target_pos": 37,
  "prompt_tokens": 336204,
  "completion_tokens": 458,
  "sec": 5.813668251037598,
  "cost": 0.0507054,
  "web_tools": [],
  "n_calls": 1,
  "finish_reason": "stop"
 },
 {
  "id": "T15",
  "target": "eink_diary",
  "model": "deepseek-v4.1-flash",
  "condition": "full_all",
  "answer": "不需要自己改时间窗，直接用 `--full-day` 就行。\n\n按 `eink_diary` skill 的规则：\n\n```bash\neink-diary run --full-day\n```\n\n**凌晨 1:30 属于 00:00–02:00 这个区间**，所以 `--full-day` 会自动解释为**昨天完整一天**，即：\n\n```\n昨天 00:00 .. 今天 00:00\n```\n\n也就是说，你现在（凌晨 1:30）跑 `--full-day`，取的是**昨天一整天**的数据来生成这幅画，而不是\"今天 00:00 到现在\"（那样只有 1.5 小时，几乎没内容）。\n\n对照规则原文：\n\n> **全天语义**：用户说\"今天全天/全天版本\"时优先用 `--full-day`，不要手算 `--minutes`。本地时间 00:00–02:00 之间，`--full-day` 自动解释为昨天完整一天（`yesterday 00:00..today 00:00`）；其他时间解释为今天从 00:00 到当前时刻。\n\n所以：\n\n- **不要**自己算 `--minutes` 或手动指定 `--end`，容易算错。\n- 直接 `eink-diary run --full-day`，让 CLI 按本地时间判断。\n- 如果你确实想显式回放某个窗口（比如补刷），才用 `--end 2026-06-06T20:00` 这类参数。\n\n补充一点：`--full-day` 的\"本地时间\"取决于运行环境的时区，确保机器时区设置正确即可。",
  "error": null,
  "n_skills": 139,
  "target_pos": 83,
  "prompt_tokens": 336201,
  "completion_tokens": 352,
  "sec": 4.458188056945801,
  "cost": 0.05064135,
  "web_tools": [],
  "n_calls": 1,
  "finish_reason": "stop"
 },
 {
  "id": "T16",
  "target": "skill_google_maps_routing",
  "model": "deepseek-v4.1-flash",
  "condition": "full_all",
  "answer": "根据 Google Maps Routing skill 的说明：\n\n**参数规则：出发时间和到达时间必须二选一，不能同时提供。**\n\n- `--departure-time` 和 `--arrival-time` 是互斥的，必须且只能提供其中一个。\n\n**\"现在出发\"的正确填法：**\n\n```bash\nscripts/google_maps_routing route \\\n  --origin \"起点地址\" \\\n  --destination \"终点地址\" \\\n  --departure-time now \\\n  --format json\n```\n\n**关于传接近当前时刻的 ISO 时间戳：**\n\n会。skill 里明确写了这个坑：\n\n> Use `--departure-time now` for current traffic. The CLI avoids sending a fragile explicit near-current timestamp because Routes API may reject it as not sufficiently in the future.\n\n也就是说，Routes API 可能因为\"时间不够靠后\"而拒绝一个接近当前时刻的显式时间戳。所以 CLI 特意用 `now` 这个关键字来规避——它不会把一个脆弱的、接近当前时刻的显式时间戳发出去。\n\n**结论：**\n- 想算\"现在出发\"，直接传 `--departure-time now`，不要自己拼一个当前时刻的 ISO 时间戳。\n- 如果你确实要传显式时间戳，请确保它是一个**明确在未来**的、带时区的 ISO 时间（例如 `2026-04-05T13:00:00-07:00`），否则可能收到 `Timestamp must be set to a future time` 这类错误。\n\n另外提醒：`route` 命令直接用地址字符串即可，不需要先 `geocode`（除非你明确需要坐标）。",
  "error": null,
  "n_skills": 139,
  "target_pos": 58,
  "prompt_tokens": 336216,
  "completion_tokens": 346,
  "sec": 4.76150107383728,
  "cost": 0.050640000000000004,
  "web_tools": [],
  "n_calls": 1,
  "finish_reason": "stop"
 },
 {
  "id": "T17",
  "target": "hybrid_ai_video",
  "model": "deepseek-v4.1-flash",
  "condition": "full_all",
  "answer": "不能直接重发。按 `hybrid_ai_video` skill 的规则，POST 超时或没拿到 task ID 属于**歧义提交（ambiguous submission）**，必须：\n\n1. **保留这次尝试的状态和预算预留**，不要自动重发。\n2. **阻塞自动重提交**，等 owner/provider 对账（reconciliation）后再决定。\n3. 如果本地已经存下了 `request_id`，后续只能**继续用 GET 轮询**（`GET https://api.x.ai/v1/videos/{id}`），**绝不能再发一次 POST**。\n4. 只有在确认这次 POST 确实没有建成任务（对账结果明确）之后，才允许重新提交。\n\n具体做法：\n\n- 先查本地私有 resume state / attempt ledger，看有没有存下 `request_id` 或任务 ID。\n- 有 ID → 只做 GET 轮询，不 POST。\n- 没有 ID → 把这次尝试标记为 ambiguous，保留预算预留，停下来向用户/凭证 owner 报告，等对账结论；不要用\"换个 key / 换个身份 / 换个模型\"绕过。\n\n另外注意：即使重发，生成接口本身也不是幂等的，重复 POST 会真的产生第二个任务并重复计费，所以\"直接再发一次\"在成本和状态上都是错的。",
  "error": null,
  "n_skills": 139,
  "target_pos": 30,
  "prompt_tokens": 336196,
  "completion_tokens": 285,
  "sec": 5.993185758590698,
  "cost": 0.0506004,
  "web_tools": [],
  "n_calls": 1,
  "finish_reason": "stop"
 },
 {
  "id": "T18",
  "target": "ecg_plot",
  "model": "deepseek-v4.1-flash",
  "condition": "full_all",
  "answer": "直接拒绝出图，不会裁剪。\n\n按 `ecg_plot` skill 的核对项，全部样本必须落在 0 到 30 秒内。超出 6 条 5 秒窗口的点**不会被裁掉，而是拒绝作图**。\n\n具体行为：\n\n- 核对失败时**仍写完整性 JSON**，但**不写 PNG**。这是刻意的设计——避免把缺样、截断或坏点画成一条看起来完整的曲线。\n- 所以你会拿到一份完整性报告，里面记录失败原因，但没有图。\n\n另外两点相关约束：\n\n- 输出文件名必须是新路径。如果图或完整性报告已经存在，命令会拒绝覆盖。\n- 这个命令只读一份已经导出的 JSON，不查询数据库，也不调用 `ecg export`。所以如果你需要完整波形，得回到导出环节处理，而不是指望绘图命令帮你截断。\n\n如果你确实只想要前 30 秒，那需要在导出或输入 JSON 阶段就把数据裁到 30 秒以内，再喂给 `ecg_plot`。",
  "error": null,
  "n_skills": 139,
  "target_pos": 8,
  "prompt_tokens": 336203,
  "completion_tokens": 225,
  "sec": 4.052444934844971,
  "cost": 0.05056545,
  "web_tools": [],
  "n_calls": 1,
  "finish_reason": "stop"
 },
 {
  "id": "T19",
  "target": "koyeb",
  "model": "deepseek-v4.1-flash",
  "condition": "full_all",
  "answer": "不行。`--tail` 是布尔开关，不是行数参数，`--tail 100` 会被当成多余的位置参数而报错。\n\n按 Koyeb skill 的说明，正确做法是：\n\n**实时跟随当前日志**（`--tail` 单独用，不带数字）：\n\n```bash\npython3 scripts/koyeb_env.py -- services logs example-service --app example-app --tail\n```\n\n**拉取历史日志**（用时间窗口限定，而不是行数）：\n\n```bash\npython3 scripts/koyeb_env.py -- services logs example-service --app example-app --type runtime \\\n  --start-time 2026-01-01T00:00:00Z --end-time 2026-01-01T00:10:00Z\n```\n\n几点补充：\n\n- 所有 Koyeb 命令都要通过 `scripts/koyeb_env.py` 启动器执行，避免 token 出现在 argv 或 shell history 里。\n- 历史日志的时间戳是 UTC，用 `--start-time` / `--end-time` 做有界查询。\n- 跟随模式（`--tail`）是流式的，调用方要自己控制超时，别让它无限挂着。\n- 如果只是想确认服务状态，`services describe` / `instances list` 这类控制面查询不会唤醒实例；但注意别去轮询公开 HTTP 端点来等日志，那会重置 idle timer、影响 sleep 行为。\n\n所以：想要\"最后 100 行\"这种语义，Koyeb CLI 没有直接对应参数，只能用时间窗口近似，或者用 `--tail` 跟随后再自行截断。",
  "error": null,
  "n_skills": 139,
  "target_pos": 131,
  "prompt_tokens": 336212,
  "completion_tokens": 350,
  "sec": 5.526983022689819,
  "cost": 0.0506418,
  "web_tools": [],
  "n_calls": 1,
  "finish_reason": "stop"
 },
 {
  "id": "T20",
  "target": "narwal",
  "model": "deepseek-v4.1-flash",
  "condition": "full_all",
  "answer": "# Narwal 提示 NOT_READY 的原因与处理\n\n## 原因\n\n`NOT_READY` 是机器人**主动拒绝**了命令，不是 bug。根据 Narwal 本地 skill 的说明：\n\n> `start`（全屋清扫）走的是 `clean/start_clean`，**这个命令只在机器人处于充电座上（docked）时才生效**；否则机器人会回答 `NOT_READY`（exit code 20）。\n\n也就是说，你直接让它在客厅开始清扫时，它**不在充电座上**，所以拒绝了。\n\n另外要记住一条核心规则：**机器人是权威**。一个合法命令被拒绝（`NOT_READY` / `CONFLICT` / `NOT_APPLICABLE`，exit 20）是一个**答案，不是错误**——**绝不要循环重试**被拒绝的命令。\n\n## 怎么做它才会接受\n\n### 1. 先确认当前状态\n\n```bash\nnarwal-local snapshot\n```\n\n看 `base_query.mode` 和 `task`：\n- `standby` 且 task 字段为 null → 空闲\n- 有清扫模式且带 `area_m2` / `elapsed_seconds` → 正在清扫\n\n注意：`is_docked` 和 `charging_state` **永远是 null**，不能直接信。判断是否在座上要看**电池趋势**（跨一个 `watch` 窗口或两次 snapshot 之间电池是否在上升），而不是这两个字段。\n\n### 2. 让它回到充电座\n\n如果它不在座上，先回座：\n\n```bash\nnarwal-local dock --yes\n```\n\n（`dock` 走 `supply/recall`。注意：**回座本身会结束当前任务**，所以不要在回座后再叠一个 `stop`。）\n\n### 3. 等它确实在座上后，再开始清扫\n\n```bash\n# 全屋清扫\nnarwal-local start --yes\n\n# 或指定房间（房间 id 从当前 get_map 拿，工具不会把房间 id 映射成家庭名称）\nnarwal-local clean --rooms 3,5 --yes\n```\n\n建议先 `--dry-run` 预览将要发送的 topic 和 payload，确认无误再真正发送：\n\n```bash\nnarwal-local start --dry-run\n```\n\n## 补充说明\n\n- **`start` 和 `clean` 都要求机器人在座上**，这是产品设计，不是可以绕过的限制。\n- 所有控制命令都需要 `--yes`；`--dry-run` 只构建 payload、不发送控制 topic。\n- 每次写入（除非加 `--no-audit`）会追加一行到 `control_audit.jsonl`。\n- **同一源 IP 只允许一个连接**，不要并行跑命令。\n- 如果 `start` 仍然返回 `NOT_READY`，说明它此刻确实不在座上（或状态不满足），**不要反复重试**——先 `snapshot` 确认状态，必要时 `dock` 回座后再试。\n\n一句话：**先 `dock` 回座，确认在座上，再 `start`。**",
  "error": null,
  "n_skills": 139,
  "target_pos": 33,
  "prompt_tokens": 336198,
  "completion_tokens": 668,
  "sec": 6.0561683177948,
  "cost": 0.0508305,
  "web_tools": [],
  "n_calls": 1,
  "finish_reason": "stop"
 },
 {
  "id": "T21",
  "target": "skill_open_router_data_scraper",
  "model": "deepseek-v4.1-flash",
  "condition": "full_all",
  "answer": "应该传 **canonical_slug**（末尾带 8 位日期的那个完整标识），而不是短 slug。\n\n这是 `skill_open_router_data_scraper` 里明确记录的已知陷阱：\n\n> | 短 slug 不被 model-activity 接受 | 返回空数组 | CLI 自动解析 canonical_slug（末尾 8 位日期）；手动传时需用 canonical_slug |\n\n具体做法：\n\n1. **优先让 CLI 自己解析**。`ords fetch <slug>` 和 `ords archive` 内部会自动把短 slug 解析成 canonical_slug，所以直接用短标识调 CLI 是没问题的——空数组只会在你手动拼 URL 调 `model-activity` 时出现。\n\n2. **手动调用时用 canonical_slug**。形如 `<model-name>-<YYYYMMDD>`（末尾 8 位日期）。例如短 slug 是 `anthropic/claude-xxx`，canonical_slug 会是 `anthropic/claude-xxx-20260101` 这种带日期后缀的完整形式。\n\n3. **怎么拿到 canonical_slug**：\n   - 先跑 `ords discover --top 20` 或 `ords models`，从返回里读 canonical_slug 字段；\n   - 或者查本地 SQLite（`data/ords.db`）里已追踪模型的 `variant_permaslug` 列。\n\n4. **注意请求头**。这些端点无需鉴权，但必须带 `Referer: https://openrouter.ai/`，否则也可能拿不到数据。\n\n另外提醒一个相关口径问题：`rankings/models?view=day` 返回的 reasoning/cached 字段是 0（缺失 telemetry），不要当成真实 0；需要完整 telemetry 时走补全层（Top-20 逐个 `model-activity`）。",
  "error": null,
  "n_skills": 139,
  "target_pos": 24,
  "prompt_tokens": 336200,
  "completion_tokens": 392,
  "sec": 5.859595060348511,
  "cost": 0.050665199999999994,
  "web_tools": [],
  "n_calls": 1,
  "finish_reason": "stop"
 },
 {
  "id": "T22",
  "target": "skill_opencode_data",
  "model": "deepseek-v4.1-flash",
  "condition": "full_all",
  "answer": "默认会展开子会话，想只动选中的会话本身要加 `--no-expand`。\n\n依据 `skill_opencode_data.md` 的 Selector Semantics 一节：\n\n> Selectors operate on sessions. Message and part rows are included only through the selected session IDs.\n>\n> By default, CLI selectors expand descendants through the session `parent_id` chain. This keeps root sessions and subagent child sessions together. Use `--no-expand` only when the user explicitly wants the raw selector result.\n\n所以按标题前缀选择时：\n\n```bash\n# 默认：连同 parent_id 链上的子会话一起选中\n.venv/bin/python -m opencode_skill plan --selector title --prefix batch-\n\n# 只动被选中的会话本身\n.venv/bin/python -m opencode_skill plan --selector title --prefix batch- --no-expand\n```\n\n两点提醒：\n\n1. 先用 `plan` 看解析出的 session/message/part 计数和 selector 描述，确认范围符合预期，再执行 `apply --confirm`。文档明确要求「Never run `apply` from a vague request without first producing a `plan`」。\n2. 如果只是想先复制验证、不删源库，`apply` 时加 `--no-delete` 做 copy-and-verify 的 dry run。",
  "error": null,
  "n_skills": 139,
  "target_pos": 19,
  "prompt_tokens": 336205,
  "completion_tokens": 284,
  "sec": 4.533693075180054,
  "cost": 0.05060115,
  "web_tools": [],
  "n_calls": 1,
  "finish_reason": "stop"
 },
 {
  "id": "T23",
  "target": "video-scoring-audio",
  "model": "deepseek-v4.1-flash",
  "condition": "full_all",
  "answer": "是的，测法有问题。**不要在测峰值前用 `-ac 1` 下混**——这正是 `video-scoring-audio` skill 里明确记录的陷阱。\n\n## 为什么 `-ac 1` 会误报削波\n\n对**相关立体声**（mono 混音同时写进左右两个声道，语音旁白场景的常见情况），ffmpeg 下混会把两个声道**相加**，大约 +3 dB。一个真实峰值 -1.5 dBFS 的混音，下混后可能测到 1.19，于是被报成削波——但原始混音根本没削。\n\n## 正确的测法\n\n**峰值必须在所有声道上测，绝不在下混之后测。** 用 numpy 读原始多声道数据，对每个声道分别取绝对值最大值：\n\n```python\nimport subprocess, numpy as np\n\ndef load(path, rate=48000):\n    ch = int(subprocess.run([\"ffprobe\",\"-v\",\"error\",\"-select_streams\",\"a:0\",\n                             \"-show_entries\",\"stream=channels\",\"-of\",\"csv=p=0\",path],\n                            capture_output=True, text=True).stdout.split()[0])\n    raw = subprocess.run([\"ffmpeg\",\"-v\",\"quiet\",\"-i\",path,\"-f\",\"f32le\",\"-ar\",str(rate),\"-\"],\n                         check=True, stdout=subprocess.PIPE).stdout\n    x = np.frombuffer(raw, dtype=\"<f4\")\n    return x[: len(x) // ch * ch].reshape(-1, ch), rate   # 保留声道\n\nxc, sr = load(\"out.wav\")\nprint(\"peak:\", float(np.max(np.abs(xc))))   # 在 xc 上测，不在下混后测\n```\n\n只有做 RMS 包络和频谱质心时，才用 `x = xc.mean(axis=1)` 下混成单声道——那是分析用途，不是峰值测量。\n\n## 交叉验证\n\n用 ffmpeg 的 `ebur128` 滤波器独立复核积分响度和真峰值，它按标准处理多声道，不做你的手动下混：\n\n```bash\nffmpeg -v error -i out.wav -af ebur128=peak=true -f null -\n```\n\n这会给出 integrated loudness（LUFS）和 true peak（dBTP）。两个数字应当与 numpy 测出的峰值一致（真峰值可能因过采样插值略高于样本峰值，这是正常的）。\n\n## 顺带提醒\n\n- 如果最终混音要交付，用**两遍 `loudnorm`、`linear=true`** 归一化，而不是单遍——单遍会随时间自适应增益，重塑各层之间的平衡（语音/音乐/音效）。\n- 归一化后仍要重新测峰值，`loudnorm` 的目标是输入，不是你能控制的输出。skill 里记录过 `I=-16` 反而在峰值 1.092 处削波的情况。\n\n一句话：**峰值测所有声道，RMS/质心才下混；用 `ebur128=peak=true` 做独立交叉验证。**",
  "error": null,
  "n_skills": 139,
  "target_pos": 113,
  "prompt_tokens": 336217,
  "completion_tokens": 678,
  "sec": 5.88745903968811,
  "cost": 0.05083935,
  "web_tools": [],
  "n_calls": 1,
  "finish_reason": "stop"
 },
 {
  "id": "T24",
  "target": "speaker_notes",
  "model": "deepseek-v4.1-flash",
  "condition": "full_all",
  "answer": "有硬性上限，而且分两档。\n\n**硬性上限（hard gate）**：普通口播句子不得超过 **22 词**。这是不可越过的红线。\n\n**推荐区间**：普通句子控制在 **12–20 词**。这个长度最容易一口气说完，也最符合口播的呼吸节奏。\n\n配套的几条约束：\n\n- **单载荷原则**：一句话最多承载一个主要信息 + 一个从属从句。如果一句话里同时出现条件、例外和后果，就拆开。\n- **呼吸测试**：每句话都要能一口气说完。这是判断句子是否过长的最终标准，比数字更可靠。\n- **注意\"嵌套\"陷阱**：有些句子词数没超，但把条件、例外、后果层层套在一起，读起来照样喘不过气。这种情况按逻辑转折点拆句，让条件、动作、后果各自独立成句。\n\n另外，口播稿不是越短越好。压缩时优先删整块内容（重复点、旁支轶事、已经放在幻灯片上的细节），而不是砍连接词——砍连接词省不了几个词，却会破坏口播的自然语气。压缩完要重读一遍，把被误删的过渡句和说话人指定的措辞补回来。\n\n如果你要写的是完整演讲稿，建议按 **每分钟约 135 个口语词** 估算时长，并给每段留出 10–15% 的余量给演示、点击和停顿。",
  "error": null,
  "n_skills": 139,
  "target_pos": 119,
  "prompt_tokens": 336194,
  "completion_tokens": 309,
  "sec": 5.535707235336304,
  "cost": 0.0506145,
  "web_tools": [],
  "n_calls": 1,
  "finish_reason": "stop"
 },
 {
  "id": "T25",
  "target": "skill_tavily",
  "model": "deepseek-v4.1-flash",
  "condition": "full_all",
  "answer": "不可以，`--stdout` 在 `search` 上是被拒绝的。\n\n具体行为（来自 tavily-skill 文档）：\n\n- **`search` 和 `extract` 都不接受 `--stdout`**。传了会被当成无法识别的参数，属于 usage error，**退出码 2**。\n- 这个错误在**发起任何网络请求之前**就返回，所以**不会真正调用 Tavily API，不消耗额度**。\n- 完整结果永远写到文件里，stdout 只返回一个轻量 status 对象（含 `output_path`、`payload_bytes`、`summary`、`payload_schema` 等），不会内联完整 payload。\n\n## 正确做法\n\n**默认（自动命名文件）：**\n\n```bash\npython -m tavily_skill search \"latest AI news\"\n```\n\nstdout 返回 status JSON，里面有 `output_path`，完整结果在那个文件里。\n\n**指定输出文件：**\n\n```bash\npython -m tavily_skill search \"AI coding tools\" --output /tmp/tavily_search.json\n```\n\nstdout 仍然是 status 对象，完整 payload 写到 `/tmp/tavily_search.json`。\n\n**读取结果**：打开 status 里的 `output_path`，在代码里选择性读取需要的字段，不要把整个 payload 打印出来（会撑爆上下文）。\n\n## 唯一能用 `--stdout` 的命令\n\n`usage` 命令支持 `--stdout`，会直接把完整 usage payload 打到 stdout：\n\n```bash\npython -m tavily_skill usage --stdout\n```\n\n注意 `usage` 的 `--stdout` 和 `--output` 互斥。\n\n## 如果你确实想要\"控制台直接看结果\"\n\n没有官方开关。可行的替代：\n\n1. 跑完 `search` 后，用脚本读 `output_path` 再打印你关心的字段（标题、URL、正文片段）。\n2. 或者用 `--raw-content off` 减小 payload，但结果仍然落文件，不会打到 stdout。\n\n另外提醒：`search` 的批量模式（多个 `--query`）会拒绝 `--output`，每个 query 各写一个文件，stdout 只返回一个 batch status envelope。",
  "error": null,
  "n_skills": 139,
  "target_pos": 17,
  "prompt_tokens": 336205,
  "completion_tokens": 474,
  "sec": 5.091216802597046,
  "cost": 0.050715149999999994,
  "web_tools": [],
  "n_calls": 1,
  "finish_reason": "stop"
 },
 {
  "id": "T26",
  "target": "zun_music",
  "model": "deepseek-v4.1-flash",
  "condition": "full_all",
  "answer": "在 4/4 的八小节里，强拍上能改的地方非常少——测试的硬上限是**每四小节最多一处**，八小节最多两处，而且通常只允许用在最后的终止式上。\n\n## 具体规则\n\n来自 `zun_music` skill 的验收标准第 2 条（skeleton check）：\n\n- **强拍定义**：4/4 拍里指第 1 拍和第 3 拍。\n- **默认要求**：在每个强拍上，编曲的音高类（pitch class）必须等于未改动的原旋律转谱（`SOURCE`）的音高类。\n- **例外机制**：每一处刻意的偏离都必须登记在 `skeleton_allow` 里，**上限是每四小节一处**。\n- **八小节换算**：8 小节 ÷ 4 = **最多 2 处**允许的强拍偏离。\n\n## 怎么才算合规\n\n1. **数量**：八小节里 `skeleton_allow` 条目 ≤ 2。超过就 `check()` 失败，`render_example.py --midi-only` 会以非零退出码报错。\n2. **登记**：每一处偏离都要在 `skeleton_allow` 里显式列出，不能默默改。\n3. **位置惯例**：skill 明确说\"normally it is only the final cadence\"——正常情况只用在最后的终止式。典型做法是把最后一个音落在小调主音上（minor tonic），这就是那个 `skeleton_allow` 条目。\n4. **其余强拍**：除了登记的那 1-2 处，所有第 1、3 拍的音高类必须和原旋律一致。\n\n## 实践含义\n\n- 你想在强拍上\"改好几处\"是**不合规**的。八小节最多两处，而且最好只留一处给终止式。\n- 真正的 ZUN 风格改造空间在**弱拍和强拍之间**：3-3-2 切分、长音提前半拍、16 分音符经过音、句尾 turn（如 C–D–C–B–C）、上行 16 分跑句连接乐句。这些都不动强拍骨架。\n- 如果改多了，听感上会变成\"和原曲完全无关\"——skill 的踩坑记录里就有这个真实失败：改成平行小调、加密集装饰、句中转调，被听者评价为\"completely unrelated to the original\"。\n\n## 验证方式\n\n```bash\npython scripts/render_example.py <tune> --midi-only   # 跑 check()，失败则非零退出\npython -m pytest tests/ -v                            # 对每个 example 跑标准 1-3\n```\n\n`check()` 会逐强拍比对音高类，并核对 `skeleton_allow` 的条目数是否超过每四小节一处。这是自动化的硬门，不是建议。",
  "error": null,
  "n_skills": 139,
  "target_pos": 74,
  "prompt_tokens": 336201,
  "completion_tokens": 644,
  "sec": 8.04526400566101,
  "cost": 0.05081655,
  "web_tools": [],
  "n_calls": 1,
  "finish_reason": "stop"
 },
 {
  "id": "T27",
  "target": "skill_image_generation",
  "model": "deepseek-v4.1-flash",
  "condition": "full_all",
  "answer": "不会生效。Gemini 接受这个 flag 但会忽略它。\n\n原因在 `skill_image_generation` 里写得很明确：`--quality` 是 provider 特有的质量档位，只对 GPT Image 模型有意义。原文是：\n\n> `--quality low|medium|high|xhigh|max|auto` applies to GPT Image models. ... Gemini accepts the flag but ignores it because quality tiers are provider-specific.\n\n所以你的命令不会报错，但也不会改变任何东西——这就是\"看起来没区别\"的原因，不是你的错觉。\n\n## 想真正控制 Gemini 出图质量，用这些\n\nGemini 的质量由**模型选择**和**尺寸/比例**决定，不是 quality 档位：\n\n```bash\n# 换模型：Flash 快而便宜，Pro 质量更高\ngenerate-image -p \"...\" -o output/x.jpg --model gemini-pro\n\n# 换尺寸和比例\ngenerate-image -p \"...\" -o output/x.jpg --size 4K --aspect-ratio 16:9\n```\n\n模型别名：`gemini-flash`（默认）、`gemini-pro`。对应的确切 ID 是 `gemini-3.1-flash-image-preview` 和 `gemini-3-pro-image-preview`。\n\n## 如果你确实需要 quality 档位\n\n那就得换到 GPT Image 模型，它的 quality 是真实生效的：\n\n```bash\ngenerate-image -p \"...\" -o output/x.png \\\n  --model gpt-image-2.5-sunburst --quality high\n```\n\n注意 GPT Image 2.5 的 quality 刻度比 GPT Image 2 更细，多了 `xhigh`、`max`、`auto`；2.5 的 `high` 大约只花 GPT Image 2 `high` 四分之一的钱，2.5 的 `max` 才相当于旧的 `high`。\n\n## 一个容易踩的坑\n\n如果你是在做要交付的文档配图，最终嵌入的图必须是 GPT Image 模型生成/重绘的 PNG/JPG/WebP（或它的压缩衍生品）。Gemini 出的图不满足这条最终交付规则——所以\"Gemini 加 quality 没区别\"这件事，在交付场景下其实不该靠调 quality 解决，而是该换模型。",
  "error": null,
  "n_skills": 139,
  "target_pos": 13,
  "prompt_tokens": 336194,
  "completion_tokens": 492,
  "sec": 5.1726367473602295,
  "cost": 0.0507243,
  "web_tools": [],
  "n_calls": 1,
  "finish_reason": "stop"
 },
 {
  "id": "T28",
  "target": "skill_process_launcher",
  "model": "deepseek-v4.1-flash",
  "condition": "full_all",
  "answer": "这是 Process Launcher 的**精确时刻冲突保护**（exact-time collision guard）在起作用。\n\n## 为什么报冲突\n\n当你用 `delay_seconds` 或 `run_at` 提交定时任务，且解析出的 `run_at` **恰好等于**某个已存在的 pending 或 running 任务的 `run_at` 时，`POST /run` 会返回 **HTTP 409**，`detail.error = \"schedule_conflict\"`，并在 `detail.conflicts` 里列出每个冲突任务（id、label、command、status、run_at）。**409 时不会创建任何东西。**\n\n注意：这个保护**只匹配完全相同的时刻**。两条相隔几秒提交、延迟略有差异的任务不会碰撞。\n\n## 方案一：两个都保留\n\n在**同一条请求**里加上 `\"confirm_conflict\": true` 重新提交：\n\n```bash\ncurl -sf -X POST http://127.0.0.1:7997/run \\\n  -H 'Content-Type: application/json' \\\n  -d '{\n    \"command\": [\"python\", \"job.py\"],\n    \"label\": \"second_job\",\n    \"run_at\": \"2026-06-10T09:00:00-07:00\",\n    \"confirm_conflict\": true\n  }'\n```\n\n`confirm_conflict` 默认是 `false`，只有显式设为 `true` 才会在冲突时刻并排调度。\n\n## 方案二：用新的替换旧的\n\n先取消冲突的旧任务，再提交新的：\n\n```bash\n# 1. 先查出来，拿到 job_id\ncurl -sf 'http://127.0.0.1:7997/scheduled?status=pending&label=<旧任务label>'\n\n# 2. 取消旧任务\ncurl -sf -X POST http://127.0.0.1:7997/scheduled/<job_id>/cancel\n\n# 3. 再提交新任务（此时不再冲突）\ncurl -sf -X POST http://127.0.0.1:7997/run \\\n  -H 'Content-Type: application/json' \\\n  -d '{\"command\": [\"python\", \"new_job.py\"], \"label\": \"new_job\", \"run_at\": \"2026-06-10T09:00:00-07:00\"}'\n```\n\n## 补充：如果只是想改时间\n\n如果旧任务本身就是要保留、只是挪个时间，用 `PATCH` 直接改它的 `run_at` 即可，job id 不变：\n\n```bash\ncurl -sf -X PATCH http://127.0.0.1:7997/scheduled/<job_id> \\\n  -H 'Content-Type: application/json' \\\n  -d '{\"run_at\": \"2026-06-11T08:00:00-07:00\"}'\n```\n\n`PATCH` 在把 `run_at` 移到另一个冲突时刻时，同样会触发这个 guard，同样可以用 `confirm_conflict: true` 放行。\n\n## 边界提醒\n\n- 只有 **pending** 状态的任务能被 `PATCH` 更新；running / completed / failed / cancelled / missed 都不能改。\n- 取消用 `POST /scheduled/{job_id}/cancel`，不是 `PATCH`。\n- 这个 guard 只认精确相等的时刻，不认\"接近\"的时刻，所以别指望它帮你发现时间上重叠但不完全相同的任务。",
  "error": null,
  "n_skills": 139,
  "target_pos": 51,
  "prompt_tokens": 336201,
  "completion_tokens": 741,
  "sec": 6.035549879074097,
  "cost": 0.05087475,
  "web_tools": [],
  "n_calls": 1,
  "finish_reason": "stop"
 },
 {
  "id": "T29",
  "target": "skill_resend_email",
  "model": "deepseek-v4.1-flash",
  "condition": "full_all",
  "answer": "不行。`received list` 和 `received export-all-md` 的 `--after` / `--before` 是 Resend 的分页游标（UUID），不是时间戳过滤器。传 ISO 时间会返回校验错误。\n\n正确的按日期窗口筛选流程：\n\n**1. 先验证配置**\n\n```bash\nop run --env-file=.env -- .venv/bin/python -m resend_email_skill.cli doctor config --format json\n```\n\n**2. 导出一个足够大的近期页面到 Markdown**\n\n```bash\nop run --env-file=.env -- .venv/bin/python -m resend_email_skill.cli received export-all-md \\\n  --limit 100 \\\n  --output-dir data/received/markdown \\\n  --format json\n```\n\n**3. 在本地按 frontmatter 的 `created_at` 过滤**\n\n导出文件带 YAML frontmatter，用 `created_at` 字段在本地筛出最近两天的文件即可。不要试图用 `--after`/`--before` 做时间过滤。\n\n**4. 采样时注意**\n\n从元数据构建采样列表，不要只读最新的几条——最新优先的样本会过度代表重复通知和促销邮件。代表性样本应覆盖：需要行动的邮件、财务/收据、安全验证码、服务告警、订阅续费、社区回复、newsletter、本地生活物流、明显的营销噪音。\n\n**隐私提醒**：导出的 Markdown 和附件属于私有邮箱数据，不要提交 `data/received/`、下载的附件、原始 MIME 或暴露个人邮件内容的派生报告。",
  "error": null,
  "n_skills": 139,
  "target_pos": 63,
  "prompt_tokens": 336206,
  "completion_tokens": 344,
  "sec": 4.9885149002075195,
  "cost": 0.0506373,
  "web_tools": [],
  "n_calls": 1,
  "finish_reason": "stop"
 },
 {
  "id": "T30",
  "target": "skill_typefully_twitter",
  "model": "deepseek-v4.1-flash",
  "condition": "full_all",
  "answer": "默认用 **Typefully long post**（长帖模式），不要套 280 字限制。用户明确说\"普通推文 / 280 字内 / short tweet\"时，加 `--no-long-post` 切到普通 X 长度校验。\n\n具体做法：\n\n1. **默认长帖**：如果账号支持长帖，在本地 `.env` 里设 `TYPEFULLY_TWITTER_DEFAULT_LONG_POST=true`，这样 `post count`、`post draft`、`post create` 默认就走长帖校验。若该环境变量没开，则在命令上显式加 `--long-post`。\n\n2. **切回普通长度**：用户明确要短推时，在命令上加 `--no-long-post`，例如：\n\n```bash\ntypefully-twitter post count --text \"...\" --no-long-post\ntypefully-twitter post draft --text \"...\" --no-long-post\n```\n\n3. **注意语义**：这里说的\"一条帖子\"指的是**一个 Typefully post**，不是一条 280 字符的普通 X 推文。所以\"只发一条\"不等于\"要短\"——除非用户明确说\"普通推文\"\"280 字内\"\"short tweet\"，否则不要自己套 280 字限制。\n\n4. **`TOO_LONG` 不是让你删内容**：如果 `post count` 返回 `TOO_LONG`，对分析类文章来说通常意味着它本来就该走长帖模式，而不是把内容压缩到 280 字。`post count` 是诊断工具，不是内容裁剪指令。\n\n5. **发布前先看实际文本**：`post draft` / `post create` 之前，用 `post count` 或 `post get` 检查真正要发出去的 `text`，确认第一行是面向读者的正文，而不是 `# Twitter 文案` 之类的内部标签。",
  "error": null,
  "n_skills": 139,
  "target_pos": 96,
  "prompt_tokens": 336214,
  "completion_tokens": 408,
  "sec": 4.617705345153809,
  "cost": 0.050676900000000004,
  "web_tools": [],
  "n_calls": 1,
  "finish_reason": "stop"
 }
]