fix: Z35 禁止用中文昵称作 Postgres 库名,发布时纠正脏数据

绑定 database_name 改为 user_{id};publish 遇非法库名 Sanitize 并回写,避免 EnsureDatabase 400。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
whm
2026-08-07 01:41:20 +08:00
parent d489169bec
commit a80559d353
4 changed files with 65 additions and 10 deletions

View File

@@ -7,9 +7,10 @@ import (
"strings" "strings"
"time" "time"
"aijianzhan/platform/internal/agentcap"
"aijianzhan/platform/internal/agentstore"
"aijianzhan/platform/internal/audit" "aijianzhan/platform/internal/audit"
"aijianzhan/platform/internal/authx" "aijianzhan/platform/internal/authx"
"aijianzhan/platform/internal/agentcap"
"aijianzhan/platform/internal/blueprint" "aijianzhan/platform/internal/blueprint"
"aijianzhan/platform/internal/meta" "aijianzhan/platform/internal/meta"
"aijianzhan/platform/internal/schema" "aijianzhan/platform/internal/schema"
@@ -264,16 +265,26 @@ func (l *PublishLogic) Publish(slug string, req *types.PublishReq) (*types.Publi
schemaName := bp.AssignSchemaName(tenantID) schemaName := bp.AssignSchemaName(tenantID)
dbName := bp.AssignDatabaseName(tenantID) dbName := bp.AssignDatabaseName(tenantID)
// Z8c智能体若绑定了 database_name新建模块优先落到该库database_per_app // Z8c智能体若绑定了合法 database_name新建模块优先落到该库database_per_app
// Z35中文昵称等非法名不得用于 EnsureDatabase回落 AssignDatabaseName
if authx.Role(l.ctx) == authx.RoleAgent && l.svcCtx.Agents != nil { if authx.Role(l.ctx) == authx.RoleAgent && l.svcCtx.Agents != nil {
if aid := authx.AgentID(l.ctx); aid > 0 { if aid := authx.AgentID(l.ctx); aid > 0 {
if acc, err := l.svcCtx.Agents.Get(l.ctx, tenantID, aid); err == nil && acc != nil { if acc, err := l.svcCtx.Agents.Get(l.ctx, tenantID, aid); err == nil && acc != nil {
if dn := strings.TrimSpace(acc.DatabaseName); dn != "" { if dn := strings.TrimSpace(acc.DatabaseName); dn != "" && schema.ValidDBName(dn) {
if !exists || existing.DatabaseName == "" { if !exists || existing.DatabaseName == "" {
bp.Storage.Mode = "database_per_app" bp.Storage.Mode = "database_per_app"
schemaName = bp.AssignSchemaName(tenantID) schemaName = bp.AssignSchemaName(tenantID)
dbName = dn dbName = dn
} }
} else if dn != "" && !schema.ValidDBName(dn) {
// 历史脏数据(如 DisplayName 当库名):纠正智能体落点库名,避免反复 400
safeName := schema.SanitizeDBName(tenantID, slug)
_, _ = l.svcCtx.Agents.Update(l.ctx, tenantID, aid, agentstore.UpdateInput{
DatabaseName: &safeName,
})
bp.Storage.Mode = "database_per_app"
schemaName = bp.AssignSchemaName(tenantID)
dbName = safeName
} }
} }
} }
@@ -294,7 +305,7 @@ func (l *PublishLogic) Publish(slug string, req *types.PublishReq) (*types.Publi
schemaName = existing.SchemaName schemaName = existing.SchemaName
bp.Storage.SchemaName = existing.SchemaName bp.Storage.SchemaName = existing.SchemaName
} }
if existing.DatabaseName != "" { if existing.DatabaseName != "" && schema.ValidDBName(existing.DatabaseName) {
dbName = existing.DatabaseName dbName = existing.DatabaseName
} }
ddl, err = schema.BuildPostgresDDL(bp) ddl, err = schema.BuildPostgresDDL(bp)

View File

@@ -191,10 +191,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)
dbName := strings.TrimSpace(u.DisplayName) // DisplayName 可中文Postgres 物理库名必须 [a-z][a-z0-9_]{1,47}Z35
if dbName == "" { display := strings.TrimSpace(u.DisplayName)
dbName = fmt.Sprintf("user_%d", u.UserID) if display == "" {
display = fmt.Sprintf("user_%d", u.UserID)
} }
dbName := fmt.Sprintf("user_%d", 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
@@ -214,7 +216,7 @@ func (l *AuthLogic) bindUserHostSync(u *userstore.User, hostKey, name, localID,
OnlineDBID: online, OnlineDBID: online,
ChannelID: ch.ID, ChannelID: ch.ID,
DatabaseName: dbName, DatabaseName: dbName,
DisplayName: dbName, DisplayName: display,
Note: note, Note: note,
}) })
return updated, secret, nil return updated, secret, nil

View File

@@ -90,3 +90,21 @@ func SanitizeDBName(tenantID int64, slug string) string {
} }
return name 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"
}

View File

