返回 DeepSeek-Reasonix
diagnosticExport.ts
根目录 / desktop / electron / src / main / browser / diagnosticExport.ts
1 import type { BrowserSurfaceManager, BrowserTab } from "./surfaceManager.js";
2 import type { BrowserGrant, GrantRegistry } from "./grants.js";
3 import type { BrowserRequestTrace } from "./diagnosticContext.js";
4 import { diagnosticText, diagnosticURL, type DiagnosticBuffer, type PageDiagnostic } from "./diagnostics.js";
5
6 interface Entry { scope: string; sequence: number; time: number; kind: string; [key: string]: unknown }
7 interface Tracked { scope: string; state: string; tab: BrowserTab; buffer?: DiagnosticBuffer; off?: () => void }
8 const LIMIT = 512, BYTE_LIMIT = 256 * 1024;
9
10 // One bounded application-wide window, independent of tab/grant lifetime.
11 // Attribution is supplied by the trusted Go host, never by page content.
12 export class BrowserDiagnosticExport {
13 private entries: Entry[] = [];
14 private bytes = 0;
15 private sequence = 0;
16 private dropped = 0;
17 private readonly startedAt = Date.now();
18 private readonly tracked = new Map<string, Tracked>();
19 private readonly stop: (() => void)[];
20 constructor(private surfaces: BrowserSurfaceManager, private grants: GrantRegistry, private metadata: { build: string; version: string; platform: string }) {
21 this.stop = [surfaces.subscribe(() => this.sync()), grants.onRevoke(grant => {
22 this.add(grant.diagnosticScope ?? "", "lifecycle", { event: "grant_revoked", taskId: grant.taskId });
23 })];
24 }
25 bind(grant: BrowserGrant): void {
26 if (!grant.diagnosticScope || !/^[a-f0-9]{64}$/.test(grant.diagnosticScope)) return;
27 this.add(grant.diagnosticScope, "lifecycle", { event: "grant_installed", taskId: grant.taskId, runtimeGeneration: grant.generation });
28 this.sync();
29 }
30 trace(scope: string, event: BrowserRequestTrace): void {
31 const identifier = (value?: string) => value && /^[\w./-]{1,160}$/.test(value) ? value : undefined;
32 this.add(scope, "host", { requestId: identifier(event.requestId), operationId: identifier(event.operationId), tabId: identifier(event.tabId), method: identifier(event.method), phase: identifier(event.phase), elapsedMs: event.elapsedMs, errorKind: identifier(event.errorKind), command: identifier(event.command), target: identifier(event.target) });
33 }
34 private add(scope: string, kind: string, fields: Record<string, unknown>): void {
35 if (!scope) return;
36 const item: Entry = { ...fields, scope, kind, sequence: ++this.sequence, time: Date.now() };
37 this.entries.push(item);
38 this.bytes += Buffer.byteLength(JSON.stringify(item));
39 while (this.entries.length > LIMIT || this.bytes > BYTE_LIMIT) {
40 this.bytes -= Buffer.byteLength(JSON.stringify(this.entries.shift()!));
41 this.dropped++;
42 }
43 }
44 private page(scope: string, tabId: string, entry: PageDiagnostic): void {
45 this.add(scope, "page", { tabId, pageSequence: entry.sequence, pageTime: entry.time, category: entry.kind, message: diagnosticText(entry.message), url: entry.url ? diagnosticURL(entry.url) : undefined, status: entry.status });
46 }
47 private sync(): void {
48 const alive = new Set<string>();
49 for (const tab of this.surfaces.all()) {
50 alive.add(tab.id);
51 let record = this.tracked.get(tab.id);
52 const scope = record?.scope ?? this.grants.diagnosticScopeForTab(tab.taskId, tab.sessionId);
53 if (!scope) continue;
54 if (!record) { record = { scope, state: "", tab }; this.tracked.set(tab.id, record); }
55 const state = { tabId: tab.id, taskId: tab.taskId, documentEpoch: tab.epoch, lifecycleEpoch: tab.lifecycleEpoch, viewportRevision: tab.viewportRevision, viewport: tab.viewport, mode: tab.mode, loading: tab.loading, crashes: tab.crashes, errorCode: tab.error?.code, active: this.surfaces.activeTabId === tab.id, webContentsId: tab.view.page.id };
56 const serialized = JSON.stringify(state);
57 if (serialized !== record.state) {
58 this.add(scope, "lifecycle", { event: record.state ? "tab_changed" : "tab_opened", ...state });
59 record.state = serialized;
60 }
61 const buffer = tab.view.diagnostics;
62 if (buffer !== record.buffer) {
63 record.off?.(); record.buffer = buffer;
64 if (buffer) {
65 const snapshot = buffer.read();
66 this.add(scope, "lifecycle", { event: "page_diagnostics_attached", tabId: tab.id, truncated: snapshot.truncated });
67 for (const item of snapshot.entries) this.page(scope, tab.id, item);
68 record.off = buffer.subscribe(item => this.page(scope, tab.id, item));
69 }
70 }
71 }
72 for (const [id, record] of this.tracked) {
73 if (!alive.has(id)) {
74 record.off?.(); this.tracked.delete(id);
75 this.add(record.scope, "lifecycle", { event: "tab_closed", tabId: id });
76 }
77 }
78 }
79 read(scope: string) {
80 if (!/^[a-f0-9]{64}$/.test(scope)) throw new Error("invalid diagnostic scope");
81 this.sync();
82 const entries = this.entries.filter(entry => entry.scope === scope).map(({ scope: _scope, ...entry }) => ({ ...entry }));
83 return {
84 available: true, scope, schemaVersion: 1, ...this.metadata, startedAt: this.startedAt, capturedAt: Date.now(),
85 sessionEvidenceAvailable: entries.length > 0,
86 retention: "In-memory application window; cleared on shell restart. Truncation may be caused by other sessions. Events without explicit attribution are omitted.",
87 truncated: this.dropped > 0, limit: LIMIT, byteLimit: BYTE_LIMIT,
88 tabs: [...this.tracked.values()].filter(row => row.scope === scope).map(row => ({ tabId: row.tab.id, pageDiagnosticsAvailable: Boolean(row.buffer) })),
89 entries,
90 };
91 }
92 dispose(): void {
93 for (const off of this.stop) off();
94 for (const row of this.tracked.values()) row.off?.();
95 this.tracked.clear(); this.entries = []; this.bytes = 0;
96 }
97 }
98
98 lines TYPESCRIPT