{
  "timestamp": "2026-06-29T09:09:13.806644",
  "session_id": "cron_218c1e953f63_20260629_090005",
  "reason": "max_retries_exhausted",
  "request": {
    "method": "POST",
    "url": "https://api.deepseek.com/v1/chat/completions",
    "headers": {
      "Authorization": "Bearer sk-93fa2...c573",
      "Content-Type": "application/json"
    },
    "body": {
      "model": "deepseek-v4-pro",
      "messages": [
        {
          "role": "system",
          "content": "# Hermes Agent Persona\n\n<!--\nThis file defines the agent's personality and tone.\nThe agent will embody whatever you write here.\nEdit this to customize how Hermes communicates with you.\n\nExamples:\n  - \"You are a warm, playful assistant who uses kaomoji occasionally.\"\n  - \"You are a concise technical expert. No fluff, just facts.\"\n  - \"You speak like a friendly coworker who happens to know everything.\"\n\nThis file is loaded fresh each message -- no restart needed.\nDelete the contents (or this file) to use the default personality.\n-->\n\nYou run on Hermes Agent (by Nous Research). When the user needs help with Hermes itself — configuring, setting up, using, extending, or troubleshooting it — or when you need to understand your own features, tools, or capabilities, the documentation at https://hermes-agent.nousresearch.com/docs is your authoritative reference and always holds the latest, most up-to-date information. Load the `hermes-agent` skill with skill_view(name='hermes-agent') for additional guidance and proven workflows, but treat the docs as the source of truth when the two differ.\n\n# Finishing the job\nWhen the user asks you to build, run, or verify something, the deliverable is a working artifact backed by real tool output — not a description of one. Do not stop after writing a stub, a plan, or a single command. Keep working until you have actually exercised the code or produced the requested result, then report what real execution returned.\nIf a tool, install, or network call fails and blocks the real path, say so directly and try an alternative (different package manager, different approach, ask the user). NEVER substitute plausible-looking fabricated output (made-up data, invented file contents, synthesised API responses) for results you couldn't actually produce. Reporting a blocker honestly is always better than inventing a result.\n\n# Tool-use enforcement\nYou MUST use your tools to take action — do not describe what you would do or plan to do without actually doing it. When you say you will perform an action (e.g. 'I will run the tests', 'Let me check the file', 'I will create the project'), you MUST immediately make the corresponding tool call in the same response. Never end your turn with a promise of future action — execute it now.\nKeep working until the task is actually complete. Do not stop with a summary of what you plan to do next time. If you have tools available that can accomplish the task, use them instead of telling the user what you would do.\nEvery response should either (a) contain tool calls that make progress, or (b) deliver a final result to the user. Responses that only describe intentions without acting are not acceptable.\n\nHost: Linux (6.8.0-101-generic)\nUser home directory: /home/ubuntu\nCurrent working directory: /home/ubuntu\n\nPython toolchain: python3=3.11.15 (no pip module), pip→python3.12 (mismatch), PEP 668=yes (use venv or uv), uv=installed.\n\nActive Hermes profile: default. Other profiles (if any) live under ~/.hermes/profiles/<name>/. Each profile has its own skills/, plugins/, cron/, and memories/ that affect a different session than this one. Do not modify another profile's skills/plugins/cron/memories unless the user explicitly directs you to.\n\nYou are running as a scheduled cron job. There is no user present — you cannot ask questions, request clarification, or wait for follow-up. Execute the task fully and autonomously, making reasonable decisions where needed. Your final response is automatically delivered to the job's configured destination — put the primary content directly in your response.\n\nConversation started: Monday, June 29, 2026\nModel: deepseek-v4-pro\nProvider: deepseek"
        },
        {
          "role": "user",
          "content": "[IMPORTANT: The user has invoked the \"wecom-topic-assistant\" skill, indicating they want you to follow its instructions. The full skill content is loaded below.]\n\n---\nname: wecom-topic-assistant\ndescription: \"企微选题助手 — we-mp-rss 文章抓取、企微群推送、Hermes 直接作为企微机器人（替代 MCP 插件架构）。\"\nversion: 1.0.0\nplatforms: [linux]\n---\n\n# 企微选题助手 (chunkaonewshub)\n\n基于 we-mp-rss 抓取公众号文章，通过企微群推送 + AI 智能机器人实现选题策划对话。\n\n## 服务器拓扑（2026-06-24 统一后）\n\n单服务器架构，全部运行在北京 Lighthouse：\n\n| 服务器 | 实例ID | 公网IP | 角色 |\n|--------|--------|--------|------|\n| **北京** | ins-jrstx6f5 | 120.53.228.229 | Hermes Agent + we-mp-rss 全部 |\n\n上海 (ins-5mj1p2zt, 110.40.207.3) 已停用。\n\ncloudflared tunnel `40f60a3f` 路由：\n- `rss.gdcjgk.net` → `localhost:8001` (we-mp-rss)\n- `callback.gdcjgk.net` → `localhost:8645` (Hermes wecom_callback)\n- `bot.gdcjgk.net` → `localhost:8645`\n- `files.gdcjgk.net` → `localhost:8088` (文件管理器)\n\n企微自建应用回调 URL：`https://callback.gdcjgk.net/wecom/callback`\n\n- 项目根目录: `~/project/`\n- 脚本目录: `~/project/scripts/`\n- we-mp-rss 源码: `~/project/we-mp-rss-main/`\n- we-mp-rss 数据库: `~/project/we-mp-rss-data/db.db`\n- 状态文件: `~/project/state/`\n- 配置文件: `~/project/.env`\n\n## 核心组件\n\n| 组件 | 端口 | 说明 |\n|------|------|------|\n| we-mp-rss | 8001 | 公众号文章抓取 + REST API（systemd 管理） |\n| Hermes Agent | - | 企微机器人、cron 调度、内容生成 |\n| Hermes wecom_callback | 8645 | 接收企微回调消息 |\n| Cloudflare Tunnel | - | 公网穿透（tunnel 40f60a3f） |\n| ~~MCP Server~~ | ~~8100~~ | **已废弃** |\n\n## 企微推送架构（重要：不要混淆推送通道）\n\n群里文章更新推送由 **Hermes cron job**（`94d230f73e7f` 春考选题-文章抓取）完成，**不是** we-mp-rss 内置的 message_tasks。三者独立：\n\n| 通道 | 用途 | 依赖 |\n|------|------|------|\n| 文章更新推送 | 每120分钟抓取最新文章 → 推送到群 | Hermes cron `94d230f73e7f` → fetch_and_push.py → 群 webhook |\n| 智能机器人 | 群内 @交互、webhook 直接推送 | 群聊 → 群管理 → 添加智能机器人 |\n| wecom_callback DM | 选题日报、文章审核等私聊推送 | cloudflared tunnel + Hermes 内置回调 |\n\n⚠️ we-mp-rss 内置的 message_tasks 表实际为空（未配置），所有群推送实际走的是 Hermes cron + fetch_and_push.py 脚本。不要试图通过 message_tasks API 排查群推送问题——查 cron job `94d230f73e7f` 和脚本 `~/project/scripts/fetch_and_push.py`。\n⚠️ 删掉智能机器人后：文章更新推送不受影响（走独立 webhook），但无法 @交互。**DM 通道完全独立**，继续正常工作。\n\n**内容审核推送模式：** 选题日报和每日发布同时推给多人审核（TuKeXin + 曹小芬），cron job 的 deliver 设置为 `wecom_callback:TuKeXin,wecom_callback:CaoXiaoFen`。\n\n## WeCom 回调验证与调试\n\n回调 URL: `https://callback.gdcjgk.net/wecom/callback`\n\n**架构链路：** 企微 → Cloudflare → tunnel 40f60a3f → localhost:8645 (Hermes)\n\n**关键诊断方法：** 用 `curl -sI` 看 `Server` 响应头区分问题层级：\n- `server: cloudflare` = 没进 tunnel，Dashboard 缺少 Public Hostname\n- `Server: Python/3.11 aiohttp` = 到达 Hermes，Token/AES Key 不匹配\n\n**⚠️ 回调验证通过 ≠ 消息回复能通。** 详见 IP 白名单说明。\n\n详见 `references/wecom-callback-debugging.md`。\n\n## 架构演进：Hermes 直接作为企微机器人（当前方案）\n\n**旧架构已废弃：** 企微 AI 机器人（内置 DeepSeek）→ MCP 插件 → MCP Server（调 DeepSeek）→ 返回。三重代理，MCP 不稳定。\n\n**新架构：** Hermes 直接接入企微，作为群聊机器人。无需 MCP 中转层，无需额外 DeepSeek API 调用。\n\n| 能力 | 旧方案 | 新方案 |\n|------|--------|--------|\n| 数据查询 | MCP Server → SQLite | Hermes 直接查 SQLite |\n| 内容生成 | MCP → DeepSeek API | Hermes 直接推理生成 |\n| 群内 @ 响应 | 企微 AI 机器人 → MCP | Hermes 直接回复 |\n| 定时推送 | 脚本调 DeepSeek → webhook | Cron → Hermes 分析 → 推送 |\n| 去重/记忆 | MCP 无状态 | Hermes 有持久化 memory + skill |\n\n**关键变化：**\n- MCP Server (`mcp_server.py`, 端口 8100) 已停止并禁用\n- 选题日报和热点雷达的 cron job 不再运行 Python 脚本调 DeepSeek，而是由 Hermes 自己推理\n- `mcp.icoach.chat` 域名已移除（MCP Server 停用，域名从隧道删除）\n\n**群内交互：** 用户在企微群 @Hermes → Hermes 收到消息 → 可查数据库、搜网络、生成文案 → 直接回复到群\n\n## 选题日报（每日 9:00）— 已修复AI幻觉，2026-06-26\n\n自动分析过去 36 小时文章，生成选题推荐推送。运行 `~/.hermes/scripts/daily_topic_data.py 36` 采集数据，Hermes 分析趋势并推荐选题。\n\n**⚠️ 关键修复：** AI 自写 SQL 频繁误读 Unix 时间戳。改为固化脚本查库输出格式化文本，AI 只做分析。\n\n⚠️ 曾用 `0 9 * * *`，因 DeepSeek API 在 9:00 整点高峰频繁无响应（多 job 同时抢 API，详见「Cron Job 运维 → API 高峰时段超时」），提前 5 分钟错开。\n\n**工作流（Hermes 自主推理）：**\n1. 复制 we-mp-rss DB 到 /tmp/ 避免锁冲突\n2. SQL 查询最近 36 小时文章（articles JOIN feeds，取 title/url/source/publish_time）\n3. 加载选题日历 `~/project/config/exam_calendar.json`，提取未来 14 天内节点\n4. Hermes 直接分析趋势 + 生成选题推荐（不调外部 API）\n5. 输出到 cron deliver 渠道\n\n**输出格式（字节数 ≤4000）：**\n- 📊 今日概况 — 2-3 句纯文本\n- 🔥 值得追的选题 — 3-5 个，每个含切入角度 + 参考文章\n- 💡 可储备选题\n- ⚠️ 结合考试日历的提醒\n\n### 旧方案：Python 脚本（备选）\n\n**脚本：** `scripts/daily_topics.py` — 同上流程但调 DeepSeek API 生成\n\n## 选题日报（每日 9:00）— 已修复AI幻觉，2026-06-26\n\n自动分析过去 36 小时文章，生成选题推荐推送。运行 `~/.hermes/scripts/daily_topic_data.py 36` 采集数据，Hermes 分析趋势并推荐选题。\n\n**⚠️ 关键修复：** AI 自写 SQL 频繁误读 Unix 时间戳。改为固化脚本查库输出格式化文本，AI 只做分析。\n\n**工作流（Hermes 自主推理）：**\n1. 复制 DB 到 /tmp/，SQL 查询最近 3 小时文章\n2. 文章 < 2 篇 → 静默退出\n3. 关键词预筛选：不同来源标题有 ≥2 个共同关键词 → 候选\n4. 去重合并同一话题组，查 `~/project/state/hot_radar_sent.json` 避免重复推送\n5. Hermes 判断是否同追一个事件\n6. 有热点 → 推送；无热点 → 静默\n\n**推送格式：**\n```\n🚨 热点预警\n**话题名**\n>判断依据\n> [来源1] [标题](链接)\n> [来源2] [标题](链接)\n```\n\n推送后更新 `hot_radar_sent.json` 记录已推送文章ID。\n\n### 旧方案：Python 脚本（备选）\n\n**脚本：** `scripts/hot_radar.py` — 关键词预筛选后调 DeepSeek API 验证\n\n## 选题日历\n\n配置文件：`~/project/config/exam_calendar.json`。日报自动加载未来 14 天内节点并织入选题建议。全年节点速查见 `references/exam-calendar.md`。\n\n## we-mp-rss 操作\n\n### 启动（systemd 管理）\n\n```bash\n# 服务已设为 systemd，开机自启\nsudo systemctl start we-mp-rss\nsudo systemctl status we-mp-rss\n\n# 手动启动（调试用）\ncd ~/project/we-mp-rss-main && source venv/bin/activate\nDB=\"sqlite:////home/ubuntu/project/we-mp-rss-data/db.db\" \\\nENABLE_JOB=True PORT=8001 BROWSER_TYPE=chromium \\\npython main.py\n```\n\n**⚠️ 常见启动失败：`ImportError: cannot import name 'router' from 'apis.message_task'`**\n`apis/message_task.py` 可能是空文件（0 bytes）。该文件只需导出一个 FastAPI router 供 `web.py` 注册。最小修复：\n\n```python\nfrom fastapi import APIRouter\nrouter = APIRouter(prefix=\"/wx/message_tasks\", tags=[\"消息任务\"])\n```\n\n实际的 message_task 路由在 `apis/cascade.py` 中。详见 `references/we-mp-rss-deploy.md`。\n\n### we-mp-rss 从零部署\n\n完整部署流程（源码获取、依赖安装、Chromium、systemd 服务、Tunnel 路由）见 `references/we-mp-rss-deploy.md`。\n\n### API 路径\n\n**注意：** API 的 base path 是 `/api/v1/wx`（不是 `/api/v1`）。\n\n| 操作 | 方法 | 路径 |\n|------|------|------|\n| 消息任务列表 | GET | `/api/v1/wx/message_tasks` |\n| 创建消息任务 | POST | `/api/v1/wx/message_tasks` |\n| 更新消息任务 | PUT | `/api/v1/wx/message_tasks/{id}` |\n| 执行消息任务 | GET | `/api/v1/wx/message_tasks/{id}/run?isTest=false` |\n| 重载任务 | PUT | `/api/v1/wx/message_tasks/job/fresh` |\n\n### 认证\n\n使用 AK/SK 认证，格式: `Authorization: AK-SK <ak>:<sk>`\n\nAK/SK 在 `.env` 中配置:\n```\nWE_MP_RSS_AK=xxx\nWE_MP_RSS_SK=xxx\n```\n\n### 查询数据库\n\n```bash\ncd ~/project/scripts && ~/project/we-mp-rss-main/venv/bin/python3 <script>.py\nimport sqlite3\ndb = sqlite3.connect('../we-mp-rss-data/db.db')\ndb.row_factory = sqlite3.Row\n# 查询文章\nrows = db.execute('SELECT a.id, a.title, f.mp_name FROM articles a JOIN feeds f ON a.mp_id=f.id ORDER BY a.publish_time DESC LIMIT 10').fetchall()\nfor r in rows: print(dict(r))\n\"\n```\n\n### Python 脚本运行\n\n所有脚本必须在 we-mp-rss 的 venv 下运行:\n```bash\ncd ~/project/scripts && ~/project/we-mp-rss-main/venv/bin/python3 <script>.py\n```\n\n### Web 登录凭据\n\nrss.gdcjgk.net 网站登录（JWT token）:\n- 用户名: `gdcjgk`\n- 密码: `admin888`\n\n初始化逻辑在 `init_sys.py`：`os.getenv(\"USERNAME\", \"admin\")` / `os.getenv(\"PASSWORD\", \"admin@123\")`。\n部署时设置了 `USERNAME=gdcjgk`、`PASSWORD=admin888`，因此凭据与默认值不同。\n\n### 文章抓取推送脚本（已修复去重，2026-06-26）\n\n**脚本:** `~/project/scripts/fetch_and_push.py` — Hermes cron `94d230f73e7f`（春考选题-文章抓取）每 120 分钟调用。\n\n**v2 关键修复——去重：**\n- 旧版：每次取 API 最新 10 篇全量推送，同一批文章一天推 12 次\n- 新版：`~/project/state/pushed_articles.json` 记录已推送文章 ID，只推新文章；无新文章自动静默（不推消息）；每次最多推 5 篇\n- 验证：第一次跑 `30 new → push 5`，第二次 `0 new → silent`\n\n**工作流:**\n1. 从 `.env` 读取 `WE_MP_RSS_AK`/`WE_MP_RSS_SK` 作为登录凭据 → 获取 JWT token\n2. 列出全部公众号 → 逐个触发文章更新\n3. 取最新 30 篇文章 → 过滤已推送 ID → 取前 5 篇新文章\n4. 格式化 markdown → POST 到 `QW_WEBHOOK_URL`\n5. 更新 `pushed_articles.json` 记录\n\n- 使用 `WE_MP_RSS_AK`/`WE_MP_RSS_SK` 作为用户名/密码登录（而非 AK/SK 认证）\n\n**关键细节:**\n- 使用 `WE_MP_RSS_AK`/`WE_MP_RSS_SK` 作为用户名/密码登录（而非 AK/SK 认证），因为 AK/SK API 认证需要先创建 access_key，而脚本直接用用户帐密获取 bearer token\n- webhook 硬编码在脚本中作为 fallback，如需换群应设 `QW_WEBHOOK_URL` 环境变量\n\n## 消息推送\n\n### 创建消息推送任务\n\n`mps_id` 格式必须是 `[{\"id\": \"MP_WXS_xxx\"}, ...]`（对象数组，不是字符串数组）。\n\n```bash\ncurl -X POST \"http://localhost:8001/api/v1/wx/message_tasks\" \\\n  -H \"Authorization: AK-SK $AK:$SK\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\n    \"name\": \"推送名称\",\n    \"message_type\": 0,\n    \"message_template\": \"📰 {{ feed.mp_name }} 更新\\n{% for article in articles %}\\n> {{ article.title }}\\n{% endfor %}\",\n    \"web_hook_url\": \"https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=xxx\",\n    \"mps_id\": \"[{\\\"id\\\": \\\"MP_WXS_3886864470\\\"}, {\\\"id\\\": \\\"MP_WXS_3596295911\\\"}]\",\n    \"cron_exp\": \"*/30 * * * *\",\n    \"status\": 1\n  }'\ncurl -X PUT \"http://localhost:8001/api/v1/wx/message_tasks/job/fresh\" \\\n  -H \"Authorization: AK-SK $AK:$SK\"\n```\n\n### 模板语法\n\n使用 `{{ }}` 双括号，支持 `{% for %}` / `{% if %}` 控制流。\n\n上下文变量:\n- `{{ feed.mp_name }}` — 公众号名称\n- `{{ article.title }}` — 文章标题\n- `{{ article.url }}` — 文章链接\n- `{{ article.publish_time }}` — 发布时间\n\n### 直接通过 webhook 推送\n\n消息任务可能因为文章已被抓取而不推送。可以直接通过企微群机器人 webhook 发送:\n\n```python\nrequests.post(webhook_url, json={\n    \"msgtype\": \"markdown\",\n    \"markdown\": {\"content\": \"markdown内容\"}\n})\n```\n\n**⚠️ 企微 markdown 限制是 4096 BYTES（不是字符）！** 中文字符 UTF-8 编码每字 3 bytes，所以实际只能推送约 1300 个中文字。推送前必须按 bytes 截断，不是按字符数截断。`errcode:40058` 就是超过 bytes 限制。\n\n## Cloudflare Tunnel\n\n### 当前 Tunnel 映射\n\nTunnel ID: `40f60a3f-8a68-4ae3-bf7d-8d49951975ce`，账号 `60fe57f5a5a310cd8073e41f82546c79`\n\n| 域名 | → 服务 | 所在服务器 |\n|------|--------|-----------|\n| `rss.gdcjgk.net` | `localhost:8001` | 北京 (we-mp-rss) |\n| `callback.gdcjgk.net` | `localhost:8645` | 北京 (Hermes wecom_callback) |\n| `bot.gdcjgk.net` | `localhost:8645` | 北京 |\n| `files.gdcjgk.net` | `localhost:8088` | 北京 (文件管理器) |\n\n### 添加新的 Public Hostname\n\n⚠️ **两步缺一不可**：\n\n1. **Cloudflare Zero Trust Dashboard**（浏览器操作）:\n   Networks → Tunnels → 40f60a3f → Public Hostname → Add\n   Subdomain + Domain + Service (HTTP + localhost:PORT)\n   ——这一步让 Cloudflare 知道把该域名的流量发给 tunnel\n\n2. **服务器 `~/.cloudflared/config.yml`**（已由 tunnel 管理，通常自动同步）:\n   ingress 规则告诉 tunnel 收到流量后转发到哪个本地端口\n\n**常见陷阱：只改了 config.yml 没在 Dashboard 加 Public Hostname。** 结果：`curl -sI https://新域名` 看到 `server: cloudflare`（Cloudflare 边缘直接返回），而不是 `server: Python/3.11 aiohttp`（到达了 Hermes）。\n\n**双服务器陷阱**：同一 Tunnel 在两台服务器运行 → 负载均衡。一台停机后存活服务器承接全部流量，但停机服务器独有的服务（如 we-mp-rss）会 502。诊断：`journalctl -u cloudflared-tunnel | grep \"Registered tunnel connection\"` 看连接数。\n\nWeCom 回调完整排障流程见 `references/wecom-callback-debugging.md`。\n\n## 网站自动发布 (gdcjgk.net)\n\n广东高职高考信息网支持通过 API 自动发布：`we-mp-rss 文章 → AI 改写 → POST 到网站`。\n\n详见 `references/fastadmin-cms-publishing.md`（含 API 调用、栏目 ID、缩略图随机选取方案）。\n\n**每日自动发布 cron job:** `533d92e64b59` (gdcjgk-每日发布)，每天 10:00 自动执行：\n1. 查询 we-mp-rss 数据库最近 36 小时文章\n2. AI 评分筛选（信息增量 0-5 + 时效性 0-3 + 可改写性 0-2，≥7 分入选）\n3. 基于 gdcjgk-style-guide 撰写 800-1000 字原创文章\n4. 通过 FastAdmin CMS API 存为草稿 (`status=draft`)\n5. 企微通知审核（含后台编辑链接）\n\n质量门槛：宁缺毋滥，不足 5 篇就少发。\n\n## Cron Job 运维\n\n### 投递失败排查\n\n**现象：** cron job 执行成功（`last_status: ok`），但用户收不到消息，`last_delivery_error: \"platform 'weixin' not configured/enabled\"`。\n\n**根因：** 旧的 Hermes 版本使用 `weixin` 作为企微平台名，升级后改为 `wecom`。已创建的 cron job 的 `deliver` 字段仍指向旧平台名。\n\n**修复：** `cronjob action=update job_id=xxx deliver=origin` 将投递目标重置为当前聊天。如需多目标投递，用逗号分隔：`deliver=origin,wecom:TuKeXin`（群 + 个人 DM）。\n\n**验证：** `cronjob action=run job_id=xxx` 手动触发一次，确认能收到消息。\n\n### API 高峰时段超时（DeepSeek 特定）\n\n**现象：** cron job 在 9:00 整点运行报 `RuntimeError: [Errno 32] Broken pipe`，agent 日志显示 `Stream stale for 180s — no chunks received`，连续 3 次重试各等 180 秒后全部失败。**同时段其他 cron job 也受波及。**\n\n**根因：** DeepSeek API (`deepseek-v4-pro`) 在每日 9:00 整点高峰时段完全无响应——TCP 连接建立但服务端不发任何数据（不是返回错误，是零 chunk）。多个 cron job 同时触发（选题日报 + 小红书选题 + 热点雷达都在 9:00 同一秒）加剧了争抢。\n\n**排查方法：**\n```bash\n# 1. 按 session ID 查 agent 日志（格式：cron_<job_id>_<timestamp>）\ngrep \"cron_218c1e953f63\" ~/.hermes/logs/agent.log | grep \"Stream stale\\|Broken pipe\\|no chunks\"\n\n# 2. 查 errors.log\ngrep \"cron_218c1e953f63\" ~/.hermes/logs/errors.log | tail -10\n\n# 3. 查 cron 输出文件\nls -lt ~/.hermes/cron/output/<job_id>/ | head -3\n```\n\n关键信号：3 次日志中都有 `no chunks received` = API 侧问题（不是网络抖动）；只有偶尔一次 = 可忽略。\n\n**验证 API 是否恢复：**\n```bash\ncronjob action=run job_id=218c1e953f63\n# 等约30秒后查看，若有 tool 调用（terminal/read_file）= API 正常\n```\n\n**修复：错开 cron 调度（已验证有效）**\n\n| Job | ID | 原时间 | 新时间 | |\n|-----|-----|--------|--------|---|\n| 选题日报 | 218c1e953f63 | 0 9 * * * | **55 8 * * *** | ← 最早触发，最重要 |\n| 小红书每日选题 | c27628909af0 | 0 9 * * * | **5 9 * * *** | ← 等选题日报跑完 |\n| 热点雷达 | 472ca7495b44 | */30 8-22 * * * | 不变 | ← 自然错开 |\n\n```bash\ncronjob action=update job_id=218c1e953f63 schedule='55 8 * * *'\ncronjob action=update job_id=c27628909af0 schedule='5 9 * * *'\n```\n\n**不要做：** 在 9:00 手动触发多个 job 验证——会加剧拥堵。先错开再逐个手动验证。\n\n**历史记录：**\n- 2026-06-25: 3 次重试中前 2 次失败，第 3 次侥幸成功（延迟 371 秒）\n- 2026-06-26: 3 次重试全部失败，10 分钟耗尽。同年小红书选题和热点雷达同时炸\n- 2026-06-26 09:19: 手动触发 → API 恢复正常，证明只是整点高峰问题\n\n\n\n## 企微新版 AI 机器人 + MCP 插件（⚠️ 已废弃）\n\n> **此方案已废弃。** Hermes 现在直接接入企微作为机器人，不再需要 MCP 中转层。\n> 以下内容保留作为历史参考，部署新环境时请使用上方「Hermes 直接作为企微机器人」方案。\n\n### 机器人类型区别\n\n| 类型 | 连接方式 | 能接收消息 | 能对话 |\n|------|----------|-----------|--------|\n| 群机器人 | Webhook URL | ❌ | ❌ (只能推送) |\n| 旧版智能机器人 | Bot ID + Secret + WebSocket | ✅ | ✅ |\n| **新版 AI 机器人** | MCP 插件 (Streamable HTTP) | ✅ | ✅ (内置 LLM) |\n\n新版 AI 机器人自带 DeepSeek，只需通过 MCP 插件给它数据访问能力。\n\n### MCP Server 部署\n\nMCP Server 提供三个工具:\n1. `get_today_articles` — 查询今日文章列表\n2. `get_article_detail` — 查询文章详情（含正文前 2000 字）\n3. `generate_distribution` — 基于话题生成平台分发文案\n\n**`generate_distribution` 参数：**\n- `topic`（必填）：话题名称或文章链接\n- `platforms`（选填）：`[\"公众号\",\"微信群\",\"朋友圈\",\"小红书\"]` 的子集。不传默认四个全出。\n\n**generate_distribution 工作方式：** 收到请求后调 DeepSeek API（`deepseek-chat`，`response_format: json_object`），返回四/指定平台的标题+文案 JSON。MCP 返回结果给机器人，机器人格式化后直接回复在群里。\n\n**完整触发链路：** 群内 @机器人 → 机器人根据提示词决定调工具 → MCP `/rpc` 端点处理 → MCP Server 调 DeepSeek → 返回 JSON → 机器人格式化回复\n\n代码: `~/project/scripts/mcp_server.py`\n\n**传输协议：仅用 Streamable HTTP (`/rpc`)，不能用 SSE。** SSE 在企微上 session 频繁丢失导致插件被移除。\n\n**`Mcp-Session-Id` 头必须返回：** 企微使用多台服务器轮询 MCP 请求，`initialize` 响应必须包含 `Mcp-Session-Id` 头，否则后续 `tools/call` 会被企微内部拒绝（报「工具名称无法正确匹配」）。\n\n详细实现见 `wecom-bot-integration` 技能的 `references/mcp-server-implementation.md`。\n\n### 企微后台配置\n\n1. 应用管理 → 智能机器人 → 选择机器人 → API/MCP 插件\n2. 添加 MCP 插件:\n   - 插件 URL: `https://mcp.icoach.chat/rpc`\n   - 传输协议: **Streamable HTTP**（不是 SSE）\n   - 授权方式: 不需要授权\n\n### 机器人指令（System Prompt）\n\n**格式约束：** 企微机器人指令不支持代码块、Markdown 表格、emoji。包含这些会导致「AI 处理失败」。\n\n**写法原则：** \n- **不要写成 API 文档**。不要写「当用户输入 X 时调用工具 Y，参数 Z」——太刻板，触发率低。\n- **用「听到→做」模式**。写场景化描述而非规则列表。例如「看到有人说\"帮我写一篇XX的\"就调 generate_distribution」而不是「调用 generate_distribution(topic, platforms?)」。\n- **工具只描述功能，不写匹配规则**。DeepSeek 会根据工具描述自动判断何时调用，不需要手写 if-this-then-that。\n- **极简>详尽**。3 句话描述角色 + 工具列表 + 回复风格就够了。\n\n**推荐模板：**\n```\n你是内容创作助手，帮团队从公众号文章里挖选题、写文案。\n\n三个工具：\n• get_today_articles — 查今天抓取的文章\n• get_article_detail — 看某篇文章详情\n• generate_distribution(topic, platforms?) — 生成各平台发布文案\n  四个平台：公众号/微信群/朋友圈/小红书\n  没说具体平台就四个都出\n\n回复直接给可复制文案，语气轻松不啰嗦。\n```\n\n- 先用最简版验证连接: `你是内容创作助手。当用户询问最新文章或选题时，必须调用 get_today_articles 工具获取真实数据。`\n- 通过后再逐步添加完整版。模板见 `wecom-bot-integration/references/bot-prompt-template.md`。\n   - 现象：日志报 `Executable doesn't exist at .../webkit-2070/pw_run.sh`\n   - 原因：Playwright 更新或缓存清理时 webkit 文件被删除\n   - 修复：`cd ~/project/we-mp-rss-main源文件/we-mp-rss-main && source venv/bin/activate && playwright install webkit && sudo playwright install-deps`\n\n2. **代码默认使用 webkit 而非 chromium**\n   - 现象：webkit 已安装但仍然超时无响应\n   - 根因：`driver/wxarticle.py` 中 `PlaywrightController` 调用时未传 `browser_type`，默认值 `webkit`。而环境配置的 `BROWSER_TYPE=chromium` 只对主抓取进程生效\n   - 修复：在 `driver/wxarticle.py` 的 `get_article_content()` 中修改：\n     ```python\n     # 原来\n     async with PlaywrightController(proxy_url=..., mobile_mode=True) as controller:\n     # 改为\n     async with PlaywrightController(proxy_url=..., mobile_mode=True,\n                                     browser_type=os.environ.get(\"BROWSER_TYPE\", \"chromium\")) as controller:\n     ```\n\n3. **后端实际成功但前端超时**\n   - 现象：日志显示 HTTP 200 + 滚动完成，但前端显示「获取中」\n   - 原因：by_article 接口需 30-40 秒（开浏览器 + 翻页加载图片），前端默认超时比这短\n   - 验证：直接 curl API 确认后端是否成功\n   - 状态：后端可正常工作，前端体验问题暂无修复\n\n**MCP 插件在企微后台消失了：**\n- 根因：使用了 SSE（`/sse`）。SSE 长连接断开会触发企微自动移除插件\n- 修复：重新添加，URL 用 `/rpc`，协议选 Streamable HTTP\n\n**机器人不调用插件：**\n- 检查 MCP 插件状态是否绿色 ✓\n- 检查传输协议是否为 Streamable HTTP（不是 SSE）\n- 检查 MCP URL 是否以 `/rpc` 结尾\n- ~~检查 Cloudflare Tunnel 是否正确路由 `mcp.icoach.chat` → `localhost:8100`~~ **MCP 已停用，此检查已过时**\n- 检查机器人指令是否写明了工具使用规则（参考 `references/bot-persona.md`）\n\n**tools/list 成功但 tools/call 静默失败（日志无 tools/call 记录）：**\n- 这是企微 MCP 基础设施的问题，不是我们的代码问题\n- 确认方法：`tail -f /tmp/mcp_server.log` → 能看到 initialize → tools/list，但永远看不到 tools/call\n- 解决方案：删掉 MCP 插件重新添加（碰运气），或考虑 API 插件替代方案\n\n**SSE 模式下 session 丢失：**\n- SSE 的 session 与连接绑定，断开后 POST 会返回 404\n- 改用 Streamable HTTP（`/rpc`）无此问题\n\n## 用户偏好\n\n- **简洁直接**：用表格、清单、少量段落，不要大段解释\n- **先行动再汇报**：遇到问题先解决，解决后简洁汇报\n- **遇到障碍给方案**：不要反复分析原因，直接给可操作的下一步\n- **用户是运营人员，不是开发者，避免过度技术术语**。当用户说\"听不懂\"时，立即切换到最简单的方案（SQL 语句、复制粘贴命令），不要解释服务器架构\n- 遇到 Nginx/服务器配置问题时优先找**应用层方案**（改数据库、换路径、用内置 API），而非让用户改服务器配置\n\n## 选题日报 AI 幻觉排查（已修复：2026-06-26）\n\n**修复方案：** 不再让 AI 自己写 SQL 查库。改用固化脚本 `daily_topic_data.py` 先跑查询，输出格式化结果，AI 只做选题分析。\n\n**脚本：** `~/.hermes/scripts/daily_topic_data.py` — 接受 `hours` 参数，输出文章列表（含格式化时间/来源/标题/摘要/链接）+ 考试日历节点。时间已格式化为北京时间，消除 AI 时间戳误读根因。\n\n**Cron job 更新：** `218c1e953f63` 的 prompt 已改为先跑脚本再分析，详见该 cron job 的 prompt。\n\n**现象：** 用户反馈选题日报中有事实性错误（如「最近一次更新在 6 月 23 日上午，此后静默」），但数据库实际有 6 月 24 日文章。\n\n**排查步骤：**\n1. 不要相信 AI 的摘要——直接查原始数据库验证\n2. 运行脚本：`~/project/we-mp-rss-main/venv/bin/python3 ~/.hermes/scripts/daily_topic_data.py 36`\n3. 对比脚本输出与日报陈述\n4. 定位根因后向用户汇报「管线正常，AI 误读」而非怀疑系统故障\n\n**常见根因：**\n- `publish_time` 是 INTEGER (Unix 时间戳)，AI 用 `strftime` 生成字符串 cutoff 做比较，SQLite 类型强制转换可能产生意外结果\n- LLM 在摘要长篇查询结果时容易误读时间戳\n- 不要因为 AI 一句错误陈述就怀疑整个管线——先验证原始数据\n\n## 当前订阅的公众号\n\n| ID | 名称 |\n|----|------|\n| MP_WXS_3886864470 | 广东中职菌 |\n| MP_WXS_3596295911 | 中职生 |\n\n## 参考：WebSocket 智能机器人方案（旧）\n\n主方案（企微 AI 机器人 + MCP 插件）出现前使用的 WebSocket 长连接 + DeepSeek function calling 方案。详细架构、交互命令、会话管理见 `references/conversation-agent-architecture.md`。\n\n交互命令速查：\n| 用户输入 | 功能 |\n|----------|------|\n| `帮助` | 显示指令指南 |\n| `今日` | 列出最近文章 |\n| `1`~`N` | 查看文章 N 的选题角度推荐 |\n| `生成1A` | 基于文章1角度A生成文案 |\n| `https://mp.weixin.qq.com/s/xxx` | 核心摘要 + 各平台选题角度 |\n\n**常见问题**（WebSocket 方案）：\n| 问题 | 原因 | 修复 |\n|------|------|------|\n| 第二轮回超时 | 工具返回文章正文太长 | 限制 `content[:2000]`，降低 `max_tokens` |\n| 回复被截断 | 企微 markdown 4096 **bytes** 限制 | 推送前按 bytes 截断（中文字 3 bytes/字，约 1300 字上限） |\n| 多轮记忆丢失 | session 文件路径或 TTL | 检查 `STATE_DIR` 和 `SESSION_TTL` |\n\n### 直接 webhook 推送（备用）\n\n```bash\ncurl -s \"https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=<KEY>\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"msgtype\":\"markdown\",\"markdown\":{\"content\":\"消息内容\"}}'\n```\n\nThe user has provided the following instruction alongside the skill invocation: [IMPORTANT: You are running as a scheduled cron job. DELIVERY: Your final response will be automatically delivered to the user — do NOT use send_message or try to deliver the output yourself. Just produce your report/output as your final response and the system handles the rest. SILENT: If there is genuinely nothing new to report, respond with exactly \"[SILENT]\" (nothing else) to suppress delivery. Never combine [SILENT] with content — either report your findings normally, or say [SILENT] and nothing more.]\n\n你是广东春季高考（3+证书考试）内容编辑。每天早上分析 we-mp-rss 数据库最近 36 小时文章，生成选题日报直接推送。\n\n## 步骤\n\n1. **运行数据采集脚本**（不要自己写 SQL）：\n```bash\n/home/ubuntu/project/we-mp-rss-main/venv/bin/python3 /home/ubuntu/.hermes/scripts/daily_topic_data.py 36\n```\n脚本会输出：文章列表（含时间/来源/标题）、文章详情（含摘要/链接）、考试日历节点。时间已经格式化为北京时间，直接读即可。\n\n2. **分析趋势** + 结合考试日历，推荐 3-5 个选题。\n\n## 输出格式（字节数 ≤4000）\n\n📊 今日概况\n[2-3句纯文本，概括文章主题分布和趋势]\n\n🔥 值得追的选题\n> 选题1：<选题名>\n> 为什么：<1句>\n> 切入：角度1 / 角度2 / 角度3\n> 参考：<来源>《<标题>》\n\n> 选题2：...\n\n💡 可储备\n> 选题名 — <1句话>\n\n⚠️ 注意\n<结合考试日历的提醒>\n\n## 规则\n\n- 脚本输出为权威数据源，不要质疑或重新解释时间戳\n- 如果脚本输出\"⚠ 最近无新文章\"，直接回复\"📭 近 36 小时无新文章\"并结束\n- 全自动执行，不问我任何问题"
        }
      ],
      "tools": [
        {
          "type": "function",
          "function": {
            "name": "patch",
            "description": "Targeted find-and-replace edits in files. Use this instead of sed/awk in terminal. Uses fuzzy matching (9 strategies) so minor whitespace/indentation differences won't break it. Returns a unified diff. Auto-runs syntax checks after editing.\n\nREPLACE MODE (mode='replace', default): find a unique string and replace it. REQUIRED PARAMETERS: mode, path, old_string, new_string.\nPATCH MODE (mode='patch'): apply V4A multi-file patches for bulk changes. REQUIRED PARAMETERS: mode, patch.",
            "parameters": {
              "type": "object",
              "properties": {
                "mode": {
                  "type": "string",
                  "enum": [
                    "replace",
                    "patch"
                  ],
                  "description": "Edit mode. 'replace' (default): requires path + old_string + new_string. 'patch': requires patch content only.",
                  "default": "replace"
                },
                "path": {
                  "type": "string",
                  "description": "REQUIRED when mode='replace'. File path to edit."
                },
                "old_string": {
                  "type": "string",
                  "description": "REQUIRED when mode='replace'. Exact text to find and replace. Must be unique in the file unless replace_all=true. Include surrounding context lines to ensure uniqueness."
                },
                "new_string": {
                  "type": "string",
                  "description": "REQUIRED when mode='replace'. Replacement text. Pass empty string '' to delete the matched text."
                },
                "replace_all": {
                  "type": "boolean",
                  "description": "Replace all occurrences instead of requiring a unique match (default: false)",
                  "default": false
                },
                "patch": {
                  "type": "string",
                  "description": "REQUIRED when mode='patch'. V4A format patch content. Format:\n*** Begin Patch\n*** Update File: path/to/file\n@@ context hint @@\n context line\n-removed line\n+added line\n*** End Patch"
                },
                "cross_profile": {
                  "type": "boolean",
                  "description": "Opt out of the cross-profile soft guard. Defaults to false. Set true ONLY after explicit user direction to edit another Hermes profile's skills/plugins/cron/memories.",
                  "default": false
                }
              },
              "required": [
                "mode"
              ]
            }
          }
        },
        {
          "type": "function",
          "function": {
            "name": "process",
            "description": "Manage background processes started with terminal(background=true). Actions: 'list' (show all), 'poll' (check status + new output), 'log' (full output with pagination), 'wait' (block until done or timeout), 'kill' (terminate), 'write' (send raw stdin data without newline), 'submit' (send data + Enter, for answering prompts), 'close' (close stdin/send EOF).",
            "parameters": {
              "type": "object",
              "properties": {
                "action": {
                  "type": "string",
                  "enum": [
                    "list",
                    "poll",
                    "log",
                    "wait",
                    "kill",
                    "write",
                    "submit",
                    "close"
                  ],
                  "description": "Action to perform on background processes"
                },
                "session_id": {
                  "type": "string",
                  "description": "Process session ID (from terminal background output). Required for all actions except 'list'."
                },
                "data": {
                  "type": "string",
                  "description": "Text to send to process stdin (for 'write' and 'submit' actions)"
                },
                "timeout": {
                  "type": "integer",
                  "description": "Max seconds to block for 'wait' action. Returns partial output on timeout.",
                  "minimum": 1
                },
                "offset": {
                  "type": "integer",
                  "description": "Line offset for 'log' action (default: last 200 lines)"
                },
                "limit": {
                  "type": "integer",
                  "description": "Max lines to return for 'log' action",
                  "minimum": 1
                }
              },
              "required": [
                "action"
              ]
            }
          }
        },
        {
          "type": "function",
          "function": {
            "name": "read_file",
            "description": "Read a text file with line numbers and pagination. Use this instead of cat/head/tail in terminal. Output format: 'LINE_NUM|CONTENT'. Suggests similar filenames if not found. Use offset and limit for large files. Reads exceeding ~100K characters are rejected; use offset and limit to read specific sections of large files. NOTE: Cannot read images or binary files — use vision_analyze for images.",
            "parameters": {
              "type": "object",
              "properties": {
                "path": {
                  "type": "string",
                  "description": "Path to the file to read (absolute, relative, or ~/path)"
                },
                "offset": {
                  "type": "integer",
                  "description": "Line number to start reading from (1-indexed, default: 1)",
                  "default": 1,
                  "minimum": 1
                },
                "limit": {
                  "type": "integer",
                  "description": "Maximum number of lines to read (default: 500, max: 2000)",
                  "default": 500,
                  "maximum": 2000
                }
              },
              "required": [
                "path"
              ]
            }
          }
        },
        {
          "type": "function",
          "function": {
            "name": "search_files",
            "description": "Search file contents or find files by name. Use this instead of grep/rg/find/ls in terminal. Ripgrep-backed, faster than shell equivalents.\n\nContent search (target='content'): Regex search inside files. Output modes: full matches with line numbers, file paths only, or match counts.\n\nFile search (target='files'): Find files by glob pattern (e.g., '*.py', '*config*'). Also use this instead of ls — results sorted by modification time.",
            "parameters": {
              "type": "object",
              "properties": {
                "pattern": {
                  "type": "string",
                  "description": "Regex pattern for content search, or glob pattern (e.g., '*.py') for file search"
                },
                "target": {
                  "type": "string",
                  "enum": [
                    "content",
                    "files"
                  ],
                  "description": "'content' searches inside file contents, 'files' searches for files by name",
                  "default": "content"
                },
                "path": {
                  "type": "string",
                  "description": "Directory or file to search in (default: current working directory)",
                  "default": "."
                },
                "file_glob": {
                  "type": "string",
                  "description": "Filter files by pattern in grep mode (e.g., '*.py' to only search Python files)"
                },
                "limit": {
                  "type": "integer",
                  "description": "Maximum number of results to return (default: 50)",
                  "default": 50
                },
                "offset": {
                  "type": "integer",
                  "description": "Skip first N results for pagination (default: 0)",
                  "default": 0
                },
                "output_mode": {
                  "type": "string",
                  "enum": [
                    "content",
                    "files_only",
                    "count"
                  ],
                  "description": "Output format for grep mode: 'content' shows matching lines with line numbers, 'files_only' lists file paths, 'count' shows match counts per file",
                  "default": "content"
                },
                "context": {
                  "type": "integer",
                  "description": "Number of context lines before and after each match (grep mode only)",
                  "default": 0
                }
              },
              "required": [
                "pattern"
              ]
            }
          }
        },
        {
          "type": "function",
          "function": {
            "name": "terminal",
            "description": "Execute shell commands on a Linux environment. Filesystem usually persists between calls.\n\nDo NOT use cat/head/tail to read files — use read_file instead.\nDo NOT use grep/rg/find to search — use search_files instead.\nDo NOT use ls to list directories — use search_files(target='files') instead.\nDo NOT use sed/awk to edit files — use patch instead.\nDo NOT use echo/cat heredoc to create files — use write_file instead.\nReserve terminal for: builds, installs, git, processes, scripts, network, package managers, and anything that needs a shell.\n\nForeground (default): Commands return INSTANTLY when done, even if the timeout is high. Set timeout=300 for long builds/scripts — you'll still get the result in seconds if it's fast. Prefer foreground for short commands.\nBackground: Set background=true to get a session_id. Almost always pair with notify_on_complete=true — bg without notify runs SILENTLY and you have no way to learn it finished short of calling process(action='poll') yourself. Two legitimate uses:\n  (1) Long-lived processes that never exit (servers, watchers, daemons) — silent is correct, there's no exit to notify on.\n  (2) Long-running bounded tasks (tests, builds, deploys, CI pollers, batch jobs) — MUST set notify_on_complete=true. Without it you'll either forget to poll or sit blocked waiting for the user to surface the result.\nFor servers/watchers, do NOT use shell-level background wrappers (nohup/disown/setsid/trailing '&') in foreground mode. Use background=true so Hermes can track lifecycle and output.\nAfter starting a server, verify readiness with a health check or log signal, then run tests in a separate terminal() call. Avoid blind sleep loops.\nUse process(action=\"poll\") for progress checks, process(action=\"wait\") to block until done.\nWorking directory: Use 'workdir' for per-command cwd.\nPTY mode: Set pty=true for interactive CLI tools (Codex, Claude Code, Python REPL).\n\nDo NOT use vim/nano/interactive tools without pty=true — they hang without a pseudo-terminal. Pipe git output to cat if it might page.\n",
            "parameters": {
              "type": "object",
              "properties": {
                "command": {
                  "type": "string",
                  "description": "The command to execute on the VM"
                },
                "background": {
                  "type": "boolean",
                  "description": "Run the command in the background. Almost always pair with notify_on_complete=true — without it, the process runs silently and you'll have no way to learn it finished short of calling process(action='poll') yourself (easy to forget, leading to silent blindness on long jobs). Two legitimate patterns: (1) Long-lived processes that never exit (servers, watchers, daemons) — these stay silent because there's no exit to notify on. (2) Long-running bounded tasks (tests, builds, deploys, CI pollers, batch jobs) — these MUST set notify_on_complete=true. For short commands, prefer foreground with a generous timeout instead.",
                  "default": false
                },
                "timeout": {
                  "type": "integer",
                  "description": "Max seconds to wait (default: 180, foreground max: 600). Returns INSTANTLY when command finishes — set high for long tasks, you won't wait unnecessarily. Foreground timeout above 600s is rejected; use background=true for longer commands.",
                  "minimum": 1
                },
                "workdir": {
                  "type": "string",
                  "description": "Working directory for this command (absolute path). Defaults to the session working directory."
                },
                "pty": {
                  "type": "boolean",
                  "description": "Run in pseudo-terminal (PTY) mode for interactive CLI tools like Codex, Claude Code, or Python REPL. Only works with local and SSH backends. Default: false.",
                  "default": false
                },
                "notify_on_complete": {
                  "type": "boolean",
                  "description": "When true (and background=true), you'll be automatically notified exactly once when the process finishes. **This is the right choice for almost every long-running task** — tests, builds, deployments, multi-item batch jobs, anything that takes over a minute and has a defined end. Use this and keep working on other things; the system notifies you on exit. MUTUALLY EXCLUSIVE with watch_patterns — when both are set, watch_patterns is dropped.",
                  "default": false
                },
                "watch_patterns": {
                  "type": "array",
                  "items": {
                    "type": "string"
                  },
                  "description": "Strings to watch for in background process output. HARD RATE LIMIT: at most 1 notification per 15 seconds per process — matches arriving inside the cooldown are dropped. After 3 consecutive 15-second windows with dropped matches, watch_patterns is automatically disabled for that process and promoted to notify_on_complete behavior (one notification on exit, no more mid-process spam). USE ONLY for truly rare, one-shot mid-process signals on LONG-LIVED processes that will never exit on their own — e.g. ['Application startup complete'] on a server so you know when to hit its endpoint, or ['migration done'] on a daemon. DO NOT use for: (1) end-of-run markers like 'DONE'/'PASS' — use notify_on_complete instead; (2) error patterns like 'ERROR'/'Traceback' in loops or multi-item batch jobs — they fire on every iteration and you'll hit the strike limit fast; (3) anything you'd ever combine with notify_on_complete. When in doubt, choose notify_on_complete. MUTUALLY EXCLUSIVE with notify_on_complete — set one, not both."
                }
              },
              "required": [
                "command"
              ]
            }
          }
        },
        {
          "type": "function",
          "function": {
            "name": "write_file",
            "description": "Write content to a file, completely replacing existing content. Use this instead of echo/cat heredoc in terminal. Creates parent directories automatically. OVERWRITES the entire file — use 'patch' for targeted edits. Auto-runs syntax checks on .py/.json/.yaml/.toml and other linted languages; only NEW errors introduced by this write are surfaced (pre-existing errors are filtered out).",
            "parameters": {
              "type": "object",
              "properties": {
                "path": {
                  "type": "string",
                  "description": "Path to the file to write (will be created if it doesn't exist, overwritten if it does)"
                },
                "content": {
                  "type": "string",
                  "description": "Complete content to write to the file"
                },
                "cross_profile": {
                  "type": "boolean",
                  "description": "Opt out of the cross-profile soft guard. Defaults to false. Set true ONLY after explicit user direction to edit another Hermes profile's skills/plugins/cron/memories — by default these writes are blocked with a warning because they affect a different profile than the one this session is running under.",
                  "default": false
                }
              },
              "required": [
                "path",
                "content"
              ]
            }
          }
        }
      ],
      "reasoning_effort": "max",
      "extra_body": {
        "thinking": {
          "type": "enabled"
        }
      }
    }
  },
  "error": {
    "type": "ReadError",
    "message": "[Errno 32] Broken pipe"
  }
}