* chore(release): stop publishing legacy Windows update packages

* feat(agents): support OpenCode Desktop across platforms

Detect CLI and desktop installations on Windows, macOS, and Linux, and add separate launch and restart controls.

Closes #156

* fix(codex): align model display names and show total session count

- preserve runtime model identity in generated catalogs
- count distinct sessions across database and rollout sources
- update session count labels in all supported languages

* fix(codex): keep config and auth files in sync

- generate auth.json for API key configuration
- preserve and validate OAuth credentials
- include auth.json in transactional updates and rollback
- retain compatibility with legacy Codex configuration states

* fix(cpa-gui): update version to 0.2.59 in Cargo files

* fix(codex): preserve OAuth mode after closing configuration

* feat(config): add TLS management and protocol-aware local URLs

- add TLS controls to advanced settings
- persist certificate and private key paths
- restart the core after TLS changes
- switch WebUI, API and management URLs between HTTP and HTTPS
- synchronize agent configurations and detection with the active protocol

* refactor(config): reorganize network, routing, and TLS settings

- split network settings and session routing into separate tabs
- move TLS configuration into network settings
- preserve TLS restart and HTTPS URL synchronization behavior

* feat(config): add independent network settings and custom listen IP

* fix(navigation): allow access to usage records while core is stopped

* refactor(home): show a single API URL based on the configured listen IP

* feat(config): add file pickers for TLS certificate and private key
This commit is contained in:
lzt404 authored and GitHub committed 2026-08-23 17:56:13 +08:00
1 parent c3cd57bd8e
commit 2b9ceb11fb
21 files changed
+1355 -261

No files matched your search

