mirror of
https://github.com/clash-verge-rev/clash-verge-rev.git
synced 2026-11-15 15:32:21 +08:00
refactor(backend): specta ipc bindings (#8195)
* refactor(ipc): generate bindings with Specta * fix(profile): project IPC inputs to consumed fields * fix(events): remove unused refresh payloads * fix(webdav): return only consumed backup metadata * fix(network): trim unused interface metadata * fix(sysinfo): return only displayed system fields * chore(knip): check generated runtime exports * fix(proxy): confirm runtime chain updates before UI changes * fix(clipboard): propagate proxy environment copy failures * refactor(dns): remove ignored runtime patch * refactor(core): trim unused upgrade response fields * refactor(ipc): avoid redundant response copies * refactor(proxy): clarify automatic proxy query naming * chore(tests): remove added IPC regression tests * refactor(ipc): enforce typed command boundaries * chore(contract): check generated bindings before push * refactor(ipc): remove unused proxy liveness payloads
This commit is contained in:
@@ -423,7 +423,7 @@ interface PersistentProxySelectProps
|
||||
value: string
|
||||
selectedName: string
|
||||
groupName: string
|
||||
fixed?: string
|
||||
fixed?: string | null
|
||||
selectedDelay: number
|
||||
options: ProxyOption[]
|
||||
onChange: (value: string) => void
|
||||
@@ -785,8 +785,8 @@ export const CurrentProxyCard = () => {
|
||||
// The profile selection is the durable source across core restarts and run modes.
|
||||
handleSelectChange(
|
||||
selectedGroup.name,
|
||||
previousProxy,
|
||||
selectedGroup.fixed,
|
||||
previousProxy!,
|
||||
selectedGroup.fixed!,
|
||||
)({
|
||||
target: { value: nextName },
|
||||
})
|
||||
@@ -940,7 +940,7 @@ export const CurrentProxyCard = () => {
|
||||
unsortedProxyOptions,
|
||||
sortType,
|
||||
delays.of,
|
||||
defaultLatencyTimeout,
|
||||
defaultLatencyTimeout!,
|
||||
),
|
||||
[
|
||||
delays,
|
||||
|
||||
@@ -56,26 +56,7 @@ const openProfileHome = (url: string) => {
|
||||
void openExternalUrl(url).catch(showNotice.error)
|
||||
}
|
||||
|
||||
// 使用类型定义,而不是导入
|
||||
interface ProfileExtra {
|
||||
upload: number
|
||||
download: number
|
||||
total: number
|
||||
expire: number
|
||||
}
|
||||
|
||||
interface ProfileItem {
|
||||
uid: string
|
||||
type?: 'local' | 'remote' | 'merge' | 'script'
|
||||
name?: string
|
||||
desc?: string
|
||||
file?: string
|
||||
url?: string
|
||||
updated?: number
|
||||
extra?: ProfileExtra
|
||||
home?: string
|
||||
option?: any
|
||||
}
|
||||
type ProfileItem = IProfileItem
|
||||
|
||||
interface HomeProfileCardProps {
|
||||
current: ProfileItem | null | undefined
|
||||
@@ -298,10 +279,13 @@ export const HomeProfileCard = ({
|
||||
|
||||
setUpdating(true)
|
||||
try {
|
||||
await mutate(() => updateProfile(current.uid, current.option), {
|
||||
id: `update-profile:${current.uid}`,
|
||||
errorNotice: false,
|
||||
})
|
||||
await mutate(
|
||||
() => updateProfile(current.uid!, current.option ?? undefined),
|
||||
{
|
||||
id: `update-profile:${current.uid}`,
|
||||
errorNotice: false,
|
||||
},
|
||||
)
|
||||
onProfileUpdated?.()
|
||||
|
||||
// 刷新首页数据
|
||||
@@ -345,7 +329,7 @@ export const HomeProfileCard = ({
|
||||
flex: 1,
|
||||
},
|
||||
}}
|
||||
title={current.name}
|
||||
title={current.name!}
|
||||
>
|
||||
<span>{current.name}</span>
|
||||
<LaunchOutlined
|
||||
|
||||
@@ -201,7 +201,7 @@ export const ProxyTunCard: FC = () => {
|
||||
onClick={() => handleTabChange('tun')}
|
||||
icon={TroubleshootRounded}
|
||||
label={t('settings.sections.system.toggles.tunMode')}
|
||||
hasIndicator={enable_tun_mode && isTunModeAvailable}
|
||||
hasIndicator={enable_tun_mode! && isTunModeAvailable}
|
||||
/>
|
||||
</Stack>
|
||||
|
||||
|
||||
@@ -164,7 +164,7 @@ export const TestCard = () => {
|
||||
}}
|
||||
>
|
||||
{testList.map((item, index) => (
|
||||
<SortableItem key={item.uid} id={item.uid} index={index}>
|
||||
<SortableItem key={item.uid} id={item.uid!} index={index}>
|
||||
<TestItem
|
||||
itemData={item}
|
||||
onEdit={() => viewerRef.current?.edit(item)}
|
||||
|
||||
@@ -120,7 +120,7 @@ const ProfileItemBase = (props: ProfileItemProps) => {
|
||||
)
|
||||
const setLoading = useCallback(
|
||||
(loading: boolean) => {
|
||||
setProfileLoading([itemData.uid], loading)
|
||||
setProfileLoading([itemData.uid!], loading)
|
||||
},
|
||||
[itemData.uid, setProfileLoading],
|
||||
)
|
||||
@@ -140,7 +140,7 @@ const ProfileItemBase = (props: ProfileItemProps) => {
|
||||
debugLog(`强制刷新定时器任务`)
|
||||
}
|
||||
|
||||
const nextUpdate = await getNextUpdateTime(itemData.uid)
|
||||
const nextUpdate = await getNextUpdateTime(itemData.uid!)
|
||||
debugLog(`获取到下次更新时间结果:`, nextUpdate)
|
||||
|
||||
if (nextUpdate) {
|
||||
@@ -245,7 +245,7 @@ const ProfileItemBase = (props: ProfileItemProps) => {
|
||||
const hasHome = !!itemData.home // only subscription url has home page
|
||||
|
||||
const { upload = 0, download = 0, total = 0 } = extra ?? {}
|
||||
const from = parseUrl(itemData.url)
|
||||
const from = parseUrl(itemData.url!)
|
||||
const description = itemData.desc
|
||||
const expire = parseExpire(extra?.expire)
|
||||
const progress = Math.min(
|
||||
@@ -253,7 +253,7 @@ const ProfileItemBase = (props: ProfileItemProps) => {
|
||||
100,
|
||||
)
|
||||
|
||||
const loading = loadingCache.has(itemData.uid)
|
||||
const loading = loadingCache.has(itemData.uid!)
|
||||
|
||||
const [, forceRefresh] = useReducer((value: number) => value + 1, 0)
|
||||
useEffect(() => {
|
||||
@@ -263,7 +263,7 @@ const ProfileItemBase = (props: ProfileItemProps) => {
|
||||
|
||||
const handler = () => {
|
||||
const now = Date.now()
|
||||
const lastUpdate = updated * 1000
|
||||
const lastUpdate = updated! * 1000
|
||||
if (now - lastUpdate >= 24 * 36e5) return
|
||||
|
||||
const wait = now - lastUpdate >= 36e5 ? 30e5 : 5e4
|
||||
@@ -293,7 +293,7 @@ const ProfileItemBase = (props: ProfileItemProps) => {
|
||||
const [confirmOpen, setConfirmOpen] = useState(false)
|
||||
const [qrOpen, setQrOpen] = useState(false)
|
||||
|
||||
const loadProfileDocument = useCallback(() => readProfileFile(uid), [uid])
|
||||
const loadProfileDocument = useCallback(() => readProfileFile(uid!), [uid])
|
||||
const loadMergeDocument = useCallback(
|
||||
() => readProfileFile(option?.merge ?? ''),
|
||||
[option?.merge],
|
||||
@@ -370,7 +370,7 @@ const ProfileItemBase = (props: ProfileItemProps) => {
|
||||
const onOpenFile = useLockFn(async () => {
|
||||
setAnchorEl(null)
|
||||
try {
|
||||
await mutate(() => viewProfile(itemData.uid), {
|
||||
await mutate(() => viewProfile(itemData.uid!), {
|
||||
id: 'view-profile',
|
||||
errorNotice: false,
|
||||
})
|
||||
@@ -402,7 +402,7 @@ const ProfileItemBase = (props: ProfileItemProps) => {
|
||||
|
||||
try {
|
||||
const payload = Object.keys(option).length > 0 ? option : undefined
|
||||
await mutate(() => updateProfile(itemData.uid, payload), {
|
||||
await mutate(() => updateProfile(itemData.uid!, payload), {
|
||||
id: `update-profile:${itemData.uid}`,
|
||||
errorNotice: false,
|
||||
})
|
||||
@@ -593,7 +593,7 @@ const ProfileItemBase = (props: ProfileItemProps) => {
|
||||
|
||||
const handleSaveProfileDocument = useLockFn(async () => {
|
||||
const currentValue = profileDocument.value
|
||||
const result = await mutate(() => saveProfileFile(uid, currentValue), {
|
||||
const result = await mutate(() => saveProfileFile(uid!, currentValue), {
|
||||
id: `save-profile-file:${uid}`,
|
||||
errorNotice: false,
|
||||
})
|
||||
@@ -728,7 +728,7 @@ const ProfileItemBase = (props: ProfileItemProps) => {
|
||||
variant="h6"
|
||||
component="h2"
|
||||
noWrap
|
||||
title={name}
|
||||
title={name!}
|
||||
>
|
||||
{name}
|
||||
</Typography>
|
||||
@@ -794,7 +794,7 @@ const ProfileItemBase = (props: ProfileItemProps) => {
|
||||
title={
|
||||
showNextUpdate
|
||||
? t('profiles.components.profileItem.tooltips.showLast')
|
||||
: `${t('shared.labels.updateTime')}: ${parseExpire(updated)}\n${t('profiles.components.profileItem.tooltips.showNext')}`
|
||||
: `${t('shared.labels.updateTime')}: ${parseExpire(updated!)}\n${t('profiles.components.profileItem.tooltips.showNext')}`
|
||||
}
|
||||
sx={{
|
||||
fontSize: 14,
|
||||
@@ -812,8 +812,8 @@ const ProfileItemBase = (props: ProfileItemProps) => {
|
||||
>
|
||||
{showNextUpdate
|
||||
? nextUpdateTime
|
||||
: updated > 0
|
||||
? dayjs(updated * 1000).fromNow()
|
||||
: updated! > 0
|
||||
? dayjs(updated! * 1000).fromNow()
|
||||
: ''}
|
||||
</Typography>
|
||||
</Box>
|
||||
@@ -831,7 +831,7 @@ const ProfileItemBase = (props: ProfileItemProps) => {
|
||||
) : (
|
||||
<Box sx={{ ...boxStyle, fontSize: 12, justifyContent: 'flex-end' }}>
|
||||
<span title={t('shared.labels.updateTime')}>
|
||||
{parseExpire(updated)}
|
||||
{parseExpire(updated!)}
|
||||
</span>
|
||||
</Box>
|
||||
)}
|
||||
@@ -896,7 +896,7 @@ const ProfileItemBase = (props: ProfileItemProps) => {
|
||||
<RulesEditorViewer
|
||||
groupsUid={option?.groups ?? ''}
|
||||
mergeUid={option?.merge ?? ''}
|
||||
profileUid={uid}
|
||||
profileUid={uid!}
|
||||
property={option?.rules ?? ''}
|
||||
open={true}
|
||||
onSave={onSave}
|
||||
@@ -905,7 +905,7 @@ const ProfileItemBase = (props: ProfileItemProps) => {
|
||||
)}
|
||||
{proxiesOpen && (
|
||||
<ProxiesEditorViewer
|
||||
profileUid={uid}
|
||||
profileUid={uid!}
|
||||
property={option?.proxies ?? ''}
|
||||
open={true}
|
||||
onSave={onSave}
|
||||
@@ -916,7 +916,7 @@ const ProfileItemBase = (props: ProfileItemProps) => {
|
||||
<GroupsEditorViewer
|
||||
mergeUid={option?.merge ?? ''}
|
||||
proxiesUid={option?.proxies ?? ''}
|
||||
profileUid={uid}
|
||||
profileUid={uid!}
|
||||
property={option?.groups ?? ''}
|
||||
open={true}
|
||||
onSave={onSave}
|
||||
@@ -976,7 +976,7 @@ const ProfileItemBase = (props: ProfileItemProps) => {
|
||||
{qrOpen && itemData.url && (
|
||||
<QrViewer
|
||||
open={true}
|
||||
value={`${itemData.url}${itemData.url.includes('?') ? '&' : '?'}name=${encodeURIComponent(name)}`}
|
||||
value={`${itemData.url}${itemData.url.includes('?') ? '&' : '?'}name=${encodeURIComponent(name!)}`}
|
||||
onClose={() => setQrOpen(false)}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -115,7 +115,13 @@ export function ProfileViewer({ onChange, ref }: ProfileViewerProps) {
|
||||
}
|
||||
|
||||
const name = form.name || `${form.type} file`
|
||||
const item = { ...form, name, option }
|
||||
const item = {
|
||||
type: form.type,
|
||||
name,
|
||||
desc: form.desc,
|
||||
option,
|
||||
...(form.type === 'remote' ? { url: form.url } : {}),
|
||||
}
|
||||
const isRemote = form.type === 'remote'
|
||||
const isUpdate = openType === 'edit'
|
||||
|
||||
@@ -139,7 +145,7 @@ export function ProfileViewer({ onChange, ref }: ProfileViewerProps) {
|
||||
t('profiles.modals.profileForm.errors.uidMissing'),
|
||||
)
|
||||
}
|
||||
await mutate(() => patchProfile(form.uid, item), {
|
||||
await mutate(() => patchProfile(form.uid!, item), {
|
||||
id: 'patch-profile',
|
||||
errorNotice: false,
|
||||
})
|
||||
@@ -157,7 +163,7 @@ export function ProfileViewer({ onChange, ref }: ProfileViewerProps) {
|
||||
t('profiles.modals.profileForm.errors.uidMissing'),
|
||||
)
|
||||
}
|
||||
await mutate(() => patchProfile(form.uid, item), {
|
||||
await mutate(() => patchProfile(form.uid!, item), {
|
||||
id: 'patch-profile',
|
||||
errorNotice: false,
|
||||
})
|
||||
@@ -190,13 +196,13 @@ export function ProfileViewer({ onChange, ref }: ProfileViewerProps) {
|
||||
t('profiles.modals.profileForm.errors.uidMissing'),
|
||||
)
|
||||
}
|
||||
await mutate(() => patchProfile(form.uid, retryItem), {
|
||||
await mutate(() => patchProfile(form.uid!, retryItem), {
|
||||
id: 'patch-profile',
|
||||
errorNotice: false,
|
||||
})
|
||||
|
||||
await mutate(
|
||||
() => patchProfile(form.uid, { option: originalOptions }),
|
||||
() => patchProfile(form.uid!, { option: originalOptions }),
|
||||
{
|
||||
id: 'patch-profile',
|
||||
errorNotice: false,
|
||||
@@ -413,7 +419,7 @@ export function ProfileViewer({ onChange, ref }: ProfileViewerProps) {
|
||||
<InputLabel>
|
||||
{t('profiles.modals.profileForm.fields.useSystemProxy')}
|
||||
</InputLabel>
|
||||
<Switch checked={field.value} {...field} color="primary" />
|
||||
<Switch checked={field.value!} {...field} color="primary" />
|
||||
</StyledBox>
|
||||
)}
|
||||
/>
|
||||
@@ -426,7 +432,7 @@ export function ProfileViewer({ onChange, ref }: ProfileViewerProps) {
|
||||
<InputLabel>
|
||||
{t('profiles.modals.profileForm.fields.useClashProxy')}
|
||||
</InputLabel>
|
||||
<Switch checked={field.value} {...field} color="primary" />
|
||||
<Switch checked={field.value!} {...field} color="primary" />
|
||||
</StyledBox>
|
||||
)}
|
||||
/>
|
||||
@@ -439,7 +445,7 @@ export function ProfileViewer({ onChange, ref }: ProfileViewerProps) {
|
||||
<InputLabel>
|
||||
{t('profiles.modals.profileForm.fields.acceptInvalidCerts')}
|
||||
</InputLabel>
|
||||
<Switch checked={field.value} {...field} color="primary" />
|
||||
<Switch checked={field.value!} {...field} color="primary" />
|
||||
</StyledBox>
|
||||
)}
|
||||
/>
|
||||
|
||||
@@ -445,10 +445,14 @@ export const ProxyChain = ({
|
||||
if (isConnected) {
|
||||
setIsConnecting(true)
|
||||
try {
|
||||
await mutate(() => updateProxyChainConfigInRuntime(null), {
|
||||
id: 'update-proxy-chain-runtime',
|
||||
errorNotice: false,
|
||||
})
|
||||
const result = await mutate(
|
||||
() => updateProxyChainConfigInRuntime(null),
|
||||
{
|
||||
id: 'update-proxy-chain-runtime',
|
||||
errorNotice: false,
|
||||
},
|
||||
)
|
||||
if (!result.ok) return
|
||||
|
||||
const targetGroup =
|
||||
mode === 'global'
|
||||
@@ -461,7 +465,7 @@ export const ProxyChain = ({
|
||||
id: `select-proxy-node:${targetGroup}`,
|
||||
errorNotice: false,
|
||||
})
|
||||
recordSelection(targetGroup, 'DIRECT')
|
||||
await recordSelection(targetGroup, 'DIRECT')
|
||||
} catch {
|
||||
if (currentProxyChain.length >= 1) {
|
||||
try {
|
||||
@@ -473,7 +477,7 @@ export const ProxyChain = ({
|
||||
errorNotice: false,
|
||||
},
|
||||
)
|
||||
recordSelection(targetGroup, currentProxyChain[0].name)
|
||||
await recordSelection(targetGroup, currentProxyChain[0].name)
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
@@ -519,10 +523,14 @@ export const ProxyChain = ({
|
||||
// 第一步:保存链式代理配置
|
||||
const chainProxies = currentProxyChain.map((node) => node.name)
|
||||
debugLog('Saving chain config:', chainProxies)
|
||||
await mutate(() => updateProxyChainConfigInRuntime(chainProxies), {
|
||||
id: 'update-proxy-chain-runtime',
|
||||
errorNotice: false,
|
||||
})
|
||||
const result = await mutate(
|
||||
() => updateProxyChainConfigInRuntime(chainProxies),
|
||||
{
|
||||
id: 'update-proxy-chain-runtime',
|
||||
errorNotice: false,
|
||||
},
|
||||
)
|
||||
if (!result.ok) return
|
||||
debugLog('Chain configuration saved successfully')
|
||||
|
||||
// 第二步:连接到代理链的最后一个节点
|
||||
@@ -545,12 +553,12 @@ export const ProxyChain = ({
|
||||
)
|
||||
// The chain moves the group like any other selection, so the profile has to learn about
|
||||
// it: what the profile holds is what gets re-applied the next time the core starts.
|
||||
recordSelection(targetGroup || 'GLOBAL', lastNode.name)
|
||||
await recordSelection(targetGroup || 'GLOBAL', lastNode.name)
|
||||
localStorage.setItem('proxy-chain-group', targetGroup || 'GLOBAL')
|
||||
localStorage.setItem('proxy-chain-exit-node', lastNode.name)
|
||||
|
||||
// 刷新代理信息以更新连接状态
|
||||
refreshProxy()
|
||||
await refreshProxy()
|
||||
debugLog('Successfully connected to proxy chain')
|
||||
} catch (error) {
|
||||
console.error('Failed to connect to proxy chain:', error)
|
||||
@@ -619,14 +627,15 @@ export const ProxyChain = ({
|
||||
<IconButton
|
||||
size="small"
|
||||
onClick={() => {
|
||||
mutate(() => updateProxyChainConfigInRuntime(null), {
|
||||
void mutate(() => updateProxyChainConfigInRuntime(null), {
|
||||
id: 'update-proxy-chain-runtime',
|
||||
errorNotice: false,
|
||||
})
|
||||
localStorage.removeItem('proxy-chain-group')
|
||||
localStorage.removeItem('proxy-chain-exit-node')
|
||||
localStorage.removeItem('proxy-chain-items')
|
||||
onUpdateChain([])
|
||||
onFulfilled: () => {
|
||||
localStorage.removeItem('proxy-chain-group')
|
||||
localStorage.removeItem('proxy-chain-exit-node')
|
||||
localStorage.removeItem('proxy-chain-items')
|
||||
onUpdateChain([])
|
||||
},
|
||||
}).catch(() => {})
|
||||
}}
|
||||
sx={{
|
||||
color: theme.palette.error.main,
|
||||
|
||||
@@ -5,11 +5,11 @@ export type ProxyEmptyStateReason =
|
||||
| 'no-proxy-info'
|
||||
|
||||
type ProfileSummary = {
|
||||
current?: string
|
||||
current?: string | null
|
||||
items?: Array<{
|
||||
uid?: string
|
||||
type?: string
|
||||
}>
|
||||
uid?: string | null
|
||||
type?: string | null
|
||||
}> | null
|
||||
}
|
||||
|
||||
interface ProxyListStateInput {
|
||||
@@ -20,7 +20,7 @@ interface ProxyListStateInput {
|
||||
isRunningModePending: boolean
|
||||
}
|
||||
|
||||
const isSubscriptionProfile = (item: { type?: string }): boolean =>
|
||||
const isSubscriptionProfile = (item: { type?: string | null }): boolean =>
|
||||
item.type === 'remote' || item.type === 'local'
|
||||
|
||||
/** Decides only pre-render states; the built list owns whether it has renderable content. */
|
||||
|
||||
@@ -400,20 +400,26 @@ export function ProxyGroupsChain(props: ProxyGroupsChainProps) {
|
||||
setRuleMenuAnchor(null)
|
||||
}
|
||||
|
||||
const handleGroupSelect = (groupName: string) => {
|
||||
onGroupSelect(groupName)
|
||||
handleGroupMenuClose()
|
||||
|
||||
const handleGroupSelect = async (groupName: string) => {
|
||||
if (mode === 'rule') {
|
||||
mutate(() => updateProxyChainConfigInRuntime(null), {
|
||||
id: 'update-proxy-chain-runtime',
|
||||
errorNotice: false,
|
||||
})
|
||||
try {
|
||||
const result = await mutate(
|
||||
() => updateProxyChainConfigInRuntime(null),
|
||||
{
|
||||
id: 'update-proxy-chain-runtime',
|
||||
},
|
||||
)
|
||||
if (!result.ok) return
|
||||
} catch {
|
||||
return
|
||||
}
|
||||
localStorage.removeItem('proxy-chain-group')
|
||||
localStorage.removeItem('proxy-chain-exit-node')
|
||||
localStorage.removeItem('proxy-chain-items')
|
||||
setProxyChain([])
|
||||
}
|
||||
onGroupSelect(groupName)
|
||||
handleGroupMenuClose()
|
||||
}
|
||||
|
||||
const handleCloseDuplicateWarning = useCallback(() => {
|
||||
|
||||
@@ -198,7 +198,7 @@ export const ProxyRender = memo(function ProxyRender(props: RenderProps) {
|
||||
{toolsOnLeft && proxyCount}
|
||||
<ProxyGroupTools
|
||||
side={toolsOnLeft ? 'left' : 'right'}
|
||||
url={group.testUrl}
|
||||
url={group.testUrl!}
|
||||
groupName={group.name}
|
||||
headState={headState!}
|
||||
onLocation={() => onLocation(group)}
|
||||
@@ -247,7 +247,7 @@ export const ProxyRender = memo(function ProxyRender(props: RenderProps) {
|
||||
return (
|
||||
<ProxyHead
|
||||
sx={{ pl: 2, pr: 3, mt: 0.5, mb: 1 }}
|
||||
url={group.testUrl}
|
||||
url={group.testUrl!}
|
||||
groupName={group.name}
|
||||
headState={headState!}
|
||||
onLocation={() => onLocation(group)}
|
||||
|
||||
@@ -17,7 +17,6 @@ const node = (memberIndex: number, delay: number, name = `${memberIndex}`) =>
|
||||
history: [{ delay }],
|
||||
extra: {
|
||||
'http://cp.cloudflare.com/generate_204': {
|
||||
alive: true,
|
||||
history: [{ time: new Date(0).toISOString(), delay }],
|
||||
},
|
||||
},
|
||||
|
||||
@@ -110,13 +110,13 @@ const CHAIN_DELAY_GROUP = 'chain-mode'
|
||||
const virtualGroup = (members: ProxyGroupView['members']): ProxyGroupView => ({
|
||||
name: CHAIN_DELAY_GROUP,
|
||||
type: 'Selector',
|
||||
alive: true,
|
||||
udp: false,
|
||||
xudp: false,
|
||||
tfo: false,
|
||||
mptcp: false,
|
||||
smux: false,
|
||||
history: [],
|
||||
extra: {},
|
||||
members,
|
||||
})
|
||||
|
||||
@@ -237,7 +237,7 @@ export const useRenderList = (
|
||||
selected?.name ?? CHAIN_DELAY_GROUP,
|
||||
'',
|
||||
0,
|
||||
latencyTimeout,
|
||||
latencyTimeout!,
|
||||
)
|
||||
if (col > 1) {
|
||||
return groupOccurrences(occurrences, col).map((memberCol) => ({
|
||||
@@ -289,8 +289,8 @@ export const useRenderList = (
|
||||
key: group.name,
|
||||
group,
|
||||
headState,
|
||||
icon: group.icon,
|
||||
testUrl: group.testUrl,
|
||||
icon: group.icon!,
|
||||
testUrl: group.testUrl!,
|
||||
},
|
||||
]
|
||||
|
||||
@@ -300,7 +300,7 @@ export const useRenderList = (
|
||||
group.name,
|
||||
headState.filterText,
|
||||
headState.sortType,
|
||||
latencyTimeout,
|
||||
latencyTimeout!,
|
||||
{
|
||||
matchCase: headState.filterMatchCase,
|
||||
matchWholeWord: headState.filterMatchWholeWord,
|
||||
@@ -337,11 +337,11 @@ export const useRenderList = (
|
||||
}
|
||||
|
||||
cache.set(group.name, {
|
||||
now: group.now,
|
||||
now: group.now!,
|
||||
members: group.members,
|
||||
headState,
|
||||
col,
|
||||
latencyTimeout,
|
||||
latencyTimeout: latencyTimeout!,
|
||||
delays: groupDelays.get(group.name),
|
||||
items: ret,
|
||||
})
|
||||
|
||||
@@ -50,9 +50,9 @@ export const BackupConfigViewer = memo(
|
||||
|
||||
const { register, handleSubmit, watch } = useForm<IWebDavConfig>({
|
||||
defaultValues: {
|
||||
url: webdav_url,
|
||||
username: webdav_username,
|
||||
password: webdav_password,
|
||||
url: webdav_url!,
|
||||
username: webdav_username!,
|
||||
password: webdav_password!,
|
||||
},
|
||||
})
|
||||
const url = watch('url')
|
||||
|
||||
@@ -196,7 +196,7 @@ export function DnsViewer({ ref }: { ref?: Ref<DialogRef> }) {
|
||||
const { verge } = useVerge()
|
||||
const { current: currentProfile } = useProfiles()
|
||||
const dnsEnabled = currentProfile
|
||||
? (verge?.profile_dns_settings?.[currentProfile.uid]?.enabled ??
|
||||
? (verge?.profile_dns_settings?.[currentProfile.uid!]?.enabled ??
|
||||
verge?.enable_dns_settings ??
|
||||
false)
|
||||
: false
|
||||
|
||||
@@ -438,7 +438,7 @@ export const SysproxyViewer = forwardRef<DialogRef>((props, ref) => {
|
||||
<Switch
|
||||
edge="end"
|
||||
disabled={!enabled}
|
||||
checked={value.pac}
|
||||
checked={value.pac!}
|
||||
onChange={(_, e) => setValue((v) => ({ ...v, pac: e }))}
|
||||
/>
|
||||
</ListItem>
|
||||
@@ -455,7 +455,7 @@ export const SysproxyViewer = forwardRef<DialogRef>((props, ref) => {
|
||||
<Switch
|
||||
edge="end"
|
||||
disabled={!enabled}
|
||||
checked={value.guard}
|
||||
checked={value.guard!}
|
||||
onChange={(_, e) => setValue((v) => ({ ...v, guard: e }))}
|
||||
sx={{ marginLeft: 'auto' }}
|
||||
/>
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { alpha, Box, Button, LinearProgress } from '@mui/material'
|
||||
import type { DownloadEvent } from '@tauri-apps/plugin-updater'
|
||||
import { useLockFn } from 'ahooks'
|
||||
import type { Ref } from 'react'
|
||||
import {
|
||||
@@ -15,6 +14,7 @@ import type { Options as ReactMarkdownOptions } from 'react-markdown'
|
||||
|
||||
import { BaseDialog, DialogRef } from '@/components/base'
|
||||
import { useUpdate } from '@/hooks/use-update'
|
||||
import type { DownloadEvent } from '@/services/bindings'
|
||||
import {
|
||||
cancelUpdateDownload,
|
||||
installUpdate,
|
||||
|
||||
@@ -40,7 +40,7 @@ const SettingClash = ({ onError }: Props) => {
|
||||
const { verge, mutateVerge } = useVerge()
|
||||
const { current: currentProfile } = useProfiles()
|
||||
const dnsEnabled = currentProfile
|
||||
? (verge?.profile_dns_settings?.[currentProfile.uid]?.enabled ??
|
||||
? (verge?.profile_dns_settings?.[currentProfile.uid!]?.enabled ??
|
||||
verge?.enable_dns_settings ??
|
||||
false)
|
||||
: false
|
||||
|
||||
@@ -9,6 +9,7 @@ import { useVerge } from '@/hooks/use-verge'
|
||||
import { navigationItems } from '@/pages/_navigation-meta'
|
||||
import { copyClashEnv } from '@/services/cmds'
|
||||
import { supportedLanguages } from '@/services/i18n'
|
||||
import { mutate } from '@/services/mutate'
|
||||
import { showNotice } from '@/services/notice-service'
|
||||
import getSystem from '@/utils/get-system'
|
||||
|
||||
@@ -75,8 +76,18 @@ const SettingVergeBasic = ({ onError }: Props) => {
|
||||
}
|
||||
|
||||
const onCopyClashEnv = useCallback(async () => {
|
||||
await copyClashEnv()
|
||||
showNotice.success('shared.feedback.notifications.common.copySuccess', 1000)
|
||||
try {
|
||||
await mutate(copyClashEnv, {
|
||||
id: 'copy-clash-env',
|
||||
onFulfilled: () =>
|
||||
showNotice.success(
|
||||
'shared.feedback.notifications.common.copySuccess',
|
||||
1000,
|
||||
),
|
||||
})
|
||||
} catch {
|
||||
// The shared mutation boundary reports the failure.
|
||||
}
|
||||
}, [])
|
||||
|
||||
return (
|
||||
|
||||
@@ -25,11 +25,11 @@ export const TestItem = ({ itemData, onEdit, onDelete: removeTest }: Props) => {
|
||||
const [position, setPosition] = useState({ left: 0, top: 0 })
|
||||
const [delay, setDelay] = useState(-1)
|
||||
const { uid, name, icon, url } = itemData
|
||||
const iconCachePath = useIconCache({ icon, cacheKey: uid })
|
||||
const iconCachePath = useIconCache({ icon: icon!, cacheKey: uid! })
|
||||
|
||||
const onDelay = useCallback(async () => {
|
||||
setDelay(-2)
|
||||
const result = await cmdTestDelay(url)
|
||||
const result = await cmdTestDelay(url!)
|
||||
setDelay(result)
|
||||
}, [url])
|
||||
|
||||
@@ -41,7 +41,7 @@ export const TestItem = ({ itemData, onEdit, onDelete: removeTest }: Props) => {
|
||||
const onDelete = useLockFn(async () => {
|
||||
setAnchorEl(null)
|
||||
try {
|
||||
removeTest(uid)
|
||||
removeTest(uid!)
|
||||
} catch (err: any) {
|
||||
showNotice.error(err)
|
||||
}
|
||||
@@ -81,17 +81,17 @@ export const TestItem = ({ itemData, onEdit, onDelete: removeTest }: Props) => {
|
||||
<Box sx={{ display: 'flex', justifyContent: 'center' }}>
|
||||
{icon.trim().startsWith('http') && (
|
||||
<img
|
||||
alt={name}
|
||||
alt={name!}
|
||||
src={iconCachePath === '' ? icon : iconCachePath}
|
||||
height="40px"
|
||||
/>
|
||||
)}
|
||||
{icon.trim().startsWith('data') && (
|
||||
<img alt={name} src={icon} height="40px" />
|
||||
<img alt={name!} src={icon!} height="40px" />
|
||||
)}
|
||||
{icon.trim().startsWith('<svg') && (
|
||||
<img
|
||||
alt={name}
|
||||
alt={name!}
|
||||
src={`data:image/svg+xml;charset=utf-8,${encodeURIComponent(icon)}`}
|
||||
height="40px"
|
||||
/>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { useCallback, useEffect, useRef } from 'react'
|
||||
|
||||
import type { ProfilePatch, ProfileSelection } from '@/services/bindings'
|
||||
import { getProfiles, patchProfile, patchProfilesConfig } from '@/services/cmds'
|
||||
import { mutate } from '@/services/mutate'
|
||||
import { migrateProfileTestUrls } from '@/services/proxy-test-url'
|
||||
@@ -46,7 +47,7 @@ export const useProfiles = () => {
|
||||
}, [])
|
||||
|
||||
const patchProfiles = useCallback(
|
||||
async (value: Partial<IProfilesConfig>) => {
|
||||
async (value: ProfileSelection) => {
|
||||
try {
|
||||
const result = await mutate(() => patchProfilesConfig(value), {
|
||||
id: 'patch-profiles-config',
|
||||
@@ -77,7 +78,7 @@ export const useProfiles = () => {
|
||||
)
|
||||
|
||||
const patchCurrent = useCallback(
|
||||
async (value: Partial<IProfileItem>) => {
|
||||
async (value: ProfilePatch) => {
|
||||
if (profiles?.current) {
|
||||
const uid = profiles.current
|
||||
await mutate(() => patchProfile(uid, value), {
|
||||
|
||||
@@ -166,10 +166,10 @@ export const useProxySelection = (options: ProxySelectionOptions = {}) => {
|
||||
|
||||
const handleProxyGroupChange = useCallback(
|
||||
(
|
||||
group: { name: string; now?: string; fixed?: string },
|
||||
group: { name: string; now?: string | null; fixed?: string | null },
|
||||
proxy: { name: string },
|
||||
) => {
|
||||
changeProxy(group.name, proxy.name, group.now, group.fixed)
|
||||
changeProxy(group.name, proxy.name, group.now!, group.fixed!)
|
||||
},
|
||||
[changeProxy],
|
||||
)
|
||||
|
||||
@@ -5,7 +5,7 @@ import { useDisplayedMixedPort } from '@/hooks/use-displayed-mixed-port'
|
||||
import { useVerge } from '@/hooks/use-verge'
|
||||
import { useSystemData } from '@/providers/app-data-context'
|
||||
import {
|
||||
getAutotemProxy,
|
||||
getAutoProxy,
|
||||
getEmbeddedServerPort,
|
||||
patchVergeConfig,
|
||||
} from '@/services/cmds'
|
||||
@@ -21,8 +21,8 @@ export const useSystemProxyState = () => {
|
||||
const { sysproxy } = useSystemData()
|
||||
const displayedMixedPort = useDisplayedMixedPort()
|
||||
const { data: autoproxy } = useQuery({
|
||||
queryKey: ['getAutotemProxy'],
|
||||
queryFn: getAutotemProxy,
|
||||
queryKey: ['getAutoProxy'],
|
||||
queryFn: getAutoProxy,
|
||||
refetchOnWindowFocus: true,
|
||||
refetchOnReconnect: true,
|
||||
})
|
||||
@@ -99,7 +99,7 @@ export const useSystemProxyState = () => {
|
||||
}
|
||||
// Kept separate so an unreadable config cannot discard OS state that did read.
|
||||
try {
|
||||
await revalidateQueries([['getSystemProxy'], ['getAutotemProxy']])
|
||||
await revalidateQueries([['getSystemProxy'], ['getAutoProxy']])
|
||||
} catch (error) {
|
||||
console.warn(
|
||||
'[system-proxy] rereading the OS proxy after a toggle failed:',
|
||||
@@ -109,14 +109,14 @@ export const useSystemProxyState = () => {
|
||||
// rather than stay live from a stale cache.
|
||||
await Promise.all([
|
||||
removeCacheData(['getSystemProxy']),
|
||||
removeCacheData(['getAutotemProxy']),
|
||||
removeCacheData(['getAutoProxy']),
|
||||
])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const invalidateProxyState = () =>
|
||||
revalidateQueries([['getSystemProxy'], ['getAutotemProxy']])
|
||||
revalidateQueries([['getSystemProxy'], ['getAutoProxy']])
|
||||
|
||||
return {
|
||||
indicator,
|
||||
|
||||
+13
-13
@@ -117,7 +117,7 @@ const ProfilePage = () => {
|
||||
} = useProfiles()
|
||||
|
||||
useEffect(() => {
|
||||
currentProfileRef.current = profiles.current
|
||||
currentProfileRef.current = profiles.current!
|
||||
}, [profiles])
|
||||
|
||||
useEffect(() => {
|
||||
@@ -263,7 +263,7 @@ const ProfilePage = () => {
|
||||
}
|
||||
|
||||
// `useProfiles` already retries three times; add only one business-level retry.
|
||||
const performRobustRefresh = async (prevCurrent?: string) => {
|
||||
const performRobustRefresh = async (prevCurrent?: string | null) => {
|
||||
let retryCount = 0
|
||||
const maxRetries = 1
|
||||
const baseDelay = 200
|
||||
@@ -580,7 +580,7 @@ const ProfilePage = () => {
|
||||
const onUpdateAll = useLockFn(async () => {
|
||||
const items = profileItems.filter((e) => e.type === 'remote')
|
||||
const target = items
|
||||
.map((item) => item.uid)
|
||||
.map((item) => item.uid!)
|
||||
.filter((uid) => !loadingCache.has(uid))
|
||||
|
||||
setLoadingProfiles(target, true)
|
||||
@@ -612,7 +612,7 @@ const ProfilePage = () => {
|
||||
}
|
||||
|
||||
const selectAllProfiles = () => {
|
||||
setSelectedProfiles(new Set(profileItems.map((item) => item.uid)))
|
||||
setSelectedProfiles(new Set(profileItems.map((item) => item.uid!)))
|
||||
}
|
||||
|
||||
const clearAllSelections = () => {
|
||||
@@ -904,20 +904,20 @@ const ProfilePage = () => {
|
||||
{profileItems.map((item, index) => (
|
||||
<ProfileItem
|
||||
key={item.uid}
|
||||
id={item.uid}
|
||||
id={item.uid!}
|
||||
index={index}
|
||||
selected={(switchTarget ?? profiles.current) === item.uid}
|
||||
activating={
|
||||
activatings.includes(item.uid) ||
|
||||
activatings.includes(item.uid!) ||
|
||||
visibleSwitchingProfile === item.uid
|
||||
}
|
||||
itemData={item}
|
||||
timerUpdateRevision={timerRevisions.get(item.uid) ?? 0}
|
||||
timerUpdateRevision={timerRevisions.get(item.uid!) ?? 0}
|
||||
completedUpdateRevision={
|
||||
completedUpdateRevisions.get(item.uid) ?? 0
|
||||
completedUpdateRevisions.get(item.uid!) ?? 0
|
||||
}
|
||||
mutateProfiles={mutateProfiles}
|
||||
onSelect={(f) => onSelect(item.uid, f)}
|
||||
onSelect={(f) => onSelect(item.uid!, f)}
|
||||
onEdit={() => viewerRef.current?.edit(item)}
|
||||
onSave={async (prev, curr) => {
|
||||
if (prev !== curr && profiles.current === item.uid) {
|
||||
@@ -926,14 +926,14 @@ const ProfilePage = () => {
|
||||
}}
|
||||
onDelete={() => {
|
||||
if (batchMode) {
|
||||
toggleProfileSelection(item.uid)
|
||||
toggleProfileSelection(item.uid!)
|
||||
} else {
|
||||
onDelete(item.uid)
|
||||
onDelete(item.uid!)
|
||||
}
|
||||
}}
|
||||
batchMode={batchMode}
|
||||
isSelected={selectedProfiles.has(item.uid)}
|
||||
onSelectionChange={() => toggleProfileSelection(item.uid)}
|
||||
isSelected={selectedProfiles.has(item.uid!)}
|
||||
onSelectionChange={() => toggleProfileSelection(item.uid!)}
|
||||
/>
|
||||
))}
|
||||
</Box>
|
||||
|
||||
+15
-11
@@ -62,7 +62,7 @@ const ProxyPage = () => {
|
||||
id: 'patch-clash-mode',
|
||||
errorNotice: false,
|
||||
})
|
||||
refreshClashConfig()
|
||||
await refreshClashConfig()
|
||||
} catch (error) {
|
||||
showNotice.error(error)
|
||||
}
|
||||
@@ -71,23 +71,27 @@ const ProxyPage = () => {
|
||||
const onToggleChainMode = useLockFn(async () => {
|
||||
const newChainMode = !isChainMode
|
||||
|
||||
setIsChainMode(newChainMode)
|
||||
// 保存链式代理按钮状态到 localStorage
|
||||
localStorage.setItem('proxy-chain-mode-enabled', newChainMode.toString())
|
||||
|
||||
if (!newChainMode) {
|
||||
// 退出链式代理模式时,清除链式代理配置
|
||||
try {
|
||||
debugLog('Exiting chain mode, clearing chain configuration')
|
||||
await mutate(() => updateProxyChainConfigInRuntime(null), {
|
||||
id: 'update-proxy-chain-runtime',
|
||||
errorNotice: false,
|
||||
})
|
||||
const result = await mutate(
|
||||
() => updateProxyChainConfigInRuntime(null),
|
||||
{
|
||||
id: 'update-proxy-chain-runtime',
|
||||
errorNotice: false,
|
||||
},
|
||||
)
|
||||
if (!result.ok) return
|
||||
debugLog('Chain configuration cleared successfully')
|
||||
} catch (error) {
|
||||
console.error('Failed to clear chain configuration:', error)
|
||||
showNotice.error(error)
|
||||
return
|
||||
}
|
||||
}
|
||||
setIsChainMode(newChainMode)
|
||||
localStorage.setItem('proxy-chain-mode-enabled', newChainMode.toString())
|
||||
})
|
||||
|
||||
// 当开启链式代理模式时,获取配置数据
|
||||
@@ -123,7 +127,7 @@ const ProxyPage = () => {
|
||||
}
|
||||
}
|
||||
|
||||
fetchChainConfig()
|
||||
fetchChainConfig().catch(() => {})
|
||||
|
||||
return () => {
|
||||
cancelled = true
|
||||
@@ -132,7 +136,7 @@ const ProxyPage = () => {
|
||||
|
||||
useEffect(() => {
|
||||
if (normalizedMode && !isMode(normalizedMode)) {
|
||||
onChangeMode('rule')
|
||||
onChangeMode('rule').catch(() => {})
|
||||
}
|
||||
}, [normalizedMode, onChangeMode])
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -72,11 +72,7 @@ export const createEventBus = (deps: BusHandlerDeps) => {
|
||||
'getRuleProviders',
|
||||
]),
|
||||
'verge://refresh-verge-config': () =>
|
||||
deps.revalidateKeys([
|
||||
'getVergeConfig',
|
||||
'getSystemProxy',
|
||||
'getAutotemProxy',
|
||||
]),
|
||||
deps.revalidateKeys(['getVergeConfig', 'getSystemProxy', 'getAutoProxy']),
|
||||
'verge://refresh-profiles': () => deps.revalidateKeys(['getProfiles']),
|
||||
'verge://refresh-proxy-config': handleRefreshProxyConfig,
|
||||
'verge://notice-message': (payload) => deps.handleNotice(payload),
|
||||
|
||||
+178
-269
@@ -1,141 +1,183 @@
|
||||
import { Channel, invoke } from '@tauri-apps/api/core'
|
||||
import type { DownloadEvent } from '@tauri-apps/plugin-updater'
|
||||
import { Channel } from '@tauri-apps/api/core'
|
||||
import dayjs from 'dayjs'
|
||||
|
||||
import type { CommandFailure } from '@/services/notice-service'
|
||||
import { showNotice } from '@/services/notice-service'
|
||||
import type { ProxyViewV1 } from '@/types/proxy-view'
|
||||
import { debugLog } from '@/utils/debug'
|
||||
|
||||
import { commands } from './bindings'
|
||||
import type {
|
||||
DownloadEvent,
|
||||
JsonValue,
|
||||
ListenerProbe,
|
||||
ProxyPortSettings,
|
||||
UnlockItem,
|
||||
ProxyViewV1_Serialize as ProxyViewV1,
|
||||
ProfileSelection,
|
||||
ProfilePatch,
|
||||
ProfileCreate,
|
||||
} from './bindings'
|
||||
|
||||
export type {
|
||||
RunningMode,
|
||||
RunStateView as RunState,
|
||||
FailedOperation,
|
||||
PendingFailure,
|
||||
SidecarFailureSnapshot,
|
||||
ServiceInstallOutcome,
|
||||
ProxyPortSettings,
|
||||
ListenerTransport,
|
||||
ListenerProbe,
|
||||
UnlockItem,
|
||||
} from './bindings'
|
||||
|
||||
export async function copyClashEnv() {
|
||||
return invoke<void>('copy_clash_env')
|
||||
return commands.copyClashEnv()
|
||||
}
|
||||
|
||||
export async function getProfiles() {
|
||||
return invoke<IProfilesConfig>('get_profiles')
|
||||
export async function getProfiles(): Promise<IProfilesConfig> {
|
||||
return commands.getProfiles()
|
||||
}
|
||||
|
||||
export async function enhanceProfiles() {
|
||||
return (
|
||||
(await invoke<ValidationOutcome>('enhance_profiles')).status === 'valid'
|
||||
)
|
||||
return (await commands.enhanceProfiles()).status === 'valid'
|
||||
}
|
||||
|
||||
export async function patchProfilesConfig(profiles: IProfilesConfig) {
|
||||
return invoke<ValidationOutcome>('patch_profiles_config', { profiles })
|
||||
export async function patchProfilesConfig(profiles: ProfileSelection) {
|
||||
return commands.patchProfilesConfig({ current: profiles.current })
|
||||
}
|
||||
|
||||
export async function createProfile(
|
||||
item: Partial<IProfileItem>,
|
||||
item: ProfileCreate,
|
||||
fileData?: string | null,
|
||||
) {
|
||||
return invoke<void>('create_profile', { item, fileData })
|
||||
}
|
||||
|
||||
export async function viewProfile(index: string) {
|
||||
return invoke<void>('view_profile', { index })
|
||||
}
|
||||
|
||||
export async function readProfileFile(index: string) {
|
||||
return invoke<string>('read_profile_file', { index })
|
||||
}
|
||||
|
||||
export async function saveProfileFile(index: string, fileData: string) {
|
||||
return (
|
||||
(
|
||||
await invoke<ValidationOutcome>('save_profile_file', {
|
||||
index,
|
||||
fileData,
|
||||
})
|
||||
).status === 'valid'
|
||||
return commands.createProfile(
|
||||
{
|
||||
type: item.type,
|
||||
name: item.name,
|
||||
desc: item.desc,
|
||||
...(item.type === 'remote' ? { url: item.url } : {}),
|
||||
option: item.option,
|
||||
},
|
||||
fileData ?? null,
|
||||
)
|
||||
}
|
||||
|
||||
export async function viewProfile(index: string) {
|
||||
return commands.viewProfile(index)
|
||||
}
|
||||
|
||||
export async function readProfileFile(index: string) {
|
||||
return commands.readProfileFile(index)
|
||||
}
|
||||
|
||||
export async function saveProfileFile(index: string, fileData: string) {
|
||||
return (await commands.saveProfileFile(index, fileData)).status === 'valid'
|
||||
}
|
||||
|
||||
export async function importProfile(url: string, option?: IProfileOption) {
|
||||
return invoke<void>('import_profile', {
|
||||
url,
|
||||
option: option || { with_proxy: true },
|
||||
})
|
||||
return commands.importProfile(url, option || { with_proxy: true })
|
||||
}
|
||||
|
||||
export async function reorderProfile(activeId: string, overId: string) {
|
||||
return invoke<void>('reorder_profile', {
|
||||
activeId,
|
||||
overId,
|
||||
})
|
||||
return commands.reorderProfile(activeId, overId)
|
||||
}
|
||||
|
||||
export async function updateProfile(index: string, option?: IProfileOption) {
|
||||
return invoke<void>('update_profile', { index, option })
|
||||
return commands.updateProfile(index, option ?? null)
|
||||
}
|
||||
|
||||
export async function deleteProfile(index: string) {
|
||||
return invoke<void>('delete_profile', { index })
|
||||
return commands.deleteProfile(index)
|
||||
}
|
||||
|
||||
export async function patchProfile(
|
||||
index: string,
|
||||
profile: Partial<IProfileItem>,
|
||||
) {
|
||||
return invoke<void>('patch_profile', { index, profile })
|
||||
export async function patchProfile(index: string, profile: ProfilePatch) {
|
||||
const {
|
||||
type,
|
||||
name,
|
||||
desc,
|
||||
file,
|
||||
url,
|
||||
selected,
|
||||
latency_test_urls,
|
||||
extra,
|
||||
updated,
|
||||
option,
|
||||
} = profile
|
||||
return commands.patchProfile(index, {
|
||||
type,
|
||||
name,
|
||||
desc,
|
||||
file,
|
||||
url,
|
||||
selected,
|
||||
latency_test_urls,
|
||||
extra,
|
||||
updated,
|
||||
option,
|
||||
})
|
||||
}
|
||||
|
||||
export async function getClashInfo() {
|
||||
return invoke<IClashInfo | null>('get_clash_info')
|
||||
return commands.getClashInfo()
|
||||
}
|
||||
|
||||
// Fallback mode read independent of strict mihomo `/configs` deserialization.
|
||||
export async function getClashMode() {
|
||||
return invoke<string | null>('get_clash_mode')
|
||||
return commands.getClashMode()
|
||||
}
|
||||
|
||||
export async function getRuntimeConfig() {
|
||||
return invoke<IConfigData | null>('get_runtime_config')
|
||||
return (await commands.getRuntimeConfig()) as IConfigData | null
|
||||
}
|
||||
|
||||
export async function getRuntimeYaml() {
|
||||
return invoke<string | null>('get_runtime_yaml')
|
||||
return commands.getRuntimeYaml()
|
||||
}
|
||||
|
||||
export async function getRuntimeLogs() {
|
||||
return invoke<Record<string, [string, string][]>>('get_runtime_logs')
|
||||
return commands.getRuntimeLogs()
|
||||
}
|
||||
|
||||
export async function getRuntimeProxyChainConfig(proxyChainExitNode: string) {
|
||||
return invoke<string>('get_runtime_proxy_chain_config', {
|
||||
proxyChainExitNode,
|
||||
})
|
||||
return commands.getRuntimeProxyChainConfig(proxyChainExitNode)
|
||||
}
|
||||
|
||||
export async function updateProxyChainConfigInRuntime(proxyChainConfig: any) {
|
||||
return invoke<void>('update_proxy_chain_config_in_runtime', {
|
||||
proxyChainConfig,
|
||||
})
|
||||
export async function updateProxyChainConfigInRuntime(
|
||||
proxyChainConfig: unknown,
|
||||
) {
|
||||
const outcome = await commands.updateProxyChainConfigInRuntime(
|
||||
proxyChainConfig == null ? null : (proxyChainConfig as JsonValue),
|
||||
)
|
||||
if (outcome.status === 'invalid') throw new Error(outcome.message)
|
||||
if (outcome.status === 'skipped') {
|
||||
throw new Error(`Proxy chain validation skipped: ${outcome.reason}`)
|
||||
}
|
||||
return outcome
|
||||
}
|
||||
|
||||
export async function patchClashConfig(payload: Partial<IConfigData>) {
|
||||
return invoke<void>('patch_clash_config', { payload })
|
||||
return commands.patchClashConfig(payload as Record<string, JsonValue>)
|
||||
}
|
||||
|
||||
export async function patchClashMode(payload: string) {
|
||||
return invoke<void>('patch_clash_mode', { payload })
|
||||
return commands.patchClashMode(payload)
|
||||
}
|
||||
|
||||
export async function syncTrayProxySelection() {
|
||||
return invoke<void>('sync_tray_proxy_selection')
|
||||
return commands.syncTrayProxySelection()
|
||||
}
|
||||
|
||||
/** Sends one selection pair so the backend can merge against current profile state. */
|
||||
export async function recordSelectedNode(groupName: string, node: string) {
|
||||
return invoke<void>('record_selected_node', { groupName, node })
|
||||
return commands.recordSelectedNode(groupName, node)
|
||||
}
|
||||
|
||||
export async function forgetSelectedNode(groupName: string) {
|
||||
return invoke<void>('forget_selected_node', { groupName })
|
||||
return commands.forgetSelectedNode(groupName)
|
||||
}
|
||||
|
||||
export async function getProxyView(): Promise<ProxyViewV1> {
|
||||
const view = await invoke<ProxyViewV1>('get_proxy_view')
|
||||
const view = await commands.getProxyView()
|
||||
if (view.schemaVersion !== 1) {
|
||||
throw new Error('Unsupported proxy view schema: ' + view.schemaVersion)
|
||||
}
|
||||
@@ -145,7 +187,7 @@ export async function getProxyView(): Promise<ProxyViewV1> {
|
||||
export async function getClashLogs() {
|
||||
const regex = /time="(.+?)"\s+level=(.+?)\s+msg="(.+?)"/
|
||||
const newRegex = /(.+?)\s+(.+?)\s+(.+)/
|
||||
const logs = await invoke<string[]>('get_clash_logs')
|
||||
const logs = await commands.getClashLogs()
|
||||
|
||||
return logs.reduce<ILogItem[]>((acc, log) => {
|
||||
const result = log.match(regex)
|
||||
@@ -165,12 +207,12 @@ export async function getClashLogs() {
|
||||
}, [])
|
||||
}
|
||||
|
||||
export async function getVergeConfig() {
|
||||
return invoke<IVergeConfig>('get_verge_config')
|
||||
export async function getVergeConfig(): Promise<IVergeConfig> {
|
||||
return commands.getVergeConfig()
|
||||
}
|
||||
|
||||
export async function patchVergeConfig(payload: IVergeConfig) {
|
||||
return invoke<void>('patch_verge_config', { payload })
|
||||
return commands.patchVergeConfig(payload)
|
||||
}
|
||||
|
||||
export async function setDnsOverride(
|
||||
@@ -178,76 +220,57 @@ export async function setDnsOverride(
|
||||
enabled: boolean,
|
||||
confirmation?: string,
|
||||
) {
|
||||
return invoke<
|
||||
{ status: 'applied' } | { status: 'confirmation_required'; source: string }
|
||||
>('set_dns_override', { profileUid, enabled, confirmation })
|
||||
return commands.setDnsOverride(profileUid, enabled, confirmation ?? null)
|
||||
}
|
||||
|
||||
export async function getDnsConfigContent() {
|
||||
return invoke<string | null>('get_dns_config_content')
|
||||
return commands.getDnsConfigContent()
|
||||
}
|
||||
|
||||
export async function saveDnsConfig(dnsConfig: Record<string, unknown>) {
|
||||
return invoke<void>('save_dns_config', { dnsConfig })
|
||||
return commands.saveDnsConfig(dnsConfig as Record<string, JsonValue>)
|
||||
}
|
||||
|
||||
export async function validateDnsConfig() {
|
||||
return invoke<ValidationOutcome>('validate_dns_config')
|
||||
return commands.validateDnsConfig()
|
||||
}
|
||||
|
||||
export async function applyDnsConfig(apply: boolean) {
|
||||
return invoke<void>('apply_dns_config', { apply })
|
||||
return commands.applyDnsConfig(apply)
|
||||
}
|
||||
|
||||
export async function takeDnsOverrideNotice() {
|
||||
return invoke<boolean>('take_dns_override_notice')
|
||||
return commands.takeDnsOverrideNotice()
|
||||
}
|
||||
|
||||
export type ServiceFallbackNotice =
|
||||
| { kind: 'unavailable' }
|
||||
| { kind: 'coreRejected'; reason: string }
|
||||
| { kind: 'notAutoStarted' }
|
||||
|
||||
export async function takeServiceFallbackNotice() {
|
||||
return invoke<ServiceFallbackNotice | null>('take_service_fallback_notice')
|
||||
}
|
||||
|
||||
export interface CoreFailure {
|
||||
kind: 'startFailed' | 'serviceCoreStopped'
|
||||
detail: string
|
||||
return commands.takeServiceFallbackNotice()
|
||||
}
|
||||
|
||||
export async function getCoreStartupError() {
|
||||
return invoke<CoreFailure | null>('get_core_startup_error')
|
||||
return commands.getCoreStartupError()
|
||||
}
|
||||
|
||||
export async function takeServiceRepairNotice() {
|
||||
return invoke<boolean>('take_service_repair_notice')
|
||||
return commands.takeServiceRepairNotice()
|
||||
}
|
||||
|
||||
export async function takeServiceOwnerNotice() {
|
||||
return invoke<string | null>('take_service_owner_notice')
|
||||
return commands.takeServiceOwnerNotice()
|
||||
}
|
||||
|
||||
export async function takeDiscardedKeysNotice() {
|
||||
return invoke<string | null>('take_discarded_keys_notice')
|
||||
return commands.takeDiscardedKeysNotice()
|
||||
}
|
||||
|
||||
export async function getSystemProxy() {
|
||||
return invoke<{
|
||||
enable: boolean
|
||||
server: string
|
||||
bypass: string
|
||||
}>('get_sys_proxy')
|
||||
return commands.getSysProxy()
|
||||
}
|
||||
|
||||
export async function getAutotemProxy() {
|
||||
export async function getAutoProxy() {
|
||||
try {
|
||||
debugLog('[API] 开始调用 get_auto_proxy')
|
||||
const result = await invoke<{
|
||||
enable: boolean
|
||||
url: string
|
||||
}>('get_auto_proxy')
|
||||
const result = await commands.getAutoProxy()
|
||||
debugLog('[API] get_auto_proxy 调用成功:', result)
|
||||
return result
|
||||
} catch (error) {
|
||||
@@ -260,30 +283,23 @@ export async function getAutotemProxy() {
|
||||
}
|
||||
|
||||
export async function getEmbeddedServerPort() {
|
||||
return invoke<number>('get_embedded_server_port')
|
||||
return commands.getEmbeddedServerPort()
|
||||
}
|
||||
|
||||
export async function changeClashCore(clashCore: string) {
|
||||
return invoke<CommandFailure | null>('change_clash_core', { clashCore })
|
||||
return commands.changeClashCore(clashCore)
|
||||
}
|
||||
|
||||
export async function restartCore() {
|
||||
return invoke<void>('restart_core')
|
||||
}
|
||||
|
||||
export interface CoreUpgradeReport {
|
||||
/** False when the managed core was already at the latest version. */
|
||||
upgraded: boolean
|
||||
from: string
|
||||
to: string
|
||||
return commands.restartCore()
|
||||
}
|
||||
|
||||
export async function upgradeClashCore(force = false) {
|
||||
return invoke<CoreUpgradeReport>('upgrade_clash_core', { force })
|
||||
return commands.upgradeClashCore(force)
|
||||
}
|
||||
|
||||
export async function restartApp() {
|
||||
return invoke<void>('restart_app')
|
||||
return commands.restartApp()
|
||||
}
|
||||
|
||||
export async function installUpdate(
|
||||
@@ -292,69 +308,57 @@ export async function installUpdate(
|
||||
) {
|
||||
const channel = new Channel<DownloadEvent>()
|
||||
channel.onmessage = onEvent
|
||||
return invoke<boolean>('install_update', { version, onEvent: channel })
|
||||
return commands.installUpdate(version, channel)
|
||||
}
|
||||
|
||||
export async function cancelUpdateDownload() {
|
||||
return invoke<void>('cancel_update_download')
|
||||
return commands.cancelUpdateDownload()
|
||||
}
|
||||
|
||||
export async function getAppDir() {
|
||||
return invoke<string>('get_app_dir')
|
||||
return commands.getAppDir()
|
||||
}
|
||||
|
||||
export async function openAppDir() {
|
||||
return invoke<void>('open_app_dir').catch((err) => showNotice.error(err))
|
||||
return commands.openAppDir().catch((err) => showNotice.error(err))
|
||||
}
|
||||
|
||||
export async function openCoreDir() {
|
||||
return invoke<void>('open_core_dir').catch((err) => showNotice.error(err))
|
||||
return commands.openCoreDir().catch((err) => showNotice.error(err))
|
||||
}
|
||||
|
||||
export async function openLogsDir() {
|
||||
return invoke<void>('open_logs_dir').catch((err) => showNotice.error(err))
|
||||
return commands.openLogsDir().catch((err) => showNotice.error(err))
|
||||
}
|
||||
|
||||
export async function syncRuntimeProviders() {
|
||||
return invoke<void>('sync_runtime_providers').catch((err) => {
|
||||
return commands.syncRuntimeProviders().catch((err) => {
|
||||
console.warn('failed to queue the provider cache sync', err)
|
||||
})
|
||||
}
|
||||
|
||||
export async function cmdTestDelay(url: string) {
|
||||
return invoke<number>('test_delay', { url })
|
||||
return commands.testDelay(url)
|
||||
}
|
||||
|
||||
export async function invoke_uwp_tool() {
|
||||
return invoke<void>('invoke_uwp_tool').catch((err) =>
|
||||
showNotice.error(err, 1500),
|
||||
)
|
||||
return commands.invokeUwpTool().catch((err) => showNotice.error(err, 1500))
|
||||
}
|
||||
|
||||
export async function openDevTools() {
|
||||
return invoke('open_devtools')
|
||||
return commands.openDevtools()
|
||||
}
|
||||
|
||||
export async function exitApp() {
|
||||
return invoke('exit_app')
|
||||
return commands.exitApp()
|
||||
}
|
||||
|
||||
export async function exportDiagnosticInfo() {
|
||||
return invoke('export_diagnostic_info')
|
||||
}
|
||||
|
||||
interface SystemInfo {
|
||||
system_name: string
|
||||
system_version: string
|
||||
system_kernel_version: string
|
||||
system_arch: string
|
||||
app_version: string
|
||||
app_core_mode: string
|
||||
app_is_admin: boolean
|
||||
return commands.exportDiagnosticInfo()
|
||||
}
|
||||
|
||||
export async function getSystemInfo() {
|
||||
return invoke<SystemInfo>('get_system_info')
|
||||
return commands.getSystemInfo()
|
||||
}
|
||||
|
||||
export async function copyIconFile(
|
||||
@@ -373,55 +377,55 @@ export async function copyIconFile(
|
||||
current_t: currentTime,
|
||||
}
|
||||
|
||||
return invoke<void>('copy_icon_file', { path, iconInfo })
|
||||
return commands.copyIconFile(path, iconInfo)
|
||||
}
|
||||
|
||||
export async function downloadIconCache(url: string, name: string) {
|
||||
return invoke<string>('download_icon_cache', { url, name })
|
||||
return commands.downloadIconCache(url, name)
|
||||
}
|
||||
|
||||
export async function getNetworkInterfaces() {
|
||||
return invoke<string[]>('get_network_interfaces')
|
||||
return commands.getNetworkInterfaces()
|
||||
}
|
||||
|
||||
export async function getSystemHostname() {
|
||||
return invoke<string>('get_system_hostname')
|
||||
return commands.getSystemHostname()
|
||||
}
|
||||
|
||||
export async function getNetworkInterfacesInfo() {
|
||||
return invoke<INetworkInterface[]>('get_network_interfaces_info')
|
||||
return commands.getNetworkInterfacesInfo()
|
||||
}
|
||||
|
||||
export async function createWebdavBackup() {
|
||||
return invoke<void>('create_webdav_backup')
|
||||
return commands.createWebdavBackup()
|
||||
}
|
||||
|
||||
export async function createLocalBackup() {
|
||||
return invoke<void>('create_local_backup')
|
||||
return commands.createLocalBackup()
|
||||
}
|
||||
|
||||
export async function deleteWebdavBackup(filename: string) {
|
||||
return invoke<void>('delete_webdav_backup', { filename })
|
||||
return commands.deleteWebdavBackup(filename)
|
||||
}
|
||||
|
||||
export async function deleteLocalBackup(filename: string) {
|
||||
return invoke<void>('delete_local_backup', { filename })
|
||||
return commands.deleteLocalBackup(filename)
|
||||
}
|
||||
|
||||
export async function restoreWebDavBackup(filename: string) {
|
||||
return invoke<void>('restore_webdav_backup', { filename })
|
||||
return commands.restoreWebdavBackup(filename)
|
||||
}
|
||||
|
||||
export async function restoreLocalBackup(filename: string) {
|
||||
return invoke<void>('restore_local_backup', { filename })
|
||||
return commands.restoreLocalBackup(filename)
|
||||
}
|
||||
|
||||
export async function importLocalBackup(source: string) {
|
||||
return invoke<string>('import_local_backup', { source })
|
||||
return commands.importLocalBackup(source)
|
||||
}
|
||||
|
||||
export async function exportLocalBackup(filename: string, destination: string) {
|
||||
return invoke<void>('export_local_backup', { filename, destination })
|
||||
return commands.exportLocalBackup(filename, destination)
|
||||
}
|
||||
|
||||
export async function saveWebdavConfig(
|
||||
@@ -429,177 +433,82 @@ export async function saveWebdavConfig(
|
||||
username: string,
|
||||
password: string,
|
||||
) {
|
||||
return invoke<void>('save_webdav_config', {
|
||||
url,
|
||||
username,
|
||||
password,
|
||||
})
|
||||
return commands.saveWebdavConfig(url, username, password)
|
||||
}
|
||||
|
||||
export async function listWebDavBackup() {
|
||||
const list: IWebDavFile[] = await invoke<IWebDavFile[]>('list_webdav_backup')
|
||||
list.forEach((item) => {
|
||||
item.filename = item.href.split('/').pop() as string
|
||||
})
|
||||
return list
|
||||
return (await commands.listWebdavBackup()).map((item) => ({
|
||||
...item,
|
||||
filename: item.href.split('/').pop() ?? '',
|
||||
}))
|
||||
}
|
||||
|
||||
export async function listLocalBackup() {
|
||||
return invoke<ILocalBackupFile[]>('list_local_backup')
|
||||
return commands.listLocalBackup()
|
||||
}
|
||||
|
||||
export type RunningMode = 'Service' | 'Sidecar' | 'NotRunning'
|
||||
|
||||
type ServiceHealth =
|
||||
| 'unknown'
|
||||
| 'ready'
|
||||
| 'notInstalled'
|
||||
| 'versionMismatch'
|
||||
| 'unavailable'
|
||||
|
||||
type PendingServiceAction =
|
||||
| 'install'
|
||||
| 'uninstall'
|
||||
| 'reinstall'
|
||||
| 'forceReinstall'
|
||||
|
||||
/** Consistent core/service snapshot with backend-derived availability flags. */
|
||||
export interface RunState {
|
||||
mode: RunningMode
|
||||
service: ServiceHealth
|
||||
serviceUnavailableReason: string | null
|
||||
pendingAction: PendingServiceAction | null
|
||||
sidecarAllowed: boolean
|
||||
isAdmin: boolean
|
||||
opInFlight: boolean
|
||||
serviceUsable: boolean
|
||||
tunCapable: boolean
|
||||
serviceNeedsAttention: boolean
|
||||
}
|
||||
|
||||
export const getRuntimeState = async () => {
|
||||
return invoke<RunState>('get_runtime_state')
|
||||
}
|
||||
|
||||
export type FailedOperation =
|
||||
| 'systemProxyEnable'
|
||||
| 'systemProxyDisable'
|
||||
| 'systemProxyRestore'
|
||||
| 'systemProxyGuard'
|
||||
|
||||
export interface PendingFailure {
|
||||
code: string
|
||||
detail: string
|
||||
operation: FailedOperation
|
||||
sequence: number
|
||||
return commands.getRuntimeState()
|
||||
}
|
||||
|
||||
export const getPendingFailures = async () => {
|
||||
return invoke<PendingFailure[]>('get_pending_failures')
|
||||
}
|
||||
|
||||
export interface SidecarFailureSnapshot {
|
||||
revision: number
|
||||
detail: string | null
|
||||
return commands.getPendingFailures()
|
||||
}
|
||||
|
||||
export const getSidecarFailure = async () => {
|
||||
return invoke<SidecarFailureSnapshot>('get_sidecar_failure')
|
||||
return commands.getSidecarFailure()
|
||||
}
|
||||
|
||||
export const getAppUptime = async () => {
|
||||
return invoke<number>('get_app_uptime')
|
||||
return commands.getAppUptime()
|
||||
}
|
||||
|
||||
export type ServiceInstallOutcome =
|
||||
| { status: 'installed' }
|
||||
| { status: 'sidecar'; reason: string }
|
||||
|
||||
export const installService = async () => {
|
||||
return invoke<ServiceInstallOutcome>('install_service')
|
||||
return commands.installService()
|
||||
}
|
||||
|
||||
export const uninstallService = async () => {
|
||||
return invoke<void>('uninstall_service')
|
||||
return commands.uninstallService()
|
||||
}
|
||||
|
||||
export const reinstallService = async () => {
|
||||
return invoke<ServiceInstallOutcome>('reinstall_service')
|
||||
return commands.reinstallService()
|
||||
}
|
||||
|
||||
export const repairService = async () => {
|
||||
return invoke<ServiceInstallOutcome>('repair_service')
|
||||
return commands.repairService()
|
||||
}
|
||||
|
||||
export const continueWithSidecar = async () => {
|
||||
return invoke<void>('continue_with_sidecar')
|
||||
return commands.continueWithSidecar()
|
||||
}
|
||||
|
||||
export const entry_lightweight_mode = async () => {
|
||||
return invoke<void>('entry_lightweight_mode')
|
||||
return commands.entryLightweightMode()
|
||||
}
|
||||
|
||||
export async function getNextUpdateTime(uid: string) {
|
||||
return invoke<number | null>('get_next_update_time', { uid })
|
||||
return commands.getNextUpdateTime(uid)
|
||||
}
|
||||
|
||||
interface ToggleableProxyPort {
|
||||
enabled: boolean
|
||||
port: number
|
||||
}
|
||||
|
||||
export interface ProxyPortSettings {
|
||||
mixedPort: number
|
||||
socks: ToggleableProxyPort
|
||||
http: ToggleableProxyPort
|
||||
redir: ToggleableProxyPort
|
||||
tproxy: ToggleableProxyPort
|
||||
}
|
||||
|
||||
export type ListenerTransport = 'tcp' | 'udp'
|
||||
|
||||
export interface ListenerProbe {
|
||||
address: string
|
||||
transports: ListenerTransport[]
|
||||
}
|
||||
|
||||
export type ListenerProbeOutcome =
|
||||
| { status: 'available' }
|
||||
| {
|
||||
status: 'conflict'
|
||||
port: number
|
||||
transport: ListenerTransport
|
||||
}
|
||||
| { status: 'invalid'; message: string }
|
||||
| { status: 'indeterminate'; message: string }
|
||||
|
||||
export type SaveProxyPortsOutcome =
|
||||
| { status: 'saved' }
|
||||
| { status: 'conflict'; port: number; transport: ListenerTransport }
|
||||
|
||||
export const probeListener = async (request: ListenerProbe) => {
|
||||
return invoke<ListenerProbeOutcome>('probe_listener', { request })
|
||||
return commands.probeListener(request)
|
||||
}
|
||||
|
||||
export const saveProxyPorts = async (settings: ProxyPortSettings) => {
|
||||
return invoke<SaveProxyPortsOutcome>('save_proxy_ports', { settings })
|
||||
}
|
||||
|
||||
export interface UnlockItem {
|
||||
name: string
|
||||
status: string
|
||||
region?: string | null
|
||||
check_time?: string | null
|
||||
return commands.saveProxyPorts(settings)
|
||||
}
|
||||
|
||||
export async function getUnlockItems() {
|
||||
return invoke<UnlockItem[]>('get_unlock_items')
|
||||
return commands.getUnlockItems()
|
||||
}
|
||||
|
||||
export async function checkMediaUnlock(onComplete: Channel<UnlockItem>) {
|
||||
return invoke<UnlockItem[]>('check_media_unlock', { onComplete })
|
||||
return commands.checkMediaUnlock(onComplete)
|
||||
}
|
||||
|
||||
export async function checkMediaUnlockItem(name: string) {
|
||||
return invoke<UnlockItem>('check_media_unlock_item', { name })
|
||||
return commands.checkMediaUnlockItem(name)
|
||||
}
|
||||
|
||||
@@ -1,56 +1 @@
|
||||
// @generated by scripts/gen-contract.mjs from the backend wire contract. DO NOT EDIT.
|
||||
// Regen: pnpm gen:contract
|
||||
|
||||
import type { RunState } from './cmds'
|
||||
|
||||
/** Wire status strings from NoticeStatus; keep in sync via `pnpm gen:contract`. */
|
||||
export type NoticeStatus =
|
||||
| 'info'
|
||||
| 'import_sub_url::ok'
|
||||
| 'import_sub_url::error'
|
||||
| 'set_config::error'
|
||||
| 'core_start::error'
|
||||
| 'service_core::repair_required'
|
||||
| 'service_core::app_data_not_owned'
|
||||
| 'service_core::sidecar_fallback'
|
||||
| 'dns_override::auto_disabled'
|
||||
| 'enhance::discarded_keys'
|
||||
| 'tun_mode::auto_disabled'
|
||||
| 'tun_mode::auto_disable_failed'
|
||||
| 'app_restart::core_stop_failed'
|
||||
| 'app_quit::core_stop_failed'
|
||||
| 'update_with_clash_proxy'
|
||||
| 'update_failed_even_with_clash'
|
||||
| 'reactivate_profiles::error'
|
||||
| 'update_failed'
|
||||
| 'config_validate::boot_error'
|
||||
| 'config_validate::error'
|
||||
| 'config_validate::process_terminated'
|
||||
| 'config_validate::script_error'
|
||||
| 'config_validate::script_syntax_error'
|
||||
| 'config_validate::script_missing_main'
|
||||
| 'config_validate::file_not_found'
|
||||
| 'config_validate::yaml_syntax_error'
|
||||
| 'config_validate::yaml_read_error'
|
||||
| 'config_validate::yaml_mapping_error'
|
||||
| 'config_validate::merge_syntax_error'
|
||||
| 'config_validate::merge_mapping_error'
|
||||
| 'config_core::change_success'
|
||||
| 'config_core::change_error'
|
||||
| 'mixed_port::fallback'
|
||||
| 'mixed_port::fallback_error'
|
||||
|
||||
/** Event name → payload type for every backend-emitted event. */
|
||||
export interface VergeEventPayloads {
|
||||
'verge://refresh-clash-config': string
|
||||
'verge://refresh-verge-config': string
|
||||
'verge://refresh-profiles': string
|
||||
'verge://refresh-proxy-config': null
|
||||
'verge://notice-message': [NoticeStatus, string]
|
||||
'verge://timer-updated': string
|
||||
'verge://run-state-changed': RunState
|
||||
'verge://pending-failures-changed': null
|
||||
'profile-changed': string
|
||||
'profile-update-started': { uid: string }
|
||||
'profile-update-completed': { uid: string }
|
||||
}
|
||||
export type { NoticeStatus, VergeEventPayloads } from './bindings'
|
||||
|
||||
@@ -66,7 +66,6 @@ test('uses newer core history after a tray test without hiding an active page te
|
||||
const cached = delayManager.setDelay(member, 'tray-group', 50)
|
||||
member.node.extra = {
|
||||
[delayManager.getUrl('tray-group')]: {
|
||||
alive: true,
|
||||
history: [
|
||||
{ time: new Date(cached.updatedAt + 1000).toISOString(), delay: 300 },
|
||||
],
|
||||
@@ -107,13 +106,11 @@ test('another URL cannot replace a group measurement, but a newer matching URL c
|
||||
history: [{ time: newer, delay: 126 }],
|
||||
extra: {
|
||||
'http://localhost/a': {
|
||||
alive: true,
|
||||
history: [
|
||||
{ time: new Date(cached.updatedAt - 1000).toISOString(), delay: 49 },
|
||||
],
|
||||
},
|
||||
'http://localhost/b': {
|
||||
alive: true,
|
||||
history: [{ time: newer, delay: 126 }],
|
||||
},
|
||||
},
|
||||
@@ -122,7 +119,6 @@ test('another URL cannot replace a group measurement, but a newer matching URL c
|
||||
Object.assign(member.node, {
|
||||
extra: {
|
||||
'http://localhost/a': {
|
||||
alive: true,
|
||||
history: [{ time: newer, delay: 75 }],
|
||||
},
|
||||
},
|
||||
@@ -190,7 +186,7 @@ test('unmeasured selectors show automatic history without replacing a manual URL
|
||||
{ time: new Date(Date.now() + 1000).toISOString(), delay: 126 },
|
||||
]
|
||||
member.node.extra = {
|
||||
'http://localhost/automatic': { alive: true, history: member.node.history },
|
||||
'http://localhost/automatic': { history: member.node.history },
|
||||
}
|
||||
expect(delayManager.getDelayFix(member, 'selector')).toBe(126)
|
||||
delayManager.setDelay(member, 'selector', 50)
|
||||
|
||||
@@ -305,11 +305,7 @@ function extractDisplayText(input: unknown): string | undefined {
|
||||
}
|
||||
}
|
||||
|
||||
export interface CommandFailure {
|
||||
code?: string
|
||||
operation?: FailedOperation
|
||||
detail: string
|
||||
}
|
||||
import type { CommandFailure_Serialize as CommandFailure } from './bindings'
|
||||
|
||||
function isCommandFailure(input: unknown): input is CommandFailure {
|
||||
if (typeof input !== 'object' || input === null) return false
|
||||
@@ -495,11 +491,11 @@ const baseShowNotice = (
|
||||
}
|
||||
|
||||
function failureCode(input: unknown): string | undefined {
|
||||
return isCommandFailure(input) ? input.code : undefined
|
||||
return isCommandFailure(input) ? input.code! : undefined
|
||||
}
|
||||
|
||||
function failureOperation(input: unknown): FailedOperation | undefined {
|
||||
return isCommandFailure(input) ? input.operation : undefined
|
||||
return isCommandFailure(input) ? input.operation! : undefined
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -32,6 +32,10 @@ test('committed edits retain their profile and merge with fresh saved URLs', asy
|
||||
items: [
|
||||
{
|
||||
uid: 'old-profile',
|
||||
type: null,
|
||||
name: null,
|
||||
file: null,
|
||||
updated: null,
|
||||
latency_test_urls: { untouched: 'https://example.com/' },
|
||||
},
|
||||
],
|
||||
@@ -49,7 +53,16 @@ test('committed edits retain their profile and merge with fresh saved URLs', asy
|
||||
|
||||
test('migration cannot overwrite a previously saved or cleared override', async () => {
|
||||
vi.mocked(getProfiles).mockResolvedValue({
|
||||
items: [{ uid: 'p', latency_test_urls: { A: '' } }],
|
||||
items: [
|
||||
{
|
||||
uid: 'p',
|
||||
type: null,
|
||||
name: null,
|
||||
file: null,
|
||||
updated: null,
|
||||
latency_test_urls: { A: '' },
|
||||
},
|
||||
],
|
||||
})
|
||||
await saveProfileTestUrls('p', { A: 'http://localhost/stale' }, true)
|
||||
expect(patchProfile).not.toHaveBeenCalled()
|
||||
@@ -59,14 +72,18 @@ test('pending edits survive a reload and older save acknowledgements', async ()
|
||||
vi.useFakeTimers()
|
||||
const profile = {
|
||||
uid: 'pending-profile',
|
||||
type: null,
|
||||
name: null,
|
||||
file: null,
|
||||
updated: null,
|
||||
latency_test_urls: { A: 'http://localhost/old' },
|
||||
}
|
||||
vi.mocked(getProfiles).mockResolvedValue({ items: [profile] })
|
||||
let finish!: () => void
|
||||
vi.mocked(patchProfile).mockImplementationOnce(
|
||||
() =>
|
||||
new Promise<void>((resolve) => {
|
||||
finish = resolve
|
||||
new Promise<null>((resolve) => {
|
||||
finish = () => resolve(null)
|
||||
}),
|
||||
)
|
||||
const first = commitProfileTestUrl(profile.uid, 'A', 'http://localhost/new')
|
||||
@@ -90,6 +107,10 @@ test('pending edits survive a reload and older save acknowledgements', async ()
|
||||
test('a failed save preserves the committed input across profile refreshes', async () => {
|
||||
const profile = {
|
||||
uid: 'failed-profile',
|
||||
type: null,
|
||||
name: null,
|
||||
file: null,
|
||||
updated: null,
|
||||
latency_test_urls: { A: 'http://localhost/old' },
|
||||
}
|
||||
vi.mocked(getProfiles).mockResolvedValue({ items: [profile] })
|
||||
|
||||
@@ -53,16 +53,17 @@ const migrating = new Set<string>()
|
||||
|
||||
export function migrateProfileTestUrls(profiles: IProfilesConfig) {
|
||||
for (const profile of profiles.items || []) {
|
||||
if (profile.latency_test_urls !== undefined || migrating.has(profile.uid))
|
||||
continue
|
||||
const urls = readLegacyTestUrls(profile.uid)
|
||||
const uid = profile.uid
|
||||
if (!uid) continue
|
||||
if (profile.latency_test_urls !== undefined || migrating.has(uid)) continue
|
||||
const urls = readLegacyTestUrls(uid)
|
||||
if (!Object.keys(urls).length) continue
|
||||
migrating.add(profile.uid)
|
||||
void saveProfileTestUrls(profile.uid, urls, true)
|
||||
migrating.add(uid)
|
||||
void saveProfileTestUrls(uid, urls, true)
|
||||
.catch((error) => {
|
||||
console.error('Failed to migrate latency test URLs:', error)
|
||||
})
|
||||
.finally(() => migrating.delete(profile.uid))
|
||||
.finally(() => migrating.delete(uid))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -92,10 +93,9 @@ export function profileTestUrls(profile: IProfileItem): Record<string, string> {
|
||||
return {
|
||||
...profile.latency_test_urls,
|
||||
...Object.fromEntries(
|
||||
Object.entries(pending.get(profile.uid) ?? {}).map(([group, edit]) => [
|
||||
group,
|
||||
edit.url,
|
||||
]),
|
||||
Object.entries(
|
||||
(profile.uid ? pending.get(profile.uid) : undefined) ?? {},
|
||||
).map(([group, edit]) => [group, edit.url]),
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
Vendored
+23
-185
@@ -13,10 +13,7 @@ type Platform =
|
||||
|
||||
declare const OS_PLATFORM: Platform
|
||||
|
||||
type ValidationOutcome =
|
||||
| { status: 'valid' | 'busy' }
|
||||
| { status: 'invalid'; kind: string; message: string }
|
||||
| { status: 'skipped'; reason: string }
|
||||
type ValidationOutcome = import('@/services/bindings').ValidationOutcome
|
||||
|
||||
interface IConfigData {
|
||||
port: number
|
||||
@@ -207,83 +204,19 @@ interface IConnectionSetting {
|
||||
layout: 'table' | 'list'
|
||||
}
|
||||
|
||||
interface IClashInfo {
|
||||
mixed_port?: number // clash mixed port
|
||||
socks_port?: number // clash socks port
|
||||
redir_port?: number // clash redir port
|
||||
tproxy_port?: number // clash tproxy port
|
||||
port?: number // clash http port
|
||||
server?: string // external-controller
|
||||
secret?: string
|
||||
}
|
||||
type IClashInfo = import('@/services/bindings').ClashInfo
|
||||
|
||||
interface IProfileItem {
|
||||
latency_test_urls?: Record<string, string>
|
||||
uid: string
|
||||
type?: 'local' | 'remote' | 'merge' | 'script'
|
||||
name?: string
|
||||
desc?: string
|
||||
file?: string
|
||||
url?: string
|
||||
updated?: number
|
||||
selected?: {
|
||||
name?: string
|
||||
now?: string
|
||||
}[]
|
||||
extra?: {
|
||||
upload: number
|
||||
download: number
|
||||
total: number
|
||||
expire: number
|
||||
}
|
||||
option?: IProfileOption
|
||||
home?: string
|
||||
}
|
||||
type IProfileItem = import('@/services/bindings').PrfItem_Serialize
|
||||
|
||||
interface IProfileOption {
|
||||
user_agent?: string
|
||||
with_proxy?: boolean
|
||||
self_proxy?: boolean
|
||||
update_interval?: number
|
||||
timeout_seconds?: number
|
||||
danger_accept_invalid_certs?: boolean
|
||||
allow_auto_update?: boolean
|
||||
merge?: string
|
||||
script?: string
|
||||
rules?: string
|
||||
proxies?: string
|
||||
groups?: string
|
||||
}
|
||||
type IProfileOption = import('@/services/bindings').PrfOption_Deserialize
|
||||
|
||||
interface IProfilesConfig {
|
||||
current?: string
|
||||
items?: IProfileItem[]
|
||||
}
|
||||
type IProfilesConfig = Partial<
|
||||
import('@/services/bindings').IProfiles_Serialize
|
||||
>
|
||||
|
||||
interface IVergeTestItem {
|
||||
uid: string
|
||||
name?: string
|
||||
icon?: string
|
||||
url: string
|
||||
}
|
||||
interface IAddress {
|
||||
V4?: {
|
||||
ip: string
|
||||
broadcast?: string
|
||||
netmask?: string
|
||||
}
|
||||
V6?: {
|
||||
ip: string
|
||||
broadcast?: string
|
||||
netmask?: string
|
||||
}
|
||||
}
|
||||
interface INetworkInterface {
|
||||
name: string
|
||||
addr: IAddress[]
|
||||
mac_addr?: string
|
||||
index: number
|
||||
}
|
||||
type IVergeTestItem = import('@/services/bindings').IVergeTestItem
|
||||
type IAddress = import('@/services/bindings').InterfaceAddress
|
||||
type INetworkInterface = import('@/services/bindings').NetworkInterfaceView
|
||||
|
||||
interface ISeqProfileConfig {
|
||||
prepend: []
|
||||
@@ -851,119 +784,24 @@ interface IProxyConfig
|
||||
| 'sudoku'
|
||||
}
|
||||
|
||||
interface IVergeConfig {
|
||||
app_log_level?: 'trace' | 'debug' | 'info' | 'warn' | 'error' | string
|
||||
app_log_max_size?: number // KB
|
||||
app_log_max_count?: number
|
||||
language?: string
|
||||
tray_event?:
|
||||
| 'main_window'
|
||||
| 'tray_menu'
|
||||
| 'system_proxy'
|
||||
| 'tun_mode'
|
||||
| string
|
||||
env_type?: 'bash' | 'cmd' | 'powershell' | 'fish' | string
|
||||
startup_script?: string
|
||||
start_page?: string
|
||||
clash_core?: string
|
||||
theme_mode?: 'light' | 'dark' | 'system'
|
||||
traffic_graph?: boolean
|
||||
enable_memory_usage?: boolean
|
||||
enable_group_icon?: boolean
|
||||
pause_render_traffic_stats_on_blur?: boolean
|
||||
menu_icon?: 'monochrome' | 'colorful' | 'disable'
|
||||
menu_order?: string[]
|
||||
proxy_group_tools_position?: 'left' | 'right'
|
||||
notice_position?: 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right'
|
||||
collapse_navbar?: boolean
|
||||
tray_icon?: 'monochrome' | 'colorful'
|
||||
common_tray_icon?: boolean
|
||||
sysproxy_tray_icon?: boolean
|
||||
tun_tray_icon?: boolean
|
||||
enable_tray_speed?: boolean
|
||||
tray_proxy_groups_display_mode?: 'default' | 'inline' | 'disable'
|
||||
tray_inline_outbound_modes?: boolean
|
||||
enable_tun_mode?: boolean
|
||||
enable_auto_light_weight_mode?: boolean
|
||||
auto_light_weight_minutes?: number
|
||||
enable_auto_launch?: boolean
|
||||
enable_silent_start?: boolean
|
||||
enable_system_proxy?: boolean
|
||||
enable_global_hotkey?: boolean
|
||||
enable_dns_settings?: boolean
|
||||
profile_dns_settings?: Record<string, { enabled: boolean }>
|
||||
proxy_auto_config?: boolean
|
||||
pac_file_content?: string
|
||||
proxy_host?: string
|
||||
enable_random_port?: boolean
|
||||
verge_mixed_port?: number
|
||||
verge_socks_port?: number
|
||||
verge_redir_port?: number
|
||||
verge_tproxy_port?: number
|
||||
verge_port?: number
|
||||
verge_redir_enabled?: boolean
|
||||
verge_tproxy_enabled?: boolean
|
||||
verge_socks_enabled?: boolean
|
||||
verge_http_enabled?: boolean
|
||||
enable_proxy_guard?: boolean
|
||||
enable_bypass_check?: boolean
|
||||
use_default_bypass?: boolean
|
||||
proxy_guard_duration?: number
|
||||
system_proxy_bypass?: string
|
||||
web_ui_list?: string[]
|
||||
hotkeys?: string[]
|
||||
theme_setting?: {
|
||||
primary_color?: string
|
||||
secondary_color?: string
|
||||
primary_text?: string
|
||||
secondary_text?: string
|
||||
info_color?: string
|
||||
error_color?: string
|
||||
warning_color?: string
|
||||
success_color?: string
|
||||
font_family?: string
|
||||
css_injection?: string
|
||||
background_image?: string
|
||||
background_blend_mode?: string
|
||||
background_opacity?: number
|
||||
}
|
||||
auto_close_connection?: boolean
|
||||
auto_check_update?: boolean
|
||||
default_latency_test?: string
|
||||
default_latency_timeout?: number
|
||||
enable_auto_delay_detection?: boolean
|
||||
auto_delay_detection_interval_minutes?: number
|
||||
enable_builtin_enhanced?: boolean
|
||||
auto_log_clean?: 0 | 1 | 2 | 3 | 4
|
||||
enable_auto_backup_schedule?: boolean
|
||||
auto_backup_interval_hours?: number
|
||||
auto_backup_on_change?: boolean
|
||||
proxy_layout_column?: number
|
||||
test_list?: IVergeTestItem[]
|
||||
webdav_url?: string
|
||||
webdav_username?: string
|
||||
webdav_password?: string
|
||||
home_cards?: Record<string, boolean>
|
||||
enable_hover_jump_navigator?: boolean
|
||||
hover_jump_navigator_delay?: number
|
||||
enable_external_controller?: boolean
|
||||
type IVergeConfig = Omit<
|
||||
Partial<import('@/services/bindings').IVerge_Serialize>,
|
||||
'theme_setting'
|
||||
> & {
|
||||
theme_setting?:
|
||||
| (import('@/services/bindings').IVergeTheme & {
|
||||
background_image?: string
|
||||
background_blend_mode?: string
|
||||
background_opacity?: number
|
||||
})
|
||||
| null
|
||||
}
|
||||
|
||||
interface IWebDavFile {
|
||||
type IWebDavFile = import('@/services/bindings').WebDavFile & {
|
||||
filename: string
|
||||
href: string
|
||||
last_modified: string
|
||||
content_length: number
|
||||
content_type: string
|
||||
tag: string
|
||||
}
|
||||
|
||||
interface ILocalBackupFile {
|
||||
filename: string
|
||||
path: string
|
||||
last_modified: string
|
||||
content_length: number
|
||||
}
|
||||
type ILocalBackupFile = import('@/services/bindings').LocalBackupFile
|
||||
|
||||
interface IWebDavConfig {
|
||||
url: string
|
||||
|
||||
+12
-83
@@ -1,86 +1,15 @@
|
||||
export interface ProxyViewV1 {
|
||||
schemaVersion: 1
|
||||
orderSource: 'runtime' | 'fallback'
|
||||
providerState: 'ready' | 'unavailable'
|
||||
global: ProxyGroupView | null
|
||||
direct: string | null
|
||||
groups: ProxyGroupView[]
|
||||
records: Record<string, ProxyNodeView>
|
||||
standalone: string[]
|
||||
providers: ProxyProviderView[]
|
||||
}
|
||||
|
||||
interface ProxyCapabilities {
|
||||
udp: boolean
|
||||
xudp: boolean
|
||||
tfo: boolean
|
||||
mptcp: boolean
|
||||
smux: boolean
|
||||
}
|
||||
|
||||
interface DelayHistory {
|
||||
time: string
|
||||
delay: number
|
||||
}
|
||||
|
||||
export interface ProxyGroupView extends ProxyCapabilities {
|
||||
name: string
|
||||
type: string
|
||||
alive: boolean
|
||||
now?: string
|
||||
fixed?: string
|
||||
hidden?: boolean
|
||||
icon?: string
|
||||
testUrl?: string
|
||||
history: DelayHistory[]
|
||||
extra?: Record<string, { alive: boolean; history: DelayHistory[] }>
|
||||
members: ProxyMemberRef[]
|
||||
}
|
||||
|
||||
export interface ProxyNodeView extends ProxyCapabilities {
|
||||
recordId: string
|
||||
name: string
|
||||
type: string
|
||||
alive: boolean
|
||||
history: DelayHistory[]
|
||||
extra?: Record<string, { alive: boolean; history: DelayHistory[] }>
|
||||
id?: string
|
||||
hidden?: boolean
|
||||
icon?: string
|
||||
testUrl?: string
|
||||
source:
|
||||
| { kind: 'core'; proxyName: string }
|
||||
| { kind: 'provider'; providerName: string; proxyName: string }
|
||||
}
|
||||
|
||||
type ProxyMemberUnresolvedReason =
|
||||
| 'missing'
|
||||
| 'ambiguous'
|
||||
| 'provider-unavailable'
|
||||
|
||||
export type ProxyMemberRef =
|
||||
| { kind: 'group'; name: string }
|
||||
| { kind: 'node'; name: string; recordId: string }
|
||||
| {
|
||||
kind: 'unresolved'
|
||||
name: string
|
||||
reason: ProxyMemberUnresolvedReason
|
||||
}
|
||||
|
||||
interface ProxyProviderView {
|
||||
name: string
|
||||
vehicleType: 'HTTP' | 'File' | 'Inline'
|
||||
updatedAt?: string
|
||||
subscriptionInfo?: ProxySubscriptionInfo
|
||||
proxyRecordIds: string[]
|
||||
}
|
||||
|
||||
interface ProxySubscriptionInfo {
|
||||
upload: number
|
||||
download: number
|
||||
total: number
|
||||
expire: number
|
||||
}
|
||||
import type {
|
||||
ProxyViewV1_Serialize as ProxyViewV1,
|
||||
ProxyGroupView_Serialize as ProxyGroupView,
|
||||
ProxyNodeView_Serialize as ProxyNodeView,
|
||||
ProxyMemberRef,
|
||||
} from '@/services/bindings'
|
||||
export type {
|
||||
ProxyViewV1_Serialize as ProxyViewV1,
|
||||
ProxyGroupView_Serialize as ProxyGroupView,
|
||||
ProxyNodeView_Serialize as ProxyNodeView,
|
||||
ProxyMemberRef,
|
||||
} from '@/services/bindings'
|
||||
|
||||
export type ResolvedProxyMember =
|
||||
| {
|
||||
|
||||
Reference in New Issue
Block a user