# Cron Job: 热点雷达

**Job ID:** 472ca7495b44
**Run Time:** 2026-06-23 21:01:25
**Schedule:** */30 8-22 * * *

## Prompt

[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.]

---
name: wecom-topic-assistant
description: "企微选题助手 — we-mp-rss 文章抓取、企微群推送、Hermes 直接作为企微机器人（替代 MCP 插件架构）。"
version: 1.0.0
platforms: [linux]
---

# 企微选题助手 (chunkaonewshub)

基于 we-mp-rss 抓取公众号文章，通过企微群推送 + AI 智能机器人实现选题策划对话。

## 项目路径

- 项目根目录: `~/project/`
- 脚本目录: `~/project/scripts/`
- we-mp-rss 源码: `~/project/we-mp-rss-main源文件/we-mp-rss-main/`
- we-mp-rss 数据库: `~/project/we-mp-rss-data/db.db`
- 状态文件: `~/project/state/`
- 配置文件: `~/project/.env`

## 核心组件

| 组件 | 端口 | 说明 |
|------|------|------|
| we-mp-rss | 8001 | 公众号文章抓取 + REST API |
| Nginx | 9443 | HTTPS 反代（80/443 被 ISP 封） |
| ~~MCP Server~~ | ~~8100~~ | **已废弃** — Hermes 直接做机器人，不再需要 MCP 中转 |
| Cloudflare Tunnel | - | 公网穿透（仅 rss.gdcjgk.net — icoach 已迁移） |
| Hermes Agent | - | **新**：直接作为企微机器人，接收群消息、分析数据、生成内容 |

## 企微推送架构（重要：不要混淆两个推送通道）

群里日常消息推送由 **「每日消息推送」** 完成（we-mp-rss 内置的消息任务机制），**不是**智能机器人。两者是独立的：

| 通道 | 用途 | 依赖 |
|------|------|------|
| 每日消息推送 | 文章抓取结果、热点预警等自动推送到群 | we-mp-rss message_tasks → 群 webhook |
| 智能机器人 | 群内 @交互、webhook 直接推送 | 群聊 → 群管理 → 添加智能机器人 |
| wecom_callback DM | 选题日报、文章审核等私聊推送 | cloudflared tunnel + Hermes 内置回调 |

⚠️ 删掉智能机器人后：群里每日推送不受影响，但群 webhook 失效、无法 @交互。**DM 通道完全独立**，继续正常工作。

**内容审核推送模式：** 选题日报和每日发布同时推给多人审核（TuKeXin + 曹小芬），cron job 的 deliver 设置为 `wecom_callback:TuKeXin,wecom_callback:CaoXiaoFen`。

## 架构演进：Hermes 直接作为企微机器人（当前方案）

**旧架构已废弃：** 企微 AI 机器人（内置 DeepSeek）→ MCP 插件 → MCP Server（调 DeepSeek）→ 返回。三重代理，MCP 不稳定。

**新架构：** Hermes 直接接入企微，作为群聊机器人。无需 MCP 中转层，无需额外 DeepSeek API 调用。

| 能力 | 旧方案 | 新方案 |
|------|--------|--------|
| 数据查询 | MCP Server → SQLite | Hermes 直接查 SQLite |
| 内容生成 | MCP → DeepSeek API | Hermes 直接推理生成 |
| 群内 @ 响应 | 企微 AI 机器人 → MCP | Hermes 直接回复 |
| 定时推送 | 脚本调 DeepSeek → webhook | Cron → Hermes 分析 → 推送 |
| 去重/记忆 | MCP 无状态 | Hermes 有持久化 memory + skill |

**关键变化：**
- MCP Server (`mcp_server.py`, 端口 8100) 已停止并禁用
- 选题日报和热点雷达的 cron job 不再运行 Python 脚本调 DeepSeek，而是由 Hermes 自己推理
- `mcp.icoach.chat` 域名已移除（MCP Server 停用，域名从隧道删除）

**群内交互：** 用户在企微群 @Hermes → Hermes 收到消息 → 可查数据库、搜网络、生成文案 → 直接回复到群

