@goodandready/dsh-remote-workspace 0.3.0 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +17 -0
- package/README.ru.md +17 -0
- package/README.zh.md +17 -0
- package/docs/plans/task_plan.md +18 -20
- package/lib/alert-service.js +109 -0
- package/lib/client.js +265 -77
- package/lib/diagnose-service.js +57 -0
- package/lib/env-service.js +122 -0
- package/lib/index.js +41 -4
- package/lib/routes.js +627 -522
- package/lib/ssh-config-parser.js +89 -0
- package/lib/tar-sync-service.js +85 -0
- package/lib/tools.js +78 -3
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -22,6 +22,16 @@
|
|
|
22
22
|
<a href="README.zh.md"><b>🇨🇳 中文说明</b></a>
|
|
23
23
|
</p>
|
|
24
24
|
|
|
25
|
+
<table align="center">
|
|
26
|
+
<tr>
|
|
27
|
+
<td align="center">
|
|
28
|
+
⭐ <strong>If you like this plugin, please star it on GitHub</strong> — it shows me that the plugin is useful to you and motivates me to keep developing it.
|
|
29
|
+
<br><br>
|
|
30
|
+
🐛 <strong>If you find a bug or would like to request a feature</strong>, open a GitHub issue in any language — I will review your proposal and implement useful suggestions in a future plugin version.
|
|
31
|
+
</td>
|
|
32
|
+
</tr>
|
|
33
|
+
</table>
|
|
34
|
+
|
|
25
35
|
</div>
|
|
26
36
|
|
|
27
37
|
---
|
|
@@ -236,3 +246,10 @@ All endpoints are hosted under `/dsh-remote-workspace`:
|
|
|
236
246
|
## 📄 License
|
|
237
247
|
|
|
238
248
|
MIT © [GooDAnDReaDY](https://github.com/GooDAnDReaDY)
|
|
249
|
+
|
|
250
|
+
### 13. Smart Tarball Sync & Diagnostics (v0.3.1)
|
|
251
|
+
- **`🚀 Fast Tarball Stream`**: High-throughput directory sync via on-the-fly streaming `tar -czf` bypassing per-file roundtrips.
|
|
252
|
+
- **`🩺 Remote Diagnostics (remote_diagnose)`**: Instant one-shot checks for occupied ports (`ports`), OOM killer events (`oom_killer`), disk consumption (`disk`), and service crash-logs (`service_logs`).
|
|
253
|
+
- **`📥 Import from ~/.ssh/config`**: One-click import of hosts, keys, and ProxyJump configurations directly into the encrypted `.env` vault.
|
|
254
|
+
- **`🗃️ Remote Environment Manager (remote_env)`**: Inspect and atomically modify remote `.env` key-values with password masking and structural preservation.
|
|
255
|
+
- **`📡 Background Anomaly Alerts`**: Proactive monitoring of disk (<10% free), memory (<5%), and restarting Docker containers via Cordis event bus (`remote-workspace/alert`).
|
package/README.ru.md
CHANGED
|
@@ -22,6 +22,16 @@
|
|
|
22
22
|
<a href="README.zh.md"><b>🇨🇳 中文说明</b></a>
|
|
23
23
|
</p>
|
|
24
24
|
|
|
25
|
+
<table align="center">
|
|
26
|
+
<tr>
|
|
27
|
+
<td align="center">
|
|
28
|
+
⭐ <strong>Если вам нравится этот плагин, поставьте ему звезду на GitHub</strong> — это покажет мне, что плагин вам полезен, и будет мотивировать меня развивать его дальше.
|
|
29
|
+
<br><br>
|
|
30
|
+
🐛 <strong>Если вы нашли баг или хотите предложить новый функционал</strong>, создайте issue на GitHub на любом языке — я рассмотрю ваше предложение и реализую полезные идеи в одной из следующих версий плагина.
|
|
31
|
+
</td>
|
|
32
|
+
</tr>
|
|
33
|
+
</table>
|
|
34
|
+
|
|
25
35
|
</div>
|
|
26
36
|
|
|
27
37
|
---
|
|
@@ -236,3 +246,10 @@ dsh-remote-workspace:
|
|
|
236
246
|
## 📄 Лицензия
|
|
237
247
|
|
|
238
248
|
MIT © [GooDAnDReaDY](https://github.com/GooDAnDReaDY)
|
|
249
|
+
|
|
250
|
+
### 13. Пакетная синхронизация и диагностика (v0.3.1)
|
|
251
|
+
- **`🚀 Быстрый Tarball поток`**: Высокоскоростная передача структуры каталогов через потоковый `tar -czf` без задержек пофайлового обхода.
|
|
252
|
+
- **`🩺 Диагностика сервера (remote_diagnose)`**: Мгновенный аудит занятых портов (`ports`), срабатываний OOM Killer (`oom_killer`), дискового пространства (`disk`) и crash-логов служб.
|
|
253
|
+
- **`📥 Импорт из ~/.ssh/config`**: Автоматическое добавление хостов, ключей и bastion-узлов в зашифрованный `.env` vault.
|
|
254
|
+
- **`🗃️ Менеджер переменных окружения (remote_env)`**: Атомарное изменение и безопасный просмотр удалённых `.env` файлов с маскированием паролей.
|
|
255
|
+
- **`📡 Мониторинг аномалий`**: Оповещение о переполнении диска (<10%), дефиците ОЗУ (<5%) и циклических перезапусках контейнеров через шину событий Cordis (`remote-workspace/alert`).
|
package/README.zh.md
CHANGED
|
@@ -22,6 +22,16 @@
|
|
|
22
22
|
<a href="README.zh.md"><b>🇨🇳 中文说明</b></a>
|
|
23
23
|
</p>
|
|
24
24
|
|
|
25
|
+
<table align="center">
|
|
26
|
+
<tr>
|
|
27
|
+
<td align="center">
|
|
28
|
+
⭐ <strong>如果您喜欢这个插件,请在 GitHub 上为它点亮 Star</strong> — 这能让我知道插件对您有用,并鼓励我继续开发和维护它。
|
|
29
|
+
<br><br>
|
|
30
|
+
🐛 <strong>如果您发现 Bug 或希望增加功能</strong>,请使用任意语言在 GitHub 上提交 Issue — 我会评估您的建议,并在后续版本中实现有价值的改进。
|
|
31
|
+
</td>
|
|
32
|
+
</tr>
|
|
33
|
+
</table>
|
|
34
|
+
|
|
25
35
|
</div>
|
|
26
36
|
|
|
27
37
|
---
|
|
@@ -236,3 +246,10 @@ dsh-remote-workspace:
|
|
|
236
246
|
## 📄 开源许可证
|
|
237
247
|
|
|
238
248
|
MIT © [GooDAnDReaDY](https://github.com/GooDAnDReaDY)
|
|
249
|
+
|
|
250
|
+
### 13. 归档流同步与综合诊断 (v0.3.1)
|
|
251
|
+
- **`🚀 快速 Tarball 流式同步`**:利用实时 `tar -czf` 数据流传输整个目录,彻底消除小文件频繁往返的延迟。
|
|
252
|
+
- **`🩺 远程系统诊断 (remote_diagnose)`**:一键检测端口占用 (`ports`)、OOM 终止记录 (`oom_killer`)、磁盘分布 (`disk`) 与服务崩溃日志。
|
|
253
|
+
- **`📥 导入 ~/.ssh/config`**:快速解析本机已有主机、私钥及跳板机配置至加密 `.env` 保管库。
|
|
254
|
+
- **`🗃️ 远程环境变量管理 (remote_env)`**:支持远程 `.env` 的安全预览与原子化变量修改,自动遮蔽敏感密码。
|
|
255
|
+
- **`📡 异常告警监控`**:当磁盘低于 10%、内存不足 5% 或容器异常重启时,主动触发 Cordis 事件总线 (`remote-workspace/alert`) 告警。
|
package/docs/plans/task_plan.md
CHANGED
|
@@ -1,23 +1,21 @@
|
|
|
1
|
-
# task_plan.md — dsh-remote-workspace
|
|
1
|
+
# task_plan.md — dsh-remote-workspace (v0.3.1)
|
|
2
2
|
|
|
3
3
|
## Цель
|
|
4
|
-
|
|
4
|
+
Реализовать 5 ключевых возможностей в рамках релиза v0.3.1:
|
|
5
|
+
1. Smart Tarball Sync (пакетное ускорение передачи каталогов).
|
|
6
|
+
2. remote_diagnose (комплексный агентский инструмент диагностики сервера).
|
|
7
|
+
3. Import from ~/.ssh/config (парсинг хостов в .env vault).
|
|
8
|
+
4. remote_env & Remote Environment Manager (безопасный редактор .env).
|
|
9
|
+
5. AlertService (фоновый мониторинг аномалий и события Cordis).
|
|
5
10
|
|
|
6
|
-
##
|
|
7
|
-
- [
|
|
8
|
-
- [
|
|
9
|
-
- [
|
|
10
|
-
- [
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
- [x] **Фаза 7**: Дополнительные ручные сценарии и документация (Docs standard EN/RU/ZH).
|
|
18
|
-
- [/] **Фаза 8**: Стабильность, аудит багов, расширение тестов и дизайн в едином стиле dsh-clinebot:
|
|
19
|
-
- [ ] 8.1. Исправление стабильности: безопасный доступ к Cordis сервисам (ctx.get), сброс соединений при смене реквизитов профиля, нормализация полей в estConnection (latencyMs / remoteOs).
|
|
20
|
-
- [ ] 8.2. Безопасность и отказоустойчивость: фильтрация isTrustedSettingsRequest на API маршрутах, обработка ошибок стримов в TunnelService, автосоздание папок в MirrorSyncService.
|
|
21
|
-
- [ ] 8.3. Редизайн UI в едином стиле dsh-clinebot: модульные карточки, статусные бейджи в шапке, добавление поля localMirrorPath, секция управления туннелями в интерфейсе.
|
|
22
|
-
- [ ] 8.4. Расширение набора тестов (тесты API routes, tools, store/apply, client registration).
|
|
23
|
-
- [ ] 8.5. Тестирование на изолированном MiniPC сервере (dsh-test-plugin).
|
|
11
|
+
## Статус фаз
|
|
12
|
+
- [ ] **Фаза 1**: Backend-сервисы (lib/tar-sync-service.js, lib/diagnose-service.js, lib/ssh-config-parser.js, lib/env-service.js, lib/alert-service.js)
|
|
13
|
+
- [ ] **Фаза 2**: Интеграция в инструменты модели (remote_diagnose, remote_env, smart sync) и API маршруты
|
|
14
|
+
- [ ] **Фаза 3**: Обновление клиентского интерфейса (кнопка импорта SSH, вкладка/модалка .env, индикатор алертов)
|
|
15
|
+
- [ ] **Фаза 4**: Модульные и интеграционные тесты (test/phase4.test.mjs)
|
|
16
|
+
- [ ] **Фаза 5**: Документация (README EN/RU/ZH, DESIGN.md) и проверка лимитов размера (<250 KiB)
|
|
17
|
+
- [ ] **Фаза 6**: Тестовый гейт на MiniPC (dsh-test-plugin) и боевая верификация на MiniAI
|
|
18
|
+
|
|
19
|
+
## Next Step
|
|
20
|
+
Создание backend-сервисов в lib/.
|
|
21
|
+
EOF
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
export class AlertService {
|
|
2
|
+
/**
|
|
3
|
+
* @param {any} ctx
|
|
4
|
+
* @param {import('./health-service.js').HealthService} healthService
|
|
5
|
+
* @param {import('./docker-service.js').DockerService} dockerService
|
|
6
|
+
*/
|
|
7
|
+
constructor(ctx, healthService, dockerService) {
|
|
8
|
+
this.ctx = ctx;
|
|
9
|
+
this.healthService = healthService;
|
|
10
|
+
this.dockerService = dockerService;
|
|
11
|
+
/** @type {NodeJS.Timeout|null} */
|
|
12
|
+
this.timer = null;
|
|
13
|
+
/** @type {Array<any>} */
|
|
14
|
+
this.activeAlerts = [];
|
|
15
|
+
this.checkIntervalMs = 5 * 60 * 1000; // 5 mins
|
|
16
|
+
this.enabled = false;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
start(intervalMs = 300000) {
|
|
20
|
+
this.stop();
|
|
21
|
+
this.checkIntervalMs = intervalMs;
|
|
22
|
+
this.enabled = true;
|
|
23
|
+
this.timer = setInterval(() => this.check(), this.checkIntervalMs);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
stop() {
|
|
27
|
+
if (this.timer) {
|
|
28
|
+
clearInterval(this.timer);
|
|
29
|
+
this.timer = null;
|
|
30
|
+
}
|
|
31
|
+
this.enabled = false;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Run single alert check across active profile
|
|
36
|
+
*/
|
|
37
|
+
async check(profile) {
|
|
38
|
+
if (!profile) return [];
|
|
39
|
+
|
|
40
|
+
const alerts = [];
|
|
41
|
+
try {
|
|
42
|
+
// 1. Health check
|
|
43
|
+
const health = await this.healthService.getHealth(profile, true);
|
|
44
|
+
if (health.disk && health.disk.percent >= 90) {
|
|
45
|
+
alerts.push({
|
|
46
|
+
id: `disk-${Date.now()}`,
|
|
47
|
+
level: health.disk.percent >= 95 ? 'critical' : 'warning',
|
|
48
|
+
type: 'disk_full',
|
|
49
|
+
message: `Disk usage on ${profile.name || profile.host} is ${health.disk.percent}%`,
|
|
50
|
+
timestamp: Date.now()
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (health.memory && health.memory.percent >= 95) {
|
|
55
|
+
alerts.push({
|
|
56
|
+
id: `mem-${Date.now()}`,
|
|
57
|
+
level: 'warning',
|
|
58
|
+
type: 'memory_high',
|
|
59
|
+
message: `Memory usage on ${profile.name || profile.host} is ${health.memory.percent}%`,
|
|
60
|
+
timestamp: Date.now()
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// 2. Docker check
|
|
65
|
+
if (this.dockerService) {
|
|
66
|
+
try {
|
|
67
|
+
const containers = await this.dockerService.listContainers(profile);
|
|
68
|
+
for (const c of containers) {
|
|
69
|
+
if (c.State === 'restarting' || (c.Status && c.Status.includes('Restarting'))) {
|
|
70
|
+
alerts.push({
|
|
71
|
+
id: `docker-${c.ID || c.Names}`,
|
|
72
|
+
level: 'warning',
|
|
73
|
+
type: 'container_restarting',
|
|
74
|
+
message: `Container ${c.Names || c.ID} is restarting repeatedly`,
|
|
75
|
+
timestamp: Date.now()
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
} catch {
|
|
80
|
+
// Docker might not be installed or active, non-blocking
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
} catch (err) {
|
|
84
|
+
// If host unreachable
|
|
85
|
+
alerts.push({
|
|
86
|
+
id: `host-unreachable-${Date.now()}`,
|
|
87
|
+
level: 'critical',
|
|
88
|
+
type: 'host_unreachable',
|
|
89
|
+
message: `Host ${profile.name || profile.host} unreachable: ${err.message}`,
|
|
90
|
+
timestamp: Date.now()
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
this.activeAlerts = alerts;
|
|
95
|
+
|
|
96
|
+
// Emit Cordis event if ctx is present
|
|
97
|
+
if (this.ctx && typeof this.ctx.emit === 'function' && alerts.length > 0) {
|
|
98
|
+
for (const alert of alerts) {
|
|
99
|
+
this.ctx.emit('remote-workspace/alert', alert);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return alerts;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
getAlerts() {
|
|
107
|
+
return this.activeAlerts;
|
|
108
|
+
}
|
|
109
|
+
}
|
package/lib/client.js
CHANGED
|
@@ -99,6 +99,13 @@ window.__ModuleLoader__.load({
|
|
|
99
99
|
.drw-container-row{display:grid;grid-template-columns:2fr 2fr 1.5fr 1.5fr auto;align-items:center;gap:10px;padding:8px 10px;border-bottom:1px solid var(--dsw-alias-border-l2);font-size:12.5px;font-family:monospace}
|
|
100
100
|
.drw-container-row:hover{background:var(--dsw-alias-bg-layer-3)}
|
|
101
101
|
|
|
102
|
+
.drw-env-table{width:100%;border-collapse:collapse}
|
|
103
|
+
.drw-env-row{display:grid;grid-template-columns:220px 1fr 90px;gap:8px;padding:6px 0;align-items:center;border-bottom:1px solid var(--dsw-alias-border-l2)}
|
|
104
|
+
.drw-alert-badge{padding:4px 8px;border-radius:6px;font-size:11px;font-family:monospace;display:flex;align-items:center;gap:6px}
|
|
105
|
+
.drw-alert-critical{background:rgba(239,68,68,0.15);color:var(--dsw-alias-state-error-primary);border:1px solid rgba(239,68,68,0.4)}
|
|
106
|
+
.drw-alert-warning{background:rgba(245,158,11,0.15);color:var(--dsw-alias-state-warning-primary);border:1px solid rgba(245,158,11,0.4)}
|
|
107
|
+
|
|
108
|
+
|
|
102
109
|
`
|
|
103
110
|
document.head.appendChild(style)
|
|
104
111
|
}
|
|
@@ -111,6 +118,22 @@ window.__ModuleLoader__.load({
|
|
|
111
118
|
tabProfiles: '🖥️ Host Profiles',
|
|
112
119
|
|
|
113
120
|
tabContainers: '🐳 Containers',
|
|
121
|
+
|
|
122
|
+
tabEnv: '🗃️ .env Manager',
|
|
123
|
+
btnImportSsh: 'Import ~/.ssh/config',
|
|
124
|
+
envTitle: 'Remote Environment Variables (.env)',
|
|
125
|
+
envKey: 'Key',
|
|
126
|
+
envVal: 'Value',
|
|
127
|
+
envSaveVar: 'Save',
|
|
128
|
+
envShowVal: 'Show',
|
|
129
|
+
envHideVal: 'Hide',
|
|
130
|
+
tarSyncFast: '🚀 Fast Tarball Stream',
|
|
131
|
+
diagnoseTitle: 'Quick Diagnostics',
|
|
132
|
+
diagnosePorts: 'Open Ports',
|
|
133
|
+
diagnoseOom: 'OOM Killer Check',
|
|
134
|
+
diagnoseDisk: 'Disk Usage',
|
|
135
|
+
alertLabel: 'Alerts ({count})',
|
|
136
|
+
|
|
114
137
|
containersTitle: 'Remote Docker / Podman Engine',
|
|
115
138
|
containerName: 'Container',
|
|
116
139
|
containerImage: 'Image',
|
|
@@ -226,80 +249,141 @@ window.__ModuleLoader__.load({
|
|
|
226
249
|
tOpenErr: 'Failed to open tunnel: {err}'
|
|
227
250
|
}
|
|
228
251
|
|
|
229
|
-
const
|
|
230
|
-
title: '
|
|
231
|
-
subtitle: 'SSH
|
|
232
|
-
status: '
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
252
|
+
const zh = {
|
|
253
|
+
title: '远程开发工作区',
|
|
254
|
+
subtitle: '支持原生 SSH 连接、SFTP 文件访问、3-way 镜像同步与端口转发隧道。',
|
|
255
|
+
status: '就绪',
|
|
256
|
+
|
|
257
|
+
tabProfiles: '🖥️ 主机配置',
|
|
258
|
+
tabContainers: '🐳 容器管理',
|
|
259
|
+
tabEnv: '🗃️ 环境变量',
|
|
260
|
+
btnImportSsh: '导入 ~/.ssh/config',
|
|
261
|
+
envTitle: '远程环境变量配置 (.env)',
|
|
262
|
+
envKey: '变量名',
|
|
263
|
+
envVal: '变量值',
|
|
264
|
+
envSaveVar: '保存',
|
|
265
|
+
envShowVal: '显示',
|
|
266
|
+
envHideVal: '隐藏',
|
|
267
|
+
tarSyncFast: '🚀 快速 Tarball 流式同步',
|
|
268
|
+
diagnoseTitle: '快速诊断',
|
|
269
|
+
diagnosePorts: '开放端口',
|
|
270
|
+
diagnoseOom: 'OOM 检查',
|
|
271
|
+
diagnoseDisk: '磁盘分析',
|
|
272
|
+
alertLabel: '告警 ({count})',
|
|
273
|
+
|
|
274
|
+
containersTitle: '远程 Docker / Podman 引擎',
|
|
275
|
+
containerName: '容器名',
|
|
276
|
+
containerImage: '镜像',
|
|
277
|
+
containerStatus: '运行状态',
|
|
278
|
+
containerPorts: '端口映射',
|
|
279
|
+
btnLogs: '日志',
|
|
280
|
+
btnRestart: '重启',
|
|
281
|
+
btnStop: '停止',
|
|
282
|
+
noContainers: '未找到运行中的容器或 Docker 守护进程未启动。',
|
|
283
|
+
healthCpu: 'CPU: {val}%',
|
|
284
|
+
healthMem: '内存: {val}% ({used}/{total} MB)',
|
|
285
|
+
healthDisk: '磁盘: {val}%',
|
|
286
|
+
healthUptime: '运行: {val}',
|
|
287
|
+
autoSyncTitle: '保存时自动增量同步',
|
|
288
|
+
autoSyncDesc: '本地文件保存时,后台自动增量推送到远程对应工作目录。',
|
|
289
|
+
btnTransfer: '跨主机传输...',
|
|
290
|
+
transferTitle: '主机间直接流式传输',
|
|
291
|
+
transferDestHost: '目标主机',
|
|
292
|
+
transferDestPath: '目标文件路径',
|
|
293
|
+
transferBtnExec: '立即直传',
|
|
294
|
+
transferSuccess: '成功传输 {bytes} 字节到 {host}:{path}',
|
|
295
|
+
fJumpHost: '跳板机 (Bastion)',
|
|
296
|
+
fJumpHostNone: '直连模式 (无跳板机)',
|
|
297
|
+
|
|
298
|
+
tabTerminal: '⚡ 网页终端',
|
|
299
|
+
tabExplorer: '📂 文件管理',
|
|
300
|
+
tabTunnels: '🔗 端口隧道',
|
|
301
|
+
tabSync: '🔄 同步控制台',
|
|
302
|
+
vaultProtectedBadge: '🛡️ .env 保管库加密保护',
|
|
303
|
+
termStart: '连接终端',
|
|
304
|
+
termStop: '断开连接',
|
|
305
|
+
termClear: '清屏',
|
|
306
|
+
termPlaceholder: '输入命令后回车 (如 htop, ls, docker ps)...',
|
|
307
|
+
termConnected: '已连接至主机 {host} 的 PTY 会话 {id}',
|
|
308
|
+
termDisconnected: '终端已断开。',
|
|
309
|
+
explorerTitle: '远程文件系统浏览',
|
|
310
|
+
filePreviewTitle: '文件查看器: {path}',
|
|
311
|
+
fileSize: '{size} KB',
|
|
312
|
+
btnSaveFile: '保存文件更改',
|
|
313
|
+
fileSaved: '文件保存成功。',
|
|
314
|
+
tunnelTraffic: '下行: {rx} | 上行: {tx}',
|
|
315
|
+
tunnelUptime: '运行: {sec}秒',
|
|
316
|
+
openLocalLink: '🌐 打开本地端口',
|
|
317
|
+
badgeOnline: '当前主机: {name} ({latency} ms)',
|
|
318
|
+
badgeOffline: '未选定活跃主机',
|
|
319
|
+
badgeTunnels: '活动隧道: {count}',
|
|
320
|
+
badgeKey: '🔑 私钥认证',
|
|
321
|
+
badgePassword: '🔒 密码认证',
|
|
322
|
+
profilesTitle: '🖥️ 远程 SSH 主机配置',
|
|
323
|
+
profilesDesc: '配置用于命令执行、SFTP 浏览与后台镜像同步的远程服务器。',
|
|
324
|
+
addBtn: '+ 添加主机',
|
|
325
|
+
nameCol: '配置名称',
|
|
326
|
+
hostCol: '主机 / 用户',
|
|
327
|
+
portCol: '端口',
|
|
328
|
+
pathCol: '远程工作区',
|
|
329
|
+
actionsCol: '操作',
|
|
330
|
+
noHosts: '暂无配置的主机。点击 "+ 添加主机" 建立首个连接。',
|
|
331
|
+
activeBadge: '活动中',
|
|
332
|
+
pinBtn: '设为默认',
|
|
333
|
+
editBtn: '编辑',
|
|
334
|
+
delBtn: '删除',
|
|
335
|
+
delConfirm: '确认删除主机配置 "{name}" 吗?',
|
|
336
|
+
formTitleNew: '添加新主机配置',
|
|
337
|
+
formTitleEdit: '编辑主机配置',
|
|
338
|
+
fName: '配置别名',
|
|
339
|
+
fHost: '主机名 / IP 地址',
|
|
340
|
+
fPort: '端口',
|
|
341
|
+
fUser: '登录用户名',
|
|
342
|
+
fAuthType: '身份认证方式',
|
|
343
|
+
fAuthKey: 'SSH 私钥',
|
|
344
|
+
fAuthPass: '账户密码',
|
|
345
|
+
fKeyPath: '私钥文件路径',
|
|
346
|
+
fKeyPathHint: '例如 ~/.ssh/id_rsa 或 /home/user/.ssh/id_ed25519',
|
|
347
|
+
fPassphrase: '私钥口令 (可选)',
|
|
348
|
+
fPassword: '登录密码',
|
|
349
|
+
fRemotePath: '远程项目工作区目录',
|
|
350
|
+
fRemotePathHint: '服务器上的绝对路径 (例如 /home/user/my-project)',
|
|
351
|
+
fLocalMirror: '本地镜像同步目录',
|
|
352
|
+
fLocalMirrorHint: '用于 3-way 差异比对与快速同步的本地镜像路径',
|
|
353
|
+
btnBrowse: '📁 浏览远程目录...',
|
|
354
|
+
btnSelectThis: '选择当前目录',
|
|
355
|
+
btnTest: '⚡ 测试网络连通性 (Ping)',
|
|
356
|
+
btnSave: '保存配置',
|
|
357
|
+
btnCancel: '取消',
|
|
358
|
+
testing: '正在测试连接...',
|
|
359
|
+
saving: '正在保存...',
|
|
360
|
+
testOk: '✅ 连接成功!延迟: {latency} ms | 系统: {os}',
|
|
361
|
+
testFail: '❌ 连接失败: {err}',
|
|
362
|
+
diagTitle: '📊 实时连接状态诊断',
|
|
363
|
+
diagDesc: '查看当前选定主机的网络延迟、系统资源及底层服务运行指标。',
|
|
364
|
+
diagProbeBtn: '⚡ 即时探测',
|
|
365
|
+
syncTitle: '🔄 双向工作区镜像同步',
|
|
366
|
+
syncDesc: '基于三方冲突感知的快速差异同步,支持本地与远程端双向推拉。',
|
|
367
|
+
pullBtn: '⬇️ 拉取至本地 (Pull)',
|
|
368
|
+
pushBtn: '⬆️ 推送至远程 (Push)',
|
|
369
|
+
pulling: '正在拉取文件…',
|
|
370
|
+
pushing: '正在推送文件…',
|
|
371
|
+
syncOk: '同步完成!共更新 {count} 个文件。',
|
|
372
|
+
syncConflict: '冲突警告: 检测到 {count} 个文件在两端均有修改。',
|
|
373
|
+
syncErr: '同步异常: {err}',
|
|
374
|
+
tunnelsTitle: '🔀 SSH 端口安全隧道',
|
|
375
|
+
tunnelsDesc: '通过加密 SSH 隧道将远程端口 (如数据库、开发服务、API) 映射至本地访问。',
|
|
376
|
+
tLocalPort: '本地监听端口',
|
|
377
|
+
tRemotePort: '远程目标端口',
|
|
378
|
+
tOpenBtn: '+ 建立隧道',
|
|
379
|
+
tOpening: '正在建立…',
|
|
380
|
+
tStopBtn: '关闭',
|
|
381
|
+
tNoTunnels: '当前没有运行中的端口转发隧道。',
|
|
382
|
+
tunnelsColId: '隧道标识',
|
|
383
|
+
tunnelsColRoute: '转发路由',
|
|
384
|
+
tunnelsColActions: '管理操作',
|
|
385
|
+
tOpenOk: '隧道已启动: 127.0.0.1:{localPort} -> Remote:{remotePort}',
|
|
386
|
+
tOpenErr: '启动隧道失败: {err}'
|
|
303
387
|
}
|
|
304
388
|
|
|
305
389
|
function makeT(dict, fallback) {
|
|
@@ -330,6 +414,110 @@ window.__ModuleLoader__.load({
|
|
|
330
414
|
|
|
331
415
|
|
|
332
416
|
|
|
417
|
+
|
|
418
|
+
// --- Sub-component: Remote Environment Tab (New in v0.3.1) ---
|
|
419
|
+
function EnvTab(props) {
|
|
420
|
+
const { activeProfile, t } = props
|
|
421
|
+
const [envEntries, setEnvEntries] = React.useState([])
|
|
422
|
+
const [loading, setLoading] = React.useState(false)
|
|
423
|
+
const [visibleKeys, setVisibleKeys] = React.useState({})
|
|
424
|
+
const [editValues, setEditValues] = React.useState({})
|
|
425
|
+
const [msg, setMsg] = React.useState('')
|
|
426
|
+
|
|
427
|
+
const loadEnv = async () => {
|
|
428
|
+
if (!activeProfile) return
|
|
429
|
+
setLoading(true)
|
|
430
|
+
try {
|
|
431
|
+
const res = await fetch('/dsh-remote-workspace/env/view', {
|
|
432
|
+
method: 'POST',
|
|
433
|
+
headers: { 'Content-Type': 'application/json' },
|
|
434
|
+
body: JSON.stringify({ profileId: activeProfile.id })
|
|
435
|
+
})
|
|
436
|
+
const data = await res.json()
|
|
437
|
+
if (data.ok && Array.isArray(data.entries)) {
|
|
438
|
+
setEnvEntries(data.entries.filter((e) => e.type === 'var'))
|
|
439
|
+
const initialEdits = {}
|
|
440
|
+
data.entries.forEach((e) => {
|
|
441
|
+
if (e.type === 'var') initialEdits[e.key] = e.value
|
|
442
|
+
})
|
|
443
|
+
setEditValues(initialEdits)
|
|
444
|
+
}
|
|
445
|
+
} catch (err) {
|
|
446
|
+
setMsg(`Error loading .env: ${err.message}`)
|
|
447
|
+
}
|
|
448
|
+
setLoading(false)
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
React.useEffect(() => {
|
|
452
|
+
loadEnv()
|
|
453
|
+
}, [activeProfile?.id])
|
|
454
|
+
|
|
455
|
+
const toggleVisible = (key) => {
|
|
456
|
+
setVisibleKeys((prev) => ({ ...prev, [key]: !prev[key] }))
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
const saveVar = async (key) => {
|
|
460
|
+
try {
|
|
461
|
+
const res = await fetch('/dsh-remote-workspace/env/save', {
|
|
462
|
+
method: 'POST',
|
|
463
|
+
headers: { 'Content-Type': 'application/json' },
|
|
464
|
+
body: JSON.stringify({
|
|
465
|
+
profileId: activeProfile.id,
|
|
466
|
+
key,
|
|
467
|
+
value: editValues[key] || ''
|
|
468
|
+
})
|
|
469
|
+
})
|
|
470
|
+
const data = await res.json()
|
|
471
|
+
if (data.ok) {
|
|
472
|
+
setMsg(`Saved ${key} successfully!`)
|
|
473
|
+
setTimeout(() => setMsg(''), 3000)
|
|
474
|
+
}
|
|
475
|
+
} catch (err) {
|
|
476
|
+
setMsg(`Save failed: ${err.message}`)
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
return React.createElement(
|
|
481
|
+
'div',
|
|
482
|
+
{ style: { display: 'flex', flexDirection: 'column', gap: '14px' } },
|
|
483
|
+
React.createElement(
|
|
484
|
+
'div',
|
|
485
|
+
{ style: { display: 'flex', justifyContent: 'space-between', alignItems: 'center' } },
|
|
486
|
+
React.createElement('div', { style: { fontWeight: 600, fontSize: '14px' } }, t('envTitle')),
|
|
487
|
+
React.createElement('button', { type: 'button', className: 'drw-btn', onClick: loadEnv, disabled: loading }, loading ? 'Loading...' : '🔄 Refresh')
|
|
488
|
+
),
|
|
489
|
+
msg ? React.createElement('div', { className: 'drw-badge drw-badge-ok' }, msg) : null,
|
|
490
|
+
React.createElement(
|
|
491
|
+
'div',
|
|
492
|
+
{ style: { border: '1px solid var(--dsw-alias-border-l2)', borderRadius: '8px', padding: '12px', background: 'var(--dsw-alias-bg-layer-2)' } },
|
|
493
|
+
envEntries.length === 0
|
|
494
|
+
? React.createElement('div', { style: { textAlign: 'center', padding: '16px', color: 'var(--dsw-alias-label-secondary)' } }, 'No variables found in remote .env')
|
|
495
|
+
: envEntries.map((e) =>
|
|
496
|
+
React.createElement(
|
|
497
|
+
'div',
|
|
498
|
+
{ key: e.key, className: 'drw-env-row' },
|
|
499
|
+
React.createElement('span', { style: { fontWeight: 600, fontFamily: 'monospace', fontSize: '12.5px' } }, e.key),
|
|
500
|
+
React.createElement('input', {
|
|
501
|
+
type: visibleKeys[e.key] ? 'text' : 'password',
|
|
502
|
+
className: 'drw-input',
|
|
503
|
+
value: editValues[e.key] !== undefined ? editValues[e.key] : e.value,
|
|
504
|
+
onChange: (ev) => {
|
|
505
|
+
const val = ev.target.value
|
|
506
|
+
setEditValues((prev) => ({ ...prev, [e.key]: val }))
|
|
507
|
+
}
|
|
508
|
+
}),
|
|
509
|
+
React.createElement(
|
|
510
|
+
'div',
|
|
511
|
+
{ style: { display: 'flex', gap: '4px', justifyContent: 'flex-end' } },
|
|
512
|
+
React.createElement('button', { type: 'button', className: 'drw-btn', style: { padding: '2px 6px', fontSize: '11px' }, onClick: () => toggleVisible(e.key) }, visibleKeys[e.key] ? t('envHideVal') : t('envShowVal')),
|
|
513
|
+
React.createElement('button', { type: 'button', className: 'drw-btn drw-btn-primary', style: { padding: '2px 6px', fontSize: '11px' }, onClick: () => saveVar(e.key) }, t('envSaveVar'))
|
|
514
|
+
)
|
|
515
|
+
)
|
|
516
|
+
)
|
|
517
|
+
)
|
|
518
|
+
)
|
|
519
|
+
}
|
|
520
|
+
|
|
333
521
|
// --- Sub-component: Docker Containers Tab ---
|
|
334
522
|
function ContainersTab(props) {
|
|
335
523
|
const { activeProfile, t } = props
|
|
@@ -926,7 +1114,7 @@ window.__ModuleLoader__.load({
|
|
|
926
1114
|
const [healthData, setHealthData] = React.useState(null)
|
|
927
1115
|
const [autoSync, setAutoSync] = React.useState(false)
|
|
928
1116
|
|
|
929
|
-
const t = props.locale === '
|
|
1117
|
+
const t = props.t || (props.locale === 'zh' ? makeT(zh, en) : makeT(en, zh))
|
|
930
1118
|
const [profiles, setProfiles] = React.useState([])
|
|
931
1119
|
const [activeId, setActiveId] = React.useState(null)
|
|
932
1120
|
const [tunnels, setTunnels] = React.useState([])
|
|
@@ -1866,7 +2054,7 @@ window.__ModuleLoader__.load({
|
|
|
1866
2054
|
// Accordion item for Settings -> Plugins section
|
|
1867
2055
|
function PluginCard(props) {
|
|
1868
2056
|
const [open, setOpen] = React.useState(false)
|
|
1869
|
-
const t = props.locale === '
|
|
2057
|
+
const t = props.t || (props.locale === 'zh' ? makeT(zh, en) : makeT(en, zh))
|
|
1870
2058
|
|
|
1871
2059
|
React.useEffect(() => {
|
|
1872
2060
|
ensureCss()
|
|
@@ -1956,7 +2144,7 @@ window.__ModuleLoader__.load({
|
|
|
1956
2144
|
|
|
1957
2145
|
function apply(ctx) {
|
|
1958
2146
|
if (ctx.locale && typeof ctx.locale.register === 'function') {
|
|
1959
|
-
try { ctx.locale.register(NS, { en,
|
|
2147
|
+
try { ctx.locale.register(NS, { en, zh }) } catch (_) {}
|
|
1960
2148
|
}
|
|
1961
2149
|
|
|
1962
2150
|
if (ctx.slots && typeof ctx.slots.inject === 'function') {
|