配置完成日期:2026-05-01
1. 架构总览
┌──────────────┐ Telegram API ┌────────────────┐
│ Telegram 客户端│ ─────────────────> │ Telegram Bot │
│ (你的手机) │ @my_connect_bot
└──────────────┘ └───────┬────────┘
│ WebSocket / Long polling
↓
┌──────────────────────────┐
│ cc-connect (systemd守护) │
│ - 监听 Telegram 消息 │
│ - 调度 Claude Code 子进程 │
│ - allow_from 鉴权 │
└──────────────┬───────────┘
│ spawn: ccr code --verbose ...
↓
┌──────────────────────────┐
│ ccr code (shim) │
│ - 强制覆盖 ANTHROPIC_* │
│ - 检测并自动拉起 ccr 服务 │
│ - exec claude │
└──────────────┬───────────┘
│ ANTHROPIC_BASE_URL=http://127.0.0.1:3456
↓
┌──────────────────────────┐
│ claude (Claude Code CLI) │
│ - stream-json 协议 │
└──────────────┬───────────┘
│ HTTP (anthropic 协议)
↓
┌──────────────────────────┐
│ ccr 服务 (127.0.0.1:3456) │
│ - 协议转换 (anthropic→OpenAI) │
│ - 路由到上游 provider │
└──────────────┬───────────┘
│
↓
┌──────────────────────────┐
│ 上游 LLM provider │
│ (LongCat / GLM / Grok 等) │
└──────────────────────────┘
每个组件的单一职责:
| 组件 | 角色 | 由谁拉起 |
|---|---|---|
| Telegram bot | 用户消息入口 | Telegram 服务端 |
| cc-connect | 把 Telegram 消息桥接到本地 Claude Code | systemd(开机自启) |
ccr code | 环境变量净化 + 进程包装(shim) | cc-connect spawn |
| ccr 服务(3456端口) | anthropic ↔ OpenAI 协议转换、模型路由 | ccr code 检测后自动拉起 |
| Claude Code | LLM 客户端 | ccr code exec |
| 上游 provider | 实际推理 | ccr 转发 |
2. 关键文件路径速查
| 文件 | 作用 |
|---|---|
/root/.cc-connect/config.toml | cc-connect 主配置 |
/root/.cc-connect/logs/cc-connect.log | cc-connect 运行日志 |
/root/.cc-connect/sessions/ | 会话状态(自动维护) |
/root/.claude-code-router/config.json | ccr 主配置(provider/router) |
/root/.claude-code-router/plugins/ | ccr 自定义 transformer 插件 |
/root/.claude-code-router/logs/ | ccr 详细请求日志(每次启动一个新文件) |
/etc/systemd/system/cc-connect.service | systemd 单元(由 daemon install 生成) |
/root/.npm-global/bin/ccr | ccr 可执行文件 |
/root/.npm-global/bin/claude | claude 可执行文件 |
/usr/local/bin/cc-connect | cc-connect 可执行文件 |
3. 当前最终配置
3.1 cc-connect 主配置
文件:/root/.cc-connect/config.toml
language = "en"
[[projects]]
name = "default"
# 启用 /shell /restart /upgrade 等管理命令(仅本人可用,project 级配置)
admin_from = "xxxxxxxxxx"
[projects.agent]
type = "claudecode"
[projects.agent.options]
work_dir = "/root/some-py/cc-connect"
mode = "default"
# 通过 ccr code 启动 claude,由 ccr 内部完成 ANTHROPIC_BASE_URL/API_KEY 注入
# --verbose 是 Claude Code 2.1.x 用 stream-json 时的强制要求
# 用绝对路径,避免 systemd daemon 模式下 PATH 不含 /root/.npm-global/bin 时找不到 ccr
cli_path = "/root/.npm-global/bin/ccr code --verbose"
[[projects.platforms]]
type = "telegram"
[projects.platforms.options]
token = "<你的 Telegram bot token>"
api_proxy = "https://my.proxy.com"
# 仅允许本人使用,屏蔽陌生人
allow_from = "xxxxxxxxxxx"
注意字段层级与类型:
| 字段 | 段位置 | 类型 | 说明 |
|---|---|---|---|
work_dir | [projects.agent.options] | string | bot 收到消息后 Claude Code 的工作目录 |
cli_path | [projects.agent.options] | string | 启动 Claude 的命令行(空格分词,首段为 binary) |
mode | [projects.agent.options] | string | claude 权限模式:default / acceptEdits / plan / bypassPermissions |
allow_from | [projects.platforms.options](平台级) | string,逗号分隔 | 允许使用 bot 的 user_id |
admin_from | [[projects]](项目级!) | string,逗号分隔 | 允许执行特权命令(/shell、/restart 等)的 user_id |
3.2 ccr 路由配置(节选)
文件:/root/.claude-code-router/config.json 中的关键字段:
{
"PORT": 3456,
"APIKEY": "<ccr 自身的接入鉴权 key>",
"Providers": [
{
"name": "mei",
"api_base_url": "https://api.longcat.chat/openai/v1/chat/completions",
"api_key": "<LongCat api key>",
"models": ["LongCat-Flash-Lite", "LongCat-Flash-Chat", "LongCat-Flash-Thinking-2601"],
"transformer": { "use": ["mei"] }
}
// ... 其他 provider 省略
],
"Router": {
"default": "mei,LongCat-Flash-Thinking-2601",
"background": "mei,LongCat-Flash-Thinking-2601",
"think": "mei,LongCat-Flash-Thinking-2601",
"longContext": "mei,LongCat-Flash-Thinking-2601",
"longContextThreshold": 60000,
"webSearch": "mei,LongCat-Flash-Thinking-2601"
}
}
3.3 systemd 单元(自动生成,无需手工维护)
文件:/etc/systemd/system/cc-connect.service(由 cc-connect daemon install 生成)
关键设置:
Type=simple+Restart=on-failure RestartSec=10异常自动恢复Environment="PATH=..."自动继承当前 shell 的完整 PATH(包含/root/.npm-global/bin)Environment="HTTP_PROXY=..."等代理设置自动继承WantedBy=multi-user.target开机自启
4. 关键设计决策(为什么这么配)
4.1 为什么用 ccr code 而不是直接 claude?
cc-connect 配置里其实有 router_url 和 router_api_key 字段,目的是让 cc-connect 自己向 claude 子进程注入 ANTHROPIC_BASE_URL 和 ANTHROPIC_API_KEY。但这种方式存在致命缺陷:
当前 shell 已有 ANTHROPIC_AUTH_TOKEN=sk-...(指向 anyrouter.top)等"脏环境变量",cc-connect 子进程会继承这些变量。在 Linux 上,同名环境变量在 envp 数组里出现多次时,getenv 通常返回第一个匹配(POSIX 标准),所以 cc-connect 后追加的 ANTHROPIC_BASE_URL=http://127.0.0.1:3456 不会被 claude 读到 — claude 读到的还是继承来的 anyrouter URL,直接撞过去,401。
ccr code 是 claude-code-router 提供的正式入口 shim,它在执行 claude 前会构造干净的 envp(不是 append,而是 set/覆盖),把所有 ANTHROPIC_* 变量都设成正确值,彻底隔离脏环境的影响。
方法论:用工具的"正式入口"(ccr code),不要绕开它自己拼装等价行为(router_url + router_api_key)— 后者表面等价,实际有隐藏的环境变量优先级陷阱。
4.2 为什么删掉 router_url / router_api_key?
既然 ccr code 已经把环境变量注入责任完整接走了,cc-connect 这两个字段就多此一举:
- 它们触发 cc-connect 内部
disableVerbose := a.routerURL != ""旧逻辑(故意不加--verbose) - 而 Claude Code 2.1.x 强制要求
stream-json必须搭配--verbose - 两边冲突,链路报错
虽然我们已经用 cli_path = "ccr code --verbose" 显式注入 verbose 绕过了这个 bug,但保留 router_url 仍会让 cc-connect 内部多余地 append 环境变量(虽不致命,但增加污染概率)。最干净的做法是只让 ccr code 管环境。
4.3 为什么 cli_path 用绝对路径?
systemd 服务的默认 PATH 是 /usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin,不包含 /root/.npm-global/bin。虽然 cc-connect daemon install 实际上会把当前 shell 的 PATH 写入 unit 文件(很贴心),但万一以后:
- 重新执行
daemon install时所在 shell 的 PATH 变了 - 手动改了 unit 文件的 PATH
- 卸载重装后的环境
绝对路径 /root/.npm-global/bin/ccr 不依赖 PATH,永远稳。
4.4 为什么 allow_from / admin_from 不能复制 INSTALL.md 示例?
INSTALL.md 里 QQ 平台的示例写 allow_from = "12345,67890"(string,逗号分隔),但很容易误以为它接受数组。直接读 cc-connect 源码 opts["allow_from"].(string) 强类型断言才是真相 — 必须是 string。
而且 admin_from 不在平台级,它是项目级配置(在 [[projects]] 段下,与 name 平级)。这点 INSTALL.md 没明示,只能从源码 config.go 的 struct tag toml:"admin_from,omitempty" 看出。
方法论:配置不工作时,直接 grep 源码里的 toml:"xxx" 标签和字段所属 struct,这是绝对真相。
5. 运维命令速查
5.1 cc-connect daemon 管理
cc-connect daemon status # 看服务是否在跑
cc-connect daemon logs # 看最近日志
cc-connect daemon logs -f # 实时跟踪日志(类似 tail -f)
cc-connect daemon logs -n 50 # 看最近 50 行
cc-connect daemon restart # 改了 config.toml 后重启
cc-connect daemon stop # 临时停止
cc-connect daemon start # 启动
cc-connect daemon uninstall # 卸载 systemd 单元(配置文件不删)
5.2 标准 systemctl(完全等价)
systemctl status cc-connect # 状态
systemctl restart cc-connect # 重启
systemctl stop cc-connect # 停止
systemctl start cc-connect # 启动
systemctl disable cc-connect # 取消开机自启
systemctl enable cc-connect # 开机自启
systemctl cat cc-connect # 查看 unit 文件
journalctl -u cc-connect -f # 看 systemd 层面日志
5.3 ccr 服务管理
ccr status # 查 ccr 服务状态
ccr start # 手动启动 ccr 服务(通常不需要,ccr code 会自动)
ccr stop # 停止 ccr 服务
ccr restart # 重启(改了 config.json 后必须)
ccr code # 启动 claude(会自动拉起 ccr 服务)
ccr ui # 浏览器打开 ccr web UI
ccr model # 交互式选择模型
5.4 实时观察 telegram 消息处理
# 同时看 cc-connect 和 ccr 两边日志
cc-connect daemon logs -f &
tail -f /root/.claude-code-router/logs/ccr-*.log &
6. 常见运维场景
6.1 修改了 config.toml 后让生效
cc-connect daemon restart
# 或
systemctl restart cc-connect
6.2 修改了 ccr 的 config.json 后让生效
ccr restart
注意:改 ccr 不需要重启 cc-connect,因为 cc-connect 通过 ccr code 间接调用,而 ccr code 每次都会读最新的 ccr 配置。
6.3 换 Telegram bot
- 在 BotFather 创建新 bot,拿到 token
- 编辑
/root/.cc-connect/config.toml,改[projects.platforms.options]下的token cc-connect daemon restart- 在新 bot 上发条消息确认 bot 名变了
6.4 添加新项目(不同的 work_dir)
复制 [[projects]] 段,改 name 和 work_dir,可以共用同一个 telegram bot(用 allow_chat 区分群组,或用 /switch 命令在不同项目间切换会话)。
[[projects]]
name = "frontend"
admin_from = "xxxxxxxxxx"
[projects.agent]
type = "claudecode"
[projects.agent.options]
work_dir = "/root/some-py/frontend"
mode = "default"
cli_path = "/root/.npm-global/bin/ccr code --verbose"
[[projects.platforms]]
type = "telegram"
[projects.platforms.options]
token = "<同一个 token>"
allow_from = "xxxxxxxxxxxx"
6.5 切换 ccr 上游模型
只动 ccr 配置:
# 编辑 /root/.claude-code-router/config.json,改 Router.default
ccr restart
# cc-connect 不用动!下条消息自动用新模型
6.6 机器重启后
需要手动先启动 ccr 服务:先 SSH 到服务器执行 ccr start,否则 telegram 消息会 401。
为什么:ccr code 的自动拉起机制在交互式 shell 里验证通过,但在 systemd daemon 环境下不可靠 — cc-connect 作为 systemd 服务调 ccr code 时,ccr code 可能无法成功 spawn ccr 服务(可能与 node 进程的 daemonize、PID file 竞争、PATH 等因素有关)。ccr code 未报错但 ccr 端口没起来,导致 claude 连 3456 端口失败,返回 401。
判断方法:如果 Telegram 发消息后等 3 分钟没回复,大概率是 ccr 没起来。
# 快速检查
ccr status # 看 ccr 是否在跑
ss -tlnp | grep :3456 # 端口有没有监听
# 没在跑就启动
ccr start
完整启动顺序:
开机
→ systemd 自动启 cc-connect daemon (bot 连接成功,但 ccr 服务还没起)
→ 你 SSH 上来执行 ccr start
→ 发 Telegram 消息 → 链路通
可选优化:如果不想每次手动 ccr start,可以:
- 把 ccr 注册为 systemd 服务(用
Wants软依赖,不用Requires避免链式重启) - 或在 cc-connect 的 unit 文件里加
ExecStartPre=/root/.npm-global/bin/ccr start
(详见第 9.2 节)
6.7 看会话历史
cc-connect 会话状态:/root/.cc-connect/sessions/default_*.json
每条消息都会在 jsonl 里留完整记录(用户输入、claude 回复、工具调用)。
6.8 在 Telegram 直接用斜杠命令
/help - 看全部 46 个命令
/new - 开新会话
/list - 列会话
/switch <id> - 切换会话
/current - 当前活跃会话
/history 20 - 看最近 20 条历史
/mode plan - 切换权限模式 (default/edit/plan/yolo)
/quiet - 切换思考过程显示
/stop - 中止当前 turn
/provider - 列出/切换 ccr provider
/shell <cmd> - 在服务器上跑 shell(仅 admin_from)
/restart - 重启 cc-connect 自身(仅 admin_from)
7. 排错指南(本次踩过的 5 大坑)
坑 1:字段名错(working_dir vs work_dir)
现象:启动看似 OK,但行为异常 / 日志里 work_dir 是默认值。
根因:cc-connect TOML 字段是 work_dir(下划线短形式),不是 working_dir。
定位法:
# 看源码里的 toml tag
curl -s https://raw.githubusercontent.com/chenhg5/cc-connect/main/agent/claudecode/claudecode.go | grep -E 'toml:"' | head
坑 2:字段位置错([projects.agent] vs [projects.agent.options])
现象:配置看似已写,但完全不生效。
根因:work_dir、mode、cli_path 等业务参数必须放在 [projects.agent.options] 内,而不是 [projects.agent] 顶层。[projects.agent] 下只放 type。
坑 3:cc-connect v1.3.2 + Claude Code 2.1.126 verbose 兼容死锁
现象:bot 收到消息但 claude 启动失败,日志反复报:
Error: When using --print, --output-format=stream-json requires --verbose
根因:cc-connect 源码 agent/claudecode/claudecode.go 第 411 行有逻辑:
disableVerbose := a.routerURL != ""
当配了 router_url(用 ccr 路由)时,故意禁用 --verbose(注释说"verbose 会污染 JSON 流" — 这条注释已过时,实测 claude 2.1.x 加 verbose 后输出仍是合法 NDJSON)。
但 Claude Code 2.1.x 强制要求 stream-json 必须搭配 --verbose,两边冲突。
修复:用 cli_path = "ccr code --verbose" 显式注入(cliExtraArgs 在 innerArgs 之前拼装,会出现在最终命令行)。
坑 4:allow_from / admin_from 字段类型与位置
现象:警告刷屏,陌生人能用 bot;管理命令(/shell 等)被禁用。
根因 1:allow_from = ["xxxxxxxxxx"](数组写法)— 错!源码是 opts["allow_from"].(string) 强转 string,必须是 string,多个用逗号分隔。
根因 2:admin_from 写在 [projects.platforms.options] 下 — 错!从源码 config.go 看,AdminFrom string toml:“admin_from”`` 在项目级 struct 里,应该写在 [[projects]] 段下与 name 平级。
关键提示:cc-connect 启动时这条 WARN 的标签透露了真相:
platform=telegram→ 平台级配置project=default→ 项目级配置
坑 5:脏 ANTHROPIC_AUTH_TOKEN 污染
现象:链路看似"修好"了,bot 真的传消息到 claude,但 claude 直接报 API Error: 401 Invalid API key。
根因:当前 shell 已被设置:
ANTHROPIC_AUTH_TOKEN=sk-ON6Cwp1807aupnQpBuRyXQhV5uivjXOCBFU2OUiUgNk4Eucd
ANTHROPIC_BASE_URL=https://anyrouter.top
(可能在 ~/.bashrc 或 ~/.zshrc 之前为别的用途配过 anyrouter)
cc-connect 子进程继承这些变量。即使 cc-connect 后追加了 ANTHROPIC_BASE_URL=http://127.0.0.1:3456,Linux libc 的 getenv 返回第一个匹配,所以 claude 读到的 BASE_URL 仍是 anyrouter。再加上 ANTHROPIC_AUTH_TOKEN 优先级高于 ANTHROPIC_API_KEY,claude 用 anyrouter 的 token 发请求 — anyrouter 当然 401。
修复:用 ccr code 启动 claude(参见 4.1)。ccr code 内部用 child_process.spawn 显式构造干净 env,完全隔离脏变量。
排错第一动作:env | grep ANTHROPIC — 一秒钟就能发现污染源,比翻路由组件源码快 100 倍。
8. 方法论收获(授人以渔)
401 排错三板斧(顺序很重要):
env | grep -iE 'anthropic|api'— 看进程会读到什么环境变量- 对比"实际请求目的地"和"配置的目的地" — 用 tcpdump / 日志 / strace
- 再 翻路由组件源码
配置不工作时的真相来源:不是 README、不是示例、不是 issue 区,而是源码里的
toml:"xxx,omitempty"struct tag。优先用工具的"正式入口":
ccr code是 claude-code-router 验证过的稳定入口,把环境变量管理责任明确交给 ccr;不要绕开它,自己用router_url + router_api_key拼装等价行为 — 表面等价,实际有环境变量优先级陷阱。systemd 服务的 PATH 问题:默认非常窄。要么让安装工具自动继承(
cc-connect daemon install就这么干了),要么在配置里写绝对路径。两者结合最稳(双保险)。隐藏的"故意为之但已过时"逻辑:开源项目升级时,某些条件分支会随依赖版本变化变成 bug。
disableVerbose := a.routerURL != ""就是典型 — 最初是合理设计,但 claude 2.1.x 强制 verbose 后这条变成了死锁。这种 bug 的特征是:注释非常合理,代码看起来对,但跑起来错。要靠"实测 + 跨版本对比" 才能识破。stream-json 模式 vs –print 模式:同一个 claude,两种调用模式对响应的容忍度不同。stream-json 对 thinking content block 比 –print 宽容得多。所以测组件兼容性必须用真实链路,光跑
claude --print不能代表 cc-connect 场景。
9. 待办 / 可选优化
9.1 清理 ~/.bashrc 里的脏 ANTHROPIC_AUTH_TOKEN(rain 自己处理)
~/.bashrc 或 ~/.zshrc 里某处有:
export ANTHROPIC_AUTH_TOKEN=sk-ON6Cwp...
export ANTHROPIC_BASE_URL=https://anyrouter.top
找到并视情况删除/注释,避免影响其他场景。
9.2 (可选)消除"每次重启后手动 ccr start"
如果不想每次服务器重启后手动 SSH 执行 ccr start,有两种方案:
方案 A: ccr 也 systemd 化(推荐)
- 写
/etc/systemd/system/ccr.service:
[Unit]
Description=Claude Code Router
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
ExecStart=/root/.npm-global/bin/ccr start --foreground
Restart=on-failure
RestartSec=5
Environment="PATH=/root/.npm-global/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
[Install]
WantedBy=multi-user.target
注意:确认 ccr start --foreground 是否支持(若不支持,查 ccr start --help,可能用 ccr start 本身已是前台模式)。如果 ccr start 是后台化的,可能需要用其他方式保持前台。
- 修改
/etc/systemd/system/cc-connect.service的[Unit]段,加:
After=ccr.service Wants=ccr.service
不要用 Requires,否则 ccr 崩溃会拖着 cc-connect 一起重启。用 Wants 是软依赖:ccr 启动失败不影响 cc-connect 运行。
systemctl daemon-reload && systemctl enable --now ccr
方案 B: ExecStartPre 拉起 ccr(简单但不够健壮)
在 cc-connect.service 的 [Service] 段加:
ExecStartPre=/root/.npm-global/bin/ccr start
缺点:如果 ccr 进程意外退出,cc-connect 不会感知也不会重启 ccr。
9.3 (可选)切换非 thinking 模型避开 ccr↔LongCat 协议小问题
测试过程中发现 ccr code --print 模式调 LongCat-Flash-Thinking-2601 会报 API Error: Content block is not a text block,但 cc-connect 走的 stream-json 模式不报。如果以后用 ccr code 进交互式 CLI 也想避开这个问题,可以把 Router.default 改成 mei,LongCat-Flash-Chat(非 thinking 版)。
10. 急救手册(如果某天突然又坏了)
按以下顺序排查,大部分情况能秒级定位:
# Step 1: ccr 服务在吗 ← 最常见故障点!
ccr status
ss -tlnp | grep :3456
# 没在跑? → ccr start
# Step 2: cc-connect 服务在吗
systemctl status cc-connect
# inactive? → systemctl start cc-connect
# Step 3: 网络通吗(ccr → 127.0.0.1:3456)
curl -s -o /dev/null -w "%{http_code}\n" http://127.0.0.1:3456
# 401 = 通了但 token 有问题,200 = 正常,无法连接 = ccr 没起来
# Step 4: bot 连接正常吗
cc-connect daemon logs -n 30 | grep -iE 'telegram|error|warn'
# Step 5: 最近一次消息处理日志
cc-connect daemon logs -n 50 | tail -30
# 关注 input_tokens: 0 = 请求没到 provider(401/502); >0 = 链路通了
# Step 6: claude 端发生了什么
ls -lt /root/.claude/projects/-root-some-py-cc-connect/*.jsonl | head -3
# 用 python 解析最新文件,看 user/assistant 对话
# Step 7: ccr 详细日志(每次启动一个新文件)
ls -lt /root/.claude-code-router/logs/ | head -3
# Step 8: 终极回退 - 从干净环境手工跑一次
env -i HOME=$HOME PATH=$PATH /root/.npm-global/bin/ccr code --print "ping"
快捷命令(一键诊断):
echo "=== ccr ===" && ccr status 2>&1 | head -4 && \
echo "=== port ===" && ss -tlnp 2>/dev/null | grep :3456 && \
echo "=== cc-connect ===" && systemctl is-active cc-connect 2>&1 && \
echo "=== last turn ===" && cc-connect daemon logs -n 10 2>&1 | grep -E 'turn complete|error|input_tokens'
附:配置过程中发现的"6个根因"备忘
| # | 根因 | 修复点 |
|---|---|---|
| 1 | TOML 字段名错:working_dir | 改 work_dir |
| 2 | TOML 段位置错 | 移到 [projects.agent.options] |
| 3 | claude 2.1.x + cc-connect 1.3.2 verbose 死锁 | cli_path = "ccr code --verbose" |
| 4 | allow_from 数组写错;admin_from 段位置错 | 改 string,admin_from 移到项目级 |
| 5 | shell 脏 ANTHROPIC_AUTH_TOKEN 污染 | 改用 ccr code 启动而非裸 claude |
| 6 | ccr code 自动拉起在 systemd daemon 下不可靠 | 每次重启后手动 ccr start,或 systemd 化 ccr(见 9.2) |
本文由 Claude(Sonnet 4.5)协助 rain1226 整理于 2026-05-01~05-02,内容基于真实排错过程,而非泛泛官方文档复述。
