/*
* noVNC: HTML5 VNC client
* IME (Input Method Editor) passthrough module
*
* 输入法透传模块 - 用于将本地输入法的输入传送到远程桌面
*
* 功能:
* - 通过 WebSocket 连接 IME 服务
* - 监听输入法事件(compositionend)
* - 将输入的文本发送到远程桌面
*/
import * as Log from '../core/util/logging.js';
// 输入法管理器类
class IMEManager {
constructor() {
// WebSocket 连接
this.ws = null;
this.connected = false;
// 隐藏输入框元素
this.inputElement = null;
// 回调函数
this.onStatusChange = null;
// 绑定事件处理函数(用于移除监听器)
this._boundCompositionEnd = this._handleCompositionEnd.bind(this);
this._boundInput = this._handleInput.bind(this);
}
/**
* 构建 IME WebSocket URL
* @param {string} baseUrl - 基础 URL (如 http://127.0.0.1:8088)
* @param {string} projectId - 项目 ID
* @param {string} userId - 用户 ID (可选,调试模式使用)
* @param {boolean} debugMode - 是否调试模式
* @returns {string} WebSocket URL
*/
buildWsUrl(baseUrl, projectId, userId = null, debugMode = false) {
// 将 http/https 转换为 ws/wss
const wsUrl = baseUrl
.replace(/^http/, 'ws')
.replace(/\/+$/, '');
// 调试模式:包含 user_id
// 正式模式:不包含 user_id
if (debugMode && userId) {
return `ws://192.168.1.34:8088/computer/ime/${userId}/${projectId}/connect`;
} else {
return `${wsUrl}/computer/ime/${projectId}/connect`;
}
}
/**
* 连接 IME 服务
* @param {string} wsUrl - WebSocket URL
* @returns {Promise}
*/
async connect(wsUrl) {
if (this.connected) {
Log.Warn('[IME] 已经连接,请先断开');
return;
}
try {
this._updateStatus('connecting', 'IME 连接中...');
Log.Info('[IME] 连接 IME 服务: ' + wsUrl);
// 创建隐藏输入框
this._createInputElement();
// 连接 WebSocket
this.ws = new WebSocket(wsUrl);
this.ws.onopen = () => {
Log.Info('[IME] WebSocket 已连接');
this.connected = true;
this._updateStatus('connected', 'IME 已连接');
// 启用输入监听
this._enableInputListeners();
};
this.ws.onmessage = (event) => {
try {
const response = JSON.parse(event.data);
if (response.status === 'ok') {
Log.Debug('[IME] 文本发送成功');
} else {
Log.Warn('[IME] 服务器响应: ' + response.message);
}
} catch (err) {
Log.Error('[IME] 响应解析失败: ' + err);
}
};
this.ws.onerror = (err) => {
Log.Error('[IME] WebSocket 错误: ' + err);
this._updateStatus('error', 'IME 连接错误');
};
this.ws.onclose = () => {
Log.Info('[IME] WebSocket 已关闭');
this.connected = false;
this._updateStatus('disconnected', 'IME 已断开');
this._disableInputListeners();
};
} catch (err) {
Log.Error('[IME] 连接失败: ' + err);
this._updateStatus('error', 'IME 启动失败');
throw err;
}
}
/**
* 断开 IME 服务
*/
disconnect() {
if (this.ws) {
this.ws.close();
this.ws = null;
}
this.connected = false;
this._disableInputListeners();
this._updateStatus('disconnected', 'IME 已停止');
Log.Info('[IME] IME 服务已断开');
}
/**
* 发送文本到远程桌面
* @param {string} text - 要发送的文本
*/
sendText(text) {
if (!this.connected || !this.ws) {
Log.Warn('[IME] 未连接,无法发送文本');
return;
}
if (!text || text.length === 0) {
return;
}
const message = JSON.stringify({
type: 'text',
text: text,
method: 'xdotool' // 使用 xdotool 输入方法
});
this.ws.send(message);
Log.Debug('[IME] 发送文本: ' + text);
}
/**
* 激活输入(聚焦到隐藏输入框)
* 当用户点击 VNC 画面时调用
*/
activateInput() {
if (this.inputElement && this.connected) {
this.inputElement.focus();
}
}
/**
* 创建隐藏输入框
* @private
*/
_createInputElement() {
if (this.inputElement) {
return;
}
// 创建隐藏的输入框,用于捕获输入法事件
this.inputElement = document.createElement('input');
this.inputElement.type = 'text';
this.inputElement.id = 'noVNC_ime_input';
this.inputElement.autocomplete = 'off';
this.inputElement.autocapitalize = 'off';
this.inputElement.spellcheck = false;
// 设置样式 - 隐藏但可聚焦
this.inputElement.style.cssText = `
position: fixed;
top: -9999px;
left: -9999px;
opacity: 0;
pointer-events: none;
width: 1px;
height: 1px;
`;
document.body.appendChild(this.inputElement);
Log.Debug('[IME] 隐藏输入框已创建');
}
/**
* 启用输入监听器
* @private
*/
_enableInputListeners() {
if (!this.inputElement) {
return;
}
// 监听输入法完成事件(如选择中文候选词)
this.inputElement.addEventListener('compositionend', this._boundCompositionEnd);
// 监听直接输入事件(非输入法输入)
this.inputElement.addEventListener('input', this._boundInput);
Log.Debug('[IME] 输入监听器已启用');
}
/**
* 禁用输入监听器
* @private
*/
_disableInputListeners() {
if (!this.inputElement) {
return;
}
this.inputElement.removeEventListener('compositionend', this._boundCompositionEnd);
this.inputElement.removeEventListener('input', this._boundInput);
Log.Debug('[IME] 输入监听器已禁用');
}
/**
* 处理输入法完成事件
* @param {CompositionEvent} event
* @private
*/
_handleCompositionEnd(event) {
const text = event.data;
if (text && text.length > 0) {
Log.Debug('[IME] 输入法完成: ' + text);
this.sendText(text);
// 清空输入框
if (this.inputElement) {
this.inputElement.value = '';
}
}
}
/**
* 处理直接输入事件
* @param {InputEvent} event
* @private
*/
_handleInput(event) {
// 如果正在进行输入法输入,忽略
if (event.isComposing) {
return;
}
const text = event.target.value;
if (text && text.length > 0) {
Log.Debug('[IME] 直接输入: ' + text);
this.sendText(text);
event.target.value = '';
}
}
/**
* 更新状态并触发回调
* @param {string} status - 状态 (connecting/connected/disconnected/error)
* @param {string} message - 状态消息
* @private
*/
_updateStatus(status, message) {
if (this.onStatusChange) {
this.onStatusChange(status, message);
}
}
/**
* 销毁 IME 管理器
*/
destroy() {
this.disconnect();
if (this.inputElement && this.inputElement.parentNode) {
this.inputElement.parentNode.removeChild(this.inputElement);
this.inputElement = null;
}
}
}
// 导出单例
const imeManager = new IMEManager();
export default imeManager;