返回 DeepSeek-Reasonix
recoveryStore.ts
根目录 / desktop / electron / src / main / browser / recoveryStore.ts
1 import { mkdirSync, readFileSync, renameSync, writeFileSync, rmSync } from "node:fs";
2 import { dirname } from "node:path";
3 import type { BrowserViewport } from "./viewport.js";
4 import { validateViewport } from "./viewport.js";
5
6 export interface FileReference { source: "workspace" | "presented" | "reference"; path: string; toolCallId: string }
7 export function validFileReference(value: unknown): value is FileReference { const item = value as FileReference | undefined; return !!item && ["workspace", "presented", "reference"].includes(item.source) && typeof item.path === "string" && item.path.length > 0 && item.path.length <= 8192 && typeof item.toolCallId === "string" && item.toolCallId.length <= 512; }
8 export interface RecoveredTab { id: string; taskId: string; sessionId: string; url: string; title: string; viewport: BrowserViewport | null; fileReference?: FileReference; [key: string]: unknown }
9 const prohibited = ["grant", "grantId", "webContentsId", "documentToken", "refs", "observationToken", "pendingOperation", "form", "password"];
10 export function recoverableURL(value: string): boolean {
11 if (value.length > 8192) return false;
12 try { const url = new URL(value); return ["http:", "https:"].includes(url.protocol) && !url.username && !url.password && !url.pathname.startsWith("/__reasonix_workspace_media/"); }
13 catch { return false; }
14 }
15 export class BrowserRecoveryStore {
16 private document: Record<string, unknown> = { version: 1, tabs: [] };
17 private writable = true;
18 private lastSaved = "";
19 readonly tabs: RecoveredTab[] = [];
20 constructor(private readonly path: string, private readonly warn: (message: string) => void) {
21 try {
22 const data = readFileSync(path, "utf8");
23 if (data.length > 256 * 1024) throw new Error("recovery metadata exceeds budget");
24 const document = JSON.parse(data) as Record<string, unknown>;
25 if (document.version !== 1 || !Array.isArray(document.tabs)) { this.writable = false; return; }
26 this.document = document;
27 const ids = new Set<string>();
28 for (const candidate of document.tabs.slice(0, 32)) {
29 const tab = candidate as RecoveredTab;
30 if (!tab || !/^tab-\d+$/.test(tab.id) || !Number.isSafeInteger(Number(tab.id.slice(4))) || ids.has(tab.id) || typeof tab.taskId !== "string" || typeof tab.sessionId !== "string" || typeof tab.url !== "string" || (!recoverableURL(tab.url) && !validFileReference(tab.fileReference))) continue;
31 let viewport: BrowserViewport | null = null;
32 try { viewport = tab.viewport ? validateViewport(tab.viewport) : null; } catch { continue; }
33 ids.add(tab.id);
34 this.tabs.push({ ...tab, title: typeof tab.title === "string" ? tab.title.slice(0, 512) : "", viewport });
35 }
36 } catch (error) {
37 if ((error as NodeJS.ErrnoException).code !== "ENOENT") { this.writable = false; warn("Browser tab recovery metadata is unreadable; preserving the original file"); }
38 }
39 }
40 save(tabs: RecoveredTab[]): void {
41 if (!this.writable) return;
42 const previous = new Map(this.tabs.map(tab => [tab.id, tab]));
43 const rows = tabs.filter(tab => recoverableURL(tab.url) || validFileReference(tab.fileReference)).slice(0, 32).map(tab => {
44 const row: RecoveredTab = { ...previous.get(tab.id), ...tab, title: tab.title.slice(0, 512) };
45 if (row.fileReference) row.url = "";
46 for (const key of prohibited) delete row[key];
47 return row;
48 });
49 const temporary = `${this.path}.tmp`;
50 try {
51 const serialized = JSON.stringify({ ...this.document, version: 1, tabs: rows });
52 if (serialized === this.lastSaved) return;
53 if (Buffer.byteLength(serialized) > 256 * 1024) throw new Error("recovery metadata exceeds budget");
54 mkdirSync(dirname(this.path), { recursive: true });
55 writeFileSync(temporary, serialized, { mode: 0o600 });
56 renameSync(temporary, this.path);
57 this.lastSaved = serialized;
58 } catch { rmSync(temporary, { force: true }); this.warn("Browser tab recovery metadata could not be saved"); }
59 }
60 }
61
61 lines TYPESCRIPT