mirror of
https://github.com/modelstudioai/cli.git
synced 2026-09-14 19:49:23 +08:00
feat(cli): 重构知识库检索命令,支持API-KEY和AK/SK鉴权
- 增加API-KEY鉴权路径,采用DashScope协议(snake_case)请求后端接口 - 保留AK/SK鉴权路径,但打印废弃警告,采用PascalCase请求后端 - 命令参数调整,新增dense-similarity-top-k、sparse-similarity-top-k等API-KEY专用选项 - 废弃部分旧参数如顶层top-k,提醒用户改用rerank-top-n - 统一输出格式以及静默模式下文本结果的打印逻辑优化 - 添加相关类型定义,完善请求与响应结构的类型支持 - CLI端增加dry-run模式,展示实际请求参数与地址 - E2E测试覆盖API-KEY和AK/SK两条路径,包含帮助提示、错误场景及关键参数测试 - 更新依赖的核心包导出与接口,新增knowledgeRetrieveEndpoint方法接口调用
This commit is contained in:
@@ -1,15 +1,21 @@
|
||||
import {
|
||||
defineCommand,
|
||||
knowledgeRetrieveEndpoint,
|
||||
signRequest,
|
||||
requestJson,
|
||||
detectOutputFormat,
|
||||
maskToken,
|
||||
resolveCredential,
|
||||
trackingHeaders,
|
||||
type Config,
|
||||
type GlobalFlags,
|
||||
type KnowledgeRetrieveRequest,
|
||||
type KnowledgeRetrieveResponse,
|
||||
type DashScopeKnowledgeRetrieveRequest,
|
||||
type DashScopeKnowledgeRetrieveResponse,
|
||||
type OutputFormat,
|
||||
BailianError,
|
||||
ExitCode,
|
||||
trackingHeaders,
|
||||
} from "bailian-cli-core";
|
||||
import { failIfMissing } from "../../output/prompt.ts";
|
||||
import { emitResult, emitBare } from "../../output/output.ts";
|
||||
@@ -18,24 +24,53 @@ const BAILIAN_HOST = "bailian.cn-beijing.aliyuncs.com";
|
||||
|
||||
export default defineCommand({
|
||||
name: "knowledge retrieve",
|
||||
description: "Retrieve from a Bailian knowledge base (requires AK/SK)",
|
||||
description: "Retrieve from a Bailian knowledge base",
|
||||
usage: "bl knowledge retrieve --index-id <id> --query <text> [flags]",
|
||||
options: [
|
||||
{ flag: "--index-id <id>", description: "Knowledge base index ID (required)", required: true },
|
||||
{ flag: "--query <text>", description: "Search query (required)", required: true },
|
||||
{
|
||||
flag: "--workspace-id <id>",
|
||||
description: "Bailian workspace ID (or env BAILIAN_WORKSPACE_ID)",
|
||||
flag: "--dense-similarity-top-k <n>",
|
||||
description: "Dense retrieval top K (API-KEY only)",
|
||||
type: "number",
|
||||
},
|
||||
{ flag: "--top-k <n>", description: "Number of results (default: 10)", type: "number" },
|
||||
{ flag: "--rerank", description: "Enable rerank" },
|
||||
{
|
||||
flag: "--sparse-similarity-top-k <n>",
|
||||
description: "Sparse retrieval top K (API-KEY only)",
|
||||
type: "number",
|
||||
},
|
||||
{ flag: "--rerank", description: "Enable reranking" },
|
||||
{ flag: "--rerank-top-n <n>", description: "Rerank top N results", type: "number" },
|
||||
{ flag: "--access-key-id <key>", description: "Alibaba Cloud Access Key ID (or env)" },
|
||||
{ flag: "--access-key-secret <key>", description: "Alibaba Cloud Access Key Secret (or env)" },
|
||||
{
|
||||
flag: "--rerank-model <name>",
|
||||
description: "Rerank model, e.g. qwen3-rerank-hybrid (API-KEY only)",
|
||||
},
|
||||
{
|
||||
flag: "--rerank-mode <mode>",
|
||||
description: "Rerank mode: qa, similar, or custom (API-KEY only)",
|
||||
},
|
||||
{
|
||||
flag: "--rerank-instruct <text>",
|
||||
description: "Custom rerank instruction, when mode=custom (API-KEY only)",
|
||||
},
|
||||
{
|
||||
flag: "--top-k <n>",
|
||||
description: "Number of results (deprecated, use --rerank-top-n)",
|
||||
type: "number",
|
||||
},
|
||||
{
|
||||
flag: "--workspace-id <id>",
|
||||
description: "Bailian workspace ID (required for AK/SK auth)",
|
||||
},
|
||||
{ flag: "--access-key-id <key>", description: "Alibaba Cloud Access Key ID (deprecated)" },
|
||||
{
|
||||
flag: "--access-key-secret <key>",
|
||||
description: "Alibaba Cloud Access Key Secret (deprecated)",
|
||||
},
|
||||
],
|
||||
examples: [
|
||||
'bl knowledge retrieve --index-id idx_xxx --query "如何使用阿里云百炼" --workspace-id ws_xxx',
|
||||
'bl knowledge retrieve --index-id idx_xxx --query "API限流" --top-k 5 --rerank',
|
||||
'bl knowledge retrieve --index-id idx_xxx --query "如何使用阿里云百炼"',
|
||||
'bl knowledge retrieve --index-id idx_xxx --query "API限流" --rerank --rerank-model qwen3-rerank-hybrid',
|
||||
],
|
||||
async run(config: Config, flags: GlobalFlags) {
|
||||
const indexId = flags.indexId as string;
|
||||
@@ -44,112 +79,201 @@ export default defineCommand({
|
||||
const query = flags.query as string;
|
||||
if (!query) failIfMissing("query", "bl knowledge retrieve --index-id <id> --query <text>");
|
||||
|
||||
const accessKeyId = (flags.accessKeyId as string) || config.accessKeyId;
|
||||
const accessKeySecret = (flags.accessKeySecret as string) || config.accessKeySecret;
|
||||
const workspaceId = (flags.workspaceId as string) || config.workspaceId;
|
||||
|
||||
if (!accessKeyId || !accessKeySecret) {
|
||||
throw new BailianError(
|
||||
"Knowledge retrieve requires Alibaba Cloud AK/SK.\n" +
|
||||
"Set via: --access-key-id / --access-key-secret flags,\n" +
|
||||
" or env: ALIBABA_CLOUD_ACCESS_KEY_ID / ALIBABA_CLOUD_ACCESS_KEY_SECRET,\n" +
|
||||
" or config: bl config set access_key_id <key>",
|
||||
ExitCode.AUTH,
|
||||
);
|
||||
}
|
||||
|
||||
if (!workspaceId) {
|
||||
throw new BailianError(
|
||||
"Knowledge retrieve requires a workspace ID.\n" +
|
||||
"Set via: --workspace-id flag, or env: BAILIAN_WORKSPACE_ID, or config: bl config set workspace_id <id>",
|
||||
ExitCode.USAGE,
|
||||
);
|
||||
}
|
||||
|
||||
const body: KnowledgeRetrieveRequest = {
|
||||
IndexId: indexId,
|
||||
Query: query,
|
||||
};
|
||||
|
||||
if (flags.topK !== undefined) body.TopK = flags.topK as number;
|
||||
if (flags.rerank) body.Rerank = true;
|
||||
if (flags.rerankTopN !== undefined) body.RerankTopN = flags.rerankTopN as number;
|
||||
|
||||
const format = detectOutputFormat(config.output);
|
||||
const pathname = `/${workspaceId}/index/retrieve`;
|
||||
|
||||
if (config.dryRun) {
|
||||
emitResult(
|
||||
{
|
||||
endpoint: `https://${BAILIAN_HOST}${pathname}`,
|
||||
workspaceId,
|
||||
request: body,
|
||||
},
|
||||
format,
|
||||
);
|
||||
return;
|
||||
// Determine auth: prefer API-KEY, fall back to AK/SK (deprecated)
|
||||
let useApiKey = false;
|
||||
try {
|
||||
await resolveCredential(config);
|
||||
useApiKey = true;
|
||||
} catch {
|
||||
// No API-KEY credential available
|
||||
}
|
||||
|
||||
const bodyStr = JSON.stringify(body);
|
||||
|
||||
const headers = signRequest({
|
||||
accessKeyId,
|
||||
accessKeySecret,
|
||||
action: "Retrieve",
|
||||
version: "2023-12-29",
|
||||
body: bodyStr,
|
||||
host: BAILIAN_HOST,
|
||||
pathname,
|
||||
});
|
||||
|
||||
const url = `https://${BAILIAN_HOST}${pathname}`;
|
||||
|
||||
if (config.verbose) {
|
||||
process.stderr.write(`> POST ${url}\n`);
|
||||
process.stderr.write(`> AK: ${maskToken(accessKeyId)}\n`);
|
||||
}
|
||||
|
||||
const timeoutMs = config.timeout * 1000;
|
||||
const res = await fetch(url, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
...headers,
|
||||
...trackingHeaders(),
|
||||
},
|
||||
body: bodyStr,
|
||||
signal: AbortSignal.timeout(timeoutMs),
|
||||
});
|
||||
|
||||
if (config.verbose) {
|
||||
process.stderr.write(`< ${res.status} ${res.statusText}\n`);
|
||||
}
|
||||
|
||||
const data = (await res.json()) as KnowledgeRetrieveResponse & {
|
||||
Code?: string;
|
||||
Message?: string;
|
||||
};
|
||||
|
||||
if (!res.ok || (data.Code && data.Code !== "Success")) {
|
||||
throw new BailianError(
|
||||
`Knowledge retrieve failed: ${data.Code || res.status} - ${data.Message || res.statusText}`,
|
||||
ExitCode.GENERAL,
|
||||
);
|
||||
}
|
||||
|
||||
if (config.quiet || format === "text") {
|
||||
const nodes = data.Data?.Nodes || [];
|
||||
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("");
|
||||
}
|
||||
}
|
||||
if (useApiKey) {
|
||||
await runWithApiKey(config, flags, indexId, query, format);
|
||||
} else {
|
||||
emitResult(data, format);
|
||||
await runWithAkSk(config, flags, indexId, query, format);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
// ---- API-KEY path (DashScope gateway, snake_case) ----
|
||||
|
||||
async function runWithApiKey(
|
||||
config: Config,
|
||||
flags: GlobalFlags,
|
||||
indexId: string,
|
||||
query: string,
|
||||
format: OutputFormat,
|
||||
): Promise<void> {
|
||||
if (flags.topK !== undefined && flags.rerankTopN === undefined) {
|
||||
process.stderr.write("Warning: --top-k is deprecated. Use --rerank-top-n instead.\n");
|
||||
flags.rerankTopN = flags.topK;
|
||||
}
|
||||
|
||||
const body: DashScopeKnowledgeRetrieveRequest = {
|
||||
index_id: indexId,
|
||||
query,
|
||||
search_filters: [],
|
||||
};
|
||||
|
||||
if (flags.denseSimilarityTopK !== undefined)
|
||||
body.dense_similarity_top_k = flags.denseSimilarityTopK as number;
|
||||
if (flags.sparseSimilarityTopK !== undefined)
|
||||
body.sparse_similarity_top_k = flags.sparseSimilarityTopK as number;
|
||||
if (flags.rerank) body.enable_reranking = true;
|
||||
if (flags.rerankTopN !== undefined) body.rerank_top_n = flags.rerankTopN as number;
|
||||
|
||||
if (flags.rerankModel) {
|
||||
const rerankEntry: { model_name: string; rerank_mode?: string; rerank_instruct?: string } = {
|
||||
model_name: flags.rerankModel as string,
|
||||
};
|
||||
if (flags.rerankMode) rerankEntry.rerank_mode = flags.rerankMode as string;
|
||||
if (flags.rerankInstruct) rerankEntry.rerank_instruct = flags.rerankInstruct as string;
|
||||
body.rerank = [rerankEntry];
|
||||
}
|
||||
|
||||
const url = knowledgeRetrieveEndpoint(config.baseUrl);
|
||||
|
||||
if (config.dryRun) {
|
||||
emitResult({ endpoint: url, request: body }, format);
|
||||
return;
|
||||
}
|
||||
|
||||
const response = await requestJson<DashScopeKnowledgeRetrieveResponse>(config, {
|
||||
url,
|
||||
method: "POST",
|
||||
body,
|
||||
});
|
||||
|
||||
const nodes = response.data?.nodes || [];
|
||||
if (config.quiet || format === "text") {
|
||||
emitTextNodes(nodes.map((n) => ({ text: n.text, score: n.score })));
|
||||
} else {
|
||||
emitResult(response, format);
|
||||
}
|
||||
}
|
||||
|
||||
// ---- AK/SK path (Bailian OpenAPI gateway, PascalCase) ----
|
||||
|
||||
async function runWithAkSk(
|
||||
config: Config,
|
||||
flags: GlobalFlags,
|
||||
indexId: string,
|
||||
query: string,
|
||||
format: OutputFormat,
|
||||
): Promise<void> {
|
||||
const accessKeyId = (flags.accessKeyId as string) || config.accessKeyId;
|
||||
const accessKeySecret = (flags.accessKeySecret as string) || config.accessKeySecret;
|
||||
const workspaceId = (flags.workspaceId as string) || config.workspaceId;
|
||||
|
||||
if (!accessKeyId || !accessKeySecret) {
|
||||
throw new BailianError(
|
||||
"No credentials found.\n" +
|
||||
"Preferred: set DASHSCOPE_API_KEY or pass --api-key.\n" +
|
||||
"Legacy (deprecated): set ALIBABA_CLOUD_ACCESS_KEY_ID / ALIBABA_CLOUD_ACCESS_KEY_SECRET.",
|
||||
ExitCode.AUTH,
|
||||
);
|
||||
}
|
||||
|
||||
if (!workspaceId) {
|
||||
throw new BailianError(
|
||||
"Knowledge retrieve requires a workspace ID.\n" +
|
||||
"Set via: --workspace-id flag, or env: BAILIAN_WORKSPACE_ID, or config: bl config set workspace_id <id>",
|
||||
ExitCode.USAGE,
|
||||
);
|
||||
}
|
||||
|
||||
process.stderr.write(
|
||||
"Warning: AK/SK auth for knowledge retrieve is deprecated. Prefer --api-key or DASHSCOPE_API_KEY.\n",
|
||||
);
|
||||
|
||||
const body: KnowledgeRetrieveRequest = {
|
||||
IndexId: indexId,
|
||||
Query: query,
|
||||
};
|
||||
|
||||
if (flags.topK !== undefined) body.TopK = flags.topK as number;
|
||||
if (flags.rerank) body.Rerank = true;
|
||||
if (flags.rerankTopN !== undefined) body.RerankTopN = flags.rerankTopN as number;
|
||||
|
||||
const pathname = `/${workspaceId}/index/retrieve`;
|
||||
|
||||
if (config.dryRun) {
|
||||
emitResult(
|
||||
{
|
||||
endpoint: `https://${BAILIAN_HOST}${pathname}`,
|
||||
workspaceId,
|
||||
request: body,
|
||||
},
|
||||
format,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const bodyStr = JSON.stringify(body);
|
||||
|
||||
const headers = signRequest({
|
||||
accessKeyId,
|
||||
accessKeySecret,
|
||||
action: "Retrieve",
|
||||
version: "2023-12-29",
|
||||
body: bodyStr,
|
||||
host: BAILIAN_HOST,
|
||||
pathname,
|
||||
});
|
||||
|
||||
const url = `https://${BAILIAN_HOST}${pathname}`;
|
||||
|
||||
if (config.verbose) {
|
||||
process.stderr.write(`> POST ${url}\n`);
|
||||
process.stderr.write(`> AK: ${maskToken(accessKeyId)}\n`);
|
||||
}
|
||||
|
||||
const timeoutMs = config.timeout * 1000;
|
||||
const res = await fetch(url, {
|
||||
method: "POST",
|
||||
headers: { ...headers, ...trackingHeaders() },
|
||||
body: bodyStr,
|
||||
signal: AbortSignal.timeout(timeoutMs),
|
||||
});
|
||||
|
||||
if (config.verbose) {
|
||||
process.stderr.write(`< ${res.status} ${res.statusText}\n`);
|
||||
}
|
||||
|
||||
const data = (await res.json()) as KnowledgeRetrieveResponse & {
|
||||
Code?: string;
|
||||
Message?: string;
|
||||
};
|
||||
|
||||
if (!res.ok || (data.Code && data.Code !== "Success")) {
|
||||
throw new BailianError(
|
||||
`Knowledge retrieve failed: ${data.Code || res.status} - ${data.Message || res.statusText}`,
|
||||
ExitCode.GENERAL,
|
||||
);
|
||||
}
|
||||
|
||||
const nodes = data.Data?.Nodes || [];
|
||||
if (config.quiet || format === "text") {
|
||||
emitTextNodes(nodes.map((n) => ({ text: n.Text, score: n.Score })));
|
||||
} else {
|
||||
emitResult(data, format);
|
||||
}
|
||||
}
|
||||
|
||||
// ---- Shared text output ----
|
||||
|
||||
function emitTextNodes(nodes: Array<{ text: string; score: number }>): void {
|
||||
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("");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -117,8 +117,17 @@ export function e2eLabelFromMetaUrl(metaUrl: string): string {
|
||||
return basename(fileURLToPath(metaUrl), ".ts").replace(/\.e2e\.test$/, "");
|
||||
}
|
||||
|
||||
/** 知识库用例:须显式索引 ID + AK/SK(workspace 可读 config / env,故不在此强制校验) */
|
||||
/** 知识库用例:须显式索引 ID + API-KEY 或 AK/SK */
|
||||
export function isKnowledgeE2EReady(): boolean {
|
||||
if (!isBailianE2EEnabled()) return false;
|
||||
if (!process.env.BAILIAN_E2E_INDEX_ID) return false;
|
||||
const hasApiKey = isDashScopeE2EReady();
|
||||
const hasAkSk =
|
||||
!!process.env.ALIBABA_CLOUD_ACCESS_KEY_ID && !!process.env.ALIBABA_CLOUD_ACCESS_KEY_SECRET;
|
||||
return hasApiKey || hasAkSk;
|
||||
}
|
||||
|
||||
export function isKnowledgeAkSkReady(): boolean {
|
||||
return (
|
||||
isBailianE2EEnabled() &&
|
||||
!!process.env.ALIBABA_CLOUD_ACCESS_KEY_ID &&
|
||||
|
||||
@@ -1,53 +1,316 @@
|
||||
import { join } from "path";
|
||||
import { tmpdir } from "os";
|
||||
import { describe, expect, test } from "vite-plus/test";
|
||||
import {
|
||||
isBailianE2EEnabled,
|
||||
isKnowledgeE2EReady,
|
||||
monorepoRoot,
|
||||
parseStdoutJson,
|
||||
runCli,
|
||||
} from "./helpers.ts";
|
||||
import { isDashScopeE2EReady, isKnowledgeAkSkReady, parseStdoutJson, runCli } from "./helpers.ts";
|
||||
|
||||
// 已开启 E2E 但 AK/SK、索引等未齐时提醒配置根目录 .env(否则本文件整组 describe 会被 skip)
|
||||
if (isBailianE2EEnabled() && !isKnowledgeE2EReady()) {
|
||||
const envFile = join(monorepoRoot(), ".env");
|
||||
console.warn(
|
||||
[
|
||||
"[e2e:knowledge] 知识库检索需要 RAM 的 AK/SK、索引 ID,以及工作空间 ID;当前未就绪,本组用例将被跳过。",
|
||||
`请在 monorepo 根目录的 .env 中配置(${envFile}):`,
|
||||
" ALIBABA_CLOUD_ACCESS_KEY_ID",
|
||||
" ALIBABA_CLOUD_ACCESS_KEY_SECRET",
|
||||
" BAILIAN_E2E_INDEX_ID",
|
||||
" BAILIAN_WORKSPACE_ID(也可执行: bl config set workspace_id <工作空间 id>)",
|
||||
].join("\n"),
|
||||
);
|
||||
// ---- Types ----
|
||||
|
||||
interface ApiKeyRetrieveBody {
|
||||
request_id?: string;
|
||||
data?: { total?: number; nodes?: Array<{ text: string; score: number }> };
|
||||
code?: string;
|
||||
message?: string;
|
||||
}
|
||||
|
||||
interface KnowledgeRetrieveBody {
|
||||
interface AkSkRetrieveBody {
|
||||
Success?: boolean;
|
||||
Code?: string;
|
||||
Data?: { Nodes?: unknown[] };
|
||||
Data?: { Nodes?: Array<{ Text: string; Score: number }> };
|
||||
}
|
||||
|
||||
/** 知识库检索(需 AK/SK + workspace + 索引;未就绪则整组跳过) */
|
||||
describe.skipIf(!isKnowledgeE2EReady())("e2e: knowledge retrieve", () => {
|
||||
test("知识库检索", async () => {
|
||||
const indexId = process.env.BAILIAN_E2E_INDEX_ID!;
|
||||
const { stdout, stderr, exitCode } = await runCli([
|
||||
interface DryRunBody {
|
||||
endpoint?: string;
|
||||
request?: {
|
||||
index_id?: string;
|
||||
query?: string;
|
||||
search_filters?: unknown[];
|
||||
rerank_top_n?: number;
|
||||
enable_reranking?: boolean;
|
||||
dense_similarity_top_k?: number;
|
||||
sparse_similarity_top_k?: number;
|
||||
rerank?: Array<{ model_name?: string; rerank_mode?: string; rerank_instruct?: string }>;
|
||||
};
|
||||
}
|
||||
|
||||
// ---- Help & missing args (no credentials needed) ----
|
||||
|
||||
describe("e2e: knowledge retrieve", () => {
|
||||
test("knowledge 分组展示子命令帮助且成功退出", async () => {
|
||||
const { stdout, stderr, exitCode } = await runCli(["knowledge"]);
|
||||
expect(exitCode, stderr).toBe(0);
|
||||
const out = `${stdout}\n${stderr}`;
|
||||
expect(out).toMatch(/knowledge|retrieve/i);
|
||||
});
|
||||
|
||||
test("knowledge retrieve --help 正常退出", async () => {
|
||||
const { stderr, exitCode } = await runCli(["knowledge", "retrieve", "--help"]);
|
||||
expect(exitCode, stderr).toBe(0);
|
||||
expect(stderr).toMatch(/--index-id/i);
|
||||
expect(stderr).toMatch(/--query/i);
|
||||
expect(stderr).toMatch(/--rerank-top-n/i);
|
||||
expect(stderr).toMatch(/deprecated/i);
|
||||
expect(stderr).toMatch(/API-KEY only/i);
|
||||
});
|
||||
|
||||
test("缺少 --index-id 时打印帮助并退出 (0)", async () => {
|
||||
const { stderr, exitCode } = await runCli([
|
||||
"knowledge",
|
||||
"retrieve",
|
||||
"--query",
|
||||
"test",
|
||||
"--non-interactive",
|
||||
]);
|
||||
expect(exitCode).toBe(0);
|
||||
expect(stderr).toMatch(/--index-id|Usage:/i);
|
||||
});
|
||||
|
||||
test("缺少 --query 时打印帮助并退出 (0)", async () => {
|
||||
const { stderr, exitCode } = await runCli([
|
||||
"knowledge",
|
||||
"retrieve",
|
||||
"--index-id",
|
||||
indexId,
|
||||
"idx_test",
|
||||
"--non-interactive",
|
||||
]);
|
||||
expect(exitCode).toBe(0);
|
||||
expect(stderr).toMatch(/--query|Usage:/i);
|
||||
});
|
||||
});
|
||||
|
||||
// ---- Error scenarios (no real credentials needed) ----
|
||||
|
||||
describe("e2e: knowledge retrieve errors", () => {
|
||||
test("无任何凭证时提示 No credentials found 并非零退出", async () => {
|
||||
const { stderr, exitCode } = await runCli(
|
||||
[
|
||||
"knowledge",
|
||||
"retrieve",
|
||||
"--index-id",
|
||||
"idx_test",
|
||||
"--query",
|
||||
"test",
|
||||
"--non-interactive",
|
||||
"--output",
|
||||
"json",
|
||||
],
|
||||
{
|
||||
DASHSCOPE_API_KEY: undefined,
|
||||
DASHSCOPE_ACCESS_TOKEN: undefined,
|
||||
ALIBABA_CLOUD_ACCESS_KEY_ID: undefined,
|
||||
ALIBABA_CLOUD_ACCESS_KEY_SECRET: undefined,
|
||||
BAILIAN_CONFIG_DIR: tmpdir(),
|
||||
},
|
||||
);
|
||||
expect(exitCode).not.toBe(0);
|
||||
expect(stderr).toMatch(/no credentials found/i);
|
||||
});
|
||||
});
|
||||
|
||||
// ---- Dry-run (no real credentials needed) ----
|
||||
|
||||
describe("e2e: knowledge retrieve dry-run", () => {
|
||||
test("--dry-run 输出 endpoint 和 snake_case body", async () => {
|
||||
const { stdout, stderr, exitCode } = await runCli(
|
||||
[
|
||||
"knowledge",
|
||||
"retrieve",
|
||||
"--dry-run",
|
||||
"--index-id",
|
||||
"idx_test",
|
||||
"--query",
|
||||
"hello",
|
||||
"--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(/api\/v1\/indices\/rag\/index\/retrieve/);
|
||||
expect(data.request?.index_id).toBe("idx_test");
|
||||
expect(data.request?.query).toBe("hello");
|
||||
});
|
||||
|
||||
test("--dry-run + --top-k 转发到 rerank_top_n 并输出废弃警告", async () => {
|
||||
const { stdout, stderr, exitCode } = await runCli(
|
||||
[
|
||||
"knowledge",
|
||||
"retrieve",
|
||||
"--dry-run",
|
||||
"--index-id",
|
||||
"idx_test",
|
||||
"--query",
|
||||
"hello",
|
||||
"--top-k",
|
||||
"5",
|
||||
"--non-interactive",
|
||||
"--output",
|
||||
"json",
|
||||
],
|
||||
{ DASHSCOPE_API_KEY: "sk-fake-for-dryrun" },
|
||||
);
|
||||
expect(exitCode, stderr).toBe(0);
|
||||
expect(stderr).toMatch(/--top-k.*deprecated/i);
|
||||
const data = parseStdoutJson<DryRunBody>(stdout);
|
||||
expect(data.request?.rerank_top_n).toBe(5);
|
||||
});
|
||||
|
||||
test("--dry-run + --rerank-top-n 优先于 --top-k", async () => {
|
||||
const { stdout, stderr, exitCode } = await runCli(
|
||||
[
|
||||
"knowledge",
|
||||
"retrieve",
|
||||
"--dry-run",
|
||||
"--index-id",
|
||||
"idx_test",
|
||||
"--query",
|
||||
"hello",
|
||||
"--top-k",
|
||||
"5",
|
||||
"--rerank-top-n",
|
||||
"10",
|
||||
"--non-interactive",
|
||||
"--output",
|
||||
"json",
|
||||
],
|
||||
{ DASHSCOPE_API_KEY: "sk-fake-for-dryrun" },
|
||||
);
|
||||
expect(exitCode, stderr).toBe(0);
|
||||
const data = parseStdoutJson<DryRunBody>(stdout);
|
||||
expect(data.request?.rerank_top_n).toBe(10);
|
||||
});
|
||||
|
||||
test("--dry-run + rerank 参数完整输出", async () => {
|
||||
const { stdout, stderr, exitCode } = await runCli(
|
||||
[
|
||||
"knowledge",
|
||||
"retrieve",
|
||||
"--dry-run",
|
||||
"--index-id",
|
||||
"idx_test",
|
||||
"--query",
|
||||
"hello",
|
||||
"--rerank",
|
||||
"--rerank-model",
|
||||
"qwen3-rerank-hybrid",
|
||||
"--rerank-mode",
|
||||
"custom",
|
||||
"--rerank-instruct",
|
||||
"按相关性排序",
|
||||
"--dense-similarity-top-k",
|
||||
"100",
|
||||
"--sparse-similarity-top-k",
|
||||
"50",
|
||||
"--non-interactive",
|
||||
"--output",
|
||||
"json",
|
||||
],
|
||||
{ DASHSCOPE_API_KEY: "sk-fake-for-dryrun" },
|
||||
);
|
||||
expect(exitCode, stderr).toBe(0);
|
||||
const data = parseStdoutJson<DryRunBody>(stdout);
|
||||
expect(data.request?.enable_reranking).toBe(true);
|
||||
expect(data.request?.dense_similarity_top_k).toBe(100);
|
||||
expect(data.request?.sparse_similarity_top_k).toBe(50);
|
||||
expect(data.request?.rerank?.[0]?.model_name).toBe("qwen3-rerank-hybrid");
|
||||
expect(data.request?.rerank?.[0]?.rerank_mode).toBe("custom");
|
||||
expect(data.request?.rerank?.[0]?.rerank_instruct).toBe("按相关性排序");
|
||||
});
|
||||
});
|
||||
|
||||
// ---- API-KEY path (real network call) ----
|
||||
|
||||
describe.skipIf(!isDashScopeE2EReady() || !process.env.BAILIAN_E2E_INDEX_ID)(
|
||||
"e2e: knowledge retrieve (API-KEY)",
|
||||
() => {
|
||||
test("API-KEY 知识库检索", async () => {
|
||||
const indexId = process.env.BAILIAN_E2E_INDEX_ID!;
|
||||
const { stdout, stderr, exitCode } = await runCli([
|
||||
"knowledge",
|
||||
"retrieve",
|
||||
"--index-id",
|
||||
indexId,
|
||||
"--query",
|
||||
"端到端检索测试",
|
||||
"--rerank-top-n",
|
||||
"3",
|
||||
"--non-interactive",
|
||||
"--output",
|
||||
"json",
|
||||
]);
|
||||
expect(exitCode, stderr).toBe(0);
|
||||
const data = parseStdoutJson<ApiKeyRetrieveBody>(stdout);
|
||||
expect(Array.isArray(data.data?.nodes)).toBe(true);
|
||||
}, 120_000);
|
||||
},
|
||||
);
|
||||
|
||||
// ---- API-KEY error paths (real network call) ----
|
||||
|
||||
describe.skipIf(!isDashScopeE2EReady())("e2e: knowledge retrieve API-KEY errors", () => {
|
||||
test("无效 API-KEY 返回认证错误", async () => {
|
||||
const { stderr, exitCode } = await runCli([
|
||||
"knowledge",
|
||||
"retrieve",
|
||||
"--api-key",
|
||||
"sk-invalid-key-for-test",
|
||||
"--index-id",
|
||||
"idx_test",
|
||||
"--query",
|
||||
"端到端检索测试",
|
||||
"--top-k",
|
||||
"3",
|
||||
"test",
|
||||
"--non-interactive",
|
||||
"--output",
|
||||
"json",
|
||||
]);
|
||||
expect(exitCode).not.toBe(0);
|
||||
expect(stderr).toMatch(/InvalidApiKey|401|auth/i);
|
||||
}, 30_000);
|
||||
|
||||
test("无效 index_id 返回索引不存在错误", async () => {
|
||||
const { stderr, exitCode } = await runCli([
|
||||
"knowledge",
|
||||
"retrieve",
|
||||
"--index-id",
|
||||
"idx_nonexistent_test",
|
||||
"--query",
|
||||
"test",
|
||||
"--non-interactive",
|
||||
"--output",
|
||||
"json",
|
||||
]);
|
||||
expect(exitCode).not.toBe(0);
|
||||
expect(stderr).toMatch(/IndexNotExist|not exist|400/i);
|
||||
}, 30_000);
|
||||
});
|
||||
|
||||
// ---- AK/SK deprecated path (real network call) ----
|
||||
|
||||
describe.skipIf(!isKnowledgeAkSkReady())("e2e: knowledge retrieve (AK/SK deprecated)", () => {
|
||||
test("AK/SK 知识库检索输出废弃警告", async () => {
|
||||
const indexId = process.env.BAILIAN_E2E_INDEX_ID!;
|
||||
const { stdout, stderr, exitCode } = await runCli(
|
||||
[
|
||||
"knowledge",
|
||||
"retrieve",
|
||||
"--index-id",
|
||||
indexId,
|
||||
"--query",
|
||||
"端到端检索测试",
|
||||
"--rerank-top-n",
|
||||
"3",
|
||||
"--non-interactive",
|
||||
"--output",
|
||||
"json",
|
||||
],
|
||||
{
|
||||
DASHSCOPE_API_KEY: undefined,
|
||||
DASHSCOPE_ACCESS_TOKEN: undefined,
|
||||
BAILIAN_CONFIG_DIR: tmpdir(),
|
||||
},
|
||||
);
|
||||
expect(exitCode, stderr).toBe(0);
|
||||
const data = parseStdoutJson<KnowledgeRetrieveBody>(stdout);
|
||||
expect(stderr).toMatch(/deprecated/i);
|
||||
const data = parseStdoutJson<AkSkRetrieveBody>(stdout);
|
||||
const ok = data.Success === true || data.Code === "Success";
|
||||
expect(ok).toBe(true);
|
||||
expect(Array.isArray(data.Data?.Nodes)).toBe(true);
|
||||
|
||||
@@ -73,6 +73,12 @@ export function userProfileEndpoint(baseUrl: string, schemaId: string): string {
|
||||
return `${baseUrl}/api/v2/apps/memory/profile_schemas/${encodeURIComponent(schemaId)}/profiles`;
|
||||
}
|
||||
|
||||
// ---- Knowledge Base Retrieve (DashScope) ----
|
||||
|
||||
export function knowledgeRetrieveEndpoint(baseUrl: string): string {
|
||||
return `${baseUrl}/api/v1/indices/rag/index/retrieve`;
|
||||
}
|
||||
|
||||
// ---- MCP Services (Streamable HTTP) ----
|
||||
|
||||
export function mcpWebSearchEndpoint(baseUrl: string): string {
|
||||
|
||||
@@ -5,6 +5,7 @@ export {
|
||||
chatEndpoint,
|
||||
imageEndpoint,
|
||||
imageSyncEndpoint,
|
||||
knowledgeRetrieveEndpoint,
|
||||
memoryAddEndpoint,
|
||||
memoryListEndpoint,
|
||||
memoryNodeEndpoint,
|
||||
|
||||
@@ -384,6 +384,35 @@ export interface KnowledgeRetrieveResponse {
|
||||
};
|
||||
}
|
||||
|
||||
// ---- Knowledge Retrieve (DashScope protocol — snake_case) ----
|
||||
|
||||
export interface DashScopeKnowledgeRetrieveRequest {
|
||||
index_id: string;
|
||||
query: string;
|
||||
search_filters?: Array<Record<string, unknown>>;
|
||||
dense_similarity_top_k?: number;
|
||||
sparse_similarity_top_k?: number;
|
||||
enable_reranking?: boolean;
|
||||
rerank_top_n?: number;
|
||||
rerank?: Array<{
|
||||
model_name: string;
|
||||
rerank_mode?: string;
|
||||
rerank_instruct?: string;
|
||||
}>;
|
||||
}
|
||||
|
||||
export interface DashScopeKnowledgeRetrieveResponse {
|
||||
request_id: string;
|
||||
data: {
|
||||
total: number;
|
||||
nodes: Array<{
|
||||
text: string;
|
||||
score: number;
|
||||
metadata: Record<string, unknown>;
|
||||
}>;
|
||||
};
|
||||
}
|
||||
|
||||
// ---- Speech Synthesis / TTS (DashScope) ----
|
||||
|
||||
export interface DashScopeTTSRequest {
|
||||
|
||||
Reference in New Issue
Block a user