添加qiming-rcoder模块

This commit is contained in:
Codex
2026-06-01 13:54:52 +08:00
parent 8092c4b1f8
commit 4b1a580132
539 changed files with 151650 additions and 0 deletions

View File

@@ -0,0 +1,260 @@
#!/usr/bin/env python3
"""
ime_server.py - 本地输入法透传服务
接收浏览器发送的文本,使用 xdotool 输入到当前焦点窗口
工作原理:
1. 用户在浏览器中使用本地输入法(如搜狗输入法)输入中文
2. 前端通过 WebSocket 发送完整的文本到此服务
3. 此服务使用 xdotool 将文本输入到远程桌面的当前焦点窗口
端口: 6091 (可通过 IME_PORT 环境变量配置)
"""
import asyncio
import json
import subprocess
import os
import signal
import sys
from datetime import datetime
# 尝试导入 websockets如果失败则提供友好提示
try:
from websockets.server import serve
from websockets.exceptions import ConnectionClosed
except ImportError:
print("[IME] Error: websockets module not found. Install with: pip3 install websockets")
sys.exit(1)
def log(level: str, message: str):
"""格式化日志输出"""
timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
print(f"[{timestamp}] [{level}] [IME] {message}", flush=True)
def sanitize_text(text: str) -> str:
"""
清理文本,防止命令注入和异常字符
安全检查:
1. 长度限制 (1000 字符)
2. 过滤危险控制字符 (NULL, ESC)
3. 使用 -- 参数分隔符防止注入
Args:
text: 待验证的文本
Returns:
清理后的文本
Raises:
ValueError: 如果文本包含危险内容
"""
# 1. 长度限制
if len(text) > 1000:
raise ValueError("Text too long (max 1000 chars)")
# 2. 过滤危险控制字符
dangerous_chars = ['\x00', '\x1b'] # NULL 和 ESC
if any(c in text for c in dangerous_chars):
raise ValueError("Text contains dangerous control characters")
return text
def type_text_xdotool(text: str) -> tuple[bool, str]:
"""
使用 xdotool 输入文本
Args:
text: 要输入的文本
Returns:
(success, error_message)
"""
try:
env = {**os.environ, 'DISPLAY': ':0'}
# 使用 xdotool type 输入文本
# --clearmodifiers: 清除修饰键状态(避免 Ctrl/Alt 等键干扰)
# --delay: 每个字符之间的延迟(毫秒),帮助应用程序处理
result = subprocess.run(
['xdotool', 'type', '--clearmodifiers', '--delay', '10', '--', text],
env=env,
capture_output=True,
text=True,
timeout=10
)
if result.returncode == 0:
return True, ""
else:
return False, result.stderr.strip()
except subprocess.TimeoutExpired:
return False, "xdotool timeout"
except FileNotFoundError:
return False, "xdotool not found"
except Exception as e:
return False, str(e)
def type_text_clipboard(text: str) -> tuple[bool, str]:
"""
备用方案: 通过剪贴板粘贴文本
当 xdotool type 对某些字符不支持时使用
Args:
text: 要输入的文本
Returns:
(success, error_message)
"""
try:
env = {**os.environ, 'DISPLAY': ':0'}
# 1. 将文本写入剪贴板
process = subprocess.Popen(
['xclip', '-selection', 'clipboard'],
stdin=subprocess.PIPE,
env=env
)
process.communicate(input=text.encode('utf-8'), timeout=5)
if process.returncode != 0:
return False, "Failed to copy to clipboard"
# 2. 模拟 Ctrl+V 粘贴
result = subprocess.run(
['xdotool', 'key', '--clearmodifiers', 'ctrl+v'],
env=env,
capture_output=True,
text=True,
timeout=5
)
if result.returncode == 0:
return True, ""
else:
return False, result.stderr.strip()
except subprocess.TimeoutExpired:
return False, "Clipboard operation timeout"
except FileNotFoundError as e:
return False, f"Required tool not found: {e}"
except Exception as e:
return False, str(e)
async def handle_client(websocket):
"""处理来自前端的 WebSocket 连接"""
client_addr = websocket.remote_address
log("INFO", f"Client connected: {client_addr}")
try:
async for message in websocket:
try:
data = json.loads(message)
msg_type = data.get('type', '')
if msg_type == 'text':
text = data.get('text', '')
method = data.get('method', 'xdotool') # 默认使用 xdotool
if not text:
await websocket.send(json.dumps({
'status': 'error',
'message': 'Empty text'
}))
continue
# 安全验证
try:
text = sanitize_text(text)
except ValueError as e:
log("WARN", f"Text validation failed: {e}")
await websocket.send(json.dumps({
'status': 'error',
'message': str(e)
}))
continue
# 根据方法选择输入方式
if method == 'clipboard':
success, error = type_text_clipboard(text)
else:
success, error = type_text_xdotool(text)
if success:
log("INFO", f"Typed ({method}): {text[:50]}{'...' if len(text) > 50 else ''}")
await websocket.send(json.dumps({'status': 'ok'}))
else:
log("ERROR", f"Failed to type: {error}")
await websocket.send(json.dumps({
'status': 'error',
'message': error
}))
elif msg_type == 'ping':
# 心跳检测
await websocket.send(json.dumps({'type': 'pong'}))
else:
log("WARN", f"Unknown message type: {msg_type}")
await websocket.send(json.dumps({
'status': 'error',
'message': f'Unknown type: {msg_type}'
}))
except json.JSONDecodeError:
log("ERROR", f"Invalid JSON: {message[:100]}")
await websocket.send(json.dumps({
'status': 'error',
'message': 'Invalid JSON'
}))
except ConnectionClosed:
log("INFO", f"Client disconnected: {client_addr}")
except Exception as e:
log("ERROR", f"Client error: {e}")
finally:
log("INFO", f"Connection closed: {client_addr}")
async def main():
"""主函数"""
port = int(os.environ.get('IME_PORT', 6091))
host = os.environ.get('IME_HOST', '0.0.0.0')
log("INFO", f"Starting IME input server on {host}:{port}...")
log("INFO", "Protocol: WebSocket")
log("INFO", "Message format: {\"type\": \"text\", \"text\": \"要输入的文本\"}")
# 优雅关闭处理
stop = asyncio.Event()
def signal_handler():
log("INFO", "Received shutdown signal")
stop.set()
loop = asyncio.get_running_loop()
for sig in (signal.SIGTERM, signal.SIGINT):
loop.add_signal_handler(sig, signal_handler)
async with serve(handle_client, host, port):
log("INFO", f"IME server is ready, listening on ws://{host}:{port}")
await stop.wait()
log("INFO", "IME server stopped")
if __name__ == '__main__':
try:
asyncio.run(main())
except KeyboardInterrupt:
log("INFO", "Server interrupted")
except Exception as e:
log("ERROR", f"Server error: {e}")
sys.exit(1)