dsh-auto-open-web 0.1.4 → 0.1.9

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/lib/index.js CHANGED
@@ -7,28 +7,58 @@
7
7
  * - Windows: lib/win32.js(全量实现)
8
8
  * - 其他平台: lib/posix.js(降级实现,记日志不打开)
9
9
  *
10
- * 行为:
10
+ * 行为(逐级降级,前一级失败即进入下一级):
11
11
  * 1. 默认打开 **WebView2 宿主窗口**(随包分发的 DshAppWindow.exe,独立进程,
12
12
  * 无标签栏/地址栏,直接加载 GUI 根地址):窗口/任务栏图标 = DSH(Form.Icon);
13
13
  * 宿主监视父进程 PID,随 DSH 退出而关闭。
14
14
  * 2. 宿主缺失/启动失败 → `--app` **专用浏览器实例**(--user-data-dir 隔离,
15
- * 不与正常浏览器页面共用进程);再失败则不打开任何东西(仅记录日志)。
15
+ * 不与正常浏览器页面共用进程)。
16
+ * 3. 再失败 → **默认浏览器打开**(官方 dsh web 同款 open 交接,普通标签页,
17
+ * 作为最后的兜底;open 包不可用时平台原生 cmd start / open / xdg-open)。
16
18
  *
17
19
  * 配置来源:行配置(cordis.patch.yml)作为启动种子;设置页卡片(客户端 bundle,
18
- * settings.plugin.item 插槽)经 /auto-open-web/config API 读写,持久化到官方
19
- * settings 命名空间 `auto-open-web`(首次保存后设置值优先于行配置)。
20
+ * settings.plugin.item 插槽)经官方 settings 域(settingsScope)读写本命名
21
+ * 空间,持久化到官方 settings 文档(首次保存后设置值优先于行配置)。
22
+ * 宿主保留的辅助路由:/auto-open-web/pick-browser(原生浏览对话框)、
23
+ * /auto-open-web/test-browser(浏览器拉起测试)。
20
24
  *
21
25
  * 端口来自 webServer 服务的真实监听值(--port 自定义、--port 0 均正确)。
22
26
  * 打开窗口的子进程 detached + unref,不阻塞 DSH、不随 DSH 退出被终止
23
27
  * (WebView2 宿主例外:随 DSH 退出而关闭)。
24
28
  */
25
29
  import { spawn } from 'node:child_process';
26
- import { existsSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
30
+ import { existsSync, rmSync, writeFileSync } from 'node:fs';
31
+ import { createRequire } from 'node:module';
27
32
  import { join } from 'node:path';
28
- import z from '@deepseek-ai/schemastery';
33
+ import { pathToFileURL } from 'node:url';
29
34
  import { dshHome, appProfileDir, testProfileDir, instanceStateFile } from './paths.js';
30
35
  import * as platform from './platform.js';
31
36
 
37
+ /**
38
+ * schemastery 依赖策略(与 win32.js 的 koffi 同一模式):本包零 dependencies,
39
+ * schemastery 由 DSH 部署自带(官方核心依赖),运行时解析:
40
+ * 常规 import(profile 已装/手动安装)优先 → Windows 全局 npm 布局的
41
+ * DSH 部署副本;两者都不可用时模块加载失败(插件依赖 Config,无法降级)。
42
+ */
43
+ let z = null;
44
+ try {
45
+ z = (await import('@deepseek-ai/schemastery')).default;
46
+ } catch {
47
+ try {
48
+ const globalRoot = process.env.APPDATA ? join(process.env.APPDATA, 'npm', 'node_modules') : '';
49
+ const dshEntry = join(globalRoot, '@deepseek-ai', 'dsh', 'package.json');
50
+ if (existsSync(dshEntry)) z = createRequire(dshEntry)('@deepseek-ai/schemastery');
51
+ } catch {
52
+ /* 两次解析均失败 → 下方抛错 */
53
+ }
54
+ }
55
+ if (z === null || z === undefined) {
56
+ throw new Error(
57
+ '[auto-open-web] schemastery not found (neither local nor DSH deployment); ' +
58
+ 'this plugin cannot build its config schema. Check the DSH deployment.',
59
+ );
60
+ }
61
+
32
62
  export const name = 'auto-open-web';
33
63
 
34
64
  /**
@@ -45,7 +75,7 @@ export const Config = z.object({
45
75
  appWindow: z.boolean().default(true),
46
76
  /** 窗口类型:webview2 = WebView2 宿主(独立进程,任务栏 DSH 图标,随 DSH 退出);
47
77
  * browser = 浏览器 --app 专用实例(--user-data-dir 隔离)。所选类型不可用时
48
- * 不打开任何东西(仅记录日志),不自动交叉兜底。 */
78
+ * 降级为最后的兜底:默认浏览器打开(官方 dsh web 同款,普通标签页)。 */
49
79
  windowKind: z.union([z.const('webview2'), z.const('browser')]).default('webview2'),
50
80
  /** 手动维护的浏览器可执行文件路径(单条;优先于内置候选 Edge → Chrome;仅 browser 模式使用)。 */
51
81
  browserPath: z.string().default(''),
@@ -53,7 +83,6 @@ export const Config = z.object({
53
83
  exitOnWindowClose: z.boolean().default(false),
54
84
  });
55
85
 
56
- const CONFIG_API_PATH = '/auto-open-web/config';
57
86
  const PICK_API_PATH = '/auto-open-web/pick-browser';
58
87
  const TEST_API_PATH = '/auto-open-web/test-browser';
59
88
  const ICON_SIZES = [16, 32, 48, 64, 128, 256];
@@ -205,6 +234,66 @@ function spawnHostWindow(exe, url, iconPath) {
205
234
  }
206
235
  }
