dsh-auto-open-web 0.1.9 → 0.1.13
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.en.md +221 -211
- package/README.md +207 -191
- package/cordis.patch.yml +22 -17
- package/host-publish/DshAppWindow.dll +0 -0
- package/host-publish/DshAppWindow.exe +0 -0
- package/host-publish/DshAppWindow.pdb +0 -0
- package/lib/client.js +235 -174
- package/lib/index.js +664 -619
- package/lib/platform.js +29 -29
- package/lib/posix.js +65 -65
- package/package.json +61 -61
package/lib/index.js
CHANGED
|
@@ -1,619 +1,664 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @deepseek-ai/dsh-auto-open-web — dsh web profile 启动后自动打开独立应用窗口。
|
|
3
|
-
*
|
|
4
|
-
* 本文件为**平台无关共通核心**:配置模型、设置卡片 API、启动流程、生命周期、
|
|
5
|
-
* 图标栅格化。平台相关实现(浏览器候选/Job Object/进程清理/原生对话框/
|
|
6
|
-
* WebView2 宿主)全部位于平台适配器,经 ./platform.js 统一接口注入:
|
|
7
|
-
* - Windows: lib/win32.js(全量实现)
|
|
8
|
-
* - 其他平台: lib/posix.js(降级实现,记日志不打开)
|
|
9
|
-
*
|
|
10
|
-
* 行为(逐级降级,前一级失败即进入下一级):
|
|
11
|
-
* 1. 默认打开 **WebView2 宿主窗口**(随包分发的 DshAppWindow.exe,独立进程,
|
|
12
|
-
* 无标签栏/地址栏,直接加载 GUI 根地址):窗口/任务栏图标 = DSH(Form.Icon);
|
|
13
|
-
* 宿主监视父进程 PID,随 DSH 退出而关闭。
|
|
14
|
-
* 2. 宿主缺失/启动失败 → `--app` **专用浏览器实例**(--user-data-dir 隔离,
|
|
15
|
-
* 不与正常浏览器页面共用进程)。
|
|
16
|
-
* 3. 再失败 → **默认浏览器打开**(官方 dsh web 同款 open 交接,普通标签页,
|
|
17
|
-
* 作为最后的兜底;open 包不可用时平台原生 cmd start / open / xdg-open)。
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
* (
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
import {
|
|
34
|
-
import {
|
|
35
|
-
import
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
*
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
*
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
/**
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
*
|
|
99
|
-
*
|
|
100
|
-
*
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
*
|
|
161
|
-
*
|
|
162
|
-
*
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
*
|
|
213
|
-
*
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
child
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
//
|
|
242
|
-
//
|
|
243
|
-
//
|
|
244
|
-
//
|
|
245
|
-
//
|
|
246
|
-
//
|
|
247
|
-
//
|
|
248
|
-
//
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
}
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
}
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
}
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
1
|
+
/**
|
|
2
|
+
* @deepseek-ai/dsh-auto-open-web — dsh web profile 启动后自动打开独立应用窗口。
|
|
3
|
+
*
|
|
4
|
+
* 本文件为**平台无关共通核心**:配置模型、设置卡片 API、启动流程、生命周期、
|
|
5
|
+
* 图标栅格化。平台相关实现(浏览器候选/Job Object/进程清理/原生对话框/
|
|
6
|
+
* WebView2 宿主)全部位于平台适配器,经 ./platform.js 统一接口注入:
|
|
7
|
+
* - Windows: lib/win32.js(全量实现)
|
|
8
|
+
* - 其他平台: lib/posix.js(降级实现,记日志不打开)
|
|
9
|
+
*
|
|
10
|
+
* 行为(逐级降级,前一级失败即进入下一级):
|
|
11
|
+
* 1. 默认打开 **WebView2 宿主窗口**(随包分发的 DshAppWindow.exe,独立进程,
|
|
12
|
+
* 无标签栏/地址栏,直接加载 GUI 根地址):窗口/任务栏图标 = DSH(Form.Icon);
|
|
13
|
+
* 宿主监视父进程 PID,随 DSH 退出而关闭。
|
|
14
|
+
* 2. 宿主缺失/启动失败 → `--app` **专用浏览器实例**(--user-data-dir 隔离,
|
|
15
|
+
* 不与正常浏览器页面共用进程)。
|
|
16
|
+
* 3. 再失败 → **默认浏览器打开**(官方 dsh web 同款 open 交接,普通标签页,
|
|
17
|
+
* 作为最后的兜底;open 包不可用时平台原生 cmd start / open / xdg-open)。
|
|
18
|
+
* appWindow: false → **与官方相同**:同样执行默认浏览器交接(官方 open 方式),
|
|
19
|
+
* 替代被本插件补丁关闭的官方核心交接,行为与未安装本插件时一致。
|
|
20
|
+
* --no-open / SSH 会话 → **与官方相同**:不打开任何窗口/页面(读取官方
|
|
21
|
+
* webStartup 服务的 openBrowser 标志;SSH 会话按官方同源环境变量判定)。
|
|
22
|
+
*
|
|
23
|
+
* 配置来源:行配置(cordis.patch.yml)作为启动种子;设置页卡片(客户端 bundle,
|
|
24
|
+
* settings.plugin.item 插槽)经官方 settings 域(settingsScope)读写本命名
|
|
25
|
+
* 空间,持久化到官方 settings 文档(首次保存后设置值优先于行配置)。
|
|
26
|
+
* 宿主保留的辅助路由:/auto-open-web/pick-browser(原生浏览对话框)、
|
|
27
|
+
* /auto-open-web/test-browser(浏览器拉起测试)。
|
|
28
|
+
*
|
|
29
|
+
* 端口来自 webServer 服务的真实监听值(--port 自定义、--port 0 均正确)。
|
|
30
|
+
* 打开窗口的子进程 detached + unref,不阻塞 DSH、不随 DSH 退出被终止
|
|
31
|
+
* (WebView2 宿主例外:随 DSH 退出而关闭)。
|
|
32
|
+
*/
|
|
33
|
+
import { spawn } from 'node:child_process';
|
|
34
|
+
import { existsSync, rmSync, writeFileSync } from 'node:fs';
|
|
35
|
+
import { createRequire } from 'node:module';
|
|
36
|
+
import { join } from 'node:path';
|
|
37
|
+
import { pathToFileURL } from 'node:url';
|
|
38
|
+
import { dshHome, appProfileDir, testProfileDir, instanceStateFile } from './paths.js';
|
|
39
|
+
import * as platform from './platform.js';
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* schemastery 依赖策略(与 win32.js 的 koffi 同一模式):本包零 dependencies,
|
|
43
|
+
* schemastery 由 DSH 部署自带(官方核心依赖),运行时解析:
|
|
44
|
+
* 常规 import(profile 已装/手动安装)优先 → Windows 全局 npm 布局的
|
|
45
|
+
* DSH 部署副本;两者都不可用时模块加载失败(插件依赖 Config,无法降级)。
|
|
46
|
+
*/
|
|
47
|
+
let z = null;
|
|
48
|
+
try {
|
|
49
|
+
z = (await import('@deepseek-ai/schemastery')).default;
|
|
50
|
+
} catch {
|
|
51
|
+
try {
|
|
52
|
+
const globalRoot = process.env.APPDATA ? join(process.env.APPDATA, 'npm', 'node_modules') : '';
|
|
53
|
+
const dshEntry = join(globalRoot, '@deepseek-ai', 'dsh', 'package.json');
|
|
54
|
+
if (existsSync(dshEntry)) z = createRequire(dshEntry)('@deepseek-ai/schemastery');
|
|
55
|
+
} catch {
|
|
56
|
+
/* 两次解析均失败 → 下方抛错 */
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
if (z === null || z === undefined) {
|
|
60
|
+
throw new Error(
|
|
61
|
+
'[auto-open-web] schemastery not found (neither local nor DSH deployment); ' +
|
|
62
|
+
'this plugin cannot build its config schema. Check the DSH deployment.',
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export const name = 'auto-open-web';
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* webServer 是硬依赖:Cordis 会等 webServer 插件 Service.init() 完成
|
|
70
|
+
* (HTTP socket 已绑定、port 已写入)后才激活本插件,因此 apply 时端口
|
|
71
|
+
* 直接可用,无需轮询等待。settings 是硬依赖(与 proxy-router 同款):
|
|
72
|
+
* 设置卡片的数据通道必须等到 settings 服务就绪后再注册命名空间,
|
|
73
|
+
* 否则 writable 为 false(只读)。
|
|
74
|
+
*/
|
|
75
|
+
export const inject = ['webServer', 'settings'];
|
|
76
|
+
|
|
77
|
+
export const Config = z.object({
|
|
78
|
+
/** 启动时自动打开独立应用窗口;false 时与官方相同:默认浏览器打开(普通标签页)。 */
|
|
79
|
+
appWindow: z.boolean().default(true),
|
|
80
|
+
/** 窗口类型:webview2 = WebView2 宿主(独立进程,任务栏 DSH 图标,随 DSH 退出);
|
|
81
|
+
* browser = 浏览器 --app 专用实例(--user-data-dir 隔离)。所选类型不可用时
|
|
82
|
+
* 降级为最后的兜底:默认浏览器打开(官方 dsh web 同款,普通标签页)。 */
|
|
83
|
+
windowKind: z.union([z.const('webview2'), z.const('browser')]).default('webview2'),
|
|
84
|
+
/** 手动维护的浏览器可执行文件路径(单条;优先于内置候选 Edge → Chrome;仅 browser 模式使用)。 */
|
|
85
|
+
browserPath: z.string().default(''),
|
|
86
|
+
/** 关闭自动打开的窗口时随之退出 DSH(默认关闭;仅 appWindow 开启时生效)。 */
|
|
87
|
+
exitOnWindowClose: z.boolean().default(false),
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
const PICK_API_PATH = '/auto-open-web/pick-browser';
|
|
91
|
+
const TEST_API_PATH = '/auto-open-web/test-browser';
|
|
92
|
+
const ICON_SIZES = [16, 32, 48, 64, 128, 256];
|
|
93
|
+
const SETTINGS_NS = 'auto-open-web';
|
|
94
|
+
|
|
95
|
+
// ── 启动流程工具(共通) ───────────────────────────────────────────────────
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* 测试浏览器能否被成功拉起(设置卡片「测试」按钮):
|
|
99
|
+
* 1. 以 --app 启动专用测试实例(独立 user-data-dir,不污染正式实例);
|
|
100
|
+
* 2. 等待 2.5 秒,确认浏览器主进程仍存活(启动器立即退出即视为失败);
|
|
101
|
+
* 3. 若平台 Job Object 可用则加入作业(DSH 退出时兜底清理);
|
|
102
|
+
* 4. 成功后在短暂展示(8 秒)内自动结束该测试进程树(平台进程树终止,
|
|
103
|
+
* 不动正式实例)。
|
|
104
|
+
* 返回 { ok:true, pid } | { ok:false, error }。
|
|
105
|
+
*/
|
|
106
|
+
async function testBrowserLaunch(exe, browser, url) {
|
|
107
|
+
const args = [
|
|
108
|
+
'--app=' + url,
|
|
109
|
+
'--user-data-dir=' + testProfileDir(browser),
|
|
110
|
+
'--no-first-run',
|
|
111
|
+
'--no-default-browser-check',
|
|
112
|
+
];
|
|
113
|
+
return new Promise((resolve) => {
|
|
114
|
+
let child;
|
|
115
|
+
try {
|
|
116
|
+
child = spawn(exe, args, { detached: true, stdio: 'ignore' });
|
|
117
|
+
} catch (error) {
|
|
118
|
+
resolve({ ok: false, error: `启动失败: ${error.message}` });
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
let settled = false;
|
|
122
|
+
const fail = (error) => {
|
|
123
|
+
if (!settled) {
|
|
124
|
+
settled = true;
|
|
125
|
+
resolve({ ok: false, error });
|
|
126
|
+
}
|
|
127
|
+
};
|
|
128
|
+
child.on('error', (error) => fail(`浏览器进程启动失败: ${error.message}`));
|
|
129
|
+
setTimeout(async () => {
|
|
130
|
+
if (settled) return;
|
|
131
|
+
let alive = true;
|
|
132
|
+
try {
|
|
133
|
+
process.kill(child.pid, 0);
|
|
134
|
+
} catch {
|
|
135
|
+
alive = false;
|
|
136
|
+
}
|
|
137
|
+
if (!alive) {
|
|
138
|
+
settled = true;
|
|
139
|
+
resolve({ ok: false, error: '浏览器进程启动后立即退出' });
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
// 加入平台 Job Object(可用时;非致命,定时清理仍在)
|
|
143
|
+
try {
|
|
144
|
+
const job = await platform.ensureBrowserJob();
|
|
145
|
+
platform.assignToBrowserJob(job, child.pid);
|
|
146
|
+
} catch {
|
|
147
|
+
/* ignore */
|
|
148
|
+
}
|
|
149
|
+
settled = true;
|
|
150
|
+
resolve({ ok: true, pid: child.pid });
|
|
151
|
+
// 短暂展示后自动清理测试实例(仅结束该测试进程树)
|
|
152
|
+
setTimeout(() => {
|
|
153
|
+
platform.killProcessTree(child.pid);
|
|
154
|
+
}, 8000);
|
|
155
|
+
}, 2500);
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* 用 --app 打开独立应用窗口(无标签栏/地址栏,外观同应用)。
|
|
161
|
+
* 使用专用 --user-data-dir:独立浏览器实例,不与正常浏览器页面共用进程/
|
|
162
|
+
* Cookie/缓存;--no-first-run 避免全新 profile 的首启欢迎页。
|
|
163
|
+
* 传入 job 时把子进程加入平台 Job Object(DSH 强退也随进程退出)。
|
|
164
|
+
* 始终注册 exit 监听:窗口进程正常退出(退出码 0,即用户关闭窗口)时,
|
|
165
|
+
* 是否随 DSH 退出由当前配置标志 exitWatcherEnabled 决定(设置卡片保存后
|
|
166
|
+
* 即时生效,当前会话无需重启;启动失败/崩溃/强杀的非 0 退出码不触发)。
|
|
167
|
+
*/
|
|
168
|
+
function openAppWindow(exe, url, browser, job) {
|
|
169
|
+
const args = [
|
|
170
|
+
'--app=' + url,
|
|
171
|
+
'--user-data-dir=' + appProfileDir(browser ?? 'edge'),
|
|
172
|
+
'--no-first-run',
|
|
173
|
+
'--no-default-browser-check',
|
|
174
|
+
];
|
|
175
|
+
try {
|
|
176
|
+
const child = spawn(exe, args, { detached: true, stdio: 'ignore' });
|
|
177
|
+
// 记录专用实例身份(pid/exe/写入时间),供平台清理校验(防 pid 复用误杀)
|
|
178
|
+
recordInstanceState(browser ?? 'edge', child.pid, exe);
|
|
179
|
+
if (job !== null && job.handle !== null) {
|
|
180
|
+
platform.assignToBrowserJob(job, child.pid); // 子进程及其后代自动继承作业成员资格
|
|
181
|
+
}
|
|
182
|
+
child.on('error', (error) => {
|
|
183
|
+
console.error(`[auto-open-web] app window spawn failed: ${error.message}`);
|
|
184
|
+
});
|
|
185
|
+
child.on('exit', (code) => {
|
|
186
|
+
if (code === 0 && exitWatcherEnabled) {
|
|
187
|
+
console.log('[auto-open-web] app window closed; exiting DSH (exitOnWindowClose)');
|
|
188
|
+
process.exit(0);
|
|
189
|
+
}
|
|
190
|
+
});
|
|
191
|
+
child.unref();
|
|
192
|
+
return true;
|
|
193
|
+
} catch (error) {
|
|
194
|
+
console.error(`[auto-open-web] app window spawn threw: ${error.message}`);
|
|
195
|
+
return false;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* 记录专用实例 pid 状态文件(平台清理时按文件校验进程身份)。
|
|
201
|
+
* 写入失败不致命:清理侧无记录时跳过(Job Object 仍是主要保障)。
|
|
202
|
+
*/
|
|
203
|
+
function recordInstanceState(browser, pid, exe) {
|
|
204
|
+
try {
|
|
205
|
+
writeFileSync(instanceStateFile(browser), JSON.stringify({ pid, exe, writtenAt: Date.now() }), 'utf8');
|
|
206
|
+
} catch (error) {
|
|
207
|
+
console.error(`[auto-open-web] instance state write failed: ${error.message}`);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* 启动 WebView2 宿主窗口(独立进程,detached 不共享控制台)。
|
|
213
|
+
* 传入 --parent-pid:宿主在后台线程监视 DSH 进程,DSH 退出(正常或强杀)
|
|
214
|
+
* 即关闭窗口 → 拉起的窗口随 DSH 一起退出,不留孤儿。
|
|
215
|
+
* 始终注册 exit 监听:宿主进程正常退出(退出码 0,即窗口被关闭)时,
|
|
216
|
+
* 是否随 DSH 退出由当前配置标志 exitWatcherEnabled 决定(保存后即时
|
|
217
|
+
* 生效;启动失败的非 0 退出码不触发)。
|
|
218
|
+
*/
|
|
219
|
+
function spawnHostWindow(exe, url, iconPath) {
|
|
220
|
+
const args = ['--url', url, '--parent-pid', String(process.pid)];
|
|
221
|
+
if (iconPath !== null && iconPath !== '') args.push('--icon', iconPath);
|
|
222
|
+
try {
|
|
223
|
+
const child = spawn(exe, args, { detached: true, stdio: 'ignore' });
|
|
224
|
+
child.on('error', (error) => {
|
|
225
|
+
console.error(`[auto-open-web] host window spawn failed: ${error.message}`);
|
|
226
|
+
});
|
|
227
|
+
child.on('exit', (code) => {
|
|
228
|
+
if (code === 0 && exitWatcherEnabled) {
|
|
229
|
+
console.log('[auto-open-web] host window closed; exiting DSH (exitOnWindowClose)');
|
|
230
|
+
process.exit(0);
|
|
231
|
+
}
|
|
232
|
+
});
|
|
233
|
+
child.unref();
|
|
234
|
+
return true;
|
|
235
|
+
} catch (error) {
|
|
236
|
+
console.error(`[auto-open-web] host window spawn threw: ${error.message}`);
|
|
237
|
+
return false;
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// ── 最后的兜底:默认浏览器打开(官方 dsh web 同款) ──────────────────────────
|
|
242
|
+
//
|
|
243
|
+
// 官方(web-app bundle)启动后把 URL 交给操作系统默认浏览器,方式是:在独立
|
|
244
|
+
// node 子进程里用 `open` npm 包打开(win32 = PowerShell Start,darwin = open,
|
|
245
|
+
// linux = xdg-open),win32 下还等待启动器退出码确认交接成功。本插件把这一
|
|
246
|
+
// 方式作为 WebView2 宿主与 --app 专用实例都失败后的最后兜底——至少用户能
|
|
247
|
+
// 打开 GUI(普通标签页)。
|
|
248
|
+
//
|
|
249
|
+
// open 包解析策略(与 schemastery/koffi 同一模式):常规 import(profile 已装)
|
|
250
|
+
// 优先,其次 DSH 部署副本(官方 web-app 的依赖,open@11,纯 ESM,须先 resolve
|
|
251
|
+
// 出文件路径再动态 import)。open 包不可用时退回平台适配器原生拉起
|
|
252
|
+
// (win32: cmd start;darwin: open;linux: xdg-open)。
|
|
253
|
+
let openPkgPromise = null;
|
|
254
|
+
|
|
255
|
+
/** 解析 open 包(惰性 + 缓存);两次解析均失败返回 null。 */
|
|
256
|
+
function getOpenPackage() {
|
|
257
|
+
if (openPkgPromise === null) {
|
|
258
|
+
openPkgPromise = (async () => {
|
|
259
|
+
try {
|
|
260
|
+
const mod = await import('open');
|
|
261
|
+
if (mod !== null && mod !== undefined && mod.default !== undefined) return mod.default;
|
|
262
|
+
} catch {
|
|
263
|
+
/* 常规解析失败,尝试部署副本 */
|
|
264
|
+
}
|
|
265
|
+
try {
|
|
266
|
+
const globalRoot = process.env.APPDATA ? join(process.env.APPDATA, 'npm', 'node_modules') : '';
|
|
267
|
+
const dshEntry = join(globalRoot, '@deepseek-ai', 'dsh', 'package.json');
|
|
268
|
+
if (existsSync(dshEntry)) {
|
|
269
|
+
const resolved = createRequire(dshEntry).resolve('open');
|
|
270
|
+
const mod = await import(pathToFileURL(resolved).href);
|
|
271
|
+
if (mod !== null && mod !== undefined && mod.default !== undefined) return mod.default;
|
|
272
|
+
}
|
|
273
|
+
} catch {
|
|
274
|
+
/* 部署解析失败 */
|
|
275
|
+
}
|
|
276
|
+
return null;
|
|
277
|
+
})();
|
|
278
|
+
}
|
|
279
|
+
return openPkgPromise;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/**
|
|
283
|
+
* 最后兜底:把 URL 交给操作系统默认浏览器(普通标签页)。
|
|
284
|
+
* 优先 open 包(与官方同一路径);不可用/启动失败时退回平台原生拉起。
|
|
285
|
+
* 返回 true = 已交给默认浏览器;false = 全部失败(调用方仅记录日志,
|
|
286
|
+
* 提示手动访问 URL)。
|
|
287
|
+
*/
|
|
288
|
+
async function openDefaultBrowser(url) {
|
|
289
|
+
const openPkg = await getOpenPackage();
|
|
290
|
+
if (openPkg !== null && openPkg !== undefined) {
|
|
291
|
+
try {
|
|
292
|
+
await openPkg(url); // wait:false → spawn 后即 resolve,子进程已 unref
|
|
293
|
+
return true;
|
|
294
|
+
} catch (error) {
|
|
295
|
+
console.error(`[auto-open-web] open package launch failed: ${error.message}`);
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
return platform.openDefaultBrowser(url);
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
// ── 与官方相同的打开抑制(--no-open / SSH 会话) ────────────────────────────
|
|
302
|
+
//
|
|
303
|
+
// 官方(web-app)的默认浏览器交接条件是 handoffBrowser = config.openBrowser &&
|
|
304
|
+
// !launchedThroughSsh(ctx):config.openBrowser 来自 web-startup 插件提供的
|
|
305
|
+
// webStartup 服务(--no-open 时 openBrowser === false,即 commander 对
|
|
306
|
+
// --no-open 的反向选项 options.open);SSH 会话(SSH_CONNECTION/SSH_TTY 进程
|
|
307
|
+
// 环境变量)同样不自动打开。本插件的 bundle 补丁把官方 openBrowser 恒置为
|
|
308
|
+
// false、打开行为由本插件接管,故此处读取**同一来源**做同样的抑制,保证
|
|
309
|
+
// `dsh web --no-open`(及 SSH 会话)与官方行为一致:不打开任何窗口/页面。
|
|
310
|
+
/**
|
|
311
|
+
* --no-open 是否已传(webStartup.openBrowser === false)。
|
|
312
|
+
* webStartup 由 web-startup 插件提供(同一 isolate 的根服务表,ctx.get 可见);
|
|
313
|
+
* 非 web 组合(服务缺失)视为未传,不误伤。
|
|
314
|
+
*/
|
|
315
|
+
function noOpenRequested(ctx) {
|
|
316
|
+
const webStartup = ctx.get('webStartup');
|
|
317
|
+
return webStartup !== undefined && webStartup !== null && webStartup.openBrowser === false;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
/** 是否 SSH 会话(与官方 launchedThroughSsh 同源:进程环境变量)。 */
|
|
321
|
+
function sshSessionDetected() {
|
|
322
|
+
return (
|
|
323
|
+
(process.env.SSH_CONNECTION !== undefined && process.env.SSH_CONNECTION !== '') ||
|
|
324
|
+
(process.env.SSH_TTY !== undefined && process.env.SSH_TTY !== '')
|
|
325
|
+
);
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
// ── 配置与历史迁移(共通) ─────────────────────────────────────────────────
|
|
329
|
+
|
|
330
|
+
/** 删除 v0.3 遗留的 state 文件(一次性清理,防孤儿数据)。 */
|
|
331
|
+
function removeLegacyState() {
|
|
332
|
+
try {
|
|
333
|
+
rmSync(join(dshHome(), 'auto-open-web.json'), { force: true });
|
|
334
|
+
} catch {
|
|
335
|
+
/* ignore */
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
/** 把任意输入规范化为完整配置(类型收紧 + 默认值;兼容旧 browsers 列表迁移)。 */
|
|
340
|
+
function normalizeState(next) {
|
|
341
|
+
const obj = next !== null && typeof next === 'object' ? next : {};
|
|
342
|
+
const legacyBrowser =
|
|
343
|
+
Array.isArray(obj.browsers) && obj.browsers.length > 0 && typeof obj.browsers[0] === 'string'
|
|
344
|
+
? obj.browsers[0].trim()
|
|
345
|
+
: '';
|
|
346
|
+
return {
|
|
347
|
+
appWindow: obj.appWindow !== false,
|
|
348
|
+
windowKind: obj.windowKind === 'browser' ? 'browser' : 'webview2',
|
|
349
|
+
browserPath: typeof obj.browserPath === 'string' ? obj.browserPath.trim() : legacyBrowser,
|
|
350
|
+
exitOnWindowClose: obj.exitOnWindowClose === true,
|
|
351
|
+
};
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
// ── 图标栅格化(WebView2 宿主窗口图标需要 .ico;共通) ─────────────────────
|
|
355
|
+
//
|
|
356
|
+
// GUI 自带的 manifest 只有 SVG 图标(favicon.svg)。这里抓取本机 favicon.svg,
|
|
357
|
+
// 用 sharp(部署自带,运行时向上解析,未声明为依赖)栅格化为多尺寸 PNG 并缓存,
|
|
358
|
+
// 再组装成 .ico 供宿主窗口的 Form.Icon 使用;失败仅告警(宿主退回默认图标)。
|
|
359
|
+
let iconCache = null;
|
|
360
|
+
let iconFetching = null;
|
|
361
|
+
|
|
362
|
+
async function getIcons(server) {
|
|
363
|
+
if (iconCache !== null) return iconCache;
|
|
364
|
+
if (iconFetching !== null) return iconFetching;
|
|
365
|
+
iconFetching = (async () => {
|
|
366
|
+
try {
|
|
367
|
+
const port = server.port;
|
|
368
|
+
if (port === undefined) throw new Error('webServer port not settled');
|
|
369
|
+
const response = await fetch(`http://127.0.0.1:${String(port)}/favicon.svg`);
|
|
370
|
+
if (!response.ok) throw new Error(`favicon fetch failed: ${response.status}`);
|
|
371
|
+
const svg = Buffer.from(await response.arrayBuffer());
|
|
372
|
+
let sharp = null;
|
|
373
|
+
try {
|
|
374
|
+
sharp = (await import('sharp')).default;
|
|
375
|
+
} catch (error) {
|
|
376
|
+
throw new Error('sharp unavailable: ' + String(error && error.message !== undefined ? error.message : error));
|
|
377
|
+
}
|
|
378
|
+
const pngs = new Map();
|
|
379
|
+
for (const size of ICON_SIZES) {
|
|
380
|
+
pngs.set(size, await sharp(svg).resize(size, size).png().toBuffer());
|
|
381
|
+
}
|
|
382
|
+
iconCache = { pngs };
|
|
383
|
+
return iconCache;
|
|
384
|
+
} catch (error) {
|
|
385
|
+
console.error(`[auto-open-web] icon rasterization failed: ${String(error && error.message !== undefined ? error.message : error)}`);
|
|
386
|
+
return null;
|
|
387
|
+
} finally {
|
|
388
|
+
iconFetching = null;
|
|
389
|
+
}
|
|
390
|
+
})();
|
|
391
|
+
return iconFetching;
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
/** 把多尺寸 PNG 组装成 ICO 容器(Vista+ 支持 PNG 压缩条目)。 */
|
|
395
|
+
function buildIco(pngs) {
|
|
396
|
+
const sizes = [...pngs.keys()].sort((a, b) => a - b);
|
|
397
|
+
const header = Buffer.alloc(6);
|
|
398
|
+
header.writeUInt16LE(1, 2); // type: icon
|
|
399
|
+
header.writeUInt16LE(sizes.length, 4);
|
|
400
|
+
const entries = [];
|
|
401
|
+
let offset = 6 + 16 * sizes.length;
|
|
402
|
+
for (const size of sizes) {
|
|
403
|
+
const png = pngs.get(size);
|
|
404
|
+
const entry = Buffer.alloc(16);
|
|
405
|
+
entry.writeUInt8(size >= 256 ? 0 : size, 0); // width (0 = 256)
|
|
406
|
+
entry.writeUInt8(size >= 256 ? 0 : size, 1); // height
|
|
407
|
+
entry.writeUInt16LE(1, 4); // planes
|
|
408
|
+
entry.writeUInt16LE(32, 6); // bpp
|
|
409
|
+
entry.writeUInt32LE(png.length, 8);
|
|
410
|
+
entry.writeUInt32LE(offset, 12);
|
|
411
|
+
offset += png.length;
|
|
412
|
+
entries.push(entry);
|
|
413
|
+
}
|
|
414
|
+
return Buffer.concat([header, ...entries, ...sizes.map((s) => pngs.get(s))]);
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
/** 生成宿主窗口用的 DSH .ico(缓存于 ~/.dsh/auto-open-web-icon.ico),失败返回 null。 */
|
|
418
|
+
async function ensureIconFile(server) {
|
|
419
|
+
try {
|
|
420
|
+
const icons = await getIcons(server);
|
|
421
|
+
if (icons === null) return null;
|
|
422
|
+
const ico = buildIco(icons.pngs);
|
|
423
|
+
const out = join(dshHome(), 'auto-open-web-icon.ico');
|
|
424
|
+
writeFileSync(out, ico);
|
|
425
|
+
return out;
|
|
426
|
+
} catch (error) {
|
|
427
|
+
console.error(`[auto-open-web] icon file generation failed: ${error.message}`);
|
|
428
|
+
return null;
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
// ── 插件主体 ─────────────────────────────────────────────────────────────
|
|
433
|
+
|
|
434
|
+
/**
|
|
435
|
+
* 窗口退出监听共享标志:"窗口关闭时退出 DSH"当前是否生效。
|
|
436
|
+
* 监听始终注册(见 openAppWindow/spawnHostWindow),行为由本标志实时决定:
|
|
437
|
+
* 设置卡片保存配置后立即同步,当前会话无需重启即生效。
|
|
438
|
+
*/
|
|
439
|
+
let exitWatcherEnabled = false;
|
|
440
|
+
|
|
441
|
+
/** 测试钩子:纯函数导出,生产代码不依赖。 */
|
|
442
|
+
export const internals = {
|
|
443
|
+
resolveBrowserExe: platform.resolveBrowserExe,
|
|
444
|
+
openAppWindow,
|
|
445
|
+
testBrowserLaunch,
|
|
446
|
+
normalizeState,
|
|
447
|
+
getIcons,
|
|
448
|
+
buildIco,
|
|
449
|
+
resolveHostExe: platform.resolveHostExe,
|
|
450
|
+
spawnHostWindow,
|
|
451
|
+
getOpenPackage,
|
|
452
|
+
openDefaultBrowser,
|
|
453
|
+
noOpenRequested,
|
|
454
|
+
sshSessionDetected,
|
|
455
|
+
/** 测试钩子:同步"窗口关闭时退出 DSH"生效标志(模拟设置保存)。 */
|
|
456
|
+
setExitWatcherEnabled(value) {
|
|
457
|
+
exitWatcherEnabled = value === true;
|
|
458
|
+
},
|
|
459
|
+
};
|
|
460
|
+
|
|
461
|
+
export function apply(ctx, config) {
|
|
462
|
+
removeLegacyState();
|
|
463
|
+
|
|
464
|
+
// browser 模式的专用实例随 DSH 退出:正常退出时结束其实例进程树
|
|
465
|
+
// (被强杀时由下次启动前的预清理兜底)。
|
|
466
|
+
process.on('exit', () => {
|
|
467
|
+
platform.killDedicatedBrowserInstances('edge');
|
|
468
|
+
platform.killDedicatedBrowserInstances('chrome');
|
|
469
|
+
});
|
|
470
|
+
|
|
471
|
+
const server = ctx.get('webServer');
|
|
472
|
+
if (server === undefined) {
|
|
473
|
+
console.error('[auto-open-web] webServer service unavailable; not opening anything');
|
|
474
|
+
return;
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
// ---- 配置状态:行配置为种子;settings 命名空间持久化(设置卡片写入) ----
|
|
478
|
+
const settingsSvc = ctx.get('settings');
|
|
479
|
+
let scope = null;
|
|
480
|
+
let writable = false;
|
|
481
|
+
let state = normalizeState(config);
|
|
482
|
+
/** 同步"窗口关闭时退出 DSH"生效标志:监听始终注册,保存配置后即时生效。 */
|
|
483
|
+
const syncExitWatcher = () => {
|
|
484
|
+
exitWatcherEnabled = state.exitOnWindowClose === true;
|
|
485
|
+
};
|
|
486
|
+
if (settingsSvc !== undefined && typeof settingsSvc.register === 'function') {
|
|
487
|
+
try {
|
|
488
|
+
scope = settingsSvc.register(SETTINGS_NS, Config);
|
|
489
|
+
writable = settingsSvc.writable !== false;
|
|
490
|
+
const rawDoc = settingsSvc.get(SETTINGS_NS);
|
|
491
|
+
if (rawDoc !== undefined && rawDoc !== null) {
|
|
492
|
+
state = normalizeState(scope.get());
|
|
493
|
+
}
|
|
494
|
+
scope.watch((next) => {
|
|
495
|
+
state = normalizeState(next);
|
|
496
|
+
syncExitWatcher();
|
|
497
|
+
});
|
|
498
|
+
} catch (error) {
|
|
499
|
+
console.error(`[auto-open-web] settings unavailable; using row config only: ${error.message}`);
|
|
500
|
+
scope = null;
|
|
501
|
+
writable = false;
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
syncExitWatcher(); // 启动时的最终生效值(settings 已合并或行配置)
|
|
505
|
+
|
|
506
|
+
// ---- 卡片辅助 API(浏览/测试;设置读写走官方 settings 域) ----
|
|
507
|
+
// 设置卡片经客户端 settingsScope(settings 域)读写本命名空间,不再自建
|
|
508
|
+
// config 路由;这里只保留官方通道无法覆盖的能力。
|
|
509
|
+
function sendJson(res, status, value) {
|
|
510
|
+
res.writeHead(status, { 'content-type': 'application/json; charset=utf-8' });
|
|
511
|
+
res.end(JSON.stringify(value));
|
|
512
|
+
}
|
|
513
|
+
async function readBody(req) {
|
|
514
|
+
let body = '';
|
|
515
|
+
for await (const chunk of req) body += chunk.toString('utf8');
|
|
516
|
+
return body;
|
|
517
|
+
}
|
|
518
|
+
if (typeof server.register === 'function') {
|
|
519
|
+
// 原生"浏览"对话框:选择浏览器可执行文件(平台能力;非 Windows 提示手动输入)。
|
|
520
|
+
server.register({
|
|
521
|
+
kind: 'exact',
|
|
522
|
+
path: PICK_API_PATH,
|
|
523
|
+
handler: async (req, res) => {
|
|
524
|
+
if (req.method !== 'POST') {
|
|
525
|
+
res.writeHead(405);
|
|
526
|
+
res.end();
|
|
527
|
+
return;
|
|
528
|
+
}
|
|
529
|
+
if (!platform.isWindows) {
|
|
530
|
+
sendJson(res, 200, { ok: false, error: '原生浏览对话框仅支持 Windows;请手动输入路径' });
|
|
531
|
+
return;
|
|
532
|
+
}
|
|
533
|
+
const picked = await platform.pickBrowserExe();
|
|
534
|
+
if (picked !== null) {
|
|
535
|
+
sendJson(res, 200, { ok: true, path: picked });
|
|
536
|
+
} else {
|
|
537
|
+
sendJson(res, 200, { ok: false, cancelled: true, error: '未选择文件' });
|
|
538
|
+
}
|
|
539
|
+
},
|
|
540
|
+
});
|
|
541
|
+
// 测试按钮:真正拉起 --app 专用测试实例,确认浏览器可被成功启动。
|
|
542
|
+
server.register({
|
|
543
|
+
kind: 'exact',
|
|
544
|
+
path: TEST_API_PATH,
|
|
545
|
+
handler: async (req, res) => {
|
|
546
|
+
if (req.method !== 'POST') {
|
|
547
|
+
res.writeHead(405);
|
|
548
|
+
res.end();
|
|
549
|
+
return;
|
|
550
|
+
}
|
|
551
|
+
// 优先使用请求携带的草稿 browserPath(未保存也能测当前输入)
|
|
552
|
+
let draftPath = '';
|
|
553
|
+
try {
|
|
554
|
+
const parsed = JSON.parse(await readBody(req));
|
|
555
|
+
if (typeof parsed.browserPath === 'string') draftPath = parsed.browserPath.trim();
|
|
556
|
+
} catch {
|
|
557
|
+
/* 空 body 用已保存路径 */
|
|
558
|
+
}
|
|
559
|
+
const resolved = platform.resolveBrowserExe(draftPath !== '' ? draftPath : state.browserPath);
|
|
560
|
+
if (resolved === null) {
|
|
561
|
+
sendJson(res, 200, { ok: false, error: '未找到可用的浏览器可执行文件(内置候选 Edge/Chrome 均不可用);可先用「浏览」选择路径' });
|
|
562
|
+
return;
|
|
563
|
+
}
|
|
564
|
+
// inject 保证 apply 时端口已可用(inject 依赖 init 完成 = socket 已绑定)
|
|
565
|
+
if (server.port === undefined) {
|
|
566
|
+
sendJson(res, 200, { ok: false, error: 'webServer 端口不可用,请稍后重试' });
|
|
567
|
+
return;
|
|
568
|
+
}
|
|
569
|
+
const url = `http://127.0.0.1:${String(server.port)}`;
|
|
570
|
+
const result = await testBrowserLaunch(resolved.exe, resolved.browser, url);
|
|
571
|
+
if (result.ok !== true) {
|
|
572
|
+
sendJson(res, 200, { ok: false, error: result.error });
|
|
573
|
+
return;
|
|
574
|
+
}
|
|
575
|
+
const label = resolved.browser === 'edge' ? 'Edge' : resolved.browser === 'chrome' ? 'Chrome' : '浏览器';
|
|
576
|
+
sendJson(res, 200, {
|
|
577
|
+
ok: true,
|
|
578
|
+
browser: resolved.browser,
|
|
579
|
+
pid: result.pid,
|
|
580
|
+
message: `已成功拉起 ${label} 测试实例(pid ${result.pid}),窗口将在数秒后自动关闭`,
|
|
581
|
+
});
|
|
582
|
+
},
|
|
583
|
+
});
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
const attempt = async () => {
|
|
587
|
+
// inject 保证 apply 时 webServer 已初始化完成(HTTP socket 已绑定、
|
|
588
|
+
// port 已写入),直接取端口,无需等待。
|
|
589
|
+
if (server.port === undefined) {
|
|
590
|
+
console.error('[auto-open-web] webServer port unavailable; not opening anything');
|
|
591
|
+
return;
|
|
592
|
+
}
|
|
593
|
+
const url = `http://127.0.0.1:${String(server.port)}`;
|
|
594
|
+
|
|
595
|
+
// 与官方相同的打开抑制:--no-open(webStartup.openBrowser === false)或
|
|
596
|
+
// SSH 会话时不打开任何窗口/页面(官方同样不交接)。
|
|
597
|
+
const noOpen = noOpenRequested(ctx);
|
|
598
|
+
const ssh = sshSessionDetected();
|
|
599
|
+
if (noOpen || ssh) {
|
|
600
|
+
console.log(`[auto-open-web] ${noOpen ? '--no-open given' : 'SSH session'}; not opening anything (official behavior)`);
|
|
601
|
+
return;
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
// 独立应用窗口:按 windowKind 显式选择(webview2 | browser),直接加载 GUI 根地址。
|
|
605
|
+
// 所选类型不可用时降级到最后兜底:默认浏览器打开(官方 dsh web 同款,
|
|
606
|
+
// 普通标签页),保证用户至少能打开 GUI。
|
|
607
|
+
const fallbackOpen = async (reason) => {
|
|
608
|
+
console.log(`[auto-open-web] ${reason}; falling back to default browser (official open method)`);
|
|
609
|
+
const ok = await openDefaultBrowser(url);
|
|
610
|
+
if (ok) {
|
|
611
|
+
console.log(`[auto-open-web] opened default browser at ${url}`);
|
|
612
|
+
} else {
|
|
613
|
+
console.error('[auto-open-web] default browser open failed; visit the URL manually');
|
|
614
|
+
}
|
|
615
|
+
};
|
|
616
|
+
|
|
617
|
+
if (state.appWindow) {
|
|
618
|
+
if (state.windowKind === 'browser') {
|
|
619
|
+
const resolved = platform.resolveBrowserExe(state.browserPath);
|
|
620
|
+
if (resolved !== null) {
|
|
621
|
+
// 预清理:上次 DSH 被强杀时残留的专用实例(避免旧窗口堆积)。
|
|
622
|
+
platform.killDedicatedBrowserInstances(resolved.browser);
|
|
623
|
+
// Job Object:DSH 无论正常退出还是被强杀,专用实例随进程一起结束。
|
|
624
|
+
const job = await platform.ensureBrowserJob();
|
|
625
|
+
const ok = openAppWindow(resolved.exe, url, resolved.browser, job);
|
|
626
|
+
if (ok) {
|
|
627
|
+
console.log(`[auto-open-web] opened browser app window (${resolved.browser} --app) at ${url}`);
|
|
628
|
+
return;
|
|
629
|
+
}
|
|
630
|
+
await fallbackOpen('browser app window launch failed');
|
|
631
|
+
return;
|
|
632
|
+
}
|
|
633
|
+
await fallbackOpen('no Edge/Chrome executable found');
|
|
634
|
+
return;
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
// webview2 模式
|
|
638
|
+
if (!platform.isWindows) {
|
|
639
|
+
await fallbackOpen('webview2 mode requires Windows');
|
|
640
|
+
return;
|
|
641
|
+
}
|
|
642
|
+
const hostExe = platform.resolveHostExe();
|
|
643
|
+
if (hostExe === null) {
|
|
644
|
+
await fallbackOpen('host exe not found');
|
|
645
|
+
return;
|
|
646
|
+
}
|
|
647
|
+
const iconPath = await ensureIconFile(server);
|
|
648
|
+
const ok = spawnHostWindow(hostExe, url, iconPath);
|
|
649
|
+
if (ok) {
|
|
650
|
+
console.log(`[auto-open-web] opened host window (DshAppWindow) at ${url}`);
|
|
651
|
+
return;
|
|
652
|
+
}
|
|
653
|
+
await fallbackOpen('host window launch failed');
|
|
654
|
+
return;
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
// appWindow 关闭:与官方 dsh web 相同——把 URL 交给系统默认浏览器
|
|
658
|
+
// (官方 open 方式,普通标签页)。本插件的 bundle 补丁已把核心的
|
|
659
|
+
// web-runtime.openBrowser 置为 false,故这里的交接替代官方核心,
|
|
660
|
+
// 行为与未安装本插件时的官方默认完全一致。
|
|
661
|
+
await fallbackOpen('appWindow disabled (official default-browser handoff)');
|
|
662
|
+
};
|
|
663
|
+
void attempt();
|
|
664
|
+
}
|