mirror of
https://github.com/sinanyuntu/trade-message-center.git
synced 2026-09-17 13:22:11 +08:00
fix: add scoped rendered-card ledger readers
This commit is contained in:
@@ -0,0 +1,291 @@
|
||||
# OneTalk Rendered Card Readers Implementation Plan
|
||||
|
||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use `subagent-driven-development` (recommended) or `executing-plans` to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||
|
||||
**Goal:** Safely observe and persist normalized order, inquiry, and product card content from OneTalk React Fiber while removing the non-existent `storeImageUrl` concept.
|
||||
|
||||
**Architecture:** Extract a contract-owned canonical product-reference module shared by base-message and rendered-card validation. Split the MAIN-world card reader from its MutationObserver lifecycle, use one strict dispatcher and three bounded typed projections, then forward the unchanged normalized observation through the existing bridge and durable coordinator.
|
||||
|
||||
**Tech Stack:** TypeScript, Node test runner, Chrome MV3 content scripts, React Fiber runtime data, pnpm workspace.
|
||||
|
||||
**Spec:** `.trellis/tasks/09-15-onetalk-rendered-card-readers/{prd.md,design.md,implement.md}`
|
||||
|
||||
## Global Constraints
|
||||
|
||||
- MAIN world may output only whitelisted normalized plain data; raw props, action params, callbacks, trace data, DOM/Fiber references, credentials, query strings, and fragments never cross the bridge.
|
||||
- Identity remains `channelAccountId + conversationId + messageId`; no DOM text, ordering, or CSS inference is permitted.
|
||||
- Unknown card templates fail closed. No generic props crawl or active page interaction is allowed.
|
||||
- Existing bridge, ledger, ACK/conflict state machine, base-message sync, database schema, and Mind events retain their current behavior.
|
||||
- `storeImageUrl` is removed completely. Products retain only `product.imageUrl`.
|
||||
- Before each shared-symbol edit, run GitNexus upstream impact; report HIGH/CRITICAL risk before proceeding.
|
||||
- The accepted prerequisite repair must preserve the IndexedDB index-refactor invariant: rendered-card pending recovery is account-scoped and index-backed, never a restored full-store scan.
|
||||
|
||||
### Task 0: Repair the rendered-card ledger consumer omitted by the IndexedDB index refactor
|
||||
|
||||
**Files:**
|
||||
|
||||
- Modify: `apps/chrome-extension/src/onetalk/service-worker/storage.ts`
|
||||
- Test: `apps/chrome-extension/test/onetalk-sync-storage.test.js`
|
||||
- Test: `apps/chrome-extension/test/onetalk-rendered-card-coordinator.test.js` only if its ledger fake must reflect the required account argument
|
||||
|
||||
**Root cause:** `6be3b839` introduced rendered-card `listPending()` using `readAll`; `61df41d` replaced `readAll` with indexed readers but missed this consumer. This is a prerequisite because it blocks extension typecheck.
|
||||
|
||||
- [ ] Write a failing storage test that calls rendered-card `listPending("account-1")` across records for two accounts and terminal/pending states. It must expect only account-1 `pending_ack` records and assert the ledger object store made no `getAll()` call.
|
||||
- [ ] Extend the existing old-version migration fixture with a v9 rendered-card record; run the focused test and record the failure caused by unresolved `readAll`.
|
||||
- [ ] Increment `ONE_TALK_SYNC_DATABASE_VERSION` to v10. In `onupgradeneeded`, create only `[channelAccountId, status]` and `[channelAccountId, conversationId]` indexes on `onetalk_rendered_card_ledger`, without record rewrite, clear, or backfill.
|
||||
- [ ] Make `listPending(channelAccountId: string)` use `readByIndex(..., [channelAccountId, "pending_ack"])`. It must not accept an omitted account or reintroduce `readAll`.
|
||||
- [ ] Include the rendered-card ledger in `clearConversationHistory`'s existing index-key deletion loop using its account/conversation index. This restores the pre-existing cleanup test and does not change the transaction's scope or status-machine behavior.
|
||||
- [ ] Run focused storage/coordinator tests and `pnpm exec tsc --noEmit -p apps/chrome-extension/tsconfig.json` before accepting the prerequisite. Preserve durable-first, first-content-wins, ACK and reconnect behavior.
|
||||
|
||||
---
|
||||
|
||||
### Task 1: Share canonical product URL validation across content contracts
|
||||
|
||||
**Files:**
|
||||
|
||||
- Create: `packages/onetalk-contract/src/product-url.ts`
|
||||
- Modify: `packages/onetalk-contract/src/content.ts`
|
||||
- Modify: `packages/onetalk-contract/src/index.ts`
|
||||
- Test: `packages/onetalk-contract/test/contract.test.ts`
|
||||
|
||||
**Consumes:** The current `normalizeOneTalkProductUrl` host/path/ID rules in `content.ts`.
|
||||
|
||||
**Produces:**
|
||||
|
||||
```ts
|
||||
export type OneTalkProductReference = { sourceUrl: string; productId: string };
|
||||
export const normalizeOneTalkProductUrl = (value: unknown): OneTalkProductReference | null;
|
||||
export const isOneTalkProductReference = (value: unknown): value is OneTalkProductReference;
|
||||
```
|
||||
|
||||
- [ ] **Step 1: Add failing contract tests for a canonical reference and invalid variants.**
|
||||
|
||||
```ts
|
||||
assert.deepEqual(normalizeOneTalkProductUrl(validProductUrl), {
|
||||
sourceUrl: validProductUrl,
|
||||
productId: "1601456609478",
|
||||
});
|
||||
assert.equal(isOneTalkProductReference({ sourceUrl: validProductUrl, productId: "wrong" }), false);
|
||||
assert.equal(normalizeOneTalkProductUrl(urlWithFragment), null);
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Run the contract test before refactoring.**
|
||||
|
||||
Run: `pnpm --filter @trade-message-center/onetalk-contract test`
|
||||
|
||||
Expected: the new reference-validator assertions fail because no standalone product-reference validator exists.
|
||||
|
||||
- [ ] **Step 3: Move only the current canonical URL parse logic into `product-url.ts`.**
|
||||
|
||||
Keep the current `chinese.alibaba.com/product-detail/...` path rule and query stripping semantics unchanged. Define `isOneTalkProductReference` by validating the exact two keys and checking that `normalizeOneTalkProductUrl(sourceUrl)` returns the same `sourceUrl` and `productId`.
|
||||
|
||||
- [ ] **Step 4: Import the normalizer from `content.ts` and export the new module through `index.ts`.**
|
||||
|
||||
The base `kind: "product"` validation must keep its public behavior. Do not import `content.ts` from `rendered-cards.ts`, because `content.ts` already consumes the rendered-card union.
|
||||
|
||||
- [ ] **Step 5: Re-run the contract suite.**
|
||||
|
||||
Run: `pnpm --filter @trade-message-center/onetalk-contract test`
|
||||
|
||||
Expected: all base-product URL tests and new canonical-reference tests pass.
|
||||
|
||||
### Task 2: Narrow rendered-product JSON to the approved product image and reference
|
||||
|
||||
**Files:**
|
||||
|
||||
- Modify: `packages/onetalk-contract/src/rendered-cards.ts`
|
||||
- Modify: `packages/onetalk-contract/src/index.ts`
|
||||
- Test: `packages/onetalk-contract/test/contract.test.ts`
|
||||
- Test: `apps/chrome-extension/test/onetalk-page-bridge.test.js`
|
||||
- Test: `apps/chrome-extension/test/onetalk-rendered-card-coordinator.test.js`
|
||||
|
||||
**Consumes:** `OneTalkProductReference` and the existing card image/text/quantity validators.
|
||||
|
||||
**Produces:**
|
||||
|
||||
```ts
|
||||
type OneTalkRenderedProductContent = {
|
||||
version: 1;
|
||||
kind: "rendered_product";
|
||||
product: OneTalkRenderedCardImage & OneTalkProductReference;
|
||||
priceDisplay: string;
|
||||
minimumOrder: OneTalkRenderedCardQuantity;
|
||||
serviceBadges: string[];
|
||||
};
|
||||
```
|
||||
|
||||
- [ ] **Step 1: Add failing strict-shape tests.**
|
||||
|
||||
```ts
|
||||
assert.equal(isOneTalkRenderedCardContent(validRenderedProduct), true);
|
||||
assert.equal(isOneTalkRenderedCardContent({ ...validRenderedProduct, storeImageUrl: null }), false);
|
||||
assert.equal(
|
||||
isOneTalkRenderedCardContent({
|
||||
...validRenderedProduct,
|
||||
product: { ...validRenderedProduct.product, sourceUrl: "https://img.alicdn.com/item.jpg" },
|
||||
}),
|
||||
false,
|
||||
);
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Run focused contract tests to establish failure.**
|
||||
|
||||
Run: `pnpm --filter @trade-message-center/onetalk-contract test`
|
||||
|
||||
Expected: the valid canonical product reference is rejected by the current image-host validator, and the old shape still accepts `storeImageUrl`.
|
||||
|
||||
- [ ] **Step 3: Remove `storeImageUrl` from the type and exact-key check.**
|
||||
|
||||
Replace `isOneTalkRenderedCardImageUrl(product.sourceUrl)` with `isOneTalkProductReference({ sourceUrl: product.sourceUrl, productId: product.productId })`, after the full product object has separately passed its exact-key, image, and title validation. This retains the exact two-key product-reference contract without allowing the rendered product's additional fields to bypass it.
|
||||
|
||||
- [ ] **Step 4: Update every rendered-product fixture or assertion.**
|
||||
|
||||
Search the entire workspace for `storeImageUrl` and remove it from valid payloads. Add one negative case at each strict decoder boundary so the removed key cannot silently re-enter through bridge or coordinator fixtures.
|
||||
|
||||
- [ ] **Step 5: Run contract and downstream targeted tests.**
|
||||
|
||||
Run:
|
||||
|
||||
```text
|
||||
pnpm --filter @trade-message-center/onetalk-contract test
|
||||
pnpm --filter @trade-message-center/chrome-extension test -- onetalk-page-bridge onetalk-rendered-card-coordinator
|
||||
```
|
||||
|
||||
Expected: valid product payloads survive cloning/fingerprinting; any `storeImageUrl`, invalid URL, or mismatched product ID is rejected.
|
||||
|
||||
### Task 3: Make the Fiber reader independently testable
|
||||
|
||||
**Files:**
|
||||
|
||||
- Create: `apps/chrome-extension/src/onetalk/main-page/card-observer/react-card-reader.ts`
|
||||
- Modify: `apps/chrome-extension/src/onetalk/main-page/card-observer/entry.ts`
|
||||
- Create: `apps/chrome-extension/test/onetalk-rendered-card-reader.test.js`
|
||||
- Modify: `apps/chrome-extension/test/onetalk-rendered-card-observer.test.js`
|
||||
|
||||
**Consumes:** `OneTalkRenderedCardContent`, `createOneTalkRenderedCardContentFingerprint`, `normalizeOneTalkProductUrl`, `readConversationSelection`, and `OneTalkPageRenderedCardBaseEvidence`.
|
||||
|
||||
**Produces:**
|
||||
|
||||
```ts
|
||||
export type OneTalkRenderedCardRead = {
|
||||
observation: OneTalkRenderedCardObservation;
|
||||
baseEvidence: OneTalkPageRenderedCardBaseEvidence;
|
||||
};
|
||||
export const readOneTalkRenderedCard = (
|
||||
pageWindow: OneTalkPageWindow,
|
||||
wrapper: Element,
|
||||
): OneTalkRenderedCardRead | null;
|
||||
```
|
||||
|
||||
- [ ] **Step 1: Write fixtures that model bounded React Fiber links.**
|
||||
|
||||
The order fixture must use `productName`; inquiry must use `cardType: 6`, `inquiryCardDTO`, `displayProducts`, `inquiryContent`, and `inquiryID`; product must use `msgType: 101`, descendant `cardType: 54`, `productAction.actionParams.url`, and visible benefit/promotion strings. Each fixture must expose a selected `data-cid` matching `conversationCode`.
|
||||
|
||||
- [ ] **Step 2: Add failing reader assertions for all three valid shapes.**
|
||||
|
||||
```ts
|
||||
assert.equal(
|
||||
readOneTalkRenderedCard(page, orderWrapper)?.observation.content.kind,
|
||||
"rendered_order",
|
||||
);
|
||||
assert.equal(
|
||||
readOneTalkRenderedCard(page, inquiryWrapper)?.observation.content.kind,
|
||||
"rendered_inquiry",
|
||||
);
|
||||
assert.equal(
|
||||
readOneTalkRenderedCard(page, productWrapper)?.observation.content.kind,
|
||||
"rendered_product",
|
||||
);
|
||||
```
|
||||
|
||||
Assert the full approved output shape, including inquiry action label order and product badge order. Assert that product output has no `storeImageUrl`, `hsfImg`, action params, trace data, or raw DTO fields.
|
||||
|
||||
- [ ] **Step 3: Add failing rejection cases.**
|
||||
|
||||
Cover `cardType: 12`, unknown 10010 cards, `msgType: 101` without renderer `cardType: 54`, missing template fields, unsafe image/reference URL, incomplete identity, and a selected conversation that changes between before/after reads.
|
||||
|
||||
- [ ] **Step 4: Implement bounded common Fiber and identity helpers.**
|
||||
|
||||
Move `FiberRecord`, `fiberFor`, `itemDataFor`, text/scalar/image guards, the fixed traversal bounds, and before/after conversation checks into `react-card-reader.ts`. Return both observation and base evidence so `entry.ts` does not re-read Fiber state after normalization.
|
||||
|
||||
- [ ] **Step 5: Implement an explicit classifier and three direct template predicates.**
|
||||
|
||||
```ts
|
||||
const classifier = classifyRenderedCard(item, fiber);
|
||||
switch (classifier.kind) {
|
||||
case "order":
|
||||
return readOrderTemplate(classifier.data);
|
||||
case "inquiry":
|
||||
return readInquiryTemplate(classifier.data);
|
||||
case "product":
|
||||
return readProductTemplate(classifier.data);
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
```
|
||||
|
||||
Do not add a generic record walk. Product renderer detection remains bounded to the same Fiber subtree. Deduplicate badges in fixed observed order and only emit nonempty validated text.
|
||||
|
||||
- [ ] **Step 6: Keep `entry.ts` lifecycle-only and run reader tests.**
|
||||
|
||||
`installOneTalkRenderedCardObserver` continues to own WeakSet deduplication, `setTimeout(0)`, root rebinding, `pagehide`, and mutation discovery. It calls the new reader once and forwards paired observation/base evidence.
|
||||
|
||||
Run:
|
||||
|
||||
```text
|
||||
pnpm --filter @trade-message-center/chrome-extension test -- onetalk-rendered-card-reader onetalk-rendered-card-observer
|
||||
```
|
||||
|
||||
Expected: all valid fixtures normalize exactly once; every lookalike produces no observation.
|
||||
|
||||
### Task 4: Verify end-to-end boundaries and finalize the change
|
||||
|
||||
**Files:**
|
||||
|
||||
- Modify only test files shown by the `storeImageUrl` and reader-symbol searches.
|
||||
- Modify task artifacts only if implementation changes a documented invariant.
|
||||
|
||||
**Consumes:** Completed contract and reader tests.
|
||||
|
||||
**Produces:** A reviewed, type-safe, buildable reader change with documented runtime evidence limits.
|
||||
|
||||
- [ ] **Step 1: Run targeted bridge and coordinator regressions.**
|
||||
|
||||
Run:
|
||||
|
||||
```text
|
||||
pnpm --filter @trade-message-center/chrome-extension test -- onetalk-page-bridge onetalk-rendered-card-coordinator onetalk-sync-storage
|
||||
```
|
||||
|
||||
Expected: normalized observations remain durable-first and are sent only after matching base candidate confirmation.
|
||||
|
||||
- [ ] **Step 2: Run workspace static and build validation.**
|
||||
|
||||
Run:
|
||||
|
||||
```text
|
||||
pnpm format:check
|
||||
pnpm typecheck
|
||||
pnpm build
|
||||
git diff --check
|
||||
```
|
||||
|
||||
Expected: no formatting/type/build failure; no server migration or generated artifact change.
|
||||
|
||||
- [ ] **Step 3: Perform GitNexus review and code review.**
|
||||
|
||||
Run `detect_changes()` against the active worktree, inspect all changed symbols and affected flows, and check that no change escapes the contract/MAIN reader/test scope. Review the diff for raw-prop leakage, URL fallback, duplicated validation, hidden success paths, and `storeImageUrl` remnants.
|
||||
|
||||
- [ ] **Step 4: Request permission before user-visible runtime refresh.**
|
||||
|
||||
Only if runtime confirmation needs new code loaded, ask the user to authorize extension reload and reopening the OneTalk tab. After authorization, use Chrome read-only probes to confirm the exact three classifiers/template predicates and the normalized bridge/ledger count. Do not click, navigate, synchronize, or read sensitive card values.
|
||||
|
||||
- [ ] **Step 5: Record validation and commit one cohesive change.**
|
||||
|
||||
Use the repository commit style after all checks pass. Include contract, reader, test, and task documentation changes in one commit; do not include generated extension artifacts.
|
||||
|
||||
## Plan self-review
|
||||
|
||||
- Spec coverage: Tasks 1-2 implement contract URL safety and field removal; Task 3 implements all three strict readers; Task 4 protects downstream semantics and runtime validation.
|
||||
- Placeholder scan: no TODO/TBD steps remain.
|
||||
- Type consistency: Task 1 defines `OneTalkProductReference`; Task 2 consumes it in the rendered product contract; Task 3 consumes the public normalizer and returns one paired observation/evidence value; Task 4 exercises existing bridge/coordinator interfaces without changing them.
|
||||
Reference in New Issue
Block a user