207
236
 
237
+ // ── 最后的兜底:默认浏览器打开(官方 dsh web 同款) ──────────────────────────
238
+ //
239
+ // 官方(web-app bundle)启动后把 URL 交给操作系统默认浏览器,方式是:在独立
240
+ // node 子进程里用 `open` npm 包打开(win32 = PowerShell Start,darwin = open,
241
+ // linux = xdg-open),win32 下还等待启动器退出码确认交接成功。本插件把这一
242
+ // 方式作为 WebView2 宿主与 --app 专用实例都失败后的最后兜底——至少用户能
243
+ // 打开 GUI(普通标签页)。
244
+ //
245
+ // open 包解析策略(与 schemastery/koffi 同一模式):常规 import(profile 已装)
246
+ // 优先,其次 DSH 部署副本(官方 web-app 的依赖,open@11,纯 ESM,须先 resolve
247
+ // 出文件路径再动态 import)。open 包不可用时退回平台适配器原生拉起
248
+ // (win32: cmd start;darwin: open;linux: xdg-open)。
249
+ let openPkgPromise = null;
250
+
251
+ /** 解析 open 包(惰性 + 缓存);两次解析均失败返回 null。 */
252
+ function getOpenPackage() {
253
+ if (openPkgPromise === null) {
254
+ openPkgPromise = (async () => {
255
+ try {
256
+ const mod = await import('open');
257
+ if (mod !== null && mod !== undefined && mod.default !== undefined) return mod.default;
258
+ } catch {
259
+ /* 常规解析失败,尝试部署副本 */
260
+ }
261
+ try {
262
+ const globalRoot = process.env.APPDATA ? join(process.env.APPDATA, 'npm', 'node_modules') : '';
263
+ const dshEntry = join(globalRoot, '@deepseek-ai', 'dsh', 'package.json');
264
+ if (existsSync(dshEntry)) {
265
+ const resolved = createRequire(dshEntry).resolve('open');
266
+ const mod = await import(pathToFileURL(resolved).href);
267
+ if (mod !== null && mod !== undefined && mod.default !== undefined) return mod.default;
268
+ }
269
+ } catch {
270
+ /* 部署解析失败 */
271
+ }
272
+ return null;
273
+ })();
274
+ }
275
+ return openPkgPromise;
276
+ }
277
+
278
+ /**
279
+ * 最后兜底:把 URL 交给操作系统默认浏览器(普通标签页)。
280
+ * 优先 open 包(与官方同一路径);不可用/启动失败时退回平台原生拉起。
281
+ * 返回 true = 已交给默认浏览器;false = 全部失败(调用方仅记录日志,
282
+ * 提示手动访问 URL)。
283
+ */
284
+ async function openDefaultBrowser(url) {
285
+ const openPkg = await getOpenPackage();
286
+ if (openPkg !== null && openPkg !== undefined) {
287
+ try {
288
+ await openPkg(url); // wait:false → spawn 后即 resolve,子进程已 unref
289
+ return true;
290
+ } catch (error) {
291
+ console.error(`[auto-open-web] open package launch failed: ${error.message}`);
292
+ }
293
+ }
294
+ return platform.openDefaultBrowser(url);
295
+ }
296
+
208
297
  // ── 配置与历史迁移(共通) ─────────────────────────────────────────────────
