diff --git a/orion-visor-ui/src/api/asset/host-sftp.ts b/orion-visor-ui/src/api/asset/host-sftp.ts index fda18c3d..d874bbd8 100644 --- a/orion-visor-ui/src/api/asset/host-sftp.ts +++ b/orion-visor-ui/src/api/asset/host-sftp.ts @@ -65,7 +65,7 @@ export function deleteHostSftpLog(idList: Array) { /** * 下载文件 */ -export function downloadWithTransferToken(channelId: string, transferToken: string) { - window.open(`${httpBaseUrl}/asset/host-sftp/download?channelId=${channelId}&transferToken=${transferToken}`, 'newWindow'); +export function getDownloadTransferUrl(channelId: string, transferToken: string) { + return `${httpBaseUrl}/asset/host-sftp/download?channelId=${channelId}&transferToken=${transferToken}`; } diff --git a/orion-visor-ui/src/utils/file.ts b/orion-visor-ui/src/utils/file.ts index dcc80f41..bdcdec9b 100644 --- a/orion-visor-ui/src/utils/file.ts +++ b/orion-visor-ui/src/utils/file.ts @@ -84,6 +84,27 @@ export function getParentPath(path: string) { return parent; } +/** + * 打开下载文件 + */ +export function openDownloadFile(url: string) { + try { + // 创建隐藏的可下载链接 + let element = document.createElement('a'); + element.setAttribute('href', url); + element.setAttribute('download', ''); + element.style.display = 'none'; + // 将其附加到文档中 + document.body.appendChild(element); + // 点击该下载链接 + element.click(); + // 移除已下载的链接 + document.body.removeChild(element); + } catch (e) { + window.open(url, 'newWindow'); + } +} + /** * 下载文件 */ diff --git a/orion-visor-ui/src/views/exec/exec-command/types/const.ts b/orion-visor-ui/src/views/exec/exec-command/types/const.ts index b88ef694..38302e24 100644 --- a/orion-visor-ui/src/views/exec/exec-command/types/const.ts +++ b/orion-visor-ui/src/views/exec/exec-command/types/const.ts @@ -1,2 +1,2 @@ -// 执行 -export const historyCount = 20; +// 历史数量 +export const historyCount = 15; diff --git a/orion-visor-ui/src/views/host/terminal/handler/sftp-transfer-manager.ts b/orion-visor-ui/src/views/host/terminal/handler/sftp-transfer-manager.ts index 2aeffa3f..ed395cd5 100644 --- a/orion-visor-ui/src/views/host/terminal/handler/sftp-transfer-manager.ts +++ b/orion-visor-ui/src/views/host/terminal/handler/sftp-transfer-manager.ts @@ -1,12 +1,13 @@ import type { ISftpTransferManager, ISftpTransferUploader, SftpTransferItem } from '../types/terminal.type'; import { ISftpTransferDownloader, SftpFile, TransferOperatorResponse } from '../types/terminal.type'; -import { TransferReceiverType, TransferStatus, TransferType } from '../types/terminal.const'; +import { sessionCloseMsg, TransferReceiverType, TransferStatus, TransferType } from '../types/terminal.const'; import { Message } from '@arco-design/web-vue'; import { getTerminalAccessToken, openHostTransferChannel } from '@/api/asset/host-terminal'; import { nextId } from '@/utils'; -import { downloadWithTransferToken } from '@/api/asset/host-sftp'; +import { getDownloadTransferUrl } from '@/api/asset/host-sftp'; import SftpTransferUploader from './sftp-transfer-uploader'; import SftpTransferDownloader from './sftp-transfer-downloader'; +import { openDownloadFile } from '@/utils/file'; // sftp 传输管理器实现 export default class SftpTransferManager implements ISftpTransferManager { @@ -257,7 +258,10 @@ export default class SftpTransferManager implements ISftpTransferManager { // 接收开始下载响应 private resolveDownloadStart(data: TransferOperatorResponse) { - downloadWithTransferToken(data.channelId as string, data.transferToken as string); + // 获取下载 url + const url = getDownloadTransferUrl(data.channelId as string, data.transferToken as string); + // 打开 + openDownloadFile(url); } // 接收下载进度响应 @@ -287,6 +291,14 @@ export default class SftpTransferManager implements ISftpTransferManager { this.run = false; // 关闭传输进度 clearInterval(this.progressIntervalId); + // 进行中和等待中的文件改为失败 + this.transferList.forEach(s => { + if (s.status === TransferStatus.WAITING || + s.status === TransferStatus.TRANSFERRING) { + s.status = TransferStatus.ERROR; + s.errorMessage = sessionCloseMsg; + } + }); } } diff --git a/orion-visor-ui/src/views/host/terminal/handler/terminal-channel.ts b/orion-visor-ui/src/views/host/terminal/handler/terminal-channel.ts index c589b3c7..1fde2bd8 100644 --- a/orion-visor-ui/src/views/host/terminal/handler/terminal-channel.ts +++ b/orion-visor-ui/src/views/host/terminal/handler/terminal-channel.ts @@ -1,5 +1,6 @@ -import type { InputPayload, ITerminalChannel, ITerminalOutputProcessor, ITerminalSessionManager, OutputPayload, Protocol, } from '../types/terminal.type'; -import { OutputProtocol } from '../types/terminal.protocol'; +import type { InputPayload, ITerminalChannel, ITerminalOutputProcessor, ITerminalSessionManager, Protocol, } from '../types/terminal.type'; +import { format, OutputProtocol, parse } from '../types/terminal.protocol'; +import { sessionCloseMsg } from '../types/terminal.const'; import { getTerminalAccessToken, openHostTerminalChannel } from '@/api/asset/host-terminal'; import { Message } from '@arco-design/web-vue'; import TerminalOutputProcessor from './terminal-output-processor'; @@ -9,9 +10,12 @@ export default class TerminalChannel implements ITerminalChannel { private client?: WebSocket; + private readonly sessionManager: ITerminalSessionManager; + private readonly processor: ITerminalOutputProcessor; constructor(sessionManager: ITerminalSessionManager) { + this.sessionManager = sessionManager; this.processor = new TerminalOutputProcessor(sessionManager, this); } @@ -29,6 +33,8 @@ export default class TerminalChannel implements ITerminalChannel { } this.client.onclose = event => { console.warn('terminal close', event); + // 关闭回调 + this.closeCallback(); }; this.client.onmessage = this.handlerMessage.bind(this); } @@ -66,6 +72,25 @@ export default class TerminalChannel implements ITerminalChannel { } } + // 关闭回调 + private closeCallback(): void { + // 关闭时将手动触发 close 消息, 有可能是其他原因关闭的, 没有接收到 close 消息, 导致已断开是终端还是显示已连接 + Object.values(this.sessionManager.sessions).forEach(s => { + if (!s?.connected) { + return; + } + // close 消息 + const data = format(OutputProtocol.CLOSE, { + type: OutputProtocol.CLOSE.type, + sessionId: s.sessionId, + forceClose: 0, + msg: sessionCloseMsg, + }); + // 触发 close 消息 + this.handlerMessage({ data } as MessageEvent); + }); + } + // 关闭 close(): void { // 关闭 client @@ -78,55 +103,3 @@ export default class TerminalChannel implements ITerminalChannel { } } - -// 分隔符 -export const SEPARATOR = '|'; - -// 解析参数 -export const parse = (payload: string) => { - const protocols = Object.values(OutputProtocol); - const useProtocol = protocols.find(p => payload.startsWith(p.type + SEPARATOR) || p.type === payload); - if (!useProtocol) { - return undefined; - } - const template = useProtocol.template; - const res = {} as OutputPayload; - let curr = 0; - let len = payload.length; - for (let i = 0, pl = template.length; i < pl; i++) { - if (i == pl - 1) { - // 最后一次 - res[template[i]] = payload.substring(curr, len); - } else { - // 非最后一次 - let tmp = ''; - for (; curr < len; curr++) { - const c = payload.charAt(curr); - if (c == SEPARATOR) { - res[template[i]] = tmp; - curr++; - break; - } else { - tmp += c; - } - } - } - } - return res; -}; - -// 格式化参数 -export const format = (protocol: Protocol, payload: InputPayload) => { - payload.type = protocol.type; - return protocol.template - .map(i => getPayloadValueString(payload[i])) - .join(SEPARATOR); -}; - -// 获取默认值 -export const getPayloadValueString = (value: unknown): any => { - if (value === undefined || value === null) { - return ''; - } - return value; -}; diff --git a/orion-visor-ui/src/views/host/terminal/handler/terminal-output-processor.ts b/orion-visor-ui/src/views/host/terminal/handler/terminal-output-processor.ts index 4cd68581..2a3a563d 100644 --- a/orion-visor-ui/src/views/host/terminal/handler/terminal-output-processor.ts +++ b/orion-visor-ui/src/views/host/terminal/handler/terminal-output-processor.ts @@ -85,7 +85,7 @@ export default class TerminalOutputProcessor implements ITerminalOutputProcessor } // 处理关闭消息 - processClose({ sessionId, msg, forceClose }: OutputPayload): void { + processClose({ sessionId, forceClose, msg }: OutputPayload): void { const session = this.sessionManager.getSession(sessionId); // 无需处理 (直接关闭 tab) if (!session) { diff --git a/orion-visor-ui/src/views/host/terminal/handler/terminal-session-manager.ts b/orion-visor-ui/src/views/host/terminal/handler/terminal-session-manager.ts index 52309f4d..3bcd820e 100644 --- a/orion-visor-ui/src/views/host/terminal/handler/terminal-session-manager.ts +++ b/orion-visor-ui/src/views/host/terminal/handler/terminal-session-manager.ts @@ -19,17 +19,17 @@ import SftpSession from './sftp-session'; // 终端会话管理器实现 export default class TerminalSessionManager implements ITerminalSessionManager { - private readonly channel: ITerminalChannel; + public sessions: Record; - private sessions: Record; + private readonly channel: ITerminalChannel; private keepAliveTaskId?: any; private readonly dispatchResizeFn: () => {}; constructor() { - this.channel = new TerminalChannel(this); this.sessions = {}; + this.channel = new TerminalChannel(this); this.dispatchResizeFn = useDebounceFn(this.dispatchResize).bind(this); } diff --git a/orion-visor-ui/src/views/host/terminal/types/terminal.const.ts b/orion-visor-ui/src/views/host/terminal/types/terminal.const.ts index 6c0452e0..b48918d1 100644 --- a/orion-visor-ui/src/views/host/terminal/types/terminal.const.ts +++ b/orion-visor-ui/src/views/host/terminal/types/terminal.const.ts @@ -352,6 +352,9 @@ export const TransferReceiverType = { DOWNLOAD_ERROR: 'downloadError', }; +// 会话关闭信息 +export const sessionCloseMsg = 'session closed...'; + // 打开 settingModal key export const openSettingModalKey = Symbol(); diff --git a/orion-visor-ui/src/views/host/terminal/types/terminal.protocol.ts b/orion-visor-ui/src/views/host/terminal/types/terminal.protocol.ts index a56a3142..a9abebdf 100644 --- a/orion-visor-ui/src/views/host/terminal/types/terminal.protocol.ts +++ b/orion-visor-ui/src/views/host/terminal/types/terminal.protocol.ts @@ -1,3 +1,8 @@ +import type { InputPayload, OutputPayload, Protocol } from './terminal.type'; + +// 分隔符 +export const SEPARATOR = '|'; + // 输入协议 export const InputProtocol = { // 主机连接检查 @@ -164,3 +169,52 @@ export const OutputProtocol = { processMethod: 'processSftpSetContent' }, }; + +// 解析参数 +export const parse = (payload: string) => { + const protocols = Object.values(OutputProtocol); + const useProtocol = protocols.find(p => payload.startsWith(p.type + SEPARATOR) || p.type === payload); + if (!useProtocol) { + return undefined; + } + const template = useProtocol.template; + const res = {} as OutputPayload; + let curr = 0; + let len = payload.length; + for (let i = 0, pl = template.length; i < pl; i++) { + if (i == pl - 1) { + // 最后一次 + res[template[i]] = payload.substring(curr, len); + } else { + // 非最后一次 + let tmp = ''; + for (; curr < len; curr++) { + const c = payload.charAt(curr); + if (c == SEPARATOR) { + res[template[i]] = tmp; + curr++; + break; + } else { + tmp += c; + } + } + } + } + return res; +}; + +// 格式化参数 +export const format = (protocol: Protocol, payload: InputPayload | OutputPayload) => { + payload.type = protocol.type; + return protocol.template + .map(i => getPayloadValueString(payload[i])) + .join(SEPARATOR); +}; + +// 获取默认值 +export const getPayloadValueString = (value: unknown): any => { + if (value === undefined || value === null) { + return ''; + } + return value; +}; diff --git a/orion-visor-ui/src/views/host/terminal/types/terminal.type.ts b/orion-visor-ui/src/views/host/terminal/types/terminal.type.ts index a8acb4bf..e1668a8d 100644 --- a/orion-visor-ui/src/views/host/terminal/types/terminal.type.ts +++ b/orion-visor-ui/src/views/host/terminal/types/terminal.type.ts @@ -153,6 +153,9 @@ export interface ITerminalPanelManager { // 终端会话管理器定义 export interface ITerminalSessionManager { + // 全部会话 + sessions: Record; + // 打开 ssh 会话 openSsh: (tab: TerminalPanelTabItem, domRef: XtermDomRef) => Promise; // 打开 sftp 会话