返回 DeepSeek-Reasonix
handshake.ts
根目录 / desktop / electron / src / main / handshake.ts
1 import { RpcError } from "./rpc.js";
2
3 export const DEFAULT_PROTOCOL_VERSION = 11;
4
5 export const HANDSHAKE_CODES = {
6 protocol_mismatch: -32001,
7 not_ready: -32002,
8 contract_mismatch: -32003,
9 build_mismatch: -32004,
10 instance_mismatch: -32005,
11 } as const;
12
13 export interface HelloIdentity {
14 protocolVersion: number;
15 contractDigest: string;
16 version: string;
17 channel: string;
18 commit: string;
19 hostVersion: string;
20 chromeVersion: string;
21 platform: string;
22 arch: string;
23 home: string;
24 dev: boolean;
25 }
26
27 export interface HelloParams {
28 protocolVersion: number;
29 contractDigest: string;
30 build: { version: string; channel: string; commit: string };
31 host: { name: "electron"; version: string; chrome: string; platform: string; arch: string };
32 instance: { home: string; dev: boolean };
33 }
34
35 export interface HelloWindow {
36 position?: { x: number; y: number };
37 width: number;
38 height: number;
39 minWidth: number;
40 minHeight: number;
41 frameless: boolean;
42 zoomFactor: number;
43 }
44
45 export interface HelloResult {
46 protocolVersion: number;
47 contractDigest: string;
48 service: { version: string; channel: string; commit: string; pid: number };
49 runtimeGeneration: string;
50 runId: string;
51 incidentId: string;
52 diagnosticsEnabled: boolean;
53 resources: { origin: string; token: string };
54 window: HelloWindow;
55 instance?: { identityVersion: number; identityDigest: string; legacyId: string };
56 }
57
58 export interface HandshakeFailure {
59 code: number | null;
60 name: string;
61 title: string;
62 detail: string;
63 }
64
65 export class HandshakeError extends Error {
66 constructor(message: string) {
67 super(message);
68 this.name = "HandshakeError";
69 }
70 }
71
72 export function buildHelloParams(identity: HelloIdentity): HelloParams {
73 return {
74 protocolVersion: identity.protocolVersion,
75 contractDigest: identity.contractDigest,
76 build: { version: identity.version, channel: identity.channel, commit: identity.commit },
77 host: {
78 name: "electron",
79 version: identity.hostVersion,
80 chrome: identity.chromeVersion,
81 platform: identity.platform,
82 arch: identity.arch,
83 },
84 instance: { home: identity.home, dev: identity.dev },
85 };
86 }
87
88 function record(value: unknown, path: string): Record<string, unknown> {
89 if (typeof value !== "object" || value === null || Array.isArray(value)) {
90 throw new HandshakeError(`hello result: ${path} must be an object`);
91 }
92 return value as Record<string, unknown>;
93 }
94
95 function str(source: Record<string, unknown>, key: string, path: string, allowEmpty = false): string {
96 const value = source[key];
97 if (typeof value !== "string" || (!allowEmpty && value === "")) {
98 throw new HandshakeError(`hello result: ${path}.${key} must be a non-empty string`);
99 }
100 return value;
101 }
102
103 function num(source: Record<string, unknown>, key: string, path: string): number {
104 const value = source[key];
105 if (typeof value !== "number" || !Number.isFinite(value)) {
106 throw new HandshakeError(`hello result: ${path}.${key} must be a finite number`);
107 }
108 return value;
109 }
110
111 export function validateHelloResult(value: unknown, expectedProtocolVersion = DEFAULT_PROTOCOL_VERSION): HelloResult {
112 const root = record(value, "result");
113 const protocolVersion = num(root, "protocolVersion", "result");
114 if (protocolVersion !== expectedProtocolVersion) {
115 throw new HandshakeError(`hello result: protocolVersion ${protocolVersion} differs from ${expectedProtocolVersion}`);
116 }
117 const service = record(root.service, "result.service");
118 const resources = record(root.resources, "result.resources");
119 const window = record(root.window, "result.window");
120 const geometry: HelloWindow = {
121 width: num(window, "width", "result.window"),
122 height: num(window, "height", "result.window"),
123 minWidth: num(window, "minWidth", "result.window"),
124 minHeight: num(window, "minHeight", "result.window"),
125 frameless: window.frameless === true,
126 zoomFactor: typeof window.zoomFactor === "number" && window.zoomFactor > 0 ? window.zoomFactor : 1,
127 };
128 if (geometry.width < 1 || geometry.height < 1 || geometry.minWidth < 1 || geometry.minHeight < 1) {
129 throw new HandshakeError("hello result: window geometry must be positive");
130 }
131 if (window.position !== undefined) {
132 const position = record(window.position, "result.window.position");
133 geometry.position = { x: num(position, "x", "result.window.position"), y: num(position, "y", "result.window.position") };
134 }
135 let instance: HelloResult["instance"];
136 if (root.instance !== undefined) {
137 const rawInstance = record(root.instance, "result.instance");
138 const identityVersion = num(rawInstance, "identityVersion", "result.instance");
139 if (!Number.isInteger(identityVersion) || identityVersion < 1) {
140 throw new HandshakeError("hello result: result.instance.identityVersion must be a positive integer");
141 }
142 instance = {
143 identityVersion,
144 identityDigest: str(rawInstance, "identityDigest", "result.instance"),
145 legacyId: str(rawInstance, "legacyId", "result.instance"),
146 };
147 }
148 return {
149 protocolVersion,
150 contractDigest: str(root, "contractDigest", "result"),
151 service: {
152 version: str(service, "version", "result.service", true),
153 channel: str(service, "channel", "result.service", true),
154 commit: str(service, "commit", "result.service", true),
155 pid: num(service, "pid", "result.service"),
156 },
157 runtimeGeneration: str(root, "runtimeGeneration", "result"),
158 runId: str(root, "runId", "result", true),
159 incidentId: str(root, "incidentId", "result", true),
160 diagnosticsEnabled: root.diagnosticsEnabled === true,
161 resources: { origin: str(resources, "origin", "result.resources"), token: str(resources, "token", "result.resources") },
162 window: geometry,
163 instance,
164 };
165 }
166
167 const FAILURE_TITLES: Record<number, { name: string; title: string }> = {
168 [HANDSHAKE_CODES.protocol_mismatch]: { name: "protocol_mismatch", title: "The desktop service speaks a different protocol version" },
169 [HANDSHAKE_CODES.not_ready]: { name: "not_ready", title: "The desktop service was not ready for the handshake" },
170 [HANDSHAKE_CODES.contract_mismatch]: { name: "contract_mismatch", title: "Mixed installation: the shell and the service disagree on the command contract" },
171 [HANDSHAKE_CODES.build_mismatch]: { name: "build_mismatch", title: "The shell and the desktop service are different builds" },
172 [HANDSHAKE_CODES.instance_mismatch]: { name: "instance_mismatch", title: "The shell and the desktop service use different data homes" },
173 };
174
175 export function describeHandshakeFailure(error: unknown): HandshakeFailure {
176 if (error instanceof RpcError) {
177 const known = FAILURE_TITLES[error.code];
178 if (known) return { code: error.code, name: known.name, title: known.title, detail: error.message };
179 return { code: error.code, name: "rpc_error", title: "The desktop service rejected the handshake", detail: error.message };
180 }
181 if (error instanceof HandshakeError) {
182 return { code: null, name: "invalid_result", title: "The desktop service returned an invalid handshake", detail: error.message };
183 }
184 const detail = error instanceof Error ? error.message : String(error);
185 return { code: null, name: "service_failure", title: "The desktop service could not be started", detail };
186 }
187
187 lines TYPESCRIPT