mirror of
https://github.com/infiniflow/ragflow.git
synced 2026-06-29 23:41:12 +08:00
### What problem does this PR solve? rename ragflow cli binary ### Type of change - [x] Refactoring Signed-off-by: Jin Hai <haijin.chn@gmail.com>
1009 lines
32 KiB
Go
1009 lines
32 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 (
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"os"
|
|
//"os/signal"
|
|
"path/filepath"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"github.com/peterh/liner"
|
|
"gopkg.in/yaml.v3"
|
|
|
|
"ragflow/internal/cli/filesystem"
|
|
)
|
|
|
|
type APIServerConfig struct {
|
|
Name string `yaml:"name"`
|
|
Host string `yaml:"host"`
|
|
UserName *string `yaml:"user_name"`
|
|
UserPassword *string `yaml:"password"`
|
|
ApiToken *string `yaml:"api_token"`
|
|
KeyFile *string `yaml:"key_file"`
|
|
IP string
|
|
Port int
|
|
}
|
|
|
|
// ConfigFile represents the rf.yml configuration file structure
|
|
type ConfigFile struct {
|
|
Host string `yaml:"host"` // default API server host
|
|
APIToken string `yaml:"api_token"` // default API server api token
|
|
UserName string `yaml:"user_name"` // default API server user name
|
|
Password string `yaml:"password"` // default API server password
|
|
APIServerMap map[string]*APIServerConfig `yaml:"api_servers"`
|
|
}
|
|
|
|
// OutputFormat represents the output format type
|
|
type OutputFormat string
|
|
|
|
const (
|
|
OutputFormatTable OutputFormat = "table" // Table format with borders
|
|
OutputFormatPlain OutputFormat = "plain" // Plain text, space-separated (no borders)
|
|
OutputFormatJSON OutputFormat = "json" // JSON format (reserved for future use)
|
|
)
|
|
|
|
type CommandLineMode string
|
|
|
|
const (
|
|
APIMode CommandLineMode = "api"
|
|
AdminMode CommandLineMode = "admin"
|
|
IngestorMode CommandLineMode = "ingestor" // If we want to access ingestor
|
|
CollectorMode CommandLineMode = "collector" // If we want to access collector
|
|
DefaultAPIServer = "default"
|
|
)
|
|
|
|
type CommandLineConfig struct {
|
|
CLIMode CommandLineMode
|
|
AdminClientConfig *AdminModeConfig
|
|
APIClientConfig APIModeConfig
|
|
ShowHelp bool
|
|
Verbose bool
|
|
Interactive bool
|
|
OutputFormat OutputFormat
|
|
Command *string
|
|
}
|
|
|
|
type AdminModeConfig struct {
|
|
AdminHost string
|
|
AdminPort int
|
|
AdminName *string
|
|
AdminPassword *string
|
|
KeyFile *string
|
|
//AdminCommand *string
|
|
}
|
|
|
|
type APIModeConfig struct {
|
|
CurrentAPIServer string
|
|
APIServerMap map[string]*APIServerConfig
|
|
}
|
|
|
|
func (c *CommandLineConfig) Print() {
|
|
b, err := json.MarshalIndent(c, "", " ")
|
|
if err == nil {
|
|
fmt.Println(string(b))
|
|
}
|
|
}
|
|
|
|
func ParseArgs(args []string) (*CommandLineConfig, error) {
|
|
commandLineConfig := &CommandLineConfig{
|
|
CLIMode: APIMode,
|
|
AdminClientConfig: nil,
|
|
ShowHelp: false,
|
|
Verbose: false,
|
|
Interactive: true,
|
|
OutputFormat: OutputFormatTable,
|
|
Command: nil,
|
|
}
|
|
for i := 0; i < len(args); i++ {
|
|
arg := args[i]
|
|
|
|
switch arg {
|
|
case "-o", "--output":
|
|
// Parse output format
|
|
if i+1 < len(args) && !strings.HasPrefix(args[i+1], "-") {
|
|
format := args[i+1]
|
|
switch format {
|
|
case "plain":
|
|
commandLineConfig.OutputFormat = OutputFormatPlain
|
|
case "json":
|
|
commandLineConfig.OutputFormat = OutputFormatJSON
|
|
default:
|
|
commandLineConfig.OutputFormat = OutputFormatTable
|
|
}
|
|
i++
|
|
}
|
|
case "-v", "--verbose":
|
|
commandLineConfig.Verbose = true
|
|
case "--admin", "-admin":
|
|
commandLineConfig.CLIMode = AdminMode
|
|
case "--help", "-help":
|
|
commandLineConfig.ShowHelp = true
|
|
default:
|
|
if !strings.HasPrefix(arg, "-") {
|
|
commandLineConfig.Interactive = false
|
|
}
|
|
}
|
|
}
|
|
|
|
var commandArgs []string
|
|
var foundCommand bool
|
|
|
|
switch commandLineConfig.CLIMode {
|
|
case APIMode:
|
|
defaultApiServerConfig := &APIServerConfig{
|
|
UserName: nil,
|
|
UserPassword: nil,
|
|
ApiToken: nil,
|
|
}
|
|
|
|
configFile := "rf.yml"
|
|
for i := 0; i < len(args); i++ {
|
|
arg := args[i]
|
|
|
|
// Handle known global flags (already parsed in first pass).
|
|
// Intercept here regardless of position so they are never
|
|
// mistaken for command args or unknown flags downstream.
|
|
switch arg {
|
|
case "-o", "--output":
|
|
if i+1 < len(args) {
|
|
i++
|
|
}
|
|
continue
|
|
case "-v", "--verbose", "--help", "-help":
|
|
continue
|
|
case "--admin", "-admin":
|
|
return nil, fmt.Errorf("unexpected parameter: --admin")
|
|
}
|
|
|
|
// If we've found the command, collect remaining args
|
|
if foundCommand {
|
|
commandArgs = append(commandArgs, arg)
|
|
continue
|
|
}
|
|
|
|
switch arg {
|
|
case "-h", "--host":
|
|
if i+1 < len(args) && !strings.HasPrefix(args[i+1], "-") {
|
|
hostVal := args[i+1]
|
|
h, port, err := parseHostPort(hostVal)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("invalid host format: %v", err)
|
|
}
|
|
defaultApiServerConfig.IP = h
|
|
defaultApiServerConfig.Port = port
|
|
i++
|
|
}
|
|
case "-t", "--token":
|
|
if i+1 < len(args) && !strings.HasPrefix(args[i+1], "-") {
|
|
defaultApiServerConfig.ApiToken = &args[i+1]
|
|
i++
|
|
}
|
|
case "-u", "--user":
|
|
if i+1 < len(args) && !strings.HasPrefix(args[i+1], "-") {
|
|
defaultApiServerConfig.UserName = &args[i+1]
|
|
i++
|
|
}
|
|
case "-p", "--password":
|
|
if i+1 < len(args) && !strings.HasPrefix(args[i+1], "-") {
|
|
defaultApiServerConfig.UserPassword = &args[i+1]
|
|
i++
|
|
}
|
|
case "-f", "--config":
|
|
if i+1 < len(args) && !strings.HasPrefix(args[i+1], "-") {
|
|
configFile = args[i+1]
|
|
// Convert to absolute path immediately
|
|
if !filepath.IsAbs(configFile) {
|
|
absPath, err := filepath.Abs(configFile)
|
|
if err == nil {
|
|
configFile = absPath
|
|
}
|
|
}
|
|
i++
|
|
}
|
|
case "-k", "--key":
|
|
if i+1 < len(args) && !strings.HasPrefix(args[i+1], "-") {
|
|
defaultApiServerConfig.KeyFile = &args[i+1]
|
|
i++
|
|
}
|
|
default:
|
|
// Non-flag argument (command)
|
|
if !strings.HasPrefix(arg, "-") {
|
|
commandArgs = append(commandArgs, arg)
|
|
foundCommand = true
|
|
}
|
|
}
|
|
}
|
|
|
|
var config ConfigFile
|
|
data, err := os.ReadFile(configFile)
|
|
if err == nil {
|
|
if err = yaml.Unmarshal(data, &config); err != nil {
|
|
return nil, fmt.Errorf("failed to parse rf.yml: %v", err)
|
|
}
|
|
if config.Host != "" {
|
|
var h string
|
|
var port int
|
|
h, port, err = parseHostPort(config.Host)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("invalid host in config file: %v", err)
|
|
}
|
|
if defaultApiServerConfig.IP == "" {
|
|
defaultApiServerConfig.IP = h
|
|
}
|
|
if defaultApiServerConfig.Port == 0 {
|
|
defaultApiServerConfig.Port = port
|
|
}
|
|
}
|
|
if config.UserName != "" {
|
|
if defaultApiServerConfig.UserName == nil {
|
|
defaultApiServerConfig.UserName = &config.UserName
|
|
}
|
|
}
|
|
if config.Password != "" {
|
|
if defaultApiServerConfig.UserPassword == nil {
|
|
defaultApiServerConfig.UserPassword = &config.Password
|
|
}
|
|
}
|
|
if config.APIToken != "" {
|
|
if defaultApiServerConfig.ApiToken == nil {
|
|
defaultApiServerConfig.ApiToken = &config.APIToken
|
|
}
|
|
}
|
|
} else {
|
|
if configFile == "rf.yml" && os.IsNotExist(err) {
|
|
} else {
|
|
return nil, fmt.Errorf("failed to read %s: %v", configFile, err)
|
|
}
|
|
}
|
|
|
|
if defaultApiServerConfig.IP == "" {
|
|
defaultApiServerConfig.IP = "127.0.0.1"
|
|
}
|
|
if defaultApiServerConfig.Port == 0 {
|
|
defaultApiServerConfig.Port = 9384
|
|
}
|
|
|
|
commandLineConfig.APIClientConfig.APIServerMap = config.APIServerMap
|
|
if commandLineConfig.APIClientConfig.APIServerMap == nil {
|
|
commandLineConfig.APIClientConfig.APIServerMap = make(map[string]*APIServerConfig)
|
|
}
|
|
if commandLineConfig.APIClientConfig.APIServerMap[DefaultAPIServer] != nil {
|
|
return nil, fmt.Errorf("'Default' API server config should be in api_servers")
|
|
}
|
|
commandLineConfig.APIClientConfig.APIServerMap[DefaultAPIServer] = defaultApiServerConfig
|
|
commandLineConfig.APIClientConfig.CurrentAPIServer = DefaultAPIServer
|
|
case AdminMode:
|
|
AdminConfig := &AdminModeConfig{
|
|
AdminHost: "127.0.0.1",
|
|
AdminPort: 9383,
|
|
//AdminName: "admin@ragflow.io",
|
|
//AdminPassword: "admin",
|
|
}
|
|
|
|
for i := 0; i < len(args); i++ {
|
|
arg := args[i]
|
|
|
|
// Handle known global flags regardless of position
|
|
switch arg {
|
|
case "-o", "--output":
|
|
if i+1 < len(args) {
|
|
i++
|
|
}
|
|
continue
|
|
case "-v", "--verbose", "--admin", "-admin", "--help", "-help":
|
|
continue
|
|
case "-t", "--token":
|
|
return nil, fmt.Errorf("token is invalid in admin mode")
|
|
case "-f", "--config":
|
|
return nil, fmt.Errorf("config is invalid in admin mode")
|
|
}
|
|
|
|
// If we've found the command, collect remaining args
|
|
if foundCommand {
|
|
commandArgs = append(commandArgs, arg)
|
|
continue
|
|
}
|
|
|
|
switch arg {
|
|
case "-h", "--host":
|
|
if i+1 < len(args) && !strings.HasPrefix(args[i+1], "-") {
|
|
hostVal := args[i+1]
|
|
h, port, err := parseHostPort(hostVal)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("invalid host format: %v", err)
|
|
}
|
|
AdminConfig.AdminHost = h
|
|
AdminConfig.AdminPort = port
|
|
i++
|
|
}
|
|
case "-u", "--user":
|
|
if i+1 < len(args) && !strings.HasPrefix(args[i+1], "-") {
|
|
AdminConfig.AdminName = &args[i+1]
|
|
i++
|
|
}
|
|
case "-k", "--key":
|
|
if i+1 < len(args) && !strings.HasPrefix(args[i+1], "-") {
|
|
AdminConfig.KeyFile = &args[i+1]
|
|
i++
|
|
}
|
|
case "-p", "--password":
|
|
if i+1 < len(args) && !strings.HasPrefix(args[i+1], "-") {
|
|
AdminConfig.AdminPassword = &args[i+1]
|
|
i++
|
|
}
|
|
default:
|
|
// Non-flag argument (command)
|
|
if !strings.HasPrefix(arg, "-") {
|
|
commandArgs = append(commandArgs, arg)
|
|
foundCommand = true
|
|
}
|
|
}
|
|
}
|
|
commandLineConfig.AdminClientConfig = AdminConfig
|
|
}
|
|
|
|
commandArgsLen := len(commandArgs)
|
|
if commandArgsLen > 0 {
|
|
if commandArgsLen == 1 {
|
|
commandLineConfig.Command = &commandArgs[0]
|
|
} else {
|
|
ApiCommand := strings.Join(commandArgs, " ")
|
|
commandLineConfig.Command = &ApiCommand
|
|
}
|
|
}
|
|
|
|
return commandLineConfig, nil
|
|
}
|
|
|
|
// 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
|
|
}
|
|
|
|
// PrintUsage prints the CLI usage information
|
|
func PrintUsage() {
|
|
fmt.Println(`RAGFlow CLI Client
|
|
|
|
Usage: ragflow-cli [options] [command]
|
|
|
|
Options:
|
|
-h, --host string RAGFlow service address (host:port, default "127.0.0.1:9380")
|
|
-t, --token string API token for authentication
|
|
-u, --user string Username for authentication
|
|
-p, --password string Password for authentication
|
|
-f, --config string Path to config file (YAML format)
|
|
-o, --output string Output format: table, plain, json (search defaults to json)
|
|
-v, --verbose Enable verbose logging (shows debug info)
|
|
--admin, -admin Run in admin mode
|
|
--help Show this help message
|
|
|
|
Mode:
|
|
--admin, -admin Run in admin mode (prompt: RAGFlow(admin)>)
|
|
Default is user mode (prompt: RAGFlow(user)>).
|
|
|
|
Authentication:
|
|
You can authenticate using either:
|
|
1. API token: -t or --token
|
|
2. Username and password: -u/--user and -p/--password
|
|
Note: These two methods are mutually exclusive.
|
|
|
|
Configuration File:
|
|
The CLI will automatically read rf.yml from the current directory if it exists.
|
|
Use -f or --config to specify a custom config file path.
|
|
Command line options override config file values.
|
|
|
|
Config file format:
|
|
host: 127.0.0.1:9380
|
|
api_token: your-api-token
|
|
user_name: your-username
|
|
password: your-password
|
|
|
|
Note: api_token and user_name/password are mutually exclusive in config file.
|
|
|
|
Commands:
|
|
SQL commands (use quotes): "LIST USERS", "CREATE USER 'email' 'password'", etc.
|
|
Filesystem commands (no quotes): ls datasets, search "keyword", cat path, etc.
|
|
Skill commands:
|
|
install-skill <space> <path|url> [options] Install a skill from local path or remote URL
|
|
uninstall-skill <space> <skill-name> Remove an installed skill
|
|
search skills -q <query> [--space space1] Search skills in a space
|
|
If no command is provided, CLI runs in interactive mode.`)
|
|
}
|
|
|
|
// 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 {
|
|
running bool
|
|
line *liner.State
|
|
|
|
APIServerClientMap map[string]*HTTPClient
|
|
AdminServerClient *HTTPClient
|
|
PasswordPrompt PasswordPromptFunc // Function for password input
|
|
ContextEngine *filesystem.Engine // Context Engine for virtual filesystem
|
|
CurrentModel *CurrentModel // Current model configuration
|
|
Config *CommandLineConfig
|
|
}
|
|
|
|
func NewCLIWithConfig(commandLineConfig *CommandLineConfig) (*CLI, error) {
|
|
// Create liner first
|
|
line := liner.NewLiner()
|
|
|
|
cli := &CLI{
|
|
line: line,
|
|
Config: commandLineConfig,
|
|
}
|
|
|
|
if commandLineConfig.CLIMode == APIMode {
|
|
apiServerConfig := commandLineConfig.APIClientConfig.APIServerMap[commandLineConfig.APIClientConfig.CurrentAPIServer]
|
|
httpClient := NewHTTPClient()
|
|
httpClient.Host = apiServerConfig.IP
|
|
httpClient.Port = apiServerConfig.Port
|
|
if apiServerConfig.ApiToken != nil {
|
|
httpClient.APIToken = apiServerConfig.ApiToken
|
|
httpClient.useAPIToken = true
|
|
}
|
|
cli.APIServerClientMap = map[string]*HTTPClient{
|
|
cli.Config.APIClientConfig.CurrentAPIServer: httpClient,
|
|
}
|
|
// Auto-login if user and password are provided (from config file)
|
|
if apiServerConfig.UserName != nil && apiServerConfig.UserPassword != nil && apiServerConfig.ApiToken == nil {
|
|
if err := cli.LoginUserInteractive(*apiServerConfig.UserName, *apiServerConfig.UserPassword); err != nil {
|
|
line.Close()
|
|
return nil, fmt.Errorf("auto-login failed: %w", err)
|
|
}
|
|
}
|
|
|
|
engine := filesystem.NewEngine()
|
|
|
|
// Register providers
|
|
// TODO: if http config change, engine http config won't be updated. They should share the same config
|
|
engine.RegisterProvider(filesystem.NewDatasetProvider(&httpClientAdapter{httpClient}))
|
|
engine.RegisterProvider(filesystem.NewFileProvider(&httpClientAdapter{httpClient}))
|
|
engine.RegisterProvider(filesystem.NewSkillProvider(&httpClientAdapter{httpClient}))
|
|
|
|
cli.ContextEngine = engine
|
|
} else if commandLineConfig.CLIMode == AdminMode {
|
|
httpClient := NewHTTPClient()
|
|
httpClient.Host = commandLineConfig.AdminClientConfig.AdminHost
|
|
httpClient.Port = commandLineConfig.AdminClientConfig.AdminPort
|
|
cli.AdminServerClient = httpClient
|
|
|
|
adminServerConfig := commandLineConfig.AdminClientConfig
|
|
// Auto-login if user and password are provided (from config file)
|
|
if adminServerConfig.AdminName != nil && adminServerConfig.AdminPassword != nil {
|
|
if err := cli.LoginUserInteractive(*adminServerConfig.AdminName, *adminServerConfig.AdminPassword); err != nil {
|
|
line.Close()
|
|
return nil, fmt.Errorf("auto-login failed: %w", err)
|
|
}
|
|
}
|
|
|
|
} else {
|
|
return nil, fmt.Errorf("invalid CLI mode: %s", commandLineConfig.CLIMode)
|
|
}
|
|
|
|
return cli, nil
|
|
}
|
|
|
|
// Run starts the interactive CLI
|
|
func (c *CLI) Run() error {
|
|
// If username is provided without password, prompt for password
|
|
cliConfig := c.Config
|
|
switch cliConfig.CLIMode {
|
|
case APIMode:
|
|
apiConfig := c.Config.APIClientConfig.APIServerMap[c.Config.APIClientConfig.CurrentAPIServer]
|
|
if apiConfig.UserName != nil && apiConfig.UserPassword == nil && apiConfig.ApiToken == nil {
|
|
// provider username but no password or api token
|
|
maxAttempts := 3
|
|
for attempt := 1; attempt <= maxAttempts; attempt++ {
|
|
fmt.Printf("Please input your password: ")
|
|
|
|
password, err := ReadPassword()
|
|
|
|
if password == "" {
|
|
if attempt < maxAttempts {
|
|
fmt.Println("Password cannot be empty, please try again")
|
|
continue
|
|
}
|
|
return errors.New("no password provided after 3 attempts")
|
|
}
|
|
|
|
apiConfig.UserPassword = &password
|
|
|
|
if err = c.VerifyAuth(*apiConfig.UserName, *apiConfig.UserPassword); err != nil {
|
|
if attempt < maxAttempts {
|
|
fmt.Printf("Authentication failed (%d/%d attempts)\n", attempt, maxAttempts)
|
|
continue
|
|
}
|
|
return fmt.Errorf("authentication failed after %d attempts", maxAttempts)
|
|
}
|
|
|
|
break
|
|
}
|
|
}
|
|
|
|
case AdminMode:
|
|
adminConfig := c.Config.AdminClientConfig
|
|
if adminConfig.AdminName != nil && adminConfig.AdminPassword == nil {
|
|
// provider username but no password or api token
|
|
maxAttempts := 3
|
|
for attempt := 1; attempt <= maxAttempts; attempt++ {
|
|
fmt.Printf("Please input your password: ")
|
|
|
|
password, err := ReadPassword()
|
|
|
|
if password == "" {
|
|
if attempt < maxAttempts {
|
|
fmt.Println("Password cannot be empty, please try again")
|
|
continue
|
|
}
|
|
return errors.New("no password provided after 3 attempts")
|
|
}
|
|
|
|
adminConfig.AdminPassword = &password
|
|
|
|
if err = c.VerifyAuth(*adminConfig.AdminName, *adminConfig.AdminPassword); err != nil {
|
|
if attempt < maxAttempts {
|
|
fmt.Printf("Authentication failed (%d/%d attempts)\n", attempt, maxAttempts)
|
|
continue
|
|
}
|
|
return fmt.Errorf("authentication failed after %d attempts", maxAttempts)
|
|
}
|
|
|
|
break
|
|
}
|
|
}
|
|
|
|
default:
|
|
return fmt.Errorf("unexpected CLI mode: %s", cliConfig.CLIMode)
|
|
}
|
|
|
|
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 {
|
|
var prompt string
|
|
switch cliConfig.CLIMode {
|
|
case APIMode:
|
|
prompt = fmt.Sprintf("RAGFlow(api/%s)> ", c.Config.APIClientConfig.CurrentAPIServer)
|
|
case AdminMode:
|
|
prompt = "RAGFlow(admin)> "
|
|
default:
|
|
return fmt.Errorf("unexpected CLI mode: %s", cliConfig.CLIMode)
|
|
}
|
|
input, err := c.line.Prompt(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(c.Config.CLIMode)
|
|
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.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 "pwd":
|
|
dir, err := os.Getwd()
|
|
if err != nil {
|
|
return fmt.Errorf("get working directory: %w", err)
|
|
}
|
|
fmt.Println(dir)
|
|
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
|
|
LOGIN USER 'email' PASSWORD 'pwd'; - Login as user with password
|
|
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
|
|
LIST PROVIDERS; - List available LLM providers
|
|
CREATE TOKEN; - Create new API token
|
|
ADD PROVIDER 'name'; - Create a provider without API key
|
|
ADD PROVIDER 'name' 'api_key'; - Create a provider with API key
|
|
DROP TOKEN 'token_value'; - Delete an API token
|
|
DELETE PROVIDER 'name'; - Delete a provider
|
|
SET TOKEN 'token_value'; - Set and validate API token
|
|
SHOW TOKEN; - Show current API token
|
|
SHOW PROVIDER 'name'; - Show provider details
|
|
SHOW CURRENT MODEL; - Show current model settings
|
|
UNSET TOKEN; - Remove current API token
|
|
ALTER PROVIDER 'name' NAME 'new_name'; - Rename a provider
|
|
USE MODEL 'provider/instance/model'; - Set current model for chat
|
|
CHAT 'message'; - Chat using current model
|
|
CHAT 'provider/instance/model' 'message'; - Chat with specified model
|
|
OPENAI_CHAT 'chat_id' 'message' [options] ; - OpenAI-compatible chat
|
|
(run openai_chat -h for detailed options)
|
|
|
|
Filesystem Commands (no quotes):
|
|
ls [path] - List resources
|
|
e.g., ls - List root (providers and folders)
|
|
e.g., ls datasets - List all datasets
|
|
e.g., ls datasets/kb1 - Show dataset info
|
|
e.g., ls myfolder - List files in 'myfolder' (file_manager)
|
|
list [path] - Same as ls
|
|
search [options] - Search resources in datasets
|
|
Use 'search -h' for detailed options
|
|
cat <path> - Show file content
|
|
e.g., cat files/docs/file.txt - Show file content
|
|
Note: cat datasets or cat datasets/kb1 will error
|
|
|
|
Examples:
|
|
ragflow-cli -f rf.yml "LIST USERS" # SQL mode (with quotes)
|
|
ragflow-cli -f rf.yml ls datasets # Filesystem mode (no quotes)
|
|
ragflow-cli -f rf.yml ls files # List files in root
|
|
ragflow-cli -f rf.yml cat datasets # Error: datasets is a directory
|
|
ragflow-cli -f rf.yml ls files/myfolder # List folder contents
|
|
|
|
For more information, see documentation.
|
|
`
|
|
fmt.Println(help)
|
|
}
|
|
|
|
// Cleanup performs cleanup before exit
|
|
func (c *CLI) Cleanup() {
|
|
// Close liner to restore terminal settings
|
|
if c.line != nil {
|
|
c.line.Close()
|
|
}
|
|
}
|
|
|
|
// RunSingleCommand executes a single command and exits
|
|
func (c *CLI) RunSingleCommand(command *string) error {
|
|
// Ensure cleanup is called on exit to restore terminal settings
|
|
defer c.Cleanup()
|
|
|
|
// Execute the command
|
|
if err := c.execute(*command); err != nil {
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// VerifyAuth verifies authentication if needed
|
|
func (c *CLI) NewVerifyAuth(username, password *string) error {
|
|
// Otherwise, use username/password authentication
|
|
if username == nil {
|
|
return fmt.Errorf("username is required")
|
|
}
|
|
|
|
if password == nil {
|
|
return fmt.Errorf("password is required")
|
|
}
|
|
|
|
// Create login command with username and password
|
|
cmd := NewCommand("login_user")
|
|
cmd.Params["email"] = *username
|
|
cmd.Params["password"] = *password
|
|
_, err := c.ExecuteCommand(cmd)
|
|
return err
|
|
}
|
|
|
|
// VerifyAuth verifies authentication if needed
|
|
func (c *CLI) VerifyAuth(username, password string) error {
|
|
// Otherwise, use username/password authentication
|
|
if username == "" {
|
|
return fmt.Errorf("username is required")
|
|
}
|
|
|
|
if password == "" {
|
|
return fmt.Errorf("password is required")
|
|
}
|
|
|
|
// Create login command with username and password
|
|
cmd := NewCommand("login_user")
|
|
cmd.Params["email"] = username
|
|
cmd.Params["password"] = password
|
|
_, err := c.ExecuteCommand(cmd)
|
|
return err
|
|
}
|
|
|
|
func (c *CLI) GetPublicKeyPEM() ([]byte, error) {
|
|
|
|
var publicKeyFile *string = nil
|
|
switch c.Config.CLIMode {
|
|
case AdminMode:
|
|
publicKeyFile = c.Config.AdminClientConfig.KeyFile
|
|
case APIMode:
|
|
publicKeyFile = c.Config.APIClientConfig.APIServerMap[c.Config.APIClientConfig.CurrentAPIServer].KeyFile
|
|
}
|
|
if publicKeyFile == nil {
|
|
result := "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEArq9XTUSeYr2+N1h3Afl/\nz8Dse/2yD0ZGrKwx+EEEcdsBLca9Ynmx3nIB5obmLlSfmskLpBo0UACBmB5rEjBp\n2Q2f3AG3Hjd4B+gNCG6BDaawuDlgANIhGnaTLrIqWrrcm4EMzJOnAOI1fgzJRsOO\nUEfaS318Eq9OVO3apEyCCt0lOQK6PuksduOjVxtltDav+guVAA068NrPYmRNabVK\nRNLJpL8w4D44sfth5RvZ3q9t+6RTArpEtc5sh5ChzvqPOzKGMXW83C95TxmXqpbK\n6olN4RevSfVjEAgCydH6HN6OhtOQEcnrU97r9H0iZOWwbw3pVrZiUkuRD1R56Wzs\n2wIDAQAB\n-----END PUBLIC KEY-----"
|
|
return []byte(result), nil
|
|
}
|
|
|
|
publicKeyPEM, err := os.ReadFile(*publicKeyFile)
|
|
if err != nil {
|
|
return []byte(""), fmt.Errorf("failed to read public key: %w", err)
|
|
}
|
|
return publicKeyPEM, nil
|
|
}
|
|
|
|
// printSearchHelp prints help for the search command
|
|
func printSearchHelp() {
|
|
help := `Search command usage: search <query> [path] [-n number]
|
|
|
|
Search for content in datasets or skills.
|
|
|
|
Arguments:
|
|
<query> Search query (required)
|
|
Example: "machine learning"
|
|
[path] Path to search in (default: datasets)
|
|
Supports:
|
|
- 'datasets' (all datasets)
|
|
- 'datasets/<kb_name>' (specific dataset)
|
|
- 'skills' (default skill space)
|
|
- 'skills/<space_name>' (specific skill space)
|
|
Example: skills/space1
|
|
|
|
Options:
|
|
-n, --number <num> Number of results to return (default: 10)
|
|
Example: -n 20
|
|
-h, --help Show this help message
|
|
|
|
Output:
|
|
Default output format is JSON. Use --output plain or --output table for other formats.
|
|
|
|
Examples:
|
|
search "neural networks" # Search all datasets
|
|
search "AI" datasets/kb1 # Search in kb1
|
|
search "RAG" skills/space1 -n 20 # Search skills in hub1, return 20 results
|
|
search "data processing" skills # Search skills (default space)
|
|
|
|
Datasets syntax (full filter set):
|
|
search 'query' on datasets 'kb_names' [with <option> <value> ...] [;]
|
|
|
|
'kb_names' is a single quoted string. Pass one name for a single
|
|
dataset, or a comma-separated list (no spaces) to search across
|
|
multiple datasets in one call:
|
|
'kb1' # one dataset
|
|
'kb1,kb2' # two datasets
|
|
'kb1,kb2,kb3' # three datasets
|
|
|
|
When 'on datasets' is given, the search runs against the named
|
|
dataset(s) and accepts the full WITH-option set below.
|
|
|
|
WITH options (space-separated, not comma-separated):
|
|
top_k <int> Number of results (default 5)
|
|
page_size <int> Page size for pagination
|
|
page <int> Page number (1-based)
|
|
similarity_threshold <float> Minimum similarity score (0.0-1.0)
|
|
vector_similarity_weight <float> Weight given to vector vs text score
|
|
keyword true|false Enable keyword extraction via LLM
|
|
use_kg true|false Enable knowledge-graph augmentation
|
|
rerank_id 'id' Rerank model to apply
|
|
tenant_rerank_id 'id' Tenant-scoped rerank model
|
|
search_id 'id' Idempotency / search-session id
|
|
meta_data_filter '<json>' Metadata filter (must be valid JSON)
|
|
cross_languages ['a','b'] Source languages to translate from
|
|
doc_ids ['d1',...] Restrict to specific document ids
|
|
|
|
Examples:
|
|
search 'AI' on datasets 'kb_chinese' with top_k 10;
|
|
search 'AI' on datasets 'kb1' 'kb2' with top_k 20 similarity_threshold 0.3 cross_languages ['Chinese']
|
|
doc_ids ['d1', 'd2'];
|
|
search 'manual' on datasets 'kb1' with
|
|
meta_data_filter '{"method":"manual","conditions":[{"key":"author","op":"eq","value":"Luo"}]}';
|
|
`
|
|
fmt.Println(help)
|
|
}
|
|
|
|
// printOpenaiChatHelp prints help for the OPENAI_CHAT command.
|
|
func printOpenaiChatHelp() {
|
|
help := `OPENAI_CHAT — hit POST /api/v1/openai/<chat_id>/chat/completions
|
|
|
|
Syntax:
|
|
OPENAI_CHAT 'chat_id' 'message'
|
|
[system "..."]
|
|
[history "user:...;assistant:...;user:..."]
|
|
[history_delimiter "<char>"]
|
|
[model <string>]
|
|
[temperature <float>] [max_tokens <int>] [stream <bool>]
|
|
[top_p <float>] [frequency_penalty <float>] [presence_penalty <float>]
|
|
[extra_body <json>] ;
|
|
|
|
Required positional:
|
|
'chat_id' the dialog id (becomes the URL path segment)
|
|
'message' the user message content
|
|
|
|
Named options (any order; all optional with defaults):
|
|
system '...' override the system prompt
|
|
history '...' prior turns: user:...;assistant:...;user:...
|
|
history_delimiter '...' turn separator for history (default ';')
|
|
model '...' 'model' (sentinel) or composite (default 'model')
|
|
temperature <float> 0..2 (default 0)
|
|
max_tokens <int> (default 0 = server/model default)
|
|
stream <bool> true|false (default false)
|
|
top_p <float> 0..1
|
|
frequency_penalty <float> -2..2
|
|
presence_penalty <float> -2..2
|
|
extra_body <json> '{"reference":true,...}'
|
|
|
|
Defaults:
|
|
model 'model' — server resolves to the dialog's configured LLM
|
|
stream false
|
|
temperature 0
|
|
history_delimiter ';' — commas in content survive unchanged
|
|
|
|
extra_body allowlist:
|
|
reference bool
|
|
reference_metadata { include?: bool, fields?: string[] }
|
|
metadata_condition { logic?: "and"|"or", conditions?: [{key, operator, value}] }
|
|
|
|
Examples:
|
|
OPENAI_CHAT 'cid' 'Hello, how are you?';
|
|
OPENAI_CHAT 'cid' 'Hello' model 'Qwen/Qwen3-8B@ling@SILICONFLOW' temperature 0.7 max_tokens 512;
|
|
OPENAI_CHAT 'cid' 'Hello' stream true;
|
|
OPENAI_CHAT 'cid' 'next' system 'You are concise.' history 'user:q1;assistant:a1';
|
|
OPENAI_CHAT 'cid' 'Hello' extra_body '{"reference":true,"metadata_condition":{"logic":"and","conditions":[{"key":"doc_type","operator":"is","value":"faq"}]}}';
|
|
`
|
|
fmt.Println(help)
|
|
}
|