ZH Linux systemd - D0n9X1n/copilot-relay GitHub Wiki
Linux:systemd 用户服务
推荐使用 systemd --user。它在登录时启动,进程退出时自动拉起,并且以你自己的身份运行
—— 因此使用的是你的 ~/.copilot-relay 令牌缓存,而不是另一份。
一次性准备
npm install -g copilot-relay@latest
copilot-relay auth
command -v copilot-relay
创建用户服务
mkdir -p ~/.config/systemd/user
RELAY_BIN="$(command -v copilot-relay)"
cat > ~/.config/systemd/user/copilot-relay.service <<EOF
[Unit]
Description=copilot-relay for Claude Code
After=network-online.target
Wants=network-online.target
StartLimitIntervalSec=300
StartLimitBurst=5
[Service]
Type=simple
ExecStart=${RELAY_BIN} start
WorkingDirectory=%h
Restart=on-failure
RestartSec=30
[Install]
WantedBy=default.target
EOF
copilot-relay start 在前台运行并处理 SIGTERM,所以 Type=simple 是正确的。
不要加 & 或 nohup —— systemd 会把立刻退出当成失败,从而无限重启。
为什么用 on-failure 而不是 always
这里有三项设置是关键,而大多数教程使用的默认值对这个服务来说都是错的。
Restart=on-failure 只在非零退出时重启。用 Restart=always 的话,systemd 在正常
关闭后也会重启 —— 于是 copilot-relay stop 看起来毫无作用,因为 systemd 立刻又把它
拉了起来。参见停止。
RestartSec=30 限制重试频率。relay 启动时会校验上游 Copilot 访问,失败则 exit(1)。
开机时网络通常还没就绪,所以第一次尝试失败是正常的。RestartSec=5 意味着对着一个还不
存在的网络每分钟重试十二次。
StartLimitIntervalSec=300 + StartLimitBurst=5 直接终止这种循环:5 分钟内失败超过
5 次,unit 进入 failed,systemd 不再重试。这把一个看不见的循环变成了一个能被发现、能
被诊断的状态。
After=network-online.target 只保证启动顺序,不保证网络真的可达,所以重试策略仍然有用。
在出站代理后面
systemd --user 服务不读取你的 shell 配置文件,所以使用 upstreamProxy: env 时,relay
看不到你在那里导出的 HTTPS_PROXY。如果这台机器只能经代理访问互联网,请在
~/.copilot-relay/config.yaml 里写上代理地址:
upstreamProxy: http://proxy.example:3128
如果仍想用 upstreamProxy: env,请在 unit 的 [Service] 段里设置这些变量:
Environment=HTTPS_PROXY=http://proxy.example:3128
upstreamProxy 在重启后生效。修改 unit 之后,按下文先 daemon-reload 再重启。规则
(包括凭据)见配置说明。
启用并启动
systemctl --user daemon-reload
systemctl --user enable --now copilot-relay.service
修改 unit 文件后要先 daemon-reload 再 restart,否则 systemd 用的还是旧定义。
登录前就启动
--user 服务默认在登录时启动、注销时停止。要让 relay 在无人登录的机器上持续运行,或
跨注销存活:
sudo loginctl enable-linger "$USER"
如何确认它真的能用
最快的办法
copilot-relay status
copilot-relay 0.2.5
process running (pid 93744, up 1h 16m)
version 0.2.5
listening http://127.0.0.1:4142
health ok (9ms)
models gpt-6-astra[1m], claude-opus-5
upstream not checked (use --deep)
log ~/.copilot-relay/logs/copilot-relay.2026-07-25.log
config ~/.copilot-relay/config.yaml
host 127.0.0.1
port 4142
…
gptModel gpt-6-astra
opusModel claude-opus-5
host, port, claudeSetup and upstreamProxy take effect on restart; the rest hot-reload.
上面的 config 块是节选;status 会打印全部解析后的配置项。参见
配置说明。
version 这一行是正在运行的守护进程自己报告的版本,和第一行不是一回事 —— 第一行
是你刚刚调用的那个 CLI。执行 npm i -g copilot-relay@latest 之后,在服务真正重启之前,
两者会不一致:
version 0.2.6 — MISMATCH, 0.3.0 is installed
在 systemd 下,这通常意味着 unit 从未被重新加载。执行 systemctl --user restart copilot-relay 后再查一次。版本不一致不会改变退出码 —— relay 仍然可用,只是它不是你
刚装上的那个版本。
加上 --deep 会额外发一个真实请求经由 Copilot 走一遍 —— 这是唯一能证明 relay 真的可以
为 Claude Code 服务的检查。它会消耗少量 token,所以默认不做:
copilot-relay status --deep
upstream ok (1191ms) — end-to-end Copilot round trip
退出码便于脚本使用:0 运行中且可达,1 未运行,2 运行中但不可用(健康检查失败,或 --deep 失败)。--json 输出
机器可读格式。因此它可以直接写进 unit 文件:
ExecStartPost=/usr/bin/env copilot-relay status
手动执行这三层
status 已经替你跑了这三层。理解它们仍然有价值,因为前两层在一个根本无法处理任何请求
的 relay 上也会通过 —— 在没有装 CLI 的机器上排查时也用得上。
第一层 —— 进程还活着吗?
systemctl --user status copilot-relay.service
curl -s http://127.0.0.1:4142/healthz
期望 active (running) 和 {"ok":true}。这个接口是静态处理器:它只能证明有个端口在
监听,别的什么都证明不了,完全不会访问 GitHub Copilot。
第二层 —— 配置解析和模型路由正常吗?
curl -s http://127.0.0.1:4142/v1/models
期望看到你配置的模型,例如 gpt-6-astra[1m] 和 claude-opus-5。结果直接来自配置,
同样不访问上游。一个 Copilot 令牌一小时前就过期的 relay,前两层照样通过。
第三层 —— 端到端
curl -s -X POST http://127.0.0.1:4142/v1/messages \
-H "content-type: application/json" \
-H "anthropic-version: 2023-06-01" \
-d '{"model":"gpt-6-astra","max_tokens":256,
"messages":[{"role":"user","content":"Reply with the single word: ok"}]}'
返回 200,带 content 和非零 usage,说明整条链路都是通的:配置、令牌刷新、Copilot
调用,以及转换回 Claude 格式。只有这一层能证明 relay 真的可以为 Claude Code 服务。
它会消耗少量 token。
256-token 上限比 CLI 诊断的 16-token 上限为推理留出更多余量,但并不保证一定有可见文字。
成功的 assistant message 即使 content 为空,只要有 stop_reason: max_tokens 和正数
usage.output_tokens,仍能证明上游可达,而非答案已完成。参见
日志与问题排查。
如果前两层通过而第三层失败,问题在鉴权或上游,不在 unit 文件 —— 执行
copilot-relay auth 并查看当天日志。
查看日志
systemd 把标准输出收进 journal,relay 自己也会写轮转日志文件。启动失败看 journal,请求 历史看日志文件。
journalctl --user -u copilot-relay.service -n 100 --no-pager
journalctl --user -u copilot-relay.service -f
tail -f ~/.copilot-relay/logs/copilot-relay.$(date +%F).log
日志按天轮转,文件名带本地日期。要搜索所有保留的日期,用通配符:
grep -n "Startup preflight failed" ~/.copilot-relay/logs/copilot-relay.*.log
停止
现在停止,下次登录时再启动:
systemctl --user stop copilot-relay.service
现在停止,并且以后登录也不再启动:
systemctl --user disable --now copilot-relay.service
重启:
systemctl --user restart copilot-relay.service
copilot-relay stop 与 systemd 的关系
copilot-relay stop 会直接找到并终止 relay 进程。它是正常退出,所以在
Restart=on-failure 下 systemd 不会再拉起 —— 命令行和守护进程的行为是一致的。
如果用 Restart=always,systemd 会在 RestartSec 之后重启它,看起来就像命令行没生效。
出现这种情况说明你的 unit 还是旧的。
不论用哪种守护方式,确认 relay 确实没了:
copilot-relay stop
ss -ltnp 'sport = :4142' # 应该没有 LISTEN 行
彻底移除
systemctl --user disable --now copilot-relay.service
rm ~/.config/systemd/user/copilot-relay.service
systemctl --user daemon-reload
排查
failed 且提示 start-limit-hit。 5 分钟内失败超过 5 次,systemd 主动放弃了 ——
这是设计如此。先修掉根因,再清除这个状态:
systemctl --user reset-failed copilot-relay.service
systemctl --user start copilot-relay.service
在你手动执行之前,它不会自己重试。
反复出现 status=1。 启动前校验在失败:鉴权过期或没有网络。看 journalctl 和当天
日志,然后执行 copilot-relay auth。
注销后服务就没了。 需要开启 linger:sudo loginctl enable-linger "$USER"。
unit 里 command -v copilot-relay 找不到。 --user unit 拿不到你交互式 shell 的
PATH。上面的 ExecStart 在创建时就写入了绝对路径,正是为了避免这个问题 —— 如果你把它
改成了裸命令,请把完整路径改回去。
relay 在跑但 Claude Code 不走它。 服务注册没问题;检查 ~/.claude/settings.json
里的 ANTHROPIC_BASE_URL。当 claudeSetup: true 时,relay 启动时会自己管理这个值。