dsh-auto-open-web 0.1.4 → 0.1.9

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