dsh-deferred-boot 0.3.0 → 0.3.1
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/CHANGELOG.md +26 -2
- package/lib/client.js +405 -40
- package/market/index.json +2 -2
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,28 @@ All notable changes to this project are documented in this file.
|
|
|
4
4
|
The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
5
5
|
and this project adheres to [Semantic Versioning](https://semver.org/).
|
|
6
6
|
|
|
7
|
+
## [0.3.1] - 2026-11-07
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
- Plugin list inside the configuration form: installed plugins are grouped into
|
|
11
|
+
third-party and official entries (official collapsed by default), each with a
|
|
12
|
+
lightweight / medium / heavy startup-cost grade and an estimated mount cost.
|
|
13
|
+
- Checkbox selection writes `deferredPlugins` entries (`id`, `name`,
|
|
14
|
+
`strategy: "idle"`); the plugin itself and `@deepseek-ai/*` core rows are locked.
|
|
15
|
+
- Estimated extra startup wait shown at the bottom of the form.
|
|
16
|
+
|
|
17
|
+
### Changed
|
|
18
|
+
- The plugin page item renders a one-line summary in the list and the full form
|
|
19
|
+
when opened (view-aware: `summary` vs `page`).
|
|
20
|
+
- Right-side columns of the plugin list are fixed-width so grades, state and
|
|
21
|
+
cost line up.
|
|
22
|
+
|
|
23
|
+
### Fixed
|
|
24
|
+
- The form no longer crashes (and disappears) before the settings controller is
|
|
25
|
+
ready: snapshot reads, strict-context access and JSON edits are guarded.
|
|
26
|
+
- The list entry is registered through `plugins.item` so settings → plugins
|
|
27
|
+
shows the card again.
|
|
28
|
+
|
|
7
29
|
## [0.3.0] - 2026-11-07
|
|
8
30
|
|
|
9
31
|
### Added
|
|
@@ -52,5 +74,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/).
|
|
|
52
74
|
- Strategies: `idle`, `manual`, `inline-lazy`, `disabled`.
|
|
53
75
|
- Tests for the scheduler, config normalization, and the client module.
|
|
54
76
|
|
|
55
|
-
[0.
|
|
56
|
-
[0.
|
|
77
|
+
[0.3.1]: https://github.com/Bu-sheng/dsh-deferred-boot/releases/tag/v0.3.1
|
|
78
|
+
[0.3.0]: https://github.com/Bu-sheng/dsh-deferred-boot/releases/tag/v0.3.0
|
|
79
|
+
[0.2.0]: https://github.com/Bu-sheng/dsh-deferred-boot/releases/tag/v0.2.0
|
|
80
|
+
[0.1.0]: https://github.com/Bu-sheng/dsh-deferred-boot/releases/tag/v0.1.0
|
package/lib/client.js
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
// Client half of dsh-deferred-boot.
|
|
2
2
|
//
|
|
3
|
-
// It renders the plugin's form
|
|
4
|
-
//
|
|
5
|
-
// namespace through ctx.configForms.
|
|
6
|
-
// four fields as volatile so this
|
|
3
|
+
// It renders the plugin's form in the settings item slot of the plugin
|
|
4
|
+
// manager, lists the installed plugins with a startup cost grade, and
|
|
5
|
+
// reads/writes the `deferred-boot` settings namespace through ctx.configForms.
|
|
6
|
+
// The host half (src/index.js) declares the four fields as volatile so this
|
|
7
|
+
// form is the only place they are edited.
|
|
7
8
|
window.__ModuleLoader__.load({
|
|
8
9
|
id: 'dsh-deferred-boot',
|
|
9
10
|
factory: (require) => {
|
|
@@ -52,6 +53,51 @@ window.__ModuleLoader__.load({
|
|
|
52
53
|
font: 'inherit',
|
|
53
54
|
},
|
|
54
55
|
status: { fontSize: 12, color: 'var(--dsh-text-3, #8b8b8b)' },
|
|
56
|
+
list: { margin: '4px 0 0', padding: 0, listStyle: 'none', display: 'flex', flexDirection: 'column', gap: 2 },
|
|
57
|
+
item: { display: 'flex', alignItems: 'center', gap: 8, padding: '3px 0' },
|
|
58
|
+
itemName: { flex: '1 1 auto', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' },
|
|
59
|
+
itemMeta: { flex: '0 0 auto', fontSize: 12, color: 'var(--dsh-text-3, #8b8b8b)' },
|
|
60
|
+
badge: {
|
|
61
|
+
flex: '0 0 auto',
|
|
62
|
+
padding: '0 6px',
|
|
63
|
+
borderRadius: 4,
|
|
64
|
+
fontSize: 11,
|
|
65
|
+
lineHeight: '18px',
|
|
66
|
+
border: '1px solid var(--dsh-border, #3a3a3a)',
|
|
67
|
+
color: 'var(--dsh-text-2, #c8c8c8)',
|
|
68
|
+
},
|
|
69
|
+
badgeHeavy: { borderColor: '#a33', color: '#e88' },
|
|
70
|
+
badgeMedium: { borderColor: '#a73', color: '#ec8' },
|
|
71
|
+
badgeLight: { borderColor: '#3a7', color: '#8d8' },
|
|
72
|
+
footer: {
|
|
73
|
+
display: 'flex',
|
|
74
|
+
justifyContent: 'space-between',
|
|
75
|
+
alignItems: 'center',
|
|
76
|
+
marginTop: 10,
|
|
77
|
+
paddingTop: 8,
|
|
78
|
+
borderTop: '1px solid var(--dsh-border, #3a3a3a)',
|
|
79
|
+
fontSize: 12,
|
|
80
|
+
color: 'var(--dsh-text-3, #8b8b8b)',
|
|
81
|
+
},
|
|
82
|
+
estimate: { color: 'var(--dsh-text-2, #c8c8c8)' },
|
|
83
|
+
groupTitle: { margin: '12px 0 2px', fontSize: 12, color: 'var(--dsh-text-2, #c8c8c8)' },
|
|
84
|
+
linkButton: {
|
|
85
|
+
background: 'transparent',
|
|
86
|
+
border: 0,
|
|
87
|
+
padding: 0,
|
|
88
|
+
color: 'var(--dsh-accent, #4d6bfe)',
|
|
89
|
+
font: 'inherit',
|
|
90
|
+
fontSize: 12,
|
|
91
|
+
cursor: 'pointer',
|
|
92
|
+
},
|
|
93
|
+
card: {
|
|
94
|
+
border: '1px solid var(--dsh-border, #3a3a3a)',
|
|
95
|
+
borderRadius: 10,
|
|
96
|
+
padding: '12px 14px',
|
|
97
|
+
marginBottom: 10,
|
|
98
|
+
background: 'var(--dsh-surface-2, #1c1c1c)',
|
|
99
|
+
},
|
|
100
|
+
cardTitle: { margin: '0 0 10px', fontSize: 13, fontWeight: 600, color: 'var(--dsh-text-1, #e8e8e8)' },
|
|
55
101
|
}
|
|
56
102
|
|
|
57
103
|
const ZH = {
|
|
@@ -66,6 +112,21 @@ window.__ModuleLoader__.load({
|
|
|
66
112
|
'hint.disposeTimeout': '卸载或热重载目标插件时最多等待多久(毫秒)。超时视为已卸载并继续,防止卸载卡住调度。',
|
|
67
113
|
'hint.readyFallbackMs': '等不到“主界面已就绪”信号时的兜底超时(毫秒)。超时后按已就绪处理,继续延迟挂载。',
|
|
68
114
|
'hint.plugins': '每项一个目标插件:id 为 loader 条目 id,name 为模块名,strategy 决定挂载方式,config 传给目标插件。',
|
|
115
|
+
'catalog.title': '可放缓启动的插件',
|
|
116
|
+
'catalog.hint': '勾选后这些插件会在主界面就绪后逐个挂载;评级按启动开销经验估算,仅供参考。',
|
|
117
|
+
'catalog.loading': '正在读取已安装插件…',
|
|
118
|
+
'catalog.failed': '读取插件列表失败(需要新版 DSH 客户端)。',
|
|
119
|
+
'catalog.empty': '没有可勾选的插件。',
|
|
120
|
+
'catalog.core': '核心',
|
|
121
|
+
'catalog.self': '本插件',
|
|
122
|
+
'catalog.running': '运行中',
|
|
123
|
+
'catalog.disabled': '已禁用',
|
|
124
|
+
'catalog.unknown': '未运行',
|
|
125
|
+
'grade.light': '较轻',
|
|
126
|
+
'grade.medium': '中等',
|
|
127
|
+
'grade.heavy': '严重',
|
|
128
|
+
'advanced': '高级:直接编辑 deferredPlugins(JSON)',
|
|
129
|
+
'estimate': '预计本次启动额外等待 ≈ {seconds} 秒(估算)',
|
|
69
130
|
save: '保存',
|
|
70
131
|
saving: '保存中…',
|
|
71
132
|
saved: '已保存,下一次操作起生效。',
|
|
@@ -75,6 +136,14 @@ window.__ModuleLoader__.load({
|
|
|
75
136
|
notArray: '延迟插件必须是 JSON 数组。',
|
|
76
137
|
unavailable: '配置服务暂不可用(需要新版 DSH 客户端)。',
|
|
77
138
|
note: '配置写入当前 profile 的 Cordis patch,无需手动编辑 cordis.patch.yml。',
|
|
139
|
+
'section.hint': '主界面就绪后再逐个挂载插件,避免成批初始化拖住首屏。',
|
|
140
|
+
'section.open': '配置',
|
|
141
|
+
'section.close': '收起',
|
|
142
|
+
'panel.timing': '启动时机',
|
|
143
|
+
'group.third': '第三方插件',
|
|
144
|
+
'group.official': '官方插件',
|
|
145
|
+
'group.expand': '展开官方插件',
|
|
146
|
+
'group.collapse': '收起官方插件',
|
|
78
147
|
}
|
|
79
148
|
|
|
80
149
|
const EN = {
|
|
@@ -89,6 +158,21 @@ window.__ModuleLoader__.load({
|
|
|
89
158
|
'hint.disposeTimeout': 'Maximum time to wait while unloading or hot-reloading a target (ms). On timeout the target is treated as unmounted and scheduling continues.',
|
|
90
159
|
'hint.readyFallbackMs': 'Fallback timeout for “main UI ready” (ms). When no ready signal arrives, mounting starts after this delay.',
|
|
91
160
|
'hint.plugins': 'One entry per deferred plugin: id is the loader entry id, name is the module name, strategy controls how it mounts, config is passed to the target.',
|
|
161
|
+
'catalog.title': 'Plugins that can start later',
|
|
162
|
+
'catalog.hint': 'Checked plugins mount one by one after the main UI is ready. Grades are rough startup-cost estimates for reference only.',
|
|
163
|
+
'catalog.loading': 'Reading installed plugins…',
|
|
164
|
+
'catalog.failed': 'Could not read the installed plugin list (a newer DSH client is required).',
|
|
165
|
+
'catalog.empty': 'No plugins to choose from.',
|
|
166
|
+
'catalog.core': 'core',
|
|
167
|
+
'catalog.self': 'this plugin',
|
|
168
|
+
'catalog.running': 'running',
|
|
169
|
+
'catalog.disabled': 'disabled',
|
|
170
|
+
'catalog.unknown': 'not running',
|
|
171
|
+
'grade.light': 'light',
|
|
172
|
+
'grade.medium': 'medium',
|
|
173
|
+
'grade.heavy': 'heavy',
|
|
174
|
+
'advanced': 'Advanced: edit deferredPlugins (JSON) directly',
|
|
175
|
+
'estimate': 'Estimated extra wait this start ≈ {seconds}s (rough)',
|
|
92
176
|
save: 'Save',
|
|
93
177
|
saving: 'Saving…',
|
|
94
178
|
saved: 'Saved; takes effect from the next operation.',
|
|
@@ -98,6 +182,27 @@ window.__ModuleLoader__.load({
|
|
|
98
182
|
notArray: 'Deferred plugins must be a JSON array.',
|
|
99
183
|
unavailable: 'The config form service is unavailable (a newer DSH client is required).',
|
|
100
184
|
note: 'The change is written to the current profile Cordis patch; cordis.patch.yml stays untouched.',
|
|
185
|
+
'section.hint': 'Mount plugins one by one after the main UI is ready so a batch of initializers cannot hold up the first screen.',
|
|
186
|
+
'section.open': 'Configure',
|
|
187
|
+
'section.close': 'Collapse',
|
|
188
|
+
'panel.timing': 'Startup timing',
|
|
189
|
+
'group.third': 'Third-party plugins',
|
|
190
|
+
'group.official': 'Official plugins',
|
|
191
|
+
'group.expand': 'Show official plugins',
|
|
192
|
+
'group.collapse': 'Hide official plugins',
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// Startup-cost grades are rough estimates used only for ordering and the
|
|
196
|
+
// bottom-right figure. They do not claim to be measurements.
|
|
197
|
+
const COST = { light: 200, medium: 600, heavy: 1800 }
|
|
198
|
+
const HEAVY = /billion-context|dshmarket|univer|office-toolkit|solidworks|mech-workbench|tabbit/
|
|
199
|
+
const MEDIUM = /dsh-pilot|whale|token-usage|plugin-mgr|dsh-unlag/
|
|
200
|
+
|
|
201
|
+
function gradeOf(name) {
|
|
202
|
+
const text = String(name || '')
|
|
203
|
+
if (HEAVY.test(text)) return 'heavy'
|
|
204
|
+
if (MEDIUM.test(text)) return 'medium'
|
|
205
|
+
return 'light'
|
|
101
206
|
}
|
|
102
207
|
|
|
103
208
|
function Row(props) {
|
|
@@ -110,44 +215,226 @@ window.__ModuleLoader__.load({
|
|
|
110
215
|
}
|
|
111
216
|
|
|
112
217
|
function useSnapshot(form) {
|
|
113
|
-
|
|
218
|
+
// A settings controller can refuse `getSnapshot()` before its describe
|
|
219
|
+
// round-trip lands. That throw used to unmount the whole slot subtree
|
|
220
|
+
// (the form "flashed" and vanished), so every read is guarded.
|
|
221
|
+
const read = () => {
|
|
222
|
+
try {
|
|
223
|
+
if (!form || typeof form.getSnapshot !== 'function') return null
|
|
224
|
+
const value = form.getSnapshot()
|
|
225
|
+
return value && typeof value === 'object' ? value : null
|
|
226
|
+
} catch {
|
|
227
|
+
return null
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
const [snapshot, setSnapshot] = React.useState(read)
|
|
114
231
|
React.useEffect(() => {
|
|
115
|
-
|
|
116
|
-
|
|
232
|
+
if (!form || typeof form.subscribe !== 'function') return undefined
|
|
233
|
+
const sync = () => {
|
|
234
|
+
try {
|
|
235
|
+
setSnapshot(form.getSnapshot())
|
|
236
|
+
} catch {}
|
|
237
|
+
}
|
|
238
|
+
sync()
|
|
239
|
+
let off = null
|
|
240
|
+
try {
|
|
241
|
+
off = form.subscribe(sync)
|
|
242
|
+
} catch {}
|
|
243
|
+
return () => {
|
|
244
|
+
try {
|
|
245
|
+
if (typeof off === 'function') off()
|
|
246
|
+
} catch {}
|
|
247
|
+
}
|
|
117
248
|
}, [form])
|
|
118
249
|
return snapshot
|
|
119
250
|
}
|
|
120
251
|
|
|
252
|
+
function toPlugins(snapshot) {
|
|
253
|
+
const value = (snapshot && snapshot.value) || {}
|
|
254
|
+
return Array.isArray(value.deferredPlugins) ? value.deferredPlugins : []
|
|
255
|
+
}
|
|
256
|
+
|
|
121
257
|
function toDraft(snapshot) {
|
|
122
258
|
const value = (snapshot && snapshot.value) || {}
|
|
123
|
-
const plugins = Array.isArray(value.deferredPlugins) ? value.deferredPlugins : []
|
|
124
259
|
return {
|
|
125
260
|
idleDelay: String(value.idleDelay ?? 1200),
|
|
126
261
|
disposeTimeout: String(value.disposeTimeout ?? 10000),
|
|
127
262
|
readyFallbackMs: String(value.readyFallbackMs ?? 5000),
|
|
128
|
-
plugins: JSON.stringify(
|
|
263
|
+
plugins: JSON.stringify(toPlugins(snapshot), null, 2),
|
|
129
264
|
}
|
|
130
265
|
}
|
|
131
266
|
|
|
267
|
+
function badgeStyle(grade) {
|
|
268
|
+
if (grade === 'heavy') return Object.assign({}, styles.badge, styles.badgeHeavy)
|
|
269
|
+
if (grade === 'medium') return Object.assign({}, styles.badge, styles.badgeMedium)
|
|
270
|
+
return Object.assign({}, styles.badge, styles.badgeLight)
|
|
271
|
+
}
|
|
272
|
+
|
|
132
273
|
// The slot system does not guarantee that a form component receives the
|
|
133
274
|
// plugin ctx, so keep the injected instance reachable from apply().
|
|
134
275
|
let pluginCtx = null
|
|
135
276
|
|
|
277
|
+
// Reading a service off a Cordis context is gated by `inject` and throws on
|
|
278
|
+
// the strict proxy when the service is not injected for this component, so
|
|
279
|
+
// every access goes through try/catch: a throw here would unmount the whole
|
|
280
|
+
// slot subtree (the form "flashes" and disappears).
|
|
281
|
+
function configFormsOf(props) {
|
|
282
|
+
try {
|
|
283
|
+
const forms = props && props.ctx && props.ctx.configForms
|
|
284
|
+
if (forms && typeof forms.get === 'function') return forms
|
|
285
|
+
} catch {}
|
|
286
|
+
try {
|
|
287
|
+
const forms = pluginCtx && pluginCtx.configForms
|
|
288
|
+
if (forms && typeof forms.get === 'function') return forms
|
|
289
|
+
} catch {}
|
|
290
|
+
return null
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// The slot renders the full form directly: opening the settings card (or
|
|
294
|
+
// the plugin page) lands on the form itself, without an extra collapsed
|
|
295
|
+
// row. The form lives in its own component so it mounts hook-safely.
|
|
136
296
|
function DeferredBootForm(props) {
|
|
137
|
-
const t = typeof props?.t === 'function'
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
297
|
+
const t = typeof props?.t === 'function'
|
|
298
|
+
? props.t
|
|
299
|
+
: (key, params) => {
|
|
300
|
+
let text = ZH[key] ?? key
|
|
301
|
+
if (params && params.seconds !== undefined) text = text.replace('{seconds}', params.seconds)
|
|
302
|
+
return text
|
|
303
|
+
}
|
|
304
|
+
const forms = configFormsOf(props)
|
|
305
|
+
if (!forms) return h('p', { style: styles.intro }, t('unavailable'))
|
|
306
|
+
let form = null
|
|
307
|
+
for (const namespace of ['deferred-boot', 'include:deferred-boot']) {
|
|
308
|
+
try {
|
|
309
|
+
const candidate = forms.get(namespace)
|
|
310
|
+
if (candidate && typeof candidate.getSnapshot === 'function') {
|
|
311
|
+
form = candidate
|
|
312
|
+
break
|
|
313
|
+
}
|
|
314
|
+
} catch {}
|
|
315
|
+
}
|
|
316
|
+
if (!form || typeof form.getSnapshot !== 'function') return h('p', { style: styles.intro }, t('unavailable'))
|
|
141
317
|
const snapshot = useSnapshot(form)
|
|
142
318
|
const [draft, setDraft] = React.useState(() => toDraft(snapshot))
|
|
143
319
|
const [status, setStatus] = React.useState('')
|
|
320
|
+
const [catalog, setCatalog] = React.useState({ status: 'loading', rows: [] })
|
|
321
|
+
const [officialOpen, setOfficialOpen] = React.useState(false)
|
|
144
322
|
|
|
145
323
|
React.useEffect(() => {
|
|
146
324
|
setDraft(toDraft(snapshot))
|
|
147
325
|
}, [snapshot])
|
|
148
326
|
|
|
327
|
+
// Read the installed plugin rows through the owner-side remote API. The
|
|
328
|
+
// owner page is always allowed, so no remoteAccess gate is needed.
|
|
329
|
+
React.useEffect(() => {
|
|
330
|
+
const ctx = pluginCtx || (props && props.ctx)
|
|
331
|
+
let alive = true
|
|
332
|
+
let timer = null
|
|
333
|
+
const fail = (message) => {
|
|
334
|
+
if (!alive) return
|
|
335
|
+
setCatalog({ status: 'failed', rows: [], error: message || '' })
|
|
336
|
+
}
|
|
337
|
+
if (!ctx) {
|
|
338
|
+
fail('no context')
|
|
339
|
+
return () => {
|
|
340
|
+
alive = false
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
// `remote` may be mounted slightly after the page loads, so retry a
|
|
344
|
+
// few times before giving up. Cordis 4 contexts are strict proxies:
|
|
345
|
+
// reading an unknown service throws, so guard every touch.
|
|
346
|
+
const attempt = (remaining) => {
|
|
347
|
+
let api = null
|
|
348
|
+
try {
|
|
349
|
+
api = (typeof ctx.get === 'function' ? ctx.get('remote.pluginManager') : null)
|
|
350
|
+
|| (ctx.remote && ctx.remote.pluginManager)
|
|
351
|
+
} catch (error) {
|
|
352
|
+
fail(error && error.message ? error.message : 'remote unavailable')
|
|
353
|
+
return
|
|
354
|
+
}
|
|
355
|
+
if (!api || typeof api.listPlugins !== 'function') {
|
|
356
|
+
fail('pluginManager/listPlugins unavailable')
|
|
357
|
+
return
|
|
358
|
+
}
|
|
359
|
+
Promise.resolve()
|
|
360
|
+
.then(() => api.listPlugins())
|
|
361
|
+
.then((answer) => {
|
|
362
|
+
if (!alive) return
|
|
363
|
+
if (!answer || !answer.ok) {
|
|
364
|
+
if (remaining > 0) {
|
|
365
|
+
timer = setTimeout(() => attempt(remaining - 1), 1200)
|
|
366
|
+
return
|
|
367
|
+
}
|
|
368
|
+
fail(answer && answer.error ? (answer.error.code || answer.error.message) : 'listPlugins refused')
|
|
369
|
+
return
|
|
370
|
+
}
|
|
371
|
+
const rows = (Array.isArray(answer.value) ? answer.value : [])
|
|
372
|
+
.filter((row) => row && typeof row.moduleName === 'string')
|
|
373
|
+
.map((row) => ({
|
|
374
|
+
id: row.entryId ?? row.moduleName,
|
|
375
|
+
name: row.moduleName,
|
|
376
|
+
enabled: row.enabled !== false,
|
|
377
|
+
phase: row.fiberPhase ?? null,
|
|
378
|
+
}))
|
|
379
|
+
setCatalog({ status: 'ready', rows })
|
|
380
|
+
})
|
|
381
|
+
.catch((error) => {
|
|
382
|
+
if (!alive) return
|
|
383
|
+
if (remaining > 0) {
|
|
384
|
+
timer = setTimeout(() => attempt(remaining - 1), 1200)
|
|
385
|
+
return
|
|
386
|
+
}
|
|
387
|
+
fail(error && error.message ? error.message : 'listPlugins failed')
|
|
388
|
+
})
|
|
389
|
+
}
|
|
390
|
+
attempt(2)
|
|
391
|
+
return () => {
|
|
392
|
+
alive = false
|
|
393
|
+
if (timer) clearTimeout(timer)
|
|
394
|
+
}
|
|
395
|
+
}, [props && props.ctx])
|
|
396
|
+
|
|
149
397
|
const set = (key) => (event) => setDraft((prev) => ({ ...prev, [key]: event.target.value }))
|
|
150
398
|
|
|
399
|
+
const plugins = React.useMemo(() => {
|
|
400
|
+
try {
|
|
401
|
+
const parsed = JSON.parse(draft.plugins)
|
|
402
|
+
return Array.isArray(parsed) ? parsed : []
|
|
403
|
+
} catch {
|
|
404
|
+
return []
|
|
405
|
+
}
|
|
406
|
+
}, [draft.plugins])
|
|
407
|
+
|
|
408
|
+
const selected = React.useMemo(() => {
|
|
409
|
+
const map = new Map()
|
|
410
|
+
for (const item of plugins) {
|
|
411
|
+
if (item && typeof item.name === 'string') map.set(item.name, item)
|
|
412
|
+
}
|
|
413
|
+
return map
|
|
414
|
+
}, [plugins])
|
|
415
|
+
|
|
416
|
+
const toggle = (row, checked) => {
|
|
417
|
+
let current = []
|
|
418
|
+
try {
|
|
419
|
+
const parsed = JSON.parse(draft.plugins)
|
|
420
|
+
current = Array.isArray(parsed) ? parsed : []
|
|
421
|
+
} catch {}
|
|
422
|
+
const list = current.slice()
|
|
423
|
+
const index = list.findIndex((item) => item && item.name === row.name)
|
|
424
|
+
if (checked && index < 0) {
|
|
425
|
+
list.push({ id: row.id, name: row.name, strategy: 'idle' })
|
|
426
|
+
} else if (!checked && index >= 0) {
|
|
427
|
+
list.splice(index, 1)
|
|
428
|
+
}
|
|
429
|
+
setDraft((prev) => ({ ...prev, plugins: JSON.stringify(list, null, 2) }))
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
const estimateMs = React.useMemo(() => {
|
|
433
|
+
let total = Number(draft.idleDelay) || 0
|
|
434
|
+
for (const name of selected.keys()) total += COST[gradeOf(name)] || 0
|
|
435
|
+
return total
|
|
436
|
+
}, [selected, draft.idleDelay])
|
|
437
|
+
|
|
151
438
|
const save = async () => {
|
|
152
439
|
let plugins
|
|
153
440
|
try {
|
|
@@ -179,50 +466,128 @@ window.__ModuleLoader__.load({
|
|
|
179
466
|
}
|
|
180
467
|
}
|
|
181
468
|
|
|
469
|
+
const rows = catalog.rows
|
|
470
|
+
const groups = React.useMemo(() => {
|
|
471
|
+
const official = []
|
|
472
|
+
const third = []
|
|
473
|
+
for (const row of rows) {
|
|
474
|
+
if (row.name.indexOf('@deepseek-ai/') === 0) official.push(row)
|
|
475
|
+
else third.push(row)
|
|
476
|
+
}
|
|
477
|
+
return { official, third }
|
|
478
|
+
}, [rows])
|
|
479
|
+
|
|
480
|
+
const renderRow = (row) => {
|
|
481
|
+
const isSelf = row.name === 'dsh-deferred-boot'
|
|
482
|
+
const isCore = row.name.indexOf('@deepseek-ai/') === 0
|
|
483
|
+
const locked = isSelf || isCore
|
|
484
|
+
const grade = gradeOf(row.name)
|
|
485
|
+
const checked = selected.has(row.name)
|
|
486
|
+
const stateText = row.phase === 'failed' ? t('catalog.unknown') : row.enabled ? t('catalog.running') : t('catalog.disabled')
|
|
487
|
+
return h('li', { key: row.id, style: styles.item },
|
|
488
|
+
h('input', {
|
|
489
|
+
type: 'checkbox',
|
|
490
|
+
checked,
|
|
491
|
+
disabled: locked,
|
|
492
|
+
onChange: (event) => { if (!locked) toggle(row, event.target.checked) },
|
|
493
|
+
}),
|
|
494
|
+
h('span', { style: styles.itemName, title: row.name }, row.name),
|
|
495
|
+
h('span', { style: Object.assign({}, badgeStyle(grade), { width: 56, flex: '0 0 56px', textAlign: 'center' }) }, t('grade.' + grade)),
|
|
496
|
+
h('span', { style: { width: 56, flex: '0 0 56px', textAlign: 'right', fontSize: 12, color: 'var(--dsh-text-3, #8b8b8b)' } }, isSelf ? t('catalog.self') : isCore ? t('catalog.core') : stateText),
|
|
497
|
+
h('span', { style: { width: 72, flex: '0 0 72px', textAlign: 'right', fontSize: 12, color: 'var(--dsh-text-3, #8b8b8b)', fontVariantNumeric: 'tabular-nums' } }, '~' + COST[grade] + 'ms'),
|
|
498
|
+
)
|
|
499
|
+
}
|
|
500
|
+
|
|
182
501
|
return h(
|
|
183
502
|
'div',
|
|
184
503
|
{ style: styles.root },
|
|
185
504
|
h('p', { style: styles.intro }, t('intro')),
|
|
186
|
-
h('
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
h('
|
|
202
|
-
|
|
203
|
-
|
|
505
|
+
h('section', { style: styles.card },
|
|
506
|
+
h('h3', { style: styles.cardTitle }, t('panel.timing')),
|
|
507
|
+
h(Row, {
|
|
508
|
+
label: t('field.idleDelay'),
|
|
509
|
+
hint: t('hint.idleDelay'),
|
|
510
|
+
}, h('input', { style: styles.input, type: 'number', value: draft.idleDelay, onChange: set('idleDelay') })),
|
|
511
|
+
h(Row, {
|
|
512
|
+
label: t('field.disposeTimeout'),
|
|
513
|
+
hint: t('hint.disposeTimeout'),
|
|
514
|
+
}, h('input', { style: styles.input, type: 'number', value: draft.disposeTimeout, onChange: set('disposeTimeout') })),
|
|
515
|
+
h(Row, {
|
|
516
|
+
label: t('field.readyFallbackMs'),
|
|
517
|
+
hint: t('hint.readyFallbackMs'),
|
|
518
|
+
}, h('input', { style: styles.input, type: 'number', value: draft.readyFallbackMs, onChange: set('readyFallbackMs') })),
|
|
519
|
+
),
|
|
520
|
+
h('section', { style: styles.card },
|
|
521
|
+
h('h3', { style: styles.cardTitle }, t('catalog.title')),
|
|
522
|
+
h('p', { style: { margin: '0 0 8px', fontSize: 12, color: 'var(--dsh-text-3, #8b8b8b)' } }, t('catalog.hint')),
|
|
523
|
+
catalog.status === 'loading'
|
|
524
|
+
? h('p', { style: Object.assign({}, styles.note, { margin: '0' }) }, t('catalog.loading'))
|
|
525
|
+
: catalog.status === 'failed'
|
|
526
|
+
? h('p', { style: Object.assign({}, styles.note, { margin: '0' }) }, t('catalog.failed') + (catalog.error ? '(' + catalog.error + ')' : ''))
|
|
527
|
+
: rows.length === 0
|
|
528
|
+
? h('p', { style: Object.assign({}, styles.note, { margin: '0' }) }, t('catalog.empty'))
|
|
529
|
+
: h('div', null,
|
|
530
|
+
h('p', { style: styles.groupTitle }, t('group.third') + '(' + groups.third.length + ')'),
|
|
531
|
+
groups.third.length === 0 ? null : h('ul', { style: styles.list }, groups.third.map(renderRow)),
|
|
532
|
+
h('button', { type: 'button', style: styles.linkButton, onClick: () => setOfficialOpen((value) => !value) },
|
|
533
|
+
(officialOpen ? t('group.collapse') : t('group.expand')) + '(' + groups.official.length + ')'),
|
|
534
|
+
officialOpen ? h('ul', { style: styles.list }, groups.official.map(renderRow)) : null,
|
|
535
|
+
),
|
|
536
|
+
),
|
|
537
|
+
|
|
538
|
+
h('details', { style: { marginTop: 10 } },
|
|
539
|
+
h('summary', { style: { cursor: 'pointer', fontSize: 12, color: 'var(--dsh-text-3, #8b8b8b)' } }, t('advanced')),
|
|
540
|
+
h('textarea', { style: styles.textarea, value: draft.plugins, onChange: set('plugins') }),
|
|
541
|
+
),
|
|
542
|
+
|
|
543
|
+
h('div', { style: styles.footer },
|
|
544
|
+
h('span', { style: styles.status }, status || t('note')),
|
|
545
|
+
h('span', { style: styles.estimate }, t('estimate', { seconds: (estimateMs / 1000).toFixed(1) })),
|
|
546
|
+
),
|
|
547
|
+
h('div', { style: styles.actions },
|
|
548
|
+
h('button', { style: styles.button, type: 'button', onClick: save }, t('save')),
|
|
549
|
+
typeof props?.onClose === 'function'
|
|
550
|
+
? h('button', { type: 'button', style: styles.linkButton, onClick: props.onClose }, t('section.close'))
|
|
551
|
+
: null,
|
|
552
|
+
),
|
|
204
553
|
)
|
|
205
554
|
}
|
|
206
555
|
|
|
556
|
+
// The settings item is rendered twice by the manager: once as the card
|
|
557
|
+
// summary in the installed list and once as the page body after opening
|
|
558
|
+
// it. `view` tells them apart, so the long form only mounts on the page.
|
|
559
|
+
function DeferredBootItem(props) {
|
|
560
|
+
const t = typeof props?.t === 'function'
|
|
561
|
+
? props.t
|
|
562
|
+
: (key) => ZH[key] ?? key
|
|
563
|
+
if (props && props.view === 'summary') {
|
|
564
|
+
return h('span', { style: { fontSize: 12, color: 'var(--dsh-text-3, #8b8b8b)' } }, t('section.hint'))
|
|
565
|
+
}
|
|
566
|
+
return h(DeferredBootForm, props)
|
|
567
|
+
}
|
|
568
|
+
|
|
207
569
|
function apply(ctx) {
|
|
208
570
|
pluginCtx = ctx
|
|
209
|
-
|
|
210
|
-
ctx.
|
|
211
|
-
|
|
571
|
+
try {
|
|
572
|
+
if (ctx.locale && typeof ctx.locale.register === 'function') {
|
|
573
|
+
ctx.effect(() => ctx.locale.register('deferredBoot', 'zh', ZH), 'dsh-deferred-boot: zh dictionary')
|
|
574
|
+
ctx.effect(() => ctx.locale.register('deferredBoot', 'en', EN), 'dsh-deferred-boot: en dictionary')
|
|
575
|
+
}
|
|
576
|
+
} catch (error) {
|
|
577
|
+
// A locale failure must never prevent the configuration form from mounting.
|
|
578
|
+
console.warn('dsh-deferred-boot: locale registration failed', error)
|
|
212
579
|
}
|
|
213
580
|
ctx.effect(
|
|
214
581
|
() =>
|
|
215
|
-
ctx.
|
|
216
|
-
ctx.slots.
|
|
217
|
-
ctx.slots.register({ name: 'plugins.bundle.config', key: 'dsh-deferred-boot', locale: 'deferredBoot' }, DeferredBootForm),
|
|
218
|
-
),
|
|
582
|
+
ctx.slots.inject('plugins.item', () =>
|
|
583
|
+
ctx.slots.register({ name: 'plugins.item', id: 'dsh-deferred-boot', order: 40, label: '延迟启动' }, DeferredBootItem),
|
|
219
584
|
),
|
|
220
|
-
'dsh-deferred-boot:
|
|
585
|
+
'dsh-deferred-boot: settings item',
|
|
221
586
|
)
|
|
222
587
|
}
|
|
223
588
|
|
|
224
589
|
exports.apply = apply
|
|
225
|
-
exports.inject = ['slots', 'configForms', 'remote'
|
|
590
|
+
exports.inject = ['slots', 'configForms', 'remote']
|
|
226
591
|
return module.exports
|
|
227
592
|
},
|
|
228
593
|
})
|
package/market/index.json
CHANGED