mirror of
https://github.com/langgenius/dify.git
synced 2026-09-08 02:43:49 +08:00
fix(knowledge): simplify query activity feed
This commit is contained in:
parent
0391141936
commit
0aa7a2ac84
@ -1,6 +1,6 @@
|
||||
{
|
||||
"schemaVersion": 5,
|
||||
"subtreeTree": "05e402ad162663945d482ebfa7c5221e73347364",
|
||||
"subtreeTree": "b480632eddd1d3946a18d45472c0856bee471b58",
|
||||
"openapiSha256": "2cf348c68bbe65dd51bbde9a0a4f91398beeebd79e89e9288c9386b26ae09796",
|
||||
"capabilityV2AuthManifestSha256": "fc0a47e23cce12544882f0298522b4933002e892b84ce1815df7e81d36a7a0c7",
|
||||
"capabilityV2AuthTestVectorSha256": "ae0de37b1ff05c40f905cf17a7b410d8971acacf64db07d5ee3d6fecfa559ce3",
|
||||
|
||||
@ -0,0 +1,28 @@
|
||||
# Query Activity Consolidation
|
||||
|
||||
## Summary
|
||||
|
||||
- Records one `query.requested` activity when a user starts a query, including the resolved
|
||||
retrieval mode and a bounded copy of the question.
|
||||
- Stops appending `query.completed`, `query.failed`, and `profile.published` activities.
|
||||
- Excludes historical terminal-query and profile-publication rows from activity-feed reads while
|
||||
retaining their storage decoding compatibility.
|
||||
- Shows the question and retrieval mode directly in recent activity and the complete activity
|
||||
drawer.
|
||||
|
||||
## Safety and Compatibility
|
||||
|
||||
- Query questions are limited to 4,000 characters in activity details; longer values are marked
|
||||
as truncated.
|
||||
- Credentials, tokens, object keys, and the legacy arbitrary `query` detail key remain excluded by
|
||||
the activity-detail allow-list.
|
||||
- Overview answer-rate and outcome calculations continue to use durable AnswerTrace and
|
||||
FailedQuery facts rather than the removed terminal activity writes.
|
||||
|
||||
## Verification
|
||||
|
||||
- `pnpm --filter @knowledge/api test`
|
||||
- `pnpm --filter @knowledge/api typecheck`
|
||||
- `pnpm exec vitest run features/new-rag/__tests__/knowledge-overview-page.spec.tsx` (from `web/`)
|
||||
- `pnpm type-check` (from `web/`)
|
||||
- `git diff --check`
|
||||
@ -123,16 +123,12 @@ describe.each(["postgres", "tidb"] as const)(
|
||||
JSON.stringify(["team:camera"]),
|
||||
]);
|
||||
const sql = select?.sql ?? "";
|
||||
expect(sql).toContain("UNION ALL");
|
||||
expect(sql).toContain("answer_traces");
|
||||
expect(sql).not.toContain("UNION ALL");
|
||||
expect(sql).not.toContain("answer_traces");
|
||||
expect(sql).toContain("NOT IN");
|
||||
expect(sql).toContain("query.completed");
|
||||
expect(sql).toContain("query.failed");
|
||||
expect(sql).toMatch(
|
||||
/stored_request\.[`"]actor_subject_id[`"] = stored_trace\.[`"]subject_id[`"]/u,
|
||||
);
|
||||
expect(sql).toMatch(
|
||||
/stored_trace\.[`"]created_at[`"] >= stored_request\.[`"]occurred_at[`"]/u,
|
||||
);
|
||||
expect(sql).toContain("profile.published");
|
||||
expect(sql.indexOf("tenant_id")).toBeLessThan(sql.indexOf("ORDER BY"));
|
||||
expect(sql.indexOf("knowledge_space_id")).toBeLessThan(sql.indexOf("ORDER BY"));
|
||||
expect(sql).toContain(dialect === "postgres" ? "::jsonb @>" : "JSON_CONTAINS");
|
||||
|
||||
@ -23,6 +23,7 @@ import {
|
||||
import { lockKnowledgeSpaceForDeletionAdmission } from "./knowledge-space-deletion-admission";
|
||||
import {
|
||||
type AppendKnowledgeSpaceActivityInput,
|
||||
HiddenKnowledgeSpaceActivityActions,
|
||||
type KnowledgeSpaceActivityAction,
|
||||
KnowledgeSpaceActivityActions,
|
||||
type KnowledgeSpaceActivityEvent,
|
||||
@ -1318,9 +1319,11 @@ function activityReadModelSql(database: DatabaseAdapter): string {
|
||||
const selectColumns = (alias: string) =>
|
||||
columns.map((column) => `${alias}.${q(database, column)}`).join(", ");
|
||||
const activity = q(database, "knowledge_space_activity_events");
|
||||
const traces = q(database, "answer_traces");
|
||||
const hiddenActions = HiddenKnowledgeSpaceActivityActions.map((action) => `'${action}'`).join(
|
||||
", ",
|
||||
);
|
||||
|
||||
return `SELECT ${selectColumns("stored_event")} FROM ${activity} stored_event WHERE NOT (stored_event.${q(database, "action")} IN ('query.completed', 'query.failed') AND EXISTS (SELECT 1 FROM ${traces} stored_trace WHERE stored_trace.${q(database, "knowledge_space_id")} = stored_event.${q(database, "knowledge_space_id")} AND ${textIdSql(database, "stored_trace", "id")} = stored_event.${q(database, "resource_id")} AND EXISTS (SELECT 1 FROM ${activity} stored_request WHERE stored_request.${q(database, "tenant_id")} = stored_event.${q(database, "tenant_id")} AND stored_request.${q(database, "knowledge_space_id")} = stored_event.${q(database, "knowledge_space_id")} AND stored_request.${q(database, "resource_id")} = stored_event.${q(database, "resource_id")} AND stored_request.${q(database, "actor_subject_id")} = stored_trace.${q(database, "subject_id")} AND stored_request.${q(database, "action")} = 'query.requested' AND stored_trace.${q(database, "created_at")} >= stored_request.${q(database, "occurred_at")}))) UNION ALL SELECT terminal_trace.${q(database, "id")} AS ${q(database, "id")}, requested.${q(database, "tenant_id")}, requested.${q(database, "knowledge_space_id")}, requested.${q(database, "actor_type")}, requested.${q(database, "actor_subject_id")}, CASE WHEN terminal_trace.${q(database, "completed")} = TRUE THEN 'query.completed' ELSE 'query.failed' END AS ${q(database, "action")}, requested.${q(database, "resource_type")}, requested.${q(database, "resource_id")}, CASE WHEN terminal_trace.${q(database, "completed")} = TRUE THEN 'success' ELSE 'failure' END AS ${q(database, "result")}, requested.${q(database, "required_permission_scope")}, requested.${q(database, "details")}, terminal_trace.${q(database, "created_at")} AS ${q(database, "occurred_at")} FROM ${traces} terminal_trace INNER JOIN ${activity} requested ON requested.${q(database, "knowledge_space_id")} = terminal_trace.${q(database, "knowledge_space_id")} AND requested.${q(database, "resource_id")} = ${textIdSql(database, "terminal_trace", "id")} AND requested.${q(database, "actor_subject_id")} = terminal_trace.${q(database, "subject_id")} AND requested.${q(database, "action")} = 'query.requested' AND requested.${q(database, "resource_type")} = 'query' AND terminal_trace.${q(database, "created_at")} >= requested.${q(database, "occurred_at")}`;
|
||||
return `SELECT ${selectColumns("stored_event")} FROM ${activity} stored_event WHERE stored_event.${q(database, "action")} NOT IN (${hiddenActions})`;
|
||||
}
|
||||
|
||||
function textIdSql(database: DatabaseAdapter, alias: string, column: string): string {
|
||||
|
||||
@ -5,6 +5,7 @@ import {
|
||||
decodeKnowledgeSpaceActivityCursor,
|
||||
deterministicKnowledgeSpaceActivityId,
|
||||
encodeKnowledgeSpaceActivityCursor,
|
||||
sanitizeKnowledgeSpaceActivityDetails,
|
||||
} from "./knowledge-space-overview";
|
||||
|
||||
const TENANT_ID = "tenant-overview";
|
||||
@ -23,7 +24,12 @@ describe("in-memory knowledge-space Overview repository", () => {
|
||||
const input = {
|
||||
action: "query.requested" as const,
|
||||
actor: { id: "member-1", type: "member" as const },
|
||||
details: { apiKey: "must-not-leak", mode: "fast", query: "must-not-leak" },
|
||||
details: {
|
||||
apiKey: "must-not-leak",
|
||||
mode: "fast",
|
||||
query: "legacy-query-key-must-not-leak",
|
||||
question: "What is the permission model?",
|
||||
},
|
||||
id,
|
||||
knowledgeSpaceId: SPACE_ID,
|
||||
occurredAt: NOW,
|
||||
@ -40,7 +46,7 @@ describe("in-memory knowledge-space Overview repository", () => {
|
||||
});
|
||||
|
||||
expect(replay).toEqual(first);
|
||||
expect(first.details).toEqual({ mode: "fast" });
|
||||
expect(first.details).toEqual({ mode: "fast", question: "What is the permission model?" });
|
||||
await expect(repository.appendActivity({ ...input, result: "failure" })).rejects.toThrow(
|
||||
"idempotency key",
|
||||
);
|
||||
@ -49,6 +55,59 @@ describe("in-memory knowledge-space Overview repository", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("keeps legacy terminal and profile events out of the product activity feed", async () => {
|
||||
const repository = createInMemoryKnowledgeSpaceOverviewRepository({
|
||||
maxEvents: 3,
|
||||
maxListLimit: 10,
|
||||
});
|
||||
const common = {
|
||||
actor: { id: "member-1", type: "member" as const },
|
||||
knowledgeSpaceId: SPACE_ID,
|
||||
occurredAt: NOW,
|
||||
requiredPermissionScope: ["team:camera"],
|
||||
tenantId: TENANT_ID,
|
||||
};
|
||||
await repository.appendActivity({
|
||||
...common,
|
||||
action: "query.requested",
|
||||
id: "00000000-0000-4000-8000-000000000021",
|
||||
resource: { id: QUERY_ID, type: "query" },
|
||||
result: "success",
|
||||
});
|
||||
await repository.appendActivity({
|
||||
...common,
|
||||
action: "query.completed",
|
||||
id: "00000000-0000-4000-8000-000000000022",
|
||||
resource: { id: QUERY_ID, type: "query" },
|
||||
result: "success",
|
||||
});
|
||||
await repository.appendActivity({
|
||||
...common,
|
||||
action: "profile.published",
|
||||
id: "00000000-0000-4000-8000-000000000023",
|
||||
resource: { id: "profile-1", type: "profile" },
|
||||
result: "success",
|
||||
});
|
||||
|
||||
const activity = await repository.listActivity({
|
||||
candidateGrants: ["team:camera"],
|
||||
knowledgeSpaceId: SPACE_ID,
|
||||
limit: 10,
|
||||
tenantId: TENANT_ID,
|
||||
});
|
||||
|
||||
expect(activity.items.map((event) => event.action)).toEqual(["query.requested"]);
|
||||
});
|
||||
|
||||
it("bounds long questions without losing the truncation marker on database re-sanitization", () => {
|
||||
const details = sanitizeKnowledgeSpaceActivityDetails({ question: "q".repeat(4_001) });
|
||||
|
||||
expect(details.question).toHaveLength(4_000);
|
||||
expect(details.question).toMatch(/…$/u);
|
||||
expect(details.questionTruncated).toBe(true);
|
||||
expect(sanitizeKnowledgeSpaceActivityDetails(details)).toEqual(details);
|
||||
});
|
||||
|
||||
it("counts distinct requested query identities and only their later successful completion", async () => {
|
||||
const repository = createInMemoryKnowledgeSpaceOverviewRepository({
|
||||
maxEvents: 20,
|
||||
|
||||
@ -19,6 +19,16 @@ export const KnowledgeSpaceActivityActions = [
|
||||
] as const;
|
||||
export type KnowledgeSpaceActivityAction = (typeof KnowledgeSpaceActivityActions)[number];
|
||||
|
||||
/** Retained for decoding historical rows, but excluded from the product activity feed. */
|
||||
export const HiddenKnowledgeSpaceActivityActions = [
|
||||
"query.completed",
|
||||
"query.failed",
|
||||
"profile.published",
|
||||
] as const satisfies readonly KnowledgeSpaceActivityAction[];
|
||||
const HiddenKnowledgeSpaceActivityActionSet: ReadonlySet<KnowledgeSpaceActivityAction> = new Set(
|
||||
HiddenKnowledgeSpaceActivityActions,
|
||||
);
|
||||
|
||||
export const KnowledgeSpaceActivityResourceTypes = [
|
||||
"knowledge-space",
|
||||
"query",
|
||||
@ -41,7 +51,7 @@ export interface KnowledgeSpaceActivityEvent {
|
||||
readonly id?: string | undefined;
|
||||
readonly type: "member" | "system";
|
||||
};
|
||||
/** A deliberately small allow-list; query text, credentials, tokens and object keys are absent. */
|
||||
/** A deliberately small allow-list; credentials, tokens and object keys are absent. */
|
||||
readonly details: Readonly<Record<string, boolean | number | string>>;
|
||||
readonly id: string;
|
||||
readonly knowledgeSpaceId: string;
|
||||
@ -399,6 +409,7 @@ export function createInMemoryKnowledgeSpaceOverviewRepository(options: {
|
||||
event.knowledgeSpaceId === input.knowledgeSpaceId &&
|
||||
candidatePermissionScopeAllows(event.requiredPermissionScope, input.candidateGrants),
|
||||
)
|
||||
.filter((event) => !HiddenKnowledgeSpaceActivityActionSet.has(event.action))
|
||||
.filter((event) => !input.action || event.action === input.action)
|
||||
.filter((event) => !input.actorType || event.actor.type === input.actorType)
|
||||
.filter((event) => !input.actorId || event.actor.id === input.actorId)
|
||||
@ -489,6 +500,8 @@ const SAFE_ACTIVITY_DETAIL_KEYS = new Set([
|
||||
"durationMs",
|
||||
"mode",
|
||||
"providerId",
|
||||
"question",
|
||||
"questionTruncated",
|
||||
"reasonCode",
|
||||
"statusCode",
|
||||
]);
|
||||
@ -502,7 +515,13 @@ export function sanitizeKnowledgeSpaceActivityDetails(
|
||||
if (typeof value !== "string" && typeof value !== "number" && typeof value !== "boolean") {
|
||||
continue;
|
||||
}
|
||||
if (typeof value === "string" && value.length > 160) continue;
|
||||
if (typeof value === "string" && key === "question" && value.length > 4_000) {
|
||||
safe.question = `${value.slice(0, 3_999)}…`;
|
||||
safe.questionTruncated = true;
|
||||
continue;
|
||||
}
|
||||
if (typeof value === "string" && value.length > 160 && key !== "question") continue;
|
||||
if (key === "questionTruncated" && value !== true) continue;
|
||||
if (typeof value === "number" && !Number.isFinite(value)) continue;
|
||||
safe[key] = value;
|
||||
}
|
||||
|
||||
@ -324,7 +324,7 @@ describe("knowledge-space profile repository", () => {
|
||||
(call) =>
|
||||
call.tableName === "knowledge_space_activity_events" && call.operation === "insert",
|
||||
),
|
||||
).toBe(true);
|
||||
).toBe(false);
|
||||
},
|
||||
);
|
||||
|
||||
|
||||
@ -34,8 +34,6 @@ import {
|
||||
assertDatabaseKnowledgeSpacePermissionFence,
|
||||
} from "./knowledge-space-access-control";
|
||||
import { lockKnowledgeSpaceForDeletionAdmission } from "./knowledge-space-deletion-admission";
|
||||
import { deterministicKnowledgeSpaceActivityId } from "./knowledge-space-overview";
|
||||
import { appendKnowledgeSpaceActivityWithExecutor } from "./knowledge-space-overview-database-repository";
|
||||
|
||||
export const KnowledgeSpaceProfileKinds = ["embedding", "retrieval"] as const;
|
||||
export type KnowledgeSpaceProfileKind = (typeof KnowledgeSpaceProfileKinds)[number];
|
||||
@ -492,28 +490,6 @@ export function createDatabaseKnowledgeSpaceProfileRepository({
|
||||
if (!head) {
|
||||
throw new Error("Activated knowledge-space profile head could not be reloaded");
|
||||
}
|
||||
await appendKnowledgeSpaceActivityWithExecutor({
|
||||
database,
|
||||
executor: transaction,
|
||||
input: {
|
||||
action: "profile.published",
|
||||
actor: { id: candidate.createdBySubjectId, type: "member" },
|
||||
details: { providerId: candidate.provider },
|
||||
id: deterministicKnowledgeSpaceActivityId(
|
||||
"profile.published",
|
||||
input.tenantId,
|
||||
input.knowledgeSpaceId,
|
||||
input.kind,
|
||||
candidate.id,
|
||||
),
|
||||
knowledgeSpaceId: input.knowledgeSpaceId,
|
||||
occurredAt: input.now,
|
||||
requiredPermissionScope: [],
|
||||
resource: { id: candidate.id, type: "profile" },
|
||||
result: "success",
|
||||
tenantId: input.tenantId,
|
||||
},
|
||||
});
|
||||
return head;
|
||||
});
|
||||
},
|
||||
@ -1588,28 +1564,6 @@ async function activateUnpublishedProfileRevision({
|
||||
|
||||
const head = await getProfileHead(database, transaction, input, false);
|
||||
if (!head) throw new Error("Activated unpublished profile head could not be reloaded");
|
||||
await appendKnowledgeSpaceActivityWithExecutor({
|
||||
database,
|
||||
executor: transaction,
|
||||
input: {
|
||||
action: "profile.published",
|
||||
actor: { id: candidate.createdBySubjectId, type: "member" },
|
||||
details: { providerId: candidate.provider },
|
||||
id: deterministicKnowledgeSpaceActivityId(
|
||||
"profile.published",
|
||||
input.tenantId,
|
||||
input.knowledgeSpaceId,
|
||||
input.kind,
|
||||
candidate.id,
|
||||
),
|
||||
knowledgeSpaceId: input.knowledgeSpaceId,
|
||||
occurredAt: input.now,
|
||||
requiredPermissionScope: [],
|
||||
resource: { id: candidate.id, type: "profile" },
|
||||
result: "success",
|
||||
tenantId: input.tenantId,
|
||||
},
|
||||
});
|
||||
return { head, replayed: false };
|
||||
}
|
||||
|
||||
|
||||
@ -221,7 +221,7 @@ describe("query handler branch coverage", () => {
|
||||
await response.body?.cancel();
|
||||
});
|
||||
|
||||
it("records requested and failed terminal activity and releases after session failure", async () => {
|
||||
it("records only the query request and releases after session failure", async () => {
|
||||
const release = vi.fn(async () => undefined);
|
||||
const appendActivity = vi.fn(async () => ({}));
|
||||
const failure = new Error("session failed");
|
||||
@ -231,19 +231,26 @@ describe("query handler branch coverage", () => {
|
||||
sessionError: failure,
|
||||
});
|
||||
await expect(fixture.invoke()).rejects.toBe(failure);
|
||||
expect(appendActivity).toHaveBeenCalledTimes(2);
|
||||
expect(appendActivity).toHaveBeenCalledOnce();
|
||||
expect(appendActivity).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
action: "query.requested",
|
||||
details: { mode: "fast", question: "question" },
|
||||
result: "success",
|
||||
}),
|
||||
);
|
||||
expect(release).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
it("records successful terminal activity when the stream completes", async () => {
|
||||
it("does not append a terminal activity when the stream completes", async () => {
|
||||
const appendActivity = vi.fn(async () => ({}));
|
||||
const fixture = queryFixture({ overview: { appendActivity } });
|
||||
const response = await fixture.invoke();
|
||||
expect(response.status).toBe(200);
|
||||
await response.text();
|
||||
expect(appendActivity).toHaveBeenCalledTimes(2);
|
||||
expect(appendActivity).toHaveBeenLastCalledWith(
|
||||
expect.objectContaining({ action: "query.completed", result: "success" }),
|
||||
expect(appendActivity).toHaveBeenCalledOnce();
|
||||
expect(appendActivity).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ action: "query.requested", result: "success" }),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@ -388,35 +388,13 @@ export function registerQueryHandlers({
|
||||
throw error;
|
||||
}
|
||||
|
||||
const appendTerminalActivity = async (status: "canceled" | "failed" | "succeeded") => {
|
||||
if (!overview) return;
|
||||
const occurredAt = new Date(now()).toISOString();
|
||||
await overview.appendActivity({
|
||||
action: status === "succeeded" ? "query.completed" : "query.failed",
|
||||
actor: { id: subject.subjectId, type: "member" },
|
||||
details: { mode: resolvedMode },
|
||||
id: deterministicKnowledgeSpaceActivityId(
|
||||
`query.${status}`,
|
||||
subject.tenantId,
|
||||
space.id,
|
||||
queryRunId,
|
||||
),
|
||||
knowledgeSpaceId: space.id,
|
||||
occurredAt,
|
||||
requiredPermissionScope: [],
|
||||
resource: { id: queryRunId, type: "query" },
|
||||
result: status === "succeeded" ? "success" : status === "canceled" ? "canceled" : "failure",
|
||||
tenantId: subject.tenantId,
|
||||
});
|
||||
};
|
||||
let requestedActivityPersisted = false;
|
||||
try {
|
||||
if (overview) {
|
||||
const occurredAt = new Date(now()).toISOString();
|
||||
await overview.appendActivity({
|
||||
action: "query.requested",
|
||||
actor: { id: subject.subjectId, type: "member" },
|
||||
details: { mode: resolvedMode },
|
||||
details: { mode: resolvedMode, ...(query ? { question: query } : {}) },
|
||||
id: deterministicKnowledgeSpaceActivityId(
|
||||
"query.requested",
|
||||
subject.tenantId,
|
||||
@ -427,10 +405,9 @@ export function registerQueryHandlers({
|
||||
occurredAt,
|
||||
requiredPermissionScope: [],
|
||||
resource: { id: queryRunId, type: "query" },
|
||||
result: "pending",
|
||||
result: "success",
|
||||
tenantId: subject.tenantId,
|
||||
});
|
||||
requestedActivityPersisted = true;
|
||||
}
|
||||
const session = await sessionRepository.recordQuery({
|
||||
activeDocumentIds: body.activeDocumentIds,
|
||||
@ -500,18 +477,10 @@ export function registerQueryHandlers({
|
||||
subject,
|
||||
traceId: queryRunId,
|
||||
},
|
||||
...(overview
|
||||
? {
|
||||
onTerminal: appendTerminalActivity,
|
||||
}
|
||||
: {}),
|
||||
sessionId: session.context.sessionId,
|
||||
traceId: queryRunId,
|
||||
});
|
||||
} catch (error) {
|
||||
if (requestedActivityPersisted) {
|
||||
await appendTerminalActivity("failed").catch(() => undefined);
|
||||
}
|
||||
await executionLease?.release().catch(() => undefined);
|
||||
throw error;
|
||||
}
|
||||
|
||||
@ -22,7 +22,7 @@ const SUBJECT = {
|
||||
};
|
||||
|
||||
describe("query Overview durability", () => {
|
||||
it("persists query.requested before admitting generation and records terminal activity", async () => {
|
||||
it("persists one query activity with its question and resolved mode before generation", async () => {
|
||||
const spaces = createInMemoryKnowledgeSpaceRepository({
|
||||
generateId: () => "018f0d60-7a49-7cc2-9c1b-5b36f18f9a02",
|
||||
maxListLimit: 10,
|
||||
@ -118,32 +118,20 @@ describe("query Overview durability", () => {
|
||||
limit: 10,
|
||||
tenantId: SUBJECT.tenantId,
|
||||
});
|
||||
expect(activity.items.map((event) => event.action).sort()).toEqual([
|
||||
"query.completed",
|
||||
"query.requested",
|
||||
expect(activity.items).toEqual([
|
||||
expect.objectContaining({
|
||||
action: "query.requested",
|
||||
details: { mode: "fast", question: "Is the request durable?" },
|
||||
id: deterministicKnowledgeSpaceActivityId(
|
||||
"query.requested",
|
||||
SUBJECT.tenantId,
|
||||
space.id,
|
||||
QUERY_RUN_ID,
|
||||
),
|
||||
resource: { id: QUERY_RUN_ID, type: "query" },
|
||||
result: "success",
|
||||
}),
|
||||
]);
|
||||
expect(activity.items).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
id: deterministicKnowledgeSpaceActivityId(
|
||||
"query.requested",
|
||||
SUBJECT.tenantId,
|
||||
space.id,
|
||||
QUERY_RUN_ID,
|
||||
),
|
||||
resource: { id: QUERY_RUN_ID, type: "query" },
|
||||
}),
|
||||
expect.objectContaining({
|
||||
id: deterministicKnowledgeSpaceActivityId(
|
||||
"query.succeeded",
|
||||
SUBJECT.tenantId,
|
||||
space.id,
|
||||
QUERY_RUN_ID,
|
||||
),
|
||||
resource: { id: QUERY_RUN_ID, type: "query" },
|
||||
}),
|
||||
]),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@ -327,6 +327,7 @@ describe('KnowledgeOverviewPage', () => {
|
||||
queryData.attention.data = []
|
||||
queryData.activity.data[0]!.action = 'source.synced'
|
||||
queryData.activity.data[0]!.actor = { id: 'dify-account:member-1', type: 'member' }
|
||||
queryData.activity.data[0]!.details = { count: 1 }
|
||||
queryData.activity.data[0]!.occurred_at = '2026-07-29T08:05:00Z'
|
||||
queryData.activity.data[0]!.result = 'success'
|
||||
queryData.stats.source_count = 3
|
||||
@ -730,6 +731,29 @@ describe('KnowledgeOverviewPage', () => {
|
||||
expect(within(dialog).getByText(/2h ago|2 hr\. ago|2 hours ago/)).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('shows one initiated query activity with its question and retrieval mode', async () => {
|
||||
const user = userEvent.setup()
|
||||
queryData.activity.data[0]!.action = 'query.requested'
|
||||
queryData.activity.data[0]!.details = {
|
||||
mode: 'research',
|
||||
question: 'How do permissions work?',
|
||||
}
|
||||
queryData.activity.data[0]!.result = 'success'
|
||||
|
||||
renderWithNuqs(<KnowledgeOverviewPage knowledgeSpaceId="space-1" />)
|
||||
|
||||
const label = 'dataset.newKnowledge.qualityPage.question: How do permissions work? — research'
|
||||
expect(within(screen.getByRole('table')).getByText(label)).toBeInTheDocument()
|
||||
|
||||
await user.click(
|
||||
screen.getByRole('button', { name: 'dataset.newKnowledge.overview.allActivity' }),
|
||||
)
|
||||
expect(within(screen.getByRole('dialog')).getByText(label)).toBeInTheDocument()
|
||||
expect(
|
||||
screen.queryByText('dataset.newKnowledge.overview.activityQueued'),
|
||||
).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('refreshes overview snapshots until they catch up with a completed background task', () => {
|
||||
queryData.tasks[0]!.state = 'running'
|
||||
queryData.tasks[0]!.updated_at = '2026-07-29T10:00:00Z'
|
||||
|
||||
@ -689,8 +689,6 @@ function activityOperationLabel(
|
||||
if (activity.action.startsWith('query.'))
|
||||
return t(($) => $['newKnowledge.overview.queryOutcomes'])
|
||||
if (activity.action === 'permission.updated') return t(($) => $['newKnowledge.permission'])
|
||||
if (activity.action === 'profile.published')
|
||||
return t(($) => $['newKnowledge.retrievalTest.title'])
|
||||
if (activity.action === 'settings.updated')
|
||||
return t(($) => $['newKnowledge.overview.updateEvidence'])
|
||||
return t(($) => $['newKnowledge.backgroundTasks'])
|
||||
@ -700,6 +698,16 @@ function activityLabel(
|
||||
activity: KnowledgeFsOverviewActivityResponse,
|
||||
t: ReturnType<typeof useTranslation<'dataset'>>['t'],
|
||||
) {
|
||||
if (activity.action === 'query.requested') {
|
||||
const question = activity.details.question
|
||||
const mode = activity.details.mode
|
||||
const label =
|
||||
typeof question === 'string' && question.trim()
|
||||
? `${t(($) => $['newKnowledge.qualityPage.question'])}: ${question}`
|
||||
: activityOperationLabel(activity, t)
|
||||
return typeof mode === 'string' && mode.trim() ? `${label} — ${mode}` : label
|
||||
}
|
||||
|
||||
const operation = activityOperationLabel(activity, t)
|
||||
let label: string
|
||||
if (activity.result === 'success')
|
||||
@ -708,11 +716,7 @@ function activityLabel(
|
||||
label = t(($) => $['newKnowledge.overview.activityFailed'], { operation })
|
||||
else if (activity.result === 'canceled')
|
||||
label = t(($) => $['newKnowledge.overview.activityCanceled'], { operation })
|
||||
else
|
||||
label =
|
||||
activity.action === 'query.requested'
|
||||
? t(($) => $['newKnowledge.overview.activityQueued'], { operation })
|
||||
: t(($) => $['newKnowledge.overview.activityRunning'], { operation })
|
||||
else label = t(($) => $['newKnowledge.overview.activityRunning'], { operation })
|
||||
|
||||
const detail = [
|
||||
activity.details.reasonCode,
|
||||
|
||||
Loading…
Reference in New Issue
Block a user