🔨 添加 vnc 会话.

This commit is contained in:
lijiahangmax
2025-07-07 14:48:34 +08:00
parent 1abc47bb56
commit f34dc75f41
13 changed files with 677 additions and 248 deletions

View File

@@ -32,15 +32,25 @@
<span class="tab-title-icon">
<component :is="item.icon" />
</span>
{{ item.title }}
<span>{{ item.title }}</span>
</span>
</template>
<!-- ssh -->
<ssh-view v-if="item.type === TerminalSessionTypes.SSH.type" :item="item" />
<ssh-view v-if="item.type === TerminalSessionTypes.SSH.type"
class="session-container"
:item="item" />
<!-- sftp -->
<sftp-view v-else-if="item.type === TerminalSessionTypes.SFTP.type" :item="item" />
<sftp-view v-else-if="item.type === TerminalSessionTypes.SFTP.type"
class="session-container"
:item="item" />
<!-- rdp -->
<rdp-view v-else-if="item.type === TerminalSessionTypes.RDP.type" :item="item" />
<rdp-view v-else-if="item.type === TerminalSessionTypes.RDP.type"
class="session-container"
:item="item" />
<!-- vnc -->
<vnc-view v-else-if="item.type === TerminalSessionTypes.VNC.type"
class="session-container"
:item="item" />
</a-tab-pane>
</a-tabs>
</div>
@@ -60,6 +70,7 @@
import SshView from '../view/ssh/ssh-view.vue';
import SftpView from '../view/sftp/sftp-view.vue';
import RdpView from '../view/rdp/rdp-view.vue';
import VncView from '../view/vnc/vnc-view.vue';
const props = defineProps<{
index: number;
@@ -118,56 +129,62 @@
.terminal-panel-container {
width: 100%;
height: 100%;
}
.tab-title-wrapper {
width: 100%;
height: 100%;
display: flex;
align-items: center;
padding: 11px 18px 9px 14px;
background: var(--bg);
position: relative;
transition: all .3s;
.tab-title-icon {
font-size: 16px;
margin-right: 6px;
}
&:hover {
filter: brightness(1.04);
}
&::after {
content: '';
width: calc(100% - 3px);
height: 2px;
background: var(--color);
position: absolute;
left: 1px;
bottom: -1px;
}
}
.panel-extra {
margin-right: 8px;
.extra-icon {
color: var(--color-panel-text-1);
transition: 0.2s;
font-size: 16px;
cursor: pointer;
width: 24px;
height: 24px;
.tab-title-wrapper {
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
border-radius: 50%;
padding: 4px 18px 4px 14px;
background: var(--bg);
position: relative;
transition: all .3s;
.tab-title-icon {
font-size: 16px;
margin-right: 6px;
}
&:hover {
background: var(--color-bg-panel-icon-1);
filter: brightness(1.04);
}
&::after {
content: '';
width: calc(100% - 3px);
height: 2px;
background: var(--color);
position: absolute;
left: 1px;
bottom: -1px;
}
}
.panel-extra {
margin-right: 8px;
.extra-icon {
color: var(--color-panel-text-1);
transition: 0.2s;
font-size: 16px;
cursor: pointer;
width: 24px;
height: 24px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 50%;
&:hover {
background: var(--color-bg-panel-icon-1);
}
}
}
.session-container {
width: 100%;
height: 100%;
position: relative;
}
}

View File

@@ -0,0 +1,208 @@
<template>
<div v-if="session.state.connectStatus === TerminalStatus.CONNECTED">
<!-- 工具栏 -->
<a-popover v-model:popup-visible="visible"
:title="undefined"
trigger="click"
:content-class="['guacd-action-bar-popover', direction]"
:content-style="{ '--action-count': Math.max(actions.length, direction === ActionBarPosition.RIGHT ? 5 : 6) }"
:position="direction === ActionBarPosition.RIGHT ? 'left' : 'bottom'"
:show-arrow="false"
:auto-fix-position="false">
<!-- 触发器 -->
<div class="action-bar-trigger" :class="[direction === ActionBarPosition.RIGHT ? 'right' : 'top']" />
<!-- 工具内容 -->
<template #content>
<!-- 按钮 -->
<a-space class="action-bar-actions"
:direction="direction === ActionBarPosition.RIGHT ? 'vertical' : 'horizontal'"
:size="16">
<div v-for="action in actions" :key="action.item">
<a-tooltip :mini="true"
:auto-fix-position="false"
:position="direction === ActionBarPosition.RIGHT ? 'left' : 'bottom'"
content-class="terminal-tooltip-content"
:show-arrow="false"
:content="action.content">
<a-button class="action-bar-button"
:disabled="action.disabled"
:type="action.active ? 'primary' : 'secondary'"
@click="toggleClickAction(action.item)">
<template #icon>
<component :is="action.icon" />
</template>
</a-button>
</a-tooltip>
</div>
</a-space>
<!-- 显示设置 -->
<div v-if="current === GuacdActionItemKeys.DISPLAY" class="action-bar-content">
<!-- 分辨率 -->
<a-space>
<span class="display-size-label">分辨率</span>
<a-select v-model="displaySize"
class="display-size-input"
placeholder="请选择分辨率"
:options="toOptions(screenResolutionKey)"
allow-create />
</a-space>
<!-- 按钮 -->
<a-space class="action-bar-content-footer">
<a-button type="primary"
size="small"
@click="fitOnce">
临时自适应
</a-button>
<a-button type="primary"
size="small"
@click="setDisplaySize">
设置
</a-button>
</a-space>
</div>
<!-- 组合键 -->
<div v-else-if="current === GuacdActionItemKeys.COMBINATION_KEY" class="action-bar-content">
<a-row :gutter="[12, 12]" wrap>
<a-col v-for="item in GuacdCombinationKeyItems"
:key="item.name"
:span="12"
class="combination-key-item"
@click="triggerCombinationKey(item.keys)">
<span>{{ item.name }}</span>
</a-col>
</a-row>
</div>
<!-- 剪切板 -->
<div v-else-if="current === GuacdActionItemKeys.CLIPBOARD" class="action-bar-content">
<a-textarea class="action-bar-clipboard"
v-model="clipboardData"
:ref="setAutoFocus"
placeholder="远程剪切板"
allow-clear />
<!-- 按钮 -->
<a-space class="action-bar-content-footer">
<a-button size="small" @click="clearClipboardData">
清空
</a-button>
<a-button type="primary"
size="small"
:disabled="!clipboardData"
@click="sendClipboardData">
发送
</a-button>
</a-space>
</div>
</template>
</a-popover>
</div>
</template>
<script lang="ts">
export default {
name: 'vncActionBar'
};
</script>
<script lang="ts" setup>
import type { IVncSession } from '@/views/terminal/interfaces';
import {
TerminalStatus,
GuacdCombinationKeyItems,
GuacdActionItemKeys,
VncActionBarItems,
screenResolutionKey,
fitDisplayValue, ActionBarPosition,
} from '@/views/terminal/types/const';
import { computed, ref, watch, onMounted } from 'vue';
import { setAutoFocus } from '@/utils/dom';
import { readText } from '@/hooks/copy';
import { useTerminalStore, useDictStore } from '@/store';
import useGuacdActionBar from '@/views/terminal/types/use-guacd-action-bar';
import useVisible from '@/hooks/visible';
const props = defineProps<{
session: IVncSession;
direction: string;
}>();
const { preference } = useTerminalStore();
const { toOptions, getDictValue } = useDictStore();
const { visible, setVisible } = useVisible();
const {
displaySize,
clipboardData,
fitOnce,
setDisplaySize,
triggerCombinationKey,
sendClipboardData,
clearClipboardData,
disconnect,
} = useGuacdActionBar({
session: props.session,
setVisible,
});
const current = ref('');
const actions = computed(() => {
return VncActionBarItems.filter(item => {
return preference.vncActionBarSetting[item.item] !== false;
}).map(item => {
const key = item.item;
return {
...item,
active: current.value === key,
disabled: (key === GuacdActionItemKeys.DISPLAY || GuacdActionItemKeys.DISCONNECT || key === GuacdActionItemKeys.CLOSE) ? false : !props.session.isWriteable(),
};
});
});
// 修改数据值
watch(() => visible.value, (val) => {
if (!val) {
return;
}
// 重新触发点击
toggleClickAction(current.value);
});
// 触发 action
const toggleClickAction = (key: string) => {
if (key === GuacdActionItemKeys.DISPLAY) {
// 显示设置
current.value = GuacdActionItemKeys.DISPLAY;
if (props.session.displayHandler?.autoFit) {
displaySize.value = fitDisplayValue;
} else {
displaySize.value = `${props.session.displayHandler?.displayWidth || 0}x${props.session.displayHandler?.displayHeight || 0}`;
}
} else if (key === GuacdActionItemKeys.COMBINATION_KEY) {
// 组合键
current.value = GuacdActionItemKeys.COMBINATION_KEY;
} else if (key === GuacdActionItemKeys.CLIPBOARD) {
// 剪切板
current.value = GuacdActionItemKeys.CLIPBOARD;
readText(false)
.then(s => clipboardData.value = s)
.catch(() => clipboardData.value = '');
} else if (key === GuacdActionItemKeys.DISCONNECT) {
// 断开连接
disconnect();
} else if (key === GuacdActionItemKeys.CLOSE) {
// 关闭工具栏
setVisible(false);
}
};
// 设置选中
onMounted(() => {
if (actions.value?.length) {
current.value = actions.value[0].item;
}
});
</script>
<style lang="less" scoped>
</style>

View File

@@ -0,0 +1,64 @@
<template>
<div class="guacd-container">
<!-- 状态 -->
<guacd-status v-if="session"
class="guacd-status-mask"
:session="session" />
<!-- 工具栏 -->
<vnc-action-bar v-if="session"
class="guacd-action-bar"
:class="[actionBarDirection === 'right' ? 'right' : 'top']"
:session="session"
:direction="actionBarDirection" />
<!-- 视口 -->
<div class="guacd-viewport" ref="viewport" />
</div>
</template>
<script lang="ts">
export default {
name: 'vnc-view'
};
</script>
<script lang="ts" setup>
import type { TerminalSessionTabItem, IVncSession } from '@/views/terminal/interfaces';
import { onMounted, ref, onUnmounted } from 'vue';
import { useTerminalStore } from '@/store';
import { ActionBarPosition } from '@/views/terminal/types/const';
import GuacdStatus from '../guacd/guacd-status.vue';
import VncActionBar from './vnc-action-bar.vue';
const props = defineProps<{
item: TerminalSessionTabItem;
}>();
const { preference, sessionManager } = useTerminalStore();
const actionBarDirection = ref(ActionBarPosition.TOP);
const viewport = ref();
const session = ref<IVncSession>();
// 初始化会话
onMounted(async () => {
// 工具栏方向
actionBarDirection.value = preference.vncActionBarSetting.position || ActionBarPosition.TOP;
// 创建终端会话
session.value = sessionManager.createSession<IVncSession>(props.item);
// 打开终端会话
await sessionManager.openVnc(props.item, {
viewport: viewport.value,
});
});
// 关闭会话
onUnmounted(() => {
if (props.item.key) {
sessionManager.closeSession(props.item.key);
}
});
</script>
<style lang="less" scoped>
</style>

View File

@@ -1,4 +1,4 @@
import type { GuacdReactiveSessionStatus, IGuacdChannel, ITerminalSession } from '@/views/terminal/interfaces';
import type { ITerminalSession, IGuacdChannel, GuacdReactiveSessionStatus } from '@/views/terminal/interfaces';
import type { OutputPayload } from '../../types/protocol';
import { InputProtocol, OutputProtocol } from '../../types/protocol';
import { TerminalCloseCode, TerminalMessages } from '@/views/terminal/types/const';
@@ -147,7 +147,8 @@ export default abstract class BaseGuacdChannel<T extends ITerminalSession<GuacdR
if (Date.now() < this.lastSentTime + PING_FREQUENCY) {
return;
}
this.sendInstruction(Guacamole.Tunnel.INTERNAL_DATA_OPCODE, 'ping', Date.now());
// this.sendInstruction(Guacamole.Tunnel.INTERNAL_DATA_OPCODE, 'ping', Date.now());
this.send(InputProtocol.PING);
}
// 发送指令 guacd 内部调用

View File

@@ -5,7 +5,7 @@ import { TerminalSessionTypes } from '@/views/terminal/types/const';
import { getTerminalAccessToken, openTerminalAccessChannel } from '@/api/terminal/terminal';
import BaseGuacdChannel from './base-guacd-channel';
// 终端通信会话 Rdp 会话实现
// 终端通信会话 RDP 会话实现
export default class RdpChannel extends BaseGuacdChannel<IRdpSession> {
// 打开 channel
@@ -20,7 +20,7 @@ export default class RdpChannel extends BaseGuacdChannel<IRdpSession> {
enableAudioInput: sessionSetting.enableAudioInput,
enableAudioOutput: sessionSetting.enableAudioOutput,
driveMountMode: sessionSetting.driveMountMode,
colorDepth: graphSetting.colorDepth || 16,
colorDepth: graphSetting.colorDepth || 24,
forceLossless: graphSetting.forceLossless,
enableWallpaper: graphSetting.enableWallpaper,
enableTheming: graphSetting.enableTheming,

View File

@@ -0,0 +1,34 @@
import type { IVncSession } from '@/views/terminal/interfaces';
import type { OutputPayload } from '@/views/terminal/types/protocol';
import { useTerminalStore } from '@/store';
import { TerminalSessionTypes } from '@/views/terminal/types/const';
import { getTerminalAccessToken, openTerminalAccessChannel } from '@/api/terminal/terminal';
import BaseGuacdChannel from './base-guacd-channel';
// 终端通信会话 VNC 会话实现
export default class VncChannel extends BaseGuacdChannel<IVncSession> {
// 打开 channel
protected async openChannel(): Promise<void> {
const setting = useTerminalStore().preference.vncGraphSetting;
const { data } = await getTerminalAccessToken({
hostId: this.session.info.hostId,
connectType: TerminalSessionTypes.VNC.type,
extra: {
colorDepth: setting.colorDepth || 24,
forceLossless: setting.forceLossless,
swapRedBlue: setting.swapRedBlue,
cursor: setting.cursor,
compressLevel: setting.compressLevel,
qualityLevel: setting.qualityLevel,
}
});
// 打开 channel
this.client = await openTerminalAccessChannel(TerminalSessionTypes.VNC.channel, data);
}
// 处理修改大小
processResize({ width, height }: OutputPayload): void {
}
}

View File

@@ -1,14 +1,14 @@
import type { IRdpSession, IRdpSessionClipboardHandler } from '@/views/terminal/interfaces';
import type { IGuacdSession, IGuacdSessionClipboardHandler } from '@/views/terminal/interfaces';
import Guacamole from 'guacamole-common-js';
import { copyToClipboard } from '@/hooks/copy';
import { isString } from '@/utils/is';
// rdp 会话剪切板处理器实现
export default class RdpSessionClipboardHandler implements IRdpSessionClipboardHandler {
// guacd 会话剪切板处理器实现
export default class GuacdSessionClipboardHandler implements IGuacdSessionClipboardHandler {
private readonly session: IRdpSession;
private readonly session: IGuacdSession;
constructor(session: IRdpSession) {
constructor(session: IGuacdSession) {
this.session = session;
}

View File

@@ -1,11 +1,11 @@
import type { IRdpSession, IRdpSessionDisplayHandler } from '@/views/terminal/interfaces';
import type { IGuacdSession, IGuacdSessionDisplayHandler } from '@/views/terminal/interfaces';
import { useDebounceFn } from '@vueuse/core';
import Guacamole from 'guacamole-common-js';
// rdp 会话视图处理器实现
export default class RdpSessionDisplayHandler implements IRdpSessionDisplayHandler {
// guacd 会话视图处理器实现
export default class GuacdSessionDisplayHandler implements IGuacdSessionDisplayHandler {
private readonly session: IRdpSession;
private readonly session: IGuacdSession;
public displayWidth: number;
public displayHeight: number;
@@ -21,7 +21,7 @@ export default class RdpSessionDisplayHandler implements IRdpSessionDisplayHandl
private readonly focusSink: () => void;
constructor(session: IRdpSession) {
constructor(session: IGuacdSession) {
this.session = session;
this.displayWidth = 0;
this.displayHeight = 0;

View File

@@ -0,0 +1,194 @@
import type {
GuacdInitConfig,
GuacdReactiveSessionStatus,
IGuacdChannel,
IGuacdSession,
IGuacdSessionClipboardHandler,
IGuacdSessionDisplayHandler,
TerminalSessionTabItem
} from '@/views/terminal/interfaces';
import type { OutputPayload } from '@/views/terminal/types/protocol';
import { InputProtocol } from '@/views/terminal/types/protocol';
import { TerminalCloseCode, TerminalMessages } from '@/views/terminal/types/const';
import { screenshot } from '@/views/terminal/types/utils';
import Guacamole from 'guacamole-common-js';
import BaseSession from './base-session';
import GuacdSessionClipboardHandler from '../handler/guacd-session-clipboard-handler';
export const CONNECT_TIMEOUT = 30000;
// guacd 会话基类
export default abstract class BaseGuacdSession extends BaseSession<GuacdReactiveSessionStatus, IGuacdChannel> implements IGuacdSession {
public config: GuacdInitConfig;
public client: Guacamole.Client;
public displayHandler: IGuacdSessionDisplayHandler;
public clipboardHandler: IGuacdSessionClipboardHandler;
protected connectTimeoutId?: number;
protected constructor(item: TerminalSessionTabItem) {
super(item, {
closeCode: 0,
closeMessage: ''
});
this.client = undefined as unknown as Guacamole.Client;
this.config = {} as unknown as GuacdInitConfig;
this.displayHandler = undefined as unknown as IGuacdSessionDisplayHandler;
this.clipboardHandler = undefined as unknown as IGuacdSessionClipboardHandler;
}
// 初始化
async init(config: GuacdInitConfig) {
this.config = config;
// 初始化
await this.reInit();
}
// 初始化 channel
async reInit(): Promise<void> {
// 初始化 channel
this.channel = this.createChannel();
// 创建 client
this.client = new Guacamole.Client(this.channel);
// 初始化 display
this.displayHandler = this.createDisplay();
// 初始化剪切板
this.clipboardHandler = this.createClipboard();
// 初始化 display
this.displayHandler.init();
// 初始化 channel
await this.channel.init();
// 注册 client 事件
this.registerClientEvent();
}
// 创建 channel
protected abstract createChannel(): IGuacdChannel;
// 创建 display
protected abstract createDisplay(): IGuacdSessionDisplayHandler;
// 创建 clipboard
protected createClipboard() {
// 创建 clipboard handler
return new GuacdSessionClipboardHandler(this);
}
// 注册 client 事件
protected registerClientEvent() {
// 错误回调
this.client.onerror = (state) => {
// 错误回调触发关闭
this.channel.closeTunnel(state.code, state.message || TerminalMessages.sessionClosed);
};
// 状态回调
this.client.onstatechange = (state) => {
if (state === Guacamole.Client.State.CONNECTED) {
// 触发连接成功回调
this.onConnected();
}
};
// 剪切板回调
this.client.onclipboard = this.clipboardHandler.receiveRemoteClipboardData.bind(this);
}
// 连接会话
connect(): void {
// 清空超时检查任务
window.clearTimeout(this.connectTimeoutId);
// 设置连接中
super.setConnecting();
// 连接 client 其实就是打开 channel 和 display
this.client.connect();
// 发送 connect 命令
this.channel.send(InputProtocol.CONNECT, {
body: JSON.stringify({
width: this.displayHandler?.displayWidth,
height: this.displayHandler?.displayHeight,
dpi: this.displayHandler?.displayDpi,
})
});
// 定时检查是否连接成功
this.connectTimeoutId = window.setTimeout(() => {
// 未连接上证明连接超时
if (!this.state.connected) {
this.channel.closeTunnel(TerminalCloseCode.CONNECT_TIMEOUT, TerminalMessages.connectTimeout);
}
}, CONNECT_TIMEOUT);
}
// 连接成功回调
protected onConnected() {
// 手动触发管道已连接
this.channel.processConnected({} as unknown as OutputPayload);
}
// 发送键
sendKeys(keys: Array<number>): void {
if (!this.isWriteable()) {
return;
}
for (let i = 0; i < keys.length; i++) {
this.client.sendKeyEvent(1, keys[i]);
}
for (let i = 0; i < keys.length; i++) {
this.client.sendKeyEvent(0, keys[i]);
}
}
// 粘贴
paste(data: string): void {
if (!this.isWriteable()) {
return;
}
// 发送至远程剪切板
this.clipboardHandler?.sendDataToRemoteClipboard(data);
// 发送粘贴命令
setTimeout(() => {
this.sendKeys([65507, 118]);
}, 100);
}
// 聚焦
focus(): void {
this.displayHandler?.focus?.();
}
// 失焦
blur(): void {
this.displayHandler?.blur?.();
}
// 自适应
fit(): void {
this.displayHandler?.fit(false);
}
// 修改大小
resize(width: number, height: number): void {
if (!this.isWriteable()) {
return;
}
// 发送重置大小
this.channel.send(InputProtocol.RESIZE, { width, height, });
}
// 截屏
async screenshot() {
await screenshot(this.client?.getDisplay()?.getElement() as HTMLElement);
}
// 断开连接
disconnect(): void {
super.disconnect();
// 关闭 client
this.client?.disconnect();
// 关闭超时检查任务
clearTimeout(this.connectTimeoutId);
}
}

View File

@@ -1,104 +1,49 @@
import type {
IGuacdChannel,
IRdpSession,
TerminalSessionTabItem,
GuacdInitConfig,
IRdpSessionDisplayHandler,
GuacdReactiveSessionStatus,
IRdpSessionClipboardHandler
} from '@/views/terminal/interfaces';
import type { OutputPayload } from '@/views/terminal/types/protocol';
import type { IGuacdChannel, IGuacdSessionDisplayHandler, IRdpSession, TerminalSessionTabItem } from '@/views/terminal/interfaces';
import { InputProtocol } from '@/views/terminal/types/protocol';
import { TerminalMessages, fitDisplayValue, TerminalCloseCode } from '@/views/terminal/types/const';
import { screenshot } from '@/views/terminal/types/utils';
import { fitDisplayValue } from '@/views/terminal/types/const';
import { Message } from '@arco-design/web-vue';
import { useTerminalStore } from '@/store';
import Guacamole from 'guacamole-common-js';
import BaseSession from './base-session';
import RdpChannel from '../channel/rdp-channel';
import RdpSessionDisplayHandler from '../handler/rdp-session-display-handler';
import RdpSessionClipboardHandler from '../handler/rdp-session-clipboard-handler';
import BaseGuacdSession from './base-guacd-session';
import GuacdSessionDisplayHandler from '../handler/guacd-session-display-handler';
export const AUDIO_INPUT_MIMETYPE = 'audio/L16;rate=44100,channels=2';
export const CONNECT_TIMEOUT = 30000;
// RDP 会话实现
export default class RdpSession extends BaseSession<GuacdReactiveSessionStatus, IGuacdChannel> implements IRdpSession {
export default class RdpSession extends BaseGuacdSession implements IRdpSession {
public fileSystemName: string;
public config: GuacdInitConfig;
public client: Guacamole.Client;
public displayHandler: IRdpSessionDisplayHandler;
public clipboardHandler: IRdpSessionClipboardHandler;
private connectTimeoutId?: number;
constructor(item: TerminalSessionTabItem) {
super(item, {
closeCode: 0,
closeMessage: ''
});
super(item);
this.fileSystemName = 'Shared Driver';
this.client = undefined as unknown as Guacamole.Client;
this.config = {} as unknown as GuacdInitConfig;
this.displayHandler = undefined as unknown as IRdpSessionDisplayHandler;
this.clipboardHandler = undefined as unknown as IRdpSessionClipboardHandler;
}
// 初始化
async init(config: GuacdInitConfig) {
this.config = config;
// 初始化
await this.reInit();
// 创建 channel
protected createChannel(): IGuacdChannel {
return new RdpChannel(this);
}
// 初始化 channel
async reInit(): Promise<void> {
// 创建 display
protected createDisplay(): IGuacdSessionDisplayHandler {
const rdpGraphSetting = useTerminalStore().preference.rdpGraphSetting;
// 创建 channel
this.channel = new RdpChannel(this);
// 创建 client
this.client = new Guacamole.Client(this.channel);
// 创建 display handler
this.displayHandler = new RdpSessionDisplayHandler(this);
// 创建 clipboard handler
this.clipboardHandler = new RdpSessionClipboardHandler(this);
// 设置 display autoFit
// 创建 display
const displayHandler = new GuacdSessionDisplayHandler(this);
// 设置自适应
const autoFit = rdpGraphSetting?.displaySize === fitDisplayValue;
this.displayHandler.autoFit = autoFit;
displayHandler.autoFit = autoFit;
// 非自适应设置分辨率
if (!autoFit) {
this.displayHandler.setDisplaySize(rdpGraphSetting?.displayWidth || 1024, rdpGraphSetting?.displayHeight || 768);
displayHandler.setDisplaySize(rdpGraphSetting?.displayWidth || 1024, rdpGraphSetting?.displayHeight || 768);
}
// 初始化 display
this.displayHandler.init();
// 初始化 channel
await this.channel.init();
// 注册 client 事件
this.registerClientEvent();
return displayHandler;
}
// 注册 client 事件
private registerClientEvent() {
// 错误回调
this.client.onerror = (state) => {
// 错误回调触发关闭
this.channel.closeTunnel(state.code, state.message || TerminalMessages.sessionClosed);
};
// 状态回调
this.client.onstatechange = (state) => {
if (state === Guacamole.Client.State.CONNECTED) {
// 触发已连接
this.onConnected();
}
};
// 剪切板回调
this.client.onclipboard = this.clipboardHandler.receiveRemoteClipboardData.bind(this);
// 文件系统回调
protected registerClientEvent() {
super.registerClientEvent();
// 注册文件系统回调
this.client.onfilesystem = (_, fileSystemName) => {
if (fileSystemName) {
this.fileSystemName = fileSystemName;
@@ -117,35 +62,9 @@ export default class RdpSession extends BaseSession<GuacdReactiveSessionStatus,
};
}
// 连接会话
connect(): void {
// 清空超时检查任务
window.clearTimeout(this.connectTimeoutId);
// 设置连接中
super.setConnecting();
// 连接 client 其实就是打开 channel 和 display
this.client.connect();
// 发送 connect 命令
this.channel.send(InputProtocol.CONNECT, {
body: JSON.stringify({
width: this.displayHandler?.displayWidth,
height: this.displayHandler?.displayHeight,
dpi: this.displayHandler?.displayDpi,
})
});
// 定时检查是否连接成功
this.connectTimeoutId = window.setTimeout(() => {
// 未连接上证明连接超时
if (!this.state.connected) {
this.channel.closeTunnel(TerminalCloseCode.CONNECT_TIMEOUT, TerminalMessages.rdpConnectTimeout);
}
}, CONNECT_TIMEOUT);
}
// 连接成功
private onConnected() {
// 手动触发管道已连接
this.channel.processConnected({} as unknown as OutputPayload);
// 连接成功回调
protected onConnected() {
super.onConnected();
// 监听音频输入
if (useTerminalStore().preference.rdpSessionSetting?.enableAudioInput) {
const requestAudioStream = (client: Guacamole.Client) => {
@@ -173,81 +92,11 @@ export default class RdpSession extends BaseSession<GuacdReactiveSessionStatus,
this.channel.send(InputProtocol.RDP_FILE_SYSTEM_EVENT, { event: JSON.stringify(event) });
}
// 发送键
sendKeys(keys: Array<number>): void {
if (!this.isWriteable()) {
return;
}
for (let i = 0; i < keys.length; i++) {
this.client.sendKeyEvent(1, keys[i]);
}
for (let i = 0; i < keys.length; i++) {
this.client.sendKeyEvent(0, keys[i]);
}
}
// 粘贴
paste(data: string): void {
if (!this.isWriteable()) {
return;
}
// 发送至远程剪切板
this.clipboardHandler?.sendDataToRemoteClipboard(data);
// 发送粘贴命令
setTimeout(() => {
this.sendKeys([65507, 118]);
}, 100);
}
// 聚焦
focus(): void {
this.displayHandler?.focus?.();
}
// 失焦
blur(): void {
this.displayHandler?.blur?.();
}
// 自适应
fit(): void {
this.displayHandler?.fit(false);
}
// 修改大小
resize(width: number, height: number): void {
if (!this.isWriteable()) {
return;
}
// 发送重置大小
this.channel.send(InputProtocol.RESIZE, { width, height, });
}
// 截屏
async screenshot() {
await screenshot(this.client?.getDisplay()?.getElement() as HTMLElement);
}
// 是否可写
isWriteable(): boolean {
return this.state.connected && this.state.canWrite;
}
// 设置为已关闭
setClosed() {
// 设置为已关闭
super.setClosed();
// 断开连接
disconnect(): void {
super.disconnect();
// 关闭文件传输
useTerminalStore().transferManager.rdp.closeBySessionKey(this.sessionKey);
}
// 断开连接
disconnect(): void {
super.disconnect();
// 关闭 client
this.client?.disconnect();
// 关闭超时检查任务
clearTimeout(this.connectTimeoutId);
}
}

View File

@@ -7,7 +7,7 @@ import type { XtermAddons } from '@/types/xterm';
import { defaultFontFamily } from '@/types/xterm';
import { useTerminalStore } from '@/store';
import { InputProtocol } from '@/views/terminal/types/protocol';
import { TerminalShortcutType } from '@/views/terminal/types/const';
import { BACKSPACE_CHAR, CTRL_H_CHAR, TerminalShortcutType } from '@/views/terminal/types/const';
import { Terminal } from '@xterm/xterm';
import { FitAddon } from '@xterm/addon-fit';
import { WebLinksAddon } from '@xterm/addon-web-links';
@@ -127,15 +127,20 @@ export default class SshSession extends BaseSession<ReactiveSessionState, ISshCh
// 注册事件
private registerEvent(dom: HTMLElement, preference: UnwrapRef<TerminalPreference>) {
// 是否替换退格符
const replaceBackspace = preference.sshInteractSetting.replaceBackspace;
// 注册输入事件
this.inst.onData(s => {
if (!this.state.canWrite || !this.state.connected) {
this.inst.onData(command => {
// 不可写
if (!this.isWriteable()) {
return;
}
// 替换退格符
if (replaceBackspace) {
command = command.replace(BACKSPACE_CHAR, CTRL_H_CHAR);
}
// 输入
this.channel.send(InputProtocol.SSH_INPUT, {
command: s
});
this.channel.send(InputProtocol.SSH_INPUT, { command });
});
// 启用响铃
if (preference.sshInteractSetting.enableBell) {
@@ -165,7 +170,8 @@ export default class SshSession extends BaseSession<ReactiveSessionState, ISshCh
addEventListen(dom, 'contextmenu', async () => {
// 右键粘贴逻辑
if (preference.sshInteractSetting.rightClickPaste) {
if (!this.state.canWrite || !this.state.connected) {
// 不可写
if (!this.isWriteable()) {
return;
}
// 未开启右键选中 || 开启并无选中的内容则粘贴

View File

@@ -7,6 +7,7 @@ import type {
ISshSession,
ITerminalSession,
ITerminalSessionManager,
IVncSession,
SshInitConfig,
TerminalSessionTabItem
} from '@/views/terminal/interfaces';
@@ -17,6 +18,7 @@ import { addEventListen, removeEventListen } from '@/utils/event';
import SshSession from './ssh-session';
import SftpSession from './sftp-session';
import RdpSession from './rdp-session';
import VncSession from './vnc-session';
// 终端会话管理器实现
export default class TerminalSessionManager implements ITerminalSessionManager {
@@ -80,7 +82,23 @@ export default class TerminalSessionManager implements ITerminalSessionManager {
// 连接会话
session.connect();
} catch (ex) {
console.error(ex);
// 异常关闭
session.close();
}
}
// 打开 vnc 会话
async openVnc(item: TerminalSessionTabItem, config: GuacdInitConfig): Promise<void> {
// 获取会话
const session: IVncSession = this.getSession(item.key);
try {
// 初始化 session
await session.init(config);
// 等待前端渲染完成
await sleep(100);
// 连接会话
session.connect();
} catch (ex) {
// 异常关闭
session.close();
}
@@ -110,6 +128,9 @@ export default class TerminalSessionManager implements ITerminalSessionManager {
} else if (item.type === TerminalSessionTypes.RDP.type) {
// RDP 会话
session = new RdpSession(item);
} else if (item.type === TerminalSessionTypes.VNC.type) {
// VNC 会话
session = new VncSession(item);
} else {
return undefined as unknown as T;
}

View File

@@ -0,0 +1,35 @@
import type { IVncSession, TerminalSessionTabItem, IGuacdSessionDisplayHandler, IGuacdChannel } from '@/views/terminal/interfaces';
import { fitDisplayValue } from '@/views/terminal/types/const';
import { useTerminalStore } from '@/store';
import BaseGuacdSession from './base-guacd-session';
import VncChannel from '../channel/vnc-channel';
import GuacdSessionDisplayHandler from '../handler/guacd-session-display-handler';
// VNC 会话实现
export default class VncSession extends BaseGuacdSession implements IVncSession {
constructor(item: TerminalSessionTabItem) {
super(item);
}
// 创建 channel
protected createChannel(): IGuacdChannel {
return new VncChannel(this);
};
// 创建 display
protected createDisplay(): IGuacdSessionDisplayHandler {
const vncGraphSetting = useTerminalStore().preference.vncGraphSetting;
// 创建 display
const displayHandler = new GuacdSessionDisplayHandler(this);
// 设置自适应
const autoFit = vncGraphSetting?.displaySize === fitDisplayValue;
displayHandler.autoFit = autoFit;
// 非自适应设置分辨率
if (!autoFit) {
displayHandler.setDisplaySize(vncGraphSetting?.displayWidth || 1024, vncGraphSetting?.displayHeight || 768);
}
return displayHandler;
};
}