mirror of
https://github.com/2930134478/AI-CS.git
synced 2026-06-15 00:44:30 +08:00
用户管理增强
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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 不合法"})
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"`
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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"`
|
||||
|
||||
@@ -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
@@ -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,
|
||||
|
||||
@@ -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)"` // 邮箱
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 对话(可选)
|
||||
|
||||
@@ -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.Permissions(JSON);若为空则兼容默认仅 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
|
||||
}
|
||||
}
|
||||
|
||||
// 更新昵称
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -62,6 +62,7 @@ export interface AgentUser {
|
||||
id: number;
|
||||
username: string;
|
||||
role: string;
|
||||
permissions?: string[];
|
||||
}
|
||||
|
||||
// 个人资料信息
|
||||
|
||||
@@ -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"];
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user