Files
mao1/shenyan-app/bridge-local/responder.js
fyah a61696f0a1 添加 shenyan-app 项目源码
- 首页 bridge 状态轮询 + 双击敲门进聊天
- 聊天屏本地桥通信(192.168.31.51:3003)
- 消息持久化(AsyncStorage + 桥历史接口)
- 桥服务器(port 3003)launchd 持久化
- 进度总览文档

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-16 00:33:57 +08:00

112 lines
3.3 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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);