| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263 |
- #!/usr/bin/env node
- const fs = require('fs');
- const path = require('path');
- const {
- buildReport,
- keywordMatrixFromProfile,
- parseList
- } = require('./industry-trend-report');
- const { collectXiaohongshuDataset } = require('./xiaohongshu-trend-collector');
- const DEFAULT_RESULT_PREFIX = 'INDUSTRY_TREND_RUNNER_RESULT';
- 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 ensureDir(dirPath) {
- fs.mkdirSync(dirPath, { recursive: true });
- }
- function readJson(filePath) {
- return JSON.parse(fs.readFileSync(filePath, 'utf8'));
- }
- function writeJson(filePath, data) {
- ensureDir(path.dirname(filePath));
- fs.writeFileSync(filePath, JSON.stringify(data, null, 2), 'utf8');
- }
- function toBool(value, fallback = false) {
- if (typeof value === 'boolean') return value;
- if (value === undefined || value === null || value === '') return fallback;
- return ['1', 'true', 'yes', 'y', 'on'].includes(String(value).toLowerCase());
- }
- function cleanPreferenceItems(value) {
- return parseList(value)
- .map(item => item
- .replace(/^(这些|这类|这种|方向是|方向:|方向:)/, '')
- .replace(/(这些方向|这些内容|这类方向|这类内容|这种方向|这种内容|方向|内容)$/g, '')
- .trim())
- .filter(Boolean);
- }
- function defaultProfilePath() {
- return path.resolve('industry-trend-intelligence/memory-templates/industry-trend-profile.json');
- }
- function hasInlineProfileArgs(args) {
- return Boolean(
- args.project ||
- args.industry ||
- args['business-type'] ||
- args.businessType ||
- args.keywords ||
- args.platforms ||
- args['target-audience'] ||
- args.targetAudience ||
- args['trend-questions'] ||
- args.trendQuestions ||
- args['blocked-directions'] ||
- args.blockedDirections
- );
- }
- function loadProfile(args) {
- const explicit = args.profile ? path.resolve(args.profile) : undefined;
- const profilePath = explicit && fs.existsSync(explicit) ? explicit : defaultProfilePath();
- const profile = explicit && fs.existsSync(explicit)
- ? readJson(profilePath)
- : (hasInlineProfileArgs(args) ? {} : (fs.existsSync(profilePath) ? readJson(profilePath) : {}));
- if (args.project) profile.project = args.project;
- if (args.industry) profile.industry = args.industry;
- if (args['business-type'] || args.businessType) profile.businessType = args['business-type'] || args.businessType;
- if (args.keywords) profile.keywords = parseList(args.keywords);
- if (args.platforms) profile.platforms = parseList(args.platforms);
- if (args['target-audience'] || args.targetAudience) profile.targetAudience = parseList(args['target-audience'] || args.targetAudience);
- if (args['trend-questions'] || args.trendQuestions) profile.trendQuestions = parseList(args['trend-questions'] || args.trendQuestions);
- if (args['blocked-directions'] || args.blockedDirections) profile.blockedDirections = parseList(args['blocked-directions'] || args.blockedDirections);
- return { profile, profilePath };
- }
- function inferProfileFromMessage(message, profile) {
- const text = String(message || '');
- if (!text) return profile;
- const next = { ...profile };
- if (text.includes('家装') || text.includes('全屋定制') || text.includes('装修')) {
- next.industry = next.industry || '家装全屋定制';
- next.businessType = next.businessType || '门店/设计服务/全屋定制服务商';
- next.platforms = next.platforms && next.platforms.length ? next.platforms : ['xiaohongshu'];
- next.keywords = next.keywords && next.keywords.length ? next.keywords : [
- '全屋定制',
- '奶油风装修',
- '小户型收纳',
- '衣柜设计避坑',
- '厨房橱柜设计',
- '装修翻车'
- ];
- next.mustTrackSignals = next.mustTrackSignals && next.mustTrackSignals.length ? next.mustTrackSignals : [
- '装修风格',
- '设计元素',
- '收纳需求',
- '环保顾虑',
- '预算焦虑',
- '翻车风险'
- ];
- }
- return next;
- }
- function updateMemoryFromMessage(message, memoryPath) {
- const text = String(message || '').trim();
- if (!text) return undefined;
- const memory = fs.existsSync(memoryPath) ? readJson(memoryPath) : {
- preferredSignals: [],
- blockedDirections: [],
- preferredOutput: [],
- updatedAt: ''
- };
- const keepMatch = text.match(/保留(.+?)(?:。|;|;|$)/);
- const dropMatch = text.match(/(?:不要|少给|降权)(.+?)(?:。|;|;|$)/);
- const outputMatch = text.match(/(?:更偏|偏)(.+?)(?:。|;|;|$)/);
- if (keepMatch) memory.preferredSignals = [...new Set([...(memory.preferredSignals || []), ...cleanPreferenceItems(keepMatch[1])])];
- if (dropMatch) memory.blockedDirections = [...new Set([...(memory.blockedDirections || []), ...cleanPreferenceItems(dropMatch[1])])];
- if (outputMatch) memory.preferredOutput = [...new Set([...(memory.preferredOutput || []), outputMatch[1].trim()])];
- memory.updatedAt = new Date().toISOString();
- writeJson(memoryPath, memory);
- return memory;
- }
- function isPreferenceMessage(message) {
- const text = String(message || '');
- return /保留|不要|少给|降权|更偏/.test(text) && !/日报|生成|启动/.test(text);
- }
- async function main() {
- const args = parseArgs(process.argv.slice(2));
- const outputDir = path.resolve(args.output || path.join('outputs', 'industry-trend-intelligence', new Date().toISOString().slice(0, 10)));
- ensureDir(outputDir);
- const message = args.message || '';
- const memoryPath = path.resolve(args.memory || 'memory/industry-trend-memory.json');
- if (isPreferenceMessage(message)) {
- const memory = updateMemoryFromMessage(message, memoryPath);
- const result = {
- status: 'ok',
- action: 'memory_updated',
- memoryPath,
- memory,
- assistantMessage: [
- '✅ 趋势偏好已保存。',
- '',
- '后续行业趋势情报日报会优先参考你保留的方向,并降低你不想看的方向。',
- '',
- '你可以继续说“重新生成日报”或“下次更偏门店转化/设计师话术”。'
- ].join('\n')
- };
- const prefix = args['result-prefix'] || args.resultPrefix || DEFAULT_RESULT_PREFIX;
- console.log(`${prefix}=${JSON.stringify(result)}`);
- return;
- }
- const { profile: loadedProfile, profilePath } = loadProfile(args);
- const profile = inferProfileFromMessage(message, loadedProfile);
- const keywordMatrix = args['keyword-matrix']
- ? readJson(path.resolve(args['keyword-matrix']))
- : keywordMatrixFromProfile(profile);
- const inputPath = args.input ? path.resolve(args.input) : undefined;
- const collectionMode = args['collection-mode'] || args.collectionMode || (toBool(args.live) ? 'live' : 'sample');
- const collectorOutputPath = path.join(outputDir, 'raw-input.json');
- let collectionResult;
- const dataset = inputPath && fs.existsSync(inputPath)
- ? readJson(inputPath)
- : (collectionResult = await collectXiaohongshuDataset({
- profile,
- keywordMatrix,
- mode: collectionMode,
- output: collectorOutputPath,
- fallbackSample: !toBool(args['no-fallback-sample']),
- keywordLimit: args['keyword-limit'] || args.keywordLimit,
- searchPages: args['search-pages'] || args.searchPages,
- notesPerKeyword: args['notes-per-keyword'] || args.notesPerKeyword,
- maxCommentPages: args['max-comment-pages'] || args.maxCommentPages,
- delayMs: args['delay-ms'] || args.delayMs,
- sort: args.sort,
- noteType: args['note-type'] || args.noteType
- })).dataset;
- writeJson(path.join(outputDir, 'profile-used.json'), profile);
- writeJson(path.join(outputDir, 'keyword-matrix.json'), keywordMatrix);
- if (inputPath) writeJson(path.join(outputDir, 'raw-input.json'), dataset);
- const report = buildReport({
- profile,
- dataset,
- keywordMatrix,
- outputDir
- });
- const result = {
- status: 'ok',
- profilePath,
- outputDir,
- files: [
- path.join(outputDir, 'profile-used.json'),
- path.join(outputDir, 'keyword-matrix.json'),
- path.join(outputDir, 'raw-input.json'),
- ...report.files
- ],
- summary: report.sample,
- oneLineJudgement: report.oneLineJudgement,
- assistantMessage: report.assistantMessage,
- markdown: report.markdown,
- keywordMatrix,
- collection: collectionResult ? {
- status: collectionResult.status,
- mode: collectionResult.mode,
- noteCount: collectionResult.noteCount,
- commentCount: collectionResult.commentCount,
- warningCount: (collectionResult.warnings || []).length,
- errorCount: (collectionResult.errors || []).length
- } : {
- status: 'provided_input',
- mode: 'input'
- },
- warnings: inputPath ? [] : (collectionResult?.warnings || []),
- errors: collectionResult?.errors || []
- };
- const prefix = args['result-prefix'] || args.resultPrefix || DEFAULT_RESULT_PREFIX;
- console.log(`${prefix}=${JSON.stringify(result)}`);
- }
- if (require.main === module) {
- main().catch(error => {
- console.error(error && error.stack ? error.stack : String(error));
- process.exit(1);
- });
- }
|