@mzzsfy/dsh-shell-select 0.3.1 → 0.4.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/README.md +25 -6
- package/cordis.patch.yml +181 -29
- package/package.json +19 -13
- package/src/apply-state.mjs +13 -0
- package/src/client.js +135 -67
- package/src/config.mjs +174 -14
- package/src/executor.mjs +211 -101
- package/src/guard-state.mjs +26 -0
- package/src/guard.js +14 -6
- package/src/index.js +163 -0
- package/src/render.mjs +5 -1
- package/src/takeover.mjs +167 -0
- package/src/tool.mjs +43 -20
- package/src/web-routes.mjs +93 -0
- package/test/client-card.test.mjs +171 -3
- package/test/client-render.test.mjs +169 -0
- package/test/config-entry.test.mjs +13 -8
- package/test/config.test.mjs +95 -4
- package/test/deny-config.test.mjs +8 -8
- package/test/executor-seam.test.mjs +3 -3
- package/test/executor.test.mjs +232 -121
- package/test/guard-state.test.mjs +46 -1
- package/test/preset-drift.test.mjs +49 -69
- package/test/render.test.mjs +39 -0
- package/test/takeover-shell.test.mjs +397 -0
- package/test/web-routes.test.mjs +267 -0
package/src/client.js
CHANGED
|
@@ -8,7 +8,8 @@ window.__ModuleLoader__.load({
|
|
|
8
8
|
const React = require('react')
|
|
9
9
|
const { useState, useEffect } = React
|
|
10
10
|
|
|
11
|
-
// 官方 primitives 图标/状态点(运行时模块表解析;dsh.client.inject 声明保证在场)
|
|
11
|
+
// 官方 primitives 图标/状态点(运行时模块表解析;dsh.client.inject 声明保证在场)。
|
|
12
|
+
// 0.1.7 起图标名带字重后缀(Regular/Medium),尺寸是 prop 不在名字里
|
|
12
13
|
const primitives = (() => {
|
|
13
14
|
try {
|
|
14
15
|
return require('@deepseek-ai/dsh-client-ui-primitives')
|
|
@@ -16,31 +17,31 @@ window.__ModuleLoader__.load({
|
|
|
16
17
|
return null
|
|
17
18
|
}
|
|
18
19
|
})()
|
|
19
|
-
const createElementOf = (
|
|
20
|
-
const
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
20
|
+
const createElementOf = (names, fallback) => {
|
|
21
|
+
for (const name of names) {
|
|
22
|
+
const Component = primitives !== null ? primitives[name] : undefined
|
|
23
|
+
if (typeof Component === 'function' || typeof Component === 'object') {
|
|
24
|
+
return (props) => React.createElement(Component, props ?? null)
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
return fallback
|
|
24
28
|
}
|
|
25
29
|
const DOT_SVG = { done: '#2e9e5b', error: '#d4553f', warning: '#d9a13b', ongoing: '#7a7f8a' }
|
|
26
30
|
// 降级自绘:模块表缺 primitives 时保持可见性(形态近似官方 StateDot/图标)
|
|
27
31
|
const TOOLVIEW_ICONS = {
|
|
28
|
-
StateDot: createElementOf('StateDot', ({ state }) => h('span', {
|
|
32
|
+
StateDot: createElementOf(['StateDot'], ({ state }) => h('span', {
|
|
29
33
|
style: {
|
|
30
34
|
width: 8, height: 8, borderRadius: '50%', background: DOT_SVG[state] ?? DOT_SVG.ongoing,
|
|
31
35
|
display: 'inline-block', flex: 'none',
|
|
32
36
|
},
|
|
33
37
|
})),
|
|
34
|
-
IconApi: createElementOf('IconApiOutline14', () => h('span', {
|
|
38
|
+
IconApi: createElementOf(['IconApiOutlineRegular', 'IconApiOutlineMedium', 'IconApiOutline14'], () => h('span', {
|
|
35
39
|
style: {
|
|
36
40
|
width: 12, height: 12, borderRadius: 3, border: '1.5px solid currentColor',
|
|
37
41
|
opacity: .7, display: 'inline-block', flex: 'none',
|
|
38
42
|
},
|
|
39
43
|
})),
|
|
40
|
-
|
|
41
|
-
style: { fontSize: (size ?? 14) - 3, lineHeight: 1, userSelect: 'none' },
|
|
42
|
-
}, '▾')),
|
|
43
|
-
IconInspect: createElementOf('IconInspectOutline12', () => h('span', {
|
|
44
|
+
IconInspect: createElementOf(['IconInspectOutlineRegular', 'IconInspectOutlineMedium', 'IconInspectOutline12'], () => h('span', {
|
|
44
45
|
style: { fontSize: 10, lineHeight: 1, opacity: .8 },
|
|
45
46
|
}, 'ⓘ')),
|
|
46
47
|
}
|
|
@@ -66,7 +67,7 @@ window.__ModuleLoader__.load({
|
|
|
66
67
|
}
|
|
67
68
|
|
|
68
69
|
// 工具卡客户端名册缓存:GET /config 单飞拉取,shell 徽章按 id→用户命名解析。
|
|
69
|
-
//
|
|
70
|
+
// 失败复位单飞(宿主启动窗口路由未就绪的 404 在下次卡渲染自然重试)
|
|
70
71
|
let clientCatalog = null
|
|
71
72
|
let clientCatalogPromise = null
|
|
72
73
|
// LOGIC-BEGIN ensureClientCatalog
|
|
@@ -74,9 +75,11 @@ window.__ModuleLoader__.load({
|
|
|
74
75
|
if (clientCatalogPromise === null) {
|
|
75
76
|
clientCatalogPromise = api(API.config).then((section) => {
|
|
76
77
|
const byId = {}
|
|
77
|
-
for (const entry of section.resolved?.shells ?? []) byId[entry.id] = entry.
|
|
78
|
+
for (const entry of section.resolved?.shells ?? []) byId[entry.id] = entry.id
|
|
78
79
|
clientCatalog = { default: section.resolved?.default, byId }
|
|
79
|
-
}).catch(() => {
|
|
80
|
+
}).catch(() => {
|
|
81
|
+
clientCatalogPromise = null
|
|
82
|
+
})
|
|
80
83
|
}
|
|
81
84
|
return clientCatalogPromise
|
|
82
85
|
}
|
|
@@ -128,8 +131,13 @@ window.__ModuleLoader__.load({
|
|
|
128
131
|
'.sls-tv__row { display:flex; align-items:center; gap:7px; padding:2px 0; cursor:default; }',
|
|
129
132
|
'.sls-tv__row--exp { cursor:pointer; user-select:none; }',
|
|
130
133
|
'.sls-tv__lead { display:flex; align-items:center; gap:4px; color:var(--dsw-alias-label-tertiary, inherit); }',
|
|
131
|
-
|
|
132
|
-
|
|
134
|
+
// 官方 ToolRow 同构:行首仅 icon,无展开箭头(可展开性由指针与整行点击承载);
|
|
135
|
+
// 错误/进行中状态点绝对定位覆盖 icon(红点掩盖 icon)
|
|
136
|
+
'.sls-tv__leadStack { position:relative; width:16px; height:16px; display:inline-flex; align-items:center; justify-content:center; flex:none; }',
|
|
137
|
+
'.sls-tv__iconIdle { display:inline-flex; }',
|
|
138
|
+
'.sls-tv__stateCover { position:absolute; inset:0; margin:auto; display:inline-flex; align-items:center; justify-content:center; background:transparent; }',
|
|
139
|
+
'.sls-tv__stateCover::before { content:""; position:absolute; inset:-2px; border-radius:50%; background:var(--dsw-alias-bg-primary, #fff); }',
|
|
140
|
+
'.sls-tv__stateCover > span { position:relative; }',
|
|
133
141
|
'.sls-tv__sr { position:absolute; width:1px; height:1px; overflow:hidden; clip:rect(0 0 0 0); }',
|
|
134
142
|
'.sls-tv__title { font-weight:400; }',
|
|
135
143
|
'.sls-tv__sep { width:2px; height:2px; border-radius:1px; background:var(--dsw-alias-label-caption, currentColor); opacity:.8; flex:none; }',
|
|
@@ -137,10 +145,15 @@ window.__ModuleLoader__.load({
|
|
|
137
145
|
'.sls-tv__sum--err { color:var(--dsw-alias-state-error-primary, #d4553f); }',
|
|
138
146
|
'.sls-tv__body { margin:6px 0 4px; border:1px solid rgba(128,128,128,.28); border-radius:8px; overflow:hidden; }',
|
|
139
147
|
'.sls-tv__head { display:flex; align-items:center; gap:8px; padding:6px 10px; border-bottom:1px solid rgba(128,128,128,.18); background:rgba(128,128,128,.05); }',
|
|
140
|
-
|
|
141
|
-
'.sls-
|
|
142
|
-
'.sls-
|
|
148
|
+
// 头部单行片段禁折行:cwd 行宽不足时收缩出省略号,徽章不收缩不折行
|
|
149
|
+
'.sls-tv__cwd { font-family:var(--sls-mono, monospace); font-size:12px; opacity:.7; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }',
|
|
150
|
+
'.sls-tv__badge { font-size:11px; padding:0 7px; border-radius:999px; border:1px solid rgba(128,128,128,.35); opacity:.85; flex:none; white-space:nowrap; }',
|
|
151
|
+
'.sls-tv__pill { font-size:12px; color:#d4553f; flex:none; white-space:nowrap; }',
|
|
143
152
|
'.sls-tv__pill--bg { color:var(--dsw-alias-label-tertiary, inherit); }',
|
|
153
|
+
'.sls-tv__duration { font-family:var(--sls-mono, monospace); font-size:12px; color:var(--dsw-alias-label-tertiary, inherit); flex:none; white-space:nowrap; font-variant-numeric:tabular-nums; }',
|
|
154
|
+
// 运行中脉冲文字:零信息窗口(参数未到)的活体反馈
|
|
155
|
+
'.sls-tv__pulse { font-size:12px; color:var(--dsw-alias-label-tertiary, inherit); flex:none; white-space:nowrap; animation:sls-pulse 1.5s ease-in-out infinite; }',
|
|
156
|
+
'@keyframes sls-pulse { 0%,100% { opacity:.35; } 50% { opacity:1; } }',
|
|
144
157
|
'.sls-tv__sp { flex:1; }',
|
|
145
158
|
'.sls-tv__copy { display:flex; align-items:center; gap:2px; flex:none; }',
|
|
146
159
|
'.sls-tv__copyBtn { padding:2px 8px; border:1px solid rgba(128,128,128,.35); border-radius:6px; background:transparent; color:inherit; cursor:pointer; font-size:12px; }',
|
|
@@ -186,7 +199,6 @@ window.__ModuleLoader__.load({
|
|
|
186
199
|
return {
|
|
187
200
|
shells: entries.map((entry) => ({
|
|
188
201
|
id: entry.id.trim(),
|
|
189
|
-
name: entry.name.trim(),
|
|
190
202
|
kind: entry.kind,
|
|
191
203
|
path: entry.path.trim(),
|
|
192
204
|
args: entry.argsText.split(/\s+/).filter((item) => item.length > 0),
|
|
@@ -243,7 +255,6 @@ window.__ModuleLoader__.load({
|
|
|
243
255
|
function toEntries(section) {
|
|
244
256
|
return section.shells.map((entry) => ({
|
|
245
257
|
id: entry.id,
|
|
246
|
-
name: entry.name,
|
|
247
258
|
kind: entry.kind,
|
|
248
259
|
path: entry.path,
|
|
249
260
|
argsText: (entry.args ?? []).join(' '),
|
|
@@ -290,7 +301,7 @@ window.__ModuleLoader__.load({
|
|
|
290
301
|
|
|
291
302
|
const addEntry = () => {
|
|
292
303
|
setDirty(true)
|
|
293
|
-
setEntries([...entries, { id: '',
|
|
304
|
+
setEntries([...entries, { id: '', kind: 'bash', path: '', argsText: '', login: false, distro: '', envText: '', available: undefined, resolved: undefined }])
|
|
294
305
|
}
|
|
295
306
|
|
|
296
307
|
const removeEntry = (index) => {
|
|
@@ -422,16 +433,9 @@ window.__ModuleLoader__.load({
|
|
|
422
433
|
h('input', {
|
|
423
434
|
className: 'sls-input',
|
|
424
435
|
value: entry.id,
|
|
425
|
-
placeholder: 'pwsh / git-bash / …(模型看到的 shell
|
|
436
|
+
placeholder: 'pwsh / git-bash / …(模型看到的 shell 参数值,亦作显示名)',
|
|
426
437
|
onChange: (event) => patchEntry(index, { id: event.target.value }),
|
|
427
438
|
}),
|
|
428
|
-
h('span', { className: 'sls-grid__label' }, '名称'),
|
|
429
|
-
h('input', {
|
|
430
|
-
className: 'sls-input',
|
|
431
|
-
value: entry.name,
|
|
432
|
-
placeholder: '显示名,如 Git Bash',
|
|
433
|
-
onChange: (event) => patchEntry(index, { name: event.target.value }),
|
|
434
|
-
}),
|
|
435
439
|
h('span', { className: 'sls-grid__label' }, '形态'),
|
|
436
440
|
h('div', { className: 'sls-row' },
|
|
437
441
|
h('select', {
|
|
@@ -606,11 +610,25 @@ window.__ModuleLoader__.load({
|
|
|
606
610
|
}
|
|
607
611
|
// LOGIC-END hasSpillNotice
|
|
608
612
|
|
|
613
|
+
// 执行事实标记(host renderResult 落,格式见 render.mjs):卡片徽章第一优先级,
|
|
614
|
+
// 配置事后变更/跨宿主查看不溯往改写历史调用
|
|
615
|
+
// LOGIC-BEGIN parseShellMark
|
|
616
|
+
function parseShellMark(text) {
|
|
617
|
+
return /\[shell: ([^\]\n]+)\]/.exec(text)?.[1]
|
|
618
|
+
}
|
|
619
|
+
// LOGIC-END parseShellMark
|
|
620
|
+
|
|
621
|
+
// LOGIC-BEGIN stripShellMark
|
|
622
|
+
function stripShellMark(text) {
|
|
623
|
+
return text.split('\n').filter((line) => !/^\[shell: [^\]]+\]$/.test(line)).join('\n')
|
|
624
|
+
}
|
|
625
|
+
// LOGIC-END stripShellMark
|
|
626
|
+
|
|
609
627
|
// block → 卡片模型(官方 terminalCardModel 同构,数据源 argsRaw + content 文本)。
|
|
610
628
|
// generic = 后台 ack / isError / 溢出预览 / persistent 形(无 description,
|
|
611
629
|
// 官方 shellCall persistent→generic 同构),交回退行;terminal = 全量卡。
|
|
612
630
|
// LOGIC-BEGIN shellCardModel
|
|
613
|
-
function shellCardModel(block, sessionCwd) {
|
|
631
|
+
function shellCardModel(block, sessionCwd, toolKey) {
|
|
614
632
|
const settled = 'kind' in block
|
|
615
633
|
const call = settled ? block.call : block
|
|
616
634
|
let args = null
|
|
@@ -622,40 +640,80 @@ window.__ModuleLoader__.load({
|
|
|
622
640
|
const description = typeof args?.description === 'string' && args.description.trim() !== '' ? args.description : undefined
|
|
623
641
|
const cwdFull = displayCwd(typeof args?.workdir === 'string' ? args.workdir : undefined, sessionCwd)
|
|
624
642
|
const cwdDir = cwdFull !== undefined ? lastSegment(cwdFull) : undefined
|
|
625
|
-
//
|
|
626
|
-
|
|
627
|
-
|
|
643
|
+
// 官方钉死客户端工具(bash/pwsh)徽章即工具名:其输出无标记、参数无 shell
|
|
644
|
+
// 字段,落默认读数会误标(官方 pwsh 调用显示当前默认客户端的事故根因)
|
|
645
|
+
const PINNED_CLIENT = { pwsh: 'pwsh', bash: 'bash' }
|
|
646
|
+
// shell 工具徽章优先级:结果标记(执行事实)→ 显式参数 → default 读数(旧块兜底,
|
|
647
|
+
// 配置即当前解析事实);读数推导仅兜无标记历史块,不覆盖事实
|
|
648
|
+
const requested = typeof args?.shell === 'string' ? args.shell : undefined
|
|
649
|
+
const shellNameOf = (mark) => PINNED_CLIENT[toolKey] ?? mark ?? clientDisplayName(requested)
|
|
650
|
+
if (command === '') return { kind: 'generic', command: '', summary: undefined, output: null, running: !settled, alert: undefined }
|
|
628
651
|
|
|
629
652
|
if (!settled) {
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
653
|
+
const startedAt = typeof block.time === 'number' ? block.time : undefined
|
|
654
|
+
// 运行中一律全量卡:阻塞时展开即可见命令与时长,不落无命令的 generic 回退行;
|
|
655
|
+
// 无结果文本,徽章按读数推导,落定后由标记校正
|
|
656
|
+
return { kind: 'terminal', status: 'running', command, description, cwdDir, cwdFull, shellName: shellNameOf(undefined), output: undefined, exitCode: undefined, signal: undefined, code: undefined, startedAt }
|
|
633
657
|
}
|
|
634
658
|
|
|
635
659
|
const contentText = (block.content ?? []).map((part) => (part.type === 'text' ? part.text : '')).filter((text) => text !== '').join('\n')
|
|
660
|
+
const shellName = shellNameOf(parseShellMark(contentText))
|
|
661
|
+
const outputText = stripShellMark(contentText)
|
|
636
662
|
// 后台 ack:独立卡呈现命令全文与任务号徽标,ack 原文作输出(不再落 generic 简版行)
|
|
637
663
|
if (args.run_in_background === true) {
|
|
638
664
|
const jobId = /started background job (\S+)/.exec(contentText)?.[1]
|
|
639
|
-
return { kind: 'background', status: 'background', command, description, cwdDir, cwdFull, shellName, output:
|
|
665
|
+
return { kind: 'background', status: 'background', command, description, cwdDir, cwdFull, shellName, output: outputText, jobId }
|
|
640
666
|
}
|
|
641
|
-
// 空结果落 generic:官方 singleResultText 无文本即回通用卡,避免空输出伪 done
|
|
642
|
-
|
|
643
|
-
|
|
667
|
+
// 空结果落 generic:官方 singleResultText 无文本即回通用卡,避免空输出伪 done 终端卡;
|
|
668
|
+
// isError/error 块带 alert(错误红点语义),其余(空输出/无描述/截断)中性无状态点
|
|
669
|
+
if (outputText === '' || block.isError === true || block.error !== undefined || description === undefined || hasSpillNotice(contentText)) {
|
|
670
|
+
const alert = block.isError === true || block.error !== undefined ? 'error' : undefined
|
|
671
|
+
return { kind: 'generic', command, summary: description, output: outputText, running: false, alert }
|
|
644
672
|
}
|
|
645
|
-
const tail = parseExitTail(
|
|
673
|
+
const tail = parseExitTail(outputText)
|
|
646
674
|
const status = tail.signal !== undefined ? 'signaled' : tail.exitCode !== 0 ? 'failed' : 'done'
|
|
647
675
|
return { kind: 'terminal', status, command, description, cwdDir, cwdFull, shellName, output: tail.output, exitCode: tail.exitCode, signal: tail.signal, code: undefined }
|
|
648
676
|
}
|
|
649
677
|
// LOGIC-END shellCardModel
|
|
650
678
|
|
|
651
|
-
// 官方
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
679
|
+
// 官方 ToolRow leading 同构:行首仅 icon;错误/进行中状态红点叠 icon 之上
|
|
680
|
+
// (掩盖 icon,官方状态点语义)
|
|
681
|
+
function leadingStack({ status, icons }) {
|
|
682
|
+
const failed = status === 'failed' || status === 'signaled' || status === 'generic-error'
|
|
683
|
+
const dot = failed ? icons.StateDot({ state: 'error' }) : status === 'generic-warn' ? icons.StateDot({ state: 'warning' }) : status === 'running' ? icons.StateDot({ state: 'ongoing' }) : null
|
|
684
|
+
if (dot !== null) {
|
|
685
|
+
return h('span', { className: 'sls-tv__leadStack' },
|
|
686
|
+
h('span', { className: 'sls-tv__iconIdle' }, icons.IconApi({ size: 14 })),
|
|
687
|
+
h('span', { className: 'sls-tv__stateCover' }, dot),
|
|
688
|
+
)
|
|
689
|
+
}
|
|
656
690
|
return icons.IconApi({ size: 14 })
|
|
657
691
|
}
|
|
658
692
|
|
|
693
|
+
// 时长格式:秒 → m:ss;超一小时 → h:mm:ss
|
|
694
|
+
// LOGIC-BEGIN formatDuration
|
|
695
|
+
function formatDuration(elapsedMs) {
|
|
696
|
+
const totalSeconds = Math.max(0, Math.floor(elapsedMs / 1000))
|
|
697
|
+
const seconds = totalSeconds % 60
|
|
698
|
+
const minutes = Math.floor(totalSeconds / 60) % 60
|
|
699
|
+
const hours = Math.floor(totalSeconds / 3600)
|
|
700
|
+
const two = (value) => (value < 10 ? '0' + value : String(value))
|
|
701
|
+
return hours > 0 ? `${hours}:${two(minutes)}:${two(seconds)}` : `${minutes}:${two(seconds)}`
|
|
702
|
+
}
|
|
703
|
+
// LOGIC-END formatDuration
|
|
704
|
+
|
|
705
|
+
// 运行中实时时长:每秒 tick;秒级粒度对"阻塞了多久"足够
|
|
706
|
+
function RunningDuration({ startedAt, en }) {
|
|
707
|
+
const [, setTick] = useState(0)
|
|
708
|
+
useEffect(() => {
|
|
709
|
+
const timer = setInterval(() => setTick((value) => value + 1), 1000)
|
|
710
|
+
return () => clearInterval(timer)
|
|
711
|
+
}, [])
|
|
712
|
+
const elapsed = startedAt !== undefined ? Date.now() - startedAt : undefined
|
|
713
|
+
const text = elapsed !== undefined && elapsed >= 0 ? formatDuration(elapsed) : en ? 'running' : '运行中'
|
|
714
|
+
return h('span', { className: 'sls-tv__duration', title: en ? 'Elapsed' : '已运行' }, text)
|
|
715
|
+
}
|
|
716
|
+
|
|
659
717
|
function statusTextOf(status, en) {
|
|
660
718
|
switch (status) {
|
|
661
719
|
case 'running': return en ? 'Running' : '运行中'
|
|
@@ -668,9 +726,9 @@ window.__ModuleLoader__.load({
|
|
|
668
726
|
function headMetaOf(model, en) {
|
|
669
727
|
switch (model.status) {
|
|
670
728
|
case 'running': return { dot: 'ongoing', label: en ? 'Running' : '运行中', pill: undefined }
|
|
671
|
-
// ack 块静态不反映 job
|
|
729
|
+
// ack 块静态不反映 job 生命周期,状态点缺省(后台任务由 bg pill 标识)
|
|
672
730
|
case 'background': return {
|
|
673
|
-
dot:
|
|
731
|
+
dot: undefined,
|
|
674
732
|
label: en ? 'Background' : '后台',
|
|
675
733
|
pill: model.jobId !== undefined ? (en ? `bg ${model.jobId}` : `后台 ${model.jobId}`) : undefined,
|
|
676
734
|
pillTone: 'bg',
|
|
@@ -700,13 +758,17 @@ window.__ModuleLoader__.load({
|
|
|
700
758
|
}, done ? (detectEnglish() ? 'Copied' : '已复制') : label)
|
|
701
759
|
}
|
|
702
760
|
|
|
703
|
-
// 非终端意图回退行(后台 ack / isError / 截断):摘要 +
|
|
761
|
+
// 非终端意图回退行(后台 ack / isError / 截断 / 参数未到):摘要 + 可展开原文。
|
|
762
|
+
// 状态语义对齐官方:运行中灰 spinner,settled 错误红点,其余中性 icon——
|
|
763
|
+
// 黄点(warning)仅真实告警,禁止挪用作"进行中"(用户把黄点当卡死)。
|
|
764
|
+
// 运行中且零信息(参数未到)显示脉冲「运行中…」,给用户活着的感觉。
|
|
704
765
|
function GenericShellRow({ model, inspect, en }) {
|
|
705
766
|
const [open, setOpen] = useState(false)
|
|
767
|
+
const status = model.running === true ? 'running' : model.alert === 'error' ? 'generic-error' : undefined
|
|
706
768
|
const summary = model.summary !== undefined && model.summary !== ''
|
|
707
769
|
? model.summary.split('\n')[0]
|
|
708
770
|
: (model.output !== null && model.output !== '' ? model.output.split('\n')[0] : '')
|
|
709
|
-
const expandable = model.output !== null && model.output !== ''
|
|
771
|
+
const expandable = (model.output !== null && model.output !== '') || model.command !== ''
|
|
710
772
|
return h('div', { className: 'sls-tv' },
|
|
711
773
|
h('div', {
|
|
712
774
|
className: 'sls-tv__row' + (expandable ? ' sls-tv__row--exp' : ''),
|
|
@@ -722,14 +784,14 @@ window.__ModuleLoader__.load({
|
|
|
722
784
|
} : undefined,
|
|
723
785
|
},
|
|
724
786
|
h('span', { className: 'sls-tv__lead' },
|
|
725
|
-
|
|
726
|
-
expandable ? h('span', { className: 'sls-tv__chev', 'data-open': open ? '1' : '0', style: { display: 'inline-flex', transform: open ? 'rotate(-90deg)' : 'none' } }, TOOLVIEW_ICONS.IconChevron({ size: 14 })) : null,
|
|
787
|
+
leadingStack({ status, icons: TOOLVIEW_ICONS }),
|
|
727
788
|
),
|
|
728
789
|
h('span', { className: 'sls-tv__title' }, 'Shell'),
|
|
729
790
|
summary !== '' ? h('span', { className: 'sls-tv__sep', 'aria-hidden': true }) : null,
|
|
730
|
-
summary !== '' ? h('span', { className: 'sls-tv__sum' }, summary) : null,
|
|
791
|
+
summary !== '' ? h('span', { className: 'sls-tv__sum' + (status === 'generic-error' ? ' sls-tv__sum--err' : '') }, summary) : null,
|
|
792
|
+
status === 'running' ? h('span', { className: 'sls-tv__pulse' }, en ? 'running…' : '运行中…') : null,
|
|
731
793
|
),
|
|
732
|
-
open && expandable ? h('pre', { className: 'sls-tv__out', style: { border: '1px solid rgba(128,128,128,.28)', borderRadius: 8 } }, model.output) : null,
|
|
794
|
+
open && expandable ? h('pre', { className: 'sls-tv__out', style: { border: '1px solid rgba(128,128,128,.28)', borderRadius: 8 } }, model.command + (model.output !== null && model.output !== '' ? `\n${model.output}` : '')) : null,
|
|
733
795
|
inspect !== undefined ? h('button', { className: 'sls-tv__inspect', onClick: inspect }, TOOLVIEW_ICONS.IconInspect({}), en ? 'Inspect' : '检查') : null,
|
|
734
796
|
)
|
|
735
797
|
}
|
|
@@ -749,16 +811,20 @@ window.__ModuleLoader__.load({
|
|
|
749
811
|
const meta = headMetaOf(model, en)
|
|
750
812
|
const copyCommandLabel = en ? 'Copy command' : '复制命令'
|
|
751
813
|
const copyOutputLabel = en ? 'Copy output' : '复制输出'
|
|
814
|
+
// 运行中头部 pill = 实时时长(组件,hook 内每秒自刷新)
|
|
815
|
+
const pill = model.status === 'running'
|
|
816
|
+
? h(RunningDuration, { startedAt: model.startedAt, en })
|
|
817
|
+
: meta.pill
|
|
752
818
|
return h('div', { className: 'sls-tv__body', 'data-status': model.status },
|
|
753
819
|
h('div', { className: 'sls-tv__head' },
|
|
754
820
|
h('span', { style: { display: 'inline-flex', alignItems: 'center', gap: 4 } },
|
|
755
|
-
TOOLVIEW_ICONS.StateDot({ state: meta.dot }),
|
|
821
|
+
meta.dot !== undefined ? TOOLVIEW_ICONS.StateDot({ state: meta.dot }) : null,
|
|
756
822
|
h('span', { className: 'sls-tv__sr' }, meta.label),
|
|
757
823
|
),
|
|
758
824
|
model.cwdDir !== undefined ? h('span', { className: 'sls-tv__cwd', title: 'pwd: ' + (model.cwdFull ?? model.cwdDir) }, model.cwdDir) : null,
|
|
759
825
|
model.shellName !== undefined ? h('span', { className: 'sls-tv__badge', title: en ? 'Shell client' : 'Shell 客户端' }, model.shellName) : null,
|
|
760
826
|
h('span', { className: 'sls-tv__sp' }),
|
|
761
|
-
|
|
827
|
+
pill !== undefined ? h('span', { className: 'sls-tv__pill' + (meta.pillTone === 'bg' ? ' sls-tv__pill--bg' : '') }, pill) : null,
|
|
762
828
|
h('span', { className: 'sls-tv__copy' },
|
|
763
829
|
h(CopyButton, {
|
|
764
830
|
label: copyCommandLabel,
|
|
@@ -781,8 +847,12 @@ window.__ModuleLoader__.load({
|
|
|
781
847
|
}
|
|
782
848
|
|
|
783
849
|
function ShellToolRow(props) {
|
|
784
|
-
const { block, cwd, inspect } = props
|
|
850
|
+
const { block, cwd, inspect, toolKey } = props
|
|
785
851
|
const en = detectEnglish()
|
|
852
|
+
const model = shellCardModel(block, cwd, toolKey)
|
|
853
|
+
// hooks 恒序:必须在任何提前 return 之前完整执行(真实 React 的 hooks
|
|
854
|
+
// 链表按调用序对位,generic 行与 terminal 行的提前 return 分叉会让
|
|
855
|
+
// 后续 hook 错位,行组件树被整棵卸载——卡片全消失事故根因)
|
|
786
856
|
const [open, setOpen] = useState(false)
|
|
787
857
|
const [, setCatalogReady] = useState(false)
|
|
788
858
|
useEffect(() => {
|
|
@@ -791,7 +861,6 @@ window.__ModuleLoader__.load({
|
|
|
791
861
|
return () => { disposed = true }
|
|
792
862
|
}, [])
|
|
793
863
|
if (model.kind === 'generic') return h(GenericShellRow, { model, inspect, en })
|
|
794
|
-
const expandable = true
|
|
795
864
|
const srStatus = statusTextOf(model.status, en)
|
|
796
865
|
const summary = model.description !== undefined ? model.description.split('\n')[0] : (en ? '(no description)' : '(无描述)')
|
|
797
866
|
const failed = model.status === 'failed' || model.status === 'signaled'
|
|
@@ -801,7 +870,6 @@ window.__ModuleLoader__.load({
|
|
|
801
870
|
role: 'button',
|
|
802
871
|
tabIndex: 0,
|
|
803
872
|
'aria-expanded': open,
|
|
804
|
-
'data-open': open ? '1' : '0',
|
|
805
873
|
onClick: () => setOpen((value) => !value),
|
|
806
874
|
onKeyDown: (event) => {
|
|
807
875
|
if (event.key === 'Enter' || event.key === ' ') {
|
|
@@ -811,13 +879,13 @@ window.__ModuleLoader__.load({
|
|
|
811
879
|
},
|
|
812
880
|
},
|
|
813
881
|
h('span', { className: 'sls-tv__lead' },
|
|
814
|
-
|
|
815
|
-
h('span', { className: 'sls-tv__chev', style: { display: 'inline-flex', transform: open ? 'rotate(-90deg)' : 'none' } }, TOOLVIEW_ICONS.IconChevron({ size: 14 })),
|
|
882
|
+
leadingStack({ status: model.status, icons: TOOLVIEW_ICONS }),
|
|
816
883
|
),
|
|
817
884
|
srStatus !== null ? h('span', { className: 'sls-tv__sr' }, srStatus) : null,
|
|
818
885
|
h('span', { className: 'sls-tv__title' }, 'Shell'),
|
|
819
886
|
h('span', { className: 'sls-tv__sep', 'aria-hidden': true }),
|
|
820
887
|
h('span', { className: 'sls-tv__sum' + (failed ? ' sls-tv__sum--err' : '') }, summary),
|
|
888
|
+
model.status === 'running' ? h(RunningDuration, { startedAt: model.startedAt, en }) : null,
|
|
821
889
|
model.status === 'background' && model.jobId !== undefined
|
|
822
890
|
? h('span', { className: 'sls-tv__badge', title: en ? 'Background job' : '后台任务' }, en ? `bg ${model.jobId}` : `后台 ${model.jobId}`)
|
|
823
891
|
: null,
|
|
@@ -844,15 +912,15 @@ window.__ModuleLoader__.load({
|
|
|
844
912
|
))
|
|
845
913
|
// 替换 shell 族工具行渲染:keyed hit 优先于 GenericToolCard 兜底;
|
|
846
914
|
// priority -1 阴影官方同 key 注册(低值先渲染)。pwsh/bash 为官方工具
|
|
847
|
-
// 名(死态窗口 guard 代挂官方 tool-pwsh
|
|
848
|
-
//
|
|
849
|
-
//
|
|
915
|
+
// 名(死态窗口 guard 代挂官方 tool-pwsh 时,其调用行同样获得增强卡);
|
|
916
|
+
// key 以 toolKey 注入卡片,官方钉死客户端工具的徽章按工具名钉死,
|
|
917
|
+
// 不落默认客户端读数。
|
|
850
918
|
const TOOLVIEW_KEYS = ['shell', 'pwsh', 'bash']
|
|
851
919
|
for (const toolKey of TOOLVIEW_KEYS) {
|
|
852
920
|
ctx.slots.inject('tool.call.toolview', () =>
|
|
853
921
|
ctx.slots.register(
|
|
854
922
|
{ name: 'tool.call.toolview', key: toolKey, priority: -1 },
|
|
855
|
-
(props) => React.createElement(ShellToolRow, props),
|
|
923
|
+
(props) => React.createElement(ShellToolRow, { ...props, toolKey }),
|
|
856
924
|
))
|
|
857
925
|
}
|
|
858
926
|
},
|
package/src/config.mjs
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
// cmd /d /s /c 与 wsl --exec 为本包定义的保守形态。
|
|
4
4
|
|
|
5
5
|
import z from '@deepseek-ai/schemastery'
|
|
6
|
+
import { MAX_TIMER_DELAY_MS } from '@deepseek-ai/dsh-timeout'
|
|
6
7
|
|
|
7
8
|
// 支持的 shell 形态
|
|
8
9
|
export const KINDS = ['pwsh', 'bash', 'cmd', 'wsl']
|
|
@@ -29,7 +30,6 @@ const EXECUTOR_DEFAULTS = {
|
|
|
29
30
|
|
|
30
31
|
const shellEntry = z.object({
|
|
31
32
|
id: z.string().required(),
|
|
32
|
-
name: z.string().required(),
|
|
33
33
|
kind: z.union(KINDS).required(),
|
|
34
34
|
path: z.string().default(RESOLVED_AUTO),
|
|
35
35
|
args: z.array(z.string()).default([]),
|
|
@@ -39,27 +39,187 @@ const shellEntry = z.object({
|
|
|
39
39
|
})
|
|
40
40
|
|
|
41
41
|
export const Config = z.object({
|
|
42
|
+
// 0.1.7 设置页写路径(settings.write)只接受 volatile 字段(免重挂载的活
|
|
43
|
+
// 字段,官方 dsh-bash-local Config 逐字段 volatile 同构);缺声明即
|
|
44
|
+
// "has no volatile fields" 保存失败
|
|
42
45
|
shells: z.array(shellEntry).default([
|
|
43
|
-
{ id: 'pwsh',
|
|
44
|
-
{ id: 'git-bash',
|
|
45
|
-
{ id: 'cmd',
|
|
46
|
-
]),
|
|
47
|
-
default: z.string().default('pwsh'),
|
|
46
|
+
{ id: 'pwsh', kind: 'pwsh', path: RESOLVED_AUTO, args: [] },
|
|
47
|
+
{ id: 'git-bash', kind: 'bash', path: RESOLVED_AUTO, args: [] },
|
|
48
|
+
{ id: 'cmd', kind: 'cmd', path: RESOLVED_AUTO, args: [] },
|
|
49
|
+
]).volatile(),
|
|
50
|
+
default: z.string().default('pwsh').volatile(),
|
|
48
51
|
// 命令黑名单(整文本正则,大小写不敏感):命中即拒,deny 绝对无豁免;
|
|
49
52
|
// 精细放行用正则前瞻在模式内表达。空数组 = 不拦截。
|
|
50
53
|
// 护栏防误触,非安全边界(真边界是沙箱与访问模式)
|
|
51
|
-
deny: z.array(z.string()).default([]),
|
|
52
|
-
cwd: z.string(),
|
|
53
|
-
timeoutMs: z.number().default(EXECUTOR_DEFAULTS.timeoutMs),
|
|
54
|
-
maxTimeoutMs: z.number().default(EXECUTOR_DEFAULTS.maxTimeoutMs),
|
|
55
|
-
maxOutputBytes: z.number().default(EXECUTOR_DEFAULTS.maxOutputBytes),
|
|
56
|
-
maxSpillBytes: z.number().default(EXECUTOR_DEFAULTS.maxSpillBytes),
|
|
57
|
-
graceMs: z.number().default(EXECUTOR_DEFAULTS.graceMs),
|
|
54
|
+
deny: z.array(z.string()).default([]).volatile(),
|
|
55
|
+
cwd: z.string().volatile(),
|
|
56
|
+
timeoutMs: z.number().default(EXECUTOR_DEFAULTS.timeoutMs).volatile(),
|
|
57
|
+
maxTimeoutMs: z.number().default(EXECUTOR_DEFAULTS.maxTimeoutMs).volatile(),
|
|
58
|
+
maxOutputBytes: z.number().default(EXECUTOR_DEFAULTS.maxOutputBytes).volatile(),
|
|
59
|
+
maxSpillBytes: z.number().default(EXECUTOR_DEFAULTS.maxSpillBytes).volatile(),
|
|
60
|
+
graceMs: z.number().default(EXECUTOR_DEFAULTS.graceMs).volatile(),
|
|
58
61
|
})
|
|
59
62
|
|
|
63
|
+
// volatile 字段解箱:schema 声明 .volatile() 后(0.1.7 设置页写路径只接受
|
|
64
|
+
// volatile 字段,缺声明即 "has no volatile fields" 拒写),Config 产物字段为
|
|
65
|
+
// boxed ref(读值须 .get(),官方 dsh-bash-local 同构)。统一在此解箱,消费者
|
|
66
|
+
// (executor/web-routes/测试)一律拿普通对象。
|
|
67
|
+
// structuredClone 深拷贝双向:输入侧(sanitize 产物)切断与调用方对象的耦合——
|
|
68
|
+
// volatile ref .get() 产物是 cosmokit snapshot 深冻结对象,schemastery resolve
|
|
69
|
+
// 原地改写入参,冻结 env 直接炸挂载;输出侧:schemastery resolve 会原地改写入参
|
|
70
|
+
// (填默认/键归一)并把产出对象置为不可扩展,二轮 resolve(快照回灌,web 写路径
|
|
71
|
+
// current+patch 合并常态)对不可扩展对象赋值即炸;clone 切断产物与 schema 的原地
|
|
72
|
+
// 耦合,保证 resolveConfig 幂等且对输入不可变。
|
|
73
|
+
export function unwrapConfig(value) {
|
|
74
|
+
return Object.fromEntries(Object.entries(value).map(([key, field]) => {
|
|
75
|
+
const raw = typeof field?.get === 'function' ? field.get() : field
|
|
76
|
+
return [key, raw !== null && typeof raw === 'object' ? structuredClone(raw) : raw]
|
|
77
|
+
}))
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** schema 应用 + 解箱 + 输入深拷:全部消费者经此入口,不直接触 Config 产物。 */
|
|
81
|
+
export function resolveConfig(entry) {
|
|
82
|
+
const sanitized = sanitizeConfigEntry(entry ?? {})
|
|
83
|
+
return unwrapConfig(Config(structuredClone(sanitized)))
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** 执行面严格解析:输入键被类型对账丢弃时抛。执行面(挂载/refresh)输入是
|
|
87
|
+
* 宿主刚 resolve 的产物,形态非法 = 宿主契约变化或调用方未解箱,必须炸在
|
|
88
|
+
* 挂载点(接管序列有回滚告警)而非静默回落出厂默认——静默曾让行 config
|
|
89
|
+
* 长期失效(0.2.0-rc.2 真机实证)。持久化读回路径(落盘写坏降级、行不死
|
|
90
|
+
* 可保存修复)仍用 resolveConfig 的宽容语义。 */
|
|
91
|
+
export function resolveConfigStrict(entry) {
|
|
92
|
+
const input = entry ?? {}
|
|
93
|
+
const sanitized = sanitizeConfigEntry(input)
|
|
94
|
+
const dropped = Object.keys(FIELD_TYPES)
|
|
95
|
+
.filter((key) => input[key] !== undefined && !(key in sanitized))
|
|
96
|
+
if (dropped.length > 0) {
|
|
97
|
+
throw new Error(`shell-select: config 字段形态非法(执行面输入须先解箱为普通对象): ${dropped.join(', ')}`)
|
|
98
|
+
}
|
|
99
|
+
return unwrapConfig(Config(structuredClone(sanitized)))
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** 用户声明值挂载前 coercing:类型清洗(sanitize)+ 语义兜底,任意用户输入
|
|
103
|
+
* 产出可服务配置——插件本体不因配置死(行活、工具活,坏字段告警后降默认)。
|
|
104
|
+
* 语义兜底:shells 清洗后为空(含条目缺 id/kind 或 kind 非法被剔除)补出厂
|
|
105
|
+
* 默认;default 不在 shells id 集归 shells[0];预算数字非正数或超 graceMs
|
|
106
|
+
* 上限降 schema 默认;deny 剔除非字符串与不可编译条目。 */
|
|
107
|
+
export function coerceServiceable(entry) {
|
|
108
|
+
const warnings = []
|
|
109
|
+
const cleaned = sanitizeConfigEntry(entry ?? {})
|
|
110
|
+
const rawShells = Array.isArray(cleaned.shells) ? cleaned.shells : []
|
|
111
|
+
const usable = rawShells.filter((item) => item !== null && typeof item === 'object'
|
|
112
|
+
&& typeof item.id === 'string' && item.id !== ''
|
|
113
|
+
&& typeof item.kind === 'string' && KINDS.includes(item.kind))
|
|
114
|
+
if (usable.length !== rawShells.length) warnings.push(`shells 剔除 ${rawShells.length - usable.length} 个缺 id/kind 或 kind 非法的条目`)
|
|
115
|
+
const shells = usable.length > 0 ? usable : defaultConfig().shells
|
|
116
|
+
if (usable.length === 0) warnings.push('shells 为空或全部非法,回落出厂默认客户端清单')
|
|
117
|
+
let defaultId = cleaned.default
|
|
118
|
+
if (typeof defaultId !== 'string' || !shells.some((item) => item.id === defaultId)) {
|
|
119
|
+
warnings.push(`default ${JSON.stringify(defaultId)} 不在 shells 清单,归 shells[0]`)
|
|
120
|
+
defaultId = shells[0].id
|
|
121
|
+
}
|
|
122
|
+
const rawDeny = Array.isArray(cleaned.deny) ? cleaned.deny : undefined
|
|
123
|
+
const deny = rawDeny?.filter((pattern) => {
|
|
124
|
+
if (typeof pattern !== 'string' || pattern === '') return false
|
|
125
|
+
try {
|
|
126
|
+
new RegExp(pattern, 'i')
|
|
127
|
+
return true
|
|
128
|
+
} catch {
|
|
129
|
+
return false
|
|
130
|
+
}
|
|
131
|
+
})
|
|
132
|
+
if (rawDeny !== undefined && deny.length !== rawDeny.length) warnings.push(`deny 剔除 ${rawDeny.length - deny.length} 个非字符串或不可编译正则的条目`)
|
|
133
|
+
const budgets = {}
|
|
134
|
+
for (const key of ['timeoutMs', 'maxTimeoutMs', 'maxOutputBytes', 'maxSpillBytes', 'graceMs']) {
|
|
135
|
+
const value = cleaned[key]
|
|
136
|
+
if (value === undefined) continue
|
|
137
|
+
const overCeiling = key === 'graceMs' && value > MAX_TIMER_DELAY_MS
|
|
138
|
+
if (!Number.isFinite(value) || value <= 0 || overCeiling) {
|
|
139
|
+
warnings.push(`${key} 必须为正有限数${key === 'graceMs' ? `且不大于 ${MAX_TIMER_DELAY_MS}` : ''},降 schema 默认`)
|
|
140
|
+
// 显式置空覆盖 cleaned 原值,交 schema 默认兜底
|
|
141
|
+
budgets[key] = undefined
|
|
142
|
+
continue
|
|
143
|
+
}
|
|
144
|
+
budgets[key] = value
|
|
145
|
+
}
|
|
146
|
+
return {
|
|
147
|
+
config: { ...cleaned, shells, default: defaultId, deny, ...budgets },
|
|
148
|
+
warnings,
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// 落盘行 config 的类型防御:设置写路径(原生页/mutate)可能把 volatile 字段
|
|
153
|
+
// 以非预期形态(ref 序列化产物 {})持久化,冷启动 Config 直接抛 → 行死 →
|
|
154
|
+
// settings describe 无此节 → 设置页保存 "no longer configurable" 死锁,
|
|
155
|
+
// 只能手工改 patch 解锁。类型不符的键降级为 schema 默认:行活、设置页活、
|
|
156
|
+
// 数据可经保存覆盖修复(官方 schema default 兜底同语义)。
|
|
157
|
+
// 字段类型契约:与 schema 声明一一对应,清洗按此对账
|
|
158
|
+
const FIELD_TYPES = {
|
|
159
|
+
shells: 'array',
|
|
160
|
+
deny: 'array',
|
|
161
|
+
default: 'string',
|
|
162
|
+
cwd: 'string',
|
|
163
|
+
timeoutMs: 'number',
|
|
164
|
+
maxTimeoutMs: 'number',
|
|
165
|
+
maxOutputBytes: 'number',
|
|
166
|
+
maxSpillBytes: 'number',
|
|
167
|
+
graceMs: 'number',
|
|
168
|
+
}
|
|
169
|
+
export function sanitizeConfigEntry(entry) {
|
|
170
|
+
if (entry === null || typeof entry !== 'object' || Array.isArray(entry)) return {}
|
|
171
|
+
const out = {}
|
|
172
|
+
for (const [key, kind] of Object.entries(FIELD_TYPES)) {
|
|
173
|
+
const value = entry[key]
|
|
174
|
+
if (value === undefined) continue
|
|
175
|
+
const valid = kind === 'array' ? Array.isArray(value) : typeof value === kind
|
|
176
|
+
if (valid) out[key] = value
|
|
177
|
+
}
|
|
178
|
+
return out
|
|
179
|
+
}
|
|
180
|
+
|
|
60
181
|
/** 出厂默认配置(供文档与测试比对;schema 内 default 为同构静态值)。 */
|
|
61
182
|
export function defaultConfig() {
|
|
62
|
-
return
|
|
183
|
+
return resolveConfig({})
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// yaml 无引号标量会把 `\` 字面落盘,任何一层再转义都让路径翻倍(C:\\ 实测):
|
|
187
|
+
// 入口统一归一,保证进 schema 的 path 就是干净值。
|
|
188
|
+
// 仅 Windows 宿主生效:posix 路径以 / 分隔,归一会把合法路径毁成反斜杠字面(CI linux 实测)
|
|
189
|
+
export function normalizeWin32Path(path) {
|
|
190
|
+
if (process.platform !== 'win32') return path
|
|
191
|
+
if (typeof path !== 'string' || path.length === 0) return path
|
|
192
|
+
return path.replace(/\\{2,}/g, '\\').replace(/\//g, '\\')
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// 更新负载深归一:shells[].path 与任意层字符串值只处理 path 键,避免误伤 args 模板
|
|
196
|
+
export function normalizeConfigPaths(patch) {
|
|
197
|
+
if (typeof patch !== 'object' || patch === null || !Array.isArray(patch.shells)) return patch
|
|
198
|
+
return {
|
|
199
|
+
...patch,
|
|
200
|
+
shells: patch.shells.map((entry) => (typeof entry?.path === 'string' ? { ...entry, path: normalizeWin32Path(entry.path) } : entry)),
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function assertPositiveFinite(name, value) {
|
|
205
|
+
if (!Number.isFinite(value) || value <= 0) throw new Error(`shell-select: ${name} must be a positive finite number`)
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/** 拒绝无法运行的已解析配置节(schema 之外的正数/时限/清单约束,官方 assertServiceable 同构)。 */
|
|
209
|
+
export function assertServiceableConfig(config) {
|
|
210
|
+
assertPositiveFinite('timeoutMs', config.timeoutMs)
|
|
211
|
+
assertPositiveFinite('maxTimeoutMs', config.maxTimeoutMs)
|
|
212
|
+
assertPositiveFinite('maxOutputBytes', config.maxOutputBytes)
|
|
213
|
+
assertPositiveFinite('maxSpillBytes', config.maxSpillBytes)
|
|
214
|
+
assertPositiveFinite('graceMs', config.graceMs)
|
|
215
|
+
if (config.graceMs > MAX_TIMER_DELAY_MS) throw new Error(`shell-select: graceMs must be no greater than ${MAX_TIMER_DELAY_MS}`)
|
|
216
|
+
if (!Array.isArray(config.shells) || config.shells.length === 0) throw new Error('shell-select: shells must not be empty')
|
|
217
|
+
const ids = new Set()
|
|
218
|
+
for (const entry of config.shells) {
|
|
219
|
+
if (ids.has(entry.id)) throw new Error(`shell-select: duplicate shell id "${entry.id}"`)
|
|
220
|
+
ids.add(entry.id)
|
|
221
|
+
}
|
|
222
|
+
if (!ids.has(config.default)) throw new Error(`shell-select: default "${config.default}" is not a configured shell id`)
|
|
63
223
|
}
|
|
64
224
|
|
|
65
225
|
/**
|