mirror of
https://github.com/modelstudioai/cli.git
synced 2026-09-14 19:49:23 +08:00
test(knowledge): 添加多模态与表格型仓库 E2E 测试套件支持
- 新增多模态问答及检索服务的 live E2E 测试用例,支持基于图像参数的功能验证 - 补充表格库的 chunk add/list/delete 闭环测试,验证了 field channel 的必填项和读写一致性 - 增加图片库 chunk list 的 metadata 验证,确保 image_url 数组和可见性标志存在 - 实现带覆盖重导功能的 doc import-oss 测试,确认覆盖后 fileId 变更及旧文件失效 - 编写自有 OSS Bucket 的幂等复用集合创建和获取测试,确保服务端的 tag-based 访问控制支持 - 在 gating 中添加对各类长驻知识库及服务环境变量的就绪检测函数
This commit is contained in:
@@ -22,7 +22,8 @@ const CHUNK_ADD_FLAGS = {
|
||||
docId: {
|
||||
type: "string",
|
||||
valueHint: "<id>",
|
||||
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.",
|
||||
|
||||
@@ -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 }
|
||||
: {}),
|
||||
},
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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<ChatJsonResult>(stdout);
|
||||
expect(data.answer).toBeTruthy();
|
||||
expect(data.answer.length).toBeGreaterThan(0);
|
||||
}, 120_000);
|
||||
},
|
||||
);
|
||||
|
||||
@@ -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<DryRunBody>(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<string, unknown> }> };
|
||||
}
|
||||
|
||||
// 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 _<row> 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<ChunkListNodes>(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<ChunkListNodes>(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);
|
||||
});
|
||||
|
||||
@@ -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<SearchResponse>(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<SearchResponse>(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);
|
||||
},
|
||||
);
|
||||
|
||||
@@ -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
|
||||
};
|
||||
|
||||
@@ -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<RagConnectorInfo>;
|
||||
export type RagGetConnectorResponse = RagConnectorResponse<RagConnectorInfo>;
|
||||
|
||||
/** 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<RagOssImportData>;
|
||||
|
||||
@@ -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()
|
||||
);
|
||||
}
|
||||
|
||||
@@ -206,7 +206,7 @@ bl knowledge chat --message "Describe these images" --image https://example.com/
|
||||
| Flag | Type | Required | Description |
|
||||
| ----------------------- | ------ | -------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `--index-id <id>` | string | yes | Knowledge base ID |
|
||||
| `--doc-id <id>` | string | no | Attach the chunk to this document (document-type knowledge bases) |
|
||||
| `--doc-id <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 <text>` | string | no | Chunk body text, up to 6000 chars (document-type); alternative to --content-file |
|
||||
| `--content-file <path>` | string | no | Read chunk body from a UTF-8 plain text file (.md/.txt etc.) |
|
||||
| `--title <text>` | 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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user