feat(settings): add automatic core startup preference

- add a default-enabled “Start Core on Launch” setting
- persist the preference in the GUI configuration
- automatically start the CPA core when the app launches
- keep the preference independent from manual core start and stop actions
- add multilingual labels and migration tests
This commit is contained in:
lzt404 committed 2026-08-14 01:28:52 +08:00
1 parent 2888042645
commit 30fb184384
11 files changed
+81 -4

No files matched your search

+7 -3
View File
@@ -178,6 +178,7 @@ pub(crate) fn software_settings(
Ok(SoftwareSettings {
close_behavior: config.close_behavior,
autostart_enabled: app_autostart_enabled(app)?,
start_core_on_launch: config.start_core_on_launch,
silent_start_enabled: config.silent_start,
})
}
@@ -206,13 +207,16 @@ pub(crate) fn save_software_settings(
}
let config = if previous_config.close_behavior == settings.close_behavior
&& previous_config.start_core_on_launch == settings.start_core_on_launch
&& previous_config.silent_start == settings.silent_start_enabled
{
previous_config
} else {
match gui_config_state
.set_software_preferences(settings.close_behavior, settings.silent_start_enabled)
{
match gui_config_state.set_software_preferences(
settings.close_behavior,
settings.start_core_on_launch,
settings.silent_start_enabled,
) {
Ok(config) => config,
Err(error) => {
let rollback_error = autostart_changed
+4
View File
@@ -847,6 +847,9 @@ pub(crate) fn load_or_create_gui_config() -> Result<GuiConfigFile, String> {
if presence.close_behavior.is_none() {
changed = true;
}
if presence.start_core_on_launch.is_none() {
changed = true;
}
if presence.silent_start.is_none() {
changed = true;
}
@@ -1178,6 +1181,7 @@ pub(crate) fn write_gui_config_to_path(
("allow-lan", value(config.allow_lan)),
("host", value(config.host.as_str())),
("run-on-startup", value(config.run_on_startup)),
("start-core-on-launch", value(config.start_core_on_launch)),
("silent-start", value(config.silent_start)),
("close-behavior", value(config.close_behavior.as_str())),
("auth-dir", value(config.auth_dir.as_str())),
+4
View File
@@ -1820,6 +1820,10 @@ pub(crate) fn should_start_hidden(config: &GuiConfigFile) -> bool {
config.silent_start && cfg!(any(target_os = "windows", target_os = "macos"))
}
pub(crate) fn should_start_core_on_launch(config: &GuiConfigFile) -> bool {
config.start_core_on_launch
}
pub(crate) fn configure_initial_main_window(
app_handle: &tauri::AppHandle,
start_hidden: bool,
+8 -1
View File
@@ -552,6 +552,7 @@ struct GuiConfigFile {
allow_lan: bool,
host: String,
run_on_startup: bool,
start_core_on_launch: bool,
silent_start: bool,
close_behavior: WindowsCloseBehavior,
window_width: Option<u32>,
@@ -662,6 +663,7 @@ impl Default for GuiConfigFile {
allow_lan: false,
host: "127.0.0.1".to_string(),
run_on_startup: false,
start_core_on_launch: true,
silent_start: false,
close_behavior: WindowsCloseBehavior::Ask,
window_width: Some(DEFAULT_MAIN_WINDOW_WIDTH),
@@ -698,6 +700,7 @@ struct GuiConfigPresence {
api_access_remarks: Option<Vec<GuiApiAccessRemark>>,
management_secret_key: Option<String>,
close_behavior: Option<WindowsCloseBehavior>,
start_core_on_launch: Option<bool>,
silent_start: Option<bool>,
usage_statistics_enabled: Option<bool>,
plugins_enabled: Option<bool>,
@@ -725,6 +728,7 @@ struct GuiSettings {
struct SoftwareSettings {
close_behavior: WindowsCloseBehavior,
autostart_enabled: bool,
start_core_on_launch: bool,
silent_start_enabled: bool,
}
@@ -733,6 +737,7 @@ struct SoftwareSettings {
struct SoftwareSettingsInput {
close_behavior: WindowsCloseBehavior,
autostart_enabled: bool,
start_core_on_launch: bool,
silent_start_enabled: bool,
}
@@ -1505,10 +1510,12 @@ impl GuiConfigState {
fn set_software_preferences(
&self,
close_behavior: WindowsCloseBehavior,
start_core_on_launch: bool,
silent_start: bool,
) -> Result<GuiConfigFile, String> {
self.update(|config| {
config.close_behavior = close_behavior;
config.start_core_on_launch = start_core_on_launch;
config.silent_start = silent_start;
Ok(())
})
@@ -1847,7 +1854,7 @@ fn main() {
Err(error) => eprintln!("自动安装 CPA 离线内核失败: {error}"),
}
if config.run_on_startup {
if should_start_core_on_launch(&config) {
if let Err(error) = start_core_process_inner(process_state.inner(), &config) {
eprintln!("自动启动 CPA 内核失败: {error}");
}
+14
View File
@@ -77,6 +77,7 @@ fn gui_config_defaults_are_stable() {
assert!(content.contains("port = 8317"));
assert!(content.contains("allow-lan = false"));
assert!(content.contains("run-on-startup = false"));
assert!(content.contains("start-core-on-launch = true"));
assert!(content.contains("silent-start = false"));
assert!(content.contains("close-behavior = \"ask\""));
assert!(content.contains("window-width = 1531"));
@@ -119,6 +120,7 @@ fn gui_window_size_round_trips_through_portable_config() {
#[test]
fn silent_start_defaults_off_and_requires_tray_support() {
let legacy = toml::from_str::<GuiConfigFile>("port = 8317\n").unwrap();
assert!(legacy.start_core_on_launch);
assert!(!legacy.silent_start);
assert!(!should_start_hidden(&legacy));
@@ -132,6 +134,18 @@ fn silent_start_defaults_off_and_requires_tray_support() {
);
}
#[test]
fn core_start_on_launch_defaults_on_and_can_be_disabled() {
let legacy = toml::from_str::<GuiConfigFile>("port = 8317\n").unwrap();
assert!(should_start_core_on_launch(&legacy));
let disabled = GuiConfigFile {
start_core_on_launch: false,
..GuiConfigFile::default()
};
assert!(!should_start_core_on_launch(&disabled));
}
#[test]
fn gui_window_size_is_clamped_and_requires_both_dimensions() {
let mut config = GuiConfigFile {
+1
View File
@@ -767,6 +767,7 @@ fn startup_preserves_all_user_owned_yaml_and_only_applies_gui_managed_values() {
allow_lan: true,
host: "0.0.0.0".to_string(),
run_on_startup: false,
start_core_on_launch: true,
silent_start: false,
close_behavior: WindowsCloseBehavior::Ask,
window_width: None,
+1
View File
@@ -3024,6 +3024,7 @@ mod tests {
allow_lan: false,
host: "127.0.0.1".to_string(),
run_on_startup: false,
start_core_on_launch: true,
silent_start: false,
close_behavior: crate::WindowsCloseBehavior::Ask,
window_width: None,
+2
View File
@@ -365,6 +365,8 @@ export const jaOverrides = {
'config.software.title': 'ソフトウェア設定',
'config.software.autostart': 'システム起動時に開始',
'config.software.autostartDescription': 'サインイン後に EasyCLIProxyAPI を自動的に起動します',
'config.software.startCoreOnLaunch': '起動時にコアを開始',
'config.software.startCoreOnLaunchDescription': 'EasyCLIProxyAPI の起動時に CPA コアを自動的に開始します',
'config.software.silentStart': 'サイレント起動',
'config.software.silentStartDescription': 'メインウィンドウを表示せず、システムトレイで直接起動します',
'config.software.closeBehavior': '終了動作',
+2
View File
@@ -300,6 +300,8 @@ export const en: Record<MessageKey, string> = {
'config.software.title': 'Software Settings',
'config.software.autostart': 'Launch at startup',
'config.software.autostartDescription': 'Automatically launch EasyCLIProxyAPI when you sign in',
'config.software.startCoreOnLaunch': 'Start Core on Launch',
'config.software.startCoreOnLaunchDescription': 'Automatically start the CPA core whenever EasyCLIProxyAPI launches',
'config.software.silentStart': 'Silent Launch',
'config.software.silentStartDescription': 'Start without showing the main window and run directly in the system tray',
'config.software.closeBehavior': 'Close behavior',
+2
View File
@@ -298,6 +298,8 @@ export const zhCN = {
'config.software.title': '软件设置',
'config.software.autostart': '开机自启',
'config.software.autostartDescription': '登录系统后自动启动 EasyCLIProxyAPI',
'config.software.startCoreOnLaunch': '启动时启动内核',
'config.software.startCoreOnLaunchDescription': '每次启动 EasyCLIProxyAPI 时自动启动 CPA 内核',
'config.software.silentStart': '静默启动',
'config.software.silentStartDescription': '启动应用时不显示主窗口,直接在系统托盘运行',
'config.software.closeBehavior': '关闭行为',
+36
View File
@@ -18,6 +18,7 @@ import {
Settings2,
ShieldCheck,
Sparkles,
Power,
Trash2,
X,
} from 'lucide-react';
@@ -75,6 +76,7 @@ type NetworkDraftDirty = Record<NetworkDraftField, boolean>;
type SoftwareSettings = {
closeBehavior: CloseBehavior;
autostartEnabled: boolean;
startCoreOnLaunch: boolean;
silentStartEnabled: boolean;
};
@@ -103,6 +105,7 @@ export function ConfigPanelPage() {
const [softwareSettingsLoading, setSoftwareSettingsLoading] = useState(true);
const [softwareCloseBehaviorDraft, setSoftwareCloseBehaviorDraft] = useState<CloseBehavior>('ask');
const [softwareAutostartDraft, setSoftwareAutostartDraft] = useState(false);
const [softwareStartCoreDraft, setSoftwareStartCoreDraft] = useState(true);
const [softwareSilentStartDraft, setSoftwareSilentStartDraft] = useState(false);
const [softwareSavedStatusVisible, setSoftwareSavedStatusVisible] = useState(false);
const [loading, setLoading] = useState(true);
@@ -237,6 +240,7 @@ export function ConfigPanelPage() {
setSoftwareSettings(result);
setSoftwareCloseBehaviorDraft(result.closeBehavior);
setSoftwareAutostartDraft(result.autostartEnabled);
setSoftwareStartCoreDraft(result.startCoreOnLaunch);
setSoftwareSilentStartDraft(result.silentStartEnabled);
} catch (error) {
setSoftwareSettings(null);
@@ -526,6 +530,7 @@ export function ConfigPanelPage() {
if (
softwareCloseBehaviorDraft === softwareSettings.closeBehavior
&& softwareAutostartDraft === softwareSettings.autostartEnabled
&& softwareStartCoreDraft === softwareSettings.startCoreOnLaunch
&& softwareSilentStartDraft === softwareSettings.silentStartEnabled
) return;
@@ -535,18 +540,21 @@ export function ConfigPanelPage() {
settings: {
closeBehavior: softwareCloseBehaviorDraft,
autostartEnabled: softwareAutostartDraft,
startCoreOnLaunch: softwareStartCoreDraft,
silentStartEnabled: softwareSilentStartDraft,
},
});
setSoftwareSettings(result);
setSoftwareCloseBehaviorDraft(result.closeBehavior);
setSoftwareAutostartDraft(result.autostartEnabled);
setSoftwareStartCoreDraft(result.startCoreOnLaunch);
setSoftwareSilentStartDraft(result.silentStartEnabled);
setSoftwareSavedStatusVisible(true);
showNotice(t('config.notice.softwareUpdated'), 'success');
} catch (error) {
setSoftwareCloseBehaviorDraft(softwareSettings.closeBehavior);
setSoftwareAutostartDraft(softwareSettings.autostartEnabled);
setSoftwareStartCoreDraft(softwareSettings.startCoreOnLaunch);
setSoftwareSilentStartDraft(softwareSettings.silentStartEnabled);
setSoftwareSavedStatusVisible(false);
showNotice(t('config.error.saveFailed', { error: String(error) }), 'error');
@@ -585,8 +593,11 @@ export function ConfigPanelPage() {
&& softwareAutostartDraft !== softwareSettings.autostartEnabled;
const softwareSilentStartDirty = softwareSettings !== null
&& softwareSilentStartDraft !== softwareSettings.silentStartEnabled;
const softwareStartCoreDirty = softwareSettings !== null
&& softwareStartCoreDraft !== softwareSettings.startCoreOnLaunch;
const softwareSettingsDirty = softwareCloseBehaviorDirty
|| softwareAutostartDirty
|| softwareStartCoreDirty
|| softwareSilentStartDirty;
const softwareStatusLabel = softwareSettingsLoading
? t('common.loading')
@@ -1272,6 +1283,31 @@ export function ConfigPanelPage() {
<span className="switch-track" />
</label>
</div>
<div className="config-software-setting-row">
<div className="config-software-setting-copy">
<span className="config-software-setting-icon" aria-hidden="true">
<Power size={18} />
</span>
<div>
<strong>{t('config.software.startCoreOnLaunch')}</strong>
<small>{t('config.software.startCoreOnLaunchDescription')}</small>
</div>
</div>
<label className="switch-control" title={t('config.software.startCoreOnLaunch')}>
<input
type="checkbox"
role="switch"
aria-label={t('config.software.startCoreOnLaunch')}
checked={softwareStartCoreDraft}
disabled={softwareSettingsLoading || softwareSettings === null || busyAction !== null}
onChange={(event) => {
setSoftwareSavedStatusVisible(false);
setSoftwareStartCoreDraft(event.currentTarget.checked);
}}
/>
<span className="switch-track" />
</label>
</div>
<div className="config-software-setting-row">
<div className="config-software-setting-copy">
<span className="config-software-setting-icon" aria-hidden="true">