diff --git a/crates/codex-plus-core/src/computer_use_guard.rs b/crates/codex-plus-core/src/computer_use_guard.rs index fa3c09ffd..6cd2ba14e 100644 --- a/crates/codex-plus-core/src/computer_use_guard.rs +++ b/crates/codex-plus-core/src/computer_use_guard.rs @@ -3,7 +3,9 @@ use std::path::{Path, PathBuf}; use anyhow::Context; -use toml_edit::{Array, DocumentMut, Item, Table}; +use toml_edit::{Array, DocumentMut, Item}; + +use crate::toml_helpers::{ensure_trailing_newline, parse_toml_document, table_mut_or_insert}; const BUNDLED_MARKETPLACE: &str = "openai-bundled"; const BUNDLED_MARKETPLACE_PLUGINS: &[&str] = &["browser", "chrome", "computer-use", "latex"]; @@ -736,28 +738,6 @@ fn windows_extended_path(path: &Path) -> String { } } -fn parse_toml_document(contents: &str) -> anyhow::Result { - if contents.trim().is_empty() { - Ok(DocumentMut::new()) - } else { - contents - .parse::() - .with_context(|| "config.toml TOML parse failed") - } -} - -fn table_mut_or_insert<'a>(doc: &'a mut DocumentMut, key: &str) -> anyhow::Result<&'a mut Table> { - if !doc.as_table().contains_key(key) { - doc[key] = toml_edit::table(); - } - if doc.get(key).and_then(Item::as_table).is_none() { - doc[key] = toml_edit::table(); - } - doc.get_mut(key) - .and_then(Item::as_table_mut) - .ok_or_else(|| anyhow::anyhow!("{key} must be a TOML table")) -} - fn ensure_plugin_enabled(doc: &mut DocumentMut, plugin_id: &str) -> anyhow::Result<()> { let plugins = table_mut_or_insert(doc, "plugins")?; if !plugins.contains_key(plugin_id) { @@ -770,13 +750,6 @@ fn ensure_plugin_enabled(doc: &mut DocumentMut, plugin_id: &str) -> anyhow::Resu Ok(()) } -fn ensure_trailing_newline(mut contents: String) -> String { - if !contents.ends_with('\n') { - contents.push('\n'); - } - contents -} - #[cfg(test)] mod tests { use super::*; diff --git a/crates/codex-plus-core/src/env_conflicts.rs b/crates/codex-plus-core/src/env_conflicts.rs index 537ec971c..d3b978e0e 100644 --- a/crates/codex-plus-core/src/env_conflicts.rs +++ b/crates/codex-plus-core/src/env_conflicts.rs @@ -1,6 +1,8 @@ use serde::Serialize; use std::path::PathBuf; +use crate::timestamp::timestamp_millis; + const WINDOWS_USER_ENV_KEY: &str = "Environment"; #[derive(Debug, Clone, PartialEq, Eq, Serialize)] @@ -152,13 +154,6 @@ fn source_order(source: EnvConflictSource) -> u8 { } } -fn timestamp_millis() -> u128 { - std::time::SystemTime::now() - .duration_since(std::time::UNIX_EPOCH) - .unwrap_or_default() - .as_millis() -} - #[cfg(windows)] fn detect_user_env_conflicts() -> Vec { crate::windows_integration::read_current_user_string_values(WINDOWS_USER_ENV_KEY) diff --git a/crates/codex-plus-core/src/json_value.rs b/crates/codex-plus-core/src/json_value.rs new file mode 100644 index 000000000..71fa2ffee --- /dev/null +++ b/crates/codex-plus-core/src/json_value.rs @@ -0,0 +1,25 @@ +use serde_json::Value; + +pub fn json_string_value(value: Option<&Value>) -> String { + match value { + Some(Value::String(value)) => value.trim().to_string(), + Some(Value::Number(value)) => value.to_string(), + _ => String::new(), + } +} + +pub fn required_json_string(raw: &Value, key: &str) -> Option { + raw.get(key) + .and_then(Value::as_str) + .map(str::trim) + .filter(|value| !value.is_empty()) + .map(ToOwned::to_owned) +} + +pub fn optional_json_string(raw: &Value, key: &str) -> String { + raw.get(key) + .and_then(Value::as_str) + .map(str::trim) + .unwrap_or_default() + .to_string() +} diff --git a/crates/codex-plus-core/src/lib.rs b/crates/codex-plus-core/src/lib.rs index 2c88721b7..5279582ea 100644 --- a/crates/codex-plus-core/src/lib.rs +++ b/crates/codex-plus-core/src/lib.rs @@ -11,6 +11,7 @@ pub mod diagnostic_log; pub mod env_conflicts; pub mod http_client; pub mod install; +pub mod json_value; pub mod launcher; pub mod model_catalog; pub mod models; @@ -25,6 +26,8 @@ pub mod routes; pub mod script_market; pub mod settings; pub mod status; +pub mod timestamp; +pub mod toml_helpers; pub mod update; pub mod upstream_worktree; pub mod user_scripts; diff --git a/crates/codex-plus-core/src/model_catalog.rs b/crates/codex-plus-core/src/model_catalog.rs index b0723a06a..e9bb1424d 100644 --- a/crates/codex-plus-core/src/model_catalog.rs +++ b/crates/codex-plus-core/src/model_catalog.rs @@ -2,6 +2,7 @@ use std::collections::{HashMap, HashSet}; use std::path::{Path, PathBuf}; use crate::settings::{RelayProfile, SettingsStore}; +use crate::toml_helpers::unquote_toml_string; use serde_json::{Value, json}; const BASE_URL_ENV_KEYS: &[&str] = &[ @@ -757,22 +758,3 @@ fn string_value(value: Option<&String>) -> String { .map(|value| value.trim().to_string()) .unwrap_or_default() } - -fn unquote_toml_string(value: &str) -> String { - let value = value.trim(); - if let Ok(parsed) = toml::from_str::(&format!("value = {value}")) { - if let Some(value) = parsed.get("value").and_then(toml::Value::as_str) { - return value.to_string(); - } - } - value - .strip_prefix('"') - .and_then(|value| value.strip_suffix('"')) - .or_else(|| { - value - .strip_prefix('\'') - .and_then(|value| value.strip_suffix('\'')) - }) - .unwrap_or(value) - .to_string() -} diff --git a/crates/codex-plus-core/src/relay_config.rs b/crates/codex-plus-core/src/relay_config.rs index 0c0b606ad..e464c22bf 100644 --- a/crates/codex-plus-core/src/relay_config.rs +++ b/crates/codex-plus-core/src/relay_config.rs @@ -3,9 +3,14 @@ use serde::Serialize; use serde_json::{Value, json}; use std::collections::HashSet; use std::path::{Path, PathBuf}; -use std::time::{SystemTime, UNIX_EPOCH}; use toml_edit::{DocumentMut, Item, Table, TableLike}; +use crate::timestamp::timestamp_millis; +use crate::toml_helpers::{ + ensure_trailing_newline, normalize_trimmed_text, parse_toml_document, table_mut_or_insert, + unquote_toml_string_simple as unquote_toml_string, +}; + use crate::settings::{RelayContextSelection, RelayProfile, RelayProtocol}; const RELAY_PROVIDER: &str = "custom"; @@ -142,18 +147,6 @@ fn set_codex_goals_feature_text_fallback(existing: &str, enabled: bool) -> Strin ensure_trailing_newline(updated) } -fn table_mut_or_insert<'a>(doc: &'a mut DocumentMut, key: &str) -> anyhow::Result<&'a mut Table> { - if !doc.as_table().contains_key(key) { - doc[key] = toml_edit::table(); - } - if doc.get(key).and_then(Item::as_table).is_none() { - doc[key] = toml_edit::table(); - } - doc.get_mut(key) - .and_then(Item::as_table_mut) - .ok_or_else(|| anyhow::anyhow!("{key} 必须是 TOML table")) -} - fn table_mut_if_exists<'a>(doc: &'a mut DocumentMut, key: &str) -> Option<&'a mut Table> { doc.get_mut(key).and_then(Item::as_table_mut) } @@ -1128,16 +1121,6 @@ fn provider_table_exists(doc: &DocumentMut, provider_id: &str) -> bool { .is_some() } -fn parse_toml_document(contents: &str) -> anyhow::Result { - if contents.trim().is_empty() { - Ok(DocumentMut::new()) - } else { - contents - .parse::() - .with_context(|| "config.toml TOML 解析失败") - } -} - fn remove_provider_specific_common_keys(table: &mut dyn TableLike) { for key in [ "model", @@ -1188,16 +1171,7 @@ fn sanitize_common_config_text_fallback(common_config: &str) -> String { kept.push(line); } - normalize_text_toml(kept.join("\n")) -} - -fn normalize_text_toml(contents: String) -> String { - let trimmed = contents.trim(); - if trimmed.is_empty() { - String::new() - } else { - ensure_trailing_newline(trimmed.to_string()) - } + normalize_trimmed_text(kept.join("\n")) } pub fn normalize_config_text(contents: &str) -> String { @@ -1241,7 +1215,7 @@ fn normalize_duplicate_toml_text(contents: &str) -> String { kept.push(line); } - normalize_text_toml(kept.join("\n")) + normalize_trimmed_text(kept.join("\n")) } fn strip_common_config_text_fallback(config_text: &str, common_config: &str) -> String { @@ -1280,7 +1254,7 @@ fn strip_common_config_text_fallback(config_text: &str, common_config: &str) -> kept.push(line); } - normalize_text_toml(kept.join("\n")) + normalize_trimmed_text(kept.join("\n")) } struct CommonConfigAnchors { @@ -2177,20 +2151,6 @@ fn create_live_backup( Ok(Some(backup_dir.to_string_lossy().to_string())) } -fn timestamp_millis() -> u128 { - SystemTime::now() - .duration_since(UNIX_EPOCH) - .unwrap_or_default() - .as_millis() -} - -fn ensure_trailing_newline(mut contents: String) -> String { - if !contents.ends_with('\n') { - contents.push('\n'); - } - contents -} - fn move_model_providers_before_profiles(contents: &str) -> String { let lines = contents.lines().collect::>(); let Some(provider_start) = lines @@ -2462,15 +2422,6 @@ fn table_values(contents: &str, table: &str) -> Option String { - let value = value.trim(); - value - .strip_prefix('"') - .and_then(|value| value.strip_suffix('"')) - .unwrap_or(value) - .to_string() -} - fn root_line_key(line: &str) -> Option<&str> { let trimmed = line.trim(); if trimmed.starts_with('#') || trimmed.starts_with('[') { diff --git a/crates/codex-plus-core/src/script_market.rs b/crates/codex-plus-core/src/script_market.rs index 35f51db50..5447f748f 100644 --- a/crates/codex-plus-core/src/script_market.rs +++ b/crates/codex-plus-core/src/script_market.rs @@ -2,6 +2,7 @@ use anyhow::Context; use serde::{Deserialize, Serialize}; use serde_json::Value; +use crate::json_value::{optional_json_string, required_json_string}; use crate::user_scripts::UserScriptManager; pub const DEFAULT_MARKET_INDEX_URL: &str = @@ -109,16 +110,16 @@ pub async fn install_market_script( } fn parse_market_script(raw: Value) -> Option { - let id = required_string(&raw, "id")?; - let name = required_string(&raw, "name")?; - let version = required_string(&raw, "version")?; - let script_url = required_string(&raw, "script_url")?; + let id = required_json_string(&raw, "id")?; + let name = required_json_string(&raw, "name")?; + let version = required_json_string(&raw, "version")?; + let script_url = required_json_string(&raw, "script_url")?; Some(MarketScript { id, name, - description: optional_string(&raw, "description"), + description: optional_json_string(&raw, "description"), version, - author: optional_string(&raw, "author"), + author: optional_json_string(&raw, "author"), tags: raw .get("tags") .and_then(Value::as_array) @@ -132,24 +133,10 @@ fn parse_market_script(raw: Value) -> Option { .collect() }) .unwrap_or_default(), - homepage: optional_string(&raw, "homepage"), + homepage: optional_json_string(&raw, "homepage"), script_url, - sha256: optional_string(&raw, "sha256"), + sha256: optional_json_string(&raw, "sha256"), }) } -fn required_string(raw: &Value, key: &str) -> Option { - raw.get(key) - .and_then(Value::as_str) - .map(str::trim) - .filter(|value| !value.is_empty()) - .map(ToOwned::to_owned) -} -fn optional_string(raw: &Value, key: &str) -> String { - raw.get(key) - .and_then(Value::as_str) - .map(str::trim) - .unwrap_or_default() - .to_string() -} diff --git a/crates/codex-plus-core/src/settings.rs b/crates/codex-plus-core/src/settings.rs index 37dacdffd..b83678f99 100644 --- a/crates/codex-plus-core/src/settings.rs +++ b/crates/codex-plus-core/src/settings.rs @@ -7,6 +7,7 @@ use serde::Deserialize; use serde_json::{Map, Value}; use toml_edit::{DocumentMut, Item}; +use crate::toml_helpers::{normalize_trimmed_text, parse_toml_document}; use crate::zed_remote::ZedOpenStrategy; #[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize, Default)] @@ -851,16 +852,6 @@ fn active_provider_id(doc: &DocumentMut) -> Option { .map(ToString::to_string) } -fn parse_toml_document(contents: &str) -> anyhow::Result { - if contents.trim().is_empty() { - Ok(DocumentMut::new()) - } else { - contents - .parse::() - .with_context(|| "config.toml TOML 解析失败") - } -} - fn settings_to_object(settings: &BackendSettings) -> Map { match serde_json::to_value(settings).unwrap_or_else(|_| Value::Object(Map::new())) { Value::Object(map) => map, @@ -903,8 +894,8 @@ fn split_context_config_sections(config: &str) -> (String, String) { } ( - normalize_text_config(common.join("\n")), - normalize_text_config(context.join("\n")), + normalize_trimmed_text(common.join("\n")), + normalize_trimmed_text(context.join("\n")), ) } @@ -921,16 +912,7 @@ fn join_config_sections(sections: &[&str]) -> String { .filter(|section| !section.is_empty()) .collect::>() .join("\n\n"); - normalize_text_config(joined) -} - -fn normalize_text_config(contents: String) -> String { - let trimmed = contents.trim(); - if trimmed.is_empty() { - String::new() - } else { - format!("{trimmed}\n") - } + normalize_trimmed_text(joined) } pub(crate) fn atomic_write(path: &Path, bytes: &[u8]) -> anyhow::Result<()> { diff --git a/crates/codex-plus-core/src/timestamp.rs b/crates/codex-plus-core/src/timestamp.rs new file mode 100644 index 000000000..605670731 --- /dev/null +++ b/crates/codex-plus-core/src/timestamp.rs @@ -0,0 +1,15 @@ +use std::time::{SystemTime, UNIX_EPOCH}; + +pub fn timestamp_millis() -> u128 { + SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap_or_default() + .as_millis() +} + +pub fn timestamp_secs_string() -> String { + SystemTime::now() + .duration_since(UNIX_EPOCH) + .map(|value| value.as_secs().to_string()) + .unwrap_or_else(|_| "0".to_string()) +} diff --git a/crates/codex-plus-core/src/toml_helpers.rs b/crates/codex-plus-core/src/toml_helpers.rs new file mode 100644 index 000000000..6d8c62042 --- /dev/null +++ b/crates/codex-plus-core/src/toml_helpers.rs @@ -0,0 +1,71 @@ +use anyhow::Context; +use toml_edit::{DocumentMut, Item, Table}; + +pub fn parse_toml_document(contents: &str) -> anyhow::Result { + if contents.trim().is_empty() { + Ok(DocumentMut::new()) + } else { + contents + .parse::() + .with_context(|| "config.toml TOML 解析失败") + } +} + +pub fn table_mut_or_insert<'a>( + doc: &'a mut DocumentMut, + key: &str, +) -> anyhow::Result<&'a mut Table> { + if !doc.as_table().contains_key(key) { + doc[key] = toml_edit::table(); + } + if doc.get(key).and_then(Item::as_table).is_none() { + doc[key] = toml_edit::table(); + } + doc.get_mut(key) + .and_then(Item::as_table_mut) + .ok_or_else(|| anyhow::anyhow!("{key} 必须是 TOML table")) +} + +pub fn ensure_trailing_newline(mut contents: String) -> String { + if !contents.ends_with('\n') { + contents.push('\n'); + } + contents +} + +pub fn normalize_trimmed_text(contents: String) -> String { + let trimmed = contents.trim(); + if trimmed.is_empty() { + String::new() + } else { + ensure_trailing_newline(trimmed.to_string()) + } +} + +pub fn unquote_toml_string(value: &str) -> String { + let value = value.trim(); + if let Ok(parsed) = toml::from_str::(&format!("value = {value}")) { + if let Some(value) = parsed.get("value").and_then(toml::Value::as_str) { + return value.to_string(); + } + } + value + .strip_prefix('"') + .and_then(|value| value.strip_suffix('"')) + .or_else(|| { + value + .strip_prefix('\'') + .and_then(|value| value.strip_suffix('\'')) + }) + .unwrap_or(value) + .to_string() +} + +pub fn unquote_toml_string_simple(value: &str) -> String { + let value = value.trim(); + value + .strip_prefix('"') + .and_then(|value| value.strip_suffix('"')) + .unwrap_or(value) + .to_string() +} diff --git a/crates/codex-plus-core/src/user_scripts.rs b/crates/codex-plus-core/src/user_scripts.rs index 2be3db4ed..e292e22ce 100644 --- a/crates/codex-plus-core/src/user_scripts.rs +++ b/crates/codex-plus-core/src/user_scripts.rs @@ -8,6 +8,7 @@ use serde::Serialize; use serde_json::{Map, Value, json}; use crate::script_market::MarketScript; +use crate::timestamp::timestamp_secs_string; #[derive(Debug, Clone, PartialEq, Eq, Serialize)] pub struct UserScriptConfig { @@ -168,7 +169,7 @@ impl UserScriptManager { version: script.version.clone(), script_url: script.script_url.clone(), homepage: script.homepage.clone(), - installed_at: current_unix_timestamp_string(), + installed_at: timestamp_secs_string(), }, ); self.save_config_unlocked(&config)?; @@ -391,9 +392,4 @@ fn sanitize_market_id(id: &str) -> String { .to_string() } -fn current_unix_timestamp_string() -> String { - std::time::SystemTime::now() - .duration_since(std::time::UNIX_EPOCH) - .map(|value| value.as_secs().to_string()) - .unwrap_or_else(|_| "0".to_string()) -} + diff --git a/crates/codex-plus-core/src/zed_remote.rs b/crates/codex-plus-core/src/zed_remote.rs index 041c1e4b2..bb66bb867 100644 --- a/crates/codex-plus-core/src/zed_remote.rs +++ b/crates/codex-plus-core/src/zed_remote.rs @@ -8,6 +8,8 @@ use serde::{Deserialize, Serialize}; use serde_json::{Value, json}; use thiserror::Error; +use crate::json_value::json_string_value as string_value; + #[derive(Debug, Error)] pub enum ZedRemoteError { #[error("{0}")] @@ -113,14 +115,6 @@ fn find_executable_on_path(name: &str) -> Option { None } -fn string_value(value: Option<&Value>) -> String { - match value { - Some(Value::String(value)) => value.trim().to_string(), - Some(Value::Number(value)) => value.to_string(), - _ => String::new(), - } -} - pub fn split_ssh_authority(value: &str) -> Result<(String, String, Option), ZedRemoteError> { let mut authority = value.trim(); if authority.is_empty() { diff --git a/crates/codex-plus-core/src/zed_remote/fallback.rs b/crates/codex-plus-core/src/zed_remote/fallback.rs index 0673a5bde..9b4183630 100644 --- a/crates/codex-plus-core/src/zed_remote/fallback.rs +++ b/crates/codex-plus-core/src/zed_remote/fallback.rs @@ -5,14 +5,7 @@ use rusqlite::Connection; use serde_json::{Value, json}; use super::{ZedRemoteError, codex_global_state_path, resolve_ssh_target_from_global_state}; - -fn string_value(value: Option<&Value>) -> String { - match value { - Some(Value::String(value)) => value.trim().to_string(), - Some(Value::Number(value)) => value.to_string(), - _ => String::new(), - } -} +use crate::json_value::json_string_value as string_value; fn ordered_remote_projects_from_global_state(state: &Value) -> Vec { let projects = state diff --git a/crates/codex-plus-core/src/zed_remote/registry.rs b/crates/codex-plus-core/src/zed_remote/registry.rs index 93898eb63..5b7205d89 100644 --- a/crates/codex-plus-core/src/zed_remote/registry.rs +++ b/crates/codex-plus-core/src/zed_remote/registry.rs @@ -11,6 +11,7 @@ use super::{ fallback_open_request_from_global_state_with_context, resolve_ssh_target_from_global_state, target_from_payload, }; +use crate::json_value::json_string_value as string_value; const REGISTRY_FILE: &str = "zed_remote_projects.json"; @@ -588,14 +589,6 @@ fn now_ms() -> i64 { .unwrap_or_default() } -fn string_value(value: Option<&Value>) -> String { - match value { - Some(Value::String(value)) => value.trim().to_string(), - Some(Value::Number(value)) => value.to_string(), - _ => String::new(), - } -} - trait NonEmptyStringExt { fn or_else_nonempty(self, fallback: F) -> String where