Refactor router config and provider handling

This commit is contained in:
musistudio
2026-06-23 20:21:28 +08:00
parent 585ff6ddd3
commit 2d868f440e
49 changed files with 10588 additions and 374 deletions
+5
View File
@@ -0,0 +1,5 @@
node_modules/
dist/
.astro/
.env
.env.*
+23
View File
@@ -0,0 +1,23 @@
# Claude Code Router Docs
Astro-powered documentation site for Claude Code Router.
## Commands
```sh
npm install
npm run dev
npm run build
npm run preview
```
The local development server runs from this `docs` directory.
## Content
Docs pages are authored in Markdown:
- Chinese: `src/content/docs/zh/index.md`
- English: `src/content/docs/en/index.md`
Frontmatter provides the page title, eyebrow, and lead text. Markdown headings generate the right-side table of contents, and fenced code blocks are compiled with Shiki highlighting.
+10
View File
@@ -0,0 +1,10 @@
import { defineConfig } from "astro/config";
export default defineConfig({
output: "static",
markdown: {
shikiConfig: {
theme: "github-light",
},
},
});
+4548
View File
File diff suppressed because it is too large Load Diff
+15
View File
@@ -0,0 +1,15 @@
{
"name": "claude-code-router-docs",
"private": true,
"version": "0.1.0",
"type": "module",
"scripts": {
"dev": "astro dev",
"build": "astro build",
"preview": "astro preview",
"astro": "astro"
},
"devDependencies": {
"astro": "7.0.0"
}
}
+5
View File
@@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
<rect width="64" height="64" rx="16" fill="#0a7f48"/>
<path d="M18 36c0-10 8-18 18-18h10v10c0 10-8 18-18 18H18V36Z" fill="#fff"/>
<circle cx="24" cy="40" r="6" fill="#b7e4cf"/>
</svg>

After

Width:  |  Height:  |  Size: 251 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 992 KiB

