Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -251,3 +251,16 @@ LBCTL_TEST_ENDPOINT=http://localhost:5300 \
```

`make cross-build` 生成 macOS、Linux、Windows 的 amd64/arm64 二进制及 `dist/checksums.txt`。交叉构建成功不代表已经在对应系统完成实机验收。

## RAG 配置发现

这些命令需要 Core 声明对应 capability,并要求 `resource.view` 权限;可用 `-o json` 或 `-o yaml` 输出。

```sh
lbctl knowledge-engine list
lbctl knowledge-engine creation-schema author/engine -o yaml
lbctl knowledge-engine retrieval-schema author/engine -o json
lbctl knowledge-parser list --mime-type application/pdf
```

从引擎列表选择 `plugin_id`;creation schema 定义创建知识库的参数,retrieval schema 定义检索参数。随后使用已有 `knowledge-base create`、`ingest`、`retrieve` 命令完成入库和检索。引擎不存在、权限不足或服务端未声明 capability 时返回非零退出码。
5 changes: 5 additions & 0 deletions internal/api/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,11 @@ type TaskListFilters struct {
}

var capabilityOperationIDs = []string{
"knowledge_engine.list",
"knowledge_engine.creation_schema",
"knowledge_engine.retrieval_schema",
"knowledge_parser.list",

"bot.list",
"bot.get",
"bot.create",
Expand Down
10 changes: 9 additions & 1 deletion internal/api/operations.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,11 @@ type Operation struct {
// ResolveOperation 只接受已登记的路径,避免 api 子命令退化为任意 HTTP 代理。
func ResolveOperation(method, requestPath string) (Operation, bool) {
parsed, err := url.Parse(requestPath)
if err != nil {
return Operation{}, false
}
escapedPath := strings.ToLower(parsed.EscapedPath())
if err != nil || parsed.IsAbs() || parsed.Host != "" || parsed.Fragment != "" ||
if parsed.IsAbs() || parsed.Host != "" || parsed.Fragment != "" ||
strings.Contains(escapedPath, "%2f") || strings.Contains(escapedPath, "%5c") {
return Operation{}, false
}
Expand Down Expand Up @@ -50,6 +53,8 @@ func ResolveOperation(method, requestPath string) (Operation, bool) {
func validOperationQuery(operation string, query url.Values) bool {
allowed := map[string]bool{}
switch operation {
case "knowledge_parser.list":
allowed = map[string]bool{"mime_type": true}
case "task.list":
allowed = map[string]bool{"type": true, "kind": true}
case "plugin.logs", "mcp_server.logs":
Expand Down Expand Up @@ -82,6 +87,9 @@ func validOperationQuery(operation string, query url.Values) bool {
}

func resolveGetOperation(path string) (Operation, bool) {
if op, ok := resolveRAGOperation(path); ok {
return op, true
}
switch path {
case tasksPath:
return Operation{ID: "task.list", Method: http.MethodGet, Path: path, ReadOnly: true, Permission: "resource.view"}, true
Expand Down
22 changes: 22 additions & 0 deletions internal/api/rag.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package api

import (
"net/http"
)

func resolveRAGOperation(path string) (Operation, bool) {
id := ""
switch path {
case "/api/v1/knowledge/engines":
id = "knowledge_engine.list"
case "/api/v1/knowledge/parsers":
id = "knowledge_parser.list"
default:
for _, kind := range []string{"creation", "retrieval"} {
if nestedPath(path, "/api/v1/knowledge/engines", 2, kind+"-schema") {
id = "knowledge_engine." + kind + "_schema"
}
}
}
return Operation{ID: id, Method: http.MethodGet, Path: path, ReadOnly: true, Permission: "resource.view"}, id != ""
}
14 changes: 14 additions & 0 deletions internal/api/rag_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package api

import (
"net/http"
"testing"
)

func TestRAGPathsRejectMalformedPluginIDs(t *testing.T) {
for _, path := range []string{"%zz", "/api/v1/knowledge/engines/author/../creation-schema", "/api/v1/knowledge/engines/author%2Fname/creation-schema", "/api/v1/knowledge/parsers?unsupported=true"} {
if _, ok := ResolveOperation(http.MethodGet, path); ok {
t.Errorf("accepted %s", path)
}
}
}
1 change: 1 addition & 0 deletions internal/command/command.go
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,7 @@ func newRoot(deps Dependencies, flags *globalFlags, service *app.Service) *cobra
root.AddCommand(newModelCommand(service, deps, flags))
root.AddCommand(newTaskCommand(service, deps, flags))
root.AddCommand(newKnowledgeBaseCommand(service, deps, flags))
root.AddCommand(newKnowledgeEngineCommand(service, deps, flags), newKnowledgeParserCommand(service, deps, flags))
root.AddCommand(newPluginCommand(service, deps, flags))
root.AddCommand(newSkillCommand(service, deps, flags))
root.AddCommand(newMCPServerCommand(service, deps, flags))
Expand Down
41 changes: 41 additions & 0 deletions internal/command/rag.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package command

import (
"github.com/langbot-app/langbot-cli/internal/app"
"github.com/spf13/cobra"
"net/http"
"net/url"
)

func ragGet(cmd *cobra.Command, service *app.Service, deps Dependencies, flags *globalFlags, path string) error {
return emitCall(deps, flags, func() (app.Result, error) {
return service.APIRequest(cmd.Context(), http.MethodGet, path, nil, connectionOptions(flags))
})
}
func newKnowledgeEngineCommand(service *app.Service, deps Dependencies, flags *globalFlags) *cobra.Command {
root := &cobra.Command{Use: "knowledge-engine", Short: "查询知识引擎及配置 schema", Args: cobra.NoArgs}
root.AddCommand(&cobra.Command{Use: "list", Short: "列出知识引擎及能力", Args: cobra.NoArgs, RunE: func(cmd *cobra.Command, _ []string) error {
return ragGet(cmd, service, deps, flags, "/api/v1/knowledge/engines")
}})
for _, kind := range []string{"creation", "retrieval"} {
root.AddCommand(&cobra.Command{Use: kind + "-schema <author/name>", Short: "读取 " + kind + " schema", Args: cobra.ExactArgs(1), RunE: func(cmd *cobra.Command, args []string) error {
// The operation registry validates the two plugin ID segments.
return ragGet(cmd, service, deps, flags, "/api/v1/knowledge/engines/"+args[0]+"/"+kind+"-schema")
}})
}
return root
}
func newKnowledgeParserCommand(service *app.Service, deps Dependencies, flags *globalFlags) *cobra.Command {
root := &cobra.Command{Use: "knowledge-parser", Short: "查询文档解析器", Args: cobra.NoArgs}
var mime string
list := &cobra.Command{Use: "list", Short: "列出文档解析器", Args: cobra.NoArgs, RunE: func(cmd *cobra.Command, _ []string) error {
path := "/api/v1/knowledge/parsers"
if mime != "" {
path += "?" + url.Values{"mime_type": {mime}}.Encode()
}
return ragGet(cmd, service, deps, flags, path)
}}
list.Flags().StringVar(&mime, "mime-type", "", "按 MIME type 筛选")
root.AddCommand(list)
return root
}
5 changes: 5 additions & 0 deletions internal/command/schema.go
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,11 @@ type schemaFlag struct {
var placeholderPattern = regexp.MustCompile(`(<[^>]+>|\[[^]]+\])`)

var schemaMetaByName = map[string]schemaCommandMeta{
"knowledge-engine.list": readMeta(operationIDs("knowledge_engine.list")...),
"knowledge-engine.creation-schema": readMeta(operationIDs("knowledge_engine.creation_schema")...),
"knowledge-engine.retrieval-schema": readMeta(operationIDs("knowledge_engine.retrieval_schema")...),
"knowledge-parser.list": readMeta(operationIDs("knowledge_parser.list")...),

"schema": {},
"completion": {},
"completion.bash": {},
Expand Down
78 changes: 78 additions & 0 deletions internal/integration/rag_cli_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
package integration_test

import (
"encoding/json"
"fmt"
"net/http"
"net/http/httptest"
"path/filepath"
"strings"
"testing"
)

func TestRAGDiscoveryCommands(t *testing.T) {
configPath := filepath.Join(t.TempDir(), "config.yaml")
permissions := []string{"resource.view"}
supported, missing := true, false
var requests []string
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
t.Errorf("unexpected mutation %s", r.Method)
w.WriteHeader(405)
return
}
switch r.URL.Path {
case "/api/v1/system/context":
p, _ := json.Marshal(permissions)
fmt.Fprintf(w, `{"code":0,"data":{"instance_uuid":"instance-a","workspace_uuid":"workspace-a","api_key_id":"key-a","permissions":%s}}`, p)
case "/api/v1/system/capabilities":
if supported {
fmt.Fprint(w, capabilitiesJSON("knowledge_engine.list", "knowledge_engine.creation_schema", "knowledge_engine.retrieval_schema", "knowledge_parser.list"))
} else {
fmt.Fprint(w, capabilitiesJSON())
}
default:
requests = append(requests, r.URL.RequestURI())
if missing {
writeCLIError(w, http.StatusNotFound)
return
}
fmt.Fprint(w, `{"code":0,"data":{"items":[],"text":"connection-secret"}}`)
}
}))
defer server.Close()
configureWriteContext(t, configPath, server.URL, "FEATURE_KEY")
env := map[string]string{"FEATURE_KEY": "connection-secret"}
for _, test := range []struct {
args []string
path string
}{{[]string{"knowledge-engine", "list"}, "/api/v1/knowledge/engines"},
{[]string{"knowledge-engine", "creation-schema", "author/engine"}, "/api/v1/knowledge/engines/author/engine/creation-schema"},
{[]string{"knowledge-engine", "retrieval-schema", "author/engine"}, "/api/v1/knowledge/engines/author/engine/retrieval-schema"},
{[]string{"knowledge-parser", "list", "--mime-type", "text/plain"}, "/api/v1/knowledge/parsers?mime_type=text%2Fplain"}} {
got := run(t, configPath, env, "", test.args...)
requireSuccess(t, got)
if strings.Contains(got.out, "connection-secret") {
t.Fatalf("secret leaked: %s", got.out)
}
if requests[len(requests)-1] != test.path {
t.Fatalf("request=%s want=%s", requests[len(requests)-1], test.path)
}
}
missing = true
if got := run(t, configPath, env, "", "knowledge-engine", "list"); got.code != 5 {
t.Fatalf("not found: %s", got.out)
}
missing = false
permissions = []string{}
before := len(requests)
if got := run(t, configPath, env, "", "knowledge-engine", "list"); got.code != 4 || len(requests) != before {
t.Fatalf("permission: %s", got.out)
}
permissions = []string{"resource.view"}
supported = false
if got := run(t, configPath, env, "", "knowledge-engine", "list"); got.code != 6 || len(requests) != before {
t.Fatalf("capability: %s", got.out)
}

}
Loading