Files
2026-06-01 13:54:52 +08:00

498 lines
16 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# RCoder Agent Runner - 远程桌面服务
本目录包含 `rcoder-agent-runner` 容器的配置文件,提供完整的远程桌面解决方案,包括 VNC 远程桌面、本地输入法透传、音频流传输等功能。
## 📋 目录
- [架构概览](#架构概览)
- [服务端口](#服务端口)
- [VNC 远程桌面](#vnc-远程桌面)
- [本地输入法透传](#本地输入法透传)
- [音频流传输](#音频流传输)
- [前端集成指南](#前端集成指南)
- [API 接口文档](#api-接口文档)
---
## 架构概览
```
┌─────────────────────────────────────────────────────────────────────────┐
│ 用户浏览器 │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ ┌──────────────────┐ ┌──────────────────┐ ┌──────────────────┐ │
│ │ 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
```html
<iframe
src="http://容器IP:6080/vnc.html?autoconnect=true&resize=scale"
width="100%"
height="600"
frameborder="0">
</iframe>
```
#### 方式二:使用 noVNC JavaScript API
```html
<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 透传已内置,无需额外配置。
#### 方式二:手动集成
```html
<!-- 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 协议
#### 发送消息格式
```json
{
"type": "text",
"text": "要输入的文本",
"method": "xdotool" // 可选: "xdotool"(默认) 或 "clipboard"
}
```
#### 响应消息格式
```json
// 成功
{ "status": "ok" }
// 失败
{ "status": "error", "message": "错误信息" }
// 心跳响应
{ "type": "pong" }
```
#### 心跳保活
```json
// 发送
{ "type": "ping" }
// 响应
{ "type": "pong" }
```
---
## 音频流传输
### 功能说明
将远程桌面的音频(如视频播放、系统提示音)通过 WebSocket 实时传输到浏览器播放。
### 技术栈
- **采集**: PulseAudio 虚拟声卡
- **编码**: Opus (48kHz, 立体声)
- **传输**: WebSocket (二进制)
- **播放**: Web Audio API
### 前端接入
#### 方式一:嵌入音频播放器页面
```html
<iframe
src="http://容器IP:6090/index.html"
width="300"
height="50"
frameborder="0">
</iframe>
```
#### 方式二:直接使用 pcmflux 客户端
```html
<!-- 引入 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 编码的音频包。
```javascript
const ws = new WebSocket('ws://容器IP:6089');
ws.binaryType = 'arraybuffer';
ws.onmessage = (event) => {
const opusData = new Uint8Array(event.data);
// 使用 Opus 解码器解码后播放
decodeAndPlay(opusData);
};
```
### 注意事项
> [!IMPORTANT]
> **浏览器安全限制**: 由于浏览器自动播放策略,音频播放必须由用户交互(如点击按钮)触发。
---
## 前端集成指南
### 完整示例
```html
<!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 | 文本输入通道 |
**消息格式**:
```typescript
// 请求
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` 环境变量。
---
## 联系方式
如有问题,请联系后端开发团队。