Files
trade-message-center/docs/superpowers/plans/2026-09-16-onetalk-dom-secondary-card-collection.md
T
YBF 784bbec2a0 feat: 支持 OneTalk 文本引用回复协议 (#66)
* feat: add OneTalk quote reply support

* chore(task): archive 09-17-onetalk-quote-reply-protocol

* chore: record journal

* docs: format OneTalk DOM card collection plan

* chore: release 0.8.29

* chore: release 0.8.30
2026-09-17 11:56:35 +08:00

9.2 KiB

OneTalk DOM Secondary Card Collection Implementation Plan

For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (- [ ]) syntax for tracking.

Goal: Replace the Fiber-backed secondary card producer with strict DOM readers for inquiry, product, and order cards, while removing four obsolete server audit columns.

Architecture: A single DOM identity resolver validates card and wrapper metadata before three typed readers produce the existing rendered-card contract. The bridge and local ledger cease carrying unused Fiber-only base evidence; the server wire and JSON content shapes remain unchanged. The migration drops only approved columns and preserves first-content-wins with conflict_count.

Tech Stack: TypeScript, MV3 MAIN/ISOLATED page bridge, IndexedDB, shared OneTalk contract, Fastify/Drizzle/PostgreSQL, Node test runner.

Spec: .trellis/tasks/09-16-onetalk-dom-secondary-card-collection/{prd,design,implement}.md

Global Constraints

  • DOM is a structural data source; no React/Fiber inspection, root traversal, timer rescan, raw metadata forwarding, or fallback path.
  • messageId must be present and match in card metadata and closest wrapper metadata whenever both exist; metadata conversation IDs must also agree when both exist.
  • Quote (cardType=8) and unknown cards fail closed.
  • Keep existing three rendered content JSON shapes, server frame, compound message identity, fingerprint, observedAtMs and conflict_count.
  • Migration never issues DELETE, TRUNCATE, table rebuild or historical JSON rewrite.

Task 1: DOM reader and legacy reader removal

Files:

  • Create: apps/chrome-extension/src/onetalk/main-page/card-observer/dom-card-reader.ts
  • Delete: 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
  • Test: apps/chrome-extension/test/onetalk-rendered-card-reader.test.js

Interfaces:

  • Produces: readOneTalkRenderedCardFromDom(pageWindow, card): OneTalkRenderedCardObservation | null.

  • Consumes: readConversationSelection, normalizeOneTalkProductUrl, rendered-card validators and fingerprint creator.

  • Step 1: Replace Fiber fixtures with DOM fixtures and red tests.

assert.equal(
    readOneTalkRenderedCardFromDom(
        page,
        cardWith({ cardMessageId: "m-1", wrapperMessageId: "m-2" }),
    ),
    null,
);
assert.equal(readOneTalkRenderedCardFromDom(page, quoteCard({ cardType: 8 })), null);
assert.equal(
    readOneTalkRenderedCardFromDom(page, productCard({ rawUrl: "https://bad.example" })),
    null,
);
  • Step 2: Run the focused reader test and confirm the missing DOM reader fails.
node --experimental-strip-types --test apps/chrome-extension/test/onetalk-rendered-card-reader.test.js
  • Step 3: Implement the shared resolver and three projections.
const messageId = cardMessageId ?? wrapperMessageId;
if (!messageId || (cardMessageId && wrapperMessageId && cardMessageId !== wrapperMessageId))
    return null;
if (!sameOptionalConversation(cardInfo, wrapperInfo)) return null;

Construct only exact rendered_inquiry, rendered_product, or rendered_order shapes, validate with isOneTalkRenderedCardContent, and return an observation whose fingerprint is recomputed from that content.

  • Step 4: Rewrite entry to scan cards and react only to DOM mutations.
for (const card of cardsForMutation(record))
    publish(readOneTalkRenderedCardFromDom(pageWindow, card));
observer.observe(document.documentElement, {
    childList: true,
    subtree: true,
    attributes: true,
    attributeFilter: ["data-expinfo", "src", "style"],
});

Remove setInterval, wrapper rescans, React imports, and the deleted reader file.

  • Step 5: Run focused reader/observer tests.
node --experimental-strip-types --test apps/chrome-extension/test/onetalk-rendered-card-reader.test.js apps/chrome-extension/test/onetalk-rendered-card-observer.test.js

Task 2: Remove Fiber-only base evidence from local boundaries

Files:

  • Modify: apps/chrome-extension/src/onetalk/page-bridge/{main.ts,model.ts}
  • Modify: apps/chrome-extension/src/onetalk/service-worker/{rendered-card-coordinator.ts,storage.ts}
  • Test: apps/chrome-extension/test/{onetalk-page-bridge,onetalk-rendered-card-coordinator,onetalk-sync-storage}.test.js

Interfaces:

  • onetalk.page.rendered-card-observed contains { channelAccountId, observations } only.

  • OneTalkRenderedCardCoordinator.observe(observations) persists and flushes observations without base evidence.

  • Step 1: Add bridge and ledger tests that accept an observation-only message and reject legacy base-evidence fields.

assert.deepEqual(
    decodeOneTalkPageMessage({ source, version, type, channelAccountId, observations }),
    expected,
);
assert.equal("baseDirection" in pendingRecord, false);
  • Step 2: Run focused tests and confirm they fail against the old exact shape.
node --experimental-strip-types --test apps/chrome-extension/test/onetalk-page-bridge.test.js apps/chrome-extension/test/onetalk-rendered-card-coordinator.test.js apps/chrome-extension/test/onetalk-sync-storage.test.js
  • Step 3: Remove baseEvidence from the page message decoder/sink and from coordinator/ledger APIs.
return coordinator.observe(message.observations);
await ledger.observe({ channelAccountId: scope.channelAccountId, observation });

Keep markSent, listPending, markAcknowledged, fingerprints and exact ACK matching unchanged.

  • Step 4: Re-run focused tests.
node --experimental-strip-types --test apps/chrome-extension/test/onetalk-page-bridge.test.js apps/chrome-extension/test/onetalk-rendered-card-coordinator.test.js apps/chrome-extension/test/onetalk-sync-storage.test.js

Task 3: Remove approved server audit columns without data cleanup

Files:

  • Modify: apps/server/src/database/schema/onetalk.ts
  • Modify: apps/server/src/onetalk/rendered-card-{model,repository,service}.ts
  • Create: generated apps/server/drizzle/0015_*.sql and matching drizzle/meta files
  • Test: apps/server/test/onetalk-rendered-card-{migration,repository,flow}.test.ts

Interfaces:

  • Stored row retains key, renderedCardContent, renderedCardContentFingerprint, renderedCardObservedAtMs, and conflictCount.

  • Same content stays duplicate; different content increments conflictCount and returns conflict without overwriting JSON.

  • Step 1: Change migration/repository test expectations to the retained row shape and conflict-only audit.

assert.equal(saved.conflictCount, 1);
assert.equal("lastObservedAt" in saved, false);
assert.doesNotMatch(migrationSql, /\b(?:DELETE|TRUNCATE)\b/i);
  • Step 2: Run the server rendered-card tests and confirm failure.
node --experimental-strip-types --test apps/server/test/onetalk-rendered-card-migration.test.ts apps/server/test/onetalk-rendered-card-repository.test.ts apps/server/test/onetalk-rendered-card-flow.test.ts
  • Step 3: Remove four Drizzle fields and generated SQL columns, then remove their repository/service writes.
ALTER TABLE "onetalk_rendered_card_content" DROP COLUMN "first_confirmed_at";
ALTER TABLE "onetalk_rendered_card_content" DROP COLUMN "last_observed_at";
ALTER TABLE "onetalk_rendered_card_content" DROP COLUMN "last_conflicting_fingerprint";
ALTER TABLE "onetalk_rendered_card_content" DROP COLUMN "last_conflict_observed_at_ms";

Retain conflict_count = conflict_count + 1 in the conflict branch and leave all supplement JSON untouched.

  • Step 4: Run migration/repository/flow tests and Drizzle validation.
node --experimental-strip-types --test apps/server/test/onetalk-rendered-card-migration.test.ts apps/server/test/onetalk-rendered-card-repository.test.ts apps/server/test/onetalk-rendered-card-flow.test.ts
pnpm --filter @trade-message-center/server db:check

Task 4: Contract-preserving integration verification

Files:

  • Test: existing contract, read-projection, extension and server suites

  • Step 1: Add read projection regressions for all three base kinds and reject mismatched supplement kinds.

assert.equal(projected.content.kind, "inquiry");
assert.throws(() => toOneTalkCenterMessage(product, renderedInquiry));
  • Step 2: Run all scoped checks.
pnpm --filter @trade-message-center/onetalk-contract test
pnpm --filter @trade-message-center/chrome-extension test
pnpm --filter @trade-message-center/chrome-extension typecheck
pnpm --filter @trade-message-center/chrome-extension build
pnpm --filter @trade-message-center/server typecheck
git diff --check
  • Step 3: Run final source-boundary searches.
rg -n 'React|Fiber|baseEvidence|baseDirection|baseSentAtMs|setInterval' apps/chrome-extension/src/onetalk/main-page/card-observer apps/chrome-extension/src/onetalk/page-bridge apps/chrome-extension/src/onetalk/service-worker

Expected: no legacy secondary-card collector references; unrelated modules may remain outside the reviewed paths.