/** * auto-reply.js — Pixel 手机微信自动回复 Plugin(接入 hermes API) * * 完整链路: * 1. termux-notification-list 监听通知(息屏兜底,定时轮询) * 2. 检测到消息后 input keyevent 224 点亮屏幕 + wm dismiss-keyguard 解锁 * 3. am start LauncherUI 把微信切到前台 * 4. AndroidWechatTrackerSQLCipher 轮询 EnMicroMsg.db 拉新消息 + 历史上下文 * 5. callHermesApi POST 到 hermes-agent (127.0.0.1:8642) 生成回复 * 6. sendTextMessageBroadcast → am broadcast com.hermes.wechat.SEND_MSG * → HermesAccessibilityService UI 自动化点击微信完成发送 * * 运行方式: * cd /data/data/com.termux/files/home/hermes-wechat * ./start-ar.sh (内部 nohup node auto-reply.js &) * * 终止: * pkill -f auto-reply.js */ const { AndroidWechatTrackerSQLCipher } = require('./android-wechat-tracker-sqlcipher'); const { sendTextMessageBroadcast, checkAccessibilityServiceEnabled, enableAccessibilityService, shell, } = require('./android-wechat-sender'); const fs = require('fs'); const path = require('path'); const http = require('http'); // ═══════════════════════════════════════════════════════════════ // 配置区 // ═══════════════════════════════════════════════════════════════ const CONFIG = { // 你的微信信息(用于判断群聊 @/引用) myWxid: process.env.MY_WXID || '', myNickname: process.env.MY_NICKNAME || '', // hermes-agent API (OpenAI-compatible, localhost:8642) hermesHost: process.env.HERMES_HOST || '127.0.0.1', hermesPort: parseInt(process.env.HERMES_PORT, 10) || 8642, hermesTimeoutMs: parseInt(process.env.HERMES_TIMEOUT, 10) || 120000, // 回复行为 replyInPrivateChat: true, replyAtInGroup: true, replyQuoteInGroup: true, maxReplyLength: 500, cooldownMs: 3000, ignoreSelf: true, // 上下文长度:每次调 hermes 时回看多少条最近消息(含本次触发的那条) historyContextSize: parseInt(process.env.HISTORY_CONTEXT_SIZE, 10) || 8, // 通知监听(息屏兜底) notificationPollIntervalMs: 3000, notificationEnabled: process.env.NOTIFICATION_ENABLED !== 'false', // 日志 logFile: path.join(__dirname, 'auto-reply.log'), stateFile: path.join(__dirname, '.auto-reply-state.json'), }; // ═══════════════════════════════════════════════════════════════ // 状态管理 // ═══════════════════════════════════════════════════════════════ class ReplyState { constructor(filePath) { this.filePath = filePath; this.repliedMsgIds = new Set(); this.lastReplyTime = {}; this.load(); } load() { try { if (fs.existsSync(this.filePath)) { const data = JSON.parse(fs.readFileSync(this.filePath, 'utf-8')); this.repliedMsgIds = new Set(data.repliedMsgIds || []); this.lastReplyTime = data.lastReplyTime || {}; } } catch (e) { log('State load failed:', e.message); } } save() { try { fs.writeFileSync(this.filePath, JSON.stringify({ repliedMsgIds: Array.from(this.repliedMsgIds), lastReplyTime: this.lastReplyTime, }), 'utf-8'); } catch (e) { log('State save failed:', e.message); } } hasReplied(msgId) { return this.repliedMsgIds.has(msgId); } markReplied(msgId, talker) { this.repliedMsgIds.add(msgId); this.lastReplyTime[talker] = Date.now(); if (this.repliedMsgIds.size > 1000) { const arr = Array.from(this.repliedMsgIds); this.repliedMsgIds = new Set(arr.slice(-500)); } this.save(); } isCooldown(talker) { const last = this.lastReplyTime[talker]; if (!last) return false; return Date.now() - last < CONFIG.cooldownMs; } } // ═══════════════════════════════════════════════════════════════ // 日志 // ═══════════════════════════════════════════════════════════════ function log(...args) { const line = `[${new Date().toISOString()}] ${args.join(' ')}`; console.log(line); try { fs.appendFileSync(CONFIG.logFile, line + '\n'); } catch {} } // ═══════════════════════════════════════════════════════════════ // Hermes API 调用 // ═══════════════════════════════════════════════════════════════ /** * 调用 hermes-agent 的 /v1/runs 流式 API,按 agent loop 的 tool 边界 * 把每一段 assistant 文本作为独立"utterance"通过 onUtterance 回调推出。 * * 事件流(SSE on /v1/runs/{id}/events): * message.delta { delta: "字符片段" } → 累积到 textBuf * tool.started { tool, preview } → flush textBuf 作为一条 utterance * tool.completed → 下一个 turn 准备开始 * reasoning.available → 不转发(hermes 思考过程) * run.completed { output, usage } → 最终 flush * run.failed / run.cancelled → reject * * @param {Array} messages OpenAI 格式(含 system / user / assistant 多轮) * @param {string} sessionId hermes session(每个 talker 独立) * @param {Function} onUtterance async (text, index, isFinal) => Promise * 串行调用;返回的 Promise 完成前不会发下一条 * @returns {Promise<{output?, usage?}>} */ async function callHermesRunStream(messages, sessionId, onUtterance) { const userMsg = messages[messages.length - 1]?.content || ''; const systemMsg = messages.find(m => m.role === 'system'); const history = messages.slice(0, -1) .filter(m => m.role !== 'system') .map(m => ({ role: m.role, content: m.content })); // 1) POST /v1/runs 拿 run_id const startBody = JSON.stringify({ input: userMsg, conversation_history: history, session_id: sessionId, instructions: systemMsg?.content, }); const runId = await new Promise((resolve, reject) => { const req = http.request({ hostname: CONFIG.hermesHost, port: CONFIG.hermesPort, path: '/v1/runs', method: 'POST', headers: { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(startBody), }, timeout: 15000, }, res => { let buf = ''; res.on('data', c => buf += c.toString()); res.on('end', () => { try { const j = JSON.parse(buf); if (j.run_id) return resolve(j.run_id); reject(new Error('No run_id in response: ' + buf.substring(0, 200))); } catch (e) { reject(e); } }); }); req.on('error', reject); req.on('timeout', () => { req.destroy(); reject(new Error('start timeout')); }); req.write(startBody); req.end(); }); log('[HERMES] Run started:', runId, 'session:', sessionId); // 2) GET /v1/runs/{runId}/events (SSE) return new Promise((resolve, reject) => { const req = http.get({ hostname: CONFIG.hermesHost, port: CONFIG.hermesPort, path: `/v1/runs/${runId}/events`, timeout: CONFIG.hermesTimeoutMs, }, res => { let buf = ''; let textBuf = ''; let utteranceIdx = 0; let sendChain = Promise.resolve(); // 串行队列:每个 flush 在前一个完成后执行 let finalResult = {}; const flush = (isFinal) => { const text = textBuf.trim(); textBuf = ''; if (!text) return; const idx = utteranceIdx++; // 关键:链式 append,不等待 sendChain = sendChain .then(() => onUtterance(text, idx, !!isFinal)) .catch(e => log('[onUtterance error]', e.message)); }; // diagnostic counters const evCount = { 'message.delta': 0 }; let lastDiagLogAt = Date.now(); const diagLog = (evt) => { evCount[evt] = (evCount[evt] || 0) + 1; const now = Date.now(); if (evt !== 'message.delta' || now - lastDiagLogAt > 10000) { log('[STREAM EV]', evt, 'counts=', JSON.stringify(evCount), 'textBuf.len=' + textBuf.length); lastDiagLogAt = now; } }; res.setEncoding('utf-8'); res.on('data', chunk => { buf += chunk; let sepIdx; while ((sepIdx = buf.indexOf('\n\n')) >= 0) { const block = buf.substring(0, sepIdx); buf = buf.substring(sepIdx + 2); for (const line of block.split('\n')) { if (!line.startsWith('data: ')) continue; let ev; try { ev = JSON.parse(line.substring(6)); } catch (_) { continue; } diagLog(ev.event); switch (ev.event) { case 'message.delta': textBuf += (ev.delta || ''); break; case 'tool.started': // turn 边界:上一段 assistant 文本完成 flush(false); break; case 'run.completed': flush(true); finalResult = { output: ev.output, usage: ev.usage }; // 等串行队列 drain 完再 resolve sendChain.then(() => resolve(finalResult)); break; case 'run.failed': reject(new Error(ev.error || 'run failed')); break; case 'run.cancelled': reject(new Error('run cancelled')); break; // tool.completed / reasoning.available / approval.* 忽略 } } } }); res.on('end', () => { flush(true); sendChain.then(() => resolve(finalResult)); }); res.on('error', reject); }); req.on('error', reject); req.on('timeout', () => { req.destroy(); reject(new Error('events stream timeout')); }); }); } /** * 调用 hermes-agent API (OpenAI-compatible) 生成回复 — 老的非流式同步 API * * @param {Array<{role:string,content:string}>} messages - OpenAI messages 格式 * @param {string} sessionId - 会话 ID(每个 talker 独立,作为 X-Hermes-Session-Id) * @returns {Promise} 回复文本 */ async function callHermesApi(messages, sessionId) { return new Promise((resolve) => { const payload = JSON.stringify({ model: 'hermes-agent', messages, stream: false, }); const req = http.request({ hostname: CONFIG.hermesHost, port: CONFIG.hermesPort, path: '/v1/chat/completions', method: 'POST', headers: { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(payload), }, timeout: CONFIG.hermesTimeoutMs, }, (res) => { let buffer = ''; res.on('data', chunk => { buffer += chunk; }); res.on('end', () => { log('[HERMES] HTTP status:', res.statusCode, 'body:', buffer.substring(0, 300)); try { const data = JSON.parse(buffer); if (data.error) { log('[HERMES] API error:', JSON.stringify(data.error)); resolve(null); return; } const content = data.choices?.[0]?.message?.content; resolve(content || null); } catch (e) { log('[HERMES] JSON parse error:', e.message); resolve(null); } }); }); req.on('error', (err) => { log('[HERMES] HTTP error:', err.message); resolve(null); }); req.on('timeout', () => { req.destroy(); log('[HERMES] API timeout'); resolve(null); }); req.write(payload); req.end(); }); } // ═══════════════════════════════════════════════════════════════ // 回复生成器 — 接入 hermes // ═══════════════════════════════════════════════════════════════ /** * 构造 OpenAI messages 数组发给 hermes-agent * * 把 talker 的最近 N 条聊天记录作为多轮 messages(自己发的为 assistant、对方发的为 user)。 * 再把本次触发消息作为最后一个 user turn,并在 system 中说明回复要求。 */ function buildHermesMessages(ctx) { const { type, talker, displayName, content, senderName, history } = ctx; let role = ''; if (type === 'private') { role = `这是和「${displayName || talker}」的微信私聊。`; } else if (type === 'group-at') { role = `这是一个微信群聊,刚才有人 @ 我("${senderName}"),群名 ${displayName || talker}。`; } else if (type === 'group-quote') { role = `这是一个微信群聊,有人引用了我的消息(发送者 "${senderName}"),群名 ${displayName || talker}。`; } else { role = `这是来自「${displayName || talker}」的消息。`; } const systemMsg = { role: 'system', content: `你是 lemonAI 助手,正在帮我回复微信消息。${role}\n` + '回复要自然、简短、得体,使用中文。只输出回复内容,不要加任何解释、不要带前缀如"回复:"。', }; // history 是 [{ isSend, content }] 旧→新;本次消息已经在最后一条 const msgs = [systemMsg]; if (Array.isArray(history) && history.length > 0) { for (const h of history) { const r = h.isSend === 1 ? 'assistant' : 'user'; // 群聊里对方发的 content 形如 "wxid_x:\n实际内容",去掉前缀 let txt = h.content || ''; if (r === 'user' && txt.includes(':\n')) { txt = txt.substring(txt.indexOf(':\n') + 2); } msgs.push({ role: r, content: txt }); } } else { // 没有历史时,直接把当前内容作为 user turn msgs.push({ role: 'user', content }); } return msgs; } /** * 接入 hermes-agent 的回复生成器 */ async function hermesReplyGenerator(ctx) { const { talker } = ctx; const sessionId = `wx-${talker}`; const messages = buildHermesMessages(ctx); log('[HERMES] Calling API for', talker, 'session:', sessionId, 'turns=' + messages.length); const reply = await callHermesApi(messages, sessionId); if (reply) { log('[HERMES] Reply:', reply.substring(0, 80)); } else { log('[HERMES] No reply generated'); } return reply; } // ═══════════════════════════════════════════════════════════════ // 消息判断 // ═══════════════════════════════════════════════════════════════ function isGroupChat(talker) { return talker && talker.endsWith('@chatroom'); } function isAtMe(content, nickname) { if (!nickname) return false; const pattern = new RegExp('@' + escapeRegex(nickname) + '(\\s|\\n|$)'); return pattern.test(content); } function isQuoteMe(content, nickname) { if (!nickname) return false; if (!content.includes('([^<]+)<\/displayname>/); if (match) return match[1] === nickname; return false; } function escapeRegex(str) { return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); } // ═══════════════════════════════════════════════════════════════ // 屏幕状态检测与微信唤醒(息屏专用) // ═══════════════════════════════════════════════════════════════ async function isScreenOn() { try { const out = await shell('dumpsys power | grep "mWakefulness"', 3000); return out.includes('mWakefulness=Awake'); } catch { return false; } } async function isKeyguardLocked() { try { const out = await shell('dumpsys window | grep "mInputRestricted"', 3000); return out.includes('mInputRestricted=true'); } catch { return false; } } async function isWechatForeground() { try { // Android 15 上 mCurrentFocus 已废弃,改用 mFocusedApp const out = await shell('dumpsys activity activities | grep "mFocusedApp"', 5000); return out.includes('com.tencent.mm'); } catch { return false; } } /** * 息屏唤醒完整流程:点亮 → 解锁(无密码时滑动) → 启动微信 → 确认前台 */ async function wakeWechat() { try { // 1. 如果屏幕是黑的,用 KEYCODE_WAKEUP(224) 点亮,不会 toggle const screenOn = await isScreenOn(); if (!screenOn) { await shell('input keyevent 224', 3000); log('[WAKE] Screen woke up'); await sleep(800); } // 2. 无密码时,wm dismiss-keyguard 可直接解除锁屏,比滑动更可靠 try { await shell('wm dismiss-keyguard', 3000); log('[WAKE] Keyguard dismissed'); } catch (e) { log('[WAKE] dismiss-keyguard failed (may already unlocked):', e.message); } await sleep(500); // 3. 启动微信到前台 await shell('/system/bin/am start -n com.tencent.mm/.ui.LauncherUI', 5000); log('[WAKE] WeChat LauncherUI started'); await sleep(1000); return true; } catch (e) { log('[WAKE] Failed:', e.message); return false; } } /** * 确保微信在前台且屏幕已解锁,轮询最多 20 秒 */ async function ensureWechatForeground(maxWaitMs = 20000) { const start = Date.now(); while (Date.now() - start < maxWaitMs) { const screenOn = await isScreenOn(); const locked = await isKeyguardLocked(); const fg = await isWechatForeground(); if (screenOn && !locked && fg) { // 模拟一次点击保持活跃 try { await shell('input keyevent 62', 1000); } catch (_) {} log('[WAKE] Confirmed: screen on + unlocked + WeChat foreground'); return true; } if (!screenOn) { log('[WAKE] Screen still off, retrying keyevent 224...'); try { await shell('input keyevent 224', 3000); } catch (_) {} } else if (locked) { log('[WAKE] Still locked, retrying dismiss-keyguard...'); try { await shell('wm dismiss-keyguard', 3000); } catch (_) {} } else if (!fg) { log('[WAKE] WeChat not foreground, retrying start...'); try { await shell('am start -n com.tencent.mm/.ui.LauncherUI', 5000); } catch (_) {} } await sleep(1500); } log('[WAKE] Foreground check timeout'); return false; } // ═══════════════════════════════════════════════════════════════ // 通知监听(息屏兜底) // ═══════════════════════════════════════════════════════════════ async function pollNotifications(bot) { const termuxBin = '/data/data/com.termux/files/usr/bin/termux-notification-list'; if (!fs.existsSync(termuxBin)) { log('[NOTIF] termux-notification-list not found, notification watcher disabled'); return; } log('[NOTIF] Notification watcher started'); const knownKeys = new Set(); while (bot.running) { try { const out = await shell(termuxBin, 10000); const notifs = JSON.parse(out); const wechatNotifs = notifs.filter(n => n.packageName === 'com.tencent.mm'); for (const n of wechatNotifs) { const key = `${n.title}:${n.content}`; if (knownKeys.has(key)) continue; knownKeys.add(key); // 限制 set 大小 if (knownKeys.size > 200) { const arr = Array.from(knownKeys); knownKeys.clear(); arr.slice(-100).forEach(k => knownKeys.add(k)); } log('[NOTIF] WeChat notification:', n.title, n.content.substring(0, 60)); // 息屏时微信可能不在前台,先点亮+启动微信 // 剩下的检测和发送交给 sqlcipher 轮询 + handleMessage const screenOn = await isScreenOn(); if (!screenOn) { log('[NOTIF] Screen off, waking WeChat...'); const ok = await wakeWechat(); if (ok) { await ensureWechatForeground(20000); log('[NOTIF] WeChat woken up, tracker will catch the message'); } } } } catch (e) { // notification-list 可能返回空或失败,忽略 } await sleep(CONFIG.notificationPollIntervalMs); } } function sleep(ms) { return new Promise(r => setTimeout(r, ms)); } // ═══════════════════════════════════════════════════════════════ // 主逻辑 // ═══════════════════════════════════════════════════════════════ class AutoReplyBot { constructor(replyGenerator = hermesReplyGenerator) { // 优先使用 sqlcipher DB 轮询(LSPosed hook 在 WeChat 8.0.72 上不稳定) this.tracker = new AndroidWechatTrackerSQLCipher(); this.state = new ReplyState(CONFIG.stateFile); this.replyGenerator = replyGenerator; this.running = false; } async handleMessage(msg) { if (msg.type !== 'recv') return; const { talker, content, msgId } = msg; if (!talker || !content) return; if (CONFIG.ignoreSelf && talker === CONFIG.myWxid) return; if (this.state.hasReplied(msgId)) return; if (this.state.isCooldown(talker)) { log('[COOLDOWN] Skip', talker); return; } let replyType = null; let senderName = ''; if (!isGroupChat(talker)) { if (CONFIG.replyInPrivateChat) replyType = 'private'; } else { const nameMatch = content.match(/^([^:\n]+):\n/); if (nameMatch) senderName = nameMatch[1]; if (CONFIG.replyAtInGroup && isAtMe(content, CONFIG.myNickname)) { replyType = 'group-at'; } else if (CONFIG.replyQuoteInGroup && isQuoteMe(content, CONFIG.myNickname)) { replyType = 'group-quote'; } } if (!replyType) return; log(`[${replyType.toUpperCase()}] ${talker}: ${content.substring(0, 80)}`); // 发送前确保微信在前台(息屏时通知监听会先唤醒) const fg = await isWechatForeground(); if (!fg) { log('[WECHAT] Not foreground, waking...'); await wakeWechat(); const ok = await ensureWechatForeground(); if (!ok) { log('[WECHAT] Could not bring WeChat to foreground, skip sending'); return; } await sleep(2000); // 额外等待网络初始化 } // 查 displayName(A11y 用它来在搜索框里搜联系人) let displayName; try { displayName = await this.tracker.getContactNickname(talker); } catch (e) { displayName = talker; } if (!displayName || displayName === talker) { log('[WARN] No displayName resolved for', talker, '— A11y 搜索可能失败'); } // 拉历史消息作为 hermes 上下文 let history = []; try { history = await this.tracker.fetchHistory(talker, CONFIG.historyContextSize); } catch (e) { log('[HISTORY] fetch failed:', e.message); } // 流式 hermes 调用直接在 onUtterance 里发送,不再先生成完整文本 // 群里被 @ 时,第一条回复带 quote + atSender;多步流式回复后续条仅文本 const baseSendOpts = { toUserDisplayName: displayName }; let firstSendOpts = { ...baseSendOpts }; if (replyType === 'group-at' || replyType === 'group-quote') { let bubbleText = content; const colonIdx = bubbleText.indexOf(':\n'); if (colonIdx > 0) bubbleText = bubbleText.substring(colonIdx + 2); const trimmed = bubbleText.replace(/<[^>]+>/g, '').trim(); if (trimmed) firstSendOpts.quoteText = trimmed.substring(0, 30); if (senderName) { try { const senderNick = await this.tracker.getContactNickname(senderName); firstSendOpts.atSender = senderNick || senderName; } catch (_) { firstSendOpts.atSender = senderName; } } log('[GROUP] first-reply quote=' + (firstSendOpts.quoteText || '-') + ' atSender=' + (firstSendOpts.atSender || '-')); } // 走流式:每个 hermes turn → 一条独立微信消息 // 第一条带 quote + atSender,后续条仅文本(避免重复引用 / @) const onUtterance = async (text, idx, isFinal) => { // 单条限长保护 let toSend = text; if (toSend.length > CONFIG.maxReplyLength) { toSend = toSend.substring(0, CONFIG.maxReplyLength) + '...'; } const opts = (idx === 0) ? firstSendOpts : baseSendOpts; log('[UTTER]', idx, isFinal ? '(final)' : '', toSend.substring(0, 60)); try { const r = await sendTextMessageBroadcast(talker, toSend, opts); if (r.ok) { log('[SENT]', talker, `#${idx}`, toSend.substring(0, 60)); } else { log('[SEND FAILED]', talker, r.error); } } catch (e) { log('[SEND ERROR]', e.message); } // 等 A11y 跑完 chat-list-click → quote → @ → type → send 完整一轮 // 不等的话下一条 broadcast 会打断 A11y 当前状态 await sleep(idx === 0 ? 14000 : 9000); // 第一条带 quote+@ 多花时间 }; try { const sessionId = `wx-${talker}`; const messages = buildHermesMessages({ type: replyType, talker, displayName, content, senderName, history, raw: msg.raw, }); log('[HERMES] Streaming for', talker, 'turns=' + messages.length); const result = await callHermesRunStream(messages, sessionId, onUtterance); log('[HERMES] Run done. usage=' + JSON.stringify(result.usage || {})); this.state.markReplied(msgId, talker); } catch (e) { log('[STREAM ERROR]', e.message); } } async start() { if (this.running) return; this.running = true; log('=== Auto-Reply Bot Started ==='); log('Hermes:', `${CONFIG.hermesHost}:${CONFIG.hermesPort}`); log('Config:', JSON.stringify({ myWxid: CONFIG.myWxid ? '***' : '(not set)', myNickname: CONFIG.myNickname || '(not set)', replyInPrivateChat: CONFIG.replyInPrivateChat, replyAtInGroup: CONFIG.replyAtInGroup, replyQuoteInGroup: CONFIG.replyQuoteInGroup, historyContextSize: CONFIG.historyContextSize, notificationEnabled: CONFIG.notificationEnabled, })); // A11y service 健康检查 — 发送 broadcast 必须依赖它 const a11y = await checkAccessibilityServiceEnabled(); if (!a11y.enabled) { log('[A11Y] NOT enabled (raw:', a11y.raw, '), trying to enable...'); const en = await enableAccessibilityService(); if (en.ok) { log('[A11Y] Enabled OK'); } else { log('[A11Y] FAILED to enable:', en.error || '(unknown)'); log('[A11Y] Manual fix: adb shell "su -c \'settings put secure enabled_accessibility_services com.hermes.wechat.xposed/com.hermes.wechat.xposed.accessibility.HermesAccessibilityService\'"'); } } else { log('[A11Y] HermesAccessibilityService is enabled'); } // 启动 tracker(sqlcipher DB 轮询,唯一稳定的消息检测方式) this.tracker.on('message', msg => this.handleMessage(msg)); this.tracker.on('error', err => log('[TRACKER ERROR]', err.message)); await this.tracker.init(); this.tracker.startPolling(3000); log('[TRACKER] DB polling started (sqlcipher)'); // 启动通知监听(息屏兜底:检测到通知时点亮屏幕+启动微信) if (CONFIG.notificationEnabled) { this.notifLoop = pollNotifications(this); } process.on('SIGINT', () => this.stop()); process.on('SIGTERM', () => this.stop()); } stop() { if (!this.running) return; this.running = false; this.tracker.stopPolling(); log('=== Auto-Reply Bot Stopped ==='); process.exit(0); } } // ═══════════════════════════════════════════════════════════════ // 入口 // ═══════════════════════════════════════════════════════════════ if (require.main === module) { if (process.env.MY_WXID) CONFIG.myWxid = process.env.MY_WXID; if (process.env.MY_NICKNAME) CONFIG.myNickname = process.env.MY_NICKNAME; if (!CONFIG.myWxid || !CONFIG.myNickname) { console.error(''); console.error('ERROR: 请设置 MY_WXID 和 MY_NICKNAME 环境变量'); console.error(''); console.error('用法示例:'); console.error(' MY_WXID=wxid_xxxxxx MY_NICKNAME=小明 nohup node auto-reply.js &'); console.error(''); process.exit(1); } const bot = new AutoReplyBot(); bot.start(); } module.exports = { AutoReplyBot, hermesReplyGenerator, callHermesApi, CONFIG };