feat(cli): 新增 knowledge search 和 knowledge chat 命令支持

- 在 CLI 命令中添加 knowledgeSearch 和 knowledgeChat 两个新命令
- 新增 knowledge 搜索命令,支持多模态图像检索及对话历史上下文传递
- 新增 knowledge 问答命令,支持多轮消息流式回答及多模态输入
- 在核心客户端库(core)中添加对应的 API 端点和类型定义
- 知识库检索接口 retrieve 标注为弃用,推荐使用 search 命令替代
- 更新 kscli 主程序入口,接入新命令并兼容旧命令
- 补充 e2e 测试覆盖 knowledge search 和 knowledge chat 的各类边界与流程
- 更新文档及命令示例,实现使用说明同步最新功能
- 增加测试配置,改善 E2E 测试环境与超时设置
This commit is contained in:
zeyu.fz
2026-06-26 15:52:44 +08:00
parent 9c8fe96a1f
commit ca69316446
21 changed files with 1385 additions and 35 deletions
+4
View File
@@ -26,6 +26,8 @@ import {
memoryProfileCreate,
memoryProfileGet,
knowledgeRetrieve,
knowledgeSearch,
knowledgeChat,
mcpCall,
mcpList,
mcpTools,
@@ -79,6 +81,8 @@ export const commands: Record<string, Command> = {
"memory profile create": memoryProfileCreate,
"memory profile get": memoryProfileGet,
"knowledge retrieve": knowledgeRetrieve,
"knowledge search": knowledgeSearch,
"knowledge chat": knowledgeChat,
"mcp call": mcpCall,
"mcp list": mcpList,
"mcp tools": mcpTools,
@@ -0,0 +1,164 @@
import { tmpdir } from "os";
import { describe, expect, test } from "vite-plus/test";
import { parseStdoutJson, runCli } from "./helpers.ts";
interface DryRunBody {
endpoint?: string;
request?: {
input?: {
messages?: Array<{ role: string; content: string }>;
};
parameters?: {
agent_options?: {
agent_id?: string;
image_list?: string[];
};
};
stream?: boolean;
};
}
describe("e2e: knowledge chat", () => {
test("knowledge chat --help 正常退出", async () => {
const { stderr, exitCode } = await runCli(["knowledge", "chat", "--help"]);
expect(exitCode, stderr).toBe(0);
expect(stderr).toMatch(/--message/i);
expect(stderr).toMatch(/--agent-id/i);
expect(stderr).toMatch(/--workspace-id/i);
});
test("缺少 --message 时打印帮助并退出 (0)", async () => {
const { stderr, exitCode } = await runCli([
"knowledge",
"chat",
"--agent-id",
"aid_test",
"--non-interactive",
]);
expect(exitCode).toBe(0);
expect(stderr).toMatch(/--message|Usage:/i);
});
test("缺少 --agent-id 时打印帮助并退出 (0)", async () => {
const { stderr, exitCode } = await runCli([
"knowledge",
"chat",
"--message",
"Hello",
"--non-interactive",
]);
expect(exitCode).toBe(0);
expect(stderr).toMatch(/--agent-id|Usage:/i);
});
test("缺少 --workspace-id 时非零退出并提示", async () => {
const { stderr, exitCode } = await runCli(
[
"knowledge",
"chat",
"--message",
"Hello",
"--agent-id",
"aid_test",
"--non-interactive",
"--output",
"json",
],
{
DASHSCOPE_API_KEY: "sk-fake",
BAILIAN_WORKSPACE_ID: undefined,
BAILIAN_CONFIG_DIR: tmpdir(),
},
);
expect(exitCode).not.toBe(0);
expect(stderr).toMatch(/workspace.*required/i);
});
test("--dry-run 输出 endpoint 和 request body", async () => {
const { stdout, stderr, exitCode } = await runCli(
[
"knowledge",
"chat",
"--dry-run",
"--message",
"什么是RAG",
"--agent-id",
"aid_test",
"--workspace-id",
"ws_test",
"--non-interactive",
"--output",
"json",
],
{ DASHSCOPE_API_KEY: "sk-fake-for-dryrun" },
);
expect(exitCode, stderr).toBe(0);
const data = parseStdoutJson<DryRunBody>(stdout);
expect(data.endpoint).toMatch(/ws_test\.cn-beijing\.maas\.aliyuncs\.com/);
expect(data.endpoint).toMatch(/api\/v2\/apps\/knowledge\/chat/);
expect(data.request?.input?.messages?.[0]?.role).toBe("user");
expect(data.request?.input?.messages?.[0]?.content).toBe("什么是RAG");
expect(data.request?.parameters?.agent_options?.agent_id).toBe("aid_test");
});
test("--dry-run 多轮消息解析 role:content 前缀", async () => {
const { stdout, stderr, exitCode } = await runCli(
[
"knowledge",
"chat",
"--dry-run",
"--message",
"user:什么是RAG",
"--message",
"assistant:RAG是检索增强生成",
"--message",
"它怎么工作",
"--agent-id",
"aid_test",
"--workspace-id",
"ws_test",
"--non-interactive",
"--output",
"json",
],
{ DASHSCOPE_API_KEY: "sk-fake-for-dryrun" },
);
expect(exitCode, stderr).toBe(0);
const data = parseStdoutJson<DryRunBody>(stdout);
const msgs = data.request?.input?.messages ?? [];
expect(msgs).toHaveLength(3);
expect(msgs[0]?.role).toBe("user");
expect(msgs[0]?.content).toBe("什么是RAG");
expect(msgs[1]?.role).toBe("assistant");
expect(msgs[1]?.content).toBe("RAG是检索增强生成");
expect(msgs[2]?.role).toBe("user");
expect(msgs[2]?.content).toBe("它怎么工作");
});
test("--dry-run + --image 输出 image_list", async () => {
const { stdout, stderr, exitCode } = await runCli(
[
"knowledge",
"chat",
"--dry-run",
"--message",
"描述这张图",
"--agent-id",
"aid_test",
"--workspace-id",
"ws_test",
"--image",
"https://example.com/img.jpg",
"--non-interactive",
"--output",
"json",
],
{ DASHSCOPE_API_KEY: "sk-fake-for-dryrun" },
);
expect(exitCode, stderr).toBe(0);
const data = parseStdoutJson<DryRunBody>(stdout);
expect(data.request?.parameters?.agent_options?.image_list).toEqual([
"https://example.com/img.jpg",
]);
});
});
@@ -0,0 +1,180 @@
import { tmpdir } from "os";
import { describe, expect, test } from "vite-plus/test";
import { parseStdoutJson, runCli } from "./helpers.ts";
interface DryRunBody {
endpoint?: string;
request?: {
query?: string;
agent_id?: string;
image_list?: string[];
query_history?: Array<{ role: string; content: string }>;
};
}
describe("e2e: knowledge search", () => {
test("knowledge search --help 正常退出", async () => {
const { stderr, exitCode } = await runCli(["knowledge", "search", "--help"]);
expect(exitCode, stderr).toBe(0);
expect(stderr).toMatch(/--query/i);
expect(stderr).toMatch(/--agent-id/i);
expect(stderr).toMatch(/--workspace-id/i);
expect(stderr).toMatch(/--image/i);
expect(stderr).toMatch(/--query-history/i);
});
test("缺少 --query 时打印帮助并退出 (0)", async () => {
const { stderr, exitCode } = await runCli([
"knowledge",
"search",
"--agent-id",
"aid_test",
"--non-interactive",
]);
expect(exitCode).toBe(0);
expect(stderr).toMatch(/--query|Usage:/i);
});
test("缺少 --agent-id 时打印帮助并退出 (0)", async () => {
const { stderr, exitCode } = await runCli([
"knowledge",
"search",
"--query",
"test",
"--non-interactive",
]);
expect(exitCode).toBe(0);
expect(stderr).toMatch(/--agent-id|Usage:/i);
});
test("缺少 --workspace-id 时非零退出并提示", async () => {
const { stderr, exitCode } = await runCli(
[
"knowledge",
"search",
"--query",
"test",
"--agent-id",
"aid_test",
"--non-interactive",
"--output",
"json",
],
{
DASHSCOPE_API_KEY: "sk-fake",
BAILIAN_WORKSPACE_ID: undefined,
BAILIAN_CONFIG_DIR: tmpdir(),
},
);
expect(exitCode).not.toBe(0);
expect(stderr).toMatch(/workspace.*required/i);
});
test("--dry-run 输出 endpoint 和 request body", async () => {
const { stdout, stderr, exitCode } = await runCli(
[
"knowledge",
"search",
"--dry-run",
"--query",
"什么是RAG",
"--agent-id",
"aid_test",
"--workspace-id",
"ws_test",
"--non-interactive",
"--output",
"json",
],
{ DASHSCOPE_API_KEY: "sk-fake-for-dryrun" },
);
expect(exitCode, stderr).toBe(0);
const data = parseStdoutJson<DryRunBody>(stdout);
expect(data.endpoint).toMatch(/ws_test\.cn-beijing\.maas\.aliyuncs\.com/);
expect(data.endpoint).toMatch(/api\/v1\/indices\/knowledge\/search/);
expect(data.request?.query).toBe("什么是RAG");
expect(data.request?.agent_id).toBe("aid_test");
});
test("--dry-run + --image 输出 image_list", async () => {
const { stdout, stderr, exitCode } = await runCli(
[
"knowledge",
"search",
"--dry-run",
"--query",
"test",
"--agent-id",
"aid_test",
"--workspace-id",
"ws_test",
"--image",
"https://example.com/a.jpg",
"--image",
"https://example.com/b.jpg",
"--non-interactive",
"--output",
"json",
],
{ DASHSCOPE_API_KEY: "sk-fake-for-dryrun" },
);
expect(exitCode, stderr).toBe(0);
const data = parseStdoutJson<DryRunBody>(stdout);
expect(data.request?.image_list).toEqual([
"https://example.com/a.jpg",
"https://example.com/b.jpg",
]);
});
test("--dry-run + --query-history 输出用户对话历史", async () => {
const { stdout, stderr, exitCode } = await runCli(
[
"knowledge",
"search",
"--dry-run",
"--query",
"它怎么工作",
"--agent-id",
"aid_test",
"--workspace-id",
"ws_test",
"--query-history",
'[{"role":"user","content":"什么是RAG"},{"role":"assistant","content":"RAG是检索增强生成"}]',
"--non-interactive",
"--output",
"json",
],
{ DASHSCOPE_API_KEY: "sk-fake-for-dryrun" },
);
expect(exitCode, stderr).toBe(0);
const data = parseStdoutJson<DryRunBody>(stdout);
expect(data.request?.query_history).toEqual([
{ role: "user", content: "什么是RAG" },
{ role: "assistant", content: "RAG是检索增强生成" },
]);
});
test("--dry-run + --query-history 无效 JSON 非零退出", async () => {
const { stderr, exitCode } = await runCli(
[
"knowledge",
"search",
"--dry-run",
"--query",
"test",
"--agent-id",
"aid_test",
"--workspace-id",
"ws_test",
"--query-history",
"not-valid-json",
"--non-interactive",
"--output",
"json",
],
{ DASHSCOPE_API_KEY: "sk-fake-for-dryrun" },
);
expect(exitCode).not.toBe(0);
expect(stderr).toMatch(/query-history.*valid JSON/i);
});
});
@@ -0,0 +1,255 @@
import {
defineCommand,
request,
knowledgeChatEndpoint,
parseSSE,
detectOutputFormat,
BailianError,
ExitCode,
isInteractive,
type Config,
type GlobalFlags,
type KnowledgeChatRequest,
type KnowledgeChatStreamChunk,
} from "bailian-cli-core";
import { failIfMissing, cmdUsage, emitResult, emitBare, promptText } from "bailian-cli-runtime";
interface ParsedMessage {
role: "user" | "assistant";
content: string;
}
function parseMessages(flags: GlobalFlags): ParsedMessage[] {
const messages: ParsedMessage[] = [];
if (flags.message) {
const validRoles = new Set(["user", "assistant"]);
const msgs = flags.message as string[];
for (const m of msgs) {
const colonIdx = m.indexOf(":");
const maybeRole = colonIdx !== -1 ? m.slice(0, colonIdx) : "";
if (validRoles.has(maybeRole)) {
messages.push({ role: maybeRole as "user" | "assistant", content: m.slice(colonIdx + 1) });
} else {
messages.push({ role: "user", content: m });
}
}
}
return messages;
}
/** SSE step_change → human-friendly progress label (TTY only) */
const STEP_LABELS: Record<string, string> = {
tool_calling: "🔍 Retrieving...",
plan_start: "🤔 Planning...",
generation_start: "✍️ Generating...",
};
export default defineCommand({
description: "Chat with a Bailian knowledge base (RAG Q&A with streaming)",
usageArgs: "--message <text> --agent-id <id> [flags]",
options: [
{
flag: "--message <text>",
description:
"Message text (repeatable). Supports role:content prefix to set role (e.g. user:hello), defaults to user. Follows OpenAI message format",
required: true,
type: "array",
},
{
flag: "--agent-id <id>",
description: "Q&A service ID (find in console knowledge Q&A page)",
required: true,
},
{
flag: "--workspace-id <id>",
description: "Workspace ID for API endpoint URL (or set BAILIAN_WORKSPACE_ID)",
},
{
flag: "--image <url>",
description: "Image URL(s) (repeatable)",
type: "array",
},
],
notes: [
"Response is returned as SSE stream events. Event lifecycle: tool_calling → tool_return → plan_start → planning → plan_end → generation_start → generating → generation_end. tool_calling → tool_return may loop multiple times.",
"Auth: uses DashScope API Key (Bearer token). Get yours from the console API Key page.",
"`--workspace-id` can be set via BAILIAN_WORKSPACE_ID env or `kscli config set workspace_id <id>`.",
'Multi-turn: use --message "user:..." and --message "assistant:..." to pass conversation history.',
],
exampleArgs: [
'--message "What is RAG?" --agent-id aid-xxx --workspace-id ws-xxx',
'--message "user:What is RAG?" --message "assistant:RAG is..." --message "How does it work?" --agent-id aid-xxx --workspace-id ws-xxx',
],
async run(config: Config, flags: GlobalFlags) {
let messages = parseMessages(flags);
if (messages.length === 0) {
if (isInteractive({ nonInteractive: config.nonInteractive })) {
const hint = await promptText({ message: "Enter your message:" });
if (!hint) {
process.stderr.write("Chat cancelled.\n");
process.exit(1);
}
messages = [{ role: "user", content: hint }];
} else {
failIfMissing("message", cmdUsage(config, "--message <text> --agent-id <id>"));
}
}
const agentId = flags.agentId as string;
if (!agentId) failIfMissing("agent-id", cmdUsage(config, "--message <text> --agent-id <id>"));
const workspaceId = (flags.workspaceId as string) || config.workspaceId;
if (!workspaceId) {
throw new BailianError(
"Workspace ID is required.",
ExitCode.USAGE,
"Pass --workspace-id, set BAILIAN_WORKSPACE_ID env, or configure: kscli config set workspace_id <id>",
);
}
const format = detectOutputFormat(config.output);
// API only supports SSE; streamOutput controls whether to print tokens in real-time
const streamOutput = format === "text" && !!process.stdout.isTTY;
const body: KnowledgeChatRequest = {
input: {
messages,
},
parameters: {
agent_options: {
agent_id: agentId,
},
},
stream: true,
};
const imageUrls = flags.image as string[] | undefined;
if (imageUrls && imageUrls.length > 0) {
body.parameters.agent_options.image_list = imageUrls;
}
const url = knowledgeChatEndpoint(workspaceId);
if (config.dryRun) {
emitResult({ endpoint: url, request: body }, format);
return;
}
const res = await request(config, {
url,
method: "POST",
body,
stream: true,
});
if (streamOutput) {
let textContent = "";
const dim = config.noColor ? "" : "\x1b[2m";
const reset = config.noColor ? "" : "\x1b[0m";
const verbose = config.verbose;
for await (const event of parseSSE(res)) {
if (event.data === "[DONE]") break;
if (event.event === "error") {
let errMsg = "Chat API error";
let errCode: string | undefined;
try {
const err = JSON.parse(event.data);
errMsg = err.message || errMsg;
errCode = err.code;
} catch {
/* use defaults */
}
throw new BailianError(
errMsg,
ExitCode.GENERAL,
errCode ? `API error: ${errCode}` : undefined,
);
}
try {
const chunk = JSON.parse(event.data) as KnowledgeChatStreamChunk;
for (const choice of chunk.output?.choices ?? []) {
const msg = choice.message;
// Progress indicator (TTY text mode)
if (msg.extra?.step_change) {
const label = STEP_LABELS[msg.extra.step_change];
if (label) {
process.stdout.write(`${dim}${label}${reset}\n`);
}
}
// Verbose: dump all events to stderr
if (verbose && msg.extra?.step_change) {
process.stderr.write(
`${dim}[event] step_change=${msg.extra.step_change} step=${msg.extra?.step ?? ""} group=${msg.extra?.group ?? ""}${reset}\n`,
);
}
// Extract generated content
if (msg.content) {
textContent += msg.content;
process.stdout.write(msg.content);
}
if (choice.finish_reason === "stop") break;
}
} catch {
// Skip unparseable chunks
}
}
process.stdout.write("\n");
} else {
// Buffered output: collect all chunks then emit
let textContent = "";
let requestId = "";
for await (const event of parseSSE(res)) {
if (event.data === "[DONE]") break;
if (event.event === "error") {
let errMsg = "Chat API error";
let errCode: string | undefined;
try {
const err = JSON.parse(event.data);
errMsg = err.message || errMsg;
errCode = err.code;
} catch {
/* use defaults */
}
throw new BailianError(
errMsg,
ExitCode.GENERAL,
errCode ? `API error: ${errCode}` : undefined,
);
}
try {
const chunk = JSON.parse(event.data) as KnowledgeChatStreamChunk;
if (chunk.request_id) requestId = chunk.request_id;
for (const choice of chunk.output?.choices ?? []) {
if (choice.message?.content) {
textContent += choice.message.content;
}
if (choice.finish_reason === "stop") break;
}
} catch {
// Skip unparseable chunks
}
}
if (config.quiet || format === "text") {
emitBare(textContent);
} else {
emitResult({ answer: textContent, request_id: requestId }, format);
}
}
},
});
@@ -23,7 +23,7 @@ import { emitResult, emitBare } from "bailian-cli-runtime";
const BAILIAN_HOST = "bailian.cn-beijing.aliyuncs.com";
export default defineCommand({
description: "Retrieve from a Bailian knowledge base",
description: "Retrieve from a Bailian knowledge base (deprecated, use `search` instead)",
skipDefaultApiKeySetup: true,
usageArgs: "--index-id <id> --query <text> [flags]",
options: [
@@ -0,0 +1,139 @@
import {
defineCommand,
requestJson,
knowledgeSearchEndpoint,
detectOutputFormat,
BailianError,
ExitCode,
isInteractive,
type Config,
type GlobalFlags,
type KnowledgeSearchRequest,
type KnowledgeSearchResponse,
} from "bailian-cli-core";
import { failIfMissing, cmdUsage, emitResult, emitBare, promptText } from "bailian-cli-runtime";
export default defineCommand({
description: "Search a Bailian knowledge base (RAG semantic retrieval)",
usageArgs: "--query <text> --agent-id <id> [flags]",
options: [
{
flag: "--query <text>",
description: "Search query text (required, cannot be empty)",
required: true,
},
{
flag: "--agent-id <id>",
description: "Retrieval service ID (find in console knowledge retrieval page)",
required: true,
},
{
flag: "--workspace-id <id>",
description: "Workspace ID for API endpoint URL (or set BAILIAN_WORKSPACE_ID)",
},
{
flag: "--image <url>",
description: "Image URL for multimodal retrieval (repeatable)",
type: "array",
},
{
flag: "--query-history <json>",
description:
'User conversation history JSON for context understanding and query rewriting. Format: \'[{"role":"user","content":"What is RAG"},{"role":"assistant","content":"RAG is..."}]\'',
},
],
notes: [
"Retrieval scope and strategy (multi-index weighting, routing, reranking, etc.) are driven by the agent_id service config. Only query and agent_id are required.",
"Auth: uses DashScope API Key (Bearer token). Get yours from the console API Key page.",
"`--workspace-id` can be set via BAILIAN_WORKSPACE_ID env or `kscli config set workspace_id <id>`.",
"`--query-history` passes prior conversation turns; the server rewrites the query based on context to improve retrieval relevance.",
],
exampleArgs: [
'--query "What is RAG?" --agent-id aid-xxx --workspace-id ws-xxx',
'--api-key $DASHSCOPE_API_KEY --query "test search" --agent-id aid-xxx --workspace-id ws-xxx --image https://example.com/img.jpg',
'--query "How does it work" --agent-id aid-xxx --workspace-id ws-xxx --query-history \'[{"role":"user","content":"What is RAG"},{"role":"assistant","content":"RAG is retrieval-augmented generation"}]\'',
],
async run(config: Config, flags: GlobalFlags) {
let query = flags.query as string | undefined;
if (!query) {
if (isInteractive({ nonInteractive: config.nonInteractive })) {
const hint = await promptText({ message: "Enter your search query:" });
if (!hint) {
process.stderr.write("Search cancelled.\n");
process.exit(1);
}
query = hint;
} else {
failIfMissing("query", cmdUsage(config, "--query <text> --agent-id <id>"));
}
}
const agentId = flags.agentId as string;
if (!agentId) failIfMissing("agent-id", cmdUsage(config, "--query <text> --agent-id <id>"));
const workspaceId = (flags.workspaceId as string) || config.workspaceId;
if (!workspaceId) {
throw new BailianError(
"Workspace ID is required.",
ExitCode.USAGE,
"Pass --workspace-id, set BAILIAN_WORKSPACE_ID env, or configure: kscli config set workspace_id <id>",
);
}
const format = detectOutputFormat(config.output);
const body: KnowledgeSearchRequest = {
query: query!,
agent_id: agentId,
};
const imageUrls = flags.image as string[] | undefined;
if (imageUrls && imageUrls.length > 0) {
body.image_list = imageUrls;
}
// Parse query_history JSON for multi-turn context
if (flags.queryHistory) {
try {
body.query_history = JSON.parse(flags.queryHistory as string) as Array<{
role: "user" | "assistant";
content: string;
}>;
} catch {
throw new BailianError(
'--query-history must be valid JSON. Example: --query-history \'[{"role":"user","content":"What is RAG"}]\'',
ExitCode.USAGE,
);
}
}
const url = knowledgeSearchEndpoint(workspaceId);
if (config.dryRun) {
emitResult({ endpoint: url, request: body }, format);
return;
}
const response = await requestJson<KnowledgeSearchResponse>(config, {
url,
method: "POST",
body,
});
const nodes = response.data?.nodes || [];
if (config.quiet || format === "text") {
if (nodes.length === 0) {
emitBare("No results found.");
} else {
for (let i = 0; i < nodes.length; i++) {
const node = nodes[i]!;
emitBare(`[${i + 1}] (score: ${node.score.toFixed(4)})`);
emitBare(node.text);
emitBare("");
}
}
} else {
emitResult(response, format);
}
},
});
+2
View File
@@ -29,6 +29,8 @@ export { default as memoryDelete } from "./commands/memory/delete.ts";
export { default as memoryProfileCreate } from "./commands/memory/profile-create.ts";
export { default as memoryProfileGet } from "./commands/memory/profile-get.ts";
export { default as knowledgeRetrieve } from "./commands/knowledge/retrieve.ts";
export { default as knowledgeSearch } from "./commands/knowledge/search.ts";
export { default as knowledgeChat } from "./commands/knowledge/chat.ts";
export { default as mcpCall } from "./commands/mcp/call.ts";
export { default as mcpList } from "./commands/mcp/list.ts";
export { default as mcpTools } from "./commands/mcp/tools.ts";
+12
View File
@@ -79,6 +79,18 @@ export function knowledgeRetrieveEndpoint(baseUrl: string): string {
return `${baseUrl}/api/v1/indices/rag/index/retrieve`;
}
// ---- Knowledge Search (新版 RAG 检索, workspace-based host) ----
export function knowledgeSearchEndpoint(workspaceId: string): string {
return `https://${workspaceId}.cn-beijing.maas.aliyuncs.com/api/v1/indices/knowledge/search`;
}
// ---- Knowledge Chat (新版 RAG 问答, workspace-based host) ----
export function knowledgeChatEndpoint(workspaceId: string): string {
return `https://${workspaceId}.cn-beijing.maas.aliyuncs.com/api/v2/apps/knowledge/chat`;
}
// ---- MCP Services (Streamable HTTP) ----
export function mcpWebSearchEndpoint(baseUrl: string): string {
+2
View File
@@ -5,7 +5,9 @@ export {
chatEndpoint,
imageEndpoint,
imageSyncEndpoint,
knowledgeChatEndpoint,
knowledgeRetrieveEndpoint,
knowledgeSearchEndpoint,
memoryAddEndpoint,
memoryListEndpoint,
memoryNodeEndpoint,
+78
View File
@@ -417,6 +417,84 @@ export interface DashScopeKnowledgeRetrieveResponse {
};
}
// ---- Knowledge Search (新版 RAG 检索 API, agent_id-based) ----
export interface KnowledgeSearchRequest {
query: string;
agent_id: string;
image_list?: string[];
query_history?: Array<{ role: "user" | "assistant"; content: string }>;
}
export interface KnowledgeSearchResponse {
code: string;
status_code: number;
request_id: string;
data: {
total: number;
cost_time: number;
nodes: Array<{
score: number;
text: string;
metadata: {
content?: string;
title?: string;
doc_id?: string;
doc_name?: string;
doc_url?: string;
pipeline_id?: string;
workspace_id?: string;
page_number?: number;
image_url?: string;
_knowledge_type?: string;
_citation_index?: number;
_score?: number;
};
}>;
};
}
// ---- Knowledge Chat (新版 RAG 问答 SSE API, agent_id-based) ----
export interface KnowledgeChatRequest {
input: {
messages: Array<{ role: "user" | "assistant"; content: string }>;
request_id?: string;
};
parameters: {
agent_options: {
agent_id: string;
image_list?: string[];
user?: {
user_id?: string;
workspace_id?: string;
};
};
};
stream: boolean;
}
export interface KnowledgeChatStreamChunk {
output: {
choices: Array<{
message: {
role: string;
content: string;
tool_calls?: unknown[];
extra?: {
group?: string;
step_change?: string;
step?: string;
};
};
finish_reason: string;
}>;
};
code: string;
message: string;
request_id: string;
}
// ---- Speech Synthesis / TTS (DashScope) ----
export interface DashScopeTTSRequest {
+4
View File
@@ -23,8 +23,12 @@ export type {
DashScopeVideoEditRequest,
DashScopeVideoRefRequest,
DashScopeVideoRequest,
KnowledgeChatRequest,
KnowledgeChatStreamChunk,
KnowledgeRetrieveRequest,
KnowledgeRetrieveResponse,
KnowledgeSearchRequest,
KnowledgeSearchResponse,
MemoryAddRequest,
MemoryAddResponse,
MemoryMessage,
+20 -11
View File
@@ -28,20 +28,29 @@ npm install -g knowledge-studio-cli
## Quick Start
```bash
# Retrieve from a knowledge base
kscli retrieve \
--index-id <your-index-id> \
--query "What is Model Studio?"
# Search a knowledge base
kscli search \
--query "What is Model Studio?" \
--agent-id <your-agent-id> \
--workspace-id <your-workspace-id>
# Chat with a knowledge base
kscli chat \
--message "What is RAG?" \
--agent-id <your-agent-id> \
--workspace-id <your-workspace-id>
```
## Commands
| Command | Description |
| :------------ | :-------------------------------- |
| `retrieve` | Query a knowledge base (RAG) |
| `config show` | Display current configuration |
| `config set` | Set a configuration value |
| `update` | Self-update to the latest version |
| Command | Description |
| :------------ | :------------------------------------------------ |
| `search` | Semantic search across knowledge bases (RAG) |
| `chat` | Knowledge-base Q&A with streaming (RAG) |
| `retrieve` | Query a knowledge base (deprecated, use `search`) |
| `config show` | Display current configuration |
| `config set` | Set a configuration value |
| `update` | Self-update to the latest version |
## Authentication
@@ -55,7 +64,7 @@ export DASHSCOPE_API_KEY=sk-xxxxx
kscli config set --key api_key --value sk-xxxxx
# Option 3: Per-command flag
kscli retrieve --api-key sk-xxxxx --index-id <id> --query "..."
kscli search --api-key sk-xxxxx --query "..." --agent-id <id> --workspace-id <id>
```
## Configuration
+19 -10
View File
@@ -29,19 +29,28 @@ npm install -g knowledge-studio-cli
```bash
# 检索知识库
kscli retrieve \
--index-id <your-index-id> \
--query "什么是 Model Studio?"
kscli search \
--query "什么是 Model Studio?" \
--agent-id <your-agent-id> \
--workspace-id <your-workspace-id>
# 知识库问答
kscli chat \
--message "什么是RAG?" \
--agent-id <your-agent-id> \
--workspace-id <your-workspace-id>
```
## 命令列表
| 命令 | 说明 |
| :------------ | :---------------- |
| `retrieve` | 查询知识库(RAG) |
| `config show` | 显示当前配置 |
| `config set` | 设置配置项 |
| `update` | 自更新到最新版本 |
| 命令 | 说明 |
| :------------ | :------------------------------------ |
| `search` | 知识库语义检索(RAG) |
| `chat` | 知识库问答(流式输出) |
| `retrieve` | 查询知识库(已弃用,请使用 `search`) |
| `config show` | 显示当前配置 |
| `config set` | 设置配置项 |
| `update` | 自更新到最新版本 |
## 认证方式
@@ -55,7 +64,7 @@ export DASHSCOPE_API_KEY=sk-xxxxx
kscli config set --key api_key --value sk-xxxxx
# 方式三:命令行参数
kscli retrieve --api-key sk-xxxxx --index-id <id> --query "..."
kscli search --api-key sk-xxxxx --query "..." --agent-id <id> --workspace-id <id>
```
## 配置
+11 -2
View File
@@ -1,6 +1,13 @@
import { createCli } from "bailian-cli-runtime";
import type { Command } from "bailian-cli-core";
import { configShow, configSet, update, knowledgeRetrieve } from "bailian-cli-commands";
import {
configShow,
configSet,
update,
knowledgeRetrieve,
knowledgeSearch,
knowledgeChat,
} from "bailian-cli-commands";
import pkg from "../package.json" with { type: "json" };
const commands: Record<string, Command> = {
@@ -8,9 +15,11 @@ const commands: Record<string, Command> = {
"config set": configSet,
update,
retrieve: knowledgeRetrieve,
search: knowledgeSearch,
chat: knowledgeChat,
};
createCli(commands, {
void createCli(commands, {
binName: "kscli",
version: pkg.version,
clientName: "knowledge-studio-cli",
+137
View File
@@ -0,0 +1,137 @@
import { describe, expect, test } from "vite-plus/test";
import { isChatE2EReady, parseStdoutJson, runKscli } from "./helpers.ts";
// ---- Types ----
interface ChatJsonResult {
answer: string;
request_id: string;
}
// ---- Real API call tests (gated by BAILIAN_E2E + credentials) ----
describe.skipIf(!isChatE2EReady())("e2e: kscli chat (live)", () => {
const agentId = process.env.BAILIAN_E2E_CHAT_AGENT_ID!;
const workspaceId = process.env.BAILIAN_WORKSPACE_ID!;
test("chat (JSON mode) returns answer", async () => {
const { stdout, stderr, exitCode } = await runKscli([
"chat",
"--message",
"什么是大模型?",
"--agent-id",
agentId,
"--workspace-id",
workspaceId,
"--non-interactive",
"--output",
"json",
]);
expect(exitCode, stderr).toBe(0);
const data = parseStdoutJson<ChatJsonResult>(stdout);
expect(data.answer).toBeTruthy();
expect(data.answer.length).toBeGreaterThan(0);
expect(data.request_id).toBeTruthy();
});
test("chat (text mode) returns plain text", async () => {
const { stdout, stderr, exitCode } = await runKscli([
"chat",
"--message",
"什么是RAG?",
"--agent-id",
agentId,
"--workspace-id",
workspaceId,
"--non-interactive",
"--output",
"text",
]);
expect(exitCode, stderr).toBe(0);
expect(stdout.trim().length).toBeGreaterThan(0);
});
test("chat (stream, JSON mode) collects and returns answer", async () => {
const { stdout, stderr, exitCode } = await runKscli([
"chat",
"--message",
"什么是检索增强生成?",
"--agent-id",
agentId,
"--workspace-id",
workspaceId,
"--non-interactive",
"--output",
"json",
]);
expect(exitCode, stderr).toBe(0);
const data = parseStdoutJson<ChatJsonResult>(stdout);
expect(data.answer).toBeTruthy();
expect(data.answer.length).toBeGreaterThan(0);
expect(data.request_id).toBeTruthy();
});
test("chat (stream, text mode) outputs streaming text", async () => {
const { stdout, stderr, exitCode } = await runKscli([
"chat",
"--message",
"什么是向量检索?",
"--agent-id",
agentId,
"--workspace-id",
workspaceId,
"--non-interactive",
"--output",
"text",
]);
expect(exitCode, stderr).toBe(0);
// Streaming text mode: output should contain some text content
expect(stdout.trim().length).toBeGreaterThan(0);
});
test("chat with multi-turn messages returns context-aware answer", async () => {
const { stdout, stderr, exitCode } = await runKscli([
"chat",
"--message",
"user:什么是大模型",
"--message",
"assistant:大模型是大规模语言模型,具有强大的理解和生成能力",
"--message",
"它有哪些应用场景?",
"--agent-id",
agentId,
"--workspace-id",
workspaceId,
"--non-interactive",
"--output",
"json",
]);
expect(exitCode, stderr).toBe(0);
const data = parseStdoutJson<ChatJsonResult>(stdout);
expect(data.answer).toBeTruthy();
expect(data.answer.length).toBeGreaterThan(0);
});
test("chat with invalid agent_id fails gracefully", async () => {
const { stderr, exitCode } = await runKscli([
"chat",
"--message",
"test",
"--agent-id",
"aid-invalid-not-exist",
"--workspace-id",
workspaceId,
"--non-interactive",
"--output",
"json",
]);
expect(exitCode).not.toBe(0);
expect(stderr).toBeTruthy();
});
});
+9
View File
@@ -0,0 +1,9 @@
import { loadRootEnv } from "./helpers.ts";
/**
* Vitest globalSetup: load monorepo root `.env` into `process.env` before tests run.
*/
export default function vitestGlobalSetup(): () => void {
loadRootEnv();
return () => {};
}
+129
View File
@@ -0,0 +1,129 @@
import { execFile } from "child_process";
import { existsSync, mkdtempSync, readFileSync } from "fs";
import { tmpdir } from "os";
import { promisify } from "util";
import { dirname, join } from "path";
import { fileURLToPath } from "url";
import { parseEnv } from "util";
const execFileAsync = promisify(execFile);
/** `packages/kscli` 根目录(含 `src/main.ts`) */
export const kscliPackageRoot = join(dirname(fileURLToPath(import.meta.url)), "..", "..");
const mainTs = join(kscliPackageRoot, "src", "main.ts");
/** Monorepo 根(含根 `package.json` 和 `.env`) */
export function monorepoRoot(): string {
return join(kscliPackageRoot, "..", "..");
}
// ---- E2E gating helpers ----
// ---- .env loader (cached) ----
let _rootEnvCache: Record<string, string | undefined> | null = null;
/** 读取 monorepo 根目录 `.env` 并缓存(.env 值优先于 shell 环境变量) */
function getRootEnv(): Record<string, string | undefined> {
if (_rootEnvCache !== null) return _rootEnvCache;
const rootEnvPath = join(monorepoRoot(), ".env");
_rootEnvCache = existsSync(rootEnvPath) ? parseEnv(readFileSync(rootEnvPath, "utf8")) : {};
return _rootEnvCache;
}
/** 从 .env 或 process.env 获取值(.env 优先) */
function envVar(key: string): string | undefined {
return getRootEnv()[key] ?? process.env[key];
}
// ---- E2E gating helpers ----
/** 显式开启后才跑真实网络 E2E */
export function isBailianE2EEnabled(): boolean {
return envVar("BAILIAN_E2E") === "1";
}
/** 是否有 DashScope API Key 可用 */
export function isDashScopeE2EReady(): boolean {
if (!isBailianE2EEnabled()) return false;
return !!envVar("DASHSCOPE_API_KEY")?.trim();
}
/** 知识检索 E2E 就绪:E2E 开启 + API Key + search agent ID + workspace ID */
export function isSearchE2EReady(): boolean {
if (!isDashScopeE2EReady()) return false;
return (
!!envVar("BAILIAN_E2E_SEARCH_AGENT_ID")?.trim() && !!envVar("BAILIAN_WORKSPACE_ID")?.trim()
);
}
/** 知识问答 E2E 就绪:E2E 开启 + API Key + chat agent ID + workspace ID */
export function isChatE2EReady(): boolean {
if (!isDashScopeE2EReady()) return false;
return !!envVar("BAILIAN_E2E_CHAT_AGENT_ID")?.trim() && !!envVar("BAILIAN_WORKSPACE_ID")?.trim();
}
// ---- CLI runner ----
export interface RunCliResult {
stdout: string;
stderr: string;
exitCode: number;
}
/**
* 子进程执行 kscli(等价于 `node packages/kscli/src/main.ts ...`)。
*/
export async function runKscli(
args: string[],
envOverrides: NodeJS.ProcessEnv = {},
): Promise<RunCliResult> {
try {
const { stdout, stderr } = await execFileAsync("node", [mainTs, ...args], {
cwd: kscliPackageRoot,
encoding: "utf8",
maxBuffer: 32 * 1024 * 1024,
env: {
...process.env,
// .env values override shell env vars (ensures correct API key is used)
...getRootEnv(),
// Unique clean config dir per run — prevents stale config.json from previous tests
BAILIAN_CONFIG_DIR: mkdtempSync(join(tmpdir(), "kscli-test-")),
NODE_NO_WARNINGS: "1",
DO_NOT_TRACK: "1",
...envOverrides,
},
});
return { stdout: stdout ?? "", stderr: stderr ?? "", exitCode: 0 };
} catch (err: unknown) {
const e = err as {
stdout?: string;
stderr?: string;
code?: number;
};
return {
stdout: e.stdout ?? "",
stderr: e.stderr ?? "",
exitCode: typeof e.code === "number" ? e.code : 1,
};
}
}
export function parseStdoutJson<T = unknown>(stdout: string): T {
const t = stdout.trim();
return JSON.parse(t) as T;
}
// ---- Global setup: load root .env ----
/**
* Vitest globalSetup:加载 monorepo 根目录 `.env` 合并到 `process.env`。
*/
export function loadRootEnv(): void {
const rootEnv = join(monorepoRoot(), ".env");
if (existsSync(rootEnv)) {
const parsed = parseEnv(readFileSync(rootEnv, "utf8"));
Object.assign(process.env, parsed);
}
}
+126
View File
@@ -0,0 +1,126 @@
import { describe, expect, test } from "vite-plus/test";
import { isSearchE2EReady, parseStdoutJson, runKscli } from "./helpers.ts";
// ---- Types ----
interface SearchResponse {
code: string;
status_code: number;
request_id: string;
data: {
total: number;
cost_time: number;
nodes: Array<{
score: number;
text: string;
metadata: {
content?: string;
title?: string;
doc_id?: string;
doc_name?: string;
doc_url?: string;
pipeline_id?: string;
workspace_id?: string;
page_number?: number;
image_url?: string;
_knowledge_type?: string;
_citation_index?: number;
_score?: number;
};
}>;
};
}
// ---- Real API call tests (gated by BAILIAN_E2E + credentials) ----
describe.skipIf(!isSearchE2EReady())("e2e: kscli search (live)", () => {
const agentId = process.env.BAILIAN_E2E_SEARCH_AGENT_ID!;
const workspaceId = process.env.BAILIAN_WORKSPACE_ID!;
test("search returns results in JSON mode", async () => {
const { stdout, stderr, exitCode } = await runKscli([
"search",
"--query",
"什么是大模型",
"--agent-id",
agentId,
"--workspace-id",
workspaceId,
"--non-interactive",
"--output",
"json",
]);
expect(exitCode, stderr).toBe(0);
const data = parseStdoutJson<SearchResponse>(stdout);
expect(data.code).toBe("Success");
expect(data.request_id).toBeTruthy();
expect(data.data.total).toBeGreaterThan(0);
expect(data.data.nodes.length).toBeGreaterThan(0);
const firstNode = data.data.nodes[0]!;
expect(typeof firstNode.score).toBe("number");
expect(firstNode.score).toBeGreaterThanOrEqual(0);
expect(typeof firstNode.text).toBe("string");
expect(firstNode.text.length).toBeGreaterThan(0);
});
test("search returns results in text mode", async () => {
const { stdout, stderr, exitCode } = await runKscli([
"search",
"--query",
"RAG",
"--agent-id",
agentId,
"--workspace-id",
workspaceId,
"--non-interactive",
"--output",
"text",
]);
expect(exitCode, stderr).toBe(0);
// Text mode: [1] (score: 0.xxxx) followed by text content
expect(stdout).toMatch(/\[1\].*score/);
});
test("search with --query-history returns results", async () => {
const { stdout, stderr, exitCode } = await runKscli([
"search",
"--query",
"它怎么工作",
"--agent-id",
agentId,
"--workspace-id",
workspaceId,
"--query-history",
'[{"role":"user","content":"什么是大模型"},{"role":"assistant","content":"大模型是大规模语言模型"}]',
"--non-interactive",
"--output",
"json",
]);
expect(exitCode, stderr).toBe(0);
const data = parseStdoutJson<SearchResponse>(stdout);
expect(data.code).toBe("Success");
expect(data.data.nodes.length).toBeGreaterThan(0);
});
test("search with invalid agent_id fails gracefully", async () => {
const { stderr, exitCode } = await runKscli([
"search",
"--query",
"test",
"--agent-id",
"aid-invalid-not-exist",
"--workspace-id",
workspaceId,
"--non-interactive",
"--output",
"json",
]);
expect(exitCode).not.toBe(0);
expect(stderr).toBeTruthy();
});
});
+6 -1
View File
@@ -1,9 +1,14 @@
import { defineConfig } from "vite-plus";
export default defineConfig({
test: {
globalSetup: "./tests/e2e/global-setup.ts",
testTimeout: 60_000,
hookTimeout: 60_000,
},
pack: {
entry: {
rag: "src/main.ts",
kscli: "src/main.ts",
},
hash: false,
minify: true,
+4 -2
View File
@@ -22,7 +22,9 @@ Use this index for the full quick index and global flags.
| `bl file upload` | Upload a local file to DashScope temporary storage (48h) | [file.md](file.md) |
| `bl image edit` | Edit an existing image with text instructions (Qwen-Image) | [image.md](image.md) |
| `bl image generate` | Generate images (Qwen-Image / wan2.x) | [image.md](image.md) |
| `bl knowledge retrieve` | Retrieve from a Bailian knowledge base | [knowledge.md](knowledge.md) |
| `bl knowledge chat` | Chat with a Bailian knowledge base (RAG Q&A with streaming) | [knowledge.md](knowledge.md) |
| `bl knowledge retrieve` | Retrieve from a Bailian knowledge base (deprecated, use `search` instead) | [knowledge.md](knowledge.md) |
| `bl knowledge search` | Search a Bailian knowledge base (RAG semantic retrieval) | [knowledge.md](knowledge.md) |
| `bl mcp call` | Call a tool on an MCP server (tools/call) | [mcp.md](mcp.md) |
| `bl mcp list` | List MCP servers activated under your Bailian account | [mcp.md](mcp.md) |
| `bl mcp tools` | List tools exposed by an MCP server (tools/list) | [mcp.md](mcp.md) |
@@ -67,7 +69,7 @@ Use this index for the full quick index and global flags.
| `console` | `call` | [console.md](console.md) |
| `file` | `upload` | [file.md](file.md) |
| `image` | `edit`, `generate` | [image.md](image.md) |
| `knowledge` | `retrieve` | [knowledge.md](knowledge.md) |
| `knowledge` | `chat`, `retrieve`, `search` | [knowledge.md](knowledge.md) |
| `mcp` | `call`, `list`, `tools` | [mcp.md](mcp.md) |
| `memory` | `add`, `delete`, `list`, `profile create`, `profile get`, `search`, `update` | [memory.md](memory.md) |
| `omni` | `(root)` | [omni.md](omni.md) |
+83 -8
View File
@@ -7,19 +7,55 @@ Index: [index.md](index.md)
## Commands in this group
| Command | Description |
| ----------------------- | -------------------------------------- |
| `bl knowledge retrieve` | Retrieve from a Bailian knowledge base |
| Command | Description |
| ----------------------- | ------------------------------------------------------------------------- |
| `bl knowledge chat` | Chat with a Bailian knowledge base (RAG Q&A with streaming) |
| `bl knowledge retrieve` | Retrieve from a Bailian knowledge base (deprecated, use `search` instead) |
| `bl knowledge search` | Search a Bailian knowledge base (RAG semantic retrieval) |
## Command details
### `bl knowledge chat`
| Field | Value |
| --------------- | ------------------------------------------------------------ |
| **Name** | `knowledge chat` |
| **Description** | Chat with a Bailian knowledge base (RAG Q&A with streaming) |
| **Usage** | `bl knowledge chat --message <text> --agent-id <id> [flags]` |
#### Options
| Flag | Type | Required | Description |
| --------------------- | ------ | -------- | -------------------------------------------------------------------------------------------------------------------------------------- |
| `--message <text>` | array | yes | Message text (repeatable). Supports role:content prefix to set role (e.g. user:hello), defaults to user. Follows OpenAI message format |
| `--agent-id <id>` | string | yes | Q&A service ID (find in console knowledge Q&A page) |
| `--workspace-id <id>` | string | no | Workspace ID for API endpoint URL (or set BAILIAN_WORKSPACE_ID) |
| `--image <url>` | array | no | Image URL(s) (repeatable) |
#### Notes
- Response is returned as SSE stream events. Event lifecycle: tool_calling → tool_return → plan_start → planning → plan_end → generation_start → generating → generation_end. tool_calling → tool_return may loop multiple times.
- Auth: uses DashScope API Key (Bearer token). Get yours from the console API Key page.
- `--workspace-id` can be set via BAILIAN_WORKSPACE_ID env or `kscli config set workspace_id <id>`.
- Multi-turn: use --message "user:..." and --message "assistant:..." to pass conversation history.
#### Examples
```bash
bl knowledge chat --message "What is RAG?" --agent-id aid-xxx --workspace-id ws-xxx
```
```bash
bl knowledge chat --message "user:What is RAG?" --message "assistant:RAG is..." --message "How does it work?" --agent-id aid-xxx --workspace-id ws-xxx
```
### `bl knowledge retrieve`
| Field | Value |
| --------------- | -------------------------------------------------------------- |
| **Name** | `knowledge retrieve` |
| **Description** | Retrieve from a Bailian knowledge base |
| **Usage** | `bl knowledge retrieve --index-id <id> --query <text> [flags]` |
| Field | Value |
| --------------- | ------------------------------------------------------------------------- |
| **Name** | `knowledge retrieve` |
| **Description** | Retrieve from a Bailian knowledge base (deprecated, use `search` instead) |
| **Usage** | `bl knowledge retrieve --index-id <id> --query <text> [flags]` |
#### Options
@@ -53,3 +89,42 @@ bl knowledge retrieve --index-id idx_xxx --query "How to use Alibaba Cloud Baili
```bash
bl knowledge retrieve --api-key $DASHSCOPE_API_KEY --index-id idx_xxx --query "RAG retrieval" --rerank --rerank-model qwen3-rerank-hybrid
```
### `bl knowledge search`
| Field | Value |
| --------------- | ------------------------------------------------------------ |
| **Name** | `knowledge search` |
| **Description** | Search a Bailian knowledge base (RAG semantic retrieval) |
| **Usage** | `bl knowledge search --query <text> --agent-id <id> [flags]` |
#### Options
| Flag | Type | Required | Description |
| ------------------------ | ------ | -------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `--query <text>` | string | yes | Search query text (required, cannot be empty) |
| `--agent-id <id>` | string | yes | Retrieval service ID (find in console knowledge retrieval page) |
| `--workspace-id <id>` | string | no | Workspace ID for API endpoint URL (or set BAILIAN_WORKSPACE_ID) |
| `--image <url>` | array | no | Image URL for multimodal retrieval (repeatable) |
| `--query-history <json>` | string | no | User conversation history JSON for context understanding and query rewriting. Format: '[{"role":"user","content":"What is RAG"},{"role":"assistant","content":"RAG is..."}]' |
#### Notes
- Retrieval scope and strategy (multi-index weighting, routing, reranking, etc.) are driven by the agent_id service config. Only query and agent_id are required.
- Auth: uses DashScope API Key (Bearer token). Get yours from the console API Key page.
- `--workspace-id` can be set via BAILIAN_WORKSPACE_ID env or `kscli config set workspace_id <id>`.
- `--query-history` passes prior conversation turns; the server rewrites the query based on context to improve retrieval relevance.
#### Examples
```bash
bl knowledge search --query "What is RAG?" --agent-id aid-xxx --workspace-id ws-xxx
```
```bash
bl knowledge search --api-key $DASHSCOPE_API_KEY --query "test search" --agent-id aid-xxx --workspace-id ws-xxx --image https://example.com/img.jpg
```
```bash
bl knowledge search --query "How does it work" --agent-id aid-xxx --workspace-id ws-xxx --query-history '[{"role":"user","content":"What is RAG"},{"role":"assistant","content":"RAG is retrieval-augmented generation"}]'
```