mirror of
https://gh.catmak.name/https://github.com/mihomo-party-org/mihomo-party
synced 2025-12-27 21:20:29 +08:00
Compare commits
No commits in common. "4137f91ccb16ad98b7e5cc9f80255a4e93fb06ec" and "199ecd26ddb3f9d30248eb6d286b268b57fbdcb3" have entirely different histories.
4137f91ccb
...
199ecd26dd
BIN
build/icon.icns
BIN
build/icon.icns
Binary file not shown.
@ -13,8 +13,6 @@
|
|||||||
- 改名后潜在的 MacOS 安装失败
|
- 改名后潜在的 MacOS 安装失败
|
||||||
- 改名后 WinGet 上传失败
|
- 改名后 WinGet 上传失败
|
||||||
- MacOS 首次启动时的 ENOENT: no such file or directory
|
- MacOS 首次启动时的 ENOENT: no such file or directory
|
||||||
- 修复 Gist url 404 error
|
|
||||||
- MacOS 下状态栏图标 Logo
|
|
||||||
|
|
||||||
### 优化 (Optimize)
|
### 优化 (Optimize)
|
||||||
- socket 管理防止内核通信失败
|
- socket 管理防止内核通信失败
|
||||||
|
|||||||
@ -13,8 +13,6 @@ import { join } from 'path'
|
|||||||
import { app } from 'electron'
|
import { app } from 'electron'
|
||||||
|
|
||||||
let profileConfig: IProfileConfig // profile.yaml
|
let profileConfig: IProfileConfig // profile.yaml
|
||||||
// 最终选中订阅ID
|
|
||||||
let targetProfileId: string | null = null
|
|
||||||
|
|
||||||
export async function getProfileConfig(force = false): Promise<IProfileConfig> {
|
export async function getProfileConfig(force = false): Promise<IProfileConfig> {
|
||||||
if (force || !profileConfig) {
|
if (force || !profileConfig) {
|
||||||
@ -40,35 +38,22 @@ export async function changeCurrentProfile(id: string): Promise<void> {
|
|||||||
const config = await getProfileConfig()
|
const config = await getProfileConfig()
|
||||||
const current = config.current
|
const current = config.current
|
||||||
|
|
||||||
if (current === id && targetProfileId !== id) {
|
if (current === id) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
targetProfileId = id
|
|
||||||
|
|
||||||
config.current = id
|
config.current = id
|
||||||
const configSavePromise = setProfileConfig(config)
|
await setProfileConfig(config)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await configSavePromise
|
|
||||||
|
|
||||||
// 检查订阅切换是否中断
|
|
||||||
if (targetProfileId !== id) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
await restartCore()
|
await restartCore()
|
||||||
if (targetProfileId === id) {
|
|
||||||
targetProfileId = null
|
|
||||||
}
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (targetProfileId === id) {
|
// 如果重启失败,恢复原来的配置
|
||||||
config.current = current
|
config.current = current
|
||||||
await setProfileConfig(config)
|
await setProfileConfig(config)
|
||||||
targetProfileId = null
|
|
||||||
throw e
|
throw e
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
export async function updateProfileItem(item: IProfileItem): Promise<void> {
|
export async function updateProfileItem(item: IProfileItem): Promise<void> {
|
||||||
const config = await getProfileConfig()
|
const config = await getProfileConfig()
|
||||||
@ -218,8 +203,7 @@ export async function getProfileStr(id: string | undefined): Promise<string> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function setProfileStr(id: string, content: string): Promise<void> {
|
export async function setProfileStr(id: string, content: string): Promise<void> {
|
||||||
// 读取最新的配置
|
const { current } = await getProfileConfig()
|
||||||
const { current } = await getProfileConfig(true)
|
|
||||||
await writeFile(profilePath(id), content, 'utf-8')
|
await writeFile(profilePath(id), content, 'utf-8')
|
||||||
if (current === id) await restartCore()
|
if (current === id) await restartCore()
|
||||||
}
|
}
|
||||||
|
|||||||
@ -25,8 +25,7 @@ let runtimeConfigStr: string
|
|||||||
let runtimeConfig: IMihomoConfig
|
let runtimeConfig: IMihomoConfig
|
||||||
|
|
||||||
export async function generateProfile(): Promise<void> {
|
export async function generateProfile(): Promise<void> {
|
||||||
// 读取最新的配置
|
const { current } = await getProfileConfig()
|
||||||
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))
|
const currentProfile = await overrideProfile(current, await getProfile(current))
|
||||||
let controledMihomoConfig = await getControledMihomoConfig()
|
let controledMihomoConfig = await getControledMihomoConfig()
|
||||||
|
|||||||
@ -79,7 +79,6 @@ let setPublicDNSTimer: NodeJS.Timeout | null = null
|
|||||||
let recoverDNSTimer: NodeJS.Timeout | null = null
|
let recoverDNSTimer: NodeJS.Timeout | null = null
|
||||||
let child: ChildProcess
|
let child: ChildProcess
|
||||||
let retry = 10
|
let retry = 10
|
||||||
let isRestarting = false
|
|
||||||
|
|
||||||
export async function startCore(detached = false): Promise<Promise<void>[]> {
|
export async function startCore(detached = false): Promise<Promise<void>[]> {
|
||||||
const {
|
const {
|
||||||
@ -103,7 +102,7 @@ export async function startCore(detached = false): Promise<Promise<void>[]> {
|
|||||||
await rm(path.join(dataDir(), 'core.pid'))
|
await rm(path.join(dataDir(), 'core.pid'))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const { current } = await getProfileConfig(true)
|
const { current } = await getProfileConfig()
|
||||||
const { tun } = await getControledMihomoConfig()
|
const { tun } = await getControledMihomoConfig()
|
||||||
const corePath = mihomoCorePath(core)
|
const corePath = mihomoCorePath(core)
|
||||||
|
|
||||||
@ -162,12 +161,6 @@ export async function startCore(detached = false): Promise<Promise<void>[]> {
|
|||||||
}
|
}
|
||||||
child.on('close', async (code, signal) => {
|
child.on('close', async (code, signal) => {
|
||||||
await managerLogger.info(`Core closed, code: ${code}, signal: ${signal}`)
|
await managerLogger.info(`Core closed, code: ${code}, signal: ${signal}`)
|
||||||
|
|
||||||
if (isRestarting) {
|
|
||||||
await managerLogger.info('Core closed during restart, skipping auto-restart')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (retry) {
|
if (retry) {
|
||||||
await managerLogger.info('Try Restart Core')
|
await managerLogger.info('Try Restart Core')
|
||||||
retry--
|
retry--
|
||||||
@ -300,17 +293,13 @@ async function cleanupWindowsNamedPipes(): Promise<void> {
|
|||||||
const pid = proc.Id
|
const pid = proc.Id
|
||||||
if (pid && pid !== process.pid) {
|
if (pid && pid !== process.pid) {
|
||||||
try {
|
try {
|
||||||
// 先检查进程是否存在
|
|
||||||
process.kill(pid, 0)
|
|
||||||
process.kill(pid, 'SIGTERM')
|
process.kill(pid, 'SIGTERM')
|
||||||
await managerLogger.info(`Terminated process ${pid} to free pipe`)
|
await managerLogger.info(`Terminated process ${pid} to free pipe`)
|
||||||
} catch (error: any) {
|
} catch (error) {
|
||||||
if (error.code !== 'ESRCH') {
|
|
||||||
await managerLogger.warn(`Failed to terminate process ${pid}:`, error)
|
await managerLogger.warn(`Failed to terminate process ${pid}:`, error)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
} catch (parseError) {
|
} catch (parseError) {
|
||||||
await managerLogger.warn('Failed to parse process list JSON:', parseError)
|
await managerLogger.warn('Failed to parse process list JSON:', parseError)
|
||||||
|
|
||||||
@ -322,11 +311,9 @@ async function cleanupWindowsNamedPipes(): Promise<void> {
|
|||||||
const pid = parseInt(match[1])
|
const pid = parseInt(match[1])
|
||||||
if (pid !== process.pid) {
|
if (pid !== process.pid) {
|
||||||
try {
|
try {
|
||||||
process.kill(pid, 0)
|
|
||||||
process.kill(pid, 'SIGTERM')
|
process.kill(pid, 'SIGTERM')
|
||||||
await managerLogger.info(`Terminated process ${pid} to free pipe`)
|
await managerLogger.info(`Terminated process ${pid} to free pipe`)
|
||||||
} catch (error: any) {
|
} catch (error) {
|
||||||
if (error.code !== 'ESRCH') {
|
|
||||||
await managerLogger.warn(`Failed to terminate process ${pid}:`, error)
|
await managerLogger.warn(`Failed to terminate process ${pid}:`, error)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -334,7 +321,6 @@ async function cleanupWindowsNamedPipes(): Promise<void> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
await managerLogger.warn('Failed to check mihomo processes:', error)
|
await managerLogger.warn('Failed to check mihomo processes:', error)
|
||||||
}
|
}
|
||||||
@ -381,20 +367,13 @@ async function validateWindowsPipeAccess(pipePath: string): Promise<void> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function restartCore(): Promise<void> {
|
export async function restartCore(): Promise<void> {
|
||||||
// 防止并发重启
|
|
||||||
if (isRestarting) {
|
|
||||||
await managerLogger.info('Core restart already in progress, skipping duplicate request')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
isRestarting = true
|
|
||||||
try {
|
try {
|
||||||
await startCore()
|
await startCore()
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
// 记录错误到日志而不是显示阻塞对话框
|
||||||
await managerLogger.error('restart core failed', e)
|
await managerLogger.error('restart core failed', e)
|
||||||
|
// 重新抛出错误,让调用者处理
|
||||||
throw e
|
throw e
|
||||||
} finally {
|
|
||||||
isRestarting = false
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -205,8 +205,6 @@ export const startMihomoTraffic = async (): Promise<void> => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const stopMihomoTraffic = (): void => {
|
export const stopMihomoTraffic = (): void => {
|
||||||
trafficRetry = 0
|
|
||||||
|
|
||||||
if (mihomoTrafficWs) {
|
if (mihomoTrafficWs) {
|
||||||
mihomoTrafficWs.removeAllListeners()
|
mihomoTrafficWs.removeAllListeners()
|
||||||
if (mihomoTrafficWs.readyState === WebSocket.OPEN) {
|
if (mihomoTrafficWs.readyState === WebSocket.OPEN) {
|
||||||
@ -264,8 +262,6 @@ export const startMihomoMemory = async (): Promise<void> => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const stopMihomoMemory = (): void => {
|
export const stopMihomoMemory = (): void => {
|
||||||
memoryRetry = 0
|
|
||||||
|
|
||||||
if (mihomoMemoryWs) {
|
if (mihomoMemoryWs) {
|
||||||
mihomoMemoryWs.removeAllListeners()
|
mihomoMemoryWs.removeAllListeners()
|
||||||
if (mihomoMemoryWs.readyState === WebSocket.OPEN) {
|
if (mihomoMemoryWs.readyState === WebSocket.OPEN) {
|
||||||
@ -310,8 +306,6 @@ export const startMihomoLogs = async (): Promise<void> => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const stopMihomoLogs = (): void => {
|
export const stopMihomoLogs = (): void => {
|
||||||
logsRetry = 0
|
|
||||||
|
|
||||||
if (mihomoLogsWs) {
|
if (mihomoLogsWs) {
|
||||||
mihomoLogsWs.removeAllListeners()
|
mihomoLogsWs.removeAllListeners()
|
||||||
if (mihomoLogsWs.readyState === WebSocket.OPEN) {
|
if (mihomoLogsWs.readyState === WebSocket.OPEN) {
|
||||||
@ -358,8 +352,6 @@ export const startMihomoConnections = async (): Promise<void> => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const stopMihomoConnections = (): void => {
|
export const stopMihomoConnections = (): void => {
|
||||||
connectionsRetry = 0
|
|
||||||
|
|
||||||
if (mihomoConnectionsWs) {
|
if (mihomoConnectionsWs) {
|
||||||
mihomoConnectionsWs.removeAllListeners()
|
mihomoConnectionsWs.removeAllListeners()
|
||||||
if (mihomoConnectionsWs.readyState === WebSocket.OPEN) {
|
if (mihomoConnectionsWs.readyState === WebSocket.OPEN) {
|
||||||
|
|||||||
@ -57,6 +57,7 @@ const ProfileItem: React.FC<Props> = (props) => {
|
|||||||
const { appConfig, patchAppConfig } = useAppConfig()
|
const { appConfig, patchAppConfig } = useAppConfig()
|
||||||
const { profileDisplayDate = 'expire' } = appConfig || {}
|
const { profileDisplayDate = 'expire' } = appConfig || {}
|
||||||
const [updating, setUpdating] = useState(false)
|
const [updating, setUpdating] = useState(false)
|
||||||
|
const [selecting, setSelecting] = useState(false)
|
||||||
const [openInfoEditor, setOpenInfoEditor] = useState(false)
|
const [openInfoEditor, setOpenInfoEditor] = useState(false)
|
||||||
const [openFileEditor, setOpenFileEditor] = useState(false)
|
const [openFileEditor, setOpenFileEditor] = useState(false)
|
||||||
const [dropdownOpen, setDropdownOpen] = useState(false)
|
const [dropdownOpen, setDropdownOpen] = useState(false)
|
||||||
@ -184,7 +185,8 @@ const ProfileItem: React.FC<Props> = (props) => {
|
|||||||
|
|
||||||
// 处理卡片选中
|
// 处理卡片选中
|
||||||
if (!isActuallyDragging && !isDragging && clickStartPos) {
|
if (!isActuallyDragging && !isDragging && clickStartPos) {
|
||||||
onPress()
|
setSelecting(true)
|
||||||
|
onPress().finally(() => setSelecting(false))
|
||||||
}
|
}
|
||||||
|
|
||||||
cleanup()
|
cleanup()
|
||||||
@ -214,7 +216,7 @@ const ProfileItem: React.FC<Props> = (props) => {
|
|||||||
fullWidth
|
fullWidth
|
||||||
isPressable={false}
|
isPressable={false}
|
||||||
onContextMenu={handleContextMenu}
|
onContextMenu={handleContextMenu}
|
||||||
className={`${isCurrent ? 'bg-primary' : ''} cursor-pointer transition-colors duration-150`}
|
className={`${isCurrent ? 'bg-primary' : ''} ${selecting ? 'blur-sm' : ''} cursor-pointer`}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
ref={setNodeRef}
|
ref={setNodeRef}
|
||||||
|
|||||||
@ -119,7 +119,7 @@ const MihomoConfig: React.FC = () => {
|
|||||||
try {
|
try {
|
||||||
const url = await getGistUrl()
|
const url = await getGistUrl()
|
||||||
if (url !== '') {
|
if (url !== '') {
|
||||||
await navigator.clipboard.writeText(`${url}/raw/clash-party.yaml`)
|
await navigator.clipboard.writeText(`${url}/raw/mihomo-party.yaml`)
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
alert(e)
|
alert(e)
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@ -25,7 +25,6 @@ export const ProfileConfigProvider: React.FC<{ children: ReactNode }> = ({ child
|
|||||||
const { data: profileConfig, mutate: mutateProfileConfig } = useSWR('getProfileConfig', () =>
|
const { data: profileConfig, mutate: mutateProfileConfig } = useSWR('getProfileConfig', () =>
|
||||||
getProfileConfig()
|
getProfileConfig()
|
||||||
)
|
)
|
||||||
const [targetProfileId, setTargetProfileId] = React.useState<string | null>(null)
|
|
||||||
|
|
||||||
const setProfileConfig = async (config: IProfileConfig): Promise<void> => {
|
const setProfileConfig = async (config: IProfileConfig): Promise<void> => {
|
||||||
try {
|
try {
|
||||||
@ -72,31 +71,23 @@ export const ProfileConfigProvider: React.FC<{ children: ReactNode }> = ({ child
|
|||||||
}
|
}
|
||||||
|
|
||||||
const changeCurrentProfile = async (id: string): Promise<void> => {
|
const changeCurrentProfile = async (id: string): Promise<void> => {
|
||||||
if (targetProfileId === id) {
|
if (profileConfig?.current === id) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
setTargetProfileId(id)
|
// 乐观更新:立即更新 UI 状态,提供即时反馈
|
||||||
|
|
||||||
// 立即更新 UI 状态和托盘菜单,提供即时反馈
|
|
||||||
if (profileConfig) {
|
if (profileConfig) {
|
||||||
const optimisticUpdate = { ...profileConfig, current: id }
|
const optimisticUpdate = { ...profileConfig, current: id }
|
||||||
mutateProfileConfig(optimisticUpdate, false)
|
mutateProfileConfig(optimisticUpdate, false)
|
||||||
window.electron.ipcRenderer.send('updateTrayMenu')
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 异步执行后台切换,不阻塞 UI
|
|
||||||
try {
|
try {
|
||||||
await change(id)
|
// 异步执行后台切换,不阻塞 UI
|
||||||
|
change(id).then(() => {
|
||||||
if (targetProfileId === id) {
|
window.electron.ipcRenderer.send('updateTrayMenu')
|
||||||
mutateProfileConfig()
|
mutateProfileConfig()
|
||||||
setTargetProfileId(null)
|
}).catch((e) => {
|
||||||
} else {
|
const errorMsg = e?.message || String(e)
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
if (targetProfileId === id) {
|
|
||||||
const errorMsg = (e as any)?.message || String(e)
|
|
||||||
// 处理 IPC 超时错误
|
// 处理 IPC 超时错误
|
||||||
if (errorMsg.includes('reply was never sent')) {
|
if (errorMsg.includes('reply was never sent')) {
|
||||||
setTimeout(() => mutateProfileConfig(), 1000)
|
setTimeout(() => mutateProfileConfig(), 1000)
|
||||||
@ -104,8 +95,10 @@ export const ProfileConfigProvider: React.FC<{ children: ReactNode }> = ({ child
|
|||||||
alert(`切换 Profile 失败: ${errorMsg}`)
|
alert(`切换 Profile 失败: ${errorMsg}`)
|
||||||
mutateProfileConfig()
|
mutateProfileConfig()
|
||||||
}
|
}
|
||||||
setTargetProfileId(null)
|
})
|
||||||
}
|
} catch (e) {
|
||||||
|
alert(`切换 Profile 失败: ${e}`)
|
||||||
|
mutateProfileConfig()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user