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