deploy: 标准目录部署 —— 运行时不再依赖源码目录

用户注意到:「当前 agentmail 是在源码目录部署的,应当改为标准目录部署」。
查证后有三处实证(都不是猜测):

1. ★ **失败通知钩子执行的是仓库里的脚本**
   (`/home/program/agentmail/deploy/service-failure-notify.mjs`,8 处引用:
   4 个 drop-in + zcode/zcode-mail-bridge 单元 + agentmail-failure-flush)。
   仓库一挪/一改名,故障通知就**静默失效** —— 而那条管线正是用来报告服务故障的。
2. **opencode-serve 的 cwd 就是源码目录**(`WorkingDirectory=/home/program/agentmail`)。
3. ★ **仓库里的 `deploy/*.service` 是旧的源码目录版本**(ExecStart 指向
   `/home/program/agentmail/plugins/...`),而机器上的已被改过 —— 也就是说
   **谁跑一次 install.sh 就会把部署退回源码目录**。drop-in 更是只存在于 /etc 里,
   仓库完全没有它们。

## 改动

- **唯一真相**:`deploy/systemd/` 镜像 systemd 目录结构,收进全部单元与 drop-in
  (8 个单元 + 12 个 drop-in),路径全部改到 `/opt`。
- 运行时脚本装到 **`/opt/agentmail/bin/service-failure-notify.mjs`**(自包含,
  无相对导入);`install.sh` 与 `redeploy-gateway.sh` 都会幂等地装它。
- opencode 的 cwd 改为 `/opt/agentmail`(与网关一致),已重启生效
  (`/proc/<pid>/cwd` 已核)。
- 删掉 `deploy/*.service` 的旧副本,避免两个真相。
- dsh 的 `cordis.patch.yml` 注释里的安装示例也改到标准位置(运行时用的是环境变量,
  那条注释是唯一残留)。

## 判据(`deploy/check-deploy-drift.mjs` 新增「标准目录部署」四条 + 自检)

① 任何 unit/drop-in 都不得引用源码目录;② 已安装单元与 `deploy/systemd/` 逐字节一致;
③ 通知脚本在标准位置且可执行;④ 各服务的 cwd/ExecStart 不在源码目录
(homeagent/dsh/zcode 是**别的产品**的标准位置,按白名单放行)。
自检两个样本:引用源码目录的必须红、干净样本必须绿(证明不是恒真)。

顺带修掉一处**真漂移**:仓库里 dsh 的 `dist/index.js` 落后于部署件(改了 src 没重建),
重建后 `check-deploy-drift` 报「四个宿主都在跑当前代码」。

## 复核

- `/etc/systemd/system/` 引用仓库:**0** 个文件;`/opt/agentmail` 下只剩旧二进制/备份里
  的构建路径(Go 嵌的源码路径,无害)与一条注释。
- 四个宿主都在跑当前代码;标准目录四项全绿。
- 全部服务 active,opencode/网关 cwd 均已在安装根下。
This commit is contained in:
2026-09-14 08:38:33 +08:00
parent 5b6fef764f
commit 51789ee72e
27 changed files with 450 additions and 40 deletions

View File

@ -0,0 +1,14 @@
# 定期把 spool 里的故障通知补投出去(配合 agentmail-failure-flush.timer)。
#
# 为什么需要它:补投原先只在**同一个单元**的 ExecStartPost 跑 `--flush`,
# 于是两条路都堵死 —— 报告在某 Agent 目录里而它一直没重启(实测隔一天才补投),
# 或报告落进 unknown-agent/(**任何** flush 都不读那个目录,实测 25 封死信)。
# 定时跑 --flush-all 让这两种情形都不再发生:它遍历所有 Agent 目录,
# 按目录名从 /etc/agentmail/<agent>.env 解析各自的身份再投。
[Unit]
Description=AgentMail 故障通知补投(排空 spool)
Documentation=file:///opt/agentmail/docs/PLAN.md
[Service]
Type=oneshot
ExecStart=/usr/bin/node /opt/agentmail/bin/service-failure-notify.mjs --flush-all

