停用/恢复文案说清密钥不会自动回来 + 原子部署脚本
## 起因 本会话踩到一次:浏览器测试点了 opencode 的「停用」按钮验证确认流程, 停用连带撤销全部密钥。插件从此拿着已撤销的密钥重试了 18 小时 (gateway 日志 2690 次 401),而 UI 只说了「已停用」。 恢复时同样没提示「密钥不会自动回来」,点完恢复以为就完事了。 ## 文案 后端 `AdminSetAgentStatus`: - 停用 detail 补一句「停用期间别人发信给它会收到 409」 - 恢复 detail 改成「停用时撤销的密钥不会自动回来 —— 必须在密钥面板 重新签发一把并写进该插件的配置,否则它会一直拿旧密钥重试并被拒(401)」 - 恢复响应加 `needs_new_key: true` 字段,前端可据此做更强的提示 前端 QuotaPanel:恢复成功的 notice 不再是「已恢复」四个字, 把重新签发这一步说全;面板说明与按钮 title 同步。 ## AdminDeleteAgent 两处修正 - 硬编码 `name == "jianf"` 改成按 `repo.IsHumanUser` 判定 —— 换管理员时 硬编码会失效,而人类账号不该走 Agent 删除端点 - detail 原来说「日历事件已保留」,实际 `DeleteAgent` 把 active 事件置为 cancelled(留着会由调度器一直触发,而发信人已不存在)。文案改成实际行为 ## deploy/redeploy-gateway.sh(新) 日常改后端不必重跑 install.sh(它重装 npm 依赖、重写 systemd 单元、 重新生成 env)。这个脚本做手工 `stop → cp → start` 不做的四件事: - `sqlite3 .backup` 备份数据库 + 立即 `PRAGMA integrity_check` 复核。 不用 cp:WAL 模式下 cp 会拿到主库与 -wal 不同步的快照 - `install -m 0755` 原子替换二进制。install 本质是 rename,要么完整 换掉要么原样不动;cp 是就地写入,中途失败会留下半截文件且旧的已被覆盖 - 旧二进制留档并打印可直接粘贴的回滚命令 - 后置验证清单:服务 active / /health 可达 / 近 2 分钟无 panic / SSE 重连计数。任一项不过**自动回滚**,不「先上着再修」 `--dry-run` 只打印动作,`--skip-tests` 急救用,`--skip-web` 跳过前端同步。 纪律来自 git-release-discipline skill 第五章。 ## 验证 - 脚本 dry-run + 真实跑通一次:备份 integrity_check=ok、原子替换、 9 个 SSE 客户端重连、验证四项全绿 - 停用/恢复文案线上实测;`DELETE /admin/agents/jianf` → 403「是人类用户」 - 端到端:jianf → opencode「部署脚本验收」→ 回信「部署验收 OK」 - 全量测试:gateway 全包 / web 176 / opencode 217 / pi 288 / dsh 241 / homeagent go ok;三方共用模块同源检查通过
This commit is contained in:
21
README.md
21
README.md
@ -52,6 +52,26 @@ sudo ./deploy/install.sh
|
||||
产物是**一个二进制加一个 .db 文件**:前端经 `go:embed` 打进二进制,数据库默认是 SQLite。
|
||||
首次运行会在 `/etc/agentmail/gateway.env` 生成随机管理员密码。
|
||||
|
||||
### 换二进制(日常改后端)
|
||||
|
||||
```bash
|
||||
bash deploy/redeploy-gateway.sh --dry-run # 先看要做什么
|
||||
bash deploy/redeploy-gateway.sh # 落地
|
||||
```
|
||||
|
||||
`install.sh` 太重(重装 npm 依赖、重写 systemd 单元、重新生成 env),日常只改后端时走这个。
|
||||
它做四件 `stop → cp → start` 不做的事:
|
||||
|
||||
- **`sqlite3 .backup` 备份数据库**,不用 `cp` —— WAL 模式下 `cp` 会拿到主库与 `-wal` 不同步的
|
||||
快照,恢复时可能丢最近写入甚至损坏;备份后立即 `PRAGMA integrity_check` 复核
|
||||
- **`install -m 0755` 原子替换二进制**,不用 `cp` —— `install` 本质是 `rename`,要么完整换掉
|
||||
要么原样不动;`cp` 是就地写入,中途失败会留下半截文件且旧的已被覆盖
|
||||
- **旧二进制留档**,打印可直接粘贴的回滚命令
|
||||
- **后置验证清单**:服务 active / `/health` 可达 / 近 2 分钟无 panic / SSE 重连计数。
|
||||
任一项不过**自动回滚**,不「先上着再修」
|
||||
|
||||
加 `--skip-tests` 急救(事后必须补跑),`--skip-web` 跳过前端同步。
|
||||
|
||||
### 构建期依赖
|
||||
|
||||
npm 只在构建期用到:Node ≥ 18(`npm run build`)与 Go ≥ 1.22。
|
||||
@ -183,6 +203,7 @@ agentmail/
|
||||
├── web/ # 前端(React + Vite + Tailwind)
|
||||
│ └── test/manual/ # 浏览器实测脚本(量真实盒子与命中区,不进 npm test)
|
||||
└── deploy/ # systemd 单元 + 安装脚本
|
||||
├── redeploy-gateway.sh # 二进制热替换(.backup + 原子 install + 后置验证 + 自动回滚)
|
||||
└── remote-agent-demo.py # 最小跨主机 Agent(纯标准库,验证协议层能力)
|
||||
```
|
||||
|
||||
|
||||
@ -175,3 +175,13 @@ 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 原子替换二进制,
|
||||
# 并在验证不过时自动回滚。
|
||||
|
||||
235
deploy/redeploy-gateway.sh
Executable file
235
deploy/redeploy-gateway.sh
Executable file
@ -0,0 +1,235 @@
|
||||
#!/usr/bin/env bash
|
||||
# redeploy-gateway.sh —— Gateway 二进制热替换(原子化,带备份与后置验证)
|
||||
#
|
||||
# 为什么需要这个脚本:日常改后端只需要换二进制,跑整个 install.sh 太重
|
||||
# (它会重装 npm 依赖、重写 systemd 单元、重新生成 env)。而手工
|
||||
# 「systemctl stop → cp → start」有两个隐患:
|
||||
#
|
||||
# 1. cp 是就地写入,会改坏**正在运行中进程**的可执行映像。
|
||||
# 即使先 stop 了,中途失败也会留下一个半截二进制且旧的已被覆盖。
|
||||
# install(1) 本质是 rename,是原子的 —— 要么完整换掉,要么原样不动。
|
||||
#
|
||||
# 2. 不备份数据库。SQLite 在 WAL 模式下 cp 会拿到不一致快照
|
||||
# (主库文件与 -wal 不同步),恢复时可能丢最近写入甚至损坏。
|
||||
# 必须用 sqlite3 .backup,它走的是官方在线备份 API。
|
||||
#
|
||||
# 纪律来自 git-release-discipline skill 第五章:
|
||||
# 发布终点不是「推上去了」,而是后置验证清单全绿。任一项不过就回滚,
|
||||
# 不要「先上着再修」。
|
||||
#
|
||||
# 用法:
|
||||
# bash deploy/redeploy-gateway.sh # 完整流程(含测试)
|
||||
# bash deploy/redeploy-gateway.sh --skip-tests # 跳过测试(急救时用)
|
||||
# bash deploy/redeploy-gateway.sh --dry-run # 只打印将执行的动作
|
||||
#
|
||||
# 退出码: 0=成功 1=失败或验证不过(已尝试回滚) 2=参数/环境问题
|
||||
|
||||
set -uo pipefail
|
||||
|
||||
REPO="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
PREFIX="${AGENTMAIL_PREFIX:-/opt/agentmail}"
|
||||
TARGET="$PREFIX/agentmail-gateway"
|
||||
DB="$PREFIX/data/agentmail.db"
|
||||
SERVICE="agentmail-gateway"
|
||||
HEALTH_URL="${AGENTMAIL_HEALTH_URL:-http://127.0.0.1:8180/health}"
|
||||
TS="$(date +%Y%m%d-%H%M%S)"
|
||||
|
||||
SKIP_TESTS=0
|
||||
DRY_RUN=0
|
||||
SYNC_WEB=1
|
||||
|
||||
while [ $# -gt 0 ]; do
|
||||
case "$1" in
|
||||
--skip-tests) SKIP_TESTS=1; shift ;;
|
||||
--skip-web) SYNC_WEB=0; shift ;;
|
||||
--dry-run) DRY_RUN=1; shift ;;
|
||||
-h|--help) sed -n '2,30p' "$0"; exit 0 ;;
|
||||
*) echo "未知参数: $1" >&2; exit 2 ;;
|
||||
esac
|
||||
done
|
||||
|
||||
export GOPROXY="${GOPROXY:-https://goproxy.cn,direct}"
|
||||
|
||||
say() { printf '\n=== %s\n' "$*"; }
|
||||
ok() { printf ' [ OK ] %s\n' "$*"; }
|
||||
bad() { printf ' [FAIL] %s\n' "$*"; }
|
||||
warn() { printf ' [WARN] %s\n' "$*"; }
|
||||
|
||||
run() {
|
||||
if [ "$DRY_RUN" = 1 ]; then
|
||||
printf ' [dry-run] %s\n' "$*"
|
||||
return 0
|
||||
fi
|
||||
printf ' $ %s\n' "$*"
|
||||
eval "$@"
|
||||
}
|
||||
|
||||
[ -d "$REPO/gateway" ] || { bad "找不到 $REPO/gateway"; exit 2; }
|
||||
command -v sqlite3 >/dev/null 2>&1 || { bad "缺少 sqlite3,无法安全备份数据库"; exit 2; }
|
||||
|
||||
say "0. 计划"
|
||||
printf ' 仓库 : %s\n' "$REPO"
|
||||
printf ' 目标 : %s\n' "$TARGET"
|
||||
printf ' 数据库 : %s\n' "$DB"
|
||||
printf ' 服务 : %s\n' "$SERVICE"
|
||||
[ "$DRY_RUN" = 1 ] && printf ' 模式 : DRY-RUN(不落地)\n' || printf ' 模式 : 真实执行\n'
|
||||
|
||||
# ---------------------------------------------------------------- 1 前端产物
|
||||
if [ "$SYNC_WEB" = 1 ] && [ -d "$REPO/web/dist/assets" ]; then
|
||||
say "1. 同步前端产物进 go:embed 目录"
|
||||
# 只清构建产物:placeholder.html 在版本库里(让 go:embed 在新克隆里能编译),
|
||||
# 删掉它会让 git 看到一个本地删除,下一次 commit -a 就把它从仓库带走。
|
||||
run "rm -rf '$REPO/gateway/internal/static/static/assets'"
|
||||
run "rm -f '$REPO/gateway/internal/static/static/index.html'"
|
||||
run "cp -r '$REPO/web/dist/.' '$REPO/gateway/internal/static/static/'"
|
||||
ok "前端产物已同步"
|
||||
else
|
||||
say "1. 跳过前端同步"
|
||||
[ "$SYNC_WEB" = 0 ] && ok "--skip-web" || warn "web/dist 不存在,先跑 cd web && npm run build"
|
||||
fi
|
||||
|
||||
# ---------------------------------------------------------------- 2 静态检查与测试
|
||||
say "2. 构建前检查"
|
||||
if [ "$DRY_RUN" = 1 ]; then
|
||||
printf ' [dry-run] go vet ./... && go test ./...\n'
|
||||
else
|
||||
( cd "$REPO/gateway" && go vet ./... ) || { bad "go vet 不过,终止"; exit 1; }
|
||||
ok "go vet 通过"
|
||||
if [ "$SKIP_TESTS" = 1 ]; then
|
||||
warn "--skip-tests:跳过测试(急救模式,事后必须补跑)"
|
||||
else
|
||||
( cd "$REPO/gateway" && timeout 280 go test ./... -timeout 250s ) \
|
||||
|| { bad "测试不过,终止部署"; exit 1; }
|
||||
ok "go test 全包通过"
|
||||
fi
|
||||
fi
|
||||
|
||||
# ---------------------------------------------------------------- 3 构建
|
||||
say "3. 构建二进制"
|
||||
STAGE="/tmp/agentmail-gateway-build-$TS"
|
||||
# 先删再建:go build -o 到已存在的路径时可能拿到 stale 二进制(此坑中过多次)
|
||||
run "rm -f '$STAGE'"
|
||||
if [ "$DRY_RUN" = 1 ]; then
|
||||
printf ' [dry-run] (cd gateway && go build -o %s ./cmd/server)\n' "$STAGE"
|
||||
else
|
||||
( cd "$REPO/gateway" && go build -o "$STAGE" ./cmd/server ) \
|
||||
|| { bad "构建失败"; exit 1; }
|
||||
ok "构建完成 $(du -h "$STAGE" | cut -f1)"
|
||||
fi
|
||||
|
||||
# ---------------------------------------------------------------- 4 备份数据库
|
||||
say "4. 备份数据库(sqlite3 .backup,不能用 cp —— WAL 下 cp 会拿到不一致快照)"
|
||||
DBBAK=""
|
||||
if [ -f "$DB" ]; then
|
||||
DBBAK="/tmp/agentmail-pre-deploy-$TS.db"
|
||||
run "sqlite3 '$DB' \".backup '$DBBAK'\"" || { bad "备份失败,终止部署"; exit 1; }
|
||||
if [ "$DRY_RUN" != 1 ]; then
|
||||
integ="$(sqlite3 "$DBBAK" 'PRAGMA integrity_check;' 2>&1 | head -1)"
|
||||
if [ "$integ" = "ok" ]; then
|
||||
ok "备份完成 $DBBAK($(du -h "$DBBAK" | cut -f1)),integrity_check=ok"
|
||||
else
|
||||
bad "备份完整性异常: $integ"; exit 1
|
||||
fi
|
||||
fi
|
||||
else
|
||||
warn "数据库不存在(首次部署?): $DB"
|
||||
fi
|
||||
|
||||
# ---------------------------------------------------------------- 5 旧二进制留档
|
||||
say "5. 旧二进制留档"
|
||||
BINBAK=""
|
||||
if [ -f "$TARGET" ]; then
|
||||
BINBAK="${TARGET}.bak-${TS}"
|
||||
run "install -m 0755 '$TARGET' '$BINBAK'"
|
||||
ok "旧二进制留档 $BINBAK"
|
||||
else
|
||||
warn "目标不存在(首次安装)"
|
||||
fi
|
||||
|
||||
# ---------------------------------------------------------------- 6 原子替换
|
||||
say "6. 停服 → 原子替换 → 起服"
|
||||
# 为什么仍要 stop:SQLite 单写者,且换掉二进制后旧进程还在跑旧代码,
|
||||
# 与新库 schema 可能不一致。install 保证的是「文件替换本身」原子,
|
||||
# 不代表可以热换正在服务的进程。
|
||||
run "systemctl stop '$SERVICE'"
|
||||
run "install -m 0755 '$STAGE' '$TARGET'" || {
|
||||
bad "替换失败"
|
||||
[ -n "$BINBAK" ] && run "install -m 0755 '$BINBAK' '$TARGET'"
|
||||
run "systemctl start '$SERVICE'"
|
||||
exit 1
|
||||
}
|
||||
run "systemctl start '$SERVICE'"
|
||||
run "sleep 6"
|
||||
|
||||
# ---------------------------------------------------------------- 7 后置验证
|
||||
say "7. 后置验证清单(发布终点是这张单子全绿,不是「换上去了」)"
|
||||
if [ "$DRY_RUN" = 1 ]; then
|
||||
cat <<'EOF'
|
||||
[dry-run] 真实执行时逐项校验:
|
||||
[ ] 服务 active
|
||||
[ ] /health 可达
|
||||
[ ] 近 2 分钟无 panic/fatal
|
||||
[ ] 四个 Agent 桥的 SSE 重新连上
|
||||
EOF
|
||||
echo
|
||||
echo " DRY-RUN 结束。确认无误后去掉 --dry-run 重跑。"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
CHECK_FAIL=0
|
||||
|
||||
if systemctl is-active --quiet "$SERVICE"; then
|
||||
ok "服务 $SERVICE active"
|
||||
else
|
||||
bad "服务未 active"; CHECK_FAIL=$((CHECK_FAIL+1))
|
||||
fi
|
||||
|
||||
if curl -sf -m 5 "$HEALTH_URL" >/dev/null 2>&1; then
|
||||
ok "健康检查通过 $HEALTH_URL"
|
||||
else
|
||||
bad "健康检查失败 $HEALTH_URL"; CHECK_FAIL=$((CHECK_FAIL+1))
|
||||
fi
|
||||
|
||||
fc="$(journalctl -u "$SERVICE" --since '2 min ago' --no-pager 2>/dev/null \
|
||||
| grep -icE 'panic|fatal|SIGSEGV' || true)"
|
||||
if [ "${fc:-0}" = "0" ]; then
|
||||
ok "近 2 分钟无 panic/fatal"
|
||||
else
|
||||
bad "近 2 分钟出现 $fc 条 panic/fatal"; CHECK_FAIL=$((CHECK_FAIL+1))
|
||||
fi
|
||||
|
||||
# 桥重连:Gateway 重启会掐断所有 SSE,插件应当在几秒内自己回来。
|
||||
# 一个都没回来通常意味着密钥被撤销(停用 Agent 会撤销密钥)或端口没起。
|
||||
sse="$(journalctl -u "$SERVICE" --since '1 min ago' --no-pager 2>/dev/null \
|
||||
| grep -c 'Client connected' || true)"
|
||||
if [ "${sse:-0}" -gt 0 ]; then
|
||||
ok "已有 $sse 个 SSE 客户端重新连上"
|
||||
else
|
||||
warn "暂未看到 SSE 重连 —— 若插件应当在线,检查密钥是否被撤销(停用会撤销密钥)"
|
||||
fi
|
||||
|
||||
echo
|
||||
echo " 仍需人工确认(脚本无法代替):"
|
||||
echo " [ ] 真实发一封邮件端到端跑通(不是只看进程起来了)"
|
||||
echo " [ ] WebUI 能登录且列表正常"
|
||||
|
||||
echo
|
||||
if [ "$CHECK_FAIL" -gt 0 ]; then
|
||||
say "结论: 验证有 $CHECK_FAIL 项失败 —— 正在回滚,不要「先上着再修」"
|
||||
if [ -n "$BINBAK" ]; then
|
||||
run "systemctl stop '$SERVICE'"
|
||||
run "install -m 0755 '$BINBAK' '$TARGET'"
|
||||
run "systemctl start '$SERVICE'"
|
||||
ok "已回滚到 $BINBAK"
|
||||
else
|
||||
warn "无旧二进制可回滚"
|
||||
fi
|
||||
[ -n "$DBBAK" ] && echo " 如需恢复数据库(先停服务): install -m 0644 '$DBBAK' '$DB'"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
say "结论: 自动项全绿"
|
||||
echo " 回滚命令(留档 24 小时内有效):"
|
||||
[ -n "$BINBAK" ] && echo " install -m 0755 '$BINBAK' '$TARGET' && systemctl restart '$SERVICE'"
|
||||
[ -n "$DBBAK" ] && echo " install -m 0644 '$DBBAK' '$DB' # 先 systemctl stop"
|
||||
exit 0
|
||||
@ -239,15 +239,14 @@ type setAgentStatusRequest struct {
|
||||
|
||||
// PUT /api/v1/admin/agents/{name}/status —— 停用或恢复一个 Agent
|
||||
//
|
||||
// 停用是可逆的「归档」,不是删除:
|
||||
// 停用是可逆的「归档」,比 DELETE 轻一档:
|
||||
// - 邮件、会话、权限记录、转发幂等键全部保留(往来里有一半是人自己写的)
|
||||
// - 从地址补全、GET /agents、可授权范围里消失
|
||||
// - 全部密钥被撤销,插件拿不到新任务也发不出信
|
||||
// - 重新注册会被拒(否则插件下次启动就把它复活了)
|
||||
// - 别人发信给它得到 409(见 repo.RecipientDeliverable)
|
||||
//
|
||||
// 不提供彻底删除:Agent 名与人类用户名共用命名空间,删掉之后历史邮件的
|
||||
// from_name 指向一个不存在的名字,此时有人注册同名 Agent(或人类账号),
|
||||
// 那些旧邮件会看起来像是他发的。
|
||||
// 与 DELETE 的分工:停用留着运行态随时可恢复,删除清掉运行态且名字退役。
|
||||
func AdminSetAgentStatus(w http.ResponseWriter, r *http.Request) {
|
||||
name := strings.TrimSpace(chi.URLParam(r, "name"))
|
||||
if name == "" {
|
||||
@ -277,9 +276,12 @@ func AdminSetAgentStatus(w http.ResponseWriter, r *http.Request) {
|
||||
if req.Disabled {
|
||||
resp["keys_revoked"] = revoked
|
||||
resp["detail"] = "已停用。邮件与会话保留;该 Agent 的密钥已全部撤销," +
|
||||
"恢复后需要重新签发。"
|
||||
"恢复后需要重新签发。停用期间别人发信给它会收到 409。"
|
||||
} else {
|
||||
resp["detail"] = "已恢复为离线状态。需要重新签发密钥,插件连上后自动转为在线。"
|
||||
resp["needs_new_key"] = true
|
||||
resp["detail"] = "已恢复为离线状态。停用时撤销的密钥不会自动回来 —— " +
|
||||
"必须在「密钥」面板重新签发一把并写进该插件的配置," +
|
||||
"否则它会一直拿旧密钥重试并被拒(401)。"
|
||||
}
|
||||
JSON(w, http.StatusOK, resp)
|
||||
}
|
||||
@ -289,18 +291,24 @@ func AdminSetAgentStatus(w http.ResponseWriter, r *http.Request) {
|
||||
// 删除 Agent 的全部运行态,保留邮件历史。
|
||||
//
|
||||
// 取舍(见 PLUGIN-CONTRACT.md):
|
||||
// - 不做彻底删除(邮件是审计凭据,且 Agent 名与人类用户名共用命名空间)
|
||||
// - 名字立即不可重建(避免同名新注册冒用历史署名)
|
||||
// - 保留的邮件、会话与日历事件不会被级联删除
|
||||
// - 邮件与会话不删(是审计凭据,且往来里有一半是人自己写的)
|
||||
// - 它建的日历事件置 cancelled(留着会由调度器一直触发,发信人却已不存在)
|
||||
// - 名字立即不可重建(Agent 名与人类用户名共用命名空间,
|
||||
// 否则下一个同名注册者会看起来像是历史邮件的发信人)
|
||||
func AdminDeleteAgent(w http.ResponseWriter, r *http.Request) {
|
||||
name := strings.TrimSpace(chi.URLParam(r, "name"))
|
||||
if name == "" {
|
||||
Error(w, http.StatusBadRequest, "Missing agent name")
|
||||
return
|
||||
}
|
||||
// 内置管理员不可删
|
||||
if name == "jianf" {
|
||||
Error(w, http.StatusForbidden, "内置管理员不可删除")
|
||||
// 人类账号不走这个端点。硬编码某个用户名会在换管理员时失效,
|
||||
// 所以按「是不是人类用户」判定。
|
||||
if isHuman, hErr := repo.IsHumanUser(r.Context(), name); hErr != nil {
|
||||
Error(w, http.StatusInternalServerError, "Failed to check recipient")
|
||||
return
|
||||
} else if isHuman {
|
||||
Error(w, http.StatusForbidden,
|
||||
"\""+name+"\" 是人类用户,不能用这个端点删除(请去用户管理)")
|
||||
return
|
||||
}
|
||||
|
||||
@ -315,10 +323,10 @@ func AdminDeleteAgent(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
JSON(w, http.StatusOK, map[string]any{
|
||||
"agent_name": name,
|
||||
"agent_name": name,
|
||||
"keys_revoked": keysRevoked,
|
||||
"detail": "已删除。邮件、会话与日历事件已保留;" +
|
||||
"密钥与平台会话镜像已清除。" +
|
||||
"detail": "已删除。邮件与会话保留(审计凭据);" +
|
||||
"密钥、平台会话镜像、模型范围已清除;它建的日历事件已置为取消。" +
|
||||
"此名字今后不可再注册(历史邮件的署名由此不会被冒用)。",
|
||||
})
|
||||
}
|
||||
|
||||
@ -411,6 +411,8 @@ export async function adminSetAgentStatus(agentName: string, disabled: boolean)
|
||||
agent_name: string;
|
||||
disabled: boolean;
|
||||
keys_revoked?: number;
|
||||
/** 恢复路径为 true:停用时撤销的密钥不会自动回来,必须重新签发 */
|
||||
needs_new_key?: boolean;
|
||||
detail: string;
|
||||
}>(
|
||||
'PUT',
|
||||
|
||||
@ -77,12 +77,17 @@ export default function QuotaPanel() {
|
||||
const result = await api.adminSetAgentStatus(name, !currentDisabled);
|
||||
await load();
|
||||
setConfirming(null);
|
||||
// 撤销了几把密钥是操作结果里最有信息量的部分 —— 恢复后要重新签发几把,
|
||||
// 撤销了几把密钥是停用操作里最有信息量的部分 —— 恢复后要重新签发几把,
|
||||
// 只说「已停用」的话用户不知道还有这一步。
|
||||
const revoked = typeof result.keys_revoked === 'number' && result.keys_revoked > 0
|
||||
? `(已撤销 ${result.keys_revoked} 把密钥)`
|
||||
: '';
|
||||
setNotice(`${name} ${result.disabled ? '已停用' : '已恢复'}${revoked}`);
|
||||
// 恢复路径必须把「密钥不会自动回来」说出口。否则用户点完恢复就算完事,
|
||||
// 而插件拿着已撤销的密钥无限重试并被 401 —— 本会话就踩过:
|
||||
// opencode 被停用后拿旧密钥重试了 18 小时,gateway 日志里 2690 次 401。
|
||||
setNotice(result.disabled
|
||||
? `${name} 已停用${revoked}`
|
||||
: `${name} 已恢复为离线。密钥不会自动回来 —— 请到「密钥」面板重新签发一把并写进插件配置,否则它会一直被拒(401)。`);
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : String(err));
|
||||
} finally {
|
||||
@ -115,8 +120,8 @@ export default function QuotaPanel() {
|
||||
插件自动转发的最终总结与权限询问不占用预算。
|
||||
<br />
|
||||
<strong className="font-medium text-gray-600">停用</strong>:撤销该 Agent 的全部密钥、
|
||||
从地址补全与联系人里隐藏,并拒绝它重新注册。
|
||||
<span className="text-gray-400">邮件、会话、模型范围全部保留,随时可恢复。</span>
|
||||
从地址补全与联系人里隐藏,拒绝它重新注册,并让别人发信给它时收到 409。
|
||||
<span className="text-gray-400">邮件、会话、模型范围全部保留,随时可恢复 —— 但密钥不会自动回来,恢复后需重新签发。</span>
|
||||
<br />
|
||||
<strong className="font-medium text-gray-600">删除</strong>:在停用的基础上清掉运行态(日历事件置 cancelled)。
|
||||
<span className="text-gray-400">邮件与会话保留(历史是审计凭据),此名字今后不可再注册。</span>
|
||||
@ -195,7 +200,7 @@ export default function QuotaPanel() {
|
||||
onClick={() => { setConfirming(s.agent_name); setConfirmAction('toggle'); }}
|
||||
disabled={busy === s.agent_name}
|
||||
title={isDisabled
|
||||
? '恢复此 Agent(恢复为离线;密钥需重新签发)'
|
||||
? '恢复此 Agent(恢复为离线;停用时撤销的密钥不会自动回来,必须重新签发)'
|
||||
: '停用此 Agent(撤销全部密钥并从补全里隐藏;邮件与会话保留,可恢复)'}
|
||||
className={`tap shrink-0 inline-flex items-center gap-1 text-[11px] px-1.5 py-0.5 rounded border ${
|
||||
isDisabled
|
||||
|
||||
Reference in New Issue
Block a user