返回 DeepSeek-Reasonix
controllerModelCommands.ts
根目录 / desktop / frontend / src / lib / controllerModelCommands.ts
1 import { app } from "./bridge";
2 import type { BalanceInfo } from "./types";
3
4 type Ref<T> = { current: T };
5 type Ports = {
6 statesRef: Ref<ReadonlyMap<string, { balance?: BalanceInfo }>>;
7 modelSwitchSeqByTab: Ref<Map<string, number>>;
8 modelSwitchSuccessVersionByTab: Ref<Map<string, number>>;
9 modelSwitchQueueByTab: Ref<ReadonlyMap<string, { fallbackBalance?: BalanceInfo }>>;
10 enqueueModelSwitch: (tabId: string, name: string, balance?: BalanceInfo) => Promise<"applied" | "superseded">;
11 clearBalanceForTab: (tabId: string) => void;
12 dispatchTo: (tabId: string, action: { type: "local_notice"; level: "warn"; text: string } | { type: "balance"; balance: BalanceInfo }) => void;
13 refreshBalanceForTab: (tabId: string) => Promise<unknown>;
14 refreshMetaForTab: (tabId: string) => Promise<void>;
15 };
16
17 /** Uses Controller-owned queues and stores; never resolves an active tab after await. */
18 export function createControllerModelCommands(ports: Ports) {
19 const { statesRef, modelSwitchSeqByTab, modelSwitchSuccessVersionByTab, modelSwitchQueueByTab,
20 enqueueModelSwitch, clearBalanceForTab, dispatchTo, refreshBalanceForTab, refreshMetaForTab } = ports;
21 const setModelForTab = async (tabId: string, name: string) => {
22 if (!tabId) return false;
23 const switchSeq = (modelSwitchSeqByTab.current.get(tabId) ?? 0) + 1;
24 const successVersion = modelSwitchSuccessVersionByTab.current.get(tabId) ?? 0;
25 const existingQueue = modelSwitchQueueByTab.current.get(tabId);
26 // Every attempt in one queued burst shares the balance that was visible
27 // before the first switch cleared it. Otherwise a later queued failure
28 // captures the placeholder and cannot restore the outgoing provider.
29 const fallbackBalance = existingQueue
30 ? existingQueue.fallbackBalance
31 : statesRef.current.get(tabId)?.balance;
32 modelSwitchSeqByTab.current.set(tabId, switchSeq);
33 // Hide the outgoing provider's wallet as soon as the user starts a hot
34 // switch. If the rebuild fails, the catch path re-queries the still-active
35 // provider and restores its balance.
36 clearBalanceForTab(tabId);
37 try {
38 const result = await enqueueModelSwitch(tabId, name, fallbackBalance);
39 if (result === "superseded") return false;
40 modelSwitchSuccessVersionByTab.current.set(
41 tabId,
42 (modelSwitchSuccessVersionByTab.current.get(tabId) ?? 0) + 1,
43 );
44 } catch (err) {
45 if (modelSwitchSeqByTab.current.get(tabId) !== switchSeq) return false;
46 const { modelSwitchNoticeText } = await import("./controllerSwitchNotices");
47 if (modelSwitchSeqByTab.current.get(tabId) !== switchSeq) return false;
48 dispatchTo(tabId, { type: "local_notice", level: "warn", text: modelSwitchNoticeText(err) });
49 const olderSwitchSucceeded =
50 (modelSwitchSuccessVersionByTab.current.get(tabId) ?? 0) !== successVersion;
51 // Restore the known balance only when no older overlapping switch
52 // completed after this attempt began. Otherwise the backend now owns a
53 // different provider and the refresh below must establish its balance.
54 if (fallbackBalance && !olderSwitchSucceeded) {
55 dispatchTo(tabId, { type: "balance", balance: fallbackBalance });
56 }
57 void refreshBalanceForTab(tabId);
58 // A superseded success deliberately skips its own UI reconciliation.
59 // If this latest queued switch then fails, reconcile the model metadata
60 // to the provider that actually became active in the backend.
61 if (olderSwitchSucceeded) await refreshMetaForTab(tabId);
62 return false;
63 }
64 if (modelSwitchSeqByTab.current.get(tabId) !== switchSeq) return false;
65 void refreshBalanceForTab(tabId);
66 await refreshMetaForTab(tabId);
67 return modelSwitchSeqByTab.current.get(tabId) === switchSeq;
68 };
69
70 const setEffortForTab = async (tabId: string, level: string) => {
71 if (!tabId) return;
72 try {
73 await app.SetEffortForTab(tabId, level);
74 } catch (err) {
75 const { effortSwitchNoticeText } = await import("./controllerSwitchNotices");
76 dispatchTo(tabId, { type: "local_notice", level: "warn", text: effortSwitchNoticeText(err) });
77 return;
78 }
79 await refreshMetaForTab(tabId);
80 };
81
82
83 return { setModelForTab, setEffortForTab };
84 }
85
85 lines TYPESCRIPT