#!/usr/bin/env node import fs from 'fs'; import path from 'path'; import { fileURLToPath } from 'url'; const __dirname = path.dirname(fileURLToPath(import.meta.url)); const ROOT = path.join(__dirname, '..'); const TASKS_FILE = path.join(ROOT, 'data', 'tasks.json'); const REPORT_DIR = path.join(ROOT, 'reports'); function parseArgs(argv) { const args = { limit: 30 }; for (let i = 0; i < argv.length; i++) { const a = argv[i]; if (a === '--task' || a === '--task-id') args.taskId = argv[++i]; else if (a === '--limit') args.limit = Number(argv[++i] || 30); else if (a === '--out') args.out = argv[++i]; } return args; } function priceNumber(value) { const m = String(value || '').replace(/,/g, '').match(/\d+(?:\.\d+)?/); return m ? Number(m[0]) : null; } function riskFlags(item, task) { const text = `${item.title || ''} ${item.description || ''}`.toLowerCase(); const flags = []; const badWords = ['维修', '求购', '租', '回收', '展示机', '资源机', '监管机', '扩容', '改装', '仅拆封包装']; for (const w of badWords) if (text.includes(w)) flags.push(`疑似${w}`); if (/配件|零件|外壳|保护壳|贴膜/.test(text) && !/配件齐|原装配件|全套配件/.test(text)) flags.push('疑似配件/零件'); if (!item.description) flags.push('详情描述为空'); if (item.wantCount && /\d/.test(item.wantCount)) flags.push(`热度: ${item.wantCount}`); const p = priceNumber(item.detailPrice || item.price); if (task.config?.priceMax && p && p > task.config.priceMax) flags.push(`超预算(${p} > ${task.config.priceMax})`); if (item.sellerRating) flags.push(item.sellerRating); if (item.localCoarseReason) flags.push(`粗筛: ${item.localCoarseReason}`); if (item.localFineReason) flags.push(`细筛: ${item.localFineReason}`); return [...new Set(flags)].slice(0, 8); } function scoreItem(item, task) { let score = 60; const flags = riskFlags(item, task); score -= flags.filter(f => /疑似|为空|超预算/.test(f)).length * 15; if (item.description && item.description.length > 30) score += 8; if (item.sellerRating && /好评/.test(item.sellerRating)) score += 5; if (item.images?.length) score += Math.min(item.images.length, 5); return Math.max(0, Math.min(100, score)); } function renderTask(task, limit) { const items = [...(task.products?.fineFiltered || [])] .map(item => ({ item, score: scoreItem(item, task), flags: riskFlags(item, task) })) .sort((a, b) => b.score - a.score) .slice(0, limit); const lines = []; lines.push(`# ${task.name || task.id} — Hermes 候选分析报告`); lines.push(''); lines.push(`- 任务ID:\`${task.id}\``); lines.push(`- 阶段:${task.stage}`); lines.push(`- 关键词:${(task.config?.queries || []).join('、') || '-'}`); lines.push(`- 需求:${task.config?.customRequirements || '-'}`); lines.push(`- 数量:采集 ${task.products?.raw?.length || 0} / 粗筛 ${task.products?.coarseFiltered?.length || 0} / 候选 ${task.products?.fineFiltered?.length || 0}`); lines.push(''); if (items.length === 0) { lines.push('暂无可分析候选。'); return lines.join('\n'); } lines.push('## 优先查看'); lines.push(''); for (const { item, score, flags } of items) { const price = item.detailPrice || item.price || '-'; const loc = item.sellerLocation || '-'; const href = item.href || item.url || ''; lines.push(`### ${score}分|${item.title || '(无标题)'}`); lines.push(`- 价格:${price}`); lines.push(`- 地区/卖家:${loc}${item.sellerName ? ` / ${item.sellerName}` : ''}`); if (href) lines.push(`- 链接:${href}`); if (item.description) lines.push(`- 描述:${String(item.description).replace(/\s+/g, ' ').slice(0, 220)}`); if (flags.length) lines.push(`- 风险/备注:${flags.join(';')}`); lines.push('- 建议:人工打开链接核对成色、拆修、序列号/保修、配件与交易方式;当前工具不会自动联系卖家。'); lines.push(''); } return lines.join('\n'); } const args = parseArgs(process.argv.slice(2)); if (!fs.existsSync(TASKS_FILE)) { console.error(`未找到 ${TASKS_FILE}`); process.exit(1); } const tasks = JSON.parse(fs.readFileSync(TASKS_FILE, 'utf-8')); const task = args.taskId ? tasks.find(t => t.id === args.taskId) : tasks[tasks.length - 1]; if (!task) { console.error(`未找到任务: ${args.taskId || '(latest)'}`); process.exit(1); } const report = renderTask(task, args.limit); fs.mkdirSync(REPORT_DIR, { recursive: true }); const out = args.out || path.join(REPORT_DIR, `${task.id}-hermes-report.md`); fs.writeFileSync(out, report, 'utf-8'); console.log(out);