feat: harden loose-offline sync for user JWT, schema, and console ops

Enable Binding-scoped agent push/pull, empty-table schema ensure, SyncPage inspect/drop-table, default module import, and agent-bound publish docs from the 宇恒联调意见.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
whm
2026-08-05 09:47:35 +08:00
parent 76cdcd760e
commit b04b180d30
59 changed files with 4762 additions and 308 deletions

View File

@@ -11,7 +11,7 @@ import (
"github.com/google/uuid"
)
// Binding 本机库 ↔ 线上库映射(P1不挡推送登记/查询)。
// Binding 本机库 ↔ 线上库映射(登记/查询;用户自助 push 时按 user_id+online_db_id 鉴权)。
type Binding struct {
ID string `json:"id"`
TenantID int64 `json:"tenant_id"`
@@ -19,9 +19,12 @@ type Binding struct {
LocalDatabaseID string `json:"local_database_id"`
OnlineDBID string `json:"online_db_id"`
ChannelID string `json:"channel_id,omitempty"`
Note string `json:"note,omitempty"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
// 可读名(与宇恒 database_name / display_name 对齐;控制台优先展示)
DatabaseName string `json:"database_name,omitempty"` // 本地库可读名
DisplayName string `json:"display_name,omitempty"` // 线上库可读名
Note string `json:"note,omitempty"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
func (s *FileStore) bindingPath() string {
@@ -37,6 +40,8 @@ func (s *FileStore) EnsureBinding(b Binding) (Binding, error) {
}
b.LocalDatabaseID = strings.TrimSpace(b.LocalDatabaseID)
b.OnlineDBID = strings.TrimSpace(b.OnlineDBID)
b.DatabaseName = strings.TrimSpace(b.DatabaseName)
b.DisplayName = strings.TrimSpace(b.DisplayName)
if b.TenantID <= 0 {
return b, fmt.Errorf("tenant_id required")
}
@@ -53,6 +58,12 @@ func (s *FileStore) EnsureBinding(b Binding) (Binding, error) {
if b.ChannelID != "" {
list[i].ChannelID = b.ChannelID
}
if b.DatabaseName != "" {
list[i].DatabaseName = b.DatabaseName
}
if b.DisplayName != "" {
list[i].DisplayName = b.DisplayName
}
if b.Note != "" {
list[i].Note = b.Note
}
@@ -79,6 +90,11 @@ func (s *FileStore) EnsureBinding(b Binding) (Binding, error) {
}
func (s *FileStore) ListBindings(tenantID int64, localDatabaseID string) ([]Binding, error) {
return s.ListBindingsFiltered(tenantID, 0, localDatabaseID)
}
// ListBindingsFiltered 按租户列出userID>0 时仅返回该用户的 Binding。
func (s *FileStore) ListBindingsFiltered(tenantID, userID int64, localDatabaseID string) ([]Binding, error) {
s.mu.Lock()
defer s.mu.Unlock()
list, err := s.readBindingsUnlocked()
@@ -90,6 +106,9 @@ func (s *FileStore) ListBindings(tenantID int64, localDatabaseID string) ([]Bind
if b.TenantID != tenantID {
continue
}
if userID > 0 && b.UserID != userID {
continue
}
if localDatabaseID != "" && b.LocalDatabaseID != localDatabaseID {
continue
}
@@ -98,6 +117,47 @@ func (s *FileStore) ListBindings(tenantID int64, localDatabaseID string) ([]Bind
return out, nil
}
// UserOwnsOnlineDB 用户是否登记了该 online_db_id可选限定 channel
func (s *FileStore) UserOwnsOnlineDB(tenantID, userID int64, channelID, onlineDBID string) bool {
if tenantID <= 0 || userID <= 0 || strings.TrimSpace(onlineDBID) == "" {
return false
}
list, err := s.ListBindingsFiltered(tenantID, userID, "")
if err != nil {
return false
}
online := strings.TrimSpace(onlineDBID)
ch := strings.TrimSpace(channelID)
for _, b := range list {
if strings.TrimSpace(b.OnlineDBID) != online {
continue
}
if ch != "" && b.ChannelID != "" && b.ChannelID != ch {
continue
}
return true
}
return false
}
// UserCanAccessChannel 用户是否登记了指向该通道的 Bindingchannel_id 空视为未限定通道)。
func (s *FileStore) UserCanAccessChannel(tenantID, userID int64, channelID string) bool {
if tenantID <= 0 || userID <= 0 || strings.TrimSpace(channelID) == "" {
return false
}
list, err := s.ListBindingsFiltered(tenantID, userID, "")
if err != nil || len(list) == 0 {
return false
}
ch := strings.TrimSpace(channelID)
for _, b := range list {
if b.ChannelID == "" || b.ChannelID == ch {
return true
}
}
return false
}
func (s *FileStore) GetBinding(tenantID int64, localDatabaseID string) (*Binding, error) {
list, err := s.ListBindings(tenantID, localDatabaseID)
if err != nil {