chore: import clean Agent Desk snapshot (2/7)

This commit is contained in:
2026-10-16 11:57:21 +00:00
parent 79176541c7
commit 89c5231c4c
461 changed files with 60092 additions and 0 deletions

View File

@@ -0,0 +1,7 @@
package migration
func init() {
register(1, "init schema migration", func() error {
return nil
})
}

View File

@@ -0,0 +1,256 @@
package migration
import (
"agent-desk/internal/models"
"agent-desk/internal/pkg/constants"
"agent-desk/internal/pkg/enums"
"agent-desk/internal/repositories"
"errors"
"log/slog"
"strings"
"time"
"github.com/mlogclub/simple/sqls"
"golang.org/x/crypto/bcrypt"
"gorm.io/gorm"
)
func init() {
register(2, "init auth builtin data", func() error {
return sqls.WithTransaction(func(ctx *sqls.TxContext) error {
permissions, err := ensurePermissions(ctx.Tx)
if err != nil {
return err
}
roles, err := ensureRoles(ctx.Tx)
if err != nil {
return err
}
if err = ensureRolePermissions(ctx.Tx, roles, permissions); err != nil {
return err
}
return ensureBootstrapAdmin(ctx.Tx, roles[constants.RoleCodeSuperAdmin])
})
})
}
func ensurePermissions(tx *gorm.DB) (map[string]*models.Permission, error) {
permissions := make(map[string]*models.Permission, len(constants.Permissions))
now := time.Now()
for _, spec := range constants.Permissions {
permission := repositories.PermissionRepository.FindOne(tx, sqls.NewCnd().Eq("code", spec.Code))
if permission == nil {
permission = &models.Permission{
Name: spec.Name,
Code: spec.Code,
Type: spec.Type,
GroupName: spec.GroupName,
Method: spec.Method,
APIPath: spec.APIPath,
SortNo: spec.SortNo,
Status: enums.StatusOk,
IsBuiltin: true,
AuditFields: models.AuditFields{
CreatedAt: now,
CreateUserID: constants.SystemAuditUserID,
CreateUserName: constants.SystemAuditUserName,
UpdatedAt: now,
UpdateUserID: constants.SystemAuditUserID,
UpdateUserName: constants.SystemAuditUserName,
},
}
if err := repositories.PermissionRepository.Create(tx, permission); err != nil {
return nil, err
}
// slog.Info("initialized builtin permission", "code", spec.Code)
} else {
if err := repositories.PermissionRepository.Updates(tx, permission.ID, map[string]any{
"name": spec.Name,
"type": spec.Type,
"group_name": spec.GroupName,
"method": spec.Method,
"api_path": spec.APIPath,
"sort_no": spec.SortNo,
"status": enums.StatusOk,
"is_builtin": true,
"update_user_id": constants.SystemAuditUserID,
"update_user_name": constants.SystemAuditUserName,
"updated_at": now,
}); err != nil {
return nil, err
}
permission = repositories.PermissionRepository.Get(tx, permission.ID)
}
permissions[spec.Code] = permission
}
return permissions, nil
}
func ensureRoles(tx *gorm.DB) (map[string]*models.Role, error) {
roles := make(map[string]*models.Role, len(constants.Roles))
now := time.Now()
for _, spec := range constants.Roles {
role := repositories.RoleRepository.GetByCode(tx, spec.Code)
if role == nil {
role = &models.Role{
Name: spec.Name,
Code: spec.Code,
Status: enums.StatusOk,
IsSystem: true,
SortNo: spec.SortNo,
AuditFields: models.AuditFields{
CreatedAt: now,
CreateUserID: constants.SystemAuditUserID,
CreateUserName: constants.SystemAuditUserName,
UpdatedAt: now,
UpdateUserID: constants.SystemAuditUserID,
UpdateUserName: constants.SystemAuditUserName,
},
}
if err := repositories.RoleRepository.Create(tx, role); err != nil {
return nil, err
}
// slog.Info("initialized builtin role", "code", spec.Code)
} else {
if err := repositories.RoleRepository.Updates(tx, role.ID, map[string]any{
"name": spec.Name,
"sort_no": spec.SortNo,
"status": enums.StatusOk,
"is_system": true,
"update_user_id": constants.SystemAuditUserID,
"update_user_name": constants.SystemAuditUserName,
"updated_at": now,
}); err != nil {
return nil, err
}
role = repositories.RoleRepository.Get(tx, role.ID)
}
roles[spec.Code] = role
}
return roles, nil
}
func ensureRolePermissions(tx *gorm.DB, roles map[string]*models.Role, permissions map[string]*models.Permission) error {
now := time.Now()
for roleCode, rolePermissions := range constants.RolePermissions {
role := roles[roleCode]
if role == nil {
return errors.New("builtin role not found: " + roleCode)
}
for _, permissionSpec := range rolePermissions {
permission := permissions[permissionSpec.Code]
if permission == nil {
return errors.New("builtin permission not found: " + permissionSpec.Code)
}
exists := repositories.RolePermissionRepository.FindOne(tx, sqls.NewCnd().
Eq("role_id", role.ID).
Eq("permission_id", permission.ID))
if exists != nil {
continue
}
if err := repositories.RolePermissionRepository.Create(tx, &models.RolePermission{
RoleID: role.ID,
PermissionID: permission.ID,
AuditFields: models.AuditFields{
CreatedAt: now,
CreateUserID: constants.SystemAuditUserID,
CreateUserName: constants.SystemAuditUserName,
UpdatedAt: now,
UpdateUserID: constants.SystemAuditUserID,
UpdateUserName: constants.SystemAuditUserName,
},
}); err != nil {
return err
}
}
}
return nil
}
func ensureBootstrapAdmin(tx *gorm.DB, superAdminRole *models.Role) error {
if superAdminRole == nil {
return errors.New("super admin role not found")
}
username := constants.BootstrapAdminUsername
nickname := constants.BootstrapAdminNickname
password := constants.BootstrapAdminPassword
if strings.TrimSpace(password) == "" {
password = "ChangeMe123!"
slog.Warn("bootstrap admin password is empty, using default password", "username", username)
}
user := repositories.UserRepository.FindOne(tx, sqls.NewCnd().Eq("username", username))
now := time.Now()
if user == nil {
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
if err != nil {
return err
}
user = &models.User{
Username: username,
Nickname: nickname,
Password: string(hashedPassword),
UserType: enums.UserTypeEmployee,
Status: enums.StatusOk,
Remark: "bootstrap super admin",
AuditFields: models.AuditFields{
CreatedAt: now,
CreateUserID: constants.SystemAuditUserID,
CreateUserName: constants.SystemAuditUserName,
UpdatedAt: now,
UpdateUserID: constants.SystemAuditUserID,
UpdateUserName: constants.SystemAuditUserName,
},
}
if err := repositories.UserRepository.Create(tx, user); err != nil {
return err
}
slog.Warn("initialized bootstrap admin user", "username", username)
} else {
if err := repositories.UserRepository.Updates(tx, user.ID, map[string]any{
"nickname": nickname,
"status": enums.StatusOk,
"update_user_id": constants.SystemAuditUserID,
"update_user_name": constants.SystemAuditUserName,
"updated_at": now,
}); err != nil {
return err
}
user = repositories.UserRepository.Get(tx, user.ID)
}
exists := repositories.UserRoleRepository.FindOne(tx, sqls.NewCnd().
Eq("user_id", user.ID).
Eq("role_id", superAdminRole.ID))
if exists != nil {
return nil
}
return repositories.UserRoleRepository.Create(tx, &models.UserRole{
UserID: user.ID,
RoleID: superAdminRole.ID,
AuditFields: models.AuditFields{
CreatedAt: now,
CreateUserID: constants.SystemAuditUserID,
CreateUserName: constants.SystemAuditUserName,
UpdatedAt: now,
UpdateUserID: constants.SystemAuditUserID,
UpdateUserName: constants.SystemAuditUserName,
},
})
}