View File

@ -0,0 +1,10 @@
[Unit]
Description=每 10 分钟补投 AgentMail 故障通知
[Timer]
OnBootSec=2min
OnUnitActiveSec=10min
Persistent=true
[Install]
WantedBy=timers.target

View File

@ -0,0 +1,36 @@
[Unit]
Description=AgentMail Gateway - email-driven multi-agent collaboration platform
Documentation=https://github.com/agentmail
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
WorkingDirectory=/opt/agentmail
ExecStart=/opt/agentmail/agentmail-gateway
# 数据库:留空即用内置 SQLite(/opt/agentmail/data/agentmail.db)。
# 要接外部 PostgreSQL 就把 DATABASE_URL 填成 postgres://user:pass@host:5432/db。
Environment=DATABASE_URL=
Environment=AGENTMAIL_DATA_DIR=/opt/agentmail/data
Environment=PORT=8180
# 附件:内容存磁盘(默认在 data/attachments),单个上限 25MB
Environment=AGENTMAIL_MAX_ATTACHMENT_BYTES=26214400
# 首启创建的管理员。密码放在 EnvironmentFile 里,避免出现在 systemctl show 输出。
EnvironmentFile=/etc/agentmail/gateway.env
Restart=always
RestartSec=5
LimitNOFILE=65535
# 只需要读自己的程序目录与写 data/,其余文件系统一律只读
NoNewPrivileges=true
PrivateTmp=true
ProtectSystem=strict
ProtectHome=true
ReadWritePaths=/opt/agentmail/data
[Install]
WantedBy=multi-user.target

View File

@ -0,0 +1,23 @@
# 2026-09-05 systemd 审计添加的 drop-in(原单元文件未改动)。
#
# 为什么加:本机的重启限速器其实是死代码。
# StartLimitBurst=5 + StartLimitIntervalUSec=10s 的含义是「10 秒内启动 5 次
# 就放弃」,但这些单元的 RestartSec 都在 2~10s 之间 —— 5 次重启必然跨过
# 10 秒,计数器每次都在触发前清零。于是一个永久坏掉的服务会以固定频率
# 无限重试,systemd 从头到尾不会说一句话。
#
# 实测后果(2026-09-05 07:54–09:13):llmsproxy 因为 config.yaml 里 headers
# 键重复而无法解析、退出码 1,systemd 每 5 秒拉一次,连拉 79 分钟 ——
# 836 次重启,占掉本次启动 986 条 "Failed with result" 里的 901 条,
# 而 "start request repeated too quickly" 一次都没出现过。
#
# 怎么修:改成指数退避。重试间隔从该单元自己的 RestartSec 出发,经 RestartSteps
# 级递增到 RestartMaxDelaySec,崩溃循环会衰减到每 5 分钟一次。服务不会被放弃
# (病根修好后它仍会自己回来),日志也不再被刷爆。
#
# 故意不动的东西:StartLimitBurst / StartLimitIntervalUSec。收紧它们会让故障单元
# 进入终态 failed、必须人工 systemctl reset-failed 才能复活 —— 这正好和这些
# 长驻 agent 依赖的自愈行为相反。
[Service]
RestartSteps=6
RestartMaxDelaySec=5min

View File

