用户管理增强

This commit is contained in:
537yaha
2026-03-30 18:24:17 +08:00
parent 03c23e0880
commit a65e22c81a
32 changed files with 667 additions and 144 deletions
+5
View File
@@ -61,6 +61,7 @@ type createAgentRequest struct {
Username string `json:"username"`
Password string `json:"password"`
Role string `json:"role"`
Permissions []string `json:"permissions"`
}
// CreateAgent 处理创建客服或管理员账号的请求。
@@ -157,6 +158,7 @@ func (a *AdminController) CreateUser(c *gin.Context) {
Username string `json:"username"`
Password string `json:"password"`
Role string `json:"role"`
Permissions []string `json:"permissions"`
Nickname *string `json:"nickname"`
Email *string `json:"email"`
}
@@ -170,6 +172,7 @@ func (a *AdminController) CreateUser(c *gin.Context) {
Username: req.Username,
Password: req.Password,
Role: req.Role,
Permissions: req.Permissions,
Nickname: req.Nickname,
Email: req.Email,
})
@@ -209,6 +212,7 @@ func (a *AdminController) UpdateUser(c *gin.Context) {
var req struct {
Role *string `json:"role"`
Permissions *[]string `json:"permissions"`
Nickname *string `json:"nickname"`
Email *string `json:"email"`
ReceiveAIConversations *bool `json:"receive_ai_conversations"`
@@ -222,6 +226,7 @@ func (a *AdminController) UpdateUser(c *gin.Context) {
user, err := a.userService.UpdateUser(service.UpdateUserInput{
UserID: uint(id),
Role: req.Role,
Permissions: req.Permissions,
Nickname: req.Nickname,
Email: req.Email,
ReceiveAIConversations: req.ReceiveAIConversations,
+18 -2
View File
@@ -10,11 +10,12 @@ import (
// AIConfigController 负责处理 AI 配置相关的 HTTP 请求。
type AIConfigController struct {
aiConfigService *service.AIConfigService
userService *service.UserService
}
// NewAIConfigController 创建 AI 配置控制器实例。
func NewAIConfigController(aiConfigService *service.AIConfigService) *AIConfigController {
return &AIConfigController{aiConfigService: aiConfigService}
func NewAIConfigController(aiConfigService *service.AIConfigService, userService *service.UserService) *AIConfigController {
return &AIConfigController{aiConfigService: aiConfigService, userService: userService}
}
type createAIConfigRequest struct {
@@ -41,6 +42,9 @@ type updateAIConfigRequest struct {
// CreateAIConfig 创建 AI 配置。
func (a *AIConfigController) CreateAIConfig(c *gin.Context) {
if !requirePermission(c, a.userService, string(service.PermSettings)) {
return
}
userID, err := parseUintParam(c, "user_id")
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "user_id 不合法"})
@@ -74,6 +78,9 @@ func (a *AIConfigController) CreateAIConfig(c *gin.Context) {
// GetAIConfig 获取 AI 配置。
func (a *AIConfigController) GetAIConfig(c *gin.Context) {
if !requirePermission(c, a.userService, string(service.PermSettings)) {
return
}
id, err := parseUintParam(c, "id")
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "id 不合法"})
@@ -91,6 +98,9 @@ func (a *AIConfigController) GetAIConfig(c *gin.Context) {
// ListAIConfigs 获取指定用户的所有 AI 配置。
func (a *AIConfigController) ListAIConfigs(c *gin.Context) {
if !requirePermission(c, a.userService, string(service.PermSettings)) {
return
}
userID, err := parseUintParam(c, "user_id")
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "user_id 不合法"})
@@ -108,6 +118,9 @@ func (a *AIConfigController) ListAIConfigs(c *gin.Context) {
// UpdateAIConfig 更新 AI 配置。
func (a *AIConfigController) UpdateAIConfig(c *gin.Context) {
if !requirePermission(c, a.userService, string(service.PermSettings)) {
return
}
id, err := parseUintParam(c, "id")
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "id 不合法"})
@@ -141,6 +154,9 @@ func (a *AIConfigController) UpdateAIConfig(c *gin.Context) {
// DeleteAIConfig 删除 AI 配置。
func (a *AIConfigController) DeleteAIConfig(c *gin.Context) {
if !requirePermission(c, a.userService, string(service.PermSettings)) {
return
}
id, err := parseUintParam(c, "id")
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "id 不合法"})
+4 -5
View File
@@ -11,17 +11,16 @@ import (
// AnalyticsController 数据分析报表(客服端查询 + 访客端埋点)
type AnalyticsController struct {
analytics *service.AnalyticsService
users *service.UserService
}
func NewAnalyticsController(analytics *service.AnalyticsService) *AnalyticsController {
return &AnalyticsController{analytics: analytics}
func NewAnalyticsController(analytics *service.AnalyticsService, users *service.UserService) *AnalyticsController {
return &AnalyticsController{analytics: analytics, users: users}
}
// GetSummary GET /agent/analytics/summary?from=YYYY-MM-DD&to=YYYY-MM-DD
func (ac *AnalyticsController) GetSummary(c *gin.Context) {
userID := getUserIDFromHeader(c)
if userID == 0 {
c.JSON(http.StatusUnauthorized, gin.H{"error": "未授权,请提供 X-User-Id"})
if !requirePermission(c, ac.users, string(service.PermAnalytics)) {
return
}
from := c.Query("from")
+11
View File
@@ -46,6 +46,17 @@ func (a *AuthController) Login(c *gin.Context) {
"user_id": user.ID,
"username": user.Username,
"role": user.Role,
// permissions 用于前端侧边栏显示(后端强校验以 X-User-Id 为准)
"permissions": func() []string {
if user.Role == "admin" {
return service.AllPermissionKeys()
}
keys := service.DecodePermissions(user.Permissions)
if len(keys) == 0 {
return service.DefaultAgentPermissions()
}
return keys
}(),
})
}
@@ -13,16 +13,19 @@ import (
type ConversationController struct {
conversationService *service.ConversationService
aiConfigService *service.AIConfigService // 用于获取开放的模型列表
users *service.UserService
}
// NewConversationController 创建 ConversationController 实例。
func NewConversationController(
conversationService *service.ConversationService,
aiConfigService *service.AIConfigService,
users *service.UserService,
) *ConversationController {
return &ConversationController{
conversationService: conversationService,
aiConfigService: aiConfigService,
users: users,
}
}
@@ -88,6 +91,9 @@ func (cc *ConversationController) InitConversation(c *gin.Context) {
// InitInternalConversation 为当前客服创建一条新的内部对话(知识库测试)。需要 query user_id。
func (cc *ConversationController) InitInternalConversation(c *gin.Context) {
if !requirePermission(c, cc.users, string(service.PermKBTest)) {
return
}
userIDStr := c.Query("user_id")
if userIDStr == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "需要 user_id"})
@@ -174,6 +180,9 @@ func (cc *ConversationController) ListConversations(c *gin.Context) {
var conversations []service.ConversationSummary
var err error
if conversationType == "internal" {
if !requirePermission(c, cc.users, string(service.PermKBTest)) {
return
}
if userID == 0 {
c.JSON(http.StatusBadRequest, gin.H{"error": "内部对话列表需要 user_id"})
return
+30 -1
View File
@@ -13,13 +13,15 @@ import (
type DocumentController struct {
documentService *service.DocumentService
embeddingConfigService *service.EmbeddingConfigService
users *service.UserService
}
// NewDocumentController 创建文档控制器实例
func NewDocumentController(documentService *service.DocumentService, embeddingConfigService *service.EmbeddingConfigService) *DocumentController {
func NewDocumentController(documentService *service.DocumentService, embeddingConfigService *service.EmbeddingConfigService, users *service.UserService) *DocumentController {
return &DocumentController{
documentService: documentService,
embeddingConfigService: embeddingConfigService,
users: users,
}
}
@@ -37,6 +39,9 @@ func (c *DocumentController) checkKBAccess(ctx *gin.Context) bool {
// ListDocuments 获取文档列表
func (c *DocumentController) ListDocuments(ctx *gin.Context) {
if !requirePermission(ctx, c.users, string(service.PermKnowledge)) {
return
}
if !c.checkKBAccess(ctx) {
return
}
@@ -70,6 +75,9 @@ func (c *DocumentController) ListDocuments(ctx *gin.Context) {
// GetDocument 获取文档详情
func (c *DocumentController) GetDocument(ctx *gin.Context) {
if !requirePermission(ctx, c.users, string(service.PermKnowledge)) {
return
}
if !c.checkKBAccess(ctx) {
return
}
@@ -92,6 +100,9 @@ func (c *DocumentController) GetDocument(ctx *gin.Context) {
// CreateDocument 创建文档
func (c *DocumentController) CreateDocument(ctx *gin.Context) {
if !requirePermission(ctx, c.users, string(service.PermKnowledge)) {
return
}
if !c.checkKBAccess(ctx) {
return
}
@@ -128,6 +139,9 @@ func (c *DocumentController) CreateDocument(ctx *gin.Context) {
// UpdateDocument 更新文档
func (c *DocumentController) UpdateDocument(ctx *gin.Context) {
if !requirePermission(ctx, c.users, string(service.PermKnowledge)) {
return
}
if !c.checkKBAccess(ctx) {
return
}
@@ -169,6 +183,9 @@ func (c *DocumentController) UpdateDocument(ctx *gin.Context) {
// DeleteDocument 删除文档
func (c *DocumentController) DeleteDocument(ctx *gin.Context) {
if !requirePermission(ctx, c.users, string(service.PermKnowledge)) {
return
}
if !c.checkKBAccess(ctx) {
return
}
@@ -190,6 +207,9 @@ func (c *DocumentController) DeleteDocument(ctx *gin.Context) {
// SearchDocuments 向量检索搜索文档
func (c *DocumentController) SearchDocuments(ctx *gin.Context) {
if !requirePermission(ctx, c.users, string(service.PermKnowledge)) {
return
}
if !c.checkKBAccess(ctx) {
return
}
@@ -236,6 +256,9 @@ func (c *DocumentController) HybridSearchDocuments(ctx *gin.Context) {
// UpdateDocumentStatus 更新文档状态
func (c *DocumentController) UpdateDocumentStatus(ctx *gin.Context) {
if !requirePermission(ctx, c.users, string(service.PermKnowledge)) {
return
}
if !c.checkKBAccess(ctx) {
return
}
@@ -266,6 +289,9 @@ func (c *DocumentController) UpdateDocumentStatus(ctx *gin.Context) {
// PublishDocument 发布文档
func (c *DocumentController) PublishDocument(ctx *gin.Context) {
if !requirePermission(ctx, c.users, string(service.PermKnowledge)) {
return
}
if !c.checkKBAccess(ctx) {
return
}
@@ -287,6 +313,9 @@ func (c *DocumentController) PublishDocument(ctx *gin.Context) {
// UnpublishDocument 取消发布文档
func (c *DocumentController) UnpublishDocument(ctx *gin.Context) {
if !requirePermission(ctx, c.users, string(service.PermKnowledge)) {
return
}
if !c.checkKBAccess(ctx) {
return
}
@@ -10,16 +10,20 @@ import (
// EmbeddingConfigController 知识库向量配置控制器
type EmbeddingConfigController struct {
service *service.EmbeddingConfigService
users *service.UserService
}
// NewEmbeddingConfigController 创建控制器实例
func NewEmbeddingConfigController(s *service.EmbeddingConfigService) *EmbeddingConfigController {
return &EmbeddingConfigController{service: s}
func NewEmbeddingConfigController(s *service.EmbeddingConfigService, users *service.UserService) *EmbeddingConfigController {
return &EmbeddingConfigController{service: s, users: users}
}
// Get 获取当前配置(API Key 脱敏)
// GET /agent/embedding-config?user_id=1
func (e *EmbeddingConfigController) Get(c *gin.Context) {
if !requirePermission(c, e.users, string(service.PermSettings)) {
return
}
_, err := parseUintQuery(c, "user_id")
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "user_id 不合法"})
@@ -37,6 +41,9 @@ func (e *EmbeddingConfigController) Get(c *gin.Context) {
// PUT /agent/embedding-config
// Body: { "user_id": 1, "embedding_type": "openai", "api_url": "...", "api_key": "...", "model": "...", "customer_can_use_kb": true }
func (e *EmbeddingConfigController) Update(c *gin.Context) {
if !requirePermission(c, e.users, string(service.PermSettings)) {
return
}
var req struct {
UserID uint `json:"user_id" binding:"required"`
EmbeddingType *string `json:"embedding_type"`
+18 -2
View File
@@ -12,16 +12,20 @@ import (
// FAQController 负责处理 FAQ(常见问题)相关的 HTTP 请求。
type FAQController struct {
faqService *service.FAQService
users *service.UserService
}
// NewFAQController 创建 FAQController 实例。
func NewFAQController(faqService *service.FAQService) *FAQController {
return &FAQController{faqService: faqService}
func NewFAQController(faqService *service.FAQService, users *service.UserService) *FAQController {
return &FAQController{faqService: faqService, users: users}
}
// ListFAQs 获取 FAQ 列表,支持关键词搜索。
// GET /faqs?query=openai%api%调用
func (f *FAQController) ListFAQs(c *gin.Context) {
if !requirePermission(c, f.users, string(service.PermFAQs)) {
return
}
// 获取查询参数
query := c.Query("query")
@@ -41,6 +45,9 @@ func (f *FAQController) ListFAQs(c *gin.Context) {
// GetFAQ 获取 FAQ 详情。
// GET /faqs/:id
func (f *FAQController) GetFAQ(c *gin.Context) {
if !requirePermission(c, f.users, string(service.PermFAQs)) {
return
}
// 获取 ID
idStr := c.Param("id")
id, err := strconv.ParseUint(idStr, 10, 64)
@@ -63,6 +70,9 @@ func (f *FAQController) GetFAQ(c *gin.Context) {
// CreateFAQ 创建新的 FAQ 记录。
// POST /faqs
func (f *FAQController) CreateFAQ(c *gin.Context) {
if !requirePermission(c, f.users, string(service.PermFAQs)) {
return
}
var req struct {
Question string `json:"question" binding:"required"`
Answer string `json:"answer" binding:"required"`
@@ -92,6 +102,9 @@ func (f *FAQController) CreateFAQ(c *gin.Context) {
// UpdateFAQ 更新 FAQ 记录。
// PUT /faqs/:id
func (f *FAQController) UpdateFAQ(c *gin.Context) {
if !requirePermission(c, f.users, string(service.PermFAQs)) {
return
}
// 获取 ID
idStr := c.Param("id")
id, err := strconv.ParseUint(idStr, 10, 64)
@@ -129,6 +142,9 @@ func (f *FAQController) UpdateFAQ(c *gin.Context) {
// DeleteFAQ 删除 FAQ 记录。
// DELETE /faqs/:id
func (f *FAQController) DeleteFAQ(c *gin.Context) {
if !requirePermission(c, f.users, string(service.PermFAQs)) {
return
}
// 获取 ID
idStr := c.Param("id")
id, err := strconv.ParseUint(idStr, 10, 64)
+17
View File
@@ -4,6 +4,7 @@ import (
"strconv"
"time"
"github.com/2930134478/AI-CS/backend/service"
"github.com/gin-gonic/gin"
)
@@ -60,3 +61,19 @@ func getTraceID(c *gin.Context) string {
}
return ""
}
// requirePermission 统一的权限校验(基于 X-User-Id)。
// 返回 true 表示允许继续;false 表示已输出错误响应。
func requirePermission(c *gin.Context, userSvc *service.UserService, perm string) bool {
if userSvc == nil {
c.JSON(500, gin.H{"error": "权限服务未初始化"})
return false
}
userID := getUserIDFromHeader(c)
if err := userSvc.CheckPermission(userID, perm); err != nil {
// 未授权/无权限统一 403(避免泄露过多信息)
c.JSON(403, gin.H{"error": err.Error()})
return false
}
return true
}
+9 -1
View File
@@ -17,13 +17,15 @@ import (
type ImportController struct {
importService *service.ImportService
embeddingConfigService *service.EmbeddingConfigService
users *service.UserService
}
// NewImportController 创建导入控制器实例
func NewImportController(importService *service.ImportService, embeddingConfigService *service.EmbeddingConfigService) *ImportController {
func NewImportController(importService *service.ImportService, embeddingConfigService *service.EmbeddingConfigService, users *service.UserService) *ImportController {
return &ImportController{
importService: importService,
embeddingConfigService: embeddingConfigService,
users: users,
}
}
@@ -43,6 +45,9 @@ func (c *ImportController) checkKBAccess(ctx *gin.Context) bool {
// ImportDocuments 批量导入文档(文件上传)
func (c *ImportController) ImportDocuments(ctx *gin.Context) {
if !requirePermission(ctx, c.users, string(service.PermKnowledge)) {
return
}
if !c.checkKBAccess(ctx) {
return
}
@@ -138,6 +143,9 @@ func (c *ImportController) ImportDocuments(ctx *gin.Context) {
// ImportFromURLs 批量导入文档(URL 爬取)
func (c *ImportController) ImportFromURLs(ctx *gin.Context) {
if !requirePermission(ctx, c.users, string(service.PermKnowledge)) {
return
}
if !c.checkKBAccess(ctx) {
return
}
@@ -13,13 +13,15 @@ import (
type KnowledgeBaseController struct {
knowledgeBaseService *service.KnowledgeBaseService
embeddingConfigService *service.EmbeddingConfigService
users *service.UserService
}
// NewKnowledgeBaseController 创建知识库控制器实例
func NewKnowledgeBaseController(knowledgeBaseService *service.KnowledgeBaseService, embeddingConfigService *service.EmbeddingConfigService) *KnowledgeBaseController {
func NewKnowledgeBaseController(knowledgeBaseService *service.KnowledgeBaseService, embeddingConfigService *service.EmbeddingConfigService, users *service.UserService) *KnowledgeBaseController {
return &KnowledgeBaseController{
knowledgeBaseService: knowledgeBaseService,
embeddingConfigService: embeddingConfigService,
users: users,
}
}
@@ -38,6 +40,9 @@ func (c *KnowledgeBaseController) checkKBAccess(ctx *gin.Context) bool {
// ListKnowledgeBases 获取知识库列表
func (c *KnowledgeBaseController) ListKnowledgeBases(ctx *gin.Context) {
if !requirePermission(ctx, c.users, string(service.PermKnowledge)) {
return
}
if !c.checkKBAccess(ctx) {
return
}
@@ -55,6 +60,9 @@ func (c *KnowledgeBaseController) ListKnowledgeBases(ctx *gin.Context) {
// GetKnowledgeBase 获取知识库详情
func (c *KnowledgeBaseController) GetKnowledgeBase(ctx *gin.Context) {
if !requirePermission(ctx, c.users, string(service.PermKnowledge)) {
return
}
if !c.checkKBAccess(ctx) {
return
}
@@ -77,6 +85,9 @@ func (c *KnowledgeBaseController) GetKnowledgeBase(ctx *gin.Context) {
// CreateKnowledgeBase 创建知识库
func (c *KnowledgeBaseController) CreateKnowledgeBase(ctx *gin.Context) {
if !requirePermission(ctx, c.users, string(service.PermKnowledge)) {
return
}
if !c.checkKBAccess(ctx) {
return
}
@@ -105,6 +116,9 @@ func (c *KnowledgeBaseController) CreateKnowledgeBase(ctx *gin.Context) {
// UpdateKnowledgeBase 更新知识库
func (c *KnowledgeBaseController) UpdateKnowledgeBase(ctx *gin.Context) {
if !requirePermission(ctx, c.users, string(service.PermKnowledge)) {
return
}
if !c.checkKBAccess(ctx) {
return
}
@@ -142,6 +156,9 @@ func (c *KnowledgeBaseController) UpdateKnowledgeBase(ctx *gin.Context) {
// DeleteKnowledgeBase 删除知识库
func (c *KnowledgeBaseController) DeleteKnowledgeBase(ctx *gin.Context) {
if !requirePermission(ctx, c.users, string(service.PermKnowledge)) {
return
}
if !c.checkKBAccess(ctx) {
return
}
@@ -163,6 +180,9 @@ func (c *KnowledgeBaseController) DeleteKnowledgeBase(ctx *gin.Context) {
// UpdateKnowledgeBaseRAGEnabled 仅更新知识库「参与 RAG」开关。
func (c *KnowledgeBaseController) UpdateKnowledgeBaseRAGEnabled(ctx *gin.Context) {
if !requirePermission(ctx, c.users, string(service.PermKnowledge)) {
return
}
if !c.checkKBAccess(ctx) {
return
}
@@ -10,16 +10,20 @@ import (
// PromptConfigController 提示词配置控制器(供「提示词」页)
type PromptConfigController struct {
service *service.PromptConfigService
users *service.UserService
}
// NewPromptConfigController 创建控制器实例
func NewPromptConfigController(s *service.PromptConfigService) *PromptConfigController {
return &PromptConfigController{service: s}
func NewPromptConfigController(s *service.PromptConfigService, users *service.UserService) *PromptConfigController {
return &PromptConfigController{service: s, users: users}
}
// Get 获取所有提示词项(含默认内容)
// GET /agent/prompts?user_id=1
func (p *PromptConfigController) Get(c *gin.Context) {
if !requirePermission(c, p.users, string(service.PermPrompts)) {
return
}
_, err := parseUintQuery(c, "user_id")
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "user_id 不合法"})
@@ -37,6 +41,9 @@ func (p *PromptConfigController) Get(c *gin.Context) {
// PUT /agent/prompts
// Body: { "user_id": 1, "key": "rag_prompt", "content": "..." }
func (p *PromptConfigController) Update(c *gin.Context) {
if !requirePermission(c, p.users, string(service.PermPrompts)) {
return
}
var req struct {
UserID uint `json:"user_id" binding:"required"`
Key string `json:"key" binding:"required"`
+4 -5
View File
@@ -11,17 +11,16 @@ import (
type SystemLogController struct {
logs *service.SystemLogService
users *service.UserService
}
func NewSystemLogController(logs *service.SystemLogService) *SystemLogController {
return &SystemLogController{logs: logs}
func NewSystemLogController(logs *service.SystemLogService, users *service.UserService) *SystemLogController {
return &SystemLogController{logs: logs, users: users}
}
// GetLogs 查询日志(客服端)。
func (lc *SystemLogController) GetLogs(c *gin.Context) {
userID := getUserIDFromHeader(c)
if userID == 0 {
c.JSON(http.StatusUnauthorized, gin.H{"error": "未授权,请提供 X-User-Id"})
if !requirePermission(c, lc.users, string(service.PermLogs)) {
return
}
var convID *uint
+10 -10
View File
@@ -460,24 +460,24 @@ func main() {
// 初始化控制器
authController := controller.NewAuthController(authService)
conversationController := controller.NewConversationController(conversationService, aiConfigService)
conversationController := controller.NewConversationController(conversationService, aiConfigService, userService)
messageController := controller.NewMessageController(messageService, conversationService, storageService)
adminController := controller.NewAdminController(authService, userService)
profileController := controller.NewProfileController(profileService)
aiConfigController := controller.NewAIConfigController(aiConfigService)
faqController := controller.NewFAQController(faqService)
documentController := controller.NewDocumentController(documentService, embeddingConfigService)
embeddingConfigController := controller.NewEmbeddingConfigController(embeddingConfigService)
promptConfigController := controller.NewPromptConfigController(promptConfigService)
knowledgeBaseController := controller.NewKnowledgeBaseController(knowledgeBaseService, embeddingConfigService)
importController := controller.NewImportController(importService, embeddingConfigService) // 导入控制器
aiConfigController := controller.NewAIConfigController(aiConfigService, userService)
faqController := controller.NewFAQController(faqService, userService)
documentController := controller.NewDocumentController(documentService, embeddingConfigService, userService)
embeddingConfigController := controller.NewEmbeddingConfigController(embeddingConfigService, userService)
promptConfigController := controller.NewPromptConfigController(promptConfigService, userService)
knowledgeBaseController := controller.NewKnowledgeBaseController(knowledgeBaseService, embeddingConfigService, userService)
importController := controller.NewImportController(importService, embeddingConfigService, userService) // 导入控制器
visitorController := controller.NewVisitorController(visitorService, embeddingConfigService)
healthController := controller.NewHealthController(healthChecker, retrievalService) // 健康检查控制器
widgetOpenRepo := repository.NewWidgetOpenRepository(db)
analyticsService := service.NewAnalyticsService(db, widgetOpenRepo)
analyticsController := controller.NewAnalyticsController(analyticsService)
systemLogController := controller.NewSystemLogController(systemLogService)
analyticsController := controller.NewAnalyticsController(analyticsService, userService)
systemLogController := controller.NewSystemLogController(systemLogService, userService)
appRouter.RegisterRoutes(
r,
+3
View File
@@ -9,6 +9,9 @@ type User struct {
Username string `json:"username" gorm:"unique"`
Password string `json:"password"`
Role string `json:"role"`
// Permissions 功能权限(JSON 数组字符串)。admin 默认视为全权限。
// 例:["chat","knowledge"]。为空时:agent 兼容默认仅 chat。
Permissions string `json:"permissions" gorm:"type:text"`
AvatarURL string `json:"avatar_url" gorm:"type:varchar(500)"` // 头像URL
Nickname string `json:"nickname" gorm:"type:varchar(100)"` // 昵称
Email string `json:"email" gorm:"type:varchar(255)"` // 邮箱
+102
View File
@@ -0,0 +1,102 @@
package service
import (
"encoding/json"
"errors"
"sort"
"strings"
)
// PermissionKey 定义客服端菜单/功能的权限键(单级开关:有/无)。
// 说明:目前系统没有真正的登录态(JWT/Session),所以权限校验依赖 X-User-Id。
type PermissionKey string
const (
PermChat PermissionKey = "chat" // 对话
PermKBTest PermissionKey = "kb_test" // 知识库测试(内部对话)
PermKnowledge PermissionKey = "knowledge" // 知识库(含文档/导入)
PermFAQs PermissionKey = "faqs" // 事件管理
PermAnalytics PermissionKey = "analytics" // 数据报表
PermLogs PermissionKey = "logs" // 日志中心
PermPrompts PermissionKey = "prompts" // 提示词
PermSettings PermissionKey = "settings" // AI 配置
PermUsers PermissionKey = "users" // 用户管理
)
func AllPermissionKeys() []string {
keys := []string{
string(PermChat),
string(PermKBTest),
string(PermKnowledge),
string(PermFAQs),
string(PermAnalytics),
string(PermLogs),
string(PermPrompts),
string(PermSettings),
string(PermUsers),
}
sort.Strings(keys)
return keys
}
func DefaultAgentPermissions() []string {
return []string{string(PermChat)}
}
func normalizePermissionKeys(keys []string) []string {
seen := map[string]bool{}
out := make([]string, 0, len(keys))
for _, k := range keys {
kk := strings.TrimSpace(k)
if kk == "" {
continue
}
if seen[kk] {
continue
}
seen[kk] = true
out = append(out, kk)
}
sort.Strings(out)
return out
}
func validatePermissionKeys(keys []string) error {
allowed := map[string]bool{}
for _, k := range AllPermissionKeys() {
allowed[k] = true
}
for _, k := range keys {
if !allowed[k] {
return errors.New("存在不支持的权限键: " + k)
}
}
return nil
}
// EncodePermissions 将权限数组编码为 JSON 字符串,用于存表。
func EncodePermissions(keys []string) (string, error) {
n := normalizePermissionKeys(keys)
if err := validatePermissionKeys(n); err != nil {
return "", err
}
b, err := json.Marshal(n)
if err != nil {
return "", err
}
return string(b), nil
}
// DecodePermissions 从 JSON 字符串解码权限数组。空串/无效 JSON 视为无权限数组。
func DecodePermissions(raw string) []string {
raw = strings.TrimSpace(raw)
if raw == "" {
return nil
}
var keys []string
if err := json.Unmarshal([]byte(raw), &keys); err != nil {
return nil
}
return normalizePermissionKeys(keys)
}
+10
View File
@@ -37,6 +37,16 @@ func (s *ProfileService) GetProfile(userID uint) (*ProfileResult, error) {
ID: user.ID,
Username: user.Username,
Role: user.Role,
Permissions: func() []string {
if user.Role == "admin" {
return AllPermissionKeys()
}
keys := DecodePermissions(user.Permissions)
if len(keys) == 0 {
return DefaultAgentPermissions()
}
return keys
}(),
AvatarURL: user.AvatarURL,
Nickname: user.Nickname,
Email: user.Email,
+4
View File
@@ -125,6 +125,7 @@ type ProfileResult struct {
ID uint `json:"id"`
Username string `json:"username"`
Role string `json:"role"`
Permissions []string `json:"permissions"`
AvatarURL string `json:"avatar_url"`
Nickname string `json:"nickname"`
Email string `json:"email"`
@@ -136,6 +137,7 @@ type UserSummary struct {
ID uint `json:"id"`
Username string `json:"username"`
Role string `json:"role"`
Permissions []string `json:"permissions"`
Nickname string `json:"nickname"`
Email string `json:"email"`
AvatarURL string `json:"avatar_url"`
@@ -149,6 +151,7 @@ type CreateUserInput struct {
Username string // 用户名(必需)
Password string // 密码(必需)
Role string // 角色:"admin" 或 "agent"(必需)
Permissions []string // 功能权限(可选;role=admin 时忽略)
Nickname *string // 昵称(可选)
Email *string // 邮箱(可选)
}
@@ -157,6 +160,7 @@ type CreateUserInput struct {
type UpdateUserInput struct {
UserID uint // 用户ID(必需)
Role *string // 角色(可选)
Permissions *[]string // 功能权限(可选;role=admin 时忽略)
Nickname *string // 昵称(可选)
Email *string // 邮箱(可选)
ReceiveAIConversations *bool // 是否接收 AI 对话(可选)
+78 -1
View File
@@ -2,6 +2,7 @@ package service
import (
"errors"
"fmt"
"strings"
"github.com/2930134478/AI-CS/backend/models"
@@ -20,6 +21,43 @@ func NewUserService(users *repository.UserRepository) *UserService {
return &UserService{users: users}
}
// EffectivePermissions 计算用户“有效权限”。
// - admin:全权限
// - agent:取 user.PermissionsJSON);若为空则兼容默认仅 chat
func (s *UserService) EffectivePermissions(user *models.User) []string {
if user == nil {
return nil
}
if user.Role == "admin" {
return AllPermissionKeys()
}
keys := DecodePermissions(user.Permissions)
if len(keys) == 0 {
return DefaultAgentPermissions()
}
return keys
}
// CheckPermission 校验用户是否拥有指定权限(用于控制器强校验)。
func (s *UserService) CheckPermission(userID uint, perm string) error {
if userID == 0 {
return errors.New("未授权访问,请提供 X-User-Id 请求头")
}
u, err := s.users.GetByID(userID)
if err != nil || u == nil {
return errors.New("用户不存在")
}
if u.Role == "admin" {
return nil
}
for _, p := range s.EffectivePermissions(u) {
if p == perm {
return nil
}
}
return fmt.Errorf("权限不足:缺少功能权限 %s", perm)
}
// ListUsers 获取所有用户列表。
func (s *UserService) ListUsers() ([]UserSummary, error) {
users, err := s.users.ListUsers()
@@ -33,6 +71,7 @@ func (s *UserService) ListUsers() ([]UserSummary, error) {
ID: user.ID,
Username: user.Username,
Role: user.Role,
Permissions: s.EffectivePermissions(&user),
Nickname: user.Nickname,
Email: user.Email,
AvatarURL: user.AvatarURL,
@@ -59,6 +98,7 @@ func (s *UserService) GetUser(id uint) (*UserSummary, error) {
ID: user.ID,
Username: user.Username,
Role: user.Role,
Permissions: s.EffectivePermissions(user),
Nickname: user.Nickname,
Email: user.Email,
AvatarURL: user.AvatarURL,
@@ -101,6 +141,19 @@ func (s *UserService) CreateUser(input CreateUserInput) (*UserSummary, error) {
ReceiveAIConversations: true, // 默认接收 AI 对话
}
// 权限:admin 默认全开(不存);agent 默认仅 chat
if input.Role != "admin" {
keys := input.Permissions
if len(keys) == 0 {
keys = DefaultAgentPermissions()
}
encoded, err := EncodePermissions(keys)
if err != nil {
return nil, err
}
user.Permissions = encoded
}
// 设置可选字段
if input.Nickname != nil {
user.Nickname = strings.TrimSpace(*input.Nickname)
@@ -117,6 +170,7 @@ func (s *UserService) CreateUser(input CreateUserInput) (*UserSummary, error) {
ID: user.ID,
Username: user.Username,
Role: user.Role,
Permissions: s.EffectivePermissions(user),
Nickname: user.Nickname,
Email: user.Email,
AvatarURL: user.AvatarURL,
@@ -129,17 +183,22 @@ func (s *UserService) CreateUser(input CreateUserInput) (*UserSummary, error) {
// UpdateUser 更新用户信息。
func (s *UserService) UpdateUser(input UpdateUserInput) (*UserSummary, error) {
// 检查用户是否存在
_, err := s.users.GetByID(input.UserID)
currentUser, err := s.users.GetByID(input.UserID)
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, errors.New("用户不存在")
}
return nil, err
}
if currentUser == nil {
return nil, errors.New("用户不存在")
}
// 构建更新字段
updates := make(map[string]interface{})
// 记录本次更新后的角色(用于决定 permissions 写入规则)
nextRole := currentUser.Role
// 更新角色
if input.Role != nil {
role := strings.TrimSpace(*input.Role)
@@ -147,6 +206,24 @@ func (s *UserService) UpdateUser(input UpdateUserInput) (*UserSummary, error) {
return nil, errors.New("角色只能是 admin 或 agent")
}
updates["role"] = role
nextRole = role
}
// 更新 permissions(仅对 agent 有意义;admin 视为全开,不存权限)
if input.Permissions != nil {
if nextRole == "admin" {
updates["permissions"] = ""
} else {
keys := *input.Permissions
if len(keys) == 0 {
keys = DefaultAgentPermissions()
}
encoded, err := EncodePermissions(keys)
if err != nil {
return nil, err
}
updates["permissions"] = encoded
}
}
// 更新昵称
+4
View File
@@ -38,6 +38,10 @@ export default function AgentLoginPage() {
localStorage.setItem("agent_user_id", String(data.user_id));
localStorage.setItem("agent_username", data.username);
localStorage.setItem("agent_role", data.role);
localStorage.setItem(
"agent_permissions",
JSON.stringify(Array.isArray(data.permissions) ? data.permissions : [])
);
// 跳转到客服工作台(三栏布局)
router.push("/agent/dashboard");
+83
View File
@@ -27,6 +27,11 @@ import { Badge } from "@/components/ui/badge";
import { Card } from "@/components/ui/card";
import { Label } from "@/components/ui/label";
import { toast } from "@/hooks/useToast";
import {
PERMISSION_OPTIONS,
defaultAgentPermissions,
type PermissionKey,
} from "@/lib/constants/agent-permissions";
import {
Plus,
Edit,
@@ -57,6 +62,7 @@ export default function UsersPage(props: any = {}) {
username: "",
password: "",
role: "agent",
permissions: defaultAgentPermissions(),
nickname: "",
email: "",
});
@@ -64,6 +70,7 @@ export default function UsersPage(props: any = {}) {
// 编辑用户表单
const [editForm, setEditForm] = useState<UpdateUserRequest>({
role: "agent",
permissions: defaultAgentPermissions(),
nickname: "",
email: "",
receive_ai_conversations: true,
@@ -123,6 +130,7 @@ export default function UsersPage(props: any = {}) {
username: "",
password: "",
role: "agent",
permissions: defaultAgentPermissions(),
nickname: "",
email: "",
});
@@ -156,6 +164,11 @@ export default function UsersPage(props: any = {}) {
setSelectedUser(user);
setEditForm({
role: user.role as "admin" | "agent",
permissions:
user.role === "admin"
? PERMISSION_OPTIONS.map((p) => p.key)
: ((user.permissions as PermissionKey[] | undefined) ??
defaultAgentPermissions()),
nickname: user.nickname || "",
email: user.email || "",
receive_ai_conversations: user.receive_ai_conversations,
@@ -428,6 +441,10 @@ export default function UsersPage(props: any = {}) {
setCreateForm({
...createForm,
role: e.target.value as "admin" | "agent",
permissions:
e.target.value === "admin"
? PERMISSION_OPTIONS.map((p) => p.key)
: defaultAgentPermissions(),
})
}
className="w-full px-3 py-2 border border-border rounded-md bg-background"
@@ -436,6 +453,39 @@ export default function UsersPage(props: any = {}) {
<option value="admin"></option>
</select>
</div>
{/* 功能权限(开/关一级;role=admin 默认全开) */}
{createForm.role !== "admin" && (
<div>
<Label></Label>
<div className="mt-2 grid grid-cols-2 gap-2">
{PERMISSION_OPTIONS.map((p) => {
const checked = (createForm.permissions ?? []).includes(p.key);
return (
<label
key={p.key}
className="flex items-center gap-2 rounded-md border border-border/70 bg-background px-3 py-2 text-sm"
>
<input
type="checkbox"
checked={checked}
onChange={(e) => {
const next = new Set(createForm.permissions ?? []);
if (e.target.checked) next.add(p.key);
else next.delete(p.key);
setCreateForm({ ...createForm, permissions: Array.from(next) });
}}
/>
<span>{p.label}</span>
</label>
);
})}
</div>
<p className="mt-1 text-xs text-muted-foreground">
403
</p>
</div>
)}
<div>
<Label htmlFor="create-nickname"></Label>
<Input
@@ -499,6 +549,10 @@ export default function UsersPage(props: any = {}) {
setEditForm({
...editForm,
role: e.target.value as "admin" | "agent",
permissions:
e.target.value === "admin"
? PERMISSION_OPTIONS.map((p) => p.key)
: defaultAgentPermissions(),
})
}
className="w-full px-3 py-2 border border-border rounded-md bg-background"
@@ -507,6 +561,35 @@ export default function UsersPage(props: any = {}) {
<option value="admin"></option>
</select>
</div>
{editForm.role !== "admin" && (
<div>
<Label></Label>
<div className="mt-2 grid grid-cols-2 gap-2">
{PERMISSION_OPTIONS.map((p) => {
const checked = (editForm.permissions ?? []).includes(p.key);
return (
<label
key={p.key}
className="flex items-center gap-2 rounded-md border border-border/70 bg-background px-3 py-2 text-sm"
>
<input
type="checkbox"
checked={checked}
onChange={(e) => {
const next = new Set(editForm.permissions ?? []);
if (e.target.checked) next.add(p.key);
else next.delete(p.key);
setEditForm({ ...editForm, permissions: Array.from(next) });
}}
/>
<span>{p.label}</span>
</label>
);
})}
</div>
</div>
)}
<div>
<Label htmlFor="edit-nickname"></Label>
<Input
@@ -6,6 +6,7 @@ import { getAvatarUrl, getAvatarColor, getAvatarInitial } from "@/utils/avatar";
import { Button } from "@/components/ui/button";
import { websiteConfig } from "@/lib/website-config";
import { Badge } from "@/components/ui/badge";
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip";
import {
AGENT_PAGES,
type NavigationPage,
@@ -36,6 +37,7 @@ export function NavigationSidebar({
const menuRef = useRef<HTMLDivElement>(null);
const isAdmin = agent?.role === "admin";
const permissions = agent?.permissions ?? [];
const handleNavigate = (page: NavigationPage) => {
onNavigate?.(page);
@@ -61,73 +63,86 @@ export function NavigationSidebar({
const displayInitial = getAvatarInitial(agent?.username || "");
const fullAvatarUrl = getAvatarUrl(avatarUrl);
const visiblePages = AGENT_PAGES.filter(
(p) => !p.adminOnly || (p.adminOnly && isAdmin)
);
const visiblePages = AGENT_PAGES.filter((p) => {
if (isAdmin) return true;
const need = p.requiredPermission;
if (!need) return true;
return permissions.includes(need);
});
return (
<div className="w-16 bg-gray-50 flex flex-col items-center py-4 border-r border-gray-200 h-full">
{visiblePages.map((page) => {
const isActive = currentPage === page.id;
const Icon = page.Icon;
const showUnread = page.id === "dashboard" && unreadChatCount > 0;
return (
<button
key={page.id}
className={`w-10 h-10 rounded-lg flex items-center justify-center mb-4 transition-colors ${
isActive
? "bg-green-600 hover:bg-green-700"
: "bg-white border border-gray-200 hover:bg-gray-100"
}`}
title={page.title}
onClick={() => handleNavigate(page.id as NavigationPage)}
>
<div className="relative w-full h-full flex items-center justify-center">
<Icon
className={`w-6 h-6 ${
isActive ? "text-white" : "text-gray-600"
}`}
/>
{showUnread && (
<Badge
variant="destructive"
className="absolute -top-1 -right-1 px-1 py-0 h-4 min-w-4 rounded-full text-[10px] leading-none flex items-center justify-center"
>
{unreadChatCount > 99 ? "99+" : unreadChatCount}
</Badge>
)}
</div>
</button>
);
})}
<TooltipProvider delayDuration={0}>
<div className="w-16 bg-gray-50 flex flex-col items-center py-4 border-r border-gray-200 h-full">
<div className="mt-2 px-3 flex flex-col items-center gap-2">
{visiblePages.map((page) => {
const isActive = currentPage === page.id;
const Icon = page.Icon;
const showUnread = page.id === "dashboard" && unreadChatCount > 0;
return (
<Tooltip key={page.id}>
<TooltipTrigger asChild>
<button
className={`w-10 h-10 rounded-lg flex items-center justify-center transition-colors ${
isActive
? "bg-green-600 hover:bg-green-700 text-white"
: "bg-white border border-gray-200 hover:bg-gray-100 text-gray-700"
}`}
onClick={() => handleNavigate(page.id as NavigationPage)}
aria-label={page.title}
>
<div className="relative flex items-center justify-center">
<Icon className={`h-5 w-5 ${isActive ? "text-white" : "text-gray-600"}`} />
{showUnread && (
<Badge
variant="destructive"
className="absolute -top-1 -right-1 px-1 py-0 h-4 min-w-4 rounded-full text-[10px] leading-none flex items-center justify-center"
>
{unreadChatCount > 99 ? "99+" : unreadChatCount}
</Badge>
)}
</div>
</button>
</TooltipTrigger>
<TooltipContent side="right">{page.title}</TooltipContent>
</Tooltip>
);
})}
</div>
{/* 个人资料按钮和 GitHub 按钮(固定在底部) */}
<div className="mt-auto flex flex-col items-center gap-2">
<div className="relative" ref={menuRef}>
<button
className={`w-10 h-10 rounded-lg flex items-center justify-center transition-colors ${
profileMenuOpen
? "bg-primary text-primary-foreground"
: "bg-white border border-gray-200 hover:bg-gray-100"
}`}
title="个人资料"
onClick={() => setProfileMenuOpen(!profileMenuOpen)}
>
{fullAvatarUrl ? (
<img
src={fullAvatarUrl}
alt={agent?.username || "用户"}
className="w-8 h-8 rounded-full object-cover"
/>
) : (
<div
className="w-8 h-8 rounded-full flex items-center justify-center text-white text-xs font-semibold"
style={{ backgroundColor: avatarColor }}
>
{displayInitial}
</div>
)}
</button>
<div className="mt-auto flex flex-col items-center gap-2">
<div className="relative" ref={menuRef}>
<Tooltip>
<TooltipTrigger asChild>
<button
className={`w-10 h-10 rounded-lg flex items-center justify-center transition-colors ${
profileMenuOpen
? "bg-primary text-primary-foreground"
: "bg-white border border-gray-200 hover:bg-gray-100"
}`}
onClick={() => setProfileMenuOpen(!profileMenuOpen)}
aria-label="个人资料"
>
<div className="flex items-center justify-center">
{fullAvatarUrl ? (
<img
src={fullAvatarUrl}
alt={agent?.username || "用户"}
className="w-8 h-8 rounded-full object-cover"
/>
) : (
<div
className="w-8 h-8 rounded-full flex items-center justify-center text-white text-xs font-semibold"
style={{ backgroundColor: avatarColor }}
>
{displayInitial}
</div>
)}
</div>
</button>
</TooltipTrigger>
<TooltipContent side="right"></TooltipContent>
</Tooltip>
{profileMenuOpen && (
<div className="absolute bottom-12 left-0 w-64 bg-white border border-gray-200 rounded-lg shadow-lg z-50">
@@ -212,28 +227,34 @@ export function NavigationSidebar({
)}
</div>
<Button
variant="ghost"
size="sm"
asChild
className="w-10 h-10 rounded-lg bg-white border border-gray-200 hover:bg-gray-100 transition-colors p-0"
title="GitHub"
>
<a
href={websiteConfig.github.repo}
target="_blank"
rel="noopener noreferrer"
>
<svg
className="w-5 h-5 text-gray-600"
fill="currentColor"
viewBox="0 0 24 24"
>
<path d="M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23.957-.266 1.983-.399 3.003-.404 1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576 4.765-1.589 8.199-6.086 8.199-11.386 0-6.627-5.373-12-12-12z" />
</svg>
</a>
</Button>
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="ghost"
size="sm"
asChild
className="w-10 h-10 rounded-lg bg-white border border-gray-200 hover:bg-gray-100 transition-colors p-0"
>
<a
href={websiteConfig.github.repo}
target="_blank"
rel="noopener noreferrer"
aria-label="GitHub"
>
<svg
className="w-5 h-5 text-gray-600"
fill="currentColor"
viewBox="0 0 24 24"
>
<path d="M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23.957-.266 1.983-.399 3.003-.404 1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576 4.765-1.589 8.199-6.086 8.199-11.386 0-6.627-5.373-12-12-12z" />
</svg>
</a>
</Button>
</TooltipTrigger>
<TooltipContent side="right">GitHub</TooltipContent>
</Tooltip>
</div>
</div>
</div>
</TooltipProvider>
);
}
+32
View File
@@ -0,0 +1,32 @@
"use client";
import * as React from "react";
import * as TooltipPrimitive from "@radix-ui/react-tooltip";
import { cn } from "@/lib/utils";
const TooltipProvider = TooltipPrimitive.Provider;
const Tooltip = TooltipPrimitive.Root;
const TooltipTrigger = TooltipPrimitive.Trigger;
const TooltipContent = React.forwardRef<
React.ElementRef<typeof TooltipPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof TooltipPrimitive.Content>
>(({ className, sideOffset = 8, ...props }, ref) => (
<TooltipPrimitive.Portal>
<TooltipPrimitive.Content
ref={ref}
sideOffset={sideOffset}
className={cn(
"z-[100] overflow-hidden rounded-md border border-border/60 bg-foreground px-2.5 py-1.5 text-xs text-background shadow-md",
"animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95",
className
)}
{...props}
/>
</TooltipPrimitive.Portal>
));
TooltipContent.displayName = TooltipPrimitive.Content.displayName;
export { TooltipProvider, Tooltip, TooltipTrigger, TooltipContent };
@@ -1,4 +1,4 @@
import { apiUrl } from "@/lib/config";
import { apiUrl, getAgentHeaders } from "@/lib/config";
// AI 配置类型定义
export interface AIConfig {
@@ -43,6 +43,7 @@ export interface UpdateAIConfigRequest {
export async function fetchAIConfigs(userId: number): Promise<AIConfig[]> {
const res = await fetch(apiUrl(`/agent/ai-config/${userId}`), {
cache: "no-store",
headers: getAgentHeaders(),
});
if (!res.ok) {
throw new Error("获取 AI 配置失败");
@@ -57,6 +58,7 @@ export async function fetchAIConfig(
): Promise<AIConfig> {
const res = await fetch(apiUrl(`/agent/ai-config/${userId}/${configId}`), {
cache: "no-store",
headers: getAgentHeaders(),
});
if (!res.ok) {
throw new Error("获取 AI 配置失败");
@@ -71,7 +73,7 @@ export async function createAIConfig(
): Promise<AIConfig> {
const res = await fetch(apiUrl(`/agent/ai-config/${userId}`), {
method: "POST",
headers: { "Content-Type": "application/json" },
headers: { "Content-Type": "application/json", ...getAgentHeaders() },
body: JSON.stringify(data),
});
if (!res.ok) {
@@ -89,7 +91,7 @@ export async function updateAIConfig(
): Promise<AIConfig> {
const res = await fetch(apiUrl(`/agent/ai-config/${userId}/${configId}`), {
method: "PUT",
headers: { "Content-Type": "application/json" },
headers: { "Content-Type": "application/json", ...getAgentHeaders() },
body: JSON.stringify(data),
});
if (!res.ok) {
@@ -106,6 +108,7 @@ export async function deleteAIConfig(
): Promise<void> {
const res = await fetch(apiUrl(`/agent/ai-config/${userId}/${configId}`), {
method: "DELETE",
headers: getAgentHeaders(),
});
if (!res.ok) {
throw new Error("删除 AI 配置失败");
@@ -1,4 +1,4 @@
import { apiUrl } from "@/lib/config";
import { apiUrl, getAgentHeaders } from "@/lib/config";
import {
ConversationDetail,
ConversationSummary,
@@ -14,7 +14,7 @@ export async function fetchConversations(
if (userId) params.set("user_id", String(userId));
if (opts?.type) params.set("type", opts.type);
const url = `${apiUrl("/conversations")}?${params.toString()}`;
const res = await fetch(url, { cache: "no-store" });
const res = await fetch(url, { cache: "no-store", headers: getAgentHeaders() });
if (!res.ok) {
throw new Error("获取对话列表失败");
}
@@ -33,7 +33,7 @@ export async function fetchConversations(
export async function initInternalConversation(userId: number): Promise<{ conversation_id: number }> {
const res = await fetch(`${apiUrl("/conversations/internal")}?user_id=${userId}`, {
method: "POST",
headers: { "Content-Type": "application/json" },
headers: { "Content-Type": "application/json", ...getAgentHeaders() },
});
if (!res.ok) {
const err = await res.json().catch(() => ({}));
@@ -52,6 +52,7 @@ export async function searchConversations(
: `${apiUrl("/conversations/search")}?q=${encodeURIComponent(query)}`;
const res = await fetch(url, {
cache: "no-store",
headers: getAgentHeaders(),
});
if (!res.ok) {
throw new Error("搜索对话失败");
@@ -74,7 +75,7 @@ export async function fetchConversationDetail(
const url = userId
? `${apiUrl(`/conversations/${conversationId}`)}?user_id=${userId}`
: apiUrl(`/conversations/${conversationId}`);
const res = await fetch(url, { cache: "no-store" });
const res = await fetch(url, { cache: "no-store", headers: getAgentHeaders() });
if (!res.ok) {
return null;
}
@@ -105,7 +106,7 @@ export async function updateConversationContact(
apiUrl(`/conversations/${conversationId}/contact`),
{
method: "PUT",
headers: { "Content-Type": "application/json" },
headers: { "Content-Type": "application/json", ...getAgentHeaders() },
body: JSON.stringify(payload),
}
);
@@ -1,4 +1,4 @@
import { apiUrl } from "@/lib/config";
import { apiUrl, getAgentHeaders } from "@/lib/config";
// 知识库向量配置(API 返回,不含明文 API Key
export interface EmbeddingConfig {
@@ -35,6 +35,7 @@ export interface UpdateEmbeddingConfigRequest {
export async function fetchEmbeddingConfig(userId: number): Promise<EmbeddingConfig> {
const res = await fetch(`${apiUrl("/agent/embedding-config")}?user_id=${userId}`, {
cache: "no-store",
headers: getAgentHeaders(),
});
if (!res.ok) {
throw new Error("获取知识库向量配置失败");
@@ -49,7 +50,7 @@ export async function updateEmbeddingConfig(
): Promise<EmbeddingConfig> {
const res = await fetch(apiUrl("/agent/embedding-config"), {
method: "PUT",
headers: { "Content-Type": "application/json" },
headers: { "Content-Type": "application/json", ...getAgentHeaders() },
body: JSON.stringify({ user_id: userId, ...data }),
});
if (!res.ok) {
@@ -1,4 +1,4 @@
import { apiUrl } from "@/lib/config";
import { apiUrl, getAgentHeaders } from "@/lib/config";
export interface PromptItem {
key: string;
@@ -15,6 +15,7 @@ export interface PromptsResponse {
export async function fetchPrompts(userId: number): Promise<PromptItem[]> {
const res = await fetch(`${apiUrl("/agent/prompts")}?user_id=${userId}`, {
cache: "no-store",
headers: getAgentHeaders(),
});
if (!res.ok) {
throw new Error("获取提示词配置失败");
@@ -37,7 +38,7 @@ export async function updatePrompt(
): Promise<void> {
const res = await fetch(apiUrl("/agent/prompts"), {
method: "PUT",
headers: { "Content-Type": "application/json" },
headers: { "Content-Type": "application/json", ...getAgentHeaders() },
body: JSON.stringify({ user_id: userId, key, content }),
});
if (!res.ok) {
@@ -5,6 +5,7 @@ export interface UserSummary {
id: number;
username: string;
role: "admin" | "agent";
permissions?: string[];
nickname: string;
email: string;
avatar_url: string;
@@ -18,6 +19,7 @@ export interface CreateUserRequest {
username: string;
password: string;
role: "admin" | "agent";
permissions?: string[];
nickname?: string;
email?: string;
}
@@ -25,6 +27,7 @@ export interface CreateUserRequest {
// 更新用户请求
export interface UpdateUserRequest {
role?: "admin" | "agent";
permissions?: string[];
nickname?: string;
email?: string;
receive_ai_conversations?: boolean;
+1
View File
@@ -62,6 +62,7 @@ export interface AgentUser {
id: number;
username: string;
role: string;
permissions?: string[];
}
// 个人资料信息
+16 -15
View File
@@ -50,7 +50,8 @@ export interface AgentPageItem {
label: string;
title: string;
Icon: LucideIcon;
adminOnly?: boolean;
/** 需要的功能权限键(单级开关)。admin 视为全权限 */
requiredPermission?: string;
/** 对话类页面:展示会话列表 + 聊天区,无独立主内容 */
isChatPage?: boolean;
/** 非对话类页面的嵌入组件;对话类不填 */
@@ -64,26 +65,26 @@ export interface AgentPageItem {
export const AGENT_PAGES = [
{
id: "dashboard",
label: "对话",
label: "会话对话",
title: "对话",
Icon: MessageCircle,
adminOnly: false,
requiredPermission: "chat",
isChatPage: true,
},
{
id: "internal-chat",
label: "知识测试",
label: "知识测试",
title: "知识库测试",
Icon: Lightbulb,
adminOnly: false,
requiredPermission: "kb_test",
isChatPage: true,
},
{
id: "knowledge",
label: "知识",
label: "知识管理",
title: "知识库",
Icon: BookOpen,
adminOnly: false,
requiredPermission: "knowledge",
component: KnowledgePage,
},
{
@@ -91,7 +92,7 @@ export const AGENT_PAGES = [
label: "事件管理",
title: "事件管理",
Icon: ClipboardList,
adminOnly: false,
requiredPermission: "faqs",
component: FAQsPage,
},
{
@@ -99,7 +100,7 @@ export const AGENT_PAGES = [
label: "数据报表",
title: "数据报表",
Icon: BarChart3,
adminOnly: false,
requiredPermission: "analytics",
component: AnalyticsPage,
},
{
@@ -107,7 +108,7 @@ export const AGENT_PAGES = [
label: "日志中心",
title: "日志中心",
Icon: ScrollText,
adminOnly: false,
requiredPermission: "logs",
component: LogsPage,
},
{
@@ -115,23 +116,23 @@ export const AGENT_PAGES = [
label: "用户管理",
title: "用户管理",
Icon: Users,
adminOnly: true,
requiredPermission: "users",
component: UsersPage,
},
{
id: "prompts",
label: "提示",
label: "提示配置",
title: "提示词",
Icon: FileText,
adminOnly: true,
requiredPermission: "prompts",
component: PromptsPage,
},
{
id: "settings",
label: "AI 配置",
label: "AI配置",
title: "AI 配置",
Icon: Settings,
adminOnly: false,
requiredPermission: "settings",
component: SettingsPage,
},
] as const;
@@ -0,0 +1,18 @@
export const PERMISSION_OPTIONS = [
{ key: "chat", label: "对话" },
{ key: "kb_test", label: "知识库测试" },
{ key: "knowledge", label: "知识库" },
{ key: "faqs", label: "事件管理" },
{ key: "analytics", label: "数据报表" },
{ key: "logs", label: "日志中心" },
{ key: "prompts", label: "提示词" },
{ key: "settings", label: "AI 配置" },
{ key: "users", label: "用户管理" },
] as const;
export type PermissionKey = (typeof PERMISSION_OPTIONS)[number]["key"];
export function defaultAgentPermissions(): PermissionKey[] {
return ["chat"];
}
+15
View File
@@ -13,6 +13,7 @@ export function getAgentUser(): AgentUser | null {
const id = window.localStorage.getItem(AGENT_ID_KEY);
const username = window.localStorage.getItem(AGENT_USERNAME_KEY);
const role = window.localStorage.getItem(AGENT_ROLE_KEY);
const permissionsRaw = window.localStorage.getItem("agent_permissions");
if (!id || !username) {
return null;
@@ -27,6 +28,15 @@ export function getAgentUser(): AgentUser | null {
id: parsedId,
username,
role: role ?? "",
permissions: (() => {
if (!permissionsRaw) return undefined;
try {
const parsed = JSON.parse(permissionsRaw);
return Array.isArray(parsed) ? (parsed as string[]) : undefined;
} catch {
return undefined;
}
})(),
};
}
@@ -37,6 +47,11 @@ export function setAgentUser(agent: AgentUser): void {
window.localStorage.setItem(AGENT_ID_KEY, String(agent.id));
window.localStorage.setItem(AGENT_USERNAME_KEY, agent.username);
window.localStorage.setItem(AGENT_ROLE_KEY, agent.role ?? "");
if (agent.permissions) {
window.localStorage.setItem("agent_permissions", JSON.stringify(agent.permissions));
} else {
window.localStorage.removeItem("agent_permissions");
}
}
export function clearAgentUser(): void {