diff --git a/README.md b/README.md index 10f4c32d..007d0360 100644 --- a/README.md +++ b/README.md @@ -41,32 +41,51 @@ -Claude Code Router Desktop is a local gateway and desktop control panel for routing agent requests from Claude Code, Codex, ZCode, and compatible clients to the model provider you actually want to use. +Claude Code Router Desktop is a local control plane for coding agents. It gives Claude Code, Codex, Grok CLI, ZCode, and compatible API clients one stable local endpoint, then lets you decide which provider, model, routing policy, tool stack, and account should handle each request. + +Instead of wiring every agent to every model service by hand, CCR centralizes the model layer on your own machine: provider presets, custom endpoints, credential pools, fallback chains, Fusion-enhanced models, MCP tools, request logs, account usage, and desktop launch profiles all live in one app.

Claude Code Router Desktop screenshot

+## What CCR Helps You Do + +| Goal | CCR gives you | +| --- | --- | +| Keep the same agent workflow while switching models | Local profiles for Claude Code, Codex, Grok CLI, and ZCode, with CLI/app launch entries and per-profile model selection | +| Try many providers without rebuilding config every time | Built-in provider presets, custom OpenAI/Anthropic/Gemini-compatible endpoints, protocol probing, model discovery, and connectivity checks | +| Make routing a runtime policy | Built-in agent routing, conditional rules, request rewrites, model-prefix routing, retries, and fallback model chains | +| Control cost and quota pressure | Credential pools, key rotation, local usage limits, account balance snapshots, token/cost dashboards, and tray status | +| Upgrade a model without replacing it | Fusion models that add vision, web search, or selected MCP tools to an existing base model | +| Keep large tool sets usable | ToolHub, a compact MCP entry point that lets agents resolve and invoke the tools needed for the current task | +| Debug what actually happened | Request logs, resolved provider/model fields, latency, token usage, estimated cost, network capture, and agent observability | + ## Why Use CCR -- Use one local endpoint for multiple agent tools instead of configuring every client separately. -- Route requests with default routing, conditional rules, fallback targets, and request rewrites instead of editing client configuration by hand. -- Mix providers without changing your workflow. CCR supports OpenAI-compatible APIs, Anthropic Messages, Gemini Generate Content, OpenRouter, DeepSeek, SiliconFlow, Moonshot, Kimi Code, Mistral, Z.AI, Bailian, and custom providers. -- Control cost and reliability with fallback routing, API key rotation, usage statistics, and request logs. +- **One gateway for your agent stack**: point clients at CCR once, then move routing, models, keys, and providers from scattered client configs into a single desktop UI. +- **Provider freedom without workflow churn**: use OpenAI Chat/Responses, Anthropic Messages, Gemini Generate Content/Interactions, OpenRouter, DeepSeek, SiliconFlow, Moonshot, Kimi Code, Mistral, Z.AI, Bailian, and custom compatible providers. +- **Reliability policies you can see and change**: define when a request should be rewritten, retried, or moved to another model, then verify the result in local logs. +- **Operational visibility for AI work**: track requests, tokens, cost estimates, success rate, latency, model distribution, provider usage, and account balances from the dashboard or tray. +- **Agent-native tools and extensions**: add Fusion capabilities, expose dynamic MCP tools through ToolHub, automate the built-in browser, relay agents through IM bots, or install local extensions. -## Features +## Feature Highlights -- **Overview dashboard**: inspect system status, usage widgets, account balances, model distribution, and share cards. -- **Provider management**: add provider presets or custom endpoints, probe protocol support, test model connectivity, manage credentials, and monitor supported account balances where available. -- **Routing rules**: configure default routing, conditional and model-prefix rules, fallback handling, and request rewrites. -- **Agent Config**: configure Claude Code, Codex, and ZCode launch entries, models, scopes, and multi-instance app profiles. -- **Gateway compatibility**: translate supported client requests through the local CCR model gateway. -- **Proxy mode**: capture supported API traffic through a local proxy with optional system proxy integration and network capture. -- **Fusion models**: combine a base model with vision, web search, or MCP tools into a reusable selectable model. +- **Agent profiles**: create profiles for Claude Code, Codex, Grok CLI, and ZCode with model overrides, scopes, CLI/app launch surfaces, environment settings, and multi-instance app workflows. +- **Provider management**: add preset providers or custom endpoints; probe supported protocols; detect model lists; run real connectivity checks; manage single keys or credential pools; import local agent login state where supported. +- **Model catalog**: search all configured models, edit model descriptions, and use those descriptions to guide Claude Code subagent, Task, and Workflow model selection. +- **Routing engine**: combine built-in agent routing, request-header/body conditions, model-prefix routing, request rewrites, retry policy, and ordered fallback targets. +- **Fusion models**: publish reusable virtual models that keep a base model's behavior while adding vision, hosted web search, or selected MCP tools. +- **ToolHub**: merge multiple MCP servers into one dynamic MCP server so agents can resolve tools only when a task needs them; desktop builds can also expose built-in browser automation and Chrome login-state import. +- **API keys and quotas**: create CCR client keys with expiration and local request/token/image limits, separate from upstream provider credentials. +- **Logs and observability**: inspect request/response details, resolved provider and model, credential, status, latency, token usage, estimated cost, tool calls, and agent execution traces. +- **Proxy and networking**: run CCR as a local HTTP/HTTPS proxy, optionally install the CA certificate, route supported API traffic through CCR, and capture network exchanges for debugging. +- **Bot relay**: connect agent profiles to supported IM platforms including Weixin iLink, WeCom, Slack, Discord, Telegram, LINE, Feishu, and DingTalk. +- **Extensions**: install wrapper plugins and core gateway plugins that can register local routes, proxy routes, provider account connectors, apps, and virtual models. ## Documentation -Read the full documentation at [ccrdesk.top](https://ccrdesk.top/). +Read the full documentation at [ccrdesk.top](https://ccrdesk.top/), including the [CLI reference](https://ccrdesk.top/en/guides/cli/) and [Docker deployment guide](https://ccrdesk.top/en/guides/docker/). ## Download And Install @@ -79,11 +98,30 @@ Read the full documentation at [ccrdesk.top](https://ccrdesk.top/). 3. Install and launch **Claude Code Router**. 4. On first launch, CCR creates its local configuration database: - macOS/Linux: `~/.claude-code-router/config.sqlite` - - Windows: `%APPDATA%\Claude Code Router\config.sqlite` + - Windows: `%APPDATA%\claude-code-router\config.sqlite` CCR stores runtime configuration in SQLite. A legacy `config.json` is read only once for migration when no SQLite config exists. -After the service is started from the **Server** page, CCR listens on `http://localhost:8080` by default. The **Server** page controls the gateway `Host`, `Port`, proxy mode, system proxy, network capture, and CA certificate status. +After the service is started from the **Server** page, CCR listens on `http://127.0.0.1:3456` by default. The **Server** page controls the gateway `Host`, `Port`, proxy mode, system proxy, network capture, and CA certificate status. + +## CLI And Docker + +The npm CLI requires Node.js 22 or newer and provides the browser management UI, gateway, and Agent Config launch commands without Electron: + +```sh +npm install -g @musistudio/claude-code-router +ccr ui +``` + +The CLI management UI defaults to `http://127.0.0.1:3458`, while its model gateway defaults to `http://127.0.0.1:3456`. See the [complete CLI reference](https://ccrdesk.top/en/guides/cli/) for background/foreground service commands, options, profile launching, authentication, and data locations. + +To run the browser UI and gateway behind one Nginx port with persistent Docker storage: + +```sh +docker compose up -d --build +``` + +Docker exposes both management and gateway routes at `http://127.0.0.1:3458` by default. Read the [Docker deployment guide](https://ccrdesk.top/en/guides/docker/) before remote exposure; it covers the internal port topology, management and gateway authentication, `CCR_PUBLIC_BASE_URL`, volumes, backup/restore, upgrades, and health checks. ## Quick Start @@ -91,25 +129,23 @@ CCR can be configured entirely from the desktop UI. Use this setup order for a c ### 1. Add a provider -Open **Providers**, click **Add Provider**, then choose a built-in preset or **Other / custom API endpoint**. Fill in the provider name, base URL, protocol, API key, and model list. Run protocol probing and model connectivity checks when available, then save the provider. +Open **Providers**, click **Add Provider**, then choose a built-in preset, import a supported local agent login state, or select **Other / custom API endpoint**. Fill in the provider name, base URL, protocol, API key, and model list. Run protocol probing and model connectivity checks when available, then save the provider. ### 2. Configure routing -Open **Routing** to add conditional rules, configure request rewrites, and set fallback behavior. - -Use **Add Routing Rule** for request conditions, model-prefix routing, or rule-level fallback targets. +Open **Routing** to enable built-in agent routes, add conditional rules, configure request rewrites, and set fallback behavior. Use **Add Routing Rule** for request conditions, model-prefix routing, or rule-level fallback targets. ### 3. Start the gateway -Open **Server** and click **Start**. After the page shows Running, CCR listens on `http://localhost:8080`. Enable **Auto start** if you want CCR to start the local gateway whenever the desktop app opens. +Open **Server** and click **Start**. After the page shows Running, CCR listens on `http://127.0.0.1:3456` by default. Enable **Auto start** if you want CCR to start the local gateway whenever the desktop app opens. ### 4. Connect your agent tool -Open **Agent Config** and choose the client you want to use. Configure Claude Code, Codex, or ZCode, select the target model and effect scope, then apply the config. For app entries, use the **Open Agent** action to open the target app through CCR. +Open **Agent Config** and choose the client you want to use. Configure Claude Code, Codex, Grok CLI, or ZCode, select the target model and effect scope, then apply the config. For app entries, use **Open Agent** to launch the target app through CCR. ### 5. Monitor and adjust -Use **Settings → Logs & Observability** to enable request logs and agent observability. Use **Logs** to confirm `request model`, `resolved provider`, `resolved model`, status, tokens, latency, and errors; use the tray window for quick token and account status. +Use **Settings → Logs & Observability** to enable request logs and agent observability. Use **Logs** to confirm `request model`, `resolved provider`, `resolved model`, status, tokens, latency, and errors. Use the dashboard and tray window for token, cost, model distribution, and account status. ## Acknowledgements @@ -210,20 +246,43 @@ Codex support is powered by [musistudio/codexl](https://github.com/musistudio/co - + code0.ai icon
code0.ai
- + claudeapi icon
claudeapi
+ + + + Qiniu Cloud AI icon +
+ Qiniu Cloud AI +
+ + + + Fenno.ai icon +
+ Fenno.ai +
+ + + + Unity2.Ai icon +
+ Unity2.Ai +
+ +