@ -0,0 +1,24 @@
[Unit]
Description=DeepSeek Harness (dsh) Web UI on loopback
After=network-online.target llmsproxy.service
Wants=network-online.target
[Service]
Type=simple
Environment="NODE_ENV=production"
Environment="DEVECO_CLI_CLT_PATH=/opt/huawei/command-line-tools"
Environment="DEVECO_HOME=/opt/huawei/command-line-tools"
Environment="DSH_WEB_FETCH_PROVIDER=http"
Environment=AGENTMAIL_GATEWAY_URL=http://127.0.0.1:8180
Environment=AGENTMAIL_AGENT_NAME=dsh
Environment=AGENTMAIL_AGENT_KEY=ak_dsh_aae41c5a77592c185b29d4bdf2506b43c22e7db0209a63eb
Environment=AGENTMAIL_REPLY_PROVIDER=llmsproxy
Environment=AGENTMAIL_REPLY_MODEL=AUTO
Environment=DEEPSEEK_API_KEY=sk-gw-8100222100bccec24320c1d92d59d63d
ExecStart=/usr/bin/dsh web --host 127.0.0.1 --port 3080 --no-open --trusted-host harness.jianfgit.xyz --trusted-host 192.168.2.60 --trusted-host 192.168.2.60:3080
Restart=on-failure
RestartSec=5
TimeoutStopSec=15
[Install]
WantedBy=multi-user.target

View File

@ -0,0 +1,14 @@
# dsh 启动时把 LAN 转发一起带起来。
#
# 依赖关系分两个方向,两边各管一半:
# dsh-lan.service 里的 PartOf=dsh.service → dsh 停止/重启时 socat 跟着停/重启
# 本文件的 Wants=dsh-lan.service → dsh 启动时 socat 跟着启动
#
# 少了这一半的症状:`systemctl stop dsh && systemctl start dsh` 之后
# dsh 活着、本地 3080 通,但 LAN 与公网全不通 —— socat 没人拉起来。
# 单元自己的 WantedBy=multi-user.target 只在开机时生效,手工 start 不走那条路。
#
# 用 Wants 而不是 Requires:转发层起不来不该阻止 dsh 本身启动,
# 本地 127.0.0.1:3080 仍然可用。
[Unit]
Wants=dsh-lan.service

View File

@ -0,0 +1,7 @@
[Service]
# 桥进程异常退出(SIGKILL / OOM / timeout / watchdog / 非零退出)时发一封邮件。
# 正常 stop/restart 不上报:脚本内 isAbnormalExit 会按 SERVICE_RESULT 提前返回。
# 进程内 uncaughtException 捕获不了 SIGKILL/OOM,所以由 systemd 统一覆盖。
ExecStopPost=-/usr/bin/node /opt/agentmail/bin/service-failure-notify.mjs --report --service dsh.service
# 下次启动补发上次 Gateway 不可达时暂存的报告(per-agent spool,不跨 Agent 混发)
ExecStartPost=-/usr/bin/node /opt/agentmail/bin/service-failure-notify.mjs --flush --service dsh.service

View File

@ -0,0 +1,23 @@
# 2026-09-05 systemd 审计添加的 drop-in(原单元文件未改动)。
#
# 为什么加:本机的重启限速器其实是死代码。
# StartLimitBurst=5 + StartLimitIntervalUSec=10s 的含义是「10 秒内启动 5 次
# 就放弃」,但这些单元的 RestartSec 都在 2~10s 之间 —— 5 次重启必然跨过
# 10 秒,计数器每次都在触发前清零。于是一个永久坏掉的服务会以固定频率
# 无限重试,systemd 从头到尾不会说一句话。
#
# 实测后果(2026-09-05 07:54–09:13):llmsproxy 因为 config.yaml 里 headers
# 键重复而无法解析、退出码 1,systemd 每 5 秒拉一次,连拉 79 分钟 ——
# 836 次重启,占掉本次启动 986 条 "Failed with result" 里的 901 条,
# 而 "start request repeated too quickly" 一次都没出现过。
#
# 怎么修:改成指数退避。重试间隔从该单元自己的 RestartSec 出发,经 RestartSteps
# 级递增到 RestartMaxDelaySec,崩溃循环会衰减到每 5 分钟一次。服务不会被放弃
# (病根修好后它仍会自己回来),日志也不再被刷爆。
#
# 故意不动的东西:StartLimitBurst / StartLimitIntervalUSec。收紧它们会让故障单元
# 进入终态 failed、必须人工 systemctl reset-failed 才能复活 —— 这正好和这些
# 长驻 agent 依赖的自愈行为相反。
[Service]
RestartSteps=6
RestartMaxDelaySec=5min

