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

@@ -0,0 +1,184 @@
package dbsync
import (
"context"
"fmt"
"strings"
)
// SchemaTableSpec 本机 → 线上:空表/表结构 ensure 请求中的一张表。
type SchemaTableSpec struct {
Table string `json:"table"`
PKColumn string `json:"pk_column,omitempty"`
Columns []string `json:"columns"`
OnlineDBID string `json:"online_db_id,omitempty"`
}
// SchemaEnsureRequest agent 批量 ensure 空表到线上 A。
type SchemaEnsureRequest struct {
OnlineDBID string `json:"online_db_id"`
Tables []SchemaTableSpec `json:"tables"`
}
// SchemaEnsureItemResult 单表 ensure 结果。
type SchemaEnsureItemResult struct {
Table string `json:"table"`
OK bool `json:"ok"`
Created bool `json:"created"` // true=本次新建false=已存在跳过
Message string `json:"message,omitempty"`
}
// SchemaEnsureResult 批量 ensure 响应。
type SchemaEnsureResult struct {
OK bool `json:"ok"`
Results []SchemaEnsureItemResult `json:"results"`
Message string `json:"message,omitempty"`
}
// SchemaDescribeRequest 从线上 A 拉取表结构(供本机建空表)。
type SchemaDescribeRequest struct {
OnlineDBID string `json:"online_db_id"`
Tables []string `json:"tables,omitempty"` // 空=全部业务表
}
// SchemaTableDesc 线上表结构描述。
type SchemaTableDesc struct {
Name string `json:"name"`
PKColumn string `json:"pk_column"`
Columns []string `json:"columns"`
RowCount int64 `json:"row_count"`
}
// SchemaDescribeResult 表结构列表。
type SchemaDescribeResult struct {
OK bool `json:"ok"`
Tables []SchemaTableDesc `json:"tables"`
Message string `json:"message,omitempty"`
}
const MaxSchemaEnsureTables = 100
// EnsureSchemasOnRemote 在线上 A 为每张表 CREATE IF NOT EXISTS空表也建
func EnsureSchemasOnRemote(ctx context.Context, ch *Channel, req SchemaEnsureRequest) (*SchemaEnsureResult, error) {
if ch == nil {
return nil, fmt.Errorf("channel is nil")
}
if len(req.Tables) == 0 {
return nil, fmt.Errorf("tables required")
}
if len(req.Tables) > MaxSchemaEnsureTables {
return nil, fmt.Errorf("tables limit %d", MaxSchemaEnsureTables)
}
db, err := AcquireRemote(ch.Remote.Driver, ch.Remote.DSN)
if err != nil {
return nil, wrapOpenRemote(err)
}
if err := EnsureMeta(ctx, db, ch.Remote.Driver); err != nil {
return nil, Retryablef("ensure meta: %v", err)
}
out := &SchemaEnsureResult{OK: true, Results: make([]SchemaEnsureItemResult, 0, len(req.Tables))}
for _, spec := range req.Tables {
table := strings.TrimSpace(spec.Table)
item := SchemaEnsureItemResult{Table: table}
if table == "" {
item.Message = "table required"
out.OK = false
out.Results = append(out.Results, item)
continue
}
if !tableInChannel(ch, table) {
item.Message = "table rejected"
out.OK = false
out.Results = append(out.Results, item)
continue
}
pkCol := strings.TrimSpace(spec.PKColumn)
if pkCol == "" && ch.PKColumns != nil && strings.TrimSpace(ch.PKColumns[table]) != "" {
pkCol = ch.PKColumns[table]
}
if pkCol == "" {
pkCol = "id"
}
existed, err := tableExists(ctx, db, ch.Remote.Driver, table)
if err != nil {
return nil, Retryablef("table exists: %v", err)
}
if err := EnsureTableFromColumns(ctx, db, ch.Remote.Driver, table, pkCol, spec.Columns); err != nil {
item.Message = err.Error()
out.OK = false
out.Results = append(out.Results, item)
continue
}
item.OK = true
item.Created = !existed
if existed {
item.Message = "already exists"
} else {
item.Message = "created"
}
out.Results = append(out.Results, item)
}
out.Message = "empty tables ensured on online A; client should also create missing local tables from schema describe"
return out, nil
}
// DescribeSchemasFromRemote 列出线上 A 业务表结构(含空表),供本机 CREATE IF NOT EXISTS。
func DescribeSchemasFromRemote(ctx context.Context, ch *Channel, req SchemaDescribeRequest) (*SchemaDescribeResult, error) {
if ch == nil {
return nil, fmt.Errorf("channel is nil")
}
db, err := AcquireRemote(ch.Remote.Driver, ch.Remote.DSN)
if err != nil {
return nil, wrapOpenRemote(err)
}
if err := EnsureMeta(ctx, db, ch.Remote.Driver); err != nil {
return nil, Retryablef("ensure meta: %v", err)
}
want := map[string]struct{}{}
for _, t := range req.Tables {
t = strings.TrimSpace(t)
if t != "" {
want[t] = struct{}{}
}
}
names, err := ListTables(ctx, db, ch.Remote.Driver)
if err != nil {
return nil, Retryablef("list tables: %v", err)
}
out := &SchemaDescribeResult{OK: true, Tables: make([]SchemaTableDesc, 0, len(names))}
for _, name := range names {
if len(want) > 0 {
if _, ok := want[name]; !ok {
continue
}
}
if !tableInChannel(ch, name) {
continue
}
pkCol := "id"
if ch.PKColumns != nil && strings.TrimSpace(ch.PKColumns[name]) != "" {
pkCol = ch.PKColumns[name]
}
cols, err := listColumns(ctx, db, ch.Remote.Driver, name)
if err != nil {
return nil, Retryablef("columns %s: %v", name, err)
}
n, _ := countRows(ctx, db, ch.Remote.Driver, name)
out.Tables = append(out.Tables, SchemaTableDesc{
Name: name,
PKColumn: pkCol,
Columns: cols,
RowCount: n,
})
}
if len(out.Tables) == 0 {
out.Message = "online A has no business tables yet; push or ensure-schema first"
} else {
out.Message = "use columns to CREATE TABLE IF NOT EXISTS on local B (including empty tables)"
}
return out, nil
}