## 选题日报（每日 9:00）

自动分析过去 36 小时文章，生成选题推荐推送。

### 当前方案：Hermes Agent Cron（推荐）

**定时：** Hermes cronjob `0 9 * * *`，job_id `218c1e953f63`

**工作流（Hermes 自主推理）：**
1. 复制 we-mp-rss DB 到 /tmp/ 避免锁冲突
2. SQL 查询最近 36 小时文章（articles JOIN feeds，取 title/url/source/publish_time）
3. 加载选题日历 `~/project/config/exam_calendar.json`，提取未来 14 天内节点
4. Hermes 直接分析趋势 + 生成选题推荐（不调外部 API）
5. 输出到 cron deliver 渠道

**输出格式（字节数 ≤4000）：**
- 📊 今日概况 — 2-3 句纯文本
- 🔥 值得追的选题 — 3-5 个，每个含切入角度 + 参考文章
- 💡 可储备选题
- ⚠️ 结合考试日历的提醒

### 旧方案：Python 脚本（备选）

**脚本：** `scripts/daily_topics.py` — 同上流程但调 DeepSeek API 生成

## 热点雷达（每 30 分钟，8-22 点）

检测多个公众号是否同时在追同一话题，有热点才推送。

### 当前方案：Hermes Agent Cron（推荐）

**定时：** Hermes cronjob `*/30 8-22 * * *`，job_id `472ca7495b44`

**工作流（Hermes 自主推理）：**
1. 复制 DB 到 /tmp/，SQL 查询最近 3 小时文章
2. 文章 < 2 篇 → 静默退出
3. 关键词预筛选：不同来源标题有 ≥2 个共同关键词 → 候选
4. 去重合并同一话题组，查 `~/project/state/hot_radar_sent.json` 避免重复推送
5. Hermes 判断是否同追一个事件
6. 有热点 → 推送；无热点 → 静默

**推送格式：**
```
🚨 热点预警
**话题名**
>判断依据
> [来源1] [标题](链接)
> [来源2] [标题](链接)
```

推送后更新 `hot_radar_sent.json` 记录已推送文章ID。

### 旧方案：Python 脚本（备选）

**脚本：** `scripts/hot_radar.py` — 关键词预筛选后调 DeepSeek API 验证

## 选题日历

配置文件：`~/project/config/exam_calendar.json`。日报自动加载未来 14 天内节点并织入选题建议。全年节点速查见 `references/exam-calendar.md`。

## we-mp-rss 操作

### 启动
```bash
cd ~/project/we-mp-rss-main源文件/we-mp-rss-main
source venv/bin/activate
DB="sqlite:////home/agentuser/project/we-mp-rss-data/db.db" \
ENABLE_JOB=True PORT=8001 BROWSER_TYPE=chromium \
python main.py -job True
```

### API 路径

**注意：** API 的 base path 是 `/api/v1/wx`（不是 `/api/v1`）。

| 操作 | 方法 | 路径 |
|------|------|------|
| 消息任务列表 | GET | `/api/v1/wx/message_tasks` |
| 创建消息任务 | POST | `/api/v1/wx/message_tasks` |
| 更新消息任务 | PUT | `/api/v1/wx/message_tasks/{id}` |
| 执行消息任务 | GET | `/api/v1/wx/message_tasks/{id}/run?isTest=false` |
| 重载任务 | PUT | `/api/v1/wx/message_tasks/job/fresh` |

### 认证

使用 AK/SK 认证，格式: `Authorization: AK-SK <ak>:<sk>`

AK/SK 在 `.env` 中配置:
```
WE_MP_RSS_AK=xxx
WE_MP_RSS_SK=xxx
```

### 查询数据库

```bash
cd ~/project/scripts && ~/project/we-mp-rss-main源文件/we-mp-rss-main/venv/bin/python3 -c "
import sqlite3
db = sqlite3.connect('../we-mp-rss-data/db.db')
db.row_factory = sqlite3.Row
# 查询文章
rows = 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()
for r in rows: print(dict(r))
"
```

### Python 脚本运行

