返回 DeepSeek-Reasonix
remoteTelemetry.ts
根目录 / desktop / frontend / src / lib / remoteTelemetry.ts
1 import type { BalanceInfo, ContextInfo, CostQuote, JobView, WireUsage } from "./types";
2 import type { State } from "./useController";
3 import { app } from "./bridge";
4
5 type RemoteTelemetryStatus = {
6 used?: unknown;
7 window?: unknown;
8 cacheHit?: unknown;
9 cacheMiss?: unknown;
10 lastUsage?: unknown;
11 balance?: unknown;
12 sessionCostQuote?: unknown;
13 jobs?: unknown;
14 };
15
16 async function retryRemote<T>(load: () => Promise<T>, maxAttempts: number, cancelled: () => boolean): Promise<T | undefined> {
17 for (let attempt = 0; attempt < maxAttempts && !cancelled(); attempt++) {
18 try {
19 return await load();
20 } catch (error) {
21 if (attempt + 1 === maxAttempts) throw error;
22 await new Promise<void>((resolve) => setTimeout(resolve, 500));
23 }
24 }
25 }
26
27 export function loadRemoteStatusSnapshot(tabId: string, maxAttempts: number, cancelled: () => boolean, valid: (status: unknown) => boolean, metadataOnly = false) {
28 return retryRemote(async () => {
29 const snapshot = metadataOnly && app.RemoteTabMetadata ? await app.RemoteTabMetadata(tabId) : await app.RemoteTabSnapshot(tabId);
30 const status = snapshot.status ?? await app.RemoteTabStatus(tabId);
31 if (!valid(status)) throw new Error("remote status is incomplete");
32 return [snapshot, status] as const;
33 }, maxAttempts, cancelled);
34 }
35
36 function finiteNumber(value: unknown): number | undefined {
37 return typeof value === "number" && Number.isFinite(value) ? value : undefined;
38 }
39
40 function remoteUsage(value: unknown): WireUsage | undefined {
41 if (!value || typeof value !== "object" || Array.isArray(value)) return undefined;
42 const raw = value as Record<string, unknown>;
43 const number = (camel: string, legacy: string) => finiteNumber(raw[camel] ?? raw[legacy]) ?? 0;
44 return {
45 promptTokens: number("promptTokens", "PromptTokens"),
46 completionTokens: number("completionTokens", "CompletionTokens"),
47 totalTokens: number("totalTokens", "TotalTokens"),
48 cacheHitTokens: number("cacheHitTokens", "CacheHitTokens"),
49 cacheMissTokens: number("cacheMissTokens", "CacheMissTokens"),
50 reasoningTokens: number("reasoningTokens", "ReasoningTokens"),
51 estimated: raw.estimated === true || raw.Estimated === true,
52 sessionCacheHitTokens: number("sessionCacheHitTokens", "SessionCacheHitTokens"),
53 sessionCacheMissTokens: number("sessionCacheMissTokens", "SessionCacheMissTokens"),
54 contextPromptTokens: number("contextPromptTokens", "ContextPromptTokens") || undefined,
55 contextCompletionTokens: number("contextCompletionTokens", "ContextCompletionTokens") || undefined,
56 contextReasoningTokens: number("contextReasoningTokens", "ContextReasoningTokens") || undefined,
57 contextCacheHitTokens: number("contextCacheHitTokens", "ContextCacheHitTokens") || undefined,
58 contextCacheMissTokens: number("contextCacheMissTokens", "ContextCacheMissTokens") || undefined,
59 };
60 }
61
62 function remoteBalance(value: unknown): BalanceInfo | undefined {
63 if (!value || typeof value !== "object" || Array.isArray(value)) return undefined;
64 const raw = value as Record<string, unknown>;
65 return {
66 available: raw.available === true,
67 display: typeof raw.display === "string" ? raw.display : "",
68 detail: typeof raw.detail === "string" ? raw.detail : undefined,
69 complete: raw.complete === true,
70 primaryCurrency: typeof raw.primaryCurrency === "string" ? raw.primaryCurrency : undefined,
71 costDisplayCurrency: typeof raw.costDisplayCurrency === "string" ? raw.costDisplayCurrency : undefined,
72 multiCurrency: raw.multiCurrency === true,
73 };
74 }
75
76 function remoteJobs(value: unknown): JobView[] {
77 if (!Array.isArray(value)) return [];
78 return value.flatMap((entry) => {
79 if (!entry || typeof entry !== "object" || Array.isArray(entry)) return [];
80 const raw = entry as Record<string, unknown>;
81 if (typeof raw.id !== "string" || raw.id.trim() === "") return [];
82 return [{
83 id: raw.id,
84 kind: typeof raw.kind === "string" ? raw.kind : "task",
85 label: typeof raw.label === "string" ? raw.label : raw.id,
86 status: typeof raw.status === "string" ? raw.status : "running",
87 startedAt: finiteNumber(raw.startedAt) ?? 0,
88 }];
89 });
90 }
91
92 export function hydrateRemoteTelemetry(state: State, status: unknown): State {
93 const raw = (status ?? {}) as RemoteTelemetryStatus;
94 const lastUsage = remoteUsage(raw.lastUsage);
95 const quote = raw.sessionCostQuote && typeof raw.sessionCostQuote === "object" && !Array.isArray(raw.sessionCostQuote)
96 ? raw.sessionCostQuote as CostQuote
97 : undefined;
98 const selectedAmount = quote?.selected ? Number.parseFloat(quote.selected.amount) : 0;
99 const cacheHit = finiteNumber(raw.cacheHit) ?? lastUsage?.sessionCacheHitTokens ?? 0;
100 const cacheMiss = finiteNumber(raw.cacheMiss) ?? lastUsage?.sessionCacheMissTokens ?? 0;
101 const context: ContextInfo = {
102 used: Math.max(0, finiteNumber(raw.used) ?? 0),
103 window: Math.max(0, finiteNumber(raw.window) ?? 0),
104 sessionTokens: Math.max(0, cacheHit + cacheMiss, lastUsage?.totalTokens ?? 0),
105 cacheHitTokens: Math.max(0, cacheHit),
106 cacheMissTokens: Math.max(0, cacheMiss),
107 sessionCost: Number.isFinite(selectedAmount) ? Math.max(0, selectedAmount) : 0,
108 sessionCurrency: quote?.selected?.currency ?? "",
109 sessionCostComplete: quote?.costComplete === true,
110 sessionCostQuote: quote,
111 estimated: quote?.estimated === true || lastUsage?.estimated === true,
112 };
113 return {
114 ...state,
115 context,
116 balance: remoteBalance(raw.balance),
117 jobs: remoteJobs(raw.jobs),
118 usage: lastUsage,
119 sessionTokens: context.sessionTokens ?? 0,
120 sessionCost: context.sessionCost ?? 0,
121 sessionCurrency: context.sessionCurrency ?? "",
122 lastTurnOutputTokens: lastUsage ? Math.max(0, lastUsage.completionTokens) : 0,
123 lastTurnOutputEstimated: lastUsage?.estimated === true,
124 };
125 }
126
126 lines TYPESCRIPT