Files
ai_site/platform/internal/schema/runner.go
whm 4b6c90904b fix: Z35 map Chinese display names to stable physical DB names
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>
2026-08-07 02:12:55 +08:00

163 lines
4.2 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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
}