Dev (#198)
* fix(gitignore): add .codex to ignored files * feat(settings): add configurable default terminal for CLI agents - Add default terminal selector under Advanced Settings > Software Settings - Detect available terminals across macOS, Windows, and Linux - Persist and migrate the default-terminal configuration - Apply the selected terminal when launching CLI agents - Add localized labels and regression tests Fixes #179 * feat(easy-mode): improve API onboarding and agent configuration flow - Fix management API 404 errors when saving API providers - Simplify the model list and support custom model IDs - Adjust API field order and default provider selection - Reuse the full agent configuration page and restore agent detection * refactor(agents): simplify embedded beginner mode UI * style(easy-mode): align onboarding heading typography * fix(easy-mode): refine API onboarding actions and notifications - Reveal model selection and save actions only after fetching models - Move the API save success toast to the bottom-right corner - Auto-dismiss the success notification after a short delay - Remove icons from the model fetch and save buttons * feat(easy-mode): refine guided setup flow and spotlight positioning - Enforce completion of each selection, input, and configuration step - Align OAuth, API access, and agent setup guidance with actual UI actions - Prevent guidance cards from overlapping highlighted areas - Improve bilingual step descriptions and completion states * feat(easy-mode): refine onboarding flow and navigation controls - Enforce accurate step validation for OAuth and API setup - Select all fetched API models by default - Prevent guide cards from overlapping highlighted areas - Improve onboarding copy and remove unnecessary dashboard actions - Reposition and highlight guide and exit controls * fix(usage): filter control messages and repair unknown records - filter empty, null, refresh, and support_refresh messages before persistence - require request_id for usage records - repair Claude cache token accounting - delete historical unknown records - show deleted record count in usage management * fix(ui): improve auth file and core install notifications - show auth file operation notices as auto-dismiss toasts - prevent duplicate core install completion notifications - close the install dialog automatically after successful installation - bump core version to 7.2.146 * fix(ui): improve responsive layouts and core install notifications - add workspace container breakpoints for management and usage pages - make the usage table fill the available viewport height - reduce unnecessary blank space in usage records - silence automatic bundled core install completion notifications - deduplicate manual core install completion feedback * chore(version): update core version to 7.2.147 * fix(management): bypass system proxy for local Core requests * fix(claude-desktop): support Claude OAuth model aliases - Resolve Claude Desktop model sources from all OAuth definitions - Write managed aliases to the matching OAuth channel - Preserve alias metadata and add regression coverage * test(claude-desktop): cover xAI OAuth model aliases * feat(config): add disable cooling toggle to retry settings * feat(config): add disable cooling toggle and refine settings layout * fix(dependencies): update cpa-gui version to 0.2.69
This commit is contained in:
1 parent
ac9f117c2e
commit
708a834e24
18 files changed
+313
-51
No files matched your search
+1
-1
@@ -1 +1 @@
|
||||
7.2.146
|
||||
7.2.147
|
||||
Generated
+1
-1
@@ -593,7 +593,7 @@ checksum = "7704b5fdd17b18ae31c4c1da5a2e0305a2bf17b5249300a9ee9ed7b72114c636"
|
||||
|
||||
[[package]]
|
||||
name = "cpa-gui"
|
||||
version = "0.2.66"
|
||||
version = "0.2.69"
|
||||
dependencies = [
|
||||
"base64 0.22.1",
|
||||
"chrono",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
[package]
|
||||
name = "cpa-gui"
|
||||
# Single source of truth for the desktop app version.
|
||||
version = "0.2.66"
|
||||
version = "0.2.69"
|
||||
description = "CPA desktop management shell"
|
||||
authors = ["lzt404"]
|
||||
edition = "2021"
|
||||
|
||||
@@ -140,7 +140,7 @@ pub(crate) async fn fetch_active_oauth_alias_channels(
|
||||
.header(reqwest::header::ACCEPT, "application/json")
|
||||
.send()
|
||||
.await
|
||||
.map_err(|error| format!("读取 OAuth 凭据来源失败: {error}"))?;
|
||||
.map_err(|error| format_management_request_error("读取 OAuth 凭据来源失败", &error))?;
|
||||
let payload = read_management_value(response).await?;
|
||||
let files = payload
|
||||
.get("files")
|
||||
@@ -218,7 +218,7 @@ pub(crate) async fn fetch_management_config_yaml(config: &GuiConfigFile) -> Resu
|
||||
)
|
||||
.send()
|
||||
.await
|
||||
.map_err(|error| format!("读取内核 YAML 配置失败: {error}"))?;
|
||||
.map_err(|error| format_management_request_error("读取内核 YAML 配置失败", &error))?;
|
||||
read_management_text(response).await
|
||||
}
|
||||
|
||||
@@ -234,7 +234,7 @@ pub(crate) async fn put_management_config_yaml(
|
||||
.body(content.to_string())
|
||||
.send()
|
||||
.await
|
||||
.map_err(|error| format!("保存内核 YAML 配置失败: {error}"))?;
|
||||
.map_err(|error| format_management_request_error("保存内核 YAML 配置失败", &error))?;
|
||||
read_management_value(response).await.map(|_| ())
|
||||
}
|
||||
|
||||
@@ -249,7 +249,7 @@ pub(crate) async fn put_management_oauth_model_aliases(
|
||||
.json(aliases)
|
||||
.send()
|
||||
.await
|
||||
.map_err(|error| format!("保存 OAuth 模型别名失败: {error}"))?;
|
||||
.map_err(|error| format_management_request_error("保存 OAuth 模型别名失败", &error))?;
|
||||
read_management_value(response).await.map(|_| ())
|
||||
}
|
||||
|
||||
@@ -319,11 +319,9 @@ pub(crate) async fn ensure_claude_desktop_model_aliases(
|
||||
let content = fetch_management_config_yaml(config).await?;
|
||||
let updated = match ensure_claude_desktop_model_aliases_in_yaml(&content, mappings, models) {
|
||||
Ok(updated) => updated,
|
||||
Err(source_error) => {
|
||||
let definitions = fetch_codex_model_definitions(config)
|
||||
.await
|
||||
.map_err(|error| format!("{source_error};{error}"))?;
|
||||
ensure_claude_desktop_model_aliases_with_codex_oauth_in_yaml(
|
||||
Err(_) => {
|
||||
let definitions = fetch_oauth_model_definitions(config).await;
|
||||
ensure_claude_desktop_model_aliases_with_oauth_definitions_in_yaml(
|
||||
&content,
|
||||
mappings,
|
||||
models,
|
||||
@@ -347,6 +345,26 @@ pub(crate) fn ensure_claude_desktop_model_aliases_with_codex_oauth_in_yaml(
|
||||
mappings: &ClaudeDesktopModelMappings,
|
||||
models: &[AgentModelOption],
|
||||
codex_oauth_models: &[CodexModelDefinition],
|
||||
) -> Result<String, String> {
|
||||
let codex_channel = oauth_alias_channel("codex")
|
||||
.ok_or_else(|| "缺少 Codex OAuth 别名 channel 定义".to_string())?;
|
||||
let definitions = [OAuthModelDefinitions {
|
||||
channel: codex_channel,
|
||||
models: codex_oauth_models.to_vec(),
|
||||
}];
|
||||
ensure_claude_desktop_model_aliases_with_oauth_definitions_in_yaml(
|
||||
content,
|
||||
mappings,
|
||||
models,
|
||||
&definitions,
|
||||
)
|
||||
}
|
||||
|
||||
pub(crate) fn ensure_claude_desktop_model_aliases_with_oauth_definitions_in_yaml(
|
||||
content: &str,
|
||||
mappings: &ClaudeDesktopModelMappings,
|
||||
models: &[AgentModelOption],
|
||||
oauth_model_definitions: &[OAuthModelDefinitions],
|
||||
) -> Result<String, String> {
|
||||
let mut document = yaml_serde_edit::YamlValue::parse(content)
|
||||
.map_err(|error| format!("解析内核 YAML 配置失败: {error}"))?;
|
||||
@@ -368,7 +386,7 @@ pub(crate) fn ensure_claude_desktop_model_aliases_with_codex_oauth_in_yaml(
|
||||
remove_managed_claude_model_alias(root, alias)?;
|
||||
}
|
||||
} else {
|
||||
ensure_claude_desktop_model_alias(root, source_model, alias, codex_oauth_models)?;
|
||||
ensure_claude_desktop_model_alias(root, source_model, alias, oauth_model_definitions)?;
|
||||
}
|
||||
}
|
||||
render_updated_core_yaml(&mut document, updated)
|
||||
@@ -559,7 +577,7 @@ pub(crate) fn ensure_claude_desktop_model_alias(
|
||||
root: &mut serde_norway::Mapping,
|
||||
source_model: &str,
|
||||
alias: &str,
|
||||
codex_oauth_models: &[CodexModelDefinition],
|
||||
oauth_model_definitions: &[OAuthModelDefinitions],
|
||||
) -> Result<(), String> {
|
||||
if let Some((existing_source, _)) = configured_model_client_identity(root, alias) {
|
||||
if existing_source.eq_ignore_ascii_case(source_model)
|
||||
@@ -572,11 +590,19 @@ pub(crate) fn ensure_claude_desktop_model_alias(
|
||||
if append_claude_desktop_model_alias(root, source_model, alias)? {
|
||||
return Ok(());
|
||||
}
|
||||
if codex_oauth_models
|
||||
.iter()
|
||||
.any(|model| model.id.eq_ignore_ascii_case(source_model))
|
||||
{
|
||||
append_managed_codex_oauth_model_alias(root, source_model, alias)?;
|
||||
if let Some(definition) = oauth_model_definitions.iter().find(|definition| {
|
||||
definition
|
||||
.models
|
||||
.iter()
|
||||
.any(|model| model.id.eq_ignore_ascii_case(source_model))
|
||||
}) {
|
||||
append_managed_oauth_model_alias(
|
||||
root,
|
||||
definition.channel.key,
|
||||
source_model,
|
||||
alias,
|
||||
definition.channel.force_mapping,
|
||||
)?;
|
||||
return Ok(());
|
||||
}
|
||||
Err(format!(
|
||||
@@ -695,21 +721,23 @@ pub(crate) fn append_claude_desktop_model_alias(
|
||||
Ok(false)
|
||||
}
|
||||
|
||||
pub(crate) fn append_managed_codex_oauth_model_alias(
|
||||
pub(crate) fn append_managed_oauth_model_alias(
|
||||
root: &mut serde_norway::Mapping,
|
||||
channel: &str,
|
||||
source_model: &str,
|
||||
alias: &str,
|
||||
force_mapping: bool,
|
||||
) -> Result<(), String> {
|
||||
let oauth_aliases = root
|
||||
.entry(yaml_key("oauth-model-alias"))
|
||||
.or_insert_with(|| serde_norway::Value::Mapping(serde_norway::Mapping::new()))
|
||||
.as_mapping_mut()
|
||||
.ok_or_else(|| "oauth-model-alias 必须是 YAML 映射".to_string())?;
|
||||
let codex_aliases = oauth_aliases
|
||||
.entry(yaml_key("codex"))
|
||||
let channel_aliases = oauth_aliases
|
||||
.entry(yaml_key(channel))
|
||||
.or_insert_with(|| serde_norway::Value::Sequence(Vec::new()))
|
||||
.as_sequence_mut()
|
||||
.ok_or_else(|| "oauth-model-alias.codex 必须是数组".to_string())?;
|
||||
.ok_or_else(|| format!("oauth-model-alias.{channel} 必须是数组"))?;
|
||||
let display_name = managed_claude_alias_display_name(alias)
|
||||
.ok_or_else(|| format!("不支持的 Claude 托管别名: {alias}"))?;
|
||||
let mut alias_mapping = serde_norway::Mapping::new();
|
||||
@@ -722,20 +750,17 @@ pub(crate) fn append_managed_codex_oauth_model_alias(
|
||||
serde_norway::Value::String(alias.to_string()),
|
||||
);
|
||||
alias_mapping.insert(yaml_key("fork"), serde_norway::Value::Bool(true));
|
||||
if force_mapping {
|
||||
alias_mapping.insert(yaml_key("force-mapping"), serde_norway::Value::Bool(true));
|
||||
}
|
||||
alias_mapping.insert(
|
||||
yaml_key("display-name"),
|
||||
serde_norway::Value::String(display_name.to_string()),
|
||||
);
|
||||
codex_aliases.push(serde_norway::Value::Mapping(alias_mapping));
|
||||
channel_aliases.push(serde_norway::Value::Mapping(alias_mapping));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) async fn fetch_codex_model_definitions(
|
||||
config: &GuiConfigFile,
|
||||
) -> Result<Vec<CodexModelDefinition>, String> {
|
||||
fetch_oauth_channel_model_definitions(config, "codex").await
|
||||
}
|
||||
|
||||
pub(crate) async fn fetch_oauth_channel_model_definitions(
|
||||
config: &GuiConfigFile,
|
||||
channel: &str,
|
||||
@@ -750,7 +775,9 @@ pub(crate) async fn fetch_oauth_channel_model_definitions(
|
||||
.header(reqwest::header::ACCEPT, "application/json")
|
||||
.send()
|
||||
.await
|
||||
.map_err(|error| format!("读取 {channel} OAuth 模型定义失败: {error}"))?;
|
||||
.map_err(|error| {
|
||||
format_management_request_error(&format!("读取 {channel} OAuth 模型定义失败"), &error)
|
||||
})?;
|
||||
let payload = read_management_value(response).await?;
|
||||
parse_codex_model_definitions(&payload)
|
||||
}
|
||||
|
||||
@@ -119,6 +119,7 @@ pub(crate) fn save_network_routing_settings(
|
||||
next.proxy_url = proxy_url.clone();
|
||||
next.routing_session_affinity = settings.routing_session_affinity;
|
||||
next.routing_session_affinity_ttl = routing_session_affinity_ttl.clone();
|
||||
next.disable_cooling = settings.disable_cooling;
|
||||
next.request_retry = settings.request_retry;
|
||||
next.max_retry_credentials = settings.max_retry_credentials;
|
||||
next.max_retry_interval = settings.max_retry_interval;
|
||||
@@ -187,6 +188,7 @@ pub(crate) fn save_retry_settings(
|
||||
) -> Result<CoreConfigView, String> {
|
||||
let previous = gui_config_state.snapshot()?;
|
||||
let mut next = previous.clone();
|
||||
next.disable_cooling = settings.disable_cooling;
|
||||
next.request_retry = settings.request_retry;
|
||||
next.max_retry_credentials = settings.max_retry_credentials;
|
||||
next.max_retry_interval = settings.max_retry_interval;
|
||||
|
||||
@@ -415,6 +415,11 @@ pub(crate) fn patch_core_network_routing_yaml(
|
||||
"session-affinity-ttl",
|
||||
serde_norway::Value::String(config.routing_session_affinity_ttl.clone()),
|
||||
)?;
|
||||
set_core_yaml_top_level_value(
|
||||
document,
|
||||
"disable-cooling",
|
||||
serde_norway::Value::Bool(config.disable_cooling),
|
||||
)?;
|
||||
set_core_yaml_top_level_value(
|
||||
document,
|
||||
"request-retry",
|
||||
@@ -466,6 +471,11 @@ pub(crate) fn patch_core_retry_yaml(
|
||||
) -> Result<Option<String>, String> {
|
||||
patch_core_yaml_document(content, |document| {
|
||||
let original = document.clone();
|
||||
set_core_yaml_top_level_value(
|
||||
document,
|
||||
"disable-cooling",
|
||||
serde_norway::Value::Bool(config.disable_cooling),
|
||||
)?;
|
||||
set_core_yaml_top_level_value(
|
||||
document,
|
||||
"request-retry",
|
||||
@@ -615,6 +625,11 @@ pub(crate) fn apply_gui_managed_settings(
|
||||
"session-affinity-ttl",
|
||||
serde_norway::Value::String(config.routing_session_affinity_ttl.clone()),
|
||||
)?;
|
||||
changed |= set_core_yaml_top_level_value(
|
||||
document,
|
||||
"disable-cooling",
|
||||
serde_norway::Value::Bool(config.disable_cooling),
|
||||
)?;
|
||||
changed |= set_core_yaml_top_level_value(
|
||||
document,
|
||||
"request-retry",
|
||||
@@ -1146,6 +1161,7 @@ pub(crate) fn load_or_create_gui_config() -> Result<GuiConfigFile, String> {
|
||||
|| presence.proxy_url.is_none()
|
||||
|| presence.routing_session_affinity.is_none()
|
||||
|| presence.routing_session_affinity_ttl.is_none()
|
||||
|| presence.disable_cooling.is_none()
|
||||
|| presence.request_retry.is_none()
|
||||
|| presence.max_retry_credentials.is_none()
|
||||
|| presence.max_retry_interval.is_none()
|
||||
@@ -1197,6 +1213,9 @@ pub(crate) fn load_or_create_gui_config() -> Result<GuiConfigFile, String> {
|
||||
if presence.routing_session_affinity_ttl.is_none() {
|
||||
config.routing_session_affinity_ttl = core_settings.routing_session_affinity_ttl;
|
||||
}
|
||||
if presence.disable_cooling.is_none() {
|
||||
config.disable_cooling = core_settings.disable_cooling;
|
||||
}
|
||||
if presence.request_retry.is_none() {
|
||||
config.request_retry = core_settings.request_retry;
|
||||
}
|
||||
@@ -1301,6 +1320,7 @@ pub(crate) fn apply_core_settings_to_gui_config(
|
||||
config.proxy_url = core_settings.proxy_url.clone();
|
||||
config.routing_session_affinity = core_settings.routing_session_affinity;
|
||||
config.routing_session_affinity_ttl = core_settings.routing_session_affinity_ttl.clone();
|
||||
config.disable_cooling = core_settings.disable_cooling;
|
||||
config.request_retry = core_settings.request_retry;
|
||||
config.max_retry_credentials = core_settings.max_retry_credentials;
|
||||
config.max_retry_interval = core_settings.max_retry_interval;
|
||||
@@ -1676,6 +1696,7 @@ pub(crate) fn write_gui_config_to_path(
|
||||
"routing-session-affinity-ttl",
|
||||
value(config.routing_session_affinity_ttl.as_str()),
|
||||
),
|
||||
("disable-cooling", value(config.disable_cooling)),
|
||||
("request-retry", value(i64::from(config.request_retry))),
|
||||
(
|
||||
"max-retry-credentials",
|
||||
|
||||
@@ -1197,6 +1197,14 @@ pub(crate) fn core_config_settings_from_value(
|
||||
})
|
||||
.transpose()?
|
||||
.unwrap_or_default();
|
||||
let disable_cooling = yaml_mapping_value(root, "disable-cooling")
|
||||
.map(|value| {
|
||||
value
|
||||
.as_bool()
|
||||
.ok_or_else(|| "disable-cooling 必须是布尔值".to_string())
|
||||
})
|
||||
.transpose()?
|
||||
.unwrap_or(DEFAULT_DISABLE_COOLING);
|
||||
let request_retry = yaml_mapping_value(root, "request-retry")
|
||||
.map(|value| {
|
||||
value
|
||||
@@ -1248,6 +1256,7 @@ pub(crate) fn core_config_settings_from_value(
|
||||
proxy_url,
|
||||
routing_session_affinity,
|
||||
routing_session_affinity_ttl,
|
||||
disable_cooling,
|
||||
request_retry,
|
||||
max_retry_credentials,
|
||||
max_retry_interval,
|
||||
|
||||
+17
-2
@@ -20,8 +20,8 @@ mod usage;
|
||||
#[cfg(test)]
|
||||
use configuration_watcher::nearest_existing_watch_directory;
|
||||
use management_api::{
|
||||
management_authorization, management_endpoint, management_http_client, read_management_text,
|
||||
read_management_value,
|
||||
format_management_request_error, management_authorization, management_endpoint,
|
||||
management_http_client, read_management_text, read_management_value,
|
||||
};
|
||||
use oauth_browser::*;
|
||||
#[cfg(test)]
|
||||
@@ -137,6 +137,7 @@ const DEFAULT_REQUEST_RETRY: u32 = 3;
|
||||
const DEFAULT_MAX_RETRY_CREDENTIALS: u32 = 0;
|
||||
const DEFAULT_MAX_RETRY_INTERVAL: u32 = 30;
|
||||
const DEFAULT_STREAMING_BOOTSTRAP_RETRIES: u32 = 0;
|
||||
const DEFAULT_DISABLE_COOLING: bool = false;
|
||||
const LEGACY_DEFAULT_MANAGEMENT_SECRET_KEY: &str = "123456";
|
||||
const MANAGED_AGENT_PROVIDER_ID: &str = "cpa-gui";
|
||||
const ZCODE_CONFIG_FILE: &str = "config.json";
|
||||
@@ -605,6 +606,7 @@ struct GuiConfigFile {
|
||||
prefer_gitcode_downloads: bool,
|
||||
routing_session_affinity: bool,
|
||||
routing_session_affinity_ttl: String,
|
||||
disable_cooling: bool,
|
||||
request_retry: u32,
|
||||
max_retry_credentials: u32,
|
||||
max_retry_interval: u32,
|
||||
@@ -889,6 +891,7 @@ impl Default for GuiConfigFile {
|
||||
prefer_gitcode_downloads: false,
|
||||
routing_session_affinity: false,
|
||||
routing_session_affinity_ttl: String::new(),
|
||||
disable_cooling: DEFAULT_DISABLE_COOLING,
|
||||
request_retry: DEFAULT_REQUEST_RETRY,
|
||||
max_retry_credentials: DEFAULT_MAX_RETRY_CREDENTIALS,
|
||||
max_retry_interval: DEFAULT_MAX_RETRY_INTERVAL,
|
||||
@@ -922,6 +925,7 @@ struct GuiConfigPresence {
|
||||
prefer_gitcode_downloads: Option<bool>,
|
||||
routing_session_affinity: Option<bool>,
|
||||
routing_session_affinity_ttl: Option<String>,
|
||||
disable_cooling: Option<bool>,
|
||||
request_retry: Option<u32>,
|
||||
max_retry_credentials: Option<u32>,
|
||||
max_retry_interval: Option<u32>,
|
||||
@@ -1330,6 +1334,8 @@ struct GuiNetworkRoutingSettings {
|
||||
proxy_url: String,
|
||||
routing_session_affinity: bool,
|
||||
routing_session_affinity_ttl: String,
|
||||
#[serde(default)]
|
||||
disable_cooling: bool,
|
||||
request_retry: u32,
|
||||
max_retry_credentials: u32,
|
||||
max_retry_interval: u32,
|
||||
@@ -1347,6 +1353,8 @@ struct GuiNetworkEndpointSettings {
|
||||
#[derive(Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct GuiRetrySettings {
|
||||
#[serde(default)]
|
||||
disable_cooling: bool,
|
||||
request_retry: u32,
|
||||
max_retry_credentials: u32,
|
||||
max_retry_interval: u32,
|
||||
@@ -1386,6 +1394,7 @@ struct CoreConfigSettings {
|
||||
proxy_url: String,
|
||||
routing_session_affinity: bool,
|
||||
routing_session_affinity_ttl: String,
|
||||
disable_cooling: bool,
|
||||
request_retry: u32,
|
||||
max_retry_credentials: u32,
|
||||
max_retry_interval: u32,
|
||||
@@ -1416,6 +1425,7 @@ struct CoreConfigView {
|
||||
proxy_url: String,
|
||||
routing_session_affinity: bool,
|
||||
routing_session_affinity_ttl: String,
|
||||
disable_cooling: bool,
|
||||
request_retry: u32,
|
||||
max_retry_credentials: u32,
|
||||
max_retry_interval: u32,
|
||||
@@ -1860,6 +1870,7 @@ impl GuiConfigState {
|
||||
config.proxy_url = settings.proxy_url.clone();
|
||||
config.routing_session_affinity = settings.routing_session_affinity;
|
||||
config.routing_session_affinity_ttl = settings.routing_session_affinity_ttl.clone();
|
||||
config.disable_cooling = settings.disable_cooling;
|
||||
config.request_retry = settings.request_retry;
|
||||
config.max_retry_credentials = settings.max_retry_credentials;
|
||||
config.max_retry_interval = settings.max_retry_interval;
|
||||
@@ -1880,6 +1891,7 @@ impl GuiConfigState {
|
||||
|
||||
fn update_retry_settings(&self, settings: &GuiConfigFile) -> Result<GuiConfigFile, String> {
|
||||
self.update(|config| {
|
||||
config.disable_cooling = settings.disable_cooling;
|
||||
config.request_retry = settings.request_retry;
|
||||
config.max_retry_credentials = settings.max_retry_credentials;
|
||||
config.max_retry_interval = settings.max_retry_interval;
|
||||
@@ -2035,6 +2047,7 @@ impl GuiConfigState {
|
||||
config.proxy_url = settings.proxy_url.clone();
|
||||
config.routing_session_affinity = settings.routing_session_affinity;
|
||||
config.routing_session_affinity_ttl = settings.routing_session_affinity_ttl.clone();
|
||||
config.disable_cooling = settings.disable_cooling;
|
||||
config.request_retry = settings.request_retry;
|
||||
config.max_retry_credentials = settings.max_retry_credentials;
|
||||
config.max_retry_interval = settings.max_retry_interval;
|
||||
@@ -2086,6 +2099,7 @@ impl From<&GuiConfigFile> for CoreConfigSettings {
|
||||
proxy_url: config.proxy_url.clone(),
|
||||
routing_session_affinity: config.routing_session_affinity,
|
||||
routing_session_affinity_ttl: config.routing_session_affinity_ttl.clone(),
|
||||
disable_cooling: config.disable_cooling,
|
||||
request_retry: config.request_retry,
|
||||
max_retry_credentials: config.max_retry_credentials,
|
||||
max_retry_interval: config.max_retry_interval,
|
||||
@@ -2115,6 +2129,7 @@ impl From<&GuiConfigFile> for CoreConfigView {
|
||||
proxy_url: config.proxy_url.clone(),
|
||||
routing_session_affinity: config.routing_session_affinity,
|
||||
routing_session_affinity_ttl: config.routing_session_affinity_ttl.clone(),
|
||||
disable_cooling: config.disable_cooling,
|
||||
request_retry: config.request_retry,
|
||||
max_retry_credentials: config.max_retry_credentials,
|
||||
max_retry_interval: config.max_retry_interval,
|
||||
|
||||
@@ -8,6 +8,7 @@ use super::{
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
error::Error,
|
||||
fs,
|
||||
path::Path,
|
||||
process::{Command, Stdio},
|
||||
@@ -94,7 +95,7 @@ pub(crate) async fn management_request(
|
||||
let response = builder
|
||||
.send()
|
||||
.await
|
||||
.map_err(|err| format!("请求管理 API 失败: {err}"))?;
|
||||
.map_err(|err| format_management_request_error("请求管理 API 失败", &err))?;
|
||||
read_management_value(response).await
|
||||
}
|
||||
|
||||
@@ -121,7 +122,7 @@ pub(crate) async fn upload_auth_file(
|
||||
.body(data)
|
||||
.send()
|
||||
.await
|
||||
.map_err(|err| format!("上传认证文件失败: {err}"))?;
|
||||
.map_err(|err| format_management_request_error("上传认证文件失败", &err))?;
|
||||
read_management_value(response).await
|
||||
}
|
||||
|
||||
@@ -179,7 +180,7 @@ pub(crate) async fn start_oauth_login(
|
||||
let response = request
|
||||
.send()
|
||||
.await
|
||||
.map_err(|err| format!("请求 OAuth 登录链接失败: {err}"))?;
|
||||
.map_err(|err| format_management_request_error("请求 OAuth 登录链接失败", &err))?;
|
||||
let payload = read_management_json::<OAuthStartApiResponse>(response).await?;
|
||||
if let Some(error) = payload
|
||||
.error
|
||||
@@ -228,7 +229,7 @@ pub(crate) async fn get_oauth_status(
|
||||
.query(&[("state", state)])
|
||||
.send()
|
||||
.await
|
||||
.map_err(|err| format!("查询 OAuth 状态失败: {err}"))?;
|
||||
.map_err(|err| format_management_request_error("查询 OAuth 状态失败", &err))?;
|
||||
let payload = read_management_json::<OAuthStatusApiResponse>(response).await?;
|
||||
let status = payload
|
||||
.status
|
||||
@@ -268,12 +269,12 @@ pub(crate) async fn submit_oauth_callback(
|
||||
.json(&body)
|
||||
.send()
|
||||
.await
|
||||
.map_err(|err| format!("提交 OAuth 回调失败: {err}"))?;
|
||||
.map_err(|err| format_management_request_error("提交 OAuth 回调失败", &err))?;
|
||||
let status = response.status();
|
||||
let text = response
|
||||
.text()
|
||||
.await
|
||||
.map_err(|err| format!("读取 OAuth 回调响应失败: {err}"))?;
|
||||
.map_err(|err| format_management_request_error("读取 OAuth 回调响应失败", &err))?;
|
||||
if !status.is_success() {
|
||||
return Err(format_management_error(status.as_u16(), &text));
|
||||
}
|
||||
@@ -282,13 +283,44 @@ pub(crate) async fn submit_oauth_callback(
|
||||
|
||||
pub(crate) fn management_http_client() -> Result<reqwest::Client, String> {
|
||||
reqwest::Client::builder()
|
||||
// GUI-to-Core management traffic must always connect directly. Upstream
|
||||
// traffic still uses Core's proxy-url, and other GUI HTTP clients keep
|
||||
// their independently configured proxy behavior.
|
||||
.no_proxy()
|
||||
.connect_timeout(Duration::from_secs(10))
|
||||
.timeout(Duration::from_secs(30))
|
||||
// Management requests target the configured local listener. This keeps
|
||||
// self-signed/private-CA certificates usable without weakening upstream clients.
|
||||
.danger_accept_invalid_certs(true)
|
||||
.build()
|
||||
.map_err(|err| format!("创建管理 API 客户端失败: {err}"))
|
||||
.map_err(|err| format_management_request_error("创建管理 API 客户端失败", &err))
|
||||
}
|
||||
|
||||
pub(crate) fn format_management_request_error(
|
||||
action: &str,
|
||||
error: &(dyn Error + 'static),
|
||||
) -> String {
|
||||
let mut messages = Vec::new();
|
||||
let mut current = Some(error);
|
||||
|
||||
while let Some(error) = current {
|
||||
let message = error.to_string();
|
||||
if !message.is_empty()
|
||||
&& messages
|
||||
.last()
|
||||
.map(|previous| previous != &message)
|
||||
.unwrap_or(true)
|
||||
{
|
||||
messages.push(message);
|
||||
}
|
||||
current = error.source();
|
||||
}
|
||||
|
||||
if messages.is_empty() {
|
||||
action.to_string()
|
||||
} else {
|
||||
format!("{action}: {}", messages.join(": "))
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn management_authorization(config: &GuiConfigFile) -> Result<String, String> {
|
||||
@@ -342,7 +374,7 @@ where
|
||||
let text = response
|
||||
.text()
|
||||
.await
|
||||
.map_err(|err| format!("读取管理 API 响应失败: {err}"))?;
|
||||
.map_err(|err| format_management_request_error("读取管理 API 响应失败", &err))?;
|
||||
if !status.is_success() {
|
||||
return Err(format_management_error(status.as_u16(), &text));
|
||||
}
|
||||
@@ -364,7 +396,7 @@ pub(crate) async fn read_management_value(
|
||||
let text = response
|
||||
.text()
|
||||
.await
|
||||
.map_err(|err| format!("读取管理 API 响应失败: {err}"))?;
|
||||
.map_err(|err| format_management_request_error("读取管理 API 响应失败", &err))?;
|
||||
if !status.is_success() {
|
||||
return Err(format_management_error(status.as_u16(), &text));
|
||||
}
|
||||
@@ -382,7 +414,7 @@ pub(crate) async fn read_management_text(response: reqwest::Response) -> Result<
|
||||
let text = response
|
||||
.text()
|
||||
.await
|
||||
.map_err(|err| format!("读取管理 API 响应失败: {err}"))?;
|
||||
.map_err(|err| format_management_request_error("读取管理 API 响应失败", &err))?;
|
||||
if !status.is_success() {
|
||||
return Err(format_management_error(status.as_u16(), &text));
|
||||
}
|
||||
|
||||
@@ -1674,6 +1674,104 @@ fn claude_desktop_aliases_support_codex_oauth_models() {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn claude_desktop_aliases_support_claude_oauth_models() {
|
||||
let input = "port: 8317\n";
|
||||
let mappings = ClaudeDesktopModelMappings::all("claude-sonnet-4-6");
|
||||
let available_models = test_agent_models(&["claude-sonnet-4-6"]);
|
||||
let definitions = vec![OAuthModelDefinitions {
|
||||
channel: oauth_alias_channel("claude").unwrap(),
|
||||
models: vec![CodexModelDefinition {
|
||||
id: "claude-sonnet-4-6".to_string(),
|
||||
display_name: None,
|
||||
description: None,
|
||||
context_window: Some(200_000),
|
||||
reasoning_levels: vec!["high".to_string()],
|
||||
supports_tools: Some(true),
|
||||
}],
|
||||
}];
|
||||
|
||||
let rendered = ensure_claude_desktop_model_aliases_with_oauth_definitions_in_yaml(
|
||||
input,
|
||||
&mappings,
|
||||
&available_models,
|
||||
&definitions,
|
||||
)
|
||||
.unwrap();
|
||||
let value: serde_norway::Value = serde_norway::from_str(&rendered).unwrap();
|
||||
let root = value.as_mapping().unwrap();
|
||||
let aliases = yaml_mapping_value(root, "oauth-model-alias")
|
||||
.and_then(serde_norway::Value::as_mapping)
|
||||
.and_then(|channels| yaml_mapping_value(channels, "claude"))
|
||||
.and_then(serde_norway::Value::as_sequence)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(aliases.len(), 3);
|
||||
assert!(aliases.iter().all(|entry| {
|
||||
configured_model_identity(entry).is_some_and(|(source, _, _)| source == "claude-sonnet-4-6")
|
||||
}));
|
||||
assert!(aliases.iter().all(|entry| {
|
||||
entry
|
||||
.as_mapping()
|
||||
.and_then(|entry| yaml_mapping_value(entry, "fork"))
|
||||
== Some(&serde_norway::Value::Bool(true))
|
||||
}));
|
||||
assert_eq!(
|
||||
ensure_claude_desktop_model_aliases_with_oauth_definitions_in_yaml(
|
||||
&rendered,
|
||||
&mappings,
|
||||
&available_models,
|
||||
&definitions,
|
||||
)
|
||||
.unwrap(),
|
||||
rendered
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn claude_desktop_aliases_support_xai_oauth_models() {
|
||||
let input = "port: 8317\n";
|
||||
let mappings = ClaudeDesktopModelMappings::all("grok-4");
|
||||
let available_models = test_agent_models(&["grok-4"]);
|
||||
let definitions = vec![OAuthModelDefinitions {
|
||||
channel: oauth_alias_channel("xai").unwrap(),
|
||||
models: vec![CodexModelDefinition {
|
||||
id: "grok-4".to_string(),
|
||||
display_name: None,
|
||||
description: None,
|
||||
context_window: Some(131_072),
|
||||
reasoning_levels: vec!["high".to_string()],
|
||||
supports_tools: Some(true),
|
||||
}],
|
||||
}];
|
||||
|
||||
let rendered = ensure_claude_desktop_model_aliases_with_oauth_definitions_in_yaml(
|
||||
input,
|
||||
&mappings,
|
||||
&available_models,
|
||||
&definitions,
|
||||
)
|
||||
.unwrap();
|
||||
let value: serde_norway::Value = serde_norway::from_str(&rendered).unwrap();
|
||||
let root = value.as_mapping().unwrap();
|
||||
let aliases = yaml_mapping_value(root, "oauth-model-alias")
|
||||
.and_then(serde_norway::Value::as_mapping)
|
||||
.and_then(|channels| yaml_mapping_value(channels, "xai"))
|
||||
.and_then(serde_norway::Value::as_sequence)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(aliases.len(), 3);
|
||||
assert!(aliases.iter().all(|entry| {
|
||||
configured_model_identity(entry).is_some_and(|(source, _, _)| source == "grok-4")
|
||||
}));
|
||||
assert!(aliases.iter().all(|entry| {
|
||||
entry
|
||||
.as_mapping()
|
||||
.and_then(|entry| yaml_mapping_value(entry, "fork"))
|
||||
== Some(&serde_norway::Value::Bool(true))
|
||||
}));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn claude_oauth_alias_changes_use_the_runtime_refresh_endpoint() {
|
||||
let current = "port: 8317\nopenai-compatibility: []\n";
|
||||
|
||||
@@ -18,6 +18,7 @@ fn gui_field_edit_preserves_comments_and_unknown_configuration() {
|
||||
auth_dir: path_to_string(&home.join("custom-auth")),
|
||||
management_secret_key: "custom-secret".to_string(),
|
||||
usage_statistics_enabled: false,
|
||||
disable_cooling: true,
|
||||
download_source: VersionDownloadSource::Gitcode,
|
||||
prefer_gitcode_downloads: true,
|
||||
..GuiConfigFile::default()
|
||||
@@ -33,6 +34,7 @@ fn gui_field_edit_preserves_comments_and_unknown_configuration() {
|
||||
assert!(content.contains("silent-start = true"));
|
||||
assert!(content.contains("management-secret-key = \"custom-secret\""));
|
||||
assert!(content.contains("usage-statistics-enabled = false"));
|
||||
assert!(content.contains("disable-cooling = true"));
|
||||
assert!(content.contains("download-source = \"gitcode\""));
|
||||
assert!(content.contains("prefer-gitcode-downloads = true"));
|
||||
assert!(!content.contains("codex-session-repair-on-launch"));
|
||||
|
||||
@@ -341,6 +341,7 @@ fn legacy_gui_config_can_seed_managed_core_settings() {
|
||||
proxy_url: "http://127.0.0.1:8080".to_string(),
|
||||
routing_session_affinity: true,
|
||||
routing_session_affinity_ttl: "45m".to_string(),
|
||||
disable_cooling: true,
|
||||
request_retry: 1,
|
||||
max_retry_credentials: 2,
|
||||
max_retry_interval: 5,
|
||||
@@ -358,6 +359,7 @@ fn legacy_gui_config_can_seed_managed_core_settings() {
|
||||
assert_eq!(config.management_secret_key, "management-secret");
|
||||
assert_eq!(config.host, "0.0.0.0");
|
||||
assert_eq!(config.port, 9000);
|
||||
assert!(config.disable_cooling);
|
||||
assert_eq!(config.auth_dir, "/tmp/external-auth");
|
||||
assert!(!config.usage_statistics_enabled);
|
||||
assert!(config.plugins_enabled);
|
||||
@@ -479,6 +481,7 @@ fn confirmed_network_routing_patch_updates_all_fields_together() {
|
||||
proxy_url: "socks5://127.0.0.1:7890".to_string(),
|
||||
routing_session_affinity: true,
|
||||
routing_session_affinity_ttl: "2h".to_string(),
|
||||
disable_cooling: true,
|
||||
request_retry: 1,
|
||||
max_retry_credentials: 2,
|
||||
max_retry_interval: 5,
|
||||
@@ -496,6 +499,7 @@ fn confirmed_network_routing_patch_updates_all_fields_together() {
|
||||
assert_eq!(document["proxy-url"], "socks5://127.0.0.1:7890");
|
||||
assert_eq!(document["routing"]["session-affinity"], true);
|
||||
assert_eq!(document["routing"]["session-affinity-ttl"], "2h");
|
||||
assert_eq!(document["disable-cooling"], true);
|
||||
assert_eq!(document["request-retry"], 1);
|
||||
assert_eq!(document["max-retry-credentials"], 2);
|
||||
assert_eq!(document["max-retry-interval"], 5);
|
||||
@@ -515,13 +519,14 @@ fn independent_network_modules_only_patch_their_own_fields() {
|
||||
proxy_url: "socks5://127.0.0.1:7890".to_string(),
|
||||
routing_session_affinity: true,
|
||||
routing_session_affinity_ttl: "2h".to_string(),
|
||||
disable_cooling: true,
|
||||
request_retry: 7,
|
||||
max_retry_credentials: 8,
|
||||
max_retry_interval: 9,
|
||||
streaming_bootstrap_retries: 10,
|
||||
..GuiConfigFile::default()
|
||||
};
|
||||
let input = "host: 127.0.0.1\nport: 8317\nproxy-url: \"\"\nrouting:\n session-affinity: false\n session-affinity-ttl: 1h\nrequest-retry: 1\nmax-retry-credentials: 2\nmax-retry-interval: 3\nstreaming:\n bootstrap-retries: 4\n";
|
||||
let input = "host: 127.0.0.1\nport: 8317\nproxy-url: \"\"\nrouting:\n session-affinity: false\n session-affinity-ttl: 1h\ndisable-cooling: false\nrequest-retry: 1\nmax-retry-credentials: 2\nmax-retry-interval: 3\nstreaming:\n bootstrap-retries: 4\n";
|
||||
|
||||
let network = patch_core_network_endpoint_yaml(input, &config)
|
||||
.unwrap()
|
||||
@@ -530,6 +535,7 @@ fn independent_network_modules_only_patch_their_own_fields() {
|
||||
assert_eq!(network["host"], "192.168.1.20");
|
||||
assert_eq!(network["port"], 9527);
|
||||
assert_eq!(network["proxy-url"], "socks5://127.0.0.1:7890");
|
||||
assert_eq!(network["disable-cooling"], false);
|
||||
assert_eq!(network["request-retry"], 1);
|
||||
assert_eq!(network["routing"]["session-affinity"], false);
|
||||
|
||||
@@ -537,6 +543,7 @@ fn independent_network_modules_only_patch_their_own_fields() {
|
||||
.unwrap()
|
||||
.expect("retry settings should change");
|
||||
let retry = serde_norway::from_str::<serde_norway::Value>(&retry).unwrap();
|
||||
assert_eq!(retry["disable-cooling"], true);
|
||||
assert_eq!(retry["request-retry"], 7);
|
||||
assert_eq!(retry["max-retry-credentials"], 8);
|
||||
assert_eq!(retry["max-retry-interval"], 9);
|
||||
@@ -826,17 +833,19 @@ fn core_config_reads_proxy_and_session_affinity_fields() {
|
||||
#[test]
|
||||
fn core_config_reads_retry_fields_and_uses_core_defaults() {
|
||||
let document = serde_norway::from_str::<serde_norway::Value>(
|
||||
"request-retry: 1\nmax-retry-credentials: 2\nmax-retry-interval: 5\nstreaming:\n bootstrap-retries: 4\n",
|
||||
"disable-cooling: true\nrequest-retry: 1\nmax-retry-credentials: 2\nmax-retry-interval: 5\nstreaming:\n bootstrap-retries: 4\n",
|
||||
)
|
||||
.unwrap();
|
||||
let settings = core_config_settings_from_value(&document).unwrap();
|
||||
|
||||
assert!(settings.disable_cooling);
|
||||
assert_eq!(settings.request_retry, 1);
|
||||
assert_eq!(settings.max_retry_credentials, 2);
|
||||
assert_eq!(settings.max_retry_interval, 5);
|
||||
assert_eq!(settings.streaming_bootstrap_retries, 4);
|
||||
|
||||
let defaults = core_config_settings_from_value(&serde_norway::from_str("{}").unwrap()).unwrap();
|
||||
assert_eq!(defaults.disable_cooling, DEFAULT_DISABLE_COOLING);
|
||||
assert_eq!(defaults.request_retry, DEFAULT_REQUEST_RETRY);
|
||||
assert_eq!(
|
||||
defaults.max_retry_credentials,
|
||||
@@ -856,6 +865,7 @@ fn managed_session_settings_use_canonical_yaml_and_preserve_unrelated_content()
|
||||
proxy_url: "http://127.0.0.1:8080".to_string(),
|
||||
routing_session_affinity: true,
|
||||
routing_session_affinity_ttl: "1h".to_string(),
|
||||
disable_cooling: true,
|
||||
request_retry: 1,
|
||||
max_retry_credentials: 2,
|
||||
max_retry_interval: 5,
|
||||
@@ -868,6 +878,7 @@ fn managed_session_settings_use_canonical_yaml_and_preserve_unrelated_content()
|
||||
assert_eq!(document["proxy-url"], "http://127.0.0.1:8080");
|
||||
assert_eq!(document["routing"]["session-affinity"], true);
|
||||
assert_eq!(document["routing"]["session-affinity-ttl"], "1h");
|
||||
assert_eq!(document["disable-cooling"], true);
|
||||
assert_eq!(document["request-retry"], 1);
|
||||
assert_eq!(document["max-retry-credentials"], 2);
|
||||
assert_eq!(document["max-retry-interval"], 5);
|
||||
@@ -1046,6 +1057,7 @@ fn startup_preserves_all_user_owned_yaml_and_only_applies_gui_managed_values() {
|
||||
prefer_gitcode_downloads: false,
|
||||
routing_session_affinity: true,
|
||||
routing_session_affinity_ttl: "1h".to_string(),
|
||||
disable_cooling: true,
|
||||
request_retry: 1,
|
||||
max_retry_credentials: 2,
|
||||
max_retry_interval: 5,
|
||||
@@ -1072,6 +1084,7 @@ fn startup_preserves_all_user_owned_yaml_and_only_applies_gui_managed_values() {
|
||||
assert_eq!(document["proxy-url"], "socks5://127.0.0.1:7890");
|
||||
assert_eq!(document["routing"]["session-affinity"], true);
|
||||
assert_eq!(document["routing"]["session-affinity-ttl"], "1h");
|
||||
assert_eq!(document["disable-cooling"], true);
|
||||
assert_eq!(document["request-retry"], 1);
|
||||
assert_eq!(document["max-retry-credentials"], 2);
|
||||
assert_eq!(document["max-retry-interval"], 5);
|
||||
|
||||
@@ -5,8 +5,9 @@ use super::executable_dir;
|
||||
#[cfg(test)]
|
||||
use super::VersionDownloadSource;
|
||||
use super::{
|
||||
apply_configured_proxy, core_base_dir, current_core_status, management_authorization,
|
||||
management_endpoint, management_http_client, CoreProcessState, GuiConfigFile, GuiConfigState,
|
||||
apply_configured_proxy, core_base_dir, current_core_status, format_management_request_error,
|
||||
management_authorization, management_endpoint, management_http_client, CoreProcessState,
|
||||
GuiConfigFile, GuiConfigState,
|
||||
};
|
||||
use chrono::{DateTime, Local};
|
||||
use rusqlite::{
|
||||
@@ -1650,12 +1651,12 @@ async fn fetch_usage_queue(config: &GuiConfigFile) -> Result<Vec<Value>, String>
|
||||
.query(&[("count", USAGE_QUEUE_BATCH_SIZE)])
|
||||
.send()
|
||||
.await
|
||||
.map_err(|error| format!("读取 CPA 使用记录队列失败: {error}"))?;
|
||||
.map_err(|error| format_management_request_error("读取 CPA 使用记录队列失败", &error))?;
|
||||
let status = response.status();
|
||||
let text = response
|
||||
.text()
|
||||
.await
|
||||
.map_err(|error| format!("读取 CPA 使用记录响应失败: {error}"))?;
|
||||
.map_err(|error| format_management_request_error("读取 CPA 使用记录响应失败", &error))?;
|
||||
if !status.is_success() {
|
||||
return Err(format!(
|
||||
"CPA 使用记录队列返回 HTTP {}: {}",
|
||||
@@ -3864,6 +3865,7 @@ mod tests {
|
||||
prefer_gitcode_downloads: false,
|
||||
routing_session_affinity: false,
|
||||
routing_session_affinity_ttl: String::new(),
|
||||
disable_cooling: crate::DEFAULT_DISABLE_COOLING,
|
||||
request_retry: crate::DEFAULT_REQUEST_RETRY,
|
||||
max_retry_credentials: crate::DEFAULT_MAX_RETRY_CREDENTIALS,
|
||||
max_retry_interval: crate::DEFAULT_MAX_RETRY_INTERVAL,
|
||||
|
||||
@@ -406,6 +406,8 @@ export const jaOverrides = {
|
||||
'config.network.sessionAffinityHint': '同じセッションでは以前選択した認証情報を優先します',
|
||||
'config.network.sessionTtl': 'セッションアフィニティ TTL',
|
||||
'config.network.sessionTtlHint': '例:1h。空欄の場合はコアのデフォルト値を使用します',
|
||||
'config.network.disableCooling': 'クールダウンを無効化',
|
||||
'config.network.disableCoolingHint': '認証情報とモデルのクールダウン制御を全体で無効にします',
|
||||
'config.network.requestRetry': 'リクエスト再試行回数',
|
||||
'config.network.requestRetryHint': '403、408、500、502、503、504 応答の再試行回数です。0 で無効になります',
|
||||
'config.network.maxRetryCredentials': '試行する認証情報の上限',
|
||||
|
||||
@@ -478,6 +478,8 @@ export const en: Record<MessageKey, string> = {
|
||||
'config.network.sessionAffinityHint': 'Prefer the previously selected credential for the same session',
|
||||
'config.network.sessionTtl': 'Session Affinity TTL',
|
||||
'config.network.sessionTtlHint': 'For example, 1h; leave blank to use the core default',
|
||||
'config.network.disableCooling': 'Disable Cooling',
|
||||
'config.network.disableCoolingHint': 'Globally disables cooldown scheduling for credentials and models',
|
||||
'config.network.requestRetry': 'Request Retries',
|
||||
'config.network.requestRetryHint': 'Retries responses with status 403, 408, 500, 502, 503, or 504; 0 disables retries',
|
||||
'config.network.maxRetryCredentials': 'Maximum Credentials Tried',
|
||||
|
||||
@@ -476,6 +476,8 @@ export const zhCN = {
|
||||
'config.network.sessionAffinityHint': '同一会话优先复用已选择的凭据',
|
||||
'config.network.sessionTtl': '会话粘性 TTL',
|
||||
'config.network.sessionTtlHint': '例如 1h;留空使用内核默认值',
|
||||
'config.network.disableCooling': '禁用冷却',
|
||||
'config.network.disableCoolingHint': '全局禁用认证凭据和模型的冷却调度',
|
||||
'config.network.requestRetry': '请求重试次数',
|
||||
'config.network.requestRetryHint': '对 403、408、500、502、503、504 响应的重试次数;0 表示不重试',
|
||||
'config.network.maxRetryCredentials': '最大尝试凭据数',
|
||||
|
||||
@@ -41,6 +41,7 @@ type CoreConfigSettings = {
|
||||
proxyUrl: string;
|
||||
routingSessionAffinity: boolean;
|
||||
routingSessionAffinityTtl: string;
|
||||
disableCooling: boolean;
|
||||
requestRetry: number;
|
||||
maxRetryCredentials: number;
|
||||
maxRetryInterval: number;
|
||||
@@ -72,6 +73,7 @@ type NetworkDraftField =
|
||||
| 'proxyUrl'
|
||||
| 'sessionAffinity'
|
||||
| 'sessionTtl'
|
||||
| 'disableCooling'
|
||||
| 'requestRetry'
|
||||
| 'maxRetryCredentials'
|
||||
| 'maxRetryInterval'
|
||||
@@ -106,6 +108,7 @@ const cleanNetworkDraft = (): NetworkDraftDirty => ({
|
||||
proxyUrl: false,
|
||||
sessionAffinity: false,
|
||||
sessionTtl: false,
|
||||
disableCooling: false,
|
||||
requestRetry: false,
|
||||
maxRetryCredentials: false,
|
||||
maxRetryInterval: false,
|
||||
@@ -159,6 +162,7 @@ export function ConfigPanelPage() {
|
||||
const [proxyUrlDraft, setProxyUrlDraft] = useState('');
|
||||
const [sessionAffinityDraft, setSessionAffinityDraft] = useState(false);
|
||||
const [sessionTtlDraft, setSessionTtlDraft] = useState('');
|
||||
const [disableCoolingDraft, setDisableCoolingDraft] = useState(false);
|
||||
const [requestRetryDraft, setRequestRetryDraft] = useState('3');
|
||||
const [maxRetryCredentialsDraft, setMaxRetryCredentialsDraft] = useState('0');
|
||||
const [maxRetryIntervalDraft, setMaxRetryIntervalDraft] = useState('30');
|
||||
@@ -218,6 +222,7 @@ export function ConfigPanelPage() {
|
||||
if (!dirty.proxyUrl) setProxyUrlDraft(result.proxyUrl);
|
||||
if (!dirty.sessionAffinity) setSessionAffinityDraft(result.routingSessionAffinity);
|
||||
if (!dirty.sessionTtl) setSessionTtlDraft(result.routingSessionAffinityTtl);
|
||||
if (!dirty.disableCooling) setDisableCoolingDraft(result.disableCooling);
|
||||
if (!dirty.requestRetry) setRequestRetryDraft(String(result.requestRetry));
|
||||
if (!dirty.maxRetryCredentials) setMaxRetryCredentialsDraft(String(result.maxRetryCredentials));
|
||||
if (!dirty.maxRetryInterval) setMaxRetryIntervalDraft(String(result.maxRetryInterval));
|
||||
@@ -232,6 +237,7 @@ export function ConfigPanelPage() {
|
||||
setProxyUrlDraft(result.proxyUrl);
|
||||
setSessionAffinityDraft(result.routingSessionAffinity);
|
||||
setSessionTtlDraft(result.routingSessionAffinityTtl);
|
||||
setDisableCoolingDraft(result.disableCooling);
|
||||
setRequestRetryDraft(String(result.requestRetry));
|
||||
setMaxRetryCredentialsDraft(String(result.maxRetryCredentials));
|
||||
setMaxRetryIntervalDraft(String(result.maxRetryInterval));
|
||||
@@ -648,12 +654,14 @@ export function ConfigPanelPage() {
|
||||
try {
|
||||
const result = await invoke<CoreConfigSettings>('save_retry_settings', {
|
||||
settings: {
|
||||
disableCooling: disableCoolingDraft,
|
||||
requestRetry,
|
||||
maxRetryCredentials,
|
||||
maxRetryInterval,
|
||||
streamingBootstrapRetries,
|
||||
},
|
||||
});
|
||||
clearDraftDirty('disableCooling');
|
||||
clearDraftDirty('requestRetry');
|
||||
clearDraftDirty('maxRetryCredentials');
|
||||
clearDraftDirty('maxRetryInterval');
|
||||
@@ -747,7 +755,8 @@ export function ConfigPanelPage() {
|
||||
|| sessionTtlDraft.trim() !== settings?.routingSessionAffinityTtl
|
||||
);
|
||||
const retrySettingsDirty = Boolean(settings) && (
|
||||
requestRetryDraft !== String(settings?.requestRetry)
|
||||
disableCoolingDraft !== settings?.disableCooling
|
||||
|| requestRetryDraft !== String(settings?.requestRetry)
|
||||
|| maxRetryCredentialsDraft !== String(settings?.maxRetryCredentials)
|
||||
|| maxRetryIntervalDraft !== String(settings?.maxRetryInterval)
|
||||
|| streamingBootstrapRetriesDraft !== String(settings?.streamingBootstrapRetries)
|
||||
@@ -871,7 +880,7 @@ export function ConfigPanelPage() {
|
||||
|
||||
{activeSubpage === 'general' ? (
|
||||
<div
|
||||
className="config-subpage-panel"
|
||||
className="config-subpage-panel config-general-subpage"
|
||||
id="config-subpage-panel-general"
|
||||
role="tabpanel"
|
||||
aria-labelledby="config-subpage-tab-general"
|
||||
@@ -1330,6 +1339,27 @@ export function ConfigPanelPage() {
|
||||
</button>
|
||||
</div>
|
||||
<div className="config-network-grid">
|
||||
<div className="config-network-field config-network-toggle">
|
||||
<div>
|
||||
<span>{t('config.network.disableCooling')}</span>
|
||||
<small>{t('config.network.disableCoolingHint')}</small>
|
||||
</div>
|
||||
<label className="switch-control" title={t('config.network.disableCooling')}>
|
||||
<input
|
||||
type="checkbox"
|
||||
role="switch"
|
||||
aria-label={t('config.network.disableCooling')}
|
||||
checked={disableCoolingDraft}
|
||||
disabled={controlsDisabled}
|
||||
onChange={(event) => {
|
||||
markDraftDirty('disableCooling');
|
||||
setDisableCoolingDraft(event.currentTarget.checked);
|
||||
}}
|
||||
/>
|
||||
<span className="switch-track" />
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<label className="config-network-field">
|
||||
<span>{t('config.network.requestRetry')}</span>
|
||||
<input
|
||||
|
||||
@@ -1914,6 +1914,11 @@ textarea:focus-visible {
|
||||
transform: translateX(20px);
|
||||
}
|
||||
|
||||
.config-general-subpage {
|
||||
display: grid;
|
||||
gap: 18px;
|
||||
}
|
||||
|
||||
.config-keys-panel {
|
||||
display: grid;
|
||||
min-height: 0;
|
||||
|
||||
Reference in new issue
Block a user