@zhangfengshun/dsh-remote-ssh 2.3.7 → 2.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/CHANGELOG.md +5 -0
- package/lib/client.js +1367 -1367
- package/lib/index.js +10 -3
- package/package.json +1 -1
package/lib/client.js
CHANGED
|
@@ -1,1367 +1,1367 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @zhangfengshun/dsh-remote-ssh — Client 半边(静态 DSH Web 插件,浏览器 bundle)
|
|
3
|
-
*
|
|
4
|
-
* 远程文件与远程终端已合并到内置「文件」和「终端」页签:
|
|
5
|
-
* - 远程文件:内置「文件」页签通过 fs.* 拦截直接 SSH 读写远程文件,无需同步。
|
|
6
|
-
* 远程终端由内置「终端」页签提供(shell wrapper 自动 SSH)。
|
|
7
|
-
* - 远程终端:shell wrapper 自动检测工作区 .remote-ssh.json,在远程工作区中打开 SSH 终端。
|
|
8
|
-
* 需在 better-sidebar 设置中将 shell 指向 wrapper 脚本(见 README)。
|
|
9
|
-
* 仍保留隐藏的 remssh:editor 页签用于远程文件编辑器。
|
|
10
|
-
* 在 DSH 设置页注册「远程连接」小节管理连接配置和远程工作区;
|
|
11
|
-
* 同时接管原生「添加工作区」流程提供「本地目录 / 远程目录」两选。
|
|
12
|
-
*/
|
|
13
|
-
window.__ModuleLoader__.load({
|
|
14
|
-
id: "@zhangfengshun/dsh-remote-ssh",
|
|
15
|
-
factory: (require) => {
|
|
16
|
-
var module = { exports: {} };
|
|
17
|
-
var exports = module.exports;
|
|
18
|
-
var React = require("react");
|
|
19
|
-
var primitives = require("@deepseek-ai/dsh-client-ui-primitives");
|
|
20
|
-
var Modal = primitives.Modal;
|
|
21
|
-
|
|
22
|
-
function h(tag, props) {
|
|
23
|
-
var args = [tag, props];
|
|
24
|
-
for (var i = 2; i < arguments.length; i++) args.push(arguments[i]);
|
|
25
|
-
return React.createElement.apply(React, args);
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
var CSS =
|
|
29
|
-
// 颜色全部使用 DSH 主题 token(--dsw-alias-*),跟随 body[data-ds-dark-theme] 自动切换明暗主题。
|
|
30
|
-
".rssh-row{margin:6px 0}.rssh-label{color:var(--dsw-alias-label-caption);font-size:11px;display:block;margin-bottom:2px}" +
|
|
31
|
-
".rssh-input{width:100%;box-sizing:border-box;background:var(--dsw-specific-input-major);color:var(--dsw-alias-label-primary);border:1px solid var(--dsw-alias-border-l2);border-radius:4px;padding:5px 7px;margin:3px 0;font-size:12px}" +
|
|
32
|
-
".rssh-select{width:100%;box-sizing:border-box;background:var(--dsw-specific-input-major);color:var(--dsw-alias-label-primary);border:1px solid var(--dsw-alias-border-l2);border-radius:4px;padding:5px 7px;margin:3px 0;font-size:12px}" +
|
|
33
|
-
".rssh-btn{background:var(--dsw-alias-button-ghost-active-fill);color:var(--dsw-alias-label-primary);border:1px solid var(--dsw-alias-border-l2);border-radius:4px;padding:4px 9px;cursor:pointer;font-size:12px}" +
|
|
34
|
-
".rssh-btn:hover{background:var(--dsw-alias-button-ghost-active-hover)}.rssh-btn:disabled{opacity:.5;cursor:default}" +
|
|
35
|
-
".rssh-card{padding:8px 10px;border:1px solid var(--dsw-alias-border-l1);border-radius:6px;margin:8px 0;background:var(--dsw-alias-bg-layer-2)}" +
|
|
36
|
-
".rssh-title{font-weight:600;font-size:13px}.rssh-sub{color:var(--dsw-alias-label-tertiary);font-size:11px}" +
|
|
37
|
-
".rssh-actions{margin-top:6px;display:flex;gap:5px;flex-wrap:wrap}" +
|
|
38
|
-
".rssh-tree-item{display:flex;align-items:center;gap:5px;padding:3px 5px;cursor:pointer;border-radius:3px}" +
|
|
39
|
-
".rssh-tree-item:hover{background:var(--dsw-alias-interactive-bg-hover)}" +
|
|
40
|
-
".rssh-tree-dir{color:var(--dsw-alias-state-business-primary)}.rssh-tree-file{color:var(--dsw-alias-label-primary)}" +
|
|
41
|
-
".rssh-tree-size{color:var(--dsw-alias-label-caption);font-size:11px;margin-left:auto}" +
|
|
42
|
-
".rssh-pathbar{display:flex;gap:4px;align-items:center;margin:4px 0}" +
|
|
43
|
-
".rssh-pathbar .rssh-input{flex:1;margin:0}" +
|
|
44
|
-
".rssh-content{white-space:pre-wrap;font-family:Consolas,monospace;font-size:12px;background:var(--dsw-alias-markdown-code-block);color:var(--dsw-alias-label-primary);padding:8px;border-radius:4px;margin-top:6px;max-height:400px;overflow:auto}" +
|
|
45
|
-
".rssh-terminal{background:var(--dsw-alias-markdown-code-block);color:var(--dsw-alias-label-primary);border-radius:4px;padding:6px;margin-top:8px;font-family:Consolas,monospace;font-size:12px}" +
|
|
46
|
-
".rssh-terminal-out{white-space:pre-wrap;max-height:320px;overflow:auto;min-height:120px;line-height:1.4}" +
|
|
47
|
-
".rssh-terminal-bar{display:flex;gap:5px;align-items:center;margin-bottom:4px}" +
|
|
48
|
-
".rssh-terminal-input-row{display:flex;gap:4px;margin-top:5px}.rssh-terminal-input-row .rssh-input{flex:1}" +
|
|
49
|
-
".rssh-ok{color:var(--dsw-alias-state-success-primary)}.rssh-err{color:var(--dsw-alias-state-error-primary)}.rssh-muted{color:var(--dsw-alias-label-tertiary);font-size:12px;padding:6px 0}" +
|
|
50
|
-
".rssh-empty{padding:12px;text-align:center;color:var(--dsw-alias-label-tertiary);font-size:12px}" +
|
|
51
|
-
".rssh-flow-dialog{width:690px!important;max-width:94vw!important;min-width:460px;resize:horizontal;overflow:auto!important}" +
|
|
52
|
-
// 添加工作区弹窗:上方本地/远程分段切换 + 路径栏 + 文件树
|
|
53
|
-
".rssh-seg{display:flex;gap:0;margin-bottom:8px;border:1px solid var(--dsw-alias-border-l2);border-radius:6px;overflow:hidden}" +
|
|
54
|
-
".rssh-seg-btn{flex:1;padding:7px 12px;text-align:center;cursor:pointer;font-size:12px;font-weight:500;background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-label-secondary);border:none;transition:background .15s}" +
|
|
55
|
-
".rssh-seg-btn:hover{background:var(--dsw-alias-interactive-bg-hover)}" +
|
|
56
|
-
".rssh-seg-btn.active{background:var(--dsw-alias-state-business-primary);color:#fff}" +
|
|
57
|
-
".rssh-pick-tree{border:1px solid var(--dsw-alias-border-l1);border-radius:4px;max-height:240px;overflow-y:auto;margin-top:4px;background:var(--dsw-alias-bg-layer-2)}" +
|
|
58
|
-
".rssh-pick-empty{padding:12px;text-align:center;color:var(--dsw-alias-label-tertiary);font-size:11px}";
|
|
59
|
-
|
|
60
|
-
// ---- 国际化(跟随 DSH 设置中的语言自动切换)----
|
|
61
|
-
var NS = "dsh-remote-ssh";
|
|
62
|
-
var zh = {
|
|
63
|
-
"tabs.files": "远程文件",
|
|
64
|
-
"tabs.term": "远程终端",
|
|
65
|
-
"settings.nav": "🖥️ 远程连接",
|
|
66
|
-
"settings.desc": "管理 Remote-SSH 的远程连接配置(SSH 密钥或密码认证)。",
|
|
67
|
-
"settings.empty": "还没有连接配置。请在下方添加。",
|
|
68
|
-
"settings.add": "添加连接",
|
|
69
|
-
"settings.workspaces": "远程工作区",
|
|
70
|
-
"settings.name": "名称",
|
|
71
|
-
"settings.namePlaceholder": "如:我的超算",
|
|
72
|
-
"settings.host": "主机 host",
|
|
73
|
-
"settings.hostPlaceholder": "login.example.com",
|
|
74
|
-
"settings.port": "端口 port",
|
|
75
|
-
"settings.user": "用户名 user",
|
|
76
|
-
"settings.userPlaceholder": "your-username",
|
|
77
|
-
"settings.auth": "认证方式",
|
|
78
|
-
"settings.key": "密钥 (推荐)",
|
|
79
|
-
"settings.password": "密码 (需 sshpass)",
|
|
80
|
-
"settings.keyPath": "私钥路径 keyPath",
|
|
81
|
-
"settings.keyPathPlaceholder": "~/.ssh/id_rsa",
|
|
82
|
-
"settings.passwordField": "密码 password",
|
|
83
|
-
"settings.remoteRoot": "远程根目录 remoteRoot",
|
|
84
|
-
"settings.proxyJump": "跳板机 ProxyJump",
|
|
85
|
-
"settings.test": "测试连接",
|
|
86
|
-
"settings.saved": "已保存",
|
|
87
|
-
"settings.connOk": "连接成功",
|
|
88
|
-
"settings.connFail": "连接失败: ",
|
|
89
|
-
"settings.errSave": "保存失败",
|
|
90
|
-
"common.selectProfile": "— 选择连接 —",
|
|
91
|
-
"common.profile": "连接配置",
|
|
92
|
-
"common.save": "保存",
|
|
93
|
-
"common.open": "打开",
|
|
94
|
-
"common.close": "关闭",
|
|
95
|
-
"common.noEntries": "(无子目录)",
|
|
96
|
-
"common.delete": "删除",
|
|
97
|
-
"common.up": "⬆ 上级",
|
|
98
|
-
"common.loading": "加载中…",
|
|
99
|
-
"common.processing": "处理中…",
|
|
100
|
-
"common.errList": "读取目录失败",
|
|
101
|
-
"files.workspace": "远程工作区",
|
|
102
|
-
"files.selectWorkspace": "— 选择工作区 —",
|
|
103
|
-
"files.orProfile": "或直接选连接配置",
|
|
104
|
-
"files.defaultDir": "默认目录(打开该工作区时进入的远程目录)",
|
|
105
|
-
"files.reading": "读取中…",
|
|
106
|
-
"files.errRead": "读取失败",
|
|
107
|
-
"files.errSave": "保存失败",
|
|
108
|
-
"files.errSaveDefaultDir": "保存默认目录失败",
|
|
109
|
-
"files.binary": "二进制文件,无法预览",
|
|
110
|
-
"term.title": "终端 {id}",
|
|
111
|
-
"term.exited": " (已退出)",
|
|
112
|
-
"term.connecting": "正在连接…",
|
|
113
|
-
"term.noOutput": "(无输出)",
|
|
114
|
-
"term.inputPlaceholder": "输入命令后回车…",
|
|
115
|
-
"term.send": "发送",
|
|
116
|
-
"term.needProfile": "请先选择连接配置",
|
|
117
|
-
"term.newTerminal": "+ 新建终端",
|
|
118
|
-
"term.emptyHint": "从上面的连接配置新建一个终端,直接操作远程环境。",
|
|
119
|
-
"term.errSpawn": "启动终端失败",
|
|
120
|
-
"picker.dir": "目录",
|
|
121
|
-
"picker.choose": "选择此目录",
|
|
122
|
-
"flow.localTitle": "选择本地目录",
|
|
123
|
-
"flow.remoteTitle": "选择远程目录",
|
|
124
|
-
"flow.localDir": "本地目录",
|
|
125
|
-
"flow.remoteDir": "远程目录",
|
|
126
|
-
"flow.addWsTitle": "添加工作区",
|
|
127
|
-
"flow.localSeg": "📁 本地目录",
|
|
128
|
-
"flow.remoteSeg": "🌐 远程目录",
|
|
129
|
-
"flow.browse": "浏览",
|
|
130
|
-
"flow.chooseRemote": "选择远程目录…",
|
|
131
|
-
"flow.backLocal": "← 选择本地目录",
|
|
132
|
-
"flow.errCreate": "创建远程工作区失败",
|
|
133
|
-
"settings.importSshConfig": "从 ~/.ssh/config 导入…",
|
|
134
|
-
"settings.importTitle": "导入 ~/.ssh/config 主机",
|
|
135
|
-
"settings.noneFound": "未在 ~/.ssh/config 中发现可用主机",
|
|
136
|
-
"settings.imported": "已导入 {n} 个连接",
|
|
137
|
-
"sync.down": "⬇ 同步",
|
|
138
|
-
"sync.up": "⬆ 推送",
|
|
139
|
-
"sync.syncing": "同步中…",
|
|
140
|
-
"sync.pushing": "推送中…",
|
|
141
|
-
"sync.done": "同步完成",
|
|
142
|
-
"sync.fail": "同步失败",
|
|
143
|
-
"ops.search": "🔍 搜索",
|
|
144
|
-
"ops.find": "按名查找",
|
|
145
|
-
"ops.searchPh": "正则或关键词…",
|
|
146
|
-
"ops.globPh": "*.py",
|
|
147
|
-
"ops.newDir": "📁 新建目录",
|
|
148
|
-
"ops.newDirPh": "目录名",
|
|
149
|
-
"ops.rename": "✏ 重命名/移动",
|
|
150
|
-
"ops.renamePh": "目标路径",
|
|
151
|
-
"ops.delete": "🗑 删除",
|
|
152
|
-
"ops.confirmDel": "确认删除远程文件/目录?(不可恢复)",
|
|
153
|
-
"ops.grepTitle": "远程内容搜索 (grep)",
|
|
154
|
-
"ops.globTitle": "按文件名查找 (glob)",
|
|
155
|
-
"ops.mkdirTitle": "新建远程目录",
|
|
156
|
-
"ops.moveTitle": "移动/重命名",
|
|
157
|
-
"ops.deleteTitle": "删除远程文件/目录",
|
|
158
|
-
"ops.noMatch": "无匹配",
|
|
159
|
-
"ops.done": "操作完成",
|
|
160
|
-
"ops.fail": "操作失败"
|
|
161
|
-
};
|
|
162
|
-
var en = {
|
|
163
|
-
"tabs.files": "Remote Files",
|
|
164
|
-
"tabs.term": "Remote Terminal",
|
|
165
|
-
"settings.nav": "🖥️ Remote Connections",
|
|
166
|
-
"settings.desc": "Manage Remote-SSH connection profiles (SSH key or password authentication).",
|
|
167
|
-
"settings.empty": "No connection profiles yet. Add one below.",
|
|
168
|
-
"settings.add": "Add Profile",
|
|
169
|
-
"settings.workspaces": "Remote Workspaces",
|
|
170
|
-
"settings.name": "Name",
|
|
171
|
-
"settings.namePlaceholder": "e.g. My HPC",
|
|
172
|
-
"settings.host": "Host",
|
|
173
|
-
"settings.hostPlaceholder": "login.example.com",
|
|
174
|
-
"settings.port": "Port",
|
|
175
|
-
"settings.user": "User",
|
|
176
|
-
"settings.userPlaceholder": "your-username",
|
|
177
|
-
"settings.auth": "Authentication",
|
|
178
|
-
"settings.key": "Key (recommended)",
|
|
179
|
-
"settings.password": "Password (requires sshpass)",
|
|
180
|
-
"settings.keyPath": "Private key path (keyPath)",
|
|
181
|
-
"settings.keyPathPlaceholder": "~/.ssh/id_rsa",
|
|
182
|
-
"settings.passwordField": "Password",
|
|
183
|
-
"settings.remoteRoot": "Remote root (remoteRoot)",
|
|
184
|
-
"settings.proxyJump": "ProxyJump (jump host)",
|
|
185
|
-
"settings.test": "Test Connection",
|
|
186
|
-
"settings.saved": "Saved",
|
|
187
|
-
"settings.connOk": "Connected",
|
|
188
|
-
"settings.connFail": "Connection failed: ",
|
|
189
|
-
"settings.errSave": "Save failed",
|
|
190
|
-
"common.selectProfile": "— Select profile —",
|
|
191
|
-
"common.profile": "Profile",
|
|
192
|
-
"common.save": "Save",
|
|
193
|
-
"common.open": "Open",
|
|
194
|
-
"common.close": "Close",
|
|
195
|
-
"common.noEntries": "(no subdirectories)",
|
|
196
|
-
"common.delete": "Delete",
|
|
197
|
-
"common.up": "⬆ Up",
|
|
198
|
-
"common.loading": "Loading…",
|
|
199
|
-
"common.processing": "Processing…",
|
|
200
|
-
"common.errList": "Failed to list directory",
|
|
201
|
-
"files.workspace": "Remote Workspace",
|
|
202
|
-
"files.selectWorkspace": "— Select workspace —",
|
|
203
|
-
"files.orProfile": "or pick a profile directly",
|
|
204
|
-
"files.defaultDir": "Default directory (entered when opening this workspace)",
|
|
205
|
-
"files.reading": "Reading…",
|
|
206
|
-
"files.errRead": "Failed to read",
|
|
207
|
-
"files.errSave": "Failed to save",
|
|
208
|
-
"files.errSaveDefaultDir": "Failed to save default directory",
|
|
209
|
-
"files.binary": "Binary file — preview not available",
|
|
210
|
-
"term.title": "Terminal {id}",
|
|
211
|
-
"term.exited": " (exited)",
|
|
212
|
-
"term.connecting": "Connecting…",
|
|
213
|
-
"term.noOutput": "(no output)",
|
|
214
|
-
"term.inputPlaceholder": "Type a command and press Enter…",
|
|
215
|
-
"term.send": "Send",
|
|
216
|
-
"term.needProfile": "Select a profile first",
|
|
217
|
-
"term.newTerminal": "+ New terminal",
|
|
218
|
-
"term.emptyHint": "Create a terminal from the profile above to operate the remote environment.",
|
|
219
|
-
"term.errSpawn": "Failed to start terminal",
|
|
220
|
-
"picker.dir": "Directory",
|
|
221
|
-
"picker.choose": "Choose this directory",
|
|
222
|
-
"flow.localTitle": "Select Local Directory",
|
|
223
|
-
"flow.remoteTitle": "Select Remote Directory",
|
|
224
|
-
"flow.localDir": "Local directory",
|
|
225
|
-
"flow.remoteDir": "Remote directory",
|
|
226
|
-
"flow.addWsTitle": "Add Workspace",
|
|
227
|
-
"flow.localSeg": "📁 Local",
|
|
228
|
-
"flow.remoteSeg": "🌐 Remote",
|
|
229
|
-
"flow.browse": "Browse",
|
|
230
|
-
"flow.chooseRemote": "Choose remote directory…",
|
|
231
|
-
"flow.backLocal": "← Choose local directory",
|
|
232
|
-
"flow.errCreate": "Failed to create remote workspace",
|
|
233
|
-
"settings.importSshConfig": "Import from ~/.ssh/config…",
|
|
234
|
-
"settings.importTitle": "Import ~/.ssh/config hosts",
|
|
235
|
-
"settings.noneFound": "No usable hosts found in ~/.ssh/config",
|
|
236
|
-
"settings.imported": "Imported {n} connection(s)",
|
|
237
|
-
"sync.down": "⬇ Sync",
|
|
238
|
-
"sync.up": "⬆ Push",
|
|
239
|
-
"sync.syncing": "Syncing…",
|
|
240
|
-
"sync.pushing": "Pushing…",
|
|
241
|
-
"sync.done": "Sync complete",
|
|
242
|
-
"sync.fail": "Sync failed",
|
|
243
|
-
"ops.search": "🔍 Search",
|
|
244
|
-
"ops.find": "Find by name",
|
|
245
|
-
"ops.searchPh": "regex or keyword…",
|
|
246
|
-
"ops.globPh": "*.py",
|
|
247
|
-
"ops.newDir": "📁 New dir",
|
|
248
|
-
"ops.newDirPh": "dir name",
|
|
249
|
-
"ops.rename": "✏ Rename/Move",
|
|
250
|
-
"ops.renamePh": "target path",
|
|
251
|
-
"ops.delete": "🗑 Delete",
|
|
252
|
-
"ops.confirmDel": "Confirm delete remote file/dir? (irreversible)",
|
|
253
|
-
"ops.grepTitle": "Remote content search (grep)",
|
|
254
|
-
"ops.globTitle": "Find by name (glob)",
|
|
255
|
-
"ops.mkdirTitle": "Create remote directory",
|
|
256
|
-
"ops.moveTitle": "Move/Rename",
|
|
257
|
-
"ops.deleteTitle": "Delete remote file/dir",
|
|
258
|
-
"ops.noMatch": "No match",
|
|
259
|
-
"ops.done": "Done",
|
|
260
|
-
"ops.fail": "Operation failed"
|
|
261
|
-
};
|
|
262
|
-
var i18n = {
|
|
263
|
-
t: function (key) { return key; },
|
|
264
|
-
subscribe: function () { return function () {}; },
|
|
265
|
-
getSnapshot: function () { return 0; }
|
|
266
|
-
};
|
|
267
|
-
function useT() {
|
|
268
|
-
var state = React.useState(0);
|
|
269
|
-
var force = state[1];
|
|
270
|
-
React.useEffect(function () {
|
|
271
|
-
return i18n.subscribe(function () { force(function (n) { return n + 1; }); });
|
|
272
|
-
}, []);
|
|
273
|
-
return i18n.t;
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
// ---- 跨会话切换的状态记忆(按 sessionId 分桶,模块级存活,页面刷新即失效)----
|
|
277
|
-
var memoryBySession = {};
|
|
278
|
-
function useSessionMemory(sessionId, key, initial) {
|
|
279
|
-
var sid = sessionId || "global";
|
|
280
|
-
var bucket = memoryBySession[sid] || (memoryBySession[sid] = {});
|
|
281
|
-
var state = React.useState(function () {
|
|
282
|
-
if (Object.prototype.hasOwnProperty.call(bucket, key)) return bucket[key];
|
|
283
|
-
var init = typeof initial === "function" ? initial() : initial;
|
|
284
|
-
return bucket[key] = init;
|
|
285
|
-
});
|
|
286
|
-
var value = state[0], setValue = state[1];
|
|
287
|
-
React.useEffect(function () { bucket[key] = value; }, [value]);
|
|
288
|
-
return [value, setValue];
|
|
289
|
-
}
|
|
290
|
-
|
|
291
|
-
// ---- 与 Host 半边的 JSON RPC ----
|
|
292
|
-
async function api(method, args) {
|
|
293
|
-
try {
|
|
294
|
-
var r = await fetch("/remote-ssh/api/" + method, {
|
|
295
|
-
method: "POST",
|
|
296
|
-
headers: { "content-type": "application/json" },
|
|
297
|
-
body: JSON.stringify(args || {})
|
|
298
|
-
});
|
|
299
|
-
var j = await r.json();
|
|
300
|
-
if (j && j.ok) return j.value;
|
|
301
|
-
return { ok: false, error: (j && j.error && j.error.message) || ("HTTP " + r.status) };
|
|
302
|
-
} catch (e) {
|
|
303
|
-
return { ok: false, error: String(e && e.message ? e.message : e) };
|
|
304
|
-
}
|
|
305
|
-
}
|
|
306
|
-
|
|
307
|
-
// ---- 跨页签「打开工作区」的轻量共享选择 ----
|
|
308
|
-
var sel = { workspaceId: null };
|
|
309
|
-
var selListeners = [];
|
|
310
|
-
function setSelWorkspace(id) { sel.workspaceId = id; selListeners.forEach(function (fn) { fn(); }); }
|
|
311
|
-
function subscribeSel(fn) { selListeners.push(fn); return function () { var i = selListeners.indexOf(fn); if (i >= 0) selListeners.splice(i, 1); }; }
|
|
312
|
-
|
|
313
|
-
// ---- 通用:拉取连接配置(refreshKey 变化时重新拉取,避免缓存过期)----
|
|
314
|
-
function useProfiles(refreshKey) {
|
|
315
|
-
var state = React.useState([]);
|
|
316
|
-
var profiles = state[0], setProfiles = state[1];
|
|
317
|
-
React.useEffect(function () {
|
|
318
|
-
api("listProfiles").then(function (r) { if (r && r.ok) setProfiles(r.profiles || []); });
|
|
319
|
-
}, [refreshKey]);
|
|
320
|
-
return profiles;
|
|
321
|
-
}
|
|
322
|
-
|
|
323
|
-
function Field(props) {
|
|
324
|
-
return h("div", { className: "rssh-row" },
|
|
325
|
-
h("span", { className: "rssh-label" }, props.label),
|
|
326
|
-
h("input", { className: "rssh-input", type: props.type || "text", value: props.value, placeholder: props.placeholder,
|
|
327
|
-
onChange: function (e) { props.onChange(e.target.value); } })
|
|
328
|
-
);
|
|
329
|
-
}
|
|
330
|
-
|
|
331
|
-
// ======================================================================
|
|
332
|
-
// 远程操作弹窗(grep / glob / mkdir / move / delete)
|
|
333
|
-
// ======================================================================
|
|
334
|
-
function OpsModal(props) {
|
|
335
|
-
var d = props.opsDlg;
|
|
336
|
-
var t = props.t;
|
|
337
|
-
var patchOps = props.patchOps;
|
|
338
|
-
var runOps = props.runOps;
|
|
339
|
-
var title = ({
|
|
340
|
-
grep: t("ops.grepTitle"), glob: t("ops.globTitle"), mkdir: t("ops.mkdirTitle"),
|
|
341
|
-
move: t("ops.moveTitle"), delete: t("ops.deleteTitle")
|
|
342
|
-
})[d.type] || "";
|
|
343
|
-
var isGrep = d.type === "grep";
|
|
344
|
-
var isGlob = d.type === "glob";
|
|
345
|
-
var isMove = d.type === "move";
|
|
346
|
-
var isDel = d.type === "delete";
|
|
347
|
-
// 结果渲染
|
|
348
|
-
var resultEl = null;
|
|
349
|
-
if (d.result) {
|
|
350
|
-
var r = d.result;
|
|
351
|
-
if (isGrep) {
|
|
352
|
-
var ms = r.matches || [];
|
|
353
|
-
resultEl = h("div", { style: { maxHeight: 260, overflow: "auto", marginTop: 8, fontSize: 11, whiteSpace: "pre-wrap", fontFamily: "monospace" } },
|
|
354
|
-
!r.ok ? h("div", { className: "rssh-err" }, r.error || t("ops.fail")) :
|
|
355
|
-
ms.length === 0 ? h("div", { className: "rssh-muted" }, t("ops.noMatch")) :
|
|
356
|
-
ms.map(function (m, i) { return h("div", { key: i }, m.file + ":" + m.line + ": " + m.content); }),
|
|
357
|
-
r.truncated ? h("div", { className: "rssh-muted" }, "…") : null
|
|
358
|
-
);
|
|
359
|
-
} else if (isGlob) {
|
|
360
|
-
var fs2 = r.files || [];
|
|
361
|
-
resultEl = h("div", { style: { maxHeight: 260, overflow: "auto", marginTop: 8, fontSize: 11, whiteSpace: "pre-wrap", fontFamily: "monospace" } },
|
|
362
|
-
!r.ok ? h("div", { className: "rssh-err" }, r.error || t("ops.fail")) :
|
|
363
|
-
fs2.length === 0 ? h("div", { className: "rssh-muted" }, t("ops.noMatch")) :
|
|
364
|
-
fs2.map(function (f, i) { return h("div", { key: i }, f); })
|
|
365
|
-
);
|
|
366
|
-
} else {
|
|
367
|
-
// exec 结果(mkdir/move/delete)
|
|
368
|
-
resultEl = h("div", { style: { marginTop: 8 } },
|
|
369
|
-
r.ok ? h("div", { className: "rssh-ok" }, t("ops.done") + (r.stderr ? " " + String(r.stderr).slice(0, 200) : "")) :
|
|
370
|
-
h("div", { className: "rssh-err" }, (r.error || r.stderr || t("ops.fail")))
|
|
371
|
-
);
|
|
372
|
-
}
|
|
373
|
-
}
|
|
374
|
-
return h(Modal, { open: true, onClose: props.close, title: title, closeLabel: t("common.close"), className: "rssh-flow-dialog" },
|
|
375
|
-
h("div", { style: { display: "flex", flexDirection: "column", gap: 6 } },
|
|
376
|
-
h("input", { className: "rssh-input", value: d.p1, onChange: function (e) { patchOps({ p1: e.target.value }); }, placeholder: isGrep ? t("ops.searchPh") : (isGlob ? t("ops.globPh") : t("ops.newDirPh")) }),
|
|
377
|
-
isGrep ? h("input", { className: "rssh-input", value: d.p2, onChange: function (e) { patchOps({ p2: e.target.value }); }, placeholder: "*.py (include)" }) : null,
|
|
378
|
-
isMove ? h("input", { className: "rssh-input", value: d.p2, onChange: function (e) { patchOps({ p2: e.target.value }); }, placeholder: t("ops.renamePh") }) : null,
|
|
379
|
-
h("div", { className: "rssh-actions" },
|
|
380
|
-
h("button", { className: "rssh-btn", onClick: runOps, disabled: d.busy }, d.busy ? t("common.processing") : (isDel ? t("ops.delete") : t("common.save"))),
|
|
381
|
-
h("button", { className: "rssh-btn", onClick: props.close, disabled: d.busy }, t("common.close"))
|
|
382
|
-
),
|
|
383
|
-
resultEl
|
|
384
|
-
)
|
|
385
|
-
);
|
|
386
|
-
}
|
|
387
|
-
|
|
388
|
-
// ======================================================================
|
|
389
|
-
// 远程文件页签
|
|
390
|
-
// ======================================================================
|
|
391
|
-
function FilesTab(props) {
|
|
392
|
-
var t = useT();
|
|
393
|
-
var sessionId = props.sessionId;
|
|
394
|
-
var betterSidebar = props.betterSidebar;
|
|
395
|
-
var [workspaces, setWorkspaces] = React.useState([]);
|
|
396
|
-
var profiles = useProfiles();
|
|
397
|
-
var [wsId, setWsId] = useSessionMemory(sessionId, "wsId", sel.workspaceId);
|
|
398
|
-
var [profileId, setProfileId] = useSessionMemory(sessionId, "profileId", "");
|
|
399
|
-
var [path, setPath] = useSessionMemory(sessionId, "path", "");
|
|
400
|
-
var [entries, setEntries] = useSessionMemory(sessionId, "entries", []);
|
|
401
|
-
var [err, setErr] = React.useState(null);
|
|
402
|
-
var [loading, setLoading] = React.useState(false);
|
|
403
|
-
var [defaultDir, setDefaultDir] = useSessionMemory(sessionId, "defaultDir", "");
|
|
404
|
-
var [opsDlg, setOpsDlg] = React.useState(null); // { type, p1, p2, result, busy }
|
|
405
|
-
|
|
406
|
-
React.useEffect(function () {
|
|
407
|
-
api("listWorkspaces").then(function (r) { if (r && r.ok) setWorkspaces(r.workspaces || []); });
|
|
408
|
-
return subscribeSel(function () { setWsId(sel.workspaceId); });
|
|
409
|
-
}, []);
|
|
410
|
-
|
|
411
|
-
function load(targetProfile, targetPath) {
|
|
412
|
-
setLoading(true); setErr(null);
|
|
413
|
-
api("listDir", { profileId: targetProfile, path: targetPath }).then(function (r) {
|
|
414
|
-
setLoading(false);
|
|
415
|
-
if (r && r.ok) { setEntries(r.entries || []); setPath(r.path || targetPath); }
|
|
416
|
-
else setErr((r && r.error) || t("common.errList"));
|
|
417
|
-
});
|
|
418
|
-
}
|
|
419
|
-
|
|
420
|
-
// 工作区/配置选择变化时进入对应目录
|
|
421
|
-
React.useEffect(function () {
|
|
422
|
-
if (!wsId && !profileId) return;
|
|
423
|
-
var pid = profileId;
|
|
424
|
-
var rootPath = "";
|
|
425
|
-
if (wsId) {
|
|
426
|
-
var ws = workspaces.find(function (w) { return w.id === wsId; });
|
|
427
|
-
if (ws) { pid = ws.profileId; rootPath = ws.remotePath; }
|
|
428
|
-
}
|
|
429
|
-
if (!pid) return;
|
|
430
|
-
setProfileId(pid);
|
|
431
|
-
load(pid, rootPath || undefined);
|
|
432
|
-
}, [wsId, profileId, workspaces]);
|
|
433
|
-
|
|
434
|
-
// 选中工作区时,把「默认目录」输入框同步为该工作区的 remotePath。
|
|
435
|
-
React.useEffect(function () {
|
|
436
|
-
if (wsId) {
|
|
437
|
-
var ws = workspaces.find(function (w) { return w.id === wsId; });
|
|
438
|
-
if (ws) setDefaultDir(ws.remotePath || "");
|
|
439
|
-
}
|
|
440
|
-
}, [wsId, workspaces]);
|
|
441
|
-
|
|
442
|
-
function saveDefaultDir() {
|
|
443
|
-
if (!wsId) return;
|
|
444
|
-
api("updateWorkspace", { id: wsId, remotePath: defaultDir }).then(function (r) {
|
|
445
|
-
if (r && r.ok) { setWorkspaces(r.workspaces || []); setErr(null); }
|
|
446
|
-
else setErr((r && r.error) || t("files.errSaveDefaultDir"));
|
|
447
|
-
});
|
|
448
|
-
}
|
|
449
|
-
|
|
450
|
-
function activeProfileId() {
|
|
451
|
-
if (wsId) {
|
|
452
|
-
var ws = workspaces.find(function (w) { return w.id === wsId; });
|
|
453
|
-
if (ws) return ws.profileId;
|
|
454
|
-
}
|
|
455
|
-
return profileId;
|
|
456
|
-
}
|
|
457
|
-
function opsBase() {
|
|
458
|
-
if (wsId) {
|
|
459
|
-
var ws = workspaces.find(function (w) { return w.id === wsId; });
|
|
460
|
-
if (ws) return ws.remotePath || path || "~";
|
|
461
|
-
}
|
|
462
|
-
return path || "~";
|
|
463
|
-
}
|
|
464
|
-
function openOps(type) {
|
|
465
|
-
var init = { type: type, p1: "", p2: "", result: null, busy: false };
|
|
466
|
-
if (type === "mkdir") init.p1 = path ? (path.replace(/\/+$/, "") + "/") : "";
|
|
467
|
-
if (type === "move") { init.p1 = path ? (path.replace(/\/+$/, "") + "/") : ""; init.p2 = path ? (path.replace(/\/+$/, "") + "/") : ""; }
|
|
468
|
-
if (type === "delete") init.p1 = path ? (path.replace(/\/+$/, "") + "/") : "";
|
|
469
|
-
setOpsDlg(init);
|
|
470
|
-
}
|
|
471
|
-
function patchOps(patch) {
|
|
472
|
-
setOpsDlg(function (d) { var n = {}; for (var k in d) n[k] = d[k]; for (var k2 in patch) n[k2] = patch[k2]; return n; });
|
|
473
|
-
}
|
|
474
|
-
function runOps() {
|
|
475
|
-
if (!opsDlg) return;
|
|
476
|
-
var pid = activeProfileId();
|
|
477
|
-
if (!pid) { patchOps({ result: { ok: false, msg: t("ops.fail") }, busy: false }); return; }
|
|
478
|
-
var base = opsBase();
|
|
479
|
-
var d = opsDlg;
|
|
480
|
-
patchOps({ busy: true, result: null });
|
|
481
|
-
function resolveP(p) {
|
|
482
|
-
if (!p) return p;
|
|
483
|
-
var c = String(p).charAt(0);
|
|
484
|
-
if (c === "/" || c === "~") return p;
|
|
485
|
-
return base.replace(/\/+$/, "") + "/" + p;
|
|
486
|
-
}
|
|
487
|
-
if (d.type === "grep") {
|
|
488
|
-
api("grep", { profileId: pid, pattern: d.p1, path: base, include: d.p2 || undefined }).then(function (r) {
|
|
489
|
-
patchOps({ busy: false, result: r });
|
|
490
|
-
});
|
|
491
|
-
} else if (d.type === "glob") {
|
|
492
|
-
api("glob", { profileId: pid, pattern: d.p1, path: base }).then(function (r) {
|
|
493
|
-
patchOps({ busy: false, result: r });
|
|
494
|
-
});
|
|
495
|
-
} else if (d.type === "mkdir") {
|
|
496
|
-
api("mkdir", { profileId: pid, path: resolveP(d.p1) }).then(function (r) {
|
|
497
|
-
patchOps({ busy: false, result: r });
|
|
498
|
-
if (r && r.ok) { setErr(null); load(pid, path); }
|
|
499
|
-
});
|
|
500
|
-
} else if (d.type === "move") {
|
|
501
|
-
api("move", { profileId: pid, src: resolveP(d.p1), dst: resolveP(d.p2) }).then(function (r) {
|
|
502
|
-
patchOps({ busy: false, result: r });
|
|
503
|
-
if (r && r.ok) { setErr(null); load(pid, path); }
|
|
504
|
-
});
|
|
505
|
-
} else if (d.type === "delete") {
|
|
506
|
-
api("deleteFile", { profileId: pid, path: resolveP(d.p1) }).then(function (r) {
|
|
507
|
-
patchOps({ busy: false, result: r });
|
|
508
|
-
if (r && r.ok) { setErr(null); load(pid, path); }
|
|
509
|
-
});
|
|
510
|
-
}
|
|
511
|
-
}
|
|
512
|
-
|
|
513
|
-
function openEntry(e) {
|
|
514
|
-
var base = path ? (path.replace(/\/+$/, "") + "/" + e.name) : e.name;
|
|
515
|
-
if (e.type === "directory") {
|
|
516
|
-
setPath(base);
|
|
517
|
-
load(profileId, base);
|
|
518
|
-
} else {
|
|
519
|
-
// 用独立页签预览远程文件(与内置「文件」页签一致),而非在文件树下方预览。
|
|
520
|
-
if (betterSidebar && betterSidebar.openTab) {
|
|
521
|
-
betterSidebar.openTab({
|
|
522
|
-
type: "remssh:editor",
|
|
523
|
-
id: "remssh:editor:" + profileId + ":" + base,
|
|
524
|
-
title: e.name,
|
|
525
|
-
path: base,
|
|
526
|
-
meta: { profileId: profileId, remotePath: base }
|
|
527
|
-
}, { sessionId: sessionId });
|
|
528
|
-
}
|
|
529
|
-
}
|
|
530
|
-
}
|
|
531
|
-
|
|
532
|
-
function goUp() {
|
|
533
|
-
if (!path) return;
|
|
534
|
-
var parts = path.replace(/\/+$/, "").split("/");
|
|
535
|
-
parts.pop();
|
|
536
|
-
var up = parts.join("/") || "/";
|
|
537
|
-
setPath(up);
|
|
538
|
-
load(profileId, up);
|
|
539
|
-
}
|
|
540
|
-
|
|
541
|
-
return h("div", { style: { display: "flex", flexDirection: "column", flex: 1, minHeight: 0, overflow: "hidden" } },
|
|
542
|
-
h("div", { style: { flex: "none" } },
|
|
543
|
-
h("div", { className: "rssh-row" },
|
|
544
|
-
h("span", { className: "rssh-label" }, t("files.workspace")),
|
|
545
|
-
h("select", { className: "rssh-select", value: wsId || "", onChange: function (e) { setWsId(e.target.value); setProfileId(""); } },
|
|
546
|
-
h("option", { value: "" }, t("files.selectWorkspace")),
|
|
547
|
-
workspaces.map(function (w) { return h("option", { key: w.id, value: w.id }, w.title); })
|
|
548
|
-
)
|
|
549
|
-
),
|
|
550
|
-
h("div", { className: "rssh-row" },
|
|
551
|
-
h("span", { className: "rssh-label" }, t("files.orProfile")),
|
|
552
|
-
h("select", { className: "rssh-select", value: profileId, onChange: function (e) { setProfileId(e.target.value); setWsId(""); } },
|
|
553
|
-
h("option", { value: "" }, t("common.selectProfile")),
|
|
554
|
-
profiles.map(function (p) { return h("option", { key: p.id, value: p.id }, p.name); })
|
|
555
|
-
)
|
|
556
|
-
),
|
|
557
|
-
wsId ? h("div", { className: "rssh-row" },
|
|
558
|
-
h("span", { className: "rssh-label" }, t("files.defaultDir")),
|
|
559
|
-
h("div", { className: "rssh-pathbar" },
|
|
560
|
-
h("input", { className: "rssh-input", value: defaultDir, onChange: function (e) { setDefaultDir(e.target.value); }, placeholder: "/home/user/project" }),
|
|
561
|
-
h("button", { className: "rssh-btn", onClick: saveDefaultDir }, t("common.save"))
|
|
562
|
-
)
|
|
563
|
-
) : null,
|
|
564
|
-
h("div", { className: "rssh-pathbar" },
|
|
565
|
-
h("input", { className: "rssh-input", value: path, onChange: function (e) { setPath(e.target.value); }, placeholder: "/home/user" }),
|
|
566
|
-
h("button", { className: "rssh-btn", onClick: function () { load(profileId, path); } }, t("common.open")),
|
|
567
|
-
h("button", { className: "rssh-btn", onClick: goUp }, t("common.up"))
|
|
568
|
-
),
|
|
569
|
-
wsId ? h("div", { className: "rssh-pathbar", style: { flexWrap: "wrap" } },
|
|
570
|
-
h("button", { className: "rssh-btn", onClick: function () { openOps("grep"); } }, t("ops.search")),
|
|
571
|
-
h("button", { className: "rssh-btn", onClick: function () { openOps("glob"); } }, t("ops.find")),
|
|
572
|
-
h("button", { className: "rssh-btn", onClick: function () { openOps("mkdir"); } }, t("ops.newDir")),
|
|
573
|
-
h("button", { className: "rssh-btn", onClick: function () { openOps("move"); } }, t("ops.rename")),
|
|
574
|
-
h("button", { className: "rssh-btn", onClick: function () { openOps("delete"); } }, t("ops.delete"))
|
|
575
|
-
) : (wsId || profileId) ? h("div", { className: "rssh-pathbar", style: { flexWrap: "wrap" } },
|
|
576
|
-
h("button", { className: "rssh-btn", onClick: function () { openOps("grep"); } }, t("ops.search")),
|
|
577
|
-
h("button", { className: "rssh-btn", onClick: function () { openOps("glob"); } }, t("ops.find")),
|
|
578
|
-
h("button", { className: "rssh-btn", onClick: function () { openOps("mkdir"); } }, t("ops.newDir")),
|
|
579
|
-
h("button", { className: "rssh-btn", onClick: function () { openOps("move"); } }, t("ops.rename")),
|
|
580
|
-
h("button", { className: "rssh-btn", onClick: function () { openOps("delete"); } }, t("ops.delete"))
|
|
581
|
-
) : null,
|
|
582
|
-
loading ? h("div", { className: "rssh-muted" }, t("common.loading")) : null,
|
|
583
|
-
err ? h("div", { className: "rssh-err", style: { whiteSpace: "pre-wrap" } }, err) : null,
|
|
584
|
-
opsDlg ? h(OpsModal, { opsDlg: opsDlg, patchOps: patchOps, runOps: runOps, close: function () { setOpsDlg(null); }, t: t }) : null
|
|
585
|
-
),
|
|
586
|
-
h("div", { style: { flex: 1, minHeight: 0, overflowY: "auto", marginTop: 6 } },
|
|
587
|
-
entries.map(function (e) {
|
|
588
|
-
return h("div", { key: e.name, className: "rssh-tree-item", onClick: function () { openEntry(e); } },
|
|
589
|
-
h("span", { className: e.type === "directory" ? "rssh-tree-dir" : "rssh-tree-file" }, e.type === "directory" ? "📁 " + e.name : "📄 " + e.name),
|
|
590
|
-
e.type === "file" ? h("span", { className: "rssh-tree-size" }, String(e.size)) : null
|
|
591
|
-
);
|
|
592
|
-
})
|
|
593
|
-
)
|
|
594
|
-
);
|
|
595
|
-
}
|
|
596
|
-
|
|
597
|
-
// ======================================================================
|
|
598
|
-
// 远程文件编辑器页签:点击文件树中的文件后单开一栏预览/编辑(与内置「文件」页签一致)
|
|
599
|
-
// ======================================================================
|
|
600
|
-
function RemoteEditor(props) {
|
|
601
|
-
var t = useT();
|
|
602
|
-
var tab = props.tab || {};
|
|
603
|
-
var ctx = props.ctx;
|
|
604
|
-
var meta = tab.meta || {};
|
|
605
|
-
var profileId = meta.profileId || "";
|
|
606
|
-
var remotePath = tab.path || meta.remotePath || "";
|
|
607
|
-
var [data, setData] = React.useState({ loading: true, content: "", binary: false, dirty: false, error: null });
|
|
608
|
-
|
|
609
|
-
React.useEffect(function () {
|
|
610
|
-
var alive = true;
|
|
611
|
-
setData({ loading: true, content: "", binary: false, dirty: false, error: null });
|
|
612
|
-
api("readFile", { profileId: profileId, path: remotePath }).then(function (r) {
|
|
613
|
-
if (!alive) return;
|
|
614
|
-
if (r && r.ok) setData({ loading: false, content: r.content || "", binary: !!r.binary, dirty: false, error: null });
|
|
615
|
-
else setData({ loading: false, content: "", binary: false, dirty: false, error: (r && r.error) || t("files.errRead") });
|
|
616
|
-
});
|
|
617
|
-
return function () { alive = false; };
|
|
618
|
-
}, [profileId, remotePath]);
|
|
619
|
-
|
|
620
|
-
function save() {
|
|
621
|
-
api("writeFile", { profileId: profileId, path: remotePath, content: data.content }).then(function (r) {
|
|
622
|
-
if (r && r.ok) setData(Object.assign({}, data, { dirty: false, error: null }));
|
|
623
|
-
else setData(Object.assign({}, data, { error: (r && r.error) || t("files.errSave") }));
|
|
624
|
-
});
|
|
625
|
-
}
|
|
626
|
-
|
|
627
|
-
function close() {
|
|
628
|
-
if (ctx && ctx.betterSidebar && tab.id) ctx.betterSidebar.closeTab(tab.id);
|
|
629
|
-
}
|
|
630
|
-
|
|
631
|
-
if (data.loading) return h("div", { className: "rssh-muted", style: { padding: 8 } }, t("files.reading"));
|
|
632
|
-
if (data.error) return h("div", { className: "rssh-err", style: { padding: 8, whiteSpace: "pre-wrap" } }, data.error);
|
|
633
|
-
if (data.binary) return h("div", { className: "rssh-muted", style: { padding: 8 } }, t("files.binary"));
|
|
634
|
-
|
|
635
|
-
return h("div", { style: { display: "flex", flexDirection: "column", flex: 1, minHeight: 0, padding: 8, overflow: "hidden" } },
|
|
636
|
-
h("div", { className: "rssh-pathbar", style: { flex: "none" } },
|
|
637
|
-
h("span", { className: "rssh-title", style: { flex: 1, wordBreak: "break-all" } }, remotePath),
|
|
638
|
-
h("button", { className: "rssh-btn", onClick: save }, t("common.save")),
|
|
639
|
-
h("button", { className: "rssh-btn", onClick: close }, t("common.close"))
|
|
640
|
-
),
|
|
641
|
-
h("textarea", { className: "rssh-content", style: { flex: 1, minHeight: 0, maxHeight: "none", width: "100%", boxSizing: "border-box", background: "var(--dsw-alias-markdown-code-block)", color: "var(--dsw-alias-label-primary)", border: "1px solid var(--dsw-alias-border-l2)" },
|
|
642
|
-
value: data.content, onChange: function (e) { setData(Object.assign({}, data, { content: e.target.value, dirty: true })); } })
|
|
643
|
-
);
|
|
644
|
-
}
|
|
645
|
-
|
|
646
|
-
// ======================================================================
|
|
647
|
-
// 远程终端页签
|
|
648
|
-
// ======================================================================
|
|
649
|
-
function TerminalView(props) {
|
|
650
|
-
var t = useT();
|
|
651
|
-
var [data, setData] = React.useState("");
|
|
652
|
-
var [status, setStatus] = React.useState("running");
|
|
653
|
-
var [input, setInput] = React.useState("");
|
|
654
|
-
var endRef = React.useRef(null);
|
|
655
|
-
|
|
656
|
-
React.useEffect(function () {
|
|
657
|
-
var timer = setInterval(function () {
|
|
658
|
-
api("terminalRead", { id: props.id }).then(function (r) {
|
|
659
|
-
if (r && r.ok) {
|
|
660
|
-
if (r.data) setData(function (d) { return d + r.data; });
|
|
661
|
-
if (r.status) setStatus(r.status);
|
|
662
|
-
}
|
|
663
|
-
});
|
|
664
|
-
}, 200);
|
|
665
|
-
return function () { clearInterval(timer); };
|
|
666
|
-
}, [props.id]);
|
|
667
|
-
|
|
668
|
-
React.useEffect(function () {
|
|
669
|
-
if (endRef.current) endRef.current.scrollTop = endRef.current.scrollHeight;
|
|
670
|
-
}, [data]);
|
|
671
|
-
|
|
672
|
-
function send() {
|
|
673
|
-
if (!input) return;
|
|
674
|
-
api("terminalWrite", { id: props.id, data: input + "\r" });
|
|
675
|
-
setInput("");
|
|
676
|
-
}
|
|
677
|
-
|
|
678
|
-
return h("div", { className: "rssh-terminal" },
|
|
679
|
-
h("div", { className: "rssh-terminal-bar" },
|
|
680
|
-
h("span", { style: { flex: 1, color: status === "running" ? "var(--dsw-alias-state-success-primary)" : "var(--dsw-alias-state-error-primary)" } }, t("term.title", { id: props.id }) + (status === "exited" ? t("term.exited") : "")),
|
|
681
|
-
h("button", { className: "rssh-btn", onClick: function () { props.onClose(props.id); } }, t("common.close"))
|
|
682
|
-
),
|
|
683
|
-
h("div", { className: "rssh-terminal-out", ref: endRef }, data || (status === "running" ? t("term.connecting") : t("term.noOutput"))),
|
|
684
|
-
h("div", { className: "rssh-terminal-input-row" },
|
|
685
|
-
h("input", { className: "rssh-input", value: input,
|
|
686
|
-
onChange: function (e) { setInput(e.target.value); },
|
|
687
|
-
onKeyDown: function (e) { if (e.key === "Enter") send(); },
|
|
688
|
-
placeholder: t("term.inputPlaceholder") }),
|
|
689
|
-
h("button", { className: "rssh-btn", onClick: send }, t("term.send"))
|
|
690
|
-
)
|
|
691
|
-
);
|
|
692
|
-
}
|
|
693
|
-
|
|
694
|
-
function TerminalTab(props) {
|
|
695
|
-
var t = useT();
|
|
696
|
-
var sessionId = props.sessionId;
|
|
697
|
-
var profiles = useProfiles();
|
|
698
|
-
var [profileId, setProfileId] = useSessionMemory(sessionId, "profileId", "");
|
|
699
|
-
var [terms, setTerms] = useSessionMemory(sessionId, "terms", []);
|
|
700
|
-
var [err, setErr] = React.useState(null);
|
|
701
|
-
|
|
702
|
-
function spawn() {
|
|
703
|
-
if (!profileId) { setErr(t("term.needProfile")); return; }
|
|
704
|
-
setErr(null);
|
|
705
|
-
api("spawnTerminal", { profileId: profileId }).then(function (r) {
|
|
706
|
-
if (r && r.ok) setTerms(function (list) { return list.concat([{ id: r.id }]); });
|
|
707
|
-
else setErr((r && r.error) || t("term.errSpawn"));
|
|
708
|
-
});
|
|
709
|
-
}
|
|
710
|
-
|
|
711
|
-
function close(id) {
|
|
712
|
-
api("terminalClose", { id: id });
|
|
713
|
-
setTerms(function (list) { return list.filter(function (x) { return x.id !== id; }); });
|
|
714
|
-
}
|
|
715
|
-
|
|
716
|
-
return h("div", null,
|
|
717
|
-
h("div", { className: "rssh-row" },
|
|
718
|
-
h("span", { className: "rssh-label" }, t("common.profile")),
|
|
719
|
-
h("select", { className: "rssh-select", value: profileId, onChange: function (e) { setProfileId(e.target.value); } },
|
|
720
|
-
h("option", { value: "" }, t("common.selectProfile")),
|
|
721
|
-
profiles.map(function (p) { return h("option", { key: p.id, value: p.id }, p.name); })
|
|
722
|
-
)
|
|
723
|
-
),
|
|
724
|
-
h("button", { className: "rssh-btn", onClick: spawn }, t("term.newTerminal")),
|
|
725
|
-
err ? h("div", { className: "rssh-err", style: { marginTop: 6 } }, err) : null,
|
|
726
|
-
terms.length === 0 ? h("div", { className: "rssh-empty" }, t("term.emptyHint")) : null,
|
|
727
|
-
terms.map(function (term) { return h(TerminalView, { key: term.id, id: term.id, onClose: close }); })
|
|
728
|
-
);
|
|
729
|
-
}
|
|
730
|
-
|
|
731
|
-
// ======================================================================
|
|
732
|
-
// 目录选择器(供「添加工作区」弹窗流程使用:本地 browse / 远程 SSH)
|
|
733
|
-
// ======================================================================
|
|
734
|
-
// 本地目录列举:走 DSH browse 能力(无原生对话框时替代 pickDirectory)。
|
|
735
|
-
function localListFn(workspaces, p) {
|
|
736
|
-
// 走 better-sidebar 的 fs.tree 端点(已被本插件拦截,本地工作区走本地 fs)
|
|
737
|
-
// 不依赖 workspaces.listDirectory(需要 browse capability,可能被禁用)
|
|
738
|
-
return fetch("/sidebar/api/fs.tree", {
|
|
739
|
-
method: "POST",
|
|
740
|
-
headers: { "content-type": "application/json" },
|
|
741
|
-
body: JSON.stringify({ sessionId: "", path: p || "" })
|
|
742
|
-
}).then(function (r) { return r.json(); }).then(function (j) {
|
|
743
|
-
if (!j || !j.ok) return { ok: false, error: (j && j.error && j.error.message) || "list failed" };
|
|
744
|
-
var v = j.value || {};
|
|
745
|
-
return {
|
|
746
|
-
ok: true,
|
|
747
|
-
path: v.path || p || "",
|
|
748
|
-
entries: (v.entries || []).filter(function (e) { return e.isDir; }).map(function (e) { return { name: e.name, type: "directory", path: e.path }; })
|
|
749
|
-
};
|
|
750
|
-
}).catch(function (e) {
|
|
751
|
-
return { ok: false, error: String(e && e.message ? e.message : e) };
|
|
752
|
-
});
|
|
753
|
-
}
|
|
754
|
-
|
|
755
|
-
// 远程目录列举:走本插件 SSH API,并为每条记录补全绝对 path。
|
|
756
|
-
function remoteListFn(profileId, p) {
|
|
757
|
-
return api("listDir", { profileId: profileId, path: p }).then(function (r) {
|
|
758
|
-
if (!(r && r.ok)) return r;
|
|
759
|
-
var parent = r.path || "";
|
|
760
|
-
var entries = (r.entries || []).map(function (e) {
|
|
761
|
-
return { name: e.name, type: e.type, path: (parent ? parent.replace(/\/+$/, "") + "/" : "") + e.name };
|
|
762
|
-
});
|
|
763
|
-
return { ok: true, path: r.path, entries: entries };
|
|
764
|
-
});
|
|
765
|
-
}
|
|
766
|
-
|
|
767
|
-
// 计算父目录,兼容 Windows(\\) 与 POSIX(/) 分隔符。
|
|
768
|
-
function parentOf(path) {
|
|
769
|
-
if (!path) return null;
|
|
770
|
-
var str = String(path).replace(/[\/\\]+$/, "");
|
|
771
|
-
if (str === "") return null;
|
|
772
|
-
var idx = Math.max(str.lastIndexOf("/"), str.lastIndexOf("\\"));
|
|
773
|
-
if (idx < 0) return null;
|
|
774
|
-
var parent = str.slice(0, idx);
|
|
775
|
-
if (/^[A-Za-z]:$/.test(parent)) return parent + "\\";
|
|
776
|
-
if (parent === "") return "/";
|
|
777
|
-
return parent;
|
|
778
|
-
}
|
|
779
|
-
|
|
780
|
-
// 通用目录选择器:listFn(path) 返回 {ok, path, entries:[{name,type,path}]}。
|
|
781
|
-
// resetKey 变化(如远程切换连接)时从根目录重新加载。
|
|
782
|
-
function DirPicker(props) {
|
|
783
|
-
var t = useT();
|
|
784
|
-
var [path, setPath] = React.useState("");
|
|
785
|
-
var [entries, setEntries] = React.useState([]);
|
|
786
|
-
var [err, setErr] = React.useState(null);
|
|
787
|
-
var [loading, setLoading] = React.useState(false);
|
|
788
|
-
|
|
789
|
-
var listFnRef = React.useRef(props.listFn);
|
|
790
|
-
listFnRef.current = props.listFn;
|
|
791
|
-
|
|
792
|
-
function load(p) {
|
|
793
|
-
setLoading(true); setErr(null);
|
|
794
|
-
listFnRef.current(p).then(function (r) {
|
|
795
|
-
setLoading(false);
|
|
796
|
-
if (r && r.ok) { setEntries(r.entries || []); setPath(r.path || p || ""); }
|
|
797
|
-
else setErr((r && r.error) || t("common.errList"));
|
|
798
|
-
});
|
|
799
|
-
}
|
|
800
|
-
React.useEffect(function () { load(undefined); }, [props.resetKey]);
|
|
801
|
-
|
|
802
|
-
function enterDir(e) { load(e.path); }
|
|
803
|
-
function goUp() {
|
|
804
|
-
var up = parentOf(path);
|
|
805
|
-
if (up !== null) load(up);
|
|
806
|
-
}
|
|
807
|
-
|
|
808
|
-
// 打开按钮:按输入框路径加载该路径下的文件树。空路径时打开主目录。
|
|
809
|
-
function open() {
|
|
810
|
-
load(path || undefined);
|
|
811
|
-
}
|
|
812
|
-
|
|
813
|
-
// 路径栏:输入框(回车导航)+ 打开按钮(紧挨在右侧)+ 上级
|
|
814
|
-
return h("div", null,
|
|
815
|
-
h("div", { className: "rssh-pathbar" },
|
|
816
|
-
h("input", { className: "rssh-input", value: path,
|
|
817
|
-
onChange: function (e) { setPath(e.target.value); },
|
|
818
|
-
onKeyDown: function (e) { if (e.key === "Enter") { e.preventDefault(); open(); } },
|
|
819
|
-
placeholder: props.placeholder || t("picker.dir") }),
|
|
820
|
-
h("button", { className: "rssh-btn", onClick: open, disabled: loading }, t("common.open")),
|
|
821
|
-
h("button", { className: "rssh-btn", onClick: goUp }, t("common.up"))
|
|
822
|
-
),
|
|
823
|
-
loading ? h("div", { className: "rssh-muted" }, t("common.loading")) : null,
|
|
824
|
-
err ? h("div", { className: "rssh-err" }, err) : null,
|
|
825
|
-
// 当前路径下的文件树(仅目录可进入)
|
|
826
|
-
h("div", { className: "rssh-pick-tree" },
|
|
827
|
-
(entries.filter(function (e) { return e.type === "directory"; })).length === 0 && !loading
|
|
828
|
-
? h("div", { className: "rssh-pick-empty" }, t("common.noEntries"))
|
|
829
|
-
: entries.filter(function (e) { return e.type === "directory"; }).map(function (e) {
|
|
830
|
-
return h("div", { key: e.path || e.name, className: "rssh-tree-item", onClick: function () { enterDir(e); } },
|
|
831
|
-
h("span", { className: "rssh-tree-dir" }, "📁 " + e.name));
|
|
832
|
-
})
|
|
833
|
-
),
|
|
834
|
-
h("div", { className: "rssh-actions", style: { marginTop: 8 } },
|
|
835
|
-
h("button", { className: "rssh-btn", disabled: !path || props.disabled, onClick: function () { props.onChoose(path); } }, t("picker.choose"))
|
|
836
|
-
)
|
|
837
|
-
);
|
|
838
|
-
}
|
|
839
|
-
|
|
840
|
-
function RemoteWorkspaceFlow(props) {
|
|
841
|
-
var t = useT();
|
|
842
|
-
var [mode, setMode] = React.useState("local");
|
|
843
|
-
var profiles = useProfiles(props.open);
|
|
844
|
-
var [profileId, setProfileId] = React.useState("");
|
|
845
|
-
var [creating, setCreating] = React.useState(false);
|
|
846
|
-
|
|
847
|
-
// 每次打开流程时重置为「本地目录」起始界面。
|
|
848
|
-
React.useEffect(function () {
|
|
849
|
-
if (props.open) { setMode("local"); setProfileId(""); }
|
|
850
|
-
}, [props.open]);
|
|
851
|
-
|
|
852
|
-
if (!props.open) return null;
|
|
853
|
-
|
|
854
|
-
function chooseLocal(p) { props.onPicked(p); }
|
|
855
|
-
|
|
856
|
-
function chooseRemote(remotePath) {
|
|
857
|
-
setCreating(true);
|
|
858
|
-
api("createRemoteWorkspace", { profileId: profileId, remotePath: remotePath }).then(function (r) {
|
|
859
|
-
setCreating(false);
|
|
860
|
-
if (r && r.ok && r.mirrorPath) props.onPicked(r.mirrorPath);
|
|
861
|
-
else props.onError((r && r.error) || t("flow.errCreate"));
|
|
862
|
-
});
|
|
863
|
-
}
|
|
864
|
-
|
|
865
|
-
var canClose = !creating && !props.busy;
|
|
866
|
-
var isRemote = mode === "remote";
|
|
867
|
-
|
|
868
|
-
// 统一弹窗:上方本地/远程分段切换 → 远程时显示连接选择 → 路径栏(浏览按钮紧挨右侧)→ 文件树
|
|
869
|
-
return h(Modal, { open: true, onClose: function () { if (canClose) props.onCancel(); }, title: t("flow.addWsTitle"), closeLabel: t("common.close"), className: "rssh-flow-dialog" },
|
|
870
|
-
h("div", { style: { padding: 8 } },
|
|
871
|
-
creating || props.busy ? h("div", { className: "rssh-muted" }, t("common.processing")) : null,
|
|
872
|
-
// 分段切换:本地 / 远程
|
|
873
|
-
h("div", { className: "rssh-seg" },
|
|
874
|
-
h("button", { className: "rssh-seg-btn" + (isRemote ? "" : " active"), onClick: function () { setMode("local"); }, disabled: !!props.busy || creating }, t("flow.localSeg")),
|
|
875
|
-
h("button", { className: "rssh-seg-btn" + (isRemote ? " active" : ""), onClick: function () { setMode("remote"); }, disabled: !!props.busy || creating }, t("flow.remoteSeg"))
|
|
876
|
-
),
|
|
877
|
-
// 远程模式:连接配置选择
|
|
878
|
-
isRemote ? h("div", { className: "rssh-row" },
|
|
879
|
-
h("span", { className: "rssh-label" }, t("common.profile")),
|
|
880
|
-
h("select", { className: "rssh-select", value: profileId, onChange: function (e) { setProfileId(e.target.value); }, disabled: creating },
|
|
881
|
-
h("option", { value: "" }, t("common.selectProfile")),
|
|
882
|
-
profiles.map(function (p) { return h("option", { key: p.id, value: p.id }, p.name); })
|
|
883
|
-
)
|
|
884
|
-
) : null,
|
|
885
|
-
// 目录选择器:远程需选好连接后才显示
|
|
886
|
-
isRemote
|
|
887
|
-
? (profileId ? h(DirPicker, { resetKey: profileId, placeholder: t("flow.remoteDir"), listFn: function (p) { return remoteListFn(profileId, p); }, onChoose: chooseRemote, disabled: creating })
|
|
888
|
-
: h("div", { className: "rssh-pick-empty" }, t("common.selectProfile")))
|
|
889
|
-
: h(DirPicker, { resetKey: "local", placeholder: t("flow.localDir"), listFn: function (p) { return localListFn(props.workspaces, p); }, onChoose: chooseLocal })
|
|
890
|
-
)
|
|
891
|
-
);
|
|
892
|
-
}
|
|
893
|
-
|
|
894
|
-
// ======================================================================
|
|
895
|
-
// 设置页小节(连接配置管理)
|
|
896
|
-
// ======================================================================
|
|
897
|
-
function SettingsSection(props) {
|
|
898
|
-
var t = useT();
|
|
899
|
-
var [profiles, setProfiles] = React.useState([]);
|
|
900
|
-
var [workspaces, setWorkspaces] = React.useState([]);
|
|
901
|
-
var [form, setForm] = React.useState({ name: "", host: "", port: "22", user: "", authMethod: "key", keyPath: "", password: "", remoteRoot: "~", proxyJump: "" });
|
|
902
|
-
var [result, setResult] = React.useState(null);
|
|
903
|
-
var [busy, setBusy] = React.useState(false);
|
|
904
|
-
var [importDlg, setImportDlg] = React.useState(null);
|
|
905
|
-
var set = function (k, v) { setForm(function (f) { var n = {}; for (var key in f) n[key] = f[key]; n[k] = v; return n; }); };
|
|
906
|
-
|
|
907
|
-
function reload() {
|
|
908
|
-
api("listProfiles").then(function (r) { if (r && r.ok) setProfiles(r.profiles || []); });
|
|
909
|
-
api("listWorkspaces").then(function (r) { if (r && r.ok) setWorkspaces(r.workspaces || []); });
|
|
910
|
-
}
|
|
911
|
-
React.useEffect(reload, []);
|
|
912
|
-
|
|
913
|
-
function profileName(id) {
|
|
914
|
-
var p = profiles.find(function (x) { return x.id === id; });
|
|
915
|
-
return p ? p.name : id;
|
|
916
|
-
}
|
|
917
|
-
function delWs(wsId) {
|
|
918
|
-
api("deleteWorkspace", { id: wsId }).then(function (r) { if (r && r.ok) setWorkspaces(r.workspaces || []); });
|
|
919
|
-
}
|
|
920
|
-
|
|
921
|
-
function save() {
|
|
922
|
-
setBusy(true); setResult(null);
|
|
923
|
-
api("saveProfile", Object.assign({}, form, { port: parseInt(form.port, 10) || 22 })).then(function (r) {
|
|
924
|
-
setBusy(false);
|
|
925
|
-
if (r && r.ok) {
|
|
926
|
-
setProfiles(r.profiles || []);
|
|
927
|
-
setForm({ name: "", host: "", port: "22", user: "", authMethod: "key", keyPath: "", password: "", remoteRoot: "~", proxyJump: "" });
|
|
928
|
-
setResult({ ok: true, msg: t("settings.saved") });
|
|
929
|
-
} else setResult({ ok: false, msg: (r && r.error) || t("settings.errSave") });
|
|
930
|
-
});
|
|
931
|
-
}
|
|
932
|
-
function test(p) {
|
|
933
|
-
setBusy(true); setResult(null);
|
|
934
|
-
api("testConnection", { id: p.id }).then(function (r) {
|
|
935
|
-
setBusy(false);
|
|
936
|
-
if (r && r.ok) setResult({ ok: true, msg: t("settings.connOk") + "\n" + (r.stdout || "") });
|
|
937
|
-
else setResult({ ok: false, msg: t("settings.connFail") + ((r && r.error) || (r && r.stderr) || "") });
|
|
938
|
-
});
|
|
939
|
-
}
|
|
940
|
-
function del(p) {
|
|
941
|
-
api("deleteProfile", { id: p.id }).then(function (r) { if (r && r.ok) { setProfiles(r.profiles || []); } });
|
|
942
|
-
}
|
|
943
|
-
|
|
944
|
-
function openImport() {
|
|
945
|
-
setImportDlg({ loading: true, hosts: [], selected: {} });
|
|
946
|
-
api("listSshConfigHosts").then(function (r) {
|
|
947
|
-
if (r && r.ok) {
|
|
948
|
-
var hosts = r.hosts || [];
|
|
949
|
-
var sel = {};
|
|
950
|
-
hosts.forEach(function (h) { sel[h.name + "|" + h.hostName] = true; });
|
|
951
|
-
setImportDlg({ loading: false, hosts: hosts, selected: sel });
|
|
952
|
-
} else {
|
|
953
|
-
setImportDlg({ loading: false, hosts: [], selected: {}, error: (r && r.error) || t("settings.noneFound") });
|
|
954
|
-
}
|
|
955
|
-
});
|
|
956
|
-
}
|
|
957
|
-
function toggleImport(key) {
|
|
958
|
-
setImportDlg(function (d) {
|
|
959
|
-
var sel = {}; for (var k in d.selected) sel[k] = d.selected[k];
|
|
960
|
-
sel[key] = !sel[key];
|
|
961
|
-
return Object.assign({}, d, { selected: sel });
|
|
962
|
-
});
|
|
963
|
-
}
|
|
964
|
-
function doImport() {
|
|
965
|
-
if (!importDlg) return;
|
|
966
|
-
var picked = importDlg.hosts.filter(function (h) { return importDlg.selected[h.name + "|" + h.hostName]; });
|
|
967
|
-
setBusy(true);
|
|
968
|
-
var i = 0;
|
|
969
|
-
function next() {
|
|
970
|
-
if (i >= picked.length) {
|
|
971
|
-
setBusy(false); setImportDlg(null);
|
|
972
|
-
setResult({ ok: true, msg: t("settings.imported").replace("{n}", String(picked.length)) });
|
|
973
|
-
reload();
|
|
974
|
-
return;
|
|
975
|
-
}
|
|
976
|
-
var h = picked[i++];
|
|
977
|
-
api("saveProfile", {
|
|
978
|
-
name: h.name, host: h.hostName, port: h.port || 22, user: h.user || "",
|
|
979
|
-
authMethod: "key", keyPath: h.identityFile || "", password: "", remoteRoot: "~", proxyJump: h.proxyJump || ""
|
|
980
|
-
}).then(next);
|
|
981
|
-
}
|
|
982
|
-
next();
|
|
983
|
-
}
|
|
984
|
-
|
|
985
|
-
return h("div", null,
|
|
986
|
-
h("p", { className: "rssh-muted" }, t("settings.desc")),
|
|
987
|
-
profiles.map(function (p) {
|
|
988
|
-
return h("div", { key: p.id, className: "rssh-card" },
|
|
989
|
-
h("div", { className: "rssh-title" }, p.name),
|
|
990
|
-
h("div", { className: "rssh-sub" }, (p.user || "") + "@" + (p.host || "") + ":" + (p.port || 22) + " [" + (p.authMethod || "key") + "]"),
|
|
991
|
-
h("div", { className: "rssh-actions" },
|
|
992
|
-
h("button", { className: "rssh-btn", onClick: function () { test(p); }, disabled: busy }, t("settings.test")),
|
|
993
|
-
h("button", { className: "rssh-btn", onClick: function () { del(p); } }, t("common.delete"))
|
|
994
|
-
)
|
|
995
|
-
);
|
|
996
|
-
}),
|
|
997
|
-
profiles.length === 0 ? h("div", { className: "rssh-empty" }, t("settings.empty")) : null,
|
|
998
|
-
workspaces.length > 0 ? h("div", { className: "rssh-card" },
|
|
999
|
-
h("div", { className: "rssh-title" }, t("settings.workspaces")),
|
|
1000
|
-
workspaces.map(function (ws) {
|
|
1001
|
-
return h("div", { key: ws.id, className: "rssh-card", style: { marginBottom: 6 } },
|
|
1002
|
-
h("div", { className: "rssh-title" }, ws.title || ws.id),
|
|
1003
|
-
h("div", { className: "rssh-sub" }, profileName(ws.profileId) + " → " + (ws.remotePath || "")),
|
|
1004
|
-
h("div", { className: "rssh-actions" },
|
|
1005
|
-
h("button", { className: "rssh-btn", onClick: function () { delWs(ws.id); } }, t("common.delete"))
|
|
1006
|
-
)
|
|
1007
|
-
);
|
|
1008
|
-
})
|
|
1009
|
-
) : null,
|
|
1010
|
-
h("div", { className: "rssh-card" },
|
|
1011
|
-
h("div", { className: "rssh-title" }, t("settings.add")),
|
|
1012
|
-
Field({ label: t("settings.name"), value: form.name, onChange: function (v) { set("name", v); }, placeholder: t("settings.namePlaceholder") }),
|
|
1013
|
-
Field({ label: t("settings.host"), value: form.host, onChange: function (v) { set("host", v); }, placeholder: t("settings.hostPlaceholder") }),
|
|
1014
|
-
Field({ label: t("settings.port"), value: form.port, onChange: function (v) { set("port", v); } }),
|
|
1015
|
-
Field({ label: t("settings.user"), value: form.user, onChange: function (v) { set("user", v); }, placeholder: t("settings.userPlaceholder") }),
|
|
1016
|
-
h("div", { className: "rssh-row" },
|
|
1017
|
-
h("span", { className: "rssh-label" }, t("settings.auth")),
|
|
1018
|
-
h("select", { className: "rssh-select", value: form.authMethod, onChange: function (e) { set("authMethod", e.target.value); } },
|
|
1019
|
-
h("option", { value: "key" }, t("settings.key")),
|
|
1020
|
-
h("option", { value: "password" }, t("settings.password"))
|
|
1021
|
-
)
|
|
1022
|
-
),
|
|
1023
|
-
form.authMethod === "key" ? Field({ label: t("settings.keyPath"), value: form.keyPath, onChange: function (v) { set("keyPath", v); }, placeholder: t("settings.keyPathPlaceholder") }) : null,
|
|
1024
|
-
form.authMethod === "password" ? Field({ label: t("settings.passwordField"), type: "password", value: form.password, onChange: function (v) { set("password", v); } }) : null,
|
|
1025
|
-
Field({ label: t("settings.remoteRoot"), value: form.remoteRoot, onChange: function (v) { set("remoteRoot", v); }, placeholder: "~/project" }),
|
|
1026
|
-
Field({ label: t("settings.proxyJump"), value: form.proxyJump, onChange: function (v) { set("proxyJump", v); }, placeholder: "bastion.example.com" }),
|
|
1027
|
-
h("div", { className: "rssh-actions" },
|
|
1028
|
-
h("button", { className: "rssh-btn", onClick: save, disabled: busy }, busy ? t("common.processing") : t("common.save")),
|
|
1029
|
-
h("button", { className: "rssh-btn", onClick: openImport, disabled: busy }, t("settings.importSshConfig"))
|
|
1030
|
-
)
|
|
1031
|
-
),
|
|
1032
|
-
result ? h("div", { className: result.ok ? "rssh-ok" : "rssh-err", style: { whiteSpace: "pre-wrap", marginTop: 6 } }, result.msg) : null,
|
|
1033
|
-
importDlg ? h(Modal, { open: true, onClose: function () { if (!busy) setImportDlg(null); }, title: t("settings.importTitle"), closeLabel: t("common.close") },
|
|
1034
|
-
importDlg.loading ? h("div", { className: "rssh-muted" }, t("common.loading")) :
|
|
1035
|
-
importDlg.error ? h("div", { className: "rssh-err" }, importDlg.error) :
|
|
1036
|
-
(importDlg.hosts.length === 0 ? h("div", { className: "rssh-muted" }, t("settings.noneFound")) :
|
|
1037
|
-
h("div", null,
|
|
1038
|
-
importDlg.hosts.map(function (hh) {
|
|
1039
|
-
var key = hh.name + "|" + hh.hostName;
|
|
1040
|
-
return h("label", { key: key, className: "rssh-tree-item", style: { cursor: "pointer" } },
|
|
1041
|
-
h("input", { type: "checkbox", checked: !!importDlg.selected[key], onChange: function () { toggleImport(key); } }),
|
|
1042
|
-
h("span", { className: "rssh-tree-dir" }, hh.name),
|
|
1043
|
-
h("span", { className: "rssh-sub" }, (hh.user || "?") + "@" + hh.hostName + ":" + (hh.port || 22) + (hh.proxyJump ? " → " + hh.proxyJump : ""))
|
|
1044
|
-
);
|
|
1045
|
-
}),
|
|
1046
|
-
h("div", { className: "rssh-actions", style: { marginTop: 8 } },
|
|
1047
|
-
h("button", { className: "rssh-btn", onClick: doImport, disabled: busy }, busy ? t("common.processing") : t("common.save")),
|
|
1048
|
-
h("button", { className: "rssh-btn", onClick: function () { setImportDlg(null); }, disabled: busy }, t("common.close"))
|
|
1049
|
-
)
|
|
1050
|
-
)
|
|
1051
|
-
)
|
|
1052
|
-
) : null
|
|
1053
|
-
);
|
|
1054
|
-
}
|
|
1055
|
-
|
|
1056
|
-
// ======================================================================
|
|
1057
|
-
// apply
|
|
1058
|
-
// ======================================================================
|
|
1059
|
-
function apply(ctx) {
|
|
1060
|
-
var betterSidebar = ctx.betterSidebar;
|
|
1061
|
-
var slots = ctx.slots;
|
|
1062
|
-
var locale = ctx.locale;
|
|
1063
|
-
|
|
1064
|
-
// 兼容 DSH Desktop ≥2.0.4:shell 不再把客户端模块系统挂到
|
|
1065
|
-
// globalThis.__DSH_MODULES__,而 dsh-better-sidebar 0.15+ 的懒加载
|
|
1066
|
-
// chunk(editor 等)会回退到该全局——缺失时侧边栏面板打不开、报
|
|
1067
|
-
// "chunk ... client module system unavailable"。这里用内核提供的
|
|
1068
|
-
// modules 服务补挂该全局;better-sidebar 已注入则保持原值。
|
|
1069
|
-
try {
|
|
1070
|
-
if (typeof globalThis !== "undefined") {
|
|
1071
|
-
var fallbackModules = null;
|
|
1072
|
-
try { if (ctx.modules && typeof ctx.modules.import === "function") fallbackModules = ctx.modules; } catch (e) {}
|
|
1073
|
-
if (!fallbackModules && ctx.get && typeof ctx.get === "function") {
|
|
1074
|
-
try { fallbackModules = ctx.get("modules"); } catch (e) { fallbackModules = null; }
|
|
1075
|
-
}
|
|
1076
|
-
// 定期兜底:better-sidebar 若把本插件应用时间之后的模块系统清掉,也补回。
|
|
1077
|
-
if (globalThis.__DSH_MODULES__ === undefined && fallbackModules === null) {
|
|
1078
|
-
var waitModules = 0;
|
|
1079
|
-
var waitTimer = setInterval(function () {
|
|
1080
|
-
waitModules += 1;
|
|
1081
|
-
try {
|
|
1082
|
-
var ms = (ctx.get && typeof ctx.get === "function") ? ctx.get("modules") : null;
|
|
1083
|
-
if (ms && typeof ms.import === "function") {
|
|
1084
|
-
globalThis.__DSH_MODULES__ = ms;
|
|
1085
|
-
clearInterval(waitTimer);
|
|
1086
|
-
}
|
|
1087
|
-
} catch (e) {}
|
|
1088
|
-
if (globalThis.__DSH_MODULES__ !== undefined || waitModules > 40) clearInterval(waitTimer);
|
|
1089
|
-
}, 250);
|
|
1090
|
-
}
|
|
1091
|
-
if (!globalThis.__DSH_MODULES__ && fallbackModules && typeof fallbackModules.import === "function") {
|
|
1092
|
-
globalThis.__DSH_MODULES__ = fallbackModules;
|
|
1093
|
-
}
|
|
1094
|
-
}
|
|
1095
|
-
} catch (e) {}
|
|
1096
|
-
|
|
1097
|
-
// 注册中英文词典并绑定当前语言;locale 切换时 i18n.subscribe 触发组件重渲染。
|
|
1098
|
-
if (locale) {
|
|
1099
|
-
ctx.effect(function () { return locale.register(NS, { zh: zh, en: en }); }, "dsh-remote-ssh: dictionaries");
|
|
1100
|
-
i18n.t = locale.bind(NS);
|
|
1101
|
-
i18n.subscribe = function (cb) { return locale.subscribe(cb); };
|
|
1102
|
-
i18n.getSnapshot = function () { return locale.getSnapshot(); };
|
|
1103
|
-
}
|
|
1104
|
-
|
|
1105
|
-
ctx.effect(function () {
|
|
1106
|
-
var el = document.createElement("style");
|
|
1107
|
-
el.textContent = CSS;
|
|
1108
|
-
document.head.appendChild(el);
|
|
1109
|
-
return function () { if (el.parentNode) el.parentNode.removeChild(el); };
|
|
1110
|
-
});
|
|
1111
|
-
|
|
1112
|
-
// ---- 去掉设置导航「远程连接」条目左侧的默认齿轮图标 ----
|
|
1113
|
-
// 壳层对未知 settings.section 一律渲染齿轮图标(IconSettingsOutline16 兜底),
|
|
1114
|
-
// 且导航 CSS 类名是构建期哈希(VOzbGW_*、zOa2rq_* 每版都会变),CSS 选择器跨版本
|
|
1115
|
-
// 不可靠。这里改为按条目文本(settings.nav)定位设置导航按钮,隐藏其 svg 图标;
|
|
1116
|
-
// 设置面板按需挂载、语言会切换,用 MutationObserver + 语言订阅自动重跑。
|
|
1117
|
-
if (slots) {
|
|
1118
|
-
function checkCell(btn) {
|
|
1119
|
-
var label = i18n.t("settings.nav");
|
|
1120
|
-
var text = (btn.textContent || "").trim();
|
|
1121
|
-
if (text !== label.trim() || text === "") return false;
|
|
1122
|
-
var icon = btn.querySelector("svg");
|
|
1123
|
-
if (icon) { icon.style.display = "none"; return true; }
|
|
1124
|
-
return false;
|
|
1125
|
-
}
|
|
1126
|
-
function checkCellIn(root) {
|
|
1127
|
-
try {
|
|
1128
|
-
if (root.tagName && String(root.tagName).toLowerCase() === "button") checkCell(root);
|
|
1129
|
-
var cells = root.querySelectorAll("button");
|
|
1130
|
-
for (var i = 0; i < cells.length; i++) checkCell(cells[i]);
|
|
1131
|
-
} catch (e) {}
|
|
1132
|
-
}
|
|
1133
|
-
function hideRemoteGearIcon() {
|
|
1134
|
-
try {
|
|
1135
|
-
var buttons = document.querySelectorAll("button");
|
|
1136
|
-
for (var i = 0; i < buttons.length; i++) checkCell(buttons[i]);
|
|
1137
|
-
} catch (e) {}
|
|
1138
|
-
}
|
|
1139
|
-
// ---- 远程工作区图标:单个「地球角标文件夹」SVG 替换壳层默认纯文件夹图标 ----
|
|
1140
|
-
// 壳层工作区行的文件夹图标是固定原语(IconFolderOpen16/Close16),无 per-workspace
|
|
1141
|
-
// 图标 API;旧方案在标题里塞 🌐 emoji 造成「文件夹 + 地球」双重标识。现按标题文本
|
|
1142
|
-
// 定位远程工作区行,找到行首 slot.folder 槽位(标题的前兄弟),向其中的 svg
|
|
1143
|
-
// 追加地球经纬线组 —— 文件夹本体保持壳层默认原语几何,零改写零对账风险。
|
|
1144
|
-
var remoteTitles = new Set();
|
|
1145
|
-
var watchedTitles = []; // 已匹配的标题元素:定时幂等复检,抵消 React 重渲染对图标的还原
|
|
1146
|
-
function refreshRemoteTitles() {
|
|
1147
|
-
api("listWorkspaces").then(function (r) {
|
|
1148
|
-
if (!(r && r.ok && Array.isArray(r.workspaces))) return;
|
|
1149
|
-
var next = new Set();
|
|
1150
|
-
for (var i = 0; i < r.workspaces.length; i++) {
|
|
1151
|
-
var t = String(r.workspaces[i].title || "").trim();
|
|
1152
|
-
if (t) { next.add(t); next.add("🌐 " + t); } // 兼容自愈前的旧前缀标题
|
|
1153
|
-
}
|
|
1154
|
-
remoteTitles = next;
|
|
1155
|
-
swapWorkspaceIconsIn(document.body); // 标题集就绪后补一次全量替换
|
|
1156
|
-
}).catch(function () {});
|
|
1157
|
-
}
|
|
1158
|
-
/** 地球经纬线组(右下角、无圆盘底色):线为白色、其余全透明;文件夹本体完全
|
|
1159
|
-
* 保留壳层默认原语(IconFolderOpen16/Close16 原始路径与几何,零改写),
|
|
1160
|
-
* 仅向 svg 追加本组 —— appendChild 不触碰 React 管理的子节点,零对账风险。 */
|
|
1161
|
-
function globeGridElement() {
|
|
1162
|
-
var NS = "http://www.w3.org/2000/svg";
|
|
1163
|
-
var g = document.createElementNS(NS, "g");
|
|
1164
|
-
g.setAttribute("data-rssh-globe", "1");
|
|
1165
|
-
g.setAttribute("fill", "none");
|
|
1166
|
-
g.setAttribute("stroke", "#ffffff");
|
|
1167
|
-
g.setAttribute("stroke-width", "0.9");
|
|
1168
|
-
function add(name, attrs) {
|
|
1169
|
-
var n = document.createElementNS(NS, name);
|
|
1170
|
-
for (var k in attrs) n.setAttribute(k, attrs[k]);
|
|
1171
|
-
g.appendChild(n);
|
|
1172
|
-
}
|
|
1173
|
-
add("circle", { cx: "11.6", cy: "11.9", r: "3.4" }); // 地球轮廓
|
|
1174
|
-
add("ellipse", { cx: "11.6", cy: "11.9", rx: "1.6", ry: "3.4" }); // 经线
|
|
1175
|
-
add("path", { d: "M8.2 11.9h6.8" }); // 赤道(纬线)
|
|
1176
|
-
add("path", { d: "M8.7 9.6c0.9 0.65 1.85 0.97 2.9 0.97s2-0.32 2.9-0.97" }); // 北纬弧
|
|
1177
|
-
add("path", { d: "M8.7 14.2c0.9-0.65 1.85-0.97 2.9-0.97s2 0.32 2.9 0.97" }); // 南纬弧
|
|
1178
|
-
return g;
|
|
1179
|
-
}
|
|
1180
|
-
/** 幂等应用:React 还原/重建子节点会清掉本组 —— 以 [data-rssh-globe] 子节点
|
|
1181
|
-
* 存在性为状态判据(覆盖属性级还原与节点重建),缺席即追加。 */
|
|
1182
|
-
function applyFolderGlobe(slotSvg) {
|
|
1183
|
-
if (slotSvg.querySelector("[data-rssh-globe]")) return;
|
|
1184
|
-
slotSvg.setAttribute("data-rssh-folder-globe", "1");
|
|
1185
|
-
slotSvg.appendChild(globeGridElement());
|
|
1186
|
-
}
|
|
1187
|
-
/** 对单个标题元素做行首槽位修复:projectRow > [slot.folder(svg), …, projectText > title],
|
|
1188
|
-
* 文件夹 svg 在标题的前兄弟槽位 —— 向上 ≤4 层取每层容器第一个子元素,首个含 svg 即是。 */
|
|
1189
|
-
function fixRowIconFor(el) {
|
|
1190
|
-
var holder = el;
|
|
1191
|
-
for (var hop = 0; hop < 4 && holder && holder.parentElement; hop++) {
|
|
1192
|
-
var row = holder.parentElement;
|
|
1193
|
-
var first = row.children && row.children[0];
|
|
1194
|
-
if (first && first.querySelector && first.querySelector("svg")) {
|
|
1195
|
-
applyFolderGlobe(first.querySelector("svg"));
|
|
1196
|
-
return true;
|
|
1197
|
-
}
|
|
1198
|
-
holder = row;
|
|
1199
|
-
}
|
|
1200
|
-
return false;
|
|
1201
|
-
}
|
|
1202
|
-
function watchTitle(el) {
|
|
1203
|
-
for (var i = 0; i < watchedTitles.length; i++) { if (watchedTitles[i] === el) return; }
|
|
1204
|
-
watchedTitles.push(el);
|
|
1205
|
-
fixRowIconFor(el);
|
|
1206
|
-
}
|
|
1207
|
-
function recheckWatched() {
|
|
1208
|
-
for (var i = watchedTitles.length - 1; i >= 0; i--) {
|
|
1209
|
-
var el = watchedTitles[i];
|
|
1210
|
-
if (!el || !el.isConnected) { watchedTitles.splice(i, 1); continue; }
|
|
1211
|
-
fixRowIconFor(el);
|
|
1212
|
-
}
|
|
1213
|
-
}
|
|
1214
|
-
function swapWorkspaceIconsIn(root) {
|
|
1215
|
-
if (!remoteTitles.size) return;
|
|
1216
|
-
try {
|
|
1217
|
-
var nodes = root.querySelectorAll("span,div,button,a,p");
|
|
1218
|
-
for (var i = 0; i < nodes.length; i++) {
|
|
1219
|
-
var el = nodes[i];
|
|
1220
|
-
var t = (el.textContent || "").trim();
|
|
1221
|
-
if (!t || !remoteTitles.has(t) || el.childElementCount > 2) continue;
|
|
1222
|
-
watchTitle(el);
|
|
1223
|
-
}
|
|
1224
|
-
} catch (e) {}
|
|
1225
|
-
}
|
|
1226
|
-
ctx.effect(function () {
|
|
1227
|
-
if (typeof document === "undefined") return;
|
|
1228
|
-
hideRemoteGearIcon();
|
|
1229
|
-
refreshRemoteTitles();
|
|
1230
|
-
var titleTimer = setInterval(refreshRemoteTitles, 30000);
|
|
1231
|
-
var recheckTimer = setInterval(recheckWatched, 1000); // 抵消 React 属性还原
|
|
1232
|
-
var timer = null;
|
|
1233
|
-
var pendingRoots = null;
|
|
1234
|
-
var schedule = function (root) {
|
|
1235
|
-
if (root && root.nodeType === 1) (pendingRoots || (pendingRoots = [])).push(root);
|
|
1236
|
-
if (timer) return;
|
|
1237
|
-
timer = setTimeout(function () {
|
|
1238
|
-
timer = null;
|
|
1239
|
-
var roots = pendingRoots;
|
|
1240
|
-
pendingRoots = null;
|
|
1241
|
-
if (document.hidden) return; // 后台页签零开销
|
|
1242
|
-
if (roots && roots.length) {
|
|
1243
|
-
// 只扫新增子树:流式聊天期间不做全文档扫描(稳态成本≈0)
|
|
1244
|
-
for (var i = 0; i < roots.length; i++) {
|
|
1245
|
-
if (roots[i].isConnected) { checkCellIn(roots[i]); swapWorkspaceIconsIn(roots[i]); }
|
|
1246
|
-
}
|
|
1247
|
-
} else {
|
|
1248
|
-
hideRemoteGearIcon(); // 语言切换等无根场景全量扫一次
|
|
1249
|
-
swapWorkspaceIconsIn(document.body);
|
|
1250
|
-
}
|
|
1251
|
-
}, 500);
|
|
1252
|
-
};
|
|
1253
|
-
var mo = new MutationObserver(function (muts) {
|
|
1254
|
-
for (var i = 0; i < muts.length; i++) {
|
|
1255
|
-
var added = muts[i].addedNodes;
|
|
1256
|
-
for (var j = 0; j < added.length; j++) schedule(added[j]);
|
|
1257
|
-
}
|
|
1258
|
-
});
|
|
1259
|
-
mo.observe(document.body, { childList: true, subtree: true });
|
|
1260
|
-
var off = i18n.subscribe(function () { schedule(null); });
|
|
1261
|
-
return function () {
|
|
1262
|
-
mo.disconnect();
|
|
1263
|
-
if (off) off();
|
|
1264
|
-
if (timer) clearTimeout(timer);
|
|
1265
|
-
clearInterval(titleTimer);
|
|
1266
|
-
clearInterval(recheckTimer);
|
|
1267
|
-
};
|
|
1268
|
-
}, "dsh-remote-ssh: settings gear hide + remote workspace folder-globe icon");
|
|
1269
|
-
}
|
|
1270
|
-
|
|
1271
|
-
// better-sidebar 0.15+ 的文件上传 UI 把原始字节流 POST 到 /sidebar/upload。
|
|
1272
|
-
// 远程工作区会话的文件树显示的是本地镜像目录,better-sidebar 会把上传写进
|
|
1273
|
-
// 本地镜像而不会到达远程。这里包装 fetch:目标目录位于某个远程工作区镜像
|
|
1274
|
-
// 目录内时,把该上传请求重定向到 /remote-ssh/upload(走 SSH 上传到远端)。
|
|
1275
|
-
ctx.effect(function () {
|
|
1276
|
-
if (typeof window === "undefined" || typeof window.fetch !== "function") return;
|
|
1277
|
-
var nativeFetch = window.fetch.bind(window);
|
|
1278
|
-
var mirrors = [];
|
|
1279
|
-
function refreshMirrors() {
|
|
1280
|
-
api("listWorkspaces").then(function (r) {
|
|
1281
|
-
if (r && r.ok && Array.isArray(r.workspaces)) {
|
|
1282
|
-
mirrors = r.workspaces.map(function (w) {
|
|
1283
|
-
return String(w && w.mirrorPath || "").replace(/\\/g, "/").replace(/\/+$/g, "").toLowerCase();
|
|
1284
|
-
}).filter(Boolean);
|
|
1285
|
-
}
|
|
1286
|
-
}).catch(function () {});
|
|
1287
|
-
}
|
|
1288
|
-
refreshMirrors();
|
|
1289
|
-
var timer = setInterval(refreshMirrors, 30000);
|
|
1290
|
-
function patchedFetch(input, init) {
|
|
1291
|
-
var raw = typeof input === "string" ? input : (input && input.url);
|
|
1292
|
-
if (typeof raw === "string") {
|
|
1293
|
-
try {
|
|
1294
|
-
var u = new URL(raw, window.location.origin);
|
|
1295
|
-
if (u.pathname === "/sidebar/upload") {
|
|
1296
|
-
var dir = (u.searchParams.get("dir") || "").replace(/\\/g, "/").replace(/\/+$/g, "").toLowerCase();
|
|
1297
|
-
var cwd = (u.searchParams.get("cwd") || "").replace(/\\/g, "/").replace(/\/+$/g, "").toLowerCase();
|
|
1298
|
-
for (var i = 0; i < mirrors.length; i++) {
|
|
1299
|
-
var m = mirrors[i];
|
|
1300
|
-
var hit = (dir && (dir === m || dir.indexOf(m + "/") === 0)) || (cwd && (cwd === m || cwd.indexOf(m + "/") === 0));
|
|
1301
|
-
if (hit) {
|
|
1302
|
-
var target = new URL("/remote-ssh/upload" + u.search, window.location.origin);
|
|
1303
|
-
return nativeFetch(target.toString(), init);
|
|
1304
|
-
}
|
|
1305
|
-
}
|
|
1306
|
-
}
|
|
1307
|
-
} catch (e) {}
|
|
1308
|
-
}
|
|
1309
|
-
return nativeFetch(input, init);
|
|
1310
|
-
}
|
|
1311
|
-
window.fetch = patchedFetch;
|
|
1312
|
-
return function () {
|
|
1313
|
-
clearInterval(timer);
|
|
1314
|
-
if (window.fetch === patchedFetch) window.fetch = nativeFetch;
|
|
1315
|
-
};
|
|
1316
|
-
}, "dsh-remote-ssh: redirect remote-workspace uploads");
|
|
1317
|
-
|
|
1318
|
-
if (betterSidebar) {
|
|
1319
|
-
// 远程文件编辑器页签(隐藏页签,仅由代码打开,每个远程文件单开一栏)。
|
|
1320
|
-
// remssh:files 和 remssh:term 已移除:远程文件由内置「文件」页签直接 SSH 读写,
|
|
1321
|
-
// 远程终端由内置「终端」页签提供(shell wrapper 自动 SSH)。
|
|
1322
|
-
ctx.effect(function () {
|
|
1323
|
-
return betterSidebar.registerTab({
|
|
1324
|
-
id: "remssh:editor",
|
|
1325
|
-
title: function () { return i18n.t("tabs.files"); },
|
|
1326
|
-
hidden: true,
|
|
1327
|
-
dedupeKey: function (tab) { return (tab && tab.meta && tab.meta.profileId || "") + "\u0000" + (tab && tab.path || ""); },
|
|
1328
|
-
icon: function () { return h("span", null, "📄"); },
|
|
1329
|
-
component: function (props) {
|
|
1330
|
-
return h(RemoteEditor, { tab: props && props.tab, ctx: props && props.ctx, scope: props && props.scope });
|
|
1331
|
-
}
|
|
1332
|
-
});
|
|
1333
|
-
});
|
|
1334
|
-
}
|
|
1335
|
-
|
|
1336
|
-
if (slots) {
|
|
1337
|
-
slots.inject("settings.section", function () {
|
|
1338
|
-
return slots.register({
|
|
1339
|
-
name: "settings.section",
|
|
1340
|
-
id: "remote-ssh",
|
|
1341
|
-
order: 200,
|
|
1342
|
-
label: function () { return i18n.t("settings.nav"); },
|
|
1343
|
-
locale: NS
|
|
1344
|
-
}, SettingsSection);
|
|
1345
|
-
});
|
|
1346
|
-
|
|
1347
|
-
// 接管原生「添加工作区」流程:用明显更低的优先级覆盖内置的本地目录选择器,
|
|
1348
|
-
// 弹窗内提供「本地目录(DSH browse)/ 远程目录(SSH)」两种浏览。
|
|
1349
|
-
var flowInject = function () { return { workspaces: ctx.get("workspaces") }; };
|
|
1350
|
-
ctx.effect(function () {
|
|
1351
|
-
return slots.inject("conversation.hero.workspace.directoryFlow", function () {
|
|
1352
|
-
return slots.register({ name: "conversation.hero.workspace.directoryFlow", priority: -100, inject: flowInject }, RemoteWorkspaceFlow);
|
|
1353
|
-
});
|
|
1354
|
-
});
|
|
1355
|
-
ctx.effect(function () {
|
|
1356
|
-
return slots.inject("sidebar.workspaces.directoryFlow", function () {
|
|
1357
|
-
return slots.register({ name: "sidebar.workspaces.directoryFlow", priority: -100, inject: flowInject }, RemoteWorkspaceFlow);
|
|
1358
|
-
});
|
|
1359
|
-
});
|
|
1360
|
-
}
|
|
1361
|
-
}
|
|
1362
|
-
|
|
1363
|
-
exports.apply = apply;
|
|
1364
|
-
exports.inject = ["betterSidebar", "slots", "locale"];
|
|
1365
|
-
return module.exports;
|
|
1366
|
-
}
|
|
1367
|
-
});
|
|
1
|
+
/**
|
|
2
|
+
* @zhangfengshun/dsh-remote-ssh — Client 半边(静态 DSH Web 插件,浏览器 bundle)
|
|
3
|
+
*
|
|
4
|
+
* 远程文件与远程终端已合并到内置「文件」和「终端」页签:
|
|
5
|
+
* - 远程文件:内置「文件」页签通过 fs.* 拦截直接 SSH 读写远程文件,无需同步。
|
|
6
|
+
* 远程终端由内置「终端」页签提供(shell wrapper 自动 SSH)。
|
|
7
|
+
* - 远程终端:shell wrapper 自动检测工作区 .remote-ssh.json,在远程工作区中打开 SSH 终端。
|
|
8
|
+
* 需在 better-sidebar 设置中将 shell 指向 wrapper 脚本(见 README)。
|
|
9
|
+
* 仍保留隐藏的 remssh:editor 页签用于远程文件编辑器。
|
|
10
|
+
* 在 DSH 设置页注册「远程连接」小节管理连接配置和远程工作区;
|
|
11
|
+
* 同时接管原生「添加工作区」流程提供「本地目录 / 远程目录」两选。
|
|
12
|
+
*/
|
|
13
|
+
window.__ModuleLoader__.load({
|
|
14
|
+
id: "@zhangfengshun/dsh-remote-ssh",
|
|
15
|
+
factory: (require) => {
|
|
16
|
+
var module = { exports: {} };
|
|
17
|
+
var exports = module.exports;
|
|
18
|
+
var React = require("react");
|
|
19
|
+
var primitives = require("@deepseek-ai/dsh-client-ui-primitives");
|
|
20
|
+
var Modal = primitives.Modal;
|
|
21
|
+
|
|
22
|
+
function h(tag, props) {
|
|
23
|
+
var args = [tag, props];
|
|
24
|
+
for (var i = 2; i < arguments.length; i++) args.push(arguments[i]);
|
|
25
|
+
return React.createElement.apply(React, args);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
var CSS =
|
|
29
|
+
// 颜色全部使用 DSH 主题 token(--dsw-alias-*),跟随 body[data-ds-dark-theme] 自动切换明暗主题。
|
|
30
|
+
".rssh-row{margin:6px 0}.rssh-label{color:var(--dsw-alias-label-caption);font-size:11px;display:block;margin-bottom:2px}" +
|
|
31
|
+
".rssh-input{width:100%;box-sizing:border-box;background:var(--dsw-specific-input-major);color:var(--dsw-alias-label-primary);border:1px solid var(--dsw-alias-border-l2);border-radius:4px;padding:5px 7px;margin:3px 0;font-size:12px}" +
|
|
32
|
+
".rssh-select{width:100%;box-sizing:border-box;background:var(--dsw-specific-input-major);color:var(--dsw-alias-label-primary);border:1px solid var(--dsw-alias-border-l2);border-radius:4px;padding:5px 7px;margin:3px 0;font-size:12px}" +
|
|
33
|
+
".rssh-btn{background:var(--dsw-alias-button-ghost-active-fill);color:var(--dsw-alias-label-primary);border:1px solid var(--dsw-alias-border-l2);border-radius:4px;padding:4px 9px;cursor:pointer;font-size:12px}" +
|
|
34
|
+
".rssh-btn:hover{background:var(--dsw-alias-button-ghost-active-hover)}.rssh-btn:disabled{opacity:.5;cursor:default}" +
|
|
35
|
+
".rssh-card{padding:8px 10px;border:1px solid var(--dsw-alias-border-l1);border-radius:6px;margin:8px 0;background:var(--dsw-alias-bg-layer-2)}" +
|
|
36
|
+
".rssh-title{font-weight:600;font-size:13px}.rssh-sub{color:var(--dsw-alias-label-tertiary);font-size:11px}" +
|
|
37
|
+
".rssh-actions{margin-top:6px;display:flex;gap:5px;flex-wrap:wrap}" +
|
|
38
|
+
".rssh-tree-item{display:flex;align-items:center;gap:5px;padding:3px 5px;cursor:pointer;border-radius:3px}" +
|
|
39
|
+
".rssh-tree-item:hover{background:var(--dsw-alias-interactive-bg-hover)}" +
|
|
40
|
+
".rssh-tree-dir{color:var(--dsw-alias-state-business-primary)}.rssh-tree-file{color:var(--dsw-alias-label-primary)}" +
|
|
41
|
+
".rssh-tree-size{color:var(--dsw-alias-label-caption);font-size:11px;margin-left:auto}" +
|
|
42
|
+
".rssh-pathbar{display:flex;gap:4px;align-items:center;margin:4px 0}" +
|
|
43
|
+
".rssh-pathbar .rssh-input{flex:1;margin:0}" +
|
|
44
|
+
".rssh-content{white-space:pre-wrap;font-family:Consolas,monospace;font-size:12px;background:var(--dsw-alias-markdown-code-block);color:var(--dsw-alias-label-primary);padding:8px;border-radius:4px;margin-top:6px;max-height:400px;overflow:auto}" +
|
|
45
|
+
".rssh-terminal{background:var(--dsw-alias-markdown-code-block);color:var(--dsw-alias-label-primary);border-radius:4px;padding:6px;margin-top:8px;font-family:Consolas,monospace;font-size:12px}" +
|
|
46
|
+
".rssh-terminal-out{white-space:pre-wrap;max-height:320px;overflow:auto;min-height:120px;line-height:1.4}" +
|
|
47
|
+
".rssh-terminal-bar{display:flex;gap:5px;align-items:center;margin-bottom:4px}" +
|
|
48
|
+
".rssh-terminal-input-row{display:flex;gap:4px;margin-top:5px}.rssh-terminal-input-row .rssh-input{flex:1}" +
|
|
49
|
+
".rssh-ok{color:var(--dsw-alias-state-success-primary)}.rssh-err{color:var(--dsw-alias-state-error-primary)}.rssh-muted{color:var(--dsw-alias-label-tertiary);font-size:12px;padding:6px 0}" +
|
|
50
|
+
".rssh-empty{padding:12px;text-align:center;color:var(--dsw-alias-label-tertiary);font-size:12px}" +
|
|
51
|
+
".rssh-flow-dialog{width:690px!important;max-width:94vw!important;min-width:460px;resize:horizontal;overflow:auto!important}" +
|
|
52
|
+
// 添加工作区弹窗:上方本地/远程分段切换 + 路径栏 + 文件树
|
|
53
|
+
".rssh-seg{display:flex;gap:0;margin-bottom:8px;border:1px solid var(--dsw-alias-border-l2);border-radius:6px;overflow:hidden}" +
|
|
54
|
+
".rssh-seg-btn{flex:1;padding:7px 12px;text-align:center;cursor:pointer;font-size:12px;font-weight:500;background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-label-secondary);border:none;transition:background .15s}" +
|
|
55
|
+
".rssh-seg-btn:hover{background:var(--dsw-alias-interactive-bg-hover)}" +
|
|
56
|
+
".rssh-seg-btn.active{background:var(--dsw-alias-state-business-primary);color:#fff}" +
|
|
57
|
+
".rssh-pick-tree{border:1px solid var(--dsw-alias-border-l1);border-radius:4px;max-height:240px;overflow-y:auto;margin-top:4px;background:var(--dsw-alias-bg-layer-2)}" +
|
|
58
|
+
".rssh-pick-empty{padding:12px;text-align:center;color:var(--dsw-alias-label-tertiary);font-size:11px}";
|
|
59
|
+
|
|
60
|
+
// ---- 国际化(跟随 DSH 设置中的语言自动切换)----
|
|
61
|
+
var NS = "dsh-remote-ssh";
|
|
62
|
+
var zh = {
|
|
63
|
+
"tabs.files": "远程文件",
|
|
64
|
+
"tabs.term": "远程终端",
|
|
65
|
+
"settings.nav": "🖥️ 远程连接",
|
|
66
|
+
"settings.desc": "管理 Remote-SSH 的远程连接配置(SSH 密钥或密码认证)。",
|
|
67
|
+
"settings.empty": "还没有连接配置。请在下方添加。",
|
|
68
|
+
"settings.add": "添加连接",
|
|
69
|
+
"settings.workspaces": "远程工作区",
|
|
70
|
+
"settings.name": "名称",
|
|
71
|
+
"settings.namePlaceholder": "如:我的超算",
|
|
72
|
+
"settings.host": "主机 host",
|
|
73
|
+
"settings.hostPlaceholder": "login.example.com",
|
|
74
|
+
"settings.port": "端口 port",
|
|
75
|
+
"settings.user": "用户名 user",
|
|
76
|
+
"settings.userPlaceholder": "your-username",
|
|
77
|
+
"settings.auth": "认证方式",
|
|
78
|
+
"settings.key": "密钥 (推荐)",
|
|
79
|
+
"settings.password": "密码 (需 sshpass)",
|
|
80
|
+
"settings.keyPath": "私钥路径 keyPath",
|
|
81
|
+
"settings.keyPathPlaceholder": "~/.ssh/id_rsa",
|
|
82
|
+
"settings.passwordField": "密码 password",
|
|
83
|
+
"settings.remoteRoot": "远程根目录 remoteRoot",
|
|
84
|
+
"settings.proxyJump": "跳板机 ProxyJump",
|
|
85
|
+
"settings.test": "测试连接",
|
|
86
|
+
"settings.saved": "已保存",
|
|
87
|
+
"settings.connOk": "连接成功",
|
|
88
|
+
"settings.connFail": "连接失败: ",
|
|
89
|
+
"settings.errSave": "保存失败",
|
|
90
|
+
"common.selectProfile": "— 选择连接 —",
|
|
91
|
+
"common.profile": "连接配置",
|
|
92
|
+
"common.save": "保存",
|
|
93
|
+
"common.open": "打开",
|
|
94
|
+
"common.close": "关闭",
|
|
95
|
+
"common.noEntries": "(无子目录)",
|
|
96
|
+
"common.delete": "删除",
|
|
97
|
+
"common.up": "⬆ 上级",
|
|
98
|
+
"common.loading": "加载中…",
|
|
99
|
+
"common.processing": "处理中…",
|
|
100
|
+
"common.errList": "读取目录失败",
|
|
101
|
+
"files.workspace": "远程工作区",
|
|
102
|
+
"files.selectWorkspace": "— 选择工作区 —",
|
|
103
|
+
"files.orProfile": "或直接选连接配置",
|
|
104
|
+
"files.defaultDir": "默认目录(打开该工作区时进入的远程目录)",
|
|
105
|
+
"files.reading": "读取中…",
|
|
106
|
+
"files.errRead": "读取失败",
|
|
107
|
+
"files.errSave": "保存失败",
|
|
108
|
+
"files.errSaveDefaultDir": "保存默认目录失败",
|
|
109
|
+
"files.binary": "二进制文件,无法预览",
|
|
110
|
+
"term.title": "终端 {id}",
|
|
111
|
+
"term.exited": " (已退出)",
|
|
112
|
+
"term.connecting": "正在连接…",
|
|
113
|
+
"term.noOutput": "(无输出)",
|
|
114
|
+
"term.inputPlaceholder": "输入命令后回车…",
|
|
115
|
+
"term.send": "发送",
|
|
116
|
+
"term.needProfile": "请先选择连接配置",
|
|
117
|
+
"term.newTerminal": "+ 新建终端",
|
|
118
|
+
"term.emptyHint": "从上面的连接配置新建一个终端,直接操作远程环境。",
|
|
119
|
+
"term.errSpawn": "启动终端失败",
|
|
120
|
+
"picker.dir": "目录",
|
|
121
|
+
"picker.choose": "选择此目录",
|
|
122
|
+
"flow.localTitle": "选择本地目录",
|
|
123
|
+
"flow.remoteTitle": "选择远程目录",
|
|
124
|
+
"flow.localDir": "本地目录",
|
|
125
|
+
"flow.remoteDir": "远程目录",
|
|
126
|
+
"flow.addWsTitle": "添加工作区",
|
|
127
|
+
"flow.localSeg": "📁 本地目录",
|
|
128
|
+
"flow.remoteSeg": "🌐 远程目录",
|
|
129
|
+
"flow.browse": "浏览",
|
|
130
|
+
"flow.chooseRemote": "选择远程目录…",
|
|
131
|
+
"flow.backLocal": "← 选择本地目录",
|
|
132
|
+
"flow.errCreate": "创建远程工作区失败",
|
|
133
|
+
"settings.importSshConfig": "从 ~/.ssh/config 导入…",
|
|
134
|
+
"settings.importTitle": "导入 ~/.ssh/config 主机",
|
|
135
|
+
"settings.noneFound": "未在 ~/.ssh/config 中发现可用主机",
|
|
136
|
+
"settings.imported": "已导入 {n} 个连接",
|
|
137
|
+
"sync.down": "⬇ 同步",
|
|
138
|
+
"sync.up": "⬆ 推送",
|
|
139
|
+
"sync.syncing": "同步中…",
|
|
140
|
+
"sync.pushing": "推送中…",
|
|
141
|
+
"sync.done": "同步完成",
|
|
142
|
+
"sync.fail": "同步失败",
|
|
143
|
+
"ops.search": "🔍 搜索",
|
|
144
|
+
"ops.find": "按名查找",
|
|
145
|
+
"ops.searchPh": "正则或关键词…",
|
|
146
|
+
"ops.globPh": "*.py",
|
|
147
|
+
"ops.newDir": "📁 新建目录",
|
|
148
|
+
"ops.newDirPh": "目录名",
|
|
149
|
+
"ops.rename": "✏ 重命名/移动",
|
|
150
|
+
"ops.renamePh": "目标路径",
|
|
151
|
+
"ops.delete": "🗑 删除",
|
|
152
|
+
"ops.confirmDel": "确认删除远程文件/目录?(不可恢复)",
|
|
153
|
+
"ops.grepTitle": "远程内容搜索 (grep)",
|
|
154
|
+
"ops.globTitle": "按文件名查找 (glob)",
|
|
155
|
+
"ops.mkdirTitle": "新建远程目录",
|
|
156
|
+
"ops.moveTitle": "移动/重命名",
|
|
157
|
+
"ops.deleteTitle": "删除远程文件/目录",
|
|
158
|
+
"ops.noMatch": "无匹配",
|
|
159
|
+
"ops.done": "操作完成",
|
|
160
|
+
"ops.fail": "操作失败"
|
|
161
|
+
};
|
|
162
|
+
var en = {
|
|
163
|
+
"tabs.files": "Remote Files",
|
|
164
|
+
"tabs.term": "Remote Terminal",
|
|
165
|
+
"settings.nav": "🖥️ Remote Connections",
|
|
166
|
+
"settings.desc": "Manage Remote-SSH connection profiles (SSH key or password authentication).",
|
|
167
|
+
"settings.empty": "No connection profiles yet. Add one below.",
|
|
168
|
+
"settings.add": "Add Profile",
|
|
169
|
+
"settings.workspaces": "Remote Workspaces",
|
|
170
|
+
"settings.name": "Name",
|
|
171
|
+
"settings.namePlaceholder": "e.g. My HPC",
|
|
172
|
+
"settings.host": "Host",
|
|
173
|
+
"settings.hostPlaceholder": "login.example.com",
|
|
174
|
+
"settings.port": "Port",
|
|
175
|
+
"settings.user": "User",
|
|
176
|
+
"settings.userPlaceholder": "your-username",
|
|
177
|
+
"settings.auth": "Authentication",
|
|
178
|
+
"settings.key": "Key (recommended)",
|
|
179
|
+
"settings.password": "Password (requires sshpass)",
|
|
180
|
+
"settings.keyPath": "Private key path (keyPath)",
|
|
181
|
+
"settings.keyPathPlaceholder": "~/.ssh/id_rsa",
|
|
182
|
+
"settings.passwordField": "Password",
|
|
183
|
+
"settings.remoteRoot": "Remote root (remoteRoot)",
|
|
184
|
+
"settings.proxyJump": "ProxyJump (jump host)",
|
|
185
|
+
"settings.test": "Test Connection",
|
|
186
|
+
"settings.saved": "Saved",
|
|
187
|
+
"settings.connOk": "Connected",
|
|
188
|
+
"settings.connFail": "Connection failed: ",
|
|
189
|
+
"settings.errSave": "Save failed",
|
|
190
|
+
"common.selectProfile": "— Select profile —",
|
|
191
|
+
"common.profile": "Profile",
|
|
192
|
+
"common.save": "Save",
|
|
193
|
+
"common.open": "Open",
|
|
194
|
+
"common.close": "Close",
|
|
195
|
+
"common.noEntries": "(no subdirectories)",
|
|
196
|
+
"common.delete": "Delete",
|
|
197
|
+
"common.up": "⬆ Up",
|
|
198
|
+
"common.loading": "Loading…",
|
|
199
|
+
"common.processing": "Processing…",
|
|
200
|
+
"common.errList": "Failed to list directory",
|
|
201
|
+
"files.workspace": "Remote Workspace",
|
|
202
|
+
"files.selectWorkspace": "— Select workspace —",
|
|
203
|
+
"files.orProfile": "or pick a profile directly",
|
|
204
|
+
"files.defaultDir": "Default directory (entered when opening this workspace)",
|
|
205
|
+
"files.reading": "Reading…",
|
|
206
|
+
"files.errRead": "Failed to read",
|
|
207
|
+
"files.errSave": "Failed to save",
|
|
208
|
+
"files.errSaveDefaultDir": "Failed to save default directory",
|
|
209
|
+
"files.binary": "Binary file — preview not available",
|
|
210
|
+
"term.title": "Terminal {id}",
|
|
211
|
+
"term.exited": " (exited)",
|
|
212
|
+
"term.connecting": "Connecting…",
|
|
213
|
+
"term.noOutput": "(no output)",
|
|
214
|
+
"term.inputPlaceholder": "Type a command and press Enter…",
|
|
215
|
+
"term.send": "Send",
|
|
216
|
+
"term.needProfile": "Select a profile first",
|
|
217
|
+
"term.newTerminal": "+ New terminal",
|
|
218
|
+
"term.emptyHint": "Create a terminal from the profile above to operate the remote environment.",
|
|
219
|
+
"term.errSpawn": "Failed to start terminal",
|
|
220
|
+
"picker.dir": "Directory",
|
|
221
|
+
"picker.choose": "Choose this directory",
|
|
222
|
+
"flow.localTitle": "Select Local Directory",
|
|
223
|
+
"flow.remoteTitle": "Select Remote Directory",
|
|
224
|
+
"flow.localDir": "Local directory",
|
|
225
|
+
"flow.remoteDir": "Remote directory",
|
|
226
|
+
"flow.addWsTitle": "Add Workspace",
|
|
227
|
+
"flow.localSeg": "📁 Local",
|
|
228
|
+
"flow.remoteSeg": "🌐 Remote",
|
|
229
|
+
"flow.browse": "Browse",
|
|
230
|
+
"flow.chooseRemote": "Choose remote directory…",
|
|
231
|
+
"flow.backLocal": "← Choose local directory",
|
|
232
|
+
"flow.errCreate": "Failed to create remote workspace",
|
|
233
|
+
"settings.importSshConfig": "Import from ~/.ssh/config…",
|
|
234
|
+
"settings.importTitle": "Import ~/.ssh/config hosts",
|
|
235
|
+
"settings.noneFound": "No usable hosts found in ~/.ssh/config",
|
|
236
|
+
"settings.imported": "Imported {n} connection(s)",
|
|
237
|
+
"sync.down": "⬇ Sync",
|
|
238
|
+
"sync.up": "⬆ Push",
|
|
239
|
+
"sync.syncing": "Syncing…",
|
|
240
|
+
"sync.pushing": "Pushing…",
|
|
241
|
+
"sync.done": "Sync complete",
|
|
242
|
+
"sync.fail": "Sync failed",
|
|
243
|
+
"ops.search": "🔍 Search",
|
|
244
|
+
"ops.find": "Find by name",
|
|
245
|
+
"ops.searchPh": "regex or keyword…",
|
|
246
|
+
"ops.globPh": "*.py",
|
|
247
|
+
"ops.newDir": "📁 New dir",
|
|
248
|
+
"ops.newDirPh": "dir name",
|
|
249
|
+
"ops.rename": "✏ Rename/Move",
|
|
250
|
+
"ops.renamePh": "target path",
|
|
251
|
+
"ops.delete": "🗑 Delete",
|
|
252
|
+
"ops.confirmDel": "Confirm delete remote file/dir? (irreversible)",
|
|
253
|
+
"ops.grepTitle": "Remote content search (grep)",
|
|
254
|
+
"ops.globTitle": "Find by name (glob)",
|
|
255
|
+
"ops.mkdirTitle": "Create remote directory",
|
|
256
|
+
"ops.moveTitle": "Move/Rename",
|
|
257
|
+
"ops.deleteTitle": "Delete remote file/dir",
|
|
258
|
+
"ops.noMatch": "No match",
|
|
259
|
+
"ops.done": "Done",
|
|
260
|
+
"ops.fail": "Operation failed"
|
|
261
|
+
};
|
|
262
|
+
var i18n = {
|
|
263
|
+
t: function (key) { return key; },
|
|
264
|
+
subscribe: function () { return function () {}; },
|
|
265
|
+
getSnapshot: function () { return 0; }
|
|
266
|
+
};
|
|
267
|
+
function useT() {
|
|
268
|
+
var state = React.useState(0);
|
|
269
|
+
var force = state[1];
|
|
270
|
+
React.useEffect(function () {
|
|
271
|
+
return i18n.subscribe(function () { force(function (n) { return n + 1; }); });
|
|
272
|
+
}, []);
|
|
273
|
+
return i18n.t;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// ---- 跨会话切换的状态记忆(按 sessionId 分桶,模块级存活,页面刷新即失效)----
|
|
277
|
+
var memoryBySession = {};
|
|
278
|
+
function useSessionMemory(sessionId, key, initial) {
|
|
279
|
+
var sid = sessionId || "global";
|
|
280
|
+
var bucket = memoryBySession[sid] || (memoryBySession[sid] = {});
|
|
281
|
+
var state = React.useState(function () {
|
|
282
|
+
if (Object.prototype.hasOwnProperty.call(bucket, key)) return bucket[key];
|
|
283
|
+
var init = typeof initial === "function" ? initial() : initial;
|
|
284
|
+
return bucket[key] = init;
|
|
285
|
+
});
|
|
286
|
+
var value = state[0], setValue = state[1];
|
|
287
|
+
React.useEffect(function () { bucket[key] = value; }, [value]);
|
|
288
|
+
return [value, setValue];
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// ---- 与 Host 半边的 JSON RPC ----
|
|
292
|
+
async function api(method, args) {
|
|
293
|
+
try {
|
|
294
|
+
var r = await fetch("/remote-ssh/api/" + method, {
|
|
295
|
+
method: "POST",
|
|
296
|
+
headers: { "content-type": "application/json", "x-requested-with": "XMLHttpRequest" },
|
|
297
|
+
body: JSON.stringify(args || {})
|
|
298
|
+
});
|
|
299
|
+
var j = await r.json();
|
|
300
|
+
if (j && j.ok) return j.value;
|
|
301
|
+
return { ok: false, error: (j && j.error && j.error.message) || ("HTTP " + r.status) };
|
|
302
|
+
} catch (e) {
|
|
303
|
+
return { ok: false, error: String(e && e.message ? e.message : e) };
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
// ---- 跨页签「打开工作区」的轻量共享选择 ----
|
|
308
|
+
var sel = { workspaceId: null };
|
|
309
|
+
var selListeners = [];
|
|
310
|
+
function setSelWorkspace(id) { sel.workspaceId = id; selListeners.forEach(function (fn) { fn(); }); }
|
|
311
|
+
function subscribeSel(fn) { selListeners.push(fn); return function () { var i = selListeners.indexOf(fn); if (i >= 0) selListeners.splice(i, 1); }; }
|
|
312
|
+
|
|
313
|
+
// ---- 通用:拉取连接配置(refreshKey 变化时重新拉取,避免缓存过期)----
|
|
314
|
+
function useProfiles(refreshKey) {
|
|
315
|
+
var state = React.useState([]);
|
|
316
|
+
var profiles = state[0], setProfiles = state[1];
|
|
317
|
+
React.useEffect(function () {
|
|
318
|
+
api("listProfiles").then(function (r) { if (r && r.ok) setProfiles(r.profiles || []); });
|
|
319
|
+
}, [refreshKey]);
|
|
320
|
+
return profiles;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
function Field(props) {
|
|
324
|
+
return h("div", { className: "rssh-row" },
|
|
325
|
+
h("span", { className: "rssh-label" }, props.label),
|
|
326
|
+
h("input", { className: "rssh-input", type: props.type || "text", value: props.value, placeholder: props.placeholder,
|
|
327
|
+
onChange: function (e) { props.onChange(e.target.value); } })
|
|
328
|
+
);
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
// ======================================================================
|
|
332
|
+
// 远程操作弹窗(grep / glob / mkdir / move / delete)
|
|
333
|
+
// ======================================================================
|
|
334
|
+
function OpsModal(props) {
|
|
335
|
+
var d = props.opsDlg;
|
|
336
|
+
var t = props.t;
|
|
337
|
+
var patchOps = props.patchOps;
|
|
338
|
+
var runOps = props.runOps;
|
|
339
|
+
var title = ({
|
|
340
|
+
grep: t("ops.grepTitle"), glob: t("ops.globTitle"), mkdir: t("ops.mkdirTitle"),
|
|
341
|
+
move: t("ops.moveTitle"), delete: t("ops.deleteTitle")
|
|
342
|
+
})[d.type] || "";
|
|
343
|
+
var isGrep = d.type === "grep";
|
|
344
|
+
var isGlob = d.type === "glob";
|
|
345
|
+
var isMove = d.type === "move";
|
|
346
|
+
var isDel = d.type === "delete";
|
|
347
|
+
// 结果渲染
|
|
348
|
+
var resultEl = null;
|
|
349
|
+
if (d.result) {
|
|
350
|
+
var r = d.result;
|
|
351
|
+
if (isGrep) {
|
|
352
|
+
var ms = r.matches || [];
|
|
353
|
+
resultEl = h("div", { style: { maxHeight: 260, overflow: "auto", marginTop: 8, fontSize: 11, whiteSpace: "pre-wrap", fontFamily: "monospace" } },
|
|
354
|
+
!r.ok ? h("div", { className: "rssh-err" }, r.error || t("ops.fail")) :
|
|
355
|
+
ms.length === 0 ? h("div", { className: "rssh-muted" }, t("ops.noMatch")) :
|
|
356
|
+
ms.map(function (m, i) { return h("div", { key: i }, m.file + ":" + m.line + ": " + m.content); }),
|
|
357
|
+
r.truncated ? h("div", { className: "rssh-muted" }, "…") : null
|
|
358
|
+
);
|
|
359
|
+
} else if (isGlob) {
|
|
360
|
+
var fs2 = r.files || [];
|
|
361
|
+
resultEl = h("div", { style: { maxHeight: 260, overflow: "auto", marginTop: 8, fontSize: 11, whiteSpace: "pre-wrap", fontFamily: "monospace" } },
|
|
362
|
+
!r.ok ? h("div", { className: "rssh-err" }, r.error || t("ops.fail")) :
|
|
363
|
+
fs2.length === 0 ? h("div", { className: "rssh-muted" }, t("ops.noMatch")) :
|
|
364
|
+
fs2.map(function (f, i) { return h("div", { key: i }, f); })
|
|
365
|
+
);
|
|
366
|
+
} else {
|
|
367
|
+
// exec 结果(mkdir/move/delete)
|
|
368
|
+
resultEl = h("div", { style: { marginTop: 8 } },
|
|
369
|
+
r.ok ? h("div", { className: "rssh-ok" }, t("ops.done") + (r.stderr ? " " + String(r.stderr).slice(0, 200) : "")) :
|
|
370
|
+
h("div", { className: "rssh-err" }, (r.error || r.stderr || t("ops.fail")))
|
|
371
|
+
);
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
return h(Modal, { open: true, onClose: props.close, title: title, closeLabel: t("common.close"), className: "rssh-flow-dialog" },
|
|
375
|
+
h("div", { style: { display: "flex", flexDirection: "column", gap: 6 } },
|
|
376
|
+
h("input", { className: "rssh-input", value: d.p1, onChange: function (e) { patchOps({ p1: e.target.value }); }, placeholder: isGrep ? t("ops.searchPh") : (isGlob ? t("ops.globPh") : t("ops.newDirPh")) }),
|
|
377
|
+
isGrep ? h("input", { className: "rssh-input", value: d.p2, onChange: function (e) { patchOps({ p2: e.target.value }); }, placeholder: "*.py (include)" }) : null,
|
|
378
|
+
isMove ? h("input", { className: "rssh-input", value: d.p2, onChange: function (e) { patchOps({ p2: e.target.value }); }, placeholder: t("ops.renamePh") }) : null,
|
|
379
|
+
h("div", { className: "rssh-actions" },
|
|
380
|
+
h("button", { className: "rssh-btn", onClick: runOps, disabled: d.busy }, d.busy ? t("common.processing") : (isDel ? t("ops.delete") : t("common.save"))),
|
|
381
|
+
h("button", { className: "rssh-btn", onClick: props.close, disabled: d.busy }, t("common.close"))
|
|
382
|
+
),
|
|
383
|
+
resultEl
|
|
384
|
+
)
|
|
385
|
+
);
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
// ======================================================================
|
|
389
|
+
// 远程文件页签
|
|
390
|
+
// ======================================================================
|
|
391
|
+
function FilesTab(props) {
|
|
392
|
+
var t = useT();
|
|
393
|
+
var sessionId = props.sessionId;
|
|
394
|
+
var betterSidebar = props.betterSidebar;
|
|
395
|
+
var [workspaces, setWorkspaces] = React.useState([]);
|
|
396
|
+
var profiles = useProfiles();
|
|
397
|
+
var [wsId, setWsId] = useSessionMemory(sessionId, "wsId", sel.workspaceId);
|
|
398
|
+
var [profileId, setProfileId] = useSessionMemory(sessionId, "profileId", "");
|
|
399
|
+
var [path, setPath] = useSessionMemory(sessionId, "path", "");
|
|
400
|
+
var [entries, setEntries] = useSessionMemory(sessionId, "entries", []);
|
|
401
|
+
var [err, setErr] = React.useState(null);
|
|
402
|
+
var [loading, setLoading] = React.useState(false);
|
|
403
|
+
var [defaultDir, setDefaultDir] = useSessionMemory(sessionId, "defaultDir", "");
|
|
404
|
+
var [opsDlg, setOpsDlg] = React.useState(null); // { type, p1, p2, result, busy }
|
|
405
|
+
|
|
406
|
+
React.useEffect(function () {
|
|
407
|
+
api("listWorkspaces").then(function (r) { if (r && r.ok) setWorkspaces(r.workspaces || []); });
|
|
408
|
+
return subscribeSel(function () { setWsId(sel.workspaceId); });
|
|
409
|
+
}, []);
|
|
410
|
+
|
|
411
|
+
function load(targetProfile, targetPath) {
|
|
412
|
+
setLoading(true); setErr(null);
|
|
413
|
+
api("listDir", { profileId: targetProfile, path: targetPath }).then(function (r) {
|
|
414
|
+
setLoading(false);
|
|
415
|
+
if (r && r.ok) { setEntries(r.entries || []); setPath(r.path || targetPath); }
|
|
416
|
+
else setErr((r && r.error) || t("common.errList"));
|
|
417
|
+
});
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
// 工作区/配置选择变化时进入对应目录
|
|
421
|
+
React.useEffect(function () {
|
|
422
|
+
if (!wsId && !profileId) return;
|
|
423
|
+
var pid = profileId;
|
|
424
|
+
var rootPath = "";
|
|
425
|
+
if (wsId) {
|
|
426
|
+
var ws = workspaces.find(function (w) { return w.id === wsId; });
|
|
427
|
+
if (ws) { pid = ws.profileId; rootPath = ws.remotePath; }
|
|
428
|
+
}
|
|
429
|
+
if (!pid) return;
|
|
430
|
+
setProfileId(pid);
|
|
431
|
+
load(pid, rootPath || undefined);
|
|
432
|
+
}, [wsId, profileId, workspaces]);
|
|
433
|
+
|
|
434
|
+
// 选中工作区时,把「默认目录」输入框同步为该工作区的 remotePath。
|
|
435
|
+
React.useEffect(function () {
|
|
436
|
+
if (wsId) {
|
|
437
|
+
var ws = workspaces.find(function (w) { return w.id === wsId; });
|
|
438
|
+
if (ws) setDefaultDir(ws.remotePath || "");
|
|
439
|
+
}
|
|
440
|
+
}, [wsId, workspaces]);
|
|
441
|
+
|
|
442
|
+
function saveDefaultDir() {
|
|
443
|
+
if (!wsId) return;
|
|
444
|
+
api("updateWorkspace", { id: wsId, remotePath: defaultDir }).then(function (r) {
|
|
445
|
+
if (r && r.ok) { setWorkspaces(r.workspaces || []); setErr(null); }
|
|
446
|
+
else setErr((r && r.error) || t("files.errSaveDefaultDir"));
|
|
447
|
+
});
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
function activeProfileId() {
|
|
451
|
+
if (wsId) {
|
|
452
|
+
var ws = workspaces.find(function (w) { return w.id === wsId; });
|
|
453
|
+
if (ws) return ws.profileId;
|
|
454
|
+
}
|
|
455
|
+
return profileId;
|
|
456
|
+
}
|
|
457
|
+
function opsBase() {
|
|
458
|
+
if (wsId) {
|
|
459
|
+
var ws = workspaces.find(function (w) { return w.id === wsId; });
|
|
460
|
+
if (ws) return ws.remotePath || path || "~";
|
|
461
|
+
}
|
|
462
|
+
return path || "~";
|
|
463
|
+
}
|
|
464
|
+
function openOps(type) {
|
|
465
|
+
var init = { type: type, p1: "", p2: "", result: null, busy: false };
|
|
466
|
+
if (type === "mkdir") init.p1 = path ? (path.replace(/\/+$/, "") + "/") : "";
|
|
467
|
+
if (type === "move") { init.p1 = path ? (path.replace(/\/+$/, "") + "/") : ""; init.p2 = path ? (path.replace(/\/+$/, "") + "/") : ""; }
|
|
468
|
+
if (type === "delete") init.p1 = path ? (path.replace(/\/+$/, "") + "/") : "";
|
|
469
|
+
setOpsDlg(init);
|
|
470
|
+
}
|
|
471
|
+
function patchOps(patch) {
|
|
472
|
+
setOpsDlg(function (d) { var n = {}; for (var k in d) n[k] = d[k]; for (var k2 in patch) n[k2] = patch[k2]; return n; });
|
|
473
|
+
}
|
|
474
|
+
function runOps() {
|
|
475
|
+
if (!opsDlg) return;
|
|
476
|
+
var pid = activeProfileId();
|
|
477
|
+
if (!pid) { patchOps({ result: { ok: false, msg: t("ops.fail") }, busy: false }); return; }
|
|
478
|
+
var base = opsBase();
|
|
479
|
+
var d = opsDlg;
|
|
480
|
+
patchOps({ busy: true, result: null });
|
|
481
|
+
function resolveP(p) {
|
|
482
|
+
if (!p) return p;
|
|
483
|
+
var c = String(p).charAt(0);
|
|
484
|
+
if (c === "/" || c === "~") return p;
|
|
485
|
+
return base.replace(/\/+$/, "") + "/" + p;
|
|
486
|
+
}
|
|
487
|
+
if (d.type === "grep") {
|
|
488
|
+
api("grep", { profileId: pid, pattern: d.p1, path: base, include: d.p2 || undefined }).then(function (r) {
|
|
489
|
+
patchOps({ busy: false, result: r });
|
|
490
|
+
});
|
|
491
|
+
} else if (d.type === "glob") {
|
|
492
|
+
api("glob", { profileId: pid, pattern: d.p1, path: base }).then(function (r) {
|
|
493
|
+
patchOps({ busy: false, result: r });
|
|
494
|
+
});
|
|
495
|
+
} else if (d.type === "mkdir") {
|
|
496
|
+
api("mkdir", { profileId: pid, path: resolveP(d.p1) }).then(function (r) {
|
|
497
|
+
patchOps({ busy: false, result: r });
|
|
498
|
+
if (r && r.ok) { setErr(null); load(pid, path); }
|
|
499
|
+
});
|
|
500
|
+
} else if (d.type === "move") {
|
|
501
|
+
api("move", { profileId: pid, src: resolveP(d.p1), dst: resolveP(d.p2) }).then(function (r) {
|
|
502
|
+
patchOps({ busy: false, result: r });
|
|
503
|
+
if (r && r.ok) { setErr(null); load(pid, path); }
|
|
504
|
+
});
|
|
505
|
+
} else if (d.type === "delete") {
|
|
506
|
+
api("deleteFile", { profileId: pid, path: resolveP(d.p1) }).then(function (r) {
|
|
507
|
+
patchOps({ busy: false, result: r });
|
|
508
|
+
if (r && r.ok) { setErr(null); load(pid, path); }
|
|
509
|
+
});
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
function openEntry(e) {
|
|
514
|
+
var base = path ? (path.replace(/\/+$/, "") + "/" + e.name) : e.name;
|
|
515
|
+
if (e.type === "directory") {
|
|
516
|
+
setPath(base);
|
|
517
|
+
load(profileId, base);
|
|
518
|
+
} else {
|
|
519
|
+
// 用独立页签预览远程文件(与内置「文件」页签一致),而非在文件树下方预览。
|
|
520
|
+
if (betterSidebar && betterSidebar.openTab) {
|
|
521
|
+
betterSidebar.openTab({
|
|
522
|
+
type: "remssh:editor",
|
|
523
|
+
id: "remssh:editor:" + profileId + ":" + base,
|
|
524
|
+
title: e.name,
|
|
525
|
+
path: base,
|
|
526
|
+
meta: { profileId: profileId, remotePath: base }
|
|
527
|
+
}, { sessionId: sessionId });
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
function goUp() {
|
|
533
|
+
if (!path) return;
|
|
534
|
+
var parts = path.replace(/\/+$/, "").split("/");
|
|
535
|
+
parts.pop();
|
|
536
|
+
var up = parts.join("/") || "/";
|
|
537
|
+
setPath(up);
|
|
538
|
+
load(profileId, up);
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
return h("div", { style: { display: "flex", flexDirection: "column", flex: 1, minHeight: 0, overflow: "hidden" } },
|
|
542
|
+
h("div", { style: { flex: "none" } },
|
|
543
|
+
h("div", { className: "rssh-row" },
|
|
544
|
+
h("span", { className: "rssh-label" }, t("files.workspace")),
|
|
545
|
+
h("select", { className: "rssh-select", value: wsId || "", onChange: function (e) { setWsId(e.target.value); setProfileId(""); } },
|
|
546
|
+
h("option", { value: "" }, t("files.selectWorkspace")),
|
|
547
|
+
workspaces.map(function (w) { return h("option", { key: w.id, value: w.id }, w.title); })
|
|
548
|
+
)
|
|
549
|
+
),
|
|
550
|
+
h("div", { className: "rssh-row" },
|
|
551
|
+
h("span", { className: "rssh-label" }, t("files.orProfile")),
|
|
552
|
+
h("select", { className: "rssh-select", value: profileId, onChange: function (e) { setProfileId(e.target.value); setWsId(""); } },
|
|
553
|
+
h("option", { value: "" }, t("common.selectProfile")),
|
|
554
|
+
profiles.map(function (p) { return h("option", { key: p.id, value: p.id }, p.name); })
|
|
555
|
+
)
|
|
556
|
+
),
|
|
557
|
+
wsId ? h("div", { className: "rssh-row" },
|
|
558
|
+
h("span", { className: "rssh-label" }, t("files.defaultDir")),
|
|
559
|
+
h("div", { className: "rssh-pathbar" },
|
|
560
|
+
h("input", { className: "rssh-input", value: defaultDir, onChange: function (e) { setDefaultDir(e.target.value); }, placeholder: "/home/user/project" }),
|
|
561
|
+
h("button", { className: "rssh-btn", onClick: saveDefaultDir }, t("common.save"))
|
|
562
|
+
)
|
|
563
|
+
) : null,
|
|
564
|
+
h("div", { className: "rssh-pathbar" },
|
|
565
|
+
h("input", { className: "rssh-input", value: path, onChange: function (e) { setPath(e.target.value); }, placeholder: "/home/user" }),
|
|
566
|
+
h("button", { className: "rssh-btn", onClick: function () { load(profileId, path); } }, t("common.open")),
|
|
567
|
+
h("button", { className: "rssh-btn", onClick: goUp }, t("common.up"))
|
|
568
|
+
),
|
|
569
|
+
wsId ? h("div", { className: "rssh-pathbar", style: { flexWrap: "wrap" } },
|
|
570
|
+
h("button", { className: "rssh-btn", onClick: function () { openOps("grep"); } }, t("ops.search")),
|
|
571
|
+
h("button", { className: "rssh-btn", onClick: function () { openOps("glob"); } }, t("ops.find")),
|
|
572
|
+
h("button", { className: "rssh-btn", onClick: function () { openOps("mkdir"); } }, t("ops.newDir")),
|
|
573
|
+
h("button", { className: "rssh-btn", onClick: function () { openOps("move"); } }, t("ops.rename")),
|
|
574
|
+
h("button", { className: "rssh-btn", onClick: function () { openOps("delete"); } }, t("ops.delete"))
|
|
575
|
+
) : (wsId || profileId) ? h("div", { className: "rssh-pathbar", style: { flexWrap: "wrap" } },
|
|
576
|
+
h("button", { className: "rssh-btn", onClick: function () { openOps("grep"); } }, t("ops.search")),
|
|
577
|
+
h("button", { className: "rssh-btn", onClick: function () { openOps("glob"); } }, t("ops.find")),
|
|
578
|
+
h("button", { className: "rssh-btn", onClick: function () { openOps("mkdir"); } }, t("ops.newDir")),
|
|
579
|
+
h("button", { className: "rssh-btn", onClick: function () { openOps("move"); } }, t("ops.rename")),
|
|
580
|
+
h("button", { className: "rssh-btn", onClick: function () { openOps("delete"); } }, t("ops.delete"))
|
|
581
|
+
) : null,
|
|
582
|
+
loading ? h("div", { className: "rssh-muted" }, t("common.loading")) : null,
|
|
583
|
+
err ? h("div", { className: "rssh-err", style: { whiteSpace: "pre-wrap" } }, err) : null,
|
|
584
|
+
opsDlg ? h(OpsModal, { opsDlg: opsDlg, patchOps: patchOps, runOps: runOps, close: function () { setOpsDlg(null); }, t: t }) : null
|
|
585
|
+
),
|
|
586
|
+
h("div", { style: { flex: 1, minHeight: 0, overflowY: "auto", marginTop: 6 } },
|
|
587
|
+
entries.map(function (e) {
|
|
588
|
+
return h("div", { key: e.name, className: "rssh-tree-item", onClick: function () { openEntry(e); } },
|
|
589
|
+
h("span", { className: e.type === "directory" ? "rssh-tree-dir" : "rssh-tree-file" }, e.type === "directory" ? "📁 " + e.name : "📄 " + e.name),
|
|
590
|
+
e.type === "file" ? h("span", { className: "rssh-tree-size" }, String(e.size)) : null
|
|
591
|
+
);
|
|
592
|
+
})
|
|
593
|
+
)
|
|
594
|
+
);
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
// ======================================================================
|
|
598
|
+
// 远程文件编辑器页签:点击文件树中的文件后单开一栏预览/编辑(与内置「文件」页签一致)
|
|
599
|
+
// ======================================================================
|
|
600
|
+
function RemoteEditor(props) {
|
|
601
|
+
var t = useT();
|
|
602
|
+
var tab = props.tab || {};
|
|
603
|
+
var ctx = props.ctx;
|
|
604
|
+
var meta = tab.meta || {};
|
|
605
|
+
var profileId = meta.profileId || "";
|
|
606
|
+
var remotePath = tab.path || meta.remotePath || "";
|
|
607
|
+
var [data, setData] = React.useState({ loading: true, content: "", binary: false, dirty: false, error: null });
|
|
608
|
+
|
|
609
|
+
React.useEffect(function () {
|
|
610
|
+
var alive = true;
|
|
611
|
+
setData({ loading: true, content: "", binary: false, dirty: false, error: null });
|
|
612
|
+
api("readFile", { profileId: profileId, path: remotePath }).then(function (r) {
|
|
613
|
+
if (!alive) return;
|
|
614
|
+
if (r && r.ok) setData({ loading: false, content: r.content || "", binary: !!r.binary, dirty: false, error: null });
|
|
615
|
+
else setData({ loading: false, content: "", binary: false, dirty: false, error: (r && r.error) || t("files.errRead") });
|
|
616
|
+
});
|
|
617
|
+
return function () { alive = false; };
|
|
618
|
+
}, [profileId, remotePath]);
|
|
619
|
+
|
|
620
|
+
function save() {
|
|
621
|
+
api("writeFile", { profileId: profileId, path: remotePath, content: data.content }).then(function (r) {
|
|
622
|
+
if (r && r.ok) setData(Object.assign({}, data, { dirty: false, error: null }));
|
|
623
|
+
else setData(Object.assign({}, data, { error: (r && r.error) || t("files.errSave") }));
|
|
624
|
+
});
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
function close() {
|
|
628
|
+
if (ctx && ctx.betterSidebar && tab.id) ctx.betterSidebar.closeTab(tab.id);
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
if (data.loading) return h("div", { className: "rssh-muted", style: { padding: 8 } }, t("files.reading"));
|
|
632
|
+
if (data.error) return h("div", { className: "rssh-err", style: { padding: 8, whiteSpace: "pre-wrap" } }, data.error);
|
|
633
|
+
if (data.binary) return h("div", { className: "rssh-muted", style: { padding: 8 } }, t("files.binary"));
|
|
634
|
+
|
|
635
|
+
return h("div", { style: { display: "flex", flexDirection: "column", flex: 1, minHeight: 0, padding: 8, overflow: "hidden" } },
|
|
636
|
+
h("div", { className: "rssh-pathbar", style: { flex: "none" } },
|
|
637
|
+
h("span", { className: "rssh-title", style: { flex: 1, wordBreak: "break-all" } }, remotePath),
|
|
638
|
+
h("button", { className: "rssh-btn", onClick: save }, t("common.save")),
|
|
639
|
+
h("button", { className: "rssh-btn", onClick: close }, t("common.close"))
|
|
640
|
+
),
|
|
641
|
+
h("textarea", { className: "rssh-content", style: { flex: 1, minHeight: 0, maxHeight: "none", width: "100%", boxSizing: "border-box", background: "var(--dsw-alias-markdown-code-block)", color: "var(--dsw-alias-label-primary)", border: "1px solid var(--dsw-alias-border-l2)" },
|
|
642
|
+
value: data.content, onChange: function (e) { setData(Object.assign({}, data, { content: e.target.value, dirty: true })); } })
|
|
643
|
+
);
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
// ======================================================================
|
|
647
|
+
// 远程终端页签
|
|
648
|
+
// ======================================================================
|
|
649
|
+
function TerminalView(props) {
|
|
650
|
+
var t = useT();
|
|
651
|
+
var [data, setData] = React.useState("");
|
|
652
|
+
var [status, setStatus] = React.useState("running");
|
|
653
|
+
var [input, setInput] = React.useState("");
|
|
654
|
+
var endRef = React.useRef(null);
|
|
655
|
+
|
|
656
|
+
React.useEffect(function () {
|
|
657
|
+
var timer = setInterval(function () {
|
|
658
|
+
api("terminalRead", { id: props.id }).then(function (r) {
|
|
659
|
+
if (r && r.ok) {
|
|
660
|
+
if (r.data) setData(function (d) { return d + r.data; });
|
|
661
|
+
if (r.status) setStatus(r.status);
|
|
662
|
+
}
|
|
663
|
+
});
|
|
664
|
+
}, 200);
|
|
665
|
+
return function () { clearInterval(timer); };
|
|
666
|
+
}, [props.id]);
|
|
667
|
+
|
|
668
|
+
React.useEffect(function () {
|
|
669
|
+
if (endRef.current) endRef.current.scrollTop = endRef.current.scrollHeight;
|
|
670
|
+
}, [data]);
|
|
671
|
+
|
|
672
|
+
function send() {
|
|
673
|
+
if (!input) return;
|
|
674
|
+
api("terminalWrite", { id: props.id, data: input + "\r" });
|
|
675
|
+
setInput("");
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
return h("div", { className: "rssh-terminal" },
|
|
679
|
+
h("div", { className: "rssh-terminal-bar" },
|
|
680
|
+
h("span", { style: { flex: 1, color: status === "running" ? "var(--dsw-alias-state-success-primary)" : "var(--dsw-alias-state-error-primary)" } }, t("term.title", { id: props.id }) + (status === "exited" ? t("term.exited") : "")),
|
|
681
|
+
h("button", { className: "rssh-btn", onClick: function () { props.onClose(props.id); } }, t("common.close"))
|
|
682
|
+
),
|
|
683
|
+
h("div", { className: "rssh-terminal-out", ref: endRef }, data || (status === "running" ? t("term.connecting") : t("term.noOutput"))),
|
|
684
|
+
h("div", { className: "rssh-terminal-input-row" },
|
|
685
|
+
h("input", { className: "rssh-input", value: input,
|
|
686
|
+
onChange: function (e) { setInput(e.target.value); },
|
|
687
|
+
onKeyDown: function (e) { if (e.key === "Enter") send(); },
|
|
688
|
+
placeholder: t("term.inputPlaceholder") }),
|
|
689
|
+
h("button", { className: "rssh-btn", onClick: send }, t("term.send"))
|
|
690
|
+
)
|
|
691
|
+
);
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
function TerminalTab(props) {
|
|
695
|
+
var t = useT();
|
|
696
|
+
var sessionId = props.sessionId;
|
|
697
|
+
var profiles = useProfiles();
|
|
698
|
+
var [profileId, setProfileId] = useSessionMemory(sessionId, "profileId", "");
|
|
699
|
+
var [terms, setTerms] = useSessionMemory(sessionId, "terms", []);
|
|
700
|
+
var [err, setErr] = React.useState(null);
|
|
701
|
+
|
|
702
|
+
function spawn() {
|
|
703
|
+
if (!profileId) { setErr(t("term.needProfile")); return; }
|
|
704
|
+
setErr(null);
|
|
705
|
+
api("spawnTerminal", { profileId: profileId }).then(function (r) {
|
|
706
|
+
if (r && r.ok) setTerms(function (list) { return list.concat([{ id: r.id }]); });
|
|
707
|
+
else setErr((r && r.error) || t("term.errSpawn"));
|
|
708
|
+
});
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
function close(id) {
|
|
712
|
+
api("terminalClose", { id: id });
|
|
713
|
+
setTerms(function (list) { return list.filter(function (x) { return x.id !== id; }); });
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
return h("div", null,
|
|
717
|
+
h("div", { className: "rssh-row" },
|
|
718
|
+
h("span", { className: "rssh-label" }, t("common.profile")),
|
|
719
|
+
h("select", { className: "rssh-select", value: profileId, onChange: function (e) { setProfileId(e.target.value); } },
|
|
720
|
+
h("option", { value: "" }, t("common.selectProfile")),
|
|
721
|
+
profiles.map(function (p) { return h("option", { key: p.id, value: p.id }, p.name); })
|
|
722
|
+
)
|
|
723
|
+
),
|
|
724
|
+
h("button", { className: "rssh-btn", onClick: spawn }, t("term.newTerminal")),
|
|
725
|
+
err ? h("div", { className: "rssh-err", style: { marginTop: 6 } }, err) : null,
|
|
726
|
+
terms.length === 0 ? h("div", { className: "rssh-empty" }, t("term.emptyHint")) : null,
|
|
727
|
+
terms.map(function (term) { return h(TerminalView, { key: term.id, id: term.id, onClose: close }); })
|
|
728
|
+
);
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
// ======================================================================
|
|
732
|
+
// 目录选择器(供「添加工作区」弹窗流程使用:本地 browse / 远程 SSH)
|
|
733
|
+
// ======================================================================
|
|
734
|
+
// 本地目录列举:走 DSH browse 能力(无原生对话框时替代 pickDirectory)。
|
|
735
|
+
function localListFn(workspaces, p) {
|
|
736
|
+
// 走 better-sidebar 的 fs.tree 端点(已被本插件拦截,本地工作区走本地 fs)
|
|
737
|
+
// 不依赖 workspaces.listDirectory(需要 browse capability,可能被禁用)
|
|
738
|
+
return fetch("/sidebar/api/fs.tree", {
|
|
739
|
+
method: "POST",
|
|
740
|
+
headers: { "content-type": "application/json" },
|
|
741
|
+
body: JSON.stringify({ sessionId: "", path: p || "" })
|
|
742
|
+
}).then(function (r) { return r.json(); }).then(function (j) {
|
|
743
|
+
if (!j || !j.ok) return { ok: false, error: (j && j.error && j.error.message) || "list failed" };
|
|
744
|
+
var v = j.value || {};
|
|
745
|
+
return {
|
|
746
|
+
ok: true,
|
|
747
|
+
path: v.path || p || "",
|
|
748
|
+
entries: (v.entries || []).filter(function (e) { return e.isDir; }).map(function (e) { return { name: e.name, type: "directory", path: e.path }; })
|
|
749
|
+
};
|
|
750
|
+
}).catch(function (e) {
|
|
751
|
+
return { ok: false, error: String(e && e.message ? e.message : e) };
|
|
752
|
+
});
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
// 远程目录列举:走本插件 SSH API,并为每条记录补全绝对 path。
|
|
756
|
+
function remoteListFn(profileId, p) {
|
|
757
|
+
return api("listDir", { profileId: profileId, path: p }).then(function (r) {
|
|
758
|
+
if (!(r && r.ok)) return r;
|
|
759
|
+
var parent = r.path || "";
|
|
760
|
+
var entries = (r.entries || []).map(function (e) {
|
|
761
|
+
return { name: e.name, type: e.type, path: (parent ? parent.replace(/\/+$/, "") + "/" : "") + e.name };
|
|
762
|
+
});
|
|
763
|
+
return { ok: true, path: r.path, entries: entries };
|
|
764
|
+
});
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
// 计算父目录,兼容 Windows(\\) 与 POSIX(/) 分隔符。
|
|
768
|
+
function parentOf(path) {
|
|
769
|
+
if (!path) return null;
|
|
770
|
+
var str = String(path).replace(/[\/\\]+$/, "");
|
|
771
|
+
if (str === "") return null;
|
|
772
|
+
var idx = Math.max(str.lastIndexOf("/"), str.lastIndexOf("\\"));
|
|
773
|
+
if (idx < 0) return null;
|
|
774
|
+
var parent = str.slice(0, idx);
|
|
775
|
+
if (/^[A-Za-z]:$/.test(parent)) return parent + "\\";
|
|
776
|
+
if (parent === "") return "/";
|
|
777
|
+
return parent;
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
// 通用目录选择器:listFn(path) 返回 {ok, path, entries:[{name,type,path}]}。
|
|
781
|
+
// resetKey 变化(如远程切换连接)时从根目录重新加载。
|
|
782
|
+
function DirPicker(props) {
|
|
783
|
+
var t = useT();
|
|
784
|
+
var [path, setPath] = React.useState("");
|
|
785
|
+
var [entries, setEntries] = React.useState([]);
|
|
786
|
+
var [err, setErr] = React.useState(null);
|
|
787
|
+
var [loading, setLoading] = React.useState(false);
|
|
788
|
+
|
|
789
|
+
var listFnRef = React.useRef(props.listFn);
|
|
790
|
+
listFnRef.current = props.listFn;
|
|
791
|
+
|
|
792
|
+
function load(p) {
|
|
793
|
+
setLoading(true); setErr(null);
|
|
794
|
+
listFnRef.current(p).then(function (r) {
|
|
795
|
+
setLoading(false);
|
|
796
|
+
if (r && r.ok) { setEntries(r.entries || []); setPath(r.path || p || ""); }
|
|
797
|
+
else setErr((r && r.error) || t("common.errList"));
|
|
798
|
+
});
|
|
799
|
+
}
|
|
800
|
+
React.useEffect(function () { load(undefined); }, [props.resetKey]);
|
|
801
|
+
|
|
802
|
+
function enterDir(e) { load(e.path); }
|
|
803
|
+
function goUp() {
|
|
804
|
+
var up = parentOf(path);
|
|
805
|
+
if (up !== null) load(up);
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
// 打开按钮:按输入框路径加载该路径下的文件树。空路径时打开主目录。
|
|
809
|
+
function open() {
|
|
810
|
+
load(path || undefined);
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
// 路径栏:输入框(回车导航)+ 打开按钮(紧挨在右侧)+ 上级
|
|
814
|
+
return h("div", null,
|
|
815
|
+
h("div", { className: "rssh-pathbar" },
|
|
816
|
+
h("input", { className: "rssh-input", value: path,
|
|
817
|
+
onChange: function (e) { setPath(e.target.value); },
|
|
818
|
+
onKeyDown: function (e) { if (e.key === "Enter") { e.preventDefault(); open(); } },
|
|
819
|
+
placeholder: props.placeholder || t("picker.dir") }),
|
|
820
|
+
h("button", { className: "rssh-btn", onClick: open, disabled: loading }, t("common.open")),
|
|
821
|
+
h("button", { className: "rssh-btn", onClick: goUp }, t("common.up"))
|
|
822
|
+
),
|
|
823
|
+
loading ? h("div", { className: "rssh-muted" }, t("common.loading")) : null,
|
|
824
|
+
err ? h("div", { className: "rssh-err" }, err) : null,
|
|
825
|
+
// 当前路径下的文件树(仅目录可进入)
|
|
826
|
+
h("div", { className: "rssh-pick-tree" },
|
|
827
|
+
(entries.filter(function (e) { return e.type === "directory"; })).length === 0 && !loading
|
|
828
|
+
? h("div", { className: "rssh-pick-empty" }, t("common.noEntries"))
|
|
829
|
+
: entries.filter(function (e) { return e.type === "directory"; }).map(function (e) {
|
|
830
|
+
return h("div", { key: e.path || e.name, className: "rssh-tree-item", onClick: function () { enterDir(e); } },
|
|
831
|
+
h("span", { className: "rssh-tree-dir" }, "📁 " + e.name));
|
|
832
|
+
})
|
|
833
|
+
),
|
|
834
|
+
h("div", { className: "rssh-actions", style: { marginTop: 8 } },
|
|
835
|
+
h("button", { className: "rssh-btn", disabled: !path || props.disabled, onClick: function () { props.onChoose(path); } }, t("picker.choose"))
|
|
836
|
+
)
|
|
837
|
+
);
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
function RemoteWorkspaceFlow(props) {
|
|
841
|
+
var t = useT();
|
|
842
|
+
var [mode, setMode] = React.useState("local");
|
|
843
|
+
var profiles = useProfiles(props.open);
|
|
844
|
+
var [profileId, setProfileId] = React.useState("");
|
|
845
|
+
var [creating, setCreating] = React.useState(false);
|
|
846
|
+
|
|
847
|
+
// 每次打开流程时重置为「本地目录」起始界面。
|
|
848
|
+
React.useEffect(function () {
|
|
849
|
+
if (props.open) { setMode("local"); setProfileId(""); }
|
|
850
|
+
}, [props.open]);
|
|
851
|
+
|
|
852
|
+
if (!props.open) return null;
|
|
853
|
+
|
|
854
|
+
function chooseLocal(p) { props.onPicked(p); }
|
|
855
|
+
|
|
856
|
+
function chooseRemote(remotePath) {
|
|
857
|
+
setCreating(true);
|
|
858
|
+
api("createRemoteWorkspace", { profileId: profileId, remotePath: remotePath }).then(function (r) {
|
|
859
|
+
setCreating(false);
|
|
860
|
+
if (r && r.ok && r.mirrorPath) props.onPicked(r.mirrorPath);
|
|
861
|
+
else props.onError((r && r.error) || t("flow.errCreate"));
|
|
862
|
+
});
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
var canClose = !creating && !props.busy;
|
|
866
|
+
var isRemote = mode === "remote";
|
|
867
|
+
|
|
868
|
+
// 统一弹窗:上方本地/远程分段切换 → 远程时显示连接选择 → 路径栏(浏览按钮紧挨右侧)→ 文件树
|
|
869
|
+
return h(Modal, { open: true, onClose: function () { if (canClose) props.onCancel(); }, title: t("flow.addWsTitle"), closeLabel: t("common.close"), className: "rssh-flow-dialog" },
|
|
870
|
+
h("div", { style: { padding: 8 } },
|
|
871
|
+
creating || props.busy ? h("div", { className: "rssh-muted" }, t("common.processing")) : null,
|
|
872
|
+
// 分段切换:本地 / 远程
|
|
873
|
+
h("div", { className: "rssh-seg" },
|
|
874
|
+
h("button", { className: "rssh-seg-btn" + (isRemote ? "" : " active"), onClick: function () { setMode("local"); }, disabled: !!props.busy || creating }, t("flow.localSeg")),
|
|
875
|
+
h("button", { className: "rssh-seg-btn" + (isRemote ? " active" : ""), onClick: function () { setMode("remote"); }, disabled: !!props.busy || creating }, t("flow.remoteSeg"))
|
|
876
|
+
),
|
|
877
|
+
// 远程模式:连接配置选择
|
|
878
|
+
isRemote ? h("div", { className: "rssh-row" },
|
|
879
|
+
h("span", { className: "rssh-label" }, t("common.profile")),
|
|
880
|
+
h("select", { className: "rssh-select", value: profileId, onChange: function (e) { setProfileId(e.target.value); }, disabled: creating },
|
|
881
|
+
h("option", { value: "" }, t("common.selectProfile")),
|
|
882
|
+
profiles.map(function (p) { return h("option", { key: p.id, value: p.id }, p.name); })
|
|
883
|
+
)
|
|
884
|
+
) : null,
|
|
885
|
+
// 目录选择器:远程需选好连接后才显示
|
|
886
|
+
isRemote
|
|
887
|
+
? (profileId ? h(DirPicker, { resetKey: profileId, placeholder: t("flow.remoteDir"), listFn: function (p) { return remoteListFn(profileId, p); }, onChoose: chooseRemote, disabled: creating })
|
|
888
|
+
: h("div", { className: "rssh-pick-empty" }, t("common.selectProfile")))
|
|
889
|
+
: h(DirPicker, { resetKey: "local", placeholder: t("flow.localDir"), listFn: function (p) { return localListFn(props.workspaces, p); }, onChoose: chooseLocal })
|
|
890
|
+
)
|
|
891
|
+
);
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
// ======================================================================
|
|
895
|
+
// 设置页小节(连接配置管理)
|
|
896
|
+
// ======================================================================
|
|
897
|
+
function SettingsSection(props) {
|
|
898
|
+
var t = useT();
|
|
899
|
+
var [profiles, setProfiles] = React.useState([]);
|
|
900
|
+
var [workspaces, setWorkspaces] = React.useState([]);
|
|
901
|
+
var [form, setForm] = React.useState({ name: "", host: "", port: "22", user: "", authMethod: "key", keyPath: "", password: "", remoteRoot: "~", proxyJump: "" });
|
|
902
|
+
var [result, setResult] = React.useState(null);
|
|
903
|
+
var [busy, setBusy] = React.useState(false);
|
|
904
|
+
var [importDlg, setImportDlg] = React.useState(null);
|
|
905
|
+
var set = function (k, v) { setForm(function (f) { var n = {}; for (var key in f) n[key] = f[key]; n[k] = v; return n; }); };
|
|
906
|
+
|
|
907
|
+
function reload() {
|
|
908
|
+
api("listProfiles").then(function (r) { if (r && r.ok) setProfiles(r.profiles || []); });
|
|
909
|
+
api("listWorkspaces").then(function (r) { if (r && r.ok) setWorkspaces(r.workspaces || []); });
|
|
910
|
+
}
|
|
911
|
+
React.useEffect(reload, []);
|
|
912
|
+
|
|
913
|
+
function profileName(id) {
|
|
914
|
+
var p = profiles.find(function (x) { return x.id === id; });
|
|
915
|
+
return p ? p.name : id;
|
|
916
|
+
}
|
|
917
|
+
function delWs(wsId) {
|
|
918
|
+
api("deleteWorkspace", { id: wsId }).then(function (r) { if (r && r.ok) setWorkspaces(r.workspaces || []); });
|
|
919
|
+
}
|
|
920
|
+
|
|
921
|
+
function save() {
|
|
922
|
+
setBusy(true); setResult(null);
|
|
923
|
+
api("saveProfile", Object.assign({}, form, { port: parseInt(form.port, 10) || 22 })).then(function (r) {
|
|
924
|
+
setBusy(false);
|
|
925
|
+
if (r && r.ok) {
|
|
926
|
+
setProfiles(r.profiles || []);
|
|
927
|
+
setForm({ name: "", host: "", port: "22", user: "", authMethod: "key", keyPath: "", password: "", remoteRoot: "~", proxyJump: "" });
|
|
928
|
+
setResult({ ok: true, msg: t("settings.saved") });
|
|
929
|
+
} else setResult({ ok: false, msg: (r && r.error) || t("settings.errSave") });
|
|
930
|
+
});
|
|
931
|
+
}
|
|
932
|
+
function test(p) {
|
|
933
|
+
setBusy(true); setResult(null);
|
|
934
|
+
api("testConnection", { id: p.id }).then(function (r) {
|
|
935
|
+
setBusy(false);
|
|
936
|
+
if (r && r.ok) setResult({ ok: true, msg: t("settings.connOk") + "\n" + (r.stdout || "") });
|
|
937
|
+
else setResult({ ok: false, msg: t("settings.connFail") + ((r && r.error) || (r && r.stderr) || "") });
|
|
938
|
+
});
|
|
939
|
+
}
|
|
940
|
+
function del(p) {
|
|
941
|
+
api("deleteProfile", { id: p.id }).then(function (r) { if (r && r.ok) { setProfiles(r.profiles || []); } });
|
|
942
|
+
}
|
|
943
|
+
|
|
944
|
+
function openImport() {
|
|
945
|
+
setImportDlg({ loading: true, hosts: [], selected: {} });
|
|
946
|
+
api("listSshConfigHosts").then(function (r) {
|
|
947
|
+
if (r && r.ok) {
|
|
948
|
+
var hosts = r.hosts || [];
|
|
949
|
+
var sel = {};
|
|
950
|
+
hosts.forEach(function (h) { sel[h.name + "|" + h.hostName] = true; });
|
|
951
|
+
setImportDlg({ loading: false, hosts: hosts, selected: sel });
|
|
952
|
+
} else {
|
|
953
|
+
setImportDlg({ loading: false, hosts: [], selected: {}, error: (r && r.error) || t("settings.noneFound") });
|
|
954
|
+
}
|
|
955
|
+
});
|
|
956
|
+
}
|
|
957
|
+
function toggleImport(key) {
|
|
958
|
+
setImportDlg(function (d) {
|
|
959
|
+
var sel = {}; for (var k in d.selected) sel[k] = d.selected[k];
|
|
960
|
+
sel[key] = !sel[key];
|
|
961
|
+
return Object.assign({}, d, { selected: sel });
|
|
962
|
+
});
|
|
963
|
+
}
|
|
964
|
+
function doImport() {
|
|
965
|
+
if (!importDlg) return;
|
|
966
|
+
var picked = importDlg.hosts.filter(function (h) { return importDlg.selected[h.name + "|" + h.hostName]; });
|
|
967
|
+
setBusy(true);
|
|
968
|
+
var i = 0;
|
|
969
|
+
function next() {
|
|
970
|
+
if (i >= picked.length) {
|
|
971
|
+
setBusy(false); setImportDlg(null);
|
|
972
|
+
setResult({ ok: true, msg: t("settings.imported").replace("{n}", String(picked.length)) });
|
|
973
|
+
reload();
|
|
974
|
+
return;
|
|
975
|
+
}
|
|
976
|
+
var h = picked[i++];
|
|
977
|
+
api("saveProfile", {
|
|
978
|
+
name: h.name, host: h.hostName, port: h.port || 22, user: h.user || "",
|
|
979
|
+
authMethod: "key", keyPath: h.identityFile || "", password: "", remoteRoot: "~", proxyJump: h.proxyJump || ""
|
|
980
|
+
}).then(next);
|
|
981
|
+
}
|
|
982
|
+
next();
|
|
983
|
+
}
|
|
984
|
+
|
|
985
|
+
return h("div", null,
|
|
986
|
+
h("p", { className: "rssh-muted" }, t("settings.desc")),
|
|
987
|
+
profiles.map(function (p) {
|
|
988
|
+
return h("div", { key: p.id, className: "rssh-card" },
|
|
989
|
+
h("div", { className: "rssh-title" }, p.name),
|
|
990
|
+
h("div", { className: "rssh-sub" }, (p.user || "") + "@" + (p.host || "") + ":" + (p.port || 22) + " [" + (p.authMethod || "key") + "]"),
|
|
991
|
+
h("div", { className: "rssh-actions" },
|
|
992
|
+
h("button", { className: "rssh-btn", onClick: function () { test(p); }, disabled: busy }, t("settings.test")),
|
|
993
|
+
h("button", { className: "rssh-btn", onClick: function () { del(p); } }, t("common.delete"))
|
|
994
|
+
)
|
|
995
|
+
);
|
|
996
|
+
}),
|
|
997
|
+
profiles.length === 0 ? h("div", { className: "rssh-empty" }, t("settings.empty")) : null,
|
|
998
|
+
workspaces.length > 0 ? h("div", { className: "rssh-card" },
|
|
999
|
+
h("div", { className: "rssh-title" }, t("settings.workspaces")),
|
|
1000
|
+
workspaces.map(function (ws) {
|
|
1001
|
+
return h("div", { key: ws.id, className: "rssh-card", style: { marginBottom: 6 } },
|
|
1002
|
+
h("div", { className: "rssh-title" }, ws.title || ws.id),
|
|
1003
|
+
h("div", { className: "rssh-sub" }, profileName(ws.profileId) + " → " + (ws.remotePath || "")),
|
|
1004
|
+
h("div", { className: "rssh-actions" },
|
|
1005
|
+
h("button", { className: "rssh-btn", onClick: function () { delWs(ws.id); } }, t("common.delete"))
|
|
1006
|
+
)
|
|
1007
|
+
);
|
|
1008
|
+
})
|
|
1009
|
+
) : null,
|
|
1010
|
+
h("div", { className: "rssh-card" },
|
|
1011
|
+
h("div", { className: "rssh-title" }, t("settings.add")),
|
|
1012
|
+
Field({ label: t("settings.name"), value: form.name, onChange: function (v) { set("name", v); }, placeholder: t("settings.namePlaceholder") }),
|
|
1013
|
+
Field({ label: t("settings.host"), value: form.host, onChange: function (v) { set("host", v); }, placeholder: t("settings.hostPlaceholder") }),
|
|
1014
|
+
Field({ label: t("settings.port"), value: form.port, onChange: function (v) { set("port", v); } }),
|
|
1015
|
+
Field({ label: t("settings.user"), value: form.user, onChange: function (v) { set("user", v); }, placeholder: t("settings.userPlaceholder") }),
|
|
1016
|
+
h("div", { className: "rssh-row" },
|
|
1017
|
+
h("span", { className: "rssh-label" }, t("settings.auth")),
|
|
1018
|
+
h("select", { className: "rssh-select", value: form.authMethod, onChange: function (e) { set("authMethod", e.target.value); } },
|
|
1019
|
+
h("option", { value: "key" }, t("settings.key")),
|
|
1020
|
+
h("option", { value: "password" }, t("settings.password"))
|
|
1021
|
+
)
|
|
1022
|
+
),
|
|
1023
|
+
form.authMethod === "key" ? Field({ label: t("settings.keyPath"), value: form.keyPath, onChange: function (v) { set("keyPath", v); }, placeholder: t("settings.keyPathPlaceholder") }) : null,
|
|
1024
|
+
form.authMethod === "password" ? Field({ label: t("settings.passwordField"), type: "password", value: form.password, onChange: function (v) { set("password", v); } }) : null,
|
|
1025
|
+
Field({ label: t("settings.remoteRoot"), value: form.remoteRoot, onChange: function (v) { set("remoteRoot", v); }, placeholder: "~/project" }),
|
|
1026
|
+
Field({ label: t("settings.proxyJump"), value: form.proxyJump, onChange: function (v) { set("proxyJump", v); }, placeholder: "bastion.example.com" }),
|
|
1027
|
+
h("div", { className: "rssh-actions" },
|
|
1028
|
+
h("button", { className: "rssh-btn", onClick: save, disabled: busy }, busy ? t("common.processing") : t("common.save")),
|
|
1029
|
+
h("button", { className: "rssh-btn", onClick: openImport, disabled: busy }, t("settings.importSshConfig"))
|
|
1030
|
+
)
|
|
1031
|
+
),
|
|
1032
|
+
result ? h("div", { className: result.ok ? "rssh-ok" : "rssh-err", style: { whiteSpace: "pre-wrap", marginTop: 6 } }, result.msg) : null,
|
|
1033
|
+
importDlg ? h(Modal, { open: true, onClose: function () { if (!busy) setImportDlg(null); }, title: t("settings.importTitle"), closeLabel: t("common.close") },
|
|
1034
|
+
importDlg.loading ? h("div", { className: "rssh-muted" }, t("common.loading")) :
|
|
1035
|
+
importDlg.error ? h("div", { className: "rssh-err" }, importDlg.error) :
|
|
1036
|
+
(importDlg.hosts.length === 0 ? h("div", { className: "rssh-muted" }, t("settings.noneFound")) :
|
|
1037
|
+
h("div", null,
|
|
1038
|
+
importDlg.hosts.map(function (hh) {
|
|
1039
|
+
var key = hh.name + "|" + hh.hostName;
|
|
1040
|
+
return h("label", { key: key, className: "rssh-tree-item", style: { cursor: "pointer" } },
|
|
1041
|
+
h("input", { type: "checkbox", checked: !!importDlg.selected[key], onChange: function () { toggleImport(key); } }),
|
|
1042
|
+
h("span", { className: "rssh-tree-dir" }, hh.name),
|
|
1043
|
+
h("span", { className: "rssh-sub" }, (hh.user || "?") + "@" + hh.hostName + ":" + (hh.port || 22) + (hh.proxyJump ? " → " + hh.proxyJump : ""))
|
|
1044
|
+
);
|
|
1045
|
+
}),
|
|
1046
|
+
h("div", { className: "rssh-actions", style: { marginTop: 8 } },
|
|
1047
|
+
h("button", { className: "rssh-btn", onClick: doImport, disabled: busy }, busy ? t("common.processing") : t("common.save")),
|
|
1048
|
+
h("button", { className: "rssh-btn", onClick: function () { setImportDlg(null); }, disabled: busy }, t("common.close"))
|
|
1049
|
+
)
|
|
1050
|
+
)
|
|
1051
|
+
)
|
|
1052
|
+
) : null
|
|
1053
|
+
);
|
|
1054
|
+
}
|
|
1055
|
+
|
|
1056
|
+
// ======================================================================
|
|
1057
|
+
// apply
|
|
1058
|
+
// ======================================================================
|
|
1059
|
+
function apply(ctx) {
|
|
1060
|
+
var betterSidebar = ctx.betterSidebar;
|
|
1061
|
+
var slots = ctx.slots;
|
|
1062
|
+
var locale = ctx.locale;
|
|
1063
|
+
|
|
1064
|
+
// 兼容 DSH Desktop ≥2.0.4:shell 不再把客户端模块系统挂到
|
|
1065
|
+
// globalThis.__DSH_MODULES__,而 dsh-better-sidebar 0.15+ 的懒加载
|
|
1066
|
+
// chunk(editor 等)会回退到该全局——缺失时侧边栏面板打不开、报
|
|
1067
|
+
// "chunk ... client module system unavailable"。这里用内核提供的
|
|
1068
|
+
// modules 服务补挂该全局;better-sidebar 已注入则保持原值。
|
|
1069
|
+
try {
|
|
1070
|
+
if (typeof globalThis !== "undefined") {
|
|
1071
|
+
var fallbackModules = null;
|
|
1072
|
+
try { if (ctx.modules && typeof ctx.modules.import === "function") fallbackModules = ctx.modules; } catch (e) {}
|
|
1073
|
+
if (!fallbackModules && ctx.get && typeof ctx.get === "function") {
|
|
1074
|
+
try { fallbackModules = ctx.get("modules"); } catch (e) { fallbackModules = null; }
|
|
1075
|
+
}
|
|
1076
|
+
// 定期兜底:better-sidebar 若把本插件应用时间之后的模块系统清掉,也补回。
|
|
1077
|
+
if (globalThis.__DSH_MODULES__ === undefined && fallbackModules === null) {
|
|
1078
|
+
var waitModules = 0;
|
|
1079
|
+
var waitTimer = setInterval(function () {
|
|
1080
|
+
waitModules += 1;
|
|
1081
|
+
try {
|
|
1082
|
+
var ms = (ctx.get && typeof ctx.get === "function") ? ctx.get("modules") : null;
|
|
1083
|
+
if (ms && typeof ms.import === "function") {
|
|
1084
|
+
globalThis.__DSH_MODULES__ = ms;
|
|
1085
|
+
clearInterval(waitTimer);
|
|
1086
|
+
}
|
|
1087
|
+
} catch (e) {}
|
|
1088
|
+
if (globalThis.__DSH_MODULES__ !== undefined || waitModules > 40) clearInterval(waitTimer);
|
|
1089
|
+
}, 250);
|
|
1090
|
+
}
|
|
1091
|
+
if (!globalThis.__DSH_MODULES__ && fallbackModules && typeof fallbackModules.import === "function") {
|
|
1092
|
+
globalThis.__DSH_MODULES__ = fallbackModules;
|
|
1093
|
+
}
|
|
1094
|
+
}
|
|
1095
|
+
} catch (e) {}
|
|
1096
|
+
|
|
1097
|
+
// 注册中英文词典并绑定当前语言;locale 切换时 i18n.subscribe 触发组件重渲染。
|
|
1098
|
+
if (locale) {
|
|
1099
|
+
ctx.effect(function () { return locale.register(NS, { zh: zh, en: en }); }, "dsh-remote-ssh: dictionaries");
|
|
1100
|
+
i18n.t = locale.bind(NS);
|
|
1101
|
+
i18n.subscribe = function (cb) { return locale.subscribe(cb); };
|
|
1102
|
+
i18n.getSnapshot = function () { return locale.getSnapshot(); };
|
|
1103
|
+
}
|
|
1104
|
+
|
|
1105
|
+
ctx.effect(function () {
|
|
1106
|
+
var el = document.createElement("style");
|
|
1107
|
+
el.textContent = CSS;
|
|
1108
|
+
document.head.appendChild(el);
|
|
1109
|
+
return function () { if (el.parentNode) el.parentNode.removeChild(el); };
|
|
1110
|
+
});
|
|
1111
|
+
|
|
1112
|
+
// ---- 去掉设置导航「远程连接」条目左侧的默认齿轮图标 ----
|
|
1113
|
+
// 壳层对未知 settings.section 一律渲染齿轮图标(IconSettingsOutline16 兜底),
|
|
1114
|
+
// 且导航 CSS 类名是构建期哈希(VOzbGW_*、zOa2rq_* 每版都会变),CSS 选择器跨版本
|
|
1115
|
+
// 不可靠。这里改为按条目文本(settings.nav)定位设置导航按钮,隐藏其 svg 图标;
|
|
1116
|
+
// 设置面板按需挂载、语言会切换,用 MutationObserver + 语言订阅自动重跑。
|
|
1117
|
+
if (slots) {
|
|
1118
|
+
function checkCell(btn) {
|
|
1119
|
+
var label = i18n.t("settings.nav");
|
|
1120
|
+
var text = (btn.textContent || "").trim();
|
|
1121
|
+
if (text !== label.trim() || text === "") return false;
|
|
1122
|
+
var icon = btn.querySelector("svg");
|
|
1123
|
+
if (icon) { icon.style.display = "none"; return true; }
|
|
1124
|
+
return false;
|
|
1125
|
+
}
|
|
1126
|
+
function checkCellIn(root) {
|
|
1127
|
+
try {
|
|
1128
|
+
if (root.tagName && String(root.tagName).toLowerCase() === "button") checkCell(root);
|
|
1129
|
+
var cells = root.querySelectorAll("button");
|
|
1130
|
+
for (var i = 0; i < cells.length; i++) checkCell(cells[i]);
|
|
1131
|
+
} catch (e) {}
|
|
1132
|
+
}
|
|
1133
|
+
function hideRemoteGearIcon() {
|
|
1134
|
+
try {
|
|
1135
|
+
var buttons = document.querySelectorAll("button");
|
|
1136
|
+
for (var i = 0; i < buttons.length; i++) checkCell(buttons[i]);
|
|
1137
|
+
} catch (e) {}
|
|
1138
|
+
}
|
|
1139
|
+
// ---- 远程工作区图标:单个「地球角标文件夹」SVG 替换壳层默认纯文件夹图标 ----
|
|
1140
|
+
// 壳层工作区行的文件夹图标是固定原语(IconFolderOpen16/Close16),无 per-workspace
|
|
1141
|
+
// 图标 API;旧方案在标题里塞 🌐 emoji 造成「文件夹 + 地球」双重标识。现按标题文本
|
|
1142
|
+
// 定位远程工作区行,找到行首 slot.folder 槽位(标题的前兄弟),向其中的 svg
|
|
1143
|
+
// 追加地球经纬线组 —— 文件夹本体保持壳层默认原语几何,零改写零对账风险。
|
|
1144
|
+
var remoteTitles = new Set();
|
|
1145
|
+
var watchedTitles = []; // 已匹配的标题元素:定时幂等复检,抵消 React 重渲染对图标的还原
|
|
1146
|
+
function refreshRemoteTitles() {
|
|
1147
|
+
api("listWorkspaces").then(function (r) {
|
|
1148
|
+
if (!(r && r.ok && Array.isArray(r.workspaces))) return;
|
|
1149
|
+
var next = new Set();
|
|
1150
|
+
for (var i = 0; i < r.workspaces.length; i++) {
|
|
1151
|
+
var t = String(r.workspaces[i].title || "").trim();
|
|
1152
|
+
if (t) { next.add(t); next.add("🌐 " + t); } // 兼容自愈前的旧前缀标题
|
|
1153
|
+
}
|
|
1154
|
+
remoteTitles = next;
|
|
1155
|
+
swapWorkspaceIconsIn(document.body); // 标题集就绪后补一次全量替换
|
|
1156
|
+
}).catch(function () {});
|
|
1157
|
+
}
|
|
1158
|
+
/** 地球经纬线组(右下角、无圆盘底色):线为白色、其余全透明;文件夹本体完全
|
|
1159
|
+
* 保留壳层默认原语(IconFolderOpen16/Close16 原始路径与几何,零改写),
|
|
1160
|
+
* 仅向 svg 追加本组 —— appendChild 不触碰 React 管理的子节点,零对账风险。 */
|
|
1161
|
+
function globeGridElement() {
|
|
1162
|
+
var NS = "http://www.w3.org/2000/svg";
|
|
1163
|
+
var g = document.createElementNS(NS, "g");
|
|
1164
|
+
g.setAttribute("data-rssh-globe", "1");
|
|
1165
|
+
g.setAttribute("fill", "none");
|
|
1166
|
+
g.setAttribute("stroke", "#ffffff");
|
|
1167
|
+
g.setAttribute("stroke-width", "0.9");
|
|
1168
|
+
function add(name, attrs) {
|
|
1169
|
+
var n = document.createElementNS(NS, name);
|
|
1170
|
+
for (var k in attrs) n.setAttribute(k, attrs[k]);
|
|
1171
|
+
g.appendChild(n);
|
|
1172
|
+
}
|
|
1173
|
+
add("circle", { cx: "11.6", cy: "11.9", r: "3.4" }); // 地球轮廓
|
|
1174
|
+
add("ellipse", { cx: "11.6", cy: "11.9", rx: "1.6", ry: "3.4" }); // 经线
|
|
1175
|
+
add("path", { d: "M8.2 11.9h6.8" }); // 赤道(纬线)
|
|
1176
|
+
add("path", { d: "M8.7 9.6c0.9 0.65 1.85 0.97 2.9 0.97s2-0.32 2.9-0.97" }); // 北纬弧
|
|
1177
|
+
add("path", { d: "M8.7 14.2c0.9-0.65 1.85-0.97 2.9-0.97s2 0.32 2.9 0.97" }); // 南纬弧
|
|
1178
|
+
return g;
|
|
1179
|
+
}
|
|
1180
|
+
/** 幂等应用:React 还原/重建子节点会清掉本组 —— 以 [data-rssh-globe] 子节点
|
|
1181
|
+
* 存在性为状态判据(覆盖属性级还原与节点重建),缺席即追加。 */
|
|
1182
|
+
function applyFolderGlobe(slotSvg) {
|
|
1183
|
+
if (slotSvg.querySelector("[data-rssh-globe]")) return;
|
|
1184
|
+
slotSvg.setAttribute("data-rssh-folder-globe", "1");
|
|
1185
|
+
slotSvg.appendChild(globeGridElement());
|
|
1186
|
+
}
|
|
1187
|
+
/** 对单个标题元素做行首槽位修复:projectRow > [slot.folder(svg), …, projectText > title],
|
|
1188
|
+
* 文件夹 svg 在标题的前兄弟槽位 —— 向上 ≤4 层取每层容器第一个子元素,首个含 svg 即是。 */
|
|
1189
|
+
function fixRowIconFor(el) {
|
|
1190
|
+
var holder = el;
|
|
1191
|
+
for (var hop = 0; hop < 4 && holder && holder.parentElement; hop++) {
|
|
1192
|
+
var row = holder.parentElement;
|
|
1193
|
+
var first = row.children && row.children[0];
|
|
1194
|
+
if (first && first.querySelector && first.querySelector("svg")) {
|
|
1195
|
+
applyFolderGlobe(first.querySelector("svg"));
|
|
1196
|
+
return true;
|
|
1197
|
+
}
|
|
1198
|
+
holder = row;
|
|
1199
|
+
}
|
|
1200
|
+
return false;
|
|
1201
|
+
}
|
|
1202
|
+
function watchTitle(el) {
|
|
1203
|
+
for (var i = 0; i < watchedTitles.length; i++) { if (watchedTitles[i] === el) return; }
|
|
1204
|
+
watchedTitles.push(el);
|
|
1205
|
+
fixRowIconFor(el);
|
|
1206
|
+
}
|
|
1207
|
+
function recheckWatched() {
|
|
1208
|
+
for (var i = watchedTitles.length - 1; i >= 0; i--) {
|
|
1209
|
+
var el = watchedTitles[i];
|
|
1210
|
+
if (!el || !el.isConnected) { watchedTitles.splice(i, 1); continue; }
|
|
1211
|
+
fixRowIconFor(el);
|
|
1212
|
+
}
|
|
1213
|
+
}
|
|
1214
|
+
function swapWorkspaceIconsIn(root) {
|
|
1215
|
+
if (!remoteTitles.size) return;
|
|
1216
|
+
try {
|
|
1217
|
+
var nodes = root.querySelectorAll("span,div,button,a,p");
|
|
1218
|
+
for (var i = 0; i < nodes.length; i++) {
|
|
1219
|
+
var el = nodes[i];
|
|
1220
|
+
var t = (el.textContent || "").trim();
|
|
1221
|
+
if (!t || !remoteTitles.has(t) || el.childElementCount > 2) continue;
|
|
1222
|
+
watchTitle(el);
|
|
1223
|
+
}
|
|
1224
|
+
} catch (e) {}
|
|
1225
|
+
}
|
|
1226
|
+
ctx.effect(function () {
|
|
1227
|
+
if (typeof document === "undefined") return;
|
|
1228
|
+
hideRemoteGearIcon();
|
|
1229
|
+
refreshRemoteTitles();
|
|
1230
|
+
var titleTimer = setInterval(refreshRemoteTitles, 30000);
|
|
1231
|
+
var recheckTimer = setInterval(recheckWatched, 1000); // 抵消 React 属性还原
|
|
1232
|
+
var timer = null;
|
|
1233
|
+
var pendingRoots = null;
|
|
1234
|
+
var schedule = function (root) {
|
|
1235
|
+
if (root && root.nodeType === 1) (pendingRoots || (pendingRoots = [])).push(root);
|
|
1236
|
+
if (timer) return;
|
|
1237
|
+
timer = setTimeout(function () {
|
|
1238
|
+
timer = null;
|
|
1239
|
+
var roots = pendingRoots;
|
|
1240
|
+
pendingRoots = null;
|
|
1241
|
+
if (document.hidden) return; // 后台页签零开销
|
|
1242
|
+
if (roots && roots.length) {
|
|
1243
|
+
// 只扫新增子树:流式聊天期间不做全文档扫描(稳态成本≈0)
|
|
1244
|
+
for (var i = 0; i < roots.length; i++) {
|
|
1245
|
+
if (roots[i].isConnected) { checkCellIn(roots[i]); swapWorkspaceIconsIn(roots[i]); }
|
|
1246
|
+
}
|
|
1247
|
+
} else {
|
|
1248
|
+
hideRemoteGearIcon(); // 语言切换等无根场景全量扫一次
|
|
1249
|
+
swapWorkspaceIconsIn(document.body);
|
|
1250
|
+
}
|
|
1251
|
+
}, 500);
|
|
1252
|
+
};
|
|
1253
|
+
var mo = new MutationObserver(function (muts) {
|
|
1254
|
+
for (var i = 0; i < muts.length; i++) {
|
|
1255
|
+
var added = muts[i].addedNodes;
|
|
1256
|
+
for (var j = 0; j < added.length; j++) schedule(added[j]);
|
|
1257
|
+
}
|
|
1258
|
+
});
|
|
1259
|
+
mo.observe(document.body, { childList: true, subtree: true });
|
|
1260
|
+
var off = i18n.subscribe(function () { schedule(null); });
|
|
1261
|
+
return function () {
|
|
1262
|
+
mo.disconnect();
|
|
1263
|
+
if (off) off();
|
|
1264
|
+
if (timer) clearTimeout(timer);
|
|
1265
|
+
clearInterval(titleTimer);
|
|
1266
|
+
clearInterval(recheckTimer);
|
|
1267
|
+
};
|
|
1268
|
+
}, "dsh-remote-ssh: settings gear hide + remote workspace folder-globe icon");
|
|
1269
|
+
}
|
|
1270
|
+
|
|
1271
|
+
// better-sidebar 0.15+ 的文件上传 UI 把原始字节流 POST 到 /sidebar/upload。
|
|
1272
|
+
// 远程工作区会话的文件树显示的是本地镜像目录,better-sidebar 会把上传写进
|
|
1273
|
+
// 本地镜像而不会到达远程。这里包装 fetch:目标目录位于某个远程工作区镜像
|
|
1274
|
+
// 目录内时,把该上传请求重定向到 /remote-ssh/upload(走 SSH 上传到远端)。
|
|
1275
|
+
ctx.effect(function () {
|
|
1276
|
+
if (typeof window === "undefined" || typeof window.fetch !== "function") return;
|
|
1277
|
+
var nativeFetch = window.fetch.bind(window);
|
|
1278
|
+
var mirrors = [];
|
|
1279
|
+
function refreshMirrors() {
|
|
1280
|
+
api("listWorkspaces").then(function (r) {
|
|
1281
|
+
if (r && r.ok && Array.isArray(r.workspaces)) {
|
|
1282
|
+
mirrors = r.workspaces.map(function (w) {
|
|
1283
|
+
return String(w && w.mirrorPath || "").replace(/\\/g, "/").replace(/\/+$/g, "").toLowerCase();
|
|
1284
|
+
}).filter(Boolean);
|
|
1285
|
+
}
|
|
1286
|
+
}).catch(function () {});
|
|
1287
|
+
}
|
|
1288
|
+
refreshMirrors();
|
|
1289
|
+
var timer = setInterval(refreshMirrors, 30000);
|
|
1290
|
+
function patchedFetch(input, init) {
|
|
1291
|
+
var raw = typeof input === "string" ? input : (input && input.url);
|
|
1292
|
+
if (typeof raw === "string") {
|
|
1293
|
+
try {
|
|
1294
|
+
var u = new URL(raw, window.location.origin);
|
|
1295
|
+
if (u.pathname === "/sidebar/upload") {
|
|
1296
|
+
var dir = (u.searchParams.get("dir") || "").replace(/\\/g, "/").replace(/\/+$/g, "").toLowerCase();
|
|
1297
|
+
var cwd = (u.searchParams.get("cwd") || "").replace(/\\/g, "/").replace(/\/+$/g, "").toLowerCase();
|
|
1298
|
+
for (var i = 0; i < mirrors.length; i++) {
|
|
1299
|
+
var m = mirrors[i];
|
|
1300
|
+
var hit = (dir && (dir === m || dir.indexOf(m + "/") === 0)) || (cwd && (cwd === m || cwd.indexOf(m + "/") === 0));
|
|
1301
|
+
if (hit) {
|
|
1302
|
+
var target = new URL("/remote-ssh/upload" + u.search, window.location.origin);
|
|
1303
|
+
return nativeFetch(target.toString(), init);
|
|
1304
|
+
}
|
|
1305
|
+
}
|
|
1306
|
+
}
|
|
1307
|
+
} catch (e) {}
|
|
1308
|
+
}
|
|
1309
|
+
return nativeFetch(input, init);
|
|
1310
|
+
}
|
|
1311
|
+
window.fetch = patchedFetch;
|
|
1312
|
+
return function () {
|
|
1313
|
+
clearInterval(timer);
|
|
1314
|
+
if (window.fetch === patchedFetch) window.fetch = nativeFetch;
|
|
1315
|
+
};
|
|
1316
|
+
}, "dsh-remote-ssh: redirect remote-workspace uploads");
|
|
1317
|
+
|
|
1318
|
+
if (betterSidebar) {
|
|
1319
|
+
// 远程文件编辑器页签(隐藏页签,仅由代码打开,每个远程文件单开一栏)。
|
|
1320
|
+
// remssh:files 和 remssh:term 已移除:远程文件由内置「文件」页签直接 SSH 读写,
|
|
1321
|
+
// 远程终端由内置「终端」页签提供(shell wrapper 自动 SSH)。
|
|
1322
|
+
ctx.effect(function () {
|
|
1323
|
+
return betterSidebar.registerTab({
|
|
1324
|
+
id: "remssh:editor",
|
|
1325
|
+
title: function () { return i18n.t("tabs.files"); },
|
|
1326
|
+
hidden: true,
|
|
1327
|
+
dedupeKey: function (tab) { return (tab && tab.meta && tab.meta.profileId || "") + "\u0000" + (tab && tab.path || ""); },
|
|
1328
|
+
icon: function () { return h("span", null, "📄"); },
|
|
1329
|
+
component: function (props) {
|
|
1330
|
+
return h(RemoteEditor, { tab: props && props.tab, ctx: props && props.ctx, scope: props && props.scope });
|
|
1331
|
+
}
|
|
1332
|
+
});
|
|
1333
|
+
});
|
|
1334
|
+
}
|
|
1335
|
+
|
|
1336
|
+
if (slots) {
|
|
1337
|
+
slots.inject("settings.section", function () {
|
|
1338
|
+
return slots.register({
|
|
1339
|
+
name: "settings.section",
|
|
1340
|
+
id: "remote-ssh",
|
|
1341
|
+
order: 200,
|
|
1342
|
+
label: function () { return i18n.t("settings.nav"); },
|
|
1343
|
+
locale: NS
|
|
1344
|
+
}, SettingsSection);
|
|
1345
|
+
});
|
|
1346
|
+
|
|
1347
|
+
// 接管原生「添加工作区」流程:用明显更低的优先级覆盖内置的本地目录选择器,
|
|
1348
|
+
// 弹窗内提供「本地目录(DSH browse)/ 远程目录(SSH)」两种浏览。
|
|
1349
|
+
var flowInject = function () { return { workspaces: ctx.get("workspaces") }; };
|
|
1350
|
+
ctx.effect(function () {
|
|
1351
|
+
return slots.inject("conversation.hero.workspace.directoryFlow", function () {
|
|
1352
|
+
return slots.register({ name: "conversation.hero.workspace.directoryFlow", priority: -100, inject: flowInject }, RemoteWorkspaceFlow);
|
|
1353
|
+
});
|
|
1354
|
+
});
|
|
1355
|
+
ctx.effect(function () {
|
|
1356
|
+
return slots.inject("sidebar.workspaces.directoryFlow", function () {
|
|
1357
|
+
return slots.register({ name: "sidebar.workspaces.directoryFlow", priority: -100, inject: flowInject }, RemoteWorkspaceFlow);
|
|
1358
|
+
});
|
|
1359
|
+
});
|
|
1360
|
+
}
|
|
1361
|
+
}
|
|
1362
|
+
|
|
1363
|
+
exports.apply = apply;
|
|
1364
|
+
exports.inject = ["betterSidebar", "slots", "locale"];
|
|
1365
|
+
return module.exports;
|
|
1366
|
+
}
|
|
1367
|
+
});
|