配置完成日期: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 Codesystemd(开机自启)
ccr code环境变量净化 + 进程包装(shim)cc-connect spawn
ccr 服务(3456端口)anthropic ↔ OpenAI 协议转换、模型路由ccr code 检测后自动拉起
Claude CodeLLM 客户端ccr code exec
上游 provider实际推理ccr 转发

2. 关键文件路径速查

文件作用
/root/.cc-connect/config.tomlcc-connect 主配置
/root/.cc-connect/logs/cc-connect.logcc-connect 运行日志
/root/.cc-connect/sessions/会话状态(自动维护)
/root/.claude-code-router/config.jsonccr 主配置(provider/router)
/root/.claude-code-router/plugins/ccr 自定义 transformer 插件
/root/.claude-code-router/logs/ccr 详细请求日志(每次启动一个新文件)
/etc/systemd/system/cc-connect.servicesystemd 单元(由 daemon install 生成)
/root/.npm-global/bin/ccrccr 可执行文件
/root/.npm-global/bin/claudeclaude 可执行文件
/usr/local/bin/cc-connectcc-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]stringbot 收到消息后 Claude Code 的工作目录
cli_path[projects.agent.options]string启动 Claude 的命令行(空格分词,首段为 binary)
mode[projects.agent.options]stringclaude 权限模式: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_urlrouter_api_key 字段,目的是让 cc-connect 自己向 claude 子进程注入 ANTHROPIC_BASE_URLANTHROPIC_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 这两个字段就多此一举:

  1. 它们触发 cc-connect 内部 disableVerbose := a.routerURL != "" 旧逻辑(故意不加 --verbose)
  2. 而 Claude Code 2.1.x 强制要求 stream-json 必须搭配 --verbose
  3. 两边冲突,链路报错

虽然我们已经用 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

  1. 在 BotFather 创建新 bot,拿到 token
  2. 编辑 /root/.cc-connect/config.toml,改 [projects.platforms.options] 下的 token
  3. cc-connect daemon restart
  4. 在新 bot 上发条消息确认 bot 名变了

6.4 添加新项目(不同的 work_dir)

复制 [[projects]] 段,改 namework_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,可以:

  1. 把 ccr 注册为 systemd 服务(用 Wants 软依赖,不用 Requires 避免链式重启)
  2. 或在 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_dirmodecli_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. 方法论收获(授人以渔)

  1. 401 排错三板斧(顺序很重要):

    1. env | grep -iE 'anthropic|api' — 看进程会读到什么环境变量
    2. 对比"实际请求目的地"和"配置的目的地" — 用 tcpdump / 日志 / strace
    3. 翻路由组件源码
  2. 配置不工作时的真相来源:不是 README、不是示例、不是 issue 区,而是源码里的 toml:"xxx,omitempty" struct tag。

  3. 优先用工具的"正式入口":ccr code 是 claude-code-router 验证过的稳定入口,把环境变量管理责任明确交给 ccr;不要绕开它,自己用 router_url + router_api_key 拼装等价行为 — 表面等价,实际有环境变量优先级陷阱。

  4. systemd 服务的 PATH 问题:默认非常窄。要么让安装工具自动继承(cc-connect daemon install 就这么干了),要么在配置里写绝对路径。两者结合最稳(双保险)。

  5. 隐藏的"故意为之但已过时"逻辑:开源项目升级时,某些条件分支会随依赖版本变化变成 bug。disableVerbose := a.routerURL != "" 就是典型 — 最初是合理设计,但 claude 2.1.x 强制 verbose 后这条变成了死锁。这种 bug 的特征是:注释非常合理,代码看起来对,但跑起来错。要靠"实测 + 跨版本对比" 才能识破。

  6. 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 化(推荐)

  1. /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 是后台化的,可能需要用其他方式保持前台。

  1. 修改 /etc/systemd/system/cc-connect.service[Unit] 段,加:
After=ccr.service Wants=ccr.service

不要用 Requires,否则 ccr 崩溃会拖着 cc-connect 一起重启。用 Wants 是软依赖:ccr 启动失败不影响 cc-connect 运行。

  1. 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个根因"备忘

#根因修复点
1TOML 字段名错:working_dirwork_dir
2TOML 段位置错移到 [projects.agent.options]
3claude 2.1.x + cc-connect 1.3.2 verbose 死锁cli_path = "ccr code --verbose"
4allow_from 数组写错;admin_from 段位置错改 string,admin_from 移到项目级
5shell 脏 ANTHROPIC_AUTH_TOKEN 污染改用 ccr code 启动而非裸 claude
6ccr code 自动拉起在 systemd daemon 下不可靠每次重启后手动 ccr start,或 systemd 化 ccr(见 9.2)

本文由 Claude(Sonnet 4.5)协助 rain1226 整理于 2026-05-01~05-02,内容基于真实排错过程,而非泛泛官方文档复述。