297 lines
8.1 KiB
JavaScript
297 lines
8.1 KiB
JavaScript
/*
|
||
* 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<void>}
|
||
*/
|
||
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;
|
||
|