150 lines
6.7 KiB
JavaScript
150 lines
6.7 KiB
JavaScript
import fs from 'fs';
|
||
import path from 'path';
|
||
import { fileURLToPath } from 'url';
|
||
import { spawn } from 'child_process';
|
||
|
||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||
const ROOT = path.join(__dirname, '..');
|
||
const DATA_DIR = path.join(ROOT, 'data');
|
||
const NOTIFY_FILE = path.join(DATA_DIR, 'notify-config.json');
|
||
|
||
const DEFAULT_CONFIG = {
|
||
enabled: true,
|
||
notifyOnlyGood: true,
|
||
minScore: 75,
|
||
channels: [
|
||
{ id: 'feishu-default', type: 'hermes-send-message', name: '飞书当前会话', enabled: true, target: 'feishu:oc_3e963a150d98f254d8f38881bcfa1812' },
|
||
{ id: 'weixin-default', type: 'hermes-send-message', name: '微信默认会话', enabled: false, target: 'weixin' },
|
||
],
|
||
};
|
||
|
||
const HERMES_TARGET_OPTIONS = [
|
||
{ value: 'feishu', label: '飞书 Home' },
|
||
{ value: 'feishu:oc_3e963a150d98f254d8f38881bcfa1812', label: '飞书当前 DM' },
|
||
{ value: 'weixin', label: '微信 Home' },
|
||
{ value: 'weixin:o9cq809DWSYDlvbA9pj1akncjrhY@im.wechat', label: '微信 DM 1' },
|
||
{ value: 'weixin:o9cq808qCd1W-cIDo0iWjteE5lHY@im.wechat', label: '微信 DM 2' },
|
||
{ value: 'qqbot', label: 'QQ 机器人 Home(如已配置)' },
|
||
];
|
||
|
||
export function getNotifyConfig() {
|
||
try {
|
||
if (!fs.existsSync(NOTIFY_FILE)) return { ...DEFAULT_CONFIG, targetOptions: HERMES_TARGET_OPTIONS };
|
||
const cfg = JSON.parse(fs.readFileSync(NOTIFY_FILE, 'utf-8'));
|
||
return { ...DEFAULT_CONFIG, ...cfg, targetOptions: HERMES_TARGET_OPTIONS, channels: Array.isArray(cfg.channels) ? cfg.channels : DEFAULT_CONFIG.channels };
|
||
} catch {
|
||
return { ...DEFAULT_CONFIG, targetOptions: HERMES_TARGET_OPTIONS };
|
||
}
|
||
}
|
||
|
||
export function saveNotifyConfig(config = {}) {
|
||
const normalized = {
|
||
enabled: config.enabled !== false,
|
||
notifyOnlyGood: config.notifyOnlyGood !== false,
|
||
minScore: Math.max(0, Math.min(100, Number(config.minScore ?? 75))),
|
||
channels: (config.channels || []).map((c, i) => ({
|
||
id: c.id || `${c.type || 'channel'}-${Date.now()}-${i}`,
|
||
type: c.type || 'hermes-send-message',
|
||
name: c.name || c.type || '通知通道',
|
||
enabled: !!c.enabled,
|
||
url: c.url || '',
|
||
token: c.token || '',
|
||
target: c.target || c.platform || 'feishu',
|
||
})),
|
||
};
|
||
fs.mkdirSync(DATA_DIR, { recursive: true });
|
||
fs.writeFileSync(NOTIFY_FILE, JSON.stringify(normalized, null, 2), 'utf-8');
|
||
return { ...normalized, targetOptions: HERMES_TARGET_OPTIONS };
|
||
}
|
||
|
||
export function getHermesTargetOptions() {
|
||
return HERMES_TARGET_OPTIONS;
|
||
}
|
||
|
||
function buildNotificationText(task, summary) {
|
||
const label = { recommend: '推荐', caution: '谨慎', reject: '排除' };
|
||
const items = summary.topItems
|
||
.filter(i => i.verdict === 'recommend' || i.score >= summary.minScore)
|
||
.slice(0, 5);
|
||
const lines = [];
|
||
lines.push(`🐟 闲鱼任务「${task.name || task.id}」发现 ${summary.recommendedCount} 个推荐候选`);
|
||
lines.push(`候选/推荐/谨慎/排除:${summary.total}/${summary.recommendedCount}/${summary.cautionCount}/${summary.rejectedCount}`);
|
||
lines.push('');
|
||
for (const item of items) {
|
||
lines.push(`${item.score}分|${label[item.verdict] || item.verdict}|${item.title || ''}`);
|
||
lines.push(`价格:${item.price || '-'}|地区:${item.location || '-'}`);
|
||
if (item.reason) lines.push(`判断:${item.reason}`);
|
||
if (item.href) lines.push(item.href);
|
||
lines.push('');
|
||
}
|
||
if (summary.reportPath) lines.push(`报告:${summary.reportPath}`);
|
||
return lines.join('\n').trim();
|
||
}
|
||
|
||
async function runHermesSendMessage(target, message) {
|
||
return new Promise((resolve, reject) => {
|
||
const script = `import json\nfrom tools.send_message_tool import send_message_tool\nprint(send_message_tool({"action":"send","target":${JSON.stringify(target)},"message":${JSON.stringify(message)}}))\n`;
|
||
const child = spawn(process.env.HERMES_PYTHON || '/Users/chick/.hermes/hermes-agent/venv/bin/python3', ['-c', script], { cwd: process.env.HERMES_AGENT_DIR || '/Users/chick/.hermes/hermes-agent' });
|
||
let stdout = '';
|
||
let stderr = '';
|
||
const timer = setTimeout(() => { child.kill('SIGKILL'); reject(new Error('Hermes send_message 超时')); }, 60000);
|
||
child.stdout.on('data', d => { stdout += d.toString(); });
|
||
child.stderr.on('data', d => { stderr += d.toString(); });
|
||
child.on('error', err => { clearTimeout(timer); reject(err); });
|
||
child.on('close', code => {
|
||
clearTimeout(timer);
|
||
if (code !== 0) return reject(new Error((stderr || stdout || `Hermes send_message 退出码 ${code}`).slice(0, 500)));
|
||
try {
|
||
const parsed = JSON.parse(stdout.trim().split('\n').pop() || '{}');
|
||
if (parsed.error) return reject(new Error(parsed.error));
|
||
resolve(parsed);
|
||
} catch {
|
||
resolve({ ok: true, stdout: stdout.trim(), stderr: stderr.trim() });
|
||
}
|
||
});
|
||
});
|
||
}
|
||
|
||
async function postJson(url, body) {
|
||
const res = await fetch(url, {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify(body),
|
||
});
|
||
const text = await res.text();
|
||
if (!res.ok) throw new Error(`HTTP ${res.status}: ${text.slice(0, 200)}`);
|
||
return text;
|
||
}
|
||
|
||
export async function sendNotifications(task, summary, options = {}) {
|
||
const cfg = options.config || getNotifyConfig();
|
||
const minScore = Number(cfg.minScore ?? 75);
|
||
const shouldNotify = cfg.enabled !== false && (!cfg.notifyOnlyGood || summary.topItems.some(i => i.verdict === 'recommend' || i.score >= minScore));
|
||
if (!shouldNotify && !options.force) {
|
||
return { ok: true, skipped: true, reason: '没有达到通知条件' };
|
||
}
|
||
|
||
const message = buildNotificationText(task, { ...summary, minScore });
|
||
const results = [];
|
||
for (const ch of (cfg.channels || []).filter(c => c.enabled)) {
|
||
try {
|
||
if (ch.type === 'hermes-send-message' || ch.type === 'hermes-webhook') {
|
||
if (!ch.target) throw new Error('请选择 Hermes 通知目标');
|
||
await runHermesSendMessage(ch.target, message);
|
||
} else if (ch.type === 'bark') {
|
||
const base = (ch.url || '').replace(/\/$/, '');
|
||
const endpoint = ch.token ? `${base}/${encodeURIComponent(ch.token)}` : base;
|
||
await postJson(endpoint, { title: `闲鱼发现候选:${task.name || task.id}`, body: message, group: 'xianyu-hunter' });
|
||
} else if (ch.type === 'pushplus') {
|
||
await postJson(ch.url || 'https://www.pushplus.plus/send', { token: ch.token, title: `闲鱼发现候选:${task.name || task.id}`, content: message, template: 'txt' });
|
||
} else {
|
||
await postJson(ch.url, { title: `闲鱼发现候选:${task.name || task.id}`, message, taskId: task.id });
|
||
}
|
||
results.push({ id: ch.id, name: ch.name, ok: true });
|
||
} catch (err) {
|
||
results.push({ id: ch.id, name: ch.name, ok: false, error: err.message });
|
||
}
|
||
}
|
||
return { ok: results.every(r => r.ok), skipped: false, message, results };
|
||
}
|