View File

@ -0,0 +1,16 @@
[Unit]
Description=HomeAgent - 24/7 AI Butler
After=network.target
[Service]
Type=simple
WorkingDirectory=/home/newqqagent
ExecStart=/usr/local/bin/homed -data /home/newqqagent
Restart=always
RestartSec=10
Environment=HOME=/root
Environment=GOMODCACHE=/root/go/pkg/mod
[Install]
WantedBy=multi-user.target

View File

@ -0,0 +1,6 @@
[Service]
EnvironmentFile=/etc/agentmail/homeagent.env
Environment=AGENTMAIL_GATEWAY_URL=http://127.0.0.1:8180
# AgentMail 身份,与插件名(homeagent-mail-bridge)分开:
# 密钥绑定的是这个名字,用插件名注册会被 Gateway 拒(403 密钥已绑定到别的 Agent)。
Environment=AGENTMAIL_AGENT_NAME=homeagent

View File

@ -0,0 +1,2 @@
[Service]
Environment=ONNXRUNTIME_DIR=/opt/onnxruntime

View File

@ -0,0 +1,8 @@
[Service]
# 桥进程异常退出(SIGKILL / OOM / timeout / watchdog / 非零退出)时发一封邮件。
# 正常 stop/restart 不上报:脚本内 isAbnormalExit 会按 SERVICE_RESULT 提前返回。
# 进程内 uncaughtException 捕获不了 SIGKILL/OOM,所以由 systemd 统一覆盖。
Environment=AGENTMAIL_AGENT_NAME=homeagent
ExecStopPost=-/usr/bin/node /opt/agentmail/bin/service-failure-notify.mjs --report --service homeagent.service
# 下次启动补发上次 Gateway 不可达时暂存的报告(per-agent spool,不跨 Agent 混发)
ExecStartPost=-/usr/bin/node /opt/agentmail/bin/service-failure-notify.mjs --flush --service homeagent.service

View File

@ -0,0 +1,23 @@
# 2026-09-05 systemd 审计添加的 drop-in(原单元文件未改动)。
#
# 为什么加:本机的重启限速器其实是死代码。
# StartLimitBurst=5 + StartLimitIntervalUSec=10s 的含义是「10 秒内启动 5 次
# 就放弃」,但这些单元的 RestartSec 都在 2~10s 之间 —— 5 次重启必然跨过
# 10 秒,计数器每次都在触发前清零。于是一个永久坏掉的服务会以固定频率
# 无限重试,systemd 从头到尾不会说一句话。
#
# 实测后果(2026-09-05 07:54–09:13):llmsproxy 因为 config.yaml 里 headers
# 键重复而无法解析、退出码 1,systemd 每 5 秒拉一次,连拉 79 分钟 ——
# 836 次重启,占掉本次启动 986 条 "Failed with result" 里的 901 条,
# 而 "start request repeated too quickly" 一次都没出现过。
#
# 怎么修:改成指数退避。重试间隔从该单元自己的 RestartSec 出发,经 RestartSteps
# 级递增到 RestartMaxDelaySec,崩溃循环会衰减到每 5 分钟一次。服务不会被放弃
# (病根修好后它仍会自己回来),日志也不再被刷爆。
#
# 故意不动的东西:StartLimitBurst / StartLimitIntervalUSec。收紧它们会让故障单元
# 进入终态 failed、必须人工 systemctl reset-failed 才能复活 —— 这正好和这些
# 长驻 agent 依赖的自愈行为相反。
[Service]
RestartSteps=6
RestartMaxDelaySec=5min

View File

