mirror of
https://github.com/sinanyuntu/trade-message-center.git
synced 2026-09-17 13:22:11 +08:00
chore(task): archive 09-14-collect-product-messages
This commit is contained in:
@@ -0,0 +1,3 @@
|
||||
{"file":".trellis/spec/project/architecture.md","reason":"检查共享合同唯一所有者和跨包依赖方向。"}
|
||||
{"file":".trellis/spec/chrome-extension/frontend/quality-guidelines.md","reason":"检查扩展的 focused test、typecheck、build 验证。"}
|
||||
{"file":".trellis/spec/server/backend/quality-guidelines.md","reason":"检查服务端迁移、读取投影和测试验证。"}
|
||||
@@ -0,0 +1,65 @@
|
||||
# 商品消息收集设计
|
||||
|
||||
## 做到什么程度
|
||||
|
||||
本任务的完成边界是“收集可安全表达的商品链接事实”,而不是“收集页面渲染的商品卡”。商品卡 DTO 中的 `productImage`、标题、价格、MOQ 来自当前页面 React 模板数据,不是原始 OneTalk 消息接口返回;它们不在本任务的可信输入与持久化范围内。
|
||||
|
||||
系统只保存一个经过严格规范化的详情页引用和从路径提取的 `productId`。后续若要做商品详情富化,应单开任务,先确定合法数据源、刷新策略、价格/库存时效和访问凭据边界。
|
||||
|
||||
## 数据流
|
||||
|
||||
```text
|
||||
OneTalk 接口 data.messageList[].content
|
||||
= 含 query 的原始商品详情 URL
|
||||
↓ SDK 历史适配
|
||||
SDK 扁平项 originalData.text
|
||||
↓ MAIN world: 共享 normalizeOneTalkProductUrl()
|
||||
{ version: 1, kind: "product", sourceUrl, productId }
|
||||
↓ 既有 page bridge / Service Worker
|
||||
message.observed 或 messages.observed
|
||||
↓ Bright: contract 校验 → DB transaction → ACK → message.created
|
||||
onetalk_message.content JSONB
|
||||
↓ Bright read projection
|
||||
Mind history / message.created 的 content
|
||||
```
|
||||
|
||||
原始实时消息入口也使用相同逻辑:`contentType=1` → `text.content.content` → `normalizeOneTalkProductUrl()`。这样实时/历史入口不会各自维护分类规则。
|
||||
|
||||
## 责任边界
|
||||
|
||||
| 层 | 负责 | 不负责 |
|
||||
| --- | --- | --- |
|
||||
| `packages/onetalk-contract/src/content.ts` | `product` 类型、精确形状、URL 规范化、`productId` 一致性校验 | 网络请求、SDK 读取、DB 写入 |
|
||||
| `content-decoder.ts`(MAIN world) | 对暂存原始文本调用共享解析,再回退为 text | 跨边界传 raw URL 或解析页面卡片 |
|
||||
| page bridge / Service Worker | 传递已验证的 observation frame | 新增商品专用协议或修改 ACK 顺序 |
|
||||
| server | 用共享合同验证、把 JSONB 事实入库、既有 ACK/发布/读取投影 | 从 URL 补商品资料 |
|
||||
| database | 容纳 `content` JSONB,允许 `kind=product` | 保存 raw 消息或提供商品缓存 |
|
||||
|
||||
## 归类规则
|
||||
|
||||
`normalizeOneTalkProductUrl(value)` 仅返回安全的 `{ sourceUrl, productId }` 或 `null`:
|
||||
|
||||
1. 输入必须是去首尾空白后未发生变化的字符串,长度受共享文本上限限制。
|
||||
2. URL 必须为 HTTPS、host 精确为 `chinese.alibaba.com`、无 port、username、password 和 hash。
|
||||
3. pathname 必须匹配 `/product-detail/<slug>-<productId>.html`;`productId` 是路径最后一个、非零开头的数字段。
|
||||
4. 输出的 `sourceUrl` 固定为 `url.origin + url.pathname`,永远不带 query;`productId` 必须等于该路径提取值。
|
||||
5. 验证已持久化的 product 时再次执行相同规范化,要求其输出严格等于存储的 `sourceUrl` 和 `productId`。
|
||||
|
||||
若任一商品规则不满足,文本正常继续归类为 `text`;因为它不是损坏媒体,也不记作 anomaly。
|
||||
|
||||
## 存储与兼容性
|
||||
|
||||
- 为 `onetalk_message_content_v1_chk` 新增 `product` 到 kind 枚举;由于数据库没有旧 `product` 行,迁移只有 drop/add constraint,不做 `UPDATE`。
|
||||
- SQL check 不扩展为每种内容的 JSON schema;与现有 text/image/file/order 一致,exact-shape 由共享合同在插件入站 frame 和服务端读取 JSONB 时强制。
|
||||
- `read-projection.ts` 已对非 `business_card` 内容做精确浅复制;`product` 走通用分支,使 history 与 `message.created` 的 content 一致。
|
||||
- 旧客户端不认识 `product` 时会在其共享合同读取边界失败,所以该变更要求扩展、shared contract 与 server 作为一个兼容版本部署;任务不提供静默降级为 text。
|
||||
|
||||
## 安全不变量
|
||||
|
||||
- 只有 MAIN world 可以看到原始 URL;离开它的第一个对象已经移除了 query。
|
||||
- `sourceUrl` 是名称明确的安全派生字段,不是重命名后的 raw `content`。
|
||||
- 任何将 `originalData`、原始 content、URL query、chatToken、加密目标或 React 卡片 DTO 放入 normalized content、frame、日志、数据库或 Mind 响应的改动都违反本设计。
|
||||
|
||||
## 回滚
|
||||
|
||||
新迁移不改写旧事实。回滚应用会停止新 product 写入;但在旧服务读取包含 product 的新行时会因其不认识该 kind 而失败,故部署/回滚须按共享合同、扩展和服务端的兼容版本整体执行。
|
||||
@@ -0,0 +1,3 @@
|
||||
{"file":".trellis/spec/guides/cross-layer-thinking-guide.md","reason":"product 从 MAIN world 经过协议、数据库和 Mind read projection 的跨层合同变更。"}
|
||||
{"file":".trellis/spec/chrome-extension/frontend/onetalk/history-sync.md","reason":"商品源字段来自 SDK 扁平历史消息,沿既有历史同步采集链路进入解码器。"}
|
||||
{"file":".trellis/spec/server/backend/database-guidelines.md","reason":"任务增加 JSONB 内容 kind 的迁移,不改变查询或引入 JOIN。"}
|
||||
@@ -0,0 +1,38 @@
|
||||
# 商品消息收集实施计划
|
||||
|
||||
## 变更边界
|
||||
|
||||
最小行为缺口是:已验证的商品详情 URL 目前被当作 text,且数据库约束不允许 product;需要在共享消息合同的唯一 owner 中表达 product,并让两个文本输入入口共用这个判定。
|
||||
|
||||
预期改动文件:
|
||||
|
||||
| 文件/区域 | 原因 |
|
||||
| --- | --- |
|
||||
| `packages/onetalk-contract/src/content.ts` 与合同测试 | 新增 product 精确类型、唯一 URL 规范化函数和所有 transport/read 验证 |
|
||||
| `apps/chrome-extension/src/onetalk/main-page/message-observer/content-decoder.ts` 与解码测试 | MAIN-world 从 raw/SDK flat history 文本安全归类 product |
|
||||
| `apps/server/src/database/schema/onetalk.ts`、下一条 Drizzle migration/meta、迁移测试 | 允许 JSONB kind `product`,不修改历史内容 |
|
||||
| `apps/server/test/onetalk-read-domain.test.ts` | 锁住 product 的 history/message.created 读取投影和畸形行拒绝 |
|
||||
|
||||
明确不改 page bridge、Service Worker、ACK/发布顺序、授权、去重、查询逻辑、商品详情访问和 UI。
|
||||
|
||||
## 实施顺序
|
||||
|
||||
1. 实施前加载 `trellis-before-dev` 规定的 shared、Chrome extension、server 规范;对每个待编辑 symbol 做 GitNexus upstream impact,HIGH/CRITICAL 影响先报告。
|
||||
2. 在共享合同实现 product 类型与 `normalizeOneTalkProductUrl()`;让 `decodeOneTalkMessageContent()` 对 product 做 exact-shape + canonical URL/ID 一致性校验。
|
||||
3. 扩展合同测试:有效 product、拒绝 query/credentials/fragment/错误 host/path/ID/额外字段,及 observed/message.created 接受 product。
|
||||
4. 将 MAIN-world `normalizeText` 改成首先调用共享解析器;raw `contentType=1` 与 SDK `msgType=101`/`subType=1` 都复用该入口。增加 raw、扁平历史、普通 URL、保密查询参数清除、既有类型不回归的测试。
|
||||
5. 扩展 server schema kind 枚举;按 Drizzle 当前流程生成新的迁移和 meta。检查迁移仅重建约束,且仍保留 business_card marker-only 条件与所有既有 kinds。
|
||||
6. 扩展 migration/read-domain 测试,锁住 JSONB product 行经 history 和 `message.created` 得到同一精确对象,以及手工畸形 product JSONB 被读取边界拒绝。
|
||||
7. 独立检查完整 diff:禁止 raw URL/credential 漏出与重复 URL parser;验证 runtime data flow 是 decode → contract → store → ACK → publish。
|
||||
|
||||
## 验证
|
||||
|
||||
1. `pnpm --filter @trade-message-center/onetalk-contract test`
|
||||
2. `pnpm --filter @trade-message-center/chrome-extension test`(60 秒超时)
|
||||
3. `pnpm --filter @trade-message-center/server test`(60 秒超时)
|
||||
4. 三个包的 `typecheck`,以及 `pnpm --filter @trade-message-center/server db:check`
|
||||
5. `pnpm format:check`、`git diff --check`,并以 GitNexus `detect_changes()` 检查仅影响合同、解码、约束迁移和读取投影。
|
||||
|
||||
## 运行时证据边界
|
||||
|
||||
本任务已有原始接口与 SDK 字段的 Chrome 运行时证据;实现后若加载新扩展,可额外验证该同一消息只发布 query-free product frame。没有重新加载扩展和触发历史读取时,不宣称浏览器端到端验收完成。
|
||||
@@ -0,0 +1,81 @@
|
||||
# 收集 OneTalk 商品消息
|
||||
|
||||
## 目标
|
||||
|
||||
将已经在 OneTalk 中发送或接收的“商品详情链接消息”收集为 `product` 消息事实,让 Bright 持久化并让 Mind 读取一个去凭据、可稳定关联商品的最小引用。
|
||||
|
||||
本任务只收集链接本身派生的信息;不爬取商品页,也不尝试还原页面商品卡的标题、价格、主图或 MOQ。
|
||||
|
||||
## 实时证据与字段来源
|
||||
|
||||
已在会话 `2208314000798-2500002169502#11011@icbu` 的真实 OneTalk 页面验证以下链路:
|
||||
|
||||
| 字段/信息 | 原始位置 | 用途 | 是否可跨 MAIN world / 入库 |
|
||||
| --- | --- | --- | --- |
|
||||
| 商品详情原始 URL | `POST /message/listRecentMessage.htm` 响应的 `data.messageList[].content` | 页面/API 的原始消息正文 | 否;查询参数携带 `chatToken` 与加密登录目标 |
|
||||
| 同一原始 URL | OneTalk SDK 扁平历史项的 `originalData.text` | 扩展历史采集的实际输入 | 仅在 MAIN world 暂存并解析 |
|
||||
| 展示文案 | 扁平历史项的 `content` | SDK 给 UI 的展示内容 | 否;不是规范消息正文,不能作为判定来源 |
|
||||
| 商品 URL 类别线索 | 扁平历史项的 `msgType=101`、`subType=1` | 与现有普通文本消息共用的 SDK 分类 | 只作为进入文本/商品识别的前提,不能单独认定为商品 |
|
||||
| `sourceUrl` | 从 `originalData.text` 使用 `URL` 解析后取 `origin + pathname` | 安全、无查询参数的商品详情页引用 | 是 |
|
||||
| `productId` | 从已验证路径 `/product-detail/<slug>-<数字>.html` 最后一个数字段提取 | 稳定商品标识 | 是 |
|
||||
|
||||
本任务只接受实时验证过的 `https://chinese.alibaba.com/product-detail/<slug>-<数字>.html` 形态:不接受其它 host、路径、协议、端口、用户名/密码、fragment 或无法提取数字 `productId` 的链接。合法但不匹配该形态的文本仍是 `text`,而不是错误或 `product`。
|
||||
|
||||
## 现状与行为缺口
|
||||
|
||||
- `history.ts` 将 SDK 扁平历史项的 `originalData` 交给 `content-decoder.ts`;现有 `msgType=101` / `subType=1` 且 `originalData.text` 为字符串的消息会进入 `normalizeText`。
|
||||
- `content-decoder.ts` 运行于 MAIN world。它已有图片、文件、名片、询盘、订单的安全归一化入口,但没有商品 URL 分支。
|
||||
- `packages/onetalk-contract` 的 `OneTalkMessageContent` 当前为 `text | image | file | business_card | inquiry | order`,所有 observation frame、服务端摄入、读取投影都依赖该共享合同。
|
||||
- `onetalk_message.content` 是唯一的规范化 JSONB 消息内容;目前数据库 check 约束的 kind 列表不含 `product`。
|
||||
|
||||
## 目标合同与数据库事实
|
||||
|
||||
新增内容必须是精确形状,不能携带任何原始字段:
|
||||
|
||||
```json
|
||||
{
|
||||
"version": 1,
|
||||
"kind": "product",
|
||||
"sourceUrl": "https://chinese.alibaba.com/product-detail/HAGO-Men-s-Breathable-Mid-Rise-1601456609478.html",
|
||||
"productId": "1601456609478"
|
||||
}
|
||||
```
|
||||
|
||||
该对象将作为 `onetalk_message.content` 的 JSONB 值写入;表中同一事实行同时已有既有消息元数据:
|
||||
|
||||
```text
|
||||
channel_account_id ← 已认证的插件/页面账号 scope(不是 URL 或 activeAccountId)
|
||||
conversation_id ← OneTalk 消息的 cid / conversationCode
|
||||
message_id ← OneTalk messageId,经既有 normalizeOneTalkMessageId 规范化
|
||||
sender_id ← OneTalk sender.uid
|
||||
direction ← 由 sender 与当前账号的既有方向判定
|
||||
sent_at_ms ← OneTalk createAt / sendTime
|
||||
content ← 上述精确 product JSONB
|
||||
participant_ids、read_status、message_status、unread_count ← 既有消息观察字段
|
||||
first_observed_at、last_observed_at ← Bright 观察时间
|
||||
```
|
||||
|
||||
不会新增 `raw_content`、`product_url`、`product_id` 等独立列,不会持久化 `originalData`,也不会保留原始 URL 的 query string。因此 `chatToken`、加密登录目标和任意追踪参数不会出现在 observation frame、数据库 JSONB、日志或 Mind 响应中。
|
||||
|
||||
## 需求
|
||||
|
||||
1. 在共享 OneTalk 内容合同中新增 `OneTalkProductContent`,精确字段为 `version`、`kind`、`sourceUrl`、`productId`;合同校验要拒绝 query、错误 host/path、ID 不一致、额外字段和其它畸形值。
|
||||
2. 在共享合同中提供唯一的商品 URL 规范化/解析函数,供 MAIN-world 解码和已归一化内容校验共用,禁止第二套路径/URL 解析规则。
|
||||
3. 原始文本与 SDK 扁平历史文本入口都先尝试该商品解析;命中后归一化为 `product`,否则保持现有 `text` 行为。
|
||||
4. 更新数据库 kind 约束并新增迁移;迁移只增加 `product` 允许值,不回填或改写已有事实。
|
||||
5. 通过现有 `message.observed` / `messages.observed`、数据库摄入、ACK 后发布、history 和 `message.created` 读取链路传递 `product`,不新增 transport、授权、去重或发布顺序。
|
||||
6. 添加合同、扩展解码、迁移与服务端读取回归测试;测试证明 raw URL 查询参数不会越过 MAIN-world 边界。
|
||||
|
||||
## 不在范围内
|
||||
|
||||
- 请求商品详情页、调用商品 API、解析 React 商品卡 DTO,或保存 `productImage`、标题、价格、MOQ、促销和下单动作。
|
||||
- 扩展支持的 host/path 以外的商品链接。
|
||||
- 商品消息发送、URL 可访问性探测、图片/附件下载,或迁移历史 `text` 行为。
|
||||
|
||||
## 验收标准
|
||||
|
||||
- [ ] 已验证 URL 形态的历史或原始文本消息会产生上述精确 `product` 内容,并进入既有观察与提交链路。
|
||||
- [ ] 普通文本 URL 与不能严格解析的候选 URL 仍为 `text`;图片、文件、名片、询盘和订单行为不变。
|
||||
- [ ] `onetalk_message.content` 保存精确的 product JSONB,读取 history 与 `message.created` 返回相同的安全对象。
|
||||
- [ ] 原始 `content`、`originalData`、URL 查询参数、`chatToken` 和加密登录目标不出现在消息 frame、数据库内容、诊断或 Mind 读模型中。
|
||||
- [ ] 合同、入站 frame 和持久化读取会拒绝畸形/附加字段的 `product` 内容;数据库迁移保留所有既有 kind 约束。
|
||||
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"id": "collect-product-messages",
|
||||
"name": "collect-product-messages",
|
||||
"title": "收集 OneTalk 商品消息",
|
||||
"description": "将商品详情消息安全归一化为 product 内容,并经既有同步链路提交给 Bright/Mind。",
|
||||
"status": "completed",
|
||||
"dev_type": null,
|
||||
"scope": null,
|
||||
"package": null,
|
||||
"priority": "P2",
|
||||
"creator": "ybf",
|
||||
"assignee": "ybf",
|
||||
"createdAt": "2026-09-14",
|
||||
"completedAt": "2026-09-14",
|
||||
"branch": "09-14-collect-product-messages",
|
||||
"base_branch": "main",
|
||||
"worktree_path": null,
|
||||
"commit": null,
|
||||
"pr_url": null,
|
||||
"subtasks": [],
|
||||
"children": [],
|
||||
"parent": null,
|
||||
"relatedFiles": [],
|
||||
"notes": "",
|
||||
"meta": {}
|
||||
}
|
||||
Reference in New Issue
Block a user