feat: publish xianyu hunter hermes skill package

This commit is contained in:
2026-05-06 19:26:45 +08:00
commit 6384156097
59 changed files with 9627 additions and 0 deletions
@@ -0,0 +1,107 @@
#!/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);