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:
@@ -14,6 +14,7 @@ import (
|
|||||||
"golang.org/x/crypto/bcrypt"
|
"golang.org/x/crypto/bcrypt"
|
||||||
|
|
||||||
"aijianzhan/platform/internal/authx"
|
"aijianzhan/platform/internal/authx"
|
||||||
|
"aijianzhan/platform/internal/schema"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -259,7 +260,7 @@ func (s *MemoryStore) Update(_ context.Context, tenantID, agentID int64, in Upda
|
|||||||
a.OnlineDBID = strings.TrimSpace(*in.OnlineDBID)
|
a.OnlineDBID = strings.TrimSpace(*in.OnlineDBID)
|
||||||
}
|
}
|
||||||
if in.DatabaseName != nil {
|
if in.DatabaseName != nil {
|
||||||
a.DatabaseName = strings.TrimSpace(*in.DatabaseName)
|
a.DatabaseName = schema.SafeSyncDatabaseName(*in.DatabaseName, agentID, 0)
|
||||||
}
|
}
|
||||||
cp := cloneAcc(&a.Account)
|
cp := cloneAcc(&a.Account)
|
||||||
return &cp, nil
|
return &cp, nil
|
||||||
@@ -394,7 +395,8 @@ func (s *MemoryStore) AttachSyncBind(_ context.Context, agentID, tenantID int64,
|
|||||||
}
|
}
|
||||||
a.ChannelID = strings.TrimSpace(channelID)
|
a.ChannelID = strings.TrimSpace(channelID)
|
||||||
a.OnlineDBID = strings.TrimSpace(onlineDBID)
|
a.OnlineDBID = strings.TrimSpace(onlineDBID)
|
||||||
a.DatabaseName = strings.TrimSpace(databaseName)
|
// Z35:禁止中文 DisplayName 等非法串写入物理库名
|
||||||
|
a.DatabaseName = schema.SafeSyncDatabaseName(databaseName, agentID, 0)
|
||||||
if activate {
|
if activate {
|
||||||
a.Status = StatusActive
|
a.Status = StatusActive
|
||||||
}
|
}
|
||||||
@@ -592,7 +594,7 @@ func (s *PostgresStore) Update(ctx context.Context, tenantID, agentID int64, in
|
|||||||
onlineDBID = strings.TrimSpace(*in.OnlineDBID)
|
onlineDBID = strings.TrimSpace(*in.OnlineDBID)
|
||||||
}
|
}
|
||||||
if in.DatabaseName != nil {
|
if in.DatabaseName != nil {
|
||||||
dbName = strings.TrimSpace(*in.DatabaseName)
|
dbName = schema.SafeSyncDatabaseName(*in.DatabaseName, agentID, 0)
|
||||||
}
|
}
|
||||||
if _, err := s.DB.ExecContext(ctx, `
|
if _, err := s.DB.ExecContext(ctx, `
|
||||||
UPDATE platform_meta.agent_accounts SET name=$1, status=$2, role_id=NULLIF($3,0),
|
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) {
|
func (s *PostgresStore) AttachSyncBind(ctx context.Context, agentID, tenantID int64, channelID, onlineDBID, databaseName string, activate bool) (*Account, error) {
|
||||||
statusSQL := ""
|
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 {
|
if activate {
|
||||||
statusSQL = ", status='active'"
|
statusSQL = ", status='active'"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import (
|
|||||||
|
|
||||||
"aijianzhan/platform/internal/agentstore"
|
"aijianzhan/platform/internal/agentstore"
|
||||||
"aijianzhan/platform/internal/dbsync"
|
"aijianzhan/platform/internal/dbsync"
|
||||||
|
"aijianzhan/platform/internal/schema"
|
||||||
"aijianzhan/platform/internal/svc"
|
"aijianzhan/platform/internal/svc"
|
||||||
"aijianzhan/platform/internal/types"
|
"aijianzhan/platform/internal/types"
|
||||||
"aijianzhan/platform/internal/userstore"
|
"aijianzhan/platform/internal/userstore"
|
||||||
@@ -19,6 +20,9 @@ func fillAgentSyncOnToken(resp *types.TokenResp, acc *agentstore.Account) {
|
|||||||
resp.ChannelID = strings.TrimSpace(acc.ChannelID)
|
resp.ChannelID = strings.TrimSpace(acc.ChannelID)
|
||||||
resp.OnlineDBID = strings.TrimSpace(acc.OnlineDBID)
|
resp.OnlineDBID = strings.TrimSpace(acc.OnlineDBID)
|
||||||
resp.DatabaseName = strings.TrimSpace(acc.DatabaseName)
|
resp.DatabaseName = strings.TrimSpace(acc.DatabaseName)
|
||||||
|
if resp.DatabaseName != "" && !schema.ValidDBName(resp.DatabaseName) {
|
||||||
|
resp.DatabaseName = ""
|
||||||
|
}
|
||||||
resp.SyncBound = resp.ChannelID != "" && resp.OnlineDBID != ""
|
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.ChannelID = strings.TrimSpace(b.ChannelID)
|
||||||
resp.OnlineDBID = strings.TrimSpace(b.OnlineDBID)
|
resp.OnlineDBID = strings.TrimSpace(b.OnlineDBID)
|
||||||
|
// Z35:database_name 仅物理库名;勿把中文 DisplayName 填进该字段
|
||||||
resp.DatabaseName = strings.TrimSpace(b.DatabaseName)
|
resp.DatabaseName = strings.TrimSpace(b.DatabaseName)
|
||||||
if resp.DatabaseName == "" {
|
if resp.DatabaseName != "" && !schema.ValidDBName(resp.DatabaseName) {
|
||||||
resp.DatabaseName = strings.TrimSpace(b.DisplayName)
|
resp.DatabaseName = ""
|
||||||
}
|
}
|
||||||
resp.SyncBound = resp.ChannelID != "" && resp.OnlineDBID != ""
|
resp.SyncBound = resp.ChannelID != "" && resp.OnlineDBID != ""
|
||||||
}
|
}
|
||||||
@@ -92,10 +97,7 @@ func (l *AgentAdminLogic) ensureAgentSyncBind(acc *agentstore.Account) (*agentst
|
|||||||
if online == "" {
|
if online == "" {
|
||||||
online = dbsync.ResolveOnlineDBID("", ch.ID)
|
online = dbsync.ResolveOnlineDBID("", ch.ID)
|
||||||
}
|
}
|
||||||
dbName := strings.TrimSpace(updated.DatabaseName)
|
dbName := schema.SafeSyncDatabaseName(updated.DatabaseName, updated.AgentID, 0)
|
||||||
if dbName == "" {
|
|
||||||
dbName = fmt.Sprintf("agent_%d", updated.AgentID)
|
|
||||||
}
|
|
||||||
updated, err = st.AttachSyncBind(l.ctx, updated.AgentID, updated.TenantID, ch.ID, online, dbName, updated.Status == agentstore.StatusActive || updated.Status == "")
|
updated, err = st.AttachSyncBind(l.ctx, updated.AgentID, updated.TenantID, ch.ID, online, dbName, updated.Status == agentstore.StatusActive || updated.Status == "")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import (
|
|||||||
"aijianzhan/platform/internal/authx"
|
"aijianzhan/platform/internal/authx"
|
||||||
"aijianzhan/platform/internal/bindcodestore"
|
"aijianzhan/platform/internal/bindcodestore"
|
||||||
"aijianzhan/platform/internal/dbsync"
|
"aijianzhan/platform/internal/dbsync"
|
||||||
|
"aijianzhan/platform/internal/schema"
|
||||||
"aijianzhan/platform/internal/smsstore"
|
"aijianzhan/platform/internal/smsstore"
|
||||||
"aijianzhan/platform/internal/userstore"
|
"aijianzhan/platform/internal/userstore"
|
||||||
)
|
)
|
||||||
@@ -58,7 +59,18 @@ func (l *AuthLogic) CreateBindCode(req BindCodeCreateReq) (*bindcodestore.BindCo
|
|||||||
}
|
}
|
||||||
channelID := strings.TrimSpace(req.ChannelID)
|
channelID := strings.TrimSpace(req.ChannelID)
|
||||||
online := strings.TrimSpace(req.OnlineDBID)
|
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 {
|
if channelID == "" && l.svcCtx.DBSync != nil {
|
||||||
// Z12h:生成绑定码前确保默认通道存在(含误删自愈)
|
// Z12h:生成绑定码前确保默认通道存在(含误删自愈)
|
||||||
if healed, err := HealTenantSyncBind(l.ctx, l.svcCtx, tid); err != nil {
|
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 == "" {
|
if online == "" {
|
||||||
online = dbsync.ResolveOnlineDBID("", channelID)
|
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
|
exp := time.Duration(req.ExpiresHours) * time.Hour
|
||||||
return l.svcCtx.BindCodes.Create(l.ctx, tid, authx.UserID(l.ctx), bindcodestore.CreateInput{
|
return l.svcCtx.BindCodes.Create(l.ctx, tid, authx.UserID(l.ctx), bindcodestore.CreateInput{
|
||||||
ChannelID: channelID,
|
ChannelID: channelID,
|
||||||
@@ -83,7 +102,7 @@ func (l *AuthLogic) CreateBindCode(req BindCodeCreateReq) (*bindcodestore.BindCo
|
|||||||
DatabaseName: dbName,
|
DatabaseName: dbName,
|
||||||
MaxUses: req.MaxUses,
|
MaxUses: req.MaxUses,
|
||||||
ExpiresIn: exp,
|
ExpiresIn: exp,
|
||||||
Note: req.Note,
|
Note: note,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -151,10 +170,7 @@ func (l *AuthLogic) RedeemBindCode(req BindCodeRedeemReq) (*BindCodeRedeemResp,
|
|||||||
if online == "" {
|
if online == "" {
|
||||||
online = dbsync.ResolveOnlineDBID("", bc.ChannelID)
|
online = dbsync.ResolveOnlineDBID("", bc.ChannelID)
|
||||||
}
|
}
|
||||||
dbName := strings.TrimSpace(bc.DatabaseName)
|
dbName := schema.SafeSyncDatabaseName(bc.DatabaseName, acc.AgentID, 0)
|
||||||
if dbName == "" {
|
|
||||||
dbName = fmt.Sprintf("agent_%d", acc.AgentID)
|
|
||||||
}
|
|
||||||
updated, err := l.svcCtx.Agents.AttachSyncBind(l.ctx, acc.AgentID, bc.TenantID, bc.ChannelID, online, dbName, true)
|
updated, err := l.svcCtx.Agents.AttachSyncBind(l.ctx, acc.AgentID, bc.TenantID, bc.ChannelID, online, dbName, true)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import (
|
|||||||
|
|
||||||
"aijianzhan/platform/internal/agentstore"
|
"aijianzhan/platform/internal/agentstore"
|
||||||
"aijianzhan/platform/internal/dbsync"
|
"aijianzhan/platform/internal/dbsync"
|
||||||
|
"aijianzhan/platform/internal/schema"
|
||||||
"aijianzhan/platform/internal/svc"
|
"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 {
|
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)
|
cid := strings.TrimSpace(a.ChannelID)
|
||||||
alive := channelAlive(store, a.TenantID, cid)
|
alive := channelAlive(store, a.TenantID, cid)
|
||||||
if cid != "" {
|
if cid != "" {
|
||||||
@@ -111,11 +116,12 @@ func HealTenantSyncBind(ctx context.Context, svcCtx *svc.ServiceContext, tenantI
|
|||||||
if online == "" {
|
if online == "" {
|
||||||
online = dbsync.ResolveOnlineDBID("", ch.ID)
|
online = dbsync.ResolveOnlineDBID("", ch.ID)
|
||||||
}
|
}
|
||||||
dbName := strings.TrimSpace(a.DatabaseName)
|
cid := strings.TrimSpace(a.ChannelID)
|
||||||
if dbName == "" {
|
if !channelAlive(svcCtx.DBSync.Store(), tenantID, cid) {
|
||||||
dbName = fmt.Sprintf("agent_%d", a.AgentID)
|
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)
|
log.Printf("dbsync heal agent tenant=%d agent=%d: %v", tenantID, a.AgentID, e)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import (
|
|||||||
"aijianzhan/platform/internal/agentstore"
|
"aijianzhan/platform/internal/agentstore"
|
||||||
"aijianzhan/platform/internal/authx"
|
"aijianzhan/platform/internal/authx"
|
||||||
"aijianzhan/platform/internal/dbsync"
|
"aijianzhan/platform/internal/dbsync"
|
||||||
|
"aijianzhan/platform/internal/schema"
|
||||||
"aijianzhan/platform/internal/types"
|
"aijianzhan/platform/internal/types"
|
||||||
"aijianzhan/platform/internal/userstore"
|
"aijianzhan/platform/internal/userstore"
|
||||||
"aijianzhan/platform/internal/yuhticket"
|
"aijianzhan/platform/internal/yuhticket"
|
||||||
@@ -191,12 +192,12 @@ func (l *AuthLogic) bindUserHostSync(u *userstore.User, hostKey, name, localID,
|
|||||||
return nil, "", fmt.Errorf("host_key 已绑定其它公司")
|
return nil, "", fmt.Errorf("host_key 已绑定其它公司")
|
||||||
}
|
}
|
||||||
online := fmt.Sprintf("%s_u%d", dbsync.ResolveOnlineDBID("", ch.ID), u.UserID)
|
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)
|
display := strings.TrimSpace(u.DisplayName)
|
||||||
if display == "" {
|
if display == "" {
|
||||||
display = fmt.Sprintf("user_%d", u.UserID)
|
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)
|
updated, err := l.svcCtx.Agents.AttachSyncBind(l.ctx, acc.AgentID, u.TenantID, ch.ID, online, dbName, true)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, "", err
|
return nil, "", err
|
||||||
@@ -215,7 +216,7 @@ func (l *AuthLogic) bindUserHostSync(u *userstore.User, hostKey, name, localID,
|
|||||||
LocalDatabaseID: localID,
|
LocalDatabaseID: localID,
|
||||||
OnlineDBID: online,
|
OnlineDBID: online,
|
||||||
ChannelID: ch.ID,
|
ChannelID: ch.ID,
|
||||||
DatabaseName: dbName,
|
DatabaseName: display, // 可读名(可中文)
|
||||||
DisplayName: display,
|
DisplayName: display,
|
||||||
Note: note,
|
Note: note,
|
||||||
})
|
})
|
||||||
|
|||||||
33
platform/internal/schema/dbname_test.go
Normal file
33
platform/internal/schema/dbname_test.go
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,11 +2,14 @@ package schema
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"crypto/sha256"
|
||||||
"database/sql"
|
"database/sql"
|
||||||
|
"encoding/hex"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/url"
|
"net/url"
|
||||||
"regexp"
|
"regexp"
|
||||||
"strings"
|
"strings"
|
||||||
|
"unicode"
|
||||||
)
|
)
|
||||||
|
|
||||||
var dbNameRe = regexp.MustCompile(`^[a-z][a-z0-9_]{1,47}$`)
|
var dbNameRe = regexp.MustCompile(`^[a-z][a-z0-9_]{1,47}$`)
|
||||||
@@ -108,3 +111,52 @@ func SafePhysicalDBName(preferred, fallback string) string {
|
|||||||
}
|
}
|
||||||
return "appdb_default"
|
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
|
||||||
|
}
|
||||||
|
|||||||
@@ -30,7 +30,7 @@
|
|||||||
| **Z23 表单推送失败禁止空等** | **宇恒已改完;智建无需** | `ask_fields_via_chat` 推送失败立即抛错;进行中再选中会话给出提示;对齐 ty_host≥1.2.35(§5.21) |
|
| **Z23 表单推送失败禁止空等** | **宇恒已改完;智建无需** | `ask_fields_via_chat` 推送失败立即抛错;进行中再选中会话给出提示;对齐 ty_host≥1.2.35(§5.21) |
|
||||||
| **Z24 已绑定勿 force 清落点** | **宇恒已改完;智建无需** | 启动自动引导绑定时禁止 `force_rebind` 清 CHANNEL;仅用户点「换绑」才清(§5.22) |
|
| **Z24 已绑定勿 force 清落点** | **宇恒已改完;智建无需** | 启动自动引导绑定时禁止 `force_rebind` 清 CHANNEL;仅用户点「换绑」才清(§5.22) |
|
||||||
| **Z34 / Z34b 换机恢复** | **宇恒半程 + 智建已落实** | 有手机静默恢复(Z34);无手机 `POST …/restore-by-host`(Z34b · §5.32) |
|
| **Z34 / Z34b 换机恢复** | **宇恒半程 + 智建已落实** | 有手机静默恢复(Z34);无手机 `POST …/restore-by-host`(Z34b · §5.32) |
|
||||||
| **Z35 库名非法中文** | **智建 · 意见已写清 · 生产须 pull** | DisplayName 当 PG 库名 → 400「婷婷管理员」;§5.33;**宇恒勿改** |
|
| **Z35 库名非法中文** | **智建已落实(须生产 pull)** | 绑定/AttachSyncBind/heal 纠正;publish 拒非法名;§5.33;**宇恒勿改** |
|
||||||
| **仍关注** | 用法 | 通道断了靠智建自愈;表数据靠宇恒双向指纹 / 数据恢复;不是「再点启动」 |
|
| **仍关注** | 用法 | 通道断了靠智建自愈;表数据靠宇恒双向指纹 / 数据恢复;不是「再点启动」 |
|
||||||
|
|
||||||
### 0.0 修改流程(冻结)
|
### 0.0 修改流程(冻结)
|
||||||
@@ -1086,7 +1086,7 @@ POST /api/v1/agent/sync/channels/{id}/pull
|
|||||||
|
|
||||||
### 5.33 【Z35 · 2026-08-07】绑定勿用中文 DisplayName 作 Postgres 库名
|
### 5.33 【Z35 · 2026-08-07】绑定勿用中文 DisplayName 作 Postgres 库名
|
||||||
|
|
||||||
**状态**:**智建负责(须生产 pull)· 宇恒勿改代码**。
|
**状态**:**智建已落实(须生产 `bash ./restart.sh --pull`)· 宇恒勿改代码**。
|
||||||
联调复现仍指向生产 `aisite…`:`HTTP 400: ensure database: invalid database name: 婷婷管理员` —— **在生产未 pull 前会继续报错**,不是宇恒终端问题。
|
联调复现仍指向生产 `aisite…`:`HTTP 400: ensure database: invalid database name: 婷婷管理员` —— **在生产未 pull 前会继续报错**,不是宇恒终端问题。
|
||||||
|
|
||||||
#### 现象
|
#### 现象
|
||||||
@@ -1099,7 +1099,7 @@ POST /api/v1/agent/sync/channels/{id}/pull
|
|||||||
|
|
||||||
#### 根因
|
#### 根因
|
||||||
|
|
||||||
1. 绑定路径 `bindUserHostSync` 把成员 **DisplayName(可中文)** 写入智能体 `database_name`。
|
1. 绑定路径曾把成员 **DisplayName(可中文)** 写入智能体 `database_name`。
|
||||||
2. 发布 Z8c:智能体带 `database_name` 时走 `database_per_app` → `EnsureDatabase` → Postgres `CREATE DATABASE`。
|
2. 发布 Z8c:智能体带 `database_name` 时走 `database_per_app` → `EnsureDatabase` → Postgres `CREATE DATABASE`。
|
||||||
3. 物理库名校验:`^[a-z][a-z0-9_]{1,47}$` —— 中文昵称非法 → 400。
|
3. 物理库名校验:`^[a-z][a-z0-9_]{1,47}$` —— 中文昵称非法 → 400。
|
||||||
|
|
||||||
@@ -1107,18 +1107,19 @@ POST /api/v1/agent/sync/channels/{id}/pull
|
|||||||
|
|
||||||
| 方 | 做什么 | 不做什么 |
|
| 方 | 做什么 | 不做什么 |
|
||||||
|----|--------|----------|
|
|----|--------|----------|
|
||||||
| **智建** | 改绑定落点库名规则 + publish 遇非法名纠正;**生产 `bash ./restart.sh --pull`** | — |
|
| **智建** | 绑定落点库名规则 + AttachSyncBind/heal 纠正脏数据 + publish 遇非法名纠正;**生产 pull** | — |
|
||||||
| **宇恒** | **不改**;意见对齐与复测 | 不在终端侧绕过/改写智建 `database_name` |
|
| **宇恒** | **不改**;意见对齐与复测 | 不在终端侧绕过/改写智建 `database_name` |
|
||||||
|
|
||||||
#### 智建改法(意见约定 · 实现要点)
|
#### 智建改法(已落实)
|
||||||
|
|
||||||
1. **绑定**:`Agents.DatabaseName` / Binding 物理名用 `user_{userID}`(或同等合法 ASCII);`Binding.DisplayName` 仍用昵称「婷婷管理员」。
|
1. **中文可接受(展示层)**:昵称/库可读名可中文,写入 `Binding.database_name` / `display_name`。
|
||||||
2. **publish**:仅当 `ValidDBName(acc.DatabaseName)` 才用于 Ensure;否则回落 `AssignDatabaseName` / `SanitizeDBName`,并**回写纠正**智能体脏 `database_name`,避免反复 400。
|
2. **物理层映射**:`Agents.database_name` 仅 ASCII —— 有用户 → `user_{id}`(改昵称不换库);无用户的中文标签 → 稳定哈希 `db`+16hex。
|
||||||
3. 已存在 app 的非法 `existing.DatabaseName` **不得**继续沿用。
|
3. **AttachSyncBind / heal / publish**:只对物理名做 EnsureDatabase;非法历史脏数据自动纠正。
|
||||||
|
4. **换票 token**:不把中文 DisplayName 误当成物理库名下发。
|
||||||
|
|
||||||
#### 验收
|
#### 验收
|
||||||
|
|
||||||
1. 生产 pull 后,昵称含中文的已绑账号 → 新建/发布模块 **不再** 400。
|
1. 生产 pull 后,昵称含中文的已绑账号 → 打开 SyncPage/agents/me(触发 heal)或再发布 → **不再** 400。
|
||||||
2. 智能体 `database_name` 为合法 `a-z0-9_`;控制台展示名仍可为中文。
|
2. 智能体 `database_name` 为合法 `a-z0-9_`;控制台展示名仍可为中文。
|
||||||
3. 宇恒侧无需发版即可复测通过。
|
3. 宇恒侧无需发版即可复测通过。
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user