添加qiming-rcoder模块

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

View File

@@ -0,0 +1,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

View 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"

View 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:

View File

@@ -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

View File

@@ -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 查看性能剖析数据

View 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']

View File

@@ -0,0 +1 @@
registry=https://registry.npmmirror.com/

View 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 "📦 安装 jqJSON 处理)..."; \
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"]

View 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-x64npmmirror 已镜像
# --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-fetchUV 全局安装到系统目录)
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

View 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}

View 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"

View 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 "📦 安装 jqJSON 处理)..."; \
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"]

View File

@@ -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"]

View File

@@ -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"]

View File

@@ -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"]

View 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 桥接)

View 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` 环境变量。
---
## 联系方式
如有问题,请联系后端开发团队。

View 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` 用户的 UID1000保持一致
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 内核检查进程的 UID1000是否匹配文件的 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** (初始版本): 基础容器镜像

View 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.")

View 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>

Binary file not shown.

View File

@@ -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 \
"$@"

View 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 \
"$@"

View File

@@ -0,0 +1,14 @@
{
"argv": [
"/usr/bin/deno",
"jupyter",
"--kernel",
"--conn",
"{connection_file}"
],
"display_name": "Deno",
"env": {
"NO_COLOR": "1"
},
"language": "typescript"
}

View 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`

View File

@@ -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",
}
}

View File

@@ -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

View 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

View File

@@ -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

View File

@@ -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

View File

@@ -0,0 +1,2 @@
pref("general.config.filename", "firefox.cfg");
pref("general.config.obscure_value", 0);

View File

@@ -0,0 +1,7 @@
{
"policies": {
"DisableFirstRunPage": true,
"OverrideFirstRunPage": "",
"OverridePostUpdatePage": ""
}
}

View 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);

View 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 "=========================================="

View 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; }
};
})();

View File

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

View File

@@ -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"
]
}
}
}

View File

@@ -0,0 +1,9 @@
{
"name": "@e2b/code-interpreter-template",
"private": true,
"version": "0.1.0",
"scripts": {
"lint": "ruff check .",
"format": "ruff format ."
}
}

View 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

View 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
}
}

View 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>

View File

@@ -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

File diff suppressed because it is too large Load Diff

View File

@@ -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
)

View File

@@ -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

Binary file not shown.

After

Width:  |  Height:  |  Size: 368 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 MiB

View 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>

View 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

View File

@@ -0,0 +1 @@
registry=https://registry.npmmirror.com/

View 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}
# ============================================================================
# 第二阶段:运行阶段(基于基础镜像)
# ============================================================================
# 重新声明 ARGFROM 之后需要重新声明才能使用)
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

View 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-x64npmmirror 已镜像
# --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-pipnode: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

View 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"

View 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+
## 反馈与建议
如果您在使用过程中有任何问题或建议,欢迎反馈!
---
**提示**: 首次使用建议按照"使用指南"的步骤逐步操作,熟悉各个功能后再进行综合测试。
人民大团结

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

Binary file not shown.

After

Width:  |  Height:  |  Size: 368 KiB