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,105 @@
package dbsync
import (
"context"
"database/sql"
"fmt"
"strings"
)
// EnsureTableFromRow 表不存在时按行字段自动建表TEXT 列 + PK满足 Z4「按 Binding 接受任意表」。
func EnsureTableFromRow(ctx context.Context, db *sql.DB, driver Driver, table, pkCol string, row map[string]any) error {
table = strings.TrimSpace(table)
pkCol = strings.TrimSpace(pkCol)
if table == "" || pkCol == "" {
return fmt.Errorf("table and pk required")
}
if row == nil {
row = map[string]any{pkCol: ""}
}
cols := make([]string, 0, len(row))
seen := map[string]struct{}{}
if _, ok := row[pkCol]; !ok {
cols = append(cols, pkCol)
seen[pkCol] = struct{}{}
}
for k := range row {
k = strings.TrimSpace(k)
if k == "" {
continue
}
if _, ok := seen[k]; ok {
continue
}
seen[k] = struct{}{}
cols = append(cols, k)
}
return EnsureTableFromColumns(ctx, db, driver, table, pkCol, cols)
}
// EnsureTableFromColumns 表不存在时按列名建空表(全部 TEXT指定 PK。已存在则幂等跳过。
// 用于空表结构同步:本机有空表 → 线上也建同名空表(无需 outbox 行)。
func EnsureTableFromColumns(ctx context.Context, db *sql.DB, driver Driver, table, pkCol string, columns []string) error {
table = strings.TrimSpace(table)
pkCol = strings.TrimSpace(pkCol)
if table == "" {
return fmt.Errorf("table required")
}
if strings.HasPrefix(table, "_ajz_") {
return fmt.Errorf("sync system table not allowed: %s", table)
}
if pkCol == "" {
pkCol = "id"
}
exists, err := tableExists(ctx, db, driver, table)
if err != nil {
return err
}
if exists {
return nil
}
seen := map[string]struct{}{}
defs := make([]string, 0, len(columns)+1)
defs = append(defs, fmt.Sprintf("%s TEXT PRIMARY KEY", quoteIdent(driver, pkCol)))
seen[pkCol] = struct{}{}
for _, c := range columns {
c = strings.TrimSpace(c)
if c == "" {
continue
}
if _, ok := seen[c]; ok {
continue
}
seen[c] = struct{}{}
defs = append(defs, fmt.Sprintf("%s TEXT", quoteIdent(driver, c)))
}
if len(defs) == 0 {
return fmt.Errorf("columns required")
}
ddl := fmt.Sprintf(`CREATE TABLE IF NOT EXISTS %s (%s)`, quoteIdent(driver, table), strings.Join(defs, ", "))
_, err = db.ExecContext(ctx, ddl)
return err
}
func tableExists(ctx context.Context, db *sql.DB, driver Driver, table string) (bool, error) {
var q string
switch driver {
case DriverSQLite:
q = `SELECT 1 FROM sqlite_master WHERE type='table' AND name=? LIMIT 1`
case DriverMySQL:
q = `SELECT 1 FROM information_schema.tables WHERE table_schema=DATABASE() AND table_name=? LIMIT 1`
case DriverPostgres:
q = `SELECT 1 FROM information_schema.tables WHERE table_schema=current_schema() AND table_name=$1 LIMIT 1`
default:
return false, fmt.Errorf("unsupported driver")
}
var n int
err := db.QueryRowContext(ctx, q, table).Scan(&n)
if err == sql.ErrNoRows {
return false, nil
}
if err != nil {
return false, err
}
return true, nil
}