mirror of
https://github.com/binaricat/Netcatty.git
synced 2026-09-24 15:49:09 +00:00
0ccea42785
- 在脚本配置中添加 targetGroups 字段用于指定动态组路径 - 更新工具规范定义以支持动态组路径参数 - 实现组范围的连接脚本动态继承功能 - 更新 CLI 适配器以支持 --target-groups 参数 - 添加测试用例验证动态组路径映射和解析 - 更新国际化资源文件添加相关提示文本 - 重构脚本目标匹配逻辑分离显式主机和组路径匹配 - 实现组路径变更时的重映射和删除处理逻辑 - 更新 UI 组件支持组路径选择和显示功能 - 修改文档描述更新动态组路径使用说明
164 lines
5.2 KiB
TypeScript
164 lines
5.2 KiB
TypeScript
import type { Host, ManagedSource, Snippet } from '../types';
|
|
|
|
export function normalizeGroupTargetPath(value: string): string {
|
|
return value
|
|
.replace(/\\/g, '/')
|
|
.split('/')
|
|
.map((part) => part.trim())
|
|
.filter(Boolean)
|
|
.join('/');
|
|
}
|
|
|
|
function replaceGroupPathPrefix(path: string, sourcePath: string, nextPath: string): string {
|
|
if (path === sourcePath) return nextPath;
|
|
if (path.startsWith(`${sourcePath}/`)) return nextPath + path.slice(sourcePath.length);
|
|
return path;
|
|
}
|
|
|
|
export function remapSnippetTargetGroupPaths(
|
|
snippets: Snippet[],
|
|
sourceValue: string,
|
|
nextValue: string,
|
|
): Snippet[] {
|
|
const sourcePath = normalizeGroupTargetPath(sourceValue);
|
|
const nextPath = normalizeGroupTargetPath(nextValue);
|
|
if (!sourcePath || !nextPath || sourcePath === nextPath) return snippets;
|
|
|
|
let changed = false;
|
|
const nextSnippets = snippets.map((snippet) => {
|
|
if (!snippet.targetGroups?.length) return snippet;
|
|
const targetGroups = Array.from(new Set(
|
|
snippet.targetGroups.map((path) => replaceGroupPathPrefix(path, sourcePath, nextPath)),
|
|
));
|
|
if (targetGroups.every((path, index) => path === snippet.targetGroups?.[index])) return snippet;
|
|
changed = true;
|
|
return { ...snippet, targetGroups };
|
|
});
|
|
return changed ? nextSnippets : snippets;
|
|
}
|
|
|
|
export function removeSnippetTargetGroupPaths(
|
|
snippets: Snippet[],
|
|
removedValues: Iterable<string>,
|
|
): Snippet[] {
|
|
const removedPaths = [...removedValues]
|
|
.map(normalizeGroupTargetPath)
|
|
.filter(Boolean);
|
|
if (removedPaths.length === 0) return snippets;
|
|
const isRemoved = (path: string) => removedPaths.some(
|
|
(removed) => path === removed || path.startsWith(`${removed}/`),
|
|
);
|
|
|
|
let changed = false;
|
|
const nextSnippets = snippets.map((snippet) => {
|
|
if (!snippet.targetGroups?.some(isRemoved)) return snippet;
|
|
const targetGroups = snippet.targetGroups.filter((path) => !isRemoved(path));
|
|
changed = true;
|
|
return { ...snippet, targetGroups: targetGroups.length > 0 ? targetGroups : undefined };
|
|
});
|
|
return changed ? nextSnippets : snippets;
|
|
}
|
|
|
|
export function groupDisplayName(groupPath: string): string {
|
|
return groupPath.split('/').filter(Boolean).pop() ?? groupPath;
|
|
}
|
|
|
|
export function computeRenamedGroupPath(renameTargetPath: string, nextName: string): string {
|
|
const segments = renameTargetPath.split('/').filter(Boolean);
|
|
const parent = segments.slice(0, -1).join('/');
|
|
return parent ? `${parent}/${nextName}` : nextName;
|
|
}
|
|
|
|
export function allocateUnnamedGroupPath(
|
|
customGroups: string[],
|
|
parentPath: string | null,
|
|
baseName: string,
|
|
): { name: string; path: string } {
|
|
let name = baseName;
|
|
let counter = 2;
|
|
while (true) {
|
|
const path = parentPath ? `${parentPath}/${name}` : name;
|
|
if (!customGroups.includes(path)) {
|
|
return { name, path };
|
|
}
|
|
name = `${baseName} ${counter}`;
|
|
counter += 1;
|
|
}
|
|
}
|
|
|
|
export function ensureAncestorPathsExpanded(
|
|
groupPath: string,
|
|
ensurePathExpanded: (path: string) => void,
|
|
) {
|
|
const segments = groupPath.split('/').filter(Boolean);
|
|
for (let i = 1; i <= segments.length; i++) {
|
|
ensurePathExpanded(segments.slice(0, i).join('/'));
|
|
}
|
|
}
|
|
|
|
export type GroupPathRenameResult =
|
|
| {
|
|
ok: true;
|
|
nextPath: string;
|
|
updatedGroups: string[];
|
|
updatedHosts: Host[];
|
|
updatedManagedSources: ManagedSource[];
|
|
}
|
|
| { ok: false; error: 'required' | 'invalidChars' | 'duplicatePath' | 'unchanged' };
|
|
|
|
export function applyGroupPathRename(params: {
|
|
renameTargetPath: string;
|
|
nextName: string;
|
|
customGroups: string[];
|
|
hosts: Host[];
|
|
managedSources: ManagedSource[];
|
|
}): GroupPathRenameResult {
|
|
const trimmed = params.nextName.trim();
|
|
if (!trimmed) {
|
|
return { ok: false, error: 'required' };
|
|
}
|
|
if (trimmed.includes('/') || trimmed.includes('\\')) {
|
|
return { ok: false, error: 'invalidChars' };
|
|
}
|
|
|
|
const nextPath = computeRenamedGroupPath(params.renameTargetPath, trimmed);
|
|
if (nextPath === params.renameTargetPath) {
|
|
return { ok: false, error: 'unchanged' };
|
|
}
|
|
if (params.customGroups.includes(nextPath)) {
|
|
return { ok: false, error: 'duplicatePath' };
|
|
}
|
|
|
|
const { renameTargetPath, customGroups, hosts, managedSources } = params;
|
|
const updatedGroups = customGroups.map((groupPath) => {
|
|
if (groupPath === renameTargetPath) return nextPath;
|
|
if (groupPath.startsWith(`${renameTargetPath}/`)) {
|
|
return nextPath + groupPath.slice(renameTargetPath.length);
|
|
}
|
|
return groupPath;
|
|
});
|
|
const updatedHosts = hosts.map((host) => {
|
|
const group = host.group || '';
|
|
if (group === renameTargetPath) return { ...host, group: nextPath };
|
|
if (group.startsWith(`${renameTargetPath}/`)) {
|
|
return { ...host, group: nextPath + group.slice(renameTargetPath.length) };
|
|
}
|
|
return host;
|
|
});
|
|
const updatedManagedSources = managedSources.map((source) => {
|
|
if (source.groupName === renameTargetPath) return { ...source, groupName: nextPath };
|
|
if (source.groupName.startsWith(`${renameTargetPath}/`)) {
|
|
return { ...source, groupName: nextPath + source.groupName.slice(renameTargetPath.length) };
|
|
}
|
|
return source;
|
|
});
|
|
|
|
return {
|
|
ok: true,
|
|
nextPath,
|
|
updatedGroups: Array.from(new Set(updatedGroups)),
|
|
updatedHosts,
|
|
updatedManagedSources,
|
|
};
|
|
}
|