mirror of
https://gh.catmak.name/https://github.com/mihomo-party-org/mihomo-party
synced 2025-12-27 05:00:30 +08:00
feat: add mrs ruleset preview suppor
This commit is contained in:
parent
19ae63b253
commit
94f52cf636
@ -14,7 +14,8 @@ export {
|
|||||||
getProfileStr,
|
getProfileStr,
|
||||||
setProfileStr,
|
setProfileStr,
|
||||||
changeCurrentProfile,
|
changeCurrentProfile,
|
||||||
updateProfileItem
|
updateProfileItem,
|
||||||
|
convertMrsRuleset
|
||||||
} from './profile'
|
} from './profile'
|
||||||
export {
|
export {
|
||||||
getOverrideConfig,
|
getOverrideConfig,
|
||||||
|
|||||||
@ -100,7 +100,7 @@ export async function addProfileItem(item: Partial<IProfileItem>): Promise<void>
|
|||||||
export async function removeProfileItem(id: string): Promise<void> {
|
export async function removeProfileItem(id: string): Promise<void> {
|
||||||
// 先清理自动更新定时器,防止已删除的订阅重新出现
|
// 先清理自动更新定时器,防止已删除的订阅重新出现
|
||||||
await removeProfileUpdater(id)
|
await removeProfileUpdater(id)
|
||||||
|
|
||||||
const config = await getProfileConfig()
|
const config = await getProfileConfig()
|
||||||
config.items = config.items?.filter((item) => item.id !== id)
|
config.items = config.items?.filter((item) => item.id !== id)
|
||||||
let shouldRestart = false
|
let shouldRestart = false
|
||||||
@ -191,7 +191,7 @@ export async function createProfile(item: Partial<IProfileItem>): Promise<IProfi
|
|||||||
timeout: subscriptionTimeout
|
timeout: subscriptionTimeout
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// 检查状态码,例如:403
|
// 检查状态码,例如:403
|
||||||
if (res.status < 200 || res.status >= 300) {
|
if (res.status < 200 || res.status >= 300) {
|
||||||
throw new Error(`Subscription failed: Request status code ${res.status}`)
|
throw new Error(`Subscription failed: Request status code ${res.status}`)
|
||||||
@ -199,7 +199,7 @@ export async function createProfile(item: Partial<IProfileItem>): Promise<IProfi
|
|||||||
|
|
||||||
const data = res.data
|
const data = res.data
|
||||||
const headers = res.headers
|
const headers = res.headers
|
||||||
|
|
||||||
// 校验是否为对象结构 (拦截 HTML字符串、普通文本、乱码)
|
// 校验是否为对象结构 (拦截 HTML字符串、普通文本、乱码)
|
||||||
const parsed = parse(data)
|
const parsed = parse(data)
|
||||||
if (typeof parsed !== 'object' || parsed === null) {
|
if (typeof parsed !== 'object' || parsed === null) {
|
||||||
@ -210,7 +210,7 @@ export async function createProfile(item: Partial<IProfileItem>): Promise<IProfi
|
|||||||
if (!profile.proxies && !profile['proxy-providers']) {
|
if (!profile.proxies && !profile['proxy-providers']) {
|
||||||
throw new Error('Subscription failed: Profile missing proxies or providers')
|
throw new Error('Subscription failed: Profile missing proxies or providers')
|
||||||
}
|
}
|
||||||
|
|
||||||
if (headers['content-disposition'] && newItem.name === 'Remote File') {
|
if (headers['content-disposition'] && newItem.name === 'Remote File') {
|
||||||
newItem.name = parseFilename(headers['content-disposition'])
|
newItem.name = parseFilename(headers['content-disposition'])
|
||||||
}
|
}
|
||||||
@ -329,3 +329,43 @@ export async function setFileStr(path: string, content: string): Promise<void> {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function convertMrsRuleset(filePath: string, behavior: string): Promise<string> {
|
||||||
|
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 <behavior> <format> <source>
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@ -48,7 +48,8 @@ import {
|
|||||||
removeOverrideItem,
|
removeOverrideItem,
|
||||||
getOverride,
|
getOverride,
|
||||||
setOverride,
|
setOverride,
|
||||||
updateOverrideItem
|
updateOverrideItem,
|
||||||
|
convertMrsRuleset
|
||||||
} from '../config'
|
} from '../config'
|
||||||
import {
|
import {
|
||||||
startSubStoreFrontendServer,
|
startSubStoreFrontendServer,
|
||||||
@ -215,6 +216,7 @@ export function registerIpcMainHandlers(): void {
|
|||||||
ipcMain.handle('getProfileStr', (_e, id) => ipcErrorWrapper(getProfileStr)(id))
|
ipcMain.handle('getProfileStr', (_e, id) => ipcErrorWrapper(getProfileStr)(id))
|
||||||
ipcMain.handle('getFileStr', (_e, path) => ipcErrorWrapper(getFileStr)(path))
|
ipcMain.handle('getFileStr', (_e, path) => ipcErrorWrapper(getFileStr)(path))
|
||||||
ipcMain.handle('setFileStr', (_e, path, str) => ipcErrorWrapper(setFileStr)(path, str))
|
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('setProfileStr', (_e, id, str) => ipcErrorWrapper(setProfileStr)(id, str))
|
||||||
ipcMain.handle('updateProfileItem', (_e, item) => ipcErrorWrapper(updateProfileItem)(item))
|
ipcMain.handle('updateProfileItem', (_e, item) => ipcErrorWrapper(updateProfileItem)(item))
|
||||||
ipcMain.handle('changeCurrentProfile', (_e, id) => ipcErrorWrapper(changeCurrentProfile)(id))
|
ipcMain.handle('changeCurrentProfile', (_e, id) => ipcErrorWrapper(changeCurrentProfile)(id))
|
||||||
|
|||||||
@ -25,7 +25,8 @@ const RuleProvider: React.FC = () => {
|
|||||||
type: '',
|
type: '',
|
||||||
title: '',
|
title: '',
|
||||||
format: '',
|
format: '',
|
||||||
privderType: ''
|
privderType: '',
|
||||||
|
behavior: ''
|
||||||
})
|
})
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (showDetails.title) {
|
if (showDetails.title) {
|
||||||
@ -37,11 +38,12 @@ const RuleProvider: React.FC = () => {
|
|||||||
setShowDetails((prev) => ({
|
setShowDetails((prev) => ({
|
||||||
...prev,
|
...prev,
|
||||||
show: true,
|
show: true,
|
||||||
path: provider?.path || `rules/${getHash(provider?.url)}`
|
path: provider?.path || `rules/${getHash(provider?.url)}`,
|
||||||
|
behavior: provider?.behavior || 'domain'
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
setShowDetails((prev) => ({ ...prev, path: '' }))
|
setShowDetails((prev) => ({ ...prev, path: '', behavior: '' }))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
fetchProviderPath(showDetails.title)
|
fetchProviderPath(showDetails.title)
|
||||||
@ -94,7 +96,8 @@ const RuleProvider: React.FC = () => {
|
|||||||
title={showDetails.title}
|
title={showDetails.title}
|
||||||
format={showDetails.format}
|
format={showDetails.format}
|
||||||
privderType={showDetails.privderType}
|
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: '' })}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<SettingItem title={t('resources.ruleProviders.title')} divider>
|
<SettingItem title={t('resources.ruleProviders.title')} divider>
|
||||||
@ -122,30 +125,29 @@ const RuleProvider: React.FC = () => {
|
|||||||
>
|
>
|
||||||
<div className="flex h-[32px] leading-[32px] text-foreground-500">
|
<div className="flex h-[32px] leading-[32px] text-foreground-500">
|
||||||
<div>{dayjs(provider.updatedAt).fromNow()}</div>
|
<div>{dayjs(provider.updatedAt).fromNow()}</div>
|
||||||
{provider.format !== 'MrsRule' && (
|
<Button
|
||||||
<Button
|
isIconOnly
|
||||||
isIconOnly
|
title={provider.vehicleType == 'File' ? t('common.editor.edit') : t('common.viewer.view')}
|
||||||
title={provider.vehicleType == 'File' ? t('common.editor.edit') : t('common.viewer.view')}
|
className="ml-2"
|
||||||
className="ml-2"
|
size="sm"
|
||||||
size="sm"
|
onPress={() => {
|
||||||
onPress={() => {
|
setShowDetails({
|
||||||
setShowDetails({
|
show: false,
|
||||||
show: false,
|
privderType: 'rule-providers',
|
||||||
privderType: 'rule-providers',
|
path: provider.name,
|
||||||
path: provider.name,
|
type: provider.vehicleType,
|
||||||
type: provider.vehicleType,
|
title: provider.name,
|
||||||
title: provider.name,
|
format: provider.format,
|
||||||
format: provider.format
|
behavior: provider.behavior || 'domain'
|
||||||
})
|
})
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{provider.vehicleType == 'File' ? (
|
{provider.vehicleType == 'File' ? (
|
||||||
<MdEditDocument className={`text-lg`} />
|
<MdEditDocument className={`text-lg`} />
|
||||||
) : (
|
) : (
|
||||||
<CgLoadbarDoc className={`text-lg`} />
|
<CgLoadbarDoc className={`text-lg`} />
|
||||||
)}
|
)}
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
|
||||||
<Button
|
<Button
|
||||||
isIconOnly
|
isIconOnly
|
||||||
title={t('common.updater.update')}
|
title={t('common.updater.update')}
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import { Modal, ModalContent, ModalHeader, ModalBody, ModalFooter, Button } from '@heroui/react'
|
import { Modal, ModalContent, ModalHeader, ModalBody, ModalFooter, Button } from '@heroui/react'
|
||||||
import React, { useEffect, useState } from 'react'
|
import React, { useEffect, useState } from 'react'
|
||||||
import { BaseEditor } from '../base/base-editor'
|
import { BaseEditor } from '../base/base-editor'
|
||||||
import { getFileStr, setFileStr } from '@renderer/utils/ipc'
|
import { getFileStr, setFileStr, convertMrsRuleset, getRuntimeConfig } from '@renderer/utils/ipc'
|
||||||
import yaml from 'js-yaml'
|
import yaml from 'js-yaml'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
type Language = 'yaml' | 'javascript' | 'css' | 'json' | 'text'
|
type Language = 'yaml' | 'javascript' | 'css' | 'json' | 'text'
|
||||||
@ -13,34 +13,60 @@ interface Props {
|
|||||||
title: string
|
title: string
|
||||||
privderType: string
|
privderType: string
|
||||||
format?: string
|
format?: string
|
||||||
|
behavior?: string
|
||||||
}
|
}
|
||||||
const Viewer: React.FC<Props> = (props) => {
|
const Viewer: React.FC<Props> = (props) => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const { type, path, title, format, privderType, onClose } = props
|
const { type, path, title, format, privderType, behavior, onClose } = props
|
||||||
const [currData, setCurrData] = useState('')
|
const [currData, setCurrData] = useState('')
|
||||||
|
const [isLoading, setIsLoading] = useState(true)
|
||||||
let language: Language = !format || format === 'YamlRule' ? 'yaml' : 'text'
|
let language: Language = !format || format === 'YamlRule' ? 'yaml' : 'text'
|
||||||
|
|
||||||
const getContent = async (): Promise<void> => {
|
const getContent = async (): Promise<void> => {
|
||||||
let fileContent: React.SetStateAction<string>
|
setIsLoading(true)
|
||||||
if (type === 'Inline') {
|
|
||||||
fileContent = await getFileStr('config.yaml')
|
|
||||||
language = 'yaml'
|
|
||||||
} else {
|
|
||||||
fileContent = await getFileStr(path)
|
|
||||||
}
|
|
||||||
try {
|
try {
|
||||||
const parsedYaml = yaml.load(fileContent)
|
let fileContent: React.SetStateAction<string>
|
||||||
if (privderType === 'proxy-providers') {
|
|
||||||
setCurrData(yaml.dump({
|
if (format === 'MrsRule') {
|
||||||
'proxies': parsedYaml[privderType][title].payload
|
language = 'text'
|
||||||
}))
|
let ruleBehavior: string = behavior || 'domain'
|
||||||
} else {
|
if (!behavior) {
|
||||||
setCurrData(yaml.dump({
|
try {
|
||||||
'rules': parsedYaml[privderType][title].payload
|
const runtimeConfig = await getRuntimeConfig()
|
||||||
}))
|
const provider = runtimeConfig['rule-providers']?.[title]
|
||||||
|
ruleBehavior = provider?.behavior || 'domain'
|
||||||
|
} catch {
|
||||||
|
ruleBehavior = 'domain'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fileContent = await convertMrsRuleset(path, ruleBehavior)
|
||||||
|
setCurrData(fileContent)
|
||||||
|
return
|
||||||
}
|
}
|
||||||
} catch (error) {
|
|
||||||
setCurrData(fileContent)
|
if (type === 'Inline') {
|
||||||
|
fileContent = await getFileStr('config.yaml')
|
||||||
|
language = 'yaml'
|
||||||
|
} else {
|
||||||
|
fileContent = await getFileStr(path)
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const parsedYaml = yaml.load(fileContent)
|
||||||
|
if (privderType === 'proxy-providers') {
|
||||||
|
setCurrData(yaml.dump({
|
||||||
|
'proxies': parsedYaml[privderType][title].payload
|
||||||
|
}))
|
||||||
|
} else {
|
||||||
|
setCurrData(yaml.dump({
|
||||||
|
'rules': parsedYaml[privderType][title].payload
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
setCurrData(fileContent)
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -61,18 +87,24 @@ const Viewer: React.FC<Props> = (props) => {
|
|||||||
<ModalContent className="h-full w-[calc(100%-100px)]">
|
<ModalContent className="h-full w-[calc(100%-100px)]">
|
||||||
<ModalHeader className="flex pb-0 app-drag">{title}</ModalHeader>
|
<ModalHeader className="flex pb-0 app-drag">{title}</ModalHeader>
|
||||||
<ModalBody className="h-full">
|
<ModalBody className="h-full">
|
||||||
<BaseEditor
|
{isLoading ? (
|
||||||
language={language}
|
<div className="flex items-center justify-center h-full">
|
||||||
value={currData}
|
<div className="text-foreground-500">{t('common.loading')}</div>
|
||||||
readOnly={type != 'File'}
|
</div>
|
||||||
onChange={(value) => setCurrData(value)}
|
) : (
|
||||||
/>
|
<BaseEditor
|
||||||
|
language={language}
|
||||||
|
value={currData}
|
||||||
|
readOnly={type != 'File' || format === 'MrsRule'}
|
||||||
|
onChange={(value) => setCurrData(value)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</ModalBody>
|
</ModalBody>
|
||||||
<ModalFooter className="pt-0">
|
<ModalFooter className="pt-0">
|
||||||
<Button size="sm" variant="light" onPress={onClose}>
|
<Button size="sm" variant="light" onPress={onClose}>
|
||||||
{t('common.close')}
|
{t('common.close')}
|
||||||
</Button>
|
</Button>
|
||||||
{type == 'File' && (
|
{type == 'File' && format !== 'MrsRule' && (
|
||||||
<Button
|
<Button
|
||||||
size="sm"
|
size="sm"
|
||||||
color="primary"
|
color="primary"
|
||||||
|
|||||||
@ -16,6 +16,7 @@
|
|||||||
"common.auto": "Auto",
|
"common.auto": "Auto",
|
||||||
"common.default": "Default",
|
"common.default": "Default",
|
||||||
"common.close": "Close",
|
"common.close": "Close",
|
||||||
|
"common.loading": "Loading...",
|
||||||
"common.pinWindow": "Pin Window",
|
"common.pinWindow": "Pin Window",
|
||||||
"common.next": "Next",
|
"common.next": "Next",
|
||||||
"common.prev": "Previous",
|
"common.prev": "Previous",
|
||||||
|
|||||||
@ -16,6 +16,7 @@
|
|||||||
"common.auto": "خودکار",
|
"common.auto": "خودکار",
|
||||||
"common.default": "پیشفرض",
|
"common.default": "پیشفرض",
|
||||||
"common.close": "بستن",
|
"common.close": "بستن",
|
||||||
|
"common.loading": "در حال بارگذاری...",
|
||||||
"common.pinWindow": "پین کردن پنجره",
|
"common.pinWindow": "پین کردن پنجره",
|
||||||
"common.next": "بعدی",
|
"common.next": "بعدی",
|
||||||
"common.prev": "قبلی",
|
"common.prev": "قبلی",
|
||||||
|
|||||||
@ -16,6 +16,7 @@
|
|||||||
"common.auto": "Авто",
|
"common.auto": "Авто",
|
||||||
"common.default": "По умолчанию",
|
"common.default": "По умолчанию",
|
||||||
"common.close": "Закрыть",
|
"common.close": "Закрыть",
|
||||||
|
"common.loading": "Загрузка...",
|
||||||
"common.pinWindow": "Закрепить окно",
|
"common.pinWindow": "Закрепить окно",
|
||||||
"common.next": "Далее",
|
"common.next": "Далее",
|
||||||
"common.prev": "Назад",
|
"common.prev": "Назад",
|
||||||
|
|||||||
@ -16,6 +16,7 @@
|
|||||||
"common.auto": "自动",
|
"common.auto": "自动",
|
||||||
"common.default": "默认",
|
"common.default": "默认",
|
||||||
"common.close": "关闭",
|
"common.close": "关闭",
|
||||||
|
"common.loading": "加载中...",
|
||||||
"common.pinWindow": "窗口置顶",
|
"common.pinWindow": "窗口置顶",
|
||||||
"common.next": "下一步",
|
"common.next": "下一步",
|
||||||
"common.prev": "上一步",
|
"common.prev": "上一步",
|
||||||
|
|||||||
@ -208,6 +208,10 @@ export async function setProfileStr(id: string, str: string): Promise<void> {
|
|||||||
return ipcErrorWrapper(await window.electron.ipcRenderer.invoke('setProfileStr', id, str))
|
return ipcErrorWrapper(await window.electron.ipcRenderer.invoke('setProfileStr', id, str))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function convertMrsRuleset(path: string, behavior: string): Promise<string> {
|
||||||
|
return ipcErrorWrapper(await window.electron.ipcRenderer.invoke('convertMrsRuleset', path, behavior))
|
||||||
|
}
|
||||||
|
|
||||||
export async function getOverrideConfig(force = false): Promise<IOverrideConfig> {
|
export async function getOverrideConfig(force = false): Promise<IOverrideConfig> {
|
||||||
return ipcErrorWrapper(await window.electron.ipcRenderer.invoke('getOverrideConfig', force))
|
return ipcErrorWrapper(await window.electron.ipcRenderer.invoke('getOverrideConfig', force))
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user