mirror of
https://github.com/langgenius/dify.git
synced 2026-09-08 11:04:27 +08:00
fix(knowledge-fs): show task object titles
This commit is contained in:
parent
222d09f474
commit
049a4c514e
@ -1,7 +1,7 @@
|
||||
{
|
||||
"schemaVersion": 5,
|
||||
"subtreeTree": "4c8a98b2c31329e0dbaf7c5e05233afc05f2611b",
|
||||
"openapiSha256": "e147709323f72bfb2296b03ee4535eedb3365bbd232847faef49467ea12a5416",
|
||||
"subtreeTree": "b28f85a3efed562af70e5061cf5a7552394cd908",
|
||||
"openapiSha256": "1214898a307c01993690cf239b0989be748cbf9b91437670f270220223407e06",
|
||||
"capabilityV2AuthManifestSha256": "e322a2fa779d1f40b95c54c1021cffecaec77abbd7b34899573dcdf4ff353109",
|
||||
"capabilityV2AuthTestVectorSha256": "ae0de37b1ff05c40f905cf17a7b410d8971acacf64db07d5ee3d6fecfa559ce3",
|
||||
"productOperationManifestSha256": "5d1241a83bcca12ebbd848928dd3cdda0d2ecaeb5f5336e955eb24a8c8db175b",
|
||||
|
||||
@ -2066,6 +2066,7 @@ class KnowledgeFSBackgroundTaskResponse(ResponseModel):
|
||||
progress_percent: int = Field(ge=0, le=100, validation_alias=AliasChoices("progress_percent", "progressPercent"))
|
||||
progress_total: int = Field(ge=0, validation_alias=AliasChoices("progress_total", "progressTotal"))
|
||||
source_id: str | None = Field(default=None, validation_alias=AliasChoices("source_id", "sourceId"))
|
||||
source_title: str | None = Field(default=None, validation_alias=AliasChoices("source_title", "sourceTitle"))
|
||||
state: Literal["canceled", "completed", "failed", "queued", "running"]
|
||||
task_kind: Literal["document", "document_bulk", "source"] = Field(
|
||||
validation_alias=AliasChoices("task_kind", "taskKind")
|
||||
|
||||
@ -8,6 +8,7 @@ from pydantic import ValidationError
|
||||
from services.knowledge_fs.product_dto import (
|
||||
KnowledgeFSBackgroundTaskListQuery,
|
||||
KnowledgeFSBackgroundTaskListResponse,
|
||||
KnowledgeFSBackgroundTaskResponse,
|
||||
KnowledgeFSBadCaseCreatePayload,
|
||||
KnowledgeFSBadCaseUpdatePayload,
|
||||
KnowledgeFSBulkJobResponse,
|
||||
@ -70,6 +71,30 @@ from services.knowledge_fs.product_dto import (
|
||||
)
|
||||
|
||||
|
||||
def test_background_task_response_accepts_source_title_alias() -> None:
|
||||
task = KnowledgeFSBackgroundTaskResponse.model_validate(
|
||||
{
|
||||
"canCancel": False,
|
||||
"canRetry": False,
|
||||
"createdAt": "2026-09-02T12:00:00Z",
|
||||
"id": "018f0d60-7a49-7cc2-9c1b-5b36f18f2c50",
|
||||
"knowledgeSpaceId": "018f0d60-7a49-7cc2-9c1b-5b36f18f2c40",
|
||||
"operation": "source_sync",
|
||||
"progressCompleted": 1,
|
||||
"progressFailed": 0,
|
||||
"progressPercent": 100,
|
||||
"progressTotal": 1,
|
||||
"sourceId": "018f0d60-7a49-7cc2-9c1b-5b36f18f2c60",
|
||||
"sourceTitle": "Notion support SOP",
|
||||
"state": "completed",
|
||||
"taskKind": "source",
|
||||
"updatedAt": "2026-09-02T12:01:00Z",
|
||||
}
|
||||
)
|
||||
|
||||
assert task.source_title == "Notion support SOP"
|
||||
|
||||
|
||||
def test_quality_replay_payload_requires_exactly_one_selection_mode() -> None:
|
||||
assert KnowledgeFSQualityReplayPayload(selection="all-active").selection == "all-active"
|
||||
assert KnowledgeFSQualityReplayPayload(golden_question_ids=["question-1"]).golden_question_ids == ["question-1"]
|
||||
|
||||
@ -0,0 +1,30 @@
|
||||
# Background Task Object Titles
|
||||
|
||||
## Problem
|
||||
|
||||
The Task drawer displayed a single-document re-index as `Re-index · 1` because progress count
|
||||
took precedence over the document title. Source sync rows depended on the Source list page already
|
||||
being loaded; otherwise their one-item progress count was also presented as the object name.
|
||||
|
||||
## Changes
|
||||
|
||||
- Persist the logical-document title or asset filename in newly created re-index bulk-operation
|
||||
items, so a single re-index has a durable display title.
|
||||
- Add an optional `sourceTitle` to the background-task contract. The task handler resolves all
|
||||
selected Source task names in one bounded, space-scoped repository query, rechecks content
|
||||
grants, and treats title lookup as non-critical enrichment.
|
||||
- Return the same Source title from cancel and retry responses. Deleting, unavailable, or
|
||||
unauthorized Sources never disclose a name and do not make task listing unavailable.
|
||||
- Prefer object names in both task drawer implementations. A true multi-document re-index may
|
||||
still show its item count, while a single task without a resolvable name no longer labels itself
|
||||
as `1`.
|
||||
|
||||
## Verification
|
||||
|
||||
- Focused KnowledgeFS background-task, Source repository, and re-index persistence tests cover
|
||||
PostgreSQL and TiDB placeholders, one-query enrichment, permission filtering, enrichment
|
||||
degradation, and durable filename capture.
|
||||
- Focused document-list and document-detail UI tests cover single re-index filenames and Source
|
||||
task-provided names when the Source list is not loaded.
|
||||
- The Dify DTO test covers camel-case `sourceTitle` validation and the generated TypeScript API
|
||||
contract.
|
||||
@ -1,4 +1,4 @@
|
||||
import type { AuthSubject } from "@knowledge/core";
|
||||
import type { AuthSubject, Source } from "@knowledge/core";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
|
||||
import {
|
||||
@ -72,18 +72,24 @@ describe("background task handlers", () => {
|
||||
const listRecentRuns = vi.fn(async () => ({
|
||||
items: [sourceRun({ createdAt: "2026-07-23T12:04:00.000Z" })],
|
||||
}));
|
||||
const getMany = vi.fn(async () => [sourceRecord()]);
|
||||
const app = backgroundTaskApp({
|
||||
bulkOperations,
|
||||
compilationJobs,
|
||||
documentTasks,
|
||||
sourceRepository: { listRecentRuns },
|
||||
sources: { getMany },
|
||||
});
|
||||
|
||||
const response = await app.request(`/knowledge-spaces/${SPACE_ID}/background-tasks?limit=2`);
|
||||
expect(response.status).toBe(200);
|
||||
const body = await response.json();
|
||||
expect(body.items).toEqual([
|
||||
expect.objectContaining({ id: SOURCE_RUN_ID, taskKind: "source" }),
|
||||
expect.objectContaining({
|
||||
id: SOURCE_RUN_ID,
|
||||
sourceTitle: "Notion support SOP",
|
||||
taskKind: "source",
|
||||
}),
|
||||
expect.objectContaining({ id: BULK_ID, taskKind: "document_bulk" }),
|
||||
]);
|
||||
expect(body.nextCursor).toEqual(expect.any(String));
|
||||
@ -98,6 +104,42 @@ describe("background task handlers", () => {
|
||||
expect(listRecentRuns).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ candidateGrants: ["scope:visible"], limit: 2 }),
|
||||
);
|
||||
expect(getMany).toHaveBeenCalledOnce();
|
||||
expect(getMany).toHaveBeenCalledWith({ ids: [SOURCE_ID], knowledgeSpaceId: SPACE_ID });
|
||||
});
|
||||
|
||||
it("does not disclose a source title outside the current content grants", async () => {
|
||||
const app = backgroundTaskApp({
|
||||
sourceRepository: { listRecentRuns: vi.fn(async () => ({ items: [sourceRun()] })) },
|
||||
sources: {
|
||||
getMany: vi.fn(async () => [sourceRecord({ permissionScope: ["scope:hidden"] })]),
|
||||
},
|
||||
});
|
||||
|
||||
const response = await app.request(`/knowledge-spaces/${SPACE_ID}/background-tasks?limit=10`);
|
||||
expect(response.status).toBe(200);
|
||||
const body = await response.json();
|
||||
expect(body.items).toEqual([
|
||||
expect.objectContaining({ id: SOURCE_RUN_ID, sourceId: SOURCE_ID }),
|
||||
]);
|
||||
expect(body.items[0]).not.toHaveProperty("sourceTitle");
|
||||
});
|
||||
|
||||
it("keeps task listing available when source title enrichment is unavailable", async () => {
|
||||
const app = backgroundTaskApp({
|
||||
sourceRepository: { listRecentRuns: vi.fn(async () => ({ items: [sourceRun()] })) },
|
||||
sources: {
|
||||
getMany: vi.fn(async () => {
|
||||
throw new Error("source catalog unavailable");
|
||||
}),
|
||||
},
|
||||
});
|
||||
|
||||
const response = await app.request(`/knowledge-spaces/${SPACE_ID}/background-tasks?limit=10`);
|
||||
expect(response.status).toBe(200);
|
||||
await expect(response.json()).resolves.toEqual({
|
||||
items: [expect.objectContaining({ id: SOURCE_RUN_ID, sourceId: SOURCE_ID })],
|
||||
});
|
||||
});
|
||||
|
||||
it("does not re-emit a grouped document after its bulk task leaves the page", async () => {
|
||||
@ -288,6 +330,7 @@ describe("background task handlers", () => {
|
||||
);
|
||||
const retry = vi.fn(async () => sourceRun({ state: "queued" }));
|
||||
const app = backgroundTaskApp({
|
||||
sources: { getMany: vi.fn(async () => [sourceRecord()]) },
|
||||
sourceWorkflows: { cancel, retry },
|
||||
});
|
||||
|
||||
@ -298,6 +341,7 @@ describe("background task handlers", () => {
|
||||
expect(canceled.status).toBe(200);
|
||||
await expect(canceled.json()).resolves.toMatchObject({
|
||||
canRetry: true,
|
||||
sourceTitle: "Notion support SOP",
|
||||
state: "canceled",
|
||||
taskKind: "source",
|
||||
});
|
||||
@ -306,7 +350,10 @@ describe("background task handlers", () => {
|
||||
{ method: "POST" },
|
||||
);
|
||||
expect(retried.status).toBe(200);
|
||||
await expect(retried.json()).resolves.toMatchObject({ state: "queued" });
|
||||
await expect(retried.json()).resolves.toMatchObject({
|
||||
sourceTitle: "Notion support SOP",
|
||||
state: "queued",
|
||||
});
|
||||
expect(cancel).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ knowledgeSpaceId: SPACE_ID, runId: SOURCE_RUN_ID }),
|
||||
);
|
||||
@ -730,6 +777,7 @@ function backgroundTaskApp(overrides: {
|
||||
readonly durableDeletions?: RegisterBackgroundTaskHandlersOptions["durableDeletions"];
|
||||
readonly space?: { readonly id: string } | null;
|
||||
readonly sourceRepository?: object;
|
||||
readonly sources?: object;
|
||||
readonly sourceWorkflows?: object;
|
||||
}) {
|
||||
const app = createKnowledgeGatewayApp();
|
||||
@ -763,6 +811,7 @@ function backgroundTaskApp(overrides: {
|
||||
...(overrides.sourceRepository
|
||||
? { sourceRepository: overrides.sourceRepository as never }
|
||||
: {}),
|
||||
...(overrides.sources ? { sources: overrides.sources as never } : {}),
|
||||
...(overrides.sourceWorkflows ? { sourceWorkflows: overrides.sourceWorkflows as never } : {}),
|
||||
spaces: {
|
||||
get: vi.fn(async ({ id, tenantId }) => {
|
||||
@ -892,6 +941,23 @@ function sourceRun(patch: Partial<SourceWorkflowRun> = {}): SourceWorkflowRun {
|
||||
};
|
||||
}
|
||||
|
||||
function sourceRecord(patch: Partial<Source> = {}): Source {
|
||||
return {
|
||||
createdAt: "2026-07-23T12:00:00.000Z",
|
||||
id: SOURCE_ID,
|
||||
knowledgeSpaceId: SPACE_ID,
|
||||
metadata: {},
|
||||
name: "Notion support SOP",
|
||||
permissionScope: ["scope:visible"],
|
||||
status: "active",
|
||||
type: "web",
|
||||
updatedAt: "2026-07-23T12:01:00.000Z",
|
||||
uri: "https://example.com",
|
||||
version: 1,
|
||||
...patch,
|
||||
};
|
||||
}
|
||||
|
||||
function authorizationDecision() {
|
||||
return {
|
||||
accessContext: {} as never,
|
||||
|
||||
@ -16,7 +16,10 @@ import {
|
||||
} from "./background-task-routes";
|
||||
import { type BulkOperationRepository, canReadBulkOperation } from "./bulk-operation";
|
||||
import { summarizeBulkOperation } from "./bulk-operation-summary";
|
||||
import { currentCandidateGrants } from "./candidate-content-authorization";
|
||||
import {
|
||||
candidatePermissionScopeAllows,
|
||||
currentCandidateGrants,
|
||||
} from "./candidate-content-authorization";
|
||||
import { issueKnowledgeSpaceDurablePermission } from "./derived-result-authorization";
|
||||
import type {
|
||||
DocumentCompilationJobStateMachine,
|
||||
@ -40,6 +43,7 @@ import type {
|
||||
SourceProductWorkflowService,
|
||||
SourceWorkflowPrincipal,
|
||||
} from "./source-product-workflow";
|
||||
import type { SourceRepository } from "./source-repository";
|
||||
|
||||
type CandidateSource = "bulk" | "document" | "source";
|
||||
|
||||
@ -59,6 +63,7 @@ export interface RegisterBackgroundTaskHandlersOptions {
|
||||
readonly durableDeletionJobs?: Pick<DurableDeletionRepository, "getJob"> | undefined;
|
||||
readonly durableDeletions?: DurableDeletionService | undefined;
|
||||
readonly sourceRepository?: SourceProductWorkflowRepository | undefined;
|
||||
readonly sources?: Pick<SourceRepository, "getMany"> | undefined;
|
||||
readonly sourceWorkflows?: SourceProductWorkflowService | undefined;
|
||||
readonly spaces: KnowledgeSpaceRepository;
|
||||
}
|
||||
@ -73,6 +78,7 @@ export function registerBackgroundTaskHandlers({
|
||||
durableDeletionJobs,
|
||||
durableDeletions,
|
||||
sourceRepository,
|
||||
sources,
|
||||
sourceWorkflows,
|
||||
spaces,
|
||||
}: RegisterBackgroundTaskHandlersOptions): void {
|
||||
@ -176,6 +182,12 @@ export function registerBackgroundTaskHandlers({
|
||||
compareCandidates,
|
||||
);
|
||||
const selected = candidates.slice(0, query.limit);
|
||||
const selectedTasks = await attachSourceTitles(
|
||||
selected.map((candidate) => candidate.task),
|
||||
sources,
|
||||
grants,
|
||||
params.id,
|
||||
);
|
||||
let next = advanceCursor(cursor, selected);
|
||||
if (documentCandidates.length === 0 && documentPage.items.length > 0) {
|
||||
const last = documentPage.items.at(-1);
|
||||
@ -191,7 +203,7 @@ export function registerBackgroundTaskHandlers({
|
||||
Boolean(documentPage.nextCursor || bulkPage.nextCursor || sourcePage.nextCursor);
|
||||
return context.json(
|
||||
{
|
||||
items: selected.map((candidate) => candidate.task),
|
||||
items: selectedTasks,
|
||||
...(hasMore ? { nextCursor: encodeBackgroundTaskCursor(next) } : {}),
|
||||
},
|
||||
200,
|
||||
@ -247,7 +259,9 @@ export function registerBackgroundTaskHandlers({
|
||||
: await controlSourceTask({
|
||||
action,
|
||||
context,
|
||||
grants,
|
||||
knowledgeSpaceId: params.id,
|
||||
sources,
|
||||
sourceWorkflows,
|
||||
taskId: params.taskId,
|
||||
});
|
||||
@ -444,7 +458,9 @@ async function controlSourceTask(input: {
|
||||
readonly action: "cancel" | "retry";
|
||||
// biome-ignore lint/suspicious/noExplicitAny: bounded OpenAPI handler context
|
||||
readonly context: any;
|
||||
readonly grants: readonly string[];
|
||||
readonly knowledgeSpaceId: string;
|
||||
readonly sources?: Pick<SourceRepository, "getMany"> | undefined;
|
||||
readonly sourceWorkflows?: SourceProductWorkflowService | undefined;
|
||||
readonly taskId: string;
|
||||
}): Promise<BackgroundTask | null> {
|
||||
@ -463,7 +479,44 @@ async function controlSourceTask(input: {
|
||||
knowledgeSpaceId: input.knowledgeSpaceId,
|
||||
runId: input.taskId,
|
||||
});
|
||||
return run ? sourceBackgroundTask(run) : null;
|
||||
if (!run) return null;
|
||||
const task = sourceBackgroundTask(run);
|
||||
return (
|
||||
(await attachSourceTitles([task], input.sources, input.grants, input.knowledgeSpaceId))[0] ??
|
||||
task
|
||||
);
|
||||
}
|
||||
|
||||
async function attachSourceTitles(
|
||||
tasks: readonly BackgroundTask[],
|
||||
sources: Pick<SourceRepository, "getMany"> | undefined,
|
||||
candidateGrants: readonly string[],
|
||||
knowledgeSpaceId: string,
|
||||
): Promise<BackgroundTask[]> {
|
||||
if (!sources) return [...tasks];
|
||||
const sourceIds = [...new Set(tasks.flatMap((task) => (task.sourceId ? [task.sourceId] : [])))];
|
||||
if (sourceIds.length === 0) return [...tasks];
|
||||
|
||||
try {
|
||||
const visibleSourceTitles = new Map(
|
||||
(
|
||||
await sources.getMany({
|
||||
ids: sourceIds,
|
||||
knowledgeSpaceId,
|
||||
})
|
||||
)
|
||||
.filter((source) => candidatePermissionScopeAllows(source.permissionScope, candidateGrants))
|
||||
.map((source) => [source.id, source.name] as const),
|
||||
);
|
||||
return tasks.map((task) => {
|
||||
const sourceTitle = task.sourceId ? visibleSourceTitles.get(task.sourceId) : undefined;
|
||||
return sourceTitle ? { ...task, sourceTitle } : task;
|
||||
});
|
||||
} catch {
|
||||
// A title is display enrichment only; task control and visibility must remain available if the
|
||||
// source catalog is temporarily unavailable.
|
||||
return [...tasks];
|
||||
}
|
||||
}
|
||||
|
||||
async function controlPermission(
|
||||
|
||||
@ -80,6 +80,7 @@ export const BackgroundTaskSchema = z.object({
|
||||
progressPercent: z.number().int().min(0).max(100),
|
||||
progressTotal: z.number().int().nonnegative(),
|
||||
sourceId: z.string().uuid().optional(),
|
||||
sourceTitle: z.string().min(1).optional(),
|
||||
state: BackgroundTaskStateSchema,
|
||||
semanticEnrichment: z
|
||||
.object({
|
||||
|
||||
@ -55,6 +55,7 @@ export interface BackgroundTask {
|
||||
readonly progressPercent: number;
|
||||
readonly progressTotal: number;
|
||||
readonly sourceId?: string | undefined;
|
||||
readonly sourceTitle?: string | undefined;
|
||||
readonly state: BackgroundTaskState;
|
||||
readonly semanticEnrichment?: DocumentSemanticEnrichmentProgress | undefined;
|
||||
readonly taskKind: BackgroundTaskKind;
|
||||
|
||||
@ -440,6 +440,7 @@ export function registerDocumentWriteHandlers({
|
||||
bulkItems.push({
|
||||
compilationJobId: compilationJob.id,
|
||||
documentId: logicalDocument?.id ?? asset.id,
|
||||
documentTitle: logicalDocument?.title ?? asset.filename,
|
||||
requiredPermissionScope: requiredPermissionScopeForAsset(asset),
|
||||
status: "queued",
|
||||
});
|
||||
|
||||
@ -3627,9 +3627,14 @@ describe("document write gateway integration", () => {
|
||||
now: () => 1_777_777_000_000,
|
||||
repository: createInMemoryDocumentCompilationJobRepository({ maxJobs: 10 }),
|
||||
});
|
||||
const bulkOperations = createInMemoryBulkOperationRepository({
|
||||
maxItems: 10,
|
||||
maxOperations: 10,
|
||||
});
|
||||
const app = createKnowledgeGateway({
|
||||
adapter,
|
||||
auth: createTestAuthVerifier(),
|
||||
bulkOperations,
|
||||
documentAssets: assets,
|
||||
documentCompilationJobs: compilationJobs,
|
||||
generateBulkUploadId: () => "bulk-reindex-1",
|
||||
@ -3692,6 +3697,14 @@ describe("document write gateway integration", () => {
|
||||
],
|
||||
total: 2,
|
||||
});
|
||||
const storedOperation = await bulkOperations.get({
|
||||
id: "bulk-reindex-1",
|
||||
tenantId: "tenant-1",
|
||||
});
|
||||
expect(storedOperation?.items[0]).toMatchObject({
|
||||
documentId: first.id,
|
||||
documentTitle: "First.md",
|
||||
});
|
||||
|
||||
const all = await app.request(
|
||||
"/knowledge-spaces/018f0d60-7a49-7cc2-9c1b-5b36f18f2c42/documents/bulk/reindex",
|
||||
|
||||
@ -2219,6 +2219,7 @@ export function createKnowledgeGateway({
|
||||
...(durableDeletionService ? { durableDeletions: durableDeletionService } : {}),
|
||||
...(sourceProduct ? { sourceRepository: sourceProduct.repository } : {}),
|
||||
...(sourceProductWorkflows ? { sourceWorkflows: sourceProductWorkflows } : {}),
|
||||
sources: sourceRepository,
|
||||
spaces,
|
||||
});
|
||||
registerSourceHandlers({
|
||||
|
||||
@ -54,6 +54,12 @@ describe("createInMemorySourceRepository", () => {
|
||||
await expect(
|
||||
repository.get({ id: created.id, knowledgeSpaceId: SPACE_A }),
|
||||
).resolves.toMatchObject({ id: created.id });
|
||||
await expect(
|
||||
repository.getMany({ ids: [created.id, "missing", created.id], knowledgeSpaceId: SPACE_A }),
|
||||
).resolves.toEqual([expect.objectContaining({ id: created.id, name: "Docs crawl" })]);
|
||||
await expect(
|
||||
repository.getMany({ ids: [created.id], knowledgeSpaceId: SPACE_B }),
|
||||
).resolves.toEqual([]);
|
||||
|
||||
const updated = await repository.update({
|
||||
id: created.id,
|
||||
@ -190,6 +196,50 @@ describe("createDatabaseSourceRepository", () => {
|
||||
expect(calls[1]?.sql).not.toContain("<> 'deleting'");
|
||||
});
|
||||
|
||||
it.each(["postgres", "tidb"] as const)(
|
||||
"batch-loads active sources in one space-scoped query for %s",
|
||||
async (dialect) => {
|
||||
const calls: DatabaseExecuteInput[] = [];
|
||||
const firstId = "00000000-0000-4000-8000-000000000001";
|
||||
const secondId = "00000000-0000-4000-8000-000000000002";
|
||||
const repository = createDatabaseSourceRepository({
|
||||
database: createSchemaDatabaseAdapter({
|
||||
executor: async (input) => {
|
||||
calls.push(input);
|
||||
return {
|
||||
rows: [
|
||||
sourceRow(firstId, { name: "First source" }),
|
||||
sourceRow(secondId, { name: "Second source" }),
|
||||
],
|
||||
rowsAffected: 2,
|
||||
};
|
||||
},
|
||||
kind: dialect,
|
||||
}),
|
||||
});
|
||||
|
||||
await expect(
|
||||
repository.getMany({
|
||||
ids: [secondId, firstId, secondId],
|
||||
knowledgeSpaceId: SPACE_A,
|
||||
}),
|
||||
).resolves.toEqual([
|
||||
expect.objectContaining({ id: firstId, name: "First source" }),
|
||||
expect.objectContaining({ id: secondId, name: "Second source" }),
|
||||
]);
|
||||
expect(calls).toHaveLength(1);
|
||||
expect(calls[0]?.params).toEqual([SPACE_A, firstId, secondId]);
|
||||
expect(calls[0]?.sql).toContain("status");
|
||||
expect(calls[0]?.sql).toContain("<> 'deleting'");
|
||||
expect(calls[0]?.sql).toContain("ORDER BY");
|
||||
if (dialect === "tidb") {
|
||||
const call = calls[0];
|
||||
if (!call) throw new Error("Expected a batch source lookup query");
|
||||
expect(call.sql.match(/\?/g)).toHaveLength(call.params.length);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
it.each(["postgres", "tidb"] as const)(
|
||||
"maps the lifecycle-only deleting status through getForDeletion for %s",
|
||||
async (dialect) => {
|
||||
|
||||
@ -41,6 +41,11 @@ export interface SourceLookupInput {
|
||||
readonly knowledgeSpaceId: string;
|
||||
}
|
||||
|
||||
export interface SourceBatchLookupInput {
|
||||
readonly ids: readonly string[];
|
||||
readonly knowledgeSpaceId: string;
|
||||
}
|
||||
|
||||
/** Internal row shape used only while a durable deletion is fenced. */
|
||||
export type SourceForDeletion = Omit<Source, "status"> & {
|
||||
readonly status: Source["status"] | "deleting";
|
||||
@ -136,6 +141,8 @@ export interface SourceRepository {
|
||||
): Promise<Source | null>;
|
||||
create(input: CreateSourceInput): Promise<Source>;
|
||||
get(input: SourceLookupInput): Promise<Source | null>;
|
||||
/** Bounded, space-scoped lookup used to enrich task lists without per-row queries. */
|
||||
getMany(input: SourceBatchLookupInput): Promise<Source[]>;
|
||||
/** Internal durable-deletion lookup; includes a row already fenced as deleting. */
|
||||
getForDeletion(input: SourceLookupInput): Promise<SourceForDeletion | null>;
|
||||
list(input: ListSourcesInput): Promise<ListSourcesResult>;
|
||||
@ -246,6 +253,13 @@ export function createInMemorySourceRepository({
|
||||
|
||||
return source && source.knowledgeSpaceId === knowledgeSpaceId ? cloneSource(source) : null;
|
||||
},
|
||||
getMany: async ({ ids, knowledgeSpaceId }) => {
|
||||
const requestedIds = normalizeSourceBatchLookupIds(ids);
|
||||
return requestedIds.flatMap((id) => {
|
||||
const source = sources.get(id);
|
||||
return source?.knowledgeSpaceId === knowledgeSpaceId ? [cloneSource(source)] : [];
|
||||
});
|
||||
},
|
||||
getForDeletion: async ({ id, knowledgeSpaceId }) => {
|
||||
const source = sources.get(id);
|
||||
|
||||
@ -517,6 +531,30 @@ export function createDatabaseSourceRepository({
|
||||
return result.rows[0] ? mapDatabaseSourceRow(result.rows[0]) : source;
|
||||
},
|
||||
get: async (input) => databaseSourceGet(database, input),
|
||||
getMany: async ({ ids, knowledgeSpaceId }) => {
|
||||
const requestedIds = normalizeSourceBatchLookupIds(ids);
|
||||
if (requestedIds.length === 0) return [];
|
||||
const params = [knowledgeSpaceId, ...requestedIds] satisfies readonly DatabaseQueryValue[];
|
||||
const result = await database.execute({
|
||||
maxRows: requestedIds.length,
|
||||
operation: "select",
|
||||
params,
|
||||
sql: `SELECT * FROM ${quoteDatabaseIdentifier(database, tableName)} WHERE ${quoteDatabaseIdentifier(
|
||||
database,
|
||||
"knowledge_space_id",
|
||||
)} = ${databasePlaceholder(database, 1)} AND ${quoteDatabaseIdentifier(
|
||||
database,
|
||||
"id",
|
||||
)} IN (${requestedIds
|
||||
.map((_, index) => databasePlaceholder(database, index + 2))
|
||||
.join(", ")}) AND ${quoteDatabaseIdentifier(
|
||||
database,
|
||||
"status",
|
||||
)} <> 'deleting' ORDER BY ${quoteDatabaseIdentifier(database, "id")} ASC;`,
|
||||
tableName,
|
||||
});
|
||||
return result.rows.map(mapDatabaseSourceRow).map(cloneSource);
|
||||
},
|
||||
getForDeletion: async (input) => databaseSourceGetForDeletion(database, input),
|
||||
list: async ({ cursor, knowledgeSpaceId, limit }) => {
|
||||
validateSourceListLimit(limit);
|
||||
@ -790,3 +828,11 @@ function validateSourceListLimit(limit: number): void {
|
||||
throw new Error("Source list limit must be at least 1");
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeSourceBatchLookupIds(ids: readonly string[]): string[] {
|
||||
const uniqueIds = [...new Set(ids)].sort((left, right) => left.localeCompare(right));
|
||||
if (uniqueIds.length > 100) {
|
||||
throw new Error("Source batch lookup exceeds 100 ids");
|
||||
}
|
||||
return uniqueIds;
|
||||
}
|
||||
|
||||
@ -174,6 +174,7 @@ export type KnowledgeFsBackgroundTaskResponse = {
|
||||
progress_percent: number
|
||||
progress_total: number
|
||||
source_id?: string | null
|
||||
source_title?: string | null
|
||||
state: 'canceled' | 'completed' | 'failed' | 'queued' | 'running'
|
||||
task_kind: 'document' | 'document_bulk' | 'source'
|
||||
updated_at: string
|
||||
@ -866,6 +867,8 @@ export type KnowledgeFsSourceWorkflowResponse = {
|
||||
| 'RESEARCH_TASK_FAILED'
|
||||
| 'RESEARCH_TASK_PERMISSION_SNAPSHOT_INVALID'
|
||||
| 'RESEARCH_TASK_RUNTIME_SNAPSHOT_INVALID'
|
||||
| 'RETRIEVAL_DELETION_IN_PROGRESS'
|
||||
| 'RETRIEVAL_EXECUTION_LEASE_LOST'
|
||||
| 'SOURCE_BULK_ACTION_FAILED'
|
||||
| 'SOURCE_CREDENTIAL_CONFIG_INVALID'
|
||||
| 'SOURCE_CREDENTIAL_MUTATION_FAILED'
|
||||
@ -886,6 +889,7 @@ export type KnowledgeFsSourceWorkflowResponse = {
|
||||
| 'SOURCE_SECRET_INTEGRITY_FAILED'
|
||||
| 'SOURCE_SECRET_REF_CONFLICT'
|
||||
| 'SOURCE_SYNC_FAILED'
|
||||
| 'SOURCE_SYNC_SELECTION_MISMATCH'
|
||||
| 'SOURCE_WEBSITE_CRAWL_CONFIG_INVALID'
|
||||
| 'SOURCE_WEBSITE_CRAWL_FAILED'
|
||||
| 'SOURCE_WORKFLOW_FAILED'
|
||||
@ -1093,6 +1097,8 @@ export type KnowledgeFsSourceCredentialTestResponse = {
|
||||
| 'RESEARCH_TASK_FAILED'
|
||||
| 'RESEARCH_TASK_PERMISSION_SNAPSHOT_INVALID'
|
||||
| 'RESEARCH_TASK_RUNTIME_SNAPSHOT_INVALID'
|
||||
| 'RETRIEVAL_DELETION_IN_PROGRESS'
|
||||
| 'RETRIEVAL_EXECUTION_LEASE_LOST'
|
||||
| 'SOURCE_BULK_ACTION_FAILED'
|
||||
| 'SOURCE_CREDENTIAL_CONFIG_INVALID'
|
||||
| 'SOURCE_CREDENTIAL_MUTATION_FAILED'
|
||||
@ -1113,6 +1119,7 @@ export type KnowledgeFsSourceCredentialTestResponse = {
|
||||
| 'SOURCE_SECRET_INTEGRITY_FAILED'
|
||||
| 'SOURCE_SECRET_REF_CONFLICT'
|
||||
| 'SOURCE_SYNC_FAILED'
|
||||
| 'SOURCE_SYNC_SELECTION_MISMATCH'
|
||||
| 'SOURCE_WEBSITE_CRAWL_CONFIG_INVALID'
|
||||
| 'SOURCE_WEBSITE_CRAWL_FAILED'
|
||||
| 'SOURCE_WORKFLOW_FAILED'
|
||||
@ -1449,6 +1456,8 @@ export type KnowledgeFsPublicFailureResponse = {
|
||||
| 'RESEARCH_TASK_FAILED'
|
||||
| 'RESEARCH_TASK_PERMISSION_SNAPSHOT_INVALID'
|
||||
| 'RESEARCH_TASK_RUNTIME_SNAPSHOT_INVALID'
|
||||
| 'RETRIEVAL_DELETION_IN_PROGRESS'
|
||||
| 'RETRIEVAL_EXECUTION_LEASE_LOST'
|
||||
| 'SOURCE_BULK_ACTION_FAILED'
|
||||
| 'SOURCE_CREDENTIAL_CONFIG_INVALID'
|
||||
| 'SOURCE_CREDENTIAL_MUTATION_FAILED'
|
||||
@ -1469,6 +1478,7 @@ export type KnowledgeFsPublicFailureResponse = {
|
||||
| 'SOURCE_SECRET_INTEGRITY_FAILED'
|
||||
| 'SOURCE_SECRET_REF_CONFLICT'
|
||||
| 'SOURCE_SYNC_FAILED'
|
||||
| 'SOURCE_SYNC_SELECTION_MISMATCH'
|
||||
| 'SOURCE_WEBSITE_CRAWL_CONFIG_INVALID'
|
||||
| 'SOURCE_WEBSITE_CRAWL_FAILED'
|
||||
| 'SOURCE_WORKFLOW_FAILED'
|
||||
@ -1985,6 +1995,8 @@ export type KnowledgeFsSourceImportFailureResponse = {
|
||||
| 'RESEARCH_TASK_FAILED'
|
||||
| 'RESEARCH_TASK_PERMISSION_SNAPSHOT_INVALID'
|
||||
| 'RESEARCH_TASK_RUNTIME_SNAPSHOT_INVALID'
|
||||
| 'RETRIEVAL_DELETION_IN_PROGRESS'
|
||||
| 'RETRIEVAL_EXECUTION_LEASE_LOST'
|
||||
| 'SOURCE_BULK_ACTION_FAILED'
|
||||
| 'SOURCE_CREDENTIAL_CONFIG_INVALID'
|
||||
| 'SOURCE_CREDENTIAL_MUTATION_FAILED'
|
||||
@ -2005,6 +2017,7 @@ export type KnowledgeFsSourceImportFailureResponse = {
|
||||
| 'SOURCE_SECRET_INTEGRITY_FAILED'
|
||||
| 'SOURCE_SECRET_REF_CONFLICT'
|
||||
| 'SOURCE_SYNC_FAILED'
|
||||
| 'SOURCE_SYNC_SELECTION_MISMATCH'
|
||||
| 'SOURCE_WEBSITE_CRAWL_CONFIG_INVALID'
|
||||
| 'SOURCE_WEBSITE_CRAWL_FAILED'
|
||||
| 'SOURCE_WORKFLOW_FAILED'
|
||||
|
||||
@ -952,6 +952,8 @@ export const zKnowledgeFsPublicFailureResponse = z.object({
|
||||
'RESEARCH_TASK_FAILED',
|
||||
'RESEARCH_TASK_PERMISSION_SNAPSHOT_INVALID',
|
||||
'RESEARCH_TASK_RUNTIME_SNAPSHOT_INVALID',
|
||||
'RETRIEVAL_DELETION_IN_PROGRESS',
|
||||
'RETRIEVAL_EXECUTION_LEASE_LOST',
|
||||
'SOURCE_BULK_ACTION_FAILED',
|
||||
'SOURCE_CREDENTIAL_CONFIG_INVALID',
|
||||
'SOURCE_CREDENTIAL_MUTATION_FAILED',
|
||||
@ -972,6 +974,7 @@ export const zKnowledgeFsPublicFailureResponse = z.object({
|
||||
'SOURCE_SECRET_INTEGRITY_FAILED',
|
||||
'SOURCE_SECRET_REF_CONFLICT',
|
||||
'SOURCE_SYNC_FAILED',
|
||||
'SOURCE_SYNC_SELECTION_MISMATCH',
|
||||
'SOURCE_WEBSITE_CRAWL_CONFIG_INVALID',
|
||||
'SOURCE_WEBSITE_CRAWL_FAILED',
|
||||
'SOURCE_WORKFLOW_FAILED',
|
||||
@ -1055,6 +1058,8 @@ export const zKnowledgeFsSourceWorkflowResponse = z.object({
|
||||
'RESEARCH_TASK_FAILED',
|
||||
'RESEARCH_TASK_PERMISSION_SNAPSHOT_INVALID',
|
||||
'RESEARCH_TASK_RUNTIME_SNAPSHOT_INVALID',
|
||||
'RETRIEVAL_DELETION_IN_PROGRESS',
|
||||
'RETRIEVAL_EXECUTION_LEASE_LOST',
|
||||
'SOURCE_BULK_ACTION_FAILED',
|
||||
'SOURCE_CREDENTIAL_CONFIG_INVALID',
|
||||
'SOURCE_CREDENTIAL_MUTATION_FAILED',
|
||||
@ -1075,6 +1080,7 @@ export const zKnowledgeFsSourceWorkflowResponse = z.object({
|
||||
'SOURCE_SECRET_INTEGRITY_FAILED',
|
||||
'SOURCE_SECRET_REF_CONFLICT',
|
||||
'SOURCE_SYNC_FAILED',
|
||||
'SOURCE_SYNC_SELECTION_MISMATCH',
|
||||
'SOURCE_WEBSITE_CRAWL_CONFIG_INVALID',
|
||||
'SOURCE_WEBSITE_CRAWL_FAILED',
|
||||
'SOURCE_WORKFLOW_FAILED',
|
||||
@ -1170,6 +1176,8 @@ export const zKnowledgeFsSourceCredentialTestResponse = z.object({
|
||||
'RESEARCH_TASK_FAILED',
|
||||
'RESEARCH_TASK_PERMISSION_SNAPSHOT_INVALID',
|
||||
'RESEARCH_TASK_RUNTIME_SNAPSHOT_INVALID',
|
||||
'RETRIEVAL_DELETION_IN_PROGRESS',
|
||||
'RETRIEVAL_EXECUTION_LEASE_LOST',
|
||||
'SOURCE_BULK_ACTION_FAILED',
|
||||
'SOURCE_CREDENTIAL_CONFIG_INVALID',
|
||||
'SOURCE_CREDENTIAL_MUTATION_FAILED',
|
||||
@ -1190,6 +1198,7 @@ export const zKnowledgeFsSourceCredentialTestResponse = z.object({
|
||||
'SOURCE_SECRET_INTEGRITY_FAILED',
|
||||
'SOURCE_SECRET_REF_CONFLICT',
|
||||
'SOURCE_SYNC_FAILED',
|
||||
'SOURCE_SYNC_SELECTION_MISMATCH',
|
||||
'SOURCE_WEBSITE_CRAWL_CONFIG_INVALID',
|
||||
'SOURCE_WEBSITE_CRAWL_FAILED',
|
||||
'SOURCE_WORKFLOW_FAILED',
|
||||
@ -1248,6 +1257,7 @@ export const zKnowledgeFsBackgroundTaskResponse = z.object({
|
||||
progress_percent: z.int().gte(0).lte(100),
|
||||
progress_total: z.int().gte(0),
|
||||
source_id: z.string().nullish(),
|
||||
source_title: z.string().nullish(),
|
||||
state: z.enum(['canceled', 'completed', 'failed', 'queued', 'running']),
|
||||
task_kind: z.enum(['document', 'document_bulk', 'source']),
|
||||
updated_at: z.iso.datetime(),
|
||||
@ -1936,6 +1946,8 @@ export const zKnowledgeFsSourceImportFailureResponse = z.object({
|
||||
'RESEARCH_TASK_FAILED',
|
||||
'RESEARCH_TASK_PERMISSION_SNAPSHOT_INVALID',
|
||||
'RESEARCH_TASK_RUNTIME_SNAPSHOT_INVALID',
|
||||
'RETRIEVAL_DELETION_IN_PROGRESS',
|
||||
'RETRIEVAL_EXECUTION_LEASE_LOST',
|
||||
'SOURCE_BULK_ACTION_FAILED',
|
||||
'SOURCE_CREDENTIAL_CONFIG_INVALID',
|
||||
'SOURCE_CREDENTIAL_MUTATION_FAILED',
|
||||
@ -1956,6 +1968,7 @@ export const zKnowledgeFsSourceImportFailureResponse = z.object({
|
||||
'SOURCE_SECRET_INTEGRITY_FAILED',
|
||||
'SOURCE_SECRET_REF_CONFLICT',
|
||||
'SOURCE_SYNC_FAILED',
|
||||
'SOURCE_SYNC_SELECTION_MISMATCH',
|
||||
'SOURCE_WEBSITE_CRAWL_CONFIG_INVALID',
|
||||
'SOURCE_WEBSITE_CRAWL_FAILED',
|
||||
'SOURCE_WORKFLOW_FAILED',
|
||||
|
||||
@ -1687,6 +1687,8 @@ export type KnowledgeFsPublicFailureResponse = {
|
||||
| 'RESEARCH_TASK_FAILED'
|
||||
| 'RESEARCH_TASK_PERMISSION_SNAPSHOT_INVALID'
|
||||
| 'RESEARCH_TASK_RUNTIME_SNAPSHOT_INVALID'
|
||||
| 'RETRIEVAL_DELETION_IN_PROGRESS'
|
||||
| 'RETRIEVAL_EXECUTION_LEASE_LOST'
|
||||
| 'SOURCE_BULK_ACTION_FAILED'
|
||||
| 'SOURCE_CREDENTIAL_CONFIG_INVALID'
|
||||
| 'SOURCE_CREDENTIAL_MUTATION_FAILED'
|
||||
@ -1707,6 +1709,7 @@ export type KnowledgeFsPublicFailureResponse = {
|
||||
| 'SOURCE_SECRET_INTEGRITY_FAILED'
|
||||
| 'SOURCE_SECRET_REF_CONFLICT'
|
||||
| 'SOURCE_SYNC_FAILED'
|
||||
| 'SOURCE_SYNC_SELECTION_MISMATCH'
|
||||
| 'SOURCE_WEBSITE_CRAWL_CONFIG_INVALID'
|
||||
| 'SOURCE_WEBSITE_CRAWL_FAILED'
|
||||
| 'SOURCE_WORKFLOW_FAILED'
|
||||
@ -1977,6 +1980,8 @@ export type KnowledgeFsSourceCredentialTestResponse = {
|
||||
| 'RESEARCH_TASK_FAILED'
|
||||
| 'RESEARCH_TASK_PERMISSION_SNAPSHOT_INVALID'
|
||||
| 'RESEARCH_TASK_RUNTIME_SNAPSHOT_INVALID'
|
||||
| 'RETRIEVAL_DELETION_IN_PROGRESS'
|
||||
| 'RETRIEVAL_EXECUTION_LEASE_LOST'
|
||||
| 'SOURCE_BULK_ACTION_FAILED'
|
||||
| 'SOURCE_CREDENTIAL_CONFIG_INVALID'
|
||||
| 'SOURCE_CREDENTIAL_MUTATION_FAILED'
|
||||
@ -1997,6 +2002,7 @@ export type KnowledgeFsSourceCredentialTestResponse = {
|
||||
| 'SOURCE_SECRET_INTEGRITY_FAILED'
|
||||
| 'SOURCE_SECRET_REF_CONFLICT'
|
||||
| 'SOURCE_SYNC_FAILED'
|
||||
| 'SOURCE_SYNC_SELECTION_MISMATCH'
|
||||
| 'SOURCE_WEBSITE_CRAWL_CONFIG_INVALID'
|
||||
| 'SOURCE_WEBSITE_CRAWL_FAILED'
|
||||
| 'SOURCE_WORKFLOW_FAILED'
|
||||
@ -2106,6 +2112,8 @@ export type KnowledgeFsSourceImportFailureResponse = {
|
||||
| 'RESEARCH_TASK_FAILED'
|
||||
| 'RESEARCH_TASK_PERMISSION_SNAPSHOT_INVALID'
|
||||
| 'RESEARCH_TASK_RUNTIME_SNAPSHOT_INVALID'
|
||||
| 'RETRIEVAL_DELETION_IN_PROGRESS'
|
||||
| 'RETRIEVAL_EXECUTION_LEASE_LOST'
|
||||
| 'SOURCE_BULK_ACTION_FAILED'
|
||||
| 'SOURCE_CREDENTIAL_CONFIG_INVALID'
|
||||
| 'SOURCE_CREDENTIAL_MUTATION_FAILED'
|
||||
@ -2126,6 +2134,7 @@ export type KnowledgeFsSourceImportFailureResponse = {
|
||||
| 'SOURCE_SECRET_INTEGRITY_FAILED'
|
||||
| 'SOURCE_SECRET_REF_CONFLICT'
|
||||
| 'SOURCE_SYNC_FAILED'
|
||||
| 'SOURCE_SYNC_SELECTION_MISMATCH'
|
||||
| 'SOURCE_WEBSITE_CRAWL_CONFIG_INVALID'
|
||||
| 'SOURCE_WEBSITE_CRAWL_FAILED'
|
||||
| 'SOURCE_WORKFLOW_FAILED'
|
||||
@ -2308,6 +2317,8 @@ export type KnowledgeFsSourceWorkflowResponse = {
|
||||
| 'RESEARCH_TASK_FAILED'
|
||||
| 'RESEARCH_TASK_PERMISSION_SNAPSHOT_INVALID'
|
||||
| 'RESEARCH_TASK_RUNTIME_SNAPSHOT_INVALID'
|
||||
| 'RETRIEVAL_DELETION_IN_PROGRESS'
|
||||
| 'RETRIEVAL_EXECUTION_LEASE_LOST'
|
||||
| 'SOURCE_BULK_ACTION_FAILED'
|
||||
| 'SOURCE_CREDENTIAL_CONFIG_INVALID'
|
||||
| 'SOURCE_CREDENTIAL_MUTATION_FAILED'
|
||||
@ -2328,6 +2339,7 @@ export type KnowledgeFsSourceWorkflowResponse = {
|
||||
| 'SOURCE_SECRET_INTEGRITY_FAILED'
|
||||
| 'SOURCE_SECRET_REF_CONFLICT'
|
||||
| 'SOURCE_SYNC_FAILED'
|
||||
| 'SOURCE_SYNC_SELECTION_MISMATCH'
|
||||
| 'SOURCE_WEBSITE_CRAWL_CONFIG_INVALID'
|
||||
| 'SOURCE_WEBSITE_CRAWL_FAILED'
|
||||
| 'SOURCE_WORKFLOW_FAILED'
|
||||
|
||||
@ -1944,6 +1944,8 @@ export const zKnowledgeFsPublicFailureResponse = z.object({
|
||||
'RESEARCH_TASK_FAILED',
|
||||
'RESEARCH_TASK_PERMISSION_SNAPSHOT_INVALID',
|
||||
'RESEARCH_TASK_RUNTIME_SNAPSHOT_INVALID',
|
||||
'RETRIEVAL_DELETION_IN_PROGRESS',
|
||||
'RETRIEVAL_EXECUTION_LEASE_LOST',
|
||||
'SOURCE_BULK_ACTION_FAILED',
|
||||
'SOURCE_CREDENTIAL_CONFIG_INVALID',
|
||||
'SOURCE_CREDENTIAL_MUTATION_FAILED',
|
||||
@ -1964,6 +1966,7 @@ export const zKnowledgeFsPublicFailureResponse = z.object({
|
||||
'SOURCE_SECRET_INTEGRITY_FAILED',
|
||||
'SOURCE_SECRET_REF_CONFLICT',
|
||||
'SOURCE_SYNC_FAILED',
|
||||
'SOURCE_SYNC_SELECTION_MISMATCH',
|
||||
'SOURCE_WEBSITE_CRAWL_CONFIG_INVALID',
|
||||
'SOURCE_WEBSITE_CRAWL_FAILED',
|
||||
'SOURCE_WORKFLOW_FAILED',
|
||||
@ -2318,6 +2321,8 @@ export const zKnowledgeFsSourceCredentialTestResponse = z.object({
|
||||
'RESEARCH_TASK_FAILED',
|
||||
'RESEARCH_TASK_PERMISSION_SNAPSHOT_INVALID',
|
||||
'RESEARCH_TASK_RUNTIME_SNAPSHOT_INVALID',
|
||||
'RETRIEVAL_DELETION_IN_PROGRESS',
|
||||
'RETRIEVAL_EXECUTION_LEASE_LOST',
|
||||
'SOURCE_BULK_ACTION_FAILED',
|
||||
'SOURCE_CREDENTIAL_CONFIG_INVALID',
|
||||
'SOURCE_CREDENTIAL_MUTATION_FAILED',
|
||||
@ -2338,6 +2343,7 @@ export const zKnowledgeFsSourceCredentialTestResponse = z.object({
|
||||
'SOURCE_SECRET_INTEGRITY_FAILED',
|
||||
'SOURCE_SECRET_REF_CONFLICT',
|
||||
'SOURCE_SYNC_FAILED',
|
||||
'SOURCE_SYNC_SELECTION_MISMATCH',
|
||||
'SOURCE_WEBSITE_CRAWL_CONFIG_INVALID',
|
||||
'SOURCE_WEBSITE_CRAWL_FAILED',
|
||||
'SOURCE_WORKFLOW_FAILED',
|
||||
@ -2481,6 +2487,8 @@ export const zKnowledgeFsSourceImportFailureResponse = z.object({
|
||||
'RESEARCH_TASK_FAILED',
|
||||
'RESEARCH_TASK_PERMISSION_SNAPSHOT_INVALID',
|
||||
'RESEARCH_TASK_RUNTIME_SNAPSHOT_INVALID',
|
||||
'RETRIEVAL_DELETION_IN_PROGRESS',
|
||||
'RETRIEVAL_EXECUTION_LEASE_LOST',
|
||||
'SOURCE_BULK_ACTION_FAILED',
|
||||
'SOURCE_CREDENTIAL_CONFIG_INVALID',
|
||||
'SOURCE_CREDENTIAL_MUTATION_FAILED',
|
||||
@ -2501,6 +2509,7 @@ export const zKnowledgeFsSourceImportFailureResponse = z.object({
|
||||
'SOURCE_SECRET_INTEGRITY_FAILED',
|
||||
'SOURCE_SECRET_REF_CONFLICT',
|
||||
'SOURCE_SYNC_FAILED',
|
||||
'SOURCE_SYNC_SELECTION_MISMATCH',
|
||||
'SOURCE_WEBSITE_CRAWL_CONFIG_INVALID',
|
||||
'SOURCE_WEBSITE_CRAWL_FAILED',
|
||||
'SOURCE_WORKFLOW_FAILED',
|
||||
@ -2683,6 +2692,8 @@ export const zKnowledgeFsSourceWorkflowResponse = z.object({
|
||||
'RESEARCH_TASK_FAILED',
|
||||
'RESEARCH_TASK_PERMISSION_SNAPSHOT_INVALID',
|
||||
'RESEARCH_TASK_RUNTIME_SNAPSHOT_INVALID',
|
||||
'RETRIEVAL_DELETION_IN_PROGRESS',
|
||||
'RETRIEVAL_EXECUTION_LEASE_LOST',
|
||||
'SOURCE_BULK_ACTION_FAILED',
|
||||
'SOURCE_CREDENTIAL_CONFIG_INVALID',
|
||||
'SOURCE_CREDENTIAL_MUTATION_FAILED',
|
||||
@ -2703,6 +2714,7 @@ export const zKnowledgeFsSourceWorkflowResponse = z.object({
|
||||
'SOURCE_SECRET_INTEGRITY_FAILED',
|
||||
'SOURCE_SECRET_REF_CONFLICT',
|
||||
'SOURCE_SYNC_FAILED',
|
||||
'SOURCE_SYNC_SELECTION_MISMATCH',
|
||||
'SOURCE_WEBSITE_CRAWL_CONFIG_INVALID',
|
||||
'SOURCE_WEBSITE_CRAWL_FAILED',
|
||||
'SOURCE_WORKFLOW_FAILED',
|
||||
|
||||
@ -250,6 +250,7 @@ const taskApiResponse = vi.hoisted(() => (item: BackgroundTask) => ({
|
||||
progress_percent: item.progressPercent,
|
||||
progress_total: item.progressTotal ?? 1,
|
||||
source_id: item.sourceId ?? null,
|
||||
source_title: item.sourceTitle ?? null,
|
||||
state:
|
||||
item.state === 'succeeded'
|
||||
? 'completed'
|
||||
@ -4125,17 +4126,24 @@ describe('DocumentsPage', () => {
|
||||
it('shows document, bulk re-index, and source tasks returned by the task list', async () => {
|
||||
const user = userEvent.setup()
|
||||
documentsQuery.data = { pages: [{ items: [document({})] }] }
|
||||
sourcesQuery.data = { pages: [{ items: [] }] }
|
||||
tasksQuery.data = {
|
||||
pages: [
|
||||
{
|
||||
items: [
|
||||
task({ id: 'document-task', state: 'succeeded' }),
|
||||
backgroundTask({ id: 'reindex-task', progressCompleted: 12, progressTotal: 12 }),
|
||||
backgroundTask({
|
||||
documentId: 'document-1',
|
||||
id: 'reindex-task',
|
||||
progressCompleted: 1,
|
||||
progressTotal: 1,
|
||||
}),
|
||||
backgroundTask({
|
||||
errorMessage: 'Source sync failed',
|
||||
id: 'source-task',
|
||||
operation: 'source_sync',
|
||||
sourceId: 'source-1',
|
||||
sourceTitle: 'Notion support SOP',
|
||||
state: 'failed',
|
||||
taskKind: 'source',
|
||||
}),
|
||||
@ -4154,13 +4162,14 @@ describe('DocumentsPage', () => {
|
||||
const panel = screen.getByRole('dialog', { name: 'dataset.newKnowledge.backgroundTasks' })
|
||||
expect(within(panel).getAllByRole('listitem')).toHaveLength(3)
|
||||
expect(
|
||||
within(panel).getByText('dataset.newKnowledge.reindexDocuments · 12'),
|
||||
within(panel).getByText('dataset.newKnowledge.reindexDocuments · sso-enterprise.pdf'),
|
||||
).toBeInTheDocument()
|
||||
expect(
|
||||
within(panel).getByText(
|
||||
'dataset.newKnowledge.overview.operation.source_sync · Notion support SOP',
|
||||
),
|
||||
).toBeInTheDocument()
|
||||
expect(within(panel).queryByText(/ · 1$/)).not.toBeInTheDocument()
|
||||
expect(within(panel).getByText('dataset.newKnowledge.taskFailure.internal')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
|
||||
@ -238,6 +238,7 @@ const taskApiResponse = vi.hoisted(() => (item: BackgroundTask) => ({
|
||||
completed_at: item.completedAt ?? null,
|
||||
created_at: item.createdAt,
|
||||
document_id: item.documentId ?? null,
|
||||
document_title: item.documentTitle ?? null,
|
||||
document_revision: item.documentRevision ?? null,
|
||||
error_code: item.errorCode ?? null,
|
||||
error_message: item.errorMessage ?? null,
|
||||
@ -250,6 +251,7 @@ const taskApiResponse = vi.hoisted(() => (item: BackgroundTask) => ({
|
||||
progress_percent: item.progressPercent,
|
||||
progress_total: item.progressTotal ?? 1,
|
||||
source_id: item.sourceId ?? null,
|
||||
source_title: item.sourceTitle ?? null,
|
||||
state:
|
||||
item.state === 'succeeded'
|
||||
? 'completed'
|
||||
@ -2034,7 +2036,7 @@ describe('DocumentDetailPage', () => {
|
||||
id: 'another-task',
|
||||
state: 'succeeded',
|
||||
}),
|
||||
backgroundTask({ id: 'bulk-reindex-task' }),
|
||||
backgroundTask({ documentId: 'document-1', id: 'bulk-reindex-task' }),
|
||||
],
|
||||
},
|
||||
],
|
||||
@ -2059,7 +2061,7 @@ describe('DocumentDetailPage', () => {
|
||||
})
|
||||
expect(within(taskDrawer).getAllByText(/dataset\.newKnowledge\.addDocument/)).toHaveLength(2)
|
||||
expect(
|
||||
within(taskDrawer).getByText('dataset.newKnowledge.reindexDocuments · 1'),
|
||||
within(taskDrawer).getByText('dataset.newKnowledge.reindexDocuments · sso-enterprise.pdf'),
|
||||
).toBeInTheDocument()
|
||||
})
|
||||
|
||||
|
||||
@ -117,7 +117,7 @@ export function DocumentTaskRow({
|
||||
: task.operation === 'document_upload'
|
||||
? `${t(($) => $['newKnowledge.addDocument'])}${progress ? ` · ${progress.total}` : ''}`
|
||||
: task.operation === 'document_reindex'
|
||||
? `${t(($) => $['newKnowledge.reindexDocuments'])}${progress ? ` · ${progress.total}` : resolvedDocumentTitle ? ` · ${resolvedDocumentTitle}` : ''}`
|
||||
? `${t(($) => $['newKnowledge.reindexDocuments'])}${resolvedDocumentTitle ? ` · ${resolvedDocumentTitle}` : progress && progress.total > 1 ? ` · ${progress.total}` : ''}`
|
||||
: task.operation === 'document_delete' && resolvedDocumentTitle
|
||||
? `${operationTitle} · ${resolvedDocumentTitle}`
|
||||
: progress
|
||||
@ -199,8 +199,12 @@ export function DocumentTaskRow({
|
||||
}
|
||||
/>
|
||||
<div className="min-w-0 flex-1">
|
||||
<p className="truncate system-sm-medium text-text-primary">{title}</p>
|
||||
<p className="mt-0.75 truncate system-xs-regular text-text-tertiary">{status}</p>
|
||||
<p className="truncate system-sm-medium text-text-primary" title={title}>
|
||||
{title}
|
||||
</p>
|
||||
<p className="mt-0.75 truncate system-xs-regular text-text-tertiary" title={status}>
|
||||
{status}
|
||||
</p>
|
||||
{taskError && (
|
||||
<p className="mt-1 system-2xs-regular wrap-break-word whitespace-pre-wrap text-text-destructive">
|
||||
{taskError}
|
||||
|
||||
@ -111,6 +111,7 @@ export type BackgroundTask = {
|
||||
| 'canceled'
|
||||
| 'superseded'
|
||||
sourceId?: string
|
||||
sourceTitle?: string
|
||||
taskKind: KnowledgeFsBackgroundTaskResponse['task_kind']
|
||||
updatedAt: string
|
||||
}
|
||||
@ -268,6 +269,7 @@ export function backgroundTaskFromApi(task: KnowledgeFsBackgroundTaskResponse):
|
||||
? 'canceled'
|
||||
: task.state,
|
||||
sourceId: task.source_id ?? undefined,
|
||||
sourceTitle: task.source_title ?? undefined,
|
||||
taskKind: task.task_kind,
|
||||
updatedAt: task.updated_at,
|
||||
}
|
||||
|
||||
@ -204,14 +204,16 @@ function useDocumentsTaskRowTitle(task: BackgroundTask) {
|
||||
: task.operation === 'document_upload'
|
||||
? `${t(($) => $['newKnowledge.addDocument'])}${progress ? ` · ${progress.total}` : ''}`
|
||||
: task.operation === 'document_reindex'
|
||||
? `${t(($) => $['newKnowledge.reindexDocuments'])}${progress ? ` · ${progress.total}` : documentTitle ? ` · ${documentTitle}` : ''}`
|
||||
? `${t(($) => $['newKnowledge.reindexDocuments'])}${documentTitle ? ` · ${documentTitle}` : progress && progress.total > 1 ? ` · ${progress.total}` : ''}`
|
||||
: task.operation === 'document_delete' && documentTitle
|
||||
? `${operationTitle} · ${documentTitle}`
|
||||
: sourceTitle
|
||||
? `${operationTitle} · ${sourceTitle}`
|
||||
: progress
|
||||
? `${operationTitle} · ${progress.total}`
|
||||
: operationTitle
|
||||
: task.operation === 'source_sync'
|
||||
? operationTitle
|
||||
: progress
|
||||
? `${operationTitle} · ${progress.total}`
|
||||
: operationTitle
|
||||
}
|
||||
|
||||
function DocumentsTaskDetails({ task }: { task: BackgroundTask }) {
|
||||
@ -266,8 +268,12 @@ function DocumentsTaskDetails({ task }: { task: BackgroundTask }) {
|
||||
}
|
||||
/>
|
||||
<div className="min-w-0 flex-1">
|
||||
<p className="truncate system-sm-medium text-text-primary">{title}</p>
|
||||
<p className="mt-0.75 truncate system-xs-regular text-text-tertiary">{status}</p>
|
||||
<p className="truncate system-sm-medium text-text-primary" title={title}>
|
||||
{title}
|
||||
</p>
|
||||
<p className="mt-0.75 truncate system-xs-regular text-text-tertiary" title={status}>
|
||||
{status}
|
||||
</p>
|
||||
{taskError && (
|
||||
<p className="mt-1 system-2xs-regular wrap-break-word whitespace-pre-wrap text-text-destructive">
|
||||
{taskError}
|
||||
|
||||
@ -119,7 +119,8 @@ export const createTaskDrawerRowLabelsAtom = (task: BackgroundTask) => {
|
||||
documentTitlePending: Boolean(
|
||||
task.documentId && !task.documentTitle && get(taskDrawerDocumentsPendingAtom),
|
||||
),
|
||||
sourceTitle: task.sourceId ? get(sourceNamesAtom).get(task.sourceId) : undefined,
|
||||
sourceTitle:
|
||||
task.sourceTitle ?? (task.sourceId ? get(sourceNamesAtom).get(task.sourceId) : undefined),
|
||||
}))
|
||||
return selectAtom(
|
||||
labelsAtom,
|
||||
|
||||
@ -58,6 +58,8 @@ const failureMessageKeyByCode = {
|
||||
RESEARCH_TASK_FAILED: 'newKnowledge.taskFailure.research',
|
||||
RESEARCH_TASK_PERMISSION_SNAPSHOT_INVALID: 'newKnowledge.taskFailure.access',
|
||||
RESEARCH_TASK_RUNTIME_SNAPSHOT_INVALID: 'newKnowledge.taskFailure.research',
|
||||
RETRIEVAL_DELETION_IN_PROGRESS: 'newKnowledge.taskFailure.conflict',
|
||||
RETRIEVAL_EXECUTION_LEASE_LOST: 'newKnowledge.taskFailure.conflict',
|
||||
SOURCE_BULK_ACTION_FAILED: 'newKnowledge.taskFailure.source',
|
||||
SOURCE_CREDENTIAL_CONFIG_INVALID: 'newKnowledge.taskFailure.source',
|
||||
SOURCE_CREDENTIAL_MUTATION_FAILED: 'newKnowledge.taskFailure.source',
|
||||
@ -78,6 +80,7 @@ const failureMessageKeyByCode = {
|
||||
SOURCE_SECRET_INTEGRITY_FAILED: 'newKnowledge.taskFailure.source',
|
||||
SOURCE_SECRET_REF_CONFLICT: 'newKnowledge.taskFailure.conflict',
|
||||
SOURCE_SYNC_FAILED: 'newKnowledge.taskFailure.source',
|
||||
SOURCE_SYNC_SELECTION_MISMATCH: 'newKnowledge.taskFailure.conflict',
|
||||
SOURCE_WEBSITE_CRAWL_CONFIG_INVALID: 'newKnowledge.taskFailure.source',
|
||||
SOURCE_WEBSITE_CRAWL_FAILED: 'newKnowledge.taskFailure.source',
|
||||
SOURCE_WORKFLOW_FAILED: 'newKnowledge.taskFailure.source',
|
||||
|
||||
Loading…
Reference in New Issue
Block a user