feat: return bind credentials and reuse sync-bound agents on register (Z28)

Phone/bind-code/ticket responses include rotated client_secret; SelfRegister reuses active sync_bound hosts instead of spawning pending; archive coop opinion and keep a living summary.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
whm
2026-08-06 21:09:56 +08:00
parent d9d126fbbb
commit e75c6732d6
8 changed files with 1138 additions and 994 deletions

View File

@@ -136,22 +136,43 @@ func (s *MemoryStore) Register(_ context.Context, tenantID int64, name, hostKey
hostKey = strings.TrimSpace(hostKey)
if hostKey != "" {
for _, a := range s.byID {
if a.TenantID == tenantID && a.HostKey == hostKey {
if a.Status == StatusPending {
secret := randomHex(24)
hash, err := bcrypt.GenerateFromPassword([]byte(secret), bcrypt.DefaultCost)
if err != nil {
return nil, "", false, err
}
a.SecretHash = string(hash)
if n := strings.TrimSpace(name); n != "" {
a.Name = n
}
cp := cloneAcc(&a.Account)
return &cp, secret, true, nil
}
return nil, "", false, fmt.Errorf("host already registered as %s (status=%s)", a.ClientID, a.Status)
if a.HostKey != hostKey {
continue
}
// Z28已有任意租户下 active+sync_bound → 禁止另起 pending
if a.Status == StatusActive && strings.TrimSpace(a.ChannelID) != "" && strings.TrimSpace(a.OnlineDBID) != "" {
if a.TenantID != tenantID {
return nil, "", false, fmt.Errorf("host_key 已同步绑定其它公司client_id=%s请使用同步绑定凭证换票勿再 SelfRegister", a.ClientID)
}
secret := randomHex(24)
hash, err := bcrypt.GenerateFromPassword([]byte(secret), bcrypt.DefaultCost)
if err != nil {
return nil, "", false, err
}
a.SecretHash = string(hash)
if n := strings.TrimSpace(name); n != "" {
a.Name = n
}
cp := cloneAcc(&a.Account)
return &cp, secret, true, nil
}
if a.TenantID != tenantID {
continue
}
if a.Status == StatusPending {
secret := randomHex(24)
hash, err := bcrypt.GenerateFromPassword([]byte(secret), bcrypt.DefaultCost)
if err != nil {
return nil, "", false, err
}
a.SecretHash = string(hash)
if n := strings.TrimSpace(name); n != "" {
a.Name = n
}
cp := cloneAcc(&a.Account)
return &cp, secret, true, nil
}
return nil, "", false, fmt.Errorf("host already registered as %s (status=%s)", a.ClientID, a.Status)
}
}
acc, secret, err := s.createLocked(tenantID, 0, CreateInput{
@@ -457,32 +478,42 @@ func (s *PostgresStore) Create(ctx context.Context, tenantID, createdBy int64, i
func (s *PostgresStore) Register(ctx context.Context, tenantID int64, name, hostKey string) (*Account, string, bool, error) {
hostKey = strings.TrimSpace(hostKey)
if hostKey != "" {
var a Account
var last sql.NullTime
err := s.DB.QueryRowContext(ctx, `
SELECT agent_id, tenant_id, name, client_id, COALESCE(host_key,''), COALESCE(role_id,0),
COALESCE(channel_id,''), COALESCE(online_db_id,''), COALESCE(database_name,''),
status, created_by, created_at, last_token_at
FROM platform_meta.agent_accounts WHERE tenant_id=$1 AND host_key=$2`, tenantID, hostKey,
).Scan(&a.AgentID, &a.TenantID, &a.Name, &a.ClientID, &a.HostKey, &a.RoleID,
&a.ChannelID, &a.OnlineDBID, &a.DatabaseName,
&a.Status, &a.CreatedBy, &a.CreatedAt, &last)
if err == nil {
if a.Status == StatusPending {
secret, err := s.RotateSecret(ctx, tenantID, a.AgentID)
// Z28先按 host_key 全局查找,避免已 sync_bound 时另起 pending
if existing, err := s.FindByHostKey(ctx, hostKey); err == nil && existing != nil {
syncBound := strings.TrimSpace(existing.ChannelID) != "" && strings.TrimSpace(existing.OnlineDBID) != ""
if existing.Status == StatusActive && syncBound {
if existing.TenantID != tenantID {
return nil, "", false, fmt.Errorf("host_key 已同步绑定其它公司client_id=%s请使用同步绑定凭证换票勿再 SelfRegister", existing.ClientID)
}
secret, err := s.RotateSecret(ctx, tenantID, existing.AgentID)
if err != nil {
return nil, "", false, err
}
if n := strings.TrimSpace(name); n != "" {
_, _ = s.Update(ctx, tenantID, a.AgentID, UpdateInput{Name: &n})
_, _ = s.Update(ctx, tenantID, existing.AgentID, UpdateInput{Name: &n})
}
acc, err := s.Get(ctx, tenantID, a.AgentID)
acc, err := s.Get(ctx, tenantID, existing.AgentID)
return acc, secret, true, err
}
return nil, "", false, fmt.Errorf("host already registered as %s (status=%s)", a.ClientID, a.Status)
}
if !errors.Is(err, sql.ErrNoRows) {
return nil, "", false, err
if existing.TenantID == tenantID && existing.Status == StatusPending {
secret, err := s.RotateSecret(ctx, tenantID, existing.AgentID)
if err != nil {
return nil, "", false, err
}
if n := strings.TrimSpace(name); n != "" {
_, _ = s.Update(ctx, tenantID, existing.AgentID, UpdateInput{Name: &n})
}
acc, err := s.Get(ctx, tenantID, existing.AgentID)
return acc, secret, true, err
}
if existing.TenantID == tenantID {
return nil, "", false, fmt.Errorf("host already registered as %s (status=%s)", existing.ClientID, existing.Status)
}
} else if err != nil {
// FindByHostKey 未找到 → 继续新建 pending
if !strings.Contains(err.Error(), "not found") {
return nil, "", false, err
}
}
}
acc, secret, err := s.insert(ctx, tenantID, 0, CreateInput{