Keep Chinese on Binding for UI; Agents use user_{id} or db{hash}; sanitize on attach/heal/publish.
Co-authored-by: Cursor <cursoragent@cursor.com>
163 lines
4.2 KiB
Go
163 lines
4.2 KiB
Go
package schema
|
||
|
||
import (
|
||
"context"
|
||
"crypto/sha256"
|
||
"database/sql"
|
||
"encoding/hex"
|
||
"fmt"
|
||
"net/url"
|
||
"regexp"
|
||
"strings"
|
||
"unicode"
|
||
)
|
||
|
||
var dbNameRe = regexp.MustCompile(`^[a-z][a-z0-9_]{1,47}$`)
|
||
|
||
type Runner interface {
|
||
ExecDDL(ctx context.Context, stmts []string) error
|
||
EnsureDatabase(ctx context.Context, dbName string) error
|
||
}
|
||
|
||
type NoopRunner struct{}
|
||
|
||
func (NoopRunner) ExecDDL(context.Context, []string) error { return nil }
|
||
func (NoopRunner) EnsureDatabase(context.Context, string) error { return nil }
|
||
|
||
type PostgresRunner struct {
|
||
DB *sql.DB
|
||
AdminDSN string // 用于 CREATE DATABASE(连到 postgres 库)
|
||
}
|
||
|
||
func (r *PostgresRunner) ExecDDL(ctx context.Context, stmts []string) error {
|
||
tx, err := r.DB.BeginTx(ctx, nil)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
defer func() { _ = tx.Rollback() }()
|
||
for _, s := range stmts {
|
||
if _, err := tx.ExecContext(ctx, s); err != nil {
|
||
return fmt.Errorf("ddl failed: %w\nsql: %s", err, s)
|
||
}
|
||
}
|
||
return tx.Commit()
|
||
}
|
||
|
||
func (r *PostgresRunner) EnsureDatabase(ctx context.Context, dbName string) error {
|
||
if !dbNameRe.MatchString(dbName) {
|
||
return fmt.Errorf("invalid database name: %s", dbName)
|
||
}
|
||
admin := r.DB
|
||
var err error
|
||
if r.AdminDSN != "" {
|
||
admin, err = sql.Open("postgres", r.AdminDSN)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
defer admin.Close()
|
||
}
|
||
var exists bool
|
||
if err := admin.QueryRowContext(ctx,
|
||
`SELECT EXISTS(SELECT 1 FROM pg_database WHERE datname=$1)`, dbName,
|
||
).Scan(&exists); err != nil {
|
||
return err
|
||
}
|
||
if exists {
|
||
return nil
|
||
}
|
||
// CREATE DATABASE 不能在事务中
|
||
_, err = admin.ExecContext(ctx, fmt.Sprintf(`CREATE DATABASE %s`, quoteIdent(dbName)))
|
||
return err
|
||
}
|
||
|
||
// DSNForDatabase 把原 DSN 的库名替换为目标库。
|
||
func DSNForDatabase(baseDSN, dbName string) (string, error) {
|
||
if !dbNameRe.MatchString(dbName) {
|
||
return "", fmt.Errorf("invalid database name")
|
||
}
|
||
u, err := url.Parse(baseDSN)
|
||
if err != nil {
|
||
return "", err
|
||
}
|
||
u.Path = "/" + dbName
|
||
return u.String(), nil
|
||
}
|
||
|
||
func QuoteIdentExport(name string) string { return quoteIdent(name) }
|
||
|
||
func SanitizeDBName(tenantID int64, slug string) string {
|
||
name := fmt.Sprintf("appdb_t%d_%s", tenantID, slug)
|
||
name = strings.ToLower(name)
|
||
if len(name) > 48 {
|
||
name = name[:48]
|
||
}
|
||
return name
|
||
}
|
||
|
||
// ValidDBName 是否可作为 Postgres 物理库名(CREATE DATABASE)。
|
||
func ValidDBName(dbName string) bool {
|
||
return dbNameRe.MatchString(strings.TrimSpace(dbName))
|
||
}
|
||
|
||
// SafePhysicalDBName 优先用 preferred;非法则用 fallback;仍非法则 user_fallback。
|
||
func SafePhysicalDBName(preferred, fallback string) string {
|
||
p := strings.TrimSpace(preferred)
|
||
if ValidDBName(p) {
|
||
return p
|
||
}
|
||
f := strings.TrimSpace(fallback)
|
||
if ValidDBName(f) {
|
||
return f
|
||
}
|
||
return "appdb_default"
|
||
}
|
||
|
||
// SafeSyncDatabaseName Z35:智能体同步落点的 Postgres 物理库名。
|
||
// - 已是合法 ASCII → 原样
|
||
// - 中文/非法串:有 userID → user_{id}(昵称可变仍稳定);否则对标签做稳定哈希 db{hex}
|
||
// - 展示中文请写 Binding.DisplayName / Binding.DatabaseName(可读名),勿把物理名当展示名
|
||
func SafeSyncDatabaseName(preferred string, agentID, userID int64) string {
|
||
p := strings.TrimSpace(preferred)
|
||
if ValidDBName(p) {
|
||
return p
|
||
}
|
||
// 有登录用户:物理名跟用户稳定绑定;中文昵称只作展示
|
||
if userID > 0 {
|
||
n := fmt.Sprintf("user_%d", userID)
|
||
if ValidDBName(n) {
|
||
return n
|
||
}
|
||
}
|
||
if p != "" {
|
||
return HashLabelDBName(p, agentID)
|
||
}
|
||
if agentID > 0 {
|
||
n := fmt.Sprintf("agent_%d", agentID)
|
||
if ValidDBName(n) {
|
||
return n
|
||
}
|
||
}
|
||
return "appdb_default"
|
||
}
|
||
|
||
// HashLabelDBName 将任意标签(含中文)稳定映射为合法物理库名:db + 16 hex。
|
||
func HashLabelDBName(label string, agentID int64) string {
|
||
label = strings.TrimSpace(label)
|
||
sum := sha256.Sum256([]byte(fmt.Sprintf("ajz-db:%d:%s", agentID, label)))
|
||
return "db" + hex.EncodeToString(sum[:8])
|
||
}
|
||
|
||
// IsDisplayLabel 是否更像「展示名」而非物理库名(含非 ASCII / 大写 / 连字符等)。
|
||
func IsDisplayLabel(s string) bool {
|
||
s = strings.TrimSpace(s)
|
||
if s == "" || ValidDBName(s) {
|
||
return false
|
||
}
|
||
for _, r := range s {
|
||
if r > unicode.MaxASCII || unicode.Is(unicode.Han, r) {
|
||
return true
|
||
}
|
||
}
|
||
return true
|
||
}
|