@ -0,0 +1,30 @@
[Unit]
Description=opencode headless server (AgentMail mail-bridge host)
After=network-online.target agentmail-gateway.service
Wants=network-online.target
[Service]
Type=simple
WorkingDirectory=/opt/agentmail
ExecStart=/usr/local/bin/opencode serve --port 4097 --hostname 127.0.0.1
# opencode 靠 HOME 定位 ~/.config/opencode(插件列表、provider 配置、认证)。
# systemd 不会自动注入 HOME,不显式给就加载不到 mail-bridge 插件。
Environment=HOME=/root
# mail-bridge 插件凭这组凭证向 Gateway 注册并收发邮件
EnvironmentFile=/etc/agentmail/opencode.env
Restart=always
RestartSec=10
# opencode 惰加载插件:进程启动时不载 plugin,要等第一个 /session 请求。
# 不预热的后果是重启后紧接着到的第一封邮件会丢(mail-bridge 还没订阅 SSE)。
# 失败不能拘到单元本身,所以结尾固定 true。
ExecStartPost=/bin/sh -c 'for i in $(seq 1 30); do \
curl -sf -m 3 -u "opencode:$OPENCODE_SERVER_PASSWORD" http://127.0.0.1:4097/session >/dev/null && exit 0; \
sleep 1; \
done; true'
[Install]
WantedBy=multi-user.target

View File

@ -0,0 +1,7 @@
[Service]
# 桥进程异常退出(SIGKILL / OOM / timeout / watchdog / 非零退出)时发一封邮件。
# 正常 stop/restart 不上报:脚本内 isAbnormalExit 会按 SERVICE_RESULT 提前返回。
# 进程内 uncaughtException 捕获不了 SIGKILL/OOM,所以由 systemd 统一覆盖。
ExecStopPost=-/usr/bin/node /opt/agentmail/bin/service-failure-notify.mjs --report --service opencode-serve.service
# 下次启动补发上次 Gateway 不可达时暂存的报告(per-agent spool,不跨 Agent 混发)
ExecStartPost=-/usr/bin/node /opt/agentmail/bin/service-failure-notify.mjs --flush --service opencode-serve.service

View File

@ -0,0 +1,23 @@
# 2026-09-05 systemd 审计添加的 drop-in(原单元文件未改动)。
#
# 为什么加:本机的重启限速器其实是死代码。
# StartLimitBurst=5 + StartLimitIntervalUSec=10s 的含义是「10 秒内启动 5 次
# 就放弃」,但这些单元的 RestartSec 都在 2~10s 之间 —— 5 次重启必然跨过
# 10 秒,计数器每次都在触发前清零。于是一个永久坏掉的服务会以固定频率
# 无限重试,systemd 从头到尾不会说一句话。
#
# 实测后果(2026-09-05 07:54–09:13):llmsproxy 因为 config.yaml 里 headers
# 键重复而无法解析、退出码 1,systemd 每 5 秒拉一次,连拉 79 分钟 ——
# 836 次重启,占掉本次启动 986 条 "Failed with result" 里的 901 条,
# 而 "start request repeated too quickly" 一次都没出现过。
#
# 怎么修:改成指数退避。重试间隔从该单元自己的 RestartSec 出发,经 RestartSteps
# 级递增到 RestartMaxDelaySec,崩溃循环会衰减到每 5 分钟一次。服务不会被放弃
# (病根修好后它仍会自己回来),日志也不再被刷爆。
#
# 故意不动的东西:StartLimitBurst / StartLimitIntervalUSec。收紧它们会让故障单元
# 进入终态 failed、必须人工 systemctl reset-failed 才能复活 —— 这正好和这些
# 长驻 agent 依赖的自愈行为相反。
[Service]
RestartSteps=6
RestartMaxDelaySec=5min

View File

