Compare commits

..

No commits in common. "main" and "v3.0.2" have entirely different histories.
main ... v3.0.2

450 changed files with 16793 additions and 78929 deletions

View file

@ -1,17 +0,0 @@
.git
.DS_Store
.env
.env.*
node_modules
dist
packages/*/dist
release
release-local
logs
tmp
test-results
playwright-report
blob-report
docs/node_modules
docs/dist
docs/.astro

33
.github/workflows/cdn.yml vendored Normal file
View file

@ -0,0 +1,33 @@
name: CDN
on:
push:
branches:
- main
paths:
- "cdn/**"
- ".github/workflows/cdn.yml"
workflow_dispatch:
permissions:
contents: read
concurrency:
group: cdn
cancel-in-progress: false
jobs:
deploy:
name: Deploy Cloudflare Pages
runs-on: ubuntu-latest
steps:
- name: Check out repository
uses: actions/checkout@v6
- name: Deploy CDN assets
uses: cloudflare/wrangler-action@v3
with:
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
workingDirectory: cdn
command: pages deploy public --project-name=claude-code-router-cdn --branch=${{ github.ref_name }}

View file

@ -14,24 +14,8 @@ concurrency:
jobs:
macos:
name: macOS (${{ matrix.display_name }})
runs-on: ${{ matrix.runner }}
strategy:
fail-fast: false
matrix:
include:
- arch: arm64
arch_flag: --arm64
runner: macos-14
display_name: Apple Silicon
release_label: Apple-Silicon
artifact_name: macos-apple-silicon-arm64
- arch: x64
arch_flag: --x64
runner: macos-15-intel
display_name: Intel
release_label: Intel
artifact_name: macos-intel-x64
name: macOS
runs-on: macos-14
steps:
- name: Check out repository
uses: actions/checkout@v4
@ -55,92 +39,16 @@ jobs:
exit 1
fi
- name: Build ad-hoc macOS artifacts
- name: Build and publish ad-hoc macOS artifacts
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
npm run build:assets
npx electron-builder \
--config build/electron-builder.local.cjs \
--mac ${{ matrix.arch_flag }} \
--publish never \
-c.mac.artifactName='Claude-Code-Router_${version}-mac-${{ matrix.release_label }}-${arch}.${ext}'
- name: Upload macOS release files
uses: actions/upload-artifact@v4
with:
name: ${{ matrix.artifact_name }}
path: |
release-local/Claude-Code-Router_*-mac-${{ matrix.release_label }}-${{ matrix.arch }}.*
release-local/latest-mac.yml
if-no-files-found: error
publish-macos:
name: Publish macOS
needs: macos
runs-on: ubuntu-latest
steps:
- name: Check out repository
uses: actions/checkout@v4
- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: 22
cache: npm
- name: Install metadata merge dependencies
run: npm ci --ignore-scripts
- name: Download macOS release files
uses: actions/download-artifact@v4
with:
pattern: macos-*
path: macos-release-artifacts
- name: Merge macOS update metadata
run: node build/merge-macos-update-metadata.mjs latest-mac.yml macos-release-artifacts/*/latest-mac.yml
- name: Create GitHub release
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
gh release view "$GITHUB_REF_NAME" >/dev/null 2>&1 || \
gh release create "$GITHUB_REF_NAME" --title "$GITHUB_REF_NAME" --generate-notes
- name: Upload macOS release assets
shell: bash
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
shopt -s nullglob
assets=(macos-release-artifacts/*/Claude-Code-Router_*-mac-*.*)
if (( ${#assets[@]} == 0 )); then
echo "No macOS release assets were found."
exit 1
fi
upload_args=()
for asset in "${assets[@]}"; do
name="$(basename "$asset")"
case "$name" in
*Apple-Silicon-arm64.dmg) label="macOS Apple Silicon DMG (arm64)" ;;
*Apple-Silicon-arm64.zip) label="macOS Apple Silicon ZIP (arm64)" ;;
*Apple-Silicon-arm64.dmg.blockmap) label="macOS Apple Silicon DMG blockmap (arm64)" ;;
*Apple-Silicon-arm64.zip.blockmap) label="macOS Apple Silicon ZIP blockmap (arm64)" ;;
*Intel-x64.dmg) label="macOS Intel DMG (x64)" ;;
*Intel-x64.zip) label="macOS Intel ZIP (x64)" ;;
*Intel-x64.dmg.blockmap) label="macOS Intel DMG blockmap (x64)" ;;
*Intel-x64.zip.blockmap) label="macOS Intel ZIP blockmap (x64)" ;;
*) label="$name" ;;
esac
upload_args+=("${asset}#${label}")
done
upload_args+=("latest-mac.yml#macOS update metadata")
gh release upload "$GITHUB_REF_NAME" "${upload_args[@]}" --clobber
npx electron-builder --config build/electron-builder.local.cjs --mac --publish always
windows:
name: Windows
needs: publish-macos
needs: macos
runs-on: windows-latest
steps:
- name: Check out repository
@ -174,7 +82,7 @@ jobs:
linux:
name: Linux
needs: publish-macos
needs: macos
runs-on: ubuntu-latest
steps:
- name: Check out repository

6
.gitignore vendored
View file

@ -13,8 +13,4 @@ release
tmp
release-local
logs
.opencat
test-results
playwright-report
blob-report
.tmp
.opencat

View file

@ -1,75 +0,0 @@
ARG NODE_IMAGE=node:22-bookworm
ARG RUNTIME_NODE_IMAGE=node:22-bookworm-slim
FROM ${NODE_IMAGE} AS build
WORKDIR /app
COPY package.json package-lock.json ./
COPY packages/cli/package.json packages/cli/package.json
COPY packages/core/package.json packages/core/package.json
COPY packages/electron/package.json packages/electron/package.json
COPY packages/ui/package.json packages/ui/package.json
RUN npm ci
COPY . .
RUN npm run build:docker
FROM ${NODE_IMAGE} AS production-deps
WORKDIR /app
ENV NODE_ENV=production
COPY package.json package-lock.json ./
COPY packages/core/package.json packages/core/package.json
RUN npm ci --omit=dev --workspace=@claude-code-router/core --include-workspace-root=false \
&& npm cache clean --force
FROM ${RUNTIME_NODE_IMAGE} AS runtime
ENV NODE_ENV=production \
CCR_DATA_DIR=/data \
CCR_WEB_HOST=127.0.0.1 \
CCR_WEB_PORT=3459 \
CCR_NGINX_PORT=8080 \
CCR_GATEWAY_HOST=127.0.0.1 \
CCR_GATEWAY_PORT=3456 \
CCR_GATEWAY_CORE_PORT=3457 \
CCR_PUBLIC_HOST=127.0.0.1 \
CCR_PUBLIC_PORT=3458
WORKDIR /app
RUN apt-get update \
&& apt-get install -y --no-install-recommends ca-certificates libstdc++6 nginx \
&& rm -rf /var/lib/apt/lists/* \
&& rm -f /etc/nginx/sites-enabled/default /etc/nginx/conf.d/default.conf \
&& rm -rf \
/opt/yarn-* \
/usr/local/bin/corepack \
/usr/local/bin/npm \
/usr/local/bin/npx \
/usr/local/bin/yarn \
/usr/local/bin/yarnpkg \
/usr/local/include/node \
/usr/local/lib/node_modules/corepack \
/usr/local/lib/node_modules/npm \
/usr/local/share/doc \
/usr/local/share/man
COPY package.json package-lock.json ./
COPY packages/core/package.json packages/core/package.json
COPY --from=production-deps /app/node_modules node_modules
COPY --from=build /app/packages/core/dist packages/core/dist
COPY --from=build /app/packages/ui/dist/renderer /usr/share/nginx/html
COPY docker/entrypoint.sh /usr/local/bin/ccr-docker-entrypoint
COPY docker/pm2.config.cjs docker/pm2.config.cjs
RUN chmod +x /usr/local/bin/ccr-docker-entrypoint \
&& mkdir -p /data /run/nginx /var/lib/nginx /var/log/nginx
VOLUME ["/data"]
EXPOSE 8080
HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \
CMD node -e "fetch('http://127.0.0.1:' + (process.env.CCR_NGINX_PORT || '8080') + '/').then((r) => process.exit(r.ok ? 0 : 1)).catch(() => process.exit(1))"
ENTRYPOINT ["ccr-docker-entrypoint"]

21
LICENSE
View file

@ -1,21 +0,0 @@
MIT License
Copyright (c) 2025 musistudio
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

594
README.md
View file

@ -1,127 +1,56 @@
<h1 align="center">Claude Code Router</h1>
<h1 align="center">Claude Code Router Desktop</h1>
<p align="center">
<a href="README_zh.md"><img alt="Chinese README" src="https://img.shields.io/badge/%F0%9F%87%A8%F0%9F%87%B3-%E4%B8%AD%E6%96%87%E7%89%88-ff0000?style=flat" /></a>
<a href="https://discord.gg/rdftVMaUcS"><img alt="Discord" src="https://img.shields.io/badge/Discord-%235865F2.svg?&logo=discord&logoColor=white" /></a>
<a href="https://x.com/musistudio2026"><img alt="X" src="https://img.shields.io/badge/X-@musistudio2026-000000?logo=x&logoColor=white" /></a>
<a href="https://github.com/musistudio/claude-code-router/blob/main/LICENSE"><img alt="License" src="https://img.shields.io/github/license/musistudio/claude-code-router" /></a>
<a href="https://github.com/musistudio/claude-code-router/releases"><img alt="Desktop downloads" src="https://img.shields.io/github/downloads/musistudio/claude-code-router/total?label=Desktop%20downloads&logo=github" /></a>
<a href="https://ccrdesk.top/"><img alt="Documentation" src="https://img.shields.io/badge/Docs-ccrdesk.top-0ea5e9?style=flat" /></a>
</p>
<div align="center">
<table width="100%">
<tr>
<td align="center">
<a href="https://www.kimi.com/code?aff=ccr">
<img src="https://gcdn.moonshot.cn/growth-cdn/sponsor/kimi-en.png" width="960" alt="Kimi K2.7 Code sponsor banner" />
</a>
<br />
<sub>
<a href="https://www.kimi.com/code?aff=ccr"><strong>Kimi Code Subscription</strong></a>
&nbsp;·&nbsp;
<a href="https://platform.kimi.ai?aff=ccr"><strong>API Global</strong></a>
&nbsp;·&nbsp;
<a href="https://platform.kimi.com?aff=ccr">API China</a>
</sub>
</td>
</tr>
<tr>
<td align="left">
<p>
<strong>Thanks to Kimi for sponsoring this project!</strong> Kimi K2.7 Code is an open-source, coding-focused agentic model developed by Moonshot AI, with substantial gains on real-world long-horizon coding tasks and higher end-to-end success across complex software engineering workflows. It also cuts thinking-token usage by approximately 30% compared with K2.6. Inside CCR, Kimi ships as built-in provider presets: import the pay-as-you-go API or the Kimi Code subscription in one click and route your coding agent's requests to Kimi, the subscription endpoint passes straight through natively with no protocol conversion, API endpoints are adapted automatically, and your balance and subscription usage show up right in the CCR dashboard.
</p>
<p align="center">
CCR already supports Kimi. Visit the Kimi Open Platform (<a href="https://platform.kimi.com?aff=ccr">中文站</a> | <a href="https://platform.kimi.ai?aff=ccr">Global</a>) to try the API, or explore the <a href="https://www.kimi.com/code?aff=ccr">cost-effective Coding Plan</a>.
</p>
</td>
</tr>
</table>
</div>
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.
<p align="center">
<img src="blog/images/claude-code-router.png" width="720" alt="Claude Code Router Desktop screenshot" />
<img src="blog/images/claude-code-router.png" alt="Claude Code Router Desktop screenshot" />
</p>
## What CCR Helps You Do
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.
| 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 |
CCR runs on your machine, keeps provider configuration in your local config directory, and exposes a local gateway at `http://127.0.0.1:3456`.
## Why Use CCR
- **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.
- Use one local endpoint for multiple agent tools instead of configuring every client separately.
- Route different workloads to different models, such as fast background work, reasoning tasks, long-context requests, image tasks, or web-search-capable models.
- Mix providers without changing your workflow. CCR supports OpenAI-compatible APIs, Anthropic Messages, Gemini Generate Content, OpenRouter, DeepSeek, SiliconFlow, Moonshot, Mistral, Z.AI, Bailian, and custom providers.
- Control cost and reliability with fallback routing, API key rotation, usage statistics, and request logs.
- Manage everything from a desktop UI instead of editing JSON by hand.
- Extend the gateway with plugins, proxy routes, local HTTP backends, and provider deeplinks.
## Feature Highlights
## Features
- **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/), including the [CLI reference](https://ccrdesk.top/en/guides/cli/) and [Docker deployment guide](https://ccrdesk.top/en/guides/docker/).
- **Desktop dashboard**: start or stop the local gateway, inspect usage, configure the tray window, and manage runtime settings.
- **Provider management**: add provider presets or custom endpoints, test connectivity, manage credentials, and monitor supported account balances where available.
- **Routing rules**: set default, background, thinking, long-context, image, web-search, subagent, model-prefix, and conditional routing rules.
- **Agent profiles**: configure Claude Code, Codex, and ZCode profiles that point to the CCR gateway.
- **Gateway compatibility**: translate client requests through the local CCR wrapper and the core gateway runtime.
- **Proxy mode**: capture supported API traffic through a local proxy with optional system proxy integration and network capture.
- **Plugins**: install or load wrapper plugins, including routes for Claude Design and Cursor Proxy style integrations.
- **Virtual models**: expose aliases or composed model profiles for clients that expect a specific model name.
- **Provider deeplinks**: import provider configuration through `ccr://provider?...` links after user confirmation.
## Download And Install
1. Open the [GitHub Releases page](https://github.com/musistudio/claude-code-router/releases).
2. Download the package for your platform:
- macOS Apple Silicon: `Claude-Code-Router_<version>-mac-Apple-Silicon-arm64.dmg` or `.zip`
- macOS Intel: `Claude-Code-Router_<version>-mac-Intel-x64.dmg` or `.zip`
- macOS: `Claude Code Router_<version>.dmg` or `.zip`
- Windows: `Claude Code Router_<version>.exe`
- Linux: `Claude Code Router_<version>.AppImage`
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`
4. On first launch, CCR creates its local configuration:
- macOS/Linux: `~/.claude-code-router/config.json`
- Windows: `%APPDATA%\Claude Code Router\config.json`
CCR stores runtime configuration in SQLite. A legacy `config.json` is read only once for migration when no SQLite config exists.
CCR starts two local services when the gateway is enabled:
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.
- CCR wrapper gateway: `http://127.0.0.1:3456`
- Core gateway runtime: `http://127.0.0.1:3457`
## Quick Start
@ -129,307 +58,226 @@ 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, 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.
Open **Providers**, click **Add Provider**, then choose a built-in preset or create a custom provider. Fill in the provider name, endpoint, protocol, API key, and model list in the form. Use the connectivity check when available, then save the provider.
### 2. Configure routing
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.
Open **Routing** and select which provider/model should handle the default route. Then fill optional routes for background work, thinking requests, long-context requests, image tasks, and web search if you want different models for those scenarios.
Use **Add Routing Rule** when you need more control, such as model-prefix routing, subagent routing, request conditions, or fallback behavior.
### 3. Start the gateway
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.
Open **Server** and click **Start**. 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, 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.
Open **Profiles** and choose the client you want to use. Configure the Claude Code, Codex, or ZCode profile from the form, select the target model, and apply the profile. For app-based profiles, use the profile action button to open 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 dashboard and tray window for token, cost, model distribution, and account status.
Use **Dashboard** for usage and provider health, the tray window for quick token and account status, **Network Logs** for debugging provider behavior, and **Extensions** for plugin configuration.
## Provider Deeplink
Provider websites can open CCR and import a model provider with a custom protocol link:
```text
ccr://provider?name=Example%20AI&base_url=https%3A%2F%2Fapi.example.com%2Fv1&api_key=sk-example&models=example-chat%2Cexample-coder&protocol=openai_chat_completions
```
Supported query parameters:
- `name`: display name for the provider.
- `base_url`: provider API base URL.
- `api_key`: optional provider API key.
- `models`: comma-separated or newline-separated model list. You can also repeat `models=...`.
- `protocol`: one of `openai_chat_completions`, `openai_responses`, `anthropic_messages`, or `gemini_generate_content`.
For larger payloads, pass `payload` as URL-encoded JSON or base64url JSON with the same fields. CCR always opens a confirmation dialog before writing a provider imported from an external link.
## Plugins
CCR has two plugin layers:
- Core gateway plugins: use `providerPlugins` and `virtualModelProfiles`; these are passed through to the core gateway.
- Wrapper plugins: use top-level `plugins` to extend the Electron wrapper, register local HTTP backends, add gateway routes, and route proxy-mode traffic to plugin backends.
Example wrapper plugin route:
```json
{
"plugins": [
{
"id": "local-admin-api",
"enabled": true,
"proxy": {
"routes": [
{
"id": "admin-api",
"host": "api.example.com",
"paths": ["/v1/admin"],
"upstream": "http://127.0.0.1:4510",
"stripPathPrefix": false
}
]
}
}
]
}
```
Plugin modules export a function or object with `setup(ctx)`. The context supports:
- `ctx.registerGatewayRoute({ method, path, auth, handler })`
- `ctx.registerHttpBackend({ id, host, port, handler })`
- `ctx.registerProxyRoute({ host, paths, upstream, stripPathPrefix, rewritePathPrefix, headers })`
- `ctx.openSqliteStore({ filename, migrate })`
- `ctx.registerCoreGatewayProviderPlugin(plugin)`
- `ctx.registerCoreGatewayVirtualModelProfile(profile)`
Local plugin examples are available in [examples/plugins](examples/plugins).
## Development
```bash
npm install
npm run dev
npm run typecheck
npm run build:assets
npm run build:app:mac
npm run build:app:win
```
`npm run build:assets` compiles the Electron main process and renderer assets into `dist/`.
`npm run build` packages the app for the current platform and writes installer artifacts to `release/`.
`npm run build:app:mac` and `npm run build:app:win` package platform-specific app artifacts. Linux AppImage packaging is configured in `electron-builder.json`.
`npm run build:app:mac` creates a local macOS test package in `release-local/` using ad-hoc signing. It is useful with a free Apple Account or Apple Development certificate, but it is not suitable for public distribution because downloaded copies will not pass Gatekeeper notarization checks.
macOS release builds are signed and notarized for distribution. Before running `npm run build:app:mac:release`, the build machine must have a `Developer ID Application` certificate available through the keychain or `CSC_LINK`/`CSC_KEY_PASSWORD`, full Xcode selected with `xcode-select`, and one notarization credential set:
- `APPLE_API_KEY`, `APPLE_API_KEY_ID`, and `APPLE_API_ISSUER`
- `APPLE_ID`, `APPLE_APP_SPECIFIC_PASSWORD`, and `APPLE_TEAM_ID`
- `APPLE_KEYCHAIN_PROFILE`, optionally with `APPLE_KEYCHAIN`
The macOS packaging hook validates codesigning, the stapled notarization ticket, and Gatekeeper assessment before writing distributable artifacts.
Packaged builds check GitHub Releases for updates through `electron-updater`. For local update feed testing, set `CCR_UPDATE_FEED_URL` to a generic electron-updater feed URL before starting the app. `CCR_UPDATE_ALLOW_PRERELEASE=1` enables prerelease updates.
## Further Reading
- [Project Motivation and How It Works](blog/en/project-motivation-and-how-it-works.md)
- [Maybe We Can Do More with the Router](blog/en/maybe-we-can-do-more-with-the-route.md)
## Acknowledgements
Codex support is powered by [musistudio/codexl](https://github.com/musistudio/codexl).
Codex support and Bot handoff are powered by [musistudio/codexl](https://github.com/musistudio/codexl).
## Support & Sponsoring
<div align="center">
If you find this project helpful, please consider sponsoring its development. Your support is greatly appreciated.
<p>If you find this project helpful, please consider sponsoring its development. Your support is greatly appreciated.</p>
[![ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/F1F31GN2GM)
[Paypal](https://paypal.me/musistudio1999)
<table>
<tr>
<td align="center" width="220">
<a href="https://ko-fi.com/F1F31GN2GM">
<img src="https://ko-fi.com/img/githubbutton_sm.svg" alt="Support on Ko-fi" />
</a>
<br />
<sub>One-time support via Ko-fi</sub>
</td>
<td align="center" width="220">
<a href="https://paypal.me/musistudio1999">
<img src="https://img.shields.io/badge/PayPal-Sponsor-003087?logo=paypal&logoColor=white" alt="Sponsor with PayPal" />
</a>
<br />
<sub>International sponsorship</sub>
</td>
<td><img src="/blog/images/alipay.jpg" width="200" alt="Alipay" /></td>
<td><img src="/blog/images/wechat.jpg" width="200" alt="WeChat Pay" /></td>
</tr>
</table>
<table>
<tr>
<td align="center" width="220">
<strong>Alipay</strong>
<br />
<img src="/blog/images/alipay.jpg" width="160" alt="Alipay QR code" />
</td>
<td align="center" width="220">
<strong>WeChat Pay</strong>
<br />
<img src="/blog/images/wechat.jpg" width="160" alt="WeChat Pay QR code" />
</td>
</tr>
</table>
</div>
### Our Sponsors
<div align="center">
A huge thank you to all our sponsors for their generous support.
<p>A huge thank you to all our sponsors for their generous support.</p>
- [AIHubmix](https://aihubmix.com/)
- [BurnCloud](https://ai.burncloud.com)
- @Simon Leischnig
- [@duanshuaimin](https://github.com/duanshuaimin)
- [@vrgitadmin](https://github.com/vrgitadmin)
- @\*o
- [@ceilwoo](https://github.com/ceilwoo)
- @\*说
- @\*更
- @K\*g
- @R\*R
- [@bobleer](https://github.com/bobleer)
- @\*苗
- @\*划
- [@Clarence-pan](https://github.com/Clarence-pan)
- [@carter003](https://github.com/carter003)
- @S\*r
- @\*晖
- @\*敏
- @Z\*z
- @\*然
- [@cluic](https://github.com/cluic)
- @\*苗
- [@PromptExpert](https://github.com/PromptExpert)
- @\*应
- [@yusnake](https://github.com/yusnake)
- @\*飞
- @董\*
- @\*汀
- @\*涯
- @\*:-
- @\*\*磊
- @\*琢
- @\*成
- @Z\*o
- @\*琨
- [@congzhangzh](https://github.com/congzhangzh)
- @\*\_
- @Z\*m
- @\*鑫
- @c\*y
- @\*昕
- [@witsice](https://github.com/witsice)
- @b\*g
- @\*亿
- @\*辉
- @JACK
- @\*光
- @W\*l
- [@kesku](https://github.com/kesku)
- [@biguncle](https://github.com/biguncle)
- @二吉吉
- @a\*g
- @\*林
- @\*咸
- @\*明
- @S\*y
- @f\*o
- @\*智
- @F\*t
- @r\*c
- [@qierkang](http://github.com/qierkang)
- @\*军
- [@snrise-z](http://github.com/snrise-z)
- @\*王
- [@greatheart1000](http://github.com/greatheart1000)
- @\*王
- @zcutlip
- [@Peng-YM](http://github.com/Peng-YM)
- @\*更
- @\*.
- @F\*t
- @\*政
- @\*铭
- @\*叶
- @七\*o
- @\*青
- @\*\*晨
- @\*远
- @\*霄
- @\*\*吉
- @\*\*飞
- @\*\*驰
- @x\*g
<table width="100%">
<tr>
<td align="center" width="330">
<a href="https://www.bigmodel.cn/claude-code?ic=FPF9IVAGFJ">
<img src="/docs/public/provider-icons/zhipu-cn-general.png" width="42" height="42" alt="Zhipu icon" />
<br />
<strong>Z智谱</strong>
</a>
</td>
<td align="center" width="330">
<a href="https://aihubmix.com/">
<img src="https://www.google.com/s2/favicons?domain=aihubmix.com&amp;sz=128" width="42" height="42" alt="AIHubmix icon" />
<br />
<strong>AIHubmix</strong>
</a>
</td>
<td align="center" width="330">
<a href="https://ai.burncloud.com">
<img src="https://www.burncloud.com/favicon.png" width="42" height="42" alt="BurnCloud icon" />
<br />
<strong>BurnCloud</strong>
</a>
</td>
<td align="center" width="330">
<a href="https://share.302.ai/ZGVF9w">
<img src="https://www.google.com/s2/favicons?domain=302.ai&amp;sz=128" width="42" height="42" alt="302.AI icon" />
<br />
<strong>302.AI</strong>
</a>
</td>
</tr>
<tr>
<td align="center" width="330">
<a href="https://runapi.co/register?aff=IX1t">
<img src="/docs/public/provider-icons/runapi.jpg" width="42" height="42" alt="RunAPI icon" />
<br />
<strong>RunAPI</strong>
</a>
</td>
<td align="center" width="330">
<a href="https://teamorouter.com/">
<img src="/docs/public/provider-icons/teamorouter.png" width="42" height="42" alt="TeamoRouter icon" />
<br />
<strong>TeamoRouter</strong>
</a>
</td>
<td align="center" width="330">
<a href="https://code0.ai/agent/register/9n9jOsSnYQoemIVL?utm_source=claudecoderouter&amp;utm_medium=partner&amp;utm_campaign=claudecoderouter_2026&amp;utm_content=default">
<img src="/docs/public/provider-icons/code0.png" width="42" height="42" alt="code0.ai icon" />
<br />
<strong>code0.ai</strong>
</a>
</td>
<td align="center" width="330">
<a href="https://console.claudeapi.com/agent/register/LbmB7Y9kPloyzhwF?utm_source=claudecoderouter&amp;utm_medium=partner&amp;utm_campaign=claudecoderouter_2026&amp;utm_content=default">
<img src="/docs/public/provider-icons/claudeapi.png" width="42" height="42" alt="claudeapi icon" />
<br />
<strong>claudeapi</strong>
</a>
</td>
</tr>
<tr>
<td align="center" width="330">
<a href="https://s.qiniu.com/AVjMVf">
<img src="/docs/public/provider-icons/qiniu-ai.png" width="42" height="42" alt="Qiniu Cloud AI icon" />
<br />
<strong>Qiniu Cloud AI</strong>
</a>
</td>
<td align="center" width="330">
<a href="https://api.fenno.ai/register?redirect=/purchase?tab=subscription%26group=16&amp;aff=9HHHAB5QLAES">
<img src="/docs/public/provider-icons/fenno.jpg" width="42" height="42" alt="Fenno.ai icon" />
<br />
<strong>Fenno.ai</strong>
</a>
</td>
<td align="center" width="330">
<a href="https://unity2.ai/register?source=claudecoderouter">
<img src="/docs/public/provider-icons/unity2.jpg" width="42" height="42" alt="Unity2.Ai icon" />
<br />
<strong>Unity2.Ai</strong>
</a>
</td>
</tr>
</table>
<h4>Community Sponsors</h4>
<table width="100%">
<tr>
<td align="center" width="220">@Simon Leischnig</td>
<td align="center" width="220"><a href="https://github.com/duanshuaimin">@duanshuaimin</a></td>
<td align="center" width="220"><a href="https://github.com/vrgitadmin">@vrgitadmin</a></td>
<td align="center" width="220">@*o</td>
<td align="center" width="220"><a href="https://github.com/ceilwoo">@ceilwoo</a></td>
<td align="center" width="220">@*说</td>
</tr>
<tr>
<td align="center" width="220">@*更</td>
<td align="center" width="220">@K*g</td>
<td align="center" width="220">@R*R</td>
<td align="center" width="220"><a href="https://github.com/bobleer">@bobleer</a></td>
<td align="center" width="220">@*苗</td>
<td align="center" width="220">@*划</td>
</tr>
<tr>
<td align="center" width="220"><a href="https://github.com/Clarence-pan">@Clarence-pan</a></td>
<td align="center" width="220"><a href="https://github.com/carter003">@carter003</a></td>
<td align="center" width="220">@S*r</td>
<td align="center" width="220">@*晖</td>
<td align="center" width="220">@*敏</td>
<td align="center" width="220">@Z*z</td>
</tr>
<tr>
<td align="center" width="220">@*然</td>
<td align="center" width="220"><a href="https://github.com/cluic">@cluic</a></td>
<td align="center" width="220">@*苗</td>
<td align="center" width="220"><a href="https://github.com/PromptExpert">@PromptExpert</a></td>
<td align="center" width="220">@*应</td>
<td align="center" width="220"><a href="https://github.com/yusnake">@yusnake</a></td>
</tr>
<tr>
<td align="center" width="220">@*飞</td>
<td align="center" width="220">@董*</td>
<td align="center" width="220">@*汀</td>
<td align="center" width="220">@*涯</td>
<td align="center" width="220">@*:-</td>
<td align="center" width="220">@**磊</td>
</tr>
<tr>
<td align="center" width="220">@*琢</td>
<td align="center" width="220">@*成</td>
<td align="center" width="220">@Z*o</td>
<td align="center" width="220">@*琨</td>
<td align="center" width="220"><a href="https://github.com/congzhangzh">@congzhangzh</a></td>
<td align="center" width="220">@*_</td>
</tr>
<tr>
<td align="center" width="220">@Z*m</td>
<td align="center" width="220">@*鑫</td>
<td align="center" width="220">@c*y</td>
<td align="center" width="220">@*昕</td>
<td align="center" width="220"><a href="https://github.com/witsice">@witsice</a></td>
<td align="center" width="220">@b*g</td>
</tr>
<tr>
<td align="center" width="220">@*亿</td>
<td align="center" width="220">@*辉</td>
<td align="center" width="220">@JACK</td>
<td align="center" width="220">@*光</td>
<td align="center" width="220">@W*l</td>
<td align="center" width="220"><a href="https://github.com/kesku">@kesku</a></td>
</tr>
<tr>
<td align="center" width="220"><a href="https://github.com/biguncle">@biguncle</a></td>
<td align="center" width="220">@二吉吉</td>
<td align="center" width="220">@a*g</td>
<td align="center" width="220">@*林</td>
<td align="center" width="220">@*咸</td>
<td align="center" width="220">@*明</td>
</tr>
<tr>
<td align="center" width="220">@S*y</td>
<td align="center" width="220">@f*o</td>
<td align="center" width="220">@*智</td>
<td align="center" width="220">@F*t</td>
<td align="center" width="220">@r*c</td>
<td align="center" width="220"><a href="https://github.com/qierkang">@qierkang</a></td>
</tr>
<tr>
<td align="center" width="220">@*军</td>
<td align="center" width="220"><a href="https://github.com/snrise-z">@snrise-z</a></td>
<td align="center" width="220">@*王</td>
<td align="center" width="220"><a href="https://github.com/greatheart1000">@greatheart1000</a></td>
<td align="center" width="220">@*王</td>
<td align="center" width="220">@zcutlip</td>
</tr>
<tr>
<td align="center" width="220"><a href="https://github.com/Peng-YM">@Peng-YM</a></td>
<td align="center" width="220">@*更</td>
<td align="center" width="220">@*.</td>
<td align="center" width="220">@F*t</td>
<td align="center" width="220">@*政</td>
<td align="center" width="220">@*铭</td>
</tr>
<tr>
<td align="center" width="220">@*叶</td>
<td align="center" width="220">@七*o</td>
<td align="center" width="220">@*青</td>
<td align="center" width="220">@**晨</td>
<td align="center" width="220">@*远</td>
<td align="center" width="220">@*霄</td>
</tr>
<tr>
<td align="center" width="220">@**吉</td>
<td align="center" width="220">@**飞</td>
<td align="center" width="220">@**驰</td>
<td align="center" width="220">@x*g</td>
<td align="center" width="220">@**东</td>
<td align="center" width="220">@*落</td>
</tr>
<tr>
<td align="center" width="220">@哆*k</td>
<td align="center" width="220">@*涛</td>
<td align="center" width="220"><a href="https://github.com/WitMiao">@苗大</a></td>
<td align="center" width="220">@*呢</td>
<td align="center" width="220">@d*u</td>
<td align="center" width="220">@crizcraig</td>
</tr>
<tr>
<td align="center" width="220">s*s</td>
<td align="center" width="220">*火</td>
<td align="center" width="220">*勤</td>
<td align="center" width="220">**锟</td>
<td align="center" width="220">*涛</td>
<td align="center" width="220">**明</td>
</tr>
<tr>
<td align="center" width="220">*知</td>
<td align="center" width="220">*语</td>
<td align="center" width="220">*瓜</td>
<td align="center" width="220"></td>
<td align="center" width="220"></td>
<td align="center" width="220"></td>
</tr>
</table>
<sub>If your name is masked, please contact me via my homepage email to update it with your GitHub username.</sub>
</div>
## License
This project is licensed under the [MIT License](LICENSE).
(If your name is masked, please contact me via my homepage email to update it with your GitHub username.)

View file

@ -1,127 +1,56 @@
<h1 align="center">Claude Code Router</h1>
<h1 align="center">Claude Code Router Desktop</h1>
<p align="center">
<a href="README.md"><img alt="English README" src="https://img.shields.io/badge/%F0%9F%87%AC%F0%9F%87%A7-English-000aff?style=flat" /></a>
<a href="https://discord.gg/rdftVMaUcS"><img alt="Discord" src="https://img.shields.io/badge/Discord-%235865F2.svg?&logo=discord&logoColor=white" /></a>
<a href="https://x.com/musistudio2026"><img alt="X" src="https://img.shields.io/badge/X-@musistudio2026-000000?logo=x&logoColor=white" /></a>
<a href="https://github.com/musistudio/claude-code-router/blob/main/LICENSE"><img alt="License" src="https://img.shields.io/github/license/musistudio/claude-code-router" /></a>
<a href="https://github.com/musistudio/claude-code-router/releases"><img alt="桌面端下载次数" src="https://img.shields.io/github/downloads/musistudio/claude-code-router/total?label=%E6%A1%8C%E9%9D%A2%E7%AB%AF%E4%B8%8B%E8%BD%BD&logo=github" /></a>
<a href="https://ccrdesk.top/"><img alt="文档" src="https://img.shields.io/badge/%E6%96%87%E6%A1%A3-ccrdesk.top-0ea5e9?style=flat" /></a>
</p>
<div align="center">
<table width="100%">
<tr>
<td align="center">
<a href="https://www.kimi.com/code?aff=ccr">
<img src="https://gcdn.moonshot.cn/growth-cdn/sponsor/kimi-zh.png" width="960" alt="Kimi K2.7 Code 赞助横幅" />
</a>
<br />
<sub>
<a href="https://www.kimi.com/code?aff=ccr"><strong>Kimi Code 订阅</strong></a>
&nbsp;·&nbsp;
<a href="https://platform.kimi.com?aff=ccr"><strong>API 中文站</strong></a>
&nbsp;·&nbsp;
<a href="https://platform.kimi.ai?aff=ccr">API Global</a>
</sub>
</td>
</tr>
<tr>
<td align="left">
<p>
<strong>感谢 Kimi 赞助本项目!</strong>Kimi K2.7 Code 是 Moonshot AI 推出的编程专用开源智能体模型,在真实长程编程与复杂软件工程工作流中显著提升端到端任务成功率,同时优化推理效率,相比 K2.6 平均减少约 30% 的推理 token 消耗。在 CCR 中Kimi 已作为内置供应商预设开箱即用:无论按量付费 API 还是 Kimi Code 订阅,一键导入即可把你的编程 Agent 请求路由到 Kimi订阅端点原生直通、无需协议转换API 端点自动适配,账户余额与订阅用量也能直接在 CCR 面板中查看。
</p>
<p align="center">
CCR 已内置 Kimi 供应商预设。前往 Kimi 开放平台(<a href="https://platform.kimi.com?aff=ccr">中文站</a><a href="https://platform.kimi.ai?aff=ccr">Global</a>)体验 API或了解高性价比 <a href="https://www.kimi.com/code?aff=ccr">Coding Plan</a> 套餐。
</p>
</td>
</tr>
</table>
</div>
Claude Code Router Desktop 是给编程 Agent 用的本地控制平面。它为 Claude Code、Codex、Grok CLI、ZCode 以及兼容 API 客户端提供一个稳定的本地入口,然后由你在 CCR 中决定每个请求应该走哪个供应商、哪个模型、哪套路由策略、哪些工具能力和哪组账号凭据。
相比在每个 Agent、每个模型服务里反复改配置CCR 把模型层收束到本机桌面应用里供应商预设、自定义端点、凭据池、Fallback、Fusion 组合模型、MCP 工具、请求日志、账号用量和 Agent 启动配置都在一个地方管理。
<p align="center">
<img src="blog/images/claude-code-router.png" width="720" alt="Claude Code Router Desktop 项目截图" />
<img src="blog/images/claude-code-router.png" alt="Claude Code Router Desktop 项目截图" />
</p>
## CCR 能帮你做什么
Claude Code Router Desktop 是一个本地网关和桌面控制台,用来把 Claude Code、Codex、ZCode 以及兼容客户端的 Agent 请求路由到你真正想使用的模型服务。
| 目标 | 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 在你的本机运行Provider 配置保存在本地配置目录,并默认暴露本地网关地址:`http://127.0.0.1:3456`
## 为什么使用 CCR
- **一个本地网关,接管整套 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或安装本地扩展。
- 用一个本地入口连接多个 Agent 工具,不需要在每个客户端里重复配置 Provider。
- 不同任务使用不同模型,例如后台任务、推理任务、长上下文、图片任务或支持联网搜索的模型。
- 在不改变工作流的情况下混用不同 Provider。CCR 支持 OpenAI 兼容 API、Anthropic Messages、Gemini Generate Content、OpenRouter、DeepSeek、SiliconFlow、Moonshot、Mistral、Z.AI、百炼以及自定义 Provider。
- 通过 fallback 路由、API Key 轮换、用量统计和请求日志来控制成本和可靠性。
- 使用桌面 UI 管理配置,减少手写 JSON。
- 通过插件、代理路由、本地 HTTP 后端和 Provider deeplink 扩展网关能力。
## 功能亮点
## 功能和特性
- **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/),其中包括 [CLI 命令参考](https://ccrdesk.top/guides/cli/) 和 [Docker 部署指南](https://ccrdesk.top/guides/docker/)。
- **桌面控制台**:启动或停止本地网关,查看用量,配置托盘窗口和运行时设置。
- **Provider 管理**:添加预设或自定义端点,检测连通性,管理凭据,并在可用时查看账号余额。
- **路由规则**配置默认、后台、thinking、长上下文、图片、Web Search、Subagent、模型前缀和条件路由。
- **Agent Profiles**:为 Claude Code、Codex 和 ZCode 配置指向 CCR 网关的 Profile。
- **网关兼容层**:通过本地 CCR wrapper 和 core gateway runtime 转换客户端请求。
- **代理模式**:通过本地代理捕获支持的 API 流量,可选系统代理和网络捕获。
- **插件系统**:安装或加载 wrapper 插件,包括 Claude Design、Cursor Proxy 这类集成路由。
- **虚拟模型**:为客户端暴露模型别名或组合模型配置,适配固定模型名场景。
- **Provider Deeplink**:通过 `ccr://provider?...` 链接导入 Provider 配置,写入前会弹出确认。
## 下载和安装
1. 打开 [GitHub Releases 页面](https://github.com/musistudio/claude-code-router/releases)。
2. 按系统下载对应安装包:
- macOS Apple 芯片:`Claude-Code-Router_<version>-mac-Apple-Silicon-arm64.dmg``.zip`
- macOS Intel 芯片:`Claude-Code-Router_<version>-mac-Intel-x64.dmg``.zip`
- macOS`Claude Code Router_<version>.dmg``.zip`
- Windows`Claude Code Router_<version>.exe`
- Linux`Claude Code Router_<version>.AppImage`
3. 安装并启动 **Claude Code Router**
4. 首次启动后CCR 会创建本地配置数据库
- macOS/Linux`~/.claude-code-router/config.sqlite`
- Windows`%APPDATA%\claude-code-router\config.sqlite`
4. 首次启动后CCR 会创建本地配置:
- macOS/Linux`~/.claude-code-router/config.json`
- Windows`%APPDATA%\Claude Code Router\config.json`
CCR 的运行配置存储在 SQLite 中。旧版 `config.json` 只会在没有 SQLite 配置时作为迁移来源读取一次。
启用网关后CCR 会启动两个本地服务:
**服务** 页面启动后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`、数据卷、备份恢复、升级和健康检查。
- CCR wrapper gateway`http://127.0.0.1:3456`
- Core gateway runtime`http://127.0.0.1:3457`
## 快速开始
@ -129,307 +58,226 @@ CCR 可以完全通过桌面 UI 完成配置。首次使用建议按下面顺序
### 1. 添加 Provider
打开 **供应商**,点击 **添加供应商**,选择内置预设、导入支持的本机 Agent 登录态,或选择 **其他 / 自定义 API 端点**。按表单填写 Provider 名称、基础 URL、协议、API Key 和模型列表。可用时先运行协议探测和模型连通性检查,然后保存 Provider。
打开 **Providers**,点击 **Add Provider**,选择内置预设或创建自定义 Provider。按表单填写 Provider 名称、端点、协议、API Key 和模型列表。可用时先运行连通性检测,然后保存 Provider。
### 2. 设置路由
打开 **路由**,启用内置 Agent 路由,添加条件规则,配置请求改写和失败降级。如果需要更细粒度控制,使用 **添加路由规则** 添加模型前缀、请求条件或规则级失败降级目标。
打开 **Routing**,先选择默认路由要使用的 provider/model。然后根据需要设置后台任务、Thinking、长上下文、图片任务和 Web Search 等场景的专用模型。
如果需要更细粒度控制,使用 **Add Routing Rule** 添加模型前缀、Subagent、请求条件或 fallback 规则。
### 3. 启动网关
打开 **服务**,点击 **启动**。页面显示运行中后CCR 默认会在本机监听 `http://127.0.0.1:3456`。如果希望每次打开桌面应用时自动启动网关,可以启用自动启动
打开 **Server**,点击 **Start** 启动本地网关。如果希望每次打开桌面应用时自动启动网关,可以启用 auto start
### 4. 连接 Agent 工具
打开 **Agent配置**,选择要使用的客户端。配置 Claude Code、Codex、Grok CLI 或 ZCode选择目标模型和作用范围然后应用配置。对于 App 入口,可以使用 **打开 Agent** 通过 CCR 打开目标应用。
打开 **Profiles**,选择要使用的客户端。通过表单配置 Claude Code、Codex 或 ZCode Profile选择目标模型并应用配置。对于 App 类型的 Profile可以使用页面里的操作按钮通过 CCR 打开目标应用。
### 5. 日常查看和调整
**设置 → 日志与观测** 打开请求日志和 Agent 观测。使用 **日志** 确认 `request model``resolved provider``resolved model`、状态码、tokens、耗时和错误使用概览仪表盘和托盘窗口查看 Token、成本、模型分布和账号状态。
使用 **Dashboard** 查看用量和 Provider 状态,使用托盘窗口快速查看 Token 和账号状态,使用 **Network Logs** 调试 Provider 行为,使用 **Extensions** 配置插件。
## Provider Deeplink
Provider 网站可以通过自定义协议打开 CCR 并导入模型服务配置:
```text
ccr://provider?name=Example%20AI&base_url=https%3A%2F%2Fapi.example.com%2Fv1&api_key=sk-example&models=example-chat%2Cexample-coder&protocol=openai_chat_completions
```
支持的 query 参数:
- `name`Provider 展示名称。
- `base_url`Provider API Base URL。
- `api_key`:可选 Provider API Key。
- `models`:逗号或换行分隔的模型列表,也可以重复传入 `models=...`
- `protocol``openai_chat_completions``openai_responses``anthropic_messages``gemini_generate_content`
更大的 payload 可以通过 URL 编码 JSON 或 base64url JSON 传入 `payload` 字段。CCR 在写入外部链接导入的 Provider 前,总会弹出确认窗口。
## 插件
CCR 有两层插件:
- Core gateway plugins使用 `providerPlugins``virtualModelProfiles`,会透传给 core gateway。
- Wrapper plugins使用顶层 `plugins` 扩展 Electron wrapper注册本地 HTTP 后端、添加 gateway route或把代理模式流量路由到插件后端。
Wrapper plugin route 示例:
```json
{
"plugins": [
{
"id": "local-admin-api",
"enabled": true,
"proxy": {
"routes": [
{
"id": "admin-api",
"host": "api.example.com",
"paths": ["/v1/admin"],
"upstream": "http://127.0.0.1:4510",
"stripPathPrefix": false
}
]
}
}
]
}
```
插件模块需要导出函数或包含 `setup(ctx)` 的对象。上下文支持:
- `ctx.registerGatewayRoute({ method, path, auth, handler })`
- `ctx.registerHttpBackend({ id, host, port, handler })`
- `ctx.registerProxyRoute({ host, paths, upstream, stripPathPrefix, rewritePathPrefix, headers })`
- `ctx.openSqliteStore({ filename, migrate })`
- `ctx.registerCoreGatewayProviderPlugin(plugin)`
- `ctx.registerCoreGatewayVirtualModelProfile(profile)`
本地插件示例见 [examples/plugins](examples/plugins)。
## 开发
```bash
npm install
npm run dev
npm run typecheck
npm run build:assets
npm run build:app:mac
npm run build:app:win
```
`npm run build:assets` 会把 Electron main process 和 renderer assets 编译到 `dist/`
`npm run build` 会为当前平台打包应用,并把安装包写入 `release/`
`npm run build:app:mac``npm run build:app:win` 会分别打包对应平台的应用产物。Linux AppImage 打包配置在 `electron-builder.json` 中。
`npm run build:app:mac` 会在 `release-local/` 生成本地测试用 macOS 包,使用 ad-hoc 签名。它适合免费 Apple Account 或只有 Apple Development 证书的本机测试,但不适合公开分发,因为用户下载后仍无法通过 Gatekeeper 公证检查。
macOS 发布包会使用 Developer ID 签名并提交 Apple 公证。运行 `npm run build:app:mac:release` 前,打包机器必须具备:可用的 `Developer ID Application` 证书(在 keychain 中,或通过 `CSC_LINK`/`CSC_KEY_PASSWORD` 提供)、已通过 `xcode-select` 选择完整 Xcode以及下面任意一组公证凭据
- `APPLE_API_KEY``APPLE_API_KEY_ID``APPLE_API_ISSUER`
- `APPLE_ID``APPLE_APP_SPECIFIC_PASSWORD``APPLE_TEAM_ID`
- `APPLE_KEYCHAIN_PROFILE`,可选 `APPLE_KEYCHAIN`
macOS 打包 hook 会在产物生成前验证代码签名、公证票据 stapling 和 Gatekeeper 评估,避免发布未公证的安装包。
打包后的应用会通过 `electron-updater` 检查 GitHub Releases。测试本地更新源时可以在启动应用前设置 `CCR_UPDATE_FEED_URL` 为 generic electron-updater feed URL。`CCR_UPDATE_ALLOW_PRERELEASE=1` 可以启用 prerelease 更新。
## 深入阅读
- [项目动机和工作原理](blog/zh/项目初衷及原理.md)
- [也许我们可以用路由器做更多事情](blog/zh/或许我们能在Router中做更多事情.md)
## 致谢
对 Codex 的支持来自于 [musistudio/codexl](https://github.com/musistudio/codexl) 这个项目。
对 Codex 的支持以及 Bot handoff 来自于 [musistudio/codexl](https://github.com/musistudio/codexl) 这个项目。
## 支持与赞助
<div align="center">
如果你觉得这个项目有帮助,欢迎赞助项目开发。非常感谢你的支持。
<p>如果你觉得这个项目有帮助,欢迎赞助项目开发。非常感谢你的支持。</p>
[![ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/F1F31GN2GM)
[Paypal](https://paypal.me/musistudio1999)
<table>
<tr>
<td align="center" width="220">
<a href="https://ko-fi.com/F1F31GN2GM">
<img src="https://ko-fi.com/img/githubbutton_sm.svg" alt="通过 Ko-fi 赞助" />
</a>
<br />
<sub>通过 Ko-fi 单次赞助</sub>
</td>
<td align="center" width="220">
<a href="https://paypal.me/musistudio1999">
<img src="https://img.shields.io/badge/PayPal-Sponsor-003087?logo=paypal&logoColor=white" alt="通过 PayPal 赞助" />
</a>
<br />
<sub>国际赞助通道</sub>
</td>
<td><img src="/blog/images/alipay.jpg" width="200" alt="Alipay" /></td>
<td><img src="/blog/images/wechat.jpg" width="200" alt="WeChat Pay" /></td>
</tr>
</table>
<table>
<tr>
<td align="center" width="220">
<strong>支付宝</strong>
<br />
<img src="/blog/images/alipay.jpg" width="160" alt="支付宝收款码" />
</td>
<td align="center" width="220">
<strong>微信支付</strong>
<br />
<img src="/blog/images/wechat.jpg" width="160" alt="微信支付收款码" />
</td>
</tr>
</table>
</div>
### 我们的赞助商
<div align="center">
非常感谢所有赞助商的慷慨支持。
<p>非常感谢所有赞助商的慷慨支持。</p>
- [AIHubmix](https://aihubmix.com/)
- [BurnCloud](https://ai.burncloud.com)
- @Simon Leischnig
- [@duanshuaimin](https://github.com/duanshuaimin)
- [@vrgitadmin](https://github.com/vrgitadmin)
- @\*o
- [@ceilwoo](https://github.com/ceilwoo)
- @\*说
- @\*更
- @K\*g
- @R\*R
- [@bobleer](https://github.com/bobleer)
- @\*苗
- @\*划
- [@Clarence-pan](https://github.com/Clarence-pan)
- [@carter003](https://github.com/carter003)
- @S\*r
- @\*晖
- @\*敏
- @Z\*z
- @\*然
- [@cluic](https://github.com/cluic)
- @\*苗
- [@PromptExpert](https://github.com/PromptExpert)
- @\*应
- [@yusnake](https://github.com/yusnake)
- @\*飞
- @董\*
- @\*汀
- @\*涯
- @\*:-
- @\*\*磊
- @\*琢
- @\*成
- @Z\*o
- @\*琨
- [@congzhangzh](https://github.com/congzhangzh)
- @\*\_
- @Z\*m
- @\*鑫
- @c\*y
- @\*昕
- [@witsice](https://github.com/witsice)
- @b\*g
- @\*亿
- @\*辉
- @JACK
- @\*光
- @W\*l
- [@kesku](https://github.com/kesku)
- [@biguncle](https://github.com/biguncle)
- @二吉吉
- @a\*g
- @\*林
- @\*咸
- @\*明
- @S\*y
- @f\*o
- @\*智
- @F\*t
- @r\*c
- [@qierkang](http://github.com/qierkang)
- @\*军
- [@snrise-z](http://github.com/snrise-z)
- @\*王
- [@greatheart1000](http://github.com/greatheart1000)
- @\*王
- @zcutlip
- [@Peng-YM](http://github.com/Peng-YM)
- @\*更
- @\*.
- @F\*t
- @\*政
- @\*铭
- @\*叶
- @七\*o
- @\*青
- @\*\*晨
- @\*远
- @\*霄
- @\*\*吉
- @\*\*飞
- @\*\*驰
- @x\*g
<table width="100%">
<tr>
<td align="center" width="330">
<a href="https://www.bigmodel.cn/claude-code?ic=FPF9IVAGFJ">
<img src="/docs/public/provider-icons/zhipu-cn-general.png" width="42" height="42" alt="智谱图标" />
<br />
<strong>Z智谱</strong>
</a>
</td>
<td align="center" width="330">
<a href="https://aihubmix.com/">
<img src="https://www.google.com/s2/favicons?domain=aihubmix.com&amp;sz=128" width="42" height="42" alt="AIHubmix 图标" />
<br />
<strong>AIHubmix</strong>
</a>
</td>
<td align="center" width="330">
<a href="https://ai.burncloud.com">
<img src="https://www.burncloud.com/favicon.png" width="42" height="42" alt="BurnCloud 图标" />
<br />
<strong>BurnCloud</strong>
</a>
</td>
<td align="center" width="330">
<a href="https://share.302.ai/ZGVF9w">
<img src="https://www.google.com/s2/favicons?domain=302.ai&amp;sz=128" width="42" height="42" alt="302.AI 图标" />
<br />
<strong>302.AI</strong>
</a>
</td>
</tr>
<tr>
<td align="center" width="330">
<a href="https://runapi.co/register?aff=IX1t">
<img src="/docs/public/provider-icons/runapi.jpg" width="42" height="42" alt="RunAPI 图标" />
<br />
<strong>RunAPI</strong>
</a>
</td>
<td align="center" width="330">
<a href="https://teamorouter.com/">
<img src="/docs/public/provider-icons/teamorouter.png" width="42" height="42" alt="TeamoRouter 图标" />
<br />
<strong>TeamoRouter</strong>
</a>
</td>
<td align="center" width="330">
<a href="https://code0.ai/agent/register/9n9jOsSnYQoemIVL?utm_source=claudecoderouter&amp;utm_medium=partner&amp;utm_campaign=claudecoderouter_2026&amp;utm_content=default">
<img src="/docs/public/provider-icons/code0.png" width="42" height="42" alt="code0.ai 图标" />
<br />
<strong>code0.ai</strong>
</a>
</td>
<td align="center" width="330">
<a href="https://console.claudeapi.com/agent/register/LbmB7Y9kPloyzhwF?utm_source=claudecoderouter&amp;utm_medium=partner&amp;utm_campaign=claudecoderouter_2026&amp;utm_content=default">
<img src="/docs/public/provider-icons/claudeapi.png" width="42" height="42" alt="claudeapi 图标" />
<br />
<strong>claudeapi</strong>
</a>
</td>
</tr>
<tr>
<td align="center" width="330">
<a href="https://s.qiniu.com/AVjMVf">
<img src="/docs/public/provider-icons/qiniu-ai.png" width="42" height="42" alt="七牛云 AI 图标" />
<br />
<strong>七牛云 AI</strong>
</a>
</td>
<td align="center" width="330">
<a href="https://api.fenno.ai/register?redirect=/purchase?tab=subscription%26group=16&amp;aff=9HHHAB5QLAES">
<img src="/docs/public/provider-icons/fenno.jpg" width="42" height="42" alt="Fenno.ai 图标" />
<br />
<strong>Fenno.ai</strong>
</a>
</td>
<td align="center" width="330">
<a href="https://unity2.ai/register?source=claudecoderouter">
<img src="/docs/public/provider-icons/unity2.jpg" width="42" height="42" alt="Unity2.Ai 图标" />
<br />
<strong>Unity2.Ai</strong>
</a>
</td>
</tr>
</table>
<h4>社区赞助者</h4>
<table width="100%">
<tr>
<td align="center" width="220">@Simon Leischnig</td>
<td align="center" width="220"><a href="https://github.com/duanshuaimin">@duanshuaimin</a></td>
<td align="center" width="220"><a href="https://github.com/vrgitadmin">@vrgitadmin</a></td>
<td align="center" width="220">@*o</td>
<td align="center" width="220"><a href="https://github.com/ceilwoo">@ceilwoo</a></td>
<td align="center" width="220">@*说</td>
</tr>
<tr>
<td align="center" width="220">@*更</td>
<td align="center" width="220">@K*g</td>
<td align="center" width="220">@R*R</td>
<td align="center" width="220"><a href="https://github.com/bobleer">@bobleer</a></td>
<td align="center" width="220">@*苗</td>
<td align="center" width="220">@*划</td>
</tr>
<tr>
<td align="center" width="220"><a href="https://github.com/Clarence-pan">@Clarence-pan</a></td>
<td align="center" width="220"><a href="https://github.com/carter003">@carter003</a></td>
<td align="center" width="220">@S*r</td>
<td align="center" width="220">@*晖</td>
<td align="center" width="220">@*敏</td>
<td align="center" width="220">@Z*z</td>
</tr>
<tr>
<td align="center" width="220">@*然</td>
<td align="center" width="220"><a href="https://github.com/cluic">@cluic</a></td>
<td align="center" width="220">@*苗</td>
<td align="center" width="220"><a href="https://github.com/PromptExpert">@PromptExpert</a></td>
<td align="center" width="220">@*应</td>
<td align="center" width="220"><a href="https://github.com/yusnake">@yusnake</a></td>
</tr>
<tr>
<td align="center" width="220">@*飞</td>
<td align="center" width="220">@董*</td>
<td align="center" width="220">@*汀</td>
<td align="center" width="220">@*涯</td>
<td align="center" width="220">@*:-</td>
<td align="center" width="220">@**磊</td>
</tr>
<tr>
<td align="center" width="220">@*琢</td>
<td align="center" width="220">@*成</td>
<td align="center" width="220">@Z*o</td>
<td align="center" width="220">@*琨</td>
<td align="center" width="220"><a href="https://github.com/congzhangzh">@congzhangzh</a></td>
<td align="center" width="220">@*_</td>
</tr>
<tr>
<td align="center" width="220">@Z*m</td>
<td align="center" width="220">@*鑫</td>
<td align="center" width="220">@c*y</td>
<td align="center" width="220">@*昕</td>
<td align="center" width="220"><a href="https://github.com/witsice">@witsice</a></td>
<td align="center" width="220">@b*g</td>
</tr>
<tr>
<td align="center" width="220">@*亿</td>
<td align="center" width="220">@*辉</td>
<td align="center" width="220">@JACK</td>
<td align="center" width="220">@*光</td>
<td align="center" width="220">@W*l</td>
<td align="center" width="220"><a href="https://github.com/kesku">@kesku</a></td>
</tr>
<tr>
<td align="center" width="220"><a href="https://github.com/biguncle">@biguncle</a></td>
<td align="center" width="220">@二吉吉</td>
<td align="center" width="220">@a*g</td>
<td align="center" width="220">@*林</td>
<td align="center" width="220">@*咸</td>
<td align="center" width="220">@*明</td>
</tr>
<tr>
<td align="center" width="220">@S*y</td>
<td align="center" width="220">@f*o</td>
<td align="center" width="220">@*智</td>
<td align="center" width="220">@F*t</td>
<td align="center" width="220">@r*c</td>
<td align="center" width="220"><a href="https://github.com/qierkang">@qierkang</a></td>
</tr>
<tr>
<td align="center" width="220">@*军</td>
<td align="center" width="220"><a href="https://github.com/snrise-z">@snrise-z</a></td>
<td align="center" width="220">@*王</td>
<td align="center" width="220"><a href="https://github.com/greatheart1000">@greatheart1000</a></td>
<td align="center" width="220">@*王</td>
<td align="center" width="220">@zcutlip</td>
</tr>
<tr>
<td align="center" width="220"><a href="https://github.com/Peng-YM">@Peng-YM</a></td>
<td align="center" width="220">@*更</td>
<td align="center" width="220">@*.</td>
<td align="center" width="220">@F*t</td>
<td align="center" width="220">@*政</td>
<td align="center" width="220">@*铭</td>
</tr>
<tr>
<td align="center" width="220">@*叶</td>
<td align="center" width="220">@七*o</td>
<td align="center" width="220">@*青</td>
<td align="center" width="220">@**晨</td>
<td align="center" width="220">@*远</td>
<td align="center" width="220">@*霄</td>
</tr>
<tr>
<td align="center" width="220">@**吉</td>
<td align="center" width="220">@**飞</td>
<td align="center" width="220">@**驰</td>
<td align="center" width="220">@x*g</td>
<td align="center" width="220">@**东</td>
<td align="center" width="220">@*落</td>
</tr>
<tr>
<td align="center" width="220">@哆*k</td>
<td align="center" width="220">@*涛</td>
<td align="center" width="220"><a href="https://github.com/WitMiao">@苗大</a></td>
<td align="center" width="220">@*呢</td>
<td align="center" width="220">@d*u</td>
<td align="center" width="220">@crizcraig</td>
</tr>
<tr>
<td align="center" width="220">s*s</td>
<td align="center" width="220">*火</td>
<td align="center" width="220">*勤</td>
<td align="center" width="220">**锟</td>
<td align="center" width="220">*涛</td>
<td align="center" width="220">**明</td>
</tr>
<tr>
<td align="center" width="220">*知</td>
<td align="center" width="220">*语</td>
<td align="center" width="220">*瓜</td>
<td align="center" width="220"></td>
<td align="center" width="220"></td>
<td align="center" width="220"></td>
</tr>
</table>
<sub>如果你的名字被打码,请通过我的主页邮箱联系我更新为 GitHub 用户名。</sub>
</div>
## 许可证
本项目基于 [MIT License](LICENSE) 发布。
(如果你的名字被打码,请通过我的主页邮箱联系我更新为 GitHub 用户名。)

View file

Before

Width:  |  Height:  |  Size: 992 KiB

After

Width:  |  Height:  |  Size: 992 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 88 KiB

After

Width:  |  Height:  |  Size: 88 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 94 KiB

After

Width:  |  Height:  |  Size: 94 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 102 KiB

After

Width:  |  Height:  |  Size: 102 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 88 KiB

After

Width:  |  Height:  |  Size: 88 KiB

Before After
Before After

View file

@ -1,4 +1,4 @@
import { buildBrowserRenderer, buildMain, buildRenderer, buildStyles, buildTrayRenderer, buildWebClientBridge, cleanDist, copyAppAssets, copyBrowserRendererHtml, copyMarketplacePlugins, copyModelCatalog, copyRendererHtml, copyTrayRendererHtml, syncUiRendererToRuntimeDists } from "./esbuild.config.mjs";
import { buildBrowserRenderer, buildMain, buildRenderer, buildStyles, buildTrayRenderer, cleanDist, copyAppAssets, copyBrowserRendererHtml, copyMarketplacePlugins, copyModelCatalog, copyRendererHtml, copyTrayRendererHtml } from "./esbuild.config.mjs";
const mode = process.argv.includes("--dev") ? "development" : "production";
@ -15,10 +15,7 @@ await Promise.all([
buildBrowserRenderer({ mode }),
buildRenderer({ mode }),
buildTrayRenderer({ mode }),
buildWebClientBridge({ mode }),
buildStyles({ minify: mode === "production" })
]);
syncUiRendererToRuntimeDists();
console.log(`Built monorepo package assets in ${mode} mode.`);
console.log(`Built Electron app assets in ${mode} mode.`);

View file

@ -2,85 +2,46 @@ import electron from "electron";
import esbuild from "esbuild";
import { createHash } from "node:crypto";
import { spawn } from "node:child_process";
import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
import { existsSync, readdirSync, readFileSync, statSync, watch } from "node:fs";
import path from "node:path";
import {
binPath,
buildStyles,
cleanDist,
browserRendererHtmlInput,
cliSourceRoot,
coreSourceRoot,
copyAppAssets,
copyBrowserRendererHtml,
copyCliRuntimeToElectronDist,
copyMarketplacePlugins,
copyModelCatalog,
copyRendererHtml,
copyTrayRendererHtml,
createBrowserRendererBuildOptions,
createCliBuildOptions,
createMainBuildOptions,
createRendererBuildOptions,
createTrayRendererBuildOptions,
createWebClientBridgeBuildOptions,
cssInput,
cssOutput,
appAssetsInput,
modelCatalogInput,
projectRoot,
rendererRoot,
rendererHtmlInput,
syncUiRendererToRuntimeDists,
trayRendererHtmlInput,
watchPlugin
} from "./esbuild.config.mjs";
let electronProcess = null;
let restartTimer = null;
let restartInFlight = false;
let restartQueued = false;
let pendingRestartReasons = [];
const watchSignatures = new Map();
let shuttingDown = false;
const restartDelayMs = 160;
const styleBuildDelayMs = 160;
const stylePollIntervalMs = 1000;
const ignoredSignatureEntries = new Set([".DS_Store"]);
let styleBuildTimer = null;
let styleBuildInFlight = false;
let queuedStyleBuildReason = null;
const ready = {
browser: false,
cli: false,
main: false,
renderer: false,
tray: false,
webBridge: false
tray: false
};
const devTarget = parseDevTarget(process.argv.slice(2));
const enabled = {
cli: devTarget === "cli" || devTarget === "electron",
electron: devTarget === "electron",
ui: true
};
const coreSharedSourceRoot = path.join(coreSourceRoot, "shared");
const styleWatchRoots = [rendererRoot, coreSharedSourceRoot].filter((watchRoot) => existsSync(watchRoot));
const activeReadyNames = new Set([
...(enabled.ui ? ["browser", "renderer", "tray", "webBridge"] : []),
...(enabled.cli ? ["cli"] : []),
...(enabled.electron ? ["main"] : [])
]);
function parseDevTarget(args) {
const target = args[0] ?? "electron";
if (target === "--help" || target === "-h") {
console.log("Usage: node build/dev.mjs [ui|cli|electron]");
process.exit(0);
}
if (target === "ui" || target === "cli" || target === "electron") {
return target;
}
console.error(`Unknown dev target "${target}". Expected ui, cli, or electron.`);
process.exit(2);
}
function logDev(message) {
console.log(`[dev] ${new Date().toISOString()} ${message}`);
@ -92,11 +53,17 @@ function relativePath(file) {
function readyState() {
return Object.entries(ready)
.filter(([name]) => activeReadyNames.has(name))
.map(([name, value]) => `${name}:${value ? "ready" : "pending"}`)
.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);
@ -172,132 +139,35 @@ function listDirectoryFiles(targetPath, basePath = targetPath) {
return files;
}
function rememberWatchSignature(label, targetPath, options = {}) {
const signature = options.metadataOnly
? metadataSignature(targetPath)
: contentSignature(targetPath);
function rememberWatchSignature(label, targetPath) {
const signature = contentSignature(targetPath);
watchSignatures.set(label, signature.key);
logDev(`watch baseline: ${label} ${relativePath(targetPath)}; ${signature.summary}`);
}
function scheduleStyleBuild(reason) {
queuedStyleBuildReason = reason;
if (styleBuildTimer) {
clearTimeout(styleBuildTimer);
}
styleBuildTimer = setTimeout(() => {
styleBuildTimer = null;
void rebuildStyles(queuedStyleBuildReason ?? reason);
}, styleBuildDelayMs);
}
async function rebuildStyles(reason) {
if (styleBuildInFlight) {
queuedStyleBuildReason = reason;
return;
}
styleBuildInFlight = true;
queuedStyleBuildReason = null;
try {
logDev(`rebuilding styles: ${reason}`);
await buildStyles({ minify: false });
syncUiRendererToRuntimeDists();
if (enabled.electron) {
scheduleRestart(`styles rebuilt: ${reason}`);
}
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
logDev(`style rebuild failed: ${message}`);
} finally {
styleBuildInFlight = false;
if (queuedStyleBuildReason) {
const queuedReason = queuedStyleBuildReason;
queuedStyleBuildReason = null;
scheduleStyleBuild(queuedReason);
}
}
}
function pollStyleWatchRoots() {
for (const styleWatchRoot of styleWatchRoots) {
const label = `styles ${relativePath(styleWatchRoot)}`;
const signature = contentSignature(styleWatchRoot);
const previousSignature = watchSignatures.get(label);
if (previousSignature === signature.key) {
continue;
}
watchSignatures.set(label, signature.key);
logDev(`watch event: ${label}; ${signature.summary}; content=changed`);
scheduleStyleBuild(label);
}
}
function pollWatchedInput(label, targetPath, onChange, options = {}) {
const signature = options.metadataOnly
? metadataSignature(targetPath)
: contentSignature(targetPath);
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);
if (previousSignature === signature.key) {
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;
}
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 });
}
onChange();
scheduleRestart(reason);
}
function markReady(name, reason = `${name} esbuild completed`) {
if (name === "browser" || name === "cli" || name === "main" || name === "renderer" || name === "tray" || name === "webBridge") {
if (name === "browser" || name === "main" || name === "renderer" || name === "tray") {
ready[name] = true;
}
logDev(`build ready: ${reason}; ${readyState()}`);
if (enabled.electron && Array.from(activeReadyNames).every((readyName) => ready[readyName])) {
if (ready.browser && ready.main && ready.renderer && ready.tray) {
scheduleRestart(reason);
}
}
@ -317,200 +187,130 @@ function scheduleRestart(reason = "unknown trigger") {
restartTimer = setTimeout(restartElectron, restartDelayMs);
}
async function restartElectron() {
if (restartInFlight) {
restartQueued = true;
return;
}
restartInFlight = true;
function restartElectron() {
const reasons = Array.from(new Set(pendingRestartReasons));
pendingRestartReasons = [];
restartTimer = null;
try {
if (electronProcess) {
await stopElectron(electronProcess);
}
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");
}
if (electronProcess) {
logDev(`stopping Electron pid=${electronProcess.pid ?? "unknown"}`);
electronProcess.kill();
electronProcess = null;
}
}
async function stopElectron(child) {
logDev(`stopping Electron pid=${child.pid ?? "unknown"}`);
if (child.exitCode !== null || child.signalCode !== null) {
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;
}
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"}`);
logDev("starting dev build");
cleanDist();
if (enabled.electron) {
copyAppAssets();
}
if (enabled.cli || enabled.electron) {
copyMarketplacePlugins();
copyModelCatalog();
}
copyAppAssets();
copyMarketplacePlugins();
copyModelCatalog();
copyBrowserRendererHtml();
copyRendererHtml();
copyTrayRendererHtml();
await buildStyles({ minify: false });
syncUiRendererToRuntimeDists();
const tailwindProcess = spawn(binPath("tailwindcss"), ["-i", cssInput, "-o", cssOutput, "--watch"], {
cwd: projectRoot,
stdio: "inherit",
shell: process.platform === "win32"
});
logDev(`Tailwind watcher started pid=${tailwindProcess.pid ?? "unknown"} input=${relativePath(cssInput)} output=${relativePath(cssOutput)}`);
tailwindProcess.on("exit", (code, signal) => {
logDev(`Tailwind watcher exited code=${code ?? "null"} signal=${signal ?? "null"}`);
});
rememberWatchSignature("home html", rendererHtmlInput);
rememberWatchSignature("browser html", browserRendererHtmlInput);
rememberWatchSignature("tray html", trayRendererHtmlInput);
for (const styleWatchRoot of styleWatchRoots) {
rememberWatchSignature(`styles ${relativePath(styleWatchRoot)}`, styleWatchRoot);
}
if (enabled.electron) {
rememberWatchSignature("app assets", appAssetsInput);
}
if ((enabled.cli || enabled.electron) && existsSync(modelCatalogInput)) {
rememberWatchSignature("model catalog", modelCatalogInput, { metadataOnly: true });
rememberWatchSignature("app assets", appAssetsInput);
if (existsSync(modelCatalogInput)) {
rememberWatchSignature("model catalog", modelCatalogInput);
}
const sourcePoller = setInterval(() => {
pollStyleWatchRoots();
pollSourceWatchTargets();
}, stylePollIntervalMs);
const htmlWatcher = watch(rendererHtmlInput, { persistent: true }, (eventType, filename) => {
handleWatchedInput("home html", rendererHtmlInput, eventType, filename, undefined, copyRendererHtml);
});
const contexts = [];
const browserHtmlWatcher = watch(browserRendererHtmlInput, { persistent: true }, (eventType, filename) => {
handleWatchedInput("browser html", browserRendererHtmlInput, eventType, filename, undefined, copyBrowserRendererHtml);
});
if (enabled.electron) {
contexts.push(
await esbuild.context(
createMainBuildOptions({
mode: "development",
plugins: [watchPlugin("main", (name) => markReady(name))]
})
)
);
}
const trayHtmlWatcher = watch(trayRendererHtmlInput, { persistent: true }, (eventType, filename) => {
handleWatchedInput("tray html", trayRendererHtmlInput, eventType, filename, undefined, copyTrayRendererHtml);
});
if (enabled.cli) {
contexts.push(
await esbuild.context(
createCliBuildOptions({
mode: "development",
plugins: [
watchPlugin("cli", (name) => {
if (enabled.electron) {
copyCliRuntimeToElectronDist();
}
markReady(name);
})
]
})
)
);
}
const appAssetsWatcher = watch(appAssetsInput, { persistent: true }, (eventType, filename) => {
handleWatchedInput("app assets", appAssetsInput, eventType, filename, { isDirectory: true }, copyAppAssets);
});
if (enabled.ui) {
contexts.push(
await esbuild.context(
createRendererBuildOptions({
mode: "development",
plugins: [
watchPlugin("renderer", (name) => {
copyRendererHtml();
syncUiRendererToRuntimeDists();
markReady(name);
})
]
})
),
await esbuild.context(
createTrayRendererBuildOptions({
mode: "development",
plugins: [
watchPlugin("tray", (name) => {
copyTrayRendererHtml();
syncUiRendererToRuntimeDists();
markReady(name);
})
]
})
),
await esbuild.context(
createBrowserRendererBuildOptions({
mode: "development",
plugins: [
watchPlugin("browser", (name) => {
copyBrowserRendererHtml();
syncUiRendererToRuntimeDists();
markReady(name);
})
]
})
),
await esbuild.context(
createWebClientBridgeBuildOptions({
mode: "development",
plugins: [
watchPlugin("webBridge", (name) => {
syncUiRendererToRuntimeDists();
markReady(name);
})
]
})
)
);
}
const modelCatalogWatcher = existsSync(modelCatalogInput)
? watch(modelCatalogInput, { persistent: true }, (eventType, filename) => {
handleWatchedInput("model catalog", modelCatalogInput, eventType, filename, undefined, copyModelCatalog);
})
: { close: () => undefined };
await Promise.all(contexts.map((context) => context.watch()));
const mainContext = await esbuild.context(
createMainBuildOptions({
mode: "development",
plugins: [watchPlugin("main", (name) => markReady(name))]
})
);
const rendererContext = await esbuild.context(
createRendererBuildOptions({
mode: "development",
plugins: [
watchPlugin("renderer", (name) => {
copyRendererHtml();
markReady(name);
})
]
})
);
const trayRendererContext = await esbuild.context(
createTrayRendererBuildOptions({
mode: "development",
plugins: [
watchPlugin("tray", (name) => {
copyTrayRendererHtml();
markReady(name);
})
]
})
);
const browserRendererContext = await esbuild.context(
createBrowserRendererBuildOptions({
mode: "development",
plugins: [
watchPlugin("browser", (name) => {
copyBrowserRendererHtml();
markReady(name);
})
]
})
);
await Promise.all([mainContext.watch(), rendererContext.watch(), trayRendererContext.watch(), browserRendererContext.watch()]);
logDev("watchers are active");
async function shutdown() {
@ -520,13 +320,15 @@ async function shutdown() {
clearTimeout(restartTimer);
}
if (electronProcess) {
await stopElectron(electronProcess);
electronProcess.kill();
}
if (styleBuildTimer) {
clearTimeout(styleBuildTimer);
}
clearInterval(sourcePoller);
await Promise.all(contexts.map((context) => context.dispose()));
tailwindProcess.kill();
htmlWatcher.close();
browserHtmlWatcher.close();
trayHtmlWatcher.close();
appAssetsWatcher.close();
modelCatalogWatcher.close();
await Promise.all([mainContext.dispose(), rendererContext.dispose(), trayRendererContext.dispose(), browserRendererContext.dispose()]);
process.exit(0);
}

View file

@ -1,37 +0,0 @@
import {
buildBrowserRenderer,
buildCoreServer,
buildRenderer,
buildStyles,
buildTrayRenderer,
buildWebClientBridge,
cleanDist,
copyBrowserRendererHtml,
copyMarketplacePlugins,
copyModelCatalog,
copyRendererHtml,
copyTrayRendererHtml,
syncUiRendererToRuntimeDists
} from "./esbuild.config.mjs";
const mode = process.argv.includes("--dev") ? "development" : "production";
cleanDist();
copyMarketplacePlugins();
copyModelCatalog();
copyBrowserRendererHtml();
copyRendererHtml();
copyTrayRendererHtml();
await Promise.all([
buildCoreServer({ mode }),
buildBrowserRenderer({ mode }),
buildRenderer({ mode }),
buildTrayRenderer({ mode }),
buildWebClientBridge({ mode }),
buildStyles({ minify: mode === "production" })
]);
syncUiRendererToRuntimeDists();
console.log(`Built Docker core server and UI assets in ${mode} mode.`);

View file

@ -1,63 +1,23 @@
import esbuild from "esbuild";
import { spawn } from "node:child_process";
import { chmodSync, cpSync, existsSync, mkdirSync, readFileSync, rmSync, statSync, writeFileSync } from "node:fs";
import { builtinModules, createRequire } from "node:module";
import { cpSync, existsSync, mkdirSync, readFileSync, rmSync, statSync, writeFileSync } from "node:fs";
import { builtinModules } from "node:module";
import path from "node:path";
import { fileURLToPath } from "node:url";
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const requireFromHere = createRequire(import.meta.url);
export const projectRoot = path.resolve(__dirname, "..");
export const packagesRoot = path.join(projectRoot, "packages");
export const cliRoot = path.join(packagesRoot, "cli");
export const coreRoot = path.join(packagesRoot, "core");
export const electronRoot = path.join(packagesRoot, "electron");
export const uiRoot = path.join(packagesRoot, "ui");
export const cliSourceRoot = path.join(cliRoot, "src");
export const coreSourceRoot = path.join(coreRoot, "src");
export const electronSourceRoot = path.join(electronRoot, "src");
export const uiSourceRoot = path.join(uiRoot, "src");
export const legacyDistDir = path.join(projectRoot, "dist");
export const cliDistDir = path.join(cliRoot, "dist");
export const coreDistDir = path.join(coreRoot, "dist");
export const electronDistDir = path.join(electronRoot, "dist");
export const uiDistDir = path.join(uiRoot, "dist");
export const distDir = electronDistDir;
export const cliMainOutDir = path.join(cliDistDir, "main");
export const coreMainOutDir = path.join(coreDistDir, "main");
export const electronMainOutDir = path.join(electronDistDir, "main");
export const mainOutDir = electronMainOutDir;
export const gatewayPackageRoot = path.dirname(requireFromHere.resolve("@the-next-ai/ai-gateway/package.json"));
export const gatewayRuntimeInput = path.join(gatewayPackageRoot, "bin", "next-ai-gateway.js");
export const electronGatewayRuntimeOutput = path.join(electronMainOutDir, "next-ai-gateway.js");
export const botGatewaySdkPackageRoot = path.dirname(requireFromHere.resolve("@the-next-ai/bot-gateway-sdk/package.json"));
export const botGatewaySdkEntryInput = path.join(botGatewaySdkPackageRoot, "dist", "index.js");
export const botGatewaySdkRunnerInput = path.join(botGatewaySdkPackageRoot, "bin", "bot-gateway-stdio.mjs");
export const electronBotGatewaySdkRootDir = path.join(electronMainOutDir, "bot-gateway-sdk");
export const electronBotGatewaySdkDistDir = path.join(electronBotGatewaySdkRootDir, "dist");
export const electronBotGatewaySdkBinDir = path.join(electronBotGatewaySdkRootDir, "bin");
export const electronBotGatewaySdkPackageOutput = path.join(electronBotGatewaySdkRootDir, "package.json");
export const electronBotGatewaySdkEntryOutput = path.join(electronBotGatewaySdkDistDir, "index.js");
export const electronBotGatewaySdkRunnerOutput = path.join(electronBotGatewaySdkBinDir, "bot-gateway-stdio.mjs");
export const rendererOutDir = path.join(uiDistDir, "renderer");
export const cliRendererOutDir = path.join(cliDistDir, "renderer");
export const coreRendererOutDir = path.join(coreDistDir, "renderer");
export const electronRendererOutDir = path.join(electronDistDir, "renderer");
export const runtimeRendererOutDirs = [cliRendererOutDir, coreRendererOutDir, electronRendererOutDir];
export const appAssetsDir = path.join(electronDistDir, "assets");
export const distDir = path.join(projectRoot, "dist");
export const mainOutDir = path.join(distDir, "main");
export const rendererOutDir = path.join(distDir, "renderer");
export const appAssetsDir = path.join(distDir, "assets");
export const rendererAssetsDir = path.join(rendererOutDir, "assets");
export const cliMarketplacePluginsDir = path.join(cliDistDir, "marketplace", "plugins");
export const coreMarketplacePluginsDir = path.join(coreDistDir, "marketplace", "plugins");
export const electronMarketplacePluginsDir = path.join(electronDistDir, "marketplace", "plugins");
export const marketplacePluginsDir = electronMarketplacePluginsDir;
export const appAssetsInput = path.join(electronRoot, "assets");
export const modelCatalogInput = path.join(coreRoot, "models.json");
export const cliModelCatalogOutput = path.join(cliDistDir, "models.json");
export const coreModelCatalogOutput = path.join(coreDistDir, "models.json");
export const electronModelCatalogOutput = path.join(electronDistDir, "models.json");
export const modelCatalogOutput = electronModelCatalogOutput;
export const rendererRoot = uiSourceRoot;
export const marketplacePluginsDir = path.join(distDir, "marketplace", "plugins");
export const appAssetsInput = path.join(projectRoot, "assets");
export const modelCatalogInput = path.join(projectRoot, "models.json");
export const modelCatalogOutput = path.join(distDir, "models.json");
export const rendererRoot = path.join(projectRoot, "src", "renderer");
export const rendererHtmlInput = path.join(rendererRoot, "pages", "home", "index.html");
export const rendererHtmlOutput = path.join(rendererOutDir, "pages", "home", "index.html");
export const browserRendererHtmlInput = path.join(rendererRoot, "pages", "browser", "index.html");
@ -66,19 +26,6 @@ export const trayRendererHtmlInput = path.join(rendererRoot, "pages", "tray", "i
export const trayRendererHtmlOutput = path.join(rendererOutDir, "pages", "tray", "index.html");
export const cssInput = path.join(rendererRoot, "styles", "globals.css");
export const cssOutput = path.join(rendererAssetsDir, "main.css");
export const webClientBridgeOutput = path.join(rendererAssetsDir, "web-client-bridge.js");
export const electronUndiciProxyAgentInput = path.join(coreSourceRoot, "proxy", "undici-proxy-agent.ts");
const lightweightMcpBundleNames = ["browser-web-search-proxy-mcp.js", "fusion-vision-mcp.js", "fusion-tool-fallback-mcp.js"];
const lightweightMcpBundleMaxBytes = 128 * 1024;
const forbiddenLightweightMcpInputs = [
{ prefix: "packages/core/src/config/", reason: "config modules can pull in native storage side effects" },
{ prefix: "packages/core/src/storage/", reason: "native SQLite storage is not allowed in lightweight MCP subprocesses" },
{ prefix: "packages/electron/src/", reason: "Electron runtime modules are not allowed in lightweight MCP subprocesses" },
{ prefix: "packages/ui/src/", reason: "UI modules do not belong in stdio MCP subprocesses" },
{ prefix: "node_modules/better-sqlite3/", reason: "native SQLite is not allowed in lightweight MCP subprocesses" },
{ prefix: "node_modules/electron/", reason: "Electron runtime modules are not allowed in lightweight MCP subprocesses" }
];
const forbiddenLightweightMcpExternalImports = new Set(["better-sqlite3", "electron"]);
const nodeExternals = [
"electron",
@ -88,28 +35,15 @@ const nodeExternals = [
];
export function cleanDist() {
rmSync(legacyDistDir, { force: true, recursive: true });
rmSync(cliDistDir, { force: true, recursive: true });
rmSync(coreDistDir, { force: true, recursive: true });
rmSync(electronDistDir, { force: true, recursive: true });
rmSync(uiDistDir, { force: true, recursive: true });
rmSync(distDir, { force: true, recursive: true });
ensureDist();
}
export function ensureDist() {
mkdirSync(cliMainOutDir, { recursive: true });
mkdirSync(coreMainOutDir, { recursive: true });
mkdirSync(electronMainOutDir, { recursive: true });
mkdirSync(electronBotGatewaySdkDistDir, { recursive: true });
mkdirSync(electronBotGatewaySdkBinDir, { recursive: true });
mkdirSync(mainOutDir, { recursive: true });
mkdirSync(appAssetsDir, { recursive: true });
mkdirSync(cliMarketplacePluginsDir, { recursive: true });
mkdirSync(coreMarketplacePluginsDir, { recursive: true });
mkdirSync(electronMarketplacePluginsDir, { recursive: true });
mkdirSync(marketplacePluginsDir, { recursive: true });
mkdirSync(rendererAssetsDir, { recursive: true });
for (const outputDir of runtimeRendererOutDirs) {
mkdirSync(path.join(outputDir, "assets"), { recursive: true });
}
mkdirSync(path.dirname(rendererHtmlOutput), { recursive: true });
mkdirSync(path.dirname(browserRendererHtmlOutput), { recursive: true });
mkdirSync(path.dirname(trayRendererHtmlOutput), { recursive: true });
@ -125,16 +59,12 @@ export function copyAppAssets() {
export function copyModelCatalog() {
ensureDist();
if (existsSync(modelCatalogInput)) {
cpSync(modelCatalogInput, cliModelCatalogOutput);
cpSync(modelCatalogInput, coreModelCatalogOutput);
cpSync(modelCatalogInput, electronModelCatalogOutput);
cpSync(modelCatalogInput, modelCatalogOutput);
}
}
export function copyRendererHtml() {
copyRendererPageHtml(rendererHtmlInput, rendererHtmlOutput, "main.js", {
beforeModuleScriptTags: [' <script src="../../assets/web-client-bridge.js"></script>']
});
copyRendererPageHtml(rendererHtmlInput, rendererHtmlOutput, "main.js");
}
export function copyTrayRendererHtml() {
@ -149,25 +79,14 @@ export function copyMarketplacePlugins() {
ensureDist();
for (const filename of ["claude-design-plugin.cjs", "cursor-proxy-plugin.cjs"]) {
const source = path.join(projectRoot, "examples", "plugins", filename);
const target = path.join(marketplacePluginsDir, filename);
if (existsSync(source)) {
cpSync(source, path.join(cliMarketplacePluginsDir, filename));
cpSync(source, path.join(coreMarketplacePluginsDir, filename));
cpSync(source, path.join(electronMarketplacePluginsDir, filename));
cpSync(source, target);
}
}
}
export function syncUiRendererToRuntimeDists() {
ensureDist();
for (const outputDir of runtimeRendererOutDirs) {
rmSync(outputDir, { force: true, recursive: true });
if (existsSync(rendererOutDir)) {
cpSync(rendererOutDir, outputDir, { recursive: true });
}
}
}
function copyRendererPageHtml(input, output, scriptName, options = {}) {
function copyRendererPageHtml(input, output, scriptName) {
ensureDist();
const source = readFileSync(input, "utf8");
const styleTag = ' <link rel="stylesheet" href="../../assets/main.css" />';
@ -176,12 +95,6 @@ function copyRendererPageHtml(input, output, scriptName, options = {}) {
? source.replace(' <script type="module" src="./main.tsx"></script>', scriptTag)
: source.replace("</body>", `${scriptTag}\n </body>`);
for (const extraScriptTag of options.beforeModuleScriptTags ?? []) {
if (!hasScriptTag(html, extraScriptTag)) {
html = html.replace(scriptTag, `${extraScriptTag}\n${scriptTag}`);
}
}
if (!html.includes('href="../../assets/main.css"')) {
html = html.replace("</head>", `${styleTag}\n </head>`);
}
@ -189,96 +102,26 @@ function copyRendererPageHtml(input, output, scriptName, options = {}) {
writeFileSync(output, html, "utf8");
}
function hasScriptTag(html, scriptTag) {
const sourceMatch = scriptTag.match(/\bsrc="([^"]+)"/);
return sourceMatch ? html.includes(sourceMatch[1]) : html.includes(scriptTag);
}
function normalizeDuplicateShebangs(source) {
const lines = source.split("\n");
if (!lines[0]?.startsWith("#!")) {
return source;
}
let index = 1;
while (lines[index]?.startsWith("#!")) {
index += 1;
}
return [lines[0], ...lines.slice(index)].join("\n");
}
export function createMainBuildOptions({ mode = "production", plugins = [] } = {}) {
return {
absWorkingDir: projectRoot,
bundle: true,
entryNames: "[name]",
entryPoints: [
path.join(electronSourceRoot, "main", "main.ts"),
path.join(electronSourceRoot, "main", "browser-preload.ts"),
gatewayRuntimeInput,
path.join(coreSourceRoot, "mcp", "browser-web-search-proxy-mcp.ts"),
path.join(coreSourceRoot, "mcp", "fusion-vision-mcp.ts"),
path.join(coreSourceRoot, "mcp", "fusion-tool-fallback-mcp.ts"),
path.join(coreSourceRoot, "mcp", "toolhub-mcp.ts"),
electronUndiciProxyAgentInput,
path.join(electronSourceRoot, "main", "preload.ts")
path.join(projectRoot, "src", "main", "main.ts"),
path.join(projectRoot, "src", "main", "browser-preload.ts"),
path.join(projectRoot, "src", "main", "cli.ts"),
path.join(projectRoot, "src", "server", "mcp", "fusion-vision-mcp.ts"),
path.join(projectRoot, "src", "main", "preload.ts")
],
external: nodeExternals,
format: "cjs",
legalComments: "none",
logLevel: "info",
metafile: true,
minify: mode === "production",
outdir: electronMainOutDir,
outdir: mainOutDir,
platform: "node",
plugins: [packageAliasPlugin(), ...plugins],
sourcemap: mode !== "production",
target: "node22"
};
}
export function createCliBuildOptions({ mode = "production", plugins = [] } = {}) {
return {
absWorkingDir: projectRoot,
bundle: true,
entryNames: "[name]",
entryPoints: [
path.join(cliSourceRoot, "cli.ts"),
path.join(coreSourceRoot, "mcp", "fusion-vision-mcp.ts"),
path.join(coreSourceRoot, "mcp", "fusion-tool-fallback-mcp.ts"),
path.join(coreSourceRoot, "mcp", "toolhub-mcp.ts")
],
external: nodeExternals.filter((moduleName) => moduleName !== "electron"),
format: "cjs",
legalComments: "none",
logLevel: "info",
minify: mode === "production",
outdir: cliMainOutDir,
platform: "node",
plugins: [forbidCliElectronPlugin(), packageAliasPlugin(), ...plugins],
sourcemap: mode !== "production",
target: "node22"
};
}
export function createCoreServerBuildOptions({ mode = "production", plugins = [] } = {}) {
return {
absWorkingDir: projectRoot,
bundle: true,
entryNames: "[name]",
entryPoints: [
path.join(coreSourceRoot, "entrypoints", "server.ts"),
path.join(coreSourceRoot, "mcp", "fusion-vision-mcp.ts"),
path.join(coreSourceRoot, "mcp", "fusion-tool-fallback-mcp.ts"),
path.join(coreSourceRoot, "mcp", "toolhub-mcp.ts")
],
external: nodeExternals.filter((moduleName) => moduleName !== "electron"),
format: "cjs",
legalComments: "none",
logLevel: "info",
minify: mode === "production",
outdir: coreMainOutDir,
platform: "node",
plugins: [forbidCliElectronPlugin(), packageAliasPlugin(), ...plugins],
plugins,
sourcemap: mode !== "production",
target: "node22"
};
@ -309,7 +152,7 @@ export function createRendererBuildOptions({ mode = "production", plugins = [] }
minify: mode === "production",
outfile: path.join(rendererAssetsDir, "main.js"),
platform: "browser",
plugins: [rendererAliasPlugin(), packageAliasPlugin(), ...plugins],
plugins: [rendererAliasPlugin(), ...plugins],
publicPath: "../../assets",
sourcemap: mode !== "production",
target: "chrome120"
@ -332,44 +175,6 @@ export function createBrowserRendererBuildOptions({ mode = "production", plugins
};
}
export function createWebClientBridgeBuildOptions({ mode = "production", plugins = [] } = {}) {
return {
absWorkingDir: projectRoot,
bundle: true,
entryPoints: [path.join(uiSourceRoot, "web-client-bridge.ts")],
format: "iife",
legalComments: "none",
logLevel: "info",
minify: mode === "production",
outfile: webClientBridgeOutput,
platform: "browser",
plugins: [packageAliasPlugin(), ...plugins],
sourcemap: mode !== "production",
target: "chrome120"
};
}
export function createBotGatewaySdkBuildOptions({ mode = "production", plugins = [] } = {}) {
return {
absWorkingDir: projectRoot,
bundle: true,
entryPoints: [botGatewaySdkEntryInput],
external: [
...builtinModules,
...builtinModules.map((moduleName) => `node:${moduleName}`)
],
format: "esm",
legalComments: "none",
logLevel: "info",
minify: mode === "production",
outfile: electronBotGatewaySdkEntryOutput,
platform: "node",
plugins,
sourcemap: mode !== "production",
target: "node22"
};
}
export function watchPlugin(name, onEnd) {
return {
name: `${name}-watch`,
@ -384,38 +189,7 @@ export function watchPlugin(name, onEnd) {
}
export async function buildMain(options = {}) {
const [mainBuildResult] = await Promise.all([
esbuild.build(createMainBuildOptions(options)),
buildBotGatewaySdkRuntime(options),
buildCoreServer(options),
buildCli(options)
]);
copyCliRuntimeToElectronDist();
validateLightweightMcpBundles(mainBuildResult.metafile);
}
export async function buildBotGatewaySdkRuntime(options = {}) {
ensureDist();
await esbuild.build(createBotGatewaySdkBuildOptions(options));
writeFileSync(
electronBotGatewaySdkPackageOutput,
`${JSON.stringify({ private: true, type: "module" }, null, 2)}\n`,
"utf8"
);
writeFileSync(
electronBotGatewaySdkRunnerOutput,
normalizeDuplicateShebangs(readFileSync(botGatewaySdkRunnerInput, "utf8")),
"utf8"
);
chmodSync(electronBotGatewaySdkRunnerOutput, 0o755);
}
export async function buildCli(options = {}) {
await esbuild.build(createCliBuildOptions(options));
}
export async function buildCoreServer(options = {}) {
await esbuild.build(createCoreServerBuildOptions(options));
await esbuild.build(createMainBuildOptions(options));
}
export async function buildRenderer(options = {}) {
@ -430,18 +204,6 @@ export async function buildBrowserRenderer(options = {}) {
await esbuild.build(createBrowserRendererBuildOptions(options));
}
export async function buildWebClientBridge(options = {}) {
await esbuild.build(createWebClientBridgeBuildOptions(options));
}
export function copyCliRuntimeToElectronDist() {
ensureDist();
const cliRuntime = path.join(cliMainOutDir, "cli.js");
if (existsSync(cliRuntime)) {
cpSync(cliRuntime, path.join(electronMainOutDir, "cli.js"));
}
}
export async function buildStyles({ minify = false } = {}) {
ensureDist();
const args = ["-i", cssInput, "-o", cssOutput];
@ -487,109 +249,20 @@ function rendererAliasPlugin() {
};
}
function packageAliasPlugin() {
return {
name: "ccr-package-alias",
setup(build) {
build.onResolve({ filter: /^@ccr\/cli\// }, (args) => {
return { path: resolvePackageImport(cliSourceRoot, args.path.slice("@ccr/cli/".length)) };
});
build.onResolve({ filter: /^@ccr\/core\// }, (args) => {
return { path: resolvePackageImport(coreSourceRoot, args.path.slice("@ccr/core/".length)) };
});
build.onResolve({ filter: /^@ccr\/electron\// }, (args) => {
return { path: resolvePackageImport(electronSourceRoot, args.path.slice("@ccr/electron/".length)) };
});
build.onResolve({ filter: /^@ccr\/ui\// }, (args) => {
return { path: resolvePackageImport(uiSourceRoot, args.path.slice("@ccr/ui/".length)) };
});
}
};
}
function forbidCliElectronPlugin() {
return {
name: "forbid-cli-electron",
setup(build) {
build.onResolve({ filter: /^electron$/ }, () => {
return {
errors: [
{
text: "CLI bundle must not import electron. Move the dependency behind a desktop-only boundary."
}
]
};
});
}
};
}
function validateLightweightMcpBundles(metafile) {
if (!metafile) {
return;
}
const outputsByName = new Map(
Object.entries(metafile.outputs).map(([outputPath, output]) => [path.basename(outputPath), { output, outputPath }])
);
for (const bundleName of lightweightMcpBundleNames) {
const entry = outputsByName.get(bundleName);
if (!entry) {
continue;
}
const violations = [];
if (entry.output.bytes > lightweightMcpBundleMaxBytes) {
violations.push(`bundle size ${entry.output.bytes} bytes exceeds ${lightweightMcpBundleMaxBytes} bytes`);
}
for (const inputPath of Object.keys(entry.output.inputs ?? {})) {
const normalizedInput = normalizeBuildPath(inputPath);
for (const rule of forbiddenLightweightMcpInputs) {
if (normalizedInput.startsWith(rule.prefix)) {
violations.push(`${normalizedInput} (${rule.reason})`);
}
}
}
for (const imported of entry.output.imports ?? []) {
if (imported.external && forbiddenLightweightMcpExternalImports.has(imported.path)) {
violations.push(`${imported.path} (external native/runtime dependency is not allowed)`);
}
}
if (violations.length > 0) {
throw new Error([
`Lightweight MCP bundle ${bundleName} crossed its dependency boundary.`,
...violations.map((violation) => `- ${violation}`)
].join("\n"));
}
}
}
function normalizeBuildPath(value) {
return value.split(path.sep).join("/");
}
function resolveRendererImport(importPath) {
return resolvePackageImport(rendererRoot, importPath);
}
function resolvePackageImport(rootDir, importPath) {
const packageBasePath = path.resolve(rootDir, importPath);
const basePath = path.resolve(rendererRoot, importPath);
const candidates = [
packageBasePath,
`${packageBasePath}.tsx`,
`${packageBasePath}.ts`,
`${packageBasePath}.jsx`,
`${packageBasePath}.js`,
`${packageBasePath}.json`,
`${packageBasePath}.css`,
path.join(packageBasePath, "index.tsx"),
path.join(packageBasePath, "index.ts"),
path.join(packageBasePath, "index.jsx"),
path.join(packageBasePath, "index.js")
basePath,
`${basePath}.tsx`,
`${basePath}.ts`,
`${basePath}.jsx`,
`${basePath}.js`,
`${basePath}.json`,
`${basePath}.css`,
path.join(basePath, "index.tsx"),
path.join(basePath, "index.ts"),
path.join(basePath, "index.jsx"),
path.join(basePath, "index.js")
];
for (const candidate of candidates) {
@ -598,5 +271,5 @@ function resolvePackageImport(rootDir, importPath) {
}
}
return packageBasePath;
return basePath;
}

View file

@ -1,78 +0,0 @@
import { readFileSync, writeFileSync } from "node:fs";
import path from "node:path";
import yaml from "js-yaml";
const [outputFile, ...inputFiles] = process.argv.slice(2);
if (!outputFile || inputFiles.length < 2) {
console.error("Usage: node build/merge-macos-update-metadata.mjs <output> <latest-mac.yml> <latest-mac.yml> [...]");
process.exit(1);
}
const updateInfos = inputFiles.map((file) => {
const info = yaml.load(readFileSync(file, "utf8"));
if (!info || typeof info !== "object") {
throw new Error(`${file} is not a valid update metadata object`);
}
if (!Array.isArray(info.files) || info.files.length === 0) {
throw new Error(`${file} does not contain any update files`);
}
return { file, info };
});
const version = updateInfos[0].info.version;
if (!version || updateInfos.some(({ info }) => info.version !== version)) {
throw new Error("All macOS update metadata files must have the same version");
}
const files = uniqueByUrl(
updateInfos
.flatMap(({ info }) => info.files)
.filter((file) => file && typeof file.url === "string")
.sort(compareMacUpdateFile)
);
const defaultZip = files.find((file) => file.url.endsWith(".zip") && file.url.includes("arm64")) ?? files.find((file) => file.url.endsWith(".zip"));
if (!defaultZip?.sha512) {
throw new Error("Merged macOS update metadata must include at least one ZIP with sha512");
}
const releaseDate = updateInfos
.map(({ info }) => info.releaseDate)
.filter(Boolean)
.sort()
.at(-1);
const { files: _files, path: _path, sha512: _sha512, releaseDate: _releaseDate, ...baseInfo } = updateInfos[0].info;
const mergedInfo = {
...baseInfo,
version,
files,
path: defaultZip.url,
sha512: defaultZip.sha512,
...(releaseDate ? { releaseDate } : {})
};
writeFileSync(outputFile, yaml.dump(mergedInfo, { lineWidth: 120, noRefs: true }), "utf8");
console.log(`Wrote ${path.relative(process.cwd(), outputFile)} with ${files.length} macOS artifacts.`);
function uniqueByUrl(files) {
const seen = new Set();
return files.filter((file) => {
if (seen.has(file.url)) {
return false;
}
seen.add(file.url);
return true;
});
}
function compareMacUpdateFile(left, right) {
return fileRank(left) - fileRank(right) || left.url.localeCompare(right.url);
}
function fileRank(file) {
const isZip = file.url.endsWith(".zip") ? 0 : 10;
const isAppleSilicon = file.url.includes("arm64") ? 0 : 1;
return isZip + isAppleSilicon;
}

View file

@ -1,67 +0,0 @@
import electron from "electron";
import { spawn } from "node:child_process";
import { mkdtempSync, rmSync } from "node:fs";
import os from "node:os";
import path from "node:path";
import { fileURLToPath } from "node:url";
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const projectRoot = path.resolve(__dirname, "..");
const testHome = mkdtempSync(path.join(os.tmpdir(), "ccr-test-home-"));
const testSuites = ["main", "renderer"];
const requestedSuites = process.argv.slice(2);
const suites = requestedSuites.length === 0 ? testSuites : requestedSuites;
for (const suite of suites) {
if (!testSuites.includes(suite)) {
cleanup();
throw new Error(`Unknown test suite: ${suite}`);
}
}
try {
for (const suite of suites) {
await runSuite(suite);
}
} finally {
cleanup();
}
function runSuite(suite) {
console.log(`\nRunning ${suite} tests...`);
return new Promise((resolve, reject) => {
const child = spawn(electron, ["--test", `dist/tests/${suite}/*.test.js`], {
cwd: projectRoot,
env: {
...process.env,
CCR_INTERNAL_APP_DATA_DIR: path.join(testHome, "app-data"),
CCR_INTERNAL_HOME_DIR: testHome,
CCR_INTERNAL_USER_DATA_DIR: path.join(testHome, "user-data"),
HOME: testHome,
ELECTRON_RUN_AS_NODE: "1"
},
shell: process.platform === "win32",
stdio: "inherit"
});
child.on("error", reject);
child.on("exit", (code, signal) => {
if (signal) {
process.kill(process.pid, signal);
return;
}
if (code === 0) {
resolve();
return;
}
reject(new Error(`${suite} tests exited with code ${code ?? 1}`));
});
});
}
function cleanup() {
rmSync(testHome, { force: true, recursive: true });
}

View file

@ -1,145 +0,0 @@
import esbuild from "esbuild";
import { existsSync, readdirSync, rmSync, statSync } from "node:fs";
import path from "node:path";
import { fileURLToPath } from "node:url";
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const projectRoot = path.resolve(__dirname, "..");
const testsOutDir = path.join(projectRoot, "dist", "tests");
const rendererRoot = path.join(projectRoot, "packages", "ui", "src");
const cliSourceRoot = path.join(projectRoot, "packages", "cli", "src");
const coreSourceRoot = path.join(projectRoot, "packages", "core", "src");
const testSuites = [
{ name: "main", testDir: path.join(projectRoot, "tests", "main") },
{ name: "renderer", testDir: path.join(projectRoot, "tests", "renderer") }
];
const requestedSuites = new Set(process.argv.slice(2));
const suiteNames = new Set(testSuites.map((suite) => suite.name));
const unknownSuites = [...requestedSuites].filter((suite) => !suiteNames.has(suite));
const selectedSuites = requestedSuites.size === 0
? testSuites
: testSuites.filter((suite) => requestedSuites.has(suite.name));
if (unknownSuites.length > 0) {
throw new Error(`Unknown test suite: ${unknownSuites.join(", ")}`);
}
rmSync(testsOutDir, { force: true, recursive: true });
for (const suite of selectedSuites) {
const entryPoints = [
...findTestFiles(suite.testDir),
...runtimeEntryPointsForSuite(suite.name)
];
if (entryPoints.length === 0) {
continue;
}
await esbuild.build({
absWorkingDir: projectRoot,
bundle: true,
entryNames: "[name]",
entryPoints,
external: [
"better-sqlite3",
"electron"
],
format: "cjs",
legalComments: "none",
loader: {
".gif": "file",
".ico": "file",
".jpg": "file",
".jpeg": "file",
".png": "file",
".svg": "file",
".webp": "file"
},
logLevel: "info",
outdir: path.join(testsOutDir, suite.name),
platform: "node",
plugins: [rendererAliasPlugin(), packageAliasPlugin()],
target: "node22"
});
}
function runtimeEntryPointsForSuite(suiteName) {
if (suiteName !== "main") {
return [];
}
return [
path.join(coreSourceRoot, "mcp", "fusion-vision-mcp.ts"),
path.join(coreSourceRoot, "mcp", "toolhub-mcp.ts")
];
}
function findTestFiles(dir) {
if (!existsSync(dir)) {
return [];
}
const files = [];
for (const name of readdirSync(dir)) {
const file = path.join(dir, name);
const stat = statSync(file);
if (stat.isDirectory()) {
files.push(...findTestFiles(file));
} else if (/\.(test|spec)\.(mjs|ts|tsx)$/.test(name)) {
files.push(file);
}
}
return files.sort();
}
function rendererAliasPlugin() {
return {
name: "renderer-test-alias",
setup(build) {
build.onResolve({ filter: /^@\// }, (args) => {
return { path: resolveRendererImport(args.path.slice(2)) };
});
}
};
}
function packageAliasPlugin() {
return {
name: "test-package-alias",
setup(build) {
build.onResolve({ filter: /^@ccr\/cli\// }, (args) => {
return { path: resolvePackageImport(cliSourceRoot, args.path.slice("@ccr/cli/".length)) };
});
build.onResolve({ filter: /^@ccr\/core\// }, (args) => {
return { path: resolvePackageImport(coreSourceRoot, args.path.slice("@ccr/core/".length)) };
});
}
};
}
function resolveRendererImport(importPath) {
return resolvePackageImport(rendererRoot, importPath);
}
function resolvePackageImport(rootDir, importPath) {
const basePath = path.resolve(rootDir, importPath);
const candidates = [
basePath,
`${basePath}.tsx`,
`${basePath}.ts`,
`${basePath}.jsx`,
`${basePath}.js`,
`${basePath}.json`,
path.join(basePath, "index.tsx"),
path.join(basePath, "index.ts"),
path.join(basePath, "index.jsx"),
path.join(basePath, "index.js")
];
for (const candidate of candidates) {
if (existsSync(candidate) && statSync(candidate).isFile()) {
return candidate;
}
}
return basePath;
}

View file

@ -1,233 +0,0 @@
const fs = require("node:fs");
const path = require("node:path");
const betterSqliteNativeRelativePath = path.join(
"app.asar.unpacked",
"node_modules",
"better-sqlite3",
"build",
"Release",
"better_sqlite3.node"
);
const betterSqlitePackageRelativePath = path.join("app.asar.unpacked", "node_modules", "better-sqlite3");
const betterSqlitePrunablePaths = [
"deps",
"src",
"binding.gyp",
"README.md",
"docs",
"benchmark",
"benchmarks",
"test",
path.join("build", "Release", "obj"),
path.join("build", "Release", "obj.target")
];
module.exports = async function verifyPackagedApp(context) {
const platform = context?.electronPlatformName;
const arch = normalizeArch(context?.arch);
const appOutDir = context?.appOutDir;
if (!platform || !appOutDir) {
throw new Error("Packaged app verification did not receive electron-builder platform context.");
}
const resourcesDir = findResourcesDir(appOutDir, platform);
assertFile(path.join(resourcesDir, "app.asar"), "Packaged app archive");
cleanupBetterSqlitePackage(resourcesDir);
const nativeModule = path.join(resourcesDir, betterSqliteNativeRelativePath);
assertFile(nativeModule, "better-sqlite3 native module");
const nativeInfo = inspectNativeModule(nativeModule);
if (nativeInfo.platform !== platform) {
throw new Error(
`Packaged better-sqlite3 native module targets ${formatNativeInfo(nativeInfo)}, ` +
`but electron-builder is packaging ${platform}/${arch}. Rebuild native dependencies for the target platform before packaging.`
);
}
if (arch && !nativeArchMatches(nativeInfo, arch)) {
throw new Error(
`Packaged better-sqlite3 native module targets ${formatNativeInfo(nativeInfo)}, ` +
`but electron-builder is packaging ${platform}/${arch}. Run npm run rebuild:sqlite3 on the target platform before packaging.`
);
}
};
function cleanupBetterSqlitePackage(resourcesDir) {
const packageDir = path.join(resourcesDir, betterSqlitePackageRelativePath);
for (const relativePath of betterSqlitePrunablePaths) {
fs.rmSync(path.join(packageDir, relativePath), { force: true, recursive: true });
}
}
function findResourcesDir(appOutDir, platform) {
if (platform !== "darwin") {
return path.join(appOutDir, "resources");
}
const appBundle = fs.readdirSync(appOutDir)
.find((entry) => entry.endsWith(".app") && fs.statSync(path.join(appOutDir, entry)).isDirectory());
if (!appBundle) {
throw new Error(`Could not find a .app bundle in ${appOutDir}`);
}
return path.join(appOutDir, appBundle, "Contents", "Resources");
}
function assertFile(file, label) {
let stat;
try {
stat = fs.statSync(file);
} catch {
throw new Error(`${label} is missing: ${file}`);
}
if (!stat.isFile() || stat.size === 0) {
throw new Error(`${label} is empty or not a file: ${file}`);
}
}
function inspectNativeModule(file) {
const buffer = fs.readFileSync(file);
if (buffer.length < 32) {
throw new Error(`Native module is too small to identify: ${file}`);
}
if (buffer[0] === 0x4d && buffer[1] === 0x5a) {
return inspectPe(buffer, file);
}
if (buffer[0] === 0x7f && buffer[1] === 0x45 && buffer[2] === 0x4c && buffer[3] === 0x46) {
return inspectElf(buffer);
}
const magicLe = buffer.readUInt32LE(0);
const magicBe = buffer.readUInt32BE(0);
if (magicLe === 0xfeedface || magicLe === 0xfeedfacf || magicLe === 0xcefaedfe || magicLe === 0xcffaedfe) {
return inspectMachO(buffer, magicLe);
}
if (magicBe === 0xcafebabe || magicBe === 0xcafebabf) {
return inspectFatMachO(buffer, magicBe);
}
throw new Error(`Native module has an unknown binary format: ${file}`);
}
function inspectPe(buffer, file) {
const peOffset = buffer.readUInt32LE(0x3c);
if (peOffset + 6 >= buffer.length || buffer.toString("ascii", peOffset, peOffset + 4) !== "PE\0\0") {
throw new Error(`Native module has an invalid PE header: ${file}`);
}
const machine = buffer.readUInt16LE(peOffset + 4);
return {
arch: peMachineArch(machine),
platform: "win32"
};
}
function inspectElf(buffer) {
const machine = buffer.readUInt16LE(18);
return {
arch: elfMachineArch(machine),
platform: "linux"
};
}
function inspectMachO(buffer, magicLe) {
const bigEndian = magicLe === 0xcefaedfe || magicLe === 0xcffaedfe;
const cpuType = bigEndian ? buffer.readUInt32BE(4) : buffer.readUInt32LE(4);
return {
arch: machCpuArch(cpuType),
platform: "darwin"
};
}
function inspectFatMachO(buffer, magicBe) {
const archs = [];
const count = buffer.readUInt32BE(4);
const entrySize = magicBe === 0xcafebabf ? 32 : 20;
for (let index = 0; index < count; index += 1) {
const offset = 8 + index * entrySize;
if (offset + 8 > buffer.length) {
break;
}
archs.push(machCpuArch(buffer.readUInt32BE(offset)));
}
return {
arch: "universal",
archs,
platform: "darwin"
};
}
function peMachineArch(machine) {
if (machine === 0x8664) {
return "x64";
}
if (machine === 0xaa64) {
return "arm64";
}
if (machine === 0x014c) {
return "ia32";
}
return `unknown-pe-${machine.toString(16)}`;
}
function elfMachineArch(machine) {
if (machine === 0x3e) {
return "x64";
}
if (machine === 0xb7) {
return "arm64";
}
if (machine === 0x03) {
return "ia32";
}
if (machine === 0x28) {
return "armv7l";
}
return `unknown-elf-${machine.toString(16)}`;
}
function machCpuArch(cpuType) {
if (cpuType === 0x01000007) {
return "x64";
}
if (cpuType === 0x0100000c) {
return "arm64";
}
if (cpuType === 0x00000007) {
return "ia32";
}
return `unknown-macho-${cpuType.toString(16)}`;
}
function normalizeArch(arch) {
if (typeof arch === "string") {
return arch;
}
const electronBuilderArchNames = new Map([
[0, "ia32"],
[1, "x64"],
[2, "armv7l"],
[3, "arm64"],
[4, "universal"]
]);
return electronBuilderArchNames.get(arch);
}
function nativeArchMatches(info, arch) {
if (info.arch === arch) {
return true;
}
if (info.arch === "universal") {
return arch === "universal" || Boolean(info.archs?.includes(arch));
}
return false;
}
function formatNativeInfo(info) {
return info.arch === "universal" && info.archs?.length
? `${info.platform}/${info.arch} (${info.archs.join(", ")})`
: `${info.platform}/${info.arch}`;
}

View file

@ -2,7 +2,6 @@
"name": "claude-code-router-cdn",
"private": true,
"version": "0.1.0",
"license": "MIT",
"type": "module",
"scripts": {
"deploy": "wrangler pages deploy public --project-name=claude-code-router-cdn",

View file

@ -5,7 +5,7 @@
"tsx": true,
"tailwind": {
"config": "",
"css": "packages/ui/src/styles/globals.css",
"css": "src/renderer/styles/globals.css",
"baseColor": "neutral",
"cssVariables": true,
"prefix": ""

View file

@ -1,15 +0,0 @@
services:
ccr:
build:
context: .
dockerfile: Dockerfile
image: claude-code-router:local
ports:
# Publish only Nginx. Internal web/gateway listeners stay inside the container.
- "3458:8080"
volumes:
- ccr-data:/data
restart: unless-stopped
volumes:
ccr-data:

View file

@ -1,339 +0,0 @@
# Docker Deployment
[中文说明](#中文说明) · [Project documentation](https://ccrdesk.top/en/) · [GitHub](https://github.com/musistudio/claude-code-router)
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 -d --build
docker compose logs -f ccr
```
Open <http://127.0.0.1:3458>. 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**.
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
ports:
- "127.0.0.1:3458:8080"
```
Stop or remove the container without deleting its named volume:
```sh
docker compose stop
docker compose down
```
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
```
`npm run docker:run` uses port `3458` and the `ccr-data` volume, but runs an ephemeral container without a fixed name or restart policy.
## 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
```
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.
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 \
--build-arg NODE_IMAGE=node:22-bookworm \
--build-arg RUNTIME_NODE_IMAGE=node:22-bookworm-slim \
-t claude-code-router:local .
```
Run the isolated Docker smoke test:
```sh
npm run test:docker
```
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.
## 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
```
打开 <http://127.0.0.1:3458>。首次启动时管理 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 可访问。
完整的环境变量、端口拓扑、远程部署、构建参数和烟雾测试说明见本页英文主体,对应变量名和命令在中英文环境中完全相同。

View file

@ -1,207 +0,0 @@
#!/bin/sh
set -eu
CCR_DATA_DIR="${CCR_DATA_DIR:-/data}"
CCR_WEB_HOST="${CCR_WEB_HOST:-127.0.0.1}"
CCR_WEB_PORT="${CCR_WEB_PORT:-3459}"
CCR_NGINX_PORT="${CCR_NGINX_PORT:-8080}"
CCR_GATEWAY_HOST="${CCR_GATEWAY_HOST:-127.0.0.1}"
CCR_GATEWAY_PORT="${CCR_GATEWAY_PORT:-3456}"
CCR_GATEWAY_CORE_PORT="${CCR_GATEWAY_CORE_PORT:-3457}"
CCR_PUBLIC_HOST="${CCR_PUBLIC_HOST:-127.0.0.1}"
CCR_PUBLIC_PORT="${CCR_PUBLIC_PORT:-3458}"
CCR_PUBLIC_BASE_URL="${CCR_PUBLIC_BASE_URL:-http://${CCR_PUBLIC_HOST}:${CCR_PUBLIC_PORT}}"
CCR_NO_GATEWAY="${CCR_NO_GATEWAY:-0}"
if [ -z "${CCR_WEB_AUTH_TOKEN:-}" ]; then
CCR_WEB_AUTH_TOKEN="$(node -e "process.stdout.write(require('node:crypto').randomBytes(32).toString('base64url'))")"
fi
CCR_WEB_AUTH_TOKEN_QUERY="$(node -e "process.stdout.write(encodeURIComponent(process.argv[1] || ''))" "${CCR_WEB_AUTH_TOKEN}")"
export HOME="${CCR_DATA_DIR}"
export CCR_DATA_DIR
export CCR_GATEWAY_CORE_PORT
export CCR_GATEWAY_HOST
export CCR_GATEWAY_PORT
export CCR_NGINX_PORT
export CCR_NO_GATEWAY
export CCR_PUBLIC_BASE_URL
export CCR_PUBLIC_HOST
export CCR_PUBLIC_PORT
export CCR_WEB_AUTH_TOKEN
export CCR_WEB_AUTH_TOKEN_QUERY
export CCR_WEB_HOST
export CCR_WEB_PORT
CONFIG_DIR="${HOME}/.claude-code-router"
CONFIG_FILE="${CONFIG_DIR}/config.json"
APP_CONFIG_DB_FILE="${CONFIG_DIR}/config.sqlite"
mkdir -p "${CONFIG_DIR}" "${CONFIG_DIR}/app-data" /run/nginx /var/lib/nginx /var/log/nginx
if [ "${CCR_DOCKER_INIT_CONFIG:-1}" != "0" ] && [ ! -f "${CONFIG_FILE}" ] && [ ! -f "${APP_CONFIG_DB_FILE}" ]; then
node - <<'NODE'
const fs = require("node:fs");
const path = require("node:path");
const configDir = path.join(process.env.HOME, ".claude-code-router");
const configFile = path.join(configDir, "config.json");
const gatewayHost = process.env.CCR_GATEWAY_HOST || "0.0.0.0";
const gatewayPort = Number(process.env.CCR_GATEWAY_PORT || "3456");
const gatewayCorePort = Number(process.env.CCR_GATEWAY_CORE_PORT || "3457");
const publicBaseUrl = (process.env.CCR_PUBLIC_BASE_URL || `http://127.0.0.1:${process.env.CCR_PUBLIC_PORT || "3458"}`).replace(/\/+$/, "");
fs.mkdirSync(configDir, { recursive: true, mode: 0o700 });
fs.writeFileSync(configFile, `${JSON.stringify({
HOST: gatewayHost,
PORT: gatewayPort,
gateway: {
coreHost: "127.0.0.1",
corePort: gatewayCorePort,
enabled: true,
host: gatewayHost,
port: gatewayPort
},
routerEndpoint: publicBaseUrl
}, null, 2)}\n`, { mode: 0o600 });
NODE
fi
if [ "${CCR_DOCKER_SYNC_PUBLIC_ENDPOINT:-1}" != "0" ]; then
node - <<'NODE'
const fs = require("node:fs");
const path = require("node:path");
const configDir = path.join(process.env.HOME, ".claude-code-router");
const configFile = path.join(configDir, "config.json");
const appConfigDbFile = path.join(configDir, "config.sqlite");
const gatewayHost = process.env.CCR_GATEWAY_HOST || "127.0.0.1";
const gatewayPort = Number(process.env.CCR_GATEWAY_PORT || "3456");
const gatewayCorePort = Number(process.env.CCR_GATEWAY_CORE_PORT || "3457");
const publicBaseUrl = (process.env.CCR_PUBLIC_BASE_URL || `http://127.0.0.1:${process.env.CCR_PUBLIC_PORT || "3458"}`).replace(/\/+$/, "");
function syncConfig(value) {
if (!value || typeof value !== "object" || Array.isArray(value)) {
return value;
}
value.HOST = gatewayHost;
value.PORT = gatewayPort;
value.gateway = {
...(value.gateway && typeof value.gateway === "object" && !Array.isArray(value.gateway) ? value.gateway : {}),
coreHost: "127.0.0.1",
corePort: gatewayCorePort,
enabled: true,
host: gatewayHost,
port: gatewayPort
};
value.routerEndpoint = publicBaseUrl;
return value;
}
function syncJsonFile() {
if (!fs.existsSync(configFile)) {
return;
}
const parsed = JSON.parse(fs.readFileSync(configFile, "utf8"));
fs.writeFileSync(configFile, `${JSON.stringify(syncConfig(parsed), null, 2)}\n`, { mode: 0o600 });
}
function syncSqliteConfig() {
if (!fs.existsSync(appConfigDbFile)) {
return;
}
let Database;
try {
Database = require("better-sqlite3");
} catch {
return;
}
const db = new Database(appConfigDbFile);
try {
const row = db.prepare("select value_json from app_config where key = ?").get("default");
if (!row?.value_json) {
return;
}
const parsed = JSON.parse(row.value_json);
db.prepare("update app_config set value_json = ?, updated_at = ? where key = ?")
.run(JSON.stringify(syncConfig(parsed)), new Date().toISOString(), "default");
} finally {
db.close();
}
}
syncJsonFile();
syncSqliteConfig();
NODE
fi
cat > /etc/nginx/conf.d/default.conf <<EOF
server {
listen ${CCR_NGINX_PORT};
server_name _;
root /usr/share/nginx/html;
index pages/home/index.html;
absolute_redirect off;
client_max_body_size 8m;
location = / {
return 302 /pages/home/index.html?ccr_web_token=${CCR_WEB_AUTH_TOKEN_QUERY};
}
location = /pages/home/index.html {
if (\$arg_ccr_web_token = "") {
return 302 /pages/home/index.html?ccr_web_token=${CCR_WEB_AUTH_TOKEN_QUERY};
}
try_files /pages/home/index.html =404;
}
location = /api/ccr/rpc {
proxy_http_version 1.1;
proxy_set_header Host ${CCR_WEB_HOST}:${CCR_WEB_PORT};
proxy_set_header Origin http://${CCR_WEB_HOST}:${CCR_WEB_PORT};
proxy_set_header Referer http://${CCR_WEB_HOST}:${CCR_WEB_PORT}/;
proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Host \$host;
proxy_set_header X-Forwarded-Proto \$scheme;
proxy_pass http://${CCR_WEB_HOST}:${CCR_WEB_PORT};
}
location = /health {
proxy_http_version 1.1;
proxy_set_header Host ${CCR_GATEWAY_HOST}:${CCR_GATEWAY_PORT};
proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Host \$host;
proxy_set_header X-Forwarded-Proto \$scheme;
proxy_pass http://${CCR_GATEWAY_HOST}:${CCR_GATEWAY_PORT};
}
location ~ ^/(v1|v1beta|mcp|messages|chat/completions|responses|interactions)(/|$) {
proxy_http_version 1.1;
proxy_buffering off;
proxy_request_buffering off;
proxy_read_timeout 3600s;
proxy_send_timeout 3600s;
proxy_set_header Connection "";
proxy_set_header Host ${CCR_GATEWAY_HOST}:${CCR_GATEWAY_PORT};
proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Host \$host;
proxy_set_header X-Forwarded-Proto \$scheme;
proxy_pass http://${CCR_GATEWAY_HOST}:${CCR_GATEWAY_PORT};
}
location / {
try_files \$uri \$uri/ /pages/home/index.html;
}
}
EOF
if [ "$#" -gt 0 ]; then
exec "$@"
fi
if [ -x /app/node_modules/.bin/pm2-runtime ]; then
exec /app/node_modules/.bin/pm2-runtime docker/pm2.config.cjs
fi
exec /app/packages/core/node_modules/.bin/pm2-runtime docker/pm2.config.cjs

View file

@ -1,35 +0,0 @@
const noGateway = /^(1|true|yes)$/i.test(process.env.CCR_NO_GATEWAY || "");
const serverArgs = [
"--host",
process.env.CCR_WEB_HOST || "127.0.0.1",
"--port",
process.env.CCR_WEB_PORT || "3459",
"--no-open"
];
if (noGateway) {
serverArgs.push("--no-gateway");
}
module.exports = {
apps: [
{
name: "ccr-core-server",
script: "/app/packages/core/dist/main/server.js",
args: serverArgs,
cwd: "/app",
interpreter: "node",
env: {
...process.env,
NODE_ENV: "production"
}
},
{
name: "ccr-nginx",
script: "/usr/sbin/nginx",
args: ["-g", "daemon off;"],
cwd: "/app",
interpreter: "none"
}
]
};

View file

@ -7,7 +7,6 @@
"": {
"name": "claude-code-router-docs",
"version": "0.1.0",
"license": "MIT",
"devDependencies": {
"astro": "7.0.0",
"lucide-astro": "^0.556.0"

View file

@ -2,7 +2,6 @@
"name": "claude-code-router-docs",
"private": true,
"version": "0.1.0",
"license": "MIT",
"type": "module",
"scripts": {
"dev": "astro dev",

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 97 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 29 KiB

View file

@ -51,7 +51,7 @@ const isSectionDoc = !docProp || docProp === sectionDoc;
const doc = docProp ?? sectionDoc;
const { Content, frontmatter } = doc;
const allHeadings = doc.getHeadings();
const tocHeadings = allHeadings.filter((heading) => heading.depth >= 2 && heading.depth <= 5);
const headings = allHeadings.filter((heading) => heading.depth === 2);
const isExplicitHref = (value: string) =>
value.startsWith("#") ||
value.startsWith("/") ||
@ -155,10 +155,9 @@ const sidebarTree = sidebarNavItems.map((navItem, index) => {
})),
};
});
const tocItems = tocHeadings.map((heading) => ({
const tocItems = headings.map((heading) => ({
label: heading.text,
href: `#${heading.slug}`,
depth: heading.depth,
}));
const pageMarkdown = doc.rawContent();
---

View file

@ -2,37 +2,25 @@
title: Claude Code Router Detailed Configuration
pageTitle: Detailed Configuration
eyebrow: Detailed Configuration
lead: "Separate main app pages from settings pages while following the app's actual order: main pages cover overview, providers, Agent Config, routing, Fusion, API keys, logs and observability, server, and extensions; settings pages cover ToolHub, Bots, data, and tray."
lead: Configure providers, routing, Agent Config, Fusion, Bots, and the config file location in detail.
---
## Page Structure
Detailed configuration docs are split into standalone pages. Every left-sidebar item opens a page; the right outline is reserved for headings inside the current page. Main pages follow the app's left navigation order. Settings pages are grouped separately and follow the settings dialog order.
## Main Pages
Detailed configuration docs are split into standalone pages. Every left-sidebar item opens a page; the right outline is reserved for headings inside the current page.
| Page | Covers |
| --- | --- |
| Overview Dashboard | System status, account balance, usage widgets, layout editing, and share cards |
| Provider Config | Upstream services, protocol, Base URL, model list, and credentials |
| One click import | Provider deeplink protocol, manifest import, one-click import buttons, and security boundaries |
| Agent Config | Agent launch method, model, scope, multi-instance launching, and Bot binding |
| Routing Config | Default routing, conditional rules, fallback, and request rewrites |
| Fusion Models | Combine a base model with vision, search, or MCP tools into a new selectable model |
| API Keys | Client access keys, expiration, and local limits |
| Logs & Observability | Request logs, Agent execution traces, tool calls, and tool results |
| Server | Host, port, proxy mode, system proxy, network capture, and CA certificate |
| Fusion Models | Combine a base model with vision, search, or MCP tools into a new selectable model |
| Agent Config | Agent launch method, model, scope, multi-instance launching, and Bot binding |
| Extension Mechanism | Wrapper plugins, core gateway plugins, custom extension creation, and debugging |
## Settings Pages
| Page | Covers |
| --- | --- |
| ToolHub | Collapse many MCP servers into one dynamic tool resolution entry point for agents |
| Bots And IM Agent Relay | Bot forwarding, handoff mode, and platform pages |
| Config Database Location | SQLite config database location maintained by the desktop app |
| Tray Configuration | Tray icon, balance progress, and tray window widgets |
| Config File Location | Config file location maintained by the desktop app |
## Content Relationships
Overview Dashboard shows system status and usage. Provider Config and One click import cover how upstream model services enter CCR. Agent Config covers Claude Code, Codex, and ZCode launch, multi-instance usage, and model selection. Routing determines where model requests go. Fusion covers vision, web search, and MCP tools. API Keys control client access to CCR. Logs & Observability cover request logs and agent execution traces. Server controls the local gateway listener and proxy features. Extension Mechanism covers local plugin creation, installation, and debugging. ToolHub, Bots, Config Database Location, and Tray Configuration match the corresponding settings pages in the settings dialog.
Provider Config and One click import cover how upstream model services enter CCR. Routing determines where model requests go. Agent Config covers Claude Code, Codex, and ZCode launch, multi-instance usage, and model selection. Fusion covers vision, web search, and MCP tools. Extension Mechanism covers local plugin creation, installation, and debugging. Bots cover IM platform relay.

View file

@ -1,46 +0,0 @@
---
title: API Keys
pageTitle: API Keys
eyebrow: Detailed Configuration
lead: Manage API keys that clients use to access the CCR gateway, with expiration and local limits.
---
## List Fields
| Field | Capability |
| --- | --- |
| Search API keys | Filters the list by key name or key value. |
| Add API key | Opens the create dialog and generates a new client access key. |
| Name | Display name for the key. Use it to identify a client, team, purpose, or automation. |
| Key | Masked access key. Use `Copy API key` to copy the full key. |
| Expires | Expiration time. After expiration, clients can no longer use the key to access CCR. |
| Limits | Local limit summary. Shows `No limits configured` when no limits are set. |
| Edit API key | Edits expiration and limits. The key value itself is not shown again. |
| Remove API key | Deletes the client access key. Deleted keys stop working immediately. |
## Create And Edit
| Field | Capability |
| --- | --- |
| Name | Display name for the new key. Examples: `Claude Code - laptop`, `CI`, or a team name. |
| Expiration | Selects the validity period: `Never`, `7 days`, `30 days`, `90 days`, or `Custom`. |
| Expires at | Appears for `Custom` expiration and sets the exact date and time. |
| API key created | Confirmation dialog after creation. It displays the full key. |
| Copy this key now. It may not be shown again. | Reminder to copy the key immediately because CCR will not show it again after the dialog closes. |
## Advanced Settings
`Advanced settings` adds local limits to a client key. When a limit is reached, requests using that key are rejected or limited; provider-side quota is not changed.
| Field | Capability |
| --- | --- |
| Advanced settings | Expands or collapses limit editing. |
| No limits configured | The key has no local limits. |
| Requests | Limits by request count. |
| Tokens | Limits by token count. |
| Images | Limits by image count. |
| per minute | Uses a 1-minute limit window. |
| per hour | Uses a 1-hour limit window. |
| per day | Uses a 1-day limit window. |
| Add limit | Adds one limit rule. |
| Remove limit | Removes the current limit rule. |

View file

@ -11,9 +11,8 @@ 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. 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.
5. Choose **Forward agent messages** or **Handoff**, then reopen the agent from CCR.
## Verification
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.
Open the agent from CCR and send a test message. Confirm that Logs include Bot records and the IM side receives the message.

View file

@ -5,61 +5,15 @@ 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.
## What Bots Do
Bots forward agent messages to instant-messaging platforms and can hand off active work after the desktop has been idle.
## 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 <text>` | Search project names and paths. |
| `/project current` | Show the current Project. |
| `/project use <n>` | Change Project and clear the previous Session selection. |
| `/project name <label>` | Set the Bot display label for this Project. |
### Session Commands
| Command | Purpose |
| --- | --- |
| `/session` | Show all Session commands. |
| `/session list [page]`, `/session find <text>` | Browse Sessions only in the current Project. |
| `/session current`, `new [title]`, `use <n>`, `reset` | Inspect, create, continue, or clear a Session selection. |
| `/session status`, `cancel` | Inspect the active turn/queue, or cancel it and clear the queue. |
| `/session approve [session]`, `deny`, `answer <text>` | Answer an Agent-generated permission or input request; every platform has text commands, and card-capable platforms also show action buttons. |
| `/session name <label>` | Rename the current Session. |
| `/session archive <n>`, `restore <n>`, `delete <n> confirm` | Archive, restore, or permanently delete with confirmation. |
| `/session history [count]`, `usage` | Show recent history and token/cache/cost summaries. |
| `/session models`, `model`, `effort`, `mode` | Inspect or change this conversation's Session runtime settings. |
| `/session memory ...`, `skills`, `skill`, `shortcut ...` | Manage persistent context, Agent skills, and shortcuts. |
| `/session doctor`, `deliveries` | Show connection, outbox, recent-delivery, and redacted-error diagnostics. |
The public Bot command domains are `/project` and `/session`. Other slash commands return the unknown-command response, while plain natural language such as `help` or `list` enters the Agent as a prompt.
## Bot Settings
- **Bot language**: automatic, English, or Simplified Chinese.
- **Maximum turn time**: interrupt timed-out turns and return a final state.
- **Session idle reset**: prepare a new Session in the same Project after inactivity; `0` disables it.
- **Message chunk and attachment limits**: adapt to platform limits and bound inbound files.
- **Streaming replies and progress**: forward visible text and tool stages.
- **Send and receive attachments**: accept inbound images/files and return artifacts from the current workspace.
- **Allow Agent shell tools**: controls Agent shell-tool permission; the Bot command surface remains `/project` and `/session`.
The local state keeps bounded deduplication records, pending turns, a durable outbox, and recent delivery results. Event idempotency gives each Agent turn one execution, and pending delivery resumes while the App is online again.
## Platform Pages
Slack, Discord, Telegram, LINE, Weixin, WeCom, Feishu, and DingTalk each have a dedicated page; iMessage uses a local integration. The SDK selects Markdown, cards, streaming updates, file messages, or text according to platform capabilities.
Slack, Discord, Telegram, LINE, Weixin, WeCom, Feishu, and DingTalk each have a dedicated page.

View file

@ -1,19 +1,16 @@
---
title: Config Database Location
pageTitle: Config Database Location
title: Config File Location
pageTitle: Config File Location
eyebrow: Detailed Configuration
lead: Locate the SQLite configuration database maintained by the CCR desktop app.
lead: Locate the JSON configuration file maintained by the CCR desktop app.
---
## Default Locations
- **macOS/Linux**: `~/.claude-code-router/config.sqlite`
- **Windows**: `%APPDATA%\claude-code-router\config.sqlite`
Docker sets `HOME=/data`, so its configuration database is `/data/.claude-code-router/config.sqlite`. Persist the complete `/data` directory rather than mounting only one database file.
- **macOS**: `~/Library/Application Support/claude-code-router/config.json`
- **Windows**: `%APPDATA%/claude-code-router/config.json`
- **Linux**: `~/.config/claude-code-router/config.json`
## Applying Changes
CCR stores runtime configuration in SQLite. A legacy `config.json` is read only once as a migration source when no SQLite config exists; after migration, editing `config.json` does not affect the current configuration.
Use the desktop UI to change configuration, or export a backup from **Settings**. Do not edit `config.sqlite` directly while CCR is running; SQLite also maintains companion `config.sqlite-wal` and `config.sqlite-shm` files in the same directory.
Restart CCR after editing the file directly.

View file

@ -5,7 +5,7 @@ eyebrow: Detailed Configuration
lead: Learn how CCR extensions are loaded, what they can register, and how to create, install, and debug your own extension.
---
## Extension Types
## What Extensions Are
CCR has two extension layers:
@ -177,7 +177,7 @@ The recommended flow is through the desktop UI:
4. Save the config.
5. Open **Server** and restart the gateway.
CCR stores runtime configuration in SQLite. Add extensions through the UI; the legacy JSON config file is kept here only as a reference. The extension entry has this shape:
You can also edit the config file directly:
```json
{
@ -194,7 +194,7 @@ CCR stores runtime configuration in SQLite. Add extensions through the UI; the l
}
```
Restart the gateway after saving the extension config. See [Config Database Location](/en/configuration/configuration-file/).
Restart CCR after editing the config file directly. See [Config File Location](/en/configuration/configuration-file/).
The local directory picker recognizes entry metadata from:
@ -270,7 +270,7 @@ Proxy route matching rules:
| Response is 401 | Routes require gateway API key by default; set `auth: "none"` for debug routes |
| Code changes do not apply | Wrapper plugins are not hot reloaded; restart the gateway or CCR |
| Port is already in use | Omit `port` in `registerHttpBackend` so CCR can allocate one automatically |
| Proxy route misses requests | Confirm proxy mode is enabled, the certificate is installed, and host matches the real request hostname |
| Proxy route is not hit | Confirm proxy mode is enabled, the certificate is installed, and host matches the real request hostname |
## Security Notes

View file

@ -11,20 +11,7 @@ Use `ccr-fusion-builtins / web_search`.
## Search Providers
Supported providers include In-app Browser, Brave, Bing, Google CSE, Serper, SerpAPI, Tavily, and Exa.
## In-app Browser
`In-app Browser` runs searches through a hidden built-in browser window in CCR Desktop, opens result pages, extracts visible content, and passes that evidence to the Fusion model. It does not require an external search API key, so it is useful when you want desktop-side browser retrieval.
Configuration options include search engine, language, country or region, and safe-search level:
- Search engine: Bing, Google, or DuckDuckGo.
- Language: for example `en` or `zh-CN`.
- Country or region: for example `US` or `CN`.
- Safe search: default, moderate, strict, or off.
> Note: `In-app Browser` depends on CCR Desktop's Electron built-in browser capability and is only available in the desktop app. CLI, server deployments, and pure web environments do not have the built-in browser integration; use Brave, Bing, Google CSE, Serper, SerpAPI, Tavily, or Exa instead.
Supported providers include Brave, Bing, Google CSE, Serper, SerpAPI, Tavily, and Exa.
## Troubleshooting

View file

@ -1,131 +0,0 @@
---
title: Overview Dashboard
pageTitle: Overview Dashboard
eyebrow: Detailed Configuration
lead: Customize the CCR home dashboard to inspect system status, account balance, requests, tokens, cost, and model distribution.
---
## When To Use It
| Scenario | What to inspect |
| --- | --- |
| Check gateway health | System status, success rate, errors, average latency |
| Estimate recent spend | Requests, input / output / cache tokens, estimated cost |
| Compare upstream usage | Provider analysis, model distribution, client analysis |
| Watch account quota | Balance, subscription quota, remaining quota, account status |
| Report or share usage | AI Usage Wrapped, CCR Route Map, Model Leaderboard, Spend Receipt, and other share cards |
## Time Range
The `Usage over time` control at the top drives every widget that depends on usage stats. After you switch ranges, requests, tokens, cost, trends, distribution, and share cards are recomputed for the selected window.
| Option | Window |
| --- | --- |
| `Today` | Current local date from 00:00 to now, bucketed hourly. |
| `24h` | Last 24 hours, bucketed hourly. |
| `7d` | Last 7 days, bucketed daily. |
| `30d` | Last 30 days, bucketed daily. |
The account balance widget does not use this time range. It shows the latest snapshot returned by provider account connectors.
## Edit Layout
Click the pencil button in the upper-right corner to enter editing mode. Editing mode has three columns:
| Area | Purpose |
| --- | --- |
| Components | Left palette. Click a template to add it to the dashboard. |
| Preview | Middle layout preview. Drag widgets to reorder them or click a widget to select it. |
| Component properties | Right property panel for changing type, data, size, style, or removing the selected widget. |
Common operations:
1. Add a widget: click a template in `Components`.
2. Reorder widgets: drag them in `Preview`.
3. Resize a widget: select it, then drag the right, bottom, or bottom-right resize handle.
4. Change data: use `Component category` and `Data` in `Component properties`.
5. Change presentation: choose `Widget size` and `Style`.
6. Save the result: click `Done`; the layout is persisted in app configuration.
7. Restore defaults: click `Reset layout` while editing.
Removing a widget only removes that card from the overview layout. It does not delete request logs, providers, account connectors, or upstream configuration. If all widgets are removed, the page shows `No widgets configured`.
## Widget Catalog
Sizes are written as `width:height`, with both dimensions from `1` to `4`. The overview grid has up to 4 columns on desktop and collapses automatically on narrow screens.
| Widget | Data | Default size | Default style | Styles |
| --- | --- | --- | --- | --- |
| Status component | System status | `4:1` | Timeline | Timeline, Compact |
| Account component | All accounts or one account | `4:2` | Cards | Cards, Compact, Bars, Ring, Semicircle, Arc, Nested rings |
| Metric component | Requests, tokens, cost | `1:1` | Cards | Cards, Compact, Bar, Ring |
| Trend component | Usage over time | `3:2` | Composed | Composed, Area, Line, Bar |
| Activity component | Token activity | `4:2` | Heatmap | Heatmap |
| Breakdown component | Token distribution / Model distribution | Token distribution: `1:2`; Model distribution: `2:2` | Token distribution: Bars; Model distribution: Pie | Bars, Stacked, Donut, Pie |
| Analysis component | Client Analysis / Provider Analysis | `2:2` | Table | Table, Compact |
| Share card | AI Usage Wrapped, CCR Route Map, Model Leaderboard, AI Fuel Cockpit, Token Calendar Poster, Spend Receipt | `1:4` | Card | Card |
Size constraints:
| Rule | Reason |
| --- | --- |
| Share cards have a minimum size of `1:4`. | PNG export uses a vertical poster ratio and needs enough height. |
| The account widget has a minimum size of `2:2` when showing All accounts with the Compact style. | Multi-account lists need readable space. |
| Legacy aliases are still accepted: `small` -> `1:1`, `medium` / `large` -> `2:2`, `wide` -> `3:2`, `full` -> `4:1` or `4:2`. | Backward compatibility for older config. |
## Metric Data
`metric` widgets use the `metric` field to choose the displayed value.
| `metric` | Meaning |
| --- | --- |
| `requests` | Request count |
| `total-tokens` | Total tokens |
| `input-tokens` | Input tokens |
| `output-tokens` | Output tokens |
| `cache-tokens` | Cache tokens |
| `cache-ratio` | Cache ratio |
| `estimated-cost` | Estimated cost, calculated from model pricing data |
| `success-rate` | Success rate |
| `errors` | Error count |
| `avg-latency` | Average latency |
## Account Widget
The account widget reads provider account / usage connectors. To show balance or remaining quota, first enable and test `Fetch usage` in provider configuration.
| Data selection | Behavior |
| --- | --- |
| `All accounts` | Shows every available account snapshot. |
| One account | Shows only one provider or credential snapshot. The internal config value is usually `provider` or `provider::credentialId`. |
If the account widget is empty, check:
1. Whether the provider has an account / usage connector configured.
2. Whether the `Fetch usage` test succeeds.
3. Whether the API key or account endpoint is still valid.
4. Whether the selected account was deleted or renamed.
## Share Cards
Share card widgets can export PNGs through the download button in the card header. The desktop app uses native export when available; browser environments fall back to frontend canvas export. The exported image size is `1080 x 1350`.
| Card | `type` | Content |
| --- | --- | --- |
| AI Usage Wrapped | `share-usage-wrapped` | Total tokens, requests, estimated cost, cache ratio, longest activity streak, top model, top provider, peak day. |
| CCR Route Map | `share-route-map` | Main client-to-provider/model route relationships, plus client, provider, and model counts. |
| Model Leaderboard | `share-model-leaderboard` | Models ranked by tokens. |
| AI Fuel Cockpit | `share-fuel-cockpit` | Up to 3 account quota gauges. Requires account / usage connectors. |
| Token Calendar Poster | `share-token-calendar` | Contribution-calendar style token activity poster. |
| Spend Receipt | `share-spend-receipt` | Estimated cost, requests, tokens, latency, and success rate for the selected range. |
## Data Sources And Troubleshooting
| Symptom | Likely cause | What to do |
| --- | --- | --- |
| Requests, tokens, or cost are 0 | No requests went through CCR in the selected range, or usage capture has not recorded data yet. | Try `24h` / `7d`, and confirm the client is actually using CCR. |
| Cost shows `$0.00` | The model has no pricing data, or usage is very small. | Check model catalog matching and provider model names; values under 0.01 USD are shown with extra decimals. |
| Success rate or errors look unexpected | The overview only aggregates request results captured by CCR. | Compare with records on the Logs page. |
| Account balance is empty | No account connector exists, or `Fetch usage` failed. | Test account / usage field mapping in provider configuration. |
| Distribution charts have no data | Request logs lack model, provider, or token information. | Confirm requests go through CCR and upstream responses include token usage. |
| PNG export fails | Canvas export is unavailable, the element has no size, or the save dialog was canceled. | Retry in the desktop app, and make sure the card is visible and not resized too small. |

View file

@ -2,19 +2,14 @@
title: Agent Config
pageTitle: Agent Config
eyebrow: Detailed Configuration
lead: Create reusable launch configurations for Claude Code, Codex, Grok CLI, and ZCode, and open separate agent instances from different configs.
lead: Create reusable launch configurations for Claude Code, Codex, and ZCode, and open separate agent instances from different configs.
---
## Configuration Flow
## What Agent Config Is
1. Add at least one usable provider and model in **Provider Config**, or create the Fusion model you want to use.
2. Open **Agent Config** and click **Add profile**.
3. Choose the agent type, name the config, then choose the effect scope and entry mode.
4. Select a model. The value is usually `Provider name/model name`, and Fusion models can be selected too.
5. If the entry mode includes App, optionally bind a Bot and choose whether to forward agent messages or enable handoff.
6. Save the config, then open it from the Agent Config card: the terminal button copies the CLI command, and the play button starts the App instance.
Agent Config is the desktop app capability for managing Claude Code, Codex, and ZCode launch entries. It is not a provider or a routing rule; it is the full entry point for one agent launch: agent type, entry mode, model, effect scope, config file location, and optional Bot binding.
During trial, prefer **Only opened from CCR** and always open the agent from CCR. That keeps the config limited to CCR-launched instances and avoids changing the Claude Code, Codex, Grok CLI, or ZCode setup you open directly from the system.
This page exists in Detailed Configuration to explain which config opens which agent instance, rather than provider, routing, or Fusion fields.
## Multi-Instance Mechanism
@ -23,140 +18,41 @@ Every Agent Config has its own `id` and name. When CCR opens an agent, it finds
| Mechanism | Actual behavior |
| --- | --- |
| Separate config files | With **Only opened from CCR**, Claude Code and Codex write CCR-managed config files in directories separated by config `id` |
| Separate launchers | Claude Code and Grok CLI use separate launch wrappers; Codex and ZCode use separate middleware launchers; filenames are also separated by config `id` or name |
| Separate app data directories | When opening App mode, Claude App, ChatGPT (the renamed Codex desktop app), and ZCode App use user-data directories separated by config `id` |
| Separate launchers | Claude Code uses a separate launch wrapper; Codex and ZCode use separate middleware launchers; filenames are also separated by config `id` or name |
| Separate app data directories | When opening App mode, Claude App, Codex App, and ZCode App use user-data directories separated by config `id` |
| Runtime state | CCR tracks running app instances by entry mode and config `id`; reopening the same config activates the existing window, while a different config can open a separate instance |
This lets you create multiple configs for the same agent, such as "Claude Code - Work Project", "Claude Code - Test Model", or "Codex - Fusion Vision". They can use different models, scopes, and Bots, then open as separate agent instances.
## Common Options
| Option | Applies to | Description |
| --- | --- | --- |
| Agent | All | Claude Code, Codex, OpenCode, Grok CLI, or ZCode. Grok CLI supports CLI only; ZCode supports App only. |
| Config name | All | Identifies the config in CCR and can be used as the `ccr-app <config-name>` launch target. Names can contain spaces; copied commands are quoted automatically. |
| Enabled | All | Disabled configs are not exposed as active launch entries and are not applied as effective startup configs. |
| Effect scope | All | **Only opened from CCR** uses CCR-managed isolated config; **System default** writes the agent's default config. Only one enabled system-default config is allowed per agent. |
| Entry mode | Claude Code, Codex, OpenCode, Grok CLI | `CLI & APP` exposes both CLI and App entry points; `CLI only` only generates a CLI command; `App only` only exposes the App entry point. Grok CLI is fixed to `CLI only`. |
| Model | All | Default model for the opened agent, either a provider model or Fusion model. For Claude Code, leaving it empty keeps the Claude Code default. |
| Bot | App entry | Bot forwarding only works for App mode opened from CCR. CLI does not forward Bot messages yet. |
| Environment variables | All | Extra environment variables injected into this config. Claude Code includes `CLAUDE_CODE_ENABLE_GATEWAY_MODEL_DISCOVERY=1` by default so gateway model discovery is enabled. |
## Per-Agent Options
### Claude Code
| Option | What it does |
| Option | Description |
| --- | --- |
| Model override | Writes `ANTHROPIC_MODEL` for Claude Code. Leave it empty to keep Claude Code's own default model. |
| Small fast model | Writes `ANTHROPIC_SMALL_FAST_MODEL` for Claude Code lightweight tasks. Leave it empty to keep the Claude Code default. |
| Settings file | System-default mode uses the Claude Code default settings file; Only opened from CCR creates an isolated settings file under CCR's config directory, separated by Agent Config `id`. |
| Environment variables | Merged into the Claude Code settings `env`. CCR also writes the gateway endpoint, API key helper, and launch wrapper. |
| Bot | Applies only to the Claude App entry. Select a saved Bot, then choose message forwarding or handoff. |
After Claude Code CLI is opened from CCR, it uses CCR gateway model discovery. In Claude Code CLI, enter `/model` to view and switch the models exposed by CCR, including normal provider models and visible Fusion models.
Claude App is **zero-config**: when CCR opens Claude App from the desktop app, CCR automatically writes the Claude App gateway config, API key, model discovery list, and isolated user-data directory. No extra user action is required; opening Claude App from CCR automatically completes all necessary configuration. If Claude App is already running, restart it or reopen it from CCR when prompted.
Claude App and Claude Code CLI use different model-list adapters:
| Entry | Model list source | Notes |
| --- | --- | --- |
| Claude Code CLI | CCR gateway model discovery | Use `/model` in the CLI to view the list; selected requests still go through CCR providers, routing, and Fusion. |
| Claude App | CCR-generated Claude App inference models | Claude App needs Claude-compatible model names. CCR maps `Provider/model` and Fusion models into model entries Claude App can recognize, while display labels keep the real model meaning visible. |
### OpenCode
| Option | What it does |
| --- | --- |
| Provider ID | Writes the OpenCode provider reference, defaulting to `claude-code-router`. |
| Provider name | Display name shown in OpenCode, defaulting to `Claude Code Router`. |
| OpenCode model | Default model for OpenCode CLI and App. It can be a provider model or Fusion model. |
| Config file | System-default mode uses OpenCode's default config; Only opened from CCR writes a profile-specific config under CCR's config directory. |
| Environment variables | Injected into OpenCode CLI, OpenCode App, and its Bot worker. |
| Bot | Applies to the OpenCode App entry opened from CCR. Incoming Bot messages run through OpenCode CLI and replies are sent back to the same Bot conversation. |
CCR keeps one OpenCode Bot worker next to the OpenCode App process. The worker stores a project and optional session for each Bot conversation. Send `/project list|current|use` to select an Agent project, then use `/session list|current|new|use|reset` to manage sessions inside that project. Selecting another project clears the previous session, and sessions from another project cannot be selected. Only these slash-command domains are intercepted; removed `/task` and legacy flat commands are not supported.
The OpenCode CLI must be available as `opencode` in the CCR Desktop process environment. If it is installed elsewhere, set `CCR_OPENCODE_BIN` in the Agent Config environment variables. Bot sessions default to the filesystem root used by a fresh OpenCode Desktop workspace; set `CCR_OPENCODE_BOT_CWD` to the same project directory currently opened in OpenCode App when using another workspace. CCR passes that directory explicitly through `opencode run --dir`, so the resulting session appears under the matching App project. Permissions are not auto-approved by default; `CCR_OPENCODE_BOT_AUTO_APPROVE=true` enables OpenCode's dangerous `--auto` mode and should be used only in a trusted environment.
### Codex
| Option | What it does |
| --- | --- |
| Provider ID | Writes Codex `model_provider`, defaulting to `claude-code-router`. Keep it stable and use only letters, numbers, dots, underscores, or hyphens. |
| Provider name | Display name shown in Codex, defaulting to `Claude Code Router`. |
| Codex model | Default Codex model. It can be a provider model or Fusion model; if left empty, CCR uses the first available default model. |
| Show all sessions | Lets Codex show all sessions. ZCode does not expose this option. |
| Config file | Defaults to `~/.codex/config.toml`. Only opened from CCR writes into CCR-managed isolated config directories. |
| Environment variables | Injected into Codex CLI or ChatGPT. Claude Code-specific model discovery variables are not passed to Codex. |
| Bot | Applies only to the ChatGPT app entry. |
After saving, use the terminal button on the config card to copy the Codex CLI command, for example `ccr-app "Codex - Work"`. Use the play button to open ChatGPT. Following the CodexL launch model, CCR starts the Electron executable inside the ChatGPT app bundle directly, gives it an isolated user-data directory, and points `CODEX_CLI_PATH` at the CCR middleware. The middleware forwards app-server traffic to ChatGPT's bundled Codex CLI and only adapts the account display: an existing valid ChatGPT token is shown as the real ChatGPT account, while a profile without credentials uses a tokenless ChatGPT-shaped workspace identity so the desktop renderer keeps model selection available without storing a real user login. To make the native app-server select its official API marketplace, CCR creates the exact `ccr-local-profile` bootstrap only during process startup and removes it after the first native response; it is also cleaned after startup or abnormal exit and is never retained as login state. Every other authentication file is preserved. Older `Codex.app` installations remain supported.
Model and public plugin listings are not synthesized by the middleware. The native Codex app-server reads the generated `model_catalog_json` and handles `model/list` plus public `plugin/list` requests unchanged. This lets Codex refresh the official public [`openai/plugins`](https://github.com/openai/plugins) Git marketplace over the network. In a virtual workspace, only account-private marketplace requests are answered with an explicit empty result because the native service requires real ChatGPT authentication for those sections; they are never replaced with local plugins. Any downloaded Git checkout is owned only by Codex as its normal last-known-good data, not used by CCR as a replacement catalog.
### Grok CLI
Grok CLI profiles are fixed to **Only opened from CCR** and **CLI only**. After saving, copy and run the card command, for example `ccr-app "Grok - Work"`.
The generated wrapper sets Grok's model base URL and model-list URL to CCR's `/v1` gateway, supplies the profile-specific CCR API key, and sets the selected CCR model as the default. If the CCR Desktop gateway is not running, `ccr-app` starts a shared temporary service for Grok sessions and cleans it up after the last session exits. Grok CLI does not expose a separate user-config-file option, so CCR points `GROK_HOME` at a profile-specific directory. Its `config.toml` starts as a private copy of the user's config and can change independently, while `auth.json` is excluded to prevent a local xAI OAuth token from overriding the CCR key. Plugins, skills, and sessions remain shared with the original Grok home. Inside Grok CLI, use `/model` to switch among the provider and Fusion models returned by CCR; switched requests continue through CCR.
### ZCode
| Option | What it does |
| --- | --- |
| Provider ID | Writes the ZCode provider reference, defaulting to `claude-code-router`. |
| Provider name | Display name shown in ZCode, defaulting to `Claude Code Router`. |
| ZCode model | Default model when ZCode App opens. It can be a provider model or Fusion model. |
| Config file | Defaults to `~/.zcode/cli/config.json`; CCR also writes ZCode v2 config and model cache. |
| Environment variables | Injected into ZCode App and the middleware launcher. |
| Bot | Applies only to the ZCode App entry. |
ZCode supports App only, so its entry mode is fixed to `App only`. The `Show all sessions` option is hidden for ZCode.
## CLI And App Modes
| Mode | How to open | Best for | Key differences |
| --- | --- | --- | --- |
| CLI | Click the terminal button to copy the command, then run `ccr-app <config-name>` in a terminal | Working inside a project directory, shell workflows, scripting | Uses the config-specific wrapper or middleware launcher; usually stays in the terminal without opening a desktop window; Bot forwarding support is pending. |
| App | Click the play button in the CCR desktop app | Desktop windows, Bot forwarding, handoff | Reopening the same config activates the existing window. Multi-instance behavior depends on the Agent; OpenCode Desktop is single-instance, so CCR stops the managed instance before switching OpenCode profiles. |
| CLI & APP | One config exposes both CLI and App entry points | Reusing the same model config in both terminal and desktop App workflows | Both entries share the config name, model, effect scope, and environment variables, but launch differently. |
| Agent | Claude Code, Codex, or ZCode |
| Config name | Identifies the config in CCR and can be used as the `ccr <config-name>` launch target |
| Effect scope | **Only opened from CCR** uses CCR-managed isolated config; **System default** writes the agent's default config |
| Entry mode | `CLI & APP`, `CLI only`, or `App only`; ZCode supports App only |
| Model | Default model for the opened agent, either a provider model or Fusion model |
| Bot | App entry can bind a Bot for IM forwarding or handoff |
## Agent Differences
### Claude Code
Claude Code CLI config writes a settings file. With **Only opened from CCR**, CCR creates an isolated settings file under its own config directory and opens Claude Code through a separate launch wrapper.
Claude Code config writes a settings file. With **Only opened from CCR**, CCR creates an isolated settings file under its own config directory and opens Claude Code through a separate launch wrapper.
When opening Claude App from the desktop app, CCR also prepares a separate user-data directory for that config. Different Agent Config entries use different directories, so multiple Claude App instances can run at the same time.
With a Bot bound, Claude App's companion worker exposes Projects/Sessions, streaming replies, attachments, Session usage, and native permission/Ask User requests to IM. The worker stops with the App.
### Codex
Codex config writes `config.toml` and a model catalog file. With **Only opened from CCR**, CCR stores those files in a directory separated by config `id`.
Codex supports CLI and App. CLI opens through the launcher for the selected config; App launches ChatGPT, uses a separate user-data directory, and passes the selected model and provider into the app.
With a Bot bound, the Codex App companion worker uses native Codex rollout Sessions for Project/Session browsing and continuation, queueing, cancellation, model settings, usage, attachments, and diagnostics. It exists only alongside the managed App.
### OpenCode
OpenCode config writes a JSON/JSONC config that routes the selected provider and model through CCR. CLI opens through a profile-specific wrapper; App launches the installed OpenCode Desktop executable with the same effective config.
When a Bot is selected and the App is opened from CCR, CCR starts a companion worker using OpenCode-native Sessions and the same Project/Session, queue, media, settings, and diagnostics contract as the other Apps. The worker stops when the managed OpenCode App exits or the profile is switched.
### Grok CLI
Grok CLI supports CLI only. CCR opens it through a profile-specific wrapper that injects the CCR model gateway, model discovery endpoint, API key, and default model. A profile-specific Grok home excludes xAI OAuth credentials so inference reliably uses the CCR key without rewriting the user's original Grok home.
Codex supports CLI and App. CLI opens through the launcher for the selected config; App uses a separate user-data directory and passes the selected model and provider into Codex App.
### ZCode
ZCode supports App only. CCR writes ZCode CLI config, v2 config, and model cache based on ZCode home or a custom config file, then starts the App with the current Agent Config's model, provider, and separate user-data directory.
With a Bot bound, ZCode uses the Codex-compatible companion worker and native Session discovery. Closing ZCode App immediately takes the relay offline.
## Multi-Instance Suggestions
1. Create one Agent Config for each agent instance that should run independently.

View file

@ -2,102 +2,66 @@
title: One click import
pageTitle: One click import
eyebrow: Detailed Configuration
lead: Quickly add common model providers, review the details, and save them without filling everything in by hand.
lead: Use ccr://provider links to hand provider configuration to CCR, or open the import confirmation page with preset provider buttons.
---
## One-Click Import
Choose a provider below to get started. CCR shows what will be added before saving it; when using a custom entry point, make sure the source is one you trust.
The buttons below open the CCR desktop app's provider import confirmation dialog. Preset buttons do not include API keys; custom provider links may include one. Always confirm the provider name, Base URL, protocol, and models before importing a key.
<div class="provider-import-grid" aria-label="Preset provider import buttons">
<a class="provider-import-button provider-openai" href="ccr://provider?name=OpenAI&amp;base_url=https%3A%2F%2Fapi.openai.com%2Fv1&amp;protocol=openai_responses&amp;models=gpt-5.5%2Cgpt-5.5-pro%2Cgpt-5.5-instant%2Cgpt-5.4-mini" aria-label="Import OpenAI provider">
<a class="provider-import-button provider-openai" href="ccr://provider?name=OpenAI&amp;base_url=https%3A%2F%2Fapi.openai.com%2Fv1&amp;protocol=openai_responses&amp;models=gpt-4o" aria-label="Import OpenAI provider">
<span class="provider-import-icon-shell"><img src="../../../provider-icons/openai.png" alt="" loading="lazy" /></span>
<span class="provider-import-copy"><span class="provider-import-name">OpenAI</span><span class="provider-import-meta">Responses / Chat Completions</span></span>
</a>
<a class="provider-import-button provider-anthropic" href="ccr://provider?name=Anthropic&amp;base_url=https%3A%2F%2Fapi.anthropic.com&amp;protocol=anthropic_messages&amp;models=claude-fable-5%2Cclaude-opus-4-8%2Cclaude-sonnet-4-6%2Cclaude-haiku-4-5" aria-label="Import Anthropic provider">
<a class="provider-import-button provider-anthropic" href="ccr://provider?name=Anthropic&amp;base_url=https%3A%2F%2Fapi.anthropic.com&amp;protocol=anthropic_messages&amp;models=claude-sonnet-4-20250514" aria-label="Import Anthropic provider">
<span class="provider-import-icon-shell"><img src="../../../provider-icons/anthropic.png" alt="" loading="lazy" /></span>
<span class="provider-import-copy"><span class="provider-import-name">Anthropic</span><span class="provider-import-meta">Anthropic Messages</span></span>
</a>
<a class="provider-import-button provider-gemini" href="ccr://provider?name=Google+Gemini&amp;base_url=https%3A%2F%2Fgenerativelanguage.googleapis.com&amp;protocol=gemini_generate_content&amp;models=gemini-3.5-flash%2Cgemini-3.1-pro-preview%2Cgemini-3-flash-preview" aria-label="Import Google Gemini provider">
<a class="provider-import-button provider-gemini" href="ccr://provider?name=Google+Gemini&amp;base_url=https%3A%2F%2Fgenerativelanguage.googleapis.com&amp;protocol=gemini_generate_content" aria-label="Import Google Gemini provider">
<span class="provider-import-icon-shell"><img src="../../../provider-icons/gemini.svg" alt="" loading="lazy" /></span>
<span class="provider-import-copy"><span class="provider-import-name">Google Gemini</span><span class="provider-import-meta">Gemini Generate Content</span></span>
</a>
<a class="provider-import-button provider-openrouter" href="ccr://provider?name=OpenRouter&amp;base_url=https%3A%2F%2Fopenrouter.ai%2Fapi%2Fv1&amp;protocol=openai_chat_completions&amp;models=%7Eopenai%2Fgpt-latest%2C%7Eanthropic%2Fclaude-opus-latest%2C%7Eanthropic%2Fclaude-sonnet-latest%2Cgoogle%2Fgemini-3.5-flash%2Cz-ai%2Fglm-5.2" aria-label="Import OpenRouter provider">
<a class="provider-import-button provider-openrouter" href="ccr://provider?name=OpenRouter&amp;base_url=https%3A%2F%2Fopenrouter.ai%2Fapi%2Fv1&amp;protocol=openai_chat_completions" aria-label="Import OpenRouter provider">
<span class="provider-import-icon-shell"><img src="../../../provider-icons/openrouter.ico" alt="" loading="lazy" /></span>
<span class="provider-import-copy"><span class="provider-import-name">OpenRouter</span><span class="provider-import-meta">OpenAI compatible gateway</span></span>
</a>
<a class="provider-import-button provider-deepseek" href="ccr://provider?name=DeepSeek&amp;base_url=https%3A%2F%2Fapi.deepseek.com&amp;protocol=openai_chat_completions&amp;models=deepseek-v4-pro%2Cdeepseek-v4-flash%2Cdeepseek-v3.2%2Cdeepseek-reasoner%2Cdeepseek-chat" aria-label="Import DeepSeek provider">
<a class="provider-import-button provider-deepseek" href="ccr://provider?name=DeepSeek&amp;base_url=https%3A%2F%2Fapi.deepseek.com&amp;protocol=openai_chat_completions" aria-label="Import DeepSeek provider">
<span class="provider-import-icon-shell"><img src="../../../provider-icons/deepseek.ico" alt="" loading="lazy" /></span>
<span class="provider-import-copy"><span class="provider-import-name">DeepSeek</span><span class="provider-import-meta">Chat Completions</span></span>
</a>
<a class="provider-import-button provider-zhipu-coding" href="ccr://provider?name=Zhipu+AI+%28China%29+-+Coding+Plan&amp;base_url=https%3A%2F%2Fopen.bigmodel.cn%2Fapi%2Fcoding%2Fpaas%2Fv4&amp;protocol=openai_chat_completions&amp;models=glm-5.2%2Cglm-5.1%2Cglm-5-turbo%2Cglm-5v-turbo%2Cglm-4.7" aria-label="Import Zhipu AI China Coding Plan provider">
<a class="provider-import-button provider-zhipu-coding" href="ccr://provider?name=Zhipu+AI+%28China%29+-+Coding+Plan&amp;base_url=https%3A%2F%2Fopen.bigmodel.cn%2Fapi%2Fcoding%2Fpaas%2Fv4&amp;protocol=openai_chat_completions&amp;models=glm-5.2%2Cglm-5.1%2Cglm-4.7%2Cglm-4.5-air" aria-label="Import Zhipu AI China Coding Plan provider">
<span class="provider-import-icon-shell"><img src="../../../provider-icons/zhipu-cn-coding.png" alt="" loading="lazy" /></span>
<span class="provider-import-copy"><span class="provider-import-name">Zhipu Coding</span><span class="provider-import-meta">China Coding Plan</span></span>
</a>
<a class="provider-import-button provider-zhipu-general" href="ccr://provider?name=Zhipu+AI+%28China%29+-+General+Endpoint&amp;base_url=https%3A%2F%2Fopen.bigmodel.cn%2Fapi%2Fpaas%2Fv4&amp;protocol=openai_chat_completions&amp;models=glm-5.2%2Cglm-5.1%2Cglm-5%2Cglm-5v-turbo%2Cglm-4.7" aria-label="Import Zhipu AI China General Endpoint provider">
<a class="provider-import-button provider-zhipu-general" href="ccr://provider?name=Zhipu+AI+%28China%29+-+General+Endpoint&amp;base_url=https%3A%2F%2Fopen.bigmodel.cn%2Fapi%2Fpaas%2Fv4&amp;protocol=openai_chat_completions&amp;models=glm-5.2%2Cglm-5.1%2Cglm-4.7%2Cglm-4.5-air" aria-label="Import Zhipu AI China General Endpoint provider">
<span class="provider-import-icon-shell"><img src="../../../provider-icons/zhipu-cn-general.png" alt="" loading="lazy" /></span>
<span class="provider-import-copy"><span class="provider-import-name">Zhipu General</span><span class="provider-import-meta">China General Endpoint</span></span>
</a>
<a class="provider-import-button provider-zai-coding" href="ccr://provider?name=Z.ai+%28Global%29+-+Coding+Plan&amp;base_url=https%3A%2F%2Fapi.z.ai%2Fapi%2Fcoding%2Fpaas%2Fv4&amp;protocol=openai_chat_completions&amp;models=glm-5.2%2Cglm-5.1%2Cglm-5-turbo%2Cglm-5v-turbo%2Cglm-4.7" aria-label="Import Z.ai Global Coding Plan provider">
<a class="provider-import-button provider-zai-coding" href="ccr://provider?name=Z.ai+%28Global%29+-+Coding+Plan&amp;base_url=https%3A%2F%2Fapi.z.ai%2Fapi%2Fcoding%2Fpaas%2Fv4&amp;protocol=openai_chat_completions&amp;models=glm-5.2%2Cglm-5.1%2Cglm-4.7%2Cglm-4.5-air" aria-label="Import Z.ai Global Coding Plan provider">
<span class="provider-import-icon-shell"><img src="../../../provider-icons/zai-global-coding.svg" alt="" loading="lazy" /></span>
<span class="provider-import-copy"><span class="provider-import-name">Z.ai Coding</span><span class="provider-import-meta">Global Coding Plan</span></span>
</a>
<a class="provider-import-button provider-zai-general" href="ccr://provider?name=Z.ai+%28Global%29+-+General+Endpoint&amp;base_url=https%3A%2F%2Fapi.z.ai%2Fapi%2Fpaas%2Fv4&amp;protocol=openai_chat_completions&amp;models=glm-5.2%2Cglm-5.1%2Cglm-5%2Cglm-5v-turbo%2Cglm-4.7" aria-label="Import Z.ai Global General Endpoint provider">
<a class="provider-import-button provider-zai-general" href="ccr://provider?name=Z.ai+%28Global%29+-+General+Endpoint&amp;base_url=https%3A%2F%2Fapi.z.ai%2Fapi%2Fpaas%2Fv4&amp;protocol=openai_chat_completions&amp;models=glm-5.2%2Cglm-5.1%2Cglm-4.7%2Cglm-4.5-air" aria-label="Import Z.ai Global General Endpoint provider">
<span class="provider-import-icon-shell"><img src="../../../provider-icons/zai-global-general.svg" alt="" loading="lazy" /></span>
<span class="provider-import-copy"><span class="provider-import-name">Z.ai General</span><span class="provider-import-meta">Global General Endpoint</span></span>
</a>
<a class="provider-import-button provider-mistral" href="ccr://provider?name=Mistral&amp;base_url=https%3A%2F%2Fapi.mistral.ai%2Fv1&amp;protocol=openai_chat_completions&amp;models=mistral-medium-3-5%2Cmistral-large-3%2Cministral-3-14b-instruct-2512%2Cdevstral-2512" aria-label="Import Mistral provider">
<a class="provider-import-button provider-mistral" href="ccr://provider?name=Mistral&amp;base_url=https%3A%2F%2Fapi.mistral.ai%2Fv1&amp;protocol=openai_chat_completions" aria-label="Import Mistral provider">
<span class="provider-import-icon-shell"><img src="../../../provider-icons/mistral.webp" alt="" loading="lazy" /></span>
<span class="provider-import-copy"><span class="provider-import-name">Mistral</span><span class="provider-import-meta">Chat Completions</span></span>
</a>
<a class="provider-import-button provider-moonshot" href="ccr://provider?name=Kimi+API+%28China%29&amp;base_url=https%3A%2F%2Fapi.moonshot.cn%2Fv1&amp;protocol=openai_chat_completions&amp;models=kimi-k2.7-code%2Ckimi-k2.6%2Ckimi-latest%2Ckimi-thinking-preview" aria-label="Import Kimi API China provider">
<a class="provider-import-button provider-moonshot" href="ccr://provider?name=Moonshot+Kimi&amp;base_url=https%3A%2F%2Fapi.moonshot.cn%2Fv1&amp;protocol=openai_chat_completions&amp;models=moonshot-v1-8k" aria-label="Import Moonshot Kimi provider">
<span class="provider-import-icon-shell"><img src="../../../provider-icons/moonshot.ico" alt="" loading="lazy" /></span>
<span class="provider-import-copy"><span class="provider-import-name">Kimi API (China)</span><span class="provider-import-meta">China platform</span></span>
<span class="provider-import-copy"><span class="provider-import-name">Moonshot Kimi</span><span class="provider-import-meta">Chat Completions</span></span>
</a>
<a class="provider-import-button provider-moonshot-global" href="ccr://provider?name=Kimi+API+%28Global%29&amp;base_url=https%3A%2F%2Fapi.moonshot.ai%2Fv1&amp;protocol=openai_chat_completions&amp;models=kimi-k2.7-code%2Ckimi-k2.6%2Ckimi-latest%2Ckimi-thinking-preview" aria-label="Import Kimi API Global provider">
<span class="provider-import-icon-shell"><img src="../../../provider-icons/moonshot.ico" alt="" loading="lazy" /></span>
<span class="provider-import-copy"><span class="provider-import-name">Kimi API (Global)</span><span class="provider-import-meta">Global platform</span></span>
</a>
<a class="provider-import-button provider-kimi-coding" href="ccr://provider?name=Kimi+Code+-+Coding+Plan&amp;base_url=https%3A%2F%2Fapi.kimi.com%2Fcoding%2Fv1&amp;protocol=openai_chat_completions&amp;models=kimi-for-coding" aria-label="Import Kimi Code Coding Plan provider">
<span class="provider-import-icon-shell"><img src="../../../provider-icons/moonshot.ico" alt="" loading="lazy" /></span>
<span class="provider-import-copy"><span class="provider-import-name">Kimi Code</span><span class="provider-import-meta">Coding Plan</span></span>
</a>
<a class="provider-import-button provider-bailian" href="ccr://provider?name=Alibaba+Bailian&amp;base_url=https%3A%2F%2Fdashscope.aliyuncs.com%2Fcompatible-mode%2Fv1&amp;protocol=openai_chat_completions&amp;models=qwen3.7-max%2Cqwen3.7-plus%2Cqwen3.6-max-preview%2Cqwen3-coder-plus%2Cqwen3-max" aria-label="Import Alibaba Bailian provider">
<a class="provider-import-button provider-bailian" href="ccr://provider?name=Alibaba+Bailian&amp;base_url=https%3A%2F%2Fdashscope.aliyuncs.com%2Fcompatible-mode%2Fv1&amp;protocol=openai_chat_completions" aria-label="Import Alibaba Bailian provider">
<span class="provider-import-icon-shell"><img src="../../../provider-icons/bailian.ico" alt="" loading="lazy" /></span>
<span class="provider-import-copy"><span class="provider-import-name">Alibaba Bailian</span><span class="provider-import-meta">DashScope compatible</span></span>
</a>
<a class="provider-import-button provider-siliconflow" href="ccr://provider?name=SiliconFlow&amp;base_url=https%3A%2F%2Fapi.siliconflow.cn%2Fv1&amp;protocol=openai_chat_completions&amp;models=zai-org%2FGLM-5.2%2Cdeepseek-ai%2Fdeepseek-v4-pro%2Cdeepseek-ai%2Fdeepseek-v4-flash%2Czai-org%2FGLM-5.1%2Cdeepseek-ai%2FDeepSeek-V3.2" aria-label="Import SiliconFlow provider">
<a class="provider-import-button provider-siliconflow" href="ccr://provider?name=SiliconFlow&amp;base_url=https%3A%2F%2Fapi.siliconflow.cn%2Fv1&amp;protocol=openai_chat_completions" aria-label="Import SiliconFlow provider">
<span class="provider-import-icon-shell"><img src="../../../provider-icons/siliconflow.png" alt="" loading="lazy" /></span>
<span class="provider-import-copy"><span class="provider-import-name">SiliconFlow</span><span class="provider-import-meta">Chat Completions</span></span>
</a>
<a class="provider-import-button provider-runapi" href="ccr://provider?name=RunAPI&amp;base_url=https%3A%2F%2Frunapi.co%2Fv1&amp;protocol=openai_responses" aria-label="Import RunAPI provider">
<span class="provider-import-icon-shell"><img src="../../../provider-icons/runapi.jpg" alt="" loading="lazy" /></span>
<span class="provider-import-copy"><span class="provider-import-name">RunAPI</span><span class="provider-import-meta">Responses / Chat Completions</span></span>
</a>
<a class="provider-import-button provider-teamorouter" href="ccr://provider?name=TeamoRouter&amp;base_url=https%3A%2F%2Fapi.teamorouter.com&amp;protocol=anthropic_messages&amp;source=https%3A%2F%2Fteamorouter.com%2F" aria-label="Import TeamoRouter provider">
<span class="provider-import-icon-shell"><img src="../../../provider-icons/teamorouter.png" alt="" loading="lazy" /></span>
<span class="provider-import-copy"><span class="provider-import-name">TeamoRouter</span><span class="provider-import-meta">Anthropic / Chat / Responses</span></span>
</a>
<a class="provider-import-button provider-unity2" href="ccr://provider?name=Unity2.Ai&amp;base_url=https%3A%2F%2Funity2.ai%2Fv1&amp;protocol=openai_chat_completions&amp;source=https%3A%2F%2Funity2.ai%2Fregister%3Fsource%3Dclaudecoderouter" aria-label="Import Unity2.Ai provider">
<span class="provider-import-icon-shell"><img src="../../../provider-icons/unity2.jpg" alt="" loading="lazy" /></span>
<span class="provider-import-copy"><span class="provider-import-name">Unity2.Ai</span><span class="provider-import-meta">OpenAI compatible gateway</span></span>
</a>
<a class="provider-import-button provider-code0" href="ccr://provider?name=code0.ai&amp;base_url=https%3A%2F%2Fconsole.code0.ai&amp;protocol=anthropic_messages&amp;source=https%3A%2F%2Fcode0.ai%2Fagent%2Fregister%2F9n9jOsSnYQoemIVL%3Futm_source%3Dclaudecoderouter%26utm_medium%3Dpartner%26utm_campaign%3Dclaudecoderouter_2026%26utm_content%3Ddefault" aria-label="Import code0.ai provider">
<span class="provider-import-icon-shell"><img src="../../../provider-icons/code0.png" alt="" loading="lazy" /></span>
<span class="provider-import-copy"><span class="provider-import-name">code0.ai</span><span class="provider-import-meta">Anthropic / Chat / Responses</span></span>
</a>
<a class="provider-import-button provider-claudeapi" href="ccr://provider?name=claudeapi&amp;base_url=https%3A%2F%2Fgw.claudeapi.com&amp;protocol=anthropic_messages&amp;source=https%3A%2F%2Fconsole.claudeapi.com%2Fagent%2Fregister%2FLbmB7Y9kPloyzhwF%3Futm_source%3Dclaudecoderouter%26utm_medium%3Dpartner%26utm_campaign%3Dclaudecoderouter_2026%26utm_content%3Ddefault" aria-label="Import claudeapi provider">
<span class="provider-import-icon-shell"><img src="../../../provider-icons/claudeapi.png" alt="" loading="lazy" /></span>
<span class="provider-import-copy"><span class="provider-import-name">claudeapi</span><span class="provider-import-meta">Anthropic Messages</span></span>
</a>
<a class="provider-import-button provider-qiniu-ai" href="ccr://provider?name=%E4%B8%83%E7%89%9B%E4%BA%91+AI&amp;base_url=https%3A%2F%2Fapi.qnaigc.com&amp;protocol=openai_chat_completions&amp;source=https%3A%2F%2Fs.qiniu.com%2FAVjMVf" aria-label="Import Qiniu Cloud AI provider">
<span class="provider-import-icon-shell"><img src="../../../provider-icons/qiniu-ai.png" alt="" loading="lazy" /></span>
<span class="provider-import-copy"><span class="provider-import-name">Qiniu Cloud AI</span><span class="provider-import-meta">Chat / Responses / Anthropic / Gemini Generate</span></span>
</a>
<a class="provider-import-button provider-fenno" href="ccr://provider?name=Fenno.ai&amp;base_url=https%3A%2F%2Fapi.fenno.ai&amp;protocol=openai_chat_completions&amp;source=https%3A%2F%2Fapi.fenno.ai%2Fregister%3Fredirect%3D%2Fpurchase%3Ftab%3Dsubscription%2526group%3D16%26aff%3D9HHHAB5QLAES" aria-label="Import Fenno.ai provider">
<span class="provider-import-icon-shell"><img src="../../../provider-icons/fenno.jpg" alt="" loading="lazy" /></span>
<span class="provider-import-copy"><span class="provider-import-name">Fenno.ai</span><span class="provider-import-meta">Chat / Responses / Anthropic</span></span>
</a>
</div>
## Embeddable Button Component
@ -157,7 +121,7 @@ For larger configs, pass a manifest:
| `name` | Provider display name |
| `base_url` | Provider API Base URL, required for direct imports |
| `api_key` | Optional provider API key |
| `protocol` | Protocol, one of `openai_chat_completions`, `openai_responses`, `anthropic_messages`, `gemini_generate_content`, `gemini_interactions` |
| `protocol` | Protocol, one of `openai_chat_completions`, `openai_responses`, `anthropic_messages`, `gemini_generate_content` |
| `models` | Model list. Use comma/newline-separated text in HTML, or a string/array in JavaScript |
| `icon` | Provider icon URL |
| `source` | Provider website or config source |
@ -285,7 +249,7 @@ Complete manifest example:
| `name` | Provider display name |
| `base_url` | Provider API Base URL, required |
| `api_key` | Optional provider API key |
| `protocol` | Protocol, one of `openai_chat_completions`, `openai_responses`, `anthropic_messages`, `gemini_generate_content`, `gemini_interactions` |
| `protocol` | Protocol, one of `openai_chat_completions`, `openai_responses`, `anthropic_messages`, `gemini_generate_content` |
| `models` | Model list, comma-separated, newline-separated, or repeated |
| `icon` | Provider icon URL |
| `source` | Provider website or config source |

View file

@ -5,166 +5,22 @@ eyebrow: Detailed Configuration
lead: Configure upstream model services, credentials, protocols, base URLs, and model lists.
---
## Import Local Agent Login
## Basic Concept
When you add a provider, CCR scans for reusable local agent login state. If usable credentials are found, the add dialog shows the matching import entry. Importing creates a normal provider plus provider plugins, so CCR can reuse the local agent authorization without requiring a pasted API key.
### Claude Code
Claude Code import reads local Claude Code OAuth credentials. When a usable access token is available, CCR can import it as a `Claude Code API` provider.
After import:
1. The protocol is `anthropic_messages`.
2. The default model list includes `claude-sonnet-4-20250514`; you can later add or remove models in the provider model list.
3. CCR creates OAuth provider plugins that convert requests to use the Claude Code login state.
4. Account usage uses the Anthropic OAuth usage endpoint, so quota state can appear in the provider list, tray, and account panels.
If CCR only detects login traces but no usable access token, the import entry shows why it cannot be imported. Re-authenticate in Claude Code, then return to CCR and add the provider again.
### Codex
Codex import reads the local Codex auth file and model cache. When a Codex access token or refresh token is available, CCR can import it as a `Codex API` provider.
After import:
1. The protocol is `openai_responses`.
2. The API endpoint points to the Codex backend. The model list always includes at least `gpt-5-codex` and also merges models and display names from the local model cache.
3. CCR creates Codex OAuth provider plugins and refreshes access credentials when needed.
4. Account usage reads Codex quota, balance, and token-stat endpoints.
After import, select `Codex API/model-name` in routing or Agent Config. If the model cache is stale, open Codex first so it refreshes the model list, then return to CCR to import again or edit the models.
### ZCode
ZCode import reads provider API keys, API endpoints, and model lists from local ZCode config. It can be imported as a `ZCode API` provider only when CCR finds a usable provider key and Base URL.
After import:
1. The protocol is `anthropic_messages`.
2. Models come from local ZCode config first; if none are configured, CCR uses the ZCode runtime cache or default models.
3. CCR creates API-key provider plugins that use the key from local ZCode config for request authentication.
4. If the API endpoint matches a built-in CCR preset, account usage settings are reused from that preset.
If CCR detects ZCode login state but no usable provider API key, the import entry remains unavailable. Configure a usable model provider in ZCode first, then return to CCR and add the provider.
## Main Fields
| Field | Capability |
| --- | --- |
| Select preset provider | Applies a built-in provider template, including default endpoint, supported protocols, default models, icon, provider website, and sometimes account usage settings. Choose `Other / custom API endpoint` for any OpenAI, Anthropic, or Gemini compatible upstream. |
| Name | Internal CCR display name. It is also used by routing, model selectors, logs, and config references. Names must be unique. |
| API endpoint | Upstream API base URL. It controls where requests are sent, and is also used for protocol probing, model discovery, icon detection, and safety checks. Preset providers hide it by default while adding, but it can be overridden in Advanced settings. Custom providers must provide it. |
| API key | Default provider credential. When the credential pool is empty, model requests use this key. Protocol probing, model discovery, connection checks, and default usage fetching also use it. Only use a key issued for the selected endpoint. |
| Models | Model IDs exposed by CCR. Routing rules, profile model selectors, the model catalog, and client `/models` responses all use this list. |
| Search models / All / Clear | When CCR can discover models from the upstream or catalog, you can search, select all, clear, and choose models. Selected models are saved to the provider. |
| Custom models | Manually adds model IDs that discovery did not return. Use this when the provider lacks a `/models` endpoint or a new model is not in the catalog yet. |
| Check Connection | Sends real test requests with the current endpoint, API key, protocol, and selected models. It verifies key, model name, and protocol usability. |
| Models to check | Model selection inside the connection-check confirmation dialog. Use it to test only some models. |
| Check results | Shows whether each model is available, which protocol matched, and the upstream diagnostic message. Results are diagnostic. Add models through the main model selection when you want them saved. |
## Connectivity Checks
`Check Connection` sends real model requests for the models you select. It verifies whether the endpoint, API key, protocol, and model IDs are usable. The check limits generated output, but it can still create extra token usage or count against provider-side request limits.
If the provider bills by request, input tokens, or output tokens, select only the models you need to verify. Checking every model at once can create unnecessary usage. Review the diagnostics, then adjust the model list or usage-fetching settings manually when needed.
A provider is an upstream model service. CCR needs at least one provider with a valid protocol, Base URL, model list, and credential.
## Credentials
`API key` is the simplest single-key setup. For multiple upstream keys, expand `Credential pool` in Advanced settings.
Credentials store API keys. Multiple credentials can rotate by priority and weight, and can switch when a key hits a limit or fails.
| Field | Capability |
Use recognizable labels so failed keys are easy to identify in Logs.
## Provider Options
| Field | Description |
| --- | --- |
| Show credential settings | Expands or collapses credential pool editing. Collapsing does not remove saved credentials. |
| Import JSON | Imports credentials from a JSON file. CCR accepts a top-level array, or an object with a `credentials`, `keys`, or `apiKeys` array. |
| Add key | Adds one upstream API key row. |
| Enable | Controls whether this credential participates in request forwarding and usage fetching. Disabled credentials are kept but not selected. |
| Name | Display name for the credential. It appears in account usage, logs, and diagnostics, so use a recognizable purpose or quota source. |
| API key | The actual key sent to the upstream for this credential. When a credential pool is configured, CCR expands enabled credentials into internal upstream targets and prefers the pool over the main form API key for model requests. |
| Remove | Deletes the credential row. |
| Advanced key options | Expands per-key scheduling and limit fields. |
| Priority | Credential priority. Lower numbers are tried first. If omitted, the row order is used. |
| Weight | Tie-break weight among credentials with the same priority and similar usage. Higher numbers are preferred. Defaults to `1`. |
| Limits JSON | Local limit rules for this key. CCR tracks request, token, or image usage windows and skips a key once it would exceed its limit, then tries another key on the same provider. |
Common `Limits JSON` fields:
| Field | Meaning |
| --- | --- |
| `rpm` / `rph` / `rpd` | Max requests per minute / hour / day |
| `tpm` / `tph` / `tpd` | Max tokens per minute / hour / day |
| `ipm` / `iph` / `ipd` | Max images per minute / hour / day |
| `maxRequests` + `windowMs` | Max requests in a custom time window |
| `maxTokens` + `quotaWindowMs` | Max tokens in a custom time window |
Example:
```json
{
"rpm": 60,
"tpm": 100000
}
```
The credential pool is an upstream provider key pool. It is separate from the client access keys configured on the API Keys page.
## Usage Fetching
`Fetch usage` lets CCR show balance, subscription quota, status, and messages in the provider list, tray, and account panels. It does not affect whether models can be requested.
| Field | Capability |
| --- | --- |
| Fetch usage | Enables or disables account usage fetching for this provider. |
| Usage mode | Usage connector mode. `Standard usage endpoint` uses CCR standard account endpoints; `HTTP JSON request` maps a custom JSON endpoint; `Raw connector JSON` edits the connector array directly. |
| Refresh interval ms | Usage refresh interval in milliseconds. Empty uses the default interval. The minimum effective interval is 30000ms. |
### Standard Usage Endpoint
This mode tries provider-hosted CCR account endpoints such as `/.well-known/ccr/account` and `/v1/account/limits`. It is best for providers or presets that already implement CCR's standard account format.
### HTTP JSON Request
Use this mode when the provider has a balance or quota endpoint that returns a custom JSON shape.
| Field | Capability |
| --- | --- |
| Method | Usage request method, `GET` or `POST`. |
| Usage request URL | Usage endpoint URL. It can be a full URL. The request includes the provider API key unless changed through raw connector JSON. |
| Headers | Extra headers for the usage endpoint. Avoid hard-coding sensitive auth headers here; prefer provider API key auth. |
| Body | `POST` request body. Must be valid JSON. |
| Balance remaining field | JSON path for remaining balance. |
| Balance total field | JSON path for total balance or total credits. |
| Balance used field | JSON path for used balance. |
| Balance unit | Balance unit, such as `USD`, `CNY`, or `%`. |
| Subscription remaining field | JSON path for remaining subscription, token, quota, or package amount. |
| Subscription limit field | JSON path for subscription, token, quota, or package limit. |
| Subscription reset field | JSON path for reset time. It may resolve to an ISO string, seconds timestamp, or milliseconds timestamp. |
| Subscription unit | Subscription unit, such as `tokens`, `requests`, or `hours`. |
| Status field | JSON path for account status. Supported values are `ok`, `warning`, `critical`, `error`, and `unsupported`. |
| Message field | JSON path for account message. Useful for provider errors, plan notes, or risk-control messages. |
| Test usage request | Requests and parses the usage endpoint before saving. |
| Response fields | Lists selectable paths from the response. Buttons such as `Balance rem`, `Balance total`, `Balance used`, `Sub rem`, `Sub limit`, and `Reset` fill the matching field. |
Field paths use CCR's lightweight JSONPath syntax:
| Syntax | Meaning |
| --- | --- |
| `$` | Whole response object |
| `$.balance.remaining` | Object field |
| `$.items[0].value` | Array index |
| `$["weird-key"]` | Field name with special characters |
| `$.limits[?(@.type=="TOKENS")].remaining` | First array item matching simple equality filters |
| `100 - $.data.percentage` | Numeric subtraction expression, often used to convert used percent into remaining percent |
### Raw Connector JSON
`Connectors JSON` edits the `account.connectors` array directly for more complex provider setups.
| Connector type | Capability |
| --- | --- |
| `standard` | Uses CCR standard account endpoints. |
| `http-json` | Requests a JSON endpoint and maps balance, subscription, status, and message fields. |
| `plugin` | Calls an account usage connector registered by an installed plugin. |
| `local-estimate` | Shows estimated quota from local time-window config without a remote request. |
`Insert example` fills an example connector array containing `standard`, `http-json`, `plugin`, and `local-estimate` connectors.
| Name | Internal display name in CCR; keep it short and recognizable |
| Base URL | Upstream service address; custom providers should include the correct API path |
| Protocol | OpenAI, Anthropic, Gemini, or compatible protocol |
| Models | Model list exposed to CCR selectors |
| Request headers | Extra headers required by some compatible services |

View file

@ -5,141 +5,20 @@ eyebrow: Detailed Configuration
lead: Choose the model for a request, then automatically retry or switch to fallback models when the request fails.
---
## Built-In Routing
## How Routing Works
### Claude Code
CCR first decides which model the request should use, then forwards the request upstream. The current implementation follows this order:
The built-in Claude Code route detects requests from Claude Code and routes main requests to the Claude Code Agent Config model.
1. If the incoming `model` is already a known `provider/model` selector, CCR uses it directly.
2. If a custom router script is configured, the model returned by that script takes priority over UI routing rules.
3. Routing rules are evaluated from top to bottom. The first matching rule applies its request rewrite.
4. If no rule matches, CCR uses the default route. If no default is configured, it keeps the original request model.
Claude Code **main requests** use the Claude Code Agent Config model. If that model is unset, the built-in route remains inactive. CCR also automatically removes the first `x-anthropic-billing-header` system message injected by Claude Code so that billing helper messages do not affect later routing decisions. Claude Code Subagent, Task, and Workflow-created agents can still choose different models through the tag mechanism below.
The core shape of a rule is **Condition + Request action**. The condition decides whether the rule matches; the request action changes request fields. The most common action is setting `request.body.model` to a provider model or Fusion model.
#### Subagent / Workflow Auto-Routing
## What Fallback Does
Claude Code Agent / Task / Workflow can spawn additional model requests. CCR uses tag injection to let those spawned requests choose a more appropriate CCR model:
```text
<CCR-SUBAGENT-MODEL>provider/model</CCR-SUBAGENT-MODEL>
```
The full flow is:
1. A Claude Code main request matches the built-in route, so CCR inspects the current tool list.
2. If at least one model has a **Description**, CCR injects the available models and descriptions into the `Agent` / `Task` tool description and `prompt` field description.
3. If the tool list includes `Workflow`, CCR appends a Workflow-specific instruction: whenever the workflow creates an `Agent` / `Task`, each spawned agent prompt must start with the same model tag.
4. When Claude Code calls `Agent` / `Task`, or when a Workflow creates an agent, the prompt starts with `<CCR-SUBAGENT-MODEL>provider/model</CCR-SUBAGENT-MODEL>`.
5. When the spawned request reaches CCR, CCR extracts and removes the tag from the system prompt or the first two user messages, then routes that request to the tagged model.
Subagent / Workflow auto-routing therefore does not use headers such as `x-claude-code-agent-id` as the model selector. Those headers can help with observation, but the actual model selection comes from the prompt tag.
##### Pairing It With The Models Page
The **Description** field on the Models page is both the enablement switch and the selection guide for this mechanism. If no model has a Description, CCR does not inject Agent / Task / Workflow routing instructions, so it does not write an empty model list into tool descriptions.
Recommended setup:
1. Add usable models under **Providers**, and verify that the model IDs can be requested.
2. Open **Models** and fill Description for the models you want Subagents to choose automatically. Describe task fit, speed, cost, and limits.
3. Enable a Claude Code config under **Agent Config**, and choose the main model. This model handles the main Claude Code conversation.
4. Confirm that the built-in **Claude Code** route is enabled on the **Routing** page.
5. Use Agent, Task, or Workflow in Claude Code. When Claude Code spawns an agent, it can choose a CCR model from the descriptions and write the tag.
Write descriptions around tasks instead of only naming the provider. For example:
| Model purpose | Description example |
| --- | --- |
| Fast low-cost model | Good for code search, file triage, summaries, small edits, and low-cost parallel Subagents. |
| Strong reasoning model | Good for complex architecture analysis, large refactor planning, cross-file reasoning, and high-risk code review. |
| Long-context model | Good for reading large logs, long documents, repository-scale context gathering, and Workflow summaries. |
After saving, CCR formats those descriptions as “Configured CCR gateway models” in the injected Claude Code instructions. When Claude Code picks a model, request logs should show `builtin:claude-code-subagent`, and the tagged model becomes the final `resolved model`.
### Codex
The built-in Codex route adapts Codex's `apply_patch` file-editing tool for third-party or non-GPT models. The goal is for those models to edit files through the patch tool instead of generating commands or scripts such as `cat >`, `sed -i`, `python`, or `node`.
Technically, this is a tool protocol bridge. Native Codex `apply_patch` is a custom/freeform tool whose input is raw patch text, while many OpenAI-compatible third-party models handle ordinary function tools more reliably. CCR rewrites `apply_patch` into an upstream-visible `virtual_apply_patch` function tool and injects the full `apply_patch.lark` grammar into the tool description, requiring the model to put the patch in the `patch` field.
When the model returns `virtual_apply_patch`, CCR rewrites it back to Codex's expected shape: `custom_tool_call` with `name = apply_patch` and `input = raw patch text`. CCR does not edit files directly; Codex still executes the resulting patch. This adaptation follows the built-in **Codex** route and has no separate switch. GPT-named models keep using Codex's native freeform `apply_patch` path.
## Custom Routing
Custom routes are configured in the Routing page rule list. The top **Search routing rules** field filters by rule name, condition, request action, and related row text; the top-right **Add** button opens the **Add Routing Rule** dialog. The table shows each rule under **Name**, **Condition**, **Request action**, **Status**, and **Action**.
Custom rules match in list order, and the first enabled matching rule rewrites the request. Use the move up and move down buttons to adjust priority. Use the edit button to open **Edit Routing Rule**, and the delete button to open a confirmation dialog. Turning off the **Status** toggle keeps the rule in the list but removes it from matching.
### Add Or Edit A Rule
The dialog fields map directly to the saved rule:
| UI field | How to fill it | Saved meaning |
| --- | --- | --- |
| **Name** | Enter a recognizable rule name. This field is required. | Shown in the **Name** column and included in search. |
| **Condition** | Choose `request.header` or `request.body`, then fill in field, operator, and value. | Builds `condition.left`, `condition.operator`, and `condition.right`. |
| **Rewrite request parameters** | Keep at least one rewrite row. Each row chooses an operation, target key, and required value fields. | Builds `rewrites`, applied when the rule matches. |
| **Enabled** | Turn the rule on or off. | Controls `enabled`; disabled rules do not match. |
| **On failure** | Configure fallback behavior for this rule. | Overrides **Default on failure** when this rule matches. |
The **Add** or **Save** button is enabled only when the form is valid: name, condition field, and condition value are required; every rewrite row must have a key. **Delete** only requires a key. **Replace in array** requires both **Match value** and **Value**. Other operations require **Value**.
### Condition
The **Condition** area has four controls: source, field, operator, and value.
| Source | Field examples | Matched path |
| --- | --- | --- |
| `request.header` | `user-agent`, `x-api-key`, `x-client-name` | `request.header.user-agent` |
| `request.body` | `model`, `messages`, `messages.0.role`, `tools` | `request.body.model` |
Header names are case-insensitive. Body fields use dot-path lookup, and numeric segments address array indexes; for example, `messages.0.role` reads the first message role. For nested arrays such as `messages` or `tools`, `contains deep` is usually more robust than a fixed index.
The value field is parsed as a common literal when possible: `true`, `false`, `null`, numbers, JSON objects, and JSON arrays compare as their corresponding types. Other input is treated as a string. To force a value to stay string-like, wrap it as `"123"` or `'123'`.
| Operator | Use |
| --- | --- |
| `==` / `!=` | Compare actual and expected values. Numbers compare numerically; other values compare by comparable text. |
| `>` / `>=` / `<` / `<=` | Compare numerically when both sides are numbers; otherwise compare text order. |
| `starts with` | Check whether the actual value starts with the input value. Useful for model-prefix routing. |
| `contains` | Check substring containment for strings; for arrays, check direct array elements. |
| `contains deep` | Recursively checks objects and arrays. Useful for searching `messages` and `tools`. |
| `not contains` | The inverse of `contains`. |
### Rewrite Request Parameters
The **Rewrite request parameters** area starts with one `request.body.model` row. This is the common model-routing path: choose **Set**, use key `request.body.model`, and set the value to a target `provider/model` or Fusion model.
Click **Add parameter** to add more rewrite rows. The trash button removes a row, but the last row cannot be removed. When the rule matches, CCR applies the rewrite rows in order.
| Operation | Required fields | Behavior |
| --- | --- | --- |
| **Set** | key, value | Sets a request field, such as `request.body.model = provider/model` or `request.body.temperature = 0.2`. |
| **Delete** | key | Deletes a request field. Deleting `request.header.x-test` removes that header; deleting `request.body.foo` removes that body field. |
| **Append to array** | key, value | Appends the value to the target array. If the target is not an array, CCR starts from an empty array. |
| **Prepend to array** | key, value | Prepends the value to the target array. |
| **Remove from array** | key, value | Removes array elements equal to the value. |
| **Replace in array** | key, match value, value | Replaces array elements matching **Match value** with the new value. |
Rewrite values are also parsed as literals, so `0.2` becomes a number, `true` becomes a boolean, and `{"type":"web_search"}` becomes an object. Only `request.body.model` receives additional CCR model-selector normalization.
### On Failure
The dialog **On failure** control is the same control used by the page-level **Default on failure** setting. Choose **Off** to avoid fallback. Choose **Retry** to reveal **Retries**. Choose **Fallback targets** to reveal the **Fallback target** input and **Add** button. Added targets appear as tags with move up, move down, and remove buttons for ordering the fallback chain.
When a rule matches, its **On failure** setting is used. Requests that do not match a rule continue to use the page-level default.
### Examples
| Goal | Condition source | Field | Operator | Value | Rewrite request parameters |
| --- | --- | --- | --- | --- | --- |
| Route by client header | `request.header` | `x-client-name` | `==` | `claude-code` | **Set** `request.body.model = provider/model` |
| Route by original model prefix | `request.body` | `model` | `starts with` | `claude-` | **Set** `request.body.model = provider/model` |
| Route message content to a vision model | `request.body` | `messages` | `contains deep` | `image` | **Set** `request.body.model = vision-provider/model` |
| Remove a debug header | `request.header` | `x-debug-route` | `==` | `1` | **Delete** `request.header.x-debug-route` |
After saving, the rule appears in the list. Use request logs, especially `request model`, `resolved provider`, `resolved model`, and route reason, to verify that it matched.
## Fallback Handling
Fallback is the failure strategy after a model or upstream request fails. Routing picks the first model; Fallback decides whether CCR should keep trying after the current target fails.
Fallback is the failure strategy after a model or upstream request fails. It does not pick the first model; it decides whether CCR should keep trying after the current target fails.
The **Default on failure** control at the top of the Routing page is the global Fallback. Each rule also has **On failure**. When a rule matches, its rule-level Fallback overrides the global Fallback.
@ -162,7 +41,7 @@ Network errors move to the next attempt. Status-code fallback depends on the mod
| Retry | `408`, `409`, `429`, `5xx` |
| Fallback targets | Any `4xx` or `5xx` |
Before moving to the next attempt, CCR waits for every fallback-triggering failure, including network errors. It honors a positive `Retry-After` header when the upstream provides one; otherwise it uses exponential backoff starting at 1 second and capped at 30 seconds per attempt.
For `429` rate-limit responses, CCR waits before the next attempt. It honors `Retry-After` when the upstream provides it; otherwise it uses exponential backoff starting at 1 second and capped at 30 seconds per attempt.
**Fallback targets** also switches on `4xx` because model-not-found, auth, or provider-side rejection errors may only affect the current target. If the fallback model works, the request can still succeed.
@ -176,7 +55,7 @@ Configure **Default on failure** at the top of the Routing page:
2. If you choose **Retry**, set `Retries`.
3. If you choose **Fallback targets**, add backup models in priority order.
Global Fallback applies to routing rules that do not define their own Fallback.
Global Fallback applies to the default route and to rules that do not define their own Fallback.
### Rule-Level Fallback
@ -184,6 +63,24 @@ When adding or editing a routing rule, configure **On failure** for that rule.
Rule-level Fallback is useful for high-risk or expensive targets. For example, route image tasks to a Fusion vision model first, then fall back to another multimodal model; or route complex tasks to a strong model first, then fall back to a stable model.
### Conditional Routing
The current UI primarily creates conditional rules. Conditions can read request headers or the request body:
| Source | Example |
| --- | --- |
| `request.header` | `x-client-name == claude-code` |
| `request.body` | `model starts-with claude-` |
| `request.body` | `messages contains-deep image` |
After a match, the request action can set, delete, or modify request fields. The most common action is:
```text
set request.body.model = provider/model
```
The model can also be a Fusion model, so routing can send selected requests to vision, search, or tool-augmented models.
## Verification
After saving, send a request and inspect Logs:

View file

@ -1,56 +0,0 @@
---
title: Server
pageTitle: Server
eyebrow: Detailed Configuration
lead: Configure the CCR gateway host, port, and Proxy mode for MITM interception and proxying into CCR.
---
## Management And Gateway Addresses Are Separate
The Host/Port fields under **Server** configure the model gateway, not the browser management page:
| Distribution | Management entry | Model gateway |
| --- | --- | --- |
| Desktop | App window | `http://127.0.0.1:3456` by default |
| npm CLI | `http://127.0.0.1:3458` by default | `http://127.0.0.1:3456` by default |
| Docker | Public `http://127.0.0.1:3458` by default | Combined into the same public Nginx endpoint |
CLI `--host`/`--port` options configure management; this page configures the gateway. Docker internal listeners should not be published separately. See [Docker Deployment](../../guides/docker/).
## Main Fields
| Field | Capability |
| --- | --- |
| Host | Host address the CCR gateway listens on. Common values are `127.0.0.1` and `0.0.0.0`. |
| Port | Gateway listening port. Clients should point their API base URL to this port. |
`127.0.0.1` allows local access only; `0.0.0.0` listens on every IPv4 interface. Use a wildcard only for intentional LAN/remote access, together with CCR client API keys, firewall/private-network controls, and TLS at a reverse proxy.
Management tokens, CCR client API keys, and upstream credentials are separate. Gateway clients use keys created under **API Keys** and should never receive upstream provider credentials.
## Start And Verify
1. Add at least one provider and model.
2. Create a client key under **API Keys**.
3. Click **Start** or **Restart**.
4. Confirm Running status and request the gateway `/health` route.
5. Send a minimal model request and inspect the resolved provider/model under Logs.
A reachable management UI does not prove the gateway is running. Docker returns `502` from `/health` until the gateway starts, and desktop/CLI can keep management available without usable models.
## Proxy Mode
Proxy mode is the local proxy capability. When enabled, clients can send HTTP/HTTPS traffic to CCR. CCR uses MITM interception to identify and decrypt HTTPS requests, then proxies supported model requests into the CCR gateway path.
| Field | Capability |
| --- | --- |
| Proxy mode | Enables Proxy mode. CCR can receive client traffic as an HTTP/HTTPS proxy and use MITM interception to proxy model requests into CCR. |
| System proxy | Points the system proxy at CCR so apps that honor system proxy settings can go through CCR automatically. |
| Capture network | Stores network requests that pass through proxy mode so the Networking page can show request and response details. |
| CA certificate | Trust status of the current proxy CA certificate. |
| Install CA | Installs the CCR proxy CA into the system or user trust store. Installation differs by OS. |
| Check Trust | Checks again whether the proxy CA is trusted by the system. |
| Proxy status | Shows whether the proxy service is running. |
| Restart Proxy | Restarts the proxy service when proxy mode is enabled. |
Proxy mode changes local networking and certificate trust and is primarily a desktop feature. Container deployments should normally point clients directly at the public CCR Nginx gateway instead of trying to change the host system proxy or install a host CA from inside the container.

View file

@ -1,167 +0,0 @@
---
title: ToolHub
pageTitle: ToolHub
eyebrow: Detailed Configuration
lead: Collapse many MCP servers into one compact entry point so agents lazy-load task-specific tools and save context.
---
## When To Use It
As your MCP setup grows, exposing every tool directly to an agent makes the eager tool list large and easier to misuse. ToolHub exposes one `ccr-toolhub` MCP server with two meta tools:
- `tool_hub.resolve`: searches the available MCP tool catalog for the current task.
- `tool_hub.invoke`: calls a real MCP tool that was selected for this task.
Use ToolHub for tools that are not needed often but are still useful occasionally. It lazy-loads them only when a task actually needs them. The main value is saving context: large low-frequency tool catalogs do not have to stay in the agent's eager tool list, which reduces context use and the chance of selecting the wrong tool. Simple local code, file, or conversation tasks usually do not need ToolHub.
## How It Works
1. Enable ToolHub in **Settings → ToolHub**.
2. Select a configured model as the **Resolver model**. It reads the MCP tool catalog and chooses the tools needed for the task. Prefer `deepseek-v4-flash`, or another stable lightweight model in a similar flash-price tier.
3. Add or import backend MCP servers. ToolHub supports `stdio`, `streamable-http`, and `sse`.
4. Open Claude Code or Codex from CCR. CCR writes the `ccr-toolhub` MCP server into that agent config.
5. When the agent receives a request about external services, installed MCP capabilities, or business APIs, it calls `tool_hub.resolve` first, then uses `tool_hub.invoke` to run the selected tools.
ToolHub combines MCP servers configured on the ToolHub page with compatible global Agent MCP servers from older configs, and excludes `ccr-toolhub` itself to avoid recursive calls.
## Built-In Browser Automation
When ToolHub is enabled in CCR Desktop and **Built-in browser automation** is turned on, agents can use the desktop built-in browser for web tasks. You do not need to add a browser backend on the ToolHub page, and you do not need a separate API key; CCR connects to it through the local gateway authentication path.
To enable it:
1. Open **Settings → ToolHub** and turn on **Enable ToolHub**.
2. Turn on **Built-in browser automation** on the same page. This switch is shown only after ToolHub is enabled.
3. After saving settings, reopen Claude Code or Codex from CCR so the new agent instance loads the latest configuration.
> Already-running agent instances usually do not pick up this switch immediately. Restart the agent instance, or use the agent's own controls to restart ToolHub.
Built-in browser automation is useful for tasks that need real browser state, such as opening sites, reading pages, filling forms, clicking buttons, scrolling, or completing web flows like ordering, booking, lookup, and checkout when no domain-specific capability exists. After it is enabled, the agent can:
- Open or attach built-in browser tabs, then navigate to URLs or search queries.
- Read page content and find buttons, links, form fields, and other page elements.
- Click, type, select, press keys, and scroll page elements.
- Wait for page loads, navigation, dialogs, or human handoff results before continuing.
- Request human help for login, verification codes, CAPTCHA, human checks, or manual confirmation.
When a web flow needs login, verification codes, CAPTCHA, a human check, or manual confirmation, CCR shows the built-in browser window and displays the requested action in the top toolbar. After the user clicks **Done** or **Hide**, the agent receives the result and continues. Handoff waits support up to 10 minutes.
### Chrome Login Import Extension
Built-in browser automation can also import login state for selected domains from system Chrome into CCR's in-app browser. This lets the agent reuse sites where you are already signed in to Chrome. It requires the unpacked Chrome extension in this repository: `extension/chrome`.
Install it:
1. Open `chrome://extensions` in Chrome.
2. Enable **Developer mode**.
3. Click **Load unpacked**.
4. Select the repository's `extension/chrome` directory.
Import flow:
1. When a task needs existing Chrome login state, the agent can request an import; the user can also click the key button in CCR's in-app browser toolbar.
2. CCR creates a one-time import job and opens a confirmation page. If your default browser is not Chrome, copy the confirmation URL into Chrome with the extension installed.
3. Review the requested domains on the confirmation page, then click **Confirm and Import**.
4. The Chrome extension reads cookies and localStorage for those domains and submits them to CCR. After it completes, the agent can continue the task in the built-in browser.
The extension reads only the domains listed in the CCR import job. It does not enumerate every Chrome cookie. For localStorage, the extension temporarily opens non-active tabs for the selected origins, reads `localStorage`, and closes those tabs. If the confirmation page says the extension does not have site access, allow the extension to access the target domains in Chrome extension settings, reload the unpacked extension, and try again.
> Note: Built-in browser automation depends on CCR Desktop's built-in browser and is only available in the desktop app. CLI, server deployments, and pure web environments do not include this built-in capability; use an external browser automation MCP server instead.
## Options
| Option | Description |
| --- | --- |
| Enable ToolHub | Exposes `ccr-toolhub` to agents. If no backend MCP server is available, CCR does not generate a ToolHub MCP config. |
| Built-in browser automation | Shown only after ToolHub is enabled. Lets agents use CCR Desktop's built-in browser for web tasks. |
| Resolver model | Choose from configured provider models. Prefer `deepseek-v4-flash`, or another stable lightweight model in a similar flash-price tier with enough tool-description understanding. |
| Max tools | Maximum tools returned by one resolve call. Range `1` to `20`, default `10`. |
| Timeout ms | Base timeout for ToolHub resolving and invocation. Range `8000` to `300000`, default `60000`. If a backend MCP server needs a longer request timeout, CCR raises the effective invocation timeout to match the backend. |
| MCP servers | Backend tool sources. Each server needs a unique name plus transport, command or URL, environment variables, headers, and timeouts. |
| Import JSON | Imports common MCP JSON shapes. Supports a root object, array, `mcpServers`, or `mcp_servers`. |
## Add MCP Servers
### stdio
Use `stdio` for local command-line MCP servers. Configure:
- **Command**: launch command, such as `npx`, `node`, or `python`.
- **Arguments**: command arguments.
- **Working directory**: optional working directory.
- **Stdio message mode**: keep `content-length` by default; use `newline-json` for line-delimited JSON servers.
- **Environment variables**: variables needed only by this MCP server.
### streamable-http / sse
Remote MCP servers need a URL. Authentication can use:
- **API key**: stored directly in the config.
- **API key env**: read from an environment variable.
- **Headers**: custom request headers.
If a remote server starts slowly or has long-running calls, adjust that server's **Startup timeout** or **Request timeout**.
## JSON Example
The desktop app's SQLite config is the effective source, so prefer editing through the UI. The fields below are useful for backups, migration, or troubleshooting:
```json
{
"toolHub": {
"enabled": true,
"browserAutomation": true,
"llm": {
"apiKey": "sk-...",
"baseUrl": "https://api.openai.com/v1",
"model": "gpt-5-mini"
},
"maxTools": 10,
"requestTimeoutMs": 60000,
"mcpServers": [
{
"name": "filesystem",
"transport": "stdio",
"command": "npx",
"args": ["-y", "@modelcontextprotocol/server-filesystem", "/tmp"],
"env": {},
"stdioMessageMode": "content-length",
"requestTimeoutMs": 30000,
"startupTimeoutMs": 600000
}
]
}
}
```
The import dialog also accepts common MCP JSON:
```json
{
"mcpServers": {
"filesystem": {
"command": "npx",
"args": ["-y", "@modelcontextprotocol/server-filesystem", "/tmp"]
}
}
}
```
## ToolHub vs Fusion MCP
| Capability | ToolHub | Fusion Custom MCP Tool |
| --- | --- | --- |
| Entry point | Agent-side `ccr-toolhub` MCP server | Capability inside one Fusion model |
| Tool selection | Dynamically resolves a tool bundle for each task | Fixed tools selected in the model config |
| Best for | Many MCP servers, changing tool catalogs, agent-led capability discovery | Adding a known tool set to one model |
| Visibility | Claude Code or Codex configs opened through CCR | Routes or agents that select that Fusion model |
## Troubleshooting
- Agent cannot see ToolHub: make sure ToolHub is enabled and at least one backend MCP server is configured or **Built-in browser automation** is turned on, then reopen Claude Code or Codex from CCR.
- Missing resolver model or API key: select a configured resolver model and confirm the provider credential works.
- The agent cannot use built-in browser automation: make sure you are using CCR Desktop, turned on **Built-in browser automation** in **Settings → ToolHub**, and reopened Claude Code or Codex from CCR. CLI, server deployments, and pure web environments do not include this built-in capability.
- Chrome login import confirmation keeps waiting for the extension: make sure the unpacked `extension/chrome` extension is loaded in Chrome and has site access for the target domains. If your default browser is not Chrome, copy the confirmation URL into Chrome manually.
- No tools are resolved: confirm the MCP server can list tools, improve tool names and descriptions, or increase **Max tools**.
- Calls time out: check ToolHub **Timeout ms** and the backend server request/startup timeouts.
- Import fails: validate JSON, avoid duplicate server names, make sure `stdio` entries have a command and remote entries have a URL.

View file

@ -1,41 +0,0 @@
---
title: Tray Configuration
pageTitle: Tray Configuration
eyebrow: Detailed Configuration
lead: Configure the CCR system tray icon, balance progress, and tray window widgets.
---
## Top Fields
| Field | Capability |
| --- | --- |
| Tray mascot | Selects the tray icon style. Options include `Random`, `Auralis`, `Solara`, `Vesper`, and `Balance progress`. |
| Balance progress | Uses provider account usage as tray icon progress. Requires `Fetch usage` on a provider first. |
| Account | Selects the provider account used for balance progress. |
| Data | Selects the balance, subscription, or quota meter used as the progress source. |
If no account data is available, the page shows `No account data is available. Enable account monitoring on a provider first.` This usually means no provider has `Fetch usage` enabled, or usage fetching has not succeeded yet.
## Tray Window Layout
| Area | Capability |
| --- | --- |
| Components | Left-side component palette for adding or enabling tray window widgets. |
| Preview | Middle preview area showing the current tray window layout. Widgets can be dragged to reorder. |
| Component properties | Right-side editor for the selected widget's `Style`, or for removing the widget. |
## Component Types
| Component | Capability |
| --- | --- |
| Provider component | Shows `Provider tabs` for switching provider data in the tray window. It is a singleton component and can be enabled only once. |
| Header component | Shows `Title and status`. It is a singleton component and can be enabled only once. |
| Account component | Shows `Account balance`. Multiple account components can be added with different styles. |
| Trend component | Shows `Token flow chart`. |
| Activity component | Shows `Token activity`. |
| Metric component | Shows `Token stats`. |
| Breakdown component | Shows `Token mix`, `Circular metrics`, or `Model share`. |
## Styles
Different components support different `Style` options. Common styles include `Cards`, `Compact`, `List`, `Pills`, `Line`, `Area`, `Bar`, `Ring`, `Donut`, `Gauges`, `Sparkline`, and `Stacked`. Style changes only affect the tray display; they do not change routing, providers, or usage stats.

View file

@ -7,15 +7,17 @@ lead: Start from installation, connect a provider, let agents send requests thro
## Install And Start CCR
CCR is available as a desktop app, a Node.js 22+ npm CLI, and a single-entrypoint Docker deployment.
### Download And Install
| Distribution | Start entry | Default management | Default model gateway |
| --- | --- | --- | --- |
| Desktop | App UI / `ccr-app` | In-app window | `http://127.0.0.1:3456` |
| npm CLI | `ccr ui` / `ccr serve` | `http://127.0.0.1:3458` | `http://127.0.0.1:3456` |
| Docker | `docker compose up -d --build` | Shared `http://127.0.0.1:3458` | Shared Nginx endpoint |
1. Open the [GitHub Releases](https://github.com/musistudio/claude-code-router/releases) page.
2. Download the package for your system: `.dmg` or `.zip` for macOS, `.exe` for Windows, and `.AppImage` for Linux.
3. Install and open **Claude Code Router** like a normal desktop app.
Use the [installation page](install/) to choose a distribution. See the [CLI reference](cli/) for terminal commands and [Docker Deployment](docker/) for container ports, authentication, persistence, and upgrades.
### Start The Service
Open the **Server** page and click **Start**. After the page shows Running, CCR listens on the default local address `http://localhost:8080`.
If you want the service to start when the app opens, enable **Auto start** on the Server page.
## Add A Provider
@ -55,7 +57,7 @@ If you want the overview to show balance or remaining quota, open the provider's
## Connect Agent Config
Agent Config lets Claude Code, Codex, Grok CLI, ZCode, and other agents use CCR's providers, routing, and model selection.
Agent Config lets Claude Code, Codex, ZCode, and other agents use CCR's providers, routing, and model selection.
General guidance:
@ -71,17 +73,13 @@ In **Agent Config**, choose Claude Code, set the model, small fast model, and se
In **Agent Config**, choose Codex and confirm Provider ID, Provider Name, model, and config file. Only fill Codex CLI path and Codex home when you need a specific CLI or home directory.
### Grok CLI
Choose Grok CLI and select a default model, then run the copied `ccr-app <profile-name>` command. The command starts a shared temporary gateway service when CCR Desktop is not already serving one; concurrent Grok sessions keep it alive until the last session exits. CCR points Grok model discovery and inference at the local gateway; use `/model` inside Grok to switch CCR models.
### ZCode
ZCode mainly uses model, Provider ID, Provider Name, and whether it is launched from CCR. It uses the App surface and does not need Codex CLI path fields.
### Reuse A Locally Logged-In Agent
If Claude Code, Codex, Grok CLI, or ZCode is already logged in on this machine, import it as a **Local Agent Provider** from **Providers** to reuse the existing authorization without applying for another key.
If Claude Code, Codex, or ZCode is already logged in on this machine, import it as a **Local Agent Provider** from **Providers** to reuse the existing authorization without applying for another key.
## Logs & Observability

View file

@ -2,7 +2,7 @@
title: Connect Agent Config
pageTitle: Connect Agent Config
eyebrow: Quick Start
lead: Let Claude Code, Codex, Grok CLI, ZCode, and other agents use CCR's providers, routing, and model selection.
lead: Let Claude Code, Codex, ZCode, and other agents use CCR's providers, routing, and model selection.
---
## General Guidance
@ -23,14 +23,10 @@ In **Agent Config**, choose Codex and confirm Provider ID, Provider Name, model,
Only fill Codex CLI path and Codex home when you need a specific CLI or home directory.
## Grok CLI
Choose Grok CLI, select a model, and run the copied `ccr-app <profile-name>` command. When the CCR Desktop gateway is not running, the command starts a shared temporary gateway service that remains available until the last concurrent Grok session exits. Use `/model` inside Grok to switch among models exposed by CCR.
## ZCode
ZCode mainly uses model, Provider ID, Provider Name, and whether it is launched from CCR. It uses the App surface and does not need Codex CLI path fields.
## Reuse A Locally Logged-In Agent
If Claude Code, Codex, Grok CLI, or ZCode is already logged in on this machine, import it as a **Local Agent Provider** from **Providers** to reuse the existing authorization without applying for another key.
If Claude Code, Codex, or ZCode is already logged in on this machine, import it as a **Local Agent Provider** from **Providers** to reuse the existing authorization without applying for another key.

View file

@ -1,161 +0,0 @@
---
title: CLI Installation And Reference
pageTitle: CLI Installation And Reference
eyebrow: Quick Start
lead: Run the browser management UI and model gateway from npm, and launch locally installed agents through CCR profiles without Electron.
---
## `ccr` And `ccr-app`
CCR has two related commands:
| Command | Source | Primary use |
| --- | --- | --- |
| `ccr` | npm package `@musistudio/claude-code-router` | Electron-free management UI, gateway service, and profile launches. |
| `ccr-app` | CCR desktop application | Desktop-managed profile launcher used by commands copied from Agent Config cards. |
Both distributions use the same local configuration directory, but their command names are not interchangeable. Use the desktop app for tray features, notifications, automatic app updates, and desktop-only browser integrations. Use the npm CLI for headless hosts or external process supervision.
## Install, Upgrade, Or Remove
Node.js 22 or newer is required:
```sh
node --version
npm install -g @musistudio/claude-code-router
ccr --help
```
Upgrade or uninstall with npm:
```sh
npm install -g @musistudio/claude-code-router@latest
npm uninstall -g @musistudio/claude-code-router
```
Uninstalling the package does not delete local configuration or databases. If `ccr` is not found, run `npm prefix -g`, add npm's global binary directory to `PATH`, and open a new shell.
## First Start
Start the background service and open the UI:
```sh
ccr ui
```
For SSH or headless sessions:
```sh
ccr ui --no-open
```
Then add a provider/model, create a CCR client key under **API Keys**, configure routing if needed, and confirm the gateway is running under **Server**. The management UI defaults to `http://127.0.0.1:3458`; the model gateway defaults to `http://127.0.0.1:3456`.
The management token and CCR client keys are separate credentials. The first protects UI/RPC access; the second authenticates model gateway requests.
## Service Command Summary
| Command | Mode | Purpose |
| --- | --- | --- |
| `ccr start` | Background | Starts management and the gateway, then prints the authenticated management URL. |
| `ccr ui` | Background | Reuses or starts the background service and opens a browser. |
| `ccr stop` | One-shot | Stops the service created by `start` or `ui`. |
| `ccr serve` | Foreground | Runs in the current terminal for logs or process supervision. |
| `ccr web` | Foreground | Alias of `serve`. |
| `ccr <profile>` | Foreground | Launches an enabled Agent Config profile. |
## Service Options
```text
ccr start [--host <host>] [--port <port>] [--open|--no-open] [--gateway|--no-gateway]
ccr ui [--host <host>] [--port <port>] [--open|--no-open] [--gateway|--no-gateway]
ccr serve [--host <host>] [--port <port>] [--open|--no-open] [--gateway|--no-gateway]
ccr stop
```
| Option | Description |
| --- | --- |
| `--host <host>` | Management listener, default `127.0.0.1`. `--host=value` is also accepted. |
| `--port <port>` | Preferred management port, default `3458`. `--port=value` is also accepted. |
| `--open` / `--no-open` | Enables or disables browser opening. `ui` opens by default. |
| `--gateway` | Explicitly requests model gateway startup; this is the default. |
| `--no-gateway` | Starts management without starting the model gateway during service startup. |
When the preferred port is occupied, CCR tries following ports and prints the actual URL. `serve` handles `SIGINT` and `SIGTERM`; `ccr stop` manages only detached services.
## Background Service Reuse
`start` and `ui` store the process ID, URL, and a private service token in `service.json`. A later invocation verifies both the process and RPC identity before reuse.
- A valid service is reused rather than duplicated.
- New host, port, or `--no-gateway` options do not reconfigure an already running service.
- A command that requires the gateway can ask the existing management process to start it.
- Stale state is removed before a replacement service starts.
Stop first when changing listener settings:
```sh
ccr stop
ccr start --host 127.0.0.1 --port 3458
```
## Launch Agent Config Profiles
Create and enable a profile under **Agent Config**, then use:
```text
ccr <profile-name-or-id> [cli|app] [-- <agent arguments>]
```
Examples:
```sh
ccr "Codex - Work"
ccr "Codex - Work" app
ccr "Claude - Review" cli -- --model sonnet
ccr profile-id -- --help
```
- `--cli` and `--app` are alternatives to the positional surface.
- Put agent arguments after `--` to avoid ambiguity.
- Claude Code, Codex, and Grok default to CLI; ZCode defaults to App.
- Grok supports CLI only; ZCode supports App only.
- Claude App and ZCode App reject trailing agent arguments.
- App launches require a locally installed application and graphical session.
- Only enabled profiles are launchable. Use the profile ID when names are ambiguous.
Most profiles require the CCR gateway to be running. Grok CLI can create a managed temporary shared service and stops it after the final managed Grok session exits.
## Configuration And Data
| Platform | Configuration directory |
| --- | --- |
| macOS / Linux | `~/.claude-code-router` |
| Windows | `%APPDATA%\claude-code-router` |
Important paths include `config.sqlite`, `app-data/`, `service.json`, `gateway.config.json`, `profiles/`, and generated launch wrappers under `bin/`. Do not edit or copy live SQLite files. Use **Settings → Export data**, or stop CCR before taking a filesystem backup.
## Authentication And Remote Access
`CCR_WEB_HOST` and `CCR_WEB_PORT` provide defaults when command-line listener options are omitted. Set `CCR_WEB_AUTH_TOKEN` to keep a stable management UI/RPC token; otherwise a random token is generated for the process. The authenticated management URL contains `ccr_web_token`; treat the full URL as a password.
Keep the listener on `127.0.0.1` unless remote access is intentional. A remote deployment should use a strong fixed token, firewall/private network controls, and TLS at a trusted reverse proxy. Create separate CCR client API keys for gateway access and protect the data directory because it contains upstream credentials.
## Process Supervisors
Use `ccr serve --no-open` with an external supervisor. Fix the service user, `HOME`, listener, and `CCR_WEB_AUTH_TOKEN`. Do not also run a detached `ccr start` service, which can create a second management listener or make both processes compete for the same configuration.
## Troubleshooting
- **UI works but gateway requests fail:** add a provider/model and CCR client key, start the gateway under **Server**, and use `ccr serve` to inspect startup errors.
- **The UI is not on port 3458:** the preferred port was occupied; use the printed URL or stop the conflict.
- **Profile not found:** confirm it is enabled, use its ID when names are ambiguous, and re-save it if generated launchers are missing.
- **Old background options remain active:** run `ccr stop`, then start again with the new options.
- **A foreground service does not stop through `ccr stop`:** stop `ccr serve` from its terminal or supervisor.
## Related Pages
- [Install And Start CCR](../install/)
- [Agent Config](../../configuration/profiles/)
- [Server](../../configuration/server/)
- [Docker Deployment](../docker/)

View file

@ -1,198 +0,0 @@
---
title: Docker Deployment
pageTitle: Docker Deployment
eyebrow: Quick Start
lead: Run CCR Core and the browser UI behind a single Nginx entrypoint with documented ports, authentication, persistence, upgrades, and troubleshooting.
---
## Scope And Limitations
The image contains CCR Core, the built management UI, PM2, and Nginx. It is intended for a persistent model gateway and browser administration. It does not include Electron, the npm `ccr` command, tray features, host desktop Agent/App launching, desktop automatic updates, or desktop-only browser integrations.
Use the desktop distribution for local app profiles and tray workflows, or the [CLI](../cli/) for an Electron-free host command.
## Process And Port Topology
```text
host 3458 -> container Nginx 8080
|-> static management UI
|-> management RPC: 127.0.0.1:3459
|-> model gateway: 127.0.0.1:3456
`-> core runtime: 127.0.0.1:3457
```
Publish only container port `8080`. The other listeners are implementation details and should remain private.
| Public route | Purpose |
| --- | --- |
| `/`, `/pages/home/index.html` | Management UI. The root redirects to a tokenized page URL. |
| `/api/ccr/rpc` | Authenticated management RPC. |
| `/health` | Model gateway health, not UI/container health. |
| `/v1/*`, `/v1beta/*`, `/messages`, `/chat/completions`, `/responses`, `/interactions`, `/mcp/*` | Model and MCP gateway routes. |
## Start With Compose
From the repository root:
```sh
docker compose up -d --build
docker compose logs -f ccr
```
Open <http://127.0.0.1:3458>. Add a provider/model, create a CCR client key under **API Keys**, and start the gateway under **Server**. A fresh UI is available immediately, but `/health` can return `502` until the gateway has usable models.
Stop or remove the container without deleting its volume:
```sh
docker compose stop
docker compose down
```
Do not add `--volumes` unless all persisted CCR data should be deleted.
The repository mapping `3458:8080` binds every host interface. For local-only access, use:
```yaml
ports:
- "127.0.0.1:3458:8080"
```
## Authentication
CCR uses three distinct credential types:
| Credential | Purpose | Location |
| --- | --- | --- |
| `CCR_WEB_AUTH_TOKEN` | Management UI/RPC | Container environment |
| CCR client API key | Model gateway requests | **API Keys** page |
| Upstream credential | Requests from CCR to a provider | **Providers** page |
Without `CCR_WEB_AUTH_TOKEN`, the entrypoint generates a new random token for every container start. Opening `/` still works because Nginx redirects to a URL containing the current token. Use a fixed strong token for persistent or remote deployments.
Keep secrets out of shell history by using an ignored environment file:
```dotenv
CCR_WEB_AUTH_TOKEN=replace-with-a-long-random-value
CCR_PUBLIC_BASE_URL=http://127.0.0.1:3458
```
Pass it through `docker run --env-file` or the Compose service `environment`. Treat the complete management URL as a secret because `ccr_web_token` can be captured in browser history, proxy logs, screenshots, and tickets.
## Change The Public Address
Changing the host-facing port, hostname, or scheme also requires the exact client URL in `CCR_PUBLIC_BASE_URL`:
```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` updates CCR's public router endpoint. It does not publish a Docker port.
For TLS at a reverse proxy or ingress, set the HTTPS URL and keep the host port private:
```yaml
environment:
CCR_PUBLIC_BASE_URL: https://ccr.example.com
CCR_WEB_AUTH_TOKEN: ${CCR_WEB_AUTH_TOKEN:?set CCR_WEB_AUTH_TOKEN}
```
Proxy every path, allow long-lived requests and adequate body sizes, and disable buffering for SSE/model streams. Add firewall, private-network, or equivalent access controls before exposing management to an untrusted network.
## Persistent Data
The entrypoint sets `HOME=/data`; CCR data is under `/data/.claude-code-router/`, including `config.sqlite`, `gateway.config.json`, `app-data/`, `profiles/`, and generated files under `bin/`.
Prefer a named volume. A bind mount must be writable by the container, and two running CCR containers must not share the same data directory.
On a completely empty volume, the entrypoint writes minimal bootstrap `config.json`. Once the UI saves configuration, SQLite is authoritative. Startup also synchronizes persisted listener/router endpoint fields to the Docker public address unless disabled.
## Backup, Restore, And Upgrade
Use **Settings → Export data** for an application-level backup. For a complete copy, stop writes first:
```sh
docker compose stop ccr
docker compose cp ccr:/data/. ./ccr-data-backup/
docker compose start ccr
```
The backup contains secrets and may contain request/response data. Restore into a new empty volume or empty `/data` while the container is stopped. Do not overlay an old copy onto populated live data because SQLite WAL/SHM and newer runtime files can be mixed.
Upgrade after backing up:
```sh
git pull
docker compose build --pull
docker compose up -d
docker compose ps
docker compose logs --tail=200 ccr
```
Rollback should pair the previous image/source revision with a pre-upgrade backup; an older build may not understand a newer database.
## Environment Reference
Most installations should change only `CCR_WEB_AUTH_TOKEN`, `CCR_PUBLIC_BASE_URL`, and the Docker port mapping.
| Variable | Default | Description |
| --- | --- | --- |
| `CCR_WEB_AUTH_TOKEN` | Random per start | Management UI/RPC token. |
| `CCR_PUBLIC_BASE_URL` | `http://127.0.0.1:3458` | Exact public URL written to CCR configuration. |
| `CCR_PUBLIC_HOST` | `127.0.0.1` | Used only to derive the public URL when the full URL is unset. |
| `CCR_PUBLIC_PORT` | `3458` | Used only to derive the public URL when the full URL is unset. |
| `CCR_DATA_DIR` | `/data` | Data root and process `HOME`. |
| `CCR_NGINX_PORT` | `8080` | Container-private Nginx port. |
| `CCR_WEB_HOST` | `127.0.0.1` | Container-private management host. |
| `CCR_WEB_PORT` | `3459` | Container-private management port. |
| `CCR_GATEWAY_HOST` | `127.0.0.1` | Container-private model gateway host. |
| `CCR_GATEWAY_PORT` | `3456` | Container-private model gateway port used by Nginx. |
| `CCR_GATEWAY_CORE_PORT` | `3457` | Container-private core runtime port. |
| `CCR_NO_GATEWAY` | `0` | `1`, `true`, or `yes` starts management without the gateway at boot. |
| `CCR_DOCKER_INIT_CONFIG` | `1` | `0` disables empty-volume bootstrap `config.json`. |
| `CCR_DOCKER_SYNC_PUBLIC_ENDPOINT` | `1` | `0` disables startup synchronization of persisted listener/public endpoint fields. |
Internal ports normally should not change. Publish only `CCR_NGINX_PORT`.
## Build And Smoke Test
```sh
docker build \
--build-arg NODE_IMAGE=node:22-bookworm \
--build-arg RUNTIME_NODE_IMAGE=node:22-bookworm-slim \
-t claude-code-router:local .
npm run test:docker
```
The smoke test creates temporary resources and verifies the single Nginx entrypoint, UI/RPC auth, public endpoint migration, gateway startup, and `/health`. Set `CCR_DOCKER_TEST_SKIP_BUILD=1` to reuse an image or `CCR_DOCKER_TEST_IMAGE` to select another local tag.
## Operations And Troubleshooting
```sh
docker compose ps
docker compose logs -f ccr
docker compose restart ccr
docker compose config
```
- **`/` returns `302`:** expected tokenized management-page redirect.
- **`/health` returns `502`:** the model gateway is not yet configured/running; this is separate from container health.
- **UI returns `401` after a token change:** reopen the bare root URL and close tabs/bookmarks containing the old token.
- **Clients use an old host/port:** update `CCR_PUBLIC_BASE_URL` and recreate the container with endpoint synchronization enabled.
- **Data disappears after recreation:** verify the same `/data` volume is mounted; `docker compose down --volumes` deletes it.
- **Bind mount permission errors:** ensure the host directory exists and is writable, or use a named volume.
- **Container is healthy but requests fail:** inspect Server status, provider connectivity, CCR client-key auth, routing, request logs, and `docker compose logs --tail=200 ccr`.
## Related Pages
- [Install And Start CCR](../install/)
- [CLI Installation And Reference](../cli/)
- [Server](../../configuration/server/)
- [API Keys](../../configuration/api-keys/)

View file

@ -2,66 +2,17 @@
title: Install And Start CCR
pageTitle: Install And Start CCR
eyebrow: Quick Start
lead: Choose the desktop app, npm CLI, or Docker for the deployment, and distinguish the management address from the model gateway address.
lead: Download the desktop app, install it, and start the local CCR service.
---
## Choose A Distribution
## Download And Install
| Distribution | Best for | Entry | Default management address | Default gateway address |
| --- | --- | --- | --- | --- |
| Desktop app | Daily local use, tray, multi-instance Agent Apps, desktop integrations | App UI, `ccr-app` | In-app window | `http://127.0.0.1:3456` |
| npm CLI | Terminal, SSH, no Electron, external process supervisors | `ccr` | `http://127.0.0.1:3458` | `http://127.0.0.1:3456` |
| Docker | Persistent servers and container operations | Nginx | Shared public endpoint | `http://127.0.0.1:3458` with the default mapping |
1. Open the [GitHub Releases](https://github.com/musistudio/claude-code-router/releases) page.
2. Download the package for your system: `.dmg` or `.zip` for macOS, `.exe` for Windows, and `.AppImage` for Linux.
3. Install and open **Claude Code Router** like a normal desktop app.
In desktop/CLI deployments, management and the model gateway do not use the same port. Do not use CLI management port `3458` as the default model gateway. Docker intentionally combines both through one Nginx endpoint.
## Start The Service
## Install The Desktop App
Open the **Server** page and click **Start**. After the page shows Running, CCR listens on the default local address `http://localhost:8080`.
1. Open [GitHub Releases](https://github.com/musistudio/claude-code-router/releases).
2. Download `.dmg`/`.zip` for macOS, `.exe` for Windows, or `.AppImage` for Linux.
3. Install and open **Claude Code Router**.
4. Add a provider/model, create a client key under **API Keys**, then click **Start** under **Server**.
When Server shows Running, the model gateway defaults to `http://127.0.0.1:3456`. Enable automatic startup under Server if the gateway should start whenever the app opens.
## Install The npm CLI
Node.js 22 or newer is required:
```sh
npm install -g @musistudio/claude-code-router
ccr ui
```
`ccr ui` starts a background service and opens the browser. Use `ccr ui --no-open` on a headless host or `ccr serve --no-open` under a process supervisor. See [CLI Installation And Reference](../cli/) for all commands and profile launches.
## Use Docker
From a source checkout:
```sh
docker compose up -d --build
```
Open <http://127.0.0.1:3458>. Docker publishes one Nginx endpoint shared by management and the gateway. Add a provider/model, create a CCR client key, and start the gateway under Server. See [Docker Deployment](../docker/) for ports, authentication, persistence, backups, and remote access.
## Verify The Installation
After configuring a provider, model, and CCR client key:
1. Confirm Server shows Running.
2. Request `/health` on the deployment's gateway address and expect a `200` running response.
3. Send one minimal model request to a compatible endpoint using the CCR client key.
4. Confirm requested/resolved model, provider, status, and latency under Logs.
A reachable management UI does not prove that the model gateway is usable. Docker `/health` returning `502` is expected before a provider/model has been configured.
## Data Locations
| Distribution | Configuration location |
| --- | --- |
| Desktop / CLI on macOS or Linux | `~/.claude-code-router` |
| Desktop / CLI on Windows | `%APPDATA%\claude-code-router` |
| Docker | `/data/.claude-code-router`; persist `/data` |
Current configuration is stored in `config.sqlite`. Legacy `config.json` is only a migration source when SQLite does not exist, or an initial Docker bootstrap. Do not edit live SQLite files.
If you want the service to start when the app opens, enable **Auto start** on the Server page.

View file

@ -5,6 +5,19 @@ eyebrow: Product Documentation
lead: Learn what CCR is for, where its boundaries are, and how the docs are organized. Start with Quick Start when you want to configure it; use Detailed Configuration for fields, Bots, or Fusion.
---
## What CCR Can Do
**Claude Code Router (CCR) is a local model gateway.** It sits between agents such as Claude Code, Codex, and ZCode and upstream model services, then centralizes model management, API keys, routing rules, logs, observability, and Bot relay.
CCR is useful when:
- you do not want to maintain the same models and keys separately in every agent.
- you want different tasks to automatically use different models: fast models for lightweight background work, stronger models for complex tasks, and Fusion for image or web-search work.
- you need request logs showing which provider and model handled a request, whether it succeeded, how long it took, and roughly how much it cost.
- you want to forward long-running agent messages to Slack, Telegram, Feishu, WeCom, or other IM platforms.
CCR listens on the local default address `http://localhost:8080`. Once an agent points to this address, CCR can take over the request and forward it to upstream providers according to routing rules.
## Documentation Structure
The top navigation is split into four standalone pages:
@ -12,8 +25,8 @@ The top navigation is split into four standalone pages:
| Page | Contents |
| --- | --- |
| [Documentation](./) | Product positioning, architecture overview, and reading path |
| [Quick Start](guides/) | Desktop, CLI, and Docker installation plus provider and Agent setup |
| [Detailed Configuration](configuration/overview/) | Overview dashboard, API keys, server, providers, routing, Agent Config, Fusion, Bots, tray, and config database location |
| [Quick Start](guides/) | From installation and provider setup to connecting an agent |
| [Detailed Configuration](configuration/) | Providers, routing, Agent Config, Fusion, Bots, and config file location |
| [Q&A](troubleshooting/) | Request logs, observability panel, and common questions |
Bot platform guides are child pages under Detailed Configuration. Each platform has its own page so platform dashboard fields, callback URLs, signatures, and FAQs can be expanded independently.
@ -22,8 +35,7 @@ Bot platform guides are child pages under Detailed Configuration. Each platform
If this is your first time using CCR:
1. Choose desktop, npm CLI, or Docker on the [installation page](guides/install/), then use the dedicated [CLI](guides/cli/) or [Docker](guides/docker/) guide.
2. Continue through [Quick Start](guides/) to connect a provider and Agent Config.
3. Use request logs to confirm whether requests are passing through CCR.
4. Open [Detailed Configuration](configuration/overview/) for the overview dashboard, API keys, server, providers, vision, web search, MCP tools, tray, and IM relay.
5. Use [Q&A](troubleshooting/) for 401, 404, timeout, wrong-routing, or Bot delivery questions.
1. Start with [Quick Start](guides/) to connect a provider and Agent Config.
2. Use the app's request logs to confirm whether requests are passing through CCR.
3. Open [Detailed Configuration](configuration/) for vision, web search, MCP tools, and IM relay.
4. Use [Q&A](troubleshooting/) for 401, 404, timeout, wrong-routing, or Bot delivery questions.

View file

@ -13,7 +13,7 @@ LINE 适合把 Agent 消息接入已有的 LINE 好友、群聊或 LINE Official
## 你会用到哪些字段
CCR 里 LINE 的认证方式叫 **Bot Token**这里需要填写下面这两个 channel 字段:
CCR 里 LINE 的认证方式叫 **Bot Token**但填的不是 Telegram 那种 token而是下面这两个 channel 字段:
| LINE 后台里的名字 | CCR 字段 | 是否必填 | 说明 |
| --- | --- | --- | --- |

View file

@ -2,37 +2,25 @@
title: Claude Code Router 详细配置
pageTitle: 详细配置
eyebrow: 详细配置
lead: 按应用中的实际顺序将主页页面和设置页分开说明主页覆盖概览、供应商、Agent配置、路由、Fusion、API 密钥、日志&观测、服务和扩展;设置页覆盖 ToolHub、Bot、数据和托盘
lead: 深入配置供应商、路由、Agent配置、Fusion、Bot 和配置文件位置。这里是按功能查字段和扩展能力的地方
---
## 页面结构
详细配置文档已经拆成独立页面。左侧目录中的每一项都会进入一个页面;当前页面内的标题由右侧大纲负责。主页页面跟随应用左侧主导航顺序;设置页单独分组,并按设置弹窗顺序排列。
## 主页页面
详细配置文档已经拆成独立页面。左侧目录中的每一项都会进入一个页面;当前页面内的标题由右侧大纲负责。
| 页面 | 内容 |
| --- | --- |
| 概览仪表盘 | 系统状态、账户余额、用量组件、布局编辑和分享卡片 |
| 供应商配置 | 上游服务、协议、基础 URL、模型列表和凭据 |
| 一键导入供应商 | Provider deeplink 协议、Manifest 导入、一键导入按钮和安全边界 |
| Agent配置 | Agent 启动方式、模型、作用范围、多开和 Bot 绑定 |
| 路由配置 | 条件规则、fallback 和请求改写 |
| Fusion 组合模型 | 把基础模型与视觉、搜索、MCP 工具组合成新的可选模型 |
| API 密钥 | 客户端访问 Key、过期时间和本地限额 |
| 路由配置 | 默认路由、条件规则、fallback 和请求改写 |
| 日志&观测 | 请求日志、Agent 执行追踪、工具调用和工具结果 |
| 服务配置 | Host、Port、代理模式、系统代理、网络捕获和 CA 证书 |
| Fusion 组合模型 | 把基础模型与视觉、搜索、MCP 工具组合成新的可选模型 |
| Agent配置 | Agent 启动方式、模型、作用范围、多开和 Bot 绑定 |
| 扩展机制 | Wrapper plugin、Core gateway plugin、自定义扩展创建和调试 |
## 设置页
| 页面 | 内容 |
| --- | --- |
| ToolHub | 将多个 MCP server 收束成一个 Agent 可用的动态工具检索入口 |
| Bot 与 IM 接力 Agent | Bot 转发、接力模式和平台页面 |
| 配置数据库位置 | 桌面 App 维护的 SQLite 配置数据库位置 |
| 托盘配置 | 托盘图标、余额进度条和托盘窗口组件 |
| 配置文件位置 | 桌面 App 维护的配置文件位置 |
## 内容关系
概览仪表盘用于查看系统状态和用量;供应商配置和一键导入供应商页面覆盖上游模型服务如何进入 CCRAgent配置页面覆盖 Claude Code、Codex 和 ZCode 的启动、多开与模型选择;路由决定模型请求的上游去向;Fusion 页面覆盖图像、搜索和 MCP 工具;API 密钥控制客户端访问 CCR日志&观测覆盖请求日志和 Agent 执行链路服务配置控制本地网关监听和代理能力扩展机制覆盖本地插件的创建、安装和调试。ToolHub、Bot、配置数据库位置和托盘配置对应设置弹窗中的同名配置页
供应商配置和一键导入供应商页面覆盖上游模型服务如何进入 CCR路由决定模型请求的上游去向Agent配置页面覆盖 Claude Code、Codex 和 ZCode 的启动、多开与模型选择Fusion 页面覆盖图像、搜索和 MCP 工具扩展机制页面覆盖本地插件的创建、安装和调试Bot 页面覆盖 IM 平台接力。

View file

@ -1,46 +0,0 @@
---
title: API 密钥
pageTitle: API 密钥
eyebrow: 详细配置
lead: 管理客户端访问 CCR 网关时使用的 API Key并为每个 Key 设置过期时间和本地限额。
---
## 列表字段
| 字段 | 代表的能力 |
| --- | --- |
| 搜索 API 密钥 | 按名称或 Key 内容过滤列表。 |
| 添加 API 密钥 | 打开创建弹窗,生成新的客户端访问 Key。 |
| 名称 | API Key 的显示名,用于区分客户端、团队、用途或自动化来源。 |
| Key | 脱敏后的访问 Key。可以点击 `复制 API 密钥` 复制完整 Key。 |
| 过期 | 当前 Key 的过期时间。过期后客户端不能继续使用这个 Key 访问 CCR。 |
| 限制 | 当前 Key 的本地限额摘要。没有配置限额时显示“未配置限制”。 |
| 编辑 API 密钥 | 修改过期时间和限额。出于安全原因,已创建的 Key 本身不会重新明文展示。 |
| 移除 API 密钥 | 删除当前客户端访问 Key。删除后立即不能再用于请求。 |
## 创建和编辑
| 字段 | 代表的能力 |
| --- | --- |
| 名称 | 新 Key 的显示名。建议写成客户端或用途,例如 `Claude Code - laptop``CI` 或团队名。 |
| 过期时间 | 选择 Key 的有效期:`永不``7 天``30 天``90 天``自定义`。 |
| 过期于 | 选择 `自定义` 时出现,用于填写精确的过期日期和时间。 |
| API 密钥已创建 | 创建成功后的确认弹窗。这里会显示完整 Key。 |
| 请现在复制保存这个密钥,之后可能不会再次完整显示。 | 提醒你立即复制 Key。关闭弹窗后CCR 不会再次展示完整 Key。 |
## 高级设置
`高级设置` 用于给单个客户端 Key 添加本地限额。限额命中后,使用这个 Key 的客户端请求会被拒绝或限制;它不会修改供应商侧额度。
| 字段 | 代表的能力 |
| --- | --- |
| 高级设置 | 展开或收起限额编辑区。 |
| 未配置限制 | 当前 Key 没有任何本地限额。 |
| 请求 | 按请求次数限制。 |
| 令牌 | 按 token 数量限制。 |
| 图片 | 按图片数量限制。 |
| 每分钟 | 限额窗口为 1 分钟。 |
| 每小时 | 限额窗口为 1 小时。 |
| 每天 | 限额窗口为 1 天。 |
| 添加限制 | 新增一条限额规则。 |
| 移除限制 | 删除当前限额规则。 |

View file

@ -5,61 +5,15 @@ eyebrow: 详细配置
lead: 通过 IM Bot 转发 Agent 消息,或在桌面空闲后把任务接力到手机。
---
CCR App Relay 的在线周期与由 CCR 打开的 Agent App 保持一致。打开 Claude、Codex、ZCode 或 OpenCode App 时CCR 启动对应的伴生 workerApp 退出时 worker 和 Bot 连接同步停止。
## 能做什么
Bot 能把 Agent 消息转发到 IM 平台,也可以在桌面空闲后把任务接力到手机上继续看。
## 常见模式
- **转发 Agent 消息**:把消息同步到 IM。
- **接力**:桌面空闲后,把交互接力到 IM。
- **仅回复**:关闭“转发 Agent 消息”和接力后,只回复从 IM 主动发起的 turn。
同一个 IM conversation 中的普通消息按顺序执行。`/project``/session status``/session cancel` 等管理命令保持即时响应;排队、取消、超时和 worker 重启恢复都有明确状态。
## Project 与 Session
Project 对应 Agent 的项目或工作目录Session 对应该 Project 下的 Agent 原生会话。
### Project 命令
| 命令 | 作用 |
| --- | --- |
| `/project` | 查看 Project 命令帮助和 App 在线边界。 |
| `/project list [page]` | 分页列出 Agent 已知项目。 |
| `/project find <text>` | 搜索项目名称或路径。 |
| `/project current` | 查看当前 Project。 |
| `/project use <n>` | 切换 Project并清除原 Session 选择。 |
| `/project name <label>` | 设置当前 Project 的 Bot 显示名称。 |
### Session 命令
| 命令 | 作用 |
| --- | --- |
| `/session` | 查看全部 Session 命令。 |
| `/session list [page]``/session find <text>` | 只浏览当前 Project 中的 Sessions。 |
| `/session current``new [title]``use <n>``reset` | 查看、新建、继续或清除 Session 选择。 |
| `/session status``cancel` | 查看当前 turn/队列,或取消当前 turn 并清空队列。 |
| `/session approve [session]``deny``answer <text>` | 响应 Agent 已经产生的权限或输入请求;所有平台提供文本命令,具备卡片能力的平台同时显示操作按钮。 |
| `/session name <label>` | 重命名当前 Session。 |
| `/session archive <n>``restore <n>``delete <n> confirm` | 归档、恢复或确认永久删除。 |
| `/session history [count]``usage` | 查看最近历史和 Token/缓存/成本摘要。 |
| `/session models``model``effort``mode` | 查看或调整当前 conversation 的 Session 运行设置。 |
| `/session memory ...``skills``skill``shortcut ...` | 管理持久上下文、Agent Skills 和快捷指令。 |
| `/session doctor``deliveries` | 查看连接、outbox、最近投递和脱敏错误。 |
Bot 的公开命令域为 `/project``/session`。其他 slash command 统一返回未知命令;普通自然语言(包括 `help``list`)作为 prompt 进入 Agent。
## Bot 设置
- **Bot 语言**自动、English 或简体中文。
- **最长 turn 时间**:超时后中断 Agent turn 并回报最终状态。
- **Session 空闲重置**:超过指定时间后在当前 Project 中准备新 Session设为 `0` 关闭。
- **消息分片与附件上限**:适配平台消息长度并限制入站文件大小。
- **流式回复与进度**:仅发送可见文本和工具阶段。
- **收发附件**:允许入站图片/文件和当前工作区产物回传。
- **允许 Agent 使用 Shell 工具**:控制 Agent 的 Shell 工具权限Bot 命令域保持为 `/project``/session`
状态文件持久保存有界的去重记录、待处理 turn、outbox 和最近投递结果。事件幂等保证每个 Agent turn 执行一次;再次打开 App 后,在 App 在线期间恢复待投递消息。
## 平台页面
Slack、Discord、Telegram、LINE、微信、企业微信、飞书和钉钉都有独立页面iMessage 使用本机接入。SDK 按平台能力选择 Markdown、卡片、流式更新、文件消息或文本消息
Slack、Discord、Telegram、LINE、微信、企业微信、飞书和钉钉都有独立页面。

View file

@ -11,9 +11,8 @@ lead: 添加 Bot、绑定 Agent配置并选择消息转发或接力模式。
2. 选择平台并填写 Token、Secret、Signing Secret、Robot Code 或 OAuth 信息。
3. 保存 Bot。
4. 打开目标 Agent配置开启 **Bot**
5. 按需设置转发、接力、语言、超时、附件、流式回复和 **允许 Agent 使用 Shell 工具**
6. 从 CCR 重新打开 Claude、Codex、ZCode 或 OpenCode App。Bot 只在 App 存活期间在线。
5. 选择 **转发 Agent 消息****接力**,并重新从 CCR 打开 Agent。
## 验证方式
从 CCR 打开 Agent App 后,在 IM 发送 `/project current``/session list` 和一条普通消息。Profile 卡片会显示 Bot 连接、最后事件、最后投递、待投递数量和脱敏错误;也可发送 `/session doctor` 查看诊断。关闭 App 后Bot 状态应切换为离线
从 CCR 打开 Agent 后发一条测试消息,确认请求日志中有 Bot 相关记录IM 端也能收到消息

View file

@ -1,19 +1,16 @@
---
title: 配置数据库位置
pageTitle: 配置数据库位置
title: 配置文件位置
pageTitle: 配置文件位置
eyebrow: 详细配置
lead: 找到 CCR 桌面 App 默认维护的 SQLite 配置数据库
lead: 找到 CCR 桌面 App 默认维护的配置文件
---
## 默认位置
- macOS/Linux`~/.claude-code-router/config.sqlite`
- Windows`%APPDATA%\claude-code-router\config.sqlite`
Docker 设置 `HOME=/data`,因此配置数据库位于 `/data/.claude-code-router/config.sqlite`;需要持久化挂载整个 `/data`,而不是只挂载单个数据库文件。
- macOS`~/Library/Application Support/claude-code-router/config.json`
- Windows`%APPDATA%/claude-code-router/config.json`
- Linux`~/.config/claude-code-router/config.json`
## 生效方式
CCR 的运行配置存储在 SQLite 中。旧版 `config.json` 只会在没有 SQLite 配置时作为迁移来源读取一次,迁移完成后继续编辑 `config.json` 不会影响当前配置。
建议通过桌面 UI 修改配置,或在 **Settings** 中导出备份。不要在 CCR 运行时直接编辑 `config.sqlite`SQLite 还会维护同目录的 `config.sqlite-wal``config.sqlite-shm` 辅助文件。
直接编辑配置文件后,需要重启 CCR。

View file

@ -5,7 +5,7 @@ eyebrow: 详细配置
lead: 了解 CCR 扩展如何加载、能注册哪些能力,并从零创建、安装和调试自己的扩展。
---
## 扩展类型
## 扩展是什么
CCR 的扩展分为两层:
@ -177,7 +177,7 @@ module.exports = {
4. 保存配置。
5. 打开 **Server** 页面,重启网关。
CCR 的运行配置存储在 SQLite 中。请通过 UI 添加扩展;旧版 JSON 配置文件仅用于参考。扩展条目的配置结构如下
也可以直接编辑配置文件
```json
{
@ -194,7 +194,7 @@ CCR 的运行配置存储在 SQLite 中。请通过 UI 添加扩展;旧版 JSO
}
```
保存扩展配置后需要重启网关。配置数据库位置见 [配置数据库位置](/configuration/config-file/)。
直接编辑配置文件后需要重启 CCR。配置文件位置见 [配置文件位置](/configuration/config-file/)。
本地目录选择器会按顺序识别这些入口信息:

View file

@ -11,20 +11,7 @@ lead: 使用 CCR 内置 Web Search 工具为模型提供联网检索能力。
## 搜索服务
支持 In-app Browser、Brave、Bing、Google CSE、Serper、SerpAPI、Tavily、Exa 等搜索服务。
## In-app Browser
`In-app Browser` 会通过 CCR Desktop 的隐藏内置浏览器窗口执行搜索,打开搜索结果页面并提取可见内容,再把证据提供给 Fusion 模型。它不需要外部搜索 API Key适合希望用桌面端内置浏览器完成联网检索的场景。
可配置项包括搜索引擎、语言、地区和安全搜索级别:
- 搜索引擎Bing、Google、DuckDuckGo。
- 语言:例如 `en``zh-CN`
- 地区:例如 `US``CN`
- 安全搜索:默认、中等、严格或关闭。
> 注意:`In-app Browser` 依赖 CCR Desktop 的 Electron 内置浏览器能力只在桌面端可用。CLI、服务器部署或纯 Web 环境没有内置浏览器集成,请改用 Brave、Bing、Google CSE、Serper、SerpAPI、Tavily 或 Exa 等搜索服务。
支持 Brave、Bing、Google CSE、Serper、SerpAPI、Tavily、Exa 等搜索服务。
## 排查要点

View file

@ -9,7 +9,7 @@ lead: 把基础模型和能力模型组合成新的可选模型,让你已经
Fusion 的价值在于保留基础模型的手感同时补齐它缺少的能力。基础模型继续负责推理、写作和代码生成CCR 在需要时调用图像、搜索或 MCP 工具,把结果整理进上下文,再交给基础模型完成回答。
保存后的 Fusion 模型会像普通模型一样出现在路由和配置中。保存结果是一个可复用的新模型:把强文本模型升级成视觉模型,把稳定代码模型升级成可联网模型,也可以把 Agent 模型接入团队内部工具。
保存后的 Fusion 模型会像普通模型一样出现在路由和配置中。它不是额外的一次手动流程,而是一个可复用的新模型:把强文本模型升级成视觉模型,把稳定代码模型升级成可联网模型,也可以把 Agent 模型接入团队内部工具。
## 适用能力

View file

@ -1,131 +0,0 @@
---
title: 概览仪表盘
pageTitle: 概览仪表盘
eyebrow: 详细配置
lead: 自定义 CCR 首页仪表盘,查看系统状态、账户余额、请求量、令牌、成本和模型分布。
---
## 适用场景
| 场景 | 看什么 |
| --- | --- |
| 检查网关是否正常 | 系统状态、成功率、错误数、平均延迟 |
| 估算今日或近期消耗 | 请求数、输入 / 输出 / 缓存令牌、估算成本 |
| 比较上游使用情况 | 供应商分析、模型分布、客户端分析 |
| 关注账户额度 | 账户余额、套餐额度、剩余额度、账户状态 |
| 汇报或分享用量 | AI 用量年报、CCR 路由图、模型排行榜、消费小票等分享卡片 |
## 时间范围
顶部的 `按时间查看用量` 控制所有依赖用量统计的组件。切换后,请求、令牌、成本、趋势、分布和分享卡片会按新的窗口重新聚合。
| 选项 | 统计窗口 |
| --- | --- |
| `今天` | 当前本地日期从 00:00 到现在,按小时分桶。 |
| `24 小时` | 最近 24 小时,按小时分桶。 |
| `7 天` | 最近 7 天,按天分桶。 |
| `30 天` | 最近 30 天,按天分桶。 |
账户余额组件不按这个时间窗口重算,它展示供应商账户连接器最近一次获取到的快照。
## 编辑布局
点击右上角铅笔按钮进入编辑模式。编辑模式分为三栏:
| 区域 | 作用 |
| --- | --- |
| 组件 | 左侧组件库,点击任一组件即可添加到当前仪表盘。 |
| 预览 | 中间布局预览。可以拖拽组件排序,也可以点击组件选中它。 |
| 组件属性 | 右侧属性面板,用于修改类型、数据、尺寸、样式或移除组件。 |
常用操作:
1. 添加组件:在左侧 `组件` 中点击组件模板。
2. 调整顺序:在 `预览` 中拖拽组件。
3. 调整尺寸:选中组件后拖动右侧、底部或右下角的缩放手柄。
4. 修改数据:在 `组件属性` 里切换 `组件类型``数据`
5. 修改展示:在 `组件大小``样式` 中选择合适的布局。
6. 保存结果:点击 `完成` 退出编辑模式;布局会保存在应用配置中。
7. 恢复默认:编辑模式下点击 `重置布局`
移除组件只会从概览布局中删除该卡片,不会删除请求日志、供应商、账户连接器或任何上游配置。如果所有组件都被移除,页面会显示 `未配置组件`
## 组件目录
尺寸使用 `宽:高` 表示,宽高范围都是 `1``4`。概览网格在桌面端最多 4 列,在窄屏上会自动折叠。
| 组件 | 可选数据 | 默认尺寸 | 默认样式 | 可选样式 |
| --- | --- | --- | --- | --- |
| 状态组件 | 系统状态 | `4:1` | 时间线 | 时间线、紧凑 |
| 账户组件 | 所有账户或指定账户 | `4:2` | 卡片 | 卡片、紧凑、横条、圆环、半圆、弧形、内外圆 |
| 指标组件 | 请求、Token、成本 | `1:1` | 卡片 | 卡片、紧凑、柱状图、圆环 |
| 趋势组件 | 按时间查看用量 | `3:2` | 组合图 | 组合图、面积图、折线图、柱状图 |
| 活跃度组件 | Token 活跃度 | `4:2` | 热力图 | 热力图 |
| 构成组件 | Token 分布 / 模型分布 | Token 分布:`1:2`;模型分布:`2:2` | Token 分布:横条;模型分布:饼图 | 横条、堆叠、环形图、饼图 |
| 分析组件 | 客户端分析 / 供应商分析 | `2:2` | 表格 | 表格、紧凑 |
| 分享卡片 | AI 用量年报、CCR 路由图、模型排行榜、AI 燃料仪表、Token 日历海报、消费小票 | `1:4` | 卡片 | 卡片 |
尺寸约束:
| 规则 | 原因 |
| --- | --- |
| 分享卡片最小高度是 `1:4`。 | 导出图片使用竖版海报比例,需要足够高度。 |
| 账户组件在展示所有账户且使用“紧凑”样式时,最小尺寸是 `2:2`。 | 多账户列表需要保留可读空间。 |
| 旧版尺寸别名仍可被解析:`small` -> `1:1``medium` / `large` -> `2:2``wide` -> `3:2``full` -> `4:1``4:2`。 | 用于兼容旧配置。 |
## 指标数据
`metric` 组件通过 `metric` 字段选择要展示的指标。
| `metric` | 含义 |
| --- | --- |
| `requests` | 请求数 |
| `total-tokens` | 总令牌 |
| `input-tokens` | 输入令牌 |
| `output-tokens` | 输出令牌 |
| `cache-tokens` | 缓存令牌 |
| `cache-ratio` | 缓存率 |
| `estimated-cost` | 估算成本,按模型价格信息计算 |
| `success-rate` | 成功率 |
| `errors` | 错误数 |
| `avg-latency` | 平均延迟 |
## 账户组件
账户组件读取供应商配置里的账户 / 用量连接器。要让它显示余额或剩余额度,需要先在供应商配置中启用并测试 `获取用量`
| 数据选择 | 行为 |
| --- | --- |
| `所有账户` | 展示所有可用账户快照。 |
| 指定账户 | 只展示某个供应商或某个凭据的账户快照。内部配置值格式通常是 `provider``provider::credentialId`。 |
如果账户组件为空,优先检查:
1. 供应商是否配置了账户 / 用量连接器。
2. `获取用量` 测试是否成功。
3. API Key 或账户接口是否仍有效。
4. 当前选择的指定账户是否已经被删除或重命名。
## 分享卡片
分享卡片组件可以通过右上角下载按钮导出 PNG。桌面 App 会优先使用原生导出能力,浏览器环境会退回到前端 canvas 导出。导出图片尺寸为 `1080 x 1350`
| 卡片 | `type` | 内容 |
| --- | --- | --- |
| AI 用量年报 | `share-usage-wrapped` | 总令牌、请求数、估算成本、缓存率、最长连续、最高频模型、最高频供应商、峰值日期。 |
| CCR 路由图 | `share-route-map` | 客户端到供应商 / 模型的主要路由关系,以及客户端、供应商、模型数量。 |
| 模型排行榜 | `share-model-leaderboard` | 按令牌排序的模型排行榜。 |
| AI 燃料仪表 | `share-fuel-cockpit` | 最多 3 个账户额度仪表,依赖账户 / 用量连接器。 |
| Token 日历海报 | `share-token-calendar` | 类似贡献日历的 Token 活跃度海报。 |
| 消费小票 | `share-spend-receipt` | 当前时间范围内的估算成本、请求、令牌、延迟和成功率小票。 |
## 数据来源与排障
| 现象 | 可能原因 | 处理方式 |
| --- | --- | --- |
| 请求、令牌或成本为 0 | 当前时间范围内没有经过 CCR 的请求,或用量捕获尚未记录。 | 切换到 `24h` / `7d`,确认客户端请求确实走 CCR。 |
| 成本显示为 `$0.00` | 模型没有价格信息,或用量过小。 | 检查模型目录和供应商模型名是否能匹配价格;低于 0.01 美元会显示更多小数。 |
| 成功率、错误数不符合预期 | 只统计 CCR 捕获到的请求结果。 | 对照日志页面中的请求记录。 |
| 账户余额为空 | 没有账户连接器,或 `获取用量` 失败。 | 到供应商配置中测试账户 / 用量字段映射。 |
| 分布图没有数据 | 请求日志里缺少模型、供应商或 token 信息。 | 确认请求经过 CCR并检查上游响应是否返回 token usage。 |
| PNG 导出失败 | 浏览器不支持 canvas 导出、元素尺寸为空,或用户取消了保存。 | 在桌面 App 中重试,确保卡片可见且没有被缩到过小。 |

View file

@ -2,19 +2,14 @@
title: Agent配置
pageTitle: Agent配置
eyebrow: 详细配置
lead: 为 Claude Code、Codex、Grok CLI、ZCode 创建可复用的启动配置,并通过不同配置打开不同的 Agent 实例。
lead: 为 Claude Code、Codex、ZCode 创建可复用的启动配置,并通过不同配置打开不同的 Agent 实例。
---
## 配置流程
## Agent配置是什么
1. 先在 **供应商配置** 中添加至少一个可用供应商和模型,或先创建需要使用的 Fusion 模型。
2. 打开 **Agent配置**,点击 **添加配置**
3. 选择 Agent 类型,填写配置名称,并选择作用范围和入口模式。
4. 选择模型。模型值通常是 `供应商名称/模型名称`,也可以选择 Fusion 模型。
5. 如果入口模式包含 App可以绑定 Bot并选择是否转发 Agent 消息或开启接力。
6. 保存后,从 Agent配置卡片打开终端图标会复制 CLI 命令,播放图标会启动 App 实例。
Agent配置是桌面 App 中管理 Claude Code、Codex、ZCode 启动入口的能力。它不是供应商或路由规则,而是一次 Agent 启动所需的完整入口Agent 类型、打开方式、模型、作用范围、配置文件位置,以及可选的 Bot 绑定。
试用阶段建议选择 **仅从 CCR 打开时生效**,并且总是从 CCR 打开 Agent。这样配置只影响 CCR 启动的实例,不会改掉你系统里原本直接打开的 Claude Code、Codex、Grok CLI 或 ZCode
因此详细配置里会有这个页面。它用于解释“用哪个配置打开哪个 Agent 实例”,而不是继续拆分供应商、路由或 Fusion 的字段。
## 多开机制
@ -23,140 +18,41 @@ lead: 为 Claude Code、Codex、Grok CLI、ZCode 创建可复用的启动配置
| 机制 | 实际行为 |
| --- | --- |
| 独立配置文件 | 选择“仅从 CCR 打开时生效”时Claude Code 和 Codex 会写入 CCR 管理的独立配置目录,路径按配置 `id` 区分 |
| 独立启动器 | Claude Code 和 Grok CLI 使用独立启动包装器Codex 和 ZCode 使用独立中间层启动器,文件名同样按配置 `id` 或名称区分 |
| 独立 App 数据目录 | 从 App 打开时Claude App、ChatGPTCodex 桌面端的新名称)、ZCode App 都会使用按配置 `id` 区分的用户数据目录 |
| 独立启动器 | Claude Code 使用独立启动包装器Codex 和 ZCode 使用独立中间层启动器,文件名同样按配置 `id` 或名称区分 |
| 独立 App 数据目录 | 从 App 打开时Claude App、Codex App、ZCode App 都会使用按配置 `id` 区分的用户数据目录 |
| 运行状态 | CCR 按打开入口和配置 `id` 记录运行中的 App 实例;同一个配置再次打开会激活已有窗口,不同配置可以打开不同实例 |
这意味着你可以为同一个 Agent 建多个配置例如“Claude Code - 工作项目”“Claude Code - 测试模型”“Codex - Fusion 图像能力”。它们可以选择不同模型、不同作用范围和不同 Bot打开后就是不同的 Agent 实例。
## 常用选项
| 选项 | 适用范围 | 说明 |
| --- | --- | --- |
| Agent | 全部 | 选择 Claude Code、Codex、OpenCode、Grok CLI 或 ZCode。Grok CLI 只支持 CLIZCode 只支持 App。 |
| 配置名称 | 全部 | 用于在 CCR 中识别配置,也会作为 `ccr-app <配置名称>` 的打开目标。名称可以有空格,复制命令时 CCR 会自动加引号。 |
| 启用开关 | 全部 | 关闭后该配置不会出现在打开入口中,也不会被应用为有效启动配置。 |
| 作用范围 | 全部 | **仅从 CCR 打开时生效** 会使用 CCR 管理的独立配置;**系统默认** 会写入对应 Agent 的默认配置。同一个 Agent 同时只能有一个启用的系统默认配置。 |
| 入口模式 | Claude Code、Codex、OpenCode、Grok CLI | `CLI & APP` 同时显示 CLI 和 App 打开入口;`CLI only` 只生成 CLI 命令;`App only` 只显示 App 打开入口。Grok CLI 固定为 `CLI only`。 |
| 模型 | 全部 | 该 Agent 打开后的默认模型,可以选择普通供应商模型或 Fusion 模型。Claude Code 留空表示保留 Claude Code 默认模型。 |
| Bot | App 入口 | 只有从 CCR 打开的 App 模式会转发 Bot 消息。CLI 当前不转发 Bot 消息。 |
| 环境变量 | 全部 | 为该配置注入额外环境变量。Claude Code 默认带 `CLAUDE_CODE_ENABLE_GATEWAY_MODEL_DISCOVERY=1`,用于启用网关模型发现。 |
## 各 Agent 的配置项
### Claude Code
| 配置项 | 作用 |
| 选项 | 说明 |
| --- | --- |
| 模型覆盖 | 写入 Claude Code 使用的 `ANTHROPIC_MODEL`。留空时不覆盖 Claude Code 自己的默认模型。 |
| 小模型 | 写入 `ANTHROPIC_SMALL_FAST_MODEL`,供 Claude Code 的轻量任务使用。留空时保留 Claude Code 默认值。 |
| 设置文件 | 系统默认模式使用 Claude Code 默认设置文件;仅从 CCR 打开时生效会在 CCR 配置目录下按 Agent配置 `id` 生成独立设置文件。 |
| 环境变量 | 会合并到 Claude Code 设置文件的 `env` 中。CCR 同时写入网关地址、API Key helper 和启动包装器。 |
| Bot | 只在 Claude App 入口生效,可选择已保存 Bot并配置转发 Agent 消息或接力。 |
Claude Code CLI 从 CCR 打开后,会通过 CCR 网关获取模型发现信息。进入 Claude Code CLI 后可以输入 `/model` 查看并切换 CCR 暴露的模型列表,包括普通供应商模型和可见的 Fusion 模型。
Claude App 是 **零配置zero-config**:从 CCR 桌面 App 打开 Claude App 时CCR 会自动写入 Claude App 网关配置、API Key、模型发现列表和独立用户数据目录。用户不需要增加额外操作直接使用 CCR 打开就会自动完成所有必要配置;如果 Claude App 已经打开,按提示重启或从 CCR 重新打开即可。
Claude App 和 Claude Code CLI 的模型列表适配方式不同:
| 入口 | 模型列表来源 | 说明 |
| --- | --- | --- |
| Claude Code CLI | CCR 网关模型发现 | CLI 内使用 `/model` 查看列表;选择后请求仍走 CCR 的供应商、路由和 Fusion。 |
| Claude App | CCR 生成的 Claude App inference models | Claude App 需要 Claude 兼容的模型名。CCR 会把 `供应商/模型` 和 Fusion 模型映射成 Claude App 可识别的模型项,并用显示名称保留真实模型含义。 |
### OpenCode
| 配置项 | 作用 |
| --- | --- |
| Provider ID | 写入 OpenCode 的供应商引用,默认是 `claude-code-router`。 |
| Provider name | OpenCode 中展示的供应商名称,默认是 `Claude Code Router`。 |
| OpenCode model | OpenCode CLI 和 App 的默认模型,可以选择普通供应商模型或 Fusion 模型。 |
| 配置文件 | 系统默认模式使用 OpenCode 默认配置;仅从 CCR 打开时生效会在 CCR 配置目录下写入配置专属文件。 |
| 环境变量 | 注入 OpenCode CLI、OpenCode App 以及对应的 Bot worker。 |
| Bot | 在从 CCR 打开的 OpenCode App 入口生效。收到 Bot 消息后会通过 OpenCode CLI 执行,并把回复发回同一个 Bot 会话。 |
CCR 会为 OpenCode App 启动一个配套 Bot worker并为每个 Bot conversation 分别保存 Project 与可选 Session。先用 `/project list|current|use` 选择 Agent Project再用 `/session list|current|new|use|reset` 管理该 Project 下的 Agent Session。切换 Project 会清除原 Session其他 Project 的 Session 无法被选中。Bot 只拦截这两个 slash 命令域;已移除的 `/task` 和旧平铺命令不再兼容。
CCR Desktop 进程环境中必须能够执行 `opencode`。如果 CLI 安装在其他位置,可以在 Agent配置环境变量中设置 `CCR_OPENCODE_BIN`。Bot session 默认使用全新 OpenCode Desktop 工作区对应的文件系统根目录;如果 App 当前打开了其他项目,应通过 `CCR_OPENCODE_BOT_CWD` 设置同一个项目目录。CCR 会把该目录显式传给 `opencode run --dir`,使新 session 出现在 App 对应的项目下。默认不会自动批准权限;只有在可信环境中才应设置 `CCR_OPENCODE_BOT_AUTO_APPROVE=true`,因为它会启用 OpenCode 的高风险 `--auto` 模式。
### Codex
| 配置项 | 作用 |
| --- | --- |
| Provider ID | 写入 Codex 的 `model_provider`,默认是 `claude-code-router`。建议保持稳定,只使用字母、数字、点、下划线或短横线。 |
| Provider name | Codex 中展示的供应商名称,默认是 `Claude Code Router`。 |
| Codex model | 写入 Codex 默认模型。可以选择普通供应商模型或 Fusion 模型;留空时 CCR 使用可用模型中的默认值。 |
| Show all sessions | 让 Codex 显示所有会话。ZCode 不提供该项。 |
| 配置文件 | 默认是 `~/.codex/config.toml`。仅从 CCR 打开时生效会写入 CCR 管理的独立配置目录。 |
| 环境变量 | 注入 Codex CLI 或 ChatGPT。Claude Code 专用的模型发现变量不会传给 Codex。 |
| Bot | 只在 ChatGPT App 入口生效。 |
保存后Codex CLI 使用配置卡片里的终端图标复制命令,例如 `ccr-app "Codex - Work"`。ChatGPT 使用播放图标打开。CCR 按照 CodexL 的启动方式,直接运行 ChatGPT App bundle 内的 Electron 可执行文件,为它设置隔离的用户数据目录,并把 `CODEX_CLI_PATH` 指向 CCR 中间层。中间层把 app-server 流量转发给 ChatGPT 内置的 Codex CLI只适配账号展示隔离目录已有有效 ChatGPT token 时显示真实账号;没有凭据时使用无 token、ChatGPT 形态的虚拟工作区身份,让桌面端在不保存真实用户登录的情况下仍可使用模型选择。为让原生 app-server 选择官方 API marketplaceCCR 只在进程启动阶段创建精确的 `ccr-local-profile` 引导标记,收到第一条原生响应后立即删除;正常启动后或异常退出时也会清理,不会把它保留成登录状态。其他认证文件全部保留。旧版 `Codex.app` 仍然兼容。
模型和公共插件列表不再由中间层合成。原生 Codex app-server 读取生成的 `model_catalog_json`,并原样处理 `model/list` 与公共 `plugin/list` 请求,因此 Codex 可以自行联网刷新官方公开 [`openai/plugins`](https://github.com/openai/plugins) Git marketplace。虚拟 workspace 中,只有必须使用真实 ChatGPT 鉴权的账号私有 marketplace 请求会得到明确空结果,绝不会用本地插件替代。下载后的 Git checkout 只作为 Codex 自己的常规 last-known-good 数据CCR 不会拿它替代远端目录。
### Grok CLI
Grok CLI 配置固定为 **仅从 CCR 打开时生效****CLI only**。保存后复制并运行配置卡片上的命令,例如 `ccr-app "Grok - Work"`
生成的包装器会把 Grok 的模型网关和模型列表地址指向 CCR 的 `/v1`,注入该配置专属的 CCR API Key并把选中的 CCR 模型设为默认模型。如果 CCR Desktop 网关尚未运行,`ccr-app` 会为 Grok 会话启动一个可共享的临时服务并在最后一个会话退出后清理。Grok CLI 没有单独指定用户配置文件的选项,因此 CCR 会把 `GROK_HOME` 指向配置专属目录;其中的 `config.toml` 初始复制自用户配置,之后可以独立修改,不会回写原文件,同时隔离 `auth.json`,避免本机 xAI OAuth token 覆盖 CCR Key。插件、技能和会话目录仍与原 Grok home 共享。进入 Grok CLI 后可以使用 `/model` 切换 CCR 返回的普通供应商模型或 Fusion 模型,切换后的请求仍然经过 CCR。
### ZCode
| 配置项 | 作用 |
| --- | --- |
| Provider ID | 写入 ZCode 供应商引用,默认是 `claude-code-router`。 |
| Provider name | ZCode 中展示的供应商名称,默认是 `Claude Code Router`。 |
| ZCode model | ZCode App 打开后的默认模型。可以选择普通供应商模型或 Fusion 模型。 |
| 配置文件 | 默认是 `~/.zcode/cli/config.json`CCR 还会写入 ZCode v2 配置和模型缓存。 |
| 环境变量 | 注入 ZCode App 和中间层启动器。 |
| Bot | 只在 ZCode App 入口生效。 |
ZCode 只支持 App 打开,因此入口模式固定为 `App only`,也不会显示 `Show all sessions`
## CLI 与 App 模式区别
| 模式 | 如何打开 | 适合场景 | 主要差异 |
| --- | --- | --- | --- |
| CLI | 点击终端图标复制命令,然后在终端运行 `ccr-app <配置名称>` | 在项目目录中运行 Agent、需要 shell 工作流、需要把命令放进脚本 | 使用对应配置的包装器或中间层启动;通常不启动桌面窗口;当前不转发 Bot 消息。 |
| App | 点击播放图标从 CCR 桌面 App 启动 | 需要桌面窗口、Bot 消息转发或接力 | 同一配置重复打开会激活已有窗口。是否支持多开取决于 AgentOpenCode Desktop 是单实例应用,切换 OpenCode 配置时 CCR 会先停止其管理的旧实例。 |
| CLI & APP | 同一个配置同时提供 CLI 和 App 入口 | 同一套模型配置既用于终端,也用于桌面 App | 两个入口共用配置名称、模型、作用范围和环境变量,但启动方式不同。 |
| Agent | 选择 Claude Code、Codex 或 ZCode |
| 配置名称 | 用于在 CCR 中识别配置,也会作为 `ccr <配置名称>` 的打开目标 |
| 作用范围 | “仅从 CCR 打开时生效”会使用 CCR 管理的独立配置;“系统默认”会写入对应 Agent 的默认配置 |
| 入口模式 | `CLI & APP``CLI only``App only`ZCode 只支持 App |
| 模型 | 该 Agent 打开后的默认模型,可以选择普通供应商模型或 Fusion 模型 |
| Bot | App 入口可以绑定 Bot用于 IM 消息转发或接力 |
## 各 Agent 的差异
### Claude Code
Claude Code CLI 配置会写入设置文件。选择“仅从 CCR 打开时生效”时CCR 会在自己的配置目录下为这个 Agent配置生成独立设置文件并通过独立启动包装器打开 Claude Code。
Claude Code 配置会写入设置文件。选择“仅从 CCR 打开时生效”时CCR 会在自己的配置目录下为这个 Agent配置生成独立设置文件并通过独立启动包装器打开 Claude Code。
从桌面 App 打开 Claude App 时CCR 还会为该配置准备独立用户数据目录。不同 Agent配置使用不同目录因此可以同时打开多个 Claude App 实例。
绑定 Bot 后Claude App 的伴生 worker 会把 Project/Session、流式回复、附件、会话用量和原生权限/Ask User 请求接入 IMApp 退出时 worker 同步停止。
### Codex
Codex 配置会写入 `config.toml`,并生成模型目录文件。选择“仅从 CCR 打开时生效”时CCR 会把这些文件放在按配置 `id` 区分的目录中。
Codex 支持 CLI 和 App。CLI 会通过对应配置的启动器打开App 会启动 ChatGPT、使用独立用户数据目录并把当前配置中的模型和供应商信息带入 App。
绑定 Bot 后Codex App 的伴生 worker 使用 Codex 原生 rollout Session实现 Project/Session 浏览、续接、队列、取消、模型设置、用量、附件和诊断。该 worker 只随受管 App 存活。
### OpenCode
OpenCode 配置会写入 JSON/JSONC 文件,把当前选择的供应商和模型路由到 CCR。CLI 通过配置专属包装器启动App 使用相同的有效配置启动已安装的 OpenCode Desktop。
选择 Bot 并从 CCR 打开 App 后CCR 会启动配套 worker通过 OpenCode 原生 Session 处理收到的 Bot 消息,并提供与其他 App 一致的 Project/Session、队列、媒体、设置和诊断合同。受管 OpenCode App 退出或切换配置时,该 worker 也会同步停止。
### Grok CLI
Grok CLI 只支持 CLI。CCR 通过配置专属包装器启动它,注入 CCR 模型网关、模型发现地址、API Key 和默认模型,并通过不含 xAI OAuth 凭据的配置专属 Grok home 保证推理使用 CCR Key用户原有的 Grok home 不会被改写。
Codex 支持 CLI 和 App。CLI 会通过对应配置的启动器打开App 会使用独立用户数据目录,并把当前配置中的模型和供应商信息带入 Codex App。
### ZCode
ZCode 只支持 App 打开。CCR 会根据 ZCode home 或自定义配置文件写入 ZCode 的 CLI 配置、v2 配置和模型缓存,并在 App 启动时使用当前 Agent配置的模型、供应商和独立用户数据目录。
绑定 Bot 后ZCode 使用与 Codex 同类的 App 伴生 worker 和原生 Session 扫描ZCode App 关闭时接力立即离线。
## 多开建议
1. 为每个需要独立运行的 Agent 实例创建一个 Agent配置。

View file

@ -2,102 +2,66 @@
title: 一键导入供应商
pageTitle: 一键导入供应商
eyebrow: 详细配置
lead: 快速添加常见模型供应商,确认无误后即可保存,减少手动配置的繁琐步骤
lead: 使用 ccr://provider 链接把供应商配置带入 CCR并通过现有 preset 供应商按钮快速打开导入确认页
---
## 一键导入
选择下面的供应商即可开始添加。CCR 会先显示即将添加的内容,确认无误后再保存;使用自定义入口时,请确保来源可信
下面的按钮会打开 CCR 桌面 App 的供应商导入确认页。预设按钮不会携带 API Key自定义供应商链接可以携带 Key。导入前始终确认供应商名称、Base URL、协议和模型
<div class="provider-import-grid" aria-label="Preset provider import buttons">
<a class="provider-import-button provider-openai" href="ccr://provider?name=OpenAI&amp;base_url=https%3A%2F%2Fapi.openai.com%2Fv1&amp;protocol=openai_responses&amp;models=gpt-5.5%2Cgpt-5.5-pro%2Cgpt-5.5-instant%2Cgpt-5.4-mini" aria-label="导入 OpenAI 官方供应商">
<a class="provider-import-button provider-openai" href="ccr://provider?name=OpenAI&amp;base_url=https%3A%2F%2Fapi.openai.com%2Fv1&amp;protocol=openai_responses&amp;models=gpt-4o" aria-label="导入 OpenAI 官方供应商">
<span class="provider-import-icon-shell"><img src="../../provider-icons/openai.png" alt="" loading="lazy" /></span>
<span class="provider-import-copy"><span class="provider-import-name">OpenAI 官方</span><span class="provider-import-meta">Responses / Chat Completions</span></span>
</a>
<a class="provider-import-button provider-anthropic" href="ccr://provider?name=Anthropic&amp;base_url=https%3A%2F%2Fapi.anthropic.com&amp;protocol=anthropic_messages&amp;models=claude-fable-5%2Cclaude-opus-4-8%2Cclaude-sonnet-4-6%2Cclaude-haiku-4-5" aria-label="导入 Anthropic 官方供应商">
<a class="provider-import-button provider-anthropic" href="ccr://provider?name=Anthropic&amp;base_url=https%3A%2F%2Fapi.anthropic.com&amp;protocol=anthropic_messages&amp;models=claude-sonnet-4-20250514" aria-label="导入 Anthropic 官方供应商">
<span class="provider-import-icon-shell"><img src="../../provider-icons/anthropic.png" alt="" loading="lazy" /></span>
<span class="provider-import-copy"><span class="provider-import-name">Anthropic 官方</span><span class="provider-import-meta">Anthropic Messages</span></span>
</a>
<a class="provider-import-button provider-gemini" href="ccr://provider?name=Google+Gemini&amp;base_url=https%3A%2F%2Fgenerativelanguage.googleapis.com&amp;protocol=gemini_generate_content&amp;models=gemini-3.5-flash%2Cgemini-3.1-pro-preview%2Cgemini-3-flash-preview" aria-label="导入谷歌 Gemini 供应商">
<a class="provider-import-button provider-gemini" href="ccr://provider?name=Google+Gemini&amp;base_url=https%3A%2F%2Fgenerativelanguage.googleapis.com&amp;protocol=gemini_generate_content" aria-label="导入谷歌 Gemini 供应商">
<span class="provider-import-icon-shell"><img src="../../provider-icons/gemini.svg" alt="" loading="lazy" /></span>
<span class="provider-import-copy"><span class="provider-import-name">谷歌 Gemini</span><span class="provider-import-meta">Gemini Generate Content</span></span>
</a>
<a class="provider-import-button provider-openrouter" href="ccr://provider?name=OpenRouter&amp;base_url=https%3A%2F%2Fopenrouter.ai%2Fapi%2Fv1&amp;protocol=openai_chat_completions&amp;models=%7Eopenai%2Fgpt-latest%2C%7Eanthropic%2Fclaude-opus-latest%2C%7Eanthropic%2Fclaude-sonnet-latest%2Cgoogle%2Fgemini-3.5-flash%2Cz-ai%2Fglm-5.2" aria-label="导入 OpenRouter 路由供应商">
<a class="provider-import-button provider-openrouter" href="ccr://provider?name=OpenRouter&amp;base_url=https%3A%2F%2Fopenrouter.ai%2Fapi%2Fv1&amp;protocol=openai_chat_completions" aria-label="导入 OpenRouter 路由供应商">
<span class="provider-import-icon-shell"><img src="../../provider-icons/openrouter.ico" alt="" loading="lazy" /></span>
<span class="provider-import-copy"><span class="provider-import-name">OpenRouter 路由</span><span class="provider-import-meta">OpenAI compatible gateway</span></span>
</a>
<a class="provider-import-button provider-deepseek" href="ccr://provider?name=DeepSeek&amp;base_url=https%3A%2F%2Fapi.deepseek.com&amp;protocol=openai_chat_completions&amp;models=deepseek-v4-pro%2Cdeepseek-v4-flash%2Cdeepseek-v3.2%2Cdeepseek-reasoner%2Cdeepseek-chat" aria-label="导入 DeepSeek 深度求索供应商">
<a class="provider-import-button provider-deepseek" href="ccr://provider?name=DeepSeek&amp;base_url=https%3A%2F%2Fapi.deepseek.com&amp;protocol=openai_chat_completions" aria-label="导入 DeepSeek 深度求索供应商">
<span class="provider-import-icon-shell"><img src="../../provider-icons/deepseek.ico" alt="" loading="lazy" /></span>
<span class="provider-import-copy"><span class="provider-import-name">DeepSeek 深度求索</span><span class="provider-import-meta">Chat Completions</span></span>
</a>
<a class="provider-import-button provider-zhipu-coding" href="ccr://provider?name=Zhipu+AI+%28China%29+-+Coding+Plan&amp;base_url=https%3A%2F%2Fopen.bigmodel.cn%2Fapi%2Fcoding%2Fpaas%2Fv4&amp;protocol=openai_chat_completions&amp;models=glm-5.2%2Cglm-5.1%2Cglm-5-turbo%2Cglm-5v-turbo%2Cglm-4.7" aria-label="导入智谱 Coding 供应商">
<a class="provider-import-button provider-zhipu-coding" href="ccr://provider?name=Zhipu+AI+%28China%29+-+Coding+Plan&amp;base_url=https%3A%2F%2Fopen.bigmodel.cn%2Fapi%2Fcoding%2Fpaas%2Fv4&amp;protocol=openai_chat_completions&amp;models=glm-5.2%2Cglm-5.1%2Cglm-4.7%2Cglm-4.5-air" aria-label="导入智谱 Coding 供应商">
<span class="provider-import-icon-shell"><img src="../../provider-icons/zhipu-cn-coding.png" alt="" loading="lazy" /></span>
<span class="provider-import-copy"><span class="provider-import-name">智谱 Coding</span><span class="provider-import-meta">中国 Coding 计划</span></span>
</a>
<a class="provider-import-button provider-zhipu-general" href="ccr://provider?name=Zhipu+AI+%28China%29+-+General+Endpoint&amp;base_url=https%3A%2F%2Fopen.bigmodel.cn%2Fapi%2Fpaas%2Fv4&amp;protocol=openai_chat_completions&amp;models=glm-5.2%2Cglm-5.1%2Cglm-5%2Cglm-5v-turbo%2Cglm-4.7" aria-label="导入智谱通用供应商">
<a class="provider-import-button provider-zhipu-general" href="ccr://provider?name=Zhipu+AI+%28China%29+-+General+Endpoint&amp;base_url=https%3A%2F%2Fopen.bigmodel.cn%2Fapi%2Fpaas%2Fv4&amp;protocol=openai_chat_completions&amp;models=glm-5.2%2Cglm-5.1%2Cglm-4.7%2Cglm-4.5-air" aria-label="导入智谱通用供应商">
<span class="provider-import-icon-shell"><img src="../../provider-icons/zhipu-cn-general.png" alt="" loading="lazy" /></span>
<span class="provider-import-copy"><span class="provider-import-name">智谱通用</span><span class="provider-import-meta">中国通用端点</span></span>
</a>
<a class="provider-import-button provider-zai-coding" href="ccr://provider?name=Z.ai+%28Global%29+-+Coding+Plan&amp;base_url=https%3A%2F%2Fapi.z.ai%2Fapi%2Fcoding%2Fpaas%2Fv4&amp;protocol=openai_chat_completions&amp;models=glm-5.2%2Cglm-5.1%2Cglm-5-turbo%2Cglm-5v-turbo%2Cglm-4.7" aria-label="导入智谱国际 Coding 供应商">
<a class="provider-import-button provider-zai-coding" href="ccr://provider?name=Z.ai+%28Global%29+-+Coding+Plan&amp;base_url=https%3A%2F%2Fapi.z.ai%2Fapi%2Fcoding%2Fpaas%2Fv4&amp;protocol=openai_chat_completions&amp;models=glm-5.2%2Cglm-5.1%2Cglm-4.7%2Cglm-4.5-air" aria-label="导入智谱国际 Coding 供应商">
<span class="provider-import-icon-shell"><img src="../../provider-icons/zai-global-coding.svg" alt="" loading="lazy" /></span>
<span class="provider-import-copy"><span class="provider-import-name">智谱国际 Coding</span><span class="provider-import-meta">全球 Coding 计划</span></span>
</a>
<a class="provider-import-button provider-zai-general" href="ccr://provider?name=Z.ai+%28Global%29+-+General+Endpoint&amp;base_url=https%3A%2F%2Fapi.z.ai%2Fapi%2Fpaas%2Fv4&amp;protocol=openai_chat_completions&amp;models=glm-5.2%2Cglm-5.1%2Cglm-5%2Cglm-5v-turbo%2Cglm-4.7" aria-label="导入智谱国际通用供应商">
<a class="provider-import-button provider-zai-general" href="ccr://provider?name=Z.ai+%28Global%29+-+General+Endpoint&amp;base_url=https%3A%2F%2Fapi.z.ai%2Fapi%2Fpaas%2Fv4&amp;protocol=openai_chat_completions&amp;models=glm-5.2%2Cglm-5.1%2Cglm-4.7%2Cglm-4.5-air" aria-label="导入智谱国际通用供应商">
<span class="provider-import-icon-shell"><img src="../../provider-icons/zai-global-general.svg" alt="" loading="lazy" /></span>
<span class="provider-import-copy"><span class="provider-import-name">智谱国际通用</span><span class="provider-import-meta">全球通用端点</span></span>
</a>
<a class="provider-import-button provider-mistral" href="ccr://provider?name=Mistral&amp;base_url=https%3A%2F%2Fapi.mistral.ai%2Fv1&amp;protocol=openai_chat_completions&amp;models=mistral-medium-3-5%2Cmistral-large-3%2Cministral-3-14b-instruct-2512%2Cdevstral-2512" aria-label="导入 Mistral 官方供应商">
<a class="provider-import-button provider-mistral" href="ccr://provider?name=Mistral&amp;base_url=https%3A%2F%2Fapi.mistral.ai%2Fv1&amp;protocol=openai_chat_completions" aria-label="导入 Mistral 官方供应商">
<span class="provider-import-icon-shell"><img src="../../provider-icons/mistral.webp" alt="" loading="lazy" /></span>
<span class="provider-import-copy"><span class="provider-import-name">Mistral 官方</span><span class="provider-import-meta">Chat Completions</span></span>
</a>
<a class="provider-import-button provider-moonshot" href="ccr://provider?name=Kimi+API+%28China%29&amp;base_url=https%3A%2F%2Fapi.moonshot.cn%2Fv1&amp;protocol=openai_chat_completions&amp;models=kimi-k2.7-code%2Ckimi-k2.6%2Ckimi-latest%2Ckimi-thinking-preview" aria-label="导入 Kimi API 国内供应商">
<a class="provider-import-button provider-moonshot" href="ccr://provider?name=Moonshot+Kimi&amp;base_url=https%3A%2F%2Fapi.moonshot.cn%2Fv1&amp;protocol=openai_chat_completions&amp;models=moonshot-v1-8k" aria-label="导入月之暗面 Kimi 供应商">
<span class="provider-import-icon-shell"><img src="../../provider-icons/moonshot.ico" alt="" loading="lazy" /></span>
<span class="provider-import-copy"><span class="provider-import-name">Kimi API国内</span><span class="provider-import-meta">国内平台</span></span>
<span class="provider-import-copy"><span class="provider-import-name">月之暗面 Kimi</span><span class="provider-import-meta">Chat Completions</span></span>
</a>
<a class="provider-import-button provider-moonshot-global" href="ccr://provider?name=Kimi+API+%28Global%29&amp;base_url=https%3A%2F%2Fapi.moonshot.ai%2Fv1&amp;protocol=openai_chat_completions&amp;models=kimi-k2.7-code%2Ckimi-k2.6%2Ckimi-latest%2Ckimi-thinking-preview" aria-label="导入 Kimi API 海外供应商">
<span class="provider-import-icon-shell"><img src="../../provider-icons/moonshot.ico" alt="" loading="lazy" /></span>
<span class="provider-import-copy"><span class="provider-import-name">Kimi API海外</span><span class="provider-import-meta">海外平台</span></span>
</a>
<a class="provider-import-button provider-kimi-coding" href="ccr://provider?name=Kimi+Code+-+Coding+Plan&amp;base_url=https%3A%2F%2Fapi.kimi.com%2Fcoding%2Fv1&amp;protocol=openai_chat_completions&amp;models=kimi-for-coding" aria-label="导入 Kimi Code Coding Plan 供应商">
<span class="provider-import-icon-shell"><img src="../../provider-icons/moonshot.ico" alt="" loading="lazy" /></span>
<span class="provider-import-copy"><span class="provider-import-name">Kimi Code</span><span class="provider-import-meta">Coding Plan</span></span>
</a>
<a class="provider-import-button provider-bailian" href="ccr://provider?name=Alibaba+Bailian&amp;base_url=https%3A%2F%2Fdashscope.aliyuncs.com%2Fcompatible-mode%2Fv1&amp;protocol=openai_chat_completions&amp;models=qwen3.7-max%2Cqwen3.7-plus%2Cqwen3.6-max-preview%2Cqwen3-coder-plus%2Cqwen3-max" aria-label="导入阿里百炼供应商">
<a class="provider-import-button provider-bailian" href="ccr://provider?name=Alibaba+Bailian&amp;base_url=https%3A%2F%2Fdashscope.aliyuncs.com%2Fcompatible-mode%2Fv1&amp;protocol=openai_chat_completions" aria-label="导入阿里百炼供应商">
<span class="provider-import-icon-shell"><img src="../../provider-icons/bailian.ico" alt="" loading="lazy" /></span>
<span class="provider-import-copy"><span class="provider-import-name">阿里百炼</span><span class="provider-import-meta">DashScope 兼容</span></span>
</a>
<a class="provider-import-button provider-siliconflow" href="ccr://provider?name=SiliconFlow&amp;base_url=https%3A%2F%2Fapi.siliconflow.cn%2Fv1&amp;protocol=openai_chat_completions&amp;models=zai-org%2FGLM-5.2%2Cdeepseek-ai%2Fdeepseek-v4-pro%2Cdeepseek-ai%2Fdeepseek-v4-flash%2Czai-org%2FGLM-5.1%2Cdeepseek-ai%2FDeepSeek-V3.2" aria-label="导入硅基流动供应商">
<a class="provider-import-button provider-siliconflow" href="ccr://provider?name=SiliconFlow&amp;base_url=https%3A%2F%2Fapi.siliconflow.cn%2Fv1&amp;protocol=openai_chat_completions" aria-label="导入硅基流动供应商">
<span class="provider-import-icon-shell"><img src="../../provider-icons/siliconflow.png" alt="" loading="lazy" /></span>
<span class="provider-import-copy"><span class="provider-import-name">硅基流动</span><span class="provider-import-meta">Chat Completions</span></span>
</a>
<a class="provider-import-button provider-runapi" href="ccr://provider?name=RunAPI&amp;base_url=https%3A%2F%2Frunapi.co%2Fv1&amp;protocol=openai_responses" aria-label="导入 RunAPI 供应商">
<span class="provider-import-icon-shell"><img src="../../provider-icons/runapi.jpg" alt="" loading="lazy" /></span>
<span class="provider-import-copy"><span class="provider-import-name">RunAPI</span><span class="provider-import-meta">Responses / Chat Completions</span></span>
</a>
<a class="provider-import-button provider-teamorouter" href="ccr://provider?name=TeamoRouter&amp;base_url=https%3A%2F%2Fapi.teamorouter.com&amp;protocol=anthropic_messages&amp;source=https%3A%2F%2Fteamorouter.com%2F" aria-label="导入 TeamoRouter 供应商">
<span class="provider-import-icon-shell"><img src="../../provider-icons/teamorouter.png" alt="" loading="lazy" /></span>
<span class="provider-import-copy"><span class="provider-import-name">TeamoRouter</span><span class="provider-import-meta">Anthropic / Chat / Responses</span></span>
</a>
<a class="provider-import-button provider-unity2" href="ccr://provider?name=Unity2.Ai&amp;base_url=https%3A%2F%2Funity2.ai%2Fv1&amp;protocol=openai_chat_completions&amp;source=https%3A%2F%2Funity2.ai%2Fregister%3Fsource%3Dclaudecoderouter" aria-label="导入 Unity2.Ai 供应商">
<span class="provider-import-icon-shell"><img src="../../provider-icons/unity2.jpg" alt="" loading="lazy" /></span>
<span class="provider-import-copy"><span class="provider-import-name">Unity2.Ai</span><span class="provider-import-meta">OpenAI 兼容网关</span></span>
</a>
<a class="provider-import-button provider-code0" href="ccr://provider?name=code0.ai&amp;base_url=https%3A%2F%2Fconsole.code0.ai&amp;protocol=anthropic_messages&amp;source=https%3A%2F%2Fcode0.ai%2Fagent%2Fregister%2F9n9jOsSnYQoemIVL%3Futm_source%3Dclaudecoderouter%26utm_medium%3Dpartner%26utm_campaign%3Dclaudecoderouter_2026%26utm_content%3Ddefault" aria-label="导入 code0.ai 供应商">
<span class="provider-import-icon-shell"><img src="../../provider-icons/code0.png" alt="" loading="lazy" /></span>
<span class="provider-import-copy"><span class="provider-import-name">code0.ai</span><span class="provider-import-meta">Anthropic / Chat / Responses</span></span>
</a>
<a class="provider-import-button provider-claudeapi" href="ccr://provider?name=claudeapi&amp;base_url=https%3A%2F%2Fgw.claudeapi.com&amp;protocol=anthropic_messages&amp;source=https%3A%2F%2Fconsole.claudeapi.com%2Fagent%2Fregister%2FLbmB7Y9kPloyzhwF%3Futm_source%3Dclaudecoderouter%26utm_medium%3Dpartner%26utm_campaign%3Dclaudecoderouter_2026%26utm_content%3Ddefault" aria-label="导入 claudeapi 供应商">
<span class="provider-import-icon-shell"><img src="../../provider-icons/claudeapi.png" alt="" loading="lazy" /></span>
<span class="provider-import-copy"><span class="provider-import-name">claudeapi</span><span class="provider-import-meta">Anthropic Messages</span></span>
</a>
<a class="provider-import-button provider-qiniu-ai" href="ccr://provider?name=%E4%B8%83%E7%89%9B%E4%BA%91+AI&amp;base_url=https%3A%2F%2Fapi.qnaigc.com&amp;protocol=openai_chat_completions&amp;source=https%3A%2F%2Fs.qiniu.com%2FAVjMVf" aria-label="导入七牛云 AI 供应商">
<span class="provider-import-icon-shell"><img src="../../provider-icons/qiniu-ai.png" alt="" loading="lazy" /></span>
<span class="provider-import-copy"><span class="provider-import-name">七牛云 AI</span><span class="provider-import-meta">Chat / Responses / Anthropic / Gemini Generate</span></span>
</a>
<a class="provider-import-button provider-fenno" href="ccr://provider?name=Fenno.ai&amp;base_url=https%3A%2F%2Fapi.fenno.ai&amp;protocol=openai_chat_completions&amp;source=https%3A%2F%2Fapi.fenno.ai%2Fregister%3Fredirect%3D%2Fpurchase%3Ftab%3Dsubscription%2526group%3D16%26aff%3D9HHHAB5QLAES" aria-label="导入 Fenno.ai 供应商">
<span class="provider-import-icon-shell"><img src="../../provider-icons/fenno.jpg" alt="" loading="lazy" /></span>
<span class="provider-import-copy"><span class="provider-import-name">Fenno.ai</span><span class="provider-import-meta">Chat / Responses / Anthropic</span></span>
</a>
</div>
## 嵌入式按钮组件
@ -157,7 +121,7 @@ CCR 也提供了一个无框架的按钮脚本,供应商可以嵌入到自己
| `name` | Provider 展示名称 |
| `base_url` | Provider API Base URL直链导入时必填 |
| `api_key` | 可选 Provider API Key |
| `protocol` | 协议类型,支持 `openai_chat_completions``openai_responses``anthropic_messages``gemini_generate_content``gemini_interactions` |
| `protocol` | 协议类型,支持 `openai_chat_completions``openai_responses``anthropic_messages``gemini_generate_content` |
| `models` | 模型列表。HTML 中用逗号或换行分隔JS 中可传字符串或数组 |
| `icon` | Provider 图标 URL |
| `source` | Provider 官网或配置来源 |
@ -285,7 +249,7 @@ Manifest 可以把供应商信息放在顶层 `provider` 对象中:
| `name` | Provider 展示名称 |
| `base_url` | Provider API Base URL必填 |
| `api_key` | 可选 Provider API Key |
| `protocol` | 协议类型,支持 `openai_chat_completions``openai_responses``anthropic_messages``gemini_generate_content``gemini_interactions` |
| `protocol` | 协议类型,支持 `openai_chat_completions``openai_responses``anthropic_messages``gemini_generate_content` |
| `models` | 模型列表,支持逗号或换行分隔,也可以重复传入 |
| `icon` | Provider 图标 URL |
| `source` | Provider 官网或配置来源 |

View file

@ -5,166 +5,22 @@ eyebrow: 详细配置
lead: 配置 CCR 的上游模型服务,包括协议、基础 URL、模型列表和凭据。
---
## 导入本机 Agent 登录态
## 基本概念
添加供应商时CCR 会扫描本机已有的 Agent 登录状态。检测到可复用凭据后,添加弹窗会显示对应导入入口。导入会创建一个普通供应商和配套 provider plugin让 CCR 复用本机 Agent 授权访问上游服务,不需要再手动粘贴常规 API Key。
### Claude Code
Claude Code 导入会读取本机 Claude Code OAuth 凭据。检测到可用 access token 时,可以导入为 `Claude Code API` 供应商。
导入后:
1. 协议使用 `anthropic_messages`
2. 默认模型包含 `claude-sonnet-4-20250514`,后续可以在供应商模型列表中增减模型。
3. CCR 会创建 OAuth provider plugin把请求认证转换为 Claude Code 登录态。
4. 账号用量会使用 Anthropic OAuth 用量接口,适合在供应商列表、托盘或账号面板里查看额度状态。
如果只检测到登录痕迹但没有可用 access token导入入口会显示不可导入原因。此时先在 Claude Code 中重新登录,再回到 CCR 添加供应商。
### Codex
Codex 导入会读取本机 Codex 登录文件和模型缓存。检测到 Codex access token 或 refresh token 时,可以导入为 `Codex API` 供应商。
导入后:
1. 协议使用 `openai_responses`
2. API 地址指向 Codex 后端,默认模型至少包含 `gpt-5-codex`,也会合并本机模型缓存中的模型和显示名。
3. CCR 会创建 Codex OAuth provider plugin并在需要时刷新访问凭据。
4. 账号用量会读取 Codex 额度、余额和 token 统计接口。
导入后可以直接在路由或 Agent配置中选择 `Codex API/模型名`。如果模型缓存较旧,可以先打开 Codex 让它刷新模型列表,再回到 CCR 重新导入或编辑模型。
### ZCode
ZCode 导入会读取本机 ZCode 配置中的供应商 API Key、API 地址和模型列表。只有检测到可用供应商 Key 和 Base URL 时,才能导入为 `ZCode API` 供应商。
导入后:
1. 协议使用 `anthropic_messages`
2. 模型优先来自 ZCode 本机配置;没有配置时会使用 ZCode 运行缓存或默认模型。
3. CCR 会创建 API Key provider plugin把 ZCode 本机配置中的 Key 用于请求认证。
4. 如果 API 地址命中 CCR 内置预设,账号用量配置会复用对应预设。
如果只检测到 ZCode 登录态,但没有检测到可用供应商 API Key导入入口会显示不可导入。此时需要先在 ZCode 中配置可用模型供应商,再回到 CCR 添加供应商。
## 主字段
| 字段 | 代表的能力 |
| --- | --- |
| 选择 预设供应商 | 套用 CCR 内置供应商模板,包括默认 API 地址、可用协议、默认模型、图标、官网链接和部分供应商的用量读取配置。选择 `其他 / 自定义 API 地址` 时,可以接入任意兼容 OpenAI、Anthropic 或 Gemini 协议的上游服务。 |
| 名称 | CCR 内部显示名,也是路由、模型选择、日志和配置中识别供应商的名字。名称必须唯一,建议短且稳定。 |
| API 地址 | 上游 API Base URL。它决定请求实际发往哪里也用于协议探测、模型列表探测、图标探测和安全校验。预设供应商添加时默认隐藏该字段可在高级设置里覆盖。自定义供应商必须填写。 |
| API 密钥 | 默认供应商凭据。没有配置凭据池时,模型请求会使用这条 Key协议探测、模型探测、连通性检测和默认用量读取也会使用它。只填写由当前 API 地址对应供应商签发的 Key。 |
| 模型 | 暴露给 CCR 的模型 ID 列表。路由规则、Profile 模型选择、模型目录和客户端 `/models` 响应都会基于这里的模型。 |
| 搜索模型 / 全部 / 清除 | 当 CCR 能从上游或模型目录拿到模型列表时,可以搜索、全选、清除并勾选模型。勾选结果会保存到供应商的模型列表。 |
| 自定义模型 | 手动添加没有被探测出来的模型 ID。适合供应商没有 `/models` 接口,或新模型还未进入模型目录的情况。 |
| 检测连通性 | 用当前 API 地址、API 密钥、协议和所选模型发送真实测试请求。它可以验证 Key、模型名和协议是否真的可调用。 |
| 要检测的模型 | 连接检查确认弹窗中的模型选择。用于控制只测试部分模型,避免一次性检查全部模型造成额外消耗。 |
| 检测结果 | 展示每个模型是否可用、命中的协议和上游返回的诊断信息。可用结果不会自动增加模型,仍以弹窗主表单中的模型选择为准。 |
## 连通性检测
`检测连通性` 会对你选择的模型发送真实的模型请求,用来确认 API 地址、API 密钥、协议和模型 ID 是否可用。检测请求会限制输出长度,但仍然可能产生额外 token 消耗或计入供应商侧请求次数。
如果供应商按请求、输入 token 或输出 token 计费,建议只勾选需要确认的模型,不要一次性检查全部模型。检测结果只用于诊断连通性,不会自动修改模型列表或用量读取配置。
供应商是 CCR 转发请求的上游模型服务。每个供应商至少需要协议、基础 URL、模型列表和一条可用凭据。
## 凭据
`API 密钥` 是最简单的单 Key 配置。需要管理多条上游 Key 时,展开“高级设置”中的“凭据池”
凭据用来保存 API Key。多条凭据可以按优先级和权重轮换也可以在某条 Key 触发限额或失败时自动切换。
| 字段 | 代表的能力 |
建议给每条 Key 设置容易识别的名称,方便在请求日志里定位问题。
## 供应商选项
| 字段 | 说明 |
| --- | --- |
| 显示凭据配置 | 展开或收起凭据池配置。未展开不影响已保存的凭据,只是隐藏编辑区域。 |
| 导入 JSON | 从 JSON 文件批量导入凭据。支持顶层数组,或对象中的 `credentials``keys``apiKeys` 数组。 |
| 添加 Key | 新增一条上游 API Key。 |
| 启用 | 控制单条凭据是否参与请求转发和用量读取。关闭后保留配置,但不会被选中。 |
| 名称 | 单条凭据的显示名。会出现在账号用量、日志和内部诊断里,建议写成可识别的用途或额度来源。 |
| API 密钥 | 该凭据实际发送给上游的 Key。配置了凭据池后CCR 会把启用的凭据展开成多个内部上游目标,并优先使用凭据池中的 Key。主表单的默认 `API 密钥` 只作为单 Key 配置使用。 |
| 移除 | 删除当前凭据行。 |
| Key 高级选项 | 展开单条凭据的调度和限额字段。 |
| 优先级 | 凭据优先级,数字越小越优先。未填写时按凭据行顺序作为优先级。 |
| 权重 | 同优先级、相近使用率下的排序权重,数字越大越优先。未填写时为 `1`。 |
| 限制 JSON | 该 Key 的本地限额规则。CCR 会按请求、tokens 或图片数量统计窗口使用量,达到上限后自动跳过该 Key尝试同供应商的其他可用 Key。 |
`限制 JSON` 支持的常用字段:
| 字段 | 含义 |
| --- | --- |
| `rpm` / `rph` / `rpd` | 每分钟 / 每小时 / 每天最多请求数 |
| `tpm` / `tph` / `tpd` | 每分钟 / 每小时 / 每天最多 tokens |
| `ipm` / `iph` / `ipd` | 每分钟 / 每小时 / 每天最多图片数 |
| `maxRequests` + `windowMs` | 自定义时间窗口内最多请求数 |
| `maxTokens` + `quotaWindowMs` | 自定义时间窗口内最多 tokens |
示例:
```json
{
"rpm": 60,
"tpm": 100000
}
```
凭据池的作用是“上游 Key 池”不同于“API 密钥”页面里的 CCR 客户端访问 Key。前者控制 CCR 调用供应商时使用哪个 Key后者控制客户端访问 CCR 时使用哪个 Key。
## 用量读取
“获取用量”用于让 CCR 在供应商列表、托盘或账号面板中展示余额、套餐额度、状态和错误信息。它不会影响模型是否能请求,只影响账号用量展示。
| 字段 | 代表的能力 |
| --- | --- |
| 获取用量 | 启用或关闭该供应商的账号用量读取。关闭后不请求用量接口。 |
| 用量模式 | 用量读取方式。`标准用量端点` 使用 CCR 标准账号端点;`HTTP JSON 请求` 手动配置一个 JSON 接口;`原始连接器 JSON` 直接编辑 connector 数组。 |
| 刷新间隔(毫秒) | 用量刷新间隔,单位毫秒。未填写时使用默认刷新间隔,最小有效间隔为 30000ms。 |
### 标准用量端点
该模式会尝试供应商侧的 CCR 标准账号端点,例如 `/.well-known/ccr/account``/v1/account/limits`。适合已经适配 CCR 标准格式的供应商或内置预设。
### HTTP JSON 请求
该模式适合供应商已有自己的余额或额度接口,且返回自定义 JSON 格式的情况。
| 字段 | 代表的能力 |
| --- | --- |
| 方法 | 用量请求方法,支持 `GET``POST`。 |
| 用量请求 URL | 用量接口地址。可以是完整 URL。请求会附带供应商 API Key除非在 raw connector 中改成其他认证方式。 |
| 请求头 | 用量接口需要的额外请求头。不要在这里写固定的敏感认证头,优先使用供应商 API Key 认证。 |
| 请求体 | `POST` 请求体,必须是合法 JSON。 |
| 余额剩余字段 | 余额剩余值在响应 JSON 中的路径。 |
| 余额总额字段 | 余额总量或充值总额在响应 JSON 中的路径。 |
| 余额已用字段 | 已用余额在响应 JSON 中的路径。 |
| 余额单位 | 余额单位,例如 `USD``CNY``%`。 |
| 订阅剩余字段 | 套餐、订阅、tokens 或配额剩余量路径。 |
| 订阅上限字段 | 套餐、订阅、tokens 或配额总量路径。 |
| 订阅重置字段 | 套餐重置时间路径。可以返回 ISO 时间,也可以返回秒级或毫秒级时间戳。 |
| 订阅单位 | 套餐单位,例如 `tokens``requests``hours`。 |
| 状态字段 | 账号状态路径。支持 `ok``warning``critical``error``unsupported`。 |
| 消息字段 | 账号提示信息路径。适合展示供应商返回的错误、套餐说明或风控提示。 |
| 测试用量请求 | 立即请求用量接口并解析映射结果,方便在保存前验证字段路径。 |
| 响应字段 | 测试后列出响应 JSON 中可选字段。点击 `余额剩余``余额总额``余额已用``订阅剩余``订阅上限``重置时间` 可以把该路径快速填入对应字段。 |
字段路径支持 CCR 的轻量 JSONPath 语法:
| 写法 | 说明 |
| --- | --- |
| `$` | 整个响应对象 |
| `$.balance.remaining` | 读取对象字段 |
| `$.items[0].value` | 读取数组下标 |
| `$["weird-key"]` | 读取包含特殊字符的字段名 |
| `$.limits[?(@.type=="TOKENS")].remaining` | 在数组中查找第一个满足简单等值条件的对象 |
| `100 - $.data.percentage` | 数值字段支持简单减法表达式,常用于把“已用百分比”转换成“剩余百分比” |
### 原始连接器 JSON
`连接器 JSON` 允许直接编辑 `account.connectors` 数组,适合需要更复杂能力的供应商。
| 连接器类型 | 能力 |
| --- | --- |
| `standard` | 使用 CCR 标准账号端点。 |
| `http-json` | 请求一个 JSON 接口,并用 mapping 字段映射余额、套餐、状态和消息。 |
| `plugin` | 调用已安装插件注册的账号用量 connector。 |
| `local-estimate` | 不请求远程接口,基于本地窗口配置展示估算额度。 |
点击 `插入示例` 会填入一个包含 `standard``http-json``plugin``local-estimate` 的示例 connector 数组。
| 名称 | CCR 内部显示名,建议短且可识别 |
| 基础 URL | 上游服务地址,自定义供应商要确认包含正确 API 路径 |
| 协议 | OpenAI / Anthropic / Gemini 等协议 |
| 模型 | 暴露给 CCR 的模型列表 |
| 请求头 | 少数兼容服务需要的额外请求头 |

View file

@ -5,141 +5,20 @@ eyebrow: 详细配置
lead: 设置请求如何选择模型,并在失败时通过 Fallback 自动重试或切换到备用模型。
---
## 内置路由
## 路由如何生效
### Claude Code
CCR 的路由会先决定本次请求使用哪个模型,然后再把请求交给上游。当前代码中的主要顺序是:
Claude Code 内置路由的作用是识别 Claude Code 发来的请求,并把主请求路由到 Claude Code Agent 配置中的模型。
1. 如果请求里的 `model` 已经是已知的 `供应商/模型` 形式CCR 会直接使用这个模型。
2. 如果配置了自定义路由脚本,脚本返回的模型优先级高于界面里的路由规则。
3. 路由规则按列表顺序匹配,第一条命中的规则会执行对应的请求改写。
4. 没有规则命中时,使用默认路由;如果没有默认路由,就保留请求原本的模型。
Claude Code **主请求** 使用 Claude Code Agent 配置中的模型如果未设置该内置路由不会生效。CCR 也会自动删除 Claude Code 注入的第一条 `x-anthropic-billing-header` system 消息避免这类计费辅助消息影响后续路由判断。Claude Code 创建的 Subagent、Task 或 Workflow 内部 Agent 可以继续用下面的标签机制自动选择不同模型。
路由规则的核心是 **条件 + 请求动作**。条件判断请求是否命中,请求动作修改请求字段。最常用的动作是把 `request.body.model` 设置为目标模型或 Fusion 模型。
#### Subagent / Workflow 自动路由
## Fallback 是什么
Claude Code 的 Agent / Task / Workflow 可以派生新的模型请求。CCR 使用标签注入来让这些派生请求选择更合适的 CCR 模型:
```text
<CCR-SUBAGENT-MODEL>供应商/模型</CCR-SUBAGENT-MODEL>
```
完整流程如下:
1. Claude Code 主请求命中内置路由后CCR 会检查当前工具列表。
2. 如果至少有一个模型配置了 **Description**CCR 会把可用模型及其说明注入到 `Agent` / `Task` 工具说明和 `prompt` 字段说明里。
3. 如果工具列表里有 `Workflow`CCR 会给 Workflow 工具说明追加要求workflow 内部创建 `Agent` / `Task` 时,每个派生 Agent 的 prompt 第一行都要带同样的模型标签。
4. Claude Code 调用 `Agent` / `Task`,或 Workflow 内部创建 Agent 时prompt 第一行会携带 `<CCR-SUBAGENT-MODEL>供应商/模型</CCR-SUBAGENT-MODEL>`
5. 派生请求进入 CCR 后CCR 从 system 或前两条 user message 中提取并删除这个标签,然后把该请求路由到标签里的模型。
因此Subagent / Workflow 的自动路由由 prompt 标签决定模型。`x-claude-code-agent-id` 等 Header 用于观测,模型选择以标签为准。
##### 与模型页配合
模型页里的 **Description** 是这套机制的开关和选择依据。没有任何模型 Description 时CCR 不会注入 Agent / Task / Workflow 路由提示词,避免把空模型列表写进工具说明。
推荐配置步骤:
1. 在 **供应商** 中添加可用模型,确认模型 ID 可以真实请求。
2. 打开 **模型** 页面,为希望 Subagent 自动选择的模型填写 Description。说明要写清模型适合的任务、速度、成本和限制。
3. 在 **Agent配置** 中启用 Claude Code 配置,并设置主模型。这个模型负责 Claude Code 主会话。
4. 在 **路由** 页面确认 **Claude Code** 内置路由已启用。
5. 在 Claude Code 中使用 Agent、Task 或 Workflow。需要派生 Agent 时Claude Code 会根据模型 Description 选择一个 CCR 模型并写入标签。
Description 建议写成任务导向,而不是只写模型厂商名。例如:
| 模型用途 | Description 示例 |
| --- | --- |
| 快速便宜模型 | 适合代码搜索、文件梳理、摘要、简单修改和低成本并行 Subagent。 |
| 强推理模型 | 适合复杂架构分析、大规模重构计划、跨文件推理和高风险代码审查。 |
| 长上下文模型 | 适合读取大量日志、长文档、仓库级上下文整理和 Workflow 汇总。 |
保存后CCR 会把这些 Description 组织成 “Configured CCR gateway models” 注入给 Claude Code。Claude Code 选择模型后CCR 会在派生请求上看到 `builtin:claude-code-subagent`,并把标签里的模型作为最终 `resolved model`
### Codex
Codex 内置路由会为第三方或非 GPT 模型适配 Codex 的 `apply_patch` 文件编辑工具。目标是让这些模型通过 patch 工具完成文件修改,而不是生成 `cat >``sed -i``python``node` 等命令或脚本来编辑文件。
技术原理是做一次工具协议桥接Codex 原生的 `apply_patch` 是 custom/freeform 工具,入参是原始 patch 文本;很多 OpenAI-compatible 三方模型更擅长普通 function tool。CCR 会在上游请求中把 `apply_patch` 转成 `virtual_apply_patch` function tool并在工具说明里注入完整的 `apply_patch.lark` 语法,要求模型把 patch 写入 `patch` 字段。
模型返回 `virtual_apply_patch`CCR 会把它转换回 Codex 期望的 `custom_tool_call``name = apply_patch``input = 原始 patch 文本`。CCR 不直接修改文件,真正执行 patch 的仍然是 Codex 客户端。这个适配跟随 **Codex** 内置路由启用或关闭没有单独开关GPT 命名模型继续使用 Codex 原生 freeform `apply_patch` 路径。
## 自定义路由
自定义路由在路由页的规则列表中配置。页面顶部的 **搜索路由规则** 可以按名称、条件、请求动作等文本过滤列表;右上角 **添加** 按钮打开 **添加路由规则** 弹窗。规则表格按 **名称**、**条件**、**请求动作**、**状态**、**操作** 展示每条规则。
自定义规则按列表顺序匹配,第一条命中的启用规则会改写请求。表格右侧的上移、下移按钮用来调整优先级,编辑按钮打开 **编辑路由规则**,删除按钮会先弹出确认框。**状态** 列的开关关闭后,规则保留在列表里,但不会参与匹配。
### 添加或编辑规则
弹窗里的字段和保存后的配置一一对应:
| UI 字段 | 填写方式 | 保存后的含义 |
| --- | --- | --- |
| **名称** | 填一个便于识别的规则名。该字段不能为空。 | 显示在列表 **名称** 列,也参与搜索。 |
| **条件** | 选择 `request.header``request.body`,填写字段名、操作符和值。 | 生成 `condition.left``condition.operator``condition.right`。 |
| **改写请求参数** | 至少保留一行 rewrite。每行选择操作、目标 key 和需要的值。 | 生成 `rewrites`,规则命中后按行改写请求。 |
| **启用** | 打开或关闭规则。 | 控制 `enabled`,关闭时不会匹配。 |
| **失败时** | 配置这条规则自己的 Fallback。 | 规则命中后覆盖页面顶部的 **默认失败处理**。 |
**添加** 或 **保存** 按钮只有在表单有效时才可点击:名称、条件字段、条件值都必须填写;每条 rewrite 都必须有 key。`删除` 操作只需要 key`替换数组元素` 需要同时填写 **匹配值****值**;其他操作需要填写 **值**
### 条件
**条件** 区域有四个输入:来源、字段、操作符和值。
| 来源 | 字段示例 | 实际匹配路径 |
| --- | --- | --- |
| `request.header` | `user-agent``x-api-key``x-client-name` | `request.header.user-agent` |
| `request.body` | `model``messages``messages.0.role``tools` | `request.body.model` |
Header 名不区分大小写。Body 字段按点号路径读取,数字片段表示数组下标;例如 `messages.0.role` 读取第一条 message 的 role。对于 messages、tools 这类嵌套数组,通常用 `contains deep` 比固定下标更稳。
值输入框会按常见字面量解析:`true``false``null`、数字、JSON 对象或数组会按对应类型比较;其他内容按字符串处理。需要强制作为字符串时,可以写成 `"123"``'123'`
| 操作符 | 用法 |
| --- | --- |
| `==` / `!=` | 比较实际值和输入值。数字会按数字比较,其他值按可比较文本比较。 |
| `>` / `>=` / `<` / `<=` | 两边都是数字时按数字比较,否则按文本顺序比较。 |
| `starts with` | 判断实际值是否以输入值开头,适合模型前缀分流。 |
| `contains` | 对字符串做包含判断;对数组只检查数组元素。 |
| `contains deep` | 递归检查对象和数组,适合在 `messages``tools` 中查找内容。 |
| `not contains` | `contains` 的反向判断。 |
### 改写请求参数
**改写请求参数** 区域默认给出一行 `request.body.model`。这也是最常用的模型路由写法:选择 **设置**key 填 `request.body.model`,值填目标 `供应商/模型` 或 Fusion 模型。
点击 **添加参数** 可以追加多行 rewrite垃圾桶按钮删除当前行最后一行不能删除。规则命中后CCR 会按列表顺序应用这些 rewrite。
| 操作 | 需要填写 | 行为 |
| --- | --- | --- |
| **设置** | key、值 | 设置请求里的字段,例如 `request.body.model = provider/model``request.body.temperature = 0.2`。 |
| **删除** | key | 删除请求字段。删除 `request.header.x-test` 会移除对应 Header删除 `request.body.foo` 会移除 body 字段。 |
| **追加到数组** | key、值 | 把值追加到目标数组末尾。目标不是数组时按空数组开始。 |
| **插入到数组开头** | key、值 | 把值插到目标数组开头。 |
| **从数组移除** | key、值 | 从目标数组中移除等于该值的元素。 |
| **替换数组元素** | key、匹配值、值 | 把数组中匹配 **匹配值** 的元素替换为新值。 |
Rewrite 的值也会按字面量解析,所以 `0.2` 会变成数字,`true` 会变成布尔值,`{"type":"web_search"}` 会变成对象。只有 `request.body.model` 的值会额外按 CCR 的模型选择器格式规范化。
### 失败时
弹窗底部的 **失败时** 和页面顶部的 **默认失败处理** 是同一套控件。选择 **关闭** 时不降级;选择 **继续重试** 时会出现 **重试次数**;选择 **失败降级目标** 时会出现 **失败降级目标** 输入框和 **添加** 按钮。添加后的目标会以标签形式显示,标签上的上移、下移、移除按钮用于调整降级顺序。
规则命中时会使用这条规则自己的 **失败时** 设置;没有命中的请求才继续使用页面顶部的默认设置。
### 配置示例
| 目标 | 条件来源 | 字段 | 操作符 | 值 | 改写请求参数 |
| --- | --- | --- | --- | --- | --- |
| 按客户端 Header 分流 | `request.header` | `x-client-name` | `==` | `claude-code` | **设置** `request.body.model = 供应商/模型` |
| 按原始模型前缀分流 | `request.body` | `model` | `starts with` | `claude-` | **设置** `request.body.model = 供应商/模型` |
| 按消息内容分流到视觉模型 | `request.body` | `messages` | `contains deep` | `image` | **设置** `request.body.model = 视觉供应商/模型` |
| 删除调试 Header | `request.header` | `x-debug-route` | `==` | `1` | **删除** `request.header.x-debug-route` |
保存后,规则会出现在列表中。请求日志里的 `request model``resolved provider``resolved model` 和路由原因可以用来确认规则是否命中。
## Fallback 处理
Fallback 处理请求失败后的降级。第一次选模型由路由完成当前模型或上游失败时Fallback 决定是否继续尝试。
Fallback 是请求失败后的降级策略。它不负责第一次选模型,而是在当前模型或上游失败时决定是否继续尝试。
路由页面顶部的 **默认失败处理** 是全局 Fallback。每条路由规则里的 **失败时** 是规则级 Fallback当某条规则命中时规则级配置会覆盖全局配置。
@ -162,7 +41,7 @@ Fallback 处理请求失败后的降级。第一次选模型由路由完成;
| 继续重试 | `408``409``429``5xx` |
| 失败降级目标 | 任意 `4xx``5xx` |
进入下一次尝试前CCR 会对每个触发 Fallback 的失败进行等待,包括网络错误。上游提供正数 `Retry-After` 时会优先遵守;否则使用从 1 秒开始、单次最多 30 秒的指数退避。
对于 `429` 限流响应CCR 会在下一次尝试前等待。上游提供 `Retry-After` 时会优先遵守;否则使用从 1 秒开始、单次最多 30 秒的指数退避。
**失败降级目标** 对 `4xx` 也会切换,是因为模型不存在、鉴权或供应商侧拒绝等错误可能只影响当前目标。切换后如果备用模型可用,请求仍然可以成功。
@ -176,7 +55,7 @@ Fallback 处理请求失败后的降级。第一次选模型由路由完成;
2. 如果选择 **继续重试**,填写 `Retries`
3. 如果选择 **失败降级目标**,按优先级添加备用模型。
全局 Fallback 会应用到没有单独配置 Fallback 的规则。
全局 Fallback 会应用到默认路由,以及没有单独配置 Fallback 的规则。
### 规则级失败降级
@ -184,6 +63,24 @@ Fallback 处理请求失败后的降级。第一次选模型由路由完成;
规则级 Fallback 适合高风险或高成本模型。例如:图片任务先走 Fusion 视觉模型,失败后切到另一个多模态模型;复杂任务先走强模型,失败后切到稳定模型。
### 条件路由
当前界面新增规则时主要使用条件路由。条件可以读取请求 Header 或请求 Body
| 来源 | 示例 |
| --- | --- |
| `request.header` | `x-client-name == claude-code` |
| `request.body` | `model starts-with claude-` |
| `request.body` | `messages contains-deep image` |
命中后,请求动作可以设置、删除或修改请求字段。最常用的是:
```text
set request.body.model = 供应商/模型
```
模型也可以是 Fusion 模型。这样路由可以把特定请求导向视觉、搜索或工具增强模型。
## 验证方式
保存后发一次请求,到请求日志里检查:

View file

@ -1,56 +0,0 @@
---
title: 服务配置
pageTitle: 服务配置
eyebrow: 详细配置
lead: 配置 CCR 网关监听地址、端口,以及通过代理模式进行 MITM 劫持并代理到 CCR 的能力。
---
## 先区分管理地址和网关地址
**服务配置** 中的 Host / Port 指模型网关,不是浏览器管理页面:
| 运行方式 | 管理入口 | 模型网关 |
| --- | --- | --- |
| 桌面应用 | 应用窗口 | 默认 `http://127.0.0.1:3456` |
| npm CLI | 默认 `http://127.0.0.1:3458` | 默认 `http://127.0.0.1:3456` |
| Docker | 默认公开入口 `http://127.0.0.1:3458` | 由 Nginx 合并到同一公开入口 |
CLI 的 `--host` / `--port` 配置管理服务本页字段配置模型网关。Docker 的内部管理和网关端口不应单独发布,详见 [Docker 部署](../../guides/docker/)。
## 主字段
| 字段 | 代表的能力 |
| --- | --- |
| Host | CCR 网关监听的主机地址。常见值是 `127.0.0.1``0.0.0.0`。 |
| Port | CCR 网关监听端口。客户端需要把 API Base URL 指向这个端口。 |
Host 使用 `127.0.0.1` 时仅本机可访问;`0.0.0.0` 会监听所有 IPv4 网卡。只有在确实需要局域网或远程访问时才使用通配地址,并同时配置 CCR 客户端 API Key、防火墙 / 私网和 TLS 反向代理。
管理 Token、CCR 客户端 API Key 和上游供应商凭据彼此独立。客户端访问网关时使用 **API 密钥** 页面创建的 CCR Key不要直接暴露上游凭据。
## 启动和验证
1. 至少添加一个供应商和模型。
2. 在 **API 密钥** 页面创建客户端 Key。
3. 点击 **启动****重启**
4. 确认状态显示运行中,并请求网关的 `/health`
5. 发出最小模型请求,再到请求日志核对最终供应商 / 模型。
管理 UI 可访问不代表模型网关已运行。Docker 在网关未启动时会让 `/health` 返回 `502`;桌面版 / CLI 也可能在没有可用模型时只保留管理服务。
## 代理模式
代理模式是本地代理能力。开启后,客户端可以把 HTTP/HTTPS 流量交给 CCRCCR 会通过 MITM 劫持识别和解密 HTTPS 请求,并把可处理的模型请求代理到 CCR 网关链路。
| 字段 | 代表的能力 |
| --- | --- |
| 代理模式 | 启用本地代理能力。开启后 CCR 可以作为 HTTP/HTTPS 代理接收客户端流量,并通过 MITM 劫持把模型请求代理到 CCR。 |
| 系统代理 | 将系统代理指向 CCR。适合让支持系统代理的应用自动经过 CCR。 |
| 捕获网络 | 保存代理模式下经过 CCR 的网络请求,用于“网络”页面查看请求和响应详情。 |
| CA 证书 | 当前代理 CA 证书的信任状态。 |
| 安装 CA | 将 CCR 代理 CA 安装到系统或当前用户信任存储中。不同系统的安装方式不同。 |
| 检查信任 | 重新检测代理 CA 是否已被系统信任。 |
| 代理状态 | 显示代理服务当前是否运行。 |
| 重启代理 | 代理模式开启时,重新启动代理服务。 |
代理模式需要操作本机网络和证书信任,主要面向桌面环境。容器部署通常应把客户端直接指向 CCR 的 Nginx 网关入口,不建议依赖容器修改宿主机系统代理或安装宿主机 CA。

View file

@ -1,167 +0,0 @@
---
title: ToolHub
pageTitle: ToolHub
eyebrow: 详细配置
lead: 将多个 MCP server 收束成一个紧凑入口,让 Agent 按任务懒加载需要的工具,减少工具列表占用的上下文。
---
## 适用场景
当你接入的 MCP server 越来越多时,直接把所有工具暴露给 Agent 会让工具列表变长也更容易选错工具。ToolHub 会向 Agent 暴露一个 `ccr-toolhub` MCP server里面只有两个元工具
- `tool_hub.resolve`:根据用户任务和上下文检索可用 MCP 工具。
- `tool_hub.invoke`:调用已经被本轮任务选中的真实 MCP 工具。
它适合把不常用但偶尔会用到的工具统一懒加载,在任务真正需要时才交给 Agent 使用。核心价值是节省上下文:避免把大量低频工具常驻在 Agent 的工具列表里,减少上下文占用和选错工具的概率。简单本地代码、文件或普通聊天任务通常不需要经过 ToolHub。
## 工作方式
1. 在 **设置 → ToolHub** 中启用 ToolHub。
2. 选择一个已配置模型作为 **检索模型**。它负责阅读 MCP 工具目录并挑选本轮任务需要的工具;建议使用 `deepseek-v4-flash`,或同等 Flash 价位、响应稳定的轻量模型。
3. 添加或导入后端 MCP server。ToolHub 支持 `stdio``streamable-http``sse`
4. 从 CCR 打开 Claude Code 或 Codex。CCR 会在对应 Agent 配置中写入 `ccr-toolhub`
5. Agent 遇到外部服务、已安装 MCP 能力或业务 API 相关请求时,先调用 `tool_hub.resolve`,再用 `tool_hub.invoke` 执行选中的工具。
ToolHub 会合并 **ToolHub 页面配置的 MCP servers** 和兼容旧配置中的全局 Agent MCP servers并自动排除 `ccr-toolhub` 自身,避免递归调用。
## 内置浏览器自动化
在 CCR Desktop 中启用 ToolHub并打开 **内置浏览器自动化** 开关后Agent 可以使用桌面端内置浏览器完成网页操作。不需要在 ToolHub 页面手动添加浏览器后端,也不需要额外 API KeyCCR 会使用本地网关鉴权连接它。
启用步骤:
1. 打开 **设置 → ToolHub**,先开启 **启用 ToolHub**
2. 在同一页打开 **内置浏览器自动化** 开关。该开关只会在 ToolHub 已启用时显示。
3. 保存设置后,从 CCR 重新打开 Claude Code 或 Codex让新的 Agent 实例加载最新配置。
> 已经运行中的 Agent 实例通常不会立即拿到这个开关变化。要让现有会话生效,请重启该 Agent 实例,或使用 Agent 自身能力重启 ToolHub。
内置浏览器自动化适合让 Agent 处理需要真实浏览器状态的任务,例如打开网站、读取页面、填写表单、点击按钮、在页面中滚动,或在没有专用业务能力时完成下单、预约、查询、结账等网页流程。开启后 Agent 可以:
- 打开或附加内置浏览器标签页、导航 URL 或搜索词。
- 读取页面内容,并找到按钮、链接、输入框等页面元素。
- 点击、输入、选择、按键和滚动页面元素。
- 等待页面加载、跳转、弹窗或人类接管结果,再继续后续步骤。
- 在登录、验证码、CAPTCHA、人机验证或人工确认时请求用户接管。
当网页流程需要登录、验证码、CAPTCHA、人机验证或人工确认时CCR 会显示内置浏览器窗口,并在顶部工具栏提示用户需要完成的步骤。用户点击 **Done****Hide**Agent 会收到结果并继续执行。接管等待最长支持 10 分钟。
### Chrome 登录态导入扩展
内置浏览器自动化还支持把系统 Chrome 中指定域名的登录状态导入 CCR 内置浏览器。这样 Agent 处理网页任务时,可以复用你已经在 Chrome 中登录过的网站状态。该能力需要安装仓库里的 Chrome 解包扩展:`extension/chrome`
安装方式:
1. 在 Chrome 打开 `chrome://extensions`
2. 开启 **Developer mode**
3. 点击 **Load unpacked**
4. 选择仓库中的 `extension/chrome` 目录。
导入流程:
1. 当任务需要复用 Chrome 登录状态时Agent 会请求导入;用户也可以在 CCR 内置浏览器工具栏点击钥匙按钮主动发起。
2. CCR 创建一次性导入任务,并打开确认页。如果默认浏览器不是 Chrome请把确认页 URL 复制到已安装扩展的 Chrome 中打开。
3. 用户在确认页检查要导入的域名,点击 **Confirm and Import**
4. Chrome 扩展读取这些域名的 cookies 和 localStorage提交给 CCR完成后 Agent 可以继续使用内置浏览器执行任务。
扩展只读取 CCR 导入任务列出的域名,不会枚举 Chrome 中的全部 cookies。读取 localStorage 时,扩展会临时打开对应 origin 的非激活标签页,读取后自动关闭。若确认页提示扩展没有站点访问权限,请在 Chrome 扩展设置中允许该扩展访问目标域名,然后重新加载解包扩展再重试。
> 注意:内置浏览器自动化依赖 CCR Desktop 的内置浏览器只在桌面端可用。CLI、服务器部署或纯 Web 环境没有这项内置能力,请改用外部浏览器自动化 MCP server。
## 配置项
| 配置项 | 说明 |
| --- | --- |
| 启用 ToolHub | 开启后才会向 Agent 暴露 `ccr-toolhub`。如果没有可用后端 MCP serverCCR 不会生成 ToolHub MCP 配置。 |
| 内置浏览器自动化 | 仅在启用 ToolHub 后显示。开启后让 Agent 可以使用 CCR Desktop 的内置浏览器完成网页操作。 |
| 检索模型 | 从已配置供应商模型中选择。建议使用 `deepseek-v4-flash`,或同等 Flash 价位、响应稳定、工具理解能力足够的轻量模型。 |
| 最大工具数 | 单次解析最多返回的工具数量,范围 `1``20`,默认 `10`。 |
| 超时毫秒 | ToolHub 解析和调用的基础超时时间,范围 `8000``300000`,默认 `60000`。如果后端 MCP server 需要更长 request timeoutCCR 会按后端超时自动抬高实际调用超时。 |
| MCP servers | 后端工具来源。每个 server 需要唯一名称,并配置 transport、命令或 URL、环境变量、headers 和超时。 |
| Import JSON | 导入常见 MCP JSON。支持根对象、数组、`mcpServers``mcp_servers`。 |
## 添加 MCP Server
### stdio
`stdio` 适合本地命令行 MCP server。需要填写
- **Command**:启动命令,例如 `npx``node``python`
- **Arguments**:命令参数。
- **Working directory**:可选工作目录。
- **Stdio message mode**:默认 `content-length`,如果 server 使用逐行 JSON选择 `newline-json`
- **Environment variables**:只放这个 MCP server 需要的变量。
### streamable-http / sse
远程 MCP server 需要填写 URL。鉴权可以使用
- **API key**:直接保存在配置中。
- **API key env**:从环境变量读取。
- **Headers**:添加自定义请求头。
如果远程服务启动慢或请求耗时长,可以单独调高该 server 的 **Startup timeout****Request timeout**
## JSON 示例
桌面 App 的 SQLite 配置是当前生效来源,建议优先通过 UI 修改。下面字段适用于备份、迁移或排查时理解 ToolHub 配置结构:
```json
{
"toolHub": {
"enabled": true,
"browserAutomation": true,
"llm": {
"apiKey": "sk-...",
"baseUrl": "https://api.openai.com/v1",
"model": "gpt-5-mini"
},
"maxTools": 10,
"requestTimeoutMs": 60000,
"mcpServers": [
{
"name": "filesystem",
"transport": "stdio",
"command": "npx",
"args": ["-y", "@modelcontextprotocol/server-filesystem", "/tmp"],
"env": {},
"stdioMessageMode": "content-length",
"requestTimeoutMs": 30000,
"startupTimeoutMs": 600000
}
]
}
}
```
导入 MCP JSON 时也可以使用常见格式:
```json
{
"mcpServers": {
"filesystem": {
"command": "npx",
"args": ["-y", "@modelcontextprotocol/server-filesystem", "/tmp"]
}
}
}
```
## 与 Fusion MCP 的区别
| 能力 | ToolHub | Fusion 自定义 MCP 工具 |
| --- | --- | --- |
| 使用入口 | Agent 侧的 `ccr-toolhub` MCP server | 某个 Fusion 模型内部能力 |
| 工具选择 | 每个任务动态检索并返回工具包 | 模型配置中固定选择工具 |
| 适合场景 | MCP server 很多、工具目录经常变化、希望 Agent 自主发现能力 | 给某个模型补一组明确工具 |
| 可见范围 | 通过 CCR 打开的 Claude Code 或 Codex 配置 | 选择该 Fusion 模型的路由或 Agent |
## 排查
- Agent 看不到 ToolHub确认已启用 ToolHub并且至少配置了一个后端 MCP server 或开启了 **内置浏览器自动化**,然后从 CCR 重新打开 Claude Code 或 Codex。
- 提示缺少检索模型或 API Key**检索模型** 中选择已配置模型,并确认供应商凭据可用。
- Agent 无法使用内置浏览器自动化:确认正在使用 CCR Desktop并且已在 **设置 → ToolHub** 中开启 **内置浏览器自动化**,然后从 CCR 重新打开 Claude Code 或 Codex。CLI、服务器部署或纯 Web 环境没有这项内置能力。
- Chrome 登录态导入确认页一直等待扩展:确认已在 Chrome 中加载 `extension/chrome` 解包扩展,并允许扩展访问要导入的目标域名。如果默认浏览器不是 Chrome请手动把确认页 URL 复制到 Chrome。
- 解析不到工具:检查 MCP server 是否能正常列出工具,工具名称和描述是否足够清楚,必要时提高 **最大工具数**
- 调用超时:分别检查 ToolHub 的 **超时毫秒** 和单个 MCP server 的 request/startup timeout。
- 导入失败:检查 JSON 是否有效、server 名称是否重复、`stdio` 是否有 command远程 transport 是否有 URL。

View file

@ -1,41 +0,0 @@
---
title: 托盘配置
pageTitle: 托盘配置
eyebrow: 详细配置
lead: 配置 CCR 系统托盘图标、余额进度条和托盘窗口组件。
---
## 顶部字段
| 字段 | 代表的能力 |
| --- | --- |
| 托盘小精灵 | 选择托盘图标样式。可选“随机”“晴岚”“暖阳”“星澜”和“余额进度条”。 |
| 余额进度条 | 使用供应商账户用量作为托盘图标进度。需要先为供应商启用“获取用量”。 |
| 账户 | 选择用于余额进度条的供应商账户。 |
| 数据 | 选择账户里的余额、套餐或额度数据作为进度来源。 |
如果没有可用账户数据,页面会提示“暂无可用账户数据,请先为供应商启用账户监控。”这通常意味着还没有供应商开启“获取用量”,或用量读取尚未成功。
## 托盘窗口布局
| 区域 | 代表的能力 |
| --- | --- |
| 组件区 | 左侧组件库,用于添加或启用托盘窗口组件。 |
| 预览 | 中间预览区,展示当前托盘窗口布局。组件可以拖拽排序。 |
| 组件属性 | 右侧配置区,用于调整选中组件的“样式”,或移除当前组件。 |
## 组件类型
| 组件 | 代表的能力 |
| --- | --- |
| 供应商组件 | 显示“供应商切换”,用于在托盘窗口中按供应商查看数据。该组件是单例组件,只能启用一次。 |
| 标题组件 | 显示“标题和状态”。该组件是单例组件,只能启用一次。 |
| 账户组件 | 显示“账户余额”。可以添加多个账户组件,并选择不同样式。 |
| 趋势组件 | 显示“Token 趋势图”。 |
| 活跃度组件 | 显示“Token 活跃度”。 |
| 指标组件 | 显示“Token 指标”。 |
| 构成组件 | 显示“Token 构成”“环形指标”或“模型占比”。 |
## 样式
不同组件支持不同“样式”。常见样式包括“卡片”“紧凑”“列表”“胶囊”“折线图”“面积图”“柱状图”“圆环”“环形图”“仪表盘”“迷你折线”“堆叠”等。样式只影响托盘窗口展示方式,不改变请求路由、供应商配置或用量统计。

View file

@ -7,15 +7,17 @@ lead: 从安装开始,逐步接入供应商、让 Agent 通过 CCR 发请求
## 安装并启动 CCR
CCR 提供三种发行方式桌面应用、Node.js 22+ 的 npm CLI以及 Docker 单入口部署。
### 下载安装
| 方式 | 启动入口 | 默认管理地址 | 默认模型网关 |
| --- | --- | --- | --- |
| 桌面应用 | 应用界面 / `ccr-app` | 应用内窗口 | `http://127.0.0.1:3456` |
| npm CLI | `ccr ui` / `ccr serve` | `http://127.0.0.1:3458` | `http://127.0.0.1:3456` |
| Docker | `docker compose up -d --build` | 与网关共用 `http://127.0.0.1:3458` | 与管理界面共用 Nginx 入口 |
1. 打开 [GitHub Releases](https://github.com/musistudio/claude-code-router/releases) 页面。
2. 按你的系统下载安装包macOS 使用 `.dmg``.zip`Windows 使用 `.exe`Linux 使用 `.AppImage`
3. 像普通桌面软件一样安装并打开 **Claude Code Router**
先阅读[安装页](install/)选择发行方式;完整终端命令见 [CLI 参考](cli/),容器端口、鉴权、持久化和升级见 [Docker 部署](docker/)。
### 启动服务
进入 **服务** 页面,点击 **启动**。页面显示运行中后CCR 会在本机监听默认地址 `http://localhost:8080`
如果希望打开 App 后自动启动服务,可以在服务页面开启自动启动。
## 接入供应商
@ -55,7 +57,7 @@ CCR 提供三种发行方式桌面应用、Node.js 22+ 的 npm CLI以及 D
## 接入 Agent配置
Agent配置让 Claude Code、Codex、Grok CLI、ZCode 等 Agent 使用 CCR 的供应商、路由和模型选择配置。
Agent配置让 Claude Code、Codex、ZCode 等 Agent 使用 CCR 的供应商、路由和模型选择配置。
通用建议:
@ -71,17 +73,13 @@ Agent配置让 Claude Code、Codex、Grok CLI、ZCode 等 Agent 使用 CCR 的
**Agent配置** 中选择 Codex确认供应商 ID、供应商名称、模型和配置文件。需要特定 CLI 时再填写 Codex CLI path 和 Codex home。
### Grok CLI
选择 Grok CLI 并设置默认模型,然后运行复制出的 `ccr-app <配置名称>` 命令。即使 CCR Desktop 网关尚未运行,该命令也会启动一个可共享的临时网关服务;并发 Grok 会话会共同保持服务运行直到最后一个会话退出。CCR 会把 Grok 的模型发现和推理请求指向本地网关;进入 Grok 后可以用 `/model` 切换 CCR 模型。
### ZCode
ZCode 主要关注模型、供应商 ID、供应商名称以及是否从 CCR 启动。它走 App surface不需要 Codex CLI 的路径字段。
### 复用本机已登录的 Agent
如果本机已经登录过 Claude Code、Codex、Grok CLI 或 ZCode可以在 **供应商** 中导入为 **本机 Agent 供应商**,复用已有授权,不必额外申请 Key。
如果本机已经登录过 Claude Code、Codex 或 ZCode可以在 **供应商** 中导入为 **本机 Agent 供应商**,复用已有授权,不必额外申请 Key。
## 日志&观测

View file

@ -2,7 +2,7 @@
title: 接入 Agent配置
pageTitle: 接入 Agent配置
eyebrow: 快速开始
lead: 让 Claude Code、Codex、Grok CLI、ZCode 等 Agent 使用 CCR 的供应商、路由和模型选择配置。
lead: 让 Claude Code、Codex、ZCode 等 Agent 使用 CCR 的供应商、路由和模型选择配置。
---
## 通用建议
@ -23,14 +23,10 @@ lead: 让 Claude Code、Codex、Grok CLI、ZCode 等 Agent 使用 CCR 的供应
需要特定 CLI 时再填写 Codex CLI path 和 Codex home。
## Grok CLI
选择 Grok CLI、设置模型然后运行复制出的 `ccr-app <配置名称>` 命令。CCR Desktop 网关尚未运行时,该命令会启动一个可共享的临时网关服务,并保持运行到最后一个并发 Grok 会话退出。进入 Grok 后可以使用 `/model` 切换 CCR 暴露的模型。
## ZCode
ZCode 主要关注模型、供应商 ID、供应商名称以及是否从 CCR 启动。它走 App surface不需要 Codex CLI 的路径字段。
## 复用本机已登录的 Agent
如果本机已经登录过 Claude Code、Codex、Grok CLI 或 ZCode可以在 **供应商** 中导入为 **本机 Agent 供应商**,复用已有授权,不必额外申请 Key。
如果本机已经登录过 Claude Code、Codex 或 ZCode可以在 **供应商** 中导入为 **本机 Agent 供应商**,复用已有授权,不必额外申请 Key。

View file

@ -1,220 +0,0 @@
---
title: CLI 安装与命令参考
pageTitle: CLI 安装与命令参考
eyebrow: 快速开始
lead: 使用 npm 版 CCR 在开发机或无桌面服务器上运行管理界面、模型网关,并按 Agent 配置启动本机工具。
---
## CLI 与桌面版命令的区别
CCR 有两个相关命令:
| 命令 | 来源 | 主要用途 |
| --- | --- | --- |
| `ccr` | npm 包 `@musistudio/claude-code-router` | 不依赖 Electron启动浏览器管理界面、模型网关和 Agent 配置。 |
| `ccr-app` | CCR 桌面应用 | 桌面版生成的配置启动器Agent配置卡片复制的命令使用这个名称。 |
两个发行版会读取同一套本机配置目录,但不要把命令名混用。需要托盘、桌面通知、自动更新和桌面专属浏览器集成时,使用桌面版;需要无桌面部署或由进程管理器托管时,使用 npm CLI。
## 安装、升级与卸载
CLI 要求 Node.js 22 或更高版本:
```sh
node --version
npm install -g @musistudio/claude-code-router
ccr --help
```
升级和卸载:
```sh
npm install -g @musistudio/claude-code-router@latest
npm uninstall -g @musistudio/claude-code-router
```
卸载 npm 包不会删除 CCR 的本地配置和数据库。
如果安装成功但找不到命令,执行 `npm prefix -g`,确认 npm 全局可执行目录已经加入 `PATH`,然后打开一个新终端。
## 第一次启动
在后台启动 CCR 并打开管理界面:
```sh
ccr ui
```
SSH 或无桌面环境使用:
```sh
ccr ui --no-open
```
随后按这个顺序完成配置:
1. 添加供应商和至少一个模型。
2. 在 **API 密钥** 页面创建用于访问网关的 CCR 客户端 Key。
3. 按需要设置默认模型、路由规则和 Fallback。
4. 在 **服务** 页面确认网关已经运行。
5. 把客户端 Base URL 指向界面显示的网关地址。
管理界面默认使用 `http://127.0.0.1:3458`,模型网关默认使用 `http://127.0.0.1:3456`。管理 Token 与 CCR 客户端 Key 是两种独立凭据:前者保护 UI / RPC后者验证模型请求。
## 服务命令总览
| 命令 | 运行方式 | 用途 |
| --- | --- | --- |
| `ccr start` | 后台 | 启动管理服务和模型网关,打印带认证信息的管理 URL。 |
| `ccr ui` | 后台 | 复用或启动后台服务,并打开浏览器。 |
| `ccr stop` | 一次性 | 停止由 `start``ui` 启动的后台服务。 |
| `ccr serve` | 前台 | 在当前终端运行,适合查看日志或交给进程管理器。 |
| `ccr web` | 前台 | `serve` 的别名。 |
| `ccr <配置名称或 ID>` | 前台 | 启动一个已启用的 Agent 配置。 |
## `ccr start`
```text
ccr start [--host <host>] [--port <port>] [--open|--no-open] [--gateway|--no-gateway]
```
| 选项 | 说明 |
| --- | --- |
| `--host <host>` | 管理服务监听地址,默认 `127.0.0.1`。也接受 `--host=value`。 |
| `--port <port>` | 管理服务首选端口,默认 `3458`。也接受 `--port=value`。 |
| `--open` | 启动后打开浏览器。 |
| `--no-open` | 不打开浏览器。 |
| `--gateway` | 明确要求启动模型网关;这是默认行为。 |
| `--no-gateway` | 只启动管理服务,不在启动阶段拉起模型网关。 |
如果首选端口被占用CCR 会继续尝试后续端口并打印实际 URL。端口必须是 `1``65535` 的整数。
## `ccr ui`
```text
ccr ui [--host <host>] [--port <port>] [--open|--no-open] [--gateway|--no-gateway]
```
`ui``start` 使用同一个后台服务,但默认会打开浏览器。管理 URL 包含 `ccr_web_token` 查询参数;请把完整 URL 当作密码,不要粘贴到日志、工单或公开截图。
## `ccr serve`
```text
ccr serve [--host <host>] [--port <port>] [--open|--no-open] [--gateway|--no-gateway]
```
`serve` 留在前台,收到 `SIGINT``SIGTERM` 后关闭管理服务和已配置服务。排查启动错误时优先使用它,因为错误会直接输出到当前终端。
`ccr stop` 只管理后台服务。前台 `serve` 应通过当前终端或外部进程管理器停止。
## 后台服务的复用规则
`start``ui` 会把进程 ID、URL 和私有服务 Token 写入 `service.json`。再次执行时CCR 会先验证对应进程和 RPC 身份:
- 服务有效时直接复用,不会再启动第二个后台进程。
- 新传入的 Host、Port 和 `--no-gateway` 不会重配已经运行的进程。
- 如果新命令要求网关运行CCR 会尝试在现有管理进程中启动网关。
- 状态文件失效或进程已经退出时CCR 会清理旧状态并启动新服务。
需要修改监听参数时先执行:
```sh
ccr stop
ccr start --host 127.0.0.1 --port 3458
```
## 按 Agent 配置启动
先在 **Agent配置** 中创建并启用配置,然后使用:
```text
ccr <配置名称或 ID> [cli|app] [-- <Agent 参数>]
```
示例:
```sh
ccr "Codex - Work"
ccr "Codex - Work" app
ccr "Claude - Review" cli -- --model sonnet
ccr profile-id -- --help
```
规则如下:
- `--cli``--app` 可以替代位置形式的 `cli` / `app`
- Agent 自己的参数放到 `--` 后,避免与 CCR 选项或入口名冲突。
- 省略入口时Claude Code、Codex、Grok CLI 默认使用 CLIZCode 默认使用 App。
- Grok 只支持 CLIZCode 只支持 App。
- Claude App 和 ZCode App 不支持额外 Agent 参数。
- 启动 App 需要本机安装对应桌面应用,并且当前环境有图形会话。
- 只有已启用的配置可以启动。名称产生歧义时使用配置 ID。
大多数配置要求 CCR 网关已经运行。Grok CLI 是例外:如果服务不存在,它可以自动启动一个受管的临时共享服务,并在最后一个 Grok 会话退出后关闭。
## 配置和数据位置
| 平台 | 配置目录 |
| --- | --- |
| macOS / Linux | `~/.claude-code-router` |
| Windows | `%APPDATA%\claude-code-router` |
常见文件和目录:
| 路径 | 用途 |
| --- | --- |
| `config.sqlite` | 当前应用配置。 |
| `app-data/` | API Key、用量、请求日志、证书等运行数据。 |
| `service.json` | 后台 CLI 服务状态和私有 Token。 |
| `gateway.config.json` | 生成的网关运行配置。 |
| `profiles/` | 按 Agent 配置隔离的文件。 |
| `bin/` | CCR 生成的 Agent 启动包装器。 |
不要在 CCR 运行时直接编辑或复制活跃 SQLite 文件。优先使用 **Settings → Export data**;文件级备份前先停止 CLI 和桌面应用。
## 环境变量与远程访问
公开的管理认证变量是:
| 变量 | 说明 |
| --- | --- |
| `CCR_WEB_HOST` | 省略 `--host` 时使用的管理服务监听地址。 |
| `CCR_WEB_PORT` | 省略 `--port` 时使用的管理服务端口。 |
| `CCR_WEB_AUTH_TOKEN` | 固定管理 UI / RPC Token不设置时进程会生成随机 Token。 |
监听到 `0.0.0.0` 会让管理界面进入局域网或外部网络。只有在确实需要时才这样配置,并同时使用固定强 Token、主机防火墙或私网以及可信反向代理提供的 TLS。
模型网关还需要单独创建 CCR 客户端 Key。上游供应商凭据保存在本地数据目录因此目录和备份都应按敏感数据保护。
## 进程管理器示例
生产环境应使用 `ccr serve --no-open`,让外部管理器负责重启和日志。启动命令至少应固定工作用户、`HOME`、监听地址和 `CCR_WEB_AUTH_TOKEN`。不要同时运行由 `ccr start` 创建的后台服务,否则可能得到两个管理端口或竞争同一套配置。
## 常见问题
### UI 能打开,但 `/health` 或模型请求失败
管理服务可以在没有可用模型网关时运行。添加供应商和模型、创建 CCR 客户端 Key然后从 **服务** 页面启动或重启网关。使用 `ccr serve` 查看启动错误。
### 实际管理端口不是 3458
3458 已被占用CCR 使用了后续可用端口。以命令打印的 URL 为准;需要固定端口时,先停止冲突进程。
### 找不到 Agent 配置
确认配置已启用并检查名称是否重复。CCR 会按 ID、名称、忽略大小写的名称和清理后的名称匹配多个结果时必须使用 ID。
### 提示启动器不存在
先打开一次 CCR 或重新保存该 Agent 配置,让 CCR 重新生成 `bin/` 下的启动包装器。
### 后台服务无法停止
先运行 `ccr stop`。如果状态文件已经失效,命令会清理它并报告服务未运行。前台 `ccr serve` 不受 `ccr stop` 管理,应回到对应终端或进程管理器停止。
## 相关页面
- [安装并启动 CCR](../install/)
- [Agent配置](../../configuration/profile/)
- [服务配置](../../configuration/server/)
- [Docker 部署](../docker/)

View file

@ -1,297 +0,0 @@
---
title: Docker 部署
pageTitle: Docker 部署
eyebrow: 快速开始
lead: 使用 Nginx 单入口运行 CCR Core 和浏览器管理界面,并正确处理端口、鉴权、持久化、远程访问、备份和升级。
---
## 适用范围与限制
Docker 镜像适合常驻模型网关和浏览器管理。它包含 CCR Core、构建后的管理 UI、PM2 和 Nginx但不包含
- Electron 桌面应用、系统托盘和桌面通知;
- npm 发行版的 `ccr` 命令;
- 从容器中启动宿主机 Claude App、ChatGPT、ZCode 等桌面 App
- 桌面自动更新和桌面专属的内置浏览器集成。
如果主要需求是本机 Agent 多开、托盘或桌面 App 启动,请使用桌面版;如果需要终端命令但不需要容器,请使用 [CLI](../cli/)。
## 进程和端口拓扑
```text
宿主机 3458 -> 容器 Nginx 8080
|-> 静态管理 UI
|-> 管理 RPC127.0.0.1:3459
|-> 模型网关127.0.0.1:3456
`-> Core Runtime127.0.0.1:3457
```
只应发布 Nginx 的容器端口 `8080``3459``3456``3457` 都是容器内部实现端口,不要分别映射到宿主机。
Nginx 对外提供:
| 路径 | 用途 |
| --- | --- |
| `/``/pages/home/index.html` | 管理 UI。根路径会跳转到带管理 Token 的页面。 |
| `/api/ccr/rpc` | 需要管理 Token 的管理 RPC。 |
| `/health` | 模型网关健康状态,不是容器或 UI 健康状态。 |
| `/v1/*``/v1beta/*``/messages``/chat/completions``/responses``/interactions``/mcp/*` | 模型和 MCP 网关接口。 |
## 使用 Compose 快速启动
在仓库根目录执行:
```sh
docker compose up -d --build
docker compose logs -f ccr
```
打开 <http://127.0.0.1:3458>。新数据卷上管理 UI 会立即可用;模型网关要在添加供应商和模型后才能正常启动。
首次配置顺序:
1. 添加供应商和至少一个模型。
2. 在 **API 密钥** 页面创建 CCR 客户端 Key。
3. 在 **服务** 页面启动网关。
4. 请求 `/health`,确认返回 `200` 和运行状态。
5. 把客户端 Base URL 指向 `http://127.0.0.1:3458`,并使用刚创建的 CCR 客户端 Key。
停止或移除容器不会自动删除命名卷:
```sh
docker compose stop
docker compose down
```
不要给 `docker compose down` 添加 `--volumes`,除非你明确要删除全部 CCR 数据。
## 只允许本机访问
仓库默认映射 `3458:8080` 会监听宿主机所有网卡。如果只从当前机器访问,修改为:
```yaml
services:
ccr:
ports:
- "127.0.0.1:3458:8080"
```
端口映射左侧是宿主机地址和端口,右侧是 Nginx 容器端口。不要把右侧改为内部网关的 `3456`
## 使用 `docker run`
不使用 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
```
仓库也提供 `npm run docker:build``npm run docker:run`。后者使用 `3458``ccr-data`,但容器带 `--rm`,没有固定名称和自动重启策略,更适合临时验证。
## 三类凭据不要混用
| 凭据 | 用途 | 配置位置 |
| --- | --- | --- |
| `CCR_WEB_AUTH_TOKEN` | 管理 UI / RPC 鉴权 | 容器环境变量 |
| CCR 客户端 API Key | 模型网关请求鉴权 | UI 的 **API 密钥** 页面 |
| 上游供应商凭据 | CCR 调用模型供应商 | UI 的 **供应商** 页面 |
不设置 `CCR_WEB_AUTH_TOKEN`EntryPoint 每次启动容器都会生成新的随机 Token。打开根地址仍可工作因为 Nginx 会跳转到包含当前 Token 的 URL但持久部署和远程部署应固定一个足够长的强 Token。
不要把 Token 直接写进 Shell 历史。可以创建不进入版本控制的环境文件:
```dotenv
CCR_WEB_AUTH_TOKEN=replace-with-a-long-random-value
CCR_PUBLIC_BASE_URL=http://127.0.0.1:3458
```
通过 `docker run --env-file` 使用,或把同名变量映射到 Compose 服务的 `environment`。包含 `ccr_web_token` 的完整管理 URL 也应按密码保护,因为它可能出现在浏览器历史、反向代理日志、截图和工单中。
## 修改外部端口或地址
宿主机对外地址与容器内部端口是两层配置。修改宿主机端口时,还要把 `CCR_PUBLIC_BASE_URL` 设置为客户端真实使用的完整地址:
```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` 会同步到 CCR 的公开 Router Endpoint。它本身不会发布 Docker 端口,也不会改变 Nginx 监听地址。
## 域名、HTTPS 与反向代理
由反向代理或 Ingress 终止 TLS 时:
```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}
```
反向代理应把全部路径交给 CCR Nginx并满足
- 支持长时间模型请求;
- 不缓冲 SSE 和流式模型响应;
- 允许足够的请求体大小;
- 只有反向代理入口对外公开,宿主机 `3458` 保持仅本机监听;
- 配合防火墙、VPN / 私网或额外访问控制,避免管理界面直接暴露到不可信网络。
## 持久化目录
EntryPoint 会设置 `HOME=/data`,实际数据位于:
```text
/data/.claude-code-router/
├── config.sqlite
├── gateway.config.json
├── app-data/
│ ├── api-keys.sqlite
│ ├── request-logs.sqlite
│ ├── usage.sqlite
│ └── certs/
├── profiles/
└── bin/
```
优先使用命名卷。Bind Mount 目录必须允许容器写入,而且不能让两个运行中的 CCR 容器共享同一份数据。
全新数据目录中既没有 `config.json` 也没有 `config.sqlite`EntryPoint 默认写入最小的旧格式 `config.json` 作为首次引导。UI 保存后 SQLite 成为权威配置。每次启动默认还会把 JSON / SQLite 中的网关监听字段和 `routerEndpoint` 同步到当前 Docker 公开地址。
## 备份与恢复
应用级备份优先使用 **Settings → Export data**。做完整文件备份时,先停止写入:
```sh
docker compose stop ccr
docker compose cp ccr:/data/. ./ccr-data-backup/
docker compose start ccr
```
备份包含供应商凭据、CCR 客户端 Key并可能包含请求 / 响应数据,必须按敏感数据保存。
完整恢复时,应把备份复制到新的空卷或空 `/data` 目录,并确保容器已停止。不要把旧备份直接覆盖到仍有新数据的活动目录,否则旧 SQLite WAL / SHM 和新运行文件可能混合。替换现有数据前再做一份备份。
## 升级与回滚
先备份 `/data`,再更新源码、刷新基础镜像并重建:
```sh
git pull
docker compose build --pull
docker compose up -d
docker compose ps
docker compose logs --tail=200 ccr
```
升级会对持久化数据执行当前版本需要的迁移。回滚时应同时使用旧镜像 / 旧源码和升级前备份,不要假设旧版本一定能读取新版本数据库。
## 环境变量完整参考
一般部署只需要设置 `CCR_WEB_AUTH_TOKEN``CCR_PUBLIC_BASE_URL` 和 Docker Port Mapping。内部监听变量通常不需要修改。
| 变量 | 默认值 | 说明 |
| --- | --- | --- |
| `CCR_WEB_AUTH_TOKEN` | 每次启动随机生成 | 管理 UI / RPC Token。持久或远程部署应设置固定强值。 |
| `CCR_PUBLIC_BASE_URL` | `http://127.0.0.1:3458` | 写入 CCR 配置的完整公开地址;设置后优先于 Public Host / Port。 |
| `CCR_PUBLIC_HOST` | `127.0.0.1` | 仅在没有完整公开 URL 时用于拼接公开地址,不会改变 Docker 端口绑定。 |
| `CCR_PUBLIC_PORT` | `3458` | 仅在没有完整公开 URL 时用于拼接公开地址。 |
| `CCR_DATA_DIR` | `/data` | 数据根目录,同时作为进程 `HOME`。 |
| `CCR_NGINX_PORT` | `8080` | Nginx 容器内监听端口,应与 Port Mapping 右侧一致。 |
| `CCR_WEB_HOST` | `127.0.0.1` | 管理服务容器内监听地址。 |
| `CCR_WEB_PORT` | `3459` | 管理服务容器内端口。 |
| `CCR_GATEWAY_HOST` | `127.0.0.1` | 模型网关容器内监听地址。 |
| `CCR_GATEWAY_PORT` | `3456` | Nginx 转发到的模型网关容器内端口。 |
| `CCR_GATEWAY_CORE_PORT` | `3457` | Core Gateway Runtime 容器内端口。 |
| `CCR_NO_GATEWAY` | `0` | 设为 `1``true``yes` 时,启动阶段只运行管理 UI。 |
| `CCR_DOCKER_INIT_CONFIG` | `1` | 设为 `0` 时禁用首次最小 `config.json` 引导。 |
| `CCR_DOCKER_SYNC_PUBLIC_ENDPOINT` | `1` | 设为 `0` 时不再在启动时同步已有 JSON / SQLite 的监听和公开地址字段。 |
修改内部端口需要同时保证 PM2 和 Nginx 变量一致,正常部署没有收益。对外仍然只发布 `CCR_NGINX_PORT`
## 构建和烟雾测试
默认使用 `node:22-bookworm` 构建原生依赖,再把生产依赖和构建产物复制到 `node:22-bookworm-slim`。需要替换基础镜像时:
```sh
docker build \
--build-arg NODE_IMAGE=node:22-bookworm \
--build-arg RUNTIME_NODE_IMAGE=node:22-bookworm-slim \
-t claude-code-router:local .
```
运行 Docker 烟雾测试:
```sh
npm run test:docker
```
测试会创建临时容器和数据卷,检查单一 Nginx 端口、UI / RPC 鉴权、公开地址迁移、网关启动和 `/health`,最后自动清理。使用 `CCR_DOCKER_TEST_SKIP_BUILD=1` 复用已有镜像,或通过 `CCR_DOCKER_TEST_IMAGE` 指定本地 Tag。
## 日常运维命令
```sh
docker compose ps
docker compose logs -f ccr
docker compose restart ccr
docker compose config
```
`docker compose ps` 显示的是容器健康;`/health` 显示的是模型网关健康。两者不能互相替代。
## 常见问题
### 根地址返回 `302`
这是正常行为。Nginx 正在把根地址跳转到带 URL 编码管理 Token 的页面。
### `/health` 返回 `502`
它检查模型网关,不检查 Nginx 或 UI。新数据卷尚未配置供应商 / 模型时会返回 `502`。先打开 UI 完成配置并启动网关。
### 修改 Token 后 UI 返回 `401`
重新打开不带参数的根地址,让 Nginx 生成包含新 Token 的 URL关闭仍使用旧 `ccr_web_token` 的标签页和书签。
### 客户端仍使用旧端口或域名
更新 `CCR_PUBLIC_BASE_URL` 并重新创建容器。保持 `CCR_DOCKER_SYNC_PUBLIC_ENDPOINT=1`,让已有 SQLite 配置在启动时同步。
### 重建后配置消失
确认 `/data` 仍挂载同一个命名卷或 Bind Mount。`docker compose down` 保留卷,`docker compose down --volumes` 删除卷。
### Bind Mount 权限错误
确认宿主机目录存在、容器可写且没有只读挂载。命名卷通常可以避免宿主机 UID、所有权和安全标签问题。
### 容器健康,但模型请求失败
容器健康只代表 Nginx / UI 可访问。继续检查 **服务** 状态、供应商连通性、CCR 客户端 Key、路由和请求日志并查看
```sh
docker compose logs --tail=200 ccr
```
## 相关页面
- [安装并启动 CCR](../install/)
- [CLI 安装与命令参考](../cli/)
- [服务配置](../../configuration/server/)
- [API 密钥](../../configuration/api-keys/)

View file

@ -2,66 +2,17 @@
title: 安装并启动 CCR
pageTitle: 安装并启动 CCR
eyebrow: 快速开始
lead: 根据桌面版、npm CLI 或 Docker 的运行场景选择安装方式,并确认管理界面与模型网关的不同地址
lead: 下载桌面应用,安装后启动本地 CCR 服务
---
## 选择发行方式
| 方式 | 适合场景 | 入口 | 默认管理地址 | 默认网关地址 |
| --- | --- | --- | --- | --- |
| 桌面应用 | 日常本机使用、托盘、多开 Agent App、桌面集成 | 应用界面、`ccr-app` | 应用内窗口 | `http://127.0.0.1:3456` |
| npm CLI | 终端、SSH、无 Electron 环境、进程管理器 | `ccr` | `http://127.0.0.1:3458` | `http://127.0.0.1:3456` |
| Docker | 常驻服务器、容器运维、统一浏览器入口 | Nginx | 与网关共用公开地址 | `http://127.0.0.1:3458`(默认端口映射) |
管理 UI 地址和模型网关地址在桌面版 / CLI 中不是同一个端口。不要把 CLI 的管理端口 `3458` 当成默认模型网关端口Docker 才通过 Nginx 把两者合并到同一公开入口。
## 安装桌面应用
## 下载安装
1. 打开 [GitHub Releases](https://github.com/musistudio/claude-code-router/releases) 页面。
2. 按系统下载macOS 使用 `.dmg``.zip`Windows 使用 `.exe`Linux 使用 `.AppImage`
3. 安装并打开 **Claude Code Router**
4. 添加供应商和模型,在 **API 密钥** 中创建客户端 Key然后从 **服务** 页面点击 **启动**
2. 按你的系统下载安装包macOS 使用 `.dmg``.zip`Windows 使用 `.exe`Linux 使用 `.AppImage`
3. 像普通桌面软件一样安装并打开 **Claude Code Router**
页面显示运行中后,模型网关默认监听 `http://127.0.0.1:3456`。需要打开应用时自动启动网关,可在 **服务** 页面开启自动启动。
## 启动服务
## 安装 npm CLI
进入 **Server** 页面,点击 **Start**。页面显示 Running 后CCR 会在本机监听默认地址 `http://localhost:8080`
要求 Node.js 22 或更高版本:
```sh
npm install -g @musistudio/claude-code-router
ccr ui
```
`ccr ui` 会启动后台服务并打开浏览器。无桌面环境使用 `ccr ui --no-open`,生产前台托管使用 `ccr serve --no-open`。完整命令和 Profile 启动说明见 [CLI 安装与命令参考](../cli/)。
## 使用 Docker
在源码仓库根目录执行:
```sh
docker compose up -d --build
```
打开 <http://127.0.0.1:3458>。Docker 只发布 Nginx 单入口,管理 UI 和模型网关共用该地址。首次启动后仍需添加供应商 / 模型、创建 CCR 客户端 Key并从 **服务** 页面启动网关。端口、鉴权、持久化、备份和远程部署见 [Docker 部署](../docker/)。
## 验证安装
完成供应商、模型和 CCR 客户端 Key 配置后:
1. 在 **服务** 页面确认状态为运行中。
2. 请求当前部署的 `/health`;成功时应返回 `200` 和运行状态。
3. 用 CCR 客户端 Key 向兼容路径发送一个最小模型请求。
4. 在 **日志** 页面确认请求模型、最终供应商 / 模型、状态码和耗时。
管理界面能打开并不代表模型网关已经可用。没有供应商 / 模型时Docker 的 `/health` 返回 `502` 属于预期行为。
## 数据位置
| 方式 | 配置位置 |
| --- | --- |
| 桌面 / CLImacOS、Linux | `~/.claude-code-router` |
| 桌面 / CLIWindows | `%APPDATA%\claude-code-router` |
| Docker | `/data/.claude-code-router`,应持久化挂载 `/data` |
CCR 当前配置存储在 `config.sqlite` 中;`config.json` 只在没有 SQLite 配置时作为旧版迁移或 Docker 首次引导来源。不要在 CCR 运行时直接编辑 SQLite。
如果希望打开 App 后自动启动服务,可以在 Server 页面开启 **Auto start**

View file

@ -5,6 +5,19 @@ eyebrow: 产品文档
lead: 了解 CCR 的定位、能力边界和文档结构。需要动手配置时从顶部的「快速开始」开始需要查字段、Bot 或 Fusion 时,进入「详细配置」。
---
## CCR 能帮你做什么
**Claude Code RouterCCR是本地运行的模型网关。** 它位于 Claude Code、Codex、ZCode 等 Agent 和上游模型服务之间统一管理模型、API Key、路由规则、日志观测和 Bot 接力。
CCR 适合解决这些问题:
- 你不想在每个 Agent 里重复维护模型和 Key。
- 你希望不同任务自动走不同模型:轻量后台任务用快模型,复杂任务用强模型,看图或联网任务走 Fusion。
- 你需要在请求日志里看到请求实际去了哪个供应商、哪个模型、是否成功、延迟和成本大概是多少。
- 你想把长时间运行的 Agent 消息转发到 Slack、Telegram、飞书、企业微信等 IM 平台。
CCR 默认监听本机地址 `http://localhost:8080`。Agent 只要指向这个地址,请求就可以被 CCR 接管并按路由规则转发到上游供应商。
## 文档结构
顶部栏现在对应四个独立页面:
@ -12,8 +25,8 @@ lead: 了解 CCR 的定位、能力边界和文档结构。需要动手配置时
| 分类 | 内容 |
| --- | --- |
| [文档](./) | 产品定位、架构概览、阅读路径 |
| [快速开始](guides/) | 桌面版、CLI、Docker 安装部署,以及供应商和 Agent 接入流程 |
| [详细配置](configuration/overview/) | 概览仪表盘、API 密钥、服务、供应商、路由、Agent配置、Fusion、Bot、托盘和配置数据库位置 |
| [快速开始](guides/) | 从安装、接供应商,到接入 Agent 的上手流程 |
| [详细配置](configuration/) | 供应商、路由、配置、Fusion、Bot 和配置文件位置 |
| [Q&A](troubleshooting/) | 请求日志、观测面板和常见问题 |
Bot 平台教程是「详细配置」分类下的子页面,每个平台有独立页面,方便逐步补齐平台后台字段、回调 URL、签名和 FAQ。
@ -22,10 +35,9 @@ Bot 平台教程是「详细配置」分类下的子页面,每个平台有独
第一次使用时可以从这些页面了解 CCR 的主要流程:
1. 从[安装页](guides/install/)选择桌面版、npm CLI 或 Docker对应细节见 [CLI](guides/cli/) 和 [Docker](guides/docker/) 页面。
2. [快速开始](guides/) 继续覆盖供应商接入和 Agent配置。
3. App 的请求日志页面展示请求是否经过 CCR。
4. [详细配置](configuration/overview/) 覆盖概览仪表盘、API 密钥、服务、供应商、图像、联网搜索、MCP 工具、托盘和 IM 接力。
5. [Q&A](troubleshooting/) 覆盖 401、404、超时、路由不对或 Bot 收不到消息等常见问题。
1. [快速开始](guides/) 覆盖供应商接入和 Agent配置。
2. App 的请求日志页面展示请求是否经过 CCR。
3. [详细配置](configuration/) 覆盖图像、联网搜索、MCP 工具和 IM 接力。
4. [Q&A](troubleshooting/) 覆盖 401、404、超时、路由不对或 Bot 收不到消息等常见问题。
这样文档不会挤在一个长页面里,后续也能按顶部分类逐步扩展。

View file

@ -13,9 +13,9 @@ A: 服务运行状态、Agent 启动方式、配置应用状态和作用范围
### Q: 请求命中了错误模型怎么办?
A: 请求日志会展示 `request model``resolved provider``resolved model`。路由配置页包含规则顺序、匹配条件和 fallback。
A: 请求日志会展示 `request model``resolved provider``resolved model`。路由配置页包含默认路由、规则顺序、匹配条件和 fallback。
### Q: 供应商返回 401 或 403 怎么处理
### Q: 供应商返回 401 或 403 是什么原因
A: 相关字段包括 API Key、凭据启用状态、基础 URL、协议和额外请求头。供应商页面提供模型连通性检查。

View file

@ -15,7 +15,7 @@ export const docsContent = {
navItems: [
{ label: "文档", href: "/", pageKey: "documentation" },
{ label: "快速开始", href: "/guides/", pageKey: "guides" },
{ label: "详细配置", href: "/configuration/overview/", pageKey: "configuration" },
{ label: "详细配置", href: "/configuration/", pageKey: "configuration" },
{ label: "Q&A", href: "/troubleshooting/", pageKey: "troubleshooting" },
],
pages: {
@ -40,8 +40,6 @@ export const docsContent = {
icon: "book",
items: [
"安装并启动 CCR",
"CLI 安装与命令参考",
"Docker 部署",
"接入供应商",
"接入 Agent配置",
"日志&观测",
@ -53,8 +51,6 @@ export const docsContent = {
sidebarChildren: {},
sidebarLinks: {
"安装并启动 CCR": "/guides/install/",
"CLI 安装与命令参考": "/guides/cli/",
"Docker 部署": "/guides/docker/",
: "/guides/provider/",
"接入 Agent配置": "/guides/agent-profile/",
"日志&观测": "/guides/observability/",
@ -64,32 +60,20 @@ export const docsContent = {
configuration: {
sidebarGroups: [
{
label: "主页页面",
label: "详细配置",
icon: "wand",
items: [
"概览仪表盘",
"供应商配置",
"一键导入供应商",
"Agent配置",
"路由配置",
"Fusion 组合模型",
"API 密钥",
"日志&观测",
"服务配置",
"Fusion 组合模型",
"Agent配置",
"扩展机制",
],
active: "概览仪表盘",
},
{
label: "设置页",
icon: "book",
items: [
"ToolHub",
"Bot 与 IM 接力 Agent",
"配置数据库位置",
"托盘配置",
"配置文件位置",
],
active: "",
active: "供应商配置",
},
],
expandableSidebarItems: ["Fusion 组合模型", "Bot 与 IM 接力 Agent"],
@ -108,7 +92,6 @@ export const docsContent = {
],
},
sidebarLinks: {
: "/configuration/overview/",
: "/configuration/provider/",
"一键导入供应商": "/configuration/provider-deeplink/",
: "/configuration/routing/",
@ -117,11 +100,7 @@ export const docsContent = {
: "/configuration/fusion-vision/",
: "/configuration/fusion-web-search/",
"自定义 MCP 工具": "/configuration/fusion-mcp-tool/",
ToolHub: "/configuration/toolhub/",
Agent配置: "/configuration/profile/",
"API 密钥": "/configuration/api-keys/",
: "/configuration/server/",
: "/configuration/tray/",
: "/configuration/extensions/",
"Bot 与 IM 接力 Agent": "/configuration/bot-relay/",
: "/configuration/bot-setup/",
@ -133,7 +112,7 @@ export const docsContent = {
: "/bot-与-im-接力-agent/wecom/",
: "/bot-与-im-接力-agent/feishu/",
: "/bot-与-im-接力-agent/dingtalk/",
: "/configuration/config-file/",
: "/configuration/config-file/",
},
sidebarTargets: {},
},
@ -176,7 +155,7 @@ export const docsContent = {
navItems: [
{ label: "Documentation", href: "/en/", pageKey: "documentation" },
{ label: "Quick Start", href: "/en/guides/", pageKey: "guides" },
{ label: "Detailed Configuration", href: "/en/configuration/overview/", pageKey: "configuration" },
{ label: "Detailed Configuration", href: "/en/configuration/", pageKey: "configuration" },
{ label: "Q&A", href: "/en/troubleshooting/", pageKey: "troubleshooting" },
],
pages: {
@ -201,8 +180,6 @@ export const docsContent = {
icon: "book",
items: [
"Install And Start CCR",
"CLI Installation And Reference",
"Docker Deployment",
"Add A Provider",
"Connect Agent Config",
"Logs & Observability",
@ -214,8 +191,6 @@ export const docsContent = {
sidebarChildren: {},
sidebarLinks: {
"Install And Start CCR": "/en/guides/install/",
"CLI Installation And Reference": "/en/guides/cli/",
"Docker Deployment": "/en/guides/docker/",
"Add A Provider": "/en/guides/provider/",
"Connect Agent Config": "/en/guides/agent-profile/",
"Logs & Observability": "/en/guides/observability/",
@ -225,32 +200,20 @@ export const docsContent = {
configuration: {
sidebarGroups: [
{
label: "Main Pages",
label: "Detailed Configuration",
icon: "wand",
items: [
"Overview Dashboard",
"Provider Config",
"One click import",
"Agent Config",
"Routing Config",
"Fusion Models",
"API Keys",
"Logs & Observability",
"Server",
"Fusion Models",
"Agent Config",
"Extension Mechanism",
],
active: "Overview Dashboard",
},
{
label: "Settings Pages",
icon: "book",
items: [
"ToolHub",
"Bots And IM Agent Relay",
"Config Database Location",
"Tray Configuration",
"Config File Location",
],
active: "",
active: "Provider Config",
},
],
expandableSidebarItems: ["Fusion Models", "Bots And IM Agent Relay"],
@ -259,7 +222,6 @@ export const docsContent = {
"Bots And IM Agent Relay": ["Setup", "Slack", "Discord", "Telegram", "LINE", "Weixin", "WeCom", "Feishu", "DingTalk"],
},
sidebarLinks: {
"Overview Dashboard": "/en/configuration/overview/",
"Provider Config": "/en/configuration/providers/",
"One click import": "/en/configuration/provider-deeplink/",
"Routing Config": "/en/configuration/routing/",
@ -268,11 +230,7 @@ export const docsContent = {
"Built-In Vision": "/en/configuration/fusion-vision/",
"Built-In Web Search": "/en/configuration/fusion-web-search/",
"Custom MCP Tool": "/en/configuration/fusion-mcp-tool/",
ToolHub: "/en/configuration/toolhub/",
"Agent Config": "/en/configuration/profiles/",
"API Keys": "/en/configuration/api-keys/",
Server: "/en/configuration/server/",
"Tray Configuration": "/en/configuration/tray/",
"Extension Mechanism": "/en/configuration/extensions/",
"Bots And IM Agent Relay": "/en/configuration/bots/",
Setup: "/en/configuration/bot-setup/",
@ -284,7 +242,7 @@ export const docsContent = {
WeCom: "/en/relay-agents-in-im-with-bots/wecom/",
Feishu: "/en/relay-agents-in-im-with-bots/feishu/",
DingTalk: "/en/relay-agents-in-im-with-bots/dingtalk/",
"Config Database Location": "/en/configuration/configuration-file/",
"Config File Location": "/en/configuration/configuration-file/",
},
sidebarTargets: {},
},

View file

@ -80,7 +80,7 @@ const resolvedNavItems = navItems.map((item, index) => {
};
});
const homeHref = withBase(locale === "en" ? "/en/" : "/");
const faviconHref = withBase("/ccr-icon.png");
const faviconHref = withBase("/favicon.svg");
const logoSrc = withBase("/logo.png");
const sidebarIcons = {
rocket: Rocket,
@ -177,7 +177,7 @@ const sidebarCloseLabel = locale === "zh" ? "关闭目录" : "Close navigation";
}
})();
</script>
<link rel="icon" href={faviconHref} type="image/png" />
<link rel="icon" href={faviconHref} type="image/svg+xml" />
{
resolvedLanguageOptions.map((option) => (
<link
@ -291,11 +291,6 @@ const sidebarCloseLabel = locale === "zh" ? "关闭目录" : "Close navigation";
<ul class="sidebar-children directory-children">
{section.groups.map((group) => (
<li class="directory-group">
{(section.groups.length > 1 || group.label !== section.label) && (
<div class="directory-group-label">
<span>{group.label}</span>
</div>
)}
<ul>
{group.items.map((item) => {
const isExpandable = item.children.length > 0;
@ -399,21 +394,14 @@ const sidebarCloseLabel = locale === "zh" ? "关闭目录" : "Close navigation";
</h2>
<nav>
{
tocItems.map((item, index) => {
const tocItem = typeof item === "string"
? { label: item, href: `#section-${index + 1}`, depth: 2 }
: { depth: 2, ...item };
const depth = Math.min(Math.max(Number(tocItem.depth), 2), 6);
return (
<a
class:list={[`toc-depth-${depth}`, { active: index === 0 }]}
href={tocItem.href}
>
{tocItem.label}
</a>
);
})
tocItems.map((item, index) => (
<a
class:list={{ active: index === 0 }}
href={typeof item === "string" ? `#section-${index + 1}` : item.href}
>
{typeof item === "string" ? item : item.label}
</a>
))
}
</nav>
</div>

View file

@ -1,17 +1,6 @@
---
const target = "/configuration/overview/";
import DocPage from "../components/DocPage.astro";
import * as doc from "../content/docs/zh/configuration.md";
---
<html lang="zh-CN">
<head>
<meta charset="utf-8" />
<meta http-equiv="refresh" content={`0;url=${target}`} />
<link rel="canonical" href={target} />
<script is:inline>
window.location.replace("/configuration/overview/");
</script>
</head>
<body>
<a href={target}>概览仪表盘</a>
</body>
</html>
<DocPage locale="zh" pageKey="configuration" doc={doc} />

View file

@ -4,8 +4,6 @@ import { configurationSlugFromPath, zhConfigurationDocs } from "../../configurat
export function getStaticPaths() {
const activeLabels: Record<string, string> = {
"api-keys": "API 密钥",
overview: "概览仪表盘",
provider: "供应商配置",
"provider-deeplink": "一键导入供应商",
routing: "路由配置",
@ -15,13 +13,10 @@ export function getStaticPaths() {
"fusion-vision": "内置图像能力",
"fusion-web-search": "内置联网搜索",
"fusion-mcp-tool": "自定义 MCP 工具",
toolhub: "ToolHub",
extensions: "扩展机制",
server: "服务配置",
tray: "托盘配置",
"bot-relay": "Bot 与 IM 接力 Agent",
"bot-setup": "配置步骤",
"config-file": "配置数据库位置",
"config-file": "配置文件位置",
};
return Object.entries(zhConfigurationDocs).map(([filePath, mod]) => {

View file

@ -1,17 +1,6 @@
---
const target = "/en/configuration/overview/";
import DocPage from "../../components/DocPage.astro";
import * as doc from "../../content/docs/en/configuration.md";
---
<html lang="en">
<head>
<meta charset="utf-8" />
<meta http-equiv="refresh" content={`0;url=${target}`} />
<link rel="canonical" href={target} />
<script is:inline>
window.location.replace("/en/configuration/overview/");
</script>
</head>
<body>
<a href={target}>Overview Dashboard</a>
</body>
</html>
<DocPage locale="en" pageKey="configuration" doc={doc} />

View file

@ -4,8 +4,6 @@ import { configurationSlugFromPath, enConfigurationDocs } from "../../../configu
export function getStaticPaths() {
const activeLabels: Record<string, string> = {
"api-keys": "API Keys",
overview: "Overview Dashboard",
providers: "Provider Config",
"provider-deeplink": "One click import",
routing: "Routing Config",
@ -15,13 +13,10 @@ export function getStaticPaths() {
"fusion-vision": "Built-In Vision",
"fusion-web-search": "Built-In Web Search",
"fusion-mcp-tool": "Custom MCP Tool",
toolhub: "ToolHub",
extensions: "Extension Mechanism",
server: "Server",
tray: "Tray Configuration",
bots: "Bots And IM Agent Relay",
"bot-setup": "Setup",
"configuration-file": "Config Database Location",
"configuration-file": "Config File Location",
};
return Object.entries(enConfigurationDocs).map(([filePath, mod]) => {

View file

@ -5,8 +5,6 @@ import { enGuideDocs, sectionSlugFromPath } from "../../../section-docs";
export function getStaticPaths() {
const activeLabels: Record<string, string> = {
install: "Install And Start CCR",
cli: "CLI Installation And Reference",
docker: "Docker Deployment",
provider: "Add A Provider",
"agent-profile": "Connect Agent Config",
observability: "Logs & Observability",

View file

@ -5,8 +5,6 @@ import { sectionSlugFromPath, zhGuideDocs } from "../../section-docs";
export function getStaticPaths() {
const activeLabels: Record<string, string> = {
install: "安装并启动 CCR",
cli: "CLI 安装与命令参考",
docker: "Docker 部署",
provider: "接入供应商",
"agent-profile": "接入 Agent配置",
observability: "日志&观测",

View file

@ -642,26 +642,28 @@ pre {
}
.directory-group {
margin: 8px 0 14px;
margin: 2px 0 9px;
}
.directory-group:last-child {
margin-bottom: 4px;
}
.directory-group + .directory-group {
margin-top: 20px;
.directory-group-label {
display: flex;
align-items: center;
gap: 8px;
min-height: 28px;
padding: 7px 8px 3px 0;
color: var(--sidebar-heading);
font-size: 12px;
font-weight: 720;
line-height: 1.35;
}
.directory-group-label {
display: block;
min-height: 20px;
padding: 2px 8px 5px 0;
color: var(--sidebar-subtle);
font-size: 11px;
font-weight: 760;
letter-spacing: 0.04em;
line-height: 1.3;
.directory-group-label .group-icon {
width: 14px;
height: 14px;
}
.sidebar-group h2 {
@ -766,7 +768,7 @@ pre {
}
.sidebar-directory .sidebar-children {
padding-left: 18px;
padding-left: 24px;
}
.sidebar-children::before {
@ -784,14 +786,6 @@ pre {
left: 8px;
}
.sidebar-directory .directory-children {
padding-left: 13px;
}
.sidebar-directory .directory-children::before {
display: none;
}
.sidebar-details[open] > .sidebar-children {
margin-bottom: 7px;
}
@ -853,8 +847,6 @@ pre {
h1,
h2,
h3,
h4,
h5,
p {
overflow-wrap: anywhere;
}
@ -964,24 +956,6 @@ h1 {
font-weight: 720;
}
.doc-article h4,
.doc-markdown > h4 {
margin: 24px 0 8px;
color: var(--heading);
font-size: 15px;
line-height: 1.45;
font-weight: 700;
}
.doc-article h5,
.doc-markdown > h5 {
margin: 18px 0 6px;
color: var(--heading);
font-size: 14px;
line-height: 1.45;
font-weight: 680;
}
.doc-markdown > ul,
.doc-markdown > ol {
margin: 0 0 22px;
@ -1306,36 +1280,6 @@ h1 {
--provider-brand-3: #d9e3f1;
}
.doc-markdown a.provider-import-button.provider-runapi {
--provider-brand: #070707;
--provider-brand-2: #747474;
--provider-brand-3: #f3f3f3;
}
.doc-markdown a.provider-import-button.provider-teamorouter {
--provider-brand: #0c0c0f;
--provider-brand-2: #555b64;
--provider-brand-3: #f4f4f5;
}
.doc-markdown a.provider-import-button.provider-unity2 {
--provider-brand: #050505;
--provider-brand-2: #7a7f85;
--provider-brand-3: #f4f5f6;
}
.doc-markdown a.provider-import-button.provider-code0 {
--provider-brand: #101214;
--provider-brand-2: #267dff;
--provider-brand-3: #d8e8ff;
}
.doc-markdown a.provider-import-button.provider-claudeapi {
--provider-brand: #0d1b2a;
--provider-brand-2: #2f8f83;
--provider-brand-3: #d7fff8;
}
.doc-markdown a.provider-import-button.provider-deepseek {
--provider-brand: #173aa8;
--provider-brand-2: #4e69ff;
@ -1378,18 +1322,6 @@ h1 {
--provider-brand-3: #e4ddff;
}
.doc-markdown a.provider-import-button.provider-moonshot-global {
--provider-brand: #0f1f42;
--provider-brand-2: #4f7cff;
--provider-brand-3: #dce7ff;
}
.doc-markdown a.provider-import-button.provider-kimi-coding {
--provider-brand: #111237;
--provider-brand-2: #5c6bff;
--provider-brand-3: #dfe4ff;
}
.doc-markdown a.provider-import-button.provider-bailian {
--provider-brand: #5a2300;
--provider-brand-2: #ff6a00;
@ -1608,7 +1540,6 @@ pre code span {
}
.toc a {
--toc-dot-left: 0px;
position: relative;
display: block;
padding: 5px 0 5px 21px;
@ -1616,25 +1547,6 @@ pre code span {
line-height: 1.35;
}
.toc a.toc-depth-3 {
--toc-dot-left: 12px;
padding-left: 33px;
font-size: 13.5px;
}
.toc a.toc-depth-4 {
--toc-dot-left: 24px;
padding-left: 45px;
font-size: 13px;
}
.toc a.toc-depth-5,
.toc a.toc-depth-6 {
--toc-dot-left: 36px;
padding-left: 57px;
font-size: 12.5px;
}
.toc a.active {
color: var(--green);
font-weight: 720;
@ -1642,7 +1554,7 @@ pre code span {
.toc a.active::before {
position: absolute;
left: var(--toc-dot-left);
left: 0;
top: 13px;
width: 5px;
height: 5px;

View file

@ -5,21 +5,17 @@
"asarUnpack": [
"**/*.node"
],
"electronLanguages": ["en-US", "zh-CN", "zh-TW", "zh_CN", "zh_TW"],
"npmRebuild": true,
"publish": [
{
"provider": "github",
"owner": "musistudio",
"repo": "claude-code-router",
"releaseType": "release"
"repo": "claude-code-router"
}
],
"directories": {
"app": "packages/electron",
"output": "release/${version}"
},
"afterPack": "build/verify-packaged-app.cjs",
"afterAllArtifactBuild": "build/verify-update-metadata.cjs",
"protocols": [
{
@ -29,14 +25,7 @@
],
"files": [
"dist",
"package.json",
"!node_modules/better-sqlite3/deps/**",
"!node_modules/better-sqlite3/src/**",
"!node_modules/better-sqlite3/binding.gyp",
"!node_modules/better-sqlite3/README.md",
"!node_modules/better-sqlite3/docs/**",
"!node_modules/better-sqlite3/benchmark/**",
"!node_modules/better-sqlite3/test/**"
"package.json"
],
"mac": {
"icon": "build/icon.icns",
@ -61,7 +50,6 @@
"artifactName": "Claude-Code-Router_${version}.${ext}"
},
"linux": {
"executableName": "claude-code-router",
"target": ["AppImage"],
"artifactName": "Claude-Code-Router_${version}.${ext}"
},

View file

@ -1,23 +0,0 @@
# CCR Login Import Chrome Extension
This unpacked Chrome extension imports cookies and localStorage for explicitly selected domains into CCR's in-app browser.
## Development install
1. Open `chrome://extensions`.
2. Enable **Developer mode**.
3. Click **Load unpacked**.
4. Select this `extension/chrome` directory.
After changing extension files, click **Reload** for this unpacked extension in `chrome://extensions`.
The confirmation-page flow uses the site access declared in `manifest.json`; it does not request new host permissions from the page click.
## Flow
1. An agent calls CCR's Chrome login import browser tool, or the user clicks the key button in CCR's in-app browser.
2. CCR opens a one-time confirmation page in the system browser.
3. Review the requested domains and click **Confirm and Import**.
The extension reads only the domains listed in the CCR job. It does not enumerate all Chrome cookies.
For localStorage, the extension temporarily opens non-active tabs for the selected origins, reads `localStorage`, then closes those tabs.

View file

@ -1,262 +0,0 @@
chrome.runtime.onMessage.addListener((message, _sender, sendResponse) => {
if (!message || message.type !== "ccr-login-import-confirm") {
return false;
}
runImport(message.importUrl)
.then((result) => sendResponse({ ok: true, result }))
.catch((error) => sendResponse({ error: formatError(error), ok: false }));
return true;
});
async function runImport(importUrl) {
const normalizedImportUrl = normalizeImportUrl(importUrl);
if (!normalizedImportUrl) {
throw new Error("Invalid CCR import URL.");
}
const job = await fetchImportJob(normalizedImportUrl);
const domains = Array.isArray(job.domains) ? job.domains.map(normalizeDomain).filter(Boolean) : [];
if (domains.length === 0) {
throw new Error("CCR import job does not include any domains.");
}
await ensureHostPermissions(domains);
const cookies = await readCookiesForDomains(domains);
const localStorageEntries = await readLocalStorageForDomains(domains, cookies);
if (cookies.length === 0 && localStorageEntries.length === 0) {
throw new Error("No cookies or localStorage entries were found for the selected domains.");
}
return await submitLoginState(normalizedImportUrl, cookies, localStorageEntries, domains);
}
async function fetchImportJob(importUrl) {
const response = await fetch(importUrl, {
headers: {
"x-ccr-login-import": "chrome-extension"
}
});
const body = await response.json().catch(() => ({}));
if (!response.ok) {
throw new Error(body?.error?.message || `CCR import job request failed (${response.status}).`);
}
if (!body.job || body.job.status !== "pending") {
throw new Error(`CCR import job is ${body.job?.status || "unavailable"}.`);
}
return body.job;
}
async function submitLoginState(importUrl, cookies, localStorage, domains) {
const response = await fetch(`${importUrl.replace(/\/+$/, "")}/cookies`, {
body: JSON.stringify({
cookies,
domains,
localStorage,
source: {
browser: "chrome",
extension: chrome.runtime.getManifest().version
}
}),
headers: {
"content-type": "application/json",
"x-ccr-login-import": "chrome-extension"
},
method: "POST"
});
const body = await response.json().catch(() => ({}));
if (!response.ok) {
throw new Error(body?.error?.message || `CCR login import failed (${response.status}).`);
}
return body.result || {};
}
async function readCookiesForDomains(domains) {
const cookies = [];
for (const domain of domains) {
cookies.push(...await chrome.cookies.getAll({ domain }));
}
return dedupeCookies(cookies);
}
async function readLocalStorageForDomains(domains, cookies) {
const origins = localStorageOriginsForDomains(domains, cookies);
const entries = [];
for (const origin of origins) {
const entry = await readLocalStorageForOrigin(origin).catch(() => undefined);
if (entry && Object.keys(entry.items).length > 0) {
entries.push(entry);
}
}
return entries;
}
async function readLocalStorageForOrigin(origin) {
const tab = await chrome.tabs.create({
active: false,
url: `${origin}/`
});
try {
await waitForTabLoad(tab.id, 15000);
const [frameResult] = await chrome.scripting.executeScript({
func: () => {
const items = {};
for (let index = 0; index < window.localStorage.length; index += 1) {
const key = window.localStorage.key(index);
if (key !== null) {
items[key] = window.localStorage.getItem(key) || "";
}
}
return {
items,
origin: window.location.origin
};
},
target: { tabId: tab.id },
world: "MAIN"
});
const result = frameResult?.result;
if (!result || !allowedLocalStorageOrigin(result.origin, origin)) {
return undefined;
}
return result;
} finally {
if (tab.id !== undefined) {
await chrome.tabs.remove(tab.id).catch(() => undefined);
}
}
}
function waitForTabLoad(tabId, timeoutMs) {
return new Promise((resolve) => {
let done = false;
const finish = () => {
if (done) {
return;
}
done = true;
chrome.tabs.onUpdated.removeListener(listener);
clearTimeout(timeout);
resolve();
};
const listener = (updatedTabId, changeInfo) => {
if (updatedTabId === tabId && changeInfo.status === "complete") {
finish();
}
};
const timeout = setTimeout(finish, timeoutMs);
chrome.tabs.onUpdated.addListener(listener);
});
}
function dedupeCookies(cookies) {
const seen = new Set();
const result = [];
for (const cookie of cookies) {
const partitionKey = cookie.partitionKey ? JSON.stringify(cookie.partitionKey) : "";
const key = `${cookie.storeId || ""}\n${cookie.domain}\n${cookie.path}\n${cookie.name}\n${partitionKey}`;
if (!seen.has(key)) {
seen.add(key);
result.push(cookie);
}
}
return result;
}
function localStorageOriginsForDomains(domains, cookies) {
const origins = new Set();
for (const domain of domains) {
if (domain === "localhost" || isIpAddress(domain)) {
origins.add(`http://${domain}`);
origins.add(`https://${domain}`);
} else {
origins.add(`https://${domain}`);
}
}
for (const cookie of cookies) {
const host = normalizeDomain(cookie.domain);
if (!host || !cookie.hostOnly || !domains.some((domain) => host === domain || host.endsWith(`.${domain}`))) {
continue;
}
origins.add(`${cookie.secure === false ? "http" : "https"}://${host}`);
}
return [...origins];
}
function originsForDomains(domains) {
return [...new Set(domains.flatMap((domain) => {
if (domain === "localhost" || isIpAddress(domain)) {
return [
`http://${domain}/*`,
`https://${domain}/*`
];
}
return [
`http://${domain}/*`,
`https://${domain}/*`,
`http://*.${domain}/*`,
`https://*.${domain}/*`
];
}))];
}
async function ensureHostPermissions(domains) {
const origins = originsForDomains(domains);
const granted = await chrome.permissions.contains({ origins });
if (granted) {
return;
}
throw new Error(
[
`CCR Login Import does not have Chrome site access for ${domains.join(", ")}.`,
"Reload the unpacked extension after updating it, then grant the extension site access for the requested domains in Chrome extensions settings."
].join(" ")
);
}
function normalizeImportUrl(value) {
const raw = typeof value === "string" ? value.trim() : "";
if (!raw) {
return "";
}
try {
const url = new URL(raw);
if (!["127.0.0.1", "localhost"].includes(url.hostname)) {
return "";
}
if (!/^\/chrome-import\/jobs\/[^/]+\/?$/.test(url.pathname)) {
return "";
}
url.pathname = url.pathname.replace(/\/+$/, "");
return url.toString();
} catch {
return "";
}
}
function normalizeDomain(value) {
return typeof value === "string"
? value.trim().replace(/^\*\./, "").replace(/^\./, "").toLowerCase()
: "";
}
function allowedLocalStorageOrigin(actualOrigin, requestedOrigin) {
try {
const actual = new URL(actualOrigin);
const requested = new URL(requestedOrigin);
return actual.protocol === requested.protocol && actual.hostname === requested.hostname && actual.port === requested.port;
} catch {
return false;
}
}
function isIpAddress(value) {
return /^\d{1,3}(?:\.\d{1,3}){3}$/.test(value) || value.includes(":");
}
function formatError(error) {
return error instanceof Error ? error.message : String(error);
}

View file

@ -1,46 +0,0 @@
const root = document.getElementById("ccr-chrome-login-import");
const button = document.getElementById("ccr-confirm-import");
const statusElement = document.getElementById("ccr-import-status");
if (root && button && statusElement) {
const importUrl = root.getAttribute("data-import-url") || "";
button.disabled = false;
button.textContent = "Confirm and Import";
setStatus("CCR Login Import extension is connected. Review the domains, then confirm.");
button.addEventListener("click", () => {
void confirmImport(importUrl);
});
}
async function confirmImport(importUrl) {
button.disabled = true;
setStatus("Importing Chrome cookies and localStorage into CCR...");
try {
const response = await chrome.runtime.sendMessage({
importUrl,
type: "ccr-login-import-confirm"
});
if (!response?.ok) {
throw new Error(response?.error || "Chrome login import failed.");
}
const result = response.result || {};
setStatus(
`Imported ${result.cookieImported || 0} cookies and ${result.localStorageImported || 0} localStorage items. Skipped ${result.skipped || 0}.`,
"ok"
);
button.textContent = "Imported";
} catch (error) {
button.disabled = false;
setStatus(formatError(error), "error");
}
}
function setStatus(message, kind = "") {
statusElement.textContent = message;
statusElement.className = `status ${kind}`.trim();
}
function formatError(error) {
return error instanceof Error ? error.message : String(error);
}

View file

@ -1,35 +0,0 @@
{
"manifest_version": 3,
"name": "CCR Login Import",
"description": "Import selected Chrome site login cookies into CCR's in-app browser.",
"version": "0.1.0",
"action": {
"default_popup": "popup.html",
"default_title": "CCR Login Import"
},
"background": {
"service_worker": "background.js"
},
"content_scripts": [
{
"js": [
"confirm.js"
],
"matches": [
"http://127.0.0.1/*",
"http://localhost/*"
]
}
],
"permissions": [
"cookies",
"scripting",
"storage"
],
"host_permissions": [
"http://127.0.0.1/*",
"http://localhost/*",
"http://*/*",
"https://*/*"
]
}

View file

@ -1,110 +0,0 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>CCR Login Import</title>
<style>
:root {
color-scheme: light dark;
font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
}
* {
box-sizing: border-box;
}
body {
margin: 0;
min-width: 360px;
background: Canvas;
color: CanvasText;
}
main {
display: grid;
gap: 12px;
padding: 14px;
}
h1 {
font-size: 15px;
line-height: 1.25;
margin: 0;
}
label {
display: grid;
gap: 6px;
font-size: 12px;
font-weight: 650;
}
textarea {
background: color-mix(in srgb, CanvasText 4%, Canvas);
border: 1px solid color-mix(in srgb, CanvasText 14%, transparent);
border-radius: 8px;
color: CanvasText;
font: inherit;
min-height: 76px;
outline: none;
padding: 8px;
resize: vertical;
width: 100%;
}
textarea:focus {
border-color: #2563eb;
}
button {
align-items: center;
background: #0f766e;
border: 1px solid #0f766e;
border-radius: 8px;
color: white;
cursor: pointer;
display: inline-flex;
font: inherit;
font-weight: 700;
height: 34px;
justify-content: center;
padding: 0 12px;
}
button:disabled {
cursor: default;
opacity: 0.58;
}
.status {
border-radius: 8px;
color: color-mix(in srgb, CanvasText 74%, transparent);
font-size: 12px;
line-height: 1.35;
min-height: 18px;
overflow-wrap: anywhere;
}
.status.error {
color: #b91c1c;
}
.status.ok {
color: #15803d;
}
</style>
</head>
<body>
<main>
<h1>CCR Login Import</h1>
<label>
Import URL
<textarea id="import-url" spellcheck="false" placeholder="Paste the URL copied from CCR"></textarea>
</label>
<button id="import-button" type="button">Import Selected Domains</button>
<div id="status" class="status" role="status"></div>
</main>
<script src="popup.js"></script>
</body>
</html>

View file

@ -1,297 +0,0 @@
const importUrlInput = document.getElementById("import-url");
const importButton = document.getElementById("import-button");
const statusElement = document.getElementById("status");
document.addEventListener("DOMContentLoaded", () => {
void restoreLastImportUrl();
});
importButton.addEventListener("click", () => {
void runImport();
});
async function restoreLastImportUrl() {
const stored = await chrome.storage.local.get(["lastImportUrl"]);
if (typeof stored.lastImportUrl === "string") {
importUrlInput.value = stored.lastImportUrl;
}
}
async function runImport() {
const importUrl = normalizeImportUrl(importUrlInput.value);
if (!importUrl) {
setStatus("Paste the import URL copied from CCR.", "error");
return;
}
importButton.disabled = true;
try {
await chrome.storage.local.set({ lastImportUrl: importUrl });
setStatus("Reading CCR import job...");
const job = await fetchImportJob(importUrl);
const domains = Array.isArray(job.domains) ? job.domains.map(normalizeDomain).filter(Boolean) : [];
if (domains.length === 0) {
throw new Error("CCR import job does not include any domains.");
}
await ensureHostPermissions(domains);
setStatus(`Reading cookies for ${domains.join(", ")}...`);
const cookies = await readCookiesForDomains(domains);
setStatus(`Reading localStorage for ${domains.join(", ")}...`);
const localStorageEntries = await readLocalStorageForDomains(domains, cookies);
if (cookies.length === 0 && localStorageEntries.length === 0) {
throw new Error("No cookies or localStorage entries were found for the selected domains.");
}
setStatus(`Sending ${cookies.length} cookies and ${localStorageItemCount(localStorageEntries)} localStorage items to CCR...`);
const result = await submitCookies(importUrl, cookies, localStorageEntries, domains);
setStatus(
`Imported ${result.cookieImported ?? 0} cookies and ${result.localStorageImported ?? 0} localStorage items. Skipped ${result.skipped ?? 0}.`,
"ok"
);
} catch (error) {
setStatus(formatError(error), "error");
} finally {
importButton.disabled = false;
}
}
async function fetchImportJob(importUrl) {
const response = await fetch(importUrl, {
headers: {
"x-ccr-login-import": "chrome-extension"
}
});
const body = await response.json().catch(() => ({}));
if (!response.ok) {
throw new Error(body?.error?.message || `CCR import job request failed (${response.status}).`);
}
if (!body.job || body.job.status !== "pending") {
throw new Error(`CCR import job is ${body.job?.status || "unavailable"}.`);
}
return body.job;
}
async function submitCookies(importUrl, cookies, localStorage, domains) {
const response = await fetch(`${importUrl.replace(/\/+$/, "")}/cookies`, {
body: JSON.stringify({
cookies,
domains,
localStorage,
source: {
browser: "chrome",
extension: chrome.runtime.getManifest().version
}
}),
headers: {
"content-type": "application/json",
"x-ccr-login-import": "chrome-extension"
},
method: "POST"
});
const body = await response.json().catch(() => ({}));
if (!response.ok) {
throw new Error(body?.error?.message || `CCR cookie import failed (${response.status}).`);
}
return body.result || {};
}
async function readCookiesForDomains(domains) {
const cookies = [];
for (const domain of domains) {
cookies.push(...await chrome.cookies.getAll({ domain }));
}
return dedupeCookies(cookies);
}
async function readLocalStorageForDomains(domains, cookies) {
const origins = localStorageOriginsForDomains(domains, cookies);
const entries = [];
for (const origin of origins) {
const entry = await readLocalStorageForOrigin(origin).catch(() => undefined);
if (entry && Object.keys(entry.items).length > 0) {
entries.push(entry);
}
}
return entries;
}
async function readLocalStorageForOrigin(origin) {
const tab = await chrome.tabs.create({
active: false,
url: `${origin}/`
});
try {
await waitForTabLoad(tab.id, 15000);
const [frameResult] = await chrome.scripting.executeScript({
func: () => {
const items = {};
for (let index = 0; index < window.localStorage.length; index += 1) {
const key = window.localStorage.key(index);
if (key !== null) {
items[key] = window.localStorage.getItem(key) || "";
}
}
return {
items,
origin: window.location.origin
};
},
target: { tabId: tab.id },
world: "MAIN"
});
const result = frameResult?.result;
if (!result || !allowedLocalStorageOrigin(result.origin, origin)) {
return undefined;
}
return result;
} finally {
if (tab.id !== undefined) {
await chrome.tabs.remove(tab.id).catch(() => undefined);
}
}
}
function waitForTabLoad(tabId, timeoutMs) {
return new Promise((resolve) => {
let done = false;
const finish = () => {
if (done) {
return;
}
done = true;
chrome.tabs.onUpdated.removeListener(listener);
window.clearTimeout(timeout);
resolve();
};
const listener = (updatedTabId, changeInfo) => {
if (updatedTabId === tabId && changeInfo.status === "complete") {
finish();
}
};
const timeout = window.setTimeout(finish, timeoutMs);
chrome.tabs.onUpdated.addListener(listener);
});
}
function dedupeCookies(cookies) {
const seen = new Set();
const result = [];
for (const cookie of cookies) {
const partitionKey = cookie.partitionKey ? JSON.stringify(cookie.partitionKey) : "";
const key = `${cookie.storeId || ""}\n${cookie.domain}\n${cookie.path}\n${cookie.name}\n${partitionKey}`;
if (!seen.has(key)) {
seen.add(key);
result.push(cookie);
}
}
return result;
}
function localStorageOriginsForDomains(domains, cookies) {
const origins = new Set();
for (const domain of domains) {
if (domain === "localhost" || isIpAddress(domain)) {
origins.add(`http://${domain}`);
origins.add(`https://${domain}`);
} else {
origins.add(`https://${domain}`);
}
}
for (const cookie of cookies) {
const host = normalizeDomain(cookie.domain);
if (!host || !cookie.hostOnly || !domains.some((domain) => host === domain || host.endsWith(`.${domain}`))) {
continue;
}
origins.add(`${cookie.secure === false ? "http" : "https"}://${host}`);
}
return [...origins];
}
function allowedLocalStorageOrigin(actualOrigin, requestedOrigin) {
try {
const actual = new URL(actualOrigin);
const requested = new URL(requestedOrigin);
return actual.protocol === requested.protocol && actual.hostname === requested.hostname && actual.port === requested.port;
} catch {
return false;
}
}
function localStorageItemCount(entries) {
return entries.reduce((count, entry) => count + Object.keys(entry.items || {}).length, 0);
}
function originsForDomains(domains) {
return [...new Set(domains.flatMap((domain) => {
if (domain === "localhost" || isIpAddress(domain)) {
return [
`http://${domain}/*`,
`https://${domain}/*`
];
}
return [
`http://${domain}/*`,
`https://${domain}/*`,
`http://*.${domain}/*`,
`https://*.${domain}/*`
];
}))];
}
async function ensureHostPermissions(domains) {
const origins = originsForDomains(domains);
const granted = await chrome.permissions.contains({ origins });
if (granted) {
return;
}
throw new Error(
[
`CCR Login Import does not have Chrome site access for ${domains.join(", ")}.`,
"Reload the unpacked extension after updating it, then grant the extension site access for the requested domains in Chrome extensions settings."
].join(" ")
);
}
function normalizeImportUrl(value) {
const raw = value.trim();
if (!raw) {
return "";
}
try {
const url = new URL(raw);
if (!["127.0.0.1", "localhost"].includes(url.hostname)) {
return "";
}
if (!/^\/chrome-import\/jobs\/[^/]+\/?$/.test(url.pathname)) {
return "";
}
url.pathname = url.pathname.replace(/\/+$/, "");
return url.toString();
} catch {
return "";
}
}
function normalizeDomain(value) {
return typeof value === "string"
? value.trim().replace(/^\*\./, "").replace(/^\./, "").toLowerCase()
: "";
}
function isIpAddress(value) {
return /^\d{1,3}(?:\.\d{1,3}){3}$/.test(value) || value.includes(":");
}
function setStatus(message, kind = "") {
statusElement.textContent = message;
statusElement.className = `status ${kind}`.trim();
}
function formatError(error) {
return error instanceof Error ? error.message : String(error);
}

1607
package-lock.json generated

File diff suppressed because it is too large Load diff

Some files were not shown because too many files have changed in this diff Show more