mirror of
https://github.com/sinanyuntu/trade-message-center.git
synced 2026-09-17 13:22:11 +08:00
chore(task): archive 09-12-onetalk-profile-active-sync
This commit is contained in:
@@ -0,0 +1,4 @@
|
||||
{"file":".trellis/spec/chrome-extension/frontend/quality-guidelines.md","reason":"Focused test, typecheck and build verification requirements."}
|
||||
{"file":".trellis/spec/chrome-extension/frontend/onetalk/contact-profile-sync.md","reason":"Verify profile durable-first, hash, batching, ACK and account isolation invariants."}
|
||||
{"file":".trellis/spec/chrome-extension/frontend/onetalk/page-bridge.md","reason":"Verify command validation, exact page routing and stale identity fences."}
|
||||
{"file":".trellis/spec/chrome-extension/frontend/onetalk/runtime-sync.md","reason":"Verify message/profile lifecycle separation and no duplicate page activation."}
|
||||
@@ -0,0 +1,80 @@
|
||||
# 技术设计:OneTalk 客户资料活跃同步
|
||||
|
||||
## 目标边界
|
||||
|
||||
基础客户资料是低优先级事实。它不再由 page hello、会话选择变化、Port 重连、Bright 重连或历史同步主动全量采集;仅有首次认证初始化、新会话和任一方向的 live 消息活动三类业务触发。页面资料可以重复跨 MAIN → bridge → Service Worker;唯一的去重与上传资格判断在 durable profile hash。
|
||||
|
||||
Bright WebSocket `contact.profile.observed`、服务端 profile transaction、ACK CAS、profile 白名单与 `(channelAccountId, conversationId)` ledger key 均保持不变。
|
||||
|
||||
## 现状与根因
|
||||
|
||||
`OneTalkPageRuntimeHost.handlePageIdentity()` 和 `replayTo()` 目前无条件调用 account-level `onetalk.contact.snapshot`。MAIN observer 的 `snapshot()` 每次读取完整 `__conversationListData__` 并无条件发布全部 profile;coordinator 随后逐条调用 `putPendingProfile()`。已上传的相同指纹虽不会进入 Bright frame,却仍产生 IndexedDB high-water 写入和 `profile_observed: skipped` 诊断。
|
||||
|
||||
已存在的 `profileFingerprint` 是 `v1-xxxxxxxx`:8 位十六进制 FNV-1a 结果,输入是当前白名单基础 profile 字段。它已被 ledger 以 `pending.fingerprint` / `lastUploadedFingerprint` 持久化,不新建 hash 或第二份 profile 状态。
|
||||
|
||||
## 目标数据流
|
||||
|
||||
```text
|
||||
首次安装账号 + ws.accepted + 页面可用
|
||||
-> account snapshot(页面已加载 direct profiles)
|
||||
-> 全部 durable 写入
|
||||
-> 每帧最多 50 条发送
|
||||
|
||||
新会话 / syncData
|
||||
-> page profile observed(允许旧会话再次经过 bridge)
|
||||
-> IndexedDB hash compare
|
||||
-> 相同 hash 静默结束;不同 hash 才 durable write + upload
|
||||
|
||||
任一 live message(sent / received)
|
||||
-> 唯一 conversationId
|
||||
-> account-level targeted collect(不切换页面、不扫全表)
|
||||
-> IndexedDB hash compare
|
||||
-> hash 变化才 durable write + Bright upload
|
||||
```
|
||||
|
||||
history、手动历史同步和 `messageType: "history"` 不走 targeted collect。
|
||||
|
||||
## MAIN 页面观察器与命令
|
||||
|
||||
扩展 `OneTalkContactProfileObserver` 为三个明确操作:
|
||||
|
||||
- `snapshot()`:读取当前已加载 direct profile,并发布完整 profile 列表;仅首次认证初始化使用。
|
||||
- `collectConversation(conversationId)`:从当前 `__conversationListData__` 精确查找一个 direct conversation,并只发布该 profile;找不到、群聊或不完整身份返回 `profileCount: 0`,不猜测、不切换 UI。
|
||||
|
||||
`syncData` 与 CRM 观察保留页面侧已有的资料发布能力;它们不负责也不拥有 profile 上传去重。旧会话即使再次发布 profile,也由 Service Worker 的 durable hash 读比较静默结束。MAIN 不保存 `seen` 作为资料资格或上传 gate;新会话和 live activity 的识别仅决定何时尝试读取页面资料。
|
||||
|
||||
新增两个 MAIN 命令:
|
||||
|
||||
- `onetalk.contact.collect`:account-level,命令 payload 带严格的 `conversationId`;它不是“当前选择会话”路由,必须只投递到唯一同账号页面。
|
||||
|
||||
保留 `onetalk.contact.snapshot` 作为首次初始化命令。两个命令在 MAIN dispatcher 中显式校验 action 与 exact payload,未知/多余字段走既有 `invalid_request`。
|
||||
|
||||
## Service Worker 生命周期
|
||||
|
||||
`OneTalkPageRuntimeHost` 将 page identity 的消息同步职责与 profile 初始化职责解耦:
|
||||
|
||||
- page identity 仍立刻驱动 sync engine 和 buyer fact 的既有 lifecycle。
|
||||
- host 记录当前 authenticated account;`ws.accepted` 与 page identity 任何先后顺序都调用同一个 guarded profile setup。
|
||||
- profile setup 读取 profile ledger 是否已有该 account 的任何记录:无记录时 claim 一次首次 snapshot;有记录时不发 profile page command。claim 绑定 configuration epoch、page connection epoch、active profile coordinator,防止 hello/replay/status 的竞态重复请求。
|
||||
- 失败、页面不可用、配置/Port/coordinator 失效不会留下成功 claim;下一个有效认证或页面 lifecycle 可重试。成功后同一 active setup 不重复请求。
|
||||
- `replayTo()` 不再直接请求 snapshot,只复用这个 setup gate。
|
||||
|
||||
在 profile page bridge 消息抵达时,host 先执行既有 account/coordinator fence,再交给 coordinator。对 live 消息,host 从 `OneTalkPageObservedMessage.batch` 提取 `messageType === "new"` 的唯一 conversationId,发起 `onetalk.contact.collect`。收发方向不参与筛选;history 不触发。每个 conversationId 有短生命周期 in-flight correlation,避免同一批或未完成命令重复路由;完成后下一条 live 消息仍会重新读取并比较 hash。
|
||||
|
||||
## Durable 去重与发送
|
||||
|
||||
在调用 `putPendingProfile()` 前,coordinator 对每个收到的 profile 读取同 key ledger;这是唯一去重闸门:
|
||||
|
||||
- `pending.fingerprint` 或 `lastUploadedFingerprint` 等于新 `profileFingerprint`:读取后直接返回。不得更新 observed timestamp/high-water、不得写 IndexedDB、不得发 `profile_observed` 诊断、不得调用 flush。
|
||||
- 指纹不同或记录不存在:保持 current durable-first `putPendingProfile()`,再进入 flush。
|
||||
|
||||
initial snapshot 的所有输入必须先完成 durable 写入,再开始一次 flush。发送分组策略从协议上限 100 收紧为扩展本地 `50`,同时继续执行 256 KiB 实际 frame 大小检查。服务端仍可接收协议允许的 1..100,不改 contract 或数据库。
|
||||
|
||||
`profile_observed` 仅在实际进入 pending 的 profile 上报。首次 snapshot 可使用一条不含 profile 值的聚合诊断;相同 hash 是正常静默路径。
|
||||
|
||||
## 风险与回滚
|
||||
|
||||
- 首次 snapshot 只包含 OneTalk 页面已经加载的资料;不会新增 SDK/CRM 请求。若列表随后更新,new-conversation `syncData` 与 live message targeted collect 仍可补齐。
|
||||
- 旧会话再次穿过 MAIN、bridge 和 Service Worker 是允许的低成本路径;相同 hash 只做一个 IndexedDB 读取。不要重新引入 MAIN-side `seen` 或账号级 refresh state 作为第二个去重来源。
|
||||
- 8 位 FNV hash 有理论碰撞风险,但沿用已发布、低优先级资料的现有 fingerprint;本任务不改变字段或 hash 算法,避免重新采集和兼容性迁移。
|
||||
- 回滚只需恢复原触发逻辑;ledger 与 server schema 均不迁移,已有 pending 仍按现有 ACK 规则恢复。
|
||||
@@ -0,0 +1,5 @@
|
||||
{"file":".trellis/spec/guides/index.md","reason":"Cross-layer trigger and ownership review guide."}
|
||||
{"file":".trellis/spec/chrome-extension/frontend/index.md","reason":"Chrome extension package baseline and required quality checks."}
|
||||
{"file":".trellis/spec/chrome-extension/frontend/onetalk/contact-profile-sync.md","reason":"Profile whitelist, durable ledger, ACK, identity and sensitive-field contract."}
|
||||
{"file":".trellis/spec/chrome-extension/frontend/onetalk/page-bridge.md","reason":"MAIN/ISOLATED/Service Worker routing and page identity constraints."}
|
||||
{"file":".trellis/spec/chrome-extension/frontend/onetalk/runtime-sync.md","reason":"Page command ownership, lifecycle and runtime composition rules."}
|
||||
@@ -0,0 +1,36 @@
|
||||
# 执行计划:OneTalk 客户资料活跃同步
|
||||
|
||||
## 1. MAIN 资料观察与命令
|
||||
|
||||
1. 更新 `contact-observer/entry.ts`:保留页面资料发布行为,增加 targeted conversation collect;不使用 MAIN `seen` 作为 profile 去重或上传资格判断。
|
||||
2. 更新 `contact-observer/page-command.ts` 与 `main-page/commands/index.ts`:显式处理 targeted collect,严格校验 `conversationId`,保留 snapshot。
|
||||
3. 更新 page command/bridge routing:新命令以 account-level 方式精确路由到唯一同账号页面,禁止按当前选择会话猜测或广播。
|
||||
4. 增加 observer / MAIN command focused tests:首次 snapshot、重复资料可再次发布、targeted collect、live target missing/group、pagehide 与账号切换。
|
||||
|
||||
## 2. Service Worker lifecycle 与 live trigger
|
||||
|
||||
1. 从 `handlePageIdentity()`、`replayTo()` 移除无条件 profile snapshot;保持消息/buyer 的原有 page-ready 行为。
|
||||
2. 在 Bright authenticated 状态和 page identity 两端接入相同 profile setup gate;无 profile ledger record 时执行 initial snapshot,有记录时不发初始化资料命令,并覆盖两者乱序、重复 hello、reconnect、配置替换和 Port 失效。
|
||||
3. 在 page observed message 的 Service Worker 边界提取唯一 live conversationId,路由 targeted collect;history 不触发,收发方向均触发。
|
||||
4. 增加 runtime/controller/session tests,确认首次认证只全量一次、已初始化账号不再发初始化命令、会话切换不 snapshot、live/history 分流和 account/page fence。
|
||||
|
||||
## 3. Ledger 去重与发送策略
|
||||
|
||||
1. 为 profile store 增加只读的 account-record existence 查询,不改变 store schema 或记录形状。
|
||||
2. coordinator 对每个收到的 profile 在 `putPendingProfile()` 前读取 pending / uploaded fingerprint;相同 hash 在这次唯一 durable 读取后完全短路,不写 timestamp、不发诊断、不 flush。
|
||||
3. 将 extension outbound profile batch policy 固定为 50 条;保留 shared contract 的 100 条接收上限与 256 KiB frame 检查。
|
||||
4. 收敛 profile diagnostics:只报告真正 pending 的 observation,initial snapshot 用安全 aggregate,移除正常 skipped 风暴。
|
||||
5. 增加 store/coordinator tests:重复跨桥 profile 的一次读/零写/零诊断/零发送、same pending、same uploaded、different hash、50/51/101 条分包、ACK CAS、offline/reconnect、future-skew 与无 profile record 的初始化判定。
|
||||
|
||||
## 4. 质量门与实际验证
|
||||
|
||||
1. 运行扩展 focused tests(profile observer、page commands/bridge、service-worker runtime、sync runtime/controller、profile store/coordinator)。
|
||||
2. 运行 `pnpm --filter @trade-message-center/chrome-extension typecheck`、相关包 test 与根级 format/diff 检查。
|
||||
3. 运行 `gitnexus detect_changes` 审核受影响 symbol / flow;检查 diff 不混入既有 `apps/mind-test-harness/src/config.ts` 改动。
|
||||
4. 用 Chromium 验证:首次 auth 一次性 profile frames(每帧 ≤50);连续 hello/会话切换无全量 frame;live sent 和 received 各触发目标会话检查;相同 hash 无 IndexedDB 写/诊断/WS frame。
|
||||
|
||||
## 回滚点
|
||||
|
||||
- MAIN command/observer 改动完成后,先跑 observer 与 bridge tests。
|
||||
- lifecycle gate 完成后,先跑 runtime/controller tests;若 page identity 或 auth 顺序有回归,先回滚该 gate,不改 ACK/ledger 语义。
|
||||
- batch / hash short-circuit 完成后,先跑 coordinator/store tests;不得以隐藏诊断代替验证无写入、无 flush 的真实短路。
|
||||
@@ -0,0 +1,40 @@
|
||||
# 收敛 OneTalk 客户资料活跃同步
|
||||
|
||||
## Goal
|
||||
|
||||
将 OneTalk 基础客户资料采集收敛为低优先级、由实际业务活动驱动的流程:首次安装后的首次 Bright 认证采集一次已加载资料;后续新会话或新消息活动可以再次把资料送到 Service Worker。已持久化的资料指纹是唯一去重闸门:相同则只做一次 IndexedDB 读取,不产生写入、`profile_observed` 诊断或 Bright 上传。
|
||||
|
||||
## Confirmed Facts
|
||||
|
||||
- 当前页面 hello、会话选择变化和 runtime replay 可触发全量 `onetalk.contact.snapshot`,其每次都会读取并发布当前 `__conversationListData__`,造成重复 `profile_observed: skipped`。
|
||||
- `profileFingerprint` 已是白名单基础资料生成的 `v1-xxxxxxxx`(8 位十六进制 hash),以 `[channelAccountId, conversationId]` 为 key 持久化在 profile IndexedDB ledger 的 pending/last-uploaded 状态中。
|
||||
- 当前 profile coordinator 会在每次 `observe()` 完成写入后立即 flush;协议上限为 100 条,但产品要求后续实际上传以 50 条为一组。
|
||||
- 已归一化 live 消息带有 `messageType: "new"`、`direction` 与 `conversationId`,因此可作为目标会话资料检查的精确触发,而不必扫描完整会话列表。
|
||||
|
||||
## Requirements
|
||||
|
||||
1. 首次采集:仅当本次扩展安装对应的 channel account 尚无 profile ledger 记录、Bright 已认证且 OneTalk 页面可用时,采集页面已经加载的基础客户资料一次。页面 hello、会话切换、Port 重连与 Bright 重连不得重复触发该全量采集。
|
||||
2. 新会话:`im-conversation-list:syncData` 的 direct profile 可以正常经过 MAIN → bridge → Service Worker;MAIN 不维护用于决定是否上传的 profile 去重状态,durable hash 是唯一的上传资格判断。
|
||||
3. 新消息:收到任一方向的 live 新消息时,只检查消息所属的一个 direct conversation 的资料;不得把 history、手动历史同步或全量会话扫描当作资料采集触发。资料是否上传仍完全由 durable hash 比较决定。
|
||||
4. 指纹去重:每个到达 Service Worker 的 profile 都用已有白名单字段计算现有 8 位 hash,并对同一 account/conversation 执行一次 durable profile ledger 读取;与 pending 或已上传 hash 相同则完全无副作用。不同或没有 ledger record 才 durable-first 写入并进入上传。
|
||||
5. 上传:一次初始采集或其他收集完成后,先完成所有待发 profile 的 durable 写入,再按最多 50 条 profile 一个 `contact.profile.observed` frame 发送。保留现有 256 KiB 限制、ACK CAS、future-skew 拒绝、离线 pending 恢复和账号隔离。
|
||||
6. 诊断:`profile_observed` 只表示实际需要发送的目标会话资料;首次采集可以使用一个安全的聚合诊断,不能为已知/相同 hash 的资料逐条记录 `skipped`。
|
||||
|
||||
## Out of Scope
|
||||
|
||||
- 不增加 24 小时定时或机会式全量刷新。
|
||||
- 不引入微批 timer、第二条 WebSocket、主动 CRM/HTTP 请求,或扫描/切换页面会话。
|
||||
- 不修改 Bright 服务端协议、数据库 schema 或 profile 的白名单字段。
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- [ ] 新安装、认证完成且页面可用时,恰好进行一次当前已加载资料的全量采集;重复 hello、会话切换、Port/Bright 重连均不重复全量采集。
|
||||
- [ ] 初始采集后的旧会话资料即使再次经过 MAIN → bridge → Service Worker,相同 hash 也只产生一次 IndexedDB 读取;没有写入、`profile_observed` 诊断或 Bright frame。
|
||||
- [ ] 一个合格的 live 新消息只请求/检查其所属 conversationId 的 profile;history 不触发资料检查。
|
||||
- [ ] 相同 8 位 profile hash 不写 ledger、不产生 `profile_observed`、不向 Bright 发送;不同 hash 仍按 durable-first、ACK CAS 正确上传。
|
||||
- [ ] 初始或多会话待发资料按每帧最多 50 条拆分,同时满足既有 256 KiB 限制;50 以下不被人为拆分。
|
||||
- [ ] group、缺少登录身份、未知目标会话和账号不匹配继续 fail closed,不推断 identity。
|
||||
|
||||
## Key Decision
|
||||
|
||||
- live 新消息不区分 `direction`;客户入站和用户发送均触发该会话的一次目标资料检查。相同 hash 是正常无副作用路径,不产生 ledger 写入、诊断或上传。
|
||||
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"id": "onetalk-profile-active-sync",
|
||||
"name": "onetalk-profile-active-sync",
|
||||
"title": "收敛 OneTalk 客户资料活跃同步",
|
||||
"description": "仅在首次认证和会话活跃时采集客户资料,按持久化指纹去重并以 50 条分包上传",
|
||||
"status": "completed",
|
||||
"dev_type": null,
|
||||
"scope": null,
|
||||
"package": null,
|
||||
"priority": "P2",
|
||||
"creator": "ybf",
|
||||
"assignee": "ybf",
|
||||
"createdAt": "2026-09-12",
|
||||
"completedAt": "2026-09-13",
|
||||
"branch": "09-12-onetalk-profile-active-sync",
|
||||
"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