@ -0,0 +1,56 @@
[Unit]
Description=pi mail-bridge (AgentMail ↔ @earendil-works/pi-coding-agent)
After=network-online.target agentmail-gateway.service
Wants=network-online.target
[Service]
Type=simple
# 桥自己不需要工作目录 —— 每条会话的 cwd 来自邮件寻址的 path 位。
# 但 systemd 要求一个存在的目录,且 pi 的 SettingsManager 会在这里找
# 项目级配置,因此指向仓库而不是 /。
WorkingDirectory=/opt/agentmail/plugins/pi-mail-bridge/current
ExecStart=/usr/bin/node /opt/agentmail/plugins/pi-mail-bridge/current/src/index.mjs
# pi 靠 HOME 定位 ~/.pi/agent(settings.json、auth.json、models.json、sessions/)。
# systemd 不会自动注入 HOME,不显式给就:
# - 读不到 provider 凭证 → 每封邮件都"没有可用模型"
# - 会话落到 /.pi 或直接失败 → C-8 的会话快照永远是空的
Environment=HOME=/root
# pi 的全局扩展 pi-a2a / pi-acp 绑死 127.0.0.1:12010 / 12011。
# 桥用 noExtensions:true 起会话,本进程不会去 bind 那两个端口;
# 这两个变量是给「同机还跑着 pi CLI」的情况留的隔离位,
# 万一将来放开扩展加载,端口也不会和交互式 pi 撞。
Environment=PI_A2A_PORT=13010
Environment=PI_ACP_PORT=13011
EnvironmentFile=/etc/agentmail/pi.env
Restart=always
RestartSec=10
# 桥用 O_EXCL 建 <AGENTMAIL_CONFIG_DIR>/pi-bridge.lock 保证单实例
# (两个实例各自订阅 SSE 会让同一封邮件各起一条会话,发件人收到两封回信,
# 而去重集合在各自内存里、彼此看不到)。
#
# 被 SIGKILL 或主机掉电时锁文件留在盘上 —— 桥自己会检查锁里的 pid 是否还活着,
# 但那要求 /proc 可读且 pid 未被复用。systemd 重启前清一次更直接:
# ExecStartPre 里的 pid 必然已经死了(systemd 确认进程退出后才重启)。
#
# 用 `-` 前缀:文件不存在时 rm 失败不该拦住启动。
ExecStartPre=-/bin/rm -f /root/.agentmail-pi/pi-bridge.lock
# 桥是长驻守护进程,启动即注册 + 立刻打一次心跳 + 订阅 SSE(B-1),
# 不像 opencode 那样惰加载,因此不需要 ExecStartPost 预热。
# pi 会话在内存里持有整条对话,长跑之后常驻几百 MB。给一个上限让它被
# OOM killer 挑中而不是拖垮整机;Restart=always 会把它拉回来。
MemoryMax=4G
# 优雅停机:桥收到 SIGTERM 后要注销 SSE、清锁文件。
# 默认 90 秒太长(停机时人在等),10 秒足够那两件事。
TimeoutStopSec=10
[Install]
WantedBy=multi-user.target

View File

@ -0,0 +1,7 @@
[Service]
# 桥进程异常退出(SIGKILL / OOM / timeout / watchdog / 非零退出)时发一封邮件。
# 正常 stop/restart 不上报:脚本内 isAbnormalExit 会按 SERVICE_RESULT 提前返回。
# 进程内 uncaughtException 捕获不了 SIGKILL/OOM,所以由 systemd 统一覆盖。
ExecStopPost=-/usr/bin/node /opt/agentmail/bin/service-failure-notify.mjs --report --service pi-mail-bridge.service
# 下次启动补发上次 Gateway 不可达时暂存的报告(per-agent spool,不跨 Agent 混发)
ExecStartPost=-/usr/bin/node /opt/agentmail/bin/service-failure-notify.mjs --flush --service pi-mail-bridge.service

View File