+62
View File
@@ -0,0 +1,62 @@
export type BotPlatformModule = {
Content: any;
frontmatter: {
title?: string;
pageTitle?: string;
eyebrow?: string;
lead?: string;
[key: string]: unknown;
};
getHeadings: () => { depth: number; slug: string; text: string }[];
rawContent: () => string;
};
export const zhBotDocs = import.meta.glob<BotPlatformModule>(
"./content/docs/zh/bots/*.md",
{ eager: true }
);
export const enBotDocs = import.meta.glob<BotPlatformModule>(
"./content/docs/en/bots/*.md",
{ eager: true }
);
export const BOT_PLATFORM_ORDER = [
"slack",
"discord",
"telegram",
"line",
"weixin-ilink",
"wecom",
"feishu",
"dingtalk",
] as const;
export type BotPlatformSlug = (typeof BOT_PLATFORM_ORDER)[number];
export const BOT_PLATFORM_LABELS_ZH: Record<BotPlatformSlug, string> = {
slack: "Slack",
discord: "Discord",
telegram: "Telegram",
line: "LINE",
"weixin-ilink": "微信",
wecom: "企业微信",
feishu: "飞书",
dingtalk: "钉钉",
};
export const BOT_PLATFORM_LABELS_EN: Record<BotPlatformSlug, string> = {
slack: "Slack",
discord: "Discord",
telegram: "Telegram",
line: "LINE",
"weixin-ilink": "Weixin",
wecom: "WeCom",
feishu: "Feishu",
dingtalk: "DingTalk",
};
export function botPlatformFromPath(filePath: string): string {
const file = filePath.split("/").pop() ?? filePath;
return file.replace(/\.md$/, "");
}
+174
View File
@@ -0,0 +1,174 @@
---
import DocsLayout from "../layouts/DocsLayout.astro";
import { docsContent, type Locale } from "../i18n/content";
import * as enDoc from "../content/docs/en/index.md";
import * as zhDoc from "../content/docs/zh/index.md";
interface Props {
locale: Locale;
doc?: any;
}
const { locale, doc: docProp } = Astro.props;
const content = docsContent[locale];
const doc = docProp ?? (locale === "en" ? enDoc : zhDoc);
const { Content, frontmatter } = doc;
const headings = doc.getHeadings().filter((heading) => heading.depth === 2);
const tocItems = headings.map((heading) => ({
label: heading.text,
href: `#${heading.slug}`,
}));
const pageMarkdown = doc.rawContent();
---
<DocsLayout
title={frontmatter.pageTitle ?? content.pageTitle}
htmlLang={content.htmlLang}
locale={locale}
languageLabel={content.languageLabel}
languageOptions={content.languageOptions}
navItems={content.navItems}
sidebarGroups={content.sidebarGroups}
expandableSidebarItems={content.expandableSidebarItems}
sidebarChildren={content.sidebarChildren}
sidebarLinks={content.sidebarLinks}
tocTitle={content.tocTitle}
tocItems={tocItems}
ui={content.ui}
>
<article class="doc-article">
<header class="article-header">
<div>
<p class="eyebrow">{frontmatter.eyebrow}</p>
<h1>{frontmatter.title}</h1>
<p class="lead">{frontmatter.lead}</p>
</div>
<button class="copy-page" type="button" data-copy-page>
<svg viewBox="0 0 20 20" aria-hidden="true">
<rect x="7" y="7" width="9" height="9" rx="2" />
<path d="M4 13V5a1 1 0 0 1 1-1h8" />
</svg>
<span data-copy-label>{content.ui.copyPage}</span>
</button>
</header>
<div class="doc-markdown" data-markdown-content>
<Content />
</div>
</article>
<script define:vars={{
pageMarkdown,
copyPageLabel: content.ui.copyPage,
copiedLabel: content.ui.copied,
copyFailedLabel: content.ui.copyFailed,
copyCodeLabel: content.ui.copyCode,
copiedCodeLabel: content.ui.copiedCode,
copyCodeFailedLabel: content.ui.copyCodeFailed,
}}>
const copyButton = document.querySelector("[data-copy-page]");
const copyLabel = document.querySelector("[data-copy-label]");
const writeClipboard = async (text) => {
if (navigator.clipboard?.writeText) {
await navigator.clipboard.writeText(text);
return;
}
const textarea = document.createElement("textarea");
textarea.value = text;
textarea.setAttribute("readonly", "");
textarea.style.position = "fixed";
textarea.style.left = "-9999px";
document.body.append(textarea);
textarea.select();
document.execCommand("copy");
textarea.remove();
};
copyButton?.addEventListener("click", async () => {
try {
await writeClipboard(pageMarkdown);
copyButton.classList.add("copied");
if (copyLabel) copyLabel.textContent = copiedLabel;
} catch {
if (copyLabel) copyLabel.textContent = copyFailedLabel;
}
window.setTimeout(() => {
copyButton.classList.remove("copied");
if (copyLabel) copyLabel.textContent = copyPageLabel;
}, 1800);
});
const buildCopyIcon = () => `
<svg class="copy-icon" viewBox="0 0 20 20" aria-hidden="true">
<rect x="7" y="7" width="9" height="9" rx="2"></rect>
<path d="M4 13V5a1 1 0 0 1 1-1h8"></path>
</svg>
<svg class="success-icon" viewBox="0 0 20 20" aria-hidden="true">
<path d="M4 10.5l4 4L16 6"></path>
</svg>
`;
document.querySelectorAll("[data-markdown-content] pre.astro-code").forEach((pre) => {
if (!(pre instanceof HTMLElement) || pre.closest(".code-panel")) return;
const language = pre.dataset.language || "text";
const panel = document.createElement("div");
panel.className = "code-panel markdown-code-panel";
panel.setAttribute("aria-label", language);
const toolbar = document.createElement("div");
toolbar.className = "code-toolbar";
const title = document.createElement("span");
title.textContent = language === "text" ? "text" : language;
const button = document.createElement("button");
button.className = "code-copy";
button.type = "button";
button.dataset.copyCode = "";
button.setAttribute("aria-label", copyCodeLabel);
button.innerHTML = buildCopyIcon();
const template = document.createElement("template");
template.dataset.codeSource = "";
template.textContent = pre.innerText.trimEnd();
toolbar.append(title, button);
panel.append(toolbar, template);
pre.replaceWith(panel);
panel.append(pre);
});
document.addEventListener("click", async (event) => {
const button = event.target instanceof Element
? event.target.closest("[data-copy-code]")
: null;
if (!(button instanceof HTMLButtonElement)) return;
const panel = button.closest(".code-panel");
const source = panel?.querySelector("[data-code-source]");
const text = source instanceof HTMLTemplateElement
? source.content.textContent ?? ""
: source?.textContent ?? "";
try {
if (!text) throw new Error("No code content to copy");
await writeClipboard(text);
button.classList.add("copied");
button.setAttribute("aria-label", copiedCodeLabel);
} catch {
button.classList.add("copy-failed");
button.setAttribute("aria-label", copyCodeFailedLabel);
}
window.setTimeout(() => {
button.classList.remove("copied", "copy-failed");
button.setAttribute("aria-label", copyCodeLabel);
}, 1600);
});
</script>
</DocsLayout>
+96
View File
@@ -0,0 +1,96 @@
---
title: DingTalk Bot Setup
pageTitle: DingTalk Bot
eyebrow: Bot Platforms
lead: Route agent messages into DingTalk's enterprise collaboration environment, with relay after your screen locks. This page walks you from creating an app in the DingTalk developer backend to a working setup in CCR.
---
## Who This Is For
DingTalk is for bringing agent messages into an enterprise collaboration environment. CCR connects to DingTalk apps using App Secret auth.
> New to bots? Start with the "Relay Agent Messages Into IM With Bots" section of the main guide to understand the overall flow and the Forward/Handoff distinction, then come back here.
## The Fields You'll Use
| Name in the DingTalk dashboard | CCR field | Required | Notes |
| --- | --- | --- | --- |
| Client ID / AppKey | App Key | Required | App identifier |
| Client Secret / AppSecret | App Secret | Required | App secret |
| RobotCode | Robot Code | Optional | May be needed for multi-bot or media scenarios |
> Newer DingTalk configures the bot as an "app capability" — don't start from the old standalone "bot" entry.
## Step 1: Create A DingTalk App
1. Open the [DingTalk developer backend](https://open-dev.dingtalk.com/).
2. Log in with your DingTalk account.
3. Pick the dev organization to connect.
4. Open `应用开发` (App Development) at the top.
5. Click `创建应用` (Create App).
6. Name it, e.g. `CCR`.
7. Fill in the description; leave other options default.
8. Click create.
## Step 2: Copy The App Key And App Secret
1. Open the app you just created.
2. On the left, open `应用信息` (App Info) or `凭证与基础信息` (Credentials & Basic Info).
3. Copy `Client ID` for CCR's App Key.
4. Copy `Client Secret` for CCR's App Secret.
> The dashboard may still show the old names `AppKey` / `AppSecret` — map them by field name.
## Step 3: Enable The Bot Capability
1. In the app, open `机器人与消息推送` (Bot & Message Push), or open `应用能力` (App Capabilities) and choose `机器人` (Bot).
2. Enable `机器人配置` (Bot Config).
3. Fill in the bot name, avatar, and description.
4. Choose **Stream mode** for message receiving.
5. Save.
6. If the page shows a `RobotCode`, copy it for CCR's Robot Code.
## Step 4: Publish The App And Join A Chat
1. Open `版本管理与发布` (Version Management & Release) and create a new version.
2. Set the visibility scope — for testing, choose just yourself or a test group.
3. Submit for release.
4. After release, search the bot name in the DingTalk client.
5. Open the bot chat, or add the bot to the target group via group settings.
## Wire It Up In CCR
1. Open CCR's **Bots** page and click **Add Bot**.
2. Pick **DingTalk** as the platform.
3. Auth is **App Secret**.
4. Paste the Client ID into **App Key**.
5. Paste the Client Secret into **App Secret**.
6. If you copied a RobotCode, paste it into **Robot Code**.
7. Save the bot.
8. Open **Profiles** and edit the agent profile you want to attach it to.
9. Turn on **Bot** and select the bot.
10. Optionally enable **Forward agent messages** or **Handoff**.
11. Reopen the agent from CCR.
## Forward vs Handoff
- **Forward agent messages**: forwards regardless of lock state. Good when you want full output in DingTalk.
- **Handoff**: only forwards after the screen locks. Pair with Idle seconds and a target device.
> Only want lock-screen alerts? Skip Forward.
## Test It
1. Open the agent from CCR and trigger a message.
2. Check DingTalk to confirm the app received it and replied.
3. For groups, confirm the app or bot is in the target group and can post.
> **How to tell it worked:** DingTalk shows the agent's message, and replies keep the agent going.
## Common Issues
- **Auth fails**: re-copy App Key and App Secret.
- **Bot-identifier errors**: check that Robot Code matches the platform dashboard.
- **Bot receives nothing**: confirm the bot capability is enabled in the app and the receive mode matches CCR's config.
- **Users can't find the bot**: check that the app is published and the visibility scope includes the current user or group members.
- **Handoff doesn't trigger**: confirm the screen is locked, and check the Handoff toggle, idle time, and target device.
+100
View File
@@ -0,0 +1,100 @@
---
title: Discord Bot Setup
pageTitle: Discord Bot
eyebrow: Bot Platforms
lead: Route agent messages into Discord server channels or DMs, and relay them after your screen locks. This page walks you from creating the Discord app to a working setup in CCR.
---
## Who This Is For
Discord is for routing agent messages into a server channel, a private collab server, or a personal DM. A Bot Token is the most common setup and is straightforward.
> New to bots? Start with the "Relay Agent Messages Into IM With Bots" section of the main guide to understand the overall flow and the Forward/Handoff distinction, then come back here.
## The Fields You'll Use
| Name in the Discord dashboard | CCR field | Required | Notes |
| --- | --- | --- | --- |
| Token | Bot Token | Required | The bot token from the Bot page |
| Application ID | Application ID | Optional | App ID from General Information |
| Public Key | Public Key | Optional | May be needed for interaction callbacks |
A Bot Token is usually enough. Only choose OAuth 2.0 if your flow explicitly requires it.
## Step 1: Create The Discord App And Bot
1. Open the [Discord Developer Portal](https://discord.com/developers/applications).
2. Click `New Application`.
3. Name it, e.g. `CCR`.
4. Open the app after it's created.
5. Open `Bot` on the left.
6. If there's no bot yet, click `Add Bot`.
7. Set the avatar and username.
## Step 2: Enable The Required Intents
1. Still on the `Bot` page, find `Privileged Gateway Intents`.
2. Enable **Message Content Intent**. Without it, the bot likely can't see message bodies.
3. Enable **Server Members Intent** if you gate on members, roles, or usernames.
4. **Presence Intent** is usually unnecessary unless you read online status.
## Step 3: Copy The Bot Token
1. On the `Bot` page, find `Token`.
2. Click `Reset Token` or `Copy`.
3. On first creation, `Reset Token` generates the first token — it doesn't mean you broke anything.
4. Copy the token for CCR's Bot Token.
> This token is effectively the bot's password. Don't post it in Discord or paste it into an agent prompt.
## Step 4: Invite The Bot Into A Server
1. Open `OAuth2` on the left.
2. Open `URL Generator`.
3. In `Scopes`, tick `bot` and `applications.commands`.
4. In `Bot Permissions`, tick at least `View Channels`, `Send Messages`, `Read Message History`, `Embed Links`, `Attach Files`.
5. Tick `Add Reactions` if you want reactions on approval messages.
6. Copy the generated URL, open it in a browser, and authorize into the target server.
## Step 5: Copy Optional Fields (If Needed)
If something asks for Application ID or Public Key:
1. Back in the Developer Portal, open `General Information`.
2. Copy `Application ID` and `Public Key`.
## Wire It Up In CCR
1. Open CCR's **Bots** page and click **Add Bot**.
2. Pick **Discord** as the platform.
3. Keep the default **Bot Token** auth.
4. Paste the token into **Bot Token**.
5. Add **Application ID** and **Public Key** if needed.
6. Save the bot.
7. Open **Profiles** and edit the agent profile you want to attach it to.
8. Turn on **Bot** and select the bot.
9. Optionally enable **Forward agent messages** or **Handoff**.
10. Reopen the agent from CCR.
## Forward vs Handoff
- **Forward agent messages**: forwards regardless of lock state. Good for debugging or full-record channels.
- **Handoff**: only forwards after the screen locks. Pair with Idle seconds and a target device.
> Only want lock-screen alerts? Skip Forward.
## Test It
1. Open the agent from CCR and trigger a message.
2. Check Discord to confirm the bot received it and replied.
3. For server channels, confirm the bot is in the server and can post.
> **How to tell it worked:** Discord shows the agent's message, and replies keep the agent going.
## Common Issues
- **Bot doesn't respond**: confirm the Bot Token was copied correctly.
- **Bot is online but can't see your messages**: check that Message Content Intent is on.
- **No messages in a channel**: confirm the bot is in that server and channel permissions let it post.
- **No permission options in the invite URL**: make sure the OAuth2 URL Generator has `bot` scoped.
- **Handoff doesn't trigger**: confirm the screen is locked, Handoff is on, and idle time/target device are set.
+111
View File
@@ -0,0 +1,111 @@
---
title: Feishu Bot Setup
pageTitle: Feishu Bot
eyebrow: Bot Platforms
lead: Route agent messages into Feishu (Lark) groups or app chats, with relay after your screen locks. This page walks you from creating an enterprise self-built app on the Feishu Open Platform to a working setup in CCR.
---
## Who This Is For
Feishu is for teams that want agent messages in a Feishu group or app chat. CCR connects to Feishu apps using App Secret auth.
> New to bots? Start with the "Relay Agent Messages Into IM With Bots" section of the main guide to understand the overall flow and the Forward/Handoff distinction, then come back here.
## The Fields You'll Use
| Name in the Feishu dashboard | CCR field | Required | Notes |
| --- | --- | --- | --- |
| App ID | App ID | Required | App identifier, usually starts with `cli_` |
| App Secret | App Secret | Required | App secret |
| Feishu / Lark domain | Domain | Optional | Usually blank for mainland Feishu; fill for Lark or special domains |
## Step 1: Create An Enterprise Self-Built App
1. Open the [Feishu Open Platform](https://open.feishu.cn/).
2. Go to the developer backend.
3. Click `创建应用` (Create App).
4. Choose `企业自建应用` (Enterprise Self-Built App).
5. Name it, e.g. `CCR`.
6. Fill in the description and upload an icon.
7. Create the app.
## Step 2: Copy The App ID And App Secret
1. Open the app you just created.
2. Open `基础信息` (Basic Info).
3. Go to `凭证与基础信息` (Credentials & Basic Info).
4. Copy `App ID`.
5. Copy `App Secret`.
These two are the required fields in CCR.
## Step 3: Enable The Bot Capability
1. In the app backend, open `应用能力` (App Capabilities).
2. Click `添加应用能力` (Add App Capability).
3. Find `机器人` (Bot) and add or enable it.
4. Set the bot name and avatar.
> Without the bot capability, the Feishu chat may show no input box and won't receive user messages.
## Step 4: Request Message Permissions
1. Open `开发配置` (Development Config).
2. Go to `权限管理` (Permission Management).
3. Add application identity permissions.
4. At minimum, enable "read single-chat messages sent to the bot".
5. To support @-mentions in groups, enable "read group messages that @-mention the bot".
6. To let the agent reply, enable "send messages as the app".
7. Save.
> Permission names vary slightly across tenants. When you see identifiers like `im:message.p2p_msg:readonly`, `im:message.group_at_msg:readonly`, `im:message:send_as_bot`, prefer those message-related ones.
## Step 5: Configure Event Subscriptions
1. Open `事件与回调` (Events & Callbacks).
2. Choose long-connection (or WebSocket) mode.
3. Add the event `im.message.receive_v1`.
4. Save.
## Step 6: Publish Or Install The App
1. Open `版本管理与发布` (Version Management & Release) and create a new version.
2. Confirm the visibility scope — for testing, choose just yourself or a small range.
3. Submit for release.
4. If the enterprise requires review, wait for approval.
5. Find the app in the Feishu client, or add the bot to the target group.
## Wire It Up In CCR
1. Open CCR's **Bots** page and click **Add Bot**.
2. Pick **Feishu** as the platform.
3. Auth is **App Secret**.
4. Fill in **App ID** and **App Secret**.
5. For Lark or a special domain, fill in **Domain**.
6. Save the bot.
8. Open **Profiles** and edit the agent profile you want to attach it to.
9. Turn on **Bot** and select the bot.
10. Optionally enable **Forward agent messages** or **Handoff**.
11. Reopen the agent from CCR.
## Forward vs Handoff
- **Forward agent messages**: forwards regardless of lock state. Good when you want full output in Feishu.
- **Handoff**: only forwards after the screen locks. Pair with Idle seconds and a target device.
> Only want lock-screen alerts? Skip Forward.
## Test It
1. Open the agent from CCR and trigger a message.
2. Check Feishu to confirm the app received it and replied.
3. For groups, add the app to the target group first and confirm members can see it.
> **How to tell it worked:** Feishu shows the agent's message, and replies keep the agent going.
## Common Issues
- **Auth fails**: re-copy App ID and App Secret.
- **No input box in chat**: check the bot capability, event subscription, and that the app is published to the current member's visibility scope.
- **No response in a group**: @-mention the bot first, and confirm the event subscription includes `im.message.receive_v1`.
- **Lark / special domain**: confirm Domain is the value the platform requires.
+88
View File
@@ -0,0 +1,88 @@
---
title: LINE Bot Setup
pageTitle: LINE Bot
eyebrow: Bot Platforms
lead: Route agent messages into LINE friends, groups, or an Official Account, and relay them after your screen locks. This page walks you from creating a LINE Messaging API channel to a working setup in CCR.
---
## Who This Is For
LINE is for routing agent messages into an existing LINE friend list, group chat, or LINE Official Account. CCR uses a Channel Access Token as the primary auth field.
> New to bots? Start with the "Relay Agent Messages Into IM With Bots" section of the main guide to understand the overall flow and the Forward/Handoff distinction, then come back here.
## The Fields You'll Use
In CCR, LINE's auth type is labeled **Bot Token**, but you don't paste a Telegram-style token — you fill in these two channel fields:
| Name in the LINE dashboard | CCR field | Required | Notes |
| --- | --- | --- | --- |
| Channel access token | Channel Access Token | Required | Lets the bot call the LINE Messaging API |
| Channel secret | Channel Secret | Recommended | Used to verify requests from LINE |
## Step 1: Create A Messaging API Channel
1. Open the [LINE Developers Console](https://developers.line.biz/console/).
2. Log in with your LINE account.
3. Create a Provider, or pick an existing one.
4. Click `Create a new channel`.
5. Choose `Messaging API`.
6. Fill in the Channel name, description, icon, category, etc.
7. Open the channel after it's created.
> If you already have a LINE Official Account, you can enable Messaging API in its settings, then come back to the console to copy credentials.
## Step 2: Copy The Channel Secret
1. Open the Messaging API channel you just created.
2. Open `Basic settings`.
3. Find `Channel secret` and copy it for CCR's Channel Secret.
## Step 3: Issue A Channel Access Token
1. Open the `Messaging API` tab.
2. Find `Channel access token`.
3. Click `Issue` or `Reissue`.
4. Copy the generated token for CCR's Channel Access Token.
> Prefer a long-lived token. Reissuing invalidates the old token, so update CCR at the same time.
## Step 4: Open The Chat Entry
1. For groups, turn on `Allow bot to join group chats`.
2. Consider disabling the LINE Official Account auto-reply so users don't get both the default reply and the agent's.
## Wire It Up In CCR
1. Open CCR's **Bots** page and click **Add Bot**.
2. Pick **LINE** as the platform.
3. Auth is **Bot Token** (this is LINE's fixed auth type in CCR).
4. Paste the token into **Channel Access Token**.
5. Paste the secret into **Channel Secret**.
6. Save the bot.
7. Open **Profiles** and edit the agent profile you want to attach it to.
8. Turn on **Bot** and select the bot.
9. Optionally enable **Forward agent messages** or **Handoff**.
10. Reopen the agent from CCR.
## Forward vs Handoff
- **Forward agent messages**: forwards regardless of lock state. Good when you want full output in LINE.
- **Handoff**: only forwards after the screen locks. Pair with Idle seconds and a target device.
> Only want lock-screen alerts? Skip Forward.
## Test It
1. Open the agent from CCR and trigger a message.
2. Check LINE to confirm the bot received it and replied.
3. For groups, confirm the bot has joined and can post.
> **How to tell it worked:** LINE shows the agent's message, and replies keep the agent going.
## Common Issues
- **Auth fails**: re-copy the Channel Access Token.
- **Can send but can't receive**: confirm the Channel Access Token is valid and CCR is running and connected to LINE.
- **Groups don't work**: confirm `Allow bot to join group chats` is on, then re-add the bot to the group.
- **Only want lock-screen alerts**: skip Forward, use Handoff only.
+92
View File
@@ -0,0 +1,92 @@
---
title: Slack Bot Setup
pageTitle: Slack Bot
eyebrow: Bot Platforms
lead: Route agent messages into Slack channels or DMs, and relay them to Slack after your screen locks. This page walks you from creating the Slack app all the way to a working setup in CCR.
---
## Who This Is For
Slack is for teams that want agent messages in an existing channel, DM, or workspace app. You need a Bot Token and an App Token.
> New to bots? Start with the "Relay Agent Messages Into IM With Bots" section of the main guide to understand the overall flow and the difference between Forward and Handoff, then come back here for a single platform.
## The Fields You'll Use
| Name in the Slack dashboard | CCR field | Looks like | When you need it |
| --- | --- | --- | --- |
| Bot User OAuth Token | Bot Token | `xoxb-...` | Required — lets the bot send and receive |
| App-Level Token | App Token | `xapp-...` | Lets Socket Mode establish the connection |
## Step 1: Create The Slack App
1. Open [Slack API Apps](https://api.slack.com/apps).
2. Click `Create New App`.
3. Choose `From scratch`.
4. Name it, e.g. `CCR`.
5. Pick the Slack workspace to connect.
6. Click `Create App`.
## Step 2: Turn On Socket Mode
1. Open `Socket Mode` on the left.
2. Enable `Socket Mode`.
3. When prompted for an App-Level Token, create one.
4. Name it anything, e.g. `ccr-socket`.
5. Choose the `connections:write` scope.
6. Copy the `xapp-...` App-Level Token for CCR's App Token.
## Step 3: Add Bot Scopes And Install
1. Open `OAuth & Permissions`.
2. Find `Bot Token Scopes` under `Scopes`.
3. Add at least: `app_mentions:read`, `channels:history`, `channels:read`, `chat:write`, `im:history`, `im:read`, `im:write`.
4. Add `files:read` and `files:write` to send/receive files.
5. Add `groups:history` and `groups:read` for private channels.
6. Click `Install to Workspace` at the top and authorize.
7. Copy the `Bot User OAuth Token` (starts with `xoxb-`) for CCR's Bot Token.
## Step 4: Invite The Bot Into A Channel
Skip this if you only use DMs.
1. Open the target Slack channel.
2. Type `/invite @YourBotName` in the message box.
3. Send it and confirm the bot appears in the member list.
> Without an invite, the bot usually only gets DMs — not channel messages.
## Wire It Up In CCR
1. Open CCR's **Bots** page and click **Add Bot**.
2. Pick **Slack** as the platform.
3. Keep the default **Bot Token** auth (unless you specifically need OAuth).
4. Paste `xoxb-...` into **Bot Token**.
5. Paste `xapp-...` into **App Token**.
6. Save the bot.
8. Open **Profiles** and edit the agent profile you want to attach it to.
9. Turn on **Bot** and select the bot you just saved.
10. Optionally enable **Forward agent messages** or **Handoff** (next section).
11. Reopen the agent from CCR.
## Forward vs Handoff
- **Forward agent messages**: forwards every new agent message to Slack regardless of screen lock. Good for full logs or debugging.
- **Handoff**: only forwards after the screen locks. Pair it with **Idle seconds** and a Wi-Fi/Bluetooth target device.
> Only want lock-screen alerts? Use Handoff, not Forward — otherwise it gets noisy.
## Test It
1. Open the agent from CCR and trigger a message.
2. Check Slack to confirm the bot received it and replied.
3. If you use a channel, make sure the bot is in it.
> **How to tell it worked:** Slack shows the agent's message, and when you reply, the agent continues.
## Common Issues
- **No messages reach Slack**: confirm the Bot Token is still valid and the app is in the target channel.
- **Socket Mode won't connect**: check that the App Token starts with `xapp-` and has `connections:write`.
- **Channel silent but DMs work**: the bot isn't in the channel, or it lacks `channels:*` / `groups:*` scopes. Reinstall to the workspace after adding scopes.
- **Too many messages**: turn off Forward, keep Handoff only.
+81
View File
@@ -0,0 +1,81 @@
---
title: Telegram Bot Setup
pageTitle: Telegram Bot
eyebrow: Bot Platforms
lead: Route agent messages into Telegram and relay them after your screen locks. Telegram is the simplest platform of all — you only need a Bot Token, and you can be live in minutes.
---
## Who This Is For
Telegram is for individuals or small teams who want agent messages fast. It has the fewest fields — just a `Bot Token`. If you want the quickest possible bot, start here.
> New to bots? Start with the "Relay Agent Messages Into IM With Bots" section of the main guide to understand the overall flow and the Forward/Handoff distinction, then come back here.
## The Fields You'll Use
| Name in Telegram | CCR field | Required | Notes |
| --- | --- | --- | --- |
| HTTP API token | Bot Token | Required | The token `@BotFather` returns after creating the bot |
## Step 1: Create The Bot With BotFather
1. Open Telegram.
2. Search `@BotFather` and confirm the username matches exactly (the official bot).
3. In the chat, send `/newbot`.
4. Enter a display name when prompted, e.g. `CCR Assistant`.
5. Enter a username — it must end in `bot`, e.g. `ccr_demo_bot`.
6. On success, `@BotFather` returns an HTTP API token.
7. Copy it for CCR's Bot Token.
> **Never share this token.** Anyone who has it has full control of your Telegram bot.
## Step 2: Set Up Group Support (Optional)
Skip this if you only use DMs.
To use it in groups:
1. Send `/setjoingroups` to `@BotFather`.
2. Pick your bot.
3. Choose to allow joining groups.
4. To let the bot see all group messages, send `/setprivacy`.
5. Pick the bot again.
6. Choose `Disable` to turn off privacy mode.
7. Add the bot to the target group.
> With privacy mode on, the bot usually only sees commands, @-mentions, and some service messages. After disabling it, kick and re-add the bot so the change takes effect immediately.
## Wire It Up In CCR
1. Open CCR's **Bots** page and click **Add Bot**.
2. Pick **Telegram** as the platform.
3. Auth is **Bot Token**.
4. Paste the token into **Bot Token**.
5. Save the bot.
6. Open **Profiles** and edit the agent profile you want to attach it to.
7. Turn on **Bot** and select the bot.
8. Optionally enable **Forward agent messages** or **Handoff**.
9. Reopen the agent from CCR.
## Forward vs Handoff
- **Forward agent messages**: forwards regardless of lock state. Good when you want full output in Telegram.
- **Handoff**: only forwards after the screen locks. Pair with Idle seconds and a target device.
> Only want lock-screen alerts? Skip Forward.
## Test It
1. Open the agent from CCR and trigger a message.
2. Check Telegram to confirm the bot received it and replied.
3. For groups, confirm the bot is in the group and can read/write.
> **How to tell it worked:** Telegram shows the agent's message, and replies keep the agent going.
## Common Issues
- **Auth fails**: re-copy the Bot Token.
- **DMs work but groups don't**: check that the bot is in the group and group permissions let it read.
- **Only `/command` triggers the bot in a group**: check `/setprivacy` in `@BotFather`, or promote the bot to group admin.
- **You reset the token**: the old token dies instantly — update CCR and restart.
- **Too many messages**: turn off Forward, keep Handoff only.
+85
View File
@@ -0,0 +1,85 @@
---
title: WeCom Bot Setup
pageTitle: WeCom Bot
eyebrow: Bot Platforms
lead: Route agent messages into WeCom (Enterprise WeChat) so your team can receive and reply in WeCom, with relay after your screen locks. This page walks you from creating a self-built app in the WeCom admin console to a working setup in CCR.
---
## Who This Is For
WeCom is for bringing the agent into an enterprise messaging environment, so team members can receive and reply to agent messages inside WeCom.
> New to bots? Start with the "Relay Agent Messages Into IM With Bots" section of the main guide to understand the overall flow and the Forward/Handoff distinction, then come back here.
## The Fields You'll Use
| Name in the WeCom dashboard | CCR field | Required | Notes |
| --- | --- | --- | --- |
| CorpID / 企业ID | Corp ID | Required | Enterprise-level ID, under "My Enterprise" |
| AgentId | Agent ID | Required | The self-built app's ID |
| Secret | Secret | Required | App secret — admins usually confirm on their phone to view it |
> CCR exchanges Corp ID and the app Secret for a WeCom access_token for you — you don't fetch it manually.
## Step 1: Get The Corp ID
1. Open the [WeCom admin console](https://work.weixin.qq.com/wework_admin/frame).
2. Log in as an admin.
3. Open `我的企业` (My Enterprise) at the top.
4. Go to `企业信息` (Enterprise Info).
5. Find `企业ID` (CorpID) and copy it for CCR's Corp ID.
## Step 2: Create A Self-Built App
1. In the admin console, open `应用管理` (App Management).
2. Find the `自建` (Self-built) section.
3. Click `创建应用` (Create App).
4. Name it, e.g. `CCR`.
5. Upload a logo.
6. Pick a visibility scope — for testing, choose just yourself or a small test department.
7. Click create.
## Step 3: Copy The Agent ID And Secret
1. Open the self-built app you just created.
2. Copy `AgentId` for CCR's Agent ID.
3. Find `Secret` and click to view it.
4. Confirm on your phone's WeCom as prompted.
5. Copy the displayed `Secret`.
> If WeCom asks for `企业可信IP` (Trusted Enterprise IPs), add the outbound public IP of the machine running the CCR Bot Gateway (or your relay service's outbound IP).
## Wire It Up In CCR
1. Open CCR's **Bots** page and click **Add Bot**.
2. Pick **WeCom** as the platform.
3. Auth is **App Secret**.
4. Fill in **Corp ID**, **Agent ID**, and **Secret**.
5. Save the bot.
6. Open **Profiles** and edit the agent profile you want to attach it to.
7. Turn on **Bot** and select the bot.
8. Optionally enable **Forward agent messages** or **Handoff**.
9. Reopen the agent from CCR.
## Forward vs Handoff
- **Forward agent messages**: forwards regardless of lock state. Increases message volume — use only for full logs or troubleshooting.
- **Handoff**: only forwards after the screen locks. Pair with Idle seconds and a target device.
> Only want lock-screen alerts? Skip Forward.
## Test It
1. Open the agent from CCR and trigger a message.
2. Check WeCom to confirm the app received it and replied.
3. Lock the screen, wait past your idle threshold, and confirm new agent messages arrive via handoff.
> **How to tell it worked:** WeCom shows the agent's message, and replies keep the agent going.
## Common Issues
- **Auth fails**: re-copy Corp ID, Agent ID, and Secret.
- **Starts but receives nothing**: check that the WeCom app is allowed to receive messages and that the current member has access.
- **Send fails with an untrusted IP**: configure `企业可信IP` in the WeCom dashboard.
- **Some members can't see the app**: check the self-built app's visibility scope.
- **Handoff doesn't trigger**: confirm the screen is locked, and check the Handoff toggle, idle time, and target device.
@@ -0,0 +1,78 @@
---
title: Weixin Bot Setup
pageTitle: Weixin Bot
eyebrow: Bot Platforms
lead: Route agent messages into WeChat (Weixin) and relay them after your screen locks. The easiest path is QR Login — you scan a code and you're in, no token copying required.
---
## Who This Is For
Weixin is for individuals who want agent messages in their everyday chat window. The simplest method is QR Login, which needs no manual token.
> New to bots? Start with the "Relay Agent Messages Into IM With Bots" section of the main guide to understand the overall flow and the Forward/Handoff distinction, then come back here.
## Two Login Methods
| Method | What you need | Who it's for |
| --- | --- | --- |
| QR Login | A WeChat account that can scan and confirm | Most individuals |
| Bot Token | A token from an external WeChat bot service or iLink plugin | Users who already run a third-party WeChat bot service |
> **Prefer QR Login.** A WeChat session is tightly bound to account safety — use a dedicated bot account, not your main account that handles payments, customer service, or important contacts.
## Method 1: QR Login (Recommended)
1. Open CCR's **Bots** page and click **Add Bot**.
2. Pick **Weixin iLink** as the platform.
3. Choose **QR Login** (the default).
4. CCR opens a QR code window.
5. Scan the code with your phone's WeChat.
6. Confirm the login on your phone.
7. Wait for CCR to show login success.
8. Save the bot.
> QR codes expire. If the scan page says it's expired, close the login window and start over.
## Method 2: Bot Token
Use this only if you already have a token from an external WeChat bot service, an iLink service, or a plugin.
1. Copy the `Bot Token` from the provider's dashboard or local plugin output.
2. If the provider also gave an `Account ID`, copy it.
3. If it gave a `User ID`, copy that too.
4. Open CCR's **Bots** page and click **Add Bot**.
5. Pick **Weixin iLink** and choose **Bot Token** auth.
6. Fill in **Bot Token**, and **Account ID** / **User ID** if you have them.
7. Save the bot.
## Bind It To An Agent In CCR
Whichever login you used, bind the bot to an agent profile:
1. Open **Profiles** and edit the agent profile you want to attach it to.
2. Turn on **Bot** and select the bot you just saved.
3. Optionally enable **Forward agent messages** or **Handoff** (next section).
4. Reopen the agent from CCR.
## Forward vs Handoff
- **Forward agent messages**: forwards every new agent message to WeChat regardless of lock state. Good when you want every line of output in WeChat.
- **Handoff**: only forwards after the screen locks. Pair with **Idle seconds** and a Wi-Fi/Bluetooth target device.
> Only want lock-screen alerts? Use Handoff, not Forward.
## Test It
1. Open the agent from CCR and trigger a message.
2. Check WeChat to confirm the bot received it and replied.
3. Lock the screen, wait past your idle threshold, and confirm new agent messages arrive in WeChat.
> **How to tell it worked:** WeChat shows the agent's message, and replies keep the agent going.
## Common Issues
- **QR code expired**: close the login window and scan again.
- **Scan succeeded but nothing forwards**: confirm the agent profile was restarted and the Bot toggle is still on.
- **Drops shortly after scanning**: check that phone and computer networks are stable; verify WeChat didn't log in elsewhere and invalidate the session.
- **Token mode won't connect**: re-copy the Bot Token — avoid expired values or stray spaces.
- **Third-party service needs Account ID / User ID**: make sure these come from the same account — don't mix a token and IDs from different accounts.
+366
View File
@@ -0,0 +1,366 @@
---
title: Claude Code Router Guide
pageTitle: Guide
eyebrow: Getting Started
lead: A hands-on, step-by-step guide to getting Claude Code Router running. We start from the download, add your first model, wire an agent into CCR, and actually watch a request flow through it. You will not need to edit any config file by hand.
---
If this is your first time with CCR, read it top to bottom — about ten minutes and the whole pipeline will be live. If you already know the basics, jump straight to the section you need. Every step includes a "how to tell it worked" check so you never have to guess whether you did it right.
## What CCR Actually Does For You
In one sentence: **it funnels every AI agent on your machine (Claude Code, Codex, ZCode, and friends) through a single local entrypoint, and you decide which model each request uses.**
Why that's worth it:
- No more configuring the model and key in every single agent. CCR manages it once.
- Different work can hit different models — cheap fast models for grunt work, strong models for hard problems, multimodal models for images, search-capable models when you need fresh info.
- When one model fails, CCR can fall back to another automatically. No manual config surgery.
- Every request is logged, so you can see where the money goes, which model is slow, and which key is erroring.
CCR runs locally on your machine and keeps everything in a local config file. It exposes two local addresses, but day to day you only care about one:
| Purpose | Address | Notes |
| --- | --- | --- |
| Agent entrypoint | `http://127.0.0.1:3456` | Your agents connect here |
| Internal CCR service | `http://127.0.0.1:3457` | Internal — you can ignore it |
Everything that follows is about getting your agents to send traffic to that `3456` entrypoint.
## Step 1: Install And Start CCR
### Download And Install
1. Open the [GitHub Releases](https://github.com/musistudio/claude-code-router/releases) page.
2. Grab the package for your platform:
- macOS: `.dmg` or `.zip`
- Windows: `.exe`
- Linux: `.AppImage`
3. Install and launch **Claude Code Router** like any normal app.
On first launch, CCR creates its local config file (handy for backup and troubleshooting — you won't normally edit it by hand):
- macOS / Linux: `~/.claude-code-router/config.json`
- Windows: `%APPDATA%\Claude Code Router\config.json`
### Start The Local Gateway
Once the app is open, go to the **Server** page and click **Start** to bring up the local gateway.
> **How to tell it worked:** The Server page shows the gateway running and port `3456` listening. If you want the gateway to start automatically whenever the app opens, turn on **Auto start**.
CCR itself is now alive — but it can't do anything useful yet, because it doesn't know where to forward requests. That's the next step.
## Step 2: Connect Your First Provider
A Provider is the upstream model service that CCR forwards requests to — OpenRouter, DeepSeek, Z.AI, or anything that speaks the OpenAI / Anthropic / Gemini protocols.
### Add A Provider
1. Go to **Providers** and click **Add Provider**.
2. First, pick a **Provider preset** from the built-in list. Presets are nice because they auto-fill the common Base URL, icon, and protocol so you don't have to look them up. If your service isn't listed, choose **Other / custom API endpoint**.
3. Fill in the fields:
- **Name**: the label shown inside CCR. Keep it short and recognizable, like `openrouter` or `deepseek`.
- **Base URL**: the upstream endpoint. For custom providers, double-check this includes the correct API path.
- **Protocol**: the protocol the upstream actually supports. Get this wrong and the connectivity check usually fails. If you're unsure, run the protocol probe below.
- **API Key**: your key. One key lives here; use Credentials only when you need several.
- **Models**: list the models this provider should expose to CCR. The model picker reads from here.
4. Don't save just yet — run the connectivity checks first.
### Picking A Protocol
| Protocol | When to use it |
| --- | --- |
| OpenAI Chat Completions | Almost any OpenAI-compatible service (most common) |
| OpenAI Responses | Services that support the Responses API |
| Anthropic Messages | Anthropic itself, or services compatible with it |
| Gemini Generate Content | Gemini itself, or services compatible with it |
> **Not sure which protocol?** Run the built-in Protocol probe and let it scan the Base URL. The result is a hint, not a verdict — confirm against the provider's docs and the connectivity check.
### Three Checks Before You Save
Catching problems here keeps them from masquerading as Routing or Agent issues later:
1. **Protocol probe**: confirm which protocols the Base URL actually supports.
2. **Model connectivity check**: send a test request to one or two models and see if they respond.
3. **Account usage test** (optional): if you've enabled usage meters, confirm balance and quota come back correctly.
When all three are green, hit save.
> **How to tell it worked:** The provider appears in your list and at least one model shows as available. Fire off a test request and you should get a normal response back.
### Want Multiple Keys? (Optional)
A single key is fine for personal use. For a team or high-volume traffic, open **Credentials** and add several — CCR rotates them for you:
1. Open **Credentials** in the provider form.
2. Click **Add credential**.
3. Give each key a **Label** so you can recognize it in Logs.
4. Set **Priority**: lower numbers are tried first.
5. Set **Weight**: at the same priority, higher weight gets more requests.
6. If a key has quota limits, fill in **Limits** so you can see when it's near the cap.
7. Save, send a few test requests, then filter Logs by Credential to confirm rotation behaves as expected.
### Want To See Balance In The Dashboard? (Optional)
If you'd like Overview to display a provider's balance and remaining quota, turn on **Account / Usage**:
1. Pick a usage connector. Prefer a built-in standard endpoint when one exists; fall back to HTTP JSON or a plugin otherwise.
2. Fill in auth mode and endpoint.
3. Click **Test** to pull one reading.
4. From the result, select the fields you care about (balance, remaining quota, used amount, reset time, etc.).
5. Back in Overview, add an **Account balance** widget.
> **Security note:** Never send your provider API key to an untrusted usage endpoint. Verify the domain and permission scope of any custom endpoint before filling it in.
CCR now knows which models are available. What it doesn't know yet is which one to use — that's routing.
## Step 3: Configure Routing (Which Model Handles What)
Go to the **Routing** page. This is where you decide **which model handles which kind of request.**
The one item that matters most is **Default route** — the fallback model used when no special rule matches. Set this and you already have a working minimal config.
### Recommended Order
1. **Default**: a stable model that can carry the main workload. This is your workhorse.
2. **Background**: a cheap, fast model for summaries, context compaction, and other low-priority work.
3. **Thinking**: a strong reasoning model for tasks that need depth.
4. **Long context**: a large-context model plus the threshold (how many tokens trigger the switch).
5. **Image**: a multimodal or Fusion model for image tasks, if you want those routed separately.
6. **Web search**: a Fusion model for search-augmented work, if applicable.
7. **Fallback**: what happens when the chosen model fails. A common pattern is to retry first, then walk a model chain of backups.
> **For your first setup:** Just set Default. You can come back and add Background, Thinking, and the rest whenever you actually need them.
### Want Finer Control? (Optional)
Click **Add Routing Rule**. What each rule type is for:
| Rule type | Use when |
| --- | --- |
| model-prefix | The client sends a specific model-name prefix and you want to route those requests apart |
| subagent | You want to route by subagent signal |
| thinking / long-context / image / web-search | You want to route by workload type |
| condition | You want to match on request fields, headers, or body content |
| rewrite | You need to adjust request-body fields for a compatibility edge case |
> **How to tell it worked:** Save, send any request, then open **Logs** and read the `request model`, `resolved provider`, `resolved model`, and status code on that row. If it didn't hit the model you expected, check rule order, match conditions, and fallback first.
CCR now knows both *which models exist* and *which one each request should use*. The last step is making your agent actually send its traffic through CCR.
## Step 4: Point Your Agent At CCR (Profiles)
Go to the **Profiles** page. A Profile takes a chosen agent (Claude Code / Codex / ZCode) and points it at CCR's entrypoint `http://127.0.0.1:3456`, so its requests get routed and recorded.
Before you start, two options that apply to every profile:
- **Scope**:
- **Only opened from CCR**: traffic only goes through CCR when you launch the agent from inside CCR. Your system's default agent setup is untouched. **Strongly recommended for your first try** — easy to experiment, easy to walk back.
- **System default**: the agent uses CCR by default. Switch to this once you're confident the setup is stable.
- **Surface**: APP, CLI, or automatic — pick based on how you intend to start the agent.
- **Model**: can be a provider model or a Fusion model.
> **One habit to keep:** After you Apply, launch the agent from CCR's "open agent" button. That's what lets Bot and app-related features work.
### Claude Code
1. Select **Claude Code** in Profiles.
2. Pick the **Model** for normal requests.
3. If you want a cheaper model for lightweight background work, set **Small fast model**.
4. Confirm **Settings file** points at your local Claude Code settings path (the default is usually right).
5. Add **Env** variables if you need any.
6. Click **Apply**.
7. Launch Claude Code via **Open Agent** from CCR.
> **Verify:** Send one request, then open **Logs**. The Client should read Claude Code, and the provider/model should match your Routing. If so, the whole chain is live.
### Codex
1. Select **Codex** in Profiles.
2. Confirm **Provider ID** and **Provider Name** (defaults are usually fine).
3. Pick the **Model** — a provider model or a Fusion model.
4. Confirm **Config file** (the default is Codex's config path).
5. If you use a specific Codex CLI build, fill in **Codex CLI path** and **Codex home**; otherwise leave them.
6. Toggle **CLI middleware** and **Show all sessions** as needed.
7. Click **Apply** and open Codex from CCR.
Use **Only opened from CCR** while trialing. Switch to **System default** once you're happy.
### ZCode
ZCode uses the app surface. Focus on **Model**, **Provider ID**, **Provider Name**, and whether you open it from CCR. The Codex-CLI-only fields don't apply here.
### Reuse An Agent You're Already Logged Into (Optional)
If your machine is already logged into Claude Code, Codex, or ZCode, you can import it from **Providers** as a **Local Agent Provider**. It then shows up in the model picker like any normal provider — handy for reusing an existing local authorization instead of fetching a new key.
At this point you have a **complete, working minimal system**: providers connected → routing set → agent wired in. Next, let's open the observability panels and confirm everything really is behaving the way you configured.
## Step 5: Open The Observability Panels And Confirm Traffic
The goal of this step is to make things *visible* — whether requests are arriving, which model they hit, how much they cost, and whether anything errored.
### Turn The Switches On First
Overview can't show data until CCR is allowed to record it. Go to **Settings → Observability**:
1. Turn on **Request logs** — this feeds Logs and most Overview widgets. **Turn it on.**
2. Turn on **Agent analysis** if you want agent-level summaries in Observability.
3. **Capture network** (under Server → Proxy) is only for inspecting raw traffic. Turn it on solely while debugging, and turn it off when you're done — it records more complete, and therefore more sensitive, information.
### What To Look At: Overview
Go to **Overview** and click **Edit widgets** to add components. The most useful ones:
| Widget | The question it answers |
| --- | --- |
| System status | Is the gateway running, is there recent activity |
| Requests / Success rate | How much traffic, what's the success rate |
| Estimated cost | How much money have I spent |
| Token mix | Input / output / cache / reasoning token split |
| Model distribution | Which models get used the most |
| Provider analysis | Which provider has the most traffic, highest latency, most errors |
| Account balance | How much balance and quota is left per provider |
You can drag widgets around, resize them, switch display variants, delete, or reset to the default layout. A few ready-made layouts:
- **Daily glance:** System status, Requests, Success rate, Usage trend, Provider analysis.
- **Cost watch:** Estimated cost, Token mix, Model distribution, Account balance.
- **Performance hunt:** Average latency, Errors, Provider analysis, Logs.
> **What Overview is for:** It answers "is the overall trend healthy." When a number looks off (a model's cost suddenly spikes, say), jump to Logs to inspect the individual requests. Don't try to diagnose a single failure from Overview alone.
### What To Look At: Logs
**Logs** is your main tool for debugging individual requests. It requires Request logs to be on.
The moves you'll use most:
- Filter by **status** to isolate successes or failures.
- Filter by **Provider / Model** to focus on one upstream.
- Filter by **Credential** to focus on one API key.
- Use the search box for request id, model name, request content, or response content.
- Click any row to drill into headers, request body, response body, errors, duration, tokens, and cost.
That's the full usage loop. You're now a competent CCR user. Everything below is "advanced" — come back to it when you need it.
## Advanced: Compose A Model With Tools Using Fusion
Go to the **Fusion** page. Its job: **bundle a base model with a tool capability into a new model option** you can then select in Routing or Profiles just like any normal model.
Typical use cases: give a model the ability to see images, search the web, or call an MCP tool.
### Create A Fusion Model
1. Click **Add Fusion**.
2. Enter a **New model** alias. Name it after the capability — something with a `vision`, `search`, or `tool` suffix.
3. Pick the **Base model** — the one that gives the final answer.
4. Pick a built-in tool or a custom MCP tool under **Tools**.
5. If you chose the image tool, configure the **Vision model**; if you chose search, configure the **Search provider** and its environment variables.
6. Save, then select this Fusion model in Routing or Profiles.
> **Play it safe:** Validate a Fusion model in a dedicated profile first. Once you're sure it behaves, promote it into global Default or a special route.
### Built-In Vision
Select `ccr-fusion-builtins / vision_understand`. Good for screenshot diagnosis, OCR, UI comparison, chart reading, and multi-image analysis.
Key points:
- The **Vision model** must genuinely support image understanding — it's the one that "reads" the image.
- The **Base model** is the one that "answers."
- Test with a single screenshot before dropping it into a complex agent workflow.
### Built-In Web Search
Select `ccr-fusion-builtins / web_search`. Supported providers: Brave, Bing, Google CSE, Serper, SerpAPI, Tavily, and Exa.
Key points:
- Pick a **Search provider** you've actually enabled.
- Fill the API key or environment variables it requires under **Provider configuration**.
- Test with a question that needs current information (today's weather somewhere, for instance).
- If search fails, check the search API key first, then look at Fusion tool errors in Logs.
### Custom MCP Tools
Click **Add custom MCP** and choose a transport:
- **stdio**: a local command-line tool. Fill Command, Arguments, Working directory, and Environment variables.
- **streamable-http / sse**: a remote MCP service. Fill URL and Headers.
- **Discover tools**: lists the tools the MCP server exposes.
- **Request timeout / Startup timeout**: bump these up if the tool or server is slow.
> **Tip:** Only wire stable, predictably-fast MCP tools into Fusion. Validate anything risky in a separate profile first.
## Advanced: Relay Agent Messages Into IM With Bots
Go to **Bots**, configure one, then attach it in **Profiles**.
A Bot forwards an agent's messages into IM, and can hand the task off to your phone after you've been idle for a while. Great for long-running jobs, checking progress remotely, or letting a human take over.
### Setup Steps
1. Open **Bots** and click **Add Bot**.
2. Pick a platform. Supported: Weixin iLink, WeCom, Slack, Discord, Telegram, LINE, Feishu, DingTalk.
3. Pick an auth method — the fields differ by platform (Bot Token, OAuth, App Secret, QR Login, and so on).
4. Fill in whatever the platform asks for: IDs, tokens, secrets, signing secrets, or robot code.
5. Save the bot.
6. Open **Profiles** and edit the agent profile you want to attach it to.
7. Turn on **Bot** and select the bot you just created.
8. Turn on **Forward agent messages** if you want the agent's output relayed too.
9. Turn on **Handoff**, set **Idle seconds**, and pick a scanned Wi-Fi or Bluetooth target if you want phone handoff.
> **Note:** Bots currently only forward agent messages produced inside an app opened through CCR. Messages from the CLI are not forwarded. Handoff target scanning is available in the Electron desktop app.
### Per-Platform Guides
Every platform has its own page with the full walk-through (how to create the app on the platform, a field-by-field mapping, and a troubleshooting FAQ):
- [Slack](/en/bots/slack)
- [Discord](/en/bots/discord)
- [Telegram](/en/bots/telegram)
- [LINE](/en/bots/line)
- [Weixin](/en/bots/weixin-ilink)
- [WeCom](/en/bots/wecom)
- [Feishu](/en/bots/feishu)
- [DingTalk](/en/bots/dingtalk)
## When Something Goes Wrong, Look Here
CCR gives you three debugging surfaces: **Logs** (request history), **Observability** (agent summaries), and **Networking** (temporary raw captures).
### Quick Reference
| Symptom | Check first |
| --- | --- |
| Agent isn't using CCR | Is the Server running, did you Apply the profile, did you open the agent from CCR, is the Scope right |
| Request hits the wrong model | Routing Default, rule order, match conditions, fallback — then resolved model in Logs |
| Provider auth fails (401/403) | API key, credential, Base URL, protocol, extra headers |
| model not found (404) | Is the model in the provider's list, does the model Routing selected actually exist |
| Fusion tool never gets called | Fusion tool selection, does the Vision model support images, is the search key right, MCP Discover tools and timeout |
| Requests time out | Is the upstream itself slow, is a Fusion tool slow, did you set timeout too low |
| Cost suddenly spikes | Filter by model, then look at token mix and request body size |
| One key keeps failing | Filter by Credential; if it's really that key, disable it if needed |
| Bot receives no messages | Is Bot enabled in the profile, was the app opened from CCR, is Forward agent messages on, is the platform token still valid |
| Overview has no data | Are Request logs and Agent analysis on, have any new requests actually come in since |
### About Network Capture
If you need to inspect the rawest possible exchange (request/response summary, headers, query, body, raw), turn on **Server → Capture network** and open **Networking**. You can pause, resume, refresh, and clear captures.
> **Reminder:** Network capture records very complete — and therefore sensitive — information. **Turn it on only while debugging, and off when you're done.** Don't leave it running long-term.
## A Few Habits Worth Keeping
- **Treat secrets as secrets.** API keys, bot tokens, secrets, and usage endpoints are sensitive. Configure them only in a trusted environment, and never send them to an unverified service.
- **Test before you change globals.** Before touching global Routing or Default, validate the change in a separate profile first. Saves you from disrupting an agent that's actively in use.
- **Skim Logs now and then.** A periodic glance at errors, tokens, cost, and latency catches problems before they grow.
- **Keep backup keys for important providers.** Give critical providers several Credentials with priority, weight, and limits, so one dead key doesn't take everything down.
- **Verify deeplinks before importing.** Before importing a provider via a `ccr://provider?...` link, glance at the source, Base URL, protocol, and model list, then confirm.
---
That's the whole loop: **connect providers → set routing → wire in an agent → turn on observability → extend as needed.** From here it's just usage — come back to whichever section matches the problem in front of you. Enjoy.
+96
View File
@@ -0,0 +1,96 @@
---
title: 钉钉 Bot 配置
pageTitle: 钉钉 Bot
eyebrow: Bot 平台
lead: 把 Agent 的消息接入钉钉的企业协作环境,并在电脑锁屏后接力。这一页从钉钉开发者后台创建应用开始,带你走到在 CCR 里跑通。
---
## 这个方式适合谁
钉钉适合把 Agent 消息接入企业协作环境。CCR 用 App Secret 方式连接钉钉应用。
> 还没看过 Bot 总览?先回到主文档的「把 Agent 消息转发到 IM(Bot)」那一节,了解整体流程和 Forward / Handoff 的区别,再回来配单个平台。
## 你会用到哪些字段
| 钉钉后台里的名字 | CCR 字段 | 是否必填 | 说明 |
| --- | --- | --- | --- |
| Client ID / AppKey | App Key | 必填 | 应用标识 |
| Client Secret / AppSecret | App Secret | 必填 | 应用密钥 |
| RobotCode | Robot Code | 可选 | 多机器人或媒体能力场景可能需要 |
> 新版钉钉把机器人作为「应用能力」来配,别从旧的独立「机器人」入口开始。
## 第一步:创建钉钉应用
1. 打开 [钉钉开发者后台](https://open-dev.dingtalk.com/)。
2. 登录钉钉账号。
3. 选要接入的开发组织。
4. 顶部打开 `应用开发`
5.`创建应用`
6. 填应用名,比如 `CCR`
7. 填应用描述,其他先默认。
8. 点创建。
## 第二步:复制 App Key 和 App Secret
1. 进入刚创建的应用详情。
2. 左侧打开 `应用信息``凭证与基础信息`
3. 复制 `Client ID`,对应 CCR 的 App Key。
4. 复制 `Client Secret`,对应 CCR 的 App Secret。
> 钉钉后台可能还显示旧名 `AppKey` / `AppSecret`,按字段名对应复制即可。
## 第三步:开启机器人能力
1. 在应用详情打开 `机器人与消息推送`,或打开 `应用能力` 后选 `机器人`
2. 开启 `机器人配置`
3. 填机器人名称、头像、简介。
4. 消息接收模式选 **Stream 模式**
5. 保存。
6. 页面显示 `RobotCode` 的话,复制下来,待会儿填 Robot Code。
## 第四步:发布应用并加入会话
1. 打开 `版本管理与发布`,创建新版本。
2. 设可见范围,测试时先选你自己或一个测试群。
3. 提交发布。
4. 发布后,在钉钉客户端搜机器人名称。
5. 进机器人会话,或在目标群的群设置里加这个机器人。
## 在 CCR 中接入
1. 打开 CCR 的 **Bots** 页面,点 **Add Bot**
2. 平台选 **钉钉(DingTalk**
3. 认证方式是 **App Secret**
4. 把 Client ID 填进 **App Key**
5. 把 Client Secret 填进 **App Secret**
6. 复制到了 RobotCode 就填 **Robot Code**
7. 保存这个 Bot。
8. 打开 **Profiles**,编辑你要接 Bot 的 Agent Profile。
9. 打开 **Bot** 开关,选刚保存的 Bot。
10. 按需打开 **Forward agent messages****Handoff**(见下一节)。
11. 从 CCR 重新打开 Agent。
## 消息接力:Forward 还是 Handoff
- **Forward agent messages**:不管锁不锁屏都转发,适合要在钉钉里保留完整输出。
- **Handoff(接力)**:只在电脑锁屏后转发,配合 Idle seconds 和目标设备。
> 只想锁屏后提醒,别开 Forward。
## 测试
1. 从 CCR 打开 Agent,触发一条消息。
2. 到钉钉确认应用能收到并回复。
3. 群聊用的话,确认应用或机器人已加进目标群、有发言权限。
> **怎么算成功:** 钉钉里能看到 Agent 消息,你回复后 Agent 也能继续。
## 常见问题
- **认证失败**:重新复制 App Key 和 App Secret。
- **机器人标识相关错误**:检查 Robot Code 和平台后台一致。
- **机器人收不到消息**:确认应用内开了机器人能力、消息接收模式和 CCR 配置一致。
- **用户找不到机器人**:检查应用已发布、可见范围包含当前用户或群成员。
- **接力不触发**:确认电脑已锁屏,检查 Handoff 开关、空闲时间和目标设备。
+101
View File
@@ -0,0 +1,101 @@
---
title: Discord Bot 配置
pageTitle: Discord Bot
eyebrow: Bot 平台
lead: 把 Agent 的消息接入 Discord 的服务器频道或私聊,并在电脑锁屏后把新消息接力到 Discord。这一页从创建 Discord 应用开始,带你一直走到在 CCR 里跑通。
---
## 这个方式适合谁
Discord 适合把 Agent 消息接入服务器频道、私有协作服务器或个人 DM。最常用的是 Bot Token,配置直接。
> 还没看过 Bot 总览?先回到主文档的「把 Agent 消息转发到 IM(Bot)」那一节,了解整体流程和 Forward / Handoff 的区别,再回来配单个平台。
## 你会用到哪些字段
| Discord 后台里的名字 | CCR 字段 | 是否必填 | 说明 |
| --- | --- | --- | --- |
| Token | Bot Token | 必填 | Bot 页里的机器人 token |
| Application ID | Application ID | 可选 | General Information 页里的应用 ID |
| Public Key | Public Key | 可选 | 交互回调场景可能会用到 |
通常用 Bot Token 就够了。只有接入流程明确要求 OAuth 时,才选 OAuth 2.0。
## 第一步:创建 Discord 应用和 Bot
1. 打开 [Discord Developer Portal](https://discord.com/developers/applications)。
2.`New Application`
3. 填名字,比如 `CCR`
4. 创建后进入应用详情。
5. 左侧打开 `Bot`
6. 页面还没有 Bot 的话,点 `Add Bot`
7. 给机器人设头像和用户名。
## 第二步:打开必要权限
1. 仍在 `Bot` 页,找到 `Privileged Gateway Intents`
2. 打开 **Message Content Intent**。没有它,Bot 很可能看不到用户发的消息正文。
3. 要按成员、角色或用户名做判断,再打开 **Server Members Intent**
4. **Presence Intent** 一般不用开,除非你要读在线状态。
## 第三步:复制 Bot Token
1.`Bot` 页找到 `Token`
2.`Reset Token``Copy`
3. 第一次创建时 `Reset Token` 会生成第一个 token,不代表你弄坏了什么。
4. 复制生成的 token,待会儿填进 CCR 的 Bot Token。
> 这个 token 等同于机器人密码。不要发进 Discord,也不要贴进 Agent 的 prompt。
## 第四步:邀请 Bot 进服务器
1. 左侧打开 `OAuth2`
2. 打开 `URL Generator`
3. `Scopes` 勾选 `bot``applications.commands`
4. `Bot Permissions` 至少勾选 `View Channels``Send Messages``Read Message History``Embed Links``Attach Files`
5. 想给审批消息加反应,再勾 `Add Reactions`
6. 复制底部生成的 URL,在浏览器打开,选目标服务器并授权。
## 第五步:复制可选字段(如需要)
如果某处要求填 Application ID 或 Public Key
1. 回到 Discord Developer Portal。
2. 打开应用的 `General Information`
3. 复制 `Application ID``Public Key`
## 在 CCR 中接入
1. 打开 CCR 的 **Bots** 页面,点 **Add Bot**
2. 平台选 **Discord**
3. 认证方式默认是 **Bot Token**,保持即可。
4. 把 Token 填进 **Bot Token**
5. 需要的话补上 **Application ID****Public Key**
6. 保存这个 Bot。
7. 打开 **Profiles**,编辑你要接 Bot 的 Agent Profile。
8. 打开 **Bot** 开关,选刚保存的 Bot。
9. 按需打开 **Forward agent messages****Handoff**(见下一节)。
10. 从 CCR 重新打开 Agent。
## 消息接力:Forward 还是 Handoff
- **Forward agent messages**:不管锁不锁屏都转发,适合调试或要完整记录的频道。
- **Handoff(接力)**:只在电脑锁屏后转发,配合 Idle seconds 和目标设备。
> 只想锁屏后提醒,别开 Forward。
## 测试
1. 从 CCR 打开 Agent,触发一条消息。
2. 到 Discord 确认 Bot 能收到并回复。
3. 用服务器频道的话,确认 Bot 在该服务器里、有发言权限。
> **怎么算成功:** Discord 里能看到 Agent 消息,回复后 Agent 也能继续。
## 常见问题
- **Bot 没响应**:先确认 Bot Token 复制对了。
- **Bot 在线但看不到你发的内容**:检查 Message Content Intent 打开了没。
- **频道里没消息**:确认 Bot 在该服务器里、频道权限允许它发言。
- **邀请链接里看不到权限选项**:确认 OAuth2 URL Generator 勾了 `bot` scope。
- **接力不触发**:确认电脑已锁屏、Handoff 已开,检查空闲时间和目标设备。
+111
View File
@@ -0,0 +1,111 @@
---
title: 飞书 Bot 配置
pageTitle: 飞书 Bot
eyebrow: Bot 平台
lead: 把 Agent 的消息接入飞书的群或应用会话,并在电脑锁屏后接力。这一页从飞书开放平台创建企业自建应用开始,带你走到在 CCR 里跑通。
---
## 这个方式适合谁
飞书适合团队把 Agent 消息接入飞书群或应用会话。CCR 用 App Secret 方式连接飞书应用。
> 还没看过 Bot 总览?先回到主文档的「把 Agent 消息转发到 IM(Bot)」那一节,了解整体流程和 Forward / Handoff 的区别,再回来配单个平台。
## 你会用到哪些字段
| 飞书后台里的名字 | CCR 字段 | 是否必填 | 说明 |
| --- | --- | --- | --- |
| App ID | App ID | 必填 | 应用标识,通常以 `cli_` 开头 |
| App Secret | App Secret | 必填 | 应用密钥 |
| 飞书 / Lark 域 | Domain | 可选 | 国内飞书一般不填;Lark 或特殊域环境再填 |
## 第一步:创建企业自建应用
1. 打开 [飞书开放平台](https://open.feishu.cn/)。
2. 进入 `开发者后台`
3.`创建应用`
4.`企业自建应用`
5. 填应用名,比如 `CCR`
6. 填应用描述并上传图标。
7. 创建应用。
## 第二步:复制 App ID 和 App Secret
1. 进入刚创建的应用。
2. 打开 `基础信息`
3. 进入 `凭证与基础信息`
4. 复制 `App ID`
5. 复制 `App Secret`
这两个就是 CCR 里的必填字段。
## 第三步:开启机器人能力
1. 在应用后台打开 `应用能力`
2.`添加应用能力`
3. 找到 `机器人`,添加或启用。
4. 设机器人名称和头像。
> 没开机器人能力,飞书聊天窗口可能看不到输入框,也收不到用户消息。
## 第四步:申请消息权限
1. 打开 `开发配置`
2. 进入 `权限管理`
3. 添加应用身份权限。
4. 至少开通「读取用户发给机器人的单聊消息」权限。
5. 要在群里 @ 机器人,开通「读取群聊中 @ 机器人消息」权限。
6. 要让 Agent 回复,开通「以应用身份发送消息」权限。
7. 保存。
> 不同租户后台权限名可能略有不同。看到 `im:message.p2p_msg:readonly`、`im:message.group_at_msg:readonly`、`im:message:send_as_bot` 这类标识时,优先选这些。
## 第五步:配置事件订阅
1. 打开 `事件与回调`
2. 选择长连接(或 WebSocket)模式。
3. 添加事件 `im.message.receive_v1`
4. 保存。
## 第六步:发布或安装应用
1. 打开 `版本管理与发布`,创建新版本。
2. 确认可见范围,测试时先选你自己或小范围。
3. 提交发布。
4. 企业要审核的话,等审核通过。
5. 在飞书客户端找到这个应用,或把机器人加进目标群。
## 在 CCR 中接入
1. 打开 CCR 的 **Bots** 页面,点 **Add Bot**
2. 平台选 **飞书(Feishu**
3. 认证方式是 **App Secret**
4.**App ID****App Secret**
5. 用 Lark 或特殊域环境,再填 **Domain**
6. 保存这个 Bot。
8. 打开 **Profiles**,编辑你要接 Bot 的 Agent Profile。
9. 打开 **Bot** 开关,选刚保存的 Bot。
10. 按需打开 **Forward agent messages****Handoff**(见下一节)。
11. 从 CCR 重新打开 Agent。
## 消息接力:Forward 还是 Handoff
- **Forward agent messages**:不管锁不锁屏都转发,适合要在飞书里保留完整输出。
- **Handoff(接力)**:只在电脑锁屏后转发,配合 Idle seconds 和目标设备。
> 只想锁屏后提醒,别开 Forward。
## 测试
1. 从 CCR 打开 Agent,触发一条消息。
2. 到飞书确认应用能收到并回复。
3. 群里用的话,先把应用加进目标群、确认成员可见。
> **怎么算成功:** 飞书里能看到 Agent 消息,你回复后 Agent 也能继续。
## 常见问题
- **认证失败**:重新复制 App ID 和 App Secret。
- **聊天窗口没输入框**:检查机器人能力开了没、事件订阅了没、应用发布到当前成员可见范围了没。
- **群里没响应**:先 @ 机器人测,确认事件订阅含 `im.message.receive_v1`
- **Lark / 特殊域**:确认 Domain 填的是平台要求的值。
+88
View File
@@ -0,0 +1,88 @@
---
title: LINE Bot 配置
pageTitle: LINE Bot
eyebrow: Bot 平台
lead: 把 Agent 的消息接入 LINE 的好友、群聊或 Official Account,并在电脑锁屏后把新消息接力过去。这一页从创建 LINE Messaging API channel 开始,带你走到在 CCR 里跑通。
---
## 这个方式适合谁
LINE 适合把 Agent 消息接入已有的 LINE 好友、群聊或 LINE Official Account。CCR 用 Channel Access Token 作为主要认证字段。
> 还没看过 Bot 总览?先回到主文档的「把 Agent 消息转发到 IM(Bot)」那一节,了解整体流程和 Forward / Handoff 的区别,再回来配单个平台。
## 你会用到哪些字段
CCR 里 LINE 的认证方式叫 **Bot Token**,但填的不是 Telegram 那种 token,而是下面这两个 channel 字段:
| LINE 后台里的名字 | CCR 字段 | 是否必填 | 说明 |
| --- | --- | --- | --- |
| Channel access token | Channel Access Token | 必填 | 让 Bot 调用 LINE Messaging API |
| Channel secret | Channel Secret | 建议填 | 用来校验 LINE 发来的请求 |
## 第一步:创建 Messaging API channel
1. 打开 [LINE Developers Console](https://developers.line.biz/console/)。
2. 登录 LINE 账号。
3. 创建一个 Provider,或选已有的。
4.`Create a new channel`
5.`Messaging API`
6. 按页面要求填 Channel 名称、描述、图标、分类等。
7. 创建后进入这个 Messaging API channel。
> 已有 LINE Official Account 的话,也可以在该账号设置里启用 Messaging API,再回控制台复制凭证。
## 第二步:复制 Channel Secret
1. 进入刚创建的 Messaging API channel。
2. 打开 `Basic settings`
3. 找到 `Channel secret`,复制,待会儿填到 CCR 的 Channel Secret。
## 第三步:签发 Channel Access Token
1. 打开 `Messaging API` 标签页。
2. 找到 `Channel access token`
3.`Issue``Reissue`
4. 复制生成的 token,待会儿填到 CCR 的 Channel Access Token。
> 优先用长效 token。重新签发会让旧 token 失效,要同步更新 CCR。
## 第四步:打开聊天入口
1. 要群聊就把 `Allow bot to join group chats` 打开。
2. 建议关掉 LINE 官方账号的自动回复,免得用户同时收到默认回复和 Agent 回复。
## 在 CCR 中接入
1. 打开 CCR 的 **Bots** 页面,点 **Add Bot**
2. 平台选 **LINE**
3. 认证方式是 **Bot Token**(这就是 LINE 在 CCR 里的固定认证方式)。
4. 把 token 填进 **Channel Access Token**
5. 把 secret 填进 **Channel Secret**
6. 保存这个 Bot。
7. 打开 **Profiles**,编辑你要接 Bot 的 Agent Profile。
8. 打开 **Bot** 开关,选刚保存的 Bot。
9. 按需打开 **Forward agent messages****Handoff**(见下一节)。
10. 从 CCR 重新打开 Agent。
## 消息接力:Forward 还是 Handoff
- **Forward agent messages**:不管锁不锁屏都转发,适合要在 LINE 里看完整输出时。
- **Handoff(接力)**:只在电脑锁屏后转发,配合 Idle seconds 和目标设备。
> 只想锁屏后提醒,别开 Forward。
## 测试
1. 从 CCR 打开 Agent,触发一条消息。
2. 到 LINE 确认机器人能收到并回复。
3. 群聊用的话,确认机器人已经进群、有发言权限。
> **怎么算成功:** LINE 里能看到 Agent 消息,你回复后 Agent 也能继续。
## 常见问题
- **认证失败**:重新复制 Channel Access Token。
- **能发不能收**:确认 Channel Access Token 有效、CCR 已启动并连上 LINE。
- **群聊不可用**:确认 `Allow bot to join group chats` 打开了,把 Bot 重新加进群。
- **只想锁屏后提醒**:别开 Forward,只开 Handoff。
+92
View File
@@ -0,0 +1,92 @@
---
title: Slack Bot 配置
pageTitle: Slack Bot
eyebrow: Bot 平台
lead: 把 Agent 的消息接入 Slack 的频道或私聊,并在电脑锁屏后把新消息接力到 Slack。这一页从创建 Slack 应用开始,一直带你走到在 CCR 里跑通。
---
## 这个方式适合谁
Slack 适合团队把 Agent 消息接入已有的频道、私聊或工作区应用。你需要准备一个 Bot Token 和一个 App Token。
> 还没看过 Bot 总览?先回到主文档的「把 Agent 消息转发到 IM(Bot)」那一节,了解 Bot 的整体流程、Forward 和 Handoff 的区别,再回来配单个平台。
## 你会用到哪些字段
| Slack 后台里的名字 | CCR 字段 | 长什么样 | 什么时候需要 |
| --- | --- | --- | --- |
| Bot User OAuth Token | Bot Token | `xoxb-...` | 必填,让 Bot 收发消息 |
| App-Level Token | App Token | `xapp-...` | 让 Socket Mode 建立连接 |
## 第一步:创建 Slack 应用
1. 打开 [Slack API Apps](https://api.slack.com/apps)。
2.`Create New App`
3.`From scratch`
4. 填应用名,比如 `CCR`
5. 选要接入的 Slack workspace。
6.`Create App`
## 第二步:打开 Socket Mode
1. 在应用左侧打开 `Socket Mode`
2. 打开 `Enable Socket Mode`
3. 页面提示需要 App-Level Token 时,点创建 token。
4. Token 名字随便填,比如 `ccr-socket`
5. Scope 选 `connections:write`
6. 创建后复制 `xapp-...` 开头的 App-Level Token,待会儿填到 CCR 的 App Token。
## 第三步:添加 Bot 权限并安装
1. 左侧打开 `OAuth & Permissions`
2. 找到 `Scopes` 里的 `Bot Token Scopes`
3. 至少加这几个 scope`app_mentions:read``channels:history``channels:read``chat:write``im:history``im:read``im:write`
4. 要收发文件再加 `files:read``files:write`
5. 要在私有频道用再加 `groups:history``groups:read`
6. 回到页面顶部点 `Install to Workspace`,授权。
7. 安装后复制 `Bot User OAuth Token``xoxb-` 开头),待会儿填到 CCR 的 Bot Token。
## 第四步:把 Bot 拉进目标频道
只在私聊用的话可以跳过。
1. 打开 Slack 目标频道。
2. 在消息框输入 `/invite @你的Bot名字`
3. 发送后确认成员列表里能看到这个 Bot。
> 没把 Bot 邀请进频道,它通常只能收到私聊,看不到频道消息。
## 在 CCR 中接入
1. 打开 CCR 的 **Bots** 页面,点 **Add Bot**
2. 平台选 **Slack**
3. 认证方式默认是 **Bot Token**,保持即可(除非你明确要走 OAuth 流程)。
4.`xoxb-...` 填进 **Bot Token**
5.`xapp-...` 填进 **App Token**
6. 保存这个 Bot。
8. 打开 **Profiles**,编辑你要接 Bot 的那个 Agent Profile。
9. 打开 **Bot** 开关,选刚保存的 Bot。
10. 按需打开 **Forward agent messages****Handoff**(见下一节)。
11. 从 CCR 重新打开 Agent。
## 消息接力:Forward 还是 Handoff
- **Forward agent messages**:不管电脑锁没锁屏,都把 Agent 的新消息转发到 Slack。适合要完整记录或调试时。
- **Handoff(接力)**:只在电脑锁屏后才转发。配合 **Idle seconds**(锁屏后空闲多久才接力)和 Wi-Fi / 蓝牙目标设备一起用。
> 只想锁屏后收到提醒?开 Handoff 就行,别开 Forward,否则消息会很密。
## 测试
1. 从 CCR 打开 Agent,触发一条消息。
2. 到 Slack 里确认 Bot 能收到并回复。
3. 用频道的话,先确认 Bot 已经在频道里。
> **怎么算成功:** Slack 里能看到 Agent 的消息,你回复后 Agent 也能接着处理。
## 常见问题
- **消息没进 Slack**:先确认 Bot Token 还有效,再确认应用在目标频道里。
- **Socket Mode 连不上**:检查 App Token 是不是 `xapp-` 开头、有没有 `connections:write` scope。
- **频道没响应、私聊有响应**:通常是 Bot 没进频道,或缺少 `channels:*` / `groups:*` 权限。补 scope 后要重新安装到 workspace。
- **消息太多**:关掉 Forward,只留 Handoff。
+81
View File
@@ -0,0 +1,81 @@
---
title: Telegram Bot 配置
pageTitle: Telegram Bot
eyebrow: Bot 平台
lead: 把 Agent 的消息接入 Telegram,并在电脑锁屏后把新消息接力过去。Telegram 是所有平台里配置最简单的——只需要一个 Bot Token,几分钟就能跑通。
---
## 这个方式适合谁
Telegram 适合个人或小团队快速接收 Agent 消息。字段最少,只需要 `Bot Token`。如果你只想最快跑通一个 Bot,从 Telegram 开始最省事。
> 还没看过 Bot 总览?先回到主文档的「把 Agent 消息转发到 IM(Bot)」那一节,了解整体流程和 Forward / Handoff 的区别,再回来配单个平台。
## 你会用到哪些字段
| Telegram 里的名字 | CCR 字段 | 是否必填 | 说明 |
| --- | --- | --- | --- |
| HTTP API token | Bot Token | 必填 | `@BotFather` 创建机器人后返回的 token |
## 第一步:用 BotFather 创建机器人
1. 打开 Telegram。
2. 搜索 `@BotFather`,确认用户名完全一致(官方机器人)。
3. 进会话后发 `/newbot`
4. 按提示输入机器人显示名,比如 `CCR Assistant`
5. 再输入机器人用户名——必须以 `bot` 结尾,比如 `ccr_demo_bot`
6. 创建成功后,`@BotFather` 会返回一段 HTTP API token。
7. 复制这段 token,待会儿填进 CCR 的 Bot Token。
> **别把 token 发给任何人。** 拿到 token 的人就能完全控制你的 Telegram Bot。
## 第二步:按需设置群聊能力
只用私聊的话可以跳过。
要在群里用:
1.`@BotFather``/setjoingroups`
2. 选你的机器人。
3. 选允许加入群组。
4. 想让 Bot 看到群里所有消息,发 `/setprivacy`
5. 再次选刚才的 Bot。
6.`Disable` 关闭隐私模式。
7. 把 Bot 加进目标群。
> Telegram 隐私模式打开时,Bot 通常只能看到命令、@ 它的消息和部分服务消息。关掉隐私模式后,建议把 Bot 移出群再重新加,让设置立刻生效。
## 在 CCR 中接入
1. 打开 CCR 的 **Bots** 页面,点 **Add Bot**
2. 平台选 **Telegram**
3. 认证方式是 **Bot Token**
4. 把 token 填进 **Bot Token**
5. 保存这个 Bot。
6. 打开 **Profiles**,编辑你要接 Bot 的 Agent Profile。
7. 打开 **Bot** 开关,选刚保存的 Bot。
8. 按需打开 **Forward agent messages****Handoff**(见下一节)。
9. 从 CCR 重新打开 Agent。
## 消息接力:Forward 还是 Handoff
- **Forward agent messages**:不管锁不锁屏都转发,适合要在 Telegram 里看完整输出时。
- **Handoff(接力)**:只在电脑锁屏后转发,配合 Idle seconds 和目标设备。
> 只想锁屏后提醒,别开 Forward。
## 测试
1. 从 CCR 打开 Agent,触发一条消息。
2. 到 Telegram 确认机器人能收到并回复。
3. 群里用的话,先确认机器人已经进群、能读写消息。
> **怎么算成功:** Telegram 里能看到 Agent 消息,你回复后 Agent 也能继续。
## 常见问题
- **认证失败**:重新复制 Bot Token。
- **私聊可用、群不可用**:检查机器人进群了没、群权限允不允许它读消息。
- **群里只有 `/command` 能触发**:检查 `@BotFather``/setprivacy`,或把 Bot 设为群管理员。
- **重置过 token**:旧 token 立刻失效,要回 CCR 更新并重启。
- **消息太多**:关掉 Forward,只留 Handoff。
+85
View File
@@ -0,0 +1,85 @@
---
title: 企业微信 Bot 配置
pageTitle: 企业微信 Bot
eyebrow: Bot 平台
lead: 把 Agent 的消息接入企业微信,让团队成员在企业微信里接收并回复,并在电脑锁屏后接力。这一页从企业微信管理后台创建自建应用开始,带你走到在 CCR 里跑通。
---
## 这个方式适合谁
企业微信适合把 Agent 接入企业内部消息环境,让团队成员在企业微信里接收 Agent 消息并回复。
> 还没看过 Bot 总览?先回到主文档的「把 Agent 消息转发到 IM(Bot)」那一节,了解整体流程和 Forward / Handoff 的区别,再回来配单个平台。
## 你会用到哪些字段
| 企业微信后台里的名字 | CCR 字段 | 是否必填 | 说明 |
| --- | --- | --- | --- |
| 企业ID / CorpID | Corp ID | 必填 | 企业级标识,在「我的企业」里 |
| AgentId | Agent ID | 必填 | 自建应用的应用 ID |
| Secret | Secret | 必填 | 自建应用密钥,通常要管理员在手机端确认查看 |
> CCR 会用 Corp ID 和应用 Secret 去换企业微信接口的 access_token,你不用自己手动获取。
## 第一步:获取 Corp ID
1. 打开 [企业微信管理后台](https://work.weixin.qq.com/wework_admin/frame)。
2. 用管理员账号登录。
3. 顶部打开 `我的企业`
4. 进入 `企业信息`
5. 找到 `企业ID`,复制,待会儿填到 CCR 的 Corp ID。
## 第二步:创建自建应用
1. 在管理后台打开 `应用管理`
2. 找到 `自建` 区域。
3.`创建应用`
4. 填应用名,比如 `CCR`
5. 上传应用 Logo。
6. 选可见范围。测试时先选你自己或一个小测试部门。
7. 点创建。
## 第三步:复制 Agent ID 和 Secret
1. 进入刚创建的自建应用详情。
2. 复制 `AgentId`,待会儿填到 CCR 的 Agent ID。
3. 找到 `Secret`,点查看。
4. 按企业微信提示,在手机企业微信里确认。
5. 复制显示出的 `Secret`
> 如果企业微信要求配 `企业可信 IP`,需要把运行 CCR Bot Gateway 的出口公网 IP(或你用的中继服务出口 IP)加进去。
## 在 CCR 中接入
1. 打开 CCR 的 **Bots** 页面,点 **Add Bot**
2. 平台选 **企业微信(WeCom**
3. 认证方式是 **App Secret**
4.**Corp ID**、**Agent ID**、**Secret**。
5. 保存这个 Bot。
6. 打开 **Profiles**,编辑你要接 Bot 的 Agent Profile。
7. 打开 **Bot** 开关,选刚保存的 Bot。
8. 按需打开 **Forward agent messages****Handoff**(见下一节)。
9. 从 CCR 重新打开 Agent。
## 消息接力:Forward 还是 Handoff
- **Forward agent messages**:不管锁不锁屏都转发。会增加消息量,只在要完整记录或排查问题时用。
- **Handoff(接力)**:只在电脑锁屏后转发,配合 Idle seconds 和目标设备。
> 只想锁屏后提醒,别开 Forward。
## 测试
1. 从 CCR 打开 Agent,触发一条消息。
2. 到企业微信确认应用能收到并回复。
3. 锁屏电脑,等过你设的空闲时间,确认接力触发后新消息会进企业微信。
> **怎么算成功:** 企业微信里能看到 Agent 消息,你回复后 Agent 也能继续。
## 常见问题
- **认证失败**:重新复制 Corp ID、Agent ID 和 Secret。
- **能启动但收不到消息**:检查企业微信应用是否允许接收消息、当前成员有没有使用权限。
- **发送失败提示 IP 不可信**:回企业微信后台配 `企业可信 IP`
- **部分成员看不到应用**:检查自建应用的可见范围。
- **接力不触发**:确认电脑已锁屏,检查 Handoff 开关、空闲时间和目标设备。
@@ -0,0 +1,78 @@
---
title: 微信 Bot 配置
pageTitle: 微信 Bot
eyebrow: Bot 平台
lead: 把 Agent 的消息接入微信,并在电脑锁屏后把新消息接力过去。微信最简单的接法是二维码登录,不用手动复制任何 token,扫一下就行。
---
## 这个方式适合谁
微信适合个人把 Agent 消息接入常用聊天窗口。最简单的方式是二维码登录,不需要手动复制 token。
> 还没看过 Bot 总览?先回到主文档的「把 Agent 消息转发到 IM(Bot)」那一节,了解整体流程和 Forward / Handoff 的区别,再回来配单个平台。
## 两种登录方式
| 方式 | 需要准备 | 适合谁 |
| --- | --- | --- |
| QR Login(二维码登录) | 能扫码确认的微信账号 | 大多数个人用户 |
| Bot Token | 外部微信 Bot 服务或 iLink 插件给的 token | 已经有第三方微信 Bot 服务的用户 |
> **建议优先用二维码登录。** 微信登录态和账号安全强相关,建议用专门的 Bot 账号,别用绑定支付、客服或重要联系人的主账号。
## 方式一:二维码登录(推荐)
1. 打开 CCR 的 **Bots** 页面,点 **Add Bot**
2. 平台选 **微信(Weixin iLink**
3. 认证方式选 **QR Login**(这是默认项)。
4. CCR 会弹出一个二维码窗口。
5. 用手机微信扫这个码。
6. 在手机上确认登录。
7. 等 CCR 显示登录成功。
8. 保存这个 Bot。
> 二维码会过期。如果扫码页提示过期,关掉登录窗口重新开始扫。
## 方式二:Bot Token
只有当你已经有外部微信 Bot 服务、iLink 服务或插件提供的 token 时才用这个方式。
1. 在提供方后台或本地插件输出里复制 `Bot Token`
2. 提供方同时给了 `Account ID` 的话一起复制。
3. 给了 `User ID` 的话也一起复制。
4. 打开 CCR 的 **Bots** 页面,点 **Add Bot**
5. 平台选 **微信**,认证方式选 **Bot Token**
6.**Bot Token**,按需填 **Account ID****User ID**
7. 保存这个 Bot。
## 在 CCR 中绑定到 Agent
不管用哪种登录方式,都要再把 Bot 绑到 Agent Profile 上:
1. 打开 **Profiles**,编辑你要接 Bot 的 Agent Profile。
2. 打开 **Bot** 开关,选刚保存的 Bot。
3. 按需打开 **Forward agent messages****Handoff**(见下一节)。
4. 从 CCR 重新打开 Agent。
## 消息接力:Forward 还是 Handoff
- **Forward agent messages**:不管锁不锁屏都转发,适合要在微信里看每条 Agent 输出。
- **Handoff(接力)**:只在电脑锁屏后转发。配合 **Idle seconds**(锁屏后空闲多久才接力)和 Wi-Fi / 蓝牙目标设备。
> 只想锁屏后收到提醒?开 Handoff 就行,别开 Forward。
## 测试
1. 从 CCR 打开 Agent,触发一条消息。
2. 到微信确认 Bot 能收到并回复。
3. 锁屏电脑,等过你设的空闲时间,确认 Agent 新消息会自动进微信。
> **怎么算成功:** 微信里能看到 Agent 消息,你回复后 Agent 也能继续。
## 常见问题
- **二维码过期**:关掉登录窗口重新扫码。
- **扫码成功但消息没转发**:确认 Agent Profile 重启过、Bot 开关还开着。
- **扫码后很快掉线**:确认手机和电脑网络稳定;检查微信是不是在别的设备上重新登录导致登录态失效。
- **Token 方式连不上**:重新复制 Bot Token,避免复制到过期值或多余空格。
- **第三方服务要 Account ID / User ID**:确认这些字段来自同一个账号,别混用不同账号的 token 和 ID。
+366
View File
@@ -0,0 +1,366 @@
---
title: Claude Code Router 使用指南
pageTitle: 使用指南
eyebrow: 上手指南
lead: 这是一份手把手带你跑通 Claude Code Router 的指南。我们会从下载安装开始,一步步接入你的第一个模型、把 Agent 接进来、看到请求真的走了 CCR。不需要你手动编辑任何配置文件。
---
如果你是第一次用 CCR,建议从头顺着读一遍——大概十几分钟,你就能把整套链路跑通。已经有经验的读者可以直接跳到对应章节,每一步都包含了「怎么确认自己没做错」的验证方法。
## CCR 到底帮你做了什么
一句话:**它把你电脑上的各种 AI AgentClaude Code、Codex、ZCode 等)统一接到一个本地入口,再由你决定每个请求用哪个模型。**
这样做的好处是:
- 你不用在每个 Agent 里重复配置模型和 Key,CCR 统一管理。
- 不同的任务可以走不同的模型——简单任务用便宜的快模型,难题用强模型,看图用多模态模型,需要联网的用带搜索的模型。
- 一个模型挂了能自动切换到备用模型,不用你手动改配置。
- 所有请求都被记录下来,你能看到钱花在哪、哪个模型慢、哪个 Key 报错了。
CCR 跑在你自己机器上,配置都存在本地。它对外只暴露两个本地地址,你日常只需要关心其中一个:
| 用途 | 地址 | 说明 |
| --- | --- | --- |
| Agent 连接入口 | `http://127.0.0.1:3456` | 你的 Agent 就连这个 |
| CCR 内部服务 | `http://127.0.0.1:3457` | 内部用,不用管 |
后面所有的配置,目标都是让 Agent 把请求发到 `3456` 这个入口。
## 第一步:安装并启动 CCR
### 下载安装
1. 打开 [GitHub Releases](https://github.com/musistudio/claude-code-router/releases) 页面。
2. 按你的系统下载对应的安装包:
- macOS`.dmg``.zip`
- Windows`.exe`
- Linux`.AppImage`
3. 像装普通软件一样安装并打开 **Claude Code Router**
第一次启动时,CCR 会在本地生成一份配置文件,路径如下(备份或排查问题时会用到,平时不用手动改):
- macOS / Linux`~/.claude-code-router/config.json`
- Windows`%APPDATA%\Claude Code Router\config.json`
### 启动本地网关
打开 App 后,进入 **Server** 页面,点击 **Start** 启动本地网关。
> **怎么算成功:** Server 页面显示网关正在运行,端口 `3456` 处于监听状态。如果你想以后开 App 就自动起网关,把 **Auto start** 打开。
到这里 CCR 本身就跑起来了,但还干不了活——因为它还不知道该把请求转发给谁。下一步我们就来接入模型。
## 第二步:接入你的第一个 Provider
Provider 就是 CCR 转发请求要去到的「上游模型服务」,比如 OpenRouter、DeepSeek、Z.AI,或者任何兼容 OpenAI / Anthropic / Gemini 协议的服务。
### 添加 Provider
1. 进入 **Providers** 页面,点击 **Add Provider**
2. 先在 **Provider preset** 里挑一个内置预设。预设的好处是它会自动帮你填好常见的 Base URL、图标和协议,省得你查文档。如果你的服务不在列表里,选 **Other / custom API endpoint**
3. 依次填写:
- **Name**:在 CCR 里显示的名字,起个短一点、认得出的就行,比如 `openrouter``deepseek`
- **Base URL**:上游服务地址。自定义 Provider 一定要确认地址里包含了正确的 API 路径。
- **Protocol**:上游真正支持的协议。选错了通常连通性检查就过不了。拿不准的话用下面的协议探测。
- **API Key**:填你的 Key。只用一个 Key 的话填这里就够了。
- **Models**:把这个 Provider 要暴露给 CCR 的模型列出来,模型选择器就是从这里取选项的。
4. 填完别急着保存,先做连通性检查(见下)。
### 协议怎么选
| 协议 | 适用场景 |
| --- | --- |
| OpenAI Chat Completions | 绝大多数 OpenAI 兼容服务(最常见) |
| OpenAI Responses | 支持 Responses API 的服务 |
| Anthropic Messages | Anthropic 官方或兼容它的服务 |
| Gemini Generate Content | Gemini 官方或兼容它的服务 |
> **拿不准协议时:** 先用 App 自带的协议探测(Protocol probe)扫一下。探测结果只是参考,最终还是要以 Provider 官方文档和连通性检查为准。
### 保存前做这三项检查
把问题在这一步卡住,后面 Routing 和 Agent 出错时就不会乱猜了:
1. **Protocol probe**:确认这个 Base URL 到底支持哪些协议。
2. **Model connectivity check**:挑一两个模型实际发个测试请求,看能不能通。
3. **Account usage test**(可选):如果你开了用量统计,顺便确认余额和配额能正常读出来。
三项都绿了,再点保存。
> **怎么算成功:** 保存后这个 Provider 出现在列表里,至少一个模型的状态是可用的。你可以随手发一个测试请求,应该能拿到正常响应。
### 想用多个 Key?(可选)
个人用单 Key 够了;如果是团队或高频调用,建议打开 **Credentials** 加多条 Key,CCR 会自动轮换:
1. 在 Provider 表单里打开 **Credentials**
2.**Add credential**
3. 给每条 Key 填一个 **Label**,方便在 Logs 里认出来。
4.**Priority**:数字越小越优先用。
5.**Weight**:同一优先级里,权重越大分到的请求越多。
6. 如果 Key 有配额限制,填 **Limits**,方便看出哪条快到顶了。
7. 保存后发几条测试请求,到 Logs 里按 Credential 筛一下,确认轮换符合预期。
### 想在面板上看余额?(可选)
如果你希望 Overview 上直接显示某个 Provider 的余额、剩余配额,就打开 **Account / Usage**
1. 选一个 usage 接入方式。有内置标准接口就优先用内置;覆盖不到再考虑 HTTP JSON 或 Plugin。
2. 填好认证方式和 endpoint。
3.**Test** 读一次数据。
4. 从返回结果里勾选「余额、剩余量、已用量、重置时间」等字段。
5. 回到 Overview,加一个 **Account balance** 组件。
> **安全提醒:** 别把 Provider 的 API Key 发给来路不明的 usage endpoint。自定义 endpoint 一定要先确认域名和权限范围再填。
接入 Provider 之后,CCR 已经知道「有哪些模型可以用」了,但它还不知道「该用哪一个」。下一步就来定路由。
## 第三步:设置路由(决定请求去哪个模型)
进入 **Routing** 页面。这里控制的是:**不同的请求,分别交给哪个模型处理。**
最核心的一项是 **Default route**——它是所有没有命中任何特殊规则时的兜底模型。先把这一项配好,你就已经有一个能用的最小配置了。
### 推荐的配置顺序
1. **Default**:选一个稳定、能扛主要任务的模型。这是你的「主力」。
2. **Background**:选一个便宜、快的模型,用来跑后台总结、上下文压缩这类不重要的活。
3. **Thinking**:选一个推理能力强的模型,留给需要深度思考的任务。
4. **Long context**:选一个上下文窗口大的模型,并设好触发阈值(超过多少 token 才切到它)。
5. **Image**:如果图片任务你想走 Fusion 或某个多模态模型,在这里指定。
6. **Web search**:如果搜索任务走 Fusion,在这里指定。
7. **Fallback**:选一个模型挂掉时的兜底策略。常见做法是先 retry,失败再按一条 model chain 依次尝试备用模型。
> **新手建议:** 第一次配置时,把 Default 配好就够了。Background、Thinking 这些都可以等你有需要了再回来加。
### 需要更精细的控制?(可选)
**Add Routing Rule** 可以加规则。每种规则的用途:
| 规则类型 | 什么时候用 |
| --- | --- |
| model-prefix | 客户端传了特定模型名前缀时,分流到指定模型 |
| subagent | 按 subagent 信号选模型 |
| thinking / long-context / image / web-search | 按任务类型分流 |
| condition | 按请求字段、Header 或请求体内容匹配 |
| rewrite | 命中规则后改写请求体的某些字段,处理少数兼容性问题 |
> **怎么算成功:** 保存后随便发一个请求,打开 **Logs**,看这条记录里的 `request model`、`resolved provider`、`resolved model` 和状态码。如果命中的不是你想要的模型,优先检查:规则的顺序、匹配条件、以及 fallback 设置。
到这一步,CCR 已经知道「有哪些模型」和「请求该去哪个模型」了。最后一步,是让你的 Agent 真的把请求发给 CCR。
## 第四步:让 Agent 走 CCRProfile
进入 **Profiles** 页面。Profile 的作用,就是把你选定的 AgentClaude Code / Codex / ZCode)的配置,指向 CCR 的入口 `http://127.0.0.1:3456`,这样它的请求才会经过 CCR 路由和记录。
开始前,先理解两个通用选项:
- **Scope**
-**Only opened from CCR**:只在你从 CCR 里点开这个 Agent 时才走 CCR,不影响你系统里原本的 Agent 设置。**强烈建议先用这个**,方便试用又不出岔子。
-**System default**:让这个 Agent 默认就走 CCR。等你确认稳定了再切到这个。
- **Surface**:选 APP、CLI 或自动,取决于你打算从哪里启动这个 Agent。
- **Model**:可以选某个 Provider 模型,也可以选 Fusion 模型。
> **一条通用习惯:** Apply 之后,尽量从 CCR 里的「打开 Agent」按钮来启动它,这样 Bot、App 相关的能力才能生效。
### Claude Code
1. 在 Profiles 里选 **Claude Code**
2.**Model**(常规请求的模型)。
3. 如果想让后台轻量任务用便宜模型,设一下 **Small fast model**
4. 确认 **Settings file** 指向你本机 Claude Code 的配置路径(默认值通常就对)。
5. 需要额外环境变量就在 **Env** 里加。
6.**Apply**
7.**Open Agent** 从 CCR 启动 Claude Code。
> **验证:** 发一次请求,然后打开 **Logs**。这条记录的 Client 应该显示为 Claude CodeProvider 和模型应该和你在 Routing 里配的一致。如果是,说明整条链路通了。
### Codex
1. 在 Profiles 里选 **Codex**
2. 确认 **Provider ID****Provider Name**(默认值一般直接能用)。
3.**Model**,可以是普通 Provider 模型,也可以是 Fusion 模型。
4. 确认 **Config file**(默认是 Codex 的配置文件路径)。
5. 如果你用的是特定版本的 Codex CLI,填 **Codex CLI path****Codex home**;用默认安装的话不用填。
6. 按需打开 **CLI middleware****Show all sessions**
7.**Apply**,从 CCR 打开 Codex。
试用阶段用 **Only opened from CCR**,确认稳定后再改成 **System default**
### ZCode
ZCode 走的是 App surface。配置时关注 **Model**、**Provider ID**、**Provider Name**,以及是否从 CCR 打开。它不需要 Codex CLI 那些字段。
### 复用本机已登录的 Agent(可选)
如果你这台机器已经登录过 Claude Code、Codex 或 ZCode,可以在 **Providers** 里把它们导入成 **Local Agent Provider**。导入后它们就像普通 Provider 一样出现在模型选择器里,适合复用已有的本地授权,不用再去申请 Key。
到这里,你已经拥有了一套**完整可用的最小系统**:Provider 接好了 → 路由配好了 → Agent 接进来了。下一步我们打开观察面板,确认一切真的在按你的预期运转。
## 第五步:打开观察面板,确认请求走了 CCR
这一步的目的是让你「看得见」——看得见请求来了没有、走了哪个模型、花了多少钱、有没有报错。
### 先把开关打开
Overview 想有数据,得先让 CCR 开始记录。去 **Settings → Observability**
1. 打开 **Request logs**(这是 Logs 和大部分 Overview 组件的数据来源,**必开**)。
2. 打开 **Agent analysis**(如果你想在 Observability 里看 Agent 维度的汇总)。
3. **Capture network**(在 Server → Proxy 里)只在需要看原始网络包时才开,排查完记得关掉,因为它记录的信息更完整、也更敏感。
### 看什么:Overview
进入 **Overview**,点 **Edit widgets** 可以加组件。几个最常用的:
| 组件 | 回答的问题 |
| --- | --- |
| System status | 网关在不在跑、最近有没有活动 |
| Requests / Success rate | 请求量多少、成功率多少 |
| Estimated cost | 钱花多少了 |
| Token mix | 输入/输出/缓存/推理 token 各占多少 |
| Model distribution | 哪些模型被用得最多 |
| Provider analysis | 哪个 Provider 请求多、延迟高、报错多 |
| Account balance | 各 Provider 余额、配额还剩多少 |
你可以拖动调整位置和大小、切换展示样式、删除或重置成默认布局。几个现成的布局思路:
- **日常盯盘**System status、Requests、Success rate、Usage trend、Provider analysis。
- **盯成本**Estimated cost、Token mix、Model distribution、Account balance。
- **查性能**Average latency、Errors、Provider analysis、Logs。
> **Overview 的定位:** 它回答的是「整体趋势正不正常」。一旦你看到某个数字不对劲(比如某模型成本突然飙高),再跳到 Logs 看具体某条请求。不要只用 Overview 去判断单次失败的原因。
### 看什么:Logs
**Logs** 是你排查单条请求的主战场。前提是 Request logs 已打开。
常用的玩法:
- 按**状态**筛成功或失败的请求。
-**Provider / Model** 筛某个上游服务。
-**Credential** 筛某条 API Key。
- 用搜索框找 request id、模型名、请求内容或响应内容。
- 点开任意一行,看 Header、请求体、响应体、报错、耗时、token、成本。
到这里,你就已经是一名合格的 CCR 使用者了。后面的内容是「进阶」——需要的时候再回来看。
## 进阶一:用 Fusion 把模型和工具组合起来
进入 **Fusion** 页面。它的作用是:**把「一个基础模型 + 一种工具能力」打包成一个新的模型选项**,保存后你能在 Routing 或 Profiles 里像选普通模型一样选它。
典型场景:让某个模型能看图、能联网搜索,或者能调用某个 MCP 工具。
### 创建一个 Fusion 模型
1.**Add Fusion**
2.**New model** 里填一个别名。建议起个能体现能力的名字,比如带 `vision``search``tool` 后缀。
3.**Base model** 里选负责最终回答的模型。
4.**Tools** 里选内置工具或自定义 MCP 工具。
5. 选了图像工具,就接着配 **Vision model**;选了搜索工具,就接着配 **Search provider** 和相关环境变量。
6. 保存后,去 Routing 或 Profiles 里选这个 Fusion 模型。
> **稳妥做法:** 先用一个独立的 Profile 验证 Fusion 模型好不好用,确认没问题了,再把它放到全局 Default 或特殊路由里。
### 内置图像能力
`ccr-fusion-builtins / vision_understand`。适合截图诊断、OCR、UI 对比、图表解读、多图分析。
要点:
- **Vision model** 必须是真正支持图像理解的模型——它负责「看懂图」。
- **Base model** 负责「给出最终答案」。
- 先拿一张截图测通了,再把它塞进复杂的 Agent 工作流。
### 内置联网搜索
`ccr-fusion-builtins / web_search`。支持的搜索服务有:Brave、Bing、Google CSE、Serper、SerpAPI、Tavily、Exa。
要点:
- **Search provider** 选一个你已经开通的服务。
-**Provider configuration** 里填该服务要求的 API Key 或环境变量。
- 保存后,用一个「需要实时信息」的问题测一下(比如「今天某地天气」)。
- 搜索失败的话,先检查搜索服务的 Key,再看 Logs 里的 Fusion 工具报错。
### 接自定义 MCP 工具
**Add custom MCP**,按工具类型选 transport
- **stdio**:本地命令行工具。填 Command、Arguments、Working directory、Environment variables。
- **streamable-http / sse**:远程 MCP 服务。填 URL 和 Headers。
- **Discover tools**:读出这个 MCP server 暴露了哪些工具。
- **Request timeout / Startup timeout**:工具慢或启动慢就适当调大。
> **建议:** 只把稳定、响应快的 MCP 工具接进 Fusion。高风险的工具先在独立 Profile 里验证再放开。
## 进阶二:把 Agent 消息转发到 IM(Bot
进入 **Bots** 页面,配置好后再到 **Profiles** 里绑定。
Bot 能把 Agent 的消息转发到 IM,还能在你空闲一段时间后,把任务接力到手机上继续看。适合长时间运行的任务、远程查看进度、或者人工接管。
### 配置步骤
1. 打开 **Bots**,点 **Add Bot**
2. 选平台。支持:Weixin iLink、WeCom、Slack、Discord、Telegram、LINE、Feishu、DingTalk。
3. 选认证方式,不同平台要填的字段不一样(Bot Token、OAuth、App Secret、QR Login 之类)。
4. 填好平台要求的 ID、Token、Secret、Signing Secret 或 Robot Code。
5. 保存这个 Bot。
6. 打开 **Profiles**,编辑你想接 Bot 的那个 Agent Profile。
7. 打开 **Bot** 开关,选刚才创建的 Bot。
8. 想把 Agent 的输出也转过去,就开 **Forward agent messages**
9. 想要手机接力,就开 **Handoff**,设好 **Idle seconds**,再选扫描到的 Wi-Fi 或蓝牙目标。
> **注意:** Bot 目前只转发「从 CCR 打开的 App」里产生的 Agent 消息,CLI 里跑的消息不会被转发。Handoff 目标扫描需要在 Electron 桌面 App 里使用。
### 各平台详细教程
每个平台的完整步骤(平台后台怎么建应用、字段对照、排查 FAQ)都有单独一篇:
- [Slack](/bots/slack)
- [Discord](/bots/discord)
- [Telegram](/bots/telegram)
- [LINE](/bots/line)
- [微信](/bots/weixin-ilink)
- [企业微信](/bots/wecom)
- [飞书](/bots/feishu)
- [钉钉](/bots/dingtalk)
## 遇到问题时,照着这个查
CCR 给了你三个排查入口:**Logs**(看请求历史)、**Observability**(看 Agent 汇总)、**Networking**(看临时网络抓包)。
### 快速对照表
| 你遇到的现象 | 先查这些 |
| --- | --- |
| Agent 没走 CCR | Server 在不在跑、Profile 有没有 Apply、Agent 是不是从 CCR 打开的、Scope 对不对 |
| 请求命中了错误的模型 | Routing 的 Default、规则顺序、匹配条件、fallback,再看 Logs 里的 resolved model |
| Provider 鉴权失败(401/403 | API Key、Credential、Base URL、协议、额外的 Header |
| 报 model not found404 | Provider 的模型列表对不对、Routing 选的那个模型存不存在 |
| Fusion 没调用工具 | Fusion 工具选对没、Vision model 支不支持、搜索服务 Key 对不对、MCP 的 Discover tools 和 timeout |
| 请求超时 | 上游服务本身慢不慢、Fusion 工具慢不慢、timeout 设小了没 |
| 成本突然变高 | 按模型筛一下,看 token 组成和请求体大小 |
| 某条 Key 一直失败 | 按 Credential 筛,确认是它的问题后,必要时停用这条 Key |
| Bot 收不到消息 | Profile 里 Bot 开关开了没、是不是从 CCR 打开的 App、Forward agent messages 开了没、平台 Token 还有效吗 |
| Overview 没有数据 | Request logs 和 Agent analysis 开了没、之后有没有真的产生新请求 |
### 关于 Network capture
如果你需要看最原始的网络交换(请求/响应的 summary、header、query、body、raw),就去 **Server → Capture network** 打开,再到 **Networking** 看。可以暂停、恢复、刷新、清空。
> **提醒:** Network capture 记录的信息很完整,也因此更敏感。**只在排查时打开,查完就关掉**,别长期开着。
## 几个值得养成的好习惯
- **敏感信息要当心**API Key、Bot Token、Secret、Usage endpoint 都属于敏感信息,只在可信环境里配置,别发到来路不明的服务。
- **改全局路由前先试**:要动全局 Routing 或 Default 时,先用一个单独的 Profile 验证,确认没问题再放开,避免影响正在用的 Agent。
- **定期看 Logs**:偶尔翻一下错误、token、成本和延迟,能在问题变大之前发现它。
- **重要 Provider 多备 Key**:给重要的 Provider 配多条 Credential,并设好优先级、权重和限制,单 Key 挂了不至于全线中断。
- **导入 deeplink 前核对**:通过 `ccr://provider?...` 导入 Provider 配置前,先看一眼来源、Base URL、协议和模型对不对,再点确认。
---
走到这里,你已经掌握了 CCR 的完整使用闭环:**接入 Provider → 配置路由 → 接上 Agent → 打开观察 → 按需扩展**。剩下的就是用起来,遇到具体问题再回到对应章节查。祝用得顺手。
+191
View File
@@ -0,0 +1,191 @@
export type Locale = "zh" | "en";
const languageOptions = [
{ locale: "zh", label: "中文", href: "/" },
{ locale: "en", label: "English", href: "/en/" },
] as const;
export const docsContent = {
zh: {
htmlLang: "zh-CN",
pageTitle: "文档",
languageLabel: "中文",
languageOptions,
navItems: ["文档", "指南", "配置", "排查"],
sidebarGroups: [
{
label: "开始",
icon: "rocket",
items: ["工作流概览", "安装与本地地址", "五分钟接入"],
active: "工作流概览",
},
{
label: "模型接入",
icon: "book",
items: ["Provider 接入", "Routing 路由", "Fusion 组合模型"],
},
{
label: "Agent 接入",
icon: "wand",
items: ["Agent Profile 接入", "Bot 与 IM 接力 Agent"],
},
{
label: "Bot 平台配置",
icon: "wand",
items: ["Slack", "Discord", "Telegram", "LINE", "微信", "企业微信", "飞书", "钉钉"],
},
{
label: "观测排查",
icon: "pen",
items: ["Overview 自定义组件", "日志、分析与问题排查", "维护与安全建议"],
},
],
expandableSidebarItems: [
"Provider 接入",
"Agent Profile 接入",
"Overview 自定义组件",
"日志、分析与问题排查",
],
sidebarChildren: {
"Provider 接入": ["Provider 字段", "API Key 与用量", "连通性检查"],
"Agent Profile 接入": ["Claude Code", "Codex", "ZCode"],
"Overview 自定义组件": ["组件类型", "自定义布局", "分析口径"],
"日志、分析与问题排查": ["Request logs", "Agent analysis", "Network capture"],
},
sidebarLinks: {
: "#工作流概览",
: "#安装与本地地址",
: "#五分钟接入",
"Provider 字段": "#provider-字段",
"API Key 与用量": "#api-key-与用量",
: "#连通性检查",
"Routing 路由": "#routing-路由",
"Fusion 组合模型": "#fusion-组合模型",
"Claude Code": "#claude-code",
Codex: "#codex",
ZCode: "#zcode",
"Bot 与 IM 接力 Agent": "#bot-与-im-接力-agent",
: "#组件类型",
: "#自定义布局",
: "#分析口径",
"Request logs": "#request-logs",
"Agent analysis": "#agent-analysis",
"Network capture": "#network-capture",
: "#维护与安全建议",
Slack: "/bots/slack",
Discord: "/bots/discord",
Telegram: "/bots/telegram",
LINE: "/bots/line",
: "/bots/weixin-ilink",
: "/bots/wecom",
: "/bots/feishu",
: "/bots/dingtalk",
},
tocTitle: "本页内容",
ui: {
searchLabel: "搜索文档",
searchPlaceholder: "搜索...",
copyPage: "复制页面",
copied: "已复制",
copyFailed: "复制失败",
downloadLabel: "下载",
githubLabel: "GitHub 仓库",
themeLabel: "主题",
starsFallback: "Stars",
copyCode: "复制代码",
copiedCode: "代码已复制",
copyCodeFailed: "代码复制失败",
},
},
en: {
htmlLang: "en",
pageTitle: "Documentation",
languageLabel: "English",
languageOptions,
navItems: ["Documentation", "Guides", "Configuration", "Troubleshooting"],
sidebarGroups: [
{
label: "Start",
icon: "rocket",
items: ["Workflow Overview", "Installation And Local Endpoints", "Five Minute Setup"],
active: "Workflow Overview",
},
{
label: "Model Setup",
icon: "book",
items: ["Connect Providers", "Configure Routing", "Compose Models With Fusion"],
},
{
label: "Agent Setup",
icon: "wand",
items: ["Connect Agent Profiles", "Relay Agents In IM With Bots"],
},
{
label: "Bot Platforms",
icon: "wand",
items: ["Slack", "Discord", "Telegram", "LINE", "Weixin", "WeCom", "Feishu", "DingTalk"],
},
{
label: "Observe",
icon: "pen",
items: ["Customize Overview Widgets", "Logs Analysis And Troubleshooting", "Maintenance And Security"],
},
],
expandableSidebarItems: [
"Connect Providers",
"Connect Agent Profiles",
"Customize Overview Widgets",
"Logs Analysis And Troubleshooting",
],
sidebarChildren: {
"Connect Providers": ["Provider Fields", "API Keys And Usage", "Connectivity Checks"],
"Connect Agent Profiles": ["Claude Code", "Codex", "ZCode"],
"Customize Overview Widgets": ["Widget Types", "Custom Layout", "Analysis Scope"],
"Logs Analysis And Troubleshooting": ["Request logs", "Agent analysis", "Network capture"],
},
sidebarLinks: {
"Workflow Overview": "#workflow-overview",
"Installation And Local Endpoints": "#installation-and-local-endpoints",
"Five Minute Setup": "#five-minute-setup",
"Provider Fields": "#provider-fields",
"API Keys And Usage": "#api-keys-and-usage",
"Connectivity Checks": "#connectivity-checks",
"Configure Routing": "#configure-routing",
"Compose Models With Fusion": "#compose-models-with-fusion",
"Claude Code": "#claude-code",
Codex: "#codex",
ZCode: "#zcode",
"Relay Agents In IM With Bots": "#relay-agents-in-im-with-bots",
"Widget Types": "#widget-types",
"Custom Layout": "#custom-layout",
"Analysis Scope": "#analysis-scope",
"Request logs": "#request-logs",
"Agent analysis": "#agent-analysis",
"Network capture": "#network-capture",
"Maintenance And Security": "#maintenance-and-security",
Slack: "/en/bots/slack",
Discord: "/en/bots/discord",
Telegram: "/en/bots/telegram",
LINE: "/en/bots/line",
Weixin: "/en/bots/weixin-ilink",
WeCom: "/en/bots/wecom",
Feishu: "/en/bots/feishu",
DingTalk: "/en/bots/dingtalk",
},
tocTitle: "On this page",
ui: {
searchLabel: "Search docs",
searchPlaceholder: "Search...",
copyPage: "Copy page",
copied: "Copied",
copyFailed: "Copy failed",
downloadLabel: "Download",
githubLabel: "GitHub repository",
themeLabel: "Theme",
starsFallback: "Stars",
copyCode: "Copy code",
copiedCode: "Copied code",
copyCodeFailed: "Copy failed",
},
},
} as const;
+321
View File
@@ -0,0 +1,321 @@
---
import "../styles/global.css";
const {
title = "Documentation",
htmlLang = "zh-CN",
locale = "zh",
languageLabel = "中文",
languageOptions = [
{ locale: "zh", label: "中文", href: "/" },
{ locale: "en", label: "English", href: "/en/" },
],
navItems = ["Documentation", "Guides", "API reference", "Changelog"],
sidebarGroups = [],
expandableSidebarItems = [],
sidebarChildren = {},
sidebarLinks = {},
tocTitle = "On this page",
tocItems = [],
ui = {
searchLabel: "Search docs",
searchPlaceholder: "Search...",
downloadLabel: "Download",
githubLabel: "GitHub repository",
themeLabel: "Theme",
starsFallback: "Stars",
},
} = Astro.props;
const homeHref = locale === "en" ? "/en/" : "/";
---
<!doctype html>
<html lang={htmlLang}>
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta
name="description"
content="Claude Code Router documentation site built with Astro."
/>
<link rel="icon" href="/favicon.svg" type="image/svg+xml" />
{
languageOptions.map((option) => (
<link
rel="alternate"
hreflang={option.locale === "zh" ? "zh-CN" : "en"}
href={option.href}
/>
))
}
<title>{title} | Claude Code Router Docs</title>
</head>
<body>
<div class="shell">
<header class="topbar">
<a class="brand" href={homeHref} aria-label="Claude Code Router Docs">
<img src="/logo.png" alt="" class="brand-logo" />
<span>CCR docs</span>
</a>
<details class="language-switcher">
<summary>
<span>{languageLabel}</span>
<svg viewBox="0 0 16 16" aria-hidden="true">
<path d="M4 6l4 4 4-4" />
</svg>
</summary>
<div class="language-menu">
{
languageOptions.map((option) => (
<a
class:list={{ active: option.locale === locale }}
href={option.href}
hreflang={option.locale === "zh" ? "zh-CN" : "en"}
>
{option.label}
</a>
))
}
</div>
</details>
<nav class="main-nav" aria-label="Primary navigation">
{
navItems.map((item, index) => (
<a class:list={["nav-link", { active: index === 0 }]} href="#">
{item}
</a>
))
}
</nav>
<div class="top-actions">
<label class="search" aria-label={ui.searchLabel}>
<svg viewBox="0 0 20 20" aria-hidden="true">
<path d="M14.5 14.5L18 18" />
<circle cx="8.5" cy="8.5" r="5.5" />
</svg>
<input type="search" placeholder={ui.searchPlaceholder} />
<kbd>⌘K</kbd>
</label>
<a
class="soft-button icon-link"
href="https://github.com/musistudio/claude-code-router/releases"
aria-label={ui.downloadLabel}
>
<svg viewBox="0 0 20 20" aria-hidden="true">
<path d="M10 3v9" />
<path d="M6 8l4 4 4-4" />
<path d="M4 16h12" />
</svg>
</a>
<a
class="soft-button github-button hide-small"
href="https://github.com/musistudio/claude-code-router"
aria-label={ui.githubLabel}
>
<svg viewBox="0 0 20 20" aria-hidden="true">
<path d="M8.5 16.5c-4 .9-4-2-5.5-2.5" />
<path d="M13.5 18v-3.5c0-1 .1-1.4-.5-2 2.8-.3 5.5-1.4 5.5-6A4.6 4.6 0 0 0 17.2 3c.1-.4.6-1.7-.2-3 0 0-1.1-.3-3.5 1.3a12.1 12.1 0 0 0-6.4 0C4.7-.3 3.6 0 3.6 0c-.8 1.3-.3 2.6-.2 3A4.6 4.6 0 0 0 2 6.5c0 4.6 2.7 5.7 5.5 6-.6.5-.9 1.2-.9 2.4V18" />
</svg>
<strong id="github-stars" aria-label="GitHub stars">Stars</strong>
</a>
<button class="icon-button" type="button" aria-label={ui.themeLabel}>
<svg viewBox="0 0 20 20" aria-hidden="true">
<circle cx="10" cy="10" r="3" />
<path d="M10 1v3M10 16v3M1 10h3M16 10h3M3.6 3.6l2.1 2.1M14.3 14.3l2.1 2.1M16.4 3.6l-2.1 2.1M5.7 14.3l-2.1 2.1" />
</svg>
</button>
</div>
</header>
<div class="content-grid">
<aside class="sidebar" aria-label="Documentation sidebar">
<div class="sidebar-inner">
{
sidebarGroups.map((group) => (
<section class="sidebar-group">
<h2>
<span class="group-icon" data-icon={group.icon}></span>
{group.label}
</h2>
<ul>
{group.items.map((item) => {
const childItems = sidebarChildren[item] ?? [];
const isExpandable = expandableSidebarItems.includes(item) && childItems.length > 0;
return (
<li>
{isExpandable ? (
<details class="sidebar-details">
<summary class="sidebar-link" aria-expanded="false">
<span>{item}</span>
<svg viewBox="0 0 16 16" aria-hidden="true">
<path d="M6 4l4 4-4 4" />
</svg>
</summary>
<ul class="sidebar-children">
{childItems.map((child) => (
<li>
<a class="sidebar-child-link" href={sidebarLinks[child] ?? "#"}>
{child}
</a>
</li>
))}
</ul>
</details>
) : (
<a
class:list={["sidebar-link", { active: group.active === item }]}
href={sidebarLinks[item] ?? "#"}
>
<span>{item}</span>
</a>
)}
</li>
);
})}
</ul>
</section>
))
}
</div>
</aside>
<main class="doc-main">
<slot />
</main>
<aside class="toc" aria-label={tocTitle}>
<div class="toc-card">
<h2>
<svg viewBox="0 0 20 20" aria-hidden="true">
<path d="M5 6h10M5 10h7M5 14h4" />
</svg>
{tocTitle}
</h2>
<nav>
{
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>
</aside>
</div>
</div>
<script define:vars={{ starsFallback: ui.starsFallback }}>
const starBadge = document.querySelector("#github-stars");
const formatStars = (count) => {
if (!Number.isFinite(count)) return starsFallback;
if (count >= 1000) {
const rounded = Math.round(count / 100) / 10;
return `${rounded.toFixed(rounded % 1 === 0 ? 0 : 1)}k`;
}
return count.toLocaleString();
};
fetch("https://api.github.com/repos/musistudio/claude-code-router", {
headers: { Accept: "application/vnd.github+json" },
})
.then((response) => {
if (!response.ok) throw new Error("GitHub request failed");
return response.json();
})
.then((repo) => {
if (starBadge) {
starBadge.textContent = formatStars(repo.stargazers_count);
}
})
.catch(() => {
if (starBadge) {
starBadge.textContent = starsFallback;
}
});
</script>
<script>
const sidebarDetails = document.querySelectorAll(".sidebar-details");
const reduceMotion = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
sidebarDetails.forEach((details) => {
const summary = details.querySelector("summary");
const children = details.querySelector(".sidebar-children");
if (!(summary instanceof HTMLElement) || !(children instanceof HTMLElement)) {
return;
}
summary.setAttribute("aria-expanded", details.open ? "true" : "false");
summary.addEventListener("click", (event) => {
event.preventDefault();
if (details.dataset.animating === "true") return;
const isOpen = details.open;
details.dataset.animating = "true";
if (reduceMotion) {
details.open = !isOpen;
summary.setAttribute("aria-expanded", details.open ? "true" : "false");
delete details.dataset.animating;
return;
}
if (isOpen) {
children.style.height = `${children.scrollHeight}px`;
children.getBoundingClientRect();
details.classList.add("is-collapsing");
summary.setAttribute("aria-expanded", "false");
window.requestAnimationFrame(() => {
children.style.height = "0px";
});
const finishClose = (transitionEvent) => {
if (transitionEvent.propertyName !== "height") return;
children.removeEventListener("transitionend", finishClose);
details.open = false;
details.classList.remove("is-collapsing");
children.style.height = "";
delete details.dataset.animating;
};
children.addEventListener("transitionend", finishClose);
return;
}
details.open = true;
summary.setAttribute("aria-expanded", "true");
children.style.height = "0px";
children.style.opacity = "0";
children.getBoundingClientRect();
window.requestAnimationFrame(() => {
children.style.height = `${children.scrollHeight}px`;
children.style.opacity = "";
});
const finishOpen = (transitionEvent) => {
if (transitionEvent.propertyName !== "height") return;
children.removeEventListener("transitionend", finishOpen);
children.style.height = "";
delete details.dataset.animating;
};
children.addEventListener("transitionend", finishOpen);
});
});
</script>
</body>
</html>
+15
View File
@@ -0,0 +1,15 @@
---
import DocPage from "../../components/DocPage.astro";
import { zhBotDocs, botPlatformFromPath } from "../../bot-platforms";
export function getStaticPaths() {
return Object.entries(zhBotDocs).map(([filePath, mod]) => ({
params: { platform: botPlatformFromPath(filePath) },
props: { mod },
}));
}
const { mod } = Astro.props;
---
<DocPage locale="zh" doc={mod} />
+15
View File
@@ -0,0 +1,15 @@
---
import DocPage from "../../components/DocPage.astro";
import { enBotDocs, botPlatformFromPath } from "../../bot-platforms";
export function getStaticPaths() {
return Object.entries(enBotDocs).map(([filePath, mod]) => ({
params: { platform: botPlatformFromPath(filePath) },
props: { mod },
}));
}
const { mod } = Astro.props;
---
<DocPage locale="en" doc={mod} />
+5
View File
@@ -0,0 +1,5 @@
---
import DocPage from "../../components/DocPage.astro";
---
<DocPage locale="en" />
+5
View File
@@ -0,0 +1,5 @@
---
import DocPage from "../components/DocPage.astro";
---
<DocPage locale="zh" />
File diff suppressed because it is too large Load Diff
+5
View File
@@ -0,0 +1,5 @@
{
"extends": "astro/tsconfigs/strict",
"include": [".astro/types.d.ts", "**/*"],
"exclude": ["dist"]
}
+7 -4
View File
@@ -88,6 +88,7 @@ const REMOVED_LEGACY_ROUTER_RULE_IDS = new Set([
"legacy-web-search",
"legacy-image"
]);
const INTERNAL_GATEWAY_CORE_HOST = "127.0.0.1";
const DEFAULT_CONFIG: AppConfig = {
APIKEY: "",
@@ -141,7 +142,7 @@ const DEFAULT_CONFIG: AppConfig = {
tenantId: "ccr"
},
gateway: {
coreHost: "127.0.0.1",
coreHost: INTERNAL_GATEWAY_CORE_HOST,
corePort: 3457,
enabled: true,
generatedConfigFile: GATEWAY_CONFIG_FILE,
@@ -371,6 +372,7 @@ export async function loadAppConfig(): Promise<AppConfig> {
gateway: {
...DEFAULT_CONFIG.gateway,
...gatewayConfig,
coreHost: INTERNAL_GATEWAY_CORE_HOST,
corePort,
generatedConfigFile: GATEWAY_CONFIG_FILE,
host: gatewayConfig.host ?? host,
@@ -586,6 +588,10 @@ function sanitizeConfigForDisk(config: AppConfig): AppConfig {
...config,
APIKEY: "",
APIKEYS: [],
gateway: {
...config.gateway,
coreHost: INTERNAL_GATEWAY_CORE_HOST
},
profile: sanitizeProfileConfigForDisk(config.profile)
};
}
@@ -685,9 +691,6 @@ function pickConfig(value: Partial<AppConfig>): LoadedAppConfig {
if (gatewayPort) {
gatewayConfig.port = gatewayPort;
}
if (typeof gateway.coreHost === "string" && gateway.coreHost.trim()) {
gatewayConfig.coreHost = gateway.coreHost.trim();
}
const gatewayCorePort = readPort(gateway.corePort);
if (gatewayCorePort) {
gatewayConfig.corePort = gatewayCorePort;
+3 -2
View File
@@ -23,12 +23,12 @@ import { getProfileOpenCommand, getProfileRuntimeStatus, openProfileFromCcr, sto
import { ensureProxyCertificateAuthority } from "../server/proxy/certificates";
import { proxyService } from "../server/proxy/service";
import { listMcpServerTools } from "../server/mcp/tool-discovery";
import { getAgentAnalysis, getRequestLogs } from "./request-log-store";
import { getAgentAnalysis, getAgentTracePayload, getRequestLogs } from "./request-log-store";
import trayController from "./tray-controller";
import { appUpdateService } from "./update-service";
import { getUsageStats } from "./usage-store";
import windowsManager from "./windows";
import type { AgentAnalysisFilter, ApiKeyConfig, AppConfig, AppInfo, BotGatewayQrLoginCancelRequest, BotGatewayQrLoginStartRequest, BotGatewayQrLoginWaitRequest, BotGatewayQrWindowCloseRequest, BotGatewayQrWindowOpenRequest, GatewayMcpServerConfig, GatewayPluginAppConfig, GatewayProviderConnectivityCheckRequest, GatewayProviderProbeCandidatesRequest, GatewayProviderProbeRequest, GatewayStatus, LocalAgentProviderImportRequest, PluginDependency, PluginDirectorySelection, PluginMarketplaceEntry, ProfileApplyResult, ProfileOpenRequest, ProviderAccountSnapshotRequestOptions, ProviderAccountTestRequest, ProviderCatalogModelsRequest, ProviderIconDetectionRequest, ProviderManifestFetchRequest, RequestLogListFilter, UsageStatsFilter, UsageStatsRange } from "../shared/app";
import type { AgentAnalysisFilter, AgentAnalysisTracePayloadRequest, ApiKeyConfig, AppConfig, AppInfo, BotGatewayQrLoginCancelRequest, BotGatewayQrLoginStartRequest, BotGatewayQrLoginWaitRequest, BotGatewayQrWindowCloseRequest, BotGatewayQrWindowOpenRequest, GatewayMcpServerConfig, GatewayPluginAppConfig, GatewayProviderConnectivityCheckRequest, GatewayProviderProbeCandidatesRequest, GatewayProviderProbeRequest, GatewayStatus, LocalAgentProviderImportRequest, PluginDependency, PluginDirectorySelection, PluginMarketplaceEntry, ProfileApplyResult, ProfileOpenRequest, ProviderAccountSnapshotRequestOptions, ProviderAccountTestRequest, ProviderCatalogModelsRequest, ProviderIconDetectionRequest, ProviderManifestFetchRequest, RequestLogListFilter, UsageStatsFilter, UsageStatsRange } from "../shared/app";
const pluginMarketplace: PluginMarketplaceEntry[] = [
{
@@ -86,6 +86,7 @@ ipcMain.handle(IPC_CHANNELS.appGetProviderAccountSnapshots, (_event, provider?:
ipcMain.handle(IPC_CHANNELS.appGetProviderCatalogModels, (_event, request: ProviderCatalogModelsRequest) => getProviderCatalogModels(request));
ipcMain.handle(IPC_CHANNELS.appGetProviderPresets, () => getProviderPresets());
ipcMain.handle(IPC_CHANNELS.appGetAgentAnalysis, (_event, filter?: AgentAnalysisFilter) => getAgentAnalysis(filter));
ipcMain.handle(IPC_CHANNELS.appGetAgentTracePayload, (_event, request: AgentAnalysisTracePayloadRequest) => getAgentTracePayload(request));
ipcMain.handle(IPC_CHANNELS.appGetGatewayStatus, () => gatewayService.getStatus());
ipcMain.handle(IPC_CHANNELS.appGetProxyCertificateStatus, () => proxyService.getCertificateStatus());
ipcMain.handle(IPC_CHANNELS.appGetProxyNetworkCaptures, () => proxyService.getNetworkCaptures());
+3
View File
@@ -3,6 +3,8 @@ import { IPC_CHANNELS } from "../shared/ipc-channels";
import type {
AgentAnalysisFilter,
AgentAnalysisSnapshot,
AgentAnalysisTracePayloadFullResult,
AgentAnalysisTracePayloadRequest,
AppConfig,
AppInfo,
AppUpdateStatus,
@@ -73,6 +75,7 @@ contextBridge.exposeInMainWorld("ccr", {
detectProviderIcon: (request: ProviderIconDetectionRequest) => ipcRenderer.invoke(IPC_CHANNELS.appDetectProviderIcon, request) as Promise<ProviderIconDetectionResult>,
fetchProviderManifest: (request: ProviderManifestFetchRequest) => ipcRenderer.invoke(IPC_CHANNELS.appFetchProviderManifest, request) as Promise<ProviderManifestFetchResult>,
getAgentAnalysis: (filter?: AgentAnalysisFilter) => ipcRenderer.invoke(IPC_CHANNELS.appGetAgentAnalysis, filter) as Promise<AgentAnalysisSnapshot>,
getAgentTracePayload: (request: AgentAnalysisTracePayloadRequest) => ipcRenderer.invoke(IPC_CHANNELS.appGetAgentTracePayload, request) as Promise<AgentAnalysisTracePayloadFullResult>,
getAppInfo: () => ipcRenderer.invoke(IPC_CHANNELS.appGetInfo) as Promise<AppInfo>,
getConfig: () => ipcRenderer.invoke(IPC_CHANNELS.appGetConfig) as Promise<AppConfig>,
getGatewayStatus: () => ipcRenderer.invoke(IPC_CHANNELS.appGetGatewayStatus) as Promise<GatewayStatus>,
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+2 -1
View File
@@ -404,7 +404,8 @@ export function normalizeConfig(config: AppConfig): AppConfig {
botGateway: normalizeBotGatewayRuntimeConfig(config.botGateway) ?? fallbackConfig.botGateway,
gateway: {
...fallbackConfig.gateway,
...(config.gateway || {})
...(config.gateway || {}),
coreHost: fallbackConfig.gateway.coreHost
},
observability: normalizeObservabilityConfig(config.observability),
proxy: {
+6 -1
View File
@@ -140,6 +140,11 @@ import type {
AgentAnalysisFilter,
AgentAnalysisSessionSelection,
AgentAnalysisSnapshot,
AgentAnalysisTrace,
AgentAnalysisTracePayloadFullResult,
AgentAnalysisTracePayloadRequest,
AgentAnalysisTraceRun,
AgentAnalysisTraceRunKind,
AgentKind,
AppConfig,
AppInfo,
@@ -560,7 +565,7 @@ export {
};
export type {
HTMLAttributes, ReactPointerEvent, ReactNode, CollisionDetection, DragEndEvent, DragOverEvent, DragStartEvent,
LucideIcon, AgentAnalysisFilter, AgentAnalysisSessionSelection, AgentAnalysisSnapshot, AgentKind, AppConfig, AppInfo, AppUpdateStatus, ApiKeyConfig,
LucideIcon, AgentAnalysisFilter, AgentAnalysisSessionSelection, AgentAnalysisSnapshot, AgentAnalysisTrace, AgentAnalysisTracePayloadFullResult, AgentAnalysisTracePayloadRequest, AgentAnalysisTraceRun, AgentAnalysisTraceRunKind, AgentKind, AppConfig, AppInfo, AppUpdateStatus, ApiKeyConfig,
ApiKeyLimitConfig, BotGatewayQrLoginCancelRequest, BotGatewayQrLoginCancelResult, BotGatewayQrLoginStartRequest, BotGatewayQrLoginStartResult, BotGatewayQrLoginWaitRequest, BotGatewayQrLoginWaitResult, BotGatewayQrWindowOpenResult, BotGatewayRuntimeConfig, BotGatewaySavedConfig, BotHandoffScanTarget, GatewayProviderConfig, GatewayProviderCapability, GatewayPluginAppConfig, GatewayProviderProbeResult, GatewayProviderProtocol, GatewayMcpServerConfig,
GatewayMcpServerTransport, GatewayMcpStdioMessageMode, GatewayMcpToolInfo, GatewayStatus, OverviewMetricKind, OverviewWidgetConfig, OverviewWidgetSize, OverviewWidgetType,
OverviewWidgetVariant, PluginDependency, PluginDirectorySelection, PluginMarketplaceEntry, ProviderAccountConfig, ProviderAccountConnectorConfig, ProviderAccountHttpJsonConnectorConfig,
+21
View File
@@ -599,6 +599,7 @@ export const appCopy: Record<ResolvedLanguage, AppCopy> = {
"Checking for updates": "正在检查更新",
"Capture network": "捕获网络",
"Connection verified": "连通性已验证",
"Action": "操作",
"Check trust": "检查信任",
"Choose where each agent uses CCR.": "选择每个 Agent 在哪里使用 CCR。",
"Click Check Connection to verify connectivity with a real model request.": "点击检测连通性,用一次真实模型请求验证是否可用。",
@@ -697,6 +698,7 @@ export const appCopy: Record<ResolvedLanguage, AppCopy> = {
"Endpoint Health": "端点健康",
"Endpoint information": "端点信息",
"Let's start": "开始吧",
"Error": "错误",
"Errors": "错误数",
"Failed requests": "失败请求",
"Expiration": "过期时间",
@@ -710,6 +712,8 @@ export const appCopy: Record<ResolvedLanguage, AppCopy> = {
"Fallback targets": "失败降级目标",
"Failure handling": "故障处理",
"Forward agent messages": "转发 Agent 消息",
"Full content": "完整内容",
"Full content unavailable": "无法读取完整内容",
"Bot only forwards messages when opening the APP from CCR. CLI does not forward messages yet.": "Bot仅在使用CCR打开APP的情况下才会转发消息,cli目前不会转发消息",
"Messages are forwarded only when using the corresponding app.": "仅在使用对应 App 时才会转发消息。",
"First enabled": "首个启用规则",
@@ -789,6 +793,7 @@ export const appCopy: Record<ResolvedLanguage, AppCopy> = {
"No provider yet": "还没有供应商",
"No requests captured yet": "暂无请求记录",
"No bots configured": "尚未配置 Bot",
"No data": "无数据",
"No route activity": "暂无路由活动",
"No targets found": "未发现目标",
"No": "否",
@@ -811,6 +816,7 @@ export const appCopy: Record<ResolvedLanguage, AppCopy> = {
"P50": "P50",
"P95": "P95",
"P99": "P99",
"Parameters": "参数",
"Path": "路径",
"Platform": "平台",
"Platform conversation ID": "平台会话 ID",
@@ -957,6 +963,7 @@ export const appCopy: Record<ResolvedLanguage, AppCopy> = {
"Remove widget": "移除组件",
"Preview": "预览",
"Requests, tokens, cost": "请求、Token、成本",
"Result": "结果",
"Reset layout": "重置布局",
"Resize widget height": "调整组件高度",
"Resize widget size": "调整组件大小",
@@ -983,12 +990,22 @@ export const appCopy: Record<ResolvedLanguage, AppCopy> = {
"Yes": "是",
"Set as default provider": "设为默认供应商",
"Show credential settings": "显示凭据配置",
"Show full content": "查看完整内容",
"Session": "会话",
"Session Detail": "会话详情",
"Session Requests": "会话请求",
"Sessions": "会话",
"Clear session": "清除会话",
"Loading session metrics": "正在加载会话指标",
"Trace Detail": "链路详情",
"Trace duration": "链路耗时",
"Trace runs": "链路节点",
"Call chain": "调用链路",
"Runs": "节点",
"Run": "节点",
"LLM": "LLM",
"LLM calls": "LLM 调用",
"No trace runs": "暂无链路节点",
"No model activity": "暂无模型活动",
"No session requests": "暂无会话请求",
"Show all sessions": "显示所有会话",
@@ -997,6 +1014,9 @@ export const appCopy: Record<ResolvedLanguage, AppCopy> = {
"Stream": "流式",
"Streaming": "流式",
"Non-streaming": "非流式",
"Started": "开始时间",
"Source truncated": "源数据已截断",
"Details": "详情",
"Subagent": "子代理",
"Subagent Routing": "Subagent 路由",
"Subagent calls": "Subagent 调用",
@@ -1016,6 +1036,7 @@ export const appCopy: Record<ResolvedLanguage, AppCopy> = {
"Total tokens": "总令牌",
"Today": "今天",
"Token threshold": "令牌阈值",
"Truncated": "已截断",
"Tokens": "令牌",
"tokens": "令牌",
"Tool": "工具",
@@ -7,13 +7,19 @@ export function TrayStatusStrip({ totalTokens }: { totalTokens: number }) {
return (
<div className="mb-3 flex min-w-0 items-center justify-between gap-3 border-b border-white/10 pb-2">
<div className="flex min-w-0 items-center gap-2">
<button
aria-label={t("Open CCR")}
className="-ml-1 flex min-w-0 items-center gap-2 rounded-md px-1 py-0.5 text-left transition hover:bg-white/[.06] focus:outline-none focus:ring-2 focus:ring-cyan-300/35"
title={t("Open CCR")}
type="button"
onClick={() => void window.ccr?.showMainWindow()}
>
<TrayWindowHeaderIcon />
<div className="min-w-0">
<div className="truncate text-[12px] font-semibold text-slate-50">{formatCompactNumber(totalTokens)} {t("tokens")}</div>
<div className="truncate text-[10px] font-medium text-slate-400">CCR</div>
</div>
</div>
</button>
<button
aria-label={t("Quit")}
className="flex h-7 w-7 shrink-0 items-center justify-center rounded-md border border-white/10 bg-white/[.04] text-slate-300 hover:border-white/16 hover:bg-white/[.08] hover:text-slate-50"
+1
View File
@@ -74,6 +74,7 @@ export const trayText: Record<ResolvedLanguage, Record<string, string>> = {
"No usage captured yet": "暂无用量记录",
"Output": "输出",
"Overview": "概览",
"Open CCR": "打开 CCR",
"Quit": "退出",
"Subscription": "订阅",
"Success": "成功",
+3
View File
@@ -3,6 +3,8 @@ export {};
import type {
AgentAnalysisFilter,
AgentAnalysisSnapshot,
AgentAnalysisTracePayloadFullResult,
AgentAnalysisTracePayloadRequest,
AppConfig,
AppInfo,
AppUpdateStatus,
@@ -75,6 +77,7 @@ declare global {
detectProviderIcon: (request: ProviderIconDetectionRequest) => Promise<ProviderIconDetectionResult>;
fetchProviderManifest: (request: ProviderManifestFetchRequest) => Promise<ProviderManifestFetchResult>;
getAgentAnalysis: (filter?: AgentAnalysisFilter) => Promise<AgentAnalysisSnapshot>;
getAgentTracePayload: (request: AgentAnalysisTracePayloadRequest) => Promise<AgentAnalysisTracePayloadFullResult>;
getAppInfo: () => Promise<AppInfo>;
getConfig: () => Promise<AppConfig>;
getGatewayStatus: () => Promise<GatewayStatus>;
+101 -40
View File
@@ -1,5 +1,5 @@
import { spawn, type ChildProcess } from "node:child_process";
import { randomUUID } from "node:crypto";
import { randomBytes, randomUUID } from "node:crypto";
import { createServer, type IncomingHttpHeaders, type IncomingMessage, type Server, type ServerResponse } from "node:http";
import { createRequire } from "node:module";
import { networkInterfaces } from "node:os";
@@ -42,7 +42,7 @@ import { fetchWithSystemProxy, getSystemProxyUrlForProtocol } from "../../main/s
import { handleNetworkCaptureMcpRequest, isNetworkCaptureMcpPath } from "../mcp/network-capture-mcp";
import { pluginService } from "../../main/plugins/service";
import { proxyService } from "../proxy/service";
import { recordGatewayRequestLog, updateGatewayRequestLogFromRawTrace, type RequestLogRawTraceUpdateInput } from "../../main/request-log-store";
import { createSseErrorDetector, recordGatewayRequestLog, updateGatewayRequestLogFromRawTrace, type RequestLogRawTraceUpdateInput } from "../../main/request-log-store";
import { recordGatewayUsageCapture } from "../../main/usage-store";
import { ClaudeCodeRouterPlugin, normalizeRouteSelector } from "./claude-code-router-plugin";
import {
@@ -173,6 +173,8 @@ class UpstreamRequestError extends Error {
}
const requireFromHere = createRequire(__filename);
const coreGatewayAuthHeader = "x-ccr-core-auth";
const coreGatewayAuthTokenEnv = "CCR_CORE_GATEWAY_AUTH_TOKEN";
const localObservabilityHeaderNames = new Set([
"x-ccr-claude-app-model-rewrite",
"x-ccr-claude-model-discovery",
@@ -181,7 +183,7 @@ const localObservabilityHeaderNames = new Set([
"x-ccr-provider-credential-chain",
"x-ccr-provider-credential-saturated"
]);
const proxyHeaderDenyList = new Set(["connection", "host", "upgrade"]);
const proxyHeaderDenyList = new Set(["connection", coreGatewayAuthHeader, "host", "upgrade"]);
const responseHeaderDenyList = new Set(["connection", "content-encoding", "transfer-encoding"]);
const maxUsageCaptureBytes = 8 * 1024 * 1024;
const maxPendingRawTraceUpdates = 200;
@@ -228,6 +230,7 @@ const gatewayProviderProtocolFallbackOrder: GatewayProviderProtocol[] = [
class GatewayService {
private child?: ChildProcess;
private config?: AppConfig;
private coreAuthToken = "";
private plugin?: ClaudeCodeRouterPlugin;
private readonly pendingRawTraceUpdates = new Map<string, PendingRawTraceUpdate>();
private readonly rawTraceSyncToken = randomUUID();
@@ -241,8 +244,17 @@ class GatewayService {
};
async start(config: AppConfig): Promise<GatewayStatus> {
const coreHostError = loopbackCoreHostError(config.gateway.coreHost);
if (coreHostError) {
return {
...this.getStatus(),
lastError: coreHostError,
state: "error"
};
}
await this.stop();
this.config = config;
this.coreAuthToken = generateCoreGatewayAuthToken();
this.plugin = new ClaudeCodeRouterPlugin(config);
this.status = {
coreEndpoint: endpoint(config.gateway.coreHost, config.gateway.corePort),
@@ -259,6 +271,7 @@ class GatewayService {
if (!shouldRunServer) {
await pluginService.stop();
await backendService.stopAll();
this.coreAuthToken = "";
this.status = {
...this.status,
state: "stopped"
@@ -278,24 +291,18 @@ class GatewayService {
writeCoreGatewayConfig(config, this.rawTraceSyncToken);
await stopPreviousManagedCoreGateway(config, this.status.coreEndpoint);
if (await isCoreGatewayHealthy(this.status.coreEndpoint)) {
this.status = {
...this.status,
coreManagedExternally: true,
lastError: undefined,
pid: undefined
};
} else {
await proxyService.refreshUpstreamProxyFromCurrentSystem();
const runtimeId = randomUUID();
const upstreamProxyUrl = proxyService.getUpstreamProxyUrl("https") ?? await getSystemProxyUrlForProtocol("https");
this.child = spawnGatewayProcess(config, upstreamProxyUrl, runtimeId);
writeManagedCoreGatewayMarker(config, this.child, runtimeId);
this.child.stdout?.on("data", (chunk) => console.info(`[gateway] ${chunk.toString().trimEnd()}`));
this.child.stderr?.on("data", (chunk) => console.warn(`[gateway] ${chunk.toString().trimEnd()}`));
this.child.on("exit", (code, signal) => {
void this.handleCoreGatewayExit(code, signal);
});
throw new Error(`Core gateway endpoint is already in use: ${this.status.coreEndpoint}`);
}
await proxyService.refreshUpstreamProxyFromCurrentSystem();
const runtimeId = randomUUID();
const upstreamProxyUrl = proxyService.getUpstreamProxyUrl("https") ?? await getSystemProxyUrlForProtocol("https");
this.child = spawnGatewayProcess(config, upstreamProxyUrl, runtimeId, this.coreAuthToken);
writeManagedCoreGatewayMarker(config, this.child, runtimeId);
this.child.stdout?.on("data", (chunk) => console.info(`[gateway] ${chunk.toString().trimEnd()}`));
this.child.stderr?.on("data", (chunk) => console.warn(`[gateway] ${chunk.toString().trimEnd()}`));
this.child.on("exit", (code, signal) => {
void this.handleCoreGatewayExit(code, signal);
});
}
this.status = {
@@ -321,6 +328,7 @@ class GatewayService {
const child = this.child;
const config = this.config;
this.child = undefined;
this.coreAuthToken = "";
if (child && !child.killed) {
child.kill();
}
@@ -355,6 +363,7 @@ class GatewayService {
}
updateConfig(config: AppConfig): void {
assertLoopbackCoreHost(config.gateway.coreHost);
this.config = config;
this.plugin = new ClaudeCodeRouterPlugin(config);
proxyService.updateConfig(config);
@@ -397,16 +406,6 @@ class GatewayService {
return;
}
removeManagedCoreGatewayMarker(this.config);
if (await isCoreGatewayHealthy(this.status.coreEndpoint)) {
this.status = {
...this.status,
coreManagedExternally: true,
lastError: undefined,
pid: undefined,
state: "running"
};
return;
}
this.status = {
...this.status,
coreManagedExternally: undefined,
@@ -645,6 +644,7 @@ class GatewayService {
method,
path,
routedModel,
coreAuthToken: this.coreAuthToken,
upstreamUrl
});
} catch (error) {
@@ -703,17 +703,34 @@ class GatewayService {
const upstreamBody = Readable.fromWeb(upstreamResponse.body as unknown as import("node:stream/web").ReadableStream);
const responseBody = upstreamBody;
const sampler = createBodySampler();
const sseErrorDetector = createSseErrorDetector(responseHeaders.get("content-type") ?? undefined);
let streamDetectedError: string | undefined;
let logRecorded = false;
const writeStreamLog = (error?: string) => {
if (logRecorded) {
return;
}
logRecorded = true;
writeRequestLog(upstreamResponse.status, responseHeaders, sampler.read(), sampler.isTruncated(), error);
writeRequestLog(
upstreamResponse.status,
responseHeaders,
sampler.read(),
sampler.isTruncated(),
error ?? streamDetectedError
);
};
responseBody.on("data", (chunk) => sampler.append(chunk));
responseBody.once("end", () => writeStreamLog());
responseBody.once("error", (error) => writeStreamLog(formatError(error)));
responseBody.on("data", (chunk) => {
sampler.append(chunk);
streamDetectedError ??= sseErrorDetector.append(chunk);
});
responseBody.once("end", () => {
streamDetectedError ??= sseErrorDetector.finish();
writeStreamLog();
});
responseBody.once("error", (error) => {
streamDetectedError ??= sseErrorDetector.finish();
writeStreamLog(formatError(error));
});
if (shouldCaptureUsage) {
responseBody.once("end", () => {
void recordGatewayUsageCapture({
@@ -796,6 +813,7 @@ class GatewayService {
export const gatewayService = new GatewayService();
function writeCoreGatewayConfig(config: AppConfig, rawTraceSyncToken: string): void {
assertLoopbackCoreHost(config.gateway.coreHost);
mkdirSync(dirname(config.gateway.generatedConfigFile), { recursive: true });
const pluginCoreGatewayConfig = pluginService.getCoreGatewayConfig();
const providerPlugins = withCodexOauthRuntimeDefaults([
@@ -823,8 +841,16 @@ function writeCoreGatewayConfig(config: AppConfig, rawTraceSyncToken: string): v
...(config.agent?.mcpServers ?? [])
];
const payload = {
...pluginCoreGatewayConfig,
auth: {
enabled: false
enabled: true,
mode: "static_api_key",
required: true,
staticApiKeys: {
keyBearerOnly: false,
keyEnv: coreGatewayAuthTokenEnv,
keyHeader: coreGatewayAuthHeader
}
},
billing: {
enabled: true
@@ -842,7 +868,6 @@ function writeCoreGatewayConfig(config: AppConfig, rawTraceSyncToken: string): v
},
port: config.gateway.corePort,
upstreamTimeoutMs: Number(config.API_TIMEOUT_MS) || 0,
...pluginCoreGatewayConfig,
agent: {
...pluginAgentConfig,
mcpServers
@@ -1754,6 +1779,7 @@ function rewriteCapabilityResponseHeaders(headers: Headers, config: AppConfig):
async function fetchUpstreamWithFallback(input: {
body?: Buffer;
config: AppConfig;
coreAuthToken: string;
fallback: RouterFallbackConfig;
headers: Record<string, string>;
method: string;
@@ -1778,7 +1804,7 @@ async function fetchUpstreamWithFallback(input: {
try {
const response = await fetchWithSystemProxy(input.upstreamUrl, {
body: shouldSendBody(input.method) ? attempt.body?.toString("utf8") : undefined,
headers: omitLocalObservabilityHeaders(attempt.headers ?? input.headers),
headers: withCoreGatewayAuthHeader(omitLocalObservabilityHeaders(attempt.headers ?? input.headers), input.coreAuthToken),
method: input.method
});
@@ -2147,10 +2173,10 @@ function uniqueStrings(values: Array<string | undefined>): string[] {
return result;
}
function spawnGatewayProcess(config: AppConfig, upstreamProxyUrl: string | undefined, runtimeId: string): ChildProcess {
function spawnGatewayProcess(config: AppConfig, upstreamProxyUrl: string | undefined, runtimeId: string, coreAuthToken: string): ChildProcess {
const gatewayEntry = resolveGatewayEntry();
const patchedGatewayEntry = writePatchedGatewayRuntimeEntry(config, gatewayEntry);
const env = createGatewayProcessEnv(config, upstreamProxyUrl, runtimeId);
const env = createGatewayProcessEnv(config, upstreamProxyUrl, runtimeId, coreAuthToken);
return spawn(process.execPath, [patchedGatewayEntry], {
cwd: dirname(config.gateway.generatedConfigFile),
env,
@@ -2210,10 +2236,17 @@ function buildGatewayRuntimePatchEntry(gatewayEntry: string): string {
].join("\n");
}
function createGatewayProcessEnv(config: AppConfig, upstreamProxyUrl: string | undefined, runtimeId: string): NodeJS.ProcessEnv {
function createGatewayProcessEnv(config: AppConfig, upstreamProxyUrl: string | undefined, runtimeId: string, coreAuthToken: string): NodeJS.ProcessEnv {
const env: NodeJS.ProcessEnv = {
...process.env,
AUTH_ENABLED: "true",
AUTH_MODE: "static_api_key",
AUTH_REQUIRED: "true",
AUTH_STATIC_API_KEY_BEARER_ONLY: "false",
AUTH_STATIC_API_KEY_ENV: coreGatewayAuthTokenEnv,
AUTH_STATIC_API_KEY_HEADER: coreGatewayAuthHeader,
CCR_GATEWAY_RUNTIME_ID: runtimeId,
[coreGatewayAuthTokenEnv]: coreAuthToken,
ELECTRON_RUN_AS_NODE: "1",
GATEWAY_CONFIG_PATH: config.gateway.generatedConfigFile,
HOST: config.gateway.coreHost,
@@ -2752,6 +2785,24 @@ function delay(ms: number): Promise<void> {
return new Promise((resolve) => setTimeout(resolve, ms));
}
function assertLoopbackCoreHost(host: string): void {
const error = loopbackCoreHostError(host);
if (error) {
throw new Error(error);
}
}
function loopbackCoreHostError(host: string): string | undefined {
const normalized = host.trim().toLowerCase();
return normalized === "127.0.0.1" || normalized === "::1"
? undefined
: "Core gateway host must be 127.0.0.1 or ::1.";
}
function generateCoreGatewayAuthToken(): string {
return randomBytes(32).toString("base64url");
}
async function isCoreGatewayHealthy(coreEndpoint: string): Promise<boolean> {
const health = await readCoreGatewayHealth(coreEndpoint);
return health?.status === "ok";
@@ -3851,6 +3902,16 @@ function omitLocalObservabilityHeaders(headers: Record<string, string>): Record<
return forwarded;
}
function withCoreGatewayAuthHeader(headers: Record<string, string>, token: string): Record<string, string> {
if (!token) {
throw new Error("Core gateway auth token is not initialized.");
}
return {
...headers,
[coreGatewayAuthHeader]: token
};
}
function filteredResponseHeaders(headers: Headers): Array<[string, string]> {
const entries: Array<[string, string]> = [];
headers.forEach((value, key) => {
+84
View File
@@ -1541,6 +1541,89 @@ export type AgentAnalysisSubagentRow = {
totalTokens: number;
};
export type AgentAnalysisTraceRunKind = "agent" | "llm" | "route" | "subagent" | "tool";
export type AgentAnalysisTraceRunStatus = "error" | "success";
export type AgentAnalysisTracePayloadPreview = {
kind: "empty" | "json" | "text";
preview: string;
sizeBytes: number;
truncated: boolean;
};
export type AgentAnalysisTracePayloadPart = "tool-input" | "tool-result";
export type AgentAnalysisTracePayloadRequest = {
callId?: string;
part: AgentAnalysisTracePayloadPart;
requestLogId: number;
};
export type AgentAnalysisTracePayloadFullResult = {
content: string;
found: boolean;
kind: "empty" | "json" | "text";
sizeBytes: number;
sourceTruncated: boolean;
};
export type AgentAnalysisTraceToolDetail = {
callId?: string;
input?: AgentAnalysisTracePayloadPreview;
result?: AgentAnalysisTracePayloadPreview;
resultRequestId?: string;
resultRequestLogId?: number;
};
export type AgentAnalysisTraceRun = {
agent: AgentKind;
cacheReadTokens: number;
cacheWriteTokens: number;
concurrentRequests: number;
depth: number;
durationMs: number;
endedAt: string;
error?: string;
id: string;
inputTokens: number;
kind: AgentAnalysisTraceRunKind;
model?: string;
name: string;
offsetMs: number;
outputTokens: number;
parentId?: string;
path?: string;
provider?: string;
requestId?: string;
requestLogId?: number;
routeReason?: string;
sessionId: string;
startedAt: string;
status: AgentAnalysisTraceRunStatus;
statusCode?: number;
tool?: AgentAnalysisTraceToolDetail;
toolName?: string;
totalTokens: number;
};
export type AgentAnalysisTrace = {
agent: AgentKind;
durationMs: number;
endedAt: string;
errorCount: number;
id: string;
llmRunCount: number;
maxDepth: number;
rootRunId: string;
runCount: number;
runs: AgentAnalysisTraceRun[];
sessionId: string;
startedAt: string;
subagentRunCount: number;
toolRunCount: number;
};
export type AgentObservabilityClientRow = AgentAnalysisTotals & {
agent: AgentKind;
key: string;
@@ -1604,6 +1687,7 @@ export type AgentAnalysisSessionDetail = {
subagents: AgentAnalysisSubagentRow[];
tools: AgentAnalysisToolRow[];
totals: AgentAnalysisTotals;
trace: AgentAnalysisTrace;
};
export type AgentAnalysisSnapshot = {
+1
View File
@@ -4,6 +4,7 @@ export const IPC_CHANNELS = {
appDetectProviderIcon: "ccr:app:detect-provider-icon",
appGetConfig: "ccr:app:get-config",
appGetAgentAnalysis: "ccr:app:get-agent-analysis",
appGetAgentTracePayload: "ccr:app:get-agent-trace-payload",
appGetGatewayStatus: "ccr:app:get-gateway-status",
appGetInfo: "ccr:app:get-info",
appGetOnboardingFinished: "ccr:app:get-onboarding-finished",