提交qiming-noVNC
This commit is contained in:
296
qiming-noVNC/app/ime.js
Normal file
296
qiming-noVNC/app/ime.js
Normal file
@@ -0,0 +1,296 @@
|
||||
/*
|
||||
* 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;
|
||||
|
||||
Reference in New Issue
Block a user