Files
xianyu-hunter-hermes-skill/project/xianyu-hunter/lib/hermes_optimizer.mjs
T

126 lines
5.7 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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-3maxItems 建议 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 };