import express from 'express'; import { createServer } from 'http'; import { WebSocketServer } from 'ws'; import path from 'path'; import { fileURLToPath } from 'url'; 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, } 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'; const __dirname = path.dirname(fileURLToPath(import.meta.url)); const PORT = process.env.PORT || 3000; const app = express(); const server = createServer(app); const wss = new WebSocketServer({ server }); app.use(express.json()); app.use(express.static(path.join(__dirname, 'public'), { etag: false, maxAge: 0, setHeaders: (res) => res.setHeader('Cache-Control', 'no-store'), })); const clients = new Set(); wss.on('connection', (ws) => { clients.add(ws); ws.on('close', () => clients.delete(ws)); ws.on('error', () => clients.delete(ws)); ws.send(JSON.stringify({ event: 'connected', data: { tasks: taskManager.getAllTasks(), configured: true, }, })); }); function broadcast(msg) { const payload = JSON.stringify(msg); for (const ws of clients) { if (ws.readyState === ws.OPEN) { ws.send(payload); } } } const taskManager = new TaskManager(broadcast); // ========== Task API ========== app.get('/api/tasks', (req, res) => { res.json(taskManager.getAllTasks()); }); app.get('/api/tasks/:id', (req, res) => { const task = taskManager.getTask(req.params.id); if (!task) return res.status(404).json({ error: 'Task not found' }); res.json(task); }); app.post('/api/tasks', (req, res) => { const task = taskManager.createTask(req.body); if (req.body?.autoStart !== false) taskManager.startTask(task.id); res.json(taskManager.getTask(task.id)); }); app.put('/api/tasks/:id', (req, res) => { const task = taskManager.updateTask(req.params.id, req.body || {}); if (!task) return res.status(404).json({ error: 'Task not found or task is running' }); res.json(taskManager.getTask(req.params.id)); }); app.post('/api/tasks/:id/start', (req, res) => { const task = taskManager.getTask(req.params.id); if (!task) return res.status(404).json({ error: 'Task not found' }); taskManager.startTask(req.params.id, { reset: !!req.body?.reset }); res.json({ ok: true }); }); app.post('/api/tasks/:id/stop', (req, res) => { taskManager.stopTask(req.params.id); res.json({ ok: true }); }); app.post('/api/tasks/:id/duplicate', (req, res) => { const newTask = taskManager.duplicateTask(req.params.id); if (!newTask) return res.status(404).json({ error: 'Source task not found' }); res.json(newTask); }); app.delete('/api/tasks/:id', (req, res) => { taskManager.deleteTask(req.params.id); res.json({ ok: true }); }); app.post('/api/tasks/:id/clear-cache', (req, res) => { const result = taskManager.clearTaskCache(req.params.id, { mode: req.body?.mode || 'all', minScore: Number(req.body?.minScore ?? 75), }); if (!result) return res.status(404).json({ error: 'Task not found or task is running' }); res.json(result); }); app.post('/api/tasks/:id/analyze', async (req, res) => { const task = taskManager.getTaskFull(req.params.id); if (!task) return res.status(404).json({ error: 'Task not found' }); const analysis = taskManager.analyzeTask(req.params.id, { limit: Number(req.body?.limit || 50) }); const report = renderAnalysisMarkdown(taskManager.getTaskFull(req.params.id), analysis.summary); analysis.summary.reportPath = report.path; taskManager.setTaskAnalysis(req.params.id, analysis); let notification = null; if (req.body?.notify !== false) { notification = await sendNotifications(taskManager.getTaskFull(req.params.id), analysis.summary, { force: !!req.body?.forceNotify }); } res.json({ ok: true, analysis: analysis.summary, reportPath: report.path, notification }); }); app.get('/api/notify-config', (req, res) => { res.json(getNotifyConfig()); }); app.put('/api/notify-config', (req, res) => { res.json(saveNotifyConfig(req.body || {})); }); app.post('/api/notify-test', async (req, res) => { try { const config = req.body?.config || getNotifyConfig(); const targetLabel = (config.channels || []).filter(c => c.enabled).map(c => c.name || c.target || c.type).join('、') || '未启用通道'; const task = { id: 'notify-test', name: '通知渠道测试' }; const summary = { minScore: Number(config.minScore ?? 75), total: 1, recommendedCount: 1, cautionCount: 0, rejectedCount: 0, reportPath: '这是测试通知,不对应真实报告', topItems: [{ title: req.body?.title || 'xianyu-hunter 测试通知', score: 99, verdict: 'recommend', price: '测试', location: '本机', reason: `测试通知渠道:${targetLabel}`, href: '', }], }; const result = await sendNotifications(task, summary, { force: true, config: { ...config, enabled: true, notifyOnlyGood: false } }); res.status(result.ok ? 200 : 500).json(result); } catch (err) { res.status(500).json({ ok: false, error: err.message }); } }); 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: '请输入商品/需求描述后再润色' }); const fallback = buildLocalOptimizedTask({ seed, product, budget, region, currentRequirements }); try { 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 }); } }); app.get('/api/default-persona', (req, res) => { res.json({ persona: DEFAULT_PERSONA }); }); app.get('/api/defaults', (req, res) => { res.json({ persona: DEFAULT_PERSONA, coarsePrompt: DEFAULT_COARSE_PROMPT, finePrompt: DEFAULT_FINE_PROMPT, }); }); // ========== Login / Cookie API ========== // Open the persistent Playwright Chromium profile for manual Goofish login. // Cookies are saved by Chromium under ./browser-data and reused by later tasks. app.get('/api/login/status', async (req, res) => { try { const [status, pages] = await Promise.all([ getLoginStatus(), getOpenPagesInfo(), ]); res.json({ ok: true, ...status, ...pages }); } catch (err) { res.status(500).json({ ok: false, error: err.message }); } }); app.post('/api/login/open', async (req, res) => { try { const logs = []; const status = await openLoginPage(msg => logs.push(msg)); res.json({ ok: true, ...status, logs }); } catch (err) { res.status(500).json({ ok: false, error: err.message }); } }); app.post('/api/login/qr', async (req, res) => { try { const logs = []; const status = await getLoginQr(msg => logs.push(msg)); res.json({ ok: true, ...status, logs }); } catch (err) { res.status(500).json({ ok: false, error: err.message }); } }); app.post('/api/login/wait', async (req, res) => { try { const logs = []; const ok = await checkLogin(msg => logs.push(msg)); const status = await getLoginStatus(); res.json({ ok: true, loggedIn: ok && status.loggedIn, ...status, logs }); } catch (err) { res.status(500).json({ ok: false, error: err.message }); } }); app.post('/api/login/close-browser', async (req, res) => { await closeBrowser(); res.json({ ok: true }); }); app.post('/api/login/import-cookies', async (req, res) => { try { const logs = []; const payload = req.body?.cookies ?? req.body?.data ?? req.body; const result = await importCookiesFromJson(payload, msg => logs.push(msg)); res.json({ ok: true, imported: result.imported, ...result.status, logs }); } catch (err) { res.status(400).json({ ok: false, error: err.message }); } }); server.listen(PORT, () => { console.log(`🚀 闲鱼智能助手已启动: http://localhost:${PORT}`); console.log(`🦐 分析模式: 项目只负责采集和本地预筛,深度分析由 Hermes 完成,无需配置外部模型接口`); });