dsh-ssh-workbench 1.6.4 → 1.7.0
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 +49 -1
- package/CONTRIBUTING.md +5 -4
- package/README.md +41 -22
- package/SECURITY.md +2 -0
- package/docs/architecture.md +58 -36
- package/docs/execution-model.md +83 -0
- package/docs/images/chat-at-resource.png +0 -0
- package/docs/images/plugin.png +0 -0
- package/docs/images/remote-workbench.png +0 -0
- package/docs/images/workbench.png +0 -0
- package/docs/quickstart.md +20 -8
- package/lib/client.js +171 -157
- package/lib/core/activity.js +5 -0
- package/lib/core/broker.js +10 -7
- package/lib/core/dialect.js +1 -1
- package/lib/core/execution-stats.js +88 -0
- package/lib/core/gate-pwsh.js +1 -1
- package/lib/core/guard.js +86 -34
- package/lib/core/live-executions.js +101 -0
- package/lib/core/managed-context.js +276 -0
- package/lib/core/{targets.js → mentions.js} +49 -83
- package/lib/core/posix-shell.js +89 -0
- package/lib/core/prefs.js +1 -1
- package/lib/core/remote-exec-tool.js +394 -0
- package/lib/core/remote-file-tools.js +559 -0
- package/lib/core/{remote-bash-tool.js → remote-shell-tool.js} +58 -69
- package/lib/core/secrets.js +0 -1
- package/lib/core/server-ref.js +61 -0
- package/lib/core/shell-adapters.js +1 -1
- package/lib/core/win32-adapter.js +1 -1
- package/lib/index.js +113 -154
- package/lib/routes.js +28 -2
- package/package.json +7 -6
- package/lib/core/remote-bash.js +0 -337
- package/lib/core/server-reference.js +0 -299
package/lib/client.js
CHANGED
|
@@ -23,6 +23,10 @@ window.__ModuleLoader__.load({
|
|
|
23
23
|
// don't expose the primitives table.
|
|
24
24
|
let primitives = null
|
|
25
25
|
try { primitives = require('@deepseek-ai/dsh-client-ui-primitives') } catch { primitives = null }
|
|
26
|
+
// Portal the dock panel to document.body so the composer's stacking context
|
|
27
|
+
// cannot clip or cover it. Optional: fall back to inline when unavailable.
|
|
28
|
+
let ReactDOM = null
|
|
29
|
+
try { ReactDOM = require('react-dom') } catch { ReactDOM = null }
|
|
26
30
|
|
|
27
31
|
// The merged SSH guide card remembers which page (server manager or remote
|
|
28
32
|
// files) its main button should open; the chevron always offers both.
|
|
@@ -247,7 +251,6 @@ window.__ModuleLoader__.load({
|
|
|
247
251
|
'overview.kind.server-update': '更新资源',
|
|
248
252
|
'overview.kind.server-remove': '删除资源',
|
|
249
253
|
'common.loading': '加载中…',
|
|
250
|
-
'tool.target': '切换远程目标',
|
|
251
254
|
'tool.running': '执行中…',
|
|
252
255
|
'tool.noOutput': '(无输出)',
|
|
253
256
|
'tool.input': '参数',
|
|
@@ -265,8 +268,15 @@ window.__ModuleLoader__.load({
|
|
|
265
268
|
'files.chunkIncomplete': '文件分块读取不完整',
|
|
266
269
|
'settings.tabServers': '纳管资源',
|
|
267
270
|
'settings.tabActivity': '操作日志',
|
|
268
|
-
'target.
|
|
269
|
-
'target.
|
|
271
|
+
'target.popTitle': '远程执行',
|
|
272
|
+
'target.popCount': '{n} 次',
|
|
273
|
+
'target.rowCount': '{n} 次',
|
|
274
|
+
'target.execChip': '远程执行 · {n} 次',
|
|
275
|
+
'target.execTitle': '本会话远程执行次数',
|
|
276
|
+
'target.runningOne': '{name} · 执行中',
|
|
277
|
+
'target.runningMany': '{n} 个目标 · 执行中',
|
|
278
|
+
'target.runningValue': '{n} 个目标执行中',
|
|
279
|
+
'target.runningLabel': '执行中',
|
|
270
280
|
'common.close': '关闭',
|
|
271
281
|
}
|
|
272
282
|
|
|
@@ -465,7 +475,6 @@ window.__ModuleLoader__.load({
|
|
|
465
475
|
'overview.kind.server-update': 'Resource updated',
|
|
466
476
|
'overview.kind.server-remove': 'Resource removed',
|
|
467
477
|
'common.loading': 'Loading…',
|
|
468
|
-
'tool.target': 'Switch remote target',
|
|
469
478
|
'tool.running': 'Running…',
|
|
470
479
|
'tool.noOutput': '(no output)',
|
|
471
480
|
'tool.input': 'Input',
|
|
@@ -483,8 +492,15 @@ window.__ModuleLoader__.load({
|
|
|
483
492
|
'files.chunkIncomplete': 'File chunk read was incomplete',
|
|
484
493
|
'settings.tabServers': 'Managed resources',
|
|
485
494
|
'settings.tabActivity': 'Activity log',
|
|
486
|
-
'target.
|
|
487
|
-
'target.
|
|
495
|
+
'target.popTitle': 'Remote executions',
|
|
496
|
+
'target.popCount': '{n} runs',
|
|
497
|
+
'target.rowCount': '{n} runs',
|
|
498
|
+
'target.execChip': 'Remote · {n} runs',
|
|
499
|
+
'target.execTitle': 'Remote executions in this session',
|
|
500
|
+
'target.runningOne': '{name} · running',
|
|
501
|
+
'target.runningMany': '{n} targets · running',
|
|
502
|
+
'target.runningValue': '{n} targets running',
|
|
503
|
+
'target.runningLabel': 'running',
|
|
488
504
|
'common.close': 'Close',
|
|
489
505
|
'files.transferUploading': 'Uploading {name}',
|
|
490
506
|
'files.transferDownloading': 'Downloading {name}',
|
|
@@ -590,13 +606,19 @@ window.__ModuleLoader__.load({
|
|
|
590
606
|
}
|
|
591
607
|
|
|
592
608
|
/**
|
|
593
|
-
*
|
|
594
|
-
*
|
|
595
|
-
*
|
|
609
|
+
* Composer dock for the Session's remote resources. The popover deliberately
|
|
610
|
+
* mirrors the shipped usage counter: one header value, a hairline divider,
|
|
611
|
+
* and a label/value grid. It is read-only: targets are shown with their
|
|
612
|
+
* plugin-local execution count for this Session.
|
|
596
613
|
*/
|
|
597
614
|
function TargetDock(props) {
|
|
598
615
|
const sessionId = props && props.sessionId
|
|
599
|
-
const [
|
|
616
|
+
const [stats, setStats] = useState(null)
|
|
617
|
+
const [running, setRunning] = useState(null)
|
|
618
|
+
const [open, setOpen] = useState(false)
|
|
619
|
+
const [rect, setRect] = useState(null)
|
|
620
|
+
const rootRef = useRef(null)
|
|
621
|
+
const panelRef = useRef(null)
|
|
600
622
|
useEffect(() => {
|
|
601
623
|
ensurePanelStyle()
|
|
602
624
|
}, [])
|
|
@@ -606,56 +628,147 @@ window.__ModuleLoader__.load({
|
|
|
606
628
|
const load = async () => {
|
|
607
629
|
try {
|
|
608
630
|
const body = await api('/target?session=' + encodeURIComponent(sessionId))
|
|
609
|
-
if (alive)
|
|
631
|
+
if (!alive) return
|
|
632
|
+
setStats(body && body.stats && typeof body.stats === 'object' ? body.stats : null)
|
|
633
|
+
setRunning(body && body.running && typeof body.running === 'object' ? body.running : null)
|
|
610
634
|
} catch {
|
|
611
|
-
if (alive)
|
|
635
|
+
if (!alive) return
|
|
636
|
+
setStats(null)
|
|
637
|
+
setRunning(null)
|
|
612
638
|
}
|
|
613
639
|
}
|
|
614
640
|
void load()
|
|
615
|
-
|
|
641
|
+
// The dock is a read-only projection of the execution ledger. Tool
|
|
642
|
+
// events already trigger a refresh on the next turn; this low-frequency
|
|
643
|
+
// fallback keeps a long turn from showing stale counts.
|
|
644
|
+
const timer = setInterval(load, 1500)
|
|
616
645
|
return () => {
|
|
617
646
|
alive = false
|
|
618
647
|
clearInterval(timer)
|
|
619
648
|
}
|
|
620
649
|
}, [sessionId])
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
return
|
|
650
|
+
useEffect(() => {
|
|
651
|
+
if (!open || typeof document === 'undefined') return undefined
|
|
652
|
+
const onPointerDown = (event) => {
|
|
653
|
+
const node = event.target
|
|
654
|
+
if (node && typeof node.closest === 'function' && node.closest('[data-sdh-target-dock]')) return
|
|
655
|
+
if (rootRef.current && node && rootRef.current.contains(node)) return
|
|
656
|
+
if (panelRef.current && node && panelRef.current.contains(node)) return
|
|
657
|
+
setOpen(false)
|
|
627
658
|
}
|
|
628
|
-
|
|
629
|
-
|
|
659
|
+
const onKeyDown = (event) => {
|
|
660
|
+
if (event.key === 'Escape') {
|
|
661
|
+
event.stopPropagation()
|
|
662
|
+
setOpen(false)
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
document.addEventListener('mousedown', onPointerDown, true)
|
|
666
|
+
document.addEventListener('keydown', onKeyDown, true)
|
|
667
|
+
return () => {
|
|
668
|
+
document.removeEventListener('mousedown', onPointerDown, true)
|
|
669
|
+
document.removeEventListener('keydown', onKeyDown, true)
|
|
670
|
+
}
|
|
671
|
+
}, [open])
|
|
672
|
+
|
|
673
|
+
const tr = (key, fallback) => {
|
|
630
674
|
try {
|
|
631
|
-
|
|
675
|
+
const value = String(translate(key))
|
|
676
|
+
return value === key ? fallback : value
|
|
632
677
|
} catch {
|
|
633
|
-
return
|
|
678
|
+
return fallback
|
|
634
679
|
}
|
|
635
|
-
}
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
680
|
+
}
|
|
681
|
+
const statsServers = stats && Array.isArray(stats.servers) ? stats.servers : []
|
|
682
|
+
const rows = statsServers.filter((bucket) => bucket && Number.isFinite(bucket.count) && bucket.count > 0)
|
|
683
|
+
const total = stats && Number.isFinite(stats.count) ? stats.count : 0
|
|
684
|
+
const runningServers = running && Array.isArray(running.servers) ? running.servers.filter((entry) => entry && entry.name) : []
|
|
685
|
+
const runningNames = [...new Set(runningServers.map((entry) => entry.name))]
|
|
686
|
+
// A session with no remote execution has no dock. While a call is in
|
|
687
|
+
// flight we still render: the live target is the point of the indicator.
|
|
688
|
+
if (rows.length === 0 && total === 0 && runningNames.length === 0) return null
|
|
689
|
+
const togglePanel = (event) => {
|
|
690
|
+
if (open) {
|
|
691
|
+
setOpen(false)
|
|
692
|
+
return
|
|
693
|
+
}
|
|
694
|
+
const bounds = event.currentTarget.getBoundingClientRect()
|
|
695
|
+
const viewportWidth = typeof window !== 'undefined' && Number.isFinite(window.innerWidth) ? window.innerWidth : 320
|
|
696
|
+
const viewportHeight = typeof window !== 'undefined' && Number.isFinite(window.innerHeight) ? window.innerHeight : 600
|
|
697
|
+
const right = Math.max(8, viewportWidth - bounds.right)
|
|
698
|
+
const bottom = Math.max(8, viewportHeight - bounds.top + 6)
|
|
699
|
+
setRect({ right, bottom })
|
|
700
|
+
setOpen(true)
|
|
701
|
+
}
|
|
702
|
+
const runningOne = runningNames.length === 1
|
|
703
|
+
const label = runningOne
|
|
704
|
+
? tr('target.runningOne', '{name} · running').replace('{name}', runningNames[0])
|
|
705
|
+
: runningNames.length > 1
|
|
706
|
+
? tr('target.runningMany', '{n} targets · running').replace('{n}', String(runningNames.length))
|
|
707
|
+
: tr('target.execChip', 'Remote · {n}').replace('{n}', String(total))
|
|
708
|
+
const title = runningNames.length > 0
|
|
709
|
+
? runningOne
|
|
710
|
+
? tr('target.runningOne', '{name} · running').replace('{name}', runningNames[0])
|
|
711
|
+
: tr('target.runningMany', '{n} targets · running').replace('{n}', String(runningNames.length))
|
|
712
|
+
: tr('target.execTitle', 'Remote executions in this session')
|
|
713
|
+
const icon = h('svg', {
|
|
714
|
+
viewBox: '0 0 24 24', fill: 'none', stroke: 'currentColor',
|
|
715
|
+
strokeWidth: 1.7, strokeLinecap: 'round', strokeLinejoin: 'round', 'aria-hidden': true,
|
|
716
|
+
},
|
|
717
|
+
h('rect', { x: 3, y: 4, width: 18, height: 7, rx: 1.5 }),
|
|
718
|
+
h('rect', { x: 3, y: 13, width: 18, height: 7, rx: 1.5 }),
|
|
719
|
+
h('path', { d: 'M6.5 7.5h.01M6.5 16.5h.01M10 7.5h7M10 16.5h7' }))
|
|
720
|
+
const button = h('button', {
|
|
721
|
+
type: 'button',
|
|
722
|
+
className: 'dsh-ssh-workbench-target-pill',
|
|
723
|
+
title,
|
|
724
|
+
'aria-label': title,
|
|
725
|
+
'aria-haspopup': 'dialog',
|
|
726
|
+
'aria-expanded': open ? 'true' : 'false',
|
|
727
|
+
onClick: togglePanel,
|
|
728
|
+
},
|
|
729
|
+
icon,
|
|
730
|
+
h('span', { className: 'dsh-ssh-workbench-target-label' }, label))
|
|
731
|
+
|
|
732
|
+
const rowByName = new Map(rows.map((bucket) => [bucket.name, bucket]))
|
|
733
|
+
const detailNames = [
|
|
734
|
+
...runningNames,
|
|
735
|
+
...rows.map((bucket) => bucket.name).filter((name) => !runningNames.includes(name)),
|
|
736
|
+
]
|
|
737
|
+
const details = []
|
|
738
|
+
for (const name of detailNames) {
|
|
739
|
+
const bucket = rowByName.get(name)
|
|
740
|
+
const active = runningNames.includes(name)
|
|
741
|
+
const parts = []
|
|
742
|
+
if (active) parts.push(tr('target.runningLabel', 'running'))
|
|
743
|
+
if (bucket) parts.push(tr('target.rowCount', '{n} runs').replace('{n}', String(bucket.count)))
|
|
744
|
+
details.push(h('dt', { key: name + ':label' }, name))
|
|
745
|
+
details.push(h('dd', {
|
|
746
|
+
key: name + ':value',
|
|
747
|
+
...(active ? { className: 'dsh-ssh-workbench-target-row-running' } : {}),
|
|
748
|
+
}, parts.join(' · ')))
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
const panel = open && rect
|
|
752
|
+
? h('div', {
|
|
753
|
+
ref: panelRef,
|
|
754
|
+
className: 'dsh-ssh-workbench-target-popover',
|
|
755
|
+
role: 'dialog',
|
|
756
|
+
'aria-label': tr('target.popTitle', 'Remote executions'),
|
|
757
|
+
'data-sdh-target-dock': '',
|
|
758
|
+
style: { right: rect.right + 'px', bottom: rect.bottom + 'px' },
|
|
654
759
|
},
|
|
655
|
-
h('
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
760
|
+
h('div', { className: 'dsh-ssh-workbench-target-title' },
|
|
761
|
+
h('span', { className: 'dsh-ssh-workbench-target-title-label' }, icon, tr('target.popTitle', 'Remote executions')),
|
|
762
|
+
h('span', { className: 'dsh-ssh-workbench-target-title-value' }, runningNames.length > 0
|
|
763
|
+
? tr('target.runningValue', '{n} targets running').replace('{n}', String(runningNames.length))
|
|
764
|
+
: tr('target.popCount', '{n} runs').replace('{n}', String(total)))),
|
|
765
|
+
h('div', { className: 'dsh-ssh-workbench-target-rule', 'aria-hidden': true }),
|
|
766
|
+
h('dl', { className: 'dsh-ssh-workbench-target-details' }, ...details))
|
|
767
|
+
: null
|
|
768
|
+
const mountedPanel = panel && ReactDOM && typeof document !== 'undefined'
|
|
769
|
+
? ReactDOM.createPortal(panel, document.body)
|
|
770
|
+
: panel
|
|
771
|
+
return h('span', { ref: rootRef, className: 'dsh-ssh-workbench-target-anchor', 'data-sdh-target-dock': '' }, button, mountedPanel)
|
|
659
772
|
}
|
|
660
773
|
|
|
661
774
|
/**
|
|
@@ -867,7 +980,18 @@ window.__ModuleLoader__.load({
|
|
|
867
980
|
'@media (hover:hover){.dsh-ssh-workbench-target-pill:hover{background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-secondary)}}',
|
|
868
981
|
'.dsh-ssh-workbench-target-pill:focus-visible{outline:var(--dsw-focus-ring-width) solid var(--dsw-focus-ring-color,var(--dsw-alias-state-business-primary));outline-offset:-1px}',
|
|
869
982
|
'.dsh-ssh-workbench-target-label{min-width:0;overflow:hidden;text-overflow:ellipsis}',
|
|
870
|
-
'.dsh-ssh-workbench-
|
|
983
|
+
'.dsh-ssh-workbench-target-popover{position:fixed;z-index:1100;box-sizing:border-box;width:max-content;min-width:min(300px,calc(100vw - 24px),var(--dsh-anchored-max-width,100vw));max-width:min(440px,calc(100vw - 24px),var(--dsh-anchored-max-width,100vw));max-height:var(--dsh-anchored-max-height,70vh);overflow:hidden auto;padding:16px;border:0;border-radius:var(--dsw-radius-lg);background:var(--dsw-specific-menu,var(--dsw-menu-surface-fill));backdrop-filter:var(--dsw-menu-backdrop-filter);--dsw-elevation-stroke-color:var(--dsw-alias-border-l1);box-shadow:var(--dsw-elevation-prominent);color:var(--dsw-alias-label-secondary);cursor:default;font-size:12px;line-height:18px;--dsh-scrollbar-thumb:var(--dsw-alias-scrollbar-bg-l2);--dsh-scrollbar-thumb-hover:var(--dsw-alias-scrollbar-hover-l2)}',
|
|
984
|
+
'.dsh-ssh-workbench-target-title{display:flex;justify-content:space-between;gap:16px;margin-bottom:8px;color:var(--dsw-alias-label-primary);font-weight:500}',
|
|
985
|
+
'.dsh-ssh-workbench-target-title-label{display:inline-flex;align-items:center;gap:6px;min-width:0}',
|
|
986
|
+
'.dsh-ssh-workbench-target-title-label svg{flex:none;width:14px;height:14px}',
|
|
987
|
+
'.dsh-ssh-workbench-target-title-value{font-variant-numeric:tabular-nums}',
|
|
988
|
+
'.dsh-ssh-workbench-target-rule{border-top:.5px solid var(--dsw-alias-border-l2);margin-bottom:10px}',
|
|
989
|
+
'.dsh-ssh-workbench-target-details{display:grid;grid-template-columns:minmax(76px,auto) minmax(0,1fr);gap:6px 16px;margin:0;color:var(--dsw-alias-label-tertiary)}',
|
|
990
|
+
'.dsh-ssh-workbench-target-details dt,.dsh-ssh-workbench-target-details dd{min-width:0;margin:0}',
|
|
991
|
+
'.dsh-ssh-workbench-target-details dt{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}',
|
|
992
|
+
'.dsh-ssh-workbench-target-details dd{color:var(--dsw-alias-label-secondary);font-variant-numeric:tabular-nums;text-align:right}',
|
|
993
|
+
'.dsh-ssh-workbench-target-details dd.dsh-ssh-workbench-target-row-running{color:var(--dsw-alias-label-primary);font-weight:500}',
|
|
994
|
+
'.dsh-ssh-workbench-input{box-sizing:border-box;border:.5px solid var(--dsw-alias-border-l4);border-radius:var(--dsw-radius-md);background:var(--dsw-alias-bg-layer-3);color:var(--dsw-alias-label-primary);font:inherit;font-size:13px;line-height:20px;padding:0 12px;min-width:0}',
|
|
871
995
|
'.dsh-ssh-workbench-input:focus-visible{outline:none;border-color:var(--dsw-alias-state-business-primary)}',
|
|
872
996
|
'.dsh-ssh-workbench-input-compact{height:28px;padding:0 8px;border-radius:var(--dsw-radius-sm);border-color:var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-1);font-size:12px;line-height:18px}',
|
|
873
997
|
'.dsh-ssh-workbench-select-trigger{display:flex;align-items:center;justify-content:space-between;gap:8px;text-align:left;cursor:pointer}',
|
|
@@ -2013,106 +2137,6 @@ window.__ModuleLoader__.load({
|
|
|
2013
2137
|
: tr('tool.failed', '调用失败')
|
|
2014
2138
|
}
|
|
2015
2139
|
|
|
2016
|
-
function SshOpsToolRow(props) {
|
|
2017
|
-
const phase = props?.phase ?? 'result'
|
|
2018
|
-
const block = props?.block
|
|
2019
|
-
const args = toolArgs(block)
|
|
2020
|
-
const toolName = props?.toolName ?? block?.call?.name ?? 'ssh_workbench'
|
|
2021
|
-
const command = typeof args?.command === 'string'
|
|
2022
|
-
? args.command
|
|
2023
|
-
: typeof args?.commands === 'string'
|
|
2024
|
-
? args.commands
|
|
2025
|
-
: typeof args?.pack === 'string'
|
|
2026
|
-
? args.pack
|
|
2027
|
-
: typeof args?.text === 'string'
|
|
2028
|
-
? args.text
|
|
2029
|
-
: ''
|
|
2030
|
-
const target = Array.isArray(args?.servers)
|
|
2031
|
-
? args.servers.join(', ')
|
|
2032
|
-
: args?.tag
|
|
2033
|
-
? 'tag:' + args.tag
|
|
2034
|
-
: args?.server ?? args?.session ?? ''
|
|
2035
|
-
const body = phase === 'result' ? textFromToolContent(block) : ''
|
|
2036
|
-
const bodyText = body.trim()
|
|
2037
|
-
const errorCode = phase === 'result' ? toolErrorCode(block) : ''
|
|
2038
|
-
const failed = phase === 'result' && (block?.isError || block?.error)
|
|
2039
|
-
const running = phase === 'preparing' || phase === 'start'
|
|
2040
|
-
const [expanded, setExpanded] = useState(false)
|
|
2041
|
-
const labels = {
|
|
2042
|
-
ssh_workbench_target: tr('tool.target', '切换远程目标'),
|
|
2043
|
-
}
|
|
2044
|
-
const label = labels[toolName] ?? tr('tool.target', '切换远程目标')
|
|
2045
|
-
const argsText = args ? JSON.stringify(args, null, 2) : ''
|
|
2046
|
-
const canExpand = Boolean(bodyText || argsText)
|
|
2047
|
-
const summary = failed
|
|
2048
|
-
? toolRowFailureText(bodyText, errorCode)
|
|
2049
|
-
: target && command
|
|
2050
|
-
? target + ' · ' + command
|
|
2051
|
-
: target || command || (running ? tr('tool.running', '执行中…') : tr('tool.noOutput', '(无输出)'))
|
|
2052
|
-
const state = failed ? 'error' : running ? 'running' : 'ok'
|
|
2053
|
-
const leading = running
|
|
2054
|
-
? spinner(14)
|
|
2055
|
-
: h('svg', {
|
|
2056
|
-
width: 14, height: 14, viewBox: '0 0 16 16', fill: 'none',
|
|
2057
|
-
'aria-hidden': 'true', style: { display: 'block' },
|
|
2058
|
-
},
|
|
2059
|
-
state === 'error'
|
|
2060
|
-
? [
|
|
2061
|
-
h('circle', { key: 'ring', cx: 8, cy: 8, r: 6, stroke: 'currentColor', strokeWidth: 1.2 }),
|
|
2062
|
-
h('line', { key: 'a', x1: 5.8, y1: 5.8, x2: 10.2, y2: 10.2, stroke: 'currentColor', strokeWidth: 1.2 }),
|
|
2063
|
-
h('line', { key: 'b', x1: 10.2, y1: 5.8, x2: 5.8, y2: 10.2, stroke: 'currentColor', strokeWidth: 1.2 }),
|
|
2064
|
-
]
|
|
2065
|
-
: h('polyline', {
|
|
2066
|
-
points: '3.5 8.5 6.5 11.5 12.5 5',
|
|
2067
|
-
stroke: 'currentColor', strokeWidth: 1.2, strokeLinecap: 'round', strokeLinejoin: 'round',
|
|
2068
|
-
}))
|
|
2069
|
-
const ioSection = (labelText, text) => h('div', { className: 'dsh-ssh-workbench-io-section' },
|
|
2070
|
-
h('span', { className: 'dsh-ssh-workbench-io-label' }, labelText),
|
|
2071
|
-
h('span', { className: 'dsh-ssh-workbench-io-text' }, text))
|
|
2072
|
-
const ioDivider = (key) => h('div', { key, className: 'dsh-ssh-workbench-io-divider' })
|
|
2073
|
-
const sections = []
|
|
2074
|
-
if (argsText) sections.push(ioSection(tr('tool.input', '参数'), argsText))
|
|
2075
|
-
if (bodyText) sections.push(ioSection(tr('tool.output', '输出'), bodyText))
|
|
2076
|
-
const bodyBlocks = sections.reduce((accumulator, section, index) => {
|
|
2077
|
-
if (index > 0) accumulator.push(ioDivider('divider-' + index))
|
|
2078
|
-
accumulator.push(section)
|
|
2079
|
-
return accumulator
|
|
2080
|
-
}, [])
|
|
2081
|
-
return h('div', {
|
|
2082
|
-
'data-sdh-tool-card': toolName,
|
|
2083
|
-
style: { display: 'flex', flexDirection: 'column', minWidth: 0 },
|
|
2084
|
-
},
|
|
2085
|
-
h('button', {
|
|
2086
|
-
type: 'button',
|
|
2087
|
-
className: 'dsh-ssh-workbench-tool-row',
|
|
2088
|
-
'data-sdh-tool-details-toggle': '',
|
|
2089
|
-
'aria-expanded': expanded ? 'true' : 'false',
|
|
2090
|
-
disabled: canExpand !== true,
|
|
2091
|
-
onClick: () => setExpanded((value) => !value),
|
|
2092
|
-
},
|
|
2093
|
-
h('span', { className: 'dsh-ssh-workbench-tool-leading', 'data-state': state }, leading),
|
|
2094
|
-
h('span', { className: 'dsh-ssh-workbench-tool-title' }, label),
|
|
2095
|
-
h('span', { className: 'dsh-ssh-workbench-tool-sep', 'aria-hidden': 'true' }),
|
|
2096
|
-
h('span', {
|
|
2097
|
-
className: 'dsh-ssh-workbench-tool-summary',
|
|
2098
|
-
'data-error': failed ? '' : undefined,
|
|
2099
|
-
'data-stopped': !failed && !running && /stop|cancel/i.test(errorCode) ? '' : undefined,
|
|
2100
|
-
}, summary),
|
|
2101
|
-
canExpand
|
|
2102
|
-
? h('span', { className: 'dsh-ssh-workbench-tool-action' }, expanded ? tr('tool.collapse', '收起详情') : tr('tool.expand', '查看详情'))
|
|
2103
|
-
: null,
|
|
2104
|
-
),
|
|
2105
|
-
expanded && canExpand
|
|
2106
|
-
? h('div', { className: 'dsh-ssh-workbench-tool-body' },
|
|
2107
|
-
h('div', { className: 'dsh-ssh-workbench-io-card' },
|
|
2108
|
-
bodyBlocks.length > 0
|
|
2109
|
-
? bodyBlocks
|
|
2110
|
-
: h('div', { className: 'dsh-ssh-workbench-io-section' },
|
|
2111
|
-
h('span', { className: 'dsh-ssh-workbench-io-label' }, tr('tool.output', '输出')),
|
|
2112
|
-
h('span', { className: 'dsh-ssh-workbench-io-text' }, tr('tool.noOutput', '(无输出)')))))
|
|
2113
|
-
: null,
|
|
2114
|
-
)
|
|
2115
|
-
}
|
|
2116
2140
|
let translate = (key) => key
|
|
2117
2141
|
/** Locale lookup with a Chinese fallback for hosts/tests where the locale service is not bound. */
|
|
2118
2142
|
const tr = (key, fallback) => {
|
|
@@ -4836,16 +4860,6 @@ ${d.join(`
|
|
|
4836
4860
|
TargetDock,
|
|
4837
4861
|
),
|
|
4838
4862
|
)
|
|
4839
|
-
for (const toolName of ['ssh_workbench_target']) {
|
|
4840
|
-
slots.inject('tool.call.toolview', () =>
|
|
4841
|
-
slots.register(
|
|
4842
|
-
{ name: 'tool.call.toolview', key: toolName },
|
|
4843
|
-
function SdhSshOpsToolView(props) {
|
|
4844
|
-
return h(SshOpsToolRow, props)
|
|
4845
|
-
},
|
|
4846
|
-
),
|
|
4847
|
-
)
|
|
4848
|
-
}
|
|
4849
4863
|
}
|
|
4850
4864
|
|
|
4851
4865
|
// Native right sidebar: a page type opened by kind, plus a guide entry.
|
package/lib/core/activity.js
CHANGED
|
@@ -63,6 +63,11 @@ export function createActivityStore({ file = activityPath(), maxEntries = MAX_AC
|
|
|
63
63
|
...(entry?.server !== undefined ? { server: String(entry.server) } : {}),
|
|
64
64
|
...(entry?.summary !== undefined ? { summary: String(entry.summary) } : {}),
|
|
65
65
|
...(entry?.outcome !== undefined ? { outcome: String(entry.outcome) } : {}),
|
|
66
|
+
// Session/duration let the composer dock summarize this session's remote
|
|
67
|
+
// executions without reading DSH's private session metrics. Absent on
|
|
68
|
+
// older entries; readers must tolerate that.
|
|
69
|
+
...(typeof entry?.sessionId === 'string' && entry.sessionId.length > 0 ? { sessionId: entry.sessionId } : {}),
|
|
70
|
+
...(Number.isFinite(entry?.durationMs) && entry.durationMs >= 0 ? { durationMs: Math.round(entry.durationMs) } : {}),
|
|
66
71
|
}
|
|
67
72
|
const next = [normalized, ...load()].slice(0, maxEntries)
|
|
68
73
|
save(next)
|
package/lib/core/broker.js
CHANGED
|
@@ -177,13 +177,16 @@ export function createSessionBroker({ getSecret, knownHosts, config = {}, log =
|
|
|
177
177
|
return `${server.host}|${server.port}|${server.username}|${auth}`
|
|
178
178
|
}
|
|
179
179
|
|
|
180
|
-
function connectOptions(server, state) {
|
|
180
|
+
function connectOptions(server, state, connectTimeoutMs) {
|
|
181
181
|
const hostKeyMode = settings.hostKeyMode === 'accept-new' ? 'accept-new' : 'strict'
|
|
182
|
+
const readyTimeout = Number.isInteger(connectTimeoutMs) && connectTimeoutMs > 0
|
|
183
|
+
? Math.min(connectTimeoutMs, settings.connectTimeoutMs)
|
|
184
|
+
: settings.connectTimeoutMs
|
|
182
185
|
return {
|
|
183
186
|
host: server.host,
|
|
184
187
|
port: server.port,
|
|
185
188
|
username: server.username,
|
|
186
|
-
readyTimeout
|
|
189
|
+
readyTimeout,
|
|
187
190
|
keepaliveInterval: settings.keepaliveInterval,
|
|
188
191
|
keepaliveCountMax: settings.keepaliveCountMax,
|
|
189
192
|
hostHash: 'sha256',
|
|
@@ -197,7 +200,7 @@ export function createSessionBroker({ getSecret, knownHosts, config = {}, log =
|
|
|
197
200
|
}
|
|
198
201
|
|
|
199
202
|
/** Open a fresh client and resolve on `ready`. */
|
|
200
|
-
async function connectClient(server) {
|
|
203
|
+
async function connectClient(server, connectTimeoutMs) {
|
|
201
204
|
const { Client } = await loadSsh2()
|
|
202
205
|
const state = {}
|
|
203
206
|
const client = new Client()
|
|
@@ -226,7 +229,7 @@ export function createSessionBroker({ getSecret, knownHosts, config = {}, log =
|
|
|
226
229
|
log('ssh client error', { server: server.name, error: error instanceof Error ? error.message : String(error) })
|
|
227
230
|
})
|
|
228
231
|
try {
|
|
229
|
-
client.connect(connectOptions(server, state))
|
|
232
|
+
client.connect(connectOptions(server, state, connectTimeoutMs))
|
|
230
233
|
} catch (error) {
|
|
231
234
|
fail(error)
|
|
232
235
|
}
|
|
@@ -281,7 +284,7 @@ export function createSessionBroker({ getSecret, knownHosts, config = {}, log =
|
|
|
281
284
|
}
|
|
282
285
|
|
|
283
286
|
|
|
284
|
-
async function acquire(server) {
|
|
287
|
+
async function acquire(server, options = {}) {
|
|
285
288
|
const id = server.id ?? server.name
|
|
286
289
|
const key = serverKey(server)
|
|
287
290
|
let entry = entries.get(id)
|
|
@@ -328,7 +331,7 @@ export function createSessionBroker({ getSecret, knownHosts, config = {}, log =
|
|
|
328
331
|
}
|
|
329
332
|
created.pending = (async () => {
|
|
330
333
|
try {
|
|
331
|
-
const { client, fingerprint, connectMs } = await connectClient(server)
|
|
334
|
+
const { client, fingerprint, connectMs } = await connectClient(server, options.connectTimeoutMs)
|
|
332
335
|
created.client = client
|
|
333
336
|
created.fingerprint = fingerprint
|
|
334
337
|
created.connectMs = connectMs
|
|
@@ -589,7 +592,7 @@ export function createSessionBroker({ getSecret, knownHosts, config = {}, log =
|
|
|
589
592
|
for (;;) {
|
|
590
593
|
let acquired
|
|
591
594
|
try {
|
|
592
|
-
acquired = await acquire(server)
|
|
595
|
+
acquired = await acquire(server, { connectTimeoutMs: options.connectTimeoutMs })
|
|
593
596
|
} catch (error) {
|
|
594
597
|
const normalized = error instanceof SshError ? error : normalizeError(error)
|
|
595
598
|
const result = errorResult(normalized.code, normalized.message, { durationMs: now() - startedAt })
|
package/lib/core/dialect.js
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
11
|
/** The dialect values this plugin understands. */
|
|
12
|
-
|
|
12
|
+
const DIALECTS = Object.freeze({ POSIX: 'posix', WINDOWS: 'windows' })
|
|
13
13
|
|
|
14
14
|
/** Normalize one raw dialect/platform value. */
|
|
15
15
|
export function normalizeDialect(value) {
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Per-session execution summary for the composer target dock.
|
|
3
|
+
*
|
|
4
|
+
* DSH owns conversation/model metrics (turns, TTFT, TPS); the plugin cannot
|
|
5
|
+
* read them. This module only summarizes the activity entries the plugin
|
|
6
|
+
* writes for remote executions, so the dock can show accurate SSH-side counts
|
|
7
|
+
* and latency without inventing values it does not own.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
function numericDuration(entry) {
|
|
11
|
+
return Number.isFinite(entry?.durationMs) && entry.durationMs >= 0 ? entry.durationMs : 0
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function emptyBucket(name) {
|
|
15
|
+
return { name, count: 0, success: 0, failed: 0, durationMs: 0, averageMs: 0, lastAt: null }
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function addToBucket(bucket, entry) {
|
|
19
|
+
bucket.count += 1
|
|
20
|
+
if (entry.outcome === 'success') bucket.success += 1
|
|
21
|
+
else bucket.failed += 1
|
|
22
|
+
bucket.durationMs += numericDuration(entry)
|
|
23
|
+
if (typeof entry.at === 'string' && (bucket.lastAt === null || entry.at > bucket.lastAt)) bucket.lastAt = entry.at
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Summarize the plugin's remote execution activity for one Session.
|
|
28
|
+
*
|
|
29
|
+
* @param {object[]} entries newest-first activity entries
|
|
30
|
+
* @param {{ sessionId?: string, candidates?: string[] }} options
|
|
31
|
+
* @returns {{
|
|
32
|
+
* sessionId: string|null,
|
|
33
|
+
* count: number,
|
|
34
|
+
* success: number,
|
|
35
|
+
* failed: number,
|
|
36
|
+
* durationMs: number,
|
|
37
|
+
* averageMs: number,
|
|
38
|
+
* lastAt: string|null,
|
|
39
|
+
* servers: Array<{name:string,count:number,success:number,failed:number,durationMs:number,averageMs:number,lastAt:string|null}>
|
|
40
|
+
* }}
|
|
41
|
+
*/
|
|
42
|
+
export function summarizeExecution(entries, { sessionId, candidates = [] } = {}) {
|
|
43
|
+
const wanted = typeof sessionId === 'string' && sessionId.length > 0 ? sessionId : null
|
|
44
|
+
const byServer = new Map()
|
|
45
|
+
const summary = {
|
|
46
|
+
sessionId: wanted,
|
|
47
|
+
count: 0,
|
|
48
|
+
success: 0,
|
|
49
|
+
failed: 0,
|
|
50
|
+
durationMs: 0,
|
|
51
|
+
averageMs: 0,
|
|
52
|
+
lastAt: null,
|
|
53
|
+
servers: [],
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (wanted !== null) {
|
|
57
|
+
for (const entry of Array.isArray(entries) ? entries : []) {
|
|
58
|
+
if (!entry || entry.kind !== 'exec' || entry.sessionId !== wanted) continue
|
|
59
|
+
summary.count += 1
|
|
60
|
+
if (entry.outcome === 'success') summary.success += 1
|
|
61
|
+
else summary.failed += 1
|
|
62
|
+
summary.durationMs += numericDuration(entry)
|
|
63
|
+
if (typeof entry.at === 'string' && (summary.lastAt === null || entry.at > summary.lastAt)) summary.lastAt = entry.at
|
|
64
|
+
const name = typeof entry.server === 'string' && entry.server.length > 0 ? entry.server : null
|
|
65
|
+
if (name === null) continue
|
|
66
|
+
if (!byServer.has(name)) byServer.set(name, emptyBucket(name))
|
|
67
|
+
addToBucket(byServer.get(name), entry)
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
summary.averageMs = summary.count > 0 ? Math.round(summary.durationMs / summary.count) : 0
|
|
72
|
+
|
|
73
|
+
// Keep the mentioned resources in mention order, then append any target that
|
|
74
|
+
// ran from an explicit remote_exec task without being a session candidate.
|
|
75
|
+
const names = []
|
|
76
|
+
for (const name of Array.isArray(candidates) ? candidates : []) {
|
|
77
|
+
const value = String(name)
|
|
78
|
+
if (value && !names.includes(value)) names.push(value)
|
|
79
|
+
}
|
|
80
|
+
for (const name of [...byServer.keys()].sort()) {
|
|
81
|
+
if (!names.includes(name)) names.push(name)
|
|
82
|
+
}
|
|
83
|
+
summary.servers = names.map((name) => {
|
|
84
|
+
const bucket = byServer.get(name) ?? emptyBucket(name)
|
|
85
|
+
return { ...bucket, averageMs: bucket.count > 0 ? Math.round(bucket.durationMs / bucket.count) : 0 }
|
|
86
|
+
})
|
|
87
|
+
return summary
|
|
88
|
+
}
|
package/lib/core/gate-pwsh.js
CHANGED
|
@@ -58,7 +58,7 @@ const RULES = [
|
|
|
58
58
|
]
|
|
59
59
|
|
|
60
60
|
/** Split one PowerShell command into independently-executed statements. */
|
|
61
|
-
|
|
61
|
+
function powerShellStatements(command) {
|
|
62
62
|
return String(command ?? '')
|
|
63
63
|
.split(/[;\r\n]|\|\||&&|\|/)
|
|
64
64
|
.map((part) => part.trim())
|