Compare commits

...

2 Commits

Author SHA1 Message Date
Memory
dae4939390
feat: add Fish and Nushell support for environment variables 2026-02-03 21:16:42 +08:00
Memory
d2f700a0ef
feat: add rule statistics and disable toggle 2026-02-03 20:25:57 +08:00
14 changed files with 156 additions and 22 deletions

View File

@ -85,6 +85,11 @@ export const mihomoRules = async (): Promise<IMihomoRulesInfo> => {
return await instance.get('/rules')
}
export const mihomoRulesDisable = async (rules: Record<string, boolean>): Promise<void> => {
const instance = await getAxios()
return await instance.patch('/rules/disable', rules)
}
export const mihomoProxies = async (): Promise<IMihomoProxies> => {
const instance = await getAxios()
const proxies = (await instance.get('/proxies')) as IMihomoProxies

View File

@ -460,26 +460,42 @@ async function updateTrayMenu(): Promise<void> {
}
}
export async function copyEnv(type: 'bash' | 'cmd' | 'powershell'): Promise<void> {
export async function copyEnv(
type: 'bash' | 'cmd' | 'powershell' | 'fish' | 'nushell'
): Promise<void> {
const { 'mixed-port': mixedPort = 7890 } = await getControledMihomoConfig()
const { sysProxy } = await getAppConfig()
const { host } = sysProxy
const proxyUrl = `http://${host || '127.0.0.1'}:${mixedPort}`
switch (type) {
case 'bash': {
clipboard.writeText(
`export https_proxy=http://${host || '127.0.0.1'}:${mixedPort} http_proxy=http://${host || '127.0.0.1'}:${mixedPort} all_proxy=http://${host || '127.0.0.1'}:${mixedPort}`
`export https_proxy=${proxyUrl} http_proxy=${proxyUrl} all_proxy=${proxyUrl}`
)
break
}
case 'cmd': {
clipboard.writeText(
`set http_proxy=http://${host || '127.0.0.1'}:${mixedPort}\r\nset https_proxy=http://${host || '127.0.0.1'}:${mixedPort}`
`set http_proxy=${proxyUrl}\r\nset https_proxy=${proxyUrl}`
)
break
}
case 'powershell': {
clipboard.writeText(
`$env:HTTP_PROXY="http://${host || '127.0.0.1'}:${mixedPort}"; $env:HTTPS_PROXY="http://${host || '127.0.0.1'}:${mixedPort}"`
`$env:HTTP_PROXY="${proxyUrl}"; $env:HTTPS_PROXY="${proxyUrl}"`
)
break
}
case 'fish': {
clipboard.writeText(
`set -x http_proxy ${proxyUrl}; set -x https_proxy ${proxyUrl}; set -x all_proxy ${proxyUrl}`
)
break
}
case 'nushell': {
clipboard.writeText(
`$env.HTTP_PROXY = "${proxyUrl}"; $env.HTTPS_PROXY = "${proxyUrl}"; $env.ALL_PROXY = "${proxyUrl}"`
)
break
}

View File

@ -24,7 +24,8 @@ import {
mihomoVersion,
patchMihomoConfig,
mihomoSmartGroupWeights,
mihomoSmartFlushCache
mihomoSmartFlushCache,
mihomoRulesDisable
} from '../core/mihomoApi'
import { checkAutoRun, disableAutoRun, enableAutoRun } from '../sys/autoRun'
import {
@ -206,6 +207,7 @@ const asyncHandlers: Record<string, AsyncFn> = {
mihomoCloseConnection,
mihomoCloseAllConnections,
mihomoRules,
mihomoRulesDisable,
mihomoProxies,
mihomoGroups,
mihomoProxyProviders,

View File

@ -7,6 +7,7 @@ const validInvokeChannels = [
'mihomoCloseConnection',
'mihomoCloseAllConnections',
'mihomoRules',
'mihomoRulesDisable',
'mihomoProxies',
'mihomoGroups',
'mihomoProxyProviders',

View File

@ -1,18 +1,95 @@
import { Card, CardBody } from '@heroui/react'
import React from 'react'
import { Card, CardBody, Chip, Switch } from '@heroui/react'
import React, { useState, useEffect } from 'react'
import { mihomoRulesDisable } from '@renderer/utils/ipc'
import { useTranslation } from 'react-i18next'
interface RuleItemProps extends IMihomoRulesDetail {
index: number
}
const RuleItem: React.FC<RuleItemProps> = (props) => {
const { t } = useTranslation()
const { type, payload, proxy, index: listIndex, extra } = props
const ruleIndex = props.index ?? listIndex
const { disabled, hitCount, hitAt, missCount, missAt } = extra
const [isEnabled, setIsEnabled] = useState(!disabled)
useEffect(() => {
setIsEnabled(!disabled)
}, [disabled])
const handleToggle = async (v: boolean): Promise<void> => {
setIsEnabled(v)
try {
await mihomoRulesDisable({ [ruleIndex]: !v })
} catch (error) {
console.error('Failed to toggle rule:', error)
setIsEnabled(!v)
}
}
const totalCount = hitCount + missCount
const hitRate = totalCount > 0 ? (hitCount / totalCount) * 100 : 0
const formatRelativeTime = (timestamp: string): string => {
const now = Date.now()
const time = new Date(timestamp).getTime()
const diff = Math.floor((now - time) / 1000)
if (diff < 60) return t('rules.hitAt.seconds')
if (diff < 3600) return t('rules.hitAt.minutes', { count: Math.floor(diff / 60) })
if (diff < 86400) return t('rules.hitAt.hours', { count: Math.floor(diff / 3600) })
return t('rules.hitAt.days', { count: Math.floor(diff / 86400) })
}
const hasStats = totalCount > 0
const RuleItem: React.FC<IMihomoRulesDetail & { index: number }> = (props) => {
const { type, payload, proxy, index } = props
return (
<div className={`w-full px-2 pb-2 ${index === 0 ? 'pt-2' : ''}`}>
<Card>
<CardBody className="w-full">
<div title={payload} className="text-ellipsis whitespace-nowrap overflow-hidden">
{payload}
</div>
<div className="flex justify-start text-foreground-500">
<div>{type}</div>
<div className="ml-2">{proxy}</div>
<div className={`w-full px-2 pb-2 ${listIndex === 0 ? 'pt-2' : ''}`}>
<Card className={!isEnabled ? 'opacity-50' : ''}>
<CardBody className="py-3 px-4">
<div className="flex justify-between items-center gap-4">
{/* 左侧:规则信息 */}
<div className="flex-1 min-w-0 flex items-center gap-3">
{/* 规则内容 */}
<div className="flex-1 min-w-0">
<div title={payload} className="text-sm font-medium truncate mb-1.5">
{payload}
</div>
<div className="flex items-center gap-2">
<Chip size="sm" variant="flat" color="default" className="text-xs">
{type}
</Chip>
<Chip size="sm" variant="flat" color="default" className="text-xs">
{proxy}
</Chip>
</div>
</div>
{/* 统计信息 */}
{hasStats && (
<div className="flex items-center gap-3 text-xs shrink-0">
<span className="text-foreground-500 whitespace-nowrap">
{formatRelativeTime(hitAt || missAt)}
</span>
<span className="text-foreground-600 font-medium whitespace-nowrap">
{hitCount}/{totalCount}
</span>
<Chip size="sm" variant="flat" color="primary" className="text-xs">
{hitRate.toFixed(1)}%
</Chip>
</div>
)}
</div>
{/* 右侧开关 */}
<Switch
size="sm"
isSelected={isEnabled}
onValueChange={handleToggle}
aria-label="Toggle rule"
/>
</div>
</CardBody>
</Card>

View File

@ -249,7 +249,7 @@ const GeneralConfig: React.FC = () => {
onSelectionChange={async (v) => {
try {
await patchAppConfig({
envType: Array.from(v) as ('bash' | 'cmd' | 'powershell')[]
envType: Array.from(v) as ('bash' | 'cmd' | 'powershell' | 'fish' | 'nushell')[]
})
} catch (e) {
toast.error(String(e))
@ -259,6 +259,8 @@ const GeneralConfig: React.FC = () => {
<SelectItem key="bash">Bash</SelectItem>
<SelectItem key="cmd">CMD</SelectItem>
<SelectItem key="powershell">PowerShell</SelectItem>
<SelectItem key="fish">Fish</SelectItem>
<SelectItem key="nushell">Nushell</SelectItem>
</Select>
</SettingItem>
<SettingItem title={t('settings.showFloatingWindow')} divider>

View File

@ -529,6 +529,10 @@
"outbound.modes.direct": "Direct",
"rules.title": "Rules",
"rules.filter": "Filter Rules",
"rules.hitAt.seconds": "Hit moments ago",
"rules.hitAt.minutes": "Hit {{count}} min ago",
"rules.hitAt.hours": "Hit {{count}} hrs ago",
"rules.hitAt.days": "Hit {{count}} days ago",
"override.title": "Override",
"override.input.placeholder": "Enter override URL",
"override.import": "Import",

View File

@ -498,6 +498,10 @@
"outbound.modes.direct": "مستقیم",
"rules.title": "قوانین",
"rules.filter": "فیلتر قوانین",
"rules.hitAt.seconds": "چند لحظه پیش",
"rules.hitAt.minutes": "{{count}} دقیقه پیش",
"rules.hitAt.hours": "{{count}} ساعت پیش",
"rules.hitAt.days": "{{count}} روز پیش",
"override.title": "جایگزینی",
"override.input.placeholder": "وارد کردن URL جایگزین",
"override.import": "وارد کردن",

View File

@ -500,6 +500,10 @@
"outbound.modes.direct": "Прямой",
"rules.title": "Правила",
"rules.filter": "Фильтр правил",
"rules.hitAt.seconds": "Совпадение несколько секунд назад",
"rules.hitAt.minutes": "Совпадение {{count}} мин назад",
"rules.hitAt.hours": "Совпадение {{count}} ч назад",
"rules.hitAt.days": "Совпадение {{count}} дн назад",
"override.title": "Переопределение",
"override.input.placeholder": "Введите URL переопределения",
"override.import": "Импорт",

View File

@ -529,6 +529,10 @@
"outbound.modes.direct": "直连",
"rules.title": "分流规则",
"rules.filter": "筛选过滤",
"rules.hitAt.seconds": "最近命中于 几秒前",
"rules.hitAt.minutes": "最近命中于 {{count}} 分钟前",
"rules.hitAt.hours": "最近命中于 {{count}} 小时前",
"rules.hitAt.days": "最近命中于 {{count}} 天前",
"override.title": "覆写",
"override.input.placeholder": "输入覆写 URL",
"override.import": "导入",

View File

@ -529,6 +529,10 @@
"outbound.modes.direct": "直連",
"rules.title": "分流規則",
"rules.filter": "篩選過濾",
"rules.hitAt.seconds": "最近命中於 幾秒前",
"rules.hitAt.minutes": "最近命中於 {{count}} 分鐘前",
"rules.hitAt.hours": "最近命中於 {{count}} 小時前",
"rules.hitAt.days": "最近命中於 {{count}} 天前",
"override.title": "覆寫",
"override.input.placeholder": "輸入覆寫 URL",
"override.import": "匯入",

View File

@ -43,11 +43,12 @@ const Rules: React.FC = () => {
data={filteredRules}
itemContent={(i, rule) => (
<RuleItem
index={i}
index={rule.index ?? i}
type={rule.type}
payload={rule.payload}
proxy={rule.proxy}
size={rule.size}
extra={rule.extra}
/>
)}
/>

View File

@ -19,6 +19,7 @@ interface IpcApi {
mihomoCloseConnection: (id: string) => Promise<void>
mihomoCloseAllConnections: () => Promise<void>
mihomoRules: () => Promise<IMihomoRulesInfo>
mihomoRulesDisable: (rules: Record<string, boolean>) => Promise<void>
mihomoProxies: () => Promise<IMihomoProxies>
mihomoGroups: () => Promise<IMihomoMixedGroup[]>
mihomoProxyProviders: () => Promise<IMihomoProxyProviders>
@ -99,7 +100,7 @@ interface IpcApi {
setupFirewall: () => Promise<void>
getInterfaces: () => Promise<Record<string, NetworkInterfaceInfo[]>>
setNativeTheme: (theme: 'system' | 'light' | 'dark') => Promise<void>
copyEnv: (type: 'bash' | 'cmd' | 'powershell') => Promise<void>
copyEnv: (type: 'bash' | 'cmd' | 'powershell' | 'fish' | 'nushell') => Promise<void>
// Update
checkUpdate: () => Promise<IAppVersion | undefined>
downloadAndInstallUpdate: (version: string) => Promise<void>
@ -173,6 +174,7 @@ export const {
mihomoCloseConnection,
mihomoCloseAllConnections,
mihomoRules,
mihomoRulesDisable,
mihomoProxies,
mihomoGroups,
mihomoProxyProviders,

10
src/shared/types.d.ts vendored
View File

@ -72,6 +72,14 @@ interface IMihomoRulesDetail {
payload: string
proxy: string
size: number
index: number
extra: {
disabled: boolean
hitCount: number
hitAt: string
missCount: number
missAt: string
}
}
interface IMihomoConnectionsInfo {
@ -227,7 +235,7 @@ interface IAppConfig {
proxyDisplayMode: 'simple' | 'full'
proxyDisplayOrder: 'default' | 'delay' | 'name'
profileDisplayDate?: 'expire' | 'update'
envType?: ('bash' | 'cmd' | 'powershell')[]
envType?: ('bash' | 'cmd' | 'powershell' | 'fish' | 'nushell')[]
proxyCols: 'auto' | '1' | '2' | '3' | '4'
hideUnavailableProxies?: boolean
connectionDirection: 'asc' | 'desc'