返回 DeepSeek-Reasonix
topicRemovalMock.ts
根目录 / desktop / frontend / src / lib / topicRemovalMock.ts
1 import type { ProjectNode } from "./types";
2 import type { TopicRemovalTarget, TopicRemovalRequest, TopicRemovalInspection, TopicRemovalResult, TrashEntry } from "../generated/desktopContract.generated";
3 import { mockProjectGroups, notifyMockProjectTreeChanged } from "./mockProjectTreeOrganization";
4
5 type Receipt = { request: string; result: TopicRemovalResult; parent: ProjectNode; node: ProjectNode; order: number; entry?: TrashEntry };
6 const states = new WeakMap<ProjectNode[], { automatic: Set<string>; receipts: Map<string, Receipt>; generation: number }>();
7 function state(tree: ProjectNode[]) {
8 let value = states.get(tree);
9 if (!value) { value = { automatic: new Set(), receipts: new Map(), generation: 1 }; states.set(tree, value); }
10 return value;
11 }
12 export function markTopic(tree: ProjectNode[], id: string, automatic: boolean) {
13 if (automatic) state(tree).automatic.add(id); else state(tree).automatic.delete(id);
14 }
15 function owner(tree: ProjectNode[], id: string) {
16 const parents = tree.filter(parent => parent.children?.some(node => node.topicId === id));
17 if (parents.length !== 1) throw new Error("Topic ownership conflict");
18 const parent = parents[0], order = parent.children!.findIndex(node => node.topicId === id), node = parent.children![order];
19 const workspaceId = parent.kind === "global_folder" ? "global" : `project-${(parent.root || parent.key).replace(/[^a-zA-Z0-9._-]/g, "-")}`;
20 return { parent, order, node, workspaceId };
21 }
22 export function inspect(tree: ProjectNode[], target: TopicRemovalTarget): TopicRemovalInspection {
23 const { parent, node, workspaceId } = owner(tree, target.topicId);
24 if (target.workspaceId && target.workspaceId !== workspaceId) throw new Error("Topic ownership conflict");
25 const groups = mockProjectGroups(parent.kind === "global_folder" ? "global" : "project", parent.root || "");
26 const organized = groups.some(group => group.topicIds?.includes(target.topicId));
27 const disposition = node.session ? "archive_sessions" : state(tree).automatic.has(target.topicId) && !node.pinned && !organized ? "discard_placeholder" : "archive_placeholder";
28 return { target: { ...target, workspaceId }, disposition, allowed: !node.running, reason: node.running ? "busy" : undefined, token: JSON.stringify([node, groups, disposition]) };
29 }
30 export function remove(tree: ProjectNode[], request: TopicRemovalRequest): TopicRemovalResult {
31 const saved = state(tree), encoded = JSON.stringify(request), previous = saved.receipts.get(request.operationId);
32 const conflict = { committed: false, disposition: "", retryable: false, errorCode: "state_conflict" };
33 if (previous) return previous.request === encoded ? previous.result : conflict;
34 const view = inspect(tree, request.target);
35 if (!view.allowed || view.token !== request.expectedToken) return conflict;
36 const { parent, node, order, workspaceId } = owner(tree, request.target.topicId);
37 const result: TopicRemovalResult = { committed: true, disposition: view.disposition, retryable: false };
38 const receipt: Receipt = { request: encoded, result, parent, node: structuredClone(node), order };
39 if (view.disposition !== "discard_placeholder") {
40 result.recoveryEntryId = `topic-removal:${request.operationId}`;
41 receipt.entry = { id: result.recoveryEntryId, recoveryEntryId: result.recoveryEntryId, workspaceId, workspaceTitle: parent.label, title: node.label,
42 archivedAt: Date.now(), health: "ready", canPreview: false, canRestore: true, canPurge: true };
43 }
44 parent.children!.splice(order, 1);
45 saved.receipts.set(request.operationId, receipt); saved.generation++;
46 notifyMockProjectTreeChanged();
47 return result;
48 }
49 export function trash(tree: ProjectNode[]) {
50 const saved = state(tree);
51 return { generation: saved.generation, items: [...saved.receipts.values()].flatMap(receipt => receipt.entry ? [receipt.entry] : []) };
52 }
53 export function recover(tree: ProjectNode[], id: string, workspaceId: string, action: string) {
54 const saved = state(tree), receipt = saved.receipts.get(id.replace(/^topic-removal:/, ""));
55 if (!receipt?.entry || receipt.entry.workspaceId !== workspaceId) throw new Error("Recovery conflict");
56 if (action === "restore") {
57 if (!tree.includes(receipt.parent) || tree.some(parent => parent.children?.some(node => node.topicId === receipt.node.topicId))) throw new Error("Restore conflict");
58 (receipt.parent.children ??= []).splice(receipt.order, 0, structuredClone(receipt.node));
59 } else if (action !== "purge") throw new Error("Invalid metadata lifecycle action");
60 receipt.entry = undefined; saved.generation++;
61 notifyMockProjectTreeChanged();
62 }
63
63 lines TYPESCRIPT