工程现场

洞见采集链路:先把一篇文章完整地送进系统

记录浏览器扩展如何处理文本、图片、授权和入库,以及为什么采集协议必须先于网站适配确定下来。

洞见采集链路:先把一篇文章完整地送进系统

“采集到 Mira”看起来像一个按钮,真正实现时却是一条跨越浏览器扩展、授权、内容解析、附件处理和数据库入库的业务链路。最初的想法很直接:读取当前页面的文字,发给后端,保存起来。这个方案很快就遇到三个问题:用户到底想采集什么,正文如何从网页噪声中分离出来,保存后的内容能不能在以后重新处理。

因此第一步不是写网站适配,而是先确定采集协议。扩展只负责读取用户明确选择的内容和页面上下文,Mira 负责清洗、去重、保存和后续分析。扩展不启动后端,不携带 Python,也不维护一套独立的数据处理逻辑。它的技术边界就是 Vanilla JavaScript、浏览器 API 和一个稳定的请求协议。

先区分三种采集意图

右键菜单只保留一个“采集到 Mira”。菜单名称表达的是业务结果,而不是实现细节。真正的采集范围由当前页面状态决定:

选中文本  -> 采集选区文本和页面来源
选中图片  -> 采集图片、图片地址和附近上下文
没有选择  ->  尝试采集整页正文

这里不能注册两个含义相近的菜单,例如“采集正文”和“采集到 Mira”同时出现。用户看到两个入口时,不知道哪个会带图片,哪个会带全文。统一入口可以让扩展根据 selection.type 做分支,而不是把选择成本交给用户。

请求数据需要先规范化,不能把网页 DOM 原样传给后端。当前协议可以按材料、来源和内容块拆开:

{
  "source": {
    "url": "https://example.com/article",
    "title": "文章标题",
    "siteName": "Example",
    "capturedAt": "2026-07-13T08:00:00.000Z"
  },
  "selection": {
    "type": "page",
    "text": ""
  },
  "blocks": [
    { "type": "text", "text": "第一段正文" },
    { "type": "image", "url": "https://example.com/a.jpg", "alt": "图片说明" },
    { "type": "text", "text": "第二段正文" }
  ],
  "metadata": {
    "tags": [],
    "note": "",
    "triggerRebuild": false
  }
}

内容块比单独的 text 字段更重要。图文文章不能先把文字抽出来、图片另外存成附件,再在页面上猜测它们的关系。图片在正文中的位置本身就是信息。入库时保存 textimage 的顺序,展示和重建时才能恢复原文结构。

授权只解决连接,不把扩展变成客户端

扩展需要知道 Mira 服务在哪里,但不能硬编码端口。授权码中可以携带经过编码的连接信息,扩展拿到授权码后解析出服务地址,再向授权接口提交。扩展不需要知道数据库结构,也不需要实现账户登录流程。

未授权状态只展示授权码入口。授权成功后,扩展显示成功色小圆点和“Mira”,表示连接已经建立。连接失败时要区分服务不可达、授权码无效和接口返回错误,不能全部显示成“Network Error”。对用户而言,这三种情况的处理方式完全不同:启动服务、重新授权,或者检查请求内容。

清洗和入库必须保留原始材料

后端收到请求后,先做协议校验,再做内容清洗:

1. 校验授权和来源 URL
2. 校验 blocks 的类型和顺序
3. 删除空块、脚本、样式和明显导航内容
4. 合并被网页结构打断的段落
5. 保留图片、图片说明和原始顺序
6. 计算正文指纹
7. 判断重复材料
8. 保存原始数据、清洗结果和处理状态

原始数据和清洗结果不能只留下一个。清洗规则会变化,网页解析也会出现误判。如果数据库只保存清洗后的正文,后续发现正文缺段时只能重新回网页抓取,无法判断是网页变了还是清洗规则错了。建议至少保存:

rawPayload       扩展提交的完整请求
cleanContent     清洗后的完整内容
contentBlocks    保持图文顺序的结构化内容
sourceUrl        原始来源
contentHash      内容指纹
processingStatus 当前处理状态

预览文本和完整正文也必须分开。预览只用于用户确认,不能成为正式入库字段。长文章如果在预览截断、请求序列化或界面状态中只剩十几行,后面的主题提取和知识重建就失去了材料基础。

图片则分为外链和本地附件两种状态。下载成功时保存附件 ID,同时保留原始 URL;下载失败时不能删除图片块,而应保留外链并记录失败状态。这样用户至少还能回到来源页面,系统也不会把一篇图文文章静默变成纯文本。

采集成功不等于重建完成

入库请求的返回值应该明确告诉前端发生了什么:

{
  "success": true,
  "materialId": "mat_123",
  "status": "stored",
  "cleanedBlockCount": 18,
  "imageCount": 3,
  "duplicate": false
}

“已保存”只表示材料进入数据库,不代表洞见已经更新。全量重建是另一项耗时业务,必须由用户明确勾选后触发。普通采集默认 triggerRebuild: false,勾选后才创建重建任务。这样既避免每采集一篇文章就重新处理全部历史数据,也能让用户知道结果变化是由哪次操作引起的。

这条链路的第一目标不是支持尽可能多的网站,而是保证任何来源都能进入同一个稳定协议:用户主动选择,扩展准确发送,后端完整清洗,图片和文字保持关系,原始材料可追溯,失败原因可解释。网站适配只能改善识别率,不能替代这套业务边界。