From 4c634c6fdecb38ad8c0688a354c0137b20005803 Mon Sep 17 00:00:00 2001 From: Baobhan Sith <80159437+Heavrnl@users.noreply.github.com> Date: Sun, 11 May 2025 17:34:26 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0=E2=80=9C=E8=84=9A?= =?UTF-8?q?=E6=9C=AC=E6=A8=A1=E5=BC=8F=E2=80=9D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Refs #28 --- .../src/components/AddConnectionForm.vue | 412 +++++++++++++++++- packages/frontend/src/locales/en-US.json | 26 +- packages/frontend/src/locales/ja-JP.json | 26 +- packages/frontend/src/locales/zh-CN.json | 26 +- 4 files changed, 473 insertions(+), 17 deletions(-) diff --git a/packages/frontend/src/components/AddConnectionForm.vue b/packages/frontend/src/components/AddConnectionForm.vue index 1d7afc9..e74347e 100644 --- a/packages/frontend/src/components/AddConnectionForm.vue +++ b/packages/frontend/src/components/AddConnectionForm.vue @@ -66,9 +66,20 @@ const hostTooltipStyle = ref({}); const hostIconRef = ref(null); const hostTooltipContentRef = ref(null); +// Script Mode State +const isScriptModeActive = ref(false); +const scriptInputText = ref(''); + // 计算属性判断是否为编辑模式 const isEditMode = computed(() => !!props.connectionToEdit); +// When switching to edit mode, disable script mode +watch(isEditMode, (editing) => { + if (editing) { + isScriptModeActive.value = false; + } +}); + // 计算属性动态设置表单标题 const formTitle = computed(() => { return isEditMode.value ? t('connections.form.titleEdit') : t('connections.form.title'); @@ -204,8 +215,338 @@ const parseIpRange = (ipRangeStr: string): string[] | { error: string } => { return ips; }; +// Helper function to parse a single script line +const parseScriptLine = (line: string): { type: 'SSH' | 'RDP' | 'VNC' | null, userHostPort: string, name: string | null, password: string | null, keyName: string | null, tags: string[], note: string | null, error?: string } => { + // 首先提取 user@host:port 部分 + const firstSpaceIndex = line.indexOf(' '); + let userHostPortPart = ''; + let remainingLine = line; + + if (firstSpaceIndex !== -1) { + userHostPortPart = line.substring(0, firstSpaceIndex); + remainingLine = line.substring(firstSpaceIndex + 1).trim(); + } else { + userHostPortPart = line; + remainingLine = ''; + } + + if (!userHostPortPart) return { type: null, userHostPort: '', name: null, password: null, keyName: null, tags: [], note: null, error: t('connections.form.scriptErrorMissingHost', '缺少 user@host:port 部分') }; + + let type: 'SSH' | 'RDP' | 'VNC' | null = 'SSH'; // Default to SSH + let name: string | null = null; + let password: string | null = null; + let keyName: string | null = null; + const tags: string[] = []; + let note: string | null = null; + let currentArg: string | null = null; + let noteParts: string[] = []; + + // 使用正则表达式来匹配参数和值,支持带引号的值 + const argRegex = /(-[^=\s]+)(?:\s+("(?:\\"|[^"])*"|[^-\s][^\s]*))?/g; + let match; + let lastIndex = 0; + + while ((match = argRegex.exec(remainingLine)) !== null) { + const arg = match[1]; + let value = match[2] || ''; + + // 清理引号 + if (value.startsWith('"') && value.endsWith('"')) { + value = value.slice(1, -1).replace(/\\"/g, '"'); + } + + // 处理上一个参数的收尾工作 + if (noteParts.length > 0 && currentArg === '-note') { + note = noteParts.join(' '); + noteParts = []; + } + + currentArg = arg; + + if (arg === '-tags') { + // 标签将在后续处理 + } else if (arg === '-note') { + // 备注将在后续处理 + } + + if (value) { + switch (arg) { + case '-type': + const upperType = value.toUpperCase(); + if (upperType === 'SSH' || upperType === 'RDP' || upperType === 'VNC') { + type = upperType as 'SSH' | 'RDP' | 'VNC'; + } else { + return { type: null, userHostPort: userHostPortPart, name, password, keyName, tags, note, error: t('connections.form.scriptErrorInvalidType', { type: value }) }; + } + currentArg = null; + break; + case '-name': + name = value; + currentArg = null; + break; + case '-p': + password = value; + currentArg = null; + break; + case '-k': + keyName = value; + currentArg = null; + break; + case '-tags': + tags.push(value); + break; + case '-note': + noteParts.push(value); + break; + default: + if (currentArg === '-note') { + noteParts.push(value); + } else { + return { type, userHostPort: userHostPortPart, name, password, keyName, tags, note, error: t('connections.form.scriptErrorUnknownArg', { arg: currentArg }) }; + } + } + } + lastIndex = argRegex.lastIndex; + } + + // 处理剩余的部分(可能是标签或备注) + const remainingPart = remainingLine.substring(lastIndex).trim(); + if (remainingPart) { + if (currentArg === '-tags') { + // 按空格分割剩余部分为多个标签,支持引号 + const tagRegex = /("(?:\\"|[^"])*"|[^\s"]+)/g; + let tagMatch; + while ((tagMatch = tagRegex.exec(remainingPart)) !== null) { + let tag = tagMatch[1]; + if (tag.startsWith('"') && tag.endsWith('"')) { + tag = tag.slice(1, -1).replace(/\\"/g, '"'); + } + tags.push(tag); + } + } else if (currentArg === '-note') { + noteParts.push(remainingPart); + } else { + return { type, userHostPort: userHostPortPart, name, password, keyName, tags, note, error: t('connections.form.scriptErrorUnexpectedToken', { token: remainingPart }) }; + } + } + + if (noteParts.length > 0 && currentArg === '-note') { + note = noteParts.join(' '); + } + + // Basic validation for userHostPort + const userHostPortRegex = /^[^@]+@[^:]+(:[0-9]+)?$/; + if (!userHostPortRegex.test(userHostPortPart)) { + return { type, userHostPort: userHostPortPart, name, password, keyName, tags, note, error: t('connections.form.scriptErrorInvalidUserHostPort', { part: userHostPortPart })}; + } + + return { type, userHostPort: userHostPortPart, name, password, keyName, tags, note }; +}; + // 处理表单提交 +const handleScriptModeSubmit = async () => { + const lines = scriptInputText.value.split('\n').filter(line => line.trim() !== ''); + + if (lines.length === 0) { + uiNotificationsStore.showError(t('connections.form.scriptModeEmpty', '脚本输入不能为空。')); + return; + } + + let allConnectionsValid = true; + const connectionsToAdd = []; + + for (const line of lines) { + const parsed = parseScriptLine(line); + if (parsed.error) { + uiNotificationsStore.showError(t('connections.form.scriptErrorInLine', { line, error: parsed.error })); + allConnectionsValid = false; + break; // Stop on first error for now, or collect all errors + } + + if (!parsed.type) { // Should be caught by parsed.error, but as a safeguard + uiNotificationsStore.showError(t('connections.form.scriptErrorMissingType', { line })); + allConnectionsValid = false; + break; + } + + const [userHost, portStr] = parsed.userHostPort.split(':'); + const [username, host] = userHost.split('@'); + const port = portStr ? parseInt(portStr, 10) : (parsed.type === 'RDP' ? 3389 : (parsed.type === 'VNC' ? 5900 : 22)); + + if (!username || !host) { + uiNotificationsStore.showError(t('connections.form.scriptErrorInvalidUserHostFormat', { line })); + allConnectionsValid = false; + break; + } + if (isNaN(port) || port <= 0 || port > 65535) { + uiNotificationsStore.showError(t('connections.form.scriptErrorInvalidPort', { line, port: portStr || (parsed.type === 'RDP' ? '3389' : (parsed.type === 'VNC' ? '5900' : '22')) })); + allConnectionsValid = false; + break; + } + + const connectionData: any = { + type: parsed.type, + name: parsed.name || `${username}@${host}`, + host, + port, + username, + notes: parsed.note || '', + // tag_ids will be resolved later if tags are provided + tag_names: parsed.tags, // Store tag names for now, resolve to IDs before API call + }; + + if (parsed.type === 'SSH') { + connectionData.auth_method = parsed.keyName ? 'key' : 'password'; + if (connectionData.auth_method === 'password') { + if (!parsed.password) { + uiNotificationsStore.showError(t('connections.form.scriptErrorMissingPasswordForSsh', { line })); + allConnectionsValid = false; + break; + } + connectionData.password = parsed.password; + } else { // key auth + if (!parsed.keyName) { // Should not happen if auth_method is 'key' + uiNotificationsStore.showError(t('connections.form.scriptErrorMissingKeyNameForSsh', { line })); + allConnectionsValid = false; + break; + } + // We'll need to find ssh_key_id from keyName later + connectionData.ssh_key_name = parsed.keyName; + } + } else if (parsed.type === 'RDP' || parsed.type === 'VNC') { + if (!parsed.password) { + uiNotificationsStore.showError(t('connections.form.scriptErrorMissingPasswordForType', { line, type: parsed.type })); + allConnectionsValid = false; + break; + } + connectionData.password = parsed.password; + } + connectionsToAdd.push(connectionData); + } + + if (!allConnectionsValid || connectionsToAdd.length === 0) { + if (connectionsToAdd.length > 0 && !allConnectionsValid) { + // Errors were already shown + } else if (lines.length > 0 && connectionsToAdd.length === 0 && allConnectionsValid) { + // This case should ideally not be reached if parsing is correct + uiNotificationsStore.showError(t('connections.form.scriptErrorInternal', '内部解析错误。')); + } + return; + } + + console.log('Parsed connections to add (pre-resolution):', connectionsToAdd); + + const fullyProcessedConnections = []; + let resolutionErrorOccurred = false; + + // Resolve tag names and SSH key names to IDs + for (const connData of connectionsToAdd) { + // Resolve Tag IDs + if (connData.tag_names && connData.tag_names.length > 0) { + const tagIds = []; + for (const tagName of connData.tag_names) { + const foundTag = tags.value.find(t => t.name === tagName); + if (foundTag) { + tagIds.push(foundTag.id); + } else { + // Option: Create tag if not found, or show error. For now, error. + uiNotificationsStore.showError(t('connections.form.scriptErrorTagNotFound', { tagName })); + resolutionErrorOccurred = true; + break; + } + } + if (resolutionErrorOccurred) break; + connData.tag_ids = tagIds; + } else { + connData.tag_ids = []; + } + delete connData.tag_names; // Remove temporary field + + // Resolve SSH Key ID + if (connData.type === 'SSH' && connData.auth_method === 'key' && connData.ssh_key_name) { + const foundKey = sshKeysStore.sshKeys.find(k => k.name === connData.ssh_key_name); + if (foundKey) { + connData.ssh_key_id = foundKey.id; + } else { + uiNotificationsStore.showError(t('connections.form.scriptErrorSshKeyNotFound', { keyName: connData.ssh_key_name })); + resolutionErrorOccurred = true; + break; + } + delete connData.ssh_key_name; // Remove temporary field + } + + // Clean up unnecessary fields based on type and auth_method, similar to single add + if (connData.type !== 'SSH' || connData.auth_method !== 'key') delete connData.ssh_key_id; + if (connData.type === 'SSH' && connData.auth_method === 'key') delete connData.password; // No password if key auth + if (connData.type !== 'SSH') delete connData.auth_method; // RDP/VNC don't have auth_method in backend + + fullyProcessedConnections.push(connData); + } + + if (resolutionErrorOccurred || (fullyProcessedConnections.length === 0 && lines.length > 0)) { + // Errors shown by resolver, or if no connections were processed but there were lines + if (!resolutionErrorOccurred && lines.length > 0 && fullyProcessedConnections.length === 0) { + uiNotificationsStore.showError(t('connections.form.scriptErrorNothingToProcess', '没有可处理的有效连接数据。')); + } + return; + } + + if (fullyProcessedConnections.length === 0) { // Should be caught by earlier checks + return; + } + + console.log('Fully processed connections:', fullyProcessedConnections); + + let successCount = 0; + let errorCount = 0; + let firstErrorEncountered: string | null = null; + + uiNotificationsStore.showInfo(t('connections.form.scriptModeAddingConnections', { count: fullyProcessedConnections.length })); + + for (const finalConnectionData of fullyProcessedConnections) { + const success = await connectionsStore.addConnection(finalConnectionData); + if (success) { + successCount++; + } else { + errorCount++; + if (!firstErrorEncountered) { + firstErrorEncountered = connectionsStore.error || t('errors.unknown', '未知错误'); + } + // Optionally, log which connection failed + console.error(`Failed to add connection: ${finalConnectionData.name}`, connectionsStore.error); + } + } + + if (errorCount > 0) { + const message = t('connections.form.errorBatchAddResult', { successCount, errorCount, firstErrorEncountered: firstErrorEncountered || t('errors.unknown', '未知错误') }); + if (successCount > 0) { + uiNotificationsStore.showWarning(message); + } else { + uiNotificationsStore.showError(message); + } + } + + if (successCount > 0) { + if (errorCount === 0) { // All successful + uiNotificationsStore.showSuccess(t('connections.form.successBatchAddResult', { successCount })); + } + emit('connection-added'); // Emit even if there were partial successes + if (errorCount === 0) { // Clear input only if all were successful + scriptInputText.value = ''; + // emit('close'); // Optionally close form on full success + } + } + + // If successCount is 0 and errorCount is 0 but there were lines, it means something went wrong before this loop. + // That case should be handled by the `fullyProcessedConnections.length === 0` check earlier. +}; + const handleSubmit = async () => { + if (isScriptModeActive.value) { + await handleScriptModeSubmit(); + return; + } + formError.value = null; connectionsStore.error = null; proxiesStore.error = null; // 同时清除代理 store 的错误 @@ -616,10 +957,12 @@ const handleHostIconMouseLeave = () => {

