feat: 脚本模式下自动创建标签

Refs #28
This commit is contained in:
Baobhan Sith
2025-05-12 19:19:16 +08:00
parent 170d2f3939
commit 6354f5d856
4 changed files with 156 additions and 116 deletions
@@ -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;