209
298
 
210
299
  /** 删除 v0.3 遗留的 state 文件(一次性清理,防孤儿数据)。 */
@@ -311,48 +400,6 @@ async function ensureIconFile(server) {
311
400
 
312
401
  // ── 插件主体 ─────────────────────────────────────────────────────────────
313
402
 
314
- /**
315
- * dsh 宿主版本兼容范围(与 package.json 的 peerDependencies 一致):
316
- * settings.plugin.item 自 dsh 0.1.0-rc.7 起为 keyed 插槽(注册需 options.key);
317
- * 0.1.4 的卡片注册同时提供 id 与 key,新旧两代 dsh 均可挂载,但低于
318
- * 0.1.0-rc.7 时官方 UI 未定义该插槽的 keyed 语义,超出支持范围。
319
- */
320
- const DSH_MIN_VERSION = '0.1.0-rc.7';
321
- const DSH_MAX_VERSION = '0.2.0';
322
-
323
- /** 解析 "x.y.z[-rc.N]" 为数值数组;rc 缺失视为 Infinity(正式版 > 同号 rc)。失败返回 null。 */
324
- function parseVersion(text) {
325
- const m = /^(\d+)\.(\d+)\.(\d+)(?:-rc\.(\d+))?/.exec(String(text).trim());
326
- if (m === null) return null;
327
- return [Number(m[1]), Number(m[2]), Number(m[3]), m[4] !== undefined ? Number(m[4]) : Infinity];
328
- }
329
-
330
- function compareVersions(a, b) {
331
- return a[0] - b[0] || a[1] - b[1] || a[2] - b[2] || a[3] - b[3];
332
- }
333
-
334
- /** dsh 版本是否在支持范围内;版本未知(读不到/解析失败)返回 true(不误伤)。 */
335
- export function isDshVersionSupported(versionText) {
336
- const v = parseVersion(versionText);
337
- const min = parseVersion(DSH_MIN_VERSION);
338
- const max = parseVersion(DSH_MAX_VERSION);
339
- if (v === null || min === null || max === null) return true;
340
- return compareVersions(v, min) >= 0 && compareVersions(v, max) < 0;
341
- }
342
-
343
- /** 尽力读取部署的 dsh 版本(Windows 全局 npm 布局;其余环境返回 null)。 */
344
- export function detectDshVersion() {
345
- try {
346
- const globalRoot = process.env.APPDATA ? join(process.env.APPDATA, 'npm', 'node_modules') : '';
347
- const entry = join(globalRoot, '@deepseek-ai', 'dsh', 'package.json');
348
- if (!existsSync(entry)) return null;
349
- const version = JSON.parse(readFileSync(entry, 'utf8')).version;
350
- return typeof version === 'string' && version !== '' ? version : null;
351
- } catch {
352
- return null;
353
- }
354
- }
355
-
356
403
  /**
357
404
  * 窗口退出监听共享标志:"窗口关闭时退出 DSH"当前是否生效。
358
405
  * 监听始终注册(见 openAppWindow/spawnHostWindow),行为由本标志实时决定:
@@ -370,28 +417,17 @@ export const internals = {
370
417
  buildIco,
371
418
  resolveHostExe: platform.resolveHostExe,
372
419
  spawnHostWindow,
420
+ getOpenPackage,
421
+ openDefaultBrowser,
373
422
  /** 测试钩子:同步"窗口关闭时退出 DSH"生效标志(模拟设置保存)。 */
374
423
  setExitWatcherEnabled(value) {
375
424
  exitWatcherEnabled = value === true;
376
425
  },
377
- isDshVersionSupported,
378
- detectDshVersion,
379
426
  };
