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:
@@ -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{
|
||||
|
||||
Reference in New Issue
Block a user