-
Notifications
You must be signed in to change notification settings - Fork 14.7k
feat: 支持自托管的 remote-control-server #214
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
c155f30
e4e1e9c
e020f39
0afac98
495ce88
855cc4b
e508941
333ea0b
3304558
38d5e16
1446742
324103e
b1aaefa
d407d39
8c1fb25
ab67e36
442f639
0f86c66
821ad5d
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,11 @@ | ||
| node_modules | ||
| dist | ||
| .git | ||
| .githooks | ||
| .github | ||
| docs | ||
| *.md | ||
| packages/remote-control-server/data/*.db | ||
| packages/remote-control-server/data/*.db-wal | ||
| packages/remote-control-server/data/*.db-shm | ||
| .claude |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,75 @@ | ||
| name: Release RCS Docker Image | ||
|
|
||
| on: | ||
| push: | ||
| tags: | ||
| - 'rcs-v*' | ||
|
|
||
| env: | ||
| REGISTRY: ghcr.io | ||
| IMAGE_NAME: ${{ github.repository_owner }}/remote-control-server | ||
|
|
||
| jobs: | ||
| build-and-push: | ||
| runs-on: ubuntu-latest | ||
| permissions: | ||
| contents: read | ||
| packages: write | ||
|
|
||
| steps: | ||
| - uses: actions/checkout@v4 | ||
|
|
||
| - name: Login to GHCR | ||
| uses: docker/login-action@v3 | ||
| with: | ||
| registry: ${{ env.REGISTRY }} | ||
| username: ${{ github.actor }} | ||
| password: ${{ secrets.GITHUB_TOKEN }} | ||
|
|
||
| - name: Set up Docker Buildx | ||
| uses: docker/setup-buildx-action@v3 | ||
|
|
||
| - name: Extract version | ||
| id: version | ||
| run: echo "VERSION=${GITHUB_REF_NAME#rcs-v}" >> "$GITHUB_OUTPUT" | ||
|
|
||
| - name: Generate tags | ||
| id: tags | ||
| run: | | ||
| VERSION="${{ steps.version.outputs.VERSION }}" | ||
| IMAGE="${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}" | ||
| TAGS="${IMAGE}:${VERSION}" | ||
| IFS='.' read -r MAJOR MINOR PATCH <<< "$VERSION" | ||
| if [ -n "$MAJOR" ] && [ -n "$MINOR" ]; then | ||
| TAGS="${TAGS},${IMAGE}:${MAJOR}.${MINOR}" | ||
| fi | ||
| TAGS="${TAGS},${IMAGE}:latest" | ||
| echo "tags=$TAGS" >> "$GITHUB_OUTPUT" | ||
|
|
||
| - name: Build Docker image | ||
| uses: docker/build-push-action@v5 | ||
| with: | ||
| context: . | ||
| file: packages/remote-control-server/Dockerfile | ||
| push: false | ||
| load: true | ||
| tags: ${{ steps.tags.outputs.tags }} | ||
| build-args: VERSION=${{ steps.version.outputs.VERSION }} | ||
| cache-from: type=gha | ||
| cache-to: type=gha,mode=max | ||
|
|
||
| - name: Verify image | ||
| run: | | ||
| IMAGE_TAG=$(echo "${{ steps.tags.outputs.tags }}" | cut -d',' -f1) | ||
| docker run -d --name rcs-test -p 3000:3000 "$IMAGE_TAG" | ||
| sleep 5 | ||
| curl -sf http://localhost:3000/health || { docker logs rcs-test; exit 1; } | ||
| docker stop rcs-test | ||
| docker rm rcs-test | ||
|
|
||
| - name: Push Docker image | ||
| run: | | ||
| IFS=',' read -ra TAGS <<< "${{ steps.tags.outputs.tags }}" | ||
| for TAG in "${TAGS[@]}"; do | ||
| docker push "$TAG" | ||
| done |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -27,3 +27,5 @@ src/utils/vendor/ | |
| __pycache__/ | ||
| *.pyc | ||
| logs | ||
|
|
||
| data | ||
Large diffs are not rendered by default.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| data |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,32 @@ | ||
| # ---- Stage 1: Install deps + build ---- | ||
| FROM oven/bun:1 AS builder | ||
| WORKDIR /app | ||
|
|
||
| ARG VERSION=0.1.0 | ||
|
|
||
| COPY packages/remote-control-server/package.json ./package.json | ||
| RUN bun install | ||
|
|
||
| COPY packages/remote-control-server/src ./src | ||
| RUN bun build src/index.ts --outfile=dist/server.js --target=bun \ | ||
| --define "process.env.RCS_VERSION=\"${VERSION}\"" | ||
|
|
||
| # ---- Stage 2: Runtime ---- | ||
| FROM oven/bun:1-slim AS runtime | ||
|
|
||
| ARG VERSION=0.1.0 | ||
| ENV RCS_VERSION=${VERSION} | ||
|
|
||
| WORKDIR /app | ||
|
|
||
| COPY --from=builder /app/dist/server.js ./dist/server.js | ||
| COPY packages/remote-control-server/web ./web | ||
|
|
||
| VOLUME /app/data | ||
|
|
||
| EXPOSE 3000 | ||
|
|
||
| HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \ | ||
| CMD bun run -e "fetch('http://localhost:3000/health').then(r => r.ok ? process.exit(0) : process.exit(1)).catch(() => process.exit(1))" | ||
|
|
||
| CMD ["bun", "run", "dist/server.js"] | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,167 @@ | ||
| # Remote Control Server (RCS) | ||
|
|
||
| Remote Control Server 是 Claude Code 的远程控制后端,允许你通过浏览器 Web UI 远程监控和操作 Claude Code 会话。 | ||
|
|
||
| ## 功能 | ||
|
|
||
| - **会话管理** — 创建、监控、归档 Claude Code 会话 | ||
| - **实时消息流** — WebSocket / SSE 双向传输,实时查看对话和工具调用 | ||
| - **权限审批** — 在 Web UI 中审批 Claude Code 的工具权限请求 | ||
| - **多环境管理** — 注册多个运行环境,支持心跳和断线重连 | ||
| - **认证安全** — API Key + JWT 双层认证 | ||
|
|
||
| ## 快速开始 | ||
|
|
||
| ### Docker 部署(推荐) | ||
|
|
||
| ```bash | ||
| docker run -d \ | ||
| --name rcs \ | ||
| -p 3000:3000 \ | ||
| -e RCS_API_KEYS=your-api-key-here \ | ||
| -v rcs-data:/app/data \ | ||
| ghcr.io/claude-code-best/remote-control-server:latest | ||
|
Comment on lines
+22
to
+23
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Clarify/remove persistent volume examples for an in-memory server. Line [22]-[23] and Line [95]-[99] suggest persistent storage, but Line [149] says data is in-memory and cleared on restart. This is confusing for deployment expectations. Also applies to: 95-99, 149-149 🤖 Prompt for AI Agents |
||
| ``` | ||
|
|
||
| ## 环境变量 | ||
|
|
||
| ### 服务器配置 | ||
|
|
||
| | 变量 | 默认值 | 说明 | | ||
| |------|--------|------| | ||
| | `RCS_PORT` | `3000` | 监听端口 | | ||
| | `RCS_HOST` | `0.0.0.0` | 监听地址 | | ||
| | `RCS_API_KEYS` | _(空)_ | API 密钥列表,逗号分隔。客户端和 Worker 连接时需要提供 | | ||
| | `RCS_BASE_URL` | _(自动)_ | 外部访问地址,例如 `https://rcs.example.com`。用于生成 WebSocket 连接 URL | | ||
| | `RCS_VERSION` | `0.1.0` | 服务版本号,显示在 `/health` 响应中 | | ||
|
|
||
| ### 超时与心跳 | ||
|
|
||
| | 变量 | 默认值 | 说明 | | ||
| |------|--------|------| | ||
| | `RCS_POLL_TIMEOUT` | `8` | V1 轮询超时(秒) | | ||
| | `RCS_HEARTBEAT_INTERVAL` | `20` | 心跳间隔(秒) | | ||
| | `RCS_JWT_EXPIRES_IN` | `3600` | JWT 令牌有效期(秒) | | ||
| | `RCS_DISCONNECT_TIMEOUT` | `300` | 断线判定超时(秒) | | ||
|
|
||
| ## Claude Code 客户端配置 | ||
|
|
||
| ### 连接到自托管服务器 | ||
|
|
||
| 在 Claude Code 所在环境设置以下变量: | ||
|
|
||
| ```bash | ||
| # 指向你的 RCS 服务器地址 | ||
| export CLAUDE_BRIDGE_BASE_URL="https://rcs.example.com" | ||
|
|
||
| # 认证令牌(与 RCS_API_KEYS 中的值对应) | ||
| export CLAUDE_BRIDGE_OAUTH_TOKEN="your-api-key-here" | ||
| ``` | ||
|
|
||
| 然后启动远程控制模式: | ||
|
|
||
| ```bash | ||
| ccb --remote-control | ||
| ``` | ||
|
|
||
| > **注意**:远程控制功能需要启用 `BRIDGE_MODE` feature flag。开发模式下默认启用。 | ||
|
|
||
| ### 环境变量参考 | ||
|
|
||
| | 变量 | 说明 | | ||
| |------|------| | ||
| | `CLAUDE_BRIDGE_BASE_URL` | RCS 服务器地址,覆盖默认的 Anthropic 云端地址 | | ||
| | `CLAUDE_BRIDGE_OAUTH_TOKEN` | 认证令牌,用于连接 RCS 服务器 | | ||
| | `CLAUDE_BRIDGE_SESSION_INGRESS_URL` | WebSocket 入口地址(默认与 BASE_URL 相同) | | ||
| | `CLAUDE_CODE_REMOTE` | 设为 `1` 时标记为远程执行模式 | | ||
|
|
||
| ## Docker Compose 示例 | ||
|
|
||
| ```yaml | ||
| version: "3.8" | ||
| services: | ||
| rcs: | ||
| build: | ||
| context: . | ||
| dockerfile: packages/remote-control-server/Dockerfile | ||
| args: | ||
| VERSION: "0.1.0" | ||
| ports: | ||
| - "3000:3000" | ||
| environment: | ||
| - RCS_API_KEYS=sk-rcs-change-me | ||
| - RCS_BASE_URL=https://rcs.example.com | ||
| volumes: | ||
| - rcs-data:/app/data | ||
| restart: unless-stopped | ||
|
|
||
| volumes: | ||
| rcs-data: | ||
| ``` | ||
|
|
||
| ## 反向代理配置 | ||
|
|
||
| 使用 Nginx 或 Caddy 反向代理时,需要支持 WebSocket 升级: | ||
|
|
||
| ```nginx | ||
| server { | ||
| listen 443 ssl; | ||
| server_name rcs.example.com; | ||
|
|
||
| location / { | ||
| proxy_pass http://127.0.0.1:3000; | ||
| proxy_http_version 1.1; | ||
| proxy_set_header Upgrade $http_upgrade; | ||
| proxy_set_header Connection "upgrade"; | ||
| proxy_set_header Host $host; | ||
| proxy_set_header X-Real-IP $remote_addr; | ||
| proxy_read_timeout 86400s; | ||
| } | ||
| } | ||
| ``` | ||
|
|
||
| Caddy 配置更简单,自动处理 WebSocket: | ||
|
|
||
| ``` | ||
| rcs.example.com { | ||
| reverse_proxy localhost:3000 | ||
| } | ||
| ``` | ||
|
Comment on lines
+125
to
+129
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Add language identifiers to fenced code blocks (MD040). Line [125] and Line [133] use unlabeled fences; markdownlint flags this and it hurts renderer/tooling behavior. Suggested patch-```
+```caddyfile
rcs.example.com {
reverse_proxy localhost:3000
}@@ Also applies to: 133-146 🧰 Tools🪛 markdownlint-cli2 (0.22.0)[warning] 125-125: Fenced code blocks should have a language specified (MD040, fenced-code-language) 🤖 Prompt for AI Agents |
||
|
|
||
| ## 架构概览 | ||
|
|
||
| ``` | ||
| ┌─────────────┐ WebSocket/SSE ┌──────────────────┐ | ||
| │ Claude Code │ ◄──────────────────► │ Remote Control │ | ||
| │ (Bridge CLI)│ HTTP API │ Server │ | ||
| └─────────────┘ │ │ | ||
| │ ┌────────────┐ │ | ||
| ┌─────────────┐ HTTP/SSE │ │ Event Bus │ │ | ||
| │ Web UI │ ◄────────────────── │ └────────────┘ │ | ||
| │ (/code/*) │ │ ┌────────────┐ │ | ||
| └─────────────┘ │ │ In-Memory │ │ | ||
| │ │ Store │ │ | ||
| │ └────────────┘ │ | ||
| └──────────────────┘ | ||
| ``` | ||
|
|
||
| - **传输层**:WebSocket(V1)和 SSE + HTTP POST(V2) | ||
| - **存储**:纯内存存储(Map),重启后数据清除 | ||
| - **认证**:API Key(客户端)+ JWT(Worker) | ||
| - **前端**:原生 JS SPA,通过 `/code/*` 路径访问 | ||
|
|
||
| ## 开发 | ||
|
|
||
| ```bash | ||
| # 安装依赖 | ||
| bun install | ||
|
|
||
| # 开发模式(热重载) | ||
| bun run dev | ||
|
|
||
| # 类型检查 | ||
| bun run typecheck | ||
|
|
||
| # 运行测试 | ||
| bun test packages/remote-control-server/ | ||
| ``` | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,27 @@ | ||
| { | ||
| "name": "@anthropic/remote-control-server", | ||
| "version": "0.1.0", | ||
| "type": "module", | ||
| "scripts": { | ||
| "dev": "bun run --watch src/index.ts", | ||
| "start": "bun run src/index.ts", | ||
| "build:web": "cd web && bun run build", | ||
| "typecheck": "tsc --noEmit" | ||
| }, | ||
| "dependencies": { | ||
| "hono": "^4.7.0", | ||
| "uuid": "^11.0.0" | ||
| }, | ||
| "devDependencies": { | ||
| "@types/uuid": "^10.0.0", | ||
| "typescript": "^5.7.0", | ||
| "vite": "^6.0.0", | ||
| "@vitejs/plugin-react": "^4.0.0", | ||
| "react": "^19.0.0", | ||
| "react-dom": "^19.0.0", | ||
| "@types/react": "^19.0.0", | ||
| "@types/react-dom": "^19.0.0", | ||
| "tailwindcss": "^4.0.0", | ||
| "@tailwindcss/vite": "^4.0.0" | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
🌐 Web query:
In the officialoven/bun:1-slimimage, isbuna valid non-root user, and what is the recommended least-privilege pattern for production containers?💡 Result:
Yes. In the
oven/bunslim images,bunis a real non-root account created in the image (e.g.,groupadd bun --gid 1000 && useradd bun --uid 1000 ... --create-home), with the working directory set under/home/bun/app. [1] Bun’s own Docker guide also usesUSER bunin the final (“release”) stage, implying this user is intended to be used for runtime. [2]Recommended least-privilege pattern for production containers
USER bun, or your own dedicated runtime user) and ensure files are owned appropriately (typically viaCOPY --chown=...). [2]Sources: [1] [2] [3]
Run runtime stage as non-root user
The runtime image does not set a non-root
USER, which is a container hardening gap matching the static finding (DS-0002). Theoven/bun:1-slimbase image includes thebunnon-root user, and usingUSER bunis the recommended least-privilege pattern for production.Proposed fix
🤖 Prompt for AI Agents