diff --git a/internal/admin/handler_ee.go b/internal/admin/handler_ee.go index 36317d0595..dd19506242 100644 --- a/internal/admin/handler_ee.go +++ b/internal/admin/handler_ee.go @@ -23,6 +23,7 @@ import ( "ragflow/internal/dao" "strconv" "strings" + "time" "github.com/gin-gonic/gin" ) @@ -1893,3 +1894,77 @@ func (h *Handler) BatchDeleteWhiteList(c *gin.Context) { common.SuccessWithData(c, result, "Batch delete white list successfully") } + +// GetTokenStats returns API token statistics for the current user's tenant. +func (h *Handler) GetTokenStats(c *gin.Context) { + + userName := c.Query("user_name") + if userName == "" { + common.ErrorWithCode(c, common.CodeBadRequest, "User name is required") + return + } + + now := time.Now() + fromDate := c.DefaultQuery("from", now.AddDate(0, 0, -7).Format("2006-01-02 00:00:00")) + toDate := c.DefaultQuery("to", now.Format("2006-01-02 15:04:05")) + if len(toDate) == 10 { + toDate += " 23:59:59" + } + + granularity := c.Query("granularity") + if granularity == "" { + granularity = "hour" + } + granularity = strings.ToLower(granularity) + + stats, err := h.service.GetTokenStats(userName, fromDate, toDate, granularity) + if err != nil { + common.ErrorWithCode(c, common.CodeDataError, err.Error()) + return + } + + common.SuccessWithData(c, stats, "success") +} + +// GetTokenUsersStats returns API token statistics summary for the current user's tenant. +func (h *Handler) GetTokenUsersStats(c *gin.Context) { + now := time.Now() + fromDate := c.DefaultQuery("from", now.AddDate(0, 0, -7).Format("2006-01-02 00:00:00")) + toDate := c.DefaultQuery("to", now.Format("2006-01-02 15:04:05")) + if len(toDate) == 10 { + toDate += " 23:59:59" + } + + topStr := c.Query("top") + top, err := strconv.Atoi(topStr) + if err != nil { + common.ErrorWithCode(c, common.CodeBadRequest, "Invalid top") + return + } + + stats, err := h.service.GetTokenUsersStats(fromDate, toDate, top) + if err != nil { + common.ErrorWithCode(c, common.CodeDataError, err.Error()) + return + } + + common.SuccessWithData(c, stats, "success") +} + +// GetTokenStatsSummary returns API token statistics summary for the current user's tenant. +func (h *Handler) GetTokenStatsSummary(c *gin.Context) { + now := time.Now() + fromDate := c.DefaultQuery("from", now.AddDate(0, 0, -7).Format("2006-01-02 00:00:00")) + toDate := c.DefaultQuery("to", now.Format("2006-01-02 15:04:05")) + if len(toDate) == 10 { + toDate += " 23:59:59" + } + + stats, err := h.service.GetTokenStatsSummary(fromDate, toDate) + if err != nil { + common.ErrorWithCode(c, common.CodeDataError, err.Error()) + return + } + + common.SuccessWithData(c, stats, "success") +} diff --git a/internal/admin/router.go b/internal/admin/router.go index 3ab5155725..3ba48f8679 100644 --- a/internal/admin/router.go +++ b/internal/admin/router.go @@ -238,6 +238,11 @@ func (r *Router) Setup(engine *gin.Engine) { protected.GET("/system/license", r.handler.ShowSystemLicense) protected.PUT("/system/license/config", r.handler.UpdateSystemLicenseConfig) + // Token statistics + protected.GET("/stats/token", r.handler.GetTokenStats) + protected.GET("/stats/token/users", r.handler.GetTokenUsersStats) + protected.GET("/stats/token/summary", r.handler.GetTokenStatsSummary) + // Fingerprint protected.GET("/fingerprint", r.handler.GetFingerprint) // License diff --git a/internal/admin/service_ee.go b/internal/admin/service_ee.go index 7d071b8430..c1a155ac0d 100644 --- a/internal/admin/service_ee.go +++ b/internal/admin/service_ee.go @@ -1287,3 +1287,44 @@ func (s *Service) BatchDeleteWhiteList(ids []int) (map[string]interface{}, error } return result, nil } + +// GetTokenStats returns API token statistics for the user. +func (s *Service) GetTokenStats(userName, fromDate, toDate, granularity string) ([]map[string]interface{}, error) { + result := []map[string]interface{}{ + { + "command": "get_token_stats", + "user_name": userName, + "from_date": fromDate, + "to_date": toDate, + "granularity": granularity, + "error": "'Get API token stats' is not supported", + }, + } + return result, nil +} + +// GetTokenUsersStats returns API token statistics for all users. +func (s *Service) GetTokenUsersStats(fromDate, toDate string, top int) ([]map[string]interface{}, error) { + result := []map[string]interface{}{ + { + "command": "get_token_users_stats", + "from_date": fromDate, + "to_date": toDate, + "top": top, + "error": "'Get API token users stats' is not supported", + }, + } + return result, nil +} + +// GetTokenStatsSummary returns API token statistics summary for all users. +func (s *Service) GetTokenStatsSummary(fromDate, toDate string) (map[string]interface{}, error) { + result := map[string]interface{}{ + "command": "get_token_stats_summary", + "from_date": fromDate, + "to_date": toDate, + "error": "'Get API token stats summary' is not supported", + } + + return result, nil +} diff --git a/internal/cli/admin_command.go b/internal/cli/admin_command.go index 2178cf97bc..ab8910fce1 100644 --- a/internal/cli/admin_command.go +++ b/internal/cli/admin_command.go @@ -19,6 +19,7 @@ package cli import ( "encoding/json" "fmt" + "net/url" "ragflow/internal/common" ) @@ -1733,6 +1734,102 @@ func (c *CLI) AdminShowUsersPlanQuotaCommand(cmd *Command) (ResponseIf, error) { return HandleCommonDataResponse(resp, "get users plan quota") } +// AdminStatsUserCommand stats user token usage +func (c *CLI) AdminStatsUserCommand(cmd *Command) (ResponseIf, error) { + if c.Config.CLIMode != AdminMode || c.AdminServerClient.LoginToken == nil { + return nil, fmt.Errorf("this command is only allowed in ADMIN mode or already login") + } + + userName, ok := cmd.Params["user_name"].(string) + if !ok { + return nil, fmt.Errorf("user name not provided") + } + fromTime, _ := cmd.Params["from"].(string) + toTime, _ := cmd.Params["to"].(string) + granularity, ok := cmd.Params["granularity"].(string) + if !ok { + return nil, fmt.Errorf("granularity not provided") + } + + q := url.Values{} + q.Set("user_name", userName) + if fromTime != "" { + q.Set("from", fromTime) + } + if toTime != "" { + q.Set("to", toTime) + } + q.Set("granularity", granularity) + + baseUrl := fmt.Sprintf("/admin/stats/token?%s", q.Encode()) + + resp, err := c.AdminServerClient.Request("GET", baseUrl, "admin", nil, nil) + if err != nil { + return nil, fmt.Errorf("failed to get stats user: %w", err) + } + + return HandleCommonResponse(resp, "get stats user") +} + +// AdminStatsUsersCommand stats users token usage +func (c *CLI) AdminStatsUsersCommand(cmd *Command) (ResponseIf, error) { + if c.Config.CLIMode != AdminMode || c.AdminServerClient.LoginToken == nil { + return nil, fmt.Errorf("this command is only allowed in ADMIN mode or already login") + } + + top, ok := cmd.Params["top"].(int) + if !ok { + return nil, fmt.Errorf("top not provided") + } + fromTime, _ := cmd.Params["from"].(string) + toTime, _ := cmd.Params["to"].(string) + + q := url.Values{} + q.Set("top", fmt.Sprintf("%d", top)) + if fromTime != "" { + q.Set("from", fromTime) + } + if toTime != "" { + q.Set("to", toTime) + } + + url := fmt.Sprintf("/admin/stats/token/users?%s", q.Encode()) + + resp, err := c.AdminServerClient.Request("GET", url, "web", nil, nil) + if err != nil { + return nil, fmt.Errorf("failed to get stats: %w", err) + } + + return HandleCommonResponse(resp, "get stats") +} + +// AdminStatsSummaryCommand stats summary token usage +func (c *CLI) AdminStatsSummaryCommand(cmd *Command) (ResponseIf, error) { + if c.Config.CLIMode != AdminMode || c.AdminServerClient.LoginToken == nil { + return nil, fmt.Errorf("this command is only allowed in ADMIN mode or already login") + } + + fromTime, _ := cmd.Params["from"].(string) + toTime, _ := cmd.Params["to"].(string) + + q := url.Values{} + if fromTime != "" { + q.Set("from", fromTime) + } + if toTime != "" { + q.Set("to", toTime) + } + + baseUrl := fmt.Sprintf("/admin/stats/token/summary?%s", q.Encode()) + + resp, err := c.AdminServerClient.Request("GET", baseUrl, "admin", nil, nil) + if err != nil { + return nil, fmt.Errorf("failed to get stats summary: %w", err) + } + + return HandleCommonDataResponse(resp, "get stats summary") +} + // ListUsers lists all users (admin mode only) // Returns (result_map, error) - result_map is non-nil for benchmark mode func (c *CLI) AdminListUsersCommand(cmd *Command) (ResponseIf, error) { diff --git a/internal/cli/admin_parser.go b/internal/cli/admin_parser.go index 984ae40ee5..9c3670e1ca 100644 --- a/internal/cli/admin_parser.go +++ b/internal/cli/admin_parser.go @@ -949,6 +949,151 @@ func (p *Parser) parseCommonShowPoolModel() (*Command, error) { // endregion SHOW commands +// region STATS commands + +// STATS USER 'user_name' FROM TO HOUR, DAY, MONTH +// STATS USER 'user_name' FROM TO +// STATS USERS TOP FROM TO +// STATS SUMMARY FROM TO +func (p *Parser) parseAdminStatsCommands() (*Command, error) { + p.nextToken() // consume STATS + + switch p.curToken.Type { + case TokenUser: + return p.parseAdminStatsUser() + case TokenUsers: + return p.parseAdminStatsUsers() + case TokenSummary: + return p.parseAdminStatsSummary() + default: + return nil, fmt.Errorf("expected USER or MODEL or SUMMARY after STATS") + } +} + +// STATS USER 'user_name' FROM TO HOUR, DAY, MONTH +// STATS USER 'user_name' FROM TO +func (p *Parser) parseAdminStatsUser() (*Command, error) { + p.nextToken() // consume USER + + cmd := NewCommand("admin_stats_user") + + userName, err := p.parseQuotedString() + if err != nil { + return nil, err + } + cmd.Params["user_name"] = userName + p.nextToken() + + // Parse time range + if p.curToken.Type == TokenFrom { + p.nextToken() + cmd.Params["from"], err = p.parseQuotedString() + if err != nil { + return nil, err + } + p.nextToken() + } + if p.curToken.Type == TokenTo { + p.nextToken() + cmd.Params["to"], err = p.parseQuotedString() + if err != nil { + return nil, err + } + p.nextToken() + } + + switch p.curToken.Type { + case TokenHour: + p.nextToken() + cmd.Params["granularity"] = "HOUR" + case TokenDay: + p.nextToken() + cmd.Params["granularity"] = "DAY" + case TokenMonth: + p.nextToken() + cmd.Params["granularity"] = "MONTH" + default: + p.nextToken() + cmd.Params["granularity"] = "HOUR" + } + + // Semicolon is optional + if p.curToken.Type == TokenSemicolon { + p.nextToken() + } + return cmd, nil +} + +// STATS USERS TOP FROM TO +func (p *Parser) parseAdminStatsUsers() (*Command, error) { + p.nextToken() // consume USERS + cmd := NewCommand("admin_stats_users") + + cmd.Params["top"] = 10 + + var err error + if p.curToken.Type == TokenTop { + p.nextToken() + cmd.Params["top"], err = p.parseNumber() + if err != nil { + return nil, err + } + p.nextToken() + } + + if p.curToken.Type == TokenFrom { + p.nextToken() + cmd.Params["from"], err = p.parseQuotedString() + if err != nil { + return nil, err + } + p.nextToken() + } + if p.curToken.Type == TokenTo { + p.nextToken() + cmd.Params["to"], err = p.parseQuotedString() + if err != nil { + return nil, err + } + p.nextToken() + } + + if p.curToken.Type == TokenSemicolon { + p.nextToken() + } + return cmd, nil +} + +// STATS SUMMARY FROM TO +func (p *Parser) parseAdminStatsSummary() (*Command, error) { + p.nextToken() // consume SUMMARY + cmd := NewCommand("admin_stats_summary") + + var err error + if p.curToken.Type == TokenFrom { + p.nextToken() + cmd.Params["from"], err = p.parseQuotedString() + if err != nil { + return nil, err + } + p.nextToken() + } + if p.curToken.Type == TokenTo { + p.nextToken() + cmd.Params["to"], err = p.parseQuotedString() + if err != nil { + return nil, err + } + p.nextToken() + } + if p.curToken.Type == TokenSemicolon { + p.nextToken() + } + return cmd, nil +} + +// endregion STATS commands + // CHECK LICENSE // CHECK PROVIDER 'provider_name' REGION 'region_name' KEY 'api_key' [URL 'base_url']; // CHECK PROVIDER 'provider_name' INSTANCE 'instance_name'; diff --git a/internal/cli/cli_http.go b/internal/cli/cli_http.go index 97ef1b3bb7..220c4237ae 100644 --- a/internal/cli/cli_http.go +++ b/internal/cli/cli_http.go @@ -207,6 +207,12 @@ func (c *CLI) ExecuteAdminCommand(cmd *Command) (ResponseIf, error) { return c.AdminShowUsersPlanSummaryCommand(cmd) case "admin_show_users_plan_quota": return c.AdminShowUsersPlanQuotaCommand(cmd) + case "admin_stats_user": + return c.AdminStatsUserCommand(cmd) + case "admin_stats_users": + return c.AdminStatsUsersCommand(cmd) + case "admin_stats_summary": + return c.AdminStatsSummaryCommand(cmd) case "admin_list_users_command": return c.AdminListUsersCommand(cmd) case "admin_list_users_condition_command": diff --git a/internal/cli/lexer.go b/internal/cli/lexer.go index 8952c98cbe..f4588a1b6e 100644 --- a/internal/cli/lexer.go +++ b/internal/cli/lexer.go @@ -513,6 +513,8 @@ func (l *Lexer) lookupIdent(ident string) Token { return Token{Type: TokenAnalyze, Value: ident} case "SUMMARY": return Token{Type: TokenSummary, Value: ident} + case "STATS": + return Token{Type: TokenStats, Value: ident} case "STORAGE": return Token{Type: TokenStorage, Value: ident} case "QUOTA": @@ -523,6 +525,12 @@ func (l *Lexer) lookupIdent(ident string) Token { return Token{Type: TokenOrphan, Value: ident} case "DAYS": return Token{Type: TokenDays, Value: ident} + case "HOUR": + return Token{Type: TokenHour, Value: ident} + case "DAY": + return Token{Type: TokenDay, Value: ident} + case "MONTH": + return Token{Type: TokenMonth, Value: ident} case "WINDOW": return Token{Type: TokenWindow, Value: ident} case "ACTIVITY": diff --git a/internal/cli/parser.go b/internal/cli/parser.go index 7b83183d92..6549b733a1 100644 --- a/internal/cli/parser.go +++ b/internal/cli/parser.go @@ -94,6 +94,8 @@ func (p *Parser) parseAdminCommand() (*Command, error) { return p.parseAdminListCommands() case TokenShow: return p.parseAdminShowCommands() + case TokenStats: + return p.parseAdminStatsCommands() case TokenCheck: return p.parseAdminCheck() case TokenCreate: diff --git a/internal/cli/types.go b/internal/cli/types.go index 661d71c8c4..69dfe5a48b 100644 --- a/internal/cli/types.go +++ b/internal/cli/types.go @@ -187,10 +187,14 @@ const ( TokenNoACK TokenAnalyze TokenSummary + TokenStats TokenStorage TokenQuota TokenTree TokenOrphan + TokenHour + TokenDay + TokenMonth TokenDays TokenWindow TokenActivity