Files
alibaba/docs/superpowers/specs/2026-07-13-alibaba-inquiry-backend-design.md
T
2026-08-27 16:12:37 +08:00

311 lines
21 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 阿里询盘管理后台 设计文档
- 日期: 2026-07-13(修订 v3:纳入二次评审发现)
- 状态: 修订完成,待用户审查
- 关联: 现有 Chrome 插件位于 `d:\alibb\`
- 修订说明: v2 修正了 spec review 发现的 4 个致命问题(host_permissions 运行时不可改 / messageId 丢失 / 队列存储布局 / 淘汰丢数据)与 8 个重要问题(询盘时间源、批量写入、alarms 周期、时区、索引、HTTP 推送、behavior 类型、外键可空)。
- v3 修正二次评审发现:① **早返回逻辑反转**(失败批次永不重试 → inquiries 表新增 `has_failed_batches` 列 + `messages_count` 改存实际插入数);② **IndexedDB 写入上下文**(content script 的 IDB 属页面源,须由 SW 落盘)+ 双存储一致性写入顺序;③ **MySQL 8 默认 max_allowed_packet 是 64MB 非 4MB**;④ 批量行数 1000/500 统一为 **500**;⑤ direction ENUM 补 `'system'`;⑥ §8.6 补 behavior null 强制转换 / country 来源替换 / translation 双重 JSON.parse。
## 1. 概述
### 1.1 目标
把现有 Chrome 插件(抓取阿里国际站询盘/客户/聊天)的「本地 CSV 导出」升级为完整链路:
> 插件自动推送 → 自建后端 → MySQL → 管理后台(查看 / 筛选 / 导出 / 统计)
### 1.2 使用模式(已确认)
- **主账号统一抓取**:登录阿里主管理员账号(可看到所有业务员的询盘),**一个人操作插件**一次性抓取全部询盘。
- **后台单管理员**:现阶段就一个管理员账号,统览全部数据。
- 询盘记录里的 `manager`(负责人)字段天然标识业务员,后台按此字段统计/筛选,**不需要业务员各自登录或各自推送**。
- **数据时效 = 上次手动抓取的快照**:插件靠用户在阿里页点「开始抓取」触发,**不是实时同步**;后台看到的是最近一次抓取的数据。后续可考虑定时自动抓(需浏览器保持阿里页打开)。
### 1.3 MVP 范围
**纳入**:插件自动推送;后端入库(三实体幂等);管理后台(列表/详情/导出/统计)。
**不纳入(后续)**:多业务员登录/权限;跟进管理/CRM;客户合并/标签;聊天全文搜索;复杂报表;增量推送;**后台数据编辑/删除(MVP 只读,错误数据需重抓或直接改库)**。
## 2. 背景:现有插件能力
`d:\alibb\` 的 Chrome MV3 插件已实现并验证:列表/详情/行为数据抓取;聊天 API 直拉(`querySummary.htm` → `getOpMessages.htm` 游标循环,单询盘实测 8000+ 条);chrome.storage 状态持久化。
**本方案对抓取主流程增量扩展,但必须改动消息映射**:
- 现有 `apiMsgToChat()` 只产出 `{sender, time, original, translation, direction}`,**丢弃了 messageId 和毫秒时间戳**。
- 本方案要求扩展为:`{messageId, sendTime(ISO UTC), sender, content, direction, translation}`,并从 `querySummary.htm` 响应顺带提取询盘权威时间 `gmtCreate`/`gmtModified`、买家国家等。
- 因此 §8 的插件改造**不是纯增量**,包含对 `apiMsgToChat` 与 `fetchChatByApi` 的字段扩展。
## 3. 需求
### 3.1 功能需求
| ID | 需求 |
|---|---|
| F1 | 插件每抓完一个询盘(主体 + 全量聊天)→ 自动 POST 到后端 |
| F2 | 后端按 客户/询盘/聊天 三表存储,重复推送幂等无副作用 |
| F3 | 后台列表:分页 + 筛选(国家/业务员/状态/时间范围)+ 关键词搜索(询价单号/买家名/公司)+ 排序 |
| F4 | 后台详情:客户基本信息 + 行为数据卡片 + 完整聊天(虚拟滚动,左右气泡按方向) |
| F5 | 导出:按当前筛选导出 CSV(**仅列表字段,不含聊天**;聊天在详情页单独导出) |
| F6 | 统计:总数、按国家 Top10、按业务员分布、按月趋势(基于询盘 gmtCreate) |
### 3.2 非功能需求
| ID | 需求 |
|---|---|
| NF1 可靠 | 推送不丢(队列持久化 + 重试 + 淘汰策略不丢 pending);入库幂等(唯一键 upsert) |
| NF2 简单好维护 | TypeScript;Prisma schema 单一数据定义源;前后端共享类型 |
| NF3 稳定 | 单操作者低并发;Docker 部署;MySQL 每日备份 |
| NF4 安全 | 正式 HTTPS;插件 Bearer token;管理员 JWT(带过期 + 刷新) |
## 4. 技术栈
| 层 | 选型 | 理由 |
|---|---|---|
| 后端 | **Node.js 18 + TypeScript + Express + Prisma** | 和插件同语言;Prisma schema 直观;TS 类型安全 |
| 数据库 | **MySQL 8** | 事务 + 唯一约束保证幂等 |
| 前端 | **Vue3 + TS + Element Plus + Vite + Pinia + Vue Router** | 管理后台组件成熟 |
| 部署 | **Docker Compose + Nginx** | 一键起 |
| 插件 | 现有 MV3 + 推送模块(chrome.alarms + 队列 + optional permissions) | 见 §8 |
**决策记录:为何 Node 不选 Python**:用户优先级是"简单 + 好维护 + 稳定 + 好实现(主导搭建)",Node 全栈与插件同语言、可复用数据转换逻辑、搭建排错最快;Python 数据生态优势对 MVP 简单统计是过度设计,且引入前后端两语言复杂度。后续复杂分析可加 Python 微服务。
## 5. 架构与数据流
### 5.1 组件拓扑
```
[操作者电脑 · 登录阿里主账号]
Chrome 插件
① 抓取(保留) → 扩展消息映射(messageId/sendTime ISO/gmtCreate)
② 每抓完一条 → 经 SW 写入【待推队列】(payload 存扩展 IndexedDB,queue_index 存 chrome.storage.local;content script 不能直接写扩展 IDB,见 §8.2)
③ chrome.alarms(1 分钟)唤醒 SW → 消费队列 → POST /api/ingest
↓ 失败指数退避重试,成功才出队
[Linux 服务器 · Docker Compose]
Nginx(client_max_body_size 50m)─┬─ /api/* → Backend
└─ /* → Frontend 静态
MySQL 8(数据卷 + 每日 mysqldump)
↓ 幂等写入(唯一键: inquiry_id / customer_id / message_id)
[管理员浏览器] 登录(JWT)→ 列表/详情/导出/统计
```
### 5.2 数据流时序(/api/ingest 处理)
1. zod 校验 payload 结构。
2. **幂等早返回**:若 `inquiry_id` 已存在 **且 `has_failed_batches=false`** 且 `messages_count` + `last_send_time` 均未变 → 直接 200 返回,跳过消息批写(避免无效重写几千条)。`has_failed_batches` 必须参与判断,否则上次失败的批次会被早返回永远跳过(详见 R6)。
3. 一个事务 upsert customer + inquiry(customer_id 可空时跳过 customer)。
4. 聊天消息用**原生批量** `INSERT INTO chat_messages (...) VALUES (...),(...)... ON DUPLICATE KEY UPDATE content=VALUES(content), send_time=VALUES(send_time), ...`,**分批**(每批 500 行,见 §10)执行,每批独立事务;失败批次记入失败列表,不回滚整条询盘。累计 `actualInserted` = 各批实际落盘行数之和。
5. 更新 inquiry:`messages_count = actualInserted`(实际插入数,**非 payload 长度**)、`last_send_time = max(sendTime)`、`has_failed_batches = (失败批次>0)`。用实际插入数才能让"失败重推时 count 不一致 → 不早返回 → 失败批次自然补写"(v2 用 payload 长度是逻辑反转)。
6. 返回 `{ok, inserted: actualInserted, failedBatches}`。
## 6. 数据模型
四张表。关系:customers 1→多 inquiries 1→多 chat_messages。
### 6.1 customers(客户)
| 字段 | 类型 | 说明 |
|---|---|---|
| id | BIGINT PK auto | |
| customer_id | VARCHAR(64) **UNIQUE** | 阿里 customerId(加密串) |
| name | VARCHAR(128) | |
| country | VARCHAR(64) **INDEX** | 国家(统计/筛选用) |
| email / company | VARCHAR | |
| register_time | DATETIME NULL | UTC,可空 |
| avatar_url | VARCHAR(512) | |
| buyer_tags / buyer_features | VARCHAR(255) | 逗号分隔 |
| behavior_product_views / valid_inquiries / valid_rfq / login_days / spam_inquiries | INT **NULL** | 可空(抓不到时 null,不存 0 以区分"无数据"与"真为 0") |
| behavior_top_industry | VARCHAR(255) | |
| created_at / updated_at | DATETIME | UTC |
### 6.2 inquiries(询盘)
| 字段 | 类型 | 说明 |
|---|---|---|
| id | BIGINT PK auto | |
| inquiry_id | VARCHAR(32) **UNIQUE** | 询价单号 |
| customer_id | VARCHAR(64) **NULL** INDEX | FK→customers.customer_id,**可空**(抓不到 customerId 时留空) |
| manager | VARCHAR(128) **INDEX** | 业务员名 |
| create_time | DATETIME | UTC,源自 API `gmtCreate` |
| update_time | DATETIME | UTC,源自 API `gmtModified`(缺失回落 gmtCreate) |
| status | VARCHAR(64) | |
| msg_preview | VARCHAR(512) | |
| is_flagged | BOOLEAN | |
| messages_count | INT DEFAULT 0 | 冗余,供幂等早返回 |
| last_send_time | DATETIME NULL | 冗余,供幂等早返回 |
| has_failed_batches | BOOLEAN DEFAULT FALSE | 上次推送是否有失败批次;为真则关闭早返回、强制重试(v3 新增) |
| created_at / updated_at | DATETIME | UTC |
### 6.3 chat_messages(聊天明细)
| 字段 | 类型 | 说明 |
|---|---|---|
| id | BIGINT PK auto | |
| message_id | VARCHAR(64) **UNIQUE** | 阿里 messageId(幂等键,全局唯一) |
| inquiry_id | VARCHAR(32) **INDEX** FK→inquiries.inquiry_id | |
| sender | VARCHAR(128) | |
| direction | ENUM('incoming','outgoing','system') | 插件 apiMsgToChat 在 messageType 非 send/rec 时兜底产出 'system'(样本暂未出现,保留以防插入失败) |
| send_time | DATETIME | UTC,源自毫秒戳 |
| content | **MEDIUMTEXT** | 原文(已解码实体)。卡片消息体量待实测确认(样本中为 URL ~500 字符);MEDIUMTEXT 上限 16MB 留足余量 |
| translation | TEXT | 翻译,从 `extInfo.icbuData.extend.extParams.inputTranslateInfo.translate` 提取(可空) |
| created_at | DATETIME | UTC |
> **索引**:`UNIQUE(message_id)`、`INDEX(inquiry_id, send_time, message_id)`(游标分页复合键)。
### 6.4 users(管理员账号)
| 字段 | 类型 |
|---|---|
| id PK / username UNIQUE / password_hash (bcrypt) / created_at |
> **bootstrap**:后端启动时若 users 表空,根据 `ADMIN_USERNAME` / `ADMIN_PASSWORD` 环境变量 seed 一条记录。推送 token 写 `.env`(`INGEST_TOKEN`),不建表。
### 6.5 时间来源与时区(修订)
- **询盘 create_time / update_time**:用 `querySummary.htm` 返回的 `gmtCreate` / `gmtModified` 毫秒戳 → `new Date(ms).toISOString()`。**不再用"最早消息兜底"**(语义错:询盘创建 ≠ 首条消息)。列表页那个只有时分的文本时间不入库,仅必要时回退展示。
- **消息 send_time**:用 API 毫秒戳 → ISO UTC。
- 所有 DATETIME 存 UTC;前端用 dayjs 转本地时区展示。
## 7. API 设计
基地址 `/api`,JSON。
| 方法 | 路径 | 鉴权 | 说明 |
|---|---|---|---|
| POST | `/api/ingest` | Bearer token | 插件推送。zod 校验。限流 60/min/token。幂等早返回(见 §5.2) |
| POST | `/api/auth/login` | - | `{username,password}` → `{token, expiresIn}` |
| POST | `/api/auth/refresh` | JWT | 刷新 token |
| GET | `/api/inquiries` | JWT | 列表,query: page,size,country,manager,status,dateFrom,dateTo,q(搜 inquiry_id/msg_preview/customer.name),sort |
| GET | `/api/inquiries/:id` | JWT | 详情 + 客户 + 行为 |
| GET | `/api/inquiries/:id/messages` | JWT | **游标分页**:query `afterSendTime` + `afterMessageId`(默认空=从最早)、size;`WHERE inquiry_id=? AND (send_time, message_id) > (?, ?) ORDER BY send_time, message_id LIMIT ?`(复合游标,避免同时间戳消息丢失) |
| GET | `/api/inquiries/:id/messages/export` | JWT | 单询盘聊天导出(stream) |
| GET | `/api/stats/overview` | JWT | 总数/国家 Top10/业务员/月趋势 |
| GET | `/api/export` | JWT | **仅列表字段**(不含聊天),按筛选 stream 导出 |
### 7.1 `/api/ingest` payload 结构
```jsonc
{
"inquiry": {
"inquiryId": "13776617920",
"manager": "cara Hu",
"createTime": "2026-03-13T10:11:50Z", // 来自 gmtCreate, ISO UTC
"updateTime": "2026-07-13T10:11:50Z", // 来自 gmtModified
"status": "洽谈中", "msgPreview": "...", "isFlagged": false
},
"customer": { // customerId 为空时整个 customer 省略
"customerId": "259b4e6f...", "name": "Carlos Loayza Marin",
"country": "PE", "email": null, "company": "World Shop",
"registerTime": "2025-06-23T00:00:00Z",
"avatarUrl": "...", "buyerTags": "TM沟通", "buyerFeatures": "...",
"behavior": { "productViews": null, "validInquiries": null, "validRfq": 39 } // 捕不到为 null
},
"messages": [
{ "messageId": "4198482017852", "sendTime": "2026-07-13T02:11:50Z",
"sender": "cara Hu", "direction": "outgoing", "content": "hello...",
"translation": "你好..." }
]
}
```
- 空字符串数字字段在插件侧 coerce 为 null;后端 zod 接受 `number | null`。
## 8. 插件改造(在 `d:\alibb\` 基础上)
### 8.1 配置页
popup 增「后端设置」:服务器 URL、Token、启用开关。存 chrome.storage.local。
**首次保存 URL 时**,调用 `chrome.permissions.request({origins:[serverUrl]})`(需在用户点击手势中触发,会弹 Chrome 授权框)——因为 host_permissions 运行时不可改,见 §8.5。
### 8.2 待推队列(修订:IndexedDB 存 payload + 不丢 pending)
- **payload 默认存扩展 IndexedDB**(`idb` 库),每条一个 record(key=id);chrome.storage.local 只存轻量 `queue_index`(有序 id 列表)+ 每项元数据 `{id, retries, nextRetry, status}`。避免 chrome.storage.local 在 GB 级的读写性能问题。
- **写入上下文(关键,v3)**:content script 的 `indexedDB` 是**页面源**(`message.alibaba.com`)的,不是扩展的;只有 service worker / popup / options 才能访问扩展自己的 IndexedDB。因此正确链路是:**content.js 抓完一条 → `chrome.runtime.sendMessage` 把 payload 传给 SW → SW 落盘到扩展 IndexedDB**。绝不能让 content.js 直接写 IDB(会写到页面的库里)。
- **双存储一致性(v3)**:IndexedDB 与 chrome.storage 之间无跨库事务。写入顺序固定为**先写 IDB payload,再追加 queue_index**(保证 index 永不引用不存在的 payload);删除反之(先标 success → 删 IDB → 移 index)。SW 启动时跑一次 GC,清理 IDB 中无 index 引用的孤儿 payload。
- manifest 声明 `"unlimited_storage"`(IndexedDB 容量也受其约束)。
- **不丢未推数据**:推送成功 → 立即从 IndexedDB + queue_index 删除该队列项;`pending/failed` 永不淘汰。触顶(默认 500 条 pending)→ **暂停抓取**(content.js 每抓一条前向 SW 查队列深度)+ popup 红字提示"队列拥堵,请检查后端"。
### 8.3 推送 worker(修订:alarms 1 分钟)
- 用 **`chrome.alarms`**(`periodInMinutes: 1`),**不用 setInterval**(MV3 SW 会休眠)。
- 注:打包/正式版 alarms 最短 1 分钟(开发版可临时 30s)。
- alarm 触发 → 取最多 N(默认 3)条 pending 且到 nextRetry 的项 → POST → 成功则删除该队列项 / 失败 `retries++`、`nextRetry = now + min(2^retries, 3600)s`,超 8 次标 failed 保留。
- **吞吐量(v3)**:1 分钟 × 3 条 = 180 条/小时 ≈ 4320 条/天。全量几千条要好几天才推完,这是异步后蹲、与"慢抓"主题一致;**抓完 ≠ 入库**,popup 的"已推/待推"计数要让使用者意识到。瓶颈在 worker 的 3/min,服务器限流 60/min(§7)永远用不到;日后想加速可提高每 tick 的 N。
### 8.4 状态展示
popup 显示「已推 N / 待推 M / 失败 K」,失败可查看/重试。
### 8.5 manifest 改动(修订:optional permissions)
```jsonc
{
"permissions": ["storage", "activeTab", "alarms", "unlimitedStorage"],
"optional_host_permissions": ["http://*/*", "https://*/*"],
"host_permissions": [
"*://message.alibaba.com/*", "*://alicrm.alibaba.com/*", "*://onetalk.alibaba.com/*"
]
}
```
- 后端服务器地址**不写死在 host_permissions**(运行时不可改,且占位符非法会拒载)。
- 用 `optional_host_permissions: ["http://*/*","https://*/*"]` + 用户在配置页通过 `chrome.permissions.request({origins:[serverUrl]})` **运行时授权**具体后端地址。
- 替代:若插件私有/不上架,可直接 `host_permissions: ["*://*/*"]` 一劳永逸(但上架审核会问)。
### 8.6 抓取侧字段扩展(修订)
- `fetchChatByApi()` 在调 `querySummary.htm` 后,**提取 `gmtCreate`/`gmtModified`/`country` 等字段**并随 payload 返回。**country 改用 `querySummary.contact.countryCode`**(样本中 `"countryCode":"PE"`,比 DOM 的 `detailCountry` 可靠),是**替换**而非新增字段。
- `apiMsgToChat()` 扩展产出 `{messageId: String(m.messageId), sendTime: new Date(m.sendTime).toISOString(), sender, content, direction, translation}`,其中 translation 从 `m.extInfo.icbuData.extend.extParams.inputTranslateInfo.translate` 解析(失败留空)。**注意双重 JSON.parse(v3)**:`extend` 本身是 JSON 字符串,`extParams.inputTranslateInfo` 又是嵌套 JSON 字符串,需 `JSON.parse(JSON.parse(extend).extParams.inputTranslateInfo).translate` 两层解析,直接对象访问会拿到 undefined。
- **behavior 数字字段强制转 null(v3)**:`productViews/validInquiries/validRfq/loginDays/spamInquiries` 在 content.js 现为字符串('' 或数字串),payload 序列化时空串与抓不到的值都要 coerce 为 `null`,匹配后端 zod 的 `number | null`。
## 9. 后台前端
| 路由 | 页面 | 要点 |
|---|---|---|
| `/login` | 登录 | |
| `/inquiries` | 列表 | Element Plus 表格 + 筛选栏(国家/业务员/状态/时间)+ 搜索 + 排序 + 分页 + 导出按钮 |
| `/inquiries/:id` | 详情 | 客户信息+行为卡片 + 完整聊天(虚拟滚动,游标加载) |
| `/stats` | 统计 | 数字卡片 + 国家 Top10 饼图 + 业务员柱状图 + 月趋势折线(ECharts) |
性能:聊天用虚拟滚动(vue-virtual-scroller);列表分页默认 20/页。
## 10. 可靠性与安全
| # | 措施 |
|---|---|
| 幂等 | 唯一键 upsert + 早返回(inquiry 存在 **且 has_failed_batches=false** 且 count/lastSendTime 未变则跳过) |
| 队列不丢 | 独立 key 持久化 + 只淘汰 success + 触顶暂停抓取 |
| 批量写入 | 原生 `INSERT ON DUPLICATE KEY` 分批 500 行/批(见 §5.2),独立事务,失败批次隔离 |
| 鉴权 | 插件 Bearer token(env);管理员 JWT(access 短期 + refresh) |
| 密码 | bcrypt |
| CORS | `/api/ingest` 用 token 鉴权,`Access-Control-Allow-Origin: *` 不做 origin 校验;后台 API 限前端域名 |
| XSS | Vue 默认转义,禁用 v-html 渲染用户数据 |
| body limit | Express `limit:50mb`;**nginx `client_max_body_size 50m`** |
| MySQL | `max_allowed_packet` 设为 128MB(**MySQL 8 服务端默认 64MB,非 4MB**;4MB 是老版本/客户端库默认值,易误传);部署前 `SHOW VARIABLES LIKE 'max_allowed_packet'` 实测确认。单批 500 行(含 MEDIUMTEXT 时 500 行 × 极端 64KB ≈ 32MB,留余量) |
| 备份 | 每日 mysqldump → gzip,保留 14 天,异地复制 |
## 11. 部署
### 11.1 docker-compose(结构)
```yaml
services:
mysql: # mysql:8, 卷, healthcheck, max_allowed_packet 调大
backend: # Node API, depends_on mysql, .env
nginx: # 反代 + 静态 + client_max_body_size 50m; 正式加 HTTPS
cron: # 备份 sidecar(或宿主 crontab),每日 mysqldump
```
### 11.2 测试环境(无域名)
- 公网 IP + 端口,后端 HTTP。
- **扩展 SW 的 fetch 起源于 `chrome-extension://` 上下文,不受网页 mixed-content 规则约束**,只要 optional_host_permissions 授权了该 HTTP URL,推送正常。
- **私网 IP(192.168/10.x)注意**:Chrome 的 Private Network Access 会要求预检;优先用公网 IP 测试。
### 11.3 正式环境(有域名)
- 域名 + Nginx + Let's Encrypt(自动续期)+ 全程 HTTPS。
## 12. 项目结构
```
d:\alibb\ # 现有插件(增量改造:推送模块 + 字段扩展)
d:\alibb-server\
backend\ prisma\schema.prisma + src\{routes,services,middleware,utils} + Dockerfile
frontend\ Vue3 工程 + Dockerfile
docker-compose.yml
.env.example (DATABASE_URL / INGEST_TOKEN / JWT_SECRET / JWT_REFRESH_SECRET / ADMIN_USERNAME / ADMIN_PASSWORD)
```
## 13. 分期
**MVP**:后端骨架(Express+Prisma+MySQL)+ `/api/ingest`(校验+幂等+批量)+ auth/JWT + 列表/详情/消息/统计/导出 API + 前端四件套 + 插件推送模块 + Docker + 测试部署 + 备份。
**后续**:增量推送、聊天全文搜索、跟进管理、客户合并/标签、复杂报表、多管理员、存量 CSV 导入。
## 14. 风险与待确认
- **R1 payload 体量**:单询盘几千条约 2-5MB;body limit 50MB + nginx 50m + max_allowed_packet 64MB。>1万条极端询盘 MVP 后改分批推送。
- **R2 增量**:MVP 全量重推 + 早返回优化(未变则跳过);日抓量大后做真增量(插件记 last sendTime)。
- **R3 manager 重名**:字符串统计,同名合并;后续加 employees 表。
- **R4 测试环境网络**:公网 IP+HTTP 推送无混合内容问题(扩展 fetch 独立上下文);私网 IP 需注意 Private Network Access 预检。已删除"自签 HTTPS"复杂方案。
- **R5 customerId 缺失**:customer_id 可空,无 customerId 的询盘只存 inquiry+messages,customer 留空。
- **R6 早返回的边界**:幂等早返回条件为 `has_failed_batches=false` 且 `messages_count + last_send_time` 未变。
- **失败批次重试(v3 修正)**:早返回已加入 `has_failed_batches`——上次有批次失败时强制不走早返回,确保失败批次被补写。inquiries 表新增 `has_failed_batches` 列;`messages_count` 存实际插入数(payload 长度会让重推 count 恒定一致,反而触发早返回——这是 v2 的逻辑反转)。
- **内容修订**:同 messageId 内容(如 translation)被上游修订时,早返回仍会跳过更新。MVP 接受(重推主场景是新增消息)。
- **R7 首次全量回填耗时**:首次把现有几千询盘全抓入库,按"每询盘几千条聊天 × API 拉取 + 推送"估算需 **数小时**,且**浏览器必须一直打开阿里页**不能关。要求插件支持**分批抓取 + 可中断续抓**(基于 queue/inquiryId 进度持久化,中断后重开继续,而非重头来)。日常增量抓取不受此影响。