11import {
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" ;
1420import { failIfMissing } from "../../output/prompt.ts" ;
1521import { emitResult , emitBare } from "../../output/output.ts" ;
@@ -18,24 +24,53 @@ const BAILIAN_HOST = "bailian.cn-beijing.aliyuncs.com";
1824
1925export 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+ }
0 commit comments