Files
jackwener__opencli/scripts/test-site.mjs
stometaverse c370bd0582 feat(xiaohongshu): add 4 creator analytics commands (creator-profile, creator-stats, creator-notes, creator-note-detail) (#49)
* feat(xiaohongshu): add 4 creator analytics commands

Add creator backend support for Xiaohongshu (小红书), enabling
creators to access their analytics data from the command line.

New commands:
- creator-profile: account info (followers, likes, creator level)
- creator-stats: 7-day/30-day overview (views, likes, collects,
  comments, shares, new followers) with daily trend data
- creator-notes: note list with per-note metrics from note manager
- creator-note-detail: single note analytics breakdown
  (organic vs promoted vs video traffic)

API discovery:
- /api/galaxy/creator/home/personal_info (cookie auth, 200 OK)
- /api/galaxy/creator/data/note_detail_new (cookie auth, 200 OK)
- /api/galaxy/creator/data/note_detail?note_id=xxx (cookie auth, 200 OK)
- Note manager DOM extraction for note list (bypasses v2 signature)

All endpoints verified working with real creator account.
Screenshots (redacted) included in docs/screenshots/.

Requires: Chrome logged into creator.xiaohongshu.com

* chore: remove screenshots from repo (will host externally for PR)

* review: fix creator analytics CLI integration

Co-authored-by: stone16 <stone2paul@gmail.com>

* test: add site-scoped test runner

Co-authored-by: stone16 <stone2paul@gmail.com>

* review: ignore publish timestamps in creator note metrics

---------

Co-authored-by: jackwener <jakevingoo@gmail.com>
2026-03-18 22:27:25 +08:00

71 lines
1.7 KiB
JavaScript

#!/usr/bin/env node
import { spawnSync } from 'node:child_process';
import * as fs from 'node:fs';
import * as path from 'node:path';
const site = process.argv[2]?.trim();
if (!site) {
console.error('Usage: npm run test:site -- <site>');
process.exit(1);
}
const repoRoot = path.resolve(new URL('..', import.meta.url).pathname);
const srcDir = path.join(repoRoot, 'src');
function runStep(label, command, args) {
console.log(`\n==> ${label}`);
const result = spawnSync(command, args, {
cwd: repoRoot,
stdio: 'inherit',
env: process.env,
});
if (result.status !== 0) {
process.exit(result.status ?? 1);
}
}
function walk(dir) {
const files = [];
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
const fullPath = path.join(dir, entry.name);
if (entry.isDirectory()) {
files.push(...walk(fullPath));
} else {
files.push(fullPath);
}
}
return files;
}
function toPosix(filePath) {
return filePath.split(path.sep).join('/');
}
function findSiteTests() {
return walk(srcDir)
.filter(filePath => filePath.endsWith('.test.ts'))
.filter(filePath => {
const normalized = toPosix(path.relative(repoRoot, filePath));
return normalized.includes(`/clis/${site}/`) || normalized.includes(`/${site}.test.ts`);
})
.sort();
}
runStep('Typecheck', 'npm', ['run', 'typecheck']);
runStep('Targeted verify', 'npx', ['tsx', 'src/main.ts', 'verify', site]);
const testFiles = findSiteTests();
if (testFiles.length === 0) {
console.log(`\nNo site-specific vitest files found for "${site}". Skipping full vitest run.`);
process.exit(0);
}
runStep(
`Site tests (${site})`,
'npx',
['vitest', 'run', ...testFiles.map(filePath => path.relative(repoRoot, filePath))],
);