| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290 |
- #!/usr/bin/env node
- const fs = require('fs');
- const os = require('os');
- const path = require('path');
- const { spawnSync } = require('child_process');
- function parseArgs(argv) {
- const args = {};
- for (let i = 0; i < argv.length; i++) {
- const token = argv[i];
- if (!token.startsWith('--')) continue;
- const eq = token.indexOf('=');
- if (eq >= 0) {
- args[token.slice(2, eq)] = token.slice(eq + 1);
- } else {
- const key = token.slice(2);
- const next = argv[i + 1];
- if (next && !next.startsWith('--')) {
- args[key] = next;
- i++;
- } else {
- args[key] = true;
- }
- }
- }
- return args;
- }
- function usage() {
- return [
- 'Usage:',
- ' node voc-single-platform-report.js --platform <platform> --category <category> --raw-dir <raw-data-dir> [--output <out-dir>]',
- ' node voc-single-platform-report.js --platform xiaohongshu --category <category> --sample-mode true [--output <out-dir>]',
- '',
- 'Inputs:',
- ' --raw-dir Raw platform data directory for normalization',
- ' --normalized-dir Existing normalized VOC directory containing _merged.json and comments-flat.jsonl',
- ' --sample-mode Use packaged Xiaohongshu course sample data when true',
- '',
- 'Outputs:',
- ' normalized/_merged.json',
- ' platform-mini-report/platform-mini-report.md',
- ' platform-mini-report/platform-mini-report.json',
- ' platform-mini-report/platform-mini-report.html',
- ' audit/audit-report.md',
- ' audit/audit-result.json'
- ].join('\n');
- }
- function toBool(value) {
- if (typeof value === 'boolean') return value;
- if (value === undefined || value === null) return false;
- return ['1', 'true', 'yes', 'y'].includes(String(value).toLowerCase());
- }
- function slugify(value) {
- return String(value || 'voc-report')
- .trim()
- .replace(/[\\/:*?"<>|\s]+/g, '-')
- .replace(/-+/g, '-')
- .replace(/^-|-$/g, '') || 'voc-report';
- }
- function parseKeywords(value) {
- if (!value) return [];
- if (Array.isArray(value)) return value;
- const text = String(value).trim();
- if (!text) return [];
- if (text.startsWith('[')) {
- try {
- const parsed = JSON.parse(text);
- return Array.isArray(parsed) ? parsed.map(String).filter(Boolean) : [];
- } catch {
- return [];
- }
- }
- return text.split(/[,,;;|]/).map(item => item.trim()).filter(Boolean);
- }
- function readJson(filePath) {
- return JSON.parse(fs.readFileSync(filePath, 'utf8'));
- }
- function ensureDir(dirPath) {
- fs.mkdirSync(dirPath, { recursive: true });
- }
- function unique(values) {
- return Array.from(new Set(values.filter(Boolean)));
- }
- function existingPath(candidates) {
- return unique(candidates).find(candidate => fs.existsSync(candidate));
- }
- function resolveTool(relativePath) {
- const home = os.homedir();
- const openclawTools = path.join(home, '.openclaw', 'tools');
- const openclawWorkspace = path.join(home, '.openclaw', 'workspace');
- const projectRoot = path.resolve(__dirname, '..', '..');
- const baseName = path.basename(relativePath);
- return existingPath([
- path.isAbsolute(relativePath) ? relativePath : '',
- path.join(process.cwd(), relativePath),
- path.join(projectRoot, relativePath),
- path.join(__dirname, relativePath),
- path.join(__dirname, baseName),
- path.join(openclawTools, relativePath.replace(/^scripts[\\/]tools[\\/]/, '')),
- path.join(openclawTools, relativePath),
- path.join(openclawWorkspace, relativePath)
- ]);
- }
- function resolveSampleDir(platform) {
- const normalizedPlatform = platform === 'xhs' ? 'xiaohongshu' : platform;
- const home = os.homedir();
- return existingPath([
- path.join(process.cwd(), 'demo', normalizedPlatform),
- path.join(path.resolve(__dirname, '..', '..'), 'demo', normalizedPlatform),
- path.join(__dirname, 'course-samples', normalizedPlatform),
- path.join(home, '.openclaw', 'tools', 'course-samples', normalizedPlatform),
- path.join(home, '.openclaw', 'workspace', 'scripts', 'tools', 'course-samples', normalizedPlatform)
- ]);
- }
- function parseLastJson(stdout) {
- const text = String(stdout || '').trim();
- if (!text) return {};
- for (let i = text.lastIndexOf('{'); i >= 0; i = text.lastIndexOf('{', i - 1)) {
- try {
- return JSON.parse(text.slice(i));
- } catch {
- continue;
- }
- }
- return {};
- }
- function runNode(scriptPath, args, label) {
- const child = spawnSync(process.execPath, [scriptPath, ...args], {
- cwd: process.cwd(),
- encoding: 'utf8',
- maxBuffer: 1024 * 1024 * 100
- });
- if (child.stdout) process.stdout.write(child.stdout);
- if (child.stderr) process.stderr.write(child.stderr);
- if (child.error) throw child.error;
- if (child.status !== 0) throw new Error(`${label} failed with exit code ${child.status}`);
- return parseLastJson(child.stdout);
- }
- function requiredTool(relativePath) {
- const resolved = resolveTool(relativePath);
- if (!resolved) throw new Error(`Required tool not found: ${relativePath}`);
- return resolved;
- }
- function main() {
- const args = parseArgs(process.argv.slice(2));
- if (args.help) {
- console.log(usage());
- return;
- }
- const platform = args.platform || 'xiaohongshu';
- const category = args.category || '';
- const project = args.project || 'openclaw-voc-course';
- const outputFormat = args['output-format'] || args.outputFormat || 'both';
- const sampleMode = toBool(args['sample-mode'] ?? args.sampleMode);
- const keywords = parseKeywords(args.keywords);
- const date = args.date || new Date().toISOString().slice(0, 10);
- const owner = args.owner || 'OpenClaw VOC Skills';
- if (!category) throw new Error('Missing required argument: --category');
- if (!['markdown', 'html', 'both'].includes(outputFormat)) throw new Error(`Invalid --output-format: ${outputFormat}`);
- const outputRoot = path.resolve(args.output || path.join(process.cwd(), 'openclaw-voc-output', `${slugify(platform)}-${slugify(category)}-${date}`));
- const normalizedDir = path.resolve(args['normalized-dir'] || args.normalizedDir || path.join(outputRoot, 'normalized'));
- const reportDir = path.join(outputRoot, 'platform-mini-report');
- const auditDir = path.join(outputRoot, 'audit');
- ensureDir(outputRoot);
- const rawDir = args['raw-dir'] || args.rawDir
- ? path.resolve(args['raw-dir'] || args.rawDir)
- : sampleMode
- ? resolveSampleDir(platform)
- : undefined;
- if (!rawDir && !fs.existsSync(path.join(normalizedDir, '_merged.json'))) {
- throw new Error('Provide --raw-dir, --normalized-dir, or --sample-mode true with packaged sample data. The local executable does not perform remote data collection by itself.');
- }
- const normalizer = requiredTool('scripts/tools/voc-data-normalizer.js');
- const miniReport = requiredTool('scripts/tools/platform-mini-report-generator.js');
- const auditor = requiredTool('scripts/tools/voc-report-auditor.js');
- const htmlGenerator = outputFormat === 'html' || outputFormat === 'both'
- ? requiredTool('voc-report-factory/html-v2/gen-report-template.js')
- : undefined;
- const steps = {};
- if (rawDir) {
- steps.normalizer = runNode(normalizer, [
- '--input', rawDir,
- '--output', normalizedDir,
- '--project', project,
- '--category', category
- ], 'voc-data-normalizer');
- }
- steps.miniReport = runNode(miniReport, [
- '--input', normalizedDir,
- '--output', reportDir,
- '--project', project,
- '--category', category,
- '--platform', platform,
- '--owner', owner,
- '--date', date,
- '--business-question', args['business-question'] || args.businessQuestion || '',
- '--keywords', keywords.join(',')
- ], 'platform-mini-report-generator');
- let htmlReportPath;
- if (htmlGenerator) {
- htmlReportPath = path.join(reportDir, 'platform-mini-report.html');
- steps.htmlReport = runNode(htmlGenerator, [
- '--data', path.join(reportDir, 'platform-mini-report.json'),
- '--output', htmlReportPath
- ], 'voc-html-report-generator');
- }
- steps.audit = runNode(auditor, [
- '--report', path.join(reportDir, 'platform-mini-report.md'),
- '--merged', path.join(normalizedDir, '_merged.json'),
- '--comments', path.join(normalizedDir, 'comments-flat.jsonl'),
- '--output', auditDir
- ], 'voc-report-auditor');
- const merged = readJson(path.join(normalizedDir, '_merged.json'));
- const report = readJson(path.join(reportDir, 'platform-mini-report.json'));
- const audit = readJson(path.join(auditDir, 'audit-result.json'));
- const result = {
- status: audit.status === 'fail' ? 'needs_review' : 'ok',
- metadata: {
- project,
- platform,
- category,
- businessQuestion: args['business-question'] || args.businessQuestion || '',
- keywords,
- generatedAt: new Date().toISOString()
- },
- summary: {
- itemCount: merged.metadata?.itemCount || 0,
- commentCount: merged.metadata?.validVocCount || report.stats?.commentCount || 0,
- keywordCount: keywords.length || merged.metadata?.keywords?.length || 0,
- evidenceCardCount: report.audit?.evidenceCardCount || 0,
- warningCount: (report.audit?.warningCount || 0) + (audit.summary?.warn || 0),
- auditStatus: audit.status
- },
- outputs: {
- outputRoot,
- normalized: normalizedDir,
- markdownReport: path.join(reportDir, 'platform-mini-report.md'),
- jsonReport: path.join(reportDir, 'platform-mini-report.json'),
- htmlReport: htmlReportPath,
- auditReport: path.join(auditDir, 'audit-report.md'),
- auditResult: path.join(auditDir, 'audit-result.json')
- },
- steps,
- nextActions: [
- '复核 audit-report.md 中的 warn/fail 项',
- '补充真实采集数据后复跑同一链路',
- '将 P0 机会点转成内容或产品验证动作'
- ]
- };
- console.log(`SINGLE_PLATFORM_REPORT_RESULT=${JSON.stringify(result)}`);
- }
- try {
- main();
- } catch (error) {
- const result = {
- status: 'error',
- message: error.message,
- generatedAt: new Date().toISOString()
- };
- console.error(error.message);
- console.log(`SINGLE_PLATFORM_REPORT_RESULT=${JSON.stringify(result)}`);
- process.exit(1);
- }
|