@linxin666/dsh-ssh 0.2.7 → 0.2.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.i18n.yaml +2 -2
- package/README.md +1 -0
- package/README.zh.md +1 -0
- package/lib/client.js +19 -8
- package/lib/client.js.map +1 -1
- package/lib/index.js +45 -13
- package/lib/types/client/locales.d.ts +1 -1
- package/lib/types/dsh-home.d.ts +16 -0
- package/lib/types/index.d.ts +1 -1
- package/lib/types/protocol.d.ts +1 -1
- package/lib/types/store.d.ts +3 -2
- package/package.json +16 -13
- package/src/client/locales.ts +2 -2
- package/src/client/panel/HostsTab.tsx +15 -3
- package/src/client/panel/SshPanel.tsx +2 -2
- package/src/client/panel/panel.module.css +15 -4
- package/src/client/sidebar-entry.ts +2 -2
- package/src/dsh-home.ts +36 -0
- package/src/index.ts +1 -1
- package/src/protocol.ts +1 -1
- package/src/store.ts +5 -3
- package/src/tools.ts +7 -7
package/lib/index.js
CHANGED
|
@@ -2,7 +2,7 @@ import { installSettingsSection, settingsNamespace } from "@deepseek-ai/dsh-sett
|
|
|
2
2
|
import { closeSync, createReadStream, createWriteStream, existsSync, lstatSync, mkdirSync, openSync, readFileSync, readdirSync, renameSync, statSync, writeFileSync } from "node:fs";
|
|
3
3
|
import { Client } from "ssh2";
|
|
4
4
|
import { homedir, tmpdir } from "node:os";
|
|
5
|
-
import { basename, dirname, join, relative, resolve } from "node:path";
|
|
5
|
+
import { basename, dirname, isAbsolute, join, relative, resolve } from "node:path";
|
|
6
6
|
import { createServer } from "node:net";
|
|
7
7
|
import { unlink } from "node:fs/promises";
|
|
8
8
|
import { randomBytes } from "node:crypto";
|
|
@@ -412,7 +412,7 @@ var require_lib$1 = /* @__PURE__ */ __commonJSMin(((exports, module) => {
|
|
|
412
412
|
});
|
|
413
413
|
}));
|
|
414
414
|
//#endregion
|
|
415
|
-
//#region src/
|
|
415
|
+
//#region src/dsh-home.ts
|
|
416
416
|
var import_lib = /* @__PURE__ */ __toESM((/* @__PURE__ */ __commonJSMin(((exports, module) => {
|
|
417
417
|
var __defProp = Object.defineProperty;
|
|
418
418
|
var __name = (target, value) => __defProp(target, "name", {
|
|
@@ -1025,7 +1025,39 @@ var import_lib = /* @__PURE__ */ __toESM((/* @__PURE__ */ __commonJSMin(((export
|
|
|
1025
1025
|
module.exports = Schema;
|
|
1026
1026
|
})))(), 1);
|
|
1027
1027
|
/**
|
|
1028
|
-
*
|
|
1028
|
+
* DSH_HOME resolution shared by the plugin family's Host halves: the
|
|
1029
|
+
* environment override wins, the platform home fallback follows. Mirrors
|
|
1030
|
+
* what dsh-pet and dsh-liangshen each used to implement locally.
|
|
1031
|
+
*/
|
|
1032
|
+
/** Expand a leading ~ (or ~user) in a path, platform-style. */
|
|
1033
|
+
function expandHome$1(path, home = homedir()) {
|
|
1034
|
+
if (path === "~") return home;
|
|
1035
|
+
if (path.startsWith("~/") || path.startsWith("~\\")) return join(home, path.slice(2));
|
|
1036
|
+
return path;
|
|
1037
|
+
}
|
|
1038
|
+
/**
|
|
1039
|
+
* Resolve the DSH home directory.
|
|
1040
|
+
* @param env - process environment to read DSH_HOME from.
|
|
1041
|
+
* @param home - platform home directory fallback (test seam).
|
|
1042
|
+
* @returns the absolute DSH home path.
|
|
1043
|
+
*/
|
|
1044
|
+
function resolveDshHome(env = process.env, home = homedir()) {
|
|
1045
|
+
const raw = env.DSH_HOME;
|
|
1046
|
+
if (raw !== void 0 && raw.trim() !== "") {
|
|
1047
|
+
const expanded = expandHome$1(raw.trim(), home);
|
|
1048
|
+
return isAbsolute(expanded) ? expanded : join(process.cwd(), expanded);
|
|
1049
|
+
}
|
|
1050
|
+
return join(home, ".dsh");
|
|
1051
|
+
}
|
|
1052
|
+
/** Resolve the DSH home directory from the live environment. */
|
|
1053
|
+
function dshHome() {
|
|
1054
|
+
return resolveDshHome();
|
|
1055
|
+
}
|
|
1056
|
+
//#endregion
|
|
1057
|
+
//#region src/store.ts
|
|
1058
|
+
/**
|
|
1059
|
+
* Host config store: one JSON file (`$DSH_HOME/dsh-ssh.json`, defaulting
|
|
1060
|
+
* to `~/.dsh`) holding every
|
|
1029
1061
|
* SSH host entry, written atomically (tmp + rename). Also parses the user's
|
|
1030
1062
|
* standard `~/.ssh/config` for one-shot import. Secrets (passwords,
|
|
1031
1063
|
* passphrases) live in this user-owned file in plaintext — same trust model
|
|
@@ -1033,9 +1065,9 @@ var import_lib = /* @__PURE__ */ __toESM((/* @__PURE__ */ __commonJSMin(((export
|
|
|
1033
1065
|
*/
|
|
1034
1066
|
/** File format version. */
|
|
1035
1067
|
const FORMAT_VERSION = 1;
|
|
1036
|
-
/** Store file location:
|
|
1068
|
+
/** Store file location: $DSH_HOME/dsh-ssh.json (defaults to ~/.dsh). */
|
|
1037
1069
|
function storePath() {
|
|
1038
|
-
return join(
|
|
1070
|
+
return join(dshHome(), "dsh-ssh.json");
|
|
1039
1071
|
}
|
|
1040
1072
|
/** The user's standard OpenSSH config path. */
|
|
1041
1073
|
function sshConfigPath() {
|
|
@@ -2950,7 +2982,7 @@ function sshListTool(engine) {
|
|
|
2950
2982
|
function sshExecTool(engine) {
|
|
2951
2983
|
return defineTool({
|
|
2952
2984
|
name: "ssh_exec",
|
|
2953
|
-
description: "Execute a command on a
|
|
2985
|
+
description: "Execute a shell command on a REMOTE SSH host by alias; the command runs on the remote host, never on this machine. For commands on this machine, use the local bash tool. Prefer combining independent read-only queries into one command. Triggers: run command on server, deploy, check server/status, service control, view logs, any remote operation.",
|
|
2954
2986
|
parameters: {
|
|
2955
2987
|
alias: {
|
|
2956
2988
|
type: "string",
|
|
@@ -3022,7 +3054,7 @@ function sshExecTool(engine) {
|
|
|
3022
3054
|
function sshUploadTool(engine) {
|
|
3023
3055
|
return defineTool({
|
|
3024
3056
|
name: "ssh_upload",
|
|
3025
|
-
description: "
|
|
3057
|
+
description: "Transfer a file FROM this machine (the dsh host) TO a remote SSH host. Use this only when the file must be copied to the remote host. Files that stay on this machine are handled with the local file tools (read / write / edit), not ssh_upload. Triggers: upload file to server, deploy artifact, copy config to server.",
|
|
3026
3058
|
parameters: {
|
|
3027
3059
|
alias: {
|
|
3028
3060
|
type: "string",
|
|
@@ -3032,12 +3064,12 @@ function sshUploadTool(engine) {
|
|
|
3032
3064
|
localPath: {
|
|
3033
3065
|
type: "string",
|
|
3034
3066
|
required: true,
|
|
3035
|
-
description: "Absolute
|
|
3067
|
+
description: "Absolute path of the source file on THIS machine (the dsh host) — not a path on the remote host."
|
|
3036
3068
|
},
|
|
3037
3069
|
remotePath: {
|
|
3038
3070
|
type: "string",
|
|
3039
3071
|
required: true,
|
|
3040
|
-
description: "
|
|
3072
|
+
description: "Absolute destination path on the remote SSH host (parent dirs are created)."
|
|
3041
3073
|
}
|
|
3042
3074
|
},
|
|
3043
3075
|
output: {
|
|
@@ -3077,7 +3109,7 @@ function sshUploadTool(engine) {
|
|
|
3077
3109
|
function sshDownloadTool(engine) {
|
|
3078
3110
|
return defineTool({
|
|
3079
3111
|
name: "ssh_download",
|
|
3080
|
-
description: "
|
|
3112
|
+
description: "Copy a remote FILE from a configured SSH host to this machine (the dsh host). Use this only when the source is on the remote host; files already on this machine are read with the local file tools (read / write / edit), not ssh_download. Directory download is not supported — download files individually. Triggers: download file from server, fetch remote log/artifact.",
|
|
3081
3113
|
parameters: {
|
|
3082
3114
|
alias: {
|
|
3083
3115
|
type: "string",
|
|
@@ -3087,12 +3119,12 @@ function sshDownloadTool(engine) {
|
|
|
3087
3119
|
remotePath: {
|
|
3088
3120
|
type: "string",
|
|
3089
3121
|
required: true,
|
|
3090
|
-
description: "
|
|
3122
|
+
description: "Absolute path of the source file on the remote SSH host."
|
|
3091
3123
|
},
|
|
3092
3124
|
localPath: {
|
|
3093
3125
|
type: "string",
|
|
3094
3126
|
required: true,
|
|
3095
|
-
description: "Absolute destination path on
|
|
3127
|
+
description: "Absolute destination path on THIS machine (the dsh host)."
|
|
3096
3128
|
}
|
|
3097
3129
|
},
|
|
3098
3130
|
output: {
|
|
@@ -3457,7 +3489,7 @@ const DEFAULT_ANNOUNCE = false;
|
|
|
3457
3489
|
/** Order of the announcement section within the tool-guidance band. */
|
|
3458
3490
|
const SECTION_ORDER = 150;
|
|
3459
3491
|
/** Model-facing announcement: plugin presence, capabilities, and limits. */
|
|
3460
|
-
const SSH_GUIDANCE = "本机已安装 dsh-ssh 插件(DSH 远程 SSH 运维):侧边栏「SSH」入口;在 dsh-web-ui 插件全家桶仓库(packages/dsh-ssh)统一维护。能力:主机配置存
|
|
3492
|
+
const SSH_GUIDANCE = "本机已安装 dsh-ssh 插件(DSH 远程 SSH 运维):侧边栏「SSH」入口;在 dsh-web-ui 插件全家桶仓库(packages/dsh-ssh)统一维护。能力:主机配置存 $DSH_HOME/dsh-ssh.json(默认 ~/.dsh)(可从 ~/.ssh/config 导入);持久连接池复用长连接(空闲 30 分钟自动断开);ssh_list 列出主机、ssh_exec 执行远程命令、ssh_upload/ssh_download 传输文件、ssh_tunnel 本地端口转发(访问远程数据库/内网服务)、ssh_cluster 集群并发执行;支持密钥/密码/ssh-agent 认证、passphrase 密钥与 ProxyJump 跳板机;Web 终端走 WebSocket。限制:主机操作由用户在 GUI 中配置后 agent 方可使用;密码以明文存在用户主目录私有文件(权限 0600);命令输出原样返回、可能含敏感信息;断线重连可能重放非幂等命令;传输/执行消耗真实远程资源,先确认再操作。路径区分:本机(dsh host)上的文件与命令一律用本地工具(read / write / edit / bash),ssh_* 工具只针对远程主机上的路径。用户提到「SSH / 远程服务器 / 服务器操作 / 跳板机 / 隧道 / 部署 / 上传下载」时即指本插件,请据此协作。";
|
|
3461
3493
|
/**
|
|
3462
3494
|
* Mount the SSH engine, routes, tools, and announcement.
|
|
3463
3495
|
* @param ctx - host plugin context carrying webServer/tools/systemPrompt.
|
|
@@ -59,7 +59,7 @@ export declare const zh: {
|
|
|
59
59
|
readonly 'form.keyPathHint': "如 ~/.ssh/id_ed25519";
|
|
60
60
|
readonly 'form.passphrase': "密钥口令";
|
|
61
61
|
readonly 'form.password': "密码";
|
|
62
|
-
readonly 'form.passwordHint': "明文保存在
|
|
62
|
+
readonly 'form.passwordHint': "明文保存在 $DSH_HOME/dsh-ssh.json(本机私有文件)";
|
|
63
63
|
readonly 'form.passwordRequired': "密码认证需要填写密码。";
|
|
64
64
|
readonly 'form.proxyJump': "跳板机别名";
|
|
65
65
|
readonly 'form.proxyJumpHint': "逗号分隔的本地主机别名,按顺序连接";
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DSH_HOME resolution shared by the plugin family's Host halves: the
|
|
3
|
+
* environment override wins, the platform home fallback follows. Mirrors
|
|
4
|
+
* what dsh-pet and dsh-liangshen each used to implement locally.
|
|
5
|
+
*/
|
|
6
|
+
/** Expand a leading ~ (or ~user) in a path, platform-style. */
|
|
7
|
+
export declare function expandHome(path: string, home?: string): string;
|
|
8
|
+
/**
|
|
9
|
+
* Resolve the DSH home directory.
|
|
10
|
+
* @param env - process environment to read DSH_HOME from.
|
|
11
|
+
* @param home - platform home directory fallback (test seam).
|
|
12
|
+
* @returns the absolute DSH home path.
|
|
13
|
+
*/
|
|
14
|
+
export declare function resolveDshHome(env?: NodeJS.ProcessEnv, home?: string): string;
|
|
15
|
+
/** Resolve the DSH home directory from the live environment. */
|
|
16
|
+
export declare function dshHome(): string;
|
package/lib/types/index.d.ts
CHANGED
|
@@ -38,7 +38,7 @@ export interface Config {
|
|
|
38
38
|
}
|
|
39
39
|
export declare const Config: z<Config>;
|
|
40
40
|
/** Model-facing announcement: plugin presence, capabilities, and limits. */
|
|
41
|
-
export declare const SSH_GUIDANCE = "\u672C\u673A\u5DF2\u5B89\u88C5 dsh-ssh \u63D2\u4EF6\uFF08DSH \u8FDC\u7A0B SSH \u8FD0\u7EF4\uFF09\uFF1A\u4FA7\u8FB9\u680F\u300CSSH\u300D\u5165\u53E3\uFF1B\u5728 dsh-web-ui \u63D2\u4EF6\u5168\u5BB6\u6876\u4ED3\u5E93\uFF08packages/dsh-ssh\uFF09\u7EDF\u4E00\u7EF4\u62A4\u3002\u80FD\u529B\uFF1A\u4E3B\u673A\u914D\u7F6E\u5B58
|
|
41
|
+
export declare const SSH_GUIDANCE = "\u672C\u673A\u5DF2\u5B89\u88C5 dsh-ssh \u63D2\u4EF6\uFF08DSH \u8FDC\u7A0B SSH \u8FD0\u7EF4\uFF09\uFF1A\u4FA7\u8FB9\u680F\u300CSSH\u300D\u5165\u53E3\uFF1B\u5728 dsh-web-ui \u63D2\u4EF6\u5168\u5BB6\u6876\u4ED3\u5E93\uFF08packages/dsh-ssh\uFF09\u7EDF\u4E00\u7EF4\u62A4\u3002\u80FD\u529B\uFF1A\u4E3B\u673A\u914D\u7F6E\u5B58 $DSH_HOME/dsh-ssh.json\uFF08\u9ED8\u8BA4 ~/.dsh\uFF09\uFF08\u53EF\u4ECE ~/.ssh/config \u5BFC\u5165\uFF09\uFF1B\u6301\u4E45\u8FDE\u63A5\u6C60\u590D\u7528\u957F\u8FDE\u63A5\uFF08\u7A7A\u95F2 30 \u5206\u949F\u81EA\u52A8\u65AD\u5F00\uFF09\uFF1Bssh_list \u5217\u51FA\u4E3B\u673A\u3001ssh_exec \u6267\u884C\u8FDC\u7A0B\u547D\u4EE4\u3001ssh_upload/ssh_download \u4F20\u8F93\u6587\u4EF6\u3001ssh_tunnel \u672C\u5730\u7AEF\u53E3\u8F6C\u53D1\uFF08\u8BBF\u95EE\u8FDC\u7A0B\u6570\u636E\u5E93/\u5185\u7F51\u670D\u52A1\uFF09\u3001ssh_cluster \u96C6\u7FA4\u5E76\u53D1\u6267\u884C\uFF1B\u652F\u6301\u5BC6\u94A5/\u5BC6\u7801/ssh-agent \u8BA4\u8BC1\u3001passphrase \u5BC6\u94A5\u4E0E ProxyJump \u8DF3\u677F\u673A\uFF1BWeb \u7EC8\u7AEF\u8D70 WebSocket\u3002\u9650\u5236\uFF1A\u4E3B\u673A\u64CD\u4F5C\u7531\u7528\u6237\u5728 GUI \u4E2D\u914D\u7F6E\u540E agent \u65B9\u53EF\u4F7F\u7528\uFF1B\u5BC6\u7801\u4EE5\u660E\u6587\u5B58\u5728\u7528\u6237\u4E3B\u76EE\u5F55\u79C1\u6709\u6587\u4EF6\uFF08\u6743\u9650 0600\uFF09\uFF1B\u547D\u4EE4\u8F93\u51FA\u539F\u6837\u8FD4\u56DE\u3001\u53EF\u80FD\u542B\u654F\u611F\u4FE1\u606F\uFF1B\u65AD\u7EBF\u91CD\u8FDE\u53EF\u80FD\u91CD\u653E\u975E\u5E42\u7B49\u547D\u4EE4\uFF1B\u4F20\u8F93/\u6267\u884C\u6D88\u8017\u771F\u5B9E\u8FDC\u7A0B\u8D44\u6E90\uFF0C\u5148\u786E\u8BA4\u518D\u64CD\u4F5C\u3002\u8DEF\u5F84\u533A\u5206\uFF1A\u672C\u673A\uFF08dsh host\uFF09\u4E0A\u7684\u6587\u4EF6\u4E0E\u547D\u4EE4\u4E00\u5F8B\u7528\u672C\u5730\u5DE5\u5177\uFF08read / write / edit / bash\uFF09\uFF0Cssh_* \u5DE5\u5177\u53EA\u9488\u5BF9\u8FDC\u7A0B\u4E3B\u673A\u4E0A\u7684\u8DEF\u5F84\u3002\u7528\u6237\u63D0\u5230\u300CSSH / \u8FDC\u7A0B\u670D\u52A1\u5668 / \u670D\u52A1\u5668\u64CD\u4F5C / \u8DF3\u677F\u673A / \u96A7\u9053 / \u90E8\u7F72 / \u4E0A\u4F20\u4E0B\u8F7D\u300D\u65F6\u5373\u6307\u672C\u63D2\u4EF6\uFF0C\u8BF7\u636E\u6B64\u534F\u4F5C\u3002";
|
|
42
42
|
/**
|
|
43
43
|
* Mount the SSH engine, routes, tools, and announcement.
|
|
44
44
|
* @param ctx - host plugin context carrying webServer/tools/systemPrompt.
|
package/lib/types/protocol.d.ts
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
*/
|
|
6
6
|
/** Authentication flavors a host entry may carry. */
|
|
7
7
|
export type SshAuthKind = 'key' | 'password' | 'agent';
|
|
8
|
-
/** One stored host entry (the
|
|
8
|
+
/** One stored host entry (the $DSH_HOME/dsh-ssh.json store shape). */
|
|
9
9
|
export interface SshHostEntry {
|
|
10
10
|
/** Stable, user-chosen identifier used by every operation. */
|
|
11
11
|
alias: string;
|
package/lib/types/store.d.ts
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Host config store: one JSON file (
|
|
2
|
+
* Host config store: one JSON file (`$DSH_HOME/dsh-ssh.json`, defaulting
|
|
3
|
+
* to `~/.dsh`) holding every
|
|
3
4
|
* SSH host entry, written atomically (tmp + rename). Also parses the user's
|
|
4
5
|
* standard `~/.ssh/config` for one-shot import. Secrets (passwords,
|
|
5
6
|
* passphrases) live in this user-owned file in plaintext — same trust model
|
|
6
7
|
* as ssh-skill's annotated ssh-config comments; document it, never log it.
|
|
7
8
|
*/
|
|
8
9
|
import type { HostPayload, ImportResult, SshHostEntry, SshHostSummary } from './protocol.ts';
|
|
9
|
-
/** Store file location:
|
|
10
|
+
/** Store file location: $DSH_HOME/dsh-ssh.json (defaults to ~/.dsh). */
|
|
10
11
|
export declare function storePath(): string;
|
|
11
12
|
/** The user's standard OpenSSH config path. */
|
|
12
13
|
export declare function sshConfigPath(): string;
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@linxin666/dsh-ssh",
|
|
3
3
|
"description": "Remote SSH operations for the dsh web GUI: host config store (~/.dsh/dsh-ssh.json, import from ~/.ssh/config), a persistent ssh2 connection pool with jump-host support, exec / PTY web terminal / SFTP transfer / local port-forward tunnels / cluster execution, plus agent tools (ssh_list, ssh_exec, ssh_upload, ssh_download, ssh_tunnel, ssh_cluster). Hot-pluggable — mounted via ~/.dsh/cordis.patch.yml + a profile node_modules symlink, no dsh source changes.",
|
|
4
|
-
"version": "0.2.
|
|
4
|
+
"version": "0.2.9",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"engines": {
|
|
7
7
|
"node": "^22.19.0 || >=24.0.0"
|
|
@@ -25,6 +25,9 @@
|
|
|
25
25
|
"./package.json": "./package.json"
|
|
26
26
|
},
|
|
27
27
|
"dsh": {
|
|
28
|
+
"engines": {
|
|
29
|
+
"dsh": ">=0.1.1-rc.1"
|
|
30
|
+
},
|
|
28
31
|
"bundle": {
|
|
29
32
|
"patch": "./cordis.patch.yml"
|
|
30
33
|
},
|
|
@@ -49,18 +52,18 @@
|
|
|
49
52
|
},
|
|
50
53
|
"devDependencies": {
|
|
51
54
|
"@deepseek-ai/cordis": "^4.0.1",
|
|
52
|
-
"@deepseek-ai/dsh-client-connection": "^0.1.1-rc.
|
|
53
|
-
"@deepseek-ai/dsh-client-locale": "^0.1.1-rc.
|
|
54
|
-
"@deepseek-ai/dsh-client-runtime": "^0.1.1-rc.
|
|
55
|
-
"@deepseek-ai/dsh-client-ui-settings": "^0.1.1-rc.
|
|
56
|
-
"@deepseek-ai/dsh-client-ui-slots": "^0.1.1-rc.
|
|
57
|
-
"@deepseek-ai/dsh-host-webserver": "^0.1.1-rc.
|
|
58
|
-
"@deepseek-ai/dsh-llm": "^0.1.1-rc.
|
|
59
|
-
"@deepseek-ai/dsh-scope": "^0.1.1-rc.
|
|
60
|
-
"@deepseek-ai/dsh-settings": "^0.1.1-rc.
|
|
61
|
-
"@deepseek-ai/dsh-system-prompt": "^0.1.1-rc.
|
|
62
|
-
"@deepseek-ai/dsh-timeout": "^0.1.1-rc.
|
|
63
|
-
"@deepseek-ai/dsh-tools": "^0.1.1-rc.
|
|
55
|
+
"@deepseek-ai/dsh-client-connection": "^0.1.1-rc.2",
|
|
56
|
+
"@deepseek-ai/dsh-client-locale": "^0.1.1-rc.2",
|
|
57
|
+
"@deepseek-ai/dsh-client-runtime": "^0.1.1-rc.2",
|
|
58
|
+
"@deepseek-ai/dsh-client-ui-settings": "^0.1.1-rc.2",
|
|
59
|
+
"@deepseek-ai/dsh-client-ui-slots": "^0.1.1-rc.2",
|
|
60
|
+
"@deepseek-ai/dsh-host-webserver": "^0.1.1-rc.2",
|
|
61
|
+
"@deepseek-ai/dsh-llm": "^0.1.1-rc.2",
|
|
62
|
+
"@deepseek-ai/dsh-scope": "^0.1.1-rc.2",
|
|
63
|
+
"@deepseek-ai/dsh-settings": "^0.1.1-rc.2",
|
|
64
|
+
"@deepseek-ai/dsh-system-prompt": "^0.1.1-rc.2",
|
|
65
|
+
"@deepseek-ai/dsh-timeout": "^0.1.1-rc.2",
|
|
66
|
+
"@deepseek-ai/dsh-tools": "^0.1.1-rc.2",
|
|
64
67
|
"@types/node": "^22.20.0",
|
|
65
68
|
"@types/react": "~18.3.1",
|
|
66
69
|
"@types/react-dom": "^18.3.5",
|
package/src/client/locales.ts
CHANGED
|
@@ -62,7 +62,7 @@ export const zh = {
|
|
|
62
62
|
'form.keyPathHint': '如 ~/.ssh/id_ed25519',
|
|
63
63
|
'form.passphrase': '密钥口令',
|
|
64
64
|
'form.password': '密码',
|
|
65
|
-
'form.passwordHint': '明文保存在
|
|
65
|
+
'form.passwordHint': '明文保存在 $DSH_HOME/dsh-ssh.json(本机私有文件)',
|
|
66
66
|
'form.passwordRequired': '密码认证需要填写密码。',
|
|
67
67
|
'form.proxyJump': '跳板机别名',
|
|
68
68
|
'form.proxyJumpHint': '逗号分隔的本地主机别名,按顺序连接',
|
|
@@ -209,7 +209,7 @@ export const en: Record<keyof typeof zh, string> = {
|
|
|
209
209
|
'form.keyPathHint': 'e.g. ~/.ssh/id_ed25519',
|
|
210
210
|
'form.passphrase': 'Key passphrase',
|
|
211
211
|
'form.password': 'Password',
|
|
212
|
-
'form.passwordHint': 'Stored in plaintext in
|
|
212
|
+
'form.passwordHint': 'Stored in plaintext in $DSH_HOME/dsh-ssh.json (private local file)',
|
|
213
213
|
'form.passwordRequired': 'Password auth requires a password.',
|
|
214
214
|
'form.proxyJump': 'Jump host aliases',
|
|
215
215
|
'form.proxyJumpHint': 'Comma-separated local aliases, chained in order',
|
|
@@ -105,15 +105,21 @@ export function HostsTab({ api, onConnect }: HostsTabProps) {
|
|
|
105
105
|
return () => { clearTimeout(timer) }
|
|
106
106
|
}, [search, load])
|
|
107
107
|
|
|
108
|
+
// Every async setState path guards with mountedRef, not just load(): a
|
|
109
|
+
// promise settling after unmount would setState against the torn-down
|
|
110
|
+
// environment (window is not defined, main-CI flake).
|
|
108
111
|
const runTest = async (alias: string): Promise<void> => {
|
|
112
|
+
if (!mountedRef.current) return
|
|
109
113
|
setTestingAlias(alias)
|
|
110
114
|
try {
|
|
111
115
|
const result = await api.testHost(alias)
|
|
116
|
+
if (!mountedRef.current) return
|
|
112
117
|
setTestResults(prev => ({ ...prev, [alias]: result }))
|
|
113
118
|
} catch (cause) {
|
|
119
|
+
if (!mountedRef.current) return
|
|
114
120
|
setTestResults(prev => ({ ...prev, [alias]: { ok: false, error: errorMessage(cause) } }))
|
|
115
121
|
} finally {
|
|
116
|
-
setTestingAlias(null)
|
|
122
|
+
if (mountedRef.current) setTestingAlias(null)
|
|
117
123
|
}
|
|
118
124
|
}
|
|
119
125
|
|
|
@@ -121,32 +127,38 @@ export function HostsTab({ api, onConnect }: HostsTabProps) {
|
|
|
121
127
|
if (!window.confirm(tt('hosts.deleteConfirm', { alias }))) return
|
|
122
128
|
try {
|
|
123
129
|
await api.deleteHost(alias)
|
|
130
|
+
if (!mountedRef.current) return
|
|
124
131
|
void load()
|
|
125
132
|
} catch (cause) {
|
|
133
|
+
if (!mountedRef.current) return
|
|
126
134
|
setError(errorMessage(cause))
|
|
127
135
|
}
|
|
128
136
|
}
|
|
129
137
|
|
|
130
138
|
// Group-header batch action (#379): test every host in the group.
|
|
131
139
|
const testGroup = async (group: HostGroup): Promise<void> => {
|
|
140
|
+
if (!mountedRef.current) return
|
|
132
141
|
setTestingGroup(group.key)
|
|
133
142
|
try {
|
|
134
143
|
await Promise.all(group.hosts.map(host => runTest(host.alias)))
|
|
135
144
|
} finally {
|
|
136
|
-
setTestingGroup(null)
|
|
145
|
+
if (mountedRef.current) setTestingGroup(null)
|
|
137
146
|
}
|
|
138
147
|
}
|
|
139
148
|
|
|
140
149
|
const importConfig = async (): Promise<void> => {
|
|
150
|
+
if (!mountedRef.current) return
|
|
141
151
|
setImporting(true)
|
|
142
152
|
try {
|
|
143
153
|
const result = await api.importSshConfig()
|
|
154
|
+
if (!mountedRef.current) return
|
|
144
155
|
setNotice(tt('hosts.imported', { parsed: result.parsed, added: result.added, skipped: result.skipped }))
|
|
145
156
|
void load()
|
|
146
157
|
} catch (cause) {
|
|
158
|
+
if (!mountedRef.current) return
|
|
147
159
|
setError(errorMessage(cause))
|
|
148
160
|
} finally {
|
|
149
|
-
setImporting(false)
|
|
161
|
+
if (mountedRef.current) setImporting(false)
|
|
150
162
|
}
|
|
151
163
|
}
|
|
152
164
|
|
|
@@ -55,7 +55,7 @@ export function SshPanel({ controller, api, terminalFont }: SshPanelProps) {
|
|
|
55
55
|
}
|
|
56
56
|
|
|
57
57
|
return (
|
|
58
|
-
<div className={css.panel}>
|
|
58
|
+
<div className={css.panel} data-dsh-plugin="ssh">
|
|
59
59
|
<div className={css.panelHeader}>
|
|
60
60
|
<button
|
|
61
61
|
type="button"
|
|
@@ -70,7 +70,7 @@ export function SshPanel({ controller, api, terminalFont }: SshPanelProps) {
|
|
|
70
70
|
</div>
|
|
71
71
|
<div className={css.tabBar} role="tablist" data-dsh-part="tab-bar">
|
|
72
72
|
{TABS.map(tab => (
|
|
73
|
-
<button key={tab.id} type="button" role="tab" aria-selected={activeTab === tab.id} data-active={activeTab === tab.id ? '' : undefined} className={css.tab} onClick={() => { setActiveTab(tab.id) }}>
|
|
73
|
+
<button key={tab.id} type="button" role="tab" aria-selected={activeTab === tab.id} data-active={activeTab === tab.id ? '' : undefined} data-dsh-part="tab" className={css.tab} onClick={() => { setActiveTab(tab.id) }}>
|
|
74
74
|
{tab.label()}
|
|
75
75
|
</button>
|
|
76
76
|
))}
|
|
@@ -49,12 +49,13 @@ html[data-dsh-ssh-active]:not([data-dsh-taskboard-active]) [class*='centerCol']
|
|
|
49
49
|
/* --- sidebar entry row ------------------------------------------------------- */
|
|
50
50
|
|
|
51
51
|
.entry {
|
|
52
|
+
box-sizing: border-box;
|
|
52
53
|
display: flex;
|
|
53
54
|
align-items: center;
|
|
54
|
-
gap:
|
|
55
|
+
gap: 10px;
|
|
55
56
|
width: 100%;
|
|
56
|
-
height:
|
|
57
|
-
padding: 0
|
|
57
|
+
min-height: 36px;
|
|
58
|
+
padding: 0 10px;
|
|
58
59
|
background: transparent;
|
|
59
60
|
border: none;
|
|
60
61
|
border-radius: 8px;
|
|
@@ -79,9 +80,17 @@ html[data-dsh-ssh-active]:not([data-dsh-taskboard-active]) [class*='centerCol']
|
|
|
79
80
|
display: inline-flex;
|
|
80
81
|
align-items: center;
|
|
81
82
|
justify-content: center;
|
|
83
|
+
width: 24px;
|
|
84
|
+
height: 24px;
|
|
82
85
|
flex: none;
|
|
83
86
|
}
|
|
84
87
|
|
|
88
|
+
.entryIcon svg {
|
|
89
|
+
display: block;
|
|
90
|
+
width: 18px;
|
|
91
|
+
height: 18px;
|
|
92
|
+
}
|
|
93
|
+
|
|
85
94
|
.entryLabel {
|
|
86
95
|
overflow: hidden;
|
|
87
96
|
text-overflow: ellipsis;
|
|
@@ -91,7 +100,9 @@ html[data-dsh-ssh-active]:not([data-dsh-taskboard-active]) [class*='centerCol']
|
|
|
91
100
|
[data-dsh-frame][data-sidebar-collapsed] .entry {
|
|
92
101
|
justify-content: center;
|
|
93
102
|
padding: 0;
|
|
94
|
-
width:
|
|
103
|
+
width: 40px;
|
|
104
|
+
min-height: 40px;
|
|
105
|
+
margin-inline: auto;
|
|
95
106
|
}
|
|
96
107
|
|
|
97
108
|
[data-dsh-frame][data-sidebar-collapsed] .entryLabel {
|
|
@@ -16,8 +16,8 @@ import { mountSidebarEntry as mountSharedSidebarEntry } from './sidebar-entry-co
|
|
|
16
16
|
/** Stable data attribute identifying the injected entry row. */
|
|
17
17
|
export const ENTRY_SELECTOR = '[data-dsh-ssh-entry]'
|
|
18
18
|
|
|
19
|
-
/** Inline
|
|
20
|
-
const ICON = '<svg viewBox="0 0 16 16" width="
|
|
19
|
+
/** Inline terminal glyph sized to the shell's current sidebar navigation icons. */
|
|
20
|
+
const ICON = '<svg viewBox="0 0 16 16" width="18" height="18" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="1.75" y="2.25" width="12.5" height="11.5" rx="1.75"/><path d="M4.25 5.25l2.75 2.75-2.75 2.75"/><path d="M8.5 10.75h3.25"/></svg>'
|
|
21
21
|
|
|
22
22
|
/**
|
|
23
23
|
* Mount the sidebar entry, waiting for the shell to render and self-healing
|
package/src/dsh-home.ts
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
// Generated by scripts/sync-shared.mjs from shared/host/dsh-home.ts. Do not edit this copy; edit the shared source and run "node scripts/sync-shared.mjs".
|
|
2
|
+
/**
|
|
3
|
+
* DSH_HOME resolution shared by the plugin family's Host halves: the
|
|
4
|
+
* environment override wins, the platform home fallback follows. Mirrors
|
|
5
|
+
* what dsh-pet and dsh-liangshen each used to implement locally.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { homedir } from 'node:os'
|
|
9
|
+
import { isAbsolute, join } from 'node:path'
|
|
10
|
+
|
|
11
|
+
/** Expand a leading ~ (or ~user) in a path, platform-style. */
|
|
12
|
+
export function expandHome(path: string, home: string = homedir()): string {
|
|
13
|
+
if (path === '~') return home
|
|
14
|
+
if (path.startsWith('~/') || path.startsWith('~\\')) return join(home, path.slice(2))
|
|
15
|
+
return path
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Resolve the DSH home directory.
|
|
20
|
+
* @param env - process environment to read DSH_HOME from.
|
|
21
|
+
* @param home - platform home directory fallback (test seam).
|
|
22
|
+
* @returns the absolute DSH home path.
|
|
23
|
+
*/
|
|
24
|
+
export function resolveDshHome(env: NodeJS.ProcessEnv = process.env, home: string = homedir()): string {
|
|
25
|
+
const raw = env.DSH_HOME
|
|
26
|
+
if (raw !== undefined && raw.trim() !== '') {
|
|
27
|
+
const expanded = expandHome(raw.trim(), home)
|
|
28
|
+
return isAbsolute(expanded) ? expanded : join(process.cwd(), expanded)
|
|
29
|
+
}
|
|
30
|
+
return join(home, '.dsh')
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** Resolve the DSH home directory from the live environment. */
|
|
34
|
+
export function dshHome(): string {
|
|
35
|
+
return resolveDshHome()
|
|
36
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -64,7 +64,7 @@ const DEFAULT_ANNOUNCE = false
|
|
|
64
64
|
const SECTION_ORDER = 150
|
|
65
65
|
|
|
66
66
|
/** Model-facing announcement: plugin presence, capabilities, and limits. */
|
|
67
|
-
export const SSH_GUIDANCE = '本机已安装 dsh-ssh 插件(DSH 远程 SSH 运维):侧边栏「SSH」入口;在 dsh-web-ui 插件全家桶仓库(packages/dsh-ssh)统一维护。能力:主机配置存
|
|
67
|
+
export const SSH_GUIDANCE = '本机已安装 dsh-ssh 插件(DSH 远程 SSH 运维):侧边栏「SSH」入口;在 dsh-web-ui 插件全家桶仓库(packages/dsh-ssh)统一维护。能力:主机配置存 $DSH_HOME/dsh-ssh.json(默认 ~/.dsh)(可从 ~/.ssh/config 导入);持久连接池复用长连接(空闲 30 分钟自动断开);ssh_list 列出主机、ssh_exec 执行远程命令、ssh_upload/ssh_download 传输文件、ssh_tunnel 本地端口转发(访问远程数据库/内网服务)、ssh_cluster 集群并发执行;支持密钥/密码/ssh-agent 认证、passphrase 密钥与 ProxyJump 跳板机;Web 终端走 WebSocket。限制:主机操作由用户在 GUI 中配置后 agent 方可使用;密码以明文存在用户主目录私有文件(权限 0600);命令输出原样返回、可能含敏感信息;断线重连可能重放非幂等命令;传输/执行消耗真实远程资源,先确认再操作。路径区分:本机(dsh host)上的文件与命令一律用本地工具(read / write / edit / bash),ssh_* 工具只针对远程主机上的路径。用户提到「SSH / 远程服务器 / 服务器操作 / 跳板机 / 隧道 / 部署 / 上传下载」时即指本插件,请据此协作。'
|
|
68
68
|
|
|
69
69
|
/**
|
|
70
70
|
* Mount the SSH engine, routes, tools, and announcement.
|
package/src/protocol.ts
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
/** Authentication flavors a host entry may carry. */
|
|
8
8
|
export type SshAuthKind = 'key' | 'password' | 'agent'
|
|
9
9
|
|
|
10
|
-
/** One stored host entry (the
|
|
10
|
+
/** One stored host entry (the $DSH_HOME/dsh-ssh.json store shape). */
|
|
11
11
|
export interface SshHostEntry {
|
|
12
12
|
/** Stable, user-chosen identifier used by every operation. */
|
|
13
13
|
alias: string
|
package/src/store.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Host config store: one JSON file (
|
|
2
|
+
* Host config store: one JSON file (`$DSH_HOME/dsh-ssh.json`, defaulting
|
|
3
|
+
* to `~/.dsh`) holding every
|
|
3
4
|
* SSH host entry, written atomically (tmp + rename). Also parses the user's
|
|
4
5
|
* standard `~/.ssh/config` for one-shot import. Secrets (passwords,
|
|
5
6
|
* passphrases) live in this user-owned file in plaintext — same trust model
|
|
@@ -9,14 +10,15 @@
|
|
|
9
10
|
import { existsSync, mkdirSync, readFileSync, renameSync, statSync, writeFileSync } from 'node:fs'
|
|
10
11
|
import { homedir } from 'node:os'
|
|
11
12
|
import { dirname, join, resolve } from 'node:path'
|
|
13
|
+
import { dshHome } from './dsh-home.ts'
|
|
12
14
|
import type { HostPayload, ImportResult, SshHostEntry, SshHostSummary } from './protocol.ts'
|
|
13
15
|
|
|
14
16
|
/** File format version. */
|
|
15
17
|
const FORMAT_VERSION = 1
|
|
16
18
|
|
|
17
|
-
/** Store file location:
|
|
19
|
+
/** Store file location: $DSH_HOME/dsh-ssh.json (defaults to ~/.dsh). */
|
|
18
20
|
export function storePath(): string {
|
|
19
|
-
return join(
|
|
21
|
+
return join(dshHome(), 'dsh-ssh.json')
|
|
20
22
|
}
|
|
21
23
|
|
|
22
24
|
/** The user's standard OpenSSH config path. */
|
package/src/tools.ts
CHANGED
|
@@ -109,7 +109,7 @@ export function sshListTool(engine: SshEngine) {
|
|
|
109
109
|
export function sshExecTool(engine: SshEngine) {
|
|
110
110
|
return defineTool({
|
|
111
111
|
name: 'ssh_exec',
|
|
112
|
-
description: 'Execute a command on a
|
|
112
|
+
description: 'Execute a shell command on a REMOTE SSH host by alias; the command runs on the remote host, never on this machine. For commands on this machine, use the local bash tool. Prefer combining independent read-only queries into one command. ' +
|
|
113
113
|
'Triggers: run command on server, deploy, check server/status, service control, view logs, any remote operation.',
|
|
114
114
|
parameters: {
|
|
115
115
|
alias: { type: 'string', required: true, description: 'Host alias from ssh_list.' },
|
|
@@ -154,12 +154,12 @@ export function sshExecTool(engine: SshEngine) {
|
|
|
154
154
|
export function sshUploadTool(engine: SshEngine) {
|
|
155
155
|
return defineTool({
|
|
156
156
|
name: 'ssh_upload',
|
|
157
|
-
description: '
|
|
157
|
+
description: 'Transfer a file FROM this machine (the dsh host) TO a remote SSH host. Use this only when the file must be copied to the remote host. Files that stay on this machine are handled with the local file tools (read / write / edit), not ssh_upload. ' +
|
|
158
158
|
'Triggers: upload file to server, deploy artifact, copy config to server.',
|
|
159
159
|
parameters: {
|
|
160
160
|
alias: { type: 'string', required: true, description: 'Host alias from ssh_list.' },
|
|
161
|
-
localPath: { type: 'string', required: true, description: 'Absolute
|
|
162
|
-
remotePath: { type: 'string', required: true, description: '
|
|
161
|
+
localPath: { type: 'string', required: true, description: 'Absolute path of the source file on THIS machine (the dsh host) — not a path on the remote host.' },
|
|
162
|
+
remotePath: { type: 'string', required: true, description: 'Absolute destination path on the remote SSH host (parent dirs are created).' },
|
|
163
163
|
},
|
|
164
164
|
output: {
|
|
165
165
|
schema: {
|
|
@@ -191,12 +191,12 @@ export function sshUploadTool(engine: SshEngine) {
|
|
|
191
191
|
export function sshDownloadTool(engine: SshEngine) {
|
|
192
192
|
return defineTool({
|
|
193
193
|
name: 'ssh_download',
|
|
194
|
-
description: '
|
|
194
|
+
description: 'Copy a remote FILE from a configured SSH host to this machine (the dsh host). Use this only when the source is on the remote host; files already on this machine are read with the local file tools (read / write / edit), not ssh_download. Directory download is not supported — download files individually. ' +
|
|
195
195
|
'Triggers: download file from server, fetch remote log/artifact.',
|
|
196
196
|
parameters: {
|
|
197
197
|
alias: { type: 'string', required: true, description: 'Host alias from ssh_list.' },
|
|
198
|
-
remotePath: { type: 'string', required: true, description: '
|
|
199
|
-
localPath: { type: 'string', required: true, description: 'Absolute destination path on
|
|
198
|
+
remotePath: { type: 'string', required: true, description: 'Absolute path of the source file on the remote SSH host.' },
|
|
199
|
+
localPath: { type: 'string', required: true, description: 'Absolute destination path on THIS machine (the dsh host).' },
|
|
200
200
|
},
|
|
201
201
|
output: {
|
|
202
202
|
schema: {
|