# Cron Job: 热点雷达

**Job ID:** 472ca7495b44
**Run Time:** 2026-06-14 09:05:04
**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 | - | **新**：直接作为企微机器人，接收群消息、分析数据、生成内容 |

## 架构演进：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、缩略图随机选取方案）。

## 企微新版 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

[SILENT]
