star-chart-search-enhancer/docs/superpowers/plans/2026-05-18-market-audience-profile-export.md

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.