feat(wechat): 微信里发一条链接,正文和配图抓下来存成知识
CI / backend (push) Canceled after 0s

公众号的文章也能抓:走 mp.weixin.qq.com 的 DOM(#js_content),
按 data-src 取图,不靠通用抽取器——它会把公众号的图全丢掉。

正文里的外链图下载进我们自己的存储(最多 12 张、单张 8 MB),
地址改成 /attachments/{id}/raw?签名:公众号那边一换图,外链就是一排红叉。

抓取入口谁都能触发,所以只允许公网地址(内网段、回环、链路本地、
保留段、解析不出的域名一律拒绝),否则就是个 SSRF 跳板。

抓不到要说人话:"这个链接没抓下来(原因)。想直接记成任务的话,
加「任务:」前缀再发一次。" 反爬验证页、SPA 空壳都走这条路。

- 新增 services/article.py:extract_url / parse / fetch / store_images / compose_body
- wechat_intake:整条消息是链接时走抓取,落知识草稿,回「已存成知识草稿:标题」
- 依赖 trafilatura==2.0.0(装法见 requirements.txt 注释:必须逐个装再 --no-deps)
- 测试 9 例:链接判定、公众号解析、验证页、通用页裁导航、SSRF 拒绝、图片本地化、失败提示
This commit is contained in:
Steven
2026-11-06 00:54:21 +08:00
parent f6e7616756
commit 626f83fb9c
7 changed files with 859 additions and 1 deletions
+19
View File
@@ -750,6 +750,25 @@ Query:
图片/文件/视频目前只在 `bot_message.raw` 里留痕,正文不搬;语音如果服务端给了转写就用转写文本。
### 6.5.2.1 发一条链接:抓正文存成知识
整个消息就是一条 `http://` / `https://` 链接时(前后带标点不算),不当任务,而是去把文章抓下来:
| 环节 | 行为 |
|---|---|
| 判断 | `extract_url()`:去掉首尾标点后只剩一个链接;带了别的话("看下 https://… 这个")仍按任务处理 |
| 抓取 | `httpx` 取页面(25 秒超时、8 MB 上限),`trafilatura` 抽正文;标题取 `og:title`/`<h1>`/`<title>` |
| 公众号 | `mp.weixin.qq.com` 单独走 DOM:标题 `#activity-name`、作者 `#js_name`、正文 `#js_content`,图片取 `data-src`(通用抽取器会把公众号的图丢光) |
| 清洗 | 导航/页脚按行边界裁掉(`_trim_chrome`);`trafilatura` 结果太短(<120 字)就退到容器打分兜底 |
| 配图 | 正文里的外链图下载进我们自己的存储,最多 12 张、单张 8 MB,正文改写成 `/attachments/{id}/raw?签名`——公众号一换图,外链就是一排红叉 |
| 落库 | 知识草稿(`status="draft"`,`doc_type="note"`),正文首行是 `> 原文:<url>` |
| 回话 | `已存成知识草稿:标题`(存了图再加「,连图 N 张」) |
| 失败 | `这个链接没抓下来(原因)。想直接记成任务的话,加「任务:」前缀再发一次。` |
安全:抓取入口对任何能给机器人发消息的人开放,所以**只允许公网地址**——`127.0.0.1`、内网段、
链路本地、保留地址、解析不出来的域名一律拒绝(`ArticleError`),否则就是个 SSRF 跳板。
反爬/需登录的页面(微信"环境异常"验证页、Cloudflare 302、SPA 空壳)会回上面那句失败提示,不静默吞掉。
### 6.5.3 错误码
| 场景 | code | HTTP |
+18 -1
View File
@@ -840,7 +840,24 @@
**教训:接入第三方协议,先去已有实现里找协议,别从直觉倒推。** 那一版的迁移 `0012` 保留
(`bot_channel` / `bot_message` 两张表还在用),`token_hash` 列留着但不再使用。
### 13.8 还没做
### 13.8 发一条链接,抓成知识
需求原话:「如果发送给机器人的是一个链接,那么就需要把链接内的文章内容给抓取下来,然后保存为知识;
就算是微信公众号的链接也需要抓取下来」。
- 选型 `trafilatura==2.0.0`(中文正文抽取最稳)+ `lxml`;公众号单独走 DOM(`#js_content`、图片在
`data-src`),因为通用抽取器会把公众号的图全丢掉。
- 抓下来的图下载进自己的存储(最多 12 张、单张 8 MB),正文改写成本地 `raw?签名` 地址——
外链图在公众号侧一换就全红叉。
- 只允许公网地址:谁都能给机器人发消息,不拦住内网就是 SSRF 跳板(`127.0.0.1`、`192.168.x`、链路本地、
保留段、解析不出的域名一律拒绝)。
- 失败要说话:反爬验证页、Cloudflare 302、SPA 空壳统一回「这个链接没抓下来(原因)。想直接记成任务
的话,加「任务:」前缀再发一次。」
- **装依赖的坑**:`pip install trafilatura` 在本机会卡死(依赖解析完就挂住),要逐个装
`lxml / courlan / htmldate / jusText / lxml_html_clean / …` 再 `pip install --no-deps trafilatura`;
`lxml` 6 起 `lxml.html.clean` 拆成 `lxml_html_clean`,不装就 `ImportError`。
### 13.9 还没做
- 只做了"进"(收消息建事项),没做"出"(任务有进展回一条微信通知)。
- 消息不认人:按通道创建者记账;要认人得先做微信身份绑定。