feat: sys-call framework + channel.weixin stub (v1.4.0)

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
stswangzhiping
2026-05-14 21:47:27 +08:00
parent 8cebf062a2
commit 3dba9fde32
4 changed files with 163 additions and 1 deletions

108
lib/sys-call.js Normal file
View File

@@ -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: '<UUID>',
* 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 };