mirror of
https://gh.catmak.name/https://github.com/mihomo-party-org/mihomo-party
synced 2025-12-27 05:00:30 +08:00
Refactor rule (#1326)
* refactor: enhance prepend/append deletion in rule editor * fix: Reject empty payload for non-MATCH rules * More validator * feat(backup): add rules directory support to local and WebDAV backups
This commit is contained in:
parent
0e58f6f314
commit
4af5cae356
@ -12,10 +12,11 @@ import {
|
|||||||
mihomoProfileWorkDir,
|
mihomoProfileWorkDir,
|
||||||
mihomoWorkConfigPath,
|
mihomoWorkConfigPath,
|
||||||
mihomoWorkDir,
|
mihomoWorkDir,
|
||||||
overridePath
|
overridePath,
|
||||||
|
rulePath
|
||||||
} from '../utils/dirs'
|
} from '../utils/dirs'
|
||||||
import { parse, stringify } from '../utils/yaml'
|
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 { deepMerge } from '../utils/merge'
|
||||||
import vm from 'vm'
|
import vm from 'vm'
|
||||||
import { existsSync, writeFileSync } from 'fs'
|
import { existsSync, writeFileSync } from 'fs'
|
||||||
@ -24,11 +25,41 @@ import path from 'path'
|
|||||||
let runtimeConfigStr: string
|
let runtimeConfigStr: string
|
||||||
let runtimeConfig: IMihomoConfig
|
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<void> {
|
export async function generateProfile(): Promise<void> {
|
||||||
// 读取最新的配置
|
// 读取最新的配置
|
||||||
const { current } = await getProfileConfig(true)
|
const { current } = await getProfileConfig(true)
|
||||||
const { diffWorkDir = false, controlDns = true, controlSniff = true, useNameserverPolicy } = await getAppConfig()
|
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()
|
let controledMihomoConfig = await getControledMihomoConfig()
|
||||||
|
|
||||||
// 根据开关状态过滤控制配置
|
// 根据开关状态过滤控制配置
|
||||||
@ -44,6 +75,49 @@ export async function generateProfile(): Promise<void> {
|
|||||||
delete controledMihomoConfig?.dns?.['nameserver-policy']
|
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)
|
const profile = deepMerge(currentProfile, controledMihomoConfig)
|
||||||
// 确保可以拿到基础日志信息
|
// 确保可以拿到基础日志信息
|
||||||
// 使用 debug 可以调试内核相关问题 `debug/pprof`
|
// 使用 debug 可以调试内核相关问题 `debug/pprof`
|
||||||
|
|||||||
@ -9,6 +9,7 @@ import {
|
|||||||
overrideDir,
|
overrideDir,
|
||||||
profileConfigPath,
|
profileConfigPath,
|
||||||
profilesDir,
|
profilesDir,
|
||||||
|
rulesDir,
|
||||||
subStoreDir,
|
subStoreDir,
|
||||||
themesDir
|
themesDir
|
||||||
} from '../utils/dirs'
|
} from '../utils/dirs'
|
||||||
@ -38,6 +39,7 @@ export async function webdavBackup(): Promise<boolean> {
|
|||||||
zip.addLocalFolder(themesDir(), 'themes')
|
zip.addLocalFolder(themesDir(), 'themes')
|
||||||
zip.addLocalFolder(profilesDir(), 'profiles')
|
zip.addLocalFolder(profilesDir(), 'profiles')
|
||||||
zip.addLocalFolder(overrideDir(), 'override')
|
zip.addLocalFolder(overrideDir(), 'override')
|
||||||
|
zip.addLocalFolder(rulesDir(), 'rules')
|
||||||
zip.addLocalFolder(subStoreDir(), 'substore')
|
zip.addLocalFolder(subStoreDir(), 'substore')
|
||||||
const date = new Date()
|
const date = new Date()
|
||||||
const zipFileName = `${process.platform}_${dayjs(date).format('YYYY-MM-DD_HH-mm-ss')}.zip`
|
const zipFileName = `${process.platform}_${dayjs(date).format('YYYY-MM-DD_HH-mm-ss')}.zip`
|
||||||
@ -229,6 +231,9 @@ export async function exportLocalBackup(): Promise<boolean> {
|
|||||||
if (existsSync(subStoreDir())) {
|
if (existsSync(subStoreDir())) {
|
||||||
zip.addLocalFolder(subStoreDir(), 'substore')
|
zip.addLocalFolder(subStoreDir(), 'substore')
|
||||||
}
|
}
|
||||||
|
if (existsSync(rulesDir())) {
|
||||||
|
zip.addLocalFolder(rulesDir(), 'rules')
|
||||||
|
}
|
||||||
|
|
||||||
const date = new Date()
|
const date = new Date()
|
||||||
const zipFileName = `clash-party-backup-${dayjs(date).format('YYYY-MM-DD_HH-mm-ss')}.zip`
|
const zipFileName = `clash-party-backup-${dayjs(date).format('YYYY-MM-DD_HH-mm-ss')}.zip`
|
||||||
|
|||||||
@ -158,3 +158,11 @@ export function coreLogPath(): string {
|
|||||||
const name = `core-${year}-${month}-${day}`
|
const name = `core-${year}-${month}-${day}`
|
||||||
return path.join(logDir(), `${name}.log`)
|
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`)
|
||||||
|
}
|
||||||
|
|||||||
@ -11,6 +11,7 @@ import {
|
|||||||
profilePath,
|
profilePath,
|
||||||
profilesDir,
|
profilesDir,
|
||||||
resourcesFilesDir,
|
resourcesFilesDir,
|
||||||
|
rulesDir,
|
||||||
subStoreDir,
|
subStoreDir,
|
||||||
themesDir
|
themesDir
|
||||||
} from './dirs'
|
} from './dirs'
|
||||||
@ -106,6 +107,7 @@ async function initDirs(): Promise<void> {
|
|||||||
themesDir(),
|
themesDir(),
|
||||||
profilesDir(),
|
profilesDir(),
|
||||||
overrideDir(),
|
overrideDir(),
|
||||||
|
rulesDir(),
|
||||||
mihomoWorkDir(),
|
mihomoWorkDir(),
|
||||||
logDir(),
|
logDir(),
|
||||||
mihomoTestDir(),
|
mihomoTestDir(),
|
||||||
|
|||||||
@ -99,7 +99,7 @@ import {
|
|||||||
writeTheme
|
writeTheme
|
||||||
} from '../resolve/theme'
|
} from '../resolve/theme'
|
||||||
import { subStoreCollections, subStoreSubs } from '../core/subStoreApi'
|
import { subStoreCollections, subStoreSubs } from '../core/subStoreApi'
|
||||||
import { logDir } from './dirs'
|
import { logDir, rulePath } from './dirs'
|
||||||
import path from 'path'
|
import path from 'path'
|
||||||
import v8 from 'v8'
|
import v8 from 'v8'
|
||||||
import { getGistUrl } from '../resolve/gistApi'
|
import { getGistUrl } from '../resolve/gistApi'
|
||||||
@ -148,6 +148,18 @@ export async function clearMihomoVersionCache(): Promise<void> {
|
|||||||
clearVersionCache('MetaCubeX', 'mihomo')
|
clearVersionCache('MetaCubeX', 'mihomo')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function getRuleStr(id: string): Promise<string> {
|
||||||
|
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<void> {
|
||||||
|
const { writeFile } = await import('fs/promises')
|
||||||
|
const filePath = rulePath(id)
|
||||||
|
await writeFile(filePath, str, 'utf-8')
|
||||||
|
}
|
||||||
|
|
||||||
export function registerIpcMainHandlers(): void {
|
export function registerIpcMainHandlers(): void {
|
||||||
ipcMain.handle('mihomoVersion', ipcErrorWrapper(mihomoVersion))
|
ipcMain.handle('mihomoVersion', ipcErrorWrapper(mihomoVersion))
|
||||||
ipcMain.handle('mihomoCloseConnection', (_e, id) => ipcErrorWrapper(mihomoCloseConnection)(id))
|
ipcMain.handle('mihomoCloseConnection', (_e, id) => ipcErrorWrapper(mihomoCloseConnection)(id))
|
||||||
@ -348,4 +360,8 @@ export function registerIpcMainHandlers(): void {
|
|||||||
|
|
||||||
// 注册清除版本缓存的IPC处理程序
|
// 注册清除版本缓存的IPC处理程序
|
||||||
ipcMain.handle('clearMihomoVersionCache', () => ipcErrorWrapper(clearMihomoVersionCache)())
|
ipcMain.handle('clearMihomoVersionCache', () => ipcErrorWrapper(clearMihomoVersionCache)())
|
||||||
|
|
||||||
|
// 规则相关IPC处理程序
|
||||||
|
ipcMain.handle('getRuleStr', (_e, id) => ipcErrorWrapper(getRuleStr)(id))
|
||||||
|
ipcMain.handle('setRuleStr', (_e, id, str) => ipcErrorWrapper(setRuleStr)(id, str))
|
||||||
}
|
}
|
||||||
@ -15,7 +15,7 @@ import {
|
|||||||
Divider
|
Divider
|
||||||
} from '@heroui/react'
|
} from '@heroui/react'
|
||||||
import React, { useEffect, useState } from '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 { useTranslation } from 'react-i18next'
|
||||||
import yaml from 'js-yaml'
|
import yaml from 'js-yaml'
|
||||||
import { IoMdTrash, IoMdArrowUp, IoMdArrowDown, IoMdUndo } from 'react-icons/io'
|
import { IoMdTrash, IoMdArrowUp, IoMdArrowDown, IoMdUndo } from 'react-icons/io'
|
||||||
@ -32,8 +32,37 @@ interface RuleItem {
|
|||||||
payload: string
|
payload: string
|
||||||
proxy: string
|
proxy: string
|
||||||
additionalParams?: 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 => {
|
const portValidator = (value: string): boolean => {
|
||||||
return new RegExp(
|
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])$",
|
"^(?:[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<string, {
|
|||||||
["DOMAIN", {
|
["DOMAIN", {
|
||||||
name: "DOMAIN",
|
name: "DOMAIN",
|
||||||
example: "example.com",
|
example: "example.com",
|
||||||
|
validator: (value) => domainValidator(value)
|
||||||
}],
|
}],
|
||||||
["DOMAIN-SUFFIX", {
|
["DOMAIN-SUFFIX", {
|
||||||
name: "DOMAIN-SUFFIX",
|
name: "DOMAIN-SUFFIX",
|
||||||
example: "example.com",
|
example: "example.com",
|
||||||
|
validator: (value) => domainSuffixValidator(value)
|
||||||
}],
|
}],
|
||||||
["DOMAIN-KEYWORD", {
|
["DOMAIN-KEYWORD", {
|
||||||
name: "DOMAIN-KEYWORD",
|
name: "DOMAIN-KEYWORD",
|
||||||
example: "example",
|
example: "example",
|
||||||
|
validator: (value) => domainKeywordValidator(value)
|
||||||
}],
|
}],
|
||||||
["DOMAIN-REGEX", {
|
["DOMAIN-REGEX", {
|
||||||
name: "DOMAIN-REGEX",
|
name: "DOMAIN-REGEX",
|
||||||
example: "example.*",
|
example: "example.*",
|
||||||
|
validator: (value) => domainRegexValidator(value)
|
||||||
}],
|
}],
|
||||||
["GEOSITE", {
|
["GEOSITE", {
|
||||||
name: "GEOSITE",
|
name: "GEOSITE",
|
||||||
@ -241,7 +274,7 @@ const getRuleExample = (ruleType: string): string => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const isAddRuleDisabled = (newRule: RuleItem, validateRulePayload: (ruleType: string, payload: string) => boolean): boolean => {
|
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));
|
(newRule.type !== 'MATCH' && newRule.payload.trim() !== '' && !validateRulePayload(newRule.type, newRule.payload));
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -249,11 +282,13 @@ const EditRulesModal: React.FC<Props> = (props) => {
|
|||||||
const { id, onClose } = props
|
const { id, onClose } = props
|
||||||
const [rules, setRules] = useState<RuleItem[]>([])
|
const [rules, setRules] = useState<RuleItem[]>([])
|
||||||
const [filteredRules, setFilteredRules] = useState<RuleItem[]>([])
|
const [filteredRules, setFilteredRules] = useState<RuleItem[]>([])
|
||||||
const [profileContent, setProfileContent] = useState('')
|
const [, setProfileContent] = useState('')
|
||||||
const [newRule, setNewRule] = useState<RuleItem>({ type: 'DOMAIN', payload: '', proxy: 'DIRECT', additionalParams: [] })
|
const [newRule, setNewRule] = useState<RuleItem>({ type: 'DOMAIN', payload: '', proxy: 'DIRECT', additionalParams: [] })
|
||||||
const [searchTerm, setSearchTerm] = useState('')
|
const [searchTerm, setSearchTerm] = useState('')
|
||||||
const [proxyGroups, setProxyGroups] = useState<string[]>([])
|
const [proxyGroups, setProxyGroups] = useState<string[]>([])
|
||||||
const [deletedRules, setDeletedRules] = useState<Set<number>>(new Set()) // Store indices of deleted rules
|
const [deletedRules, setDeletedRules] = useState<Set<number>>(new Set())
|
||||||
|
const [prependRules, setPrependRules] = useState<Set<number>>(new Set())
|
||||||
|
const [appendRules, setAppendRules] = useState<Set<number>>(new Set())
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
|
|
||||||
const getContent = async (): Promise<void> => {
|
const getContent = async (): Promise<void> => {
|
||||||
@ -262,12 +297,15 @@ const EditRulesModal: React.FC<Props> = (props) => {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const parsed = yaml.load(content) as any
|
const parsed = yaml.load(content) as any
|
||||||
|
let initialRules: RuleItem[] = [];
|
||||||
|
|
||||||
if (parsed && parsed.rules && Array.isArray(parsed.rules)) {
|
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(',')
|
const parts = rule.split(',')
|
||||||
if (parts[0] === 'MATCH') {
|
if (parts[0] === 'MATCH') {
|
||||||
return {
|
return {
|
||||||
type: 'MATCH',
|
type: 'MATCH',
|
||||||
|
payload: '',
|
||||||
proxy: parts[1]
|
proxy: parts[1]
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@ -279,21 +317,18 @@ const EditRulesModal: React.FC<Props> = (props) => {
|
|||||||
additionalParams
|
additionalParams
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
});
|
||||||
setRules(parsedRules)
|
|
||||||
setFilteredRules(parsedRules)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 从配置文件中提取代理组
|
// 提取代理组
|
||||||
if (parsed) {
|
if (parsed) {
|
||||||
const groups: string[] = []
|
const groups: string[] = []
|
||||||
|
|
||||||
// 添加代理组名称
|
// 添加代理组和代理名称
|
||||||
if (Array.isArray(parsed['proxy-groups'])) {
|
if (Array.isArray(parsed['proxy-groups'])) {
|
||||||
groups.push(...parsed['proxy-groups'].map((group: any) => group?.name).filter(Boolean))
|
groups.push(...parsed['proxy-groups'].map((group: any) => group?.name).filter(Boolean))
|
||||||
}
|
}
|
||||||
|
|
||||||
// 添加代理名称
|
|
||||||
if (Array.isArray(parsed['proxies'])) {
|
if (Array.isArray(parsed['proxies'])) {
|
||||||
groups.push(...parsed['proxies'].map((proxy: any) => proxy?.name).filter(Boolean))
|
groups.push(...parsed['proxies'].map((proxy: any) => proxy?.name).filter(Boolean))
|
||||||
}
|
}
|
||||||
@ -304,8 +339,116 @@ const EditRulesModal: React.FC<Props> = (props) => {
|
|||||||
// 去重
|
// 去重
|
||||||
setProxyGroups([...new Set(groups)])
|
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<number>();
|
||||||
|
const newAppendRules = new Set<number>();
|
||||||
|
const newDeletedRules = new Set<number>();
|
||||||
|
|
||||||
|
// 处理前置规则
|
||||||
|
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) {
|
} catch (e) {
|
||||||
console.error('解析配置文件内容失败', e)
|
console.error('Failed to parse profile content', e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -329,91 +472,42 @@ const EditRulesModal: React.FC<Props> = (props) => {
|
|||||||
|
|
||||||
const handleSave = async (): Promise<void> => {
|
const handleSave = async (): Promise<void> => {
|
||||||
try {
|
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 appendRuleStrings = Array.from(appendRules)
|
||||||
const ruleStrings = rulesToSave.map(rule => {
|
.filter(index => !deletedRules.has(index) && index < rules.length)
|
||||||
const parts = [rule.type]
|
.map(index => convertRuleToString(rules[index]));
|
||||||
if (rule.payload) parts.push(rule.payload)
|
|
||||||
if (rule.proxy) parts.push(rule.proxy)
|
// 保存删除的规则
|
||||||
|
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) {
|
if (rule.additionalParams && rule.additionalParams.length > 0) {
|
||||||
parts.push(...rule.additionalParams)
|
parts.push(...rule.additionalParams);
|
||||||
}
|
}
|
||||||
return parts.join(',')
|
return parts.join(',');
|
||||||
})
|
});
|
||||||
|
|
||||||
// 直接在原始内容中替换规则部分,保持原有格式
|
// 创建规则数据对象
|
||||||
let updatedContent = profileContent
|
const ruleData = {
|
||||||
|
prepend: prependRuleStrings,
|
||||||
|
append: appendRuleStrings,
|
||||||
|
delete: deletedRuleStrings
|
||||||
|
};
|
||||||
|
|
||||||
// 将内容按行分割,便于处理
|
// 保存到 YAML 文件
|
||||||
const lines = profileContent.split('\n')
|
const ruleYaml = yaml.dump(ruleData);
|
||||||
const newLines: string[] = []
|
await setRuleStr(id, ruleYaml);
|
||||||
let inRulesSection = false
|
onClose();
|
||||||
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}`)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 跳过原有的 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)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 如果没有找到 rules 部分,添加到文件末尾
|
|
||||||
if (!rulesSectionFound) {
|
|
||||||
newLines.push('') // 空行
|
|
||||||
newLines.push('rules:')
|
|
||||||
for (const rule of ruleStrings) {
|
|
||||||
newLines.push(` - ${rule}`)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
updatedContent = newLines.join('\n')
|
|
||||||
|
|
||||||
await setProfileStr(id, updatedContent)
|
|
||||||
onClose()
|
|
||||||
} catch (e) {
|
} 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> = (props) => {
|
|||||||
let updatedRules: RuleItem[];
|
let updatedRules: RuleItem[];
|
||||||
|
|
||||||
if (position === 'prepend') {
|
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 {
|
} 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)
|
setRules(updatedRules);
|
||||||
setNewRule({ type: 'DOMAIN', payload: '', proxy: 'DIRECT', additionalParams: [] })
|
setFilteredRules(updatedRules);
|
||||||
|
setNewRule({ type: 'DOMAIN', payload: '', proxy: 'DIRECT', additionalParams: [] });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleRemoveRule = (index: number): void => {
|
const handleRemoveRule = (index: number): void => {
|
||||||
setDeletedRules(prev => {
|
setDeletedRules(prev => {
|
||||||
const newSet = new Set(prev)
|
const newSet = new Set(prev);
|
||||||
if (newSet.has(index)) {
|
if (newSet.has(index)) {
|
||||||
newSet.delete(index) // 如果已经标记为删除,则取消标记
|
newSet.delete(index); // 如果已经标记为删除,则取消标记
|
||||||
} else {
|
} else {
|
||||||
newSet.add(index) // 标记为删除
|
newSet.add(index); // 标记为删除
|
||||||
}
|
}
|
||||||
return newSet
|
return newSet;
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleMoveRuleUp = (index: number): void => {
|
const handleMoveRuleUp = (index: number): void => {
|
||||||
if (index <= 0) return
|
if (index <= 0) return;
|
||||||
const updatedRules = [...rules]
|
const updatedRules = [...rules];
|
||||||
const temp = updatedRules[index]
|
const temp = updatedRules[index];
|
||||||
updatedRules[index] = updatedRules[index - 1]
|
updatedRules[index] = updatedRules[index - 1];
|
||||||
updatedRules[index - 1] = temp
|
updatedRules[index - 1] = temp;
|
||||||
setRules(updatedRules)
|
|
||||||
setFilteredRules(updatedRules)
|
// 更新前置规则偏移量
|
||||||
setDeletedRules(prev => {
|
if (prependRules.has(index)) {
|
||||||
const newSet = new Set<number>()
|
updatedRules[index - 1] = {
|
||||||
prev.forEach(idx => {
|
...updatedRules[index - 1],
|
||||||
if (idx === index) {
|
offset: Math.max(0, (updatedRules[index - 1].offset || 0) - 1)
|
||||||
newSet.add(index - 1)
|
};
|
||||||
} else if (idx === index - 1) {
|
|
||||||
newSet.add(index)
|
|
||||||
} else {
|
|
||||||
newSet.add(idx)
|
|
||||||
}
|
}
|
||||||
})
|
|
||||||
return newSet
|
// 更新后置规则偏移量
|
||||||
})
|
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 => {
|
const handleMoveRuleDown = (index: number): void => {
|
||||||
if (index >= rules.length - 1) return
|
if (index >= rules.length - 1) return;
|
||||||
const updatedRules = [...rules]
|
const updatedRules = [...rules];
|
||||||
const temp = updatedRules[index]
|
const temp = updatedRules[index];
|
||||||
updatedRules[index] = updatedRules[index + 1]
|
updatedRules[index] = updatedRules[index + 1];
|
||||||
updatedRules[index + 1] = temp
|
updatedRules[index + 1] = temp;
|
||||||
setRules(updatedRules)
|
|
||||||
setFilteredRules(updatedRules)
|
// 更新前置规则偏移量
|
||||||
setDeletedRules(prev => {
|
if (prependRules.has(index)) {
|
||||||
const newSet = new Set<number>()
|
updatedRules[index + 1] = {
|
||||||
prev.forEach(idx => {
|
...updatedRules[index + 1],
|
||||||
if (idx === index) {
|
offset: (updatedRules[index + 1].offset || 0) + 1
|
||||||
newSet.add(index + 1)
|
};
|
||||||
} else if (idx === index + 1) {
|
|
||||||
newSet.add(index)
|
|
||||||
} else {
|
|
||||||
newSet.add(idx)
|
|
||||||
}
|
}
|
||||||
})
|
|
||||||
return newSet
|
// 更新后置规则偏移量
|
||||||
})
|
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 => {
|
const validateRulePayload = (ruleType: string, payload: string): boolean => {
|
||||||
@ -551,6 +701,174 @@ const EditRulesModal: React.FC<Props> = (props) => {
|
|||||||
return rule?.validator;
|
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<number> } => {
|
||||||
|
const updatedRules = [...allRules];
|
||||||
|
const ruleIndices = new Set<number>();
|
||||||
|
|
||||||
|
// 按顺序处理规则
|
||||||
|
rules.forEach((rule) => {
|
||||||
|
const targetPosition = positionCalculator(rule, updatedRules);
|
||||||
|
const actualPosition = Math.min(targetPosition, updatedRules.length);
|
||||||
|
updatedRules.splice(actualPosition, 0, rule);
|
||||||
|
|
||||||
|
// 更新索引
|
||||||
|
const newRuleIndices = new Set<number>();
|
||||||
|
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<number> } => {
|
||||||
|
const updatedRules = [...allRules];
|
||||||
|
const ruleIndices = new Set<number>();
|
||||||
|
|
||||||
|
// 按顺序处理规则
|
||||||
|
rules.forEach((rule) => {
|
||||||
|
const targetPosition = positionCalculator(rule, updatedRules);
|
||||||
|
const actualPosition = Math.min(targetPosition, updatedRules.length);
|
||||||
|
updatedRules.splice(actualPosition, 0, rule);
|
||||||
|
|
||||||
|
// 更新索引
|
||||||
|
const newRuleIndices = new Set<number>();
|
||||||
|
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<number>, index1: number, index2: number): Set<number> => {
|
||||||
|
const newSet = new Set<number>();
|
||||||
|
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<number>, appendRules: Set<number>, deletedRules: Set<number>, insertPosition: number, isNewPrependRule: boolean = false, isNewAppendRule: boolean = false): { newPrependRules: Set<number>, newAppendRules: Set<number>, newDeletedRules: Set<number> } => {
|
||||||
|
const newPrependRules = new Set<number>();
|
||||||
|
const newAppendRules = new Set<number>();
|
||||||
|
const newDeletedRules = new Set<number>();
|
||||||
|
|
||||||
|
// 更新前置规则索引
|
||||||
|
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 (
|
return (
|
||||||
<Modal
|
<Modal
|
||||||
backdrop="blur"
|
backdrop="blur"
|
||||||
@ -689,10 +1007,19 @@ const EditRulesModal: React.FC<Props> = (props) => {
|
|||||||
: t('profiles.editRules.noRules')}
|
: t('profiles.editRules.noRules')}
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
filteredRules.map((rule) => {
|
filteredRules.map((rule, index) => {
|
||||||
const originalIndex = rules.indexOf(rule);
|
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 (
|
return (
|
||||||
<div key={originalIndex} className={`flex items-center gap-2 p-2 rounded-lg ${deletedRules.has(originalIndex) ? 'bg-danger-50 opacity-70' : 'bg-content2'}`}>
|
<div key={`${originalIndex}-${index}`} className={`flex items-center gap-2 p-2 rounded-lg ${bgColorClass}`}>
|
||||||
<div className="flex flex-col">
|
<div className="flex flex-col">
|
||||||
<div className="flex items-center gap-1">
|
<div className="flex items-center gap-1">
|
||||||
<Chip size="sm" variant="flat">
|
<Chip size="sm" variant="flat">
|
||||||
@ -709,40 +1036,40 @@ const EditRulesModal: React.FC<Props> = (props) => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex-1 min-w-0">
|
<div className="flex-1 min-w-0">
|
||||||
<div className="font-medium truncate">
|
<div className={`font-medium truncate ${textStyleClass}`}>
|
||||||
{rule.type === 'MATCH' ? rule.proxy : rule.payload}
|
{rule.type === 'MATCH' ? rule.proxy : rule.payload}
|
||||||
</div>
|
</div>
|
||||||
{rule.proxy && rule.type !== 'MATCH' && (
|
{rule.proxy && rule.type !== 'MATCH' && (
|
||||||
<div className="text-sm text-foreground-500 truncate">{rule.proxy}</div>
|
<div className={`text-sm text-foreground-500 truncate ${textStyleClass}`}>{rule.proxy}</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex gap-1">
|
<div className="flex gap-1">
|
||||||
<Button
|
<Button
|
||||||
size="sm"
|
size="sm"
|
||||||
variant="light"
|
variant="light"
|
||||||
onPress={() => handleMoveRuleUp(originalIndex)}
|
onPress={() => originalIndex !== -1 && handleMoveRuleUp(originalIndex)}
|
||||||
isIconOnly
|
isIconOnly
|
||||||
isDisabled={originalIndex === 0 || deletedRules.has(originalIndex)}
|
isDisabled={originalIndex === -1 || originalIndex === 0 || deletedRules.has(originalIndex)}
|
||||||
>
|
>
|
||||||
<IoMdArrowUp className="text-lg" />
|
<IoMdArrowUp className="text-lg" />
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
size="sm"
|
size="sm"
|
||||||
variant="light"
|
variant="light"
|
||||||
onPress={() => handleMoveRuleDown(originalIndex)}
|
onPress={() => originalIndex !== -1 && handleMoveRuleDown(originalIndex)}
|
||||||
isIconOnly
|
isIconOnly
|
||||||
isDisabled={originalIndex === rules.length - 1 || deletedRules.has(originalIndex)}
|
isDisabled={originalIndex === -1 || originalIndex === rules.length - 1 || deletedRules.has(originalIndex)}
|
||||||
>
|
>
|
||||||
<IoMdArrowDown className="text-lg" />
|
<IoMdArrowDown className="text-lg" />
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
size="sm"
|
size="sm"
|
||||||
color={deletedRules.has(originalIndex) ? "success" : "danger"}
|
color={originalIndex !== -1 && deletedRules.has(originalIndex) ? "success" : "danger"}
|
||||||
variant="light"
|
variant="light"
|
||||||
onPress={() => handleRemoveRule(originalIndex)}
|
onPress={() => originalIndex !== -1 && handleRemoveRule(originalIndex)}
|
||||||
isIconOnly
|
isIconOnly
|
||||||
>
|
>
|
||||||
{deletedRules.has(originalIndex) ? <IoMdUndo className="text-lg" /> : <IoMdTrash className="text-lg" />}
|
{originalIndex !== -1 && deletedRules.has(originalIndex) ? <IoMdUndo className="text-lg" /> : <IoMdTrash className="text-lg" />}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -549,6 +549,14 @@ export async function copyEnv(type: 'bash' | 'cmd' | 'powershell'): Promise<void
|
|||||||
return ipcErrorWrapper(await window.electron.ipcRenderer.invoke('copyEnv', type))
|
return ipcErrorWrapper(await window.electron.ipcRenderer.invoke('copyEnv', type))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function getRuleStr(id: string): Promise<string> {
|
||||||
|
return ipcErrorWrapper(await window.electron.ipcRenderer.invoke('getRuleStr', id))
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function setRuleStr(id: string, str: string): Promise<void> {
|
||||||
|
return ipcErrorWrapper(await window.electron.ipcRenderer.invoke('setRuleStr', id, str))
|
||||||
|
}
|
||||||
|
|
||||||
async function alert<T>(msg: T): Promise<void> {
|
async function alert<T>(msg: T): Promise<void> {
|
||||||
const msgStr = typeof msg === 'string' ? msg : JSON.stringify(msg)
|
const msgStr = typeof msg === 'string' ? msg : JSON.stringify(msg)
|
||||||
return await window.electron.ipcRenderer.invoke('alert', msgStr)
|
return await window.electron.ipcRenderer.invoke('alert', msgStr)
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user