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>
This commit is contained in:
whm
2026-08-07 02:12:55 +08:00
parent 7290f91d3a
commit 4b6c90904b
8 changed files with 146 additions and 32 deletions

View File

@@ -14,6 +14,7 @@ import (
"golang.org/x/crypto/bcrypt"
"aijianzhan/platform/internal/authx"
"aijianzhan/platform/internal/schema"
)
const (
@@ -259,7 +260,7 @@ func (s *MemoryStore) Update(_ context.Context, tenantID, agentID int64, in Upda
a.OnlineDBID = strings.TrimSpace(*in.OnlineDBID)
}
if in.DatabaseName != nil {
a.DatabaseName = strings.TrimSpace(*in.DatabaseName)
a.DatabaseName = schema.SafeSyncDatabaseName(*in.DatabaseName, agentID, 0)
}
cp := cloneAcc(&a.Account)
return &cp, nil
@@ -394,7 +395,8 @@ func (s *MemoryStore) AttachSyncBind(_ context.Context, agentID, tenantID int64,
}
a.ChannelID = strings.TrimSpace(channelID)
a.OnlineDBID = strings.TrimSpace(onlineDBID)
a.DatabaseName = strings.TrimSpace(databaseName)
// Z35禁止中文 DisplayName 等非法串写入物理库名
a.DatabaseName = schema.SafeSyncDatabaseName(databaseName, agentID, 0)
if activate {
a.Status = StatusActive
}
@@ -592,7 +594,7 @@ func (s *PostgresStore) Update(ctx context.Context, tenantID, agentID int64, in
onlineDBID = strings.TrimSpace(*in.OnlineDBID)
}
if in.DatabaseName != nil {
dbName = strings.TrimSpace(*in.DatabaseName)
dbName = schema.SafeSyncDatabaseName(*in.DatabaseName, agentID, 0)
}
if _, err := s.DB.ExecContext(ctx, `
UPDATE platform_meta.agent_accounts SET name=$1, status=$2, role_id=NULLIF($3,0),
@@ -760,7 +762,8 @@ ORDER BY agent_id DESC LIMIT 1`, hostKey,
func (s *PostgresStore) AttachSyncBind(ctx context.Context, agentID, tenantID int64, channelID, onlineDBID, databaseName string, activate bool) (*Account, error) {
statusSQL := ""
args := []any{tenantID, strings.TrimSpace(channelID), strings.TrimSpace(onlineDBID), strings.TrimSpace(databaseName), agentID}
safeDB := schema.SafeSyncDatabaseName(databaseName, agentID, 0)
args := []any{tenantID, strings.TrimSpace(channelID), strings.TrimSpace(onlineDBID), safeDB, agentID}
if activate {
statusSQL = ", status='active'"
}

View File

@@ -6,6 +6,7 @@ import (
"aijianzhan/platform/internal/agentstore"
"aijianzhan/platform/internal/dbsync"
"aijianzhan/platform/internal/schema"
"aijianzhan/platform/internal/svc"
"aijianzhan/platform/internal/types"
"aijianzhan/platform/internal/userstore"
@@ -19,6 +20,9 @@ func fillAgentSyncOnToken(resp *types.TokenResp, acc *agentstore.Account) {
resp.ChannelID = strings.TrimSpace(acc.ChannelID)
resp.OnlineDBID = strings.TrimSpace(acc.OnlineDBID)
resp.DatabaseName = strings.TrimSpace(acc.DatabaseName)
if resp.DatabaseName != "" && !schema.ValidDBName(resp.DatabaseName) {
resp.DatabaseName = ""
}
resp.SyncBound = resp.ChannelID != "" && resp.OnlineDBID != ""
}
@@ -40,9 +44,10 @@ func fillUserSyncOnToken(svcCtx *svc.ServiceContext, resp *types.TokenResp, u *u
}
resp.ChannelID = strings.TrimSpace(b.ChannelID)
resp.OnlineDBID = strings.TrimSpace(b.OnlineDBID)
// Z35database_name 仅物理库名;勿把中文 DisplayName 填进该字段
resp.DatabaseName = strings.TrimSpace(b.DatabaseName)
if resp.DatabaseName == "" {
resp.DatabaseName = strings.TrimSpace(b.DisplayName)
if resp.DatabaseName != "" && !schema.ValidDBName(resp.DatabaseName) {
resp.DatabaseName = ""
}
resp.SyncBound = resp.ChannelID != "" && resp.OnlineDBID != ""
}
@@ -92,10 +97,7 @@ func (l *AgentAdminLogic) ensureAgentSyncBind(acc *agentstore.Account) (*agentst
if online == "" {
online = dbsync.ResolveOnlineDBID("", ch.ID)
}
dbName := strings.TrimSpace(updated.DatabaseName)
if dbName == "" {
dbName = fmt.Sprintf("agent_%d", updated.AgentID)
}
dbName := schema.SafeSyncDatabaseName(updated.DatabaseName, updated.AgentID, 0)
updated, err = st.AttachSyncBind(l.ctx, updated.AgentID, updated.TenantID, ch.ID, online, dbName, updated.Status == agentstore.StatusActive || updated.Status == "")
if err != nil {
return nil, err

View File

@@ -11,6 +11,7 @@ import (
"aijianzhan/platform/internal/authx"
"aijianzhan/platform/internal/bindcodestore"
"aijianzhan/platform/internal/dbsync"
"aijianzhan/platform/internal/schema"
"aijianzhan/platform/internal/smsstore"
"aijianzhan/platform/internal/userstore"
)
@@ -58,7 +59,18 @@ func (l *AuthLogic) CreateBindCode(req BindCodeCreateReq) (*bindcodestore.BindCo
}
channelID := strings.TrimSpace(req.ChannelID)
online := strings.TrimSpace(req.OnlineDBID)
dbName := strings.TrimSpace(req.DatabaseName)
rawDB := strings.TrimSpace(req.DatabaseName)
dbName := ""
displayNote := ""
if rawDB != "" {
if schema.ValidDBName(rawDB) {
dbName = rawDB
} else {
// 接受中文等展示名:落码存物理哈希;原文记入 note 便于对照
dbName = schema.HashLabelDBName(rawDB, 0)
displayNote = rawDB
}
}
if channelID == "" && l.svcCtx.DBSync != nil {
// Z12h生成绑定码前确保默认通道存在含误删自愈
if healed, err := HealTenantSyncBind(l.ctx, l.svcCtx, tid); err != nil {
@@ -76,6 +88,13 @@ func (l *AuthLogic) CreateBindCode(req BindCodeCreateReq) (*bindcodestore.BindCo
if online == "" {
online = dbsync.ResolveOnlineDBID("", channelID)
}
note := strings.TrimSpace(req.Note)
if displayNote != "" {
if note != "" {
note = note + " | "
}
note += "display:" + displayNote
}
exp := time.Duration(req.ExpiresHours) * time.Hour
return l.svcCtx.BindCodes.Create(l.ctx, tid, authx.UserID(l.ctx), bindcodestore.CreateInput{
ChannelID: channelID,
@@ -83,7 +102,7 @@ func (l *AuthLogic) CreateBindCode(req BindCodeCreateReq) (*bindcodestore.BindCo
DatabaseName: dbName,
MaxUses: req.MaxUses,
ExpiresIn: exp,
Note: req.Note,
Note: note,
})
}
@@ -151,10 +170,7 @@ func (l *AuthLogic) RedeemBindCode(req BindCodeRedeemReq) (*BindCodeRedeemResp,
if online == "" {
online = dbsync.ResolveOnlineDBID("", bc.ChannelID)
}
dbName := strings.TrimSpace(bc.DatabaseName)
if dbName == "" {
dbName = fmt.Sprintf("agent_%d", acc.AgentID)
}
dbName := schema.SafeSyncDatabaseName(bc.DatabaseName, acc.AgentID, 0)
updated, err := l.svcCtx.Agents.AttachSyncBind(l.ctx, acc.AgentID, bc.TenantID, bc.ChannelID, online, dbName, true)
if err != nil {
return nil, err

View File

@@ -9,6 +9,7 @@ import (
"aijianzhan/platform/internal/agentstore"
"aijianzhan/platform/internal/dbsync"
"aijianzhan/platform/internal/schema"
"aijianzhan/platform/internal/svc"
)
@@ -40,6 +41,10 @@ func channelAlive(store *dbsync.FileStore, tenantID int64, channelID string) boo
}
func agentNeedsSyncHeal(store *dbsync.FileStore, a agentstore.Account) bool {
// Z35非法物理库名如中文昵称也要纠正否则发布 EnsureDatabase 400
if dn := strings.TrimSpace(a.DatabaseName); dn != "" && !schema.ValidDBName(dn) {
return true
}
cid := strings.TrimSpace(a.ChannelID)
alive := channelAlive(store, a.TenantID, cid)
if cid != "" {
@@ -111,11 +116,12 @@ func HealTenantSyncBind(ctx context.Context, svcCtx *svc.ServiceContext, tenantI
if online == "" {
online = dbsync.ResolveOnlineDBID("", ch.ID)
}
dbName := strings.TrimSpace(a.DatabaseName)
if dbName == "" {
dbName = fmt.Sprintf("agent_%d", a.AgentID)
cid := strings.TrimSpace(a.ChannelID)
if !channelAlive(svcCtx.DBSync.Store(), tenantID, cid) {
cid = ch.ID
}
if _, e := svcCtx.Agents.AttachSyncBind(ctx, a.AgentID, tenantID, ch.ID, online, dbName, a.Status == agentstore.StatusActive || a.Status == ""); e != nil {
dbName := schema.SafeSyncDatabaseName(a.DatabaseName, a.AgentID, 0)
if _, e := svcCtx.Agents.AttachSyncBind(ctx, a.AgentID, tenantID, cid, online, dbName, a.Status == agentstore.StatusActive || a.Status == ""); e != nil {
log.Printf("dbsync heal agent tenant=%d agent=%d: %v", tenantID, a.AgentID, e)
continue
}

View File

@@ -8,6 +8,7 @@ import (
"aijianzhan/platform/internal/agentstore"
"aijianzhan/platform/internal/authx"
"aijianzhan/platform/internal/dbsync"
"aijianzhan/platform/internal/schema"
"aijianzhan/platform/internal/types"
"aijianzhan/platform/internal/userstore"
"aijianzhan/platform/internal/yuhticket"
@@ -191,12 +192,12 @@ func (l *AuthLogic) bindUserHostSync(u *userstore.User, hostKey, name, localID,
return nil, "", fmt.Errorf("host_key 已绑定其它公司")
}
online := fmt.Sprintf("%s_u%d", dbsync.ResolveOnlineDBID("", ch.ID), u.UserID)
// DisplayName 可中文Postgres 物理库名必须 [a-z][a-z0-9_]{1,47}Z35
// 中文昵称可接受:写入 Binding 展示字段;物理库名映射为 user_{id} / 哈希Z35
display := strings.TrimSpace(u.DisplayName)
if display == "" {
display = fmt.Sprintf("user_%d", u.UserID)
}
dbName := fmt.Sprintf("user_%d", u.UserID)
dbName := schema.SafeSyncDatabaseName(display, acc.AgentID, u.UserID)
updated, err := l.svcCtx.Agents.AttachSyncBind(l.ctx, acc.AgentID, u.TenantID, ch.ID, online, dbName, true)
if err != nil {
return nil, "", err
@@ -215,7 +216,7 @@ func (l *AuthLogic) bindUserHostSync(u *userstore.User, hostKey, name, localID,
LocalDatabaseID: localID,
OnlineDBID: online,
ChannelID: ch.ID,
DatabaseName: dbName,
DatabaseName: display, // 可读名(可中文)
DisplayName: display,
Note: note,
})

View File

@@ -0,0 +1,33 @@
package schema
import "testing"
func TestSafeSyncDatabaseName(t *testing.T) {
// 有用户:中文昵称 → 稳定 user_{id}(改昵称不换库)
if got := SafeSyncDatabaseName("婷婷管理员", 12, 34); got != "user_34" {
t.Fatalf("want user_34, got %s", got)
}
// 无用户:中文/非法标签 → 稳定哈希
h1 := SafeSyncDatabaseName("婷婷管理员", 12, 0)
h2 := SafeSyncDatabaseName("婷婷管理员", 12, 0)
if h1 != h2 || !ValidDBName(h1) || h1[:2] != "db" {
t.Fatalf("hash mapping bad: %s", h1)
}
if SafeSyncDatabaseName("另一中文", 12, 0) == h1 {
t.Fatal("different labels should hash differently")
}
if got := SafeSyncDatabaseName("ok_db", 1, 2); got != "ok_db" {
t.Fatalf("want ok_db, got %s", got)
}
if !ValidDBName("user_1") || ValidDBName("婷婷管理员") {
t.Fatal("ValidDBName mismatch")
}
}
func TestHashLabelDBNameStable(t *testing.T) {
a := HashLabelDBName("宇信达", 7)
b := HashLabelDBName("宇信达", 7)
if a != b || !ValidDBName(a) {
t.Fatalf("unstable or invalid: %s", a)
}
}

View File

@@ -2,11 +2,14 @@ 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}$`)
@@ -108,3 +111,52 @@ func SafePhysicalDBName(preferred, fallback string) string {
}
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
}