RCoder Agent Runner - 远程桌面服务
本目录包含 rcoder-agent-runner 容器的配置文件,提供完整的远程桌面解决方案,包括 VNC 远程桌面、本地输入法透传、音频流传输等功能。
📋 目录
架构概览
┌─────────────────────────────────────────────────────────────────────────┐
│ 用户浏览器 │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ ┌──────────────────┐ ┌──────────────────┐ ┌──────────────────┐ │
│ │ noVNC Canvas │ │ IME 隐藏输入框 │ │ Audio Player │ │
│ │ (VNC 画面) │ │ (输入法捕获) │ │ (音频播放) │ │
│ └────────┬─────────┘ └────────┬─────────┘ └────────┬─────────┘ │
│ │ │ │ │
│ │ ws://host:6080 │ ws://host:6091 │ ws://host:6089│
│ │ (VNC 协议) │ (文本输入) │ (Opus 音频) │
│ ▼ ▼ ▼ │
└────────────┬─────────────────────┬─────────────────────┬───────────────┘
│ │ │
┌────────────┴─────────────────────┴─────────────────────┴───────────────┐
│ 容器内部 │
├────────────────────────────────────────────────────────────────────────┤
│ │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ ┌────────────┐ │
│ │ noVNC Proxy │ │ IME Server │ │ Audio Server │ │ x11vnc │ │
│ │ Port: 6080 │ │ Port: 6091 │ │ Port: 6089 │ │ Port: 5900 │ │
│ └──────────────┘ └──────────────┘ └──────────────┘ └────────────┘ │
│ │ │ │ │ │
│ ▼ ▼ ▼ ▼ │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ X11 Display (:0) │ │
│ │ XFCE4 桌面环境 │ │
│ └─────────────────────────────────────────────────────────────────┘ │
│ │
└────────────────────────────────────────────────────────────────────────┘
服务端口
| 端口 | 服务 | 协议 | 说明 |
|---|---|---|---|
| 6080 | noVNC | WebSocket (VNC) | 远程桌面画面和键鼠输入 |
| 6091 | IME Server | WebSocket (JSON) | 本地输入法文本透传 |
| 6089 | Audio Server | WebSocket (Binary) | 音频流传输 (Opus 编码) |
| 6090 | Audio HTTP | HTTP | 音频播放器静态页面 |
| 5900 | x11vnc | VNC | 原始 VNC 协议(内部使用) |
VNC 远程桌面
功能说明
- 基于 noVNC: 纯 Web VNC 客户端,无需安装插件
- 分辨率: 默认 1280x800,可通过 URL 参数调整
- 自动连接: 支持
autoconnect=true参数 - 自适应缩放: 支持
resize=scale参数
前端接入
方式一:直接嵌入 iframe
<iframe
src="http://容器IP:6080/vnc.html?autoconnect=true&resize=scale"
width="100%"
height="600"
frameborder="0">
</iframe>
方式二:使用 noVNC JavaScript API
<script src="http://容器IP:6080/core/rfb.js" type="module"></script>
<div id="vnc-container"></div>
<script type="module">
import RFB from 'http://容器IP:6080/core/rfb.js';
const rfb = new RFB(
document.getElementById('vnc-container'),
`ws://容器IP:6080`
);
rfb.scaleViewport = true;
rfb.resizeSession = true;
rfb.addEventListener('connect', () => console.log('VNC connected'));
rfb.addEventListener('disconnect', () => console.log('VNC disconnected'));
</script>
URL 参数
| 参数 | 说明 | 示例 |
|---|---|---|
autoconnect |
自动连接 | true |
resize |
缩放模式 | scale / remote |
quality |
画面质量 | 0-9 (9 最高) |
compression |
压缩级别 | 0-9 (9 最高压缩) |
本地输入法透传
功能说明
允许用户使用宿主机的输入法(如搜狗输入法、微软拼音等)直接输入到远程桌面,无需在远程桌面内切换输入法。
工作原理
1. 用户点击 VNC 画面 → 焦点切换到隐藏输入框
2. 用户使用本地输入法输入(如输入 "zhongguo" 选择 "中国")
3. 前端捕获 compositionend 事件,获取 "中国"
4. 通过 WebSocket 发送到 IME Server
5. IME Server 使用 xdotool 将文本输入到远程桌面当前焦点窗口
前端接入
方式一:使用内置脚本(推荐)
如果使用我们提供的 vnc.html,IME 透传已内置,无需额外配置。
方式二:手动集成
<!-- 1. 创建隐藏输入框 -->
<textarea id="ime-capture" style="position:fixed;left:-9999px;opacity:0;"></textarea>
<!-- 2. 建立 WebSocket 连接 -->
<script>
const imeWs = new WebSocket('ws://容器IP:6091');
// 连接状态
imeWs.onopen = () => console.log('IME connected');
imeWs.onclose = () => console.log('IME disconnected');
imeWs.onerror = (e) => console.error('IME error:', e);
// 响应处理
imeWs.onmessage = (event) => {
const data = JSON.parse(event.data);
if (data.status === 'ok') {
console.log('Text sent successfully');
} else if (data.status === 'error') {
console.error('Error:', data.message);
}
};
// 3. 监听输入法事件
const imeInput = document.getElementById('ime-capture');
imeInput.addEventListener('compositionend', (e) => {
if (e.data && imeWs.readyState === WebSocket.OPEN) {
imeWs.send(JSON.stringify({
type: 'text',
text: e.data
}));
imeInput.value = '';
}
});
// 4. VNC 画面点击时聚焦到隐藏输入框
document.getElementById('vnc-container').addEventListener('click', () => {
imeInput.focus();
});
</script>
WebSocket 协议
发送消息格式
{
"type": "text",
"text": "要输入的文本",
"method": "xdotool" // 可选: "xdotool"(默认) 或 "clipboard"
}
响应消息格式
// 成功
{ "status": "ok" }
// 失败
{ "status": "error", "message": "错误信息" }
// 心跳响应
{ "type": "pong" }
心跳保活
// 发送
{ "type": "ping" }
// 响应
{ "type": "pong" }
音频流传输
功能说明
将远程桌面的音频(如视频播放、系统提示音)通过 WebSocket 实时传输到浏览器播放。
技术栈
- 采集: PulseAudio 虚拟声卡
- 编码: Opus (48kHz, 立体声)
- 传输: WebSocket (二进制)
- 播放: Web Audio API
前端接入
方式一:嵌入音频播放器页面
<iframe
src="http://容器IP:6090/index.html"
width="300"
height="50"
frameborder="0">
</iframe>
方式二:直接使用 pcmflux 客户端
<!-- 引入 pcmflux 客户端库 -->
<script src="http://容器IP:6090/pcmflux.js"></script>
<button id="audio-toggle">🔊 开启音频</button>
<script>
let audioContext = null;
let audioPlayer = null;
document.getElementById('audio-toggle').addEventListener('click', async () => {
if (!audioContext) {
// 初始化 Audio Context (需要用户交互)
audioContext = new AudioContext({ sampleRate: 48000 });
// 连接 WebSocket
const ws = new WebSocket('ws://容器IP:6089');
ws.binaryType = 'arraybuffer';
// 创建 Opus 解码器和播放器
audioPlayer = new PCMFluxPlayer(audioContext, ws);
audioPlayer.start();
document.getElementById('audio-toggle').textContent = '🔇 关闭音频';
} else {
// 关闭音频
audioPlayer.stop();
audioContext.close();
audioContext = null;
document.getElementById('audio-toggle').textContent = '🔊 开启音频';
}
});
</script>
WebSocket 协议
音频数据通过 WebSocket 以二进制帧传输,格式为 Opus 编码的音频包。
const ws = new WebSocket('ws://容器IP:6089');
ws.binaryType = 'arraybuffer';
ws.onmessage = (event) => {
const opusData = new Uint8Array(event.data);
// 使用 Opus 解码器解码后播放
decodeAndPlay(opusData);
};
注意事项
Important
浏览器安全限制: 由于浏览器自动播放策略,音频播放必须由用户交互(如点击按钮)触发。
前端集成指南
完整示例
<!DOCTYPE html>
<html>
<head>
<title>远程桌面</title>
<style>
body { margin: 0; font-family: Arial, sans-serif; }
#container { display: flex; flex-direction: column; height: 100vh; }
#toolbar { padding: 10px; background: #333; color: white; display: flex; gap: 10px; align-items: center; }
#vnc-wrapper { flex: 1; position: relative; }
#vnc-container { width: 100%; height: 100%; }
#ime-capture { position: fixed; left: -9999px; opacity: 0; }
.status { padding: 5px 10px; border-radius: 4px; font-size: 12px; }
.connected { background: #4CAF50; }
.disconnected { background: #f44336; }
</style>
</head>
<body>
<div id="container">
<!-- 工具栏 -->
<div id="toolbar">
<span>远程桌面</span>
<span id="vnc-status" class="status disconnected">VNC: 未连接</span>
<span id="ime-status" class="status disconnected">输入法: 未连接</span>
<button id="audio-btn">🔊 开启音频</button>
</div>
<!-- VNC 画面 -->
<div id="vnc-wrapper">
<div id="vnc-container"></div>
</div>
</div>
<!-- IME 隐藏输入框 -->
<textarea id="ime-capture"></textarea>
<script type="module">
// ========== 配置 ==========
const HOST = location.hostname || 'localhost';
const VNC_PORT = 6080;
const IME_PORT = 6091;
const AUDIO_PORT = 6089;
// ========== VNC 连接 ==========
import RFB from `http://${HOST}:${VNC_PORT}/core/rfb.js`;
const rfb = new RFB(
document.getElementById('vnc-container'),
`ws://${HOST}:${VNC_PORT}`
);
rfb.scaleViewport = true;
rfb.addEventListener('connect', () => {
document.getElementById('vnc-status').textContent = 'VNC: 已连接';
document.getElementById('vnc-status').className = 'status connected';
});
rfb.addEventListener('disconnect', () => {
document.getElementById('vnc-status').textContent = 'VNC: 已断开';
document.getElementById('vnc-status').className = 'status disconnected';
});
// ========== IME 输入法透传 ==========
const imeWs = new WebSocket(`ws://${HOST}:${IME_PORT}`);
const imeInput = document.getElementById('ime-capture');
imeWs.onopen = () => {
document.getElementById('ime-status').textContent = '输入法: 已连接';
document.getElementById('ime-status').className = 'status connected';
};
imeWs.onclose = () => {
document.getElementById('ime-status').textContent = '输入法: 已断开';
document.getElementById('ime-status').className = 'status disconnected';
};
imeInput.addEventListener('compositionend', (e) => {
if (e.data && imeWs.readyState === WebSocket.OPEN) {
imeWs.send(JSON.stringify({ type: 'text', text: e.data }));
imeInput.value = '';
}
});
document.getElementById('vnc-container').addEventListener('click', () => {
imeInput.focus();
});
// ========== 音频 ==========
let audioWs = null;
document.getElementById('audio-btn').addEventListener('click', () => {
if (!audioWs) {
audioWs = new WebSocket(`ws://${HOST}:${AUDIO_PORT}`);
audioWs.binaryType = 'arraybuffer';
// 音频处理逻辑...
document.getElementById('audio-btn').textContent = '🔇 关闭音频';
} else {
audioWs.close();
audioWs = null;
document.getElementById('audio-btn').textContent = '🔊 开启音频';
}
});
</script>
</body>
</html>
API 接口文档
VNC 服务 (Port 6080)
| 端点 | 说明 |
|---|---|
GET /vnc.html |
noVNC 客户端页面 |
GET /vnc_lite.html |
noVNC 精简版页面 |
GET /core/rfb.js |
noVNC RFB 模块 |
WS / |
VNC WebSocket 连接 |
IME 服务 (Port 6091)
| 端点 | 协议 | 说明 |
|---|---|---|
WS / |
WebSocket | 文本输入通道 |
消息格式:
// 请求
interface IMERequest {
type: 'text' | 'ping';
text?: string; // type='text' 时必填
method?: 'xdotool' | 'clipboard'; // 可选,默认 xdotool
}
// 响应
interface IMEResponse {
status?: 'ok' | 'error';
message?: string; // status='error' 时的错误信息
type?: 'pong'; // 心跳响应
}
音频服务 (Port 6089/6090)
| 端点 | 协议 | 说明 |
|---|---|---|
GET :6090/ |
HTTP | 音频播放器页面 |
GET :6090/pcmflux.js |
HTTP | PCMFlux 客户端库 |
WS :6089/ |
WebSocket (Binary) | 音频流 (Opus) |
文件说明
| 文件 | 说明 |
|---|---|
Dockerfile |
容器构建配置 |
start-up.sh |
容器启动脚本,启动所有服务 |
ime_server.py |
IME 输入法透传后端服务 |
ime_passthrough.js |
IME 输入法透传前端脚本 |
audio_server.py |
音频流传输服务 |
audio_static/ |
音频播放器静态文件 |
常见问题
Q: 中文输入不生效?
A: 确保点击了 VNC 画面中的文本输入框,且 IME WebSocket 已连接。
Q: 音频没有声音?
A: 浏览器需要用户交互才能播放音频,请点击"开启音频"按钮。
Q: VNC 画面模糊?
A: 尝试在 URL 中添加 ?quality=9 参数提高画质。
Q: 如何禁用 IME 透传?
A: 启动容器时设置 ENABLE_IME_PASSTHROUGH=false 环境变量。
联系方式
如有问题,请联系后端开发团队。