380
427
 
381
428
  export function apply(ctx, config) {
382
429
  removeLegacyState();
383
430
 
384
- // dsh 宿主版本约束(声明性):低于支持范围时明确提示(宿主能力兼容,
385
- // 卡片注册做了新旧双兼容,不硬停;日志指导用户升级 dsh)。
386
- const dshVersion = detectDshVersion();
387
- if (dshVersion !== null && !isDshVersionSupported(dshVersion)) {
388
- console.error(
389
- `[auto-open-web] DSH version ${dshVersion} is outside the supported range ` +
390
- `(${DSH_MIN_VERSION} <= dsh < ${DSH_MAX_VERSION}); the settings card may not mount. ` +
391
- 'Please upgrade dsh to continue using this plugin.',
392
- );
393
- }
394
-
395
431
  // browser 模式的专用实例随 DSH 退出:正常退出时结束其实例进程树
396
432
  // (被强杀时由下次启动前的预清理兜底)。
397
433
  process.on('exit', () => {
@@ -434,7 +470,9 @@ export function apply(ctx, config) {
434
470
  }
435
471
  syncExitWatcher(); // 启动时的最终生效值(settings 已合并或行配置)
436
472
 
437
- // ---- 设置卡片 API(读写与测试,配置由宿主权威校验) ----
473
+ // ---- 卡片辅助 API(浏览/测试;设置读写走官方 settings 域) ----
474
+ // 设置卡片经客户端 settingsScope(settings 域)读写本命名空间,不再自建
475
+ // config 路由;这里只保留官方通道无法覆盖的能力。
438
476
  function sendJson(res, status, value) {
439
477
  res.writeHead(status, { 'content-type': 'application/json; charset=utf-8' });
440
478
  res.end(JSON.stringify(value));
@@ -445,44 +483,6 @@ export function apply(ctx, config) {
445
483
  return body;
446
484
  }
447
485
  if (typeof server.register === 'function') {
448
- server.register({
449
- kind: 'exact',
450
- path: CONFIG_API_PATH,
451
- handler: async (req, res) => {
452
- if (req.method === 'GET') {
453
- sendJson(res, 200, { ok: true, ...state, writable });
454
- return;
455
- }
456
- if (req.method === 'POST') {
457
- if (!writable) {
458
- sendJson(res, 200, { ok: false, error: '当前配置只读(settings 不可写)' });
459
- return;
460
- }
461
- let parsed;
462
- try {
463
- parsed = JSON.parse(await readBody(req));
464
- } catch {
465
- sendJson(res, 400, { ok: false, error: 'invalid JSON body' });
466
- return;
467
- }
468
- try {
469
- const next = normalizeState(parsed);
470
- if (scope !== null) await scope.update(next);
471
- state = next;
472
- syncExitWatcher(); // 保存后即时生效,当前会话无需重启
473
- sendJson(res, 200, { ok: true, ...state });
474
- } catch (error) {
475
- sendJson(res, 200, {
476
- ok: false,
477
- error: String(error && error.message !== undefined ? error.message : error),
478
- });
479
- }
480
- return;
481
- }
482
- res.writeHead(405);
483
- res.end();
484
- },
485
- });
486
486
  // 原生"浏览"对话框:选择浏览器可执行文件(平台能力;非 Windows 提示手动输入)。
487
487
  server.register({
488
488
  kind: 'exact',
@@ -560,7 +560,18 @@ export function apply(ctx, config) {
560
560
  const url = `http://127.0.0.1:${String(server.port)}`;
561
561
 
562
562
  // 独立应用窗口:按 windowKind 显式选择(webview2 | browser),直接加载 GUI 根地址。
563
- // 所选类型不可用时仅记录日志,不打开任何东西,不自动交叉兜底。
563
+ // 所选类型不可用时降级到最后兜底:默认浏览器打开(官方 dsh web 同款,
564
+ // 普通标签页),保证用户至少能打开 GUI。
565
+ const fallbackOpen = async (reason) => {
566
+ console.log(`[auto-open-web] ${reason}; falling back to default browser (official open method)`);
567
+ const ok = await openDefaultBrowser(url);
568
+ if (ok) {
569
+ console.log(`[auto-open-web] opened default browser at ${url}`);
570
+ } else {
571
+ console.error('[auto-open-web] default browser open failed; visit the URL manually');
572
+ }
573
+ };
574
+
564
575
  if (state.appWindow) {
565
576
  if (state.windowKind === 'browser') {
566
577
  const resolved = platform.resolveBrowserExe(state.browserPath);
@@ -574,21 +585,21 @@ export function apply(ctx, config) {
574
585
  console.log(`[auto-open-web] opened browser app window (${resolved.browser} --app) at ${url}`);
575
586
  return;
576
587
  }
577
- console.log('[auto-open-web] browser app window launch failed; not opening anything');
578
- } else {
579
- console.log('[auto-open-web] no Edge/Chrome executable found; not opening anything');
588
+ await fallbackOpen('browser app window launch failed');
589
+ return;
580
590
  }
591
+ await fallbackOpen('no Edge/Chrome executable found');
581
592
  return;
582
593
  }
583
594
 
584
595
  // webview2 模式
585
596
  if (!platform.isWindows) {
586
- console.log('[auto-open-web] webview2 mode requires Windows; not opening anything');
597
+ await fallbackOpen('webview2 mode requires Windows');
587
598
  return;
588
599
  }
589
600
  const hostExe = platform.resolveHostExe();
590
601
  if (hostExe === null) {
591
- console.log('[auto-open-web] host exe not found; not opening anything');
602
+ await fallbackOpen('host exe not found');
592
603
  return;
593
604
  }
594
605
  const iconPath = await ensureIconFile(server);
@@ -597,7 +608,7 @@ export function apply(ctx, config) {
597
608
  console.log(`[auto-open-web] opened host window (DshAppWindow) at ${url}`);
598
609
  return;
599
610
  }
600
- console.log('[auto-open-web] host window launch failed; not opening anything');
611
+ await fallbackOpen('host window launch failed');
601
612
  return;
602
613
  }
603
614
 
package/lib/platform.js CHANGED
@@ -9,6 +9,7 @@
9
9
  // killProcessTree(pid)
10
10
  // pickBrowserExe() → Promise<string | null>
11
11
  // resolveHostExe() → string | null
12
+ // openDefaultBrowser(url) → Promise<boolean>(原生拉起默认浏览器,兜底)
12
13
  //
13
14
  // 按平台**条件加载**适配器(ESM 顶层 await):
14
15
  // - Windows:加载 win32.js(koffi 驱动 Job Object / 进程校验 / 原生对话框)
@@ -25,3 +26,4 @@ export const killDedicatedBrowserInstances = impl.killDedicatedBrowserInstances;
25
26
  export const killProcessTree = impl.killProcessTree;
26
27
  export const pickBrowserExe = impl.pickBrowserExe;
27
28
  export const resolveHostExe = impl.resolveHostExe;
29
+ export const openDefaultBrowser = impl.openDefaultBrowser;
package/lib/posix.js CHANGED
@@ -6,7 +6,10 @@
6
6
  // - 专用实例清理:无操作(Unix 下浏览器实例由 Job-less 方式管理,暂无实现)
7
7
  // - 进程树终止:尽力而为(SIGKILL 单进程,后代进程不保证)
8
8
  // - 原生文件对话框:不支持(返回 null,卡片提示手动输入)
9
- // - WebView2 宿主:无(返回 null,webview2 模式记日志不打开)
9
+ // - WebView2 宿主:无(返回 null,webview2 模式降级到最后兜底)
10
+ // - 默认浏览器打开:原生拉起(darwin: open;linux: xdg-open)
11
+ import { spawn } from 'node:child_process';
12
+
10
13
  export const isWindows = false;
11
14
 
12
15
  export function resolveBrowserExe() {
@@ -40,3 +43,23 @@ export function pickBrowserExe() {
40
43
  export function resolveHostExe() {
41
44
  return null;
42
45
  }
46
+
47
+ /**
48
+ * 原生默认浏览器打开(最后兜底,open 包不可用时):darwin 用 open,其余
49
+ * 平台用 xdg-open。detached + stdio ignore,不阻塞 DSH;启动失败返回 false。
50
+ */
51
+ export function openDefaultBrowser(url) {
52
+ const command = process.platform === 'darwin' ? 'open' : 'xdg-open';
53
+ return new Promise((resolve) => {
54
+ let child;
55
+ try {
56
+ child = spawn(command, [url], { detached: true, stdio: 'ignore' });
57
+ } catch {
58
+ resolve(false);
59
+ return;
60
+ }
61
+ child.on('error', () => resolve(false));
62
+ child.on('exit', (code) => resolve(code === 0));
63
+ child.unref();
64
+ });
65
+ }
package/lib/win32.js CHANGED
@@ -381,3 +381,28 @@ export function resolveHostExe() {
381
381
  const exe = fileURLToPath(new URL('../host-publish/DshAppWindow.exe', import.meta.url));
382
382
  return existsSync(exe) ? exe : null;
383
383
  }
384
+
385
+ // ── 原生默认浏览器打开(最后兜底,open 包不可用时) ─────────────────────────
386
+ //
387
+ // 与官方 web-app 同效但零依赖:cmd /c start 把 URL 交给系统默认浏览器
388
+ // (普通标签页)。detached + stdio ignore + windowsHide,不闪控制台窗口、
389
+ // 不阻塞 DSH;启动失败(找不到 cmd)返回 false。exit code 仅作参考——start
390
+ // 交出 URL 后 cmd 即退出,交接是否成功由操作系统决定。
391
+ export function openDefaultBrowser(url) {
392
+ return new Promise((resolve) => {
393
+ let child;
394
+ try {
395
+ child = spawn('cmd.exe', ['/d', '/c', 'start', '', url], {
396
+ detached: true,
397
+ stdio: 'ignore',
398
+ windowsHide: true,
399
+ });
400
+ } catch {
401
+ resolve(false);
402
+ return;
403
+ }
404
+ child.on('error', () => resolve(false));
405
+ child.on('exit', (code) => resolve(code === 0));
406
+ child.unref();
407
+ });
408
+ }
package/package.json CHANGED
@@ -1,7 +1,11 @@
1
1
  {
2
2
  "name": "dsh-auto-open-web",
3
- "version": "0.1.4",
3
+ "version": "0.1.9",
4
4
  "description": "Open the DSH Web GUI in an app-style WebView2 window on profile start, with a settings card for browser paths",
5
+ "repository": {
6
+ "type": "git",
7
+ "url": "https://github.com/jinsiyu/dsh-auto-open-web.git"
8
+ },
5
9
  "type": "module",
6
10
  "main": "lib/index.js",
7
11
  "scripts": {
@@ -22,7 +26,9 @@
22
26
  "inject": [
23
27
  "@deepseek-ai/dsh-client-connection",
24
28
  "@deepseek-ai/dsh-api-remotes",
25
- "@deepseek-ai/dsh-client-ui-settings"
29
+ "@deepseek-ai/dsh-client-ui-settings",
30
+ "@deepseek-ai/dsh-client-runtime",
31
+ "@deepseek-ai/dsh-client-locale"
26
32
  ]
27
33
  }
28
34
  },
@@ -39,15 +45,16 @@
39
45
  "README.md",
40
46
  "README.en.md"
41
47
  ],
42
- "dependencies": {
43
- "@deepseek-ai/schemastery": "^3.18.1"
44
- },
45
48
  "peerDependencies": {
46
- "@deepseek-ai/dsh": ">=0.1.0-rc.7 <0.2.0"
49
+ "@deepseek-ai/dsh": ">=0.1.0-rc.7 <0.2.0",
50
+ "@deepseek-ai/schemastery": "^3.18.1"
47
51
  },
48
52
  "peerDependenciesMeta": {
49
53
  "@deepseek-ai/dsh": {
50
54
  "optional": true
55
+ },
56
+ "@deepseek-ai/schemastery": {
57
+ "optional": true
51
58
  }
52
59
  },
53
60
  "license": "MIT"