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:
@@ -40,9 +40,7 @@ func PushToRemote(ctx context.Context, ch *Channel, store *FileStore, item PushI
|
||||
if table == "" {
|
||||
return nil, fmt.Errorf("table required")
|
||||
}
|
||||
if !tableInChannel(ch, table) {
|
||||
return nil, fmt.Errorf("table %s 不在通道白名单", table)
|
||||
}
|
||||
// Z4 冻结:通道表白名单不再作为拒收依据;Binding + JWT 才是权限源。
|
||||
pkCol := "id"
|
||||
if ch.PKColumns != nil && strings.TrimSpace(ch.PKColumns[table]) != "" {
|
||||
pkCol = ch.PKColumns[table]
|
||||
@@ -57,14 +55,21 @@ func PushToRemote(ctx context.Context, ch *Channel, store *FileStore, item PushI
|
||||
version = time.Now().UnixNano()
|
||||
}
|
||||
|
||||
db, err := Open(ch.Remote.Driver, ch.Remote.DSN)
|
||||
db, err := AcquireRemote(ch.Remote.Driver, ch.Remote.DSN)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("open remote: %w", err)
|
||||
return nil, wrapOpenRemote(err)
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
if err := EnsureMeta(ctx, db, ch.Remote.Driver); err != nil {
|
||||
return nil, fmt.Errorf("ensure meta: %w", err)
|
||||
return nil, Retryablef("ensure meta: %v", err)
|
||||
}
|
||||
|
||||
if op != "delete" {
|
||||
var row map[string]any
|
||||
_ = json.Unmarshal([]byte(payload), &row)
|
||||
if err := EnsureTableFromRow(ctx, db, ch.Remote.Driver, table, pkCol, row); err != nil {
|
||||
return nil, Retryablef("ensure table: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
res := &PushResult{
|
||||
@@ -74,13 +79,16 @@ func PushToRemote(ctx context.Context, ch *Channel, store *FileStore, item PushI
|
||||
|
||||
tgtVer, has, err := GetMetaVersion(ctx, db, ch.Remote.Driver, table, rowPK)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, Retryablef("meta version: %v", err)
|
||||
}
|
||||
// 幂等:同 version 已落地 → 跳过
|
||||
if has && tgtVer == version {
|
||||
res.OK = true
|
||||
res.Skipped = true
|
||||
res.Message = "already applied (same version)"
|
||||
if store != nil {
|
||||
_ = store.PatchStats(ch.ID, func(c *Channel) { c.Stats.PushedSkipped++ })
|
||||
}
|
||||
return res, nil
|
||||
}
|
||||
if has && tgtVer > version {
|
||||
@@ -109,11 +117,14 @@ func PushToRemote(ctx context.Context, ch *Channel, store *FileStore, item PushI
|
||||
res.OK = true
|
||||
res.Skipped = true
|
||||
res.Message = "target newer; kept (lww_target)"
|
||||
if store != nil {
|
||||
_ = store.PatchStats(ch.ID, func(c *Channel) { c.Stats.PushedSkipped++ })
|
||||
}
|
||||
return res, nil
|
||||
case PolicyLWWSource:
|
||||
loser := SnapshotTargetRow(ctx, db, ch.Remote.Driver, table, pkCol, rowPK)
|
||||
if err := ApplyChange(ctx, db, ch.Remote.Driver, table, pkCol, op, payload, version); err != nil {
|
||||
return nil, err
|
||||
return nil, Retryablef("apply: %v", err)
|
||||
}
|
||||
RecordLwwOverride(store, LwwOverride{
|
||||
TenantID: ch.TenantID,
|
||||
@@ -130,7 +141,10 @@ func PushToRemote(ctx context.Context, ch *Channel, store *FileStore, item PushI
|
||||
SourceVer: version,
|
||||
})
|
||||
if store != nil {
|
||||
_ = store.PatchStats(ch.ID, func(c *Channel) { c.Stats.PushedOK++ })
|
||||
_ = store.PatchStats(ch.ID, func(c *Channel) {
|
||||
c.Stats.PushedOK++
|
||||
c.Stats.PushedApplied++
|
||||
})
|
||||
}
|
||||
res.OK = true
|
||||
res.Applied = true
|
||||
@@ -150,7 +164,10 @@ func PushToRemote(ctx context.Context, ch *Channel, store *FileStore, item PushI
|
||||
SourceVer: version,
|
||||
Message: "target version newer than agent push",
|
||||
})
|
||||
_ = store.PatchStats(ch.ID, func(c *Channel) { c.Stats.Conflicts++ })
|
||||
_ = store.PatchStats(ch.ID, func(c *Channel) {
|
||||
c.Stats.Conflicts++
|
||||
c.Stats.PushedSkipped++
|
||||
})
|
||||
}
|
||||
res.OK = true
|
||||
res.Skipped = true
|
||||
@@ -161,10 +178,13 @@ func PushToRemote(ctx context.Context, ch *Channel, store *FileStore, item PushI
|
||||
}
|
||||
|
||||
if err := ApplyChange(ctx, db, ch.Remote.Driver, table, pkCol, op, payload, version); err != nil {
|
||||
return nil, err
|
||||
return nil, Retryablef("apply: %v", err)
|
||||
}
|
||||
if store != nil {
|
||||
_ = store.PatchStats(ch.ID, func(c *Channel) { c.Stats.PushedOK++ })
|
||||
_ = store.PatchStats(ch.ID, func(c *Channel) {
|
||||
c.Stats.PushedOK++
|
||||
c.Stats.PushedApplied++
|
||||
})
|
||||
}
|
||||
res.OK = true
|
||||
res.Applied = true
|
||||
@@ -192,12 +212,9 @@ func PushBatchToRemote(ctx context.Context, ch *Channel, store *FileStore, items
|
||||
}
|
||||
|
||||
func tableInChannel(ch *Channel, table string) bool {
|
||||
for _, t := range uniqueTables(ch.Local.Tables, ch.Remote.Tables) {
|
||||
if strings.EqualFold(strings.TrimSpace(t), table) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
// Z4 冻结:通道 tables 仅历史兼容展示,push/pull 不再据此拒收。
|
||||
_ = ch
|
||||
return strings.TrimSpace(table) != ""
|
||||
}
|
||||
|
||||
func normalizePushPayload(item PushItem, pkCol string) (op string, payload string, rowPK string, err error) {
|
||||
|
||||
Reference in New Issue
Block a user