333 lines
9.3 KiB
Markdown
333 lines
9.3 KiB
Markdown
# Market Audience Profile Export 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 selected-creators-only `导出画像CSV` flow that exports current market columns plus detail-page "连接用户" audience profile distributions.
|
|
|
|
**Architecture:** Keep the existing CSV export untouched and add a separate profile export path. The content controller reuses current selection and market row hydration, loads one selected creator profile at a time through a focused detail-page profile client, then writes a separate CSV with structured audience columns.
|
|
|
|
**Tech Stack:** TypeScript, Chrome MV3 content scripts, Xingtu authenticated pages, Vitest, jsdom, tsup
|
|
|
|
---
|
|
|
|
## File Map
|
|
|
|
- Modify: `src/background/auth/controller.ts`
|
|
- Keep token-readable auth state behavior from the previous fix.
|
|
- Modify: `src/background/auth/state.ts`
|
|
- Keep logged-out `lastError` support from the previous fix.
|
|
- Modify: `tests/background-auth-controller.test.ts`
|
|
- Keep token-expired regression coverage.
|
|
- Modify: `src/content/market/auth-gate.ts`
|
|
- Render expired-login text when auth state carries a token-expired error.
|
|
- Modify: `src/content/index.ts`
|
|
- Pass auth failure text into the market auth gate if needed.
|
|
- Modify: `src/content/market/plugin-toolbar.ts`
|
|
- Add a `导出画像CSV` button and handler.
|
|
- Create: `src/content/market/audience-profile-types.ts`
|
|
- Define normalized distribution and export-row types.
|
|
- Create: `src/content/market/audience-profile-client.ts`
|
|
- Load one creator detail page and extract normalized audience profile data.
|
|
- Create: `src/content/market/audience-profile-csv.ts`
|
|
- Build CSV columns from market records plus profile distributions.
|
|
- Modify: `src/content/market/index.ts`
|
|
- Add selected-only profile export flow and serial profile loading.
|
|
- Test: `tests/market-auth-gating.test.ts`
|
|
- Verify expired-login text.
|
|
- Test: `tests/plugin-toolbar.test.ts`
|
|
- Verify new toolbar button wiring.
|
|
- Test: `tests/audience-profile-csv.test.ts`
|
|
- Verify structured CSV column expansion.
|
|
- Test: `tests/audience-profile-client.test.ts`
|
|
- Verify parser behavior against representative detail-page payload/state shapes.
|
|
- Modify: `tests/market-content-entry.test.ts`
|
|
- Verify selected-only export behavior and failure handling.
|
|
|
|
## Task 1: Expired Login Message
|
|
|
|
**Files:**
|
|
- Modify: `src/content/market/auth-gate.ts`
|
|
- Modify: `src/content/index.ts`
|
|
- Test: `tests/market-auth-gating.test.ts`
|
|
|
|
- [ ] **Step 1: Write the failing auth gate test**
|
|
|
|
Add a test where `sendAuthMessage` returns:
|
|
|
|
```ts
|
|
{
|
|
ok: true,
|
|
type: "auth:state",
|
|
value: {
|
|
isAuthenticated: false,
|
|
lastError: "Token 已过期"
|
|
}
|
|
}
|
|
```
|
|
|
|
Assert the page shows `登录已过期,请重新登录`.
|
|
|
|
- [ ] **Step 2: Run the failing test**
|
|
|
|
Run:
|
|
|
|
```bash
|
|
npm test -- tests/market-auth-gating.test.ts
|
|
```
|
|
|
|
Expected: FAIL because the gate only renders `请先登录插件`.
|
|
|
|
- [ ] **Step 3: Implement minimal auth gate text support**
|
|
|
|
Update `renderMarketAuthGate` to accept an optional message string and render it instead of the default title. Update `bootContentScript` to pass `登录已过期,请重新登录` when `lastError` contains `token` or `过期`.
|
|
|
|
- [ ] **Step 4: Verify**
|
|
|
|
Run:
|
|
|
|
```bash
|
|
npm test -- tests/market-auth-gating.test.ts tests/popup-entry.test.ts
|
|
```
|
|
|
|
Expected: PASS.
|
|
|
|
## Task 2: Toolbar Button
|
|
|
|
**Files:**
|
|
- Modify: `src/content/market/plugin-toolbar.ts`
|
|
- Test: `tests/plugin-toolbar.test.ts`
|
|
|
|
- [ ] **Step 1: Write failing toolbar tests**
|
|
|
|
Create tests that:
|
|
|
|
- render the toolbar
|
|
- assert a button with text `导出画像CSV` exists
|
|
- click it and assert `onExportAudienceProfile` was called
|
|
- assert `setToolbarBusyState` disables the new button
|
|
|
|
- [ ] **Step 2: Run the failing tests**
|
|
|
|
Run:
|
|
|
|
```bash
|
|
npm test -- tests/plugin-toolbar.test.ts
|
|
```
|
|
|
|
Expected: FAIL because the button and handler do not exist.
|
|
|
|
- [ ] **Step 3: Implement toolbar support**
|
|
|
|
Add `onExportAudienceProfile` to `PluginToolbarHandlers`, add `audienceProfileExportButton` to `PluginToolbarDom`, render the new button, wire click handling, and include it in busy-state disabling.
|
|
|
|
- [ ] **Step 4: Verify**
|
|
|
|
Run:
|
|
|
|
```bash
|
|
npm test -- tests/plugin-toolbar.test.ts tests/market-content-entry.test.ts
|
|
```
|
|
|
|
Expected: PASS.
|
|
|
|
## Task 3: Profile CSV Builder
|
|
|
|
**Files:**
|
|
- Create: `src/content/market/audience-profile-types.ts`
|
|
- Create: `src/content/market/audience-profile-csv.ts`
|
|
- Test: `tests/audience-profile-csv.test.ts`
|
|
|
|
- [ ] **Step 1: Write failing CSV tests**
|
|
|
|
Define a sample market record and sample profile:
|
|
|
|
```ts
|
|
const profile = {
|
|
status: "success",
|
|
gender: [
|
|
{ label: "男性", value: "40.6%" },
|
|
{ label: "女性", value: "59.4%" }
|
|
],
|
|
age: [{ label: "18-23", value: "28.6%" }],
|
|
province: [{ label: "广东", value: "15%" }],
|
|
regionTop: [{ label: "北京", value: "15%" }],
|
|
cityTier: [{ label: "一线", value: "20%" }],
|
|
interestTop: [{ label: "亲子", value: "18%" }],
|
|
crowd: [{ label: "精致妈妈", value: "12%" }]
|
|
};
|
|
```
|
|
|
|
Assert the CSV contains separate headers like `连接用户-男性占比`, `省份-广东占比`, `地域TOP1名称`, `地域TOP1占比`.
|
|
|
|
- [ ] **Step 2: Run the failing tests**
|
|
|
|
Run:
|
|
|
|
```bash
|
|
npm test -- tests/audience-profile-csv.test.ts
|
|
```
|
|
|
|
Expected: FAIL because the files do not exist.
|
|
|
|
- [ ] **Step 3: Implement CSV builder**
|
|
|
|
Create:
|
|
|
|
- `AudienceProfileDistributionItem`
|
|
- `AudienceProfileResult`
|
|
- `buildAudienceProfileCsv(records, profilesByAuthorId)`
|
|
|
|
Reuse `escapeCsvCell` and existing base/rate/backend metric column conventions. Add `画像抓取状态`.
|
|
|
|
- [ ] **Step 4: Verify**
|
|
|
|
Run:
|
|
|
|
```bash
|
|
npm test -- tests/audience-profile-csv.test.ts tests/csv-exporter.test.ts
|
|
```
|
|
|
|
Expected: PASS.
|
|
|
|
## Task 4: Detail Profile Client and Parser
|
|
|
|
**Files:**
|
|
- Create: `src/content/market/audience-profile-client.ts`
|
|
- Test: `tests/audience-profile-client.test.ts`
|
|
|
|
- [ ] **Step 1: Use a logged-in browser to identify the real data source**
|
|
|
|
Run a Playwright probe against an authenticated `https://xingtu.cn/ad/creator/author-homepage/douyin-video/<authorId>` page. Capture only `/gw/api/...` JSON responses and page Vue/ECharts state. Record representative payload/state samples in the test file as small fixtures.
|
|
|
|
- [ ] **Step 2: Write failing parser tests**
|
|
|
|
Use the captured fixture to assert the parser returns normalized arrays for gender, age, province, region top 10, city tier, interest top 10, and crowd.
|
|
|
|
- [ ] **Step 3: Run the failing tests**
|
|
|
|
Run:
|
|
|
|
```bash
|
|
npm test -- tests/audience-profile-client.test.ts
|
|
```
|
|
|
|
Expected: FAIL because the client/parser does not exist.
|
|
|
|
- [ ] **Step 4: Implement parser and client**
|
|
|
|
Implement a small parser first. Then implement the client with injectable dependencies:
|
|
|
|
```ts
|
|
createAudienceProfileClient({
|
|
fetchDetailPage?: (authorId: string) => Promise<unknown>;
|
|
readProfileFromPage?: (authorId: string) => Promise<unknown>;
|
|
})
|
|
```
|
|
|
|
Prefer parsed API JSON. Fall back to page state when API JSON is unavailable.
|
|
|
|
- [ ] **Step 5: Verify**
|
|
|
|
Run:
|
|
|
|
```bash
|
|
npm test -- tests/audience-profile-client.test.ts
|
|
```
|
|
|
|
Expected: PASS.
|
|
|
|
## Task 5: Controller Export Flow
|
|
|
|
**Files:**
|
|
- Modify: `src/content/market/index.ts`
|
|
- Test: `tests/market-content-entry.test.ts`
|
|
|
|
- [ ] **Step 1: Write failing selected-only export tests**
|
|
|
|
Add tests that:
|
|
|
|
- select one of two visible rows
|
|
- click `导出画像CSV`
|
|
- assert only the selected author profile is requested
|
|
- assert `onCsvReady` receives a CSV containing profile columns
|
|
|
|
- [ ] **Step 2: Write failing no-selection test**
|
|
|
|
Assert clicking `导出画像CSV` with no selected rows sets status to `请先勾选需要导出画像的达人` and makes no profile requests.
|
|
|
|
- [ ] **Step 3: Write failing partial-failure test**
|
|
|
|
Mock two selected profiles where one succeeds and one fails. Assert CSV is still generated with one success row and one `画像抓取状态=失败` row.
|
|
|
|
- [ ] **Step 4: Run failing tests**
|
|
|
|
Run:
|
|
|
|
```bash
|
|
npm test -- tests/market-content-entry.test.ts
|
|
```
|
|
|
|
Expected: FAIL because the controller has no profile export flow.
|
|
|
|
- [ ] **Step 5: Implement controller flow**
|
|
|
|
Add an injected option `loadAudienceProfile?: (record: MarketRecord) => Promise<AudienceProfileResult>`. Add handler:
|
|
|
|
- sync selected state from DOM
|
|
- reject empty selection
|
|
- hydrate current-page selected records
|
|
- load profiles serially
|
|
- cache successful results by author ID
|
|
- build profile CSV
|
|
- call `onCsvReady`
|
|
- update toolbar progress/status
|
|
|
|
- [ ] **Step 6: Verify**
|
|
|
|
Run:
|
|
|
|
```bash
|
|
npm test -- tests/market-content-entry.test.ts tests/audience-profile-csv.test.ts tests/plugin-toolbar.test.ts
|
|
```
|
|
|
|
Expected: PASS.
|
|
|
|
## Task 6: Full Verification
|
|
|
|
**Files:**
|
|
- Modify only if failures identify necessary scoped fixes.
|
|
|
|
- [ ] **Step 1: Run focused suite**
|
|
|
|
Run:
|
|
|
|
```bash
|
|
npm test -- tests/market-auth-gating.test.ts tests/plugin-toolbar.test.ts tests/audience-profile-csv.test.ts tests/audience-profile-client.test.ts tests/market-content-entry.test.ts
|
|
```
|
|
|
|
Expected: PASS.
|
|
|
|
- [ ] **Step 2: Run all tests**
|
|
|
|
Run:
|
|
|
|
```bash
|
|
npm test
|
|
```
|
|
|
|
Expected: PASS.
|
|
|
|
- [ ] **Step 3: Run build**
|
|
|
|
Run:
|
|
|
|
```bash
|
|
npm run build
|
|
```
|
|
|
|
Expected: PASS.
|
|
|
|
- [ ] **Step 4: Manual logged-in browser verification**
|
|
|
|
Load the built extension in Chrome, select one or two market creators, click `导出画像CSV`, and verify the downloaded CSV contains structured profile columns with detail-page data.
|