diff --git a/packages/cli/src/commands/knowledge/retrieve.ts b/packages/cli/src/commands/knowledge/retrieve.ts index 37dd073..91f235d 100644 --- a/packages/cli/src/commands/knowledge/retrieve.ts +++ b/packages/cli/src/commands/knowledge/retrieve.ts @@ -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 --query [flags]", options: [ { flag: "--index-id ", description: "Knowledge base index ID (required)", required: true }, { flag: "--query ", description: "Search query (required)", required: true }, { - flag: "--workspace-id ", - description: "Bailian workspace ID (or env BAILIAN_WORKSPACE_ID)", + flag: "--dense-similarity-top-k ", + description: "Dense retrieval top K (API-KEY only)", + type: "number", }, - { flag: "--top-k ", description: "Number of results (default: 10)", type: "number" }, - { flag: "--rerank", description: "Enable rerank" }, + { + flag: "--sparse-similarity-top-k ", + description: "Sparse retrieval top K (API-KEY only)", + type: "number", + }, + { flag: "--rerank", description: "Enable reranking" }, { flag: "--rerank-top-n ", description: "Rerank top N results", type: "number" }, - { flag: "--access-key-id ", description: "Alibaba Cloud Access Key ID (or env)" }, - { flag: "--access-key-secret ", description: "Alibaba Cloud Access Key Secret (or env)" }, + { + flag: "--rerank-model ", + description: "Rerank model, e.g. qwen3-rerank-hybrid (API-KEY only)", + }, + { + flag: "--rerank-mode ", + description: "Rerank mode: qa, similar, or custom (API-KEY only)", + }, + { + flag: "--rerank-instruct ", + description: "Custom rerank instruction, when mode=custom (API-KEY only)", + }, + { + flag: "--top-k ", + description: "Number of results (deprecated, use --rerank-top-n)", + type: "number", + }, + { + flag: "--workspace-id ", + description: "Bailian workspace ID (required for AK/SK auth)", + }, + { flag: "--access-key-id ", description: "Alibaba Cloud Access Key ID (deprecated)" }, + { + flag: "--access-key-secret ", + 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 --query "); - 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 ", - 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 ", - 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 { + 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(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 { + 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 ", + 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(""); + } + } +} diff --git a/packages/cli/tests/e2e/helpers.ts b/packages/cli/tests/e2e/helpers.ts index bd58df2..e35b8a3 100644 --- a/packages/cli/tests/e2e/helpers.ts +++ b/packages/cli/tests/e2e/helpers.ts @@ -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 && diff --git a/packages/cli/tests/e2e/knowledge.e2e.test.ts b/packages/cli/tests/e2e/knowledge.e2e.test.ts index 27c6fd2..175467e 100644 --- a/packages/cli/tests/e2e/knowledge.e2e.test.ts +++ b/packages/cli/tests/e2e/knowledge.e2e.test.ts @@ -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(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(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(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(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(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(stdout); + expect(stderr).toMatch(/deprecated/i); + const data = parseStdoutJson(stdout); const ok = data.Success === true || data.Code === "Success"; expect(ok).toBe(true); expect(Array.isArray(data.Data?.Nodes)).toBe(true); diff --git a/packages/core/src/client/endpoints.ts b/packages/core/src/client/endpoints.ts index 2c8e0df..7cb4ab2 100644 --- a/packages/core/src/client/endpoints.ts +++ b/packages/core/src/client/endpoints.ts @@ -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 { diff --git a/packages/core/src/client/index.ts b/packages/core/src/client/index.ts index bcbefdb..22be23e 100644 --- a/packages/core/src/client/index.ts +++ b/packages/core/src/client/index.ts @@ -5,6 +5,7 @@ export { chatEndpoint, imageEndpoint, imageSyncEndpoint, + knowledgeRetrieveEndpoint, memoryAddEndpoint, memoryListEndpoint, memoryNodeEndpoint, diff --git a/packages/core/src/types/api.ts b/packages/core/src/types/api.ts index 585339e..c99fee8 100644 --- a/packages/core/src/types/api.ts +++ b/packages/core/src/types/api.ts @@ -384,6 +384,35 @@ export interface KnowledgeRetrieveResponse { }; } +// ---- Knowledge Retrieve (DashScope protocol — snake_case) ---- + +export interface DashScopeKnowledgeRetrieveRequest { + index_id: string; + query: string; + search_filters?: Array>; + 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; + }>; + }; +} + // ---- Speech Synthesis / TTS (DashScope) ---- export interface DashScopeTTSRequest {