dsh-auto-open-web 0.1.22 → 0.1.29
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 +31 -19
- package/README.md +44 -23
- package/lib/client.js +308 -146
- package/lib/index.js +73 -7
- package/package.json +2 -2
package/README.en.md
CHANGED
|
@@ -84,23 +84,33 @@ cleanup.
|
|
|
84
84
|
|
|
85
85
|
Two equivalent ways:
|
|
86
86
|
|
|
87
|
-
1. **Settings form** (recommended)
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
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
|
|
98
108
|
`appWindow` (independent app window), `windowKind` (WebView2 host / browser app window),
|
|
99
109
|
`browserPath` (browser executable, with a native "Browse" file dialog; located below the
|
|
100
110
|
window-type field and enabled only when "Browser app window" is selected),
|
|
101
111
|
`exitOnWindowClose` (exit DSH when the window closes, off by default).
|
|
102
|
-
After saving, values
|
|
103
|
-
|
|
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,
|
|
104
114
|
settings take precedence over row configuration. The host keeps only the "Browse" / "Test"
|
|
105
115
|
helper routes (capabilities the official channel cannot cover).
|
|
106
116
|
2. **Row configuration** (`cordis.patch.yml`): acts as the startup seed, effective until the
|
|
@@ -193,12 +203,14 @@ without touching the package.
|
|
|
193
203
|
|
|
194
204
|
- **Zero dependencies**: every runtime dependency is provided by the **DSH deployment** and
|
|
195
205
|
declared as an optional peer (no install warnings):
|
|
196
|
-
- `@deepseek-ai/dsh` (the host; declares the compatibility range
|
|
197
|
-
the
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
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)
|
|
202
214
|
- `@deepseek-ai/schemastery` (config schema validator; resolved at runtime: normal import
|
|
203
215
|
first, then the DSH deployment copy under the Windows global npm layout)
|
|
204
216
|
- `koffi` (Windows only: Job Object / process verification / native file dialog; same runtime
|
package/README.md
CHANGED
|
@@ -3,8 +3,9 @@
|
|
|
3
3
|
> English: [README.en.md](README.en.md)
|
|
4
4
|
|
|
5
5
|
dsh web profile 启动后自动打开独立应用窗口(或网页标签页)的常驻插件,并在
|
|
6
|
-
|
|
7
|
-
|
|
6
|
+
设置界面提供配置表单(手动维护浏览器位置等)。**最新版**的配置入口是
|
|
7
|
+
「插件管理」页里本插件**那一行**的配置页;**最新 rc 线(0.1.5-rc.x)**为
|
|
8
|
+
**设置 → 插件配置**卡片。
|
|
8
9
|
|
|
9
10
|
## 行为
|
|
10
11
|
|
|
@@ -79,22 +80,30 @@ Job Object(强杀也生效)+ 退出清理结束进程树。
|
|
|
79
80
|
|
|
80
81
|
两种途径,等价:
|
|
81
82
|
|
|
82
|
-
1.
|
|
83
|
-
-
|
|
84
|
-
`dsh-auto-open-web
|
|
85
|
-
(插槽 `plugins.bundle.config
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
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 宿主 /
|
|
92
101
|
浏览器应用窗口)、`browserPath`(浏览器可执行文件,支持「浏览」原生对话框
|
|
93
102
|
选择;位于窗口类型下方,仅选择「浏览器应用窗口」时使能)、
|
|
94
103
|
`exitOnWindowClose`(窗口关闭时退出 DSH,默认关闭)。
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
104
|
+
保存后设置值优先于行配置(新版落在该行的用户层覆盖上,rc 线落在
|
|
105
|
+
`auto-open-web` 设置命名空间);宿主仅保留「浏览」「测试」辅助路由
|
|
106
|
+
(官方通道无法覆盖的能力)。
|
|
98
107
|
2. **行配置**(cordis.patch.yml):作为启动种子,设置表单保存前生效。
|
|
99
108
|
|
|
100
109
|
| 字段 | 默认 | 说明 |
|
|
@@ -166,8 +175,8 @@ dsh plugin --profile web remove dsh-auto-open-web # 同时移除依赖与对
|
|
|
166
175
|
安装后:pnpm 将包加入 `profiles/web/node_modules`,`dsh` 把
|
|
167
176
|
`dsh-auto-open-web` 追加到 `dsh.profile.bundles`;启动时 bundle 的
|
|
168
177
|
`cordis.patch.yml` 插入插件行(`name: auto-open-web`,按包名解析)。
|
|
169
|
-
重启 `dsh web` 后,配置表单出现在对应入口(
|
|
170
|
-
`dsh-auto-open-web`
|
|
178
|
+
重启 `dsh web` 后,配置表单出现在对应入口(最新版:「插件管理」页里
|
|
179
|
+
`dsh-auto-open-web` 那一行的配置页;最新 rc 线:设置 → 插件配置卡片)。
|
|
171
180
|
客户端 bundle 由 modules 行按 `dsh.client` 声明在启动时扫描进浏览器清单;
|
|
172
181
|
插件在客户端**同时**向 `plugins.bundle.config`(新)与 `settings.plugin.item`
|
|
173
182
|
(旧)两个插槽注册,由客户端插槽机制决定哪个生效(不存在者保持 pending,
|
|
@@ -182,10 +191,11 @@ dsh plugin --profile web remove dsh-auto-open-web # 同时移除依赖与对
|
|
|
182
191
|
|
|
183
192
|
- **本包零 dependencies**:所有运行时依赖均由 **DSH 部署提供**,以 optional
|
|
184
193
|
peer 声明(安装无警告):
|
|
185
|
-
- `@deepseek-ai/dsh`(宿主,声明兼容范围
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
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` 回退已移除);不做运行时版本检测)
|
|
189
199
|
- `@deepseek-ai/schemastery`(配置 schema 校验器;运行时解析:常规 import
|
|
190
200
|
优先,其次 Windows 全局 npm 布局下的 DSH 部署副本)
|
|
191
201
|
- `koffi`(仅 Windows 的 Job Object / 进程校验 / 原生对话框;同样运行时
|
|
@@ -197,6 +207,16 @@ dsh plugin --profile web remove dsh-auto-open-web # 同时移除依赖与对
|
|
|
197
207
|
(不 require `dsh-client-ui-primitives`,不复刻官方样式表),卡片外壳、
|
|
198
208
|
字段、按钮、输入框、图标全部手写;观感经官方设计令牌变量
|
|
199
209
|
(`--dsw-alias-*` / `--dsw-static-*`)对齐,浅/深色自动跟随主题。
|
|
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`)。
|
|
200
220
|
**维护提示(0.1.22)**:主按钮(保存)必须用**随主题反转**的令牌对
|
|
201
221
|
`background:var(--dsw-alias-label-primary)` +
|
|
202
222
|
`color:var(--dsw-alias-bg-layer-3)`(与官方 `PluginConfigForm` 的 save 同款)。
|
|
@@ -205,8 +225,9 @@ dsh plugin --profile web remove dsh-auto-open-web # 同时移除依赖与对
|
|
|
205
225
|
同理不要引用并不存在的 `--dsw-alias-label-error`(真名是
|
|
206
226
|
`--dsw-alias-state-error-primary`)。这两条已由 `smoke-client-slots.mjs`
|
|
207
227
|
断言守护。
|
|
208
|
-
仅使用官方公开的数据/机制接口:`
|
|
209
|
-
(
|
|
228
|
+
仅使用官方公开的数据/机制接口:`slots`(插槽)、`locale`(文案)、
|
|
229
|
+
行 Config 表单(新版,页面下发 `form`)、rc 线的 `settingsScope`
|
|
230
|
+
(惰性读取)、快照 store。
|
|
210
231
|
- **npm 包已含 WebView2 宿主编译产物**(prepack 编译后发布);**GitHub
|
|
211
232
|
main 分支与源码 checkout 方式不含** `host-publish/`(构建产物被
|
|
212
233
|
.gitignore 忽略):webview2 模式需先在 `node_modules/dsh-auto-open-web`
|
package/lib/client.js
CHANGED
|
@@ -4,16 +4,24 @@
|
|
|
4
4
|
// **不** require @deepseek-ai/dsh-client-ui-primitives 等官方 UI 包。
|
|
5
5
|
//
|
|
6
6
|
// 依赖边界(与官方数据的对接面,均为公开接口):
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
//
|
|
12
|
-
//
|
|
13
|
-
//
|
|
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(不报错)。
|
|
14
21
|
// - locale:文案注册(zh/en/ja/ko/fr/de/ru,跟随界面语言)
|
|
15
|
-
// - store:createSnapshotStore 快照 store(
|
|
16
|
-
// dsh-client-runtime
|
|
22
|
+
// - store:createSnapshotStore 快照 store(dsh-client-store,缺失时内置最小实现)
|
|
23
|
+
// —— rc.8 时代的 dsh-client-runtime 回退已按兼容策略移除(0.1.2-alpha.2 起
|
|
24
|
+
// 统一为 dsh-client-store)。
|
|
17
25
|
//
|
|
18
26
|
// 视觉(全部自产):
|
|
19
27
|
// - 卡片外壳、字段、按钮、输入框、复选/单选、徽章、图标均为手写实现;
|
|
@@ -103,23 +111,15 @@ window.__ModuleLoader__.load({
|
|
|
103
111
|
injectCss(CONTROLS_TAG, CONTROLS_CSS)
|
|
104
112
|
|
|
105
113
|
// ── 共享模块 ─────────────────────────────────────────────────────────
|
|
106
|
-
// 快照 store
|
|
107
|
-
//
|
|
108
|
-
//
|
|
109
|
-
// 缺失或版本不含 createSnapshotStore 时退化为内置实现,绝不抛错。
|
|
114
|
+
// 快照 store:@deepseek-ai/dsh-client-store(壳层静态模块,自 dsh 0.1.2-alpha.2
|
|
115
|
+
// 起提供;最新 rc 线与最新版都包含它)。rc.8 时代的 dsh-client-runtime 回退
|
|
116
|
+
// 已按兼容策略移除;若该模块缺失(异常部署)退化为内置最小实现,绝不抛错。
|
|
110
117
|
var createSnapshotStore = null
|
|
111
118
|
try {
|
|
112
119
|
createSnapshotStore = require('@deepseek-ai/dsh-client-store').createSnapshotStore
|
|
113
120
|
} catch (e) {
|
|
114
121
|
console.error('[auto-open-web] dsh-client-store require failed:', e !== null && e !== undefined && e.message !== undefined ? e.message : String(e))
|
|
115
122
|
}
|
|
116
|
-
if (createSnapshotStore === undefined || createSnapshotStore === null) {
|
|
117
|
-
try {
|
|
118
|
-
createSnapshotStore = require('@deepseek-ai/dsh-client-runtime').createSnapshotStore
|
|
119
|
-
} catch (e) {
|
|
120
|
-
console.error('[auto-open-web] dsh-client-runtime require failed:', e !== null && e !== undefined && e.message !== undefined ? e.message : String(e))
|
|
121
|
-
}
|
|
122
|
-
}
|
|
123
123
|
if (createSnapshotStore === undefined || createSnapshotStore === null) {
|
|
124
124
|
createSnapshotStore = fallbackSnapshotStore
|
|
125
125
|
}
|
|
@@ -467,99 +467,56 @@ window.__ModuleLoader__.load({
|
|
|
467
467
|
}
|
|
468
468
|
}
|
|
469
469
|
|
|
470
|
-
// ──
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
const discardRef = React.useRef(props.discard)
|
|
485
|
-
discardRef.current = props.discard
|
|
486
|
-
React.useEffect(() => () => {
|
|
487
|
-
if (isPage) discardRef.current()
|
|
488
|
-
}, [])
|
|
489
|
-
|
|
490
|
-
const browse = () => {
|
|
491
|
-
if (!state.writable || picking) return
|
|
492
|
-
setPicking(true)
|
|
493
|
-
fetch('/auto-open-web/pick-browser', { method: 'POST' })
|
|
494
|
-
.then((response) => response.json())
|
|
495
|
-
.then((result) => {
|
|
496
|
-
if (result !== null && result !== undefined && result.ok === true && typeof result.path === 'string') {
|
|
497
|
-
props.edit('browserPath', result.path)
|
|
498
|
-
}
|
|
499
|
-
})
|
|
500
|
-
.catch(() => { /* 选择失败静默;用户可重试 */ })
|
|
501
|
-
.then(() => { setPicking(false) })
|
|
502
|
-
}
|
|
503
|
-
|
|
504
|
-
const testBrowser = () => {
|
|
505
|
-
if (testing) return
|
|
506
|
-
setTesting(true)
|
|
507
|
-
setTestResult(null)
|
|
508
|
-
fetch('/auto-open-web/test-browser', {
|
|
509
|
-
method: 'POST',
|
|
510
|
-
headers: { 'content-type': 'application/json' },
|
|
511
|
-
body: JSON.stringify({ browserPath: state.browserPath.text }),
|
|
512
|
-
})
|
|
513
|
-
.then((response) => response.json())
|
|
514
|
-
.then((result) => {
|
|
515
|
-
if (result !== null && result !== undefined && result.ok === true) {
|
|
516
|
-
setTestResult({ ok: true, text: typeof result.message === 'string' ? result.message : t('testOk') })
|
|
517
|
-
} else {
|
|
518
|
-
setTestResult({ ok: false, text: t('testFail') + ': ' + (result !== null && result !== undefined && typeof result.error === 'string' ? result.error : 'unknown') })
|
|
519
|
-
}
|
|
520
|
-
})
|
|
521
|
-
.catch((error) => { setTestResult({ ok: false, text: t('testFail') + ': ' + String(error !== null && error !== undefined && error.message !== undefined ? error.message : error) }) })
|
|
522
|
-
.then(() => { setTesting(false) })
|
|
523
|
-
}
|
|
524
|
-
|
|
525
|
-
const browserDisabled = !state.writable || state.windowKind.text !== 'browser'
|
|
526
|
-
|
|
527
|
-
const badgesFor = (field) => React.createElement(FieldBadges, {
|
|
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,
|
|
528
484
|
overridden: state[field].overridden,
|
|
529
485
|
disabled: !state.writable,
|
|
530
486
|
overriddenLabel: t('overridden'),
|
|
531
487
|
resetLabel: t('reset'),
|
|
532
|
-
onReset: () =>
|
|
488
|
+
onReset: () => resetField(field)
|
|
533
489
|
})
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
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 [
|
|
542
501
|
React.createElement(Field, {
|
|
543
502
|
key: 'appWindow',
|
|
544
503
|
label: t('appWindow'),
|
|
545
|
-
badges: badgesFor('appWindow'),
|
|
546
|
-
control: React.createElement(Checkbox, { checked: state.appWindow.text === true, disabled: !state.writable, onChange: (v) =>
|
|
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'))
|
|
547
506
|
}),
|
|
548
|
-
// 窗口类型
|
|
549
507
|
React.createElement(Field, {
|
|
550
508
|
key: 'windowKind',
|
|
551
509
|
label: t('windowKind'),
|
|
552
|
-
badges: badgesFor('windowKind'),
|
|
510
|
+
badges: badgesFor(t, state, 'windowKind', resetField),
|
|
553
511
|
control: React.createElement(React.Fragment, null,
|
|
554
|
-
React.createElement(Radio, { checked: state.windowKind.text === 'webview2', disabled: !state.writable, value: 'webview2', onChange: (v) =>
|
|
555
|
-
React.createElement(Radio, { checked: state.windowKind.text === 'browser', disabled: !state.writable, value: 'browser', onChange: (v) =>
|
|
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'))
|
|
556
514
|
)
|
|
557
515
|
}),
|
|
558
|
-
// 浏览器可执行文件(输入 + 浏览/测试按钮)
|
|
559
516
|
React.createElement(Field, {
|
|
560
517
|
key: 'browserPath',
|
|
561
518
|
label: t('browserPath'),
|
|
562
|
-
badges: badgesFor('browserPath'),
|
|
519
|
+
badges: badgesFor(t, state, 'browserPath', resetField),
|
|
563
520
|
hint: t('browserPathHint'),
|
|
564
521
|
invalid: state.browserPath.invalid,
|
|
565
522
|
invalidLabel: t('browserPathInvalid'),
|
|
@@ -568,54 +525,241 @@ window.__ModuleLoader__.load({
|
|
|
568
525
|
id: 'auto-open-web-browser-path',
|
|
569
526
|
value: state.browserPath.text,
|
|
570
527
|
placeholder: t('browserPathPlaceholder'),
|
|
571
|
-
disabled: browserDisabled,
|
|
528
|
+
disabled: ui.browserDisabled,
|
|
572
529
|
invalid: state.browserPath.invalid,
|
|
573
|
-
onChange: (text) =>
|
|
530
|
+
onChange: (text) => edit('browserPath', text)
|
|
574
531
|
}),
|
|
575
|
-
React.createElement(Btn, { disabled: !state.writable || picking, onClick: browse }, picking ? t('browsePicking') : t('browse')),
|
|
576
|
-
React.createElement(Btn, { disabled: testing || state.windowKind.text !== 'browser', onClick: testBrowser }, testing ? t('testRunning') : t('test'))
|
|
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'))
|
|
577
534
|
),
|
|
578
|
-
footer: testResult !== null
|
|
579
|
-
? React.createElement('p', { className: testResult.ok ? 'aow-test-ok' : 'aow-test-fail', role: 'status' },
|
|
580
|
-
(testResult.ok ? '✓ ' : '✗ ') + testResult.text
|
|
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
|
|
581
538
|
)
|
|
582
539
|
: null
|
|
583
540
|
}),
|
|
584
|
-
// 窗口关闭时退出 DSH(实验性)
|
|
585
541
|
React.createElement(Field, {
|
|
586
542
|
key: 'exitOnWindowClose',
|
|
587
543
|
label: t('exitOnWindowClose'),
|
|
588
|
-
badges:
|
|
589
|
-
|
|
590
|
-
overridden: state.exitOnWindowClose.overridden,
|
|
591
|
-
disabled: !state.writable,
|
|
592
|
-
overriddenLabel: t('overridden'),
|
|
593
|
-
resetLabel: t('reset'),
|
|
594
|
-
onReset: () => props.resetField('exitOnWindowClose')
|
|
595
|
-
}),
|
|
596
|
-
control: React.createElement(Checkbox, { checked: state.exitOnWindowClose.text === true, disabled: !state.writable, onChange: (v) => props.edit('exitOnWindowClose', v) }, t('exitOnWindowCloseHint'))
|
|
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'))
|
|
597
546
|
})
|
|
598
547
|
]
|
|
548
|
+
}
|
|
549
|
+
/** 宿主辅助路由(原生浏览对话框 / 拉起测试):两代插槽共用。 */
|
|
550
|
+
function useBrowserTools(t, state, edit) {
|
|
551
|
+
const [picking, setPicking] = React.useState(false)
|
|
552
|
+
const [testing, setTesting] = React.useState(false)
|
|
553
|
+
const [testResult, setTestResult] = React.useState(null) // { ok, text } | null
|
|
554
|
+
const browse = () => {
|
|
555
|
+
if (!state.writable || picking) return
|
|
556
|
+
setPicking(true)
|
|
557
|
+
fetch('/auto-open-web/pick-browser', { method: 'POST' })
|
|
558
|
+
.then((response) => response.json())
|
|
559
|
+
.then((result) => {
|
|
560
|
+
if (result !== null && result !== undefined && result.ok === true && typeof result.path === 'string') {
|
|
561
|
+
edit('browserPath', result.path)
|
|
562
|
+
}
|
|
563
|
+
})
|
|
564
|
+
.catch(() => { /* 选择失败静默;用户可重试 */ })
|
|
565
|
+
.then(() => { setPicking(false) })
|
|
566
|
+
}
|
|
567
|
+
const testBrowser = () => {
|
|
568
|
+
if (testing) return
|
|
569
|
+
setTesting(true)
|
|
570
|
+
setTestResult(null)
|
|
571
|
+
fetch('/auto-open-web/test-browser', {
|
|
572
|
+
method: 'POST',
|
|
573
|
+
headers: { 'content-type': 'application/json' },
|
|
574
|
+
body: JSON.stringify({ browserPath: state.browserPath.text })
|
|
575
|
+
})
|
|
576
|
+
.then((response) => response.json())
|
|
577
|
+
.then((result) => {
|
|
578
|
+
if (result !== null && result !== undefined && result.ok === true) {
|
|
579
|
+
setTestResult({ ok: true, text: typeof result.message === 'string' ? result.message : t('testOk') })
|
|
580
|
+
} else {
|
|
581
|
+
setTestResult({ ok: false, text: t('testFail') + ': ' + (result !== null && result !== undefined && typeof result.error === 'string' ? result.error : 'unknown') })
|
|
582
|
+
}
|
|
583
|
+
})
|
|
584
|
+
.catch((error) => { setTestResult({ ok: false, text: t('testFail') + ': ' + String(error !== null && error !== undefined && error.message !== undefined ? error.message : error) }) })
|
|
585
|
+
.then(() => { setTesting(false) })
|
|
586
|
+
}
|
|
587
|
+
return {
|
|
588
|
+
picking,
|
|
589
|
+
testing,
|
|
590
|
+
testResult,
|
|
591
|
+
browse,
|
|
592
|
+
testBrowser,
|
|
593
|
+
browserDisabled: !state.writable || state.windowKind.text !== 'browser'
|
|
594
|
+
}
|
|
595
|
+
}
|
|
599
596
|
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
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
|
|
606
624
|
}
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
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)
|
|
616
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
|
+
}
|
|
617
753
|
|
|
618
|
-
|
|
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)
|
|
619
763
|
return React.createElement(Card, {
|
|
620
764
|
t,
|
|
621
765
|
titleKey: 'title',
|
|
@@ -628,8 +772,14 @@ window.__ModuleLoader__.load({
|
|
|
628
772
|
|
|
629
773
|
// ---- 文案(locale 机制,zh/en/ja/ko/fr/de/ru) ----
|
|
630
774
|
const NS = 'auto-open-web'
|
|
631
|
-
/** bundle
|
|
775
|
+
/** bundle 包名(manifest 名,与安装名一致)。 */
|
|
632
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]
|
|
633
783
|
const en = {
|
|
634
784
|
title: 'Auto-open web',
|
|
635
785
|
description: 'Open the DSH Web GUI in an app-style window on profile start; the browser path can be configured manually.',
|
|
@@ -802,30 +952,42 @@ window.__ModuleLoader__.load({
|
|
|
802
952
|
function apply(ctx) {
|
|
803
953
|
// 文案(locale 机制,跟随界面语言)
|
|
804
954
|
ctx.effect(() => ctx.locale.register(NS, { zh, en, ja, ko, fr, de, ru }), 'auto-open-web: card dictionary')
|
|
805
|
-
//
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
//
|
|
809
|
-
//
|
|
810
|
-
//
|
|
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
|
|
811
965
|
ctx.slots.inject('plugins.bundle.config', () =>
|
|
812
966
|
ctx.slots.register(
|
|
813
|
-
{ name: 'plugins.bundle.config', key: BUNDLE_NAME, locale: NS
|
|
814
|
-
|
|
967
|
+
{ name: 'plugins.bundle.config', key: BUNDLE_NAME, locale: NS },
|
|
968
|
+
(props) => AutoOpenRowForm(Object.assign({}, props, { configForms }))
|
|
815
969
|
)
|
|
816
970
|
)
|
|
817
|
-
// ──
|
|
818
|
-
//
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
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
|
+
)
|
|
823
983
|
)
|
|
824
|
-
|
|
984
|
+
}
|
|
825
985
|
}
|
|
826
986
|
|
|
827
|
-
// 代码级服务依赖(cordis)
|
|
828
|
-
|
|
987
|
+
// 代码级服务依赖(cordis):两代共有的服务,不含任何 UI 包。
|
|
988
|
+
// 注意:settingsScope(rc 线)/ configForms(新版)互不存在,二者都必须惰性读取,
|
|
989
|
+
// 否则硬注入会让插件在另一条线上永远 pending。
|
|
990
|
+
var clientInject = ['slots', 'locale', 'connection', 'remote']
|
|
829
991
|
|
|
830
992
|
exports.apply = apply
|
|
831
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';
|
|
@@ -183,7 +194,7 @@ function openAppWindow(exe, url, browser, job) {
|
|
|
183
194
|
console.error(`[auto-open-web] app window spawn failed: ${error.message}`);
|
|
184
195
|
});
|
|
185
196
|
child.on('exit', (code) => {
|
|
186
|
-
if (code === 0 &&
|
|
197
|
+
if (code === 0 && exitOnCloseActive()) {
|
|
187
198
|
console.log('[auto-open-web] app window closed; exiting DSH (exitOnWindowClose)');
|
|
188
199
|
process.exit(0);
|
|
189
200
|
}
|
|
@@ -225,7 +236,7 @@ function spawnHostWindow(exe, url, iconPath) {
|
|
|
225
236
|
console.error(`[auto-open-web] host window spawn failed: ${error.message}`);
|
|
226
237
|
});
|
|
227
238
|
child.on('exit', (code) => {
|
|
228
|
-
if (code === 0 &&
|
|
239
|
+
if (code === 0 && exitOnCloseActive()) {
|
|
229
240
|
console.log('[auto-open-web] host window closed; exiting DSH (exitOnWindowClose)');
|
|
230
241
|
process.exit(0);
|
|
231
242
|
}
|
|
@@ -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 : {};
|
|
@@ -445,6 +463,18 @@ async function ensureIconFile(server) {
|
|
|
445
463
|
*/
|
|
446
464
|
let exitWatcherEnabled = false;
|
|
447
465
|
|
|
466
|
+
/**
|
|
467
|
+
* 关窗退出的**实时判定**(由 apply 注入)。
|
|
468
|
+
*
|
|
469
|
+
* 为什么不能只靠 `exitWatcherEnabled` 快照:dsh 0.1.7 起 volatile 字段的改动
|
|
470
|
+
* **不重跑 apply** —— loader 对 volatile 字段视作"相等"(`deepEqual(..., true)`),
|
|
471
|
+
* 只调 `updateVolatile(ref, source)` **就地改配置对象**再 emit
|
|
472
|
+
* `loader/volatile-update`。于是 apply 时快照下来的布尔值永远是旧值,
|
|
473
|
+
* 用户打开"窗口关闭时退出 DSH"后关窗不会退出(设置"没生效")。
|
|
474
|
+
* 这里改为在**判定时刻**读活配置(rc 线仍以 settings 已合并的 state 为准)。
|
|
475
|
+
*/
|
|
476
|
+
let exitOnCloseActive = () => false;
|
|
477
|
+
|
|
448
478
|
/** 测试钩子:纯函数导出,生产代码不依赖。 */
|
|
449
479
|
export const internals = {
|
|
450
480
|
resolveBrowserExe: platform.resolveBrowserExe,
|
|
@@ -492,7 +522,7 @@ export function apply(ctx, config) {
|
|
|
492
522
|
: base;
|
|
493
523
|
};
|
|
494
524
|
|
|
495
|
-
// ---- 配置状态:行配置为种子;settings 命名空间持久化(
|
|
525
|
+
// ---- 配置状态:行配置为种子;settings 命名空间持久化(rc 线由设置卡片写入) ----
|
|
496
526
|
const settingsSvc = ctx.get('settings');
|
|
497
527
|
let scope = null;
|
|
498
528
|
let writable = false;
|
|
@@ -521,6 +551,34 @@ export function apply(ctx, config) {
|
|
|
521
551
|
}
|
|
522
552
|
syncExitWatcher(); // 启动时的最终生效值(settings 已合并或行配置)
|
|
523
553
|
|
|
554
|
+
// 关窗退出的实时判定(见 exitOnCloseActive 的说明):
|
|
555
|
+
// · rc 线:settings 已合并进 state,由 scope.watch 维护;
|
|
556
|
+
// · 新版:settings.register 不存在,配置由宿主**就地更新** —— 判定时刻现读。
|
|
557
|
+
// 读活配置的口径与官方一致(见 dsh-experimental-speech-to-text / dsh-llm-deepseek):
|
|
558
|
+
// 条目上的 `ctx.fiber.entry.options.config` 是 loader 用 updateVolatile 维护的那份;
|
|
559
|
+
// apply 的 config 形参在 volatile 更新后可能仍是旧对象,故只作兜底。
|
|
560
|
+
const liveConfig = () => {
|
|
561
|
+
try {
|
|
562
|
+
const entry = ctx.fiber !== undefined && ctx.fiber !== null ? ctx.fiber.entry : undefined;
|
|
563
|
+
const fromEntry = entry !== undefined && entry !== null && entry.options !== undefined ? entry.options.config : undefined;
|
|
564
|
+
return fromEntry !== undefined && fromEntry !== null ? fromEntry : config;
|
|
565
|
+
} catch (error) {
|
|
566
|
+
return config;
|
|
567
|
+
}
|
|
568
|
+
};
|
|
569
|
+
const currentState = () => normalizeState(liveConfig());
|
|
570
|
+
exitOnCloseActive = () => (scope !== null ? state.exitOnWindowClose === true : currentState().exitOnWindowClose === true);
|
|
571
|
+
|
|
572
|
+
// 新版(0.1.7+)volatile 字段改动不重跑 apply:监听官方事件把 state 刷新,
|
|
573
|
+
// 让"浏览/测试"等辅助路由与退出判定都看得见新值。
|
|
574
|
+
if (typeof ctx.on === 'function') {
|
|
575
|
+
ctx.on('loader/volatile-update', () => {
|
|
576
|
+
state = currentState();
|
|
577
|
+
syncExitWatcher();
|
|
578
|
+
console.log(`[auto-open-web] volatile config updated (exitOnWindowClose=${String(state.exitOnWindowClose)}, appWindow=${String(state.appWindow)}, windowKind=${state.windowKind})`);
|
|
579
|
+
});
|
|
580
|
+
}
|
|
581
|
+
|
|
524
582
|
// ---- 卡片辅助 API(浏览/测试;设置读写走官方 settings 域) ----
|
|
525
583
|
// 设置卡片经客户端 settingsScope(settings 域)读写本命名空间,不再自建
|
|
526
584
|
// config 路由;这里只保留官方通道无法覆盖的能力。
|
|
@@ -619,6 +677,14 @@ export function apply(ctx, config) {
|
|
|
619
677
|
return;
|
|
620
678
|
}
|
|
621
679
|
|
|
680
|
+
// 每进程每端口只打开一次:新版设置是 live 应用的(行 Config 投影),
|
|
681
|
+
// 用户保存配置会让本条目重新 apply —— 没有这道守卫就会再弹一个窗口。
|
|
682
|
+
if (openedForUrl === url) {
|
|
683
|
+
console.log('[auto-open-web] already opened for this process; skipping re-open (entry re-applied by a config change)');
|
|
684
|
+
return;
|
|
685
|
+
}
|
|
686
|
+
openedForUrl = url;
|
|
687
|
+
|
|
622
688
|
// 独立应用窗口:按 windowKind 显式选择(webview2 | browser),直接加载 GUI 根地址。
|
|
623
689
|
// 所选类型不可用时降级到最后兜底:默认浏览器打开(官方 dsh web 同款,
|
|
624
690
|
// 普通标签页),保证用户至少能打开 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.29",
|
|
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",
|
|
@@ -45,7 +45,7 @@
|
|
|
45
45
|
"README.en.md"
|
|
46
46
|
],
|
|
47
47
|
"peerDependencies": {
|
|
48
|
-
"@deepseek-ai/dsh": ">=0.1.
|
|
48
|
+
"@deepseek-ai/dsh": ">=0.1.5-rc.1 <0.2.0",
|
|
49
49
|
"@deepseek-ai/schemastery": "^3.18.1"
|
|
50
50
|
},
|
|
51
51
|
"peerDependenciesMeta": {
|