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
@@ -3,4 +3,4 @@
"notifyOnlyGood": true,
"minScore": 75,
"channels": []
}
}
@@ -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 };
+52 -11
View File
@@ -127,18 +127,56 @@ export async function openLoginPage(emitLog = () => {}) {
return status;
}
function isGoofishOrLoginPage(page) {
const url = page.url();
return /goofish\.com|login\.taobao\.com|havanaone/.test(url);
}
async function clickIfVisible(page, selector, timeout = 1000) {
const loc = page.locator(selector).first();
if (await loc.isVisible({ timeout }).catch(() => false)) {
await loc.click({ timeout: 3000 }).catch(() => {});
return true;
}
return false;
}
async function findQrLikeLocator(page) {
const selectors = [
'canvas',
'img[src^="data:image"]',
'img[src*="qr"]',
'img[src*="qrcode"]',
'[class*="qrcode"] canvas',
'[class*="qrcode"] img',
'[class*="qr"] canvas',
'[class*="qr"] img',
'[class*="login"] canvas',
'[class*="login"] img',
];
for (const selector of selectors) {
const loc = page.locator(selector).first();
if (await loc.isVisible({ timeout: 1200 }).catch(() => false)) return loc;
}
for (const frame of page.frames()) {
for (const selector of selectors) {
const loc = frame.locator(selector).first();
if (await loc.isVisible({ timeout: 800 }).catch(() => false)) return loc;
}
}
return null;
}
export async function getLoginQr(emitLog = () => {}) {
const ctx = await getBrowserContext();
const page = ctx.pages().find(p => /goofish\.com|login\.taobao\.com|havanaone/.test(p.url())) || ctx.pages()[0] || await ctx.newPage();
emitLog('正在打开闲鱼 Web 登录入口并生成后台二维码截图...');
const page = ctx.pages().find(isGoofishOrLoginPage) || ctx.pages()[0] || await ctx.newPage();
emitLog('正在从闲鱼官网入口打开登录页;如跳转到淘宝/支付宝统一登录,会在日志中标明来源。');
await page.goto(QR_LOGIN_URL, { waitUntil: 'domcontentloaded', timeout: 30000 });
await sleep(2500);
let clicked = false;
for (const selector of ['text=立即登录', 'text=登录', 'button:has-text("登录")', 'a:has-text("登录")']) {
const loc = page.locator(selector).first();
if (await loc.isVisible({ timeout: 1000 }).catch(() => false)) {
await loc.click({ timeout: 3000 }).catch(() => {});
if (await clickIfVisible(page, selector)) {
clicked = true;
await sleep(2500);
break;
@@ -146,26 +184,29 @@ export async function getLoginQr(emitLog = () => {}) {
}
for (const selector of ['text=扫码登录', 'text=二维码登录', 'text=使用二维码登录', '.icon-qrcode', '[class*=qrcode]']) {
const loc = page.locator(selector).first();
if (await loc.isVisible({ timeout: 1000 }).catch(() => false)) {
await loc.click({ timeout: 2500 }).catch(() => {});
if (await clickIfVisible(page, selector)) {
await sleep(1500);
break;
}
}
const currentUrl = page.url();
const redirectedProvider = /login\.taobao\.com|havanaone/.test(currentUrl) ? '淘宝/阿里统一登录' : '闲鱼 Web 登录入口';
const status = await getLoginStatus(page);
let qrImage = '';
let screenshotKind = '';
if (!status.loggedIn) {
const qrLike = await findQrLikeLocator(page);
const modal = page.locator('[class*="login-modal"], [class*="Login"], [class*="qrcode"], [class*="qr"], iframe').first();
const target = await modal.isVisible({ timeout: 1500 }).catch(() => false) ? modal : page.locator('body');
const target = qrLike || (await modal.isVisible({ timeout: 1500 }).catch(() => false) ? modal : page.locator('body'));
screenshotKind = qrLike ? '二维码区域' : '登录面板截图';
const buf = await target.screenshot({ type: 'png', timeout: 8000 }).catch(() => null);
if (buf) qrImage = `data:image/png;base64,${buf.toString('base64')}`;
}
if (status.loggedIn) emitLog('✅ 当前账号已登录,无需扫码');
else emitLog(`生成登录截图${clicked ? '' : '(未找到明确登录按钮,已截取当前页面)'};请在后台页面扫码后点“检测登录状态”`);
return { ...status, qrImage, qrCheckedAt: new Date().toISOString() };
else emitLog(`${redirectedProvider}生成${screenshotKind || '登录截图'}${clicked ? '' : '(未找到明确登录按钮,已截取当前页面)'};请扫码后点“刷新账号状态”。当前 URL:${currentUrl}`);
return { ...status, qrImage, qrCheckedAt: new Date().toISOString(), qrSource: redirectedProvider, qrUrl: currentUrl, qrScreenshotKind: screenshotKind };
}
export async function getLoginStatus(existingPage = null) {
+54 -3
View File
@@ -5,7 +5,8 @@ import { genId, timestamp } from './utils.mjs';
import { checkLogin } from './login.mjs';
import { searchProducts } from './search.mjs';
import { coarseFilter, fineFilter } from './filter.mjs';
import { buildAnalysis } from './analyzer.mjs';
import { buildAnalysis, renderAnalysisMarkdown } from './analyzer.mjs';
import { sendNotifications } from './notifier.mjs';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const DATA_DIR = path.join(__dirname, '..', 'data');
@@ -36,6 +37,8 @@ export class TaskManager {
finePrompt: config.finePrompt || '',
maxPages: Math.max(1, Math.min(20, Number(config.maxPages || 3))),
maxItems: Math.max(1, Math.min(500, Number(config.maxItems || 60))),
autoAnalyze: config.autoAnalyze !== false,
autoNotify: config.autoNotify !== false,
schedule: {
enabled: !!scheduleEnabled,
intervalMinutes: Math.max(0, Number(scheduleIntervalMinutes || 0) || 0),
@@ -123,7 +126,7 @@ export class TaskManager {
async startTask(id, configUpdates = {}) {
const task = this.tasks.get(id);
if (!task || task.running) return;
if (!task || task.running) return null;
if (Object.keys(configUpdates || {}).length) {
const { reset, scheduled, ...patch } = configUpdates;
@@ -151,6 +154,7 @@ export class TaskManager {
try {
await this._runPipeline(task, emitLog, shouldStop);
if (!task.stopped) {
await this._runAutoAnalysis(task, emitLog);
task.stage = 'completed';
}
} catch (err) {
@@ -268,6 +272,52 @@ export class TaskManager {
return task;
}
async _runAutoAnalysis(task, emitLog) {
if (task.config?.autoAnalyze === false) {
emitLog('🦐 已关闭自动 Hermes 分析:可在详情页手动执行分析');
return null;
}
if (!task.products?.fineFiltered?.length) {
emitLog('🦐 没有细筛候选,跳过自动 Hermes 分析');
return null;
}
emitLog(`🦐 自动执行 Hermes 分析:${task.products.fineFiltered.length} 个候选`);
const analysis = this.analyzeTask(task.id, { limit: Number(task.config.maxItems || 50) });
if (!analysis?.summary) {
emitLog('⚠️ Hermes 分析未生成结果');
return null;
}
try {
const report = renderAnalysisMarkdown(this.getTaskFull(task.id), analysis.summary);
analysis.summary.reportPath = report.path;
this.setTaskAnalysis(task.id, analysis);
emitLog(`📝 Hermes 分析报告已生成:${report.path}`);
} catch (err) {
emitLog(`⚠️ Hermes 报告生成失败:${err.message}`);
}
if (task.config?.autoNotify !== false) {
try {
const notification = await sendNotifications(this.getTaskFull(task.id), analysis.summary);
if (notification?.skipped) {
emitLog(`🔕 自动通知跳过:${notification.reason || '未达到通知条件'}`);
} else if (notification?.ok) {
emitLog('📣 自动通知已发送');
} else {
const errors = (notification?.results || []).filter(r => !r.ok).map(r => `${r.name || r.id}: ${r.error}`).join('');
emitLog(`⚠️ 自动通知部分失败:${errors || '未知错误'}`);
}
} catch (err) {
emitLog(`⚠️ 自动通知失败:${err.message}`);
}
} else {
emitLog('🔕 已关闭自动通知');
}
return analysis;
}
getAllTasks() {
return [...this.tasks.values()].map(t => this._serialize(t));
}
@@ -370,7 +420,7 @@ export class TaskManager {
task.chatSessionsFull = [];
this._emit(task.id, 'task:chats', task.chatSessions);
this._persistToDisk();
emitLog(`🦐 已关闭联系卖家功能:保留 ${task.products.fineFiltered.length} 个候选商品,后续由 Hermes 读取 data/tasks.json 做深度分析报告`);
emitLog(`🦐 采集和本地预筛完成:保留 ${task.products.fineFiltered.length} 个候选,准备自动执行 Hermes 分析`);
emitLog('========== 采集流程结束 ==========');
}
@@ -547,6 +597,7 @@ export class TaskManager {
}
_startScheduler() {
if (process.env.XIANHU_DISABLE_SCHEDULER === '1') return;
this.schedulerTimer = setInterval(() => this._tickSchedules(), 30000);
}
+15 -1
View File
@@ -232,6 +232,16 @@ function renderLoginStatus(data) {
function renderLoginQr(data) {
const panel = document.getElementById('login-qr-panel');
const img = document.getElementById('login-qr-img');
const title = document.getElementById('login-qr-title');
const sub = document.getElementById('login-qr-sub');
if (title) title.textContent = data.qrScreenshotKind || '扫码登录';
if (sub) {
const source = data.qrSource || '闲鱼 Web 登录入口';
const redirected = data.qrUrl && /login\.taobao\.com|havanaone/.test(data.qrUrl)
? '(已从闲鱼入口跳转到淘宝/阿里统一登录,这是官方登录链路)'
: '';
sub.textContent = `来源:${source}${redirected}。二维码过期就重新获取。`;
}
if (data.qrImage) {
img.src = data.qrImage;
panel.style.display = 'block';
@@ -358,6 +368,8 @@ function collectTaskForm() {
maxItems: maxItems ? Number(maxItems) : 60,
region,
personalSeller,
autoAnalyze: document.getElementById('f-auto-analyze')?.checked !== false,
autoNotify: document.getElementById('f-auto-notify')?.checked !== false,
customRequirements,
coarsePrompt: coarsePrompt || '',
finePrompt: finePrompt || '',
@@ -403,7 +415,7 @@ async function optimizeTaskForm() {
currentRequirements: document.getElementById('f-requirements').value.trim(),
});
fillTaskForm(result.task || {});
status.textContent = result.source === 'ai' ? '已由 AI 优化' : 'AI不可用,已用本地规则优化';
status.textContent = result.source === 'hermes' ? '已由 Hermes Agent 优化' : '已用本地规则优化Hermes 不可用时兜底)';
} catch (err) {
status.textContent = `优化失败:${err.message}`;
}
@@ -419,6 +431,8 @@ function fillTaskForm(taskOrConfig, name = '') {
document.getElementById('f-max-items').value = c.maxItems || 60;
document.getElementById('f-region').value = c.region || '';
document.getElementById('f-personal').checked = !!c.personalSeller;
const autoAnalyze = document.getElementById('f-auto-analyze'); if (autoAnalyze) autoAnalyze.checked = c.autoAnalyze !== false;
const autoNotify = document.getElementById('f-auto-notify'); if (autoNotify) autoNotify.checked = c.autoNotify !== false;
document.getElementById('f-requirements').value = c.customRequirements || '';
document.getElementById('f-coarse-prompt').value = c.coarsePrompt || defaultCoarsePrompt;
document.getElementById('f-fine-prompt').value = c.finePrompt || defaultFinePrompt;
+19 -9
View File
@@ -226,8 +226,8 @@
<div class="login-qr-panel" id="login-qr-panel" style="display:none">
<div class="login-qr-header">
<div>
<div class="login-qr-title">扫码登录</div>
<div class="login-qr-sub">点击“获取登录二维码”后,在这里直接扫码;二维码过期就重新获取</div>
<div class="login-qr-title" id="login-qr-title">扫码登录</div>
<div class="login-qr-sub" id="login-qr-sub">点击“获取登录二维码”后,会先从闲鱼官网入口打开登录链路;如跳转到淘宝/阿里统一登录,会在这里标明来源</div>
</div>
<button class="btn btn-sm" onclick="clearLoginQr()">清空</button>
</div>
@@ -240,10 +240,11 @@
<button class="btn" onclick="checkLoginStatus()">刷新账号状态</button>
</div>
<div class="login-help">
<p>1. 点击“获取/刷新登录二维码”后,后台会从闲鱼 Web 登录入口生成二维码/登录面板截图,可直接在本页面扫码</p>
<p>2. 扫码完成后点击“刷新账号状态”,本页会展示当前账号昵称、头像、Cookie 数和登录墙状态</p>
<p>3. Cookie 会自动保存在项目的 <code>browser-data/</code> 目录,后续采集任务复用这个登录态。</p>
<p>4. 如果二维码过期或页面显示登录墙,重新获取二维码后再扫码</p>
<p>1. 点击“获取/刷新登录二维码”后,后台会先进入闲鱼官网,再沿官方登录链路生成二维码/登录面板截图。</p>
<p>2. 闲鱼当前可能跳转到淘宝/阿里统一登录;页面会明确显示来源,不再把淘宝登录截图伪装成“闲鱼二维码”</p>
<p>3. 扫码完成后点击“刷新账号状态”,本页会展示当前账号昵称、头像、Cookie 数和登录墙状态。</p>
<p>4. Cookie 会自动保存在项目的 <code>browser-data/</code> 目录,后续采集任务复用这个登录态</p>
<p>5. 如果二维码过期或页面显示登录墙,重新获取二维码后再扫码。</p>
</div>
<pre class="login-log" id="login-log"></pre>
</div>
@@ -298,12 +299,21 @@
<textarea id="f-requirements" rows="3" placeholder="例如:只要成色9新以上的,电池健康90%以上,价格4500以内优先,最好有原装配件和发票。不要华强北翻新机。"></textarea>
</div>
<div class="form-group task-optimize-card">
<label>AI 优化生成 <span class="hint">(根据当前商品/预算/需求生成关键词和筛选条件)</span></label>
<label>任务智能润色 <span class="hint">(根据当前商品/预算/需求生成关键词和筛选条件;优先调用 Hermes Agent,失败自动降级本地规则</span></label>
<div class="persona-actions">
<button type="button" class="btn btn-sm btn-primary" onclick="optimizeTaskForm()">AI 优化当前表单</button>
<button type="button" class="btn btn-sm btn-primary" onclick="optimizeTaskForm()">润色当前表单</button>
<span class="hint" id="optimize-status"></span>
</div>
</div>
<div class="form-row">
<div class="form-group checkbox-group">
<label><input id="f-auto-analyze" type="checkbox" checked> 采集完成后自动执行 Hermes 分析</label>
</div>
<div class="form-group checkbox-group">
<label><input id="f-auto-notify" type="checkbox" checked> 分析后按通知设置自动推送</label>
</div>
</div>
<div class="hint">默认全自动:保存并采集 → 本地预筛 → Hermes 分析 → 命中通知;无需再手动点“执行分析”。</div>
<details class="advanced-section" open>
<summary class="advanced-toggle">定时任务</summary>
<div class="form-row" style="margin-top:12px">
@@ -349,6 +359,6 @@
<script src="app.js?v=19"></script>
<script src="app.js?v=20"></script>
</body>
</html>
+5 -30
View File
@@ -7,8 +7,9 @@ import { TaskManager } from './lib/task.mjs';
import { checkLogin, getLoginStatus, openLoginPage, importCookiesFromJson, getLoginQr } from './lib/login.mjs';
import { closeBrowser, getOpenPagesInfo } from './lib/browser.mjs';
import {
DEFAULT_PERSONA, DEFAULT_COARSE_PROMPT, DEFAULT_FINE_PROMPT, chatCompletion,
DEFAULT_PERSONA, DEFAULT_COARSE_PROMPT, DEFAULT_FINE_PROMPT,
} from './lib/ai.mjs';
import { optimizeTaskWithHermes, buildLocalOptimizedTask } from './lib/hermes_optimizer.mjs';
import { buildAnalysis, renderAnalysisMarkdown } from './lib/analyzer.mjs';
import { getNotifyConfig, saveNotifyConfig, sendNotifications } from './lib/notifier.mjs';
@@ -161,43 +162,17 @@ app.post('/api/notify-test', async (req, res) => {
app.post('/api/tasks/optimize', async (req, res) => {
const { seed = '', product = '', budget = '', region = '', currentRequirements = '' } = req.body || {};
const base = [product, seed, currentRequirements].filter(Boolean).join('\n');
if (!base.trim()) return res.status(400).json({ error: '请输入商品/需求描述后再优化' });
if (!base.trim()) return res.status(400).json({ error: '请输入商品/需求描述后再润色' });
const fallback = buildLocalOptimizedTask({ seed, product, budget, region, currentRequirements });
try {
const prompt = `你是闲鱼二手商品监测任务配置助手。根据用户想找的商品,生成更适合闲鱼搜索和风控筛选的任务配置。只输出JSON,不要解释。
字段:name:string, queries:string[], priceMin:number|null, priceMax:number|null, maxPages:number, maxItems:number, personalSeller:boolean, customRequirements:string, coarsePrompt:string, finePrompt:string。
要求:queries 3-6个,包含常见别名/空格/大小写变体;customRequirements 写清排除配件/维修/求购/商家批量/异常低价;maxItems 保守在20-80。
用户输入:${JSON.stringify({ seed, product, budget, region, currentRequirements })}`;
const reply = await chatCompletion([{ role: 'user', content: prompt }], { temperature: 0.2, maxTokens: 1200, timeoutMs: 20000 });
const cleaned = reply.replace(/```json?\n?/g, '').replace(/```/g, '').trim();
const parsed = JSON.parse(cleaned);
res.json({ ok: true, source: 'ai', task: { ...fallback, ...parsed } });
const result = await optimizeTaskWithHermes({ seed, product, budget, region, currentRequirements });
res.json({ ok: true, source: result.source || 'hermes', task: result.task });
} catch (err) {
res.json({ ok: true, source: 'local-fallback', warning: err.message, task: fallback });
}
});
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,
customRequirements: `${currentRequirements || `目标:${text}${budgetNum ? `,预算${budgetNum}左右` : ''}`};只做采集分析,不自动联系卖家;排除配件/维修/求购/租赁/回收/商家批量/标题党;优先个人自用、描述清楚、价格合理、可核验。`,
coarsePrompt: DEFAULT_COARSE_PROMPT,
finePrompt: DEFAULT_FINE_PROMPT,
};
}
app.get('/api/default-persona', (req, res) => {
res.json({ persona: DEFAULT_PERSONA });
});