fix(knowledge): 修正文档上传接口请求的字段名为 docIds

- 将请求体中的 dataSource.fileIds 改为扁平结构的 docIds 字段
- 移除嵌套的 dataSource 对象,显式指定 sourceType 字段
- 更新相关单元测试以匹配新的请求参数格式和字段名称
- 在知识库删除测试中增加了导入结果和最终状态的断言,确保导入流程完整
- 新增校验导入文件在知识库文档列表中正确显示
- 调整测试中对请求体结构的断言逻辑以适配改动
This commit is contained in:
zeyu.fz
2026-08-10 16:24:00 +08:00
parent 99a3dbae2d
commit 1d9852805f
3 changed files with 56 additions and 19 deletions
@@ -149,10 +149,10 @@ export default defineCommand({
endpoint: ragEndpoint(workspaceId, RAG_PATHS.indexJobCreate),
request: {
indexId: flags.indexId,
// Gotcha (live-verified): job/create requires the nested dataSource shape;
// the public docs' flat documentIds body returns Index.InvalidParameter.
// Omitting sourceType would import the entire data center.
dataSource: { sourceType: "DATA_CENTER_FILE", fileIds: ["<fileId>"] },
// Live-verified: the field name is docIds (not documentIds as in the
// public docs); omitting sourceType would import the entire data center.
sourceType: "DATA_CENTER_FILE",
docIds: ["<fileId>"],
} as unknown,
});
}
@@ -259,11 +259,10 @@ export default defineCommand({
method: "POST",
body: {
indexId: flags.indexId,
// Live-verified shape: nested dataSource (the docs' flat documentIds is rejected)
dataSource: {
sourceType: "DATA_CENTER_FILE",
fileIds: uploaded.map((item) => item.fileId),
},
// Live-verified: the field name is docIds (not documentIds as in the
// public docs); omitting sourceType would import the entire data center.
sourceType: "DATA_CENTER_FILE",
docIds: uploaded.map((item) => item.fileId),
},
});
ingestionId = job.data?.ingestionId;
@@ -113,7 +113,7 @@ describe("e2e: knowledge doc upload", () => {
expect(data.skipped).toEqual([]);
});
test("--dry-run 带 --index-id 输出 4 步且 job 请求含显式 sourceType", async () => {
test("--dry-run 带 --index-id 输出 4 步且 job 请求含扁平 docIds", async () => {
const { stdout, stderr, exitCode } = await runCommandE2e(KNOWLEDGE_DOC_UPLOAD_ROUTES, [
"knowledge",
"doc",
@@ -135,13 +135,15 @@ describe("e2e: knowledge doc upload", () => {
expect(data.steps).toHaveLength(4);
const jobRequest = data.steps[3]!.request as {
indexId?: string;
dataSource?: { sourceType?: string; fileIds?: string[] };
sourceType?: string;
docIds?: string[];
};
// Live-verified gotcha: job/create requires the nested dataSource shape, and
// omitting sourceType would import the entire data center
expect(jobRequest.dataSource?.sourceType).toBe("DATA_CENTER_FILE");
// Live-verified: the field name is docIds (not documentIds as in the
// public docs); omitting sourceType would import the entire data center.
expect(jobRequest.sourceType).toBe("DATA_CENTER_FILE");
expect(jobRequest.docIds).toEqual(["<fileId>"]);
expect(jobRequest.indexId).toBe("idx_test");
expect(jobRequest).not.toHaveProperty("documentIds");
expect(jobRequest).not.toHaveProperty("dataSource");
});
test("--file <dir> dry-run 展开目录且 skipped 包含不支持的文件", async () => {
@@ -2,7 +2,7 @@
// (import orchestration) → delete → list to verify removal.
import { mkdtempSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { basename, join } from "node:path";
import { describe, expect, test } from "vite-plus/test";
import { isKbAdminE2EReady, parseStdoutJson, runCommandE2e } from "../helpers.ts";
import { deleteKbWithRetry } from "./journeys/journey-helpers.ts";
@@ -132,7 +132,9 @@ describe.skipIf(!isKbAdminE2EReady())("e2e: knowledge kb 写链路 (live, 自清
// 2.7) Live coverage of the upload → import orchestration (doc upload --index-id --wait):
// the journeys always upload bare files and import via kb create, so this is the
// only place the createImportJob step runs live
// only place the createImportJob step runs live. Using --output json (not --quiet)
// so we can assert final_status and ingestion_id — a regression in the job/create
// request body (e.g. wrong field name) is caught by the server returning HTTP 400.
const importFilePath = join(fixtureDir, `chain-import-${Date.now()}.md`);
writeFileSync(importFilePath, "# kb chain e2e import fixture\n");
const importUploadRun = await runCommandE2e(KNOWLEDGE_KB_DELETE_ROUTES, [
@@ -146,13 +148,47 @@ describe.skipIf(!isKbAdminE2EReady())("e2e: knowledge kb 写链路 (live, 自清
"--wait",
"--workspace-id",
workspaceId,
"--quiet",
"--output",
"json",
]);
expect(importUploadRun.exitCode, importUploadRun.stderr).toBe(0);
const importedFileId = importUploadRun.stdout.trim();
const importData = parseStdoutJson<{
files: Array<{ fileId: string }>;
ingestion_id?: string;
final_status?: string;
}>(importUploadRun.stdout);
// These assertions verify the full pipeline completed: ingestion_id proves the
// job was created, and final_status COMPLETED proves parsing finished successfully.
expect(importData.ingestion_id).toBeTruthy();
expect(importData.final_status).toBe("COMPLETED");
const importedFileId = importData.files?.[0]?.fileId;
expect(importedFileId).toMatch(/^file_/);
fixtureFileIds.push(importedFileId);
// 2.8) Verify the imported file is actually visible in the KB — final_status
// COMPLETED only proves the job finished; an independent doc list query
// confirms the file was registered as a document in the index
const importedFileName = basename(importFilePath);
const docListRun = await runCommandE2e(KNOWLEDGE_KB_DELETE_ROUTES, [
"knowledge",
"doc",
"list",
"--index-id",
indexId,
"--workspace-id",
workspaceId,
"--output",
"json",
]);
expect(docListRun.exitCode, docListRun.stderr).toBe(0);
const docListData = parseStdoutJson<{
data?: { rows?: Array<{ doc_name?: string; status?: string }> };
}>(docListRun.stdout);
const importedDoc = docListData.data?.rows?.find((row) =>
row.doc_name?.includes(importedFileName),
);
expect(importedDoc, `expected doc list to contain file "${importedFileName}"`).toBeTruthy();
// 3) Delete the base (--yes non-interactive; deleteKbWithRetry retries on
// IndexStatusError: readiness can lag briefly even after the import completes)
const deleteRun = await deleteKbWithRetry(