# Market Batch Submit 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:** Add a standalone `提交批次` toolbar action that collects the currently selected export range of Xingtu creators, prompts for a batch name, builds a batch payload with Logto user identity plus creator IDs, and submits the batch to a protected API endpoint with a Bearer token. **Architecture:** Reuse the existing export-range collection path so multi-page batch submission behaves exactly like CSV export. Keep responsibilities split: toolbar UI emits a submit intent, a batch-payload module assembles the request body, and a protected batch-submit client owns token injection plus POST behavior. For first-pass verification, extend the local mock server with a batch endpoint that echoes the submitted payload. **Tech Stack:** Chrome MV3, TypeScript, Vitest, tsup, Node HTTP server, `@logto/chrome-extension` --- ## File Map ### New files - `src/content/market/batch-payload.ts` - `src/shared/batch-submit-client.ts` - `tests/batch-payload.test.ts` - `tests/batch-submit-client.test.ts` ### Existing files to modify - `src/content/market/plugin-toolbar.ts` - `src/content/market/index.ts` - `src/shared/auth-messages.ts` - `src/background/auth/state.ts` - `src/popup/view.ts` - `scripts/mock-protected-api.mjs` - `tests/market-content-entry.test.ts` - `tests/popup-entry.test.ts` - `tests/mock-protected-api.test.ts` - `README.md` ### Responsibilities - `src/content/market/plugin-toolbar.ts`: add a dedicated submit button and include it in busy-state handling. - `src/content/market/index.ts`: orchestrate prompt input, range collection reuse, payload creation, and batch submission. - `src/content/market/batch-payload.ts`: create the batch payload from batch name, timestamp, auth state, and market records. - `src/shared/batch-submit-client.ts`: POST JSON to the protected batch endpoint with `Authorization: Bearer `. - `src/shared/auth-messages.ts` and `src/background/auth/state.ts`: expose stable access to Logto user `sub` and display name already present in auth state. - `scripts/mock-protected-api.mjs`: add a local `/api/mock/batches` endpoint that validates Bearer auth and echoes the payload. ## Task 1: Add The Batch Payload Builder **Files:** - Create: `src/content/market/batch-payload.ts` - Create: `tests/batch-payload.test.ts` - [ ] **Step 1: Write the failing batch payload tests** Create `tests/batch-payload.test.ts` with cases for: ```ts import { describe, expect, test } from "vitest"; import { createBatchPayload } from "../src/content/market/batch-payload"; describe("batch-payload", () => { test("builds a batch id from the batch name and timestamp", () => { const payload = createBatchPayload({ authState: { isAuthenticated: true, resource: "https://talent-search.intelligrow.cn", userInfo: { name: "王少卿", sub: "p7pdhhtde8kj" } }, batchName: "618达人筛选第一批", createdAt: "2026-04-22T12:30:00.000Z", records: [ { authorId: "111", authorName: "达人A", status: "success" }, { authorId: "222", authorName: "达人B", status: "success" } ] }); expect(payload).toEqual({ authors: [ { authorId: "111", authorName: "达人A" }, { authorId: "222", authorName: "达人B" } ], batchId: "618达人筛选第一批-2026-04-22T12:30:00.000Z", batchName: "618达人筛选第一批", createdAt: "2026-04-22T12:30:00.000Z", creatorName: "王少卿", logtoUserId: "p7pdhhtde8kj", resource: "https://talent-search.intelligrow.cn" }); }); test("throws when the user id is unavailable", () => { expect(() => createBatchPayload({ authState: { isAuthenticated: true, resource: "https://talent-search.intelligrow.cn", userInfo: { name: "王少卿" } }, batchName: "批次A", createdAt: "2026-04-22T12:30:00.000Z", records: [{ authorId: "111", authorName: "达人A", status: "success" }] }) ).toThrow(/user/i); }); }); ``` - [ ] **Step 2: Run the batch payload test to verify red** Run: `npm test -- tests/batch-payload.test.ts` Expected: FAIL because `createBatchPayload` does not exist yet - [ ] **Step 3: Implement the minimal batch payload builder** Create `src/content/market/batch-payload.ts` with: ```ts import type { AuthStateValue } from "../../shared/auth-messages"; import type { MarketRecord } from "./types"; export interface BatchPayload { authors: Array<{ authorId: string; authorName: string; }>; batchId: string; batchName: string; createdAt: string; creatorName: string; logtoUserId: string; resource: string; } export function createBatchPayload(options: { authState: AuthStateValue; batchName: string; createdAt: string; records: MarketRecord[]; }): BatchPayload { const logtoUserId = options.authState.userInfo?.sub?.trim(); if (!logtoUserId) { throw new Error("batch submit user id unavailable"); } const resource = options.authState.resource?.trim(); if (!resource) { throw new Error("batch submit resource unavailable"); } const batchName = options.batchName.trim(); if (!batchName) { throw new Error("batch submit batch name is required"); } return { authors: options.records.map((record) => ({ authorId: record.authorId, authorName: record.authorName })), batchId: `${batchName}-${options.createdAt}`, batchName, createdAt: options.createdAt, creatorName: options.authState.userInfo?.name ?? options.authState.userInfo?.username ?? logtoUserId, logtoUserId, resource }; } ``` - [ ] **Step 4: Run the batch payload test to verify green** Run: `npm test -- tests/batch-payload.test.ts` Expected: PASS - [ ] **Step 5: Commit the payload builder slice** Run: ```bash git add src/content/market/batch-payload.ts tests/batch-payload.test.ts git commit -m "feat: add batch payload builder" ``` ## Task 2: Build The Protected Batch Submit Client **Files:** - Create: `src/shared/batch-submit-client.ts` - Create: `tests/batch-submit-client.test.ts` - [ ] **Step 1: Write the failing batch submit client tests** Create `tests/batch-submit-client.test.ts` with: ```ts import { describe, expect, test, vi } from "vitest"; import { createBatchSubmitClient } from "../src/shared/batch-submit-client"; describe("batch-submit-client", () => { test("posts the batch payload with a Bearer token", async () => { const sendMessage = vi.fn(async () => ({ ok: true, type: "auth:token", value: { accessToken: "abc123" } })); const fetchImpl = vi.fn(async () => ({ ok: true, status: 200, json: async () => ({ ok: true, acceptedCount: 2 }) })); const client = createBatchSubmitClient({ baseUrl: "http://127.0.0.1:4319", fetchImpl, sendMessage }); await client.submitBatch({ authors: [{ authorId: "111", authorName: "达人A" }], batchId: "批次A-2026-04-22T12:30:00.000Z", batchName: "批次A", createdAt: "2026-04-22T12:30:00.000Z", creatorName: "王少卿", logtoUserId: "p7pdhhtde8kj", resource: "https://talent-search.intelligrow.cn" }); expect(fetchImpl).toHaveBeenCalledWith( "http://127.0.0.1:4319/api/mock/batches", expect.objectContaining({ body: JSON.stringify( expect.objectContaining({ batchId: "批次A-2026-04-22T12:30:00.000Z" }) ), headers: expect.objectContaining({ Authorization: "Bearer abc123", "Content-Type": "application/json" }), method: "POST" }) ); }); test("throws on unauthorized responses", async () => { const client = createBatchSubmitClient({ baseUrl: "http://127.0.0.1:4319", fetchImpl: vi.fn(async () => ({ ok: false, status: 401, json: async () => ({ ok: false, error: "unauthorized" }) })), sendMessage: vi.fn(async () => ({ ok: true, type: "auth:token", value: { accessToken: "abc123" } })) }); await expect( client.submitBatch({ authors: [], batchId: "批次A-2026-04-22T12:30:00.000Z", batchName: "批次A", createdAt: "2026-04-22T12:30:00.000Z", creatorName: "王少卿", logtoUserId: "p7pdhhtde8kj", resource: "https://talent-search.intelligrow.cn" }) ).rejects.toThrow(/unauthorized/i); }); }); ``` - [ ] **Step 2: Run the batch submit client test to verify red** Run: `npm test -- tests/batch-submit-client.test.ts` Expected: FAIL because `createBatchSubmitClient` does not exist yet - [ ] **Step 3: Implement the minimal batch submit client** Create `src/shared/batch-submit-client.ts` with: ```ts import { isAuthResponseMessage } from "./auth-messages"; import type { BatchPayload } from "../content/market/batch-payload"; interface FetchResponseLike { json(): Promise; ok: boolean; status: number; } type FetchLike = ( input: string, init?: RequestInit ) => Promise; type SendMessageLike = (message: unknown) => Promise; export function createBatchSubmitClient(options: { baseUrl: string; fetchImpl?: FetchLike; sendMessage: SendMessageLike; }) { const fetchImpl = options.fetchImpl ?? fetch; return { async submitBatch(payload: BatchPayload) { const token = await readAccessToken(options.sendMessage); const response = await fetchImpl( new URL("/api/mock/batches", options.baseUrl).toString(), { body: JSON.stringify(payload), headers: { Authorization: `Bearer ${token}`, "Content-Type": "application/json" }, method: "POST" } ); if (response.status === 401 || response.status === 403) { throw new Error("batch submit unauthorized"); } if (!response.ok) { throw new Error(`batch submit failed: ${response.status}`); } return response.json(); } }; } async function readAccessToken(sendMessage: SendMessageLike): Promise { const response = await sendMessage({ type: "auth:get-access-token" }); if ( !isAuthResponseMessage(response) || !response.ok || response.type !== "auth:token" || !response.value.accessToken.trim() ) { throw new Error("batch submit token unavailable"); } return response.value.accessToken; } ``` - [ ] **Step 4: Run the batch submit client test to verify green** Run: `npm test -- tests/batch-submit-client.test.ts` Expected: PASS - [ ] **Step 5: Commit the batch submit client slice** Run: ```bash git add src/shared/batch-submit-client.ts tests/batch-submit-client.test.ts git commit -m "feat: add batch submit client" ``` ## Task 3: Extend The Toolbar With A Batch Submit Action **Files:** - Modify: `src/content/market/plugin-toolbar.ts` - Modify: `tests/market-content-entry.test.ts` - [ ] **Step 1: Write the failing toolbar tests** Add tests to `tests/market-content-entry.test.ts` for: ```ts test("renders a batch submit button in the toolbar", async () => { document.body.innerHTML = buildMarketFixture(); const { createMarketController } = await import("../src/content/market/index"); const controller = createMarketController({ document, loadAuthorMetrics: async () => ({ success: true, rates: { personalVideoAfterSearchRate: "0.01% - 0.1%", singleVideoAfterSearchRate: "0.01% - 0.1%" } }), window }); await controller.ready; expect(document.querySelector('[data-plugin-batch-submit="button"]')).not.toBeNull(); }); ``` Also add a test that `setToolbarBusyState()` disables the batch submit button. - [ ] **Step 2: Run the focused market toolbar tests to verify red** Run: `npm test -- tests/market-content-entry.test.ts` Expected: FAIL because the batch submit button does not exist yet - [ ] **Step 3: Add the minimal toolbar button** Update `src/content/market/plugin-toolbar.ts`: - extend `PluginToolbarHandlers` with `onSubmitBatch()` - extend `PluginToolbarDom` with `batchSubmitButton: HTMLButtonElement` - create a new button: ```ts const batchSubmitButton = document.createElement("button"); batchSubmitButton.type = "button"; batchSubmitButton.dataset.pluginBatchSubmit = "button"; batchSubmitButton.textContent = "提交批次"; ``` - append it next to `exportButton` - wire its click handler to `handlers.onSubmitBatch()` - include it in `readToolbarDom()` and `setToolbarBusyState()` - [ ] **Step 4: Re-run the focused market toolbar tests to verify green** Run: `npm test -- tests/market-content-entry.test.ts` Expected: PASS - [ ] **Step 5: Commit the toolbar slice** Run: ```bash git add src/content/market/plugin-toolbar.ts tests/market-content-entry.test.ts git commit -m "feat: add batch submit toolbar action" ``` ## Task 4: Wire Batch Submission Into The Market Controller **Files:** - Modify: `src/content/market/index.ts` - Modify: `src/shared/auth-messages.ts` - Modify: `src/background/auth/state.ts` - Modify: `tests/market-content-entry.test.ts` - [ ] **Step 1: Write the failing controller tests** Add tests to `tests/market-content-entry.test.ts` for: ```ts test("prompts for a batch name before submitting the current range", async () => { document.body.innerHTML = buildMarketFixture(); const prompt = vi.fn(() => "618达人筛选第一批"); const submitBatch = vi.fn(async () => ({ ok: true })); const { createMarketController } = await import("../src/content/market/index"); const controller = createMarketController({ document, getAuthState: async () => ({ isAuthenticated: true, resource: "https://talent-search.intelligrow.cn", userInfo: { name: "王少卿", sub: "p7pdhhtde8kj" } }), loadAuthorMetrics: async () => ({ success: true, rates: { personalVideoAfterSearchRate: "0.01% - 0.1%", singleVideoAfterSearchRate: "0.01% - 0.1%" } }), promptBatchName: prompt, submitBatch, window }); await controller.ready; (document.querySelector('[data-plugin-batch-submit="button"]') as HTMLButtonElement).click(); await Promise.resolve(); expect(prompt).toHaveBeenCalled(); expect(submitBatch).toHaveBeenCalledWith( expect.objectContaining({ batchName: "618达人筛选第一批", logtoUserId: "p7pdhhtde8kj" }) ); }); test("shows an error when the batch name is blank", async () => { // same setup, but prompt returns " " // expect toolbar status to contain "请输入批次名称" }); test("does nothing when the prompt is cancelled", async () => { // prompt returns null // submitBatch is not called }); ``` - [ ] **Step 2: Run the focused market controller tests to verify red** Run: `npm test -- tests/market-content-entry.test.ts` Expected: FAIL because batch submission wiring does not exist yet - [ ] **Step 3: Add the minimal market controller wiring** Update `src/content/market/index.ts`: - add optional dependencies: ```ts getAuthState?: () => Promise; promptBatchName?: () => string | null; submitBatch?: (payload: BatchPayload) => Promise; ``` - default `getAuthState` to a `chrome.runtime.sendMessage({ type: "auth:get-state" })` wrapper - default `promptBatchName` to `() => window.prompt("请输入批次名称")` - default `submitBatch` to `createBatchSubmitClient(...).submitBatch` - add `onSubmitBatch` handler in `ensurePluginToolbar(...)` - behavior: - read target via `readToolbarExportTarget()` - prompt for batch name - cancel on `null` - reject blank names with `setToolbarExportStatus(toolbar, "请输入批次名称")` - collect records via existing `exportRecords(target)` - read auth state - build payload with `createBatchPayload(...)` - call `submitBatch(payload)` - show `批次提交成功` on success - show thrown message or `批次提交失败,请稍后重试` on failure - [ ] **Step 4: Extend auth state only if the tests expose a gap** If tests reveal missing fields, ensure `src/background/auth/state.ts` continues exposing: - `userInfo.sub` - `userInfo.name` - `resource` If `src/shared/auth-messages.ts` needs typing support, update only types, not message kinds. - [ ] **Step 5: Re-run the focused market controller tests to verify green** Run: `npm test -- tests/market-content-entry.test.ts` Expected: PASS - [ ] **Step 6: Commit the market controller slice** Run: ```bash git add src/content/market/index.ts src/shared/auth-messages.ts src/background/auth/state.ts tests/market-content-entry.test.ts git commit -m "feat: wire market batch submission flow" ``` ## Task 5: Extend The Mock Server And Popup Debugging **Files:** - Modify: `scripts/mock-protected-api.mjs` - Modify: `tests/mock-protected-api.test.ts` - Modify: `src/popup/view.ts` - Modify: `tests/popup-entry.test.ts` only if needed for debug output wording - Modify: `README.md` - [ ] **Step 1: Write the failing mock batch endpoint tests** Add cases to `tests/mock-protected-api.test.ts`: ```ts test("accepts a batch payload when a Bearer token is present", async () => { const server = createMockProtectedApiServer({ port: 0 }); await server.start(); const response = await fetch(`${server.baseUrl}/api/mock/batches`, { body: JSON.stringify({ authors: [{ authorId: "111", authorName: "达人A" }], batchId: "批次A-2026-04-22T12:30:00.000Z", batchName: "批次A", createdAt: "2026-04-22T12:30:00.000Z", creatorName: "王少卿", logtoUserId: "p7pdhhtde8kj", resource: "https://talent-search.intelligrow.cn" }), headers: { Authorization: "Bearer abc123", "Content-Type": "application/json" }, method: "POST" }); expect(response.status).toBe(200); await expect(response.json()).resolves.toEqual( expect.objectContaining({ acceptedCount: 1, ok: true, source: "mock-batch-submit" }) ); }); ``` - [ ] **Step 2: Run the mock server tests to verify red** Run: `npm test -- tests/mock-protected-api.test.ts` Expected: FAIL because the batch endpoint does not exist yet - [ ] **Step 3: Implement the minimal mock batch endpoint** Update `scripts/mock-protected-api.mjs`: - keep the existing `/api/mock/protected` - add `/api/mock/batches` for `POST` - require the same Bearer header validation - read the JSON body and return: ```json { "ok": true, "source": "mock-batch-submit", "acceptedCount": 1, "batchId": "" } ``` - [ ] **Step 4: Update the docs** Add to `README.md`: - how to trigger `提交批次` - how the prompt should be filled - expected mock response - [ ] **Step 5: Re-run the mock server tests to verify green** Run: `npm test -- tests/mock-protected-api.test.ts` Expected: PASS - [ ] **Step 6: Commit the mock batch slice** Run: ```bash git add scripts/mock-protected-api.mjs tests/mock-protected-api.test.ts README.md git commit -m "feat: add mock batch submit endpoint" ``` ## Final Verification - [ ] Run the focused batch feature tests: ```bash npm test -- tests/auth-config.test.ts tests/batch-payload.test.ts tests/batch-submit-client.test.ts tests/mock-protected-api.test.ts tests/market-content-entry.test.ts ``` Expected: PASS - [ ] Run the full test suite: ```bash npm test ``` Expected: PASS - [ ] Run the production build: ```bash npm run build ``` Expected: build completes and updates `dist/` - [ ] Perform the manual smoke test: 1. Start `npm run mock:protected-api` 2. Reload the unpacked extension from `dist/` 3. Log in through the popup 4. Open the Xingtu market page 5. Choose an export range 6. Click `提交批次` 7. Enter a batch name in `prompt()` 8. Confirm success text appears 9. Verify the mock response contains the batch id and accepted creator count