添加qiming-rcoder模块
This commit is contained in:
694
qiming-rcoder/docker/README.md
Normal file
694
qiming-rcoder/docker/README.md
Normal file
@@ -0,0 +1,694 @@
|
||||
# Docker 本地测试配置说明
|
||||
|
||||
> 本文档说明如何在本地使用 Docker Compose 启动 RCoder 开发环境,包含完整的监控服务(Pyroscope、Prometheus、Grafana)和 eBPF 诊断工具。
|
||||
|
||||
## 📑 目录
|
||||
|
||||
- [快速开始](#-快速开始)
|
||||
- [环境要求](#-环境要求)
|
||||
- [架构概览](#-架构概览)
|
||||
- [监控服务](#-监控服务)
|
||||
- [配置说明](#-配置说明)
|
||||
- [常用命令](#-常用命令)
|
||||
- [测试页面](#-测试页面)
|
||||
- [eBPF 诊断工具](#-ebpf-诊断工具)
|
||||
- [故障排查](#-故障排查)
|
||||
- [常见问题](#-常见问题)
|
||||
- [目录结构](#-目录结构)
|
||||
|
||||
---
|
||||
|
||||
## ⚡ 快速开始
|
||||
|
||||
### 一键启动
|
||||
|
||||
```bash
|
||||
# 1. 构建镜像
|
||||
make dev-build
|
||||
|
||||
# 2. 启动所有服务(包含监控)
|
||||
make dev-up
|
||||
|
||||
# 3. 查看日志
|
||||
make dev-logs
|
||||
```
|
||||
|
||||
### 访问服务
|
||||
|
||||
| 服务 | 地址 | 用途 |
|
||||
|------|------|------|
|
||||
| **Pyroscope** | http://localhost:4040 | CPU 性能分析火焰图 |
|
||||
| **Prometheus** | http://localhost:9091 | 时序指标查询 |
|
||||
| **Grafana** | http://localhost:3000 | 进程监控 Dashboard (admin/admin) |
|
||||
|
||||
### 创建测试容器
|
||||
|
||||
```bash
|
||||
# 发送聊天请求,自动创建 agent_runner 容器
|
||||
curl -X POST http://127.0.0.1:8088/computer/chat \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"user_id": "user_123", "prompt": "hello"}'
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 💻 环境要求
|
||||
|
||||
### 必需组件
|
||||
|
||||
| 组件 | 版本要求 | 用途 |
|
||||
|------|----------|------|
|
||||
| **Docker** | 20.10+ | 容器运行时 |
|
||||
| **Docker Compose** | 2.0+ | 多容器编排 |
|
||||
| **Make** | 任意 | 构建自动化 |
|
||||
| **Rust** | 1.75+ | 编译项目(本地开发) |
|
||||
|
||||
### 可选组件
|
||||
|
||||
| 组件 | 用途 |
|
||||
|------|------|
|
||||
| **Python 3** | 启动测试页面 HTTP 服务器 |
|
||||
| **cURL** | API 测试 |
|
||||
|
||||
### 端口占用检查
|
||||
|
||||
启动前请确保以下端口未被占用:
|
||||
|
||||
```bash
|
||||
# 检查端口占用
|
||||
lsof -i :4040 # Pyroscope
|
||||
lsof -i :9091 # Prometheus
|
||||
lsof -i :3000 # Grafana
|
||||
lsof -i :8088 # RCoder API
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🏗️ 架构概览
|
||||
|
||||
### 整体架构
|
||||
|
||||
```mermaid
|
||||
flowchart TB
|
||||
subgraph Docker["🐳 Docker Compose 环境"]
|
||||
direction TB
|
||||
|
||||
RCoder["RCoder 主服务<br/>端口: 8088<br/>镜像: master-rcoder:latest<br/>功能: HTTP API + 容器管理"]
|
||||
|
||||
subgraph Agent["Agent Runner 子容器(动态创建)"]
|
||||
Runner["AI 代理运行时<br/>gRPC 服务端<br/>镜像: master-rcoder:latest"]
|
||||
end
|
||||
|
||||
subgraph Monitoring["📊 监控服务"]
|
||||
direction LR
|
||||
Pyro["Pyroscope<br/>:4040"]
|
||||
Prom["Prometheus<br/>:9091"]
|
||||
Graf["Grafana<br/>:3000"]
|
||||
Pyro --> Prom --> Graf
|
||||
end
|
||||
|
||||
RCoder ==>|"gRPC<br/>内部网络"| Runner
|
||||
end
|
||||
|
||||
style RCoder fill:#e1f5fe
|
||||
style Runner fill:#fff3e0
|
||||
style Pyro fill:#f3e5f5
|
||||
style Prom fill:#e8f5e9
|
||||
style Graf fill:#fce4ec
|
||||
```
|
||||
|
||||
> **提示**: 在支持 Mermaid 的平台(GitHub、GitLab、IDE 插件)上,上图会渲染为交互式流程图。
|
||||
|
||||
### 数据流向
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
subgraph AgentRunner["Agent Runner 容器"]
|
||||
direction TB
|
||||
Alloy["Grafana Alloy"]
|
||||
|
||||
subgraph Collectors["数据采集器"]
|
||||
EBPF["eBPF Profiler<br/>97 Hz"]
|
||||
PE["Process Exporter<br/>15s"]
|
||||
end
|
||||
|
||||
Alloy --> EBPF
|
||||
Alloy --> PE
|
||||
end
|
||||
|
||||
subgraph Storage["存储与分析"]
|
||||
Pyro["Pyroscope<br/>火焰图 + Top 函数"]
|
||||
Prom["Prometheus<br/>时序数据存储"]
|
||||
end
|
||||
|
||||
subgraph Viz["可视化"]
|
||||
Graf["Grafana<br/>统一 Dashboard"]
|
||||
end
|
||||
|
||||
EBPF -->|"CPU 性能数据"| Pyro
|
||||
PE -->|"进程指标"| Prom
|
||||
Pyro --> Graf
|
||||
Prom --> Graf
|
||||
|
||||
style AgentRunner fill:#e3f2fd
|
||||
style Pyro fill:#f3e5f5
|
||||
style Prom fill:#e8f5e9
|
||||
style Graf fill:#fce4ec
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📊 监控服务
|
||||
|
||||
### 服务概览
|
||||
|
||||
| 服务 | 端口 | 登录信息 | 数据源 | 用途 |
|
||||
|------|------|----------|--------|------|
|
||||
| **Pyroscope** | 4040 | 无需登录 | Alloy eBPF (97 Hz) | CPU 性能分析火焰图 |
|
||||
| **Prometheus** | 9091 | 无需登录 | Alloy Process Exporter (15s) | 时序指标存储 |
|
||||
| **Grafana** | 3000 | admin / admin | Prometheus | 可视化 Dashboard |
|
||||
|
||||
### Grafana Dashboard
|
||||
|
||||
**Dashboard 名称**: `Agent Runner 进程监控`
|
||||
|
||||
**包含面板**:
|
||||
- **概览**: RSS/VSZ 内存、CPU 使用率、文件描述符
|
||||
- **内存趋势**: RSS 和 VSZ 的时间序列图
|
||||
- **I/O 监控**: 读取/写入速率
|
||||
- **上下文切换**: 自愿/非自愿切换速率
|
||||
- **线程详情**: 线程数量、FD 使用率
|
||||
- **缺页错误**: 次要/主要缺页错误速率
|
||||
|
||||
**可用变量**:
|
||||
- `project_id`: 过滤项目 ID
|
||||
- `instance`: 过滤实例
|
||||
- `process_name`: 过滤进程名称
|
||||
- `resolution`: 查询分辨率 (15s, 30s, 1m, 5m, 15m)
|
||||
|
||||
---
|
||||
|
||||
## ⚙️ 配置说明
|
||||
|
||||
### 核心配置文件
|
||||
|
||||
#### `docker-compose.yml`
|
||||
监控服务配置,定义所有服务容器。
|
||||
|
||||
**服务列表**:
|
||||
```yaml
|
||||
services:
|
||||
rcoder: # 主 RCoder 服务
|
||||
pyroscope: # CPU 性能分析服务器
|
||||
prometheus: # 时序指标数据库
|
||||
grafana: # 可视化平台
|
||||
```
|
||||
|
||||
#### `config.yml`
|
||||
本地 Docker 容器测试专用配置,用于在 docker-compose 启动的容器中测试动态启动子容器。
|
||||
|
||||
### 镜像配置
|
||||
|
||||
所有容器使用相同的镜像,确保环境一致性:
|
||||
|
||||
```yaml
|
||||
# 主容器和子容器使用相同镜像
|
||||
image: "master-rcoder:latest"
|
||||
```
|
||||
|
||||
### 路径配置
|
||||
|
||||
| 类型 | 容器内路径 | 宿主机映射路径 | 说明 |
|
||||
|------|-----------|---------------|------|
|
||||
| **项目工作目录** | `/app/project_workspace` | `./docker/project_workspace` | 项目代码存放 |
|
||||
| **日志目录** | `/app/logs` | `./docker/logs` | 容器日志输出 |
|
||||
| **规范目录** | `/app/specs` | - | 规范文件存放 |
|
||||
|
||||
### 与生产环境对比
|
||||
|
||||
| 配置项 | 本地测试 | 生产环境 |
|
||||
|--------|---------|---------|
|
||||
| **镜像** | `master-rcoder:latest` | `registry.yichamao.com/rcoder:latest-arm64` |
|
||||
| **配置文件** | `docker/config.yml` | `config.yml` |
|
||||
| **项目路径** | `/app/project_workspace` | `./project_workspace` |
|
||||
| **监控服务** | 完整(Pyroscope + Prometheus + Grafana) | 按需部署 |
|
||||
|
||||
---
|
||||
|
||||
## 🔧 常用命令
|
||||
|
||||
### Make 命令
|
||||
|
||||
```bash
|
||||
# 构建镜像
|
||||
make dev-build
|
||||
|
||||
# 启动服务
|
||||
make dev-up
|
||||
|
||||
# 查看日志
|
||||
make dev-logs
|
||||
|
||||
# 重启服务(代码修改后)
|
||||
make dev-restart
|
||||
|
||||
# 停止服务
|
||||
make dev-down
|
||||
```
|
||||
|
||||
### Docker 命令
|
||||
|
||||
```bash
|
||||
# 查看运行中的容器
|
||||
docker ps | grep rcoder
|
||||
|
||||
# 查看容器日志
|
||||
docker logs -f <container_id>
|
||||
|
||||
# 进入容器
|
||||
docker exec -it <container_id> bash
|
||||
|
||||
# 检查容器资源使用
|
||||
docker stats <container_id>
|
||||
|
||||
# 检查挂载点
|
||||
docker inspect <container_id> | grep Mounts -A 20
|
||||
```
|
||||
|
||||
### API 测试
|
||||
|
||||
```bash
|
||||
# 发送聊天请求(创建容器)
|
||||
curl -X POST http://127.0.0.1:8088/computer/chat \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"user_id": "user_123", "prompt": "hello"}'
|
||||
|
||||
# 查询 Agent 状态
|
||||
curl http://127.0.0.1:8088/agent/status/user_123
|
||||
|
||||
# 取消会话
|
||||
curl -X POST http://127.0.0.1:8088/agent/session/cancel \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"session_id": "<session_id>"}'
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 💚 健康检查
|
||||
|
||||
### 快速验证所有服务
|
||||
|
||||
```bash
|
||||
# 一键检查所有服务状态
|
||||
curl -s http://localhost:4040/health # Pyroscope
|
||||
curl -s http://localhost:9091/-/healthy # Prometheus
|
||||
curl -s http://localhost:3000/api/health # Grafana
|
||||
curl -s http://localhost:8088/health # RCoder
|
||||
```
|
||||
|
||||
### 检查脚本
|
||||
|
||||
```bash
|
||||
# 保存为 check-health.sh
|
||||
#!/bin/bash
|
||||
services=(
|
||||
"Pyroscope:4040:/health"
|
||||
"Prometheus:9091:/-/healthy"
|
||||
"Grafana:3000:/api/health"
|
||||
"RCoder:8088:/health"
|
||||
)
|
||||
|
||||
for service in "${services[@]}"; do
|
||||
IFS=':' read -r name port endpoint <<< "$service"
|
||||
if curl -s "http://localhost:${port}${endpoint}" > /dev/null 2>&1; then
|
||||
echo "✅ $name (${port})"
|
||||
else
|
||||
echo "❌ $name (${port}) - 未响应"
|
||||
fi
|
||||
done
|
||||
```
|
||||
|
||||
### 监控数据验证
|
||||
|
||||
```bash
|
||||
# 检查 Prometheus 是否接收指标
|
||||
curl -s 'http://localhost:9091/api/v1/query?query=up' | jq '.data.result[]'
|
||||
|
||||
# 检查 Pyroscope 是否有应用数据
|
||||
curl -s 'http://localhost:4040/ingest?name=agent_runner' | jq
|
||||
|
||||
# 检查 Grafana 数据源连接
|
||||
curl -s 'http://admin:admin@localhost:3000/api/datasources' | jq '.[] | select(.name=="Prometheus") | .isDefault'
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🧪 测试页面
|
||||
|
||||
`test-page/` 目录提供 VNC、音频和输入法透传功能的集成测试。
|
||||
|
||||
### 文件说明
|
||||
|
||||
| 文件 | 说明 |
|
||||
|------|------|
|
||||
| `vnc-test.html` | 集成测试页面(VNC + 音频 + IME) |
|
||||
| `opus-decoder.min.js` | Opus 音频解码库(86KB) |
|
||||
|
||||
### 支持功能
|
||||
|
||||
- **VNC 远程桌面**: WebSocket 连接到容器的 noVNC 服务
|
||||
- **音频流播放**: 接收并播放容器的音频输出(Opus 编码)
|
||||
- **输入法透传**: 使用本地输入法输入到远程桌面
|
||||
|
||||
### 使用步骤
|
||||
|
||||
#### 1. 启动 HTTP 服务器
|
||||
|
||||
```bash
|
||||
# 进入测试页面目录(从项目根目录执行)
|
||||
cd docker/test-page
|
||||
|
||||
# 或者直接指定绝对路径
|
||||
# cd $(git rev-parse --show-toplevel)/docker/test-page
|
||||
|
||||
# 使用 Python 启动服务器
|
||||
python3 -m http.server 8000
|
||||
```
|
||||
|
||||
#### 2. 访问测试页面
|
||||
|
||||
在浏览器打开:http://127.0.0.1:8000/vnc-test.html
|
||||
|
||||
#### 3. 配置连接参数
|
||||
|
||||
**推荐使用 RCoder 代理模式**:
|
||||
- RCoder 服务地址: `http://127.0.0.1:8088`
|
||||
- User ID: `user_123`
|
||||
- Project ID: 留空或填写实际项目 ID
|
||||
|
||||
#### 4. 创建测试容器
|
||||
|
||||
```bash
|
||||
curl -X POST http://127.0.0.1:8088/computer/chat \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"user_id": "user_123", "prompt": "hello"}'
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🔬 eBPF 诊断工具
|
||||
|
||||
### 概述
|
||||
|
||||
RCoder 集成 eBPF 诊断工具,用于开发环境中快速定位进程阻塞和性能问题。
|
||||
|
||||
> **详细文档**: `/docker/rcoder-agent-runner/ebpf-tools/README.md`
|
||||
|
||||
### ⚠️ 安全警告
|
||||
|
||||
| 模式 | Feature | 容器特权 | 安全性 | 调试能力 |
|
||||
|------|---------|----------|--------|----------|
|
||||
| `make dev-restart` | `ebpf-debug` 启用 | 特权 (SYS_ADMIN) | ⚠️ 降低 | ✅ 完整 |
|
||||
| 生产模式 | 默认关闭 | 限制 | ✅ 高 | ❌ 无 |
|
||||
|
||||
> **仅在受信任的调试环境使用 eBPF 模式!**
|
||||
|
||||
### 监控能力
|
||||
|
||||
| 工具 | 类型 | 频率 | 输出位置 |
|
||||
|------|------|------|----------|
|
||||
| **Alloy eBPF** | 持续 CPU 监控 | 97 Hz | Pyroscope Web UI |
|
||||
| **Alloy Process Exporter** | 进程指标 | 15 秒 | Grafana Dashboard |
|
||||
| **offcpu-monitor** | 阻塞火焰图 | 60 秒 | `/app/container-logs/diag/*.svg` |
|
||||
| **syscall-monitor** | 系统调用追踪 | 60 秒 | `/app/container-logs/diag/*.log` |
|
||||
|
||||
### 快捷诊断命令
|
||||
|
||||
```bash
|
||||
# 1. 进入容器
|
||||
docker exec -it <container> bash
|
||||
|
||||
# 2. 获取 agent_runner 进程 PID
|
||||
PID=$(pgrep agent_runner)
|
||||
|
||||
# 3. 执行诊断命令
|
||||
e-offcpu $PID # CPU 性能分析(显示耗时函数)
|
||||
e-flame $PID 60 # 生成 60 秒火焰图
|
||||
e-profile $PID # 性能分析
|
||||
e-all $PID # 综合诊断(包含所有分析)
|
||||
```
|
||||
|
||||
### 导出诊断数据
|
||||
|
||||
```bash
|
||||
# 导出所有诊断数据
|
||||
docker cp <container>:/app/container-logs/diag ./diag-results
|
||||
|
||||
# 导出单个火焰图
|
||||
docker cp <container>:/app/container-logs/diag/flame-<pid>.svg ./
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🐛 故障排查
|
||||
|
||||
### 按症状分类
|
||||
|
||||
#### 症状:子容器无法启动
|
||||
|
||||
**可能原因**: 镜像不存在
|
||||
|
||||
```bash
|
||||
# 检查镜像
|
||||
docker images | grep master-rcoder
|
||||
|
||||
# 解决方案:重新构建
|
||||
make dev-build
|
||||
```
|
||||
|
||||
#### 症状:配置文件未生效
|
||||
|
||||
**可能原因**: 挂载路径错误
|
||||
|
||||
```bash
|
||||
# 检查配置文件挂载
|
||||
docker exec -it <container_id> cat /app/config.yml
|
||||
|
||||
# 检查日志
|
||||
make dev-logs
|
||||
```
|
||||
|
||||
#### 症状:监控服务无数据
|
||||
|
||||
**诊断步骤**:
|
||||
|
||||
```bash
|
||||
# 1. 检查监控服务状态
|
||||
docker ps | grep -E "pyroscope|prometheus|grafana"
|
||||
|
||||
# 2. 检查 agent_runner 容器
|
||||
docker ps | grep agent_runner
|
||||
|
||||
# 3. 检查 Prometheus 指标
|
||||
curl -s 'http://localhost:9091/api/v1/query?query=up' | jq
|
||||
|
||||
# 4. 检查 Alloy 日志
|
||||
docker exec <container> tail -f /app/container-logs/diag/alloy.log
|
||||
```
|
||||
|
||||
#### 症状:Grafana 显示 "No Data"
|
||||
|
||||
**可能原因**: 没有 agent_runner 容器运行
|
||||
|
||||
**解决方案**:
|
||||
1. 创建容器(发送聊天请求)
|
||||
2. 等待 15-30 秒让数据采集
|
||||
3. 刷新 Dashboard
|
||||
|
||||
#### 症状:端口冲突
|
||||
|
||||
**Prometheus 端口冲突**(9090 → 9091):
|
||||
|
||||
```yaml
|
||||
# docker-compose.yml 已配置端口映射
|
||||
prometheus:
|
||||
ports:
|
||||
- "9091:9090" # 宿主机 9091 → 容器 9090
|
||||
```
|
||||
|
||||
**检查端口占用**:
|
||||
|
||||
```bash
|
||||
lsof -i :4040 # Pyroscope
|
||||
lsof -i :9091 # Prometheus
|
||||
lsof -i :3000 # Grafana
|
||||
lsof -i :8088 # RCoder
|
||||
```
|
||||
|
||||
### 系统化诊断流程
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
Start["🐛 问题发生"]
|
||||
Step1["1️⃣ 检查容器状态<br/>docker ps"]
|
||||
Step2["2️⃣ 查看容器日志<br/>docker logs"]
|
||||
Step3["3️⃣ 检查网络连通性<br/>docker network inspect"]
|
||||
Step4["4️⃣ 检查资源使用<br/>docker stats"]
|
||||
Step5["5️⃣ 进入容器调试<br/>docker exec"]
|
||||
Solve["✅ 问题解决"]
|
||||
|
||||
Start --> Step1
|
||||
Step1 --> Step2
|
||||
Step2 --> Step3
|
||||
Step3 --> Step4
|
||||
Step4 --> Step5
|
||||
Step5 --> Solve
|
||||
|
||||
style Start fill:#ffcdd2
|
||||
style Step1 fill:#e1f5fe
|
||||
style Step2 fill:#e1f5fe
|
||||
style Step3 fill:#e1f5fe
|
||||
style Step4 fill:#fff3e0
|
||||
style Step5 fill:#f3e5f5
|
||||
style Solve fill:#c8e6c9
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ❓ 常见问题
|
||||
|
||||
### Q1: 如何修改镜像名称?
|
||||
|
||||
编辑 `docker-compose.yml`:
|
||||
|
||||
```yaml
|
||||
services:
|
||||
rcoder:
|
||||
image: "your-custom-image:tag"
|
||||
```
|
||||
|
||||
然后运行 `make dev-restart`。
|
||||
|
||||
### Q2: 如何持久化监控数据?
|
||||
|
||||
编辑 `docker-compose.yml`,添加数据卷:
|
||||
|
||||
```yaml
|
||||
services:
|
||||
prometheus:
|
||||
volumes:
|
||||
- ./prometheus/data:/prometheus
|
||||
grafana:
|
||||
volumes:
|
||||
- ./grafana/data:/var/lib/grafana
|
||||
```
|
||||
|
||||
### Q3: 如何禁用某个监控服务?
|
||||
|
||||
注释掉 `docker-compose.yml` 中对应的服务配置。
|
||||
|
||||
### Q4: 如何调整资源限制?
|
||||
|
||||
编辑 `docker-compose.yml`:
|
||||
|
||||
```yaml
|
||||
services:
|
||||
rcoder:
|
||||
deploy:
|
||||
resources:
|
||||
limits:
|
||||
cpus: '2'
|
||||
memory: 4G
|
||||
```
|
||||
|
||||
### Q5: 容器间通信超时怎么办?
|
||||
|
||||
检查 Docker 网络:
|
||||
|
||||
```bash
|
||||
# 查看网络
|
||||
docker network ls
|
||||
|
||||
# 检查网络详情
|
||||
docker network inspect rcoder_agent-network
|
||||
|
||||
# 测试连通性
|
||||
docker exec <container1> ping <container2_ip>
|
||||
```
|
||||
|
||||
### Q6: 如何清理所有容器和数据?
|
||||
|
||||
```bash
|
||||
# 停止并删除所有容器
|
||||
make dev-down
|
||||
|
||||
# 删除数据卷(谨慎操作)
|
||||
docker volume prune
|
||||
|
||||
# 完全清理
|
||||
docker system prune -a
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📚 附录
|
||||
|
||||
### Prometheus 查询示例
|
||||
|
||||
```promql
|
||||
# 内存使用趋势
|
||||
process_resident_memory_bytes{project_id="user_123"}
|
||||
|
||||
# CPU 使用率
|
||||
rate(process_cpu_seconds_total{project_id="user_123"}[30s]) * 100
|
||||
|
||||
# I/O 读取速率
|
||||
rate(process_read_bytes_total{project_id="user_123"}[30s])
|
||||
|
||||
# 文件描述符使用率
|
||||
process_open_fds{project_id="user_123"} / process_max_fds{project_id="user_123"}
|
||||
|
||||
# 上下文切换速率
|
||||
rate(process_context_switches_total{project_id="user_123",context_switch_type="voluntary"}[30s])
|
||||
```
|
||||
|
||||
### 目录结构
|
||||
|
||||
```
|
||||
docker/
|
||||
├── README.md # 本文档
|
||||
├── config.yml # 容器内配置
|
||||
├── docker-compose.yml # 服务编排配置
|
||||
├── computer-cache/ # 计算机缓存
|
||||
├── computer-project-workspace/ # 计算机项目工作区
|
||||
├── grafana/ # Grafana 配置
|
||||
│ └── provisioning/ # 自动配置
|
||||
│ ├── dashboards/ # Dashboard 定义
|
||||
│ └── datasources/ # 数据源配置
|
||||
├── logs/ # 日志目录
|
||||
├── project_workspace/ # 项目工作区
|
||||
├── prometheus/ # Prometheus 配置
|
||||
│ └── prometheus.yml # 规则文件
|
||||
├── rcoder-agent-runner/ # Agent Runner 配置
|
||||
│ └── ebpf-tools/ # eBPF 工具
|
||||
│ └── README.md # 详细文档
|
||||
├── rcoder-master/ # 主服务配置
|
||||
├── start-rcoder.sh # 启动脚本
|
||||
└── test-page/ # 测试页面
|
||||
├── vnc-test.html # VNC 测试页面
|
||||
└── opus-decoder.min.js # Opus 解码库
|
||||
```
|
||||
|
||||
> **注意**: `Make` 命令在项目根目录的 `Makefile` 中定义,使用 `make -C docker` 或从项目根目录执行。
|
||||
|
||||
### 相关文档
|
||||
|
||||
- [项目主文档](../README.md)
|
||||
- [CLAUDE.md](../CLAUDE.md) - 项目架构和开发指南
|
||||
- [eBPF 工具详细文档](./rcoder-agent-runner/ebpf-tools/README.md)
|
||||
- [Makefile](../Makefile) - 构建命令说明
|
||||
|
||||
---
|
||||
|
||||
**最后更新**: 2026-01-13
|
||||
**维护者**: RCoder Team
|
||||
206
qiming-rcoder/docker/config.yml
Normal file
206
qiming-rcoder/docker/config.yml
Normal file
@@ -0,0 +1,206 @@
|
||||
# RCoder 本地 Docker 容器测试配置
|
||||
# 用于在 docker-compose 启动的容器中测试动态启动子容器
|
||||
|
||||
# 默认使用的 AI 代理类型 (Claude/Codex)
|
||||
default_agent: "Claude"
|
||||
|
||||
# 项目工作目录
|
||||
projects_dir: "/app/project_workspace"
|
||||
|
||||
# 主服务端口
|
||||
port: 8087
|
||||
|
||||
# 容器清理配置
|
||||
cleanup_config:
|
||||
# 闲置超时时间(秒),超过此时间未活动的容器将被清理
|
||||
# 默认值: 600 (10分钟)
|
||||
idle_timeout_seconds: 300
|
||||
# 清理任务执行间隔(秒)
|
||||
# 默认值: 300 (5分钟)
|
||||
cleanup_interval_seconds: 3
|
||||
# Docker 容器停止超时时间(秒)
|
||||
# 默认值: 30
|
||||
docker_stop_timeout_seconds: 6
|
||||
# 容器最小保护时间(秒),刚启动的容器在此时间内不会被清理
|
||||
# 默认值: 300 (5分钟)
|
||||
container_protection_seconds: 60
|
||||
|
||||
# Pingora 反向代理配置
|
||||
proxy_config:
|
||||
listen_port: 8088
|
||||
default_backend_port: 8087
|
||||
backend_host: "127.0.0.1"
|
||||
port_param: "port"
|
||||
health_check:
|
||||
enabled: true
|
||||
interval_seconds: 5
|
||||
timeout_seconds: 1
|
||||
healthy_threshold: 2
|
||||
unhealthy_threshold: 3
|
||||
|
||||
# Docker 配置
|
||||
docker_config:
|
||||
# 网络基础名称(不含 project name 前缀)
|
||||
# Docker Compose 会自动添加 project name 前缀,实际网络名称为 {project_name}_{network_base_name}
|
||||
# 例如: network_base_name="agent-network" 时,实际网络为 "rcoder_agent-network"
|
||||
network_base_name: "agent-network"
|
||||
|
||||
# 多镜像配置系统
|
||||
multi_image_config:
|
||||
# 全局默认配置
|
||||
global_defaults:
|
||||
registry_prefix: "nuwax-docker-images-registry.cn-hangzhou.cr.aliyuncs.com/dev"
|
||||
|
||||
# 服务镜像配置
|
||||
services:
|
||||
# RCoder 主服务配置(使用与主容器相同的镜像)
|
||||
rcoder:
|
||||
service_type: "RCoder"
|
||||
# 🎯 使用阿里云镜像
|
||||
image: "nuwax-docker-images-registry.cn-hangzhou.cr.aliyuncs.com/dev/master-rcoder:latest"
|
||||
arm64_image: "nuwax-docker-images-registry.cn-hangzhou.cr.aliyuncs.com/dev/master-rcoder:latest"
|
||||
amd64_image: "nuwax-docker-images-registry.cn-hangzhou.cr.aliyuncs.com/dev/master-rcoder:latest"
|
||||
default_image: "nuwax-docker-images-registry.cn-hangzhou.cr.aliyuncs.com/dev/master-rcoder:latest"
|
||||
image_tag_prefix: "master-rcoder"
|
||||
enabled: true
|
||||
|
||||
# rcoder 容器内用于反向解析宿主机路径的基准路径
|
||||
workspace_resolution_path: "/app/project_workspace"
|
||||
|
||||
environment:
|
||||
RUST_LOG: "debug"
|
||||
SERVICE_MODE: "full"
|
||||
API_PORT: "8086"
|
||||
PROJECT_WORKSPACE_BASE: "/app/project_workspace"
|
||||
command:
|
||||
- "/app/bin/agent_runner"
|
||||
- "--port"
|
||||
- "8086"
|
||||
|
||||
resource_limits:
|
||||
memory_limit: 2147483648 # 2GB
|
||||
cpu_limit: 2.0
|
||||
swap_limit: 4294967296 # 4GB
|
||||
|
||||
work_dir: "/app"
|
||||
network_mode: "bridge"
|
||||
|
||||
# 🎯 主挂载:项目工作目录
|
||||
# 宿主机: /project_workspace/{project_id} → 容器: /app/project_workspace/{project_id}
|
||||
# 注意:当 pod_id 存在时,会根据 isolation_type 动态调整挂载路径
|
||||
# - space: /app/project_workspace/{tenant_id}/{space_id}
|
||||
# - tenant: /app/project_workspace/{tenant_id}
|
||||
# - project: /app/project_workspace/{tenant_id}/{space_id}/{project_id}
|
||||
mounts:
|
||||
# - container_path: "/app/project_workspace/{project_id}"
|
||||
# host_path: "{resolved_path}/{project_id}"
|
||||
# read_only: false
|
||||
# mount_type: "bind"
|
||||
# resolve_from: "/app/project_workspace"
|
||||
# 动态挂载:日志目录 (使用变量替换)
|
||||
# resolve_from: 指定从哪个容器内路径解析宿主机基础路径
|
||||
# {resolved_path}: 解析后的宿主机基础路径
|
||||
# {log_dir_name}: 日志目录名(container_name-timestamp)
|
||||
- container_path: "/app/logs"
|
||||
host_path: "{resolved_path}/page-logs/{log_dir_name}"
|
||||
read_only: false
|
||||
mount_type: "bind"
|
||||
resolve_from: "/app/logs"
|
||||
# ComputerAgentRunner 服务配置
|
||||
computer-agent-runner:
|
||||
service_type: "ComputerAgentRunner"
|
||||
# 🎯 使用阿里云镜像
|
||||
image: "nuwax-docker-images-registry.cn-hangzhou.cr.aliyuncs.com/dev/rcoder-agent-runner:latest"
|
||||
arm64_image: "nuwax-docker-images-registry.cn-hangzhou.cr.aliyuncs.com/dev/rcoder-agent-runner:latest"
|
||||
amd64_image: "nuwax-docker-images-registry.cn-hangzhou.cr.aliyuncs.com/dev/rcoder-agent-runner:latest"
|
||||
default_image: "nuwax-docker-images-registry.cn-hangzhou.cr.aliyuncs.com/dev/rcoder-agent-runner:latest"
|
||||
image_tag_prefix: "computer-agent-runner"
|
||||
enabled: true
|
||||
|
||||
# rcoder 容器内用于反向解析宿主机路径的基准路径
|
||||
workspace_resolution_path: "/app/computer-project-workspace"
|
||||
|
||||
environment:
|
||||
RUST_LOG: "debug"
|
||||
SERVICE_MODE: "agent-only"
|
||||
AGENT_PORT: "8086"
|
||||
PROJECT_WORKSPACE_BASE: "/home/user"
|
||||
# 🔥 Agent 清理配置(闲置超时时间,秒)
|
||||
# 默认值: 3600 (1小时)
|
||||
RCODER_AGENT_IDLE_TIMEOUT_SECS: "3600"
|
||||
# 🔥 Agent 并发会话槽位数量
|
||||
# 默认值: 10
|
||||
RCODER_AGENT_CONCURRENCY_LIMIT: "5"
|
||||
# 🖼️ 自定义壁纸配置
|
||||
# 通过挂载的 /app/assets 目录使用宿主机壁纸
|
||||
# 使用方法:在宿主机 ./wallpaper 目录放置 wallpaper.jpeg
|
||||
# CUSTOM_WALLPAPER_PATH: "/app/assets/wallpaper.jpeg"
|
||||
|
||||
command:
|
||||
- "/usr/local/bin/agent_runner"
|
||||
- "--port"
|
||||
- "8086"
|
||||
|
||||
resource_limits:
|
||||
memory_limit: 4294967296 # 4GB
|
||||
cpu_limit: 2.0
|
||||
swap_limit: 6442450944 # 6GB (实际 swap = 2GB)
|
||||
|
||||
work_dir: "/app"
|
||||
network_mode: "bridge"
|
||||
|
||||
mounts:
|
||||
# 🎯 主挂载:用户主目录(持久化 VNC 桌面数据)
|
||||
# 宿主机: /computer-project-workspace/{user_id} → 容器: /home/user
|
||||
- container_path: "/home/user"
|
||||
host_path: "{resolved_path}/{user_id}"
|
||||
read_only: false
|
||||
mount_type: "bind"
|
||||
resolve_from: "/app/computer-project-workspace"
|
||||
# 动态挂载:日志目录 (使用变量替换)
|
||||
# resolve_from: 指定从哪个容器内路径解析宿主机基础路径
|
||||
# {resolved_path}: 解析后的宿主机基础路径
|
||||
# {log_dir_name}: 日志目录名(container_name-timestamp)
|
||||
# 🔥 panic 日志也会写入此目录 (agent_runner_panic.log)
|
||||
- container_path: "/app/container-logs"
|
||||
host_path: "{resolved_path}/{log_dir_name}"
|
||||
read_only: false
|
||||
mount_type: "bind"
|
||||
resolve_from: "/app/logs/container"
|
||||
# 🖼️ 自定义壁纸挂载
|
||||
# 将宿主机 ./wallpaper 目录挂载到容器内
|
||||
# rcoder 主服务已将 wallpaper 挂载到 /app/assets
|
||||
- container_path: "/app/assets"
|
||||
host_path: "{resolved_path}"
|
||||
read_only: true
|
||||
mount_type: "bind"
|
||||
resolve_from: "/app/assets"
|
||||
|
||||
# 镜像选择策略
|
||||
selection_strategy: "ServiceOnly"
|
||||
|
||||
# 缓存配置
|
||||
cache_config:
|
||||
enabled: true
|
||||
ttl_seconds: 3600
|
||||
max_entries: 50
|
||||
|
||||
# 其他 Docker 配置
|
||||
network_mode: "bridge"
|
||||
work_dir: "/app"
|
||||
auto_cleanup: true
|
||||
container_ttl_seconds: 3600
|
||||
|
||||
# API Key 鉴权配置(可选)
|
||||
api_key_auth:
|
||||
# 是否启用 API Key 鉴权(默认关闭)
|
||||
# 启用后,所有 HTTP 请求必须携带正确的 x-api-key header
|
||||
# 豁免端点:/health, /metrics, /api/docs, /proxy/status, /proxy/stats
|
||||
enabled: false
|
||||
|
||||
# API Key 值(首次生成时自动创建随机密钥)
|
||||
# 格式:sk-{32位十六进制字符串}
|
||||
# 示例:sk-a1b2c3d4e5f6789012345678abcdef01
|
||||
# 可以通过环境变量 RCODER_API_KEY 覆盖此配置
|
||||
# 生产环境请务必修改此密钥或使用环境变量
|
||||
api_key: "YOUR_API_KEY_HERE_CHANGE_THIS"
|
||||
97
qiming-rcoder/docker/docker-compose.yml
Normal file
97
qiming-rcoder/docker/docker-compose.yml
Normal file
@@ -0,0 +1,97 @@
|
||||
name: rcoder
|
||||
services:
|
||||
rcoder:
|
||||
image: ${RCODER_IMAGE:-nuwax-docker-images-registry.cn-hangzhou.cr.aliyuncs.com/dev/master-rcoder:latest}
|
||||
ports:
|
||||
- "${RCODER_PORT:-8090}:${RCODER_PORT:-8090}"
|
||||
- "8088:8088" # Pingora 反向代理端口(VNC 代理、健康检查等)
|
||||
- "60001:60000"
|
||||
- "8199:80"
|
||||
environment:
|
||||
- TZ=Asia/Shanghai
|
||||
- RCODER_PORT=${RCODER_PORT:-8090}
|
||||
- RUST_LOG=debug # 设置调试日志级别
|
||||
# 🎯 关键:获取docker sock,用于检测宿主机路径
|
||||
- DOCKER_SOCKET_PATH=${DOCKER_SOCKET_PATH:-/var/run/docker.sock}
|
||||
volumes:
|
||||
# 挂载 Docker socket
|
||||
- ${DOCKER_SOCKET_PATH:-/var/run/docker.sock}:/var/run/docker.sock
|
||||
# 🎯 挂载本地测试配置文件(使用与主容器相同的镜像)
|
||||
- ./config.yml:/app/config.yml
|
||||
# 挂载项目工作目录 - 这是关键的挂载!
|
||||
- ./project_workspace:/app/project_workspace
|
||||
# 挂载 computer 项目工作目录
|
||||
- ./computer-project-workspace:/app/computer-project-workspace
|
||||
# 挂载工作目录
|
||||
- ./logs/:/app/logs
|
||||
# 挂载启动脚本
|
||||
- ./start-rcoder.sh:/app/start-rcoder.sh
|
||||
# 专门用于npm,pnpm,bun,uvx 缓存使用的目录
|
||||
- ./computer-cache:/root/.cache
|
||||
# 🖼️ 挂载壁纸目录(供 agent_runner 子容器使用)
|
||||
# - ./wallpaper:/app/assets
|
||||
command: ["/bin/bash", "/app/start-rcoder.sh"]
|
||||
healthcheck:
|
||||
test:
|
||||
["CMD", "curl", "-f", "http://localhost:${RCODER_PORT:-8090}/health"]
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 3
|
||||
start_period: 60s
|
||||
restart: always
|
||||
|
||||
# Pyroscope Server - 持续剖析数据存储和可视化
|
||||
pyroscope:
|
||||
image: grafana/pyroscope:latest
|
||||
container_name: rcoder-pyroscope
|
||||
ports:
|
||||
- "4040:4040" # Web UI
|
||||
environment:
|
||||
- TZ=Asia/Shanghai
|
||||
volumes:
|
||||
- pyroscope-data:/data
|
||||
command:
|
||||
- server
|
||||
restart: unless-stopped
|
||||
|
||||
# Prometheus Server - 时序数据库,存储常规指标
|
||||
prometheus:
|
||||
image: prom/prometheus:latest
|
||||
container_name: rcoder-prometheus
|
||||
command:
|
||||
- "--config.file=/etc/prometheus/prometheus.yml"
|
||||
- "--storage.tsdb.path=/prometheus"
|
||||
- "--web.console.libraries=/usr/share/prometheus/console_libraries"
|
||||
- "--web.console.templates=/usr/share/prometheus/consoles"
|
||||
- "--web.enable-lifecycle"
|
||||
ports:
|
||||
- "9091:9090" # 宿主机 9091 → 容器 9090(避免端口冲突)
|
||||
volumes:
|
||||
- ./prometheus/prometheus.yml:/etc/prometheus/prometheus.yml:ro
|
||||
- prometheus-data:/prometheus
|
||||
environment:
|
||||
- TZ=Asia/Shanghai
|
||||
restart: unless-stopped
|
||||
|
||||
# Grafana - 可视化平台
|
||||
grafana:
|
||||
image: grafana/grafana:latest
|
||||
container_name: rcoder-grafana
|
||||
ports:
|
||||
- "3000:3000"
|
||||
environment:
|
||||
- GF_SECURITY_ADMIN_USER=admin
|
||||
- GF_SECURITY_ADMIN_PASSWORD=admin
|
||||
- GF_USERS_ALLOW_SIGN_UP=false
|
||||
- TZ=Asia/Shanghai
|
||||
volumes:
|
||||
- grafana-data:/var/lib/grafana
|
||||
- ./grafana/provisioning:/etc/grafana/provisioning:ro
|
||||
depends_on:
|
||||
- prometheus
|
||||
restart: unless-stopped
|
||||
|
||||
volumes:
|
||||
pyroscope-data:
|
||||
prometheus-data:
|
||||
grafana-data:
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,12 @@
|
||||
apiVersion: 1
|
||||
|
||||
providers:
|
||||
- name: 'Default'
|
||||
orgId: 1
|
||||
folder: ''
|
||||
type: file
|
||||
disableDeletion: false
|
||||
updateIntervalSeconds: 10
|
||||
allowUiUpdates: true
|
||||
options:
|
||||
path: /etc/grafana/provisioning/dashboards
|
||||
@@ -0,0 +1,16 @@
|
||||
apiVersion: 1
|
||||
|
||||
datasources:
|
||||
# Prometheus 数据源(容器内部网络)
|
||||
- name: Prometheus
|
||||
type: prometheus
|
||||
access: proxy
|
||||
url: http://prometheus:9090
|
||||
isDefault: true
|
||||
editable: true
|
||||
jsonData:
|
||||
timeInterval: "15s"
|
||||
|
||||
# 注意: Pyroscope 有独立的 Web UI (http://localhost:4040)
|
||||
# Grafana 原生不支持 Pyroscope 作为 datasource
|
||||
# 请直接访问 Pyroscope Web UI 查看性能剖析数据
|
||||
22
qiming-rcoder/docker/prometheus/prometheus.yml
Normal file
22
qiming-rcoder/docker/prometheus/prometheus.yml
Normal file
@@ -0,0 +1,22 @@
|
||||
global:
|
||||
scrape_interval: 15s
|
||||
evaluation_interval: 15s
|
||||
external_labels:
|
||||
cluster: 'rcoder'
|
||||
environment: 'dev'
|
||||
|
||||
# ============================================================================
|
||||
# Scrape 配置
|
||||
# ============================================================================
|
||||
# 注意: Alloy 使用 prometheus.remote_write 直接写入数据,不需要 scrape
|
||||
# 这里只配置 Prometheus 自身和 Grafana 的监控
|
||||
scrape_configs:
|
||||
# Prometheus 自身监控
|
||||
- job_name: 'prometheus'
|
||||
static_configs:
|
||||
- targets: ['localhost:9090']
|
||||
|
||||
# Grafana 监控 (可选)
|
||||
- job_name: 'grafana'
|
||||
static_configs:
|
||||
- targets: ['grafana:3000']
|
||||
1
qiming-rcoder/docker/rcoder-agent-runner/.npmrc
Normal file
1
qiming-rcoder/docker/rcoder-agent-runner/.npmrc
Normal file
@@ -0,0 +1 @@
|
||||
registry=https://registry.npmmirror.com/
|
||||
246
qiming-rcoder/docker/rcoder-agent-runner/Dockerfile
Normal file
246
qiming-rcoder/docker/rcoder-agent-runner/Dockerfile
Normal file
@@ -0,0 +1,246 @@
|
||||
# ============================================================================
|
||||
# 最终镜像 Dockerfile - rcoder-agent-runner
|
||||
# ============================================================================
|
||||
# 此镜像基于基础镜像 rcoder-agent-base:latest
|
||||
# 只包含业务代码和配置文件,构建速度快
|
||||
# 构建命令: make docker-build-agent-runner 或 make dev-restart
|
||||
# ============================================================================
|
||||
|
||||
# 使用本地基础镜像
|
||||
ARG BASE_IMAGE=rcoder-agent-base:latest
|
||||
FROM ${BASE_IMAGE}
|
||||
|
||||
# 设置工作目录
|
||||
WORKDIR /home/user
|
||||
|
||||
# agent-browser 默认配置(独立 Chromium profile,避免与 MCP 锁冲突)
|
||||
ENV AGENT_BROWSER_EXECUTABLE_PATH=/usr/local/bin/chromium \
|
||||
AGENT_BROWSER_PROFILE=/home/user/.config/agent-browser/chromium \
|
||||
AGENT_BROWSER_HEADED=1
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# 📦 复制业务代码和配置文件
|
||||
# ============================================================================
|
||||
|
||||
# 使用构建参数破坏缓存(每次构建都获取最新版本的工具)
|
||||
ARG CACHEBUST=1
|
||||
|
||||
# 安装 mcp-stdio-proxy(频繁更新的工具,放在最终镜像以加快构建)
|
||||
# 使用 CACHEBUST 变量触发缓存失效
|
||||
RUN . /opt/cargo/env && echo "Cache bust: ${CACHEBUST}" && cargo install mcp-stdio-proxy
|
||||
|
||||
# # 安装 claude-code-acp-rs CLI 工具
|
||||
# RUN . /opt/cargo/env && echo "Cache bust: ${CACHEBUST}" && cargo install claude-code-acp-rs --no-default-features --features otel,sacp-flush
|
||||
|
||||
# 安装 agent-browser CLI(仅安装命令行工具,不在构建期下载浏览器)
|
||||
RUN . /opt/cargo/env && \
|
||||
echo "Cache bust: ${CACHEBUST}" && \
|
||||
cargo install agent-browser && \
|
||||
agent-browser --version
|
||||
|
||||
# 测试agent
|
||||
RUN echo "Cache bust: ${CACHEBUST}" && npm install -g claude-code-acp-ts@latest
|
||||
|
||||
|
||||
# 安装 nuwaxcode(频繁更新的工具,放在最终镜像以加快构建)
|
||||
# 使用 CACHEBUST 变量触发缓存失效
|
||||
# 安装失败时主动退出,避免构建包含不完整的镜像
|
||||
RUN echo "Cache bust: ${CACHEBUST}" && \
|
||||
echo "📦 开始安装 nuwaxcode..." && \
|
||||
npm i -g nuwaxcode@latest && \
|
||||
echo "✅ nuwaxcode 安装完成,开始验证..." && \
|
||||
which nuwaxcode && \
|
||||
echo "✅ nuwaxcode 验证成功: $(nuwaxcode -v)" || \
|
||||
(echo "❌ nuwaxcode 安装或验证失败,构建中断" && exit 1)
|
||||
|
||||
|
||||
# 配置 nuwaxcode - 禁用 websearch, webfetch 和 question 权限
|
||||
RUN mkdir -p /root/.config/opencode && \
|
||||
echo '{\n "permission": {\n "websearch": "deny",\n "webfetch": "deny",\n "question": "deny"\n }\n}' > /root/.config/opencode/opencode.json
|
||||
|
||||
|
||||
# # 安装 opencode - AI 驱动的代码导航工具
|
||||
# # 注意:npm 包不支持 ARM64,使用官方安装脚本
|
||||
# RUN echo "📦 安装 opencode..." && \
|
||||
# curl -fsSL https://opencode.ai/install | bash && \
|
||||
# ln -sf /root/.opencode/bin/opencode /usr/local/bin/opencode && \
|
||||
# echo "验证 opencode 安装..." && \
|
||||
# opencode --version && \
|
||||
# echo "✅ opencode 安装成功(已创建符号链接到 /usr/local/bin)"
|
||||
|
||||
# ============================================================================
|
||||
# 🔧 eBPF 诊断工具安装
|
||||
# 注意:这些工具会增加镜像大小 (~500MB)
|
||||
# 通过 make dev-restart 默认启用
|
||||
# ============================================================================
|
||||
ARG INSTALL_EBPF_TOOLS=false
|
||||
|
||||
RUN if [ "$INSTALL_EBPF_TOOLS" = "true" ]; then \
|
||||
echo "🔧 开始安装 eBPF 诊断工具..."; \
|
||||
apt-get update && \
|
||||
echo "📦 安装 bpftrace(核心 eBPF 诊断工具)..."; \
|
||||
apt-get install -y bpftrace && \
|
||||
echo "📦 安装 strace(系统调用追踪)..."; \
|
||||
apt-get install -y strace && \
|
||||
echo "📦 安装 sysstat(性能监控:iostat, mpstat 等)..."; \
|
||||
apt-get install -y sysstat && \
|
||||
echo "📦 安装 jq(JSON 处理)..."; \
|
||||
apt-get install -y jq && \
|
||||
rm -rf /var/lib/apt/lists/*; \
|
||||
echo "✅ eBPF 诊断工具安装完成"; \
|
||||
which bpftrace && bpftrace --version || echo "❌ bpftrace 未安装"; \
|
||||
else \
|
||||
echo "⚠️ eBPF 工具未安装(生产模式)"; \
|
||||
fi
|
||||
|
||||
# 安装 FlameGraph 工具
|
||||
RUN if [ "$INSTALL_EBPF_TOOLS" = "true" ]; then \
|
||||
if [ "${USE_GITHUB_MIRROR}" = "true" ]; then \
|
||||
git clone ${GITHUB_MIRROR_URL}brendangregg/FlameGraph /tmp/FlameGraph; \
|
||||
else \
|
||||
git clone https://github.com/brendangregg/FlameGraph /tmp/FlameGraph; \
|
||||
fi && \
|
||||
cp /tmp/FlameGraph/*.pl /usr/local/bin/ \
|
||||
&& rm -rf /tmp/FlameGraph \
|
||||
&& echo "✅ FlameGraph 工具已安装"; \
|
||||
fi
|
||||
|
||||
# ============================================================================
|
||||
# 📊 Pyroscope Agent - 持续剖析(已废弃,保留用于兼容性)
|
||||
# ============================================================================
|
||||
ARG INSTALL_PYROSCOPE=false
|
||||
RUN if [ "$INSTALL_PYROSCOPE" = "true" ]; then \
|
||||
echo "📦 安装 Pyroscope Agent..."; \
|
||||
apt-get update && \
|
||||
apt-get install -y curl ca-certificates && \
|
||||
echo "下载 Pyroscope Agent (v1.17.0)..." && \
|
||||
curl -fsSL "https://github.com/grafana/pyroscope/releases/download/v1.17.0/pyroscope_1.17.0_linux_arm64.tar.gz" \
|
||||
-o /tmp/pyroscope.tar.gz && \
|
||||
echo "解压 Pyroscope Agent..." && \
|
||||
tar -xzf /tmp/pyroscope.tar.gz -C /usr/local/bin/ && \
|
||||
rm /tmp/pyroscope.tar.gz && \
|
||||
chmod +x /usr/local/bin/pyroscope && \
|
||||
echo "验证 Pyroscope Agent..." && \
|
||||
pyroscope --version || echo "⚠️ Pyroscope 安装失败"; \
|
||||
else \
|
||||
echo "⚠️ Pyroscope 未安装"; \
|
||||
fi
|
||||
|
||||
# ============================================================================
|
||||
# 📊 Grafana Alloy - 持续性能数据采集(替代已废弃的 Pyroscope Agent)
|
||||
# ============================================================================
|
||||
ARG INSTALL_ALLOY=false
|
||||
ARG ALLOY_VERSION=v1.12.2
|
||||
RUN if [ "$INSTALL_ALLOY" = "true" ]; then \
|
||||
echo "📦 安装 Grafana Alloy ${ALLOY_VERSION}..."; \
|
||||
apt-get update && \
|
||||
apt-get install -y wget ca-certificates && \
|
||||
echo "检测系统架构..."; \
|
||||
ARCH=$(dpkg --print-architecture); \
|
||||
echo "系统架构: ${ARCH}"; \
|
||||
ALLOY_VER=${ALLOY_VERSION}; \
|
||||
ALLOY_VER_NUM=${ALLOY_VER#v}; \
|
||||
echo "下载 Alloy deb 包 (${ARCH})..."; \
|
||||
wget -qO- "https://github.com/grafana/alloy/releases/download/${ALLOY_VER}/alloy-${ALLOY_VER_NUM}-1.${ARCH}.deb" > /tmp/alloy.deb && \
|
||||
echo "下载完成,文件大小: $(ls -lh /tmp/alloy.deb | awk '{print $5}')"; \
|
||||
echo "安装 Alloy..."; \
|
||||
dpkg -i /tmp/alloy.deb && \
|
||||
apt-get install -y -f && \
|
||||
rm /tmp/alloy.deb && \
|
||||
echo "验证 Alloy 安装..."; \
|
||||
alloy --version; \
|
||||
else \
|
||||
echo "⚠️ Alloy 未安装"; \
|
||||
fi
|
||||
|
||||
# ============================================================================
|
||||
# 🔍 bpfcc-tools - Off-CPU 阻塞分析工具
|
||||
# ============================================================================
|
||||
RUN if [ "$INSTALL_EBPF_TOOLS" = "true" ]; then \
|
||||
echo "📦 安装 bpfcc-tools (包含 offcputime)..."; \
|
||||
apt-get update && \
|
||||
apt-get install -y bpfcc-tools && \
|
||||
rm -rf /var/lib/apt/lists/* && \
|
||||
which offcputime-bpfcc && offcputime-bpfcc --help | head -3 || echo "⚠️ offcputime-bpfcc 安装失败"; \
|
||||
fi
|
||||
|
||||
# 复制预构建的 agent_runner 二进制文件
|
||||
COPY bin/agent_runner /usr/local/bin/agent_runner
|
||||
RUN chmod +x /usr/local/bin/agent_runner
|
||||
|
||||
# 复制启动脚本
|
||||
COPY ./start-up.sh /root/
|
||||
RUN chmod +x /root/start-up.sh
|
||||
|
||||
# 复制中文输入法修复脚本(容器内运行 fix-ime 命令)
|
||||
COPY ./fix-chrome-ime-final.sh /usr/local/bin/fix-ime
|
||||
RUN chmod +x /usr/local/bin/fix-ime
|
||||
|
||||
# 复制音频服务脚本和静态文件
|
||||
COPY audio_server.py /usr/local/bin/audio_server.py
|
||||
COPY audio_static /usr/local/share/audio_static
|
||||
RUN chmod +x /usr/local/bin/audio_server.py
|
||||
|
||||
# 复制 MCP 配置文件(用于容器启动时的共享 MCP 代理服务)
|
||||
COPY mcp /etc/mcp
|
||||
|
||||
# 复制 IME 服务脚本(允许用户使用宿主机输入法输入到远程桌面)
|
||||
COPY ime_server.py /usr/local/bin/ime_server.py
|
||||
RUN chmod +x /usr/local/bin/ime_server.py
|
||||
|
||||
# ============================================================================
|
||||
# 🔧 eBPF 工具目录
|
||||
# ============================================================================
|
||||
# 复制整个 eBPF 工具目录到容器中
|
||||
COPY ebpf-tools /usr/local/bin/ebpf-tools
|
||||
RUN chmod +x /usr/local/bin/ebpf-tools/*.sh && \
|
||||
ls -la /usr/local/bin/ebpf-tools/ && \
|
||||
echo "✅ eBPF 工具目录已创建: /usr/local/bin/ebpf-tools/"
|
||||
|
||||
# 创建便捷的快捷命令(包装脚本)
|
||||
RUN echo '#!/bin/bash\n/usr/local/bin/ebpf-tools/diag-tool.sh offcpu "$@"' > /usr/local/bin/e-offcpu && \
|
||||
echo '#!/bin/bash\n/usr/local/bin/ebpf-tools/diag-tool.sh flame "$@"' > /usr/local/bin/e-flame && \
|
||||
echo '#!/bin/bash\n/usr/local/bin/ebpf-tools/diag-tool.sh profile "$@"' > /usr/local/bin/e-profile && \
|
||||
echo '#!/bin/bash\n/usr/local/bin/ebpf-tools/diag-tool.sh all "$@"' > /usr/local/bin/e-all && \
|
||||
echo '#!/bin/bash\n/usr/local/bin/ebpf-tools/auto-flamegraph.sh start' > /usr/local/bin/e-auto-flame && \
|
||||
echo '#!/bin/bash\n/usr/local/bin/ebpf-tools/offcpu-monitor.sh start' > /usr/local/bin/e-offcpu-monitor && \
|
||||
echo '#!/bin/bash\n/usr/local/bin/ebpf-tools/syscall-monitor.sh start' > /usr/local/bin/e-syscall-monitor && \
|
||||
chmod +x /usr/local/bin/e-offcpu /usr/local/bin/e-flame /usr/local/bin/e-profile /usr/local/bin/e-all /usr/local/bin/e-auto-flame /usr/local/bin/e-offcpu-monitor /usr/local/bin/e-syscall-monitor && \
|
||||
echo "✅ eBPF 诊断快捷命令已创建:" && \
|
||||
echo " - e-offcpu, e-flame, e-profile, e-all (手动 CPU 诊断)" && \
|
||||
echo " - e-auto-flame (自动火焰图生成)" && \
|
||||
echo " - e-offcpu-monitor (Off-CPU 阻塞监控)" && \
|
||||
echo " - e-syscall-monitor (系统调用监控)"
|
||||
|
||||
# ============================================================================
|
||||
# 📊 Grafana Alloy 配置文件
|
||||
# ============================================================================
|
||||
# 复制 Alloy 配置文件到容器(从 ebpf-tools 目录统一管理)
|
||||
COPY ebpf-tools/alloy-config.alloy /etc/alloy/config.alloy
|
||||
|
||||
# 验证 Alloy 配置文件语法
|
||||
RUN if command -v alloy >/dev/null 2>&1; then \
|
||||
echo "验证 Alloy 配置文件..." && \
|
||||
alloy validate /etc/alloy/config.alloy || echo "⚠️ Alloy 配置验证失败"; \
|
||||
else \
|
||||
echo "⚠️ Alloy 未安装,跳过配置验证"; \
|
||||
fi
|
||||
|
||||
# ============================================================================
|
||||
# 🔧 暴露端口
|
||||
# ============================================================================
|
||||
# 暴露 agent_runner 端口
|
||||
EXPOSE 8086
|
||||
|
||||
# 暴露音频流端口
|
||||
EXPOSE 6090
|
||||
EXPOSE 6089
|
||||
|
||||
# 暴露 IME 服务端口
|
||||
EXPOSE 6091
|
||||
|
||||
# ============================================================================
|
||||
# 🚀 启动配置
|
||||
# ============================================================================
|
||||
ENTRYPOINT ["/root/start-up.sh"]
|
||||
887
qiming-rcoder/docker/rcoder-agent-runner/Dockerfile.base
Normal file
887
qiming-rcoder/docker/rcoder-agent-runner/Dockerfile.base
Normal file
@@ -0,0 +1,887 @@
|
||||
# ============================================================================
|
||||
# 基础镜像 Dockerfile - rcoder-agent-base
|
||||
# ============================================================================
|
||||
# 此镜像包含所有系统软件、工具链和环境配置,不包含业务代码
|
||||
# 构建命令: make docker-build-agent-base
|
||||
# 平时不需要重新构建,只有在修改系统依赖时才需要重新构建
|
||||
# ============================================================================
|
||||
|
||||
ARG DOCKER_MIRROR
|
||||
# GitHub 镜像加速配置(默认启用)
|
||||
ARG USE_GITHUB_MIRROR=true
|
||||
# 基础镜像: buildpack-deps:bookworm-curl
|
||||
# - 基于 debian:12 (bookworm), 与 debian:12 100% 兼容
|
||||
# - 预装 curl + wget + ca-certificates + gnupg, 无需先 apt 安装
|
||||
# - Docker 官方维护, 与 debian:12 共享底层 layer, 拉取几乎零增量
|
||||
# - 避免"先用海外 apt 源装 curl 再配置镜像"的鸡生蛋死循环
|
||||
FROM ${DOCKER_MIRROR}buildpack-deps:bookworm-curl
|
||||
|
||||
# GitHub 镜像加速环境变量(必须在 FROM 之后设置)
|
||||
ENV GITHUB_MIRROR_URL=https://gh-proxy.org/https://github.com/
|
||||
|
||||
# Base image with desktop environment and applications
|
||||
# Environment variables:
|
||||
|
||||
ENV \
|
||||
# Avoid system prompts: \
|
||||
DEBIAN_FRONTEND=noninteractive \
|
||||
DEBIAN_PRIORITY=high \
|
||||
# Timezone: \
|
||||
TZ=Asia/Shanghai \
|
||||
# Pip settings: \
|
||||
PIP_DEFAULT_TIMEOUT=100 \
|
||||
PIP_DISABLE_PIP_VERSION_CHECK=1 \
|
||||
PIP_NO_CACHE_DIR=1
|
||||
|
||||
# 使用 LinuxMirrors 一键配置阿里云镜像源
|
||||
# 参考: https://github.com/SuperManito/LinuxMirrors
|
||||
# buildpack-deps:bookworm-curl 已预装 curl,可直接执行脚本配置镜像源
|
||||
RUN curl -sSL https://linuxmirrors.cn/main.sh | bash -s -- \
|
||||
--source mirrors.aliyun.com \
|
||||
--protocol https \
|
||||
--use-intranet-source false \
|
||||
--install-epel false \
|
||||
--backup false \
|
||||
--upgrade-software false \
|
||||
--clean-cache true \
|
||||
--ignore-backup-tips
|
||||
|
||||
|
||||
# 复制 npmrc 配置镜像(使用国内镜像加速)
|
||||
COPY .npmrc /root/.npmrc
|
||||
|
||||
|
||||
|
||||
# Desktop environment and applications (merged for smaller image size):
|
||||
|
||||
RUN apt-get update && \
|
||||
# Install all packages in one layer to reduce image size
|
||||
apt-get install -y \
|
||||
# X window server:
|
||||
xserver-xorg xorg x11-xserver-utils xvfb x11-utils xauth \
|
||||
# XFCE desktop environment:
|
||||
xfce4 xfce4-goodies \
|
||||
# 中文输入法(fcitx5 完整安装)
|
||||
fcitx5 fcitx5-pinyin fcitx5-frontend-gtk3 fcitx5-frontend-gtk2 fcitx5-frontend-qt5 fcitx5-module-xorg fcitx5-config-qt im-config dbus-x11 \
|
||||
# Basic system utilities:
|
||||
util-linux sudo curl git \
|
||||
# Python pip:
|
||||
python3-pip \
|
||||
# Network analysis tools:
|
||||
iputils-ping traceroute dnsutils tcpdump mtr-tiny nmap iperf3 httping telnet \
|
||||
# Desktop SDK tools:
|
||||
xdotool scrot \
|
||||
# FFmpeg 8 编译依赖:
|
||||
yasm nasm pkg-config libx264-dev libx265-dev libnuma-dev libvpx-dev libmp3lame-dev libopus-dev libspeex-dev libtheora-dev libtool libva-dev libvdpau-dev libvorbis-dev libxcb1-dev libxcb-shm0-dev libxcb-xfixes0-dev texinfo zlib1g-dev \
|
||||
# VNC and streaming:
|
||||
tigervnc-standalone-server tigervnc-xorg-extension net-tools netcat-openbsd \
|
||||
# Audio streaming (PulseAudio + pcmflux dependencies):
|
||||
pulseaudio pulseaudio-utils libpulse-dev libopus-dev \
|
||||
# Standard applications:
|
||||
x11-apps xpdf gedit xpaint tint2 galculator pcmanfm \
|
||||
# Dependencies for later installations:
|
||||
apt-transport-https ca-certificates gnupg \
|
||||
socat supervisor \
|
||||
xclip \
|
||||
wget software-properties-common \
|
||||
# Suna dependencies:
|
||||
poppler-utils wkhtmltopdf antiword unrtf catdoc gawk file jq csvkit xmlstarlet zip unzip tmux vim tree rsync && \
|
||||
# Configure X11 wrapper to allow any user:
|
||||
sed -i 's/allowed_users=console/allowed_users=anybody/' /etc/X11/Xwrapper.config && \
|
||||
# Set the default terminal
|
||||
ln -sf /usr/bin/xfce4-terminal.wrapper /etc/alternatives/x-terminal-emulator
|
||||
|
||||
# ============================================================================
|
||||
# 🎬 编译安装 FFmpeg 8.0 (最新版本)
|
||||
# ============================================================================
|
||||
# Debian 12 官方仓库的 FFmpeg 版本是 5.1.8,这里从源码编译安装最新的 FFmpeg 8
|
||||
RUN echo "Building FFmpeg 8.0 from source..." && \
|
||||
cd /tmp && \
|
||||
if [ "${USE_GITHUB_MIRROR}" = "true" ]; then \
|
||||
git clone --depth 1 --branch release/8.0 ${GITHUB_MIRROR_URL}FFmpeg/FFmpeg.git ffmpeg-8.0; \
|
||||
else \
|
||||
git clone --depth 1 --branch release/8.0 https://github.com/FFmpeg/FFmpeg.git ffmpeg-8.0; \
|
||||
fi && \
|
||||
cd ffmpeg-8.0 && \
|
||||
./configure \
|
||||
--extra-version="8.0" \
|
||||
--disable-debug \
|
||||
--disable-doc \
|
||||
--disable-ffplay \
|
||||
--enable-gpl \
|
||||
--enable-libx264 \
|
||||
--enable-libx265 \
|
||||
--enable-libvpx \
|
||||
--enable-libmp3lame \
|
||||
--enable-libopus && \
|
||||
make -j$(nproc) && \
|
||||
make install && \
|
||||
ldconfig && \
|
||||
echo "✅ FFmpeg 8.0 installed: $(ffmpeg -version | head -1)" && \
|
||||
# Clean up build artifacts:
|
||||
rm -rf /tmp/ffmpeg-8.0 && \
|
||||
# Clean up package cache and temporary files to reduce image size:
|
||||
apt-get autoremove -y && \
|
||||
apt-get autoclean && \
|
||||
rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* /root/.cache
|
||||
|
||||
# ============================================================================
|
||||
# 🔊 配置 PulseAudio 系统模式(容器环境优化)
|
||||
# ============================================================================
|
||||
# 配置 PulseAudio 系统模式,支持以 root 身份运行
|
||||
RUN mkdir -p /etc/pulse && \
|
||||
# 创建系统模式配置
|
||||
printf '[General]\n\
|
||||
default-sample-format = s16le\n\
|
||||
default-sample-rate = 48000\n\
|
||||
default-sample-channels = 2\n\
|
||||
default-fragments = 2\n\
|
||||
default-fragment-size-msec = 125\n' > /etc/pulse/daemon.conf && \
|
||||
# 允许所有用户连接系统模式 PulseAudio
|
||||
printf '[General]\n\
|
||||
allow-autospawn-for-root = yes\n\
|
||||
default-server = unix:/var/run/pulse/native\n' > /etc/pulse/client.conf && \
|
||||
# 创建系统模式 pulse 用户组
|
||||
groupadd -r pulse-access 2>/dev/null || true && \
|
||||
usermod -aG pulse-access root 2>/dev/null || true && \
|
||||
usermod -aG pulse-access user 2>/dev/null || true && \
|
||||
# 创建 PulseAudio 运行时目录
|
||||
mkdir -p /var/run/pulse && \
|
||||
chmod 1777 /var/run/pulse && \
|
||||
echo "✅ PulseAudio system mode configured"
|
||||
|
||||
# ============================================================================
|
||||
# 🐍 配置 pip 镜像源 (系统级配置,所有用户共享)
|
||||
# ============================================================================
|
||||
# 使用阿里云 PyPI 镜像源(稳定可靠,无限流限制)
|
||||
RUN mkdir -p /etc/pip && \
|
||||
printf '[global]\n\
|
||||
index-url = https://mirrors.aliyun.com/pypi/simple/\n\
|
||||
trusted-host = mirrors.aliyun.com\n\
|
||||
timeout = 120\n\
|
||||
\n\
|
||||
[install]\n\
|
||||
trusted-host = mirrors.aliyun.com\n' > /etc/pip/pip.conf && \
|
||||
# 同时为 root 用户创建配置(备用)
|
||||
mkdir -p /root/.config/pip && \
|
||||
cp /etc/pip/pip.conf /root/.config/pip/pip.conf && \
|
||||
# 为 user 用户创建配置目录(后续会设置权限)
|
||||
mkdir -p /home/user/.config/pip && \
|
||||
cp /etc/pip/pip.conf /home/user/.config/pip/pip.conf && \
|
||||
echo "✅ pip 镜像源配置完成: https://mirrors.aliyun.com/pypi/simple/"
|
||||
|
||||
# ============================================================================
|
||||
# 📦 安装常用 Python 数据科学库 (系统级,所有用户可用)
|
||||
# ============================================================================
|
||||
# 🎯 最佳实践:指定稳定版本,避免版本冲突
|
||||
# 1. 升级基础工具确保依赖解析正确
|
||||
# 2. 按依赖关系顺序安装(numpy -> pandas/scipy -> matplotlib -> 其他)
|
||||
# 3. 使用经过验证的稳定版本组合
|
||||
|
||||
# 🎯 使用 requirements.txt 方式安装依赖
|
||||
# ======================================
|
||||
# 优势:版本锁定、可重现构建、便于维护
|
||||
|
||||
# 复制 requirements.txt 文件到容器
|
||||
COPY requirements.txt /tmp/requirements.txt
|
||||
|
||||
# 升级基础工具并安装依赖
|
||||
RUN pip3 install --break-system-packages --upgrade pip setuptools wheel && \
|
||||
pip3 install --break-system-packages -r /tmp/requirements.txt && \
|
||||
echo "✅ Python 数据科学库安装完成" && \
|
||||
echo "已安装的包:" && \
|
||||
pip3 list | grep -E '(numpy|pandas|scipy|matplotlib|openpyxl|python-docx|PyMuPDF|xlrd|xlwt|seaborn|requests|BeautifulSoup|lxml|scikit-learn)'
|
||||
|
||||
# ============================================================================
|
||||
# 📺 noVNC 远程桌面客户端(based on official v1.6.0, with customized vnc.html):
|
||||
# ============================================================================
|
||||
# 添加构建参数以强制打破缓存(每次构建时传入不同的 CACHEBUST_NOVNC)
|
||||
ARG CACHEBUST_NOVNC=1
|
||||
RUN echo "noVNC cache bust: ${CACHEBUST_NOVNC}" && \
|
||||
if [ "${USE_GITHUB_MIRROR}" = "true" ]; then \
|
||||
echo "Using GitHub mirror: ${GITHUB_MIRROR_URL}"; \
|
||||
echo "Cloning noVNC from: ${GITHUB_MIRROR_URL}nuwax-ai/noVNC.git"; \
|
||||
git clone --depth 1 -b release ${GITHUB_MIRROR_URL}nuwax-ai/noVNC.git /opt/noVNC && \
|
||||
echo "Cloning websockify from: ${GITHUB_MIRROR_URL}novnc/websockify"; \
|
||||
git clone --depth 1 -b v0.12.0 ${GITHUB_MIRROR_URL}novnc/websockify /opt/noVNC/utils/websockify; \
|
||||
else \
|
||||
echo "Using direct GitHub (no mirror)"; \
|
||||
git clone --depth 1 -b release https://github.com/nuwax-ai/noVNC.git /opt/noVNC && \
|
||||
git clone --depth 1 -b v0.12.0 https://github.com/novnc/websockify /opt/noVNC/utils/websockify; \
|
||||
fi && \
|
||||
cd /opt/noVNC && \
|
||||
ln -s /opt/noVNC/vnc.html /opt/noVNC/index.html
|
||||
|
||||
# ============================================================================
|
||||
# Install LibreOffice
|
||||
# ============================================================================
|
||||
# Installation logic:
|
||||
# 1. If local tar.gz exists in downloads/ (pre-downloaded via Makefile) -> use it
|
||||
# 2. Otherwise -> download from official URL (fallback)
|
||||
#
|
||||
# This allows:
|
||||
# - Fast build with pre-downloaded files (avoid slow external download)
|
||||
# - Fallback to online download if local file is missing
|
||||
# ============================================================================
|
||||
ARG LIBREOFFICE_FILE
|
||||
ARG TARGETARCH
|
||||
|
||||
COPY downloads /tmp/downloads
|
||||
|
||||
# Determine URL based on architecture
|
||||
RUN echo "Installing LibreOffice..." && \
|
||||
case "${TARGETARCH}" in \
|
||||
"amd64") \
|
||||
LIBREOFFICE_URL="https://download.documentfoundation.org/libreoffice/stable/25.8.5/deb/x86_64/${LIBREOFFICE_FILE}" \
|
||||
;; \
|
||||
"arm64") \
|
||||
LIBREOFFICE_URL="https://download.documentfoundation.org/libreoffice/stable/25.8.5/deb/aarch64/${LIBREOFFICE_FILE}" \
|
||||
;; \
|
||||
*) echo "❌ Unsupported architecture: ${TARGETARCH}" && exit 1 ;; \
|
||||
esac && \
|
||||
# Check if local pre-downloaded file exists
|
||||
if [ -f "/tmp/downloads/${LIBREOFFICE_FILE}" ]; then \
|
||||
echo "📦 Using pre-downloaded file: /tmp/downloads/${LIBREOFFICE_FILE}"; \
|
||||
cp /tmp/downloads/${LIBREOFFICE_FILE} /tmp/libreoffice.tar.gz; \
|
||||
else \
|
||||
echo "🌐 No local file found, downloading from official URL..."; \
|
||||
wget -q -O /tmp/libreoffice.tar.gz "${LIBREOFFICE_URL}" || { \
|
||||
echo "❌ ERROR: Failed to download LibreOffice from ${LIBREOFFICE_URL}"; \
|
||||
exit 1; \
|
||||
}; \
|
||||
fi && \
|
||||
# Extract and install
|
||||
cd /tmp && \
|
||||
tar -xzf libreoffice.tar.gz && \
|
||||
cd LibreOffice_*/DEBS && \
|
||||
dpkg -i *.deb && \
|
||||
apt-get update && \
|
||||
apt-get install -f -y && \
|
||||
# Create symlinks for LibreOffice executables
|
||||
LIBREOFFICE_BIN=$(find /opt -name "soffice" -path "*/program/*" 2>/dev/null | head -1) && \
|
||||
if [ -n "$LIBREOFFICE_BIN" ]; then \
|
||||
LIBREOFFICE_DIR=$(dirname "$LIBREOFFICE_BIN") && \
|
||||
ln -sf "$LIBREOFFICE_BIN" /usr/bin/soffice && \
|
||||
ln -sf "$LIBREOFFICE_DIR/libreoffice" /usr/bin/libreoffice 2>/dev/null || true && \
|
||||
echo "✅ LibreOffice installed: $LIBREOFFICE_BIN -> /usr/bin/soffice"; \
|
||||
else \
|
||||
echo "❌ ERROR: LibreOffice soffice binary not found in /opt"; \
|
||||
exit 1; \
|
||||
fi && \
|
||||
# Clean up
|
||||
rm -rf /tmp/LibreOffice_* /tmp/libreoffice.tar.gz /tmp/downloads && \
|
||||
apt-get autoremove -y && \
|
||||
apt-get autoclean && \
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
|
||||
|
||||
# User applications:
|
||||
|
||||
# ~ Make your changes to this template BELOW this line ~
|
||||
|
||||
# Install Chromium from Debian official repository (optimized single layer)
|
||||
RUN apt-get update && \
|
||||
apt-get install -y chromium chromium-driver && \
|
||||
# Clean up package cache to reduce image size:
|
||||
apt-get autoremove -y && \
|
||||
apt-get autoclean && \
|
||||
rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*
|
||||
|
||||
# Configure Chromium to skip first-run experience and enable CDP
|
||||
RUN mkdir -p /etc/chromium/policies/managed/ && \
|
||||
echo '{"CommandLineFlagSecurityWarningsEnabled": false, "AllowInsecureLocalConnections": true, "RemoteDebuggingAllowed": true, "RemoteDebuggingPort": "9222", "RemoteDebuggingAddress":"0.0.0.0", "AutoplayAllowed": true, "MetricsReportingEnabled": false, "BrowserSignin": 0, "SyncDisabled": true, "PromotionalTabsEnabled": false, "DefaultBrowserSettingEnabled": false, "DefaultSearchProviderEnabled": false, "HideWebStoreIcon": true, "PasswordManagerEnabled": false, "PromptForDownloadLocation": false, "SafeBrowsingEnabled": false, "SafeBrowsingExtendedReportingEnabled": false, "UrlKeyedAnonymizedDataCollectionEnabled": false, "CloudPrintSubmitEnabled": false, "CloudManagementEnrollmentToken": "", "SpellcheckEnabled": false}' > /etc/chromium/policies/managed/policies.json
|
||||
|
||||
# Create user and configure Chrome (merged for smaller image size)
|
||||
RUN apt-get update && \
|
||||
apt-get install -y gnome-keyring policykit-1-gnome policykit-1 && \
|
||||
# Create default user
|
||||
useradd -ms /bin/bash user && \
|
||||
usermod -aG sudo user && \
|
||||
passwd -d user && \
|
||||
echo "user ALL=(ALL:ALL) NOPASSWD: ALL" >>/etc/sudoers && \
|
||||
# Create Chromium configuration directories
|
||||
mkdir -p /home/user/.config/chromium/Default/ && \
|
||||
echo '{ "browser": { "custom_chrome_frame": true, "has_seen_welcome_page": true }, "profile": { "default_content_setting_values": { "geolocation": 1 }, "exit_type": "Normal", "exited_cleanly": true, "password_manager_enabled": false }, "credentials_enable_service": false, "credentials_enable_autosignin": false }' > /home/user/.config/chromium/Default/Preferences && \
|
||||
# Configure GNOME keyring with empty password (禁用密码提示)
|
||||
mkdir -p /home/user/.local/share/keyrings && \
|
||||
# 创建 Default keyring(无密码,禁用锁定)
|
||||
printf '[keyring]\ndisplay-name=Default keyring\nctime=1732780800\nmtime=1732780800\nlock-on-idle=false\nlock-after=false\n' > /home/user/.local/share/keyrings/Default_keyring.keyring && \
|
||||
# 设置为默认 keyring
|
||||
echo 'Default_keyring' > /home/user/.local/share/keyrings/default && \
|
||||
chmod 600 /home/user/.local/share/keyrings/Default_keyring.keyring && \
|
||||
chmod 600 /home/user/.local/share/keyrings/default && \
|
||||
# Configure PolicyKit to auto-allow Chromium operations
|
||||
mkdir -p /etc/polkit-1/localauthority/50-local.d && \
|
||||
# Create comprehensive policy for user
|
||||
printf '[Allow all for user]\nIdentity=unix-user:user\nAction=*\nResultAny=yes\nResultInactive=yes\nResultActive=yes\n\n[Allow all for user - specific patterns]\nIdentity=unix-user:user\nAction=org.freedesktop.policykit.*\nResultAny=yes\nResultInactive=yes\nResultActive=yes\n\n[Allow Chromium specific]\nIdentity=unix-user:user\nAction=org.chromium.*\nResultAny=yes\nResultInactive=yes\nResultActive=yes' > /etc/polkit-1/localauthority/50-local.d/comprehensive-allow.pkla && \
|
||||
# ========== 系统级自启动配置 (不受用户目录挂载影响) ==========
|
||||
mkdir -p /etc/xdg/autostart && \
|
||||
# PolicyKit authentication agent autostart
|
||||
printf "[Desktop Entry]\nType=Application\nName=PolicyKit Auth\nExec=/usr/lib/policykit-1-gnome/polkit-gnome-authentication-agent-1\nHidden=false\nNoDisplay=false\nX-GNOME-Autostart-enabled=true\nStartupNotify=false" > /etc/xdg/autostart/polkit-gnome-auth.desktop && \
|
||||
# D-Bus session autostart
|
||||
printf "[Desktop Entry]\nType=Application\nName=D-Bus Session\nExec=/usr/bin/dbus-launch --sh-syntax\nHidden=false\nNoDisplay=false\nX-GNOME-Autostart-enabled=true" > /etc/xdg/autostart/dbus-session.desktop && \
|
||||
# PolicyKit daemon autostart
|
||||
printf "[Desktop Entry]\nType=Application\nName=PolicyKit Daemon\nExec=/usr/lib/policykit-1/polkitd --no-debug\nHidden=false\nNoDisplay=false\nX-GNOME-Autostart-enabled=true" > /etc/xdg/autostart/polkitd.desktop && \
|
||||
# Fcitx5 autostart
|
||||
printf '[Desktop Entry]\nType=Application\nName=Fcitx 5\nComment=Input Method Framework\nExec=fcitx5 -d --replace\nIcon=fcitx5\nStartupNotify=false\nTerminal=false\nCategories=System;Utility;\nX-GNOME-Autostart-enabled=true\nX-XFCE-Autostart-enabled=true' > /etc/xdg/autostart/fcitx5.desktop && \
|
||||
# 创建用户运行时目录
|
||||
mkdir -p /run/user/$(id -u user) && \
|
||||
# ========== 系统级 fcitx5 输入法配置 ==========
|
||||
mkdir -p /etc/xdg/fcitx5/conf && \
|
||||
# fcitx5 全局配置
|
||||
printf '[Hotkey]\nEnumerateWithTriggerKeys=True\nAltTriggerKeys=\nEnumerateForwardKeys=\nEnumerateBackwardKeys=\nEnumerateSkipFirst=False\n\n[Hotkey/TriggerKeys]\n0=Control+space\n1=Shift+Control+space\n\n[Hotkey/EnumerateGroupForwardKeys]\n0=Super+space\n\n[Hotkey/EnumerateGroupBackwardKeys]\n0=Shift+Super+space\n\n[Hotkey/ActivateKeys]\n0=Hangul_Hanja\n\n[Hotkey/DeactivateKeys]\n0=Hangul_Romaja\n\n[Hotkey/PrevPage]\n0=Up\n\n[Hotkey/NextPage]\n0=Down\n\n[Hotkey/PrevCandidate]\n0=Shift+Tab\n\n[Hotkey/NextCandidate]\n0=Tab\n\n[Hotkey/TogglePreedit]\n0=Control+Alt+P\n\n[Behavior]\nActiveByDefault=False\nShareInputState=No\nPreloadInputMethod=True\nShowInputMethodInformation=True\nShowInputMethodInformationWhenFocusIn=False\nCompactInputMethodInformation=True\nShowFirstInputMethodInformation=True\nDefaultPageSize=5\nOverrideXkbOption=False\nCustomXkbOption=\nEnabledAddons=\nDisabledAddons=\nPreeditEnabledByDefault=True' > /etc/xdg/fcitx5/config && \
|
||||
# 拼音配置
|
||||
printf '[Addon]\nEnabled=True\n\n[Behavior]\nPreeditInApplication=False\n\n[PinyinEngine]\nShuangpinProfile=Ziranma\nShowActualPinyinInHint=False\nPredictionEnabled=True\nPredictionSize=10\nCloudPinyinEnabled=False\nCloudPinyinIndex=2' > /etc/xdg/fcitx5/conf/pinyin.conf && \
|
||||
# 禁用云拼音提示
|
||||
printf '[Addon]\nEnabled=False\n\n[Behavior]\nShowNotification=False' > /etc/xdg/fcitx5/conf/cloudpinyin.conf && \
|
||||
# 禁用通知
|
||||
printf '[Addon]\nEnabled=False\n\n[Behavior]\nHiddenNotifications=' > /etc/xdg/fcitx5/conf/notifications.conf && \
|
||||
# fcitx5 profile (系统默认: 英文键盘,用户可通过 Ctrl+Space 切换到中文拼音)
|
||||
printf '[Groups/0]\nName=Default\nDefault Layout=us\nDefaultIM=keyboard-us\n\n[Groups/0/Items/0]\nName=keyboard-us\nLayout=\n\n[Groups/0/Items/1]\nName=pinyin\nLayout=\n\n[GroupOrder]\n0=Default\n\n[Behavior]\nActiveByDefault=False\nShareInputState=No' > /etc/xdg/fcitx5/profile && \
|
||||
# ========== 系统级环境变量配置 (替代 .bashrc/.xprofile) ==========
|
||||
mkdir -p /etc/profile.d && \
|
||||
printf '#!/bin/sh\n# Fcitx5 Input Method Environment Variables\nexport GTK_IM_MODULE=fcitx5\nexport QT_IM_MODULE=fcitx5\nexport XMODIFIERS=@im=fcitx5\nexport INPUT_METHOD=fcitx5\nexport SDL_IM_MODULE=fcitx5\nexport GLFW_IM_MODULE=fcitx5\n' > /etc/profile.d/fcitx5-env.sh && \
|
||||
chmod +x /etc/profile.d/fcitx5-env.sh && \
|
||||
# ========== 系统级 GTK 配置 ==========
|
||||
mkdir -p /etc/gtk-3.0 && \
|
||||
printf '[Settings]\ngtk-im-module=fcitx5' > /etc/gtk-3.0/settings.ini && \
|
||||
# ========== 隐藏 root 用户警告 ==========
|
||||
# 为 root 用户创建 GTK CSS 配置,彻底隐藏 Thunar 和 XFCE 的各种 root 警告
|
||||
mkdir -p /root/.config/gtk-3.0 && \
|
||||
printf '/* Hide Thunar root warnings completely */\n.thunar-window infobar.warning { min-height: 0; max-height: 0; padding: 0; margin: 0; opacity: 0; }\n.thunar-window infobar.warning * { min-height: 0; max-height: 0; padding: 0; margin: 0; opacity: 0; }\n.thunar-window infobar.warning button { min-height: 0; min-width: 0; max-height: 0; padding: 0; margin: 0; opacity: 0; }\ninfobar.warning { min-height: 0; max-height: 0; padding: 0; margin: 0; opacity: 0; }\ninfobar.warning * { min-height: 0; max-height: 0; padding: 0; margin: 0; opacity: 0; }\n/* Hide XFCE root warning */\n.root-warning { display: none !important; opacity: 0 !important; }\n.root-warning * { display: none !important; opacity: 0 !important; }\n' > /root/.config/gtk-3.0/gtk.css && \
|
||||
# 同时为 /home/user 添加配置(因为 HOME=/home/user)
|
||||
mkdir -p /home/user/.config/gtk-3.0 && \
|
||||
cp /root/.config/gtk-3.0/gtk.css /home/user/.config/gtk-3.0/gtk.css && \
|
||||
# 禁用 XFCE 会话的 root 警告(系统级配置)
|
||||
mkdir -p /root/.config/xfce4/xfconf/xfce-perchannel-xml && \
|
||||
echo '<?xml version="1.0" encoding="UTF-8"?>\n<channel name="xfce4-session" version="1.0">\n <property name="general" type="empty">\n <property name="ShowRootWarning" type="bool" value="false"/>\n </property>\n</channel>' > /root/.config/xfce4/xfconf/xfce-perchannel-xml/xfce4-session.xml && \
|
||||
# 同时为 user 配置
|
||||
mkdir -p /home/user/.config/xfce4/xfconf/xfce-perchannel-xml && \
|
||||
cp /root/.config/xfce4/xfconf/xfce-perchannel-xml/xfce4-session.xml /home/user/.config/xfce4/xfconf/xfce-perchannel-xml/xfce4-session.xml && \
|
||||
# ========== 系统级 im-config ==========
|
||||
echo 'fcitx5' > /etc/X11/xinit/xinputrc 2>/dev/null || true && \
|
||||
# ========== 用户目录 - 只保留需要持久化的配置 ==========
|
||||
# Chromium 配置 (用户可能需要持久化浏览器数据)
|
||||
mkdir -p /home/user/.config/chromium/Default/ && \
|
||||
echo '{ "browser": { "custom_chrome_frame": true, "has_seen_welcome_page": true }, "profile": { "default_content_setting_values": { "geolocation": 1 }, "exit_type": "Normal", "exited_cleanly": true, "password_manager_enabled": false }, "credentials_enable_service": false, "credentials_enable_autosignin": false }' > /home/user/.config/chromium/Default/Preferences && \
|
||||
# GNOME keyring (禁用密码提示)
|
||||
mkdir -p /home/user/.local/share/keyrings && \
|
||||
printf '[keyring]\ndisplay-name=Default keyring\nctime=1732780800\nmtime=1732780800\nlock-on-idle=false\nlock-after=false\n' > /home/user/.local/share/keyrings/Default_keyring.keyring && \
|
||||
echo 'Default_keyring' > /home/user/.local/share/keyrings/default && \
|
||||
chmod 600 /home/user/.local/share/keyrings/Default_keyring.keyring && \
|
||||
chmod 600 /home/user/.local/share/keyrings/default && \
|
||||
# 创建基本的用户 .bashrc (只包含别名,不包含输入法配置)
|
||||
printf '# User aliases\nalias chromium="/usr/local/bin/chromium"\nalias chrome="/usr/local/bin/chromium"\nalias code="code --no-sandbox"\n' > /home/user/.bashrc && \
|
||||
# 设置权限(安全配置:755 允许读取和执行,所有者可写)
|
||||
chown -R user:user /home/user /run/user/$(id -u user) && \
|
||||
chmod 755 /home/user && \
|
||||
chmod 700 /run/user/$(id -u user) && \
|
||||
chown -R 1000:1000 /home/user/.config && \
|
||||
chown -R 1000:1000 /home/user/.local && \
|
||||
# Clean up package cache
|
||||
apt-get autoremove -y && \
|
||||
apt-get autoclean && \
|
||||
rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*
|
||||
|
||||
# Copy Chromium browser launcher script with fcitx support (runs as root, HOME=/home/user)
|
||||
# IMPORTANT: Replace /usr/bin/chromium with our launcher so XFCE's exo-open uses it
|
||||
COPY chromium-browser-launcher.sh /usr/bin/chromium-browser-launcher
|
||||
RUN chmod 755 /usr/bin/chromium-browser-launcher && \
|
||||
# Backup original chromium wrapper
|
||||
mv /usr/bin/chromium /usr/bin/chromium-bin && \
|
||||
# Replace /usr/bin/chromium with our launcher (XFCE uses this path)
|
||||
cp /usr/bin/chromium-browser-launcher /usr/bin/chromium && \
|
||||
ln -sf /usr/bin/chromium-browser-launcher /usr/local/bin/chromium
|
||||
|
||||
# Copy MCP-specific Chromium wrapper script (runs as user to connect to fcitx5)
|
||||
COPY chromium-for-mcp.sh /usr/local/bin/chromium-for-mcp
|
||||
RUN chmod 755 /usr/local/bin/chromium-for-mcp
|
||||
|
||||
# Create a socat service with supervisor to ensure port forwarding is always running
|
||||
# RUN mkdir -p /etc/supervisor/conf.d && \
|
||||
# echo '[supervisord]\nnodaemon=true\n\n[program:socat]\ncommand=socat TCP4-LISTEN:9223,fork,reuseaddr TCP4:127.0.0.1:9222\nautostart=true\nautorestart=true\nstdout_logfile=/var/log/socat.log\nstdout_logfile_maxbytes=1MB\nstdout_logfile_backups=10\nstderr_logfile=/var/log/socat.err\nstderr_logfile_maxbytes=1MB\nstderr_logfile_backups=10' > /etc/supervisor/conf.d/socat.conf
|
||||
|
||||
# Start the socat service at system boot
|
||||
# RUN echo '#!/bin/bash\n# Start supervisor to manage socat service\n/usr/bin/supervisord -c /etc/supervisor/supervisord.conf &' > /usr/local/bin/start-port-forward.sh && \
|
||||
# chmod +x /usr/local/bin/start-port-forward.sh && \
|
||||
# echo "[Desktop Entry]\nType=Application\nName=PortForward\nExec=/usr/local/bin/start-port-forward.sh\nHidden=false\nNoDisplay=false\nX-GNOME-Autostart-enabled=true" > /home/user/.config/autostart/port-forward.desktop
|
||||
|
||||
# 添加自动启动supervisord的桌面启动项 (使用系统级配置)
|
||||
RUN echo '#!/bin/bash\n# 启动supervisord服务\nsudo /usr/bin/supervisord -c /etc/supervisor/supervisord.conf &' > /usr/local/bin/start-supervisord.sh && \
|
||||
chmod +x /usr/local/bin/start-supervisord.sh && \
|
||||
echo "[Desktop Entry]\nType=Application\nName=Supervisord\nExec=/usr/local/bin/start-supervisord.sh\nHidden=false\nNoDisplay=false\nX-GNOME-Autostart-enabled=true" > /etc/xdg/autostart/supervisord.desktop && \
|
||||
chmod 644 /etc/xdg/autostart/supervisord.desktop
|
||||
|
||||
# Install VS Code - 使用 apt 仓库方式(比直接下载 deb 更可靠)
|
||||
RUN echo "📦 Adding Microsoft VS Code repository..." && \
|
||||
# 添加微软 GPG 密钥
|
||||
curl -fsSL https://packages.microsoft.com/keys/microsoft.asc | gpg --dearmor -o /usr/share/keyrings/microsoft-archive-keyring.gpg && \
|
||||
# 添加 apt 仓库
|
||||
echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/microsoft-archive-keyring.gpg] https://packages.microsoft.com/repos/code stable main" > /etc/apt/sources.list.d/vscode.list && \
|
||||
echo "📥 Installing VS Code via apt..." && \
|
||||
apt-get update && \
|
||||
apt-get install -y code && \
|
||||
echo "🔍 Checking installed VS Code..." && \
|
||||
which code && \
|
||||
ls -la /usr/bin/code && \
|
||||
echo "✅ VS Code installed via apt!" && \
|
||||
# Cleanup
|
||||
apt-get autoremove -y && \
|
||||
apt-get autoclean && \
|
||||
rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*
|
||||
|
||||
# 验证关键软件安装是否成功
|
||||
RUN echo "🔍 Verifying installations..." && \
|
||||
# VS Code - 检查多个可能的路径
|
||||
CODE_PATH="" && \
|
||||
if [ -x /usr/bin/code ]; then CODE_PATH="/usr/bin/code"; \
|
||||
elif [ -x /usr/share/code/bin/code ]; then CODE_PATH="/usr/share/code/bin/code"; \
|
||||
elif command -v code >/dev/null 2>&1; then CODE_PATH="$(command -v code)"; \
|
||||
fi && \
|
||||
if [ -z "$CODE_PATH" ]; then \
|
||||
echo "❌ VS Code installation failed! No 'code' binary found" && \
|
||||
echo "📂 Searching for code binary..." && \
|
||||
find /usr -name "code" -type f 2>/dev/null | head -10 && \
|
||||
exit 1; \
|
||||
fi && \
|
||||
echo "✅ VS Code found at: $CODE_PATH" && \
|
||||
# 如果 /usr/bin/code 不存在,创建软链接
|
||||
if [ ! -x /usr/bin/code ] && [ -n "$CODE_PATH" ]; then \
|
||||
ln -sf "$CODE_PATH" /usr/bin/code && \
|
||||
echo "🔗 Created symlink: /usr/bin/code -> $CODE_PATH"; \
|
||||
fi && \
|
||||
# Chromium
|
||||
if [ ! -x /usr/bin/chromium ]; then \
|
||||
echo "❌ Chromium installation failed!" && exit 1; \
|
||||
fi && \
|
||||
echo "✅ Chromium: $(/usr/bin/chromium --version 2>/dev/null || echo 'installed')" && \
|
||||
echo "🎉 All critical software verified!"
|
||||
|
||||
RUN mkdir -p /home/user/.config/Code/User
|
||||
COPY ./settings.json /home/user/.config/Code/User/settings.json
|
||||
|
||||
# Copy desktop background for XFCE
|
||||
COPY ./wallpaper.jpeg /usr/share/backgrounds/xfce/wallpaper.jpeg
|
||||
# Copy xfce4-desktop.xml to system-level config
|
||||
RUN mkdir -p /etc/xdg/xfce4/xfconf/xfce-perchannel-xml/
|
||||
COPY ./xfce4-desktop.xml /etc/xdg/xfce4/xfconf/xfce-perchannel-xml/xfce4-desktop.xml
|
||||
|
||||
# ========== 系统级 XFCE 配置 (不受用户目录挂载影响) ==========
|
||||
RUN mkdir -p /etc/xdg/xfce4/xfconf/xfce-perchannel-xml/ && \
|
||||
# Disable screen saver
|
||||
echo '<?xml version="1.0" encoding="UTF-8"?>\n<channel name="xfce4-screensaver" version="1.0">\n <property name="saver" type="empty">\n <property name="enabled" type="bool" value="false"/>\n <property name="mode" type="int" value="0"/>\n </property>\n <property name="lock" type="empty">\n <property name="enabled" type="bool" value="false"/>\n </property>\n</channel>' > /etc/xdg/xfce4/xfconf/xfce-perchannel-xml/xfce4-screensaver.xml && \
|
||||
# Disable power management screen dimming
|
||||
echo '<?xml version="1.0" encoding="UTF-8"?>\n<channel name="xfce4-power-manager" version="1.0">\n <property name="xfce4-power-manager" type="empty">\n <property name="blank-on-ac" type="int" value="0"/>\n <property name="blank-on-battery" type="int" value="0"/>\n <property name="dpms-enabled" type="bool" value="false"/>\n <property name="dpms-on-ac-sleep" type="uint" value="0"/>\n <property name="dpms-on-ac-off" type="uint" value="0"/>\n <property name="dpms-on-battery-sleep" type="uint" value="0"/>\n <property name="dpms-on-battery-off" type="uint" value="0"/>\n </property>\n</channel>' > /etc/xdg/xfce4/xfconf/xfce-perchannel-xml/xfce4-power-manager.xml && \
|
||||
# Configure XFCE panel with top bar (panel-1) and bottom dock (panel-2)
|
||||
echo '<?xml version="1.0" encoding="UTF-8"?>\n<channel name="xfce4-panel" version="1.0">\n <property name="configver" type="int" value="2"/>\n <property name="panels" type="array">\n <value type="int" value="1"/>\n <value type="int" value="2"/>\n <property name="dark-mode" type="bool" value="true"/>\n <property name="panel-1" type="empty">\n <property name="position" type="string" value="p=6;x=0;y=0"/>\n <property name="length" type="uint" value="100"/>\n <property name="position-locked" type="bool" value="true"/>\n <property name="icon-size" type="uint" value="16"/>\n <property name="size" type="uint" value="26"/>\n <property name="plugin-ids" type="array">\n <value type="int" value="1"/>\n <value type="int" value="2"/>\n <value type="int" value="3"/>\n <value type="int" value="4"/>\n <value type="int" value="5"/>\n <value type="int" value="6"/>\n <value type="int" value="7"/>\n <value type="int" value="8"/>\n </property>\n </property>\n <property name="panel-2" type="empty">\n <property name="autohide-behavior" type="uint" value="1"/>\n <property name="position" type="string" value="p=10;x=0;y=0"/>\n <property name="length" type="uint" value="1"/>\n <property name="position-locked" type="bool" value="true"/>\n <property name="size" type="uint" value="48"/>\n <property name="plugin-ids" type="array">\n <value type="int" value="15"/>\n <value type="int" value="16"/>\n <value type="int" value="17"/>\n <value type="int" value="18"/>\n <value type="int" value="19"/>\n <value type="int" value="20"/>\n <value type="int" value="21"/>\n </property>\n </property>\n </property>\n <property name="plugins" type="empty">\n <property name="plugin-1" type="string" value="applicationsmenu"/>\n <property name="plugin-2" type="string" value="tasklist">\n <property name="grouping" type="uint" value="1"/>\n </property>\n <property name="plugin-3" type="string" value="separator">\n <property name="expand" type="bool" value="true"/>\n <property name="style" type="uint" value="0"/>\n </property>\n <property name="plugin-4" type="string" value="systray">\n <property name="square-icons" type="bool" value="true"/>\n </property>\n <property name="plugin-5" type="string" value="separator">\n <property name="style" type="uint" value="0"/>\n </property>\n <property name="plugin-6" type="string" value="notification-plugin"/>\n <property name="plugin-7" type="string" value="clock"/>\n <property name="plugin-8" type="string" value="actions"/>\n <property name="plugin-15" type="string" value="showdesktop"/>\n <property name="plugin-16" type="string" value="separator"/>\n <property name="plugin-17" type="string" value="launcher">\n <property name="items" type="array">\n <value type="string" value="xfce4-terminal-emulator.desktop"/>\n </property>\n </property>\n <property name="plugin-18" type="string" value="launcher">\n <property name="items" type="array">\n <value type="string" value="xfce4-file-manager.desktop"/>\n </property>\n </property>\n <property name="plugin-19" type="string" value="launcher">\n <property name="items" type="array">\n <value type="string" value="chromium.desktop"/>\n </property>\n </property>\n <property name="plugin-20" type="string" value="launcher">\n <property name="items" type="array">\n <value type="string" value="xfce4-appfinder.desktop"/>\n </property>\n </property>\n <property name="plugin-21" type="string" value="directorymenu"/>\n </property>\n</channel>' > /etc/xdg/xfce4/xfconf/xfce-perchannel-xml/xfce4-panel.xml && \
|
||||
# Create launcher directories for bottom dock (系统级)
|
||||
mkdir -p /etc/xdg/xfce4/panel/launcher-17 && \
|
||||
mkdir -p /etc/xdg/xfce4/panel/launcher-18 && \
|
||||
mkdir -p /etc/xdg/xfce4/panel/launcher-19 && \
|
||||
mkdir -p /etc/xdg/xfce4/panel/launcher-20 && \
|
||||
# Create terminal launcher (plugin-17)
|
||||
printf '[Desktop Entry]\nVersion=1.0\nType=Application\nExec=xfce4-terminal\nIcon=org.xfce.terminalemulator\nStartupNotify=true\nTerminal=false\nCategories=Utility;X-XFCE;\nName=Terminal Emulator\nComment=Use the command line\n' > /etc/xdg/xfce4/panel/launcher-17/xfce4-terminal-emulator.desktop && \
|
||||
# Create file manager launcher (plugin-18)
|
||||
printf '[Desktop Entry]\nVersion=1.0\nType=Application\nExec=thunar\nIcon=org.xfce.filemanager\nStartupNotify=true\nTerminal=false\nCategories=Utility;X-XFCE;\nName=File Manager\nComment=Browse the file system\n' > /etc/xdg/xfce4/panel/launcher-18/xfce4-file-manager.desktop && \
|
||||
# Create Chromium launcher (plugin-19) - replaces default Web Browser
|
||||
printf '[Desktop Entry]\nVersion=1.0\nType=Application\nName=Chromium\nComment=Web Browser\nExec=/usr/bin/chromium-browser-launcher\nIcon=chromium\nTerminal=false\nCategories=Network;WebBrowser;\nStartupNotify=true\n' > /etc/xdg/xfce4/panel/launcher-19/chromium.desktop && \
|
||||
# Create app finder launcher (plugin-20)
|
||||
printf '[Desktop Entry]\nVersion=1.0\nType=Application\nExec=xfce4-appfinder\nIcon=org.xfce.appfinder\nStartupNotify=true\nTerminal=false\nCategories=Utility;X-XFCE;\nName=Application Finder\nComment=Find and launch applications\n' > /etc/xdg/xfce4/panel/launcher-20/xfce4-appfinder.desktop && \
|
||||
# Create system-wide Chromium desktop entry
|
||||
mkdir -p /usr/share/applications && \
|
||||
printf "[Desktop Entry]\nVersion=1.0\nType=Application\nName=Chromium\nComment=Web Browser\nExec=/usr/bin/chromium-browser-launcher\nIcon=chromium\nTerminal=false\nCategories=Network;WebBrowser;" > /usr/share/applications/chromium-browser.desktop && \
|
||||
ln -sf /usr/share/applications/chromium-browser.desktop /usr/share/applications/chromium.desktop && \
|
||||
chmod 644 /usr/share/applications/chromium-browser.desktop /usr/share/applications/chromium.desktop && \
|
||||
# Set Chromium as default web browser (系统级)
|
||||
mkdir -p /usr/share/applications && \
|
||||
echo "[Default Applications]\ntext/html=chromium.desktop\ntext/xml=chromium.desktop\napplication/xhtml+xml=chromium.desktop\napplication/xml=chromium.desktop\napplication/rss+xml=chromium.desktop\napplication/rdf+xml=chromium.desktop\nimage/gif=chromium.desktop\nimage/jpeg=chromium.desktop\nimage/png=chromium.desktop\nx-scheme-handler/http=chromium.desktop\nx-scheme-handler/https=chromium.desktop\nx-scheme-handler/ftp=chromium.desktop\nx-scheme-handler/chrome=chromium.desktop\nvideo/webm=chromium.desktop\napplication/x-xpinstall=chromium.desktop" > /usr/share/applications/mimeapps.list
|
||||
|
||||
# Create desktop icons for terminal, VS Code, and Chrome
|
||||
RUN mkdir -p /home/user/Desktop /usr/share/applications && \
|
||||
# Create safe desktop files in system directory (not in user's home)
|
||||
printf "[Desktop Entry]\nVersion=1.0\nType=Application\nName=Terminal\nComment=Terminal Emulator\nExec=xfce4-terminal\nIcon=utilities-terminal\nTerminal=false\nCategories=System;TerminalEmulator;" > /usr/share/applications/user-terminal.desktop && \
|
||||
printf "[Desktop Entry]\nVersion=1.0\nType=Application\nName=Visual Studio Code\nComment=Code Editor\nExec=/usr/bin/code --no-sandbox --user-data-dir=/home/user/.config/Code\nIcon=vscode\nTerminal=false\nCategories=Development;IDE;" > /usr/share/applications/user-vscode.desktop && \
|
||||
chmod 644 /usr/share/applications/user-terminal.desktop /usr/share/applications/user-vscode.desktop && \
|
||||
# Create symbolic links from Desktop to system apps (安全软链接)
|
||||
ln -sf /usr/share/applications/user-terminal.desktop /home/user/Desktop/terminal.desktop && \
|
||||
ln -sf /usr/share/applications/user-vscode.desktop /home/user/Desktop/vscode.desktop && \
|
||||
ln -sf /usr/share/applications/chromium-browser.desktop /home/user/Desktop/chromium.desktop && \
|
||||
chown -R user:user /home/user/Desktop
|
||||
|
||||
# ========== 创建骨架目录 /etc/skel-user-desktop ==========
|
||||
# 将 /home/user 下的关键配置备份到此目录
|
||||
# 启动时,如果检测到 /home/user 是空的(被挂载覆盖),则从这里恢复
|
||||
# 这样可以解决:
|
||||
# 1. 挂载空目录到 /home/user 导致桌面图标消失
|
||||
# 2. 挂载空目录到 /home/user 导致 VNC 花屏(缺少关键缓存目录)
|
||||
RUN mkdir -p /etc/skel-user-desktop && \
|
||||
# 复制整个 /home/user 目录结构到骨架目录
|
||||
cp -a /home/user/. /etc/skel-user-desktop/ && \
|
||||
# 确保骨架目录只读(防止意外修改)
|
||||
chmod 755 /etc/skel-user-desktop && \
|
||||
echo "✅ Skeleton directory created at /etc/skel-user-desktop"
|
||||
|
||||
# Copy firefox policies
|
||||
COPY firefox-policies.json /usr/lib/firefox-esr/distribution/policies.json
|
||||
COPY firefox-autoconfig.js /usr/lib/firefox-esr/defaults/pref/autoconfig.js
|
||||
COPY firefox.cfg /usr/lib/firefox-esr/firefox.cfg
|
||||
|
||||
|
||||
# Code interpretor environment setup
|
||||
# 系统依赖和Python 3安装 (merged for smaller image size)
|
||||
RUN DEBIAN_FRONTEND=noninteractive apt-get update && \
|
||||
apt-get install -y --no-install-recommends \
|
||||
build-essential curl git util-linux jq sudo fonts-noto-cjk fonts-noto-color-emoji \
|
||||
systemd ca-certificates chrony r-base software-properties-common \
|
||||
libzmq3-dev libzmq5 pkg-config && \
|
||||
# 使用 Debian 12 默认的 Python 3.11 (更稳定)
|
||||
apt-get install -y python3 python3-dev python3-venv python3-pip && \
|
||||
ln -sf /usr/bin/python3 /usr/bin/python && \
|
||||
# Python/Jupyter services and kernels removed, but Python3 environment kept
|
||||
# Clean up package cache to reduce image size:
|
||||
apt-get autoremove -y && \
|
||||
apt-get autoclean && \
|
||||
rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*
|
||||
|
||||
# 安装 Node.js 22.x (现在支持,因为不再依赖 ijavascript)
|
||||
RUN apt-get update && \
|
||||
apt-get install -y curl gnupg ca-certificates && \
|
||||
# 添加 NodeSource PPA for Node.js 22.x
|
||||
curl -fsSL https://deb.nodesource.com/setup_22.x | bash - && \
|
||||
# 安装 Node.js (npm 会一同被安装)
|
||||
apt-get install -y nodejs && \
|
||||
# 更新证书
|
||||
update-ca-certificates && \
|
||||
# Clean up package cache to reduce image size:
|
||||
apt-get autoremove -y && \
|
||||
apt-get autoclean && \
|
||||
rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*
|
||||
|
||||
# 使用 npm 安装 pnpm(使用阿里云镜像)
|
||||
RUN npm install -g pnpm@latest && \
|
||||
# 创建 pnpm 环境配置脚本(用户通过终端使用时会加载)
|
||||
echo '#!/bin/sh' > /etc/profile.d/pnpm-env.sh && \
|
||||
echo '# PNPM environment configuration' >> /etc/profile.d/pnpm-env.sh && \
|
||||
echo 'export PNPM_HOME="/home/user/.local/share/pnpm"' >> /etc/profile.d/pnpm-env.sh && \
|
||||
echo 'export npm_config_registry="https://registry.npmmirror.com"' >> /etc/profile.d/pnpm-env.sh && \
|
||||
chmod +x /etc/profile.d/pnpm-env.sh && \
|
||||
# Clean up npm cache
|
||||
npm cache clean --force && \
|
||||
echo "Node.js version: $(node -v)" && \
|
||||
echo "npm version: $(npm -v)" && \
|
||||
echo "pnpm version: $(pnpm -v)"
|
||||
|
||||
# 环境变量设置
|
||||
ENV R_VERSION=4.4.2
|
||||
|
||||
ENV R_HOME=/opt/R/${R_VERSION}
|
||||
|
||||
# # 创建默认用户
|
||||
# RUN useradd -ms /bin/bash user && \
|
||||
# usermod -aG sudo user && \
|
||||
# passwd -d user && \
|
||||
# echo "user ALL=(ALL:ALL) NOPASSWD: ALL" >>/etc/sudoers
|
||||
|
||||
# 设置目录权限(安全配置)
|
||||
RUN mkdir -p /home/user && \
|
||||
chown -R user:user /home/user && \
|
||||
chmod 755 /home/user && \
|
||||
chmod 755 /usr/local
|
||||
|
||||
# Python/Jupyter components removed
|
||||
|
||||
# JavaScript/TypeScript 使用 Deno kernel (不需要安装 ijavascript)
|
||||
# Deno kernel 在上面已经安装并配置完成
|
||||
|
||||
# Deno runtime (for JavaScript/TypeScript support)
|
||||
COPY --from=denoland/deno:bin-2.5.6 /deno /usr/bin/deno
|
||||
RUN chmod +x /usr/bin/deno && \
|
||||
# Clean up deno cache
|
||||
rm -rf /root/.cache/deno
|
||||
# Note: deno.json not copied as Jupyter is removed
|
||||
|
||||
# Bash Kernel (Python-based) removed
|
||||
|
||||
# R Kernel
|
||||
# RUN R -e "install.packages('IRkernel', repos='https://cloud.r-project.org'); IRkernel::installspec(user = FALSE, name = 'r', displayname = 'R')"
|
||||
|
||||
|
||||
# envd 服务已删除 - 不再需要环境守护进程
|
||||
|
||||
# 配置 chrony
|
||||
RUN mkdir -p /etc/chrony
|
||||
RUN echo 'makestep 1 -1' > /etc/chrony/chrony.conf
|
||||
|
||||
RUN mkdir -p /etc/systemd/system/chrony.service.d
|
||||
RUN echo '[Service]\n\
|
||||
ExecStart=\n\
|
||||
ExecStart=/usr/sbin/chronyd\n\
|
||||
User=root\n\
|
||||
Group=root' > /etc/systemd/system/chrony.service.d/override.conf
|
||||
|
||||
RUN systemctl enable chrony
|
||||
|
||||
# Java 配置 (完整 JDK 工具集)
|
||||
RUN apt-get update && \
|
||||
apt-get install -y openjdk-17-jdk && \
|
||||
# 验证安装
|
||||
java -version && \
|
||||
javac -version && \
|
||||
echo "✅ OpenJDK 17 installed with full toolset" && \
|
||||
# 确认 javac 存在
|
||||
which javac && \
|
||||
ls -la /usr/lib/jvm/java-17-openjdk-*/bin/javac && \
|
||||
# 清理缓存(但不 autoremove,避免删除 JDK 依赖)
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# ============================================================================
|
||||
# 📦 Maven 配置(使用阿里云镜像加速)
|
||||
# ============================================================================
|
||||
# 安装 Maven
|
||||
RUN apt-get update && apt-get install -y maven && \
|
||||
rm -rf /var/lib/apt/lists/* && \
|
||||
echo "✅ Maven installed: $(mvn --version | head -1)"
|
||||
|
||||
# 创建 Maven 配置目录(系统级和用户级)
|
||||
RUN mkdir -p /home/user/.m2 && \
|
||||
mkdir -p /etc/maven
|
||||
|
||||
# 复制 Maven 配置文件(阿里云镜像已配置)
|
||||
COPY settings.xml /home/user/.m2/settings.xml
|
||||
COPY settings.xml /etc/maven/settings.xml
|
||||
|
||||
# 设置权限
|
||||
RUN chown -R user:user /home/user/.m2 && \
|
||||
chmod 644 /home/user/.m2/settings.xml && \
|
||||
echo "✅ Maven configured with Aliyun mirror"
|
||||
|
||||
# 配置自动登录
|
||||
RUN mkdir -p /etc/systemd/system/serial-getty@ttyS0.service.d
|
||||
RUN echo '[Service]\n\
|
||||
ExecStart=\n\
|
||||
ExecStart=-/sbin/agetty --noissue --autologin root %I 115200,38400,9600 vt102' \
|
||||
> /etc/systemd/system/serial-getty@ttyS0.service.d/autologin.conf
|
||||
|
||||
# Python server components removed
|
||||
|
||||
# IPython/Jupyter config files removed
|
||||
|
||||
ENV DISPLAY=:0
|
||||
|
||||
# 设置全局输入法环境变量(容器级别 - 纯 fcitx)
|
||||
ENV GTK_IM_MODULE=fcitx \
|
||||
QT_IM_MODULE=fcitx \
|
||||
XMODIFIERS=@im=fcitx \
|
||||
INPUT_METHOD=fcitx \
|
||||
SDL_IM_MODULE=fcitx \
|
||||
GLFW_IM_MODULE=ibus
|
||||
|
||||
# 配置 D-Bus 和 PolicyKit(已在前面安装 dbus-x11)
|
||||
RUN mkdir -p /etc/dbus-1/session.d && \
|
||||
printf '<!-- Session bus for user -->\n<!DOCTYPE busconfig PUBLIC "-//freedesktop//DTD D-Bus Bus Configuration 1.0//EN" "http://www.freedesktop.org/standards/dbus/1.0/busconfig.dtd">\n<busconfig>\n <policy user="user">\n <allow own="*"/>\n <allow own_prefix="*"/>\n <allow send_destination="*"/>\n <allow receive_sender="*"/>\n </policy>\n</busconfig>' > /etc/dbus-1/session.d/user-session.conf && \
|
||||
printf '<!-- Allow root to access user session bus for fcitx5 input method -->\n<!DOCTYPE busconfig PUBLIC "-//freedesktop//DTD D-Bus Bus Configuration 1.0//EN" "http://www.freedesktop.org/standards/dbus/1.0/busconfig.dtd">\n<busconfig>\n <policy user="root">\n <allow own="*"/>\n <allow own_prefix="*"/>\n <allow send_destination="*"/>\n <allow receive_sender="*"/>\n <allow eavesdrop="true"/>\n </policy>\n</busconfig>' > /etc/dbus-1/session.d/allow-root.conf && \
|
||||
# Set proper permissions
|
||||
chown -R root:root /etc/polkit-1 /etc/dbus-1 && \
|
||||
chmod 755 /etc/polkit-1/localauthority/50-local.d /etc/dbus-1/session.d && \
|
||||
chmod 644 /etc/polkit-1/localauthority/50-local.d/*.pkla /etc/dbus-1/session.d/*.conf
|
||||
|
||||
# 安装 agent_runner 运行时依赖(与 rcoder/Dockerfile-runner 保持一致)
|
||||
RUN apt-get update && apt-get install -y \
|
||||
ca-certificates \
|
||||
curl \
|
||||
wget \
|
||||
gnupg \
|
||||
unzip \
|
||||
zip \
|
||||
tzdata \
|
||||
lsof \
|
||||
iproute2 \
|
||||
net-tools \
|
||||
nginx \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# 设置系统时区为东八区(Asia/Shanghai)
|
||||
RUN ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime && \
|
||||
echo "Asia/Shanghai" > /etc/timezone
|
||||
|
||||
# 清理默认nginx配置以避免冲突
|
||||
RUN rm -f /etc/nginx/sites-enabled/default && \
|
||||
rm -f /etc/nginx/conf.d/default.conf
|
||||
|
||||
# 配置 npm 使用国内镜像源(系统级配置)
|
||||
RUN npm config set registry https://registry.npmmirror.com && \
|
||||
# 创建 npm 环境配置脚本(用户通过终端使用时会加载)
|
||||
echo '#!/bin/sh' > /etc/profile.d/npm-env.sh && \
|
||||
echo '# NPM environment configuration' >> /etc/profile.d/npm-env.sh && \
|
||||
echo 'export npm_config_registry=https://registry.npmmirror.com' >> /etc/profile.d/npm-env.sh && \
|
||||
chmod +x /etc/profile.d/npm-env.sh
|
||||
|
||||
# 设置工作目录并确保权限
|
||||
RUN mkdir -p /app/computer-project-workspace /home/user/.cache && \
|
||||
chown -R user:user /app /home/user
|
||||
|
||||
# 安装 Bun 到全局路径(需要 root)
|
||||
ENV BUN_CONFIG_REGISTRY="https://registry.npmmirror.com" \
|
||||
BUN_CONFIG_INSTALL_CACHE_DIR="/home/user/.cache/bun/install" \
|
||||
BUN_CONFIG_GLOBAL_CACHE_DIR="/home/user/.cache/bun/global" \
|
||||
BUN_INSTALL="/usr/local" \
|
||||
PATH="/usr/local/bin:$PATH"
|
||||
|
||||
# 使用 npm 从阿里云 npmmirror 安装 bun 的 prebuilt 二进制,避免从 bun.sh/install 走 GitHub 下载不稳定
|
||||
# bun 的 npm 包通过 optionalDependencies 引入平台特定二进制(如 @oven/bun-linux-x64),npmmirror 已镜像
|
||||
# --prefix=/usr/local 让 bun 安装到 /usr/local/bin/bun(与上方 BUN_INSTALL 路径保持一致)
|
||||
RUN npm install -g --prefix=/usr/local --registry=https://registry.npmmirror.com bun && \
|
||||
/usr/local/bin/bun --version && \
|
||||
# 创建 bun 环境配置脚本(用户通过终端使用时会加载)
|
||||
echo '#!/bin/sh' > /etc/profile.d/bun-env.sh && \
|
||||
echo '# Bun environment configuration' >> /etc/profile.d/bun-env.sh && \
|
||||
echo 'export BUN_CONFIG_REGISTRY="https://registry.npmmirror.com"' >> /etc/profile.d/bun-env.sh && \
|
||||
echo 'export BUN_CONFIG_INSTALL_CACHE_DIR="/home/user/.cache/bun/install"' >> /etc/profile.d/bun-env.sh && \
|
||||
echo 'export BUN_CONFIG_GLOBAL_CACHE_DIR="/home/user/.cache/bun/global"' >> /etc/profile.d/bun-env.sh && \
|
||||
echo 'export BUN_INSTALL="/usr/local"' >> /etc/profile.d/bun-env.sh && \
|
||||
chmod +x /etc/profile.d/bun-env.sh && \
|
||||
# Clean up npm cache
|
||||
npm cache clean --force
|
||||
|
||||
# 安装全局 npm 包(需要 root,因为安装到 /usr/lib/node_modules)
|
||||
RUN npm install -g @zed-industries/claude-code-acp@latest && \
|
||||
npm install -g @anthropic-ai/claude-code@latest && \
|
||||
npm install -g chrome-devtools-mcp@latest && \
|
||||
# 安装 pptxgenjs 和 sharp(全局安装,不受 /home/user 挂载影响)
|
||||
npm install -g pptxgenjs sharp && \
|
||||
# Clean up npm cache
|
||||
npm cache clean --force && \
|
||||
echo "✅ pptxgenjs and sharp installed globally"
|
||||
|
||||
# # 安装 Playwright Chromium(独立 RUN 以利用 Docker 缓存)
|
||||
# RUN npx playwright install chromium && \
|
||||
# echo "✅ Playwright Chromium installed"
|
||||
|
||||
# 安装 OpenSSL 开发包和构建依赖(mcp-stdio-proxy 需要)
|
||||
RUN apt-get update && apt-get install -y \
|
||||
libssl-dev \
|
||||
pkg-config \
|
||||
build-essential && \
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# 安装 Rust toolchain(需要 root)
|
||||
# 注意:mcp-stdio-proxy 的安装已移至 Dockerfile,以便频繁更新时无需重建基础镜像
|
||||
ENV RUSTUP_HOME=/opt/rustup \
|
||||
CARGO_HOME=/opt/cargo \
|
||||
PATH="/opt/cargo/bin:$PATH"
|
||||
RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y && \
|
||||
chmod -R 755 /opt/rustup /opt/cargo && \
|
||||
# 确保 user 可以使用 cargo
|
||||
chmod -R o+rX /opt/cargo /opt/rustup
|
||||
|
||||
# 安装 uv 到全局路径(需要 root)
|
||||
# 注意:UV_CACHE_DIR 在后续会根据 root/user 模式分别设置,这里不设置全局默认
|
||||
# 使用阿里云 PyPI 镜像源(与 pip 保持一致,稳定可靠,无限流限制)
|
||||
# 使用 pip 安装(避免从 astral.sh/GitHub 下载二进制慢/不稳定),pip 全局安装默认放 /usr/local/bin
|
||||
ENV UV_INDEX_URL=https://mirrors.aliyun.com/pypi/simple
|
||||
RUN pip3 install --break-system-packages --no-cache-dir -i https://mirrors.aliyun.com/pypi/simple uv && \
|
||||
uv --version && \
|
||||
# 创建 uv 环境配置脚本(用户通过终端使用时会加载)
|
||||
echo '#!/bin/sh' > /etc/profile.d/uv-env.sh && \
|
||||
echo '# UV environment configuration' >> /etc/profile.d/uv-env.sh && \
|
||||
echo 'export UV_INDEX_URL=https://mirrors.aliyun.com/pypi/simple' >> /etc/profile.d/uv-env.sh && \
|
||||
echo 'export UV_TOOL_DIR=/home/user/.local/share/uv/tools' >> /etc/profile.d/uv-env.sh && \
|
||||
echo 'export UV_CACHE_DIR=/home/user/.cache/uv' >> /etc/profile.d/uv-env.sh && \
|
||||
chmod +x /etc/profile.d/uv-env.sh
|
||||
|
||||
# 添加 cargo bin 和 uv 到 PATH
|
||||
# 添加所有工具安装路径到 PATH:
|
||||
# - /root/.local/bin: root 用户使用 uv tool install 时的可执行文件位置
|
||||
# - /home/user/.local/bin: user 用户使用 uv tool install 时的可执行文件位置
|
||||
# - /opt/cargo/bin: Rust cargo 安装的工具
|
||||
# - /usr/local/bin: 系统级工具(bun, uv, pnpm 等)
|
||||
ENV PNPM_HOME="/home/user/.local/share/pnpm" \
|
||||
PATH="/root/.local/bin:/home/user/.local/bin:/opt/cargo/bin:/usr/local/bin:${PATH}"
|
||||
|
||||
# ============================================================================
|
||||
# 🔊 pcmflux 音频流服务安装
|
||||
# ============================================================================
|
||||
# 安装 pcmflux Python 包(需要 root,用于编译 C++ 扩展)
|
||||
# 使用 --break-system-packages 绕过 Debian 12 的 PEP 668 限制
|
||||
RUN pip3 install --break-system-packages websockets && \
|
||||
if [ "${USE_GITHUB_MIRROR}" = "true" ]; then \
|
||||
pip3 install --break-system-packages git+${GITHUB_MIRROR_URL}linuxserver/pcmflux.git; \
|
||||
else \
|
||||
pip3 install --break-system-packages git+https://github.com/linuxserver/pcmflux.git; \
|
||||
fi && \
|
||||
echo "✅ pcmflux audio streaming installed"
|
||||
|
||||
# 暴露音频流端口
|
||||
EXPOSE 6090
|
||||
EXPOSE 6089
|
||||
|
||||
# 暴露 IME 服务端口
|
||||
EXPOSE 6091
|
||||
|
||||
# ============================================================================
|
||||
# 预安装全局工具(root 模式)- 系统级别,不受 /home/user 挂载影响
|
||||
# ============================================================================
|
||||
# 预安装 context7 MCP 工具(Bun 全局安装)
|
||||
# 代码里通过 bunx 调用 MCP,预装到 bun 全局后 bunx 可直接命中 ~/.bun/install/global,无需运行时再下载
|
||||
RUN bun add -g @upstash/context7-mcp
|
||||
|
||||
# 预安装 mcp-server-fetch(UV 全局安装到系统目录)
|
||||
ENV UV_TOOL_DIR=/usr/local/share/uv/tools \
|
||||
UV_CACHE_DIR=/var/cache/uv
|
||||
RUN mkdir -p "$UV_TOOL_DIR" "$UV_CACHE_DIR" && \
|
||||
uv tool install mcp-server-fetch && \
|
||||
echo "✅ mcp-server-fetch installed to $UV_TOOL_DIR"
|
||||
|
||||
# ============================================================================
|
||||
# 🎯 切换到普通用户 - 后续操作以 user 身份执行
|
||||
# ============================================================================
|
||||
USER user
|
||||
WORKDIR /home/user
|
||||
|
||||
# 配置 pnpm(用户级配置)
|
||||
RUN pnpm config set registry https://registry.npmmirror.com && \
|
||||
pnpm config set store-dir /home/user/.cache/pnpm
|
||||
|
||||
# 配置 Bun 用户配置文件
|
||||
RUN mkdir -p /home/user/.cache/bun/install /home/user/.cache/bun/global && \
|
||||
echo '[install]' > /home/user/.bunfig.toml && \
|
||||
echo 'registry = "https://registry.npmmirror.com"' >> /home/user/.bunfig.toml && \
|
||||
echo '' >> /home/user/.bunfig.toml && \
|
||||
echo '[install.cache]' >> /home/user/.bunfig.toml && \
|
||||
echo 'dir = "/home/user/.cache/bun"' >> /home/user/.bunfig.toml && \
|
||||
cat /home/user/.bunfig.toml && \
|
||||
echo "✅ Bun configured for user"
|
||||
|
||||
# 配置 UV 用户环境(用户自己安装的工具会保存在这里,挂载后保留)
|
||||
ENV UV_TOOL_DIR=/home/user/.local/share/uv/tools \
|
||||
UV_CACHE_DIR=/home/user/.cache/uv
|
||||
RUN mkdir -p "$UV_TOOL_DIR" "$UV_CACHE_DIR" && \
|
||||
echo "✅ UV configured for user: tools in $UV_TOOL_DIR, cache in $UV_CACHE_DIR"
|
||||
|
||||
# 创建 Claude Code 配置目录
|
||||
RUN mkdir -p /home/user/.claude && \
|
||||
echo '{"permissions":{"deny":["WebFetch", "WebSearch"]}}' > /home/user/.claude/settings.json
|
||||
|
||||
# ============================================================================
|
||||
# 🎯 切回 root - 更新骨架目录
|
||||
# ============================================================================
|
||||
USER root
|
||||
|
||||
# 更新骨架目录(包含 user 阶段创建的配置)
|
||||
# 这样当宿主机挂载空目录到 /home/user 时,start-up.sh 可以从这里恢复
|
||||
RUN echo "📦 Updating skeleton directory with user configs..." && \
|
||||
cp -a /home/user/. /etc/skel-user-desktop/ && \
|
||||
echo "✅ Skeleton directory updated at /etc/skel-user-desktop"
|
||||
|
||||
# ============================================================================
|
||||
# 基础镜像构建完成
|
||||
# ============================================================================
|
||||
# 此处不设置 ENTRYPOINT,由最终镜像设置
|
||||
# 暴露 agent_runner 端口(供最终镜像使用)
|
||||
EXPOSE 8086
|
||||
|
||||
# 设置 agent_runner 默认端口环境变量
|
||||
ENV AGENT_RUNNER_PORT=8086
|
||||
|
||||
# 设置默认工作目录
|
||||
WORKDIR /home/user
|
||||
40
qiming-rcoder/docker/rcoder-agent-runner/Dockerfile.build
Normal file
40
qiming-rcoder/docker/rcoder-agent-runner/Dockerfile.build
Normal file
@@ -0,0 +1,40 @@
|
||||
# 独立的构建 Dockerfile - 仅用于构建 agent_runner 二进制
|
||||
# 使用 debian:12 作为基础镜像,确保 GLIBC 版本与运行环境一致
|
||||
|
||||
FROM debian:12
|
||||
|
||||
# 设置工作目录
|
||||
WORKDIR /build
|
||||
|
||||
# 安装系统依赖和 Rust 工具链(这些层会被缓存)
|
||||
RUN apt-get update && apt-get install -y \
|
||||
curl \
|
||||
build-essential \
|
||||
cmake \
|
||||
pkg-config \
|
||||
protobuf-compiler \
|
||||
libprotobuf-dev \
|
||||
libssl-dev \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# 安装 Rust 1.90(这一层会被缓存)
|
||||
# 安装后立即验证 cargo 可用,失败则中断构建,避免缓存不完整的层
|
||||
RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y \
|
||||
&& . "$HOME/.cargo/env" \
|
||||
&& cargo --version \
|
||||
&& rustc --version
|
||||
ENV PATH="/root/.cargo/bin:${PATH}"
|
||||
|
||||
# 🎯 代码哈希标记:只有当 CRATES_HASH 变化时,以下步骤才会重新执行
|
||||
# 这样可以缓存上面的系统依赖和 Rust 安装
|
||||
ARG CRATES_HASH=unknown
|
||||
RUN echo "Code hash: ${CRATES_HASH}"
|
||||
|
||||
# 复制 Cargo.toml 和 Cargo.lock
|
||||
COPY Cargo.toml Cargo.lock ./
|
||||
COPY crates ./crates
|
||||
|
||||
# 🔧 支持 CARGO_FLAGS 参数,允许外部传递 feature flags
|
||||
ARG CARGO_FLAGS=""
|
||||
RUN echo "🔧 Cargo flags: ${CARGO_FLAGS}" && \
|
||||
cargo build --release --bin agent_runner ${CARGO_FLAGS}
|
||||
477
qiming-rcoder/docker/rcoder-agent-runner/Dockerfile.old
Normal file
477
qiming-rcoder/docker/rcoder-agent-runner/Dockerfile.old
Normal file
@@ -0,0 +1,477 @@
|
||||
FROM debian:12
|
||||
|
||||
# Base image with desktop environment and applications
|
||||
# Environment variables:
|
||||
|
||||
ENV \
|
||||
# Avoid system prompts: \
|
||||
DEBIAN_FRONTEND=noninteractive \
|
||||
DEBIAN_PRIORITY=high \
|
||||
# Pip settings: \
|
||||
PIP_DEFAULT_TIMEOUT=100 \
|
||||
PIP_DISABLE_PIP_VERSION_CHECK=1 \
|
||||
PIP_NO_CACHE_DIR=1
|
||||
|
||||
# Desktop environment and applications (merged for smaller image size):
|
||||
|
||||
RUN apt-get update && \
|
||||
# Install all packages in one layer to reduce image size
|
||||
apt-get install -y \
|
||||
# X window server:
|
||||
xserver-xorg xorg x11-xserver-utils xvfb x11-utils xauth \
|
||||
# XFCE desktop environment:
|
||||
xfce4 xfce4-goodies \
|
||||
# 中文输入法(fcitx5 完整安装)
|
||||
fcitx5 fcitx5-pinyin fcitx5-frontend-gtk3 fcitx5-frontend-gtk2 fcitx5-frontend-qt5 fcitx5-module-xorg fcitx5-config-qt im-config dbus-x11 \
|
||||
# Basic system utilities:
|
||||
util-linux sudo curl git \
|
||||
# Python pip:
|
||||
python3-pip \
|
||||
# Desktop SDK tools:
|
||||
xdotool scrot ffmpeg \
|
||||
# VNC and streaming:
|
||||
x11vnc net-tools netcat-openbsd \
|
||||
# Standard applications:
|
||||
x11-apps xpdf gedit xpaint tint2 galculator pcmanfm \
|
||||
# Dependencies for later installations:
|
||||
apt-transport-https ca-certificates gnupg \
|
||||
socat supervisor \
|
||||
xclip \
|
||||
wget software-properties-common \
|
||||
# Suna dependencies:
|
||||
poppler-utils wkhtmltopdf antiword unrtf catdoc gawk file jq csvkit xmlstarlet zip unzip tmux vim tree rsync && \
|
||||
# Configure X11 wrapper to allow any user:
|
||||
sed -i 's/allowed_users=console/allowed_users=anybody/' /etc/X11/Xwrapper.config && \
|
||||
# Set the default terminal
|
||||
ln -sf /usr/bin/xfce4-terminal.wrapper /etc/alternatives/x-terminal-emulator && \
|
||||
# Install Python packages:
|
||||
# apt-get install -y python3-numpy && \
|
||||
# Clone NoVNC and websockify:
|
||||
git clone -b e2b-desktop https://github.com/e2b-dev/noVNC.git /opt/noVNC && \
|
||||
ln -s /opt/noVNC/vnc.html /opt/noVNC/index.html && \
|
||||
git clone -b v0.12.0 https://github.com/novnc/websockify /opt/noVNC/utils/websockify && \
|
||||
# Clean up package cache and temporary files to reduce image size:
|
||||
apt-get autoremove -y && \
|
||||
apt-get autoclean && \
|
||||
rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* /root/.cache
|
||||
|
||||
# Install LibreOffice from official website
|
||||
ARG TARGETARCH
|
||||
RUN case "$TARGETARCH" in \
|
||||
"amd64") LO_ARCH="x86_64" ;; \
|
||||
"arm64") LO_ARCH="aarch64" ;; \
|
||||
*) echo "Unsupported architecture: $TARGETARCH" && exit 1 ;; \
|
||||
esac && \
|
||||
case "$TARGETARCH" in \
|
||||
"amd64") LO_ARCH2="x86-64" ;; \
|
||||
"arm64") LO_ARCH2="aarch64" ;; \
|
||||
*) echo "Unsupported architecture: $TARGETARCH" && exit 1 ;; \
|
||||
esac && \
|
||||
wget -O /tmp/libreoffice.tar.gz "https://mirrors.ustc.edu.cn/tdf/libreoffice/stable/25.8.1/deb/${LO_ARCH}/LibreOffice_25.8.1_Linux_${LO_ARCH2}_deb.tar.gz" && \
|
||||
cd /tmp && \
|
||||
tar -xzf libreoffice.tar.gz && \
|
||||
cd LibreOffice_*/DEBS && \
|
||||
dpkg -i *.deb || true && \
|
||||
apt-get update && \
|
||||
apt-get install -f -y && \
|
||||
# Clean up
|
||||
rm -rf /tmp/LibreOffice_* /tmp/libreoffice.tar.gz && \
|
||||
apt-get autoremove -y && \
|
||||
apt-get autoclean && \
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
|
||||
|
||||
# User applications:
|
||||
|
||||
# ~ Make your changes to this template BELOW this line ~
|
||||
|
||||
# Install Chromium from Debian official repository (optimized single layer)
|
||||
RUN apt-get update && \
|
||||
apt-get install -y chromium chromium-driver && \
|
||||
# Clean up package cache to reduce image size:
|
||||
apt-get autoremove -y && \
|
||||
apt-get autoclean && \
|
||||
rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*
|
||||
|
||||
# Configure Chromium to skip first-run experience and enable CDP
|
||||
RUN mkdir -p /etc/chromium/policies/managed/ && \
|
||||
echo '{"AllowInsecureLocalConnections": true, "RemoteDebuggingAllowed": true, "RemoteDebuggingPort": "9222", "RemoteDebuggingAddress":"0.0.0.0", "AutoplayAllowed": true, "MetricsReportingEnabled": false, "BrowserSignin": 0, "SyncDisabled": true, "PromotionalTabsEnabled": false, "DefaultBrowserSettingEnabled": false, "DefaultSearchProviderEnabled": false, "HideWebStoreIcon": true, "PasswordManagerEnabled": false, "PromptForDownloadLocation": false, "SafeBrowsingEnabled": false, "SafeBrowsingExtendedReportingEnabled": false, "UrlKeyedAnonymizedDataCollectionEnabled": false, "CloudPrintSubmitEnabled": false, "CloudManagementEnrollmentToken": "", "SpellcheckEnabled": false}' > /etc/chromium/policies/managed/policies.json
|
||||
|
||||
# Create user and configure Chrome (merged for smaller image size)
|
||||
RUN apt-get update && \
|
||||
apt-get install -y gnome-keyring policykit-1-gnome policykit-1 && \
|
||||
# Create default user
|
||||
useradd -ms /bin/bash user && \
|
||||
usermod -aG sudo user && \
|
||||
passwd -d user && \
|
||||
echo "user ALL=(ALL:ALL) NOPASSWD: ALL" >>/etc/sudoers && \
|
||||
# Create Chromium configuration directories
|
||||
mkdir -p /home/user/.config/chromium/Default/ && \
|
||||
echo '{ "browser": { "custom_chrome_frame": true, "has_seen_welcome_page": true }, "profile": { "default_content_setting_values": { "geolocation": 1 }, "exit_type": "Normal", "exited_cleanly": true, "password_manager_enabled": false }, "credentials_enable_service": false, "credentials_enable_autosignin": false }' > /home/user/.config/chromium/Default/Preferences && \
|
||||
# Configure GNOME keyring with empty password (禁用密码提示)
|
||||
mkdir -p /home/user/.local/share/keyrings && \
|
||||
# 创建 Default keyring(无密码,禁用锁定)
|
||||
printf '[keyring]\ndisplay-name=Default keyring\nctime=1732780800\nmtime=1732780800\nlock-on-idle=false\nlock-after=false\n' > /home/user/.local/share/keyrings/Default_keyring.keyring && \
|
||||
# 设置为默认 keyring
|
||||
echo 'Default_keyring' > /home/user/.local/share/keyrings/default && \
|
||||
chmod 600 /home/user/.local/share/keyrings/Default_keyring.keyring && \
|
||||
chmod 600 /home/user/.local/share/keyrings/default && \
|
||||
# Configure PolicyKit to auto-allow Chromium operations
|
||||
mkdir -p /etc/polkit-1/localauthority/50-local.d && \
|
||||
# Create comprehensive policy for user
|
||||
printf '[Allow all for user]\nIdentity=unix-user:user\nAction=*\nResultAny=yes\nResultInactive=yes\nResultActive=yes\n\n[Allow all for user - specific patterns]\nIdentity=unix-user:user\nAction=org.freedesktop.policykit.*\nResultAny=yes\nResultInactive=yes\nResultActive=yes\n\n[Allow Chromium specific]\nIdentity=unix-user:user\nAction=org.chromium.*\nResultAny=yes\nResultInactive=yes\nResultActive=yes' > /etc/polkit-1/localauthority/50-local.d/comprehensive-allow.pkla && \
|
||||
mkdir -p /home/user/.config/autostart && \
|
||||
# Create PolicyKit authentication agent autostart
|
||||
printf "[Desktop Entry]\nType=Application\nName=PolicyKit Auth\nExec=/usr/lib/policykit-1-gnome/polkit-gnome-authentication-agent-1\nHidden=false\nNoDisplay=false\nX-GNOME-Autostart-enabled=true\nStartupNotify=false" > /home/user/.config/autostart/polkit-gnome-auth.desktop && \
|
||||
# Create D-Bus session autostart
|
||||
printf "[Desktop Entry]\nType=Application\nName=D-Bus Session\nExec=/usr/bin/dbus-launch --sh-syntax\nHidden=false\nNoDisplay=false\nX-GNOME-Autostart-enabled=true" > /home/user/.config/autostart/dbus-session.desktop && \
|
||||
# Create PolicyKit daemon autostart
|
||||
printf "[Desktop Entry]\nType=Application\nName=PolicyKit Daemon\nExec=/usr/lib/policykit-1/polkitd --no-debug\nHidden=false\nNoDisplay=false\nX-GNOME-Autostart-enabled=true" > /home/user/.config/autostart/polkitd.desktop && \
|
||||
# Create Chromium data directory and set permissions
|
||||
mkdir -p /home/user/chromium-data && \
|
||||
# 创建用户运行时目录
|
||||
mkdir -p /run/user/$(id -u user) && \
|
||||
# ========== 配置中文输入法 fcitx5 ==========
|
||||
mkdir -p /home/user/.config/fcitx5 && \
|
||||
mkdir -p /home/user/.config/autostart && \
|
||||
mkdir -p /home/user/.config/environment.d && \
|
||||
# 1. 在 .bashrc 中设置环境变量(终端使用 - 纯 fcitx5)
|
||||
echo 'export GTK_IM_MODULE=fcitx5' >> /home/user/.bashrc && \
|
||||
echo 'export QT_IM_MODULE=fcitx5' >> /home/user/.bashrc && \
|
||||
echo 'export XMODIFIERS=@im=fcitx5' >> /home/user/.bashrc && \
|
||||
echo 'export INPUT_METHOD=fcitx5' >> /home/user/.bashrc && \
|
||||
echo 'export SDL_IM_MODULE=fcitx5' >> /home/user/.bashrc && \
|
||||
echo 'export GLFW_IM_MODULE=fcitx5' >> /home/user/.bashrc && \
|
||||
# 2. 在 .xprofile 中设置环境变量(X 会话启动时加载,GUI 应用可用)
|
||||
echo 'export GTK_IM_MODULE=fcitx5' > /home/user/.xprofile && \
|
||||
echo 'export QT_IM_MODULE=fcitx5' >> /home/user/.xprofile && \
|
||||
echo 'export XMODIFIERS=@im=fcitx5' >> /home/user/.xprofile && \
|
||||
echo 'export INPUT_METHOD=fcitx5' >> /home/user/.xprofile && \
|
||||
echo 'export SDL_IM_MODULE=fcitx5' >> /home/user/.xprofile && \
|
||||
echo 'export GLFW_IM_MODULE=fcitx5' >> /home/user/.xprofile && \
|
||||
# 3. 在 systemd 用户环境中设置(现代 Linux 推荐方式)
|
||||
echo 'GTK_IM_MODULE=fcitx5' > /home/user/.config/environment.d/fcitx5.conf && \
|
||||
echo 'QT_IM_MODULE=fcitx5' >> /home/user/.config/environment.d/fcitx5.conf && \
|
||||
echo 'XMODIFIERS=@im=fcitx5' >> /home/user/.config/environment.d/fcitx5.conf && \
|
||||
echo 'INPUT_METHOD=fcitx5' >> /home/user/.config/environment.d/fcitx5.conf && \
|
||||
echo 'SDL_IM_MODULE=fcitx5' >> /home/user/.config/environment.d/fcitx5.conf && \
|
||||
echo 'GLFW_IM_MODULE=fcitx5' >> /home/user/.config/environment.d/fcitx5.conf && \
|
||||
# 4. 创建自启动配置 - 让 fcitx5 在 XFCE 启动后自动启动
|
||||
printf '[Desktop Entry]\nType=Application\nName=Fcitx 5\nComment=Input Method Framework\nExec=fcitx5 -d --replace\nIcon=fcitx5\nStartupNotify=false\nTerminal=false\nCategories=System;Utility;\nX-GNOME-Autostart-enabled=true\nX-XFCE-Autostart-enabled=true' > /home/user/.config/autostart/fcitx5.desktop && \
|
||||
# 6. 配置 fcitx5 全局选项(启用云拼音、设置切换快捷键)
|
||||
mkdir -p /home/user/.config/fcitx5/conf && \
|
||||
printf '[Hotkey]\n# Enumerate when press trigger key repeatedly\nEnumerateWithTriggerKeys=True\n# Temporally switch between first and current Input Method\nAltTriggerKeys=\n# Enumerate Input Method Forward\nEnumerateForwardKeys=\n# Enumerate Input Method Backward\nEnumerateBackwardKeys=\n# Skip first input method while enumerating\nEnumerateSkipFirst=False\n\n[Hotkey/TriggerKeys]\n0=Control+space\n1=Shift+Control+space\n\n[Hotkey/EnumerateGroupForwardKeys]\n0=Super+space\n\n[Hotkey/EnumerateGroupBackwardKeys]\n0=Shift+Super+space\n\n[Hotkey/ActivateKeys]\n0=Hangul_Hanja\n\n[Hotkey/DeactivateKeys]\n0=Hangul_Romaja\n\n[Hotkey/PrevPage]\n0=Up\n\n[Hotkey/NextPage]\n0=Down\n\n[Hotkey/PrevCandidate]\n0=Shift+Tab\n\n[Hotkey/NextCandidate]\n0=Tab\n\n[Hotkey/TogglePreedit]\n0=Control+Alt+P\n\n[Behavior]\n# Active By Default\nActiveByDefault=True\n# Share Input State\nShareInputState=No\n# Preload input method to be used by default\nPreloadInputMethod=True\n# Show Input Method Information when switch input method\nShowInputMethodInformation=True\n# Show Input Method Information when changing focus\nShowInputMethodInformationWhenFocusIn=False\n# Show compact input method information\nCompactInputMethodInformation=True\n# Show first input method information\nShowFirstInputMethodInformation=True\n# Default page size\nDefaultPageSize=5\n# Override Xkb Option\nOverrideXkbOption=False\n# Custom Xkb Option\nCustomXkbOption=\n# Force Enabled Addons\nEnabledAddons=\n# Force Disabled Addons\nDisabledAddons=\n# Preload input method to be used by default\nPreeditEnabledByDefault=True' > /home/user/.config/fcitx5/config && \
|
||||
# 7. 配置拼音输入法选项(禁用云拼音提示)
|
||||
mkdir -p /home/user/.config/fcitx5/conf && \
|
||||
printf '[Addon]\n# Enabled\nEnabled=True\n\n[Behavior]\n# Show preedit within application\nPreeditInApplication=False\n\n[PinyinEngine]\n# Shuangpin Profile\nShuangpinProfile=Ziranma\n# Show current shuangpin mode\nShowActualPinyinInHint=False\n# Enable Prediction\nPredictionEnabled=True\n# Prediction Size\nPredictionSize=10\n# Enable Cloud Pinyin (默认禁用,避免首次提示)\nCloudPinyinEnabled=False\n# Cloud Pinyin Index\nCloudPinyinIndex=2' > /home/user/.config/fcitx5/conf/pinyin.conf && \
|
||||
# 7.1 配置云拼音选项(禁用首次提示)
|
||||
printf '[Addon]\n# Enabled\nEnabled=False\n\n[Behavior]\n# Show notification when toggled\nShowNotification=False' > /home/user/.config/fcitx5/conf/cloudpinyin.conf && \
|
||||
# 7.2 禁用 fcitx5 所有通知
|
||||
printf '[Addon]\nEnabled=False\n\n[Behavior]\nHiddenNotifications=' > /home/user/.config/fcitx5/conf/notifications.conf && \
|
||||
# 8. 创建输入法启用列表(关键:确保拼音输入法被启用)
|
||||
mkdir -p /home/user/.local/share/fcitx5/inputmethod && \
|
||||
printf 'keyboard-us:True\npinyin:True' > /home/user/.local/share/fcitx5/inputmethod/default.conf && \
|
||||
# 9. 启用拼音输入法 addon
|
||||
mkdir -p /home/user/.config/fcitx5/conf && \
|
||||
printf '[Addons]\n0=fcitx5-pinyin\n1=fcitx5-chinese-addons' > /home/user/.config/fcitx5/addon.conf && \
|
||||
# 10. 创建简化的 profile(使用更兼容的格式)
|
||||
printf '[Groups/0]\nName=Default\nDefault Layout=us\nDefaultIM=pinyin\n\n[Groups/0/Items/0]\nName=keyboard-us\nLayout=\n\n[Groups/0/Items/1]\nName=pinyin\nLayout=\n\n[GroupOrder]\n0=Default\n\n[Behavior]\nActiveByDefault=True\nShareInputState=No' > /home/user/.config/fcitx5/profile && \
|
||||
# 11. 配置 im-config(系统级输入法框架选择)
|
||||
mkdir -p /home/user/.config && \
|
||||
echo 'fcitx5' > /home/user/.config/im-config && \
|
||||
# 12. 创建 GTK 3.0 im-module 配置(使用 fcitx5)
|
||||
mkdir -p /home/user/.config/gtk-3.0 && \
|
||||
printf '[Settings]\ngtk-im-module=fcitx5' > /home/user/.config/gtk-3.0/settings.ini && \
|
||||
chown -R user:user /home/user /run/user/$(id -u user) && \
|
||||
chmod -R 777 /home/user/chromium-data && \
|
||||
chmod 777 /home/user && \
|
||||
chmod 700 /run/user/$(id -u user) && \
|
||||
# Create Chromium wrapper script with fcitx5 support
|
||||
printf '#!/bin/bash\n# 加载 D-Bus 地址\nif [ -f /tmp/dbus-session-env ]; then\n source /tmp/dbus-session-env\nfi\n\n# 设置输入法环境变量\nexport DISPLAY=:0\nexport GTK_IM_MODULE=fcitx5\nexport QT_IM_MODULE=fcitx5\nexport XMODIFIERS=@im=fcitx5\nexport INPUT_METHOD=fcitx5\n\n# 启动 Chromium(禁用密码管理)\nexec /usr/bin/chromium \\\n --user-data-dir=/home/user/chromium-data \\\n --no-sandbox \\\n --disable-dev-shm-usage \\\n --remote-debugging-port=9222 \\\n --remote-debugging-address=0.0.0.0 \\\n --no-first-run \\\n --no-default-browser-check \\\n --password-store=basic \\\n --use-mock-keychain "$@"\n' > /usr/bin/chromium-browser-launcher && \
|
||||
chmod 755 /usr/bin/chromium-browser-launcher && \
|
||||
# Add Chromium aliases
|
||||
echo 'alias chromium="/usr/local/bin/chromium"' >> /home/user/.bashrc && \
|
||||
echo 'alias chrome="/usr/local/bin/chromium"' >> /home/user/.bashrc && \
|
||||
# Add VS Code alias
|
||||
echo 'alias code="code --no-sandbox"' >> /home/user/.bashrc && \
|
||||
chown -R 1000:1000 /home/user/.config && \
|
||||
chown -R 1000:1000 /home/user/.local && \
|
||||
# Clean up package cache to reduce image size:
|
||||
apt-get autoremove -y && \
|
||||
apt-get autoclean && \
|
||||
rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*
|
||||
|
||||
# Create Chromium launcher with fcitx5 support
|
||||
RUN printf '#!/bin/bash\n# 加载 D-Bus 地址\nif [ -f /tmp/dbus-session-env ]; then\n source /tmp/dbus-session-env\nfi\n\n# 设置输入法环境变量\nexport DISPLAY=:0\nexport GTK_IM_MODULE=fcitx5\nexport QT_IM_MODULE=fcitx5\nexport XMODIFIERS=@im=fcitx5\nexport INPUT_METHOD=fcitx5\n\n# 以 user 用户身份运行 Chromium(禁用密码管理)\nsu - user -c "export DISPLAY=:0; export DBUS_SESSION_BUS_ADDRESS=\\\"$DBUS_SESSION_BUS_ADDRESS\\\"; export GTK_IM_MODULE=fcitx5; export QT_IM_MODULE=fcitx5; export XMODIFIERS=@im=fcitx5; export INPUT_METHOD=fcitx5; /usr/bin/chromium --user-data-dir=/home/user/chromium-data --no-sandbox --disable-dev-shm-usage --remote-debugging-port=9222 --remote-debugging-address=0.0.0.0 --no-first-run --no-default-browser-check --password-store=basic --use-mock-keychain"\n' > /usr/local/bin/chromium && \
|
||||
chmod +x /usr/local/bin/chromium
|
||||
|
||||
# Create a socat service with supervisor to ensure port forwarding is always running
|
||||
# RUN mkdir -p /etc/supervisor/conf.d && \
|
||||
# echo '[supervisord]\nnodaemon=true\n\n[program:socat]\ncommand=socat TCP4-LISTEN:9223,fork,reuseaddr TCP4:127.0.0.1:9222\nautostart=true\nautorestart=true\nstdout_logfile=/var/log/socat.log\nstdout_logfile_maxbytes=1MB\nstdout_logfile_backups=10\nstderr_logfile=/var/log/socat.err\nstderr_logfile_maxbytes=1MB\nstderr_logfile_backups=10' > /etc/supervisor/conf.d/socat.conf
|
||||
|
||||
# Start the socat service at system boot
|
||||
# RUN echo '#!/bin/bash\n# Start supervisor to manage socat service\n/usr/bin/supervisord -c /etc/supervisor/supervisord.conf &' > /usr/local/bin/start-port-forward.sh && \
|
||||
# chmod +x /usr/local/bin/start-port-forward.sh && \
|
||||
# echo "[Desktop Entry]\nType=Application\nName=PortForward\nExec=/usr/local/bin/start-port-forward.sh\nHidden=false\nNoDisplay=false\nX-GNOME-Autostart-enabled=true" > /home/user/.config/autostart/port-forward.desktop
|
||||
|
||||
# 添加自动启动supervisord的桌面启动项
|
||||
RUN echo '#!/bin/bash\n# 启动supervisord服务\nsudo /usr/bin/supervisord -c /etc/supervisor/supervisord.conf &' > /usr/local/bin/start-supervisord.sh && \
|
||||
chmod +x /usr/local/bin/start-supervisord.sh && \
|
||||
echo "[Desktop Entry]\nType=Application\nName=Supervisord\nExec=/usr/local/bin/start-supervisord.sh\nHidden=false\nNoDisplay=false\nX-GNOME-Autostart-enabled=true" > /home/user/.config/autostart/supervisord.desktop && \
|
||||
chmod +x /home/user/.config/autostart/supervisord.desktop && \
|
||||
chown -R user:user /home/user/.config/autostart
|
||||
|
||||
# Install VS Code (optimized single layer)
|
||||
ARG TARGETARCH
|
||||
RUN case "$TARGETARCH" in \
|
||||
"amd64") VS_ARCH="x64" ;; \
|
||||
"arm64") VS_ARCH="arm64" ;; \
|
||||
*) echo "Unsupported architecture: $TARGETARCH" && exit 1 ;; \
|
||||
esac && \
|
||||
wget -O /tmp/vscode.deb "https://code.visualstudio.com/sha/download?build=stable&os=linux-deb-${VS_ARCH}" && \
|
||||
dpkg -i /tmp/vscode.deb || true && \
|
||||
apt-get update && \
|
||||
apt-get install -f -y && \
|
||||
# Clean up package cache to reduce image size:
|
||||
apt-get autoremove -y && \
|
||||
apt-get autoclean && \
|
||||
rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*
|
||||
RUN mkdir -p /home/user/.config/Code/User
|
||||
COPY ./settings.json /home/user/.config/Code/User/settings.json
|
||||
|
||||
# Copy desktop background for XFCE
|
||||
COPY ./wallpaper.png /usr/share/backgrounds/xfce/wallpaper.png
|
||||
RUN mkdir -p /home/user/.config/xfce4/xfconf/xfce-perchannel-xml/
|
||||
COPY ./xfce4-desktop.xml /home/user/.config/xfce4/xfconf/xfce-perchannel-xml/xfce4-desktop.xml
|
||||
|
||||
# Disable screen saver and power management, configure XFCE panel
|
||||
RUN mkdir -p /home/user/.config/xfce4/xfconf/xfce-perchannel-xml/ && \
|
||||
echo '<?xml version="1.0" encoding="UTF-8"?>\n<channel name="xfce4-screensaver" version="1.0">\n <property name="saver" type="empty">\n <property name="enabled" type="bool" value="false"/>\n <property name="mode" type="int" value="0"/>\n </property>\n <property name="lock" type="empty">\n <property name="enabled" type="bool" value="false"/>\n </property>\n</channel>' > /home/user/.config/xfce4/xfconf/xfce-perchannel-xml/xfce4-screensaver.xml && \
|
||||
echo '<?xml version="1.0" encoding="UTF-8"?>\n<channel name="xfce4-power-manager" version="1.0">\n <property name="xfce4-power-manager" type="empty">\n <property name="blank-on-ac" type="int" value="0"/>\n <property name="blank-on-battery" type="int" value="0"/>\n <property name="dpms-enabled" type="bool" value="false"/>\n <property name="dpms-on-ac-sleep" type="uint" value="0"/>\n <property name="dpms-on-ac-off" type="uint" value="0"/>\n <property name="dpms-on-battery-sleep" type="uint" value="0"/>\n <property name="dpms-on-battery-off" type="uint" value="0"/>\n </property>\n</channel>' > /home/user/.config/xfce4/xfconf/xfce-perchannel-xml/xfce4-power-manager.xml && \
|
||||
# Configure XFCE panel with Chromium launcher in taskbar
|
||||
echo '<?xml version="1.0" encoding="UTF-8"?>\n<channel name="xfce4-panel" version="1.0">\n <property name="configver" type="int" value="2"/>\n <property name="panels" type="array">\n <value type="int" value="1"/>\n <property name="panel-1" type="empty">\n <property name="position" type="string" value="p=6;x=0;y=0"/>\n <property name="length" type="uint" value="100"/>\n <property name="position-locked" type="bool" value="true"/>\n <property name="size" type="uint" value="30"/>\n <property name="plugin-ids" type="array">\n <value type="int" value="1"/>\n <value type="int" value="2"/>\n <value type="int" value="3"/>\n <value type="int" value="4"/>\n <value type="int" value="5"/>\n <value type="int" value="6"/>\n <value type="int" value="7"/>\n </property>\n </property>\n </property>\n <property name="plugins" type="empty">\n <property name="plugin-1" type="string" value="applicationsmenu"/>\n <property name="plugin-2" type="string" value="separator"/>\n <property name="plugin-3" type="string" value="tasklist"/>\n <property name="plugin-4" type="string" value="launcher">\n <property name="items" type="array">\n <value type="string" value="chromium.desktop"/>\n </property>\n </property>\n <property name="plugin-5" type="string" value="separator"/>\n <property name="plugin-6" type="string" value="systray"/>\n <property name="plugin-7" type="string" value="clock"/>\n </property>\n</channel>' > /home/user/.config/xfce4/xfconf/xfce-perchannel-xml/xfce4-panel.xml && \
|
||||
# Create system-wide Chromium desktop entry (支持 fcitx5 输入法)
|
||||
mkdir -p /usr/share/applications && \
|
||||
printf "[Desktop Entry]\nVersion=1.0\nType=Application\nName=Chromium\nComment=Web Browser\nExec=/usr/bin/chromium-browser-launcher\nIcon=chromium\nTerminal=false\nCategories=Network;WebBrowser;" > /usr/share/applications/chromium-browser.desktop && \
|
||||
ln -sf /usr/share/applications/chromium-browser.desktop /usr/share/applications/chromium.desktop && \
|
||||
chmod 644 /usr/share/applications/chromium-browser.desktop /usr/share/applications/chromium.desktop && \
|
||||
# Override user-level as well
|
||||
mkdir -p /home/user/.local/share/applications && \
|
||||
printf "[Desktop Entry]\nVersion=1.0\nType=Application\nName=Chromium\nComment=Web Browser\nExec=/usr/bin/chromium-browser-launcher\nIcon=chromium\nTerminal=false\nCategories=Network;WebBrowser;" > /home/user/.local/share/applications/chromium.desktop && \
|
||||
chmod 644 /home/user/.local/share/applications/chromium.desktop && \
|
||||
# Set Chromium as default web browser
|
||||
echo "[Default Applications]\ntext/html=chromium.desktop\ntext/xml=chromium.desktop\napplication/xhtml+xml=chromium.desktop\napplication/xml=chromium.desktop\napplication/rss+xml=chromium.desktop\napplication/rdf+xml=chromium.desktop\nimage/gif=chromium.desktop\nimage/jpeg=chromium.desktop\nimage/png=chromium.desktop\nx-scheme-handler/http=chromium.desktop\nx-scheme-handler/https=chromium.desktop\nx-scheme-handler/ftp=chromium.desktop\nx-scheme-handler/chrome=chromium.desktop\nvideo/webm=chromium.desktop\napplication/x-xpinstall=chromium.desktop" > /home/user/.config/mimeapps.list && \
|
||||
chown -R user:user /home/user/.config /home/user/.local
|
||||
|
||||
# Create desktop icons for terminal, VS Code, and Chrome
|
||||
RUN mkdir -p /home/user/Desktop /usr/share/applications && \
|
||||
# Create safe desktop files in system directory (not in user's home)
|
||||
printf "[Desktop Entry]\nVersion=1.0\nType=Application\nName=Terminal\nComment=Terminal Emulator\nExec=xfce4-terminal\nIcon=utilities-terminal\nTerminal=false\nCategories=System;TerminalEmulator;" > /usr/share/applications/user-terminal.desktop && \
|
||||
printf "[Desktop Entry]\nVersion=1.0\nType=Application\nName=Visual Studio Code\nComment=Code Editor\nExec=/usr/bin/code --no-sandbox\nIcon=vscode\nTerminal=false\nCategories=Development;IDE;" > /usr/share/applications/user-vscode.desktop && \
|
||||
chmod 644 /usr/share/applications/user-terminal.desktop /usr/share/applications/user-vscode.desktop && \
|
||||
# Create symbolic links from Desktop to system apps (安全软链接)
|
||||
ln -sf /usr/share/applications/user-terminal.desktop /home/user/Desktop/terminal.desktop && \
|
||||
ln -sf /usr/share/applications/user-vscode.desktop /home/user/Desktop/vscode.desktop && \
|
||||
ln -sf /usr/share/applications/chromium-browser.desktop /home/user/Desktop/chromium.desktop && \
|
||||
chown -R user:user /home/user/Desktop
|
||||
|
||||
# Copy firefox policies
|
||||
COPY firefox-policies.json /usr/lib/firefox-esr/distribution/policies.json
|
||||
COPY firefox-autoconfig.js /usr/lib/firefox-esr/defaults/pref/autoconfig.js
|
||||
COPY firefox.cfg /usr/lib/firefox-esr/firefox.cfg
|
||||
|
||||
|
||||
# Code interpretor environment setup
|
||||
# 系统依赖和Python 3.12安装 (merged for smaller image size)
|
||||
RUN DEBIAN_FRONTEND=noninteractive apt-get update && \
|
||||
apt-get install -y --no-install-recommends \
|
||||
build-essential curl git util-linux jq sudo fonts-noto-cjk \
|
||||
systemd ca-certificates chrony r-base software-properties-common \
|
||||
libzmq3-dev libzmq5 pkg-config && \
|
||||
# 使用 Debian 12 默认的 Python 3.11 (更稳定)
|
||||
apt-get install -y python3 python3-dev python3-venv python3-pip && \
|
||||
ln -sf /usr/bin/python3 /usr/bin/python && \
|
||||
# curl -sS https://bootstrap.pypa.io/get-pip.py | python && \
|
||||
# Clean up package cache to reduce image size:
|
||||
apt-get autoremove -y && \
|
||||
apt-get autoclean && \
|
||||
rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*
|
||||
|
||||
# 安装 Node.js 22.x (现在支持,因为不再依赖 ijavascript)
|
||||
RUN apt-get update && \
|
||||
apt-get install -y curl gnupg ca-certificates && \
|
||||
# 添加 NodeSource PPA for Node.js 22.x
|
||||
curl -fsSL https://deb.nodesource.com/setup_22.x | bash - && \
|
||||
# 安装 Node.js (npm 会一同被安装)
|
||||
apt-get install -y nodejs && \
|
||||
# 更新证书
|
||||
update-ca-certificates && \
|
||||
# 配置 pnpm
|
||||
corepack enable && \
|
||||
corepack prepare pnpm@latest --activate && \
|
||||
echo "Node.js version: $(node -v)" && \
|
||||
echo "npm version: $(npm -v)" && \
|
||||
echo "pnpm version: $(pnpm -v)" && \
|
||||
# Clean up package cache to reduce image size:
|
||||
apt-get autoremove -y && \
|
||||
apt-get autoclean && \
|
||||
rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*
|
||||
|
||||
# 环境变量设置
|
||||
ENV PIP_DEFAULT_TIMEOUT=100 \
|
||||
PIP_DISABLE_PIP_VERSION_CHECK=1 \
|
||||
PIP_NO_CACHE_DIR=1 \
|
||||
JUPYTER_CONFIG_PATH="/root/.jupyter" \
|
||||
IPYTHON_CONFIG_PATH="/root/.ipython" \
|
||||
SERVER_PATH="/root/.server" \
|
||||
R_VERSION=4.4.2
|
||||
|
||||
ENV R_HOME=/opt/R/${R_VERSION} \
|
||||
JAVA_HOME=/opt/java/openjdk
|
||||
|
||||
# # 创建默认用户
|
||||
# RUN useradd -ms /bin/bash user && \
|
||||
# usermod -aG sudo user && \
|
||||
# passwd -d user && \
|
||||
# echo "user ALL=(ALL:ALL) NOPASSWD: ALL" >>/etc/sudoers
|
||||
|
||||
# 设置目录权限
|
||||
RUN mkdir -p /home/user && \
|
||||
chmod 777 -R /home/user && \
|
||||
chmod 777 -R /usr/local
|
||||
|
||||
# 安装 Jupyter (with cleanup)
|
||||
COPY ./requirements.txt requirements.txt
|
||||
RUN pip install --break-system-packages --upgrade pip && \
|
||||
pip install --break-system-packages --no-cache-dir -r requirements.txt && \
|
||||
ipython kernel install --name "python3" --user && \
|
||||
# Clean up pip cache
|
||||
rm -rf /root/.cache
|
||||
|
||||
# JavaScript/TypeScript 使用 Deno kernel (不需要安装 ijavascript)
|
||||
# Deno kernel 在上面已经安装并配置完成
|
||||
|
||||
# Deno Kernel (upgraded to latest 2.5.6)
|
||||
COPY --from=denoland/deno:bin-2.5.6 /deno /usr/bin/deno
|
||||
RUN chmod +x /usr/bin/deno && \
|
||||
deno jupyter --unstable --install && \
|
||||
# Clean up deno cache
|
||||
rm -rf /root/.cache/deno
|
||||
COPY ./deno.json /root/.local/share/jupyter/kernels/deno/kernel.json
|
||||
|
||||
# Bash Kernel (with cleanup)
|
||||
RUN pip install --break-system-packages bash_kernel && \
|
||||
python -m bash_kernel.install && \
|
||||
# Clean up pip cache
|
||||
rm -rf /root/.cache
|
||||
|
||||
# R Kernel
|
||||
# RUN R -e "install.packages('IRkernel', repos='https://cloud.r-project.org'); IRkernel::installspec(user = FALSE, name = 'r', displayname = 'R')"
|
||||
|
||||
|
||||
# 配置 envd 服务
|
||||
ARG TARGETARCH
|
||||
RUN mkdir -p /etc/systemd/system/multi-user.target.wants
|
||||
# 复制所有 envd 二进制文件
|
||||
COPY ./bin/ /tmp/envd-bin/
|
||||
# 根据目标架构选择对应的 envd 二进制文件
|
||||
RUN if [ "$TARGETARCH" = "arm64" ]; then \
|
||||
cp /tmp/envd-bin/envd-arm64 /usr/bin/envd; \
|
||||
else \
|
||||
cp /tmp/envd-bin/envd /usr/bin/envd; \
|
||||
fi && \
|
||||
chmod +x /usr/bin/envd && \
|
||||
rm -rf /tmp/envd-bin
|
||||
RUN echo '[Unit]\n\
|
||||
Description=Env Daemon Service\n\
|
||||
\n\
|
||||
[Service]\n\
|
||||
Type=simple\n\
|
||||
Restart=always\n\
|
||||
User=root\n\
|
||||
Group=root\n\
|
||||
ExecStart=/bin/bash -l -c /usr/bin/envd\n\
|
||||
\n\
|
||||
[Install]\n\
|
||||
WantedBy=multi-user.target' > /etc/systemd/system/envd.service
|
||||
|
||||
RUN ln -s /etc/systemd/system/envd.service /etc/systemd/system/multi-user.target.wants/envd.service
|
||||
|
||||
# 配置 chrony
|
||||
RUN mkdir -p /etc/chrony
|
||||
RUN echo 'makestep 1 -1' > /etc/chrony/chrony.conf
|
||||
|
||||
RUN mkdir -p /etc/systemd/system/chrony.service.d
|
||||
RUN echo '[Service]\n\
|
||||
ExecStart=\n\
|
||||
ExecStart=/usr/sbin/chronyd\n\
|
||||
User=root\n\
|
||||
Group=root' > /etc/systemd/system/chrony.service.d/override.conf
|
||||
|
||||
RUN systemctl enable chrony
|
||||
|
||||
# Java 配置 (with cleanup)
|
||||
COPY --from=eclipse-temurin:11-jdk $JAVA_HOME $JAVA_HOME
|
||||
RUN rm -f /usr/bin/java && \
|
||||
ln -s ${JAVA_HOME}/bin/java /usr/bin/java && \
|
||||
wget https://github.com/SpencerPark/IJava/releases/download/v1.3.0/ijava-1.3.0.zip && \
|
||||
unzip ijava-1.3.0.zip && \
|
||||
python install.py --sys-prefix && \
|
||||
# Clean up installation files
|
||||
rm -rf ijava-1.3.0.zip install.py java/
|
||||
|
||||
# 配置自动登录
|
||||
RUN mkdir -p /etc/systemd/system/serial-getty@ttyS0.service.d
|
||||
RUN echo '[Service]\n\
|
||||
ExecStart=\n\
|
||||
ExecStart=-/sbin/agetty --noissue --autologin root %I 115200,38400,9600 vt102' \
|
||||
> /etc/systemd/system/serial-getty@ttyS0.service.d/autologin.conf
|
||||
|
||||
# 创建虚拟环境并安装服务器依赖 (with cleanup)
|
||||
RUN python -m venv $SERVER_PATH/.venv
|
||||
COPY ./server/requirements.txt $SERVER_PATH
|
||||
RUN $SERVER_PATH/.venv/bin/pip install --no-cache-dir -r $SERVER_PATH/requirements.txt && \
|
||||
# Clean up pip cache in virtual environment
|
||||
rm -rf $SERVER_PATH/.venv/pip-cache $SERVER_PATH/.venv/lib/python*/site-packages/pip/_vendor/distlib/*.whl
|
||||
COPY ./server $SERVER_PATH
|
||||
|
||||
# 复制配置文件
|
||||
COPY matplotlibrc /root/.config/matplotlib/.matplotlibrc
|
||||
COPY ./start-up.sh $JUPYTER_CONFIG_PATH/
|
||||
RUN chmod +x $JUPYTER_CONFIG_PATH/start-up.sh
|
||||
COPY ./jupyter_server_config.py $JUPYTER_CONFIG_PATH/
|
||||
|
||||
# 复制中文输入法修复脚本(容器内运行 fix-ime 命令)
|
||||
COPY ./fix-chrome-ime-final.sh /usr/local/bin/fix-ime
|
||||
RUN chmod +x /usr/local/bin/fix-ime
|
||||
|
||||
|
||||
RUN mkdir -p $IPYTHON_CONFIG_PATH/profile_default
|
||||
COPY ipython_kernel_config.py $IPYTHON_CONFIG_PATH/profile_default/
|
||||
RUN mkdir -p $IPYTHON_CONFIG_PATH/profile_default/startup
|
||||
COPY startup_scripts/* $IPYTHON_CONFIG_PATH/profile_default/startup
|
||||
|
||||
ENV DISPLAY=:0
|
||||
|
||||
# 设置全局输入法环境变量(容器级别 - 纯 fcitx5)
|
||||
ENV GTK_IM_MODULE=fcitx5 \
|
||||
QT_IM_MODULE=fcitx5 \
|
||||
XMODIFIERS=@im=fcitx5 \
|
||||
INPUT_METHOD=fcitx5 \
|
||||
SDL_IM_MODULE=fcitx5 \
|
||||
GLFW_IM_MODULE=fcitx5
|
||||
|
||||
# 配置 D-Bus 和 PolicyKit(已在前面安装 dbus-x11)
|
||||
RUN mkdir -p /etc/dbus-1/session.d && \
|
||||
printf '<!-- Session bus for user -->\n<!DOCTYPE busconfig PUBLIC "-//freedesktop//DTD D-Bus Bus Configuration 1.0//EN" "http://www.freedesktop.org/standards/dbus/1.0/busconfig.dtd">\n<busconfig>\n <policy user="user">\n <allow own="*"/>\n <allow own_prefix="*"/>\n <allow send_destination="*"/>\n <allow receive_sender="*"/>\n </policy>\n</busconfig>' > /etc/dbus-1/session.d/user-session.conf && \
|
||||
# Set proper permissions
|
||||
chown -R root:root /etc/polkit-1 /etc/dbus-1 && \
|
||||
chmod 755 /etc/polkit-1/localauthority/50-local.d /etc/dbus-1/session.d && \
|
||||
chmod 644 /etc/polkit-1/localauthority/50-local.d/*.pkla /etc/dbus-1/session.d/*.conf
|
||||
|
||||
|
||||
|
||||
|
||||
ENTRYPOINT $JUPYTER_CONFIG_PATH/start-up.sh
|
||||
|
||||
#/bin/bash -l -c "/usr/bin/envd"
|
||||
41
qiming-rcoder/docker/rcoder-agent-runner/Dockerfile.test
Normal file
41
qiming-rcoder/docker/rcoder-agent-runner/Dockerfile.test
Normal file
@@ -0,0 +1,41 @@
|
||||
# ============================================================================
|
||||
# 测试 Dockerfile - 快速验证 eBPF 工具安装逻辑
|
||||
# ============================================================================
|
||||
FROM debian:12
|
||||
|
||||
# 设置工作目录
|
||||
WORKDIR /test
|
||||
|
||||
# ============================================================================
|
||||
# 测试 eBPF 工具安装逻辑(从 Dockerfile 复制的核心逻辑)
|
||||
# ============================================================================
|
||||
ARG INSTALL_EBPF_TOOLS=true
|
||||
|
||||
RUN echo "🔍 INSTALL_EBPF_TOOLS = [$INSTALL_EBPF_TOOLS]"
|
||||
|
||||
RUN if [ "$INSTALL_EBPF_TOOLS" = "true" ]; then \
|
||||
echo "✅ 条件满足,开始安装 eBPF 工具..."; \
|
||||
apt-get update && \
|
||||
echo "📦 安装 bpftrace(核心诊断工具)..."; \
|
||||
apt-get install -y bpftrace && \
|
||||
echo "📦 安装 strace(系统调用追踪)..."; \
|
||||
apt-get install -y strace && \
|
||||
echo "📦 安装 sysstat(性能监控)..."; \
|
||||
apt-get install -y sysstat && \
|
||||
echo "📦 安装 jq(JSON 处理)..."; \
|
||||
apt-get install -y jq && \
|
||||
rm -rf /var/lib/apt/lists/*; \
|
||||
echo "✅ eBPF 工具安装完成"; \
|
||||
which bpftrace && bpftrace --version && echo "✅ bpftrace 已安装" || echo "❌ bpftrace 未找到"; \
|
||||
which strace && echo "✅ strace 已安装" || echo "❌ strace 未找到"; \
|
||||
else \
|
||||
echo "⚠️ 条件不满足,跳过 eBPF 工具安装"; \
|
||||
echo "INSTALL_EBPF_TOOLS=[$INSTALL_EBPF_TOOLS]"; \
|
||||
fi
|
||||
|
||||
# 最终验证
|
||||
RUN echo "=== 最终验证 ===" && \
|
||||
which bpftrace && echo "✅ bpftrace 已安装" || echo "❌ bpftrace 未安装" && \
|
||||
which strace && echo "✅ strace 已安装" || echo "❌ strace 未安装"
|
||||
|
||||
CMD ["/bin/bash"]
|
||||
@@ -0,0 +1,56 @@
|
||||
# ============================================================================
|
||||
# 快速测试 Dockerfile - 验证 Alloy 安装逻辑
|
||||
# ============================================================================
|
||||
FROM debian:12
|
||||
|
||||
WORKDIR /test
|
||||
|
||||
# ============================================================================
|
||||
# 测试参数传递
|
||||
# ============================================================================
|
||||
ARG INSTALL_ALLOY=true
|
||||
ARG ALLOY_VERSION=v1.12.2
|
||||
|
||||
RUN echo "🔍 INSTALL_ALLOY = [$INSTALL_ALLOY]"
|
||||
RUN echo "🔍 ALLOY_VERSION = [$ALLOY_VERSION]"
|
||||
|
||||
# ============================================================================
|
||||
# 测试 Grafana Alloy 安装(与主 Dockerfile 相同的逻辑)
|
||||
# ============================================================================
|
||||
RUN if [ "$INSTALL_ALLOY" = "true" ]; then \
|
||||
echo "📦 安装 Grafana Alloy ${ALLOY_VERSION}..."; \
|
||||
apt-get update && \
|
||||
apt-get install -y wget ca-certificates && \
|
||||
echo "检测系统架构..."; \
|
||||
ARCH=$(dpkg --print-architecture); \
|
||||
echo "系统架构: $${ARCH}"; \
|
||||
ALLOY_VER=${ALLOY_VERSION}; \
|
||||
ALLOY_VER_NUM=$${ALLOY_VER#v}; \
|
||||
echo "Alloy 版本: $${ALLOY_VER}"; \
|
||||
echo "Alloy 版本号: $${ALLOY_VER_NUM}"; \
|
||||
if [ "$${ARCH}" = "amd64" ] || [ "$${ARCH}" = "arm64" ]; then \
|
||||
echo "下载 Alloy deb 包 ($${ARCH})..."; \
|
||||
wget -qO- "https://github.com/grafana/alloy/releases/download/$${ALLOY_VER}/alloy-$${ALLOY_VER_NUM}-1.$${ARCH}.deb" > /tmp/alloy.deb && \
|
||||
echo "下载完成,文件大小: $$(ls -lh /tmp/alloy.deb | awk '{print $5}')"; \
|
||||
else \
|
||||
echo "❌ 不支持的架构: $${ARCH}"; \
|
||||
exit 1; \
|
||||
fi && \
|
||||
echo "安装 Alloy..."; \
|
||||
dpkg -i /tmp/alloy.deb; \
|
||||
echo "修复依赖..."; \
|
||||
apt-get install -y -f; \
|
||||
rm /tmp/alloy.deb && \
|
||||
echo "验证 Alloy 安装..."; \
|
||||
alloy --version; \
|
||||
else \
|
||||
echo "⚠️ Alloy 未安装"; \
|
||||
fi
|
||||
|
||||
# ============================================================================
|
||||
# 最终验证
|
||||
# ============================================================================
|
||||
RUN echo "=== 最终验证 ===" && \
|
||||
which alloy && echo "✅ alloy 已安装" || echo "❌ alloy 未安装"
|
||||
|
||||
CMD ["/bin/bash"]
|
||||
@@ -0,0 +1,19 @@
|
||||
# 快速测试 Alloy 安装
|
||||
FROM debian:12
|
||||
ARG INSTALL_ALLOY=true
|
||||
ARG ALLOY_VERSION=v1.12.2
|
||||
|
||||
RUN apt-get update && \
|
||||
apt-get install -y wget ca-certificates && \
|
||||
ARCH=$(dpkg --print-architecture) && \
|
||||
echo "=== Architecture: $ARCH ===" && \
|
||||
if [ "$ARCH" = "arm64" ]; then \
|
||||
wget -O /tmp/alloy.deb https://github.com/grafana/alloy/releases/download/v1.12.2/alloy-1.12.2-1.arm64.deb && \
|
||||
ls -lh /tmp/alloy.deb && \
|
||||
dpkg -i /tmp/alloy.deb && \
|
||||
apt-get install -y -f && \
|
||||
which alloy && \
|
||||
alloy --version; \
|
||||
fi
|
||||
|
||||
CMD ["/bin/bash"]
|
||||
@@ -0,0 +1,55 @@
|
||||
# ============================================================================
|
||||
# 完整测试 Dockerfile - 验证 Pyroscope + Off-CPU 工具安装
|
||||
# ============================================================================
|
||||
FROM debian:12
|
||||
|
||||
WORKDIR /test
|
||||
|
||||
# ============================================================================
|
||||
# 测试参数传递
|
||||
# ============================================================================
|
||||
ARG INSTALL_EBPF_TOOLS=true
|
||||
ARG INSTALL_PYROSCOPE=true
|
||||
|
||||
RUN echo "🔍 INSTALL_EBPF_TOOLS = [$INSTALL_EBPF_TOOLS]"
|
||||
RUN echo "🔍 INSTALL_PYROSCOPE = [$INSTALL_PYROSCOPE]"
|
||||
|
||||
# ============================================================================
|
||||
# 测试 Pyroscope Agent 安装
|
||||
# ============================================================================
|
||||
RUN if [ "$INSTALL_PYROSCOPE" = "true" ]; then \
|
||||
echo "📦 安装 Pyroscope Agent..."; \
|
||||
apt-get update && \
|
||||
apt-get install -y curl ca-certificates && \
|
||||
echo "下载 Pyroscope Agent (v1.17.0)..." && \
|
||||
curl -fsSL "https://github.com/grafana/pyroscope/releases/download/v1.17.0/pyroscope_1.17.0_linux_arm64.tar.gz" \
|
||||
-o /tmp/pyroscope.tar.gz && \
|
||||
echo "解压 Pyroscope Agent..." && \
|
||||
tar -xzf /tmp/pyroscope.tar.gz -C /usr/local/bin/ && \
|
||||
rm /tmp/pyroscope.tar.gz && \
|
||||
chmod +x /usr/local/bin/pyroscope && \
|
||||
echo "验证 Pyroscope Agent..." && \
|
||||
pyroscope --version || echo "⚠️ Pyroscope 安装失败"; \
|
||||
else \
|
||||
echo "⚠️ Pyroscope 未安装"; \
|
||||
fi
|
||||
|
||||
# ============================================================================
|
||||
# 测试 bpfcc-tools 安装(包含 offcputime)
|
||||
# ============================================================================
|
||||
RUN if [ "$INSTALL_EBPF_TOOLS" = "true" ]; then \
|
||||
echo "📦 安装 bpfcc-tools (包含 offcputime)..."; \
|
||||
apt-get update && \
|
||||
apt-get install -y bpfcc-tools && \
|
||||
rm -rf /var/lib/apt/lists/* && \
|
||||
which offcputime-bpfcc && offcputime-bpfcc --help | head -3 || echo "⚠️ offcputime-bpfcc 安装失败"; \
|
||||
fi
|
||||
|
||||
# ============================================================================
|
||||
# 最终验证
|
||||
# ============================================================================
|
||||
RUN echo "=== 最终验证 ===" && \
|
||||
which pyroscope && echo "✅ pyroscope 已安装" || echo "❌ pyroscope 未安装" && \
|
||||
which offcputime-bpfcc && echo "✅ offcputime-bpfcc 已安装" || echo "❌ offcputime-bpfcc 未安装"
|
||||
|
||||
CMD ["/bin/bash"]
|
||||
140
qiming-rcoder/docker/rcoder-agent-runner/FINAL_IME_SOLUTION.md
Normal file
140
qiming-rcoder/docker/rcoder-agent-runner/FINAL_IME_SOLUTION.md
Normal file
@@ -0,0 +1,140 @@
|
||||
# 🎉 Docker 中文输入法最终解决方案
|
||||
|
||||
## ✅ 成功方案
|
||||
|
||||
**纯 fcitx5 方案**(不使用 ibus 桥接)
|
||||
|
||||
## 核心架构
|
||||
|
||||
```
|
||||
Chromium (GTK3 应用)
|
||||
↓ GTK_IM_MODULE=fcitx5
|
||||
fcitx5 (输入法引擎)
|
||||
↓
|
||||
fcitx5-pinyin (拼音输入法)
|
||||
```
|
||||
|
||||
## 关键修改
|
||||
|
||||
### 1. 软件包(Dockerfile 第 25 行)
|
||||
```bash
|
||||
fcitx5 fcitx5-pinyin fcitx5-frontend-gtk3 fcitx5-frontend-gtk2 fcitx5-frontend-qt5 fcitx5-module-xorg fcitx5-config-qt im-config dbus-x11
|
||||
```
|
||||
|
||||
**移除了**: `ibus ibus-gtk ibus-gtk3`(不再需要)
|
||||
|
||||
### 2. 环境变量(所有位置)
|
||||
```bash
|
||||
export GTK_IM_MODULE=fcitx5 # 不是 ibus!
|
||||
export QT_IM_MODULE=fcitx5
|
||||
export XMODIFIERS=@im=fcitx5
|
||||
export INPUT_METHOD=fcitx5
|
||||
```
|
||||
|
||||
### 3. Chromium 启动脚本
|
||||
创建 `/usr/bin/chromium-browser-launcher`:
|
||||
```bash
|
||||
#!/bin/bash
|
||||
# 加载 D-Bus 地址
|
||||
if [ -f /tmp/dbus-session-env ]; then
|
||||
source /tmp/dbus-session-env
|
||||
fi
|
||||
|
||||
# 设置输入法环境变量
|
||||
export DISPLAY=:0
|
||||
export GTK_IM_MODULE=fcitx5
|
||||
export QT_IM_MODULE=fcitx5
|
||||
export XMODIFIERS=@im=fcitx5
|
||||
export INPUT_METHOD=fcitx5
|
||||
|
||||
# 启动 Chromium
|
||||
exec /usr/bin/chromium \
|
||||
--user-data-dir=/home/user/chromium-data \
|
||||
--no-sandbox \
|
||||
--disable-dev-shm-usage \
|
||||
--remote-debugging-port=9222 \
|
||||
--remote-debugging-address=0.0.0.0 \
|
||||
--no-first-run \
|
||||
--no-default-browser-check "$@"
|
||||
```
|
||||
|
||||
### 4. XFCE 自启动
|
||||
只需要 `fcitx5.desktop`:
|
||||
```desktop
|
||||
[Desktop Entry]
|
||||
Type=Application
|
||||
Name=Fcitx 5
|
||||
Exec=fcitx5 -d --replace
|
||||
Icon=fcitx5
|
||||
StartupNotify=false
|
||||
Terminal=false
|
||||
X-GNOME-Autostart-enabled=true
|
||||
X-XFCE-Autostart-enabled=true
|
||||
```
|
||||
|
||||
## 为什么这个方案有效?
|
||||
|
||||
1. **Chromium 启动脚本**:确保 Chromium 启动时就有正确的环境变量
|
||||
2. **纯 fcitx5**:避免了 ibus 桥接的复杂性和兼容性问题
|
||||
3. **D-Bus 地址**:从 `/tmp/dbus-session-env` 加载,确保连接正确
|
||||
|
||||
## 之前方案失败的原因
|
||||
|
||||
### ibus 桥接方案失败
|
||||
- fcitx5 的 `ibusfrontend` 模块虽然加载,但**从未成功注册到 ibus**
|
||||
- `ibus list-engine` 从未显示 fcitx5 的输入法引擎
|
||||
- ibus 和 fcitx5 之间的桥接从一开始就没有工作
|
||||
|
||||
### 环境变量传递失败
|
||||
- 在 shell 脚本中设置环境变量,但 XFCE 子进程没有继承
|
||||
- `/proc/<pid>/environ` 读取权限问题导致无法验证
|
||||
- `dbus-launch` 创建新会话,覆盖了之前的 D-Bus 地址
|
||||
|
||||
## 测试验证
|
||||
|
||||
### 成功标志
|
||||
- ✅ 输入法托盘显示拼音图标
|
||||
- ✅ Ctrl+Space 可以切换输入法
|
||||
- ✅ Chromium 搜索框可以输入中文
|
||||
- ✅ 终端、gedit 等应用可以输入中文
|
||||
|
||||
### 测试步骤
|
||||
1. 启动容器
|
||||
2. 连接 VNC
|
||||
3. 打开 Chromium
|
||||
4. 访问 baidu.com
|
||||
5. 按 Ctrl+Space 切换输入法
|
||||
6. 输入拼音(如 nihao)
|
||||
|
||||
## 构建和使用
|
||||
|
||||
```bash
|
||||
# 构建镜像
|
||||
docker build -t edgebox-sandbox .
|
||||
|
||||
# 启动容器
|
||||
docker run -d --name sandbox -p 8080:8080 edgebox-sandbox
|
||||
|
||||
# 访问 VNC
|
||||
open http://localhost:8080
|
||||
```
|
||||
|
||||
## 文件清单
|
||||
|
||||
### 修改的文件
|
||||
- `Dockerfile`: 完全移除 ibus,改用纯 fcitx5
|
||||
- `start-up.sh`: 环境变量改为 fcitx5
|
||||
- `chromium-browser-launcher`: 新的启动脚本
|
||||
|
||||
### 移除的内容
|
||||
- ❌ ibus 相关包
|
||||
- ❌ ibus 自启动配置
|
||||
- ❌ start-ibus.sh 脚本
|
||||
- ❌ GTK_IM_MODULE=ibus 配置
|
||||
|
||||
---
|
||||
|
||||
**状态**: ✅ 已验证可用
|
||||
**测试日期**: 2025-11-28
|
||||
**环境**: Debian 12 + XFCE + Chromium + fcitx5
|
||||
**方案**: 纯 fcitx5(无 ibus 桥接)
|
||||
497
qiming-rcoder/docker/rcoder-agent-runner/README.md
Normal file
497
qiming-rcoder/docker/rcoder-agent-runner/README.md
Normal file
@@ -0,0 +1,497 @@
|
||||
# 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` 环境变量。
|
||||
|
||||
---
|
||||
|
||||
## 联系方式
|
||||
|
||||
如有问题,请联系后端开发团队。
|
||||
208
qiming-rcoder/docker/rcoder-agent-runner/TROUBLESHOOTING.md
Normal file
208
qiming-rcoder/docker/rcoder-agent-runner/TROUBLESHOOTING.md
Normal file
@@ -0,0 +1,208 @@
|
||||
# RCoder Agent Runner 故障排查指南
|
||||
|
||||
## 常见问题
|
||||
|
||||
### 1. VNC 虚拟桌面文件权限问题
|
||||
|
||||
#### 问题描述
|
||||
|
||||
在 VNC 虚拟桌面中尝试打开文件时,提示:
|
||||
```
|
||||
Could not open the file "/home/user/xxx/file.xxx".
|
||||
You do not have the permissions necessary to open the file.
|
||||
```
|
||||
|
||||
#### 根本原因
|
||||
|
||||
这是 Docker 容器用户 UID 不匹配导致的权限问题:
|
||||
|
||||
1. **宿主机目录挂载**:当宿主机目录挂载到容器的 `/home/user` 时,宿主机文件的 UID 可能与容器内 `user` 用户的 UID(默认 1000)不同
|
||||
2. **权限检查失败**:容器内的 `user` 用户无法读取其他 UID 所有者的文件
|
||||
3. **VNC 用户权限**:VNC 桌面环境以 `user` 用户身份运行,继承了权限限制
|
||||
|
||||
#### 解决方案
|
||||
|
||||
##### 方案 A:自动修复(推荐)
|
||||
|
||||
从 **v1.x.x** 版本开始,容器启动脚本会自动修复挂载目录的权限问题。
|
||||
|
||||
**修复逻辑**(在 `start-up.sh` 中自动执行):
|
||||
```bash
|
||||
# 修复整个 /home/user 的所有者
|
||||
chown -R user:user /home/user
|
||||
|
||||
# 修复目录权限:确保 user 可以读取、写入、执行目录
|
||||
find /home/user -type d -exec chmod u+rwx {} \;
|
||||
|
||||
# 修复文件权限:确保 user 可以读取和写入文件
|
||||
find /home/user -type f -exec chmod u+rw {} \;
|
||||
```
|
||||
|
||||
**使用新版本镜像**:
|
||||
```bash
|
||||
# 重新构建镜像
|
||||
make dev-build
|
||||
|
||||
# 停止旧容器
|
||||
make dev-down
|
||||
|
||||
# 启动新容器(自动修复权限)
|
||||
make dev-up
|
||||
```
|
||||
|
||||
##### 方案 B:手动修复(临时方案)
|
||||
|
||||
如果使用旧版本镜像,可以手动在容器内修复权限:
|
||||
|
||||
```bash
|
||||
# 1. 查找容器名称
|
||||
docker ps | grep rcoder
|
||||
|
||||
# 2. 进入容器
|
||||
docker exec -it <container_name> bash
|
||||
|
||||
# 3. 修复整个 /home/user 目录权限
|
||||
chown -R user:user /home/user
|
||||
find /home/user -type d -exec chmod u+rwx {} \;
|
||||
find /home/user -type f -exec chmod u+rw {} \;
|
||||
|
||||
# 4. 退出容器
|
||||
exit
|
||||
```
|
||||
|
||||
**注意**:手动修复后,在 VNC 桌面中需要重新打开文件管理器或应用程序才能生效。
|
||||
|
||||
##### 方案 C:宿主机侧修复(不推荐)
|
||||
|
||||
在宿主机上修改文件所有者(需要 sudo 权限):
|
||||
|
||||
```bash
|
||||
# 查看容器内 user 用户的 UID
|
||||
docker exec <container_name> id -u user
|
||||
# 输出:1000
|
||||
|
||||
# 在宿主机上修改文件所有者为容器内的 UID
|
||||
sudo chown -R 1000:1000 /path/to/host/directory
|
||||
```
|
||||
|
||||
**缺点**:
|
||||
- 需要 root 权限
|
||||
- 会影响宿主机文件系统
|
||||
- 不适合多用户环境
|
||||
|
||||
#### 预防措施
|
||||
|
||||
**推荐做法**:
|
||||
|
||||
1. **使用最新版本镜像**:确保使用包含自动权限修复逻辑的镜像版本
|
||||
2. **避免混合 UID**:尽量让宿主机用户的 UID 与容器内 `user` 用户的 UID(1000)保持一致
|
||||
3. **使用 Docker Volume**:对于需要持久化的数据,使用 Docker Volume 而不是直接挂载宿主机目录
|
||||
|
||||
**Docker Compose 配置示例**:
|
||||
```yaml
|
||||
services:
|
||||
agent-runner:
|
||||
image: rcoder-agent-runner:latest
|
||||
volumes:
|
||||
# 推荐:使用 Docker Volume
|
||||
- user_home_volume:/home/user
|
||||
|
||||
# 或者:挂载宿主机目录(会自动修复权限)
|
||||
- ./host-directory:/home/user
|
||||
|
||||
volumes:
|
||||
user_home_volume:
|
||||
```
|
||||
|
||||
#### 技术细节
|
||||
|
||||
**为什么会出现 UID 不匹配?**
|
||||
|
||||
Docker 容器使用 Linux 内核的命名空间(namespace)隔离,但文件系统的 UID/GID 是全局的:
|
||||
|
||||
- **容器内 `user` 用户**:UID = 1000, GID = 1000
|
||||
- **宿主机用户**:UID 可能是 1001, 1002, ... 或任意值
|
||||
- **挂载目录**:保留宿主机文件的原始 UID/GID
|
||||
|
||||
当容器内的进程尝试访问宿主机文件时,Linux 内核检查进程的 UID(1000)是否匹配文件的 UID(如 1001),不匹配则拒绝访问。
|
||||
|
||||
**权限位说明**:
|
||||
- `u+rwx`:所有者可读(r)、可写(w)、可执行(x)目录
|
||||
- `u+rw`:所有者可读(r)、可写(w)文件
|
||||
|
||||
### 2. Chromium 浏览器无法打开
|
||||
|
||||
#### 问题描述
|
||||
VNC 桌面中 Chromium 浏览器无法启动或显示空白页面。
|
||||
|
||||
#### 解决方案
|
||||
检查 Chromium 数据目录权限(已在启动脚本中自动处理):
|
||||
```bash
|
||||
# 容器内执行
|
||||
ls -la /home/user/.config/chromium
|
||||
```
|
||||
|
||||
如果权限不正确,容器会在启动时自动修复。
|
||||
|
||||
### 3. 中文输入法无法使用
|
||||
|
||||
#### 问题描述
|
||||
在 VNC 桌面中无法使用中文输入法(fcitx5)。
|
||||
|
||||
#### 解决方案
|
||||
1. 检查 fcitx5 进程是否运行:
|
||||
```bash
|
||||
docker exec <container_name> pgrep fcitx5
|
||||
```
|
||||
|
||||
2. 如果未运行,手动启动:
|
||||
```bash
|
||||
docker exec -u user <container_name> bash -c "DISPLAY=:0 fcitx5 -d"
|
||||
```
|
||||
|
||||
3. 在 VNC 桌面中使用 `Ctrl+Space` 切换输入法
|
||||
|
||||
## 日志和调试
|
||||
|
||||
### 查看容器启动日志
|
||||
```bash
|
||||
# 查看最新日志
|
||||
docker logs <container_name>
|
||||
|
||||
# 实时查看日志
|
||||
docker logs -f <container_name>
|
||||
```
|
||||
|
||||
### 进入容器调试
|
||||
```bash
|
||||
# 以 root 用户进入
|
||||
docker exec -it <container_name> bash
|
||||
|
||||
# 以 user 用户进入
|
||||
docker exec -it -u user <container_name> bash
|
||||
```
|
||||
|
||||
### 检查 VNC 服务状态
|
||||
```bash
|
||||
# 检查 x11vnc 进程
|
||||
docker exec <container_name> pgrep x11vnc
|
||||
|
||||
# 检查 noVNC 端口
|
||||
docker exec <container_name> netstat -tuln | grep 6080
|
||||
```
|
||||
|
||||
## 联系支持
|
||||
|
||||
如果以上方案无法解决问题,请:
|
||||
|
||||
1. 收集以下信息:
|
||||
- 容器版本:`docker inspect <container_name> | grep Image`
|
||||
- 错误日志:`docker logs <container_name>`
|
||||
- 宿主机操作系统和版本
|
||||
|
||||
2. 提交 Issue 到 GitHub 仓库
|
||||
|
||||
## 更新记录
|
||||
|
||||
- **v1.x.x** (2025-12-19): 添加自动权限修复逻辑
|
||||
- **v1.0.0** (初始版本): 基础容器镜像
|
||||
264
qiming-rcoder/docker/rcoder-agent-runner/audio_server.py
Normal file
264
qiming-rcoder/docker/rcoder-agent-runner/audio_server.py
Normal file
@@ -0,0 +1,264 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
pcmflux Audio Server for noVNC Remote Desktop
|
||||
|
||||
This script captures audio from PulseAudio virtual sink and streams it
|
||||
to web browsers via WebSocket using Opus encoding.
|
||||
|
||||
Based on: https://github.com/linuxserver/pcmflux
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import ctypes
|
||||
import mimetypes
|
||||
import os
|
||||
import websockets
|
||||
import websockets.asyncio.server as ws_async
|
||||
|
||||
from pcmflux import AudioCapture, AudioCaptureSettings, AudioChunkCallback
|
||||
|
||||
# --- Global Context for managing shared state across async tasks and threads. ---
|
||||
g_loop = None # The main asyncio event loop.
|
||||
g_settings = None # The audio capture configuration.
|
||||
g_callback = None # The C-compatible callback function pointer.
|
||||
g_module = None # The pcmflux.AudioCapture module instance.
|
||||
g_clients = set() # A set of currently connected WebSocket clients.
|
||||
g_is_capturing = False # A flag to track the audio capture state.
|
||||
g_audio_queue = None # An asyncio.Queue for passing audio data between threads.
|
||||
g_send_task = None # The asyncio.Task that broadcasts audio to clients.
|
||||
|
||||
# --- Configuration ---
|
||||
# Audio streaming ports - configurable via environment variables
|
||||
HTTP_PORT = int(os.environ.get("AUDIO_HTTP_PORT", "6090"))
|
||||
WS_PORT = int(os.environ.get("AUDIO_WS_PORT", "6089"))
|
||||
AUDIO_DEVICE = os.environ.get("AUDIO_DEVICE", "virtual_speaker.monitor")
|
||||
|
||||
# --- End Global Context ---
|
||||
|
||||
async def send_audio_chunks():
|
||||
"""
|
||||
An asynchronous task that runs continuously to broadcast audio.
|
||||
|
||||
It retrieves encoded Opus audio chunks from the thread-safe queue and sends
|
||||
them to all currently connected WebSocket clients concurrently.
|
||||
"""
|
||||
global g_audio_queue, g_clients
|
||||
print("Audio chunk broadcasting task started.")
|
||||
try:
|
||||
while True:
|
||||
# Wait for an Opus chunk to arrive from the audio capture thread.
|
||||
opus_bytes = await g_audio_queue.get()
|
||||
|
||||
# If no clients are connected, just clear the queue item and wait.
|
||||
if not g_clients:
|
||||
g_audio_queue.task_done()
|
||||
continue
|
||||
|
||||
# We define a simple protocol: a 1-byte header (0x01) indicates
|
||||
# that the payload is an Opus audio chunk.
|
||||
message_to_send = b'\x01' + opus_bytes
|
||||
|
||||
# Broadcast the message to all clients concurrently.
|
||||
active_clients = list(g_clients)
|
||||
tasks = [client.send(message_to_send) for client in active_clients]
|
||||
if tasks:
|
||||
# asyncio.gather runs all send operations in parallel.
|
||||
await asyncio.gather(*tasks, return_exceptions=True)
|
||||
|
||||
g_audio_queue.task_done()
|
||||
except asyncio.CancelledError:
|
||||
print("Audio chunk broadcasting task cancelled.")
|
||||
finally:
|
||||
print("Audio chunk broadcasting task finished.")
|
||||
|
||||
async def health_check(connection, request):
|
||||
"""
|
||||
A pre-processor for incoming connections to the WebSocket port.
|
||||
"""
|
||||
if request.path == "/favicon.ico":
|
||||
return connection.respond(204, headers=[], body=b"")
|
||||
return None
|
||||
|
||||
async def ws_handler(websocket, path=None):
|
||||
"""
|
||||
Handles the lifecycle of each WebSocket client connection.
|
||||
"""
|
||||
global g_clients, g_is_capturing, g_audio_queue, g_module, g_send_task
|
||||
global g_settings, g_callback
|
||||
|
||||
# Register the new client.
|
||||
g_clients.add(websocket)
|
||||
print(f"Client connected: {websocket.remote_address}. "
|
||||
f"Total clients: {len(g_clients)}")
|
||||
|
||||
# If this is the first client, start the audio capture process.
|
||||
if not g_is_capturing and g_module:
|
||||
print("First client connected. Starting audio capture...")
|
||||
g_audio_queue = asyncio.Queue()
|
||||
g_module.start_capture(g_settings, g_callback)
|
||||
g_is_capturing = True
|
||||
|
||||
# Ensure the broadcasting task is running.
|
||||
if g_send_task is None or g_send_task.done():
|
||||
g_send_task = asyncio.create_task(send_audio_chunks())
|
||||
print("Audio capture process initiated.")
|
||||
|
||||
try:
|
||||
# Wait for messages from the client.
|
||||
async for _ in websocket:
|
||||
pass
|
||||
except websockets.exceptions.ConnectionClosed:
|
||||
pass
|
||||
finally:
|
||||
# Unregister the client upon disconnection.
|
||||
if websocket in g_clients:
|
||||
g_clients.remove(websocket)
|
||||
print(f"Client disconnected. Remaining clients: {len(g_clients)}")
|
||||
|
||||
# If this was the last client, stop the audio capture to save resources.
|
||||
if g_is_capturing and not g_clients and g_module:
|
||||
print("Last client disconnected. Stopping audio capture...")
|
||||
g_module.stop_capture()
|
||||
g_is_capturing = False
|
||||
if g_send_task:
|
||||
g_send_task.cancel()
|
||||
g_send_task = None
|
||||
g_audio_queue = None
|
||||
print("Audio capture process stopped.")
|
||||
|
||||
def py_audio_callback(result_ptr, user_data):
|
||||
"""
|
||||
A C-style callback function that bridges the C++ and Python worlds.
|
||||
"""
|
||||
global g_is_capturing, g_audio_queue, g_loop
|
||||
|
||||
if g_is_capturing and result_ptr and g_audio_queue is not None:
|
||||
result = result_ptr.contents
|
||||
if result.data and result.size > 0:
|
||||
data_bytes = bytes(ctypes.cast(
|
||||
result.data, ctypes.POINTER(ctypes.c_ubyte * result.size)
|
||||
).contents)
|
||||
|
||||
if g_loop and not g_loop.is_closed():
|
||||
asyncio.run_coroutine_threadsafe(
|
||||
g_audio_queue.put(data_bytes), g_loop)
|
||||
|
||||
async def handle_http_request(reader, writer):
|
||||
"""Handle HTTP requests by serving static files."""
|
||||
try:
|
||||
request_line = await reader.readline()
|
||||
if not request_line:
|
||||
return
|
||||
|
||||
parts = request_line.split()
|
||||
if len(parts) < 2 or parts[0] != b'GET':
|
||||
writer.write(b'HTTP/1.1 405 Method Not Allowed\r\n\r\n')
|
||||
return
|
||||
|
||||
path = parts[1].decode()
|
||||
if path == '/':
|
||||
path = '/index.html'
|
||||
|
||||
script_dir = os.path.dirname(os.path.abspath(__file__))
|
||||
# Look for files in the audio_static directory
|
||||
# When installed via Dockerfile, files are in /usr/local/share/audio_static
|
||||
static_dir = '/usr/local/share/audio_static'
|
||||
if not os.path.exists(static_dir):
|
||||
# Fallback to script directory for local development
|
||||
static_dir = os.path.join(script_dir, 'audio_static')
|
||||
if not os.path.exists(static_dir):
|
||||
static_dir = script_dir
|
||||
full_path = os.path.join(static_dir, path.lstrip('/'))
|
||||
|
||||
# Security check: prevent directory traversal
|
||||
if not os.path.normpath(full_path).startswith(os.path.normpath(static_dir)):
|
||||
writer.write(b'HTTP/1.1 403 Forbidden\r\n\r\n')
|
||||
return
|
||||
|
||||
if os.path.isfile(full_path):
|
||||
with open(full_path, 'rb') as f:
|
||||
content = f.read()
|
||||
|
||||
content_type = mimetypes.guess_type(full_path)[0] or 'application/octet-stream'
|
||||
|
||||
headers = f'HTTP/1.1 200 OK\r\nContent-Type: {content_type}\r\nContent-Length: {len(content)}\r\n\r\n'
|
||||
writer.write(headers.encode())
|
||||
writer.write(content)
|
||||
else:
|
||||
writer.write(b'HTTP/1.1 404 Not Found\r\n\r\n')
|
||||
|
||||
except Exception as e:
|
||||
print(f"[HTTP Error] {e}")
|
||||
writer.write(b'HTTP/1.1 500 Internal Server Error\r\n\r\n')
|
||||
finally:
|
||||
await writer.drain()
|
||||
writer.close()
|
||||
|
||||
async def main_async():
|
||||
"""The main routine to initialize and run the servers."""
|
||||
global g_loop, g_settings, g_callback, g_module
|
||||
|
||||
g_loop = asyncio.get_running_loop()
|
||||
|
||||
# --- Configure Audio Capture Parameters ---
|
||||
g_settings = AudioCaptureSettings()
|
||||
# Capture from the virtual speaker monitor (created by PulseAudio)
|
||||
g_settings.device_name = AUDIO_DEVICE.encode() if AUDIO_DEVICE else None
|
||||
g_settings.sample_rate = 48000
|
||||
g_settings.channels = 2
|
||||
g_settings.opus_bitrate = 128000
|
||||
g_settings.frame_duration_ms = 20
|
||||
g_settings.use_vbr = True
|
||||
g_settings.use_silence_gate = True # Skip silent audio to save bandwidth
|
||||
g_settings.debug_logging = False
|
||||
# --- End Configuration ---
|
||||
|
||||
# Create the C-compatible callback object.
|
||||
g_callback = AudioChunkCallback(py_audio_callback)
|
||||
g_module = AudioCapture()
|
||||
print("pcmflux audio capture module initialized.")
|
||||
print(f"Audio device: {AUDIO_DEVICE}")
|
||||
|
||||
# Start HTTP server
|
||||
http_server = await asyncio.start_server(
|
||||
handle_http_request, '0.0.0.0', HTTP_PORT
|
||||
)
|
||||
print(f"HTTP server started on http://0.0.0.0:{HTTP_PORT}")
|
||||
print(f"-> Open http://localhost:{HTTP_PORT}/ in your browser for audio player.")
|
||||
|
||||
# Start the WebSocket server.
|
||||
ws_server = await ws_async.serve(
|
||||
ws_handler,
|
||||
'0.0.0.0',
|
||||
WS_PORT,
|
||||
process_request=health_check
|
||||
)
|
||||
print(f"WebSocket server started on ws://0.0.0.0:{WS_PORT}")
|
||||
|
||||
try:
|
||||
# Keep the main coroutine running indefinitely.
|
||||
await asyncio.Event().wait()
|
||||
except KeyboardInterrupt:
|
||||
pass
|
||||
finally:
|
||||
# Perform a graceful shutdown.
|
||||
print("\nShutting down...")
|
||||
if g_is_capturing and g_module:
|
||||
g_module.stop_capture()
|
||||
if g_send_task:
|
||||
g_send_task.cancel()
|
||||
if ws_server:
|
||||
ws_server.close()
|
||||
await ws_server.wait_closed()
|
||||
if g_module:
|
||||
del g_module
|
||||
print("Cleanup complete.")
|
||||
|
||||
if __name__ == "__main__":
|
||||
print("=" * 60)
|
||||
print("pcmflux Audio Server for noVNC Remote Desktop")
|
||||
print("=" * 60)
|
||||
try:
|
||||
asyncio.run(main_async())
|
||||
except KeyboardInterrupt:
|
||||
print("\nApplication exiting.")
|
||||
308
qiming-rcoder/docker/rcoder-agent-runner/audio_static/index.html
Normal file
308
qiming-rcoder/docker/rcoder-agent-runner/audio_static/index.html
Normal file
@@ -0,0 +1,308 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Audio Player - noVNC Remote Desktop</title>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<link rel="icon" href="data:,">
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
background: linear-gradient(135deg, #1a1a2e 0%, #16213e 50%, #0f3460 100%);
|
||||
color: white;
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
.container {
|
||||
text-align: center;
|
||||
padding: 2rem;
|
||||
max-width: 500px;
|
||||
}
|
||||
h1 {
|
||||
font-size: 1.5rem;
|
||||
margin-bottom: 1rem;
|
||||
opacity: 0.9;
|
||||
}
|
||||
.status-card {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
backdrop-filter: blur(10px);
|
||||
border-radius: 16px;
|
||||
padding: 2rem;
|
||||
margin-bottom: 1.5rem;
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
#status {
|
||||
font-size: 1.1rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
.status-connected {
|
||||
color: #4ade80;
|
||||
}
|
||||
.status-playing {
|
||||
color: #60a5fa;
|
||||
}
|
||||
.status-error {
|
||||
color: #f87171;
|
||||
}
|
||||
.dot {
|
||||
display: inline-block;
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
background-color: #ffffff;
|
||||
margin: 0 3px;
|
||||
animation: blink 1.4s infinite both;
|
||||
}
|
||||
.dot.one { animation-delay: 0.0s; }
|
||||
.dot.two { animation-delay: 0.2s; }
|
||||
.dot.three { animation-delay: 0.4s; }
|
||||
@keyframes blink {
|
||||
0% { opacity: .2; }
|
||||
20% { opacity: 1; }
|
||||
100% { opacity: .2; }
|
||||
}
|
||||
.start-btn {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 1rem 2rem;
|
||||
font-size: 1.1rem;
|
||||
border-radius: 50px;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
display: none;
|
||||
}
|
||||
.start-btn:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 10px 20px rgba(102, 126, 234, 0.3);
|
||||
}
|
||||
.start-btn.show {
|
||||
display: inline-block;
|
||||
}
|
||||
.info {
|
||||
font-size: 0.85rem;
|
||||
opacity: 0.7;
|
||||
margin-top: 1rem;
|
||||
}
|
||||
.volume-icon {
|
||||
font-size: 3rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
.pulse {
|
||||
animation: pulse 2s infinite;
|
||||
}
|
||||
@keyframes pulse {
|
||||
0% { transform: scale(1); opacity: 1; }
|
||||
50% { transform: scale(1.1); opacity: 0.8; }
|
||||
100% { transform: scale(1); opacity: 1; }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<h1>🔊 Remote Desktop Audio</h1>
|
||||
<div class="status-card">
|
||||
<div class="volume-icon" id="volumeIcon">🔇</div>
|
||||
<div id="status">
|
||||
Connecting...
|
||||
<div><span class="dot one"></span><span class="dot two"></span><span class="dot three"></span></div>
|
||||
</div>
|
||||
<button class="start-btn" id="startBtn" onclick="initAudio()">
|
||||
🎵 Enable Audio
|
||||
</button>
|
||||
</div>
|
||||
<p class="info">
|
||||
Audio from the remote desktop will play here.<br>
|
||||
Keep this tab open while using noVNC.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// --- Configuration ---
|
||||
// WebSocket URL - uses same host as HTTP, different port
|
||||
const WS_PORT = 6089;
|
||||
const wsProtocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
|
||||
const WEBSOCKET_URL = `${wsProtocol}//${window.location.hostname}:${WS_PORT}`;
|
||||
|
||||
const statusDiv = document.getElementById('status');
|
||||
const startBtn = document.getElementById('startBtn');
|
||||
const volumeIcon = document.getElementById('volumeIcon');
|
||||
|
||||
let audioContext;
|
||||
let audioDecoder;
|
||||
let audioWorkletNode;
|
||||
let workletBufferLength = 0;
|
||||
let websocket;
|
||||
|
||||
function updateStatus(text, className = '') {
|
||||
statusDiv.textContent = text;
|
||||
statusDiv.className = className;
|
||||
}
|
||||
|
||||
function connectWebSocket() {
|
||||
websocket = new WebSocket(WEBSOCKET_URL);
|
||||
websocket.binaryType = 'arraybuffer';
|
||||
|
||||
websocket.onopen = () => {
|
||||
console.log("[WS] WebSocket connected.");
|
||||
updateStatus("Connected. Click to enable audio.", "status-connected");
|
||||
startBtn.classList.add('show');
|
||||
volumeIcon.textContent = '🔈';
|
||||
};
|
||||
|
||||
websocket.onmessage = (event) => {
|
||||
if (!audioDecoder || audioDecoder.state !== "configured") return;
|
||||
|
||||
if (audioContext && audioContext.state === 'suspended') {
|
||||
audioContext.resume();
|
||||
}
|
||||
updateStatus("🎵 Receiving audio...", "status-playing");
|
||||
volumeIcon.textContent = '🔊';
|
||||
volumeIcon.classList.add('pulse');
|
||||
|
||||
const dataView = new DataView(event.data);
|
||||
const dataType = dataView.getUint8(0);
|
||||
|
||||
if (dataType === 0x01) {
|
||||
const opusData = event.data.slice(1);
|
||||
|
||||
if (audioDecoder.decodeQueueSize > 10) {
|
||||
return;
|
||||
}
|
||||
|
||||
const chunk = new EncodedAudioChunk({
|
||||
type: 'key',
|
||||
timestamp: audioContext.currentTime * 1000000,
|
||||
data: opusData
|
||||
});
|
||||
|
||||
try {
|
||||
audioDecoder.decode(chunk);
|
||||
} catch (e) {
|
||||
console.error("[Decoder] Error:", e);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
websocket.onclose = () => {
|
||||
console.log("[WS] WebSocket disconnected.");
|
||||
updateStatus("Disconnected. Reconnecting...", "status-error");
|
||||
volumeIcon.textContent = '🔇';
|
||||
volumeIcon.classList.remove('pulse');
|
||||
startBtn.classList.remove('show');
|
||||
// Reconnect after 3 seconds
|
||||
setTimeout(connectWebSocket, 3000);
|
||||
};
|
||||
|
||||
websocket.onerror = (err) => {
|
||||
console.error("[WS] WebSocket error:", err);
|
||||
updateStatus("Connection error", "status-error");
|
||||
};
|
||||
}
|
||||
|
||||
async function initAudio() {
|
||||
if (audioContext) return;
|
||||
|
||||
try {
|
||||
startBtn.classList.remove('show');
|
||||
updateStatus("Initializing audio...");
|
||||
|
||||
audioContext = new (window.AudioContext || window.webkitAudioContext)({
|
||||
sampleRate: 48000,
|
||||
latencyHint: 'interactive'
|
||||
});
|
||||
|
||||
const decoderConfig = { codec: 'opus', sampleRate: 48000, numberOfChannels: 2 };
|
||||
const support = await AudioDecoder.isConfigSupported(decoderConfig);
|
||||
if (!support.supported) {
|
||||
throw new Error("Opus not supported by this browser.");
|
||||
}
|
||||
|
||||
audioDecoder = new AudioDecoder({
|
||||
output: (audioData) => {
|
||||
const bufferSizeInBytes = audioData.allocationSize({ planeIndex: 0 });
|
||||
const pcmData = new Float32Array(bufferSizeInBytes / Float32Array.BYTES_PER_ELEMENT);
|
||||
audioData.copyTo(pcmData, { planeIndex: 0 });
|
||||
audioData.close();
|
||||
|
||||
if (audioWorkletNode) {
|
||||
audioWorkletNode.port.postMessage(pcmData.buffer, [pcmData.buffer]);
|
||||
}
|
||||
},
|
||||
error: (e) => console.error("[Decoder] Error:", e)
|
||||
});
|
||||
audioDecoder.configure(decoderConfig);
|
||||
|
||||
const workletCode = `
|
||||
class PlaybackProcessor extends AudioWorkletProcessor {
|
||||
constructor() {
|
||||
super();
|
||||
this.queue = [];
|
||||
this.currentBuffer = null;
|
||||
this.bufferOffset = 0;
|
||||
this.port.onmessage = (event) => {
|
||||
this.queue.push(new Float32Array(event.data));
|
||||
};
|
||||
}
|
||||
process(inputs, outputs, parameters) {
|
||||
const output = outputs[0];
|
||||
const leftChannel = output[0];
|
||||
const rightChannel = output[1];
|
||||
const bufferSize = leftChannel.length;
|
||||
let outputPos = 0;
|
||||
while (outputPos < bufferSize) {
|
||||
if (!this.currentBuffer || this.bufferOffset >= this.currentBuffer.length) {
|
||||
if (this.queue.length > 0) {
|
||||
this.currentBuffer = this.queue.shift();
|
||||
this.bufferOffset = 0;
|
||||
} else {
|
||||
leftChannel.fill(0, outputPos);
|
||||
rightChannel.fill(0, outputPos);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
const samplesLeftInBuffer = (this.currentBuffer.length - this.bufferOffset) / 2;
|
||||
const samplesToCopy = Math.min(bufferSize - outputPos, samplesLeftInBuffer);
|
||||
for (let i = 0; i < samplesToCopy; i++) {
|
||||
leftChannel[outputPos + i] = this.currentBuffer[this.bufferOffset++];
|
||||
rightChannel[outputPos + i] = this.currentBuffer[this.bufferOffset++];
|
||||
}
|
||||
outputPos += samplesToCopy;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
registerProcessor('playback-processor', PlaybackProcessor);
|
||||
`;
|
||||
|
||||
const workletBlob = new Blob([workletCode], { type: 'application/javascript' });
|
||||
const workletURL = URL.createObjectURL(workletBlob);
|
||||
await audioContext.audioWorklet.addModule(workletURL);
|
||||
|
||||
audioWorkletNode = new AudioWorkletNode(audioContext, 'playback-processor', {
|
||||
outputChannelCount: [2]
|
||||
});
|
||||
audioWorkletNode.connect(audioContext.destination);
|
||||
|
||||
updateStatus("Audio ready. Waiting for stream...", "status-connected");
|
||||
console.log("[Audio] Pipeline initialized successfully.");
|
||||
|
||||
} catch (err) {
|
||||
console.error("[Audio] Init failed:", err);
|
||||
updateStatus("Error: " + err.message, "status-error");
|
||||
startBtn.classList.add('show');
|
||||
}
|
||||
}
|
||||
|
||||
// Start connection
|
||||
connectWebSocket();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
BIN
qiming-rcoder/docker/rcoder-agent-runner/bin/envd
Normal file
BIN
qiming-rcoder/docker/rcoder-agent-runner/bin/envd
Normal file
Binary file not shown.
BIN
qiming-rcoder/docker/rcoder-agent-runner/bin/envd-arm64
Normal file
BIN
qiming-rcoder/docker/rcoder-agent-runner/bin/envd-arm64
Normal file
Binary file not shown.
@@ -0,0 +1,38 @@
|
||||
#!/bin/bash
|
||||
# Chromium Browser Launcher with fcitx input method support
|
||||
# Runs as root with HOME=/home/user
|
||||
|
||||
# 加载环境配置
|
||||
[ -f /etc/profile.d/fcitx5-env.sh ] && source /etc/profile.d/fcitx5-env.sh
|
||||
[ -f /tmp/dbus-session-env ] && source /tmp/dbus-session-env
|
||||
[ -f /etc/profile.d/chromium-env.sh ] && source /etc/profile.d/chromium-env.sh
|
||||
[ -f /etc/profile.d/ime-env.sh ] && source /etc/profile.d/ime-env.sh
|
||||
|
||||
export DISPLAY=:0
|
||||
export HOME=/home/user
|
||||
|
||||
# 使用 @im=fcitx 保持一致性
|
||||
export GTK_IM_MODULE=fcitx
|
||||
export QT_IM_MODULE=fcitx
|
||||
export XMODIFIERS=@im=fcitx
|
||||
export INPUT_METHOD=fcitx
|
||||
|
||||
CHROMIUM_DATA_DIR="${CHROMIUM_USER_DATA_DIR:-/home/user/.config/chromium}"
|
||||
|
||||
# 调用原始的 chromium wrapper(已备份为 chromium-bin)
|
||||
exec /usr/bin/chromium-bin \
|
||||
--user-data-dir="$CHROMIUM_DATA_DIR" \
|
||||
--no-sandbox \
|
||||
--disable-dev-shm-usage \
|
||||
--remote-debugging-port=9222 \
|
||||
--remote-debugging-address=0.0.0.0 \
|
||||
--no-first-run \
|
||||
--no-default-browser-check \
|
||||
--password-store=basic \
|
||||
--use-mock-keychain \
|
||||
--disable-session-crashed-bubble \
|
||||
--disable-infobars \
|
||||
--no-process-singleton-dialog \
|
||||
--force-color-profile=srgb \
|
||||
--start-maximized \
|
||||
"$@"
|
||||
50
qiming-rcoder/docker/rcoder-agent-runner/chromium-for-mcp.sh
Normal file
50
qiming-rcoder/docker/rcoder-agent-runner/chromium-for-mcp.sh
Normal file
@@ -0,0 +1,50 @@
|
||||
#!/bin/bash
|
||||
# MCP 专用 Chromium 包装器
|
||||
# 以 root 用户身份运行 Chromium,但 HOME 设置为 /home/user
|
||||
# 确保能连接到 fcitx5 输入法的 D-Bus 会话
|
||||
|
||||
# 读取 D-Bus 会话地址
|
||||
if [ -f /tmp/dbus-session-env ]; then
|
||||
source /tmp/dbus-session-env
|
||||
fi
|
||||
|
||||
# 读取 Chromium 环境变量
|
||||
if [ -f /etc/profile.d/chromium-env.sh ]; then
|
||||
source /etc/profile.d/chromium-env.sh
|
||||
fi
|
||||
|
||||
# 读取输入法环境变量
|
||||
if [ -f /etc/profile.d/ime-env.sh ]; then
|
||||
source /etc/profile.d/ime-env.sh
|
||||
fi
|
||||
|
||||
# 使用用户主目录的 Chromium 配置(持久化)
|
||||
CHROMIUM_DATA_DIR="${CHROMIUM_USER_DATA_DIR:-/home/user/.config/chromium}"
|
||||
|
||||
# 设置环境变量(以 root 运行,但 HOME=/home/user)
|
||||
export DISPLAY=:0
|
||||
export HOME=/home/user
|
||||
export DBUS_SESSION_BUS_ADDRESS="${DBUS_SESSION_BUS_ADDRESS}"
|
||||
export GTK_IM_MODULE=fcitx
|
||||
export QT_IM_MODULE=fcitx
|
||||
export XMODIFIERS=@im=fcitx
|
||||
export INPUT_METHOD=fcitx
|
||||
export SDL_IM_MODULE=fcitx
|
||||
export GLFW_IM_MODULE=ibus
|
||||
export LANG=C.UTF-8
|
||||
export LC_ALL=C.UTF-8
|
||||
export CHROMIUM_USER_DATA_DIR="${CHROMIUM_DATA_DIR}"
|
||||
|
||||
# 直接以 root 运行 Chromium (HOME=/home/user)
|
||||
exec /usr/bin/chromium \
|
||||
--user-data-dir="${CHROMIUM_DATA_DIR}" \
|
||||
--no-sandbox \
|
||||
--disable-dev-shm-usage \
|
||||
--remote-debugging-port=9222 \
|
||||
--remote-debugging-address=0.0.0.0 \
|
||||
--no-first-run \
|
||||
--no-default-browser-check \
|
||||
--password-store=basic \
|
||||
--use-mock-keychain \
|
||||
--start-maximized \
|
||||
"$@"
|
||||
14
qiming-rcoder/docker/rcoder-agent-runner/deno.json
Normal file
14
qiming-rcoder/docker/rcoder-agent-runner/deno.json
Normal file
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"argv": [
|
||||
"/usr/bin/deno",
|
||||
"jupyter",
|
||||
"--kernel",
|
||||
"--conn",
|
||||
"{connection_file}"
|
||||
],
|
||||
"display_name": "Deno",
|
||||
"env": {
|
||||
"NO_COLOR": "1"
|
||||
},
|
||||
"language": "typescript"
|
||||
}
|
||||
449
qiming-rcoder/docker/rcoder-agent-runner/ebpf-tools/README.md
Normal file
449
qiming-rcoder/docker/rcoder-agent-runner/ebpf-tools/README.md
Normal file
@@ -0,0 +1,449 @@
|
||||
# eBPF 诊断工具
|
||||
|
||||
本目录包含用于监控和诊断 `agent_runner` 及其子进程性能的 eBPF 工具。
|
||||
|
||||
## 📊 监控架构总览
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────────────┐
|
||||
│ 进程性能监控完整方案 │
|
||||
├─────────────────────────────────────────────────────────────────────────┤
|
||||
│ │
|
||||
│ ┌─────────────────────────────────────────────────────────────────┐ │
|
||||
│ │ CPU 性能监控(持续) │ │
|
||||
│ │ │ │
|
||||
│ │ Grafana Alloy (eBPF) → Pyroscope Server → Web UI (4040) │ │
|
||||
│ │ - 97 Hz 采样率 │ │
|
||||
│ │ - 每 15 秒发送数据 │ │
|
||||
│ │ - 自动发现进程 │ │
|
||||
│ │ - 支持历史查询 │ │
|
||||
│ └─────────────────────────────────────────────────────────────────┘ │
|
||||
│ ↓ │
|
||||
│ ┌─────────────────────────────────────────────────────────────────┐ │
|
||||
│ │ 进程指标监控(持续) │ │
|
||||
│ │ │ │
|
||||
│ │ Alloy Process Exporter → Prometheus → Grafana Dashboard │ │
|
||||
│ │ - CPU、内存、I/O、FD、线程数 │ │
|
||||
│ │ - 15 秒采集间隔 │ │
|
||||
│ │ - 时序数据存储 │ │
|
||||
│ │ - Dashboard 可视化 (3000) │ │
|
||||
│ └─────────────────────────────────────────────────────────────────┘ │
|
||||
│ ↓ │
|
||||
│ ┌─────────────────────────────────────────────────────────────────┐ │
|
||||
│ │ Off-CPU 阻塞监控(定期) │ │
|
||||
│ │ │ │
|
||||
│ │ offcputime-bpfcc → SVG 火焰图文件 │ │
|
||||
│ │ - 每 60 秒生成一次 │ │
|
||||
│ │ - 显示阻塞堆栈 │ │
|
||||
│ │ - 识别 I/O、锁、等待等阻塞 │ │
|
||||
│ └─────────────────────────────────────────────────────────────────┘ │
|
||||
│ ↓ │
|
||||
│ ┌─────────────────────────────────────────────────────────────────┐ │
|
||||
│ │ 系统调用监控(持续) │ │
|
||||
│ │ │ │
|
||||
│ │ syscount-bpfcc → 统计文件 │ │
|
||||
│ │ execsnoop-bpfcc → 进程创建日志 │ │
|
||||
│ │ opensnoop-bpfcc → 文件访问日志 │ │
|
||||
│ │ - 每 60 秒统计一次 │ │
|
||||
│ │ - 持续追踪进程和文件访问 │ │
|
||||
│ └─────────────────────────────────────────────────────────────────┘ │
|
||||
│ ↓ │
|
||||
│ ┌─────────────────────────────────────────────────────────────────┐ │
|
||||
│ │ 手动诊断工具(按需使用) │ │
|
||||
│ │ │ │
|
||||
│ │ diag-tool.sh - 综合诊断工具 │ │
|
||||
│ │ auto-flamegraph.sh - 自动火焰图生成 │ │
|
||||
│ └─────────────────────────────────────────────────────────────────┘ │
|
||||
│ │
|
||||
└─────────────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
## 目录结构
|
||||
|
||||
```
|
||||
ebpf-tools/
|
||||
├── README.md # 本文档
|
||||
├── alloy-config.alloy # Grafana Alloy 配置(CPU 监控 + 进程指标导出)
|
||||
├── diag-tool.sh # 手动诊断工具
|
||||
├── auto-flamegraph.sh # 自动火焰图生成
|
||||
├── offcpu-monitor.sh # Off-CPU 阻塞监控
|
||||
└── syscall-monitor.sh # 系统调用监控
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🔧 工具详解
|
||||
|
||||
### 1. alloy-config.alloy - Grafana Alloy 配置
|
||||
|
||||
**用途**: 持续 CPU 性能监控 + 进程指标导出
|
||||
|
||||
**工作原理**:
|
||||
- 使用 eBPF 自动发现 `agent_runner` 及其子进程
|
||||
- 以 97 Hz 频率采样 CPU 性能数据,发送到 Pyroscope
|
||||
- 采集进程指标(CPU、内存、I/O、FD、线程数),发送到 Prometheus
|
||||
- 在 Web UI 中实时查看:Pyroscope (4040) + Grafana (3000)
|
||||
|
||||
**采集的指标**:
|
||||
| 指标类别 | 指标名称 | 说明 |
|
||||
|---------|---------|------|
|
||||
| **CPU** | `process_cpu_seconds_total` | CPU 时间(累计) |
|
||||
| **内存** | `process_resident_memory_bytes` | 常驻内存(RSS) |
|
||||
| **内存** | `process_virtual_memory_bytes` | 虚拟内存(VSZ) |
|
||||
| **FD** | `process_open_fds` | 打开的文件描述符 |
|
||||
| **I/O** | `process_read_bytes_total` | 读取字节数 |
|
||||
| **I/O** | `process_write_bytes_total` | 写入字节数 |
|
||||
| **线程** | `process_num_threads` | 线程数量 |
|
||||
| **上下文切换** | `process_context_switches_total` | 上下文切换次数 |
|
||||
|
||||
**进程标签**:
|
||||
- `process_pid`: 进程 PID
|
||||
- `process_name`: 进程名称
|
||||
- `process_exe`: 完整执行路径
|
||||
- `parent_pid`: 父进程 PID
|
||||
- `project_id`: 项目 ID
|
||||
- `container_id`: 容器 ID
|
||||
|
||||
**环境变量**:
|
||||
| 变量 | 默认值 | 说明 |
|
||||
|------|--------|------|
|
||||
| `ENABLE_ALLOY` | - | 是否启用(由 ebpf-debug feature 控制) |
|
||||
| `PYROSCOPE_URL` | http://pyroscope:4040 | Pyroscope Server 地址 |
|
||||
| `PROMETHEUS_URL` | http://prometheus:9090/api/v1/write | Prometheus 写入地址 |
|
||||
|
||||
**查看数据**:
|
||||
```bash
|
||||
# 1. Pyroscope Web UI (CPU 性能)
|
||||
open http://localhost:4040
|
||||
|
||||
# 2. Grafana Dashboard (进程指标)
|
||||
open http://localhost:3000
|
||||
# 登录: admin / admin
|
||||
# 查找: "Agent Runner 进程监控"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 2. offcpu-monitor.sh - Off-CPU 阻塞监控
|
||||
|
||||
**用途**: 定期生成 Off-CPU 阻塞火焰图,分析进程阻塞原因
|
||||
|
||||
**工作原理**:
|
||||
- 每 60 秒自动运行一次(可配置)
|
||||
- 对 `agent_runner` 及其所有子进程进行采样
|
||||
- 使用 `offcputime-bpfcc` 捕获阻塞堆栈
|
||||
- 生成 SVG 火焰图文件,自动清理旧文件(最多保留 50 个)
|
||||
|
||||
**环境变量**:
|
||||
| 变量 | 默认值 | 说明 |
|
||||
|------|--------|------|
|
||||
| `ENABLE_OFFCPUTIME` | - | 是否启用(由 ebpf-debug feature 控制) |
|
||||
| `OFFCPU_DURATION` | 30 | 每次采样时长(秒) |
|
||||
| `OFFCPU_INTERVAL` | 60 | 生成间隔(秒) |
|
||||
| `MAX_OFFCPU_FILES` | 50 | 最多保留文件数量 |
|
||||
|
||||
**输出文件**:
|
||||
```
|
||||
/app/container-logs/diag/
|
||||
├── offcpu-monitor.log # 监控日志
|
||||
├── offcpu-agent_runner-1-20250111_143025.svg # 主进程阻塞火焰图
|
||||
└── offcpu-claude-code-acp-123-*.svg # 子进程阻塞火焰图
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 3. syscall-monitor.sh - 系统调用监控
|
||||
|
||||
**用途**: 监控进程的系统调用活动,包括进程创建、文件访问和系统调用统计
|
||||
|
||||
**工作原理**:
|
||||
- 每 60 秒统计一次系统调用(可配置)
|
||||
- 后台持续追踪进程创建 (`execsnoop-bpfcc`)
|
||||
- 后台持续追踪文件访问 (`opensnoop-bpfcc`)
|
||||
- 定期生成系统调用统计报告
|
||||
|
||||
**采集的数据**:
|
||||
| 工具 | 数据 | 说明 |
|
||||
|------|------|------|
|
||||
| `syscount-bpfcc` | 系统调用统计 | 每个系统调用的次数和耗时 |
|
||||
| `execsnoop-bpfcc` | 进程创建日志 | 新进程的创建时间和命令行 |
|
||||
| `opensnoop-bpfcc` | 文件访问日志 | 文件打开/关闭操作 |
|
||||
|
||||
**环境变量**:
|
||||
| 变量 | 默认值 | 说明 |
|
||||
|------|--------|------|
|
||||
| `ENABLE_SYSCALL_MONITOR` | - | 是否启用(由 ebpf-debug feature 控制) |
|
||||
| `SAMPLE_DURATION` | 30 | 每次采样时长(秒) |
|
||||
| `GENERATE_INTERVAL` | 60 | 生成间隔(秒) |
|
||||
|
||||
**输出文件**:
|
||||
```
|
||||
/app/container-logs/diag/
|
||||
├── syscall-monitor.log # 监控日志
|
||||
├── syscall-count-agent_runner-1-*.txt # 系统调用统计
|
||||
├── execsnoop-*.log # 进程创建日志
|
||||
└── opensnoop-*.log # 文件访问日志
|
||||
```
|
||||
|
||||
**日志说明**:
|
||||
- **控制台输出**: 每次采样只输出一行汇总日志
|
||||
- **文件日志**: 详细日志写入 `syscall-monitor.log`
|
||||
|
||||
---
|
||||
|
||||
### 4. diag-tool.sh - 手动诊断工具
|
||||
|
||||
手动触发的 eBPF 诊断工具,用于在怀疑有性能问题时主动采集数据。
|
||||
|
||||
**用法**:
|
||||
```bash
|
||||
diag-tool.sh {offcpu|flame|profile|all} <pid> [duration]
|
||||
```
|
||||
|
||||
**命令**:
|
||||
| 命令 | 说明 |
|
||||
|------|------|
|
||||
| `offcpu <pid> [duration]` | 分析 off-cpu 堆栈,默认 30 秒 |
|
||||
| `flame <pid> [duration]` | 生成火焰图,默认 30 秒 |
|
||||
| `profile <pid> [duration]` | CPU 性能分析,默认 30 秒 |
|
||||
| `all <pid>` | 综合诊断(包含所有分析) |
|
||||
|
||||
**快捷命令**:
|
||||
- `e-offcpu` - 等同于 `diag-tool.sh offcpu`
|
||||
- `e-flame` - 等同于 `diag-tool.sh flame`
|
||||
- `e-profile` - 等同于 `diag-tool.sh profile`
|
||||
- `e-all` - 等同于 `diag-tool.sh all`
|
||||
|
||||
---
|
||||
|
||||
### 5. auto-flamegraph.sh - 自动火焰图生成
|
||||
|
||||
持续在后台运行的火焰图生成工具,自动监控 `agent_runner` 及其所有子进程。
|
||||
|
||||
**工作原理**:
|
||||
1. 自动检测 `agent_runner` 进程 PID
|
||||
2. 递归获取所有子进程 PID
|
||||
3. 使用 bpftrace 采样性能数据(默认 30 秒)
|
||||
4. 生成火焰图 SVG 文件
|
||||
5. 每 60 秒重复一次
|
||||
|
||||
---
|
||||
|
||||
## 🎯 使用场景
|
||||
|
||||
### 场景 1: 持续监控 CPU 性能
|
||||
|
||||
**目标**: 在 Web UI 中实时查看 agent_runner 性能数据
|
||||
|
||||
**步骤**:
|
||||
1. 确保容器已启动(Alloy 自动运行)
|
||||
2. 打开 http://localhost:4040 (Pyroscope)
|
||||
3. 选择应用 `agent_runner`
|
||||
4. 按需过滤标签(如 `process_name="agent_runner"`)
|
||||
5. 查看火焰图、时序数据、Top 函数
|
||||
|
||||
**适用问题**:
|
||||
- CPU 使用率异常
|
||||
- 函数调用热点分析
|
||||
- 性能回归检测
|
||||
|
||||
---
|
||||
|
||||
### 场景 2: 查看进程指标趋势
|
||||
|
||||
**目标**: 监控内存、I/O、FD 等进程指标的趋势
|
||||
|
||||
**步骤**:
|
||||
1. 打开 http://localhost:3000 (Grafana)
|
||||
2. 登录(admin / admin)
|
||||
3. 查找 "Agent Runner 进程监控" Dashboard
|
||||
4. 选择 `project_id`、`instance`、`process_name` 过滤
|
||||
5. 查看各面板数据
|
||||
|
||||
**可用面板**:
|
||||
- 概览: RSS/VSZ 内存、CPU 使用率、文件描述符
|
||||
- 内存趋势: RSS 和 VSZ 的时间序列图
|
||||
- I/O 监控: 读取/写入速率
|
||||
- 上下文切换: 自愿/非自愿切换速率
|
||||
- 线程详情: 线程数量、FD 使用率
|
||||
- 缺页错误: 次要/主要缺页错误速率
|
||||
|
||||
---
|
||||
|
||||
### 场景 3: 分析进程阻塞问题
|
||||
|
||||
**目标**: 找出进程为什么被阻塞(等待 I/O、锁等)
|
||||
|
||||
**步骤**:
|
||||
1. 等待 offcpu-monitor 自动生成火焰图(或手动触发)
|
||||
2. 导出 SVG 文件到本地
|
||||
3. 在浏览器中打开火焰图
|
||||
4. 查找宽的阻塞堆栈
|
||||
|
||||
```bash
|
||||
# 导出最新的 Off-CPU 火焰图
|
||||
docker cp <container>:/app/container-logs/diag/offcpu-*.svg ./
|
||||
open offcpu-*.svg
|
||||
```
|
||||
|
||||
**适用问题**:
|
||||
- `new_session` 超时
|
||||
- 进程响应缓慢
|
||||
- I/O 阻塞
|
||||
- 锁竞争
|
||||
|
||||
---
|
||||
|
||||
### 场景 4: 分析系统调用模式
|
||||
|
||||
**目标**: 了解进程的系统调用行为,找出系统调用热点
|
||||
|
||||
**步骤**:
|
||||
1. 查看系统调用统计日志
|
||||
2. 分析哪些系统调用最频繁
|
||||
3. 查看 execsnoop/opensnoop 日志了解进程和文件访问
|
||||
|
||||
```bash
|
||||
# 查看系统调用统计
|
||||
docker exec <container> cat /app/container-logs/diag/syscall-count-*.txt | head -20
|
||||
|
||||
# 查看进程创建日志
|
||||
docker exec <container> tail -f /app/container-logs/diag/execsnoop-*.log
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 场景 5: 手动诊断已知问题
|
||||
|
||||
```bash
|
||||
# 进入容器
|
||||
docker exec -it <container> bash
|
||||
|
||||
# 诊断 agent_runner
|
||||
e-all $(pgrep agent_runner)
|
||||
|
||||
# 导出结果
|
||||
docker cp <container>:/app/container-logs/diag ./diag-results
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📈 火焰图分析
|
||||
|
||||
### CPU 火焰图(Alloy + Pyroscope)
|
||||
|
||||
**如何阅读**:
|
||||
- **横轴**: CPU 时间占比(越宽表示占用越多)
|
||||
- **纵轴**: 调用堆栈(从上到下是调用关系)
|
||||
- **颜色**: 暖色调表示热点函数
|
||||
|
||||
**典型问题识别**:
|
||||
| 现象 | 可能原因 |
|
||||
|------|----------|
|
||||
| 某个函数占据大部分宽度 | CPU 密集型计算 |
|
||||
| 深层函数很宽 | 递归调用或深层嵌套 |
|
||||
| 出现 `syscall` 大量时间 | 系统调用开销 |
|
||||
| 出现 `sleep`/`usleep` | 主动休眠或等待 |
|
||||
|
||||
### Off-CPU 火焰图(offcputime-bpfcc)
|
||||
|
||||
**如何阅读**:
|
||||
- **横轴**: 阻塞时间占比(越宽表示阻塞越久)
|
||||
- **纵轴**: 阻塞时的调用堆栈
|
||||
- **颜色**: 暖色调表示阻塞热点
|
||||
|
||||
**典型问题识别**:
|
||||
| 现象 | 可能原因 |
|
||||
|------|----------|
|
||||
| `schedule()` 占据大量时间 | 进程被调度出去(CPU 竞争) |
|
||||
| `do_wait()`/`wait_event()` | 等待事件或信号 |
|
||||
| `__sock_sendmsg()`/`__sock_recvmsg()` | 网络 I/O 阻塞 |
|
||||
| `blk_mq_submit_bio()` | 磁盘 I/O 阻塞 |
|
||||
| `futex_wait()` | 锁等待(互斥锁) |
|
||||
|
||||
---
|
||||
|
||||
## 🔍 故障排查
|
||||
|
||||
### Grafana Dashboard 显示 "No Data"
|
||||
|
||||
```bash
|
||||
# 1. 检查 Prometheus 中是否有数据
|
||||
curl -s 'http://localhost:9091/api/v1/query?query=process_resident_memory_bytes'
|
||||
|
||||
# 2. 检查标签值
|
||||
curl -s 'http://localhost:9091/api/v1/label/project_id/values'
|
||||
|
||||
# 3. 确认 agent_runner 容器正在运行
|
||||
docker ps | grep agent_runner
|
||||
```
|
||||
|
||||
### 变量下拉框为空
|
||||
|
||||
```bash
|
||||
# 检查标签值是否存在
|
||||
curl -s 'http://localhost:9091/api/v1/label/project_id/values'
|
||||
|
||||
# 如果没有数据,说明没有 agent_runner 容器在运行
|
||||
# 启动一个 agent_runner 容器后再检查
|
||||
```
|
||||
|
||||
### Pyroscope Web UI 无数据
|
||||
|
||||
```bash
|
||||
# 1. 检查 Pyroscope Server
|
||||
docker ps | grep pyroscope
|
||||
docker logs rcoder-pyroscope
|
||||
|
||||
# 2. 检查 Alloy 是否发送数据
|
||||
docker exec <container> grep "collected profiles" /app/container-logs/diag/alloy.log
|
||||
|
||||
# 3. 检查网络连接
|
||||
docker exec <container> curl http://pyroscope:4040
|
||||
```
|
||||
|
||||
### Off-CPU 火焰图未生成
|
||||
|
||||
```bash
|
||||
# 1. 检查 offcputime-bpfcc 是否可用
|
||||
docker exec <container> which offcputime-bpfcc
|
||||
|
||||
# 2. 检查监控进程
|
||||
docker exec <container> ps aux | grep offcpu-monitor
|
||||
|
||||
# 3. 查看监控日志
|
||||
docker exec <container> tail -f /app/container-logs/diag/offcpu-monitor.log
|
||||
```
|
||||
|
||||
### 系统调用监控无输出
|
||||
|
||||
```bash
|
||||
# 1. 检查 syscount-bpfcc 是否可用
|
||||
docker exec <container> which syscount-bpfcc
|
||||
|
||||
# 2. 检查监控脚本是否运行
|
||||
docker exec <container> ps aux | grep syscall-monitor
|
||||
|
||||
# 3. 查看监控日志
|
||||
docker exec <container> tail -f /app/container-logs/diag/syscall-monitor.log
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ⚠️ 安全注意事项
|
||||
|
||||
⚠️ **eBPF 工具需要容器特权模式运行**,仅在受信任的调试环境使用!
|
||||
|
||||
生产环境请使用 `make docker-build-agent-production` 构建无 eBPF 工具的镜像。
|
||||
|
||||
---
|
||||
|
||||
## 📚 相关文档
|
||||
|
||||
- [Grafana Alloy Documentation](https://grafana.com/docs/alloy/latest/)
|
||||
- [Pyroscope Documentation](https://pyroscope.io/docs/)
|
||||
- [Prometheus Documentation](https://prometheus.io/docs/)
|
||||
- [Grafana Documentation](https://grafana.com/docs/)
|
||||
- [Brendan Gregg's FlameGraph](https://github.com/brendangregg/FlameGraph)
|
||||
- [bpftrace 参考指南](https://bpftrace.dev/)
|
||||
- 主项目文档: `/docker/README.md`
|
||||
@@ -0,0 +1,142 @@
|
||||
// ============================================================================
|
||||
// Grafana Alloy 配置文件 - 用于监控 agent_runner 进程
|
||||
// ============================================================================
|
||||
// 此配置文件实现:
|
||||
// 1. 自动发现 agent_runner 及其子进程
|
||||
// 2. 使用 eBPF 进行 CPU 性能剖析
|
||||
// 3. 将性能数据发送到 Pyroscope Server
|
||||
// ============================================================================
|
||||
|
||||
// ============================================================================
|
||||
// 进程发现 - 发现所有运行中的进程
|
||||
// ============================================================================
|
||||
discovery.process "all" {
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// 进程过滤 - 只保留 agent_runner 相关进程
|
||||
// ============================================================================
|
||||
discovery.relabel "agent_runner_filter" {
|
||||
targets = discovery.process.all.targets
|
||||
|
||||
// 规则 1: 保留 agent_runner 主进程及其所有子进程
|
||||
// 匹配条件:进程名是 agent_runner 或者父进程 PID 是 1
|
||||
rule {
|
||||
source_labels = ["__meta_process_exe", "__meta_process_ppid"]
|
||||
regex = ".*/agent_runner|;1"
|
||||
action = "keep"
|
||||
}
|
||||
|
||||
// 规则 2: 添加进程 PID 标签
|
||||
rule {
|
||||
source_labels = ["__meta_process_pid"]
|
||||
target_label = "process_pid"
|
||||
}
|
||||
|
||||
// 规则 3: 添加进程名称标签
|
||||
rule {
|
||||
source_labels = ["__meta_process_comm"]
|
||||
target_label = "process_name"
|
||||
}
|
||||
|
||||
// 规则 4: 添加完整执行路径标签
|
||||
rule {
|
||||
source_labels = ["__meta_process_exe"]
|
||||
target_label = "process_exe"
|
||||
}
|
||||
|
||||
// 规则 5: 添加父进程 PID 标签
|
||||
rule {
|
||||
source_labels = ["__meta_process_ppid"]
|
||||
target_label = "parent_pid"
|
||||
}
|
||||
|
||||
// 规则 6: 添加命令行参数标签
|
||||
rule {
|
||||
source_labels = ["__meta_process_cmdline"]
|
||||
target_label = "process_cmdline"
|
||||
}
|
||||
|
||||
// 规则 7: 添加环境标签
|
||||
rule {
|
||||
target_label = "env"
|
||||
replacement = "dev"
|
||||
}
|
||||
|
||||
// 规则 8: 添加容器 ID 标签(从环境变量动态组合)
|
||||
rule {
|
||||
target_label = "container_id"
|
||||
replacement = "agent-${env:USER_ID}"
|
||||
}
|
||||
|
||||
// 规则 9: 添加应用名称标签
|
||||
rule {
|
||||
target_label = "job"
|
||||
replacement = "agent_runner"
|
||||
}
|
||||
|
||||
// 规则 10: 添加实例标签(从环境变量动态组合)
|
||||
rule {
|
||||
target_label = "instance"
|
||||
replacement = "agent-${env:USER_ID}"
|
||||
}
|
||||
|
||||
// 添加集群标签
|
||||
rule {
|
||||
target_label = "cluster"
|
||||
replacement = "rcoder"
|
||||
}
|
||||
|
||||
// 添加数据中心标签
|
||||
rule {
|
||||
target_label = "datacenter"
|
||||
replacement = "local"
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// 写入 Pyroscope Server
|
||||
// ============================================================================
|
||||
pyroscope.write "remote" {
|
||||
endpoint {
|
||||
url = "http://pyroscope:4040"
|
||||
}
|
||||
|
||||
external_labels = {
|
||||
"env" = "dev",
|
||||
"datacenter" = "local",
|
||||
"cluster" = "rcoder",
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// eBPF 性能分析组件
|
||||
// ============================================================================
|
||||
pyroscope.ebpf "agent_runner" {
|
||||
forward_to = [pyroscope.write.remote.receiver]
|
||||
|
||||
targets = discovery.relabel.agent_runner_filter.output
|
||||
|
||||
sample_rate = 97
|
||||
collect_interval = "15s"
|
||||
|
||||
collect_user_profile = true
|
||||
collect_kernel_profile = false
|
||||
|
||||
python_enabled = true
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Prometheus Remote Write
|
||||
// ============================================================================
|
||||
prometheus.remote_write "metrics" {
|
||||
endpoint {
|
||||
url = "http://prometheus:9090/api/v1/write"
|
||||
}
|
||||
|
||||
external_labels = {
|
||||
"env" = "dev",
|
||||
"datacenter" = "local",
|
||||
"cluster" = "rcoder",
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,240 @@
|
||||
#!/bin/bash
|
||||
# eBPF 自动持续火焰图生成脚本
|
||||
# 以 agent_runner 为入口,监控其所有子进程,定期生成火焰图
|
||||
|
||||
set -e
|
||||
|
||||
DIAG_OUTPUT_DIR="${DIAG_OUTPUT_DIR:-/app/container-logs/diag}"
|
||||
MONITOR_LOG="$DIAG_OUTPUT_DIR/auto-flamegraph.log"
|
||||
SAMPLE_DURATION=${SAMPLE_DURATION:-30} # 每次采样时长(秒),默认 30 秒
|
||||
GENERATE_INTERVAL=${GENERATE_INTERVAL:-60} # 生成火焰图间隔(秒),默认 60 秒
|
||||
MAX_FLAMEFILES=${MAX_FLAMEFILES:-50} # 最多保留火焰图文件数量
|
||||
|
||||
mkdir -p "$DIAG_OUTPUT_DIR"
|
||||
|
||||
log() {
|
||||
echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*" | tee -a "$MONITOR_LOG"
|
||||
}
|
||||
|
||||
# 获取 agent_runner 的 PID
|
||||
get_agent_runner_pid() {
|
||||
pgrep -f "agent_runner" | head -1
|
||||
}
|
||||
|
||||
# 获取 agent_runner 及其所有子进程的 PID 列表
|
||||
get_process_tree_pids() {
|
||||
local parent_pid=$1
|
||||
if [ -z "$parent_pid" ]; then
|
||||
return
|
||||
fi
|
||||
|
||||
# 输出父进程 PID
|
||||
echo "$parent_pid"
|
||||
|
||||
# 递归获取所有子进程
|
||||
local child_pids=$(pgrep -P "$parent_pid" 2>/dev/null || true)
|
||||
for child_pid in $child_pids; do
|
||||
get_process_tree_pids "$child_pid"
|
||||
done
|
||||
}
|
||||
|
||||
# 生成火焰图
|
||||
generate_flamegraph() {
|
||||
local timestamp=$(date '+%Y%m%d_%H%M%S')
|
||||
local bpftrace_data="$DIAG_OUTPUT_DIR/profile-${timestamp}.bt"
|
||||
local folded_output="$DIAG_OUTPUT_DIR/profile-${timestamp}.folded"
|
||||
local flamegraph_svg="$DIAG_OUTPUT_DIR/flamegraph-${timestamp}.svg"
|
||||
local agent_pid=$1
|
||||
|
||||
log "🔥 开始生成火焰图 (agent_runner PID: $agent_pid)..."
|
||||
|
||||
# 获取进程树 PID 列表
|
||||
local pids=$(get_process_tree_pids "$agent_pid" | tr '\n' ' ')
|
||||
log "📋 监控进程树: $pids"
|
||||
|
||||
# 构建 bpftrace 脚本,监控所有相关进程
|
||||
local pid_filter=""
|
||||
for pid in $pids; do
|
||||
if [ -n "$pid_filter" ]; then
|
||||
pid_filter="$pid_filter || "
|
||||
fi
|
||||
pid_filter="${pid_filter}pid == $pid"
|
||||
done
|
||||
|
||||
log "📊 采样 ${SAMPLE_DURATION} 秒..."
|
||||
|
||||
# 使用 bpftrace 采样(后台运行)
|
||||
timeout ${SAMPLE_DURATION}s bpftrace -e "
|
||||
profile:hz:99 /($pid_filter) && comm != \"bpftrace\"/ {
|
||||
@[ustack] = count();
|
||||
}
|
||||
" 2>/dev/null | sort -rn -k2 > "$bpftrace_data" || true
|
||||
|
||||
if [ ! -s "$bpftrace_data" ]; then
|
||||
log "⚠️ 未采集到性能数据(进程可能已结束)"
|
||||
rm -f "$bpftrace_data"
|
||||
return 1
|
||||
fi
|
||||
|
||||
log "📊 采样完成,共 $(wc -l < "$bpftrace_data") 个堆栈样本"
|
||||
|
||||
# 调试:保存原始数据样本(前 10 行)
|
||||
head -10 "$bpftrace_data" > "$DIAG_OUTPUT_DIR/debug-bpftrace-sample.txt"
|
||||
log "📋 原始数据样本已保存到: $DIAG_OUTPUT_DIR/debug-bpftrace-sample.txt"
|
||||
|
||||
# 转换为 FlameGraph 格式
|
||||
log "🔄 转换为火焰图格式..."
|
||||
|
||||
# bpftrace 实际输出格式(多行):
|
||||
# ]: 10
|
||||
# Attaching 1 probe...
|
||||
# @[
|
||||
# 0x634c5c
|
||||
# 0x634758
|
||||
# ...
|
||||
#
|
||||
# 使用 awk 解析多行格式
|
||||
awk '
|
||||
BEGIN {
|
||||
count = 0
|
||||
in_stack = 0
|
||||
stack = ""
|
||||
}
|
||||
/^]:/ {
|
||||
# 提取计数值: ]: 10
|
||||
count = $2
|
||||
next
|
||||
}
|
||||
/^\@\[/ {
|
||||
# 开始新的堆栈
|
||||
in_stack = 1
|
||||
stack = ""
|
||||
next
|
||||
}
|
||||
/^$/ {
|
||||
# 空行结束当前堆栈
|
||||
if (in_stack && stack != "" && count > 0) {
|
||||
# 移除末尾分号并输出
|
||||
gsub(/;$/, "", stack)
|
||||
print stack " " count
|
||||
}
|
||||
in_stack = 0
|
||||
stack = ""
|
||||
count = 0
|
||||
next
|
||||
}
|
||||
{
|
||||
# 跳过 Attaching 消息等非堆栈行
|
||||
if (in_stack && $0 !~ /^Attaching/) {
|
||||
# 提取地址(缩进或未缩进的十六进制地址)
|
||||
if (match($0, /0x[0-9a-f]+/)) {
|
||||
addr = substr($0, RSTART, RLENGTH)
|
||||
if (stack != "") {
|
||||
stack = stack ";"
|
||||
}
|
||||
stack = stack addr
|
||||
}
|
||||
}
|
||||
}
|
||||
END {
|
||||
# 处理最后一个堆栈
|
||||
if (in_stack && stack != "" && count > 0) {
|
||||
gsub(/;$/, "", stack)
|
||||
print stack " " count
|
||||
}
|
||||
}
|
||||
' "$bpftrace_data" > "$folded_output"
|
||||
|
||||
# 检查转换结果
|
||||
if [ ! -s "$folded_output" ]; then
|
||||
log "⚠️ 火焰图格式转换失败,保存原始数据用于调试"
|
||||
cp "$bpftrace_data" "$DIAG_OUTPUT_DIR/debug-${timestamp}.bt"
|
||||
log "📋 请检查以下文件以诊断问题:"
|
||||
log " - 原始数据: $DIAG_OUTPUT_DIR/debug-${timestamp}.bt"
|
||||
log " - 数据样本: $DIAG_OUTPUT_DIR/debug-bpftrace-sample.txt"
|
||||
return 1
|
||||
fi
|
||||
|
||||
log "📊 转换完成,共 $(wc -l < "$folded_output") 个有效堆栈"
|
||||
|
||||
# 调试:保存转换后的样本(前 5 行)
|
||||
head -5 "$folded_output" > "$DIAG_OUTPUT_DIR/debug-folded-sample.txt"
|
||||
log "📋 转换后样本已保存到: $DIAG_OUTPUT_DIR/debug-folded-sample.txt"
|
||||
|
||||
# 生成火焰图
|
||||
log "🎨 生成 SVG 火焰图..."
|
||||
if command -v flamegraph.pl &> /dev/null; then
|
||||
flamegraph.pl \
|
||||
--title="agent_runner 进程树火焰图 (${timestamp})" \
|
||||
--width=1600 \
|
||||
--height=800 \
|
||||
"$folded_output" > "$flamegraph_svg"
|
||||
|
||||
if [ -s "$flamegraph_svg" ]; then
|
||||
log "✅ 火焰图已生成: $flamegraph_svg"
|
||||
log "💡 复制到宿主机查看: docker cp <container>:$flamegraph_svg ./"
|
||||
|
||||
# 清理临时文件
|
||||
rm -f "$bpftrace_data" "$folded_output"
|
||||
|
||||
# 清理旧火焰图文件(保留最新的 MAX_FLAMEFILES 个)
|
||||
cleanup_old_flamegraphs
|
||||
return 0
|
||||
else
|
||||
log "❌ 火焰图生成失败"
|
||||
return 1
|
||||
fi
|
||||
else
|
||||
log "❌ flamegraph.pl 未安装,无法生成火焰图"
|
||||
log "📋 原始数据保存在: $bpftrace_data"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
# 清理旧火焰图文件
|
||||
cleanup_old_flamegraphs() {
|
||||
local flame_count=$(ls -1 "$DIAG_OUTPUT_DIR"/flamegraph-*.svg 2>/dev/null | wc -l)
|
||||
|
||||
if [ "$flame_count" -gt "$MAX_FLAMEFILES" ]; then
|
||||
local delete_count=$((flame_count - MAX_FLAMEFILES))
|
||||
log "🗑️ 清理 ${delete_count} 个旧火焰图文件..."
|
||||
|
||||
ls -1t "$DIAG_OUTPUT_DIR"/flamegraph-*.svg | tail -n "$delete_count" | while read -r file; do
|
||||
log " 删除: $(basename "$file")"
|
||||
rm -f "$file"
|
||||
done
|
||||
fi
|
||||
}
|
||||
|
||||
# 主监控循环
|
||||
monitor_loop() {
|
||||
log "🚀 eBPF 自动火焰图生成已启动"
|
||||
log "📋 配置: 采样时长=${SAMPLE_DURATION}s, 生成间隔=${GENERATE_INTERVAL}s"
|
||||
log "💡 火焰图将保存到: $DIAG_OUTPUT_DIR/flamegraph-*.svg"
|
||||
|
||||
while true; do
|
||||
# 获取 agent_runner PID
|
||||
local agent_pid=$(get_agent_runner_pid)
|
||||
|
||||
if [ -n "$agent_pid" ]; then
|
||||
log "✅ 检测到 agent_runner 进程 (PID: $agent_pid)"
|
||||
generate_flamegraph "$agent_pid"
|
||||
else
|
||||
log "⚠️ 未检测到 agent_runner 进程,等待中..."
|
||||
fi
|
||||
|
||||
log "⏰ ${GENERATE_INTERVAL} 秒后生成下一张火焰图..."
|
||||
sleep "$GENERATE_INTERVAL"
|
||||
done
|
||||
}
|
||||
|
||||
# 启动监控
|
||||
case "$1" in
|
||||
start)
|
||||
monitor_loop
|
||||
;;
|
||||
*)
|
||||
echo "用法: $0 start"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
255
qiming-rcoder/docker/rcoder-agent-runner/ebpf-tools/diag-tool.sh
Normal file
255
qiming-rcoder/docker/rcoder-agent-runner/ebpf-tools/diag-tool.sh
Normal file
@@ -0,0 +1,255 @@
|
||||
#!/bin/bash
|
||||
# eBPF 诊断工具快捷脚本
|
||||
# 使用方法: diag-tool.sh {offcpu|flame|profile|all} <pid> [duration]
|
||||
|
||||
DIAG_OUTPUT_DIR="${DIAG_OUTPUT_DIR:-/app/container-logs/diag}"
|
||||
mkdir -p "$DIAG_OUTPUT_DIR"
|
||||
|
||||
log_info() {
|
||||
echo "$(date '+%Y-%m-%d %H:%M:%S') INFO ℹ️ $*"
|
||||
}
|
||||
|
||||
log_success() {
|
||||
echo "$(date '+%Y-%m-%d %H:%M:%S') INFO ✓ $*"
|
||||
}
|
||||
|
||||
log_error() {
|
||||
echo "$(date '+%Y-%m-%d %H:%M:%S') ERROR ❌ $*"
|
||||
}
|
||||
|
||||
# 检查 eBPF 工具是否可用
|
||||
check_ebpf_tools() {
|
||||
if ! command -v bpftrace &> /dev/null; then
|
||||
log_error "bpftrace 未安装,请检查 Dockerfile 中 INSTALL_EBPF_TOOLS 参数"
|
||||
return 1
|
||||
fi
|
||||
return 0
|
||||
}
|
||||
|
||||
# off-cpu 分析 - 定位进程阻塞位置
|
||||
diag_offcpu() {
|
||||
local pid=$1
|
||||
local duration=${2:-30}
|
||||
|
||||
if [ -z "$pid" ]; then
|
||||
log_error "缺少 PID 参数"
|
||||
return 1
|
||||
fi
|
||||
|
||||
if ! kill -0 "$pid" 2>/dev/null; then
|
||||
log_error "进程 $pid 不存在"
|
||||
return 1
|
||||
fi
|
||||
|
||||
check_ebpf_tools || return 1
|
||||
|
||||
log_info "分析进程 $pid 的 CPU 性能堆栈 (${duration}s)..."
|
||||
log_info "输出文件: $DIAG_OUTPUT_DIR/offcpu-${pid}.txt"
|
||||
log_info "提示: 这将显示进程在哪些函数上花费最多 CPU 时间"
|
||||
log_info "注意: 如果进程 CPU 使用率低,可能需要更长的采样时间"
|
||||
|
||||
# 使用 bpftrace 进行 CPU 性能分析(后台运行)
|
||||
timeout ${duration}s bpftrace -e "profile:hz:99 /pid == $pid/ && comm != \"bpftrace\"/ { @[ustack] = count(); }" \
|
||||
2>/dev/null > "$DIAG_OUTPUT_DIR/offcpu-${pid}.bpftrace" &
|
||||
local bpftrace_pid=$!
|
||||
|
||||
# 等待采样完成
|
||||
sleep $duration
|
||||
wait $bpftrace_pid 2>/dev/null
|
||||
|
||||
# 处理输出
|
||||
if [ -s "$DIAG_OUTPUT_DIR/offcpu-${pid}.bpftrace" ]; then
|
||||
cat "$DIAG_OUTPUT_DIR/offcpu-${pid}.bpftrace" | sort -rn -k2 | head -50 > "$DIAG_OUTPUT_DIR/offcpu-${pid}.txt"
|
||||
log_success "性能分析完成: $DIAG_OUTPUT_DIR/offcpu-${pid}.txt"
|
||||
echo ""
|
||||
echo "📊 Top 10 CPU 消耗堆栈:"
|
||||
head -10 "$DIAG_OUTPUT_DIR/offcpu-${pid}.txt"
|
||||
else
|
||||
log_error "性能分析失败(未收集到数据,进程可能空闲或采样时间过短)"
|
||||
log_info "建议: 使用更长的采样时间(如 60 秒)或在进程高负载时分析"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
# 生成火焰图
|
||||
diag_flame() {
|
||||
local pid=$1
|
||||
local duration=${2:-30}
|
||||
|
||||
if [ -z "$pid" ]; then
|
||||
log_error "缺少 PID 参数"
|
||||
return 1
|
||||
fi
|
||||
|
||||
if ! kill -0 "$pid" 2>/dev/null; then
|
||||
log_error "进程 $pid 不存在"
|
||||
return 1
|
||||
fi
|
||||
|
||||
check_ebpf_tools || return 1
|
||||
|
||||
# 检查 FlameGraph 工具
|
||||
if ! command -v flamegraph.pl &> /dev/null; then
|
||||
log_error "flamegraph.pl 未安装,请检查 FlameGraph 工具安装"
|
||||
return 1
|
||||
fi
|
||||
|
||||
if ! command -v stackcollapse-perf.pl &> /dev/null; then
|
||||
log_error "stackcollapse-perf.pl 未安装,请检查 FlameGraph 工具安装"
|
||||
return 1
|
||||
fi
|
||||
|
||||
log_info "生成进程 $pid 的火焰图 (${duration}s)..."
|
||||
log_info "输出文件: $DIAG_OUTPUT_DIR/flame-${pid}.svg"
|
||||
log_info "提示: 火焰图可直观显示 CPU 性能瓶颈"
|
||||
|
||||
# 使用 bpftrace 收集数据到临时文件(后台运行)
|
||||
timeout ${duration}s bpftrace -e "profile:hz:99 /pid == $pid/ && comm != \"bpftrace\"/ { @[ustack] = count(); }" \
|
||||
2>/dev/null > "$DIAG_OUTPUT_DIR/flame-${pid}.bpftrace" &
|
||||
local bpftrace_pid=$!
|
||||
|
||||
# 等待采样完成
|
||||
sleep $duration
|
||||
wait $bpftrace_pid 2>/dev/null
|
||||
|
||||
# 处理数据生成火焰图
|
||||
if [ -s "$DIAG_OUTPUT_DIR/flame-${pid}.bpftrace" ]; then
|
||||
cat "$DIAG_OUTPUT_DIR/flame-${pid}.bpftrace" | \
|
||||
stackcollapse-perf.pl 2>/dev/null | \
|
||||
flamegraph.pl > "$DIAG_OUTPUT_DIR/flame-${pid}.svg" 2>/dev/null
|
||||
fi
|
||||
|
||||
if [ $? -eq 0 ] && [ -s "$DIAG_OUTPUT_DIR/flame-${pid}.svg" ]; then
|
||||
log_success "火焰图生成完成: $DIAG_OUTPUT_DIR/flame-${pid}.svg"
|
||||
log_info "将 SVG 文件复制到宿主机查看: docker cp <container>:$DIAG_OUTPUT_DIR/flame-${pid}.svg ./"
|
||||
else
|
||||
log_error "火焰图生成失败(未收集到数据,进程可能空闲或采样时间过短)"
|
||||
log_info "建议: 使用更长的采样时间(如 60 秒)或在进程高负载时分析"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
# CPU 性能分析
|
||||
diag_profile() {
|
||||
local pid=$1
|
||||
local duration=${2:-30}
|
||||
|
||||
if [ -z "$pid" ]; then
|
||||
log_error "缺少 PID 参数"
|
||||
return 1
|
||||
fi
|
||||
|
||||
if ! kill -0 "$pid" 2>/dev/null; then
|
||||
log_error "进程 $pid 不存在"
|
||||
return 1
|
||||
fi
|
||||
|
||||
log_info "分析进程 $pid 的 CPU 性能 (${duration}s)..."
|
||||
log_info "输出文件: $DIAG_OUTPUT_DIR/profile-${pid}.txt"
|
||||
|
||||
# 使用 bpftrace 进行性能分析(后台运行)
|
||||
timeout ${duration}s bpftrace -e "profile:hz:99 /pid == $pid/ && comm != \"bpftrace\"/ { @[ustack] = count(); }" \
|
||||
2>/dev/null > "$DIAG_OUTPUT_DIR/profile-${pid}.bpftrace" &
|
||||
local bpftrace_pid=$!
|
||||
|
||||
# 等待采样完成
|
||||
sleep $duration
|
||||
wait $bpftrace_pid 2>/dev/null
|
||||
|
||||
# 处理输出
|
||||
if [ -s "$DIAG_OUTPUT_DIR/profile-${pid}.bpftrace" ]; then
|
||||
cat "$DIAG_OUTPUT_DIR/profile-${pid}.bpftrace" > "$DIAG_OUTPUT_DIR/profile-${pid}.txt"
|
||||
log_success "性能分析完成: $DIAG_OUTPUT_DIR/profile-${pid}.txt"
|
||||
else
|
||||
log_error "性能分析失败(未收集到数据,进程可能空闲或采样时间过短)"
|
||||
log_info "建议: 使用更长的采样时间(如 60 秒)或在进程高负载时分析"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
# 综合诊断
|
||||
diag_all() {
|
||||
local pid=$1
|
||||
|
||||
if [ -z "$pid" ]; then
|
||||
log_error "缺少 PID 参数"
|
||||
return 1
|
||||
fi
|
||||
|
||||
if ! kill -0 "$pid" 2>/dev/null; then
|
||||
log_error "进程 $pid 不存在"
|
||||
return 1
|
||||
fi
|
||||
|
||||
log_info "综合诊断进程 $pid..."
|
||||
mkdir -p "$DIAG_OUTPUT_DIR/all-${pid}"
|
||||
|
||||
# 保存当前输出目录
|
||||
local old_output_dir="$DIAG_OUTPUT_DIR"
|
||||
export DIAG_OUTPUT_DIR="$DIAG_OUTPUT_DIR/all-${pid}"
|
||||
|
||||
# 执行各项诊断
|
||||
diag_offcpu $pid 30
|
||||
diag_flame $pid 30
|
||||
diag_profile $pid 30
|
||||
|
||||
# 恢复输出目录
|
||||
export DIAG_OUTPUT_DIR="$old_output_dir"
|
||||
|
||||
log_success "诊断完成,结果保存在: $DIAG_OUTPUT_DIR/all-${pid}/"
|
||||
echo ""
|
||||
echo "📊 诊断结果:"
|
||||
echo " - off-cpu 堆栈: $DIAG_OUTPUT_DIR/all-${pid}/offcpu-${pid}.txt"
|
||||
echo " - 火焰图: $DIAG_OUTPUT_DIR/all-${pid}/flame-${pid}.svg"
|
||||
echo " - CPU 性能: $DIAG_OUTPUT_DIR/all-${pid}/profile-${pid}.txt"
|
||||
echo ""
|
||||
echo "💡 导出所有诊断数据:"
|
||||
echo " docker cp <container>:$DIAG_OUTPUT_DIR/all-${pid} ./diag-results"
|
||||
}
|
||||
|
||||
# 显示用法
|
||||
show_usage() {
|
||||
echo "eBPF 诊断工具"
|
||||
echo ""
|
||||
echo "用法: $0 {offcpu|flame|profile|all} <pid> [duration]"
|
||||
echo ""
|
||||
echo "命令:"
|
||||
echo " offcpu <pid> [duration] - 分析 off-cpu 堆栈(定位阻塞位置),默认 30 秒"
|
||||
echo " flame <pid> [duration] - 生成火焰图,默认 30 秒"
|
||||
echo " profile <pid> [duration] - CPU 性能分析,默认 30 秒"
|
||||
echo " all <pid> - 综合诊断(包含所有分析)"
|
||||
echo ""
|
||||
echo "示例:"
|
||||
echo " $0 offcpu \$(pgrep agent_runner) # 分析 agent_runner 阻塞位置"
|
||||
echo " $0 flame \$(pgrep agent_runner) 60 # 生成 60 秒火焰图"
|
||||
echo " $0 all \$(pgrep agent_runner) # 综合诊断"
|
||||
echo ""
|
||||
echo "输出目录: $DIAG_OUTPUT_DIR"
|
||||
echo ""
|
||||
echo "环境变量:"
|
||||
echo " DIAG_OUTPUT_DIR - 自定义输出目录(默认: /app/container-logs/diag)"
|
||||
echo ""
|
||||
echo "快捷命令:"
|
||||
echo " e-offcpu <pid> - 等同于 diag-tool.sh offcpu"
|
||||
echo " e-flame <pid> - 等同于 diag-tool.sh flame"
|
||||
echo " e-profile <pid> - 等同于 diag-tool.sh profile"
|
||||
echo " e-all <pid> - 等同于 diag-tool.sh all"
|
||||
}
|
||||
|
||||
case "$1" in
|
||||
offcpu)
|
||||
diag_offcpu "$2" "${3:-30}"
|
||||
;;
|
||||
flame)
|
||||
diag_flame "$2" "${3:-30}"
|
||||
;;
|
||||
profile)
|
||||
diag_profile "$2" "${3:-30}"
|
||||
;;
|
||||
all)
|
||||
diag_all "$2"
|
||||
;;
|
||||
*)
|
||||
show_usage
|
||||
;;
|
||||
esac
|
||||
@@ -0,0 +1,120 @@
|
||||
#!/bin/bash
|
||||
# offcputime 自动监控脚本
|
||||
# 专门捕获进程阻塞点堆栈
|
||||
|
||||
set -e
|
||||
|
||||
DIAG_OUTPUT_DIR="${DIAG_OUTPUT_DIR:-/app/container-logs/diag}"
|
||||
OFFCPU_LOG="$DIAG_OUTPUT_DIR/offcpu-monitor.log"
|
||||
SAMPLE_DURATION=${OFFCPU_DURATION:-30} # 每次采样时长
|
||||
GENERATE_INTERVAL=${OFFCPU_INTERVAL:-60} # 生成间隔(秒,默认 1 分钟)
|
||||
MAX_OFFCPU_FILES=${MAX_OFFCPU_FILES:-50} # 最多保留火焰图文件数量
|
||||
|
||||
mkdir -p "$DIAG_OUTPUT_DIR"
|
||||
|
||||
log() {
|
||||
echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*" | tee -a "$OFFCPU_LOG"
|
||||
}
|
||||
|
||||
# 静默日志(只写入文件,不输出到控制台)
|
||||
log_silent() {
|
||||
echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*" >> "$OFFCPU_LOG"
|
||||
}
|
||||
|
||||
# 获取进程树 PID
|
||||
get_process_tree_pids() {
|
||||
local parent_pid=$1
|
||||
if [ -z "$parent_pid" ]; then
|
||||
return
|
||||
fi
|
||||
|
||||
echo "$parent_pid"
|
||||
|
||||
local child_pids=$(pgrep -P "$parent_pid" 2>/dev/null || true)
|
||||
for child_pid in $child_pids; do
|
||||
get_process_tree_pids "$child_pid"
|
||||
done
|
||||
}
|
||||
|
||||
# 清理旧火焰图文件
|
||||
cleanup_old_offcpu_files() {
|
||||
local offcpu_count=$(ls -1 "$DIAG_OUTPUT_DIR"/offcpu-*.svg 2>/dev/null | wc -l)
|
||||
|
||||
if [ "$offcpu_count" -gt "$MAX_OFFCPU_FILES" ]; then
|
||||
local delete_count=$((offcpu_count - MAX_OFFCPU_FILES))
|
||||
log_silent "🗑️ 清理 ${delete_count} 个旧火焰图文件..."
|
||||
|
||||
ls -1t "$DIAG_OUTPUT_DIR"/offcpu-*.svg 2>/dev/null | tail -n "$delete_count" | while read -r file; do
|
||||
log_silent " 删除: $(basename "$file")"
|
||||
rm -f "$file"
|
||||
done
|
||||
fi
|
||||
}
|
||||
|
||||
# 生成 off-cpu 火焰图(静默模式)
|
||||
generate_offcpu_flamegraph() {
|
||||
local timestamp=$(date '+%Y%m%d_%H%M%S')
|
||||
local agent_pid=$(pgrep -f "agent_runner" | head -1)
|
||||
|
||||
if [ -z "$agent_pid" ]; then
|
||||
log_silent "⚠️ 未检测到 agent_runner 进程"
|
||||
return 1
|
||||
fi
|
||||
|
||||
# 获取进程树 PID
|
||||
local pids=$(get_process_tree_pids "$agent_pid" | tr '\n' ' ')
|
||||
local pid_count=$(echo $pids | wc -w)
|
||||
local success_count=0
|
||||
|
||||
# 为每个 PID 生成 off-cpu 火焰图
|
||||
for pid in $pids; do
|
||||
local comm=$(ps -p "$pid" -o comm= 2>/dev/null || echo "unknown")
|
||||
local output_file="$DIAG_OUTPUT_DIR/offcpu-${comm}-${pid}-${timestamp}.svg"
|
||||
|
||||
# 使用 offcputime-bpfcc 生成火焰图(完全静默)
|
||||
timeout ${SAMPLE_DURATION}s offcputime-bpfcc \
|
||||
-p "$pid" \
|
||||
-f "$output_file" \
|
||||
--full-stacks 2>/dev/null || true
|
||||
|
||||
if [ -s "$output_file" ]; then
|
||||
((success_count++))
|
||||
# 只记录到文件,不输出到控制台
|
||||
log_silent " ✅ $comm ($pid): 已保存火焰图"
|
||||
else
|
||||
rm -f "$output_file"
|
||||
fi
|
||||
done
|
||||
|
||||
# 清理旧火焰图文件
|
||||
cleanup_old_offcpu_files
|
||||
|
||||
# 只在汇总时输出一行日志
|
||||
log "🔍 Off-CPU 阻塞分析完成: ${success_count}/${pid_count} 个进程"
|
||||
}
|
||||
|
||||
# 主监控循环
|
||||
monitor_loop() {
|
||||
log "🚀 Off-CPU 阻塞监控已启动 (每 ${GENERATE_INTERVAL} 秒采样一次)"
|
||||
log "📝 详细日志: $OFFCPU_LOG"
|
||||
|
||||
local iteration=0
|
||||
while true; do
|
||||
((iteration++))
|
||||
generate_offcpu_flamegraph
|
||||
# 只记录到文件,不输出到控制台
|
||||
log_silent "⏰ 第 ${iteration} 次采样完成,${GENERATE_INTERVAL} 秒后进行下一次..."
|
||||
sleep "$GENERATE_INTERVAL"
|
||||
done
|
||||
}
|
||||
|
||||
# 启动监控
|
||||
case "$1" in
|
||||
start)
|
||||
monitor_loop
|
||||
;;
|
||||
*)
|
||||
echo "用法: $0 start"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
@@ -0,0 +1,137 @@
|
||||
#!/bin/bash
|
||||
# 系统调用监控脚本
|
||||
# 使用 bpfcc-tools 追踪进程的系统调用活动
|
||||
|
||||
set -e
|
||||
|
||||
DIAG_OUTPUT_DIR="${DIAG_OUTPUT_DIR:-/app/container-logs/diag}"
|
||||
SYSLOG="$DIAG_OUTPUT_DIR/syscall-monitor.log"
|
||||
SAMPLE_DURATION=${SAMPLE_DURATION:-30} # 每次采样时长(秒)
|
||||
GENERATE_INTERVAL=${GENERATE_INTERVAL:-60} # 生成间隔(秒,默认 1 分钟)
|
||||
|
||||
mkdir -p "$DIAG_OUTPUT_DIR"
|
||||
|
||||
log() {
|
||||
echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*" | tee -a "$SYSLOG"
|
||||
}
|
||||
|
||||
# 静默日志(只写入文件,不输出到控制台)
|
||||
log_silent() {
|
||||
echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*" >> "$SYSLOG"
|
||||
}
|
||||
|
||||
# 获取进程树 PID
|
||||
get_process_tree_pids() {
|
||||
local parent_pid=$1
|
||||
if [ -z "$parent_pid" ]; then
|
||||
return
|
||||
fi
|
||||
|
||||
echo "$parent_pid"
|
||||
|
||||
local child_pids=$(pgrep -P "$parent_pid" 2>/dev/null || true)
|
||||
for child_pid in $child_pids; do
|
||||
get_process_tree_pids "$child_pid"
|
||||
done
|
||||
}
|
||||
|
||||
# 系统调用计数统计(静默模式)
|
||||
collect_syscall_counts() {
|
||||
local pids="$1"
|
||||
local timestamp=$(date '+%Y%m%d_%H%M%S')
|
||||
local success_count=0
|
||||
local total_count=0
|
||||
|
||||
for pid in $pids; do
|
||||
((total_count++))
|
||||
local comm=$(ps -p "$pid" -o comm= 2>/dev/null || echo "unknown")
|
||||
local output_file="$DIAG_OUTPUT_DIR/syscall-count-${comm}-${pid}-${timestamp}.txt"
|
||||
|
||||
# 使用 syscount-bpfcc 统计系统调用(完全静默)
|
||||
timeout ${SAMPLE_DURATION}s syscount-bpfcc -p "$pid" -s 2>/dev/null > "$output_file" || true
|
||||
|
||||
if [ -s "$output_file" ]; then
|
||||
((success_count++))
|
||||
# 只记录到文件,不输出到控制台
|
||||
log_silent " ✅ $comm ($pid): 已保存统计"
|
||||
else
|
||||
rm -f "$output_file"
|
||||
fi
|
||||
done
|
||||
|
||||
# 只在汇总时输出一行日志
|
||||
log "🔍 系统调用统计完成: ${success_count}/${total_count} 个进程"
|
||||
}
|
||||
|
||||
# 进程创建追踪(静默启动)
|
||||
trace_process_creation() {
|
||||
local output_file="$DIAG_OUTPUT_DIR/execsnoop-$(date '+%Y%m%d_%H%M%S').log"
|
||||
|
||||
# 后台运行 execsnoop-bpfcc(完全静默)
|
||||
execsnoop-bpfcc -t -n 1 > "$output_file" 2>/dev/null &
|
||||
local execsnoop_pid=$!
|
||||
|
||||
log_silent "✅ execsnoop-bpfcc 已启动 (PID: $execsnoop_pid)"
|
||||
echo "$execsnoop_pid"
|
||||
}
|
||||
|
||||
# 文件访问追踪(静默启动)
|
||||
trace_file_access() {
|
||||
local output_file="$DIAG_OUTPUT_DIR/opensnoop-$(date '+%Y%m%d_%H%M%S').log"
|
||||
|
||||
# 后台运行 opensnoop-bpfcc(完全静默)
|
||||
opensnoop-bpfcc -t -n 1 > "$output_file" 2>/dev/null &
|
||||
local opensnoop_pid=$!
|
||||
|
||||
log_silent "✅ opensnoop-bpfcc 已启动 (PID: $opensnoop_pid)"
|
||||
echo "$opensnoop_pid"
|
||||
}
|
||||
|
||||
# 主监控循环
|
||||
monitor_loop() {
|
||||
log "🚀 系统调用监控已启动 (每 ${GENERATE_INTERVAL} 秒采样一次)"
|
||||
log "📝 详细日志: $SYSLOG"
|
||||
|
||||
# 启动持续追踪(静默)
|
||||
local execsnoop_pid=$(trace_process_creation)
|
||||
local opensnoop_pid=$(trace_file_access)
|
||||
|
||||
# 清理函数
|
||||
cleanup() {
|
||||
log "🛑 停止系统调用监控..."
|
||||
kill "$execsnoop_pid" "$opensnoop_pid" 2>/dev/null || true
|
||||
log "✅ 系统调用监控已停止"
|
||||
}
|
||||
|
||||
trap cleanup EXIT TERM INT
|
||||
|
||||
# 定期生成系统调用统计
|
||||
local iteration=0
|
||||
while true; do
|
||||
((iteration++))
|
||||
local agent_pid=$(pgrep -f "agent_runner" | head -1)
|
||||
|
||||
if [ -n "$agent_pid" ]; then
|
||||
local pids=$(get_process_tree_pids "$agent_pid" | tr '\n' ' ')
|
||||
# 每次只输出一行汇总日志
|
||||
collect_syscall_counts "$pids"
|
||||
else
|
||||
log_silent "⚠️ 未检测到 agent_runner 进程"
|
||||
fi
|
||||
|
||||
# 只记录到文件,不输出到控制台
|
||||
log_silent "⏰ 第 ${iteration} 次采样完成,${GENERATE_INTERVAL} 秒后进行下一次..."
|
||||
sleep "$GENERATE_INTERVAL"
|
||||
done
|
||||
}
|
||||
|
||||
# 启动监控
|
||||
case "$1" in
|
||||
start)
|
||||
monitor_loop
|
||||
;;
|
||||
*)
|
||||
echo "用法: $0 start"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
@@ -0,0 +1,2 @@
|
||||
pref("general.config.filename", "firefox.cfg");
|
||||
pref("general.config.obscure_value", 0);
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"policies": {
|
||||
"DisableFirstRunPage": true,
|
||||
"OverrideFirstRunPage": "",
|
||||
"OverridePostUpdatePage": ""
|
||||
}
|
||||
}
|
||||
34
qiming-rcoder/docker/rcoder-agent-runner/firefox.cfg
Normal file
34
qiming-rcoder/docker/rcoder-agent-runner/firefox.cfg
Normal file
@@ -0,0 +1,34 @@
|
||||
// Disable first-run and onboarding
|
||||
pref("browser.startup.homepage_override.mstone", "ignore");
|
||||
pref("browser.startup.homepage_override.buildID", "");
|
||||
pref("browser.aboutwelcome.enabled", false);
|
||||
pref("browser.messaging-system.whatsNewPanel.enabled", false);
|
||||
|
||||
// Disable Firefox studies and telemetry
|
||||
pref("app.shield.optoutstudies.enabled", false);
|
||||
pref("app.normandy.enabled", false);
|
||||
pref("app.normandy.api_url", "");
|
||||
pref("toolkit.telemetry.enabled", false);
|
||||
pref("toolkit.telemetry.unified", false);
|
||||
pref("toolkit.telemetry.archive.enabled", false);
|
||||
pref("datareporting.healthreport.uploadEnabled", false);
|
||||
pref("datareporting.policy.dataSubmissionEnabled", false);
|
||||
|
||||
// Disable sponsored suggestions in address bar (Firefox Suggest)
|
||||
pref("browser.urlbar.suggest.quicksuggest.nonsponsored", false);
|
||||
pref("browser.urlbar.suggest.quicksuggest.sponsored", false);
|
||||
pref("browser.urlbar.quicksuggest.enabled", false);
|
||||
|
||||
// Disable Pocket and all new tab sponsored stuff
|
||||
pref("extensions.pocket.enabled", false);
|
||||
pref("browser.newtabpage.activity-stream.feeds.section.topstories", false);
|
||||
pref("browser.newtabpage.activity-stream.feeds.snippets", false);
|
||||
pref("browser.newtabpage.activity-stream.showSponsored", false);
|
||||
pref("browser.newtabpage.activity-stream.showSponsoredTopSites", false);
|
||||
|
||||
// Disable extension recommendations
|
||||
pref("extensions.htmlaboutaddons.recommendations.enabled", false);
|
||||
pref("browser.discovery.enabled", false);
|
||||
|
||||
// Disable automatic updates
|
||||
pref("app.update.auto", false);
|
||||
110
qiming-rcoder/docker/rcoder-agent-runner/fix-chrome-ime-final.sh
Normal file
110
qiming-rcoder/docker/rcoder-agent-runner/fix-chrome-ime-final.sh
Normal file
@@ -0,0 +1,110 @@
|
||||
#!/bin/bash
|
||||
# Chromium 中文输入法修复工具(fcitx5 方案)
|
||||
# 使用方法:在 VNC 终端中运行 fix-ime
|
||||
|
||||
echo "=========================================="
|
||||
echo " Chromium 中文输入法修复工具"
|
||||
echo "=========================================="
|
||||
echo ""
|
||||
|
||||
# ========== 加载全局输入法环境 ==========
|
||||
if [ -f /etc/profile.d/ime-env.sh ]; then
|
||||
source /etc/profile.d/ime-env.sh
|
||||
echo "✓ 已加载全局输入法环境配置"
|
||||
else
|
||||
echo "⚠ 未找到全局配置,手动设置环境变量"
|
||||
# 设置环境变量
|
||||
export DISPLAY=:0
|
||||
export XDG_RUNTIME_DIR=/run/user/1000
|
||||
export LANG=C.UTF-8
|
||||
export LC_ALL=C.UTF-8
|
||||
export GTK_IM_MODULE=fcitx
|
||||
export QT_IM_MODULE=fcitx
|
||||
export XMODIFIERS=@im=fcitx
|
||||
export INPUT_METHOD=fcitx
|
||||
|
||||
# 导入 D-Bus 会话地址
|
||||
if [ -f /tmp/dbus-session-env ]; then
|
||||
source /tmp/dbus-session-env
|
||||
fi
|
||||
fi
|
||||
|
||||
echo "当前环境变量:"
|
||||
echo " DISPLAY=$DISPLAY"
|
||||
echo " GTK_IM_MODULE=$GTK_IM_MODULE"
|
||||
echo " XMODIFIERS=$XMODIFIERS"
|
||||
echo " LANG=$LANG"
|
||||
echo " DBUS_SESSION_BUS_ADDRESS=$DBUS_SESSION_BUS_ADDRESS"
|
||||
|
||||
echo ""
|
||||
echo "第 1 步:检查 fcitx5 状态..."
|
||||
if pgrep -x fcitx5 > /dev/null; then
|
||||
echo " ✓ fcitx5 已在运行 (PID: $(pgrep -x fcitx5 | head -1))"
|
||||
else
|
||||
echo " ⚠ fcitx5 未运行,正在启动..."
|
||||
fcitx5 -d --replace >/tmp/fcitx5.log 2>&1 &
|
||||
sleep 2
|
||||
|
||||
if pgrep -x fcitx5 > /dev/null; then
|
||||
echo " ✓ fcitx5 启动成功"
|
||||
else
|
||||
echo " ✗ fcitx5 启动失败,查看 /tmp/fcitx5.log"
|
||||
fi
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "第 2 步:停止所有 Chromium 进程..."
|
||||
killall -9 chromium chrome 2>/dev/null || true
|
||||
sleep 2
|
||||
echo "✓ Chromium 进程已清理"
|
||||
|
||||
echo ""
|
||||
echo "第 3 步:使用正确的环境变量启动 Chromium..."
|
||||
echo "提示:Chromium 将继承以下输入法环境:"
|
||||
echo " - GTK_IM_MODULE=fcitx"
|
||||
echo " - XMODIFIERS=@im=fcitx"
|
||||
echo " - LANG=C.UTF-8"
|
||||
echo ""
|
||||
|
||||
/usr/bin/chromium \
|
||||
--user-data-dir=/home/user/chromium-data \
|
||||
--no-sandbox \
|
||||
--disable-dev-shm-usage \
|
||||
--remote-debugging-port=9222 \
|
||||
--remote-debugging-address=0.0.0.0 \
|
||||
--no-first-run \
|
||||
--no-default-browser-check \
|
||||
--password-store=basic \
|
||||
--use-mock-keychain \
|
||||
>/tmp/chromium-ime.log 2>&1 &
|
||||
|
||||
sleep 3
|
||||
|
||||
if pgrep chromium > /dev/null; then
|
||||
echo " ✓ Chromium 启动成功 (PID: $(pgrep chromium | head -1))"
|
||||
|
||||
# 验证 Chromium 是否继承了正确的环境变量
|
||||
CHROME_PID=$(pgrep chromium | head -1)
|
||||
echo ""
|
||||
echo "验证 Chromium 环境变量:"
|
||||
cat /proc/$CHROME_PID/environ | tr '\0' '\n' | grep -E "(GTK_IM_MODULE|XMODIFIERS|LANG)" || echo " ⚠ 警告:未找到输入法环境变量"
|
||||
else
|
||||
echo " ✗ Chromium 启动失败,查看 /tmp/chromium-ime.log"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "=========================================="
|
||||
echo " ✅ 修复完成!"
|
||||
echo "=========================================="
|
||||
echo ""
|
||||
echo "现在请测试:"
|
||||
echo " 1. 在 Chromium 中访问 baidu.com 或任何网页"
|
||||
echo " 2. 点击输入框(搜索框、文本框等)"
|
||||
echo " 3. 按 Ctrl+Space 切换输入法"
|
||||
echo " 4. 输入拼音(如 nihao)应显示候选词"
|
||||
echo ""
|
||||
echo "如果仍无法输入中文,请尝试:"
|
||||
echo " - 重启容器使环境变量全局生效"
|
||||
echo " - 或者使用 'source /etc/profile.d/ime-env.sh' 再次运行本脚本"
|
||||
echo "=========================================="
|
||||
315
qiming-rcoder/docker/rcoder-agent-runner/ime_passthrough.js
Normal file
315
qiming-rcoder/docker/rcoder-agent-runner/ime_passthrough.js
Normal file
@@ -0,0 +1,315 @@
|
||||
/**
|
||||
* ime_passthrough.js - 本地输入法透传前端脚本
|
||||
*
|
||||
* 使用方法:
|
||||
* 1. 将此文件放到 noVNC 目录中
|
||||
* 2. 在 vnc.html 的 </body> 前添加:
|
||||
* <script src="ime_passthrough.js"></script>
|
||||
*
|
||||
* 工作原理:
|
||||
* 1. 创建一个隐藏的输入框,用于捕获用户的输入法输入
|
||||
* 2. 当用户点击 VNC 画面时,焦点切换到隐藏输入框
|
||||
* 3. 用户使用本地输入法(如搜狗输入法)输入中文
|
||||
* 4. 输入完成后,通过 WebSocket 发送到 IME 服务器
|
||||
* 5. 服务器使用 xdotool 将文本输入到远程桌面
|
||||
*/
|
||||
|
||||
(function () {
|
||||
'use strict';
|
||||
|
||||
// 配置
|
||||
const CONFIG = {
|
||||
// IME 服务端口(与 ime_server.py 中的端口一致)
|
||||
IME_PORT: 6091,
|
||||
// 重连间隔(毫秒)
|
||||
RECONNECT_INTERVAL: 3000,
|
||||
// 心跳间隔(毫秒)
|
||||
HEARTBEAT_INTERVAL: 30000,
|
||||
// 是否启用调试日志
|
||||
DEBUG: false
|
||||
};
|
||||
|
||||
// 日志函数
|
||||
function log(level, message) {
|
||||
if (level === 'DEBUG' && !CONFIG.DEBUG) return;
|
||||
const timestamp = new Date().toISOString();
|
||||
console.log(`[${timestamp}] [${level}] [IME] ${message}`);
|
||||
}
|
||||
|
||||
// 状态变量
|
||||
let imeWebSocket = null;
|
||||
let imeInput = null;
|
||||
let isComposing = false;
|
||||
let reconnectTimer = null;
|
||||
let heartbeatTimer = null;
|
||||
|
||||
/**
|
||||
* 创建隐藏的输入框
|
||||
*/
|
||||
function createImeInput() {
|
||||
if (imeInput) return imeInput;
|
||||
|
||||
imeInput = document.createElement('textarea');
|
||||
imeInput.id = 'ime-capture';
|
||||
imeInput.setAttribute('autocomplete', 'off');
|
||||
imeInput.setAttribute('autocorrect', 'off');
|
||||
imeInput.setAttribute('autocapitalize', 'off');
|
||||
imeInput.setAttribute('spellcheck', 'false');
|
||||
imeInput.style.cssText = `
|
||||
position: fixed;
|
||||
left: -9999px;
|
||||
top: 50%;
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
z-index: -1;
|
||||
`;
|
||||
|
||||
document.body.appendChild(imeInput);
|
||||
|
||||
// 监听输入法事件
|
||||
imeInput.addEventListener('compositionstart', onCompositionStart);
|
||||
imeInput.addEventListener('compositionend', onCompositionEnd);
|
||||
imeInput.addEventListener('input', onInput);
|
||||
imeInput.addEventListener('keydown', onKeyDown);
|
||||
|
||||
log('INFO', 'IME input element created');
|
||||
return imeInput;
|
||||
}
|
||||
|
||||
/**
|
||||
* 连接到 IME WebSocket 服务
|
||||
*/
|
||||
function connectWebSocket() {
|
||||
if (imeWebSocket && imeWebSocket.readyState === WebSocket.OPEN) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 构建 WebSocket URL(使用当前页面的主机名)
|
||||
const wsUrl = `ws://${location.hostname}:${CONFIG.IME_PORT}`;
|
||||
log('INFO', `Connecting to IME server: ${wsUrl}`);
|
||||
|
||||
try {
|
||||
imeWebSocket = new WebSocket(wsUrl);
|
||||
|
||||
imeWebSocket.onopen = function () {
|
||||
log('INFO', 'Connected to IME server');
|
||||
clearReconnectTimer();
|
||||
startHeartbeat();
|
||||
};
|
||||
|
||||
imeWebSocket.onclose = function (event) {
|
||||
log('INFO', `Disconnected from IME server (code: ${event.code})`);
|
||||
stopHeartbeat();
|
||||
scheduleReconnect();
|
||||
};
|
||||
|
||||
imeWebSocket.onerror = function (error) {
|
||||
log('ERROR', 'WebSocket error');
|
||||
// onclose 会自动触发
|
||||
};
|
||||
|
||||
imeWebSocket.onmessage = function (event) {
|
||||
try {
|
||||
const data = JSON.parse(event.data);
|
||||
if (data.status === 'ok') {
|
||||
log('DEBUG', 'Text sent successfully');
|
||||
} else if (data.status === 'error') {
|
||||
log('ERROR', `Server error: ${data.message}`);
|
||||
} else if (data.type === 'pong') {
|
||||
log('DEBUG', 'Heartbeat pong received');
|
||||
}
|
||||
} catch (e) {
|
||||
log('ERROR', `Failed to parse response: ${e}`);
|
||||
}
|
||||
};
|
||||
} catch (e) {
|
||||
log('ERROR', `Failed to connect: ${e}`);
|
||||
scheduleReconnect();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送文本到 IME 服务器
|
||||
*/
|
||||
function sendText(text, method = 'xdotool') {
|
||||
if (!text) return;
|
||||
|
||||
if (!imeWebSocket || imeWebSocket.readyState !== WebSocket.OPEN) {
|
||||
log('WARN', 'WebSocket not connected, text not sent');
|
||||
return;
|
||||
}
|
||||
|
||||
const message = JSON.stringify({
|
||||
type: 'text',
|
||||
text: text,
|
||||
method: method
|
||||
});
|
||||
|
||||
try {
|
||||
imeWebSocket.send(message);
|
||||
log('DEBUG', `Sent text: ${text.substring(0, 50)}${text.length > 50 ? '...' : ''}`);
|
||||
} catch (e) {
|
||||
log('ERROR', `Failed to send text: ${e}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 输入法开始组合
|
||||
*/
|
||||
function onCompositionStart(e) {
|
||||
isComposing = true;
|
||||
log('DEBUG', 'Composition started');
|
||||
}
|
||||
|
||||
/**
|
||||
* 输入法完成组合
|
||||
*/
|
||||
function onCompositionEnd(e) {
|
||||
isComposing = false;
|
||||
const text = e.data;
|
||||
|
||||
if (text) {
|
||||
log('DEBUG', `Composition ended: ${text}`);
|
||||
sendText(text);
|
||||
}
|
||||
|
||||
// 清空输入框
|
||||
setTimeout(() => {
|
||||
if (imeInput) imeInput.value = '';
|
||||
}, 10);
|
||||
}
|
||||
|
||||
/**
|
||||
* 普通输入(非输入法)
|
||||
*/
|
||||
function onInput(e) {
|
||||
// 如果正在使用输入法组合,不处理
|
||||
if (isComposing) return;
|
||||
|
||||
const text = imeInput.value;
|
||||
if (text) {
|
||||
log('DEBUG', `Direct input: ${text}`);
|
||||
sendText(text);
|
||||
imeInput.value = '';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 键盘按下事件
|
||||
* 用于处理特殊键(如回车、退格等)
|
||||
*/
|
||||
function onKeyDown(e) {
|
||||
// 如果正在使用输入法组合,让输入法处理
|
||||
if (isComposing) return;
|
||||
|
||||
// 这些键由 VNC 处理,不需要 IME 透传
|
||||
// 用户可以点击 VNC 画面来让 VNC 处理键盘
|
||||
}
|
||||
|
||||
/**
|
||||
* 定时重连
|
||||
*/
|
||||
function scheduleReconnect() {
|
||||
if (reconnectTimer) return;
|
||||
|
||||
log('INFO', `Will reconnect in ${CONFIG.RECONNECT_INTERVAL}ms`);
|
||||
reconnectTimer = setTimeout(() => {
|
||||
reconnectTimer = null;
|
||||
connectWebSocket();
|
||||
}, CONFIG.RECONNECT_INTERVAL);
|
||||
}
|
||||
|
||||
function clearReconnectTimer() {
|
||||
if (reconnectTimer) {
|
||||
clearTimeout(reconnectTimer);
|
||||
reconnectTimer = null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 心跳保活
|
||||
*/
|
||||
function startHeartbeat() {
|
||||
stopHeartbeat();
|
||||
heartbeatTimer = setInterval(() => {
|
||||
if (imeWebSocket && imeWebSocket.readyState === WebSocket.OPEN) {
|
||||
try {
|
||||
imeWebSocket.send(JSON.stringify({ type: 'ping' }));
|
||||
log('DEBUG', 'Heartbeat ping sent');
|
||||
} catch (e) {
|
||||
log('ERROR', `Heartbeat failed: ${e}`);
|
||||
}
|
||||
}
|
||||
}, CONFIG.HEARTBEAT_INTERVAL);
|
||||
}
|
||||
|
||||
function stopHeartbeat() {
|
||||
if (heartbeatTimer) {
|
||||
clearInterval(heartbeatTimer);
|
||||
heartbeatTimer = null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化 IME 透传功能
|
||||
*/
|
||||
function init() {
|
||||
log('INFO', 'Initializing IME passthrough...');
|
||||
|
||||
// 创建隐藏输入框
|
||||
createImeInput();
|
||||
|
||||
// 连接 WebSocket
|
||||
connectWebSocket();
|
||||
|
||||
// 监听 VNC 画面点击,切换焦点到隐藏输入框
|
||||
// 需要找到 noVNC 的 screen 元素
|
||||
const setupClickHandler = () => {
|
||||
// noVNC 可能使用不同的元素 ID
|
||||
const vncScreen = document.getElementById('screen') ||
|
||||
document.querySelector('canvas') ||
|
||||
document.querySelector('.noVNC_canvas');
|
||||
|
||||
if (vncScreen) {
|
||||
vncScreen.addEventListener('click', () => {
|
||||
if (imeInput) {
|
||||
imeInput.focus();
|
||||
log('DEBUG', 'IME input focused');
|
||||
}
|
||||
});
|
||||
log('INFO', 'VNC screen click handler attached');
|
||||
} else {
|
||||
log('WARN', 'VNC screen element not found, will retry');
|
||||
setTimeout(setupClickHandler, 1000);
|
||||
}
|
||||
};
|
||||
|
||||
// 等待 DOM 完全加载
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', setupClickHandler);
|
||||
} else {
|
||||
setupClickHandler();
|
||||
}
|
||||
|
||||
log('INFO', 'IME passthrough initialized');
|
||||
log('INFO', 'Usage: Click on VNC screen, then type using your local input method');
|
||||
}
|
||||
|
||||
// 页面加载后初始化
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', init);
|
||||
} else {
|
||||
init();
|
||||
}
|
||||
|
||||
// 导出到全局(方便调试)
|
||||
window.IMEPassthrough = {
|
||||
sendText: sendText,
|
||||
connect: connectWebSocket,
|
||||
isConnected: () => imeWebSocket && imeWebSocket.readyState === WebSocket.OPEN,
|
||||
setDebug: (enabled) => { CONFIG.DEBUG = enabled; }
|
||||
};
|
||||
|
||||
})();
|
||||
260
qiming-rcoder/docker/rcoder-agent-runner/ime_server.py
Normal file
260
qiming-rcoder/docker/rcoder-agent-runner/ime_server.py
Normal 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)
|
||||
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"mcpServers": {
|
||||
"chrome-devtools": {
|
||||
"command": "chrome-devtools-mcp",
|
||||
"args": [
|
||||
"--userDataDir=/home/user/.config/chromium",
|
||||
"--executablePath=/usr/local/bin/chromium-for-mcp",
|
||||
"--no-usage-statistics",
|
||||
"--chrome-arg=--no-sandbox",
|
||||
"--chrome-arg=--no-zygote",
|
||||
"--chrome-arg=--disable-dev-shm-usage",
|
||||
"--chrome-arg=--disable-gpu",
|
||||
"--chrome-arg=--remote-debugging-port=9222"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
9
qiming-rcoder/docker/rcoder-agent-runner/package.json
Normal file
9
qiming-rcoder/docker/rcoder-agent-runner/package.json
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"name": "@e2b/code-interpreter-template",
|
||||
"private": true,
|
||||
"version": "0.1.0",
|
||||
"scripts": {
|
||||
"lint": "ruff check .",
|
||||
"format": "ruff format ."
|
||||
}
|
||||
}
|
||||
31
qiming-rcoder/docker/rcoder-agent-runner/requirements.txt
Normal file
31
qiming-rcoder/docker/rcoder-agent-runner/requirements.txt
Normal file
@@ -0,0 +1,31 @@
|
||||
# Python 数据科学库依赖 - 最新稳定版本
|
||||
# ===============================================
|
||||
# ✅ 已更新为截至 2025年1月 的最新可用版本
|
||||
# 验证:这些版本在 Debian 12 容器中兼容
|
||||
# 适用场景:Docker容器、Python 3.11+、Debian 12
|
||||
|
||||
# 第1层:核心计算库(基础依赖)
|
||||
numpy==2.4.1
|
||||
scipy==1.17.0
|
||||
|
||||
# 第2层:数据处理和可视化库(依赖 numpy)
|
||||
pandas==3.0.0
|
||||
matplotlib==3.10.8
|
||||
seaborn==0.13.2
|
||||
|
||||
# 第3层:机器学习库(依赖 numpy/scipy)
|
||||
scikit-learn==1.8.0
|
||||
|
||||
# 第4层:文档和文件处理库
|
||||
openpyxl==3.1.5
|
||||
python-docx==1.1.2
|
||||
xlrd==2.0.1
|
||||
xlwt==1.3.0
|
||||
|
||||
# 第5层:网络和数据处理库
|
||||
requests==2.32.3
|
||||
beautifulsoup4==4.12.3
|
||||
lxml==5.3.0
|
||||
|
||||
# 第6层:PDF 处理库
|
||||
PyMuPDF==1.26.7
|
||||
12
qiming-rcoder/docker/rcoder-agent-runner/settings.json
Normal file
12
qiming-rcoder/docker/rcoder-agent-runner/settings.json
Normal file
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"files.autoSave": "afterDelay",
|
||||
"files.autoSaveDelay": 200,
|
||||
"security.allowHttp": true,
|
||||
"security.workspace.trust.startupPrompt": "never",
|
||||
"security.workspace.trust.enabled": false,
|
||||
"security.workspace.trust.banner": "never",
|
||||
"security.workspace.trust.emptyWindow": false,
|
||||
"github.copilot.enable": {
|
||||
"*": false
|
||||
}
|
||||
}
|
||||
79
qiming-rcoder/docker/rcoder-agent-runner/settings.xml
Normal file
79
qiming-rcoder/docker/rcoder-agent-runner/settings.xml
Normal file
@@ -0,0 +1,79 @@
|
||||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<settings
|
||||
xmlns="http://maven.apache.org/SETTINGS/1.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/SETTINGS/1.0.0 http://maven.apache.org/xsd/settings-1.0.0.xsd"
|
||||
>
|
||||
<!-- 指定本地仓库路径 -->
|
||||
<localRepository>/home/user/.m2/repository</localRepository>
|
||||
|
||||
<mirrors>
|
||||
<!-- 阿里云 Maven 仓库 -->
|
||||
<mirror>
|
||||
<id>aliyun-maven-repository</id>
|
||||
<mirrorOf>*</mirrorOf>
|
||||
<name>阿里云公共仓库</name>
|
||||
<url>https://maven.aliyun.com/repository/public</url>
|
||||
</mirror>
|
||||
<!-- Maven 中央仓库 -->
|
||||
<mirror>
|
||||
<id>maven-central</id>
|
||||
<mirrorOf>central</mirrorOf>
|
||||
<name>Maven Central</name>
|
||||
<url>https://repo1.maven.org/maven2</url>
|
||||
</mirror>
|
||||
<!-- JBoss 仓库 -->
|
||||
<mirror>
|
||||
<id>jboss-public-repository</id>
|
||||
<mirrorOf>central</mirrorOf>
|
||||
<name>JBoss Public Repository</name>
|
||||
<url>https://repository.jboss.org/nexus/content/groups/public</url>
|
||||
</mirror>
|
||||
</mirrors>
|
||||
|
||||
<profiles>
|
||||
<profile>
|
||||
<id>spring</id>
|
||||
<repositories>
|
||||
<repository>
|
||||
<id>spring-milestones</id>
|
||||
<name>Spring Milestones</name>
|
||||
<url>https://repo.spring.io/milestone</url>
|
||||
<snapshots>
|
||||
<enabled>false</enabled>
|
||||
</snapshots>
|
||||
</repository>
|
||||
<repository>
|
||||
<id>spring-snapshots</id>
|
||||
<name>Spring Snapshots</name>
|
||||
<url>https://repo.spring.io/snapshot</url>
|
||||
<releases>
|
||||
<enabled>false</enabled>
|
||||
</releases>
|
||||
</repository>
|
||||
</repositories>
|
||||
<pluginRepositories>
|
||||
<pluginRepository>
|
||||
<id>spring-milestones</id>
|
||||
<name>Spring Milestones</name>
|
||||
<url>https://repo.spring.io/milestone</url>
|
||||
<snapshots>
|
||||
<enabled>false</enabled>
|
||||
</snapshots>
|
||||
</pluginRepository>
|
||||
<pluginRepository>
|
||||
<id>spring-snapshots</id>
|
||||
<name>Spring Snapshots</name>
|
||||
<url>https://repo.spring.io/snapshot</url>
|
||||
<releases>
|
||||
<enabled>false</enabled>
|
||||
</releases>
|
||||
</pluginRepository>
|
||||
</pluginRepositories>
|
||||
</profile>
|
||||
</profiles>
|
||||
|
||||
<activeProfiles>
|
||||
<activeProfile>spring</activeProfile>
|
||||
</activeProfiles>
|
||||
</settings>
|
||||
@@ -0,0 +1,48 @@
|
||||
#!/bin/bash
|
||||
# agent_runner HTTP Server 模式启动脚本
|
||||
#
|
||||
# 启用 http-server feature,直接提供 HTTP REST API
|
||||
# 不需要 gRPC,由 agent_runner 自身处理所有请求
|
||||
#
|
||||
# 使用方式:
|
||||
# ./start-http-server.sh # 使用默认配置
|
||||
# PORT=8086 ./start-http-server.sh # 指定端口
|
||||
|
||||
set -e
|
||||
|
||||
# ==================== 配置 ====================
|
||||
PORT=${PORT:-8086}
|
||||
PROJECTS_DIR=${PROJECTS_DIR:-/app/project_workspace}
|
||||
LOG_DIR=${LOG_DIR:-/app/container-logs}
|
||||
|
||||
# ==================== 初始化 ====================
|
||||
echo "=============================================="
|
||||
echo "agent_runner HTTP Server 模式启动"
|
||||
echo "=============================================="
|
||||
echo " PORT: ${PORT}"
|
||||
echo " PROJECTS_DIR: ${PROJECTS_DIR}"
|
||||
echo " LOG_DIR: ${LOG_DIR}"
|
||||
echo "=============================================="
|
||||
|
||||
# 创建日志目录
|
||||
mkdir -p ${LOG_DIR}
|
||||
|
||||
# 创建项目工作目录
|
||||
mkdir -p ${PROJECTS_DIR}
|
||||
|
||||
# ==================== 启动 agent_runner ====================
|
||||
echo "[INFO] 启动 agent_runner HTTP Server..."
|
||||
echo "[INFO] 可用端点:"
|
||||
echo " POST /chat - RCoder Agent 对话"
|
||||
echo " GET /agent/status/:id - 查询 Agent 状态"
|
||||
echo " POST /agent/stop - 停止 Agent"
|
||||
echo " POST /agent/session/cancel - 取消任务"
|
||||
echo " GET /agent/progress/:sid - SSE 进度流"
|
||||
echo " GET /health - 健康检查"
|
||||
echo " GET /api/docs - Swagger 文档"
|
||||
echo ""
|
||||
|
||||
exec agent_runner \
|
||||
--port ${PORT} \
|
||||
--projects-dir ${PROJECTS_DIR} \
|
||||
--enable-proxy
|
||||
2181
qiming-rcoder/docker/rcoder-agent-runner/start-up.sh
Normal file
2181
qiming-rcoder/docker/rcoder-agent-runner/start-up.sh
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,82 @@
|
||||
import pandas
|
||||
from matplotlib.pyplot import Figure
|
||||
import IPython
|
||||
from IPython.core.formatters import BaseFormatter, JSONFormatter
|
||||
from traitlets.traitlets import Unicode, ObjectName
|
||||
|
||||
from e2b_charts import chart_figure_to_dict
|
||||
import orjson
|
||||
|
||||
|
||||
def _figure_repr_e2b_chart_(self: Figure):
|
||||
"""
|
||||
This method is used to extract data from the figure object to a dictionary
|
||||
"""
|
||||
# Get all Axes objects from the Figure
|
||||
try:
|
||||
return chart_figure_to_dict(self)
|
||||
except: # noqa: E722
|
||||
return {}
|
||||
|
||||
|
||||
def _dataframe_repr_e2b_data_(self: pandas.DataFrame):
|
||||
result = self.to_dict(orient="list")
|
||||
for key, value in result.items():
|
||||
# Check each column's values
|
||||
result[key] = [
|
||||
v.isoformat() if isinstance(v, pandas.Timestamp) else v for v in value
|
||||
]
|
||||
return result
|
||||
|
||||
|
||||
class E2BDataFormatter(BaseFormatter):
|
||||
format_type = Unicode("e2b/data")
|
||||
|
||||
print_method = ObjectName("_repr_e2b_data_")
|
||||
_return_type = (dict, str)
|
||||
|
||||
type_printers = {pandas.DataFrame: _dataframe_repr_e2b_data_}
|
||||
|
||||
|
||||
class E2BChartFormatter(BaseFormatter):
|
||||
format_type = Unicode("e2b/chart")
|
||||
|
||||
print_method = ObjectName("_repr_e2b_chart_")
|
||||
_return_type = (dict, str)
|
||||
|
||||
def __call__(self, obj):
|
||||
# Figure object is for some reason removed on execution of the cell,
|
||||
# so it can't be used in type_printers or with top-level import
|
||||
from matplotlib.pyplot import Figure
|
||||
|
||||
if isinstance(obj, Figure):
|
||||
return _figure_repr_e2b_chart_(obj)
|
||||
return super().__call__(obj)
|
||||
|
||||
|
||||
class E2BJSONFormatter(JSONFormatter):
|
||||
def __call__(self, obj):
|
||||
if isinstance(obj, (list, dict)):
|
||||
try:
|
||||
return orjson.loads(
|
||||
orjson.dumps(
|
||||
obj, option=orjson.OPT_SERIALIZE_NUMPY | orjson.OPT_NON_STR_KEYS
|
||||
)
|
||||
), {"expanded": True}
|
||||
except TypeError:
|
||||
pass
|
||||
|
||||
return super().__call__(obj)
|
||||
|
||||
|
||||
ip = IPython.get_ipython()
|
||||
ip.display_formatter.formatters["e2b/data"] = E2BDataFormatter(
|
||||
parent=ip.display_formatter
|
||||
)
|
||||
ip.display_formatter.formatters["e2b/chart"] = E2BChartFormatter(
|
||||
parent=ip.display_formatter
|
||||
)
|
||||
|
||||
ip.display_formatter.formatters["application/json"] = E2BJSONFormatter(
|
||||
parent=ip.display_formatter
|
||||
)
|
||||
@@ -0,0 +1,24 @@
|
||||
from typing import Any
|
||||
|
||||
from IPython.core.display_functions import display
|
||||
from PIL.Image import Image
|
||||
from PIL.ImageShow import UnixViewer
|
||||
|
||||
|
||||
def show_file(self, path: str, **options: Any) -> int:
|
||||
# To prevent errors from trying to display image without any display
|
||||
return 0
|
||||
|
||||
|
||||
UnixViewer.show_file = show_file
|
||||
original_save = Image.save
|
||||
|
||||
|
||||
def save(image, fp, format=None, **options):
|
||||
if isinstance(fp, str):
|
||||
display(image)
|
||||
|
||||
original_save(image, fp, format, **options)
|
||||
|
||||
|
||||
Image.save = save
|
||||
BIN
qiming-rcoder/docker/rcoder-agent-runner/wallpaper.jpeg
Normal file
BIN
qiming-rcoder/docker/rcoder-agent-runner/wallpaper.jpeg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 368 KiB |
BIN
qiming-rcoder/docker/rcoder-agent-runner/wallpaper.png
Normal file
BIN
qiming-rcoder/docker/rcoder-agent-runner/wallpaper.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.9 MiB |
99
qiming-rcoder/docker/rcoder-agent-runner/xfce4-desktop.xml
Normal file
99
qiming-rcoder/docker/rcoder-agent-runner/xfce4-desktop.xml
Normal file
@@ -0,0 +1,99 @@
|
||||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<channel name="xfce4-desktop" version="1.0">
|
||||
<property name="backdrop" type="empty">
|
||||
<property name="screen0" type="empty">
|
||||
<!-- 监视器 VNC-0 (TigerVNC 使用这个名称) -->
|
||||
<property name="monitorVNC-0" type="empty">
|
||||
<property name="workspace0" type="empty">
|
||||
<property name="color-style" type="int" value="0" />
|
||||
<property name="image-style" type="int" value="5" />
|
||||
<property name="last-image" type="string" value="/usr/share/backgrounds/xfce/wallpaper.jpeg" />
|
||||
</property>
|
||||
<property name="workspace1" type="empty">
|
||||
<property name="color-style" type="int" value="0" />
|
||||
<property name="image-style" type="int" value="5" />
|
||||
<property name="last-image" type="string" value="/usr/share/backgrounds/xfce/wallpaper.jpeg" />
|
||||
</property>
|
||||
<property name="workspace2" type="empty">
|
||||
<property name="color-style" type="int" value="0" />
|
||||
<property name="image-style" type="int" value="5" />
|
||||
<property name="last-image" type="string" value="/usr/share/backgrounds/xfce/wallpaper.jpeg" />
|
||||
</property>
|
||||
<property name="workspace3" type="empty">
|
||||
<property name="color-style" type="int" value="0" />
|
||||
<property name="image-style" type="int" value="5" />
|
||||
<property name="last-image" type="string" value="/usr/share/backgrounds/xfce/wallpaper.jpeg" />
|
||||
</property>
|
||||
</property>
|
||||
<!-- 兼容 monitorscreen 路径 -->
|
||||
<property name="monitorscreen" type="empty">
|
||||
<property name="workspace0" type="empty">
|
||||
<property name="color-style" type="int" value="0" />
|
||||
<property name="image-style" type="int" value="5" />
|
||||
<property name="last-image" type="string" value="/usr/share/backgrounds/xfce/wallpaper.jpeg" />
|
||||
</property>
|
||||
<property name="workspace1" type="empty">
|
||||
<property name="color-style" type="int" value="0" />
|
||||
<property name="image-style" type="int" value="5" />
|
||||
<property name="last-image" type="string" value="/usr/share/backgrounds/xfce/wallpaper.jpeg" />
|
||||
</property>
|
||||
<property name="workspace2" type="empty">
|
||||
<property name="color-style" type="int" value="0" />
|
||||
<property name="image-style" type="int" value="5" />
|
||||
<property name="last-image" type="string" value="/usr/share/backgrounds/xfce/wallpaper.jpeg" />
|
||||
</property>
|
||||
<property name="workspace3" type="empty">
|
||||
<property name="color-style" type="int" value="0" />
|
||||
<property name="image-style" type="int" value="5" />
|
||||
<property name="last-image" type="string" value="/usr/share/backgrounds/xfce/wallpaper.jpeg" />
|
||||
</property>
|
||||
</property>
|
||||
<!-- 兼容 monitor0 路径 -->
|
||||
<property name="monitor0" type="empty">
|
||||
<property name="workspace0" type="empty">
|
||||
<property name="color-style" type="int" value="0" />
|
||||
<property name="image-style" type="int" value="5" />
|
||||
<property name="last-image" type="string" value="/usr/share/backgrounds/xfce/wallpaper.jpeg" />
|
||||
</property>
|
||||
<property name="workspace1" type="empty">
|
||||
<property name="color-style" type="int" value="0" />
|
||||
<property name="image-style" type="int" value="5" />
|
||||
<property name="last-image" type="string" value="/usr/share/backgrounds/xfce/wallpaper.jpeg" />
|
||||
</property>
|
||||
<property name="workspace2" type="empty">
|
||||
<property name="color-style" type="int" value="0" />
|
||||
<property name="image-style" type="int" value="5" />
|
||||
<property name="last-image" type="string" value="/usr/share/backgrounds/xfce/wallpaper.jpeg" />
|
||||
</property>
|
||||
<property name="workspace3" type="empty">
|
||||
<property name="color-style" type="int" value="0" />
|
||||
<property name="image-style" type="int" value="5" />
|
||||
<property name="last-image" type="string" value="/usr/share/backgrounds/xfce/wallpaper.jpeg" />
|
||||
</property>
|
||||
</property>
|
||||
<!-- 兼容 monitor1 路径 -->
|
||||
<property name="monitor1" type="empty">
|
||||
<property name="workspace0" type="empty">
|
||||
<property name="color-style" type="int" value="0" />
|
||||
<property name="image-style" type="int" value="5" />
|
||||
<property name="last-image" type="string" value="/usr/share/backgrounds/xfce/wallpaper.jpeg" />
|
||||
</property>
|
||||
<property name="workspace1" type="empty">
|
||||
<property name="color-style" type="int" value="0" />
|
||||
<property name="image-style" type="int" value="5" />
|
||||
<property name="last-image" type="string" value="/usr/share/backgrounds/xfce/wallpaper.jpeg" />
|
||||
</property>
|
||||
<property name="workspace2" type="empty">
|
||||
<property name="color-style" type="int" value="0" />
|
||||
<property name="image-style" type="int" value="5" />
|
||||
<property name="last-image" type="string" value="/usr/share/backgrounds/xfce/wallpaper.jpeg" />
|
||||
</property>
|
||||
<property name="workspace3" type="empty">
|
||||
<property name="color-style" type="int" value="0" />
|
||||
<property name="image-style" type="int" value="5" />
|
||||
<property name="last-image" type="string" value="/usr/share/backgrounds/xfce/wallpaper.jpeg" />
|
||||
</property>
|
||||
</property>
|
||||
</property>
|
||||
</property>
|
||||
</channel>
|
||||
32
qiming-rcoder/docker/rcoder-master/.cargo/config.toml
Normal file
32
qiming-rcoder/docker/rcoder-master/.cargo/config.toml
Normal file
@@ -0,0 +1,32 @@
|
||||
[source.crates-io]
|
||||
replace-with = 'rsproxy-sparse'
|
||||
|
||||
[source.rsproxy]
|
||||
registry = "https://rsproxy.cn/crates.io-index"
|
||||
|
||||
[source.rsproxy-sparse]
|
||||
registry = "sparse+https://rsproxy.cn/index/"
|
||||
|
||||
[source.aliyun]
|
||||
registry = "https://mirrors.aliyun.com/crates.io-index"
|
||||
|
||||
[source.aliyun-sparse]
|
||||
registry = "sparse+https://mirrors.aliyun.com/crates.io-index/"
|
||||
|
||||
[source.tencent]
|
||||
registry = "https://mirrors.cloud.tencent.com/crates.io-index"
|
||||
|
||||
[source.tencent-sparse]
|
||||
registry = "sparse+https://mirrors.cloud.tencent.com/crates.io-index/"
|
||||
|
||||
[source.ustc]
|
||||
registry = "https://mirrors.ustc.edu.cn/crates.io-index"
|
||||
|
||||
[source.ustc-sparse]
|
||||
registry = "sparse+https://mirrors.ustc.edu.cn/crates.io-index/"
|
||||
|
||||
[registries.rsproxy]
|
||||
index = "https://rsproxy.cn/crates.io-index"
|
||||
|
||||
[net]
|
||||
git-fetch-with-cli = true
|
||||
1
qiming-rcoder/docker/rcoder-master/.npmrc
Normal file
1
qiming-rcoder/docker/rcoder-master/.npmrc
Normal file
@@ -0,0 +1 @@
|
||||
registry=https://registry.npmmirror.com/
|
||||
128
qiming-rcoder/docker/rcoder-master/Dockerfile
Normal file
128
qiming-rcoder/docker/rcoder-master/Dockerfile
Normal file
@@ -0,0 +1,128 @@
|
||||
# ============================================================================
|
||||
# 最终镜像 Dockerfile - master-rcoder
|
||||
# ============================================================================
|
||||
# 此 Dockerfile 使用多阶段构建:
|
||||
# 1. 第一阶段:在 node:22 中构建 Rust 项目
|
||||
# 2. 第二阶段:基于 master-rcoder-base 基础镜像运行
|
||||
# ============================================================================
|
||||
|
||||
# 全局 ARG 声明(必须在第一个 FROM 之前,才能在后续 FROM 中使用)
|
||||
ARG BASE_IMAGE=master-rcoder-base:latest
|
||||
|
||||
# ============================================================================
|
||||
# 第一阶段:构建 Rust 项目
|
||||
# ============================================================================
|
||||
FROM node:22 AS rust-builder
|
||||
|
||||
# 设置环境变量避免交互式提示
|
||||
ENV DEBIAN_FRONTEND=noninteractive
|
||||
ENV TZ=Asia/Shanghai
|
||||
|
||||
# 设置 ARG TARGETARCH
|
||||
ARG TARGETARCH
|
||||
|
||||
# 设置工作目录
|
||||
WORKDIR /build
|
||||
|
||||
# 使用 LinuxMirrors 一键配置阿里云镜像源
|
||||
# 参考: https://github.com/SuperManito/LinuxMirrors
|
||||
RUN curl -sSL https://linuxmirrors.cn/main.sh | bash -s -- \
|
||||
--source mirrors.aliyun.com \
|
||||
--protocol https \
|
||||
--use-intranet-source false \
|
||||
--install-epel false \
|
||||
--backup false \
|
||||
--upgrade-software false \
|
||||
--clean-cache true \
|
||||
--ignore-backup-tips
|
||||
|
||||
# 安装构建依赖(包括cmake和protoc)
|
||||
RUN apt-get update && apt-get install -y \
|
||||
build-essential \
|
||||
cmake \
|
||||
pkg-config \
|
||||
protobuf-compiler \
|
||||
libprotobuf-dev \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# 安装 Rust
|
||||
RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y && \
|
||||
. /root/.cargo/env && cargo --version
|
||||
ENV PATH="/root/.cargo/bin:${PATH}"
|
||||
|
||||
# 复制整个项目源代码(通过 .dockerignore 排除不需要的文件)
|
||||
COPY . .
|
||||
|
||||
# 🔧 支持 CARGO_FLAGS 参数,允许外部传递 feature flags
|
||||
ARG CARGO_FLAGS=""
|
||||
RUN echo "🔧 Cargo flags: ${CARGO_FLAGS}" && \
|
||||
cargo build --release --bin rcoder --bin agent_runner ${CARGO_FLAGS}
|
||||
|
||||
# ============================================================================
|
||||
# 第二阶段:运行阶段(基于基础镜像)
|
||||
# ============================================================================
|
||||
# 重新声明 ARG(FROM 之后需要重新声明才能使用)
|
||||
ARG BASE_IMAGE
|
||||
FROM ${BASE_IMAGE}
|
||||
|
||||
# 设置工作目录
|
||||
WORKDIR /app
|
||||
|
||||
# 安装 git(用于 nuwaxcode 源码安装)
|
||||
RUN apt-get update && apt-get install -y \
|
||||
git \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# 复制 .npmrc 文件
|
||||
COPY docker/rcoder-master/.npmrc /root/.npmrc
|
||||
|
||||
# 创建 bin 目录并复制构建产物
|
||||
RUN mkdir -p /app/bin
|
||||
COPY --from=rust-builder /build/target/release/rcoder /app/bin/rcoder
|
||||
COPY --from=rust-builder /build/target/release/agent_runner /app/bin/agent_runner
|
||||
|
||||
# 只有在二进制文件存在时才复制(避免feature构建时的错误)
|
||||
RUN if [ -f /build/target/release/codex-acp-agent ]; then \
|
||||
cp /build/target/release/codex-acp-agent /app/bin/codex-acp-agent; \
|
||||
fi
|
||||
|
||||
# 设置执行权限
|
||||
RUN chmod +x /app/bin/rcoder /app/bin/agent_runner
|
||||
|
||||
|
||||
|
||||
# 使用构建参数破坏缓存(每次构建都获取最新版本的工具)
|
||||
ARG CACHEBUST=1
|
||||
|
||||
# agent install
|
||||
RUN echo "Cache bust: ${CACHEBUST}" && npm install -g claude-code-acp-ts@latest
|
||||
|
||||
# # 安装 claude-code-acp-ts(从源码构建,用于测试)
|
||||
# # 使用 npm pack + npm install -g <tarball> 确保复制文件而非 symlink
|
||||
# RUN echo "Cache bust: ${CACHEBUST}" && \
|
||||
# rm -rf /tmp/claude-code-acp-ts && \
|
||||
# git clone -b feat/claude-code-acp-ts https://github.com/nuwax-ai/claude-code-acp-ts.git /tmp/claude-code-acp-ts && \
|
||||
# cd /tmp/claude-code-acp-ts && npm install && npm run build && \
|
||||
# npm pack . && \
|
||||
# npm install -g ./claude-code-acp-ts-*.tgz && \
|
||||
# echo "验证 claude-code-acp-ts 安装..." && \
|
||||
# (which claude-code-acp-ts > /dev/null 2>&1 && echo "✅ claude-code-acp-ts 安装验证通过") || (echo "❌ claude-code-acp-ts 未正确安装" && exit 1)
|
||||
|
||||
|
||||
# 安装 nuwaxcode(频繁更新的工具,放在最终镜像以加快构建)
|
||||
# 使用 CACHEBUST 变量触发缓存失效
|
||||
RUN echo "Cache bust: ${CACHEBUST}" && npm i -g nuwaxcode@latest
|
||||
|
||||
# 安装 nuwax-file-server(文件服务器)
|
||||
RUN echo "Cache bust: ${CACHEBUST}" && npm i -g nuwax-file-server@latest
|
||||
|
||||
# 配置 nuwaxcode - 禁用 websearch, webfetch 和 question 权限
|
||||
RUN mkdir -p /root/.config/opencode && \
|
||||
echo '{\n "permission": {\n "websearch": "deny",\n "webfetch": "deny",\n "question": "deny"\n }\n}' > /root/.config/opencode/opencode.json
|
||||
|
||||
|
||||
|
||||
# 暴露端口
|
||||
EXPOSE 8086
|
||||
EXPOSE 60000
|
||||
EXPOSE 80
|
||||
149
qiming-rcoder/docker/rcoder-master/Dockerfile.base
Normal file
149
qiming-rcoder/docker/rcoder-master/Dockerfile.base
Normal file
@@ -0,0 +1,149 @@
|
||||
# ============================================================================
|
||||
# 基础镜像 Dockerfile - master-rcoder-base
|
||||
# ============================================================================
|
||||
# 此镜像包含所有运行时依赖、工具链和环境配置,不包含业务代码
|
||||
# 构建命令: make docker-build-master-base
|
||||
# 平时不需要重新构建,只有在修改系统依赖时才需要重新构建
|
||||
# ============================================================================
|
||||
|
||||
FROM node:22
|
||||
|
||||
# 设置环境变量
|
||||
ENV TZ=Asia/Shanghai
|
||||
|
||||
# 使用 LinuxMirrors 一键配置阿里云镜像源
|
||||
# 参考: https://github.com/SuperManito/LinuxMirrors
|
||||
# node:22 基础镜像已预装 curl,可直接执行脚本配置镜像源
|
||||
RUN curl -sSL https://linuxmirrors.cn/main.sh | bash -s -- \
|
||||
--source mirrors.aliyun.com \
|
||||
--protocol https \
|
||||
--use-intranet-source false \
|
||||
--install-epel false \
|
||||
--backup false \
|
||||
--upgrade-software false \
|
||||
--clean-cache true \
|
||||
--ignore-backup-tips
|
||||
|
||||
# 安装必要的运行时依赖(apt 包管理器)
|
||||
RUN apt-get update && apt-get install -y \
|
||||
ca-certificates \
|
||||
curl \
|
||||
wget \
|
||||
gnupg \
|
||||
unzip \
|
||||
zip \
|
||||
tzdata \
|
||||
lsof \
|
||||
iproute2 \
|
||||
net-tools \
|
||||
nginx \
|
||||
vim \
|
||||
fonts-liberation \
|
||||
lsb-release \
|
||||
xdg-utils \
|
||||
chromium \
|
||||
chromium-driver \
|
||||
&& rm -rf /var/lib/apt/lists/* \
|
||||
&& ln -snf /usr/share/zoneinfo/$TZ /etc/localtime \
|
||||
&& echo $TZ > /etc/timezone
|
||||
|
||||
# 复制 npmrc 配置镜像(使用国内镜像加速)
|
||||
COPY .npmrc /root/.npmrc
|
||||
|
||||
# 配置 npm 使用国内镜像源
|
||||
RUN npm config set registry https://registry.npmmirror.com
|
||||
|
||||
# 安装 pnpm
|
||||
RUN npm install -g pnpm
|
||||
|
||||
# 配置 pnpm 使用国内镜像源
|
||||
RUN pnpm config set registry https://registry.npmmirror.com
|
||||
|
||||
# 创建 pnpm 全局存储目录
|
||||
RUN mkdir -p /app/project_workspace/.pnpm-store
|
||||
|
||||
# 配置 pnpm 使用新的存储路径
|
||||
RUN pnpm config set store-dir /app/project_workspace/.pnpm-store
|
||||
|
||||
# 配置 yarn 使用国内镜像源
|
||||
RUN yarn config set registry https://registry.npmmirror.com
|
||||
|
||||
# 安装 vite(最新版本)
|
||||
RUN npm install -g vite@latest
|
||||
|
||||
# 安装额外的命令工具(强制更新到最新版本)
|
||||
RUN npm install -g @zed-industries/claude-code-acp@latest
|
||||
|
||||
# 安装 Claude CLI 工具(强制更新到最新版本)
|
||||
RUN npm install -g @anthropic-ai/claude-code@latest
|
||||
|
||||
# 安装 OpenAI Codex(强制更新到最新版本)
|
||||
RUN npm install -g @openai/codex@latest
|
||||
|
||||
# 安装 Chrome DevTools MCP(强制更新到最新版本)
|
||||
RUN npm install -g chrome-devtools-mcp@latest
|
||||
|
||||
# 安装 Bun(使用 npm 从阿里云 npmmirror 安装 prebuilt 二进制,避免从 bun.sh/install 走 GitHub 下载不稳定)
|
||||
# bun 的 npm 包通过 optionalDependencies 引入平台特定二进制(如 @oven/bun-linux-x64),npmmirror 已镜像
|
||||
# --prefix=/usr/local 让 bun 安装到 /usr/local/bin/bun(默认就在 PATH 中,无需额外 ENV)
|
||||
RUN npm install -g --prefix=/usr/local --registry=https://registry.npmmirror.com bun && \
|
||||
/usr/local/bin/bun --version
|
||||
|
||||
# Bun 运行时配置(bun install 使用的 registry)
|
||||
RUN echo '[install]' > /root/.bunfig.toml && \
|
||||
echo 'registry = "https://registry.npmmirror.com"' >> /root/.bunfig.toml && \
|
||||
echo '' >> /root/.bunfig.toml && \
|
||||
echo '[install.scopes]' >> /root/.bunfig.toml && \
|
||||
# @upstash scope 走 npmmirror 加速:bun 装 @upstash 包默认从这里下载,国内速度快
|
||||
# 注意:bun 与 npmmirror 的 tarball 校验存在偶发性 bug(#21493),但 docker build 期
|
||||
# 预装 context7-mcp 时已用 --registry npmjs.org 命令行参数覆盖,避开 build 失败
|
||||
# 运行时 bunx @upstash/context7-mcp 会命中 ~/.bun/install/global 全局缓存,无需再下载
|
||||
echo '"@upstash" = "https://registry.npmmirror.com"' >> /root/.bunfig.toml
|
||||
|
||||
# 安装 python3-pip(node:22 基础镜像默认不带 Python,用 pip 装 uv 需要先安装)
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends python3 python3-pip && \
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# 安装 uv 并验证(使用 pip 从阿里云 PyPI 镜像安装,避免从 astral.sh 下载二进制慢/不稳定)
|
||||
# --break-system-packages: Debian 12+ 的 PEP 668 限制,系统 Python 默认不允许直接 pip install
|
||||
RUN pip3 install --break-system-packages --no-cache-dir -i https://mirrors.aliyun.com/pypi/simple uv && \
|
||||
uv --version || (echo "ERROR: uv installation failed" && exit 1)
|
||||
|
||||
# 设置 uv 镜像加速地址
|
||||
ENV UV_INDEX_URL=https://mirrors.aliyun.com/pypi/simple
|
||||
|
||||
# 设置工作目录
|
||||
WORKDIR /app
|
||||
|
||||
# 创建 bin 目录
|
||||
RUN mkdir -p /app/bin
|
||||
|
||||
# 预安装 MCP 工具到 uv 缓存中
|
||||
RUN uv tool install mcp-server-fetch
|
||||
|
||||
# 预安装 context7 MCP 工具(Bun 全局安装)
|
||||
# 代码里通过 bunx 调用 MCP,预装到 bun 全局后 bunx 可直接命中 ~/.bun/install/global,无需运行时再下载
|
||||
RUN bun add -g @upstash/context7-mcp
|
||||
|
||||
# 创建 Claude Code 配置目录并禁用 webfetch 功能,添加 MCP 工具
|
||||
RUN mkdir -p /root/.claude && \
|
||||
echo '{"permissions":{"deny":["WebFetch", "WebSearch"]},"mcpServers":{"mcp-fetch":{"args":["mcp-server-fetch"],"command":"uvx"},"chrome-devtools":{"command":"npx","args":["-y","chrome-devtools-mcp@latest"]}}}' > /root/.claude/settings.json
|
||||
|
||||
# 添加所有工具安装路径到 PATH
|
||||
ENV PNPM_HOME="/home/user/.local/share/pnpm" \
|
||||
PATH="/app/bin:/root/.local/bin:/home/user/.local/bin:/home/user/.local/share/pnpm:/opt/cargo/bin:/usr/local/bin:${PATH}"
|
||||
|
||||
|
||||
# 清理默认nginx配置以避免冲突
|
||||
RUN rm -f /etc/nginx/sites-enabled/default && \
|
||||
rm -f /etc/nginx/conf.d/default.conf
|
||||
|
||||
# ============================================================================
|
||||
# 基础镜像构建完成
|
||||
# ============================================================================
|
||||
# 此处不设置 ENTRYPOINT/CMD,由最终镜像设置
|
||||
|
||||
# 暴露端口(供最终镜像使用)
|
||||
EXPOSE 8086
|
||||
EXPOSE 60000
|
||||
EXPOSE 80
|
||||
20
qiming-rcoder/docker/start-rcoder.sh
Normal file
20
qiming-rcoder/docker/start-rcoder.sh
Normal file
@@ -0,0 +1,20 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
echo "🚀 启动 RCoder 服务..."
|
||||
|
||||
# 设置环境变量
|
||||
export RUST_LOG=${RUST_LOG:-info}
|
||||
export RCODER_PORT=${RCODER_PORT:-8087}
|
||||
|
||||
# 创建必要的目录
|
||||
mkdir -p /app/logs /app/project_workspace
|
||||
|
||||
echo "🔧 环境配置:"
|
||||
echo " RUST_LOG: $RUST_LOG"
|
||||
echo " RCODER_PORT: $RCODER_PORT"
|
||||
echo " DOCKER_SOCKET_PATH: $DOCKER_SOCKET_PATH"
|
||||
|
||||
# 启动 rcoder 服务
|
||||
echo "📡 启动 rcoder 服务 (端口: $RCODER_PORT)..."
|
||||
exec /app/bin/rcoder --port "$RCODER_PORT"
|
||||
233
qiming-rcoder/docker/test-page/README.md
Normal file
233
qiming-rcoder/docker/test-page/README.md
Normal file
@@ -0,0 +1,233 @@
|
||||
# VNC + 音频 + 输入法 集成测试
|
||||
|
||||
这是一个用于测试 RCoder Computer Agent 功能的集成测试页面,支持:
|
||||
- 🖥️ **VNC 远程桌面** - 可视化访问容器桌面环境
|
||||
- 🎵 **音频流播放** - 实时播放容器内的音频输出
|
||||
- ⌨️ **输入法透传** - 使用本地输入法(如中文)输入到远程桌面
|
||||
|
||||
## 快速开始
|
||||
|
||||
### 1. 启动测试服务
|
||||
|
||||
```bash
|
||||
# 进入测试目录
|
||||
cd test-page
|
||||
|
||||
# 启动 HTTP 服务器(Python 3)
|
||||
python3 -m http.server 8000
|
||||
```
|
||||
|
||||
### 2. 访问测试页面
|
||||
|
||||
在浏览器中打开:
|
||||
```
|
||||
http://127.0.0.1:8000/vnc-test.html
|
||||
```
|
||||
|
||||
### 3. 测试前准备
|
||||
|
||||
确保 RCoder 服务正在运行:
|
||||
```bash
|
||||
# 检查服务状态(默认端口 8087)
|
||||
curl http://127.0.0.1:8087/health
|
||||
```
|
||||
|
||||
如果服务未运行,请先启动 RCoder:
|
||||
```bash
|
||||
make dev-up
|
||||
```
|
||||
|
||||
## 使用指南
|
||||
|
||||
### 第一步:创建测试容器
|
||||
|
||||
在测试页面的聊天输入框中输入任意消息(如 "hello"),点击"发送聊天消息"。
|
||||
|
||||
这会自动创建一个 Computer Agent 容器,包含完整的桌面环境。
|
||||
|
||||
### 第二步:测试 VNC 远程桌面
|
||||
|
||||
1. 点击页面上的 **"打开 VNC 桌面"** 按钮
|
||||
2. 在新打开的标签页中,可以看到远程桌面界面
|
||||
3. 使用鼠标和键盘与远程桌面交互
|
||||
|
||||
### 第三步:测试音频流
|
||||
|
||||
1. 确保浏览器控制台显示:`[Audio] ✅ OpusDecoder 加载成功`
|
||||
2. 点击 **"启动音频流"** 按钮
|
||||
3. 在容器内播放音频(如播放 YouTube 视频)
|
||||
4. 应该能在本地听到音频输出
|
||||
|
||||
### 第四步:测试输入法透传
|
||||
|
||||
1. 点击 **"连接 IME 服务"** 按钮
|
||||
2. 等待连接成功提示:`[IME] WebSocket 已连接`
|
||||
3. 在输入框中切换到中文输入法(或其他输入法)
|
||||
4. 输入中文文本
|
||||
5. 点击 **"发送文本"**
|
||||
6. 在 VNC 桌面中打开文本编辑器,应该能看到输入的中文
|
||||
|
||||
## 界面说明
|
||||
|
||||
### 连接配置
|
||||
|
||||
**RCoder 代理模式**(推荐使用):
|
||||
- **RCoder 服务地址**: 默认 `http://127.0.0.1:8087`
|
||||
- **User ID**: 测试用户名,如 `user_123`
|
||||
- **Project ID**: 可选,留空则自动生成
|
||||
|
||||
**直接端口模式**(仅开发调试):
|
||||
- 需要手动配置容器端口映射
|
||||
- 一般用户不需要使用此模式
|
||||
|
||||
### 控制按钮
|
||||
|
||||
| 按钮 | 功能 |
|
||||
|------|------|
|
||||
| 打开 VNC 桌面 | 在新标签页打开远程桌面 |
|
||||
| 启动音频流 | 开始接收和播放容器音频 |
|
||||
| 停止音频流 | 停止音频播放 |
|
||||
| 连接 IME 服务 | 建立输入法透传连接 |
|
||||
| 断开 IME 服务 | 断开输入法连接 |
|
||||
| 发送聊天消息 | 创建容器或与 Agent 对话 |
|
||||
|
||||
## 常见问题
|
||||
|
||||
### Q: 提示 "OpusDecoder 加载失败"
|
||||
|
||||
**A**: 这是浏览器加载音频解码库失败,不影响 VNC 和输入法功能。
|
||||
|
||||
如果需要测试音频功能,请确保:
|
||||
1. `opus-decoder.min.js` 文件与 `vnc-test.html` 在同一目录
|
||||
2. 使用 HTTP 服务器访问,不要直接双击 HTML 文件
|
||||
|
||||
### Q: IME 连接失败
|
||||
|
||||
**A**: 可能的原因:
|
||||
1. **容器未创建** - 先发送聊天消息创建容器
|
||||
2. **Project ID 错误** - 确保使用正确的 Project ID
|
||||
3. **服务未启动** - 检查 RCoder 服务是否正常运行
|
||||
|
||||
解决方法:
|
||||
```bash
|
||||
# 查看容器列表(Computer Agent 容器前缀为 computer-agent-runner-)
|
||||
docker ps | grep computer-agent-runner
|
||||
|
||||
# 查看服务日志
|
||||
make dev-logs
|
||||
```
|
||||
|
||||
### Q: VNC 桌面显示空白或连接失败
|
||||
|
||||
**A**: 可能的原因:
|
||||
1. 容器正在启动中 - 等待 10-20 秒后重试
|
||||
2. User ID 或 Project ID 不匹配 - 检查配置是否正确
|
||||
3. VNC 服务未启动 - 查看容器日志确认服务状态
|
||||
|
||||
解决方法:
|
||||
```bash
|
||||
# 查看容器日志
|
||||
docker logs <container_id>
|
||||
|
||||
# 重启容器
|
||||
docker restart <container_id>
|
||||
```
|
||||
|
||||
### Q: 音频有延迟或卡顿
|
||||
|
||||
**A**: 这是正常现象,原因:
|
||||
1. 音频需要编码传输(容器 → 浏览器)
|
||||
2. 网络延迟影响播放实时性
|
||||
3. Opus 编解码需要时间
|
||||
|
||||
正常延迟在 1-3 秒范围内是可接受的。
|
||||
|
||||
### Q: 输入法输入的文本显示乱码
|
||||
|
||||
**A**: 可能的原因:
|
||||
1. 字符编码问题 - 确保使用 UTF-8 编码
|
||||
2. 容器内字体缺失 - 容器默认包含中文字体
|
||||
|
||||
## 技术支持
|
||||
|
||||
如果遇到其他问题:
|
||||
|
||||
1. **查看浏览器控制台** (F12)
|
||||
- 查看错误信息和警告
|
||||
- 截图错误信息便于定位问题
|
||||
|
||||
2. **查看容器状态**
|
||||
```bash
|
||||
docker ps -a | grep computer-agent-runner
|
||||
docker logs <container_id>
|
||||
```
|
||||
|
||||
3. **查看服务日志**
|
||||
```bash
|
||||
make dev-logs
|
||||
```
|
||||
|
||||
4. **联系开发团队**
|
||||
- 提供详细的错误信息和操作步骤
|
||||
- 附上浏览器控制台截图
|
||||
- 说明使用的操作系统和浏览器版本
|
||||
|
||||
## 系统要求
|
||||
|
||||
### 浏览器要求
|
||||
|
||||
| 浏览器 | 最低版本 | 说明 |
|
||||
|--------|---------|------|
|
||||
| Chrome | 90+ | 推荐使用 |
|
||||
| Firefox | 88+ | 完全支持 |
|
||||
| Safari | 14+ | macOS / iOS |
|
||||
| Edge | 90+ | 完全支持 |
|
||||
|
||||
**注意**: 不支持 Internet Explorer
|
||||
|
||||
### 网络要求
|
||||
|
||||
- 浏览器能访问 `http://127.0.0.1:8087`
|
||||
- WebSocket 连接正常(防火墙不阻止)
|
||||
- 建议使用有线网络或稳定的 Wi-Fi
|
||||
|
||||
## 功能演示
|
||||
|
||||
### 演示 1:远程桌面操作
|
||||
|
||||
1. 创建容器
|
||||
2. 打开 VNC 桌面
|
||||
3. 在远程桌面中打开文件管理器、浏览器等应用
|
||||
4. 观察操作是否流畅
|
||||
|
||||
### 演示 2:音频播放
|
||||
|
||||
1. 创建容器并打开 VNC 桌面
|
||||
2. 在远程桌面中打开 YouTube
|
||||
3. 播放一段视频
|
||||
4. 启动音频流
|
||||
5. 验证是否能听到音频
|
||||
|
||||
### 演示 3:中文输入
|
||||
|
||||
1. 创建容器并打开 VNC 桌面
|
||||
2. 在远程桌面中打开文本编辑器(如 gedit)
|
||||
3. 连接 IME 服务
|
||||
4. 使用本地中文输入法输入一段文字
|
||||
5. 发送到远程桌面
|
||||
6. 验证文字是否正确显示
|
||||
|
||||
## 版本信息
|
||||
|
||||
- **测试页面版本**: v1.0
|
||||
- **最后更新**: 2026-01-17
|
||||
- **支持的 RCoder 版本**: v1.0.0+
|
||||
|
||||
## 反馈与建议
|
||||
|
||||
如果您在使用过程中有任何问题或建议,欢迎反馈!
|
||||
|
||||
---
|
||||
|
||||
**提示**: 首次使用建议按照"使用指南"的步骤逐步操作,熟悉各个功能后再进行综合测试。
|
||||
人民大团结
|
||||
194
qiming-rcoder/docker/test-page/opus-decoder.min.js
vendored
Normal file
194
qiming-rcoder/docker/test-page/opus-decoder.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
1385
qiming-rcoder/docker/test-page/vnc-test.html
Normal file
1385
qiming-rcoder/docker/test-page/vnc-test.html
Normal file
File diff suppressed because it is too large
Load Diff
BIN
qiming-rcoder/docker/wallpaper/wallpaper.jpeg
Normal file
BIN
qiming-rcoder/docker/wallpaper/wallpaper.jpeg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 368 KiB |
Reference in New Issue
Block a user