+6 -2
View File
@@ -582,12 +582,14 @@ pub(crate) async fn fetch_agent_models(
if port == 0 {
return Err("内核端口无效".to_string());
}
let tls_enabled = managed_core_tls_enabled();
let client = reqwest::Client::builder()
.connect_timeout(Duration::from_secs(3))
.timeout(Duration::from_secs(15))
.danger_accept_invalid_certs(tls_enabled)
.build()
.map_err(|error| format!("创建模型列表客户端失败: {error}"))?;
let base_url = format!("http://127.0.0.1:{port}");
let base_url = managed_core_loopback_origin(port);
let endpoints = [
format!("{base_url}/v1/models"),
format!("{base_url}/models"),
@@ -633,12 +635,14 @@ pub(crate) async fn fetch_codex_runtime_models(
if port == 0 {
return Err("内核端口无效".to_string());
}
let tls_enabled = managed_core_tls_enabled();
let client = reqwest::Client::builder()
.connect_timeout(Duration::from_secs(3))
.timeout(Duration::from_secs(15))
.danger_accept_invalid_certs(tls_enabled)
.build()
.map_err(|error| format!("创建 Codex 模型列表客户端失败: {error}"))?;
let base_url = format!("http://127.0.0.1:{port}");
let base_url = managed_core_loopback_origin(port);
let endpoints = [
format!("{base_url}/v1/models"),
format!("{base_url}/models"),
+1 -1
View File
@@ -42,7 +42,7 @@ pub(crate) fn build_agent_updates_with_oauth(
claude_desktop_model_mappings,
} = options;
let paths = agent_config_paths(client, home);
let root_base = format!("http://127.0.0.1:{port}");
let root_base = managed_core_loopback_origin(port);
let openai_base = format!("{root_base}/v1");
match client {
AgentClient::ClaudeCode => {
+37 -13
View File
@@ -356,7 +356,7 @@ pub(crate) fn inspect_pi_provider_status(
let object = root
.as_object()
.ok_or_else(|| "Pi CLIProxyAPI 配置根节点必须是 JSON 对象".to_string())?;
let expected_base_url = format!("http://127.0.0.1:{port}");
let expected_base_url = managed_core_loopback_origin(port);
credentials_match = object
.get("baseUrl")
.and_then(serde_json::Value::as_str)
@@ -491,7 +491,7 @@ pub(crate) fn repair_pi_provider_inner(
} else {
None
};
let base_url = format!("http://127.0.0.1:{port}");
let base_url = managed_core_loopback_origin(port);
let rendered = build_pi_provider_config(existing.as_deref(), &base_url, api_key)?;
if existing.as_deref() != Some(rendered.as_str()) {
write_bytes_atomically(&config_path, rendered.as_bytes())?;
@@ -1365,7 +1365,31 @@ pub(crate) fn agent_has_managed_marker(
pub(crate) fn is_managed_agent_base_url(value: &str) -> bool {
let value = value.trim().to_ascii_lowercase();
value.starts_with("http://127.0.0.1:") || value.starts_with("http://localhost:")
if value.starts_with("http://127.0.0.1:")
|| value.starts_with("http://localhost:")
|| value.starts_with("https://127.0.0.1:")
|| value.starts_with("https://localhost:")
{
return true;
}
#[cfg(not(test))]
{
let Ok(settings) = read_installed_core_config_settings() else {
return false;
};
let host = core_connect_host(&settings.host);
let host = if host.contains(':') {
format!("[{host}]")
} else {
host
};
value.starts_with(&format!("http://{host}:"))
|| value.starts_with(&format!("https://{host}:"))
}
#[cfg(test)]
{
false
}
}
pub(crate) fn inspect_managed_toml_model_marker(
@@ -2767,7 +2791,7 @@ pub(crate) fn inspect_claude_agent_config(
)
.map_err(|error| format!("解析 Claude Code 配置失败: {error}"))?;
let env = root.get("env").and_then(serde_json::Value::as_object);
let expected_base = format!("http://127.0.0.1:{port}");
let expected_base = managed_core_loopback_origin(port);
let configured = env
.and_then(|env| env.get("ANTHROPIC_BASE_URL"))
.and_then(serde_json::Value::as_str)
@@ -3100,7 +3124,7 @@ pub(crate) fn inspect_claude_desktop_agent_config(
let threep = read_agent_json_or_empty(&paths[1], "Claude Desktop 3P 配置")?;
let profile = read_agent_json_or_empty(&paths[2], "Claude Desktop 网关配置")?;
let meta = read_agent_json_or_empty(&paths[3], "Claude Desktop 配置索引")?;
let expected_base = format!("http://127.0.0.1:{port}");
let expected_base = managed_core_loopback_origin(port);
let configured = normal
.get("deploymentMode")
.and_then(serde_json::Value::as_str)
@@ -3191,7 +3215,7 @@ pub(crate) fn inspect_codex_agent_config(
&fs::read_to_string(path).map_err(|error| format!("读取 Codex 配置失败: {error}"))?,
)
.map_err(|error| format!("解析 Codex 配置失败: {error}"))?;
let expected_base = format!("http://127.0.0.1:{port}/v1");
let expected_base = format!("{}/v1", managed_core_loopback_origin(port));
let provider = root
.get("model_providers")
.and_then(toml::Value::as_table)
@@ -3264,7 +3288,7 @@ pub(crate) fn inspect_opencode_agent_config(
let provider = root
.get("provider")
.and_then(|providers| providers.get(MANAGED_AGENT_PROVIDER_ID));
let expected_base = format!("http://127.0.0.1:{port}/v1");
let expected_base = format!("{}/v1", managed_core_loopback_origin(port));
let configured = provider
.and_then(|provider| provider.get("options"))
.and_then(|options| options.get("baseURL"))
@@ -3301,7 +3325,7 @@ pub(crate) fn inspect_zcode_agent_config(
let provider = root
.get("provider")
.and_then(|providers| providers.get(MANAGED_AGENT_PROVIDER_ID));
let expected_base = format!("http://127.0.0.1:{port}");
let expected_base = managed_core_loopback_origin(port);
let api_format_matches = provider
.and_then(|provider| provider.get("apiFormat"))
.and_then(serde_json::Value::as_str)
@@ -3363,7 +3387,7 @@ pub(crate) fn inspect_kimi_code_agent_config(
&fs::read_to_string(path).map_err(|error| format!("读取 Kimi Code 配置失败: {error}"))?,
)
.map_err(|error| format!("解析 Kimi Code 配置失败: {error}"))?;
let expected_base = format!("http://127.0.0.1:{port}/v1");
let expected_base = format!("{}/v1", managed_core_loopback_origin(port));
let provider = root
.get("providers")
.and_then(toml::Value::as_table)
@@ -3420,7 +3444,7 @@ pub(crate) fn inspect_grok_build_agent_config(
&fs::read_to_string(path).map_err(|error| format!("读取 Grok Build 配置失败: {error}"))?,
)
.map_err(|error| format!("解析 Grok Build 配置失败: {error}"))?;
let expected_base = format!("http://127.0.0.1:{port}/v1");
let expected_base = format!("{}/v1", managed_core_loopback_origin(port));
let prefix = format!("{MANAGED_AGENT_PROVIDER_ID}/");
let selected = root
.get("models")
@@ -3474,7 +3498,7 @@ pub(crate) fn inspect_openclaw_agent_config(
.get("models")
.and_then(|models| models.get("providers"))
.and_then(|providers| providers.get(MANAGED_AGENT_PROVIDER_ID));
let expected_base = format!("http://127.0.0.1:{port}/v1");
let expected_base = format!("{}/v1", managed_core_loopback_origin(port));
let configured = provider
.and_then(|provider| provider.get("baseUrl"))
.and_then(serde_json::Value::as_str)
@@ -3518,7 +3542,7 @@ pub(crate) fn inspect_hermes_agent_config(
== Some(MANAGED_AGENT_PROVIDER_ID)
})
});
let expected_base = format!("http://127.0.0.1:{port}/v1");
let expected_base = format!("{}/v1", managed_core_loopback_origin(port));
let configured = provider
.and_then(|provider| provider.get("base_url"))
.and_then(serde_yaml::Value::as_str)
@@ -3574,7 +3598,7 @@ pub(crate) fn inspect_deepseek_harness_config(
.map(str::to_string)
})
.flatten();
let expected_base = format!("http://127.0.0.1:{port}/v1");
let expected_base = format!("{}/v1", managed_core_loopback_origin(port));
let credential = yaml_mapping_value(&credentials, DEEPSEEK_HARNESS_CREDENTIAL)
.and_then(serde_norway::Value::as_str);
let model_is_published = model.as_deref().is_some_and(|selected| {
+1 -1
View File
@@ -689,7 +689,7 @@ pub(crate) fn fresh_agent_contents_with_oauth(
claude_desktop_model_mappings,
..
} = options;
let root_base = format!("http://127.0.0.1:{port}");
let root_base = managed_core_loopback_origin(port);
let openai_base = format!("{root_base}/v1");
match client {
AgentClient::ClaudeCode => Ok(vec![build_claude_agent_config(