@ -0,0 +1,23 @@
# 2026-09-05 systemd 审计添加的 drop-in(原单元文件未改动)。
#
# 为什么加:本机的重启限速器其实是死代码。
# StartLimitBurst=5 + StartLimitIntervalUSec=10s 的含义是「10 秒内启动 5 次
# 就放弃」,但这些单元的 RestartSec 都在 2~10s 之间 —— 5 次重启必然跨过
# 10 秒,计数器每次都在触发前清零。于是一个永久坏掉的服务会以固定频率
# 无限重试,systemd 从头到尾不会说一句话。
#
# 实测后果(2026-09-05 07:54–09:13):llmsproxy 因为 config.yaml 里 headers
# 键重复而无法解析、退出码 1,systemd 每 5 秒拉一次,连拉 79 分钟 ——
# 836 次重启,占掉本次启动 986 条 "Failed with result" 里的 901 条,
# 而 "start request repeated too quickly" 一次都没出现过。
#
# 怎么修:改成指数退避。重试间隔从该单元自己的 RestartSec 出发,经 RestartSteps
# 级递增到 RestartMaxDelaySec,崩溃循环会衰减到每 5 分钟一次。服务不会被放弃
# (病根修好后它仍会自己回来),日志也不再被刷爆。
#
# 故意不动的东西:StartLimitBurst / StartLimitIntervalUSec。收紧它们会让故障单元
# 进入终态 failed、必须人工 systemctl reset-failed 才能复活 —— 这正好和这些
# 长驻 agent 依赖的自愈行为相反。
[Service]
RestartSteps=6
RestartMaxDelaySec=5min

View File

@ -0,0 +1,45 @@
[Unit]
Description=zcode mail-bridge (AgentMail ↔ ZCode headless CLI)
Documentation=file:/opt/agentmail/plugins/zcode-mail-bridge/README.md
After=network-online.target agentmail-gateway.service
Wants=network-online.target
[Service]
Type=simple
# 驱动自己不需要工作目录(每一轮 cwd 来自邮件寻址的 path 位),
# 但 systemd 要求一个存在的目录,且相对路径(`../hooks/hooks.json` 的自检)
# 以进程启动位置为准。
# 生产跑**快照**,不跑仓库工作区:仓库会被我们随手编辑,而邮件驱动的服务
# 重启后会直接跑起当前 HEAD —— 一次未完成的修改就变成线上行为。
# 与 pi / opencode / dsh 同一条约定,切换靠 deploy/redeploy-plugin.sh 的原子软链。
WorkingDirectory=/opt/agentmail/plugins/zcode-mail-bridge/current
ExecStart=/usr/bin/node /opt/agentmail/plugins/zcode-mail-bridge/current/src/index.mjs
# ZCode 靠 HOME 定位 ~/.zcode(OAuth 凭据、cli/config.json、会话库、日志)。
# systemd 不会自动注入 HOME,不显式给就:
# - 读不到登录凭据 → 每一轮都报「Model config is missing」,全部回成失败信
# - 插件发现落到 /.zcode → 空目录,MCP 工具一个都没有
Environment=HOME=/root
EnvironmentFile=/etc/agentmail/zcode.env
Restart=always
RestartSec=10
# 异常退出邮件上报:进程内的钩子捕获不了 SIGKILL/OOM,只能由 systemd 覆盖。
# 正常 stop/restart 不上报(脚本内 isAbnormalExit 提前返回)。
ExecStopPost=-/usr/bin/node /opt/agentmail/bin/service-failure-notify.mjs --report --service zcode-mail-bridge.service
# 补发上次网关不可达时暂存的报告(per-agent spool)
ExecStartPost=-/usr/bin/node /opt/agentmail/bin/service-failure-notify.mjs --flush --service zcode-mail-bridge.service
# 一轮 ZCode 会派生一个 node 进程(模型 + 工具),内存占用比另三个桥高。
# 给一个上限让它被 OOM killer 挑中而不是拖垮整机;Restart=always 会拉回来。
MemoryMax=4G
# 停机:驱动收到 SIGTERM 后会终止在途回合(否则会留下跑工具的孤儿),
# 再关掉 SSE。默认 90 秒太长,15 秒够。
TimeoutStopSec=15
[Install]
WantedBy=multi-user.target

View File

