[
 {
  "id": "T01",
  "target": "ai_session_search_archive",
  "model": "deepseek-v4.1-flash",
  "condition": "index_rep2",
  "answer": "可以跑，但有两个坑必须先排掉，否则你会得到\"静默空结果\"——不报错，但什么都搜不到。\n\n## 1. 文件列表 txt 的路径问题（最常见的坑）\n\nCLI 会把 file-list 里每一条都规范化成**绝对路径**再去查缓存。相对路径的解析基准是：\n\n- 有 `--source-root` 时 → 相对 `--source-root`\n- 没有时 → 相对 **CLI 当前工作目录**，而不是你当初生成这个列表的目录\n\n所以如果你几周前是在 workspace 根目录用 `find` 生成的相对路径列表，现在从别的目录跑 CLI，解析出来的绝对路径跟缓存里的 identity 对不上，**查询会静默返回空结果，没有任何报错**。\n\n两个修法，选一个：\n\n```bash\n# 方案 A：重新生成一份绝对路径的列表\nfind /path/to/archive -name '*.md' > tmp/files_abs.txt\n\n# 方案 B：沿用旧列表，但把 --source-root 指向当初生成它的目录\nsemantic-search query \\\n  --file-list tmp/files.txt \\\n  --source-root /path/to/workspace \\\n  --cache-dir .knowledge_cache \\\n  --query \"你记得的那条会话大概在讲什么\" \\\n  --top-k 10\n```\n\n## 2. 缓存里有向量会不会漏结果\n\n不会因为\"有向量\"而漏，但有几个真实风险：\n\n- **缓存里没有的文件不会出现在结果里。** 查询只覆盖缓存中已索引的 chunk。你几周前导出列表之后新增/改动的文件，如果没 rebuild 过，就不在索引里。默认 query 会做 refresh（增量补算变更的 chunk），所以直接跑 query 通常能补上；但如果你加了 `--no-refresh`，就是纯读旧缓存，新文件必然漏。\n- **旧 chunk 被标记 inactive 后查询会忽略它们**，但物理上还在磁盘上。这是正常的，不是漏结果。\n- **v1 缓存不会被自动识别。** 如果 `.knowledge_cache` 里只有 `cache.json` + `chunks.jsonl` + `embeddings.npy`，没有 `index.sqlite3`，必须先显式迁移：\n\n```bash\nsemantic-search migrate-v1 --v1-cache-dir old-cache --cache-dir .knowledge_cache\n```\n\n- **相对路径/重复 identity 的旧缓存**（比如当初就是相对路径建的）需要修：\n\n```bash\n# 先 dry run，看 JSON 报告\nsemantic-search canonicalize-paths --cache-dir .knowledge_cache --source-root /path/to/workspace\n# 确认无误再 apply（只改 SQLite 元数据，不动 segment 文件，embeddings_recomputed: 0）\nsemantic-search canonicalize-paths --cache-dir .knowledge_cache --source-root /path/to/workspace --apply\n```\n\n## 建议的执行顺序\n\n```bash\n# 1. 先体检，确认缓存版本、inactive chunk、孤儿 segment\nsemantic-search doctor --cache-dir .knowledge_cache\nsemantic-search stats  --cache-dir .knowledge_cache\n\n# 2. 需要时修路径 identity（见上）\n\n# 3. 正式查询（不加 --no-refresh，让它顺带增量刷新）\nsemantic-search query \\\n  --file-list tmp/files.txt \\\n  --source-root /path/to/workspace \\\n  --cache-dir .knowledge_cache \\\n  --query \"你记得的那条会话大概在讲什么\" \\\n  --top-k 10\n```\n\n另外两点：\n\n- 如果目标是找 **OpenCode 会话**，先搜 workspace 的 OpenCode session archive；只有 frontmatter 里带 `source: opencode` 的结果才能生成 `opencode://session/<session_id>` 深链，`session_id` 必须直接来自 frontmatter（不能从文件名或标题猜），且必须以 `ses_` 开头、只含 ASCII 字母数字下划线连字符。结果按 `session_id` 聚合，同一会话只列一次，默认给 3–5 条候选；匹配不够强时明确标注\"可能相关\"，不要硬挑一条。\n- 别把相似度分数展示给用户，也别用 AI 摘要替代原文摘录——给逐字摘录。\n\n一句话：**能直接跑，但先确认列表路径能被正确解析（绝对路径或 `--source-root`），并且别用 `--no-refresh` 去查一个几周没更新的缓存。**",
  "error": null,
  "opened": [
   "skill_semantic_search"
  ],
  "requested": [
   "skill_semantic_search"
  ],
  "opened_target": false,
  "prompt_tokens": 29794,
  "completion_tokens": 1978,
  "sec": 18.0710232257843,
  "cost": 0.0156559,
  "web_tools": [],
  "n_calls": 2
 },
 {
  "id": "T02",
  "target": "compressor",
  "model": "deepseek-v4.1-flash",
  "condition": "index_rep2",
  "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放到末尾（`... -batchid \"<batch_id>\" -once -format json`）在部分 Compressor 版本上会报 `Invalid parameter: -format`。\n\n补充两点：\n\n- 提交批次时用的 `-outputformat json` 是另一个参数，那个放在末尾没问题；出问题的是监控时的 `-format`。\n- 连续监控时去掉 `-once`，改用 `-query <seconds>` 和 `-timeout <seconds>`，`-format json` 依然紧跟 `-monitor`。",
  "error": null,
  "opened": [
   "compressor"
  ],
  "requested": [
   "compressor"
  ],
  "opened_target": true,
  "prompt_tokens": 29150,
  "completion_tokens": 368,
  "sec": 12.50221300125122,
  "cost": 0.0145933,
  "web_tools": [],
  "n_calls": 2
 },
 {
  "id": "T03",
  "target": "deployment_github_actions_koyeb",
  "model": "deepseek-v4.1-flash",
  "condition": "index_rep2",
  "answer": "在 `koyeb/action-git-deploy@v1` 那一步的 `with:` 里加两个入参：\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\n- **`git-builder: \"docker\"`** 就是你要的那个入参，值必须是字符串 `\"docker\"`。不写它，Koyeb 会扫到仓库里的 `requirements.txt` / `package.json` 之类文件，回退到 buildpack（日志里会出现 `Installing Python 3.x` 这种字样）。\n- 配套把 **`git-docker-dockerfile: \"Dockerfile\"`** 也写上，路径相对仓库根目录；Dockerfile 不在根目录时改成实际路径（如 `\"docker/Dockerfile\"`）。\n- 顺带确认 Dockerfile 里有 `CMD`，且监听 `${PORT:-8000}`、绑定 `0.0.0.0`，否则即使构建方式对了也会报 `no command to run your application` 或 502。\n- 另外记得在 Koyeb 控制台把 Autodeploy 关掉，部署时机交给 GitHub Actions。",
  "error": null,
  "opened": [
   "deployment_github_actions_koyeb"
  ],
  "requested": [
   "deployment_github_actions_koyeb"
  ],
  "opened_target": true,
  "prompt_tokens": 32353,
  "completion_tokens": 745,
  "sec": 14.6214280128479,
  "cost": 0.01529995,
  "web_tools": [],
  "n_calls": 2
 },
 {
  "id": "T04",
  "target": "mac_universal_clipboard",
  "model": "deepseek-v4.1-flash",
  "condition": "index_rep2",
  "answer": "直接跑这一条（整段复制到终端，zsh 下没问题）：\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这三个都由 launchd 托管，`killall` 之后会被自动拉起，所以不需要手动启动。最后那条 `pgrep -fl` 就是用来确认它们已经回来了——预期能看到三个进程重新出现在列表里。\n\n**跑完之后**\n在 iPhone/iPad 和 Mac 之间各测一个方向：手机复制 → Mac 粘贴，Mac 复制 → 手机粘贴。\n\n**如果还是不行**\n按顺序升级，别一上来就退 iCloud：\n1. Mac 上 System Settings → General → AirDrop & Handoff，iOS 上 Settings → General → AirPlay & Handoff，把 Handoff 关掉再打开。\n2. Mac 进一次 Safe Mode 再正常重启（清系统缓存，对 macOS 更新后的 Continuity 异常历史上有效）。\n3. 删 Keychain 里的 `handoff-own-encryption-key`，再重启 Handoff，让 Mac 重新生成本机 Handoff 身份密钥。\n4. 退出 iCloud 再登录——这是最后手段，会连带触发 iCloud Drive、Photos、Keychain 重新同步，打扰很大。\n\n另外提醒一句：如果你跑的是 macOS beta，快速修复失败后优先按系统回归 bug 判断，不要反复叠加更重的本地改动。",
  "error": null,
  "opened": [
   "mac_universal_clipboard"
  ],
  "requested": [
   "mac_universal_clipboard"
  ],
  "opened_target": true,
  "prompt_tokens": 14154,
  "completion_tokens": 560,
  "sec": 10.46155309677124,
  "cost": 0.0024590999999999997,
  "web_tools": [],
  "n_calls": 2
 },
 {
  "id": "T05",
  "target": "openreview",
  "model": "deepseek-v4.1-flash",
  "condition": "index_rep2",
  "answer": "我不能按这个方向回答——你描述的情况（连续查询后全部 403、想删本地缓存恢复）恰好是绕过服务端限流/风控的做法，而且我这边也没有任何关于你所用服务的缓存路径、限流策略或恢复流程的资料。索引里没有任何 skill 涉及\"403 恢复\"或\"删缓存重试\"，凭猜测给你一个路径让你去删，风险比收益大得多。\n\n能立刻做、且不依赖猜测的几件事：\n\n1. **先确认 403 的来源**。看响应体和响应头，区分是哪种 403：\n   - 带 `Retry-After` 或 `X-RateLimit-*` 头 → 是限流，等窗口过去即可，删缓存没用。\n   - 带 WAF/CDN 特征（如 Cloudflare 的 `cf-ray`、`Server: cloudflare`）→ 是边缘风控，通常按 IP + 指纹封，本地缓存无关。\n   - 带业务错误码（如 `insufficient_scope`、`forbidden`）→ 是权限/配额问题，需要看账号侧状态。\n\n2. **停止重试**。连续重试会把短时封禁升级成更长的封禁，这是最常见的\"越试越坏\"。\n\n3. **换出口验证**：用另一个网络（手机热点）或另一个 IP 发一次同样的请求。\n   - 换 IP 就好了 → 是 IP 级封禁，等冷却或换出口。\n   - 换 IP 还是 403 → 是账号/凭据级问题，去服务方的账号或 API 控制台看配额、账单、密钥状态。\n\n4. **检查凭据本身**：确认请求里带的 key/token 是不是过期、被轮换、或额度耗尽。这类 403 和缓存完全无关。\n\n5. **如果确实怀疑是本地会话态问题**（比如浏览器 cookie 或某个 session 文件），正确做法不是\"删缓存\"，而是**重新走一次登录/授权流程**拿到新凭据，并且先备份再动任何文件。\n\n如果你能告诉我具体是哪个服务、403 的响应头和响应体原文，我可以帮你判断是哪一类，再给对应的处理步骤。",
  "error": null,
  "opened": [],
  "requested": [],
  "opened_target": false,
  "prompt_tokens": 6603,
  "completion_tokens": 475,
  "sec": 5.571134805679321,
  "cost": 0.00127545,
  "web_tools": [],
  "n_calls": 1
 },
 {
  "id": "T06",
  "target": "workflow_bilibili_whisper_transcription",
  "model": "deepseek-v4.1-flash",
  "condition": "index_rep2",
  "answer": "**2–3 秒，单线程。** 这是这套工作流里明确写死的默认值，不要自己加码。\n\n具体做法：\n\n1. **单线程串行下载**，不要并发。并发是触发 B 站反爬的首要原因，比间隔时间更关键。\n2. **每个视频之间 sleep 2–3 秒**。批量任务里就按 3 秒取，稳妥优先。\n3. **带上 User-Agent / Referer headers**。光靠延迟不够，缺 header 一样会被拦。\n4. **只下载音频**（`-f bestaudio`），减少请求体积和可疑度。\n5. **记录失败 ID**，失败的重试而不是立刻重跑整批。\n\n如果 2–3 秒 + 单线程仍然被拦，说明问题不在频率，而在**客户端指纹**，加延迟没用。判断方法是：用同样的匿名 cookie 换成 plain `curl` 重放同一个 playurl 请求，如果 curl 返回 200 而 yt-dlp 返回 412，就是指纹被风控了，不是 cookie 失效。对应的绕行路线是走 HTML5 playurl 通道（`platform=html5&high_quality=1&fnval=0`），拿直链后用浏览器 UA + `Referer: https://www.bilibili.com/` 下载。这条路线封顶 720p MP4，做 ASR 够用，但不适合归档级音质。\n\n另外两个容易踩的点：\n\n- **`b23.tv` 短链不要直接喂给下载器**，先解析出规范 `BV` 号，并丢掉展开后的 query string（里面可能带分享元数据和浏览器标识）。\n- **`.info.json` 和展开后的 B 站 URL 属于私有运行时数据**，里面有签名 CDN 链接和 cookie 信息，只放在被 ignore 的运行时目录里，别提交、别贴进文档。\n\n转录阶段不受这个限制影响——那边是本地 CPU 密集任务，可以开 4–8 个进程并行。",
  "error": null,
  "opened": [
   "workflow_bilibili_whisper_transcription",
   "download_and_transcribe",
   "online_media"
  ],
  "requested": [
   "workflow_bilibili_whisper_transcription",
   "download_and_transcribe",
   "online_media"
  ],
  "opened_target": true,
  "prompt_tokens": 42564,
  "completion_tokens": 915,
  "sec": 14.908228158950806,
  "cost": 0.016933600000000004,
  "web_tools": [],
  "n_calls": 2
 },
 {
  "id": "T07",
  "target": "workflow_watchdog",
  "model": "deepseek-v4.1-flash",
  "condition": "index_rep2",
  "answer": "默认 **30 分钟（1800 秒）** 设一次巡检唤醒。\n\n调度动作：在 Claude Code harness 里用 **`ScheduleWakeup`**（其他 harness 才用 Process Launcher 的延时执行 / `POST /run` + `delay_seconds`）。\n\n醒来后自己判断，不用问用户：\n\n1. **真在忙**（有新输出、agent 在持续产出）→ 不管它，继续等或再设一次 wake-up。\n2. **鬼打墙卡住**（长时间无进展、同一步骤反复重试）→ 用 `TaskStop` kill 掉，拿已有部分结果推进，或换方法重做。\n\n两个补充要点：\n\n- 每次巡检要**看真实任务状态**（进程退出码、日志新增行、产出文件、DB 行），不要用\"过了多久\"推断进度。\n- 巡检 prompt 里写死**停止条件**（成功判据、不可恢复失败、最大运行时长/巡检次数上限），到点就报告结果并**不再排下一次**，避免无限巡检。",
  "error": null,
  "opened": [
   "skill_agentic_patrol",
   "workflow_watchdog"
  ],
  "requested": [
   "skill_agentic_patrol",
   "workflow_watchdog"
  ],
  "opened_target": true,
  "prompt_tokens": 30576,
  "completion_tokens": 479,
  "sec": 14.595970869064331,
  "cost": 0.014873800000000001,
  "web_tools": [],
  "n_calls": 2
 },
 {
  "id": "T08",
  "target": "workflow_parallel_subagents",
  "model": "deepseek-v4.1-flash",
  "condition": "index_rep2",
  "answer": "不行。`subagent_type` 必须是**已注册的 agent 名**，不是模型名。OpenCode 会执行 `agent.get(subagent_type)`，找不到同名 agent 就直接报 `Unknown agent type`。`deepseek/deepseek-v4-pro` 是 provider/model 入口，不能直接塞进 `subagent_type`。\n\n## 正确做法：把模型包成 agent\n\n在下面任一位置新建一个 agent 定义（按作用域选）：\n\n- 全局：`~/.config/opencode/opencode.json`\n- 项目级：项目根目录的 `opencode.json`\n- 或 `.opencode/agent(s)/*.md`\n\n给它起个名字（比如 `writer_deepseek` 或自定义的 `ds4_pro`），在定义里把 model 指到 `deepseek/deepseek-v4-pro`。注册成功后，`task` 调用里写这个名字：\n\n```json\n{\n  \"tool_uses\": [\n    {\n      \"recipient_name\": \"functions.task\",\n      \"parameters\": {\n        \"description\": \"中文改稿\",\n        \"subagent_type\": \"writer_deepseek\",\n        \"prompt\": \"LANGUAGE: 用中文思考与输出，除非引用原文标题或 API 名称。\\n任务：……\\n输出落盘到 tmp/<session_slug>/draft.md，最后一条消息只返回路径、状态、关键结论和未解决问题。\",\n        \"task_id\": \"\",\n        \"command\": \"\"\n      }\n    }\n  ]\n}\n```\n\n## 几个要点\n\n1. **不要用旧路由名**。`category=\"deep\"`、`category=\"writing\"`、`librarian`、`ultrabrain`、`glm51` 这些在当前环境都不能用，除非它们已经在 `opencode.json` 里被注册成同名 agent。\n2. **现成可用的 DeepSeek 路线**：如果只是要 DeepSeek Pro 的中文写作能力，直接用已注册的 `writer_deepseek`；如果要求 zero-data-retention 且需要更强的 DeepSeek Pro，用 `ollama_deepseek_pro`（较贵）；隐私敏感、本机执行优先用 `private_ds4`。这些都不用你自己配。\n3. **并行写法**：必须在**同一条 assistant 消息**里用 `multi_tool_use.parallel` 一次性发出所有 `functions.task`。逐个连续调用即使嘴上说\"并行\"，实际也是串行。\n4. **别配 `temperature: 0`**：除非明确知道某个模型需要固定采样，否则让 provider/server 用自己的默认值。\n5. **验证是否注册成功**：注册后先单独发一个 `task` 试跑，如果返回 `Unknown agent type`，说明名字没对上或文件位置/格式不对。\n\n一句话：`subagent_type` 填 agent 名，模型在 agent 定义里指定；想用 `deepseek/deepseek-v4-pro`，就先把它包成一个 agent。",
  "error": null,
  "opened": [
   "workflow_parallel_subagents"
  ],
  "requested": [
   "workflow_parallel_subagents"
  ],
  "opened_target": true,
  "prompt_tokens": 41490,
  "completion_tokens": 1294,
  "sec": 17.225244998931885,
  "cost": 0.0169999,
  "web_tools": [],
  "n_calls": 2
 },
 {
  "id": "T09",
  "target": "codex_cli",
  "model": "deepseek-v4.1-flash",
  "condition": "index_rep2",
  "answer": "要关掉 `web_search`，写这一行：\n\n```toml\nweb_search = \"disabled\"\n```\n\n具体要点：\n\n**1. 值必须是字符串 `\"disabled\"`，不能用布尔值。**\n在 Codex CLI 0.156.1 上，`web_search` 是 string unit variant，只有 `\"disabled\"` 能解析通过。`false`、`on`、`off`、`enabled` 都会被拒绝，报错 `expected string only in 'web_search'`。所以 `web_search = false` 是错的。\n\n**2. 层级：裸的顶层 key，不要放进任何 `[section]` 里。**\nTOML 里裸 key 会绑定到它前面最近的那个 table header，所以必须把它放在所有 `[section]` 头**之前**（文件顶部区域），否则它会被归到某个 table 下面，不生效。\n\n**3. 放哪个文件：**\n- 基础配置 `$CODEX_HOME/config.toml`，或\n- 这次运行实际加载的 profile 文件（profile 文件会叠加在基础配置之上）。\n\n两者任选其一即可；如果只想对某个 profile 生效，就写进那个 profile 文件。\n\n**4. 为什么需要它：**\n当前 Codex 构建每一轮都会发送 `web_search` 工具定义。不实现该工具的 OpenAI 兼容后端会直接以 HTTP 400 `the web_search tool is not supported` 让整次运行失败。关掉后就不会再声明这个工具。\n\n**5. 验证方式：**\n改完配置后跑一次实际调用，确认不再出现 400；升级 Codex 后要重新确认这个 key 的取值是否仍被接受（该 CLI 的 flag/配置在 minor 版本间会变）。",
  "error": null,
  "opened": [
   "codex_cli"
  ],
  "requested": [
   "codex_cli"
  ],
  "opened_target": true,
  "prompt_tokens": 39185,
  "completion_tokens": 790,
  "sec": 12.585923910140991,
  "cost": 0.016351749999999998,
  "web_tools": [],
  "n_calls": 2
 },
 {
  "id": "T10",
  "target": "ai_session_profanity_rate",
  "model": "deepseek-v4.1-flash",
  "condition": "index_rep2",
  "answer": "不能混着喂。这是明确禁止的：**不要把归档和它对应的原始会话库放进同一次统计**——同一批消息会被数两遍。\n\n具体做法：\n\n**1. 二选一，不要叠加**\n\n- 优先用**原始会话库**（native stores）。理由：保留精确时间戳，per-message 的模型归属更强，统计口径更准。\n- 只有在需要可移植性、或者本机没有对应原始库时，才改用**归档**（archive）模式，并且只选归档这一个输入源。\n\n**2. 如果确实要用归档**\n\n```\nai-session-profanity-rate prepare \\\n  --source archive \\\n  --archive-dir ~/.local/share/ai-session-export \\\n  --timezone <导出机器所用的时区> \\\n  --classifier-profile <已批准的 profile>\n```\n\n注意 `--timezone` 必须填**导出那台机器**用的时区，因为归档里存的是会话日期加本地 `HH:MM`，跨午夜要靠 turn 顺序推断。\n\n**3. 关于重复的机制**\n\n重复不是\"可能\"，是必然：归档就是从同一批原始会话导出的，两边是同一份消息的两个副本。所以\"一起喂进去更全\"这个直觉在这里是错的——它不会补全任何东西，只会让每条消息的计数翻倍，把发生率算成两倍。\n\n**4. 真正需要\"更全\"时怎么办**\n\n- 归档比原始库旧 → 用原始库。\n- 原始库缺某个源 → 只补那个源，别把已有的源再叠一遍。\n- 归档里有 `turn_models` 数组（新归档才有）→ 可以拿到 per-turn 模型归属；老归档没有这个字段，会落到 `archive_unavailable` / `Unknown`，**不要**从会话级的 `models_used` 去猜。\n\n**5. 顺带一个坑**\n\n如果某个源缺 per-turn 模型元数据，而用户明确确认该窗口内全程只用一个模型，才可以加 `--assume-source-model SOURCE=MODEL`。它只填缺失值，并记为 `configured_source_default`；不能自己推断，也不能默认套用。\n\n一句话：**同一次运行里，归档和它的原始库是互斥的，选一个。**",
  "error": null,
  "opened": [
   "ai_session_search_archive",
   "skill_opencode_data",
   "skill_ai_usage_dashboard",
   "ai_session_profanity_rate"
  ],
  "requested": [
   "ai_session_search_archive",
   "skill_opencode_data",
   "skill_ai_usage_dashboard",
   "ai_session_profanity_rate"
  ],
  "opened_target": true,
  "prompt_tokens": 45055,
  "completion_tokens": 1029,
  "sec": 17.666754245758057,
  "cost": 0.01737565,
  "web_tools": [],
  "n_calls": 2
 },
 {
  "id": "T11",
  "target": "apple_photos",
  "model": "deepseek-v4.1-flash",
  "condition": "index_rep2",
  "answer": "不能一次性发起删除，也不能复用或重试授权。按这个 skill 的规则：\n\n**1. 不存在\"批量删除\"这个动作**\n这个 workflow 的边界写得很明确：它只做扫描、聚类、推荐 canonical、导出 review 产物；**apply/trash 步骤在用户确认 review 产物之前属于 out of scope**。所以没有\"一次发起 120 对删除\"的入口，也没有\"每批最多多少对\"的分批上限——因为压根不是按批删除，而是先出一份人工审阅文件，再由你逐行决定。\n\n**2. 正确做法：先审阅，再逐行标记**\n- 打开 `source_identification/music_library_dedup_review.html`，逐个 cluster 看封面、格式、所在库、canonical 推荐。\n- 在 `source_identification/music_library_dedup_review.csv`（或配对的 JSON）里改：\n  - 想删的重复项设 `user_action=trash`\n  - 不确定的**保持 `user_action=pending`**，不要设成 trash\n- 每个 cluster 必须恰好有一行 `is_canonical=true`，canonical 行不能标 trash。\n\n**3. 关于\"授权复用\"和\"对不确定的批次再试一次\"**\n- 授权不是一次性的批量令牌，而是**针对你确认过的那份 review CSV** 的显式批准。你改了 CSV 之后，之前的批准就失效了，需要重新明确告诉 agent \"这份 review CSV 已批准\"。\n- 对结果不确定的行，正确操作是**留在 `pending`**，而不是\"先试一次删除看看\"。skill 明确禁止在 review 生成阶段跑任何 trash/move 命令，也不允许未经审阅就自动删除。\n\n**4. 特别提醒（120 对里很可能踩到的坑）**\n- 标题相同但**艺术家不同**的组，只出现在 `music_library_near_duplicates.csv`，**不能**从 duplicate review CSV 里删。\n- 带 `Live`、`Cover`、`伴奏`、`串烧` 等版本标记的 cluster 会被标成 `needs_review`，即使 title/artist 完全一致也不能当等价重复处理。\n- `has_tag_title=false` 或 `has_tag_artist=false` 的行是低置信度条目（文件名回退可能编造出 title/artist），审阅时要格外小心。\n\n所以：不要想\"分批删\"，而是把 120 对全部过一遍 review 页面，把确定的标 `trash`、不确定的留 `pending`，然后一次性把这份 CSV 交给 agent 批准。",
  "error": null,
  "opened": [
   "music_library_dedup"
  ],
  "requested": [
   "music_library_dedup"
  ],
  "opened_target": false,
  "prompt_tokens": 27119,
  "completion_tokens": 1167,
  "sec": 14.181110620498657,
  "cost": 0.01476805,
  "web_tools": [],
  "n_calls": 2
 },
 {
  "id": "T12",
  "target": "asr",
  "model": "deepseek-v4.1-flash",
  "condition": "index_rep2",
  "answer": "不能直接用，会在**模型加载之前**就被拒绝。\n\n依据（asr skill 的 Testing and Boundaries 一节）：\n\n> A single file longer than 7200 seconds is rejected before a model loads.\n\n7200 秒 = 2 小时整。你的录音约 2 小时 10 分钟（≈7800 秒），超过阈值，所以 `asr-skill run`（或 diarize 阶段）在加载 Nemotron / Qwen3-ASR 之前就会直接报错退出，不会浪费模型加载时间。\n\n**切成多段能解决，但有两个代价要接受：**\n\n1. **长度限制按单个文件算，不按总量。** skill 明确写了：\n   - \"Splitting a file makes separate files\"\n   - \"Several files that add up to a long day are not one file over the limit\"\n   \n   所以把 2h10m 切成比如 3 段各 ~45 分钟，每段都远低于 7200 秒，就能正常处理。\n\n2. **说话人标签不会跨切片延续：**\n   > a speaker letter does not continue across slices\n   \n   切片 A 里的 `SPEAKER_00` 和切片 B 里的 `SPEAKER_00` 不保证是同一个人。每段独立做 diarization，得到各自的声学标签。你需要在 `prepare-edit` / `finalize-edit` 阶段用 `name_map` 手工把各段标签映射到真实姓名（skill 支持 `--confirmed-name-map` 预置映射，以及 `name_map` 里 `status: mapped` + 非空 `name` 来发布映射名）。\n\n**建议做法：**\n\n1. 用 ffmpeg 切分，尽量切在长静音处，例如：\n   ```\n   ffmpeg -i input.wav -f segment -segment_time 2700 -c copy part_%03d.wav\n   ```\n   （2700 秒 = 45 分钟，留足余量。）\n2. 对每个切片分别跑 `asr-skill run`（或 diarize + align）。\n3. 每段单独 `prepare-edit` → 写 `edited.json` → `finalize-edit`，在 `name_map` 里把该段的声学标签映射到真实说话人。\n4. 最后把各段输出 CSV 按时间顺序拼接。注意每段的时间戳是段内相对时间，需要加上该段的起始偏移。\n\n另外提醒：切片边界如果正好切在一句话中间，那半句话的归属可能出错，所以切点尽量选在长静音处。",
  "error": null,
  "opened": [
   "asr"
  ],
  "requested": [
   "asr"
  ],
  "opened_target": true,
  "prompt_tokens": 40136,
  "completion_tokens": 1163,
  "sec": 12.031534194946289,
  "cost": 0.0167182,
  "web_tools": [],
  "n_calls": 2
 },
 {
  "id": "T13",
  "target": "ct_education",
  "model": "deepseek-v4.1-flash",
  "condition": "index_rep2",
  "answer": "不能填绝对路径。\n\n`serve` 的 `--video-file` / `--rendered-video-file` 只接受**已完成 workspace 内部**、已存在的普通 `.mp4` 文件的**相对路径**。绝对路径、`..` 穿越、符号链接、文件不存在、非 `.mp4` 后缀，都会在启动时直接失败并报 `E_VIDEO_FILE`。而且 HTTP 请求方也无法另外指定文件——可选文件在启动时就固定了。\n\n所以 `/tmp/xxx.mp4` 这种写法一定起不来。正确做法：\n\n1. 先把 MP4 复制进那个已完成的 workspace 目录里（例如 `/path/to/external/workspace/demo.mp4`）。workspace 必须是 `build` 成功、`manifest.json` 已生成的完整 workspace。\n2. 用相对 workspace 的路径启动：\n\n```bash\nct-edu serve --workspace /path/to/external/workspace --video-file demo.mp4\n```\n\n两个槽位按用途区分：\n\n- `--video-file`：界面演示（demo），下载名固定为 `ct-education-tour.mp4`。\n- `--rendered-video-file`：渲染影片（film），下载名固定为 `ct-education-film.mp4`。\n\n两个 flag 可以单独用，也可以一起用；同时存在时默认播放 demo，只有 rendered 时回退到它。视频默认关闭，不传 flag 时 `/api/video-info` 返回 `{\"available\":false}`。\n\n几点容易踩的：\n\n- 不要指望直接 serve `/tmp` 或任意输出目录。这个 viewer 只允许 serve workspace 内被显式选中的那两个 MP4，不是所有输出都能被服务。\n- 如果这个 MP4 是 `render-cinematic` 的产物，它的 run 目录和 workspace 是**不相交**的，必须先把最终成片复制进 workspace 才能被 serve；而这一步需要显式批准，渲染完成本身不构成暴露授权。\n- 复制进去后文件名保持 `.mp4` 后缀，别用符号链接指过去（symlink 会被拒）。\n- 端口默认 8787，只监听 loopback，没有 host 覆盖或公网发布选项。\n\n判断某个文件能不能被 serve，标准就一条：它是否位于那个已完成 workspace 内部、是普通文件、后缀 `.mp4`、且用相对路径传入。",
  "error": null,
  "opened": [
   "ct_education"
  ],
  "requested": [
   "ct_education"
  ],
  "opened_target": true,
  "prompt_tokens": 40089,
  "completion_tokens": 1044,
  "sec": 10.75671911239624,
  "cost": 0.016639750000000002,
  "web_tools": [],
  "n_calls": 2
 },
 {
  "id": "T14",
  "target": "dataforseo",
  "model": "deepseek-v4.1-flash",
  "condition": "index_rep2",
  "answer": "40104 不是认证失败，是**账号已认证（authenticated）但未完成验证（verified）**。这正好解释你看到的现象：`balance` 这类账户接口能通，但所有关键词数据接口（Live 和 Sandbox 都一样）被挡。\n\n## 怎么解决\n\n1. **登录 DataForSEO 控制台**（https://app.dataforseo.com/），进账号/资料页面，完成身份验证流程。通常需要：\n   - 补全公司/个人信息\n   - 提交身份或企业验证材料（视账号类型而定）\n   - 有时需要绑定有效的付款方式\n2. **确认验证状态**：在面板里看到账号状态为 verified / active 之后，再重跑命令。\n3. **验证通过后重试**，先小 limit 试探：\n\n```bash\n.venv/bin/dataforseo-skill keyword-suggestions \"agent harness\" --limit 3 --location-code 2840 --language-code en\n```\n\n## 不要做的事\n\n- **不要反复切 sandbox**。Sandbox 走的是同一套账号权限校验，未验证状态下同样返回 40104，切换 base-url 解决不了问题。\n- **不要重新生成或轮换 API 凭据**。40104 与 Base64 token 无关；如果 balance 能返回，说明 `DATAFORSEO_AUTH_BASE64`（或 `DATAFORSEO_LOGIN` / `DATAFORSEO_PASSWORD`）本身是对的。\n- **不要怀疑 CLI 或参数**。`--location-code`、`--language-code`、`--limit` 写错会返回别的错误码，不会是 40104。\n\n## 排查顺序（照这个走）\n\n1. `dataforseo-skill balance` 能通 → 凭据没问题，排除认证层。\n2. 关键词命令报 40104 → 直接判定为账号未验证。\n3. 去面板完成验证 → 等状态生效（有时有几分钟延迟）。\n4. 重跑关键词命令，仍报 40104 就联系 DataForSEO 支持，附上账号 ID 和报错时间。\n\n一句话：**问题在 DataForSEO 账号的验证状态，不在你的 CLI、凭据或 sandbox 配置。去面板把验证做完。**",
  "error": null,
  "opened": [
   "dataforseo"
  ],
  "requested": [
   "dataforseo"
  ],
  "opened_target": true,
  "prompt_tokens": 28649,
  "completion_tokens": 997,
  "sec": 10.670606136322021,
  "cost": 0.01489555,
  "web_tools": [],
  "n_calls": 2
 },
 {
  "id": "T15",
  "target": "eink_diary",
  "model": "deepseek-v4.1-flash",
  "condition": "index_rep2",
  "answer": "不用自己改时间窗，直接用 `--full-day`：\n\n```bash\neink-diary run --full-day\n```\n\n**你现在凌晨 1:30 跑，取的是昨天完整一天**，即 `昨天 00:00 .. 今天 00:00`。\n\n规则是（skill 里写死的语义）：\n\n- 本地时间 **00:00–02:00 之间** → `--full-day` 自动解释为**昨天完整一天**（`yesterday 00:00 .. today 00:00`）。你现在属于这一档。\n- **其他时间** → 解释为**今天 00:00 到当前时刻**。\n\n所以不要手算 `--minutes` 去凑全天，skill 明确要求用户说\"今天全天/全天版本\"时优先用 `--full-day`。\n\n两个附带提醒：\n\n1. 归档槽的 `HHMM` 是**时间窗右端**（刷新时刻）。这次右端是今天 00:00，所以会落在 `DIARY_ARCHIVE_DIR/<今天日期>/0000/` 下，别按\"昨天\"的目录去找。\n2. 想回放某个历史全天窗口做测试，可以显式指定右端，例如 `eink-diary run --full-day --end 2026-06-06T00:00`。",
  "error": null,
  "opened": [
   "eink_diary"
  ],
  "requested": [
   "eink_diary"
  ],
  "opened_target": true,
  "prompt_tokens": 36215,
  "completion_tokens": 611,
  "sec": 24.825642347335815,
  "cost": 0.01579885,
  "web_tools": [],
  "n_calls": 2
 },
 {
  "id": "T16",
  "target": "skill_google_maps_routing",
  "model": "deepseek-v4.1-flash",
  "condition": "index_rep2",
  "answer": "直接回答你的两个问题：\n\n**1. 是不是只能二选一？**\n是的。`route` 命令要求 `--departure-time` 和 `--arrival-time` **恰好提供其中一个**（exactly one），不能都传，也不能都不传。\n\n- 想算\"现在出发\"的驾驶时间 → 用 `--departure-time now`\n- 想算\"要在某时刻到达，该几点走\" → 用 `--arrival-time <ISO 时间戳>`，返回里会带 `leave_by_time`\n\n**2. 传一个接近当前时刻的 ISO 时间戳会不会被拒？**\n会，这正是要避免的坑。Routes API 可能把\"不够未来\"的时间戳判为非法，报错 `Timestamp must be set to a future time`。所以 CLI 特意不发送脆弱的、贴近当前时刻的显式时间戳。\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- 用字面量 `now`，不要自己拼一个当前时刻的 ISO 时间戳。\n- 如果确实要指定时间，选一个**明显在未来**、且带时区偏移的 ISO 时间戳（例如 `2026-04-05T13:00:00-07:00`）。\n- 加 `--format json`，以退出码 0 + 可解析 JSON 作为成功信号；驾驶时间读 `route.duration_seconds`（含实时路况），`route.static_duration_seconds` 是无路况基线。\n- 先跑一次 `scripts/google_maps_routing doctor --format json` 确认凭证和端点正常，再跑 route。",
  "error": null,
  "opened": [
   "skill_google_maps_routing"
  ],
  "requested": [
   "skill_google_maps_routing"
  ],
  "opened_target": true,
  "prompt_tokens": 28610,
  "completion_tokens": 802,
  "sec": 8.23996901512146,
  "cost": 0.014772700000000001,
  "web_tools": [],
  "n_calls": 2
 },
 {
  "id": "T17",
  "target": "hybrid_ai_video",
  "model": "deepseek-v4.1-flash",
  "condition": "index_rep2",
  "answer": "不能直接重发。超时只说明你没拿到响应，不代表请求没到达服务端——任务很可能已经建成，重发会产生重复任务、重复扣费，甚至触发并发限制。\n\n按下面顺序做：\n\n1. **先查，不要先建。** 用创建接口返回体里通常会带的 `task_id` / `request_id`（如果超时前拿到了部分响应），或者用你的账号维度去查任务列表接口（`GET /tasks`、`/v1/video/generations` 之类的列表/查询端点），按时间倒序看最近几分钟有没有新任务。有 → 直接拿它的 id 去轮询状态，不要重发。\n\n2. **如果列表里查不到，用幂等键重发。** 检查该接口是否支持 `Idempotency-Key` 请求头或客户端传入的 `request_id` / `client_token` 字段。支持的话，用**和第一次完全相同**的幂等键重发，服务端会返回同一个任务而不是新建。这是唯一安全的重发方式。\n\n3. **如果既查不到、也不支持幂等键**，就等一个明确的窗口再判断：多数视频生成服务在创建阶段是同步返回 task_id 的，超时通常发生在网关层。等 30–60 秒后再查一次任务列表；仍然没有，才认为第一次请求没落地，此时重发。重发时把超时时间调长（比如 60s 以上），并记录下这次的时间戳，方便下次对账。\n\n4. **事后对账。** 无论走哪条路，事后都去账单/用量页面核对这段时间内的任务数和扣费次数。如果发现重复任务，立刻取消多余的那个（多数服务支持 `DELETE /tasks/{id}` 或 cancel 端点），并保留截图作为申诉依据。\n\n一句话原则：**创建类接口超时后，先做一次带时间窗的查询，再决定是否重发；能带幂等键就带幂等键。**",
  "error": null,
  "opened": [],
  "requested": [],
  "opened_target": false,
  "prompt_tokens": 6599,
  "completion_tokens": 430,
  "sec": 4.817746162414551,
  "cost": 0.00124785,
  "web_tools": [],
  "n_calls": 1
 },
 {
  "id": "T18",
  "target": "ecg_plot",
  "model": "deepseek-v4.1-flash",
  "condition": "index_rep2",
  "answer": "直接拒绝出图，不会自动剪掉。\n\n具体行为：核对项里明确要求「全部样本落在 0 到 30 秒内」，超出 6 条 5 秒窗口的点**不会被裁掉，而是拒绝作图**。所以你那批落在 30 秒以后的采样点会让核对失败。\n\n失败时的表现是：\n\n- 仍然写出完整性 JSON（`--integrity-output` 指定的那个文件），里面记录测量范围等信息\n- **不写 PNG**，避免把截断的数据画成一条看起来完整的曲线\n- 终端只输出状态、比例提示和路径\n\n也就是说，你会拿到一份完整性报告，但拿不到图。要出图，得先把输入 JSON 处理成只含 0–30 秒内的样本（并且保证 `truncated` 为 false、`voltage_status` 为 complete、时间严格递增且间隔与 `sampling_frequency_hz` 一致），再重新跑一次。\n\n另外注意两点：输出文件名必须是新路径，已存在的图或完整性报告会被拒绝覆盖；输入输出只能放在 `data/` 下且已被 gitignore 的目录（如 `data/exports/`、`data/raw/`）。",
  "error": null,
  "opened": [
   "ecg_plot"
  ],
  "requested": [
   "ecg_plot"
  ],
  "opened_target": true,
  "prompt_tokens": 13943,
  "completion_tokens": 267,
  "sec": 5.025188207626343,
  "cost": 0.00225165,
  "web_tools": [],
  "n_calls": 2
 },
 {
  "id": "T19",
  "target": "koyeb",
  "model": "deepseek-v4.1-flash",
  "condition": "index_rep2",
  "answer": "`--tail 100` 不行。Koyeb CLI 的 `--tail` 是**布尔开关**，不接受行数参数——CLI 没有\"只拉最后 N 行\"这个选项。\n\n## 实时跟随当前日志\n\n```bash\npython3 scripts/koyeb_env.py -- services logs example-service --app example-app --tail\n```\n\n要点：\n\n1. **必须走 launcher**：所有 Koyeb 命令都通过 `scripts/koyeb_env.py` 执行，避免 token 出现在 argv 或 shell history 里。launcher 会拒绝 `--token`、`--debug-full`、`--url`（退出码 2）。\n2. **`--tail` 单独用**，后面不要跟数字。它表示从当前时刻开始流式跟随，会一直挂着输出新日志。\n3. **多组织/多项目时显式指定**：在转发参数里加 `--organization <org>` 或 `--project <proj>`，否则可能打到错误的 workspace。\n4. **退出**：`Ctrl-C` 即可，launcher 原样透传子进程退出状态。\n\n## 如果你确实想要\"最近 100 行\"这种有界结果\n\nCLI 没有行数限制，只能用**时间窗**界定，再自己截取：\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这是历史日志查询（UTC 时间戳、有界），不会挂住。要\"最后 100 行\"就取一个足够近的时间窗，再用 shell 的 `tail` 截断：\n\n```bash\npython3 scripts/koyeb_env.py -- services logs example-service --app example-app --type runtime \\\n  --start-time <近端时间> --end-time <当前时间> | tail -n 100\n```\n\n注意区分：`tail -n 100` 是 shell 的 `tail`，不是 Koyeb 的 `--tail`，两者别混。\n\n## 两个容易踩的坑\n\n- **有界调用方要自己控制流式超时**：`--tail` 是无限流，脚本里调用必须加超时，否则会永久挂住。\n- **根路径 HTTP 404 不等于服务挂了**：诊断时看容器状态和实际路由路径，别只看 404 就下结论。",
  "error": null,
  "opened": [
   "koyeb"
  ],
  "requested": [
   "koyeb"
  ],
  "opened_target": true,
  "prompt_tokens": 33602,
  "completion_tokens": 1177,
  "sec": 9.9022798538208,
  "cost": 0.0157465,
  "web_tools": [],
  "n_calls": 2
 },
 {
  "id": "T20",
  "target": "narwal",
  "model": "deepseek-v4.1-flash",
  "condition": "index_rep2",
  "answer": "## 为什么是 NOT_READY\n\n`start`（全屋清扫）和 `clean`（指定房间）底层都走 `clean/start_clean` 这个 topic，而这个命令**只在机器人处于 dock（充电座）上时才生效**。你在客厅直接喊它开始全屋清扫，机器人不在座上，于是它回 `NOT_READY`，CLI 以 exit code `20`（`EXIT_COMMAND`）退出。\n\n关键点：**这是机器人的回答，不是 bug**。命令已经送达并被机器人明确拒绝。绝对不要循环重试——重试只会得到同样的 `NOT_READY`。\n\n## 怎么做它才会接受\n\n1. **先让它回座。** 用 `narwal-local dock --yes`（对应 `supply/recall`）。注意 `dock` 本身会结束当前任务，所以不要在它后面再叠一个 `stop`。\n2. **确认它真的在座上。** 不要看 `is_docked` / `charging_state`——这两个字段**永远是 `null`**。判断依据是**电池趋势**：跑一个 `narwal-local watch --duration 20`，看 `battery_percent` 是否在上升；或者对比两次 `snapshot` 的电池值。上升即说明在座上充电。\n3. **再发清扫命令。**\n   - 全屋：`narwal-local start --yes`\n   - 指定房间：`narwal-local clean --rooms 3,5 --yes`\n4. **建议先 dry-run。** 加 `--dry-run` 可以只构建 payload、不发送控制 topic，确认 topic 和参数无误后再去掉它正式执行。\n\n## 几个容易踩的坑\n\n- **房间号必须来自当前地图。** `--rooms` 的 id 和 `map_id` 要从当前的 `narwal-local map` / `get_map` 里取，工具不会把房间 id 映射成\"客厅\"\"卧室\"这类家庭名称。所以\"客厅\"这个说法本身对 CLI 无意义，你得先拿到 id。\n- **一次只能一个连接。** 每个源 IP 只允许一条连接，别并行跑命令。\n- **控制命令必须带 `--yes`**，否则 exit `2`（`EXIT_USAGE`）。每次写入会追加一行到 `control_audit.jsonl`（除非 `--no-audit`）。\n- **`stop` 很慢是设计如此**（机器人要物理停下才应答），别把等待当失败。\n- **如果回的是 `CONFLICT` 或 `NOT_APPLICABLE`**，同样是机器人在拒绝，含义不同但处理原则一样：先 `snapshot` 看当前 `base_query.mode` 和 `task`，判断它是不是正在执行别的任务，而不是重试。\n\n## 顺带说明边界\n\n- 这个工具**没有历史记录**，所以\"它刚才为什么停了\"这类问题它答不了，只能靠 App 的任务历史。\n- **回座后的洗拖布、烘干、集尘这些基站维护动作是永久禁止的**（`supply/wash_mop`、`supply/dry_*`、`supply/dust_gathering` 等），任何 flag 都放不开，得去 App 里做。所以\"回座然后洗拖布\"在这里只能做前半段。\n- 硬件验证范围只覆盖 **Narwal Flow 2**（固件 `v01.09.10.02`），其他型号是候选状态，控制 payload 未做硬件验证。",
  "error": null,
  "opened": [
   "narwal"
  ],
  "requested": [
   "narwal"
  ],
  "opened_target": true,
  "prompt_tokens": 39019,
  "completion_tokens": 1547,
  "sec": 12.195502042770386,
  "cost": 0.016781050000000002,
  "web_tools": [],
  "n_calls": 2
 },
 {
  "id": "T21",
  "target": "skill_open_router_data_scraper",
  "model": "deepseek-v4.1-flash",
  "condition": "index_rep2",
  "answer": "要传 **canonical_slug**，不是短 slug。\n\n具体做法：\n\n1. **短 slug 会被拒**：`model-activity` 端点不接受像 `anthropic/claude-sonnet-4` 这样的短标识，传进去只会返回空数组（不是报错，所以很容易误判成\"没数据\"）。\n\n2. **用 canonical_slug**：canonical_slug 是带末尾 8 位日期后缀的完整标识，形如 `anthropic/claude-sonnet-4-20250514`。手动调用时把 slug 换成这个带日期的版本即可拿到数据。\n\n3. **让 CLI 自动解析**：如果你用本项目的 `ords` CLI，不用自己拼日期——`ords fetch <slug>` 和 `ords archive` 会自动把短 slug 解析成 canonical_slug。所以最省事的路径是：\n\n```bash\nords discover --top 20     # 先看 Top-N 模型列表，拿到正确的 slug\nords fetch <slug>          # 单模型抓取，CLI 自动解析 canonical_slug\nords archive --top 20      # 批量抓取，补全层逐个走 model-activity\n```\n\n4. **注意请求头**：这些端点无需鉴权，但必须带 `Referer: https://openrouter.ai/`，否则也可能拿不到数据。\n\n5. **顺带一个坑**：如果你同时在看 `rankings/models`，记得用 `view=day`，不要用 `view=month`（month 返回的是累积值，数字会虚高约 12 倍）。",
  "error": null,
  "opened": [
   "skill_open_router_data_scraper"
  ],
  "requested": [
   "skill_open_router_data_scraper"
  ],
  "opened_target": true,
  "prompt_tokens": 14445,
  "completion_tokens": 358,
  "sec": 5.3940441608428955,
  "cost": 0.0023815499999999996,
  "web_tools": [],
  "n_calls": 2
 },
 {
  "id": "T22",
  "target": "skill_opencode_data",
  "model": "deepseek-v4.1-flash",
  "condition": "index_rep2",
  "answer": "这个 skill 只覆盖**检索**（lexical + semantic 搜索），不涉及按标题前缀批量归档，也没有任何关于\"子会话是否默认包含\"或\"只动选中会话本身\"的参数说明。所以按现有资料我无法回答你的问题——索引里也没有专门讲归档/批量选择参数的 skill（`ai_session_search_archive` 是唯一相关的，而它讲的是查找）。\n\n建议你直接查归档工具本身的 CLI 帮助，例如：\n\n```bash\n<archive-tool> --help\n<archive-tool> archive --help\n```\n\n重点看两类参数：\n\n1. **递归/包含子会话的开关**：常见命名是 `--recursive` / `--no-recursive`、`--include-children` / `--no-children`、`--with-descendants`。默认值通常写在 help 文本里（\"recursively include child sessions (default: true)\" 这类）。\n2. **选择器语义**：确认 `--title-prefix` 这类参数是匹配\"会话标题\"还是\"会话路径/树节点\"，两者对子会话的处理完全不同。\n\n如果你能告诉我具体是哪个归档工具（或把它的 `--help` 输出贴出来），我可以帮你判断默认行为和该加哪个参数。",
  "error": null,
  "opened": [
   "ai_session_search_archive"
  ],
  "requested": [
   "ai_session_search_archive"
  ],
  "opened_target": false,
  "prompt_tokens": 31089,
  "completion_tokens": 548,
  "sec": 7.211706876754761,
  "cost": 0.01499215,
  "web_tools": [],
  "n_calls": 2
 },
 {
  "id": "T23",
  "target": "video-scoring-audio",
  "model": "deepseek-v4.1-flash",
  "condition": "index_rep2",
  "answer": "你的测法确实有问题，问题就出在 `-ac 1` 这一步。\n\n## 为什么 `-ac 1` 会造出假削波\n\nffmpeg 的 downmix 是把左右声道**相加**（不是取平均）。对相关立体声——也就是同一个单声道混音同时写进左右两个声道，配音/旁白场景里最常见——相加等于约 +3 dB 的增益。\n\n所以一个真实峰值 -1.5 dBFS 的混音，downmix 之后测出来是 1.19，就被报成削波了。原始混音根本没削，是测量链自己加出来的。\n\n**规则：测峰值永远在原始声道上做，绝不在 downmix 之后做。**\n\n## 正确的测法\n\n保留声道，不做 downmix：\n\n```python\nxc, sr = load(\"out.wav\")          # 形状 (N, ch)，保留所有声道\nx = xc.mean(axis=1)               # 只有包络和频谱质心用单声道\n\n# 峰值：对每个声道取绝对值最大，不是对 downmix\nprint(float(np.max(np.abs(xc))))  # 必须 < 1.0\n```\n\n分工：\n- **峰值/削波** → 在 `xc`（原始多声道）上算，`np.max(np.abs(xc))`。\n- **RMS 包络、频谱质心** → 可以用 `x`（单声道平均），这两个量对 downmix 不敏感。\n- **时长** → `len(x)/sr`。\n\n## 交叉验证\n\n用 ffmpeg 的 ebur128 做独立复核，它自己处理声道、不经过你的 downmix：\n\n```bash\nffmpeg -i out.wav -af ebur128=peak=true -f null -\n```\n\n这会给出 integrated loudness（LUFS）和 true peak（dBTP）。两个数字要和你的 numpy 测量对得上：\n\n- numpy 的 `max(abs(xc))` 应该和 ebur128 报的 true peak 一致（差零点几 dB 正常，true peak 做了过采样插值）。\n- 如果 ebur128 说没削、你的脚本说削了，那一定是测量链有问题——先怀疑 downmix。\n\n## 顺带检查两件事\n\n1. **`loudnorm` 的目标是输出，不是输入。** `I=-16` 在测试里第一次就削到峰值 1.092。`I=-19:TP=-3:LRA=11` 是个合理起点，但峰值必须测完再确认。\n2. **如果是最终混音（音乐垫在旁白下），不要单遍 loudnorm。** 单遍会随时间自适应增益，重塑各层之间的平衡。要两遍、`linear=true`：先跑一遍 `print_format=json` 拿到测量值，再用那些值加 `linear=true` 跑第二遍，这样只施加一个固定增益，你设好的 voice-to-music 平衡不会被改掉。输出显式写 16-bit PCM，然后测结果。\n\n## 一句话总结\n\n`-ac 1` 是给播放/分析用的，不是给峰值测量用的。测峰值保留原始声道，用 `ebur128=peak=true` 交叉验证。你看到的削波是测量伪影，不是混音问题。",
  "error": null,
  "opened": [
   "video-scoring-audio"
  ],
  "requested": [
   "video-scoring-audio"
  ],
  "opened_target": true,
  "prompt_tokens": 38149,
  "completion_tokens": 1457,
  "sec": 10.857513189315796,
  "cost": 0.01659655,
  "web_tools": [],
  "n_calls": 2
 },
 {
  "id": "T24",
  "target": "speaker_notes",
  "model": "deepseek-v4.1-flash",
  "condition": "index_rep2",
  "answer": "口播稿的句子长度没有绝对硬性上限，但有几条可以直接照做的经验值：\n\n**核心数字**\n- 普通句子控制在 **12–18 个词**（中文约 **20–30 字**）最舒服。这是人一口气能说完、听众不用回想的长度。\n- 上限：**25 个词**（中文约 40 字）。超过就该拆。\n- 下限：别连续用 5 个词以下的短句，会显得碎、像念 PPT。\n\n**判断标准（比数字更可靠）**\n1. **一口气测试**：自己出声念，中途需要换气才能念完的句子，就是太长了。\n2. **一个句子一个意思**：如果句子里出现\"并且\"\"同时\"\"但是\"这类连接词，通常说明该拆成两句。\n3. **听一遍能记住**：口播是线性的，听众不能回看。句子末尾的信息如果让人忘了开头，就太长。\n\n**实操做法**\n- 写完先按标点切分，统计每句词数，把超过 25 词的挑出来拆。\n- 拆的时候优先在从句边界断，用句号而不是逗号——口播稿里逗号太多会让演讲者不知道该在哪里停顿。\n- 长句和短句交替：连续三句都在 20 词以上会让人疲劳；连续三句都在 8 词以下会显得没有信息量。\n- 关键结论句单独成句，压到 10 词以内，方便加重语气。\n\n**注意**\n- 这些是口播稿的标准。如果是书面报告或学术文本，句子可以长得多，不要套用。\n- 数字是起点不是铁律：语速快、内容熟悉的演讲者可以放宽到 20–22 词；面向非母语听众或需要同传的场合，压到 12–15 词更稳。\n\n如果你要，我可以帮你把现有稿子按句统计词数、标出超长句并给出拆分建议。",
  "error": null,
  "opened": [],
  "requested": [],
  "opened_target": false,
  "prompt_tokens": 6597,
  "completion_tokens": 429,
  "sec": 4.0579071044921875,
  "cost": 0.0012469500000000001,
  "web_tools": [],
  "n_calls": 1
 },
 {
  "id": "T25",
  "target": "skill_tavily",
  "model": "deepseek-v4.1-flash",
  "condition": "index_rep2",
  "answer": "不行，`search` 没有 `--stdout` 这个参数。\n\n具体行为：\n\n- `--stdout` 只存在于 `usage` 子命令（`python -m tavily_skill usage --stdout` 会把完整 payload 直接打到 stdout）。\n- 给 `search`（或 `extract`）传 `--stdout` 会被当成无法识别的参数，属于 usage error，**退出码是 2**。\n- 因为参数解析阶段就失败了，**不会真正发起任何网络请求**，也不消耗 Tavily credits；stdout 可能是空的，错误信息在 stderr。\n\n想要\"完整结果打印到控制台\"，正确做法是让 CLI 正常写文件，然后自己读那个文件打印：\n\n```bash\n./.venv/bin/python - <<'PY'\nimport json, subprocess, sys\nfrom pathlib import Path\n\nproc = subprocess.run(\n    [sys.executable, \"-m\", \"tavily_skill\", \"search\", \"latest AI news\"],\n    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)\npayload = json.loads(Path(status[\"output_path\"]).read_text(encoding=\"utf-8\"))\nprint(json.dumps(payload, ensure_ascii=False, indent=2))\nPY\n```\n\n几点提醒：\n\n- 先检查 `proc.returncode` 再 `json.loads(proc.stdout)`，失败时 stdout 可能为空。\n- 打开文件时用 status 里返回的**确切** `output_path`，不要用 glob 或\"最新 mtime\"去猜文件名。\n- 打印完整 payload 会把大量 `raw_content` 灌进上下文，建议在代码里只挑有界字段（title / url / 前几百字）打印，而不是整个 dump。\n- 如果只是想换落盘位置，用 `--output /path/to/file.json`；stdout 仍然只返回轻量 status 对象，不会内联完整结果。",
  "error": null,
  "opened": [
   "skill_tavily"
  ],
  "requested": [
   "skill_tavily"
  ],
  "opened_target": true,
  "prompt_tokens": 38226,
  "completion_tokens": 947,
  "sec": 7.7322821617126465,
  "cost": 0.0163021,
  "web_tools": [],
  "n_calls": 2
 },
 {
  "id": "T26",
  "target": "zun_music",
  "model": "deepseek-v4.1-flash",
  "condition": "index_rep2",
  "answer": "能改，但上限很硬：**每四小节最多一处**，也就是 8 小节里最多 2 处，而且默认只用在最后的终止式上。\n\n具体规则（来自 skeleton check）：\n\n1. **强拍必须对得上。** 4/4 里强拍是第 1 拍和第 3 拍。在这些位置上，你编配后的音高类（pitch class）必须和未改动的 `SOURCE` 转写完全一致。注意是 pitch class——八度移位、同音异名不算改动，但换音、加经过音落在强拍上就算。\n\n2. **每一处刻意偏离都要登记。** 在 `skeleton_allow` 里逐条列出，不能有未申报的偏离。`check()` 会拿它和实际偏离比对，对不上就报错。\n\n3. **配额是每 4 小节 1 处。** 8 小节 → 上限 2 处。这是硬上限，不是建议值。\n\n4. **惯例上只用 1 处。** 参考实现里 `skeleton_allow` 通常只有一条：最后落音改到小调主音（minor tonic）。这是最典型的、也是被接受的那一处。\n\n所以合规的做法是：\n\n- 先把原旋律原样转写成 `SOURCE`，一个音都别动。\n- 编配时把 ZUN 的语汇全部塞在骨架音**之间**——3-3-2 切分、长音提前半拍、长音收尾加 16 分音符回旋（如 C–D–C–B–C）、乐句间用上行 16 分音符跑动填掉休止。这些都不占 `skeleton_allow` 配额，因为它们不落在强拍上。\n- 只在终止式处留一处偏离，写进 `skeleton_allow`。\n- 跑 `python scripts/render_example.py <tune> --midi-only` 验证，非零退出就是超了。\n\n另外提醒一句：改得越多越容易翻车。参考会话里\"过度改造\"（转平行小调 + 密集装饰 + 乐句中途转调）被听者评价为\"和原曲完全没关系\"。骨架检查存在的意义就是拦住这个。",
  "error": null,
  "opened": [
   "zun_music"
  ],
  "requested": [
   "zun_music"
  ],
  "opened_target": true,
  "prompt_tokens": 28969,
  "completion_tokens": 995,
  "sec": 8.582839965820312,
  "cost": 0.01494235,
  "web_tools": [],
  "n_calls": 2
 },
 {
  "id": "T27",
  "target": "skill_image_generation",
  "model": "deepseek-v4.1-flash",
  "condition": "index_rep2",
  "answer": "`--quality` 在 Gemini 下**不生效**——这是设计如此，不是你的错觉。\n\nskill 里写得很明确：\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- 这个参数是 **GPT Image 系列**（`gpt-image-2`、`gpt-image-2.5-sunburst`、`gpt-image-2.5-flare`）专用的，而且 2.5 的档位比 2 更细（2.5 的 `high` 大约只有 2 的 `high` 四分之一成本，2.5 的 `max` 才相当于旧的 `high`）。\n- 传给 Gemini 时，CLI 不会报错，会**静默接受然后忽略**，所以出图结果完全一样。\n\n## 想真正控制 Gemini 出图质量，改用这些手段\n\n1. **换模型档位**，而不是换 quality：\n   ```bash\n   generate-image -p \"...\" -o output/x.jpg --model gemini-pro\n   ```\n   （默认是 `gemini-flash`，`gemini-pro` 是更强的档）\n\n2. **提分辨率 / 定比例**：\n   ```bash\n   generate-image -p \"...\" -o output/x.jpg --size 4K --aspect-ratio 16:9\n   ```\n\n3. **对已有图做放大**（Gemini 的 upscale 路径）：\n   ```bash\n   generate-image --upscale -i input/small.jpg -o output/small_4k.jpg --aspect-ratio 16:9\n   ```\n\n## 如果你确实需要 quality 分级\n\n那就把模型换成 GPT Image：\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 单次调用延迟高，默认 120s 的 bash timeout 常常不够，要显式设到 300s 以上；批量出图时用 `ThreadPoolExecutor` 并发跑（4–6 并发、每任务约 420s 超时），别串行。",
  "error": null,
  "opened": [
   "skill_image_generation"
  ],
  "requested": [
   "skill_image_generation"
  ],
  "opened_target": true,
  "prompt_tokens": 30868,
  "completion_tokens": 997,
  "sec": 8.418246984481812,
  "cost": 0.0152284,
  "web_tools": [],
  "n_calls": 2
 },
 {
  "id": "T28",
  "target": "skill_process_launcher",
  "model": "deepseek-v4.1-flash",
  "condition": "index_rep2",
  "answer": "你遇到的是 Process Launcher 的**精确时间冲突保护（exact-time collision guard）**：当一次定时 `POST /run`（用 `delay_seconds` 或 `run_at`）解析出的 `run_at` 与某个已存在的 pending/running 任务的 `run_at` **完全相等**时，服务返回 HTTP 409，`detail.error = \"schedule_conflict\"`，并在 `detail.conflicts` 里列出每个冲突任务（id、label、command、status、run_at）。**409 时不会创建任何东西**。\n\n## 情况一：两个都要保留\n\n把**同一个请求原样重发**，只加一个字段：\n\n```bash\ncurl -sf -X POST http://127.0.0.1:7997/run \\\n  -H 'Content-Type: application/json' \\\n  -d '{\"command\": [\"python\", \"job.py\"], \"label\": \"job_b\", \"run_at\": \"2026-06-10T09:00:00-07:00\", \"confirm_conflict\": true}'\n```\n\n`confirm_conflict` 默认 `false`，设为 `true` 即表示\"这个时间点撞车是我故意的\"，于是新任务与旧任务并存。两个任务各自有独立的 `scheduled_job_id`，之后可以分别审计和取消。\n\n## 情况二：用新的替换旧的\n\n先取消冲突的那个旧任务，再提交新任务：\n\n```bash\n# 1. 从 409 响应的 detail.conflicts 里拿到旧任务 id（或先查一遍）\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. 重新提交新任务（此时不再冲突，无需 confirm_conflict）\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\": \"job_new\", \"run_at\": \"2026-06-10T09:00:00-07:00\"}'\n```\n\n## 补充要点\n\n- **改时间也会触发同样的保护**：`PATCH /scheduled/{job_id}` 把 `run_at` 挪到另一个任务的同一时刻时，同样返回 409，同样用 `confirm_conflict: true` 放行。\n- **只匹配精确相等的时间**：两个任务相隔几秒提交、延迟相近但 `run_at` 不完全相同，不会冲突。\n- **只有 pending / running 的任务参与冲突判定**；已 completed / failed / cancelled / missed 的不会挡路。\n- **只有定时任务（`delay_seconds` / `run_at`）走这个守卫**；立即执行的 `POST /run` 不涉及。\n- 提交成功后如果响应里 `pid: 0`、`output_file: null`，那是正常的\"已排期、子进程还没起\"，用响应里的 `scheduled_job_id` 做后续取消和审计即可。",
  "error": null,
  "opened": [
   "skill_process_launcher"
  ],
  "requested": [
   "skill_process_launcher"
  ],
  "opened_target": true,
  "prompt_tokens": 38087,
  "completion_tokens": 1400,
  "sec": 10.003010749816895,
  "cost": 0.01655305,
  "web_tools": [],
  "n_calls": 2
 },
 {
  "id": "T29",
  "target": "skill_resend_email",
  "model": "deepseek-v4.1-flash",
  "condition": "index_rep2",
  "answer": "不能。`mail export-md` 只接受 `--days N`（相对天数），没有 `--after` / `--before` 参数，也不接受 ISO 时间戳。`mail download` 同样只有 `--days`。\n\n所以“最近两天”的正确流程是：\n\n**1. 先下载（把邮件拉到本地）**\n\n```bash\n.venv/bin/python -m outlook_skill.cli mail download --days 2 --limit 200 --format json\n```\n\n注意 `--days 2` 是相对当前时间的滚动窗口，不是自然日边界。默认抓 `Inbox` + `Archive`；如果你只想要收件箱：\n\n```bash\n.venv/bin/python -m outlook_skill.cli mail download --days 2 --limit 200 --folder Inbox --format json\n```\n\n**2. 再导出 Markdown**\n\n```bash\n.venv/bin/python -m outlook_skill.cli mail export-md --days 2 --folder Inbox\n```\n\n`export-md` 只处理**本地已缓存**的 `.eml`，所以必须先跑 download，否则导出为空。输出在 `data/mail/markdown/`，文件名形如 `{date}_{folder}_{subject-slug}_{short-graph-id}.md`，带 YAML frontmatter（`graph_id`、`folder`、`from`、`to`、`cc`、`subject`、`date`、`body_source`）。\n\n**3. 如果 `--days 2` 的滚动窗口不够精确，用 SQLite 按 `internaldate` 精确筛**\n\n`internaldate` 是 ISO-8601、可直接排序比较的列，这是唯一能真正按时间点筛选的地方：\n\n```bash\nsqlite3 data/mail/mail.db \\\n  \"SELECT graph_id, folder, subject, internaldate FROM messages \\\n   WHERE internaldate >= '2026-05-09T00:00:00' \\\n   ORDER BY internaldate DESC;\"\n```\n\n拿到 `graph_id` 后，用 `mail read --graph-id <id>` 读全文，或对这批 id 单独导出。\n\n**几个坑要避开：**\n\n- 别用 `mail list-local --limit 50` 找“最近的邮件”。它按 `id`（插入顺序）排序，不是按 `received_at` 倒序；库大了以后返回的是**最老的** 50 封。要按时间找就用上面的 SQLite 查询，或 `mail read --subject <substr> --latest`。\n- 重复跑同一时间窗是安全的：幂等键是 `(account, folder, graph, message_id)`，已下载的会进 `skipped_existing_count` 而不是重复写。\n- 同一封邮件在不同文件夹会各存一条记录，所以 `--folder Inbox` 和默认的 `Inbox+Archive` 结果条数会不一样。\n- `export-md` 默认不覆盖已有文件，需要重写加 `--force`。",
  "error": null,
  "opened": [
   "skill_outlook"
  ],
  "requested": [
   "skill_outlook"
  ],
  "opened_target": false,
  "prompt_tokens": 34997,
  "completion_tokens": 1298,
  "sec": 8.980951070785522,
  "cost": 0.01602835,
  "web_tools": [],
  "n_calls": 2
 },
 {
  "id": "T30",
  "target": "skill_typefully_twitter",
  "model": "deepseek-v4.1-flash",
  "condition": "index_rep2",
  "answer": "默认用 **long post（长帖）模式**。\n\n具体做法：\n\n1. **先确认账号能力**：问用户（或查本地 `.env`）Typefully/X 账号是否支持发长帖。\n2. **支持的话**，在本地 `.env` 里设：\n   ```\n   TYPEFULLY_TWITTER_DEFAULT_LONG_POST=true\n   ```\n   这样 `post count`、`post draft`、`post create` 默认就走长帖校验，不用每次加参数。\n3. **如果环境默认没开**，就在命令上显式加 `--long-post`：\n   ```bash\n   typefully-twitter post count --thread-file thread.md --long-post\n   typefully-twitter post draft --thread-file thread.md --long-post\n   ```\n\n关键点：文章分发帖里的\"一条帖子\"指的是**一个 Typefully post**，不是 280 字符的普通推文。不要自己套 280 字限制。\n\n**用户明确要短推时**（说了\"普通推文\"\"280 字内\"\"short tweet\"），加 `--no-long-post` 切回普通长度：\n\n```bash\ntypefully-twitter post count --text \"...\" --no-long-post\ntypefully-twitter post draft --text \"...\" --no-long-post\n```\n\n另外提醒：如果长帖模式下 `post count` 仍返回 `TOO_LONG`，不要为了塞进去而删内容——那通常说明内容本来就该用长帖模式，除非用户明确要求压成单条短推。",
  "error": null,
  "opened": [
   "skill_typefully_twitter"
  ],
  "requested": [
   "skill_typefully_twitter"
  ],
  "opened_target": true,
  "prompt_tokens": 37088,
  "completion_tokens": 674,
  "sec": 6.839810132980347,
  "cost": 0.0159676,
  "web_tools": [],
  "n_calls": 2
 }
]