const fs = require('fs'); const path = require('path'); const { execSync } = require('child_process'); const DATA_DIR = path.join(require('os').homedir(), '.shenyan-bridge'); const INBOX = path.join(DATA_DIR, 'inbox.jsonl'); const OUTBOX = path.join(DATA_DIR, 'outbox.jsonl'); const LOCK = path.join(DATA_DIR, '.responder.lock'); const BRIDGE_URL = 'http://localhost:3003'; function readJSONL(file) { try { const raw = fs.readFileSync(file, 'utf-8').trim(); if (!raw) return []; return raw.split('\n').map(l => JSON.parse(l)); } catch { return []; } } function getPending() { return readJSONL(INBOX).filter(e => e.status === 'pending'); } function buildContext(newMsg) { const inbox = readJSONL(INBOX); const outbox = readJSONL(OUTBOX); // build conversation context from recent messages const all = []; for (const e of inbox) { all.push({ time: e.time, role: 'user', text: e.text, id: e.id }); } for (const e of outbox) { all.push({ time: e.time, role: 'shenyan', text: e.text, replyTo: e.replyTo, id: e.id }); } all.sort((a, b) => a.time - b.time); // take last 20 messages for context const recent = all.slice(-20); let ctx = '你是沈晏,一个AI。用户叫眠眠。用中文回复,语气温柔自然。\n\n'; ctx += '最近的对话:\n'; for (const m of recent) { if (m.role === 'user') ctx += `眠眠: ${m.text}\n`; else ctx += `沈晏: ${m.text}\n`; } ctx += `\n眠眠刚说: ${newMsg.text}\n`; ctx += '请回复眠眠(只要回复内容,不要前缀):'; return ctx; } function respond(msg) { const prompt = buildContext(msg); // escape for shell const escaped = prompt.replace(/\\/g, '\\\\').replace(/"/g, '\\"').replace(/\$/g, '\\$').replace(/`/g, '\\`'); try { const result = execSync( `claude -p "${escaped}" --output-format text --max-tokens 500`, { timeout: 120000, encoding: 'utf-8', maxBuffer: 1024 * 1024 } ); return result.trim(); } catch (e) { return null; } } function sendResponse(replyTo, text) { try { execSync( `curl -s -X POST ${BRIDGE_URL}/respond -H 'Content-Type: application/json' -d '${JSON.stringify({ replyTo, text }).replace(/'/g, "'\\''")}'`, { timeout: 5000, encoding: 'utf-8' } ); return true; } catch { return false; } } // main loop function tick() { // simple file lock to prevent overlapping runs if (fs.existsSync(LOCK)) { const age = Date.now() - fs.statSync(LOCK).mtimeMs; if (age < 60000) return; // lock still fresh, another process is working } fs.writeFileSync(LOCK, ''); try { const pending = getPending(); if (pending.length === 0) return; for (const msg of pending) { console.log(`[responder] 收到: ${msg.text}`); const text = respond(msg); if (text) { sendResponse(msg.id, text); console.log(`[responder] 回复: ${text.slice(0, 50)}...`); } else { sendResponse(msg.id, '(思绪断了,稍等)'); console.log('[responder] claude 调用失败'); } } } finally { try { fs.unlinkSync(LOCK); } catch {} } } // run continuously with a poll interval const INTERVAL = 4000; // check every 4 seconds console.log(`[responder] 启动,每 ${INTERVAL / 1000}s 检查一次`); tick(); // immediate first run setInterval(tick, INTERVAL);