108 lines
4.6 KiB
JavaScript
108 lines
4.6 KiB
JavaScript
#!/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);
|