feat: harden loose-offline sync for user JWT, schema, and console ops

Enable Binding-scoped agent push/pull, empty-table schema ensure, SyncPage inspect/drop-table, default module import, and agent-bound publish docs from the 宇恒联调意见.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
whm
2026-08-05 09:47:35 +08:00
parent 76cdcd760e
commit b04b180d30
59 changed files with 4762 additions and 308 deletions

View File

@@ -23,37 +23,47 @@ const (
)
type Account struct {
AgentID int64 `json:"agent_id"`
TenantID int64 `json:"tenant_id"`
Name string `json:"name"`
ClientID string `json:"client_id"`
HostKey string `json:"host_key,omitempty"`
RoleID int64 `json:"role_id,omitempty"`
RoleCode string `json:"role_code,omitempty"`
RoleName string `json:"role_name,omitempty"`
Status string `json:"status"`
Perms []string `json:"permissions"`
AppSlugs []string `json:"app_slugs"`
CreatedBy int64 `json:"created_by"`
CreatedAt time.Time `json:"created_at"`
LastTokenAt *time.Time `json:"last_token_at,omitempty"`
AgentID int64 `json:"agent_id"`
TenantID int64 `json:"tenant_id"`
Name string `json:"name"`
ClientID string `json:"client_id"`
HostKey string `json:"host_key,omitempty"`
RoleID int64 `json:"role_id,omitempty"`
RoleCode string `json:"role_code,omitempty"`
RoleName string `json:"role_name,omitempty"`
Status string `json:"status"`
Perms []string `json:"permissions"`
AppSlugs []string `json:"app_slugs"`
// Z8b智能体绑定的同步通道 / 线上库(模块与松离线同一目标)
ChannelID string `json:"channel_id,omitempty"`
OnlineDBID string `json:"online_db_id,omitempty"`
DatabaseName string `json:"database_name,omitempty"`
CreatedBy int64 `json:"created_by"`
CreatedAt time.Time `json:"created_at"`
LastTokenAt *time.Time `json:"last_token_at,omitempty"`
}
type CreateInput struct {
Name string
Perms []string
AppSlugs []string
Status string // 空则 active
HostKey string
RoleID int64
Name string
Perms []string
AppSlugs []string
Status string // 空则 active
HostKey string
RoleID int64
ChannelID string
OnlineDBID string
DatabaseName string
}
type UpdateInput struct {
Name *string
Status *string
Perms *[]string
AppSlugs *[]string
RoleID *int64
Name *string
Status *string
Perms *[]string
AppSlugs *[]string
RoleID *int64
ChannelID *string
OnlineDBID *string
DatabaseName *string
}
type Store interface {
@@ -165,17 +175,20 @@ func (s *MemoryStore) createLocked(tenantID, createdBy int64, in CreateInput) (*
s.seq++
a := &memAcc{
Account: Account{
AgentID: s.seq,
TenantID: tenantID,
Name: strings.TrimSpace(in.Name),
ClientID: cid,
HostKey: strings.TrimSpace(in.HostKey),
RoleID: in.RoleID,
Status: st,
Perms: authx.NormalizePerms(uniq(in.Perms)),
AppSlugs: uniq(in.AppSlugs),
CreatedBy: createdBy,
CreatedAt: time.Now().UTC(),
AgentID: s.seq,
TenantID: tenantID,
Name: strings.TrimSpace(in.Name),
ClientID: cid,
HostKey: strings.TrimSpace(in.HostKey),
RoleID: in.RoleID,
Status: st,
Perms: authx.NormalizePerms(uniq(in.Perms)),
AppSlugs: uniq(in.AppSlugs),
ChannelID: strings.TrimSpace(in.ChannelID),
OnlineDBID: strings.TrimSpace(in.OnlineDBID),
DatabaseName: strings.TrimSpace(in.DatabaseName),
CreatedBy: createdBy,
CreatedAt: time.Now().UTC(),
},
SecretHash: string(hash),
}
@@ -214,6 +227,15 @@ func (s *MemoryStore) Update(_ context.Context, tenantID, agentID int64, in Upda
if in.RoleID != nil {
a.RoleID = *in.RoleID
}
if in.ChannelID != nil {
a.ChannelID = strings.TrimSpace(*in.ChannelID)
}
if in.OnlineDBID != nil {
a.OnlineDBID = strings.TrimSpace(*in.OnlineDBID)
}
if in.DatabaseName != nil {
a.DatabaseName = strings.TrimSpace(*in.DatabaseName)
}
cp := cloneAcc(&a.Account)
return &cp, nil
}
@@ -327,7 +349,9 @@ func NewPostgresStore(db *sql.DB) *PostgresStore { return &PostgresStore{DB: db}
func (s *PostgresStore) List(ctx context.Context, tenantID int64) ([]Account, error) {
rows, err := s.DB.QueryContext(ctx, `
SELECT agent_id, tenant_id, name, client_id, COALESCE(host_key,''), COALESCE(role_id,0), status, created_by, created_at, last_token_at
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
ORDER BY CASE status WHEN 'pending' THEN 0 WHEN 'active' THEN 1 ELSE 2 END, agent_id`, tenantID)
if err != nil {
@@ -338,7 +362,9 @@ ORDER BY CASE status WHEN 'pending' THEN 0 WHEN 'active' THEN 1 ELSE 2 END, agen
for rows.Next() {
var a Account
var last sql.NullTime
if err := rows.Scan(&a.AgentID, &a.TenantID, &a.Name, &a.ClientID, &a.HostKey, &a.RoleID, &a.Status, &a.CreatedBy, &a.CreatedAt, &last); err != nil {
if err := rows.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); err != nil {
return nil, err
}
if last.Valid {
@@ -359,9 +385,13 @@ func (s *PostgresStore) Get(ctx context.Context, tenantID, agentID int64) (*Acco
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), status, created_by, created_at, last_token_at
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 agent_id=$1 AND tenant_id=$2`, agentID, tenantID,
).Scan(&a.AgentID, &a.TenantID, &a.Name, &a.ClientID, &a.HostKey, &a.RoleID, &a.Status, &a.CreatedBy, &a.CreatedAt, &last)
).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 errors.Is(err, sql.ErrNoRows) {
return nil, fmt.Errorf("agent not found")
}
@@ -390,9 +420,13 @@ func (s *PostgresStore) Register(ctx context.Context, tenantID int64, name, host
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), status, created_by, created_at, last_token_at
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.Status, &a.CreatedBy, &a.CreatedAt, &last)
).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)
@@ -439,11 +473,16 @@ func (s *PostgresStore) insert(ctx context.Context, tenantID, createdBy int64, i
}
var a Account
err = s.DB.QueryRowContext(ctx, `
INSERT INTO platform_meta.agent_accounts(tenant_id, name, client_id, client_secret_hash, status, created_by, host_key, role_id)
VALUES($1,$2,$3,$4,$5,$6,$7,NULLIF($8,0))
RETURNING agent_id, tenant_id, name, client_id, COALESCE(host_key,''), COALESCE(role_id,0), status, created_by, created_at`,
INSERT INTO platform_meta.agent_accounts(tenant_id, name, client_id, client_secret_hash, status, created_by, host_key, role_id, channel_id, online_db_id, database_name)
VALUES($1,$2,$3,$4,$5,$6,$7,NULLIF($8,0),$9,$10,$11)
RETURNING 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`,
tenantID, name, cid, string(hash), st, createdBy, strings.TrimSpace(in.HostKey), in.RoleID,
).Scan(&a.AgentID, &a.TenantID, &a.Name, &a.ClientID, &a.HostKey, &a.RoleID, &a.Status, &a.CreatedBy, &a.CreatedAt)
strings.TrimSpace(in.ChannelID), strings.TrimSpace(in.OnlineDBID), strings.TrimSpace(in.DatabaseName),
).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)
if err != nil {
return nil, "", err
}
@@ -462,6 +501,7 @@ func (s *PostgresStore) Update(ctx context.Context, tenantID, agentID int64, in
}
name, status := a.Name, a.Status
roleID := a.RoleID
channelID, onlineDBID, dbName := a.ChannelID, a.OnlineDBID, a.DatabaseName
if in.Name != nil {
name = strings.TrimSpace(*in.Name)
}
@@ -474,9 +514,20 @@ func (s *PostgresStore) Update(ctx context.Context, tenantID, agentID int64, in
if in.RoleID != nil {
roleID = *in.RoleID
}
if in.ChannelID != nil {
channelID = strings.TrimSpace(*in.ChannelID)
}
if in.OnlineDBID != nil {
onlineDBID = strings.TrimSpace(*in.OnlineDBID)
}
if in.DatabaseName != nil {
dbName = strings.TrimSpace(*in.DatabaseName)
}
if _, err := s.DB.ExecContext(ctx, `
UPDATE platform_meta.agent_accounts SET name=$1, status=$2, role_id=NULLIF($3,0) WHERE agent_id=$4 AND tenant_id=$5`,
name, status, roleID, agentID, tenantID); err != nil {
UPDATE platform_meta.agent_accounts SET name=$1, status=$2, role_id=NULLIF($3,0),
channel_id=$4, online_db_id=$5, database_name=$6
WHERE agent_id=$7 AND tenant_id=$8`,
name, status, roleID, channelID, onlineDBID, dbName, agentID, tenantID); err != nil {
return nil, err
}
perms, slugs := a.Perms, a.AppSlugs
@@ -525,9 +576,13 @@ func (s *PostgresStore) Authenticate(ctx context.Context, clientID, clientSecret
var hash string
var last sql.NullTime
err := s.DB.QueryRowContext(ctx, `
SELECT agent_id, tenant_id, name, client_id, COALESCE(host_key,''), COALESCE(role_id,0), client_secret_hash, status, created_by, created_at, last_token_at
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,''),
client_secret_hash, status, created_by, created_at, last_token_at
FROM platform_meta.agent_accounts WHERE client_id=$1`, clientID,
).Scan(&a.AgentID, &a.TenantID, &a.Name, &a.ClientID, &a.HostKey, &a.RoleID, &hash, &a.Status, &a.CreatedBy, &a.CreatedAt, &last)
).Scan(&a.AgentID, &a.TenantID, &a.Name, &a.ClientID, &a.HostKey, &a.RoleID,
&a.ChannelID, &a.OnlineDBID, &a.DatabaseName,
&hash, &a.Status, &a.CreatedBy, &a.CreatedAt, &last)
if errors.Is(err, sql.ErrNoRows) {
return nil, fmt.Errorf("invalid client credentials")
}