View File

@@ -0,0 +1,21 @@
package migration
import "github.com/mlogclub/simple/sqls"
func init() {
register(4, "sync ai agent permissions", func() error {
return sqls.WithTransaction(func(ctx *sqls.TxContext) error {
permissions, err := ensurePermissions(ctx.Tx)
if err != nil {
return err
}
roles, err := ensureRoles(ctx.Tx)
if err != nil {
return err
}
return ensureRolePermissions(ctx.Tx, roles, permissions)
})
})
}

View File

@@ -0,0 +1,24 @@
package migration
import (
"agent-desk/internal/models"
"github.com/mlogclub/simple/sqls"
)
func init() {
register(6, "backfill conversation customer name", func() error {
db := sqls.DB()
if !db.Migrator().HasColumn(&models.Conversation{}, "customer_name") {
return nil
}
return db.Exec(`
UPDATE t_conversation
SET customer_name = (
SELECT name FROM t_customer WHERE t_customer.id = t_conversation.customer_id
)
WHERE customer_id > 0
AND (customer_name = '' OR customer_name IS NULL)
`).Error
})
}

View File

@@ -0,0 +1,21 @@
package migration
import "github.com/mlogclub/simple/sqls"
func init() {
register(7, "sync notification permissions", func() error {
return sqls.WithTransaction(func(ctx *sqls.TxContext) error {
permissions, err := ensurePermissions(ctx.Tx)
if err != nil {
return err
}
roles, err := ensureRoles(ctx.Tx)
if err != nil {
return err
}
return ensureRolePermissions(ctx.Tx, roles, permissions)
})
})
}

