@morlay/dsh-desktop-shell 0.2.5 → 0.2.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -4
- package/dist/{appconfig-BZ9wPROk.mjs → appconfig-CSZh3JG7.mjs} +2 -2
- package/dist/appconfig.d.mts +21 -0
- package/dist/appconfig.mjs +1 -2
- package/dist/{dshhome-Dp89FOr-.mjs → dshhome-DAJ9dkn2.mjs} +2 -2
- package/dist/dshhome.d.mts +6 -0
- package/dist/dshhome.mjs +1 -2
- package/dist/index.d.mts +6 -0
- package/dist/index.mjs +1050 -139
- package/dist/inline-packages.d.mts +14 -0
- package/dist/inline-packages.mjs +21 -0
- package/dist/official-BmgBsdhq.mjs +5 -0
- package/dist/official.d.mts +4 -0
- package/dist/official.mjs +1 -2
- package/dist/preload-app.cjs +5 -1
- package/dist/profile-project.d.mts +20 -0
- package/dist/profile-project.mjs +1 -2
- package/dist/{seed-BF71UAuI.mjs → seed-Bxr0BsCk.mjs} +52 -5
- package/dist/seed.d.mts +21 -0
- package/dist/seed.mjs +1 -2
- package/package.json +42 -21
- package/src/__tests__/developer-endpoint.spec.ts +17 -0
- package/src/__tests__/document-marks.spec.ts +42 -0
- package/src/__tests__/dshhome.spec.ts +108 -0
- package/src/__tests__/host-process.spec.ts +205 -0
- package/src/__tests__/ipc.spec.ts +39 -0
- package/src/__tests__/profile-project.spec.ts +204 -0
- package/src/__tests__/seed-profile.spec.ts +189 -0
- package/src/__tests__/shell-env.spec.ts +131 -0
- package/src/__tests__/shell-import-boundary.spec.ts +69 -0
- package/src/__tests__/stream-uplink.spec.ts +49 -0
- package/src/appconfig.ts +79 -0
- package/src/developer-endpoint.ts +30 -0
- package/src/developer-menu.ts +198 -0
- package/src/directory-picker.ts +37 -0
- package/src/document-marks.ts +58 -0
- package/src/dshhome.ts +29 -0
- package/src/electron.ts +33 -0
- package/src/host-process.ts +533 -0
- package/src/index.ts +716 -0
- package/src/inline-packages.ts +18 -0
- package/src/ipc.ts +58 -0
- package/src/keybindings.ts +35 -0
- package/src/official.ts +8 -0
- package/src/preload-app.ts +147 -0
- package/src/profile-project.ts +138 -0
- package/src/seed.ts +183 -0
- package/src/shell-env.ts +31 -0
- package/src/shortcuts.ts +236 -0
- package/src/stream-uplink.ts +41 -0
- package/dist/official-Cz9Mry1n.mjs +0 -167
package/dist/index.mjs
CHANGED
|
@@ -1,16 +1,42 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import { a as
|
|
5
|
-
import {
|
|
6
|
-
import { access, mkdir, readFile, rename, rm, writeFile } from "node:fs/promises";
|
|
1
|
+
import { t as DESKTOP_HOST_PACKAGE } from "./official-BmgBsdhq.mjs";
|
|
2
|
+
import { r as loadAppConfig, t as PROFILE_NAME } from "./appconfig-CSZh3JG7.mjs";
|
|
3
|
+
import { n as resolveDshHome } from "./dshhome-DAJ9dkn2.mjs";
|
|
4
|
+
import { a as ensureSeedProfile, i as SEED_RUNTIME_DIR_NAME } from "./seed-Bxr0BsCk.mjs";
|
|
5
|
+
import { createRequire } from "node:module";
|
|
6
|
+
import { access, appendFile, mkdir, readFile, rename, rm, writeFile } from "node:fs/promises";
|
|
7
7
|
import { basename, dirname, join, resolve } from "node:path";
|
|
8
|
-
import {
|
|
9
|
-
import
|
|
8
|
+
import { format } from "node:util";
|
|
9
|
+
import "rolldown";
|
|
10
10
|
import { spawn } from "node:child_process";
|
|
11
11
|
import { once } from "node:events";
|
|
12
|
-
import { Readable, Writable } from "node:stream";
|
|
13
12
|
import { randomBytes } from "node:crypto";
|
|
13
|
+
//#region src/electron.ts
|
|
14
|
+
/**
|
|
15
|
+
* `electron` 模块的**唯一入口**(壳自己的其它文件都从这里取)。
|
|
16
|
+
*
|
|
17
|
+
* 壳产物是 **ESM**(`.mjs`,Electron 28+ 支持 ESM 入口),而 `electron` 在 ESM 下**只有默认导出**:具名导入
|
|
18
|
+
* 会抛 `SyntaxError: The requested module 'electron' does not provide an export named 'BrowserWindow'`,
|
|
19
|
+
* 主进程在 `load` 阶段就没了。更麻烦的是**打包器会把"默认导入 + 解构 / 取属性"优化回具名导入**——那样又回到
|
|
20
|
+
* 同一个错误。
|
|
21
|
+
*
|
|
22
|
+
* 所以这里**走运行期 `require`**:壳就是 Electron 主进程,`require("electron")` 正是它给这个模块的原生形态,
|
|
23
|
+
* 打包器碰不到它。类型仍从 `electron` 取(类型导入会被擦除,不影响运行)。
|
|
24
|
+
*/
|
|
25
|
+
const electron = createRequire(import.meta.url)("electron");
|
|
26
|
+
const app = electron.app;
|
|
27
|
+
const BrowserWindow = electron.BrowserWindow;
|
|
28
|
+
const dialog = electron.dialog;
|
|
29
|
+
const ipcMain = electron.ipcMain;
|
|
30
|
+
const nativeTheme = electron.nativeTheme;
|
|
31
|
+
const protocol = electron.protocol;
|
|
32
|
+
const Menu = electron.Menu;
|
|
33
|
+
const MenuItem = electron.MenuItem;
|
|
34
|
+
//#endregion
|
|
35
|
+
//#region ../dsh-desktop-host/src/preload-source.ts
|
|
36
|
+
/** 壳取 preload 的路径(`GET` 它得到可执行的 CJS 字节)。 */
|
|
37
|
+
const PRELOAD_SOURCE_PATH = "/@dsh/preload.cjs";
|
|
38
|
+
join("dist", "preload.cjs");
|
|
39
|
+
//#endregion
|
|
14
40
|
//#region src/ipc.ts
|
|
15
41
|
const DESKTOP_IPC = {
|
|
16
42
|
directoryPick: "dsh-desktop:directory-pick",
|
|
@@ -61,6 +87,179 @@ function installDesktopDirectoryPicker(getWindow, scheme) {
|
|
|
61
87
|
return result;
|
|
62
88
|
});
|
|
63
89
|
}
|
|
90
|
+
//#endregion
|
|
91
|
+
//#region ../dsh-desktop-host/src/paths.ts
|
|
92
|
+
/** 应用 origin 下托管调试窗口前端产物的前缀。 */
|
|
93
|
+
const DEVTOOLS_ASSETS_PREFIX = "/node-devtools";
|
|
94
|
+
//#endregion
|
|
95
|
+
//#region src/developer-endpoint.ts
|
|
96
|
+
/** DevTools 前端在托管前缀下的入口路径(与上游底部面板用的是同一份产物)。 */
|
|
97
|
+
const DEVTOOLS_ENTRY = "devtools_app.html";
|
|
98
|
+
/**
|
|
99
|
+
* 把 host 回报的调试端点拼成应用 origin 下的 DevTools 入口。
|
|
100
|
+
* @param scheme - 应用自定义协议(壳派生自 app 名)。
|
|
101
|
+
* @param endpoint - host 回报的 WebSocket 端点(`ws://127.0.0.1:<port>/<uuid>`)。
|
|
102
|
+
* @returns 可在窗口里加载的入口 URL。
|
|
103
|
+
* @throws 端点不是 WebSocket 端点,或它的值不能原样进查询串。
|
|
104
|
+
*/
|
|
105
|
+
function nodeInspectorDevtoolsUrl(scheme, endpoint) {
|
|
106
|
+
const target = new URL(endpoint);
|
|
107
|
+
if (target.protocol !== "ws:" && target.protocol !== "wss:") throw new Error(`dsh desktop: the inspector endpoint is not a WebSocket url: ${endpoint}`);
|
|
108
|
+
if (target.search !== "") throw new Error(`dsh desktop: the inspector endpoint must not carry a query: ${endpoint}`);
|
|
109
|
+
const socket = `${target.host}${target.pathname}`;
|
|
110
|
+
if (!/^[\u0021-\u007e]+$/u.test(socket) || socket.includes("&") || socket.includes("#")) throw new Error(`dsh desktop: the inspector endpoint is not usable in a query: ${endpoint}`);
|
|
111
|
+
return `${scheme}://app${DEVTOOLS_ASSETS_PREFIX}/${DEVTOOLS_ENTRY}?ws=${socket}&panel=console&disableLocaleInfoBar=true`;
|
|
112
|
+
}
|
|
113
|
+
//#endregion
|
|
114
|
+
//#region src/developer-menu.ts
|
|
115
|
+
function reportFailure(options, title, detail) {
|
|
116
|
+
console.error(`[dsh-shell] ${title}: ${detail}`);
|
|
117
|
+
const parent = options.mainWindow();
|
|
118
|
+
if (parent === void 0 || parent.isDestroyed()) dialog.showErrorBox(title, detail);
|
|
119
|
+
else dialog.showMessageBox(parent, {
|
|
120
|
+
type: "error",
|
|
121
|
+
message: title,
|
|
122
|
+
detail
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
function insertIntoViewMenu(menu, toggle) {
|
|
126
|
+
const view = menu?.items.find((entry) => entry.role === "viewMenu")?.submenu;
|
|
127
|
+
if (view === void 0) return false;
|
|
128
|
+
const devtools = view.items.findIndex((entry) => entry.role === "toggleDevTools");
|
|
129
|
+
const at = devtools === -1 ? view.items.length : devtools + 1;
|
|
130
|
+
view.insert(at, new MenuItem({ type: "separator" }));
|
|
131
|
+
view.insert(at + 1, new MenuItem(toggle));
|
|
132
|
+
return true;
|
|
133
|
+
}
|
|
134
|
+
function buildFallbackMenu(toggle) {
|
|
135
|
+
const template = [
|
|
136
|
+
...process.platform === "darwin" ? [{ role: "appMenu" }] : [],
|
|
137
|
+
{ role: "fileMenu" },
|
|
138
|
+
{ role: "editMenu" },
|
|
139
|
+
{
|
|
140
|
+
label: "View",
|
|
141
|
+
submenu: [
|
|
142
|
+
{ role: "reload" },
|
|
143
|
+
{ role: "forceReload" },
|
|
144
|
+
{ role: "toggleDevTools" },
|
|
145
|
+
{ type: "separator" },
|
|
146
|
+
toggle,
|
|
147
|
+
{ type: "separator" },
|
|
148
|
+
{ role: "resetZoom" },
|
|
149
|
+
{ role: "zoomIn" },
|
|
150
|
+
{ role: "zoomOut" },
|
|
151
|
+
{ type: "separator" },
|
|
152
|
+
{ role: "togglefullscreen" }
|
|
153
|
+
]
|
|
154
|
+
},
|
|
155
|
+
{ role: "windowMenu" }
|
|
156
|
+
];
|
|
157
|
+
return Menu.buildFromTemplate(template);
|
|
158
|
+
}
|
|
159
|
+
/**
|
|
160
|
+
* 把「后端调试(Node Inspector)」挂进应用菜单的 View 里,并管理它那个 DevTools 窗口。
|
|
161
|
+
* 勾选 ⇔ 端点开着 ⇔ 窗口在:关窗口、取消勾选、host 回报端点没了,三条都收敛到同一状态。
|
|
162
|
+
* @param options - 协议、app 名与 host 取值函数。
|
|
163
|
+
* @returns 退订与收尾。
|
|
164
|
+
*/
|
|
165
|
+
function installDeveloperMenu(options) {
|
|
166
|
+
let window;
|
|
167
|
+
let closingWindow = false;
|
|
168
|
+
let releaseInspected;
|
|
169
|
+
let toggleItem;
|
|
170
|
+
const setChecked = (checked) => {
|
|
171
|
+
if (toggleItem !== void 0) toggleItem.checked = checked;
|
|
172
|
+
};
|
|
173
|
+
function closeWindow() {
|
|
174
|
+
const current = window;
|
|
175
|
+
window = void 0;
|
|
176
|
+
if (current === void 0 || current.isDestroyed()) return;
|
|
177
|
+
closingWindow = true;
|
|
178
|
+
current.close();
|
|
179
|
+
}
|
|
180
|
+
function closeInspectorHost() {
|
|
181
|
+
releaseInspected?.();
|
|
182
|
+
releaseInspected = void 0;
|
|
183
|
+
closeWindow();
|
|
184
|
+
try {
|
|
185
|
+
options.host()?.endInspect();
|
|
186
|
+
} catch (error) {
|
|
187
|
+
console.error("[dsh-shell] host inspector shutdown failed", error);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
function openWindow(endpoint) {
|
|
191
|
+
const current = window;
|
|
192
|
+
if (current !== void 0 && !current.isDestroyed()) {
|
|
193
|
+
current.focus();
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
let target;
|
|
197
|
+
try {
|
|
198
|
+
target = nodeInspectorDevtoolsUrl(options.scheme, endpoint);
|
|
199
|
+
} catch (error) {
|
|
200
|
+
setChecked(false);
|
|
201
|
+
releaseInspected?.();
|
|
202
|
+
releaseInspected = void 0;
|
|
203
|
+
reportFailure(options, "后端调试", error instanceof Error ? error.message : String(error));
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
const created = new BrowserWindow({
|
|
207
|
+
width: 1280,
|
|
208
|
+
height: 820,
|
|
209
|
+
title: `${options.appName} · Node Inspector`,
|
|
210
|
+
backgroundColor: "#202124"
|
|
211
|
+
});
|
|
212
|
+
window = created;
|
|
213
|
+
created.on("closed", () => {
|
|
214
|
+
if (window === created) window = void 0;
|
|
215
|
+
const closingOurselves = closingWindow;
|
|
216
|
+
closingWindow = false;
|
|
217
|
+
if (closingOurselves) return;
|
|
218
|
+
closeInspectorHost();
|
|
219
|
+
setChecked(false);
|
|
220
|
+
});
|
|
221
|
+
created.loadURL(target);
|
|
222
|
+
}
|
|
223
|
+
const toggle = {
|
|
224
|
+
id: "desktop-host-inspector",
|
|
225
|
+
label: "后端调试(Node Inspector)",
|
|
226
|
+
type: "checkbox",
|
|
227
|
+
checked: false,
|
|
228
|
+
click: (menu) => {
|
|
229
|
+
toggleItem = menu;
|
|
230
|
+
if (!menu.checked) {
|
|
231
|
+
closeInspectorHost();
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
234
|
+
const host = options.host();
|
|
235
|
+
if (host === void 0) {
|
|
236
|
+
setChecked(false);
|
|
237
|
+
reportFailure(options, "后端调试", "后端进程还没就绪,稍后再试。");
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
240
|
+
releaseInspected?.();
|
|
241
|
+
releaseInspected = host.onInspected((state) => {
|
|
242
|
+
if (state.url !== null) {
|
|
243
|
+
openWindow(state.url);
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
246
|
+
setChecked(false);
|
|
247
|
+
closeWindow();
|
|
248
|
+
if (state.message !== void 0) reportFailure(options, "后端调试端点没有开起来", state.message);
|
|
249
|
+
});
|
|
250
|
+
try {
|
|
251
|
+
host.inspect(0);
|
|
252
|
+
} catch (error) {
|
|
253
|
+
releaseInspected?.();
|
|
254
|
+
releaseInspected = void 0;
|
|
255
|
+
setChecked(false);
|
|
256
|
+
reportFailure(options, "后端调试", error instanceof Error ? error.message : String(error));
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
};
|
|
260
|
+
if (!insertIntoViewMenu(Menu.getApplicationMenu(), toggle)) Menu.setApplicationMenu(buildFallbackMenu(toggle));
|
|
261
|
+
return { dispose: closeInspectorHost };
|
|
262
|
+
}
|
|
64
263
|
const DESKTOP_STREAM_PATH = "/.dsh/remote-stream";
|
|
65
264
|
function encodeDesktopStreamOpen(endpoint, payload) {
|
|
66
265
|
return `${JSON.stringify({
|
|
@@ -206,6 +405,7 @@ function isDesktopHostEvent(value) {
|
|
|
206
405
|
if (!isRecord(value)) return false;
|
|
207
406
|
if (value.type === "ready") return value.protocolVersion === 1;
|
|
208
407
|
if (value.type === "fatal") return typeof value.message === "string" && (value.diagnostic === void 0 || typeof value.diagnostic === "string");
|
|
408
|
+
if (value.type === "inspected") return (value.url === null || typeof value.url === "string") && (value.message === void 0 || typeof value.message === "string");
|
|
209
409
|
return false;
|
|
210
410
|
}
|
|
211
411
|
//#endregion
|
|
@@ -230,7 +430,7 @@ async function exitsWithin(exit, milliseconds) {
|
|
|
230
430
|
var DesktopHostProcess = class {
|
|
231
431
|
node;
|
|
232
432
|
runtimeDir;
|
|
233
|
-
|
|
433
|
+
profileDir;
|
|
234
434
|
inspectPort;
|
|
235
435
|
options;
|
|
236
436
|
child;
|
|
@@ -241,6 +441,7 @@ var DesktopHostProcess = class {
|
|
|
241
441
|
nextStreamId = 1;
|
|
242
442
|
pending = /* @__PURE__ */ new Map();
|
|
243
443
|
blockedResponses = /* @__PURE__ */ new Set();
|
|
444
|
+
inspectedListeners = /* @__PURE__ */ new Set();
|
|
244
445
|
readyResolve;
|
|
245
446
|
readyReject;
|
|
246
447
|
readyPromise = new Promise((resolve, reject) => {
|
|
@@ -251,16 +452,16 @@ var DesktopHostProcess = class {
|
|
|
251
452
|
stderr = "";
|
|
252
453
|
failureReported = false;
|
|
253
454
|
stopping = false;
|
|
254
|
-
constructor(node, runtimeDir,
|
|
455
|
+
constructor(node, runtimeDir, profileDir, inspectPort, options) {
|
|
255
456
|
this.node = node;
|
|
256
457
|
this.runtimeDir = runtimeDir;
|
|
257
|
-
this.
|
|
458
|
+
this.profileDir = profileDir;
|
|
258
459
|
this.inspectPort = inspectPort;
|
|
259
460
|
this.options = options;
|
|
260
461
|
}
|
|
261
462
|
async start() {
|
|
262
463
|
if (this.child !== void 0) return this.readyPromise;
|
|
263
|
-
const entry =
|
|
464
|
+
const entry = this.options.entry;
|
|
264
465
|
const packageManager = this.options.packageManager;
|
|
265
466
|
const args = [
|
|
266
467
|
"--expose-internals",
|
|
@@ -268,58 +469,46 @@ var DesktopHostProcess = class {
|
|
|
268
469
|
...this.inspectPort === void 0 ? [] : [`--inspect=127.0.0.1:${String(this.inspectPort)}`],
|
|
269
470
|
...this.options.nodeArgs ?? [],
|
|
270
471
|
entry,
|
|
472
|
+
"--profile-dir",
|
|
473
|
+
this.profileDir,
|
|
474
|
+
"--runtime-dir",
|
|
271
475
|
this.runtimeDir,
|
|
272
|
-
|
|
273
|
-
|
|
476
|
+
...packageManager === void 0 ? [] : [
|
|
477
|
+
"--pnpm",
|
|
478
|
+
packageManager.pnpm,
|
|
479
|
+
"--node-bin",
|
|
480
|
+
packageManager.nodeBin
|
|
481
|
+
]
|
|
274
482
|
];
|
|
275
483
|
const env = {
|
|
276
484
|
...Object.fromEntries(Object.entries(process.env).filter(([name]) => name !== "NODE_OPTIONS" && !name.startsWith("DSH_DESKTOP_") && !/^(?:npm|pnpm|corepack)_/iu.test(name))),
|
|
277
485
|
...this.options.extraEnv
|
|
278
486
|
};
|
|
279
487
|
const child = (this.options.spawn ?? spawn)(this.node, args, {
|
|
280
|
-
cwd: this.
|
|
488
|
+
cwd: this.profileDir,
|
|
281
489
|
env,
|
|
282
490
|
stdio: [
|
|
283
491
|
"ignore",
|
|
284
492
|
"pipe",
|
|
285
493
|
"pipe",
|
|
286
|
-
"pipe",
|
|
287
|
-
"pipe",
|
|
288
494
|
"ipc"
|
|
289
495
|
]
|
|
290
496
|
});
|
|
291
|
-
const requestPipe = child.stdio[3];
|
|
292
|
-
const responsePipe = child.stdio[4];
|
|
293
|
-
if (!(requestPipe instanceof Writable) || !(responsePipe instanceof Readable)) {
|
|
294
|
-
child.kill("SIGTERM");
|
|
295
|
-
throw new Error("dsh desktop host did not expose the required byte pipes and IPC channel");
|
|
296
|
-
}
|
|
297
497
|
this.child = child;
|
|
298
|
-
|
|
299
|
-
|
|
498
|
+
const persist = (chunk) => {
|
|
499
|
+
if (this.options.logFile === void 0) return;
|
|
500
|
+
appendFile(this.options.logFile, chunk).catch(() => void 0);
|
|
501
|
+
};
|
|
300
502
|
child.stderr?.setEncoding("utf8");
|
|
301
503
|
child.stderr?.on("data", (chunk) => {
|
|
302
504
|
this.stderr = (this.stderr + chunk).slice(-65536);
|
|
303
505
|
process.stderr.write(`[dsh-host] ${chunk}`);
|
|
506
|
+
persist(chunk);
|
|
304
507
|
});
|
|
305
|
-
child.stdout?.
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
responsePipe.once("end", () => {
|
|
310
|
-
try {
|
|
311
|
-
this.responseDecoder.finish();
|
|
312
|
-
} catch (error) {
|
|
313
|
-
this.fail(errorOf(error, "dsh desktop host response pipe failed"));
|
|
314
|
-
return;
|
|
315
|
-
}
|
|
316
|
-
this.fail(/* @__PURE__ */ new Error("dsh desktop host response pipe ended"));
|
|
317
|
-
});
|
|
318
|
-
requestPipe.once("error", (error) => {
|
|
319
|
-
this.fail(error);
|
|
320
|
-
});
|
|
321
|
-
responsePipe.once("error", (error) => {
|
|
322
|
-
this.fail(error);
|
|
508
|
+
child.stdout?.setEncoding("utf8");
|
|
509
|
+
child.stdout?.on("data", (chunk) => {
|
|
510
|
+
process.stdout.write(chunk);
|
|
511
|
+
persist(chunk);
|
|
323
512
|
});
|
|
324
513
|
child.on("message", (message) => {
|
|
325
514
|
if (!isDesktopHostEvent(message)) {
|
|
@@ -332,7 +521,14 @@ var DesktopHostProcess = class {
|
|
|
332
521
|
this.fail(new Error(message.message));
|
|
333
522
|
return;
|
|
334
523
|
}
|
|
335
|
-
|
|
524
|
+
if (message.type === "inspected") {
|
|
525
|
+
for (const listener of this.inspectedListeners) listener(message);
|
|
526
|
+
return;
|
|
527
|
+
}
|
|
528
|
+
this.readyResolve({
|
|
529
|
+
protocolVersion: message.protocolVersion,
|
|
530
|
+
origin: message.origin
|
|
531
|
+
});
|
|
336
532
|
});
|
|
337
533
|
child.once("error", (error) => {
|
|
338
534
|
this.fail(error);
|
|
@@ -388,6 +584,21 @@ var DesktopHostProcess = class {
|
|
|
388
584
|
});
|
|
389
585
|
});
|
|
390
586
|
}
|
|
587
|
+
inspect(port = 0) {
|
|
588
|
+
this.send({
|
|
589
|
+
type: "inspect",
|
|
590
|
+
port
|
|
591
|
+
});
|
|
592
|
+
}
|
|
593
|
+
endInspect() {
|
|
594
|
+
this.send({ type: "inspect-off" });
|
|
595
|
+
}
|
|
596
|
+
onInspected(listener) {
|
|
597
|
+
this.inspectedListeners.add(listener);
|
|
598
|
+
return () => {
|
|
599
|
+
this.inspectedListeners.delete(listener);
|
|
600
|
+
};
|
|
601
|
+
}
|
|
391
602
|
async stop() {
|
|
392
603
|
const child = this.child;
|
|
393
604
|
if (child === void 0) return;
|
|
@@ -592,33 +803,7 @@ var DesktopHostProcess = class {
|
|
|
592
803
|
}
|
|
593
804
|
};
|
|
594
805
|
//#endregion
|
|
595
|
-
//#region ../../../vendor/deepseek-harness/packages/
|
|
596
|
-
/** Duplicate-install-safe JSON and immutable-value helpers. @module @deepseek-ai/dsh-util-values */
|
|
597
|
-
/**
|
|
598
|
-
* Mark an unreachable closed-union branch.
|
|
599
|
-
* @param value - impossible value; an unhandled typed variant fails at the call site.
|
|
600
|
-
* @param context - optional switch-site label included in the failure message.
|
|
601
|
-
* @returns never; a runtime value that escaped its type always throws.
|
|
602
|
-
*/
|
|
603
|
-
function assertNever(value, context) {
|
|
604
|
-
const rendered = JSON.stringify(value) ?? String(value);
|
|
605
|
-
throw new Error(`unreachable variant${context ? ` in ${context}` : ""}: ${rendered}`);
|
|
606
|
-
}
|
|
607
|
-
//#endregion
|
|
608
|
-
//#region ../../../vendor/deepseek-harness/packages/util/crypto/lib/index.js
|
|
609
|
-
/**
|
|
610
|
-
* Random v4 UUID, minted from `crypto.getRandomValues`.
|
|
611
|
-
* @returns the UUID string.
|
|
612
|
-
*/
|
|
613
|
-
function randomUUID() {
|
|
614
|
-
const bytes = globalThis.crypto.getRandomValues(/* @__PURE__ */ new Uint8Array(16));
|
|
615
|
-
const hex = Array.from(bytes, (byte, index) => {
|
|
616
|
-
return (index === 6 ? byte & 15 | 64 : index === 8 ? byte & 63 | 128 : byte).toString(16).padStart(2, "0");
|
|
617
|
-
}).join("");
|
|
618
|
-
return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
|
|
619
|
-
}
|
|
620
|
-
//#endregion
|
|
621
|
-
//#region ../../../vendor/deepseek-harness/packages/client/shortcuts/lib/protocol.js
|
|
806
|
+
//#region ../../../vendor/deepseek-harness/packages/client/shortcuts/src/binding.ts
|
|
622
807
|
const keyNames = {
|
|
623
808
|
Slash: "/",
|
|
624
809
|
Comma: ",",
|
|
@@ -704,6 +889,644 @@ function isWebBindingAllowed(binding, platform) {
|
|
|
704
889
|
}
|
|
705
890
|
].some((candidate) => bindingKey(binding) === bindingKey(normalizeBinding(candidate, platform)));
|
|
706
891
|
}
|
|
892
|
+
//#endregion
|
|
893
|
+
//#region ../../../vendor/deepseek-harness/packages/util/values/src/partial-json.ts
|
|
894
|
+
/**
|
|
895
|
+
* Lazily scanned view of one JSON object's top-level fields, built from text
|
|
896
|
+
* that may still be streaming or from an already parsed object. Nothing is
|
|
897
|
+
* scanned until a reader asks; the view remembers every question it answered
|
|
898
|
+
* and reports changed answers when the owner refreshes for publication.
|
|
899
|
+
* Used for model tool-call arguments: a row reads the fields it
|
|
900
|
+
* cares about at whatever granularity it displays, at every stage of the call.
|
|
901
|
+
* @module @deepseek-ai/dsh-util-values/src/partial-json
|
|
902
|
+
*/
|
|
903
|
+
const SIMPLE_ESCAPES = {
|
|
904
|
+
"\"": "\"",
|
|
905
|
+
"\\": "\\",
|
|
906
|
+
"/": "/",
|
|
907
|
+
b: "\b",
|
|
908
|
+
f: "\f",
|
|
909
|
+
n: "\n",
|
|
910
|
+
r: "\r",
|
|
911
|
+
t: " "
|
|
912
|
+
};
|
|
913
|
+
const CONTENT_ESCAPE = /[\\\u0000-\u001f]/u;
|
|
914
|
+
function isWhitespace(c) {
|
|
915
|
+
return c === " " || c === "\n" || c === "\r" || c === " ";
|
|
916
|
+
}
|
|
917
|
+
function isHex(c) {
|
|
918
|
+
return c >= "0" && c <= "9" || c >= "a" && c <= "f" || c >= "A" && c <= "F";
|
|
919
|
+
}
|
|
920
|
+
(class PartialArguments {
|
|
921
|
+
/** The view of a call with no arguments available. */
|
|
922
|
+
static EMPTY = PartialArguments.fromObject({});
|
|
923
|
+
/**
|
|
924
|
+
* View finished argument text without scanning it until a reader asks.
|
|
925
|
+
* @param text - the complete argument JSON text.
|
|
926
|
+
* @returns a sealed view.
|
|
927
|
+
*/
|
|
928
|
+
static fromText(text) {
|
|
929
|
+
const view = new PartialArguments();
|
|
930
|
+
view.append(text);
|
|
931
|
+
view.sealed = true;
|
|
932
|
+
return view;
|
|
933
|
+
}
|
|
934
|
+
/**
|
|
935
|
+
* View an already parsed argument payload, such as a PTC dispatch object.
|
|
936
|
+
* @param value - the parsed argument value.
|
|
937
|
+
* @returns a sealed view; a non-object payload has no fields.
|
|
938
|
+
*/
|
|
939
|
+
static fromObject(value) {
|
|
940
|
+
const view = new PartialArguments();
|
|
941
|
+
view.object = typeof value === "object" && value !== null && !Array.isArray(value) ? value : {};
|
|
942
|
+
view.sealed = true;
|
|
943
|
+
return view;
|
|
944
|
+
}
|
|
945
|
+
/**
|
|
946
|
+
* The source: text so far or a parsed object, plus whether it can still grow.
|
|
947
|
+
* These are the only enumerable fields, so two views over the same source
|
|
948
|
+
* compare equal structurally however far each has been read.
|
|
949
|
+
*/
|
|
950
|
+
chunks = [];
|
|
951
|
+
object;
|
|
952
|
+
sealed = false;
|
|
953
|
+
#ends = [];
|
|
954
|
+
#size = 0;
|
|
955
|
+
#consumed = 0;
|
|
956
|
+
#mode = "root";
|
|
957
|
+
#escape = false;
|
|
958
|
+
#keyStart = 0;
|
|
959
|
+
#keyEscaped = false;
|
|
960
|
+
#key = "";
|
|
961
|
+
#current = null;
|
|
962
|
+
#nestedEnds = [];
|
|
963
|
+
#nestedInString = false;
|
|
964
|
+
#invalidAt;
|
|
965
|
+
#invalidValue = false;
|
|
966
|
+
#entries = /* @__PURE__ */ new Map();
|
|
967
|
+
#order = [];
|
|
968
|
+
#reads = /* @__PURE__ */ new Map();
|
|
969
|
+
/** Whether this view rejects further appends; does not scan text or register reads. */
|
|
970
|
+
get isSealed() {
|
|
971
|
+
return this.sealed;
|
|
972
|
+
}
|
|
973
|
+
/** Whether indexing or a content read found invalid JSON; unread value contents are not validated. */
|
|
974
|
+
get invalid() {
|
|
975
|
+
this.scan();
|
|
976
|
+
return this.#mode === "invalid" || this.#invalidValue;
|
|
977
|
+
}
|
|
978
|
+
/**
|
|
979
|
+
* Retain streamed argument text without scanning or comparing observed answers.
|
|
980
|
+
* @param fragment - the text following every fragment appended before.
|
|
981
|
+
*/
|
|
982
|
+
append(fragment) {
|
|
983
|
+
if (this.sealed) throw new Error("PartialArguments: cannot append to a sealed view");
|
|
984
|
+
if (fragment.length === 0) return;
|
|
985
|
+
this.chunks.push(fragment);
|
|
986
|
+
this.#size += fragment.length;
|
|
987
|
+
this.#ends.push(this.#size);
|
|
988
|
+
}
|
|
989
|
+
/**
|
|
990
|
+
* Reconcile a streamed prefix with authoritative complete text without joining the fragments.
|
|
991
|
+
* @param text - the final argument text, which replaces missing or conflicting deltas.
|
|
992
|
+
* @returns this view sealed with its caches retained when every character matches; otherwise a new sealed view.
|
|
993
|
+
*/
|
|
994
|
+
settle(text) {
|
|
995
|
+
if (this.object !== void 0 || text.length !== this.#size) return PartialArguments.fromText(text);
|
|
996
|
+
let offset = 0;
|
|
997
|
+
for (const chunk of this.chunks) {
|
|
998
|
+
if (!text.startsWith(chunk, offset)) return PartialArguments.fromText(text);
|
|
999
|
+
offset += chunk.length;
|
|
1000
|
+
}
|
|
1001
|
+
this.chunks = text.length === 0 ? [] : [text];
|
|
1002
|
+
this.#ends = text.length === 0 ? [] : [text.length];
|
|
1003
|
+
this.sealed = true;
|
|
1004
|
+
return this;
|
|
1005
|
+
}
|
|
1006
|
+
/**
|
|
1007
|
+
* Compare observed answers and advance their publication baseline. Unread views remain unscanned.
|
|
1008
|
+
* @returns whether any observed answer changed since its first read or the preceding refresh.
|
|
1009
|
+
*/
|
|
1010
|
+
refresh() {
|
|
1011
|
+
if (this.#reads.size === 0) return false;
|
|
1012
|
+
this.scan();
|
|
1013
|
+
let changed = false;
|
|
1014
|
+
let completions = false;
|
|
1015
|
+
for (const read of this.#reads.values()) {
|
|
1016
|
+
if (read.completion) {
|
|
1017
|
+
completions = true;
|
|
1018
|
+
continue;
|
|
1019
|
+
}
|
|
1020
|
+
changed = this.refreshRead(read) || changed;
|
|
1021
|
+
}
|
|
1022
|
+
if (completions) {
|
|
1023
|
+
for (const read of this.#reads.values()) if (read.completion) changed = this.refreshRead(read) || changed;
|
|
1024
|
+
}
|
|
1025
|
+
if (this.sealed) this.#reads.clear();
|
|
1026
|
+
return changed;
|
|
1027
|
+
}
|
|
1028
|
+
refreshRead(read) {
|
|
1029
|
+
const now = read.answer();
|
|
1030
|
+
if (Object.is(now, read.last)) return false;
|
|
1031
|
+
read.last = now;
|
|
1032
|
+
return true;
|
|
1033
|
+
}
|
|
1034
|
+
/**
|
|
1035
|
+
* Check whether no further fields can arrive.
|
|
1036
|
+
* @returns whether the outer object closed, indexing failed, or the view is sealed; unread values are not validated.
|
|
1037
|
+
*/
|
|
1038
|
+
closed() {
|
|
1039
|
+
return this.remember("closed", "", () => this.closedNow());
|
|
1040
|
+
}
|
|
1041
|
+
/**
|
|
1042
|
+
* List discovered fields in first-appearance order.
|
|
1043
|
+
* @returns top-level keys seen so far, in first-appearance order.
|
|
1044
|
+
*/
|
|
1045
|
+
keys() {
|
|
1046
|
+
return this.remember("keys", "", () => this.keysNow(), (keys) => keys.length);
|
|
1047
|
+
}
|
|
1048
|
+
/**
|
|
1049
|
+
* Check whether a top-level field has appeared.
|
|
1050
|
+
* @param key - argument name.
|
|
1051
|
+
* @returns whether the field has appeared (a string opened or another value began).
|
|
1052
|
+
*/
|
|
1053
|
+
has(key) {
|
|
1054
|
+
return this.remember("has", key, () => this.hasNow(key));
|
|
1055
|
+
}
|
|
1056
|
+
/**
|
|
1057
|
+
* Check whether a field's closing delimiter has arrived, without validating its contents.
|
|
1058
|
+
* @param key - argument name.
|
|
1059
|
+
* @returns whether its delimiter arrived and no content reader has reported an error for this value.
|
|
1060
|
+
*/
|
|
1061
|
+
complete(key) {
|
|
1062
|
+
return this.remember("complete", key, () => this.completeNow(key));
|
|
1063
|
+
}
|
|
1064
|
+
/**
|
|
1065
|
+
* Read string length without materializing its text.
|
|
1066
|
+
* @param key - argument name.
|
|
1067
|
+
* @param options - change granularity for a streaming string.
|
|
1068
|
+
* @returns decoded UTF-16 length of the string field so far; undefined when absent or not a string.
|
|
1069
|
+
*/
|
|
1070
|
+
stringLength(key, options) {
|
|
1071
|
+
const step = Math.max(1, Math.floor(options?.step ?? 1));
|
|
1072
|
+
const offset = options?.offset ?? 0;
|
|
1073
|
+
return this.remember(`length:${step}:${offset}`, key, () => this.lengthNow(key), (length) => length === void 0 ? void 0 : Math.ceil((length + offset) / step));
|
|
1074
|
+
}
|
|
1075
|
+
/**
|
|
1076
|
+
* Check a string against a decoded UTF-16 length limit without materializing it.
|
|
1077
|
+
* @param key - argument name.
|
|
1078
|
+
* @param maxLength - decoded UTF-16 limit, floored to at least zero.
|
|
1079
|
+
* @returns whether the string is longer than the limit; false when absent or not a string.
|
|
1080
|
+
*/
|
|
1081
|
+
stringExceeds(key, maxLength) {
|
|
1082
|
+
const limit = Math.max(0, Math.floor(maxLength));
|
|
1083
|
+
return this.remember(`exceeds:${limit}`, key, () => (this.lengthNow(key, limit + 1) ?? 0) > limit);
|
|
1084
|
+
}
|
|
1085
|
+
/**
|
|
1086
|
+
* Read a decoded string, including a streaming prefix.
|
|
1087
|
+
* @param key - argument name.
|
|
1088
|
+
* @returns the string field's decoded text so far; undefined when absent or not a string.
|
|
1089
|
+
*/
|
|
1090
|
+
text(key) {
|
|
1091
|
+
return this.remember("text", key, () => this.textNow(key));
|
|
1092
|
+
}
|
|
1093
|
+
/**
|
|
1094
|
+
* Read at most the first decoded UTF-16 units of a string.
|
|
1095
|
+
* @param key - argument name.
|
|
1096
|
+
* @param maxLength - maximum decoded UTF-16 length, floored to at least one.
|
|
1097
|
+
* @returns the bounded string prefix; undefined when absent or not a string.
|
|
1098
|
+
*/
|
|
1099
|
+
textPrefix(key, maxLength) {
|
|
1100
|
+
const limit = Math.max(1, Math.floor(maxLength));
|
|
1101
|
+
return this.remember(`prefix:${limit}`, key, () => this.textPrefixNow(key, limit));
|
|
1102
|
+
}
|
|
1103
|
+
/**
|
|
1104
|
+
* Read a completed non-string argument.
|
|
1105
|
+
* @param key - argument name.
|
|
1106
|
+
* @returns the parsed non-string value once it closed; undefined while open, absent, or a string.
|
|
1107
|
+
*/
|
|
1108
|
+
value(key) {
|
|
1109
|
+
return this.remember("value", key, () => this.valueNow(key));
|
|
1110
|
+
}
|
|
1111
|
+
/** Answer a question and, on a streaming view, remember it for change detection. */
|
|
1112
|
+
remember(kind, key, read, comparison) {
|
|
1113
|
+
this.scan();
|
|
1114
|
+
const result = read();
|
|
1115
|
+
if (!this.sealed) {
|
|
1116
|
+
const id = `${kind}/${key}`;
|
|
1117
|
+
if (!this.#reads.has(id)) this.#reads.set(id, {
|
|
1118
|
+
completion: kind === "complete",
|
|
1119
|
+
answer: comparison === void 0 ? read : () => comparison(read()),
|
|
1120
|
+
last: comparison === void 0 ? result : comparison(result)
|
|
1121
|
+
});
|
|
1122
|
+
}
|
|
1123
|
+
return result;
|
|
1124
|
+
}
|
|
1125
|
+
closedNow() {
|
|
1126
|
+
return this.sealed || this.#mode === "closed" || this.#mode === "invalid";
|
|
1127
|
+
}
|
|
1128
|
+
keysNow() {
|
|
1129
|
+
return this.object === void 0 ? this.#order : Object.keys(this.object);
|
|
1130
|
+
}
|
|
1131
|
+
hasNow(key) {
|
|
1132
|
+
return this.object === void 0 ? this.#entries.has(key) : Object.hasOwn(this.object, key);
|
|
1133
|
+
}
|
|
1134
|
+
completeNow(key) {
|
|
1135
|
+
if (this.object !== void 0) return Object.hasOwn(this.object, key);
|
|
1136
|
+
const entry = this.#entries.get(key);
|
|
1137
|
+
return entry !== void 0 && entry.end >= 0 && (entry.kind === "string" ? entry.invalidAt === void 0 : !entry.invalid);
|
|
1138
|
+
}
|
|
1139
|
+
lengthNow(key, limit = Number.POSITIVE_INFINITY) {
|
|
1140
|
+
if (this.object !== void 0) {
|
|
1141
|
+
const field = Object.hasOwn(this.object, key) ? this.object[key] : void 0;
|
|
1142
|
+
return typeof field === "string" ? field.length : void 0;
|
|
1143
|
+
}
|
|
1144
|
+
const entry = this.#entries.get(key);
|
|
1145
|
+
if (entry?.kind !== "string") return void 0;
|
|
1146
|
+
if (entry.text !== void 0 && entry.text.at === entry.end) return entry.text.length;
|
|
1147
|
+
const read = entry.length ??= {
|
|
1148
|
+
at: entry.start,
|
|
1149
|
+
length: 0,
|
|
1150
|
+
text: ""
|
|
1151
|
+
};
|
|
1152
|
+
this.readString(entry, read, limit, false);
|
|
1153
|
+
return read.length;
|
|
1154
|
+
}
|
|
1155
|
+
textNow(key) {
|
|
1156
|
+
if (this.object !== void 0) {
|
|
1157
|
+
const field = Object.hasOwn(this.object, key) ? this.object[key] : void 0;
|
|
1158
|
+
return typeof field === "string" ? field : void 0;
|
|
1159
|
+
}
|
|
1160
|
+
const entry = this.#entries.get(key);
|
|
1161
|
+
if (entry?.kind !== "string") return void 0;
|
|
1162
|
+
if (entry.text === void 0 && entry.end >= 0 && entry.needsDecoding && entry.invalidAt === void 0) {
|
|
1163
|
+
let text;
|
|
1164
|
+
try {
|
|
1165
|
+
text = JSON.parse(`"${this.slice(entry.start, entry.end)}"`);
|
|
1166
|
+
} catch (_error) {}
|
|
1167
|
+
if (text !== void 0) entry.text = {
|
|
1168
|
+
at: entry.end,
|
|
1169
|
+
length: text.length,
|
|
1170
|
+
text
|
|
1171
|
+
};
|
|
1172
|
+
}
|
|
1173
|
+
const read = entry.text ??= {
|
|
1174
|
+
at: entry.start,
|
|
1175
|
+
length: 0,
|
|
1176
|
+
text: ""
|
|
1177
|
+
};
|
|
1178
|
+
this.readString(entry, read, Number.POSITIVE_INFINITY, true);
|
|
1179
|
+
return read.text;
|
|
1180
|
+
}
|
|
1181
|
+
textPrefixNow(key, maxLength) {
|
|
1182
|
+
if (this.object !== void 0) {
|
|
1183
|
+
const field = Object.hasOwn(this.object, key) ? this.object[key] : void 0;
|
|
1184
|
+
return typeof field === "string" ? field.slice(0, maxLength) : void 0;
|
|
1185
|
+
}
|
|
1186
|
+
const entry = this.#entries.get(key);
|
|
1187
|
+
if (entry?.kind !== "string") return void 0;
|
|
1188
|
+
const prefixes = entry.prefixes ??= /* @__PURE__ */ new Map();
|
|
1189
|
+
let read = prefixes.get(maxLength);
|
|
1190
|
+
if (read === void 0) {
|
|
1191
|
+
read = {
|
|
1192
|
+
at: entry.start,
|
|
1193
|
+
length: 0,
|
|
1194
|
+
text: ""
|
|
1195
|
+
};
|
|
1196
|
+
prefixes.set(maxLength, read);
|
|
1197
|
+
}
|
|
1198
|
+
this.readString(entry, read, maxLength, true);
|
|
1199
|
+
return read.text;
|
|
1200
|
+
}
|
|
1201
|
+
valueNow(key) {
|
|
1202
|
+
if (this.object !== void 0) {
|
|
1203
|
+
if (!Object.hasOwn(this.object, key)) return void 0;
|
|
1204
|
+
const field = this.object[key];
|
|
1205
|
+
return typeof field === "string" ? void 0 : field;
|
|
1206
|
+
}
|
|
1207
|
+
const entry = this.#entries.get(key);
|
|
1208
|
+
if (entry?.kind !== "value" || entry.end < 0 || entry.invalid) return void 0;
|
|
1209
|
+
if (entry.parsed === void 0) try {
|
|
1210
|
+
entry.parsed = JSON.parse(this.slice(entry.start, entry.end));
|
|
1211
|
+
} catch (_error) {
|
|
1212
|
+
entry.invalid = true;
|
|
1213
|
+
this.#invalidValue = true;
|
|
1214
|
+
}
|
|
1215
|
+
return entry.parsed;
|
|
1216
|
+
}
|
|
1217
|
+
chunkAt(at) {
|
|
1218
|
+
let low = 0;
|
|
1219
|
+
let high = this.#ends.length;
|
|
1220
|
+
while (low < high) {
|
|
1221
|
+
const mid = low + high >>> 1;
|
|
1222
|
+
if (this.#ends[mid] <= at) low = mid + 1;
|
|
1223
|
+
else high = mid;
|
|
1224
|
+
}
|
|
1225
|
+
return low;
|
|
1226
|
+
}
|
|
1227
|
+
/** Materialize only a requested range, never the cumulative source. */
|
|
1228
|
+
slice(start, end) {
|
|
1229
|
+
if (start >= end) return "";
|
|
1230
|
+
const first = this.chunkAt(start);
|
|
1231
|
+
const last = this.chunkAt(end - 1);
|
|
1232
|
+
const base = first === 0 ? 0 : this.#ends[first - 1];
|
|
1233
|
+
if (first === last) return this.chunks[first].slice(start - base, end - base);
|
|
1234
|
+
const parts = [this.chunks[first].slice(start - base)];
|
|
1235
|
+
for (let i = first + 1; i < last; i++) parts.push(this.chunks[i]);
|
|
1236
|
+
parts.push(this.chunks[last].slice(0, end - this.#ends[last - 1]));
|
|
1237
|
+
return parts.join("");
|
|
1238
|
+
}
|
|
1239
|
+
readString(entry, read, limit, materialize) {
|
|
1240
|
+
const end = Math.min(entry.end < 0 ? this.#consumed : entry.end, entry.invalidAt ?? Number.POSITIVE_INFINITY, this.#invalidAt ?? Number.POSITIVE_INFINITY);
|
|
1241
|
+
if (!entry.needsDecoding) {
|
|
1242
|
+
const length = Math.min(end - read.at, limit - read.length);
|
|
1243
|
+
if (length <= 0) return;
|
|
1244
|
+
if (materialize) read.text += this.slice(read.at, read.at + length);
|
|
1245
|
+
read.at += length;
|
|
1246
|
+
read.length += length;
|
|
1247
|
+
return;
|
|
1248
|
+
}
|
|
1249
|
+
let chunkIndex = this.chunkAt(read.at);
|
|
1250
|
+
while (read.at < end && read.length < limit) {
|
|
1251
|
+
const base = chunkIndex === 0 ? 0 : this.#ends[chunkIndex - 1];
|
|
1252
|
+
const chunk = this.chunks[chunkIndex];
|
|
1253
|
+
const remaining = chunk.slice(read.at - base, Math.min(chunk.length, end - base));
|
|
1254
|
+
const boundary = remaining.search(CONTENT_ESCAPE);
|
|
1255
|
+
const length = Math.min(boundary < 0 ? remaining.length : boundary, limit - read.length);
|
|
1256
|
+
if (length > 0) {
|
|
1257
|
+
if (materialize) read.text += remaining.slice(0, length);
|
|
1258
|
+
read.at += length;
|
|
1259
|
+
read.length += length;
|
|
1260
|
+
if (read.at === base + chunk.length) chunkIndex++;
|
|
1261
|
+
continue;
|
|
1262
|
+
}
|
|
1263
|
+
const type = remaining.length > 1 ? remaining[1] : read.at + 1 < end ? this.chunks[chunkIndex + 1][0] : void 0;
|
|
1264
|
+
let decoded;
|
|
1265
|
+
let width = 2;
|
|
1266
|
+
if (remaining[0] === "\\" && type === void 0 && entry.end < 0) return;
|
|
1267
|
+
if (remaining[0] === "\\" && type === "u") {
|
|
1268
|
+
const hex = this.slice(read.at + 2, Math.min(end, read.at + 6));
|
|
1269
|
+
let valid = true;
|
|
1270
|
+
for (let i = 0; i < hex.length; i++) if (!isHex(hex[i])) valid = false;
|
|
1271
|
+
if (valid) {
|
|
1272
|
+
if (hex.length < 4 && entry.end < 0) return;
|
|
1273
|
+
if (hex.length === 4) decoded = String.fromCharCode(Number.parseInt(hex, 16));
|
|
1274
|
+
}
|
|
1275
|
+
width = 6;
|
|
1276
|
+
} else if (remaining[0] === "\\" && type !== void 0) decoded = SIMPLE_ESCAPES[type];
|
|
1277
|
+
if (decoded === void 0) {
|
|
1278
|
+
entry.invalidAt = read.at;
|
|
1279
|
+
this.#invalidValue = true;
|
|
1280
|
+
return;
|
|
1281
|
+
}
|
|
1282
|
+
if (materialize) read.text += decoded;
|
|
1283
|
+
read.length++;
|
|
1284
|
+
read.at += width;
|
|
1285
|
+
while (chunkIndex < this.chunks.length && read.at >= this.#ends[chunkIndex]) chunkIndex++;
|
|
1286
|
+
}
|
|
1287
|
+
}
|
|
1288
|
+
/** Locate new field ranges without decoding or parsing their contents. */
|
|
1289
|
+
scan() {
|
|
1290
|
+
if (this.object !== void 0 || this.#consumed === this.#size) return;
|
|
1291
|
+
for (let i = this.chunkAt(this.#consumed); i < this.chunks.length && this.#invalidAt === void 0; i++) {
|
|
1292
|
+
const pending = this.chunks[i];
|
|
1293
|
+
const base = i === 0 ? 0 : this.#ends[i - 1];
|
|
1294
|
+
for (let index = this.#consumed - base; index < pending.length && this.#mode !== "invalid"; index++) {
|
|
1295
|
+
if (this.#mode === "string" || this.#mode === "nested" && this.#nestedInString) {
|
|
1296
|
+
const end = this.stringBoundary(pending, index);
|
|
1297
|
+
this.#consumed += end - index;
|
|
1298
|
+
index = end;
|
|
1299
|
+
if (index === pending.length) break;
|
|
1300
|
+
}
|
|
1301
|
+
this.step(pending[index], this.#consumed);
|
|
1302
|
+
this.#consumed++;
|
|
1303
|
+
}
|
|
1304
|
+
}
|
|
1305
|
+
}
|
|
1306
|
+
/** Only raw quotes and their preceding backslash runs can terminate a string. */
|
|
1307
|
+
stringBoundary(fragment, start) {
|
|
1308
|
+
let at = start;
|
|
1309
|
+
while (true) {
|
|
1310
|
+
const quote = fragment.indexOf("\"", at);
|
|
1311
|
+
const end = quote < 0 ? fragment.length : quote;
|
|
1312
|
+
if (this.#mode === "string") {
|
|
1313
|
+
const entry = this.#current;
|
|
1314
|
+
if (!entry.needsDecoding && CONTENT_ESCAPE.test(fragment.slice(at, end))) entry.needsDecoding = true;
|
|
1315
|
+
}
|
|
1316
|
+
let slashStart = end;
|
|
1317
|
+
while (slashStart > at && fragment[slashStart - 1] === "\\") slashStart--;
|
|
1318
|
+
const escaped = (end - slashStart) % 2 === 1 !== (slashStart === at && this.#escape);
|
|
1319
|
+
this.#escape = quote < 0 && escaped;
|
|
1320
|
+
if (quote < 0 || !escaped) return end;
|
|
1321
|
+
at = quote + 1;
|
|
1322
|
+
}
|
|
1323
|
+
}
|
|
1324
|
+
step(c, at) {
|
|
1325
|
+
switch (this.#mode) {
|
|
1326
|
+
case "root":
|
|
1327
|
+
if (isWhitespace(c)) return;
|
|
1328
|
+
if (c === "{") {
|
|
1329
|
+
this.#mode = "key-or-end";
|
|
1330
|
+
return;
|
|
1331
|
+
}
|
|
1332
|
+
this.fail();
|
|
1333
|
+
return;
|
|
1334
|
+
case "key-or-end":
|
|
1335
|
+
if (isWhitespace(c)) return;
|
|
1336
|
+
if (c === "}") {
|
|
1337
|
+
this.#mode = "closed";
|
|
1338
|
+
return;
|
|
1339
|
+
}
|
|
1340
|
+
if (c === "\"") {
|
|
1341
|
+
this.beginKey(at);
|
|
1342
|
+
return;
|
|
1343
|
+
}
|
|
1344
|
+
this.fail();
|
|
1345
|
+
return;
|
|
1346
|
+
case "key-only":
|
|
1347
|
+
if (isWhitespace(c)) return;
|
|
1348
|
+
if (c === "\"") {
|
|
1349
|
+
this.beginKey(at);
|
|
1350
|
+
return;
|
|
1351
|
+
}
|
|
1352
|
+
this.fail();
|
|
1353
|
+
return;
|
|
1354
|
+
case "key":
|
|
1355
|
+
this.stepKey(c, at);
|
|
1356
|
+
return;
|
|
1357
|
+
case "colon":
|
|
1358
|
+
if (isWhitespace(c)) return;
|
|
1359
|
+
if (c === ":") {
|
|
1360
|
+
this.#mode = "value";
|
|
1361
|
+
return;
|
|
1362
|
+
}
|
|
1363
|
+
this.fail();
|
|
1364
|
+
return;
|
|
1365
|
+
case "value":
|
|
1366
|
+
this.beginValue(c, at);
|
|
1367
|
+
return;
|
|
1368
|
+
case "string": {
|
|
1369
|
+
const entry = this.#current;
|
|
1370
|
+
entry.end = at;
|
|
1371
|
+
this.#current = null;
|
|
1372
|
+
this.#mode = "comma-or-end";
|
|
1373
|
+
return;
|
|
1374
|
+
}
|
|
1375
|
+
case "scalar":
|
|
1376
|
+
this.stepScalar(c, at);
|
|
1377
|
+
return;
|
|
1378
|
+
case "nested":
|
|
1379
|
+
this.stepNested(c, at);
|
|
1380
|
+
return;
|
|
1381
|
+
case "comma-or-end":
|
|
1382
|
+
if (isWhitespace(c)) return;
|
|
1383
|
+
if (c === ",") {
|
|
1384
|
+
this.#mode = "key-only";
|
|
1385
|
+
return;
|
|
1386
|
+
}
|
|
1387
|
+
if (c === "}") {
|
|
1388
|
+
this.#mode = "closed";
|
|
1389
|
+
return;
|
|
1390
|
+
}
|
|
1391
|
+
this.fail();
|
|
1392
|
+
return;
|
|
1393
|
+
case "closed":
|
|
1394
|
+
if (isWhitespace(c)) return;
|
|
1395
|
+
this.fail();
|
|
1396
|
+
return;
|
|
1397
|
+
/* v8 ignore next 2 -- scan() stops stepping once the view is invalid. */
|
|
1398
|
+
case "invalid": return;
|
|
1399
|
+
/* v8 ignore next 2 -- Every scanner mode has a handler above. */
|
|
1400
|
+
default: assertNever(this.#mode);
|
|
1401
|
+
}
|
|
1402
|
+
}
|
|
1403
|
+
fail() {
|
|
1404
|
+
this.#invalidAt = this.#consumed;
|
|
1405
|
+
this.#mode = "invalid";
|
|
1406
|
+
this.#current = null;
|
|
1407
|
+
}
|
|
1408
|
+
beginKey(at) {
|
|
1409
|
+
this.#mode = "key";
|
|
1410
|
+
this.#keyStart = at + 1;
|
|
1411
|
+
this.#keyEscaped = false;
|
|
1412
|
+
this.#escape = false;
|
|
1413
|
+
}
|
|
1414
|
+
stepKey(c, at) {
|
|
1415
|
+
if (c < " ") {
|
|
1416
|
+
this.fail();
|
|
1417
|
+
return;
|
|
1418
|
+
}
|
|
1419
|
+
if (this.#escape) {
|
|
1420
|
+
this.#escape = false;
|
|
1421
|
+
return;
|
|
1422
|
+
}
|
|
1423
|
+
if (c === "\\") {
|
|
1424
|
+
this.#escape = true;
|
|
1425
|
+
this.#keyEscaped = true;
|
|
1426
|
+
return;
|
|
1427
|
+
}
|
|
1428
|
+
if (c !== "\"") return;
|
|
1429
|
+
const raw = this.slice(this.#keyStart, at);
|
|
1430
|
+
if (this.#keyEscaped) try {
|
|
1431
|
+
this.#key = JSON.parse(`"${raw}"`);
|
|
1432
|
+
} catch (_error) {
|
|
1433
|
+
this.fail();
|
|
1434
|
+
return;
|
|
1435
|
+
}
|
|
1436
|
+
else this.#key = raw;
|
|
1437
|
+
this.#mode = "colon";
|
|
1438
|
+
}
|
|
1439
|
+
open(entry) {
|
|
1440
|
+
if (!this.#entries.has(this.#key)) this.#order.push(this.#key);
|
|
1441
|
+
this.#entries.set(this.#key, entry);
|
|
1442
|
+
this.#current = entry;
|
|
1443
|
+
}
|
|
1444
|
+
beginValue(c, at) {
|
|
1445
|
+
if (isWhitespace(c)) return;
|
|
1446
|
+
if (c === "\"") {
|
|
1447
|
+
this.open({
|
|
1448
|
+
kind: "string",
|
|
1449
|
+
start: at + 1,
|
|
1450
|
+
end: -1,
|
|
1451
|
+
needsDecoding: false,
|
|
1452
|
+
invalidAt: void 0,
|
|
1453
|
+
length: void 0,
|
|
1454
|
+
text: void 0,
|
|
1455
|
+
prefixes: void 0
|
|
1456
|
+
});
|
|
1457
|
+
this.#escape = false;
|
|
1458
|
+
this.#mode = "string";
|
|
1459
|
+
return;
|
|
1460
|
+
}
|
|
1461
|
+
if (c === "}" || c === "," || c === ":" || c === "]") {
|
|
1462
|
+
this.fail();
|
|
1463
|
+
return;
|
|
1464
|
+
}
|
|
1465
|
+
this.open({
|
|
1466
|
+
kind: "value",
|
|
1467
|
+
start: at,
|
|
1468
|
+
end: -1,
|
|
1469
|
+
parsed: void 0,
|
|
1470
|
+
invalid: false
|
|
1471
|
+
});
|
|
1472
|
+
if (c === "{" || c === "[") {
|
|
1473
|
+
this.#mode = "nested";
|
|
1474
|
+
this.#nestedEnds = [c === "{" ? "}" : "]"];
|
|
1475
|
+
this.#nestedInString = false;
|
|
1476
|
+
this.#escape = false;
|
|
1477
|
+
return;
|
|
1478
|
+
}
|
|
1479
|
+
this.#mode = "scalar";
|
|
1480
|
+
}
|
|
1481
|
+
stepScalar(c, at) {
|
|
1482
|
+
if (c !== "," && c !== "}" && !isWhitespace(c)) return;
|
|
1483
|
+
this.closeValue(at);
|
|
1484
|
+
this.#mode = c === "," ? "key-only" : c === "}" ? "closed" : "comma-or-end";
|
|
1485
|
+
}
|
|
1486
|
+
stepNested(c, at) {
|
|
1487
|
+
if (this.#nestedInString) {
|
|
1488
|
+
this.#nestedInString = false;
|
|
1489
|
+
return;
|
|
1490
|
+
}
|
|
1491
|
+
if (c === "\"") {
|
|
1492
|
+
this.#nestedInString = true;
|
|
1493
|
+
return;
|
|
1494
|
+
}
|
|
1495
|
+
if (c === "{" || c === "[") {
|
|
1496
|
+
this.#nestedEnds.push(c === "{" ? "}" : "]");
|
|
1497
|
+
return;
|
|
1498
|
+
}
|
|
1499
|
+
if (c === "}" || c === "]") {
|
|
1500
|
+
if (this.#nestedEnds.pop() !== c) {
|
|
1501
|
+
this.fail();
|
|
1502
|
+
return;
|
|
1503
|
+
}
|
|
1504
|
+
if (this.#nestedEnds.length === 0) {
|
|
1505
|
+
this.closeValue(at + 1);
|
|
1506
|
+
this.#mode = "comma-or-end";
|
|
1507
|
+
}
|
|
1508
|
+
}
|
|
1509
|
+
}
|
|
1510
|
+
closeValue(end) {
|
|
1511
|
+
const entry = this.#current;
|
|
1512
|
+
entry.end = end;
|
|
1513
|
+
this.#current = null;
|
|
1514
|
+
}
|
|
1515
|
+
});
|
|
1516
|
+
//#endregion
|
|
1517
|
+
//#region ../../../vendor/deepseek-harness/packages/util/values/src/index.ts
|
|
1518
|
+
/**
|
|
1519
|
+
* Mark an unreachable closed-union branch.
|
|
1520
|
+
* @param value - impossible value; an unhandled typed variant fails at the call site.
|
|
1521
|
+
* @param context - optional switch-site label included in the failure message.
|
|
1522
|
+
* @returns never; a runtime value that escaped its type always throws.
|
|
1523
|
+
*/
|
|
1524
|
+
function assertNever(value, context) {
|
|
1525
|
+
const rendered = JSON.stringify(value) ?? String(value);
|
|
1526
|
+
throw new Error(`unreachable variant${context ? ` in ${context}` : ""}: ${rendered}`);
|
|
1527
|
+
}
|
|
1528
|
+
//#endregion
|
|
1529
|
+
//#region ../../../vendor/deepseek-harness/packages/client/shortcuts/src/configuration.ts
|
|
707
1530
|
/** Validated preference documents and deterministic conflict resolution, without browser dependencies. */
|
|
708
1531
|
const record = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
709
1532
|
const commandPattern = /^[a-z][a-zA-Z0-9-]*(?:\.[a-zA-Z][a-zA-Z0-9-]*)+$/u;
|
|
@@ -950,6 +1773,21 @@ function parseShortcutDefinitions(value) {
|
|
|
950
1773
|
}
|
|
951
1774
|
return definitions;
|
|
952
1775
|
}
|
|
1776
|
+
//#endregion
|
|
1777
|
+
//#region ../../../vendor/deepseek-harness/packages/util/crypto/src/index.ts
|
|
1778
|
+
/**
|
|
1779
|
+
* Random v4 UUID, minted from `crypto.getRandomValues`.
|
|
1780
|
+
* @returns the UUID string.
|
|
1781
|
+
*/
|
|
1782
|
+
function randomUUID() {
|
|
1783
|
+
const bytes = globalThis.crypto.getRandomValues(/* @__PURE__ */ new Uint8Array(16));
|
|
1784
|
+
const hex = Array.from(bytes, (byte, index) => {
|
|
1785
|
+
return (index === 6 ? byte & 15 | 64 : index === 8 ? byte & 63 | 128 : byte).toString(16).padStart(2, "0");
|
|
1786
|
+
}).join("");
|
|
1787
|
+
return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
|
|
1788
|
+
}
|
|
1789
|
+
//#endregion
|
|
1790
|
+
//#region ../../../vendor/deepseek-harness/packages/client/shortcuts/src/persistence.ts
|
|
953
1791
|
/** Serialized preference transactions; storage owners publish only accepted writes or read diagnostics. */
|
|
954
1792
|
/**
|
|
955
1793
|
* Create a disabled initial snapshot for asynchronous adapter startup.
|
|
@@ -1098,7 +1936,7 @@ var ShortcutPersistence = class {
|
|
|
1098
1936
|
}
|
|
1099
1937
|
};
|
|
1100
1938
|
//#endregion
|
|
1101
|
-
//#region ../../../vendor/deepseek-harness/packages/util/atomic-write/
|
|
1939
|
+
//#region ../../../vendor/deepseek-harness/packages/util/atomic-write/src/index.ts
|
|
1102
1940
|
/**
|
|
1103
1941
|
* Zero-dependency atomic file replacement and writer coordination.
|
|
1104
1942
|
* `writeFileAtomic` writes a random-suffix sibling with exclusive create and
|
|
@@ -1399,13 +2237,20 @@ function shellWrappedSpawn(command, args, options) {
|
|
|
1399
2237
|
//#endregion
|
|
1400
2238
|
//#region src/index.ts
|
|
1401
2239
|
let focusPrimaryWindow = () => {};
|
|
2240
|
+
/** app 目录(dev 是装配目录、打包是 `Resources/app`):由引导写进全局。 */
|
|
2241
|
+
function shellRoot() {
|
|
2242
|
+
const root = globalThis.__DSH_SHELL_ROOT__;
|
|
2243
|
+
if (root === void 0 || root === "") throw new Error("dsh desktop: the shell root is missing (bootstrap did not report it)");
|
|
2244
|
+
return root;
|
|
2245
|
+
}
|
|
1402
2246
|
function appConfigDir() {
|
|
1403
|
-
const configured =
|
|
2247
|
+
const configured = developmentLayout() === void 0 ? void 0 : join(shellRoot(), "..", "runtime");
|
|
1404
2248
|
return app.isPackaged || configured === void 0 || configured === "" ? process.resourcesPath : configured;
|
|
1405
2249
|
}
|
|
1406
2250
|
function startupFailure(error) {
|
|
1407
2251
|
const message = error instanceof Error ? error.message : String(error);
|
|
1408
|
-
|
|
2252
|
+
shellLog("入口失败或收尾失败 %o", error);
|
|
2253
|
+
shellLog(`启动失败:${message}`);
|
|
1409
2254
|
dialog.showErrorBox("dsh desktop startup failed", message);
|
|
1410
2255
|
app.exit(1);
|
|
1411
2256
|
throw error instanceof Error ? error : new Error(message);
|
|
@@ -1429,44 +2274,49 @@ protocol.registerSchemesAsPrivileged([{
|
|
|
1429
2274
|
codeCache: true
|
|
1430
2275
|
}
|
|
1431
2276
|
}]);
|
|
1432
|
-
const WEB_FRONTEND_PACKAGE = "@deepseek-ai/dsh-web-frontend";
|
|
1433
|
-
const APPLICATION_URL = `${SCHEME}://app/`;
|
|
1434
2277
|
const WINDOWS_TITLEBAR_HEIGHT = 40;
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
2278
|
+
/**
|
|
2279
|
+
* 打包形态下"当 node 用"的可执行文件。
|
|
2280
|
+
*
|
|
2281
|
+
* macOS 上 spawn 主二进制(`process.execPath`)会让系统**多出一个叫 `exec` 的 Dock 图标**——它落在 `.app`
|
|
2282
|
+
* 里、被当成 GUI 应用注册。改用同 bundle 的 **Helper**(它的 `Info.plist` 带 `LSUIElement`)就没有这个图标,
|
|
2283
|
+
* 而 `ELECTRON_RUN_AS_NODE` 对它一样生效。其余平台/形态直接用 `process.execPath`。
|
|
2284
|
+
* @returns 可执行文件的绝对路径。
|
|
2285
|
+
*/
|
|
2286
|
+
async function nodeExecutableOf() {
|
|
2287
|
+
if (process.platform === "darwin") {
|
|
2288
|
+
const app = basename(process.execPath);
|
|
2289
|
+
const helper = join(dirname(process.execPath), "..", "Frameworks", `${app} Helper.app`, "Contents", "MacOS", `${app} Helper`);
|
|
2290
|
+
if (await pathExistsOf(helper)) return helper;
|
|
1441
2291
|
}
|
|
2292
|
+
return process.execPath;
|
|
1442
2293
|
}
|
|
1443
|
-
function runtimeResources() {
|
|
1444
|
-
const
|
|
1445
|
-
const node = (
|
|
1446
|
-
const seed =
|
|
2294
|
+
async function runtimeResources() {
|
|
2295
|
+
const resourcesDir = developmentLayout() === void 0 ? process.resourcesPath : join(shellRoot(), "..");
|
|
2296
|
+
const node = developmentLayout() === void 0 ? await nodeExecutableOf() : "node";
|
|
2297
|
+
const seed = join(resourcesDir, "seed");
|
|
1447
2298
|
return {
|
|
1448
2299
|
node,
|
|
1449
2300
|
seed,
|
|
1450
|
-
runtime: join(seed, SEED_RUNTIME_DIR_NAME),
|
|
1451
|
-
pnpm: join(
|
|
1452
|
-
nodeBin: join(
|
|
2301
|
+
runtime: developmentLayout() === void 0 ? join(resourcesDir, "runtime") : join(seed, SEED_RUNTIME_DIR_NAME),
|
|
2302
|
+
pnpm: join(resourcesDir, "runtime", "pnpm", "bin", "pnpm.mjs"),
|
|
2303
|
+
nodeBin: join(resourcesDir, "runtime", "bin"),
|
|
2304
|
+
loader: join(resourcesDir, "runtime", "loader", "register.mjs")
|
|
1453
2305
|
};
|
|
1454
2306
|
}
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
const overrides = await runtimeOverrides(profileDir, resources.runtime);
|
|
1458
|
-
await writeFile(settingsPath, workspaceWithOverrides(await readFile(settingsPath, "utf8"), overrides));
|
|
1459
|
-
await installProfile({
|
|
1460
|
-
node: resources.node,
|
|
1461
|
-
pnpmEntry: resources.pnpm,
|
|
1462
|
-
profileDir
|
|
1463
|
-
});
|
|
2307
|
+
function configuredPath(value) {
|
|
2308
|
+
return value === void 0 || value === "" ? void 0 : value;
|
|
1464
2309
|
}
|
|
1465
|
-
function
|
|
1466
|
-
const
|
|
1467
|
-
|
|
1468
|
-
if (
|
|
1469
|
-
|
|
2310
|
+
function developmentLayout() {
|
|
2311
|
+
const profileDir = configuredPath(process.env.DSH_DESKTOP_DEV_PROFILE_DIR);
|
|
2312
|
+
const runtimeDir = configuredPath(process.env.DSH_DESKTOP_DEV_RUNTIME_DIR);
|
|
2313
|
+
if (profileDir === void 0 && runtimeDir === void 0) return void 0;
|
|
2314
|
+
if (app.isPackaged) throw new Error("dsh desktop: the development layout is unavailable in packaged applications");
|
|
2315
|
+
if (profileDir === void 0 || runtimeDir === void 0) throw new Error("dsh desktop: DSH_DESKTOP_DEV_PROFILE_DIR and DSH_DESKTOP_DEV_RUNTIME_DIR must be set together");
|
|
2316
|
+
return {
|
|
2317
|
+
profileDir: resolve(profileDir),
|
|
2318
|
+
runtimeDir: resolve(runtimeDir)
|
|
2319
|
+
};
|
|
1470
2320
|
}
|
|
1471
2321
|
function windowFrame() {
|
|
1472
2322
|
if (process.platform === "darwin") return {
|
|
@@ -1516,31 +2366,55 @@ function developmentHostInspectPort(enabled) {
|
|
|
1516
2366
|
const configured = process.env.DSH_DESKTOP_HOST_INSPECT_PORT;
|
|
1517
2367
|
if (!enabled || configured === void 0 || configured === "") return void 0;
|
|
1518
2368
|
const port = Number(configured);
|
|
1519
|
-
if (!Number.isSafeInteger(port) || port <
|
|
2369
|
+
if (!Number.isSafeInteger(port) || port < 0 || port > 65535) throw new Error("dsh desktop: DSH_DESKTOP_HOST_INSPECT_PORT must be an integer from 0 through 65535");
|
|
1520
2370
|
return port;
|
|
1521
2371
|
}
|
|
1522
|
-
function
|
|
1523
|
-
|
|
1524
|
-
|
|
2372
|
+
function sourceLoader(resources) {
|
|
2373
|
+
return resources.loader;
|
|
2374
|
+
}
|
|
2375
|
+
function sourceLoaderEnvironment(path) {
|
|
2376
|
+
return { DSH_SOURCE_LOADER: path };
|
|
2377
|
+
}
|
|
2378
|
+
/**
|
|
2379
|
+
* 壳的日志。
|
|
2380
|
+
*
|
|
2381
|
+
* 打包形态的 Electron **不把 `console` 写进 stdout**(它进系统日志),出了事只看到"窗口没起来"。
|
|
2382
|
+
* 所以壳自己的关键节点(启动、宿主就绪、fatal)落一份文件日志,与宿主那份(`${DSH_HOME}/logs/dev-host.log`,
|
|
2383
|
+
* 由 `@morlay/dsh-client-modules` 写)同目录,两边对着看就能定位"页面 → 壳 → 宿主"断在哪一段。
|
|
2384
|
+
*/
|
|
2385
|
+
async function shellLog(message, ...args) {
|
|
2386
|
+
const home = resolveDshHome(appConfig) ?? join(process.cwd(), ".dsh-store");
|
|
2387
|
+
const file = join(home, "logs", "shell.log");
|
|
2388
|
+
try {
|
|
2389
|
+
await mkdir(dirname(file), { recursive: true });
|
|
2390
|
+
await appendFile(file, `${(/* @__PURE__ */ new Date()).toISOString()} ${format(message, ...args)}\n`, "utf8");
|
|
2391
|
+
} catch {}
|
|
2392
|
+
}
|
|
2393
|
+
/** 文件在不在(项目禁同步 IO)。 */
|
|
2394
|
+
async function pathExistsOf(path) {
|
|
2395
|
+
return await access(path).then(() => true, () => false);
|
|
1525
2396
|
}
|
|
1526
2397
|
async function main() {
|
|
1527
|
-
const development =
|
|
1528
|
-
const resources = runtimeResources();
|
|
2398
|
+
const development = developmentLayout();
|
|
2399
|
+
const resources = await runtimeResources();
|
|
1529
2400
|
const hostInspectPort = developmentHostInspectPort(development !== void 0);
|
|
1530
|
-
const
|
|
1531
|
-
const
|
|
1532
|
-
const
|
|
1533
|
-
const
|
|
2401
|
+
const profileDir = development?.profileDir ?? join(resolveDshHome(appConfig) ?? "", "profiles", "desktop");
|
|
2402
|
+
const runtimeDir = development?.runtimeDir ?? resources.runtime;
|
|
2403
|
+
const dataDir = resolveDshHome(appConfig);
|
|
2404
|
+
const bundleCount = await readFile(join(profileDir, "package.json"), "utf8").then((text) => JSON.parse(text).dsh?.profile?.bundles?.length).catch(() => void 0);
|
|
2405
|
+
await shellLog(`启动:形态=${development === void 0 ? "bundle" : "dev"} root=${globalThis.__DSH_SHELL_ROOT__ ?? "(未知)"} profileDir=${profileDir} dataDir=${dataDir ?? "(未解析)"} dataDirSpec=${appConfig.dataDir} bundles=${String(bundleCount)}`);
|
|
1534
2406
|
if (development === void 0) {
|
|
1535
|
-
if (
|
|
1536
|
-
|
|
2407
|
+
if (dataDir === void 0) throw new Error("dsh desktop: packaged applications require a concrete dataDir");
|
|
2408
|
+
const seeded = await ensureSeedProfile(resources.seed, dataDir);
|
|
2409
|
+
if (seeded === "no-seed") throw new Error(`dsh desktop: the packaged app carries no ${PROFILE_NAME} profile seed at ${resources.seed}`);
|
|
2410
|
+
await shellLog(`profile 种子:${seeded === "seeded" ? "已重种" : "已是最新"}`);
|
|
1537
2411
|
}
|
|
1538
|
-
if (!await pathExists(join(webDocumentRoot, "index.html"))) throw new Error(`dsh desktop: the Web frontend is missing at ${webDocumentRoot}; the runtime closure must carry ${WEB_FRONTEND_PACKAGE}`);
|
|
1539
2412
|
let host;
|
|
1540
2413
|
let mainWindow;
|
|
1541
2414
|
let quitConfirmed = false;
|
|
1542
2415
|
let quitPrompting = false;
|
|
1543
|
-
|
|
2416
|
+
/** preload 落盘的位置:字节由宿主现场打包(sandboxed preload 只能是 CJS),这里只是放它。 */
|
|
2417
|
+
const preloadPathOf = () => join(app.getPath("userData"), "preload-app.cjs");
|
|
1544
2418
|
const confirmQuit = async (window) => {
|
|
1545
2419
|
if (quitPrompting) return;
|
|
1546
2420
|
quitPrompting = true;
|
|
@@ -1567,17 +2441,26 @@ async function main() {
|
|
|
1567
2441
|
const handleHostFailure = (error) => {
|
|
1568
2442
|
if (hostFailureHandled) return;
|
|
1569
2443
|
hostFailureHandled = true;
|
|
1570
|
-
|
|
2444
|
+
shellLog(`后端停止:${error.message}`);
|
|
2445
|
+
shellLog(`宿主停止:${error.message}`);
|
|
1571
2446
|
dialog.showErrorBox("dsh desktop backend stopped", error.message);
|
|
1572
2447
|
quitConfirmed = true;
|
|
1573
2448
|
app.exit(1);
|
|
1574
2449
|
};
|
|
1575
|
-
const startHost = async (
|
|
1576
|
-
const
|
|
1577
|
-
|
|
2450
|
+
const startHost = async (target = profileDir) => {
|
|
2451
|
+
const loader = sourceLoader(resources);
|
|
2452
|
+
const next = new DesktopHostProcess(resources.node, runtimeDir, target, hostInspectPort, {
|
|
2453
|
+
nodeArgs: [`--import=${loader}`],
|
|
2454
|
+
logFile: join(dataDir ?? join(process.cwd(), ".dsh-store"), "logs", "host.log"),
|
|
2455
|
+
entry: createRequire(import.meta.url).resolve(DESKTOP_HOST_PACKAGE),
|
|
1578
2456
|
extraEnv: {
|
|
1579
|
-
|
|
1580
|
-
...
|
|
2457
|
+
ELECTRON_RUN_AS_NODE: "1",
|
|
2458
|
+
...dataDir === void 0 ? {} : { DSH_HOME: dataDir },
|
|
2459
|
+
...globalThis.__DSH_SHELL_ROOT__ === void 0 ? {} : { DSH_DESKTOP_SHELL_ROOT: globalThis.__DSH_SHELL_ROOT__ },
|
|
2460
|
+
DSH_DEV_FORM: "desktop",
|
|
2461
|
+
UV_THREADPOOL_SIZE: process.env["UV_THREADPOOL_SIZE"] ?? "64",
|
|
2462
|
+
...development === void 0 ? { ELECTRON_RUN_AS_NODE: "1" } : {},
|
|
2463
|
+
...sourceLoaderEnvironment(loader)
|
|
1581
2464
|
},
|
|
1582
2465
|
processTitle: `${appConfig.name}-server`,
|
|
1583
2466
|
...development === void 0 ? { packageManager: {
|
|
@@ -1587,17 +2470,27 @@ async function main() {
|
|
|
1587
2470
|
spawn: shellWrappedSpawn,
|
|
1588
2471
|
onFailure: handleHostFailure
|
|
1589
2472
|
});
|
|
1590
|
-
|
|
1591
|
-
|
|
2473
|
+
return {
|
|
2474
|
+
process: next,
|
|
2475
|
+
ready: await next.start()
|
|
2476
|
+
};
|
|
1592
2477
|
};
|
|
1593
2478
|
protocol.handle(SCHEME, async (request) => {
|
|
1594
2479
|
const url = new URL(request.url);
|
|
1595
2480
|
if (url.hostname !== "app") return new Response(null, { status: 404 });
|
|
1596
2481
|
const active = host;
|
|
1597
2482
|
if (active === void 0) return new Response("backend unavailable", { status: 503 });
|
|
1598
|
-
if (!app.isPackaged)
|
|
1599
|
-
const
|
|
1600
|
-
|
|
2483
|
+
if (!app.isPackaged) shellLog(`recv ${request.method} ${url.pathname}`);
|
|
2484
|
+
const conditional = new Request(request.url, {
|
|
2485
|
+
method: request.method,
|
|
2486
|
+
headers: [...request.headers].filter(([name]) => name !== "if-none-match" && name !== "if-modified-since"),
|
|
2487
|
+
...request.body === null ? {} : {
|
|
2488
|
+
body: request.body,
|
|
2489
|
+
duplex: "half"
|
|
2490
|
+
}
|
|
2491
|
+
});
|
|
2492
|
+
const response = await active.fetch(conditional);
|
|
2493
|
+
if (!app.isPackaged) shellLog(`${request.method} ${url.pathname} → ${String(response.status)}`);
|
|
1601
2494
|
return response;
|
|
1602
2495
|
});
|
|
1603
2496
|
installDesktopDirectoryPicker(() => mainWindow, SCHEME);
|
|
@@ -1611,7 +2504,7 @@ async function main() {
|
|
|
1611
2504
|
try {
|
|
1612
2505
|
uplink.close();
|
|
1613
2506
|
} catch (error) {
|
|
1614
|
-
if (!app.isPackaged)
|
|
2507
|
+
if (!app.isPackaged) shellLog("stream uplink close failed %o", error);
|
|
1615
2508
|
}
|
|
1616
2509
|
};
|
|
1617
2510
|
ipcMain.handle(DESKTOP_IPC.streamOpen, async (event, endpoint, payload) => {
|
|
@@ -1619,7 +2512,7 @@ async function main() {
|
|
|
1619
2512
|
if (typeof endpoint !== "string") throw new Error("dsh desktop: stream endpoint must be text");
|
|
1620
2513
|
const active = host;
|
|
1621
2514
|
if (active === void 0) throw new Error("dsh desktop: Host is unavailable");
|
|
1622
|
-
if (!app.isPackaged)
|
|
2515
|
+
if (!app.isPackaged) shellLog(`stream open ${endpoint}`);
|
|
1623
2516
|
const id = nextStreamId++;
|
|
1624
2517
|
const abort = new AbortController();
|
|
1625
2518
|
const stream = {
|
|
@@ -1646,7 +2539,7 @@ async function main() {
|
|
|
1646
2539
|
(async () => {
|
|
1647
2540
|
try {
|
|
1648
2541
|
const response = await active.fetch(request);
|
|
1649
|
-
if (!app.isPackaged)
|
|
2542
|
+
if (!app.isPackaged) shellLog(`stream ${endpoint} → ${String(response.status)}`);
|
|
1650
2543
|
event.sender.send(DESKTOP_IPC.streamChunk, {
|
|
1651
2544
|
id,
|
|
1652
2545
|
status: response.status
|
|
@@ -1686,7 +2579,7 @@ async function main() {
|
|
|
1686
2579
|
try {
|
|
1687
2580
|
uplink.enqueue(streamEncoder.encode(encodeDesktopStreamItem(item)));
|
|
1688
2581
|
} catch (error) {
|
|
1689
|
-
if (!app.isPackaged)
|
|
2582
|
+
if (!app.isPackaged) shellLog("stream uplink rejected %o", error);
|
|
1690
2583
|
}
|
|
1691
2584
|
});
|
|
1692
2585
|
ipcMain.on(DESKTOP_IPC.streamUplinkEnd, (event, id) => {
|
|
@@ -1704,6 +2597,9 @@ async function main() {
|
|
|
1704
2597
|
app.on("will-quit", () => {
|
|
1705
2598
|
shortcuts.dispose();
|
|
1706
2599
|
});
|
|
2600
|
+
/** 宿主回环的 ws 端口:宿主起好之后填入(窗口重建时仍用它)。 */
|
|
2601
|
+
let hostOrigin = "";
|
|
2602
|
+
let appPreload = preloadPathOf();
|
|
1707
2603
|
const createMainWindow = () => {
|
|
1708
2604
|
const window = createWindow(appPreload, appConfig.window);
|
|
1709
2605
|
mainWindow = window;
|
|
@@ -1724,16 +2620,31 @@ async function main() {
|
|
|
1724
2620
|
focusPrimaryWindow = () => {
|
|
1725
2621
|
const window = mainWindow;
|
|
1726
2622
|
if (window === void 0 || window.isDestroyed()) {
|
|
1727
|
-
createMainWindow().loadURL(
|
|
2623
|
+
createMainWindow().loadURL(hostOrigin);
|
|
1728
2624
|
return;
|
|
1729
2625
|
}
|
|
1730
2626
|
if (window.isMinimized()) window.restore();
|
|
1731
2627
|
window.show();
|
|
1732
2628
|
window.focus();
|
|
1733
2629
|
};
|
|
1734
|
-
|
|
2630
|
+
const developerMenu = installDeveloperMenu({
|
|
2631
|
+
scheme: SCHEME,
|
|
2632
|
+
appName: appConfig.displayName,
|
|
2633
|
+
host: () => host,
|
|
2634
|
+
mainWindow: () => mainWindow
|
|
2635
|
+
});
|
|
2636
|
+
app.on("will-quit", () => {
|
|
2637
|
+
developerMenu.dispose();
|
|
2638
|
+
});
|
|
2639
|
+
const started = await startHost();
|
|
2640
|
+
host = started.process;
|
|
2641
|
+
hostOrigin = started.ready.origin;
|
|
2642
|
+
appPreload = preloadPathOf();
|
|
2643
|
+
const preload = await fetch(new Request(`${hostOrigin}${PRELOAD_SOURCE_PATH}`));
|
|
2644
|
+
if (!preload.ok) throw new Error(`dsh desktop: preload bundling failed (${String(preload.status)}): ${await preload.text()}`);
|
|
2645
|
+
await writeFile(appPreload, await preload.text(), "utf8");
|
|
1735
2646
|
mainWindow = createMainWindow();
|
|
1736
|
-
await mainWindow.loadURL(
|
|
2647
|
+
await mainWindow.loadURL(hostOrigin);
|
|
1737
2648
|
if (development !== void 0 && process.env.DSH_DESKTOP_OPEN_DEVTOOLS !== "0") mainWindow.webContents.openDevTools({ mode: "detach" });
|
|
1738
2649
|
app.on("activate", () => {
|
|
1739
2650
|
if (BrowserWindow.getAllWindows().length === 0) focusPrimaryWindow();
|
|
@@ -1752,7 +2663,7 @@ async function main() {
|
|
|
1752
2663
|
const active = host;
|
|
1753
2664
|
host = void 0;
|
|
1754
2665
|
active.stop().catch((error) => {
|
|
1755
|
-
|
|
2666
|
+
shellLog("入口失败或收尾失败 %o", error);
|
|
1756
2667
|
}).finally(() => {
|
|
1757
2668
|
app.quit();
|
|
1758
2669
|
});
|
|
@@ -1763,7 +2674,7 @@ if (app.isPackaged) try {
|
|
|
1763
2674
|
await mkdir(profile, { recursive: true });
|
|
1764
2675
|
app.setPath("userData", profile);
|
|
1765
2676
|
} catch (error) {
|
|
1766
|
-
|
|
2677
|
+
shellLog("入口失败或收尾失败 %o", error);
|
|
1767
2678
|
}
|
|
1768
2679
|
if ((() => {
|
|
1769
2680
|
if (!app.requestSingleInstanceLock()) {
|
|
@@ -1776,7 +2687,7 @@ if ((() => {
|
|
|
1776
2687
|
return true;
|
|
1777
2688
|
})()) app.whenReady().then(main).catch(async (error) => {
|
|
1778
2689
|
const message = error instanceof Error ? error.message : String(error);
|
|
1779
|
-
|
|
2690
|
+
shellLog("入口失败或收尾失败 %o", error);
|
|
1780
2691
|
const diagnosticFile = process.env.DSH_DESKTOP_DIAGNOSTIC_FILE;
|
|
1781
2692
|
if (diagnosticFile !== void 0) await import("node:fs/promises").then((fs) => fs.writeFile(diagnosticFile, `${error instanceof Error ? error.stack ?? message : message}\n`)).catch(() => void 0);
|
|
1782
2693
|
dialog.showErrorBox("dsh desktop startup failed", message);
|