Files
ragflow/internal/cli/cli.go
Jin Hai b308cd3a02 Update go cli (#13717)
### What problem does this PR solve?

Go cli

### Type of change

- [x] New Feature (non-breaking change which adds functionality)

---------

Signed-off-by: Jin Hai <haijin.chn@gmail.com>
2026-03-24 20:08:36 +08:00

634 lines
17 KiB
Go

//
// Copyright 2026 The InfiniFlow Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
package cli
import (
"errors"
"flag"
"fmt"
"os"
"os/signal"
"strconv"
"strings"
"syscall"
"github.com/peterh/liner"
"golang.org/x/term"
"gopkg.in/yaml.v3"
)
// ConfigFile represents the rf.yml configuration file structure
type ConfigFile struct {
Host string `yaml:"host"`
User string `yaml:"user"`
Password string `yaml:"password"`
APIToken string `yaml:"api_token"`
}
// ConnectionArgs holds the parsed command line arguments
type ConnectionArgs struct {
Host string
Port int
Password string
Key string
Type string
Username string
Command string
ShowHelp bool
}
// LoadDefaultConfigFile reads the rf.yml file from current directory if it exists
func LoadDefaultConfigFile() (*ConfigFile, error) {
// Try to read rf.yml from current directory
data, err := os.ReadFile("rf.yml")
if err != nil {
// File doesn't exist, return nil without error
if os.IsNotExist(err) {
return nil, nil
}
return nil, err
}
var config ConfigFile
if err = yaml.Unmarshal(data, &config); err != nil {
return nil, fmt.Errorf("failed to parse rf.yml: %v", err)
}
return &config, nil
}
// LoadConfigFileFromPath reads a config file from the specified path
func LoadConfigFileFromPath(path string) (*ConfigFile, error) {
data, err := os.ReadFile(path)
if err != nil {
return nil, fmt.Errorf("failed to read config file %s: %v", path, err)
}
var config ConfigFile
if err = yaml.Unmarshal(data, &config); err != nil {
return nil, fmt.Errorf("failed to parse config file %s: %v", path, err)
}
return &config, nil
}
// parseHostPort parses a host:port string and returns host and port
func parseHostPort(hostPort string) (string, int, error) {
if hostPort == "" {
return "", -1, nil
}
// Split host and port
parts := strings.Split(hostPort, ":")
if len(parts) != 2 {
return "", -1, fmt.Errorf("invalid host format, expected host:port, got: %s", hostPort)
}
host := parts[0]
port, err := strconv.Atoi(parts[1])
if err != nil {
return "", -1, fmt.Errorf("invalid port number: %s", parts[1])
}
return host, port, nil
}
// ParseConnectionArgs parses command line arguments similar to Python's parse_connection_args
func ParseConnectionArgs(args []string) (*ConnectionArgs, error) {
// First, scan args to check for help and config file
var configFilePath string
var hasOtherFlags bool
for i := 0; i < len(args); i++ {
arg := args[i]
if arg == "--help" {
return &ConnectionArgs{ShowHelp: true}, nil
} else if arg == "-f" && i+1 < len(args) {
configFilePath = args[i+1]
i++
} else if strings.HasPrefix(arg, "-") && arg != "-f" {
// Check if it's a flag (not a command)
if arg == "-h" || arg == "-p" || arg == "-w" || arg == "-k" ||
arg == "-u" || arg == "-admin" || arg == "-user" {
hasOtherFlags = true
}
}
}
// Load config file with priority: -f > rf.yml > none
var config *ConfigFile
var err error
if configFilePath != "" {
// User specified config file via -f
config, err = LoadConfigFileFromPath(configFilePath)
if err != nil {
return nil, err
}
} else {
// Try default rf.yml
config, err = LoadDefaultConfigFile()
if err != nil {
return nil, err
}
}
if config != nil {
if hasOtherFlags {
return nil, fmt.Errorf("cannot use command line flags (-h, -p, -w, -k, -u, -admin, -user) when using config file. Please use config file or command line flags, not both")
}
return buildArgsFromConfig(config, args)
}
// Create a new flag set
fs := flag.NewFlagSet("ragflow_cli", flag.ContinueOnError)
// Define flags
host := fs.String("h", "127.0.0.1", "Admin or RAGFlow service host")
port := fs.Int("p", -1, "Admin or RAGFlow service port (default: 9381 for admin, 9380 for user)")
password := fs.String("w", "", "Superuser password")
key := fs.String("k", "", "API key for authentication")
_ = fs.String("f", "", "Path to config file (YAML format)") // Already parsed above
_ = fs.Bool("admin", false, "Run in admin mode (default)")
userMode := fs.Bool("user", false, "Run in user mode")
username := fs.String("u", "", "Username (email). In admin mode defaults to admin@ragflow.io, in user mode required")
// Parse the arguments
if err = fs.Parse(args); err != nil {
return nil, fmt.Errorf("failed to parse arguments: %v", err)
}
// Otherwise, use command line flags
return buildArgsFromFlags(host, port, password, key, userMode, username, fs.Args())
}
// buildArgsFromConfig builds ConnectionArgs from config file
func buildArgsFromConfig(config *ConfigFile, remainingArgs []string) (*ConnectionArgs, error) {
result := &ConnectionArgs{}
// Parse host:port from config file
if config.Host != "" {
host, port, err := parseHostPort(config.Host)
if err != nil {
return nil, fmt.Errorf("invalid host in config file: %v", err)
}
result.Host = host
result.Port = port
} else {
result.Host = "127.0.0.1"
}
// Apply auth info from config
result.Username = config.User
result.Password = config.Password
result.Key = config.APIToken
// Determine mode: if config has auth info, use user mode
if config.User != "" || config.APIToken != "" {
result.Type = "user"
} else {
result.Type = "admin"
result.Username = "admin@ragflow.io"
}
// Set default port if not specified in config
if result.Port == -1 {
if result.Type == "admin" {
result.Port = 9381
} else {
result.Port = 9380
}
}
// Get command from remaining args (no need for quotes or semicolon)
if len(remainingArgs) > 0 {
result.Command = strings.Join(remainingArgs, " ") + ";"
}
return result, nil
}
// buildArgsFromFlags builds ConnectionArgs from command line flags
func buildArgsFromFlags(host *string, port *int, password *string, key *string, userMode *bool, username *string, remainingArgs []string) (*ConnectionArgs, error) {
result := &ConnectionArgs{
Host: *host,
Port: *port,
Password: *password,
Key: *key,
Username: *username,
}
// Determine mode
if *userMode {
result.Type = "user"
} else {
result.Type = "admin"
}
// Set default port based on type if not specified
if result.Port == -1 {
if result.Type == "admin" {
result.Port = 9383
} else {
result.Port = 9384
}
}
// Determine username based on mode
if result.Type == "admin" && result.Username == "" {
result.Username = "admin@ragflow.io"
}
// Get command from remaining args (no need for quotes or semicolon)
if len(remainingArgs) > 0 {
result.Command = strings.Join(remainingArgs, " ") + ";"
}
return result, nil
}
// PrintUsage prints the CLI usage information
func PrintUsage() {
fmt.Println(`RAGFlow CLI Client
Usage: ragflow_cli [options] [command]
Options:
-h string Admin or RAGFlow service host (default "127.0.0.1")
-p int Admin or RAGFlow service port (default 9381 for admin, 9380 for user)
-w string Superuser password
-k string API key for authentication
-f string Path to config file (YAML format)
-admin Run in admin mode (default)
-user Run in user mode
-u string Username (email). In admin mode defaults to admin@ragflow.io
--help Show this help message
Configuration File:
The CLI will automatically read rf.yml from the current directory if it exists.
Use -f to specify a custom config file path.
Config file format:
host: 127.0.0.1:9380
user: your-email@example.com
password: your-password
api_token: your-api-token
When using a config file, you cannot use other command line flags except -help.
The command line is only for the SQL command.
Commands:
SQL commands like: LOGIN USER 'email'; LIST USERS; etc.
`)
}
// HistoryFile returns the path to the history file
func HistoryFile() string {
return os.Getenv("HOME") + "/" + historyFileName
}
const historyFileName = ".ragflow_cli_history"
// CLI represents the command line interface
type CLI struct {
client *RAGFlowClient
prompt string
running bool
line *liner.State
args *ConnectionArgs
}
// NewCLI creates a new CLI instance
func NewCLI() (*CLI, error) {
return NewCLIWithArgs(nil)
}
// NewCLIWithArgs creates a new CLI instance with connection arguments
func NewCLIWithArgs(args *ConnectionArgs) (*CLI, error) {
// Create liner first
line := liner.NewLiner()
// Determine server type
serverType := "user"
if args != nil && args.Type != "" {
serverType = args.Type
}
// Create client with password prompt using liner
client := NewRAGFlowClient(serverType)
client.PasswordPrompt = line.PasswordPrompt
// Apply connection arguments if provided
client.HTTPClient.Host = args.Host
client.HTTPClient.Port = args.Port
// Set prompt based on server type
prompt := "RAGFlow> "
if serverType == "admin" {
prompt = "RAGFlow(admin)> "
}
return &CLI{
prompt: prompt,
client: client,
line: line,
args: args,
}, nil
}
// Run starts the interactive CLI
func (c *CLI) Run() error {
if c.args.Type == "admin" {
// Allow 3 attempts for password verification
maxAttempts := 3
for attempt := 1; attempt <= maxAttempts; attempt++ {
var input string
var err error
// Check if terminal supports password masking
if term.IsTerminal(int(os.Stdin.Fd())) {
input, err = c.line.PasswordPrompt("Please input your password: ")
} else {
// Terminal doesn't support password masking, use regular prompt
fmt.Println("Warning: This terminal does not support secure password input")
input, err = c.line.Prompt("Please input your password (will be visible): ")
}
if err != nil {
fmt.Printf("Error reading input: %v\n", err)
return err
}
input = strings.TrimSpace(input)
if input == "" {
if attempt < maxAttempts {
fmt.Println("Password cannot be empty, please try again")
continue
}
return errors.New("no password provided after 3 attempts")
}
// Set the password for verification
c.args.Password = input
if err = c.VerifyAuth(); err != nil {
if attempt < maxAttempts {
fmt.Printf("Authentication failed: %v (%d/%d attempts)\n", err, attempt, maxAttempts)
continue
}
return fmt.Errorf("authentication failed after %d attempts: %v", maxAttempts, err)
}
// Authentication successful
break
}
}
c.running = true
// Load history from file
histFile := HistoryFile()
if f, err := os.Open(histFile); err == nil {
c.line.ReadHistory(f)
f.Close()
}
// Save history on exit
defer func() {
if f, err := os.Create(histFile); err == nil {
c.line.WriteHistory(f)
f.Close()
}
c.line.Close()
}()
fmt.Println("Welcome to RAGFlow CLI")
fmt.Println("Type \\? for help, \\q to quit")
fmt.Println()
for c.running {
input, err := c.line.Prompt(c.prompt)
if err != nil {
fmt.Printf("Error reading input: %v\n", err)
continue
}
input = strings.TrimSpace(input)
if input == "" {
continue
}
// Add to history (skip meta commands)
if !strings.HasPrefix(input, "\\") {
c.line.AppendHistory(input)
}
if err = c.execute(input); err != nil {
fmt.Printf("CLI error: %v\n", err)
}
}
return nil
}
func (c *CLI) execute(input string) error {
p := NewParser(input)
cmd, err := p.Parse()
if err != nil {
return err
}
if cmd == nil {
return nil
}
// Handle meta commands
if cmd.Type == "meta" {
return c.handleMetaCommand(cmd)
}
// Execute the command using the client
var result ResponseIf
result, err = c.client.ExecuteCommand(cmd)
if result != nil {
result.PrintOut()
}
return err
}
func (c *CLI) handleMetaCommand(cmd *Command) error {
command := cmd.Params["command"].(string)
args, _ := cmd.Params["args"].([]string)
switch command {
case "q", "quit", "exit":
fmt.Println("Goodbye!")
c.running = false
case "?", "h", "help":
c.printHelp()
case "c", "clear":
// Clear screen (simple approach)
fmt.Print("\033[H\033[2J")
case "admin":
c.client.ServerType = "admin"
c.prompt = "RAGFlow(admin)> "
fmt.Println("Switched to ADMIN mode")
case "user":
c.client.ServerType = "user"
c.prompt = "RAGFlow> "
fmt.Println("Switched to USER mode")
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)
}
return nil
}
func (c *CLI) printHelp() {
help := `
RAGFlow CLI Help
================
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
Commands (User Mode):
LOGIN USER 'email'; - Login as user
REGISTER USER 'name' AS 'nickname' PASSWORD 'pwd'; - Register new user
SHOW VERSION; - Show version info
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
LIST TOKENS; - List API tokens
CREATE TOKEN; - Create new API token
DROP TOKEN 'token_value'; - Delete an API token
SET TOKEN 'token_value'; - Set and validate API token
SHOW TOKEN; - Show current API token
UNSET TOKEN; - Remove current API token
Commands (Admin Mode):
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 <id>; - Show service details
PING; - Ping server
... and many more
Meta Commands:
\? or \h - Show this help
\q or \quit - Exit CLI
\c or \clear - Clear screen
For more information, see documentation.
`
fmt.Println(help)
}
// Cleanup performs cleanup before exit
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()
}
// RunSingleCommand executes a single command and exits
func (c *CLI) RunSingleCommand(command string) error {
// Execute the command
if err := c.execute(command); err != nil {
return err
}
return nil
}
// VerifyAuth verifies authentication if needed
func (c *CLI) VerifyAuth() error {
if c.args == nil {
return nil
}
if c.args.Username == "" {
return fmt.Errorf("username is required")
}
if c.args.Password == "" {
return fmt.Errorf("password is required")
}
// Create login command with username and password
cmd := NewCommand("login_user")
cmd.Params["email"] = c.args.Username
cmd.Params["password"] = c.args.Password
_, err := c.client.ExecuteCommand(cmd)
return err
}