import { spawn } from 'child_process'; import { DEFAULT_COARSE_PROMPT, DEFAULT_FINE_PROMPT } from './ai.mjs'; const HERMES_BIN = process.env.HERMES_BIN || '/Users/chick/.local/bin/hermes'; function extractFirstJsonObject(text = '') { const raw = String(text || '').trim().replace(/```json\s*/g, '').replace(/```/g, '').trim(); const direct = tryParse(raw); if (direct) return direct; const start = raw.indexOf('{'); if (start < 0) throw new Error('Hermes 返回中没有 JSON 对象'); let depth = 0; let inString = false; let escape = false; for (let i = start; i < raw.length; i++) { const ch = raw[i]; if (escape) { escape = false; continue; } if (ch === '\\') { escape = true; continue; } if (ch === '"') { inString = !inString; continue; } if (inString) continue; if (ch === '{') depth++; if (ch === '}') { depth--; if (depth === 0) { const parsed = tryParse(raw.slice(start, i + 1)); if (parsed) return parsed; } } } throw new Error('Hermes 返回的 JSON 无法解析'); } function tryParse(text) { try { return JSON.parse(text); } catch { return null; } } function buildLocalOptimizedTask({ seed = '', product = '', budget = '', region = '', currentRequirements = '' }) { const text = `${product || seed}`.trim(); const queries = [...new Set(text.split(/[,,、]/).map(s => s.trim()).filter(Boolean))]; if (queries.length === 0 && text) queries.push(text); const budgetNum = Number(String(budget).match(/\d+(?:\.\d+)?/)?.[0] || '') || null; return { name: text ? `${text} 监测` : '闲鱼商品监测', queries: queries.slice(0, 6), priceMin: null, priceMax: budgetNum, region, maxPages: 2, maxItems: 40, personalSeller: true, autoAnalyze: true, autoNotify: true, customRequirements: `${currentRequirements || `目标:${text}${budgetNum ? `,预算${budgetNum}左右` : ''}`};只做采集分析,不自动联系卖家;排除配件/维修/求购/租赁/回收/商家批量/标题党;优先个人自用、描述清楚、价格合理、可核验。`, coarsePrompt: DEFAULT_COARSE_PROMPT, finePrompt: DEFAULT_FINE_PROMPT, }; } function sanitizeTask(candidate, fallback) { const merged = { ...fallback, ...(candidate || {}) }; const queries = Array.isArray(merged.queries) ? merged.queries.map(q => String(q || '').trim()).filter(Boolean) : fallback.queries; return { name: String(merged.name || fallback.name || '闲鱼商品监测').slice(0, 80), queries: queries.length ? [...new Set(queries)].slice(0, 6) : fallback.queries, priceMin: merged.priceMin == null || merged.priceMin === '' ? null : Number(merged.priceMin), priceMax: merged.priceMax == null || merged.priceMax === '' ? fallback.priceMax : Number(merged.priceMax), region: String(merged.region || fallback.region || '').slice(0, 30), maxPages: Math.max(1, Math.min(20, Number(merged.maxPages || fallback.maxPages || 2))), maxItems: Math.max(1, Math.min(500, Number(merged.maxItems || fallback.maxItems || 40))), personalSeller: merged.personalSeller !== false, autoAnalyze: merged.autoAnalyze !== false, autoNotify: merged.autoNotify !== false, customRequirements: String(merged.customRequirements || fallback.customRequirements || '').slice(0, 1200), coarsePrompt: String(merged.coarsePrompt || fallback.coarsePrompt || DEFAULT_COARSE_PROMPT).slice(0, 2000), finePrompt: String(merged.finePrompt || fallback.finePrompt || DEFAULT_FINE_PROMPT).slice(0, 2000), }; } export async function optimizeTaskWithHermes(input = {}) { const fallback = buildLocalOptimizedTask(input); const prompt = `你是 Hermes Agent,正在为 BOSS 的闲鱼监控项目润色任务配置。请直接返回一个 JSON 对象,不要 Markdown,不要解释。 输出字段必须是: {"name":string,"queries":string[],"priceMin":number|null,"priceMax":number|null,"region":string,"maxPages":number,"maxItems":number,"personalSeller":boolean,"autoAnalyze":boolean,"autoNotify":boolean,"customRequirements":string,"coarsePrompt":string,"finePrompt":string} 要求: - queries 生成 3-6 个适合闲鱼/Goofish 搜索的关键词,包含别名、空格/无空格、大小写/中文变体。 - customRequirements 要写成可执行筛选规则,明确排除配件、维修、坏件、求购、租赁、回收、商家批量、异常低价、标题党。 - 保持安全:只采集和分析,不自动联系卖家、不下单、不承诺交易。 - maxPages 建议 1-3,maxItems 建议 20-80。 - autoAnalyze 和 autoNotify 默认 true。 - 如果预算存在,priceMax 使用预算数字;priceMin 通常为 null。 用户输入:${JSON.stringify(input, null, 2)} 本地兜底参考:${JSON.stringify(fallback, null, 2)}`; const stdout = await new Promise((resolve, reject) => { const child = spawn(HERMES_BIN, ['-z', prompt, '--ignore-rules', '-t', 'terminal'], { cwd: process.env.XIANHU_ROOT || process.cwd(), env: { ...process.env, HERMES_ACCEPT_HOOKS: '1' }, }); let out = ''; let err = ''; const timer = setTimeout(() => { child.kill('SIGKILL'); reject(new Error('Hermes 润色超时')); }, Number(process.env.HERMES_OPTIMIZE_TIMEOUT_MS || 90000)); child.stdout.on('data', d => { out += d.toString(); }); child.stderr.on('data', d => { err += d.toString(); }); child.on('error', e => { clearTimeout(timer); reject(e); }); child.on('close', code => { clearTimeout(timer); if (code !== 0) return reject(new Error((err || out || `Hermes 退出码 ${code}`).slice(0, 800))); resolve(out); }); }); const parsed = extractFirstJsonObject(stdout); return { source: 'hermes', task: sanitizeTask(parsed, fallback), raw: stdout.trim() }; } export { buildLocalOptimizedTask };