diff --git a/src-tauri/src/core_config/commands.rs b/src-tauri/src/core_config/commands.rs index 59c07db..ee884bd 100644 --- a/src-tauri/src/core_config/commands.rs +++ b/src-tauri/src/core_config/commands.rs @@ -394,6 +394,25 @@ pub(crate) fn set_core_plugins_enabled( Ok(CoreConfigView::from(&config)) } +#[tauri::command] +pub(crate) fn set_core_request_log( + gui_config_state: tauri::State<'_, GuiConfigState>, + enabled: bool, +) -> Result { + let mut settings = current_core_config_settings(gui_config_state.inner())?; + let previous_enabled = settings.request_log; + settings.request_log = enabled; + patch_core_request_log(settings.request_log)?; + let config = match gui_config_state.sync_core_settings(&settings) { + Ok(config) => config, + Err(error) => { + let rollback_error = patch_core_request_log(previous_enabled).err(); + return Err(config_update_error_with_rollback(error, rollback_error)); + } + }; + Ok(CoreConfigView::from(&config)) +} + #[tauri::command] pub(crate) fn set_core_routing_strategy( gui_config_state: tauri::State<'_, GuiConfigState>, diff --git a/src-tauri/src/core_config/settings.rs b/src-tauri/src/core_config/settings.rs index 0309f1d..f3c2635 100644 --- a/src-tauri/src/core_config/settings.rs +++ b/src-tauri/src/core_config/settings.rs @@ -590,6 +590,11 @@ pub(crate) fn apply_gui_managed_settings( "usage-statistics-enabled", serde_norway::Value::Bool(config.usage_statistics_enabled), )?; + changed |= set_core_yaml_top_level_value( + document, + "request-log", + serde_norway::Value::Bool(config.request_log), + )?; changed |= set_core_yaml_nested_value( document, "remote-management", @@ -1156,6 +1161,7 @@ pub(crate) fn load_or_create_gui_config() -> Result { || presence.api_keys.is_none() || presence.management_secret_key.is_none() || presence.usage_statistics_enabled.is_none() + || presence.request_log.is_none() || presence.plugins_enabled.is_none() || presence.routing_strategy.is_none() || presence.proxy_url.is_none() @@ -1193,6 +1199,9 @@ pub(crate) fn load_or_create_gui_config() -> Result { if presence.usage_statistics_enabled.is_none() { config.usage_statistics_enabled = core_settings.usage_statistics_enabled; } + if presence.request_log.is_none() { + config.request_log = core_settings.request_log; + } if presence.management_secret_key.is_none() { config.management_secret_key = core_settings .management_secret_key @@ -1315,6 +1324,7 @@ pub(crate) fn apply_core_settings_to_gui_config( config.management_secret_key = secret_key.to_string(); } config.usage_statistics_enabled = core_settings.usage_statistics_enabled; + config.request_log = core_settings.request_log; config.plugins_enabled = core_settings.plugins_enabled; config.routing_strategy = core_settings.routing_strategy.clone(); config.proxy_url = core_settings.proxy_url.clone(); @@ -1680,6 +1690,7 @@ pub(crate) fn write_gui_config_to_path( "usage-statistics-enabled", value(config.usage_statistics_enabled), ), + ("request-log", value(config.request_log)), ("plugins-enabled", value(config.plugins_enabled)), ("routing-strategy", value(config.routing_strategy.as_str())), ("proxy-url", value(config.proxy_url.as_str())), diff --git a/src-tauri/src/core_config/yaml.rs b/src-tauri/src/core_config/yaml.rs index 3a25ca1..26f88f9 100644 --- a/src-tauri/src/core_config/yaml.rs +++ b/src-tauri/src/core_config/yaml.rs @@ -269,6 +269,12 @@ pub(crate) fn patch_core_plugins_enabled(enabled: bool) -> Result<(), String> { }) } +pub(crate) fn patch_core_request_log(enabled: bool) -> Result<(), String> { + patch_existing_core_config(|document| { + set_core_yaml_top_level_value(document, "request-log", serde_norway::Value::Bool(enabled)) + }) +} + pub(crate) fn patch_core_routing_strategy(strategy: &str) -> Result<(), String> { let strategy = strategy.to_string(); patch_existing_core_config(move |document| { @@ -1146,6 +1152,14 @@ pub(crate) fn core_config_settings_from_value( }) .transpose()? .unwrap_or(true); + let request_log = yaml_mapping_value(root, "request-log") + .map(|value| { + value + .as_bool() + .ok_or_else(|| "request-log 必须是布尔值".to_string()) + }) + .transpose()? + .unwrap_or(false); let api_keys = extract_core_api_keys(root)? .into_iter() .filter(|api_key| !is_example_core_api_key(api_key)) @@ -1251,6 +1265,7 @@ pub(crate) fn core_config_settings_from_value( .as_deref() .is_some_and(|value| !value.is_empty()), usage_statistics_enabled, + request_log, plugins_enabled, routing_strategy, proxy_url, diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index 8f332b9..7752563 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -595,6 +595,7 @@ struct GuiConfigFile { api_access_remarks: Vec, management_secret_key: String, usage_statistics_enabled: bool, + request_log: bool, plugins_enabled: bool, routing_strategy: String, proxy_url: String, @@ -882,6 +883,7 @@ impl Default for GuiConfigFile { // configuration. Core hashes the value written into config.yaml. management_secret_key: String::new(), usage_statistics_enabled: true, + request_log: false, plugins_enabled: false, routing_strategy: "round-robin".to_string(), proxy_url: String::new(), @@ -916,6 +918,7 @@ struct GuiConfigPresence { start_core_on_launch: Option, silent_start: Option, usage_statistics_enabled: Option, + request_log: Option, plugins_enabled: Option, routing_strategy: Option, proxy_url: Option, @@ -1389,6 +1392,7 @@ struct CoreConfigSettings { management_secret_configured: bool, #[serde(skip_serializing)] usage_statistics_enabled: bool, + request_log: bool, plugins_enabled: bool, routing_strategy: String, proxy_url: String, @@ -1420,6 +1424,7 @@ struct CoreConfigView { management_secret_configured: bool, port: u16, allow_lan: bool, + request_log: bool, plugins_enabled: bool, routing_strategy: String, proxy_url: String, @@ -2034,6 +2039,7 @@ impl GuiConfigState { config.allow_lan = !is_loopback_host(&settings.host); config.auth_dir = settings.auth_dir.clone(); config.usage_statistics_enabled = settings.usage_statistics_enabled; + config.request_log = settings.request_log; if let Some(secret_key) = settings .management_secret_key .as_deref() @@ -2094,6 +2100,7 @@ impl From<&GuiConfigFile> for CoreConfigSettings { api_keys: gui_api_key_values(&config.api_keys), management_secret_configured: !config.management_secret_key.is_empty(), usage_statistics_enabled: config.usage_statistics_enabled, + request_log: config.request_log, plugins_enabled: config.plugins_enabled, routing_strategy: config.routing_strategy.clone(), proxy_url: config.proxy_url.clone(), @@ -2124,6 +2131,7 @@ impl From<&GuiConfigFile> for CoreConfigView { management_secret_configured: !config.management_secret_key.is_empty(), port: config.port, allow_lan: config.allow_lan, + request_log: config.request_log, plugins_enabled: config.plugins_enabled, routing_strategy: config.routing_strategy.clone(), proxy_url: config.proxy_url.clone(), @@ -2436,6 +2444,7 @@ fn main() { get_core_tls_settings, save_core_tls_settings, get_core_config_settings, + set_core_request_log, add_core_api_key, update_core_api_key, delete_core_api_key, diff --git a/src-tauri/src/tests/core_config.rs b/src-tauri/src/tests/core_config.rs index ec617f4..6b1b364 100644 --- a/src-tauri/src/tests/core_config.rs +++ b/src-tauri/src/tests/core_config.rs @@ -86,6 +86,7 @@ fn core_config_view_exposes_api_key_metadata_for_the_webview() { assert_eq!(view["apiKeys"][0]["remark"], DEFAULT_API_KEY_INITIAL_REMARK); assert!(view["apiKeys"][0].get("builtIn").is_none()); assert_eq!(view["managementSecretConfigured"], true); + assert_eq!(view["requestLog"], false); assert!(view.get("managementSecretKey").is_none()); } @@ -336,6 +337,7 @@ fn legacy_gui_config_can_seed_managed_core_settings() { api_keys: vec!["existing-key".to_string()], management_secret_configured: true, usage_statistics_enabled: false, + request_log: true, plugins_enabled: true, routing_strategy: "fill-first".to_string(), proxy_url: "http://127.0.0.1:8080".to_string(), @@ -351,6 +353,7 @@ fn legacy_gui_config_can_seed_managed_core_settings() { assert!(presence.api_keys.is_none()); assert!(presence.management_secret_key.is_none()); + assert!(presence.request_log.is_none()); assert!(presence.plugins_enabled.is_none()); assert!(presence.routing_strategy.is_none()); apply_core_settings_to_gui_config(&mut config, &core_settings); @@ -362,6 +365,7 @@ fn legacy_gui_config_can_seed_managed_core_settings() { assert!(config.disable_cooling); assert_eq!(config.auth_dir, "/tmp/external-auth"); assert!(!config.usage_statistics_enabled); + assert!(config.request_log); assert!(config.plugins_enabled); assert_eq!(config.routing_strategy, "fill-first"); assert_eq!(config.proxy_url, "http://127.0.0.1:8080"); @@ -805,6 +809,16 @@ fn core_config_reads_legacy_api_key_entries() { assert_eq!(settings.routing_strategy, "round-robin"); } +#[test] +fn core_config_reads_request_log_and_defaults_to_disabled() { + let enabled = serde_norway::from_str::("request-log: true\n").unwrap(); + let settings = core_config_settings_from_value(&enabled).unwrap(); + assert!(settings.request_log); + + let defaults = core_config_settings_from_value(&serde_norway::from_str("{}").unwrap()).unwrap(); + assert!(!defaults.request_log); +} + #[test] fn core_config_reads_proxy_and_session_affinity_fields() { let canonical = serde_norway::from_str::( @@ -1048,6 +1062,7 @@ fn startup_preserves_all_user_owned_yaml_and_only_applies_gui_managed_values() { api_access_remarks: Vec::new(), management_secret_key: String::new(), usage_statistics_enabled: false, + request_log: true, plugins_enabled: true, routing_strategy: "fill-first".to_string(), proxy_url: "socks5://127.0.0.1:7890".to_string(), @@ -1090,6 +1105,7 @@ fn startup_preserves_all_user_owned_yaml_and_only_applies_gui_managed_values() { assert_eq!(document["max-retry-interval"], 5); assert_eq!(document["streaming"]["bootstrap-retries"], 1); assert_eq!(document["usage-statistics-enabled"], false); + assert_eq!(document["request-log"], true); assert!(document.get("new-option").is_none()); assert_eq!(document["nested"], current_document["nested"]); assert!(document["nested"].get("added").is_none()); @@ -1162,6 +1178,7 @@ fn startup_merge_without_current_config_uses_gui_defaults() { assert_eq!(document["plugins"]["enabled"], false); assert_eq!(document["routing"]["strategy"], "round-robin"); assert_eq!(document["usage-statistics-enabled"], true); + assert_eq!(document["request-log"], false); assert_eq!( document["remote-management"]["secret-key"], config.management_secret_key diff --git a/src/i18n/ja.ts b/src/i18n/ja.ts index 2f9c57e..998a364 100644 --- a/src/i18n/ja.ts +++ b/src/i18n/ja.ts @@ -349,6 +349,14 @@ export const jaOverrides = { 'config.notice.sessionAffinityEnabled': 'セッションアフィニティを有効にしました', 'config.notice.sessionAffinityDisabled': 'セッションアフィニティを無効にしました', 'config.notice.sessionTtlUpdated': 'セッションアフィニティ TTL を保存しました', + 'config.diagnostics.title': 'ログ診断', + 'config.diagnostics.heading': '詳細なリクエストとレスポンスのログを記録', + 'config.diagnostics.description': '有効にすると、モデル呼び出しや上流通信の問題を調査するために、CPA が詳細なリクエストとレスポンスを保存します。', + 'config.diagnostics.warning': 'ログには機密性の高いリクエスト内容が含まれ、ディスク使用量が増える可能性があります。問題調査時のみ有効にしてください。', + 'config.diagnostics.enabled': '有効', + 'config.diagnostics.disabled': '無効', + 'config.diagnostics.notice.enabled': 'ログ診断を有効にしました', + 'config.diagnostics.notice.disabled': 'ログ診断を無効にしました', 'config.webuiKey.title': 'WebUI キー', 'config.webuiKey.configured': '設定済み', 'config.webuiKey.unconfigured': '未設定', diff --git a/src/i18n/locales/en.ts b/src/i18n/locales/en.ts index 499f6d6..a8aed33 100644 --- a/src/i18n/locales/en.ts +++ b/src/i18n/locales/en.ts @@ -430,6 +430,14 @@ export const en: Record = { 'config.software.behavior.ask': 'Ask every time', 'config.software.behavior.minimize': 'Minimize to Tray', 'config.software.behavior.exit': 'Exit App', + 'config.diagnostics.title': 'Log Diagnostics', + 'config.diagnostics.heading': 'Record detailed request and response logs', + 'config.diagnostics.description': 'When enabled, CPA saves detailed request and response data for troubleshooting model calls and upstream communication.', + 'config.diagnostics.warning': 'Logs may contain sensitive request content and use additional disk space. Enable only while troubleshooting.', + 'config.diagnostics.enabled': 'Enabled', + 'config.diagnostics.disabled': 'Disabled', + 'config.diagnostics.notice.enabled': 'Log diagnostics enabled', + 'config.diagnostics.notice.disabled': 'Log diagnostics disabled', 'config.webuiKey.title': 'WebUI Key', 'config.webuiKey.configured': 'Configured', 'config.webuiKey.unconfigured': 'Not configured', diff --git a/src/i18n/locales/zh-CN.ts b/src/i18n/locales/zh-CN.ts index 545335f..145a112 100644 --- a/src/i18n/locales/zh-CN.ts +++ b/src/i18n/locales/zh-CN.ts @@ -428,6 +428,14 @@ export const zhCN = { 'config.software.behavior.ask': '每次询问', 'config.software.behavior.minimize': '最小化到托盘', 'config.software.behavior.exit': '退出程序', + 'config.diagnostics.title': '日志诊断', + 'config.diagnostics.heading': '记录详细请求与响应日志', + 'config.diagnostics.description': '开启后,CPA 会保存详细请求和响应信息,用于定位模型调用与上游通信问题。', + 'config.diagnostics.warning': '日志可能包含敏感请求内容并增加磁盘占用,仅在排查问题时开启。', + 'config.diagnostics.enabled': '已开启', + 'config.diagnostics.disabled': '已关闭', + 'config.diagnostics.notice.enabled': '日志诊断已开启', + 'config.diagnostics.notice.disabled': '日志诊断已关闭', 'config.webuiKey.title': 'WebUI 密钥', 'config.webuiKey.configured': '已配置', 'config.webuiKey.unconfigured': '未配置', diff --git a/src/pages/ConfigPanel.tsx b/src/pages/ConfigPanel.tsx index 86418eb..527a843 100644 --- a/src/pages/ConfigPanel.tsx +++ b/src/pages/ConfigPanel.tsx @@ -9,6 +9,7 @@ import { Clock3, Eye, EyeOff, + FileText, FolderOpen, KeyRound, Link2, @@ -34,6 +35,7 @@ import { ThinkingAliasesPage } from './ThinkingAliasesPage'; type CoreConfigSettings = { apiKeys: CoreApiKey[]; managementSecretConfigured: boolean; + requestLog: boolean; host: string; port: number; allowLan: boolean; @@ -58,6 +60,7 @@ type ConfigAction = | 'update-key' | 'delete-key' | 'management-secret' + | 'request-log' | 'routing' | 'network' | 'retry' @@ -449,6 +452,18 @@ export function ConfigPanelPage() { } }; + const setRequestLogEnabled = async (enabled: boolean) => { + if (!settings || busyAction !== null) return; + await runMutation( + 'request-log', + 'set_core_request_log', + { enabled }, + enabled + ? t('config.diagnostics.notice.enabled') + : t('config.diagnostics.notice.disabled'), + ); + }; + const confirmDelete = async () => { if (deleteIndex === null) { return; @@ -1105,6 +1120,44 @@ export function ConfigPanelPage() { + +
+
+
+
+ {!loading && settings ? ( + + {settings.requestLog + ? t('config.diagnostics.enabled') + : t('config.diagnostics.disabled')} + + ) : null} +
+ +
+
+ {t('config.diagnostics.heading')} +

{t('config.diagnostics.description')}

+ + +
+ +
+
) : activeSubpage === 'network' || activeSubpage === 'routing' ? (