From 3dba9fde329ab025a886d6943ba383dba10b6151 Mon Sep 17 00:00:00 2001 From: stswangzhiping <59632378+stswangzhiping@users.noreply.github.com> Date: Thu, 14 May 2026 21:47:27 +0800 Subject: [PATCH] feat: sys-call framework + channel.weixin stub (v1.4.0) Co-authored-by: Cursor --- lib/channel/weixin.js | 50 +++++++++++++++++++ lib/client.js | 4 ++ lib/sys-call.js | 108 ++++++++++++++++++++++++++++++++++++++++++ package.json | 2 +- 4 files changed, 163 insertions(+), 1 deletion(-) create mode 100644 lib/channel/weixin.js create mode 100644 lib/sys-call.js diff --git a/lib/channel/weixin.js b/lib/channel/weixin.js new file mode 100644 index 0000000..9ba85a9 --- /dev/null +++ b/lib/channel/weixin.js @@ -0,0 +1,50 @@ +'use strict'; + +/** + * channel.weixin — WeChat login via box-side WeChat SDK. + * + * method: login + * params : { callId, timeout, emit } + * returns: abort function (called on cancel) + * + * emit(msg) sends a sys-call reply back to VPS: + * action:'event', event:'qrcode', data:{ url, expire, index } + * action:'progress', event:'scanned', data:{ status:'waiting_confirm' } + * action:'finish', event:'success', data:{ wxid } + * action:'finish', event:'failed', code, message + * + * TODO: integrate with a concrete WeChat SDK (wechaty / itchat / custom binary). + */ + +const log = require('../logger'); + +function login({ callId, timeout = 180, emit }) { + log.info('weixin', `login requested callId=${callId} timeout=${timeout}`); + + // TODO: start WeChat SDK, get QR code, watch for scan / confirm / expire + // Example skeleton: + // + // const bot = startWechatyBot(); + // + // bot.on('scan', (url, status) => { + // emit({ action: 'event', event: 'qrcode', data: { url, expire: 30, index: ++qrIndex } }); + // }); + // bot.on('login', (user) => { + // emit({ action: 'finish', event: 'success', data: { wxid: user.id } }); + // }); + // bot.on('error', (err) => { + // emit({ action: 'finish', event: 'failed', code: 1001, message: err.message }); + // }); + // bot.start(); + // + // return () => bot.stop(); // ← abort function + + // Temporary stub: immediately report not implemented + emit({ action: 'finish', event: 'failed', code: 501, message: 'weixin SDK not implemented' }); + + return () => { + log.info('weixin', `login cancelled callId=${callId}`); + }; +} + +module.exports = { login }; diff --git a/lib/client.js b/lib/client.js index 36f9748..e34bed7 100644 --- a/lib/client.js +++ b/lib/client.js @@ -17,6 +17,7 @@ const { ProvisionManager } = require('./provisioning'); const { BtMonitor } = require('./bt-monitor'); const { hasInternet, hasWiredInternetProbe, getLocalIps, getLocalNetworks } = require('./network'); const { applyFullProviderFromVps, removeProviderByName, refreshModelsIfChanged, isFullProvider } = require('./openclaw-provider'); +const sysCall = require('./sys-call'); const led = require('./led'); const MAX_BACKOFF_MS = 60_000; @@ -403,6 +404,9 @@ class ClawClient { case 'upgrade': this._handleUpgrade(msg); break; + case 'sys-call': + sysCall.handle(msg, (reply) => this._send({ type: 'sys-call', ...reply })); + break; case 'headscale_logout': headscale.logout().catch(e => log.error('headscale', 'logout 失败:', e.message)); break; diff --git a/lib/sys-call.js b/lib/sys-call.js new file mode 100644 index 0000000..9e1ef57 --- /dev/null +++ b/lib/sys-call.js @@ -0,0 +1,108 @@ +'use strict'; + +/** + * sys-call dispatcher. + * + * Routes incoming sys-call messages (action='request'|'cancel') from VPS + * to the correct channel handler, and wires the emit callback so that + * handler replies are sent back over the WebSocket. + * + * Message envelope (shared by all sys-call messages): + * { + * type: 'sys-call', + * id: '', + * api: 'channel.weixin', + * method: 'login', + * action: 'request' | 'reply' | 'notify' | 'progress' | 'event' | 'finish' | 'cancel', + * event: '', + * code: 0, + * message: '', + * data: {} + * } + */ + +const log = require('./logger'); + +// ── channel handlers ────────────────────────────────────────────────────────── +const handlers = { + 'channel.weixin': require('./channel/weixin'), +}; + +// ── running tasks: callId → abort() ────────────────────────────────────────── +const running = new Map(); + +/** + * Handle an incoming sys-call message from VPS. + * + * @param {object} msg - parsed message object + * @param {function} send - send(replyPayload) → forwards to VPS over WS; + * caller prepends { type:'sys-call' } + */ +function handle(msg, send) { + const { id: callId, api, method, action } = msg; + + if (!callId) { + log.warn('sys-call', 'missing id field, ignoring'); + return; + } + + // ── cancel ──────────────────────────────────────────────────────────────── + if (action === 'cancel') { + const abort = running.get(callId); + if (abort) { + log.info('sys-call', `cancel callId=${callId}`); + try { abort(); } catch (e) { log.warn('sys-call', `abort error: ${e.message}`); } + running.delete(callId); + } else { + log.debug('sys-call', `cancel for unknown/finished callId=${callId}`); + } + return; + } + + // ── request ─────────────────────────────────────────────────────────────── + if (action !== 'request') { + log.warn('sys-call', `unexpected action=${action} callId=${callId}`); + return; + } + + const handler = handlers[api]; + if (!handler || typeof handler[method] !== 'function') { + log.warn('sys-call', `unknown api=${api} method=${method}`); + send({ + id: callId, api, method, + action: 'finish', event: 'failed', + code: 404, message: `unknown api: ${api}.${method}`, + }); + return; + } + + // emit: wraps handler replies, cleans up running map on finish + const emit = (payload) => { + send({ id: callId, api, method, event: '', code: 0, message: '', ...payload }); + if (payload.action === 'finish') { + running.delete(callId); + log.info('sys-call', `finished callId=${callId}`); + } + }; + + log.info('sys-call', `start api=${api} method=${method} callId=${callId}`); + + let abort; + try { + abort = handler[method]({ callId, ...(msg.data || {}), emit }); + } catch (e) { + log.error('sys-call', `handler threw: ${e.message}`); + send({ + id: callId, api, method, + action: 'finish', event: 'failed', + code: 500, message: e.message, + }); + return; + } + + if (typeof abort === 'function') { + running.set(callId, abort); + } +} + +module.exports = { handle }; diff --git a/package.json b/package.json index 9284418..7f84e99 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "clawd", - "version": "1.3.9", + "version": "1.4.0", "description": "Claw Box daemon - connects local Linux box to claw.cutos.ai via WebSocket", "main": "lib/client.js", "bin": {