feat: update xianyu web login and Hermes optimizer

This commit is contained in:
2026-05-06 20:36:19 +08:00
parent 6384156097
commit d5294a3f9b
15 changed files with 800 additions and 62 deletions
@@ -0,0 +1,125 @@
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 };