Files
terminal-lab/terminal/apps/miniprogram/utils/wxTransport.js
douboer@gmail.com 3b7c1d558a first commit
2026-03-03 13:23:14 +08:00

202 lines
6.7 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.

/**
* 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 };