diff --git a/packages/frontend/src/composables/useAddConnectionForm.ts b/packages/frontend/src/composables/useAddConnectionForm.ts index 43f020a..fc06467 100644 --- a/packages/frontend/src/composables/useAddConnectionForm.ts +++ b/packages/frontend/src/composables/useAddConnectionForm.ts @@ -199,129 +199,125 @@ export function useAddConnectionForm(props: AddConnectionFormProps, emit: AddCon 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, proxyName: string | null, tags: string[], note: string | null, error?: string } => { - 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, proxyName: null, tags: [], note: null, error: t('connections.form.scriptErrorMissingHost', '缺少 user@host:port 部分') }; + // Helper function to parse a single script line using minimist - let type: 'SSH' | 'RDP' | 'VNC' | null = 'SSH'; - let name: string | null = null; + + const parseScriptLine = (line: string): { type: 'SSH' | 'RDP' | 'VNC', userHostPort: string, name: string, password: string | null, keyName: string | null, proxyName: string | null, tags: string[], note: string | null, error?: string } => { + line = line.trim(); + if (!line) { + return { type: 'SSH', userHostPort: '', name: '', password: null, keyName: null, proxyName: null, tags: [], note: null, error: t('connections.form.scriptErrorEmptyLine', 'Input line cannot be empty') }; + } + + // 1. Extract user@host:port + const firstSpaceIndex = line.indexOf(' '); + const userHostPortPart = firstSpaceIndex === -1 ? line : line.substring(0, firstSpaceIndex); + const optionsString = firstSpaceIndex === -1 ? '' : line.substring(firstSpaceIndex + 1).trim(); + + // 2. Validate user@host:port (allow user@host without port) + const userHostPortRegex = /^([^@\s]+)@([^:\s]+)(?::([0-9]+))?$/; + const match = userHostPortPart.match(userHostPortRegex); + if (!match) { + return { type: 'SSH', userHostPort: userHostPortPart, name: '', password: null, keyName: null, proxyName: null, tags: [], note: null, error: t('connections.form.scriptErrorInvalidUserHostPortFormat', { part: userHostPortPart }) }; + } + const [, user, host /*, portStr */] = match; // portStr not used for now + const defaultName = `${user}@${host}`; // Default name + + // 3. Initialize results and defaults + let type: 'SSH' | 'RDP' | 'VNC' = 'SSH'; + let name: string = defaultName; let password: string | null = null; let keyName: string | null = null; let proxyName: string | null = null; - const tags: string[] = []; + let 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') { - // Tags handled later - } else if (arg === '-note') { - // Note handled later - } - - 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, proxyName, 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 '-proxy': - proxyName = 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, proxyName, 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, '"'); + // 4. Parse optionsString + // Regex to split by space, respecting quotes + const args = optionsString.match(/(?:[^\s"]+|"[^"]*")+/g) || []; + let i = 0; + while (i < args.length) { + const arg = args[i]; + if (arg.startsWith('-')) { + const key = arg.substring(1).toLowerCase(); + i++; // Move to the expected position of the value + + if (key === 'tags') { + // Handle -tags, which can be followed by zero or more tags + tags = []; + while (i < args.length && !args[i].startsWith('-')) { + tags.push(args[i].replace(/^"|"$/g, '')); // Remove surrounding quotes + i++; } - tags.push(tag); + // No need to i++ here, the next loop iteration or outer loop handles it + } else if (key === 'note') { + // Handle -note, which consumes the rest of the line + const noteParts = []; + while (i < args.length) { + noteParts.push(args[i]); + i++; + } + note = noteParts.join(' ').replace(/^"|"$/g, ''); // Join parts and remove quotes + break; // Exit the outer loop as note consumes the rest + } else if (i >= args.length) { + // All other options require a value + return { type, userHostPort: userHostPortPart, name, password, keyName, proxyName, tags, note, error: t('connections.form.scriptErrorMissingValueForKey', { key: arg }) }; + } else { + // Handle options that require a single value + const value = args[i].replace(/^"|"$/g, ''); // Remove surrounding quotes + switch (key) { + case 'type': + const typeValue = value.toUpperCase(); + if (typeValue === 'SSH' || typeValue === 'RDP' || typeValue === 'VNC') { + type = typeValue; + } else { + return { type, userHostPort: userHostPortPart, name, password, keyName, proxyName, tags, note, error: t('connections.form.scriptErrorInvalidType', { value: args[i] }) }; + } + break; + case 'name': + name = value; + break; + case 'p': // password + password = value; + break; + case 'k': // key name + keyName = value; + break; + case 'proxy': + proxyName = value; + break; + default: + return { type, userHostPort: userHostPortPart, name, password, keyName, proxyName, tags, note, error: t('connections.form.scriptErrorUnknownOption', { option: arg }) }; + } + i++; // Move past the value } - } else if (currentArg === '-note') { - noteParts.push(remainingPart); } else { - return { type, userHostPort: userHostPortPart, name, password, keyName, proxyName, tags, note, error: t('connections.form.scriptErrorUnexpectedToken', { token: remainingPart }) }; + // Arguments after user@host:port must start with '-' + return { type, userHostPort: userHostPortPart, name, password, keyName, proxyName, tags, note, error: t('connections.form.scriptErrorUnexpectedArgument', { argument: arg }) }; } } - if (noteParts.length > 0 && currentArg === '-note') { - note = noteParts.join(' '); + // 5. Validation based on type + if (type === 'SSH') { + if (!password && !keyName) { + return { type, userHostPort: userHostPortPart, name, password, keyName, proxyName, tags, note, error: t('connections.form.scriptErrorMissingAuthForSsh') }; + } + // Allow both password and key, handle precedence in handleScriptModeSubmit + } else if (type === 'RDP') { + if (!password) { + return { type, userHostPort: userHostPortPart, name, password, keyName, proxyName, tags, note, error: t('connections.form.scriptErrorMissingPasswordForRdp') }; + } + if (keyName) { + return { type, userHostPort: userHostPortPart, name, password, keyName, proxyName, tags, note, error: t('connections.form.scriptErrorKeyNotApplicableForRdp') }; + } + } else if (type === 'VNC') { + if (!password) { + return { type, userHostPort: userHostPortPart, name, password, keyName, proxyName, tags, note, error: t('connections.form.scriptErrorMissingPasswordForVnc') }; + } + if (keyName) { + return { type, userHostPort: userHostPortPart, name, password, keyName, proxyName, tags, note, error: t('connections.form.scriptErrorKeyNotApplicableForVnc') }; + } } - const userHostPortRegex = /^[^@]+@[^:]+(:[0-9]+)?$/; - if (!userHostPortRegex.test(userHostPortPart)) { - return { type, userHostPort: userHostPortPart, name, password, keyName, proxyName, tags, note, error: t('connections.form.scriptErrorInvalidUserHostPort', { part: userHostPortPart })}; - } - return { type, userHostPort: userHostPortPart, name, password, keyName, proxyName, tags, note }; }; @@ -421,14 +417,22 @@ export function useAddConnectionForm(props: AddConnectionFormProps, emit: AddCon 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); // Renamed t to t_ to avoid conflict - if (foundTag) { - tagIds.push(foundTag.id); - } else { - uiNotificationsStore.showError(t('connections.form.scriptErrorTagNotFound', { tagName })); - resolutionErrorOccurred = true; - break; + let foundTag = tags.value.find(t_ => t_.name === tagName); // Renamed t to t_ to avoid conflict + if (!foundTag) { + // 自动创建不存在的标签 + const newTag = await tagsStore.addTag(tagName); + if (newTag) { + foundTag = newTag; + uiNotificationsStore.showInfo(t('connections.form.scriptTagCreated', { tagName })); + // 确保标签列表已更新 + await tagsStore.fetchTags(); + } else { + uiNotificationsStore.showError(t('connections.form.scriptErrorTagCreationFailed', { tagName })); + resolutionErrorOccurred = true; + break; + } } + tagIds.push(foundTag.id); } if (resolutionErrorOccurred) break; connData.tag_ids = tagIds; diff --git a/packages/frontend/src/locales/en-US.json b/packages/frontend/src/locales/en-US.json index 8aae8f9..a7e5c9b 100644 --- a/packages/frontend/src/locales/en-US.json +++ b/packages/frontend/src/locales/en-US.json @@ -220,6 +220,18 @@ "scriptErrorTagNotFound": "Script processing error: Tag '{tagName}' not found.", "scriptErrorSshKeyNotFound": "Script processing error: SSH Key '{keyName}' not found.", "scriptErrorNothingToProcess": "No valid connection data to process.", + "scriptErrorMissingAuthForSsh": "SSH connection must provide password (-p) or key name (-k)", + "scriptErrorMissingPasswordForRdp": "RDP connection must provide password (-p)", + "scriptErrorKeyNotApplicableForRdp": "Key name (-k) is not applicable for RDP connection", + "scriptErrorMissingPasswordForVnc": "VNC connection must provide password (-p)", + "scriptErrorKeyNotApplicableForVnc": "Key name (-k) is not applicable for VNC connection", + "scriptErrorMissingValueForKey": "Missing value for parameter '{key}'", + "scriptErrorUnknownOption": "Unknown option '{option}'", + "scriptErrorUnexpectedArgument": "Unexpected argument '{argument}'", + "scriptErrorEmptyLine": "Input line cannot be empty", + "scriptErrorInvalidUserHostPortFormat": "Invalid format for '{part}', expected format is 'user@host' or 'user@host:port'", + "scriptTagCreated": "Tag '{tagName}' created", + "scriptErrorTagCreationFailed": "Failed to create tag '{tagName}'", "scriptModeAddingConnections": "Adding {count} connections via script mode..." }, "test": { diff --git a/packages/frontend/src/locales/ja-JP.json b/packages/frontend/src/locales/ja-JP.json index 18ebe7c..25e2068 100644 --- a/packages/frontend/src/locales/ja-JP.json +++ b/packages/frontend/src/locales/ja-JP.json @@ -205,6 +205,18 @@ "scriptErrorTagNotFound": "スクリプト処理エラー: タグ '{tagName}' が見つかりません。", "scriptErrorSshKeyNotFound": "スクリプト処理エラー: SSH キー '{keyName}' が見つかりません。", "scriptErrorNothingToProcess": "処理する有効な接続データがありません。", + "scriptErrorMissingAuthForSsh": "SSH接続にはパスワード (-p) またはキー名 (-k) が必要です", + "scriptErrorMissingPasswordForRdp": "RDP接続にはパスワード (-p) が必要です", + "scriptErrorKeyNotApplicableForRdp": "キー名 (-k) はRDP接続には適用されません", + "scriptErrorMissingPasswordForVnc": "VNC接続にはパスワード (-p) が必要です", + "scriptErrorKeyNotApplicableForVnc": "キー名 (-k) はVNC接続には適用されません", + "scriptErrorMissingValueForKey": "パラメータ '{key}' に対応する値がありません", + "scriptErrorUnknownOption": "不明なオプション '{option}'", + "scriptErrorUnexpectedArgument": "予期しない引数 '{argument}'", + "scriptErrorEmptyLine": "入力行は空にできません", + "scriptErrorInvalidUserHostPortFormat": "'{part}' の形式が無効です、期待される形式は 'user@host' または 'user@host:port' です", + "scriptTagCreated": "タグ '{tagName}' が作成されました", + "scriptErrorTagCreationFailed": "タグ '{tagName}' の作成に失敗しました", "scriptModeAddingConnections": "スクリプトモードで {count} 個の接続を追加しています..." }, "noConnections": "接続がありません。'新しい接続を追加'をクリックして作成してください。", diff --git a/packages/frontend/src/locales/zh-CN.json b/packages/frontend/src/locales/zh-CN.json index 782535d..59d9d31 100644 --- a/packages/frontend/src/locales/zh-CN.json +++ b/packages/frontend/src/locales/zh-CN.json @@ -219,6 +219,18 @@ "scriptErrorTagNotFound": "脚本处理错误:未找到标签 '{tagName}'。", "scriptErrorSshKeyNotFound": "脚本处理错误:未找到 SSH 密钥 '{keyName}'。", "scriptErrorNothingToProcess": "没有可处理的有效连接数据。", + "scriptErrorMissingAuthForSsh": "SSH连接必须提供密码 (-p) 或密钥名称 (-k)", + "scriptErrorMissingPasswordForRdp": "RDP连接必须提供密码 (-p)", + "scriptErrorKeyNotApplicableForRdp": "密钥名称 (-k) 不适用于 RDP 连接", + "scriptErrorMissingPasswordForVnc": "VNC连接必须提供密码 (-p)", + "scriptErrorKeyNotApplicableForVnc": "密钥名称 (-k) 不适用于 VNC 连接", + "scriptErrorMissingValueForKey": "参数 '{key}' 缺少对应的值", + "scriptErrorUnknownOption": "未知选项 '{option}'", + "scriptErrorUnexpectedArgument": "意外参数 '{argument}'", + "scriptErrorEmptyLine": "输入行不能为空", + "scriptErrorInvalidUserHostPortFormat": "'{part}' 部分格式无效,期望格式为 'user@host' 或 'user@host:port'", + "scriptTagCreated": "标签 '{tagName}' 已创建", + "scriptErrorTagCreationFailed": "创建标签 '{tagName}' 失败", "scriptModeAddingConnections": "正在通过脚本模式添加 {count} 个连接..." }, "test": {