mirror of
https://github.com/langgenius/dify.git
synced 2026-09-05 00:31:19 +08:00
Merge remote-tracking branch 'origin/deploy/konwledge' into deploy/konwledge
This commit is contained in:
commit
08910f8d79
@ -1,6 +1,6 @@
|
||||
{
|
||||
"schemaVersion": 5,
|
||||
"subtreeTree": "67bd568555a900179301ec69121b276a88332aa3",
|
||||
"subtreeTree": "7250e700e493b975bf070a97d5424c5a0c7def62",
|
||||
"openapiSha256": "47936a7d9ffdc27e2b2b8982a90e1936dc3bf59a64c316f452a6912ec1d2fcd6",
|
||||
"capabilityV2AuthManifestSha256": "fc0a47e23cce12544882f0298522b4933002e892b84ce1815df7e81d36a7a0c7",
|
||||
"capabilityV2AuthTestVectorSha256": "ae0de37b1ff05c40f905cf17a7b410d8971acacf64db07d5ee3d6fecfa559ce3",
|
||||
|
||||
@ -70,6 +70,7 @@ import {
|
||||
isTerminalKnowledgeSpaceProfileMigrationError,
|
||||
} from "./knowledge-space-profile-migration";
|
||||
import {
|
||||
type KnowledgeSpaceProfileMigrationPrincipal,
|
||||
type KnowledgeSpaceProfileMigrationService,
|
||||
KnowledgeSpaceProfileMigrationServiceError,
|
||||
toPublicKnowledgeSpaceProfileMigration,
|
||||
@ -1223,37 +1224,11 @@ export function registerKnowledgeSpaceHandlers({
|
||||
409,
|
||||
);
|
||||
}
|
||||
const candidateRevision = await nextPublishedRetrievalCandidateRevision(profiles, {
|
||||
activeRevision: actualRevision,
|
||||
knowledgeSpaceId,
|
||||
tenantId: subject.tenantId,
|
||||
});
|
||||
const candidateSnapshot = createKnowledgeSpaceRetrievalProfile(
|
||||
body.profile,
|
||||
candidateRevision,
|
||||
);
|
||||
const capabilitySnapshot = {
|
||||
reasoning: capabilitySnapshots.reasoning ?? null,
|
||||
rerank: capabilitySnapshots.rerank ?? null,
|
||||
verification: "verified",
|
||||
} as const;
|
||||
let candidate: KnowledgeSpaceProfileRevision;
|
||||
try {
|
||||
candidate = await getOrCreateSettingsProfileCandidate(profiles, {
|
||||
capabilitySnapshot,
|
||||
createdBySubjectId: subject.subjectId,
|
||||
kind: "retrieval",
|
||||
knowledgeSpaceId,
|
||||
now: now(),
|
||||
snapshot: candidateSnapshot,
|
||||
tenantId: subject.tenantId,
|
||||
});
|
||||
} catch (error) {
|
||||
if (error instanceof SettingsProfileCandidateConflictError) {
|
||||
return context.json({ code: error.code, error: error.message }, 409);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
const authenticatedApiKey = context.get("authenticatedApiKey");
|
||||
const capabilityGrantId = context.get("capabilityV2Grant")?.grantId;
|
||||
const migrationPrincipal = {
|
||||
@ -1264,7 +1239,21 @@ export function registerKnowledgeSpaceHandlers({
|
||||
subject,
|
||||
} as const;
|
||||
try {
|
||||
let migration = await profileMigrations.request({
|
||||
const candidate = await getOrSupersedeRetrievalSettingsCandidate({
|
||||
activeRevision: actualRevision,
|
||||
capabilitySnapshot,
|
||||
createdBySubjectId: subject.subjectId,
|
||||
migrationPrincipal,
|
||||
migrations: profileMigrations,
|
||||
now,
|
||||
profile: body.profile,
|
||||
profiles,
|
||||
});
|
||||
let migration = await profileMigrations.findByCandidate({
|
||||
...migrationPrincipal,
|
||||
candidateProfileId: candidate.id,
|
||||
});
|
||||
migration ??= await profileMigrations.request({
|
||||
...migrationPrincipal,
|
||||
candidateRevision: candidate.revision,
|
||||
changedKind: "retrieval",
|
||||
@ -1291,6 +1280,9 @@ export function registerKnowledgeSpaceHandlers({
|
||||
}
|
||||
return context.json(toPublicKnowledgeSpaceProfileMigration(migration), 202);
|
||||
} catch (error) {
|
||||
if (error instanceof SettingsProfileCandidateConflictError) {
|
||||
return context.json({ code: error.code, error: error.message }, 409);
|
||||
}
|
||||
if (error instanceof KnowledgeSpaceProfileMigrationConflictError) {
|
||||
return context.json({ code: error.code, error: error.message }, 409);
|
||||
}
|
||||
@ -2592,6 +2584,91 @@ class SettingsProfileCandidateConflictError extends Error {
|
||||
}
|
||||
}
|
||||
|
||||
async function getOrSupersedeRetrievalSettingsCandidate(input: {
|
||||
readonly activeRevision: number;
|
||||
readonly capabilitySnapshot: Readonly<Record<string, unknown>>;
|
||||
readonly createdBySubjectId: string;
|
||||
readonly migrationPrincipal: KnowledgeSpaceProfileMigrationPrincipal & {
|
||||
readonly knowledgeSpaceId: string;
|
||||
};
|
||||
readonly migrations: KnowledgeSpaceProfileMigrationService;
|
||||
readonly now: () => string;
|
||||
readonly profile: KnowledgeSpaceRetrievalProfileInput;
|
||||
readonly profiles: KnowledgeSpaceProfileRepository;
|
||||
}): Promise<KnowledgeSpaceProfileRevision> {
|
||||
for (;;) {
|
||||
const candidateRevision = await nextPublishedRetrievalCandidateRevision(input.profiles, {
|
||||
activeRevision: input.activeRevision,
|
||||
knowledgeSpaceId: input.migrationPrincipal.knowledgeSpaceId,
|
||||
tenantId: input.migrationPrincipal.subject.tenantId,
|
||||
});
|
||||
const candidateInput = {
|
||||
capabilitySnapshot: input.capabilitySnapshot,
|
||||
createdBySubjectId: input.createdBySubjectId,
|
||||
kind: "retrieval" as const,
|
||||
knowledgeSpaceId: input.migrationPrincipal.knowledgeSpaceId,
|
||||
now: input.now(),
|
||||
snapshot: createKnowledgeSpaceRetrievalProfile(input.profile, candidateRevision),
|
||||
tenantId: input.migrationPrincipal.subject.tenantId,
|
||||
};
|
||||
try {
|
||||
return await getOrCreateSettingsProfileCandidate(input.profiles, candidateInput);
|
||||
} catch (error) {
|
||||
if (!(error instanceof SettingsProfileCandidateConflictError)) throw error;
|
||||
const existing = await input.profiles.getRevision({
|
||||
kind: "retrieval",
|
||||
knowledgeSpaceId: input.migrationPrincipal.knowledgeSpaceId,
|
||||
revision: candidateRevision,
|
||||
tenantId: input.migrationPrincipal.subject.tenantId,
|
||||
});
|
||||
if (
|
||||
!existing ||
|
||||
existing.state !== "candidate" ||
|
||||
existing.createdBySubjectId !== input.createdBySubjectId ||
|
||||
existing.snapshotDigest === knowledgeSpaceProfileSnapshotDigest(candidateInput.snapshot)
|
||||
) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
const previousMigration = await input.migrations.findByCandidate({
|
||||
...input.migrationPrincipal,
|
||||
candidateProfileId: existing.id,
|
||||
});
|
||||
if (previousMigration) {
|
||||
const canceled = await input.migrations.cancel({
|
||||
...input.migrationPrincipal,
|
||||
reason: "Superseded by a newer retrieval settings update",
|
||||
runId: previousMigration.id,
|
||||
});
|
||||
if (!canceled || canceled.runState !== "canceled") throw error;
|
||||
} else {
|
||||
try {
|
||||
await input.profiles.failCandidate({
|
||||
errorCode: "PROFILE_SETTINGS_CANDIDATE_SUPERSEDED",
|
||||
errorMessage: "Superseded by a newer retrieval settings update",
|
||||
kind: "retrieval",
|
||||
knowledgeSpaceId: input.migrationPrincipal.knowledgeSpaceId,
|
||||
now: input.now(),
|
||||
revision: existing.revision,
|
||||
tenantId: input.migrationPrincipal.subject.tenantId,
|
||||
});
|
||||
} catch (retirementError) {
|
||||
if (retirementError instanceof KnowledgeSpaceProfileTransitionError) throw error;
|
||||
throw retirementError;
|
||||
}
|
||||
}
|
||||
|
||||
const retired = await input.profiles.getRevision({
|
||||
kind: "retrieval",
|
||||
knowledgeSpaceId: input.migrationPrincipal.knowledgeSpaceId,
|
||||
revision: existing.revision,
|
||||
tenantId: input.migrationPrincipal.subject.tenantId,
|
||||
});
|
||||
if (!retired || retired.state === "candidate") throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function nextPublishedRetrievalCandidateRevision(
|
||||
profiles: KnowledgeSpaceProfileRepository,
|
||||
input: {
|
||||
|
||||
@ -188,13 +188,13 @@ function retrievalUpdateBody() {
|
||||
} as const;
|
||||
}
|
||||
|
||||
function rerankOnlyRetrievalUpdateBody() {
|
||||
function rerankOnlyRetrievalUpdateBody(rerankModel = RERANK_V2) {
|
||||
return {
|
||||
expectedRevision: 1,
|
||||
profile: {
|
||||
defaultMode: "deep",
|
||||
reasoningModel: REASONING_V1,
|
||||
rerank: { enabled: true, model: RERANK_V2 },
|
||||
rerank: { enabled: true, model: rerankModel },
|
||||
scoreThreshold: { enabled: false, stage: "mode-final" },
|
||||
topK: 8,
|
||||
},
|
||||
@ -743,6 +743,7 @@ describe("knowledge-space profile handler behavior", () => {
|
||||
knowledgeSpaceManifests: manifests,
|
||||
knowledgeSpaceProfileMigrations: {
|
||||
cancel: async () => null,
|
||||
findByCandidate: async () => null,
|
||||
get: async () => null,
|
||||
request,
|
||||
requiresMigration: async () => true,
|
||||
@ -793,6 +794,7 @@ describe("knowledge-space profile handler behavior", () => {
|
||||
knowledgeSpaceManifests: retrievalManifests,
|
||||
knowledgeSpaceProfileMigrations: {
|
||||
cancel: async () => null,
|
||||
findByCandidate: async () => null,
|
||||
get: async () => null,
|
||||
request,
|
||||
requiresMigration: async () => true,
|
||||
@ -852,6 +854,121 @@ describe("knowledge-space profile handler behavior", () => {
|
||||
).resolves.toMatchObject({ state: "candidate" });
|
||||
});
|
||||
|
||||
it("supersedes an owned pending retrieval candidate when the rerank model changes", async () => {
|
||||
const manifests = createInMemoryKnowledgeSpaceManifestRepository({
|
||||
maxListLimit: 10,
|
||||
maxManifests: 10,
|
||||
});
|
||||
const profiles = profileRepository();
|
||||
const request = vi.fn(
|
||||
async (input: { readonly candidateRevision: number }) =>
|
||||
({
|
||||
changedKind: "retrieval",
|
||||
checkpoint: "queued",
|
||||
createdAt: NOW,
|
||||
id: `migration-retrieval-${input.candidateRevision}`,
|
||||
knowledgeSpaceId: SPACE_ID,
|
||||
rebuildScope: "clone-publication",
|
||||
runState: "queued",
|
||||
updatedAt: NOW,
|
||||
}) as never,
|
||||
);
|
||||
const cancel = vi.fn(async (input: { readonly runId: string }) => {
|
||||
expect(input.runId).toBe("migration-retrieval-2");
|
||||
await profiles.failCandidate({
|
||||
errorCode: "PROFILE_MIGRATION_CANCELED",
|
||||
errorMessage: "Superseded by a newer retrieval settings update",
|
||||
kind: "retrieval",
|
||||
knowledgeSpaceId: SPACE_ID,
|
||||
now: NOW,
|
||||
revision: 2,
|
||||
tenantId: "tenant-1",
|
||||
});
|
||||
return {
|
||||
changedKind: "retrieval",
|
||||
checkpoint: "queued",
|
||||
completedAt: NOW,
|
||||
createdAt: NOW,
|
||||
id: input.runId,
|
||||
knowledgeSpaceId: SPACE_ID,
|
||||
rebuildScope: "clone-publication",
|
||||
runState: "canceled",
|
||||
updatedAt: NOW,
|
||||
} as never;
|
||||
});
|
||||
const findByCandidate = vi.fn(async (input: { readonly candidateProfileId: string }) => {
|
||||
const revision = await profiles.getRevision({
|
||||
kind: "retrieval",
|
||||
knowledgeSpaceId: SPACE_ID,
|
||||
revision: 2,
|
||||
tenantId: "tenant-1",
|
||||
});
|
||||
if (request.mock.calls.length === 0 || revision?.id !== input.candidateProfileId) return null;
|
||||
return {
|
||||
changedKind: "retrieval",
|
||||
checkpoint: "queued",
|
||||
createdAt: NOW,
|
||||
id: "migration-retrieval-2",
|
||||
knowledgeSpaceId: SPACE_ID,
|
||||
rebuildScope: "clone-publication",
|
||||
runState: "queued",
|
||||
updatedAt: NOW,
|
||||
} as never;
|
||||
});
|
||||
const app = publishedProfileApp(manifests, profiles, {
|
||||
cancel,
|
||||
findByCandidate,
|
||||
get: async () => null,
|
||||
request,
|
||||
requiresMigration: async () => true,
|
||||
retry: async () => null,
|
||||
});
|
||||
await createSpace(app);
|
||||
await seedLegacyManifestWithoutRerank(manifests);
|
||||
|
||||
const first = await app.request(`/knowledge-spaces/${SPACE_ID}/retrieval-profile`, {
|
||||
body: JSON.stringify(rerankOnlyRetrievalUpdateBody(RERANK_V1)),
|
||||
headers: headers(),
|
||||
method: "PUT",
|
||||
});
|
||||
expect(first.status).toBe(202);
|
||||
|
||||
const replacement = await app.request(`/knowledge-spaces/${SPACE_ID}/retrieval-profile`, {
|
||||
body: JSON.stringify(rerankOnlyRetrievalUpdateBody(RERANK_V2)),
|
||||
headers: headers(),
|
||||
method: "PUT",
|
||||
});
|
||||
const replacementBody = await replacement.json();
|
||||
|
||||
expect(replacement.status, JSON.stringify(replacementBody)).toBe(202);
|
||||
expect(replacementBody).toMatchObject({
|
||||
id: "migration-retrieval-3",
|
||||
runState: "queued",
|
||||
});
|
||||
expect(request).toHaveBeenCalledTimes(2);
|
||||
expect(findByCandidate).toHaveBeenCalledTimes(3);
|
||||
expect(cancel).toHaveBeenCalledOnce();
|
||||
await expect(
|
||||
profiles.getRevision({
|
||||
kind: "retrieval",
|
||||
knowledgeSpaceId: SPACE_ID,
|
||||
revision: 2,
|
||||
tenantId: "tenant-1",
|
||||
}),
|
||||
).resolves.toMatchObject({ state: "failed" });
|
||||
await expect(
|
||||
profiles.getRevision({
|
||||
kind: "retrieval",
|
||||
knowledgeSpaceId: SPACE_ID,
|
||||
revision: 3,
|
||||
tenantId: "tenant-1",
|
||||
}),
|
||||
).resolves.toMatchObject({
|
||||
snapshot: { rerank: { enabled: true, model: RERANK_V2 } },
|
||||
state: "candidate",
|
||||
});
|
||||
});
|
||||
|
||||
it("retries a nonterminal failed retrieval migration when settings are saved again", async () => {
|
||||
const manifests = createInMemoryKnowledgeSpaceManifestRepository({
|
||||
maxListLimit: 10,
|
||||
@ -887,6 +1004,7 @@ describe("knowledge-space profile handler behavior", () => {
|
||||
);
|
||||
const app = publishedProfileApp(manifests, profiles, {
|
||||
cancel: async () => null,
|
||||
findByCandidate: async () => null,
|
||||
get: async () => null,
|
||||
request,
|
||||
requiresMigration: async () => true,
|
||||
@ -921,21 +1039,20 @@ describe("knowledge-space profile handler behavior", () => {
|
||||
maxManifests: 10,
|
||||
});
|
||||
const profiles = profileRepository();
|
||||
const request = vi.fn(
|
||||
async () =>
|
||||
({
|
||||
changedKind: "retrieval",
|
||||
checkpoint: "queued",
|
||||
createdAt: NOW,
|
||||
id: "migration-retrieval",
|
||||
knowledgeSpaceId: SPACE_ID,
|
||||
rebuildScope: "clone-publication",
|
||||
runState: "queued",
|
||||
updatedAt: NOW,
|
||||
}) as never,
|
||||
);
|
||||
const migration = {
|
||||
changedKind: "retrieval",
|
||||
checkpoint: "queued",
|
||||
createdAt: NOW,
|
||||
id: "migration-retrieval",
|
||||
knowledgeSpaceId: SPACE_ID,
|
||||
rebuildScope: "clone-publication",
|
||||
runState: "queued",
|
||||
updatedAt: NOW,
|
||||
} as never;
|
||||
const request = vi.fn(async () => migration);
|
||||
const app = publishedProfileApp(manifests, profiles, {
|
||||
cancel: async () => null,
|
||||
findByCandidate: async () => null,
|
||||
get: async () => null,
|
||||
request,
|
||||
requiresMigration: async () => true,
|
||||
@ -994,23 +1111,23 @@ describe("knowledge-space profile handler behavior", () => {
|
||||
const profiles = profileRepository();
|
||||
let checkedAt = NOW;
|
||||
let capabilityDigest = `sha256:${"c".repeat(64)}`;
|
||||
const request = vi.fn(
|
||||
async () =>
|
||||
({
|
||||
changedKind: "retrieval",
|
||||
checkpoint: "queued",
|
||||
createdAt: NOW,
|
||||
id: "migration-retrieval",
|
||||
knowledgeSpaceId: SPACE_ID,
|
||||
rebuildScope: "clone-publication",
|
||||
runState: "queued",
|
||||
updatedAt: NOW,
|
||||
}) as never,
|
||||
);
|
||||
const migration = {
|
||||
changedKind: "retrieval",
|
||||
checkpoint: "queued",
|
||||
createdAt: NOW,
|
||||
id: "migration-retrieval",
|
||||
knowledgeSpaceId: SPACE_ID,
|
||||
rebuildScope: "clone-publication",
|
||||
runState: "queued",
|
||||
updatedAt: NOW,
|
||||
} as never;
|
||||
const request = vi.fn(async () => migration);
|
||||
const findByCandidate = vi.fn(async () => (request.mock.calls.length === 0 ? null : migration));
|
||||
const app = profileApp({
|
||||
knowledgeSpaceManifests: manifests,
|
||||
knowledgeSpaceProfileMigrations: {
|
||||
cancel: async () => null,
|
||||
findByCandidate,
|
||||
get: async () => null,
|
||||
request,
|
||||
requiresMigration: async () => true,
|
||||
@ -1061,7 +1178,8 @@ describe("knowledge-space profile handler behavior", () => {
|
||||
changedKind: "retrieval",
|
||||
id: "migration-retrieval",
|
||||
});
|
||||
expect(request).toHaveBeenCalledTimes(2);
|
||||
expect(request).toHaveBeenCalledOnce();
|
||||
expect(findByCandidate).toHaveBeenCalledTimes(2);
|
||||
|
||||
capabilityDigest = `sha256:${"e".repeat(64)}`;
|
||||
const changedCapability = await app.request(`/knowledge-spaces/${SPACE_ID}/retrieval-profile`, {
|
||||
@ -1073,7 +1191,7 @@ describe("knowledge-space profile handler behavior", () => {
|
||||
await expect(changedCapability.json()).resolves.toMatchObject({
|
||||
code: "KNOWLEDGE_SPACE_SETTINGS_CANDIDATE_CONFLICT",
|
||||
});
|
||||
expect(request).toHaveBeenCalledTimes(2);
|
||||
expect(request).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
it("carries an integrated settings Capability grant into the durable migration", async () => {
|
||||
@ -1121,6 +1239,7 @@ describe("knowledge-space profile handler behavior", () => {
|
||||
knowledgeSpaceManifests: manifests,
|
||||
knowledgeSpaceProfileMigrations: {
|
||||
cancel: async () => null,
|
||||
findByCandidate: async () => null,
|
||||
get: async () => null,
|
||||
request,
|
||||
requiresMigration: async () => true,
|
||||
@ -1182,6 +1301,7 @@ describe("knowledge-space profile handler behavior", () => {
|
||||
knowledgeSpaceManifests: manifests,
|
||||
knowledgeSpaceProfileMigrations: {
|
||||
cancel: async () => null,
|
||||
findByCandidate: async () => null,
|
||||
get: async () => null,
|
||||
request,
|
||||
requiresMigration: async () => true,
|
||||
@ -2403,6 +2523,7 @@ describe("knowledge-space profile failure behavior", () => {
|
||||
knowledgeSpaceManifests: manifests,
|
||||
knowledgeSpaceProfileMigrations: {
|
||||
cancel: async () => null,
|
||||
findByCandidate: async () => null,
|
||||
get: async () => null,
|
||||
request: async () => ({}) as never,
|
||||
requiresMigration: async () => true,
|
||||
@ -2539,6 +2660,7 @@ describe("knowledge-space profile failure behavior", () => {
|
||||
knowledgeSpaceManifests: manifests,
|
||||
knowledgeSpaceProfileMigrations: {
|
||||
cancel: async () => null,
|
||||
findByCandidate: async () => null,
|
||||
get: async () => null,
|
||||
request: async () => ({}) as never,
|
||||
requiresMigration: async () => true,
|
||||
@ -2604,6 +2726,7 @@ describe("knowledge-space profile failure behavior", () => {
|
||||
knowledgeSpaceManifests: manifests,
|
||||
knowledgeSpaceProfileMigrations: {
|
||||
cancel: async () => null,
|
||||
findByCandidate: async () => null,
|
||||
get: async () => null,
|
||||
request: async () => ({}) as never,
|
||||
requiresMigration: async () => true,
|
||||
@ -2653,6 +2776,7 @@ describe("knowledge-space profile failure behavior", () => {
|
||||
knowledgeSpaceManifests: manifests,
|
||||
knowledgeSpaceProfileMigrations: {
|
||||
cancel: async () => null,
|
||||
findByCandidate: async () => null,
|
||||
get: async () => null,
|
||||
request: async () => ({}) as never,
|
||||
requiresMigration: async () => true,
|
||||
|
||||
@ -135,6 +135,42 @@ describe.each(["postgres", "tidb"] as const)(
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it("looks up the latest candidate migration independently of authorization provenance", async () => {
|
||||
const calls: DatabaseExecuteInput[] = [];
|
||||
const execute = async (input: DatabaseExecuteInput): Promise<DatabaseExecuteResult> => {
|
||||
calls.push(input);
|
||||
return { rows: [runRow()], rowsAffected: 1 };
|
||||
};
|
||||
const database = createSchemaDatabaseAdapter({
|
||||
executor: execute,
|
||||
kind: dialect,
|
||||
transaction: async (callback) => callback({ execute }),
|
||||
});
|
||||
const repository = createDatabaseKnowledgeSpaceProfileMigrationRepository({
|
||||
database,
|
||||
maxClaimBatchSize: 10,
|
||||
});
|
||||
|
||||
await expect(
|
||||
repository.findLatestByCandidate({
|
||||
candidateProfileId: candidateId,
|
||||
knowledgeSpaceId: spaceId,
|
||||
tenantId,
|
||||
}),
|
||||
).resolves.toMatchObject({ id: runId, runState: "queued" });
|
||||
|
||||
expect(calls).toHaveLength(1);
|
||||
expect(calls[0]?.params).toEqual([tenantId, spaceId, candidateId]);
|
||||
expect(calls[0]?.sql).toContain(
|
||||
dialect === "postgres"
|
||||
? '"candidate_profile_revision_id"'
|
||||
: "`candidate_profile_revision_id`",
|
||||
);
|
||||
expect(calls[0]?.sql).toContain(
|
||||
dialect === "postgres" ? '"active_slot" IS NOT NULL' : "`active_slot` IS NOT NULL",
|
||||
);
|
||||
});
|
||||
|
||||
it("rejects malformed admissions and unsafe claim leases before database access", async () => {
|
||||
const execute = async (): Promise<DatabaseExecuteResult> => {
|
||||
throw new Error("database should not be called");
|
||||
|
||||
@ -234,6 +234,7 @@ export function createDatabaseKnowledgeSpaceProfileMigrationRepository({
|
||||
|
||||
get: (id) => getById(database, database, id, false),
|
||||
findByRequest: (input) => getByIdempotency(database, database, input, false),
|
||||
findLatestByCandidate: (input) => getLatestByCandidate(database, database, input),
|
||||
|
||||
claim: async (input) => {
|
||||
positiveInteger(input.limit, "claim.limit");
|
||||
@ -1135,6 +1136,37 @@ async function getByIdempotency(
|
||||
return replay;
|
||||
}
|
||||
|
||||
async function getLatestByCandidate(
|
||||
database: DatabaseAdapter,
|
||||
executor: DatabaseExecutor,
|
||||
input: {
|
||||
readonly candidateProfileId: string;
|
||||
readonly knowledgeSpaceId: string;
|
||||
readonly tenantId: string;
|
||||
},
|
||||
): Promise<KnowledgeSpaceProfileMigrationRun | null> {
|
||||
const result = await executor.execute({
|
||||
maxRows: 1,
|
||||
operation: "select",
|
||||
params: [input.tenantId, input.knowledgeSpaceId, input.candidateProfileId],
|
||||
sql: `SELECT * FROM ${q(database, runTable)} WHERE ${q(database, "tenant_id")} = ${p(
|
||||
database,
|
||||
1,
|
||||
)} AND ${q(database, "knowledge_space_id")} = ${p(database, 2)} AND ${q(
|
||||
database,
|
||||
"candidate_profile_revision_id",
|
||||
)} = ${p(database, 3)} ORDER BY CASE WHEN ${q(
|
||||
database,
|
||||
"active_slot",
|
||||
)} IS NOT NULL THEN 0 ELSE 1 END ASC, ${q(database, "created_at")} DESC, ${q(
|
||||
database,
|
||||
"id",
|
||||
)} DESC LIMIT 1;`,
|
||||
tableName: runTable,
|
||||
});
|
||||
return result.rows[0] ? mapRun(result.rows[0]) : null;
|
||||
}
|
||||
|
||||
function profileMigrationIdempotencyDigest(input: {
|
||||
readonly capabilityGrantId?: string | undefined;
|
||||
readonly idempotencyKey: string;
|
||||
|
||||
@ -114,6 +114,35 @@ describe("knowledge-space profile migration service behavior", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("finds and cancels a candidate migration after capability grant rotation", async () => {
|
||||
const fixture = serviceFixture();
|
||||
const run = await fixture.service.request(capabilityRequest());
|
||||
const rotatedCapabilityGrantId = "70000000-0000-4000-8000-000000000002";
|
||||
|
||||
await expect(
|
||||
fixture.service.findByCandidate({
|
||||
callerKind: "agent",
|
||||
candidateProfileId: run.candidateProfile.id,
|
||||
capabilityGrantId: rotatedCapabilityGrantId,
|
||||
knowledgeSpaceId: spaceId,
|
||||
subject,
|
||||
}),
|
||||
).resolves.toEqual(run);
|
||||
await expect(
|
||||
fixture.service.cancel({
|
||||
callerKind: "agent",
|
||||
capabilityGrantId: rotatedCapabilityGrantId,
|
||||
knowledgeSpaceId: spaceId,
|
||||
runId: run.id,
|
||||
subject,
|
||||
}),
|
||||
).resolves.toMatchObject({
|
||||
capabilityGrantId: rotatedCapabilityGrantId,
|
||||
runState: "canceled",
|
||||
});
|
||||
expect(fixture.authorization.authorize).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("classifies retrieval migrations from reasoning-model compatibility", async () => {
|
||||
const compatibleCandidate = profileRevision(
|
||||
"retrieval",
|
||||
|
||||
@ -54,6 +54,12 @@ export interface KnowledgeSpaceProfileMigrationService {
|
||||
readonly runId: string;
|
||||
},
|
||||
): Promise<KnowledgeSpaceProfileMigrationRun | null>;
|
||||
findByCandidate(
|
||||
input: KnowledgeSpaceProfileMigrationPrincipal & {
|
||||
readonly candidateProfileId: string;
|
||||
readonly knowledgeSpaceId: string;
|
||||
},
|
||||
): Promise<KnowledgeSpaceProfileMigrationRun | null>;
|
||||
requiresMigration(input: {
|
||||
readonly knowledgeSpaceId: string;
|
||||
readonly tenantId: string;
|
||||
@ -241,6 +247,14 @@ export function createKnowledgeSpaceProfileMigrationService({
|
||||
});
|
||||
},
|
||||
get: (input) => getAuthorized(input, input.knowledgeSpaceId, input.runId),
|
||||
findByCandidate: async (input) => {
|
||||
await authorize(input, input.knowledgeSpaceId);
|
||||
return repository.findLatestByCandidate({
|
||||
candidateProfileId: input.candidateProfileId,
|
||||
knowledgeSpaceId: input.knowledgeSpaceId,
|
||||
tenantId: input.subject.tenantId,
|
||||
});
|
||||
},
|
||||
cancel: async (input) => {
|
||||
const run = await getAuthorized(input, input.knowledgeSpaceId, input.runId);
|
||||
if (!run) return null;
|
||||
|
||||
@ -31,6 +31,20 @@ describe("knowledge-space profile migration durable repository", () => {
|
||||
const input = startInput();
|
||||
const first = await repository.start(input);
|
||||
expect(await repository.start(input)).toEqual(first);
|
||||
await expect(
|
||||
repository.findLatestByCandidate({
|
||||
candidateProfileId: input.candidateProfile.id,
|
||||
knowledgeSpaceId: spaceId,
|
||||
tenantId,
|
||||
}),
|
||||
).resolves.toEqual(first);
|
||||
await expect(
|
||||
repository.findLatestByCandidate({
|
||||
candidateProfileId: input.candidateProfile.id,
|
||||
knowledgeSpaceId: spaceId,
|
||||
tenantId: "tenant-other",
|
||||
}),
|
||||
).resolves.toBeNull();
|
||||
await expect(
|
||||
repository.start({
|
||||
...input,
|
||||
|
||||
@ -149,6 +149,11 @@ export interface KnowledgeSpaceProfileMigrationRepository {
|
||||
readonly requestedBySubjectId?: string | undefined;
|
||||
readonly tenantId: string;
|
||||
}): Promise<KnowledgeSpaceProfileMigrationRun | null>;
|
||||
findLatestByCandidate(input: {
|
||||
readonly candidateProfileId: string;
|
||||
readonly knowledgeSpaceId: string;
|
||||
readonly tenantId: string;
|
||||
}): Promise<KnowledgeSpaceProfileMigrationRun | null>;
|
||||
get(runId: string): Promise<KnowledgeSpaceProfileMigrationRun | null>;
|
||||
heartbeat(
|
||||
input: KnowledgeSpaceProfileMigrationFence & {
|
||||
@ -270,6 +275,21 @@ export function createInMemoryKnowledgeSpaceProfileMigrationRepository({
|
||||
const id = requestKeys.get(key);
|
||||
return id ? (runs.get(id) ?? null) : null;
|
||||
},
|
||||
findLatestByCandidate: async (input) =>
|
||||
[...runs.values()]
|
||||
.filter(
|
||||
(run) =>
|
||||
run.tenantId === input.tenantId &&
|
||||
run.knowledgeSpaceId === input.knowledgeSpaceId &&
|
||||
run.candidateProfile.id === input.candidateProfileId,
|
||||
)
|
||||
.sort(
|
||||
(left, right) =>
|
||||
Number(right.runState === "queued" || right.runState === "running") -
|
||||
Number(left.runState === "queued" || left.runState === "running") ||
|
||||
right.createdAt.localeCompare(left.createdAt) ||
|
||||
right.id.localeCompare(left.id),
|
||||
)[0] ?? null,
|
||||
claim: async (raw) => {
|
||||
const now = validDate(raw.now, "claim.now");
|
||||
const leaseExpiresAt = validDate(raw.leaseExpiresAt, "claim.leaseExpiresAt");
|
||||
|
||||
@ -151,6 +151,8 @@ import {
|
||||
zGetKnowledgeFsSpacesByControlSpaceIdSourceWorkflowsByRunIdPagesResponse,
|
||||
zGetKnowledgeFsSpacesByControlSpaceIdSourceWorkflowsByRunIdPath,
|
||||
zGetKnowledgeFsSpacesByControlSpaceIdSourceWorkflowsByRunIdResponse,
|
||||
zGetKnowledgeFsSpacesByControlSpaceIdTagsPath,
|
||||
zGetKnowledgeFsSpacesByControlSpaceIdTagsResponse,
|
||||
zGetKnowledgeFsSpacesByControlSpaceIdTracesByTraceIdConflictsPath,
|
||||
zGetKnowledgeFsSpacesByControlSpaceIdTracesByTraceIdConflictsQuery,
|
||||
zGetKnowledgeFsSpacesByControlSpaceIdTracesByTraceIdConflictsResponse,
|
||||
@ -327,6 +329,9 @@ import {
|
||||
zPutKnowledgeFsSpacesByControlSpaceIdSourcesBySourceIdSyncPolicyBody,
|
||||
zPutKnowledgeFsSpacesByControlSpaceIdSourcesBySourceIdSyncPolicyPath,
|
||||
zPutKnowledgeFsSpacesByControlSpaceIdSourcesBySourceIdSyncPolicyResponse,
|
||||
zPutKnowledgeFsSpacesByControlSpaceIdTagsBody,
|
||||
zPutKnowledgeFsSpacesByControlSpaceIdTagsPath,
|
||||
zPutKnowledgeFsSpacesByControlSpaceIdTagsResponse,
|
||||
} from './zod.gen'
|
||||
|
||||
export const get = oc
|
||||
@ -2299,6 +2304,38 @@ export const sources = {
|
||||
}
|
||||
|
||||
export const get45 = oc
|
||||
.route({
|
||||
inputStructure: 'detailed',
|
||||
method: 'GET',
|
||||
operationId: 'getKnowledgeFsSpacesByControlSpaceIdTags',
|
||||
path: '/knowledge-fs/spaces/{control_space_id}/tags',
|
||||
tags: ['console'],
|
||||
})
|
||||
.input(z.object({ params: zGetKnowledgeFsSpacesByControlSpaceIdTagsPath }))
|
||||
.output(zGetKnowledgeFsSpacesByControlSpaceIdTagsResponse)
|
||||
|
||||
export const put5 = oc
|
||||
.route({
|
||||
inputStructure: 'detailed',
|
||||
method: 'PUT',
|
||||
operationId: 'putKnowledgeFsSpacesByControlSpaceIdTags',
|
||||
path: '/knowledge-fs/spaces/{control_space_id}/tags',
|
||||
tags: ['console'],
|
||||
})
|
||||
.input(
|
||||
z.object({
|
||||
body: zPutKnowledgeFsSpacesByControlSpaceIdTagsBody,
|
||||
params: zPutKnowledgeFsSpacesByControlSpaceIdTagsPath,
|
||||
}),
|
||||
)
|
||||
.output(zPutKnowledgeFsSpacesByControlSpaceIdTagsResponse)
|
||||
|
||||
export const tags = {
|
||||
get: get45,
|
||||
put: put5,
|
||||
}
|
||||
|
||||
export const get46 = oc
|
||||
.route({
|
||||
inputStructure: 'detailed',
|
||||
method: 'GET',
|
||||
@ -2315,10 +2352,10 @@ export const get45 = oc
|
||||
.output(zGetKnowledgeFsSpacesByControlSpaceIdTracesByTraceIdConflictsResponse)
|
||||
|
||||
export const conflicts = {
|
||||
get: get45,
|
||||
get: get46,
|
||||
}
|
||||
|
||||
export const get46 = oc
|
||||
export const get47 = oc
|
||||
.route({
|
||||
inputStructure: 'detailed',
|
||||
method: 'GET',
|
||||
@ -2335,10 +2372,10 @@ export const get46 = oc
|
||||
.output(zGetKnowledgeFsSpacesByControlSpaceIdTracesByTraceIdEvidenceResponse)
|
||||
|
||||
export const evidence = {
|
||||
get: get46,
|
||||
get: get47,
|
||||
}
|
||||
|
||||
export const get47 = oc
|
||||
export const get48 = oc
|
||||
.route({
|
||||
inputStructure: 'detailed',
|
||||
method: 'GET',
|
||||
@ -2355,10 +2392,10 @@ export const get47 = oc
|
||||
.output(zGetKnowledgeFsSpacesByControlSpaceIdTracesByTraceIdMissingResponse)
|
||||
|
||||
export const missing = {
|
||||
get: get47,
|
||||
get: get48,
|
||||
}
|
||||
|
||||
export const get48 = oc
|
||||
export const get49 = oc
|
||||
.route({
|
||||
inputStructure: 'detailed',
|
||||
method: 'GET',
|
||||
@ -2370,13 +2407,13 @@ export const get48 = oc
|
||||
.output(zGetKnowledgeFsSpacesByControlSpaceIdTracesByTraceIdResponse)
|
||||
|
||||
export const byTraceId = {
|
||||
get: get48,
|
||||
get: get49,
|
||||
conflicts,
|
||||
evidence,
|
||||
missing,
|
||||
}
|
||||
|
||||
export const get49 = oc
|
||||
export const get50 = oc
|
||||
.route({
|
||||
inputStructure: 'detailed',
|
||||
method: 'GET',
|
||||
@ -2393,7 +2430,7 @@ export const get49 = oc
|
||||
.output(zGetKnowledgeFsSpacesByControlSpaceIdTracesResponse)
|
||||
|
||||
export const traces = {
|
||||
get: get49,
|
||||
get: get50,
|
||||
byTraceId,
|
||||
}
|
||||
|
||||
@ -2531,7 +2568,7 @@ export const delete13 = oc
|
||||
.input(z.object({ params: zDeleteKnowledgeFsSpacesByControlSpaceIdPath }))
|
||||
.output(zDeleteKnowledgeFsSpacesByControlSpaceIdResponse)
|
||||
|
||||
export const get50 = oc
|
||||
export const get51 = oc
|
||||
.route({
|
||||
inputStructure: 'detailed',
|
||||
method: 'GET',
|
||||
@ -2560,7 +2597,7 @@ export const patch9 = oc
|
||||
|
||||
export const byControlSpaceId = {
|
||||
delete: delete13,
|
||||
get: get50,
|
||||
get: get51,
|
||||
patch: patch9,
|
||||
appBindings,
|
||||
backgroundTasks,
|
||||
@ -2584,11 +2621,12 @@ export const byControlSpaceId = {
|
||||
sourceProviders,
|
||||
sourceWorkflows,
|
||||
sources,
|
||||
tags,
|
||||
traces,
|
||||
uploadSessions,
|
||||
}
|
||||
|
||||
export const get51 = oc
|
||||
export const get52 = oc
|
||||
.route({
|
||||
inputStructure: 'detailed',
|
||||
method: 'GET',
|
||||
@ -2612,7 +2650,7 @@ export const post40 = oc
|
||||
.output(zPostKnowledgeFsSpacesResponse)
|
||||
|
||||
export const spaces = {
|
||||
get: get51,
|
||||
get: get52,
|
||||
post: post40,
|
||||
byControlSpaceId,
|
||||
}
|
||||
|
||||
@ -1084,6 +1084,14 @@ export type KnowledgeFsSourceWorkflowImportPayload =
|
||||
kind: 'online-drive-import'
|
||||
} & KnowledgeFsOnlineDriveWorkflowImportPayload)
|
||||
|
||||
export type KnowledgeFsSpaceTagListResponse = {
|
||||
data: Array<KnowledgeFsSpaceTagResponse>
|
||||
}
|
||||
|
||||
export type KnowledgeFsSpaceTagsReplacePayload = {
|
||||
tag_ids?: Array<string>
|
||||
}
|
||||
|
||||
export type KnowledgeFsTraceListResponse = {
|
||||
data: Array<KnowledgeFsTraceResponse>
|
||||
next_cursor?: string | null
|
||||
@ -1220,6 +1228,7 @@ export type KnowledgeFsSpaceListItemResponse = {
|
||||
permission_keys: Array<KnowledgeFsProductPermission>
|
||||
resource_version: number
|
||||
state: KnowledgeFsControlSpaceState
|
||||
tags?: Array<KnowledgeFsSpaceTagResponse>
|
||||
technical_status: 'available' | 'not_ready' | 'unavailable'
|
||||
technical_summary?: KnowledgeFsTechnicalSummary | null
|
||||
updated_at: string
|
||||
@ -1905,6 +1914,12 @@ export type KnowledgeFsOnlineDriveWorkflowImportPayload = {
|
||||
kind: 'online-drive-import'
|
||||
}
|
||||
|
||||
export type KnowledgeFsSpaceTagResponse = {
|
||||
id: string
|
||||
name: string
|
||||
type?: 'knowledge'
|
||||
}
|
||||
|
||||
export type KnowledgeFsTraceResponse = {
|
||||
completed: boolean
|
||||
created_at: string
|
||||
@ -2241,6 +2256,7 @@ export type GetKnowledgeFsSpacesData = {
|
||||
limit?: number
|
||||
page?: number
|
||||
query?: string
|
||||
tag_ids?: Array<string>
|
||||
}
|
||||
url: '/knowledge-fs/spaces'
|
||||
}
|
||||
@ -4012,6 +4028,38 @@ export type PostKnowledgeFsSpacesByControlSpaceIdSourcesBySourceIdWorkflowImport
|
||||
export type PostKnowledgeFsSpacesByControlSpaceIdSourcesBySourceIdWorkflowImportsResponse =
|
||||
PostKnowledgeFsSpacesByControlSpaceIdSourcesBySourceIdWorkflowImportsResponses[keyof PostKnowledgeFsSpacesByControlSpaceIdSourcesBySourceIdWorkflowImportsResponses]
|
||||
|
||||
export type GetKnowledgeFsSpacesByControlSpaceIdTagsData = {
|
||||
body?: never
|
||||
path: {
|
||||
control_space_id: string
|
||||
}
|
||||
query?: never
|
||||
url: '/knowledge-fs/spaces/{control_space_id}/tags'
|
||||
}
|
||||
|
||||
export type GetKnowledgeFsSpacesByControlSpaceIdTagsResponses = {
|
||||
200: KnowledgeFsSpaceTagListResponse
|
||||
}
|
||||
|
||||
export type GetKnowledgeFsSpacesByControlSpaceIdTagsResponse =
|
||||
GetKnowledgeFsSpacesByControlSpaceIdTagsResponses[keyof GetKnowledgeFsSpacesByControlSpaceIdTagsResponses]
|
||||
|
||||
export type PutKnowledgeFsSpacesByControlSpaceIdTagsData = {
|
||||
body: KnowledgeFsSpaceTagsReplacePayload
|
||||
path: {
|
||||
control_space_id: string
|
||||
}
|
||||
query?: never
|
||||
url: '/knowledge-fs/spaces/{control_space_id}/tags'
|
||||
}
|
||||
|
||||
export type PutKnowledgeFsSpacesByControlSpaceIdTagsResponses = {
|
||||
200: KnowledgeFsSpaceTagListResponse
|
||||
}
|
||||
|
||||
export type PutKnowledgeFsSpacesByControlSpaceIdTagsResponse =
|
||||
PutKnowledgeFsSpacesByControlSpaceIdTagsResponses[keyof PutKnowledgeFsSpacesByControlSpaceIdTagsResponses]
|
||||
|
||||
export type GetKnowledgeFsSpacesByControlSpaceIdTracesData = {
|
||||
body?: never
|
||||
path: {
|
||||
|
||||
@ -549,6 +549,13 @@ export const zKnowledgeFsSourceSyncPolicyPayload = z.object({
|
||||
mode: z.enum(['custom', 'interval', 'manual', 'provider']),
|
||||
})
|
||||
|
||||
/**
|
||||
* KnowledgeFSSpaceTagsReplacePayload
|
||||
*/
|
||||
export const zKnowledgeFsSpaceTagsReplacePayload = z.object({
|
||||
tag_ids: z.array(z.string()).max(100).optional(),
|
||||
})
|
||||
|
||||
/**
|
||||
* KnowledgeFSUploadSessionCreatePayload
|
||||
*/
|
||||
@ -867,34 +874,6 @@ export const zKnowledgeFsSpaceDetailResponse = z.object({
|
||||
visibility: zKnowledgeFsControlSpaceVisibility,
|
||||
})
|
||||
|
||||
/**
|
||||
* KnowledgeFSSpaceListItemResponse
|
||||
*/
|
||||
export const zKnowledgeFsSpaceListItemResponse = z.object({
|
||||
control_space_id: z.string(),
|
||||
created_at: z.iso.datetime(),
|
||||
knowledge_space_id: z.string().nullable(),
|
||||
linked_apps: z.int().gte(0),
|
||||
owner_account_id: z.string(),
|
||||
permission_keys: z.array(zKnowledgeFsProductPermission),
|
||||
resource_version: z.int(),
|
||||
state: zKnowledgeFsControlSpaceState,
|
||||
technical_status: z.enum(['available', 'not_ready', 'unavailable']),
|
||||
technical_summary: zKnowledgeFsTechnicalSummary.nullish(),
|
||||
updated_at: z.iso.datetime(),
|
||||
visibility: zKnowledgeFsControlSpaceVisibility,
|
||||
})
|
||||
|
||||
/**
|
||||
* KnowledgeFSSpaceListResponse
|
||||
*/
|
||||
export const zKnowledgeFsSpaceListResponse = z.object({
|
||||
data: z.array(zKnowledgeFsSpaceListItemResponse),
|
||||
has_more: z.boolean(),
|
||||
limit: z.int(),
|
||||
page: z.int(),
|
||||
})
|
||||
|
||||
/**
|
||||
* KnowledgeFSAppSpaceJoinType
|
||||
*/
|
||||
@ -1997,6 +1976,51 @@ export const zKnowledgeFsSourceImportFilesPayload = z.object({
|
||||
files: z.array(zKnowledgeFsSourceImportFilePayload).min(1).max(200),
|
||||
})
|
||||
|
||||
/**
|
||||
* KnowledgeFSSpaceTagResponse
|
||||
*/
|
||||
export const zKnowledgeFsSpaceTagResponse = z.object({
|
||||
id: z.string(),
|
||||
name: z.string(),
|
||||
type: z.literal('knowledge').optional().default('knowledge'),
|
||||
})
|
||||
|
||||
/**
|
||||
* KnowledgeFSSpaceTagListResponse
|
||||
*/
|
||||
export const zKnowledgeFsSpaceTagListResponse = z.object({
|
||||
data: z.array(zKnowledgeFsSpaceTagResponse),
|
||||
})
|
||||
|
||||
/**
|
||||
* KnowledgeFSSpaceListItemResponse
|
||||
*/
|
||||
export const zKnowledgeFsSpaceListItemResponse = z.object({
|
||||
control_space_id: z.string(),
|
||||
created_at: z.iso.datetime(),
|
||||
knowledge_space_id: z.string().nullable(),
|
||||
linked_apps: z.int().gte(0),
|
||||
owner_account_id: z.string(),
|
||||
permission_keys: z.array(zKnowledgeFsProductPermission),
|
||||
resource_version: z.int(),
|
||||
state: zKnowledgeFsControlSpaceState,
|
||||
tags: z.array(zKnowledgeFsSpaceTagResponse).optional(),
|
||||
technical_status: z.enum(['available', 'not_ready', 'unavailable']),
|
||||
technical_summary: zKnowledgeFsTechnicalSummary.nullish(),
|
||||
updated_at: z.iso.datetime(),
|
||||
visibility: zKnowledgeFsControlSpaceVisibility,
|
||||
})
|
||||
|
||||
/**
|
||||
* KnowledgeFSSpaceListResponse
|
||||
*/
|
||||
export const zKnowledgeFsSpaceListResponse = z.object({
|
||||
data: z.array(zKnowledgeFsSpaceListItemResponse),
|
||||
has_more: z.boolean(),
|
||||
limit: z.int(),
|
||||
page: z.int(),
|
||||
})
|
||||
|
||||
/**
|
||||
* KnowledgeFSAnswerTraceStepResponse
|
||||
*/
|
||||
@ -2837,6 +2861,7 @@ export const zGetKnowledgeFsSpacesQuery = z.object({
|
||||
limit: z.int().gte(1).lte(100).optional().default(20),
|
||||
page: z.int().gte(1).optional().default(1),
|
||||
query: z.string().max(255).optional(),
|
||||
tag_ids: z.array(z.string().min(1).max(255)).max(100).optional(),
|
||||
})
|
||||
|
||||
/**
|
||||
@ -4182,6 +4207,26 @@ export const zPostKnowledgeFsSpacesByControlSpaceIdSourcesBySourceIdWorkflowImpo
|
||||
export const zPostKnowledgeFsSpacesByControlSpaceIdSourcesBySourceIdWorkflowImportsResponse =
|
||||
zKnowledgeFsSourceWorkflowResponse
|
||||
|
||||
export const zGetKnowledgeFsSpacesByControlSpaceIdTagsPath = z.object({
|
||||
control_space_id: z.string(),
|
||||
})
|
||||
|
||||
/**
|
||||
* KnowledgeFS space tags
|
||||
*/
|
||||
export const zGetKnowledgeFsSpacesByControlSpaceIdTagsResponse = zKnowledgeFsSpaceTagListResponse
|
||||
|
||||
export const zPutKnowledgeFsSpacesByControlSpaceIdTagsBody = zKnowledgeFsSpaceTagsReplacePayload
|
||||
|
||||
export const zPutKnowledgeFsSpacesByControlSpaceIdTagsPath = z.object({
|
||||
control_space_id: z.string(),
|
||||
})
|
||||
|
||||
/**
|
||||
* KnowledgeFS space tags replaced
|
||||
*/
|
||||
export const zPutKnowledgeFsSpacesByControlSpaceIdTagsResponse = zKnowledgeFsSpaceTagListResponse
|
||||
|
||||
export const zGetKnowledgeFsSpacesByControlSpaceIdTracesPath = z.object({
|
||||
control_space_id: z.string(),
|
||||
})
|
||||
|
||||
@ -199,6 +199,7 @@ const taskApiResponse = vi.hoisted(() => (item: BackgroundTask) => ({
|
||||
document_revision: item.documentRevision ?? null,
|
||||
error_code: item.errorCode ?? null,
|
||||
error_message: item.errorMessage ?? null,
|
||||
failure: item.failure ?? null,
|
||||
id: item.id,
|
||||
knowledge_space_id: item.knowledgeSpaceId,
|
||||
operation: item.operation ?? 'document_processing',
|
||||
@ -1010,6 +1011,46 @@ describe('DocumentsPage', () => {
|
||||
expect(screen.queryByText('Ready handbook.pdf')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('reveals the latest document task failure reason from the failed status', async () => {
|
||||
const user = userEvent.setup()
|
||||
documentsQuery.data = {
|
||||
pages: [{ items: [document({ id: 'failed-document', title: 'Failed report.pdf' })] }],
|
||||
}
|
||||
tasksQuery.data = {
|
||||
pages: [
|
||||
{
|
||||
items: [
|
||||
task({
|
||||
documentId: 'failed-document',
|
||||
failure: {
|
||||
action: 'configure_model',
|
||||
category: 'configuration',
|
||||
code: 'MODEL_SELECTION_NOT_FOUND',
|
||||
message: 'Select another model.',
|
||||
retryPolicy: 'after_configuration',
|
||||
},
|
||||
id: 'failed-task',
|
||||
state: 'failed',
|
||||
}),
|
||||
],
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
render(<DocumentsPage knowledgeSpaceId="space-1" />)
|
||||
|
||||
const failedStatus = screen.getByRole('button', {
|
||||
name: 'dataset.newKnowledge.documentStatus.failed: dataset.newKnowledge.taskFailure.configuration',
|
||||
})
|
||||
expect(screen.queryByText('dataset.newKnowledge.taskFailure.configuration')).toBeNull()
|
||||
|
||||
await user.hover(failedStatus)
|
||||
|
||||
expect(
|
||||
await screen.findByText('dataset.newKnowledge.taskFailure.configuration'),
|
||||
).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('downloads the active revision from the document action menu', async () => {
|
||||
const user = userEvent.setup()
|
||||
documentsQuery.data = {
|
||||
|
||||
@ -1264,7 +1264,7 @@ describe('KnowledgeSettingsForm', () => {
|
||||
).not.toHaveAttribute('aria-disabled', 'true')
|
||||
})
|
||||
|
||||
it('keeps API access available while model validation is pending', () => {
|
||||
it('keeps API access available without showing an idle pending-validation status', () => {
|
||||
renderForm({
|
||||
settings: {
|
||||
...settings,
|
||||
@ -1280,7 +1280,7 @@ describe('KnowledgeSettingsForm', () => {
|
||||
expect(apiAccessSwitch).toHaveAccessibleDescription(
|
||||
'dataset.newKnowledge.settings.apiAccessDescription',
|
||||
)
|
||||
expect(screen.getByRole('status')).toHaveTextContent('common.provider.validating')
|
||||
expect(screen.queryByRole('status')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('shows a recovery alert when initial model validation fails', () => {
|
||||
|
||||
@ -18,6 +18,7 @@ type KnowledgeSpaceList = {
|
||||
revision: number
|
||||
slug: string
|
||||
tenantId: string
|
||||
tags?: Array<{ id: string; name: string }>
|
||||
updatedAt: string
|
||||
documentCount?: number
|
||||
}>
|
||||
@ -33,6 +34,7 @@ type ListKnowledgeSpacesInfiniteOptions = {
|
||||
limit: number
|
||||
page: number
|
||||
query?: string
|
||||
tag_ids?: string[]
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -57,6 +59,7 @@ const knowledgeSpaceApiResponse = vi.hoisted(
|
||||
permission_keys: space.permissionKeys ?? ['knowledge_space_read'],
|
||||
resource_version: space.revision,
|
||||
state: 'active',
|
||||
tags: space.tags?.map((tag) => ({ ...tag, type: 'knowledge' as const })),
|
||||
technical_status: 'available',
|
||||
technical_summary: {
|
||||
description: space.description ?? null,
|
||||
@ -213,6 +216,35 @@ vi.mock('@/features/system-features/state', () => ({
|
||||
knowledgeFsUploadEnabledAtom: systemFeaturesStateMock.knowledgeFsUploadEnabledAtom,
|
||||
}))
|
||||
|
||||
vi.mock('@/features/tag-management/components/tag-filter', () => ({
|
||||
TagFilter: ({ onChange, value }: { onChange: (value: string[]) => void; value: string[] }) => (
|
||||
<button
|
||||
type="button"
|
||||
aria-label="dataset.newKnowledge.tags"
|
||||
onClick={() => onChange(value.length ? [] : ['tag-1', 'tag-2'])}
|
||||
>
|
||||
dataset.newKnowledge.tags
|
||||
</button>
|
||||
),
|
||||
}))
|
||||
|
||||
vi.mock('@/features/tag-management/components/tag-management-modal', () => ({
|
||||
TagManagementModal: () => null,
|
||||
}))
|
||||
|
||||
vi.mock('../components/knowledge-space-card-tags', () => ({
|
||||
KnowledgeSpaceCardTags: ({
|
||||
knowledgeSpace,
|
||||
}: {
|
||||
knowledgeSpace: { tags?: Array<{ id: string; name: string }> }
|
||||
}) => (
|
||||
<div>
|
||||
<span>dataset.newKnowledge.tags</span>
|
||||
{knowledgeSpace.tags?.map((tag) => tag.name).join(', ')}
|
||||
</div>
|
||||
),
|
||||
}))
|
||||
|
||||
vi.mock('@/features/account-profile/client', () => ({
|
||||
userProfileQueryOptions: () => ({}),
|
||||
}))
|
||||
@ -308,6 +340,10 @@ describe('NewKnowledgeList', () => {
|
||||
name: 'Support knowledge',
|
||||
revision: 1,
|
||||
slug: 'support-knowledge',
|
||||
tags: [
|
||||
{ id: 'tag-1', name: 'Customer support' },
|
||||
{ id: 'tag-2', name: 'Public docs' },
|
||||
],
|
||||
tenantId: 'tenant-1',
|
||||
updatedAt: '2026-07-18T00:00:00Z',
|
||||
},
|
||||
@ -327,6 +363,8 @@ describe('NewKnowledgeList', () => {
|
||||
const supportCard = within(list).getByRole('link', {
|
||||
name: 'Support knowledge',
|
||||
})
|
||||
const supportCardItem = supportCard.closest('li')
|
||||
expect(supportCardItem).not.toBeNull()
|
||||
expect(supportCard).toHaveAttribute('href', '/datasets/new/space-1')
|
||||
expect(supportCard).toBeInTheDocument()
|
||||
expect(
|
||||
@ -339,7 +377,8 @@ describe('NewKnowledgeList', () => {
|
||||
expect(within(supportCard).getByLabelText('camera')).toBeInTheDocument()
|
||||
expect(within(list).getAllByText('dataset.newKnowledge.cardType')).toHaveLength(2)
|
||||
expect(within(list).getAllByText('dataset.newKnowledge.tags')).toHaveLength(2)
|
||||
expect(within(supportCard).getByText('12')).toBeInTheDocument()
|
||||
expect(within(list).getByText('Customer support, Public docs')).toBeInTheDocument()
|
||||
expect(within(supportCardItem!).getByText('12')).toBeInTheDocument()
|
||||
expect(supportCard).toHaveAccessibleDescription('dataset.newKnowledge.overview.linkedApps: 0')
|
||||
expect(
|
||||
within(list).getByRole('link', { name: 'Engineering handbook' }),
|
||||
@ -453,7 +492,7 @@ describe('NewKnowledgeList', () => {
|
||||
expect(toastMock.success).toHaveBeenCalledWith('dataset.datasetDeleted')
|
||||
})
|
||||
|
||||
it('keeps backend-dependent metadata filters interactive and sends search to the collection API', async () => {
|
||||
it('syncs tag filters to the URL and collection API while keeping search interactive', async () => {
|
||||
const user = userEvent.setup()
|
||||
setResolvedPage([
|
||||
{
|
||||
@ -493,13 +532,23 @@ describe('NewKnowledgeList', () => {
|
||||
expect(tags).toBeEnabled()
|
||||
expect(creators).toBeEnabled()
|
||||
await user.click(tags)
|
||||
expect(toastMock.info).toHaveBeenCalledWith('dataset.newKnowledge.filtersUnavailable')
|
||||
await waitFor(() => {
|
||||
expect(onUrlUpdate.mock.calls.at(-1)?.[0].searchParams.get('tag_ids')).toBe('tag-1;tag-2')
|
||||
})
|
||||
let options = consoleQueryMock.infiniteOptions.mock.calls.at(-1)?.[0]
|
||||
expect(options?.input(1)).toEqual({
|
||||
query: { limit: 30, page: 1, tag_ids: ['tag-1', 'tag-2'] },
|
||||
})
|
||||
await user.click(tags)
|
||||
await waitFor(() => {
|
||||
expect(onUrlUpdate.mock.calls.at(-1)?.[0].searchParams.has('tag_ids')).toBe(false)
|
||||
})
|
||||
expect(search).toBeEnabled()
|
||||
expect(create).toHaveAttribute('href', '/datasets/new/create')
|
||||
|
||||
await user.type(search, 'customer support')
|
||||
await waitFor(() => {
|
||||
const options = consoleQueryMock.infiniteOptions.mock.calls.at(-1)?.[0]
|
||||
options = consoleQueryMock.infiniteOptions.mock.calls.at(-1)?.[0]
|
||||
expect(options?.input(1)).toEqual({
|
||||
query: { limit: 30, page: 1, query: 'customer support' },
|
||||
})
|
||||
@ -507,6 +556,19 @@ describe('NewKnowledgeList', () => {
|
||||
expect(onUrlUpdate.mock.calls.at(-1)?.[0].searchParams.get('query')).toBe('customer support')
|
||||
})
|
||||
|
||||
it('restores tag filters from the URL and sends match-any IDs to the collection API', () => {
|
||||
setResolvedPage()
|
||||
|
||||
renderWithNuqs(<NewKnowledgeList view="new" onViewChange={vi.fn()} />, {
|
||||
searchParams: '?tag_ids=tag-1%3Btag-2',
|
||||
})
|
||||
|
||||
const options = consoleQueryMock.infiniteOptions.mock.calls.at(-1)?.[0]
|
||||
expect(options?.input(1)).toEqual({
|
||||
query: { limit: 30, page: 1, tag_ids: ['tag-1', 'tag-2'] },
|
||||
})
|
||||
})
|
||||
|
||||
it('restores server search from the URL and shows its empty state', async () => {
|
||||
const user = userEvent.setup()
|
||||
setResolvedPage()
|
||||
|
||||
@ -426,7 +426,12 @@ describe('RetrievalTestPage', () => {
|
||||
expect(apiMock.queryAdmission).not.toHaveBeenCalled()
|
||||
expect(apiMock.planResearch).not.toHaveBeenCalled()
|
||||
expect(apiMock.createResearch).not.toHaveBeenCalled()
|
||||
const dialog = screen.getByRole('dialog', { name: 'common.provider.validating' })
|
||||
const dialog = screen.getByRole('dialog', {
|
||||
name: 'dataset.newKnowledge.overview.attention.modelReadiness.pendingTitle',
|
||||
})
|
||||
expect(dialog).toHaveTextContent(
|
||||
'dataset.newKnowledge.overview.attention.modelReadiness.pendingDescription',
|
||||
)
|
||||
expect(dialog).not.toHaveTextContent(
|
||||
'dataset.newKnowledge.overview.attention.modelReadiness.profilesMissing',
|
||||
)
|
||||
|
||||
@ -118,7 +118,7 @@ describe('KnowledgeModelReadinessBanner', () => {
|
||||
)
|
||||
})
|
||||
|
||||
it('shows validation progress while keeping the configuration route accessible', () => {
|
||||
it('stays hidden while model validation is waiting for the first document', () => {
|
||||
queryState.data = {
|
||||
...queryState.data,
|
||||
configuration_state: 'pending-validation',
|
||||
@ -127,15 +127,7 @@ describe('KnowledgeModelReadinessBanner', () => {
|
||||
|
||||
render(<KnowledgeModelReadinessBanner knowledgeSpaceId="space-1" />)
|
||||
|
||||
expect(screen.getByRole('status')).toHaveTextContent('common.provider.validating')
|
||||
expect(
|
||||
screen.getByRole('link', {
|
||||
name: 'dataset.newKnowledge.overview.attention.action.configureModels',
|
||||
}),
|
||||
).toHaveAttribute(
|
||||
'href',
|
||||
'/datasets/new/space-1/settings?returnTo=%2Fdatasets%2Fnew%2Fspace-1%2Fdocuments%3Fstatus%3Dfailed',
|
||||
)
|
||||
expect(screen.queryByRole('status')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('keeps a readiness fetch failure separate and retryable', async () => {
|
||||
|
||||
@ -0,0 +1,203 @@
|
||||
import type { KnowledgeFsSpaceListItemResponse } from '@dify/contracts/api/console/knowledge-fs/types.gen'
|
||||
import type { TagResponse as Tag } from '@dify/contracts/api/console/tags/types.gen'
|
||||
import { screen, waitFor } from '@testing-library/react'
|
||||
import userEvent from '@testing-library/user-event'
|
||||
import { render } from '@/test/console/render'
|
||||
import { KnowledgeSpaceCardTags } from '../knowledge-space-card-tags'
|
||||
|
||||
const { invalidateQueries, putTags, toastMock } = vi.hoisted(() => ({
|
||||
invalidateQueries: vi.fn(),
|
||||
putTags: vi.fn(),
|
||||
toastMock: {
|
||||
error: vi.fn(),
|
||||
success: vi.fn(),
|
||||
},
|
||||
}))
|
||||
|
||||
const workspacePermissionKeys = vi.hoisted(() => ({
|
||||
value: ['dataset.tag.manage'] as string[],
|
||||
}))
|
||||
|
||||
const tagQuery = vi.hoisted(() => ({
|
||||
data: [] as Tag[],
|
||||
}))
|
||||
|
||||
const knowledgeTags: Tag[] = [
|
||||
{ binding_count: '1', id: 'tag-1', name: 'Frontend', type: 'knowledge' },
|
||||
{ binding_count: '1', id: 'tag-2', name: 'Backend', type: 'knowledge' },
|
||||
{ binding_count: '0', id: 'tag-3', name: 'Public docs', type: 'knowledge' },
|
||||
]
|
||||
|
||||
vi.mock('@langgenius/dify-ui/toast', () => ({ toast: toastMock }))
|
||||
|
||||
vi.mock('@/context/permission-state', async () => {
|
||||
const { createPermissionStateModuleMock } = await import('@/test/console/state-fixture')
|
||||
return createPermissionStateModuleMock(() => ({
|
||||
workspacePermissionKeys: workspacePermissionKeys.value,
|
||||
}))
|
||||
})
|
||||
|
||||
vi.mock('@/features/tag-management/hooks/use-tag-mutations', () => ({
|
||||
useApplyTagBindingsMutation: () => ({ mutate: vi.fn() }),
|
||||
}))
|
||||
|
||||
vi.mock('@tanstack/react-query', async (importOriginal) => {
|
||||
const original = await importOriginal<typeof import('@tanstack/react-query')>()
|
||||
return {
|
||||
...original,
|
||||
useMutation: (options: {
|
||||
mutationFn?: (input: unknown) => Promise<unknown>
|
||||
onError?: () => void
|
||||
onSettled?: () => void
|
||||
onSuccess?: () => void
|
||||
}) => ({
|
||||
isPending: false,
|
||||
mutate: (input: unknown) => {
|
||||
Promise.resolve(options.mutationFn?.(input))
|
||||
.then(
|
||||
() => options.onSuccess?.(),
|
||||
() => options.onError?.(),
|
||||
)
|
||||
.finally(() => options.onSettled?.())
|
||||
},
|
||||
}),
|
||||
useQuery: () => ({ data: tagQuery.data }),
|
||||
useQueryClient: () => ({ invalidateQueries }),
|
||||
}
|
||||
})
|
||||
|
||||
vi.mock('@/service/client', () => ({
|
||||
consoleQuery: {
|
||||
knowledgeFs: {
|
||||
spaces: {
|
||||
byControlSpaceId: {
|
||||
tags: {
|
||||
put: {
|
||||
mutationOptions: () => ({ mutationFn: putTags }),
|
||||
},
|
||||
},
|
||||
},
|
||||
get: {
|
||||
key: () => ['knowledge-fs', 'spaces'],
|
||||
},
|
||||
},
|
||||
},
|
||||
tags: {
|
||||
get: {
|
||||
key: () => ['tags'],
|
||||
queryOptions: () => ({}),
|
||||
},
|
||||
post: {
|
||||
mutationOptions: () => ({ mutationFn: vi.fn() }),
|
||||
},
|
||||
},
|
||||
},
|
||||
}))
|
||||
|
||||
function createKnowledgeSpace(
|
||||
permissionKeys: KnowledgeFsSpaceListItemResponse['permission_keys'],
|
||||
): KnowledgeFsSpaceListItemResponse {
|
||||
return {
|
||||
control_space_id: 'space-1',
|
||||
created_at: '2026-08-13T00:00:00Z',
|
||||
knowledge_space_id: 'knowledge-space-1',
|
||||
linked_apps: 0,
|
||||
owner_account_id: 'account-1',
|
||||
permission_keys: permissionKeys,
|
||||
resource_version: 1,
|
||||
state: 'active',
|
||||
tags: [
|
||||
{ id: 'tag-1', name: 'Frontend', type: 'knowledge' },
|
||||
{ id: 'tag-2', name: 'Backend', type: 'knowledge' },
|
||||
],
|
||||
technical_status: 'available',
|
||||
updated_at: '2026-08-13T00:00:00Z',
|
||||
visibility: 'only_me',
|
||||
}
|
||||
}
|
||||
|
||||
describe('KnowledgeSpaceCardTags', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
putTags.mockResolvedValue({ data: [] })
|
||||
invalidateQueries.mockResolvedValue(undefined)
|
||||
workspacePermissionKeys.value = ['dataset.tag.manage']
|
||||
tagQuery.data = knowledgeTags
|
||||
})
|
||||
|
||||
it('shows tags returned with the knowledge space', () => {
|
||||
tagQuery.data = []
|
||||
render(
|
||||
<KnowledgeSpaceCardTags
|
||||
knowledgeSpace={createKnowledgeSpace(['knowledge_space_read'])}
|
||||
onOpenTagManagement={vi.fn()}
|
||||
/>,
|
||||
)
|
||||
|
||||
expect(screen.getByText('Frontend')).toBeInTheDocument()
|
||||
expect(screen.getByText('Backend')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('submits the final tag set and refreshes the list and binding counts', async () => {
|
||||
const user = userEvent.setup()
|
||||
render(
|
||||
<KnowledgeSpaceCardTags
|
||||
knowledgeSpace={createKnowledgeSpace(['knowledge_space_edit', 'knowledge_space_read'])}
|
||||
onOpenTagManagement={vi.fn()}
|
||||
/>,
|
||||
)
|
||||
|
||||
const trigger = screen.getByRole('combobox', { name: 'Frontend, Backend' })
|
||||
await user.click(trigger)
|
||||
await user.click(await screen.findByRole('option', { name: 'Frontend' }))
|
||||
await user.click(screen.getByRole('option', { name: 'Public docs' }))
|
||||
await user.click(trigger)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(putTags).toHaveBeenCalledWith({
|
||||
body: { tag_ids: ['tag-2', 'tag-3'] },
|
||||
params: { control_space_id: 'space-1' },
|
||||
})
|
||||
})
|
||||
expect(invalidateQueries).toHaveBeenCalledWith({
|
||||
queryKey: ['knowledge-fs', 'spaces'],
|
||||
})
|
||||
expect(invalidateQueries).toHaveBeenCalledWith({ queryKey: ['tags'] })
|
||||
expect(toastMock.success).toHaveBeenCalledWith('common.actionMsg.modifiedSuccessfully')
|
||||
})
|
||||
|
||||
it('can clear every tag from an editable knowledge space', async () => {
|
||||
const user = userEvent.setup()
|
||||
render(
|
||||
<KnowledgeSpaceCardTags
|
||||
knowledgeSpace={createKnowledgeSpace(['knowledge_space_edit', 'knowledge_space_read'])}
|
||||
onOpenTagManagement={vi.fn()}
|
||||
/>,
|
||||
)
|
||||
|
||||
const trigger = screen.getByRole('combobox', { name: 'Frontend, Backend' })
|
||||
await user.click(trigger)
|
||||
await user.click(await screen.findByRole('option', { name: 'Frontend' }))
|
||||
await user.click(screen.getByRole('option', { name: 'Backend' }))
|
||||
await user.click(trigger)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(putTags).toHaveBeenCalledWith({
|
||||
body: { tag_ids: [] },
|
||||
params: { control_space_id: 'space-1' },
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
it('does not allow binding changes without space edit permission', () => {
|
||||
render(
|
||||
<KnowledgeSpaceCardTags
|
||||
knowledgeSpace={createKnowledgeSpace(['knowledge_space_read'])}
|
||||
onOpenTagManagement={vi.fn()}
|
||||
/>,
|
||||
)
|
||||
|
||||
expect(screen.getByRole('combobox', { name: 'Frontend, Backend' })).toBeDisabled()
|
||||
expect(putTags).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
@ -34,10 +34,12 @@ export function KnowledgeModelReadinessBanner({
|
||||
}),
|
||||
)
|
||||
const readiness = query.data
|
||||
const isPendingValidation = readiness?.configuration_state === 'pending-validation'
|
||||
const requestedCapabilityAvailable =
|
||||
capability !== undefined && readiness?.capabilities[capability] === true
|
||||
if (
|
||||
query.isPending ||
|
||||
isPendingValidation ||
|
||||
(!query.isError &&
|
||||
(requestedCapabilityAvailable ||
|
||||
(capability === undefined &&
|
||||
@ -46,21 +48,17 @@ export function KnowledgeModelReadinessBanner({
|
||||
)
|
||||
return null
|
||||
|
||||
const isPendingValidation = readiness?.configuration_state === 'pending-validation'
|
||||
const isFailure = query.isError || readiness?.configuration_state === 'validation-failed'
|
||||
const title = query.isError
|
||||
? tCommon(($) => $['api.actionFailed'])
|
||||
: isPendingValidation
|
||||
? tCommon(($) => $['provider.validating'])
|
||||
: readiness?.configuration_state === 'validation-failed'
|
||||
? tCommon(($) => $['api.actionFailed'])
|
||||
: t(($) => $['newKnowledge.overview.attention.modelReadiness.title'])
|
||||
const description =
|
||||
query.isError || isPendingValidation
|
||||
? undefined
|
||||
: readiness?.active_profile_available
|
||||
? t(($) => $['newKnowledge.overview.attention.modelReadiness.description'])
|
||||
: t(($) => $['newKnowledge.overview.attention.modelReadiness.profilesMissing'])
|
||||
: readiness?.configuration_state === 'validation-failed'
|
||||
? tCommon(($) => $['api.actionFailed'])
|
||||
: t(($) => $['newKnowledge.overview.attention.modelReadiness.title'])
|
||||
const description = query.isError
|
||||
? undefined
|
||||
: readiness?.active_profile_available
|
||||
? t(($) => $['newKnowledge.overview.attention.modelReadiness.description'])
|
||||
: t(($) => $['newKnowledge.overview.attention.modelReadiness.profilesMissing'])
|
||||
|
||||
return (
|
||||
<KnowledgeModelReadinessNotice
|
||||
@ -81,7 +79,7 @@ export function KnowledgeModelReadinessBanner({
|
||||
className={className}
|
||||
description={description}
|
||||
title={title}
|
||||
tone={isFailure ? 'destructive' : isPendingValidation ? 'progress' : 'warning'}
|
||||
tone={isFailure ? 'destructive' : 'warning'}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import type { ReactNode } from 'react'
|
||||
import { cn } from '@langgenius/dify-ui/cn'
|
||||
|
||||
export type KnowledgeModelReadinessTone = 'destructive' | 'progress' | 'warning'
|
||||
export type KnowledgeModelReadinessTone = 'destructive' | 'warning'
|
||||
|
||||
export const knowledgeModelReadinessActionClassName =
|
||||
'shrink-0 cursor-pointer rounded-sm system-sm-medium text-text-accent outline-hidden hover:underline focus-visible:ring-2 focus-visible:ring-state-accent-solid'
|
||||
@ -20,17 +20,12 @@ export function KnowledgeModelReadinessNotice({
|
||||
tone: KnowledgeModelReadinessTone
|
||||
}) {
|
||||
const isDestructive = tone === 'destructive'
|
||||
const isProgress = tone === 'progress'
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
'flex items-center gap-2.5 overflow-hidden rounded-lg px-3.5 py-2.5',
|
||||
isDestructive
|
||||
? 'bg-state-destructive-hover'
|
||||
: isProgress
|
||||
? 'bg-state-accent-hover'
|
||||
: 'bg-state-warning-hover',
|
||||
isDestructive ? 'bg-state-destructive-hover' : 'bg-state-warning-hover',
|
||||
className,
|
||||
)}
|
||||
role={isDestructive ? 'alert' : 'status'}
|
||||
@ -39,11 +34,9 @@ export function KnowledgeModelReadinessNotice({
|
||||
aria-hidden
|
||||
className={cn(
|
||||
'size-4 shrink-0',
|
||||
isProgress
|
||||
? 'i-ri-loader-2-line animate-spin text-text-accent motion-reduce:animate-none'
|
||||
: isDestructive
|
||||
? 'i-ri-error-warning-fill text-text-destructive'
|
||||
: 'i-ri-error-warning-fill text-text-warning',
|
||||
isDestructive
|
||||
? 'i-ri-error-warning-fill text-text-destructive'
|
||||
: 'i-ri-error-warning-fill text-text-warning',
|
||||
)}
|
||||
/>
|
||||
<p className="min-w-0 flex-1 system-sm-regular text-text-secondary">
|
||||
|
||||
@ -11,7 +11,7 @@ function readinessTitle(
|
||||
tCommon: ReturnType<typeof useTranslation<'common'>>['t'],
|
||||
) {
|
||||
if (readiness?.configuration_state === 'pending-validation')
|
||||
return tCommon(($) => $['provider.validating'])
|
||||
return t(($) => $['newKnowledge.overview.attention.modelReadiness.pendingTitle'])
|
||||
if (readiness?.configuration_state === 'validation-failed')
|
||||
return tCommon(($) => $['api.actionFailed'])
|
||||
return t(($) => $['newKnowledge.overview.attention.modelReadiness.title'])
|
||||
@ -40,7 +40,7 @@ export function KnowledgeModelSetupDialog({
|
||||
}
|
||||
const pendingValidation = readiness?.configuration_state === 'pending-validation'
|
||||
const description = pendingValidation
|
||||
? undefined
|
||||
? t(($) => $['newKnowledge.overview.attention.modelReadiness.pendingDescription'])
|
||||
: readiness?.active_profile_available
|
||||
? t(($) => $['newKnowledge.overview.attention.modelReadiness.description'])
|
||||
: t(($) => $['newKnowledge.overview.attention.modelReadiness.profilesMissing'])
|
||||
|
||||
@ -0,0 +1,67 @@
|
||||
'use client'
|
||||
|
||||
import type { KnowledgeFsSpaceListItemResponse } from '@dify/contracts/api/console/knowledge-fs/types.gen'
|
||||
import type { TagResponse as Tag } from '@dify/contracts/api/console/tags/types.gen'
|
||||
import { toast } from '@langgenius/dify-ui/toast'
|
||||
import { useMutation, useQueryClient } from '@tanstack/react-query'
|
||||
import { useMemo } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { TagSelector } from '@/features/tag-management/components/tag-selector'
|
||||
import { consoleQuery } from '@/service/client'
|
||||
|
||||
export function KnowledgeSpaceCardTags({
|
||||
knowledgeSpace,
|
||||
onOpenTagManagement,
|
||||
}: {
|
||||
knowledgeSpace: KnowledgeFsSpaceListItemResponse
|
||||
onOpenTagManagement: () => void
|
||||
}) {
|
||||
const { t } = useTranslation('common')
|
||||
const queryClient = useQueryClient()
|
||||
const canEdit = knowledgeSpace.permission_keys.includes('knowledge_space_edit')
|
||||
const tags = useMemo<Tag[]>(
|
||||
() =>
|
||||
(knowledgeSpace.tags ?? []).map((tag) => ({
|
||||
binding_count: '',
|
||||
id: tag.id,
|
||||
name: tag.name,
|
||||
type: 'knowledge',
|
||||
})),
|
||||
[knowledgeSpace.tags],
|
||||
)
|
||||
const replaceTagsMutation = useMutation({
|
||||
...consoleQuery.knowledgeFs.spaces.byControlSpaceId.tags.put.mutationOptions(),
|
||||
onError: () => toast.error(t(($) => $['actionMsg.modifiedUnsuccessfully'])),
|
||||
onSuccess: () => toast.success(t(($) => $['actionMsg.modifiedSuccessfully'])),
|
||||
onSettled: () => {
|
||||
void queryClient.invalidateQueries({
|
||||
queryKey: consoleQuery.knowledgeFs.spaces.get.key(),
|
||||
})
|
||||
void queryClient.invalidateQueries({
|
||||
queryKey: consoleQuery.tags.get.key({
|
||||
type: 'query',
|
||||
input: { query: { type: 'knowledge' } },
|
||||
}),
|
||||
})
|
||||
},
|
||||
})
|
||||
|
||||
return (
|
||||
<TagSelector
|
||||
type="knowledge"
|
||||
targetId={knowledgeSpace.control_space_id}
|
||||
value={tags}
|
||||
canBindOrUnbindTags={canEdit}
|
||||
requiresTargetEditPermission
|
||||
showProvidedTagNames
|
||||
onOpenTagManagement={onOpenTagManagement}
|
||||
onApplyTags={(tagIds) =>
|
||||
replaceTagsMutation.mutate({
|
||||
params: { control_space_id: knowledgeSpace.control_space_id },
|
||||
body: { tag_ids: tagIds },
|
||||
})
|
||||
}
|
||||
className="relative z-1 mx-3 w-auto"
|
||||
/>
|
||||
)
|
||||
}
|
||||
@ -5,6 +5,7 @@ import { useFormatTimeFromNow } from '@/hooks/use-format-time-from-now'
|
||||
import Link from '@/next/link'
|
||||
import { newKnowledgeOverviewPath } from '../routes'
|
||||
import { KnowledgeSpaceActions } from './knowledge-space-actions'
|
||||
import { KnowledgeSpaceCardTags } from './knowledge-space-card-tags'
|
||||
import { KnowledgeSpaceIcon } from './knowledge-space-icon'
|
||||
|
||||
function getBuiltinIconName(iconRef: string | undefined) {
|
||||
@ -14,13 +15,14 @@ function getBuiltinIconName(iconRef: string | undefined) {
|
||||
|
||||
export function KnowledgeSpaceCard({
|
||||
knowledgeSpace,
|
||||
onOpenTagManagement,
|
||||
}: {
|
||||
knowledgeSpace: KnowledgeFsSpaceListItemResponse
|
||||
onOpenTagManagement: () => void
|
||||
}) {
|
||||
const { t } = useTranslation('dataset')
|
||||
const { formatTimeFromNow } = useFormatTimeFromNow()
|
||||
const linkedAppsDescriptionId = useId()
|
||||
const unavailable = t(($) => $['cornerLabel.unavailable'])
|
||||
const summary = knowledgeSpace.technical_summary
|
||||
const name = summary?.name ?? knowledgeSpace.control_space_id
|
||||
const linkedApps = knowledgeSpace.linked_apps
|
||||
@ -31,14 +33,14 @@ export function KnowledgeSpaceCard({
|
||||
: formatTimeFromNow(updatedAt)
|
||||
|
||||
return (
|
||||
<li className="group relative">
|
||||
<li className="group relative flex h-41.5 flex-col overflow-hidden rounded-xl border-[0.5px] border-components-card-border bg-components-card-bg text-left shadow-xs outline-hidden transition-shadow hover:shadow-md motion-reduce:transition-none">
|
||||
<Link
|
||||
href={newKnowledgeOverviewPath(knowledgeSpace.control_space_id)}
|
||||
aria-label={name}
|
||||
aria-describedby={linkedAppsDescriptionId}
|
||||
className="relative flex h-41.5 w-full flex-col overflow-hidden rounded-xl border-[0.5px] border-components-card-border bg-components-card-bg text-left shadow-xs outline-hidden transition-shadow hover:shadow-md focus-visible:ring-2 focus-visible:ring-state-accent-solid motion-reduce:transition-none"
|
||||
className="block outline-hidden after:absolute after:inset-0 after:z-0 after:rounded-xl after:content-[''] focus-visible:after:ring-2 focus-visible:after:ring-state-accent-solid"
|
||||
>
|
||||
<div className="flex w-full items-center gap-3 px-4 pt-4 pb-1.5">
|
||||
<div className="relative z-1 flex w-full items-center gap-3 px-4 pt-4 pb-1.5">
|
||||
<div
|
||||
aria-label={iconName ?? t(($) => $['newKnowledge.cardType'])}
|
||||
title={iconName}
|
||||
@ -53,40 +55,35 @@ export function KnowledgeSpaceCard({
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<p className="line-clamp-2 min-h-8 w-full px-4 py-0.5 body-xs-regular text-text-tertiary">
|
||||
<p className="relative z-1 line-clamp-2 min-h-8 w-full px-4 py-0.5 body-xs-regular text-text-tertiary">
|
||||
{summary?.description || t(($) => $['newKnowledge.noDescription'])}
|
||||
</p>
|
||||
<div
|
||||
aria-label={`${t(($) => $['newKnowledge.tags'])}. ${unavailable}`}
|
||||
className="mt-1 flex min-w-0 items-center gap-1 px-4"
|
||||
>
|
||||
<span className="rounded-md bg-background-section px-1.5 py-0.5 system-2xs-medium-uppercase text-text-disabled">
|
||||
{t(($) => $['newKnowledge.tags'])}
|
||||
</span>
|
||||
<span className="system-2xs-regular text-text-disabled">{unavailable}</span>
|
||||
</div>
|
||||
<div className="mt-auto flex w-full min-w-0 items-center gap-2 px-4 pt-1 pb-2.5 system-xs-regular text-text-tertiary">
|
||||
<span className="flex shrink-0 items-center gap-1 text-text-disabled">
|
||||
<span aria-hidden className="i-ri-file-text-line size-3.5" />
|
||||
<span>{summary?.document_count ?? 0}</span>
|
||||
</span>
|
||||
<span className="flex shrink-0 items-center gap-1 text-text-disabled">
|
||||
<span aria-hidden className="i-ri-robot-2-line size-3.5" />
|
||||
<span aria-hidden>{linkedApps}</span>
|
||||
<span id={linkedAppsDescriptionId} className="sr-only">
|
||||
{t(($) => $['newKnowledge.overview.linkedApps'])}: {linkedApps}
|
||||
</span>
|
||||
</span>
|
||||
<span aria-hidden className="text-divider-deep">
|
||||
/
|
||||
</span>
|
||||
<span className="ml-auto min-w-0 truncate text-right">
|
||||
{t(($) => $['newKnowledge.updated'], {
|
||||
date: formattedUpdatedAt,
|
||||
})}
|
||||
</span>
|
||||
</div>
|
||||
</Link>
|
||||
<KnowledgeSpaceCardTags
|
||||
knowledgeSpace={knowledgeSpace}
|
||||
onOpenTagManagement={onOpenTagManagement}
|
||||
/>
|
||||
<div className="pointer-events-none relative z-1 mt-auto flex w-full min-w-0 items-center gap-2 px-4 pt-1 pb-2.5 system-xs-regular text-text-tertiary">
|
||||
<span className="flex shrink-0 items-center gap-1 text-text-disabled">
|
||||
<span aria-hidden className="i-ri-file-text-line size-3.5" />
|
||||
<span>{summary?.document_count ?? 0}</span>
|
||||
</span>
|
||||
<span className="flex shrink-0 items-center gap-1 text-text-disabled">
|
||||
<span aria-hidden className="i-ri-robot-2-line size-3.5" />
|
||||
<span aria-hidden>{linkedApps}</span>
|
||||
<span id={linkedAppsDescriptionId} className="sr-only">
|
||||
{t(($) => $['newKnowledge.overview.linkedApps'])}: {linkedApps}
|
||||
</span>
|
||||
</span>
|
||||
<span aria-hidden className="text-divider-deep">
|
||||
/
|
||||
</span>
|
||||
<span className="ml-auto min-w-0 truncate text-right">
|
||||
{t(($) => $['newKnowledge.updated'], {
|
||||
date: formattedUpdatedAt,
|
||||
})}
|
||||
</span>
|
||||
</div>
|
||||
<KnowledgeSpaceActions knowledgeSpace={knowledgeSpace} />
|
||||
</li>
|
||||
)
|
||||
|
||||
@ -16,6 +16,12 @@ import {
|
||||
import { Button } from '@langgenius/dify-ui/button'
|
||||
import { Checkbox } from '@langgenius/dify-ui/checkbox'
|
||||
import { cn } from '@langgenius/dify-ui/cn'
|
||||
import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverDescription,
|
||||
PopoverTrigger,
|
||||
} from '@langgenius/dify-ui/popover'
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
@ -55,6 +61,59 @@ const statusTextClass: Record<DocumentDisplayStatus, string> = {
|
||||
disabled: 'font-medium text-text-tertiary',
|
||||
}
|
||||
|
||||
function DocumentStatus({
|
||||
failureReason,
|
||||
status,
|
||||
}: {
|
||||
failureReason?: string
|
||||
status: DocumentDisplayStatus
|
||||
}) {
|
||||
const { t } = useTranslation('dataset')
|
||||
const statusLabel = t(($) => $[`newKnowledge.documentStatus.${status}`])
|
||||
const content = (
|
||||
<>
|
||||
<span
|
||||
aria-hidden
|
||||
className={cn(status === 'processing' ? 'size-4' : 'size-3.5', statusIconClass[status])}
|
||||
/>
|
||||
{statusLabel}
|
||||
</>
|
||||
)
|
||||
const className = cn(
|
||||
'inline-flex items-center gap-1.5 text-xs leading-4',
|
||||
statusTextClass[status],
|
||||
)
|
||||
|
||||
if (status !== 'failed' || !failureReason) return <span className={className}>{content}</span>
|
||||
|
||||
return (
|
||||
<Popover>
|
||||
<PopoverTrigger
|
||||
openOnHover
|
||||
delay={300}
|
||||
closeDelay={200}
|
||||
render={
|
||||
<button
|
||||
type="button"
|
||||
aria-label={`${statusLabel}: ${failureReason}`}
|
||||
className={cn(
|
||||
className,
|
||||
'rounded-sm text-left outline-hidden focus-visible:ring-2 focus-visible:ring-state-accent-solid',
|
||||
)}
|
||||
>
|
||||
{content}
|
||||
</button>
|
||||
}
|
||||
/>
|
||||
<PopoverContent placement="top" popupClassName="max-w-80 px-3 py-2">
|
||||
<PopoverDescription className="system-xs-regular wrap-break-word whitespace-pre-wrap text-text-tertiary">
|
||||
{failureReason}
|
||||
</PopoverDescription>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
)
|
||||
}
|
||||
|
||||
function TaskTrigger({
|
||||
activeTaskCount,
|
||||
attentionTaskBadge,
|
||||
@ -113,6 +172,7 @@ const DocumentRow = memo(
|
||||
({
|
||||
document,
|
||||
documentHref,
|
||||
failureReason,
|
||||
formatTimeFromNow,
|
||||
canDownload,
|
||||
onDownload,
|
||||
@ -135,6 +195,7 @@ const DocumentRow = memo(
|
||||
canDownload: boolean
|
||||
document: LogicalDocument
|
||||
documentHref: string
|
||||
failureReason?: string
|
||||
formatTimeFromNow: (time: number) => string
|
||||
onDownload: (documentId: string) => Promise<boolean>
|
||||
onRemove: (documentId: string) => Promise<boolean>
|
||||
@ -216,21 +277,7 @@ const DocumentRow = memo(
|
||||
<span className="sr-only">{tCommon(($) => $.loading)}</span>
|
||||
</span>
|
||||
) : (
|
||||
<span
|
||||
className={cn(
|
||||
'inline-flex items-center gap-1.5 text-xs leading-4',
|
||||
statusTextClass[status],
|
||||
)}
|
||||
>
|
||||
<span
|
||||
aria-hidden
|
||||
className={cn(
|
||||
status === 'processing' ? 'size-4' : 'size-3.5',
|
||||
statusIconClass[status],
|
||||
)}
|
||||
/>
|
||||
{t(($) => $[`newKnowledge.documentStatus.${status}`])}
|
||||
</span>
|
||||
<DocumentStatus failureReason={failureReason} status={status} />
|
||||
)}
|
||||
</td>
|
||||
<td className="hidden w-43.5 pr-6 align-middle system-xs-regular text-text-tertiary lg:table-cell">
|
||||
@ -325,6 +372,7 @@ export function DocumentsList({
|
||||
canUpload,
|
||||
completingResults,
|
||||
documents,
|
||||
failureReasons,
|
||||
filter,
|
||||
getDocumentHref,
|
||||
hasNextPage,
|
||||
@ -374,6 +422,7 @@ export function DocumentsList({
|
||||
canUpload: boolean
|
||||
completingResults: boolean
|
||||
documents: LogicalDocument[]
|
||||
failureReasons: Map<string, string>
|
||||
filter: DocumentFilter
|
||||
getDocumentHref: (documentId: string) => string
|
||||
hasNextPage: boolean
|
||||
@ -562,6 +611,7 @@ export function DocumentsList({
|
||||
canDownload={canDownload}
|
||||
document={document}
|
||||
documentHref={getDocumentHref(document.id)}
|
||||
failureReason={failureReasons.get(document.id)}
|
||||
formatTimeFromNow={formatTimeFromNow}
|
||||
onDownload={onDownloadDocument}
|
||||
onRemove={onRemoveDocument}
|
||||
|
||||
@ -59,6 +59,7 @@ import {
|
||||
} from './document-models'
|
||||
import { DocumentUploadForm } from './document-upload-form'
|
||||
import { documentUploadIssue } from './document-upload-policy'
|
||||
import { knowledgeFsTaskFailureMessageKey } from './knowledge-fs-task-error'
|
||||
import {
|
||||
discardKnowledgeFsStagedUpload,
|
||||
stageKnowledgeFsDocument,
|
||||
@ -785,6 +786,29 @@ export function DocumentsPage({ knowledgeSpaceId }: { knowledgeSpaceId: string }
|
||||
),
|
||||
[documents, taskByDocument],
|
||||
)
|
||||
const documentFailureReasons = useMemo(
|
||||
() =>
|
||||
new Map(
|
||||
documents.flatMap((document) => {
|
||||
if (documentStatuses.get(document.id) !== 'failed') return []
|
||||
const task = taskByDocument.get(document.id)
|
||||
const messageKey =
|
||||
knowledgeFsTaskFailureMessageKey(
|
||||
task?.failure,
|
||||
task?.errorCode ?? (task?.errorMessage ? 'LEGACY_TASK_FAILURE' : undefined),
|
||||
) ?? 'newKnowledge.taskFailure.internal'
|
||||
const message = t(($) => $[messageKey])
|
||||
const reference =
|
||||
task?.failure?.traceId && task.failure.category === 'internal'
|
||||
? t(($) => $['newKnowledge.taskFailure.reference'], {
|
||||
traceId: task.failure.traceId,
|
||||
})
|
||||
: undefined
|
||||
return [[document.id, reference ? `${message}\n${reference}` : message] as const]
|
||||
}),
|
||||
),
|
||||
[documentStatuses, documents, t, taskByDocument],
|
||||
)
|
||||
const filterActive = filter !== 'all' || Boolean(search.trim())
|
||||
const statusFilterActive = filter !== 'all'
|
||||
const availableDocumentIds = useMemo(
|
||||
@ -2849,6 +2873,7 @@ export function DocumentsPage({ knowledgeSpaceId }: { knowledgeSpaceId: string }
|
||||
canUpload={canUpload}
|
||||
completingResults={completingFilteredResults}
|
||||
documents={filteredDocuments}
|
||||
failureReasons={documentFailureReasons}
|
||||
filter={filter}
|
||||
getDocumentHref={(documentId) =>
|
||||
newKnowledgeDocumentDetailPath(knowledgeSpaceId, documentId)
|
||||
|
||||
@ -853,32 +853,25 @@ export function KnowledgeSettingsForm({
|
||||
</div>
|
||||
)}
|
||||
|
||||
{(settings.configuration_state !== 'active' || settings.issues.length > 0) && (
|
||||
<KnowledgeModelReadinessNotice
|
||||
className="mb-3"
|
||||
description={
|
||||
settings.issues.length > 0
|
||||
? settings.issues.map(({ field }) => readinessFieldLabel(field)).join(' · ')
|
||||
: settings.active_profile_available
|
||||
? t(($) => $['newKnowledge.overview.attention.modelReadiness.description'])
|
||||
: undefined
|
||||
}
|
||||
title={
|
||||
settings.configuration_state === 'pending-validation'
|
||||
? tCommon(($) => $['provider.validating'])
|
||||
: settings.configuration_state === 'validation-failed'
|
||||
{settings.configuration_state !== 'pending-validation' &&
|
||||
(settings.configuration_state !== 'active' || settings.issues.length > 0) && (
|
||||
<KnowledgeModelReadinessNotice
|
||||
className="mb-3"
|
||||
description={
|
||||
settings.issues.length > 0
|
||||
? settings.issues.map(({ field }) => readinessFieldLabel(field)).join(' · ')
|
||||
: settings.active_profile_available
|
||||
? t(($) => $['newKnowledge.overview.attention.modelReadiness.description'])
|
||||
: undefined
|
||||
}
|
||||
title={
|
||||
settings.configuration_state === 'validation-failed'
|
||||
? tCommon(($) => $['api.actionFailed'])
|
||||
: tCommon(($) => $['modelProvider.toBeConfigured'])
|
||||
}
|
||||
tone={
|
||||
settings.configuration_state === 'pending-validation'
|
||||
? 'progress'
|
||||
: settings.configuration_state === 'validation-failed'
|
||||
? 'destructive'
|
||||
: 'warning'
|
||||
}
|
||||
/>
|
||||
)}
|
||||
}
|
||||
tone={settings.configuration_state === 'validation-failed' ? 'destructive' : 'warning'}
|
||||
/>
|
||||
)}
|
||||
|
||||
{saveErrorSlice && (
|
||||
<div
|
||||
|
||||
@ -1,8 +1,7 @@
|
||||
'use client'
|
||||
|
||||
import { Button } from '@langgenius/dify-ui/button'
|
||||
import { toast } from '@langgenius/dify-ui/toast'
|
||||
import { useInfiniteQuery } from '@tanstack/react-query'
|
||||
import { useInfiniteQuery, useQueryClient } from '@tanstack/react-query'
|
||||
import { useDebounce } from 'ahooks'
|
||||
import { useAtomValue } from 'jotai'
|
||||
import { createParser, parseAsString, useQueryState } from 'nuqs'
|
||||
@ -13,6 +12,8 @@ import ExternalAPIPanel from '@/app/components/datasets/external-api/external-ap
|
||||
import ServiceApi from '@/app/components/datasets/extra-info/service-api'
|
||||
import { workspacePermissionKeysAtom } from '@/context/permission-state'
|
||||
import { knowledgeFsUploadEnabledAtom } from '@/features/system-features/state'
|
||||
import { TagFilter } from '@/features/tag-management/components/tag-filter'
|
||||
import { TagManagementModal } from '@/features/tag-management/components/tag-management-modal'
|
||||
import Link from '@/next/link'
|
||||
import { consoleQuery } from '@/service/client'
|
||||
import { useDatasetApiBaseUrl } from '@/service/knowledge/use-dataset'
|
||||
@ -32,6 +33,8 @@ import {
|
||||
} from './components/new-knowledge-list-states'
|
||||
|
||||
const PAGE_SIZE = 30
|
||||
const TAG_FILTER_MAX_ID_LENGTH = 255
|
||||
const TAG_FILTER_MAX_SELECTION = 100
|
||||
const searchParser = parseAsString.withDefault('').withOptions({
|
||||
history: 'replace',
|
||||
})
|
||||
@ -51,6 +54,21 @@ const creatorIdsParser = createParser<string[]>({
|
||||
.withDefault([])
|
||||
.withOptions({ history: 'push' })
|
||||
|
||||
function normalizeTagIds(tagIds: string[]) {
|
||||
return [...new Set(tagIds)]
|
||||
.filter((tagId) => tagId.length > 0 && tagId.length <= TAG_FILTER_MAX_ID_LENGTH)
|
||||
.slice(0, TAG_FILTER_MAX_SELECTION)
|
||||
}
|
||||
|
||||
const tagIdsParser = createParser<string[]>({
|
||||
eq: (left, right) =>
|
||||
left.length === right.length && left.every((tagId, index) => tagId === right[index]),
|
||||
parse: (query) => normalizeTagIds(query.split(';')),
|
||||
serialize: (tagIds) => normalizeTagIds(tagIds).join(';'),
|
||||
})
|
||||
.withDefault([])
|
||||
.withOptions({ history: 'push' })
|
||||
|
||||
function isUnavailableError(error: unknown) {
|
||||
if (!error || typeof error !== 'object') return false
|
||||
const status = 'status' in error ? error.status : undefined
|
||||
@ -62,19 +80,6 @@ function isUnavailableError(error: unknown) {
|
||||
return dataStatus === 404 || dataStatus === 503
|
||||
}
|
||||
|
||||
function MetadataFilter({ label, onClick }: { label: string; onClick: () => void }) {
|
||||
return (
|
||||
<Button
|
||||
variant="tertiary"
|
||||
className="gap-0 border-0 bg-components-input-bg-normal px-2 font-normal text-text-tertiary shadow-none hover:bg-components-input-bg-hover"
|
||||
onClick={onClick}
|
||||
>
|
||||
<span className="px-1 system-sm-regular">{label}</span>
|
||||
<span aria-hidden className="i-ri-arrow-down-s-line size-4" />
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
|
||||
export function NewKnowledgeList({
|
||||
view,
|
||||
onViewChange,
|
||||
@ -84,18 +89,19 @@ export function NewKnowledgeList({
|
||||
}) {
|
||||
const { t } = useTranslation('dataset')
|
||||
const { t: tCommon } = useTranslation('common')
|
||||
const queryClient = useQueryClient()
|
||||
const { data: apiBaseInfo } = useDatasetApiBaseUrl()
|
||||
const [showExternalApiPanel, setShowExternalApiPanel] = useState(false)
|
||||
const [showTagManagementModal, setShowTagManagementModal] = useState(false)
|
||||
const workspacePermissionKeys = useAtomValue(workspacePermissionKeysAtom)
|
||||
const uploadAvailable = useAtomValue(knowledgeFsUploadEnabledAtom)
|
||||
const canCreate = hasPermission(workspacePermissionKeys, 'dataset.create_and_management')
|
||||
const canConnect = hasPermission(workspacePermissionKeys, 'dataset.external.connect')
|
||||
const filtersUnavailable = t(($) => $['newKnowledge.filtersUnavailable'])
|
||||
const showFilterBoundary = () => toast.info(filtersUnavailable)
|
||||
const createLabel = tCommon(($) => $['operation.create'])
|
||||
const [searchValue, setSearchValue] = useQueryState('query', searchParser)
|
||||
const debouncedSearchValue = useDebounce(searchValue.trim(), { wait: 300 })
|
||||
const [creatorIds, setCreatorIds] = useQueryState('creator_ids', creatorIdsParser)
|
||||
const [tagIds, setTagIds] = useQueryState('tag_ids', tagIdsParser)
|
||||
const knowledgeSpacesQuery = useInfiniteQuery(
|
||||
consoleQuery.knowledgeFs.spaces.get.infiniteOptions({
|
||||
input: (pageParam) => ({
|
||||
@ -103,6 +109,7 @@ export function NewKnowledgeList({
|
||||
limit: PAGE_SIZE,
|
||||
page: pageParam,
|
||||
...(creatorIds.length > 0 ? { creator_ids: creatorIds } : {}),
|
||||
...(tagIds.length > 0 ? { tag_ids: tagIds } : {}),
|
||||
...(debouncedSearchValue ? { query: debouncedSearchValue } : {}),
|
||||
},
|
||||
}),
|
||||
@ -145,7 +152,12 @@ export function NewKnowledgeList({
|
||||
</div>
|
||||
<div className="flex flex-wrap items-center justify-between gap-2">
|
||||
<div className="flex min-w-0 flex-1 flex-wrap items-center gap-2">
|
||||
<MetadataFilter label={t(($) => $['newKnowledge.tags'])} onClick={showFilterBoundary} />
|
||||
<TagFilter
|
||||
type="knowledge"
|
||||
value={tagIds}
|
||||
onChange={(nextTagIds) => void setTagIds(nextTagIds)}
|
||||
onOpenTagManagement={() => setShowTagManagementModal(true)}
|
||||
/>
|
||||
<CreatorFilter
|
||||
value={creatorIds}
|
||||
onChange={(nextCreatorIds) => void setCreatorIds(nextCreatorIds)}
|
||||
@ -209,7 +221,7 @@ export function NewKnowledgeList({
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
) : knowledgeSpaces.length === 0 && creatorIds.length === 0 ? (
|
||||
) : knowledgeSpaces.length === 0 && creatorIds.length === 0 && tagIds.length === 0 ? (
|
||||
<NewKnowledgeEmptyState
|
||||
canConnect={canConnect}
|
||||
canCreate={canCreate}
|
||||
@ -218,7 +230,7 @@ export function NewKnowledgeList({
|
||||
) : knowledgeSpaces.length === 0 ? (
|
||||
<div className="flex min-h-105 items-center justify-center px-6 text-center text-text-tertiary">
|
||||
{tCommon(($) => $['operation.noSearchResults'], {
|
||||
content: t(($) => $['newKnowledge.creators']),
|
||||
content: t(($) => $.knowledge),
|
||||
})}
|
||||
</div>
|
||||
) : (
|
||||
@ -228,6 +240,7 @@ export function NewKnowledgeList({
|
||||
<KnowledgeSpaceCard
|
||||
key={knowledgeSpace.control_space_id}
|
||||
knowledgeSpace={knowledgeSpace}
|
||||
onOpenTagManagement={() => setShowTagManagementModal(true)}
|
||||
/>
|
||||
))}
|
||||
</ul>
|
||||
@ -251,6 +264,16 @@ export function NewKnowledgeList({
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<TagManagementModal
|
||||
type="knowledge"
|
||||
show={showTagManagementModal}
|
||||
onClose={() => setShowTagManagementModal(false)}
|
||||
onTagsChange={() => {
|
||||
void queryClient.invalidateQueries({
|
||||
queryKey: consoleQuery.knowledgeFs.spaces.get.key(),
|
||||
})
|
||||
}}
|
||||
/>
|
||||
{showExternalApiPanel && canConnect && (
|
||||
<ExternalAPIPanel
|
||||
canManageExternalKnowledgeApi={canConnect}
|
||||
|
||||
@ -139,6 +139,7 @@ export const TagFilter = ({
|
||||
type={type}
|
||||
inputValue={inputValue}
|
||||
onInputValueChange={setInputValue}
|
||||
canBindOrUnbindTags
|
||||
onOpenTagManagement={onOpenTagManagement}
|
||||
onClose={() => setOpen(false)}
|
||||
/>
|
||||
|
||||
@ -26,6 +26,7 @@ type TagSearchContentProps = {
|
||||
onOpenTagManagement?: () => void
|
||||
onClose?: () => void
|
||||
canBindOrUnbindTags?: boolean
|
||||
requiresTargetEditPermission?: boolean
|
||||
}
|
||||
|
||||
export const TagSearchContent = ({
|
||||
@ -35,10 +36,14 @@ export const TagSearchContent = ({
|
||||
onOpenTagManagement,
|
||||
onClose,
|
||||
canBindOrUnbindTags = false,
|
||||
requiresTargetEditPermission = false,
|
||||
}: TagSearchContentProps) => {
|
||||
const { t } = useTranslation()
|
||||
const workspacePermissionKeys = useAtomValue(workspacePermissionKeysAtom)
|
||||
const canManageTags = hasPermission(workspacePermissionKeys, getTagManagePermissionKey(type))
|
||||
const canChangeBindings = requiresTargetEditPermission
|
||||
? canBindOrUnbindTags
|
||||
: canBindOrUnbindTags || canManageTags
|
||||
const filteredItems = useComboboxFilteredItems<TagComboboxItem>()
|
||||
const realItemCount = filteredItems.filter((tag) => !isCreateTagOption(tag)).length
|
||||
const placeholder = t(($) => $['tag.selectorPlaceholder'], { ns: 'common' }) || ''
|
||||
@ -93,11 +98,7 @@ export const TagSearchContent = ({
|
||||
}
|
||||
|
||||
return (
|
||||
<ComboboxItem
|
||||
key={tag.id}
|
||||
value={tag}
|
||||
disabled={!canBindOrUnbindTags && !canManageTags}
|
||||
>
|
||||
<ComboboxItem key={tag.id} value={tag} disabled={!canChangeBindings}>
|
||||
<ComboboxItemText title={tag.name}>{tag.name}</ComboboxItemText>
|
||||
<ComboboxItemIndicator />
|
||||
</ComboboxItem>
|
||||
|
||||
@ -64,8 +64,11 @@ export type TagSelectorProps = TagSelectorRootProps &
|
||||
type: TagType
|
||||
value: Tag[]
|
||||
canBindOrUnbindTags?: boolean
|
||||
requiresTargetEditPermission?: boolean
|
||||
showProvidedTagNames?: boolean
|
||||
onOpenTagManagement?: () => void
|
||||
onTagsChange?: () => void
|
||||
onApplyTags?: (tagIds: string[]) => void
|
||||
}
|
||||
|
||||
export const TagSelector = ({
|
||||
@ -73,10 +76,13 @@ export const TagSelector = ({
|
||||
type,
|
||||
value,
|
||||
canBindOrUnbindTags,
|
||||
requiresTargetEditPermission = false,
|
||||
showProvidedTagNames = false,
|
||||
className,
|
||||
onClick,
|
||||
onOpenTagManagement = () => {},
|
||||
onTagsChange,
|
||||
onApplyTags,
|
||||
placement = 'bottom-start',
|
||||
sideOffset = 4,
|
||||
alignOffset = 0,
|
||||
@ -92,6 +98,9 @@ export const TagSelector = ({
|
||||
const [inputValue, setInputValue] = useState('')
|
||||
const workspacePermissionKeys = useAtomValue(workspacePermissionKeysAtom)
|
||||
const canManageTags = hasPermission(workspacePermissionKeys, getTagManagePermissionKey(type))
|
||||
const canChangeBindings = requiresTargetEditPermission
|
||||
? !!canBindOrUnbindTags
|
||||
: !!canBindOrUnbindTags || canManageTags
|
||||
|
||||
const applyTagBindingsMutation = useApplyTagBindingsMutation()
|
||||
const { isPending: isCreatingTag, mutate: createTag } = useMutation(
|
||||
@ -113,10 +122,10 @@ export const TagSelector = ({
|
||||
|
||||
const tagNameById = new Map(tagList.map((tag) => [tag.id, tag.name]))
|
||||
return value.flatMap((tag) => {
|
||||
const tagName = tagNameById.get(tag.id)
|
||||
const tagName = tagNameById.get(tag.id) ?? (showProvidedTagNames ? tag.name : undefined)
|
||||
return tagName ? [tagName] : []
|
||||
})
|
||||
}, [tagList, value])
|
||||
}, [showProvidedTagNames, tagList, value])
|
||||
const emptyTriggerLabel = canBindOrUnbindTags
|
||||
? t(($) => $['tag.addTag'], { ns: 'common' })
|
||||
: t(($) => $['tag.noTag'], { ns: 'common' })
|
||||
@ -159,6 +168,11 @@ export const TagSelector = ({
|
||||
|
||||
if (!tagSelectionChanged) return
|
||||
|
||||
if (onApplyTags) {
|
||||
onApplyTags(draftTagIds)
|
||||
return
|
||||
}
|
||||
|
||||
const toastId = `tag-bindings-${type}-${targetId}`
|
||||
|
||||
applyTagBindingsMutation.mutate(
|
||||
@ -190,7 +204,16 @@ export const TagSelector = ({
|
||||
},
|
||||
},
|
||||
)
|
||||
}, [applyTagBindingsMutation, draftTags, onTagsChange, selectedTagIds, t, targetId, type])
|
||||
}, [
|
||||
applyTagBindingsMutation,
|
||||
draftTags,
|
||||
onApplyTags,
|
||||
onTagsChange,
|
||||
selectedTagIds,
|
||||
t,
|
||||
targetId,
|
||||
type,
|
||||
])
|
||||
|
||||
const handleOpenChange = useCallback(
|
||||
(nextOpen: boolean) => {
|
||||
@ -259,7 +282,7 @@ export const TagSelector = ({
|
||||
isItemEqualToValue={isSameTag}
|
||||
>
|
||||
<ComboboxTrigger
|
||||
disabled={!canManageTags && !canBindOrUnbindTags}
|
||||
disabled={!canChangeBindings}
|
||||
aria-label={triggerLabel}
|
||||
className={cn(
|
||||
'group/tag-area relative h-auto w-full cursor-pointer rounded-lg border-0 bg-transparent p-1 hover:bg-state-base-hover focus-visible:bg-transparent data-disabled:bg-transparent data-disabled:opacity-50 data-disabled:hover:bg-transparent data-popup-open:bg-state-base-hover data-popup-open:hover:bg-state-base-hover',
|
||||
@ -291,6 +314,7 @@ export const TagSelector = ({
|
||||
inputValue={inputValue}
|
||||
onInputValueChange={setInputValue}
|
||||
canBindOrUnbindTags={canBindOrUnbindTags}
|
||||
requiresTargetEditPermission={requiresTargetEditPermission}
|
||||
onOpenTagManagement={onOpenTagManagement}
|
||||
onClose={() => handleOpenChange(false)}
|
||||
/>
|
||||
|
||||
@ -351,6 +351,8 @@
|
||||
"newKnowledge.overview.attention.lowQualityQuery.title": "جودة الاستعلام تحتاج إلى مراجعة",
|
||||
"newKnowledge.overview.attention.modelReadiness.bindingMissing": "الفهرس المنشور غير مرتبط بتهيئة النموذج النشطة. أعد إنشاء الفهرس أو نشره.",
|
||||
"newKnowledge.overview.attention.modelReadiness.description": "ملفات تعريف النماذج المطلوبة مفقودة أو غير مرتبطة بالفهرس المنشور حاليًا.",
|
||||
"newKnowledge.overview.attention.modelReadiness.pendingDescription": "سيتم التحقق من النماذج المحددة عند معالجة المستند الأول.",
|
||||
"newKnowledge.overview.attention.modelReadiness.pendingTitle": "تكوين النموذج في انتظار التحقق",
|
||||
"newKnowledge.overview.attention.modelReadiness.profilesMissing": "هيّئ نماذج التضمين والاستدلال وإعادة الترتيب المطلوبة وانشرها.",
|
||||
"newKnowledge.overview.attention.modelReadiness.title": "تهيئة النموذج غير مكتملة",
|
||||
"newKnowledge.overview.attention.staleSource.description": "لم تتم مزامنة هذا المصدر بنجاح من قبل، أو مضى أكثر من 7 أيام على آخر مزامنة ناجحة.",
|
||||
|
||||
@ -351,6 +351,8 @@
|
||||
"newKnowledge.overview.attention.lowQualityQuery.title": "Abfragequalität muss geprüft werden",
|
||||
"newKnowledge.overview.attention.modelReadiness.bindingMissing": "Der veröffentlichte Index ist nicht mit der aktiven Modellkonfiguration verknüpft. Erstellen oder veröffentlichen Sie den Index erneut.",
|
||||
"newKnowledge.overview.attention.modelReadiness.description": "Erforderliche Modellprofile fehlen oder sind nicht mit dem aktuell veröffentlichten Index verknüpft.",
|
||||
"newKnowledge.overview.attention.modelReadiness.pendingDescription": "Die ausgewählten Modelle werden beim Verarbeiten des ersten Dokuments validiert.",
|
||||
"newKnowledge.overview.attention.modelReadiness.pendingTitle": "Die Modellkonfiguration wartet auf die Validierung",
|
||||
"newKnowledge.overview.attention.modelReadiness.profilesMissing": "Konfigurieren und veröffentlichen Sie die erforderlichen Embedding-, Reasoning- und Rerank-Modelle.",
|
||||
"newKnowledge.overview.attention.modelReadiness.title": "Modellkonfiguration ist unvollständig",
|
||||
"newKnowledge.overview.attention.staleSource.description": "Diese Quelle wurde noch nie erfolgreich synchronisiert oder die letzte erfolgreiche Synchronisierung liegt mehr als 7 Tage zurück.",
|
||||
|
||||
@ -351,6 +351,8 @@
|
||||
"newKnowledge.overview.attention.lowQualityQuery.title": "Query quality needs review",
|
||||
"newKnowledge.overview.attention.modelReadiness.bindingMissing": "The published index is not bound to the active model configuration; rebuild or republish the index.",
|
||||
"newKnowledge.overview.attention.modelReadiness.description": "Required model profiles are missing or are not bound to the current published index.",
|
||||
"newKnowledge.overview.attention.modelReadiness.pendingDescription": "The selected models will be validated when the first document is processed.",
|
||||
"newKnowledge.overview.attention.modelReadiness.pendingTitle": "Model configuration is awaiting validation",
|
||||
"newKnowledge.overview.attention.modelReadiness.profilesMissing": "Configure and publish the required embedding, reasoning, and rerank models.",
|
||||
"newKnowledge.overview.attention.modelReadiness.title": "Model configuration is incomplete",
|
||||
"newKnowledge.overview.attention.staleSource.description": "This source has never synced successfully or its last successful sync was more than 7 days ago.",
|
||||
|
||||
@ -351,6 +351,8 @@
|
||||
"newKnowledge.overview.attention.lowQualityQuery.title": "La calidad de la consulta requiere revisión",
|
||||
"newKnowledge.overview.attention.modelReadiness.bindingMissing": "El índice publicado no está vinculado a la configuración de modelos activa. Vuelve a generar o publicar el índice.",
|
||||
"newKnowledge.overview.attention.modelReadiness.description": "Faltan perfiles de modelo obligatorios o no están vinculados al índice publicado actual.",
|
||||
"newKnowledge.overview.attention.modelReadiness.pendingDescription": "Los modelos seleccionados se validarán al procesar el primer documento.",
|
||||
"newKnowledge.overview.attention.modelReadiness.pendingTitle": "La configuración del modelo está pendiente de validación",
|
||||
"newKnowledge.overview.attention.modelReadiness.profilesMissing": "Configura y publica los modelos obligatorios de embedding, razonamiento y rerank.",
|
||||
"newKnowledge.overview.attention.modelReadiness.title": "La configuración de modelos está incompleta",
|
||||
"newKnowledge.overview.attention.staleSource.description": "Esta fuente nunca se ha sincronizado correctamente o su última sincronización correcta fue hace más de 7 días.",
|
||||
|
||||
@ -351,6 +351,8 @@
|
||||
"newKnowledge.overview.attention.lowQualityQuery.title": "کیفیت پرسوجو نیاز به بررسی دارد",
|
||||
"newKnowledge.overview.attention.modelReadiness.bindingMissing": "نمایه منتشرشده به پیکربندی فعال مدل متصل نیست. نمایه را دوباره بسازید یا منتشر کنید.",
|
||||
"newKnowledge.overview.attention.modelReadiness.description": "پروفایلهای مدل ضروری وجود ندارند یا به نمایه منتشرشده فعلی متصل نیستند.",
|
||||
"newKnowledge.overview.attention.modelReadiness.pendingDescription": "مدلهای انتخابشده هنگام پردازش اولین سند اعتبارسنجی میشوند.",
|
||||
"newKnowledge.overview.attention.modelReadiness.pendingTitle": "پیکربندی مدل در انتظار اعتبارسنجی است",
|
||||
"newKnowledge.overview.attention.modelReadiness.profilesMissing": "مدلهای ضروری تعبیهسازی، استدلال و بازرتبهبندی را پیکربندی و منتشر کنید.",
|
||||
"newKnowledge.overview.attention.modelReadiness.title": "پیکربندی مدل ناقص است",
|
||||
"newKnowledge.overview.attention.staleSource.description": "این منبع هرگز با موفقیت همگامسازی نشده یا بیش از ۷ روز از آخرین همگامسازی موفق آن گذشته است.",
|
||||
|
||||
@ -351,6 +351,8 @@
|
||||
"newKnowledge.overview.attention.lowQualityQuery.title": "La qualité de la requête doit être vérifiée",
|
||||
"newKnowledge.overview.attention.modelReadiness.bindingMissing": "L’index publié n’est pas lié à la configuration de modèles active. Reconstruisez ou republiez l’index.",
|
||||
"newKnowledge.overview.attention.modelReadiness.description": "Des profils de modèles requis sont absents ou ne sont pas liés à l’index actuellement publié.",
|
||||
"newKnowledge.overview.attention.modelReadiness.pendingDescription": "Les modèles sélectionnés seront validés lors du traitement du premier document.",
|
||||
"newKnowledge.overview.attention.modelReadiness.pendingTitle": "La configuration des modèles est en attente de validation",
|
||||
"newKnowledge.overview.attention.modelReadiness.profilesMissing": "Configurez et publiez les modèles d’embedding, de raisonnement et de rerank requis.",
|
||||
"newKnowledge.overview.attention.modelReadiness.title": "La configuration des modèles est incomplète",
|
||||
"newKnowledge.overview.attention.staleSource.description": "Cette source n’a jamais été synchronisée avec succès ou sa dernière synchronisation réussie date de plus de 7 jours.",
|
||||
|
||||
@ -351,6 +351,8 @@
|
||||
"newKnowledge.overview.attention.lowQualityQuery.title": "क्वेरी की गुणवत्ता की समीक्षा आवश्यक है",
|
||||
"newKnowledge.overview.attention.modelReadiness.bindingMissing": "प्रकाशित इंडेक्स सक्रिय मॉडल कॉन्फ़िगरेशन से जुड़ा नहीं है। इंडेक्स को फिर से बनाएँ या प्रकाशित करें।",
|
||||
"newKnowledge.overview.attention.modelReadiness.description": "आवश्यक मॉडल प्रोफ़ाइल मौजूद नहीं हैं या वर्तमान प्रकाशित इंडेक्स से जुड़ी नहीं हैं।",
|
||||
"newKnowledge.overview.attention.modelReadiness.pendingDescription": "पहले दस्तावेज़ के प्रोसेस होने पर चुने गए मॉडल सत्यापित किए जाएंगे।",
|
||||
"newKnowledge.overview.attention.modelReadiness.pendingTitle": "मॉडल कॉन्फ़िगरेशन सत्यापन की प्रतीक्षा में है",
|
||||
"newKnowledge.overview.attention.modelReadiness.profilesMissing": "आवश्यक एम्बेडिंग, रीजनिंग और रीरैंक मॉडल कॉन्फ़िगर और प्रकाशित करें।",
|
||||
"newKnowledge.overview.attention.modelReadiness.title": "मॉडल कॉन्फ़िगरेशन अधूरा है",
|
||||
"newKnowledge.overview.attention.staleSource.description": "यह स्रोत कभी सफलतापूर्वक सिंक नहीं हुआ या पिछला सफल सिंक 7 दिन से अधिक पहले हुआ था।",
|
||||
|
||||
@ -351,6 +351,8 @@
|
||||
"newKnowledge.overview.attention.lowQualityQuery.title": "Kualitas kueri perlu ditinjau",
|
||||
"newKnowledge.overview.attention.modelReadiness.bindingMissing": "Indeks yang diterbitkan tidak terhubung ke konfigurasi model aktif. Bangun atau terbitkan ulang indeks.",
|
||||
"newKnowledge.overview.attention.modelReadiness.description": "Profil model wajib tidak tersedia atau tidak terhubung ke indeks yang saat ini diterbitkan.",
|
||||
"newKnowledge.overview.attention.modelReadiness.pendingDescription": "Model yang dipilih akan divalidasi saat dokumen pertama diproses.",
|
||||
"newKnowledge.overview.attention.modelReadiness.pendingTitle": "Konfigurasi model menunggu validasi",
|
||||
"newKnowledge.overview.attention.modelReadiness.profilesMissing": "Konfigurasikan dan terbitkan model embedding, penalaran, dan rerank yang wajib.",
|
||||
"newKnowledge.overview.attention.modelReadiness.title": "Konfigurasi model belum lengkap",
|
||||
"newKnowledge.overview.attention.staleSource.description": "Sumber ini belum pernah berhasil disinkronkan atau sinkronisasi terakhir yang berhasil terjadi lebih dari 7 hari lalu.",
|
||||
|
||||
@ -351,6 +351,8 @@
|
||||
"newKnowledge.overview.attention.lowQualityQuery.title": "La qualità della query richiede una verifica",
|
||||
"newKnowledge.overview.attention.modelReadiness.bindingMissing": "L’indice pubblicato non è associato alla configurazione dei modelli attiva. Ricrea o ripubblica l’indice.",
|
||||
"newKnowledge.overview.attention.modelReadiness.description": "Mancano profili di modello obbligatori oppure non sono associati all’indice attualmente pubblicato.",
|
||||
"newKnowledge.overview.attention.modelReadiness.pendingDescription": "I modelli selezionati verranno convalidati durante l’elaborazione del primo documento.",
|
||||
"newKnowledge.overview.attention.modelReadiness.pendingTitle": "La configurazione dei modelli è in attesa di convalida",
|
||||
"newKnowledge.overview.attention.modelReadiness.profilesMissing": "Configura e pubblica i modelli di embedding, ragionamento e rerank obbligatori.",
|
||||
"newKnowledge.overview.attention.modelReadiness.title": "La configurazione dei modelli è incompleta",
|
||||
"newKnowledge.overview.attention.staleSource.description": "Questa fonte non è mai stata sincronizzata correttamente oppure l’ultima sincronizzazione riuscita risale a più di 7 giorni fa.",
|
||||
|
||||
@ -351,6 +351,8 @@
|
||||
"newKnowledge.overview.attention.lowQualityQuery.title": "クエリ品質の確認が必要です",
|
||||
"newKnowledge.overview.attention.modelReadiness.bindingMissing": "公開済みインデックスが現在のモデル設定に関連付けられていません。インデックスを再構築または再公開してください。",
|
||||
"newKnowledge.overview.attention.modelReadiness.description": "必須のモデルプロファイルが不足しているか、現在の公開済みインデックスに関連付けられていません。",
|
||||
"newKnowledge.overview.attention.modelReadiness.pendingDescription": "選択したモデルは、最初のドキュメントを処理するときに検証されます。",
|
||||
"newKnowledge.overview.attention.modelReadiness.pendingTitle": "モデル設定は検証待ちです",
|
||||
"newKnowledge.overview.attention.modelReadiness.profilesMissing": "必須の Embedding、推論、Rerank モデルを設定して公開してください。",
|
||||
"newKnowledge.overview.attention.modelReadiness.title": "モデル設定が未完了です",
|
||||
"newKnowledge.overview.attention.staleSource.description": "このソースは一度も正常に同期されていないか、最後の正常な同期から 7 日以上経過しています。",
|
||||
|
||||
@ -351,6 +351,8 @@
|
||||
"newKnowledge.overview.attention.lowQualityQuery.title": "쿼리 품질 검토 필요",
|
||||
"newKnowledge.overview.attention.modelReadiness.bindingMissing": "게시된 인덱스가 활성 모델 구성에 연결되어 있지 않습니다. 인덱스를 다시 빌드하거나 게시하세요.",
|
||||
"newKnowledge.overview.attention.modelReadiness.description": "필수 모델 프로필이 없거나 현재 게시된 인덱스에 연결되어 있지 않습니다.",
|
||||
"newKnowledge.overview.attention.modelReadiness.pendingDescription": "선택한 모델은 첫 번째 문서를 처리할 때 검증됩니다.",
|
||||
"newKnowledge.overview.attention.modelReadiness.pendingTitle": "모델 구성이 검증을 기다리고 있습니다",
|
||||
"newKnowledge.overview.attention.modelReadiness.profilesMissing": "필수 임베딩, 추론 및 리랭크 모델을 구성하고 게시하세요.",
|
||||
"newKnowledge.overview.attention.modelReadiness.title": "모델 구성이 완료되지 않음",
|
||||
"newKnowledge.overview.attention.staleSource.description": "이 소스는 동기화에 성공한 적이 없거나 마지막 성공 이후 7일이 지났습니다.",
|
||||
|
||||
@ -330,6 +330,8 @@
|
||||
"newKnowledge.overview.attention.lowQualityQuery.title": "ຄຸນນະພາບຄຳຖາມຕ້ອງການການກວດສອບ",
|
||||
"newKnowledge.overview.attention.modelReadiness.bindingMissing": "ດັດຊະນີທີ່ເຜີຍແຜ່ຍັງບໍ່ໄດ້ເຊື່ອມກັບການຕັ້ງຄ່າໂມເດວທີ່ໃຊ້ງານ. ສ້າງ ຫຼືເຜີຍແຜ່ດັດຊະນີອີກຄັ້ງ.",
|
||||
"newKnowledge.overview.attention.modelReadiness.description": "ໂປຣໄຟລ໌ໂມເດວທີ່ຈຳເປັນຂາດຫາຍ ຫຼືຍັງບໍ່ໄດ້ເຊື່ອມກັບດັດຊະນີທີ່ເຜີຍແຜ່ປັດຈຸບັນ.",
|
||||
"newKnowledge.overview.attention.modelReadiness.pendingDescription": "ໂມເດວທີ່ເລືອກຈະຖືກກວດສອບເມື່ອປະມວນຜົນເອກະສານທຳອິດ.",
|
||||
"newKnowledge.overview.attention.modelReadiness.pendingTitle": "ການກຳນົດຄ່າໂມເດວກຳລັງລໍຖ້າການກວດສອບ",
|
||||
"newKnowledge.overview.attention.modelReadiness.profilesMissing": "ຕັ້ງຄ່າ ແລະເຜີຍແຜ່ໂມເດວ embedding, reasoning ແລະ rerank ທີ່ຈຳເປັນ.",
|
||||
"newKnowledge.overview.attention.modelReadiness.title": "ການຕັ້ງຄ່າໂມເດວຍັງບໍ່ສຳເລັດ",
|
||||
"newKnowledge.overview.attention.staleSource.description": "ແຫຼ່ງຂໍ້ມູນນີ້ບໍ່ເຄີຍຊິງຄ໌ສຳເລັດ ຫຼືການຊິງຄ໌ສຳເລັດລ່າສຸດຜ່ານມາຫຼາຍກວ່າ 7 ມື້.",
|
||||
|
||||
@ -351,6 +351,8 @@
|
||||
"newKnowledge.overview.attention.lowQualityQuery.title": "Querykwaliteit moet worden gecontroleerd",
|
||||
"newKnowledge.overview.attention.modelReadiness.bindingMissing": "De gepubliceerde index is niet gekoppeld aan de actieve modelconfiguratie. Bouw of publiceer de index opnieuw.",
|
||||
"newKnowledge.overview.attention.modelReadiness.description": "Vereiste modelprofielen ontbreken of zijn niet gekoppeld aan de huidige gepubliceerde index.",
|
||||
"newKnowledge.overview.attention.modelReadiness.pendingDescription": "De geselecteerde modellen worden gevalideerd wanneer het eerste document wordt verwerkt.",
|
||||
"newKnowledge.overview.attention.modelReadiness.pendingTitle": "De modelconfiguratie wacht op validatie",
|
||||
"newKnowledge.overview.attention.modelReadiness.profilesMissing": "Configureer en publiceer de vereiste embedding-, redeneer- en rerankmodellen.",
|
||||
"newKnowledge.overview.attention.modelReadiness.title": "Modelconfiguratie is onvolledig",
|
||||
"newKnowledge.overview.attention.staleSource.description": "Deze bron is nooit met succes gesynchroniseerd of de laatste succesvolle synchronisatie was meer dan 7 dagen geleden.",
|
||||
|
||||
@ -351,6 +351,8 @@
|
||||
"newKnowledge.overview.attention.lowQualityQuery.title": "Jakość zapytania wymaga sprawdzenia",
|
||||
"newKnowledge.overview.attention.modelReadiness.bindingMissing": "Opublikowany indeks nie jest powiązany z aktywną konfiguracją modeli. Ponownie utwórz lub opublikuj indeks.",
|
||||
"newKnowledge.overview.attention.modelReadiness.description": "Brakuje wymaganych profili modeli lub nie są one powiązane z obecnie opublikowanym indeksem.",
|
||||
"newKnowledge.overview.attention.modelReadiness.pendingDescription": "Wybrane modele zostaną zweryfikowane podczas przetwarzania pierwszego dokumentu.",
|
||||
"newKnowledge.overview.attention.modelReadiness.pendingTitle": "Konfiguracja modeli oczekuje na weryfikację",
|
||||
"newKnowledge.overview.attention.modelReadiness.profilesMissing": "Skonfiguruj i opublikuj wymagane modele embeddingu, wnioskowania i rerankingu.",
|
||||
"newKnowledge.overview.attention.modelReadiness.title": "Konfiguracja modeli jest niepełna",
|
||||
"newKnowledge.overview.attention.staleSource.description": "To źródło nigdy nie zostało pomyślnie zsynchronizowane lub od ostatniej udanej synchronizacji minęło ponad 7 dni.",
|
||||
|
||||
@ -351,6 +351,8 @@
|
||||
"newKnowledge.overview.attention.lowQualityQuery.title": "A qualidade da consulta precisa de revisão",
|
||||
"newKnowledge.overview.attention.modelReadiness.bindingMissing": "O índice publicado não está vinculado à configuração de modelos ativa. Reconstrua ou publique o índice novamente.",
|
||||
"newKnowledge.overview.attention.modelReadiness.description": "Perfis de modelo obrigatórios estão ausentes ou não estão vinculados ao índice publicado atual.",
|
||||
"newKnowledge.overview.attention.modelReadiness.pendingDescription": "Os modelos selecionados serão validados quando o primeiro documento for processado.",
|
||||
"newKnowledge.overview.attention.modelReadiness.pendingTitle": "A configuração do modelo aguarda validação",
|
||||
"newKnowledge.overview.attention.modelReadiness.profilesMissing": "Configure e publique os modelos obrigatórios de embedding, raciocínio e rerank.",
|
||||
"newKnowledge.overview.attention.modelReadiness.title": "A configuração de modelos está incompleta",
|
||||
"newKnowledge.overview.attention.staleSource.description": "Esta fonte nunca foi sincronizada com sucesso ou a última sincronização bem-sucedida ocorreu há mais de 7 dias.",
|
||||
|
||||
@ -351,6 +351,8 @@
|
||||
"newKnowledge.overview.attention.lowQualityQuery.title": "Calitatea interogării trebuie verificată",
|
||||
"newKnowledge.overview.attention.modelReadiness.bindingMissing": "Indexul publicat nu este asociat configurării active a modelelor. Reconstruiește sau republică indexul.",
|
||||
"newKnowledge.overview.attention.modelReadiness.description": "Profilurile de model obligatorii lipsesc sau nu sunt asociate indexului publicat în prezent.",
|
||||
"newKnowledge.overview.attention.modelReadiness.pendingDescription": "Modelele selectate vor fi validate la procesarea primului document.",
|
||||
"newKnowledge.overview.attention.modelReadiness.pendingTitle": "Configurația modelelor așteaptă validarea",
|
||||
"newKnowledge.overview.attention.modelReadiness.profilesMissing": "Configurează și publică modelele obligatorii de embedding, raționament și rerank.",
|
||||
"newKnowledge.overview.attention.modelReadiness.title": "Configurarea modelelor este incompletă",
|
||||
"newKnowledge.overview.attention.staleSource.description": "Această sursă nu s-a sincronizat niciodată cu succes sau ultima sincronizare reușită a avut loc acum mai mult de 7 zile.",
|
||||
|
||||
@ -351,6 +351,8 @@
|
||||
"newKnowledge.overview.attention.lowQualityQuery.title": "Качество запроса требует проверки",
|
||||
"newKnowledge.overview.attention.modelReadiness.bindingMissing": "Опубликованный индекс не связан с активной конфигурацией моделей. Перестройте или повторно опубликуйте индекс.",
|
||||
"newKnowledge.overview.attention.modelReadiness.description": "Обязательные профили моделей отсутствуют или не связаны с текущим опубликованным индексом.",
|
||||
"newKnowledge.overview.attention.modelReadiness.pendingDescription": "Выбранные модели будут проверены при обработке первого документа.",
|
||||
"newKnowledge.overview.attention.modelReadiness.pendingTitle": "Конфигурация моделей ожидает проверки",
|
||||
"newKnowledge.overview.attention.modelReadiness.profilesMissing": "Настройте и опубликуйте обязательные модели эмбеддингов, рассуждений и реранжирования.",
|
||||
"newKnowledge.overview.attention.modelReadiness.title": "Конфигурация моделей не завершена",
|
||||
"newKnowledge.overview.attention.staleSource.description": "Этот источник ни разу не синхронизировался успешно или с последней успешной синхронизации прошло более 7 дней.",
|
||||
|
||||
@ -351,6 +351,8 @@
|
||||
"newKnowledge.overview.attention.lowQualityQuery.title": "Kakovost poizvedbe je treba pregledati",
|
||||
"newKnowledge.overview.attention.modelReadiness.bindingMissing": "Objavljeni indeks ni povezan z aktivno konfiguracijo modelov. Znova zgradite ali objavite indeks.",
|
||||
"newKnowledge.overview.attention.modelReadiness.description": "Zahtevani profili modelov manjkajo ali niso povezani s trenutno objavljenim indeksom.",
|
||||
"newKnowledge.overview.attention.modelReadiness.pendingDescription": "Izbrani modeli bodo preverjeni ob obdelavi prvega dokumenta.",
|
||||
"newKnowledge.overview.attention.modelReadiness.pendingTitle": "Konfiguracija modelov čaka na preverjanje",
|
||||
"newKnowledge.overview.attention.modelReadiness.profilesMissing": "Konfigurirajte in objavite zahtevane modele za embedding, sklepanje in rerank.",
|
||||
"newKnowledge.overview.attention.modelReadiness.title": "Konfiguracija modelov ni dokončana",
|
||||
"newKnowledge.overview.attention.staleSource.description": "Ta vir še nikoli ni bil uspešno sinhroniziran ali pa je od zadnje uspešne sinhronizacije minilo več kot 7 dni.",
|
||||
|
||||
@ -351,6 +351,8 @@
|
||||
"newKnowledge.overview.attention.lowQualityQuery.title": "คุณภาพคิวรีต้องได้รับการตรวจสอบ",
|
||||
"newKnowledge.overview.attention.modelReadiness.bindingMissing": "ดัชนีที่เผยแพร่ไม่ได้เชื่อมโยงกับการกำหนดค่าโมเดลที่ใช้งานอยู่ โปรดสร้างหรือเผยแพร่ดัชนีอีกครั้ง",
|
||||
"newKnowledge.overview.attention.modelReadiness.description": "โปรไฟล์โมเดลที่จำเป็นขาดหายไปหรือไม่ได้เชื่อมโยงกับดัชนีที่เผยแพร่ในปัจจุบัน",
|
||||
"newKnowledge.overview.attention.modelReadiness.pendingDescription": "โมเดลที่เลือกจะได้รับการตรวจสอบเมื่อประมวลผลเอกสารแรก",
|
||||
"newKnowledge.overview.attention.modelReadiness.pendingTitle": "การกำหนดค่าโมเดลกำลังรอการตรวจสอบ",
|
||||
"newKnowledge.overview.attention.modelReadiness.profilesMissing": "กำหนดค่าและเผยแพร่โมเดล embedding, reasoning และ rerank ที่จำเป็น",
|
||||
"newKnowledge.overview.attention.modelReadiness.title": "การกำหนดค่าโมเดลยังไม่สมบูรณ์",
|
||||
"newKnowledge.overview.attention.staleSource.description": "แหล่งข้อมูลนี้ไม่เคยซิงค์สำเร็จ หรือการซิงค์สำเร็จครั้งล่าสุดผ่านมานานกว่า 7 วันแล้ว",
|
||||
|
||||
@ -351,6 +351,8 @@
|
||||
"newKnowledge.overview.attention.lowQualityQuery.title": "Sorgu kalitesinin incelenmesi gerekiyor",
|
||||
"newKnowledge.overview.attention.modelReadiness.bindingMissing": "Yayımlanan dizin etkin model yapılandırmasına bağlı değil. Dizini yeniden oluşturun veya yayımlayın.",
|
||||
"newKnowledge.overview.attention.modelReadiness.description": "Gerekli model profilleri eksik veya şu anda yayımlanan dizine bağlı değil.",
|
||||
"newKnowledge.overview.attention.modelReadiness.pendingDescription": "Seçilen modeller ilk belge işlendiğinde doğrulanacak.",
|
||||
"newKnowledge.overview.attention.modelReadiness.pendingTitle": "Model yapılandırması doğrulama bekliyor",
|
||||
"newKnowledge.overview.attention.modelReadiness.profilesMissing": "Gerekli embedding, akıl yürütme ve rerank modellerini yapılandırıp yayımlayın.",
|
||||
"newKnowledge.overview.attention.modelReadiness.title": "Model yapılandırması tamamlanmadı",
|
||||
"newKnowledge.overview.attention.staleSource.description": "Bu kaynak hiç başarıyla eşitlenmedi veya son başarılı eşitlemenin üzerinden 7 günden fazla zaman geçti.",
|
||||
|
||||
@ -351,6 +351,8 @@
|
||||
"newKnowledge.overview.attention.lowQualityQuery.title": "Якість запиту потребує перевірки",
|
||||
"newKnowledge.overview.attention.modelReadiness.bindingMissing": "Опублікований індекс не пов’язаний з активною конфігурацією моделей. Перебудуйте або повторно опублікуйте індекс.",
|
||||
"newKnowledge.overview.attention.modelReadiness.description": "Обов’язкові профілі моделей відсутні або не пов’язані з поточним опублікованим індексом.",
|
||||
"newKnowledge.overview.attention.modelReadiness.pendingDescription": "Вибрані моделі буде перевірено під час обробки першого документа.",
|
||||
"newKnowledge.overview.attention.modelReadiness.pendingTitle": "Конфігурація моделей очікує на перевірку",
|
||||
"newKnowledge.overview.attention.modelReadiness.profilesMissing": "Налаштуйте й опублікуйте обов’язкові моделі ембедингів, міркування та реранжування.",
|
||||
"newKnowledge.overview.attention.modelReadiness.title": "Конфігурацію моделей не завершено",
|
||||
"newKnowledge.overview.attention.staleSource.description": "Це джерело ще жодного разу не синхронізувалося успішно або від останньої успішної синхронізації минуло понад 7 днів.",
|
||||
|
||||
@ -351,6 +351,8 @@
|
||||
"newKnowledge.overview.attention.lowQualityQuery.title": "Chất lượng truy vấn cần được xem xét",
|
||||
"newKnowledge.overview.attention.modelReadiness.bindingMissing": "Chỉ mục đã xuất bản chưa được liên kết với cấu hình mô hình đang hoạt động. Hãy tạo lại hoặc xuất bản lại chỉ mục.",
|
||||
"newKnowledge.overview.attention.modelReadiness.description": "Thiếu hồ sơ mô hình bắt buộc hoặc các hồ sơ này chưa được liên kết với chỉ mục hiện đã xuất bản.",
|
||||
"newKnowledge.overview.attention.modelReadiness.pendingDescription": "Các mô hình đã chọn sẽ được xác thực khi tài liệu đầu tiên được xử lý.",
|
||||
"newKnowledge.overview.attention.modelReadiness.pendingTitle": "Cấu hình mô hình đang chờ xác thực",
|
||||
"newKnowledge.overview.attention.modelReadiness.profilesMissing": "Hãy cấu hình và xuất bản các mô hình embedding, suy luận và rerank bắt buộc.",
|
||||
"newKnowledge.overview.attention.modelReadiness.title": "Cấu hình mô hình chưa hoàn tất",
|
||||
"newKnowledge.overview.attention.staleSource.description": "Nguồn này chưa từng đồng bộ thành công hoặc lần đồng bộ thành công gần nhất đã cách đây hơn 7 ngày.",
|
||||
|
||||
@ -351,6 +351,8 @@
|
||||
"newKnowledge.overview.attention.lowQualityQuery.title": "查询质量需要检查",
|
||||
"newKnowledge.overview.attention.modelReadiness.bindingMissing": "已发布索引尚未绑定当前模型配置,请重建或重新发布索引。",
|
||||
"newKnowledge.overview.attention.modelReadiness.description": "缺少必需的模型配置,或模型配置尚未绑定当前已发布索引。",
|
||||
"newKnowledge.overview.attention.modelReadiness.pendingDescription": "处理首个文档时将验证所选模型。",
|
||||
"newKnowledge.overview.attention.modelReadiness.pendingTitle": "模型配置等待验证",
|
||||
"newKnowledge.overview.attention.modelReadiness.profilesMissing": "请配置并发布必需的 Embedding、推理和 Rerank 模型。",
|
||||
"newKnowledge.overview.attention.modelReadiness.title": "模型配置未完成",
|
||||
"newKnowledge.overview.attention.staleSource.description": "该数据源从未成功同步,或距离上次成功同步已超过 7 天。",
|
||||
|
||||
@ -351,6 +351,8 @@
|
||||
"newKnowledge.overview.attention.lowQualityQuery.title": "查詢品質需要檢查",
|
||||
"newKnowledge.overview.attention.modelReadiness.bindingMissing": "已發佈索引尚未綁定目前模型設定,請重建或重新發佈索引。",
|
||||
"newKnowledge.overview.attention.modelReadiness.description": "缺少必要的模型設定,或模型設定尚未綁定目前已發佈索引。",
|
||||
"newKnowledge.overview.attention.modelReadiness.pendingDescription": "處理第一份文件時將驗證所選模型。",
|
||||
"newKnowledge.overview.attention.modelReadiness.pendingTitle": "模型設定正在等待驗證",
|
||||
"newKnowledge.overview.attention.modelReadiness.profilesMissing": "請設定並發佈必要的 Embedding、推理和 Rerank 模型。",
|
||||
"newKnowledge.overview.attention.modelReadiness.title": "模型設定未完成",
|
||||
"newKnowledge.overview.attention.staleSource.description": "此資料來源從未成功同步,或距離上次成功同步已超過 7 天。",
|
||||
|
||||
@ -504,6 +504,18 @@ describe('normalizeConsoleOpenAPIURL', () => {
|
||||
expect(searchParams.has('creator_ids[0]')).toBe(false)
|
||||
})
|
||||
|
||||
it('should serialize KnowledgeFS list query arrays as repeated params', () => {
|
||||
const url = normalizeConsoleOpenAPIURL(
|
||||
'https://example.com/console/api/knowledge-fs/spaces?tag_ids%5B0%5D=tag-1&creator_ids%5B0%5D=user-1',
|
||||
)
|
||||
const searchParams = new URL(url).searchParams
|
||||
|
||||
expect(searchParams.getAll('tag_ids')).toEqual(['tag-1'])
|
||||
expect(searchParams.getAll('creator_ids')).toEqual(['user-1'])
|
||||
expect(searchParams.has('tag_ids[0]')).toBe(false)
|
||||
expect(searchParams.has('creator_ids[0]')).toBe(false)
|
||||
})
|
||||
|
||||
it('should serialize snippet list query arrays as repeated params', () => {
|
||||
const url = normalizeConsoleOpenAPIURL(
|
||||
'https://example.com/console/api/workspaces/current/customized-snippets?tag_ids%5B0%5D=tag-1&creators%5B0%5D=user-1',
|
||||
|
||||
@ -15,7 +15,7 @@ const repeatedQueryArrayRules: readonly QueryArrayCompatibilityRule[] = [
|
||||
{ path: /\/datasets$/, fields: ['ids', 'tag_ids'] },
|
||||
{ path: /\/datasets\/[^/]+\/documents\/[^/]+\/segment\/[^/]+$/, fields: ['segment_id'] },
|
||||
{ path: /\/datasets\/[^/]+\/documents\/[^/]+\/segments$/, fields: ['segment_id', 'status'] },
|
||||
{ path: /\/knowledge-fs\/spaces$/, fields: ['creator_ids'] },
|
||||
{ path: /\/knowledge-fs\/spaces$/, fields: ['creator_ids', 'tag_ids'] },
|
||||
{ path: /\/trial-apps\/[^/]+\/datasets$/, fields: ['ids'] },
|
||||
{ path: /\/workspaces\/current\/customized-snippets$/, fields: ['tag_ids', 'creators'] },
|
||||
{ path: /\/workspaces\/current\/plugin\/[^/]+\/list$/, fields: ['tags'] },
|
||||
|
||||
Loading…
Reference in New Issue
Block a user