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.tssrc/shared/batch-submit-client.tstests/batch-payload.test.tstests/batch-submit-client.test.ts
Existing files to modify
src/content/market/plugin-toolbar.tssrc/content/market/index.tssrc/shared/auth-messages.tssrc/background/auth/state.tssrc/popup/view.tsscripts/mock-protected-api.mjstests/market-content-entry.test.tstests/popup-entry.test.tstests/mock-protected-api.test.tsREADME.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 withAuthorization: Bearer <token>.src/shared/auth-messages.tsandsrc/background/auth/state.ts: expose stable access to Logto usersuband display name already present in auth state.scripts/mock-protected-api.mjs: add a local/api/mock/batchesendpoint 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
PluginToolbarHandlerswithonSubmitBatch() - extend
PluginToolbarDomwithbatchSubmitButton: 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()andsetToolbarBusyState() -
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
getAuthStateto achrome.runtime.sendMessage({ type: "auth:get-state" })wrapper -
default
promptBatchNameto() => window.prompt("请输入批次名称") -
default
submitBatchtocreateBatchSubmitClient(...).submitBatch -
add
onSubmitBatchhandler inensurePluginToolbar(...) -
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
- read target via
-
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.subuserInfo.nameresource
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.tsonly 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/batchesforPOST - 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:
- Start
npm run mock:protected-api - Reload the unpacked extension from
dist/ - Log in through the popup
- Open the Xingtu market page
- Choose an export range
- Click
提交批次 - Enter a batch name in
prompt() - Confirm success text appears
- Verify the mock response contains the batch id and accepted creator count