@zhangfengshun/dsh-remote-ssh 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +68 -0
- package/LICENSE +21 -0
- package/README.md +158 -0
- package/cordis.patch.yml +7 -0
- package/lib/client.js +778 -0
- package/lib/index.js +691 -0
- package/package.json +76 -0
package/lib/client.js
ADDED
|
@@ -0,0 +1,778 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dsh-remote-ssh — Client 半边(静态 DSH Web 插件,浏览器 bundle)
|
|
3
|
+
*
|
|
4
|
+
* 通过 dsh-better-sidebar 的 `ctx.betterSidebar.registerTab` 注册两个侧边栏页签:
|
|
5
|
+
* - remssh:files 远程文件(文件浏览器 + 工作区选择)
|
|
6
|
+
* - remssh:term 远程终端(ssh -tt 管道终端)
|
|
7
|
+
* 并在 DSH 设置页注册「远程连接」小节(settings.section)管理连接配置;
|
|
8
|
+
* 同时接管原生「添加工作区」流程(directoryFlow 槽位,优先级 -1 覆盖内置选择器),
|
|
9
|
+
* 提供「本地目录 / 远程目录」两选:远程分支创建本地镜像目录并注册为原生工作区。
|
|
10
|
+
*/
|
|
11
|
+
window.__ModuleLoader__.load({
|
|
12
|
+
id: "dsh-remote-ssh",
|
|
13
|
+
factory: (require) => {
|
|
14
|
+
var module = { exports: {} };
|
|
15
|
+
var exports = module.exports;
|
|
16
|
+
var React = require("react");
|
|
17
|
+
var primitives = require("@deepseek-ai/dsh-client-ui-primitives");
|
|
18
|
+
var Modal = primitives.Modal;
|
|
19
|
+
|
|
20
|
+
function h(tag, props) {
|
|
21
|
+
var args = [tag, props];
|
|
22
|
+
for (var i = 2; i < arguments.length; i++) args.push(arguments[i]);
|
|
23
|
+
return React.createElement.apply(React, args);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
var CSS =
|
|
27
|
+
".rssh-row{margin:6px 0}.rssh-label{color:#9aa0a6;font-size:11px;display:block;margin-bottom:2px}" +
|
|
28
|
+
".rssh-input{width:100%;box-sizing:border-box;background:#121316;color:#eee;border:1px solid #444;border-radius:4px;padding:5px 7px;margin:3px 0;font-size:12px}" +
|
|
29
|
+
".rssh-select{width:100%;box-sizing:border-box;background:#121316;color:#eee;border:1px solid #444;border-radius:4px;padding:5px 7px;margin:3px 0;font-size:12px}" +
|
|
30
|
+
".rssh-btn{background:#3a3d41;color:#eee;border:1px solid #555;border-radius:4px;padding:4px 9px;cursor:pointer;font-size:12px}" +
|
|
31
|
+
".rssh-btn:hover{background:#45494e}.rssh-btn:disabled{opacity:.5;cursor:default}" +
|
|
32
|
+
".rssh-card{padding:8px 10px;border:1px solid #333;border-radius:6px;margin:8px 0;background:#26282c}" +
|
|
33
|
+
".rssh-title{font-weight:600;font-size:13px}.rssh-sub{color:#9aa0a6;font-size:11px}" +
|
|
34
|
+
".rssh-actions{margin-top:6px;display:flex;gap:5px;flex-wrap:wrap}" +
|
|
35
|
+
".rssh-tree-item{display:flex;align-items:center;gap:5px;padding:3px 5px;cursor:pointer;border-radius:3px}" +
|
|
36
|
+
".rssh-tree-item:hover{background:#2f3237}" +
|
|
37
|
+
".rssh-tree-dir{color:#7ab0ff}.rssh-tree-file{color:#d7d7d7}" +
|
|
38
|
+
".rssh-tree-size{color:#6a7076;font-size:11px;margin-left:auto}" +
|
|
39
|
+
".rssh-pathbar{display:flex;gap:4px;align-items:center;margin:4px 0}" +
|
|
40
|
+
".rssh-pathbar .rssh-input{flex:1;margin:0}" +
|
|
41
|
+
".rssh-content{white-space:pre-wrap;font-family:Consolas,monospace;font-size:12px;background:#121316;padding:8px;border-radius:4px;margin-top:6px;max-height:400px;overflow:auto}" +
|
|
42
|
+
".rssh-terminal{background:#0d0e10;border-radius:4px;padding:6px;margin-top:8px;font-family:Consolas,monospace;font-size:12px}" +
|
|
43
|
+
".rssh-terminal-out{white-space:pre-wrap;max-height:320px;overflow:auto;min-height:120px;line-height:1.4}" +
|
|
44
|
+
".rssh-terminal-bar{display:flex;gap:5px;align-items:center;margin-bottom:4px}" +
|
|
45
|
+
".rssh-terminal-input-row{display:flex;gap:4px;margin-top:5px}.rssh-terminal-input-row .rssh-input{flex:1}" +
|
|
46
|
+
".rssh-ok{color:#4ade80}.rssh-err{color:#f87171}.rssh-muted{color:#9aa0a6;font-size:12px;padding:6px 0}" +
|
|
47
|
+
".rssh-empty{padding:12px;text-align:center;color:#9aa0a6;font-size:12px}" +
|
|
48
|
+
".rssh-flow-dialog{width:690px!important;max-width:94vw!important;min-width:460px;resize:horizontal;overflow:auto!important}" +
|
|
49
|
+
// 设置面板导航:隐藏「远程连接」栏目左侧壳层硬编码的齿轮图标(本栏目 order=200 位于末尾)。
|
|
50
|
+
".VOzbGW_navList .VOzbGW_navCell:last-child .VOzbGW_navIcon{display:none}";
|
|
51
|
+
|
|
52
|
+
// ---- 国际化(跟随 DSH 设置中的语言自动切换)----
|
|
53
|
+
var NS = "dsh-remote-ssh";
|
|
54
|
+
var zh = {
|
|
55
|
+
"tabs.files": "远程文件",
|
|
56
|
+
"tabs.term": "远程终端",
|
|
57
|
+
"settings.nav": "🖥️ 远程连接",
|
|
58
|
+
"settings.desc": "管理 Remote-SSH 的远程连接配置(SSH 密钥或密码认证)。",
|
|
59
|
+
"settings.empty": "还没有连接配置。请在下方添加。",
|
|
60
|
+
"settings.add": "添加连接",
|
|
61
|
+
"settings.name": "名称",
|
|
62
|
+
"settings.namePlaceholder": "如:我的超算",
|
|
63
|
+
"settings.host": "主机 host",
|
|
64
|
+
"settings.hostPlaceholder": "login.example.com",
|
|
65
|
+
"settings.port": "端口 port",
|
|
66
|
+
"settings.user": "用户名 user",
|
|
67
|
+
"settings.userPlaceholder": "your-username",
|
|
68
|
+
"settings.auth": "认证方式",
|
|
69
|
+
"settings.key": "密钥 (推荐)",
|
|
70
|
+
"settings.password": "密码 (需 sshpass)",
|
|
71
|
+
"settings.keyPath": "私钥路径 keyPath",
|
|
72
|
+
"settings.keyPathPlaceholder": "~/.ssh/id_rsa",
|
|
73
|
+
"settings.passwordField": "密码 password",
|
|
74
|
+
"settings.remoteRoot": "远程根目录 remoteRoot",
|
|
75
|
+
"settings.test": "测试连接",
|
|
76
|
+
"settings.saved": "已保存",
|
|
77
|
+
"settings.connOk": "连接成功",
|
|
78
|
+
"settings.connFail": "连接失败: ",
|
|
79
|
+
"settings.errSave": "保存失败",
|
|
80
|
+
"common.selectProfile": "— 选择连接 —",
|
|
81
|
+
"common.profile": "连接配置",
|
|
82
|
+
"common.save": "保存",
|
|
83
|
+
"common.open": "打开",
|
|
84
|
+
"common.close": "关闭",
|
|
85
|
+
"common.delete": "删除",
|
|
86
|
+
"common.up": "⬆ 上级",
|
|
87
|
+
"common.loading": "加载中…",
|
|
88
|
+
"common.processing": "处理中…",
|
|
89
|
+
"common.errList": "读取目录失败",
|
|
90
|
+
"files.workspace": "远程工作区",
|
|
91
|
+
"files.selectWorkspace": "— 选择工作区 —",
|
|
92
|
+
"files.orProfile": "或直接选连接配置",
|
|
93
|
+
"files.defaultDir": "默认目录(打开该工作区时进入的远程目录)",
|
|
94
|
+
"files.reading": "读取中…",
|
|
95
|
+
"files.errRead": "读取失败",
|
|
96
|
+
"files.errSave": "保存失败",
|
|
97
|
+
"files.errSaveDefaultDir": "保存默认目录失败",
|
|
98
|
+
"term.title": "终端 {id}",
|
|
99
|
+
"term.exited": " (已退出)",
|
|
100
|
+
"term.connecting": "正在连接…",
|
|
101
|
+
"term.noOutput": "(无输出)",
|
|
102
|
+
"term.inputPlaceholder": "输入命令后回车…",
|
|
103
|
+
"term.send": "发送",
|
|
104
|
+
"term.needProfile": "请先选择连接配置",
|
|
105
|
+
"term.newTerminal": "+ 新建终端",
|
|
106
|
+
"term.emptyHint": "从上面的连接配置新建一个终端,直接操作远程环境。",
|
|
107
|
+
"term.errSpawn": "启动终端失败",
|
|
108
|
+
"picker.dir": "目录",
|
|
109
|
+
"picker.choose": "选择此目录",
|
|
110
|
+
"flow.localTitle": "选择本地目录",
|
|
111
|
+
"flow.remoteTitle": "选择远程目录",
|
|
112
|
+
"flow.localDir": "本地目录",
|
|
113
|
+
"flow.remoteDir": "远程目录",
|
|
114
|
+
"flow.chooseRemote": "选择远程目录…",
|
|
115
|
+
"flow.backLocal": "← 选择本地目录",
|
|
116
|
+
"flow.errCreate": "创建远程工作区失败"
|
|
117
|
+
};
|
|
118
|
+
var en = {
|
|
119
|
+
"tabs.files": "Remote Files",
|
|
120
|
+
"tabs.term": "Remote Terminal",
|
|
121
|
+
"settings.nav": "🖥️ Remote Connections",
|
|
122
|
+
"settings.desc": "Manage Remote-SSH connection profiles (SSH key or password authentication).",
|
|
123
|
+
"settings.empty": "No connection profiles yet. Add one below.",
|
|
124
|
+
"settings.add": "Add Profile",
|
|
125
|
+
"settings.name": "Name",
|
|
126
|
+
"settings.namePlaceholder": "e.g. My HPC",
|
|
127
|
+
"settings.host": "Host",
|
|
128
|
+
"settings.hostPlaceholder": "login.example.com",
|
|
129
|
+
"settings.port": "Port",
|
|
130
|
+
"settings.user": "User",
|
|
131
|
+
"settings.userPlaceholder": "your-username",
|
|
132
|
+
"settings.auth": "Authentication",
|
|
133
|
+
"settings.key": "Key (recommended)",
|
|
134
|
+
"settings.password": "Password (requires sshpass)",
|
|
135
|
+
"settings.keyPath": "Private key path (keyPath)",
|
|
136
|
+
"settings.keyPathPlaceholder": "~/.ssh/id_rsa",
|
|
137
|
+
"settings.passwordField": "Password",
|
|
138
|
+
"settings.remoteRoot": "Remote root (remoteRoot)",
|
|
139
|
+
"settings.test": "Test Connection",
|
|
140
|
+
"settings.saved": "Saved",
|
|
141
|
+
"settings.connOk": "Connected",
|
|
142
|
+
"settings.connFail": "Connection failed: ",
|
|
143
|
+
"settings.errSave": "Save failed",
|
|
144
|
+
"common.selectProfile": "— Select profile —",
|
|
145
|
+
"common.profile": "Profile",
|
|
146
|
+
"common.save": "Save",
|
|
147
|
+
"common.open": "Open",
|
|
148
|
+
"common.close": "Close",
|
|
149
|
+
"common.delete": "Delete",
|
|
150
|
+
"common.up": "⬆ Up",
|
|
151
|
+
"common.loading": "Loading…",
|
|
152
|
+
"common.processing": "Processing…",
|
|
153
|
+
"common.errList": "Failed to list directory",
|
|
154
|
+
"files.workspace": "Remote Workspace",
|
|
155
|
+
"files.selectWorkspace": "— Select workspace —",
|
|
156
|
+
"files.orProfile": "or pick a profile directly",
|
|
157
|
+
"files.defaultDir": "Default directory (entered when opening this workspace)",
|
|
158
|
+
"files.reading": "Reading…",
|
|
159
|
+
"files.errRead": "Failed to read",
|
|
160
|
+
"files.errSave": "Failed to save",
|
|
161
|
+
"files.errSaveDefaultDir": "Failed to save default directory",
|
|
162
|
+
"term.title": "Terminal {id}",
|
|
163
|
+
"term.exited": " (exited)",
|
|
164
|
+
"term.connecting": "Connecting…",
|
|
165
|
+
"term.noOutput": "(no output)",
|
|
166
|
+
"term.inputPlaceholder": "Type a command and press Enter…",
|
|
167
|
+
"term.send": "Send",
|
|
168
|
+
"term.needProfile": "Select a profile first",
|
|
169
|
+
"term.newTerminal": "+ New terminal",
|
|
170
|
+
"term.emptyHint": "Create a terminal from the profile above to operate the remote environment.",
|
|
171
|
+
"term.errSpawn": "Failed to start terminal",
|
|
172
|
+
"picker.dir": "Directory",
|
|
173
|
+
"picker.choose": "Choose this directory",
|
|
174
|
+
"flow.localTitle": "Select Local Directory",
|
|
175
|
+
"flow.remoteTitle": "Select Remote Directory",
|
|
176
|
+
"flow.localDir": "Local directory",
|
|
177
|
+
"flow.remoteDir": "Remote directory",
|
|
178
|
+
"flow.chooseRemote": "Choose remote directory…",
|
|
179
|
+
"flow.backLocal": "← Choose local directory",
|
|
180
|
+
"flow.errCreate": "Failed to create remote workspace"
|
|
181
|
+
};
|
|
182
|
+
var i18n = {
|
|
183
|
+
t: function (key) { return key; },
|
|
184
|
+
subscribe: function () { return function () {}; },
|
|
185
|
+
getSnapshot: function () { return 0; }
|
|
186
|
+
};
|
|
187
|
+
function useT() {
|
|
188
|
+
var state = React.useState(0);
|
|
189
|
+
var force = state[1];
|
|
190
|
+
React.useEffect(function () {
|
|
191
|
+
return i18n.subscribe(function () { force(function (n) { return n + 1; }); });
|
|
192
|
+
}, []);
|
|
193
|
+
return i18n.t;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// ---- 跨会话切换的状态记忆(按 sessionId 分桶,模块级存活,页面刷新即失效)----
|
|
197
|
+
var memoryBySession = {};
|
|
198
|
+
function useSessionMemory(sessionId, key, initial) {
|
|
199
|
+
var sid = sessionId || "global";
|
|
200
|
+
var bucket = memoryBySession[sid] || (memoryBySession[sid] = {});
|
|
201
|
+
var state = React.useState(function () {
|
|
202
|
+
if (Object.prototype.hasOwnProperty.call(bucket, key)) return bucket[key];
|
|
203
|
+
var init = typeof initial === "function" ? initial() : initial;
|
|
204
|
+
return bucket[key] = init;
|
|
205
|
+
});
|
|
206
|
+
var value = state[0], setValue = state[1];
|
|
207
|
+
React.useEffect(function () { bucket[key] = value; }, [value]);
|
|
208
|
+
return [value, setValue];
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// ---- 与 Host 半边的 JSON RPC ----
|
|
212
|
+
async function api(method, args) {
|
|
213
|
+
try {
|
|
214
|
+
var r = await fetch("/remote-ssh/api/" + method, {
|
|
215
|
+
method: "POST",
|
|
216
|
+
headers: { "content-type": "application/json" },
|
|
217
|
+
body: JSON.stringify(args || {})
|
|
218
|
+
});
|
|
219
|
+
var j = await r.json();
|
|
220
|
+
if (j && j.ok) return j.value;
|
|
221
|
+
return { ok: false, error: (j && j.error && j.error.message) || ("HTTP " + r.status) };
|
|
222
|
+
} catch (e) {
|
|
223
|
+
return { ok: false, error: String(e && e.message ? e.message : e) };
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// ---- 跨页签「打开工作区」的轻量共享选择 ----
|
|
228
|
+
var sel = { workspaceId: null };
|
|
229
|
+
var selListeners = [];
|
|
230
|
+
function setSelWorkspace(id) { sel.workspaceId = id; selListeners.forEach(function (fn) { fn(); }); }
|
|
231
|
+
function subscribeSel(fn) { selListeners.push(fn); return function () { var i = selListeners.indexOf(fn); if (i >= 0) selListeners.splice(i, 1); }; }
|
|
232
|
+
|
|
233
|
+
// ---- 通用:拉取连接配置(refreshKey 变化时重新拉取,避免缓存过期)----
|
|
234
|
+
function useProfiles(refreshKey) {
|
|
235
|
+
var state = React.useState([]);
|
|
236
|
+
var profiles = state[0], setProfiles = state[1];
|
|
237
|
+
React.useEffect(function () {
|
|
238
|
+
api("listProfiles").then(function (r) { if (r && r.ok) setProfiles(r.profiles || []); });
|
|
239
|
+
}, [refreshKey]);
|
|
240
|
+
return profiles;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
function Field(props) {
|
|
244
|
+
return h("div", { className: "rssh-row" },
|
|
245
|
+
h("span", { className: "rssh-label" }, props.label),
|
|
246
|
+
h("input", { className: "rssh-input", type: props.type || "text", value: props.value, placeholder: props.placeholder,
|
|
247
|
+
onChange: function (e) { props.onChange(e.target.value); } })
|
|
248
|
+
);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// ======================================================================
|
|
252
|
+
// 远程文件页签
|
|
253
|
+
// ======================================================================
|
|
254
|
+
function FilesTab(props) {
|
|
255
|
+
var t = useT();
|
|
256
|
+
var sessionId = props.sessionId;
|
|
257
|
+
var [workspaces, setWorkspaces] = React.useState([]);
|
|
258
|
+
var profiles = useProfiles();
|
|
259
|
+
var [wsId, setWsId] = useSessionMemory(sessionId, "wsId", sel.workspaceId);
|
|
260
|
+
var [profileId, setProfileId] = useSessionMemory(sessionId, "profileId", "");
|
|
261
|
+
var [path, setPath] = useSessionMemory(sessionId, "path", "");
|
|
262
|
+
var [entries, setEntries] = useSessionMemory(sessionId, "entries", []);
|
|
263
|
+
var [err, setErr] = React.useState(null);
|
|
264
|
+
var [loading, setLoading] = React.useState(false);
|
|
265
|
+
var [openFile, setOpenFile] = useSessionMemory(sessionId, "openFile", null);
|
|
266
|
+
var [defaultDir, setDefaultDir] = useSessionMemory(sessionId, "defaultDir", "");
|
|
267
|
+
|
|
268
|
+
React.useEffect(function () {
|
|
269
|
+
api("listWorkspaces").then(function (r) { if (r && r.ok) setWorkspaces(r.workspaces || []); });
|
|
270
|
+
return subscribeSel(function () { setWsId(sel.workspaceId); });
|
|
271
|
+
}, []);
|
|
272
|
+
|
|
273
|
+
function load(targetProfile, targetPath) {
|
|
274
|
+
setLoading(true); setErr(null);
|
|
275
|
+
api("listDir", { profileId: targetProfile, path: targetPath }).then(function (r) {
|
|
276
|
+
setLoading(false);
|
|
277
|
+
if (r && r.ok) { setEntries(r.entries || []); setPath(r.path || targetPath); }
|
|
278
|
+
else setErr((r && r.error) || t("common.errList"));
|
|
279
|
+
});
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// 工作区/配置选择变化时进入对应目录
|
|
283
|
+
React.useEffect(function () {
|
|
284
|
+
if (!wsId && !profileId) return;
|
|
285
|
+
var pid = profileId;
|
|
286
|
+
var rootPath = "";
|
|
287
|
+
if (wsId) {
|
|
288
|
+
var ws = workspaces.find(function (w) { return w.id === wsId; });
|
|
289
|
+
if (ws) { pid = ws.profileId; rootPath = ws.remotePath; }
|
|
290
|
+
}
|
|
291
|
+
if (!pid) return;
|
|
292
|
+
setProfileId(pid);
|
|
293
|
+
load(pid, rootPath || undefined);
|
|
294
|
+
}, [wsId, profileId, workspaces]);
|
|
295
|
+
|
|
296
|
+
// 选中工作区时,把「默认目录」输入框同步为该工作区的 remotePath。
|
|
297
|
+
React.useEffect(function () {
|
|
298
|
+
if (wsId) {
|
|
299
|
+
var ws = workspaces.find(function (w) { return w.id === wsId; });
|
|
300
|
+
if (ws) setDefaultDir(ws.remotePath || "");
|
|
301
|
+
}
|
|
302
|
+
}, [wsId, workspaces]);
|
|
303
|
+
|
|
304
|
+
function saveDefaultDir() {
|
|
305
|
+
if (!wsId) return;
|
|
306
|
+
api("updateWorkspace", { id: wsId, remotePath: defaultDir }).then(function (r) {
|
|
307
|
+
if (r && r.ok) { setWorkspaces(r.workspaces || []); setErr(null); }
|
|
308
|
+
else setErr((r && r.error) || t("files.errSaveDefaultDir"));
|
|
309
|
+
});
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
function openEntry(e) {
|
|
313
|
+
var base = path ? (path.replace(/\/+$/, "") + "/" + e.name) : e.name;
|
|
314
|
+
if (e.type === "directory") {
|
|
315
|
+
setPath(base);
|
|
316
|
+
load(profileId, base);
|
|
317
|
+
} else {
|
|
318
|
+
setOpenFile({ path: base, content: "", loading: true });
|
|
319
|
+
api("readFile", { profileId: profileId, path: base }).then(function (r) {
|
|
320
|
+
if (r && r.ok) setOpenFile({ path: base, content: r.content, binary: r.binary, dirty: false, loading: false });
|
|
321
|
+
else setOpenFile({ path: base, content: "", error: (r && r.error) || t("files.errRead"), loading: false });
|
|
322
|
+
});
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
function goUp() {
|
|
327
|
+
if (!path) return;
|
|
328
|
+
var parts = path.replace(/\/+$/, "").split("/");
|
|
329
|
+
parts.pop();
|
|
330
|
+
var up = parts.join("/") || "/";
|
|
331
|
+
setPath(up);
|
|
332
|
+
load(profileId, up);
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
function saveFile() {
|
|
336
|
+
if (!openFile) return;
|
|
337
|
+
api("writeFile", { profileId: profileId, path: openFile.path, content: openFile.content }).then(function (r) {
|
|
338
|
+
if (r && r.ok) setOpenFile(Object.assign({}, openFile, { dirty: false }));
|
|
339
|
+
else setErr((r && r.error) || t("files.errSave"));
|
|
340
|
+
});
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
return h("div", null,
|
|
344
|
+
h("div", { className: "rssh-row" },
|
|
345
|
+
h("span", { className: "rssh-label" }, t("files.workspace")),
|
|
346
|
+
h("select", { className: "rssh-select", value: wsId || "", onChange: function (e) { setWsId(e.target.value); setProfileId(""); } },
|
|
347
|
+
h("option", { value: "" }, t("files.selectWorkspace")),
|
|
348
|
+
workspaces.map(function (w) { return h("option", { key: w.id, value: w.id }, w.title); })
|
|
349
|
+
)
|
|
350
|
+
),
|
|
351
|
+
h("div", { className: "rssh-row" },
|
|
352
|
+
h("span", { className: "rssh-label" }, t("files.orProfile")),
|
|
353
|
+
h("select", { className: "rssh-select", value: profileId, onChange: function (e) { setProfileId(e.target.value); setWsId(""); } },
|
|
354
|
+
h("option", { value: "" }, t("common.selectProfile")),
|
|
355
|
+
profiles.map(function (p) { return h("option", { key: p.id, value: p.id }, p.name); })
|
|
356
|
+
)
|
|
357
|
+
),
|
|
358
|
+
wsId ? h("div", { className: "rssh-row" },
|
|
359
|
+
h("span", { className: "rssh-label" }, t("files.defaultDir")),
|
|
360
|
+
h("div", { className: "rssh-pathbar" },
|
|
361
|
+
h("input", { className: "rssh-input", value: defaultDir, onChange: function (e) { setDefaultDir(e.target.value); }, placeholder: "/home/user/project" }),
|
|
362
|
+
h("button", { className: "rssh-btn", onClick: saveDefaultDir }, t("common.save"))
|
|
363
|
+
)
|
|
364
|
+
) : null,
|
|
365
|
+
h("div", { className: "rssh-pathbar" },
|
|
366
|
+
h("input", { className: "rssh-input", value: path, onChange: function (e) { setPath(e.target.value); }, placeholder: "/home/user" }),
|
|
367
|
+
h("button", { className: "rssh-btn", onClick: function () { load(profileId, path); } }, t("common.open")),
|
|
368
|
+
h("button", { className: "rssh-btn", onClick: goUp }, t("common.up"))
|
|
369
|
+
),
|
|
370
|
+
loading ? h("div", { className: "rssh-muted" }, t("common.loading")) : null,
|
|
371
|
+
err ? h("div", { className: "rssh-err", style: { whiteSpace: "pre-wrap" } }, err) : null,
|
|
372
|
+
h("div", { style: { marginTop: 6 } },
|
|
373
|
+
entries.map(function (e) {
|
|
374
|
+
return h("div", { key: e.name, className: "rssh-tree-item", onClick: function () { openEntry(e); } },
|
|
375
|
+
h("span", { className: e.type === "directory" ? "rssh-tree-dir" : "rssh-tree-file" }, e.type === "directory" ? "📁 " + e.name : "📄 " + e.name),
|
|
376
|
+
e.type === "file" ? h("span", { className: "rssh-tree-size" }, String(e.size)) : null
|
|
377
|
+
);
|
|
378
|
+
})
|
|
379
|
+
),
|
|
380
|
+
openFile ? h("div", null,
|
|
381
|
+
h("div", { className: "rssh-pathbar", style: { marginTop: 8 } },
|
|
382
|
+
h("span", { className: "rssh-title", style: { flex: 1, wordBreak: "break-all" } }, openFile.path),
|
|
383
|
+
h("button", { className: "rssh-btn", onClick: saveFile }, t("common.save")),
|
|
384
|
+
h("button", { className: "rssh-btn", onClick: function () { setOpenFile(null); } }, t("common.close"))
|
|
385
|
+
),
|
|
386
|
+
openFile.loading ? h("div", { className: "rssh-muted" }, t("files.reading")) :
|
|
387
|
+
openFile.error ? h("div", { className: "rssh-err" }, openFile.error) :
|
|
388
|
+
h("textarea", { className: "rssh-content", style: { width: "100%", boxSizing: "border-box", minHeight: 200, background: "#121316", color: "#eee", border: "1px solid #444" },
|
|
389
|
+
value: openFile.content, onChange: function (e) { setOpenFile(Object.assign({}, openFile, { content: e.target.value, dirty: true })); } })
|
|
390
|
+
) : null
|
|
391
|
+
);
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
// ======================================================================
|
|
395
|
+
// 远程终端页签
|
|
396
|
+
// ======================================================================
|
|
397
|
+
function TerminalView(props) {
|
|
398
|
+
var t = useT();
|
|
399
|
+
var [data, setData] = React.useState("");
|
|
400
|
+
var [status, setStatus] = React.useState("running");
|
|
401
|
+
var [input, setInput] = React.useState("");
|
|
402
|
+
var endRef = React.useRef(null);
|
|
403
|
+
|
|
404
|
+
React.useEffect(function () {
|
|
405
|
+
var timer = setInterval(function () {
|
|
406
|
+
api("terminalRead", { id: props.id }).then(function (r) {
|
|
407
|
+
if (r && r.ok) {
|
|
408
|
+
if (r.data) setData(function (d) { return d + r.data; });
|
|
409
|
+
if (r.status) setStatus(r.status);
|
|
410
|
+
}
|
|
411
|
+
});
|
|
412
|
+
}, 200);
|
|
413
|
+
return function () { clearInterval(timer); };
|
|
414
|
+
}, [props.id]);
|
|
415
|
+
|
|
416
|
+
React.useEffect(function () {
|
|
417
|
+
if (endRef.current) endRef.current.scrollTop = endRef.current.scrollHeight;
|
|
418
|
+
}, [data]);
|
|
419
|
+
|
|
420
|
+
function send() {
|
|
421
|
+
if (!input) return;
|
|
422
|
+
api("terminalWrite", { id: props.id, data: input + "\r" });
|
|
423
|
+
setInput("");
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
return h("div", { className: "rssh-terminal" },
|
|
427
|
+
h("div", { className: "rssh-terminal-bar" },
|
|
428
|
+
h("span", { style: { flex: 1, color: status === "running" ? "#4ade80" : "#f87171" } }, t("term.title", { id: props.id }) + (status === "exited" ? t("term.exited") : "")),
|
|
429
|
+
h("button", { className: "rssh-btn", onClick: function () { props.onClose(props.id); } }, t("common.close"))
|
|
430
|
+
),
|
|
431
|
+
h("div", { className: "rssh-terminal-out", ref: endRef }, data || (status === "running" ? t("term.connecting") : t("term.noOutput"))),
|
|
432
|
+
h("div", { className: "rssh-terminal-input-row" },
|
|
433
|
+
h("input", { className: "rssh-input", value: input,
|
|
434
|
+
onChange: function (e) { setInput(e.target.value); },
|
|
435
|
+
onKeyDown: function (e) { if (e.key === "Enter") send(); },
|
|
436
|
+
placeholder: t("term.inputPlaceholder") }),
|
|
437
|
+
h("button", { className: "rssh-btn", onClick: send }, t("term.send"))
|
|
438
|
+
)
|
|
439
|
+
);
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
function TerminalTab(props) {
|
|
443
|
+
var t = useT();
|
|
444
|
+
var sessionId = props.sessionId;
|
|
445
|
+
var profiles = useProfiles();
|
|
446
|
+
var [profileId, setProfileId] = useSessionMemory(sessionId, "profileId", "");
|
|
447
|
+
var [terms, setTerms] = useSessionMemory(sessionId, "terms", []);
|
|
448
|
+
var [err, setErr] = React.useState(null);
|
|
449
|
+
|
|
450
|
+
function spawn() {
|
|
451
|
+
if (!profileId) { setErr(t("term.needProfile")); return; }
|
|
452
|
+
setErr(null);
|
|
453
|
+
api("spawnTerminal", { profileId: profileId }).then(function (r) {
|
|
454
|
+
if (r && r.ok) setTerms(function (list) { return list.concat([{ id: r.id }]); });
|
|
455
|
+
else setErr((r && r.error) || t("term.errSpawn"));
|
|
456
|
+
});
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
function close(id) {
|
|
460
|
+
api("terminalClose", { id: id });
|
|
461
|
+
setTerms(function (list) { return list.filter(function (x) { return x.id !== id; }); });
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
return h("div", null,
|
|
465
|
+
h("div", { className: "rssh-row" },
|
|
466
|
+
h("span", { className: "rssh-label" }, t("common.profile")),
|
|
467
|
+
h("select", { className: "rssh-select", value: profileId, onChange: function (e) { setProfileId(e.target.value); } },
|
|
468
|
+
h("option", { value: "" }, t("common.selectProfile")),
|
|
469
|
+
profiles.map(function (p) { return h("option", { key: p.id, value: p.id }, p.name); })
|
|
470
|
+
)
|
|
471
|
+
),
|
|
472
|
+
h("button", { className: "rssh-btn", onClick: spawn }, t("term.newTerminal")),
|
|
473
|
+
err ? h("div", { className: "rssh-err", style: { marginTop: 6 } }, err) : null,
|
|
474
|
+
terms.length === 0 ? h("div", { className: "rssh-empty" }, t("term.emptyHint")) : null,
|
|
475
|
+
terms.map(function (term) { return h(TerminalView, { key: term.id, id: term.id, onClose: close }); })
|
|
476
|
+
);
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
// ======================================================================
|
|
480
|
+
// 目录选择器(供「添加工作区」弹窗流程使用:本地 browse / 远程 SSH)
|
|
481
|
+
// ======================================================================
|
|
482
|
+
// 本地目录列举:走 DSH browse 能力(无原生对话框时替代 pickDirectory)。
|
|
483
|
+
function localListFn(workspaces, p) {
|
|
484
|
+
return workspaces.listDirectory(p).then(function (r) {
|
|
485
|
+
return {
|
|
486
|
+
ok: true,
|
|
487
|
+
path: r.path,
|
|
488
|
+
entries: (r.entries || []).map(function (e) { return { name: e.name, type: "directory", path: e.path }; })
|
|
489
|
+
};
|
|
490
|
+
}).catch(function (e) {
|
|
491
|
+
return { ok: false, error: String(e && e.message ? e.message : e) };
|
|
492
|
+
});
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
// 远程目录列举:走本插件 SSH API,并为每条记录补全绝对 path。
|
|
496
|
+
function remoteListFn(profileId, p) {
|
|
497
|
+
return api("listDir", { profileId: profileId, path: p }).then(function (r) {
|
|
498
|
+
if (!(r && r.ok)) return r;
|
|
499
|
+
var parent = r.path || "";
|
|
500
|
+
var entries = (r.entries || []).map(function (e) {
|
|
501
|
+
return { name: e.name, type: e.type, path: (parent ? parent.replace(/\/+$/, "") + "/" : "") + e.name };
|
|
502
|
+
});
|
|
503
|
+
return { ok: true, path: r.path, entries: entries };
|
|
504
|
+
});
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
// 计算父目录,兼容 Windows(\\) 与 POSIX(/) 分隔符。
|
|
508
|
+
function parentOf(path) {
|
|
509
|
+
if (!path) return null;
|
|
510
|
+
var str = String(path).replace(/[\/\\]+$/, "");
|
|
511
|
+
if (str === "") return null;
|
|
512
|
+
var idx = Math.max(str.lastIndexOf("/"), str.lastIndexOf("\\"));
|
|
513
|
+
if (idx < 0) return null;
|
|
514
|
+
var parent = str.slice(0, idx);
|
|
515
|
+
if (/^[A-Za-z]:$/.test(parent)) return parent + "\\";
|
|
516
|
+
if (parent === "") return "/";
|
|
517
|
+
return parent;
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
// 通用目录选择器:listFn(path) 返回 {ok, path, entries:[{name,type,path}]}。
|
|
521
|
+
// resetKey 变化(如远程切换连接)时从根目录重新加载。
|
|
522
|
+
function DirPicker(props) {
|
|
523
|
+
var t = useT();
|
|
524
|
+
var [path, setPath] = React.useState("");
|
|
525
|
+
var [entries, setEntries] = React.useState([]);
|
|
526
|
+
var [err, setErr] = React.useState(null);
|
|
527
|
+
var [loading, setLoading] = React.useState(false);
|
|
528
|
+
|
|
529
|
+
var listFnRef = React.useRef(props.listFn);
|
|
530
|
+
listFnRef.current = props.listFn;
|
|
531
|
+
|
|
532
|
+
function load(p) {
|
|
533
|
+
setLoading(true); setErr(null);
|
|
534
|
+
listFnRef.current(p).then(function (r) {
|
|
535
|
+
setLoading(false);
|
|
536
|
+
if (r && r.ok) { setEntries(r.entries || []); setPath(r.path || p || ""); }
|
|
537
|
+
else setErr((r && r.error) || t("common.errList"));
|
|
538
|
+
});
|
|
539
|
+
}
|
|
540
|
+
React.useEffect(function () { load(undefined); }, [props.resetKey]);
|
|
541
|
+
|
|
542
|
+
function enterDir(e) { load(e.path); }
|
|
543
|
+
function goUp() {
|
|
544
|
+
var up = parentOf(path);
|
|
545
|
+
if (up !== null) load(up);
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
return h("div", { style: { border: "1px solid #333", borderRadius: 6, padding: 8, marginTop: 6 } },
|
|
549
|
+
h("div", { className: "rssh-pathbar" },
|
|
550
|
+
h("input", { className: "rssh-input", value: path, onChange: function (e) { setPath(e.target.value); }, placeholder: props.placeholder || t("picker.dir") }),
|
|
551
|
+
h("button", { className: "rssh-btn", onClick: function () { load(path); } }, t("common.open")),
|
|
552
|
+
h("button", { className: "rssh-btn", onClick: goUp }, t("common.up"))
|
|
553
|
+
),
|
|
554
|
+
loading ? h("div", { className: "rssh-muted" }, t("common.loading")) : null,
|
|
555
|
+
err ? h("div", { className: "rssh-err" }, err) : null,
|
|
556
|
+
h("div", { style: { maxHeight: 180, overflow: "auto", marginTop: 4 } },
|
|
557
|
+
entries.filter(function (e) { return e.type === "directory"; }).map(function (e) {
|
|
558
|
+
return h("div", { key: e.path || e.name, className: "rssh-tree-item", onClick: function () { enterDir(e); } },
|
|
559
|
+
h("span", { className: "rssh-tree-dir" }, "📁 " + e.name));
|
|
560
|
+
})
|
|
561
|
+
),
|
|
562
|
+
h("button", { className: "rssh-btn", style: { marginTop: 8 }, disabled: !path, onClick: function () { props.onChoose(path); } }, t("picker.choose"))
|
|
563
|
+
);
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
function RemoteWorkspaceFlow(props) {
|
|
567
|
+
var t = useT();
|
|
568
|
+
var [mode, setMode] = React.useState("local");
|
|
569
|
+
var profiles = useProfiles(props.open);
|
|
570
|
+
var [profileId, setProfileId] = React.useState("");
|
|
571
|
+
var [creating, setCreating] = React.useState(false);
|
|
572
|
+
|
|
573
|
+
// 每次打开流程时重置为「本地目录」起始界面。
|
|
574
|
+
React.useEffect(function () {
|
|
575
|
+
if (props.open) { setMode("local"); setProfileId(""); }
|
|
576
|
+
}, [props.open]);
|
|
577
|
+
|
|
578
|
+
if (!props.open) return null;
|
|
579
|
+
|
|
580
|
+
function chooseLocal(p) { props.onPicked(p); }
|
|
581
|
+
|
|
582
|
+
function chooseRemote(remotePath) {
|
|
583
|
+
setCreating(true);
|
|
584
|
+
api("createRemoteWorkspace", { profileId: profileId, remotePath: remotePath }).then(function (r) {
|
|
585
|
+
setCreating(false);
|
|
586
|
+
if (r && r.ok && r.mirrorPath) props.onPicked(r.mirrorPath);
|
|
587
|
+
else props.onError((r && r.error) || t("flow.errCreate"));
|
|
588
|
+
});
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
var canClose = !creating && !props.busy;
|
|
592
|
+
|
|
593
|
+
if (mode === "remote") {
|
|
594
|
+
return h(Modal, { open: true, onClose: function () { if (canClose) props.onCancel(); }, title: t("flow.remoteTitle"), closeLabel: t("common.close"), className: "rssh-flow-dialog" },
|
|
595
|
+
h("div", { style: { padding: 8 } },
|
|
596
|
+
creating || props.busy ? h("div", { className: "rssh-muted" }, t("common.processing")) : null,
|
|
597
|
+
h("div", { className: "rssh-row" },
|
|
598
|
+
h("span", { className: "rssh-label" }, t("common.profile")),
|
|
599
|
+
h("select", { className: "rssh-select", value: profileId, onChange: function (e) { setProfileId(e.target.value); } },
|
|
600
|
+
h("option", { value: "" }, t("common.selectProfile")),
|
|
601
|
+
profiles.map(function (p) { return h("option", { key: p.id, value: p.id }, p.name); })
|
|
602
|
+
)
|
|
603
|
+
),
|
|
604
|
+
profileId ? h(DirPicker, { resetKey: profileId, placeholder: t("flow.remoteDir"), listFn: function (p) { return remoteListFn(profileId, p); }, onChoose: chooseRemote }) : null,
|
|
605
|
+
h("div", { className: "rssh-actions", style: { marginTop: 8 } },
|
|
606
|
+
h("button", { className: "rssh-btn", onClick: function () { setMode("local"); }, disabled: !!props.busy }, t("flow.backLocal"))
|
|
607
|
+
)
|
|
608
|
+
)
|
|
609
|
+
);
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
return h(Modal, { open: true, onClose: function () { if (canClose) props.onCancel(); }, title: t("flow.localTitle"), closeLabel: t("common.close"), className: "rssh-flow-dialog" },
|
|
613
|
+
h("div", { style: { padding: 8 } },
|
|
614
|
+
props.busy ? h("div", { className: "rssh-muted" }, t("common.processing")) : null,
|
|
615
|
+
h(DirPicker, { resetKey: "local", placeholder: t("flow.localDir"), listFn: function (p) { return localListFn(props.workspaces, p); }, onChoose: chooseLocal }),
|
|
616
|
+
h("div", { className: "rssh-actions", style: { marginTop: 8 } },
|
|
617
|
+
h("button", { className: "rssh-btn", onClick: function () { setMode("remote"); }, disabled: !!props.busy }, t("flow.chooseRemote"))
|
|
618
|
+
)
|
|
619
|
+
)
|
|
620
|
+
);
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
// ======================================================================
|
|
624
|
+
// 设置页小节(连接配置管理)
|
|
625
|
+
// ======================================================================
|
|
626
|
+
function SettingsSection(props) {
|
|
627
|
+
var t = useT();
|
|
628
|
+
var [profiles, setProfiles] = React.useState([]);
|
|
629
|
+
var [form, setForm] = React.useState({ name: "", host: "", port: "22", user: "", authMethod: "key", keyPath: "", password: "", remoteRoot: "~" });
|
|
630
|
+
var [result, setResult] = React.useState(null);
|
|
631
|
+
var [busy, setBusy] = React.useState(false);
|
|
632
|
+
var set = function (k, v) { setForm(function (f) { var n = {}; for (var key in f) n[key] = f[key]; n[k] = v; return n; }); };
|
|
633
|
+
|
|
634
|
+
function reload() { api("listProfiles").then(function (r) { if (r && r.ok) setProfiles(r.profiles || []); }); }
|
|
635
|
+
React.useEffect(reload, []);
|
|
636
|
+
|
|
637
|
+
function save() {
|
|
638
|
+
setBusy(true); setResult(null);
|
|
639
|
+
api("saveProfile", Object.assign({}, form, { port: parseInt(form.port, 10) || 22 })).then(function (r) {
|
|
640
|
+
setBusy(false);
|
|
641
|
+
if (r && r.ok) {
|
|
642
|
+
setProfiles(r.profiles || []);
|
|
643
|
+
setForm({ name: "", host: "", port: "22", user: "", authMethod: "key", keyPath: "", password: "", remoteRoot: "~" });
|
|
644
|
+
setResult({ ok: true, msg: t("settings.saved") });
|
|
645
|
+
} else setResult({ ok: false, msg: (r && r.error) || t("settings.errSave") });
|
|
646
|
+
});
|
|
647
|
+
}
|
|
648
|
+
function test(p) {
|
|
649
|
+
setBusy(true); setResult(null);
|
|
650
|
+
api("testConnection", { id: p.id }).then(function (r) {
|
|
651
|
+
setBusy(false);
|
|
652
|
+
if (r && r.ok) setResult({ ok: true, msg: t("settings.connOk") + "\n" + (r.stdout || "") });
|
|
653
|
+
else setResult({ ok: false, msg: t("settings.connFail") + ((r && r.error) || (r && r.stderr) || "") });
|
|
654
|
+
});
|
|
655
|
+
}
|
|
656
|
+
function del(p) {
|
|
657
|
+
api("deleteProfile", { id: p.id }).then(function (r) { if (r && r.ok) { setProfiles(r.profiles || []); } });
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
return h("div", null,
|
|
661
|
+
h("p", { className: "rssh-muted" }, t("settings.desc")),
|
|
662
|
+
profiles.map(function (p) {
|
|
663
|
+
return h("div", { key: p.id, className: "rssh-card" },
|
|
664
|
+
h("div", { className: "rssh-title" }, p.name),
|
|
665
|
+
h("div", { className: "rssh-sub" }, (p.user || "") + "@" + (p.host || "") + ":" + (p.port || 22) + " [" + (p.authMethod || "key") + "]"),
|
|
666
|
+
h("div", { className: "rssh-actions" },
|
|
667
|
+
h("button", { className: "rssh-btn", onClick: function () { test(p); }, disabled: busy }, t("settings.test")),
|
|
668
|
+
h("button", { className: "rssh-btn", onClick: function () { del(p); } }, t("common.delete"))
|
|
669
|
+
)
|
|
670
|
+
);
|
|
671
|
+
}),
|
|
672
|
+
profiles.length === 0 ? h("div", { className: "rssh-empty" }, t("settings.empty")) : null,
|
|
673
|
+
h("div", { className: "rssh-card" },
|
|
674
|
+
h("div", { className: "rssh-title" }, t("settings.add")),
|
|
675
|
+
Field({ label: t("settings.name"), value: form.name, onChange: function (v) { set("name", v); }, placeholder: t("settings.namePlaceholder") }),
|
|
676
|
+
Field({ label: t("settings.host"), value: form.host, onChange: function (v) { set("host", v); }, placeholder: t("settings.hostPlaceholder") }),
|
|
677
|
+
Field({ label: t("settings.port"), value: form.port, onChange: function (v) { set("port", v); } }),
|
|
678
|
+
Field({ label: t("settings.user"), value: form.user, onChange: function (v) { set("user", v); }, placeholder: t("settings.userPlaceholder") }),
|
|
679
|
+
h("div", { className: "rssh-row" },
|
|
680
|
+
h("span", { className: "rssh-label" }, t("settings.auth")),
|
|
681
|
+
h("select", { className: "rssh-select", value: form.authMethod, onChange: function (e) { set("authMethod", e.target.value); } },
|
|
682
|
+
h("option", { value: "key" }, t("settings.key")),
|
|
683
|
+
h("option", { value: "password" }, t("settings.password"))
|
|
684
|
+
)
|
|
685
|
+
),
|
|
686
|
+
form.authMethod === "key" ? Field({ label: t("settings.keyPath"), value: form.keyPath, onChange: function (v) { set("keyPath", v); }, placeholder: t("settings.keyPathPlaceholder") }) : null,
|
|
687
|
+
form.authMethod === "password" ? Field({ label: t("settings.passwordField"), type: "password", value: form.password, onChange: function (v) { set("password", v); } }) : null,
|
|
688
|
+
Field({ label: t("settings.remoteRoot"), value: form.remoteRoot, onChange: function (v) { set("remoteRoot", v); }, placeholder: "~/project" }),
|
|
689
|
+
h("button", { className: "rssh-btn", onClick: save, disabled: busy }, busy ? t("common.processing") : t("common.save"))
|
|
690
|
+
),
|
|
691
|
+
result ? h("div", { className: result.ok ? "rssh-ok" : "rssh-err", style: { whiteSpace: "pre-wrap", marginTop: 6 } }, result.msg) : null
|
|
692
|
+
);
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
// ======================================================================
|
|
696
|
+
// apply
|
|
697
|
+
// ======================================================================
|
|
698
|
+
function apply(ctx) {
|
|
699
|
+
var betterSidebar = ctx.betterSidebar;
|
|
700
|
+
var slots = ctx.slots;
|
|
701
|
+
var locale = ctx.locale;
|
|
702
|
+
|
|
703
|
+
// 注册中英文词典并绑定当前语言;locale 切换时 i18n.subscribe 触发组件重渲染。
|
|
704
|
+
if (locale) {
|
|
705
|
+
ctx.effect(function () { return locale.register(NS, { zh: zh, en: en }); }, "dsh-remote-ssh: dictionaries");
|
|
706
|
+
i18n.t = locale.bind(NS);
|
|
707
|
+
i18n.subscribe = function (cb) { return locale.subscribe(cb); };
|
|
708
|
+
i18n.getSnapshot = function () { return locale.getSnapshot(); };
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
ctx.effect(function () {
|
|
712
|
+
var el = document.createElement("style");
|
|
713
|
+
el.textContent = CSS;
|
|
714
|
+
document.head.appendChild(el);
|
|
715
|
+
return function () { if (el.parentNode) el.parentNode.removeChild(el); };
|
|
716
|
+
});
|
|
717
|
+
|
|
718
|
+
if (betterSidebar) {
|
|
719
|
+
ctx.effect(function () {
|
|
720
|
+
return betterSidebar.registerTab({
|
|
721
|
+
id: "remssh:files",
|
|
722
|
+
title: function () { return i18n.t("tabs.files"); },
|
|
723
|
+
order: 90,
|
|
724
|
+
single: true,
|
|
725
|
+
icon: function () { return h("span", null, "🗂️"); },
|
|
726
|
+
component: function (props) {
|
|
727
|
+
var sid = props && props.scope ? props.scope.sessionId : null;
|
|
728
|
+
return h(FilesTab, { key: "files:" + (sid || "global"), sessionId: sid });
|
|
729
|
+
}
|
|
730
|
+
});
|
|
731
|
+
});
|
|
732
|
+
ctx.effect(function () {
|
|
733
|
+
return betterSidebar.registerTab({
|
|
734
|
+
id: "remssh:term",
|
|
735
|
+
title: function () { return i18n.t("tabs.term"); },
|
|
736
|
+
order: 91,
|
|
737
|
+
single: true,
|
|
738
|
+
icon: function () { return h("span", null, "💻"); },
|
|
739
|
+
component: function (props) {
|
|
740
|
+
var sid = props && props.scope ? props.scope.sessionId : null;
|
|
741
|
+
return h(TerminalTab, { key: "term:" + (sid || "global"), sessionId: sid });
|
|
742
|
+
}
|
|
743
|
+
});
|
|
744
|
+
});
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
if (slots) {
|
|
748
|
+
slots.inject("settings.section", function () {
|
|
749
|
+
return slots.register({
|
|
750
|
+
name: "settings.section",
|
|
751
|
+
id: "remote-ssh",
|
|
752
|
+
order: 200,
|
|
753
|
+
label: function () { return i18n.t("settings.nav"); },
|
|
754
|
+
locale: NS
|
|
755
|
+
}, SettingsSection);
|
|
756
|
+
});
|
|
757
|
+
|
|
758
|
+
// 接管原生「添加工作区」流程:用明显更低的优先级覆盖内置的本地目录选择器,
|
|
759
|
+
// 弹窗内提供「本地目录(DSH browse)/ 远程目录(SSH)」两种浏览。
|
|
760
|
+
var flowInject = function () { return { workspaces: ctx.get("workspaces") }; };
|
|
761
|
+
ctx.effect(function () {
|
|
762
|
+
return slots.inject("conversation.hero.workspace.directoryFlow", function () {
|
|
763
|
+
return slots.register({ name: "conversation.hero.workspace.directoryFlow", priority: -100, inject: flowInject }, RemoteWorkspaceFlow);
|
|
764
|
+
});
|
|
765
|
+
});
|
|
766
|
+
ctx.effect(function () {
|
|
767
|
+
return slots.inject("sidebar.workspaces.directoryFlow", function () {
|
|
768
|
+
return slots.register({ name: "sidebar.workspaces.directoryFlow", priority: -100, inject: flowInject }, RemoteWorkspaceFlow);
|
|
769
|
+
});
|
|
770
|
+
});
|
|
771
|
+
}
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
exports.apply = apply;
|
|
775
|
+
exports.inject = ["betterSidebar", "slots", "locale"];
|
|
776
|
+
return module.exports;
|
|
777
|
+
}
|
|
778
|
+
});
|