View File

@@ -0,0 +1,21 @@
package migration
import "github.com/mlogclub/simple/sqls"
func init() {
register(9, "sync wxwork outbox permissions", func() error {
return sqls.WithTransaction(func(ctx *sqls.TxContext) error {
permissions, err := ensurePermissions(ctx.Tx)
if err != nil {
return err
}
roles, err := ensureRoles(ctx.Tx)
if err != nil {
return err
}
return ensureRolePermissions(ctx.Tx, roles, permissions)
})
})
}

View File

@@ -0,0 +1,34 @@
package migration
import (
"agent-desk/internal/models"
"agent-desk/internal/pkg/constants"
"agent-desk/internal/pkg/enums"
"time"
"github.com/mlogclub/simple/sqls"
)
func init() {
register(10, "repair bootstrap admin user type", func() error {
db := sqls.DB()
superAdmins := db.Table("t_user_role AS ur").Select("ur.user_id").
Joins("JOIN t_role AS r ON r.id = ur.role_id").
Where("r.code = ? AND r.status = ?", constants.RoleCodeSuperAdmin, enums.StatusOk)
// Migration 2 has already run on existing installations. Repair only the
// active bootstrap account that already has the super admin role.
return db.Model(&models.User{}).
Where("username = ?", constants.BootstrapAdminUsername).
Where("user_type = ?", enums.UserTypeUser).
Where("status = ?", enums.StatusOk).
Where("deleted_at IS NULL").
Where("id IN (?)", superAdmins).
Updates(map[string]any{
"user_type": enums.UserTypeEmployee,
"updated_at": time.Now(),
"update_user_id": constants.SystemAuditUserID,
"update_user_name": constants.SystemAuditUserName,
}).Error
})
}

View File

