254 lines
8.5 KiB
JavaScript
254 lines
8.5 KiB
JavaScript
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 完成,无需配置外部模型接口`);
|
|
});
|