mirror of
https://github.com/infiniflow/ragflow.git
synced 2026-05-04 09:17:48 +08:00
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:
87
internal/cli/README.md
Normal file
87
internal/cli/README.md
Normal 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
318
internal/cli/benchmark.go
Normal 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
140
internal/cli/cli.go
Normal 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
496
internal/cli/client.go
Normal 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
106
internal/cli/crypt.go
Normal 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
248
internal/cli/http_client.go
Normal 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
301
internal/cli/lexer.go
Normal 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
1568
internal/cli/parser.go
Normal file
File diff suppressed because it is too large
Load Diff
167
internal/cli/table.go
Normal file
167
internal/cli/table.go
Normal 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
123
internal/cli/types.go
Normal 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{}),
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user