把 magpie 装进无头服务器:一次部署、三次判断被推翻与双号池接入
把 magpie 装进无头服务器:一次部署、三次判断被推翻与双号池接入
这篇文章记录在同一天里做完的一件事:把 magpie 装进一台没有图形界面的 Ubuntu 服务器,用 Web UI 管理它,最后把服务器上那套模型号池接进本地的 DSH。中间有三次判断被自己的实测推翻,都记下来。
提前说明:这里的 magpie 不是 Windows 上那个窗口放大工具,而是
yetone/magpie—— 一个 agent 模型统一入口。
先搞清楚 magpie 是什么
它做两件事:
- 统一改写各家 agent 的模型配置(Claude Code、Codex、OpenCode、Pi……),一处切换全部生效;
- 内置一个 OpenAI 兼容网关,默认监听
127.0.0.1:3425。
对我们要做的事来说,第二点才是关键:只要网关能访问,任何 OpenAI 兼容客户端都能借到它背后的账号池。
它在 Linux 上发两个构建,这个分叉后来决定了整个部署路径:
| 构建 | 文件 | 依赖 |
|---|---|---|
| 桌面版 | magpie-linux-amd64 |
需要 GTK 3 + WebKitGTK 4.1 |
| 终端版 | magpie-cli-linux-amd64 |
静态链接,零系统依赖 |
终端版包含 TUI + Web UI + CLI 三套界面,Web UI 不需要图形环境。
第一次推翻:桌面版装不上
服务器是 Ubuntu 24.04 / x86_64 / glibc 2.39,纯 headless,一个 GUI 库都没有。我按直觉先把桌面版拷过去试:
$ ldd /tmp/magpie-probe | grep 'not found'
libwayland-client.so.0 => not found
libgtk-3.so.0 => not found
libgdk-3.so.0 => not found
libgdk_pixbuf-2.0.so.0 => not found
libwebkit2gtk-4.1.so.0 => not found
libsoup-3.0.so.0 => not found
libjavascriptcoregtk-4.1.so.0 => not found
$ /tmp/magpie-probe --version
error while loading shared libraries: libwayland-client.so.0: cannot open shared object file
7 个缺失库,启动阶段就挂 —— 哪怕我只想跑 web 模式。动态链接器在 main() 之前就检查 DT_NEEDED,不看你用不用得到那个功能。
补库要多少?
$ apt-get -s install --no-install-recommends libgtk-3-0t64 libwebkit2gtk-4.1-0 ...
134 newly installed
而服务器磁盘只剩 4.8G(88% 已用)。为了跑一个工具装上百个包,不划算。
正解藏在官方 install.sh 的注释里:
Linux: puts magpie in
~/.local/bin— the desktop app when GTK 3 and WebKitGTK 4.1 are installed, the terminal build otherwise.
脚本里有 has_gui_libs(),用 ldconfig -p 查那两个库,没有就自动选终端版。而终端版是静态的:
$ file magpie-cli
ELF 64-bit LSB executable, x86-64, statically linked
$ ldd magpie-cli
not a dynamic executable
结论:headless 机器上不要装桌面版,直接用 terminal build。
第二个坑:GitHub 只有 17 KB/s
终端版 64 MB,服务器直连 GitHub:
$ curl -o magpie-cli .../magpie-cli-linux-amd64
# 5 分钟后:5,251,072 / 64,368,802 字节
约 17 KB/s。本机(有代理)同一文件是 8.5 MB/s。
所以改成"本机下载 + scp":
| 步骤 | 耗时 |
|---|---|
| 本机下载 64 MB | 约 8 秒 |
scp 到服务器 |
1 分 24 秒(约 760 KB/s) |
校验后才装:SHA-256 ddcc5ddb…eb6f9eb、尺寸 64,368,802 字节,与官方 release 一致。
这条经验后面又救了一次场(见 Bun 那节)。在国内云上装 GitHub 分发的东西,"本机下好再传"几乎总是比直连快一个数量级。
让它活下来:systemd + linger
最初用 setsid 裸起:
setsid -f env MAGPIE_WEB_KEY=... magpie web --addr 127.0.0.1:3500 --no-open
能用,但服务器一重启就没了。
改用 systemd user service 时踩到一个隐蔽的坑:user manager 默认会跟着最后一个 SSH 会话一起退出。
$ loginctl show-user admin -p Linger
Linger=no
于是:
sudo loginctl enable-linger admin
验证方式不是看命令返回值,而是看进程树:
$ ps -o pid,ppid,cmd -p $(pgrep -u admin -f 'systemd --user')
PID PPID CMD
2679568 1 /usr/lib/systemd/systemd --user
PPID=1 才说明它真的独立于 SSH 会话了。
服务单元:
[Unit]
Description=magpie web UI and gateway (headless)
[Service]
Type=simple
EnvironmentFile=/home/admin/.config/magpie/web.env
ExecStart=/home/admin/.local/bin/magpie web --addr 127.0.0.1:3500 --no-open
Restart=always
RestartSec=5
自愈实测:kill -9 主进程后 10 秒内换新 PID,端口恢复监听,页面依旧 200。
对外访问的三次演进
第一版:SSH 隧道
gateway 和 Web UI 都绑在 127.0.0.1,本机要访问只能建隧道:
ssh -N -L 127.0.0.1:3500:127.0.0.1:3500 ztb-zeb-server
做成 systemd user service 后能自愈。但缺陷很明显:每台想用的电脑都得自己建隧道,手机根本没法用。
顺带纠正一个常见误解:nohup / setsid 只解决"终端关了进程还活着",不解决重连。实测关掉 shell 后 ssh 进程确实被 init 收养(PPID 变成 847)、端口继续响应,但网络抖一次它就成僵尸——进程还在、端口还占着、连接已死。所以最后还是落到 systemd 的 Restart=always。
第二版:绑 Tailscale IP
服务器上本来就装了 Tailscale(1.102.2),tailscale0 = 100.64.x.x。把监听地址从 127.0.0.1 换成这个 IP:
ExecStart=... magpie web --addr 100.64.x.x:3500 --no-open
本机、Windows 桌面、手机,只要在同一个 tailnet 里,浏览器直接开就能用。隧道退役。
我也试过看起来更优雅的 tailscale serve(自带 HTTPS 反代),结果卡在证书获取:国内访问 Tailscale 的证书服务太慢,命令挂住,serve status 始终是 No serve config。清理掉,放弃这条路。
第三版:magpie 原生的「局域网共享」(最终)
后来才发现,magpie 的「设置 › 网络与共享」里本来就有一项「局域网共享」:
- 开了它,gateway 从
127.0.0.1:3425变成*:3425(所有网卡) - 自动生成网关密钥
lanKey - 配套「允许的地址」CIDR 白名单
开启后实测:
# 带密钥
$ curl -H "Authorization: Bearer sk-magpie-key-…" \
http://100.64.x.x:3425/v1/models
200 → 服务器号池的 3 个模型
# 不带密钥
$ curl http://100.64.x.x:3425/v1/models
401
到这一步,SSH 隧道完全多余了 —— agent 直接连 tailnet 地址即可。这才是它设计好的用法,我前面绕了远路。
安全上补一刀:在「允许的地址」里填 100.64.0.0/10(CGNAT 段,即 tailnet 范围),非 tailnet 来源一律拒绝,即使带着密钥。
插件宿主需要 Bun,又撞上 GitHub
装 WorkBuddy 插件时,Web UI 直接红字报错:
downloading Bun 1.4.2 to run plugins:
https://github.com/oven-sh/bun/releases/download/bun-v1.4.2/bun-linux-x64-baseline.zip:
http: client connection lost
magpie 的插件宿主(plugin host)用 Bun 运行,第一次加载插件时去 GitHub 拉 36.6 MB 的 zip。又是那条 17 KB/s 的链路。
解法还是"本机下载 + 传送",但得知道它放哪。本机已有的缓存可以做参照:
~/.cache/magpie/bun/
├── 1.4.3/magpie-bun
├── 1.4.4/magpie-bun
└── state.json {"current":"1.4.4","previous":"1.4.3","checked":"…"}
于是照抄这个布局:
# 本机(36,646,949 字节)
curl -O .../bun-v1.4.2/bun-linux-x64-baseline.zip
unzip → bun-linux-x64-baseline/bun # 79,500,640 字节
scp bun ztb-zeb-server:/tmp/magpie-bun-1.4.2
# 服务器
install -m 755 /tmp/magpie-bun-1.4.2 ~/.cache/magpie/bun/1.4.2/magpie-bun
cat > ~/.cache/magpie/bun/state.json <<'EOF'
{"current": "1.4.2", "previous": "", "checked": "2026-10-31T10:10:18Z"}
EOF
systemctl --user restart magpie-web
验证成功的标志不是"没有报错",而是这两样:
$ ~/.cache/magpie/bun/1.4.2/magpie-bun --version
1.4.2
$ ls ~/.cache/magpie/plugin-host/
host-57e4026712e7.js pi-2694d2cfa370.js
插件宿主脚本被生成出来,说明 Bun 真的被调起来了。接着:
$ magpie plugin add workbuddy
✓ added @magpie-community/opencode-workbuddy-auth@latest
workbuddy-ai (16 models) not signed in
workbuddy (16 models) not signed in
值得注意的是:插件本体走 npm,服务器访问 npm 完全正常(npmmirror / npmjs 都 200)。坏的只有 GitHub 这一条链路。
双号池:为什么故意不合并
本机 magpie 上挂着 9 个以上 WorkBuddy 账号:
workbuddy · Free plan · . Credits 55% (2200.75 / 4000)
workbuddy · Free plan · 137****4011 Credits 17%
workbuddy · Free plan · 13962297849 Credits 50%
...
服务器上是另一套,2 个账号:
workbuddy · Free plan · 192****5634 Credits 96% (2893.16 / 3000)
workbuddy · Free plan · 192****7516 Credits 57%
我的选择是不合并:两套各自轮询、各自限速,互不影响。于是本地 DSH 需要两个 provider:
DSH
├─ provider magpie → 127.0.0.1:3425 → 本机 magpie → 本机号池
└─ provider magpie-server → 100.64.x.x:3425 → 服务器 magpie → 服务器号池
两者确实不是同一套,模型列表都不一样:
| 端点 | 模型 |
|---|---|
127.0.0.1:3425 |
22 个(auto/river/codex/qoder/workbuddy…) |
100.64.x.x:3425 |
3 个(workbuddy/deepseek-v4.1-flash、glm-5.3、glm-5.3-flash) |
接进 DSH:判断错了两次
错一:以为协议必须改
在「设置 › 模型 › 自定义模型 API」里新建 provider 时,「API 协议」默认是 OpenAI Responses。我第一反应是"这不对,magpie gateway 是 Chat Completions 兼容",于是断言必须改成 openai-completions。
一测就打脸:
$ curl -X POST http://100.64.x.x:3425/v1/responses \
-d '{"model":"workbuddy/glm-5.3-flash","input":"say OK"}'
{"object":"response","model":"glm-5.3-flash",
"output":[{"type":"reasoning",…},{"text":"OK"}]}
两种协议都支持。 更直接的反证其实一直摆在眼前:配置文件里早就有一个 magpie-responses provider,用的就是 api: openai-responses 指向同一个 gateway。我看到了,却没当回事。
错二:以为必须重启 DSH
DSH 的 cordis.patch.yml 只在启动时加载,没有热重载,所以我断言改完必须 systemctl --user restart。
但实际上,在 UI 里补上密钥之后直接就好了,进程根本没重启:
$ systemctl --user show dsh-web.service -p MainPID -p ActiveEnterTimestamp --value
Sat 2026-10-31 12:13:17 CST
886
原因在源码里:llm-pi-ai 适配器是 "dormant settings-driven" 的,settings 一到就动态注册 routes。UI 里的改动即时生效;外部改文件才需要重启。
真正的原因只有一个
本轮运行失败 No API key for provider: magpie-server
表单里密钥留空,provider 就没有 apiKeyEnv,DSH 在发请求前直接拒掉。跟协议、跟重启都无关。
最终配置:
magpie-server:
displayName: Magpie 服务器号池
apiKeyEnv: MAGPIE_GATEWAY_KEY
api: openai-responses
baseURL: http://100.64.x.x:3425/v1
这里有个小技巧:本机 magpie 对任意 key 都返回 200(loopback 免校验),所以可以直接把凭证里的占位值换成服务器的 lanKey —— 一个变量同时喂两个 provider:
- 本机 provider(loopback)→ 不校验,随便过
- 服务器 provider(LAN)→ lanKey 正确,401 变 200
最终架构
本机 DSH
├─ provider magpie → 127.0.0.1:3425 ─────────┐
│ ├→ 本机 magpie → 本机号池(9 账号)
└─ provider magpie-server → 100.64.x.x:3425 │
├ tailnet(WireGuard) │
├ lanKey 认证 │
├ CIDR 100.64.0.0/10 │
└→ 服务器 magpie ───────┴→ 服务器号池(2 账号)
访问面
├─ Web UI http://100.64.x.x:3500/?k=… 仅 tailnet
└─ Gateway http://100.64.x.x:3425/v1 仅 tailnet + lanKey
服务器侧组件:
| 组件 | 形态 | 自愈 |
|---|---|---|
magpie-web.service |
systemd user service + Linger=yes |
Restart=always,5 秒 |
| 运行时 | 静态二进制 ~/.local/bin/magpie |
无动态依赖 |
| 插件宿主 | Bun 1.4.2(离线预置在 ~/.cache/magpie/bun/) |
随插件加载 |
公网零新增端口,云安全组保持原样。
值得记下来的坑
- headless 上别装 GUI 版。 动态链接器在
main()之前就校验 DT_NEEDED,缺库直接拒绝启动,跟你跑不跑 GUI 无关。看install.sh的注释比看 README 快。 - 国内云装 GitHub 分发物,先本机下再传。 17 KB/s 对 8.5 MB/s,差两个数量级;scp 走 SSH 链路通常也有 700 KB/s 以上。
- user service ≠ 常驻。
Linger=no时会话断完就退出,验证要看systemd --user的 PPID 是否为 1。 nohup不解决重连。 活着的僵尸隧道比死了的更麻烦——端口还占着,你还得先找 pid 杀掉才能重开。tailscale serve在国内可能卡证书。 别把它当首选反代,直连 tailnet IP + 应用自带鉴权更省事。- 先翻一遍 UI 的「网络/共享」页。 magpie 本来就有局域网共享 + 密钥 + CIDR 白名单,我一开始完全没注意到,多建了一层隧道。
- 插件宿主是独立运行时。 它不是内嵌执行的,要 Bun;缓存路径固定为
~/.cache/magpie/bun/<version>/magpie-bun,还配套一个state.json。 - UI 建的自定义 provider,创建后只有密钥可改。 协议等字段锁死,建错只能删重建或直接改配置文件。
- "配置文件只读一次"≠"改了必须重启"。 要看改的是文件,还是通过 UI/settings API —— 后者是运行时生效的。
- 说"能不能用"之前先实测。 我两次断言(协议必须改、必须重启)都被一条 curl 和一个进程时间戳推翻了。
最后
这次真正耗时的不是部署,而是三次判断被自己的实测推翻:
- 以为 headless 只能装 GUI 版 → 官方早就分好了两个构建
- 以为 gateway 只支持一种协议 → 两种都通
- 以为改配置必须重启 → UI 改的是运行时
每一次都是因为多跑了一条命令才发现前面想错了。结论还是那句老话:判断靠推理,结论靠实测。