{{ formTitle }}

- -
-

{{ t('connections.form.sectionBasic', '基本信息') }}

-
+ + + + +
+
+

{{ t('connections.form.sectionScriptMode', '脚本模式') }}

+ +
+
+ + +

+ {{ t('connections.form.scriptModeFormatInfo', '格式: user@host:port [-type TYPE] [-name NAME] [-p PASSWORD] [-k KEY_NAME] [-tags TAG1 TAG2...] [-note NOTE_TEXT]') }} +

+
+
+ + + - +
- -
+ +
- -
+ +
+
- diff --git a/packages/frontend/src/locales/en-US.json b/packages/frontend/src/locales/en-US.json index 38c0b8a..77dc061 100644 --- a/packages/frontend/src/locales/en-US.json +++ b/packages/frontend/src/locales/en-US.json @@ -196,7 +196,31 @@ "errorPasswordRequiredForBatchVNC": "When batch adding VNC connections, a VNC password must be provided.", "errorBatchAddResult": "Batch add: {successCount} succeeded, {errorCount} failed. First error: {firstErrorEncountered}", "successBatchAddResult": "Batch add successful: {successCount} connections created.", - "errorIpRangeNotAllowedInEditMode": "IP range is not supported in edit mode. Please use a single IP address." + "errorIpRangeNotAllowedInEditMode": "IP range is not supported in edit mode. Please use a single IP address.", + "scriptModeSubmitPlaceholder": "Script mode submission logic to be implemented.", + "scriptModeEmpty": "Script input cannot be empty.", + "scriptModeSubmitPending": "Processing script mode submission...", + "sectionScriptMode": "Script Mode", + "scriptModeInputLabel": "Connection Script (one per line)", + "scriptModePlaceholder": "Enter connection script, one connection configuration per line.", + "scriptModeFormatInfo": "Format: user@host:port [-type TYPE] [-name NAME] [-p PASSWORD] [-k KEY_NAME] [-tags TAG1 TAG2...] [-note NOTE_TEXT]", + "scriptErrorMissingHost": "Script line '{line}' is missing the 'user@host:port' part.", + "scriptErrorInvalidType": "Invalid type '{type}' in script line '{line}'. Valid types are SSH, RDP, VNC.", + "scriptErrorUnknownArg": "Unknown argument '{arg}' in script line '{line}'.", + "scriptErrorUnexpectedToken": "Unexpected token '{token}' in script line '{line}'.", + "scriptErrorInvalidUserHostPort": "Invalid format for '{part}' in script line '{line}'. Expected 'user@host' or 'user@host:port'.", + "scriptErrorInLine": "Error parsing script line: \"{line}\" - Error: {error}", + "scriptErrorMissingType": "Script line '{line}' is missing connection type or type is invalid.", + "scriptErrorInvalidUserHostFormat": "Invalid user@host format in script line '{line}'.", + "scriptErrorInvalidPort": "Invalid port '{port}' in script line '{line}'.", + "scriptErrorMissingPasswordForSsh": "Script line '{line}' (SSH password auth) is missing password (-p).", + "scriptErrorMissingKeyNameForSsh": "Script line '{line}' (SSH key auth) is missing key name (-k).", + "scriptErrorMissingPasswordForType": "Script line '{line}' (type {type}) is missing password (-p).", + "scriptErrorInternal": "Internal parsing error while processing script input.", + "scriptErrorTagNotFound": "Script processing error: Tag '{tagName}' not found.", + "scriptErrorSshKeyNotFound": "Script processing error: SSH Key '{keyName}' not found.", + "scriptErrorNothingToProcess": "No valid connection data to process.", + "scriptModeAddingConnections": "Adding {count} connections via script mode..." }, "test": { "success": "Connection test successful!", diff --git a/packages/frontend/src/locales/ja-JP.json b/packages/frontend/src/locales/ja-JP.json index 9ace5d4..7b48377 100644 --- a/packages/frontend/src/locales/ja-JP.json +++ b/packages/frontend/src/locales/ja-JP.json @@ -181,7 +181,31 @@ "errorPasswordRequiredForBatchVNC": "VNC接続を一括追加する場合、VNCパスワードを提供する必要があります。", "errorBatchAddResult": "一括追加: {successCount} 件成功, {errorCount} 件失敗。最初のエラー: {firstErrorEncountered}", "successBatchAddResult": "一括追加成功: {successCount} 件の接続が作成されました。", - "errorIpRangeNotAllowedInEditMode": "編集モードではIP範囲はサポートされていません。単一のIPアドレスを使用してください。" + "errorIpRangeNotAllowedInEditMode": "編集モードではIP範囲はサポートされていません。単一のIPアドレスを使用してください。", + "scriptModeSubmitPlaceholder": "スクリプトモードの送信ロジックは実装予定です。", + "scriptModeEmpty": "スクリプト入力は空にできません。", + "scriptModeSubmitPending": "スクリプトモードの送信を処理中...", + "sectionScriptMode": "スクリプトモード", + "scriptModeInputLabel": "接続スクリプト (1行に1つ)", + "scriptModePlaceholder": "接続スクリプトを入力してください。1行に1つの接続設定。", + "scriptModeFormatInfo": "形式: user@host:port [-type TYPE] [-name NAME] [-p PASSWORD] [-k KEY_NAME] [-tags TAG1 TAG2...] [-note NOTE_TEXT]", + "scriptErrorMissingHost": "スクリプト行 '{line}' には 'user@host:port' 部分がありません。", + "scriptErrorInvalidType": "スクリプト行 '{line}' のタイプ '{type}' は無効です。有効なタイプは SSH, RDP, VNC です。", + "scriptErrorUnknownArg": "スクリプト行 '{line}' に不明な引数 '{arg}' があります。", + "scriptErrorUnexpectedToken": "スクリプト行 '{line}' に予期しないトークン '{token}' があります。", + "scriptErrorInvalidUserHostPort": "スクリプト行 '{line}' の '{part}' 部分の形式が無効です。期待される形式は 'user@host' または 'user@host:port' です。", + "scriptErrorInLine": "スクリプト行の解析中にエラーが発生しました: \"{line}\" - エラー: {error}", + "scriptErrorMissingType": "スクリプト行 '{line}' には接続タイプがないか、タイプが無効です。", + "scriptErrorInvalidUserHostFormat": "スクリプト行 '{line}' の user@host 部分の形式が無効です。", + "scriptErrorInvalidPort": "スクリプト行 '{line}' のポート '{port}' は無効です。", + "scriptErrorMissingPasswordForSsh": "スクリプト行 '{line}' (SSHパスワード認証) にはパスワード (-p) がありません。", + "scriptErrorMissingKeyNameForSsh": "スクリプト行 '{line}' (SSHキー認証) にはキー名 (-k) がありません。", + "scriptErrorMissingPasswordForType": "スクリプト行 '{line}' ({type}タイプ) にはパスワード (-p) がありません。", + "scriptErrorInternal": "スクリプト入力の処理中に内部解析エラーが発生しました。", + "scriptErrorTagNotFound": "スクリプト処理エラー: タグ '{tagName}' が見つかりません。", + "scriptErrorSshKeyNotFound": "スクリプト処理エラー: SSH キー '{keyName}' が見つかりません。", + "scriptErrorNothingToProcess": "処理する有効な接続データがありません。", + "scriptModeAddingConnections": "スクリプトモードで {count} 個の接続を追加しています..." }, "noConnections": "接続がありません。'新しい接続を追加'をクリックして作成してください。", "noUntaggedConnections": "タグなしの接続はありません。", diff --git a/packages/frontend/src/locales/zh-CN.json b/packages/frontend/src/locales/zh-CN.json index 2cee2ec..596f93a 100644 --- a/packages/frontend/src/locales/zh-CN.json +++ b/packages/frontend/src/locales/zh-CN.json @@ -195,7 +195,31 @@ "errorPasswordRequiredForBatchVNC": "批量添加 VNC 连接时,必须提供 VNC 密码。", "errorBatchAddResult": "批量添加: {successCount} 个成功, {errorCount} 个失败。首个错误: {firstErrorEncountered}", "successBatchAddResult": "批量添加成功: {successCount} 个连接已创建。", - "errorIpRangeNotAllowedInEditMode": "编辑模式下不支持 IP 范围。请使用单个 IP 地址。" + "errorIpRangeNotAllowedInEditMode": "编辑模式下不支持 IP 范围。请使用单个 IP 地址。", + "scriptModeSubmitPlaceholder": "脚本模式提交逻辑待实现。", + "scriptModeEmpty": "脚本输入不能为空。", + "scriptModeSubmitPending": "正在处理脚本模式提交...", + "sectionScriptMode": "脚本模式", + "scriptModeInputLabel": "连接脚本 (每行一个)", + "scriptModePlaceholder": "请输入连接脚本,每行一个连接配置。", + "scriptModeFormatInfo": "格式: user@host:port [-type TYPE] [-name NAME] [-p PASSWORD] [-k KEY_NAME] [-tags TAG1 TAG2...] [-note NOTE_TEXT]", + "scriptErrorMissingHost": "脚本行 '{line}' 缺少 'user@host:port' 部分。", + "scriptErrorInvalidType": "脚本行 '{line}' 中的类型 '{type}' 无效。有效类型为 SSH, RDP, VNC。", + "scriptErrorUnknownArg": "脚本行 '{line}' 中存在未知参数 '{arg}'。", + "scriptErrorUnexpectedToken": "脚本行 '{line}' 中出现意外标记 '{token}'。", + "scriptErrorInvalidUserHostPort": "脚本行 '{line}' 中的 '{part}' 部分格式无效。期望格式为 'user@host' 或 'user@host:port'。", + "scriptErrorInLine": "解析脚本行时出错: \"{line}\" - 错误: {error}", + "scriptErrorMissingType": "脚本行 '{line}' 缺少连接类型或类型无效。", + "scriptErrorInvalidUserHostFormat": "脚本行 '{line}' 的 user@host 部分格式无效。", + "scriptErrorInvalidPort": "脚本行 '{line}' 的端口 '{port}' 无效。", + "scriptErrorMissingPasswordForSsh": "脚本行 '{line}' (SSH密码认证) 缺少密码 (-p)。", + "scriptErrorMissingKeyNameForSsh": "脚本行 '{line}' (SSH密钥认证) 缺少密钥名称 (-k)。", + "scriptErrorMissingPasswordForType": "脚本行 '{line}' ({type}类型) 缺少密码 (-p)。", + "scriptErrorInternal": "处理脚本输入时发生内部解析错误。", + "scriptErrorTagNotFound": "脚本处理错误:未找到标签 '{tagName}'。", + "scriptErrorSshKeyNotFound": "脚本处理错误:未找到 SSH 密钥 '{keyName}'。", + "scriptErrorNothingToProcess": "没有可处理的有效连接数据。", + "scriptModeAddingConnections": "正在通过脚本模式添加 {count} 个连接..." }, "test": { "success": "连接测试成功!",