Files
clash-verge-rev/src/services/notice-handlers.ts
T
Tunglies d2180e2d18 feat(events): route all backend events through a single event bus and store
Adds src/services/bus (the only listen/emit site: one handler per contract
event plus the frontend-internal test-all event) and the React
context/reducer app store. Migrates the five scattered subscriptions
(layout events, pending failures, proxy refresh, profile update/timer,
test-all), deletes the old glue (events.ts, use-listen, use-layout-events,
LoadingCacheProvider), moves run state into the store, and adds an
architecture gate script wired into CI. Behavior is unchanged; the
run-state query's poll/focus safety net is preserved by the store
provider.
2026-09-30 20:39:24 +08:00

261 lines
8.9 KiB
TypeScript

import { getCurrentWindow } from '@tauri-apps/api/window'
import {
getCoreStartupError,
takeDiscardedKeysNotice,
takeDnsOverrideNotice,
takeServiceFallbackNotice,
takeServiceOwnerNotice,
takeServiceRepairNotice,
} from '@/services/cmds'
import { hideNotice, showNotice } from '@/services/notice-service'
import { requestService } from '@/services/service-request'
type NavigateFunction = (path: string, options?: any) => void
type TranslateFunction = (key: string) => string
let shownStartupError: string | null = null
// Reads can settle out of order; only the newest may act, and a reset counts as one.
let startupErrorReads = 0
let settledStartupErrorRead = 0
let shownOwnerNotice: number | null = null
/** A running core has resolved every failure shown before it. */
export const forgetShownStartupError = () => {
shownStartupError = null
settledStartupErrorRead = ++startupErrorReads
}
export const handleNoticeMessage = (
status: string,
msg: string,
t: TranslateFunction,
navigate: NavigateFunction,
) => {
const handlers: Record<string, () => void> = {
'import_sub_url::ok': () => {
// 空 msg 传入,我们不希望导致 后端-前端-后端 死循环,这里只做提醒。
// 未来细分事件通知时,可以考虑传入订阅 ID 或其他标识符
// navigate("/profile", { state: { current: msg } });
navigate('/profile')
showNotice.success(
'shared.feedback.notifications.importSubscriptionSuccess',
)
},
'import_sub_url::error': () => {
navigate('/profile')
showNotice.error(msg)
},
'set_config::error': () => showNotice.error(msg),
'core_start::error': () => {
const read = ++startupErrorReads
void getCoreStartupError()
.then(async (failure) => {
if (read < settledStartupErrorRead) return
settledStartupErrorRead = read
if (!failure) {
shownStartupError = null
return
}
const window = getCurrentWindow()
const [visible, minimized] = await Promise.all([
window.isVisible(),
window.isMinimized(),
])
const shown = `${failure.kind}:${failure.detail}`
if (
read === settledStartupErrorRead &&
visible &&
!minimized &&
shownStartupError !== shown
) {
shownStartupError = shown
showNotice.error(
failure.kind === 'serviceCoreStopped'
? 'settings.feedback.errors.clash.serviceCoreStopped'
: 'settings.feedback.errors.clash.startFailed',
failure.detail,
)
}
})
.catch((error) => {
console.error('Failed to read the pending core startup error', error)
})
},
'service_core::repair_required': () => {
void takeServiceRepairNotice()
.then((pending) => {
if (pending) {
requestService({ reason: 'serviceLocationRefused' })
}
})
.catch((error) => {
console.error(
'Failed to read the pending service repair notice',
error,
)
})
},
'service_core::app_data_not_owned': () => {
void takeServiceOwnerNotice()
.then((command) => {
if (command) {
if (shownOwnerNotice !== null) hideNotice(shownOwnerNotice)
shownOwnerNotice = showNotice.warning(
'settings.feedback.notifications.clashService.appDataNotOwned',
{ command },
0,
)
}
})
.catch((error) => {
console.error(
'Failed to read the pending service owner notice',
error,
)
})
},
'service_core::sidecar_fallback': () => {
void takeServiceFallbackNotice()
.then((pending) => {
if (pending) {
showNotice.warning(
'settings.feedback.notifications.clashService.sidecarFallback',
)
}
})
.catch((error) => {
console.error(
'Failed to read the pending service fallback notice',
error,
)
})
},
'dns_override::auto_disabled': () => {
void takeDnsOverrideNotice()
.then((pending) => {
if (pending) {
showNotice.info('settings.modals.dns.protection.autoDisabled')
}
})
.catch((error) => {
console.error('Failed to read the pending DNS override notice', error)
})
},
'enhance::discarded_keys': () => {
void takeDiscardedKeysNotice()
.then((keys) => {
if (keys) {
showNotice.warning('profiles.page.feedback.notices.discardedKeys', {
keys,
})
}
})
.catch((error) => {
console.error(
'Failed to read the pending discarded keys notice',
error,
)
})
},
'tun_mode::auto_disabled': () =>
showNotice.info(
'settings.sections.system.notifications.tunMode.autoDisabled',
),
'tun_mode::auto_disable_failed': () =>
showNotice.error(
'settings.sections.system.notifications.tunMode.autoDisableFailed',
),
'app_restart::core_stop_failed': () =>
showNotice.error(
'layout.feedback.errors.restartCoreStopFailed',
msg || undefined,
),
'app_quit::core_stop_failed': () =>
showNotice.error(
'layout.feedback.errors.quitCoreStopFailed',
msg || undefined,
),
update_with_clash_proxy: () =>
showNotice.success(
'settings.feedback.notifications.updater.withClashProxySuccess',
msg,
),
update_failed_even_with_clash: () =>
showNotice.error(
'settings.feedback.notifications.updater.withClashProxyFailed',
msg,
),
'reactivate_profiles::error': () => showNotice.error(msg),
update_failed: () => showNotice.error(msg),
'config_validate::boot_error': () =>
showNotice.error('shared.feedback.validation.config.bootFailed', msg),
'config_validate::core_change': () =>
showNotice.error(
'shared.feedback.validation.config.coreChangeFailed',
msg,
),
'config_validate::error': () =>
showNotice.error('shared.feedback.validation.config.failed', msg),
'config_validate::process_terminated': () =>
showNotice.error('shared.feedback.validation.config.processTerminated'),
'config_validate::stdout_error': () =>
showNotice.error('shared.feedback.validation.config.failed', msg),
'config_validate::script_error': () =>
showNotice.error('shared.feedback.validation.script.fileError', msg),
'config_validate::script_syntax_error': () =>
showNotice.error('shared.feedback.validation.script.syntaxError', msg),
'config_validate::script_missing_main': () =>
showNotice.error('shared.feedback.validation.script.missingMain', msg),
'config_validate::file_not_found': () =>
showNotice.error('shared.feedback.validation.script.fileNotFound', msg),
'config_validate::yaml_syntax_error': () =>
showNotice.error('shared.feedback.validation.yaml.syntaxError', msg),
'config_validate::yaml_read_error': () =>
showNotice.error('shared.feedback.validation.yaml.readError', msg),
'config_validate::yaml_mapping_error': () =>
showNotice.error('shared.feedback.validation.yaml.mappingError', msg),
'config_validate::yaml_key_error': () =>
showNotice.error('shared.feedback.validation.yaml.keyError', msg),
'config_validate::yaml_error': () =>
showNotice.error('shared.feedback.validation.yaml.generalError', msg),
'config_validate::merge_syntax_error': () =>
showNotice.error('shared.feedback.validation.merge.syntaxError', msg),
'config_validate::merge_mapping_error': () =>
showNotice.error('shared.feedback.validation.merge.mappingError', msg),
'config_validate::merge_key_error': () =>
showNotice.error('shared.feedback.validation.merge.keyError', msg),
'config_validate::merge_error': () =>
showNotice.error('shared.feedback.validation.merge.generalError', msg),
'config_core::change_success': () =>
showNotice.success(
'settings.feedback.notifications.clash.changeSuccess',
msg,
),
'config_core::change_error': () =>
showNotice.error(
'settings.feedback.notifications.clash.changeFailed',
msg,
),
'mixed_port::fallback': () => {
const [oldPort, newPort] = msg.split(',')
showNotice.info('settings.modals.clashPort.messages.automaticFallback', {
oldPort,
newPort,
})
},
'mixed_port::fallback_error': () =>
showNotice.error(
'settings.modals.clashPort.messages.automaticFallbackFailed',
{ error: msg },
),
}
const handler = handlers[status]
if (handler) {
handler()
} else {
console.warn(`未处理的通知状态: ${status}`)
}
}