202 lines
6.7 KiB
JavaScript
202 lines
6.7 KiB
JavaScript
/**
|
||
* WxTransport — 微信小程序 wx.connectSocket 实现的 TerminalTransport。
|
||
*
|
||
* 帧协议与 Web 端 GatewayTransport 完全一致(§8.2):
|
||
* 入站:{ type: 'init'|'stdin'|'resize'|'control', ... }
|
||
* 出站:{ type: 'stdout'|'stderr'|'control'|'error', ... }
|
||
*
|
||
* 使用方式:
|
||
* const t = new WxTransport({ gatewayUrl, gatewayToken });
|
||
* const off = t.on(event => { ... });
|
||
* await t.connect({ host, port, username, password });
|
||
* t.send("ls -la\r");
|
||
* t.resize(80, 24);
|
||
* t.disconnect();
|
||
*/
|
||
|
||
const { sanitizeTerminalOutput } = require('../../../packages/terminal-core/dist-miniprogram/index');
|
||
|
||
function buildWsUrl(rawUrl, token) {
|
||
let base = String(rawUrl || '').trim();
|
||
if (!base) throw new Error('WxTransport: 网关地址为空');
|
||
if (base.startsWith('http://')) base = 'ws://' + base.slice(7);
|
||
if (base.startsWith('https://')) base = 'wss://' + base.slice(8);
|
||
if (!base.startsWith('ws://') && !base.startsWith('wss://')) {
|
||
base = 'wss://' + base;
|
||
}
|
||
base = base.replace(/\/+$/, '');
|
||
return base + '/ws/terminal?token=' + encodeURIComponent(String(token || ''));
|
||
}
|
||
|
||
function WxTransport(options) {
|
||
const cfg = options || {};
|
||
this._gatewayUrl = cfg.gatewayUrl || '';
|
||
this._gatewayToken = cfg.gatewayToken || '';
|
||
|
||
this._state = 'idle'; // SessionState
|
||
this._sessionId = '';
|
||
this._socket = null; // wx.SocketTask
|
||
this._listeners = []; // Array<(event) => void>
|
||
this._pingTimer = null;
|
||
this._pingAt = 0;
|
||
}
|
||
|
||
// ── 接口实现 ──────────────────────────────────────────────────────────────────
|
||
|
||
/**
|
||
* 连接网关并发送 init 帧。
|
||
* 返回 Promise,在 control.connected 帧到达后 resolve,错误时 reject。
|
||
*/
|
||
WxTransport.prototype.connect = function(params) {
|
||
const self = this;
|
||
return new Promise(function(resolve, reject) {
|
||
if (self._state === 'connecting' || self._state === 'connected') {
|
||
resolve();
|
||
return;
|
||
}
|
||
|
||
self._state = 'connecting';
|
||
const sessionId = 'wx_' + Date.now() + '_' + Math.random().toString(36).slice(2);
|
||
self._sessionId = sessionId;
|
||
const url = buildWsUrl(self._gatewayUrl, self._gatewayToken);
|
||
|
||
const task = wx.connectSocket({
|
||
url: url,
|
||
protocols: ['v1.terminal'],
|
||
success: function() { /* socket created */ },
|
||
fail: function(err) {
|
||
self._state = 'error';
|
||
self._emit({ type: 'error', code: 'HOST_UNREACHABLE', message: JSON.stringify(err) });
|
||
reject(new Error('wx.connectSocket fail: ' + JSON.stringify(err)));
|
||
}
|
||
});
|
||
|
||
self._socket = task;
|
||
let resolved = false;
|
||
|
||
task.onOpen(function() {
|
||
// 发送 init 帧
|
||
self._sendFrame({ type: 'init', sessionId: sessionId, params: params });
|
||
// 启动心跳
|
||
self._startPing();
|
||
});
|
||
|
||
task.onMessage(function(res) {
|
||
let frame;
|
||
try {
|
||
frame = JSON.parse(res.data);
|
||
} catch(e) {
|
||
return;
|
||
}
|
||
|
||
if (frame.type === 'stdout' || frame.type === 'stderr') {
|
||
const raw = typeof frame.data === 'string' ? frame.data : '';
|
||
const sanitized = sanitizeTerminalOutput(raw);
|
||
if (sanitized) self._emit({ type: frame.type, data: sanitized });
|
||
} else if (frame.type === 'control') {
|
||
if (frame.action === 'connected') {
|
||
self._state = 'connected';
|
||
self._emit({ type: 'control', action: 'connected', data: frame.data });
|
||
if (!resolved) { resolved = true; resolve(); }
|
||
} else if (frame.action === 'disconnect') {
|
||
self._cleanup();
|
||
self._state = 'disconnected';
|
||
self._emit({ type: 'control', action: 'disconnect' });
|
||
} else if (frame.action === 'pong') {
|
||
self._emit({ type: 'control', action: 'pong' });
|
||
}
|
||
} else if (frame.type === 'error') {
|
||
self._state = 'error';
|
||
self._emit({ type: 'error', code: frame.code || 'INTERNAL_ERROR', message: frame.message || '' });
|
||
if (!resolved) { resolved = true; reject(new Error(frame.message)); }
|
||
}
|
||
});
|
||
|
||
task.onError(function(err) {
|
||
self._cleanup();
|
||
self._state = 'error';
|
||
self._emit({ type: 'error', code: 'HOST_UNREACHABLE', message: JSON.stringify(err) });
|
||
if (!resolved) { resolved = true; reject(new Error(JSON.stringify(err))); }
|
||
});
|
||
|
||
task.onClose(function() {
|
||
self._cleanup();
|
||
if (self._state !== 'disconnected' && self._state !== 'error') {
|
||
self._state = 'disconnected';
|
||
self._emit({ type: 'control', action: 'disconnect' });
|
||
}
|
||
});
|
||
});
|
||
};
|
||
|
||
WxTransport.prototype.send = function(data, meta) {
|
||
if (this._state !== 'connected') return;
|
||
this._sendFrame({ type: 'stdin', data: data, meta: meta });
|
||
};
|
||
|
||
WxTransport.prototype.resize = function(cols, rows) {
|
||
if (this._state !== 'connected') return;
|
||
this._sendFrame({ type: 'resize', cols: cols, rows: rows });
|
||
};
|
||
|
||
WxTransport.prototype.disconnect = function(reason) {
|
||
this._sendFrame({ type: 'control', action: 'disconnect', reason: reason || 'manual' });
|
||
this._cleanup();
|
||
this._state = 'disconnected';
|
||
};
|
||
|
||
WxTransport.prototype.on = function(listener) {
|
||
const self = this;
|
||
self._listeners.push(listener);
|
||
return function() {
|
||
const idx = self._listeners.indexOf(listener);
|
||
if (idx >= 0) self._listeners.splice(idx, 1);
|
||
};
|
||
};
|
||
|
||
WxTransport.prototype.getState = function() {
|
||
return this._state;
|
||
};
|
||
|
||
// ── 私有方法 ──────────────────────────────────────────────────────────────────
|
||
|
||
WxTransport.prototype._emit = function(event) {
|
||
for (let i = 0; i < this._listeners.length; i++) {
|
||
try { this._listeners[i](event); } catch(e) { /* 单个监听器异常不影响其他 */ }
|
||
}
|
||
};
|
||
|
||
WxTransport.prototype._sendFrame = function(frame) {
|
||
if (!this._socket) return;
|
||
try {
|
||
this._socket.send({ data: JSON.stringify(frame) });
|
||
} catch(e) { /* 忽略发送时 socket 已关闭 */ }
|
||
};
|
||
|
||
WxTransport.prototype._startPing = function() {
|
||
const self = this;
|
||
self._clearPing();
|
||
self._pingTimer = setInterval(function() {
|
||
if (self._state !== 'connected') return;
|
||
self._pingAt = Date.now();
|
||
self._sendFrame({ type: 'control', action: 'ping' });
|
||
}, 10000);
|
||
};
|
||
|
||
WxTransport.prototype._clearPing = function() {
|
||
if (this._pingTimer !== null) {
|
||
clearInterval(this._pingTimer);
|
||
this._pingTimer = null;
|
||
}
|
||
};
|
||
|
||
WxTransport.prototype._cleanup = function() {
|
||
this._clearPing();
|
||
if (this._socket) {
|
||
try { this._socket.close(); } catch(e) { /* ignore */ }
|
||
this._socket = null;
|
||
}
|
||
};
|
||
|
||
module.exports = { WxTransport: WxTransport };
|