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,
|
||||
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<void> {
|
||||
// 读取最新的配置
|
||||
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<void> {
|
||||
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`
|
||||
|
||||
@ -9,6 +9,7 @@ import {
|
||||
overrideDir,
|
||||
profileConfigPath,
|
||||
profilesDir,
|
||||
rulesDir,
|
||||
subStoreDir,
|
||||
themesDir
|
||||
} from '../utils/dirs'
|
||||
@ -38,6 +39,7 @@ export async function webdavBackup(): Promise<boolean> {
|
||||
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<boolean> {
|
||||
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`
|
||||
|
||||
@ -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`)
|
||||
}
|
||||
|
||||
@ -11,6 +11,7 @@ import {
|
||||
profilePath,
|
||||
profilesDir,
|
||||
resourcesFilesDir,
|
||||
rulesDir,
|
||||
subStoreDir,
|
||||
themesDir
|
||||
} from './dirs'
|
||||
@ -106,6 +107,7 @@ async function initDirs(): Promise<void> {
|
||||
themesDir(),
|
||||
profilesDir(),
|
||||
overrideDir(),
|
||||
rulesDir(),
|
||||
mihomoWorkDir(),
|
||||
logDir(),
|
||||
mihomoTestDir(),
|
||||
|
||||
@ -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<void> {
|
||||
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 {
|
||||
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))
|
||||
}
|
||||
@ -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<string, {
|
||||
["DOMAIN", {
|
||||
name: "DOMAIN",
|
||||
example: "example.com",
|
||||
validator: (value) => 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> = (props) => {
|
||||
const { id, onClose } = props
|
||||
const [rules, setRules] = 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 [searchTerm, setSearchTerm] = useState('')
|
||||
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 getContent = async (): Promise<void> => {
|
||||
@ -262,12 +297,15 @@ const EditRulesModal: React.FC<Props> = (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> = (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> = (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<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) {
|
||||
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> => {
|
||||
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)
|
||||
const appendRuleStrings = Array.from(appendRules)
|
||||
.filter(index => !deletedRules.has(index) && index < rules.length)
|
||||
.map(index => convertRuleToString(rules[index]));
|
||||
|
||||
// 保存删除的规则
|
||||
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)
|
||||
parts.push(...rule.additionalParams);
|
||||
}
|
||||
return parts.join(',')
|
||||
})
|
||||
return parts.join(',');
|
||||
});
|
||||
|
||||
// 直接在原始内容中替换规则部分,保持原有格式
|
||||
let updatedContent = profileContent
|
||||
// 创建规则数据对象
|
||||
const ruleData = {
|
||||
prepend: prependRuleStrings,
|
||||
append: appendRuleStrings,
|
||||
delete: deletedRuleStrings
|
||||
};
|
||||
|
||||
// 将内容按行分割,便于处理
|
||||
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}`)
|
||||
}
|
||||
|
||||
// 跳过原有的 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()
|
||||
// 保存到 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> = (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<number>()
|
||||
prev.forEach(idx => {
|
||||
if (idx === index) {
|
||||
newSet.add(index - 1)
|
||||
} else if (idx === index - 1) {
|
||||
newSet.add(index)
|
||||
} else {
|
||||
newSet.add(idx)
|
||||
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)
|
||||
};
|
||||
}
|
||||
})
|
||||
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 => {
|
||||
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<number>()
|
||||
prev.forEach(idx => {
|
||||
if (idx === index) {
|
||||
newSet.add(index + 1)
|
||||
} else if (idx === index + 1) {
|
||||
newSet.add(index)
|
||||
} else {
|
||||
newSet.add(idx)
|
||||
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
|
||||
};
|
||||
}
|
||||
})
|
||||
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 => {
|
||||
@ -551,6 +701,174 @@ const EditRulesModal: React.FC<Props> = (props) => {
|
||||
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 (
|
||||
<Modal
|
||||
backdrop="blur"
|
||||
@ -689,10 +1007,19 @@ const EditRulesModal: React.FC<Props> = (props) => {
|
||||
: t('profiles.editRules.noRules')}
|
||||
</div>
|
||||
) : (
|
||||
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 (
|
||||
<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 items-center gap-1">
|
||||
<Chip size="sm" variant="flat">
|
||||
@ -709,40 +1036,40 @@ const EditRulesModal: React.FC<Props> = (props) => {
|
||||
</div>
|
||||
</div>
|
||||
<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}
|
||||
</div>
|
||||
{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 className="flex gap-1">
|
||||
<Button
|
||||
size="sm"
|
||||
variant="light"
|
||||
onPress={() => handleMoveRuleUp(originalIndex)}
|
||||
onPress={() => originalIndex !== -1 && handleMoveRuleUp(originalIndex)}
|
||||
isIconOnly
|
||||
isDisabled={originalIndex === 0 || deletedRules.has(originalIndex)}
|
||||
isDisabled={originalIndex === -1 || originalIndex === 0 || deletedRules.has(originalIndex)}
|
||||
>
|
||||
<IoMdArrowUp className="text-lg" />
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="light"
|
||||
onPress={() => handleMoveRuleDown(originalIndex)}
|
||||
onPress={() => originalIndex !== -1 && handleMoveRuleDown(originalIndex)}
|
||||
isIconOnly
|
||||
isDisabled={originalIndex === rules.length - 1 || deletedRules.has(originalIndex)}
|
||||
isDisabled={originalIndex === -1 || originalIndex === rules.length - 1 || deletedRules.has(originalIndex)}
|
||||
>
|
||||
<IoMdArrowDown className="text-lg" />
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
color={deletedRules.has(originalIndex) ? "success" : "danger"}
|
||||
color={originalIndex !== -1 && deletedRules.has(originalIndex) ? "success" : "danger"}
|
||||
variant="light"
|
||||
onPress={() => handleRemoveRule(originalIndex)}
|
||||
onPress={() => originalIndex !== -1 && handleRemoveRule(originalIndex)}
|
||||
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>
|
||||
</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))
|
||||
}
|
||||
|
||||
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> {
|
||||
const msgStr = typeof msg === 'string' ? msg : JSON.stringify(msg)
|
||||
return await window.electron.ipcRenderer.invoke('alert', msgStr)
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user