所有脚本必须在 we-mp-rss 的 venv 下运行:
```bash
cd ~/project/scripts && ~/project/we-mp-rss-main源文件/we-mp-rss-main/venv/bin/python3 <script>.py
```

## 消息推送

### 创建消息推送任务

`mps_id` 格式必须是 `[{"id": "MP_WXS_xxx"}, ...]`（对象数组，不是字符串数组）。

```bash
curl -X POST "http://localhost:8001/api/v1/wx/message_tasks" \
  -H "Authorization: AK-SK $AK:$SK" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "推送名称",
    "message_type": 0,
    "message_template": "📰 {{ feed.mp_name }} 更新\n{% for article in articles %}\n> {{ article.title }}\n{% endfor %}",
    "web_hook_url": "https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=xxx",
    "mps_id": "[{\"id\": \"MP_WXS_3886864470\"}, {\"id\": \"MP_WXS_3596295911\"}]",
    "cron_exp": "*/30 * * * *",
    "status": 1
  }'
curl -X PUT "http://localhost:8001/api/v1/wx/message_tasks/job/fresh" \
  -H "Authorization: AK-SK $AK:$SK"
```

### 模板语法

使用 `{{ }}` 双括号，支持 `{% for %}` / `{% if %}` 控制流。

上下文变量:
- `{{ feed.mp_name }}` — 公众号名称
- `{{ article.title }}` — 文章标题
- `{{ article.url }}` — 文章链接
- `{{ article.publish_time }}` — 发布时间

### 直接通过 webhook 推送

消息任务可能因为文章已被抓取而不推送。可以直接通过企微群机器人 webhook 发送:

```python
requests.post(webhook_url, json={
    "msgtype": "markdown",
    "markdown": {"content": "markdown内容"}
})
```

**⚠️ 企微 markdown 限制是 4096 BYTES（不是字符）！** 中文字符 UTF-8 编码每字 3 bytes，所以实际只能推送约 1300 个中文字。推送前必须按 bytes 截断，不是按字符数截断。`errcode:40058` 就是超过 bytes 限制。

## Cloudflare Tunnel

### 查看当前配置

Tunnel 名: `icoach-tunnel`

当前域名映射（2026-06-13 更新）:
- `rss.gdcjgk.net` → `localhost:8001` (we-mp-rss)
- ~~`icoach.chat` → `localhost:5000`~~ **已移除** — icoach 项目迁移至新服务器
- ~~`rss.icoach.chat` → `localhost:8001`~~ **已移除** — 域名迁至 rss.gdcjgk.net
- ~~`mcp.icoach.chat` → `localhost:8100`~~ **已移除** — MCP Server 已停用

### 获取 Tunnel ID

Tunnel 使用 `--token` 模式运行。从进程提取 token 后解码:

```bash
cat /proc/$(pgrep -f "cloudflared tunnel")/cmdline | tr '\0' '\n' | grep -A1 '\--token'
```

Token 是 base64 编码的 JSON，解码后包含 `t` (tunnel ID) 和 `a` (account ID)。

CNAME 目标格式: `<tunnel-id>.cfargotunnel.com`

### 添加新的 Public Hostname

在 Cloudflare Zero Trust Dashboard 操作:
1. Networks → Tunnels → icoach-tunnel → Public Hostname → Add
2. Subdomain + Domain + Service (HTTP + localhost:PORT)

DNS CNAME 记录也需要添加，指向 `<tunnel-id>.cfargotunnel.com`。

## 网站自动发布 (gdcjgk.net)

广东高职高考信息网支持通过 API 自动发布：`we-mp-rss 文章 → AI 改写 → POST 到网站`。

详见 `references/fastadmin-cms-publishing.md`（含 API 调用、栏目 ID、缩略图随机选取方案）。

**每日自动发布 cron job:** `533d92e64b59` (gdcjgk-每日发布)，每天 10:00 自动执行：
1. 查询 we-mp-rss 数据库最近 36 小时文章
2. AI 评分筛选（信息增量 0-5 + 时效性 0-3 + 可改写性 0-2，≥7 分入选）
3. 基于 gdcjgk-style-guide 撰写 800-1000 字原创文章
4. 通过 FastAdmin CMS API 存为草稿 (`status=draft`)
5. 企微通知审核（含后台编辑链接）

