Files
MailUI4Agents/deploy/install.sh
JianFeeeee 3be824849d fix(deploy): env-defaults 的顺序错(③ 的探测依赖 ④ 的产物)+ 非 root 的 HOME 陷阱 + 兜底行可见 + 补 root 断言
pi 读了新加的 `deploy/lib/env-defaults.sh`(三个 source 点他都确认对),指出三条,逐一实测后处理:

**1. 顺序错(真错,而且正好落在它自己要消的那类假设上)**
③ 用 `command -v go` 探测,而 `command -v` **走 PATH**;④ 才修 PATH ⇒
**在 ④ 要修的那个环境里(PATH 为空),③ 的探测必然失败**,紧接着 ④ 把 PATH 装上、
后面的步骤**又能**找到 go —— **探测结论与实际可用性相反**。
实测(`env -i`):顺序翻转前 `go` 在 ③ 处找不到、④ 之后 `/usr/bin/go` 就在了。
已把 PATH 那段**挪到最前**,并在文件头写明顺序是**正确性而不是风格**。
(pi 自己也说了严重度:今天对 go 大概率无影响,因为 ① 已兜住 HOME、现代 go 会从 `$HOME/go`
自推缓存 —— 他把它当形状问题提,这个判断我认;一条探测所依赖的东西正是同文件后面要修的东西,
这正是本文件存在的理由。)

**2. `HOME=/root` 在非 root 调用者手里会把环境问题变成代码问题**
`redeploy-gateway.sh` 原先没有 EUID 断言(只有 `install.sh` 有),于是"非 root + 空 HOME"
会拿到 `HOME=/root`,接着 `go build`/`npm ci` 往 `/root/go`、`/root/.npm` 写 ⇒ **EACCES**,
而那串报错看起来是代码/工程问题 —— 正是本文件要消的东西。
已按身份分叉 + **验证可写**(判据落在"能不能写",不落在"路径长得像不像"),
兜底落到 `${TMPDIR:-/tmp}/agentmail-home-$(id -u)`;连一处可写的都找不到 ⇒ exit 2 + 人话。

★ **顺着他的思路又实测出第二个口子**:`HOME` **已给**但不可写时,上面只判"未设"就放行 ——
后果与空 HOME 完全相同,只是入参不同(`sudo -E`、从 root shell 继承、容器挂错)。
`mkdir -p` 对**已存在的不可写目录会返回成功**,所以必须单独判 `-w`。
实测 `setpriv --reuid=65534 env -i HOME=/root` ⇒ `touch $HOME/probe` 被拒。
已覆盖"已给但不可写",并**先说清再改判**(`[WARN] 调用者给的 HOME=… 不可写;改判到 …`),
不静默换目录 —— 静默换会让"东西写到哪去了"变成谜。
五种场景实测(全空 / 环境齐 / 非 root+空 / 非 root+不可写 HOME / root+可写):全部符合预期。

**3. "某条脚本忘了 source"没有信号** ⇒ 采纳
`AGENTMAIL_ENV_DEFAULTS` 只是被 export、值不进正常输出 ⇒ 谁把 `source` 删了,
输出与"环境本来就齐"**完全同形**(又是"看起来在兜、其实没兜")。
新增 `agentmail_env_report()`,三个脚本各自打一行(兜了哪些 / "(无 —— 调用者已提供全部)";
忘了 source 就没有这一行)。实测三个脚本在空环境下各自都打出来了 ——
这也把验收从"一条脚本"变成"三条各自可读"。

**4. 顺带补 `redeploy-gateway.sh` 的 root 断言**
它要往 `$PREFIX`(默认 /opt/agentmail)写,非 root 必然失败在写权限上,
而报错来自 `install`/`cp`、看起来像工程问题。用退出码 **2**(环境/权限),口径与 env-defaults 一致。
实测非 root 下:`[FAIL] 环境不足:本脚本要写 /opt/agentmail,需要 root。` 退出码 2。

验证:install.sh --check 空环境 exit 0、正常环境 exit 0;npm test exit 0;
prune 自检 22/22;drift 自检 35/0;check-shared-libs exit 0;四个脚本 bash -n 通过。
2026-09-14 21:01:19 +08:00

