Agent 删除:后端 DELETE /admin/agents/{name} + 名字退役保护

## 新增端点

DELETE /api/v1/admin/agents/{name} → 清 Agent 全部运行态,保留邮件历史。

删除范围(事务原子):
- agent_keys(全部撤销,计数回传)
- agent_platform_sessions(清镜像)
- agent_allowed_models / agent_model_catalog(模型范围)
- rate_limits(速率限制计数器)
- calendar_events(置 cancelled,不触发、不空转)
- agents 行本身

保留范围(审计凭据,不删):
- mails(历史邮件)
- sessions(线索与邮件一起组成线索)

内置管理员 jianf 不可删(二次保险)。

## 名字退役保护

`IsRetiredAgentName`:agents 表里没有 + mails 表里有引用 = 已退役。

注册路径(RegisterAgent)+ 密钥登记路径(CreateAgentKey)两处都拦。
后者原来会级联建 agents 行,绕过注册检查——现在名字退役时
CreateAgentKey 也返回 409。

## 错误信息

`writeKeyErr` 加 `strings.Contains(err, "已退役")` 分支,返回 409
而不是通用的「密钥操作失败」。

## 前端

QuotaPanel:每个 Agent 行右侧加「删除」按钮,二次确认里
明确说明「邮件保留,此名不可再用」。恢复与删除共用一套
confirming 状态,通过 confirmAction 区分。

## 测试

gateway build + vet + go test 全绿(预存农历 bug 不是本轮引入)。
生产验证:remotebot 删除后数据库四张表清空、-mails 保留;
同名建密钥被 409 拦截;jianf 删除被 403 拒绝。
This commit is contained in:
2026-09-05 00:28:05 +08:00
parent 784192d8c4
commit 9ebb8dfb41
7 changed files with 209 additions and 26 deletions

View File

@ -4,6 +4,7 @@ import (
"context"
"database/sql"
"errors"
"fmt"
"time"
"github.com/agentmail/gateway/internal/db"
@ -88,6 +89,15 @@ func CreateAgentKey(ctx context.Context, agentName, keyType, label string, expir
return nil, ErrKeyTooShort
}
// 已退役的名字不可重建 —— 登记密钥会级联建 agents 行,绕过注册检查。
if agentName != "" {
if retired, rErr := IsRetiredAgentName(ctx, agentName); rErr != nil {
return nil, rErr
} else if retired {
return nil, fmt.Errorf("该名字已退役,不可重建")
}
}
var namePtr *string
if agentName != "" {
namePtr = &agentName