#!/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)" PREFIX=/opt/agentmail ETC=/etc/agentmail [[ $EUID -eq 0 ]] || { echo "需要 root:sudo $0" >&2; exit 1; } echo "==> 构建前端" ( cd "$REPO/web" && npm ci --no-audit --no-fund 2>/dev/null || npm install --no-audit --no-fund ) ( cd "$REPO/web" && npm run typecheck && npm test && npm run build ) 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/ 不进版本库 # (与 web/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 echo "==> 前端产物嵌入 Gateway" # 只清构建产物,不能 rm -rf 整个目录: # placeholder.html 在版本库里(让 go:embed 在新克隆里能编译), # 删掉它会让 git 看到一个本地删除,下一次 commit -a 就把它从仓库里带走了。 rm -rf "$REPO/gateway/internal/static/static/assets" rm -f "$REPO/gateway/internal/static/static/index.html" cp -r "$REPO/web/dist/." "$REPO/gateway/internal/static/static/" echo "==> 构建 Gateway(单二进制,内含前端 + SQLite)" # 先删再建:go build -o 到已存在的路径时可能拿到 stale 二进制(此坑中过多次) rm -f "$REPO/gateway/agentmail-gateway" ( cd "$REPO/gateway" && go vet ./... && go test ./... && go build -o "$REPO/gateway/agentmail-gateway" ./cmd/server ) echo "==> 安装到 $PREFIX" install -d "$PREFIX" "$PREFIX/data" "$ETC" install -m 0755 "$REPO/gateway/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" < "$ETC/opencode.env" <