质量门槛：宁缺毋滥，不足 5 篇就少发。

## Cron Job 运维

### 投递失败排查

**现象：** cron job 执行成功（`last_status: ok`），但用户收不到消息，`last_delivery_error: "platform 'weixin' not configured/enabled"`。

**根因：** 旧的 Hermes 版本使用 `weixin` 作为企微平台名，升级后改为 `wecom`。已创建的 cron job 的 `deliver` 字段仍指向旧平台名。

**修复：** `cronjob action=update job_id=xxx deliver=origin` 将投递目标重置为当前聊天。如需多目标投递，用逗号分隔：`deliver=origin,wecom:TuKeXin`（群 + 个人 DM）。

**验证：** `cronjob action=run job_id=xxx` 手动触发一次，确认能收到消息。

## 企微新版 AI 机器人 + MCP 插件（⚠️ 已废弃）

> **此方案已废弃。** Hermes 现在直接接入企微作为机器人，不再需要 MCP 中转层。
> 以下内容保留作为历史参考，部署新环境时请使用上方「Hermes 直接作为企微机器人」方案。

### 机器人类型区别

| 类型 | 连接方式 | 能接收消息 | 能对话 |
|------|----------|-----------|--------|
| 群机器人 | Webhook URL | ❌ | ❌ (只能推送) |
| 旧版智能机器人 | Bot ID + Secret + WebSocket | ✅ | ✅ |
| **新版 AI 机器人** | MCP 插件 (Streamable HTTP) | ✅ | ✅ (内置 LLM) |

新版 AI 机器人自带 DeepSeek，只需通过 MCP 插件给它数据访问能力。

### MCP Server 部署

MCP Server 提供三个工具:
1. `get_today_articles` — 查询今日文章列表
2. `get_article_detail` — 查询文章详情（含正文前 2000 字）
3. `generate_distribution` — 基于话题生成平台分发文案

**`generate_distribution` 参数：**
- `topic`（必填）：话题名称或文章链接
- `platforms`（选填）：`["公众号","微信群","朋友圈","小红书"]` 的子集。不传默认四个全出。

**generate_distribution 工作方式：** 收到请求后调 DeepSeek API（`deepseek-chat`，`response_format: json_object`），返回四/指定平台的标题+文案 JSON。MCP 返回结果给机器人，机器人格式化后直接回复在群里。

**完整触发链路：** 群内 @机器人 → 机器人根据提示词决定调工具 → MCP `/rpc` 端点处理 → MCP Server 调 DeepSeek → 返回 JSON → 机器人格式化回复

代码: `~/project/scripts/mcp_server.py`

**传输协议：仅用 Streamable HTTP (`/rpc`)，不能用 SSE。** SSE 在企微上 session 频繁丢失导致插件被移除。

**`Mcp-Session-Id` 头必须返回：** 企微使用多台服务器轮询 MCP 请求，`initialize` 响应必须包含 `Mcp-Session-Id` 头，否则后续 `tools/call` 会被企微内部拒绝（报「工具名称无法正确匹配」）。

详细实现见 `wecom-bot-integration` 技能的 `references/mcp-server-implementation.md`。

### 企微后台配置

1. 应用管理 → 智能机器人 → 选择机器人 → API/MCP 插件
2. 添加 MCP 插件:
   - 插件 URL: `https://mcp.icoach.chat/rpc`
   - 传输协议: **Streamable HTTP**（不是 SSE）
   - 授权方式: 不需要授权

### 机器人指令（System Prompt）

**格式约束：** 企微机器人指令不支持代码块、Markdown 表格、emoji。包含这些会导致「AI 处理失败」。

