diff --git a/src/main/config/index.ts b/src/main/config/index.ts index ef76486..0f869e3 100644 --- a/src/main/config/index.ts +++ b/src/main/config/index.ts @@ -14,7 +14,8 @@ export { getProfileStr, setProfileStr, changeCurrentProfile, - updateProfileItem + updateProfileItem, + convertMrsRuleset } from './profile' export { getOverrideConfig, diff --git a/src/main/config/profile.ts b/src/main/config/profile.ts index d16b8f5..475e835 100644 --- a/src/main/config/profile.ts +++ b/src/main/config/profile.ts @@ -100,7 +100,7 @@ export async function addProfileItem(item: Partial): Promise export async function removeProfileItem(id: string): Promise { // 先清理自动更新定时器,防止已删除的订阅重新出现 await removeProfileUpdater(id) - + const config = await getProfileConfig() config.items = config.items?.filter((item) => item.id !== id) let shouldRestart = false @@ -191,7 +191,7 @@ export async function createProfile(item: Partial): Promise= 300) { throw new Error(`Subscription failed: Request status code ${res.status}`) @@ -199,7 +199,7 @@ export async function createProfile(item: Partial): Promise): Promise { ) } } + +export async function convertMrsRuleset(filePath: string, behavior: string): Promise { + const { exec } = await import('child_process') + const { promisify } = await import('util') + const execAsync = promisify(exec) + const { mihomoCorePath } = await import('../utils/dirs') + const { getAppConfig } = await import('./app') + const { tmpdir } = await import('os') + const { randomBytes } = await import('crypto') + const { unlink } = await import('fs/promises') + + const { core = 'mihomo' } = await getAppConfig() + const corePath = mihomoCorePath(core) + const { diffWorkDir = false } = await getAppConfig() + const { current } = await getProfileConfig() + let fullPath: string + if (isAbsolutePath(filePath)) { + fullPath = filePath + } else { + fullPath = join(diffWorkDir ? mihomoProfileWorkDir(current) : mihomoWorkDir(), filePath) + } + + const tempFileName = `mrs-convert-${randomBytes(8).toString('hex')}.txt` + const tempFilePath = join(tmpdir(), tempFileName) + + try { + // 使用 mihomo convert-ruleset 命令转换 MRS 文件为 text 格式 + // 命令格式: mihomo convert-ruleset + await execAsync(`"${corePath}" convert-ruleset ${behavior} mrs "${fullPath}" "${tempFilePath}"`) + const content = await readFile(tempFilePath, 'utf-8') + await unlink(tempFilePath) + + return content + } catch (error) { + try { + await unlink(tempFilePath) + } catch {} + throw error + } +} diff --git a/src/main/utils/ipc.ts b/src/main/utils/ipc.ts index be56ac2..bcdbf83 100644 --- a/src/main/utils/ipc.ts +++ b/src/main/utils/ipc.ts @@ -48,7 +48,8 @@ import { removeOverrideItem, getOverride, setOverride, - updateOverrideItem + updateOverrideItem, + convertMrsRuleset } from '../config' import { startSubStoreFrontendServer, @@ -215,6 +216,7 @@ export function registerIpcMainHandlers(): void { ipcMain.handle('getProfileStr', (_e, id) => ipcErrorWrapper(getProfileStr)(id)) ipcMain.handle('getFileStr', (_e, path) => ipcErrorWrapper(getFileStr)(path)) ipcMain.handle('setFileStr', (_e, path, str) => ipcErrorWrapper(setFileStr)(path, str)) + ipcMain.handle('convertMrsRuleset', (_e, path, behavior) => ipcErrorWrapper(convertMrsRuleset)(path, behavior)) ipcMain.handle('setProfileStr', (_e, id, str) => ipcErrorWrapper(setProfileStr)(id, str)) ipcMain.handle('updateProfileItem', (_e, item) => ipcErrorWrapper(updateProfileItem)(item)) ipcMain.handle('changeCurrentProfile', (_e, id) => ipcErrorWrapper(changeCurrentProfile)(id)) diff --git a/src/renderer/src/components/resources/rule-provider.tsx b/src/renderer/src/components/resources/rule-provider.tsx index 2656cfe..5928d33 100644 --- a/src/renderer/src/components/resources/rule-provider.tsx +++ b/src/renderer/src/components/resources/rule-provider.tsx @@ -25,7 +25,8 @@ const RuleProvider: React.FC = () => { type: '', title: '', format: '', - privderType: '' + privderType: '', + behavior: '' }) useEffect(() => { if (showDetails.title) { @@ -37,11 +38,12 @@ const RuleProvider: React.FC = () => { setShowDetails((prev) => ({ ...prev, show: true, - path: provider?.path || `rules/${getHash(provider?.url)}` + path: provider?.path || `rules/${getHash(provider?.url)}`, + behavior: provider?.behavior || 'domain' })) } } catch { - setShowDetails((prev) => ({ ...prev, path: '' })) + setShowDetails((prev) => ({ ...prev, path: '', behavior: '' })) } } fetchProviderPath(showDetails.title) @@ -94,7 +96,8 @@ const RuleProvider: React.FC = () => { title={showDetails.title} format={showDetails.format} privderType={showDetails.privderType} - onClose={() => setShowDetails({ show: false, path: '', type: '', title: '', format: '', privderType: '' })} + behavior={showDetails.behavior} + onClose={() => setShowDetails({ show: false, path: '', type: '', title: '', format: '', privderType: '', behavior: '' })} /> )} @@ -122,30 +125,29 @@ const RuleProvider: React.FC = () => { >
{dayjs(provider.updatedAt).fromNow()}
- {provider.format !== 'MrsRule' && ( - - )} + - {type == 'File' && ( + {type == 'File' && format !== 'MrsRule' && (