Compare commits

..

No commits in common. "4137f91ccb16ad98b7e5cc9f80255a4e93fb06ec" and "199ecd26ddb3f9d30248eb6d286b268b57fbdcb3" have entirely different histories.

11 changed files with 33 additions and 86 deletions

Binary file not shown.

View File

@ -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 管理防止内核通信失败

View File

@ -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()
} }

View File

@ -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()

View File

@ -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
} }
} }

View File

@ -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) {

View File

@ -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}

View File

@ -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

View File

@ -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()
} }
} }