RAGFlow go API server (#13240)

# RAGFlow Go Implementation Plan 🚀

This repository tracks the progress of porting RAGFlow to Go. We'll
implement core features and provide performance comparisons between
Python and Go versions.

## Implementation Checklist

- [x] User Management APIs
- [x] Dataset Management Operations
- [x] Retrieval Test
- [x] Chat Management Operations
- [x] Infinity Go SDK

---------

Signed-off-by: Jin Hai <haijin.chn@gmail.com>
Co-authored-by: Yingfeng Zhang <yingfeng.zhang@gmail.com>
This commit is contained in:
Jin Hai
2026-03-04 19:17:16 +08:00
committed by GitHub
parent 2508c46c8f
commit 70e9743ef1
257 changed files with 80490 additions and 6 deletions

87
internal/cli/README.md Normal file
View File

@ -0,0 +1,87 @@
# RAGFlow CLI (Go Version)
This is the Go implementation of the RAGFlow command-line interface, compatible with the Python version's syntax.
## Features
- Interactive mode only
- Full compatibility with Python CLI syntax
- Recursive descent parser for SQL-like commands
- Support for all major commands:
- User management: LOGIN, REGISTER, CREATE USER, DROP USER, LIST USERS, etc.
- Service management: LIST SERVICES, SHOW SERVICE, STARTUP/SHUTDOWN/RESTART SERVICE
- Role management: CREATE ROLE, DROP ROLE, LIST ROLES, GRANT/REVOKE PERMISSION
- Dataset management: CREATE DATASET, DROP DATASET, LIST DATASETS
- Model management: SET/RESET DEFAULT LLM/VLM/EMBEDDING/etc.
- And more...
## Usage
Build and run:
```bash
go build -o ragflow_cli ./cmd/ragflow_cli.go
./ragflow_cli
```
## Architecture
```
internal/cli/
├── cli.go # Main CLI loop and interaction
├── parser/ # Command parser package
│ ├── types.go # Token and Command types
│ ├── lexer.go # Lexical analyzer
│ └── parser.go # Recursive descent parser
```
## Command Examples
```sql
-- Authentication
LOGIN USER 'admin@example.com';
-- User management
REGISTER USER 'john' AS 'John Doe' PASSWORD 'secret';
CREATE USER 'jane' 'password123';
DROP USER 'jane';
LIST USERS;
SHOW USER 'john';
-- Service management
LIST SERVICES;
SHOW SERVICE 1;
STARTUP SERVICE 1;
SHUTDOWN SERVICE 1;
RESTART SERVICE 1;
PING;
-- Role management
CREATE ROLE admin DESCRIPTION 'Administrator role';
LIST ROLES;
GRANT read,write ON datasets TO ROLE admin;
-- Dataset management
CREATE DATASET 'my_dataset' WITH EMBEDDING 'text-embedding-ada-002' PARSER 'naive';
LIST DATASETS;
DROP DATASET 'my_dataset';
-- Model configuration
SET DEFAULT LLM 'gpt-4';
SET DEFAULT EMBEDDING 'text-embedding-ada-002';
RESET DEFAULT LLM;
-- Meta commands
\? -- Show help
\q -- Quit
\c -- Clear screen
```
## Parser Implementation
The parser uses a hand-written recursive descent approach instead of go-yacc for:
- Better control over error messages
- Easier to extend and maintain
- No code generation step required
The parser structure follows the grammar defined in the Python version, ensuring full syntax compatibility.

318
internal/cli/benchmark.go Normal file
View File

@ -0,0 +1,318 @@
//
// 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 (
"fmt"
"strings"
"sync"
"time"
)
// BenchmarkResult holds the result of a benchmark run
type BenchmarkResult struct {
Duration float64
TotalCommands int
SuccessCount int
FailureCount int
QPS float64
ResponseList []*Response
}
// RunBenchmark runs a benchmark with the given concurrency and iterations
func (c *RAGFlowClient) RunBenchmark(cmd *Command) error {
concurrency, ok := cmd.Params["concurrency"].(int)
if !ok {
concurrency = 1
}
iterations, ok := cmd.Params["iterations"].(int)
if !ok {
iterations = 1
}
nestedCmd, ok := cmd.Params["command"].(*Command)
if !ok {
return fmt.Errorf("benchmark command not found")
}
if concurrency < 1 {
return fmt.Errorf("concurrency must be greater than 0")
}
// Add iterations to the nested command
nestedCmd.Params["iterations"] = iterations
if concurrency == 1 {
return c.runBenchmarkSingle(concurrency, iterations, nestedCmd)
}
return c.runBenchmarkConcurrent(concurrency, iterations, nestedCmd)
}
// runBenchmarkSingle runs benchmark with single concurrency (sequential execution)
func (c *RAGFlowClient) runBenchmarkSingle(concurrency, iterations int, nestedCmd *Command) error {
commandType := nestedCmd.Type
startTime := time.Now()
responseList := make([]*Response, 0, iterations)
// For search_on_datasets, convert dataset names to IDs first
if commandType == "search_on_datasets" && iterations > 1 {
datasets, _ := nestedCmd.Params["datasets"].(string)
datasetNames := strings.Split(datasets, ",")
datasetIDs := make([]string, 0, len(datasetNames))
for _, name := range datasetNames {
name = strings.TrimSpace(name)
id, err := c.getDatasetID(name)
if err != nil {
return err
}
datasetIDs = append(datasetIDs, id)
}
nestedCmd.Params["dataset_ids"] = datasetIDs
}
// Check if command supports native benchmark (iterations > 1)
supportsNative := false
if iterations > 1 {
result, err := c.ExecuteCommand(nestedCmd)
if err == nil && result != nil {
// Command supports benchmark natively
supportsNative = true
duration, _ := result["duration"].(float64)
respList, _ := result["response_list"].([]*Response)
responseList = respList
// Calculate and print results
successCount := 0
for _, resp := range responseList {
if isSuccess(resp, commandType) {
successCount++
}
}
qps := float64(0)
if duration > 0 {
qps = float64(iterations) / duration
}
fmt.Printf("command: %s, Concurrency: %d, iterations: %d\n", commandType, concurrency, iterations)
fmt.Printf("total duration: %.4fs, QPS: %.2f, COMMAND_COUNT: %d, SUCCESS: %d, FAILURE: %d\n",
duration, qps, iterations, successCount, iterations-successCount)
return nil
}
}
// Manual execution: run iterations times
if !supportsNative {
// Remove iterations param to avoid native benchmark
delete(nestedCmd.Params, "iterations")
for i := 0; i < iterations; i++ {
singleResult, err := c.ExecuteCommand(nestedCmd)
if err != nil {
// Command failed, add a failed response
responseList = append(responseList, &Response{StatusCode: 0})
continue
}
// For commands that return a single response (like ping with iterations=1)
if singleResult != nil {
if respList, ok := singleResult["response_list"].([]*Response); ok {
responseList = append(responseList, respList...)
}
} else {
// Command executed successfully but returned no data
// Mark as success for now
responseList = append(responseList, &Response{StatusCode: 200, Body: []byte("pong")})
}
}
}
duration := time.Since(startTime).Seconds()
successCount := 0
for _, resp := range responseList {
if isSuccess(resp, commandType) {
successCount++
}
}
qps := float64(0)
if duration > 0 {
qps = float64(iterations) / duration
}
// Print results
fmt.Printf("command: %s, Concurrency: %d, iterations: %d\n", commandType, concurrency, iterations)
fmt.Printf("total duration: %.4fs, QPS: %.2f, COMMAND_COUNT: %d, SUCCESS: %d, FAILURE: %d\n",
duration, qps, iterations, successCount, iterations-successCount)
return nil
}
// runBenchmarkConcurrent runs benchmark with multiple concurrent workers
func (c *RAGFlowClient) runBenchmarkConcurrent(concurrency, iterations int, nestedCmd *Command) error {
results := make([]map[string]interface{}, concurrency)
var wg sync.WaitGroup
// For search_on_datasets, convert dataset names to IDs first
if nestedCmd.Type == "search_on_datasets" {
datasets, _ := nestedCmd.Params["datasets"].(string)
datasetNames := strings.Split(datasets, ",")
datasetIDs := make([]string, 0, len(datasetNames))
for _, name := range datasetNames {
name = strings.TrimSpace(name)
id, err := c.getDatasetID(name)
if err != nil {
return err
}
datasetIDs = append(datasetIDs, id)
}
nestedCmd.Params["dataset_ids"] = datasetIDs
}
startTime := time.Now()
// Launch concurrent workers
for i := 0; i < concurrency; i++ {
wg.Add(1)
go func(idx int) {
defer wg.Done()
// Create a new client for each goroutine to avoid race conditions
workerClient := NewRAGFlowClient(c.ServerType)
workerClient.HTTPClient = c.HTTPClient // Share the same HTTP client config
// Execute benchmark silently (no output)
responseList := workerClient.executeBenchmarkSilent(nestedCmd, iterations)
results[idx] = map[string]interface{}{
"duration": 0.0,
"response_list": responseList,
}
}(i)
}
wg.Wait()
endTime := time.Now()
totalDuration := endTime.Sub(startTime).Seconds()
successCount := 0
commandType := nestedCmd.Type
for _, result := range results {
if result == nil {
continue
}
responseList, _ := result["response_list"].([]*Response)
for _, resp := range responseList {
if isSuccess(resp, commandType) {
successCount++
}
}
}
totalCommands := iterations * concurrency
qps := float64(0)
if totalDuration > 0 {
qps = float64(totalCommands) / totalDuration
}
// Print results
fmt.Printf("command: %s, Concurrency: %d, iterations: %d\n", commandType, concurrency, iterations)
fmt.Printf("total duration: %.4fs, QPS: %.2f, COMMAND_COUNT: %d, SUCCESS: %d, FAILURE: %d\n",
totalDuration, qps, totalCommands, successCount, totalCommands-successCount)
return nil
}
// executeBenchmarkSilent executes a command for benchmark without printing output
func (c *RAGFlowClient) executeBenchmarkSilent(cmd *Command, iterations int) []*Response {
responseList := make([]*Response, 0, iterations)
for i := 0; i < iterations; i++ {
var resp *Response
var err error
switch cmd.Type {
case "ping_server":
resp, err = c.HTTPClient.Request("GET", "/system/ping", false, "web", nil, nil)
case "list_user_datasets":
resp, err = c.HTTPClient.Request("POST", "/kb/list", false, "web", nil, nil)
case "list_datasets":
userName, _ := cmd.Params["user_name"].(string)
resp, err = c.HTTPClient.Request("GET", fmt.Sprintf("/admin/users/%s/datasets", userName), true, "admin", nil, nil)
case "search_on_datasets":
question, _ := cmd.Params["question"].(string)
datasetIDs, _ := cmd.Params["dataset_ids"].([]string)
payload := map[string]interface{}{
"kb_id": datasetIDs,
"question": question,
"similarity_threshold": 0.2,
"vector_similarity_weight": 0.3,
}
resp, err = c.HTTPClient.Request("POST", "/chunk/retrieval_test", false, "web", nil, payload)
default:
// For other commands, we would need to add specific handling
// For now, mark as failed
resp = &Response{StatusCode: 0}
}
if err != nil {
resp = &Response{StatusCode: 0}
}
responseList = append(responseList, resp)
}
return responseList
}
// isSuccess checks if a response is successful based on command type
func isSuccess(resp *Response, commandType string) bool {
if resp == nil {
return false
}
switch commandType {
case "ping_server":
return resp.StatusCode == 200 && string(resp.Body) == "pong"
case "list_user_datasets", "list_datasets", "search_on_datasets":
// Check status code and JSON response code for dataset commands
if resp.StatusCode != 200 {
return false
}
resJSON, err := resp.JSON()
if err != nil {
return false
}
code, ok := resJSON["code"].(float64)
return ok && code == 0
default:
// For other commands, check status code and response code
if resp.StatusCode != 200 {
return false
}
resJSON, err := resp.JSON()
if err != nil {
return false
}
code, ok := resJSON["code"].(float64)
return ok && code == 0
}
}

