refactor(auth): delegate access control to be-system

Remove Agent Desk users, roles, login sessions, tokens, and local permission persistence. Expose the backend as an embeddable ai-agent module with host-provided subject lookup and operation authorization callbacks, and complete the frontend/backend repository split.
This commit is contained in:
t
2026-08-21 00:41:07 +08:00
parent 3d47227fbd
commit 2bbf42b741
447 changed files with 1901 additions and 8920 deletions
+1 -10
View File
@@ -1,7 +1,7 @@
package main
import (
"agent-desk/internal/models"
"code.tczkiot.com/wlw/ai-agent/internal/models"
"github.com/mlogclub/codegen"
)
@@ -19,19 +19,10 @@ func main() {
WebEdit: false,
},
codegen.GetGenerateStruct(&models.Migration{}),
codegen.GetGenerateStruct(&models.User{}),
codegen.GetGenerateStruct(&models.UserIdentity{}),
codegen.GetGenerateStruct(&models.Company{}),
codegen.GetGenerateStruct(&models.Customer{}),
codegen.GetGenerateStruct(&models.CustomerIdentity{}),
codegen.GetGenerateStruct(&models.CustomerContact{}),
codegen.GetGenerateStruct(&models.Role{}),
codegen.GetGenerateStruct(&models.Permission{}),
codegen.GetGenerateStruct(&models.UserRole{}),
codegen.GetGenerateStruct(&models.RolePermission{}),
codegen.GetGenerateStruct(&models.UserPermission{}),
codegen.GetGenerateStruct(&models.LoginSession{}),
codegen.GetGenerateStruct(&models.LoginCredentialLog{}),
codegen.GetGenerateStruct(&models.Asset{}),
codegen.GetGenerateStruct(&models.Tag{}),
codegen.GetGenerateStruct(&models.Conversation{}),
+3 -3
View File
@@ -3,9 +3,9 @@ package main
import (
"log/slog"
"agent-desk/internal/bootstrap"
"agent-desk/internal/pkg/config"
"agent-desk/internal/pkg/logx"
"code.tczkiot.com/wlw/ai-agent/internal/bootstrap"
"code.tczkiot.com/wlw/ai-agent/internal/pkg/config"
"code.tczkiot.com/wlw/ai-agent/internal/pkg/logx"
)
func main() {
+2 -2
View File
@@ -4,8 +4,8 @@ import (
"flag"
"log/slog"
"agent-desk/internal/bootstrap"
"agent-desk/internal/pkg/config"
"code.tczkiot.com/wlw/ai-agent/internal/bootstrap"
"code.tczkiot.com/wlw/ai-agent/internal/pkg/config"
)
func main() {
-200
View File
@@ -1,200 +0,0 @@
package agentteam
import (
"agent-desk/cmd/testdata/seedlang"
"agent-desk/cmd/testdata/seeds"
"agent-desk/internal/models"
"agent-desk/internal/pkg/constants"
"agent-desk/internal/pkg/enums"
"agent-desk/internal/repositories"
"fmt"
"time"
"github.com/mlogclub/simple/sqls"
"golang.org/x/crypto/bcrypt"
)
type InitResult struct {
TeamCreated bool
UsersCreated int
ProfilesCreated int
UpdatesApplied int
}
// Init 初始化客服组和客服用户
// 创建:
// 1. 客服组,组长为管理员用户
// 2. 客服A 用户
// 3. 客服B 用户
// 4. 为客服A和客服B创建客服档案,关联到该客服组
func Init(lang seedlang.Language) (*InitResult, error) {
result := &InitResult{}
// 获取管理员用户
adminUser := repositories.UserRepository.Take(
sqls.DB(),
"username = ?",
constants.BootstrapAdminUsername,
)
if adminUser == nil {
return result, fmt.Errorf("bootstrap admin user not found")
}
err := sqls.WithTransaction(func(ctx *sqls.TxContext) error {
return initTeamAndUsers(ctx, adminUser, result, lang)
})
if err != nil {
return result, fmt.Errorf("init team and users failed: %w", err)
}
return result, nil
}
func initTeamAndUsers(ctx *sqls.TxContext, leaderUser *models.User, result *InitResult, lang seedlang.Language) error {
teamName := seeds.AgentTeamName(lang)
now := time.Now()
team := repositories.AgentTeamRepository.Take(ctx.Tx, "name = ?", teamName)
if team != nil {
if err := ctx.Tx.Model(team).Updates(map[string]any{
"leader_user_id": leaderUser.ID,
"update_user_id": constants.SystemAuditUserID,
"update_user_name": constants.SystemAuditUserName,
"updated_at": now,
}).Error; err != nil {
return err
}
} else {
team = &models.AgentTeam{
Name: teamName,
LeaderUserID: leaderUser.ID,
Status: enums.StatusOk,
Description: "Local testdata seed - default service team",
AuditFields: models.AuditFields{
CreatedAt: now,
CreateUserID: constants.SystemAuditUserID,
CreateUserName: constants.SystemAuditUserName,
UpdatedAt: now,
UpdateUserID: constants.SystemAuditUserID,
UpdateUserName: constants.SystemAuditUserName,
},
}
if err := ctx.Tx.Create(team).Error; err != nil {
return err
}
result.TeamCreated = true
}
agentUsers := seeds.AgentUsers(lang, leaderUser.Username)
for _, agentUser := range agentUsers {
userID, userCreated, err := createOrGetUser(ctx, agentUser.Username, agentUser.Nickname)
if err != nil {
return err
}
if userCreated {
result.UsersCreated++
}
// 创建或更新客服档案
profileCreated, err := createOrUpdateProfile(ctx, userID, team.ID, agentUser.Code, agentUser.Nickname)
if err != nil {
return err
}
if profileCreated {
result.ProfilesCreated++
} else {
result.UpdatesApplied++
}
}
return nil
}
func createOrGetUser(ctx *sqls.TxContext, username, nickname string) (int64, bool, error) {
user := repositories.UserRepository.Take(
ctx.Tx,
"username = ?",
username,
)
if user != nil {
return user.ID, false, nil
}
// 创建新用户
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(constants.BootstrapAdminPassword), bcrypt.DefaultCost)
if err != nil {
return 0, false, err
}
now := time.Now()
newUser := &models.User{
Username: username,
Nickname: nickname,
Password: string(hashedPassword),
Status: enums.StatusOk,
Remark: "Local testdata seed",
AuditFields: models.AuditFields{
CreatedAt: now,
CreateUserID: constants.SystemAuditUserID,
CreateUserName: constants.SystemAuditUserName,
UpdatedAt: now,
UpdateUserID: constants.SystemAuditUserID,
UpdateUserName: constants.SystemAuditUserName,
},
}
if err := ctx.Tx.Create(newUser).Error; err != nil {
return 0, false, err
}
return newUser.ID, true, nil
}
func createOrUpdateProfile(ctx *sqls.TxContext, userID, teamID int64, agentCode, displayName string) (bool, error) {
profile := repositories.AgentProfileRepository.Take(
ctx.Tx,
"user_id = ?",
userID,
)
now := time.Now()
if profile != nil {
// 档案已存在,更新关键信息
return false, ctx.Tx.Model(profile).Updates(map[string]any{
"team_id": teamID,
"agent_code": agentCode,
"display_name": displayName,
"status": enums.StatusOk,
"update_user_id": constants.SystemAuditUserID,
"update_user_name": constants.SystemAuditUserName,
"updated_at": now,
}).Error
}
// 创建新档案
newProfile := &models.AgentProfile{
UserID: userID,
TeamID: teamID,
AgentCode: agentCode,
DisplayName: displayName,
Avatar: "",
ServiceStatus: enums.ServiceStatusIdle,
MaxConcurrentCount: 5,
PriorityLevel: 10,
AutoAssignEnabled: true,
Status: enums.StatusOk,
Remark: "Local testdata seed",
AuditFields: models.AuditFields{
CreatedAt: now,
CreateUserID: constants.SystemAuditUserID,
CreateUserName: constants.SystemAuditUserName,
UpdatedAt: now,
UpdateUserID: constants.SystemAuditUserID,
UpdateUserName: constants.SystemAuditUserName,
},
}
return true, ctx.Tx.Create(newProfile).Error
}
-23
View File
@@ -1,23 +0,0 @@
package agentteam
import (
"agent-desk/cmd/testdata/seedlang"
"agent-desk/cmd/testdata/seeds"
"regexp"
"testing"
)
var hanTextPattern = regexp.MustCompile(`\p{Han}`)
func TestEnglishAgentTeamTextDoesNotContainChineseText(t *testing.T) {
values := []string{seeds.AgentTeamName(seedlang.English)}
for _, user := range seeds.AgentUsers(seedlang.English, "admin") {
values = append(values, user.Nickname)
}
for _, value := range values {
if hanTextPattern.MatchString(value) {
t.Fatalf("english agent team seed contains Chinese text: %q", value)
}
}
}
+6 -6
View File
@@ -1,12 +1,12 @@
package aiagent
import (
"agent-desk/cmd/testdata/seedlang"
"agent-desk/cmd/testdata/seeds"
"agent-desk/internal/models"
"agent-desk/internal/pkg/enums"
"agent-desk/internal/pkg/utils"
"agent-desk/internal/repositories"
"code.tczkiot.com/wlw/ai-agent/cmd/testdata/seedlang"
"code.tczkiot.com/wlw/ai-agent/cmd/testdata/seeds"
"code.tczkiot.com/wlw/ai-agent/internal/models"
"code.tczkiot.com/wlw/ai-agent/internal/pkg/enums"
"code.tczkiot.com/wlw/ai-agent/internal/pkg/utils"
"code.tczkiot.com/wlw/ai-agent/internal/repositories"
"fmt"
"strings"
"time"
+2 -2
View File
@@ -1,8 +1,8 @@
package aiagent
import (
"agent-desk/cmd/testdata/seedlang"
"agent-desk/cmd/testdata/seeds"
"code.tczkiot.com/wlw/ai-agent/cmd/testdata/seedlang"
"code.tczkiot.com/wlw/ai-agent/cmd/testdata/seeds"
"regexp"
"strings"
"testing"
+4 -4
View File
@@ -6,10 +6,10 @@ import (
"strings"
"time"
"agent-desk/internal/models"
"agent-desk/internal/pkg/constants"
"agent-desk/internal/pkg/enums"
"agent-desk/internal/repositories"
"code.tczkiot.com/wlw/ai-agent/internal/models"
"code.tczkiot.com/wlw/ai-agent/internal/pkg/constants"
"code.tczkiot.com/wlw/ai-agent/internal/pkg/enums"
"code.tczkiot.com/wlw/ai-agent/internal/repositories"
"github.com/mlogclub/simple/sqls"
"gopkg.in/yaml.v3"
+5 -5
View File
@@ -1,11 +1,11 @@
package channel
import (
"agent-desk/cmd/testdata/seedlang"
"agent-desk/cmd/testdata/seeds"
"agent-desk/internal/models"
"agent-desk/internal/pkg/enums"
"agent-desk/internal/repositories"
"code.tczkiot.com/wlw/ai-agent/cmd/testdata/seedlang"
"code.tczkiot.com/wlw/ai-agent/cmd/testdata/seeds"
"code.tczkiot.com/wlw/ai-agent/internal/models"
"code.tczkiot.com/wlw/ai-agent/internal/pkg/enums"
"code.tczkiot.com/wlw/ai-agent/internal/repositories"
"fmt"
"time"
+2 -2
View File
@@ -1,8 +1,8 @@
package channel
import (
"agent-desk/cmd/testdata/seedlang"
"agent-desk/cmd/testdata/seeds"
"code.tczkiot.com/wlw/ai-agent/cmd/testdata/seedlang"
"code.tczkiot.com/wlw/ai-agent/cmd/testdata/seeds"
"regexp"
"testing"
)
+6 -6
View File
@@ -1,12 +1,12 @@
package kb
import (
"agent-desk/cmd/testdata/seedlang"
"agent-desk/cmd/testdata/seeds"
"agent-desk/internal/models"
"agent-desk/internal/pkg/constants"
"agent-desk/internal/pkg/enums"
"agent-desk/internal/repositories"
"code.tczkiot.com/wlw/ai-agent/cmd/testdata/seedlang"
"code.tczkiot.com/wlw/ai-agent/cmd/testdata/seeds"
"code.tczkiot.com/wlw/ai-agent/internal/models"
"code.tczkiot.com/wlw/ai-agent/internal/pkg/constants"
"code.tczkiot.com/wlw/ai-agent/internal/pkg/enums"
"code.tczkiot.com/wlw/ai-agent/internal/repositories"
"encoding/json"
"time"
+2 -2
View File
@@ -1,8 +1,8 @@
package kb
import (
"agent-desk/cmd/testdata/seedlang"
"agent-desk/cmd/testdata/seeds"
"code.tczkiot.com/wlw/ai-agent/cmd/testdata/seedlang"
"code.tczkiot.com/wlw/ai-agent/cmd/testdata/seeds"
"regexp"
"testing"
)
+10 -21
View File
@@ -1,17 +1,16 @@
package main
import (
"agent-desk/cmd/testdata/agentteam"
"agent-desk/cmd/testdata/aiagent"
"agent-desk/cmd/testdata/aiconfig"
"agent-desk/cmd/testdata/channel"
"agent-desk/cmd/testdata/kb"
"agent-desk/cmd/testdata/quickreply"
"agent-desk/cmd/testdata/seedlang"
"agent-desk/cmd/testdata/skill"
"agent-desk/cmd/testdata/tag"
"agent-desk/internal/bootstrap"
"agent-desk/internal/pkg/config"
"code.tczkiot.com/wlw/ai-agent/cmd/testdata/aiagent"
"code.tczkiot.com/wlw/ai-agent/cmd/testdata/aiconfig"
"code.tczkiot.com/wlw/ai-agent/cmd/testdata/channel"
"code.tczkiot.com/wlw/ai-agent/cmd/testdata/kb"
"code.tczkiot.com/wlw/ai-agent/cmd/testdata/quickreply"
"code.tczkiot.com/wlw/ai-agent/cmd/testdata/seedlang"
"code.tczkiot.com/wlw/ai-agent/cmd/testdata/skill"
"code.tczkiot.com/wlw/ai-agent/cmd/testdata/tag"
"code.tczkiot.com/wlw/ai-agent/internal/bootstrap"
"code.tczkiot.com/wlw/ai-agent/internal/pkg/config"
"flag"
"fmt"
"log/slog"
@@ -94,16 +93,6 @@ func run() error {
}
slog.Info("skill init success", slog.Int("created", skillResult.Created), slog.Int("updated", skillResult.Updated))
agentTeamResult, err := agentteam.Init(lang)
if err != nil {
return fmt.Errorf("init agent team failed: %w", err)
}
slog.Info("agent team init success", slog.Bool("teamCreated", agentTeamResult.TeamCreated),
slog.Int("usersCreated", agentTeamResult.UsersCreated),
slog.Int("profilesCreated", agentTeamResult.ProfilesCreated),
slog.Int("updatesApplied", agentTeamResult.UpdatesApplied),
)
aiAgentResult, err := aiagent.Init(lang)
if err != nil {
return fmt.Errorf("init ai agent failed: %w", err)
+4 -4
View File
@@ -1,10 +1,10 @@
package quickreply
import (
"agent-desk/cmd/testdata/seedlang"
"agent-desk/cmd/testdata/seeds"
"agent-desk/internal/models"
"agent-desk/internal/repositories"
"code.tczkiot.com/wlw/ai-agent/cmd/testdata/seedlang"
"code.tczkiot.com/wlw/ai-agent/cmd/testdata/seeds"
"code.tczkiot.com/wlw/ai-agent/internal/models"
"code.tczkiot.com/wlw/ai-agent/internal/repositories"
"time"
"github.com/mlogclub/simple/sqls"
+2 -2
View File
@@ -1,8 +1,8 @@
package quickreply
import (
"agent-desk/cmd/testdata/seedlang"
"agent-desk/cmd/testdata/seeds"
"code.tczkiot.com/wlw/ai-agent/cmd/testdata/seedlang"
"code.tczkiot.com/wlw/ai-agent/cmd/testdata/seeds"
"regexp"
"testing"
)
+1 -1
View File
@@ -1,6 +1,6 @@
package seeds
import "agent-desk/cmd/testdata/seedlang"
import "code.tczkiot.com/wlw/ai-agent/cmd/testdata/seedlang"
type AgentUserSeed struct {
Username string
+2 -2
View File
@@ -1,8 +1,8 @@
package seeds
import (
"agent-desk/cmd/testdata/seedlang"
"agent-desk/internal/pkg/enums"
"code.tczkiot.com/wlw/ai-agent/cmd/testdata/seedlang"
"code.tczkiot.com/wlw/ai-agent/internal/pkg/enums"
)
type AIAgentSeed struct {
+3 -3
View File
@@ -1,9 +1,9 @@
package seeds
import (
"agent-desk/cmd/testdata/seedlang"
"agent-desk/internal/pkg/dto"
"agent-desk/internal/pkg/enums"
"code.tczkiot.com/wlw/ai-agent/cmd/testdata/seedlang"
"code.tczkiot.com/wlw/ai-agent/internal/pkg/dto"
"code.tczkiot.com/wlw/ai-agent/internal/pkg/enums"
"github.com/mlogclub/simple/common/jsons"
)
+1 -2
View File
@@ -1,6 +1,6 @@
package seeds
import "agent-desk/cmd/testdata/seedlang"
import "code.tczkiot.com/wlw/ai-agent/cmd/testdata/seedlang"
type KnowledgeBaseSeed struct {
Name string
@@ -83,7 +83,6 @@ func chineseKnowledgeFAQSeeds() []KnowledgeFAQSeed {
{Question: "新成员加入后如何开通后台账号?", Answer: "企业管理员进入“组织设置-成员管理”,点击“新增成员”,填写姓名、邮箱、所属团队和角色后保存。系统会自动发送激活邮件,成员首次登录时设置密码即可。若你们开通了单点登录,也可以直接从企业身份系统同步成员。", SimilarQuestions: []string{"怎么给新客服开账号", "新增员工账号在哪里", "成员怎么加入后台"}, Remark: "成员管理"},
{Question: "成员离职后如何停用账号?", Answer: "请在“组织设置-成员管理”中找到对应成员,点击“停用”即可。停用后该账号无法继续登录,但历史会话、工单处理记录和质检数据会保留,不会影响报表统计。若后续确认不再使用,也可以在完成交接后删除账号。", SimilarQuestions: []string{"离职员工账号怎么处理", "怎么禁用成员账号", "停用客服账号"}, Remark: "成员管理"},
{Question: "角色权限修改后多久生效?", Answer: "角色权限保存后通常即时生效。已在线的成员可能需要刷新页面或重新登录,才能拿到最新权限菜单和接口授权。如果修改后仍能访问原页面,请清理浏览器缓存后再试。", SimilarQuestions: []string{"权限修改什么时候生效", "调整角色后没变化", "角色更新后要重登吗"}, Remark: "成员管理"},
{Question: "支持企业微信或钉钉单点登录吗?", Answer: "支持对接企业微信、钉钉和标准 SAML/OIDC 单点登录。开通后成员可通过企业身份系统直接登录平台,无需单独维护密码。具体配置需要管理员在“安全设置-单点登录”中填写回调地址、应用凭证并完成测试。", SimilarQuestions: []string{"能接企业微信登录吗", "支持钉钉 SSO 吗", "单点登录怎么接"}, Remark: "成员管理"},
{Question: "坐席在线、忙碌、离线状态有什么区别?", Answer: "在线表示可正常接待新会话,忙碌表示当前暂不分配新会话但仍可处理已有会话,离线表示不参与会话分配也不接收实时提醒。若开启自动状态切换,长时间无操作或退出登录后,系统会自动变更为离线。", SimilarQuestions: []string{"客服状态怎么理解", "在线忙碌离线区别", "坐席状态说明"}, Remark: "坐席接待"},
{Question: "会话是怎么分配给坐席的?", Answer: "默认按技能组和轮询策略分配,也可结合坐席当前负载、最近响应时长和优先级规则进行智能分流。若客户命中了指定渠道、语言或标签条件,系统会优先路由到匹配该条件的团队或坐席。", SimilarQuestions: []string{"客户咨询怎么分配", "会话路由规则是什么", "新会话按什么分给客服"}, Remark: "坐席接待"},
{Question: "如何把会话转接给其他团队?", Answer: "在会话详情页点击“转接”,选择目标团队或指定坐席,并填写转接备注即可。转接后,原坐席仍可在历史记录中查看会话内容,但新消息会优先提醒接收方。若目标团队离线人数过多,建议先确认有人值班。", SimilarQuestions: []string{"会话怎么转给别的组", "转接客服在哪里", "咨询如何分配给其他团队"}, Remark: "坐席接待"},
+2 -2
View File
@@ -1,8 +1,8 @@
package seeds
import (
"agent-desk/cmd/testdata/seedlang"
"agent-desk/internal/pkg/enums"
"code.tczkiot.com/wlw/ai-agent/cmd/testdata/seedlang"
"code.tczkiot.com/wlw/ai-agent/internal/pkg/enums"
)
type QuickReplySeed struct {
+2 -2
View File
@@ -1,8 +1,8 @@
package seeds
import (
"agent-desk/cmd/testdata/seedlang"
"agent-desk/internal/pkg/enums"
"code.tczkiot.com/wlw/ai-agent/cmd/testdata/seedlang"
"code.tczkiot.com/wlw/ai-agent/internal/pkg/enums"
)
type SkillDefinitionSeed struct {
+1 -1
View File
@@ -1,6 +1,6 @@
package seeds
import "agent-desk/cmd/testdata/seedlang"
import "code.tczkiot.com/wlw/ai-agent/cmd/testdata/seedlang"
type TagSeed struct {
ID int64
+4 -4
View File
@@ -1,10 +1,10 @@
package skill
import (
"agent-desk/cmd/testdata/seedlang"
"agent-desk/cmd/testdata/seeds"
"agent-desk/internal/models"
"agent-desk/internal/repositories"
"code.tczkiot.com/wlw/ai-agent/cmd/testdata/seedlang"
"code.tczkiot.com/wlw/ai-agent/cmd/testdata/seeds"
"code.tczkiot.com/wlw/ai-agent/internal/models"
"code.tczkiot.com/wlw/ai-agent/internal/repositories"
"fmt"
"strings"
"time"
+2 -2
View File
@@ -1,8 +1,8 @@
package skill
import (
"agent-desk/cmd/testdata/seedlang"
"agent-desk/cmd/testdata/seeds"
"code.tczkiot.com/wlw/ai-agent/cmd/testdata/seedlang"
"code.tczkiot.com/wlw/ai-agent/cmd/testdata/seeds"
"regexp"
"testing"
)
+5 -5
View File
@@ -1,11 +1,11 @@
package tag
import (
"agent-desk/cmd/testdata/seedlang"
"agent-desk/cmd/testdata/seeds"
"agent-desk/internal/models"
"agent-desk/internal/pkg/enums"
"agent-desk/internal/repositories"
"code.tczkiot.com/wlw/ai-agent/cmd/testdata/seedlang"
"code.tczkiot.com/wlw/ai-agent/cmd/testdata/seeds"
"code.tczkiot.com/wlw/ai-agent/internal/models"
"code.tczkiot.com/wlw/ai-agent/internal/pkg/enums"
"code.tczkiot.com/wlw/ai-agent/internal/repositories"
"time"
"github.com/mlogclub/simple/sqls"
+2 -2
View File
@@ -1,8 +1,8 @@
package tag
import (
"agent-desk/cmd/testdata/seedlang"
"agent-desk/cmd/testdata/seeds"
"code.tczkiot.com/wlw/ai-agent/cmd/testdata/seedlang"
"code.tczkiot.com/wlw/ai-agent/cmd/testdata/seeds"
"regexp"
"testing"
)