原 PLUGIN-GUIDE 是叙事式的「怎么做 + 踩过的坑」,读者要自己从散文里推断 「我到底必须做什么」。接第三个平台时这不够用 —— 尤其当照着实现的是一个代理。 改为规格式,编号可引用、强度明确标注、每条尽量给出可机械核对的判据。 旧文档的内容全部保留(迁进第八、九节),另补上原先没有的四类: ## 一、能力矩阵(新增) 回答「这个平台能不能接」。七项必需能力(C-1..C-7)加七项可选(C-8..C-14), 每项给出判据。附一个七问自检 —— 任何一问答不出来就先别写代码。 其中 C-4「轮次结束信号必须能区分成功与出错」在两次适配里都被漏掉过, 两次都造成「无效模型被判成成功」,所以单独标了出来。 ## 二、行为约定(重写) 原先散在各节的要求收拢成一个状态机,按事件逐条规定:B-1 启动 / B-2 心跳 / B-3 new_mail / B-4 permission_decision / B-5 轮次结束 / B-6 无法处理时回信 / B-7 启动补拉 / B-8 权限询问 / B-9 关停。 ## 四、降级语义(新增) 平台缺某项能力时的确切退化路径(D-1..D-7)。原文档只说了「可选」, 没说缺了之后该怎么办 —— 于是「不支持权限钩子」很容易被实现成 「提供 request_permission 工具补偿」,而那正是 I-1 反对的模式。 ## 六、不变量与禁止事项(新增) 12 条 MUST NOT,每条附「违反会怎样」。这些是测试全绿、跑起来也不报错, 但行为就是错的那类问题 —— 例如拉取失败时传 [] 而非省略字段会清空服务端目录。 ## 七、验收清单(新增) 八组可勾选项,每条给出具体命令:grep 自查禁止事项、sqlite3 查在线状态与 配额未被消耗、停插件发信再启动看补投日志。 ## 核对过的事实 写完逐项核对了代码,不是凭记忆: - 12 个端点全部在 main.go 里存在且方法一致 - 发信 9 个字段名与 sendMailRequest 的 json tag 一致 - 心跳响应 12 个字段名与 handler 一致 - 九个数字(30s 心跳 / 25MB 附件 / 20 次每小时 / 补投 5 封 / 快照 200 条 / 模型上限 10 / 目录上限 300 / 降级超时 60s / inbox 默认 5)都能在代码里找到出处 - 验收清单里的六条 sqlite 查询都在生产库上跑通 - 38 个编号无重复,18 处交叉引用全部有定义 引用同步:PLAN.md、PHASE7-REMAINING.md、API.md、README.md、 install.sh、check-shared-libs.sh。
131 lines
5.6 KiB
Bash
Executable File
131 lines
5.6 KiB
Bash
Executable File
#!/usr/bin/env bash
|
||
#
|
||
# 把 AgentMail Gateway 与 opencode serve 安装为 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
|
||
|
||
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" <<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=/opt/agentmail/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
|
||
|
||
echo "==> 安装 systemd 单元"
|
||
install -m 0644 "$REPO/deploy/agentmail-gateway.service" /etc/systemd/system/
|
||
install -m 0644 "$REPO/deploy/opencode-serve.service" /etc/systemd/system/
|
||
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
|
||
|
||
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"
|