@ -0,0 +1,48 @@
[Unit]
Description=ZCode headless (Xvfb 虚拟屏) —— 供 Web 远控 / 官方中转访问
Documentation=https://zcode.z.ai
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
# 为什么用虚拟屏而不是 --headless:ZCode 的 Web 远控(web-remote-control)是
# **桌面端发起**的功能(界面上有 copyLink / 二维码,"Try again from desktop")。
# 它没有对应的命令行开关 —— 我一开始把 `--web-remote-navigation-height`(CSS 变量)
# 与 `--remote-debugging-port`(Chrome)误读成 ZCode 的开关了。
# 所以要让远控可用,就必须真的把桌面应用跑起来,而无头服务器上没有显示 →
# 用 Xvfb 提供一个。
#
# --no-sandbox:Electron 以 root 运行时拒绝启用沙箱(会 FATAL 退出)。
# 这是本机的既有取舍(其它服务也以 root 跑),但要知道它降低了渲染进程的隔离。
#
# --remote-debugging-port:暴露 CDP,用于程序化驱动界面(登录、开启远控、
# 截图核对状态),不必依赖 xdotool 之类的坐标点击。
#
# 为什么套一层 dbus-run-session:Electron 的**单实例 / 深链转发**依赖会话总线,
# 而下面把 DBUS_SESSION_BUS_ADDRESS 设成了 disabled:(无会话总线时的既有噪声)。
# 这里用 dbus-run-session 给本服务单独起一个会话总线 —— 只为该子进程提供,
# **不动系统总线配置**。
#
# 启动命令只在此处定义一次(唯一的 ExecStart)。需要改启动方式就改这里,
# 不要在 drop-in 里整条重写 —— drop-in 重写会**静默盖住**本行,
# 导致这里的改动不生效(历史上已因此出现过崩溃循环)。
ExecStart=/usr/bin/dbus-run-session -- /usr/bin/xvfb-run -a -s "-screen 0 1600x1000x24" /opt/ZCode/zcode --no-sandbox --disable-gpu --remote-debugging-port=9333
Environment=HOME=/root
# 把 xdg-open 包装放到 PATH 最前:用于抓取 shell.openExternal 的目标 URL
# (无头机上没有浏览器,否则这个信息会丢失,登录链接就拿不到)
Environment=PATH=/root/gotmp/zcodewrap:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
Environment=ELECTRON_DISABLE_SECURITY_WARNINGS=1
# 无会话总线时的既有噪声(dbus 报错),不影响功能,不因此改系统总线配置
Environment=DBUS_SESSION_BUS_ADDRESS=disabled:
Restart=always
RestartSec=5
# 崩溃可见性:与 AgentMail 其它宿主同款约定(失败要有人知道,不能静默)
ExecStopPost=-/usr/bin/node /opt/agentmail/bin/service-failure-notify.mjs --report --service zcode.service
ExecStartPost=-/usr/bin/node /opt/agentmail/bin/service-failure-notify.mjs --flush --service zcode.service
[Install]
WantedBy=multi-user.target

View File

@ -0,0 +1,14 @@
[Service]
# 清掉主单元的 DBUS_SESSION_BUS_ADDRESS=disabled(Electron 单实例 / 深链转发依赖 DBus)。
#
# 本文件**只覆盖这一个环境变量**。启动命令(ExecStart,已含 dbus-run-session 包装)
# 由主单元 /etc/systemd/system/zcode.service 唯一定义。
#
# 历史教训:本文件曾经写成 `ExecStart=` 清空 + 整条重写来加 dbus-run-session 包装。
# 那样做的两个问题:
# 1. 它**静默盖住**主单元的 ExecStart —— 以后改主单元的启动参数不会生效,
# 且看 `systemctl cat` 才能发现(排查时极易误判)
# 2. 主单元那条 ExecStart 原本是「反斜杠续行」的两行,抄过来时续行被压平成空格,
# 一度出现过 `xvfb-run: 200: 0: not found` 的崩溃循环
# 所以包装归主单元,drop-in 只管它名字里那件事(dbus 环境变量)。
Environment=DBUS_SESSION_BUS_ADDRESS=