@@ -30,6 +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 库名非法中文** | **智建已改完** | 绑定 DisplayName 不再作 PG 库名publish 纠正脏数据§5.33 |
| **仍关注** | 用法 | 通道断了靠智建自愈;表数据靠宇恒双向指纹 / 数据恢复;不是「再点启动」 | | **仍关注** | 用法 | 通道断了靠智建自愈;表数据靠宇恒双向指纹 / 数据恢复;不是「再点启动」 |
### 0.0 修改流程(冻结) ### 0.0 修改流程(冻结)
@@ -96,6 +97,7 @@
| **Z12h / Z12h-1 / Z12h-2** | `HealTenantSyncBind`Ensure 默认通道 + 重挂 Agent/Binding默认同步**可删**,删后立即 Ensure+heal列表/换票/`agents/me`/ensure Binding 触发;见 §5.9 | | **Z12h / Z12h-1 / Z12h-2** | `HealTenantSyncBind`Ensure 默认通道 + 重挂 Agent/Binding默认同步**可删**,删后立即 Ensure+heal列表/换票/`agents/me`/ensure Binding 触发;见 §5.9 |
| **数据恢复§5.13** | 成功同步后线上库快照latest+previousSyncPage「数据恢复」`GET …/checkpoint` + `POST …/restore` | | **数据恢复§5.13** | 成功同步后线上库快照latest+previousSyncPage「数据恢复」`GET …/checkpoint` + `POST …/restore` |
| **Z34b** | `POST /api/v1/auth/yuheng/restore-by-host`HMAC 凭票phone 可空)按 `host_key` 恢复已 sync_bound 落点并轮换密钥;见 §5.32 | | **Z34b** | `POST /api/v1/auth/yuheng/restore-by-host`HMAC 凭票phone 可空)按 `host_key` 恢复已 sync_bound 落点并轮换密钥;见 §5.32 |
| **Z35** | 绑定 `database_name=user_{id}`publish 拒绝中文库名并纠正脏数据;见 §5.33 |
> 产品一句:**账号已绑定 ⇒ 落点信息固定;通道没了平台自动补,终端无需手填 channel_id。** > 产品一句:**账号已绑定 ⇒ 落点信息固定;通道没了平台自动补,终端无需手填 channel_id。**
@@ -103,7 +105,7 @@
| 优先级 | 编号 | 项 | 说明 | | 优先级 | 编号 | 项 | 说明 |
|--------|------|----|------| |--------|------|----|------|
| — | — | **无阻塞开发项** | Z34b 已合入;宇恒接入 restore-by-host 后联调;生产 pull 见 B | | — | — | Z35 已合入见 B | 其余无阻塞开发项;生产 pull 见 B |
#### B‴. 智建 · 本次明确不改Z15 #### B‴. 智建 · 本次明确不改Z15
@@ -119,7 +121,7 @@
| 优先级 | 项 | 说明 | | 优先级 | 项 | 说明 |
|--------|----|------| |--------|----|------|
| **P0** | **生产 pull 本批** | Z10d + fingerprint + Z14c + **Z12h** + **数据恢复** + **Z34b**`bash ./restart.sh --pull` | | **P0** | **生产 pull 本批** | Z10d + fingerprint + Z14c + **Z12h** + **数据恢复** + **Z34b** + **Z35**`bash ./restart.sh --pull` |
| **P0** | 成员手机 | 「宇信达」绑 **`13531041944`**;勿超管号 | | **P0** | 成员手机 | 「宇信达」绑 **`13531041944`**;勿超管号 |
| **P0** | 通道表白名单 | **勿**「填入测试默认」;形态 B 可空 | | **P0** | 通道表白名单 | **勿**「填入测试默认」;形态 B 可空 |
| **P1** | 凭票 Secret | `YuhengTicket.Secret``YXD_YUHENG_TICKET_SECRET` | | **P1** | 凭票 Secret | `YuhengTicket.Secret``YXD_YUHENG_TICKET_SECRET` |
@@ -1082,6 +1084,28 @@ POST /api/v1/agent/sync/channels/{id}/pull
| **智建** | ✅ Z34b + gateway 路由 + 文档;生产 pull | | **智建** | ✅ Z34b + gateway 路由 + 文档;生产 pull |
| **宇恒** | 接入 `restore-by-host`(优先于手机静默路径);联调 | | **宇恒** | 接入 `restore-by-host`(优先于手机静默路径);联调 |
### 5.33 【Z35 · 2026-08-07】绑定勿用中文 DisplayName 作 Postgres 库名
**状态****智建已改完;宇恒无需**(报错来自平台 publish EnsureDatabase
#### 现象
模块发布 `HTTP 400: ensure database: invalid database name: 婷婷管理员`
#### 根因
`bindUserHostSync` 把成员 `DisplayName` 写入智能体 `database_name`publish Z8c 用该字段 `CREATE DATABASE`,而物理库名须匹配 `^[a-z][a-z0-9_]{1,47}$`
#### 智建改
1. 绑定:`DatabaseName=user_{userID}``Binding.DisplayName` 仍用昵称。
2. publish仅当 `ValidDBName` 才采用智能体库名;非法则 `SanitizeDBName` 并回写纠正。
3. 已存在非法 `existing.DatabaseName` 不再沿用。
#### 验收
昵称含中文的账号发布模块 → 2xx智能体 `database_name` 为合法英文/数字下划线。
## 6. 联系与附件 ## 6. 联系与附件
- **待改清单(优先看)**§0.2**A 宇恒接入 restore-by-host · B 运维 pull****改代码前先写本意见**§0.0);配合见 **§0.3** - **待改清单(优先看)**§0.2**A 宇恒接入 restore-by-host · B 运维 pull****改代码前先写本意见**§0.0);配合见 **§0.3**