dsh-remote 0.8.63 → 0.8.65
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/client.js +92 -30
- package/package.json +2 -2
package/lib/client.js
CHANGED
|
@@ -55,6 +55,10 @@ window.__ModuleLoader__.load({
|
|
|
55
55
|
'settings.badgeKey': '私钥',
|
|
56
56
|
'settings.openWorkspace': '选目录',
|
|
57
57
|
'settings.openWorkspaceHint': '在这台机器上选一个远程目录作为工作区',
|
|
58
|
+
'settings.recentWorkspacesFor': '最近工作区({n})',
|
|
59
|
+
'settings.recentWorkspacesHint': '这台机器用过的远程目录,点开直接打开',
|
|
60
|
+
'settings.openWorkspaceHere': '打开',
|
|
61
|
+
'settings.openWorkspaceOnHint': '在 {host} 上打开这个目录(会创建/复用本地镜像)',
|
|
58
62
|
'settings.workspaceOpened': '已打开远程工作区:{path}',
|
|
59
63
|
'settings.addMachineManual': '手动添加(密码 / 私钥 / 跳板机)',
|
|
60
64
|
// 列表的搜索、排序与连接状态(机器一多就要用)。
|
|
@@ -143,8 +147,6 @@ window.__ModuleLoader__.load({
|
|
|
143
147
|
'settings.testConn': '测试连接',
|
|
144
148
|
'settings.saving': '保存中…',
|
|
145
149
|
'settings.save': '保存',
|
|
146
|
-
'settings.recentWorkspaces': '当前机器最近工作区(一键切换)',
|
|
147
|
-
'settings.switch': '切换',
|
|
148
150
|
'settings.forwards': '端口转发(SSH 隧道)',
|
|
149
151
|
'settings.fwdStop': '停止',
|
|
150
152
|
'settings.fwdStart': '启动',
|
|
@@ -223,7 +225,6 @@ window.__ModuleLoader__.load({
|
|
|
223
225
|
'settings.secretStoreRefused': '密码未能写入系统密钥库({msg}),已放弃保存该密码(不会以明文落盘)。修复密钥库后重新保存,或把 credentialFallback 设为 plain 以接受明文回退。',
|
|
224
226
|
'settings.cleared': '已取消当前远程机 — 本地会话不再自动使用远程上下文',
|
|
225
227
|
'settings.filledSsh': '已从 ~/.ssh/config 填入(仅引用私钥路径,不读取内容)',
|
|
226
|
-
'settings.workspaceSwitched': '已切换到工作区 {path}',
|
|
227
228
|
'settings.connOk': '连接成功',
|
|
228
229
|
'settings.connOkWith': '连接成功({user}@{host},{latency}ms){shell}',
|
|
229
230
|
'settings.connFail': '连接失败',
|
|
@@ -382,6 +383,10 @@ window.__ModuleLoader__.load({
|
|
|
382
383
|
'settings.badgeKey': 'key',
|
|
383
384
|
'settings.openWorkspace': 'Folder',
|
|
384
385
|
'settings.openWorkspaceHint': 'Pick a remote folder on this machine as the workspace',
|
|
386
|
+
'settings.recentWorkspacesFor': 'Recent folders ({n})',
|
|
387
|
+
'settings.recentWorkspacesHint': 'Remote folders this machine used before — open one directly',
|
|
388
|
+
'settings.openWorkspaceHere': 'Open',
|
|
389
|
+
'settings.openWorkspaceOnHint': 'Open this folder on {host} (creates or reuses the local mirror)',
|
|
385
390
|
'settings.workspaceOpened': 'Opened remote workspace: {path}',
|
|
386
391
|
'settings.addMachineManual': 'Add manually (password / key / jump host)',
|
|
387
392
|
'settings.hostFilter': 'Search hosts (name / address / user)',
|
|
@@ -463,8 +468,6 @@ window.__ModuleLoader__.load({
|
|
|
463
468
|
'settings.testConn': 'Test connection',
|
|
464
469
|
'settings.saving': 'Saving…',
|
|
465
470
|
'settings.save': 'Save',
|
|
466
|
-
'settings.recentWorkspaces': 'Recent workspaces of the current machine (one-click switch)',
|
|
467
|
-
'settings.switch': 'Switch',
|
|
468
471
|
'settings.forwards': 'Port forwarding (SSH tunnels)',
|
|
469
472
|
'settings.fwdStop': 'Stop',
|
|
470
473
|
'settings.fwdStart': 'Start',
|
|
@@ -543,7 +546,6 @@ window.__ModuleLoader__.load({
|
|
|
543
546
|
'settings.secretStoreRefused': 'The OS keychain refused the password ({msg}); the password was NOT saved (it will not be written to disk in plaintext). Repair the keychain and save again, or set credentialFallback to plain to accept the plaintext fallback.',
|
|
544
547
|
'settings.cleared': 'Cleared current remote — local sessions no longer use remote context automatically',
|
|
545
548
|
'settings.filledSsh': 'Filled from ~/.ssh/config (only references the private key path, never reads its content)',
|
|
546
|
-
'settings.workspaceSwitched': 'Switched to workspace {path}',
|
|
547
549
|
'settings.connOk': 'Connection successful',
|
|
548
550
|
'settings.connOkWith': 'Connection successful ({user}@{host}, {latency}ms){shell}',
|
|
549
551
|
'settings.connFail': 'Connection failed',
|
|
@@ -1009,7 +1011,6 @@ window.__ModuleLoader__.load({
|
|
|
1009
1011
|
const [forwards, setForwards] = React.useState([])
|
|
1010
1012
|
const [fwdForm, setFwdForm] = React.useState({ listenPort: '', targetHost: '127.0.0.1', targetPort: '', direction: 'local', autoStart: false })
|
|
1011
1013
|
const [auditLines, setAuditLines] = React.useState([])
|
|
1012
|
-
const [workspaces, setWorkspaces] = React.useState([])
|
|
1013
1014
|
const [advOpen, setAdvOpen] = React.useState(false) // 高级配置折叠
|
|
1014
1015
|
// Collapsed by default: the forwards panel sits BELOW the host list and used to
|
|
1015
1016
|
// push the update/audit cards far down the page for a feature most sessions
|
|
@@ -1046,6 +1047,8 @@ window.__ModuleLoader__.load({
|
|
|
1046
1047
|
const [manualOpen, setManualOpen] = React.useState(false)
|
|
1047
1048
|
const [dirPick, setDirPick] = React.useState(null) // { machineId } while the picker is open
|
|
1048
1049
|
const [hostFilter, setHostFilter] = React.useState('')
|
|
1050
|
+
// Which machine rows have their 最近工作区 list expanded (issue #73: host → folders).
|
|
1051
|
+
const [openWs, setOpenWs] = React.useState({})
|
|
1049
1052
|
// session's machine", which the host resolves from the session binding.
|
|
1050
1053
|
React.useEffect(() => { ensureChromeCss() }, [])
|
|
1051
1054
|
|
|
@@ -1105,11 +1108,25 @@ window.__ModuleLoader__.load({
|
|
|
1105
1108
|
|
|
1106
1109
|
const refresh = () => api('GET', '/dsh-remote/machines').then((r) => {
|
|
1107
1110
|
setMachines(r.machines || []); setCurrentId(r.currentId || '')
|
|
1108
|
-
const cur = (r.machines || []).find((m) => m.id === r.currentId)
|
|
1109
|
-
setWorkspaces((cur && cur.recentWorkspaces) || [])
|
|
1110
1111
|
})
|
|
1111
1112
|
React.useEffect(() => { refresh() }, [])
|
|
1112
1113
|
|
|
1114
|
+
// Issue #73 follow-up: the settings page must notice changes made ELSEWHERE —
|
|
1115
|
+
// an agent calling rw_connect, or another tab switching machines — without the
|
|
1116
|
+
// user reloading. A light poll of the registry (a local JSON read) plus a
|
|
1117
|
+
// refresh on window focus; the ssh-config list is re-read on focus only, since
|
|
1118
|
+
// that one touches the filesystem.
|
|
1119
|
+
React.useEffect(() => {
|
|
1120
|
+
const poll = () => { refresh().catch(() => {}) }
|
|
1121
|
+
const timer = setInterval(poll, 5000)
|
|
1122
|
+
const onFocus = () => { poll(); loadSshHosts() }
|
|
1123
|
+
try { window.addEventListener('focus', onFocus) } catch { /* headless host */ }
|
|
1124
|
+
return () => {
|
|
1125
|
+
clearInterval(timer)
|
|
1126
|
+
try { window.removeEventListener('focus', onFocus) } catch { /* headless host */ }
|
|
1127
|
+
}
|
|
1128
|
+
}, [])
|
|
1129
|
+
|
|
1113
1130
|
// ── update state ────────────────────────────────────────────────────────
|
|
1114
1131
|
const [upd, setUpd] = React.useState(null) // { current, latest, updateAvailable, updateMode, updatedMarker }
|
|
1115
1132
|
const [updBusy, setUpdBusy] = React.useState(false)
|
|
@@ -1403,10 +1420,25 @@ window.__ModuleLoader__.load({
|
|
|
1403
1420
|
.finally(() => setConnectingKey(''))
|
|
1404
1421
|
}
|
|
1405
1422
|
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1423
|
+
/**
|
|
1424
|
+
* Open a folder this machine used before — one click from the row's
|
|
1425
|
+
* 「最近工作区」 list (issue #73: the VSCode shape is host → folders).
|
|
1426
|
+
*
|
|
1427
|
+
* Goes through /mirror rather than /workspace: /workspace only changes the
|
|
1428
|
+
* ACTIVE machine's workspace, while the user may be clicking a folder that
|
|
1429
|
+
* belongs to a machine they are not on. /mirror re-asserts the machine, creates
|
|
1430
|
+
* (or reuses) the mirror and persists the choice on that machine.
|
|
1431
|
+
*/
|
|
1432
|
+
const openWorkspaceOn = (machineId, path) => {
|
|
1433
|
+
setBusy(true); setErr(''); setMsg('')
|
|
1434
|
+
api('POST', '/dsh-remote/mirror', { path, machineId })
|
|
1435
|
+
.then((r) => {
|
|
1436
|
+
setCurrentId((r && r.currentId) || machineId)
|
|
1437
|
+
setMsg(tr('settings.workspaceOpened', { path }))
|
|
1438
|
+
refresh()
|
|
1439
|
+
})
|
|
1409
1440
|
.catch((e) => setErr(String((e && e.message) || e)))
|
|
1441
|
+
.finally(() => setBusy(false))
|
|
1410
1442
|
}
|
|
1411
1443
|
|
|
1412
1444
|
// ── forwards panel ────────────────────────────────────────────────────
|
|
@@ -1575,7 +1607,31 @@ window.__ModuleLoader__.load({
|
|
|
1575
1607
|
(m.sshConfigResolved && m.sshConfigResolved.warnings && m.sshConfigResolved.warnings.length)
|
|
1576
1608
|
? React.createElement('div', { style: { fontSize: 11, color: T.warn, marginTop: 2 } }, tr('settings.sshResolvedWarn', { msg: m.sshConfigResolved.warnings.join(' ') }))
|
|
1577
1609
|
: null,
|
|
1578
|
-
rowResult('machine:' + m.id)
|
|
1610
|
+
rowResult('machine:' + m.id),
|
|
1611
|
+
// 最近工作区挂在这一行下面(host → folders,issue #73):点一下就在这台
|
|
1612
|
+
// 机器上打开那个目录(走 /mirror,会建/复用镜像并记住选择)。
|
|
1613
|
+
(Array.isArray(m.recentWorkspaces) && m.recentWorkspaces.length)
|
|
1614
|
+
? React.createElement('div', { style: { marginTop: 4 } },
|
|
1615
|
+
React.createElement('button', {
|
|
1616
|
+
type: 'button', className: 'dsh-rw-section-btn',
|
|
1617
|
+
style: { fontSize: 11, padding: 0 },
|
|
1618
|
+
title: tr('settings.recentWorkspacesHint'),
|
|
1619
|
+
onClick: () => setOpenWs((prev) => ({ ...prev, [m.id]: !prev[m.id] })),
|
|
1620
|
+
}, React.createElement('span', { style: { opacity: 0.7, width: 10, display: 'inline-block' } }, openWs[m.id] ? '▾' : '▸'),
|
|
1621
|
+
tr('settings.recentWorkspacesFor', { n: m.recentWorkspaces.length })),
|
|
1622
|
+
openWs[m.id]
|
|
1623
|
+
? React.createElement('div', { style: { marginTop: 2, marginLeft: 16 } },
|
|
1624
|
+
m.recentWorkspaces.map((w, wi) => React.createElement('div', { key: w + wi, style: { display: 'flex', gap: 6, alignItems: 'center', padding: '1px 0' } },
|
|
1625
|
+
React.createElement('code', { style: { flex: 1, fontSize: 11, fontFamily: 'monospace', opacity: 0.85, wordBreak: 'break-all' } }, w),
|
|
1626
|
+
React.createElement('button', {
|
|
1627
|
+
type: 'button', className: 'dsh-rw-btn',
|
|
1628
|
+
style: { ...buttonS, fontSize: 11, padding: '1px 8px', whiteSpace: 'nowrap' },
|
|
1629
|
+
disabled: busy,
|
|
1630
|
+
title: tr('settings.openWorkspaceOnHint', { host: m.name }),
|
|
1631
|
+
onClick: () => openWorkspaceOn(m.id, w),
|
|
1632
|
+
}, tr('settings.openWorkspaceHere')))))
|
|
1633
|
+
: null)
|
|
1634
|
+
: null),
|
|
1579
1635
|
React.createElement('div', { style: { display: 'flex', gap: 6, flexShrink: 0 } },
|
|
1580
1636
|
React.createElement('button', { type: 'button', className: 'dsh-rw-btn', style: buttonS, onClick: () => startEdit(m) }, tr('settings.edit')),
|
|
1581
1637
|
React.createElement('button', { type: 'button', className: 'dsh-rw-btn', style: buttonS, onClick: () => del(m.id) }, tr('settings.delete')),
|
|
@@ -1723,12 +1779,8 @@ window.__ModuleLoader__.load({
|
|
|
1723
1779
|
),
|
|
1724
1780
|
) : null,
|
|
1725
1781
|
),
|
|
1726
|
-
//
|
|
1727
|
-
|
|
1728
|
-
React.createElement('div', { style: { marginBottom: 6, fontSize: 13, fontWeight: 600 } }, tr('settings.recentWorkspaces')),
|
|
1729
|
-
workspaces.map((w, i) => React.createElement('div', { key: w + i, style: { display: 'flex', gap: 8, alignItems: 'center', padding: '4px 0' } },
|
|
1730
|
-
React.createElement('code', { style: { flex: 1, fontSize: 12, fontFamily: 'monospace' } }, w),
|
|
1731
|
-
React.createElement('button', { type: 'button', className: 'dsh-rw-btn', style: { ...buttonS, padding: '2px 10px' }, onClick: () => setWorkspaceNow(w) }, tr('settings.switch'))))) : null,
|
|
1782
|
+
// 最近工作区现在挂在各自的机器行下面(issue #73:host → folders 的树),
|
|
1783
|
+
// 这个「只显示当前机器」的卡片就没有意义了。
|
|
1732
1784
|
// 远程 DSH 界面(issue #46)
|
|
1733
1785
|
React.createElement('div', { style: boxS },
|
|
1734
1786
|
sectionToggle(webAttachOpen, setWebAttachOpen, tr('settings.webAttach')),
|
|
@@ -1941,7 +1993,11 @@ window.__ModuleLoader__.load({
|
|
|
1941
1993
|
: React.createElement('div', { style: { opacity: 0.6, fontSize: 12 } }, tr('settings.auditEmpty')))
|
|
1942
1994
|
: React.createElement('div', { style: { fontSize: 11, opacity: 0.55 } }, String(auditLines.length)),
|
|
1943
1995
|
),
|
|
1944
|
-
|
|
1996
|
+
// Panel-level messages. They used to live ONLY inside the manual form, which is
|
|
1997
|
+
// collapsed by default now — so "已打开远程工作区 …" and a failed 连接 were
|
|
1998
|
+
// invisible after clicking something in the list.
|
|
1999
|
+
msg ? React.createElement('div', { style: { color: T.ok, fontSize: 12, lineHeight: 1.5 } }, msg) : null,
|
|
2000
|
+
err ? React.createElement('div', { style: { color: T.danger, fontSize: 13, lineHeight: 1.5 } }, err) : null,
|
|
1945
2001
|
React.createElement('div', { style: boxS },
|
|
1946
2002
|
React.createElement('div', { style: { marginBottom: 6, fontSize: 13, fontWeight: 600 } }, tr('settings.update')),
|
|
1947
2003
|
React.createElement('div', { style: { display: 'flex', gap: 8, alignItems: 'center', flexWrap: 'wrap', fontSize: 12 } },
|
|
@@ -1992,17 +2048,18 @@ window.__ModuleLoader__.load({
|
|
|
1992
2048
|
// 选目录 on a machine that is NOT current must browse THAT machine, not
|
|
1993
2049
|
// whichever one happens to be current.
|
|
1994
2050
|
initialMachineId: dirPick.machineId,
|
|
1995
|
-
onPicked: (
|
|
2051
|
+
onPicked: (localMirror, remotePath) => {
|
|
1996
2052
|
const target = dirPick.machineId
|
|
1997
2053
|
setDirPick(null)
|
|
1998
|
-
|
|
1999
|
-
|
|
2000
|
-
|
|
2001
|
-
|
|
2002
|
-
|
|
2003
|
-
|
|
2004
|
-
|
|
2005
|
-
|
|
2054
|
+
// The picker already created the mirror; posting /mirror again would
|
|
2055
|
+
// send a local path as if it were remote (see commitPath).
|
|
2056
|
+
const shown = remotePath || localMirror
|
|
2057
|
+
if (!shown) return
|
|
2058
|
+
setCurrentId(target)
|
|
2059
|
+
setMsg(tr('settings.workspaceOpened', { path: shown }))
|
|
2060
|
+
// The mirror list is per machine now, so re-reading the registry is
|
|
2061
|
+
// what makes the new folder appear under that machine's row.
|
|
2062
|
+
refresh()
|
|
2006
2063
|
},
|
|
2007
2064
|
onCancel: () => setDirPick(null),
|
|
2008
2065
|
})
|
|
@@ -2458,7 +2515,12 @@ window.__ModuleLoader__.load({
|
|
|
2458
2515
|
// instead of trusting whichever happens to be active.
|
|
2459
2516
|
ensureCurrent(machineId)
|
|
2460
2517
|
.then(() => api('POST', '/dsh-remote/mirror', { path: target, machineId }))
|
|
2461
|
-
|
|
2518
|
+
// `onPicked(localMirror, remotePath)`: the mirror is ALREADY created here.
|
|
2519
|
+
// The sidebar opens the local mirror (1st arg); a caller that only wants to
|
|
2520
|
+
// report/continue from the remote path uses the 2nd. Handing back only the
|
|
2521
|
+
// local path made the settings page post /mirror a SECOND time with a
|
|
2522
|
+
// Windows path as if it were remote ("not a directory (or unreachable)").
|
|
2523
|
+
.then((res) => (res && res.localMirror ? onPicked(res.localMirror, res.path || target) : setErr((res && res.error) || '')))
|
|
2462
2524
|
.catch((e) => setErr(String((e && e.message) || e)))
|
|
2463
2525
|
}
|
|
2464
2526
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-remote",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.65",
|
|
4
4
|
"description": "Remote-work assistant for DeepSeek Harness: connect SSH (password/key/agent/keyboard-interactive, multi-hop ProxyJump), pick a remote workspace, operate on it with 22 rw_* tools, conflict-aware mirror sync, an embedded PTY terminal, one-click remote dsh deploy + web UI attach, port forwarding, host grouping, remote sandbox fencing, audit log, keychain passwords.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "flymysql <flyphp@outlook.com>",
|
|
@@ -92,7 +92,7 @@
|
|
|
92
92
|
"semver": "^7.8.5"
|
|
93
93
|
},
|
|
94
94
|
"scripts": {
|
|
95
|
-
"test": "node --import ./test/setup-telemetry-off.mjs --test --test-timeout=40000 --test-force-exit",
|
|
95
|
+
"test": "node --import ./test/setup-telemetry-off.mjs --test --test-timeout=40000 --test-force-exit --test-concurrency=8",
|
|
96
96
|
"test:coverage": "node scripts/coverage-union.mjs",
|
|
97
97
|
"test:coverage:gate": "node scripts/coverage-union.mjs --gate 70"
|
|
98
98
|
}
|