From 0da9c4618d37cfa6c5bf526ec6e526e17146a560 Mon Sep 17 00:00:00 2001 From: chanx <1243304602@qq.com> Date: Thu, 12 Mar 2026 13:33:13 +0800 Subject: [PATCH] feat(cli): Enhance CLI functionality and add administrator mode support (#13539) ### What problem does this PR solve? feat(cli): Enhance CLI functionality and add administrator mode support - Modify `parseActivateUser` in `parser.go` to support 'on'/'off' states - Add administrator mode switching and host port settings functionality to `cli.go` - Implement user management API calls in `client.go` ### Type of change - [x] New Feature (non-breaking change which adds functionality) --- internal/cli/cli.go | 86 +++++++- internal/cli/client.go | 452 ++++++++++++++++++++++++++++++++++++++++- internal/cli/parser.go | 11 +- 3 files changed, 537 insertions(+), 12 deletions(-) diff --git a/internal/cli/cli.go b/internal/cli/cli.go index dd88c6c6a3..b74344815d 100644 --- a/internal/cli/cli.go +++ b/internal/cli/cli.go @@ -19,7 +19,10 @@ package cli import ( "fmt" "os" + "os/signal" + "strconv" "strings" + "syscall" "github.com/peterh/liner" ) @@ -127,6 +130,7 @@ func (c *CLI) execute(input string) error { func (c *CLI) handleMetaCommand(cmd *Command) error { command := cmd.Params["command"].(string) + args, _ := cmd.Params["args"].([]string) switch command { case "q", "quit", "exit": @@ -137,6 +141,39 @@ func (c *CLI) handleMetaCommand(cmd *Command) error { case "c", "clear": // Clear screen (simple approach) fmt.Print("\033[H\033[2J") + case "admin": + c.client.ServerType = "admin" + c.client.HTTPClient.Port = 9381 + c.prompt = "RAGFlow(admin)> " + fmt.Println("Switched to ADMIN mode (port 9381)") + case "user": + c.client.ServerType = "user" + c.client.HTTPClient.Port = 9380 + c.prompt = "RAGFlow> " + fmt.Println("Switched to USER mode (port 9380)") + case "host": + if len(args) == 0 { + fmt.Printf("Current host: %s\n", c.client.HTTPClient.Host) + } else { + c.client.HTTPClient.Host = args[0] + fmt.Printf("Host set to: %s\n", args[0]) + } + case "port": + if len(args) == 0 { + fmt.Printf("Current port: %d\n", c.client.HTTPClient.Port) + } else { + port, err := strconv.Atoi(args[0]) + if err != nil { + return fmt.Errorf("invalid port number: %s", args[0]) + } + if port < 1 || port > 65535 { + return fmt.Errorf("port must be between 1 and 65535") + } + c.client.HTTPClient.Port = port + fmt.Printf("Port set to: %d\n", port) + } + case "status": + fmt.Printf("Server: %s:%d (mode: %s)\n", c.client.HTTPClient.Host, c.client.HTTPClient.Port, c.client.ServerType) default: return fmt.Errorf("unknown meta command: \\%s", command) } @@ -148,13 +185,39 @@ func (c *CLI) printHelp() { RAGFlow CLI Help ================ -SQL Commands: +Meta Commands: + \admin - Switch to ADMIN mode (port 9381) + \user - Switch to USER mode (port 9380) + \host [ip] - Show or set server host (default: 127.0.0.1) + \port [num] - Show or set server port (default: 9380 for user, 9381 for admin) + \status - Show current connection status + \? or \h - Show this help + \q or \quit - Exit CLI + \c or \clear - Clear screen + +SQL Commands (User Mode): LOGIN USER 'email'; - Login as user REGISTER USER 'name' AS 'nickname' PASSWORD 'pwd'; - Register new user SHOW VERSION; - Show version info - SHOW CURRENT USER; - Show current user + PING; - Ping server + LIST DATASETS; - List user datasets + LIST AGENTS; - List user agents + LIST CHATS; - List user chats + LIST MODEL PROVIDERS; - List model providers + LIST DEFAULT MODELS; - List default models + +SQL Commands (Admin Mode): + LOGIN USER 'email'; - Login as admin LIST USERS; - List all users + SHOW USER 'email'; - Show user details + CREATE USER 'email' 'password'; - Create new user + DROP USER 'email'; - Delete user + ALTER USER PASSWORD 'email' 'new_password'; - Change user password + ALTER USER ACTIVE 'email' on/off; - Activate/deactivate user + GRANT ADMIN 'email'; - Grant admin role + REVOKE ADMIN 'email'; - Revoke admin role LIST SERVICES; - List services + SHOW SERVICE ; - Show service details PING; - Ping server ... and many more @@ -172,3 +235,22 @@ For more information, see documentation. func (c *CLI) Cleanup() { fmt.Println("\nCleaning up...") } + +// RunInteractive runs the CLI in interactive mode +func RunInteractive() error { + cli, err := NewCLI() + if err != nil { + return fmt.Errorf("failed to create CLI: %v", err) + } + + // Handle interrupt signal + sigChan := make(chan os.Signal, 1) + signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM) + go func() { + <-sigChan + cli.Cleanup() + os.Exit(0) + }() + + return cli.Run() +} diff --git a/internal/cli/client.go b/internal/cli/client.go index 99c97ba151..f69665bb19 100644 --- a/internal/cli/client.go +++ b/internal/cli/client.go @@ -38,27 +38,130 @@ type RAGFlowClient struct { // NewRAGFlowClient creates a new RAGFlow client func NewRAGFlowClient(serverType string) *RAGFlowClient { + httpClient := NewHTTPClient() + // Set port from configuration file based on server type + if serverType == "admin" { + httpClient.Port = 9381 + } else { + httpClient.Port = 9380 + } + return &RAGFlowClient{ - HTTPClient: NewHTTPClient(), + HTTPClient: httpClient, ServerType: serverType, } } -// LoginUser performs user login -func (c *RAGFlowClient) LoginUser(cmd *Command) error { +// LoginUserInteractive performs interactive login with username and password +func (c *RAGFlowClient) LoginUserInteractive(username, password string) error { // First, ping the server to check if it's available - resp, err := c.HTTPClient.Request("GET", "/system/ping", false, "web", nil, nil) + // For admin mode, use /admin/ping with useAPIBase=true + // For user mode, use /system/ping with useAPIBase=false + var pingPath string + var useAPIBase bool + if c.ServerType == "admin" { + pingPath = "/admin/ping" + useAPIBase = true + } else { + pingPath = "/system/ping" + useAPIBase = false + } + + resp, err := c.HTTPClient.Request("GET", pingPath, useAPIBase, "web", nil, nil) if err != nil { fmt.Printf("Error: %v\n", err) fmt.Println("Can't access server for login (connection failed)") return err } - if resp.StatusCode != 200 || string(resp.Body) != "pong" { + if resp.StatusCode != 200 { fmt.Println("Server is down") return fmt.Errorf("server is down") } + // Check response - admin returns JSON with message "PONG", user returns plain "pong" + resJSON, err := resp.JSON() + if err == nil { + // Admin mode returns {"code":0,"message":"PONG"} + if msg, ok := resJSON["message"].(string); !ok || msg != "PONG" { + fmt.Println("Server is down") + return fmt.Errorf("server is down") + } + } else { + // User mode returns plain "pong" + if string(resp.Body) != "pong" { + fmt.Println("Server is down") + return fmt.Errorf("server is down") + } + } + + // If password is not provided, prompt for it + if password == "" { + fmt.Printf("password for %s: ", username) + var err error + password, err = readPassword() + if err != nil { + return fmt.Errorf("failed to read password: %w", err) + } + password = strings.TrimSpace(password) + } + + // Login + token, err := c.loginUser(username, password) + if err != nil { + fmt.Printf("Error: %v\n", err) + fmt.Println("Can't access server for login (connection failed)") + return err + } + + c.HTTPClient.LoginToken = token + fmt.Printf("Login user %s successfully\n", username) + return nil +} + +// LoginUser performs user login +func (c *RAGFlowClient) LoginUser(cmd *Command) error { + // First, ping the server to check if it's available + // For admin mode, use /admin/ping with useAPIBase=true + // For user mode, use /system/ping with useAPIBase=false + var pingPath string + var useAPIBase bool + if c.ServerType == "admin" { + pingPath = "/admin/ping" + useAPIBase = true + } else { + pingPath = "/system/ping" + useAPIBase = false + } + + resp, err := c.HTTPClient.Request("GET", pingPath, useAPIBase, "web", nil, nil) + if err != nil { + fmt.Printf("Error: %v\n", err) + fmt.Println("Can't access server for login (connection failed)") + return err + } + + if resp.StatusCode != 200 { + fmt.Println("Server is down") + return fmt.Errorf("server is down") + } + + // Check response - admin returns JSON with message "PONG", user returns plain "pong" + resJSON, err := resp.JSON() + if err == nil { + // Admin mode returns {"code":0,"message":"PONG"} + if msg, ok := resJSON["message"].(string); !ok || msg != "PONG" { + fmt.Println("Server is down") + return fmt.Errorf("server is down") + } + } else { + // User mode returns plain "pong" + if string(resp.Body) != "pong" { + fmt.Println("Server is down") + return fmt.Errorf("server is down") + } + } + email, ok := cmd.Params["email"].(string) if !ok { return fmt.Errorf("email not provided") @@ -503,8 +606,347 @@ func (c *RAGFlowClient) ExecuteCommand(cmd *Command) (map[string]interface{}, er return c.ListDatasets(cmd) case "search_on_datasets": return c.SearchOnDatasets(cmd) + case "list_users": + return c.ListUsers(cmd) + case "grant_admin": + return nil, c.GrantAdmin(cmd) + case "revoke_admin": + return nil, c.RevokeAdmin(cmd) + case "show_current_user": + return c.ShowCurrentUser(cmd) + case "create_user": + return nil, c.CreateUser(cmd) + case "activate_user": + return nil, c.ActivateUser(cmd) + case "alter_user": + return nil, c.AlterUserPassword(cmd) + case "drop_user": + return nil, c.DropUser(cmd) // TODO: Implement other commands default: return nil, fmt.Errorf("command '%s' would be executed with API", cmd.Type) } } + +// ListUsers lists all users (admin mode only) +// Returns (result_map, error) - result_map is non-nil for benchmark mode +func (c *RAGFlowClient) ListUsers(cmd *Command) (map[string]interface{}, error) { + if c.ServerType != "admin" { + return nil, fmt.Errorf("this command is only allowed in ADMIN mode") + } + + // Check for benchmark iterations + iterations := 1 + if val, ok := cmd.Params["iterations"].(int); ok && val > 1 { + iterations = val + } + + if iterations > 1 { + // Benchmark mode - return raw result for benchmark stats + return c.HTTPClient.RequestWithIterations("GET", "/admin/users", true, "admin", nil, nil, iterations) + } + + resp, err := c.HTTPClient.Request("GET", "/admin/users", true, "admin", nil, nil) + if err != nil { + return nil, fmt.Errorf("failed to list users: %w", err) + } + + if resp.StatusCode != 200 { + return nil, fmt.Errorf("failed to list users: HTTP %d, body: %s", resp.StatusCode, string(resp.Body)) + } + + resJSON, err := resp.JSON() + if err != nil { + return nil, fmt.Errorf("invalid JSON response: %w", err) + } + + code, ok := resJSON["code"].(float64) + if !ok || code != 0 { + msg, _ := resJSON["message"].(string) + return nil, fmt.Errorf("failed to list users: %s", msg) + } + + data, ok := resJSON["data"].([]interface{}) + if !ok { + return nil, fmt.Errorf("invalid response format") + } + + // Convert to slice of maps and remove sensitive fields + tableData := make([]map[string]interface{}, 0, len(data)) + for _, item := range data { + if itemMap, ok := item.(map[string]interface{}); ok { + // Remove sensitive fields + delete(itemMap, "password") + delete(itemMap, "access_token") + tableData = append(tableData, itemMap) + } + } + + PrintTableSimple(tableData) + return nil, nil +} + +// GrantAdmin grants admin privileges to a user (admin mode only) +func (c *RAGFlowClient) GrantAdmin(cmd *Command) error { + if c.ServerType != "admin" { + return fmt.Errorf("this command is only allowed in ADMIN mode") + } + + userName, ok := cmd.Params["user_name"].(string) + if !ok { + return fmt.Errorf("user_name not provided") + } + + resp, err := c.HTTPClient.Request("PUT", fmt.Sprintf("/admin/users/%s/admin", userName), true, "admin", nil, nil) + if err != nil { + return fmt.Errorf("failed to grant admin: %w", err) + } + + if resp.StatusCode != 200 { + return fmt.Errorf("failed to grant admin: HTTP %d, body: %s", resp.StatusCode, string(resp.Body)) + } + + resJSON, err := resp.JSON() + if err != nil { + return fmt.Errorf("invalid JSON response: %w", err) + } + + code, ok := resJSON["code"].(float64) + if !ok || code != 0 { + msg, _ := resJSON["message"].(string) + return fmt.Errorf("failed to grant admin: %s", msg) + } + + fmt.Printf("Admin role granted to user: %s\n", userName) + return nil +} + +// RevokeAdmin revokes admin privileges from a user (admin mode only) +func (c *RAGFlowClient) RevokeAdmin(cmd *Command) error { + if c.ServerType != "admin" { + return fmt.Errorf("this command is only allowed in ADMIN mode") + } + + userName, ok := cmd.Params["user_name"].(string) + if !ok { + return fmt.Errorf("user_name not provided") + } + + resp, err := c.HTTPClient.Request("DELETE", fmt.Sprintf("/admin/users/%s/admin", userName), true, "admin", nil, nil) + if err != nil { + return fmt.Errorf("failed to revoke admin: %w", err) + } + + if resp.StatusCode != 200 { + return fmt.Errorf("failed to revoke admin: HTTP %d, body: %s", resp.StatusCode, string(resp.Body)) + } + + resJSON, err := resp.JSON() + if err != nil { + return fmt.Errorf("invalid JSON response: %w", err) + } + + code, ok := resJSON["code"].(float64) + if !ok || code != 0 { + msg, _ := resJSON["message"].(string) + return fmt.Errorf("failed to revoke admin: %s", msg) + } + + fmt.Printf("Admin role revoked from user: %s\n", userName) + return nil +} + +// ShowCurrentUser shows the current logged-in user information +// TODO: Implement showing current user information when API is available +func (c *RAGFlowClient) ShowCurrentUser(cmd *Command) (map[string]interface{}, error) { + // TODO: Call the appropriate API to get current user information + // Currently there is no /admin/user/info or /user/info API available + // The /admin/auth API only verifies authorization, does not return user info + return nil, fmt.Errorf("command 'SHOW CURRENT USER' is not yet implemented") +} + +// CreateUser creates a new user (admin mode only) +func (c *RAGFlowClient) CreateUser(cmd *Command) error { + if c.ServerType != "admin" { + return fmt.Errorf("this command is only allowed in ADMIN mode") + } + + userName, ok := cmd.Params["user_name"].(string) + if !ok { + return fmt.Errorf("user_name not provided") + } + + password, ok := cmd.Params["password"].(string) + if !ok { + return fmt.Errorf("password not provided") + } + + // Encrypt password using RSA + encryptedPassword, err := EncryptPassword(password) + if err != nil { + return fmt.Errorf("failed to encrypt password: %w", err) + } + + payload := map[string]interface{}{ + "username": userName, + "password": encryptedPassword, + "role": "user", + } + + resp, err := c.HTTPClient.Request("POST", "/admin/users", true, "admin", nil, payload) + if err != nil { + return fmt.Errorf("failed to create user: %w", err) + } + + if resp.StatusCode != 200 { + return fmt.Errorf("failed to create user: HTTP %d, body: %s", resp.StatusCode, string(resp.Body)) + } + + resJSON, err := resp.JSON() + if err != nil { + return fmt.Errorf("invalid JSON response: %w", err) + } + + code, ok := resJSON["code"].(float64) + if !ok || code != 0 { + msg, _ := resJSON["message"].(string) + return fmt.Errorf("failed to create user: %s", msg) + } + + fmt.Printf("User created successfully: %s\n", userName) + return nil +} + +// ActivateUser activates or deactivates a user (admin mode only) +func (c *RAGFlowClient) ActivateUser(cmd *Command) error { + if c.ServerType != "admin" { + return fmt.Errorf("this command is only allowed in ADMIN mode") + } + + userName, ok := cmd.Params["user_name"].(string) + if !ok { + return fmt.Errorf("user_name not provided") + } + + activateStatus, ok := cmd.Params["activate_status"].(string) + if !ok { + return fmt.Errorf("activate_status not provided") + } + + // Validate activate_status + if activateStatus != "on" && activateStatus != "off" { + return fmt.Errorf("activate_status must be 'on' or 'off'") + } + + payload := map[string]interface{}{ + "activate_status": activateStatus, + } + + resp, err := c.HTTPClient.Request("PUT", fmt.Sprintf("/admin/users/%s/activate", userName), true, "admin", nil, payload) + if err != nil { + return fmt.Errorf("failed to update user activate status: %w", err) + } + + if resp.StatusCode != 200 { + return fmt.Errorf("failed to update user activate status: HTTP %d, body: %s", resp.StatusCode, string(resp.Body)) + } + + resJSON, err := resp.JSON() + if err != nil { + return fmt.Errorf("invalid JSON response: %w", err) + } + + code, ok := resJSON["code"].(float64) + if !ok || code != 0 { + msg, _ := resJSON["message"].(string) + return fmt.Errorf("failed to update user activate status: %s", msg) + } + + fmt.Printf("User '%s' activate status set to '%s'\n", userName, activateStatus) + return nil +} + +// AlterUserPassword changes a user's password (admin mode only) +func (c *RAGFlowClient) AlterUserPassword(cmd *Command) error { + if c.ServerType != "admin" { + return fmt.Errorf("this command is only allowed in ADMIN mode") + } + + userName, ok := cmd.Params["user_name"].(string) + if !ok { + return fmt.Errorf("user_name not provided") + } + + password, ok := cmd.Params["password"].(string) + if !ok { + return fmt.Errorf("password not provided") + } + + // Encrypt password using RSA + encryptedPassword, err := EncryptPassword(password) + if err != nil { + return fmt.Errorf("failed to encrypt password: %w", err) + } + + payload := map[string]interface{}{ + "new_password": encryptedPassword, + } + + resp, err := c.HTTPClient.Request("PUT", fmt.Sprintf("/admin/users/%s/password", userName), true, "admin", nil, payload) + if err != nil { + return fmt.Errorf("failed to change user password: %w", err) + } + + if resp.StatusCode != 200 { + return fmt.Errorf("failed to change user password: HTTP %d, body: %s", resp.StatusCode, string(resp.Body)) + } + + resJSON, err := resp.JSON() + if err != nil { + return fmt.Errorf("invalid JSON response: %w", err) + } + + code, ok := resJSON["code"].(float64) + if !ok || code != 0 { + msg, _ := resJSON["message"].(string) + return fmt.Errorf("failed to change user password: %s", msg) + } + + fmt.Printf("Password changed for user: %s\n", userName) + return nil +} + +// DropUser deletes a user (admin mode only) +func (c *RAGFlowClient) DropUser(cmd *Command) error { + if c.ServerType != "admin" { + return fmt.Errorf("this command is only allowed in ADMIN mode") + } + + userName, ok := cmd.Params["user_name"].(string) + if !ok { + return fmt.Errorf("user_name not provided") + } + + resp, err := c.HTTPClient.Request("DELETE", fmt.Sprintf("/admin/users/%s", userName), true, "admin", nil, nil) + if err != nil { + return fmt.Errorf("failed to delete user: %w", err) + } + + if resp.StatusCode != 200 { + return fmt.Errorf("failed to delete user: HTTP %d, body: %s", resp.StatusCode, string(resp.Body)) + } + + resJSON, err := resp.JSON() + if err != nil { + return fmt.Errorf("invalid JSON response: %w", err) + } + + code, ok := resJSON["code"].(float64) + if !ok || code != 0 { + msg, _ := resJSON["message"].(string) + return fmt.Errorf("failed to delete user: %s", msg) + } + + fmt.Printf("User deleted: %s\n", userName) + return nil +} diff --git a/internal/cli/parser.go b/internal/cli/parser.go index bd33656603..7c839d1bb8 100644 --- a/internal/cli/parser.go +++ b/internal/cli/parser.go @@ -950,10 +950,11 @@ func (p *Parser) parseActivateUser() (*Command, error) { } p.nextToken() - status, err := p.parseIdentifier() - if err != nil { - return nil, err - } + // Accept 'on' or 'off' as identifier + status := p.curToken.Value + if status != "on" && status != "off" { + return nil, fmt.Errorf("expected 'on' or 'off', got %s", p.curToken.Value) + } cmd := NewCommand("activate_user") cmd.Params["user_name"] = userName @@ -962,7 +963,7 @@ func (p *Parser) parseActivateUser() (*Command, error) { p.nextToken() if err := p.expectSemicolon(); err != nil { return nil, err - } + } return cmd, nil }