From 8ee2c378f5ef471d04922ec198580c1f26c489fb Mon Sep 17 00:00:00 2001 From: "zeyu.fz" Date: Wed, 5 Aug 2026 18:14:23 +0800 Subject: [PATCH] =?UTF-8?q?test(knowledge):=20=E6=B7=BB=E5=8A=A0=E5=A4=9A?= =?UTF-8?q?=E6=A8=A1=E6=80=81=E4=B8=8E=E8=A1=A8=E6=A0=BC=E5=9E=8B=E4=BB=93?= =?UTF-8?q?=E5=BA=93=20E2E=20=E6=B5=8B=E8=AF=95=E5=A5=97=E4=BB=B6=E6=94=AF?= =?UTF-8?q?=E6=8C=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增多模态问答及检索服务的 live E2E 测试用例,支持基于图像参数的功能验证 - 补充表格库的 chunk add/list/delete 闭环测试,验证了 field channel 的必填项和读写一致性 - 增加图片库 chunk list 的 metadata 验证,确保 image_url 数组和可见性标志存在 - 实现带覆盖重导功能的 doc import-oss 测试,确认覆盖后 fileId 变更及旧文件失效 - 编写自有 OSS Bucket 的幂等复用集合创建和获取测试,确保服务端的 tag-based 访问控制支持 - 在 gating 中添加对各类长驻知识库及服务环境变量的就绪检测函数 --- .../src/commands/knowledge/chunk-add.ts | 4 +- .../commands/knowledge/collection-create.ts | 7 +- .../src/commands/knowledge/collection-get.ts | 7 +- .../src/commands/knowledge/doc-import-oss.ts | 12 +- packages/commands/tests/e2e/helpers.ts | 6 + .../e2e/knowledge/knowledge-chat.e2e.test.ts | 38 ++- .../knowledge-chunk-category-file.e2e.test.ts | 296 +++++++++++++++++- .../knowledge/knowledge-search.e2e.test.ts | 70 ++++- packages/commands/tests/e2e/topic-routes.ts | 1 + packages/core/src/types/knowledge-admin.ts | 16 +- packages/e2e/src/gating.ts | 38 +++ skills/bailian-cli/reference/knowledge.md | 5 +- 12 files changed, 481 insertions(+), 19 deletions(-) diff --git a/packages/commands/src/commands/knowledge/chunk-add.ts b/packages/commands/src/commands/knowledge/chunk-add.ts index ca6130f..512981a 100644 --- a/packages/commands/src/commands/knowledge/chunk-add.ts +++ b/packages/commands/src/commands/knowledge/chunk-add.ts @@ -22,7 +22,8 @@ const CHUNK_ADD_FLAGS = { docId: { type: "string", valueHint: "", - description: "Attach the chunk to this document (document-type knowledge bases)", + description: + "Owning document ID; required for table/image knowledge bases (the server rejects field-channel chunks without it), optional for document-type", }, content: { type: "string", @@ -73,6 +74,7 @@ export default defineCommand({ flags: CHUNK_ADD_FLAGS, notes: [ "Document / table / image knowledge bases are supported; audio-video ones are not.", + "Table/image knowledge bases require --doc-id — verified live: the server returns HTTP 500 (dataId不能为空) without it. Use the document-level id from the doc list command; the per-row doc_id in chunk list metadata is rejected (Index.InvalidParameter).", "The API is idempotent but rate-limited to 10 calls per second — throttle batch scripts.", "The response carries no chunk id; list chunks afterwards to find the new one.", "For table/image knowledge bases use --field with Excel column headers as keys; values are passed through as strings.", diff --git a/packages/commands/src/commands/knowledge/collection-create.ts b/packages/commands/src/commands/knowledge/collection-create.ts index cfd1839..67fa53f 100644 --- a/packages/commands/src/commands/knowledge/collection-create.ts +++ b/packages/commands/src/commands/knowledge/collection-create.ts @@ -47,6 +47,7 @@ export default defineCommand({ flags: COLLECTION_CREATE_FLAGS, notes: [ "Store type defaults to platform (managed storage); custom uses your authorized OSS bucket.", + "Custom buckets must carry the bucket tag bailian-connector-access=ReadAndWrite (Bailian's tag-based access control); without it the server rejects creation with a misleading 'setBucketCORS failed' error.", "There is no collection delete API — create collections deliberately.", ], exampleArgs: [ @@ -71,7 +72,9 @@ export default defineCommand({ const format = detectOutputFormat(settings.output); const storeType = (flags.storeType ?? "platform").toUpperCase(); - // The server contract still uses connector* fields; only the CLI-facing term is collection + // The server contract still uses connector* fields; only the CLI-facing term is collection. + // CUSTOM fields are regionId/bucketName per api/connector/add-connector.md (live-verified; + // the earlier ossRegionId/ossBucket naming was an implementation error, rejected with InvalidParameter). const body = { connectorType: "FILE", connectorName: flags.name, @@ -79,7 +82,7 @@ export default defineCommand({ fileConnectorConfig: { storeType, ...(storeType === "CUSTOM" - ? { ossRegionId: flags.ossRegion, ossBucket: flags.ossBucket } + ? { regionId: flags.ossRegion, bucketName: flags.ossBucket } : {}), }, }; diff --git a/packages/commands/src/commands/knowledge/collection-get.ts b/packages/commands/src/commands/knowledge/collection-get.ts index 167856e..81eaa73 100644 --- a/packages/commands/src/commands/knowledge/collection-get.ts +++ b/packages/commands/src/commands/knowledge/collection-get.ts @@ -69,8 +69,11 @@ export default defineCommand({ const storeType = collection?.fileConnectorConfig?.storeType; emitBare(`storeType: ${storeType ?? "-"}`); if (storeType === "CUSTOM") { - emitBare(` ossRegion: ${collection?.fileConnectorConfig?.ossRegionId ?? "-"}`); - emitBare(` ossBucket: ${collection?.fileConnectorConfig?.ossBucket ?? "-"}`); + // Defensive branch: live-verified getConnector responses do NOT echo + // fileConnectorConfig at all (storeType prints "-"); kept in case the + // server starts returning it. Field names follow the create contract. + emitBare(` ossRegion: ${collection?.fileConnectorConfig?.regionId ?? "-"}`); + emitBare(` ossBucket: ${collection?.fileConnectorConfig?.bucketName ?? "-"}`); } return; } diff --git a/packages/commands/src/commands/knowledge/doc-import-oss.ts b/packages/commands/src/commands/knowledge/doc-import-oss.ts index 40330c9..9db0468 100644 --- a/packages/commands/src/commands/knowledge/doc-import-oss.ts +++ b/packages/commands/src/commands/knowledge/doc-import-oss.ts @@ -54,6 +54,7 @@ export default defineCommand({ notes: [ "The bucket must be authorized to the platform service role beforehand; permission errors from the server are passed through with a pointer to check AliyunServiceRoleForBailian in the RAM console.", "File names are derived from the OSS key basename.", + "--overwrite replaces the previously imported file and issues a NEW fileId (the old one becomes invalid) — verified live.", ], exampleArgs: [ "--bucket my-bucket --region cn-beijing --oss-key docs/a.pdf --workspace-id ws-xxx", @@ -94,14 +95,21 @@ export default defineCommand({ body, }); - const fileIds = response.data?.fileIds ?? []; + // Live-verified shape: results come back as addFileResultList (the docs' flat + // fileIds field is not returned); per-file status is SUCCESS on success + const results = response.data?.addFileResultList ?? []; + const fileIds = results + .map((result) => result.fileId) + .filter((fileId): fileId is string => !!fileId); if (settings.quiet) { for (const fileId of fileIds) emitBare(fileId); return; } if (format === "text") { emitBare(`imported: ${fileIds.length} file(s)`); - for (const fileId of fileIds) emitBare(` ${fileId}`); + for (const result of results) { + emitBare(` ${result.fileId ?? "-"} ${result.status ?? "-"} ${result.ossKey ?? ""}`); + } return; } emitResult(response, format); diff --git a/packages/commands/tests/e2e/helpers.ts b/packages/commands/tests/e2e/helpers.ts index f733b7f..a81f08e 100644 --- a/packages/commands/tests/e2e/helpers.ts +++ b/packages/commands/tests/e2e/helpers.ts @@ -29,9 +29,15 @@ export { isConnectorE2EReady, isConsoleE2EReady, isDashScopeE2EReady, + isImageKbE2EReady, isKbAdminE2EReady, + isMultimodalChatE2EReady, + isMultimodalSearchE2EReady, isOpenApiE2EReady, + isOssImportE2EReady, isSearchE2EReady, + isTableKbE2EReady, + isTableSearchE2EReady, } from "e2e/gating"; const e2eDir = dirname(fileURLToPath(import.meta.url)); diff --git a/packages/commands/tests/e2e/knowledge/knowledge-chat.e2e.test.ts b/packages/commands/tests/e2e/knowledge/knowledge-chat.e2e.test.ts index 737c2a9..9d09f7a 100644 --- a/packages/commands/tests/e2e/knowledge/knowledge-chat.e2e.test.ts +++ b/packages/commands/tests/e2e/knowledge/knowledge-chat.e2e.test.ts @@ -1,5 +1,10 @@ import { describe, expect, test } from "vite-plus/test"; -import { isChatE2EReady, parseStdoutJson, runCommandE2e } from "../helpers.ts"; +import { + isChatE2EReady, + isMultimodalChatE2EReady, + parseStdoutJson, + runCommandE2e, +} from "../helpers.ts"; import { KNOWLEDGE_CHAT_ROUTES } from "../topic-routes.ts"; interface ContentPart { @@ -389,3 +394,34 @@ describe.skipIf(!isChatE2EReady())("e2e: knowledge chat (live)", () => { expect(stderr).toBeTruthy(); }); }); + +// Long-lived console-created fixture (multimodal Q&A services cannot be created +// via the CLI — see .env BAILIAN_E2E_IMAGE_CHAT_AGENT_ID) +describe.skipIf(!isMultimodalChatE2EReady())( + "e2e: knowledge chat --image live (常驻 fixture)", + () => { + const workspaceId = process.env.BAILIAN_WORKSPACE_ID!; + const imageChatAgentId = process.env.BAILIAN_E2E_IMAGE_CHAT_AGENT_ID!; + + test("多模态问答接受 --image 并返回非空回答", async () => { + const { stdout, stderr, exitCode } = await runCommandE2e(KNOWLEDGE_CHAT_ROUTES, [ + "knowledge", + "chat", + "--message", + "图里有什么", + "--image", + "https://dashscope.oss-cn-beijing.aliyuncs.com/images/dog_and_girl.jpeg", + "--agent-id", + imageChatAgentId, + "--workspace-id", + workspaceId, + "--output", + "json", + ]); + expect(exitCode, stderr).toBe(0); + const data = parseStdoutJson(stdout); + expect(data.answer).toBeTruthy(); + expect(data.answer.length).toBeGreaterThan(0); + }, 120_000); + }, +); diff --git a/packages/commands/tests/e2e/knowledge/knowledge-chunk-category-file.e2e.test.ts b/packages/commands/tests/e2e/knowledge/knowledge-chunk-category-file.e2e.test.ts index 35e2249..2ae7914 100644 --- a/packages/commands/tests/e2e/knowledge/knowledge-chunk-category-file.e2e.test.ts +++ b/packages/commands/tests/e2e/knowledge/knowledge-chunk-category-file.e2e.test.ts @@ -5,7 +5,14 @@ import { mkdtempSync, writeFileSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { describe, expect, test } from "vite-plus/test"; -import { isKbAdminE2EReady, parseStdoutJson, runCommandE2e } from "../helpers.ts"; +import { + isImageKbE2EReady, + isKbAdminE2EReady, + isOssImportE2EReady, + isTableKbE2EReady, + parseStdoutJson, + runCommandE2e, +} from "../helpers.ts"; import { KNOWLEDGE_CHUNK_CATEGORY_FILE_ROUTES, KNOWLEDGE_KB_DELETE_ROUTES, @@ -690,7 +697,7 @@ describe("e2e: kb stats / category / file / connector / import-oss (静态)", () expect(connectorConfig?.storeType).toBe("PLATFORM"); }); - test("collection create: custom dry-run 断言 ossRegionId/ossBucket 键名映射", async () => { + test("collection create: custom dry-run 断言 regionId/bucketName 键名映射", async () => { const { stdout, stderr, exitCode } = await runCommandE2e(KNOWLEDGE_CHUNK_CATEGORY_FILE_ROUTES, [ "knowledge", "collection", @@ -710,11 +717,15 @@ describe("e2e: kb stats / category / file / connector / import-oss (静态)", () expect(exitCode, stderr).toBe(0); const data = parseStdoutJson(stdout); const connectorConfig = data.request?.fileConnectorConfig as - | { storeType?: string; ossRegionId?: string; ossBucket?: string } + | { storeType?: string; regionId?: string; bucketName?: string } | undefined; expect(connectorConfig?.storeType).toBe("CUSTOM"); - expect(connectorConfig?.ossRegionId).toBe("cn-beijing"); - expect(connectorConfig?.ossBucket).toBe("my-bucket"); + // CUSTOM fields are regionId/bucketName per add-connector.md — the earlier + // ossRegionId/ossBucket implementation was rejected live with InvalidParameter + expect(connectorConfig?.regionId).toBe("cn-beijing"); + expect(connectorConfig?.bucketName).toBe("my-bucket"); + expect(connectorConfig).not.toHaveProperty("ossRegionId"); + expect(connectorConfig).not.toHaveProperty("ossBucket"); }); test("collection get: 都缺 / 都传 均 USAGE (2)", async () => { @@ -1343,3 +1354,278 @@ describe.skipIf(!isKbAdminE2EReady())( }, 600_000); }, ); + +interface ChunkListNodes { + data?: { nodes?: Array<{ metadata?: Record }> }; +} + +// Long-lived console-created fixture: the CLI can only create document-type bases, +// so the field channel (table/image bases) rides on BAILIAN_E2E_TABLE_INDEX_ID. +describe.skipIf(!isTableKbE2EReady())("e2e: chunk --field 表格库 live (常驻 fixture)", () => { + const workspaceId = process.env.BAILIAN_WORKSPACE_ID!; + const tableIndexId = process.env.BAILIAN_E2E_TABLE_INDEX_ID!; + + test("field 闭环: 取 doc_id → add --field → list 回读命中 → delete", async () => { + // Gotcha (verified live): dataId must be the document-level id from doc list; + // the per-row doc_id in chunk metadata (with a _ suffix) is rejected with + // Index.InvalidParameter + const docListRun = await runCommandE2e(KNOWLEDGE_CHUNK_CATEGORY_FILE_ROUTES, [ + "knowledge", + "doc", + "list", + "--index-id", + tableIndexId, + "--workspace-id", + workspaceId, + "--quiet", + ]); + expect(docListRun.exitCode, docListRun.stderr).toBe(0); + const tableDocId = docListRun.stdout.trim().split("\n")[0]; + expect(tableDocId, "表格 fixture 库应至少有 1 个文档").toBeTruthy(); + + // Server gotcha (verified live): table bases reject field chunks without --doc-id + // — HTTP 500 "dataId不能为空", passed through verbatim as a server error + const noDocIdRun = await runCommandE2e(KNOWLEDGE_CHUNK_CATEGORY_FILE_ROUTES, [ + "knowledge", + "chunk", + "add", + "--index-id", + tableIndexId, + "--field", + "ZH=missing-doc-id-probe", + "--workspace-id", + workspaceId, + ]); + expect(noDocIdRun.exitCode).not.toBe(0); + + // add a row via the field channel (ZH/KO are the fixture table's Excel headers) + const marker = `e2e-field-${Date.now()}`; + const addRun = await runCommandE2e(KNOWLEDGE_CHUNK_CATEGORY_FILE_ROUTES, [ + "knowledge", + "chunk", + "add", + "--index-id", + tableIndexId, + "--doc-id", + tableDocId!, + "--field", + `ZH=${marker}`, + "--field", + "KO=e2e-probe", + "--workspace-id", + workspaceId, + ]); + expect(addRun.exitCode, addRun.stderr).toBe(0); + + // read back by sweeping pages until the marker row shows up, grab its chunk id + let markerChunkId = ""; + for (let pageNumber = 1; pageNumber <= 5 && !markerChunkId; pageNumber++) { + const pageRun = await runCommandE2e(KNOWLEDGE_CHUNK_CATEGORY_FILE_ROUTES, [ + "knowledge", + "chunk", + "list", + "--index-id", + tableIndexId, + "--page-number", + String(pageNumber), + "--page-size", + "100", + "--workspace-id", + workspaceId, + "--output", + "json", + ]); + expect(pageRun.exitCode, pageRun.stderr).toBe(0); + const pageNodes = parseStdoutJson(pageRun.stdout).data?.nodes ?? []; + const match = pageNodes.find((node) => node.metadata?.ZH === marker); + if (match) markerChunkId = (match.metadata?._id as string | undefined) ?? ""; + if (pageNodes.length < 100) break; + } + expect(markerChunkId, `field 新增行未在 list 中回读到 (marker=${marker})`).toBeTruthy(); + + // clean up the exact row we added — the fixture base itself is never touched + const deleteRun = await runCommandE2e(KNOWLEDGE_CHUNK_CATEGORY_FILE_ROUTES, [ + "knowledge", + "chunk", + "delete", + "--index-id", + tableIndexId, + "--chunk-id", + markerChunkId, + "--yes", + "--workspace-id", + workspaceId, + ]); + expect(deleteRun.exitCode, deleteRun.stderr).toBe(0); + }, 120_000); +}); + +describe.skipIf(!isImageKbE2EReady())("e2e: 图片库 chunk 回读 live (常驻 fixture)", () => { + const workspaceId = process.env.BAILIAN_WORKSPACE_ID!; + const imageIndexId = process.env.BAILIAN_E2E_IMAGE_INDEX_ID!; + + test("chunk list 返回 image_url 数组与可见性标志", async () => { + const listRun = await runCommandE2e(KNOWLEDGE_CHUNK_CATEGORY_FILE_ROUTES, [ + "knowledge", + "chunk", + "list", + "--index-id", + imageIndexId, + "--page-size", + "2", + "--workspace-id", + workspaceId, + "--output", + "json", + ]); + expect(listRun.exitCode, listRun.stderr).toBe(0); + const nodes = parseStdoutJson(listRun.stdout).data?.nodes ?? []; + expect(nodes.length).toBeGreaterThan(0); + // Image-type chunks carry image_url arrays (shape verified against the live fixture) + const firstMeta = nodes[0]?.metadata ?? {}; + expect(Array.isArray(firstMeta.image_url)).toBe(true); + expect((firstMeta.image_url as string[]).length).toBeGreaterThan(0); + expect(typeof firstMeta.is_displayed_chunk_content).toBe("boolean"); + }, 60_000); +}); + +// Requires a bucket pre-authorized to the platform service role with a fixed test +// object in place (see .env BAILIAN_E2E_OSS_BUCKET/REGION/KEY) +describe.skipIf(!isOssImportE2EReady())("e2e: doc import-oss live (授权 bucket fixture)", () => { + const workspaceId = process.env.BAILIAN_WORKSPACE_ID!; + const ossBucket = process.env.BAILIAN_E2E_OSS_BUCKET!; + const ossRegion = process.env.BAILIAN_E2E_OSS_REGION!; + const ossKey = process.env.BAILIAN_E2E_OSS_KEY!; + + test("导入 → overwrite 重导(新 fileId, 旧 id 作废) → file delete 自清理", async () => { + // 1) first import returns a fileId (locks the addFileResultList response shape: + // the docs' flat fileIds field is not returned — live-verified) + const importRun = await runCommandE2e(KNOWLEDGE_CHUNK_CATEGORY_FILE_ROUTES, [ + "knowledge", + "doc", + "import-oss", + "--bucket", + ossBucket, + "--region", + ossRegion, + "--oss-key", + ossKey, + "--workspace-id", + workspaceId, + "--quiet", + ]); + expect(importRun.exitCode, importRun.stderr).toBe(0); + const firstFileId = importRun.stdout.trim(); + expect(firstFileId).toMatch(/^file_/); + + // 2) re-import with --overwrite: the server replaces the file and issues a NEW + // fileId; the old one becomes invalid (live-verified semantics) + const overwriteRun = await runCommandE2e(KNOWLEDGE_CHUNK_CATEGORY_FILE_ROUTES, [ + "knowledge", + "doc", + "import-oss", + "--bucket", + ossBucket, + "--region", + ossRegion, + "--oss-key", + ossKey, + "--overwrite", + "--workspace-id", + workspaceId, + "--quiet", + ]); + expect(overwriteRun.exitCode, overwriteRun.stderr).toBe(0); + const overwriteFileId = overwriteRun.stdout.trim(); + expect(overwriteFileId).toMatch(/^file_/); + expect(overwriteFileId).not.toBe(firstFileId); + + // the pre-overwrite id is gone — file get on it fails + const staleGetRun = await runCommandE2e(KNOWLEDGE_CHUNK_CATEGORY_FILE_ROUTES, [ + "knowledge", + "file", + "get", + "--file-id", + firstFileId, + "--workspace-id", + workspaceId, + ]); + expect(staleGetRun.exitCode).not.toBe(0); + + // 3) clean up the surviving imported file + const deleteRun = await runCommandE2e(KNOWLEDGE_CHUNK_CATEGORY_FILE_ROUTES, [ + "knowledge", + "file", + "delete", + "--file-id", + overwriteFileId, + "--yes", + "--workspace-id", + workspaceId, + ]); + expect(deleteRun.exitCode, deleteRun.stderr).toBe(0); + }, 120_000); + + test("collection create custom live: 幂等复用固定名自有存储集合", async () => { + // No collection delete API — fixed-name idempotent reuse, same pattern as the + // platform collection test. Requires the bucket tag + // bailian-connector-access=ReadAndWrite (tag-based access control). + const connectorName = "e2e-test-custom-conn"; // exactly 20 chars (server name limit) + const firstGetRun = await runCommandE2e(KNOWLEDGE_CHUNK_CATEGORY_FILE_ROUTES, [ + "knowledge", + "collection", + "get", + "--name", + connectorName, + "--workspace-id", + workspaceId, + "--quiet", + ]); + let connectorId = firstGetRun.exitCode === 0 ? firstGetRun.stdout.trim() : ""; + + if (!connectorId) { + const createRun = await runCommandE2e(KNOWLEDGE_CHUNK_CATEGORY_FILE_ROUTES, [ + "knowledge", + "collection", + "create", + "--name", + connectorName, + "--description", + "e2e fixture custom connector (own OSS bucket, reused across runs)", + "--store-type", + "custom", + "--oss-region", + process.env.BAILIAN_E2E_OSS_REGION!, + "--oss-bucket", + process.env.BAILIAN_E2E_OSS_BUCKET!, + "--workspace-id", + workspaceId, + "--quiet", + ]); + expect(createRun.exitCode, createRun.stderr).toBe(0); + connectorId = createRun.stdout.trim(); + } + expect(connectorId).toBeTruthy(); + + // get by id: identity fields only — live-verified getConnector does NOT echo + // fileConnectorConfig (storeType/regionId/bucketName are absent from readback) + const getRun = await runCommandE2e(KNOWLEDGE_CHUNK_CATEGORY_FILE_ROUTES, [ + "knowledge", + "collection", + "get", + "--collection-id", + connectorId, + "--workspace-id", + workspaceId, + "--output", + "json", + ]); + expect(getRun.exitCode, getRun.stderr).toBe(0); + const customDetail = parseStdoutJson<{ + data: { connectorId?: string; connectorName?: string; connectorType?: string }; + }>(getRun.stdout); + expect(customDetail.data.connectorId).toBe(connectorId); + expect(customDetail.data.connectorName).toBe(connectorName); + expect(customDetail.data.connectorType).toBe("FILE"); + }, 60_000); +}); diff --git a/packages/commands/tests/e2e/knowledge/knowledge-search.e2e.test.ts b/packages/commands/tests/e2e/knowledge/knowledge-search.e2e.test.ts index 06f2c8a..e9fc6ca 100644 --- a/packages/commands/tests/e2e/knowledge/knowledge-search.e2e.test.ts +++ b/packages/commands/tests/e2e/knowledge/knowledge-search.e2e.test.ts @@ -1,5 +1,11 @@ import { describe, expect, test } from "vite-plus/test"; -import { isSearchE2EReady, parseStdoutJson, runCommandE2e } from "../helpers.ts"; +import { + isMultimodalSearchE2EReady, + isSearchE2EReady, + isTableSearchE2EReady, + parseStdoutJson, + runCommandE2e, +} from "../helpers.ts"; import { KNOWLEDGE_SEARCH_ROUTES } from "../topic-routes.ts"; interface DryRunBody { @@ -293,3 +299,65 @@ describe.skipIf(!isSearchE2EReady())("e2e: knowledge search (live)", () => { expect(stderr).toBeTruthy(); }); }); + +// Long-lived console-created fixtures (multimodal / table services cannot be +// created via the CLI — see .env BAILIAN_E2E_IMAGE_SEARCH_AGENT_ID etc.) +describe.skipIf(!isMultimodalSearchE2EReady())( + "e2e: knowledge search --image live (常驻 fixture)", + () => { + const workspaceId = process.env.BAILIAN_WORKSPACE_ID!; + const imageSearchAgentId = process.env.BAILIAN_E2E_IMAGE_SEARCH_AGENT_ID!; + + test("多模态服务接受 --image 并返回 Success", async () => { + // Recall is not asserted: hits depend on the fixture base's image contents. + // The functional contract under test: the images param is accepted end-to-end. + const { stdout, stderr, exitCode } = await runCommandE2e(KNOWLEDGE_SEARCH_ROUTES, [ + "knowledge", + "search", + "--query", + "图里有什么", + "--agent-id", + imageSearchAgentId, + "--workspace-id", + workspaceId, + "--image", + "https://dashscope.oss-cn-beijing.aliyuncs.com/images/dog_and_girl.jpeg", + "--output", + "json", + ]); + expect(exitCode, stderr).toBe(0); + const data = parseStdoutJson(stdout); + expect(data.code).toBe("Success"); + expect(Array.isArray(data.data.nodes)).toBe(true); + }, 60_000); + }, +); + +describe.skipIf(!isTableSearchE2EReady())( + "e2e: knowledge search 表格库 live (常驻 fixture)", + () => { + const workspaceId = process.env.BAILIAN_WORKSPACE_ID!; + const tableSearchAgentId = process.env.BAILIAN_E2E_TABLE_SEARCH_AGENT_ID!; + + test("表格行召回: 命中 fixture 已知单元格值", async () => { + // “颜色填充” is a known ZH-column cell in the fixture table (nop0ludera) + const { stdout, stderr, exitCode } = await runCommandE2e(KNOWLEDGE_SEARCH_ROUTES, [ + "knowledge", + "search", + "--query", + "颜色填充", + "--agent-id", + tableSearchAgentId, + "--workspace-id", + workspaceId, + "--output", + "json", + ]); + expect(exitCode, stderr).toBe(0); + const data = parseStdoutJson(stdout); + expect(data.code).toBe("Success"); + expect(data.data.nodes.length).toBeGreaterThan(0); + expect(data.data.nodes.map((node) => node.text).join("\n")).toContain("颜色填充"); + }, 60_000); + }, +); diff --git a/packages/commands/tests/e2e/topic-routes.ts b/packages/commands/tests/e2e/topic-routes.ts index d7f812f..dbf5889 100644 --- a/packages/commands/tests/e2e/topic-routes.ts +++ b/packages/commands/tests/e2e/topic-routes.ts @@ -262,6 +262,7 @@ export const KNOWLEDGE_CHUNK_CATEGORY_FILE_ROUTES: E2eRouteExports = { "knowledge collection get": "knowledgeCollectionGet", "knowledge doc import-oss": "knowledgeDocImportOss", "knowledge list": "knowledgeKbList", // live grabs a real index id + "knowledge doc list": "knowledgeDocList", // table fixture: resolve the document-level dataId "knowledge doc upload": "knowledgeDocUpload", // live produces a fileId "knowledge doc delete": "knowledgeDocDelete", // live verifies document-level delete semantics }; diff --git a/packages/core/src/types/knowledge-admin.ts b/packages/core/src/types/knowledge-admin.ts index f99e8ce..3d68ab7 100644 --- a/packages/core/src/types/knowledge-admin.ts +++ b/packages/core/src/types/knowledge-admin.ts @@ -332,8 +332,9 @@ export interface RagConnectorInfo { connectorType?: string; fileConnectorConfig?: { storeType?: string; - ossRegionId?: string; - ossBucket?: string; + /** CUSTOM request fields: regionId/bucketName (per add-connector.md, live-verified) */ + regionId?: string; + bucketName?: string; [key: string]: unknown; }; [key: string]: unknown; @@ -341,9 +342,16 @@ export interface RagConnectorInfo { export type RagAddConnectorResponse = RagConnectorResponse; export type RagGetConnectorResponse = RagConnectorResponse; -/** POST addFilesFromAuthorizedOss */ +/** POST addFilesFromAuthorizedOss — live shape is addFileResultList (the docs' fileIds is not returned) */ +export interface RagOssImportFileResult { + fileId?: string; + ossKey?: string; + status?: string; + msg?: string; + [key: string]: unknown; +} export interface RagOssImportData { - fileIds?: string[]; + addFileResultList?: RagOssImportFileResult[]; [key: string]: unknown; } export type RagOssImportResponse = RagConnectorResponse; diff --git a/packages/e2e/src/gating.ts b/packages/e2e/src/gating.ts index 5df459c..c70dadc 100644 --- a/packages/e2e/src/gating.ts +++ b/packages/e2e/src/gating.ts @@ -74,3 +74,41 @@ export function isKbAdminE2EReady(): boolean { export function isConnectorE2EReady(): boolean { return isKbAdminE2EReady() && process.env.BAILIAN_E2E_CONNECTOR === "1"; } + +// ---- Long-lived knowledge fixtures (created manually in the console; the CLI +// cannot create table/image-type bases or multimodal services itself) ---- + +/** 表格型知识库 fixture(chunk --field live 闭环) */ +export function isTableKbE2EReady(): boolean { + return isKbAdminE2EReady() && !!process.env.BAILIAN_E2E_TABLE_INDEX_ID?.trim(); +} + +/** 图片型知识库 fixture(图片 chunk 回读) */ +export function isImageKbE2EReady(): boolean { + return isKbAdminE2EReady() && !!process.env.BAILIAN_E2E_IMAGE_INDEX_ID?.trim(); +} + +/** 多模态检索服务 fixture(search --image live) */ +export function isMultimodalSearchE2EReady(): boolean { + return isKbAdminE2EReady() && !!process.env.BAILIAN_E2E_IMAGE_SEARCH_AGENT_ID?.trim(); +} + +/** 多模态问答服务 fixture(chat --image live) */ +export function isMultimodalChatE2EReady(): boolean { + return isKbAdminE2EReady() && !!process.env.BAILIAN_E2E_IMAGE_CHAT_AGENT_ID?.trim(); +} + +/** 表格库检索服务 fixture(表格行召回 live) */ +export function isTableSearchE2EReady(): boolean { + return isKbAdminE2EReady() && !!process.env.BAILIAN_E2E_TABLE_SEARCH_AGENT_ID?.trim(); +} + +/** 已授权 OSS bucket fixture(doc import-oss live;需提前在 RAM 授权并放好固定测试文件) */ +export function isOssImportE2EReady(): boolean { + return ( + isKbAdminE2EReady() && + !!process.env.BAILIAN_E2E_OSS_BUCKET?.trim() && + !!process.env.BAILIAN_E2E_OSS_REGION?.trim() && + !!process.env.BAILIAN_E2E_OSS_KEY?.trim() + ); +} diff --git a/skills/bailian-cli/reference/knowledge.md b/skills/bailian-cli/reference/knowledge.md index ff7fe05..531bf0d 100644 --- a/skills/bailian-cli/reference/knowledge.md +++ b/skills/bailian-cli/reference/knowledge.md @@ -206,7 +206,7 @@ bl knowledge chat --message "Describe these images" --image https://example.com/ | Flag | Type | Required | Description | | ----------------------- | ------ | -------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- | | `--index-id ` | string | yes | Knowledge base ID | -| `--doc-id ` | string | no | Attach the chunk to this document (document-type knowledge bases) | +| `--doc-id ` | string | no | Owning document ID; required for table/image knowledge bases (the server rejects field-channel chunks without it), optional for document-type | | `--content ` | string | no | Chunk body text, up to 6000 chars (document-type); alternative to --content-file | | `--content-file ` | string | no | Read chunk body from a UTF-8 plain text file (.md/.txt etc.) | | `--title ` | string | no | Chunk title, up to 50 chars (document-type) | @@ -219,6 +219,7 @@ bl knowledge chat --message "Describe these images" --image https://example.com/ #### Notes - Document / table / image knowledge bases are supported; audio-video ones are not. +- Table/image knowledge bases require --doc-id — verified live: the server returns HTTP 500 (dataId不能为空) without it. Use the document-level id from the doc list command; the per-row doc_id in chunk list metadata is rejected (Index.InvalidParameter). - The API is idempotent but rate-limited to 10 calls per second — throttle batch scripts. - The response carries no chunk id; list chunks afterwards to find the new one. - For table/image knowledge bases use --field with Excel column headers as keys; values are passed through as strings. @@ -366,6 +367,7 @@ bl knowledge chunk update --index-id idx-xxx --chunk-id chunk-xxx --doc-id file- #### Notes - Store type defaults to platform (managed storage); custom uses your authorized OSS bucket. +- Custom buckets must carry the bucket tag bailian-connector-access=ReadAndWrite (Bailian's tag-based access control); without it the server rejects creation with a misleading 'setBucketCORS failed' error. - There is no collection delete API — create collections deliberately. #### Examples @@ -538,6 +540,7 @@ bl knowledge doc delete --index-id idx-xxx --doc-id file-a --doc-id file-b --yes - The bucket must be authorized to the platform service role beforehand; permission errors from the server are passed through with a pointer to check AliyunServiceRoleForBailian in the RAM console. - File names are derived from the OSS key basename. +- --overwrite replaces the previously imported file and issues a NEW fileId (the old one becomes invalid) — verified live. #### Examples