@@ -0,0 +1,131 @@
package migration
import (
"path/filepath"
"testing"
"time"
"agent-desk/internal/models"
"agent-desk/internal/pkg/constants"
"agent-desk/internal/pkg/enums"
"github.com/glebarez/sqlite"
"github.com/mlogclub/simple/sqls"
"gorm.io/gorm"
"gorm.io/gorm/schema"
)
func setupBootstrapAdminTestDB(t *testing.T) *gorm.DB {
t.Helper()
db, err := gorm.Open(sqlite.Open(filepath.Join(t.TempDir(), "auth.db")), &gorm.Config{
NamingStrategy: schema.NamingStrategy{TablePrefix: "t_", SingularTable: true},
})
if err != nil {
t.Fatal(err)
}
if err := db.AutoMigrate(&models.User{}, &models.Role{}, &models.UserRole{}); err != nil {
t.Fatal(err)
}
conn, err := db.DB()
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() { _ = conn.Close() })
return db
}
func TestBootstrapAdminCreatedAsEmployee(t *testing.T) {
db := setupBootstrapAdminTestDB(t)
role := models.Role{Code: constants.RoleCodeSuperAdmin, Status: enums.StatusOk}
if err := db.Create(&role).Error; err != nil {
t.Fatal(err)
}
if err := ensureBootstrapAdmin(db, &role); err != nil {
t.Fatal(err)
}
var user models.User
if err := db.Where("username = ?", constants.BootstrapAdminUsername).First(&user).Error; err != nil {
t.Fatal(err)
}
if user.UserType != enums.UserTypeEmployee {
t.Fatalf("bootstrap admin type = %q, want employee", user.UserType)
}
var count int64
if err := db.Model(&models.UserRole{}).Where("user_id = ? AND role_id = ?", user.ID, role.ID).Count(&count).Error; err != nil {
t.Fatal(err)
}
if count != 1 {
t.Fatalf("super admin role assignments = %d, want 1", count)
}
}
func TestRepairBootstrapAdminUserType(t *testing.T) {
repair, ok := migrationFuncs[10]
if !ok {
t.Fatal("bootstrap admin repair migration is not registered")
}
for _, tc := range []struct {
name string
username string
userType enums.UserType
status enums.Status
roleCode string
roleStatus enums.Status
deleted bool
want enums.UserType
}{
{"legacy admin", "admin", enums.UserTypeUser, enums.StatusOk, constants.RoleCodeSuperAdmin, enums.StatusOk, false, enums.UserTypeEmployee},
{"already employee", "admin", enums.UserTypeEmployee, enums.StatusOk, constants.RoleCodeSuperAdmin, enums.StatusOk, false, enums.UserTypeEmployee},
{"ordinary user", "customer", enums.UserTypeUser, enums.StatusOk, "", enums.StatusOk, false, enums.UserTypeUser},
{"same name without role", "admin", enums.UserTypeUser, enums.StatusOk, "", enums.StatusOk, false, enums.UserTypeUser},
{"other super admin", "other", enums.UserTypeUser, enums.StatusOk, constants.RoleCodeSuperAdmin, enums.StatusOk, false, enums.UserTypeUser},
{"disabled admin", "admin", enums.UserTypeUser, enums.StatusDisabled, constants.RoleCodeSuperAdmin, enums.StatusOk, false, enums.UserTypeUser},
{"disabled role", "admin", enums.UserTypeUser, enums.StatusOk, constants.RoleCodeSuperAdmin, enums.StatusDisabled, false, enums.UserTypeUser},
{"deleted admin", "admin", enums.UserTypeUser, enums.StatusOk, constants.RoleCodeSuperAdmin, enums.StatusOk, true, enums.UserTypeUser},
} {
t.Run(tc.name, func(t *testing.T) {
db := setupBootstrapAdminTestDB(t)
sqls.SetDB(db)
t.Cleanup(func() { sqls.SetDB(nil) })
user := models.User{Username: tc.username, Password: "unchanged-test-value", UserType: tc.userType, Status: tc.status}
if tc.deleted {
now := time.Now()
user.DeletedAt = &now
}
if err := db.Create(&user).Error; err != nil {
t.Fatal(err)
}
if tc.roleCode != "" {
role := models.Role{Code: tc.roleCode, Status: tc.roleStatus}
if err := db.Create(&role).Error; err != nil {
t.Fatal(err)
}
if err := db.Create(&models.UserRole{UserID: user.ID, RoleID: role.ID}).Error; err != nil {
t.Fatal(err)
}
}
if err := repair.Fn(); err != nil {
t.Fatal(err)
}
var got models.User
if err := db.First(&got, user.ID).Error; err != nil {
t.Fatal(err)
}
if got.UserType != tc.want {
t.Fatalf("type = %q, want %q", got.UserType, tc.want)
}
if got.Password != user.Password || got.Status != user.Status {
t.Fatal("repair changed password or account status")
}
updatedAt := got.UpdatedAt
if err := repair.Fn(); err != nil {
t.Fatal(err)
}
if err := db.First(&got, user.ID).Error; err != nil {
t.Fatal(err)
}
if !got.UpdatedAt.Equal(updatedAt) {
t.Fatal("second repair changed updated_at")
}
})
}
}

View File

