@goodandready/dsh-remote-workspace 0.3.7 → 0.3.8
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 +4 -2
- package/README.ru.md +4 -2
- package/README.zh.md +4 -2
- package/lib/client.js +490 -323
- package/lib/updater-service.js +3 -18
- package/package.json +1 -2
- package/docs/design/DESIGN.md +0 -91
package/README.md
CHANGED
|
@@ -127,8 +127,10 @@ Four orthogonal, high-leverage tools exposed directly to LLM agents:
|
|
|
127
127
|
- Designed strictly to DSH UX guidelines and styled after `dsh-clinebot`.
|
|
128
128
|
- **Segmented Auth Switcher**: Clean tabbed toggle between Private Key and Password authentication.
|
|
129
129
|
- **Remote Directory Browser Modal**: Interactive remote file browser with breadcrumb navigation and one-click path selection.
|
|
130
|
-
- **Connection Diagnostic Badge**: Real-time ping testing with visual latency indicators and remote OS display.
|
|
131
|
-
- **Action Triggers**: Quick buttons for directional synchronization and tunnel monitoring.
|
|
130
|
+
- **Connection Diagnostic Badge**: Real-time ping testing with visual latency indicators and remote OS display. The header badge follows `/dsh-remote-workspace/state`: it shows a loading, empty, ready, or unavailable connection instead of a permanent Ready label.
|
|
131
|
+
- **Action Triggers**: Quick buttons for directional synchronization and tunnel monitoring. A failed save, delete, activation, directory browse, or tunnel stop shows the server error in an alert on the card. Test Connection posts the profile fields, including `host`.
|
|
132
|
+
- **Plugin list label**: English `Remote Workspace` or Chinese `远程开发工作区`, taken from the dictionaries already loaded with the card.
|
|
133
|
+
- **Updater version**: the row shows the installed version returned by the status request. Before that response it shows "Version unknown".
|
|
132
134
|
|
|
133
135
|
---
|
|
134
136
|
|
package/README.ru.md
CHANGED
|
@@ -127,8 +127,10 @@ graph LR
|
|
|
127
127
|
- Полноценная интеграция в слот `settings.plugin.item` с ключом `dsh-remote-workspace`.
|
|
128
128
|
- **Сегментированный переключатель авторизации**: Удобное переключение между SSH-ключом и паролем.
|
|
129
129
|
- **Интерактивный браузер удалённых папок**: Модальное окно с навигацией по каталогам и выбором рабочей директории в один клик.
|
|
130
|
-
- **Индикатор статуса и задержки**: Проверка связи с отображением пинга и ОС удалённого узла.
|
|
131
|
-
- **Кнопки действий**: Быстрый запуск синхронизации и контроль туннелей прямо из настроек.
|
|
130
|
+
- **Индикатор статуса и задержки**: Проверка связи с отображением пинга и ОС удалённого узла. Бейдж шапки читает `/dsh-remote-workspace/state` и показывает проверку, отсутствие активного хоста, готовность или недоступность статуса.
|
|
131
|
+
- **Кнопки действий**: Быстрый запуск синхронизации и контроль туннелей прямо из настроек. Ошибка сохранения, удаления, выбора активного профиля, обзора каталога или остановки туннеля показывается в предупреждении на карточке. Проверка соединения отправляет поля профиля, включая `host`.
|
|
132
|
+
- **Подпись в списке плагинов**: английская `Remote Workspace` или китайская `远程开发工作区` из словарей карточки.
|
|
133
|
+
- **Версия в блоке обновления**: строка показывает установленную версию из ответа статуса. Пока ответ не пришёл, показывается нейтральная подпись неизвестной версии.
|
|
132
134
|
|
|
133
135
|
---
|
|
134
136
|
|
package/README.zh.md
CHANGED
|
@@ -127,8 +127,10 @@ graph LR
|
|
|
127
127
|
- 深度适配 `settings.plugin.item` 插槽(Key: `dsh-remote-workspace`)。
|
|
128
128
|
- **分段式认证切换器**:优雅切换私钥认证与密码认证。
|
|
129
129
|
- **远程目录浏览器弹窗**:可视化浏览远程服务器目录树,支持面包屑导航与一键选取。
|
|
130
|
-
-
|
|
131
|
-
-
|
|
130
|
+
- **实时连接状态徽章**:可视化展示连通性、网络延迟以及远程操作系统信息。页眉徽章读取 `/dsh-remote-workspace/state`,分别表示正在检查、没有活动主机、主机已就绪或状态不可用,不再固定显示“就绪”。
|
|
131
|
+
- **快捷动作触发**:一键发起定向文件同步并监控隧道运行状态。保存、删除、设为活动、浏览目录或关闭隧道失败时,卡片上的警告会显示服务器返回的错误。连接测试提交的是配置本身,其中包含 `host`。
|
|
132
|
+
- **插件列表名称**:英文为 `Remote Workspace`,中文为 `远程开发工作区`,取自随卡片加载的词典。
|
|
133
|
+
- **更新版本**:该行显示状态接口返回的已安装版本。结果返回前显示“版本未知”。
|
|
132
134
|
|
|
133
135
|
---
|
|
134
136
|
|
package/lib/client.js
CHANGED
|
@@ -117,6 +117,23 @@ window.__ModuleLoader__.load({
|
|
|
117
117
|
title: 'Remote Workspace',
|
|
118
118
|
subtitle: 'Native SSH connections, SFTP file access, 3-way mirror sync, and port forwarding tunnels.',
|
|
119
119
|
status: 'Ready',
|
|
120
|
+
actionFailed: 'The request failed.',
|
|
121
|
+
versionUnknown: 'Version unknown',
|
|
122
|
+
updaterTitle: 'Plugin update',
|
|
123
|
+
currentVersion: 'Installed version',
|
|
124
|
+
latestVersion: 'Latest on npm',
|
|
125
|
+
checkUpdate: 'Check for updates',
|
|
126
|
+
checking: 'Checking...',
|
|
127
|
+
updateNow: 'Update now',
|
|
128
|
+
updating: 'Updating...',
|
|
129
|
+
upToDate: 'Up to date',
|
|
130
|
+
updateAvailable: 'Update available',
|
|
131
|
+
restartRequired: 'The update is installed. Restart DSH to use it.',
|
|
132
|
+
updateFailed: 'Update failed',
|
|
133
|
+
statusLoading: 'Checking connection',
|
|
134
|
+
statusEmpty: 'No active host',
|
|
135
|
+
statusReady: 'Host ready',
|
|
136
|
+
statusError: 'Status unavailable',
|
|
120
137
|
|
|
121
138
|
tabProfiles: '🖥️ Host Profiles',
|
|
122
139
|
|
|
@@ -256,6 +273,12 @@ window.__ModuleLoader__.load({
|
|
|
256
273
|
title: '远程开发工作区',
|
|
257
274
|
subtitle: '支持原生 SSH 连接、SFTP 文件访问、3-way 镜像同步与端口转发隧道。',
|
|
258
275
|
status: '就绪',
|
|
276
|
+
actionFailed: '请求失败。',
|
|
277
|
+
versionUnknown: '版本未知',
|
|
278
|
+
statusLoading: '正在检查连接',
|
|
279
|
+
statusEmpty: '没有活动主机',
|
|
280
|
+
statusReady: '主机已就绪',
|
|
281
|
+
statusError: '状态不可用',
|
|
259
282
|
|
|
260
283
|
tabProfiles: '🖥️ 主机配置',
|
|
261
284
|
tabContainers: '🐳 容器管理',
|
|
@@ -1135,237 +1158,31 @@ window.__ModuleLoader__.load({
|
|
|
1135
1158
|
)
|
|
1136
1159
|
}
|
|
1137
1160
|
|
|
1138
|
-
function
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
const loadState = async () => {
|
|
1165
|
-
try {
|
|
1166
|
-
const res = await fetch('/dsh-remote-workspace/state')
|
|
1167
|
-
if (res.ok) {
|
|
1168
|
-
const data = await res.json()
|
|
1169
|
-
if (data.ok) {
|
|
1170
|
-
setProfiles(data.profiles || [])
|
|
1171
|
-
setActiveId(data.activeId || null)
|
|
1172
|
-
setTunnels(data.tunnels || [])
|
|
1173
|
-
setAutoSync(Boolean(data.autoSync))
|
|
1174
|
-
if (data.activeId) {
|
|
1175
|
-
fetch(`/dsh-remote-workspace/health?profileId=${data.activeId}`).then(r => r.json()).then(h => { if (h.ok) setHealthData(h.health) }).catch(() => {})
|
|
1176
|
-
}
|
|
1177
|
-
}
|
|
1178
|
-
}
|
|
1179
|
-
} catch (err) { /* best-effort cleanup */ }
|
|
1180
|
-
}
|
|
1181
|
-
|
|
1182
|
-
React.useEffect(() => {
|
|
1183
|
-
ensureCss()
|
|
1184
|
-
loadState()
|
|
1185
|
-
}, [])
|
|
1186
|
-
|
|
1187
|
-
const handleSave = async (profile) => {
|
|
1188
|
-
setBusy('saving')
|
|
1189
|
-
try {
|
|
1190
|
-
const res = await fetch('/dsh-remote-workspace/profiles/save', {
|
|
1191
|
-
method: 'POST',
|
|
1192
|
-
headers: { 'Content-Type': 'application/json' },
|
|
1193
|
-
body: JSON.stringify(profile)
|
|
1194
|
-
})
|
|
1195
|
-
if (res.ok) {
|
|
1196
|
-
setEditing(null)
|
|
1197
|
-
setBrowserOpen(false)
|
|
1198
|
-
setTestResult(null)
|
|
1199
|
-
await loadState()
|
|
1200
|
-
}
|
|
1201
|
-
} catch (err) { /* best-effort cleanup */ }
|
|
1202
|
-
setBusy(null)
|
|
1203
|
-
}
|
|
1204
|
-
|
|
1205
|
-
const handleDelete = async (id, name) => {
|
|
1206
|
-
if (!confirm(t('delConfirm', { name: name || id }))) return
|
|
1207
|
-
try {
|
|
1208
|
-
const res = await fetch('/dsh-remote-workspace/profiles/delete', {
|
|
1209
|
-
method: 'POST',
|
|
1210
|
-
headers: { 'Content-Type': 'application/json' },
|
|
1211
|
-
body: JSON.stringify({ id })
|
|
1212
|
-
})
|
|
1213
|
-
if (res.ok) {
|
|
1214
|
-
if (editing && editing.id === id) setEditing(null)
|
|
1215
|
-
await loadState()
|
|
1216
|
-
}
|
|
1217
|
-
} catch (err) { /* best-effort cleanup */ }
|
|
1218
|
-
}
|
|
1219
|
-
|
|
1220
|
-
const handleSetActive = async (id) => {
|
|
1221
|
-
try {
|
|
1222
|
-
const res = await fetch('/dsh-remote-workspace/profiles/active', {
|
|
1223
|
-
method: 'POST',
|
|
1224
|
-
headers: { 'Content-Type': 'application/json' },
|
|
1225
|
-
body: JSON.stringify({ id })
|
|
1226
|
-
})
|
|
1227
|
-
if (res.ok) {
|
|
1228
|
-
await loadState()
|
|
1229
|
-
}
|
|
1230
|
-
} catch (err) { /* best-effort cleanup */ }
|
|
1231
|
-
}
|
|
1232
|
-
|
|
1233
|
-
const handleTest = async (prof) => {
|
|
1234
|
-
setBusy('testing')
|
|
1235
|
-
setTestResult(null)
|
|
1236
|
-
try {
|
|
1237
|
-
const res = await fetch('/dsh-remote-workspace/test', {
|
|
1238
|
-
method: 'POST',
|
|
1239
|
-
headers: { 'Content-Type': 'application/json' },
|
|
1240
|
-
body: JSON.stringify({ profile: prof })
|
|
1241
|
-
})
|
|
1242
|
-
const data = await res.json()
|
|
1243
|
-
setTestResult(data)
|
|
1244
|
-
} catch (err) {
|
|
1245
|
-
setTestResult({ ok: false, error: err.message })
|
|
1246
|
-
}
|
|
1247
|
-
setBusy(null)
|
|
1248
|
-
}
|
|
1249
|
-
|
|
1250
|
-
const openDirectoryBrowser = async (initialPath) => {
|
|
1251
|
-
setBrowserOpen(true)
|
|
1252
|
-
const target = initialPath && initialPath.startsWith('/') ? initialPath : '/'
|
|
1253
|
-
await fetchRemoteDir(target)
|
|
1254
|
-
}
|
|
1255
|
-
|
|
1256
|
-
const fetchRemoteDir = async (dirPath) => {
|
|
1257
|
-
setBrowserLoading(true)
|
|
1258
|
-
try {
|
|
1259
|
-
const res = await fetch('/dsh-remote-workspace/browse', {
|
|
1260
|
-
method: 'POST',
|
|
1261
|
-
headers: { 'Content-Type': 'application/json' },
|
|
1262
|
-
body: JSON.stringify({ profile: editing, remotePath: dirPath })
|
|
1263
|
-
})
|
|
1264
|
-
const data = await res.json()
|
|
1265
|
-
if (data.ok) {
|
|
1266
|
-
setBrowserPath(data.currentPath || dirPath)
|
|
1267
|
-
setBrowserEntries(data.entries || [])
|
|
1268
|
-
}
|
|
1269
|
-
} catch (err) { /* best-effort cleanup */ }
|
|
1270
|
-
setBrowserLoading(false)
|
|
1271
|
-
}
|
|
1272
|
-
|
|
1273
|
-
const handleSync = async (direction) => {
|
|
1274
|
-
setBusy(`sync_${direction}`)
|
|
1275
|
-
setSyncMsg(null)
|
|
1276
|
-
try {
|
|
1277
|
-
const res = await fetch('/dsh-remote-workspace/sync', {
|
|
1278
|
-
method: 'POST',
|
|
1279
|
-
headers: { 'Content-Type': 'application/json' },
|
|
1280
|
-
body: JSON.stringify({ direction })
|
|
1281
|
-
})
|
|
1282
|
-
const data = await res.json()
|
|
1283
|
-
if (data.ok) {
|
|
1284
|
-
const count = (direction === 'push' ? data.pushed?.length : data.pulled?.length) || 0
|
|
1285
|
-
if (data.conflicts && data.conflicts.length > 0) {
|
|
1286
|
-
setSyncMsg({ ok: false, text: t('syncConflict', { count: data.conflicts.length }) })
|
|
1287
|
-
} else {
|
|
1288
|
-
setSyncMsg({ ok: true, text: t('syncOk', { count }) })
|
|
1289
|
-
}
|
|
1290
|
-
} else {
|
|
1291
|
-
setSyncMsg({ ok: false, text: t('syncErr', { err: data.error || 'Unknown error' }) })
|
|
1292
|
-
}
|
|
1293
|
-
} catch (err) {
|
|
1294
|
-
setSyncMsg({ ok: false, text: t('syncErr', { err: err.message }) })
|
|
1295
|
-
}
|
|
1296
|
-
setBusy(null)
|
|
1297
|
-
}
|
|
1298
|
-
|
|
1299
|
-
const handleStartTunnel = async () => {
|
|
1300
|
-
if (!activeId) return
|
|
1301
|
-
setBusy('tunnel_open')
|
|
1302
|
-
setTunnelMsg(null)
|
|
1303
|
-
try {
|
|
1304
|
-
const res = await fetch('/dsh-remote-workspace/tunnels/start', {
|
|
1305
|
-
method: 'POST',
|
|
1306
|
-
headers: { 'Content-Type': 'application/json' },
|
|
1307
|
-
body: JSON.stringify({
|
|
1308
|
-
profileId: activeId,
|
|
1309
|
-
localPort: newTunnelLocal,
|
|
1310
|
-
remotePort: newTunnelRemote
|
|
1311
|
-
})
|
|
1312
|
-
})
|
|
1313
|
-
const data = await res.json()
|
|
1314
|
-
if (data.ok) {
|
|
1315
|
-
setTunnelMsg({ ok: true, text: t('tOpenOk', { localPort: newTunnelLocal, remotePort: newTunnelRemote }) })
|
|
1316
|
-
await loadState()
|
|
1317
|
-
} else {
|
|
1318
|
-
setTunnelMsg({ ok: false, text: t('tOpenErr', { err: data.error || 'Failed' }) })
|
|
1319
|
-
}
|
|
1320
|
-
} catch (err) {
|
|
1321
|
-
setTunnelMsg({ ok: false, text: t('tOpenErr', { err: err.message }) })
|
|
1322
|
-
}
|
|
1323
|
-
setBusy(null)
|
|
1324
|
-
}
|
|
1325
|
-
|
|
1326
|
-
const handleStopTunnel = async (tunnelId) => {
|
|
1327
|
-
try {
|
|
1328
|
-
const res = await fetch('/dsh-remote-workspace/tunnels/stop', {
|
|
1329
|
-
method: 'POST',
|
|
1330
|
-
headers: { 'Content-Type': 'application/json' },
|
|
1331
|
-
body: JSON.stringify({ tunnelId })
|
|
1332
|
-
})
|
|
1333
|
-
if (res.ok) {
|
|
1334
|
-
await loadState()
|
|
1335
|
-
}
|
|
1336
|
-
} catch (err) { /* best-effort cleanup */ }
|
|
1337
|
-
}
|
|
1338
|
-
|
|
1339
|
-
const activeProfile = profiles.find((p) => p.id === activeId)
|
|
1340
|
-
|
|
1341
|
-
return React.createElement(
|
|
1342
|
-
'div',
|
|
1343
|
-
{ className: 'drw-page' },
|
|
1344
|
-
|
|
1345
|
-
// Header with Live Badges (ClineBot Style)
|
|
1346
|
-
React.createElement(
|
|
1347
|
-
'div',
|
|
1348
|
-
{ className: 'drw-header' },
|
|
1349
|
-
React.createElement(
|
|
1350
|
-
'div',
|
|
1351
|
-
{ className: 'drw-header-top' },
|
|
1352
|
-
React.createElement('div', { className: 'drw-page-title' }, '🌐 ' + t('title')),
|
|
1353
|
-
React.createElement(
|
|
1354
|
-
'div',
|
|
1355
|
-
{ className: 'drw-header-badges' },
|
|
1356
|
-
activeProfile
|
|
1357
|
-
? React.createElement('span', { className: 'drw-badge drw-badge-ok' }, t('badgeOnline', { name: activeProfile.name || activeProfile.host, latency: testResult?.latencyMs ?? 15 }))
|
|
1358
|
-
: React.createElement('span', { className: 'drw-badge drw-badge-warn' }, t('badgeOffline')),
|
|
1359
|
-
activeProfile
|
|
1360
|
-
? React.createElement('span', { className: 'drw-badge' }, activeProfile.authType === 'password' ? t('badgePassword') : t('badgeKey'))
|
|
1361
|
-
: null,
|
|
1362
|
-
React.createElement('span', { className: `drw-badge ${tunnels.length > 0 ? 'drw-badge-ok' : ''}` }, t('badgeTunnels', { count: tunnels.length }))
|
|
1363
|
-
)
|
|
1364
|
-
),
|
|
1365
|
-
React.createElement('div', { className: 'drw-page-sub' }, t('subtitle'))
|
|
1366
|
-
),
|
|
1367
|
-
|
|
1368
|
-
// Section 1: SSH Profiles Table & Management
|
|
1161
|
+
function ProfilesPanel(panel) {
|
|
1162
|
+
const {
|
|
1163
|
+
t,
|
|
1164
|
+
editing,
|
|
1165
|
+
setEditing,
|
|
1166
|
+
setTestResult,
|
|
1167
|
+
profiles,
|
|
1168
|
+
activeId,
|
|
1169
|
+
busy,
|
|
1170
|
+
handleSave,
|
|
1171
|
+
testResult,
|
|
1172
|
+
handleTest,
|
|
1173
|
+
handleSetActive,
|
|
1174
|
+
handleDelete,
|
|
1175
|
+
browserOpen,
|
|
1176
|
+
setBrowserOpen,
|
|
1177
|
+
browserPath,
|
|
1178
|
+
browserEntries,
|
|
1179
|
+
browserLoading,
|
|
1180
|
+
openDirectoryBrowser,
|
|
1181
|
+
fetchRemoteDir,
|
|
1182
|
+
showPassword,
|
|
1183
|
+
setShowPassword,
|
|
1184
|
+
} = panel
|
|
1185
|
+
return (
|
|
1369
1186
|
React.createElement(
|
|
1370
1187
|
'div',
|
|
1371
1188
|
{ className: 'drw-card' },
|
|
@@ -1826,104 +1643,67 @@ window.__ModuleLoader__.load({
|
|
|
1826
1643
|
: !editing
|
|
1827
1644
|
? React.createElement('div', { style: { fontSize: '13px', color: 'var(--dsw-alias-label-secondary)' } }, t('noHosts'))
|
|
1828
1645
|
: null
|
|
1829
|
-
)
|
|
1646
|
+
)
|
|
1647
|
+
)
|
|
1648
|
+
}
|
|
1830
1649
|
|
|
1831
|
-
|
|
1650
|
+
function SyncPanel(panel) {
|
|
1651
|
+
const {
|
|
1652
|
+
t,
|
|
1653
|
+
busy,
|
|
1654
|
+
activeProfile,
|
|
1655
|
+
handleSync,
|
|
1656
|
+
syncMsg,
|
|
1657
|
+
autoSync,
|
|
1658
|
+
setAutoSync,
|
|
1659
|
+
reportAction,
|
|
1660
|
+
setActionError,
|
|
1661
|
+
} = panel
|
|
1662
|
+
return (
|
|
1832
1663
|
activeProfile
|
|
1833
1664
|
? React.createElement(
|
|
1834
1665
|
'div',
|
|
1835
1666
|
{ className: 'drw-card' },
|
|
1667
|
+
React.createElement('div', { className: 'drw-card-title' }, t('syncTitle')),
|
|
1668
|
+
React.createElement('div', { className: 'drw-card-desc' }, t('syncDesc')),
|
|
1669
|
+
|
|
1836
1670
|
React.createElement(
|
|
1837
1671
|
'div',
|
|
1838
|
-
{
|
|
1839
|
-
React.createElement(
|
|
1672
|
+
{ style: { display: 'flex', alignItems: 'center', justifyContent: 'space-between', padding: '10px 14px', background: 'var(--dsw-alias-bg-layer-2)', borderRadius: '8px', border: '1px solid var(--dsw-alias-border-l2)', marginBottom: '12px' } },
|
|
1673
|
+
React.createElement(
|
|
1674
|
+
'div',
|
|
1675
|
+
{ style: { display: 'flex', flexDirection: 'column', gap: '2px' } },
|
|
1676
|
+
React.createElement('span', { style: { fontWeight: 600, fontSize: '13px' } }, t('autoSyncTitle')),
|
|
1677
|
+
React.createElement('span', { style: { fontSize: '11px', color: 'var(--dsw-alias-label-secondary)' } }, t('autoSyncDesc'))
|
|
1678
|
+
),
|
|
1840
1679
|
React.createElement(
|
|
1841
1680
|
'button',
|
|
1842
1681
|
{
|
|
1843
1682
|
type: 'button',
|
|
1844
|
-
className: 'drw-btn'
|
|
1845
|
-
style: {
|
|
1846
|
-
|
|
1847
|
-
|
|
1683
|
+
className: `drw-btn ${autoSync ? 'drw-btn-primary' : ''}`,
|
|
1684
|
+
style: { padding: '4px 12px', fontSize: '12px' },
|
|
1685
|
+
onClick: async () => {
|
|
1686
|
+
const next = !autoSync
|
|
1687
|
+
try {
|
|
1688
|
+
const res = await fetch('/dsh-remote-workspace/watcher/toggle', {
|
|
1689
|
+
method: 'POST',
|
|
1690
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1691
|
+
body: JSON.stringify({ enabled: next })
|
|
1692
|
+
})
|
|
1693
|
+
const data = await reportAction(res)
|
|
1694
|
+
if (data && data.ok) setAutoSync(data.autoSync)
|
|
1695
|
+
} catch (err) {
|
|
1696
|
+
setActionError(err && err.message ? err.message : t('actionFailed'))
|
|
1697
|
+
}
|
|
1698
|
+
}
|
|
1848
1699
|
},
|
|
1849
|
-
|
|
1700
|
+
autoSync ? '⚡ Active (ON)' : '⚪ Off'
|
|
1850
1701
|
)
|
|
1851
1702
|
),
|
|
1852
|
-
|
|
1703
|
+
|
|
1853
1704
|
React.createElement(
|
|
1854
1705
|
'div',
|
|
1855
|
-
{ className: 'drw-
|
|
1856
|
-
React.createElement(
|
|
1857
|
-
'div',
|
|
1858
|
-
{ className: 'drw-stat-box' },
|
|
1859
|
-
React.createElement('div', { className: 'drw-stat-val' }, activeProfile.name || activeProfile.host),
|
|
1860
|
-
React.createElement('div', { className: 'drw-stat-lbl' }, `SSH: ${activeProfile.username}@${activeProfile.host}:${activeProfile.port || 22}`)
|
|
1861
|
-
),
|
|
1862
|
-
React.createElement(
|
|
1863
|
-
'div',
|
|
1864
|
-
{ className: 'drw-stat-box' },
|
|
1865
|
-
React.createElement('div', { className: 'drw-stat-val' }, activeProfile.remoteWorkspace || '—'),
|
|
1866
|
-
React.createElement('div', { className: 'drw-stat-lbl' }, 'Remote Workspace')
|
|
1867
|
-
),
|
|
1868
|
-
React.createElement(
|
|
1869
|
-
'div',
|
|
1870
|
-
{ className: 'drw-stat-box' },
|
|
1871
|
-
React.createElement('div', { className: 'drw-stat-val' }, activeProfile.localMirrorPath || '—'),
|
|
1872
|
-
React.createElement('div', { className: 'drw-stat-lbl' }, 'Local Mirror Path')
|
|
1873
|
-
),
|
|
1874
|
-
React.createElement(
|
|
1875
|
-
'div',
|
|
1876
|
-
{ className: 'drw-stat-box' },
|
|
1877
|
-
React.createElement('div', { className: 'drw-stat-val', style: { color: 'var(--dsw-alias-state-success-primary)' } }, 'Ready ✓'),
|
|
1878
|
-
React.createElement('div', { className: 'drw-stat-lbl' }, activeProfile.authType === 'password' ? 'Password Auth' : 'SSH Key Auth')
|
|
1879
|
-
)
|
|
1880
|
-
)
|
|
1881
|
-
)
|
|
1882
|
-
: null,
|
|
1883
|
-
|
|
1884
|
-
// Section 3: Mirror Sync (ClineBot Action Card style)
|
|
1885
|
-
activeProfile
|
|
1886
|
-
? React.createElement(
|
|
1887
|
-
'div',
|
|
1888
|
-
{ className: 'drw-card' },
|
|
1889
|
-
React.createElement('div', { className: 'drw-card-title' }, t('syncTitle')),
|
|
1890
|
-
React.createElement('div', { className: 'drw-card-desc' }, t('syncDesc')),
|
|
1891
|
-
|
|
1892
|
-
React.createElement(
|
|
1893
|
-
'div',
|
|
1894
|
-
{ style: { display: 'flex', alignItems: 'center', justifyContent: 'space-between', padding: '10px 14px', background: 'var(--dsw-alias-bg-layer-2)', borderRadius: '8px', border: '1px solid var(--dsw-alias-border-l2)', marginBottom: '12px' } },
|
|
1895
|
-
React.createElement(
|
|
1896
|
-
'div',
|
|
1897
|
-
{ style: { display: 'flex', flexDirection: 'column', gap: '2px' } },
|
|
1898
|
-
React.createElement('span', { style: { fontWeight: 600, fontSize: '13px' } }, t('autoSyncTitle')),
|
|
1899
|
-
React.createElement('span', { style: { fontSize: '11px', color: 'var(--dsw-alias-label-secondary)' } }, t('autoSyncDesc'))
|
|
1900
|
-
),
|
|
1901
|
-
React.createElement(
|
|
1902
|
-
'button',
|
|
1903
|
-
{
|
|
1904
|
-
type: 'button',
|
|
1905
|
-
className: `drw-btn ${autoSync ? 'drw-btn-primary' : ''}`,
|
|
1906
|
-
style: { padding: '4px 12px', fontSize: '12px' },
|
|
1907
|
-
onClick: async () => {
|
|
1908
|
-
const next = !autoSync
|
|
1909
|
-
try {
|
|
1910
|
-
const res = await fetch('/dsh-remote-workspace/watcher/toggle', {
|
|
1911
|
-
method: 'POST',
|
|
1912
|
-
headers: { 'Content-Type': 'application/json' },
|
|
1913
|
-
body: JSON.stringify({ enabled: next })
|
|
1914
|
-
})
|
|
1915
|
-
const d = await res.json()
|
|
1916
|
-
if (d.ok) setAutoSync(d.autoSync)
|
|
1917
|
-
} catch (err) { /* best-effort cleanup */ }
|
|
1918
|
-
}
|
|
1919
|
-
},
|
|
1920
|
-
autoSync ? '⚡ Active (ON)' : '⚪ Off'
|
|
1921
|
-
)
|
|
1922
|
-
),
|
|
1923
|
-
|
|
1924
|
-
React.createElement(
|
|
1925
|
-
'div',
|
|
1926
|
-
{ className: 'drw-row' },
|
|
1706
|
+
{ className: 'drw-row' },
|
|
1927
1707
|
React.createElement(
|
|
1928
1708
|
'button',
|
|
1929
1709
|
{
|
|
@@ -1958,9 +1738,25 @@ window.__ModuleLoader__.load({
|
|
|
1958
1738
|
)
|
|
1959
1739
|
: null
|
|
1960
1740
|
)
|
|
1961
|
-
: null
|
|
1741
|
+
: null
|
|
1742
|
+
)
|
|
1743
|
+
}
|
|
1962
1744
|
|
|
1963
|
-
|
|
1745
|
+
function TunnelsPanel(panel) {
|
|
1746
|
+
const {
|
|
1747
|
+
t,
|
|
1748
|
+
busy,
|
|
1749
|
+
activeProfile,
|
|
1750
|
+
newTunnelLocal,
|
|
1751
|
+
setNewTunnelLocal,
|
|
1752
|
+
newTunnelRemote,
|
|
1753
|
+
setNewTunnelRemote,
|
|
1754
|
+
handleStartTunnel,
|
|
1755
|
+
tunnelMsg,
|
|
1756
|
+
tunnels,
|
|
1757
|
+
handleStopTunnel,
|
|
1758
|
+
} = panel
|
|
1759
|
+
return (
|
|
1964
1760
|
React.createElement(
|
|
1965
1761
|
'div',
|
|
1966
1762
|
{ className: 'drw-card' },
|
|
@@ -2077,6 +1873,330 @@ window.__ModuleLoader__.load({
|
|
|
2077
1873
|
)
|
|
2078
1874
|
}
|
|
2079
1875
|
|
|
1876
|
+
function SettingsView(props) {
|
|
1877
|
+
const [activeTab, setActiveTab] = React.useState('profiles')
|
|
1878
|
+
const [healthData, setHealthData] = React.useState(null)
|
|
1879
|
+
const [autoSync, setAutoSync] = React.useState(false)
|
|
1880
|
+
|
|
1881
|
+
const t = props.t || (props.locale === 'zh' ? makeT(zh, en) : makeT(en, zh))
|
|
1882
|
+
const [profiles, setProfiles] = React.useState([])
|
|
1883
|
+
const [activeId, setActiveId] = React.useState(null)
|
|
1884
|
+
const [tunnels, setTunnels] = React.useState([])
|
|
1885
|
+
const [editing, setEditing] = React.useState(null)
|
|
1886
|
+
const [busy, setBusy] = React.useState(null)
|
|
1887
|
+
const [testResult, setTestResult] = React.useState(null)
|
|
1888
|
+
const [actionError, setActionError] = React.useState('')
|
|
1889
|
+
const [syncMsg, setSyncMsg] = React.useState(null)
|
|
1890
|
+
const [tunnelMsg, setTunnelMsg] = React.useState(null)
|
|
1891
|
+
const [showPassword, setShowPassword] = React.useState(false)
|
|
1892
|
+
|
|
1893
|
+
// Tunnel creation inputs
|
|
1894
|
+
const [newTunnelLocal, setNewTunnelLocal] = React.useState('3000')
|
|
1895
|
+
const [newTunnelRemote, setNewTunnelRemote] = React.useState('3000')
|
|
1896
|
+
|
|
1897
|
+
// Remote Directory Browser State
|
|
1898
|
+
const [browserOpen, setBrowserOpen] = React.useState(false)
|
|
1899
|
+
const [browserPath, setBrowserPath] = React.useState('/')
|
|
1900
|
+
const [browserEntries, setBrowserEntries] = React.useState([])
|
|
1901
|
+
const [browserLoading, setBrowserLoading] = React.useState(false)
|
|
1902
|
+
|
|
1903
|
+
|
|
1904
|
+
function actionErrorText(status, data) {
|
|
1905
|
+
if (status >= 200 && status < 300 && data && data.ok !== false) return ''
|
|
1906
|
+
if (data && typeof data.error === 'string' && data.error) return data.error
|
|
1907
|
+
return 'HTTP ' + status
|
|
1908
|
+
}
|
|
1909
|
+
|
|
1910
|
+
const reportAction = async (res) => {
|
|
1911
|
+
let data = null
|
|
1912
|
+
try { data = await res.json() } catch (err) { data = null }
|
|
1913
|
+
const text = actionErrorText(res.status, data)
|
|
1914
|
+
setActionError(text ? ((data && data.error) ? text : t('actionFailed')) : '')
|
|
1915
|
+
return text ? null : data
|
|
1916
|
+
}
|
|
1917
|
+
|
|
1918
|
+
const loadState = async () => {
|
|
1919
|
+
try {
|
|
1920
|
+
const res = await fetch('/dsh-remote-workspace/state')
|
|
1921
|
+
const data = await reportAction(res)
|
|
1922
|
+
if (data && data.ok) {
|
|
1923
|
+
setProfiles(data.profiles || [])
|
|
1924
|
+
setActiveId(data.activeId || null)
|
|
1925
|
+
setTunnels(data.tunnels || [])
|
|
1926
|
+
setAutoSync(Boolean(data.autoSync))
|
|
1927
|
+
if (data.activeId) {
|
|
1928
|
+
fetch(`/dsh-remote-workspace/health?profileId=${data.activeId}`).then(r => r.json()).then(h => { if (h.ok) setHealthData(h.health) }).catch(() => {})
|
|
1929
|
+
}
|
|
1930
|
+
}
|
|
1931
|
+
} catch (err) {
|
|
1932
|
+
setActionError(err && err.message ? err.message : t('actionFailed'))
|
|
1933
|
+
}
|
|
1934
|
+
}
|
|
1935
|
+
|
|
1936
|
+
React.useEffect(() => {
|
|
1937
|
+
ensureCss()
|
|
1938
|
+
loadState()
|
|
1939
|
+
}, [])
|
|
1940
|
+
|
|
1941
|
+
const handleSave = async (profile) => {
|
|
1942
|
+
setBusy('saving')
|
|
1943
|
+
try {
|
|
1944
|
+
const res = await fetch('/dsh-remote-workspace/profiles/save', {
|
|
1945
|
+
method: 'POST',
|
|
1946
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1947
|
+
body: JSON.stringify(profile)
|
|
1948
|
+
})
|
|
1949
|
+
const data = await reportAction(res)
|
|
1950
|
+
if (data) {
|
|
1951
|
+
setEditing(null)
|
|
1952
|
+
setBrowserOpen(false)
|
|
1953
|
+
setTestResult(null)
|
|
1954
|
+
await loadState()
|
|
1955
|
+
}
|
|
1956
|
+
} catch (err) {
|
|
1957
|
+
setActionError(err && err.message ? err.message : t('actionFailed'))
|
|
1958
|
+
}
|
|
1959
|
+
setBusy(null)
|
|
1960
|
+
}
|
|
1961
|
+
|
|
1962
|
+
const handleDelete = async (id, name) => {
|
|
1963
|
+
if (!confirm(t('delConfirm', { name: name || id }))) return
|
|
1964
|
+
try {
|
|
1965
|
+
const res = await fetch('/dsh-remote-workspace/profiles/delete', {
|
|
1966
|
+
method: 'POST',
|
|
1967
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1968
|
+
body: JSON.stringify({ id })
|
|
1969
|
+
})
|
|
1970
|
+
const data = await reportAction(res)
|
|
1971
|
+
if (data) {
|
|
1972
|
+
if (editing && editing.id === id) setEditing(null)
|
|
1973
|
+
await loadState()
|
|
1974
|
+
}
|
|
1975
|
+
} catch (err) {
|
|
1976
|
+
setActionError(err && err.message ? err.message : t('actionFailed'))
|
|
1977
|
+
}
|
|
1978
|
+
}
|
|
1979
|
+
|
|
1980
|
+
const handleSetActive = async (id) => {
|
|
1981
|
+
try {
|
|
1982
|
+
const res = await fetch('/dsh-remote-workspace/profiles/active', {
|
|
1983
|
+
method: 'POST',
|
|
1984
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1985
|
+
body: JSON.stringify({ id })
|
|
1986
|
+
})
|
|
1987
|
+
const data = await reportAction(res)
|
|
1988
|
+
if (data) await loadState()
|
|
1989
|
+
} catch (err) {
|
|
1990
|
+
setActionError(err && err.message ? err.message : t('actionFailed'))
|
|
1991
|
+
}
|
|
1992
|
+
}
|
|
1993
|
+
|
|
1994
|
+
const handleTest = async (prof) => {
|
|
1995
|
+
setBusy('testing')
|
|
1996
|
+
setTestResult(null)
|
|
1997
|
+
try {
|
|
1998
|
+
const res = await fetch('/dsh-remote-workspace/test', {
|
|
1999
|
+
method: 'POST',
|
|
2000
|
+
headers: { 'Content-Type': 'application/json' },
|
|
2001
|
+
body: JSON.stringify(prof)
|
|
2002
|
+
})
|
|
2003
|
+
const data = await res.json()
|
|
2004
|
+
setTestResult(data)
|
|
2005
|
+
} catch (err) {
|
|
2006
|
+
setTestResult({ ok: false, error: err.message })
|
|
2007
|
+
}
|
|
2008
|
+
setBusy(null)
|
|
2009
|
+
}
|
|
2010
|
+
|
|
2011
|
+
const openDirectoryBrowser = async (initialPath) => {
|
|
2012
|
+
setBrowserOpen(true)
|
|
2013
|
+
const target = initialPath && initialPath.startsWith('/') ? initialPath : '/'
|
|
2014
|
+
await fetchRemoteDir(target)
|
|
2015
|
+
}
|
|
2016
|
+
|
|
2017
|
+
const fetchRemoteDir = async (dirPath) => {
|
|
2018
|
+
setBrowserLoading(true)
|
|
2019
|
+
try {
|
|
2020
|
+
const res = await fetch('/dsh-remote-workspace/browse', {
|
|
2021
|
+
method: 'POST',
|
|
2022
|
+
headers: { 'Content-Type': 'application/json' },
|
|
2023
|
+
body: JSON.stringify({ profile: editing, remotePath: dirPath })
|
|
2024
|
+
})
|
|
2025
|
+
const data = await reportAction(res)
|
|
2026
|
+
if (data && data.ok) {
|
|
2027
|
+
setBrowserPath(data.currentPath || dirPath)
|
|
2028
|
+
setBrowserEntries(data.entries || [])
|
|
2029
|
+
}
|
|
2030
|
+
} catch (err) {
|
|
2031
|
+
setActionError(err && err.message ? err.message : t('actionFailed'))
|
|
2032
|
+
}
|
|
2033
|
+
setBrowserLoading(false)
|
|
2034
|
+
}
|
|
2035
|
+
|
|
2036
|
+
const handleSync = async (direction) => {
|
|
2037
|
+
setBusy(`sync_${direction}`)
|
|
2038
|
+
setSyncMsg(null)
|
|
2039
|
+
try {
|
|
2040
|
+
const res = await fetch('/dsh-remote-workspace/sync', {
|
|
2041
|
+
method: 'POST',
|
|
2042
|
+
headers: { 'Content-Type': 'application/json' },
|
|
2043
|
+
body: JSON.stringify({ direction })
|
|
2044
|
+
})
|
|
2045
|
+
const data = await res.json()
|
|
2046
|
+
if (data.ok) {
|
|
2047
|
+
const count = (direction === 'push' ? data.pushed?.length : data.pulled?.length) || 0
|
|
2048
|
+
if (data.conflicts && data.conflicts.length > 0) {
|
|
2049
|
+
setSyncMsg({ ok: false, text: t('syncConflict', { count: data.conflicts.length }) })
|
|
2050
|
+
} else {
|
|
2051
|
+
setSyncMsg({ ok: true, text: t('syncOk', { count }) })
|
|
2052
|
+
}
|
|
2053
|
+
} else {
|
|
2054
|
+
setSyncMsg({ ok: false, text: t('syncErr', { err: data.error || 'Unknown error' }) })
|
|
2055
|
+
}
|
|
2056
|
+
} catch (err) {
|
|
2057
|
+
setSyncMsg({ ok: false, text: t('syncErr', { err: err.message }) })
|
|
2058
|
+
}
|
|
2059
|
+
setBusy(null)
|
|
2060
|
+
}
|
|
2061
|
+
|
|
2062
|
+
const handleStartTunnel = async () => {
|
|
2063
|
+
if (!activeId) return
|
|
2064
|
+
setBusy('tunnel_open')
|
|
2065
|
+
setTunnelMsg(null)
|
|
2066
|
+
try {
|
|
2067
|
+
const res = await fetch('/dsh-remote-workspace/tunnels/start', {
|
|
2068
|
+
method: 'POST',
|
|
2069
|
+
headers: { 'Content-Type': 'application/json' },
|
|
2070
|
+
body: JSON.stringify({
|
|
2071
|
+
profileId: activeId,
|
|
2072
|
+
localPort: newTunnelLocal,
|
|
2073
|
+
remotePort: newTunnelRemote
|
|
2074
|
+
})
|
|
2075
|
+
})
|
|
2076
|
+
const data = await res.json()
|
|
2077
|
+
if (data.ok) {
|
|
2078
|
+
setTunnelMsg({ ok: true, text: t('tOpenOk', { localPort: newTunnelLocal, remotePort: newTunnelRemote }) })
|
|
2079
|
+
await loadState()
|
|
2080
|
+
} else {
|
|
2081
|
+
setTunnelMsg({ ok: false, text: t('tOpenErr', { err: data.error || 'Failed' }) })
|
|
2082
|
+
}
|
|
2083
|
+
} catch (err) {
|
|
2084
|
+
setTunnelMsg({ ok: false, text: t('tOpenErr', { err: err.message }) })
|
|
2085
|
+
}
|
|
2086
|
+
setBusy(null)
|
|
2087
|
+
}
|
|
2088
|
+
|
|
2089
|
+
const handleStopTunnel = async (tunnelId) => {
|
|
2090
|
+
try {
|
|
2091
|
+
const res = await fetch('/dsh-remote-workspace/tunnels/stop', {
|
|
2092
|
+
method: 'POST',
|
|
2093
|
+
headers: { 'Content-Type': 'application/json' },
|
|
2094
|
+
body: JSON.stringify({ tunnelId })
|
|
2095
|
+
})
|
|
2096
|
+
const data = await reportAction(res)
|
|
2097
|
+
if (data) await loadState()
|
|
2098
|
+
} catch (err) {
|
|
2099
|
+
setActionError(err && err.message ? err.message : t('actionFailed'))
|
|
2100
|
+
}
|
|
2101
|
+
}
|
|
2102
|
+
|
|
2103
|
+
const activeProfile = profiles.find((p) => p.id === activeId)
|
|
2104
|
+
|
|
2105
|
+
return React.createElement(
|
|
2106
|
+
'div',
|
|
2107
|
+
{ className: 'drw-page' },
|
|
2108
|
+
|
|
2109
|
+
// Header with Live Badges (ClineBot Style)
|
|
2110
|
+
React.createElement(
|
|
2111
|
+
'div',
|
|
2112
|
+
{ className: 'drw-header' },
|
|
2113
|
+
React.createElement(
|
|
2114
|
+
'div',
|
|
2115
|
+
{ className: 'drw-header-top' },
|
|
2116
|
+
React.createElement('div', { className: 'drw-page-title' }, '🌐 ' + t('title')),
|
|
2117
|
+
React.createElement(
|
|
2118
|
+
'div',
|
|
2119
|
+
{ className: 'drw-header-badges' },
|
|
2120
|
+
activeProfile
|
|
2121
|
+
? React.createElement('span', { className: 'drw-badge drw-badge-ok' }, t('badgeOnline', { name: activeProfile.name || activeProfile.host, latency: testResult?.latencyMs ?? 15 }))
|
|
2122
|
+
: React.createElement('span', { className: 'drw-badge drw-badge-warn' }, t('badgeOffline')),
|
|
2123
|
+
activeProfile
|
|
2124
|
+
? React.createElement('span', { className: 'drw-badge' }, activeProfile.authType === 'password' ? t('badgePassword') : t('badgeKey'))
|
|
2125
|
+
: null,
|
|
2126
|
+
React.createElement('span', { className: `drw-badge ${tunnels.length > 0 ? 'drw-badge-ok' : ''}` }, t('badgeTunnels', { count: tunnels.length }))
|
|
2127
|
+
)
|
|
2128
|
+
),
|
|
2129
|
+
React.createElement('div', { className: 'drw-page-sub' }, t('subtitle')),
|
|
2130
|
+
actionError
|
|
2131
|
+
? React.createElement('div', { className: 'drw-badge drw-badge-err', role: 'alert' }, actionError)
|
|
2132
|
+
: null
|
|
2133
|
+
),
|
|
2134
|
+
|
|
2135
|
+
|
|
2136
|
+
React.createElement(ProfilesPanel, { t, editing, setEditing, setTestResult, profiles, activeId, busy, handleSave, testResult, handleTest, handleSetActive, handleDelete, browserOpen, setBrowserOpen, browserPath, browserEntries, browserLoading, openDirectoryBrowser, fetchRemoteDir, showPassword, setShowPassword }),
|
|
2137
|
+
|
|
2138
|
+
|
|
2139
|
+
// Section 2: Active Connection Diagnostics (ClineBot Card 4 style)
|
|
2140
|
+
activeProfile
|
|
2141
|
+
? React.createElement(
|
|
2142
|
+
'div',
|
|
2143
|
+
{ className: 'drw-card' },
|
|
2144
|
+
React.createElement(
|
|
2145
|
+
'div',
|
|
2146
|
+
{ className: 'drw-card-title' },
|
|
2147
|
+
React.createElement('span', null, t('diagTitle')),
|
|
2148
|
+
React.createElement(
|
|
2149
|
+
'button',
|
|
2150
|
+
{
|
|
2151
|
+
type: 'button',
|
|
2152
|
+
className: 'drw-btn',
|
|
2153
|
+
style: { fontSize: '11px', padding: '3px 9px' },
|
|
2154
|
+
disabled: busy === 'testing',
|
|
2155
|
+
onClick: () => handleTest(activeProfile)
|
|
2156
|
+
},
|
|
2157
|
+
busy === 'testing' ? t('testing') : t('diagProbeBtn')
|
|
2158
|
+
)
|
|
2159
|
+
),
|
|
2160
|
+
React.createElement('div', { className: 'drw-card-desc' }, t('diagDesc')),
|
|
2161
|
+
React.createElement(
|
|
2162
|
+
'div',
|
|
2163
|
+
{ className: 'drw-grid-4' },
|
|
2164
|
+
React.createElement(
|
|
2165
|
+
'div',
|
|
2166
|
+
{ className: 'drw-stat-box' },
|
|
2167
|
+
React.createElement('div', { className: 'drw-stat-val' }, activeProfile.name || activeProfile.host),
|
|
2168
|
+
React.createElement('div', { className: 'drw-stat-lbl' }, `SSH: ${activeProfile.username}@${activeProfile.host}:${activeProfile.port || 22}`)
|
|
2169
|
+
),
|
|
2170
|
+
React.createElement(
|
|
2171
|
+
'div',
|
|
2172
|
+
{ className: 'drw-stat-box' },
|
|
2173
|
+
React.createElement('div', { className: 'drw-stat-val' }, activeProfile.remoteWorkspace || '—'),
|
|
2174
|
+
React.createElement('div', { className: 'drw-stat-lbl' }, 'Remote Workspace')
|
|
2175
|
+
),
|
|
2176
|
+
React.createElement(
|
|
2177
|
+
'div',
|
|
2178
|
+
{ className: 'drw-stat-box' },
|
|
2179
|
+
React.createElement('div', { className: 'drw-stat-val' }, activeProfile.localMirrorPath || '—'),
|
|
2180
|
+
React.createElement('div', { className: 'drw-stat-lbl' }, 'Local Mirror Path')
|
|
2181
|
+
),
|
|
2182
|
+
React.createElement(
|
|
2183
|
+
'div',
|
|
2184
|
+
{ className: 'drw-stat-box' },
|
|
2185
|
+
React.createElement('div', { className: 'drw-stat-val', style: { color: 'var(--dsw-alias-state-success-primary)' } }, 'Ready ✓'),
|
|
2186
|
+
React.createElement('div', { className: 'drw-stat-lbl' }, activeProfile.authType === 'password' ? 'Password Auth' : 'SSH Key Auth')
|
|
2187
|
+
)
|
|
2188
|
+
)
|
|
2189
|
+
)
|
|
2190
|
+
: null,
|
|
2191
|
+
|
|
2192
|
+
|
|
2193
|
+
React.createElement(SyncPanel, { t, busy, activeProfile, handleSync, syncMsg, autoSync, setAutoSync, reportAction, setActionError }),
|
|
2194
|
+
|
|
2195
|
+
React.createElement(TunnelsPanel, { t, busy, activeProfile, newTunnelLocal, setNewTunnelLocal, newTunnelRemote, setNewTunnelRemote, handleStartTunnel, tunnelMsg, tunnels, handleStopTunnel }),
|
|
2196
|
+
|
|
2197
|
+
)
|
|
2198
|
+
}
|
|
2199
|
+
|
|
2080
2200
|
// Accordion item for Settings -> Plugins section
|
|
2081
2201
|
|
|
2082
2202
|
function UpdaterSection(props) {
|
|
@@ -2154,8 +2274,8 @@ window.__ModuleLoader__.load({
|
|
|
2154
2274
|
React.createElement(
|
|
2155
2275
|
'div',
|
|
2156
2276
|
null,
|
|
2157
|
-
React.createElement('span', { style: { fontWeight: 500 } }, 'v' +
|
|
2158
|
-
updateStatus?.
|
|
2277
|
+
React.createElement('span', { style: { fontWeight: 500 } }, (updateStatus?.currentVersion ? 'v' + updateStatus.currentVersion : t('versionUnknown'))),
|
|
2278
|
+
updateStatus?.updateAvailable
|
|
2159
2279
|
? React.createElement('span', { style: { marginLeft: '8px', color: 'var(--dsw-alias-state-warning-text)' } }, '→ v' + updateStatus.latestVersion)
|
|
2160
2280
|
: null,
|
|
2161
2281
|
msg
|
|
@@ -2198,10 +2318,34 @@ window.__ModuleLoader__.load({
|
|
|
2198
2318
|
const [open, setOpen] = React.useState(!!page)
|
|
2199
2319
|
const t = props.t || (props.locale === 'zh' ? makeT(zh, en) : makeT(en, zh))
|
|
2200
2320
|
|
|
2321
|
+
const [conn, setConn] = React.useState('loading')
|
|
2322
|
+
|
|
2201
2323
|
React.useEffect(() => {
|
|
2202
2324
|
ensureCss()
|
|
2203
2325
|
}, [])
|
|
2204
2326
|
|
|
2327
|
+
React.useEffect(() => {
|
|
2328
|
+
let cancelled = false
|
|
2329
|
+
fetch('/dsh-remote-workspace/state')
|
|
2330
|
+
.then((res) => {
|
|
2331
|
+
if (!res.ok) throw new Error('HTTP ' + res.status)
|
|
2332
|
+
return res.json()
|
|
2333
|
+
})
|
|
2334
|
+
.then((data) => {
|
|
2335
|
+
if (cancelled) return
|
|
2336
|
+
if (!data || data.ok === false) {
|
|
2337
|
+
setConn('error')
|
|
2338
|
+
return
|
|
2339
|
+
}
|
|
2340
|
+
const active = (data.profiles || []).find((item) => item.id === data.activeId)
|
|
2341
|
+
setConn(active ? 'ready' : 'empty')
|
|
2342
|
+
})
|
|
2343
|
+
.catch(() => {
|
|
2344
|
+
if (!cancelled) setConn('error')
|
|
2345
|
+
})
|
|
2346
|
+
return () => { cancelled = true }
|
|
2347
|
+
}, [])
|
|
2348
|
+
|
|
2205
2349
|
// Row seat (plugins.row.config): the host page draws title/icon/crumb and the
|
|
2206
2350
|
// padding, so the summary is a one-liner and the page drops our card chrome.
|
|
2207
2351
|
if (props && props.view === 'summary') {
|
|
@@ -2235,11 +2379,22 @@ window.__ModuleLoader__.load({
|
|
|
2235
2379
|
React.createElement('div', { style: { fontWeight: 600, fontSize: '15px', color: 'var(--dsw-alias-label-primary)' } }, t('title')),
|
|
2236
2380
|
React.createElement('div', { style: { fontSize: '13px', color: 'var(--dsw-alias-label-secondary)' } }, t('subtitle'))
|
|
2237
2381
|
),
|
|
2238
|
-
React.createElement('span', {
|
|
2382
|
+
React.createElement('span', {
|
|
2383
|
+
className: 'drw-badge ' + (
|
|
2384
|
+
conn === 'ready' ? 'drw-badge-ok'
|
|
2385
|
+
: conn === 'error' ? 'drw-badge-err'
|
|
2386
|
+
: conn === 'empty' ? 'drw-badge-warn' : ''
|
|
2387
|
+
),
|
|
2388
|
+
style: { marginRight: '14px' }
|
|
2389
|
+
}, t(
|
|
2390
|
+
conn === 'ready' ? 'statusReady'
|
|
2391
|
+
: conn === 'error' ? 'statusError'
|
|
2392
|
+
: conn === 'empty' ? 'statusEmpty' : 'statusLoading'
|
|
2393
|
+
)),
|
|
2239
2394
|
React.createElement(
|
|
2240
2395
|
'span',
|
|
2241
2396
|
{ style: { transform: open ? 'rotate(180deg)' : 'none', transition: 'transform .16s' } },
|
|
2242
|
-
React.createElement(FallbackChevron)
|
|
2397
|
+
React.createElement(ChevronIcon || FallbackChevron)
|
|
2243
2398
|
)
|
|
2244
2399
|
),
|
|
2245
2400
|
(page || open)
|
|
@@ -2301,16 +2456,28 @@ window.__ModuleLoader__.load({
|
|
|
2301
2456
|
|
|
2302
2457
|
if (ctx.slots && typeof ctx.slots.inject === 'function') {
|
|
2303
2458
|
// Plugin-list seat (plugins.item) first: the seat the current core
|
|
2304
|
-
// (0.1.6-alpha.2) renders as the plugin's own page with its configuration.
|
|
2305
|
-
// label
|
|
2306
|
-
//
|
|
2459
|
+
// (0.1.6-alpha.2) renders as the plugin's own page with its configuration.
|
|
2460
|
+
// The label getter runs during page render. A locale-service lookup there
|
|
2461
|
+
// crashes the client batch, so the title is read from the dictionaries
|
|
2462
|
+
// already loaded in this bundle. English is the result when the active
|
|
2463
|
+
// language cannot be read.
|
|
2307
2464
|
ctx.slots.inject('plugins.item', () =>
|
|
2308
2465
|
ctx.slots.register(
|
|
2309
2466
|
{
|
|
2310
2467
|
name: 'plugins.item',
|
|
2311
2468
|
id: ROW_ID,
|
|
2312
2469
|
order: 60,
|
|
2313
|
-
label: () =>
|
|
2470
|
+
label: () => {
|
|
2471
|
+
try {
|
|
2472
|
+
const lang = ctx.locale && typeof ctx.locale.getLocale === 'function'
|
|
2473
|
+
? ctx.locale.getLocale()
|
|
2474
|
+
: ''
|
|
2475
|
+
if (typeof lang === 'string' && (lang === 'zh' || lang.indexOf('zh') === 0)) return zh.title
|
|
2476
|
+
} catch (err) {
|
|
2477
|
+
return en.title
|
|
2478
|
+
}
|
|
2479
|
+
return en.title
|
|
2480
|
+
},
|
|
2314
2481
|
locale: NS,
|
|
2315
2482
|
inject: () => ({ ctx })
|
|
2316
2483
|
},
|
package/lib/updater-service.js
CHANGED
|
@@ -31,21 +31,9 @@ function isLoopback(value) {
|
|
|
31
31
|
)
|
|
32
32
|
}
|
|
33
33
|
|
|
34
|
-
function isPrivateLan(value) {
|
|
35
|
-
const address = value?.toLowerCase().replace(/^\[|\]$/g, '')
|
|
36
|
-
if (!address) return false
|
|
37
|
-
const ipv4 = address.startsWith('::ffff:') ? address.slice(7) : address
|
|
38
|
-
return (
|
|
39
|
-
ipv4.startsWith('192.168.') ||
|
|
40
|
-
ipv4.startsWith('10.') ||
|
|
41
|
-
/^172\.(1[6-9]|2[0-9]|3[0-1])\./.test(ipv4)
|
|
42
|
-
)
|
|
43
|
-
}
|
|
44
|
-
|
|
45
34
|
export function isTrustedUpdateRequest(request) {
|
|
46
35
|
if (header(request, UPDATE_HEADER) !== '1') return false
|
|
47
|
-
|
|
48
|
-
if (!isLoopback(remote) && !isPrivateLan(remote)) return false
|
|
36
|
+
if (!isLoopback(request.socket?.remoteAddress)) return false
|
|
49
37
|
const site = header(request, 'sec-fetch-site')
|
|
50
38
|
if (site !== undefined && site !== 'same-origin') return false
|
|
51
39
|
const origin = header(request, 'origin')
|
|
@@ -53,11 +41,8 @@ export function isTrustedUpdateRequest(request) {
|
|
|
53
41
|
if (origin === undefined || host === undefined) return false
|
|
54
42
|
try {
|
|
55
43
|
const url = new URL(origin)
|
|
56
|
-
return (
|
|
57
|
-
(url.
|
|
58
|
-
(isLoopback(url.hostname) || isPrivateLan(url.hostname)) &&
|
|
59
|
-
url.host === host
|
|
60
|
-
)
|
|
44
|
+
return (url.protocol === 'http:' || url.protocol === 'https:')
|
|
45
|
+
&& isLoopback(url.hostname) && url.host === host
|
|
61
46
|
} catch (err) { /* best-effort fallback */
|
|
62
47
|
return false
|
|
63
48
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@goodandready/dsh-remote-workspace",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.8",
|
|
4
4
|
"description": "Enterprise Remote Workspace Plugin for DeepSeek Harness: SSH2 connection pool, SFTP operations, 3-way conflict-aware mirror sync, port forwarding, and native DSH UI.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|
|
@@ -16,7 +16,6 @@
|
|
|
16
16
|
"README.md",
|
|
17
17
|
"README.ru.md",
|
|
18
18
|
"README.zh.md",
|
|
19
|
-
"docs/design/DESIGN.md",
|
|
20
19
|
"LICENSE"
|
|
21
20
|
],
|
|
22
21
|
"scripts": {
|
package/docs/design/DESIGN.md
DELETED
|
@@ -1,91 +0,0 @@
|
|
|
1
|
-
# DESIGN.md — dsh-remote-workspace
|
|
2
|
-
|
|
3
|
-
## Product / Purpose
|
|
4
|
-
- **Назначение**: Полнофункциональный корпоративный плагин удалённой разработки для DeepSeek Harness (DSH). Обеспечивает подключение к удалённым серверам по SSH (ключ / пароль), SFTP-навигацию, 3-way синхронизацию локального зеркала, SSH-туннелирование (port forwarding) и компактный набор инструментов для LLM агента (
|
|
5
|
-
emote_exec,
|
|
6
|
-
emote_fs,
|
|
7
|
-
emote_sync,
|
|
8
|
-
emote_tunnel).
|
|
9
|
-
- **Аудитория**: Разработчики и инженеры, использующие DSH для работы с проектами на VPS, удаленных серверах, облачных инстансах и MiniPC.
|
|
10
|
-
- **Статус**: Стабильный релиз v0.3.3 (One-Click Updater, Cross-Plugin API, Clean Tokens, Modular Routes).
|
|
11
|
-
|
|
12
|
-
## User Surfaces
|
|
13
|
-
- **Web/UI**: Нативная карточка настроек профилей хостов (settings.plugin.item), удаленный SFTP браузер каталогов, чип статуса подключения в шапке сессии (conversation.session.header.utilities).
|
|
14
|
-
- **DSH UI / settings / slots**:
|
|
15
|
-
- Карточка плагина: settings.plugin.item для @goodandready/dsh-remote-workspace.
|
|
16
|
-
- Статусный чип сессии: conversation.session.header.utilities (order: 30).
|
|
17
|
-
- Запрещённые слоты: settings.section (запрещён стандартом authoring, все настройки только внутри settings.plugin.item).
|
|
18
|
-
- **API маршруты**:
|
|
19
|
-
- GET /dsh-remote-workspace/state: получение профилей, активного хоста и туннелей.
|
|
20
|
-
- POST /dsh-remote-workspace/profiles/save: сохранение профиля (с валидацией и сбросом старых соединений).
|
|
21
|
-
- POST /dsh-remote-workspace/profiles/delete: удаление профиля.
|
|
22
|
-
- POST /dsh-remote-workspace/profiles/active: переключение активного профиля.
|
|
23
|
-
- POST /dsh-remote-workspace/test: live-проверка связи (Ping, OS, Latency).
|
|
24
|
-
- POST /dsh-remote-workspace/browse: чтение содержимого удалённой папки (SFTP).
|
|
25
|
-
- POST /dsh-remote-workspace/sync: запуск двусторонней 3-way синхронизации зеркала (pull/push).
|
|
26
|
-
- POST /dsh-remote-workspace/tunnels/start: создание локального SSH-туннеля.
|
|
27
|
-
- POST /dsh-remote-workspace/tunnels/stop: остановка активного SSH-туннеля.
|
|
28
|
-
- **CLI / Model Tools**:
|
|
29
|
-
-
|
|
30
|
-
emote_exec: выполнение команд на сервере.
|
|
31
|
-
-
|
|
32
|
-
emote_fs: чтение, атомарная запись, stat, mkdir, remove.
|
|
33
|
-
-
|
|
34
|
-
emote_sync: запуск 3-way синхронизации с контролем конфликтов.
|
|
35
|
-
-
|
|
36
|
-
emote_tunnel: создание/остановка/листинг туннелей.
|
|
37
|
-
|
|
38
|
-
## Visual Direction (Единый стиль dsh-clinebot)
|
|
39
|
-
- **Атмосфера**: Премиальный инженерный интерфейс уровня VS Code Remote / Linear. Строгая типографика, плотные функциональные карточки, чистые переходы и отсутствие лишних теней.
|
|
40
|
-
- **Токены ядра DSH**:
|
|
41
|
-
- Фоны: ar(--dsw-alias-bg-layer-3) (карточки), ar(--dsw-alias-bg-layer-2) (поля ввода, вложенные блоки, таблицы), ar(--dsw-alias-bg-layer-1) (сегменты, подложка).
|
|
42
|
-
- Границы: ar(--dsw-alias-border-l2).
|
|
43
|
-
- Текст: ar(--dsw-alias-label-primary), ar(--dsw-alias-label-secondary), ar(--dsw-alias-label-dimmed).
|
|
44
|
-
- Статусы: ar(--dsw-alias-state-success-primary), ar(--dsw-alias-state-warning-primary), ar(--dsw-alias-state-error-primary), ar(--dsw-alias-state-brand-primary).
|
|
45
|
-
- **Шапка карточки**:
|
|
46
|
-
- Название плагина, подзаголовок, статусный аккордеонный шеврон.
|
|
47
|
-
- Статусные пилюли (Badges): Host online / Offline, Active Profile, Active Tunnels count.
|
|
48
|
-
|
|
49
|
-
## Components And States
|
|
50
|
-
1. **Секция 1: Профили серверов (SSH Profiles)**:
|
|
51
|
-
- Таблица настроенных машин с бейджами типа аутентификации (🔑 key / 🔒 pwd), меткой активного сервера, кнопками «Сделать активным», «Редактировать», «Удалить».
|
|
52
|
-
- Пустое состояние: ясное сообщение и кнопка + Добавить хост.
|
|
53
|
-
2. **Секция 2: Интерактивный редактор профиля**:
|
|
54
|
-
- Название, Хост/IP, Порт, Пользователь.
|
|
55
|
-
- Сегментированный переключатель авторизации (SSH-ключ vs Пароль).
|
|
56
|
-
- Поля авторизации: путь к приватному ключу, парольная фраза, либо пароль (маскированный).
|
|
57
|
-
- Директории: Удалённый каталог проекта (с кнопкой вызова SFTP браузера) и **Локальная папка зеркала** (с подсказкой назначения).
|
|
58
|
-
- Кнопки: «⚡ Проверить связь (Ping)», «Отмена», «Сохранить».
|
|
59
|
-
- Результат пинга: отображение Latency: X ms | OS: Y или понятный текст ошибки.
|
|
60
|
-
3. **Секция 3: Диагностика активного подключения**:
|
|
61
|
-
- Четырёхколоночная сетка метрик активного сервера (Хост, Удалённая папка, Метод авторизации, Статус ядра).
|
|
62
|
-
4. **Секция 4: Синхронизация зеркала (Mirror Sync)**:
|
|
63
|
-
- Кнопки Pull (Remote -> Local) и Push (Local -> Remote) с индикацией процесса, результатом и предупреждением при конфликтах.
|
|
64
|
-
5. **Секция 5: SSH-туннели (Port Forwarding)**:
|
|
65
|
-
- Таблица активных туннелей (ID, Локальный порт -> Удалённый порт) с кнопкой закрытия.
|
|
66
|
-
- Форма создания нового туннеля (Local Port, Remote Port, кнопка «Открыть туннель»).
|
|
67
|
-
|
|
68
|
-
## Security & Reliability
|
|
69
|
-
- Защита всех мутирующих эндпоинтов проверкой isTrustedSettingsRequest(req) (защита от cross-site атак).
|
|
70
|
-
- Безопасное чтение Cordis сервисов через .get() с fallback.
|
|
71
|
-
- Изоляция жизненного цикла стримов в TunnelService (прослушивание сокетных и потоковых ошибок).
|
|
72
|
-
|
|
73
|
-
### v0.3.3 Client Architecture Modularization (Refs: #14)
|
|
74
|
-
- Client-side code decomposed into modular ES components under `src/client/`:
|
|
75
|
-
- `src/client/styles.js` — CSS styling with DSH theme variables
|
|
76
|
-
- `src/client/locales.js` — English and Chinese translation dictionaries
|
|
77
|
-
- `src/client/env-editor.js` — Remote .env manager tab component
|
|
78
|
-
- `src/client/docker.js` — Container inspector tab component
|
|
79
|
-
- `src/client/terminal.js` — Web terminal tab component
|
|
80
|
-
- `src/client/file-browser.js` — Remote file explorer tab component
|
|
81
|
-
- `src/client/settings-view.js` — Profiles, tunnels, and sync settings component
|
|
82
|
-
- `src/client/updater-section.js` — One-click updater section component
|
|
83
|
-
- `src/client/plugin-card.js` — Plugin card and status chip components
|
|
84
|
-
- `src/client/entry.js` — Cordis client plugin apply and slot registration
|
|
85
|
-
- Added native builder `scripts/build-client.mjs` hooked into `prepack` and `npm test`.
|
|
86
|
-
|
|
87
|
-
### v0.3.6 Model Tools JSON Schema Standardization (Refs: #33)
|
|
88
|
-
- Все 9 инструментов (`remote_exec`, `remote_fs`, `remote_sync`, `remote_tunnel`, `remote_docker`, `remote_service`, `remote_transfer`, `remote_diagnose`, `remote_env`) переведены на компиляцию через `defineTool()` из `@deepseek-ai/dsh-tools`.
|
|
89
|
-
- Параметры инструментов компилируются в строгий JSON Schema с корнем `{ type: 'object', properties: ..., required: [...] }`.
|
|
90
|
-
- Предотвращены ошибки валидации схем в OpenAI-совместимых провайдерах (`Invalid schema: schema must be a JSON Schema of 'type: "object"', got 'type: null'`).
|
|
91
|
-
- Схема вывода стандартизирована с явным `{ type: 'object', additionalProperties: true }`.
|