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 +40 -12
- package/README.md +59 -17
- package/host-publish/DshAppWindow.exe +0 -0
- package/host-publish/DshAppWindow.pdb +0 -0
- package/lib/client.js +337 -105
- package/lib/index.js +30 -4
- package/package.json +2 -3
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
|
|
5
|
-
|
|
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
|
|
87
|
-
|
|
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
|
|
93
|
-
|
|
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
|
-
|
|
170
|
-
|
|
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
|
|
183
|
-
rc
|
|
184
|
-
|
|
185
|
-
|
|
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
|
-
|
|
83
|
-
|
|
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
|
-
|
|
87
|
-
|
|
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`
|
|
161
|
-
|
|
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`(宿主,声明兼容范围
|
|
173
|
-
|
|
174
|
-
|
|
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
|
-
|
|
188
|
-
(
|
|
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(
|
|
1
|
+
// dsh-auto-open-web — Client bundle(设置表单;UI 完全自绘,不依赖官方组件/CSS 模板)
|
|
2
2
|
// 格式:window.__ModuleLoader__.load({ id, factory });factory(require) 的
|
|
3
|
-
// require
|
|
4
|
-
//
|
|
3
|
+
// require 解析浏览器端冻结模块表(react、@deepseek-ai/dsh-client-store 等)。
|
|
4
|
+
// **不** require @deepseek-ai/dsh-client-ui-primitives 等官方 UI 包。
|
|
5
5
|
//
|
|
6
|
-
// 依赖边界(
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
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
|
-
// -
|
|
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-
|
|
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
|
-
|
|
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-
|
|
70
|
-
'.aow-invalid{color:var(--dsw-alias-
|
|
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-
|
|
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
|
|
98
|
-
//
|
|
99
|
-
//
|
|
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
|
-
|
|
463
|
-
|
|
464
|
-
|
|
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
|
-
|
|
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
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
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
|
-
//
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
//
|
|
749
|
-
|
|
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: '
|
|
752
|
-
|
|
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)
|
|
758
|
-
|
|
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.
|
|
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.
|
|
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": {
|