diff --git a/src/main/core/factory.ts b/src/main/core/factory.ts index 64bad07..70cb69d 100644 --- a/src/main/core/factory.ts +++ b/src/main/core/factory.ts @@ -12,10 +12,11 @@ import { mihomoProfileWorkDir, mihomoWorkConfigPath, mihomoWorkDir, - overridePath + overridePath, + rulePath } from '../utils/dirs' import { parse, stringify } from '../utils/yaml' -import { copyFile, mkdir, writeFile } from 'fs/promises' +import { copyFile, mkdir, writeFile, readFile } from 'fs/promises' import { deepMerge } from '../utils/merge' import vm from 'vm' import { existsSync, writeFileSync } from 'fs' @@ -24,11 +25,41 @@ import path from 'path' let runtimeConfigStr: string let runtimeConfig: IMihomoConfig +// 辅助函数:处理带偏移量的规则 +function processRulesWithOffset(ruleStrings: string[], currentRules: string[], isAppend = false) { + const normalRules: string[] = [] + let rules = [...currentRules] + + ruleStrings.forEach(ruleStr => { + const parts = ruleStr.split(',') + const firstPartIsNumber = !isNaN(Number(parts[0])) && parts[0].trim() !== '' && parts.length >= 3 + + if (firstPartIsNumber) { + const offset = parseInt(parts[0]) + const rule = parts.slice(1).join(',') + + if (isAppend) { + // 后置规则的插入位置计算 + const insertPosition = Math.max(0, rules.length - Math.min(offset, rules.length)) + rules.splice(insertPosition, 0, rule) + } else { + // 前置规则的插入位置计算 + const insertPosition = Math.min(offset, rules.length) + rules.splice(insertPosition, 0, rule) + } + } else { + normalRules.push(ruleStr) + } + }) + + return { normalRules, insertRules: rules } +} + export async function generateProfile(): Promise { // 读取最新的配置 const { current } = await getProfileConfig(true) const { diffWorkDir = false, controlDns = true, controlSniff = true, useNameserverPolicy } = await getAppConfig() - const currentProfile = await overrideProfile(current, await getProfile(current)) + let currentProfile = await overrideProfile(current, await getProfile(current)) let controledMihomoConfig = await getControledMihomoConfig() // 根据开关状态过滤控制配置 @@ -44,6 +75,49 @@ export async function generateProfile(): Promise { delete controledMihomoConfig?.dns?.['nameserver-policy'] } + // 应用规则文件 + try { + const ruleFilePath = rulePath(current || 'default') + if (existsSync(ruleFilePath)) { + const ruleFileContent = await readFile(ruleFilePath, 'utf-8') + const ruleData = parse(ruleFileContent) as { prepend?: string[], append?: string[], delete?: string[] } | null + + if (ruleData && typeof ruleData === 'object') { + // 确保 rules 数组存在 + if (!currentProfile.rules) { + currentProfile.rules = [] as unknown as [] + } + + let rules = [...currentProfile.rules] as unknown as string[] + + // 处理前置规则 + if (ruleData.prepend?.length) { + const { normalRules: prependRules, insertRules } = processRulesWithOffset(ruleData.prepend, rules) + rules = [...prependRules, ...insertRules] + } + + // 处理后置规则 + if (ruleData.append?.length) { + const { normalRules: appendRules, insertRules } = processRulesWithOffset(ruleData.append, rules, true) + rules = [...insertRules, ...appendRules] + } + + // 处理删除规则 + if (ruleData.delete?.length) { + const deleteSet = new Set(ruleData.delete) + rules = rules.filter(rule => { + const ruleStr = Array.isArray(rule) ? rule.join(',') : rule + return !deleteSet.has(ruleStr) + }) + } + + currentProfile.rules = rules as unknown as [] + } + } + } catch (error) { + console.error('读取或应用规则文件时出错:', error) + } + const profile = deepMerge(currentProfile, controledMihomoConfig) // 确保可以拿到基础日志信息 // 使用 debug 可以调试内核相关问题 `debug/pprof` diff --git a/src/main/resolve/backup.ts b/src/main/resolve/backup.ts index 892a73d..1eb3bdf 100644 --- a/src/main/resolve/backup.ts +++ b/src/main/resolve/backup.ts @@ -9,6 +9,7 @@ import { overrideDir, profileConfigPath, profilesDir, + rulesDir, subStoreDir, themesDir } from '../utils/dirs' @@ -38,6 +39,7 @@ export async function webdavBackup(): Promise { zip.addLocalFolder(themesDir(), 'themes') zip.addLocalFolder(profilesDir(), 'profiles') zip.addLocalFolder(overrideDir(), 'override') + zip.addLocalFolder(rulesDir(), 'rules') zip.addLocalFolder(subStoreDir(), 'substore') const date = new Date() const zipFileName = `${process.platform}_${dayjs(date).format('YYYY-MM-DD_HH-mm-ss')}.zip` @@ -229,6 +231,9 @@ export async function exportLocalBackup(): Promise { if (existsSync(subStoreDir())) { zip.addLocalFolder(subStoreDir(), 'substore') } + if (existsSync(rulesDir())) { + zip.addLocalFolder(rulesDir(), 'rules') + } const date = new Date() const zipFileName = `clash-party-backup-${dayjs(date).format('YYYY-MM-DD_HH-mm-ss')}.zip` diff --git a/src/main/utils/dirs.ts b/src/main/utils/dirs.ts index f0f8cbf..12181de 100644 --- a/src/main/utils/dirs.ts +++ b/src/main/utils/dirs.ts @@ -158,3 +158,11 @@ export function coreLogPath(): string { const name = `core-${year}-${month}-${day}` return path.join(logDir(), `${name}.log`) } + +export function rulesDir(): string { + return path.join(dataDir(), 'rules') +} + +export function rulePath(id: string): string { + return path.join(rulesDir(), `${id}.yaml`) +} diff --git a/src/main/utils/init.ts b/src/main/utils/init.ts index 88a1b72..844d021 100644 --- a/src/main/utils/init.ts +++ b/src/main/utils/init.ts @@ -11,6 +11,7 @@ import { profilePath, profilesDir, resourcesFilesDir, + rulesDir, subStoreDir, themesDir } from './dirs' @@ -106,6 +107,7 @@ async function initDirs(): Promise { themesDir(), profilesDir(), overrideDir(), + rulesDir(), mihomoWorkDir(), logDir(), mihomoTestDir(), diff --git a/src/main/utils/ipc.ts b/src/main/utils/ipc.ts index f42c9af..be56ac2 100644 --- a/src/main/utils/ipc.ts +++ b/src/main/utils/ipc.ts @@ -99,7 +99,7 @@ import { writeTheme } from '../resolve/theme' import { subStoreCollections, subStoreSubs } from '../core/subStoreApi' -import { logDir } from './dirs' +import { logDir, rulePath } from './dirs' import path from 'path' import v8 from 'v8' import { getGistUrl } from '../resolve/gistApi' @@ -148,6 +148,18 @@ export async function clearMihomoVersionCache(): Promise { clearVersionCache('MetaCubeX', 'mihomo') } +export async function getRuleStr(id: string): Promise { + const { readFile } = await import('fs/promises') + const filePath = rulePath(id) + return await readFile(filePath, 'utf-8') +} + +export async function setRuleStr(id: string, str: string): Promise { + const { writeFile } = await import('fs/promises') + const filePath = rulePath(id) + await writeFile(filePath, str, 'utf-8') +} + export function registerIpcMainHandlers(): void { ipcMain.handle('mihomoVersion', ipcErrorWrapper(mihomoVersion)) ipcMain.handle('mihomoCloseConnection', (_e, id) => ipcErrorWrapper(mihomoCloseConnection)(id)) @@ -348,4 +360,8 @@ export function registerIpcMainHandlers(): void { // 注册清除版本缓存的IPC处理程序 ipcMain.handle('clearMihomoVersionCache', () => ipcErrorWrapper(clearMihomoVersionCache)()) + + // 规则相关IPC处理程序 + ipcMain.handle('getRuleStr', (_e, id) => ipcErrorWrapper(getRuleStr)(id)) + ipcMain.handle('setRuleStr', (_e, id, str) => ipcErrorWrapper(setRuleStr)(id, str)) } \ No newline at end of file diff --git a/src/renderer/src/components/profiles/edit-rules-modal.tsx b/src/renderer/src/components/profiles/edit-rules-modal.tsx index e8d16bd..e1c4808 100644 --- a/src/renderer/src/components/profiles/edit-rules-modal.tsx +++ b/src/renderer/src/components/profiles/edit-rules-modal.tsx @@ -15,7 +15,7 @@ import { Divider } from '@heroui/react' import React, { useEffect, useState } from 'react' -import { getProfileStr, setProfileStr } from '@renderer/utils/ipc' +import { getProfileStr, setRuleStr, getRuleStr } from '@renderer/utils/ipc' import { useTranslation } from 'react-i18next' import yaml from 'js-yaml' import { IoMdTrash, IoMdArrowUp, IoMdArrowDown, IoMdUndo } from 'react-icons/io' @@ -32,8 +32,37 @@ interface RuleItem { payload: string proxy: string additionalParams?: string[] + offset?: number } +const domainValidator = (value: string): boolean => { + if (value.length > 253 || value.length < 2) return false; + + return new RegExp( + "^(?:(?:[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)\\.)+[a-zA-Z]{2,}$" + ).test(value) || + ["localhost", "local", "localdomain"].includes(value.toLowerCase()); +}; + +const domainSuffixValidator = (value: string): boolean => { + return new RegExp( + "^(?:[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\\.)*[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\\.[a-zA-Z]{2,}$" + ).test(value); +}; + +const domainKeywordValidator = (value: string): boolean => { + return value.length > 0 && !value.includes(",") && !value.includes(" "); +}; + +const domainRegexValidator = (value: string): boolean => { + try { + new RegExp(value); + return true; + } catch { + return false; + } +}; + const portValidator = (value: string): boolean => { return new RegExp( "^(?:[1-9]\\d{0,3}|[1-5]\\d{4}|6[0-4]\\d{3}|65[0-4]\\d{2}|655[0-2]\\d|6553[0-5])$", @@ -64,18 +93,22 @@ const ruleDefinitionsMap = new Map domainValidator(value) }], ["DOMAIN-SUFFIX", { name: "DOMAIN-SUFFIX", example: "example.com", + validator: (value) => domainSuffixValidator(value) }], ["DOMAIN-KEYWORD", { name: "DOMAIN-KEYWORD", example: "example", + validator: (value) => domainKeywordValidator(value) }], ["DOMAIN-REGEX", { name: "DOMAIN-REGEX", example: "example.*", + validator: (value) => domainRegexValidator(value) }], ["GEOSITE", { name: "GEOSITE", @@ -241,7 +274,7 @@ const getRuleExample = (ruleType: string): string => { }; const isAddRuleDisabled = (newRule: RuleItem, validateRulePayload: (ruleType: string, payload: string) => boolean): boolean => { - return (!(newRule.payload.trim() || newRule.type === 'MATCH')) || !newRule.type || + return (!(newRule.payload.trim() || newRule.type === 'MATCH')) || !newRule.type || !newRule.proxy || (newRule.type !== 'MATCH' && newRule.payload.trim() !== '' && !validateRulePayload(newRule.type, newRule.payload)); }; @@ -249,11 +282,13 @@ const EditRulesModal: React.FC = (props) => { const { id, onClose } = props const [rules, setRules] = useState([]) const [filteredRules, setFilteredRules] = useState([]) - const [profileContent, setProfileContent] = useState('') + const [, setProfileContent] = useState('') const [newRule, setNewRule] = useState({ type: 'DOMAIN', payload: '', proxy: 'DIRECT', additionalParams: [] }) const [searchTerm, setSearchTerm] = useState('') const [proxyGroups, setProxyGroups] = useState([]) - const [deletedRules, setDeletedRules] = useState>(new Set()) // Store indices of deleted rules + const [deletedRules, setDeletedRules] = useState>(new Set()) + const [prependRules, setPrependRules] = useState>(new Set()) + const [appendRules, setAppendRules] = useState>(new Set()) const { t } = useTranslation() const getContent = async (): Promise => { @@ -262,12 +297,15 @@ const EditRulesModal: React.FC = (props) => { try { const parsed = yaml.load(content) as any + let initialRules: RuleItem[] = []; + if (parsed && parsed.rules && Array.isArray(parsed.rules)) { - const parsedRules = parsed.rules.map((rule: string) => { + initialRules = parsed.rules.map((rule: string) => { const parts = rule.split(',') if (parts[0] === 'MATCH') { return { type: 'MATCH', + payload: '', proxy: parts[1] } } else { @@ -279,21 +317,18 @@ const EditRulesModal: React.FC = (props) => { additionalParams } } - }) - setRules(parsedRules) - setFilteredRules(parsedRules) + }); } - // 从配置文件中提取代理组 + // 提取代理组 if (parsed) { const groups: string[] = [] - // 添加代理组名称 + // 添加代理组和代理名称 if (Array.isArray(parsed['proxy-groups'])) { groups.push(...parsed['proxy-groups'].map((group: any) => group?.name).filter(Boolean)) } - // 添加代理名称 if (Array.isArray(parsed['proxies'])) { groups.push(...parsed['proxies'].map((proxy: any) => proxy?.name).filter(Boolean)) } @@ -304,8 +339,116 @@ const EditRulesModal: React.FC = (props) => { // 去重 setProxyGroups([...new Set(groups)]) } + + // 读取规则文件 + try { + const ruleContent = await getRuleStr(id); + const ruleData = yaml.load(ruleContent) as { prepend?: string[], append?: string[], delete?: string[] }; + + if (ruleData) { + let allRules = [...initialRules]; + const newPrependRules = new Set(); + const newAppendRules = new Set(); + const newDeletedRules = new Set(); + + // 处理前置规则 + if (ruleData.prepend && Array.isArray(ruleData.prepend)) { + const prependRules: RuleItem[] = []; + ruleData.prepend.forEach((ruleStr: string) => { + prependRules.push(parseRuleString(ruleStr)); + }); + + // 插入前置规则 + const { updatedRules, ruleIndices } = processRulesWithPositions( + prependRules, + allRules, + (rule, currentRules) => { + if (rule.offset !== undefined && rule.offset < currentRules.length) { + return rule.offset; + } + return 0; + } + ); + + allRules = updatedRules; + ruleIndices.forEach(index => newPrependRules.add(index)); + } + + // 处理后置规则 + if (ruleData.append && Array.isArray(ruleData.append)) { + const appendRules: RuleItem[] = []; + ruleData.append.forEach((ruleStr: string) => { + appendRules.push(parseRuleString(ruleStr)); + }); + + // 插入后置规则 + const { updatedRules, ruleIndices } = processAppendRulesWithPositions( + appendRules, + allRules, + (rule, currentRules) => { + if (rule.offset !== undefined) { + return Math.max(0, currentRules.length - rule.offset); + } + return currentRules.length; + } + ); + + allRules = updatedRules; + + // 标记后置规则 + ruleIndices.forEach(index => newAppendRules.add(index)); + } + + // 处理删除规则 + if (ruleData.delete && Array.isArray(ruleData.delete)) { + const deleteRules = ruleData.delete.map((ruleStr: string) => { + return parseRuleString(ruleStr); + }); + + // 匹配并标记删除规则 + deleteRules.forEach(deleteRule => { + const matchedIndex = allRules.findIndex(rule => + rule.type === deleteRule.type && + rule.payload === deleteRule.payload && + rule.proxy === deleteRule.proxy && + JSON.stringify(rule.additionalParams || []) === JSON.stringify(deleteRule.additionalParams || []) + ); + + if (matchedIndex !== -1) { + newDeletedRules.add(matchedIndex); + } + }); + } + + // 更新状态 + setPrependRules(newPrependRules); + setAppendRules(newAppendRules); + setDeletedRules(newDeletedRules); + + // 设置规则列表 + setRules(allRules); + setFilteredRules(allRules); + } else { + // 使用初始规则 + setRules(initialRules); + setFilteredRules(initialRules); + // 清空规则标记 + setPrependRules(new Set()); + setAppendRules(new Set()); + setDeletedRules(new Set()); + } + } catch (ruleError) { + // 规则文件读取失败 + console.debug('规则文件读取失败:', ruleError); + setRules(initialRules); + setFilteredRules(initialRules); + // 清空规则标记 + setPrependRules(new Set()); + setAppendRules(new Set()); + setDeletedRules(new Set()); + } } catch (e) { - console.error('解析配置文件内容失败', e) + console.error('Failed to parse profile content', e) } } @@ -329,91 +472,42 @@ const EditRulesModal: React.FC = (props) => { const handleSave = async (): Promise => { try { - // 过滤掉已标记为删除的规则 - const rulesToSave = rules.filter((_, index) => !deletedRules.has(index)); + // 保存规则到文件 + const prependRuleStrings = Array.from(prependRules) + .filter(index => !deletedRules.has(index) && index < rules.length) + .map(index => convertRuleToString(rules[index])); - // 将规则转换回字符串格式 - const ruleStrings = rulesToSave.map(rule => { - const parts = [rule.type] - if (rule.payload) parts.push(rule.payload) - if (rule.proxy) parts.push(rule.proxy) - if (rule.additionalParams && rule.additionalParams.length > 0) { - parts.push(...rule.additionalParams) - } - return parts.join(',') - }) + const appendRuleStrings = Array.from(appendRules) + .filter(index => !deletedRules.has(index) && index < rules.length) + .map(index => convertRuleToString(rules[index])); - // 直接在原始内容中替换规则部分,保持原有格式 - let updatedContent = profileContent - - // 将内容按行分割,便于处理 - const lines = profileContent.split('\n') - const newLines: string[] = [] - let inRulesSection = false - let rulesSectionFound = false - let rulesIndent = '' - - for (let i = 0; i < lines.length; i++) { - const line = lines[i] - const trimmedLine = line.trim() - - // 检查是否是 rules: 开始行 - if (trimmedLine === 'rules:') { - rulesSectionFound = true - inRulesSection = true - rulesIndent = line.match(/^\s*/)?.[0] || '' - newLines.push(line) // 保留 rules: 行 - - // 添加新的规则行 - for (const rule of ruleStrings) { - newLines.push(`${rulesIndent} - ${rule}`) + // 保存删除的规则 + const deletedRuleStrings = Array.from(deletedRules) + .filter(index => index < rules.length && !prependRules.has(index) && !appendRules.has(index)) + .map(index => { + const rule = rules[index]; + const parts = [rule.type]; + if (rule.payload) parts.push(rule.payload); + if (rule.proxy) parts.push(rule.proxy); + if (rule.additionalParams && rule.additionalParams.length > 0) { + parts.push(...rule.additionalParams); } - - // 跳过原有的 rules 内容 - while (i + 1 < lines.length) { - const nextLine = lines[i + 1] - const nextTrimmed = nextLine.trim() - const nextIndent = nextLine.match(/^\s*/)?.[0] || '' - - // 如果下一行不是空行且缩进大于 rules 缩进,说明还是 rules 的内容 - if (nextTrimmed !== '' && nextIndent.length > rulesIndent.length) { - i++ // 跳过这一行 - } else { - break - } - } - continue - } - - // 如果在 rules 部分中,跳过处理 - if (inRulesSection && trimmedLine.startsWith('-')) { - // 检查是否还有 rules 行 - const currentIndent = line.match(/^\s*/)?.[0] || '' - if (currentIndent.length > rulesIndent.length) { - continue // 跳过原有规则行 - } else { - inRulesSection = false // rules 部分结束 - } - } - - newLines.push(line) - } + return parts.join(','); + }); - // 如果没有找到 rules 部分,添加到文件末尾 - if (!rulesSectionFound) { - newLines.push('') // 空行 - newLines.push('rules:') - for (const rule of ruleStrings) { - newLines.push(` - ${rule}`) - } - } + // 创建规则数据对象 + const ruleData = { + prepend: prependRuleStrings, + append: appendRuleStrings, + delete: deletedRuleStrings + }; - updatedContent = newLines.join('\n') - - await setProfileStr(id, updatedContent) - onClose() + // 保存到 YAML 文件 + const ruleYaml = yaml.dump(ruleData); + await setRuleStr(id, ruleYaml); + onClose(); } catch (e) { - alert(t('profiles.editRules.saveError') + ': ' + (e instanceof Error ? e.message : String(e))) + alert(t('profiles.editRules.saveError') + ': ' + (e instanceof Error ? e.message : String(e))); } } @@ -464,73 +558,129 @@ const EditRulesModal: React.FC = (props) => { let updatedRules: RuleItem[]; if (position === 'prepend') { - updatedRules = [newRuleItem, ...rules]; + // 前置规则插入 + const insertPosition = newRuleItem.offset !== undefined ? + Math.min(newRuleItem.offset, rules.length) : 0; + + updatedRules = [...rules]; + updatedRules.splice(insertPosition, 0, newRuleItem); + + // 更新规则索引 + const { newPrependRules, newAppendRules, newDeletedRules } = updateAllRuleIndicesAfterInsertion(prependRules, appendRules, deletedRules, insertPosition, true); + + // 更新状态 + setPrependRules(newPrependRules); + setAppendRules(newAppendRules); + setDeletedRules(newDeletedRules); } else { - updatedRules = [...rules, newRuleItem]; + // 后置规则插入 + const insertPosition = newRuleItem.offset !== undefined ? + Math.max(0, rules.length - newRuleItem.offset) : + rules.length; + + updatedRules = [...rules]; + updatedRules.splice(insertPosition, 0, newRuleItem); + + // 更新规则索引 + const { newPrependRules, newAppendRules, newDeletedRules } = updateAllRuleIndicesAfterInsertion(prependRules, appendRules, deletedRules, insertPosition, false, true); + + // 更新状态 + setPrependRules(newPrependRules); + setAppendRules(newAppendRules); + setDeletedRules(newDeletedRules); } - setRules(updatedRules) - setFilteredRules(updatedRules) - setNewRule({ type: 'DOMAIN', payload: '', proxy: 'DIRECT', additionalParams: [] }) + // 更新规则列表 + setRules(updatedRules); + setFilteredRules(updatedRules); + setNewRule({ type: 'DOMAIN', payload: '', proxy: 'DIRECT', additionalParams: [] }); } } const handleRemoveRule = (index: number): void => { setDeletedRules(prev => { - const newSet = new Set(prev) + const newSet = new Set(prev); if (newSet.has(index)) { - newSet.delete(index) // 如果已经标记为删除,则取消标记 + newSet.delete(index); // 如果已经标记为删除,则取消标记 } else { - newSet.add(index) // 标记为删除 + newSet.add(index); // 标记为删除 } - return newSet - }) + return newSet; + }); } const handleMoveRuleUp = (index: number): void => { - if (index <= 0) return - const updatedRules = [...rules] - const temp = updatedRules[index] - updatedRules[index] = updatedRules[index - 1] - updatedRules[index - 1] = temp - setRules(updatedRules) - setFilteredRules(updatedRules) - setDeletedRules(prev => { - const newSet = new Set() - prev.forEach(idx => { - if (idx === index) { - newSet.add(index - 1) - } else if (idx === index - 1) { - newSet.add(index) - } else { - newSet.add(idx) - } - }) - return newSet - }) + if (index <= 0) return; + const updatedRules = [...rules]; + const temp = updatedRules[index]; + updatedRules[index] = updatedRules[index - 1]; + updatedRules[index - 1] = temp; + + // 更新前置规则偏移量 + if (prependRules.has(index)) { + updatedRules[index - 1] = { + ...updatedRules[index - 1], + offset: Math.max(0, (updatedRules[index - 1].offset || 0) - 1) + }; + } + + // 更新后置规则偏移量 + if (appendRules.has(index)) { + updatedRules[index - 1] = { + ...updatedRules[index - 1], + offset: (updatedRules[index - 1].offset || 0) + 1 + }; + } + + // 首先更新规则数组 + setRules(updatedRules); + setFilteredRules(updatedRules); + + // 更新删除规则索引 + setDeletedRules(prev => updateRuleIndices(prev, index, index - 1)); + + // 更新前置规则索引 + setPrependRules(prev => updateRuleIndices(prev, index, index - 1)); + + // 更新后置规则索引 + setAppendRules(prev => updateRuleIndices(prev, index, index - 1)); } const handleMoveRuleDown = (index: number): void => { - if (index >= rules.length - 1) return - const updatedRules = [...rules] - const temp = updatedRules[index] - updatedRules[index] = updatedRules[index + 1] - updatedRules[index + 1] = temp - setRules(updatedRules) - setFilteredRules(updatedRules) - setDeletedRules(prev => { - const newSet = new Set() - prev.forEach(idx => { - if (idx === index) { - newSet.add(index + 1) - } else if (idx === index + 1) { - newSet.add(index) - } else { - newSet.add(idx) - } - }) - return newSet - }) + if (index >= rules.length - 1) return; + const updatedRules = [...rules]; + const temp = updatedRules[index]; + updatedRules[index] = updatedRules[index + 1]; + updatedRules[index + 1] = temp; + + // 更新前置规则偏移量 + if (prependRules.has(index)) { + updatedRules[index + 1] = { + ...updatedRules[index + 1], + offset: (updatedRules[index + 1].offset || 0) + 1 + }; + } + + // 更新后置规则偏移量 + if (appendRules.has(index)) { + updatedRules[index + 1] = { + ...updatedRules[index + 1], + offset: Math.max(0, (updatedRules[index + 1].offset || 0) - 1) + }; + } + + // 首先更新规则数组 + setRules(updatedRules); + setFilteredRules(updatedRules); + + // 更新删除规则索引 + setDeletedRules(prev => updateRuleIndices(prev, index, index + 1)); + + // 更新前置规则索引 + setPrependRules(prev => updateRuleIndices(prev, index, index + 1)); + + // 更新后置规则索引 + setAppendRules(prev => updateRuleIndices(prev, index, index + 1)); } const validateRulePayload = (ruleType: string, payload: string): boolean => { @@ -550,6 +700,174 @@ const EditRulesModal: React.FC = (props) => { const rule = ruleDefinitionsMap.get(ruleType); return rule?.validator; }; + + // 解析规则字符串 + const parseRuleString = (ruleStr: string): RuleItem => { + const parts = ruleStr.split(','); + const firstPartIsNumber = !isNaN(Number(parts[0])) && parts[0].trim() !== '' && parts.length >= 3; + + let offset = 0; + let ruleParts = parts; + + if (firstPartIsNumber) { + offset = parseInt(parts[0]); + ruleParts = parts.slice(1); + } + + if (ruleParts[0] === 'MATCH') { + return { + type: 'MATCH', + payload: '', + proxy: ruleParts[1], + offset: offset > 0 ? offset : undefined + }; + } else { + const additionalParams = ruleParts.slice(3).filter(param => param.trim() !== '') || []; + return { + type: ruleParts[0], + payload: ruleParts[1], + proxy: ruleParts[2], + additionalParams, + offset: offset > 0 ? offset : undefined + }; + } + }; + + // 规则转字符串 + const convertRuleToString = (rule: RuleItem): string => { + const parts = [rule.type]; + if (rule.payload) parts.push(rule.payload); + if (rule.proxy) parts.push(rule.proxy); + if (rule.additionalParams && rule.additionalParams.length > 0) { + parts.push(...rule.additionalParams); + } + + // 添加偏移量 + if (rule.offset !== undefined && rule.offset > 0) { + parts.unshift(rule.offset.toString()); + } + + return parts.join(','); + }; + + // 处理前置规则位置 + const processRulesWithPositions = (rules: RuleItem[], allRules: RuleItem[], positionCalculator: (rule: RuleItem, currentRules: RuleItem[]) => number): { updatedRules: RuleItem[], ruleIndices: Set } => { + const updatedRules = [...allRules]; + const ruleIndices = new Set(); + + // 按顺序处理规则 + rules.forEach((rule) => { + const targetPosition = positionCalculator(rule, updatedRules); + const actualPosition = Math.min(targetPosition, updatedRules.length); + updatedRules.splice(actualPosition, 0, rule); + + // 更新索引 + const newRuleIndices = new Set(); + ruleIndices.forEach(idx => { + if (idx >= actualPosition) { + newRuleIndices.add(idx + 1); + } else { + newRuleIndices.add(idx); + } + }); + // 添加当前规则索引 + newRuleIndices.add(actualPosition); + + // 更新索引集合 + ruleIndices.clear(); + newRuleIndices.forEach(idx => ruleIndices.add(idx)); + }); + + return { updatedRules, ruleIndices }; + }; + + // 处理后置规则位置 + const processAppendRulesWithPositions = (rules: RuleItem[], allRules: RuleItem[], positionCalculator: (rule: RuleItem, currentRules: RuleItem[]) => number): { updatedRules: RuleItem[], ruleIndices: Set } => { + const updatedRules = [...allRules]; + const ruleIndices = new Set(); + + // 按顺序处理规则 + rules.forEach((rule) => { + const targetPosition = positionCalculator(rule, updatedRules); + const actualPosition = Math.min(targetPosition, updatedRules.length); + updatedRules.splice(actualPosition, 0, rule); + + // 更新索引 + const newRuleIndices = new Set(); + ruleIndices.forEach(idx => { + if (idx >= actualPosition) { + newRuleIndices.add(idx + 1); + } else { + newRuleIndices.add(idx); + } + }); + // 添加当前规则索引 + newRuleIndices.add(actualPosition); + + // 更新索引集合 + ruleIndices.clear(); + newRuleIndices.forEach(idx => ruleIndices.add(idx)); + }); + + return { updatedRules, ruleIndices }; + }; + + // 更新规则索引 + const updateRuleIndices = (prev: Set, index1: number, index2: number): Set => { + const newSet = new Set(); + prev.forEach(idx => { + if (idx === index1) { + newSet.add(index2); + } else if (idx === index2) { + newSet.add(index1); + } else { + newSet.add(idx); + } + }); + return newSet; + }; + + // 计算插入位置的索引 + const getUpdatedIndexForInsertion = (index: number, insertPosition: number): number => { + if (index >= insertPosition) { + return index + 1; + } else { + return index; + } + }; + + // 插入规则后更新所有索引 + const updateAllRuleIndicesAfterInsertion = (prependRules: Set, appendRules: Set, deletedRules: Set, insertPosition: number, isNewPrependRule: boolean = false, isNewAppendRule: boolean = false): { newPrependRules: Set, newAppendRules: Set, newDeletedRules: Set } => { + const newPrependRules = new Set(); + const newAppendRules = new Set(); + const newDeletedRules = new Set(); + + // 更新前置规则索引 + prependRules.forEach(idx => { + newPrependRules.add(getUpdatedIndexForInsertion(idx, insertPosition)); + }); + + // 更新后置规则索引 + appendRules.forEach(idx => { + newAppendRules.add(getUpdatedIndexForInsertion(idx, insertPosition)); + }); + + // 更新删除规则索引 + deletedRules.forEach(idx => { + newDeletedRules.add(getUpdatedIndexForInsertion(idx, insertPosition)); + }); + + // 标记新规则 + if (isNewPrependRule) { + newPrependRules.add(insertPosition); + } + + if (isNewAppendRule) { + newAppendRules.add(insertPosition); + } + + return { newPrependRules, newAppendRules, newDeletedRules }; + }; return ( = (props) => { : t('profiles.editRules.noRules')} ) : ( - filteredRules.map((rule) => { + filteredRules.map((rule, index) => { const originalIndex = rules.indexOf(rule); + let bgColorClass = 'bg-content2'; + let textStyleClass = ''; + if (deletedRules.has(originalIndex)) { + bgColorClass = 'bg-danger-50 opacity-70'; + textStyleClass = 'line-through text-foreground-500'; + } else if (prependRules.has(originalIndex) || appendRules.has(originalIndex)) { + bgColorClass = 'bg-success-50'; + } + return ( -
+
@@ -709,40 +1036,40 @@ const EditRulesModal: React.FC = (props) => {
-
+
{rule.type === 'MATCH' ? rule.proxy : rule.payload}
{rule.proxy && rule.type !== 'MATCH' && ( -
{rule.proxy}
+
{rule.proxy}
)}
diff --git a/src/renderer/src/utils/ipc.ts b/src/renderer/src/utils/ipc.ts index 5b8dbe2..736797b 100644 --- a/src/renderer/src/utils/ipc.ts +++ b/src/renderer/src/utils/ipc.ts @@ -549,6 +549,14 @@ export async function copyEnv(type: 'bash' | 'cmd' | 'powershell'): Promise { + return ipcErrorWrapper(await window.electron.ipcRenderer.invoke('getRuleStr', id)) +} + +export async function setRuleStr(id: string, str: string): Promise { + return ipcErrorWrapper(await window.electron.ipcRenderer.invoke('setRuleStr', id, str)) +} + async function alert(msg: T): Promise { const msgStr = typeof msg === 'string' ? msg : JSON.stringify(msg) return await window.electron.ipcRenderer.invoke('alert', msgStr)