358 lines
18 KiB
Bash
Executable File
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env bash
#
# 把 AgentMail Gateway 与各平台的 mail-bridge 安装为 systemd 服务。
#
# sudo ./deploy/install.sh
#
# 幂等:重复执行等价于「重新构建 + 重启」。已存在的 env 文件不会被覆盖,
# 因为里面有管理员密码与 Agent secret,重装不该把它们冲掉。
set -euo pipefail
REPO="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
# 环境自足:一处给全(HOME/TMPDIR/GOMODCACHE/PATH),不在每条命令旁各写预检。
# 理由与四次历史见该文件的头注释 —— pi 的原话是"否则第五次一定会来"。
# shellcheck source=./lib/env-defaults.sh
. "$REPO/deploy/lib/env-defaults.sh"
agentmail_env_report
# 与 `redeploy-gateway.sh` / `reset-demo.sh` 同源(pi 评审 2026-09-14 对出来的):
# 那两处都写 `${AGENTMAIL_PREFIX:-/opt/agentmail}`,只有这里写死 ——
# 谁设了那个变量,install 装到 A、redeploy 和 drift-check 看 B,三个工具各说各话。
PREFIX="${AGENTMAIL_PREFIX:-/opt/agentmail}"
ETC=/etc/agentmail
#
# ./deploy/install.sh --check # 干跑:只跑所有门禁与前置检查,**不写系统目录**
#
# 为什么要有 --check(pi 2026-09-14):发布路径是**最少被跑的门** ——
# `check-shared-libs.sh` 一直红着没人发现,正是因为它只挂在 install.sh 里。
# 而"最后一个能跑这一步的人"(有 root 的那个人)每次都在**改完一堆东西之后**才第一次跑它,
# 于是第一次失败就发生在最贵的时候。干跑把"门自己跑得动吗"提前暴露出来。
CHECK_ONLY=0
ALLOW_DIRTY=0
ALLOW_STALE=0
for a in "$@"; do
case "$a" in
--check) CHECK_ONLY=1 ;;
--allow-dirty) ALLOW_DIRTY=1 ;;
--allow-stale) ALLOW_STALE=1 ;;
-h|--help) sed -n '2,20p' "$0"; exit 0 ;;
*) echo "未知参数:$a(支持 --check / --allow-dirty / --allow-stale)" >&2; exit 2 ;;
esac
done
if [[ $CHECK_ONLY -eq 0 ]]; then
[[ $EUID -eq 0 ]] || { echo "需要 root:sudo $0(只验证不安装用 ./deploy/install.sh --check)" >&2; exit 1; }
else
echo "==> 干跑模式(--check):不会写 $PREFIX 或 $ETC"
# 写权限是这一步唯一的硬门槛,先说清楚 —— 下一个拿到权限的人要一眼看到需要什么
for d in "$PREFIX" "$ETC"; do
if [[ -e "$d" && ! -w "$d" ]]; then
echo " 注意:$d 存在但当前用户不可写(正式安装需要 root/sudo 或先放行该目录)"
fi
done
fi
echo "==> 构建前端"
# 干跑不装依赖(装依赖要写 node_modules/缓存,干跑的意义是"门能不能跑",不是"装得上装不上")。
if [[ $CHECK_ONLY -eq 1 ]]; then
[[ -d "$REPO/client/electron/node_modules" ]] || {
echo " 干跑需要一个已装好依赖的工作树(client/electron/node_modules 不存在)" >&2; exit 1; }
echo " 跳过 npm ci/install(干跑不写依赖);用的是当前 node_modules"
else
( cd "$REPO/client/electron" && npm ci --no-audit --no-fund 2>/dev/null || npm install --no-audit --no-fund )
fi
# 相位(pi 2026-09-14 裁定):部署门禁问的是"**别人已经产出的产物**能不能装到这台机器上",
# 不是"产物是不是从当前源码新鲜构建的"。后者属于构建相位(`packaging`/`build-stamp`),
# 挂在这里会**恒红**(部署路径不重新打包)—— 门挂错了相位。所以这里显式声明相位。
( cd "$REPO/client/electron" && npm run typecheck && AGENTMAIL_CRITERIA_PHASE=install npm test && npm run build )
# ── 安装相位真正能判的那一半:**读产物自证**(不是重算 dist)──
# 同一个不变量("这份产物是谁")在两个相位要两种判据:构建相位重新计算,安装相位读它自己说的。
if [[ -f "$REPO/client/electron/dist/BUILD_INFO.json" ]]; then
echo "==> 产物自证(安装相位)"
read -r A_REV A_DIRTY A_RC < <(node -e '
const d = require("'"$REPO"'/client/electron/dist/BUILD_INFO.json");
console.log([d.gitRev ?? "?", d.gitDirty ? "dirty" : "clean", d.releaseCandidate === undefined ? "?" : String(d.releaseCandidate)].join(" "));
')
HEAD_REV="$(git -C "$REPO" rev-parse --short HEAD)"
echo " 产物:gitRev=$A_REV 树=$A_DIRTY releaseCandidate=$A_RC | 当前 HEAD=$HEAD_REV"
refuse=0
if [[ "$A_RC" == "false" ]]; then
echo " ✗ 这个包**不是发布候选**(构建时工作树是脏的)—— 它可能含着别人未提交的半成品。" >&2
refuse=1
fi
if [[ "$A_REV" != "$HEAD_REV" ]]; then
echo " ✗ 这个包比源码旧:产物 gitRev=$A_REV ≠ HEAD=$HEAD_REV。" >&2
refuse=1
fi
if [[ $refuse -eq 1 ]]; then
[[ $A_RC == "false" && $ALLOW_DIRTY -eq 1 ]] || [[ $A_REV != "$HEAD_REV" && $ALLOW_STALE -eq 1 ]] || true
if [[ $CHECK_ONLY -eq 1 ]]; then
echo " (干跑:正式安装会被拒绝。要放行就显式说清:--allow-dirty / --allow-stale)" >&2
elif { [[ "$A_RC" != "false" || $ALLOW_DIRTY -eq 1 ]] && [[ "$A_REV" == "$HEAD_REV" || $ALLOW_STALE -eq 1 ]]; }; then
echo " (你显式放行了:--allow-dirty/--allow-stale —— 风险由你承担)" >&2
else
echo " 拒绝安装。放行要显式:sudo $0 --allow-dirty --allow-stale" >&2
exit 1
fi
fi
else
echo "==> 产物自证:dist/BUILD_INFO.json 不存在(还没构建过)—— 本相位只能报"不知道",不能报"没问题"" >&2
fi
echo "==> 校验插件共用模块同源"
# lib/ 下的纯函数模块在三个插件里逐字节相同(见 docs/PLUGIN-CONTRACT.md 第六节)。
# 一侧改了另一侧没改,几个平台的行为就会悄悄分叉。
"$REPO/deploy/check-shared-libs.sh"
# 插件的纯函数测试(自动转发去重等)。
# 插件不参与构建产物,但它的逆行为会直接变成用户收件箱里的重复邮件,
# 因此也纳入部署前的门禁。zod 已在 node_modules 里就不重装。
if [[ -d "$REPO/plugins/opencode-mail-bridge/node_modules/zod" ]]; then
( cd "$REPO/plugins/opencode-mail-bridge" && npm test )
else
( cd "$REPO/plugins/opencode-mail-bridge" && npm install --no-audit --no-fund && npm test )
fi
# DSH 插件:先构建再跑测试。
#
# 它是 TypeScript 写的,package.json 的 main 指向 dist/index.js,而 dist/ 不进版本库
# (与 client/electron/dist 同理)—— 新克隆里不先 tsc,DSH 加载插件时会直接找不到入口。
# 测试本身只碰 lib/ 下的纯函数(不依赖 dist),但先构建能把类型错误也当成门禁。
#
# 盖住的三类约定都是「错了不当场报错、只在深处炸一个无关错误」:
# followup 消息形状、工作目录解析、会话快照的 subagent 过滤。
if [[ -d "$REPO/plugins/dsh-mail-bridge/node_modules/typescript" ]]; then
( cd "$REPO/plugins/dsh-mail-bridge" && npx tsc && npm test )
else
( cd "$REPO/plugins/dsh-mail-bridge" \
&& npm install --no-audit --no-fund && npx tsc && npm test )
fi
# pi 插件:纯 ESM,无构建步骤,唯一的外部依赖是全局装的 pi SDK。
#
# 它不 npm install:@earendil-works/pi-coding-agent 是全局包(peerDependency),
# 装在项目里会得到第二份 SDK,两份各自维护 ~/.pi/agent 的会话索引缓存。
# 因此这里只软链一次;SDK 不在时跳过测试(turn/naming 是纯函数,
# 但 lib 的测试也一起跑,没必要为缺 SDK 的机器留半套门禁)。
PI_SDK=/usr/lib/node_modules/@earendil-works/pi-coding-agent
if [[ -d "$PI_SDK" ]]; then
install -d "$REPO/plugins/pi-mail-bridge/node_modules/@earendil-works"
ln -sfn "$PI_SDK" "$REPO/plugins/pi-mail-bridge/node_modules/@earendil-works/pi-coding-agent"
( cd "$REPO/plugins/pi-mail-bridge" && npm test )
else
echo " 未找到 pi SDK($PI_SDK),跳过 pi 插件测试"
fi
# pi 邮件工具扩展:给**交互式**会话装上 send_mail / read_inbox。
#
# 守护进程(pi-mail-bridge.service)用 noExtensions:true 起会话,它的邮件工具
# 只给模型在邮件会话里用;人在 TUI 里敲的 pi 拿不到。结果是平台的建设者自己
# 收不到邮件,只能绕到 curl + 密钥直连 Gateway。这个扩展补上那一侧 ——
# TUI 与邮箱是同一条 AgentMail 身份(agent pi)的两个入口。
#
# 为什么不会重复注册:noExtensions:true 只加载 CLI `-e` 传的扩展,
# settings.json 的 extensions 数组会被排除(见 SDK resource-loader.js),
# 因此守护进程的 worker 里零扩展、本扩展只作用于交互式会话。
if [[ -d "$PI_SDK" ]]; then
# `set -u` 下 `$HOME` 未设会直接杀掉部署(实测:`HOME: unbound variable`,
# 而它发生在所有门禁跑完之后 —— 最贵的位置)。cron / env -i / 某些 sudo 配置下
# HOME 就是没有的,所以给一个兜底。同一形状我这轮在 homeagent 的 build.sh 里
# 也修过一次(cron 里 HOME 未设 → Go 拒绝工作)——**这是这轮第三次**,
# 说明"环境变量在部署路径上不存在"这条要当成常态而不是意外。
PI_HOME="${SUDO_USER:+$(getent passwd "$SUDO_USER" | cut -d: -f6)}"
PI_HOME="${PI_HOME:-${HOME:-/root}}"
PI_SETTINGS="$PI_HOME/.pi/agent/settings.json"
EXT_PATH="$REPO/plugins/pi-mail-bridge/extension/index.ts"
if [[ $CHECK_ONLY -eq 1 ]]; then
echo " [干跑] 跳过:把邮件工具扩展注册进 $PI_SETTINGS(要写工作区外的 home 目录)"
elif [[ -d "$PI_HOME/.pi/agent" || -f "$PI_SETTINGS" ]]; then
install -d "$(dirname "$PI_SETTINGS")"
node -e '
const fs = require("fs");
const [path, target] = process.argv.slice(1);
let cfg = {};
try { cfg = JSON.parse(fs.readFileSync(path, "utf8")); } catch { /* 新建 */ }
const list = Array.isArray(cfg.extensions) ? cfg.extensions : [];
if (list.includes(target)) { console.log(" 已注册,幂等跳过"); process.exit(0); }
list.push(target);
cfg.extensions = list;
fs.writeFileSync(path, JSON.stringify(cfg, null, 2) + "\n");
console.log(" 已注册 " + target);
' "$PI_SETTINGS" "$EXT_PATH"
else
echo " 未找到 $PI_HOME/.pi/agent,跳过(手动在 settings.json 加 extensions: [\"$EXT_PATH\"])"
fi
fi
if [[ $CHECK_ONLY -eq 1 ]]; then
# ★ `go vet`/`go test` 提到这个相位来跑(pi 评审 2026-09-14)。
#
# 原先的措辞是"**所有会红的门禁都跑过了**(前端 typecheck/test/build、共用模块同源、
# 各插件测试、插件构建)",而紧接着的一段就把 `go vet + go test` 列进"干跑没有执行"
# —— **那两条正是会红的门禁**,两段话自相矛盾,而读者只会读那句加粗的结论。
# 更实际的理由:这两道门恰恰最容易在**别人的机器上**红(Go 版本、依赖、平台),
# 而"第一个拿到 root 的人第一次跑门禁"正是 `--check` 要解决的场景。
# 它们不写系统目录(只写 go 缓存),所以放进这个相位没有副作用。
echo "==> 校验 Gateway 源码(go vet + go test)"
# 环境不再在这里逐个预检 —— 开头 source 的 env-defaults.sh 已经保证
# HOME/TMPDIR/GOMODCACHE/PATH 齐备(那道预检是"第四次"的补丁,已被一处取代)。
if ! ( cd "$REPO/server" && go vet ./... && go test ./... ); then
echo " [FAIL] go vet / go test 不过 —— 先修好再安装" >&2
exit 1
fi
echo " [ OK ] go vet + go test 通过"
cat <<'EOF'
==> 干跑结束:**除了"构建 Gateway 二进制"之外,所有会红的门禁都跑过了**
(前端 typecheck/test/build、共用模块同源、各插件测试、插件构建、
**go vet + go test**)。
干跑的成功只能说明"门是好的",不能说明"装得上"。
下面这些步骤干跑**没有执行**(它们要写工作区外的目录,正式安装需要 root 或先放行):
· 前端产物 → server/internal/static/static/
· 构建 Gateway(go build -trimpath -o server/agentmail-gateway;vet/test 上面已跑)
· 生成 /etc/agentmail/*.env(已存在的不覆盖)+ chmod 0600
· 装 systemd unit → /etc/systemd/system/,daemon-reload + enable --now
· 把邮件工具扩展注册进 ~/.pi/agent/settings.json
拿不准就先跑 ./deploy/install.sh --check;正式安装用 sudo ./deploy/install.sh。
EOF
exit 0
fi
echo "==> 前端产物嵌入 Gateway"
# 只清构建产物,不能 rm -rf 整个目录:
# placeholder.html 在版本库里(让 go:embed 在新克隆里能编译),
# 删掉它会让 git 看到一个本地删除,下一次 commit -a 就把它从仓库里带走了。
rm -rf "$REPO/server/internal/static/static/assets"
rm -f "$REPO/server/internal/static/static/index.html"
cp -r "$REPO/client/electron/dist/." "$REPO/server/internal/static/static/"
echo "==> 构建 Gateway(单二进制,内含前端 + SQLite)"
# 先删再建:go build -o 到已存在的路径时可能拿到 stale 二进制(此坑中过多次)
# `-trimpath` 与 redeploy-gateway.sh 同源:不带它时 Go 会把源文件绝对路径编进
# 生产二进制(2026-09-14 实测 57 处 /home/program/agentmail/…)。
rm -f "$REPO/server/agentmail-gateway"
( cd "$REPO/server" && go vet ./... && go test ./... && go build -trimpath -o "$REPO/server/agentmail-gateway" ./cmd/server )
echo "==> 安装到 $PREFIX"
install -d "$PREFIX" "$PREFIX/data" "$ETC"
install -m 0755 "$REPO/server/agentmail-gateway" "$PREFIX/agentmail-gateway"
# ---- env 文件:仅在缺失时生成,密码随机 ----
if [[ ! -f "$ETC/gateway.env" ]]; then
ADMIN_PASS="$(head -c 18 /dev/urandom | base64 | tr -d '/+=' | head -c 20)"
cat > "$ETC/gateway.env" <<EOF
# AgentMail Gateway 环境变量
ADMIN_USER=admin
ADMIN_PASSWORD=$ADMIN_PASS
# 生产环境走 HTTPS 时置 true,Cookie 才会带 Secure 标记
SECURE_COOKIE=false
# 允许的前端跨域来源(逗号分隔);单二进制自带前端时通常无需配置
# CORS_ORIGINS=https://mail.example.com
EOF
chmod 0600 "$ETC/gateway.env"
echo " 已生成 $ETC/gateway.env(管理员 admin / $ADMIN_PASS)"
else
echo " $ETC/gateway.env 已存在,保留不动"
fi
if [[ ! -f "$ETC/opencode.env" ]]; then
cat > "$ETC/opencode.env" <<EOF
# opencode mail-bridge 插件配置
AGENTMAIL_GATEWAY_URL=http://127.0.0.1:8180
AGENTMAIL_AGENT_NAME=opencode
# 接入密钥:留空时插件会在 \$AGENTMAIL_CONFIG_DIR/agent.key 本地生成一把并打印到日志,
# 拿着它到 Web 后台「Agent 密钥」登记即可接入(journalctl -u opencode-serve | grep mail-bridge)。
# 也可以先在后台签发密钥,再把它填在这里。
AGENTMAIL_AGENT_KEY=
AGENTMAIL_CONFIG_DIR="$PREFIX/agent-config"
# 处理来信时使用的模型
AGENTMAIL_REPLY_PROVIDER=llmsproxy
AGENTMAIL_REPLY_MODEL=AUTO
# opencode serve 绑在 127.0.0.1,但同机任何进程都能调它开会话,
# 因此仍然设置访问密码。
OPENCODE_SERVER_PASSWORD=$(head -c 18 /dev/urandom | base64 | tr -d '/+=' | head -c 24)
EOF
chmod 0600 "$ETC/opencode.env"
install -d -m 0700 "$PREFIX/agent-config"
echo " 已生成 $ETC/opencode.env(server password 为随机值;接入密钥首启时本地生成)"
else
echo " $ETC/opencode.env 已存在,保留不动"
fi
if [[ ! -f "$ETC/pi.env" ]]; then
cat > "$ETC/pi.env" <<EOF
# pi mail-bridge 插件配置
AGENTMAIL_GATEWAY_URL=http://127.0.0.1:8180
AGENTMAIL_AGENT_NAME=pi
# 接入密钥:留空时插件会在 \$AGENTMAIL_CONFIG_DIR/agent.key 本地生成一把并打印到日志,
# 拿着它到 Web 后台「Agent 密钥」登记即可接入(journalctl -u pi-mail-bridge | grep 密钥)。
AGENTMAIL_AGENT_KEY=
AGENTMAIL_CONFIG_DIR="$PREFIX/pi-config"
# 处理来信时首选的模型。留空表示用 pi 自己的默认(~/.pi/agent/settings.json)。
# 管理员在后台划定模型范围时,范围优先于这里的设置(B-2.2)。
AGENTMAIL_REPLY_PROVIDER=llmsproxy
AGENTMAIL_REPLY_MODEL=AUTO
# 单轮上限。超时**不算失败**:pi 仍在后台跑,转发挂在 agent_end 上,
# 说完了自然会转出去。这个值只决定「多久之后不再阻塞投递流水线」。
AGENTMAIL_TURN_TIMEOUT_MS=60000
EOF
chmod 0600 "$ETC/pi.env"
install -d -m 0700 "$PREFIX/pi-config"
echo " 已生成 $ETC/pi.env(接入密钥首启时本地生成)"
else
echo " $ETC/pi.env 已存在,保留不动"
fi
echo "==> 安装 systemd 单元"
# 单元与 drop-in 的**唯一真相**在 deploy/systemd/(镜像 systemd 的目录结构)。
#
# 以前这里逐个 install 三个 .service,而 drop-in(失败通知/重启退避等)只存在于
# /etc 里 —— 于是"仓库里的是旧的、机器上的是新的",谁跑一次本脚本就把部署退回
# 源码目录(旧版 ExecStart 指向 /home/program/agentmail/plugins/...)。
install -d "$PREFIX/bin"
install -m 0755 "$REPO/deploy/service-failure-notify.mjs" "$PREFIX/bin/service-failure-notify.mjs"
while IFS= read -r f; do
rel="${f#"$REPO/deploy/systemd/"}"
install -D -m 0644 "$f" "/etc/systemd/system/$rel"
done < <(find "$REPO/deploy/systemd" -type f)
systemctl daemon-reload
echo "==> 启用并启动"
systemctl enable --now agentmail-gateway.service
if command -v opencode >/dev/null 2>&1; then
systemctl enable --now opencode-serve.service
else
echo " 未找到 opencode,跳过 opencode-serve(装好后执行:systemctl enable --now opencode-serve)"
fi
if [[ -d "$PI_SDK" ]]; then
systemctl enable --now pi-mail-bridge.service
else
echo " 未找到 pi SDK,跳过 pi-mail-bridge(装好后执行:systemctl enable --now pi-mail-bridge)"
fi
sleep 3
echo
echo "==> 状态"
systemctl --no-pager --lines=0 status agentmail-gateway.service || true
curl -sf -m 5 http://127.0.0.1:8180/health && echo " 健康检查通过" || echo " 健康检查失败,查看:journalctl -u agentmail-gateway -n 50"
# ─────────────────────────────────────────────────────────────────
# 日常只改后端时不需要重跑本脚本(它会重装 npm 依赖、重写 systemd 单元、
# 重新生成 env)。换二进制走:
#
# bash deploy/redeploy-gateway.sh # 含测试与后置验证
# bash deploy/redeploy-gateway.sh --dry-run # 先看要做什么
#
# 那个脚本用 sqlite3 .backup 备份数据库、install -m 0755 原子替换二进制,
# 并在验证不过时自动回滚。