dsh-device-fleet 0.1.0-dev.14 → 0.1.0-dev.15
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/client.js +273 -164
- package/docs/notes/dev-release.md +13 -6
- package/package.json +1 -1
- package/src/host/config-web.js +27 -4
- package/src/plugin/index.js +70 -3
package/client.js
CHANGED
|
@@ -501,59 +501,134 @@ window.__ModuleLoader__.load({
|
|
|
501
501
|
);
|
|
502
502
|
}
|
|
503
503
|
|
|
504
|
-
|
|
505
|
-
|
|
504
|
+
// ---- DSH-styled configuration page -------------------------------------
|
|
505
|
+
const CONTROL_WIDTH = 260
|
|
506
|
+
const rowStyle = {
|
|
507
|
+
display: 'flex',
|
|
508
|
+
alignItems: 'center',
|
|
509
|
+
gap: 16,
|
|
510
|
+
minHeight: 42,
|
|
511
|
+
padding: '5px 0',
|
|
512
|
+
borderBottom: '1px solid var(--dsw-alias-border-l1, #333)',
|
|
513
|
+
}
|
|
514
|
+
const rowLabelStyle = { flex: 1, fontSize: 13, color: 'var(--dsw-alias-label-primary, #e8e8e8)' }
|
|
515
|
+
const controlStyle = {
|
|
516
|
+
width: CONTROL_WIDTH,
|
|
506
517
|
boxSizing: 'border-box',
|
|
507
|
-
|
|
518
|
+
height: 30,
|
|
519
|
+
padding: '3px 8px',
|
|
508
520
|
borderRadius: 6,
|
|
509
|
-
border: '1px solid var(--dsw-alias-border, #
|
|
510
|
-
background: '
|
|
511
|
-
color: '
|
|
521
|
+
border: '1px solid var(--dsw-alias-border-l1, #333)',
|
|
522
|
+
background: 'var(--dsw-alias-bg-layer-2, #262626)',
|
|
523
|
+
color: 'var(--dsw-alias-label-primary, #e8e8e8)',
|
|
512
524
|
fontSize: 13,
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
const
|
|
525
|
+
outline: 'none',
|
|
526
|
+
}
|
|
527
|
+
const selectStyle = {
|
|
528
|
+
...controlStyle,
|
|
529
|
+
appearance: 'none',
|
|
530
|
+
WebkitAppearance: 'none',
|
|
531
|
+
paddingRight: 24,
|
|
532
|
+
cursor: 'pointer',
|
|
533
|
+
}
|
|
534
|
+
const textareaStyle = {
|
|
535
|
+
...controlStyle,
|
|
536
|
+
width: '100%',
|
|
537
|
+
height: 'auto',
|
|
538
|
+
minHeight: 64,
|
|
539
|
+
resize: 'vertical',
|
|
540
|
+
padding: '6px 8px',
|
|
541
|
+
lineHeight: 1.5,
|
|
542
|
+
fontFamily: 'ui-monospace, monospace',
|
|
543
|
+
}
|
|
544
|
+
const sectionStyle = {
|
|
545
|
+
margin: '22px 0 4px',
|
|
546
|
+
fontSize: 13,
|
|
547
|
+
fontWeight: 600,
|
|
548
|
+
color: 'var(--dsw-alias-label-primary, #e8e8e8)',
|
|
549
|
+
}
|
|
516
550
|
|
|
517
|
-
function
|
|
518
|
-
return h('
|
|
551
|
+
function Row({ label, children, wide = false }) {
|
|
552
|
+
return h('div', { style: rowStyle },
|
|
553
|
+
h('div', { style: rowLabelStyle }, label),
|
|
554
|
+
h('div', { style: wide ? { width: '100%' } : { width: CONTROL_WIDTH } }, children),
|
|
555
|
+
)
|
|
519
556
|
}
|
|
520
557
|
|
|
521
|
-
function
|
|
522
|
-
return h(
|
|
558
|
+
function SelectControl({ value, options, onChange }) {
|
|
559
|
+
return h('select', {
|
|
560
|
+
value: value ?? '',
|
|
561
|
+
onChange: (event) => onChange(event.target.value),
|
|
562
|
+
style: selectStyle,
|
|
563
|
+
},
|
|
564
|
+
...(options ?? []).map((option) => {
|
|
565
|
+
const entry = typeof option === 'string' ? { value: option, label: option } : option
|
|
566
|
+
return h('option', { key: entry.value, value: entry.value }, entry.label)
|
|
567
|
+
}),
|
|
568
|
+
)
|
|
523
569
|
}
|
|
524
570
|
|
|
525
|
-
function
|
|
526
|
-
return h(
|
|
571
|
+
function TextControl({ value, onChange, placeholder }) {
|
|
572
|
+
return h('input', {
|
|
573
|
+
type: 'text',
|
|
574
|
+
value: value ?? '',
|
|
575
|
+
placeholder: placeholder ?? '',
|
|
576
|
+
onChange: (event) => onChange(event.target.value),
|
|
577
|
+
style: controlStyle,
|
|
578
|
+
})
|
|
527
579
|
}
|
|
528
580
|
|
|
529
|
-
function
|
|
530
|
-
return h('
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
581
|
+
function NumberControl({ value, onChange }) {
|
|
582
|
+
return h('input', {
|
|
583
|
+
type: 'number',
|
|
584
|
+
value: value ?? 0,
|
|
585
|
+
onChange: (event) => onChange(Number(event.target.value || 0)),
|
|
586
|
+
style: controlStyle,
|
|
587
|
+
})
|
|
534
588
|
}
|
|
535
589
|
|
|
536
|
-
function
|
|
537
|
-
return h(
|
|
538
|
-
|
|
539
|
-
|
|
590
|
+
function ToggleControl({ value, onChange }) {
|
|
591
|
+
return h(SelectControl, {
|
|
592
|
+
value: value ? 'on' : 'off',
|
|
593
|
+
options: [{ value: 'on', label: '开启' }, { value: 'off', label: '关闭' }],
|
|
594
|
+
onChange: (next) => onChange(next === 'on'),
|
|
595
|
+
})
|
|
540
596
|
}
|
|
541
597
|
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
598
|
+
const CAPABILITY_ROWS = [
|
|
599
|
+
['capRead', '读取文件'],
|
|
600
|
+
['capList', '列目录'],
|
|
601
|
+
['capGrep', '搜索内容'],
|
|
602
|
+
['capFetch', '获取文件'],
|
|
603
|
+
['capRun', '执行命令'],
|
|
604
|
+
['capTool', '调用原始工具'],
|
|
605
|
+
['capDelegate', '委派任务'],
|
|
606
|
+
]
|
|
607
|
+
|
|
608
|
+
function configToDraft(config = {}, options = {}) {
|
|
609
|
+
const execution = config.execution ?? {}
|
|
610
|
+
const policy = config.policy ?? {}
|
|
611
|
+
const approvals = config.approvals ?? {}
|
|
612
|
+
const direct = config.direct ?? {}
|
|
613
|
+
const relay = config.relay ?? null
|
|
614
|
+
const cwd = execution.cwd ?? ''
|
|
615
|
+
const cwdInfo = options.cwd ?? {}
|
|
616
|
+
let cwdMode = 'custom'
|
|
617
|
+
if (cwd === '') cwdMode = 'default'
|
|
618
|
+
else if (cwdInfo.workspace && cwd === cwdInfo.workspace) cwdMode = 'workspace'
|
|
619
|
+
else if (cwdInfo.dshHome && cwd === cwdInfo.dshHome) cwdMode = 'dshHome'
|
|
620
|
+
else if (cwdInfo.defaultDirectory && cwd === cwdInfo.defaultDirectory) cwdMode = 'default'
|
|
548
621
|
return {
|
|
549
622
|
deviceName: config.deviceName ?? '',
|
|
550
623
|
directHost: direct.host ?? '0.0.0.0',
|
|
551
624
|
directPort: direct.port ?? 0,
|
|
552
625
|
discovery: config.discovery !== false,
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
626
|
+
relayMode: relay && (relay.host || relay.port) ? 'custom' : 'off',
|
|
627
|
+
relayHost: relay?.host ?? '',
|
|
628
|
+
relayPort: relay?.port ?? 0,
|
|
629
|
+
relayDataPort: relay?.dataPort ?? 0,
|
|
630
|
+
cwdMode,
|
|
631
|
+
customCwd: cwdMode === 'custom' ? cwd : '',
|
|
557
632
|
execPreset: execution.agentPreset ?? '',
|
|
558
633
|
execShellTool: execution.shellTool ?? '',
|
|
559
634
|
execApprovalTimeoutMs: execution.approvalTimeoutMs ?? 60000,
|
|
@@ -574,20 +649,25 @@ window.__ModuleLoader__.load({
|
|
|
574
649
|
autoApproveAsk: config.autoApproveAsk === true,
|
|
575
650
|
promptSection: config.promptSection !== false,
|
|
576
651
|
simulation: approvals.simulation === true,
|
|
577
|
-
|
|
578
|
-
};
|
|
652
|
+
}
|
|
579
653
|
}
|
|
580
654
|
|
|
581
|
-
function draftToPatch(draft) {
|
|
655
|
+
function draftToPatch(draft, options = {}) {
|
|
656
|
+
const cwdInfo = options.cwd ?? {}
|
|
657
|
+
let cwd = ''
|
|
658
|
+
if (draft.cwdMode === 'workspace') cwd = cwdInfo.workspace ?? ''
|
|
659
|
+
else if (draft.cwdMode === 'dshHome') cwd = cwdInfo.dshHome ?? ''
|
|
660
|
+
else if (draft.cwdMode === 'default') cwd = cwdInfo.defaultDirectory || cwdInfo.workspace || ''
|
|
661
|
+
else if (draft.cwdMode === 'custom') cwd = draft.customCwd ?? ''
|
|
582
662
|
return {
|
|
583
663
|
deviceName: draft.deviceName,
|
|
584
664
|
direct: { host: draft.directHost, port: draft.directPort },
|
|
585
665
|
discovery: draft.discovery,
|
|
586
|
-
relay: draft.
|
|
666
|
+
relay: draft.relayMode === 'custom'
|
|
587
667
|
? { host: draft.relayHost, port: draft.relayPort, dataPort: draft.relayDataPort }
|
|
588
668
|
: null,
|
|
589
669
|
execution: {
|
|
590
|
-
cwd
|
|
670
|
+
cwd,
|
|
591
671
|
agentPreset: draft.execPreset,
|
|
592
672
|
shellTool: draft.execShellTool,
|
|
593
673
|
approvalTimeoutMs: draft.execApprovalTimeoutMs,
|
|
@@ -615,158 +695,187 @@ window.__ModuleLoader__.load({
|
|
|
615
695
|
},
|
|
616
696
|
autoApproveAsk: draft.autoApproveAsk,
|
|
617
697
|
promptSection: draft.promptSection,
|
|
618
|
-
}
|
|
698
|
+
}
|
|
619
699
|
}
|
|
620
700
|
|
|
621
701
|
function FleetConfig() {
|
|
622
|
-
const [draft, setDraft] = useState(null)
|
|
623
|
-
const [
|
|
624
|
-
const [
|
|
625
|
-
|
|
626
|
-
const load = async () => {
|
|
627
|
-
try {
|
|
628
|
-
const response = await fetch('/device-fleet/config', { headers: { accept: 'application/json' } });
|
|
629
|
-
if (!response.ok) throw new Error('HTTP ' + response.status);
|
|
630
|
-
const data = await response.json();
|
|
631
|
-
setDraft(configToDraft(data.config));
|
|
632
|
-
} catch (problem) {
|
|
633
|
-
setStatus('加载配置失败:' + String(problem.message ?? problem));
|
|
634
|
-
}
|
|
635
|
-
};
|
|
702
|
+
const [draft, setDraft] = useState(null)
|
|
703
|
+
const [options, setOptions] = useState({})
|
|
704
|
+
const [status, setStatus] = useState('')
|
|
705
|
+
const [busy, setBusy] = useState(false)
|
|
636
706
|
|
|
637
707
|
useEffect(() => {
|
|
638
|
-
|
|
639
|
-
|
|
708
|
+
let disposed = false
|
|
709
|
+
const load = async () => {
|
|
710
|
+
try {
|
|
711
|
+
const [configResponse, optionsResponse] = await Promise.all([
|
|
712
|
+
fetch('/device-fleet/config', { headers: { accept: 'application/json' } }),
|
|
713
|
+
fetch('/device-fleet/options', { headers: { accept: 'application/json' } }),
|
|
714
|
+
])
|
|
715
|
+
const configData = await configResponse.json()
|
|
716
|
+
const optionsData = optionsResponse.ok ? await optionsResponse.json() : {}
|
|
717
|
+
if (disposed) return
|
|
718
|
+
setOptions(optionsData ?? {})
|
|
719
|
+
setDraft(configToDraft(configData.config ?? {}, optionsData ?? {}))
|
|
720
|
+
} catch (problem) {
|
|
721
|
+
if (!disposed) setStatus('加载失败')
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
void load()
|
|
725
|
+
return () => { disposed = true }
|
|
726
|
+
}, [])
|
|
640
727
|
|
|
641
|
-
if (!draft) return h('div', { style: { padding: 8,
|
|
728
|
+
if (!draft) return h('div', { style: { padding: 8, color: 'var(--dsw-alias-label-secondary, #999)' } }, status || '加载中…')
|
|
642
729
|
|
|
643
|
-
const set = (key, value) => setDraft((current) => ({ ...current, [key]: value }))
|
|
730
|
+
const set = (key, value) => setDraft((current) => ({ ...current, [key]: value }))
|
|
644
731
|
const save = async () => {
|
|
645
|
-
setBusy(true)
|
|
646
|
-
setStatus('')
|
|
732
|
+
setBusy(true)
|
|
733
|
+
setStatus('')
|
|
647
734
|
try {
|
|
648
735
|
const response = await fetch('/device-fleet/config', {
|
|
649
736
|
method: 'POST',
|
|
650
737
|
headers: { 'content-type': 'application/json' },
|
|
651
|
-
body: JSON.stringify(draftToPatch(draft)),
|
|
652
|
-
})
|
|
653
|
-
const data = await response.json()
|
|
654
|
-
if (!response.ok) throw new Error(data.error ?? 'HTTP ' + response.status)
|
|
655
|
-
setDraft(configToDraft(data.config))
|
|
656
|
-
setStatus('
|
|
738
|
+
body: JSON.stringify(draftToPatch(draft, options)),
|
|
739
|
+
})
|
|
740
|
+
const data = await response.json()
|
|
741
|
+
if (!response.ok) throw new Error(data.error ?? 'HTTP ' + response.status)
|
|
742
|
+
setDraft(configToDraft(data.config ?? {}, options))
|
|
743
|
+
setStatus('已保存')
|
|
657
744
|
} catch (problem) {
|
|
658
|
-
setStatus('保存失败:' + String(problem.message ?? problem))
|
|
745
|
+
setStatus('保存失败:' + String(problem.message ?? problem))
|
|
659
746
|
} finally {
|
|
660
|
-
setBusy(false)
|
|
747
|
+
setBusy(false)
|
|
661
748
|
}
|
|
662
|
-
}
|
|
663
|
-
|
|
664
|
-
const sectionCard = (title, children, hint) =>
|
|
665
|
-
h('div', { style: { ...cardStyle, padding: 14 } },
|
|
666
|
-
h('div', { style: sectionTitle }, title),
|
|
667
|
-
hint ? h('div', { style: { fontSize: 11, opacity: 0.55, marginBottom: 8 } }, hint) : null,
|
|
668
|
-
children,
|
|
669
|
-
);
|
|
670
|
-
|
|
671
|
-
const capabilityFields = [
|
|
672
|
-
['capRead', 'read 读取'],
|
|
673
|
-
['capList', 'list 列目录'],
|
|
674
|
-
['capGrep', 'grep 搜索'],
|
|
675
|
-
['capFetch', 'fetch 取文件'],
|
|
676
|
-
['capRun', 'run 执行命令'],
|
|
677
|
-
['capTool', 'tool 原始工具'],
|
|
678
|
-
['capDelegate', 'delegate 委派'],
|
|
679
|
-
];
|
|
680
|
-
const twoCol = (left, right) => h('div', { style: { display: 'flex', gap: 12 } }, h('div', { style: { flex: 1 } }, left), h('div', { style: { flex: 1 } }, right));
|
|
749
|
+
}
|
|
681
750
|
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
751
|
+
const cwdOptions = [
|
|
752
|
+
{ value: 'workspace', label: 'Device Fleet 工作区' },
|
|
753
|
+
{ value: 'default', label: 'DSH 默认目录' },
|
|
754
|
+
{ value: 'dshHome', label: 'DSH 主目录' },
|
|
755
|
+
{ value: 'custom', label: '自定义路径' },
|
|
756
|
+
]
|
|
757
|
+
const presetOptions = [{ value: '', label: '默认' }, ...((options.presets ?? []).map((preset) => ({ value: preset.id, label: preset.label })))]
|
|
758
|
+
const shellOptions = (options.shells ?? ['pwsh']).map((shell) => ({ value: shell, label: shell }))
|
|
759
|
+
const providerOptions = (options.subagentProviders ?? ['spawn']).map((provider) => ({ value: provider, label: provider }))
|
|
760
|
+
const hostOptions = (options.addresses ?? ['0.0.0.0']).map((address) => ({ value: address, label: address }))
|
|
761
|
+
const modeOptions = [{ value: 'auto', label: '自动' }, { value: 'ask', label: '询问' }, { value: 'deny', label: '拒绝' }]
|
|
762
|
+
|
|
763
|
+
return h('div', { style: { maxWidth: 760, padding: '4px 2px 28px', color: 'var(--dsw-alias-label-primary, #e8e8e8)' } },
|
|
764
|
+
h('div', { style: { display: 'flex', alignItems: 'center', gap: 12, marginBottom: 4 } },
|
|
765
|
+
h('div', { style: { fontSize: 15, fontWeight: 600 } }, 'Device Fleet'),
|
|
685
766
|
h('span', { style: { flex: 1 } }),
|
|
686
|
-
status ? h('span', { style: { fontSize: 12,
|
|
767
|
+
status ? h('span', { style: { fontSize: 12, color: 'var(--dsw-alias-label-secondary, #999)' } }, status) : null,
|
|
687
768
|
h('button', {
|
|
688
769
|
type: 'button',
|
|
689
770
|
disabled: busy,
|
|
690
771
|
onClick: () => void save(),
|
|
691
772
|
style: {
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
773
|
+
height: 30,
|
|
774
|
+
padding: '0 14px',
|
|
775
|
+
borderRadius: 6,
|
|
776
|
+
border: '1px solid var(--dsw-alias-brand-primary, #2f6fed)',
|
|
777
|
+
background: 'var(--dsw-alias-brand-primary, #2f6fed)',
|
|
696
778
|
color: '#fff',
|
|
779
|
+
fontSize: 13,
|
|
697
780
|
cursor: busy ? 'default' : 'pointer',
|
|
698
781
|
opacity: busy ? 0.6 : 1,
|
|
699
782
|
},
|
|
700
|
-
}, busy ? '
|
|
783
|
+
}, busy ? '保存中' : '保存'),
|
|
701
784
|
),
|
|
702
|
-
h('div', { style:
|
|
703
|
-
|
|
704
|
-
h('
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
h('
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
785
|
+
h('div', { style: sectionStyle }, '设备'),
|
|
786
|
+
h(Row, { label: '名称' },
|
|
787
|
+
h(TextControl, { value: draft.deviceName, onChange: (value) => set('deviceName', value) }),
|
|
788
|
+
),
|
|
789
|
+
h('div', { style: sectionStyle }, '网络'),
|
|
790
|
+
h(Row, { label: '直连地址' },
|
|
791
|
+
h(SelectControl, { value: draft.directHost, options: hostOptions, onChange: (value) => set('directHost', value) }),
|
|
792
|
+
),
|
|
793
|
+
h(Row, { label: '直连端口' },
|
|
794
|
+
h(NumberControl, { value: draft.directPort, onChange: (value) => set('directPort', value) }),
|
|
795
|
+
),
|
|
796
|
+
h(Row, { label: '局域网发现' },
|
|
797
|
+
h(ToggleControl, { value: draft.discovery, onChange: (value) => set('discovery', value) }),
|
|
798
|
+
),
|
|
799
|
+
h(Row, { label: 'Relay' },
|
|
800
|
+
h(SelectControl, {
|
|
801
|
+
value: draft.relayMode,
|
|
802
|
+
options: [{ value: 'off', label: '不使用' }, { value: 'custom', label: '自定义' }],
|
|
803
|
+
onChange: (value) => set('relayMode', value),
|
|
804
|
+
}),
|
|
805
|
+
),
|
|
806
|
+
draft.relayMode === 'custom' ? h(Row, { label: 'Relay 地址' },
|
|
807
|
+
h(TextControl, { value: draft.relayHost, onChange: (value) => set('relayHost', value) }),
|
|
808
|
+
) : null,
|
|
809
|
+
draft.relayMode === 'custom' ? h(Row, { label: 'Relay 端口' },
|
|
810
|
+
h(NumberControl, { value: draft.relayPort, onChange: (value) => set('relayPort', value) }),
|
|
811
|
+
) : null,
|
|
812
|
+
draft.relayMode === 'custom' ? h(Row, { label: 'Relay 数据端口' },
|
|
813
|
+
h(NumberControl, { value: draft.relayDataPort, onChange: (value) => set('relayDataPort', value) }),
|
|
814
|
+
) : null,
|
|
815
|
+
h('div', { style: sectionStyle }, '执行上下文'),
|
|
816
|
+
h(Row, { label: '工作目录' },
|
|
817
|
+
h(SelectControl, { value: draft.cwdMode, options: cwdOptions, onChange: (value) => set('cwdMode', value) }),
|
|
818
|
+
),
|
|
819
|
+
draft.cwdMode === 'custom' ? h(Row, { label: '路径' },
|
|
820
|
+
h(TextControl, { value: draft.customCwd, onChange: (value) => set('customCwd', value) }),
|
|
821
|
+
) : null,
|
|
822
|
+
h(Row, { label: 'Agent Preset' },
|
|
823
|
+
h(SelectControl, { value: draft.execPreset, options: presetOptions, onChange: (value) => set('execPreset', value) }),
|
|
824
|
+
),
|
|
825
|
+
h(Row, { label: 'Shell' },
|
|
826
|
+
h(SelectControl, { value: draft.execShellTool, options: shellOptions, onChange: (value) => set('execShellTool', value) }),
|
|
827
|
+
),
|
|
828
|
+
h(Row, { label: '委派 Provider' },
|
|
829
|
+
h(SelectControl, { value: draft.execSubagentProvider, options: providerOptions, onChange: (value) => set('execSubagentProvider', value) }),
|
|
830
|
+
),
|
|
831
|
+
h(Row, { label: '审批超时 (ms)' },
|
|
832
|
+
h(NumberControl, { value: draft.execApprovalTimeoutMs, onChange: (value) => set('execApprovalTimeoutMs', value) }),
|
|
833
|
+
),
|
|
834
|
+
h(Row, { label: '委派超时 (ms)' },
|
|
835
|
+
h(NumberControl, { value: draft.execDelegateTimeoutMs, onChange: (value) => set('execDelegateTimeoutMs', value) }),
|
|
836
|
+
),
|
|
837
|
+
h('div', { style: sectionStyle }, '远端权限'),
|
|
838
|
+
h(Row, { label: '可访问根目录', wide: true },
|
|
839
|
+
h('textarea', {
|
|
840
|
+
value: draft.rootsText,
|
|
841
|
+
onChange: (event) => set('rootsText', event.target.value),
|
|
842
|
+
rows: 3,
|
|
843
|
+
style: textareaStyle,
|
|
844
|
+
}),
|
|
845
|
+
),
|
|
846
|
+
h(Row, { label: '允许的原始工具', wide: true },
|
|
847
|
+
h('textarea', {
|
|
848
|
+
value: draft.toolsText,
|
|
849
|
+
onChange: (event) => set('toolsText', event.target.value),
|
|
850
|
+
rows: 3,
|
|
851
|
+
style: textareaStyle,
|
|
852
|
+
}),
|
|
853
|
+
),
|
|
854
|
+
...CAPABILITY_ROWS.map(([key, label]) =>
|
|
855
|
+
h(Row, { key, label },
|
|
856
|
+
h(SelectControl, { value: draft[key], options: modeOptions, onChange: (value) => set(key, value) }),
|
|
857
|
+
),
|
|
858
|
+
),
|
|
859
|
+
h(Row, { label: '授权有效期 (ms)' },
|
|
860
|
+
h(NumberControl, { value: draft.policyApprovalTtlMs, onChange: (value) => set('policyApprovalTtlMs', value) }),
|
|
861
|
+
),
|
|
862
|
+
h('div', { style: sectionStyle }, '审批与提示'),
|
|
863
|
+
h(Row, { label: '审批超时 (ms)' },
|
|
864
|
+
h(NumberControl, { value: draft.approvalsTtlMs, onChange: (value) => set('approvalsTtlMs', value) }),
|
|
865
|
+
),
|
|
866
|
+
h(Row, { label: '最大排队数' },
|
|
867
|
+
h(NumberControl, { value: draft.approvalsMaxPending, onChange: (value) => set('approvalsMaxPending', value) }),
|
|
868
|
+
),
|
|
869
|
+
h(Row, { label: '自动批准(测试)' },
|
|
870
|
+
h(ToggleControl, { value: draft.autoApproveAsk, onChange: (value) => set('autoApproveAsk', value) }),
|
|
871
|
+
),
|
|
872
|
+
h(Row, { label: 'Fleet 系统提示' },
|
|
873
|
+
h(ToggleControl, { value: draft.promptSection, onChange: (value) => set('promptSection', value) }),
|
|
874
|
+
),
|
|
875
|
+
h(Row, { label: '审批模拟(测试)' },
|
|
876
|
+
h(ToggleControl, { value: draft.simulation, onChange: (value) => set('simulation', value) }),
|
|
877
|
+
),
|
|
878
|
+
)
|
|
770
879
|
}
|
|
771
880
|
|
|
772
881
|
return {
|
|
@@ -39,19 +39,19 @@ dsh plugin --profile desktop add dsh-device-fleet@dev
|
|
|
39
39
|
- 邀请里新增 `endpoints`(创建者的局域网/热点 IPv4 + direct 端口),加入方重启后自动向这些地址回连,绕开组播发现失败;
|
|
40
40
|
- 新增 `fleet_status`(deviceId/直连端口/发现/连接/roster/hints)与 `fleet_connect(host, port)`(手动直连,成功后会从握手学到对端 deviceId);
|
|
41
41
|
- `fleet_list` 仍是本地视图;配对成功后双方 roster 会自动固定对端公钥。
|
|
42
|
-
## 0.1.0-dev.
|
|
42
|
+
## 0.1.0-dev.14(稳定化)
|
|
43
43
|
|
|
44
44
|
- 直连端口可固定(profile `direct.port`),插件重载不再换端口;
|
|
45
45
|
- 连接断开后自动重试已知地址(不再只在启动时试一次);
|
|
46
46
|
- `fleet_connect` 手动地址会持久化到 `peer-hints.json`,重启后继续回连;
|
|
47
47
|
- 新增 `fleet_delegate`(dev.5):目标设备用 `SubagentProvider` + `delivery:'caller'` 跑 agent 并返回最终结果。
|
|
48
|
-
## 0.1.0-dev.
|
|
48
|
+
## 0.1.0-dev.14(审批收件箱,逻辑层)
|
|
49
49
|
|
|
50
50
|
- 新增 `ApprovalInbox`:ask 档请求排队、超时 fail-closed、approve(once/ttl)/deny、关闭时全部失败关闭;
|
|
51
51
|
- 直调与委派的 ask 档改为等待本机收件箱结果(不再一律拒绝);
|
|
52
52
|
- 新工具:`fleet_approvals` / `fleet_approve` / `fleet_deny`;`fleet_simulate_request`(仅 `approvals.simulation: true`)用于本机无第二台设备时演练流程;
|
|
53
53
|
- 审批面板(Client UI)与通知尚未实现,当前通过工具/后续 UI 应答。
|
|
54
|
-
## 0.1.0-dev.
|
|
54
|
+
## 0.1.0-dev.14(Room 逻辑层,本机模拟)
|
|
55
55
|
|
|
56
56
|
- `RoomService`:当前会话升级为 Room Host;本地模拟参与者(真实 DSH session/agent);
|
|
57
57
|
- 寻址消息:`room_send` 只唤醒被点名成员,Host 会话写权威 `room/message` 事件;
|
|
@@ -59,15 +59,22 @@ dsh plugin --profile desktop add dsh-device-fleet@dev
|
|
|
59
59
|
- Room Board:`room_task_add/update/list`,`dependsOn` 串行交接(前置完成时唤醒下一个 owner),无依赖任务并行不受影响;
|
|
60
60
|
- 工具:`room_create / room_add_local / room_remove_member / room_send / room_messages / room_status / room_task_* / room_close`;
|
|
61
61
|
- 远程参与者传输与 Client UI 未做(下一步)。
|
|
62
|
-
## 0.1.0-dev.
|
|
62
|
+
## 0.1.0-dev.14(审批面板 UI,第一版)
|
|
63
63
|
|
|
64
64
|
- Host 新增本地 HTTP 数据面:GET `/device-fleet/approvals`、POST `/device-fleet/approvals/decide`(仅本机 web 载体,决策仍走 ApprovalInbox);
|
|
65
65
|
- Client half:`client.js` 闭包工厂,注册 `shell.overlay` 浮动「Fleet 审批」按钮 + 面板,轮询待审批列表,支持允许一次 / 允许 15 分钟 / 拒绝;
|
|
66
66
|
- package.json 增加 `dsh.client` 与 `./client` 导出;`files` 增加 client.js;
|
|
67
67
|
- Client 视觉验证需要刷新页面;当前无浏览器控制,仅验证了 bundle 语法与 Host 路由注册。
|
|
68
|
-
## 0.1.0-dev.
|
|
68
|
+
## 0.1.0-dev.14(审批入口不遮挡)
|
|
69
69
|
|
|
70
70
|
- 触发按钮从右下角浮动改为 `sidebar.footer.action`(设置旁边),不再遮挡任务列表/内容面板;
|
|
71
71
|
- 审批 inbox 改为点击后居中的模态对话框(带遮罩,Esc/点遮罩关闭);
|
|
72
72
|
- 轮询在 Client apply 中启动并由 ctx.effect 清理;待审批数量作为按钮徽标;
|
|
73
|
-
- 已实测:面板点击「允许」后 Host `fleet_approvals` 清空。
|
|
73
|
+
- 已实测:面板点击「允许」后 Host `fleet_approvals` 清空。
|
|
74
|
+
## 0.1.0-dev.14(Phase B:运行时配置 + 配置页)
|
|
75
|
+
|
|
76
|
+
- `RuntimeConfig`:profile patch 为 base,UI 修改写入 `$DSH_HOME/device-fleet/config-overrides.json`,apply 时合并并持久化;
|
|
77
|
+
- 保存即生效:policy/execution/approvals/deviceName/promptSection/autoApproveAsk 直接更新运行时对象;direct/discovery/relay 通过 `FleetNode.reconfigure()` 原地重启网络监听(不重启插件/DSH);`dataDir` 只读;
|
|
78
|
+
- Host:`GET /device-fleet/config`、`POST /device-fleet/config`(校验/清洗后应用);
|
|
79
|
+
- Client:注册 `plugins.row.config`(key `dsh-device-fleet#device-fleet`)与 `plugins.bundle.config`(key `dsh-device-fleet`),配置页含设备/网络、执行上下文、Tool Policy、审批与提示四组表单;
|
|
80
|
+
- 单测 18/18、网络 9/9。
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-device-fleet",
|
|
3
|
-
"version": "0.1.0-dev.
|
|
3
|
+
"version": "0.1.0-dev.15",
|
|
4
4
|
"description": "DeepSeek Harness device fleet: enroll devices, direct-call remote tools, shared Rooms with lazy-woken agents, and delegated subagents across devices.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/plugin/index.js",
|
package/src/host/config-web.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
// Runtime config HTTP surface (dev.
|
|
2
|
-
// patch live. The UI posts here; no plugin or DSH restart is involved.
|
|
1
|
+
// Runtime config HTTP surface (dev.15): read effective config, list UI options,
|
|
2
|
+
// and apply a patch live. The UI posts here; no plugin or DSH restart is involved.
|
|
3
3
|
const MAX_BODY_BYTES = 256 * 1024
|
|
4
4
|
|
|
5
5
|
function json(res, status, value) {
|
|
@@ -25,9 +25,31 @@ async function readJsonBody(req) {
|
|
|
25
25
|
}
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
-
|
|
28
|
+
/**
|
|
29
|
+
* @param {object} options
|
|
30
|
+
* @param {object} options.webServer ctx.webServer
|
|
31
|
+
* @param {object} options.runtime RuntimeConfig
|
|
32
|
+
* @param {() => Promise<object>|object} [options.getOptions] UI option catalog (presets, shells, paths)
|
|
33
|
+
* @param {(patch: object) => Promise<object>|object} [options.preparePatch] side effects before applying (e.g. mkdir)
|
|
34
|
+
*/
|
|
35
|
+
export function registerConfigRoutes({ webServer, runtime, getOptions = null, preparePatch = null, logger = null }) {
|
|
29
36
|
if (!webServer?.register || !runtime) return () => {}
|
|
30
37
|
const disposers = []
|
|
38
|
+
disposers.push(
|
|
39
|
+
webServer.register({
|
|
40
|
+
kind: 'exact',
|
|
41
|
+
path: '/device-fleet/options',
|
|
42
|
+
handler: async (req, res) => {
|
|
43
|
+
if (req.method !== 'GET') return json(res, 405, { error: 'method not allowed' })
|
|
44
|
+
try {
|
|
45
|
+
json(res, 200, (await getOptions?.()) ?? {})
|
|
46
|
+
} catch (error) {
|
|
47
|
+
logger?.warn?.(`[device-fleet] options failed: ${String(error?.message ?? error)}`)
|
|
48
|
+
json(res, 500, { error: 'internal error' })
|
|
49
|
+
}
|
|
50
|
+
},
|
|
51
|
+
}),
|
|
52
|
+
)
|
|
31
53
|
disposers.push(
|
|
32
54
|
webServer.register({
|
|
33
55
|
kind: 'exact',
|
|
@@ -45,7 +67,8 @@ export function registerConfigRoutes({ webServer, runtime, logger = null }) {
|
|
|
45
67
|
try {
|
|
46
68
|
const body = await readJsonBody(req)
|
|
47
69
|
const patch = body?.patch ?? body
|
|
48
|
-
const
|
|
70
|
+
const prepared = preparePatch ? await preparePatch(patch) : patch
|
|
71
|
+
const config = await runtime.update(prepared ?? patch)
|
|
49
72
|
json(res, 200, { ok: true, config, appliedAt: Date.now() })
|
|
50
73
|
} catch (error) {
|
|
51
74
|
const status = ['bad-request', 'bad-json', 'body-too-large', 'readonly'].includes(error?.code) ? 400 : 500
|
package/src/plugin/index.js
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
// the six `fleet_*` tools, and the two adoption mechanisms that are cheap to
|
|
5
5
|
// land now (a prompt section and a first-use manual + orchestration nudge).
|
|
6
6
|
// Room, Room Board, delegation and the UI land in later steps.
|
|
7
|
-
import { readFileSync } from 'node:fs'
|
|
7
|
+
import { mkdirSync, readFileSync } from 'node:fs'
|
|
8
8
|
import { networkInterfaces } from 'node:os'
|
|
9
9
|
import { join } from 'node:path'
|
|
10
10
|
import { fileURLToPath } from 'node:url'
|
|
@@ -175,10 +175,12 @@ export function apply(ctx, profileConfig) {
|
|
|
175
175
|
capabilities: CAPABILITIES,
|
|
176
176
|
})
|
|
177
177
|
|
|
178
|
-
|
|
178
|
+
const policy = new ToolPolicy(config.policy)
|
|
179
179
|
const sessionController = service('sessionController')
|
|
180
180
|
const fs = service('fs')
|
|
181
181
|
const subagents = service('subagents')
|
|
182
|
+
const agentPresets = service('agentPresets')
|
|
183
|
+
const workingDirectory = service('workingDirectory')
|
|
182
184
|
const inbox = new ApprovalInbox({
|
|
183
185
|
ttlMs: config.approvals?.ttlMs ?? 60_000,
|
|
184
186
|
maxPending: config.approvals?.maxPending ?? 50,
|
|
@@ -218,9 +220,74 @@ export function apply(ctx, profileConfig) {
|
|
|
218
220
|
// Client approval panel data plane (dev.9). Optional service: host-only
|
|
219
221
|
// deployments simply do not expose the panel routes.
|
|
220
222
|
const webServer = service('webServer')
|
|
223
|
+
const workspaceDir = join(state.dataDir, 'workspace')
|
|
224
|
+
const getOptions = async () => {
|
|
225
|
+
const addresses = new Set(['0.0.0.0', '127.0.0.1', runtime.current.direct?.host ?? ''])
|
|
226
|
+
for (const entries of Object.values(networkInterfaces())) {
|
|
227
|
+
for (const entry of entries ?? []) {
|
|
228
|
+
if (entry.family === 'IPv4' && !entry.internal) addresses.add(entry.address)
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
addresses.delete('')
|
|
232
|
+
let toolNames = []
|
|
233
|
+
try {
|
|
234
|
+
toolNames = (ctx.tools.definitions?.() ?? []).map((definition) => definition.name)
|
|
235
|
+
} catch {
|
|
236
|
+
toolNames = []
|
|
237
|
+
}
|
|
238
|
+
const shellCandidates = ['pwsh', 'bash', 'cmd', 'sh']
|
|
239
|
+
let shells = shellCandidates.filter((name) => toolNames.includes(name))
|
|
240
|
+
if (shells.length === 0) shells = [process.platform === 'win32' ? 'pwsh' : 'bash']
|
|
241
|
+
let presets = []
|
|
242
|
+
try {
|
|
243
|
+
presets = (await agentPresets?.list?.()) ?? []
|
|
244
|
+
} catch {
|
|
245
|
+
presets = []
|
|
246
|
+
}
|
|
247
|
+
return {
|
|
248
|
+
platform: process.platform,
|
|
249
|
+
addresses: [...addresses],
|
|
250
|
+
shells,
|
|
251
|
+
subagentProviders: (() => {
|
|
252
|
+
try {
|
|
253
|
+
return subagents?.list?.() ?? ['spawn']
|
|
254
|
+
} catch {
|
|
255
|
+
return ['spawn']
|
|
256
|
+
}
|
|
257
|
+
})(),
|
|
258
|
+
presets: presets.map((preset) => ({
|
|
259
|
+
id: preset.id ?? preset.name,
|
|
260
|
+
label: preset.name ?? preset.title ?? preset.id,
|
|
261
|
+
})),
|
|
262
|
+
cwd: {
|
|
263
|
+
workspace: workspaceDir,
|
|
264
|
+
dshHome: process.env.DSH_HOME ?? '',
|
|
265
|
+
defaultDirectory: workingDirectory?.defaultDirectory ?? '',
|
|
266
|
+
current: runtime.current.execution?.cwd ?? '',
|
|
267
|
+
},
|
|
268
|
+
}
|
|
269
|
+
}
|
|
221
270
|
if (webServer) {
|
|
222
271
|
ctx.effect(() => registerApprovalRoutes({ webServer, inbox, logger }))
|
|
223
|
-
ctx.effect(() =>
|
|
272
|
+
ctx.effect(() =>
|
|
273
|
+
registerConfigRoutes({
|
|
274
|
+
webServer,
|
|
275
|
+
runtime,
|
|
276
|
+
getOptions,
|
|
277
|
+
preparePatch: async (patch) => {
|
|
278
|
+
const cwd = patch?.execution?.cwd
|
|
279
|
+
if (typeof cwd === 'string' && cwd !== '') {
|
|
280
|
+
try {
|
|
281
|
+
mkdirSync(cwd, { recursive: true })
|
|
282
|
+
} catch (error) {
|
|
283
|
+
logger.warn?.(`[device-fleet] cannot create execution cwd: ${String(error?.message ?? error)}`)
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
return patch
|
|
287
|
+
},
|
|
288
|
+
logger,
|
|
289
|
+
}),
|
|
290
|
+
)
|
|
224
291
|
ctx.effect(() =>
|
|
225
292
|
registerUiRoutes({
|
|
226
293
|
webServer,
|