**写法原则：** 
- **不要写成 API 文档**。不要写「当用户输入 X 时调用工具 Y，参数 Z」——太刻板，触发率低。
- **用「听到→做」模式**。写场景化描述而非规则列表。例如「看到有人说"帮我写一篇XX的"就调 generate_distribution」而不是「调用 generate_distribution(topic, platforms?)」。
- **工具只描述功能，不写匹配规则**。DeepSeek 会根据工具描述自动判断何时调用，不需要手写 if-this-then-that。
- **极简>详尽**。3 句话描述角色 + 工具列表 + 回复风格就够了。

**推荐模板：**
```
你是内容创作助手，帮团队从公众号文章里挖选题、写文案。

三个工具：
• get_today_articles — 查今天抓取的文章
• get_article_detail — 看某篇文章详情
• generate_distribution(topic, platforms?) — 生成各平台发布文案
  四个平台：公众号/微信群/朋友圈/小红书
  没说具体平台就四个都出

回复直接给可复制文案，语气轻松不啰嗦。
```

- 先用最简版验证连接: `你是内容创作助手。当用户询问最新文章或选题时，必须调用 get_today_articles 工具获取真实数据。`
- 通过后再逐步添加完整版。模板见 `wecom-bot-integration/references/bot-prompt-template.md`。
   - 现象：日志报 `Executable doesn't exist at .../webkit-2070/pw_run.sh`
   - 原因：Playwright 更新或缓存清理时 webkit 文件被删除
   - 修复：`cd ~/project/we-mp-rss-main源文件/we-mp-rss-main && source venv/bin/activate && playwright install webkit && sudo playwright install-deps`

2. **代码默认使用 webkit 而非 chromium**
   - 现象：webkit 已安装但仍然超时无响应
   - 根因：`driver/wxarticle.py` 中 `PlaywrightController` 调用时未传 `browser_type`，默认值 `webkit`。而环境配置的 `BROWSER_TYPE=chromium` 只对主抓取进程生效
   - 修复：在 `driver/wxarticle.py` 的 `get_article_content()` 中修改：
     ```python
     # 原来
     async with PlaywrightController(proxy_url=..., mobile_mode=True) as controller:
     # 改为
     async with PlaywrightController(proxy_url=..., mobile_mode=True,
                                     browser_type=os.environ.get("BROWSER_TYPE", "chromium")) as controller:
     ```

3. **后端实际成功但前端超时**
   - 现象：日志显示 HTTP 200 + 滚动完成，但前端显示「获取中」
   - 原因：by_article 接口需 30-40 秒（开浏览器 + 翻页加载图片），前端默认超时比这短
   - 验证：直接 curl API 确认后端是否成功
   - 状态：后端可正常工作，前端体验问题暂无修复

**MCP 插件在企微后台消失了：**
- 根因：使用了 SSE（`/sse`）。SSE 长连接断开会触发企微自动移除插件
- 修复：重新添加，URL 用 `/rpc`，协议选 Streamable HTTP

**机器人不调用插件：**
- 检查 MCP 插件状态是否绿色 ✓
- 检查传输协议是否为 Streamable HTTP（不是 SSE）
- 检查 MCP URL 是否以 `/rpc` 结尾
- ~~检查 Cloudflare Tunnel 是否正确路由 `mcp.icoach.chat` → `localhost:8100`~~ **MCP 已停用，此检查已过时**
- 检查机器人指令是否写明了工具使用规则（参考 `references/bot-persona.md`）

**tools/list 成功但 tools/call 静默失败（日志无 tools/call 记录）：**
- 这是企微 MCP 基础设施的问题，不是我们的代码问题
- 确认方法：`tail -f /tmp/mcp_server.log` → 能看到 initialize → tools/list，但永远看不到 tools/call
- 解决方案：删掉 MCP 插件重新添加（碰运气），或考虑 API 插件替代方案

**SSE 模式下 session 丢失：**
- SSE 的 session 与连接绑定，断开后 POST 会返回 404
- 改用 Streamable HTTP（`/rpc`）无此问题

## 用户偏好

