fix(mcp)★★: admin 签的 key 接不上 MCP —— 缺 agents 行

## 断点(实测出来的,不是推断)

    POST /admin/agent-keys {agent_name:"mcp-probe-tmp"}  → 签发成功,key 绑定正确
    GET  /mcp  用这把 key                                    → {"error":"密钥尚未绑定 Agent,请先调用 /agent/register 完成注册"}

`CreateAgentKey` 只写 `agent_keys`,**不建 `agents` 行** ⇒
`VerifyAgentKey` 返回空身份 ⇒ `AgentAuth` 拒掉。

而它自己的注释一直写着「登记密钥会**级联建 agents 行**,绕过注册检查」
—— **注释与实现不符**。

## 为什么这条断路没被报出来

报给客户端的是「请先调用 /agent/register 完成注册」,而 MCP 宿主
(Codex / Claude Desktop 等)**永远不会**去调它 —— 它只按配置发
`tools/list`。于是「admin 建身份 → 填进 MCP 配置 → 用」这条路是断的,
而断点没有任何报错指向 admin 侧。

## 修复

签 key 时顺带 `INSERT INTO agents`(ON CONFLICT DO NOTHING)。

★ **刻意不写成 online。** `CreateOrUpdateAgent` 那套是「插件刚启动、
自报家门」的语义(status='online' + last_seen=NOW());而 admin 预建的
身份**此刻还没连上**。写成 online 的症状很具体:WebUI 联系人里出现一个
「在线」却永远不回信的幽灵,而排查会先怀疑「是不是它卡住了」。

首次握手时 `TouchAgentLastSeen` 会把它改成在线 —— 那才是真的。
已有身份不会被改动任何字段(有判据钉住 secret/platform 不被覆盖)。

## 判据(3 格,2 个变异都转红)

- 签 key 后 agents 行存在,且 `VerifyAgentKey` 能解出身份
  (不建行 → 转红)
- 预建身份不是 online、没有 last_seen
  (offline → online → 转红)
- 已有 agent 的 secret/platform 不被签 key 改坏

14 包全绿。
This commit is contained in:
2026-10-04 14:40:17 +08:00
parent 22107308bb
commit d84c198975
2 changed files with 192 additions and 0 deletions

View File

@ -96,6 +96,20 @@ func CreateAgentKey(ctx context.Context, agentName, keyType, label string, expir
} else if retired {
return nil, fmt.Errorf("该名字已退役,不可重建")
}
// ★ 顺带确保 agents 行存在(2026-10-04 补)。
//
// 上面那句注释一直说「登记密钥会级联建 agents 行」,但实现里
// **从来没有建过** —— 于是 admin 签出一把 key,MCP 客户端拿它连上来
// 得到的是:
//
// {"error":"密钥尚未绑定 Agent,请先调用 /agent/register 完成注册"}
//
// 而它**永远不会**去调 /agent/register —— MCP 宿主只按配置发
// tools/list。所以「admin 建身份 → 填进 MCP 配置 → 用」这条
// 路是断的,而断点没有任何报错指向 admin 侧。
if err := ensureAgentRow(ctx, agentName); err != nil {
return nil, err
}
}
var namePtr *string
@ -367,3 +381,28 @@ func VerifyUserKey(ctx context.Context, token string) (*models.User, error) {
}
return u, nil
}
// ensureAgentRow 确保 agents 行存在,**不**改动已有行的任何字段。
//
// ★ 与 CreateOrUpdateAgent 的区别是刻意的:
// - 那个把 status 设成 'online'、last_seen 设成 NOW()
// —— 它服务于「插件刚启动,自报家门」,语义是「它现在活着」。
// - 这个服务于「管理员预建一个身份」。它此刻**还没连上**,
// 写成 online 会让 WebUI 显示成在线,而实际上没有任何进程在跑
// (症状:联系人里一个永远在线、却永远不回信的幽灵)。
//
// 所以这里只 INSERT,不 UPDATE;已存在就什么都不做。
// status 交给首次握手时的 TouchAgentLastSeen 去改。
func ensureAgentRow(ctx context.Context, agentName string) error {
if _, err := db.DB.ExecContext(ctx, `
INSERT INTO agents (agent_name, secret, workspaces, platform, status)
VALUES ($1, '', '[]', $2, 'offline')
ON CONFLICT (agent_name) DO NOTHING
`, agentName, "mcp"); err != nil {
if db.IsUniqueViolation(err) {
return nil // 并发下另一个请求已建好
}
return fmt.Errorf("创建 agent 身份 %s: %w", agentName, err)
}
return nil
}