@@ -0,0 +1,56 @@
package migration
import (
"github.com/mlogclub/simple/sqls"
)
func init() {
register(11, "remove chinese platforms tables and data", func() error {
return sqls.WithTransaction(func(ctx *sqls.TxContext) error {
tx := ctx.Tx
// 1. Drop orphaned tables
tables := []string{
"t_wx_work_kf_conversation",
"t_wx_work_kf_message_ref",
"t_wx_work_kf_sync_state",
}
for _, table := range tables {
if err := tx.Exec("DROP TABLE IF EXISTS " + table).Error; err != nil {
return err
}
}
// 2. Remove channel rows of removed types
removedChannelTypes := []string{"wxwork_kf", "wechat_mp", "lark", "wechat"}
if err := tx.Exec("DELETE FROM t_channel WHERE channel_type IN (?)", removedChannelTypes).Error; err != nil {
return err
}
// 3. Remove outbox entries for removed channels
if err := tx.Exec("DELETE FROM t_channel_message_outbox WHERE channel_type IN (?)", removedChannelTypes).Error; err != nil {
return err
}
// 4. Remove customer identities of removed external sources
removedExternalSources := []string{"wxwork_kf", "lark", "wechat"}
if err := tx.Exec("DELETE FROM t_customer_identity WHERE external_source IN (?)", removedExternalSources).Error; err != nil {
return err
}
// 5. Remove obsolete permissions and role permissions
removedPermissionCodes := []string{"wxworkOutbox.view"}
var permissionIDs []int64
if err := tx.Table("t_permission").Where("code IN (?)", removedPermissionCodes).Pluck("id", &permissionIDs).Error; err == nil && len(permissionIDs) > 0 {
if err := tx.Exec("DELETE FROM t_role_permission WHERE permission_id IN (?)", permissionIDs).Error; err != nil {
return err
}
if err := tx.Exec("DELETE FROM t_permission WHERE id IN (?)", permissionIDs).Error; err != nil {
return err
}
}
return nil
})
})
}

View File

@@ -0,0 +1,3 @@
# data migration
data migration only, DLL are prohibited.

View File

@@ -0,0 +1,101 @@
package migration
import (
"agent-desk/internal/models"
"agent-desk/internal/services"
"errors"
"log/slog"
"sync"
"time"
"github.com/mlogclub/simple/sqls"
"github.com/spf13/cast"
)
var migrationFuncs = make(map[int64]MigrationFunc)
var versions = make([]int64, 0)
var migrations = make(map[int64]models.Migration, 0)
var mu sync.Mutex
type MigrationFunc struct {
Version int64
Remark string
Fn func() error
}
func Migrate() error {
mu.Lock()
defer mu.Unlock()
if list := services.MigrationService.Find(sqls.NewCnd().Asc("version")); len(list) > 0 {
for _, element := range list {
migrations[element.Version] = element
}
}
for _, version := range versions {
if err := runMigration(version); err != nil {
slog.Error("migrate failed", "version", version, "error", err)
return err
}
}
return nil
}
func register(version int64, remark string, fn func() error) {
if len(versions) == 0 || version > versions[len(versions)-1] {
versions = append(versions, version)
migrationFuncs[version] = MigrationFunc{
Version: version,
Remark: remark,
Fn: fn,
}
} else {
slog.Error("register migration failed, version is less than latest version", slog.Any("version", version))
panic(errors.New("register migration failed, version is less than latest version. version: " + cast.ToString(version)))
}
}
func runMigration(version int64) error {
migration, found := migrations[version]
if found && migration.Success {
return nil
}
f, ok := migrationFuncs[version]
if !ok {
return errors.New("migration function not found")
}
err := f.Fn()
if !found {
migration = models.Migration{
Version: f.Version,
Remark: f.Remark,
Success: false,
RetryCount: 0,
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
}
}
if err == nil {
migration.Success = true
} else {
migration.Success = false
migration.ErrorInfo = err.Error()
}
migration.RetryCount++
migration.UpdatedAt = time.Now()
if found {
if e := services.MigrationService.Update(&migration); e != nil {
slog.Error("update migration failed", "version", version, "error", err)
}
} else {
if e := services.MigrationService.Create(&migration); e != nil {
slog.Error("create migration failed", "version", version, "error", err)
}
}
return err
}