- **简洁直接**：用表格、清单、少量段落，不要大段解释
- **先行动再汇报**：遇到问题先解决，解决后简洁汇报
- **遇到障碍给方案**：不要反复分析原因，直接给可操作的下一步
- **用户是运营人员，不是开发者，避免过度技术术语**。当用户说"听不懂"时，立即切换到最简单的方案（SQL 语句、复制粘贴命令），不要解释服务器架构
- 遇到 Nginx/服务器配置问题时优先找**应用层方案**（改数据库、换路径、用内置 API），而非让用户改服务器配置

## 当前订阅的公众号

| ID | 名称 |
|----|------|
| MP_WXS_3886864470 | 广东中职菌 |
| MP_WXS_3596295911 | 中职生 |

## 参考：WebSocket 智能机器人方案（旧）

主方案（企微 AI 机器人 + MCP 插件）出现前使用的 WebSocket 长连接 + DeepSeek function calling 方案。详细架构、交互命令、会话管理见 `references/conversation-agent-architecture.md`。

交互命令速查：
| 用户输入 | 功能 |
|----------|------|
| `帮助` | 显示指令指南 |
| `今日` | 列出最近文章 |
| `1`~`N` | 查看文章 N 的选题角度推荐 |
| `生成1A` | 基于文章1角度A生成文案 |
| `https://mp.weixin.qq.com/s/xxx` | 核心摘要 + 各平台选题角度 |

**常见问题**（WebSocket 方案）：
| 问题 | 原因 | 修复 |
|------|------|------|
| 第二轮回超时 | 工具返回文章正文太长 | 限制 `content[:2000]`，降低 `max_tokens` |
| 回复被截断 | 企微 markdown 4096 **bytes** 限制 | 推送前按 bytes 截断（中文字 3 bytes/字，约 1300 字上限） |
| 多轮记忆丢失 | session 文件路径或 TTL | 检查 `STATE_DIR` 和 `SESSION_TTL` |

### 直接 webhook 推送（备用）

```bash
curl -s "https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=<KEY>" \
  -H "Content-Type: application/json" \
  -d '{"msgtype":"markdown","markdown":{"content":"消息内容"}}'
```

The 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.]

检测 we-mp-rss 数据库中最近 3 小时文章，判断不同公众号（广东中职菌、中职生）是否在同追一个热点话题。

步骤：
1. 复制 ~/project/we-mp-rss-data/db.db 到 /tmp/，查询最近 3 小时文章（articles JOIN feeds，取 id/title/url/source/publish_time）
2. 如果文章 < 2 篇 → 静默退出，不输出任何内容
3. 对比不同来源的标题，找共同话题：不同来源标题有 ≥2 个共同有意义关键词 → 候选
4. 去重：同一组文章ID可能形成多对，合并
5. 判断是否真的同追一个事件，去重（查 ~/project/state/hot_radar_sent.json 避免重复推送）

输出格式（有热点才推，没热点什么都不说）：
🚨 热点预警

**话题名**
>判断依据
> [来源1] [文章标题](文章链接)
> [来源2] [文章标题](文章链接)

推送后更新 ~/project/state/hot_radar_sent.json 记录已推送的文章ID集合。

全自动，不问我任何问题。没热点就静默。

## Response

The we-mp-rss database (`~/project/we-mp-rss-data/db.db`) does not exist in this environment. No articles can be queried.

**Root cause:** This Hermes Agent environment (`/home/ubuntu`) doesn't have the we-mp-rss project deployed. The project directories referenced in the skill (`~/project/`, `~/project/we-mp-rss-data/`, `~/project/we-mp-rss-main源文件/`) are all absent.

**Likely cause:** The cron job was migrated or copied from a production server where we-mp-rss was running, but the project data wasn't included in the migration. The `hermes-migration/` directory contains only scripts and reference data, not the live database or running service.

**Next steps to restore hot radar:**
1. Migrate the we-mp-rss project from the old production server (including the SQLite database) to this environment
2. Re-deploy we-mp-rss service on port 8001
3. Verify the database path matches the skill configuration, or update `deliver` settings on `cronjob 472ca7495b44` to point to the correct server

Alternatively, if we-mp-rss is still running on the original server, update this cron job to query the remote database or disable the job on this Hermes instance and run it on the production server instead.
