dsh-auto-open-web 0.1.20 → 0.1.27

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
@@ -1,8 +1,9 @@
1
1
  # dsh-auto-open-web
2
2
 
3
3
  A persistent plugin for the `dsh web` profile that automatically opens the DSH Web GUI in an app-style
4
- window (or browser tab) on profile start, with a configuration card under Settings → Plugin
5
- configuration (manually maintained browser path, etc.).
4
+ window (or browser tab) on profile start, with a configuration form (manually maintained
5
+ browser path, etc.). On dsh ≥ 0.1.5 that entry point is the bundle's detail page on the
6
+ **Plugins page**; on rc.7–0.1.4 it is the **Settings → Plugin configuration** card.
6
7
 
7
8
  ## Behavior
8
9
 
@@ -83,14 +84,33 @@ cleanup.
83
84
 
84
85
  Two equivalent ways:
85
86
 
86
- 1. **Settings card** (recommended): Settings Plugin configuration the "自动打开网页"
87
- (auto-open web) card. Editable fields:
87
+ 1. **Settings form** (recommended) two supported version lines; the plugin registers for
88
+ both and whichever exists wins:
89
+ - **Newest version (dsh ≥ 0.1.6-alpha, incl. 0.1.7-alpha.1)**: **Plugins page →
90
+ `dsh-auto-open-web` → this plugin's row → Configure**. The form opens per **row**
91
+ (slot `plugins.row.config`, key `<package name>#<row id>` =
92
+ `dsh-auto-open-web#auto-open-web`); the page supplies the data
93
+ (`form = { state, mutate }` — the host-projected row Config snapshot plus atomic
94
+ writes) and the plugin only draws the controls. Only **Save** is offered — leaving
95
+ the page drops staged edits.
96
+ - **Latest rc line (0.1.5-rc.x)**: Settings → Plugin configuration → the "自动打开网页"
97
+ collapsible card (slot `settings.plugin.item`, keyed by settings namespace). Data goes
98
+ through the host's `settings.register` plus the client `settingsScope`.
99
+
100
+ The two lines' client services **do not coexist** (new `configForms` ↔ rc-line
101
+ `settingsScope`), so the plugin reads both **lazily** and declares only the shared
102
+ services in `inject` (`slots`/`locale`/`connection`/`remote`) — a hard inject would leave
103
+ the plugin pending forever on the other line and the settings UI would never appear.
104
+ Earlier versions (0.1.0-rc.*, 0.1.1-rc.*, 0.1.2-*, 0.1.3-alpha, 0.1.5-alpha,
105
+ 0.1.6-alpha.1) are **no longer guaranteed**.
106
+
107
+ Both entry points edit
88
108
  `appWindow` (independent app window), `windowKind` (WebView2 host / browser app window),
89
109
  `browserPath` (browser executable, with a native "Browse" file dialog; located below the
90
110
  window-type field and enabled only when "Browser app window" is selected),
91
111
  `exitOnWindowClose` (exit DSH when the window closes, off by default).