Community Sponsors

diff --git a/README_zh.md b/README_zh.md index 183df2d9..8dfa475d 100644 --- a/README_zh.md +++ b/README_zh.md @@ -41,31 +41,51 @@ -Claude Code Router Desktop 是一个本地网关和桌面控制台,用来把 Claude Code、Codex、ZCode 以及兼容客户端的 Agent 请求路由到你真正想使用的模型服务。 +Claude Code Router Desktop 是给编程 Agent 用的本地控制平面。它为 Claude Code、Codex、Grok CLI、ZCode 以及兼容 API 客户端提供一个稳定的本地入口,然后由你在 CCR 中决定每个请求应该走哪个供应商、哪个模型、哪套路由策略、哪些工具能力和哪组账号凭据。 + +相比在每个 Agent、每个模型服务里反复改配置,CCR 把模型层收束到本机桌面应用里:供应商预设、自定义端点、凭据池、Fallback、Fusion 组合模型、MCP 工具、请求日志、账号用量和 Agent 启动配置都在一个地方管理。

Claude Code Router Desktop 项目截图

+## CCR 能帮你做什么 + +| 目标 | CCR 提供的能力 | +| --- | --- | +| 保持 Agent 工作流不变,同时自由切换模型 | 为 Claude Code、Codex、Grok CLI、ZCode 创建本地配置档案,支持 CLI / App 启动入口和按配置选择模型 | +| 快速接入多个模型供应商 | 内置供应商预设、自定义 OpenAI / Anthropic / Gemini 兼容端点、协议探测、模型发现和连通性检测 | +| 把路由变成可配置策略 | 内置 Agent 路由、条件规则、请求改写、模型前缀路由、自动重试和 Fallback 模型链 | +| 控制成本和额度压力 | 凭据池、Key 轮换、本地限额、账号余额快照、Token / 成本仪表盘和托盘状态 | +| 给稳定模型补能力 | 通过 Fusion 给基础模型叠加视觉、联网搜索或指定 MCP 工具 | +| 让大量工具变得可用 | ToolHub 把多个 MCP server 收束成一个紧凑入口,让 Agent 按任务动态解析和调用工具 | +| 排查每一次请求 | 请求日志、最终供应商 / 模型、耗时、Token、成本估算、网络捕获和 Agent 观测链路 | + ## 为什么使用 CCR -- 用一个本地入口连接多个 Agent 工具,不需要在每个客户端里重复配置 Provider。 -- 在不改变工作流的情况下混用不同 Provider。CCR 支持 OpenAI 兼容 API、Anthropic Messages、Gemini Generate Content、OpenRouter、DeepSeek、SiliconFlow、Moonshot、Kimi Code、Mistral、Z.AI、百炼以及自定义 Provider。 -- 通过 fallback 路由、API Key 轮换、用量统计和请求日志来控制成本和可靠性。 +- **一个本地网关,接管整套 Agent 模型层**:客户端只需要指向 CCR,模型、供应商、Key、路由和工具能力都可以在桌面 UI 中调整。 +- **换供应商,不换工作流**:支持 OpenAI Chat / Responses、Anthropic Messages、Gemini Generate Content / Interactions、OpenRouter、DeepSeek、SiliconFlow、Moonshot、Kimi Code、Mistral、Z.AI、百炼以及自定义兼容供应商。 +- **可见、可改、可验证的可靠性策略**:配置请求什么时候改写、重试或切到备用模型,并在本地日志里确认真实命中结果。 +- **面向 AI 工作流的运营视角**:从仪表盘或托盘查看请求量、Token、成本估算、成功率、延迟、模型分布、供应商用量和账号余额。 +- **Agent 原生工具与扩展**:使用 Fusion 扩展模型能力,通过 ToolHub 暴露动态 MCP 工具,让内置浏览器参与任务,通过 IM Bot 接力 Agent,或安装本地扩展。 -## 功能和特性 +## 功能亮点 -- **概览仪表盘**:查看系统状态、用量组件、账号余额、模型分布和分享卡片。 -- **Provider 管理**:添加预设或自定义端点,探测协议支持,检测模型连通性,管理凭据,并在可用时查看账号余额。 -- **路由规则**:配置条件路由、模型前缀规则、失败降级和请求改写。 -- **Agent配置**:为 Claude Code、Codex 和 ZCode 配置启动入口、模型、作用范围和多开 App 配置。 -- **网关兼容层**:通过本地 CCR 模型网关转换支持的客户端请求。 -- **代理模式**:通过本地代理捕获支持的 API 流量,可选系统代理和网络捕获。 -- **Fusion 组合模型**:把基础模型与视觉、联网搜索或 MCP 工具组合成新的可选模型。 +- **Agent 配置档案**:为 Claude Code、Codex、Grok CLI 和 ZCode 创建配置档案,支持模型覆盖、作用范围、CLI / App 启动方式、环境变量和多开 App 工作流。 +- **供应商管理**:添加预设供应商或自定义端点;探测协议;发现模型列表;运行真实连通性检测;管理单 Key 或凭据池;在支持时导入本机 Agent 登录态。 +- **模型目录**:搜索全部已配置模型,编辑模型描述,并把这些描述用于 Claude Code Subagent、Task 和 Workflow 的模型选择提示。 +- **路由引擎**:组合内置 Agent 路由、请求 Header / Body 条件、模型前缀路由、请求改写、重试策略和有序 Fallback 目标。 +- **Fusion 组合模型**:发布可复用的虚拟模型,在保留基础模型手感的同时增加视觉、托管联网搜索或指定 MCP 工具。 +- **ToolHub**:把多个 MCP server 合并成一个动态 MCP server,让 Agent 只在任务需要时解析工具;桌面端还可暴露内置浏览器自动化和 Chrome 登录态导入。 +- **API Key 与限额**:创建访问 CCR 的客户端 Key,设置过期时间和本地请求 / Token / 图片限额,与上游供应商凭据分开管理。 +- **日志与观测**:查看请求 / 响应详情、最终供应商与模型、凭据、状态、耗时、Token、成本估算、工具调用和 Agent 执行链路。 +- **代理与网络捕获**:把 CCR 作为本地 HTTP / HTTPS 代理运行,可选安装 CA 证书,把支持的 API 流量接入 CCR,并保存网络请求用于排查。 +- **Bot 接力**:把 Agent 配置接入 Weixin iLink、企业微信、Slack、Discord、Telegram、LINE、飞书和钉钉等 IM 平台。 +- **扩展机制**:安装 wrapper plugin 和 core gateway plugin,注册本地路由、代理路由、供应商账号连接器、内置应用和虚拟模型。 ## 文档 -完整文档见 [ccrdesk.top](https://ccrdesk.top/)。 +完整文档见 [ccrdesk.top](https://ccrdesk.top/),其中包括 [CLI 命令参考](https://ccrdesk.top/guides/cli/) 和 [Docker 部署指南](https://ccrdesk.top/guides/docker/)。 ## 下载和安装 @@ -78,11 +98,30 @@ Claude Code Router Desktop 是一个本地网关和桌面控制台,用来把 C 3. 安装并启动 **Claude Code Router**。 4. 首次启动后,CCR 会创建本地配置数据库: - macOS/Linux:`~/.claude-code-router/config.sqlite` - - Windows:`%APPDATA%\Claude Code Router\config.sqlite` + - Windows:`%APPDATA%\claude-code-router\config.sqlite` CCR 的运行配置存储在 SQLite 中。旧版 `config.json` 只会在没有 SQLite 配置时作为迁移来源读取一次。 -从 **服务** 页面启动后,CCR 默认监听 `http://localhost:8080`。**服务** 页面负责配置网关 `Host`、`Port`、代理模式、系统代理、网络捕获和 CA 证书状态。 +从 **服务** 页面启动后,CCR 默认监听 `http://127.0.0.1:3456`。**服务** 页面负责配置网关 `Host`、`Port`、代理模式、系统代理、网络捕获和 CA 证书状态。 + +## CLI 与 Docker + +npm CLI 要求 Node.js 22 或更高版本,不依赖 Electron,也能提供浏览器管理界面、模型网关和 Agent 配置启动命令: + +```sh +npm install -g @musistudio/claude-code-router +ccr ui +``` + +CLI 管理界面默认是 `http://127.0.0.1:3458`,模型网关默认是 `http://127.0.0.1:3456`。后台 / 前台服务、全部选项、Profile 启动、鉴权和数据位置见[完整 CLI 参考](https://ccrdesk.top/guides/cli/)。 + +如果要使用单一 Nginx 端口和持久化 Docker 数据卷运行管理 UI 与网关: + +```sh +docker compose up -d --build +``` + +Docker 默认把管理和网关路径都发布在 `http://127.0.0.1:3458`。远程暴露前请先阅读 [Docker 部署指南](https://ccrdesk.top/guides/docker/),其中包含内部端口拓扑、管理与网关鉴权、`CCR_PUBLIC_BASE_URL`、数据卷、备份恢复、升级和健康检查。 ## 快速开始 @@ -90,25 +129,23 @@ CCR 可以完全通过桌面 UI 完成配置。首次使用建议按下面顺序 ### 1. 添加 Provider -打开 **供应商**,点击 **添加供应商**,选择内置预设或 **其他 / 自定义 API 端点**。按表单填写 Provider 名称、基础 URL、协议、API Key 和模型列表。可用时先运行协议探测和模型连通性检查,然后保存 Provider。 +打开 **供应商**,点击 **添加供应商**,选择内置预设、导入支持的本机 Agent 登录态,或选择 **其他 / 自定义 API 端点**。按表单填写 Provider 名称、基础 URL、协议、API Key 和模型列表。可用时先运行协议探测和模型连通性检查,然后保存 Provider。 ### 2. 设置路由 -打开 **路由**,添加条件规则,配置请求改写和失败降级。 - -如果需要更细粒度控制,使用 **添加路由规则** 添加模型前缀、请求条件或规则级失败降级目标。 +打开 **路由**,启用内置 Agent 路由,添加条件规则,配置请求改写和失败降级。如果需要更细粒度控制,使用 **添加路由规则** 添加模型前缀、请求条件或规则级失败降级目标。 ### 3. 启动网关 -打开 **服务**,点击 **启动**。页面显示运行中后,CCR 会在本机监听 `http://localhost:8080`。如果希望每次打开桌面应用时自动启动网关,可以启用自动启动。 +打开 **服务**,点击 **启动**。页面显示运行中后,CCR 默认会在本机监听 `http://127.0.0.1:3456`。如果希望每次打开桌面应用时自动启动网关,可以启用自动启动。 ### 4. 连接 Agent 工具 -打开 **Agent配置**,选择要使用的客户端。配置 Claude Code、Codex 或 ZCode,选择目标模型和作用范围,然后应用配置。对于 App 入口,可以使用 **打开 Agent** 操作通过 CCR 打开目标应用。 +打开 **Agent配置**,选择要使用的客户端。配置 Claude Code、Codex、Grok CLI 或 ZCode,选择目标模型和作用范围,然后应用配置。对于 App 入口,可以使用 **打开 Agent** 通过 CCR 打开目标应用。 ### 5. 日常查看和调整 -到 **设置 → 日志与观测** 打开请求日志和 Agent 观测。使用 **日志** 确认 `request model`、`resolved provider`、`resolved model`、状态码、tokens、耗时和错误;使用托盘窗口快速查看 Token 和账号状态。 +到 **设置 → 日志与观测** 打开请求日志和 Agent 观测。使用 **日志** 确认 `request model`、`resolved provider`、`resolved model`、状态码、tokens、耗时和错误;使用概览仪表盘和托盘窗口查看 Token、成本、模型分布和账号状态。 ## 致谢 @@ -209,20 +246,43 @@ CCR 可以完全通过桌面 UI 完成配置。首次使用建议按下面顺序 - + code0.ai 图标
code0.ai
- + claudeapi 图标
claudeapi
+ + + + 七牛云 AI 图标 +
+ 七牛云 AI +
+ + + + Fenno.ai 图标 +
+ Fenno.ai +
+ + + + Unity2.Ai 图标 +
+ Unity2.Ai +
+ +