140
internal/cli/cli.go Normal file
View File

@ -0,0 +1,140 @@
//
// 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 (
"bufio"
"fmt"
"os"
"strings"
)
// CLI represents the command line interface
type CLI struct {
parser *Parser
client *RAGFlowClient
prompt string
running bool
}
// NewCLI creates a new CLI instance
func NewCLI() (*CLI, error) {
return &CLI{
prompt: "RAGFlow> ",
client: NewRAGFlowClient("user"), // Default to user mode
}, nil
}
// Run starts the interactive CLI
func (c *CLI) Run() error {
c.running = true
scanner := bufio.NewScanner(os.Stdin)
fmt.Println("Welcome to RAGFlow CLI")
fmt.Println("Type \\? for help, \\q to quit")
fmt.Println()
for c.running {
fmt.Print(c.prompt)
if !scanner.Scan() {
break
}
input := scanner.Text()
input = strings.TrimSpace(input)
if input == "" {
continue
}
if err := c.execute(input); err != nil {
fmt.Printf("Error: %v\n", err)
}
}
return scanner.Err()
}
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
_, err = c.client.ExecuteCommand(cmd)
return err
}
func (c *CLI) handleMetaCommand(cmd *Command) error {
command := cmd.Params["command"].(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")
default:
return fmt.Errorf("unknown meta command: \\%s", command)
}
return nil
}
func (c *CLI) printHelp() {
help := `
RAGFlow CLI Help
================
SQL Commands:
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
LIST USERS; - List all users
LIST SERVICES; - List services
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...")
}

496
internal/cli/client.go Normal file
View File

@ -0,0 +1,496 @@
//
// 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 (
"bufio"
"fmt"
"os"
"os/exec"
"strings"
"syscall"
"unsafe"
)
// RAGFlowClient handles API interactions with the RAGFlow server
type RAGFlowClient struct {
HTTPClient *HTTPClient
ServerType string // "admin" or "user"
}
// NewRAGFlowClient creates a new RAGFlow client
func NewRAGFlowClient(serverType string) *RAGFlowClient {
return &RAGFlowClient{
HTTPClient: NewHTTPClient(),
ServerType: serverType,
}
}
// LoginUser performs user login
func (c *RAGFlowClient) LoginUser(cmd *Command) error {
// First, ping the server to check if it's available
resp, err := c.HTTPClient.Request("GET", "/system/ping", false, "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" {
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")
}
// Get password from user input (hidden)
fmt.Printf("password for %s: ", email)
password, err := readPassword()
if err != nil {
return fmt.Errorf("failed to read password: %w", err)
}
password = strings.TrimSpace(password)
// Login
token, err := c.loginUser(email, 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", email)
return nil
}
// loginUser performs the actual login request
func (c *RAGFlowClient) loginUser(email, password string) (string, error) {
// Encrypt password using scrypt (same as Python implementation)
encryptedPassword, err := EncryptPassword(password)
if err != nil {
return "", fmt.Errorf("failed to encrypt password: %w", err)
}
payload := map[string]interface{}{
"email": email,
"password": encryptedPassword,
}
var path string
if c.ServerType == "admin" {
path = "/admin/login"
} else {
path = "/user/login"
}
resp, err := c.HTTPClient.Request("POST", path, c.ServerType == "admin", "", nil, payload)
if err != nil {
return "", err
}
resJSON, err := resp.JSON()
if err != nil {
return "", fmt.Errorf("login failed: invalid JSON response (%w)", err)
}
code, ok := resJSON["code"].(float64)
if !ok || code != 0 {
msg, _ := resJSON["message"].(string)
return "", fmt.Errorf("login failed: %s", msg)
}
token := resp.Headers.Get("Authorization")
if token == "" {
return "", fmt.Errorf("login failed: missing Authorization header")
}
return token, nil
}
// PingServer pings the server to check if it's alive
// Returns benchmark result map if iterations > 1, otherwise prints status
func (c *RAGFlowClient) PingServer(cmd *Command) (map[string]interface{}, error) {
// Get iterations from command params (for benchmark)
iterations := 1
if val, ok := cmd.Params["iterations"].(int); ok && val > 1 {
iterations = val
}
if iterations > 1 {
// Benchmark mode: multiple iterations
result, err := c.HTTPClient.RequestWithIterations("GET", "/system/ping", false, "web", nil, nil, iterations)
if err != nil {
return nil, err
}
return result, nil
}
// Single ping mode
resp, err := c.HTTPClient.Request("GET", "/system/ping", false, "web", nil, nil)
if err != nil {
fmt.Printf("Error: %v\n", err)
fmt.Println("Server is down")
return nil, err
}
if resp.StatusCode == 200 && string(resp.Body) == "pong" {
fmt.Println("Server is alive")
} else {
fmt.Printf("Error: %d\n", resp.StatusCode)
}
return nil, nil
}
// ListUserDatasets lists datasets for current user (user mode)
// Returns (result_map, error) - result_map is non-nil for benchmark mode
func (c *RAGFlowClient) ListUserDatasets(cmd *Command) (map[string]interface{}, error) {
if c.ServerType != "user" {
return nil, fmt.Errorf("this command is only allowed in USER 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("POST", "/kb/list", false, "web", nil, nil, iterations)
}
// Normal mode
resp, err := c.HTTPClient.Request("POST", "/kb/list", false, "web", nil, nil)
if err != nil {
return nil, fmt.Errorf("failed to list datasets: %w", err)
}
if resp.StatusCode != 200 {
return nil, fmt.Errorf("failed to list datasets: 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 datasets: %s", msg)
}
data, ok := resJSON["data"].(map[string]interface{})
if !ok {
return nil, fmt.Errorf("invalid response format")
}
kbs, ok := data["kbs"].([]interface{})
if !ok {
return nil, fmt.Errorf("invalid response format: kbs not found")
}
// Convert to slice of maps
tableData := make([]map[string]interface{}, 0, len(kbs))
for _, kb := range kbs {
if kbMap, ok := kb.(map[string]interface{}); ok {
// Remove avatar field
delete(kbMap, "avatar")
tableData = append(tableData, kbMap)
}
}
PrintTableSimple(tableData)
return nil, nil
}
// ListDatasets lists datasets for a specific user (admin mode)
// Returns (result_map, error) - result_map is non-nil for benchmark mode
func (c *RAGFlowClient) ListDatasets(cmd *Command) (map[string]interface{}, error) {
if c.ServerType != "admin" {
return nil, fmt.Errorf("this command is only allowed in ADMIN mode")
}
userName, ok := cmd.Params["user_name"].(string)
if !ok {
return nil, fmt.Errorf("user_name not provided")
}
// 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", fmt.Sprintf("/admin/users/%s/datasets", userName), true, "admin", nil, nil, iterations)
}
fmt.Printf("Listing all datasets of user: %s\n", userName)
resp, err := c.HTTPClient.Request("GET", fmt.Sprintf("/admin/users/%s/datasets", userName), true, "admin", nil, nil)
if err != nil {
return nil, fmt.Errorf("failed to list datasets: %w", err)
}
if resp.StatusCode != 200 {
return nil, fmt.Errorf("failed to list datasets: 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)
}
data, ok := resJSON["data"].([]interface{})
if !ok {
return nil, fmt.Errorf("invalid response format")
}
// Convert to slice of maps and remove avatar
tableData := make([]map[string]interface{}, 0, len(data))
for _, item := range data {
if itemMap, ok := item.(map[string]interface{}); ok {
delete(itemMap, "avatar")
tableData = append(tableData, itemMap)
}
}
PrintTableSimple(tableData)
return nil, nil
}
// readPassword reads password from terminal without echoing
func readPassword() (string, error) {
// Check if stdin is a terminal by trying to get terminal size
if isTerminal() {
// Use stty to disable echo
cmd := exec.Command("stty", "-echo")
cmd.Stdin = os.Stdin
if err := cmd.Run(); err != nil {
// Fallback: read normally
return readPasswordFallback()
}
defer func() {
// Re-enable echo
cmd := exec.Command("stty", "echo")
cmd.Stdin = os.Stdin
cmd.Run()
}()
reader := bufio.NewReader(os.Stdin)
password, err := reader.ReadString('\n')
fmt.Println() // New line after password input
if err != nil {
return "", err
}
return strings.TrimSpace(password), nil
}
// Fallback for non-terminal input (e.g., piped input)
return readPasswordFallback()
}
// isTerminal checks if stdin is a terminal
func isTerminal() bool {
var termios syscall.Termios
_, _, err := syscall.Syscall6(syscall.SYS_IOCTL, os.Stdin.Fd(), syscall.TCGETS, uintptr(unsafe.Pointer(&termios)), 0, 0, 0)
return err == 0
}
// readPasswordFallback reads password as plain text (fallback mode)
func readPasswordFallback() (string, error) {
reader := bufio.NewReader(os.Stdin)
password, err := reader.ReadString('\n')
if err != nil {
return "", err
}
return strings.TrimSpace(password), nil
}
// getDatasetID gets dataset ID by name
func (c *RAGFlowClient) getDatasetID(datasetName string) (string, error) {
resp, err := c.HTTPClient.Request("POST", "/kb/list", false, "web", nil, nil)
if err != nil {
return "", fmt.Errorf("failed to list datasets: %w", err)
}
if resp.StatusCode != 200 {
return "", fmt.Errorf("failed to list datasets: HTTP %d", resp.StatusCode)
}
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 list datasets: %s", msg)
}
data, ok := resJSON["data"].(map[string]interface{})
if !ok {
return "", fmt.Errorf("invalid response format")
}
kbs, ok := data["kbs"].([]interface{})
if !ok {
return "", fmt.Errorf("invalid response format: kbs not found")
}
for _, kb := range kbs {
if kbMap, ok := kb.(map[string]interface{}); ok {
if name, _ := kbMap["name"].(string); name == datasetName {
if id, _ := kbMap["id"].(string); id != "" {
return id, nil
}
}
}
}
return "", fmt.Errorf("dataset '%s' not found", datasetName)
}
// SearchOnDatasets searches for chunks in specified datasets
// Returns (result_map, error) - result_map is non-nil for benchmark mode
func (c *RAGFlowClient) SearchOnDatasets(cmd *Command) (map[string]interface{}, error) {
if c.ServerType != "user" {
return nil, fmt.Errorf("this command is only allowed in USER mode")
}
question, ok := cmd.Params["question"].(string)
if !ok {
return nil, fmt.Errorf("question not provided")
}
datasets, ok := cmd.Params["datasets"].(string)
if !ok {
return nil, fmt.Errorf("datasets not provided")
}
// Parse dataset names (comma-separated) and convert to IDs
datasetNames := strings.Split(datasets, ",")
datasetIDs := make([]string, 0, len(datasetNames))
for _, name := range datasetNames {
name = strings.TrimSpace(name)
id, err := c.getDatasetID(name)
if err != nil {
return nil, err
}
datasetIDs = append(datasetIDs, id)
}
// Check for benchmark iterations
iterations := 1
if val, ok := cmd.Params["iterations"].(int); ok && val > 1 {
iterations = val
}
payload := map[string]interface{}{
"kb_id": datasetIDs,
"question": question,
"similarity_threshold": 0.2,
"vector_similarity_weight": 0.3,
}
if iterations > 1 {
// Benchmark mode - return raw result for benchmark stats
return c.HTTPClient.RequestWithIterations("POST", "/chunk/retrieval_test", false, "web", nil, payload, iterations)
}
// Normal mode
resp, err := c.HTTPClient.Request("POST", "/chunk/retrieval_test", false, "web", nil, payload)
if err != nil {
return nil, fmt.Errorf("failed to search on datasets: %w", err)
}
if resp.StatusCode != 200 {
return nil, fmt.Errorf("failed to search on datasets: 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 search on datasets: %s", msg)
}
data, ok := resJSON["data"].(map[string]interface{})
if !ok {
return nil, fmt.Errorf("invalid response format")
}
chunks, ok := data["chunks"].([]interface{})
if !ok {
return nil, fmt.Errorf("invalid response format: chunks not found")
}
// Convert to slice of maps for printing
tableData := make([]map[string]interface{}, 0, len(chunks))
for _, chunk := range chunks {
if chunkMap, ok := chunk.(map[string]interface{}); ok {
row := map[string]interface{}{
"id": chunkMap["chunk_id"],
"content": chunkMap["content_with_weight"],
"document_id": chunkMap["doc_id"],
"dataset_id": chunkMap["kb_id"],
"docnm_kwd": chunkMap["docnm_kwd"],
"image_id": chunkMap["image_id"],
"similarity": chunkMap["similarity"],
"term_similarity": chunkMap["term_similarity"],
"vector_similarity": chunkMap["vector_similarity"],
}
tableData = append(tableData, row)
}
}
PrintTableSimple(tableData)
return nil, nil
}
// ExecuteCommand executes a parsed command
// Returns benchmark result map for commands that support it (e.g., ping_server with iterations > 1)
func (c *RAGFlowClient) ExecuteCommand(cmd *Command) (map[string]interface{}, error) {
switch cmd.Type {
case "login_user":
return nil, c.LoginUser(cmd)
case "ping_server":
return c.PingServer(cmd)
case "benchmark":
return nil, c.RunBenchmark(cmd)
case "list_user_datasets":
return c.ListUserDatasets(cmd)
case "list_datasets":
return c.ListDatasets(cmd)
case "search_on_datasets":
return c.SearchOnDatasets(cmd)
// TODO: Implement other commands
default:
return nil, fmt.Errorf("command '%s' would be executed with API", cmd.Type)
}
}

106
internal/cli/crypt.go Normal file
View File

@ -0,0 +1,106 @@
//
// 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 (
"crypto/rand"
"crypto/rsa"
"crypto/x509"
"encoding/base64"
"encoding/pem"
"fmt"
"os"
"path/filepath"
)
// EncryptPassword encrypts a password using RSA public key
// This matches the Python implementation in api/utils/crypt.py
func EncryptPassword(password string) (string, error) {
// Read public key from conf/public.pem
publicKeyPath := filepath.Join(getProjectBaseDirectory(), "conf", "public.pem")
publicKeyPEM, err := os.ReadFile(publicKeyPath)
if err != nil {
return "", fmt.Errorf("failed to read public key: %w", err)
}
// Parse public key
block, _ := pem.Decode(publicKeyPEM)
if block == nil {
return "", fmt.Errorf("failed to parse public key PEM")
}
pub, err := x509.ParsePKIXPublicKey(block.Bytes)
if err != nil {
// Try parsing as PKCS1
pub, err = x509.ParsePKCS1PublicKey(block.Bytes)
if err != nil {
return "", fmt.Errorf("failed to parse public key: %w", err)
}
}
rsaPub, ok := pub.(*rsa.PublicKey)
if !ok {
return "", fmt.Errorf("not an RSA public key")
}
// Step 1: Base64 encode the password
passwordBase64 := base64.StdEncoding.EncodeToString([]byte(password))
// Step 2: Encrypt using RSA PKCS1v15
encrypted, err := rsa.EncryptPKCS1v15(rand.Reader, rsaPub, []byte(passwordBase64))
if err != nil {
return "", fmt.Errorf("failed to encrypt password: %w", err)
}
// Step 3: Base64 encode the encrypted data
return base64.StdEncoding.EncodeToString(encrypted), nil
}
// getProjectBaseDirectory returns the project base directory
func getProjectBaseDirectory() string {
// Try to find the project root by looking for go.mod or conf directory
// Start from current working directory and go up
cwd, err := os.Getwd()
if err != nil {
return "."
}
dir := cwd
for {
// Check if conf directory exists
confDir := filepath.Join(dir, "conf")
if info, err := os.Stat(confDir); err == nil && info.IsDir() {
return dir
}
// Check for go.mod
goMod := filepath.Join(dir, "go.mod")
if _, err := os.Stat(goMod); err == nil {
return dir
}
// Go up one directory
parent := filepath.Dir(dir)
if parent == dir {
// Reached root
break
}
dir = parent
}
return cwd
}

248
internal/cli/http_client.go Normal file
View File

@ -0,0 +1,248 @@
//
// 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 (
"bytes"
"crypto/tls"
"encoding/json"
"fmt"
"io"
"net/http"
"time"
)
// HTTPClient handles HTTP requests to the RAGFlow server
type HTTPClient struct {
Host string
Port int
APIVersion string
APIKey string
LoginToken string
ConnectTimeout time.Duration
ReadTimeout time.Duration
VerifySSL bool
client *http.Client
}
// NewHTTPClient creates a new HTTP client
func NewHTTPClient() *HTTPClient {
transport := &http.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
}
return &HTTPClient{
Host: "127.0.0.1",
Port: 9382,
APIVersion: "v1",
ConnectTimeout: 5 * time.Second,
ReadTimeout: 60 * time.Second,
VerifySSL: false,
client: &http.Client{
Transport: transport,
Timeout: 60 * time.Second,
},
}
}
// APIBase returns the API base URL
func (c *HTTPClient) APIBase() string {
return fmt.Sprintf("%s:%d/api/%s", c.Host, c.Port, c.APIVersion)
}
// NonAPIBase returns the non-API base URL
func (c *HTTPClient) NonAPIBase() string {
return fmt.Sprintf("%s:%d/%s", c.Host, c.Port, c.APIVersion)
}
// BuildURL builds the full URL for a given path
func (c *HTTPClient) BuildURL(path string, useAPIBase bool) string {
base := c.APIBase()
if !useAPIBase {
base = c.NonAPIBase()
}
if c.VerifySSL {
return fmt.Sprintf("https://%s%s", base, path)
}
return fmt.Sprintf("http://%s%s", base, path)
}
// Headers builds the request headers
func (c *HTTPClient) Headers(authKind string, extra map[string]string) map[string]string {
headers := make(map[string]string)
switch authKind {
case "api":
if c.APIKey != "" {
headers["Authorization"] = fmt.Sprintf("Bearer %s", c.APIKey)
}
case "web", "admin":
if c.LoginToken != "" {
headers["Authorization"] = c.LoginToken
}
}
for k, v := range extra {
headers[k] = v
}
return headers
}
// Response represents an HTTP response
type Response struct {
StatusCode int
Body []byte
Headers http.Header
}
// JSON parses the response body as JSON
func (r *Response) JSON() (map[string]interface{}, error) {
var result map[string]interface{}
if err := json.Unmarshal(r.Body, &result); err != nil {
return nil, err
}
return result, nil
}
// Request makes an HTTP request
func (c *HTTPClient) Request(method, path string, useAPIBase bool, authKind string, headers map[string]string, jsonBody map[string]interface{}) (*Response, error) {
url := c.BuildURL(path, useAPIBase)
mergedHeaders := c.Headers(authKind, headers)
var body io.Reader
if jsonBody != nil {
jsonData, err := json.Marshal(jsonBody)
if err != nil {
return nil, err
}
body = bytes.NewReader(jsonData)
if mergedHeaders == nil {
mergedHeaders = make(map[string]string)
}
mergedHeaders["Content-Type"] = "application/json"
}
req, err := http.NewRequest(method, url, body)
if err != nil {
return nil, err
}
for k, v := range mergedHeaders {
req.Header.Set(k, v)
}
resp, err := c.client.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
respBody, err := io.ReadAll(resp.Body)
if err != nil {
return nil, err
}
return &Response{
StatusCode: resp.StatusCode,
Body: respBody,
Headers: resp.Header.Clone(),
}, nil
}
// RequestWithIterations makes multiple HTTP requests for benchmarking
// Returns a map with "duration" (total time in seconds) and "response_list"
func (c *HTTPClient) RequestWithIterations(method, path string, useAPIBase bool, authKind string, headers map[string]string, jsonBody map[string]interface{}, iterations int) (map[string]interface{}, error) {
if iterations <= 1 {
resp, err := c.Request(method, path, useAPIBase, authKind, headers, jsonBody)
if err != nil {
return nil, err
}
return map[string]interface{}{
"duration": 0.0,
"response_list": []*Response{resp},
}, nil
}
url := c.BuildURL(path, useAPIBase)
mergedHeaders := c.Headers(authKind, headers)
var body io.Reader
if jsonBody != nil {
jsonData, err := json.Marshal(jsonBody)
if err != nil {
return nil, err
}
body = bytes.NewReader(jsonData)
if mergedHeaders == nil {
mergedHeaders = make(map[string]string)
}
mergedHeaders["Content-Type"] = "application/json"
}
responseList := make([]*Response, 0, iterations)
var totalDuration float64
for i := 0; i < iterations; i++ {
start := time.Now()
var reqBody io.Reader
if body != nil {
// Need to create a new reader for each request
jsonData, _ := json.Marshal(jsonBody)
reqBody = bytes.NewReader(jsonData)
}
req, err := http.NewRequest(method, url, reqBody)
if err != nil {
return nil, err
}
for k, v := range mergedHeaders {
req.Header.Set(k, v)
}
resp, err := c.client.Do(req)
if err != nil {
return nil, err
}
respBody, err := io.ReadAll(resp.Body)
resp.Body.Close()
if err != nil {
return nil, err
}
responseList = append(responseList, &Response{
StatusCode: resp.StatusCode,
Body: respBody,
Headers: resp.Header.Clone(),
})
totalDuration += time.Since(start).Seconds()
}
return map[string]interface{}{
"duration": totalDuration,
"response_list": responseList,
}, nil
}
// RequestJSON makes an HTTP request and returns JSON response
func (c *HTTPClient) RequestJSON(method, path string, useAPIBase bool, authKind string, headers map[string]string, jsonBody map[string]interface{}) (map[string]interface{}, error) {
resp, err := c.Request(method, path, useAPIBase, authKind, headers, jsonBody)
if err != nil {
return nil, err
}
return resp.JSON()
}

301
internal/cli/lexer.go Normal file
View File

@ -0,0 +1,301 @@
//
// 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 (
"strings"
"unicode"
)
// Lexer performs lexical analysis of the input
type Lexer struct {
input string
pos int
readPos int
ch byte
}
// NewLexer creates a new lexer for the given input
func NewLexer(input string) *Lexer {
l := &Lexer{input: input}
l.readChar()
return l
}
func (l *Lexer) readChar() {
if l.readPos >= len(l.input) {
l.ch = 0
} else {
l.ch = l.input[l.readPos]
}
l.pos = l.readPos
l.readPos++
}
func (l *Lexer) peekChar() byte {
if l.readPos >= len(l.input) {
return 0
}
return l.input[l.readPos]
}
func (l *Lexer) skipWhitespace() {
for l.ch == ' ' || l.ch == '\t' || l.ch == '\n' || l.ch == '\r' {
l.readChar()
}
}
// NextToken returns the next token from the input
func (l *Lexer) NextToken() Token {
var tok Token
l.skipWhitespace()
switch l.ch {
case ';':
tok = newToken(TokenSemicolon, l.ch)
l.readChar()
case ',':
tok = newToken(TokenComma, l.ch)
l.readChar()
case '\'':
tok.Type = TokenQuotedString
tok.Value = l.readQuotedString('\'')
case '"':
tok.Type = TokenQuotedString
tok.Value = l.readQuotedString('"')
case '\\':
// Meta command: backslash followed by command name
tok.Type = TokenIdentifier
tok.Value = l.readMetaCommand()
case 0:
tok.Type = TokenEOF
tok.Value = ""
default:
if isLetter(l.ch) {
ident := l.readIdentifier()
return l.lookupIdent(ident)
} else if isDigit(l.ch) {
tok.Type = TokenNumber
tok.Value = l.readNumber()
return tok
} else {
tok = newToken(TokenIllegal, l.ch)
l.readChar()
}
}
return tok
}
func (l *Lexer) readMetaCommand() string {
start := l.pos
l.readChar() // consume backslash
for isLetter(l.ch) || l.ch == '?' {
l.readChar()
}
return l.input[start:l.pos]
}
func newToken(tokenType int, ch byte) Token {
return Token{Type: tokenType, Value: string(ch)}
}
func (l *Lexer) readIdentifier() string {
start := l.pos
for isLetter(l.ch) || isDigit(l.ch) || l.ch == '_' || l.ch == '-' || l.ch == '.' {
l.readChar()
}
return l.input[start:l.pos]
}
func (l *Lexer) readNumber() string {
start := l.pos
for isDigit(l.ch) {
l.readChar()
}
return l.input[start:l.pos]
}
func (l *Lexer) readQuotedString(quote byte) string {
l.readChar() // skip opening quote
start := l.pos
for l.ch != quote && l.ch != 0 {
l.readChar()
}
str := l.input[start:l.pos]
if l.ch == quote {
l.readChar() // skip closing quote
}
return str
}
func (l *Lexer) lookupIdent(ident string) Token {
upper := strings.ToUpper(ident)
switch upper {
case "LOGIN":
return Token{Type: TokenLogin, Value: ident}
case "REGISTER":
return Token{Type: TokenRegister, Value: ident}
case "LIST":
return Token{Type: TokenList, Value: ident}
case "SERVICES":
return Token{Type: TokenServices, Value: ident}
case "SHOW":
return Token{Type: TokenShow, Value: ident}
case "CREATE":
return Token{Type: TokenCreate, Value: ident}
case "SERVICE":
return Token{Type: TokenService, Value: ident}
case "SHUTDOWN":
return Token{Type: TokenShutdown, Value: ident}
case "STARTUP":
return Token{Type: TokenStartup, Value: ident}
case "RESTART":
return Token{Type: TokenRestart, Value: ident}
case "USERS":
return Token{Type: TokenUsers, Value: ident}
case "DROP":
return Token{Type: TokenDrop, Value: ident}
case "USER":
return Token{Type: TokenUser, Value: ident}
case "ALTER":
return Token{Type: TokenAlter, Value: ident}
case "ACTIVE":
return Token{Type: TokenActive, Value: ident}
case "ADMIN":
return Token{Type: TokenAdmin, Value: ident}
case "PASSWORD":
return Token{Type: TokenPassword, Value: ident}
case "DATASET":
return Token{Type: TokenDataset, Value: ident}
case "DATASETS":
return Token{Type: TokenDatasets, Value: ident}
case "OF":
return Token{Type: TokenOf, Value: ident}
case "AGENTS":
return Token{Type: TokenAgents, Value: ident}
case "ROLE":
return Token{Type: TokenRole, Value: ident}
case "ROLES":
return Token{Type: TokenRoles, Value: ident}
case "DESCRIPTION":
return Token{Type: TokenDescription, Value: ident}
case "GRANT":
return Token{Type: TokenGrant, Value: ident}
case "REVOKE":
return Token{Type: TokenRevoke, Value: ident}
case "ALL":
return Token{Type: TokenAll, Value: ident}
case "PERMISSION":
return Token{Type: TokenPermission, Value: ident}
case "TO":
return Token{Type: TokenTo, Value: ident}
case "FROM":
return Token{Type: TokenFrom, Value: ident}
case "FOR":
return Token{Type: TokenFor, Value: ident}
case "RESOURCES":
return Token{Type: TokenResources, Value: ident}
case "ON":
return Token{Type: TokenOn, Value: ident}
case "SET":
return Token{Type: TokenSet, Value: ident}
case "RESET":
return Token{Type: TokenReset, Value: ident}
case "VERSION":
return Token{Type: TokenVersion, Value: ident}
case "VAR":
return Token{Type: TokenVar, Value: ident}
case "VARS":
return Token{Type: TokenVars, Value: ident}
case "CONFIGS":
return Token{Type: TokenConfigs, Value: ident}
case "ENVS":
return Token{Type: TokenEnvs, Value: ident}
case "KEY":
return Token{Type: TokenKey, Value: ident}
case "KEYS":
return Token{Type: TokenKeys, Value: ident}
case "GENERATE":
return Token{Type: TokenGenerate, Value: ident}
case "MODEL":
return Token{Type: TokenModel, Value: ident}
case "MODELS":
return Token{Type: TokenModels, Value: ident}
case "PROVIDER":
return Token{Type: TokenProvider, Value: ident}
case "PROVIDERS":
return Token{Type: TokenProviders, Value: ident}
case "DEFAULT":
return Token{Type: TokenDefault, Value: ident}
case "CHATS":
return Token{Type: TokenChats, Value: ident}
case "CHAT":
return Token{Type: TokenChat, Value: ident}
case "FILES":
return Token{Type: TokenFiles, Value: ident}
case "AS":
return Token{Type: TokenAs, Value: ident}
case "PARSE":
return Token{Type: TokenParse, Value: ident}
case "IMPORT":
return Token{Type: TokenImport, Value: ident}
case "INTO":
return Token{Type: TokenInto, Value: ident}
case "WITH":
return Token{Type: TokenWith, Value: ident}
case "PARSER":
return Token{Type: TokenParser, Value: ident}
case "PIPELINE":
return Token{Type: TokenPipeline, Value: ident}
case "SEARCH":
return Token{Type: TokenSearch, Value: ident}
case "CURRENT":
return Token{Type: TokenCurrent, Value: ident}
case "LLM":
return Token{Type: TokenLLM, Value: ident}
case "VLM":
return Token{Type: TokenVLM, Value: ident}
case "EMBEDDING":
return Token{Type: TokenEmbedding, Value: ident}
case "RERANKER":
return Token{Type: TokenReranker, Value: ident}
case "ASR":
return Token{Type: TokenASR, Value: ident}
case "TTS":
return Token{Type: TokenTTS, Value: ident}
case "ASYNC":
return Token{Type: TokenAsync, Value: ident}
case "SYNC":
return Token{Type: TokenSync, Value: ident}
case "BENCHMARK":
return Token{Type: TokenBenchmark, Value: ident}
case "PING":
return Token{Type: TokenPing, Value: ident}
default:
return Token{Type: TokenIdentifier, Value: ident}
}
}
func isLetter(ch byte) bool {
return unicode.IsLetter(rune(ch))
}
func isDigit(ch byte) bool {
return unicode.IsDigit(rune(ch))
}

1568
internal/cli/parser.go Normal file

File diff suppressed because it is too large Load Diff

167
internal/cli/table.go Normal file
View File

@ -0,0 +1,167 @@
//
// 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 (
"fmt"
"strings"
"unicode"
)
// PrintTableSimple prints data in a simple table format
// Similar to Python's _print_table_simple
func PrintTableSimple(data []map[string]interface{}) {
if len(data) == 0 {
fmt.Println("No data to print")
return
}
// Collect all column names
columnSet := make(map[string]bool)
for _, item := range data {
for key := range item {
columnSet[key] = true
}
}
// Sort columns
columns := make([]string, 0, len(columnSet))
for col := range columnSet {
columns = append(columns, col)
}
// Simple sort - in production you might want specific column ordering
for i := 0; i < len(columns); i++ {
for j := i + 1; j < len(columns); j++ {
if columns[i] > columns[j] {
columns[i], columns[j] = columns[j], columns[i]
}
}
}
// Calculate column widths
colWidths := make(map[string]int)
for _, col := range columns {
maxWidth := getStringWidth(col)
for _, item := range data {
value := fmt.Sprintf("%v", item[col])
valueWidth := getStringWidth(value)
if valueWidth > maxWidth {
maxWidth = valueWidth
}
}
if maxWidth < 2 {
maxWidth = 2
}
colWidths[col] = maxWidth
}
// Generate separator
separatorParts := make([]string, 0, len(columns))
for _, col := range columns {
separatorParts = append(separatorParts, strings.Repeat("-", colWidths[col]+2))
}
separator := "+" + strings.Join(separatorParts, "+") + "+"
// Print header
fmt.Println(separator)
headerParts := make([]string, 0, len(columns))
for _, col := range columns {
headerParts = append(headerParts, fmt.Sprintf(" %-*s ", colWidths[col], col))
}
fmt.Println("|" + strings.Join(headerParts, "|") + "|")
fmt.Println(separator)
// Print data rows
for _, item := range data {
rowParts := make([]string, 0, len(columns))
for _, col := range columns {
value := fmt.Sprintf("%v", item[col])
valueWidth := getStringWidth(value)
// Truncate if too long
if valueWidth > colWidths[col] {
runes := []rune(value)
truncated := truncateString(runes, colWidths[col])
value = truncated
valueWidth = getStringWidth(value)
}
// Pad to column width
padding := colWidths[col] - valueWidth + len(value)
rowParts = append(rowParts, fmt.Sprintf(" %-*s ", padding, value))
}
fmt.Println("|" + strings.Join(rowParts, "|") + "|")
}
fmt.Println(separator)
}
// getStringWidth calculates the display width of a string
// Treats CJK characters as width 2
func getStringWidth(text string) int {
width := 0
for _, r := range text {
if isHalfWidth(r) {
width++
} else {
width += 2
}
}
return width
}
// isHalfWidth checks if a rune is half-width
func isHalfWidth(r rune) bool {
// ASCII printable characters and common whitespace
if r >= 0x20 && r <= 0x7E {
return true
}
if r == '\t' || r == '\n' || r == '\r' {
return true
}
return false
}
// truncateString truncates a string to fit within maxWidth display width
func truncateString(runes []rune, maxWidth int) string {
width := 0
for i, r := range runes {
if isHalfWidth(r) {
width++
} else {
width += 2
}
if width > maxWidth-3 {
return string(runes[:i]) + "..."
}
}
return string(runes)
}
// getMax returns the maximum of two integers
func getMax(a, b int) int {
if a > b {
return a
}
return b
}
// isWideChar checks if a character is wide (CJK, etc.)
func isWideChar(r rune) bool {
return unicode.Is(unicode.Han, r) ||
unicode.Is(unicode.Hiragana, r) ||
unicode.Is(unicode.Katakana, r) ||
unicode.Is(unicode.Hangul, r)
}

123
internal/cli/types.go Normal file
View File

@ -0,0 +1,123 @@
//
// 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
// Command represents a parsed command from the CLI
type Command struct {
Type string
Params map[string]interface{}
}
// Token types for the lexer
const (
// Keywords
TokenLogin = iota
TokenRegister
TokenList
TokenServices
TokenShow
TokenCreate
TokenService
TokenShutdown
TokenStartup
TokenRestart
TokenUsers
TokenDrop
TokenUser
TokenAlter
TokenActive
TokenAdmin
TokenPassword
TokenDataset
TokenDatasets
TokenOf
TokenAgents
TokenRole
TokenRoles
TokenDescription
TokenGrant
TokenRevoke
TokenAll
TokenPermission
TokenTo
TokenFrom
TokenFor
TokenResources
TokenOn
TokenSet
TokenReset
TokenVersion
TokenVar
TokenVars
TokenConfigs
TokenEnvs
TokenKey
TokenKeys
TokenGenerate
TokenModel
TokenModels
TokenProvider
TokenProviders
TokenDefault
TokenChats
TokenChat
TokenFiles
TokenAs
TokenParse
TokenImport
TokenInto
TokenWith
TokenParser
TokenPipeline
TokenSearch
TokenCurrent
TokenLLM
TokenVLM
TokenEmbedding
TokenReranker
TokenASR
TokenTTS
TokenAsync
TokenSync
TokenBenchmark
TokenPing
// Literals
TokenIdentifier
TokenQuotedString
TokenNumber
// Special
TokenSemicolon
TokenComma
TokenEOF
TokenIllegal
)
// Token represents a lexical token
type Token struct {
Type int
Value string
}
// NewCommand creates a new command with the given type
func NewCommand(cmdType string) *Command {
return &Command{
Type: cmdType,
Params: make(map[string]interface{}),
}
}