[
 {
  "id": "T01",
  "target": "ai_session_search_archive",
  "model": "deepseek-v4.1-flash",
  "condition": "full_30",
  "answer": "不能直接用那份旧的 txt，会漏结果。原因和正确做法如下。\n\n## 为什么旧文件列表会漏结果\n\n在语义搜索里，**文件列表同时是结果 allowlist**。也就是说，即使某个文件的向量已经在缓存里，只要它不在你传入的 `--file-list` 里，它就永远不会被返回。几周前导出的 txt 会漏掉：\n\n- 这几周新增的会话文件\n- 期间改名、移动、重新导出的文件\n- 当时导出时被截断或遗漏的源\n\n所以「缓存里有向量」并不能救回被 allowlist 排除的文件——缓存只影响是否需要重新算向量，不影响哪些文件有资格被检索。\n\n## 正确做法：查询时现生成文件列表\n\n每次查询都从当前 source scope 重新生成列表，不要复用旧的 `tmp/*files*.txt`：\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\nsemantic-search query \\\n  --file-list \"$FILELIST\" \\\n  --cache-dir .knowledge_cache \\\n  --query '你记得的大概内容' \\\n  --top-k 10 \\\n  --no-refresh\n```\n\n要点：\n\n1. **source scope 按需收窄**：知道是哪个来源就只列那个目录；不知道就列全部来源目录。\n2. **`--no-refresh`**：只读查询不要顺手重建大缓存，索引刷新是单独的维护动作。\n3. **先做词法搜索**：如果记得的是产品名、人名、项目名、标题、日期或 session id，先用 `rg` 搜内容，比语义搜索更准：\n\n```bash\nrg -i -n --glob '*.md' \\\n  '你记得的名字|变体1|变体2' \\\n  contexts/ai_sessions/{opencode,claude_code,codex,antigravity,second_mind}/\n```\n\n注意 `--glob` 匹配的是文件名不是内容，别把 glob 的空结果当成「没有这条会话」。\n\n## 关于新鲜度\n\n如果目标会话比归档最近一次成功导出还新，先看导出器的 state/sync log；确实比归档新，才针对该来源做一次临时导出，查完删掉临时产物。否则直接在归档里搜。\n\n## 结果整理\n\n- 按 source + session id 归组，一条会话只出现一次\n- 展示标题、日期、来源、项目短名，以及一段可核对的原文摘录\n- 不要显示 embedding 分数，也不要用 AI 摘要替代证据\n- 深链只从 frontmatter 读 action id，不要从文件名或正文推断",
  "error": null,
  "n_skills": 30,
  "target_pos": 12,
  "prompt_tokens": 63220,
  "completion_tokens": 609,
  "sec": 6.313887119293213,
  "cost": 0.0098484,
  "web_tools": [],
  "n_calls": 1,
  "finish_reason": "stop"
 },
 {
  "id": "T02",
  "target": "compressor",
  "model": "deepseek-v4.1-flash",
  "condition": "full_30",
  "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- 需要持续监控时去掉 `-once`，改用 `-query <seconds>` 和 `-timeout <seconds>`。\n- 拿到 `batchID` 只代表批次已入队，不代表真的在编码。用 `-monitor` 确认 `percentComplete` 和 `timeElapsedSeconds` 在增长，并对照 `~/Library/Logs/Compressor/servicecontroller:com.apple.stomp.transcoder.stomp.log` 里的 `starting _processRequest` / `Done _processRequest` 记录。",
  "error": null,
  "n_skills": 30,
  "target_pos": 11,
  "prompt_tokens": 64828,
  "completion_tokens": 216,
  "sec": 4.321768283843994,
  "cost": 0.0098538,
  "web_tools": [],
  "n_calls": 1,
  "finish_reason": "stop"
 },
 {
  "id": "T03",
  "target": "deployment_github_actions_koyeb",
  "model": "deepseek-v4.1-flash",
  "condition": "full_30",
  "answer": "在 `koyeb/action-git-deploy@v1` 那一步里加这个入参：\n\n```yaml\ngit-builder: \"docker\"\n```\n\n值就是字符串 `docker`（小写，带引号）。同时建议把 Dockerfile 路径也显式写上，否则它可能找不到：\n\n```yaml\ngit-docker-dockerfile: \"Dockerfile\"\n```\n\n完整那一步长这样：\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判断是否生效：看 Koyeb 构建日志。如果日志里出现 `Installing Python 3.x` / `Installing Node.js` 这类语言运行时安装，说明还在走 buildpack；改成 `docker` 后应该看到 Docker 构建步骤（拉基础镜像、执行 Dockerfile 各层）。\n\n另外两个容易一起踩的点：\n\n- Koyeb 控制台里要把 **Autodeploy 关掉**（Settings → Source → Autodeploy），否则部署时机不受 GitHub Actions 控制。\n- Dockerfile 里必须有 `CMD`，且监听 `${PORT:-8000}` 这种动态端口，否则会报 `no command to run your application` 或 502。",
  "error": null,
  "n_skills": 30,
  "target_pos": 4,
  "prompt_tokens": 63606,
  "completion_tokens": 362,
  "sec": 4.626549005508423,
  "cost": 0.0097581,
  "web_tools": [],
  "n_calls": 1,
  "finish_reason": "stop"
 },
 {
  "id": "T04",
  "target": "mac_universal_clipboard",
  "model": "deepseek-v4.1-flash",
  "condition": "full_30",
  "answer": "直接给你一条可以整段粘贴到终端执行的命令。它做三件事：重写 Mac 端 Universal Clipboard 的偏好项、重启三个相关 daemon、然后确认它们被 launchd 重新拉起。\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**预期结果**\n`sleep 2` 之后 `pgrep` 应该列出这三个进程，说明它们已被 launchd 自动拉起。如果某个没出现，说明它没被自动重启，需要再排查。\n\n**执行后怎么验证**\n分别测两个方向：iPhone/iPad → Mac 复制粘贴，以及 Mac → iPhone/iPad 复制粘贴。你的症状是\"iPhone 和 iPad 之间正常、只要经过 Mac 就失败\"，这通常就是 Mac 端 daemon 持有了过期状态，重写偏好 + 重启这三个进程一般能解决，比重启电脑或退出 iCloud 成本低得多。\n\n**如果这条命令无效**\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`，让 Mac 重新生成本机 Handoff 身份密钥。\n4. 最后手段才是退出 iCloud 再登录（会触发 iCloud Drive、Photos、Keychain 重新同步，打扰较大）。\n\n另外提醒一句：如果你跑的是 macOS beta，快速修复无效后优先按系统回归 bug 判断，不要反复叠加更重的本地改动。",
  "error": null,
  "n_skills": 30,
  "target_pos": 24,
  "prompt_tokens": 63126,
  "completion_tokens": 634,
  "sec": 4.934545278549194,
  "cost": 0.0098493,
  "web_tools": [],
  "n_calls": 1,
  "finish_reason": "stop"
 },
 {
  "id": "T05",
  "target": "openreview",
  "model": "deepseek-v4.1-flash",
  "condition": "full_30",
  "answer": "我需要先说明一个前提：**你这个问题缺少关键信息，我无法直接给出\"删哪个缓存\"的答案**——因为不同工具报 403 的原因和恢复方式完全不同，删错缓存可能让情况更糟。\n\n## 先做一件事：确认是哪个工具\n\n你提到的\"连续执行几次查询后 403\"，在下面这些 skill 里都有对应的已知 403 场景，但恢复动作各不相同：\n\n| 工具 | 403 的典型原因 | 恢复动作 |\n|---|---|---|\n| **OpenReview** | login rate limit（每窗口最多 3 次登录） | 删 `tmp/openreview_token.json`，重试一次 |\n| **Logto** | Management API 权限/scope 问题 | 不要重试，走 `docs/onboarding.md` |\n| **grok_oauth** | SuperGrok 档位在 `/v1/chat/completions` 返回 403 | 这是 subscription-surface 失败，**不能** fallback 到 API key |\n| **Firewalla** | 只读集成，一般不是 403 | — |\n\n## 最可能的情况：OpenReview\n\n如果你说的是 OpenReview 查询，这是唯一有明确\"删本地缓存再重试\"路径的：\n\n```bash\nrm tmp/openreview_token.json\n```\n\n然后重试一次。原因：OpenReview 的 login 限 3 次/窗口，CLI 已做 token 缓存，正常使用不会触发；如果 token 过期导致 403，删掉缓存后重试一次即可。**注意只重试一次**——如果还 403，说明是窗口限流还没过，继续重试只会加深限流。\n\n## 如果不是 OpenReview\n\n请告诉我具体是哪个工具、报错原文是什么。因为：\n\n- **Logto** 的 403 明确要求\"停止重试\"，删缓存没用，要走 onboarding 文档。\n- **grok_oauth** 的 403 是订阅档位问题，删缓存和换 API key 都是错误方向。\n- 其他工具的 403 可能是权限、scope、或服务端限流，恢复方式完全不同。\n\n## 通用原则（避免越弄越糟）\n\n1. **先看报错原文**，不要凭\"403\"就猜是限流。\n2. **限流类 403**：删 token 缓存 + 等一个窗口 + 只重试一次。\n3. **权限类 403**：删缓存无效，需要修配置或走 onboarding。\n4. **不要**在 403 时盲目切换认证方式（比如从 OAuth 切到 API key），这可能掩盖真实问题或产生额外计费。\n\n把工具名和完整报错贴给我，我给你确切的恢复步骤。",
  "error": null,
  "n_skills": 30,
  "target_pos": 17,
  "prompt_tokens": 63650,
  "completion_tokens": 584,
  "sec": 6.389680862426758,
  "cost": 0.0098979,
  "web_tools": [],
  "n_calls": 1,
  "finish_reason": "stop"
 },
 {
  "id": "T06",
  "target": "workflow_bilibili_whisper_transcription",
  "model": "deepseek-v4.1-flash",
  "condition": "full_30",
  "answer": "根据 `workflow_bilibili_whisper_transcription` 这个 skill 的踩坑记录和最佳实践，稳妥做法是：\n\n**每次下载之间间隔 2-3 秒，并且单线程逐个下载。**\n\n具体照做：\n\n1. **单线程下载**，不要并发。skill 里明确把\"下载并发\"的决策点定为单线程，原因就是避免触发 B 站反爬机制。\n2. **每个音频之间 sleep 2-3 秒**。这是 skill 里给出的具体数值。\n3. **加上 User-Agent / Referer headers**。352 错误（请求被 B 站拦截）的解决方案里，除了增加延迟，还包括补上这两个 header。\n4. **只下载音频**，减少请求体积和可疑度。\n5. **记录失败的视频 ID**，遇到 404（视频已删除或 ID 错误）就跳过并记录，不要反复重试同一个。\n\n另外注意：获取视频列表阶段用 yt-dlp 提取视频 ID 时，B 站 API 也可能返回 352 错误，同样需要 headers + 延迟，或者干脆手动获取 ID。\n\n转录阶段可以放心用多进程（4-8 个），因为那是纯本地 CPU 密集任务，跟 B 站风控无关。",
  "error": null,
  "n_skills": 30,
  "target_pos": 23,
  "prompt_tokens": 63205,
  "completion_tokens": 273,
  "sec": 4.908764123916626,
  "cost": 0.00964455,
  "web_tools": [],
  "n_calls": 1,
  "finish_reason": "stop"
 },
 {
  "id": "T07",
  "target": "workflow_watchdog",
  "model": "deepseek-v4.1-flash",
  "condition": "full_30",
  "answer": "默认设 **约 30 分钟（1800 秒）** 的巡检唤醒。\n\n在 Claude Code harness 里用 **`ScheduleWakeup`** 来设这个唤醒（其他 harness 用对应的定时机制，比如 Process Launcher 的延时执行）。\n\n醒来后自己判断两种情况，不用问用户：\n\n1. **真实在忙**——有新输出、agent 在持续产出：不管它，继续等或再设一次 wake-up。\n2. **鬼打墙卡住**——长时间无进展、同一步骤反复重试：把它 kill 掉（Claude Code 用 `TaskStop`），用已有的部分结果推进，或换方法重做。\n\n\"真忙 vs 卡住\"的判断自己做，这属于技术编排决策，自己推到底。",
  "error": null,
  "n_skills": 30,
  "target_pos": 13,
  "prompt_tokens": 62529,
  "completion_tokens": 165,
  "sec": 3.4085798263549805,
  "cost": 0.00947835,
  "web_tools": [],
  "n_calls": 1,
  "finish_reason": "stop"
 },
 {
  "id": "T08",
  "target": "workflow_parallel_subagents",
  "model": "deepseek-v4.1-flash",
  "condition": "full_30",
  "answer": "不行。`subagent_type` 必须是**已注册的 agent 名**，不是模型名。OpenCode 会执行 `agent.get(subagent_type)`，找不到同名 agent 就报 `Unknown agent type`。`provider/model` 只是底层模型入口，要能被 `task` 调用，必须先包成 agent。\n\n## 正确做法\n\n在 `~/.config/opencode/opencode.json`、项目 `opencode.json`，或 `.opencode/agent(s)/*.md` 里注册一个 agent，把模型指到 `deepseek/deepseek-v4-pro`，例如：\n\n```json\n{\n  \"agent\": {\n    \"writer_deepseek\": {\n      \"model\": \"deepseek/deepseek-v4-pro\"\n    }\n  }\n}\n```\n\n然后调用时用 agent 名：\n\n```json\n{\n  \"tool_uses\": [\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```\n\n## 几点注意\n\n1. **先确认 agent 已注册**：`subagent_type` 只接受运行时 tool schema 里列出的类型。内置的只有 `general` / `explore`；其余命名 agent 都来自上面那几个配置文件。没注册就调用会直接失败。\n2. **不要用旧路由名**：`category=\"deep\"`、`category=\"writing\"`、`librarian`、`ultrabrain`、`glm51` 这类写法在当前工具 schema 里不存在，除非它们已经被注册成同名 agent。\n3. **并行要一次性发出**：多个 subagent 必须在**同一条 assistant 消息**里用 `multi_tool_use.parallel` 包多个 `functions.task`。单独连续调用多个 `task`，即使嘴上说\"并行\"，实际也是串行。\n4. **不要用 `run_in_background`**：当前工具没有这个参数，也没有 `background_output` 取结果流程；`multi_tool_use.parallel` 会在同一轮 tool response 里返回所有子任务结果。\n5. **如果只是想要 DeepSeek 路线的现成选项**：已有注册好的 `writer_deepseek`（中文写作/改稿，避免高隐私材料）和 `ollama_deepseek_pro`（Ollama Cloud DeepSeek V4 Pro，zero-data-retention，较贵）。能直接用这两个就不必自己新建 agent。\n\n一句话：`subagent_type` 填 agent 名，模型在 agent 定义里指定；想用 `deepseek/deepseek-v4-pro` 就先把它包成一个 agent。",
  "error": null,
  "n_skills": 30,
  "target_pos": 24,
  "prompt_tokens": 66788,
  "completion_tokens": 599,
  "sec": 5.179210901260376,
  "cost": 0.010377599999999999,
  "web_tools": [],
  "n_calls": 1,
  "finish_reason": "stop"
 },
 {
  "id": "T09",
  "target": "codex_cli",
  "model": "deepseek-v4.1-flash",
  "condition": "full_30",
  "answer": "在 Codex CLI 0.156.1 上，这一行是：\n\n```toml\nweb_search = \"disabled\"\n```\n\n**值必须是字符串 `\"disabled\"`，不能用布尔值。** `false`、`on`、`off`、`enabled` 都会被拒绝，报 `expected string only in 'web_search'`。\n\n**层级：必须是顶层裸键（bare top-level key），不能放进任何 `[section]` 表里。** TOML 的裸键会绑定到它前面最近的那个表头，所以要把这一行写在所有 `[section]` 之前，否则它会被解析成某个表的子键，等于没关掉。\n\n放哪里取决于你的运行方式：\n\n- 基础配置：`$CODEX_HOME/config.toml`（默认 `~/.codex/config.toml`）的顶层。\n- 如果你用 profile 运行：profile 文件会叠加在基础配置之上，把这一行放进该次运行实际加载的那个 profile 文件顶层即可。\n\n背景：当前 Codex 构建默认每一轮都会声明 `web_search` 工具定义，不实现它的 OpenAI 兼容后端会直接以 HTTP 400 `the web_search tool is not supported` 让整次运行失败。关掉它就能绕过。\n\n改完后建议做一次便宜的验证：跑一个 `codex exec` 小任务，确认不再出现该 400。另外注意升级 Codex 后要重新核对 flag 和配置键——这个 CLI 在小版本之间改动很频繁（0.144.6 → 0.156.1 就删掉了 `--full-auto` 和 `--ask-for-approval`）。",
  "error": null,
  "n_skills": 30,
  "target_pos": 23,
  "prompt_tokens": 63227,
  "completion_tokens": 359,
  "sec": 5.053489923477173,
  "cost": 0.00969945,
  "web_tools": [],
  "n_calls": 1,
  "finish_reason": "stop"
 },
 {
  "id": "T10",
  "target": "ai_session_profanity_rate",
  "model": "deepseek-v4.1-flash",
  "condition": "full_30",
  "answer": "不能，而且这正是文档里明确禁止的做法。\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 归档通常就是从同一批本机原始会话库导出的。同一条消息在两边都存在，`prepare` 会把它当成两条独立消息各标一次，`summary.json` 里的 incidence rate 和 units per 100 messages 分母分子同时虚高，而且虚高比例不可预测（取决于归档覆盖了多少、导出时间点之后新增了多少）。\n\n## 该选哪个\n\n**优先用 native stores**（OpenCode / Claude Code / Codex / Antigravity 的本地库）。文档给的理由是它们保留精确时间戳和更强的 per-message model attribution——归档的 Markdown 只存 session date 加本地 `HH:MM`，跨午夜要靠 turn 顺序推断，模型归属也常常是 `archive_unavailable` / `Unknown`。\n\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注意 `--timezone` 要填**导出那台机器**的时区，不是你现在这台。\n\n## 如果你确实想覆盖更全\n\n不要在一次 run 里混源，而是**分两次 run，各自独立出结果**：\n\n1. 先跑 native store 的 run，得到 `results.json` / `summary.json`。\n2. 再单独跑 archive 的 run。\n3. 在报告里分别陈述两个口径，并说明它们覆盖的时间窗和来源不同——不要手工把两个 `results.json` 拼起来再算比率，那等于绕过了 ingest 的 batch ID / item set 校验，也失去了缓存去重的保护。\n\n## 顺带两个容易踩的点\n\n- **缓存是按 classifier profile 失效的**。如果你两次 run 用了不同的 `--classifier-profile`（比如换了 provider 或模型），缓存不会复用，会重新生成全部 batch。想复用就保持 profile 字符串完全一致。\n- **`--assume-source-model` 只在用户明确确认某个源在整个窗口内只用一个模型时才用**，而且它只填缺失值、记录为 `configured_source_default`，不能拿来猜。归档模式下这个参数基本用不上，因为归档的模型归属本来就不可靠。\n\n一句话：选一个来源，别混；要更全就分两次跑、分开报告。",
  "error": null,
  "n_skills": 30,
  "target_pos": 6,
  "prompt_tokens": 63556,
  "completion_tokens": 602,
  "sec": 6.541547060012817,
  "cost": 0.0098946,
  "web_tools": [],
  "n_calls": 1,
  "finish_reason": "stop"
 },
 {
  "id": "T11",
  "target": "apple_photos",
  "model": "deepseek-v4.1-flash",
  "condition": "full_30",
  "answer": "不能一次性发起，也不能复用授权或重试不确定的批次。\n\n**每批上限：50 对。** 120 对必须拆成至少 3 个独立计划（例如 50 + 50 + 20），每个计划都要单独走一遍完整流程：重新冻结证据、重新生成 manifest、重新做人工复核、重新取一次授权 token。\n\n具体约束：\n\n1. **批次上限 50 对**：`apple-photos` 的删除 manifest 每个最多 50 对。超过就拆成独立计划，每个计划都需要全新的复核和授权，不能沿用上一个计划的 token。\n\n2. **授权不可复用**：删除 token 是短时、单次使用、且绑定到具体 manifest 的。它同时校验 action、manifest、snapshot、count、expiry、nonce 和 CLI 主版本号。换一批、换一个 manifest、或者 token 过期，都必须重新授权。nonce 在 helper 执行前就被消费掉，所以同一个 token 用第二次一定失败。\n\n3. **不确定的批次绝不重试**：如果某批返回 `partial` 或 `outcome_unknown`，按事务失败处理——只做只读对账（用全新的 PhotoKit 和库读取，逐个核对被冻结的候选 identifier），**不要自动重试**，也不要复用 token 重发。确认清楚实际状态后，再决定是否需要为剩余项建立新计划。\n\n4. **授权前必须停下**：`delete authorize` 的提示会明确要求 AI agent 停止，先通过提问工具或对话拿到人类最新、最明确的授权，才能输入确认短语。不能自己 pipe 或 PTY 注入确认短语。\n\n5. **删除只进\"最近删除\"**：候选资产是移到 Recently Deleted，不做永久擦除；也不要绕过 macOS 的删除确认弹窗。\n\n另外提醒两点：删除资格只支持\"字节不同、单资源、静态图像\"的配对，Live Photo、RAW+JPEG、编辑过的复合资产、不可用资源、非照片资源都要拒绝；字节完全相同的重复项也不支持用这个流程删，那种情况用 Photos 自带的重复项处理。\n\n所以 120 对的正确做法是：拆成 3 个 ≤50 对的独立计划，逐个冻结证据、逐个复核、逐个单独授权、逐个执行，任何一批出现不确定状态就停下来只读对账，不重试。",
  "error": null,
  "n_skills": 30,
  "target_pos": 12,
  "prompt_tokens": 64163,
  "completion_tokens": 513,
  "sec": 5.543454885482788,
  "cost": 0.00993225,
  "web_tools": [],
  "n_calls": 1,
  "finish_reason": "stop"
 },
 {
  "id": "T12",
  "target": "asr",
  "model": "deepseek-v4.1-flash",
  "condition": "full_30",
  "answer": "不能直接用。会被拒绝，而且是在**加载模型之前**就被拒绝。\n\n## 拒绝点\n\n按 `asr` skill 的边界规定：\n\n> A single file longer than 7200 seconds is rejected before a model loads.\n\n7200 秒 = 2 小时整。你的录音约 2 小时 10 分钟 ≈ 7800 秒，超过上限约 600 秒，所以会在 `run`（或 `diarize`/`align`）启动时、任何声学模型加载之前直接报错退出。不会浪费模型加载时间，也不会产生半截产物。\n\n## 切成多段能解决吗\n\n能绕过长度限制，但有两个必须接受的后果：\n\n1. **说话人字母不跨切片延续**。skill 明确写了：\n\n   > Splitting a file makes separate files; a speaker letter does not continue across slices.\n\n   也就是说，第 1 段里的 `SPEAKER_00` 和第 2 段里的 `SPEAKER_00` 不保证是同一个人。跨切片的说话人身份需要你自己在编辑阶段（`prepare-edit` / `edited.json` 的 `name_map`）重新对齐。\n\n2. **切分本身要你自己做**，工具不会自动帮你切。用 ffmpeg 在静音处切，例如：\n\n   ```bash\n   ffmpeg -i input.m4a -f segment -segment_time 3600 -c copy part_%02d.m4a\n   ```\n\n   建议按 30–60 分钟一段切，留足余量，别贴着 7200 秒切。\n\n## 另一个容易混淆的点\n\nskill 里特别澄清过：\n\n> Several files that add up to a long day are not one file over the limit.\n\n所以「总时长超过 2 小时」本身不是问题——**限制只针对单个文件**。你完全可以把 2 小时 10 分钟拆成 3 个 45 分钟的文件，逐个跑，每个都在限制内。\n\n## 建议做法\n\n1. 用 ffmpeg 在静音点切成 3 段（每段 ≤ 60 分钟）。\n2. 对每段分别跑 `asr-skill run`（或 `diarize` → `align`）。\n3. 在 `prepare-edit` 阶段把各段的说话人标签统一映射到真实姓名，写进 `name_map`。\n4. 用 `finalize-edit` 分别导出，再按时间顺序拼接。\n\n如果你希望说话人身份尽量连续，切点选在明显的单人长段落中间，而不是对话密集处——这样每段内部的说话人结构更干净，后续人工对齐也更容易。",
  "error": null,
  "n_skills": 30,
  "target_pos": 22,
  "prompt_tokens": 64203,
  "completion_tokens": 580,
  "sec": 5.443717002868652,
  "cost": 0.009978449999999998,
  "web_tools": [],
  "n_calls": 1,
  "finish_reason": "stop"
 },
 {
  "id": "T13",
  "target": "ct_education",
  "model": "deepseek-v4.1-flash",
  "condition": "full_30",
  "answer": "不行，`serve` 的视频参数不接受绝对路径。\n\n根据 ct_education skill 的说明：\n\n- `--video-file` / `--rendered-video-file` 只接受**已完成 workspace 内部**、已存在的普通 `.mp4` 文件的**相对路径**。\n- 绝对路径、目录穿越（`..`）、符号链接、文件不存在、非 `.mp4` 后缀，都会在启动时直接失败并报 `E_VIDEO_FILE`。\n- 而且 HTTP 请求方也无法另外指定文件——只能播放启动时选定的那个。\n\n所以 `/tmp/xxx.mp4` 这种绝对路径填进去会启动失败。\n\n## 正确做法\n\n1. 先把 MP4 放进一个**已完成的 workspace**（就是 `ct-edu build` 生成、含 `manifest.json` 的那个外部目录）里面。\n2. 用相对该 workspace 的路径来指定：\n\n```bash\nct-edu serve --workspace /path/to/external/workspace --video-file demo.mp4\n```\n\n两个参数可以单独用，也可以一起用：\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- `--video-file` 对应 \"Interface demo\"（下载名固定为 `ct-education-tour.mp4`）\n- `--rendered-video-file` 对应 \"Rendered film\"（下载名固定为 `ct-education-film.mp4`）\n- 视频默认关闭；不传这两个参数时 `/api/video-info` 返回 `{\"available\":false}`。\n\n## 两个额外注意点\n\n- **不要用符号链接**把 `/tmp` 里的文件链进 workspace——符号链接会被拒绝。\n- 如果你要放的是 cinematic 渲染出来的成片，注意 cinematic 的输出目录和 viewer workspace 是**互斥（disjoint）**的，把最终 MP4 拷进 workspace 需要**显式批准**；而且只应拷贝最终成片，不要暴露中间产物或整个 run 目录。\n\n另外，`render-video` 生成的视频是直接写在 workspace 内的（默认 `tour.mp4`），那种情况天然满足\"workspace 内相对路径\"的要求，直接 `--video-file tour.mp4` 即可。",
  "error": null,
  "n_skills": 30,
  "target_pos": 17,
  "prompt_tokens": 68843,
  "completion_tokens": 500,
  "sec": 4.829475164413452,
  "cost": 0.010626449999999999,
  "web_tools": [],
  "n_calls": 1,
  "finish_reason": "stop"
 },
 {
  "id": "T14",
  "target": "dataforseo",
  "model": "deepseek-v4.1-flash",
  "condition": "full_30",
  "answer": "这是 DataForSEO 的账号验证状态问题，不是你的 CLI 或凭据配置问题。\n\n## 判断依据\n\n`dataforseo` skill 的 Known Failure Modes 里明确写了这一条：\n\n> DataForSEO may return `40104` when the account is authenticated but not verified. In that case, account/balance can still work, but keyword data endpoints, including sandbox, may be blocked until the user completes verification in the DataForSEO panel.\n\n你的症状完全吻合：\n\n- `balance` 能跑通 → 说明 Base64 凭据本身有效，认证这一层没问题\n- 关键词命令返回 40104 → 说明账号通过了认证但没通过验证\n- sandbox 也一样 → 因为 sandbox 走的是同一套账号验证门，不是独立环境\n\n所以不用去查 CLI 参数、不用换 location/language code、也不用怀疑 `DATAFORSEO_AUTH_BASE64` 拼错了。\n\n## 解决步骤\n\n1. 登录 DataForSEO 官网的账号面板（panel）。\n2. 找到账号验证（verification）入口，完成验证流程。通常是补全公司/身份信息、确认邮箱、或按面板提示提交所需材料。\n3. 验证通过后，重新跑一次关键词命令确认：\n\n```bash\ncd adhoc_jobs/dataforseo_skill/\nsource .venv/bin/activate\n.venv/bin/dataforseo-skill keyword-suggestions \"agent harness\" --limit 3 --location-code 2840 --language-code en\n```\n\n先用 `--limit 3` 这种小请求探一下，确认 40104 消失、能返回关键词行，再放大 limit 做正式分析。\n\n## 验证通过前不要做的事\n\n- 不要反复重试或加大 limit——40104 是账号级门禁，重试不会变好，只会浪费调用。\n- 不要为了绕过它去改 `--base-url` 或换凭据——sandbox 已经被证明同样被拦，换环境无效。\n- 不要怀疑 token 泄漏去轮换凭据——40104 不是认证失败，轮换解决不了。\n\n## 顺带确认一下\n\n如果完成验证后仍然返回 40104，那才需要回头检查：是不是验证提交了但还没审核通过（有延迟），或者面板里还有未完成的必填项。这种情况下把面板里显示的账号状态截图/文字发我，我再帮你判断。\n\n另外提醒一句：`balance` 能跑通只证明认证有效，不能证明账号可用——这两件事在 DataForSEO 里是分开的，以后遇到\"余额能查、数据接口报错\"的情况，优先怀疑账号验证状态。",
  "error": null,
  "n_skills": 30,
  "target_pos": 29,
  "prompt_tokens": 62973,
  "completion_tokens": 573,
  "sec": 5.285552978515625,
  "cost": 0.00978975,
  "web_tools": [],
  "n_calls": 1,
  "finish_reason": "stop"
 },
 {
  "id": "T15",
  "target": "eink_diary",
  "model": "deepseek-v4.1-flash",
  "condition": "full_30",
  "answer": "不需要自己改时间窗，直接用 `--full-day` 就行。\n\n```bash\neink-diary run --full-day\n```\n\n具体取哪段时间：`--full-day` 在本地时间 **00:00–02:00 之间会自动解释为「昨天完整一天」**，也就是 `昨天 00:00 .. 今天 00:00`。你现在是凌晨 1:30，正落在这个区间里，所以它会取**昨天一整天**（昨天 00:00 到 今天 00:00）来生成。\n\n其他时间（02:00 之后）跑 `--full-day` 才会解释为「今天 00:00 到现在」。\n\n所以：\n\n- 不要手算 `--minutes`，用 `--full-day` 即可。\n- 凌晨 1:30 跑 → 取昨天完整一天。\n- 如果只是想回放某个具体历史窗口，才用 `--end`（例如 `eink-diary run --end 2026-06-06T20:00`）。\n\n补充一点：`--full-day` 只影响采集的时间窗，出图、推送流程不变；不想推送可以加 `--no-push`。",
  "error": null,
  "n_skills": 30,
  "target_pos": 17,
  "prompt_tokens": 65494,
  "completion_tokens": 256,
  "sec": 3.7168021202087402,
  "cost": 0.009977699999999999,
  "web_tools": [],
  "n_calls": 1,
  "finish_reason": "stop"
 },
 {
  "id": "T16",
  "target": "skill_google_maps_routing",
  "model": "deepseek-v4.1-flash",
  "condition": "full_30",
  "answer": "根据 `skill_google_maps_routing` 的说明，直接回答你的两个问题：\n\n## 1. 出发时间 / 到达时间：只能二选一\n\n是的，**必须且只能提供其中一个**。文档原文：\n\n> Exactly one of `--departure-time` or `--arrival-time` is required.\n\n所以：\n\n- 算\"现在出发\" → 用 `--departure-time now`\n- 算\"要在某时刻到达，该几点走\" → 用 `--arrival-time <ISO 时间戳>`\n\n两个都传或都不传都不行。\n\n## 2. 接近当前时刻的 ISO 时间戳：会被拒绝\n\n会。文档明确说了这个坑：\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对应的常见失败信息是：\n\n> `Timestamp must be set to a future time`\n\n也就是说，如果你手填一个\"就比现在晚几秒\"的 ISO 时间戳，Routes API 很可能判定它\"不够未来\"而报错。CLI 内部正是为了避免这个问题，才让你用 `now` 这个关键字，而不是自己算一个贴近当前的时间戳。\n\n## 推荐做法\n\n算\"现在出发\"的驾驶时间，直接这样：\n\n```bash\nscripts/google_maps_routing route \\\n  --origin \"起点地址\" \\\n  --destination \"终点地址\" \\\n  --departure-time now \\\n  --format json\n```\n\n要点：\n\n- 用 `--departure-time now`，**不要**自己构造接近当前的 ISO 时间戳。\n- 如果确实要指定一个具体出发时间，选一个**明显在未来**的、带时区的 ISO 时间戳（例如 `2026-04-05T13:00:00-07:00`），别贴着当前时刻。\n- 用 `--format json`，成功信号是退出码 0 且 stdout 是可解析 JSON；结果里读 `route.duration_seconds`（含实时路况）和 `route.static_duration_seconds`（不含路况）来对比。\n\n另外，算驾驶时间不需要先 geocode，直接传地址字符串给 `route` 即可；只有当你明确需要坐标时才先跑 `geocode`。",
  "error": null,
  "n_skills": 30,
  "target_pos": 24,
  "prompt_tokens": 63425,
  "completion_tokens": 496,
  "sec": 4.12202000617981,
  "cost": 0.00981135,
  "web_tools": [],
  "n_calls": 1,
  "finish_reason": "stop"
 },
 {
  "id": "T17",
  "target": "hybrid_ai_video",
  "model": "deepseek-v4.1-flash",
  "condition": "full_30",
  "answer": "不能直接重发。按 `hybrid_ai_video` 的规则，POST 超时属于「ambiguous submission」，必须保留本次尝试记录和预算预留，**阻塞自动重发**，等 owner 或 provider 对账后再决定。\n\n具体做法：\n\n1. **先查本地状态，不要先发请求。** 检查你为这次生成持久化的 attempt state 和 budget reservation（POST 之前就应该写好的那份）。如果里面已经存了 `request_id`，说明任务其实建成了——那就走 `GET https://api.x.ai/v1/videos/{id}` 轮询，**绝不重新 POST**。\n\n2. **如果本地没有存到 task ID**（超时发生在拿到响应之前），这就是典型的 ambiguous submission：\n   - 保留 attempt 记录和预算预留，标记为「待对账」。\n   - 不要自动重发，也不要换 key、换身份、换模型绕过。\n   - 把情况交给 owner / provider 去核对（例如查 provider 侧的任务列表或账单），确认这次 POST 到底有没有落地。\n\n3. **对账结论决定下一步：**\n   - 确认任务已创建 → 用存下的 ID 继续 GET 轮询。\n   - 确认任务未创建 → 才可以在授权范围内重新提交，并且这次要确保 POST 前先落盘 attempt state 和预算预留。\n\n4. **轮询中断的情况**：如果只是轮询 worker 被中断但 ID 已存，恢复时只做 GET 轮询，不做 POST。\n\n一句话：超时 ≠ 失败，也 ≠ 可以重试。先查 `request_id`，有就轮询，没有就挂起等对账，重发必须等确认未创建之后。",
  "error": null,
  "n_skills": 30,
  "target_pos": 12,
  "prompt_tokens": 67079,
  "completion_tokens": 371,
  "sec": 4.9197046756744385,
  "cost": 0.010284449999999999,
  "web_tools": [],
  "n_calls": 1,
  "finish_reason": "stop"
 },
 {
  "id": "T18",
  "target": "ecg_plot",
  "model": "deepseek-v4.1-flash",
  "condition": "full_30",
  "answer": "直接拒绝出图，不会裁剪。\n\n按 `ecg_plot` skill 的规定：\n\n- 核对项里明确写着「全部样本落在 0 到 30 秒内。**超出 6 条 5 秒窗口的点不会被裁掉，而是拒绝作图**」。\n- 核对失败时的行为是：**仍然写完整性 JSON，但不写 PNG**，避免把缺样、截断或坏点画成一条看起来完整的曲线。\n\n所以你的情况会得到：\n\n1. `--integrity-output` 指定的完整性 JSON 正常生成，里面会记录测量范围（超出 30 秒这件事会体现在这里）；\n2. `--output` 指定的 PNG **不会生成**；\n3. 命令不会静默截断后继续画。\n\n另外两点相关约束，避免你踩坑：\n\n- 输出文件名必须是新路径。如果图或完整性报告已经存在，命令会拒绝覆盖——所以重跑前先换个文件名或删掉旧的。\n- 输入输出只能放在 `data/` 下且已被 gitignore 的路径（如 `data/exports/`、`data/raw/`），不要把 PNG、完整性 JSON 或电压写进测试、文档或可追踪源码。\n\n如果你确实需要看 30 秒之后的部分，这个工具做不到——它固定就是 6 条 5 秒窗口。你需要先在上游（导出/采集环节）把数据切成 30 秒以内的片段，再分别对每段跑 `ecg_plot`。",
  "error": null,
  "n_skills": 30,
  "target_pos": 8,
  "prompt_tokens": 63215,
  "completion_tokens": 320,
  "sec": 3.9122209548950195,
  "cost": 0.009674249999999999,
  "web_tools": [],
  "n_calls": 1,
  "finish_reason": "stop"
 },
 {
  "id": "T19",
  "target": "koyeb",
  "model": "deepseek-v4.1-flash",
  "condition": "full_30",
  "answer": "不行。`--tail` 是布尔开关，不是行数参数，`--tail 100` 会被当成多余的位置参数而报错。\n\n正确做法：\n\n```bash\npython3 scripts/koyeb_env.py -- services logs example-service --app example-app --tail\n```\n\n要点：\n\n- `--tail` 单独使用即表示跟随当前日志（streaming），不要给它跟数字。\n- 想限定行数/时间范围，用有界查询而不是 `--tail`：\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- 有界调用方要自己控制 streaming 超时，别让命令无限挂着。\n- 注意 `services logs` 的 `--tail` 是布尔值；如果你确实需要\"最后 100 行\"这种语义，Koyeb CLI 没有行数参数，只能靠时间窗口（`--start-time`/`--end-time`）来界定范围，然后自行截取。\n- 另外，根路径 HTTP 404 不代表服务挂了，判断服务状态要看容器状态和应用路由，别只看日志里的 404。",
  "error": null,
  "n_skills": 30,
  "target_pos": 14,
  "prompt_tokens": 64730,
  "completion_tokens": 281,
  "sec": 3.6183078289031982,
  "cost": 0.009878099999999999,
  "web_tools": [],
  "n_calls": 1,
  "finish_reason": "stop"
 },
 {
  "id": "T20",
  "target": "narwal",
  "model": "deepseek-v4.1-flash",
  "condition": "full_30",
  "answer": "## 原因\n\n`NOT_READY` 是机器人**主动拒绝**了这条命令，不是工具或连接出错。Narwal 的 `clean/start_clean`（`start` 和 `clean` 两个命令都走这个 topic）**只在机器人处于 docked（在基站上）状态时才接受**。你在客厅直接喊它开始全屋清扫，它当时不在基站上，所以回了 `NOT_READY`（退出码 20）。\n\n按 skill 的规则：**被拒绝的命令是答案，不是 bug，绝对不要循环重试。**\n\n## 怎么做它才会接受\n\n1. **先让它回基站**：\n   ```bash\n   narwal-local dock --yes\n   ```\n   注意 `dock` 走的是 `supply/recall`，会自己结束当前任务；**不要在 dock 之后再叠一个 `stop`**。\n\n2. **确认它真的在基站上**。这里有个坑：`is_docked` 和 `charging_state` 字段**永远是 `null`**，不能直接读。判断方法是看**电池趋势**——跑一个短窗口的 `watch`，看电量是否在上升：\n   ```bash\n   narwal-local watch --duration 20\n   ```\n   电量在窗口内上升，才说明它在基站上充电。\n\n3. **确认在基站后，再发清扫命令**。全屋：\n   ```bash\n   narwal-local start --yes\n   ```\n   指定房间（房间 id 从当前 `get_map` 拿，工具不会把房间 id 映射成家里的房间名）：\n   ```bash\n   narwal-local clean --rooms 3,5 --yes\n   ```\n\n4. **不确定 payload 时先 dry-run**，它只构造 payload、不发控制 topic：\n   ```bash\n   narwal-local start --dry-run\n   ```\n\n## 补充说明\n\n- 控制命令都必须带 `--yes`，否则是用法错误（退出码 2）。\n- 每次写入（除非加 `--no-audit`）会往 `control_audit.jsonl` 追加一行，记录 topic、payload 和结果。\n- 同一源 IP 只允许一个连接，**不要并行跑命令**。\n- 如果 `dock` 之后它仍然回 `NOT_READY`，说明它还没真正到位或还有别的状态没满足——这时应该重新 `snapshot` / `watch` 观察当前状态，而不是反复重发 `start`。",
  "error": null,
  "n_skills": 30,
  "target_pos": 13,
  "prompt_tokens": 66534,
  "completion_tokens": 531,
  "sec": 4.748747825622559,
  "cost": 0.0102987,
  "web_tools": [],
  "n_calls": 1,
  "finish_reason": "stop"
 },
 {
  "id": "T21",
  "target": "skill_open_router_data_scraper",
  "model": "deepseek-v4.1-flash",
  "condition": "full_30",
  "answer": "应该传 **canonical_slug**，而不是短标识。\n\n具体做法：\n\n- 短 slug（比如 `openai/gpt-4o` 这类不带日期后缀的写法）不被 `model-activity` 端点接受，会直接返回空数组。\n- 需要传 **canonical_slug**，也就是末尾带 8 位日期的那一版，例如 `openai/gpt-4o-2024-08-06` 这种形式。\n\n如果你用的是 `ords` CLI，它已经内置了这个处理：`ords fetch <slug>` 和 `ords archive` 会自动把短 slug 解析成 canonical_slug，所以走 CLI 一般不会踩这个坑。只有在你手动拼请求、直接调 `model-activity` 时，才需要自己确保传的是带日期后缀的 canonical_slug。\n\n另外提醒一点：`rankings/models` 批量层返回的 reasoning/cached 字段是 0，那是缺失 telemetry 而不是真实值，别把它当成真实数据来分析——补全层（逐个 `model-activity`）才会补上这些字段。",
  "error": null,
  "n_skills": 30,
  "target_pos": 29,
  "prompt_tokens": 63212,
  "completion_tokens": 234,
  "sec": 3.3703060150146484,
  "cost": 0.009622200000000001,
  "web_tools": [],
  "n_calls": 1,
  "finish_reason": "stop"
 },
 {
  "id": "T22",
  "target": "skill_opencode_data",
  "model": "deepseek-v4.1-flash",
  "condition": "full_30",
  "answer": "默认会一起算进去，想只动选中的会话本身要加 `--no-expand`。\n\n具体说明（来自 `skill_opencode_data`）：\n\n- **默认行为**：选择器（selector）作用在会话上，但默认会沿会话的 `parent_id` 链**展开后代**，把根会话和它的 subagent 子会话一起带上。所以按标题前缀选一批会话时，子会话默认会被一并纳入。\n- **只选本身**：加 `--no-expand`，就只取选择器原始命中的那些会话，不展开子会话。\n\n对应命令形态：\n\n```bash\n# 默认：会带上子会话\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. 文档明确说 `--no-expand` 只在用户**明确**想用原始选择器结果时才用，别默认加。\n2. 这是破坏性维护操作，先跑 `plan` 看解析出的 session/message/part 数量和选择器描述，确认无误后再 `apply --confirm`；`apply` 前要确保 OpenCode 没有在写源库。",
  "error": null,
  "n_skills": 30,
  "target_pos": 8,
  "prompt_tokens": 63217,
  "completion_tokens": 283,
  "sec": 3.5776360034942627,
  "cost": 0.009652349999999999,
  "web_tools": [],
  "n_calls": 1,
  "finish_reason": "stop"
 },
 {
  "id": "T23",
  "target": "video-scoring-audio",
  "model": "deepseek-v4.1-flash",
  "condition": "full_30",
  "answer": "是的，测法有问题。`-ac 1` 会在测量前把双声道下混，对相关立体声（比如单声道混音同时写进左右两声道，旁白混音的常见情况）会叠加约 +3 dB，于是真实峰值 -1.5 dBFS 的混音被测成 1.19，被误报为削波。\n\n## 正确做法\n\n**1. 峰值必须在所有声道上测，不能在下混之后测。**\n\n用 ffmpeg 解码成 f32le 后按声道 reshape，对每个声道取绝对值最大值：\n\n```python\nimport subprocess, numpy as np\n\ndef load(path, rate=48000):\n    ch = int(subprocess.run(\n        [\"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(\n        [\"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   # 保留声道，不要 mean\n\nxc, sr = load(\"mix.wav\")\nprint(\"peak:\", float(np.max(np.abs(xc))))   # 必须 < 1.0\n```\n\n只有做 RMS 包络和频谱质心时，才 `x = xc.mean(axis=1)` 下混成单声道——那是分析用途，不是峰值测量。\n\n**2. 用 ebur128 做独立交叉验证。**\n\n```bash\nffmpeg -i mix.wav -af ebur128=peak=true -f null -\n```\n\n它会同时给出 integrated loudness（LUFS）和 true peak（dBTP），是独立于你自己 numpy 计算的第二条证据链。两个数字对得上，才敢下结论。\n\n## 交叉验证的判据\n\n- 自己算的 sample peak 与 `ebur128=peak=true` 的 true peak 应当一致（true peak 因过采样可能略高，属正常）。\n- 若两者都 < 1.0（0 dBFS），就是没削波；之前 `-ac 1` 报的 1.19 是下混伪影。\n- 若两者都 ≥ 1.0，才是真削波，需要回退到渲染阶段降增益或重新 `loudnorm`。\n\n## 顺带提醒\n\n- `loudnorm` 的目标值（如 `I=-16`）不能盲信，峰值是输出、要测，不是输入、可控。首次尝试 `I=-16` 就曾在 peak 1.092 处削波。\n- 最终混音建议两遍 `loudnorm` 且 `linear=true`：第一遍 `print_format=json` 测量，第二遍用测得值加 `linear=true` 施加单一增益，这样旁白与音乐的平衡不会被动态重塑。\n- 交付时报告两个数字：integrated LUFS 和 true peak（dBTP），并说明是在所有声道上测的。",
  "error": null,
  "n_skills": 30,
  "target_pos": 21,
  "prompt_tokens": 67437,
  "completion_tokens": 714,
  "sec": 5.695192098617554,
  "cost": 0.01054395,
  "web_tools": [],
  "n_calls": 1,
  "finish_reason": "stop"
 },
 {
  "id": "T24",
  "target": "speaker_notes",
  "model": "deepseek-v4.1-flash",
  "condition": "full_30",
  "answer": "普通口播句子建议控制在 **12–20 词**，硬性上限是 **22 词**——任何普通口播句子都不得超过 22 词。\n\n具体做法：\n\n1. **按 12–20 词写普通句子**，保证一口气能说完。\n2. **把 22 词当硬门禁**：写完逐句数词，超过 22 词的普通句子必须拆开。\n3. **一句只装一个主载荷 + 一个从句**。如果一句话里同时有条件、例外和后果，就拆成几句。\n4. **拆在逻辑转折处**，让条件、动作、后果各自成拍，而不是把长句硬切成碎片。\n5. **压缩时先删整块内容**（重复点、旁支轶事、幻灯片上已有的细节），不要靠删连接词省字数——那会破坏口播语感。\n6. **压缩后重读一遍**，把被顺手删掉的连接词、反问句和讲者指定的措辞补回来。\n\n另外两个配套约束：用自然的缩略形式（don't、it's、we've），但不要靠俚语或玩笑硬造口语感；避免重复的\"不是 X，而是 Y\"式对称口号，那会让直接朗读稿听起来像 AI 生成的。",
  "error": null,
  "n_skills": 30,
  "target_pos": 20,
  "prompt_tokens": 65129,
  "completion_tokens": 276,
  "sec": 3.677666187286377,
  "cost": 0.009934950000000001,
  "web_tools": [],
  "n_calls": 1,
  "finish_reason": "stop"
 },
 {
  "id": "T25",
  "target": "skill_tavily",
  "model": "deepseek-v4.1-flash",
  "condition": "full_30",
  "answer": "不可以，`--stdout` 在 `search` 和 `extract` 上是**未识别的参数**，会直接以退出码 **2** 失败，并且**不会发起任何网络请求**。\n\n## 具体行为\n\n- `search` / `extract` 的 stdout 只返回轻量 status 对象（`command`、`status`、`output_mode`、`output_path`、`payload_bytes`、`summary`、`payload_schema`），完整结果永远不内联在 stdout。\n- 传 `--stdout` 属于 usage error（unrecognized argument），退出码 2，进程在参数解析阶段就退出，所以不会调用 Tavily API、不消耗 credit。\n- 只有 `usage` 子命令支持 `--stdout`（`python -m tavily_skill usage --stdout`），那是另一回事。\n\n## 想拿到完整结果，正确做法\n\n完整 payload 会写到文件，你读那个文件即可：\n\n```bash\npython -m tavily_skill search \"latest AI news\"\n# stdout 返回 status JSON，其中 output_path 指向完整结果文件\n```\n\n默认落盘位置是 `tmp/tavily/`（或 `TAVILY_CLI_OUTPUT_DIR` 指定的目录）；也可以用 `--output` 指定路径：\n\n```bash\npython -m tavily_skill search \"AI coding tools\" --output /tmp/tavily_search.json\n```\n\n注意：即使加了 `--output`，stdout 仍然只是 status schema，不是完整结果。\n\n## 在代码里安全读取\n\n关键两点：先检查 `returncode` 再 `json.loads`；打开 status 里返回的**确切** `output_path`，不要 glob 或猜文件名。\n\n```python\nimport json, subprocess, sys\nfrom pathlib import Path\n\ncmd = [sys.executable, \"-m\", \"tavily_skill\", \"search\", \"latest AI news\"]\nproc = subprocess.run(cmd, capture_output=True, text=True)\n\nif proc.returncode != 0:\n    print(f\"Error (exit {proc.returncode}): {proc.stderr.strip()}\", file=sys.stderr)\n    sys.exit(proc.returncode)\n\nstatus = json.loads(proc.stdout)\nif (status.get(\"command\") != \"search\"\n        or status.get(\"status\") != \"ok\"\n        or status.get(\"output_mode\") != \"file\"\n        or not status.get(\"output_path\")):\n    print(\"Unexpected search status envelope\", file=sys.stderr)\n    sys.exit(1)\n\npayload = json.loads(Path(status[\"output_path\"]).read_text(encoding=\"utf-8\"))\nfor item in payload[\"data\"][\"results\"][:3]:\n    print(item.get(\"title\"), item.get(\"url\"))\n```\n\n## 退出码速查\n\n| 情况 | 退出码 |\n|---|---|\n| 传 `--stdout` 给 search/extract（usage error） | 2 |\n| API/写文件失败 | 1 |\n| 成功 | 0 |\n| 批量模式部分失败 | 0（stderr 有 warning） |\n| 批量模式全部失败 | 1 |\n\n另外提醒：不要指望把完整 payload 打印到终端——文档明确要求\"在代码里选择性读取有界字段\"，而不是把整个 payload 文件贴出来。",
  "error": null,
  "n_skills": 30,
  "target_pos": 3,
  "prompt_tokens": 63217,
  "completion_tokens": 719,
  "sec": 5.067563056945801,
  "cost": 0.00991395,
  "web_tools": [],
  "n_calls": 1,
  "finish_reason": "stop"
 },
 {
  "id": "T26",
  "target": "zun_music",
  "model": "deepseek-v4.1-flash",
  "condition": "full_30",
  "answer": "不能随便改。测试对强拍改动的上限是**每四小节最多一处**，而且默认情况下这一处通常只留给最后的终止式。\n\n具体规则和合规判定如下：\n\n**规则本身**\n\n- 骨架检查（skeleton check）要求：在每个强拍上（4/4 拍里就是第 1 拍和第 3 拍），编曲的音高类（pitch class）必须等于**未经改动的原旋律转写**的音高类。\n- 每一处刻意偏离都必须登记在 `skeleton_allow` 里。\n- `skeleton_allow` 的上限是**每四小节一处**。\n- 正常情况下，这个名额只用于最后的终止式（final cadence）。\n\n**所以对一个 4/4 的八小节乐句**\n\n- 强拍上最多允许 **2 处**偏离（8 小节 ÷ 4 = 2）。\n- 但这是硬上限，不是配额。默认只应有一处，即结尾终止式。\n- 如果你在八小节里塞满 2 处，虽然可能通过自动检查，但会明显损害\"能认出原曲\"这个目标——而\"能认出原曲\"是人工验收标准，只有用户能判定。\n\n**怎么才算合规**\n\n1. 强拍（第 1、3 拍）的音高类与原旋律转写一致，除非该处已登记进 `skeleton_allow`。\n2. 登记进 `skeleton_allow` 的偏离总数 ≤ 每四小节一处。\n3. 偏离通常只出现在最后终止式（例如把末音落到小调主音上）。\n4. 自动检查通过：`python scripts/render_example.py <tune> --midi-only` 会跑 `check()`（覆盖上述第 1、2 条），失败时以非零退出码结束；`python -m pytest tests/ -v` 会对每个 example 跑第 1–3 条。\n5. 人工验收：用户确认原曲可辨认且听起来像 ZUN。任一项为否就要迭代。\n\n**实践建议**\n\n- 把改动放在**弱拍**上，而不是强拍。ZUN 风格的惯用手法（3-3-2 切分、长音提前半拍、16 分音符回转、乐句间上行 16 分音符跑动）本来就发生在骨架音之间，不需要动强拍。\n- 强拍骨架保留得越完整，原曲越容易被认出来；改动越少越安全。\n- 如果确实需要第二处偏离，先确认它是否真的必要，并想清楚它会不会让听众认不出原曲。",
  "error": null,
  "n_skills": 30,
  "target_pos": 9,
  "prompt_tokens": 64838,
  "completion_tokens": 569,
  "sec": 5.517916917800903,
  "cost": 0.0100671,
  "web_tools": [],
  "n_calls": 1,
  "finish_reason": "stop"
 },
 {
  "id": "T27",
  "target": "skill_image_generation",
  "model": "deepseek-v4.1-flash",
  "condition": "full_30",
  "answer": "不会生效。Gemini 接受这个 flag 但会忽略它。\n\n`skill_image_generation` 里写得很明确：\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原因是 quality 档位是各家 provider 自己的概念，Gemini 那边没有对应的映射，所以 CLI 收下参数但不往请求里传。\n\n**要真正控制 Gemini 出图质量，改用 `--model` 选档：**\n\n```bash\n# 默认档（Flash）\ngenerate-image -p \"...\" -o output/x.jpg\n\n# 更高档\ngenerate-image -p \"...\" -o output/x.jpg --model gemini-pro\n```\n\n可用的 Gemini 别名是 `gemini-flash` 和 `gemini-pro`（对应 ID `gemini-3.1-flash-image-preview` 和 `gemini-3-pro-image-preview`）。\n\n**如果你确实需要 quality 分级**，那就得换到 GPT Image 系列，那边 `--quality` 是真起作用的，而且 2.5 的档位更细：\n\n```bash\ngenerate-image -p \"...\" -o output/x.png --model gpt-image-2.5-sunburst --quality high\n```\n\n顺带一个容易踩的点：GPT Image 2.5 的 `high` 成本大约只有 GPT Image 2 `high` 的四分之一，2.5 的 `max` 才相当于旧的 `high`。所以从 Gemini 切过去时别按老习惯直接上 `high` 就以为拿到了最高档。\n\n另外 `--size` / `--aspect-ratio` 这两个参数在 Gemini 下是正常生效的，只有 `--quality` 是空转。",
  "error": null,
  "n_skills": 30,
  "target_pos": 1,
  "prompt_tokens": 63206,
  "completion_tokens": 386,
  "sec": 4.324946880340576,
  "cost": 0.009712499999999999,
  "web_tools": [],
  "n_calls": 1,
  "finish_reason": "stop"
 },
 {
  "id": "T28",
  "target": "skill_process_launcher",
  "model": "deepseek-v4.1-flash",
  "condition": "full_30",
  "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**，响应体里：\n\n- `detail.error = \"schedule_conflict\"`\n- `detail.message` 列出每个冲突任务（id、label、command、status、run_at）\n- `detail.conflicts` 给出完整的冲突任务列表\n\n**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_b.py\"],\n    \"label\": \"job_b\",\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（从 409 响应的 detail.conflicts 里拿，或查询）\ncurl -sf 'http://127.0.0.1:7997/scheduled?status=pending'\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\", \"job_new.py\"], \"label\": \"job_new\", \"run_at\": \"2026-06-10T09:00:00-07:00\"}'\n```\n\n## 相关补充\n\n- **`PATCH /scheduled/{job_id}` 移动时间时也受同一保护**：如果你想把某个 pending 任务的 `run_at` 改到另一个任务占用的时刻，同样会返回 409，同样用 `confirm_conflict: true` 放行。\n- **只有 pending / running 状态的任务参与冲突判定**；已 completed / failed / cancelled / missed 的终态任务不参与。\n- **只有 pending 任务能被 PATCH 修改**；running 及终态任务不能改，只能取消（且只有 pending 能取消）。\n- 取消后想确认结果，用 `GET /scheduled?status=all` 或 `include_terminated=true` 查看终态。\n\n一句话总结：**要共存就加 `confirm_conflict: true` 重发；要替换就先 `POST /scheduled/{id}/cancel` 取消旧的再提交新的。**",
  "error": null,
  "n_skills": 30,
  "target_pos": 23,
  "prompt_tokens": 66155,
  "completion_tokens": 748,
  "sec": 5.435075044631958,
  "cost": 0.010372049999999999,
  "web_tools": [],
  "n_calls": 1,
  "finish_reason": "stop"
 },
 {
  "id": "T29",
  "target": "skill_resend_email",
  "model": "deepseek-v4.1-flash",
  "condition": "full_30",
  "answer": "不行，不能把 `--after`/`--before` 设成 ISO 时间。\n\n## 原因\n\n在 Resend 的 `received list` / `received export-all-md` 里，`--after` 和 `--before` 是 **Resend 的分页游标（pagination cursor）**，不是时间戳过滤器。API 期望的是 UUID 形式的游标值，传 ISO 时间会直接返回校验错误。\n\n## 正确的按日期筛选流程\n\n1. **先验证配置**（确认 API key、收件地址、数据目录）：\n\n```bash\nop run --env-file=.env -- .venv/bin/python -m resend_email_skill.cli doctor config --format json\n```\n\n2. **导出一个足够大的近期页面到 Markdown**（不要用 `--after/--before` 传日期）：\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\n3. **在本地按 `created_at` 过滤**：导出的每个 Markdown 文件带 YAML frontmatter，里面有 `created_at` 字段。用这个字段在本地筛出\"最近两天\"的文件，而不是靠 API 参数。\n\n## 补充注意\n\n- 导出后先按元数据（`created_at`、发件人等）建立采样清单，再读正文；不要只读最新的几条，最新优先的样本会过度集中在重复通知和促销上。\n- 导出的 Markdown 和附件属于私有邮箱数据，不要提交 `data/received/`、下载的附件、原始 MIME 或暴露个人邮件内容的衍生报告。",
  "error": null,
  "n_skills": 30,
  "target_pos": 15,
  "prompt_tokens": 64267,
  "completion_tokens": 380,
  "sec": 3.727621078491211,
  "cost": 0.009868050000000001,
  "web_tools": [],
  "n_calls": 1,
  "finish_reason": "stop"
 },
 {
  "id": "T30",
  "target": "skill_typefully_twitter",
  "model": "deepseek-v4.1-flash",
  "condition": "full_30",
  "answer": "默认用 **Typefully long post（长帖）模式**，不要套 280 字限制。\n\n依据 `skill_typefully_twitter` 的规则：\n\n- 文章分发帖（来自内容平台的）**默认就是 Typefully long post**。这里的\"一条帖子\"指的是一个 Typefully post，不是一条 280 字符的普通 X 推文。\n- 只有当用户明确说\"普通推文\"、\"280 字内\"、\"short tweet\"这类话时，才切到普通长度。\n\n**切到普通长度的参数**：加 `--no-long-post`。\n\n具体做法：\n\n1. 先确认账号能力。如果用户的 Typefully/X 账号能发长帖，在本地 `.env` 里设 `TYPEFULLY_TWITTER_DEFAULT_LONG_POST=true`，这样 `post count`、`post draft`、`post create` 默认就走长帖校验。\n2. 默认（长帖）时不要加任何长度开关，直接：\n   ```bash\n   typefully-twitter post count --thread-file thread.md\n   ```\n3. 用户明确要短推时，显式加 `--no-long-post`：\n   ```bash\n   typefully-twitter post count --thread-file thread.md --no-long-post\n   ```\n   注意：如果 `.env` 里没开长帖默认，则反过来——默认是普通长度，需要长帖时加 `--long-post`。\n\n另外两个配套注意点：\n\n- 如果 `post count` 在长帖模式下仍返回 `TOO_LONG`，不要为了塞进长度而删内容（除非用户明确要求单条摘要）；`TOO_LONG` 往往说明内容本就该走长帖模式。\n- 发布前用 `post count` 或 `post get` 检查实际发出的 `text`，确认第一行是面向读者的正文，而不是 `# Twitter copy`、`TYPEFULLY long post` 这类内部标签。",
  "error": null,
  "n_skills": 30,
  "target_pos": 16,
  "prompt_tokens": 64584,
  "completion_tokens": 422,
  "sec": 3.9460020065307617,
  "cost": 0.0099408,
  "web_tools": [],
  "n_calls": 1,
  "finish_reason": "stop"
 }
]