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

698 lines
20 KiB
Markdown

# 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:
```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<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:
```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<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:
```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": "<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:
```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