@linxin666/dsh-ssh 0.3.5 → 0.3.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/lib/client.js +404 -301
- package/lib/client.js.map +1 -1
- package/lib/index.js +77 -24
- package/lib/types/client/api.d.ts +7 -0
- package/lib/types/client/index.d.ts +2 -1
- package/lib/types/client/locales.d.ts +6 -0
- package/lib/types/engine/connection-pool.d.ts +7 -2
- package/lib/types/engine/pty.d.ts +2 -2
- package/lib/types/engine.d.ts +3 -3
- package/lib/types/index.d.ts +2 -1
- package/lib/types/protocol.d.ts +11 -0
- package/package.json +17 -17
- package/src/client/api.ts +16 -1
- package/src/client/index.ts +4 -1
- package/src/client/locales.ts +12 -0
- package/src/client/panel/TerminalTab.tsx +61 -1
- package/src/dsh-home.ts +1 -1
- package/src/engine/connection-pool.ts +30 -4
- package/src/engine/pty.ts +8 -3
- package/src/engine.ts +8 -3
- package/src/index.ts +27 -14
- package/src/protocol.ts +7 -0
- package/src/routes.ts +33 -2
package/lib/index.js
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { installSettingsSection, settingsNamespace } from "@deepseek-ai/dsh-settings";
|
|
2
1
|
import { closeSync, createReadStream, createWriteStream, existsSync, lstatSync, mkdirSync, openSync, readFileSync, readdirSync, renameSync, statSync, writeFileSync } from "node:fs";
|
|
3
2
|
import { Client } from "ssh2";
|
|
4
3
|
import { homedir, tmpdir } from "node:os";
|
|
@@ -1408,7 +1407,8 @@ function buildConnectConfig(entry, sock, opts) {
|
|
|
1408
1407
|
username: entry.user,
|
|
1409
1408
|
readyTimeout: opts.connectTimeoutMs,
|
|
1410
1409
|
keepaliveInterval: opts.keepaliveIntervalMs,
|
|
1411
|
-
keepaliveCountMax: 3
|
|
1410
|
+
keepaliveCountMax: 3,
|
|
1411
|
+
tryKeyboard: true
|
|
1412
1412
|
};
|
|
1413
1413
|
if (sock !== void 0) config.sock = sock;
|
|
1414
1414
|
if (entry.auth.kind === "password") config.password = entry.auth.password;
|
|
@@ -1433,7 +1433,7 @@ function resolveAgentPath(agentPath) {
|
|
|
1433
1433
|
if (process.platform === "win32") return "pageant";
|
|
1434
1434
|
}
|
|
1435
1435
|
/** Connect one ssh2 client (resolve on ready, reject on error/close). */
|
|
1436
|
-
function connectClient(config) {
|
|
1436
|
+
function connectClient(config, onKeyboardInteractive) {
|
|
1437
1437
|
return new Promise((resolve, reject) => {
|
|
1438
1438
|
const client = new Client();
|
|
1439
1439
|
let settled = false;
|
|
@@ -1450,6 +1450,20 @@ function connectClient(config) {
|
|
|
1450
1450
|
settled = true;
|
|
1451
1451
|
resolve(client);
|
|
1452
1452
|
});
|
|
1453
|
+
client.on("keyboard-interactive", (name, instructions, instructionsLang, prompts, finish) => {
|
|
1454
|
+
if (onKeyboardInteractive !== void 0) {
|
|
1455
|
+
onKeyboardInteractive(name, instructions, instructionsLang, prompts.map((p) => ({
|
|
1456
|
+
prompt: p.prompt,
|
|
1457
|
+
echo: Boolean(p.echo)
|
|
1458
|
+
})), finish);
|
|
1459
|
+
return;
|
|
1460
|
+
}
|
|
1461
|
+
if (config.password !== void 0 && prompts.length > 0 && prompts.every((p) => /password/i.test(p.prompt))) {
|
|
1462
|
+
finish(prompts.map(() => config.password));
|
|
1463
|
+
return;
|
|
1464
|
+
}
|
|
1465
|
+
fail(/* @__PURE__ */ new Error("Authentication failed (keyboard-interactive): " + (prompts.map((p) => p.prompt.trim()).join(", ") || "unsupported interactive challenge")));
|
|
1466
|
+
});
|
|
1453
1467
|
client.on("error", fail);
|
|
1454
1468
|
try {
|
|
1455
1469
|
client.connect(config);
|
|
@@ -1475,7 +1489,7 @@ function appendOutput(target, chunk, maxBytes) {
|
|
|
1475
1489
|
* order, each forwarding a stream to the next destination, ending with the
|
|
1476
1490
|
* target client. Shared by the pool and standalone shell sessions.
|
|
1477
1491
|
*/
|
|
1478
|
-
async function connectChain(engine, entry) {
|
|
1492
|
+
async function connectChain(engine, entry, onKeyboardInteractive) {
|
|
1479
1493
|
const hops = [];
|
|
1480
1494
|
let sock;
|
|
1481
1495
|
const chain = entry.proxyJump;
|
|
@@ -1484,7 +1498,7 @@ async function connectChain(engine, entry) {
|
|
|
1484
1498
|
const hopAlias = chain[index];
|
|
1485
1499
|
const hop = engine.store.find(hopAlias);
|
|
1486
1500
|
if (hop === void 0) throw new Error("proxyJump alias '" + hopAlias + "' not found — create it first");
|
|
1487
|
-
const hopClient = await connectClient(buildConnectConfig(hop, sock, engine.opts));
|
|
1501
|
+
const hopClient = await connectClient(buildConnectConfig(hop, sock, engine.opts), onKeyboardInteractive);
|
|
1488
1502
|
hops.push(hopClient);
|
|
1489
1503
|
const next = index + 1 < chain.length ? engine.store.find(chain[index + 1]) : void 0;
|
|
1490
1504
|
const nextHost = next !== void 0 ? next.host : entry.host;
|
|
@@ -1502,7 +1516,7 @@ async function connectChain(engine, entry) {
|
|
|
1502
1516
|
}
|
|
1503
1517
|
let target;
|
|
1504
1518
|
try {
|
|
1505
|
-
target = await connectClient(buildConnectConfig(entry, sock, engine.opts));
|
|
1519
|
+
target = await connectClient(buildConnectConfig(entry, sock, engine.opts), onKeyboardInteractive);
|
|
1506
1520
|
return {
|
|
1507
1521
|
client: target,
|
|
1508
1522
|
hops
|
|
@@ -1688,10 +1702,10 @@ async function execCommand(engine, alias, command, timeoutMs) {
|
|
|
1688
1702
|
* The shell is a long-lived exclusive stream: it uses its own connection so
|
|
1689
1703
|
* closing it can never tear down a pooled exec/tunnel sharing the alias.
|
|
1690
1704
|
*/
|
|
1691
|
-
async function openShell(engine, alias, size) {
|
|
1705
|
+
async function openShell(engine, alias, size, onKeyboardInteractive) {
|
|
1692
1706
|
const entry = engine.store.find(alias);
|
|
1693
1707
|
if (entry === void 0) throw new Error("alias '" + alias + "' not found — add it first");
|
|
1694
|
-
const { client, hops } = await connectChain(engine, entry);
|
|
1708
|
+
const { client, hops } = await connectChain(engine, entry, onKeyboardInteractive);
|
|
1695
1709
|
return await new Promise((resolve, reject) => {
|
|
1696
1710
|
client.shell({
|
|
1697
1711
|
term: "xterm-256color",
|
|
@@ -2161,8 +2175,8 @@ var SshEngine = class {
|
|
|
2161
2175
|
return cluster(this, options);
|
|
2162
2176
|
}
|
|
2163
2177
|
/** Open a PTY shell session for the web terminal (standalone connection). */
|
|
2164
|
-
async openShell(alias, size) {
|
|
2165
|
-
return openShell(this, alias, size);
|
|
2178
|
+
async openShell(alias, size, onKeyboardInteractive) {
|
|
2179
|
+
return openShell(this, alias, size, onKeyboardInteractive);
|
|
2166
2180
|
}
|
|
2167
2181
|
/** Upload one local file (or directory tree) to a remote path. */
|
|
2168
2182
|
async upload(alias, localPath, remotePath, recursive, onProgress) {
|
|
@@ -2796,6 +2810,7 @@ function makeRoutes(deps) {
|
|
|
2796
2810
|
let session;
|
|
2797
2811
|
let closed = false;
|
|
2798
2812
|
let paused = false;
|
|
2813
|
+
let pendingAuthFinish;
|
|
2799
2814
|
const resume = () => {
|
|
2800
2815
|
if (paused && ws.bufferedAmount < BACKPRESSURE_LOW_WATER) {
|
|
2801
2816
|
paused = false;
|
|
@@ -2811,14 +2826,39 @@ function makeRoutes(deps) {
|
|
|
2811
2826
|
}
|
|
2812
2827
|
};
|
|
2813
2828
|
const closeSession = () => {
|
|
2829
|
+
if (pendingAuthFinish !== void 0) {
|
|
2830
|
+
const fn = pendingAuthFinish;
|
|
2831
|
+
pendingAuthFinish = void 0;
|
|
2832
|
+
try {
|
|
2833
|
+
fn([]);
|
|
2834
|
+
} catch {}
|
|
2835
|
+
}
|
|
2814
2836
|
const opened = session;
|
|
2815
2837
|
session = void 0;
|
|
2816
2838
|
if (opened !== void 0) opened.close();
|
|
2817
2839
|
};
|
|
2840
|
+
const onKeyboardInteractive = (name, instructions, _lang, prompts, finish) => {
|
|
2841
|
+
const entry = store.find(alias);
|
|
2842
|
+
if (entry?.auth.kind === "password" && entry.auth.password !== void 0 && prompts.length > 0 && prompts.every((p) => /password/i.test(p.prompt))) {
|
|
2843
|
+
const password = entry.auth.password;
|
|
2844
|
+
finish(prompts.map(() => password));
|
|
2845
|
+
return;
|
|
2846
|
+
}
|
|
2847
|
+
pendingAuthFinish = finish;
|
|
2848
|
+
sendFrame({
|
|
2849
|
+
type: "auth_prompt",
|
|
2850
|
+
name,
|
|
2851
|
+
instructions,
|
|
2852
|
+
prompts: prompts.map((p) => ({
|
|
2853
|
+
prompt: p.prompt,
|
|
2854
|
+
echo: p.echo
|
|
2855
|
+
}))
|
|
2856
|
+
});
|
|
2857
|
+
};
|
|
2818
2858
|
engine.openShell(alias, {
|
|
2819
2859
|
cols: Number.isFinite(cols) ? cols : 80,
|
|
2820
2860
|
rows: Number.isFinite(rows) ? rows : 24
|
|
2821
|
-
}).then((opened) => {
|
|
2861
|
+
}, onKeyboardInteractive).then((opened) => {
|
|
2822
2862
|
if (ws.readyState !== WebSocket.OPEN) {
|
|
2823
2863
|
opened.close();
|
|
2824
2864
|
return;
|
|
@@ -2863,6 +2903,13 @@ function makeRoutes(deps) {
|
|
|
2863
2903
|
}
|
|
2864
2904
|
if (frame.type === "input") session?.send(frame.data);
|
|
2865
2905
|
else if (frame.type === "resize") session?.resize(Math.max(2, frame.cols), Math.max(1, frame.rows));
|
|
2906
|
+
else if (frame.type === "auth_response") {
|
|
2907
|
+
if (pendingAuthFinish !== void 0) {
|
|
2908
|
+
const fn = pendingAuthFinish;
|
|
2909
|
+
pendingAuthFinish = void 0;
|
|
2910
|
+
fn(frame.responses);
|
|
2911
|
+
}
|
|
2912
|
+
}
|
|
2866
2913
|
});
|
|
2867
2914
|
ws.on("close", () => {
|
|
2868
2915
|
closed = true;
|
|
@@ -3512,7 +3559,7 @@ const inject = [
|
|
|
3512
3559
|
* surface edits. Spelled here rather than imported: the browser half spells
|
|
3513
3560
|
* the same value and must not depend on a Host package.
|
|
3514
3561
|
*/
|
|
3515
|
-
const SSH_SETTINGS_NAMESPACE =
|
|
3562
|
+
const SSH_SETTINGS_NAMESPACE = "dsh-ssh";
|
|
3516
3563
|
const Config = import_lib.default.object({
|
|
3517
3564
|
announceToAgent: import_lib.default.boolean().default(false),
|
|
3518
3565
|
enabled: import_lib.default.boolean().default(true),
|
|
@@ -3532,18 +3579,21 @@ const SSH_GUIDANCE = "本机已安装 dsh-ssh 插件(DSH 远程 SSH 运维)
|
|
|
3532
3579
|
const apply = mountOnce("@linxin666/dsh-ssh", applyImpl);
|
|
3533
3580
|
function applyImpl(ctx, config) {
|
|
3534
3581
|
let current = () => config ?? {};
|
|
3582
|
+
let isSettingsBound = false;
|
|
3583
|
+
const store = new HostStore();
|
|
3584
|
+
const engine = new SshEngine(store);
|
|
3585
|
+
ctx.effect(() => () => {
|
|
3586
|
+
engine.dispose();
|
|
3587
|
+
}, "dsh-ssh: engine");
|
|
3535
3588
|
const resolve = () => {
|
|
3536
3589
|
const value = current();
|
|
3590
|
+
let enabled = value.enabled ?? true;
|
|
3591
|
+
if (enabled === false && !isSettingsBound && store.list().length > 0) enabled = true;
|
|
3537
3592
|
return {
|
|
3538
3593
|
announceToAgent: value.announceToAgent ?? DEFAULT_ANNOUNCE,
|
|
3539
|
-
enabled
|
|
3594
|
+
enabled
|
|
3540
3595
|
};
|
|
3541
3596
|
};
|
|
3542
|
-
const store = new HostStore();
|
|
3543
|
-
const engine = new SshEngine(store);
|
|
3544
|
-
ctx.effect(() => () => {
|
|
3545
|
-
engine.dispose();
|
|
3546
|
-
}, "dsh-ssh: engine");
|
|
3547
3597
|
const { routes, upgrade } = makeRoutes({
|
|
3548
3598
|
store,
|
|
3549
3599
|
engine
|
|
@@ -3594,12 +3644,15 @@ function applyImpl(ctx, config) {
|
|
|
3594
3644
|
};
|
|
3595
3645
|
}, "dsh-ssh: tools");
|
|
3596
3646
|
};
|
|
3597
|
-
|
|
3598
|
-
|
|
3599
|
-
|
|
3600
|
-
|
|
3601
|
-
|
|
3602
|
-
|
|
3647
|
+
ctx.inject(["settings"], (settingsCtx) => {
|
|
3648
|
+
settingsCtx.settings.installSection(ctx, SSH_SETTINGS_NAMESPACE, Config, config ?? {}, {
|
|
3649
|
+
setSource: (source) => {
|
|
3650
|
+
isSettingsBound = true;
|
|
3651
|
+
current = source;
|
|
3652
|
+
sync();
|
|
3653
|
+
},
|
|
3654
|
+
onChange: sync
|
|
3655
|
+
});
|
|
3603
3656
|
});
|
|
3604
3657
|
sync();
|
|
3605
3658
|
}
|
|
@@ -15,10 +15,17 @@ export interface TerminalConnection {
|
|
|
15
15
|
onOutput: ((data: string) => void) | undefined;
|
|
16
16
|
/** Fired on the exit frame (or transport error). */
|
|
17
17
|
onExit: ((code: number | null, error?: string) => void) | undefined;
|
|
18
|
+
/** Fired when the server requests keyboard-interactive 2FA. */
|
|
19
|
+
onAuthPrompt: ((name: string, instructions: string, prompts: Array<{
|
|
20
|
+
prompt: string;
|
|
21
|
+
echo: boolean;
|
|
22
|
+
}>) => void) | undefined;
|
|
18
23
|
/** Send raw input to the remote shell. */
|
|
19
24
|
send(data: string): void;
|
|
20
25
|
/** Resize the remote PTY. */
|
|
21
26
|
resize(cols: number, rows: number): void;
|
|
27
|
+
/** Send interactive 2FA response codes to the remote server. */
|
|
28
|
+
sendAuthResponse(responses: string[]): void;
|
|
22
29
|
/** Close the socket and the remote session. */
|
|
23
30
|
close(): void;
|
|
24
31
|
}
|
|
@@ -10,7 +10,8 @@
|
|
|
10
10
|
* Export discipline (packages/client rule): the /client surface carries what
|
|
11
11
|
* cordis loading needs plus types only — all value exports stay internal.
|
|
12
12
|
*/
|
|
13
|
-
import type {
|
|
13
|
+
import type { Context as ClientContext } from '@deepseek-ai/cordis';
|
|
14
|
+
import type { SettingsScope, SettingsScopeSpec } from '@deepseek-ai/dsh-client-ui-settings/client';
|
|
14
15
|
import { type SshKey } from './locales.ts';
|
|
15
16
|
declare module '@deepseek-ai/cordis' {
|
|
16
17
|
interface Context {
|
|
@@ -81,6 +81,11 @@ export declare const zh: {
|
|
|
81
81
|
readonly 'terminal.ready': "终端已连接({alias})";
|
|
82
82
|
readonly 'terminal.exited': "终端已退出({alias})";
|
|
83
83
|
readonly 'terminal.error': "终端错误:{error}";
|
|
84
|
+
readonly 'terminal.auth.title': "二次身份验证 (2FA / 交互式认证)";
|
|
85
|
+
readonly 'terminal.auth.submit': "提交验证码";
|
|
86
|
+
readonly 'terminal.auth.cancel': "取消";
|
|
87
|
+
readonly 'terminal.auth.placeholder': "请输入验证码或口令";
|
|
88
|
+
readonly 'terminal.auth.hint': "服务器请求进行交互式二次验证(输入仅本次生效,不落盘):";
|
|
84
89
|
readonly 'transfer.selectHost': "选择主机";
|
|
85
90
|
readonly 'transfer.remotePath': "远程路径";
|
|
86
91
|
readonly 'transfer.remotePathHint': "如 /tmp/ 或 /home/user/app.tar.gz";
|
|
@@ -141,6 +146,7 @@ export declare const zh: {
|
|
|
141
146
|
readonly 'common.error': "出错:{error}";
|
|
142
147
|
readonly 'common.refresh': "刷新";
|
|
143
148
|
readonly 'common.confirm': "确定";
|
|
149
|
+
readonly 'error.disabled': "SSH 插件在宿主端未启用。请前往「设置 → Web 插件 → SSH」打开 enabled 开关。";
|
|
144
150
|
};
|
|
145
151
|
export declare const en: Record<keyof typeof zh, string>;
|
|
146
152
|
/** Locale key union. */
|
|
@@ -50,8 +50,13 @@ export interface PoolEngine {
|
|
|
50
50
|
export declare function buildConnectConfig(entry: SshHostEntry, sock: ConnectConfig['sock'] | undefined, opts: Required<EngineOptions>): ConnectConfig;
|
|
51
51
|
/** Resolve the ssh2 agent path for 'agent' auth. */
|
|
52
52
|
export declare function resolveAgentPath(agentPath?: string): string | undefined;
|
|
53
|
+
/** Keyboard-interactive handler callback for 2FA / dynamic prompt flow. */
|
|
54
|
+
export type KeyboardInteractiveHandler = (name: string, instructions: string, instructionsLang: string, prompts: Array<{
|
|
55
|
+
prompt: string;
|
|
56
|
+
echo: boolean;
|
|
57
|
+
}>, finish: (responses: string[]) => void) => void;
|
|
53
58
|
/** Connect one ssh2 client (resolve on ready, reject on error/close). */
|
|
54
|
-
export declare function connectClient(config: ConnectConfig): Promise<Client>;
|
|
59
|
+
export declare function connectClient(config: ConnectConfig, onKeyboardInteractive?: KeyboardInteractiveHandler): Promise<Client>;
|
|
55
60
|
/** Cap captured output at the configured byte budget (marks truncation). */
|
|
56
61
|
export declare function appendOutput(target: {
|
|
57
62
|
text: string;
|
|
@@ -62,7 +67,7 @@ export declare function appendOutput(target: {
|
|
|
62
67
|
* order, each forwarding a stream to the next destination, ending with the
|
|
63
68
|
* target client. Shared by the pool and standalone shell sessions.
|
|
64
69
|
*/
|
|
65
|
-
export declare function connectChain(engine: PoolEngine, entry: SshHostEntry): Promise<{
|
|
70
|
+
export declare function connectChain(engine: PoolEngine, entry: SshHostEntry, onKeyboardInteractive?: KeyboardInteractiveHandler): Promise<{
|
|
66
71
|
client: Client;
|
|
67
72
|
hops: Client[];
|
|
68
73
|
}>;
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* ssh2 connection with a long-lived shell channel, resize, and pausable
|
|
4
4
|
* output delivery.
|
|
5
5
|
*/
|
|
6
|
-
import { type PoolEngine } from './connection-pool.ts';
|
|
6
|
+
import { type KeyboardInteractiveHandler, type PoolEngine } from './connection-pool.ts';
|
|
7
7
|
/** A live PTY shell session. */
|
|
8
8
|
export interface ShellSession {
|
|
9
9
|
/** Assign to receive remote output. */
|
|
@@ -29,4 +29,4 @@ export interface ShellSession {
|
|
|
29
29
|
export declare function openShell(engine: PoolEngine, alias: string, size: {
|
|
30
30
|
cols: number;
|
|
31
31
|
rows: number;
|
|
32
|
-
}): Promise<ShellSession>;
|
|
32
|
+
}, onKeyboardInteractive?: KeyboardInteractiveHandler): Promise<ShellSession>;
|
package/lib/types/engine.d.ts
CHANGED
|
@@ -7,10 +7,10 @@
|
|
|
7
7
|
*/
|
|
8
8
|
import type { ClusterResult, ExecResult, SshHostSummary, TestResult, TransferProgress, TunnelInfo } from './protocol.ts';
|
|
9
9
|
import type { HostStore } from './store.ts';
|
|
10
|
-
import { type EngineOptions, type PoolRecord } from './engine/connection-pool.ts';
|
|
10
|
+
import { type EngineOptions, type KeyboardInteractiveHandler, type PoolRecord } from './engine/connection-pool.ts';
|
|
11
11
|
import { type ShellSession } from './engine/pty.ts';
|
|
12
12
|
import { type TunnelRecord } from './engine/tunnel.ts';
|
|
13
|
-
export type { EngineOptions } from './engine/connection-pool.ts';
|
|
13
|
+
export type { EngineOptions, KeyboardInteractiveHandler } from './engine/connection-pool.ts';
|
|
14
14
|
export type { ShellSession } from './engine/pty.ts';
|
|
15
15
|
export type { PoolRecord } from './engine/connection-pool.ts';
|
|
16
16
|
export type { TunnelRecord } from './engine/tunnel.ts';
|
|
@@ -50,7 +50,7 @@ export declare class SshEngine {
|
|
|
50
50
|
openShell(alias: string, size: {
|
|
51
51
|
cols: number;
|
|
52
52
|
rows: number;
|
|
53
|
-
}): Promise<ShellSession>;
|
|
53
|
+
}, onKeyboardInteractive?: KeyboardInteractiveHandler): Promise<ShellSession>;
|
|
54
54
|
/** Upload one local file (or directory tree) to a remote path. */
|
|
55
55
|
upload(alias: string, localPath: string, remotePath: string, recursive: boolean, onProgress?: (progress: TransferProgress) => void): Promise<{
|
|
56
56
|
bytes: number;
|
package/lib/types/index.d.ts
CHANGED
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
* no dsh source changes.
|
|
9
9
|
*/
|
|
10
10
|
import type { Context } from '@deepseek-ai/cordis';
|
|
11
|
+
import type { SettingsNamespace } from '@deepseek-ai/dsh-settings';
|
|
11
12
|
import z from 'schemastery';
|
|
12
13
|
/** Stable cordis plugin name. */
|
|
13
14
|
export declare const name = "ssh";
|
|
@@ -18,7 +19,7 @@ export declare const inject: string[];
|
|
|
18
19
|
* surface edits. Spelled here rather than imported: the browser half spells
|
|
19
20
|
* the same value and must not depend on a Host package.
|
|
20
21
|
*/
|
|
21
|
-
export declare const SSH_SETTINGS_NAMESPACE:
|
|
22
|
+
export declare const SSH_SETTINGS_NAMESPACE: SettingsNamespace;
|
|
22
23
|
/** Plugin config, validated by the same-named schemastery schema. */
|
|
23
24
|
export interface Config {
|
|
24
25
|
/**
|
package/lib/types/protocol.d.ts
CHANGED
|
@@ -160,6 +160,14 @@ export type TerminalServerFrame = {
|
|
|
160
160
|
type: 'exit';
|
|
161
161
|
code: number | null;
|
|
162
162
|
error?: string;
|
|
163
|
+
} | {
|
|
164
|
+
type: 'auth_prompt';
|
|
165
|
+
name: string;
|
|
166
|
+
instructions: string;
|
|
167
|
+
prompts: Array<{
|
|
168
|
+
prompt: string;
|
|
169
|
+
echo: boolean;
|
|
170
|
+
}>;
|
|
163
171
|
};
|
|
164
172
|
export type TerminalClientFrame = {
|
|
165
173
|
type: 'input';
|
|
@@ -168,6 +176,9 @@ export type TerminalClientFrame = {
|
|
|
168
176
|
type: 'resize';
|
|
169
177
|
cols: number;
|
|
170
178
|
rows: number;
|
|
179
|
+
} | {
|
|
180
|
+
type: 'auth_response';
|
|
181
|
+
responses: string[];
|
|
171
182
|
};
|
|
172
183
|
/** Route paths the client calls (shared literals). */
|
|
173
184
|
export declare const SSH_API_BASE: "/api/dsh-ssh";
|
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.3.
|
|
4
|
+
"version": "0.3.9",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"engines": {
|
|
7
7
|
"node": "^22.19.0 || >=24.0.0"
|
|
@@ -26,15 +26,15 @@
|
|
|
26
26
|
},
|
|
27
27
|
"dsh": {
|
|
28
28
|
"engines": {
|
|
29
|
-
"dsh": ">=0.1.
|
|
29
|
+
"dsh": ">=0.1.2-alpha.1"
|
|
30
30
|
},
|
|
31
31
|
"bundle": {
|
|
32
32
|
"patch": "./cordis.patch.yml"
|
|
33
33
|
},
|
|
34
34
|
"client": {
|
|
35
35
|
"inject": [
|
|
36
|
-
"@deepseek-ai/dsh-client-
|
|
37
|
-
"@deepseek-ai/dsh-client-
|
|
36
|
+
"@deepseek-ai/dsh-client-locale",
|
|
37
|
+
"@deepseek-ai/dsh-client-ui-renderer",
|
|
38
38
|
"@deepseek-ai/dsh-client-ui-settings"
|
|
39
39
|
],
|
|
40
40
|
"platform": "web"
|
|
@@ -51,19 +51,19 @@
|
|
|
51
51
|
"react-dom": "^18.2.0"
|
|
52
52
|
},
|
|
53
53
|
"devDependencies": {
|
|
54
|
-
"@deepseek-ai/cordis": "^4.0.
|
|
55
|
-
"@deepseek-ai/dsh-client-connection": "^0.1.
|
|
56
|
-
"@deepseek-ai/dsh-client-locale": "^0.1.
|
|
57
|
-
"@deepseek-ai/dsh-client-
|
|
58
|
-
"@deepseek-ai/dsh-client-ui-settings": "^0.1.
|
|
59
|
-
"@deepseek-ai/dsh-client-ui-slots": "^0.1.
|
|
60
|
-
"@deepseek-ai/dsh-host-webserver": "^0.1.
|
|
61
|
-
"@deepseek-ai/dsh-llm": "^0.1.
|
|
62
|
-
"@deepseek-ai/dsh-scope": "^0.1.
|
|
63
|
-
"@deepseek-ai/dsh-settings": "^0.1.
|
|
64
|
-
"@deepseek-ai/dsh-system-prompt": "^0.1.
|
|
65
|
-
"@deepseek-ai/dsh-timeout": "^0.1.
|
|
66
|
-
"@deepseek-ai/dsh-tools": "^0.1.
|
|
54
|
+
"@deepseek-ai/cordis": "^4.0.2",
|
|
55
|
+
"@deepseek-ai/dsh-client-connection": "^0.1.2-alpha.2",
|
|
56
|
+
"@deepseek-ai/dsh-client-locale": "^0.1.2-alpha.2",
|
|
57
|
+
"@deepseek-ai/dsh-client-ui-renderer": "^0.1.2-alpha.2",
|
|
58
|
+
"@deepseek-ai/dsh-client-ui-settings": "^0.1.2-alpha.2",
|
|
59
|
+
"@deepseek-ai/dsh-client-ui-slots": "^0.1.2-alpha.2",
|
|
60
|
+
"@deepseek-ai/dsh-host-webserver": "^0.1.2-alpha.2",
|
|
61
|
+
"@deepseek-ai/dsh-llm": "^0.1.2-alpha.2",
|
|
62
|
+
"@deepseek-ai/dsh-scope": "^0.1.2-alpha.2",
|
|
63
|
+
"@deepseek-ai/dsh-settings": "^0.1.2-alpha.2",
|
|
64
|
+
"@deepseek-ai/dsh-system-prompt": "^0.1.2-alpha.2",
|
|
65
|
+
"@deepseek-ai/dsh-timeout": "^0.1.2-alpha.2",
|
|
66
|
+
"@deepseek-ai/dsh-tools": "^0.1.2-alpha.2",
|
|
67
67
|
"@types/node": "^22.20.0",
|
|
68
68
|
"@types/react": "~18.3.1",
|
|
69
69
|
"@types/react-dom": "^18.3.5",
|
package/src/client/api.ts
CHANGED
|
@@ -18,6 +18,7 @@ import {
|
|
|
18
18
|
type TransferStreamLine,
|
|
19
19
|
type TunnelInfo,
|
|
20
20
|
} from '../protocol.ts'
|
|
21
|
+
import { tt } from './panel/helpers.ts'
|
|
21
22
|
|
|
22
23
|
/** Minimal File System Access API surface (not in all lib.dom versions). */
|
|
23
24
|
interface WindowWithFileSystemAccess {
|
|
@@ -51,12 +52,15 @@ async function readJson<T>(response: Response): Promise<T> {
|
|
|
51
52
|
try {
|
|
52
53
|
body = await response.json()
|
|
53
54
|
} catch {
|
|
55
|
+
if (response.status === 404) {
|
|
56
|
+
throw new SshApiError(tt('error.disabled'))
|
|
57
|
+
}
|
|
54
58
|
throw new SshApiError(`HTTP ${response.status}: invalid JSON response`)
|
|
55
59
|
}
|
|
56
60
|
if (!response.ok) {
|
|
57
61
|
const message = typeof body === 'object' && body !== null && typeof (body as { error?: unknown }).error === 'string'
|
|
58
62
|
? (body as { error: string }).error
|
|
59
|
-
: `HTTP ${response.status}`
|
|
63
|
+
: (response.status === 404 ? tt('error.disabled') : `HTTP ${response.status}`)
|
|
60
64
|
throw new SshApiError(message)
|
|
61
65
|
}
|
|
62
66
|
return body as T
|
|
@@ -80,10 +84,14 @@ export interface TerminalConnection {
|
|
|
80
84
|
onOutput: ((data: string) => void) | undefined
|
|
81
85
|
/** Fired on the exit frame (or transport error). */
|
|
82
86
|
onExit: ((code: number | null, error?: string) => void) | undefined
|
|
87
|
+
/** Fired when the server requests keyboard-interactive 2FA. */
|
|
88
|
+
onAuthPrompt: ((name: string, instructions: string, prompts: Array<{ prompt: string; echo: boolean }>) => void) | undefined
|
|
83
89
|
/** Send raw input to the remote shell. */
|
|
84
90
|
send(data: string): void
|
|
85
91
|
/** Resize the remote PTY. */
|
|
86
92
|
resize(cols: number, rows: number): void
|
|
93
|
+
/** Send interactive 2FA response codes to the remote server. */
|
|
94
|
+
sendAuthResponse(responses: string[]): void
|
|
87
95
|
/** Close the socket and the remote session. */
|
|
88
96
|
close(): void
|
|
89
97
|
}
|
|
@@ -345,6 +353,7 @@ export class SshApi {
|
|
|
345
353
|
onReady: undefined,
|
|
346
354
|
onOutput: undefined,
|
|
347
355
|
onExit: undefined,
|
|
356
|
+
onAuthPrompt: undefined,
|
|
348
357
|
send: (data) => {
|
|
349
358
|
if (socket.readyState === WebSocket.OPEN) {
|
|
350
359
|
socket.send(JSON.stringify({ type: 'input', data } satisfies TerminalClientFrame))
|
|
@@ -355,6 +364,11 @@ export class SshApi {
|
|
|
355
364
|
socket.send(JSON.stringify({ type: 'resize', cols, rows } satisfies TerminalClientFrame))
|
|
356
365
|
}
|
|
357
366
|
},
|
|
367
|
+
sendAuthResponse: (responses) => {
|
|
368
|
+
if (socket.readyState === WebSocket.OPEN) {
|
|
369
|
+
socket.send(JSON.stringify({ type: 'auth_response', responses } satisfies TerminalClientFrame))
|
|
370
|
+
}
|
|
371
|
+
},
|
|
358
372
|
close: () => {
|
|
359
373
|
try { socket.close() } catch { /* already closed */ }
|
|
360
374
|
},
|
|
@@ -369,6 +383,7 @@ export class SshApi {
|
|
|
369
383
|
if (frame.type === 'ready') connection.onReady?.()
|
|
370
384
|
else if (frame.type === 'output') connection.onOutput?.(frame.data)
|
|
371
385
|
else if (frame.type === 'exit') connection.onExit?.(frame.code, frame.error)
|
|
386
|
+
else if (frame.type === 'auth_prompt') connection.onAuthPrompt?.(frame.name, frame.instructions, frame.prompts)
|
|
372
387
|
}
|
|
373
388
|
socket.onclose = () => { connection.onExit?.(null, 'connection closed') }
|
|
374
389
|
socket.onerror = () => { connection.onExit?.(null, 'connection error') }
|
package/src/client/index.ts
CHANGED
|
@@ -10,11 +10,14 @@
|
|
|
10
10
|
* Export discipline (packages/client rule): the /client surface carries what
|
|
11
11
|
* cordis loading needs plus types only — all value exports stay internal.
|
|
12
12
|
*/
|
|
13
|
-
import type {
|
|
13
|
+
import type { Context as ClientContext } from '@deepseek-ai/cordis'
|
|
14
|
+
import type { SettingsScope, SettingsScopeSpec } from '@deepseek-ai/dsh-client-ui-settings/client'
|
|
14
15
|
// Type-only: pulls the locale plugin's Context merge (ctx.locale).
|
|
15
16
|
import type {} from '@deepseek-ai/dsh-client-locale/client'
|
|
16
17
|
// Type-only: pulls the settings-surface Context merge (ctx.settingsScope).
|
|
17
18
|
import type {} from '@deepseek-ai/dsh-client-ui-settings/client'
|
|
19
|
+
// Type-only: pulls the ctx.slots merge (the renderer owns the slot registry since 0.1.2).
|
|
20
|
+
import type {} from '@deepseek-ai/dsh-client-ui-renderer/client'
|
|
18
21
|
// Type-only: pulls the LocaleNamespaceMap merge table.
|
|
19
22
|
import type {} from '@deepseek-ai/dsh-client-ui-slots'
|
|
20
23
|
import { SshApi } from './api.ts'
|
package/src/client/locales.ts
CHANGED
|
@@ -85,6 +85,11 @@ export const zh = {
|
|
|
85
85
|
'terminal.ready': '终端已连接({alias})',
|
|
86
86
|
'terminal.exited': '终端已退出({alias})',
|
|
87
87
|
'terminal.error': '终端错误:{error}',
|
|
88
|
+
'terminal.auth.title': '二次身份验证 (2FA / 交互式认证)',
|
|
89
|
+
'terminal.auth.submit': '提交验证码',
|
|
90
|
+
'terminal.auth.cancel': '取消',
|
|
91
|
+
'terminal.auth.placeholder': '请输入验证码或口令',
|
|
92
|
+
'terminal.auth.hint': '服务器请求进行交互式二次验证(输入仅本次生效,不落盘):',
|
|
88
93
|
// transfer
|
|
89
94
|
'transfer.selectHost': '选择主机',
|
|
90
95
|
'transfer.remotePath': '远程路径',
|
|
@@ -149,6 +154,7 @@ export const zh = {
|
|
|
149
154
|
'common.error': '出错:{error}',
|
|
150
155
|
'common.refresh': '刷新',
|
|
151
156
|
'common.confirm': '确定',
|
|
157
|
+
'error.disabled': 'SSH 插件在宿主端未启用。请前往「设置 → Web 插件 → SSH」打开 enabled 开关。',
|
|
152
158
|
} as const
|
|
153
159
|
|
|
154
160
|
export const en: Record<keyof typeof zh, string> = {
|
|
@@ -231,6 +237,11 @@ export const en: Record<keyof typeof zh, string> = {
|
|
|
231
237
|
'terminal.ready': 'Terminal connected ({alias})',
|
|
232
238
|
'terminal.exited': 'Terminal exited ({alias})',
|
|
233
239
|
'terminal.error': 'Terminal error: {error}',
|
|
240
|
+
'terminal.auth.title': 'Two-Factor Authentication (2FA)',
|
|
241
|
+
'terminal.auth.submit': 'Submit Code',
|
|
242
|
+
'terminal.auth.cancel': 'Cancel',
|
|
243
|
+
'terminal.auth.placeholder': 'Enter code or passphrase',
|
|
244
|
+
'terminal.auth.hint': 'The server requested interactive 2FA (session only, never saved):',
|
|
234
245
|
'transfer.selectHost': 'Select host',
|
|
235
246
|
'transfer.remotePath': 'Remote path',
|
|
236
247
|
'transfer.remotePathHint': 'e.g. /tmp/ or /home/user/app.tar.gz',
|
|
@@ -291,6 +302,7 @@ export const en: Record<keyof typeof zh, string> = {
|
|
|
291
302
|
'common.error': 'Error: {error}',
|
|
292
303
|
'common.refresh': 'Refresh',
|
|
293
304
|
'common.confirm': 'Confirm',
|
|
305
|
+
'error.disabled': 'The SSH plugin is disabled on the host. Please enable it in Settings → Web Plugins → SSH.',
|
|
294
306
|
}
|
|
295
307
|
|
|
296
308
|
/** Locale key union. */
|