@linxin666/dsh-ssh 0.4.3 → 0.4.4
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 +72 -6
- package/lib/client.js.map +1 -1
- package/lib/types/client/api.d.ts +28 -0
- package/lib/types/client/locales.d.ts +1 -0
- package/package.json +17 -17
- package/src/client/api.ts +86 -3
- package/src/client/index.ts +3 -3
- package/src/client/locales.ts +2 -0
|
@@ -7,6 +7,33 @@ import { type ClusterResult, type ExecResult, type HostPayload, type ImportResul
|
|
|
7
7
|
export declare class SshApiError extends Error {
|
|
8
8
|
constructor(message: string);
|
|
9
9
|
}
|
|
10
|
+
/** The location facts a terminal socket URL is built from. */
|
|
11
|
+
interface PageLocation {
|
|
12
|
+
/** Page scheme with its colon (for example `https:` or `dsh-app:`). */
|
|
13
|
+
protocol: string;
|
|
14
|
+
/** Page authority, empty for a scheme that carries none. */
|
|
15
|
+
host: string;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* The WebSocket URL the terminal route is reached at, or undefined when this
|
|
19
|
+
* page cannot carry one.
|
|
20
|
+
*
|
|
21
|
+
* A page delivered by an application on this machine (the official DSH
|
|
22
|
+
* Desktop shell serves its Web GUI from `dsh-app://app/`) has no WebSocket
|
|
23
|
+
* transport: its scheme handler forwards HTTP requests to the local host but
|
|
24
|
+
* cannot upgrade a socket, so `ws://app/...` never connects and the terminal
|
|
25
|
+
* reports "connection error" (issue #1744). Every other page is a web page and
|
|
26
|
+
* resolves the socket against its own origin, exactly as before.
|
|
27
|
+
*
|
|
28
|
+
* A blank authority is treated the same as an application scheme: there is
|
|
29
|
+
* nothing to dial, and the caller gets the actionable reason instead of a
|
|
30
|
+
* socket that can only fail.
|
|
31
|
+
*
|
|
32
|
+
* @param location - the page location to read.
|
|
33
|
+
* @param search - the query string carrying `alias` or `session`.
|
|
34
|
+
* @returns the absolute `ws:`/`wss:` URL, or undefined when the page cannot carry one.
|
|
35
|
+
*/
|
|
36
|
+
export declare function terminalSocketUrl(location: PageLocation, search: string): string | undefined;
|
|
10
37
|
/** One open terminal connection (WebSocket JSON frames). */
|
|
11
38
|
export interface TerminalConnection {
|
|
12
39
|
/** Fired on the ready frame (shell is up); carries the host session id and alias. */
|
|
@@ -100,3 +127,4 @@ export declare class SshApi {
|
|
|
100
127
|
/** One terminal socket over either an alias (open) or a session id (attach). */
|
|
101
128
|
private terminalSocket;
|
|
102
129
|
}
|
|
130
|
+
export {};
|
|
@@ -89,6 +89,7 @@ export declare const zh: {
|
|
|
89
89
|
readonly 'terminal.ready': "终端已连接({alias})";
|
|
90
90
|
readonly 'terminal.exited': "终端已退出({alias})";
|
|
91
91
|
readonly 'terminal.error': "终端错误:{error}";
|
|
92
|
+
readonly 'terminal.noWebSocket': "当前页面不支持 WebSocket(应用外壳只转发 HTTP),请改用浏览器打开的 Web 界面使用终端。";
|
|
92
93
|
readonly 'terminal.auth.title': "二次身份验证 (2FA / 交互式认证)";
|
|
93
94
|
readonly 'terminal.auth.submit': "提交验证码";
|
|
94
95
|
readonly 'terminal.auth.cancel': "取消";
|
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.4.
|
|
4
|
+
"version": "0.4.4",
|
|
5
5
|
"icon": "icon.svg",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"engines": {
|
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
},
|
|
28
28
|
"dsh": {
|
|
29
29
|
"engines": {
|
|
30
|
-
"dsh": ">=0.
|
|
30
|
+
"dsh": ">=0.2.0-rc.1"
|
|
31
31
|
},
|
|
32
32
|
"bundle": {
|
|
33
33
|
"patch": "./cordis.patch.yml"
|
|
@@ -50,26 +50,26 @@
|
|
|
50
50
|
"ws": "^8.18.0"
|
|
51
51
|
},
|
|
52
52
|
"peerDependencies": {
|
|
53
|
-
"@deepseek-ai/dsh": ">=0.
|
|
53
|
+
"@deepseek-ai/dsh": ">=0.2.0-rc.1",
|
|
54
54
|
"react": "^18.2.0",
|
|
55
55
|
"react-dom": "^18.2.0"
|
|
56
56
|
},
|
|
57
57
|
"devDependencies": {
|
|
58
58
|
"@deepseek-ai/cordis": "^4.0.4",
|
|
59
|
-
"@deepseek-ai/dsh-client-connection": "^0.
|
|
60
|
-
"@deepseek-ai/dsh-client-locale": "^0.
|
|
61
|
-
"@deepseek-ai/dsh-client-ui-renderer": "^0.
|
|
62
|
-
"@deepseek-ai/dsh-client-ui-layout": "^0.
|
|
63
|
-
"@deepseek-ai/dsh-client-ui-settings": "^0.
|
|
64
|
-
"@deepseek-ai/dsh-client-ui-sidebar": "^0.
|
|
65
|
-
"@deepseek-ai/dsh-client-ui-slots": "^0.
|
|
66
|
-
"@deepseek-ai/dsh-host-webserver": "^0.
|
|
67
|
-
"@deepseek-ai/dsh-llm": "^0.
|
|
68
|
-
"@deepseek-ai/dsh-scope": "^0.
|
|
69
|
-
"@deepseek-ai/dsh-settings": "^0.
|
|
70
|
-
"@deepseek-ai/dsh-system-prompt": "^0.
|
|
71
|
-
"@deepseek-ai/dsh-timeout": "^0.
|
|
72
|
-
"@deepseek-ai/dsh-tools": "^0.
|
|
59
|
+
"@deepseek-ai/dsh-client-connection": "^0.2.0-rc.1",
|
|
60
|
+
"@deepseek-ai/dsh-client-locale": "^0.2.0-rc.1",
|
|
61
|
+
"@deepseek-ai/dsh-client-ui-renderer": "^0.2.0-rc.1",
|
|
62
|
+
"@deepseek-ai/dsh-client-ui-layout": "^0.2.0-rc.1",
|
|
63
|
+
"@deepseek-ai/dsh-client-ui-settings": "^0.2.0-rc.1",
|
|
64
|
+
"@deepseek-ai/dsh-client-ui-sidebar": "^0.2.0-rc.1",
|
|
65
|
+
"@deepseek-ai/dsh-client-ui-slots": "^0.2.0-rc.1",
|
|
66
|
+
"@deepseek-ai/dsh-host-webserver": "^0.2.0-rc.1",
|
|
67
|
+
"@deepseek-ai/dsh-llm": "^0.2.0-rc.1",
|
|
68
|
+
"@deepseek-ai/dsh-scope": "^0.2.0-rc.1",
|
|
69
|
+
"@deepseek-ai/dsh-settings": "^0.2.0-rc.1",
|
|
70
|
+
"@deepseek-ai/dsh-system-prompt": "^0.2.0-rc.1",
|
|
71
|
+
"@deepseek-ai/dsh-timeout": "^0.2.0-rc.1",
|
|
72
|
+
"@deepseek-ai/dsh-tools": "^0.2.0-rc.1",
|
|
73
73
|
"@types/node": "^22.20.0",
|
|
74
74
|
"@types/react": "~18.3.1",
|
|
75
75
|
"@types/react-dom": "^18.3.5",
|
package/src/client/api.ts
CHANGED
|
@@ -76,6 +76,79 @@ function query(params: Record<string, string | number | undefined>): string {
|
|
|
76
76
|
return text === '' ? '' : '?' + text
|
|
77
77
|
}
|
|
78
78
|
|
|
79
|
+
/** The location facts a terminal socket URL is built from. */
|
|
80
|
+
interface PageLocation {
|
|
81
|
+
/** Page scheme with its colon (for example `https:` or `dsh-app:`). */
|
|
82
|
+
protocol: string
|
|
83
|
+
/** Page authority, empty for a scheme that carries none. */
|
|
84
|
+
host: string
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* The WebSocket URL the terminal route is reached at, or undefined when this
|
|
89
|
+
* page cannot carry one.
|
|
90
|
+
*
|
|
91
|
+
* A page delivered by an application on this machine (the official DSH
|
|
92
|
+
* Desktop shell serves its Web GUI from `dsh-app://app/`) has no WebSocket
|
|
93
|
+
* transport: its scheme handler forwards HTTP requests to the local host but
|
|
94
|
+
* cannot upgrade a socket, so `ws://app/...` never connects and the terminal
|
|
95
|
+
* reports "connection error" (issue #1744). Every other page is a web page and
|
|
96
|
+
* resolves the socket against its own origin, exactly as before.
|
|
97
|
+
*
|
|
98
|
+
* A blank authority is treated the same as an application scheme: there is
|
|
99
|
+
* nothing to dial, and the caller gets the actionable reason instead of a
|
|
100
|
+
* socket that can only fail.
|
|
101
|
+
*
|
|
102
|
+
* @param location - the page location to read.
|
|
103
|
+
* @param search - the query string carrying `alias` or `session`.
|
|
104
|
+
* @returns the absolute `ws:`/`wss:` URL, or undefined when the page cannot carry one.
|
|
105
|
+
*/
|
|
106
|
+
export function terminalSocketUrl(location: PageLocation, search: string): string | undefined {
|
|
107
|
+
if (WEB_PAGE_PROTOCOLS.includes(location.protocol)) {
|
|
108
|
+
const scheme = location.protocol === 'https:' ? 'wss' : 'ws'
|
|
109
|
+
return scheme + '://' + location.host + SSH_API.terminal + search
|
|
110
|
+
}
|
|
111
|
+
return undefined
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Schemes a network page can be delivered with. Every other scheme belongs to
|
|
116
|
+
* an application on this machine, which is the same classification the remote
|
|
117
|
+
* channel and the update seat use (`isWebPageProtocol` / `isApplicationDeliveredPage`).
|
|
118
|
+
*/
|
|
119
|
+
const WEB_PAGE_PROTOCOLS: readonly string[] = [
|
|
120
|
+
'http:',
|
|
121
|
+
'https:',
|
|
122
|
+
'blob:',
|
|
123
|
+
'data:',
|
|
124
|
+
'about:',
|
|
125
|
+
'filesystem:',
|
|
126
|
+
]
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* A terminal connection that never opened: it reports one failure once the
|
|
130
|
+
* view has attached its `onExit`, so the tab shows why instead of sitting on a
|
|
131
|
+
* spinner, and every later frame is a no-op.
|
|
132
|
+
*/
|
|
133
|
+
function failedTerminal(reason: string): TerminalConnection {
|
|
134
|
+
const connection: TerminalConnection = {
|
|
135
|
+
onReady: undefined,
|
|
136
|
+
onOutput: undefined,
|
|
137
|
+
onExit: undefined,
|
|
138
|
+
onAuthPrompt: undefined,
|
|
139
|
+
send: () => undefined,
|
|
140
|
+
resize: () => undefined,
|
|
141
|
+
sendAuthResponse: () => undefined,
|
|
142
|
+
detach: () => undefined,
|
|
143
|
+
close: () => undefined,
|
|
144
|
+
}
|
|
145
|
+
// The view binds onExit after the open() call returns, so the failure is
|
|
146
|
+
// delivered on the next microtask - never synchronously, which would race
|
|
147
|
+
// the handler the caller is about to assign.
|
|
148
|
+
queueMicrotask(() => { connection.onExit?.(null, reason) })
|
|
149
|
+
return connection
|
|
150
|
+
}
|
|
151
|
+
|
|
79
152
|
/** One open terminal connection (WebSocket JSON frames). */
|
|
80
153
|
export interface TerminalConnection {
|
|
81
154
|
/** Fired on the ready frame (shell is up); carries the host session id and alias. */
|
|
@@ -375,9 +448,19 @@ export class SshApi {
|
|
|
375
448
|
|
|
376
449
|
/** One terminal socket over either an alias (open) or a session id (attach). */
|
|
377
450
|
private terminalSocket(search: string): TerminalConnection {
|
|
378
|
-
const
|
|
379
|
-
|
|
380
|
-
|
|
451
|
+
const target = terminalSocketUrl(window.location, search)
|
|
452
|
+
if (target === undefined) {
|
|
453
|
+
// This page has no WebSocket transport at all (the desktop shell's
|
|
454
|
+
// custom scheme forwards HTTP only). Say so plainly instead of opening
|
|
455
|
+
// a socket that can only report "connection error".
|
|
456
|
+
return failedTerminal(tt('terminal.noWebSocket'))
|
|
457
|
+
}
|
|
458
|
+
let socket: WebSocket
|
|
459
|
+
try {
|
|
460
|
+
socket = new WebSocket(target)
|
|
461
|
+
} catch (error) {
|
|
462
|
+
return failedTerminal(error instanceof Error ? error.message : String(error))
|
|
463
|
+
}
|
|
381
464
|
// A deliberate detach/close must not surface as a transport error: the
|
|
382
465
|
// view tears down silently, and only an UNEXPECTED close reports exit.
|
|
383
466
|
let leaving = false
|
package/src/client/index.ts
CHANGED
|
@@ -54,7 +54,7 @@ declare module '@deepseek-ai/dsh-client-ui-slots' {
|
|
|
54
54
|
}
|
|
55
55
|
|
|
56
56
|
/** Required services (fiber inject waiting — the runtime must be up first). */
|
|
57
|
-
export const inject = ['slots', 'locale', 'configForms']
|
|
57
|
+
export const inject = ['slots', 'locale', 'configForms', 'layout']
|
|
58
58
|
|
|
59
59
|
/** Type-only surface (export discipline: no value exports beyond the plugin contract). */
|
|
60
60
|
export type { PanelControllerSnapshot } from './panel/controller.ts'
|
|
@@ -98,7 +98,7 @@ export function apply(ctx: ClientContext): void {
|
|
|
98
98
|
const controller = new PanelController({
|
|
99
99
|
panel: {
|
|
100
100
|
select: panelId => {
|
|
101
|
-
const layout = ctx.get('layout') as { selectPanel?: (id: string | null) => void } | undefined
|
|
101
|
+
const layout = (ctx as unknown as { get?(name: string): unknown }).get?.('layout') as { selectPanel?: (id: string | null) => void } | undefined
|
|
102
102
|
layout?.selectPanel?.(panelId)
|
|
103
103
|
},
|
|
104
104
|
},
|
|
@@ -122,7 +122,7 @@ export function apply(ctx: ClientContext): void {
|
|
|
122
122
|
disposers.push(registerSshPanel(ctx, controller, api, terminalFont))
|
|
123
123
|
// The layout's panel selection is reconciled back into the controller, so
|
|
124
124
|
// the panel follows the user clicking another panel row.
|
|
125
|
-
const layoutFace = ctx.get('layout') as { panelInfo?: { subscribe(listener: () => void): () => void; getSnapshot(): { activePanelId: unknown } } } | undefined
|
|
125
|
+
const layoutFace = (ctx as unknown as { get?(name: string): unknown }).get?.('layout') as { panelInfo?: { subscribe(listener: () => void): () => void; getSnapshot(): { activePanelId: unknown } } } | undefined
|
|
126
126
|
if (layoutFace?.panelInfo !== undefined) {
|
|
127
127
|
const sync = (): void => {
|
|
128
128
|
const active = layoutFace.panelInfo!.getSnapshot().activePanelId
|
package/src/client/locales.ts
CHANGED
|
@@ -93,6 +93,7 @@ export const zh = {
|
|
|
93
93
|
'terminal.ready': '终端已连接({alias})',
|
|
94
94
|
'terminal.exited': '终端已退出({alias})',
|
|
95
95
|
'terminal.error': '终端错误:{error}',
|
|
96
|
+
'terminal.noWebSocket': '当前页面不支持 WebSocket(应用外壳只转发 HTTP),请改用浏览器打开的 Web 界面使用终端。',
|
|
96
97
|
'terminal.auth.title': '二次身份验证 (2FA / 交互式认证)',
|
|
97
98
|
'terminal.auth.submit': '提交验证码',
|
|
98
99
|
'terminal.auth.cancel': '取消',
|
|
@@ -253,6 +254,7 @@ export const en: Record<keyof typeof zh, string> = {
|
|
|
253
254
|
'terminal.ready': 'Terminal connected ({alias})',
|
|
254
255
|
'terminal.exited': 'Terminal exited ({alias})',
|
|
255
256
|
'terminal.error': 'Terminal error: {error}',
|
|
257
|
+
'terminal.noWebSocket': 'This page cannot carry a WebSocket (the application shell forwards HTTP only). Open the Web UI in a browser to use the terminal.',
|
|
256
258
|
'terminal.auth.title': 'Two-Factor Authentication (2FA)',
|
|
257
259
|
'terminal.auth.submit': 'Submit Code',
|
|
258
260
|
'terminal.auth.cancel': 'Cancel',
|