Skip to content

Commit 6317da8

Browse files
committed
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方法接口调用
1 parent 006ea23 commit 6317da8

6 files changed

Lines changed: 568 additions & 136 deletions

File tree

Lines changed: 225 additions & 101 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,21 @@
11
import {
22
defineCommand,
3+
knowledgeRetrieveEndpoint,
34
signRequest,
5+
requestJson,
46
detectOutputFormat,
57
maskToken,
8+
resolveCredential,
9+
trackingHeaders,
610
type Config,
711
type GlobalFlags,
812
type KnowledgeRetrieveRequest,
913
type KnowledgeRetrieveResponse,
14+
type DashScopeKnowledgeRetrieveRequest,
15+
type DashScopeKnowledgeRetrieveResponse,
16+
type OutputFormat,
1017
BailianError,
1118
ExitCode,
12-
trackingHeaders,
1319
} from "bailian-cli-core";
1420
import { failIfMissing } from "../../output/prompt.ts";
1521
import { emitResult, emitBare } from "../../output/output.ts";
@@ -18,24 +24,53 @@ const BAILIAN_HOST = "bailian.cn-beijing.aliyuncs.com";
1824

1925
export default defineCommand({
2026
name: "knowledge retrieve",
21-
description: "Retrieve from a Bailian knowledge base (requires AK/SK)",
27+
description: "Retrieve from a Bailian knowledge base",
2228
usage: "bl knowledge retrieve --index-id <id> --query <text> [flags]",
2329
options: [
2430
{ flag: "--index-id <id>", description: "Knowledge base index ID (required)", required: true },
2531
{ flag: "--query <text>", description: "Search query (required)", required: true },
2632
{
27-
flag: "--workspace-id <id>",
28-
description: "Bailian workspace ID (or env BAILIAN_WORKSPACE_ID)",
33+
flag: "--dense-similarity-top-k <n>",
34+
description: "Dense retrieval top K (API-KEY only)",
35+
type: "number",
2936
},
30-
{ flag: "--top-k <n>", description: "Number of results (default: 10)", type: "number" },
31-
{ flag: "--rerank", description: "Enable rerank" },
37+
{
38+
flag: "--sparse-similarity-top-k <n>",
39+
description: "Sparse retrieval top K (API-KEY only)",
40+
type: "number",
41+
},
42+
{ flag: "--rerank", description: "Enable reranking" },
3243
{ flag: "--rerank-top-n <n>", description: "Rerank top N results", type: "number" },
33-
{ flag: "--access-key-id <key>", description: "Alibaba Cloud Access Key ID (or env)" },
34-
{ flag: "--access-key-secret <key>", description: "Alibaba Cloud Access Key Secret (or env)" },
44+
{
45+
flag: "--rerank-model <name>",
46+
description: "Rerank model, e.g. qwen3-rerank-hybrid (API-KEY only)",
47+
},
48+
{
49+
flag: "--rerank-mode <mode>",
50+
description: "Rerank mode: qa, similar, or custom (API-KEY only)",
51+
},
52+
{
53+
flag: "--rerank-instruct <text>",
54+
description: "Custom rerank instruction, when mode=custom (API-KEY only)",
55+
},
56+
{
57+
flag: "--top-k <n>",
58+
description: "Number of results (deprecated, use --rerank-top-n)",
59+
type: "number",
60+
},
61+
{
62+
flag: "--workspace-id <id>",
63+
description: "Bailian workspace ID (required for AK/SK auth)",
64+
},
65+
{ flag: "--access-key-id <key>", description: "Alibaba Cloud Access Key ID (deprecated)" },
66+
{
67+
flag: "--access-key-secret <key>",
68+
description: "Alibaba Cloud Access Key Secret (deprecated)",
69+
},
3570
],
3671
examples: [
37-
'bl knowledge retrieve --index-id idx_xxx --query "如何使用阿里云百炼" --workspace-id ws_xxx',
38-
'bl knowledge retrieve --index-id idx_xxx --query "API限流" --top-k 5 --rerank',
72+
'bl knowledge retrieve --index-id idx_xxx --query "如何使用阿里云百炼"',
73+
'bl knowledge retrieve --index-id idx_xxx --query "API限流" --rerank --rerank-model qwen3-rerank-hybrid',
3974
],
4075
async run(config: Config, flags: GlobalFlags) {
4176
const indexId = flags.indexId as string;
@@ -44,112 +79,201 @@ export default defineCommand({
4479
const query = flags.query as string;
4580
if (!query) failIfMissing("query", "bl knowledge retrieve --index-id <id> --query <text>");
4681

47-
const accessKeyId = (flags.accessKeyId as string) || config.accessKeyId;
48-
const accessKeySecret = (flags.accessKeySecret as string) || config.accessKeySecret;
49-
const workspaceId = (flags.workspaceId as string) || config.workspaceId;
50-
51-
if (!accessKeyId || !accessKeySecret) {
52-
throw new BailianError(
53-
"Knowledge retrieve requires Alibaba Cloud AK/SK.\n" +
54-
"Set via: --access-key-id / --access-key-secret flags,\n" +
55-
" or env: ALIBABA_CLOUD_ACCESS_KEY_ID / ALIBABA_CLOUD_ACCESS_KEY_SECRET,\n" +
56-
" or config: bl config set access_key_id <key>",
57-
ExitCode.AUTH,
58-
);
82+
const format = detectOutputFormat(config.output);
83+
84+
// Determine auth: prefer API-KEY, fall back to AK/SK (deprecated)
85+
let useApiKey = false;
86+
try {
87+
await resolveCredential(config);
88+
useApiKey = true;
89+
} catch {
90+
// No API-KEY credential available
5991
}
6092

61-
if (!workspaceId) {
62-
throw new BailianError(
63-
"Knowledge retrieve requires a workspace ID.\n" +
64-
"Set via: --workspace-id flag, or env: BAILIAN_WORKSPACE_ID, or config: bl config set workspace_id <id>",
65-
ExitCode.USAGE,
66-
);
93+
if (useApiKey) {
94+
await runWithApiKey(config, flags, indexId, query, format);
95+
} else {
96+
await runWithAkSk(config, flags, indexId, query, format);
6797
}
98+
},
99+
});
100+
101+
// ---- API-KEY path (DashScope gateway, snake_case) ----
102+
103+
async function runWithApiKey(
104+
config: Config,
105+
flags: GlobalFlags,
106+
indexId: string,
107+
query: string,
108+
format: OutputFormat,
109+
): Promise<void> {
110+
if (flags.topK !== undefined && flags.rerankTopN === undefined) {
111+
process.stderr.write("Warning: --top-k is deprecated. Use --rerank-top-n instead.\n");
112+
flags.rerankTopN = flags.topK;
113+
}
68114

69-
const body: KnowledgeRetrieveRequest = {
70-
IndexId: indexId,
71-
Query: query,
115+
const body: DashScopeKnowledgeRetrieveRequest = {
116+
index_id: indexId,
117+
query,
118+
search_filters: [],
119+
};
120+
121+
if (flags.denseSimilarityTopK !== undefined)
122+
body.dense_similarity_top_k = flags.denseSimilarityTopK as number;
123+
if (flags.sparseSimilarityTopK !== undefined)
124+
body.sparse_similarity_top_k = flags.sparseSimilarityTopK as number;
125+
if (flags.rerank) body.enable_reranking = true;
126+
if (flags.rerankTopN !== undefined) body.rerank_top_n = flags.rerankTopN as number;
127+
128+
if (flags.rerankModel) {
129+
const rerankEntry: { model_name: string; rerank_mode?: string; rerank_instruct?: string } = {
130+
model_name: flags.rerankModel as string,
72131
};
132+
if (flags.rerankMode) rerankEntry.rerank_mode = flags.rerankMode as string;
133+
if (flags.rerankInstruct) rerankEntry.rerank_instruct = flags.rerankInstruct as string;
134+
body.rerank = [rerankEntry];
135+
}
73136

74-
if (flags.topK !== undefined) body.TopK = flags.topK as number;
75-
if (flags.rerank) body.Rerank = true;
76-
if (flags.rerankTopN !== undefined) body.RerankTopN = flags.rerankTopN as number;
137+
const url = knowledgeRetrieveEndpoint(config.baseUrl);
77138

78-
const format = detectOutputFormat(config.output);
79-
const pathname = `/${workspaceId}/index/retrieve`;
80-
81-
if (config.dryRun) {
82-
emitResult(
83-
{
84-
endpoint: `https://${BAILIAN_HOST}${pathname}`,
85-
workspaceId,
86-
request: body,
87-
},
88-
format,
89-
);
90-
return;
91-
}
139+
if (config.dryRun) {
140+
emitResult({ endpoint: url, request: body }, format);
141+
return;
142+
}
92143

93-
const bodyStr = JSON.stringify(body);
144+
const response = await requestJson<DashScopeKnowledgeRetrieveResponse>(config, {
145+
url,
146+
method: "POST",
147+
body,
148+
});
94149

95-
const headers = signRequest({
96-
accessKeyId,
97-
accessKeySecret,
98-
action: "Retrieve",
99-
version: "2023-12-29",
100-
body: bodyStr,
101-
host: BAILIAN_HOST,
102-
pathname,
103-
});
150+
const nodes = response.data?.nodes || [];
151+
if (config.quiet || format === "text") {
152+
emitTextNodes(nodes.map((n) => ({ text: n.text, score: n.score })));
153+
} else {
154+
emitResult(response, format);
155+
}
156+
}
104157

105-
const url = `https://${BAILIAN_HOST}${pathname}`;
158+
// ---- AK/SK path (Bailian OpenAPI gateway, PascalCase) ----
106159

107-
if (config.verbose) {
108-
process.stderr.write(`> POST ${url}\n`);
109-
process.stderr.write(`> AK: ${maskToken(accessKeyId)}\n`);
110-
}
160+
async function runWithAkSk(
161+
config: Config,
162+
flags: GlobalFlags,
163+
indexId: string,
164+
query: string,
165+
format: OutputFormat,
166+
): Promise<void> {
167+
const accessKeyId = (flags.accessKeyId as string) || config.accessKeyId;
168+
const accessKeySecret = (flags.accessKeySecret as string) || config.accessKeySecret;
169+
const workspaceId = (flags.workspaceId as string) || config.workspaceId;
170+
171+
if (!accessKeyId || !accessKeySecret) {
172+
throw new BailianError(
173+
"No credentials found.\n" +
174+
"Preferred: set DASHSCOPE_API_KEY or pass --api-key.\n" +
175+
"Legacy (deprecated): set ALIBABA_CLOUD_ACCESS_KEY_ID / ALIBABA_CLOUD_ACCESS_KEY_SECRET.",
176+
ExitCode.AUTH,
177+
);
178+
}
111179

112-
const timeoutMs = config.timeout * 1000;
113-
const res = await fetch(url, {
114-
method: "POST",
115-
headers: {
116-
...headers,
117-
...trackingHeaders(),
180+
if (!workspaceId) {
181+
throw new BailianError(
182+
"Knowledge retrieve requires a workspace ID.\n" +
183+
"Set via: --workspace-id flag, or env: BAILIAN_WORKSPACE_ID, or config: bl config set workspace_id <id>",
184+
ExitCode.USAGE,
185+
);
186+
}
187+
188+
process.stderr.write(
189+
"Warning: AK/SK auth for knowledge retrieve is deprecated. Prefer --api-key or DASHSCOPE_API_KEY.\n",
190+
);
191+
192+
const body: KnowledgeRetrieveRequest = {
193+
IndexId: indexId,
194+
Query: query,
195+
};
196+
197+
if (flags.topK !== undefined) body.TopK = flags.topK as number;
198+
if (flags.rerank) body.Rerank = true;
199+
if (flags.rerankTopN !== undefined) body.RerankTopN = flags.rerankTopN as number;
200+
201+
const pathname = `/${workspaceId}/index/retrieve`;
202+
203+
if (config.dryRun) {
204+
emitResult(
205+
{
206+
endpoint: `https://${BAILIAN_HOST}${pathname}`,
207+
workspaceId,
208+
request: body,
118209
},
119-
body: bodyStr,
120-
signal: AbortSignal.timeout(timeoutMs),
121-
});
210+
format,
211+
);
212+
return;
213+
}
122214

123-
if (config.verbose) {
124-
process.stderr.write(`< ${res.status} ${res.statusText}\n`);
125-
}
215+
const bodyStr = JSON.stringify(body);
126216

127-
const data = (await res.json()) as KnowledgeRetrieveResponse & {
128-
Code?: string;
129-
Message?: string;
130-
};
217+
const headers = signRequest({
218+
accessKeyId,
219+
accessKeySecret,
220+
action: "Retrieve",
221+
version: "2023-12-29",
222+
body: bodyStr,
223+
host: BAILIAN_HOST,
224+
pathname,
225+
});
131226

132-
if (!res.ok || (data.Code && data.Code !== "Success")) {
133-
throw new BailianError(
134-
`Knowledge retrieve failed: ${data.Code || res.status} - ${data.Message || res.statusText}`,
135-
ExitCode.GENERAL,
136-
);
137-
}
227+
const url = `https://${BAILIAN_HOST}${pathname}`;
138228

139-
if (config.quiet || format === "text") {
140-
const nodes = data.Data?.Nodes || [];
141-
if (nodes.length === 0) {
142-
emitBare("No results found.");
143-
} else {
144-
for (let i = 0; i < nodes.length; i++) {
145-
const node = nodes[i];
146-
emitBare(`[${i + 1}] (score: ${node.Score.toFixed(4)})`);
147-
emitBare(node.Text);
148-
emitBare("");
149-
}
150-
}
151-
} else {
152-
emitResult(data, format);
229+
if (config.verbose) {
230+
process.stderr.write(`> POST ${url}\n`);
231+
process.stderr.write(`> AK: ${maskToken(accessKeyId)}\n`);
232+
}
233+
234+
const timeoutMs = config.timeout * 1000;
235+
const res = await fetch(url, {
236+
method: "POST",
237+
headers: { ...headers, ...trackingHeaders() },
238+
body: bodyStr,
239+
signal: AbortSignal.timeout(timeoutMs),
240+
});
241+
242+
if (config.verbose) {
243+
process.stderr.write(`< ${res.status} ${res.statusText}\n`);
244+
}
245+
246+
const data = (await res.json()) as KnowledgeRetrieveResponse & {
247+
Code?: string;
248+
Message?: string;
249+
};
250+
251+
if (!res.ok || (data.Code && data.Code !== "Success")) {
252+
throw new BailianError(
253+
`Knowledge retrieve failed: ${data.Code || res.status} - ${data.Message || res.statusText}`,
254+
ExitCode.GENERAL,
255+
);
256+
}
257+
258+
const nodes = data.Data?.Nodes || [];
259+
if (config.quiet || format === "text") {
260+
emitTextNodes(nodes.map((n) => ({ text: n.Text, score: n.Score })));
261+
} else {
262+
emitResult(data, format);
263+
}
264+
}
265+
266+
// ---- Shared text output ----
267+
268+
function emitTextNodes(nodes: Array<{ text: string; score: number }>): void {
269+
if (nodes.length === 0) {
270+
emitBare("No results found.");
271+
} else {
272+
for (let i = 0; i < nodes.length; i++) {
273+
const node = nodes[i];
274+
emitBare(`[${i + 1}] (score: ${node.score.toFixed(4)})`);
275+
emitBare(node.text);
276+
emitBare("");
153277
}
154-
},
155-
});
278+
}
279+
}

packages/cli/tests/e2e/helpers.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -117,8 +117,17 @@ export function e2eLabelFromMetaUrl(metaUrl: string): string {
117117
return basename(fileURLToPath(metaUrl), ".ts").replace(/\.e2e\.test$/, "");
118118
}
119119

120-
/** 知识库用例:须显式索引 ID + AK/SK(workspace 可读 config / env,故不在此强制校验) */
120+
/** 知识库用例:须显式索引 ID + API-KEY 或 AK/SK */
121121
export function isKnowledgeE2EReady(): boolean {
122+
if (!isBailianE2EEnabled()) return false;
123+
if (!process.env.BAILIAN_E2E_INDEX_ID) return false;
124+
const hasApiKey = isDashScopeE2EReady();
125+
const hasAkSk =
126+
!!process.env.ALIBABA_CLOUD_ACCESS_KEY_ID && !!process.env.ALIBABA_CLOUD_ACCESS_KEY_SECRET;
127+
return hasApiKey || hasAkSk;
128+
}
129+
130+
export function isKnowledgeAkSkReady(): boolean {
122131
return (
123132
isBailianE2EEnabled() &&
124133
!!process.env.ALIBABA_CLOUD_ACCESS_KEY_ID &&

0 commit comments

Comments
 (0)