dsh-auto-open-web 0.1.3 → 0.1.5

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 CHANGED
@@ -59,8 +59,10 @@ Two equivalent ways:
59
59
  `browserPath` (browser executable, with a native "Browse" file dialog; located below the
60
60
  window-type field and enabled only when "Browser app window" is selected),
61
61
  `exitOnWindowClose` (exit DSH when the window closes, off by default).
62
- After saving, values persist to the settings document (namespace `auto-open-web`); once saved,
63
- settings take precedence over row configuration.
62
+ After saving, values persist through the **official settings domain** (client `settingsScope`)
63
+ to the settings document (namespace `auto-open-web`); once saved,
64
+ settings take precedence over row configuration. The host keeps only the "Browse" / "Test"
65
+ helper routes (capabilities the official channel cannot cover).
64
66
  2. **Row configuration** (`cordis.patch.yml`): acts as the startup seed, effective until the
65
67
  settings card is saved.
66
68
 
@@ -86,14 +88,14 @@ whole config; fields not listed fall back to defaults):
86
88
 
87
89
  This package is a **bundle**: an npm package carrying a configuration layer — `dsh.bundle` in
88
90
  `package.json` declares the patch file (`cordis.patch.yml`), and a profile activates the plugin row
89
- by package name when installed. Published to the **npm registry** (`dsh-auto-open-web@0.1.3`) and
91
+ by package name when installed. Published to the **npm registry** (`dsh-auto-open-web@0.1.5`) and
90
92
  **GitHub** (https://github.com/jinsiyu/dsh-auto-open-web, `main` branch).
91
93
 
92
94
  ### Packaging
93
95
 
94
96
  ```bash
95
97
  cd dsh-auto-open-web
96
- pnpm pack # the prepack hook compiles the WebView2 host first (dotnet publish), producing dsh-auto-open-web-0.1.3.tgz
98
+ pnpm pack # the prepack hook compiles the WebView2 host first (dotnet publish), producing dsh-auto-open-web-0.1.5.tgz
97
99
  ```
98
100
 
99
101
  ### Installation (pick one)
@@ -108,7 +110,7 @@ dsh plugin --profile web add C:\path\to\dsh-auto-open-web
108
110
  **Option 2: tarball (published artifact, recommended for delivery; no build permission needed)**
109
111
 
110
112
  ```bash
111
- dsh plugin --profile web add ./dsh-auto-open-web-0.1.3.tgz
113
+ dsh plugin --profile web add ./dsh-auto-open-web-0.1.5.tgz
112
114
  ```
113
115
 
114
116
  **Option 3: npm registry (after publishing)**
@@ -145,29 +147,30 @@ without touching the package.
145
147
 
146
148
  ### Notes
147
149
 
150
+ - **Zero dependencies**: every runtime dependency is provided by the **DSH deployment** and
151
+ declared as an optional peer (no install warnings):
152
+ - `@deepseek-ai/dsh` (the host; declares the compatibility range `>=0.1.0-rc.7 <0.2.0` —
153
+ `settings.plugin.item` became a keyed slot in that release, registration requires
154
+ `options.key`; no runtime version check is performed)
155
+ - `@deepseek-ai/schemastery` (config schema validator; resolved at runtime: normal import
156
+ first, then the DSH deployment copy under the Windows global npm layout)
157
+ - `koffi` (Windows only: Job Object / process verification / native file dialog; same runtime
158
+ deployment-copy resolution, failure only degrades)
159
+ So no platform ever hits build-script blocking — HarmonyOS and other environments without
160
+ koffi prebuilds install out of the box; posix platforms use the posix fallback adapter (browser
161
+ mode works; webview2 / native dialog unavailable), and the platform adapter is loaded
162
+ conditionally so win32.js is never evaluated on posix.
163
+ - This package does not depend on `@deepseek-ai/cordis` (zero code references; the plugin identity
164
+ comes from the `dsh.bundle` / `dsh.client` fields, and Cordis is provided by the DSH deployment
165
+ at runtime), so installation produces no peer warnings.
148
166
  - **The npm package already contains the compiled WebView2 host** (built by the prepack hook before
149
167
  publishing); the **GitHub `main` branch and source-checkout installs do not** include
150
168
  `host-publish/` (build artifacts are .gitignore'd): for webview2 mode, run
151
169
  `pnpm run build:host` inside `node_modules/dsh-auto-open-web` first (requires the .NET SDK);
152
170
  browser mode needs no build.
153
- - This package does not depend on `@deepseek-ai/cordis` (zero code references; the plugin identity
154
- comes from the `dsh.bundle` / `dsh.client` fields, and Cordis is provided by the DSH deployment
155
- at runtime), so installation produces no peer warnings.
156
- - **Zero koffi dependency declared**: koffi (driving Windows Job Object / process verification /
157
- native file dialog) ships with the **DSH deployment itself** (the official native picker
158
- `@deepseek-ai/dsh-host-directory-picker-native` depends on koffi, so the deployment always has
159
- it). The plugin resolves it at runtime: normal resolution (profile-installed or manual
160
- `pnpm add koffi`) first, then the DSH deployment copy under the Windows global npm layout; if
161
- resolution fails, capabilities only degrade (Job Object unavailable → exit / pre-launch cleanup
162
- still apply; process verification skipped; native dialog unavailable) with a log entry.
163
- **Therefore no platform ever hits koffi build-script blocking** — HarmonyOS and other
164
- environments without koffi prebuilds install out of the box, with no `pnpm-workspace.yaml` or
165
- other configuration needed; posix platforms use the posix fallback adapter (browser mode works;
166
- webview2 / native dialog unavailable), and the platform adapter is loaded conditionally so
167
- win32.js is never evaluated on posix.
168
171
  - If installing by editing `package.json` manually (not via the `dsh plugin` command), you must add
169
172
  both the `dependencies` entry and `dsh.profile.bundles`; when using a local `file:` dependency,
170
- `dsh web` normalizes `file:` to `^0.1.3` at startup, which does not affect runtime.
173
+ `dsh web` normalizes `file:` to `^0.1.5` at startup, which does not affect runtime.
171
174
 
172
175
  ## Icons
173
176
 
package/README.md CHANGED
@@ -54,8 +54,9 @@ Job Object(强杀也生效)+ 退出清理结束进程树。
54
54
  `browserPath`(浏览器可执行文件,支持「浏览」原生对话框选择;位于窗口类型
55
55
  下方,仅选择「浏览器应用窗口」时使能)、
56
56
  `exitOnWindowClose`(窗口关闭时退出 DSH,默认关闭)。
57
- 保存后持久化到
58
- settings 文档(命名空间 `auto-open-web`),首次保存后设置值优先于行配置。
57
+ 保存后经**官方 settings 域**(客户端 settingsScope)持久化到
58
+ settings 文档(命名空间 `auto-open-web`),首次保存后设置值优先于行配置;
59
+ 宿主仅保留「浏览」「测试」辅助路由(官方通道无法覆盖的能力)。
59
60
  2. **行配置**(cordis.patch.yml):作为启动种子,设置卡片保存前生效。
60
61
 
61
62
  | 字段 | 默认 | 说明 |
@@ -79,14 +80,14 @@ Job Object(强杀也生效)+ 退出清理结束进程树。
79
80
 
80
81
  本包是**组合包(bundle)**:一个附带配置层的 npm 包——`package.json` 的
81
82
  `dsh.bundle` 声明配置层文件(`cordis.patch.yml`),profile 安装它时按包名激活
82
- 插件行。已发布到 **npm registry**(`dsh-auto-open-web@0.1.3`)与 **GitHub**
83
+ 插件行。已发布到 **npm registry**(`dsh-auto-open-web@0.1.5`)与 **GitHub**
83
84
  (https://github.com/jinsiyu/dsh-auto-open-web,main 分支)。
84
85
 
85
86
  ### 打包
86
87
 
87
88
  ```bash
88
89
  cd dsh-auto-open-web
89
- pnpm pack # prepack 钩子自动先编译 WebView2 宿主(dotnet publish),产出 dsh-auto-open-web-0.1.3.tgz
90
+ pnpm pack # prepack 钩子自动先编译 WebView2 宿主(dotnet publish),产出 dsh-auto-open-web-0.1.5.tgz
90
91
  ```
91
92
 
92
93
  ### 安装方式(任选其一)
@@ -101,7 +102,7 @@ dsh plugin --profile web add C:\path\to\dsh-auto-open-web
101
102
  **方式二:tarball(发布产物,推荐交付;无需构建授权)**
102
103
 
103
104
  ```bash
104
- dsh plugin --profile web add ./dsh-auto-open-web-0.1.3.tgz
105
+ dsh plugin --profile web add ./dsh-auto-open-web-0.1.5.tgz
105
106
  ```
106
107
 
107
108
  **方式三:npm 注册表(发布后)**
@@ -137,23 +138,25 @@ dsh plugin --profile web remove dsh-auto-open-web # 同时移除依赖与对
137
138
 
138
139
  ### 注意事项
139
140
 
141
+ - **本包零 dependencies**:所有运行时依赖均由 **DSH 部署提供**,以 optional
142
+ peer 声明(安装无警告):
143
+ - `@deepseek-ai/dsh`(宿主,声明兼容范围 `>=0.1.0-rc.7 <0.2.0`,
144
+ `settings.plugin.item` 自该版本起为 keyed 插槽,注册需 `options.key`;
145
+ 不做运行时版本检测)
146
+ - `@deepseek-ai/schemastery`(配置 schema 校验器;运行时解析:常规 import
147
+ 优先,其次 Windows 全局 npm 布局下的 DSH 部署副本)
148
+ - `koffi`(仅 Windows 的 Job Object / 进程校验 / 原生对话框;同样运行时
149
+ 解析部署副本,失败仅降级)
150
+ 因此任何平台安装都无构建拦截——鸿蒙等无 koffi 预编译的环境开箱即装;
151
+ posix 平台走 posix 降级适配器(browser 模式可用;webview2 / 原生对话框
152
+ 不可用),且平台适配器按平台条件加载,posix 上完全不求值 win32.js。
140
153
  - **npm 包已含 WebView2 宿主编译产物**(prepack 编译后发布);**GitHub
141
154
  main 分支与源码 checkout 方式不含** `host-publish/`(构建产物被
142
155
  .gitignore 忽略):webview2 模式需先在 `node_modules/dsh-auto-open-web`
143
156
  下执行 `pnpm run build:host` 生成(需 .NET SDK);browser 模式无需构建。
144
- - **本包零 koffi 依赖声明**:koffi(驱动 Windows 的 Job Object / 进程校验 /
145
- 原生对话框)由 **DSH 部署自带**(官方原生选择器
146
- `@deepseek-ai/dsh-host-directory-picker-native` 依赖 koffi,部署必带),
147
- 插件运行时解析:常规解析(profile 已装/手动 `pnpm add koffi`)优先,
148
- 其次 Windows 全局 npm 布局下的 DSH 部署副本;解析失败仅降级(Job
149
- Object 不可用 → 退出/预清理兜底;进程校验跳过;对话框不可用)并记录
150
- 日志。**因此任何平台安装都无 koffi 构建拦截**——鸿蒙等无 koffi
151
- 预编译的环境开箱即装,无需 `pnpm-workspace.yaml` 等额外配置;posix
152
- 平台走 posix 降级适配器(browser 模式可用;webview2 / 原生对话框
153
- 不可用),且平台适配器按平台条件加载,posix 上完全不求值 win32.js。
154
157
  - 若手动编辑 `package.json` 安装(不经 dsh plugin 命令),需同时追加
155
158
  `dependencies` 与 `dsh.profile.bundles` 两项;使用本地 `file:` 依赖时
156
- `dsh web` 启动会把 `file:` 规范化成 `^0.1.3`,运行时不受影响。
159
+ `dsh web` 启动会把 `file:` 规范化成 `^0.1.5`,运行时不受影响。
157
160
 
158
161
  ## 图标
159
162
 
@@ -6,7 +6,7 @@
6
6
  "compilationOptions": {},
7
7
  "targets": {
8
8
  ".NETCoreApp,Version=v10.0": {
9
- "DshAppWindow/0.1.3": {
9
+ "DshAppWindow/0.1.5": {
10
10
  "dependencies": {
11
11
  "Microsoft.Web.WebView2": "1.0.3124.44",
12
12
  "Microsoft.Web.WebView2.Core": "1.0.3124.44",
@@ -63,7 +63,7 @@
63
63
  }
64
64
  },
65
65
  "libraries": {
66
- "DshAppWindow/0.1.3": {
66
+ "DshAppWindow/0.1.5": {
67
67
  "type": "project",
68
68
  "serviceable": false,
69
69
  "sha512": ""
Binary file
Binary file
Binary file
package/lib/client.js CHANGED
@@ -1,26 +1,22 @@
1
- // dsh-auto-open-web — Client bundle(设置卡片,官方控件 + 官方 webServer 数据通道)
1
+ // dsh-auto-open-web — Client bundle(设置卡片,与官方 Shell/Agent loop 卡片同构)
2
2
  // 格式:window.__ModuleLoader__.load({ id, factory });factory(require) 的
3
- // require 解析浏览器端冻结模块表(react、@deepseek-ai/dsh-client-ui-primitives 等)。
3
+ // require 解析浏览器端冻结模块表(react、@deepseek-ai/dsh-client-ui-primitives
4
+ // @deepseek-ai/dsh-client-schema-form 等)。
4
5
  //
5
- // 数据通道:设置卡片经官方 ctx.webServer JSON API 读写(第三方 settings 命名空间
6
- // 不在客户端白名单,故走自定义 API,与 proxy-router 卡片同一机制):
7
- // GET /auto-open-web/config → { ok, appWindow, windowKind,
8
- // browserPath, exitOnWindowClose, writable }
9
- // POST /auto-open-web/config → { ok, ... } | { ok:false, error }
10
- // POST /auto-open-web/pick-browser 原生"浏览"对话框(宿主子进程 + koffi
11
- // IFileOpenDialog,与官方工作区选择器同机制;不使用 PowerShell),返回
12
- // { ok:true, path } | { ok:false, cancelled:true, error }
13
- // 说明:网页 <input type=file> 拿不到本地完整路径,故浏览走宿主原生对话框。
6
+ // 架构完全对齐官方 @deepseek-ai/dsh-client-ui-settings-plugins:
7
+ // - 数据通道:settingsScope.bind({namespace})(官方 settings 域,dsh ≥0.1.0-rc.7
8
+ // 已取消第三方命名空间白名单)
9
+ // - 表单:CardForm 移植(FieldSpec format/parse、staged 草稿、plan/save 计划、
10
+ // 写后以 user 层落地校验、失败保留草稿)
11
+ // - 字段:ValueField 移植(overridden 徽章 + 恢复默认 + invalid 提示;browserPath
12
+ // 附带"浏览/测试"按钮槽);checkbox/radio 值型字段自绘但带同款徽章
13
+ // - 外壳:PluginCard 移植(折叠/未保存标记/只读提示/放弃/保存/保存失败)
14
+ // - 文案:locale 注册(zh/en),与官方同机制
15
+ // - 依赖:manifest 5 包 + 代码级 5 服务,与官方 settings-plugins 完全一致
14
16
  //
15
- // 注意:不做客户端 schema 校验——浏览器内置(frontend dist 冻结)
16
- // dsh-client-schema-form 副本对本插件 schema 执行 validateDraft 会抛
17
- // "n is not a function"(与 npm 安装版行为不一致),导致保存按钮永远置灰;
18
- // 保存由宿主权威校验(normalizeState + scope.update)并持久化到 settings。
19
- //
20
- // 官方 API 使用:
21
- // - 控件:Input / Button(官方 primitives,与全站设计系统一致)
22
- // - 草稿模型:draft 普通对象,setPath 不可变编辑
23
- // 卡片外壳保持内置 PluginCard 同款外观(与 proxy-router 卡片同一份 CSS)。
17
+ // 宿主保留的辅助路由(官方通道无法覆盖):
18
+ // POST /auto-open-web/pick-browser 原生"浏览"对话框
19
+ // POST /auto-open-web/test-browser 浏览器拉起测试
24
20
  window.__ModuleLoader__.load({
25
21
  id: 'dsh-auto-open-web',
26
22
  factory: (require) => {
@@ -29,8 +25,8 @@ window.__ModuleLoader__.load({
29
25
  Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' })
30
26
  let React = require('react')
31
27
 
32
- // ---- 内置 PluginCard.module.css(默认卡片外观,原样注入) ----
33
- var CARD_CSS = '.YyYd_a_card{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-3);border-radius:12px;list-style:none;transition:border-color .16s,background .16s}.YyYd_a_card:hover{border-color:var(--dsw-alias-label-dimmed,var(--dsw-alias-border-l2))}.YyYd_a_cardOpen{background:var(--dsw-alias-bg-layer-2);border-color:var(--dsw-alias-label-dimmed,var(--dsw-alias-border-l2))}.YyYd_a_header{appearance:none;width:100%;font:inherit;color:inherit;text-align:left;cursor:pointer;background:0 0;border:0;border-radius:12px;align-items:center;gap:12px;padding:14px 16px;display:flex}.YyYd_a_header:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:-2px}.YyYd_a_headText{flex-direction:column;flex:1;gap:4px;min-width:0;display:flex}.YyYd_a_name{color:var(--dsw-alias-label-primary);font-size:15px;font-weight:600;line-height:1.4}.YyYd_a_description{color:var(--dsw-alias-label-tertiary,var(--dsw-alias-label-secondary));font-size:13px;line-height:1.5}.YyYd_a_chevron{color:var(--dsw-alias-label-tertiary,var(--dsw-alias-label-secondary));flex:none;transition:transform .16s}.YyYd_a_chevronOpen{transform:rotate(180deg)}.YyYd_a_body{border-top:1px solid var(--dsw-alias-border-l2);margin:0 16px;padding-bottom:8px}.YyYd_a_readOnly{color:var(--dsw-alias-label-tertiary,var(--dsw-alias-label-secondary));margin:12px 0 0;font-size:12px;line-height:1.5}.YyYd_a_pending{white-space:nowrap;background:var(--dsw-alias-bg-module-platform,var(--dsw-alias-bg-layer-2));color:var(--dsw-alias-label-secondary);border-radius:999px;flex:none;padding:1px 8px;font-size:11px;font-weight:500;line-height:17px}.YyYd_a_footer{border-top:1px solid var(--dsw-alias-border-l2);justify-content:flex-end;align-items:center;gap:8px;padding:12px 0 4px;display:flex}.YyYd_a_failed{min-width:0;color:var(--dsw-alias-label-error,var(--dsw-alias-state-error-primary));flex:1;margin:0;font-size:12px;line-height:1.5}.YyYd_a_discard,.YyYd_a_save{appearance:none;font:inherit;cursor:pointer;border:1px solid #0000;border-radius:8px;padding:5px 14px;font-size:13px;line-height:1.5}.YyYd_a_discard{border-color:var(--dsw-alias-border-l2);color:var(--dsw-alias-label-secondary);background:0 0}.YyYd_a_save{background:var(--dsw-alias-brand-primary);color:var(--dsw-alias-label-inverse,#fff);border-color:var(--dsw-alias-brand-primary)}.YyYd_a_save:hover:not(:disabled){filter:brightness(.95)}.YyYd_a_discard:disabled,.YyYd_a_save:disabled{opacity:.4;cursor:default}'
28
+ // ---- 内置 PluginCard.module.css(官方卡片外观,原样注入) ----
29
+ var CARD_CSS = '.YyYd_a_card{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-3);border-radius:12px;list-style:none;transition:border-color .16s,background .16s}.YyYd_a_card:hover{border-color:var(--dsw-alias-label-dimmed,var(--dsw-alias-border-l2))}.YyYd_a_cardOpen{background:var(--dsw-alias-bg-layer-2);border-color:var(--dsw-alias-label-dimmed,var(--dsw-alias-border-l2))}.YyYd_a_header{appearance:none;width:100%;font:inherit;color:inherit;text-align:left;cursor:pointer;background:0 0;border:0;border-radius:12px;align-items:center;gap:12px;padding:14px 16px;display:flex}.YyYd_a_header:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:-2px}.YyYd_a_headText{flex-direction:column;flex:1;gap:4px;min-width:0;display:flex}.YyYd_a_name{color:var(--dsw-alias-label-primary);font-size:15px;font-weight:600;line-height:1.4}.YyYd_a_description{color:var(--dsw-alias-label-tertiary,var(--dsw-alias-label-secondary));font-size:13px;line-height:1.5}.YyYd_a_chevron{color:var(--dsw-alias-label-tertiary,var(--dsw-alias-label-secondary));flex:none;transition:transform .16s}.YyYd_a_chevronOpen{transform:rotate(180deg)}.YyYd_a_body{border-top:1px solid var(--dsw-alias-border-l2);margin:0 16px;padding-bottom:8px}.YyYd_a_readOnly{color:var(--dsw-alias-label-tertiary,var(--dsw-alias-label-secondary));margin:12px 0 0;font-size:12px;line-height:1.5}.YyYd_a_pending{white-space:nowrap;background:var(--dsw-alias-bg-module-platform,var(--dsw-alias-bg-layer-2));color:var(--dsw-alias-label-secondary);border-radius:999px;flex:none;padding:1px 8px;font-size:11px;font-weight:500;line-height:17px}.YyYd_a_footer{border-top:1px solid var(--dsw-alias-border-l2);justify-content:flex-end;align-items:center;gap:8px;padding:12px 0 4px;display:flex}.YyYd_a_failed{min-width:0;color:var(--dsw-alias-label-error,var(--dsw-alias-state-error-primary));flex:1;margin:0;font-size:12px;line-height:1.5}.YyYd_a_discard,.YyYd_a_save{appearance:none;font:inherit;cursor:pointer;border:1px solid #0000;border-radius:8px;padding:5px 14px;font-size:13px;line-height:1.5}.YyYd_a_discard{border-color:var(--dsw-alias-border-l2);color:var(--dsw-alias-label-secondary);background:0 0}.YyYd_a_discard:disabled{opacity:.55;cursor:default}.YyYd_a_discard:hover:not(:disabled){color:var(--dsw-alias-label-primary)}.YyYd_a_save{background:var(--dsw-alias-brand-primary);color:var(--dsw-alias-label-inverse,#fff);font-weight:600}.YyYd_a_save:disabled{opacity:.55;cursor:default}'
34
30
  var CARD_TAG = 'dsh-auto-open-web/PluginCard.module.css'
35
31
  // ---- 内置 ValueField 字段布局样式(label/hint 排版,原样注入) ----
36
32
  var FIELDS_CSS = '.At1oFq_field{flex-direction:column;gap:6px;padding:12px 0;display:flex}.At1oFq_field+.At1oFq_field{border-top:1px solid var(--dsw-alias-border-l2)}.At1oFq_head{align-items:center;gap:8px;display:flex}.At1oFq_label{min-width:0;color:var(--dsw-alias-label-primary);flex:1;font-size:13px;font-weight:500;line-height:1.5}.At1oFq_badges{align-items:center;gap:8px;display:inline-flex}.At1oFq_badge{white-space:nowrap;background:var(--dsw-alias-bg-module-platform,var(--dsw-alias-bg-layer-2));color:var(--dsw-alias-label-secondary);border-radius:999px;padding:1px 8px;font-size:11px;font-weight:500;line-height:17px}.At1oFq_badgeMuted{white-space:nowrap;color:var(--dsw-alias-label-tertiary,var(--dsw-alias-label-secondary));border-radius:999px;padding:1px 8px;font-size:11px;line-height:17px}.At1oFq_reset{font:inherit;color:var(--dsw-alias-label-secondary);cursor:pointer;background:0 0;border:none;padding:0;font-size:12px;line-height:1.5}.At1oFq_reset:hover:not(:disabled){color:var(--dsw-alias-label-primary)}.At1oFq_reset:disabled{cursor:default}.At1oFq_input{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-3);height:34px;font:inherit;color:var(--dsw-alias-label-primary);border-radius:8px;padding:0 12px;font-size:13px;line-height:1.5}.At1oFq_input:focus-visible{border-color:var(--dsw-alias-brand-primary);outline:none}.At1oFq_input:disabled{color:var(--dsw-alias-label-tertiary,var(--dsw-alias-label-secondary));cursor:default}.At1oFq_inputInvalid{border-color:var(--dsw-alias-label-error,var(--dsw-alias-state-error-primary));}.At1oFq_invalid{color:var(--dsw-alias-label-error,var(--dsw-alias-state-error-primary));margin:0;font-size:12px;line-height:1.5}.At1oFq_hint{color:var(--dsw-alias-label-tertiary,var(--dsw-alias-label-secondary));margin:0;font-size:12px;line-height:1.5}'
@@ -42,7 +38,7 @@ window.__ModuleLoader__.load({
42
38
  '.aow-check{display:flex;align-items:center;gap:8px;cursor:pointer}' +
43
39
  '.aow-check input{accent-color:var(--dsw-alias-brand-primary);width:15px;height:15px;margin:0;flex:none}' +
44
40
  '.aow-check input:disabled{cursor:default}' +
45
- '.aow-test-ok{color:var(--dsw-alias-state-success-primary,#3a8a4a);margin:4px 0 0;font-size:12px;line-height:1.5}' +
41
+ '.aow-test-ok{color:var(--dsw-alias-state-success-primary);margin:4px 0 0;font-size:12px;line-height:1.5}' +
46
42
  '.aow-test-fail{color:var(--dsw-alias-label-error,var(--dsw-alias-state-error-primary));margin:4px 0 0;font-size:12px;line-height:1.5}'
47
43
  var EXTRA_TAG = 'dsh-auto-open-web/Extra.module.css'
48
44
  function injectCss(tagId, css) {
@@ -58,33 +54,22 @@ window.__ModuleLoader__.load({
58
54
  injectCss(FIELDS_TAG, FIELDS_CSS)
59
55
  injectCss(EXTRA_TAG, EXTRA_CSS)
60
56
 
61
- // ---- 官方共享模块(冻结表):控件 + 草稿/校验模型 ----
62
- // require 失败时降级而非崩溃(卡片仍然渲染,仅退回基础能力)
57
+ // ---- 官方共享模块(冻结表) ----
63
58
  var prim = null
64
59
  var form = null
60
+ var runtime = null
65
61
  try {
66
62
  prim = require('@deepseek-ai/dsh-client-ui-primitives')
67
63
  form = require('@deepseek-ai/dsh-client-schema-form')
64
+ runtime = require('@deepseek-ai/dsh-client-runtime')
68
65
  } catch (e) {
69
66
  console.error('[auto-open-web] shared modules require failed:', e !== null && e !== undefined && e.message !== undefined ? e.message : String(e))
70
67
  }
71
68
  var Button = prim !== null && prim.Button !== undefined ? prim.Button : null
72
69
  var Input = prim !== null && prim.Input !== undefined ? prim.Input : null
73
70
  var ChevronIcon = prim !== null && prim.IconChevronDownOutline14 !== undefined ? prim.IconChevronDownOutline14 : null
74
- // 注意:不在此处使用官方 rehydrateSchema/validateDraft——浏览器内置(frontend
75
- // dist 冻结)副本对本插件 schema 校验会抛 "n is not a function" 异常(兼容性
76
- // 差异),导致 invalid 恒真、保存按钮置灰;宿主侧 normalizeState + scope.update
77
- // 才是权威校验,本卡片字段也无格式约束,故客户端只做草稿编辑(setPath)。
78
- var setPath = form !== null ? form.setPath : null
71
+ var createSnapshotStore = runtime !== null && runtime.createSnapshotStore !== undefined ? runtime.createSnapshotStore : null
79
72
 
80
- // ---- 草稿编辑:优先官方 setPath,不可用时降级为本地实现 ----
81
- function applySetPath(draftObj, path, value) {
82
- if (setPath !== null) return setPath(draftObj, path, value)
83
- const next = Object.assign({}, draftObj)
84
- next[path[0]] = value
85
- return next
86
- }
87
- // 控件:优先官方 Input/Button,不可用时退回原生元素(用内置字段样式)
88
73
  function ProxyInput(props) {
89
74
  if (Input !== null) return React.createElement(Input, props)
90
75
  const rest = Object.assign({}, props)
@@ -120,278 +105,563 @@ window.__ModuleLoader__.load({
120
105
  React.createElement('span', { className: 'At1oFq_hint' }, props.children)
121
106
  )
122
107
  }
108
+ /** 字段头部"已覆盖 + 恢复默认"徽章(官方 ValueField 同款)。 */
109
+ function FieldHeadBadges(props) {
110
+ if (!props.overridden) return null
111
+ return React.createElement('span', { className: 'At1oFq_badges' },
112
+ React.createElement('span', { className: 'At1oFq_badge' }, props.overriddenLabel),
113
+ React.createElement('button', { type: 'button', className: 'At1oFq_reset', disabled: props.disabled, onClick: props.onReset }, props.resetLabel)
114
+ )
115
+ }
123
116
 
124
- function AutoOpenSettingsPage() {
117
+ // ---- 官方 PluginCard 移植(settings.plugin.item 卡片外壳) ----
118
+ function PluginCard(props) {
125
119
  const [open, setOpen] = React.useState(false)
126
- const [config, setConfig] = React.useState(null) // { appWindow, windowKind, browserPath, exitOnWindowClose, writable } | null
127
- const [loadError, setLoadError] = React.useState('')
128
- const [reloadTick, setReloadTick] = React.useState(0)
129
- const [draft, setDraft] = React.useState({ appWindow: true, windowKind: 'webview2', browserPath: '', exitOnWindowClose: false })
130
- const [busy, setBusy] = React.useState(false)
131
- const [picking, setPicking] = React.useState(false)
132
- const [testing, setTesting] = React.useState(false)
133
- const [testResult, setTestResult] = React.useState(null) // { ok, text } | null
134
- const [failedText, setFailedText] = React.useState('')
135
-
136
- // 配置加载:官方 webServer API(GET /auto-open-web/config)
137
- React.useEffect(() => {
138
- let cancelled = false
139
- fetch('/auto-open-web/config')
140
- .then((response) => response.json())
141
- .then((result) => {
142
- if (cancelled) return
143
- if (result === null || result === undefined || result.ok !== true) {
144
- setLoadError(result !== null && result !== undefined && typeof result.error === 'string' ? result.error : '配置加载失败')
145
- return
146
- }
147
- setConfig({
148
- appWindow: result.appWindow !== false,
149
- windowKind: result.windowKind === 'browser' ? 'browser' : 'webview2',
150
- browserPath: typeof result.browserPath === 'string' ? result.browserPath : '',
151
- exitOnWindowClose: result.exitOnWindowClose === true,
152
- writable: result.writable !== false,
153
- })
154
- })
155
- .catch((error) => {
156
- if (!cancelled) setLoadError('配置加载失败: ' + String(error !== null && error !== undefined && error.message !== undefined ? error.message : error))
157
- })
158
- return () => { cancelled = true }
159
- }, [reloadTick])
160
-
161
- const saved = config !== null
162
- ? { appWindow: config.appWindow, windowKind: config.windowKind, browserPath: config.browserPath, exitOnWindowClose: config.exitOnWindowClose }
163
- : null
164
-
165
- // 快照值 → 草稿(外部变化/保存后同步)
166
- React.useEffect(() => {
167
- if (saved !== null) setDraft({
168
- appWindow: saved.appWindow,
169
- windowKind: saved.windowKind,
170
- browserPath: saved.browserPath,
171
- exitOnWindowClose: saved.exitOnWindowClose,
172
- })
173
- }, [saved !== null ? JSON.stringify(saved) : 'none'])
174
-
175
- const loading = config === null && loadError === ''
176
- const writable = config !== null ? config.writable !== false : true
177
- // 客户端不做 schema 校验(浏览器内置 schema-form 兼容性问题,见上方注释);
178
- // 保存由宿主权威校验(normalizeState + scope.update)。
179
- const invalid = false
180
- const dirty = saved !== null && (
181
- draft.appWindow !== saved.appWindow ||
182
- draft.windowKind !== saved.windowKind ||
183
- draft.browserPath !== saved.browserPath ||
184
- draft.exitOnWindowClose !== saved.exitOnWindowClose
120
+ const { state } = props
121
+ if (!state.available) return null
122
+ const title = props.t(props.titleKey)
123
+ const blocked = !state.dirty || state.invalid || state.saving
124
+ return React.createElement('li', { className: 'YyYd_a_card' + (open ? ' YyYd_a_cardOpen' : '') },
125
+ React.createElement('button', {
126
+ type: 'button',
127
+ className: 'YyYd_a_header',
128
+ 'aria-expanded': open,
129
+ 'aria-label': (open ? props.t('collapse') : props.t('expand')) + ': ' + title,
130
+ onClick: () => setOpen(!open)
131
+ },
132
+ React.createElement('span', { className: 'YyYd_a_headText' },
133
+ React.createElement('span', { className: 'YyYd_a_name' }, title),
134
+ React.createElement('span', { className: 'YyYd_a_description' }, props.t(props.descriptionKey))
135
+ ),
136
+ state.dirty ? React.createElement('span', { className: 'YyYd_a_pending' }, props.t('unsaved')) : null,
137
+ ChevronIcon !== null
138
+ ? React.createElement(ChevronIcon, { className: 'YyYd_a_chevron' + (open ? ' YyYd_a_chevronOpen' : '') })
139
+ : React.createElement('span', { className: 'YyYd_a_chevron' + (open ? ' YyYd_a_chevronOpen' : '') }, '▾')
140
+ ),
141
+ open ? React.createElement('div', { className: 'YyYd_a_body' },
142
+ !state.writable ? React.createElement('p', { className: 'YyYd_a_readOnly', role: 'status' }, props.t('readOnly')) : null,
143
+ props.children,
144
+ React.createElement('div', { className: 'YyYd_a_footer' },
145
+ state.failed ? React.createElement('p', { className: 'YyYd_a_failed', role: 'status' }, props.t('saveFailed')) : null,
146
+ React.createElement('button', { type: 'button', className: 'YyYd_a_discard', disabled: !state.dirty || state.saving, onClick: props.onDiscard }, props.t('discard')),
147
+ React.createElement('button', { type: 'button', className: 'YyYd_a_save', disabled: blocked, onClick: props.onSave }, state.saving ? props.t('saving') : props.t('save'))
148
+ )
149
+ ) : null
185
150
  )
151
+ }
186
152
 
187
- const blocked = saved === null || !dirty || invalid || busy
188
-
189
- const save = () => {
190
- if (blocked) return
191
- setBusy(true)
192
- setFailedText('')
193
- fetch('/auto-open-web/config', {
194
- method: 'POST',
195
- headers: { 'content-type': 'application/json' },
196
- body: JSON.stringify({
197
- appWindow: draft.appWindow,
198
- windowKind: draft.windowKind,
199
- browserPath: draft.browserPath,
200
- exitOnWindowClose: draft.exitOnWindowClose,
153
+ // ---- 官方 ValueField 移植(带 overridden 徽章/重置/invalid;extra 为按钮槽) ----
154
+ function ValueField(props) {
155
+ return React.createElement('div', { className: 'At1oFq_field' },
156
+ React.createElement('div', { className: 'At1oFq_head' },
157
+ React.createElement('label', { className: 'At1oFq_label', htmlFor: props.id }, props.label),
158
+ props.overridden
159
+ ? React.createElement('span', { className: 'At1oFq_badges' },
160
+ React.createElement('span', { className: 'At1oFq_badge' }, props.overriddenLabel),
161
+ React.createElement('button', { type: 'button', className: 'At1oFq_reset', disabled: props.disabled, onClick: props.onReset }, props.resetLabel)
162
+ )
163
+ : null
164
+ ),
165
+ React.createElement('div', { className: 'aow-row' },
166
+ React.createElement('input', {
167
+ id: props.id,
168
+ className: props.invalid ? 'At1oFq_input At1oFq_inputInvalid' : 'At1oFq_input',
169
+ type: 'text',
170
+ value: props.text,
171
+ placeholder: props.placeholder !== undefined ? props.placeholder : '',
172
+ disabled: props.disabled,
173
+ onChange: (event) => props.onEdit(event.target.value)
201
174
  }),
202
- })
203
- .then((response) => response.json())
204
- .then((result) => {
205
- if (result !== null && result !== undefined && result.ok === true) {
206
- setConfig((prev) => prev !== null ? {
207
- appWindow: result.appWindow !== false,
208
- windowKind: result.windowKind === 'browser' ? 'browser' : 'webview2',
209
- browserPath: typeof result.browserPath === 'string' ? result.browserPath : '',
210
- exitOnWindowClose: result.exitOnWindowClose === true,
211
- writable: prev.writable,
212
- } : prev)
213
- } else {
214
- setFailedText('保存失败: ' + (result !== null && result !== undefined && typeof result.error === 'string' ? result.error : '未知错误'))
215
- }
216
- })
217
- .catch((error) => { setFailedText('保存失败: ' + String(error !== null && error !== undefined && error.message !== undefined ? error.message : error)) })
218
- .then(() => { setBusy(false) })
219
- }
175
+ props.extra !== undefined ? props.extra : null
176
+ ),
177
+ React.createElement('p', { className: props.invalid ? 'At1oFq_invalid' : 'At1oFq_hint' },
178
+ props.invalid ? props.invalidLabel : props.hint
179
+ ),
180
+ props.footer !== undefined ? props.footer : null
181
+ )
182
+ }
220
183
 
221
- const discard = () => {
222
- if (saved !== null) setDraft({
223
- appWindow: saved.appWindow,
224
- windowKind: saved.windowKind,
225
- browserPath: saved.browserPath,
226
- exitOnWindowClose: saved.exitOnWindowClose,
227
- })
228
- setFailedText('')
184
+ // ---- 官方 CardForm 移植(字段规格 + 草稿计划 + 保存落地校验) ----
185
+ /** 自由文本字段:空草稿 = 清除(恢复默认) */
186
+ function textField(field) {
187
+ return {
188
+ field,
189
+ format: (value) => (typeof value === 'string' ? value : ''),
190
+ parse: (text) => {
191
+ const trimmed = String(text).trim()
192
+ return trimmed === '' ? { kind: 'clear' } : { kind: 'set', value: trimmed }
193
+ }
194
+ }
195
+ }
196
+ /** 布尔字段(checkbox):值直接作为草稿。 */
197
+ function boolField(field) {
198
+ return {
199
+ field,
200
+ format: (value) => value === true,
201
+ parse: (value) => ({ kind: 'set', value: value === true })
202
+ }
203
+ }
204
+ /** 窗口类型字段(radio):仅两个合法值。 */
205
+ function windowKindField(field) {
206
+ return {
207
+ field,
208
+ format: (value) => (value === 'browser' ? 'browser' : 'webview2'),
209
+ parse: (value) => (value === 'browser' || value === 'webview2' ? { kind: 'set', value } : undefined)
210
+ }
211
+ }
212
+ /** 表单:staged 草稿 → 保存计划;写后以 user 层落地为准。 */
213
+ function CardForm(scope, specs) {
214
+ this.scope = scope
215
+ this.specs = new Map(specs.map((spec) => [spec.field, spec]))
216
+ this.staged = new Map()
217
+ this.listeners = new Set()
218
+ this.saving = false
219
+ this.failed = false
220
+ scope.subscribe(() => {
221
+ this.publish()
222
+ })
223
+ }
224
+ CardForm.prototype.bind = function (project) {
225
+ const store = createSnapshotStore(project())
226
+ this.listeners.add(() => {
227
+ store.set(project())
228
+ })
229
+ return store
230
+ }
231
+ CardForm.prototype.shell = function () {
232
+ const snapshot = this.scope.getSnapshot()
233
+ const plan = this.plan()
234
+ return {
235
+ available: snapshot.status === 'ready',
236
+ writable: snapshot.writable,
237
+ dirty: plan.length > 0,
238
+ invalid: plan.some((item) => item.run === undefined),
239
+ saving: this.saving,
240
+ failed: this.failed
241
+ }
242
+ }
243
+ CardForm.prototype.field = function (field) {
244
+ const staged = this.staged.get(field)
245
+ const spec = this.specs.get(field)
246
+ if (spec === undefined) throw new Error('plugin card has no field ' + field)
247
+ if (staged === undefined) return {
248
+ text: spec.format(this.sectionValue(field)),
249
+ overridden: this.stored(field),
250
+ invalid: false
251
+ }
252
+ const write = staged.clear ? { kind: 'clear' } : spec.parse(staged.text)
253
+ return {
254
+ text: staged.text,
255
+ overridden: write !== undefined && write.kind === 'set',
256
+ invalid: write === undefined
257
+ }
258
+ }
259
+ CardForm.prototype.actions = function () {
260
+ return {
261
+ edit: (field, text) => {
262
+ this.stage(field, { text, clear: false })
263
+ },
264
+ resetField: (field) => {
265
+ this.stage(field, { text: this.specs.get(field).format(this.baseValue(field)), clear: true })
266
+ },
267
+ save: () => {
268
+ this.save()
269
+ },
270
+ discard: () => {
271
+ if (this.staged.size === 0 && !this.failed) return
272
+ this.staged.clear()
273
+ this.failed = false
274
+ this.publish()
275
+ }
229
276
  }
277
+ }
278
+ CardForm.prototype.save = function () {
279
+ const plan = this.plan()
280
+ const writes = plan.flatMap((item) => item.run === undefined ? [] : [item.run])
281
+ if (plan.length === 0 || this.saving || writes.length !== plan.length) return
282
+ this.saving = true
283
+ this.failed = false
284
+ this.publish()
285
+ const run = async () => {
286
+ let landed = true
287
+ for (const write of writes) landed = (await write()) && landed
288
+ if (landed) this.staged.clear()
289
+ this.saving = false
290
+ this.failed = !landed
291
+ this.publish()
292
+ }
293
+ run().catch(() => {
294
+ this.saving = false
295
+ this.failed = true
296
+ this.publish()
297
+ })
298
+ }
299
+ CardForm.prototype.plan = function () {
300
+ const plan = []
301
+ for (const [field, staged] of this.staged) {
302
+ const spec = this.specs.get(field)
303
+ if (staged.clear) {
304
+ if (this.stored(field)) plan.push({ field, run: () => this.clear(field) })
305
+ continue
306
+ }
307
+ if (staged.text === spec.format(this.sectionValue(field))) continue
308
+ const write = spec.parse(staged.text)
309
+ if (write === undefined) plan.push({ field, run: undefined })
310
+ else if (write.kind === 'clear') plan.push({ field, run: () => this.clear(field) })
311
+ else plan.push({ field, run: () => this.store(field, write.value) })
312
+ }
313
+ return plan
314
+ }
315
+ CardForm.prototype.stage = function (field, edit) {
316
+ this.staged.set(field, edit)
317
+ this.failed = false
318
+ this.publish()
319
+ }
320
+ CardForm.prototype.clear = async function (field) {
321
+ await this.scope.unset(field)
322
+ return !this.stored(field)
323
+ }
324
+ CardForm.prototype.store = async function (field, value) {
325
+ await this.scope.set(field, value)
326
+ const user = this.userLayer()
327
+ return user !== undefined && user !== null && user[field] === value
328
+ }
329
+ CardForm.prototype.snapshotOf = function () {
330
+ return this.scope.getSnapshot()
331
+ }
332
+ CardForm.prototype.sectionValue = function (field) {
333
+ const snapshot = this.snapshotOf()
334
+ return snapshot.value !== undefined && snapshot.value !== null ? snapshot.value[field] : undefined
335
+ }
336
+ CardForm.prototype.baseValue = function (field) {
337
+ const snapshot = this.snapshotOf()
338
+ return snapshot.base !== undefined && snapshot.base !== null ? snapshot.base[field] : undefined
339
+ }
340
+ CardForm.prototype.userLayer = function () {
341
+ return this.snapshotOf().user
342
+ }
343
+ CardForm.prototype.stored = function (field) {
344
+ const user = this.userLayer()
345
+ return user !== undefined && user !== null && Object.prototype.hasOwnProperty.call(user, field)
346
+ }
347
+ CardForm.prototype.publish = function () {
348
+ for (const listener of this.listeners) listener()
349
+ }
230
350
 
231
- const retryLoad = () => {
232
- setLoadError('')
233
- setReloadTick((tick) => tick + 1)
351
+ // ---- 卡片控制器:scope CardForm → 投影 store + 动作面 ----
352
+ function AutoOpenCardController(scope) {
353
+ this.form = new CardForm(scope, [boolField('appWindow'), windowKindField('windowKind'), textField('browserPath'), boolField('exitOnWindowClose')])
354
+ this.store = this.form.bind(() => this.projection())
355
+ }
356
+ AutoOpenCardController.prototype.projection = function () {
357
+ return {
358
+ ...this.form.shell(),
359
+ appWindow: this.form.field('appWindow'),
360
+ windowKind: this.form.field('windowKind'),
361
+ browserPath: this.form.field('browserPath'),
362
+ exitOnWindowClose: this.form.field('exitOnWindowClose')
363
+ }
364
+ }
365
+ AutoOpenCardController.prototype.inject = function () {
366
+ return {
367
+ hooks: { autoOpenCard: this.store },
368
+ ...this.form.actions()
234
369
  }
370
+ }
371
+
372
+ // ---- 卡片组件(官方 BashCard 同构:PluginCard 包字段) ----
373
+ function AutoOpenSettingsPage(props) {
374
+ const { t } = props
375
+ const state = props.useAutoOpenCard((snapshot) => snapshot)
376
+ const [picking, setPicking] = React.useState(false)
377
+ const [testing, setTesting] = React.useState(false)
378
+ const [testResult, setTestResult] = React.useState(null) // { ok, text } | null
235
379
 
236
- // 原生"浏览"对话框:宿主弹出文件选择器,选中后回填路径
237
380
  const browse = () => {
238
- if (!writable || picking) return
381
+ if (!state.writable || picking) return
239
382
  setPicking(true)
240
- setFailedText('')
241
383
  fetch('/auto-open-web/pick-browser', { method: 'POST' })
242
384
  .then((response) => response.json())
243
385
  .then((result) => {
244
386
  if (result !== null && result !== undefined && result.ok === true && typeof result.path === 'string') {
245
- setDraft(applySetPath(draft, ['browserPath'], result.path))
246
- } else if (result === null || result === undefined || result.cancelled !== true) {
247
- setFailedText('选择失败: ' + (result !== null && result !== undefined && typeof result.error === 'string' ? result.error : '未知错误'))
387
+ props.edit('browserPath', result.path)
248
388
  }
249
389
  })
250
- .catch((error) => { setFailedText('选择失败: ' + String(error !== null && error !== undefined && error.message !== undefined ? error.message : error)) })
390
+ .catch(() => { /* 选择失败静默;用户可重试 */ })
251
391
  .then(() => { setPicking(false) })
252
392
  }
253
393
 
254
- // 测试:真正拉起 --app 专用测试实例,确认浏览器可被成功启动
255
- // (使用当前草稿路径,未保存也能测试;结果展示在字段下方)
256
394
  const testBrowser = () => {
257
395
  if (testing) return
258
396
  setTesting(true)
259
397
  setTestResult(null)
260
- setFailedText('')
261
398
  fetch('/auto-open-web/test-browser', {
262
399
  method: 'POST',
263
400
  headers: { 'content-type': 'application/json' },
264
- body: JSON.stringify({ browserPath: draft.browserPath }),
401
+ body: JSON.stringify({ browserPath: state.browserPath.text }),
265
402
  })
266
403
  .then((response) => response.json())
267
404
  .then((result) => {
268
405
  if (result !== null && result !== undefined && result.ok === true) {
269
- setTestResult({ ok: true, text: typeof result.message === 'string' ? result.message : '浏览器拉起成功' })
406
+ setTestResult({ ok: true, text: typeof result.message === 'string' ? result.message : t('testOk') })
270
407
  } else {
271
- setTestResult({ ok: false, text: '测试失败: ' + (result !== null && result !== undefined && typeof result.error === 'string' ? result.error : '未知错误') })
408
+ setTestResult({ ok: false, text: t('testFail') + ': ' + (result !== null && result !== undefined && typeof result.error === 'string' ? result.error : 'unknown') })
272
409
  }
273
410
  })
274
- .catch((error) => { setTestResult({ ok: false, text: '测试失败: ' + String(error !== null && error !== undefined && error.message !== undefined ? error.message : error) }) })
411
+ .catch((error) => { setTestResult({ ok: false, text: t('testFail') + ': ' + String(error !== null && error !== undefined && error.message !== undefined ? error.message : error) }) })
275
412
  .then(() => { setTesting(false) })
276
413
  }
277
414
 
278
- const chevronCls = 'YyYd_a_chevron' + (open ? ' YyYd_a_chevronOpen' : '')
415
+ const browserDisabled = !state.writable || state.windowKind.text !== 'browser'
279
416
 
280
- return React.createElement('li', { className: 'YyYd_a_card' + (open ? ' YyYd_a_cardOpen' : '') },
281
- React.createElement('button', {
282
- type: 'button',
283
- className: 'YyYd_a_header',
284
- 'aria-expanded': open,
285
- 'aria-label': (open ? '折叠' : '展开') + ': 自动打开网页',
286
- onClick: () => setOpen(!open)
287
- },
288
- React.createElement('span', { className: 'YyYd_a_headText' },
289
- React.createElement('span', { className: 'YyYd_a_name' }, '自动打开网页'),
290
- React.createElement('span', { className: 'YyYd_a_description' }, 'DSH 启动后自动打开独立应用窗口或网页;可手动指定浏览器位置')
417
+ return React.createElement(PluginCard, {
418
+ t,
419
+ titleKey: 'title',
420
+ descriptionKey: 'description',
421
+ state,
422
+ onSave: props.save,
423
+ onDiscard: props.discard
424
+ },
425
+ // ---- 独立应用窗口 ----
426
+ React.createElement('div', { className: 'At1oFq_field' },
427
+ React.createElement('div', { className: 'At1oFq_head' },
428
+ React.createElement('span', { className: 'At1oFq_label' }, t('appWindow')),
429
+ React.createElement(FieldHeadBadges, { overridden: state.appWindow.overridden, disabled: !state.writable, overriddenLabel: t('overridden'), resetLabel: t('reset'), onReset: () => props.resetField('appWindow') })
291
430
  ),
292
- dirty ? React.createElement('span', { className: 'YyYd_a_pending' }, '未保存') : null,
293
- ChevronIcon !== null
294
- ? React.createElement(ChevronIcon, { className: chevronCls })
295
- : React.createElement('span', { className: chevronCls }, '▾')
431
+ React.createElement(Checkbox, { checked: state.appWindow.text === true, disabled: !state.writable, onChange: (v) => props.edit('appWindow', v) }, t('appWindowHint'))
296
432
  ),
297
- open ? React.createElement('div', { className: 'YyYd_a_body' },
298
- !writable ? React.createElement('p', { className: 'YyYd_a_readOnly', role: 'status' }, '当前配置只读(settings 不可写)。') : null,
299
- loading
300
- ? React.createElement('p', { className: 'At1oFq_hint' }, '配置加载中…')
301
- : (loadError !== ''
302
- ? React.createElement('div', { className: 'At1oFq_field' },
303
- React.createElement('p', { className: 'At1oFq_invalid' }, loadError),
304
- React.createElement('div', { className: 'aow-row' },
305
- React.createElement(ProxyButton, { variant: 'outline', size: 'sm', onClick: retryLoad }, '重试')
306
- )
307
- )
308
- : React.createElement(React.Fragment, null,
309
- // ---- 独立应用窗口 ----
310
- React.createElement('div', { className: 'At1oFq_field' },
311
- React.createElement('div', { className: 'At1oFq_head' },
312
- React.createElement('span', { className: 'At1oFq_label' }, '独立应用窗口')
313
- ),
314
- React.createElement(Checkbox, { checked: draft.appWindow, disabled: !writable, onChange: (v) => setDraft(applySetPath(draft, ['appWindow'], v)) },
315
- '启动时自动打开独立应用窗口;关闭后不自动打开任何窗口'
316
- )
317
- ),
318
- // ---- 窗口类型 ----
319
- React.createElement('div', { className: 'At1oFq_field' },
320
- React.createElement('div', { className: 'At1oFq_head' },
321
- React.createElement('span', { className: 'At1oFq_label' }, '窗口类型')
322
- ),
323
- React.createElement(Radio, { checked: draft.windowKind === 'webview2', disabled: !writable, value: 'webview2', onChange: (v) => setDraft(applySetPath(draft, ['windowKind'], v)) },
324
- 'WebView2 宿主(独立进程,任务栏 DSH 图标,随 DSH 退出;需 Windows + WebView2 运行时)'
325
- ),
326
- React.createElement(Radio, { checked: draft.windowKind === 'browser', disabled: !writable, value: 'browser', onChange: (v) => setDraft(applySetPath(draft, ['windowKind'], v)) },
327
- '浏览器应用窗口(--app 专用实例,不与其他浏览器页面共用进程)'
328
- )
329
- ),
330
- // ---- 浏览器可执行文件(仅"浏览器应用窗口"模式使能) ----
331
- React.createElement('div', { className: 'At1oFq_field' },
332
- React.createElement('div', { className: 'At1oFq_head' },
333
- React.createElement('label', { className: 'At1oFq_label', htmlFor: 'auto-open-web-browser-path' }, '浏览器可执行文件')
334
- ),
335
- React.createElement('div', { className: 'aow-row' },
336
- React.createElement(ProxyInput, {
337
- id: 'auto-open-web-browser-path',
338
- className: 'aow-grow',
339
- value: draft.browserPath,
340
- placeholder: 'C:\\Program Files (x86)\\Microsoft\\Edge\\Application\\msedge.exe',
341
- disabled: !writable || draft.windowKind !== 'browser',
342
- onChange: (event) => {
343
- const next = applySetPath(draft, ['browserPath'], event.target.value)
344
- setDraft(next)
345
- }
346
- }),
347
- React.createElement(ProxyButton, { variant: 'outline', size: 'sm', disabled: !writable || picking || draft.windowKind !== 'browser', onClick: browse }, picking ? '选择中…' : '浏览'),
348
- React.createElement(ProxyButton, { variant: 'outline', size: 'sm', disabled: testing || draft.windowKind !== 'browser', onClick: testBrowser }, testing ? '测试中…' : '测试')
349
- ),
350
- testResult !== null
351
- ? React.createElement('p', { className: testResult.ok ? 'aow-test-ok' : 'aow-test-fail', role: 'status' },
352
- (testResult.ok ? '✓ ' : '✗ ') + testResult.text
353
- )
354
- : null,
355
- React.createElement('p', { className: 'At1oFq_hint' }, '仅"浏览器应用窗口"模式可编辑;留空使用内置候选(Edge → Chrome);路径不存在会被跳过并告警。「测试」会真实拉起一个专用测试实例验证可用性(数秒后自动关闭)')
356
- ),
357
- // ---- 窗口关闭时退出 DSH(实验性,默认关闭) ----
358
- React.createElement('div', { className: 'At1oFq_field' },
359
- React.createElement('div', { className: 'At1oFq_head' },
360
- React.createElement('span', { className: 'At1oFq_label' }, '窗口关闭时退出 DSH'),
361
- React.createElement('span', { className: 'At1oFq_badges' },
362
- React.createElement('span', { className: 'At1oFq_badge' }, '实验性')
363
- )
364
- ),
365
- React.createElement(Checkbox, { checked: draft.exitOnWindowClose === true, disabled: !writable, onChange: (v) => setDraft(applySetPath(draft, ['exitOnWindowClose'], v)) },
366
- '关闭自动打开的窗口时,DSH 随之退出(默认关闭)'
367
- )
368
- ),
369
- // ---- 卡片底部(与内置 PluginCard 同款按钮) ----
370
- React.createElement('div', { className: 'YyYd_a_footer' },
371
- failedText !== '' ? React.createElement('p', { className: 'YyYd_a_failed', role: 'status' }, failedText) : null,
372
- React.createElement('button', { type: 'button', className: 'YyYd_a_discard', disabled: !dirty || busy, onClick: discard }, '放弃修改'),
373
- React.createElement('button', { type: 'button', className: 'YyYd_a_save', disabled: blocked, onClick: save }, busy ? '保存中…' : '保存')
374
- )
375
- ))
376
- ) : null
433
+ // ---- 窗口类型 ----
434
+ React.createElement('div', { className: 'At1oFq_field' },
435
+ React.createElement('div', { className: 'At1oFq_head' },
436
+ React.createElement('span', { className: 'At1oFq_label' }, t('windowKind')),
437
+ React.createElement(FieldHeadBadges, { overridden: state.windowKind.overridden, disabled: !state.writable, overriddenLabel: t('overridden'), resetLabel: t('reset'), onReset: () => props.resetField('windowKind') })
438
+ ),
439
+ React.createElement(Radio, { checked: state.windowKind.text === 'webview2', disabled: !state.writable, value: 'webview2', onChange: (v) => props.edit('windowKind', v) }, t('windowKindWebview2')),
440
+ React.createElement(Radio, { checked: state.windowKind.text === 'browser', disabled: !state.writable, value: 'browser', onChange: (v) => props.edit('windowKind', v) }, t('windowKindBrowser'))
441
+ ),
442
+ // ---- 浏览器可执行文件(ValueField + 浏览/测试按钮) ----
443
+ React.createElement(ValueField, {
444
+ id: 'auto-open-web-browser-path',
445
+ label: t('browserPath'),
446
+ hint: t('browserPathHint'),
447
+ overridden: state.browserPath.overridden,
448
+ overriddenLabel: t('overridden'),
449
+ resetLabel: t('reset'),
450
+ invalid: state.browserPath.invalid,
451
+ invalidLabel: t('browserPathInvalid'),
452
+ disabled: browserDisabled,
453
+ text: state.browserPath.text,
454
+ placeholder: t('browserPathPlaceholder'),
455
+ onEdit: (text) => props.edit('browserPath', text),
456
+ onReset: () => props.resetField('browserPath'),
457
+ extra: React.createElement(React.Fragment, null,
458
+ React.createElement(ProxyButton, { variant: 'outline', size: 'sm', disabled: !state.writable || picking, onClick: browse }, picking ? t('browsePicking') : t('browse')),
459
+ React.createElement(ProxyButton, { variant: 'outline', size: 'sm', disabled: testing || state.windowKind.text !== 'browser', onClick: testBrowser }, testing ? t('testRunning') : t('test'))
460
+ ),
461
+ footer: testResult !== null
462
+ ? React.createElement('p', { className: testResult.ok ? 'aow-test-ok' : 'aow-test-fail', role: 'status' },
463
+ (testResult.ok ? '✓ ' : '✗ ') + testResult.text
464
+ )
465
+ : null
466
+ }),
467
+ // ---- 窗口关闭时退出 DSH(实验性) ----
468
+ React.createElement('div', { className: 'At1oFq_field' },
469
+ React.createElement('div', { className: 'At1oFq_head' },
470
+ React.createElement('span', { className: 'At1oFq_label' }, t('exitOnWindowClose')),
471
+ React.createElement('span', { className: 'At1oFq_badges' },
472
+ React.createElement('span', { className: 'At1oFq_badge' }, t('experimental'))
473
+ ),
474
+ React.createElement(FieldHeadBadges, { overridden: state.exitOnWindowClose.overridden, disabled: !state.writable, overriddenLabel: t('overridden'), resetLabel: t('reset'), onReset: () => props.resetField('exitOnWindowClose') })
475
+ ),
476
+ React.createElement(Checkbox, { checked: state.exitOnWindowClose.text === true, disabled: !state.writable, onChange: (v) => props.edit('exitOnWindowClose', v) }, t('exitOnWindowCloseHint'))
477
+ )
377
478
  )
378
479
  }
379
480
 
481
+ // ---- 文案(官方 locale 机制,zh/en 字典) ----
482
+ const NS = 'auto-open-web'
483
+ const en = {
484
+ title: 'Auto-open web',
485
+ description: 'Open the DSH Web GUI in an app-style window on profile start; the browser path can be configured manually.',
486
+ save: 'Save', saving: 'Saving…', discard: 'Discard', unsaved: 'Unsaved',
487
+ readOnly: 'This deployment stores settings read-only.',
488
+ saveFailed: 'The deployment did not accept these values; they were left for you to correct.',
489
+ expand: 'Show settings', collapse: 'Hide settings',
490
+ overridden: 'Overridden', reset: 'Reset to default',
491
+ appWindow: 'Independent app window',
492
+ appWindowHint: 'Automatically open an independent app window on start; off opens nothing.',
493
+ windowKind: 'Window kind',
494
+ windowKindWebview2: 'WebView2 host (own process, DSH taskbar icon, exits with DSH; needs Windows + WebView2 runtime)',
495
+ windowKindBrowser: 'Browser app window (dedicated --app instance, isolated from the normal browser)',
496
+ browserPath: 'Browser executable',
497
+ browserPathHint: 'Editable only in "Browser app window" mode; leave blank for built-in candidates (Edge → Chrome); a missing path is skipped with a warning. "Test" launches a dedicated test instance (auto-closes after a few seconds).',
498
+ browserPathPlaceholder: 'C:\\Program Files (x86)\\Microsoft\\Edge\\Application\\msedge.exe',
499
+ browserPathInvalid: 'Invalid path',
500
+ browse: 'Browse', browsePicking: 'Choosing…', test: 'Test', testRunning: 'Testing…',
501
+ testOk: 'Browser launched successfully', testFail: 'Test failed',
502
+ exitOnWindowClose: 'Exit DSH on window close',
503
+ exitOnWindowCloseHint: 'Exit DSH when the auto-opened window closes (off by default)',
504
+ experimental: 'Experimental',
505
+ }
506
+ const zh = {
507
+ title: '自动打开网页',
508
+ description: 'DSH 启动后自动打开独立应用窗口或网页;可手动指定浏览器位置',
509
+ save: '保存', saving: '保存中…', discard: '放弃修改', unsaved: '未保存',
510
+ readOnly: '本部署的设置为只读。',
511
+ saveFailed: '本部署没有接受这些值,已保留供你修改。',
512
+ expand: '展开设置', collapse: '收起设置',
513
+ overridden: '已覆盖', reset: '恢复默认',
514
+ appWindow: '独立应用窗口',
515
+ appWindowHint: '启动时自动打开独立应用窗口;关闭后不自动打开任何窗口',
516
+ windowKind: '窗口类型',
517
+ windowKindWebview2: 'WebView2 宿主(独立进程,任务栏 DSH 图标,随 DSH 退出;需 Windows + WebView2 运行时)',
518
+ windowKindBrowser: '浏览器应用窗口(--app 专用实例,不与其他浏览器页面共用进程)',
519
+ browserPath: '浏览器可执行文件',
520
+ browserPathHint: '仅"浏览器应用窗口"模式可编辑;留空使用内置候选(Edge → Chrome);路径不存在会被跳过并告警。「测试」会真实拉起一个专用测试实例验证可用性(数秒后自动关闭)',
521
+ browserPathPlaceholder: 'C:\\Program Files (x86)\\Microsoft\\Edge\\Application\\msedge.exe',
522
+ browserPathInvalid: '路径无效',
523
+ browse: '浏览', browsePicking: '选择中…', test: '测试', testRunning: '测试中…',
524
+ testOk: '浏览器拉起成功', testFail: '测试失败',
525
+ exitOnWindowClose: '窗口关闭时退出 DSH',
526
+ exitOnWindowCloseHint: '关闭自动打开的窗口时,DSH 随之退出(默认关闭)',
527
+ experimental: '实验性',
528
+ }
529
+ const ja = {
530
+ title: 'ウェブを自動で開く',
531
+ description: 'DSH 起動時に独立アプリウィンドウまたはウェブページを自動で開きます。ブラウザの場所は手動で指定できます',
532
+ save: '保存', saving: '保存中…', discard: '変更を破棄', unsaved: '未保存',
533
+ readOnly: 'このデプロイメントの設定は読み取り専用です。',
534
+ saveFailed: 'デプロイメントがこの値を受け入れませんでした。修正用に保持されています。',
535
+ expand: '設定を表示', collapse: '設定を隠す',
536
+ overridden: '上書き済み', reset: 'デフォルトに戻す',
537
+ appWindow: '独立アプリウィンドウ',
538
+ appWindowHint: '起動時に独立アプリウィンドウを自動的に開く;オフの場合は何も開かない',
539
+ windowKind: 'ウィンドウの種類',
540
+ windowKindWebview2: 'WebView2 ホスト(独立プロセス、タスクバーは DSH アイコン、DSH とともに終了;Windows + WebView2 ランタイムが必要)',
541
+ windowKindBrowser: 'ブラウザアプリウィンドウ(専用 --app インスタンス、通常のブラウザとプロセスを共有しない)',
542
+ browserPath: 'ブラウザ実行ファイル',
543
+ browserPathHint: '「ブラウザアプリウィンドウ」モードのみ編集可能;空欄で組み込み候補(Edge → Chrome)を使用;存在しないパスは警告とともにスキップ。「テスト」は専用テストインスタンスを起動します(数秒後に自動終了)',
544
+ browserPathPlaceholder: 'C:\\Program Files (x86)\\Microsoft\\Edge\\Application\\msedge.exe',
545
+ browserPathInvalid: 'パスが無効です',
546
+ browse: '参照', browsePicking: '選択中…', test: 'テスト', testRunning: 'テスト中…',
547
+ testOk: 'ブラウザの起動に成功しました', testFail: 'テスト失敗',
548
+ exitOnWindowClose: 'ウィンドウを閉じたら DSH を終了',
549
+ exitOnWindowCloseHint: '自動で開いたウィンドウを閉じたときに DSH を終了します(デフォルト:オフ)',
550
+ experimental: '実験的',
551
+ }
552
+ const ko = {
553
+ title: '웹 자동 열기',
554
+ description: 'DSH 시작 시 독립 앱 창 또는 웹 페이지를 자동으로 엽니다. 브라우저 경로는 수동으로 지정할 수 있습니다',
555
+ save: '저장', saving: '저장 중…', discard: '변경 취소', unsaved: '저장 안 됨',
556
+ readOnly: '이 배포의 설정은 읽기 전용입니다.',
557
+ saveFailed: '배포가 이 값을 수락하지 않았습니다. 수정할 수 있도록 유지됩니다.',
558
+ expand: '설정 표시', collapse: '설정 숨기기',
559
+ overridden: '재정의됨', reset: '기본값으로 재설정',
560
+ appWindow: '독립 앱 창',
561
+ appWindowHint: '시작 시 독립 앱 창을 자동으로 엽니다. 끄면 아무 창도 열지 않습니다',
562
+ windowKind: '창 종류',
563
+ windowKindWebview2: 'WebView2 호스트(독립 프로세스, 작업 표시줄 DSH 아이콘, DSH와 함께 종료; Windows + WebView2 런타임 필요)',
564
+ windowKindBrowser: '브라우저 앱 창(전용 --app 인스턴스, 일반 브라우저와 프로세스 공유 안 함)',
565
+ browserPath: '브라우저 실행 파일',
566
+ browserPathHint: "'브라우저 앱 창' 모드에서만 편집 가능; 비워 두면 내장 후보(Edge → Chrome) 사용; 없는 경로는 건너뛰고 경고. '테스트'는 전용 테스트 인스턴스를 실행합니다(몇 초 후 자동 종료)",
567
+ browserPathPlaceholder: 'C:\\Program Files (x86)\\Microsoft\\Edge\\Application\\msedge.exe',
568
+ browserPathInvalid: '잘못된 경로',
569
+ browse: '찾아보기', browsePicking: '선택 중…', test: '테스트', testRunning: '테스트 중…',
570
+ testOk: '브라우저 시작 성공', testFail: '테스트 실패',
571
+ exitOnWindowClose: '창을 닫으면 DSH 종료',
572
+ exitOnWindowCloseHint: '자동으로 열린 창을 닫을 때 DSH를 종료합니다(기본값: 꺼짐)',
573
+ experimental: '실험적',
574
+ }
575
+ const fr = {
576
+ title: 'Ouvrir le web automatiquement',
577
+ description: "Ouvre automatiquement la GUI DSH dans une fenêtre de type application au démarrage; le chemin du navigateur peut être configuré manuellement",
578
+ save: 'Enregistrer', saving: 'Enregistrement…', discard: 'Annuler les modifications', unsaved: 'Non enregistré',
579
+ readOnly: 'Les paramètres de ce déploiement sont en lecture seule.',
580
+ saveFailed: "Le déploiement n'a pas accepté ces valeurs; elles ont été conservées pour correction.",
581
+ expand: 'Afficher les paramètres', collapse: 'Masquer les paramètres',
582
+ overridden: 'Remplacé', reset: 'Rétablir la valeur par défaut',
583
+ appWindow: "Fenêtre d'application indépendante",
584
+ appWindowHint: "Ouvre automatiquement une fenêtre d'application indépendante au démarrage; désactivé n'ouvre rien",
585
+ windowKind: 'Type de fenêtre',
586
+ windowKindWebview2: 'Hôte WebView2 (processus propre, icône DSH dans la barre des tâches, se ferme avec DSH; nécessite Windows + runtime WebView2)',
587
+ windowKindBrowser: 'Fenêtre de navigateur (instance --app dédiée, isolée du navigateur normal)',
588
+ browserPath: 'Exécutable du navigateur',
589
+ browserPathHint: 'Modifiable uniquement en mode « Fenêtre de navigateur »; vide = candidats intégrés (Edge → Chrome); chemin manquant ignoré avec avertissement. « Tester » lance une instance de test dédiée (se ferme après quelques secondes)',
590
+ browserPathPlaceholder: 'C:\\Program Files (x86)\\Microsoft\\Edge\\Application\\msedge.exe',
591
+ browserPathInvalid: 'Chemin invalide',
592
+ browse: 'Parcourir', browsePicking: 'Sélection…', test: 'Tester', testRunning: 'Test en cours…',
593
+ testOk: 'Navigateur lancé avec succès', testFail: 'Échec du test',
594
+ exitOnWindowClose: "Quitter DSH à la fermeture de la fenêtre",
595
+ exitOnWindowCloseHint: 'Quitte DSH lorsque la fenêtre ouverte automatiquement est fermée (désactivé par défaut)',
596
+ experimental: 'Expérimental',
597
+ }
598
+ const de = {
599
+ title: 'Web automatisch öffnen',
600
+ description: 'Öffnet die DSH-GUI beim Start in einem app-ähnlichen Fenster; der Browserpfad kann manuell konfiguriert werden',
601
+ save: 'Speichern', saving: 'Speichern…', discard: 'Änderungen verwerfen', unsaved: 'Nicht gespeichert',
602
+ readOnly: 'Die Einstellungen dieser Bereitstellung sind schreibgeschützt.',
603
+ saveFailed: 'Die Bereitstellung hat diese Werte nicht akzeptiert; sie wurden zur Korrektur beibehalten.',
604
+ expand: 'Einstellungen anzeigen', collapse: 'Einstellungen ausblenden',
605
+ overridden: 'Überschrieben', reset: 'Standard wiederherstellen',
606
+ appWindow: 'Eigenständiges App-Fenster',
607
+ appWindowHint: 'Beim Start automatisch ein eigenständiges App-Fenster öffnen; aus = nichts öffnen',
608
+ windowKind: 'Fenstertyp',
609
+ windowKindWebview2: 'WebView2-Host (eigener Prozess, DSH-Symbol in der Taskleiste, wird mit DSH beendet; benötigt Windows + WebView2-Runtime)',
610
+ windowKindBrowser: 'Browser-App-Fenster (dedizierte --app-Instanz, getrennt vom normalen Browser)',
611
+ browserPath: 'Browser ausführbar',
612
+ browserPathHint: 'Nur im Modus „Browser-App-Fenster" bearbeitbar; leer = eingebaute Kandidaten (Edge → Chrome); fehlender Pfad wird mit Warnung übersprungen. „Test" startet eine dedizierte Testinstanz (schließt sich nach einigen Sekunden)',
613
+ browserPathPlaceholder: 'C:\\Program Files (x86)\\Microsoft\\Edge\\Application\\msedge.exe',
614
+ browserPathInvalid: 'Ungültiger Pfad',
615
+ browse: 'Durchsuchen', browsePicking: 'Auswahl…', test: 'Testen', testRunning: 'Test läuft…',
616
+ testOk: 'Browser erfolgreich gestartet', testFail: 'Test fehlgeschlagen',
617
+ exitOnWindowClose: 'DSH beim Schließen des Fensters beenden',
618
+ exitOnWindowCloseHint: 'DSH beenden, wenn das automatisch geöffnete Fenster geschlossen wird (standardmäßig aus)',
619
+ experimental: 'Experimentell',
620
+ }
621
+ const ru = {
622
+ title: 'Автоматически открывать веб',
623
+ description: 'Открывает GUI DSH в окне приложения при запуске; путь к браузеру можно настроить вручную',
624
+ save: 'Сохранить', saving: 'Сохранение…', discard: 'Отменить изменения', unsaved: 'Не сохранено',
625
+ readOnly: 'Настройки этого развёртывания доступны только для чтения.',
626
+ saveFailed: 'Развёртывание не приняло эти значения; они оставлены для исправления.',
627
+ expand: 'Показать настройки', collapse: 'Скрыть настройки',
628
+ overridden: 'Переопределено', reset: 'Вернуть по умолчанию',
629
+ appWindow: 'Независимое окно приложения',
630
+ appWindowHint: 'Автоматически открывать независимое окно приложения при запуске; выкл. — ничего не открывать',
631
+ windowKind: 'Тип окна',
632
+ windowKindWebview2: 'Хост WebView2 (собственный процесс, значок DSH на панели задач, закрывается вместе с DSH; требуется Windows + среда WebView2)',
633
+ windowKindBrowser: 'Окно браузера (выделенный экземпляр --app, изолирован от обычного браузера)',
634
+ browserPath: 'Исполняемый файл браузера',
635
+ browserPathHint: 'Редактируется только в режиме «Окно браузера»; пусто = встроенные кандидаты (Edge → Chrome); отсутствующий путь пропускается с предупреждением. «Тест» запускает выделенный тестовый экземпляр (автозакрытие через несколько секунд)',
636
+ browserPathPlaceholder: 'C:\\Program Files (x86)\\Microsoft\\Edge\\Application\\msedge.exe',
637
+ browserPathInvalid: 'Недопустимый путь',
638
+ browse: 'Обзор', browsePicking: 'Выбор…', test: 'Тест', testRunning: 'Тестирование…',
639
+ testOk: 'Браузер успешно запущен', testFail: 'Ошибка теста',
640
+ exitOnWindowClose: 'Завершать DSH при закрытии окна',
641
+ exitOnWindowCloseHint: 'Завершать DSH, когда закрывается автоматически открытое окно (по умолчанию выкл.)',
642
+ experimental: 'Экспериментально',
643
+ }
644
+
380
645
  function apply(ctx) {
381
- const slots = ctx.get('slots')
382
- if (slots === undefined) {
383
- console.error('[auto-open-web] slots service unavailable')
384
- return
385
- }
386
- slots.inject('settings.plugin.item', () =>
387
- slots.register(
388
- { name: 'settings.plugin.item', id: 'auto-open-web', order: 40, label: '自动打开网页' },
389
- () => React.createElement(AutoOpenSettingsPage, null)
646
+ // 文案(locale 机制,与官方同构;跟随界面语言)
647
+ ctx.effect(() => ctx.locale.register(NS, { zh, en, ja, ko, fr, de, ru }), 'auto-open-web: card dictionary')
648
+ // 数据通道:settingsScope(官方 settings )
649
+ const scope = ctx.settingsScope.bind({ namespace: 'auto-open-web' })
650
+ const controller = new AutoOpenCardController(scope)
651
+ // 插槽:keyed 注册 + inject(hooks + 表单动作)
652
+ ctx.slots.inject('settings.plugin.item', () =>
653
+ ctx.slots.register(
654
+ { name: 'settings.plugin.item', key: 'auto-open-web', label: '自动打开网页', locale: NS, inject: () => controller.inject() },
655
+ AutoOpenSettingsPage
390
656
  )
391
657
  )
392
658
  }
393
659
 
660
+ // 代码级服务依赖(cordis):与官方 settings-plugins 完全一致
661
+ var clientInject = ['slots', 'locale', 'connection', 'remote', 'settingsScope']
662
+
394
663
  exports.apply = apply
664
+ exports.inject = clientInject
395
665
  exports.name = 'auto-open-web'
396
666
  return module.exports
397
667
  }
package/lib/index.js CHANGED
@@ -15,20 +15,47 @@
15
15
  * 不与正常浏览器页面共用进程);再失败则不打开任何东西(仅记录日志)。
16
16
  *
17
17
  * 配置来源:行配置(cordis.patch.yml)作为启动种子;设置页卡片(客户端 bundle,
18
- * settings.plugin.item 插槽) /auto-open-web/config API 读写,持久化到官方
19
- * settings 命名空间 `auto-open-web`(首次保存后设置值优先于行配置)。
18
+ * settings.plugin.item 插槽)经官方 settings 域(settingsScope)读写本命名
19
+ * 空间,持久化到官方 settings 文档(首次保存后设置值优先于行配置)。
20
+ * 宿主保留的辅助路由:/auto-open-web/pick-browser(原生浏览对话框)、
21
+ * /auto-open-web/test-browser(浏览器拉起测试)。
20
22
  *
21
23
  * 端口来自 webServer 服务的真实监听值(--port 自定义、--port 0 均正确)。
22
24
  * 打开窗口的子进程 detached + unref,不阻塞 DSH、不随 DSH 退出被终止
23
25
  * (WebView2 宿主例外:随 DSH 退出而关闭)。
24
26
  */
25
27
  import { spawn } from 'node:child_process';
26
- import { rmSync, writeFileSync } from 'node:fs';
28
+ import { existsSync, rmSync, writeFileSync } from 'node:fs';
29
+ import { createRequire } from 'node:module';
27
30
  import { join } from 'node:path';
28
- import z from '@deepseek-ai/schemastery';
29
31
  import { dshHome, appProfileDir, testProfileDir, instanceStateFile } from './paths.js';
30
32
  import * as platform from './platform.js';
31
33
 
34
+ /**
35
+ * schemastery 依赖策略(与 win32.js 的 koffi 同一模式):本包零 dependencies,
36
+ * schemastery 由 DSH 部署自带(官方核心依赖),运行时解析:
37
+ * 常规 import(profile 已装/手动安装)优先 → Windows 全局 npm 布局的
38
+ * DSH 部署副本;两者都不可用时模块加载失败(插件依赖 Config,无法降级)。
39
+ */
40
+ let z = null;
41
+ try {
42
+ z = (await import('@deepseek-ai/schemastery')).default;
43
+ } catch {
44
+ try {
45
+ const globalRoot = process.env.APPDATA ? join(process.env.APPDATA, 'npm', 'node_modules') : '';
46
+ const dshEntry = join(globalRoot, '@deepseek-ai', 'dsh', 'package.json');
47
+ if (existsSync(dshEntry)) z = createRequire(dshEntry)('@deepseek-ai/schemastery');
48
+ } catch {
49
+ /* 两次解析均失败 → 下方抛错 */
50
+ }
51
+ }
52
+ if (z === null || z === undefined) {
53
+ throw new Error(
54
+ '[auto-open-web] schemastery not found (neither local nor DSH deployment); ' +
55
+ 'this plugin cannot build its config schema. Check the DSH deployment.',
56
+ );
57
+ }
58
+
32
59
  export const name = 'auto-open-web';
33
60
 
34
61
  /**
@@ -53,7 +80,6 @@ export const Config = z.object({
53
80
  exitOnWindowClose: z.boolean().default(false),
54
81
  });
55
82
 
56
- const CONFIG_API_PATH = '/auto-open-web/config';
57
83
  const PICK_API_PATH = '/auto-open-web/pick-browser';
58
84
  const TEST_API_PATH = '/auto-open-web/test-browser';
59
85
  const ICON_SIZES = [16, 32, 48, 64, 128, 256];
@@ -379,7 +405,9 @@ export function apply(ctx, config) {
379
405
  }
380
406
  syncExitWatcher(); // 启动时的最终生效值(settings 已合并或行配置)
381
407
 
382
- // ---- 设置卡片 API(读写与测试,配置由宿主权威校验) ----
408
+ // ---- 卡片辅助 API(浏览/测试;设置读写走官方 settings 域) ----
409
+ // 设置卡片经客户端 settingsScope(settings 域)读写本命名空间,不再自建
410
+ // config 路由;这里只保留官方通道无法覆盖的能力。
383
411
  function sendJson(res, status, value) {
384
412
  res.writeHead(status, { 'content-type': 'application/json; charset=utf-8' });
385
413
  res.end(JSON.stringify(value));
@@ -390,44 +418,6 @@ export function apply(ctx, config) {
390
418
  return body;
391
419
  }
392
420
  if (typeof server.register === 'function') {
393
- server.register({
394
- kind: 'exact',
395
- path: CONFIG_API_PATH,
396
- handler: async (req, res) => {
397
- if (req.method === 'GET') {
398
- sendJson(res, 200, { ok: true, ...state, writable });
399
- return;
400
- }
401
- if (req.method === 'POST') {
402
- if (!writable) {
403
- sendJson(res, 200, { ok: false, error: '当前配置只读(settings 不可写)' });
404
- return;
405
- }
406
- let parsed;
407
- try {
408
- parsed = JSON.parse(await readBody(req));
409
- } catch {
410
- sendJson(res, 400, { ok: false, error: 'invalid JSON body' });
411
- return;
412
- }
413
- try {
414
- const next = normalizeState(parsed);
415
- if (scope !== null) await scope.update(next);
416
- state = next;
417
- syncExitWatcher(); // 保存后即时生效,当前会话无需重启
418
- sendJson(res, 200, { ok: true, ...state });
419
- } catch (error) {
420
- sendJson(res, 200, {
421
- ok: false,
422
- error: String(error && error.message !== undefined ? error.message : error),
423
- });
424
- }
425
- return;
426
- }
427
- res.writeHead(405);
428
- res.end();
429
- },
430
- });
431
421
  // 原生"浏览"对话框:选择浏览器可执行文件(平台能力;非 Windows 提示手动输入)。
432
422
  server.register({
433
423
  kind: 'exact',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-auto-open-web",
3
- "version": "0.1.3",
3
+ "version": "0.1.5",
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
5
  "type": "module",
6
6
  "main": "lib/index.js",
@@ -22,7 +22,9 @@
22
22
  "inject": [
23
23
  "@deepseek-ai/dsh-client-connection",
24
24
  "@deepseek-ai/dsh-api-remotes",
25
- "@deepseek-ai/dsh-client-ui-settings"
25
+ "@deepseek-ai/dsh-client-ui-settings",
26
+ "@deepseek-ai/dsh-client-runtime",
27
+ "@deepseek-ai/dsh-client-locale"
26
28
  ]
27
29
  }
28
30
  },
@@ -39,8 +41,17 @@
39
41
  "README.md",
40
42
  "README.en.md"
41
43
  ],
42
- "dependencies": {
44
+ "peerDependencies": {
45
+ "@deepseek-ai/dsh": ">=0.1.0-rc.7 <0.2.0",
43
46
  "@deepseek-ai/schemastery": "^3.18.1"
44
47
  },
48
+ "peerDependenciesMeta": {
49
+ "@deepseek-ai/dsh": {
50
+ "optional": true
51
+ },
52
+ "@deepseek-ai/schemastery": {
53
+ "optional": true
54
+ }
55
+ },
45
56
  "license": "MIT"
46
57
  }