@electerm/electerm-react 3.12.0 → 3.15.28
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/client/common/default-setting.js +1 -0
- package/client/common/is-absolute-path.js +1 -1
- package/client/common/normalize-remote-path.js +20 -0
- package/client/common/resolve.js +16 -0
- package/client/components/ai/agent-tools.js +121 -1
- package/client/components/ai/ai-chat-history-item.jsx +7 -2
- package/client/components/ai/ai-chat.jsx +1 -0
- package/client/components/ai/ai-config-props.js +1 -0
- package/client/components/ai/ai-config.jsx +13 -2
- package/client/components/ai/ai-output.jsx +6 -2
- package/client/components/main/main.jsx +1 -1
- package/client/components/sftp/address-bar.jsx +22 -6
- package/client/components/sftp/file-item.jsx +43 -2
- package/client/components/sftp/file-read.js +11 -2
- package/client/components/sftp/sftp-entry.jsx +38 -3
- package/client/components/sys-menu/icons-map.jsx +10 -2
- package/client/components/terminal/osc52-addon.js +147 -0
- package/client/components/terminal/terminal-apis.js +9 -0
- package/client/components/terminal/terminal.jsx +119 -3
- package/client/components/terminal/xmodem-client.js +62 -0
- package/client/components/terminal-info/base.jsx +41 -38
- package/client/components/terminal-info/log-path-edit.jsx +3 -2
- package/client/store/common.js +1 -1
- package/client/store/load-data.js +14 -0
- package/client/store/mcp-handler.js +231 -0
- package/package.json +1 -1
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ensure remote path always starts with /
|
|
3
|
+
* Windows drive letters like c: become /c:
|
|
4
|
+
* Also fixes mixed separators like /c:\windows → /c:/windows
|
|
5
|
+
* This is needed because SFTP protocol expects paths with leading /
|
|
6
|
+
* @param {String} path
|
|
7
|
+
* @return {String}
|
|
8
|
+
*/
|
|
9
|
+
export default function normalizeRemotePath (path) {
|
|
10
|
+
if (!path) return path
|
|
11
|
+
// Fix mixed separators: /c:\windows → /c:/windows
|
|
12
|
+
if (/^\/[a-zA-Z]:\\/.test(path)) {
|
|
13
|
+
return path.replace(/\\/g, '/')
|
|
14
|
+
}
|
|
15
|
+
// Add leading / to bare drive letters: c: → /c:, c:\windows → /c:/windows
|
|
16
|
+
if (/^[a-zA-Z]:/.test(path)) {
|
|
17
|
+
return '/' + path.replace(/\\/g, '/')
|
|
18
|
+
}
|
|
19
|
+
return path
|
|
20
|
+
}
|
package/client/common/resolve.js
CHANGED
|
@@ -5,6 +5,13 @@
|
|
|
5
5
|
* @return {String}
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
+
export const isWslPath = (path) => /^\\\\(?:wsl\$|wsl\.localhost)\\/.test(path)
|
|
9
|
+
|
|
10
|
+
export const isWslDistroRoot = (path) => {
|
|
11
|
+
const trimmed = path.replace(/\\$/, '')
|
|
12
|
+
return /^\\\\(?:wsl\$|wsl\.localhost)\\[^\\]+$/.test(trimmed)
|
|
13
|
+
}
|
|
14
|
+
|
|
8
15
|
export default function resolve (basePath, nameOrDot) {
|
|
9
16
|
const hasWinDrive = (path) => /^[a-zA-Z]:/.test(path)
|
|
10
17
|
const isWin = basePath.includes('\\') || nameOrDot.includes('\\') || hasWinDrive(basePath) || hasWinDrive(nameOrDot)
|
|
@@ -15,7 +22,13 @@ export default function resolve (basePath, nameOrDot) {
|
|
|
15
22
|
if (nameOrDot.startsWith('/')) {
|
|
16
23
|
return nameOrDot.replace(/\\/g, sep)
|
|
17
24
|
}
|
|
25
|
+
if (nameOrDot.startsWith('\\\\')) {
|
|
26
|
+
return nameOrDot
|
|
27
|
+
}
|
|
18
28
|
if (nameOrDot === '..') {
|
|
29
|
+
if (isWslDistroRoot(basePath)) {
|
|
30
|
+
return '/'
|
|
31
|
+
}
|
|
19
32
|
const baseEndsWithSep = basePath.endsWith(sep)
|
|
20
33
|
const parts = basePath.split(sep)
|
|
21
34
|
if (parts.length > 1) {
|
|
@@ -27,6 +40,9 @@ export default function resolve (basePath, nameOrDot) {
|
|
|
27
40
|
}
|
|
28
41
|
return '/'
|
|
29
42
|
}
|
|
43
|
+
if (isWslDistroRoot(basePath) && !basePath.endsWith(sep)) {
|
|
44
|
+
return basePath + sep + nameOrDot
|
|
45
|
+
}
|
|
30
46
|
const result = basePath.endsWith(sep) ? basePath + nameOrDot : basePath + sep + nameOrDot
|
|
31
47
|
return isWin && result.length === 3 && result.endsWith(':\\') ? '/' : result
|
|
32
48
|
}
|
|
@@ -31,7 +31,7 @@ export const agentTools = [
|
|
|
31
31
|
type: 'function',
|
|
32
32
|
function: {
|
|
33
33
|
name: 'send_terminal_command',
|
|
34
|
-
description: 'Send a command to a terminal tab and wait for it to finish. Returns the command output.',
|
|
34
|
+
description: 'Send a command to a terminal tab and wait for it to finish. Returns the command output. For long-running commands (builds, deployments, installations), use run_background_command instead to avoid timeouts.',
|
|
35
35
|
parameters: {
|
|
36
36
|
type: 'object',
|
|
37
37
|
properties: {
|
|
@@ -334,6 +334,114 @@ export const agentTools = [
|
|
|
334
334
|
properties: {}
|
|
335
335
|
}
|
|
336
336
|
}
|
|
337
|
+
},
|
|
338
|
+
{
|
|
339
|
+
type: 'function',
|
|
340
|
+
function: {
|
|
341
|
+
name: 'get_terminal_status',
|
|
342
|
+
description: 'Check terminal status: running (actively receiving data), idle, or password prompt. Returns last 20 lines of output. Lightweight, non-blocking.',
|
|
343
|
+
parameters: {
|
|
344
|
+
type: 'object',
|
|
345
|
+
properties: {
|
|
346
|
+
tabId: {
|
|
347
|
+
type: 'string',
|
|
348
|
+
description: 'Tab ID. Omit for active terminal.'
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
},
|
|
354
|
+
{
|
|
355
|
+
type: 'function',
|
|
356
|
+
function: {
|
|
357
|
+
name: 'cancel_terminal_command',
|
|
358
|
+
description: 'Cancel the running command in a terminal by sending Ctrl+C.',
|
|
359
|
+
parameters: {
|
|
360
|
+
type: 'object',
|
|
361
|
+
properties: {
|
|
362
|
+
tabId: {
|
|
363
|
+
type: 'string',
|
|
364
|
+
description: 'Tab ID. Omit for active terminal.'
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
},
|
|
370
|
+
{
|
|
371
|
+
type: 'function',
|
|
372
|
+
function: {
|
|
373
|
+
name: 'run_background_command',
|
|
374
|
+
description: 'Run a command in the background using nohup. The terminal is freed immediately. Returns a taskId for monitoring. Use get_background_task_status and get_background_task_log to check progress.',
|
|
375
|
+
parameters: {
|
|
376
|
+
type: 'object',
|
|
377
|
+
properties: {
|
|
378
|
+
command: {
|
|
379
|
+
type: 'string',
|
|
380
|
+
description: 'The shell command to run in the background.'
|
|
381
|
+
},
|
|
382
|
+
tabId: {
|
|
383
|
+
type: 'string',
|
|
384
|
+
description: 'Tab ID. Omit for active terminal.'
|
|
385
|
+
}
|
|
386
|
+
},
|
|
387
|
+
required: ['command']
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
},
|
|
391
|
+
{
|
|
392
|
+
type: 'function',
|
|
393
|
+
function: {
|
|
394
|
+
name: 'get_background_task_status',
|
|
395
|
+
description: 'Check if a background task is running, completed (with exit code), or unknown.',
|
|
396
|
+
parameters: {
|
|
397
|
+
type: 'object',
|
|
398
|
+
properties: {
|
|
399
|
+
taskId: {
|
|
400
|
+
type: 'string',
|
|
401
|
+
description: 'Task ID from run_background_command.'
|
|
402
|
+
}
|
|
403
|
+
},
|
|
404
|
+
required: ['taskId']
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
},
|
|
408
|
+
{
|
|
409
|
+
type: 'function',
|
|
410
|
+
function: {
|
|
411
|
+
name: 'get_background_task_log',
|
|
412
|
+
description: 'Read the output log of a background task. Returns the last N lines.',
|
|
413
|
+
parameters: {
|
|
414
|
+
type: 'object',
|
|
415
|
+
properties: {
|
|
416
|
+
taskId: {
|
|
417
|
+
type: 'string',
|
|
418
|
+
description: 'Task ID from run_background_command.'
|
|
419
|
+
},
|
|
420
|
+
lines: {
|
|
421
|
+
type: 'number',
|
|
422
|
+
description: 'Number of recent lines to read (default 100).'
|
|
423
|
+
}
|
|
424
|
+
},
|
|
425
|
+
required: ['taskId']
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
},
|
|
429
|
+
{
|
|
430
|
+
type: 'function',
|
|
431
|
+
function: {
|
|
432
|
+
name: 'cancel_background_task',
|
|
433
|
+
description: 'Cancel a running background task by killing its process.',
|
|
434
|
+
parameters: {
|
|
435
|
+
type: 'object',
|
|
436
|
+
properties: {
|
|
437
|
+
taskId: {
|
|
438
|
+
type: 'string',
|
|
439
|
+
description: 'Task ID from run_background_command.'
|
|
440
|
+
}
|
|
441
|
+
},
|
|
442
|
+
required: ['taskId']
|
|
443
|
+
}
|
|
444
|
+
}
|
|
337
445
|
}
|
|
338
446
|
]
|
|
339
447
|
|
|
@@ -391,6 +499,18 @@ export async function executeToolCall (toolName, args) {
|
|
|
391
499
|
return JSON.stringify(store.mcpSftpTransferList())
|
|
392
500
|
case 'sftp_transfer_history':
|
|
393
501
|
return JSON.stringify(store.mcpSftpTransferHistory())
|
|
502
|
+
case 'get_terminal_status':
|
|
503
|
+
return JSON.stringify(store.mcpGetTerminalStatus(args))
|
|
504
|
+
case 'cancel_terminal_command':
|
|
505
|
+
return JSON.stringify(store.mcpCancelTerminalCommand(args))
|
|
506
|
+
case 'run_background_command':
|
|
507
|
+
return JSON.stringify(store.mcpRunBackgroundCommand(args))
|
|
508
|
+
case 'get_background_task_status':
|
|
509
|
+
return JSON.stringify(await store.mcpGetBackgroundTaskStatus(args))
|
|
510
|
+
case 'get_background_task_log':
|
|
511
|
+
return JSON.stringify(await store.mcpGetBackgroundTaskLog(args))
|
|
512
|
+
case 'cancel_background_task':
|
|
513
|
+
return JSON.stringify(await store.mcpCancelBackgroundTask(args))
|
|
394
514
|
default:
|
|
395
515
|
throw new Error(`Unknown agent tool: ${toolName}`)
|
|
396
516
|
}
|
|
@@ -8,7 +8,6 @@ import {
|
|
|
8
8
|
Tooltip
|
|
9
9
|
} from 'antd'
|
|
10
10
|
import {
|
|
11
|
-
UserOutlined,
|
|
12
11
|
CopyOutlined,
|
|
13
12
|
CloseOutlined,
|
|
14
13
|
CaretDownOutlined,
|
|
@@ -23,6 +22,7 @@ export default function AIChatHistoryItem ({ item }) {
|
|
|
23
22
|
const {
|
|
24
23
|
prompt,
|
|
25
24
|
sessionId,
|
|
25
|
+
nameAI,
|
|
26
26
|
modelAI,
|
|
27
27
|
roleAI,
|
|
28
28
|
baseURLAI,
|
|
@@ -172,7 +172,7 @@ export default function AIChatHistoryItem ({ item }) {
|
|
|
172
172
|
<span className='pointer mg1r' onClick={toggleOutput}>
|
|
173
173
|
{showOutput ? <CaretDownOutlined /> : <CaretRightOutlined />}
|
|
174
174
|
</span>
|
|
175
|
-
<
|
|
175
|
+
<span>{prompt}</span>
|
|
176
176
|
{renderStopButton()}
|
|
177
177
|
</div>
|
|
178
178
|
),
|
|
@@ -191,6 +191,11 @@ export default function AIChatHistoryItem ({ item }) {
|
|
|
191
191
|
function renderTitle () {
|
|
192
192
|
return (
|
|
193
193
|
<div>
|
|
194
|
+
{nameAI && (
|
|
195
|
+
<p>
|
|
196
|
+
<b>Name:</b> {nameAI}
|
|
197
|
+
</p>
|
|
198
|
+
)}
|
|
194
199
|
<p>
|
|
195
200
|
<b>Model:</b> {modelAI}
|
|
196
201
|
</p>
|
|
@@ -94,10 +94,13 @@ export default function AIConfigForm ({ initialValues, onSubmit, showAIConfig })
|
|
|
94
94
|
|
|
95
95
|
function renderHistoryItem (item) {
|
|
96
96
|
if (!item || typeof item !== 'object') return { label: 'Unknown', title: 'Unknown' }
|
|
97
|
+
const name = item.nameAI || ''
|
|
97
98
|
const model = item.modelAI || 'Default Model'
|
|
98
99
|
const rolePrefix = item.roleAI ? item.roleAI.substring(0, 15) + '...' : ''
|
|
99
|
-
const label = `[${model}] ${rolePrefix}`
|
|
100
|
-
const title =
|
|
100
|
+
const label = name || `[${model}] ${rolePrefix}`
|
|
101
|
+
const title = name
|
|
102
|
+
? `${name}\nModel: ${item.modelAI}\nURL: ${item.baseURLAI}`
|
|
103
|
+
: `Model: ${item.modelAI}\nRole: ${item.roleAI}\nURL: ${item.baseURLAI}`
|
|
101
104
|
return { label, title }
|
|
102
105
|
}
|
|
103
106
|
|
|
@@ -131,6 +134,14 @@ export default function AIConfigForm ({ initialValues, onSubmit, showAIConfig })
|
|
|
131
134
|
layout='vertical'
|
|
132
135
|
className='ai-config-form'
|
|
133
136
|
>
|
|
137
|
+
<Form.Item
|
|
138
|
+
label='Name'
|
|
139
|
+
name='nameAI'
|
|
140
|
+
>
|
|
141
|
+
<Input
|
|
142
|
+
placeholder='e.g. DeepSeek Relay, Local Ollama (optional)'
|
|
143
|
+
/>
|
|
144
|
+
</Form.Item>
|
|
134
145
|
<Form.Item label={renderApiUrlLabel()} required>
|
|
135
146
|
<Space.Compact className='width-100'>
|
|
136
147
|
<Form.Item
|
|
@@ -10,7 +10,9 @@ const e = window.translate
|
|
|
10
10
|
export default function AIOutput ({ item }) {
|
|
11
11
|
const {
|
|
12
12
|
response,
|
|
13
|
-
baseURLAI
|
|
13
|
+
baseURLAI,
|
|
14
|
+
nameAI,
|
|
15
|
+
modelAI
|
|
14
16
|
} = item
|
|
15
17
|
if (!response) {
|
|
16
18
|
return null
|
|
@@ -82,10 +84,12 @@ export default function AIOutput ({ item }) {
|
|
|
82
84
|
if (!brand) {
|
|
83
85
|
return null
|
|
84
86
|
}
|
|
87
|
+
const nameLabel = nameAI || modelAI
|
|
88
|
+
const label = nameLabel ? `${brand}:${nameLabel}` : brand
|
|
85
89
|
return (
|
|
86
90
|
<div className='pd1y'>
|
|
87
91
|
<Link to={brandUrl}>
|
|
88
|
-
<Tag>{
|
|
92
|
+
<Tag>{label}</Tag>
|
|
89
93
|
</Link>
|
|
90
94
|
</div>
|
|
91
95
|
)
|
|
@@ -282,7 +282,7 @@ export default auto(function Index (props) {
|
|
|
282
282
|
<InfoModal {...infoModalProps} />
|
|
283
283
|
<RightSidePanel {...rightPanelProps}>
|
|
284
284
|
<AIChat {...aiChatProps} />
|
|
285
|
-
<TerminalInfo {...terminalInfoProps} />
|
|
285
|
+
<TerminalInfo key={store.activeTabId} {...terminalInfoProps} />
|
|
286
286
|
</RightSidePanel>
|
|
287
287
|
<SshConfigLoadNotify {...sshConfigProps} />
|
|
288
288
|
<LoadSshConfigs
|
|
@@ -6,7 +6,8 @@ import {
|
|
|
6
6
|
ReloadOutlined,
|
|
7
7
|
ArrowRightOutlined,
|
|
8
8
|
LoadingOutlined,
|
|
9
|
-
HomeOutlined
|
|
9
|
+
HomeOutlined,
|
|
10
|
+
PlusOutlined
|
|
10
11
|
} from '@ant-design/icons'
|
|
11
12
|
import {
|
|
12
13
|
Input,
|
|
@@ -74,7 +75,7 @@ function renderAddonBefore (props, realPath) {
|
|
|
74
75
|
)
|
|
75
76
|
}
|
|
76
77
|
|
|
77
|
-
function renderAddonAfter (isLoadingRemote, onGoto, GoIcon, type) {
|
|
78
|
+
function renderAddonAfter (isLoadingRemote, onGoto, GoIcon, type, handleUploadFromBrowser) {
|
|
78
79
|
const handleClick = (e) => {
|
|
79
80
|
e.stopPropagation()
|
|
80
81
|
if (!isLoadingRemote) {
|
|
@@ -82,9 +83,24 @@ function renderAddonAfter (isLoadingRemote, onGoto, GoIcon, type) {
|
|
|
82
83
|
}
|
|
83
84
|
}
|
|
84
85
|
return (
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
86
|
+
<>
|
|
87
|
+
{
|
|
88
|
+
type === typeMap.local && window.et.isWebApp
|
|
89
|
+
? (
|
|
90
|
+
<PlusOutlined
|
|
91
|
+
className='mg1r'
|
|
92
|
+
onClick={(e) => {
|
|
93
|
+
e.stopPropagation()
|
|
94
|
+
handleUploadFromBrowser()
|
|
95
|
+
}}
|
|
96
|
+
/>
|
|
97
|
+
)
|
|
98
|
+
: null
|
|
99
|
+
}
|
|
100
|
+
<GoIcon
|
|
101
|
+
onClick={handleClick}
|
|
102
|
+
/>
|
|
103
|
+
</>
|
|
88
104
|
)
|
|
89
105
|
}
|
|
90
106
|
|
|
@@ -160,7 +176,7 @@ export default function AddressBar (props) {
|
|
|
160
176
|
onBlur={() => props.onInputBlur(type)}
|
|
161
177
|
disabled={loadingSftp}
|
|
162
178
|
suffix={
|
|
163
|
-
renderAddonAfter(isLoadingRemote, onGoto, GoIcon, type)
|
|
179
|
+
renderAddonAfter(isLoadingRemote, onGoto, GoIcon, type, props.handleUploadFromBrowser)
|
|
164
180
|
}
|
|
165
181
|
/>
|
|
166
182
|
{renderHistory(props, type)}
|
|
@@ -14,6 +14,7 @@ import copy from 'json-deep-copy'
|
|
|
14
14
|
import { pick, some } from 'lodash-es'
|
|
15
15
|
import Input from '../common/input-auto-focus'
|
|
16
16
|
import resolve from '../../common/resolve'
|
|
17
|
+
import normalizeRemotePath from '../../common/normalize-remote-path'
|
|
17
18
|
import { addClass, removeClass } from '../../common/class'
|
|
18
19
|
import {
|
|
19
20
|
mode2permission,
|
|
@@ -604,7 +605,10 @@ export default class FileSection extends React.Component {
|
|
|
604
605
|
const { type, name, isParent } = file
|
|
605
606
|
const n = `${type}Path`
|
|
606
607
|
const path = isParent ? file.path : this.props[n]
|
|
607
|
-
|
|
608
|
+
let np = resolve(path, name)
|
|
609
|
+
if (type === typeMap.remote) {
|
|
610
|
+
np = normalizeRemotePath(np)
|
|
611
|
+
}
|
|
608
612
|
const op = this.props[type + 'Path']
|
|
609
613
|
this.props.modifier({
|
|
610
614
|
[n]: np,
|
|
@@ -693,11 +697,22 @@ export default class FileSection extends React.Component {
|
|
|
693
697
|
const {
|
|
694
698
|
path, name
|
|
695
699
|
} = this.state.file
|
|
696
|
-
|
|
700
|
+
let rp = path ? resolve(path, name) : this.props[`${this.props.type}Path`]
|
|
701
|
+
if (this.props.type === typeMap.remote) {
|
|
702
|
+
rp = this.convertSftpPathToTerminalPath(rp)
|
|
703
|
+
}
|
|
697
704
|
this.props.tab.pane = paneMap.terminal
|
|
698
705
|
refs.get('term-' + this.props.tab.id)?.cd(rp)
|
|
699
706
|
}
|
|
700
707
|
|
|
708
|
+
convertSftpPathToTerminalPath = (p) => {
|
|
709
|
+
const m = p.match(/^\/([a-zA-Z]:)(.*)$/)
|
|
710
|
+
if (m) {
|
|
711
|
+
return m[1] + m[2].replace(/\//g, '\\')
|
|
712
|
+
}
|
|
713
|
+
return p
|
|
714
|
+
}
|
|
715
|
+
|
|
701
716
|
fetchEditorText = async (path, type) => {
|
|
702
717
|
// const sftp = sftpFunc()
|
|
703
718
|
const text = typeMap.remote === type
|
|
@@ -882,6 +897,25 @@ export default class FileSection extends React.Component {
|
|
|
882
897
|
window.pre.showItemInFolder(p)
|
|
883
898
|
}
|
|
884
899
|
|
|
900
|
+
downloadFromBrowser = async () => {
|
|
901
|
+
const { path, name, isDirectory } = this.state.file
|
|
902
|
+
const p = resolve(path, name)
|
|
903
|
+
const url = '/api/download?path=' + encodeURIComponent(p)
|
|
904
|
+
const res = await window.fetch(url, {
|
|
905
|
+
headers: {
|
|
906
|
+
token: window.store?.config.tokenElecterm
|
|
907
|
+
}
|
|
908
|
+
})
|
|
909
|
+
const blob = await res.blob()
|
|
910
|
+
const a = document.createElement('a')
|
|
911
|
+
a.href = URL.createObjectURL(blob)
|
|
912
|
+
a.download = isDirectory ? name + '.tar.gz' : name
|
|
913
|
+
document.body.appendChild(a)
|
|
914
|
+
a.click()
|
|
915
|
+
document.body.removeChild(a)
|
|
916
|
+
URL.revokeObjectURL(a.href)
|
|
917
|
+
}
|
|
918
|
+
|
|
885
919
|
newItem = (isDirectory) => {
|
|
886
920
|
const { type } = this.state.file
|
|
887
921
|
const list = copy(this.props[type])
|
|
@@ -1060,6 +1094,13 @@ export default class FileSection extends React.Component {
|
|
|
1060
1094
|
text: e('showInDefaultFileMananger')
|
|
1061
1095
|
})
|
|
1062
1096
|
}
|
|
1097
|
+
if (isLocal && isRealFile && window.et.isWebApp) {
|
|
1098
|
+
res.push({
|
|
1099
|
+
func: 'downloadFromBrowser',
|
|
1100
|
+
icon: 'DownloadOutlined',
|
|
1101
|
+
text: 'Download from browser'
|
|
1102
|
+
})
|
|
1103
|
+
}
|
|
1063
1104
|
if (showEdit) {
|
|
1064
1105
|
res.push({
|
|
1065
1106
|
func: 'editFile',
|
|
@@ -7,6 +7,12 @@ import fs from '../../common/fs'
|
|
|
7
7
|
import { isWin } from '../../common/constants'
|
|
8
8
|
|
|
9
9
|
export const getFileExt = fileName => {
|
|
10
|
+
if (/^\\\\(?:wsl\$|wsl\.localhost)\\/.test(fileName)) {
|
|
11
|
+
return {
|
|
12
|
+
base: fileName,
|
|
13
|
+
ext: ''
|
|
14
|
+
}
|
|
15
|
+
}
|
|
10
16
|
const sep = '.'
|
|
11
17
|
const arr = fileName.split(sep)
|
|
12
18
|
const len = arr.length
|
|
@@ -82,12 +88,15 @@ export const getFolderFromFilePath = (filePath, isRemote) => {
|
|
|
82
88
|
const arr = filePath.split(sep)
|
|
83
89
|
const len = arr.length
|
|
84
90
|
const isWinDisk = isWin && filePath.endsWith(sep)
|
|
85
|
-
const
|
|
91
|
+
const isWslRoot = isWin && /^\\\\(?:wsl\$|wsl\.localhost)\\[^\\]+$/.test(filePath.replace(/\\$/, ''))
|
|
92
|
+
const path = (isWinDisk || isWslRoot)
|
|
86
93
|
? '/'
|
|
87
94
|
: arr.slice(0, len - 1).join(sep)
|
|
88
95
|
const name = isWinDisk
|
|
89
96
|
? filePath.replace(sep, '')
|
|
90
|
-
:
|
|
97
|
+
: isWslRoot
|
|
98
|
+
? filePath
|
|
99
|
+
: arr[len - 1]
|
|
91
100
|
|
|
92
101
|
return {
|
|
93
102
|
path,
|
|
@@ -29,6 +29,7 @@ import fs from '../../common/fs'
|
|
|
29
29
|
import ListTable from './list-table-ui'
|
|
30
30
|
import deepCopy from 'json-deep-copy'
|
|
31
31
|
import isValidPath from '../../common/is-valid-path'
|
|
32
|
+
import normalizeRemotePath from '../../common/normalize-remote-path'
|
|
32
33
|
import { LoadingOutlined, ReloadOutlined } from '@ant-design/icons'
|
|
33
34
|
import * as owner from './owner-list'
|
|
34
35
|
import AddressBar from './address-bar'
|
|
@@ -292,6 +293,7 @@ export default class Sftp extends Component {
|
|
|
292
293
|
if (!path && this.sftp) {
|
|
293
294
|
path = await this.getPwd(this.props.tab.username)
|
|
294
295
|
}
|
|
296
|
+
path = normalizeRemotePath(path)
|
|
295
297
|
} else {
|
|
296
298
|
path = this.getLocalHome()
|
|
297
299
|
}
|
|
@@ -762,7 +764,7 @@ export default class Sftp extends Component {
|
|
|
762
764
|
|
|
763
765
|
if (!remotePath) {
|
|
764
766
|
if (startDirectory) {
|
|
765
|
-
remotePath = startDirectory
|
|
767
|
+
remotePath = normalizeRemotePath(startDirectory)
|
|
766
768
|
} else {
|
|
767
769
|
remotePath = await this.getPwd(username)
|
|
768
770
|
}
|
|
@@ -970,6 +972,31 @@ export default class Sftp extends Component {
|
|
|
970
972
|
})
|
|
971
973
|
}
|
|
972
974
|
|
|
975
|
+
handleUploadFromBrowser = () => {
|
|
976
|
+
const input = document.createElement('input')
|
|
977
|
+
input.type = 'file'
|
|
978
|
+
input.multiple = true
|
|
979
|
+
input.onchange = async () => {
|
|
980
|
+
const files = input.files
|
|
981
|
+
if (!files || !files.length) return
|
|
982
|
+
const { localPath } = this.state
|
|
983
|
+
for (const file of files) {
|
|
984
|
+
const formData = new FormData()
|
|
985
|
+
formData.append('file', file)
|
|
986
|
+
formData.append('path', localPath)
|
|
987
|
+
await window.fetch('/api/upload', {
|
|
988
|
+
method: 'POST',
|
|
989
|
+
body: formData,
|
|
990
|
+
headers: {
|
|
991
|
+
token: window.store?.config.tokenElecterm
|
|
992
|
+
}
|
|
993
|
+
})
|
|
994
|
+
}
|
|
995
|
+
this.localList()
|
|
996
|
+
}
|
|
997
|
+
input.click()
|
|
998
|
+
}
|
|
999
|
+
|
|
973
1000
|
parsePath = async (type, pth) => {
|
|
974
1001
|
const reg = /^%([^%]+)%/
|
|
975
1002
|
if (!reg.test(pth)) {
|
|
@@ -995,7 +1022,10 @@ export default class Sftp extends Component {
|
|
|
995
1022
|
const n = `${type}Path`
|
|
996
1023
|
const nt = n + 'Temp'
|
|
997
1024
|
const oldPath = this.state[type + 'Path']
|
|
998
|
-
|
|
1025
|
+
let np = await this.parsePath(type, this.state[nt])
|
|
1026
|
+
if (type === typeMap.remote) {
|
|
1027
|
+
np = normalizeRemotePath(np)
|
|
1028
|
+
}
|
|
999
1029
|
if (!isValidPath(np)) {
|
|
1000
1030
|
return notification.warning({
|
|
1001
1031
|
message: 'path not valid'
|
|
@@ -1003,6 +1033,7 @@ export default class Sftp extends Component {
|
|
|
1003
1033
|
}
|
|
1004
1034
|
this.setState({
|
|
1005
1035
|
[n]: np,
|
|
1036
|
+
[nt]: np,
|
|
1006
1037
|
[`${type}Keyword`]: ''
|
|
1007
1038
|
}, () => this[`${type}List`](undefined, undefined, oldPath))
|
|
1008
1039
|
}
|
|
@@ -1010,7 +1041,10 @@ export default class Sftp extends Component {
|
|
|
1010
1041
|
goParent = (type) => {
|
|
1011
1042
|
const n = `${type}Path`
|
|
1012
1043
|
const p = this.state[n]
|
|
1013
|
-
|
|
1044
|
+
let np = resolve(p, '..')
|
|
1045
|
+
if (type === typeMap.remote) {
|
|
1046
|
+
np = normalizeRemotePath(np)
|
|
1047
|
+
}
|
|
1014
1048
|
const op = this.state[n]
|
|
1015
1049
|
if (np !== p) {
|
|
1016
1050
|
this.setState({
|
|
@@ -1218,6 +1252,7 @@ export default class Sftp extends Component {
|
|
|
1218
1252
|
const addrProps = {
|
|
1219
1253
|
host,
|
|
1220
1254
|
type,
|
|
1255
|
+
handleUploadFromBrowser: this.handleUploadFromBrowser,
|
|
1221
1256
|
...pick(
|
|
1222
1257
|
this,
|
|
1223
1258
|
[
|
|
@@ -24,7 +24,11 @@ import {
|
|
|
24
24
|
LockOutlined,
|
|
25
25
|
ReloadOutlined,
|
|
26
26
|
FileZipOutlined,
|
|
27
|
-
AppstoreOutlined
|
|
27
|
+
AppstoreOutlined,
|
|
28
|
+
SaveOutlined,
|
|
29
|
+
PlayCircleFilled,
|
|
30
|
+
StopOutlined,
|
|
31
|
+
DownloadOutlined
|
|
28
32
|
} from '@ant-design/icons'
|
|
29
33
|
import IconHolder from './icon-holder'
|
|
30
34
|
|
|
@@ -52,5 +56,9 @@ export default {
|
|
|
52
56
|
LockOutlined,
|
|
53
57
|
ReloadOutlined,
|
|
54
58
|
FileZipOutlined,
|
|
55
|
-
AppstoreOutlined
|
|
59
|
+
AppstoreOutlined,
|
|
60
|
+
SaveOutlined,
|
|
61
|
+
PlayCircleFilled,
|
|
62
|
+
StopOutlined,
|
|
63
|
+
DownloadOutlined
|
|
56
64
|
}
|