社区赞助者

diff --git a/build/dev.mjs b/build/dev.mjs index b91a341a..38901c2f 100644 --- a/build/dev.mjs +++ b/build/dev.mjs @@ -2,7 +2,7 @@ import electron from "electron"; import esbuild from "esbuild"; import { createHash } from "node:crypto"; import { spawn } from "node:child_process"; -import { existsSync, readdirSync, readFileSync, statSync, watch } from "node:fs"; +import { existsSync, readdirSync, readFileSync, statSync } from "node:fs"; import path from "node:path"; import { buildStyles, @@ -35,6 +35,8 @@ import { let electronProcess = null; let restartTimer = null; +let restartInFlight = false; +let restartQueued = false; let pendingRestartReasons = []; const watchSignatures = new Map(); let shuttingDown = false; @@ -95,13 +97,6 @@ function readyState() { .join(" "); } -function describeWatchEvent(label, watchedPath, eventType, filename, isDirectory = false) { - const changedPath = filename - ? path.join(isDirectory ? watchedPath : path.dirname(watchedPath), String(filename)) - : watchedPath; - return `${label} ${eventType} ${relativePath(changedPath)}`; -} - function contentSignature(targetPath) { try { return readContentSignature(targetPath); @@ -177,31 +172,14 @@ function listDirectoryFiles(targetPath, basePath = targetPath) { return files; } -function rememberWatchSignature(label, targetPath) { - const signature = contentSignature(targetPath); +function rememberWatchSignature(label, targetPath, options = {}) { + const signature = options.metadataOnly + ? metadataSignature(targetPath) + : contentSignature(targetPath); watchSignatures.set(label, signature.key); logDev(`watch baseline: ${label} ${relativePath(targetPath)}; ${signature.summary}`); } -function handleWatchedInput(label, watchedPath, eventType, filename, options, onChange) { - const reason = describeWatchEvent(label, watchedPath, eventType, filename, options?.isDirectory); - const signature = contentSignature(watchedPath); - const previousSignature = watchSignatures.get(label); - const changed = previousSignature !== signature.key; - watchSignatures.set(label, signature.key); - logDev(`watch event: ${reason}; ${signature.summary}; content=${changed ? "changed" : "unchanged"}`); - - if (!changed) { - logDev(`restart skipped: ${reason} (content unchanged)`); - return; - } - - onChange(); - if (enabled.electron && options?.restart !== false) { - scheduleRestart(reason); - } -} - function scheduleStyleBuild(reason) { queuedStyleBuildReason = reason; if (styleBuildTimer) { @@ -256,6 +234,64 @@ function pollStyleWatchRoots() { } } +function pollWatchedInput(label, targetPath, onChange, options = {}) { + const signature = options.metadataOnly + ? metadataSignature(targetPath) + : contentSignature(targetPath); + const previousSignature = watchSignatures.get(label); + if (previousSignature === signature.key) { + return; + } + + watchSignatures.set(label, signature.key); + logDev(`watch event: ${label} ${relativePath(targetPath)}; ${signature.summary}; content=changed`); + try { + onChange(); + if (enabled.electron && options.restart !== false) { + scheduleRestart(label); + } + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + logDev(`watch action failed: ${label}; ${message}`); + } +} + +function metadataSignature(targetPath) { + if (!existsSync(targetPath)) { + return { + key: "missing", + summary: "missing" + }; + } + + const stats = statSync(targetPath); + return { + key: `metadata:${stats.size}:${stats.mtimeMs}:${stats.ctimeMs}`, + summary: `size=${stats.size} mtime=${stats.mtime.toISOString()} ctime=${stats.ctime.toISOString()}` + }; +} + +function pollSourceWatchTargets() { + pollWatchedInput("home html", rendererHtmlInput, () => { + copyRendererHtml(); + syncUiRendererToRuntimeDists(); + }); + pollWatchedInput("browser html", browserRendererHtmlInput, () => { + copyBrowserRendererHtml(); + syncUiRendererToRuntimeDists(); + }); + pollWatchedInput("tray html", trayRendererHtmlInput, () => { + copyTrayRendererHtml(); + syncUiRendererToRuntimeDists(); + }); + if (enabled.electron) { + pollWatchedInput("app assets", appAssetsInput, copyAppAssets); + } + if ((enabled.cli || enabled.electron) && existsSync(modelCatalogInput)) { + pollWatchedInput("model catalog", modelCatalogInput, copyModelCatalog, { metadataOnly: true }); + } +} + function markReady(name, reason = `${name} esbuild completed`) { if (name === "browser" || name === "cli" || name === "main" || name === "renderer" || name === "tray" || name === "webBridge") { ready[name] = true; @@ -281,34 +317,81 @@ function scheduleRestart(reason = "unknown trigger") { restartTimer = setTimeout(restartElectron, restartDelayMs); } -function restartElectron() { +async function restartElectron() { + if (restartInFlight) { + restartQueued = true; + return; + } + restartInFlight = true; const reasons = Array.from(new Set(pendingRestartReasons)); pendingRestartReasons = []; restartTimer = null; - if (electronProcess) { - logDev(`stopping Electron pid=${electronProcess.pid ?? "unknown"}`); - electronProcess.kill(); - electronProcess = null; - } + try { + if (electronProcess) { + await stopElectron(electronProcess); + } - logDev(`starting Electron; reasons=${reasons.join(" | ") || "initial start"}`); - const child = spawn(electron, ["."], { - cwd: projectRoot, - env: { - ...process.env, - NODE_ENV: "development" - }, - stdio: "inherit" - }); - electronProcess = child; - logDev(`Electron started pid=${child.pid ?? "unknown"}`); - child.on("exit", (code, signal) => { - logDev(`Electron exited pid=${child.pid ?? "unknown"} code=${code ?? "null"} signal=${signal ?? "null"}`); + if (shuttingDown) { + return; + } + logDev(`starting Electron; reasons=${reasons.join(" | ") || "initial start"}`); + const child = spawn(electron, ["."], { + cwd: projectRoot, + env: { + ...process.env, + NODE_ENV: "development" + }, + stdio: "inherit" + }); + electronProcess = child; + logDev(`Electron started pid=${child.pid ?? "unknown"}`); + child.on("exit", (code, signal) => { + logDev(`Electron exited pid=${child.pid ?? "unknown"} code=${code ?? "null"} signal=${signal ?? "null"}`); + if (electronProcess === child) { + electronProcess = null; + } + }); + } finally { + restartInFlight = false; + if (restartQueued || pendingRestartReasons.length > 0) { + restartQueued = false; + scheduleRestart("changes queued during Electron restart"); + } + } +} + +async function stopElectron(child) { + logDev(`stopping Electron pid=${child.pid ?? "unknown"}`); + if (child.exitCode !== null || child.signalCode !== null) { if (electronProcess === child) { electronProcess = null; } + return; + } + + await new Promise((resolve) => { + let forceTimer = null; + let giveUpTimer = null; + const finish = () => { + if (forceTimer) clearTimeout(forceTimer); + if (giveUpTimer) clearTimeout(giveUpTimer); + child.off("exit", finish); + resolve(); + }; + child.once("exit", finish); + child.kill(); + forceTimer = setTimeout(() => { + if (child.exitCode === null && child.signalCode === null) { + logDev(`force stopping Electron pid=${child.pid ?? "unknown"}`); + child.kill("SIGKILL"); + } + }, 2_000); + giveUpTimer = setTimeout(finish, 5_000); }); + if (electronProcess === child) { + electronProcess = null; + } } logDev(`starting dev build target=${devTarget} ui=${enabled.ui ? "on" : "off"} cli=${enabled.cli ? "on" : "off"} electron=${enabled.electron ? "on" : "off"}`); @@ -336,43 +419,13 @@ if (enabled.electron) { rememberWatchSignature("app assets", appAssetsInput); } if ((enabled.cli || enabled.electron) && existsSync(modelCatalogInput)) { - rememberWatchSignature("model catalog", modelCatalogInput); + rememberWatchSignature("model catalog", modelCatalogInput, { metadataOnly: true }); } -const htmlWatcher = watch(rendererHtmlInput, { persistent: true }, (eventType, filename) => { - handleWatchedInput("home html", rendererHtmlInput, eventType, filename, undefined, () => { - copyRendererHtml(); - syncUiRendererToRuntimeDists(); - }); -}); - -const browserHtmlWatcher = watch(browserRendererHtmlInput, { persistent: true }, (eventType, filename) => { - handleWatchedInput("browser html", browserRendererHtmlInput, eventType, filename, undefined, () => { - copyBrowserRendererHtml(); - syncUiRendererToRuntimeDists(); - }); -}); - -const trayHtmlWatcher = watch(trayRendererHtmlInput, { persistent: true }, (eventType, filename) => { - handleWatchedInput("tray html", trayRendererHtmlInput, eventType, filename, undefined, () => { - copyTrayRendererHtml(); - syncUiRendererToRuntimeDists(); - }); -}); - -const stylePoller = setInterval(pollStyleWatchRoots, stylePollIntervalMs); - -const appAssetsWatcher = enabled.electron - ? watch(appAssetsInput, { persistent: true }, (eventType, filename) => { - handleWatchedInput("app assets", appAssetsInput, eventType, filename, { isDirectory: true }, copyAppAssets); - }) - : { close: () => undefined }; - -const modelCatalogWatcher = (enabled.cli || enabled.electron) && existsSync(modelCatalogInput) - ? watch(modelCatalogInput, { persistent: true }, (eventType, filename) => { - handleWatchedInput("model catalog", modelCatalogInput, eventType, filename, undefined, copyModelCatalog); - }) - : { close: () => undefined }; +const sourcePoller = setInterval(() => { + pollStyleWatchRoots(); + pollSourceWatchTargets(); +}, stylePollIntervalMs); const contexts = []; @@ -467,17 +520,12 @@ async function shutdown() { clearTimeout(restartTimer); } if (electronProcess) { - electronProcess.kill(); + await stopElectron(electronProcess); } if (styleBuildTimer) { clearTimeout(styleBuildTimer); } - htmlWatcher.close(); - browserHtmlWatcher.close(); - trayHtmlWatcher.close(); - clearInterval(stylePoller); - appAssetsWatcher.close(); - modelCatalogWatcher.close(); + clearInterval(sourcePoller); await Promise.all(contexts.map((context) => context.dispose())); process.exit(0); } diff --git a/docker/README.md b/docker/README.md index 2b51a1f3..7a929ffe 100644 --- a/docker/README.md +++ b/docker/README.md @@ -1,68 +1,214 @@ -# Docker deployment +# Docker Deployment -This image runs the core server package with PM2 and serves the built UI package -through Nginx. Nginx is the only published entrypoint: it serves the UI, proxies -management API calls to the internal core server, and proxies gateway API calls -to the internal gateway listener. +[中文说明](#中文说明) · [Project documentation](https://ccrdesk.top/en/) · [GitHub](https://github.com/musistudio/claude-code-router) -## Build and run +The Docker image runs the CCR core server under PM2 and serves the built management UI through Nginx. Nginx is the only public container entrypoint: the browser UI, management RPC, gateway API, and health route all share one published port. + +The image is intended for a persistent gateway and browser-based administration. It does not include Electron, the npm `ccr` command, system tray features, desktop Agent/App launching, automatic desktop updates, or desktop-only browser integrations. + +## Architecture And Ports + +```text +host:3458 -> container Nginx:8080 + |-> static management UI + |-> management RPC: 127.0.0.1:3459 + |-> gateway: 127.0.0.1:3456 + `-> gateway core: 127.0.0.1:3457 +``` + +Only Nginx port `8080` should be published. The three internal ports are container implementation details and should not be exposed individually. + +Nginx routes: + +| Public route | Purpose | +| --- | --- | +| `/` and `/pages/home/index.html` | Browser management UI. `/` redirects to a URL containing the management token. | +| `/api/ccr/rpc` | Authenticated management RPC. | +| `/health` | Gateway health, not container/UI health. It can return `502` until a provider and model are configured and the gateway starts. | +| `/v1/*`, `/v1beta/*`, `/messages`, `/chat/completions`, `/responses`, `/interactions`, `/mcp/*` | Supported model and MCP gateway requests. | + +## Quick Start With Compose + +From the repository root: ```sh -docker compose up --build +docker compose up -d --build +docker compose logs -f ccr ``` -Then open: +Open . On a new volume, the management UI is immediately available. Add a provider and model, create a CCR client key under **API Keys**, and start the gateway from **Server**. -- Web UI: -- Gateway endpoint: - -`docker-compose.yml` publishes only Nginx (`3458:8080`). Behind Nginx, the image -runs separate container-private listeners for management RPC, API gateway -routing, and the core gateway runtime. They are implementation details and are -not published or configured by the default Compose file. - -To use a different host port, change the Compose port mapping and keep the -public router endpoint in sync: +The repository Compose file publishes `3458:8080`, stores data in the `ccr-data` named volume, and restarts the service unless explicitly stopped. A mapping without a host IP binds on every host interface. For local-only access, change it to: ```yaml -services: - ccr: - ports: - - "8088:8080" - environment: - CCR_PUBLIC_BASE_URL: http://127.0.0.1:8088 +ports: + - "127.0.0.1:3458:8080" ``` -The container stores config and SQLite databases under `/data`, backed by the -`ccr-data` volume in `docker-compose.yml`. +Stop or remove the container without deleting its named volume: -On a fresh data volume, the Web UI starts immediately. The gateway endpoint is -available through the same Nginx entrypoint, but the gateway only starts after at -least one provider and model are configured. +```sh +docker compose stop +docker compose down +``` -## Image scripts +Do not add `--volumes` to `docker compose down` unless you intentionally want to delete all persisted CCR data. + +## `docker run` + +Build and run without Compose: + +```sh +docker build -t claude-code-router:local . +docker run -d \ + --name claude-code-router \ + --restart unless-stopped \ + -p 127.0.0.1:3458:8080 \ + -e CCR_PUBLIC_BASE_URL=http://127.0.0.1:3458 \ + -v ccr-data:/data \ + claude-code-router:local +``` + +Equivalent repository scripts are available: ```sh npm run docker:build npm run docker:run ``` -## Smoke test +`npm run docker:run` uses port `3458` and the `ccr-data` volume, but runs an ephemeral container without a fixed name or restart policy. -```sh -npm run test:docker +## Authentication And Network Security + +There are two independent authentication layers: + +1. `CCR_WEB_AUTH_TOKEN` protects management RPC. Nginx puts it into the management-page URL, and the browser sends it to RPC as `x-ccr-web-auth`. +2. CCR client API keys created in the **API Keys** page protect model gateway requests. These are separate from upstream provider credentials. + +If `CCR_WEB_AUTH_TOKEN` is unset, the entrypoint generates a new random token on each container start. Opening `/` still works because Nginx redirects to a tokenized URL, but a stable token is recommended for persistent or remote deployments. + +Avoid putting the token directly in shell history. Create a protected environment file instead: + +```dotenv +CCR_WEB_AUTH_TOKEN=replace-with-a-long-random-value +CCR_PUBLIC_BASE_URL=http://127.0.0.1:3458 ``` -The smoke test builds the image, starts an isolated temporary container with a -special-character `CCR_WEB_AUTH_TOKEN`, verifies that only the Nginx port is -published, checks UI and RPC authentication, confirms legacy Docker config is -migrated to the public Nginx router endpoint, and removes its temporary -container and volume. Set `CCR_DOCKER_TEST_SKIP_BUILD=1` to reuse an already -built image. +Then use it with `docker run --env-file` or map the same variables under the Compose service's `environment` section. Keep this file out of version control. -The Dockerfile uses `node:22-bookworm` for build and native SQLite dependency -installation, then copies the production dependencies into a smaller -`node:22-bookworm-slim` runtime image. To use different base images: +Security guidance: + +- Bind the published port to `127.0.0.1` unless LAN or remote access is intentional. +- Never expose the management UI over untrusted networks without TLS, a firewall/private network, and a fixed strong management token. +- Treat tokenized management URLs as secrets; URLs may be recorded in browser history, proxy logs, screenshots, and support tickets. +- Create scoped CCR client API keys before exposing gateway routes. Do not reuse upstream provider credentials as client keys. +- Protect `/data` and its backups because they contain configuration, provider credentials, CCR client keys, request data, and generated certificates. + +## Changing The Public Address + +The host-facing URL is separate from the container's internal ports. Whenever the host port, hostname, or scheme changes, set `CCR_PUBLIC_BASE_URL` to the exact URL clients should use: + +```yaml +services: + ccr: + ports: + - "127.0.0.1:8088:8080" + environment: + CCR_PUBLIC_BASE_URL: http://127.0.0.1:8088 + CCR_WEB_AUTH_TOKEN: ${CCR_WEB_AUTH_TOKEN:?set CCR_WEB_AUTH_TOKEN} +``` + +`CCR_PUBLIC_BASE_URL` is written to CCR's public router endpoint. It does not publish a Docker port by itself. + +For a reverse proxy or ingress that terminates HTTPS: + +```yaml +services: + ccr: + ports: + - "127.0.0.1:3458:8080" + environment: + CCR_PUBLIC_BASE_URL: https://ccr.example.com + CCR_WEB_AUTH_TOKEN: ${CCR_WEB_AUTH_TOKEN:?set CCR_WEB_AUTH_TOKEN} +``` + +Proxy all paths to Nginx and preserve streaming. The external proxy should allow long-lived responses and should not buffer SSE/model streams. Keep the host port private when the reverse proxy is the public entrypoint. + +## Persistent Data + +The entrypoint sets `HOME=/data`, so CCR stores files under: + +```text +/data/.claude-code-router/ +├── config.sqlite +├── gateway.config.json +├── app-data/ +│ ├── api-keys.sqlite +│ ├── request-logs.sqlite +│ ├── usage.sqlite +│ └── certs/ +├── profiles/ +└── bin/ +``` + +Use a named volume unless a bind mount is operationally required. Bind mounts must be writable by the container and should not be shared by two running CCR containers. + +The first-run bootstrap writes a minimal legacy `config.json` only when neither `config.json` nor `config.sqlite` exists. When the UI saves current settings, SQLite becomes authoritative. By default, every container start also synchronizes the stored gateway listener and `routerEndpoint` to the Docker public endpoint. + +## Backup And Restore + +The safest application-level backup is **Settings → Export data**. For a full volume backup, stop writes before copying the data directory: + +```sh +docker compose stop ccr +docker compose cp ccr:/data/. ./ccr-data-backup/ +docker compose start ccr +``` + +Keep the backup private. It contains secrets and may include request/response data. + +For a full restore, use a new empty volume or empty `/data` directory, copy the backup contents into it while the CCR container is stopped, then start the container. Do not overlay an old backup onto a populated live volume: stale SQLite WAL/SHM files and newer runtime files can produce an inconsistent result. Make a second backup before replacing existing data. + +## Upgrade And Rollback + +Back up `/data`, update the source revision, rebuild with fresh base layers, and recreate the service: + +```sh +git pull +docker compose build --pull +docker compose up -d +docker compose ps +docker compose logs --tail=200 ccr +``` + +Configuration migrations run against the persistent data. To roll back, use the previous image/source revision together with a backup created before the upgrade; do not assume a newer database can always be read by an older build. + +## Environment Variables + +Most deployments should set only `CCR_WEB_AUTH_TOKEN`, `CCR_PUBLIC_BASE_URL`, and the Docker port mapping. Internal listener values normally should remain unchanged. + +| Variable | Default | Description | +| --- | --- | --- | +| `CCR_WEB_AUTH_TOKEN` | Random per container start | Management UI/RPC token. Set a stable strong value for persistent or remote use. | +| `CCR_PUBLIC_BASE_URL` | `http://127.0.0.1:3458` | Exact public gateway/UI base URL written into CCR configuration. Overrides `CCR_PUBLIC_HOST` and `CCR_PUBLIC_PORT`. | +| `CCR_PUBLIC_HOST` | `127.0.0.1` | Used only to derive `CCR_PUBLIC_BASE_URL` when the full URL is unset; it does not change Docker port publishing. | +| `CCR_PUBLIC_PORT` | `3458` | Used only to derive `CCR_PUBLIC_BASE_URL` when the full URL is unset. | +| `CCR_DATA_DIR` | `/data` | Container data root and process `HOME`. Mount persistent storage here. | +| `CCR_NGINX_PORT` | `8080` | Container-private Nginx listen port. Match the container side of the published mapping if changed. | +| `CCR_WEB_HOST` | `127.0.0.1` | Container-private management server host. | +| `CCR_WEB_PORT` | `3459` | Container-private management server port. | +| `CCR_GATEWAY_HOST` | `127.0.0.1` | Container-private gateway listener host. | +| `CCR_GATEWAY_PORT` | `3456` | Container-private gateway listener port used by Nginx. | +| `CCR_GATEWAY_CORE_PORT` | `3457` | Container-private core gateway runtime port. | +| `CCR_NO_GATEWAY` | `0` | Set to `1`, `true`, or `yes` to run the management UI without starting the gateway at boot. | +| `CCR_DOCKER_INIT_CONFIG` | `1` | Set to `0` to disable minimal first-run `config.json` bootstrap. | +| `CCR_DOCKER_SYNC_PUBLIC_ENDPOINT` | `1` | Set to `0` to stop startup from syncing existing JSON/SQLite listener and public endpoint fields to Docker values. | + +Changing internal ports requires corresponding Nginx/PM2 variables and offers no benefit in normal deployments. Publish only `CCR_NGINX_PORT`. + +## Build Options And Smoke Test + +The Dockerfile builds native dependencies with `node:22-bookworm`, then copies production dependencies and built assets into `node:22-bookworm-slim`. Override the base images when required: ```sh docker build \ @@ -71,21 +217,123 @@ docker build \ -t claude-code-router:local . ``` -## Environment +Run the isolated Docker smoke test: -Most deployments only need the published Nginx port mapping, `CCR_WEB_AUTH_TOKEN`, -and optionally `CCR_PUBLIC_BASE_URL` when the host-facing URL is not -`http://127.0.0.1:3458`. +```sh +npm run test:docker +``` -| Variable | Default | Description | -| --- | --- | --- | -| `CCR_WEB_AUTH_TOKEN` | generated | Shared management UI token used by Nginx redirects and the core server. | -| `CCR_PUBLIC_BASE_URL` | `http://127.0.0.1:3458` | Full public router endpoint override. Set this when changing the host-facing Compose port. | -| `CCR_DATA_DIR` | `/data` | Container data root. | -| `CCR_NO_GATEWAY` | `0` | Set to `1` to run only the Web UI management service. | -| `CCR_DOCKER_INIT_CONFIG` | `1` | Set to `0` to disable first-run `config.json` bootstrap. | -| `CCR_DOCKER_SYNC_PUBLIC_ENDPOINT` | `1` | Sync existing Docker config to the Nginx public router endpoint on startup. | +The test builds the image, starts a temporary container and volume, verifies that only Nginx is published, checks UI/RPC authentication, tests public-endpoint migration, starts a configured gateway, checks `/health`, and removes its resources. Set `CCR_DOCKER_TEST_SKIP_BUILD=1` to reuse an existing image or `CCR_DOCKER_TEST_IMAGE` to test a different local tag. -The first-run bootstrap writes a minimal legacy `config.json` only when neither -`config.json` nor `config.sqlite` exists in the mounted data directory. Once the -UI saves settings into SQLite, existing persisted configuration takes priority. +## Operations And Troubleshooting + +Useful commands: + +```sh +docker compose ps +docker compose logs -f ccr +docker compose restart ccr +docker compose config +``` + +### `/` returns `302` + +This is expected. Nginx redirects the root URL to the management page and URL-encodes the management token. + +### `/health` returns `502` + +`/health` checks the model gateway, not Nginx or the management UI. On a fresh volume it returns `502` until a provider/model exists and the gateway has started. Use `docker compose ps` for container health and open the UI to configure/start the gateway. + +### The UI returns `401` after a token change + +Open the bare root URL again so Nginx creates a URL with the current token. Close stale tabs and avoid bookmarks that contain an old `ccr_web_token`. + +### Clients still use the old port or hostname + +Update `CCR_PUBLIC_BASE_URL` and recreate the container. Leave `CCR_DOCKER_SYNC_PUBLIC_ENDPOINT=1` so existing SQLite configuration is synchronized at startup. + +### Configuration disappears after recreation + +Confirm that `/data` is mounted and that the same named volume or bind-mount path is being reused. `docker compose down` keeps named volumes; `docker compose down --volumes` deletes them. + +### A bind mount fails with permission errors + +Verify that the host directory exists, is writable by the container, and is not mounted read-only. Named volumes avoid most host ownership and labeling issues. + +### The container is healthy but model requests fail + +Container health only verifies Nginx/UI reachability. Check **Server** status, provider connectivity, CCR client-key authentication, routing, and request logs. Then inspect `docker compose logs --tail=200 ccr` for startup or runtime errors. + +--- + +## 中文说明 + +Docker 镜像通过 PM2 运行 CCR Core,并由 Nginx 同时提供管理 UI、管理 RPC、模型网关和健康检查。对外只应发布 Nginx 的容器端口 `8080`;`3459`、`3456`、`3457` 都是容器内部实现端口,不应单独暴露。 + +这个镜像面向常驻网关和浏览器管理,不包含 Electron、npm 的 `ccr` 命令、系统托盘、桌面 Agent/App 启动、桌面自动更新和桌面专属浏览器集成。 + +### 快速启动 + +```sh +docker compose up -d --build +docker compose logs -f ccr +``` + +打开 。首次启动时管理 UI 可以立即访问;添加供应商和模型、在 **API 密钥** 页面创建 CCR 客户端 Key,然后从 **服务** 页面启动网关。 + +仓库默认映射是 `3458:8080`,会监听宿主机所有网卡。如果只允许本机访问,请改为: + +```yaml +ports: + - "127.0.0.1:3458:8080" +``` + +### 鉴权与远程访问 + +- `CCR_WEB_AUTH_TOKEN` 用于管理 UI / RPC;不设置时,每次容器启动都会生成新的随机 Token。 +- **API 密钥** 页面创建的 CCR 客户端 Key 用于模型网关请求。 +- 上游供应商凭据是第三类凭据,不应拿来代替 CCR 客户端 Key。 + +根路径会重定向到包含 `ccr_web_token` 的管理 URL。请把该 URL 当作密码。远程部署至少应使用固定强 Token、TLS、主机防火墙或私网,并让反向代理把全部路径转发到 Nginx。流式响应和 SSE 不应被代理缓冲。 + +外部端口、域名或协议变化时,必须同步设置公开地址: + +```yaml +environment: + CCR_PUBLIC_BASE_URL: https://ccr.example.com + CCR_WEB_AUTH_TOKEN: ${CCR_WEB_AUTH_TOKEN:?set CCR_WEB_AUTH_TOKEN} +``` + +`CCR_PUBLIC_BASE_URL` 只负责写入客户端应使用的公开地址,不会自动发布 Docker 端口。 + +### 数据、备份与升级 + +数据实际位于 `/data/.claude-code-router/`,其中包括 `config.sqlite`、`app-data/`、Agent 配置和生成文件。优先使用命名卷,不要让两个运行中的 CCR 容器共享同一个数据目录。 + +完整文件备份前先停止写入: + +```sh +docker compose stop ccr +docker compose cp ccr:/data/. ./ccr-data-backup/ +docker compose start ccr +``` + +备份包含密钥和请求数据,必须按敏感数据保存。恢复时应复制到新的空卷或空 `/data`,不要把旧备份覆盖到仍有新数据的目录。升级前先备份,然后执行: + +```sh +git pull +docker compose build --pull +docker compose up -d +docker compose ps +docker compose logs --tail=200 ccr +``` + +### 常见排查 + +- `/` 返回 `302`:正常,Nginx 正在跳转到带管理 Token 的页面。 +- `/health` 返回 `502`:它检查的是模型网关;首次启动尚未配置模型时属于预期行为。 +- 修改 Token 后 UI 返回 `401`:重新打开不带参数的根地址,关闭仍使用旧 Token 的标签页。 +- 重建后配置消失:检查是否仍挂载同一个 `/data` 卷;`docker compose down --volumes` 会删除数据卷。 +- 容器健康但模型请求失败:继续检查服务状态、供应商连通性、CCR 客户端 Key、路由和请求日志;容器健康只表示 Nginx / UI 可访问。 + +完整的环境变量、端口拓扑、远程部署、构建参数和烟雾测试说明见本页英文主体,对应变量名和命令在中英文环境中完全相同。 diff --git a/docs/public/provider-icons/fenno.jpg b/docs/public/provider-icons/fenno.jpg new file mode 100644 index 00000000..364f735c Binary files /dev/null and b/docs/public/provider-icons/fenno.jpg differ diff --git a/docs/public/provider-icons/qiniu-ai.png b/docs/public/provider-icons/qiniu-ai.png new file mode 100644 index 00000000..2ff255dc Binary files /dev/null and b/docs/public/provider-icons/qiniu-ai.png differ diff --git a/docs/public/provider-icons/unity2.jpg b/docs/public/provider-icons/unity2.jpg new file mode 100644 index 00000000..e83c9e02 Binary files /dev/null and b/docs/public/provider-icons/unity2.jpg differ diff --git a/docs/src/content/docs/en/configuration/bot-setup.md b/docs/src/content/docs/en/configuration/bot-setup.md index ef33824e..c21e9a63 100644 --- a/docs/src/content/docs/en/configuration/bot-setup.md +++ b/docs/src/content/docs/en/configuration/bot-setup.md @@ -11,8 +11,9 @@ lead: Add a Bot, bind it to Agent Config, and choose message forwarding or hando 2. Select a platform and fill in the required token, secret, signing secret, robot code, or OAuth fields. 3. Save the Bot. 4. Open the target **Agent Config** and enable **Bot**. -5. Choose **Forward agent messages** or **Handoff**, then reopen the agent from CCR. +5. Configure forwarding, handoff, language, timeout, attachments, streaming, and **Allow Agent shell tools** as needed. +6. Reopen Claude, Codex, ZCode, or OpenCode App from CCR. The Bot is online only while the App is alive. ## Verification -Open the agent from CCR and send a test message. Confirm that Logs include Bot records and the IM side receives the message. +Open the Agent App from CCR, then send `/project current`, `/session list`, and one plain message from IM. The Profile card shows connection state, the last event and delivery, pending outbox count, and redacted errors; `/session doctor` provides the same runtime diagnostics. Closing the App should move the Bot to offline state. diff --git a/docs/src/content/docs/en/configuration/bots.md b/docs/src/content/docs/en/configuration/bots.md index 614dd766..1d4ae4b3 100644 --- a/docs/src/content/docs/en/configuration/bots.md +++ b/docs/src/content/docs/en/configuration/bots.md @@ -5,11 +5,61 @@ eyebrow: Detailed Configuration lead: Forward agent messages to instant-messaging platforms or hand off active work after desktop idle. --- +CCR App Relay shares the lifecycle of an Agent App opened by CCR. Opening Claude, Codex, ZCode, or OpenCode App starts its companion worker; closing the App stops both the worker and Bot connection. + ## Common Modes - **Forward agent messages**: mirror agent messages into IM. - **Handoff**: relay the interaction into IM after desktop idle. +- **Reply only**: with forwarding and handoff disabled, reply only to turns initiated from IM. + +Natural-language turns are serialized per IM conversation. `/project`, `/session status`, and `/session cancel` remain immediately responsive; queueing, timeout, cancellation, and worker-restart recovery have explicit states. + +## Projects And Sessions + +A Project is an Agent-native project or working directory, and a Session is an Agent-native conversation inside that Project. + +### Project Commands + +| Command | Purpose | +| --- | --- | +| `/project` | Show Project help and the App-online boundary. | +| `/project list [page]` | List known Agent projects with pagination. | +| `/project find ` | Search project names and paths. | +| `/project current` | Show the current Project. | +| `/project use ` | Change Project and clear the previous Session selection. | +| `/project name