92
- After saving, values persist through the **official settings domain** (client `settingsScope`)
93
- to the settings document (namespace `auto-open-web`); once saved,
112
+ After saving, values take precedence over row configuration (on the newest version they land
113
+ in that row's user layer; on the rc line in the `auto-open-web` settings namespace); once saved,
94
114
  settings take precedence over row configuration. The host keeps only the "Browse" / "Test"
95
115
  helper routes (capabilities the official channel cannot cover).
96
116
  2. **Row configuration** (`cordis.patch.yml`): acts as the startup seed, effective until the
@@ -166,8 +186,12 @@ dsh plugin --profile web remove dsh-auto-open-web # removes the dependency and
166
186
  After installation: pnpm adds the package to `profiles/web/node_modules`, and `dsh` appends
167
187
  `dsh-auto-open-web` to `dsh.profile.bundles`; at startup the bundle's `cordis.patch.yml` inserts the
168
188
  plugin row (`name: auto-open-web`, resolved by package name). After restarting `dsh web`, the
169
- "自动打开网页" card appears in the settings page (the client bundle is scanned into the browser
170
- manifest at startup via the modules line, per the `dsh.client` declaration).
189
+ configuration form shows up at the matching entry point (dsh 0.1.5: the `dsh-auto-open-web`
190
+ bundle's detail page on the Plugins page; earlier versions: the Settings → Plugin configuration
191
+ card). The client bundle is scanned into the browser manifest at startup via the modules line,
192
+ per the `dsh.client` declaration; on the client the plugin registers into **both**
193
+ `plugins.bundle.config` (new) and `settings.plugin.item` (old), and the slot machinery decides
194
+ which one takes effect (a slot that does not exist stays pending — no error, no interference).
171
195
 
172
196
  The effective configuration is composed layer by layer in this order (later layers win per row,
173
197
  replacing the whole row's config rather than deep-merging): each bundle's patch (in bundles list
@@ -179,10 +203,14 @@ without touching the package.
179
203
 
180
204
  - **Zero dependencies**: every runtime dependency is provided by the **DSH deployment** and
181
205
  declared as an optional peer (no install warnings):
182
- - `@deepseek-ai/dsh` (the host; declares the compatibility range `>=0.1.0-rc.8 <0.2.0`
183
- rc.8 removed `dsh-client-schema-form` from the client frozen table and introduced
184
- `dsh-client-runtime`; the plugin's settings card and opening behavior depend on that
185
- release; no runtime version check is performed)
206
+ - `@deepseek-ai/dsh` (the host; declares the compatibility range **`>=0.1.5-rc.1 <0.2.0`**
207
+ covering the **latest rc line (0.1.5-rc.x)** and the **newest version (≥0.1.6-alpha,
208
+ incl. 0.1.7-alpha.1)**; older 0.1.0-rc.*/0.1.1-rc.*/0.1.2-*/0.1.3-alpha/0.1.5-alpha
209
+ releases are no longer guaranteed. The snapshot store uses
210
+ `@deepseek-ai/dsh-client-store` only — the rc.8-era `dsh-client-runtime` fallback was
211
+ removed; the client registers the newest version's row slot (`plugins.row.config`) and
212
+ the rc line's settings slot (`settings.plugin.item`); no runtime version check is
213
+ performed)
186
214
  - `@deepseek-ai/schemastery` (config schema validator; resolved at runtime: normal import
187
215
  first, then the DSH deployment copy under the Windows global npm layout)
188
216
  - `koffi` (Windows only: Job Object / process verification / native file dialog; same runtime
package/README.md CHANGED
@@ -3,7 +3,9 @@
3
3
  > English: [README.en.md](README.en.md)
4
4
 
5
5
  dsh web profile 启动后自动打开独立应用窗口(或网页标签页)的常驻插件,并在
6
- 设置 → 插件配置中提供配置卡片(手动维护浏览器位置等)。
6
+ 设置界面提供配置表单(手动维护浏览器位置等)。**最新版**的配置入口是
7
+ 「插件管理」页里本插件**那一行**的配置页;**最新 rc 线(0.1.5-rc.x)**为
8
+ **设置 → 插件配置**卡片。
7
9
 
8
10
  ## 行为
9
11
 
@@ -78,15 +80,31 @@ Job Object(强杀也生效)+ 退出清理结束进程树。
78
80
 
79
81
  两种途径,等价:
80
82
 
81
- 1. **设置页卡片**(推荐):设置 → 插件配置 → 「自动打开网页」卡片。可编辑
82
- `appWindow`(独立应用窗口)、`windowKind`(WebView2 宿主 / 浏览器应用窗口)、
83
- `browserPath`(浏览器可执行文件,支持「浏览」原生对话框选择;位于窗口类型
84
- 下方,仅选择「浏览器应用窗口」时使能)、
83
+ 1. **设置表单**(推荐)——两条受支持的版本线,插件**同时**注册、哪条存在就哪条生效:
84
+ - **最新版(dsh 0.1.6-alpha,含 0.1.7-alpha.1)**:**「插件管理」页 →
85
+ `dsh-auto-open-web`(bundle 详情页)**,表单直接显示在该页说明与组件行之间
86
+ (插槽 `plugins.bundle.config`,key = **bundle 包名**)—— 与 `dsh-harness-tags`
87
+ 同款席位。页面只传 `{ view: 'page' }`,**数据由插件自绑**:命名空间 = 本条目 id,
88
+ 经客户端服务 `configForms` 取 ConfigForm(快照 + `set`/`unset`/订阅)。
89
+ 此形态只有「保存」,**离开页面即丢弃暂存**。
90
+ - **最新 rc 线(0.1.5-rc.x)**:设置 → 插件配置 → 「自动打开网页」折叠卡片
91
+ (插槽 `settings.plugin.item`,按设置命名空间 keyed)。数据走宿主
92
+ `settings.register` + 客户端 `settingsScope`。
93
+
94
+ 两条线的**客户端服务互不存在**(新版的 `configForms` ↔ rc 线的
95
+ `settingsScope`),因此插件把这两个服务都改为**惰性读取**、`inject` 只声明
96
+ 两线共有的服务(`slots`/`locale`/`connection`/`remote`)——否则硬注入会让插件
97
+ 在另一条线上永远 pending、设置界面完全不出现。更早的版本(0.1.0-rc.*、
98
+ 0.1.1-rc.*、0.1.2-*/0.1.3-alpha/0.1.5-alpha/0.1.6-alpha.1)**不再保证**。
99
+
100
+ 两种入口都可编辑 `appWindow`(独立应用窗口)、`windowKind`(WebView2 宿主 /
101
+ 浏览器应用窗口)、`browserPath`(浏览器可执行文件,支持「浏览」原生对话框
102
+ 选择;位于窗口类型下方,仅选择「浏览器应用窗口」时使能)、
85
103
  `exitOnWindowClose`(窗口关闭时退出 DSH,默认关闭)。
86
- 保存后经**官方 settings 域**(客户端 settingsScope)持久化到
87
- settings 文档(命名空间 `auto-open-web`),首次保存后设置值优先于行配置;
88
- 宿主仅保留「浏览」「测试」辅助路由(官方通道无法覆盖的能力)。
89
- 2. **行配置**(cordis.patch.yml):作为启动种子,设置卡片保存前生效。
104
+ 保存后设置值优先于行配置(新版落在该行的用户层覆盖上,rc 线落在
105
+ `auto-open-web` 设置命名空间);宿主仅保留「浏览」「测试」辅助路由
106
+ (官方通道无法覆盖的能力)。
107
+ 2. **行配置**(cordis.patch.yml):作为启动种子,设置表单保存前生效。
90
108
 
91
109
  | 字段 | 默认 | 说明 |
92
110
  | --- | --- | --- |
@@ -157,8 +175,12 @@ dsh plugin --profile web remove dsh-auto-open-web # 同时移除依赖与对
157
175
  安装后:pnpm 将包加入 `profiles/web/node_modules`,`dsh` 把
158
176
  `dsh-auto-open-web` 追加到 `dsh.profile.bundles`;启动时 bundle 的
159
177
  `cordis.patch.yml` 插入插件行(`name: auto-open-web`,按包名解析)。
160
- 重启 `dsh web` 后设置页出现「自动打开网页」卡片(客户端 bundle 由 modules
161
- 行按 `dsh.client` 声明在启动时扫描进浏览器清单)。
178
+ 重启 `dsh web` 后,配置表单出现在对应入口(最新版:「插件管理」页里
179
+ `dsh-auto-open-web` 那一行的配置页;最新 rc 线:设置 → 插件配置卡片)。
180
+ 客户端 bundle 由 modules 行按 `dsh.client` 声明在启动时扫描进浏览器清单;
181
+ 插件在客户端**同时**向 `plugins.bundle.config`(新)与 `settings.plugin.item`
182
+ (旧)两个插槽注册,由客户端插槽机制决定哪个生效(不存在者保持 pending,
183
+ 不报错、不影响另一者)。
162
184
 
163
185
  生效配置按以下顺序逐层组合(后应用的层按行胜出,整行替换 config 而非深合并):
164
186
  每个 bundle 的 patch(按 bundles 列表顺序)→ profile 自己的
@@ -169,10 +191,11 @@ dsh plugin --profile web remove dsh-auto-open-web # 同时移除依赖与对
169
191
 
170
192
  - **本包零 dependencies**:所有运行时依赖均由 **DSH 部署提供**,以 optional
171
193
  peer 声明(安装无警告):
172
- - `@deepseek-ai/dsh`(宿主,声明兼容范围 `>=0.1.0-rc.8 <0.2.0`:
173
- rc.8 起客户端冻结表移除 `dsh-client-schema-form`、引入
174
- `dsh-client-runtime`,本插件的设置卡片与打开行为均依赖该版本;
175
- 不做运行时版本检测)
194
+ - `@deepseek-ai/dsh`(宿主,声明兼容范围 **`>=0.1.5-rc.1 <0.2.0`**:
195
+ 覆盖**最新 rc 线(0.1.5-rc.x)**与**最新版(≥0.1.6-alpha,含 0.1.7-alpha.1)**;
196
+ 更早的 0.1.0-rc.*/0.1.1-rc.*/0.1.2-*/0.1.3-alpha/0.1.5-alpha 不再保证。
197
+ 快照 store 统一走 `@deepseek-ai/dsh-client-store`(自 0.1.2-alpha.2 起,
198
+ rc.8 时代的 `dsh-client-runtime` 回退已移除);不做运行时版本检测)
176
199
  - `@deepseek-ai/schemastery`(配置 schema 校验器;运行时解析:常规 import
177
200
  优先,其次 Windows 全局 npm 布局下的 DSH 部署副本)
178
201
  - `koffi`(仅 Windows 的 Job Object / 进程校验 / 原生对话框;同样运行时
@@ -184,8 +207,27 @@ dsh plugin --profile web remove dsh-auto-open-web # 同时移除依赖与对
184
207
  (不 require `dsh-client-ui-primitives`,不复刻官方样式表),卡片外壳、
185
208
  字段、按钮、输入框、图标全部手写;观感经官方设计令牌变量
186
209
  (`--dsw-alias-*` / `--dsw-static-*`)对齐,浅/深色自动跟随主题。
187
- 仅使用官方公开的数据/机制接口:`settingsScope`(settings 域)、`slots`
188
- (插槽)、`locale`(文案)、`dsh-client-runtime`(快照 store)
210
+ **维护提示(0.1.26,新版设置投影)**:dsh 0.1.7 起宿主只把**标记为
211
+ `volatile` 的 Config 字段**投影成设置表单(`dsh-settings` 的 `volatileForm()`
212
+ 对非 volatile 字段直接返回 undefined → `describe()` 里没有本条目 → 页面显示
213
+ "本部署未提供该设置命名空间")。因此 `lib/index.js` 的 `Config` 四个字段都必须
214
+ 链上 schemastery 的 **`.volatile()`**(官方 `dsh-agent-loop` 同款写法);
215
+ **不要**写成 `.set('volatile', true)` —— `Schema.prototype.set` 是写 dict 子字段,
216
+ 会把 schema 弄坏、条目 import 失败。
217
+ 0.1.7 把设置命名空间改为"**每 profile 条目一个**"(= 本条目 id),与 bundle
218
+ 席位配套:页面只给 `view`,插件自己经惰性读取的 `configForms` 绑定该命名空间
219
+ (rc 线同理,用惰性读取的 `settingsScope`)。
220
+ **维护提示(0.1.22)**:主按钮(保存)必须用**随主题反转**的令牌对
221
+ `background:var(--dsw-alias-label-primary)` +
222
+ `color:var(--dsw-alias-bg-layer-3)`(与官方 `PluginConfigForm` 的 save 同款)。
223
+ 深色模式下 `--dsw-alias-brand-primary` 是**近白色**,若沿用不存在的
224
+ `--dsw-alias-label-inverse`(回退 #fff)会渲染成白字白底、按钮不可见;
225
+ 同理不要引用并不存在的 `--dsw-alias-label-error`(真名是
226
+ `--dsw-alias-state-error-primary`)。这两条已由 `smoke-client-slots.mjs`
227
+ 断言守护。
228
+ 仅使用官方公开的数据/机制接口:`slots`(插槽)、`locale`(文案)、
229
+ 行 Config 表单(新版,页面下发 `form`)、rc 线的 `settingsScope`
230
+ (惰性读取)、快照 store。
189
231
  - **npm 包已含 WebView2 宿主编译产物**(prepack 编译后发布);**GitHub
190
232
  main 分支与源码 checkout 方式不含** `host-publish/`(构建产物被
191
233
  .gitignore 忽略):webview2 模式需先在 `node_modules/dsh-auto-open-web`
Binary file
Binary file
package/lib/client.js CHANGED
@@ -1,14 +1,27 @@
1
- // dsh-auto-open-web — Client bundle(设置卡片;UI 完全自绘,不依赖官方组件/CSS 模板)
1
+ // dsh-auto-open-web — Client bundle(设置表单;UI 完全自绘,不依赖官方组件/CSS 模板)
2
2
  // 格式:window.__ModuleLoader__.load({ id, factory });factory(require) 的
3
- // require 解析浏览器端模块表(react 为壳层静态模块,@deepseek-ai/dsh-client-runtime
4
- // 为动态包)。**不** require @deepseek-ai/dsh-client-ui-primitives 等官方 UI 包。
3
+ // require 解析浏览器端冻结模块表(react、@deepseek-ai/dsh-client-store 等)。
4
+ // **不** require @deepseek-ai/dsh-client-ui-primitives 等官方 UI 包。
5
5
  //
6
- // 依赖边界(与官方数据的对接面,均为公开接口;本插件声明 dsh ≥0.1.0-rc.8):
7
- // - settingsScope:官方 settings 域(命名空间 `auto-open-web`;第三方命名空间
8
- // 白名单自 rc.7 起已取消,rc.8 下无限制)
9
- // - slots:settings.plugin.item 插槽(keyed 注册,rc.7 起要求 options.key)
6
+ // 依赖边界(与官方数据的对接面,均为公开接口):
7
+ // 支持两条版本线(0.1.26 起;更早的 alpha/rc 不再保证):
8
+ // · 最新版(dsh ≥0.1.6-alpha,含 0.1.7-alpha.1):插件设置位于「插件管理」页,
9
+ // 配置注册在 **bundle 详情页** —— `plugins.bundle.config`,key = bundle
10
+ // 包名(`dsh-auto-open-web`),与 dsh-harness-tags 同款席位。页面只传
11
+ // `{ view:'page' }`,没有 form,故组件自己经客户端服务 `configForms` 绑命名
12
+ // 空间(条目 id `auto-open-web`);宿主只在 Config 字段标了 `.volatile()`
13
+ // 时才投影该命名空间。
14
+ // · 最新 rc 线(0.1.5-rc.x):设置页插件配置卡片 —— 槽 `settings.plugin.item`
15
+ // (按设置命名空间 keyed),数据走宿主 `settings.register` + 客户端
16
+ // `settingsScope`。
17
+ // 两条线的客户端服务互不存在(settingsScope ↔ configForms),因此:
18
+ // - `exports.inject` 只声明两线共有的服务(slots/locale/connection/remote);
19
+ // - settingsScope / configForms 都用 `ctx.get` **惰性读取**;
20
+ // - 两个槽都用 slots.inject 挂载,不存在的插槽只保持 pending(不报错)。
10
21
  // - locale:文案注册(zh/en/ja/ko/fr/de/ru,跟随界面语言)
11
- // - dsh-client-runtime:createSnapshotStore 快照 store(缺失时内置兜底实现)
22
+ // - store:createSnapshotStore 快照 store(dsh-client-store,缺失时内置最小实现)
23
+ // —— rc.8 时代的 dsh-client-runtime 回退已按兼容策略移除(0.1.2-alpha.2 起
24
+ // 统一为 dsh-client-store)。
12
25
  //
13
26
  // 视觉(全部自产):
14
27
  // - 卡片外壳、字段、按钮、输入框、复选/单选、徽章、图标均为手写实现;
@@ -44,13 +57,17 @@ window.__ModuleLoader__.load({
44
57
  '.aow-cardReadOnly{color:var(--dsw-alias-label-tertiary,var(--dsw-alias-label-secondary));margin:12px 0 0;font-size:12px;line-height:1.5}' +
45
58
  '.aow-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}' +
46
59
  '.aow-cardFooter{border-top:1px solid var(--dsw-alias-border-l2);justify-content:flex-end;align-items:center;gap:8px;padding:12px 0 4px;display:flex}' +
47
- '.aow-cardFailed{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}' +
60
+ '.aow-cardFailed{min-width:0;color:var(--dsw-alias-state-error-primary);flex:1;margin:0;font-size:12px;line-height:1.5}' +
48
61
  // 按钮:描边(outline)与主色(save)两种,禁用半透明。
49
62
  '.aow-btn{appearance:none;font:inherit;cursor:pointer;border-radius:8px;padding:5px 14px;font-size:13px;line-height:1.5;border:1px solid transparent}' +
50
63
  '.aow-btn:disabled{opacity:.55;cursor:default}' +
51
64
  '.aow-btnOutline{border-color:var(--dsw-alias-border-l2);color:var(--dsw-alias-label-secondary);background:0 0}' +
52
65
  '.aow-btnOutline:hover:not(:disabled){color:var(--dsw-alias-label-primary)}' +
53
- '.aow-btnPrimary{background:var(--dsw-alias-brand-primary);color:var(--dsw-alias-label-inverse,#fff);border-color:var(--dsw-alias-brand-primary);font-weight:600}' +
66
+ // 主按钮(保存):与官方 PluginConfigForm 的 save 同款配色 ——
67
+ // 底色 label-primary、文字 bg-layer-3,深色模式下这对令牌自动反转
68
+ // (浅色=近黑底白字,深色=近白底深字);不再引用并不存在的 label-inverse
69
+ // (深色下它会退化成 #fff,造成白字白底、保存按钮看不见)。
70
+ '.aow-btnPrimary{background:var(--dsw-alias-label-primary);color:var(--dsw-alias-bg-layer-3,#fff);font-weight:600}' +
54
71
  '.aow-btnPrimary:hover:not(:disabled){filter:brightness(.95)}'
55
72
  // 字段区:label 行 + 控件 + hint;相邻字段以 l2 分隔;徽章/恢复默认同观感。
56
73
  var FIELDS_CSS =
@@ -66,8 +83,8 @@ window.__ModuleLoader__.load({
66
83
  '.aow-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}' +
67
84
  '.aow-input:focus-visible{border-color:var(--dsw-alias-brand-primary);outline:none}' +
68
85
  '.aow-input:disabled{color:var(--dsw-alias-label-tertiary,var(--dsw-alias-label-secondary));cursor:default}' +
69
- '.aow-inputInvalid{border-color:var(--dsw-alias-label-error,var(--dsw-alias-state-error-primary))}' +
70
- '.aow-invalid{color:var(--dsw-alias-label-error,var(--dsw-alias-state-error-primary));margin:0;font-size:12px;line-height:1.5}' +
86
+ '.aow-inputInvalid{border-color:var(--dsw-alias-state-error-primary)}' +
87
+ '.aow-invalid{color:var(--dsw-alias-state-error-primary);margin:0;font-size:12px;line-height:1.5}' +
71
88
  '.aow-hint{color:var(--dsw-alias-label-tertiary,var(--dsw-alias-label-secondary));margin:0;font-size:12px;line-height:1.5}'
72
89
  // 布局与杂项:行排列、复选/单选、测试结果状态。
73
90
  var CONTROLS_CSS =
@@ -76,7 +93,7 @@ window.__ModuleLoader__.load({
76
93
  '.aow-check input{accent-color:var(--dsw-alias-brand-primary);width:15px;height:15px;margin:0;flex:none}' +
77
94
  '.aow-check input:disabled{cursor:default}' +
78
95
  '.aow-test-ok{color:var(--dsw-alias-state-success-primary);margin:4px 0 0;font-size:12px;line-height:1.5}' +
79
- '.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}'
96
+ '.aow-test-fail{color:var(--dsw-alias-state-error-primary);margin:4px 0 0;font-size:12px;line-height:1.5}'
80
97
  var CARD_TAG = 'dsh-auto-open-web/Card.module.css'
81
98
  var FIELDS_TAG = 'dsh-auto-open-web/Fields.module.css'
82
99
  var CONTROLS_TAG = 'dsh-auto-open-web/Controls.module.css'
@@ -94,23 +111,15 @@ window.__ModuleLoader__.load({
94
111
  injectCss(CONTROLS_TAG, CONTROLS_CSS)
95
112
 
96
113
  // ── 共享模块 ─────────────────────────────────────────────────────────
97
- // 快照 store 解析顺序( gowin 插件同款,双版本兼容):
98
- // @deepseek-ai/dsh-client-store(当前壳层静态模块,dsh >= 0.1.2-alpha.1)
99
- // @deepseek-ai/dsh-client-runtime(rc8 动态包) → 内置最小实现。
100
- // 缺失或版本不含 createSnapshotStore 时退化为内置实现,绝不抛错。
114
+ // 快照 store:@deepseek-ai/dsh-client-store(壳层静态模块,自 dsh 0.1.2-alpha.2
115
+ // 起提供;最新 rc 线与最新版都包含它)。rc.8 时代的 dsh-client-runtime 回退
116
+ // 已按兼容策略移除;若该模块缺失(异常部署)退化为内置最小实现,绝不抛错。
101
117
  var createSnapshotStore = null
102
118
  try {
103
119
  createSnapshotStore = require('@deepseek-ai/dsh-client-store').createSnapshotStore
104
120
  } catch (e) {
105
121
  console.error('[auto-open-web] dsh-client-store require failed:', e !== null && e !== undefined && e.message !== undefined ? e.message : String(e))
106
122
  }
107
- if (createSnapshotStore === undefined || createSnapshotStore === null) {
108
- try {
109
- createSnapshotStore = require('@deepseek-ai/dsh-client-runtime').createSnapshotStore
110
- } catch (e) {
111
- console.error('[auto-open-web] dsh-client-runtime require failed:', e !== null && e !== undefined && e.message !== undefined ? e.message : String(e))
112
- }
113
- }
114
123
  if (createSnapshotStore === undefined || createSnapshotStore === null) {
115
124
  createSnapshotStore = fallbackSnapshotStore
116
125
  }
@@ -458,14 +467,90 @@ window.__ModuleLoader__.load({
458
467
  }
459
468
  }
460
469
 
461
- // ── 设置卡片页面(自绘组件拼装) ─────────────────────────────────────
462
- function AutoOpenSettingsPage(props) {
463
- const { t } = props
464
- const state = props.useAutoOpenCard((snapshot) => snapshot)
470
+ // ── 两代插槽共用的字段构件 ───────────────────────────────────────────
471
+ /** 字段规格(两代共用:schema 字段名 → 文本格式化/解析规则) */
472
+ function fieldSpecs() {
473
+ return [
474
+ boolField('appWindow'),
475
+ windowKindField('windowKind'),
476
+ textField('browserPath'),
477
+ boolField('exitOnWindowClose')
478
+ ]
479
+ }
480
+ /** 字段头部"已覆盖 + 恢复默认"徽章(静态徽章可选)。 */
481
+ function badgesFor(t, state, field, resetField, staticLabel) {
482
+ return React.createElement(FieldBadges, {
483
+ static: staticLabel,
484
+ overridden: state[field].overridden,
485
+ disabled: !state.writable,
486
+ overriddenLabel: t('overridden'),
487
+ resetLabel: t('reset'),
488
+ onReset: () => resetField(field)
489
+ })
490
+ }
491
+ /**
492
+ * 四个字段的节点(两代插槽共用同一份 UI 与文案)。
493
+ * @param t - 文案
494
+ * @param state - 每字段 { text, overridden, invalid } + writable
495
+ * @param edit - (field, value) 暂存编辑
496
+ * @param resetField - (field) 恢复默认(清除用户层)
497
+ * @param ui - useBrowserTools 的返回值(浏览/测试按钮与结果)
498
+ */
499
+ function buildFieldNodes(t, state, edit, resetField, ui) {
500
+ return [
501
+ React.createElement(Field, {
502
+ key: 'appWindow',
503
+ label: t('appWindow'),
504
+ badges: badgesFor(t, state, 'appWindow', resetField),
505
+ control: React.createElement(Checkbox, { checked: state.appWindow.text === true, disabled: !state.writable, onChange: (v) => edit('appWindow', v) }, t('appWindowHint'))
506
+ }),
507
+ React.createElement(Field, {
508
+ key: 'windowKind',
509
+ label: t('windowKind'),
510
+ badges: badgesFor(t, state, 'windowKind', resetField),
511
+ control: React.createElement(React.Fragment, null,
512
+ React.createElement(Radio, { checked: state.windowKind.text === 'webview2', disabled: !state.writable, value: 'webview2', onChange: (v) => edit('windowKind', v) }, t('windowKindWebview2')),
513
+ React.createElement(Radio, { checked: state.windowKind.text === 'browser', disabled: !state.writable, value: 'browser', onChange: (v) => edit('windowKind', v) }, t('windowKindBrowser'))
514
+ )
515
+ }),
516
+ React.createElement(Field, {
517
+ key: 'browserPath',
518
+ label: t('browserPath'),
519
+ badges: badgesFor(t, state, 'browserPath', resetField),
520
+ hint: t('browserPathHint'),
521
+ invalid: state.browserPath.invalid,
522
+ invalidLabel: t('browserPathInvalid'),
523
+ control: React.createElement('div', { className: 'aow-row' },
524
+ React.createElement(TextInput, {
525
+ id: 'auto-open-web-browser-path',
526
+ value: state.browserPath.text,
527
+ placeholder: t('browserPathPlaceholder'),
528
+ disabled: ui.browserDisabled,
529
+ invalid: state.browserPath.invalid,
530
+ onChange: (text) => edit('browserPath', text)
531
+ }),
532
+ React.createElement(Btn, { disabled: !state.writable || ui.picking, onClick: ui.browse }, ui.picking ? t('browsePicking') : t('browse')),
533
+ React.createElement(Btn, { disabled: ui.testing || state.windowKind.text !== 'browser', onClick: ui.testBrowser }, ui.testing ? t('testRunning') : t('test'))
534
+ ),
535
+ footer: ui.testResult !== null
536
+ ? React.createElement('p', { className: ui.testResult.ok ? 'aow-test-ok' : 'aow-test-fail', role: 'status' },
537
+ (ui.testResult.ok ? '✓ ' : '✗ ') + ui.testResult.text
538
+ )
539
+ : null
540
+ }),
541
+ React.createElement(Field, {
542
+ key: 'exitOnWindowClose',
543
+ label: t('exitOnWindowClose'),
544
+ badges: badgesFor(t, state, 'exitOnWindowClose', resetField, t('experimental')),
545
+ control: React.createElement(Checkbox, { checked: state.exitOnWindowClose.text === true, disabled: !state.writable, onChange: (v) => edit('exitOnWindowClose', v) }, t('exitOnWindowCloseHint'))
546
+ })
547
+ ]
548
+ }
549
+ /** 宿主辅助路由(原生浏览对话框 / 拉起测试):两代插槽共用。 */
550
+ function useBrowserTools(t, state, edit) {
465
551
  const [picking, setPicking] = React.useState(false)
466
552
  const [testing, setTesting] = React.useState(false)
467
553
  const [testResult, setTestResult] = React.useState(null) // { ok, text } | null
468
-
469
554
  const browse = () => {
470
555
  if (!state.writable || picking) return
471
556
  setPicking(true)
@@ -473,13 +558,12 @@ window.__ModuleLoader__.load({
473
558
  .then((response) => response.json())
474
559
  .then((result) => {
475
560
  if (result !== null && result !== undefined && result.ok === true && typeof result.path === 'string') {
476
- props.edit('browserPath', result.path)
561
+ edit('browserPath', result.path)
477
562
  }
478
563
  })
479
564
  .catch(() => { /* 选择失败静默;用户可重试 */ })
480
565
  .then(() => { setPicking(false) })
481
566
  }
482
-
483
567
  const testBrowser = () => {
484
568
  if (testing) return
485
569
  setTesting(true)
@@ -487,7 +571,7 @@ window.__ModuleLoader__.load({
487
571
  fetch('/auto-open-web/test-browser', {
488
572
  method: 'POST',
489
573
  headers: { 'content-type': 'application/json' },
490
- body: JSON.stringify({ browserPath: state.browserPath.text }),
574
+ body: JSON.stringify({ browserPath: state.browserPath.text })
491
575
  })
492
576
  .then((response) => response.json())
493
577
  .then((result) => {
@@ -500,17 +584,182 @@ window.__ModuleLoader__.load({
500
584
  .catch((error) => { setTestResult({ ok: false, text: t('testFail') + ': ' + String(error !== null && error !== undefined && error.message !== undefined ? error.message : error) }) })
501
585
  .then(() => { setTesting(false) })
502
586
  }
587
+ return {
588
+ picking,
589
+ testing,
590
+ testResult,
591
+ browse,
592
+ testBrowser,
593
+ browserDisabled: !state.writable || state.windowKind.text !== 'browser'
594
+ }
595
+ }
503
596
 
504
- const browserDisabled = !state.writable || state.windowKind.text !== 'browser'
505
-
506
- const badgesFor = (field) => React.createElement(FieldBadges, {
507
- overridden: state[field].overridden,
508
- disabled: !state.writable,
509
- overriddenLabel: t('overridden'),
510
- resetLabel: t('reset'),
511
- onReset: () => props.resetField(field)
512
- })
597
+ // ── 新版(dsh 0.1.6-alpha):插件管理页 行配置页 ───────────────────
598
+ // 页面下发 `form = { state, mutate }`:state 是宿主投影的行 Config 快照
599
+ // (value/base/user/status/writable/revision),mutate(ops, revision) 原子写入。
600
+ // 与官方同一约定:只有保存会写入,离开页面即丢弃暂存(暂存只在本组件 state)。
601
+ function rowFieldStored(snapshot, field) {
602
+ const user = snapshot.user
603
+ return user !== undefined && user !== null && Object.prototype.hasOwnProperty.call(user, field)
604
+ }
605
+ /** 单字段视图(staged 优先,否则取快照值);与 FormModel.field 同语义。 */
606
+ function rowFieldView(snapshot, staged, spec) {
607
+ const edit = staged.get(spec.field)
608
+ const value = snapshot.value !== undefined && snapshot.value !== null ? snapshot.value[spec.field] : undefined
609
+ if (edit === undefined) {
610
+ return { text: spec.format(value), overridden: rowFieldStored(snapshot, spec.field), invalid: false }
611
+ }
612
+ const write = edit.clear ? { kind: 'clear' } : spec.parse(edit.text)
613
+ return { text: edit.text, overridden: write !== undefined && write.kind === 'set', invalid: write === undefined }
614
+ }
615
+ /** 暂存草稿 → 写入计划(与 FormModel.plan 同语义,产物是 path op)。 */
616
+ function rowPlan(snapshot, staged, specs) {
617
+ const plan = []
618
+ for (const [field, edit] of staged) {
619
+ const spec = specs.find((item) => item.field === field)
620
+ const value = snapshot.value !== undefined && snapshot.value !== null ? snapshot.value[field] : undefined
621
+ if (edit.clear) {
622
+ if (rowFieldStored(snapshot, field)) plan.push({ field, op: { op: 'unset', path: [field] } })
623
+ continue
624
+ }
625
+ if (edit.text === spec.format(value)) continue
626
+ const write = spec.parse(edit.text)
627
+ if (write === undefined) plan.push({ field, op: undefined })
628
+ else if (write.kind === 'clear') plan.push({ field, op: { op: 'unset', path: [field] } })
629
+ else plan.push({ field, op: { op: 'set', path: [field], value: write.value } })
630
+ }
631
+ return plan
632
+ }
633
+ /**
634
+ * bundle 配置页的数据源解析(按优先级):
635
+ * 1. 页面下发的 `props.form`(官方路径:页面把宿主投影的 Config 作为
636
+ * `{ state, mutate }` 传入——`plugins.item`/`plugins.row.config` 会这样传);
637
+ * 2. 页面**只传 view**(0.1.7-alpha.1 的 `plugins.bundle.config` 正是如此)时,
638
+ * 自行经客户端服务 `configForms` 按命名空间(= 本条目 id)解析,拿到
639
+ * ConfigForm(getSnapshot/subscribe/set/unset,与本插件 FormModel 的 scope
640
+ * 接口一致);候选顺序:行 id → 包名。
641
+ * 两者都没有才返回 null(组件显示"该命名空间未提供"——通常意味着 Config
642
+ * 字段没标 `.volatile()`,宿主压根没投影出命名空间)。
643
+ * @param props - 插槽 owner props(可能带 form)
644
+ * @param forms - configForms 服务(惰性读取,rc 线上为 undefined)
645
+ * @returns {{ state, mutate, subscribe }} 或 null
646
+ */
647
+ function resolveRowForm(props, forms) {
648
+ const supplied = props.form
649
+ if (supplied !== undefined && supplied !== null && supplied.state !== undefined && supplied.state !== null) {
650
+ return {
651
+ state: supplied.state,
652
+ mutate: (ops, revision) => supplied.mutate(ops, revision),
653
+ subscribe: null
654
+ }
655
+ }
656
+ if (forms === undefined || forms === null || typeof forms.get !== 'function') return null
657
+ for (const ns of ROW_FORM_NS_CANDIDATES) {
658
+ let bound = null
659
+ try {
660
+ bound = forms.get(ns)
661
+ } catch (e) {
662
+ bound = null
663
+ }
664
+ if (bound === undefined || bound === null || typeof bound.getSnapshot !== 'function') continue
665
+ const snapshot = bound.getSnapshot()
666
+ if (snapshot === undefined || snapshot === null || snapshot.status !== 'ready') continue
667
+ return {
668
+ state: snapshot,
669
+ mutate: (ops, revision) => bound.mutate(ops, revision),
670
+ subscribe: typeof bound.subscribe === 'function' ? (listener) => bound.subscribe(listener) : null
671
+ }
672
+ }
673
+ return null
674
+ }
675
+ function AutoOpenRowForm(props) {
676
+ const { t } = props
677
+ const form = resolveRowForm(props, props.configForms)
678
+ const [, forceTick] = React.useState(0)
679
+ const subscribe = form !== null ? form.subscribe : null
680
+ // 命名空间后续才就绪 / 值被外部改动时跟新(revision 变化即重渲染)。
681
+ React.useEffect(() => {
682
+ if (subscribe === null || subscribe === undefined) return undefined
683
+ return subscribe(() => forceTick((n) => n + 1))
684
+ }, [subscribe])
685
+ const snapshot = form !== null && form.state !== undefined && form.state !== null ? form.state : null
686
+ const [staged, setStaged] = React.useState(() => new Map())
687
+ const [saving, setSaving] = React.useState(false)
688
+ const [failed, setFailed] = React.useState(false)
689
+ const stagedRef = React.useRef(staged)
690
+ stagedRef.current = staged
691
+ const specs = React.useMemo(() => fieldSpecs(), [])
692
+ const available = snapshot !== null && snapshot.status === 'ready'
693
+ const writable = snapshot !== null && snapshot.writable === true
694
+ const plan = available ? rowPlan(snapshot, staged, specs) : []
695
+ const invalid = plan.some((item) => item.op === undefined)
696
+ const dirty = plan.length > 0
697
+ const state = {
698
+ available,
699
+ writable,
700
+ dirty,
701
+ invalid,
702
+ saving,
703
+ failed,
704
+ appWindow: { text: true, overridden: false, invalid: false },
705
+ windowKind: { text: 'webview2', overridden: false, invalid: false },
706
+ browserPath: { text: '', overridden: false, invalid: false },
707
+ exitOnWindowClose: { text: false, overridden: false, invalid: false }
708
+ }
709
+ if (snapshot !== null) {
710
+ for (const spec of specs) state[spec.field] = rowFieldView(snapshot, staged, spec)
711
+ }
712
+ const edit = (field, text) => {
713
+ const next = new Map(stagedRef.current)
714
+ next.set(field, { text, clear: false })
715
+ setStaged(next)
716
+ setFailed(false)
717
+ }
718
+ const resetField = (field) => {
719
+ const spec = specs.find((item) => item.field === field)
720
+ const base = snapshot !== null && snapshot.base !== undefined && snapshot.base !== null ? snapshot.base[field] : undefined
721
+ const next = new Map(stagedRef.current)
722
+ next.set(field, { text: spec.format(base), clear: true })
723
+ setStaged(next)
724
+ setFailed(false)
725
+ }
726
+ const save = () => {
727
+ if (!available || saving) return
728
+ const ops = plan.filter((item) => item.op !== undefined).map((item) => item.op)
729
+ if (plan.length === 0 || ops.length !== plan.length) return
730
+ setSaving(true)
731
+ setFailed(false)
732
+ Promise.resolve(form.mutate(ops, snapshot.revision))
733
+ .then((accepted) => {
734
+ if (accepted === true) setStaged(new Map())
735
+ setFailed(accepted !== true)
736
+ })
737
+ .catch(() => { setFailed(true) })
738
+ .then(() => { setSaving(false) })
739
+ }
740
+ const ui = useBrowserTools(t, state, edit)
741
+ // 行缺描述时页面取一行摘要。
742
+ if (props.view === 'summary') return t('description')
743
+ if (!available) return React.createElement('p', { className: 'aow-hint', role: 'status' }, t('unavailable'))
744
+ return React.createElement('div', null,
745
+ !writable ? React.createElement('p', { className: 'aow-cardReadOnly', role: 'status' }, t('readOnly')) : null,
746
+ ...buildFieldNodes(t, state, edit, resetField, ui),
747
+ React.createElement('div', { className: 'aow-cardFooter' },
748
+ failed ? React.createElement('p', { className: 'aow-cardFailed', role: 'status' }, t('saveFailed')) : null,
749
+ React.createElement(Btn, { variant: 'primary', disabled: !dirty || invalid || saving, onClick: save }, saving ? t('saving') : t('save'))
750
+ )
751
+ )
752
+ }
513
753
 
754
+ // ── 最新 rc 线(0.1.5-rc.x):设置页 → 插件配置卡片 ─────────────────────
755
+ // 宿主用 settings.register(ns, schema) 注册命名空间,客户端经 settingsScope
756
+ // 读写(该服务在新版已被 configForms 取代,故在 apply 里惰性读取)。
757
+ function AutoOpenSettingsPage(props) {
758
+ const { t } = props
759
+ const state = props.useAutoOpenCard((snapshot) => snapshot)
760
+ const ui = useBrowserTools(t, state, props.edit)
761
+ if (props.view === 'summary') return t('description')
762
+ const fields = buildFieldNodes(t, state, props.edit, props.resetField, ui)
514
763
  return React.createElement(Card, {
515
764
  t,
516
765
  titleKey: 'title',
@@ -518,71 +767,26 @@ window.__ModuleLoader__.load({
518
767
  state,
519
768
  onSave: props.save,
520
769
  onDiscard: props.discard
521
- },
522
- // ---- 独立应用窗口 ----
523
- React.createElement(Field, {
524
- label: t('appWindow'),
525
- badges: badgesFor('appWindow'),
526
- control: React.createElement(Checkbox, { checked: state.appWindow.text === true, disabled: !state.writable, onChange: (v) => props.edit('appWindow', v) }, t('appWindowHint'))
527
- }),
528
- // ---- 窗口类型 ----
529
- React.createElement(Field, {
530
- label: t('windowKind'),
531
- badges: badgesFor('windowKind'),
532
- control: React.createElement(React.Fragment, null,
533
- React.createElement(Radio, { checked: state.windowKind.text === 'webview2', disabled: !state.writable, value: 'webview2', onChange: (v) => props.edit('windowKind', v) }, t('windowKindWebview2')),
534
- React.createElement(Radio, { checked: state.windowKind.text === 'browser', disabled: !state.writable, value: 'browser', onChange: (v) => props.edit('windowKind', v) }, t('windowKindBrowser'))
535
- )
536
- }),
537
- // ---- 浏览器可执行文件(输入 + 浏览/测试按钮) ----
538
- React.createElement(Field, {
539
- label: t('browserPath'),
540
- badges: badgesFor('browserPath'),
541
- hint: t('browserPathHint'),
542
- invalid: state.browserPath.invalid,
543
- invalidLabel: t('browserPathInvalid'),
544
- control: React.createElement('div', { className: 'aow-row' },
545
- React.createElement(TextInput, {
546
- id: 'auto-open-web-browser-path',
547
- value: state.browserPath.text,
548
- placeholder: t('browserPathPlaceholder'),
549
- disabled: browserDisabled,
550
- invalid: state.browserPath.invalid,
551
- onChange: (text) => props.edit('browserPath', text)
552
- }),
553
- React.createElement(Btn, { disabled: !state.writable || picking, onClick: browse }, picking ? t('browsePicking') : t('browse')),
554
- React.createElement(Btn, { disabled: testing || state.windowKind.text !== 'browser', onClick: testBrowser }, testing ? t('testRunning') : t('test'))
555
- ),
556
- footer: testResult !== null
557
- ? React.createElement('p', { className: testResult.ok ? 'aow-test-ok' : 'aow-test-fail', role: 'status' },
558
- (testResult.ok ? '✓ ' : '✗ ') + testResult.text
559
- )
560
- : null
561
- }),
562
- // ---- 窗口关闭时退出 DSH(实验性) ----
563
- React.createElement(Field, {
564
- label: t('exitOnWindowClose'),
565
- badges: React.createElement(FieldBadges, {
566
- static: t('experimental'),
567
- overridden: state.exitOnWindowClose.overridden,
568
- disabled: !state.writable,
569
- overriddenLabel: t('overridden'),
570
- resetLabel: t('reset'),
571
- onReset: () => props.resetField('exitOnWindowClose')
572
- }),
573
- control: React.createElement(Checkbox, { checked: state.exitOnWindowClose.text === true, disabled: !state.writable, onChange: (v) => props.edit('exitOnWindowClose', v) }, t('exitOnWindowCloseHint'))
574
- })
575
- )
770
+ }, ...fields)
576
771
  }
577
772
 
578
773
  // ---- 文案(locale 机制,zh/en/ja/ko/fr/de/ru) ----
579
774
  const NS = 'auto-open-web'
775
+ /** bundle 包名(manifest 名,与安装名一致)。 */
776
+ const BUNDLE_NAME = 'dsh-auto-open-web'
777
+ /** 本插件在 cordis.patch.yml 里声明的行 id;新版 settings 命名空间 = 本条目 id。 */
778
+ const ROW_ID = 'auto-open-web'
779
+ /** rc 线的设置命名空间(设置页卡片按它派发)。 */
780
+ const SETTINGS_NS = 'auto-open-web'
781
+ /** 自行解析 ConfigForm 时的命名空间候选(条目 id 通常是行 id,兜底包名)。 */
782
+ const ROW_FORM_NS_CANDIDATES = [ROW_ID, BUNDLE_NAME]
580
783
  const en = {
581
784
  title: 'Auto-open web',
582
785
  description: 'Open the DSH Web GUI in an app-style window on profile start; the browser path can be configured manually.',
583
786
  save: 'Save', saving: 'Saving…', discard: 'Discard', unsaved: 'Unsaved',
584
787
  readOnly: 'This deployment stores settings read-only.',
585
788
  saveFailed: 'The deployment did not accept these values; they were left for you to correct.',
789
+ unavailable: 'This deployment does not serve these settings, so nothing can be configured here.',
586
790
  expand: 'Show settings', collapse: 'Hide settings',
587
791
  overridden: 'Overridden', reset: 'Reset to default',
588
792
  appWindow: 'Independent app window',
@@ -606,6 +810,7 @@ window.__ModuleLoader__.load({
606
810
  save: '保存', saving: '保存中…', discard: '放弃修改', unsaved: '未保存',
607
811
  readOnly: '本部署的设置为只读。',
608
812
  saveFailed: '本部署没有接受这些值,已保留供你修改。',
813
+ unavailable: '本部署未提供该设置命名空间,暂时无法在此配置。',
609
814
  expand: '展开设置', collapse: '收起设置',
610
815
  overridden: '已覆盖', reset: '恢复默认',
611
816
  appWindow: '独立应用窗口',
@@ -629,6 +834,7 @@ window.__ModuleLoader__.load({
629
834
  save: '保存', saving: '保存中…', discard: '変更を破棄', unsaved: '未保存',
630
835
  readOnly: 'このデプロイメントの設定は読み取り専用です。',
631
836
  saveFailed: 'デプロイメントがこの値を受け入れませんでした。修正用に保持されています。',
837
+ unavailable: 'このデプロイメントはこの設定を提供していないため、ここでは構成できません。',
632
838
  expand: '設定を表示', collapse: '設定を隠す',
633
839
  overridden: '上書き済み', reset: 'デフォルトに戻す',
634
840
  appWindow: '独立アプリウィンドウ',
@@ -652,6 +858,7 @@ window.__ModuleLoader__.load({
652
858
  save: '저장', saving: '저장 중…', discard: '변경 취소', unsaved: '저장 안 됨',
653
859
  readOnly: '이 배포의 설정은 읽기 전용입니다.',
654
860
  saveFailed: '배포가 이 값을 수락하지 않았습니다. 수정할 수 있도록 유지됩니다.',
861
+ unavailable: '이 배포는 이 설정을 제공하지 않아 여기에서 구성할 수 없습니다.',
655
862
  expand: '설정 표시', collapse: '설정 숨기기',
656
863
  overridden: '재정의됨', reset: '기본값으로 재설정',
657
864
  appWindow: '독립 앱 창',
@@ -675,6 +882,7 @@ window.__ModuleLoader__.load({
675
882
  save: 'Enregistrer', saving: 'Enregistrement…', discard: 'Annuler les modifications', unsaved: 'Non enregistré',
676
883
  readOnly: 'Les paramètres de ce déploiement sont en lecture seule.',
677
884
  saveFailed: "Le déploiement n'a pas accepté ces valeurs; elles ont été conservées pour correction.",
885
+ unavailable: "Ce déploiement ne fournit pas ces paramètres ; ils ne peuvent pas être configurés ici.",
678
886
  expand: 'Afficher les paramètres', collapse: 'Masquer les paramètres',
679
887
  overridden: 'Remplacé', reset: 'Rétablir la valeur par défaut',
680
888
  appWindow: "Fenêtre d'application indépendante",
@@ -698,6 +906,7 @@ window.__ModuleLoader__.load({
698
906
  save: 'Speichern', saving: 'Speichern…', discard: 'Änderungen verwerfen', unsaved: 'Nicht gespeichert',
699
907
  readOnly: 'Die Einstellungen dieser Bereitstellung sind schreibgeschützt.',
700
908
  saveFailed: 'Die Bereitstellung hat diese Werte nicht akzeptiert; sie wurden zur Korrektur beibehalten.',
909
+ unavailable: 'Diese Bereitstellung stellt diese Einstellungen nicht bereit; sie können hier nicht konfiguriert werden.',
701
910
  expand: 'Einstellungen anzeigen', collapse: 'Einstellungen ausblenden',
702
911
  overridden: 'Überschrieben', reset: 'Standard wiederherstellen',
703
912
  appWindow: 'Eigenständiges App-Fenster',
@@ -721,6 +930,7 @@ window.__ModuleLoader__.load({
721
930
  save: 'Сохранить', saving: 'Сохранение…', discard: 'Отменить изменения', unsaved: 'Не сохранено',
722
931
  readOnly: 'Настройки этого развёртывания доступны только для чтения.',
723
932
  saveFailed: 'Развёртывание не приняло эти значения; они оставлены для исправления.',
933
+ unavailable: 'Это развёртывание не предоставляет эти настройки; здесь их настроить нельзя.',
724
934
  expand: 'Показать настройки', collapse: 'Скрыть настройки',
725
935
  overridden: 'Переопределено', reset: 'Вернуть по умолчанию',
726
936
  appWindow: 'Независимое окно приложения',
@@ -742,20 +952,42 @@ window.__ModuleLoader__.load({
742
952
  function apply(ctx) {
743
953
  // 文案(locale 机制,跟随界面语言)
744
954
  ctx.effect(() => ctx.locale.register(NS, { zh, en, ja, ko, fr, de, ru }), 'auto-open-web: card dictionary')
745
- // 数据通道:settingsScope(官方 settings 域)
746
- const scope = ctx.settingsScope.bind({ namespace: 'auto-open-web' })
747
- const controller = new AutoOpenCardController(scope)
748
- // 插槽:keyed 注册 + inject 面(hooks + 表单动作)
749
- ctx.slots.inject('settings.plugin.item', () =>
955
+ // ── 新版(dsh ≥0.1.6-alpha):插件管理页 → **bundle 详情页**配置 ────────
956
+ // dsh-harness-tags 同款席位:插槽 `plugins.bundle.config`,key = **bundle 包名**
957
+ // (页面按 ledger.bundles.has(pkg.name) 判定是否渲染该区块,位置在说明与组件行之间)。
958
+ // 页面只传 `{ view: 'page' }` —— 数据要**自己绑**:命名空间 = 本条目 id,
959
+ // 由客户端服务 configForms 提供 ConfigForm(getSnapshot/subscribe/set/unset,
960
+ // 与 FormModel 的 scope 接口一致);页面若哪天也下发 form,则优先用它。
961
+ // 注意 configForms 必须**惰性读取**且不在 exports.inject 里:rc 线没有这个服务,
962
+ // 硬注入会让插件在那条线上永远 pending(harness-tags 只在 0.1.7+ 上跑,所以它
963
+ // 可以直接在 manifest 的 inject 里点名 plugin-manager,我们不能)。
964
+ const configForms = typeof ctx.get === 'function' ? ctx.get('configForms') : undefined
965
+ ctx.slots.inject('plugins.bundle.config', () =>
750
966
  ctx.slots.register(
751
- { name: 'settings.plugin.item', key: 'auto-open-web', label: '自动打开网页', locale: NS, inject: () => controller.inject() },
752
- AutoOpenSettingsPage
967
+ { name: 'plugins.bundle.config', key: BUNDLE_NAME, locale: NS },
968
+ (props) => AutoOpenRowForm(Object.assign({}, props, { configForms }))
753
969
  )
754
970
  )
971
+ // ── 最新 rc 线(0.1.5-rc.x):设置页 → 插件配置卡片 ───────────────────
972
+ // 该线仍走旧的 settings 域:宿主 settings.register(ns, schema) 注册命名空间,
973
+ // 客户端经 settingsScope 读写。该服务在新版已被 configForms 取代,故惰性读取:
974
+ // 缺失时只跳过这条注册,不影响新版路径(硬注入会让插件在新版永远 pending)。
975
+ const settingsScope = typeof ctx.get === 'function' ? ctx.get('settingsScope') : undefined
976
+ if (settingsScope !== undefined && settingsScope !== null && typeof settingsScope.bind === 'function') {
977
+ const controller = new AutoOpenCardController(settingsScope.bind({ namespace: SETTINGS_NS }))
978
+ ctx.slots.inject('settings.plugin.item', () =>
979
+ ctx.slots.register(
980
+ { name: 'settings.plugin.item', key: SETTINGS_NS, label: '自动打开网页', locale: NS, inject: () => controller.inject() },
981
+ AutoOpenSettingsPage
982
+ )
983
+ )
984
+ }
755
985
  }
756
986
 
757
- // 代码级服务依赖(cordis):数据/机制服务,不含任何 UI
758
- var clientInject = ['slots', 'locale', 'connection', 'remote', 'settingsScope']
987
+ // 代码级服务依赖(cordis):两代共有的服务,不含任何 UI 包。
988
+ // 注意:settingsScope(rc 线)/ configForms(新版)互不存在,二者都必须惰性读取,
989
+ // 否则硬注入会让插件在另一条线上永远 pending。
990
+ var clientInject = ['slots', 'locale', 'connection', 'remote']
759
991
 
760
992
  exports.apply = apply
761
993
  exports.inject = clientInject
package/lib/index.js CHANGED
@@ -74,17 +74,28 @@ export const name = 'auto-open-web';
74
74
  */
75
75
  export const inject = ['webServer', 'settings'];
76
76
 
77
+ /**
78
+ * 配置 schema。
79
+ *
80
+ * **每个字段都带 `meta.volatile`**(schemastery `.volatile()`):
81
+ * dsh 0.1.7 起,宿主只把**标记为 volatile** 的 Config 字段投影成设置表单
82
+ * (`dsh-settings` 的 `volatileForm()`:非 volatile 字段直接返回 undefined,
83
+ * 于是 `describe()` 里根本没有本条目 → 客户端拿不到 form,页面显示
84
+ * "本部署未提供该设置命名空间")。volatile 的语义是"改动无需重挂载即可生效",
85
+ * 与本插件的四个字段一致(它们都在下次打开/窗口关闭时才起作用,见下)。
86
+ * 宿主据此把用户层覆盖写回本条目,`config` 在 apply 时即含最终值。
87
+ */
77
88
  export const Config = z.object({
78
89
  /** 启动时自动打开独立应用窗口;false 时与官方相同:默认浏览器打开(普通标签页)。 */
79
- appWindow: z.boolean().default(true),
90
+ appWindow: z.boolean().default(true).volatile(),
80
91
  /** 窗口类型:webview2 = WebView2 宿主(独立进程,任务栏 DSH 图标,随 DSH 退出);
81
92
  * browser = 浏览器 --app 专用实例(--user-data-dir 隔离)。所选类型不可用时
82
93
  * 降级为最后的兜底:默认浏览器打开(官方 dsh web 同款,普通标签页)。 */
83
- windowKind: z.union([z.const('webview2'), z.const('browser')]).default('webview2'),
94
+ windowKind: z.union([z.const('webview2'), z.const('browser')]).default('webview2').volatile(),
84
95
  /** 手动维护的浏览器可执行文件路径(单条;优先于内置候选 Edge → Chrome;仅 browser 模式使用)。 */
85
- browserPath: z.string().default(''),
96
+ browserPath: z.string().default('').volatile(),
86
97
  /** 关闭自动打开的窗口时随之退出 DSH(默认关闭;仅 appWindow 开启时生效)。 */
87
- exitOnWindowClose: z.boolean().default(false),
98
+ exitOnWindowClose: z.boolean().default(false).volatile(),
88
99
  });
89
100
 
90
101
  const PICK_API_PATH = '/auto-open-web/pick-browser';
@@ -336,6 +347,13 @@ function removeLegacyState() {
336
347
  }
337
348
  }
338
349
 
350
+ /**
351
+ * 本进程已为哪个 GUI URL 打开过窗口。
352
+ * 新版设置是 live 应用的(行 Config 投影 + settings 文档),用户保存配置会让
353
+ * 本条目重新 apply;没有这道守卫就会重复弹窗。进程重启后自然重置。
354
+ */
355
+ let openedForUrl = null;
356
+
339
357
  /** 把任意输入规范化为完整配置(类型收紧 + 默认值;兼容旧 browsers 列表迁移)。 */
340
358
  function normalizeState(next) {
341
359
  const obj = next !== null && typeof next === 'object' ? next : {};
@@ -619,6 +637,14 @@ export function apply(ctx, config) {
619
637
  return;
620
638
  }
621
639
 
640
+ // 每进程每端口只打开一次:新版设置是 live 应用的(行 Config 投影),
641
+ // 用户保存配置会让本条目重新 apply —— 没有这道守卫就会再弹一个窗口。
642
+ if (openedForUrl === url) {
643
+ console.log('[auto-open-web] already opened for this process; skipping re-open (entry re-applied by a config change)');
644
+ return;
645
+ }
646
+ openedForUrl = url;
647
+
622
648
  // 独立应用窗口:按 windowKind 显式选择(webview2 | browser),直接加载 GUI 根地址。
623
649
  // 所选类型不可用时降级到最后兜底:默认浏览器打开(官方 dsh web 同款,
624
650
  // 普通标签页),保证用户至少能打开 GUI。
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-auto-open-web",
3
- "version": "0.1.20",
3
+ "version": "0.1.27",
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
  "repository": {
6
6
  "type": "git",
@@ -27,7 +27,6 @@
27
27
  "@deepseek-ai/dsh-client-connection",
28
28
  "@deepseek-ai/dsh-api-remotes",
29
29
  "@deepseek-ai/dsh-client-ui-settings",
30
- "@deepseek-ai/dsh-client-runtime",
31
30
  "@deepseek-ai/dsh-client-locale"
32
31
  ]
33
32
  }
@@ -46,7 +45,7 @@
46
45
  "README.en.md"
47
46
  ],
48
47
  "peerDependencies": {
49
- "@deepseek-ai/dsh": ">=0.1.0-rc.8 <0.2.0",
48
+ "@deepseek-ai/dsh": ">=0.1.5-rc.1 <0.2.0",
50
49
  "@deepseek-ai/schemastery": "^3.18.1"
51
50
  },
52
51
  "peerDependenciesMeta": {