feat: 完成修改挂起会话名称的功能
This commit is contained in:
@@ -62,12 +62,14 @@ export class SshSuspendService extends EventEmitter {
|
|||||||
logIdentifier,
|
logIdentifier,
|
||||||
customSuspendName,
|
customSuspendName,
|
||||||
} = details;
|
} = details;
|
||||||
|
console.log(`[SshSuspendService DEBUG] takeOverMarkedSession: Called for userId=${userId}, originalSessionId=${originalSessionId}`);
|
||||||
|
|
||||||
// 检查 SSH client 和 channel 是否仍然可用
|
// 检查 SSH client 和 channel 是否仍然可用
|
||||||
// ClientChannel 有 readable 和 writable, Client 本身没有直接的此类属性
|
// ClientChannel 有 readable 和 writable, Client 本身没有直接的此类属性
|
||||||
// 如果 channel 不可读写,通常意味着底层连接有问题。
|
// 如果 channel 不可读写,通常意味着底层连接有问题。
|
||||||
|
console.log(`[SshSuspendService DEBUG] takeOverMarkedSession: Checking channel for originalSessionId=${originalSessionId}. Readable: ${channel?.readable}, Writable: ${channel?.writable}`);
|
||||||
if (!channel || !channel.readable || !channel.writable) {
|
if (!channel || !channel.readable || !channel.writable) {
|
||||||
console.warn(`[用户: ${userId}] 尝试接管会话 ${originalSessionId} 时,SSH channel 已不可用 (readable: ${channel?.readable}, writable: ${channel?.writable})。将标记为已断开。`);
|
console.warn(`[SshSuspendService WARN] takeOverMarkedSession: userId=${userId}, originalSessionId=${originalSessionId}. SSH channel is not usable. readable=${channel?.readable}, writable=${channel?.writable}. Cannot take over.`);
|
||||||
// 确保如果 SSH 连接已经关闭,日志文件仍然保留,但不创建挂起条目。
|
// 确保如果 SSH 连接已经关闭,日志文件仍然保留,但不创建挂起条目。
|
||||||
// SshSuspendService 不会管理这个“已经断开”的会话,但日志保留供用户清理。
|
// SshSuspendService 不会管理这个“已经断开”的会话,但日志保留供用户清理。
|
||||||
try { channel?.end(); } catch (e) { /* ignore */ }
|
try { channel?.end(); } catch (e) { /* ignore */ }
|
||||||
@@ -101,50 +103,72 @@ export class SshSuspendService extends EventEmitter {
|
|||||||
};
|
};
|
||||||
|
|
||||||
userSessions.set(suspendSessionId, sessionDetails);
|
userSessions.set(suspendSessionId, sessionDetails);
|
||||||
console.log(`[用户: ${userId}] SSH会话 ${originalSessionId} 已被 SshSuspendService 接管 (新挂起ID: ${suspendSessionId})。日志文件标识: ${logIdentifier}`);
|
console.log(`[SshSuspendService INFO] takeOverMarkedSession: userId=${userId}, originalSessionId=${originalSessionId} taken over. New suspendSessionId=${suspendSessionId}, initial status=${sessionDetails.backendSshStatus}. Log identifier=${logIdentifier}`);
|
||||||
|
|
||||||
await this.logStorageService.ensureLogDirectoryExists();
|
await this.logStorageService.ensureLogDirectoryExists();
|
||||||
|
|
||||||
|
console.log(`[SshSuspendService DEBUG] takeOverMarkedSession: Setting up channel 'data' listener for suspendSessionId=${suspendSessionId}`);
|
||||||
channel.on('data', (data: Buffer) => {
|
channel.on('data', (data: Buffer) => {
|
||||||
if (userSessions.get(suspendSessionId)?.backendSshStatus === 'hanging') {
|
const currentDetails = userSessions.get(suspendSessionId);
|
||||||
|
if (currentDetails?.backendSshStatus === 'hanging') {
|
||||||
|
// console.log(`[SshSuspendService DEBUG] channel.on('data') for suspendSessionId=${suspendSessionId}: Writing to log ${logIdentifier}`);
|
||||||
this.logStorageService.writeToLog(logIdentifier, data.toString('utf-8')).catch(err => {
|
this.logStorageService.writeToLog(logIdentifier, data.toString('utf-8')).catch(err => {
|
||||||
console.error(`[用户: ${userId}, 挂起ID: ${suspendSessionId}, 日志: ${logIdentifier}] 写入挂起日志失败:`, err);
|
console.error(`[SshSuspendService ERROR] channel.on('data') for suspendSessionId=${suspendSessionId}, log=${logIdentifier}: Failed to write to log:`, err);
|
||||||
});
|
});
|
||||||
|
} else {
|
||||||
|
// console.log(`[SshSuspendService DEBUG] channel.on('data') for suspendSessionId=${suspendSessionId}: Backend status is ${currentDetails?.backendSshStatus}, not writing to log.`);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const handleSessionTermination = (reasonSuffix: string) => {
|
const handleSessionTermination = (reasonSuffix: string) => {
|
||||||
const currentSession = userSessions.get(suspendSessionId);
|
const currentSession = userSessions.get(suspendSessionId);
|
||||||
|
console.log(`[SshSuspendService DEBUG] handleSessionTermination: Called for suspendSessionId=${suspendSessionId}, reasonSuffix='${reasonSuffix}'. Session found: ${!!currentSession}. Current status: ${currentSession?.backendSshStatus}`);
|
||||||
if (currentSession && currentSession.backendSshStatus === 'hanging') {
|
if (currentSession && currentSession.backendSshStatus === 'hanging') {
|
||||||
const reason = `SSH connection ${reasonSuffix}.`;
|
const reason = `SSH connection ${reasonSuffix}.`;
|
||||||
console.warn(`[用户: ${userId}, 挂起ID: ${suspendSessionId}, 日志: ${logIdentifier}] SSH 连接在挂起期间终止。原因: ${reason}`);
|
console.warn(`[SshSuspendService WARN] handleSessionTermination: userId=${currentSession.userId}, suspendSessionId=${suspendSessionId}. SSH connection terminated during suspension. Reason: ${reason}`);
|
||||||
currentSession.backendSshStatus = 'disconnected_by_backend';
|
currentSession.backendSshStatus = 'disconnected_by_backend';
|
||||||
currentSession.disconnectionTimestamp = new Date().toISOString();
|
currentSession.disconnectionTimestamp = new Date().toISOString();
|
||||||
|
|
||||||
this.removeChannelListeners(channel, sshClient);
|
this.removeChannelListeners(channel, sshClient);
|
||||||
|
console.log(`[SshSuspendService DEBUG] handleSessionTermination: Listeners removed for suspendSessionId=${suspendSessionId}.`);
|
||||||
|
|
||||||
this.emit('sessionAutoTerminated', {
|
this.emit('sessionAutoTerminated', {
|
||||||
userId: currentSession.userId,
|
userId: currentSession.userId,
|
||||||
suspendSessionId,
|
suspendSessionId,
|
||||||
reason
|
reason
|
||||||
});
|
});
|
||||||
|
console.log(`[SshSuspendService INFO] handleSessionTermination: Emitted 'sessionAutoTerminated' for suspendSessionId=${suspendSessionId}, userId=${currentSession.userId}.`);
|
||||||
|
} else if (currentSession) {
|
||||||
|
console.log(`[SshSuspendService DEBUG] handleSessionTermination: Condition not met for suspendSessionId=${suspendSessionId}. Status was '${currentSession.backendSshStatus}', not 'hanging'. No action taken.`);
|
||||||
|
} else {
|
||||||
|
console.warn(`[SshSuspendService WARN] handleSessionTermination: Session not found for suspendSessionId=${suspendSessionId} when event '${reasonSuffix}' occurred.`);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
channel.on('close', () => handleSessionTermination('closed'));
|
console.log(`[SshSuspendService DEBUG] takeOverMarkedSession: Setting up channel/client event listeners for suspendSessionId=${suspendSessionId}`);
|
||||||
channel.on('error', (err: Error) => {
|
channel.on('close', () => {
|
||||||
console.error(`[用户: ${userId}, 挂起ID: ${suspendSessionId}, 日志: ${logIdentifier}] 挂起会话的通道错误:`, err);
|
console.log(`[SshSuspendService DEBUG] channel.on('close') triggered for suspendSessionId=${suspendSessionId}`);
|
||||||
handleSessionTermination('errored');
|
handleSessionTermination('channel closed');
|
||||||
|
});
|
||||||
|
channel.on('error', (err: Error) => {
|
||||||
|
console.error(`[SshSuspendService ERROR] channel.on('error') for suspendSessionId=${suspendSessionId}:`, err);
|
||||||
|
handleSessionTermination('channel errored');
|
||||||
|
});
|
||||||
|
channel.on('end', () => {
|
||||||
|
console.log(`[SshSuspendService DEBUG] channel.on('end') triggered for suspendSessionId=${suspendSessionId}`);
|
||||||
|
handleSessionTermination('channel ended');
|
||||||
|
});
|
||||||
|
channel.on('exit', (code: number | null, signalName: string | null) => {
|
||||||
|
console.log(`[SshSuspendService DEBUG] channel.on('exit') triggered for suspendSessionId=${suspendSessionId}. Code: ${code}, Signal: ${signalName}`);
|
||||||
|
handleSessionTermination(`channel exited with code ${code}, signal ${signalName}`);
|
||||||
});
|
});
|
||||||
channel.on('end', () => handleSessionTermination('ended'));
|
|
||||||
channel.on('exit', (code: number | null, signalName: string | null) => handleSessionTermination(`exited with code ${code}, signal ${signalName}`));
|
|
||||||
|
|
||||||
sshClient.on('error', (err: Error) => {
|
sshClient.on('error', (err: Error) => {
|
||||||
console.error(`[用户: ${userId}, 挂起ID: ${suspendSessionId}, 日志: ${logIdentifier}] 挂起会话的SSH客户端错误:`, err);
|
console.error(`[SshSuspendService ERROR] sshClient.on('error') for suspendSessionId=${suspendSessionId}:`, err);
|
||||||
handleSessionTermination('client errored');
|
handleSessionTermination('client errored');
|
||||||
});
|
});
|
||||||
sshClient.on('end', () => {
|
sshClient.on('end', () => {
|
||||||
console.log(`[用户: ${userId}, 挂起ID: ${suspendSessionId}, 日志: ${logIdentifier}] 挂起会话的SSH客户端连接结束。`);
|
console.log(`[SshSuspendService DEBUG] sshClient.on('end') triggered for suspendSessionId=${suspendSessionId}`);
|
||||||
handleSessionTermination('client ended');
|
handleSessionTermination('client ended');
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -167,11 +191,13 @@ export class SshSuspendService extends EventEmitter {
|
|||||||
* @param userId 用户ID。
|
* @param userId 用户ID。
|
||||||
* @returns Promise<SuspendedSessionInfo[]> 挂起会话信息的数组。
|
* @returns Promise<SuspendedSessionInfo[]> 挂起会话信息的数组。
|
||||||
*/
|
*/
|
||||||
async listSuspendedSessions(userId: number): Promise<SuspendedSessionInfo[]> { // userId: string -> number
|
async listSuspendedSessions(userId: number): Promise<SuspendedSessionInfo[]> {
|
||||||
|
console.log(`[SshSuspendService DEBUG] listSuspendedSessions: Called for userId=${userId}`);
|
||||||
const userSessions = this.getUserSessions(userId);
|
const userSessions = this.getUserSessions(userId);
|
||||||
const sessionsInfo: SuspendedSessionInfo[] = [];
|
const sessionsInfo: SuspendedSessionInfo[] = [];
|
||||||
|
|
||||||
for (const [suspendSessionId, details] of userSessions.entries()) {
|
for (const [suspendSessionId, details] of userSessions.entries()) {
|
||||||
|
console.log(`[SshSuspendService DEBUG] listSuspendedSessions: Processing suspendSessionId=${suspendSessionId}, status=${details.backendSshStatus}`);
|
||||||
sessionsInfo.push({
|
sessionsInfo.push({
|
||||||
suspendSessionId,
|
suspendSessionId,
|
||||||
connectionName: details.connectionName,
|
connectionName: details.connectionName,
|
||||||
|
|||||||
@@ -145,8 +145,8 @@ export const establishSshConnection = (
|
|||||||
privateKey: connDetails.privateKey,
|
privateKey: connDetails.privateKey,
|
||||||
passphrase: connDetails.passphrase,
|
passphrase: connDetails.passphrase,
|
||||||
readyTimeout: timeout,
|
readyTimeout: timeout,
|
||||||
keepaliveInterval: 10000, // 保持连接
|
keepaliveInterval: 5000, // 修改:每 5 秒发送一次 keepalive
|
||||||
keepaliveCountMax: 10,
|
keepaliveCountMax: 5, // 修改:最多尝试 5 次 (总超时约 5*5=10 秒)
|
||||||
};
|
};
|
||||||
|
|
||||||
const readyHandler = async () => { // 改为 async 函数
|
const readyHandler = async () => { // 改为 async 函数
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ export class SshSuspendController {
|
|||||||
this.getSuspendedSshSessions = this.getSuspendedSshSessions.bind(this);
|
this.getSuspendedSshSessions = this.getSuspendedSshSessions.bind(this);
|
||||||
this.terminateAndRemoveSession = this.terminateAndRemoveSession.bind(this);
|
this.terminateAndRemoveSession = this.terminateAndRemoveSession.bind(this);
|
||||||
this.removeSessionEntry = this.removeSessionEntry.bind(this);
|
this.removeSessionEntry = this.removeSessionEntry.bind(this);
|
||||||
|
this.editSessionNameHttp = this.editSessionNameHttp.bind(this); // 绑定新方法
|
||||||
}
|
}
|
||||||
|
|
||||||
public async getSuspendedSshSessions(req: Request, res: Response): Promise<void> {
|
public async getSuspendedSshSessions(req: Request, res: Response): Promise<void> {
|
||||||
@@ -104,4 +105,42 @@ export class SshSuspendController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
public async editSessionNameHttp(req: Request, res: Response): Promise<void> {
|
||||||
|
try {
|
||||||
|
const userId = req.session.userId;
|
||||||
|
const { suspendSessionId } = req.params;
|
||||||
|
const { customName } = req.body; // 从请求体获取新名称
|
||||||
|
|
||||||
|
if (!userId) {
|
||||||
|
res.status(401).json({ message: 'Unauthorized. User ID not found in session.' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!suspendSessionId) {
|
||||||
|
res.status(400).json({ message: 'Bad Request. suspendSessionId parameter is missing.' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (typeof customName !== 'string') { // 验证 customName
|
||||||
|
res.status(400).json({ message: 'Bad Request. customName must be a string and is missing or invalid.' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`[SshSuspendController] editSessionNameHttp called for user ID: ${userId}, suspendSessionId: ${suspendSessionId}, newName: "${customName}"`);
|
||||||
|
|
||||||
|
const success = await sshSuspendService.editSuspendedSessionName(userId, suspendSessionId, customName);
|
||||||
|
if (success) {
|
||||||
|
res.status(200).json({ message: `Suspended session ${suspendSessionId} name updated to "${customName}".`, customName });
|
||||||
|
} else {
|
||||||
|
// 假设服务层在找不到会话时返回 false
|
||||||
|
res.status(404).json({ message: `Failed to update name for session ${suspendSessionId}. It might not exist.` });
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`[SshSuspendController] Error editing session name for user ID: ${req.session.userId}, suspendSessionId: ${req.params.suspendSessionId}:`, error);
|
||||||
|
if (error instanceof Error) {
|
||||||
|
res.status(500).json({ message: 'Failed to edit suspended session name', error: error.message });
|
||||||
|
} else {
|
||||||
|
res.status(500).json({ message: 'Failed to edit suspended session name', error: 'Unknown error' });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -27,4 +27,11 @@ router.delete(
|
|||||||
sshSuspendController.removeSessionEntry
|
sshSuspendController.removeSessionEntry
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Route to edit a suspended session's custom name
|
||||||
|
router.put(
|
||||||
|
'/name/:suspendSessionId',
|
||||||
|
isAuthenticated,
|
||||||
|
sshSuspendController.editSessionNameHttp // 新的控制器方法
|
||||||
|
);
|
||||||
|
|
||||||
export default router;
|
export default router;
|
||||||
@@ -7,14 +7,14 @@ import {
|
|||||||
SshSuspendResumeRequest,
|
SshSuspendResumeRequest,
|
||||||
SshSuspendTerminateRequest,
|
SshSuspendTerminateRequest,
|
||||||
SshSuspendRemoveEntryRequest,
|
SshSuspendRemoveEntryRequest,
|
||||||
SshSuspendEditNameRequest,
|
// SshSuspendEditNameRequest, // Removed as it's now HTTP
|
||||||
SshSuspendStartedResponse,
|
SshSuspendStartedResponse,
|
||||||
SshSuspendListResponse,
|
SshSuspendListResponse,
|
||||||
SshSuspendResumedNotification,
|
SshSuspendResumedNotification,
|
||||||
SshOutputCachedChunk,
|
SshOutputCachedChunk,
|
||||||
SshSuspendTerminatedResponse,
|
SshSuspendTerminatedResponse,
|
||||||
SshSuspendEntryRemovedResponse,
|
SshSuspendEntryRemovedResponse,
|
||||||
SshSuspendNameEditedResponse,
|
// SshSuspendNameEditedResponse, // Removed as it's now HTTP
|
||||||
SshSuspendAutoTerminatedNotification,
|
SshSuspendAutoTerminatedNotification,
|
||||||
SshMarkForSuspendRequest,
|
SshMarkForSuspendRequest,
|
||||||
SshMarkedForSuspendAck,
|
SshMarkedForSuspendAck,
|
||||||
@@ -296,26 +296,7 @@ export function initializeConnectionHandler(wss: WebSocketServer, sshSuspendServ
|
|||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case 'SSH_SUSPEND_EDIT_NAME': {
|
// SSH_SUSPEND_EDIT_NAME case removed, handled by HTTP API now
|
||||||
const { suspendSessionId, customName } = payload as SshSuspendEditNameRequest['payload'];
|
|
||||||
if (!ws.userId) {
|
|
||||||
console.error(`[SSH_SUSPEND_EDIT_NAME] 用户 ID 未定义。`);
|
|
||||||
if (ws.readyState === WebSocket.OPEN) ws.send(JSON.stringify({ type: 'SSH_SUSPEND_NAME_EDITED_RESP', payload: { suspendSessionId, success: false, error: '用户认证失败' } }));
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
const success = await sshSuspendService.editSuspendedSessionName(ws.userId, suspendSessionId, customName);
|
|
||||||
const response: SshSuspendNameEditedResponse = {
|
|
||||||
type: 'SSH_SUSPEND_NAME_EDITED',
|
|
||||||
payload: { suspendSessionId, success, customName: success ? customName : undefined }
|
|
||||||
};
|
|
||||||
if (ws.readyState === WebSocket.OPEN) ws.send(JSON.stringify(response));
|
|
||||||
} catch (error: any) {
|
|
||||||
console.error(`[SSH_SUSPEND_EDIT_NAME] 编辑名称 ${suspendSessionId} 失败:`, error);
|
|
||||||
if (ws.readyState === WebSocket.OPEN) ws.send(JSON.stringify({ type: 'SSH_SUSPEND_NAME_EDITED_RESP', payload: { suspendSessionId, success: false, error: error.message || '编辑名称失败' } }));
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case 'SSH_MARK_FOR_SUSPEND': {
|
case 'SSH_MARK_FOR_SUSPEND': {
|
||||||
const markPayload = payload as SshMarkForSuspendRequest['payload'];
|
const markPayload = payload as SshMarkForSuspendRequest['payload'];
|
||||||
const sessionToMarkId = markPayload.sessionId;
|
const sessionToMarkId = markPayload.sessionId;
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import type {
|
|||||||
SshSuspendResumeReqMessage,
|
SshSuspendResumeReqMessage,
|
||||||
SshSuspendTerminateReqMessage,
|
SshSuspendTerminateReqMessage,
|
||||||
SshSuspendRemoveEntryReqMessage,
|
SshSuspendRemoveEntryReqMessage,
|
||||||
SshSuspendEditNameReqMessage,
|
// SshSuspendEditNameReqMessage, // Removed, using HTTP API
|
||||||
// S2C Payloads
|
// S2C Payloads
|
||||||
SshMarkedForSuspendAckPayload,
|
SshMarkedForSuspendAckPayload,
|
||||||
SshUnmarkedForSuspendAckPayload, // +++ 新增导入 +++
|
SshUnmarkedForSuspendAckPayload, // +++ 新增导入 +++
|
||||||
@@ -17,10 +17,10 @@ import type {
|
|||||||
SshOutputCachedChunkPayload,
|
SshOutputCachedChunkPayload,
|
||||||
SshSuspendTerminatedRespPayload,
|
SshSuspendTerminatedRespPayload,
|
||||||
SshSuspendEntryRemovedRespPayload,
|
SshSuspendEntryRemovedRespPayload,
|
||||||
SshSuspendNameEditedRespPayload,
|
// SshSuspendNameEditedRespPayload, // Removed, using HTTP API
|
||||||
SshSuspendAutoTerminatedNotifPayload,
|
SshSuspendAutoTerminatedNotifPayload,
|
||||||
} from '../../../types/websocket.types'; // 路径: packages/frontend/src/types/websocket.types.ts
|
} from '../../../types/websocket.types'; // 路径: packages/frontend/src/types/websocket.types.ts
|
||||||
import type { WsManagerInstance, SessionState } from '../types'; // 路径: packages/frontend/src/stores/session/types.ts
|
import type { WsManagerInstance, SessionState } from '../types'; // 路径: packages/frontend/src/stores/session/types.ts // Re-add WsManagerInstance
|
||||||
import { closeSession as closeSessionAction, activateSession as activateSessionAction, openNewSession, closeSession } from './sessionActions'; // 使用 openNewSession 和 closeSession
|
import { closeSession as closeSessionAction, activateSession as activateSessionAction, openNewSession, closeSession } from './sessionActions'; // 使用 openNewSession 和 closeSession
|
||||||
import { useConnectionsStore } from '../../connections.store'; // 用于获取连接信息
|
import { useConnectionsStore } from '../../connections.store'; // 用于获取连接信息
|
||||||
import { useUiNotificationsStore } from '../../uiNotifications.store'; // 用于显示通知
|
import { useUiNotificationsStore } from '../../uiNotifications.store'; // 用于显示通知
|
||||||
@@ -325,21 +325,43 @@ export const removeSshSessionEntry = async (suspendSessionId: string): Promise<v
|
|||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 请求编辑挂起 SSH 会话的自定义名称
|
* 请求编辑挂起 SSH 会话的自定义名称 (通过 HTTP API)
|
||||||
* @param suspendSessionId 要编辑的挂起会话 ID
|
* @param suspendSessionId 要编辑的挂起会话 ID
|
||||||
* @param customName 新的自定义名称
|
* @param newCustomName 新的自定义名称
|
||||||
*/
|
*/
|
||||||
export const editSshSessionName = (suspendSessionId: string, customName: string): void => {
|
export const editSshSessionName = async (suspendSessionId: string, newCustomName: string): Promise<void> => {
|
||||||
const wsManager = getActiveWsManager();
|
console.log(`[${t('term.sshSuspend')}] 请求通过 HTTP API 编辑挂起会话名称 (ID: ${suspendSessionId}, 新名称: "${newCustomName}")`);
|
||||||
if (wsManager) {
|
const uiNotificationsStore = useUiNotificationsStore();
|
||||||
const message: SshSuspendEditNameReqMessage = {
|
try {
|
||||||
type: 'SSH_SUSPEND_EDIT_NAME',
|
// 假设后端 API 端点为 /api/ssh-suspend/name/:suspendSessionId
|
||||||
payload: { suspendSessionId, customName },
|
// 并且它接受一个包含 { customName: string } 的 PUT 请求体
|
||||||
};
|
// 并返回包含 { message: string, customName: string } 的成功响应
|
||||||
wsManager.sendMessage(message);
|
const response = await apiClient.put<{ message: string, customName: string }>(
|
||||||
console.log(`[${t('term.sshSuspend')}] 已发送 SSH_SUSPEND_EDIT_NAME_REQ (挂起 ID: ${suspendSessionId}, 名称: "${customName}")`);
|
`ssh-suspend/name/${suspendSessionId}`,
|
||||||
} else {
|
{ customName: newCustomName }
|
||||||
console.warn(`[${t('term.sshSuspend')}] 编辑挂起名称失败 (挂起 ID: ${suspendSessionId}):无可用 WebSocket 连接。`);
|
);
|
||||||
|
|
||||||
|
console.log(`[${t('term.sshSuspend')}] HTTP API 编辑名称 ${suspendSessionId} 成功:`, response.data);
|
||||||
|
|
||||||
|
// 更新前端状态
|
||||||
|
const session = suspendedSshSessions.value.find(s => s.suspendSessionId === suspendSessionId);
|
||||||
|
if (session) {
|
||||||
|
session.customSuspendName = response.data.customName; // 使用后端返回的名称确保一致性
|
||||||
|
uiNotificationsStore.addNotification({
|
||||||
|
type: 'success',
|
||||||
|
message: t('sshSuspend.notifications.nameEditedSuccess', { name: response.data.customName }),
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// 如果会话在前端列表中找不到了(理论上不应该发生,因为是先找到再编辑的)
|
||||||
|
// 也可以选择重新获取列表
|
||||||
|
fetchSuspendedSshSessions();
|
||||||
|
}
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error(`[${t('term.sshSuspend')}] 通过 HTTP API 编辑名称 ${suspendSessionId} 失败:`, error);
|
||||||
|
uiNotificationsStore.addNotification({
|
||||||
|
type: 'error',
|
||||||
|
message: t('sshSuspend.notifications.nameEditedError', { error: error.response?.data?.message || error.message || t('term.unknownError') }),
|
||||||
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -561,26 +583,7 @@ const handleSshSuspendEntryRemovedResp = (payload: SshSuspendEntryRemovedRespPay
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSshSuspendNameEditedResp = (payload: SshSuspendNameEditedRespPayload): void => {
|
// handleSshSuspendNameEditedResp removed as edit is now via HTTP
|
||||||
const uiNotificationsStore = useUiNotificationsStore();
|
|
||||||
console.log(`[${t('term.sshSuspend')}] 接到 SSH_SUSPEND_NAME_EDITED_RESP:`, payload);
|
|
||||||
if (payload.success && payload.customName !== undefined) {
|
|
||||||
const session = suspendedSshSessions.value.find(s => s.suspendSessionId === payload.suspendSessionId);
|
|
||||||
if (session) {
|
|
||||||
session.customSuspendName = payload.customName;
|
|
||||||
uiNotificationsStore.addNotification({
|
|
||||||
type: 'success',
|
|
||||||
message: t('sshSuspend.notifications.nameEditedSuccess', { name: payload.customName }),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
uiNotificationsStore.addNotification({
|
|
||||||
type: 'error',
|
|
||||||
message: t('sshSuspend.notifications.nameEditedError', { error: payload.error || t('term.unknownError') }),
|
|
||||||
});
|
|
||||||
console.error(`[${t('term.sshSuspend')}] 编辑挂起名称失败 (ID: ${payload.suspendSessionId}): ${payload.error}`);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleSshSuspendAutoTerminatedNotif = (payload: SshSuspendAutoTerminatedNotifPayload): void => {
|
const handleSshSuspendAutoTerminatedNotif = (payload: SshSuspendAutoTerminatedNotifPayload): void => {
|
||||||
const uiNotificationsStore = useUiNotificationsStore();
|
const uiNotificationsStore = useUiNotificationsStore();
|
||||||
@@ -621,10 +624,10 @@ export const registerSshSuspendHandlers = (wsManager: WsManagerInstance): void =
|
|||||||
wsManager.onMessage('SSH_OUTPUT_CACHED_CHUNK', (p: MessagePayload) => handleSshOutputCachedChunk(p as SshOutputCachedChunkPayload));
|
wsManager.onMessage('SSH_OUTPUT_CACHED_CHUNK', (p: MessagePayload) => handleSshOutputCachedChunk(p as SshOutputCachedChunkPayload));
|
||||||
wsManager.onMessage('SSH_SUSPEND_TERMINATED_RESP', (p: MessagePayload) => handleSshSuspendTerminatedResp(p as SshSuspendTerminatedRespPayload));
|
wsManager.onMessage('SSH_SUSPEND_TERMINATED_RESP', (p: MessagePayload) => handleSshSuspendTerminatedResp(p as SshSuspendTerminatedRespPayload));
|
||||||
wsManager.onMessage('SSH_SUSPEND_ENTRY_REMOVED_RESP', (p: MessagePayload) => handleSshSuspendEntryRemovedResp(p as SshSuspendEntryRemovedRespPayload));
|
wsManager.onMessage('SSH_SUSPEND_ENTRY_REMOVED_RESP', (p: MessagePayload) => handleSshSuspendEntryRemovedResp(p as SshSuspendEntryRemovedRespPayload));
|
||||||
wsManager.onMessage('SSH_SUSPEND_NAME_EDITED_RESP', (p: MessagePayload) => handleSshSuspendNameEditedResp(p as SshSuspendNameEditedRespPayload));
|
// SSH_SUSPEND_NAME_EDITED_RESP handler removed
|
||||||
wsManager.onMessage('SSH_SUSPEND_AUTO_TERMINATED_NOTIF', (p: MessagePayload) => handleSshSuspendAutoTerminatedNotif(p as SshSuspendAutoTerminatedNotifPayload));
|
wsManager.onMessage('SSH_SUSPEND_AUTO_TERMINATED_NOTIF', (p: MessagePayload) => handleSshSuspendAutoTerminatedNotif(p as SshSuspendAutoTerminatedNotifPayload));
|
||||||
|
|
||||||
console.log(`[${t('term.sshSuspend')}] SSH 挂起模式的 WebSocket 消息处理器已注册。`);
|
console.log(`[${t('term.sshSuspend')}] SSH 挂起模式的 WebSocket 消息处理器已注册 (移除了名称编辑相关的处理器)。`);
|
||||||
|
|
||||||
// 连接建立后,主动获取一次挂起列表
|
// 连接建立后,主动获取一次挂起列表
|
||||||
// 考虑:是否应该在这里做,或者在应用启动时做一次?
|
// 考虑:是否应该在这里做,或者在应用启动时做一次?
|
||||||
|
|||||||
@@ -38,7 +38,7 @@
|
|||||||
<div class="session-info flex-grow mr-2">
|
<div class="session-info flex-grow mr-2">
|
||||||
<div class="font-bold text-lg">
|
<div class="font-bold text-lg">
|
||||||
<span
|
<span
|
||||||
v-if="!session.isEditingName"
|
v-if="editingSuspendSessionId !== session.suspendSessionId"
|
||||||
class="cursor-pointer hover:text-primary"
|
class="cursor-pointer hover:text-primary"
|
||||||
:title="$t('suspendedSshSessions.tooltip.editName')"
|
:title="$t('suspendedSshSessions.tooltip.editName')"
|
||||||
@click="startEditingName(session)"
|
@click="startEditingName(session)"
|
||||||
@@ -47,13 +47,13 @@
|
|||||||
</span>
|
</span>
|
||||||
<input
|
<input
|
||||||
v-else
|
v-else
|
||||||
v-model="session.editingNameValue"
|
ref="nameInputRef"
|
||||||
|
v-model="currentEditingNameValue"
|
||||||
type="text"
|
type="text"
|
||||||
class="text-lg font-bold w-full px-1 py-0.5 border border-primary rounded-md bg-background text-foreground focus:outline-none focus:ring-1 focus:ring-primary"
|
class="text-lg font-bold w-full px-1 py-0.5 border border-primary rounded-md bg-background text-foreground focus:outline-none focus:ring-1 focus:ring-primary"
|
||||||
autofocus
|
@blur="finishEditingName()"
|
||||||
@blur="finishEditingName(session)"
|
@keydown.enter.prevent="finishEditingName()"
|
||||||
@keydown.enter.prevent="finishEditingName(session)"
|
@keydown.esc.prevent="cancelEditingName()"
|
||||||
@keydown.esc.prevent="cancelEditingName(session)"
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="text-sm text-muted-color">
|
<div class="text-sm text-muted-color">
|
||||||
@@ -107,87 +107,43 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, onMounted, computed } from 'vue';
|
import { ref, onMounted, computed, nextTick, watch } from 'vue'; // +++ 导入 nextTick 和 watch +++
|
||||||
import { useI18n } from 'vue-i18n';
|
import { useI18n } from 'vue-i18n';
|
||||||
import { storeToRefs } from 'pinia'; // +++ 导入 storeToRefs +++
|
import { storeToRefs } from 'pinia';
|
||||||
// PrimeVue components (InputText, Button, Tag) are assumed to be globally registered
|
import { useSessionStore } from '../stores/session.store';
|
||||||
// based on the structure of other views like QuickCommandsView.vue
|
import type { SuspendedSshSession } from '../types/ssh-suspend.types';
|
||||||
// and the nature of the 'Cannot find module' errors which might indicate
|
|
||||||
// they are not meant to be imported directly here if globally available.
|
|
||||||
|
|
||||||
// 假设 sessionStore 存在并且有以下类型和方法
|
|
||||||
import { useSessionStore } from '../stores/session.store'; // 使用真实的 store
|
|
||||||
import type { SuspendedSshSession } from '../types/ssh-suspend.types'; // 确保 SuspendedSshSession 类型从正确的位置导入
|
|
||||||
|
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
// 模拟类型,实际应从 ssh-suspend.types.ts 导入 (保持这个类型扩展)
|
const sessionStore = useSessionStore();
|
||||||
interface SuspendedSshSessionUIData extends SuspendedSshSession {
|
|
||||||
isEditingName?: boolean;
|
|
||||||
editingNameValue?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
// // 模拟 sessionStore (注释掉)
|
|
||||||
// const mockSessionStore = {
|
|
||||||
// suspendedSshSessions: ref<SuspendedSshSessionUIData[]>([
|
|
||||||
// // ... mock data ...
|
|
||||||
// ]),
|
|
||||||
// fetchSuspendedSshSessions: async () => {
|
|
||||||
// console.log('[SuspendedSshSessionsView] Requesting suspended SSH sessions...');
|
|
||||||
// // 模拟 API 调用延迟
|
|
||||||
// return new Promise(resolve => setTimeout(() => {
|
|
||||||
// mockSessionStore.suspendedSshSessions.value = [
|
|
||||||
// // ... mock data ...
|
|
||||||
// ];
|
|
||||||
// isLoading.value = false;
|
|
||||||
// console.log('[SuspendedSshSessionsView] Mock sessions loaded:', mockSessionStore.suspendedSshSessions.value);
|
|
||||||
// resolve(true);
|
|
||||||
// }, 1500));
|
|
||||||
// },
|
|
||||||
// resumeSshSession: async (suspendSessionId: string, newFrontendSessionId: string) => {
|
|
||||||
// console.log(`[SuspendedSshSessionsView] Action: resumeSshSession(${suspendSessionId}, ${newFrontendSessionId})`);
|
|
||||||
// alert(`模拟恢复会话: ${suspendSessionId}`);
|
|
||||||
// },
|
|
||||||
// terminateAndRemoveSshSession: async (suspendSessionId: string) => {
|
|
||||||
// console.log(`[SuspendedSshSessionsView] Action: terminateAndRemoveSshSession(${suspendSessionId})`);
|
|
||||||
// mockSessionStore.suspendedSshSessions.value = mockSessionStore.suspendedSshSessions.value.filter(s => s.suspendSessionId !== suspendSessionId);
|
|
||||||
// alert(`模拟终止并移除会话: ${suspendSessionId}`);
|
|
||||||
// },
|
|
||||||
// removeSshSessionEntry: async (suspendSessionId: string) => {
|
|
||||||
// console.log(`[SuspendedSshSessionsView] Action: removeSshSessionEntry(${suspendSessionId})`);
|
|
||||||
// mockSessionStore.suspendedSshSessions.value = mockSessionStore.suspendedSshSessions.value.filter(s => s.suspendSessionId !== suspendSessionId);
|
|
||||||
// alert(`模拟移除已断开会话条目: ${suspendSessionId}`);
|
|
||||||
// },
|
|
||||||
// editSshSessionName: async (suspendSessionId: string, newName: string) => {
|
|
||||||
// console.log(`[SuspendedSshSessionsView] Action: editSshSessionName(${suspendSessionId}, ${newName})`);
|
|
||||||
// const session = mockSessionStore.suspendedSshSessions.value.find(s => s.suspendSessionId === suspendSessionId);
|
|
||||||
// if (session) {
|
|
||||||
// session.customSuspendName = newName;
|
|
||||||
// }
|
|
||||||
// alert(`模拟编辑名称: ${suspendSessionId} -> ${newName}`);
|
|
||||||
// },
|
|
||||||
// };
|
|
||||||
const sessionStore = useSessionStore(); // 使用真实的 store
|
|
||||||
// const sessionStore = mockSessionStore; // 使用模拟 store (注释掉)
|
|
||||||
|
|
||||||
// +++ 使用 storeToRefs 获取响应式状态,并将 isLoadingSuspendedSessions 重命名为 isLoading +++
|
|
||||||
const { suspendedSshSessions: storeSuspendedSshSessions, isLoadingSuspendedSessions: isLoading } = storeToRefs(sessionStore);
|
const { suspendedSshSessions: storeSuspendedSshSessions, isLoadingSuspendedSessions: isLoading } = storeToRefs(sessionStore);
|
||||||
|
|
||||||
const searchTerm = ref('');
|
const searchTerm = ref('');
|
||||||
// const isLoading = ref(true); // 现在从 store 的 isLoading 获取
|
|
||||||
|
|
||||||
const allSuspendedSshSessions = computed(() => storeSuspendedSshSessions.value.map((s: SuspendedSshSession) => ({ // 显式为 s 添加类型
|
// +++ 组件级编辑状态 +++
|
||||||
...(s as SuspendedSshSessionUIData), // 断言为包含 UI 状态的类型
|
const editingSuspendSessionId = ref<string | null>(null);
|
||||||
isEditingName: (s as SuspendedSshSessionUIData).isEditingName ?? false,
|
const currentEditingNameValue = ref<string>('');
|
||||||
editingNameValue: (s as SuspendedSshSessionUIData).editingNameValue ?? s.customSuspendName ?? s.connectionName,
|
const nameInputRef = ref<HTMLInputElement | null>(null);
|
||||||
})));
|
|
||||||
|
|
||||||
|
// +++ 监听编辑ID变化以聚焦输入框 +++
|
||||||
|
watch(editingSuspendSessionId, async (newId) => {
|
||||||
|
if (newId !== null) {
|
||||||
|
await nextTick(); // 确保DOM已更新,输入框已渲染
|
||||||
|
if (nameInputRef.value && typeof nameInputRef.value.focus === 'function') {
|
||||||
|
nameInputRef.value.focus();
|
||||||
|
// nameInputRef.value.select(); // 可选:如果希望选中所有文本
|
||||||
|
} else {
|
||||||
|
console.warn('[SuspendedSshSessionsView] Watcher: nameInputRef.value is not a focusable input after nextTick.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// filteredSessions 现在直接基于 storeSuspendedSshSessions
|
||||||
const filteredSessions = computed(() => {
|
const filteredSessions = computed(() => {
|
||||||
if (!searchTerm.value.trim()) {
|
if (!searchTerm.value.trim()) {
|
||||||
return allSuspendedSshSessions.value; // allSuspendedSshSessions 已经是 .value 之后的结果
|
return storeSuspendedSshSessions.value;
|
||||||
}
|
}
|
||||||
const lowerSearchTerm = searchTerm.value.toLowerCase();
|
const lowerSearchTerm = searchTerm.value.toLowerCase();
|
||||||
return allSuspendedSshSessions.value.filter((session: SuspendedSshSessionUIData) => // 为 session 添加类型
|
return storeSuspendedSshSessions.value.filter((session: SuspendedSshSession) =>
|
||||||
(session.customSuspendName?.toLowerCase() || '').includes(lowerSearchTerm) ||
|
(session.customSuspendName?.toLowerCase() || '').includes(lowerSearchTerm) ||
|
||||||
session.connectionName.toLowerCase().includes(lowerSearchTerm)
|
session.connectionName.toLowerCase().includes(lowerSearchTerm)
|
||||||
);
|
);
|
||||||
@@ -201,44 +157,46 @@ const formatDateTime = (isoString?: string) => {
|
|||||||
if (!isoString) return t('time.unknown');
|
if (!isoString) return t('time.unknown');
|
||||||
try {
|
try {
|
||||||
return new Date(isoString).toLocaleString('zh-CN', {
|
return new Date(isoString).toLocaleString('zh-CN', {
|
||||||
year: 'numeric',
|
year: 'numeric', month: '2-digit', day: '2-digit',
|
||||||
month: '2-digit',
|
hour: '2-digit', minute: '2-digit',
|
||||||
day: '2-digit',
|
|
||||||
hour: '2-digit',
|
|
||||||
minute: '2-digit',
|
|
||||||
});
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
return t('time.invalidDate');
|
return t('time.invalidDate');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const startEditingName = (session: SuspendedSshSessionUIData) => {
|
const startEditingName = (session: SuspendedSshSession) => { // async 不再需要,聚焦由 watcher 处理
|
||||||
// 确保同一时间只有一个会话处于编辑状态(可选优化)
|
editingSuspendSessionId.value = session.suspendSessionId;
|
||||||
allSuspendedSshSessions.value.forEach((s: SuspendedSshSessionUIData) => s.isEditingName = false); // 为 s 添加类型
|
currentEditingNameValue.value = session.customSuspendName || session.connectionName;
|
||||||
session.isEditingName = true;
|
// 聚焦逻辑已移至 watcher
|
||||||
session.editingNameValue = session.customSuspendName || session.connectionName;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const finishEditingName = (session: SuspendedSshSessionUIData) => {
|
const finishEditingName = () => {
|
||||||
if (!session.isEditingName) return;
|
if (editingSuspendSessionId.value === null) return;
|
||||||
session.isEditingName = false;
|
|
||||||
const newName = session.editingNameValue?.trim();
|
const sessionId = editingSuspendSessionId.value;
|
||||||
// 仅当名称有变化且不为空时才提交
|
const newName = currentEditingNameValue.value.trim();
|
||||||
if (newName && newName !== (session.customSuspendName || session.connectionName)) {
|
|
||||||
sessionStore.editSshSessionName(session.suspendSessionId, newName);
|
const originalSession = storeSuspendedSshSessions.value.find(s => s.suspendSessionId === sessionId);
|
||||||
} else {
|
if (!originalSession) {
|
||||||
// 如果名称未变或变为空,则恢复显示原始值或之前的自定义名
|
editingSuspendSessionId.value = null; // 重置状态
|
||||||
session.editingNameValue = session.customSuspendName || session.connectionName;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
editingSuspendSessionId.value = null; // 退出编辑模式
|
||||||
|
|
||||||
|
if (newName && newName !== (originalSession.customSuspendName || originalSession.connectionName)) {
|
||||||
|
sessionStore.editSshSessionName(sessionId, newName);
|
||||||
|
}
|
||||||
|
// 如果名称未变或为空,则无需操作,因为 currentEditingNameValue 不会持久化
|
||||||
};
|
};
|
||||||
|
|
||||||
const cancelEditingName = (session: SuspendedSshSessionUIData) => {
|
const cancelEditingName = () => {
|
||||||
session.isEditingName = false;
|
editingSuspendSessionId.value = null;
|
||||||
session.editingNameValue = session.customSuspendName || session.connectionName; // 恢复原值
|
// currentEditingNameValue 不需要显式重置,因为它会在下次 startEditingName 时被新值覆盖
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const resumeSession = async (session: SuspendedSshSession) => { // 参数类型改为 SuspendedSshSession
|
||||||
const resumeSession = async (session: SuspendedSshSessionUIData) => {
|
|
||||||
console.log(`[SuspendedSshSessionsView] Attempting to resume session ID: ${session.suspendSessionId}, Name: ${session.customSuspendName || session.connectionName}`);
|
console.log(`[SuspendedSshSessionsView] Attempting to resume session ID: ${session.suspendSessionId}, Name: ${session.customSuspendName || session.connectionName}`);
|
||||||
// 使用 JSON.parse(JSON.stringify()) 来记录会话对象的一个快照,避免在异步操作后因对象被修改而导致日志不准确
|
// 使用 JSON.parse(JSON.stringify()) 来记录会话对象的一个快照,避免在异步操作后因对象被修改而导致日志不准确
|
||||||
console.log('[SuspendedSshSessionsView] Session details snapshot:', JSON.parse(JSON.stringify(session)));
|
console.log('[SuspendedSshSessionsView] Session details snapshot:', JSON.parse(JSON.stringify(session)));
|
||||||
@@ -277,7 +235,7 @@ const resumeSession = async (session: SuspendedSshSessionUIData) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const removeSession = (session: SuspendedSshSessionUIData) => {
|
const removeSession = (session: SuspendedSshSession) => { // 参数类型改为 SuspendedSshSession
|
||||||
if (session.backendSshStatus === 'hanging') {
|
if (session.backendSshStatus === 'hanging') {
|
||||||
sessionStore.terminateAndRemoveSshSession(session.suspendSessionId);
|
sessionStore.terminateAndRemoveSshSession(session.suspendSessionId);
|
||||||
} else if (session.backendSshStatus === 'disconnected_by_backend') {
|
} else if (session.backendSshStatus === 'disconnected_by_backend') {
|
||||||
@@ -286,9 +244,7 @@ const removeSession = (session: SuspendedSshSessionUIData) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
// isLoading.value = true; // storeIsLoading 会自动更新
|
|
||||||
await sessionStore.fetchSuspendedSshSessions();
|
await sessionStore.fetchSuspendedSshSessions();
|
||||||
// isLoading.value = false; // fetchSuspendedSshSessions 内部应更新 storeIsLoading
|
|
||||||
});
|
});
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
Reference in New Issue
Block a user