star-chart-search-enhancer/docs/superpowers/plans/2026-04-22-market-batch-submit.md

20 KiB

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 <token>.
  • 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:

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:

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:

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:

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:

import { isAuthResponseMessage } from "./auth-messages";
import type { BatchPayload } from "../content/market/batch-payload";

interface FetchResponseLike {
  json(): Promise<unknown>;
  ok: boolean;
  status: number;
}

type FetchLike = (
  input: string,
  init?: RequestInit
) => Promise<FetchResponseLike>;

type SendMessageLike = (message: unknown) => Promise<unknown>;

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<string> {
  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:

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:

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:
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:

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:

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:
getAuthState?: () => Promise<AuthStateValue>;
promptBatchName?: () => string | null;
submitBatch?: (payload: BatchPayload) => Promise<unknown>;
  • 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:

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:

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:
{
  "ok": true,
  "source": "mock-batch-submit",
  "acceptedCount": 1,
  "batchId": "<echoed batch id>"
}
  • 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:

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:
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:
npm test

Expected: PASS

  • Run the production build:
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