feat(i18n): replace hardcoded Chinese messages with English

This commit is contained in:
lzt404 committed 2026-09-27 16:33:50 +08:00
1 parent 89bdbc7aea
commit 9c67b9d40c
77 files changed
+2385 -2304

No files matched your search

+16 -16
View File
@@ -33,7 +33,7 @@ fn object_at<'a>(root: &'a mut Value, keys: &[&str]) -> Result<&'a mut Value, St
let mut value = root;
for key in keys {
if !value.is_object() {
return Err("Antigravity 配置字段必须是对象".into());
return Err("Antigravity configuration field must be an object".into());
}
value = value
.as_object_mut()
@@ -42,7 +42,7 @@ fn object_at<'a>(root: &'a mut Value, keys: &[&str]) -> Result<&'a mut Value, St
.or_insert_with(|| json!({}));
}
if !value.is_object() {
return Err("Antigravity 配置字段必须是对象".into());
return Err("Antigravity configuration field must be an object".into());
}
Ok(value)
}
@@ -57,7 +57,7 @@ pub(crate) fn build_antigravity_config(
) -> Result<String, String> {
let mut root = parse(path, existing)?;
if !root.is_object() {
return Err("Antigravity 配置根节点必须是对象".into());
return Err("Antigravity configuration root must be an object".into());
}
if path
.file_name()
@@ -178,7 +178,7 @@ pub(crate) fn antigravity_has_marker(
_client: AgentClient,
paths: &[PathBuf],
) -> Result<bool, String> {
let path = paths.get(1).ok_or("Antigravity CLI 配置路径缺失")?;
let path = paths.get(1).ok_or("Antigravity CLI configuration path is missing")?;
let root = parse(path, read_optional_text(path)?.as_deref())?;
Ok(root.get("provider").and_then(Value::as_str) == Some(MANAGED_AGENT_PROVIDER_ID))
}
@@ -191,7 +191,7 @@ pub(crate) fn inspect_antigravity_config(
) -> Result<(bool, Option<String>), String> {
let settings = parse(&paths[0], read_optional_text(&paths[0])?.as_deref())?;
let origin = managed_core_loopback_origin(port);
let path = paths.get(1).ok_or("Antigravity CLI 配置路径缺失")?;
let path = paths.get(1).ok_or("Antigravity CLI configuration path is missing")?;
let connection = parse(path, read_optional_text(path)?.as_deref())?;
let model = connection
.get("model")
@@ -247,32 +247,32 @@ pub(crate) fn antigravity_helper_requested(args: &[std::ffi::OsString]) -> bool
fn antigravity_helper_command(args: &[std::ffi::OsString]) -> Result<Command, String> {
if !args.get(1).is_some_and(|a| a == "--cpa-antigravity-cli") {
return Err("未知的 Antigravity 启动参数".into());
return Err("Unknown Antigravity launch argument".into());
}
let home = args
.get(2)
.map(PathBuf::from)
.ok_or("Antigravity CLI 启动参数缺少用户目录")?;
.ok_or("Antigravity CLI launch arguments are missing the user directory")?;
let paths = antigravity_config_paths(AgentClient::AntigravityCli, &home);
let connection = parse(&paths[1], read_optional_text(&paths[1])?.as_deref())?;
if connection.get("provider").and_then(Value::as_str) != Some(MANAGED_AGENT_PROVIDER_ID) {
return Err("Antigravity CLI 的 CPA 配置已关闭,请重新应用配置".into());
return Err("CPA configuration is disabled in Antigravity CLI. Apply the configuration again".into());
}
let key = connection
.get("apiKey")
.and_then(Value::as_str)
.filter(|s| !s.is_empty())
.ok_or("Antigravity CLI 密钥缺失")?;
.ok_or("Antigravity CLI key is missing")?;
let url = connection
.get("baseUrl")
.and_then(Value::as_str)
.filter(|s| !s.trim().is_empty())
.ok_or("Antigravity CLI 地址缺失")?;
.ok_or("Antigravity CLI address is missing")?;
let model = connection
.get("model")
.and_then(Value::as_str)
.filter(|s| !s.trim().is_empty())
.ok_or("Antigravity CLI 模型缺失")?;
.ok_or("Antigravity CLI model is missing")?;
let settings = parse(&paths[0], read_optional_text(&paths[0])?.as_deref())?;
if settings.get("modelProvider").and_then(Value::as_str) != Some("gemini")
|| field(
@@ -287,9 +287,9 @@ fn antigravity_helper_command(args: &[std::ffi::OsString]) -> Result<Command, St
.and_then(Value::as_str)
!= Some(model)
{
return Err("Antigravity CLI 配置已变化,请重新应用配置".into());
return Err("Antigravity CLI configuration has changed. Apply the configuration again".into());
}
let executable = find_antigravity_cli(&home).ok_or("未找到 Antigravity CLI")?;
let executable = find_antigravity_cli(&home).ok_or("Antigravity CLI not found")?;
let mut command = Command::new(executable);
command
.arg(format!("--gemini_dir={}", home.join(".gemini").display()))
@@ -312,13 +312,13 @@ pub(crate) fn run_antigravity_helper(args: &[std::ffi::OsString]) -> Result<i32,
#[cfg(unix)]
{
use std::os::unix::process::CommandExt;
return Err(format!("启动 Antigravity 失败: {}", command.exec()));
return Err(format!("Failed to start Antigravity: {}", command.exec()));
}
#[cfg(not(unix))]
{
let mut child = command
.spawn()
.map_err(|error| format!("启动 Antigravity 失败: {error}"))?;
.map_err(|error| format!("Failed to start Antigravity: {error}"))?;
#[cfg(target_os = "windows")]
let job = match attach_child_to_windows_job(&child) {
Ok(job) => job,
@@ -337,6 +337,6 @@ pub(crate) fn run_antigravity_helper(args: &[std::ffi::OsString]) -> Result<i32,
}
result
.map(|s| s.code().unwrap_or(1))
.map_err(|error| format!("等待 Antigravity 退出失败: {error}"))
.map_err(|error| format!("Failed while waiting for Antigravity to exit: {error}"))
}
}
+33 -33
View File
@@ -80,14 +80,14 @@ pub(crate) fn agent_data_directory(paths: &[PathBuf]) -> Result<PathBuf, String>
let temp = std::env::temp_dir();
let relative = paths
.first()
.ok_or("没有配置路径")?
.ok_or("No configuration path")?
.strip_prefix(&temp)
.map_err(|_| "测试配置必须位于临时目录")?;
.map_err(|_| "Test configuration must be in the temporary directory")?;
let directory = temp.join(
relative
.components()
.next()
.ok_or("缺少测试目录")?
.ok_or("Test directory is missing")?
.as_os_str(),
);
assert!(paths.iter().all(|p| p.starts_with(&directory)));
@@ -120,7 +120,7 @@ fn valid_version_id(id: &str) -> bool {
fn version_path(client: &str, paths: &[PathBuf], id: &str) -> Result<PathBuf, String> {
if !valid_version_id(id) {
return Err("备份版本编号无效".into());
return Err("Invalid backup version number".into());
}
Ok(backup_directory(client, paths)?.join(format!("{id}.json")))
}
@@ -156,14 +156,14 @@ fn make_version(client: &str, paths: &[PathBuf], images: &Images) -> BackupVersi
fn write_version(paths: &[PathBuf], version: &BackupVersion) -> Result<(), String> {
let path = version_path(&version.client, paths, &version.id)?;
validate_config_path(&path)?;
let parent = path.parent().ok_or("备份目录无效")?;
fs::create_dir_all(parent).map_err(|_| "创建手动备份目录失败")?;
let parent = path.parent().ok_or("Invalid backup directory")?;
fs::create_dir_all(parent).map_err(|_| "Failed to create manual backup directory")?;
validate_config_path(&path)?;
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
fs::set_permissions(parent, fs::Permissions::from_mode(0o700))
.map_err(|_| "设置备份目录权限失败")?;
.map_err(|_| "Failed to set backup directory permissions")?;
}
let mut options = fs::OpenOptions::new();
options.write(true).create_new(true);
@@ -172,15 +172,15 @@ fn write_version(paths: &[PathBuf], version: &BackupVersion) -> Result<(), Strin
use std::os::unix::fs::OpenOptionsExt;
options.mode(0o600);
}
let payload = serde_json::to_vec(version).map_err(|_| "生成备份失败")?;
let payload = serde_json::to_vec(version).map_err(|_| "Failed to generate backup")?;
let package = BackupPackage {
payload: version.clone(),
checksum: sha256_bytes(&payload),
};
let bytes = serde_json::to_vec(&package).map_err(|_| "生成备份失败")?;
let bytes = serde_json::to_vec(&package).map_err(|_| "Failed to generate backup")?;
let mut file = options
.open(&path)
.map_err(|_| "创建手动备份失败,请检查备份目录权限")?;
.map_err(|_| "Failed to create manual backup. Check backup directory permissions")?;
if file
.write_all(&bytes)
.and_then(|_| file.sync_all())
@@ -188,23 +188,23 @@ fn write_version(paths: &[PathBuf], version: &BackupVersion) -> Result<(), Strin
{
drop(file);
let _ = fs::remove_file(&path);
return Err("保存手动备份失败".into());
return Err("Failed to save manual backup".into());
}
drop(file);
if fs::read(&path).ok().as_deref() != Some(bytes.as_slice()) {
let _ = fs::remove_file(&path);
return Err("手动备份写后校验失败".into());
return Err("Manual backup post-write verification failed".into());
}
Ok(())
}
fn read_version(client: &str, paths: &[PathBuf], id: &str) -> Result<BackupVersion, String> {
let path = version_path(client, paths, id)?;
let bytes = read_agent_bytes(&path)?.ok_or("备份文件缺失")?;
let bytes = read_agent_bytes(&path)?.ok_or("Backup file is missing")?;
let package: BackupPackage =
serde_json::from_slice(&bytes).map_err(|_| "备份包损坏,无法恢复,可删除此版本")?;
serde_json::from_slice(&bytes).map_err(|_| "Backup package is corrupted and cannot be restored. You can delete this version")?;
let version = package.payload;
if package.checksum != sha256_bytes(&serde_json::to_vec(&version).map_err(|_| "备份校验失败")?)
if package.checksum != sha256_bytes(&serde_json::to_vec(&version).map_err(|_| "Backup verification failed")?)
|| version.version != 1
|| version.client != client
|| version.id != id
@@ -213,7 +213,7 @@ fn read_version(client: &str, paths: &[PathBuf], id: &str) -> Result<BackupVersi
file.path != *path || file.hash != image_hash(file.bytes.as_deref())
})
{
return Err("备份包路径或内容校验失败,无法恢复,可删除此版本".into());
return Err("Backup package path or content verification failed; it cannot be restored. You can delete this version".into());
}
Ok(version)
}
@@ -229,12 +229,12 @@ fn backup_images(version: &BackupVersion) -> Images {
fn validate_restorable(version: &BackupVersion) -> Result<(), String> {
let images = backup_images(version);
validate_client_config_images(&version.client, &images)
.map_err(|_| "备份含有无法解析的配置,已保留原文但不可恢复".to_string())?;
.map_err(|_| "Backup contains an unparseable configuration; the original text was preserved but cannot be restored".to_string())?;
if version.client == "claude-desktop"
&& desktop_profile_needs_mapping(&images)?
&& version.mappings.is_none()
{
return Err("备份缺少可靠的 Claude Desktop 模型映射,请重新选择模型后创建备份".into());
return Err("Backup lacks a reliable Claude Desktop model mapping. Select the models again and create a new backup".into());
}
Ok(())
}
@@ -288,7 +288,7 @@ pub(crate) fn create_backup(client: &str, home: &Path) -> Result<BackupSummary,
let state_revision = mapping_revision(client, &paths)?;
let version = make_version(client, &paths, &images);
if config_images(&paths)? != images || mapping_revision(client, &paths)? != state_revision {
return Err("备份期间配置发生变化,请重试".into());
return Err("Configuration changed during backup. Try again".into());
}
write_version(&paths, &version)?;
backup_summary(client, &paths, &version.id)
@@ -305,11 +305,11 @@ pub(crate) fn list_backups(client: &str, home: &Path) -> Result<BackupList, Stri
versions: Vec::new(),
})
}
Err(_) => return Err("读取手动备份目录失败".into()),
Err(_) => return Err("Failed to read manual backup directory".into()),
};
let mut versions = Vec::new();
for entry in entries {
let path = entry.map_err(|_| "读取手动备份目录失败")?.path();
let path = entry.map_err(|_| "Failed to read manual backup directory")?.path();
if path.extension().and_then(|e| e.to_str()) != Some("json") {
continue;
}
@@ -328,8 +328,8 @@ pub(crate) fn list_backups(client: &str, home: &Path) -> Result<BackupList, Stri
pub(crate) fn delete_backup(client: &str, home: &Path, id: &str) -> Result<(), String> {
let paths = config_paths(client, home)?;
let path = version_path(client, &paths, id)?;
validate_config_path(&path.parent().ok_or("备份目录无效")?.join(".path-check"))?;
fs::remove_file(&path).map_err(|_| "删除手动备份失败,请刷新列表后重试".to_string())
validate_config_path(&path.parent().ok_or("Invalid backup directory")?.join(".path-check"))?;
fs::remove_file(&path).map_err(|_| "Failed to delete manual backup. Refresh the list and try again".to_string())
}
fn preview(
@@ -337,13 +337,13 @@ fn preview(
paths: &[PathBuf],
id: &str,
) -> Result<(BackupPreview, Images, Images), String> {
let package = read_agent_bytes(&version_path(client, paths, id)?)?.ok_or("备份文件缺失")?;
let package = read_agent_bytes(&version_path(client, paths, id)?)?.ok_or("Backup file is missing")?;
let version = read_version(client, paths, id)?;
validate_restorable(&version)?;
let current = config_images(paths)?;
let after = backup_images(&version);
if read_agent_bytes(&version_path(client, paths, id)?)?.as_deref() != Some(&package) {
return Err("备份在预览期间发生变化,请重新预览".into());
return Err("Backup changed during preview. Preview it again".into());
}
let revision = sha256_bytes(
format!(
@@ -426,13 +426,13 @@ pub(crate) fn desktop_mapping_bytes(
mappings
.map(|mappings| {
let profile_models_hash =
profile_models_hash(images).ok_or("Claude Desktop 模型配置无效")?;
profile_models_hash(images).ok_or("Invalid Claude Desktop model configuration")?;
serde_json::to_vec(&DesktopMappingState {
version: 1,
profile_models_hash,
mappings: mappings.clone(),
})
.map_err(|_| "生成当前模型映射失败".into())
.map_err(|_| "Failed to generate current model mapping".into())
})
.transpose()
}
@@ -469,10 +469,10 @@ pub(crate) fn create_agent_config_backup(
app: tauri::AppHandle,
client: String,
) -> Result<BackupSummary, String> {
let home = app.path().home_dir().map_err(|_| "无法获取用户目录")?;
let home = app.path().home_dir().map_err(|_| "Failed to get user directory")?;
let _guard = AGENT_CONFIG_FILE_LOCK
.lock()
.map_err(|_| "配置文件锁已损坏")?;
.map_err(|_| "Configuration file lock is poisoned")?;
create_backup(&client, &home)
}
@@ -481,10 +481,10 @@ pub(crate) fn list_agent_config_backups(
app: tauri::AppHandle,
client: String,
) -> Result<BackupList, String> {
let home = app.path().home_dir().map_err(|_| "无法获取用户目录")?;
let home = app.path().home_dir().map_err(|_| "Failed to get user directory")?;
let _guard = AGENT_CONFIG_FILE_LOCK
.lock()
.map_err(|_| "配置文件锁已损坏")?;
.map_err(|_| "Configuration file lock is poisoned")?;
list_backups(&client, &home)
}
@@ -494,10 +494,10 @@ pub(crate) fn delete_agent_config_backup(
client: String,
id: String,
) -> Result<(), String> {
let home = app.path().home_dir().map_err(|_| "无法获取用户目录")?;
let home = app.path().home_dir().map_err(|_| "Failed to get user directory")?;
let _guard = AGENT_CONFIG_FILE_LOCK
.lock()
.map_err(|_| "配置文件锁已损坏")?;
.map_err(|_| "Configuration file lock is poisoned")?;
delete_backup(&client, &home, &id)
}
+7 -7
View File
@@ -18,7 +18,7 @@ pub(crate) struct RestorePlan {
fn local_restore_plan(client: &str, home: &Path, id: &str) -> Result<RestorePlan, String> {
let _guard = AGENT_CONFIG_FILE_LOCK
.lock()
.map_err(|_| "配置文件锁已损坏")?;
.map_err(|_| "Configuration file lock is poisoned")?;
let paths = config_paths(client, home)?;
let (preview, before, after) = preview(client, &paths, id)?;
let version = read_version(client, &paths, id)?;
@@ -55,7 +55,7 @@ fn desktop_restore_configuration(
.filter_map(|m| m.get("name").and_then(Value::as_str))
.collect::<Vec<_>>();
let mut mappings = plan.version.mappings.as_ref().ok_or(
"此备份版本缺少 Claude Desktop 模型映射,无法安全恢复内核路由,请重新配置模型",
"This backup version lacks Claude Desktop model mappings and cannot safely restore kernel routing. Reconfigure the models",
)?.clone();
if mappings.desktop_models.is_none() {
let roles = [
@@ -71,7 +71,7 @@ fn desktop_restore_configuration(
current.eq_ignore_ascii_case(id) || legacy.eq_ignore_ascii_case(id)
}))
else {
return Err("此备份中的模型 ID 缺少对应来源,无法安全恢复内核路由".into());
return Err("A model ID in this backup has no corresponding source, so kernel routing cannot be safely restored".into());
};
if let Some(existing) = entries.iter_mut()
.find(|entry| entry.model_id().eq_ignore_ascii_case(id))
@@ -91,7 +91,7 @@ fn desktop_restore_configuration(
}
mappings.desktop_models = Some(entries);
}
let entries = mappings.desktop_models.as_ref().ok_or("缺少备份模型映射")?;
let entries = mappings.desktop_models.as_ref().ok_or("Backup model mapping is missing")?;
validate_claude_desktop_entries(entries)?;
let models = entries.iter()
.map(|entry| AgentModelOption {
@@ -206,19 +206,19 @@ pub(crate) async fn execute_restore_plan(
revision: &str,
) -> Result<AgentConfigActionResult, String> {
if plan.preview.revision != revision {
return Err("预览后配置发生变化,请重新选择备份版本".into());
return Err("Configuration changed after preview. Select the backup version again".into());
}
let commit = || {
let _guard = AGENT_CONFIG_FILE_LOCK
.lock()
.map_err(|_| "配置文件锁已损坏")?;
.map_err(|_| "Configuration file lock is poisoned")?;
let latest = preview(&plan.version.client, &plan.paths, &plan.version.id)?.0;
let version = read_version(&plan.version.client, &plan.paths, &plan.version.id)?;
if latest.revision != plan.local_revision
|| serde_json::to_vec(&version).map_err(|e| e.to_string())?
!= serde_json::to_vec(&plan.version).map_err(|e| e.to_string())?
{
return Err("恢复期间配置或备份发生变化,请重新选择备份版本".into());
return Err("Configuration or backup changed during restoration. Select the backup version again".into());
}
commit_config_with_mappings(
&plan.version.client,
+4 -4
View File
@@ -781,7 +781,7 @@ fn clear_integration_rolls_back_config_and_state_together() {
},
)
.unwrap_err();
assert!(error.contains("已回滚"));
assert!(error.contains("rolled back"));
assert_eq!(config_images(&paths).unwrap(), before);
assert_eq!(fs::read_to_string(state).unwrap(), "legacy state");
}
@@ -819,7 +819,7 @@ fn partial_writes_and_failed_verification_roll_back_without_persistent_snapshots
},
)
.unwrap_err();
assert!(error.contains("已回滚"));
assert!(error.contains("rolled back"));
assert!(!error.contains("secret-token"));
assert_eq!(before, config_images(&paths).unwrap());
}
@@ -834,14 +834,14 @@ fn partial_writes_and_failed_verification_roll_back_without_persistent_snapshots
&mut |_, _| Err("secret-token".into()),
)
.unwrap_err();
assert!(error.contains("回滚失败"));
assert!(error.contains("rollback both failed"));
assert!(!error.contains("secret-token"));
assert!(list_backups("codex", &home.0).unwrap().versions.is_empty());
save(&paths[0], "external = true");
assert!(
commit_config("codex", &paths, &before, &after, "update", None)
.unwrap_err()
.contains("其他程序")
.contains("another program")
);
assert_eq!(fs::read_to_string(&paths[0]).unwrap(), "external = true");
}
+93 -93
View File
@@ -18,7 +18,7 @@ pub(crate) fn inspect_agent_config_statuses(
let home = app
.path()
.home_dir()
.map_err(|error| format!("无法获取用户目录: {error}"))?;
.map_err(|error| format!("Failed to get user directory: {error}"))?;
let api_key = effective_agent_api_key(config);
let targets = [
AgentStatusDetectionTarget::Client(AgentClient::ClaudeCode),
@@ -47,7 +47,7 @@ pub(crate) fn inspect_agent_config_statuses(
loop {
let next = queue
.lock()
.map_err(|_| "智能体检测任务队列锁已损坏".to_string())?
.map_err(|_| "Agent detection task queue lock is poisoned".to_string())?
.next();
let Some((index, target)) = next else {
return Ok(());
@@ -62,22 +62,22 @@ pub(crate) fn inspect_agent_config_statuses(
};
results
.lock()
.map_err(|_| "智能体检测结果锁已损坏".to_string())?[index] = Some(status);
.map_err(|_| "Agent detection result lock is poisoned".to_string())?[index] = Some(status);
}
}));
}
for worker in workers {
worker
.join()
.map_err(|_| "智能体检测工作线程异常退出".to_string())??;
.map_err(|_| "Agent detection worker thread exited unexpectedly".to_string())??;
}
Ok::<(), String>(())
})?;
results
.into_inner()
.map_err(|_| "智能体检测结果锁已损坏".to_string())?
.map_err(|_| "Agent detection result lock is poisoned".to_string())?
.into_iter()
.map(|status| status.ok_or_else(|| "智能体检测结果不完整".to_string()))
.map(|status| status.ok_or_else(|| "Agent detection result is incomplete".to_string()))
.collect()
}
@@ -89,7 +89,7 @@ pub(crate) fn refresh_agent_config_status_cache(
let _refresh_guard = cache
.refresh_lock
.lock()
.map_err(|_| "智能体配置状态刷新锁已损坏".to_string())?;
.map_err(|_| "Agent configuration status refresh lock is poisoned".to_string())?;
let config = gui_config_state.snapshot()?;
let port = config.port;
let api_key = effective_agent_api_key(&config);
@@ -109,7 +109,7 @@ pub(crate) async fn get_agent_config_statuses(
let _refresh_guard = cache
.refresh_lock
.lock()
.map_err(|_| "智能体配置状态刷新锁已损坏".to_string())?;
.map_err(|_| "Agent configuration status refresh lock is poisoned".to_string())?;
let config = gui_config_state.snapshot()?;
let port = config.port;
if let Some(statuses) = cache.get(port, effective_agent_api_key(&config))? {
@@ -119,7 +119,7 @@ pub(crate) async fn get_agent_config_statuses(
refresh_agent_config_status_cache(&app, gui_config_state.inner(), cache.inner())
})
.await
.map_err(|error| format!("智能体检测后台任务失败: {error}"))?
.map_err(|error| format!("Agent detection background task failed: {error}"))?
}
#[tauri::command]
@@ -132,7 +132,7 @@ pub(crate) async fn refresh_agent_config_statuses(
refresh_agent_config_status_cache(&app, gui_config_state.inner(), cache.inner())
})
.await
.map_err(|error| format!("智能体检测后台任务失败: {error}"))?
.map_err(|error| format!("Agent detection background task failed: {error}"))?
}
#[tauri::command]
@@ -171,7 +171,7 @@ pub(crate) async fn check_pi_provider_update(
let home = app
.path()
.home_dir()
.map_err(|error| format!("无法获取用户目录: {error}"))?;
.map_err(|error| format!("Failed to get user directory: {error}"))?;
let installed_version = read_pi_provider_version(&home)?;
let Some(installed_version_value) = installed_version.as_deref() else {
return Ok(PiProviderUpdateStatus {
@@ -200,14 +200,14 @@ pub(crate) async fn install_pi_provider(
let home = app
.path()
.home_dir()
.map_err(|error| format!("无法获取用户目录: {error}"))?;
.map_err(|error| format!("Failed to get user directory: {error}"))?;
let config = gui_config_state.snapshot()?;
let executable = find_pi_executable(&home);
if executable.is_none()
&& !inspect_pi_provider_status(&home, config.port, effective_agent_api_key(&config))
.config_exists
{
return Err("未检测到 Pi CLI 或 Pi 配置文件".to_string());
return Err("Pi CLI and Pi configuration file were not detected".to_string());
}
let model = resolve_pi_default_model(&config, &model).await?;
let port = config.port;
@@ -221,7 +221,7 @@ pub(crate) async fn install_pi_provider(
}
})
.await
.map_err(|error| format!("安装 Pi CLIProxyAPI provider 任务失败: {error}"))??;
.map_err(|error| format!("Pi CLIProxyAPI provider installation task failed: {error}"))??;
cache.clear()?;
Ok(result)
}
@@ -236,10 +236,10 @@ pub(crate) async fn update_pi_provider(
let home = app
.path()
.home_dir()
.map_err(|error| format!("无法获取用户目录: {error}"))?;
.map_err(|error| format!("Failed to get user directory: {error}"))?;
let config = gui_config_state.snapshot()?;
let executable = find_pi_executable(&home)
.ok_or_else(|| "未检测到 Pi CLI,请先安装 Pi 并确保 pi 命令在 PATH 中".to_string())?;
.ok_or_else(|| "Pi CLI was not detected. Install Pi and ensure the pi command is in PATH".to_string())?;
let model = resolve_pi_default_model(&config, &model).await?;
let port = config.port;
let api_key = effective_agent_api_key(&config).to_string();
@@ -248,7 +248,7 @@ pub(crate) async fn update_pi_provider(
update_pi_provider_inner(&home, &executable, port, &api_key, &model, &proxy_url)
})
.await
.map_err(|error| format!("更新 Pi 插件任务失败: {error}"))??;
.map_err(|error| format!("Pi plugin update task failed: {error}"))??;
cache.clear()?;
Ok(result)
}
@@ -263,7 +263,7 @@ pub(crate) async fn repair_pi_provider(
let home = app
.path()
.home_dir()
.map_err(|error| format!("无法获取用户目录: {error}"))?;
.map_err(|error| format!("Failed to get user directory: {error}"))?;
let config = gui_config_state.snapshot()?;
let model = resolve_pi_default_model(&config, &model).await?;
let port = config.port;
@@ -272,7 +272,7 @@ pub(crate) async fn repair_pi_provider(
repair_pi_provider_inner(&home, port, &api_key, &model)
})
.await
.map_err(|error| format!("修复 Pi 配置任务失败: {error}"))??;
.map_err(|error| format!("Pi configuration repair task failed: {error}"))??;
cache.clear()?;
Ok(result)
}
@@ -285,14 +285,14 @@ pub(crate) async fn uninstall_pi_provider(
let home = app
.path()
.home_dir()
.map_err(|error| format!("鏃犳硶鑾峰彇鐢ㄦ埛鐩綍: {error}"))?;
.map_err(|error| format!("Failed to get user directory: {error}"))?;
let executable =
find_pi_executable(&home).ok_or_else(|| "鏈娴嬪埌 Pi CLI锛岃鍏堝畨瑁?Pi".to_string())?;
find_pi_executable(&home).ok_or_else(|| "Pi CLI was not detected. Install Pi first".to_string())?;
let result = tauri::async_runtime::spawn_blocking(move || {
uninstall_pi_provider_inner(&home, &executable)
})
.await
.map_err(|error| format!("鍗歌浇 Pi 鎻掍欢浠诲姟澶辫触: {error}"))??;
.map_err(|error| format!("Pi plugin uninstall task failed: {error}"))??;
cache.clear()?;
Ok(result)
}
@@ -302,7 +302,7 @@ pub(crate) fn check_codex_oauth_login(app: tauri::AppHandle) -> Result<(), Strin
let home = app
.path()
.home_dir()
.map_err(|error| format!("无法获取用户目录: {error}"))?;
.map_err(|error| format!("Failed to get user directory: {error}"))?;
validate_codex_oauth_login(&home)
}
@@ -325,7 +325,7 @@ pub(crate) async fn update_codex_model_catalog_inner(
.read_timeout(Duration::from_secs(15))
.timeout(Duration::from_secs(25)),
&proxy_url,
"创建 Codex 模型目录更新客户端失败",
"Failed to create Codex model catalog update client",
)?;
let response = client
.get(CODEX_MODEL_CATALOG_URL)
@@ -333,11 +333,11 @@ pub(crate) async fn update_codex_model_catalog_inner(
.header(reqwest::header::USER_AGENT, APP_USER_AGENT)
.send()
.await
.map_err(|error| format!("从 GitHub 读取 Codex 模型目录失败: {error}"))?;
.map_err(|error| format!("Failed to read Codex model catalog from GitHub: {error}"))?;
let status = response.status();
if !status.is_success() {
return Err(format!(
"从 GitHub 读取 Codex 模型目录失败: HTTP {}",
"Failed to read Codex model catalog from GitHub: HTTP {}",
status.as_u16()
));
}
@@ -346,7 +346,7 @@ pub(crate) async fn update_codex_model_catalog_inner(
.is_some_and(|size| size > MAX_CODEX_MODEL_CATALOG_BYTES as u64)
{
return Err(format!(
"GitHub Codex 模型目录超过 {} MiB 限制",
"GitHub Codex model catalog exceeds the {} MiB limit",
MAX_CODEX_MODEL_CATALOG_BYTES / 1024 / 1024
));
}
@@ -354,19 +354,19 @@ pub(crate) async fn update_codex_model_catalog_inner(
let mut bytes = Vec::new();
let mut stream = response.bytes_stream();
while let Some(chunk) = stream.next().await {
let chunk = chunk.map_err(|error| format!("读取 GitHub Codex 模型目录失败: {error}"))?;
let chunk = chunk.map_err(|error| format!("Failed to read GitHub Codex model catalog: {error}"))?;
if bytes.len().saturating_add(chunk.len()) > MAX_CODEX_MODEL_CATALOG_BYTES {
return Err(format!(
"GitHub Codex 模型目录超过 {} MiB 限制",
"GitHub Codex model catalog exceeds the {} MiB limit",
MAX_CODEX_MODEL_CATALOG_BYTES / 1024 / 1024
));
}
bytes.extend_from_slice(&chunk);
}
let catalog_json = String::from_utf8(bytes)
.map_err(|_| "GitHub Codex 模型目录不是有效的 UTF-8 文件".to_string())?;
.map_err(|_| "GitHub Codex model catalog is not a valid UTF-8 file".to_string())?;
let revision = codex_catalog::validate_catalog_json(&catalog_json)
.map_err(|error| format!("GitHub Codex 模型目录校验失败: {error}"))?;
.map_err(|error| format!("GitHub Codex model catalog verification failed: {error}"))?;
if revision < codex_catalog::current_catalog_revision()?
|| codex_catalog::current_catalog_json()? == catalog_json
@@ -388,7 +388,7 @@ pub(crate) fn codex_model_catalog_override_path(app: &tauri::AppHandle) -> Resul
let app_data = app
.path()
.app_data_dir()
.map_err(|error| format!("无法获取应用数据目录: {error}"))?;
.map_err(|error| format!("Failed to get application data directory: {error}"))?;
Ok(app_data
.join(CODEX_MODEL_CATALOG_OVERRIDE_DIR)
.join(CODEX_MODEL_CATALOG_SOURCE_FILE))
@@ -400,9 +400,9 @@ pub(crate) fn load_codex_model_catalog_override(app: &tauri::AppHandle) -> Resul
return Ok(());
}
let catalog_json = fs::read_to_string(&path)
.map_err(|error| format!("读取本地 Codex 模型目录更新文件失败: {error}"))?;
.map_err(|error| format!("Failed to read local Codex model catalog update file: {error}"))?;
codex_catalog::activate_catalog_json(&catalog_json)
.map_err(|error| format!("本地 Codex 模型目录更新文件无效: {error}"))?;
.map_err(|error| format!("Invalid local Codex model catalog update file: {error}"))?;
Ok(())
}
@@ -509,7 +509,7 @@ pub(crate) async fn get_model_alias_edit_source(
alias: String,
) -> Result<ModelAliasEditContext, String> {
let config = gui_config_state.snapshot()?;
let alias = existing_thinking_alias_model_id(&alias, "别名模型")?;
let alias = existing_thinking_alias_model_id(&alias, "Alias model")?;
let content = fetch_management_config_yaml(&config).await?;
let definitions = fetch_oauth_model_definitions(&config).await;
model_alias_edit_context(&content, &alias, &definitions)
@@ -528,17 +528,17 @@ pub(crate) async fn create_thinking_alias(
let config = gui_config_state.snapshot()?;
let source_id = source_id.trim().to_string();
if source_id.is_empty() {
return Err("请先选择原模型".to_string());
return Err("Select the source model first".to_string());
}
let original_alias = original_alias
.as_deref()
.map(|original| existing_thinking_alias_model_id(original, "别名模型"))
.map(|original| existing_thinking_alias_model_id(original, "Alias model"))
.transpose()?;
let alias = match original_alias.as_deref() {
Some(original) if original.eq_ignore_ascii_case(alias.trim()) => {
existing_thinking_alias_model_id(&alias, "别名模型")?
existing_thinking_alias_model_id(&alias, "Alias model")?
}
_ => validate_thinking_alias_model_id(&alias, "别名模型")?,
_ => validate_thinking_alias_model_id(&alias, "Alias model")?,
};
let effort = if effort.trim().is_empty() {
String::new()
@@ -569,11 +569,11 @@ pub(crate) async fn create_thinking_alias(
.into_iter()
.find(|source| source.source.id == source_id)
.ok_or_else(|| {
"原模型已不在内核当前可用模型中,或其配置来源已经变化,请刷新后重新选择".to_string()
"The source model is no longer available in the kernel, or its configuration source has changed. Refresh and select it again".to_string()
})?
};
if fast && !alias_source_supports_fast(&source) {
return Err("Fast 仅支持 OpenAI 兼容 API、Codex API 或 Codex OAuth 模型源".to_string());
return Err("Fast supports only OpenAI-compatible API, Codex API, or Codex OAuth model sources".to_string());
}
if !effort.is_empty()
&& !source
@@ -583,12 +583,12 @@ pub(crate) async fn create_thinking_alias(
.any(|level| level.eq_ignore_ascii_case(&effort))
{
return Err(format!(
"思考强度 {effort} 不在模型 {} 当前支持的等级中",
"Reasoning effort {effort} is not among the levels currently supported by model {}",
source.source.model
));
}
if source.source.model.eq_ignore_ascii_case(&alias) {
return Err("别名模型不能和原模型相同".to_string());
return Err("Alias model cannot be the same as the source model".to_string());
}
if available_models.iter().any(|model| {
@@ -597,7 +597,7 @@ pub(crate) async fn create_thinking_alias(
.as_deref()
.is_some_and(|original| original.eq_ignore_ascii_case(&alias))
}) {
return Err(format!("{alias} 已经是实际模型 ID,不能再作为别名"));
return Err(format!("{alias} is already an actual model ID and cannot also be used as an alias"));
}
let updated = match original_alias.as_deref() {
Some(original) => edit_model_alias_in_yaml(&content, original, &source, &alias, &effort, fast)?,
@@ -614,7 +614,7 @@ pub(crate) async fn delete_thinking_alias(
oauth_channel: Option<String>,
) -> Result<Vec<ThinkingAliasEntry>, String> {
let config = gui_config_state.snapshot()?;
let alias = existing_thinking_alias_model_id(&alias, "别名模型")?;
let alias = existing_thinking_alias_model_id(&alias, "Alias model")?;
let content = fetch_management_config_yaml(&config).await?;
let updated =
remove_thinking_alias_from_yaml_for_channel(&content, &alias, oauth_channel.as_deref())?;
@@ -660,9 +660,9 @@ pub(crate) async fn create_speed_alias(
let config = gui_config_state.snapshot()?;
let source_id = source_id.trim().to_string();
if source_id.is_empty() {
return Err("请先选择原模型".to_string());
return Err("Select the source model first".to_string());
}
let alias = validate_thinking_alias_model_id(&alias, "别名模型")?;
let alias = validate_thinking_alias_model_id(&alias, "Alias model")?;
let content = fetch_management_config_yaml(&config).await?;
let available_models =
fetch_agent_models(config.port, effective_agent_api_key(&config)).await?;
@@ -678,24 +678,24 @@ pub(crate) async fn create_speed_alias(
.find(|source| source.source.id == source_id)
.cloned()
.ok_or_else(|| {
"原模型已不在内核当前可用模型中,或其配置来源已经变化,请刷新后重试".to_string()
"The source model is no longer available in the kernel, or its configuration source has changed. Refresh and try again".to_string()
})?;
if source.source.model.eq_ignore_ascii_case(&alias) {
return Err("别名模型不能和原模型相同".to_string());
return Err("Alias model cannot be the same as the source model".to_string());
}
if available_models
.iter()
.any(|model| model.name.eq_ignore_ascii_case(&alias))
{
return Err(format!("{alias} 已经是实际模型 ID,不能再作为别名"));
return Err(format!("{alias} is already an actual model ID and cannot also be used as an alias"));
}
let document = serde_norway::from_str::<serde_norway::Value>(&content)
.map_err(|error| format!("解析内核 YAML 配置失败: {error}"))?;
.map_err(|error| format!("Failed to parse kernel YAML configuration: {error}"))?;
let root = document
.as_mapping()
.ok_or_else(|| "内核配置顶层必须是 YAML 映射".to_string())?;
.ok_or_else(|| "Kernel configuration root must be a YAML mapping".to_string())?;
if configured_model_alias_exists(root, &alias) {
return Err(format!("别名模型 {alias} 已存在"));
return Err(format!("Alias model {alias} already exists"));
}
let updated = add_speed_alias_to_yaml(&content, &source, &alias)?;
@@ -710,7 +710,7 @@ pub(crate) async fn delete_speed_alias(
oauth_channel: Option<String>,
) -> Result<Vec<SpeedAliasEntry>, String> {
let config = gui_config_state.snapshot()?;
let alias = existing_thinking_alias_model_id(&alias, "别名模型")?;
let alias = existing_thinking_alias_model_id(&alias, "Alias model")?;
let content = fetch_management_config_yaml(&config).await?;
let updated =
remove_speed_alias_from_yaml_for_channel(&content, &alias, oauth_channel.as_deref())?;
@@ -723,7 +723,7 @@ pub(crate) async fn fetch_agent_models(
api_key: &str,
) -> Result<Vec<AgentModelOption>, String> {
if port == 0 {
return Err("内核端口无效".to_string());
return Err("Invalid kernel port".to_string());
}
let tls_enabled = managed_core_tls_enabled();
let client = reqwest::Client::builder()
@@ -732,7 +732,7 @@ pub(crate) async fn fetch_agent_models(
.timeout(Duration::from_secs(15))
.danger_accept_invalid_certs(tls_enabled)
.build()
.map_err(|error| format!("创建模型列表客户端失败: {error}"))?;
.map_err(|error| format!("Failed to create model list client: {error}"))?;
let base_url = managed_core_loopback_origin(port);
let endpoints = [
format!("{base_url}/v1/models"),
@@ -747,16 +747,16 @@ pub(crate) async fn fetch_agent_models(
.header(reqwest::header::USER_AGENT, USER_AGENT)
.send()
.await
.map_err(|error| format!("请求本机模型列表失败: {error}"))?;
.map_err(|error| format!("Failed to request local model list: {error}"))?;
let status = response.status();
let body = response
.text()
.await
.map_err(|error| format!("读取本机模型列表失败: {error}"))?;
.map_err(|error| format!("Failed to read local model list: {error}"))?;
if status.is_success() {
let payload = serde_json::from_str::<serde_json::Value>(&body).map_err(|error| {
format!(
"解析本机模型列表失败: {error}; body={}",
"Failed to parse local model list: {error}; body={}",
truncate_for_error(&body)
)
})?;
@@ -769,7 +769,7 @@ pub(crate) async fn fetch_agent_models(
}
}
Err("本机内核不支持模型列表接口".to_string())
Err("Local kernel does not support the model list endpoint".to_string())
}
pub(crate) async fn fetch_codex_runtime_models(
@@ -777,7 +777,7 @@ pub(crate) async fn fetch_codex_runtime_models(
api_key: &str,
) -> Result<Vec<codex_catalog::CodexRuntimeModel>, String> {
if port == 0 {
return Err("内核端口无效".to_string());
return Err("Invalid kernel port".to_string());
}
let tls_enabled = managed_core_tls_enabled();
let client = reqwest::Client::builder()
@@ -786,7 +786,7 @@ pub(crate) async fn fetch_codex_runtime_models(
.timeout(Duration::from_secs(15))
.danger_accept_invalid_certs(tls_enabled)
.build()
.map_err(|error| format!("创建 Codex 模型列表客户端失败: {error}"))?;
.map_err(|error| format!("Failed to create Codex model list client: {error}"))?;
let base_url = managed_core_loopback_origin(port);
let endpoints = [
format!("{base_url}/v1/models"),
@@ -802,16 +802,16 @@ pub(crate) async fn fetch_codex_runtime_models(
.header(reqwest::header::USER_AGENT, USER_AGENT)
.send()
.await
.map_err(|error| format!("请求本地 Codex 模型列表失败: {error}"))?;
.map_err(|error| format!("Failed to request local Codex model list: {error}"))?;
let status = response.status();
let body = response
.text()
.await
.map_err(|error| format!("读取本地 Codex 模型列表失败: {error}"))?;
.map_err(|error| format!("Failed to read local Codex model list: {error}"))?;
if status.is_success() {
let payload = serde_json::from_str::<serde_json::Value>(&body).map_err(|error| {
format!(
"解析本地 Codex 模型列表失败: {error}; body={}",
"Failed to parse local Codex model list: {error}; body={}",
truncate_for_error(&body)
)
})?;
@@ -824,7 +824,7 @@ pub(crate) async fn fetch_codex_runtime_models(
}
}
Err("本地内核不支持 Codex 模型列表接口".to_string())
Err("Local kernel does not support the Codex model list endpoint".to_string())
}
pub(crate) async fn fetch_codex_catalog_runtime_models(
@@ -890,7 +890,7 @@ fn sync_prepared_codex_model_catalog(
let home = app
.path()
.home_dir()
.map_err(|error| format!("无法获取用户目录: {error}"))?;
.map_err(|error| format!("Failed to get user directory: {error}"))?;
let changed = sync_codex_model_catalog_if_configured(
&home,
config.port,
@@ -958,7 +958,7 @@ pub(crate) fn start_codex_model_catalog_sync(app: tauri::AppHandle) {
continue;
}
if let Err(error) = refresh_applied_codex_model_catalog(&app, &config).await {
eprintln!("后台同步 Codex 模型目录失败,保留现有配置: {error}");
eprintln!("Failed to synchronize Codex model catalog in the background; keeping the existing configuration: {error}");
}
}
});
@@ -1001,7 +1001,7 @@ pub(crate) fn resolve_claude_desktop_model_mappings(
if client != AgentClient::ClaudeDesktop {
return Ok(None);
}
let mut requested = requested.ok_or("请重新配置 Claude Desktop 的模型与别名")?;
let mut requested = requested.ok_or("Reconfigure the Claude Desktop model and alias")?;
let resolve =
|model: &str| resolve_available_agent_model(models, &validate_agent_model(model)?);
if let Some(entries) = requested.desktop_models.as_mut() {
@@ -1044,10 +1044,10 @@ pub(crate) fn resolve_claude_code_model_mappings(
}
let requested = requested.unwrap_or_else(|| ClaudeDesktopModelMappings::all(selected_model));
if !(100_000..=1_000_000).contains(&requested.max_context_tokens) {
return Err("Claude Code 最大窗口必须介于 100000 和 1000000 之间".to_string());
return Err("Claude Code maximum context window must be between 100000 and 1000000".to_string());
}
if !(1..=100).contains(&requested.auto_compact_pct) {
return Err("Claude Code 触发压缩百分比必须介于 1 和 100 之间".to_string());
return Err("Claude Code compaction trigger percentage must be between 1 and 100".to_string());
}
let max_context_tokens = if requested.opus_1m || requested.sonnet_1m || requested.haiku_1m {
CLAUDE_DESKTOP_EXTENDED_CONTEXT_WINDOW
@@ -1078,7 +1078,7 @@ pub(crate) fn prepare_codex_agent_models(
models: catalog.models,
codex_catalog: Some(catalog.json),
}),
Err(error) if error.contains("CPA 当前没有可写入 Codex 的模型") => {
Err(error) if error.contains("CPA currently has no models that can be written to Codex") => {
Ok(PreparedAgentModels {
models: Vec::new(),
codex_catalog: Some("{\n \"models\": []\n}\n".to_string()),
@@ -1102,7 +1102,7 @@ pub(crate) async fn apply_agent_config(
let home = app
.path()
.home_dir()
.map_err(|error| format!("无法获取用户目录: {error}"))?;
.map_err(|error| format!("Failed to get user directory: {error}"))?;
let config = gui_config_state.snapshot()?;
let api_key = effective_agent_api_key(&config);
if client == AgentClient::Codex && oauth_configuration {
@@ -1128,7 +1128,7 @@ pub(crate) async fn apply_agent_config(
commit_agent_with_core(&config, claude_desktop_model_mappings.as_ref(), &prepared.models, || {
let _guard = AGENT_CONFIG_FILE_LOCK
.lock()
.map_err(|_| "智能体配置文件锁已损坏".to_string())?;
.map_err(|_| "Agent configuration file lock is poisoned".to_string())?;
apply_agent_configuration_with_oauth(
client,
&home,
@@ -1151,10 +1151,10 @@ pub(crate) fn clear_codex_config(app: tauri::AppHandle) -> Result<Vec<String>, S
let home = app
.path()
.home_dir()
.map_err(|error| format!("无法获取用户目录: {error}"))?;
.map_err(|error| format!("Failed to get user directory: {error}"))?;
let _guard = AGENT_CONFIG_FILE_LOCK
.lock()
.map_err(|_| "智能体配置文件锁已损坏".to_string())?;
.map_err(|_| "Agent configuration file lock is poisoned".to_string())?;
clear_codex_config_files(&home)
}
@@ -1174,7 +1174,7 @@ pub(crate) async fn set_agent_config_enabled(
let home = app
.path()
.home_dir()
.map_err(|error| format!("无法获取用户目录: {error}"))?;
.map_err(|error| format!("Failed to get user directory: {error}"))?;
if enabled {
let config = gui_config_state.snapshot()?;
let port = config.port;
@@ -1199,7 +1199,7 @@ pub(crate) async fn set_agent_config_enabled(
commit_agent_with_core(&config, claude_desktop_model_mappings.as_ref(), &prepared.models, || {
let _guard = AGENT_CONFIG_FILE_LOCK
.lock()
.map_err(|_| "智能体配置文件锁已损坏".to_string())?;
.map_err(|_| "Agent configuration file lock is poisoned".to_string())?;
let oauth_configuration = if client == AgentClient::Codex {
current_codex_oauth_configuration(&home)?
} else {
@@ -1224,7 +1224,7 @@ pub(crate) async fn set_agent_config_enabled(
let config = gui_config_state.snapshot()?;
let _guard = AGENT_CONFIG_FILE_LOCK
.lock()
.map_err(|_| "智能体配置文件锁已损坏".to_string())?;
.map_err(|_| "Agent configuration file lock is poisoned".to_string())?;
let _ = force_restore;
let result = clear_agent_managed_configuration(client, &home, config.port)?;
app.state::<AgentConfigStatusCache>().clear()?;
@@ -1275,10 +1275,10 @@ pub(crate) fn validate_agent_can_enable(
pub(crate) fn validate_agent_model(value: &str) -> Result<String, String> {
let model = value.trim();
if model.is_empty() {
return Err("请先选择模型".to_string());
return Err("Select a model first".to_string());
}
if model.len() > 240 || model.chars().any(char::is_control) {
return Err("模型名称格式无效".to_string());
return Err("Invalid model name format".to_string());
}
Ok(model.to_string())
}
@@ -1288,13 +1288,13 @@ pub(crate) fn resolve_available_agent_model(
model: &str,
) -> Result<String, String> {
if models.is_empty() {
return Err("当前内核没有可选模型,无法应用配置修改".to_string());
return Err("The current kernel has no selectable models, so configuration changes cannot be applied".to_string());
}
models
.iter()
.find(|available| available.name.eq_ignore_ascii_case(model))
.map(|available| available.name.clone())
.ok_or_else(|| format!("模型 {model} 不在当前可用模型列表中,请刷新后重新选择"))
.ok_or_else(|| format!("Model {model} is not in the current list of available models. Refresh and select it again"))
}
pub(crate) fn parse_agent_model_options(
@@ -1311,7 +1311,7 @@ pub(crate) fn parse_agent_model_options(
.or_else(|| payload.get("data").and_then(serde_json::Value::as_array))
.or_else(|| payload.get("models").and_then(serde_json::Value::as_array))
.or(object_models.as_ref())
.ok_or_else(|| "本机模型列表响应缺少 data 数组或 models 目录".to_string())?;
.ok_or_else(|| "Local model list response is missing a data array or models catalog".to_string())?;
let mut models = Vec::new();
for item in source {
let name = if let Some(name) = item.as_str() {
@@ -1407,10 +1407,10 @@ pub(crate) fn mark_configured_agent_model_aliases(
content: &str,
) -> Result<(), String> {
let document = serde_norway::from_str::<serde_norway::Value>(content)
.map_err(|error| format!("解析内核 YAML 配置失败: {error}"))?;
.map_err(|error| format!("Failed to parse kernel YAML configuration: {error}"))?;
let root = document
.as_mapping()
.ok_or_else(|| "内核配置顶层必须是 YAML 映射".to_string())?;
.ok_or_else(|| "Kernel configuration root must be a YAML mapping".to_string())?;
for section in MODEL_ALIAS_CONFIG_SECTIONS {
let Some(providers) = yaml_mapping_value(root, section) else {
@@ -1418,7 +1418,7 @@ pub(crate) fn mark_configured_agent_model_aliases(
};
let providers = providers
.as_sequence()
.ok_or_else(|| format!("{section} 必须是数组"))?;
.ok_or_else(|| format!("{section} must be an array"))?;
for provider in providers {
let Some(provider) = provider.as_mapping() else {
continue;
@@ -1431,7 +1431,7 @@ pub(crate) fn mark_configured_agent_model_aliases(
};
let configured_models = configured_models
.as_sequence()
.ok_or_else(|| format!("{section}.models 必须是数组"))?;
.ok_or_else(|| format!("{section}.models must be an array"))?;
for configured in configured_models {
if configured_model_identity(configured).is_some_and(|(_, client_model, _)| {
configured_provider_model_is_enabled(provider, &client_model)
@@ -1445,7 +1445,7 @@ pub(crate) fn mark_configured_agent_model_aliases(
if let Some(oauth_aliases) = yaml_mapping_value(root, "oauth-model-alias") {
let oauth_aliases = oauth_aliases
.as_mapping()
.ok_or_else(|| "oauth-model-alias 必须是 YAML 映射".to_string())?;
.ok_or_else(|| "oauth-model-alias must be a YAML mapping".to_string())?;
for entries in oauth_aliases.values() {
let Some(entries) = entries.as_sequence() else {
continue;
@@ -1485,7 +1485,7 @@ pub(crate) fn parse_codex_model_definitions(
.as_array()
.or_else(|| payload.get("models").and_then(serde_json::Value::as_array))
.or_else(|| payload.get("data").and_then(serde_json::Value::as_array))
.ok_or_else(|| "Codex 模型定义响应缺少 models 或 data 数组".to_string())?;
.ok_or_else(|| "Codex model definition response is missing a models or data array".to_string())?;
let mut definitions = Vec::new();
for item in source {
let id = ["id", "ID", "name"]
@@ -1589,15 +1589,15 @@ pub(crate) fn format_agent_models_error(status: u16, body: &str) -> String {
.map(str::trim)
.filter(|message| !message.is_empty());
if let Some(message) = message {
return format!("获取本机模型列表失败 ({status}): {message}");
return format!("Failed to get local model list ({status}): {message}");
}
}
let body = body.trim();
if body.is_empty() {
format!("获取本机模型列表失败 ({status})")
format!("Failed to get local model list ({status})")
} else {
format!(
"获取本机模型列表失败 ({status}): {}",
"Failed to get local model list ({status}): {}",
truncate_for_error(body)
)
}
+137 -137
View File
@@ -63,7 +63,7 @@ pub(crate) fn build_agent_updates_with_oauth(
}
AgentClient::ClaudeDesktop => {
if paths.len() != 4 {
return Err("Claude Desktop 当前平台配置路径不可用".to_string());
return Err("Claude Desktop configuration path is unavailable on the current platform".to_string());
}
let normal_before = read_optional_text(&paths[0])?;
let threep_before = read_optional_text(&paths[1])?;
@@ -113,7 +113,7 @@ pub(crate) fn build_agent_updates_with_oauth(
path: paths[0].clone(),
after,
}];
let catalog = codex_catalog.ok_or_else(|| "无法生成 Codex 模型目录".to_string())?;
let catalog = codex_catalog.ok_or_else(|| "Unable to generate Codex model catalog".to_string())?;
validate_codex_catalog(catalog, model)?;
updates.push(AgentFileUpdate {
path: codex_model_catalog_path(home),
@@ -164,7 +164,7 @@ pub(crate) fn build_agent_updates_with_oauth(
}
AgentClient::DeepSeekHarness => {
if paths.len() != 2 {
return Err("DeepSeek Harness 配置路径数量无效".to_string());
return Err("Invalid number of DeepSeek Harness configuration paths".to_string());
}
let settings_before = read_optional_text(&paths[0])?;
let credentials_before = read_optional_text(&paths[1])?;
@@ -255,7 +255,7 @@ pub(crate) fn build_agent_updates_with_oauth(
pub(crate) fn read_optional_text(path: &Path) -> Result<Option<String>, String> {
read_agent_bytes(path)?.map(|bytes| String::from_utf8(bytes)
.map_err(|_| "配置不是 UTF-8 文本,请使用手动备份恢复或基础配置模板修复".to_string())).transpose()
.map_err(|_| "Configuration is not UTF-8 text. Restore it from a manual backup or repair it with the base configuration template".to_string())).transpose()
}
pub(crate) fn read_agent_yaml_mapping_or_empty(
@@ -266,7 +266,7 @@ pub(crate) fn read_agent_yaml_mapping_or_empty(
return Ok(serde_norway::Mapping::new());
}
let content = fs::read_to_string(path)
.map_err(|error| format!("读取 {label} 失败 {}: {error}", path_to_string(path)))?;
.map_err(|error| format!("Failed to read {label} {}: {error}", path_to_string(path)))?;
parse_agent_yaml_mapping(Some(&content), label)
}
@@ -278,10 +278,10 @@ pub(crate) fn parse_agent_yaml_mapping(
return Ok(serde_norway::Mapping::new());
};
serde_norway::from_str::<serde_norway::Value>(content)
.map_err(|error| format!("{label} YAML 格式无效: {error}"))?
.map_err(|error| format!("Invalid {label} YAML format: {error}"))?
.as_mapping()
.cloned()
.ok_or_else(|| format!("{label} 根节点必须是 YAML 映射"))
.ok_or_else(|| format!("{label} root must be a YAML mapping"))
}
pub(crate) fn render_agent_yaml_mapping_update<F>(
@@ -306,9 +306,9 @@ where
.map(|_| existing.unwrap_or_default())
.unwrap_or("{}\n");
let mut document = yaml_serde_edit::YamlValue::parse(source)
.map_err(|error| format!("{label} YAML 格式无效: {error}"))?;
.map_err(|error| format!("Invalid {label} YAML format: {error}"))?;
render_updated_core_yaml(&mut document, updated)
.map_err(|error| format!("更新 {label} 失败: {error}"))
.map_err(|error| format!("Failed to update {label}: {error}"))
}
pub(crate) fn build_deepseek_harness_models(
@@ -327,14 +327,14 @@ pub(crate) fn build_deepseek_harness_models(
entry.insert(
yaml_key("contextWindow"),
serde_norway::to_value(context_window)
.map_err(|error| format!("序列化 Harness 模型上下文失败: {error}"))?,
.map_err(|error| format!("Failed to serialize Harness model context: {error}"))?,
);
}
if let Some(input) = model.input_modalities {
entry.insert(
yaml_key("input"),
serde_norway::to_value(input)
.map_err(|_| "序列化 Harness 模型输入类型失败")?,
.map_err(|_| "Failed to serialize Harness model input types")?,
);
}
Ok(serde_norway::Value::Mapping(entry))
@@ -369,17 +369,17 @@ pub(crate) fn render_deepseek_harness_settings(
.entry(yaml_key("llm-pi-ai"))
.or_insert_with(|| serde_norway::Value::Mapping(serde_norway::Mapping::new()))
.as_mapping_mut()
.ok_or_else(|| "DeepSeek Harness llm-pi-ai 必须是映射".to_string())?;
.ok_or_else(|| "DeepSeek Harness llm-pi-ai must be a mapping".to_string())?;
let providers = llm
.entry(yaml_key("providers"))
.or_insert_with(|| serde_norway::Value::Mapping(serde_norway::Mapping::new()))
.as_mapping_mut()
.ok_or_else(|| "DeepSeek Harness llm-pi-ai.providers 必须是映射".to_string())?;
.ok_or_else(|| "DeepSeek Harness llm-pi-ai.providers must be a mapping".to_string())?;
let provider = providers
.entry(yaml_key(DEEPSEEK_HARNESS_PROVIDER_ID))
.or_insert_with(|| serde_norway::Value::Mapping(serde_norway::Mapping::new()))
.as_mapping_mut()
.ok_or_else(|| "DeepSeek Harness EasyCLIProxyAPI provider 必须是映射".to_string())?;
.ok_or_else(|| "DeepSeek Harness EasyCLIProxyAPI provider must be a mapping".to_string())?;
for (key, value) in [
("displayName", "EasyCLIProxyAPI"),
("apiKeyEnv", DEEPSEEK_HARNESS_CREDENTIAL),
@@ -397,7 +397,7 @@ pub(crate) fn render_deepseek_harness_settings(
.entry(yaml_key("agent-default-model"))
.or_insert_with(|| serde_norway::Value::Mapping(serde_norway::Mapping::new()))
.as_mapping_mut()
.ok_or_else(|| "DeepSeek Harness agent-default-model 必须是映射".to_string())?;
.ok_or_else(|| "DeepSeek Harness agent-default-model must be a mapping".to_string())?;
let same_model = selection.get(yaml_key("provider")).and_then(serde_norway::Value::as_str) == Some(DEEPSEEK_HARNESS_PROVIDER_ID)
&& selection.get(yaml_key("model")).and_then(serde_norway::Value::as_str) == Some(model);
selection.insert(
@@ -437,21 +437,21 @@ fn validate_deepseek_harness_versioned_credentials(
label: &str,
) -> Result<(), String> {
let version =
yaml_mapping_value(root, "version").ok_or_else(|| format!("{label} 缺少 version 字段"))?;
yaml_mapping_value(root, "version").ok_or_else(|| format!("{label} is missing the version field"))?;
if !deepseek_harness_credentials_version_is_supported(version) {
return Err(format!(
"{label} version 必须为 {DEEPSEEK_HARNESS_CREDENTIALS_VERSION}"
"{label} version must be {DEEPSEEK_HARNESS_CREDENTIALS_VERSION}"
));
}
for key in root.keys() {
let Some(key) = key.as_str() else {
return Err(format!("{label} 顶层字段名必须是字符串"));
return Err(format!("Top-level field names in {label} must be strings"));
};
let _ = key;
}
for section in ["refs", "records"] {
if yaml_mapping_value(root, section).is_some_and(|value| !value.is_mapping()) {
return Err(format!("{label} {section} 必须是映射"));
return Err(format!("{label} {section} must be a mapping"));
}
}
Ok(())
@@ -474,7 +474,7 @@ fn deepseek_harness_credentials_refs_mut<'a>(
root.entry(yaml_key("refs"))
.or_insert_with(|| serde_norway::Value::Mapping(serde_norway::Mapping::new()))
.as_mapping_mut()
.ok_or_else(|| format!("{label} refs 必须是映射"))
.ok_or_else(|| format!("{label} refs must be a mapping"))
}
fn remove_deepseek_harness_managed_credential(
@@ -531,7 +531,7 @@ pub(crate) fn write_deepseek_harness_file(
fs::set_permissions(path, fs::Permissions::from_mode(0o600)).map_err(|error| {
format!(
"设置 DeepSeek Harness 凭据权限失败 {}: {error}",
"Failed to set DeepSeek Harness credential permissions {}: {error}",
path_to_string(path)
)
})?;
@@ -573,12 +573,12 @@ pub(crate) fn build_claude_agent_config(
) -> Result<String, String> {
let mut root = match existing.map(str::trim).filter(|value| !value.is_empty()) {
Some(value) => serde_json::from_str::<serde_json::Value>(value)
.map_err(|error| format!("Claude Code settings.json 格式无效: {error}"))?,
.map_err(|error| format!("Invalid Claude Code settings.json format: {error}"))?,
None => serde_json::json!({}),
};
let root = root
.as_object_mut()
.ok_or_else(|| "Claude Code settings.json 根节点必须是对象".to_string())?;
.ok_or_else(|| "Claude Code settings.json root must be an object".to_string())?;
let mappings = mappings
.cloned()
.unwrap_or_else(|| ClaudeDesktopModelMappings::all(model));
@@ -649,7 +649,7 @@ pub(crate) fn build_claude_agent_config(
serde_json::Value::String(model_settings.sonnet),
);
let mut rendered = serde_json::to_string_pretty(&serde_json::Value::Object(root.clone()))
.map_err(|error| format!("生成 Claude Code 配置失败: {error}"))?;
.map_err(|error| format!("Failed to generate Claude Code configuration: {error}"))?;
rendered.push('\n');
Ok(rendered)
}
@@ -660,13 +660,13 @@ pub(crate) fn parse_agent_json_object(
) -> Result<serde_json::Map<String, serde_json::Value>, String> {
let value = match existing.map(str::trim).filter(|value| !value.is_empty()) {
Some(value) => serde_json::from_str::<serde_json::Value>(value)
.map_err(|error| format!("{label} 格式无效: {error}"))?,
.map_err(|error| format!("Invalid {label} format: {error}"))?,
None => serde_json::json!({}),
};
value
.as_object()
.cloned()
.ok_or_else(|| format!("{label} 根节点必须是对象"))
.ok_or_else(|| format!("{label} root must be an object"))
}
pub(crate) fn parse_agent_json5_object(
@@ -675,13 +675,13 @@ pub(crate) fn parse_agent_json5_object(
) -> Result<serde_json::Map<String, serde_json::Value>, String> {
let value = match existing.map(str::trim).filter(|value| !value.is_empty()) {
Some(value) => json5::from_str::<serde_json::Value>(value)
.map_err(|error| format!("{label} 格式无效: {error}"))?,
.map_err(|error| format!("Invalid {label} format: {error}"))?,
None => serde_json::json!({}),
};
value
.as_object()
.cloned()
.ok_or_else(|| format!("{label} 根节点必须是对象"))
.ok_or_else(|| format!("{label} root must be an object"))
}
pub(crate) fn ensure_json_object_entry<'a>(
@@ -713,7 +713,7 @@ pub(crate) fn render_agent_json(
label: &str,
) -> Result<String, String> {
let mut rendered = serde_json::to_string_pretty(&serde_json::Value::Object(root))
.map_err(|error| format!("生成 {label} 失败: {error}"))?;
.map_err(|error| format!("Failed to generate {label}: {error}"))?;
rendered.push('\n');
Ok(rendered)
}
@@ -753,9 +753,9 @@ pub(crate) fn ordered_agent_models(
pub(crate) fn build_claude_desktop_deployment_config(
existing: Option<&str>,
) -> Result<String, String> {
let mut root = parse_agent_json_object(existing, "Claude Desktop 配置")?;
let mut root = parse_agent_json_object(existing, "Claude Desktop configuration")?;
root.insert("deploymentMode".to_string(), serde_json::json!("3p"));
render_agent_json(root, "Claude Desktop 配置")
render_agent_json(root, "Claude Desktop configuration")
}
pub(crate) fn build_claude_desktop_profile(
@@ -766,7 +766,7 @@ pub(crate) fn build_claude_desktop_profile(
models: &[AgentModelOption],
mappings: Option<&ClaudeDesktopModelMappings>,
) -> Result<String, String> {
let mut root = parse_agent_json_object(existing, "Claude Desktop 网关配置")?;
let mut root = parse_agent_json_object(existing, "Claude Desktop gateway configuration")?;
root.insert(
"disableDeploymentModeChooser".to_string(),
serde_json::json!(true),
@@ -855,7 +855,7 @@ pub(crate) fn build_claude_desktop_profile(
"inferenceModels".to_string(),
serde_json::Value::Array(deduplicated_models),
);
render_agent_json(root, "Claude Desktop 网关配置")
render_agent_json(root, "Claude Desktop gateway configuration")
}
pub(crate) fn claude_desktop_inference_model(
@@ -911,7 +911,7 @@ pub(crate) fn claude_desktop_family_tier(route_model: &str) -> Option<&'static s
}
pub(crate) fn build_claude_desktop_meta(existing: Option<&str>) -> Result<String, String> {
let mut root = parse_agent_json_object(existing, "Claude Desktop 配置索引")?;
let mut root = parse_agent_json_object(existing, "Claude Desktop configuration index")?;
repair_claude_desktop_meta_names(&mut root);
let entries = ensure_json_array_entry(&mut root, "entries");
let mut managed_entry = None;
@@ -939,7 +939,7 @@ pub(crate) fn build_claude_desktop_meta(existing: Option<&str>) -> Result<String
"appliedId".to_string(),
serde_json::json!(CLAUDE_DESKTOP_PROFILE_ID),
);
render_agent_json(root, "Claude Desktop 配置索引")
render_agent_json(root, "Claude Desktop configuration index")
}
pub(crate) fn repair_claude_desktop_meta_names(root: &mut serde_json::Map<String, serde_json::Value>) {
@@ -976,7 +976,7 @@ pub(crate) fn clear_agent_managed_configuration(
let entry = after
.iter_mut()
.find(|(candidate, _)| candidate == &path)
.ok_or("配置清理路径不匹配")?;
.ok_or("Configuration cleanup path mismatch")?;
entry.1 = bytes;
}
after
@@ -1018,7 +1018,7 @@ where
return Ok(None);
}
let content = fs::read_to_string(path)
.map_err(|error| format!("读取 {label} 失败 {}: {error}", path_to_string(path)))?;
.map_err(|error| format!("Failed to read {label} {}: {error}", path_to_string(path)))?;
let mut root = parse_agent_json_object(Some(&content), label)?;
if !update(&mut root) {
return Ok(None);
@@ -1033,16 +1033,16 @@ where
pub(crate) fn prepare_claude_desktop_managed_removal(paths: &[PathBuf]) -> Result<Images, String> {
if paths.len() != 4 {
return Err("Claude Desktop 当前平台配置路径不可用".to_string());
return Err("Claude Desktop configuration path is unavailable on the current platform".to_string());
}
let meta = read_agent_json_or_empty(&paths[3], "Claude Desktop 配置索引")?;
let meta = read_agent_json_or_empty(&paths[3], "Claude Desktop configuration index")?;
let applied_id = meta.get("appliedId").and_then(serde_json::Value::as_str);
let managed_active = applied_id == Some(CLAUDE_DESKTOP_PROFILE_ID)
|| (applied_id.is_none() && agent_has_managed_marker(AgentClient::ClaudeDesktop, paths)?);
let mut changed = Vec::new();
for (path, label) in [
(&paths[0], "Claude Desktop 主配置"),
(&paths[1], "Claude Desktop 3P 配置"),
(&paths[0], "Claude Desktop main configuration"),
(&paths[1], "Claude Desktop 3P configuration"),
] {
if let Some(update) = prepare_agent_json_removal(path, label, |root| {
if managed_active
@@ -1062,7 +1062,7 @@ pub(crate) fn prepare_claude_desktop_managed_removal(paths: &[PathBuf]) -> Resul
}
if let Some(update) =
prepare_agent_json_removal(&paths[2], "Claude Desktop 网关配置", |root| {
prepare_agent_json_removal(&paths[2], "Claude Desktop gateway configuration", |root| {
let mut updated = false;
for key in [
"disableDeploymentModeChooser",
@@ -1081,7 +1081,7 @@ pub(crate) fn prepare_claude_desktop_managed_removal(paths: &[PathBuf]) -> Resul
}
if let Some(update) =
prepare_agent_json_removal(&paths[3], "Claude Desktop 配置索引", |root| {
prepare_agent_json_removal(&paths[3], "Claude Desktop configuration index", |root| {
let mut updated = false;
if root.get("appliedId").and_then(serde_json::Value::as_str)
== Some(CLAUDE_DESKTOP_PROFILE_ID)
@@ -1119,11 +1119,11 @@ pub(crate) fn prepare_claude_code_managed_removal(
expected_base_url: &str,
) -> Result<Images, String> {
let Some(path) = paths.first() else {
return Err("Claude Code 当前平台配置路径不可用".to_string());
return Err("Claude Code configuration path is unavailable on the current platform".to_string());
};
let expected_base_url = reqwest::Url::parse(expected_base_url)
.map_err(|_| "CPA 接入地址无效".to_string())?;
let updated = prepare_agent_json_removal(path, "Claude Code 配置", |root| {
.map_err(|_| "Invalid CPA access URL".to_string())?;
let updated = prepare_agent_json_removal(path, "Claude Code configuration", |root| {
let managed_model = root
.get("env")
.and_then(serde_json::Value::as_object)
@@ -1193,16 +1193,16 @@ pub(crate) fn prepare_codex_managed_removal(paths: &[PathBuf]) -> Result<Images,
use toml_edit::{Document, Item};
let Some(path) = paths.first() else {
return Err("Codex 当前平台配置路径不可用".to_string());
return Err("Codex configuration path is unavailable on the current platform".to_string());
};
if !path.is_file() {
return Ok(Vec::new());
}
let content = fs::read_to_string(path)
.map_err(|error| format!("读取 Codex 配置失败 {}: {error}", path_to_string(path)))?;
.map_err(|error| format!("Failed to read Codex configuration {}: {error}", path_to_string(path)))?;
let mut document = content
.parse::<Document>()
.map_err(|error| format!("解析 Codex 配置失败: {error}"))?;
.map_err(|error| format!("Failed to parse Codex configuration: {error}"))?;
let managed_selected =
document.get("model_provider").and_then(Item::as_str) == Some(MANAGED_AGENT_PROVIDER_ID);
let managed_catalog =
@@ -1223,7 +1223,7 @@ pub(crate) fn prepare_codex_managed_removal(paths: &[PathBuf]) -> Result<Images,
}
let rendered = document.to_string();
toml::from_str::<toml::Value>(&rendered)
.map_err(|error| format!("验证恢复后的 Codex 配置失败: {error}"))?;
.map_err(|error| format!("Failed to validate restored Codex configuration: {error}"))?;
let mut changed = vec![(
path.clone(),
(!rendered.trim().is_empty()).then(|| rendered.into_bytes()),
@@ -1236,10 +1236,10 @@ pub(crate) fn prepare_codex_managed_removal(paths: &[PathBuf]) -> Result<Images,
pub(crate) fn prepare_opencode_managed_removal(paths: &[PathBuf]) -> Result<Images, String> {
let Some(path) = paths.first() else {
return Err("OpenCode 当前平台配置路径不可用".to_string());
return Err("OpenCode configuration path is unavailable on the current platform".to_string());
};
let prefix = format!("{MANAGED_AGENT_PROVIDER_ID}/");
let updated = prepare_agent_json5_removal(path, "OpenCode 配置", |root| {
let updated = prepare_agent_json5_removal(path, "OpenCode configuration", |root| {
let mut changed = false;
if root
.get("model")
@@ -1268,12 +1268,12 @@ pub(crate) fn prepare_opencode_managed_removal(paths: &[PathBuf]) -> Result<Imag
pub(crate) fn prepare_zcode_managed_removal(paths: &[PathBuf]) -> Result<Images, String> {
if paths.is_empty() {
return Err("ZCode 当前平台配置路径不可用".to_string());
return Err("ZCode configuration path is unavailable on the current platform".to_string());
}
let prefix = format!("{MANAGED_AGENT_PROVIDER_ID}/");
let mut changed_paths = Vec::new();
for path in paths {
let updated = prepare_agent_json_removal(path, "ZCode 配置", |root| {
let updated = prepare_agent_json_removal(path, "ZCode configuration", |root| {
let mut changed = false;
let remove_model = if root
.get("model")
@@ -1355,16 +1355,16 @@ pub(crate) fn prepare_managed_toml_client_removal(
use toml_edit::{Document, Item};
let Some(path) = paths.first() else {
return Err(format!("{label} 当前平台配置路径不可用"));
return Err(format!("{label} configuration path is unavailable on the current platform"));
};
if !path.is_file() {
return Ok(Vec::new());
}
let content = fs::read_to_string(path)
.map_err(|error| format!("读取 {label} 配置失败 {}: {error}", path_to_string(path)))?;
.map_err(|error| format!("Failed to read {label} configuration {}: {error}", path_to_string(path)))?;
let mut document = content
.parse::<Document>()
.map_err(|error| format!("解析 {label} 配置失败: {error}"))?;
.map_err(|error| format!("Failed to parse {label} configuration: {error}"))?;
let prefix = format!("{MANAGED_AGENT_PROVIDER_ID}/");
let mut changed = false;
@@ -1437,7 +1437,7 @@ pub(crate) fn prepare_managed_toml_client_removal(
}
let rendered = document.to_string();
toml::from_str::<toml::Value>(&rendered)
.map_err(|error| format!("验证恢复后的 {label} 配置失败: {error}"))?;
.map_err(|error| format!("Failed to validate restored {label} configuration: {error}"))?;
Ok(vec![(
path.clone(),
(!rendered.trim().is_empty()).then(|| rendered.into_bytes()),
@@ -1456,13 +1456,13 @@ where
return Ok(None);
}
let content = fs::read_to_string(path)
.map_err(|error| format!("读取 {label} 失败 {}: {error}", path_to_string(path)))?;
.map_err(|error| format!("Failed to read {label} {}: {error}", path_to_string(path)))?;
let value = json5::from_str::<serde_json::Value>(&content)
.map_err(|error| format!("解析 {label} 失败: {error}"))?;
.map_err(|error| format!("Failed to parse {label}: {error}"))?;
let mut root = value
.as_object()
.cloned()
.ok_or_else(|| format!("{label} 根节点必须是对象"))?;
.ok_or_else(|| format!("{label} root must be an object"))?;
if !update(&mut root) {
return Ok(None);
}
@@ -1485,10 +1485,10 @@ where
pub(crate) fn prepare_openclaw_managed_removal(paths: &[PathBuf]) -> Result<Images, String> {
let Some(path) = paths.first() else {
return Err("OpenClaw 当前平台配置路径不可用".to_string());
return Err("OpenClaw configuration path is unavailable on the current platform".to_string());
};
let prefix = format!("{MANAGED_AGENT_PROVIDER_ID}/");
let updated = prepare_agent_json5_removal(path, "OpenClaw 配置", |root| {
let updated = prepare_agent_json5_removal(path, "OpenClaw configuration", |root| {
let mut changed = false;
if let Some(models) = root
.get_mut("models")
@@ -1554,19 +1554,19 @@ pub(crate) fn prepare_openclaw_managed_removal(paths: &[PathBuf]) -> Result<Imag
pub(crate) fn prepare_hermes_managed_removal(paths: &[PathBuf]) -> Result<Images, String> {
let Some(path) = paths.first() else {
return Err("Hermes 当前平台配置路径不可用".to_string());
return Err("Hermes configuration path is unavailable on the current platform".to_string());
};
if !path.is_file() {
return Ok(Vec::new());
}
let content = fs::read_to_string(path)
.map_err(|error| format!("读取 Hermes 配置失败 {}: {error}", path_to_string(path)))?;
.map_err(|error| format!("Failed to read Hermes configuration {}: {error}", path_to_string(path)))?;
let mut document = yaml_serde_edit::YamlValue::parse(&content)
.map_err(|error| format!("解析 Hermes 配置失败: {error}"))?;
.map_err(|error| format!("Failed to parse Hermes configuration: {error}"))?;
let mut updated = document.get().clone();
let root = updated
.as_mapping_mut()
.ok_or_else(|| "Hermes 配置根节点必须是映射".to_string())?;
.ok_or_else(|| "Hermes configuration root must be a mapping".to_string())?;
let mut changed = false;
let providers_empty = if let Some(providers) = root
.get_mut(yaml_key("custom_providers"))
@@ -1681,12 +1681,12 @@ pub(crate) fn prepare_deepseek_harness_managed_removal(
paths: &[PathBuf],
) -> Result<Images, String> {
if paths.len() != 2 {
return Err("DeepSeek Harness 配置路径数量无效".to_string());
return Err("Invalid number of DeepSeek Harness configuration paths".to_string());
}
let mut changed_paths = Vec::new();
if paths[0].is_file() {
let current = fs::read_to_string(&paths[0])
.map_err(|error| format!("读取 DeepSeek Harness settings 失败: {error}"))?;
.map_err(|error| format!("Failed to read DeepSeek Harness settings: {error}"))?;
let mut changed = false;
let rendered = render_agent_yaml_mapping_update(
Some(&current),
@@ -1706,7 +1706,7 @@ pub(crate) fn prepare_deepseek_harness_managed_removal(
}
if paths[1].is_file() {
let current = fs::read_to_string(&paths[1])
.map_err(|error| format!("读取 DeepSeek Harness credentials 失败: {error}"))?;
.map_err(|error| format!("Failed to read DeepSeek Harness credentials: {error}"))?;
let mut changed = false;
let rendered = render_agent_yaml_mapping_update(
Some(&current),
@@ -1777,8 +1777,8 @@ pub(crate) fn build_restored_claude_code_config(
current: &str,
original: Option<&str>,
) -> Result<Option<String>, String> {
let mut root = parse_agent_json_object(Some(current), "当前 Claude Code 配置")?;
let original_root = parse_restored_json_object(original, "原始 Claude Code 配置")?;
let mut root = parse_agent_json_object(Some(current), "Current Claude Code configuration")?;
let original_root = parse_restored_json_object(original, "Original Claude Code configuration")?;
restore_json_key(&mut root, original_root.as_ref(), "model");
let original_env = original_root
.as_ref()
@@ -1824,7 +1824,7 @@ pub(crate) fn build_restored_claude_code_config(
} else {
restore_json_key(&mut root, original_root.as_ref(), "env");
}
render_restored_json(root, original.is_some(), "恢复后的 Claude Code 配置")
render_restored_json(root, original.is_some(), "Restored Claude Code configuration")
}
pub(crate) fn build_restored_claude_desktop_config(
@@ -1832,8 +1832,8 @@ pub(crate) fn build_restored_claude_desktop_config(
current: &str,
original: Option<&str>,
) -> Result<Option<String>, String> {
let mut root = parse_agent_json_object(Some(current), "当前 Claude Desktop 配置")?;
let original_root = parse_restored_json_object(original, "原始 Claude Desktop 配置")?;
let mut root = parse_agent_json_object(Some(current), "Current Claude Desktop configuration")?;
let original_root = parse_restored_json_object(original, "Original Claude Desktop configuration")?;
match index {
0 | 1 => restore_json_key(&mut root, original_root.as_ref(), "deploymentMode"),
2 => {
@@ -1879,17 +1879,17 @@ pub(crate) fn build_restored_claude_desktop_config(
root.insert("entries".to_string(), serde_json::Value::Array(entries));
}
}
_ => return Err("Claude Desktop 配置文件索引无效".to_string()),
_ => return Err("Invalid Claude Desktop configuration file index".to_string()),
}
render_restored_json(root, original.is_some(), "恢复后的 Claude Desktop 配置")
render_restored_json(root, original.is_some(), "Restored Claude Desktop configuration")
}
pub(crate) fn build_restored_opencode_config(
current: &str,
original: Option<&str>,
) -> Result<Option<String>, String> {
let mut root = parse_agent_json5_object(Some(current), "当前 OpenCode 配置")?;
let original_root = parse_restored_json5_object(original, "原始 OpenCode 配置")?;
let mut root = parse_agent_json5_object(Some(current), "Current OpenCode configuration")?;
let original_root = parse_restored_json5_object(original, "Original OpenCode configuration")?;
for key in ["$schema", "model"] {
restore_json_key(&mut root, original_root.as_ref(), key);
}
@@ -1954,7 +1954,7 @@ pub(crate) fn build_restored_opencode_config(
} else {
restore_json_key(&mut root, original_root.as_ref(), "provider");
}
let restored = render_restored_json(root, original.is_some(), "恢复后的 OpenCode 配置")?;
let restored = render_restored_json(root, original.is_some(), "Restored OpenCode configuration")?;
Ok(restored.map(|rendered| {
let comments = extract_json5_comments(current);
if comments.is_empty() {
@@ -1969,8 +1969,8 @@ pub(crate) fn build_restored_zcode_config(
current: &str,
original: Option<&str>,
) -> Result<Option<String>, String> {
let mut root = parse_agent_json_object(Some(current), "当前 ZCode 配置")?;
let original_root = parse_restored_json_object(original, "原始 ZCode 配置")?;
let mut root = parse_agent_json_object(Some(current), "Current ZCode configuration")?;
let original_root = parse_restored_json_object(original, "Original ZCode configuration")?;
restore_json_key(&mut root, original_root.as_ref(), "model");
let original_provider = original_root
.as_ref()
@@ -2042,7 +2042,7 @@ pub(crate) fn build_restored_zcode_config(
} else {
restore_json_key(&mut root, original_root.as_ref(), "provider");
}
render_restored_json(root, original.is_some(), "恢复后的 ZCode 配置")
render_restored_json(root, original.is_some(), "Restored ZCode configuration")
}
pub(crate) fn build_restored_kimi_code_config(
@@ -2086,9 +2086,9 @@ pub(crate) fn build_restored_managed_toml_client_config(
) -> Result<Option<String>, String> {
use toml_edit::Item;
let mut current_document = parse_codex_document(Some(current), &format!("当前 {label} 配置"))?;
let mut current_document = parse_codex_document(Some(current), &format!("Current {label} configuration"))?;
let original_document = original
.map(|content| parse_codex_document(Some(content), &format!("原始 {label} 配置")))
.map(|content| parse_codex_document(Some(content), &format!("Original {label} configuration")))
.transpose()?;
if let Some(original_document) = original_document.as_ref() {
merge_missing_codex_table_items(
@@ -2199,7 +2199,7 @@ pub(crate) fn build_restored_managed_toml_client_config(
}
let rendered = current_document.to_string();
toml::from_str::<toml::Value>(&rendered)
.map_err(|error| format!("验证恢复后的 {label} 配置失败: {error}"))?;
.map_err(|error| format!("Failed to validate restored {label} configuration: {error}"))?;
if original.is_none() && rendered.trim().is_empty() {
Ok(None)
} else {
@@ -2212,18 +2212,18 @@ pub(crate) fn build_restored_openclaw_config(
original: Option<&str>,
) -> Result<Option<String>, String> {
let current_value = json5::from_str::<serde_json::Value>(current)
.map_err(|error| format!("当前 OpenClaw 配置格式无效: {error}"))?;
.map_err(|error| format!("Invalid current OpenClaw configuration format: {error}"))?;
let mut root = current_value
.as_object()
.cloned()
.ok_or_else(|| "当前 OpenClaw 配置根节点必须是对象".to_string())?;
.ok_or_else(|| "Current OpenClaw configuration root must be an object".to_string())?;
let original_root = original
.map(|content| {
json5::from_str::<serde_json::Value>(content)
.map_err(|error| format!("原始 OpenClaw 配置格式无效: {error}"))?
.map_err(|error| format!("Invalid original OpenClaw configuration format: {error}"))?
.as_object()
.cloned()
.ok_or_else(|| "原始 OpenClaw 配置根节点必须是对象".to_string())
.ok_or_else(|| "Original OpenClaw configuration root must be an object".to_string())
})
.transpose()?;
let original_models = original_root
@@ -2364,7 +2364,7 @@ pub(crate) fn build_restored_openclaw_config(
if root.is_empty() && original.is_none() {
return Ok(None);
}
let rendered = render_agent_json(root, "恢复后的 OpenClaw 配置")?;
let rendered = render_agent_json(root, "Restored OpenClaw configuration")?;
let comments = extract_json5_comments(current);
Ok(Some(if comments.is_empty() {
rendered
@@ -2390,15 +2390,15 @@ pub(crate) fn build_restored_hermes_config(
original: Option<&str>,
) -> Result<Option<String>, String> {
let mut document = yaml_serde_edit::YamlValue::parse(current)
.map_err(|error| format!("当前 Hermes 配置格式无效: {error}"))?;
.map_err(|error| format!("Invalid current Hermes configuration format: {error}"))?;
let mut updated = document.get().clone();
let root = updated
.as_mapping_mut()
.ok_or_else(|| "当前 Hermes 配置根节点必须是映射".to_string())?;
.ok_or_else(|| "Current Hermes configuration root must be a mapping".to_string())?;
let original_value = original
.map(|content| {
serde_norway::from_str::<serde_norway::Value>(content)
.map_err(|error| format!("原始 Hermes 配置格式无效: {error}"))
.map_err(|error| format!("Invalid original Hermes configuration format: {error}"))
})
.transpose()?;
let original_root = original_value
@@ -2494,12 +2494,12 @@ pub(crate) fn restore_deepseek_harness_provider(
.entry(yaml_key("llm-pi-ai"))
.or_insert_with(|| serde_norway::Value::Mapping(serde_norway::Mapping::new()))
.as_mapping_mut()
.ok_or_else(|| "当前 DeepSeek Harness llm-pi-ai 必须是映射".to_string())?;
.ok_or_else(|| "Current DeepSeek Harness llm-pi-ai must be a mapping".to_string())?;
let providers = llm
.entry(yaml_key("providers"))
.or_insert_with(|| serde_norway::Value::Mapping(serde_norway::Mapping::new()))
.as_mapping_mut()
.ok_or_else(|| "当前 DeepSeek Harness llm-pi-ai.providers 必须是映射".to_string())?;
.ok_or_else(|| "Current DeepSeek Harness llm-pi-ai.providers must be a mapping".to_string())?;
providers.insert(yaml_key(DEEPSEEK_HARNESS_PROVIDER_ID), original_provider);
} else {
let mut remove_llm = false;
@@ -2531,17 +2531,17 @@ pub(crate) fn build_restored_deepseek_harness_settings(
current: &str,
original: Option<&str>,
) -> Result<Option<String>, String> {
let original_root = parse_agent_yaml_mapping(original, "原始 DeepSeek Harness settings")?;
let original_root = parse_agent_yaml_mapping(original, "Original DeepSeek Harness settings")?;
let rendered = render_agent_yaml_mapping_update(
Some(current),
"当前 DeepSeek Harness settings",
"Current DeepSeek Harness settings",
|root| {
restore_deepseek_harness_provider(root, Some(&original_root))?;
restore_yaml_key(root, Some(&original_root), "agent-default-model");
Ok(())
},
)?;
let root = parse_agent_yaml_mapping(Some(&rendered), "恢复后的 DeepSeek Harness settings")?;
let root = parse_agent_yaml_mapping(Some(&rendered), "Restored DeepSeek Harness settings")?;
Ok((!root.is_empty() || original.is_some()).then_some(rendered))
}
@@ -2549,15 +2549,15 @@ pub(crate) fn build_restored_deepseek_harness_credentials(
current: &str,
original: Option<&str>,
) -> Result<Option<String>, String> {
let original_root = parse_agent_yaml_mapping(original, "原始 DeepSeek Harness credentials")?;
let original_root = parse_agent_yaml_mapping(original, "Original DeepSeek Harness credentials")?;
let original_credential = deepseek_harness_original_credential(&original_root).cloned();
let original_had_refs = yaml_mapping_value(&original_root, "refs").is_some();
let rendered = render_agent_yaml_mapping_update(
Some(current),
"当前 DeepSeek Harness credentials",
"Current DeepSeek Harness credentials",
|root| {
let refs =
deepseek_harness_credentials_refs_mut(root, "当前 DeepSeek Harness credentials")?;
deepseek_harness_credentials_refs_mut(root, "Current DeepSeek Harness credentials")?;
if let Some(value) = original_credential {
refs.insert(yaml_key(DEEPSEEK_HARNESS_CREDENTIAL), value);
} else {
@@ -2570,7 +2570,7 @@ pub(crate) fn build_restored_deepseek_harness_credentials(
Ok(())
},
)?;
let root = parse_agent_yaml_mapping(Some(&rendered), "恢复后的 DeepSeek Harness credentials")?;
let root = parse_agent_yaml_mapping(Some(&rendered), "Restored DeepSeek Harness credentials")?;
if deepseek_harness_credentials_document_is_empty(&root) {
if let Some(original) = original.filter(|_| original_root.is_empty()) {
return Ok(Some(original.to_string()));
@@ -2638,12 +2638,12 @@ pub(crate) fn build_agent_session_restored_bytes_with_preference(
return Ok(original.map(ToOwned::to_owned));
}
let current = std::str::from_utf8(current)
.map_err(|_| format!("当前智能体配置不是 UTF-8 文本: {}", path_to_string(path)))?;
.map_err(|_| format!("Current agent configuration is not UTF-8 text: {}", path_to_string(path)))?;
let original_bytes = original;
let original = original_bytes
.map(|bytes| {
std::str::from_utf8(bytes)
.map_err(|_| format!("原智能体配置不是 UTF-8 文本: {}", path_to_string(path)))
.map_err(|_| format!("Original agent configuration is not UTF-8 text: {}", path_to_string(path)))
})
.transpose()?;
let restored = match client {
@@ -2652,7 +2652,7 @@ pub(crate) fn build_agent_session_restored_bytes_with_preference(
let index = paths
.iter()
.position(|candidate| candidate == path)
.ok_or_else(|| "Claude Desktop 恢复路径不匹配".to_string())?;
.ok_or_else(|| "Claude Desktop restoration path mismatch".to_string())?;
build_restored_claude_desktop_config(index, current, original)?
}
AgentClient::Codex => build_restored_codex_agent_config(Some(current), original)?,
@@ -2663,11 +2663,11 @@ pub(crate) fn build_agent_session_restored_bytes_with_preference(
let index = paths
.iter()
.position(|candidate| candidate == path)
.ok_or_else(|| "DeepSeek Harness 恢复路径不匹配".to_string())?;
.ok_or_else(|| "DeepSeek Harness restoration path mismatch".to_string())?;
match index {
0 => build_restored_deepseek_harness_settings(current, original)?,
1 => build_restored_deepseek_harness_credentials(current, original)?,
_ => return Err("DeepSeek Harness 恢复路径索引无效".to_string()),
_ => return Err("Invalid DeepSeek Harness restoration path index".to_string()),
}
}
AgentClient::ZCode => build_restored_zcode_config(current, original)?,
@@ -2710,7 +2710,7 @@ pub(crate) fn build_codex_agent_config_with_oauth(
let mut document = match existing.filter(|value| !value.trim().is_empty()) {
Some(value) => value
.parse::<Document>()
.map_err(|error| format!("Codex config.toml 格式无效: {error}"))?,
.map_err(|error| format!("Invalid Codex config.toml format: {error}"))?,
None => Document::new(),
};
set_codex_table_item(
@@ -2733,7 +2733,7 @@ pub(crate) fn build_codex_agent_config_with_oauth(
}
let providers = document["model_providers"]
.as_table_mut()
.ok_or_else(|| "Codex model_providers 必须是 TOML 表".to_string())?;
.ok_or_else(|| "Codex model_providers must be a TOML table".to_string())?;
if !providers
.get(MANAGED_AGENT_PROVIDER_ID)
.is_some_and(toml_edit::Item::is_table)
@@ -2744,7 +2744,7 @@ pub(crate) fn build_codex_agent_config_with_oauth(
let provider = providers
.get_mut(MANAGED_AGENT_PROVIDER_ID)
.and_then(Item::as_table_mut)
.ok_or_else(|| "Codex cpa-gui provider 必须是 TOML 表".to_string())?;
.ok_or_else(|| "Codex cpa-gui provider must be a TOML table".to_string())?;
set_codex_table_item(provider, "name", value("EasyCLIProxyAPI"));
set_codex_table_item(provider, "base_url", value(base_url));
set_codex_table_item(provider, "wire_api", value("responses"));
@@ -2756,7 +2756,7 @@ pub(crate) fn build_codex_agent_config_with_oauth(
}
let rendered = document.to_string();
toml::from_str::<toml::Value>(&rendered)
.map_err(|error| format!("验证 Codex 配置失败: {error}"))?;
.map_err(|error| format!("Failed to validate Codex configuration: {error}"))?;
Ok(rendered)
}
@@ -2765,7 +2765,7 @@ pub(crate) fn build_codex_api_auth(api_key: &str) -> Result<String, String> {
"auth_mode": "apikey",
"OPENAI_API_KEY": api_key,
}))
.map_err(|error| format!("生成 Codex auth.json 失败: {error}"))?;
.map_err(|error| format!("Failed to generate Codex auth.json: {error}"))?;
rendered.push('\n');
Ok(rendered)
}
@@ -2779,7 +2779,7 @@ pub(crate) fn build_codex_auth_update(
let after = if oauth_configuration {
if validate_codex_oauth_login_at(&path).is_ok() {
read_optional_text(&path)?
.ok_or_else(|| "Codex OAuth 登录凭据在应用配置前已被删除,请重新登录".to_string())?
.ok_or_else(|| "Codex OAuth login credentials were deleted before the application configuration was applied. Sign in again".to_string())?
} else {
available_codex_oauth_auth(home)?
}
@@ -2848,7 +2848,7 @@ pub(crate) fn parse_codex_document(
match content.filter(|value| !value.trim().is_empty()) {
Some(value) => value
.parse::<Document>()
.map_err(|error| format!("{label} 格式无效: {error}")),
.map_err(|error| format!("Invalid {label} format: {error}")),
None => Ok(Document::new()),
}
}
@@ -2874,14 +2874,14 @@ pub(crate) fn ensure_codex_provider_table(
let providers = root
.get_mut("model_providers")
.and_then(Item::as_table_mut)
.ok_or_else(|| "Codex model_providers 必须是 TOML 表".to_string())?;
.ok_or_else(|| "Codex model_providers must be a TOML table".to_string())?;
if !providers.contains_key(MANAGED_AGENT_PROVIDER_ID) {
providers.insert(MANAGED_AGENT_PROVIDER_ID, Item::Table(Table::new()));
}
providers
.get_mut(MANAGED_AGENT_PROVIDER_ID)
.and_then(Item::as_table_mut)
.ok_or_else(|| "Codex cpa-gui provider 必须是 TOML 表".to_string())
.ok_or_else(|| "Codex cpa-gui provider must be a TOML table".to_string())
}
pub(crate) fn merge_missing_codex_table_items(
@@ -2916,9 +2916,9 @@ pub(crate) fn build_restored_codex_agent_config_with_policy(
) -> Result<Option<String>, String> {
use toml_edit::Item;
let mut current_document = parse_codex_document(current, "当前 Codex config.toml")?;
let mut current_document = parse_codex_document(current, "Current Codex config.toml")?;
let original_document = original
.map(|content| parse_codex_document(Some(content), "原始 Codex config.toml"))
.map(|content| parse_codex_document(Some(content), "Original Codex config.toml"))
.transpose()?;
if let Some(original_document) = original_document.as_ref() {
merge_missing_codex_table_items(
@@ -2995,7 +2995,7 @@ pub(crate) fn build_restored_codex_agent_config_with_policy(
let rendered = current_document.to_string();
toml::from_str::<toml::Value>(&rendered)
.map_err(|error| format!("验证恢复后的 Codex 配置失败: {error}"))?;
.map_err(|error| format!("Failed to validate restored Codex configuration: {error}"))?;
if original.is_none() && rendered.trim().is_empty() {
Ok(None)
} else {
@@ -3005,19 +3005,19 @@ pub(crate) fn build_restored_codex_agent_config_with_policy(
pub(crate) fn validate_codex_catalog(catalog: &str, model: &str) -> Result<(), String> {
let root: serde_json::Value = serde_json::from_str(catalog)
.map_err(|error| format!("Codex 模型目录格式无效: {error}"))?;
.map_err(|error| format!("Invalid Codex model catalog format: {error}"))?;
let models = root
.get("models")
.and_then(serde_json::Value::as_array)
.filter(|models| !models.is_empty())
.ok_or_else(|| "Codex 模型目录必须包含非空 models 数组".to_string())?;
.ok_or_else(|| "Codex model catalog must contain a non-empty models array".to_string())?;
if !models.iter().any(|entry| {
entry
.get("slug")
.and_then(serde_json::Value::as_str)
.is_some_and(|slug| slug.eq_ignore_ascii_case(model))
}) {
return Err(format!("默认模型 {model} 不在生成的 Codex 模型目录中"));
return Err(format!("Default model {model} is not in the generated Codex model catalog"));
}
Ok(())
}
@@ -3054,7 +3054,7 @@ pub(crate) fn build_opencode_agent_config(
"model".to_string(),
serde_json::json!(format!("{MANAGED_AGENT_PROVIDER_ID}/{model}")),
);
let rendered = render_agent_json(root, "OpenCode 配置")?;
let rendered = render_agent_json(root, "OpenCode configuration")?;
let comments = existing.map(extract_json5_comments).unwrap_or_default();
if comments.is_empty() {
Ok(rendered)
@@ -3130,7 +3130,7 @@ fn build_zcode_config(
} else {
root.insert("model".to_string(), serde_json::json!(model));
}
render_agent_json(root, "ZCode 配置")
render_agent_json(root, "ZCode configuration")
}
pub(crate) fn ensure_toml_child_table<'a>(
@@ -3205,7 +3205,7 @@ pub(crate) fn build_kimi_code_agent_config(
}
let rendered = document.to_string();
toml::from_str::<toml::Value>(&rendered)
.map_err(|error| format!("验证 Kimi Code 配置失败: {error}"))?;
.map_err(|error| format!("Failed to validate Kimi Code configuration: {error}"))?;
Ok(rendered)
}
@@ -3251,7 +3251,7 @@ pub(crate) fn build_grok_build_agent_config(
}
let rendered = document.to_string();
toml::from_str::<toml::Value>(&rendered)
.map_err(|error| format!("验证 Grok Build 配置失败: {error}"))?;
.map_err(|error| format!("Failed to validate Grok Build configuration: {error}"))?;
Ok(rendered)
}
@@ -3264,12 +3264,12 @@ pub(crate) fn build_openclaw_agent_config(
) -> Result<String, String> {
let mut root = match existing.map(str::trim).filter(|value| !value.is_empty()) {
Some(value) => json5::from_str::<serde_json::Value>(value)
.map_err(|error| format!("OpenClaw openclaw.json 格式无效: {error}"))?,
.map_err(|error| format!("Invalid OpenClaw openclaw.json format: {error}"))?,
None => serde_json::json!({}),
};
let root = root
.as_object_mut()
.ok_or_else(|| "OpenClaw openclaw.json 根节点必须是对象".to_string())?;
.ok_or_else(|| "OpenClaw openclaw.json root must be an object".to_string())?;
let ordered_models = ordered_agent_models(available_models, model);
let models = ensure_json_object_entry(root, "models");
models
@@ -3313,7 +3313,7 @@ pub(crate) fn build_openclaw_agent_config(
.unwrap_or_else(|| serde_json::json!({}));
model_catalog.insert(name, value);
}
let rendered = render_agent_json(root.clone(), "OpenClaw 配置")?;
let rendered = render_agent_json(root.clone(), "OpenClaw configuration")?;
let comments = existing.map(extract_json5_comments).unwrap_or_default();
if comments.is_empty() {
Ok(rendered)
@@ -3414,13 +3414,13 @@ pub(crate) fn build_hermes_agent_config(
) -> Result<String, String> {
let original = match existing.map(str::trim).filter(|value| !value.is_empty()) {
Some(value) => serde_norway::from_str::<serde_norway::Value>(value)
.map_err(|error| format!("Hermes config.yaml 格式无效: {error}"))?,
.map_err(|error| format!("Invalid Hermes config.yaml format: {error}"))?,
None => serde_norway::Value::Mapping(serde_norway::Mapping::new()),
};
let mut root = original.clone();
let mapping = root
.as_mapping_mut()
.ok_or_else(|| "Hermes config.yaml 根节点必须是映射".to_string())?;
.ok_or_else(|| "Hermes config.yaml root must be a mapping".to_string())?;
let providers = ensure_yaml_sequence_entry(mapping, "custom_providers");
let mut managed_provider = None;
let mut retained_providers = Vec::with_capacity(providers.len());
@@ -3447,10 +3447,10 @@ pub(crate) fn build_hermes_agent_config(
"model": model,
"models": provider_models
}))
.map_err(|error| format!("生成 Hermes provider 失败: {error}"))?;
.map_err(|error| format!("Failed to generate Hermes provider: {error}"))?;
let canonical_provider = canonical_provider
.as_mapping()
.ok_or_else(|| "生成 Hermes provider 失败: 根节点不是映射".to_string())?;
.ok_or_else(|| "Failed to generate Hermes provider: root is not a mapping".to_string())?;
let mut managed_provider = managed_provider.unwrap_or_default();
for (key, value) in canonical_provider {
managed_provider.insert(key.clone(), value.clone());
@@ -3468,11 +3468,11 @@ pub(crate) fn build_hermes_agent_config(
);
let rendered = if let Some(existing) = existing.filter(|value| !value.trim().is_empty()) {
render_yaml_value_changes(existing, &original, &root)
.or_else(|_| serde_norway::to_string(&root).map_err(|_| "生成 Hermes 配置失败".to_string()))?
.or_else(|_| serde_norway::to_string(&root).map_err(|_| "Failed to generate Hermes configuration".to_string()))?
} else {
serde_norway::to_string(&root).map_err(|error| format!("生成 Hermes 配置失败: {error}"))?
serde_norway::to_string(&root).map_err(|error| format!("Failed to generate Hermes configuration: {error}"))?
};
serde_norway::from_str::<serde_norway::Value>(&rendered)
.map_err(|error| format!("验证 Hermes 配置失败: {error}"))?;
.map_err(|error| format!("Failed to validate Hermes configuration: {error}"))?;
Ok(rendered)
}
+24 -24
View File
@@ -58,7 +58,7 @@ fn read_harness_state(bytes: Option<&[u8]>) -> Result<DeepSeekHarnessCatalogStat
bytes
.map(|bytes| {
serde_json::from_slice(bytes)
.map_err(|_| "读取 DSH 模型配置状态失败,请检查 catalog-state.json".into())
.map_err(|_| "Failed to read DSH model configuration state. Check catalog-state.json".into())
})
.unwrap_or_else(|| Ok(Default::default()))
}
@@ -74,7 +74,7 @@ fn harness_root(existing: Option<&str>) -> Result<Value, String> {
existing,
"DeepSeek Harness settings",
)?)
.map_err(|_| "DSH settings 必须是有效的配置映射".into())
.map_err(|_| "DSH settings must be a valid configuration mapping".into())
}
fn selected_fields(value: &Value, group: &str) -> Profile {
@@ -284,13 +284,13 @@ pub(crate) fn validate_harness_profile(
let field = definitions
.iter()
.find(|field| field["name"] == key.as_str())
.ok_or_else(|| format!("{path}.{key}: 不支持的配置字段"))?;
.ok_or_else(|| format!("{path}.{key}: unsupported configuration field"))?;
validate_harness_field(value, field, api, &format!("{path}.{key}"))?;
}
for field in definitions {
if field["required"] == true && !profile.contains_key(field["name"].as_str().unwrap()) {
return Err(format!(
"{path}.{}: 必须填写",
"{path}.{}: required",
field["name"].as_str().unwrap()
));
}
@@ -305,7 +305,7 @@ pub(crate) fn validate_harness_profile(
.and_then(Value::as_f64)
.unwrap_or(10000.0)
{
return Err(format!("{path}: initialDelayMs 不能大于 maxDelayMs"));
return Err(format!("{path}: initialDelayMs cannot be greater than maxDelayMs"));
}
Ok(())
}
@@ -316,14 +316,14 @@ fn validate_harness_field(
api: &str,
path: &str,
) -> Result<(), String> {
let invalid = || format!("{path}: 配置值不符合 DSH 文档要求");
let invalid = || format!("{path}: configuration value does not meet DSH documentation requirements");
if !api.is_empty()
&& field
.get("apis")
.and_then(Value::as_array)
.is_some_and(|apis| !apis.contains(&json!(api)))
{
return Err(format!("{path}: 不适用于 {api},请恢复自动或选择对应协议"));
return Err(format!("{path}: not applicable to {api}; restore automatic selection or choose the matching protocol"));
}
let valid = match field["kind"].as_str().unwrap() {
"group" => {
@@ -407,7 +407,7 @@ fn render_harness_profiles(
state: &mut DeepSeekHarnessCatalogState,
) -> Result<String, String> {
if models.is_empty() {
return Err("DSH 模型列表为空,已保留现有配置,请刷新后重试".into());
return Err("DSH model list is empty. Existing configuration was preserved; refresh and try again".into());
}
let root = harness_root(existing)?;
let old = harness_provider(&root);
@@ -452,7 +452,7 @@ fn render_harness_profiles(
.and_then(|v| v.get_mut("providers"))
.and_then(|v| v.get_mut(DEEPSEEK_HARNESS_PROVIDER_ID))
.and_then(serde_norway::Value::as_mapping_mut)
.ok_or("DSH provider 配置缺失")?;
.ok_or("DSH provider configuration is missing")?;
managed.insert(
yaml_key("baseURL"),
serde_norway::Value::String(
@@ -470,12 +470,12 @@ fn render_harness_profiles(
for (key, value) in &provider {
managed.insert(
yaml_key(key),
serde_norway::to_value(value).map_err(|_| "序列化 DSH provider 失败")?,
serde_norway::to_value(value).map_err(|_| "Failed to serialize DSH provider")?,
);
}
managed.insert(
yaml_key("models"),
serde_norway::to_value(&published).map_err(|_| "序列化 DSH 模型失败")?,
serde_norway::to_value(&published).map_err(|_| "Failed to serialize DSH model")?,
);
Ok(())
})?;
@@ -486,7 +486,7 @@ fn render_harness_profiles(
pub(crate) fn deepseek_harness_template_catalog_state(images: &Images) -> Result<Vec<u8>, String> {
let root = harness_root(text(
images.first().ok_or("缺少 DSH settings")?.1.as_deref(),
images.first().ok_or("DSH settings are missing")?.1.as_deref(),
)?)?;
let mut state = DeepSeekHarnessCatalogState::default();
if let Some(provider) = harness_provider(&root) {
@@ -501,7 +501,7 @@ pub(crate) fn deepseek_harness_template_catalog_state(images: &Images) -> Result
}
}
}
serde_json::to_vec(&state).map_err(|_| "生成 DSH 模型配置状态失败".into())
serde_json::to_vec(&state).map_err(|_| "Failed to generate DSH model configuration state".into())
}
pub(crate) fn build_deepseek_harness_catalog_settings(
@@ -559,7 +559,7 @@ pub(crate) fn prepare_deepseek_harness_configuration(
)?;
after.push((
paths[2].clone(),
Some(serde_json::to_vec(&state).map_err(|_| "生成 DSH 模型配置状态失败")?),
Some(serde_json::to_vec(&state).map_err(|_| "Failed to generate DSH model configuration state")?),
));
Ok((paths, before, after))
}
@@ -607,7 +607,7 @@ fn harness_editor_from_images(
let base_url = format!("{}/v1", managed_core_loopback_origin(port));
let revision = sha256_bytes(
&serde_json::to_vec(&(image_revision(before), &models, &base_url))
.map_err(|_| "生成 DSH 配置版本失败")?,
.map_err(|_| "Failed to generate DSH configuration version")?,
);
Ok((
HarnessEditorSnapshot {
@@ -648,15 +648,15 @@ pub(crate) fn save_harness_editor(
let before = config_images(&paths)?;
let (snapshot, mut state) = harness_editor_from_images(&before, models, port)?;
if request.revision != snapshot.revision {
return Err("DSH_MODEL_CATALOG_CHANGED: 模型列表或配置已变化,请重新加载".into());
return Err("DSH_MODEL_CATALOG_CHANGED: Model list or configuration has changed. Reload it".into());
}
if request.models.len() != models.len() {
return Err("DSH 模型列表不完整,请重新加载".into());
return Err("DSH model list is incomplete. Reload it".into());
}
let mut seen = std::collections::HashSet::new();
for model in request.models {
if !seen.insert(model.id.clone()) || !models.iter().any(|m| m.name == model.id) {
return Err("DSH 模型列表包含重复或不可用的 ID".into());
return Err("DSH model list contains duplicate or unavailable IDs".into());
}
state.models.insert(model.id, model.configuration);
}
@@ -685,7 +685,7 @@ pub(crate) fn save_harness_editor(
.as_ref()
.is_some_and(|id| !models.iter().any(|m| &m.name == id))
{
return Err("当前默认模型已不可用,请先更新默认模型".into());
return Err("The current default model is no longer available. Update the default model first".into());
}
let settings = render_harness_profiles(
text(before[0].1.as_deref())?,
@@ -707,7 +707,7 @@ pub(crate) fn save_harness_editor(
after[0] = prepared[0].clone();
}
state.models.retain(|_, profile| !profile.is_empty());
after[2].1 = Some(serde_json::to_vec(&state).map_err(|_| "生成 DSH 模型配置状态失败")?);
after[2].1 = Some(serde_json::to_vec(&state).map_err(|_| "Failed to generate DSH model configuration state")?);
commit_config(
"deepseek-harness",
&paths,
@@ -726,10 +726,10 @@ pub(crate) async fn get_deepseek_harness_model_catalog_editor(
) -> Result<HarnessEditorSnapshot, String> {
let config = gui_config_state.snapshot()?;
let models = fetch_deepseek_harness_models(&config).await?;
let home = app.path().home_dir().map_err(|_| "无法获取用户目录")?;
let home = app.path().home_dir().map_err(|_| "Failed to get user directory")?;
let _guard = AGENT_CONFIG_FILE_LOCK
.lock()
.map_err(|_| "智能体配置文件锁已损坏")?;
.map_err(|_| "Agent configuration file lock is poisoned")?;
harness_editor_snapshot(&home, &models, config.port)
}
@@ -741,9 +741,9 @@ pub(crate) async fn save_deepseek_harness_model_catalog_editor(
) -> Result<HarnessEditorSnapshot, String> {
let config = gui_config_state.snapshot()?;
let models = fetch_deepseek_harness_models(&config).await?;
let home = app.path().home_dir().map_err(|_| "无法获取用户目录")?;
let home = app.path().home_dir().map_err(|_| "Failed to get user directory")?;
let _guard = AGENT_CONFIG_FILE_LOCK
.lock()
.map_err(|_| "智能体配置文件锁已损坏")?;
.map_err(|_| "Agent configuration file lock is poisoned")?;
save_harness_editor(&home, &models, config.port, request)
}
+96 -96
View File
@@ -159,13 +159,13 @@ pub(crate) fn read_pi_provider_version(home: &Path) -> Result<Option<String>, St
}
let content = fs::read_to_string(&path).map_err(|error| {
format!(
"读取 Pi provider package.json 失败 {}: {error}",
"Failed to read Pi provider package.json {}: {error}",
path_to_string(&path)
)
})?;
let package = serde_json::from_str::<serde_json::Value>(&content).map_err(|error| {
format!(
"解析 Pi provider package.json 失败 {}: {error}",
"Failed to parse Pi provider package.json {}: {error}",
path_to_string(&path)
)
})?;
@@ -186,7 +186,7 @@ pub(crate) fn parse_pi_provider_latest_version(
.map(str::trim)
.filter(|value| !value.is_empty())
.map(str::to_string)
.ok_or_else(|| "npm registry 返回的 Pi provider 版本无效".to_string())
.ok_or_else(|| "Invalid Pi provider version returned by npm registry".to_string())
}
pub(crate) async fn fetch_pi_provider_latest_version(proxy_url: &str) -> Result<String, String> {
@@ -195,7 +195,7 @@ pub(crate) async fn fetch_pi_provider_latest_version(proxy_url: &str) -> Result<
.connect_timeout(Duration::from_secs(5))
.timeout(Duration::from_secs(12)),
proxy_url,
"创建 Pi provider 更新检测客户端失败",
"Failed to create Pi provider update check client",
)?;
let response = client
.get(PI_CLIPROXYAPI_NPM_LATEST_URL)
@@ -203,25 +203,25 @@ pub(crate) async fn fetch_pi_provider_latest_version(proxy_url: &str) -> Result<
.header(reqwest::header::USER_AGENT, APP_USER_AGENT)
.send()
.await
.map_err(|error| format!("查询 Pi provider 最新版本失败: {error}"))?;
.map_err(|error| format!("Failed to query latest Pi provider version: {error}"))?;
let status = response.status();
if !status.is_success() {
return Err(format!(
"查询 Pi provider 最新版本失败: HTTP {}",
"Failed to query latest Pi provider version: HTTP {}",
status.as_u16()
));
}
let payload = response
.json::<serde_json::Value>()
.await
.map_err(|error| format!("解析 Pi provider 最新版本失败: {error}"))?;
.map_err(|error| format!("Failed to parse latest Pi provider version: {error}"))?;
parse_pi_provider_latest_version(&payload)
}
pub(crate) fn pi_provider_update_available(installed: &str, latest: &str) -> Result<bool, String> {
let parse = |value: &str| {
semver::Version::parse(value.trim().trim_start_matches('v'))
.map_err(|error| format!("无法解析 Pi provider 版本号 {value}: {error}"))
.map_err(|error| format!("Failed to parse Pi provider version {value}: {error}"))
};
Ok(parse(latest)? > parse(installed)?)
}
@@ -238,7 +238,7 @@ pub(crate) fn read_pi_settings(home: &Path) -> Result<Option<serde_json::Value>,
}
let content = fs::read_to_string(&path).map_err(|error| {
format!(
"读取 Pi settings.json 失败 {}: {error}",
"Failed to read Pi settings.json {}: {error}",
path_to_string(&path)
)
})?;
@@ -246,7 +246,7 @@ pub(crate) fn read_pi_settings(home: &Path) -> Result<Option<serde_json::Value>,
.map(Some)
.map_err(|error| {
format!(
"解析 Pi settings.json 失败 {}: {error}",
"Failed to parse Pi settings.json {}: {error}",
path_to_string(&path)
)
})
@@ -280,16 +280,16 @@ pub(crate) fn build_pi_provider_config(
) -> Result<String, String> {
let mut root = match existing.map(str::trim).filter(|value| !value.is_empty()) {
Some(value) => serde_json::from_str::<serde_json::Value>(value)
.map_err(|error| format!("解析 Pi CLIProxyAPI 配置失败: {error}"))?,
.map_err(|error| format!("Failed to parse Pi CLIProxyAPI configuration: {error}"))?,
None => serde_json::json!({}),
};
let object = root
.as_object_mut()
.ok_or_else(|| "Pi CLIProxyAPI 配置根节点必须是 JSON 对象".to_string())?;
.ok_or_else(|| "Pi CLIProxyAPI configuration root must be a JSON object".to_string())?;
object.insert("baseUrl".to_string(), serde_json::json!(base_url));
object.insert("apiKey".to_string(), serde_json::json!(api_key));
let mut rendered = serde_json::to_string_pretty(&root)
.map_err(|error| format!("生成 Pi CLIProxyAPI 配置失败: {error}"))?;
.map_err(|error| format!("Failed to generate Pi CLIProxyAPI configuration: {error}"))?;
rendered.push('\n');
Ok(rendered)
}
@@ -300,18 +300,18 @@ pub(crate) fn build_pi_provider_settings(
) -> Result<String, String> {
let default_model = default_model.trim();
if default_model.is_empty() {
return Err("Pi 默认模型不能为空".to_string());
return Err("Pi default model cannot be empty".to_string());
}
let mut root = serde_json::from_str::<serde_json::Value>(existing)
.map_err(|error| format!("解析 Pi settings.json 失败: {error}"))?;
.map_err(|error| format!("Failed to parse Pi settings.json: {error}"))?;
let object = root
.as_object_mut()
.ok_or_else(|| "Pi settings.json 根节点必须是 JSON 对象".to_string())?;
.ok_or_else(|| "Pi settings.json root must be a JSON object".to_string())?;
let packages = object
.entry("packages")
.or_insert_with(|| serde_json::json!([]))
.as_array_mut()
.ok_or_else(|| "Pi settings.json packages 必须是数组".to_string())?;
.ok_or_else(|| "Pi settings.json packages must be an array".to_string())?;
if !packages.iter().any(|package| {
package.as_str().is_some_and(pi_package_source_matches)
|| package
@@ -327,7 +327,7 @@ pub(crate) fn build_pi_provider_settings(
);
object.insert("defaultModel".to_string(), serde_json::json!(default_model));
let mut rendered = serde_json::to_string_pretty(&root)
.map_err(|error| format!("生成 Pi settings.json 失败: {error}"))?;
.map_err(|error| format!("Failed to generate Pi settings.json: {error}"))?;
rendered.push('\n');
Ok(rendered)
}
@@ -365,13 +365,13 @@ pub(crate) fn inspect_pi_provider_status(
let mut credentials_match = false;
if config_path.is_file() {
match fs::read_to_string(&config_path)
.map_err(|error| format!("读取 Pi CLIProxyAPI 配置失败: {error}"))
.map_err(|error| format!("Failed to read Pi CLIProxyAPI configuration: {error}"))
.and_then(|content| {
let root = serde_json::from_str::<serde_json::Value>(&content)
.map_err(|error| format!("解析 Pi CLIProxyAPI 配置失败: {error}"))?;
.map_err(|error| format!("Failed to parse Pi CLIProxyAPI configuration: {error}"))?;
let object = root
.as_object()
.ok_or_else(|| "Pi CLIProxyAPI 配置根节点必须是 JSON 对象".to_string())?;
.ok_or_else(|| "Pi CLIProxyAPI configuration root must be a JSON object".to_string())?;
let expected_base_url = managed_core_loopback_origin(port);
credentials_match = object
.get("baseUrl")
@@ -393,19 +393,19 @@ pub(crate) fn inspect_pi_provider_status(
&& config_valid;
let mut warnings = Vec::new();
if executable.is_some() && !plugin_installed && config_valid {
warnings.push("Pi CLIProxyAPI provider 插件尚未安装".to_string());
warnings.push("Pi CLIProxyAPI provider plugin is not installed".to_string());
} else if plugin_installed
&& (!credentials_match || !default_provider_matches || current_model.is_none())
&& config_valid
&& config_exists
{
warnings.push(
"Pi 插件配置不完整,请使用“应用配置”重新写入凭据、默认 provider 和默认模型".to_string(),
"Pi plugin configuration is incomplete. Use “Apply Configuration” to rewrite the credentials, default provider, and default model".to_string(),
);
}
if executable.is_none() && config_exists {
warnings.push(
"只检测到 Pi 配置文件,仍可编辑配置并接入 CPA;启动功能不可用".to_string(),
"Only a Pi configuration file was detected. You can still edit the configuration and connect it to CPA, but launch functionality is unavailable".to_string(),
);
}
let modification_state = if configured {
@@ -472,10 +472,10 @@ pub(crate) fn install_pi_provider_inner(
proxy_url: &str,
) -> Result<AgentConfigActionResult, String> {
if port == 0 {
return Err("内核端口无效".to_string());
return Err("Invalid kernel port".to_string());
}
if api_key.trim().is_empty() {
return Err("EasyCLIProxyAPI 没有可用的 API key".to_string());
return Err("EasyCLIProxyAPI has no available API key".to_string());
}
let settings_path = pi_provider_settings_path(home);
let mut changed_files = Vec::new();
@@ -499,17 +499,17 @@ pub(crate) fn configure_pi_provider_without_cli_inner(
default_model: &str,
) -> Result<AgentConfigActionResult, String> {
if port == 0 {
return Err("内核端口无效".to_string());
return Err("Invalid kernel port".to_string());
}
if api_key.trim().is_empty() {
return Err("EasyCLIProxyAPI 没有可用的 API key".to_string());
return Err("EasyCLIProxyAPI has no available API key".to_string());
}
let settings_path = pi_provider_settings_path(home);
config_package_operation(home, "plugin-config", || {
let settings = if settings_path.is_file() {
fs::read_to_string(&settings_path).map_err(|error| {
format!(
"读取 Pi settings.json 失败 {}: {error}",
"Failed to read Pi settings.json {}: {error}",
path_to_string(&settings_path)
)
})?
@@ -538,7 +538,7 @@ pub(crate) fn repair_pi_provider_inner(
let settings_path = pi_provider_settings_path(home);
let _guard = AGENT_CONFIG_FILE_LOCK
.lock()
.map_err(|_| "配置文件锁已损坏")?;
.map_err(|_| "Configuration file lock is poisoned")?;
let before = config_images(&config_paths(PI_AGENT_ID, home)?)?;
validate_config_images(&before)?;
if !pi_provider_package_installed(home)? {
@@ -547,7 +547,7 @@ pub(crate) fn repair_pi_provider_inner(
let existing = if config_path.is_file() {
Some(fs::read_to_string(&config_path).map_err(|error| {
format!(
"读取 Pi CLIProxyAPI 配置失败 {}: {error}",
"Failed to read Pi CLIProxyAPI configuration {}: {error}",
path_to_string(&config_path)
)
})?)
@@ -558,7 +558,7 @@ pub(crate) fn repair_pi_provider_inner(
let rendered = build_pi_provider_config(existing.as_deref(), &base_url, api_key)?;
let settings = fs::read_to_string(&settings_path).map_err(|error| {
format!(
"读取 Pi settings.json 失败 {}: {error}",
"Failed to read Pi settings.json {}: {error}",
path_to_string(&settings_path)
)
})?;
@@ -592,7 +592,7 @@ pub(crate) fn update_pi_provider_inner(
proxy_url: &str,
) -> Result<AgentConfigActionResult, String> {
if !pi_provider_package_installed(home)? {
return Err("Pi CLIProxyAPI provider 插件尚未安装".to_string());
return Err("Pi CLIProxyAPI provider plugin is not installed".to_string());
}
config_package_operation(home, "plugin-update", || {
update_pi_package(executable, home, proxy_url)
@@ -640,7 +640,7 @@ pub(crate) fn agent_command_path(
directories.insert(0, executable_directory.to_path_buf());
}
env::join_paths(directories).map_err(|error| format!("构造智能体命令 PATH 失败: {error}"))
env::join_paths(directories).map_err(|error| format!("Failed to construct agent command PATH: {error}"))
}
pub(crate) fn configure_agent_command_environment(
@@ -672,7 +672,7 @@ pub(crate) fn install_pi_package(
configure_agent_command_environment(&mut command, home, executable)?;
let output = command
.output()
.map_err(|error| format!("执行 Pi 插件安装失败: {error}"))?;
.map_err(|error| format!("Failed to run Pi plugin installation: {error}"))?;
if output.status.success() {
return Ok(());
}
@@ -680,7 +680,7 @@ pub(crate) fn install_pi_package(
let stderr = String::from_utf8_lossy(&output.stderr).trim().to_string();
let detail = if !stderr.is_empty() { stderr } else { stdout };
Err(format!(
"Pi CLIProxyAPI provider 插件安装失败{}",
"Pi CLIProxyAPI provider plugin installation failed{}",
if detail.is_empty() {
String::new()
} else {
@@ -710,7 +710,7 @@ pub(crate) fn update_pi_package(
configure_agent_command_environment(&mut command, home, executable)?;
let output = command
.output()
.map_err(|error| format!("执行 Pi 插件更新失败: {error}"))?;
.map_err(|error| format!("Failed to run Pi plugin update: {error}"))?;
if output.status.success() {
return Ok(());
}
@@ -718,7 +718,7 @@ pub(crate) fn update_pi_package(
let stderr = String::from_utf8_lossy(&output.stderr).trim().to_string();
let detail = if !stderr.is_empty() { stderr } else { stdout };
Err(format!(
"Pi CLIProxyAPI provider 插件更新失败{}",
"Pi CLIProxyAPI provider plugin update failed{}",
if detail.is_empty() {
String::new()
} else {
@@ -742,7 +742,7 @@ pub(crate) fn remove_pi_package(executable: &Path, home: &Path) -> Result<(), St
configure_agent_command_environment(&mut command, home, executable)?;
let output = command
.output()
.map_err(|error| format!("鎵ц Pi 鎻掍欢鍗歌浇澶辫触: {error}"))?;
.map_err(|error| format!("Failed to run Pi plugin uninstall: {error}"))?;
if output.status.success() {
return Ok(());
}
@@ -750,7 +750,7 @@ pub(crate) fn remove_pi_package(executable: &Path, home: &Path) -> Result<(), St
let stderr = String::from_utf8_lossy(&output.stderr).trim().to_string();
let detail = if !stderr.is_empty() { stderr } else { stdout };
Err(format!(
"Pi CLIProxyAPI provider 鎻掍欢鍗歌浇澶辫触{}",
"Pi CLIProxyAPI provider plugin uninstall failed{}",
if detail.is_empty() {
String::new()
} else {
@@ -769,9 +769,9 @@ pub(crate) fn current_codex_oauth_configuration(home: &Path) -> Result<bool, Str
return Ok(false);
}
let root: toml::Value = toml::from_str(
&fs::read_to_string(&path).map_err(|error| format!("读取 Codex 配置失败: {error}"))?,
&fs::read_to_string(&path).map_err(|error| format!("Failed to read Codex configuration: {error}"))?,
)
.map_err(|error| format!("解析 Codex 配置失败: {error}"))?;
.map_err(|error| format!("Failed to parse Codex configuration: {error}"))?;
Ok(root
.get("model_providers")
.and_then(toml::Value::as_table)
@@ -979,7 +979,7 @@ pub(crate) fn inspect_agent_config(
if client == AgentClient::Codex && configured {
let model = model
.as_deref()
.ok_or_else(|| "Codex 配置缺少默认模型".to_string())?;
.ok_or_else(|| "Codex configuration is missing a default model".to_string())?;
validate_codex_catalog_file(&paths[0], model)?;
}
Ok((configured, model, oauth_configuration))
@@ -994,7 +994,7 @@ pub(crate) fn inspect_agent_config(
false,
false,
Some(
"配置读取或解析失败,请检查文件权限,或使用手动备份恢复、基础配置模板修复"
"Failed to read or parse configuration. Check file permissions, restore from a manual backup, or repair it with the base configuration template"
.to_string(),
),
),
@@ -1047,10 +1047,10 @@ pub(crate) fn inspect_agent_config(
);
let mut warnings = Vec::new();
if !client.supported_platform() {
warnings.push("当前平台不支持 Claude Desktop 3P 配置".to_string());
warnings.push("The current platform does not support Claude Desktop 3P configuration".to_string());
} else if !installed && config_exists {
warnings.push(
"只检测到配置文件,仍可编辑配置并接入 CPA;启动功能不可用".to_string(),
"Only a configuration file was detected. You can still edit the configuration and connect it to CPA, but launch functionality is unavailable".to_string(),
);
}
if let Some(message) = error.as_ref() {
@@ -1251,9 +1251,9 @@ pub(crate) fn inspect_agent_application(
}
let mut warnings = Vec::new();
if state.backup_files.is_empty() {
warnings.push("检测到旧版应用状态;关闭时将只移除 CPA 管理的配置字段".to_string());
warnings.push("Legacy application state detected; only CPA-managed configuration fields will be removed when disabling".to_string());
} else if !backup_available {
warnings.push("原配置会话备份不完整,暂时无法安全恢复".to_string());
warnings.push("The original configuration session backup is incomplete and cannot be safely restored yet".to_string());
}
AgentModificationInspection {
enabled: true,
@@ -1302,7 +1302,7 @@ pub(crate) fn inspect_agent_managed_config(
.map(|(configured, model)| (configured, model, false)),
AgentClient::ZCode => {
if paths.len() != 2 {
return Err("ZCode 配置路径数量无效".to_string());
return Err("Invalid number of ZCode configuration paths".to_string());
}
let (app_configured, app_model) = inspect_zcode_agent_config(&paths[0], port, api_key)?;
let (cli_configured, cli_model) = inspect_zcode_agent_config(&paths[1], port, api_key)?;
@@ -1415,7 +1415,7 @@ pub(crate) fn agent_has_managed_marker(
if !paths[0].is_file() {
return Ok(false);
}
let root = read_agent_json_or_empty(&paths[0], "Claude Code 配置")?;
let root = read_agent_json_or_empty(&paths[0], "Claude Code configuration")?;
let env = root.get("env");
Ok(env
.and_then(|value| value.get("ANTHROPIC_BASE_URL"))
@@ -1426,8 +1426,8 @@ pub(crate) fn agent_has_managed_marker(
if paths.len() != 4 {
return Ok(false);
}
let meta = read_agent_json_or_empty(&paths[3], "Claude Desktop 配置索引")?;
let profile = read_agent_json_or_empty(&paths[2], "Claude Desktop 网关配置")?;
let meta = read_agent_json_or_empty(&paths[3], "Claude Desktop configuration index")?;
let profile = read_agent_json_or_empty(&paths[2], "Claude Desktop gateway configuration")?;
Ok(meta.get("appliedId").and_then(serde_json::Value::as_str)
== Some(CLAUDE_DESKTOP_PROFILE_ID)
|| meta
@@ -1450,9 +1450,9 @@ pub(crate) fn agent_has_managed_marker(
}
let root: toml::Value = toml::from_str(
&fs::read_to_string(&paths[0])
.map_err(|error| format!("读取 Codex 配置失败: {error}"))?,
.map_err(|error| format!("Failed to read Codex configuration: {error}"))?,
)
.map_err(|error| format!("解析 Codex 配置失败: {error}"))?;
.map_err(|error| format!("Failed to parse Codex configuration: {error}"))?;
Ok(root.get("model_provider").and_then(toml::Value::as_str)
== Some(MANAGED_AGENT_PROVIDER_ID))
}
@@ -1460,7 +1460,7 @@ pub(crate) fn agent_has_managed_marker(
if !paths[0].is_file() {
return Ok(false);
}
let root = read_agent_json5_or_empty(&paths[0], "OpenCode 配置")?;
let root = read_agent_json5_or_empty(&paths[0], "OpenCode configuration")?;
let prefix = format!("{MANAGED_AGENT_PROVIDER_ID}/");
let provider_exists = root
.get("provider")
@@ -1478,9 +1478,9 @@ pub(crate) fn agent_has_managed_marker(
}
let root: serde_json::Value = json5::from_str(
&fs::read_to_string(&paths[0])
.map_err(|error| format!("读取 OpenClaw 配置失败: {error}"))?,
.map_err(|error| format!("Failed to read OpenClaw configuration: {error}"))?,
)
.map_err(|error| format!("解析 OpenClaw 配置失败: {error}"))?;
.map_err(|error| format!("Failed to parse OpenClaw configuration: {error}"))?;
let prefix = format!("{MANAGED_AGENT_PROVIDER_ID}/");
let provider_exists = root
.get("models")
@@ -1502,9 +1502,9 @@ pub(crate) fn agent_has_managed_marker(
}
let root: serde_yaml::Value = serde_yaml::from_str(
&fs::read_to_string(&paths[0])
.map_err(|error| format!("读取 Hermes 配置失败: {error}"))?,
.map_err(|error| format!("Failed to read Hermes configuration: {error}"))?,
)
.map_err(|error| format!("解析 Hermes 配置失败: {error}"))?;
.map_err(|error| format!("Failed to parse Hermes configuration: {error}"))?;
let provider_exists = root
.get("custom_providers")
.and_then(serde_yaml::Value::as_sequence)
@@ -1530,7 +1530,7 @@ pub(crate) fn agent_has_managed_marker(
if !path.is_file() {
continue;
}
let root = read_agent_json_or_empty(path, "ZCode 配置")?;
let root = read_agent_json_or_empty(path, "ZCode configuration")?;
let provider_exists = root
.get("provider")
.and_then(|value| value.get(MANAGED_AGENT_PROVIDER_ID))
@@ -1551,7 +1551,7 @@ pub(crate) fn agent_has_managed_marker(
}
AgentClient::KimiCode => inspect_managed_toml_model_marker(
&paths[0],
"Kimi Code 配置",
"Kimi Code configuration",
Some("providers"),
"models",
None,
@@ -1559,7 +1559,7 @@ pub(crate) fn agent_has_managed_marker(
),
AgentClient::GrokBuild => inspect_managed_toml_model_marker(
&paths[0],
"Grok Build 配置",
"Grok Build configuration",
None,
"model",
Some("models"),
@@ -1609,9 +1609,9 @@ pub(crate) fn inspect_managed_toml_model_marker(
return Ok(false);
}
let root: toml::Value = toml::from_str(
&fs::read_to_string(path).map_err(|error| format!("读取 {label} 失败: {error}"))?,
&fs::read_to_string(path).map_err(|error| format!("Failed to read {label}: {error}"))?,
)
.map_err(|error| format!("解析 {label} 失败: {error}"))?;
.map_err(|error| format!("Failed to parse {label}: {error}"))?;
let prefix = format!("{MANAGED_AGENT_PROVIDER_ID}/");
let selected = (if let Some(section) = default_section {
root.get(section)
@@ -2677,11 +2677,11 @@ pub(crate) fn command_output_with_timeout(
let mut stdout = child
.stdout
.take()
.ok_or_else(|| io::Error::other("无法读取智能体探测标准输出"))?;
.ok_or_else(|| io::Error::other("Unable to read agent detection standard output"))?;
let mut stderr = child
.stderr
.take()
.ok_or_else(|| io::Error::other("无法读取智能体探测错误输出"))?;
.ok_or_else(|| io::Error::other("Unable to read agent detection error output"))?;
thread::scope(|scope| {
let stdout_reader = scope.spawn(move || {
let mut output = Vec::new();
@@ -2704,10 +2704,10 @@ pub(crate) fn command_output_with_timeout(
};
let stdout = stdout_reader
.join()
.map_err(|_| io::Error::other("智能体探测输出线程异常退出"))??;
.map_err(|_| io::Error::other("Agent detection output thread exited unexpectedly"))??;
let stderr = stderr_reader
.join()
.map_err(|_| io::Error::other("智能体探测错误线程异常退出"))??;
.map_err(|_| io::Error::other("Agent detection error thread exited unexpectedly"))??;
Ok(status.map(|status| std::process::Output {
status,
stdout,
@@ -2773,9 +2773,9 @@ pub(crate) fn inspect_claude_agent_config(
return Ok((false, None));
}
let root: serde_json::Value = serde_json::from_str(
&fs::read_to_string(path).map_err(|error| format!("读取 Claude Code 配置失败: {error}"))?,
&fs::read_to_string(path).map_err(|error| format!("Failed to read Claude Code configuration: {error}"))?,
)
.map_err(|error| format!("解析 Claude Code 配置失败: {error}"))?;
.map_err(|error| format!("Failed to parse Claude Code configuration: {error}"))?;
let env = root.get("env").and_then(serde_json::Value::as_object);
let expected_base = managed_core_loopback_origin(port);
let configured = env
@@ -3097,10 +3097,10 @@ pub(crate) fn inspect_claude_desktop_agent_config(
if paths.len() != 4 || !paths.iter().any(|path| path.is_file()) {
return Ok((false, None));
}
let normal = read_agent_json_or_empty(&paths[0], "Claude Desktop 主配置")?;
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 normal = read_agent_json_or_empty(&paths[0], "Claude Desktop main configuration")?;
let threep = read_agent_json_or_empty(&paths[1], "Claude Desktop 3P configuration")?;
let profile = read_agent_json_or_empty(&paths[2], "Claude Desktop gateway configuration")?;
let meta = read_agent_json_or_empty(&paths[3], "Claude Desktop configuration index")?;
let expected_base = managed_core_loopback_origin(port);
let configured = normal
.get("deploymentMode")
@@ -3148,14 +3148,14 @@ pub(crate) fn read_agent_json_or_empty(
return Ok(serde_json::json!({}));
}
let content =
fs::read_to_string(path).map_err(|error| format!("读取 {label} 失败: {error}"))?;
fs::read_to_string(path).map_err(|error| format!("Failed to read {label}: {error}"))?;
if content.trim().is_empty() {
return Ok(serde_json::json!({}));
}
let value: serde_json::Value =
serde_json::from_str(&content).map_err(|error| format!("解析 {label} 失败: {error}"))?;
serde_json::from_str(&content).map_err(|error| format!("Failed to parse {label}: {error}"))?;
if !value.is_object() {
return Err(format!("{label} 根节点必须是对象"));
return Err(format!("{label} root must be an object"));
}
Ok(value)
}
@@ -3168,14 +3168,14 @@ pub(crate) fn read_agent_json5_or_empty(
return Ok(serde_json::json!({}));
}
let content =
fs::read_to_string(path).map_err(|error| format!("读取 {label} 失败: {error}"))?;
fs::read_to_string(path).map_err(|error| format!("Failed to read {label}: {error}"))?;
if content.trim().is_empty() {
return Ok(serde_json::json!({}));
}
let value: serde_json::Value =
json5::from_str(&content).map_err(|error| format!("解析 {label} 失败: {error}"))?;
json5::from_str(&content).map_err(|error| format!("Failed to parse {label}: {error}"))?;
if !value.is_object() {
return Err(format!("{label} 根节点必须是对象"));
return Err(format!("{label} root must be an object"));
}
Ok(value)
}
@@ -3189,9 +3189,9 @@ pub(crate) fn inspect_codex_agent_config(
return Ok((false, None, false));
}
let root: toml::Value = toml::from_str(
&fs::read_to_string(path).map_err(|error| format!("读取 Codex 配置失败: {error}"))?,
&fs::read_to_string(path).map_err(|error| format!("Failed to read Codex configuration: {error}"))?,
)
.map_err(|error| format!("解析 Codex 配置失败: {error}"))?;
.map_err(|error| format!("Failed to parse Codex configuration: {error}"))?;
let expected_base = format!("{}/v1", managed_core_loopback_origin(port));
let provider = root
.get("model_providers")
@@ -3246,7 +3246,7 @@ pub(crate) fn validate_codex_catalog_file(config_path: &Path, model: &str) -> Re
.join(CODEX_MODEL_CATALOG_FILE);
let catalog = fs::read_to_string(&catalog_path).map_err(|error| {
format!(
"读取 Codex 模型目录失败 {}: {error}",
"Failed to read Codex model catalog {}: {error}",
path_to_string(&catalog_path)
)
})?;
@@ -3261,7 +3261,7 @@ pub(crate) fn inspect_opencode_agent_config(
if !path.is_file() {
return Ok((false, None));
}
let root = read_agent_json5_or_empty(path, "OpenCode 配置")?;
let root = read_agent_json5_or_empty(path, "OpenCode configuration")?;
let provider = root
.get("provider")
.and_then(|providers| providers.get(MANAGED_AGENT_PROVIDER_ID));
@@ -3296,9 +3296,9 @@ pub(crate) fn inspect_zcode_agent_config(
return Ok((false, None));
}
let root: serde_json::Value = serde_json::from_str(
&fs::read_to_string(path).map_err(|error| format!("读取 ZCode 配置失败: {error}"))?,
&fs::read_to_string(path).map_err(|error| format!("Failed to read ZCode configuration: {error}"))?,
)
.map_err(|error| format!("解析 ZCode 配置失败: {error}"))?;
.map_err(|error| format!("Failed to parse ZCode configuration: {error}"))?;
let provider = root
.get("provider")
.and_then(|providers| providers.get(MANAGED_AGENT_PROVIDER_ID));
@@ -3361,9 +3361,9 @@ pub(crate) fn inspect_kimi_code_agent_config(
return Ok((false, None));
}
let root: toml::Value = toml::from_str(
&fs::read_to_string(path).map_err(|error| format!("读取 Kimi Code 配置失败: {error}"))?,
&fs::read_to_string(path).map_err(|error| format!("Failed to read Kimi Code configuration: {error}"))?,
)
.map_err(|error| format!("解析 Kimi Code 配置失败: {error}"))?;
.map_err(|error| format!("Failed to parse Kimi Code configuration: {error}"))?;
let expected_base = format!("{}/v1", managed_core_loopback_origin(port));
let provider = root
.get("providers")
@@ -3418,9 +3418,9 @@ pub(crate) fn inspect_grok_build_agent_config(
return Ok((false, None));
}
let root: toml::Value = toml::from_str(
&fs::read_to_string(path).map_err(|error| format!("读取 Grok Build 配置失败: {error}"))?,
&fs::read_to_string(path).map_err(|error| format!("Failed to read Grok Build configuration: {error}"))?,
)
.map_err(|error| format!("解析 Grok Build 配置失败: {error}"))?;
.map_err(|error| format!("Failed to parse Grok Build configuration: {error}"))?;
let expected_base = format!("{}/v1", managed_core_loopback_origin(port));
let prefix = format!("{MANAGED_AGENT_PROVIDER_ID}/");
let selected = root
@@ -3468,9 +3468,9 @@ pub(crate) fn inspect_openclaw_agent_config(
return Ok((false, None));
}
let content =
fs::read_to_string(path).map_err(|error| format!("读取 OpenClaw 配置失败: {error}"))?;
fs::read_to_string(path).map_err(|error| format!("Failed to read OpenClaw configuration: {error}"))?;
let root: serde_json::Value = json5::from_str(&content)
.map_err(|error| format!("解析 OpenClaw JSON5 配置失败: {error}"))?;
.map_err(|error| format!("Failed to parse OpenClaw JSON5 configuration: {error}"))?;
let provider = root
.get("models")
.and_then(|models| models.get("providers"))
@@ -3507,9 +3507,9 @@ pub(crate) fn inspect_hermes_agent_config(
return Ok((false, None));
}
let root: serde_yaml::Value = serde_yaml::from_str(
&fs::read_to_string(path).map_err(|error| format!("读取 Hermes 配置失败: {error}"))?,
&fs::read_to_string(path).map_err(|error| format!("Failed to read Hermes configuration: {error}"))?,
)
.map_err(|error| format!("解析 Hermes YAML 配置失败: {error}"))?;
.map_err(|error| format!("Failed to parse Hermes YAML configuration: {error}"))?;
let provider = root
.get("custom_providers")
.and_then(serde_yaml::Value::as_sequence)
@@ -3550,7 +3550,7 @@ pub(crate) fn inspect_deepseek_harness_config(
api_key: &str,
) -> Result<(bool, Option<String>), String> {
if paths.len() != 2 {
return Err("DeepSeek Harness 配置路径数量无效".to_string());
return Err("Invalid number of DeepSeek Harness configuration paths".to_string());
}
let settings = read_agent_yaml_mapping_or_empty(&paths[0], "DeepSeek Harness settings")?;
let credentials = read_agent_yaml_mapping_or_empty(&paths[1], "DeepSeek Harness credentials")?;
@@ -3644,7 +3644,7 @@ fn deepseek_harness_credentials_layout_supported(credentials: &serde_norway::Map
pub(crate) fn deepseek_harness_has_managed_marker(paths: &[PathBuf]) -> Result<bool, String> {
if paths.len() != 2 {
return Err("DeepSeek Harness 配置路径数量无效".to_string());
return Err("Invalid number of DeepSeek Harness configuration paths".to_string());
}
let settings = read_agent_yaml_mapping_or_empty(&paths[0], "DeepSeek Harness settings")?;
let credentials = read_agent_yaml_mapping_or_empty(&paths[1], "DeepSeek Harness credentials")?;
+90 -90
View File
@@ -131,7 +131,7 @@ pub(crate) fn launch_agent(
let home = app
.path()
.home_dir()
.map_err(|error| format!("无法获取用户目录: {error}"))?;
.map_err(|error| format!("Failed to get user directory: {error}"))?;
let config = gui_config_state.snapshot()?;
let terminal = normalize_agent_terminal(&config.default_terminal);
let requested_target = target
@@ -140,20 +140,20 @@ pub(crate) fn launch_agent(
.filter(|value| !value.is_empty());
if deepseek_harness_options.is_some() && !client.trim().eq_ignore_ascii_case("deepseek-harness")
{
return Err("DeepSeek Harness 启动选项不能用于其他客户端".to_string());
return Err("DeepSeek Harness launch options cannot be used with other clients".to_string());
}
if client.trim().eq_ignore_ascii_case(PI_AGENT_ID) {
if requested_target.is_some_and(|value| value != "cli") {
return Err("Pi 只支持 CLI 启动方式".to_string());
return Err("Pi supports only CLI launch mode".to_string());
}
let status =
inspect_pi_provider_status(&home, config.port, effective_agent_api_key(&config));
if !status.installed {
return Err("未检测到 Pi CLI,请先安装 Pi 并重新检测".to_string());
return Err("Pi CLI was not detected. Install Pi and detect it again".to_string());
}
let executable =
find_pi_executable(&home).ok_or_else(|| "未找到 Pi CLI 可执行文件".to_string())?;
find_pi_executable(&home).ok_or_else(|| "Pi CLI executable not found".to_string())?;
let launch_directory = resolve_launch_directory(working_directory.as_deref(), &home)?;
return launch_cli_agent(
&executable,
@@ -167,11 +167,11 @@ pub(crate) fn launch_agent(
let client = AgentClient::parse(&client)?;
if !client.supported_platform() {
return Err(format!("当前平台不支持启动 {}", client.name()));
return Err(format!("The current platform does not support launching {}", client.name()));
}
let status = inspect_agent_config(client, &home, config.port, effective_agent_api_key(&config));
if !status.installed {
return Err(format!("未检测到 {},请先安装并重新检测", client.name()));
return Err(format!("{} was not detected. Install it and detect it again", client.name()));
}
let default_target = status
.launch_targets
@@ -184,30 +184,30 @@ pub(crate) fn launch_agent(
(AgentClient::AntigravityCli, "cli")
if antigravity_has_marker(client, &agent_config_paths(client, &home))? => {
if !status.configured {
return Err("Antigravity CLI 配置与 CPA 不一致,请重新应用配置".into());
return Err("Antigravity CLI configuration does not match CPA. Apply the configuration again".into());
}
let executable = env::current_exe().map_err(|_| "无法定位 CPA 启动适配程序")?;
let executable = env::current_exe().map_err(|_| "Unable to locate CPA launch adapter")?;
let directory = resolve_launch_directory(working_directory.as_deref(), &home)?;
launch_cli_agent(&executable, client.name(), &directory, &antigravity_cli_helper_arguments(&home), &[], &terminal)
}
(AgentClient::WorkBuddy, "app") => {
let executable = find_workbuddy_desktop_executable(&home)
.ok_or_else(|| "未找到 WorkBuddy 应用程序".to_string())?;
.ok_or_else(|| "WorkBuddy application not found".to_string())?;
launch_desktop_agent(&executable, client.name())
}
(AgentClient::ZCode, "app") => {
let executable = find_zcode_desktop_executable(&home)
.ok_or_else(|| "未找到 ZCode 应用程序".to_string())?;
.ok_or_else(|| "ZCode application not found".to_string())?;
launch_desktop_agent(&executable, client.name())
}
(AgentClient::Codex, "app") => launch_codex_desktop(&home),
(AgentClient::OpenCode, "app") => launch_opencode_desktop(&home),
(AgentClient::ClaudeDesktop | AgentClient::ZCode | AgentClient::WorkBuddy, "cli") => {
Err(format!("{} 不支持 CLI 启动方式", client.name()))
Err(format!("{} does not support CLI launch mode", client.name()))
}
(_, "cli") => {
let executable = find_agent_executable(client, &home)
.ok_or_else(|| format!("未找到 {} 的可执行文件", client.name()))?;
.ok_or_else(|| format!("Executable for {} not found", client.name()))?;
let launch_directory = resolve_launch_directory(working_directory.as_deref(), &home)?;
if client == AgentClient::DeepSeekHarness {
let mode = deepseek_harness_launch_mode(deepseek_harness_options.as_ref())?;
@@ -237,8 +237,8 @@ pub(crate) fn launch_agent(
&terminal,
)
}
(_, "app") => Err(format!("{} 不支持桌面 App 启动方式", client.name())),
_ => Err("不支持的智能体启动方式".to_string()),
(_, "app") => Err(format!("{} does not support desktop App launch mode", client.name())),
_ => Err("Unsupported agent launch mode".to_string()),
}
}
@@ -255,7 +255,7 @@ fn deepseek_harness_launch_mode(
) {
Ok(mode)
} else {
Err(format!("不支持的 DeepSeek Harness 启动模式: {mode}"))
Err(format!("Unsupported DeepSeek Harness launch mode: {mode}"))
}
}
@@ -265,7 +265,7 @@ fn agent_cli_launch_arguments(
) -> Result<Vec<String>, String> {
if client != AgentClient::DeepSeekHarness {
if deepseek_harness_options.is_some() {
return Err("DeepSeek Harness 启动选项不能用于其他客户端".to_string());
return Err("DeepSeek Harness launch options cannot be used with other clients".to_string());
}
return Ok(Vec::new());
}
@@ -284,7 +284,7 @@ fn build_deepseek_harness_launch_arguments(
.and_then(|options| options.profile.as_deref())
.unwrap_or_default(),
)?,
_ => return Err(format!("不支持的 DeepSeek Harness 启动模式: {mode}")),
_ => return Err(format!("Unsupported DeepSeek Harness launch mode: {mode}")),
};
let mut arguments = if profile == "web" {
vec!["web".to_string()]
@@ -295,7 +295,7 @@ fn build_deepseek_harness_launch_arguments(
if let Some(options) = options {
for patch in &options.patches {
arguments.push("--patch".to_string());
arguments.push(validate_deepseek_harness_argument(patch, "patch 路径")?);
arguments.push(validate_deepseek_harness_argument(patch, "Patch path")?);
}
}
@@ -330,7 +330,7 @@ fn build_deepseek_harness_launch_arguments(
let task = options
.and_then(|options| options.task.as_deref())
.unwrap_or_default();
arguments.push(validate_deepseek_harness_argument(task, "Headless 任务")?);
arguments.push(validate_deepseek_harness_argument(task, "Headless task")?);
}
"acp" | "sdk" | "sdk-minimal" | "custom" => {}
_ => unreachable!(),
@@ -343,7 +343,7 @@ impl DeepSeekHarnessProcessState {
let mut process = self
.process
.lock()
.map_err(|_| "DeepSeek Harness 进程状态锁已损坏".to_string())?;
.map_err(|_| "DeepSeek Harness process status lock is poisoned".to_string())?;
let Some(managed) = process.as_mut() else {
return Ok(DeepSeekHarnessProcessStatus::default());
};
@@ -357,7 +357,7 @@ impl DeepSeekHarnessProcessState {
*process = None;
Ok(DeepSeekHarnessProcessStatus::default())
}
Err(error) => Err(format!("检查 DeepSeek Harness 进程状态失败: {error}")),
Err(error) => Err(format!("Failed to check DeepSeek Harness process status: {error}")),
}
}
}
@@ -388,17 +388,17 @@ fn launch_managed_deepseek_harness(
let mut process = process_state
.process
.lock()
.map_err(|_| "DeepSeek Harness 进程状态锁已损坏".to_string())?;
.map_err(|_| "DeepSeek Harness process status lock is poisoned".to_string())?;
if let Some(managed) = process.as_mut() {
match managed.child.try_wait() {
Ok(None) => {
return Err(format!(
"DeepSeek Harness 已在运行(PID {}),请先关闭当前进程",
"DeepSeek Harness is already running (PID {}). Stop the current process first",
managed.child.id()
));
}
Ok(Some(_)) => *process = None,
Err(error) => return Err(format!("检查 DeepSeek Harness 进程状态失败: {error}")),
Err(error) => return Err(format!("Failed to check DeepSeek Harness process status: {error}")),
}
}
@@ -428,7 +428,7 @@ pub(crate) async fn restart_deepseek_harness_process(
restart_managed_deepseek_harness(app.state::<DeepSeekHarnessProcessState>().inner())
})
.await
.map_err(|error| format!("重启 DeepSeek Harness Web 任务失败: {error}"))?
.map_err(|error| format!("DeepSeek Harness Web restart task failed: {error}"))?
}
fn restart_managed_deepseek_harness(
@@ -449,25 +449,25 @@ fn restart_managed_deepseek_harness_with(
let mut process = process_state
.process
.lock()
.map_err(|_| "DeepSeek Harness 进程状态锁已损坏".to_string())?;
.map_err(|_| "DeepSeek Harness process status lock is poisoned".to_string())?;
let managed = process
.as_mut()
.ok_or_else(|| "没有正在运行的 DeepSeek Harness Web 服务".to_string())?;
.ok_or_else(|| "No DeepSeek Harness Web service is running".to_string())?;
if managed
.child
.try_wait()
.map_err(|error| format!("检查 DeepSeek Harness 进程状态失败: {error}"))?
.map_err(|error| format!("Failed to check DeepSeek Harness process status: {error}"))?
.is_some()
{
*process = None;
return Err("DeepSeek Harness Web 已退出,请重新启动".to_string());
return Err("DeepSeek Harness Web has exited. Start it again".to_string());
}
if managed.mode != "web" {
return Err("只有 Web 模式支持重启".to_string());
return Err("Only Web mode supports restart".to_string());
}
let launch = managed.launch.clone();
if !launch.executable.is_file() || !launch.working_directory.is_dir() {
return Err("DeepSeek Harness 启动程序或工作目录已不存在".to_string());
return Err("DeepSeek Harness launcher or working directory no longer exists".to_string());
}
stop(&mut managed.child)?;
*process = None;
@@ -507,10 +507,10 @@ fn ensure_deepseek_harness_web_endpoint_available(
let listener = TcpListener::bind((host, port)).map_err(|error| {
if error.kind() == io::ErrorKind::AddrInUse {
format!(
"DeepSeek Harness Web 地址 {host}:{port} 已被占用,请关闭已有服务或选择其他端口"
"DeepSeek Harness Web address {host}:{port} is in use. Stop the existing service or choose another port"
)
} else {
format!("无法使用 DeepSeek Harness Web 地址 {host}:{port}: {error}")
format!("Cannot use DeepSeek Harness Web address {host}:{port}: {error}")
}
})?;
drop(listener);
@@ -541,7 +541,7 @@ fn spawn_managed_deepseek_harness(
);
return command
.spawn()
.map_err(|error| format!("启动 DeepSeek Harness 失败: {error}"));
.map_err(|error| format!("Failed to start DeepSeek Harness: {error}"));
}
#[cfg(unix)]
@@ -555,13 +555,13 @@ fn spawn_managed_deepseek_harness(
.process_group(0);
return command
.spawn()
.map_err(|error| format!("启动 DeepSeek Harness 失败: {error}"));
.map_err(|error| format!("Failed to start DeepSeek Harness: {error}"));
}
#[cfg(not(any(windows, unix)))]
{
let _ = (executable, working_directory, arguments);
Err("当前平台不支持受控启动 DeepSeek Harness".to_string())
Err("The current platform does not support managed DeepSeek Harness startup".to_string())
}
}
@@ -571,14 +571,14 @@ pub(crate) fn stop_managed_deepseek_harness(
let mut process = process_state
.process
.lock()
.map_err(|_| "DeepSeek Harness 进程状态锁已损坏".to_string())?;
.map_err(|_| "DeepSeek Harness process status lock is poisoned".to_string())?;
let Some(managed) = process.as_mut() else {
return Ok(());
};
if managed
.child
.try_wait()
.map_err(|error| format!("检查 DeepSeek Harness 进程状态失败: {error}"))?
.map_err(|error| format!("Failed to check DeepSeek Harness process status: {error}"))?
.is_some()
{
*process = None;
@@ -602,10 +602,10 @@ fn terminate_deepseek_harness_process_tree(child: &mut Child) -> Result<(), Stri
configure_background_command(&mut command);
let status = command
.status()
.map_err(|error| format!("关闭 DeepSeek Harness 进程树失败: {error}"))?;
.map_err(|error| format!("Failed to stop DeepSeek Harness process tree: {error}"))?;
if !status.success() {
return Err(format!(
"关闭 DeepSeek Harness 进程树失败: PID {process_id}"
"Failed to stop DeepSeek Harness process tree: PID {process_id}"
));
}
let _ = child.wait();
@@ -618,10 +618,10 @@ fn terminate_deepseek_harness_process_tree(child: &mut Child) -> Result<(), Stri
let term_status = Command::new("kill")
.args(["-TERM", &process_group])
.status()
.map_err(|error| format!("关闭 DeepSeek Harness 进程组失败: {error}"))?;
.map_err(|error| format!("Failed to stop DeepSeek Harness process group: {error}"))?;
if !term_status.success() {
return Err(format!(
"关闭 DeepSeek Harness 进程组失败: PID {}",
"Failed to stop DeepSeek Harness process group: PID {}",
child.id()
));
}
@@ -630,23 +630,23 @@ fn terminate_deepseek_harness_process_tree(child: &mut Child) -> Result<(), Stri
Ok(Some(_)) => return Ok(()),
Ok(None) => thread::sleep(Duration::from_millis(100)),
Err(error) => {
return Err(format!("检查 DeepSeek Harness 进程状态失败: {error}"));
return Err(format!("Failed to check DeepSeek Harness process status: {error}"));
}
}
}
let kill_status = Command::new("kill")
.args(["-KILL", &process_group])
.status()
.map_err(|error| format!("强制关闭 DeepSeek Harness 进程组失败: {error}"))?;
.map_err(|error| format!("Failed to force-stop DeepSeek Harness process group: {error}"))?;
if !kill_status.success() {
return Err(format!(
"强制关闭 DeepSeek Harness 进程组失败: PID {}",
"Failed to force-stop DeepSeek Harness process group: PID {}",
child.id()
));
}
child
.wait()
.map_err(|error| format!("等待 DeepSeek Harness 进程退出失败: {error}"))?;
.map_err(|error| format!("Failed while waiting for DeepSeek Harness process to exit: {error}"))?;
return Ok(());
}
@@ -654,10 +654,10 @@ fn terminate_deepseek_harness_process_tree(child: &mut Child) -> Result<(), Stri
{
child
.kill()
.map_err(|error| format!("关闭 DeepSeek Harness 进程失败: {error}"))?;
.map_err(|error| format!("Failed to stop DeepSeek Harness process: {error}"))?;
child
.wait()
.map_err(|error| format!("等待 DeepSeek Harness 进程退出失败: {error}"))?;
.map_err(|error| format!("Failed while waiting for DeepSeek Harness process to exit: {error}"))?;
Ok(())
}
}
@@ -665,7 +665,7 @@ fn terminate_deepseek_harness_process_tree(child: &mut Child) -> Result<(), Stri
fn validate_deepseek_harness_profile(profile: &str) -> Result<&str, String> {
let profile = profile.trim();
if profile.is_empty() {
return Err("请输入 DeepSeek Harness profile 名称".to_string());
return Err("Enter a DeepSeek Harness profile name".to_string());
}
if matches!(profile, "." | "..")
|| !profile.chars().all(|character| {
@@ -673,7 +673,7 @@ fn validate_deepseek_harness_profile(profile: &str) -> Result<&str, String> {
})
{
return Err(
"DeepSeek Harness profile 名称只能包含字母、数字、点、连字符和下划线".to_string(),
"DeepSeek Harness profile name may contain only letters, numbers, periods, hyphens, and underscores".to_string(),
);
}
Ok(profile)
@@ -682,10 +682,10 @@ fn validate_deepseek_harness_profile(profile: &str) -> Result<&str, String> {
fn validate_deepseek_harness_argument(value: &str, label: &str) -> Result<String, String> {
let value = value.trim();
if value.is_empty() {
return Err(format!("{label}不能为空"));
return Err(format!("{label} cannot be empty"));
}
if value.chars().any(char::is_control) {
return Err(format!("{label}不能包含控制字符"));
return Err(format!("{label} cannot contain control characters"));
}
Ok(value.to_string())
}
@@ -711,19 +711,19 @@ pub(crate) async fn restart_agent_app(app: tauri::AppHandle, client: String) ->
| AgentClient::ZCode
| AgentClient::WorkBuddy
) {
return Err(format!("{} 不支持桌面应用重启", client.name()));
return Err(format!("{} does not support restarting the desktop application", client.name()));
}
let home = app
.path()
.home_dir()
.map_err(|error| format!("无法获取用户目录: {error}"))?;
.map_err(|error| format!("Failed to get user directory: {error}"))?;
tauri::async_runtime::spawn_blocking(move || {
let target = find_desktop_restart_target(client, &home)?;
match client {
AgentClient::Codex => stop_codex_desktop(&target)?,
AgentClient::OpenCode => {
let DesktopAppTarget::Application(path) = &target else {
return Err("OpenCode 桌面安装类型无效".to_string());
return Err("Invalid OpenCode desktop installation type".to_string());
};
stop_opencode_desktop(path)?;
}
@@ -738,7 +738,7 @@ pub(crate) async fn restart_agent_app(app: tauri::AppHandle, client: String) ->
}
})
.await
.map_err(|error| format!("重启桌面应用任务失败: {error}"))?
.map_err(|error| format!("Desktop application restart task failed: {error}"))?
}
fn find_desktop_restart_target(
@@ -771,7 +771,7 @@ fn find_desktop_restart_target(
}
_ => None,
};
target.ok_or_else(|| format!("未检测到 {} 桌面应用,请重新检测", client.name()))
target.ok_or_else(|| format!("{} desktop application was not detected. Detect it again", client.name()))
}
#[cfg(target_os = "windows")]
@@ -794,7 +794,7 @@ fn stop_other_desktop(target: &DesktopAppTarget, label: &str) -> Result<(), Stri
#[cfg(not(any(target_os = "windows", target_os = "macos", target_os = "linux")))]
fn stop_other_desktop(_target: &DesktopAppTarget, label: &str) -> Result<(), String> {
Err(format!("当前平台不支持重启 {label}"))
Err(format!("The current platform does not support restarting {label}"))
}
fn resolve_launch_directory(value: Option<&str>, fallback: &Path) -> Result<PathBuf, String> {
@@ -802,21 +802,21 @@ fn resolve_launch_directory(value: Option<&str>, fallback: &Path) -> Result<Path
return Ok(fallback.to_path_buf());
};
if value.chars().any(char::is_control) {
return Err("工作目录包含无效字符".to_string());
return Err("Working directory contains invalid characters".to_string());
}
let path = PathBuf::from(value);
if !path.is_absolute() {
return Err("工作目录必须是绝对路径".to_string());
return Err("Working directory must be an absolute path".to_string());
}
if !path.is_dir() {
return Err(format!("工作目录不存在: {}", path_to_string(&path)));
return Err(format!("Working directory does not exist: {}", path_to_string(&path)));
}
Ok(path)
}
fn launch_codex_desktop(home: &Path) -> Result<(), String> {
let target = find_codex_app_installation(home)
.ok_or_else(|| "未检测到 Codex 桌面应用,请重新检测或改用 Codex CLI".to_string())?;
.ok_or_else(|| "Codex desktop application was not detected. Detect it again or use Codex CLI".to_string())?;
launch_codex_target(&target)
}
@@ -886,9 +886,9 @@ fn macos_processes_for_application(application: &Path) -> Result<Vec<i32>, Strin
let output = Command::new("ps")
.args(["-axww", "-o", "pid=", "-o", "comm="])
.output()
.map_err(|error| format!("读取桌面应用进程失败: {error}"))?;
.map_err(|error| format!("Failed to read desktop application processes: {error}"))?;
if !output.status.success() {
return Err("读取桌面应用进程失败".to_string());
return Err("Failed to read desktop application processes".to_string());
}
Ok(desktop_process_ids_from_ps(
&String::from_utf8_lossy(&output.stdout),
@@ -921,7 +921,7 @@ fn stop_macos_desktop_application(application: &Path, label: &str) -> Result<(),
if unsafe { libc::kill(pid, libc::SIGKILL) } != 0 {
let error = io::Error::last_os_error();
if error.raw_os_error() != Some(libc::ESRCH) {
return Err(format!("关闭 {label} 进程 {pid} 失败: {error}"));
return Err(format!("Failed to stop {label} process {pid}: {error}"));
}
}
}
@@ -931,7 +931,7 @@ fn stop_macos_desktop_application(application: &Path, label: &str) -> Result<(),
}
thread::sleep(Duration::from_millis(100));
}
Err(format!("{label} 未能完全关闭"))
Err(format!("{label} did not close completely"))
}
#[cfg(target_os = "linux")]
@@ -977,7 +977,7 @@ fn stop_linux_desktop_application(application: &Path, label: &str) -> Result<(),
Err(error)
} else {
Err(format!(
"{label} 未能完全关闭;剩余进程 ID: {}",
"{label} did not close completely; remaining process IDs: {}",
remaining
.iter()
.map(i32::to_string)
@@ -1080,18 +1080,18 @@ fn signal_linux_process(process_id: i32, signal: i32) -> Result<(), String> {
if error.raw_os_error() == Some(libc::ESRCH) {
Ok(())
} else {
Err(format!("关闭桌面应用进程 {process_id} 失败: {error}"))
Err(format!("Failed to stop desktop application process {process_id}: {error}"))
}
}
#[cfg(not(any(target_os = "windows", target_os = "macos")))]
fn stop_codex_desktop(_target: &DesktopAppTarget) -> Result<(), String> {
Err("当前平台不支持重启 Codex App".to_string())
Err("The current platform does not support restarting Codex App".to_string())
}
#[cfg(not(any(target_os = "windows", target_os = "macos", target_os = "linux")))]
fn stop_opencode_desktop(_application: &Path) -> Result<(), String> {
Err("当前平台不支持重启 OpenCode Desktop".to_string())
Err("The current platform does not support restarting OpenCode Desktop".to_string())
}
fn launch_claude_desktop(home: &Path) -> Result<(), String> {
@@ -1103,12 +1103,12 @@ fn launch_claude_desktop(home: &Path) -> Result<(), String> {
launch_windows_claude_store_app()
}
#[cfg(not(target_os = "windows"))]
Err("未检测到 Claude Desktop 应用,请先安装或重新检测".to_string())
Err("Claude Desktop application was not detected. Install or detect it again".to_string())
}
fn launch_opencode_desktop(home: &Path) -> Result<(), String> {
let application = find_opencode_desktop_application(home)
.ok_or_else(|| "未检测到 OpenCode Desktop 应用,请先安装或重新检测".to_string())?;
.ok_or_else(|| "OpenCode Desktop application was not detected. Install or detect it again".to_string())?;
launch_desktop_agent(&application, "OpenCode Desktop")
}
@@ -1130,7 +1130,7 @@ fn launch_desktop_agent(executable: &Path, label: &str) -> Result<(), String> {
#[cfg(not(any(target_os = "macos", target_os = "windows", target_os = "linux")))]
{
let _ = (executable, label);
return Err("当前平台不支持桌面智能体".to_string());
return Err("The current platform does not support desktop agents".to_string());
}
#[cfg(any(target_os = "macos", target_os = "windows", target_os = "linux"))]
@@ -1143,7 +1143,7 @@ fn launch_desktop_agent(executable: &Path, label: &str) -> Result<(), String> {
command
.spawn()
.map(|_| ())
.map_err(|error| format!("启动 {label} 失败: {error}"))
.map_err(|error| format!("Failed to start {label}: {error}"))
}
}
@@ -1159,13 +1159,13 @@ fn launch_windows_store_app(app_id: &str, label: &str) -> Result<(), String> {
command
.spawn()
.map(|_| ())
.map_err(|error| format!("启动 {label} 失败: {error}"))
.map_err(|error| format!("Failed to start {label}: {error}"))
}
#[cfg(target_os = "windows")]
fn launch_windows_claude_store_app() -> Result<(), String> {
let app_id = find_windows_claude_app_id()
.ok_or_else(|| "未找到可启动的 Claude Desktop 应用".to_string())?;
.ok_or_else(|| "No launchable Claude Desktop application found".to_string())?;
launch_windows_store_app(&app_id, "Claude Desktop")
}
@@ -1218,18 +1218,18 @@ fn launch_cli_agent(
command_line.replace('\\', "\\\\").replace('"', "\\\"")
)
} else {
return Err(format!("启动 {label} 失败:不支持所选终端"));
return Err(format!("Failed to start {label}: selected terminal is unsupported"));
};
let output = Command::new("osascript")
.arg("-e")
.arg(script)
.output()
.map_err(|error| format!("启动 {label} 终端失败: {error}"))?;
.map_err(|error| format!("Failed to start {label} terminal: {error}"))?;
if output.status.success() {
Ok(())
} else {
let detail = String::from_utf8_lossy(&output.stderr).trim().to_string();
Err(format!("启动 {label} 终端失败: {detail}"))
Err(format!("Failed to start {label} terminal: {detail}"))
}
}
@@ -1270,11 +1270,11 @@ fn launch_cli_agent(
}
}
if terminal != "auto" && last_error.is_none() {
return Err(format!("启动 {label} 失败:未找到所选终端"));
return Err(format!("Failed to start {label}: selected terminal not found"));
}
Err(match last_error {
Some(error) => format!("启动 {label} 失败: {error}"),
None => format!("启动 {label} 失败:未找到可用的终端程序"),
Some(error) => format!("Failed to start {label}: {error}"),
None => format!("Failed to start {label}: no available terminal program found"),
})
}
@@ -1365,7 +1365,7 @@ fn launch_cli_agent(
}
"windows-terminal" => {
let terminal_executable = program_on_path("wt.exe")
.ok_or_else(|| format!("启动 {label} 失败:未找到 Windows Terminal"))?;
.ok_or_else(|| format!("Failed to start {label}: Windows Terminal not found"))?;
let directory = path_to_string(working_directory);
let mut command = Command::new(terminal_executable);
command.args(["-d", &directory, "--"]);
@@ -1403,7 +1403,7 @@ fn launch_cli_agent(
command.creation_flags(CREATE_NEW_CONSOLE);
command
}
_ => return Err(format!("启动 {label} 失败:不支持所选终端")),
_ => return Err(format!("Failed to start {label}: selected terminal is unsupported")),
};
for key in environment_to_remove {
command.env_remove(key);
@@ -1411,7 +1411,7 @@ fn launch_cli_agent(
command
.spawn()
.map(|_| ())
.map_err(|error| format!("启动 {label} 失败: {error}"))
.map_err(|error| format!("Failed to start {label}: {error}"))
}
#[cfg(not(any(target_os = "macos", target_os = "linux", target_os = "windows")))]
@@ -1423,7 +1423,7 @@ fn launch_cli_agent(
_environment_to_remove: &[&str],
_terminal: &str,
) -> Result<(), String> {
Err(format!("当前平台不支持启动 {label}"))
Err(format!("The current platform does not support launching {label}"))
}
#[cfg(test)]
@@ -1464,7 +1464,7 @@ mod tests {
.unwrap()
.as_nanos(),
))
.join("用户's [workspace] $value");
.join("User's [workspace] $value");
fs::create_dir_all(&directory).unwrap();
let executable = directory.join("GUI helper.exe");
let compiled = directory.parent().unwrap().join("helper.exe");
@@ -1811,7 +1811,7 @@ class GuiHelper {
fn relative_launch_directory_is_rejected() {
let error = resolve_launch_directory(Some("relative/project"), Path::new("/fallback"))
.expect_err("relative path should be rejected");
assert!(error.contains("绝对路径"));
assert!(error.contains("absolute path"));
}
#[test]
@@ -1966,7 +1966,7 @@ class GuiHelper {
let error = ensure_deepseek_harness_web_endpoint_available(Some(&options)).unwrap_err();
assert!(error.contains("已被占用"), "{error}");
assert!(error.contains("is in use"), "{error}");
}
#[test]
+20 -20
View File
@@ -8,17 +8,17 @@ pub(crate) fn write_codex_private_file(path: &Path, content: &[u8]) -> Result<()
#[cfg(unix)]
{
use std::os::unix::fs::{OpenOptionsExt, PermissionsExt};
fs::create_dir_all(path.parent().ok_or("Codex 凭据路径无效")?)
.map_err(|_| "创建 Codex 凭据目录失败")?;
fs::create_dir_all(path.parent().ok_or("Invalid Codex credentials path")?)
.map_err(|_| "Failed to create Codex credentials directory")?;
let file = fs::OpenOptions::new()
.create(true)
.write(true)
.truncate(false)
.mode(0o600)
.open(path)
.map_err(|_| "无法打开 Codex 凭据文件")?;
.map_err(|_| "Unable to open Codex credentials file")?;
file.set_permissions(fs::Permissions::from_mode(0o600))
.map_err(|_| "设置 Codex 凭据文件权限失败")?;
.map_err(|_| "Failed to set Codex credentials file permissions")?;
}
write_bytes_directly(path, content)
}
@@ -57,9 +57,9 @@ fn parse_state(bytes: Option<&[u8]>) -> Result<NativeOAuthState, String> {
});
};
let state: NativeOAuthState = serde_json::from_slice(bytes)
.map_err(|_| "无法读取 Codex 接入方式切换记录,请检查配置文件".to_string())?;
.map_err(|_| "Unable to read Codex access mode switch record. Check the configuration file".to_string())?;
if state.version != 1 {
return Err("不支持的 Codex 接入方式切换记录版本".into());
return Err("Unsupported Codex access mode switch record version".into());
}
Ok(state)
}
@@ -90,7 +90,7 @@ pub(crate) fn codex_native_oauth_enabled(home: &Path) -> Result<bool, String> {
pub(crate) fn ensure_codex_cpa_mode(home: &Path) -> Result<(), String> {
if codex_native_oauth_enabled(home)? {
return Err("请点击“更新配置”或“一键接入”恢复 CPA 配置".into());
return Err("Click “Update Configuration” or “One-Click Setup” to restore the CPA configuration".into());
}
Ok(())
}
@@ -278,7 +278,7 @@ pub(crate) fn close_codex_configuration(home: &Path) -> Result<AgentConfigAction
let after = vec![
(
paths[0].clone(),
Some(serde_json::to_vec_pretty(&state).map_err(|_| "保存 Codex 切换记录失败")?),
Some(serde_json::to_vec_pretty(&state).map_err(|_| "Failed to save Codex switch record")?),
),
(paths[1].clone(), next_auth.map(String::into_bytes)),
(
@@ -299,10 +299,10 @@ pub(crate) fn close_codex_configuration(home: &Path) -> Result<AgentConfigAction
pub(crate) fn close_codex_config_modification(
app: tauri::AppHandle,
) -> Result<AgentConfigActionResult, String> {
let home = app.path().home_dir().map_err(|_| "无法获取用户目录")?;
let home = app.path().home_dir().map_err(|_| "Failed to get user directory")?;
let _guard = AGENT_CONFIG_FILE_LOCK
.lock()
.map_err(|_| "智能体配置文件锁已损坏")?;
.map_err(|_| "Agent configuration file lock is poisoned")?;
let result = close_codex_configuration(&home)?;
app.state::<AgentConfigStatusCache>().clear()?;
Ok(result)
@@ -362,7 +362,7 @@ fn prepare_native_oauth_switch(home: &Path, enabled: bool) -> Result<(Images, Im
.get("model")
.and_then(toml_edit::Item::as_str)
.map(str::to_string);
let saved = parse_codex_document(Some(&state.cpa_settings), "Codex CPA 接入配置")?;
let saved = parse_codex_document(Some(&state.cpa_settings), "Codex CPA access configuration")?;
for key in ROUTING_KEYS {
restore_codex_table_item(document.as_table_mut(), Some(saved.as_table()), key);
}
@@ -377,7 +377,7 @@ fn prepare_native_oauth_switch(home: &Path, enabled: bool) -> Result<(Images, Im
}
document["model_providers"]
.as_table_like_mut()
.ok_or("Codex model_providers 格式无效")?
.ok_or("Invalid Codex model_providers format")?
.insert("openai", openai.clone());
}
if oauth_auth(state.cpa_auth.as_deref())?.is_some() {
@@ -391,7 +391,7 @@ fn prepare_native_oauth_switch(home: &Path, enabled: bool) -> Result<(Images, Im
let rendered = document.to_string();
parse_codex_document(Some(&rendered), "Codex config.toml")?;
let state_bytes =
serde_json::to_vec_pretty(&state).map_err(|_| "保存 Codex 接入方式切换记录失败")?;
serde_json::to_vec_pretty(&state).map_err(|_| "Failed to save Codex access mode switch record")?;
let after = vec![
(paths[0].clone(), Some(state_bytes)),
(paths[1].clone(), next_auth.map(String::into_bytes)),
@@ -444,7 +444,7 @@ pub(crate) fn apply_codex_cpa_configuration(
state.native_auth = None;
}
native_after[0].1 =
Some(serde_json::to_vec_pretty(&state).map_err(|_| "保存 Codex 切换记录失败")?);
Some(serde_json::to_vec_pretty(&state).map_err(|_| "Failed to save Codex switch record")?);
let config_paths = config_paths("codex", home)?;
let current = config_images(&config_paths)?;
for image in &current {
@@ -452,12 +452,12 @@ pub(crate) fn apply_codex_cpa_configuration(
.iter()
.any(|(path, bytes)| path == &image.0 && bytes != &image.1)
{
return Err("配置已被其他程序修改,请刷新后重试".into());
return Err("Configuration was modified by another program. Refresh and try again".into());
}
}
let mut restored_config = native_after[2].clone();
let mut restored_document =
parse_codex_document(text(restored_config.1.as_deref())?, "Codex CPA 配置")?;
parse_codex_document(text(restored_config.1.as_deref())?, "Codex CPA configuration")?;
if restored_document.contains_key("forced_login_method") {
restored_document["forced_login_method"] =
toml_edit::value(if options.oauth_configuration {
@@ -471,9 +471,9 @@ pub(crate) fn apply_codex_cpa_configuration(
baseline
.iter_mut()
.find(|(path, _)| path == &restored_config.0)
.ok_or("Codex 配置路径不匹配")?
.ok_or("Codex configuration path mismatch")?
.1 = restored_config.1.clone();
let catalog = options.codex_catalog.ok_or("无法生成 Codex 模型目录")?;
let catalog = options.codex_catalog.ok_or("Unable to generate Codex model catalog")?;
validate_codex_catalog(catalog, model)?;
let updates = vec![
AgentFileUpdate {
@@ -518,10 +518,10 @@ pub(crate) fn apply_codex_cpa_configuration(
pub(crate) fn restore_codex_official_config(
app: tauri::AppHandle,
) -> Result<AgentConfigActionResult, String> {
let home = app.path().home_dir().map_err(|_| "无法获取用户目录")?;
let home = app.path().home_dir().map_err(|_| "Failed to get user directory")?;
let _guard = AGENT_CONFIG_FILE_LOCK
.lock()
.map_err(|_| "智能体配置文件锁已损坏")?;
.map_err(|_| "Agent configuration file lock is poisoned")?;
let result = switch_codex_native_oauth(&home, true)?;
app.state::<AgentConfigStatusCache>().clear()?;
Ok(result)
+104 -104
View File
@@ -5,18 +5,18 @@ pub(crate) fn agent_backup_path(path: &Path) -> Result<PathBuf, String> {
let file_name = path
.file_name()
.and_then(|value| value.to_str())
.ok_or_else(|| format!("智能体配置文件名无效: {}", path_to_string(path)))?;
.ok_or_else(|| format!("Invalid agent configuration filename: {}", path_to_string(path)))?;
Ok(path.with_file_name(format!("{file_name}.cpa-gui.backup")))
}
pub(crate) fn agent_state_path(paths: &[PathBuf]) -> Result<PathBuf, String> {
let primary = paths
.first()
.ok_or_else(|| "当前平台没有可用的智能体配置路径".to_string())?;
.ok_or_else(|| "No agent configuration path is available on the current platform".to_string())?;
let file_name = primary
.file_name()
.and_then(|value| value.to_str())
.ok_or_else(|| format!("智能体配置文件名无效: {}", path_to_string(primary)))?;
.ok_or_else(|| format!("Invalid agent configuration filename: {}", path_to_string(primary)))?;
Ok(primary.with_file_name(format!("{file_name}.cpa-gui.state.json")))
}
@@ -37,12 +37,12 @@ pub(crate) fn write_agent_applied_state(
if state.client == AgentClient::Codex.id() {
CODEX_APPLIED_STATES
.lock()
.map_err(|_| "Codex 应用状态内存锁已损坏".to_string())?
.map_err(|_| "Codex application state memory lock is poisoned".to_string())?
.insert(path.to_path_buf(), state.clone());
if path.is_file() {
fs::remove_file(path).map_err(|error| {
format!(
"清理旧版 Codex 应用状态失败 {}: {error}",
"Failed to clean up legacy Codex application state {}: {error}",
path_to_string(path)
)
})?;
@@ -51,7 +51,7 @@ pub(crate) fn write_agent_applied_state(
}
let mut content = serde_json::to_string_pretty(state)
.map_err(|error| format!("生成智能体应用状态失败: {error}"))?;
.map_err(|error| format!("Failed to generate agent application state: {error}"))?;
content.push('\n');
write_bytes_directly(path, content.as_bytes())
}
@@ -60,7 +60,7 @@ pub(crate) fn write_agent_applied_state(
pub(crate) fn clear_codex_applied_state(path: &Path) -> Result<(), String> {
CODEX_APPLIED_STATES
.lock()
.map_err(|_| "Codex 应用状态内存锁已损坏".to_string())?
.map_err(|_| "Codex application state memory lock is poisoned".to_string())?
.remove(path);
Ok(())
}
@@ -91,11 +91,11 @@ pub(crate) fn latest_dated_agent_backup_path(path: &Path) -> Result<Option<PathB
let file_name = path
.file_name()
.and_then(|value| value.to_str())
.ok_or_else(|| format!("智能体配置文件名无效: {}", path_to_string(path)))?;
.ok_or_else(|| format!("Invalid agent configuration filename: {}", path_to_string(path)))?;
let mut candidates = fs::read_dir(directory)
.map_err(|error| {
format!(
"读取智能体备份目录失败 {}: {error}",
"Failed to read agent backup directory {}: {error}",
path_to_string(directory)
)
})?
@@ -172,7 +172,7 @@ pub(crate) fn validate_agent_applied_state(
state: &AgentAppliedState,
) -> Result<(), String> {
if state.client != client.id() {
return Err("智能体应用状态与客户端不匹配".to_string());
return Err("Agent application state does not match the client".to_string());
}
if state.backup_files.is_empty() {
return Ok(());
@@ -197,7 +197,7 @@ pub(crate) fn validate_agent_applied_state(
.first()
.is_some_and(|path| state_paths == [path.clone()]));
if !valid_paths {
return Err("智能体应用状态文件数量或路径不匹配".to_string());
return Err("Agent application state file count or path mismatch".to_string());
}
for file in &state.backup_files {
let legacy_backup = agent_backup_path(&file.path)?;
@@ -205,7 +205,7 @@ pub(crate) fn validate_agent_applied_state(
.path
.file_name()
.and_then(|value| value.to_str())
.ok_or_else(|| format!("智能体配置文件名无效: {}", path_to_string(&file.path)))?;
.ok_or_else(|| format!("Invalid agent configuration filename: {}", path_to_string(&file.path)))?;
let dated_backup = file.backup_path.parent() == file.path.parent()
&& file
.backup_path
@@ -213,7 +213,7 @@ pub(crate) fn validate_agent_applied_state(
.and_then(|value| value.to_str())
.is_some_and(|name| is_dated_agent_backup_name(name, original_name));
if file.backup_path != legacy_backup && !dated_backup {
return Err("智能体应用状态包含非预期备份路径".to_string());
return Err("Agent application state contains an unexpected backup path".to_string());
}
}
Ok(())
@@ -229,7 +229,7 @@ pub(crate) fn load_agent_applied_state(
if client == AgentClient::Codex {
if let Some(state) = CODEX_APPLIED_STATES
.lock()
.map_err(|_| "Codex 应用状态内存锁已损坏".to_string())?
.map_err(|_| "Codex application state memory lock is poisoned".to_string())?
.get(&state_path)
.cloned()
{
@@ -248,17 +248,17 @@ pub(crate) fn load_agent_applied_state(
return Ok(recovered);
}
let content = fs::read_to_string(&state_path)
.map_err(|error| format!("读取智能体应用状态失败: {error}"))?;
.map_err(|error| format!("Failed to read agent application state: {error}"))?;
let value = serde_json::from_str::<serde_json::Value>(&content)
.map_err(|error| format!("解析智能体应用状态失败: {error}"))?;
.map_err(|error| format!("Failed to parse agent application state: {error}"))?;
let version = value
.get("version")
.and_then(serde_json::Value::as_u64)
.ok_or_else(|| "智能体应用状态缺少版本号".to_string())?;
let version = u8::try_from(version).map_err(|_| "不支持的智能体应用状态版本".to_string())?;
.ok_or_else(|| "Agent application state is missing a version number".to_string())?;
let version = u8::try_from(version).map_err(|_| "Unsupported agent application state version".to_string())?;
if version == AGENT_APPLIED_STATE_VERSION || version == 3 {
let state = serde_json::from_value::<AgentAppliedState>(value)
.map_err(|error| format!("解析智能体应用状态失败: {error}"))?;
.map_err(|error| format!("Failed to parse agent application state: {error}"))?;
validate_agent_applied_state(client, &paths, &state)?;
if client == AgentClient::Codex {
write_agent_applied_state(&state_path, &state)?;
@@ -268,11 +268,11 @@ pub(crate) fn load_agent_applied_state(
if version != LEGACY_AGENT_MODIFICATION_STATE_VERSION
&& version != AGENT_MODIFICATION_STATE_VERSION
{
return Err("不支持的智能体应用状态版本".to_string());
return Err("Unsupported agent application state version".to_string());
}
let record = serde_json::from_value::<AgentModificationRecord>(value)
.map_err(|error| format!("解析旧版智能体状态失败: {error}"))?;
.map_err(|error| format!("Failed to parse legacy agent state: {error}"))?;
validate_agent_record(client, &paths, &record)?;
let backup_files = record
.files
@@ -304,7 +304,7 @@ pub(crate) fn read_agent_bytes(path: &Path) -> Result<Option<Vec<u8>>, String> {
match fs::read(path) {
Ok(bytes) => Ok(Some(bytes)),
Err(e) if e.kind() == io::ErrorKind::NotFound => Ok(None),
Err(_) => Err(format!("读取智能体配置失败: {},请检查文件权限或占用情况", path_to_string(path))),
Err(_) => Err(format!("Failed to read agent configuration: {}. Check file permissions or whether the file is in use", path_to_string(path))),
}
}
@@ -318,16 +318,16 @@ pub(crate) fn read_agent_original_bytes(
let original_sha256 = file
.original_sha256
.as_deref()
.ok_or_else(|| format!("原配置备份缺少校验值: {}", path_to_string(&file.path)))?;
.ok_or_else(|| format!("Original configuration backup is missing a checksum: {}", path_to_string(&file.path)))?;
let bytes = fs::read(&file.backup_path).map_err(|error| {
format!(
"读取原配置备份失败 {}: {error}",
"Failed to read original configuration backup {}: {error}",
path_to_string(&file.backup_path)
)
})?;
if sha256_bytes(&bytes) != original_sha256 {
return Err(format!(
"原配置备份校验失败: {}",
"Original configuration backup verification failed: {}",
path_to_string(&file.backup_path)
));
}
@@ -342,7 +342,7 @@ pub(crate) fn read_agent_original_text(
.map(|bytes| {
String::from_utf8(bytes).map_err(|_| {
format!(
"原 Codex 配置不是 UTF-8 文本: {}",
"Original Codex configuration is not UTF-8 text: {}",
path_to_string(&file.path)
)
})
@@ -357,11 +357,11 @@ pub(crate) fn write_agent_state(
) -> Result<(), String> {
if let Some(parent) = path.parent() {
fs::create_dir_all(parent).map_err(|error| {
format!("创建智能体状态目录失败 {}: {error}", path_to_string(parent))
format!("Failed to create agent state directory {}: {error}", path_to_string(parent))
})?;
}
let mut content = serde_json::to_string_pretty(record)
.map_err(|error| format!("生成智能体备份状态失败: {error}"))?;
.map_err(|error| format!("Failed to generate agent backup state: {error}"))?;
content.push('\n');
write_yaml_if_changed(path, &content).map(|_| ())
}
@@ -375,7 +375,7 @@ pub(crate) fn validate_agent_record(
let supported_version = record.version == AGENT_MODIFICATION_STATE_VERSION
|| record.version == LEGACY_AGENT_MODIFICATION_STATE_VERSION;
if !supported_version || record.client != client.id() {
return Err("智能体备份状态版本或客户端不匹配".to_string());
return Err("Agent backup state version or client mismatch".to_string());
}
if ![
AGENT_PHASE_APPLYING,
@@ -385,7 +385,7 @@ pub(crate) fn validate_agent_record(
]
.contains(&record.phase.as_str())
{
return Err("智能体备份状态阶段无效".to_string());
return Err("Invalid agent backup state stage".to_string());
}
let expected_paths = expected_agent_record_paths(client, paths);
let record_paths = record
@@ -400,11 +400,11 @@ pub(crate) fn validate_agent_record(
.first()
.is_some_and(|path| record_paths == [path.clone()]));
if !valid_paths {
return Err("智能体备份状态文件数量或路径不匹配".to_string());
return Err("Agent backup state file count or path mismatch".to_string());
}
for file in &record.files {
if file.backup_path != agent_backup_path(&file.path)? {
return Err("智能体备份状态包含非预期路径".to_string());
return Err("Agent backup state contains an unexpected path".to_string());
}
}
Ok(())
@@ -421,9 +421,9 @@ pub(crate) fn load_agent_record(
}
let mut record: AgentModificationRecord = serde_json::from_str(
&fs::read_to_string(&state_path)
.map_err(|error| format!("读取智能体备份状态失败: {error}"))?,
.map_err(|error| format!("Failed to read agent backup state: {error}"))?,
)
.map_err(|error| format!("解析智能体备份状态失败: {error}"))?;
.map_err(|error| format!("Failed to parse agent backup state: {error}"))?;
validate_agent_record(client, paths, &record)?;
if record.version == LEGACY_AGENT_MODIFICATION_STATE_VERSION {
record.version = AGENT_MODIFICATION_STATE_VERSION;
@@ -548,17 +548,17 @@ pub(crate) fn inspect_agent_modification(
};
let mut warnings = Vec::new();
if !backup_available {
warnings.push("原配置备份不完整,恢复前请勿删除剩余备份文件".to_string());
warnings.push("Original configuration backup is incomplete. Do not delete the remaining backup files before restoration".to_string());
}
if state == AGENT_MODIFICATION_STATE_CONFLICT {
warnings.push("配置已被其他程序修改,清除修改时需要确认是否恢复".to_string());
warnings.push("Configuration was modified by another program. Confirmation is required before restoring it when clearing changes".to_string());
} else if state == "recovery" {
warnings.push("上次配置操作未完整结束,可使用“清除修改”恢复原配置".to_string());
warnings.push("The last configuration operation did not complete. Use “Clear Changes” to restore the original configuration".to_string());
} else if client == AgentClient::Codex
&& codex_record_needs_resync(&record, configured, current_model).unwrap_or(true)
{
warnings.push(
"Codex 受管配置已发生变化,将在下次更新或启动时自动重新同步".to_string(),
"Managed Codex configuration has changed and will be resynchronized automatically on the next update or launch".to_string(),
);
}
AgentModificationInspection {
@@ -595,7 +595,7 @@ pub(crate) fn inspect_agent_modification(
state: "active".to_string(),
backup_available: true,
warnings: vec![
"检测到旧版 CPA 配置和备份,可使用“清除修改”恢复原配置".to_string()
"Legacy CPA configuration and backup detected. Use “Clear Changes” to restore the original configuration".to_string()
],
}
}
@@ -604,7 +604,7 @@ pub(crate) fn inspect_agent_modification(
enabled: false,
state: "inactive".to_string(),
backup_available: false,
warnings: vec!["检测到 CPA 配置,但缺少可安全恢复的原始备份".to_string()],
warnings: vec!["CPA configuration detected, but no original backup is available for safe restoration".to_string()],
}
}
Err(error) => {
@@ -727,7 +727,7 @@ pub(crate) fn fresh_agent_contents_with_oauth(
build_deepseek_harness_settings(None, &openai_base, model, models)?,
build_deepseek_harness_credentials(None, api_key)?,
]),
AgentClient::AntigravityCli => Err("Antigravity 模板需要安装和用户目录信息".into()),
AgentClient::AntigravityCli => Err("Antigravity template requires installation and user directory information".into()),
AgentClient::WorkBuddy => Ok(vec![build_workbuddy_agent_config(
None, &openai_base, api_key, model, models,
)?]),
@@ -817,14 +817,14 @@ pub(crate) fn build_legacy_agent_record(
let (existed_before, original_sha256) = if backup_path.is_file() {
let backup = fs::read(&backup_path).map_err(|error| {
format!(
"读取旧版智能体备份失败 {}: {error}",
"Failed to read legacy agent backup {}: {error}",
path_to_string(&backup_path)
)
})?;
(true, Some(sha256_bytes(&backup)))
} else {
let actual = String::from_utf8(current.clone())
.map_err(|_| format!("智能体配置不是 UTF-8 文本: {}", path_to_string(path)))?;
.map_err(|_| format!("Agent configuration is not UTF-8 text: {}", path_to_string(path)))?;
if !agent_contents_equal(client, &actual, &generated[index]) {
return Ok(None);
}
@@ -855,16 +855,16 @@ pub(crate) fn prepare_agent_record(
updates: &[AgentFileUpdate],
) -> Result<AgentModificationRecord, String> {
if paths.len() != updates.len() {
return Err("智能体配置更新文件数量不匹配".to_string());
return Err("Agent configuration update file count mismatch".to_string());
}
let mut prepared = Vec::new();
for (path, update) in paths.iter().zip(updates) {
if path != &update.path {
return Err("智能体配置更新路径不匹配".to_string());
return Err("Agent configuration update path mismatch".to_string());
}
if let Some(parent) = path.parent() {
fs::create_dir_all(parent).map_err(|error| {
format!("创建智能体配置目录失败 {}: {error}", path_to_string(parent))
format!("Failed to create agent configuration directory {}: {error}", path_to_string(parent))
})?;
}
let backup_path = agent_backup_path(path)?;
@@ -872,13 +872,13 @@ pub(crate) fn prepare_agent_record(
let previous_backup = if backup_path.exists() {
if !backup_path.is_file() {
return Err(format!(
"智能体备份路径不是文件: {}",
"Agent backup path is not a file: {}",
path_to_string(&backup_path)
));
}
Some(fs::read(&backup_path).map_err(|error| {
format!(
"读取原有智能体备份失败 {}: {error}",
"Failed to read existing agent backup {}: {error}",
path_to_string(&backup_path)
)
})?)
@@ -907,19 +907,19 @@ pub(crate) fn prepare_agent_record(
write_bytes_atomically(&backup_path, current).and_then(|_| {
let copied = fs::read(&backup_path).map_err(|error| {
format!(
"校验智能体备份失败 {}: {error}",
"Failed to verify agent backup {}: {error}",
path_to_string(&backup_path)
)
})?;
if sha256_bytes(&copied) != sha256_bytes(current) {
return Err(format!("智能体备份校验失败: {}", path_to_string(&path)));
return Err(format!("Agent backup verification failed: {}", path_to_string(&path)));
}
Ok(())
})
} else if backup_path.exists() {
fs::remove_file(&backup_path).map_err(|error| {
format!(
"清理旧智能体备份失败 {}: {error}",
"Failed to clean up old agent backup {}: {error}",
path_to_string(&backup_path)
)
})
@@ -930,7 +930,7 @@ pub(crate) fn prepare_agent_record(
let rollback = restore_snapshots(&backup_snapshots);
return Err(match rollback {
Ok(()) => error,
Err(rollback_error) => format!("{error};恢复原有备份失败: {rollback_error}"),
Err(rollback_error) => format!("{error}; failed to restore existing backup: {rollback_error}"),
});
}
@@ -959,11 +959,11 @@ pub(crate) fn extend_agent_record_for_updates(
updates: &[AgentFileUpdate],
) -> Result<AgentRecordExtension, String> {
if record.files.len() > updates.len() {
return Err("智能体配置更新文件数量不匹配".to_string());
return Err("Agent configuration update file count mismatch".to_string());
}
for (file, update) in record.files.iter().zip(updates) {
if file.path != update.path {
return Err("智能体配置更新路径不匹配".to_string());
return Err("Agent configuration update path mismatch".to_string());
}
}
if record.files.len() == updates.len() {
@@ -974,20 +974,20 @@ pub(crate) fn extend_agent_record_for_updates(
for update in updates.iter().skip(record.files.len()) {
if let Some(parent) = update.path.parent() {
fs::create_dir_all(parent).map_err(|error| {
format!("创建智能体配置目录失败 {}: {error}", path_to_string(parent))
format!("Failed to create agent configuration directory {}: {error}", path_to_string(parent))
})?;
}
let backup_path = agent_backup_path(&update.path)?;
let previous_backup = if backup_path.exists() {
if !backup_path.is_file() {
return Err(format!(
"智能体备份路径不是文件: {}",
"Agent backup path is not a file: {}",
path_to_string(&backup_path)
));
}
Some(fs::read(&backup_path).map_err(|error| {
format!(
"读取原有智能体备份失败 {}: {error}",
"Failed to read existing agent backup {}: {error}",
path_to_string(&backup_path)
)
})?)
@@ -1008,13 +1008,13 @@ pub(crate) fn extend_agent_record_for_updates(
write_bytes_atomically(&backup_path, current).and_then(|_| {
let copied = fs::read(&backup_path).map_err(|error| {
format!(
"校验智能体备份失败 {}: {error}",
"Failed to verify agent backup {}: {error}",
path_to_string(&backup_path)
)
})?;
if sha256_bytes(&copied) != sha256_bytes(current) {
return Err(format!(
"智能体备份校验失败: {}",
"Agent backup verification failed: {}",
path_to_string(&update.path)
));
}
@@ -1023,7 +1023,7 @@ pub(crate) fn extend_agent_record_for_updates(
} else if backup_path.exists() {
fs::remove_file(&backup_path).map_err(|error| {
format!(
"清理旧智能体备份失败 {}: {error}",
"Failed to clean up old agent backup {}: {error}",
path_to_string(&backup_path)
)
})
@@ -1034,7 +1034,7 @@ pub(crate) fn extend_agent_record_for_updates(
let rollback = restore_snapshots(&backup_snapshots);
return Err(match rollback {
Ok(()) => error,
Err(rollback_error) => format!("{error};恢复原有备份失败: {rollback_error}"),
Err(rollback_error) => format!("{error}; failed to restore existing backup: {rollback_error}"),
});
}
next.files.push(AgentModificationFile {
@@ -1076,7 +1076,7 @@ pub(crate) fn restore_snapshots_with_direct_path(
let result = match bytes {
Some(bytes) => write_agent_bytes(path, bytes, direct_write_path),
None if path.exists() => fs::remove_file(path)
.map_err(|error| format!("删除配置失败 {}: {error}", path_to_string(path))),
.map_err(|error| format!("Failed to delete configuration {}: {error}", path_to_string(path))),
None => Ok(()),
};
if let Err(error) = result {
@@ -1111,7 +1111,7 @@ pub(crate) fn apply_agent_file_replacements(
None if current.is_some() => {
changed.push(path_to_string(path));
fs::remove_file(path)
.map_err(|error| format!("删除配置失败 {}: {error}", path_to_string(path)))
.map_err(|error| format!("Failed to delete configuration {}: {error}", path_to_string(path)))
}
None => Ok(()),
};
@@ -1119,7 +1119,7 @@ pub(crate) fn apply_agent_file_replacements(
let rollback = restore_snapshots_with_direct_path(&snapshots, direct_write_path);
return Err(match rollback {
Ok(()) => error,
Err(rollback_error) => format!("{error};回滚失败: {rollback_error}"),
Err(rollback_error) => format!("{error}; rollback failed: {rollback_error}"),
});
}
}
@@ -1133,12 +1133,12 @@ pub(crate) fn restore_codex_agent_record_files(
let config_file = record
.files
.first()
.ok_or_else(|| "Codex 配置状态缺少 config.toml".to_string())?;
.ok_or_else(|| "Codex configuration state is missing config.toml".to_string())?;
let current_config = read_agent_bytes(&config_file.path)?
.map(|bytes| {
String::from_utf8(bytes).map_err(|_| {
format!(
"当前 Codex 配置不是 UTF-8 文本: {}",
"Current Codex configuration is not UTF-8 text: {}",
path_to_string(&config_file.path)
)
})
@@ -1182,7 +1182,7 @@ pub(crate) fn apply_agent_updates(
let rollback = restore_snapshots_with_direct_path(&snapshots, direct_write_path);
return Err(match rollback {
Ok(()) => error,
Err(rollback_error) => format!("{error};回滚失败: {rollback_error}"),
Err(rollback_error) => format!("{error}; rollback failed: {rollback_error}"),
});
}
changed.push(path_to_string(&update.path));
@@ -1197,7 +1197,7 @@ pub(crate) fn restore_agent_snapshots_direct(snapshots: &[FileSnapshot]) -> Resu
let result = match content {
Some(content) => write_bytes_directly(path, content),
None if path.exists() => fs::remove_file(path)
.map_err(|error| format!("删除新建配置失败 {}: {error}", path_to_string(path))),
.map_err(|error| format!("Failed to delete newly created configuration {}: {error}", path_to_string(path))),
None => Ok(()),
};
if let Err(error) = result {
@@ -1216,7 +1216,7 @@ pub(crate) fn dated_agent_backup_path(path: &Path) -> Result<PathBuf, String> {
let file_name = path
.file_name()
.and_then(|value| value.to_str())
.ok_or_else(|| format!("智能体配置文件名无效: {}", path_to_string(path)))?;
.ok_or_else(|| format!("Invalid agent configuration filename: {}", path_to_string(path)))?;
let date = chrono::Local::now().format("%y-%m-%d");
Ok(path.with_file_name(format!("{file_name}.{date}.bak")))
}
@@ -1254,7 +1254,7 @@ pub(crate) fn restore_agent_applied_state_configuration(
if file.existed_before {
fs::read(&file.backup_path).map(Some).map_err(|error| {
format!(
"读取智能体备份失败 {}: {error}",
"Failed to read agent backup {}: {error}",
path_to_string(&file.backup_path)
)
})
@@ -1290,7 +1290,7 @@ pub(crate) fn restore_agent_applied_state_configuration(
} else if file.path.exists() {
fs::remove_file(&file.path).map_err(|error| {
format!(
"删除临时智能体配置失败 {}: {error}",
"Failed to delete temporary agent configuration {}: {error}",
path_to_string(&file.path)
)
})?;
@@ -1301,14 +1301,14 @@ pub(crate) fn restore_agent_applied_state_configuration(
if let Err(error) = restore_result {
return match restore_agent_snapshots_direct(&snapshots) {
Ok(()) => Err(error),
Err(rollback_error) => Err(format!("{error};回滚恢复操作失败: {rollback_error}")),
Err(rollback_error) => Err(format!("{error}; failed to roll back restoration: {rollback_error}")),
};
}
for file in &state.backup_files {
if file.existed_before && file.backup_path.is_file() {
fs::remove_file(&file.backup_path).map_err(|error| {
format!(
"清理智能体备份失败 {}: {error}",
"Failed to clean up agent backup {}: {error}",
path_to_string(&file.backup_path)
)
})?;
@@ -1321,7 +1321,7 @@ pub(crate) fn restore_agent_applied_state_configuration(
if state_path.is_file() {
fs::remove_file(state_path).map_err(|error| {
format!(
"清理智能体应用状态失败 {}: {error}",
"Failed to clean up agent application state {}: {error}",
path_to_string(state_path)
)
})?;
@@ -1354,7 +1354,7 @@ pub(crate) fn sync_codex_model_catalog_if_configured(
let paths = agent_config_paths(client, home);
let _guard = AGENT_CONFIG_FILE_LOCK
.lock()
.map_err(|_| "智能体配置文件锁已损坏".to_string())?;
.map_err(|_| "Agent configuration file lock is poisoned".to_string())?;
if codex_native_oauth_enabled(home)? { return Ok(false); }
let before = config_images(&config_paths(client.id(), home)?)?;
validate_config_images(&before)?;
@@ -1376,13 +1376,13 @@ pub(crate) fn sync_codex_model_catalog_if_configured(
validate_codex_catalog(catalog, model)?;
} else {
let root: serde_json::Value = serde_json::from_str(catalog)
.map_err(|error| format!("Codex 模型目录格式无效: {error}"))?;
.map_err(|error| format!("Invalid Codex model catalog format: {error}"))?;
if !root
.get("models")
.and_then(serde_json::Value::as_array)
.is_some_and(Vec::is_empty)
{
return Err("空模型列表必须对应空的 Codex 模型目录".to_string());
return Err("An empty model list must correspond to an empty Codex model catalog".to_string());
}
}
@@ -1396,10 +1396,10 @@ pub(crate) fn sync_codex_model_catalog_if_configured(
}
if current_model.as_deref() != next_model {
let content = fs::read_to_string(&paths[0])
.map_err(|error| format!("读取 Codex 配置失败: {error}"))?;
.map_err(|error| format!("Failed to read Codex configuration: {error}"))?;
let mut document = content
.parse::<toml_edit::Document>()
.map_err(|error| format!("Codex config.toml 格式无效: {error}"))?;
.map_err(|error| format!("Invalid Codex config.toml format: {error}"))?;
if let Some(model) = next_model {
set_codex_table_item(document.as_table_mut(), "model", toml_edit::value(model));
} else {
@@ -1466,7 +1466,7 @@ pub(crate) fn apply_agent_configuration_with_oauth(
if client == AgentClient::WorkBuddy {
error
} else {
"配置构建失败,请检查配置结构或使用手动备份恢复、基础配置模板修复".to_string()
"Failed to build configuration. Check its structure, restore from a manual backup, or repair it with the base configuration template".to_string()
}
})?;
config_updates(client.id(), home, &before, &updates, "update", Some(model.to_string()), mappings)
@@ -1559,7 +1559,7 @@ pub(crate) fn build_agent_template_updates(
},
)?;
if paths.len() != contents.len() {
return Err("基础配置模板文件数量不匹配".to_string());
return Err("Base configuration template file count mismatch".to_string());
}
let mut updates = paths
.into_iter()
@@ -1567,7 +1567,7 @@ pub(crate) fn build_agent_template_updates(
.map(|(path, after)| AgentFileUpdate { path, after })
.collect::<Vec<_>>();
if client == AgentClient::Codex {
let catalog = codex_catalog.ok_or_else(|| "无法生成 Codex 模型目录".to_string())?;
let catalog = codex_catalog.ok_or_else(|| "Unable to generate Codex model catalog".to_string())?;
validate_codex_catalog(catalog, model)?;
updates.push(AgentFileUpdate {
path: codex_model_catalog_path(home),
@@ -1600,13 +1600,13 @@ pub(crate) fn restore_agent_record_files(
let result = if file.existed_before {
let backup = fs::read(&file.backup_path).map_err(|error| {
format!(
"读取原配置备份失败 {}: {error}",
"Failed to read original configuration backup {}: {error}",
path_to_string(&file.backup_path)
)
})?;
if Some(sha256_bytes(&backup)) != file.original_sha256 {
return Err(format!(
"原配置备份校验失败: {}",
"Original configuration backup verification failed: {}",
path_to_string(&file.backup_path)
));
}
@@ -1619,7 +1619,7 @@ pub(crate) fn restore_agent_record_files(
} else if file.path.exists() {
changed.push(path_to_string(&file.path));
fs::remove_file(&file.path).map_err(|error| {
format!("删除智能体配置失败 {}: {error}", path_to_string(&file.path))
format!("Failed to delete agent configuration {}: {error}", path_to_string(&file.path))
})
} else {
Ok(())
@@ -1628,7 +1628,7 @@ pub(crate) fn restore_agent_record_files(
let rollback = restore_snapshots_with_direct_path(&snapshots, direct_write_path);
return Err(match rollback {
Ok(()) => error,
Err(rollback_error) => format!("{error};回滚失败: {rollback_error}"),
Err(rollback_error) => format!("{error}; rollback failed: {rollback_error}"),
});
}
}
@@ -1644,7 +1644,7 @@ pub(crate) fn discard_prepared_agent_backups(
if file.backup_path.exists() {
if let Err(error) = fs::remove_file(&file.backup_path) {
errors.push(format!(
"删除未启用的智能体备份失败 {}: {error}",
"Failed to delete disabled agent backup {}: {error}",
path_to_string(&file.backup_path)
));
}
@@ -1665,7 +1665,7 @@ pub(crate) fn cleanup_agent_record(
if state_path.exists() {
fs::remove_file(state_path).map_err(|error| {
format!(
"删除智能体备份状态失败 {}: {error}",
"Failed to delete agent backup state {}: {error}",
path_to_string(state_path)
)
})?;
@@ -1674,7 +1674,7 @@ pub(crate) fn cleanup_agent_record(
if file.backup_path.exists() {
if let Err(error) = fs::remove_file(&file.backup_path) {
eprintln!(
"清理智能体备份失败 {}: {error}",
"Failed to clean up agent backup {}: {error}",
path_to_string(&file.backup_path)
);
}
@@ -1736,7 +1736,7 @@ pub(crate) fn enable_agent_modification(
}
}
return Err(
"检测到 CPA 配置,但缺少可安全恢复的原始备份;请先手动恢复客户端配置".to_string(),
"CPA configuration detected, but no original backup is available for safe restoration. Restore the client configuration manually first".to_string(),
);
}
@@ -1781,7 +1781,7 @@ pub(crate) fn enable_agent_modification(
Err(restore_error) => {
record.phase = AGENT_PHASE_RECOVERY.to_string();
let _ = write_agent_state(&state_path, &record);
Err(format!("{error};恢复原配置失败: {restore_error}"))
Err(format!("{error}; failed to restore original configuration: {restore_error}"))
}
},
}
@@ -1810,9 +1810,9 @@ pub(crate) fn disable_agent_modification(
Vec::new(),
));
}
let model = model.ok_or_else(|| "无法识别当前 CPA 模型".to_string())?;
let model = model.ok_or_else(|| "Unable to identify the current CPA model".to_string())?;
let record = build_legacy_agent_record(client, home, port, &model)?
.ok_or_else(|| "检测到 CPA 配置,但缺少可安全恢复的原始备份".to_string())?;
.ok_or_else(|| "CPA configuration detected, but no original backup is available for safe restoration".to_string())?;
write_agent_state(&state_path, &record)?;
record
}
@@ -1829,7 +1829,7 @@ pub(crate) fn disable_agent_modification(
Err(error) => {
record.phase = AGENT_PHASE_RECOVERY.to_string();
let _ = write_agent_state(&state_path, &record);
Err(format!("恢复原配置失败: {error}"))
Err(format!("Failed to restore original configuration: {error}"))
}
};
}
@@ -1866,7 +1866,7 @@ pub(crate) fn disable_agent_modification(
Err(error) => {
record.phase = AGENT_PHASE_RECOVERY.to_string();
let _ = write_agent_state(&state_path, &record);
Err(format!("恢复原配置失败: {error}"))
Err(format!("Failed to restore original configuration: {error}"))
}
}
}
@@ -1888,23 +1888,23 @@ pub(crate) fn update_agent_modification(
let (_, current_model, _) =
inspect_agent_managed_config(client, &paths, port, DEFAULT_API_KEY)?;
if !agent_has_managed_marker(client, &paths)? {
return Err("请先应用配置修改".to_string());
return Err("Apply configuration changes first".to_string());
}
let current_model = current_model.ok_or_else(|| "无法识别当前 CPA 模型".to_string())?;
let current_model = current_model.ok_or_else(|| "Unable to identify the current CPA model".to_string())?;
let record = build_legacy_agent_record(client, home, port, &current_model)?
.ok_or_else(|| "缺少原配置备份,无法安全更新".to_string())?;
.ok_or_else(|| "Original configuration backup is missing; cannot update safely".to_string())?;
write_agent_state(&state_path, &record)?;
record
}
};
if record.phase != AGENT_PHASE_ACTIVE {
return Err("上次配置操作尚未完整结束,请先使用“清除修改”恢复原配置".to_string());
return Err("The last configuration operation has not completed. Use “Clear Changes” to restore the original configuration first".to_string());
}
if client != AgentClient::Codex {
let conflicts = record_conflict_files(&record)?;
if !conflicts.is_empty() {
return Err(format!(
"配置已被其他程序修改,无法更新: {}",
"Configuration was modified by another program and cannot be updated: {}",
conflicts.join("、")
));
}
@@ -1924,7 +1924,7 @@ pub(crate) fn update_agent_modification(
next.model = model.to_string();
for (file, update) in next.files.iter_mut().zip(&updates) {
if file.path != update.path {
return Err("智能体配置更新路径不匹配".to_string());
return Err("Agent configuration update path mismatch".to_string());
}
file.managed_sha256 = sha256_bytes(update.after.as_bytes());
}
@@ -1932,7 +1932,7 @@ pub(crate) fn update_agent_modification(
let rollback = restore_snapshots(&backup_snapshots);
return Err(match rollback {
Ok(()) => error,
Err(rollback_error) => format!("{error};恢复模型目录备份失败: {rollback_error}"),
Err(rollback_error) => format!("{error}; failed to restore model catalog backup: {rollback_error}"),
});
}
match apply_agent_updates(client, &updates) {
@@ -1952,10 +1952,10 @@ pub(crate) fn update_agent_modification(
let backup_rollback = restore_snapshots(&backup_snapshots).err();
let mut errors = vec![error];
if let Some(rollback_error) = state_rollback {
errors.push(format!("恢复原状态失败: {rollback_error}"));
errors.push(format!("Failed to restore original state: {rollback_error}"));
}
if let Some(rollback_error) = backup_rollback {
errors.push(format!("恢复模型目录备份失败: {rollback_error}"));
errors.push(format!("Failed to restore model catalog backup: {rollback_error}"));
}
Err(errors.join(";"))
}
+37 -30
View File
@@ -35,7 +35,7 @@ pub(crate) fn build_codex_template_auth(home: &Path) -> Result<AgentFileUpdate,
}
Ok(AgentFileUpdate {
path,
after: serde_json::to_string_pretty(&target).map_err(|_| "生成认证模板失败")?,
after: serde_json::to_string_pretty(&target).map_err(|_| "Failed to generate authentication template")?,
})
}
@@ -46,7 +46,7 @@ pub(crate) fn build_pi_template_updates(
model: &str,
) -> Result<Vec<AgentFileUpdate>, String> {
if port == 0 || api_key.trim().is_empty() {
return Err("CPA 地址或密钥无效".into());
return Err("Invalid CPA address or key".into());
}
let settings = serde_json::json!({"packages": [PI_CLIPROXYAPI_PACKAGE]}).to_string();
Ok(vec![
@@ -86,29 +86,36 @@ pub(crate) async fn prepare_desktop_core_update(
}
pub(crate) fn agent_core_error(error: String) -> String {
if error.contains("自动恢复失败") || error.contains("回滚失败") {
"内核别名或智能体配置写入失败且回滚失败,请检查当前配置".into()
} else if error.contains("已恢复原配置") {
"内核别名或智能体配置写入失败,已恢复原配置,请检查连接和模型映射后重试".into()
} else if error.contains("配置已变化") {
"内核配置已变化,请重新预览后重试".into()
} else if error.contains("已被其他模型使用") {
"模型别名已被其他模型占用,请切换其他别名后重试".into()
} else if error.contains("无法确定模型") && error.contains("CPA 配置来源") {
"无法找到原模型的有效接入来源,请确认接入已启用且未屏蔽原模型或别名,刷新模型列表后重试".into()
} else if error.starts_with("更新后的内核配置与预期值不一致")
|| error.starts_with("验证更新后的内核配置失败")
let normalized = error.to_ascii_lowercase();
if normalized.contains("automatic restoration failed")
|| normalized.contains("rollback failed")
|| normalized.contains("rollback both failed")
{
"内核配置格式兼容性校验失败,已取消别名同步,原配置未写入".into()
} else if error.starts_with("解析内核 YAML 配置失败") {
"内核 YAML 配置格式无效,请检查配置格式后重试".into()
} else if error.starts_with("管理 API 错误 (401)")
|| error.starts_with("管理 API 错误 (403)")
|| error.starts_with("管理接口不可用")
"Kernel alias or agent configuration write failed, and rollback also failed. Check the current configuration".into()
} else if normalized.contains("original configuration was restored") {
"Kernel alias or agent configuration write failed. Original configuration was restored. Check the connection and model mapping, then try again".into()
} else if normalized.contains("configuration changed") {
"Kernel configuration changed. Preview it again and retry".into()
} else if normalized.contains("already used by another model") {
"Model alias is already used by another model. Choose a different alias and try again".into()
} else if normalized.contains("unable to determine")
&& normalized.contains("cpa configuration source")
&& normalized.contains("model")
{
"内核管理接口认证失败,请检查管理密钥后重试".into()
"Unable to find a valid access source for the original model. Make sure access is enabled and the original model or alias is not blocked, then refresh the model list and try again".into()
} else if normalized.starts_with("updated kernel configuration does not match the expected value")
|| normalized.starts_with("failed to validate updated kernel configuration")
{
"Kernel configuration format compatibility check failed. Alias synchronization was canceled, and the original configuration was not written".into()
} else if normalized.starts_with("failed to parse kernel yaml configuration") {
"Invalid kernel YAML configuration format. Check the format and try again".into()
} else if normalized.starts_with("management api error (401)")
|| normalized.starts_with("management api error (403)")
|| normalized.starts_with("management interface unavailable")
{
"Kernel management interface authentication failed. Check the management key and try again".into()
} else {
"内核模型别名同步失败,请检查内核连接和模型映射".into()
"Kernel model alias synchronization failed. Check the kernel connection and model mapping".into()
}
}
@@ -144,7 +151,7 @@ async fn prepare_template_plan(
let model = resolve_pi_default_model(config, model).await?;
let _guard = AGENT_CONFIG_FILE_LOCK
.lock()
.map_err(|_| "配置文件锁已损坏")?;
.map_err(|_| "Configuration file lock is poisoned")?;
let before = config_images(&paths)?;
let updates = build_pi_template_updates(home, config.port, api_key, &model)?;
let after = prepare_config_updates(client, &paths, &before, &updates, true)?;
@@ -169,7 +176,7 @@ async fn prepare_template_plan(
)?;
let _guard = AGENT_CONFIG_FILE_LOCK
.lock()
.map_err(|_| "配置文件锁已损坏")?;
.map_err(|_| "Configuration file lock is poisoned")?;
let before = config_images(&paths)?;
let updates = build_agent_template_updates(AgentDefaultConfiguration {
client: parsed,
@@ -210,7 +217,7 @@ async fn prepare_template_plan(
&mappings,
core_revision,
))
.map_err(|_| "生成模板预览失败")?,
.map_err(|_| "Failed to generate template preview")?,
);
let preview = TemplatePreview {
revision,
@@ -235,14 +242,14 @@ async fn execute_template_plan(
revision: &str,
) -> Result<AgentConfigActionResult, String> {
if revision != plan.preview.revision {
return Err("预览后配置或模板发生变化,请重新预览基础配置模板".into());
return Err("Configuration or template changed after preview. Preview the base configuration template again".into());
}
let commit = || {
let _guard = AGENT_CONFIG_FILE_LOCK
.lock()
.map_err(|_| "配置文件锁已损坏")?;
.map_err(|_| "Configuration file lock is poisoned")?;
if mapping_revision(&plan.client, &plan.paths)? != plan.mapping_revision {
return Err("模型映射已变化,请重新预览".into());
return Err("Model mapping changed. Preview it again".into());
}
commit_config_with_mappings(
&plan.client,
@@ -273,7 +280,7 @@ pub(crate) async fn preview_agent_config_template(
claude_code_model_mappings: Option<ClaudeDesktopModelMappings>,
claude_desktop_model_mappings: Option<ClaudeDesktopModelMappings>,
) -> Result<TemplatePreview, String> {
let home = app.path().home_dir().map_err(|_| "无法获取用户目录")?;
let home = app.path().home_dir().map_err(|_| "Failed to get user directory")?;
let config = app.state::<GuiConfigState>().snapshot()?;
Ok(prepare_template_plan(
&config,
@@ -298,7 +305,7 @@ pub(crate) async fn apply_agent_config_template(
claude_desktop_model_mappings: Option<ClaudeDesktopModelMappings>,
revision: String,
) -> Result<AgentConfigActionResult, String> {
let home = app.path().home_dir().map_err(|_| "无法获取用户目录")?;
let home = app.path().home_dir().map_err(|_| "Failed to get user directory")?;
let config = app.state::<GuiConfigState>().snapshot()?;
let plan = prepare_template_plan(
&config,
+27 -12
View File
@@ -4,14 +4,14 @@ use super::*;
fn core_failure_messages_expose_outcomes_without_secret_source_text() {
for detail in [
"YAML source: key: secret-token",
"已恢复原配置 secret-token",
"自动恢复失败 secret-token",
"配置已变化 secret-token",
"Original configuration restored secret-token",
"Automatic restoration failed secret-token",
"Configuration changed secret-token",
] {
let rendered = agent_core_error(detail.into());
assert!(!rendered.contains("secret-token"));
if detail.contains("自动恢复失败") {
assert!(rendered.contains("回滚失败"));
if detail.contains("Automatic restoration failed") {
assert!(rendered.contains("rollback also failed"));
}
}
}
@@ -19,13 +19,28 @@ fn core_failure_messages_expose_outcomes_without_secret_source_text() {
#[test]
fn core_failure_messages_distinguish_alias_and_configuration_failures() {
for (detail, expected) in [
("别名 claude-sonnet-5 已被其他模型使用,请更换别名 secret-token", "请切换其他别名"),
("无法确定模型 private-model 的 CPA 配置来源,无法创建 Claude Desktop 别名 secret-token", "有效接入来源"),
("更新后的内核配置与预期值不一致(路径: secret-token),已拒绝写入", "格式兼容性校验失败"),
("验证更新后的内核配置失败: secret-token", "原配置未写入"),
("解析内核 YAML 配置失败: secret-token", "YAML 配置格式无效"),
("管理 API 错误 (401): secret-token", "认证失败"),
("管理 API 错误 (403): secret-token", "认证失败"),
(
"Already used by another model: claude-sonnet-5 secret-token",
"Choose a different alias",
),
(
"Unable to determine model private-model CPA configuration source; cannot create Claude Desktop alias secret-token",
"valid access source",
),
(
"Updated kernel configuration does not match the expected value (path: secret-token); write rejected",
"format compatibility check failed",
),
(
"Failed to validate updated kernel configuration: secret-token",
"original configuration was not written",
),
(
"Failed to parse kernel YAML configuration: secret-token",
"Invalid kernel YAML configuration format",
),
("Management API error (401): secret-token", "authentication failed"),
("Management API error (403): secret-token", "authentication failed"),
] {
let rendered = agent_core_error(detail.into());
assert!(rendered.contains(expected), "{rendered}");
+34 -34
View File
@@ -31,7 +31,7 @@ fn integration_managed_paths<'a>(client: &str, paths: &'a [PathBuf]) -> &'a [Pat
fn integration_state_path(client: &str, paths: &[PathBuf]) -> Result<PathBuf, String> {
let paths = integration_managed_paths(client, paths);
let identity = sha256_bytes(
&serde_json::to_vec(paths).map_err(|_| "生成接入配置标识失败".to_string())?,
&serde_json::to_vec(paths).map_err(|_| "Failed to generate access configuration marker".to_string())?,
);
Ok(agent_data_directory(paths)?
.join("agents")
@@ -49,7 +49,7 @@ fn parse_integration_state(
return Ok(None);
};
let state: AgentIntegrationState = serde_json::from_slice(bytes)
.map_err(|_| "接入配置合并状态损坏,请先手动备份配置".to_string())?;
.map_err(|_| "Access configuration merge state is corrupted. Back up the configuration manually first".to_string())?;
let paths = integration_managed_paths(client, paths);
let valid_images = |images: &Images| {
images.len() == paths.len()
@@ -64,13 +64,13 @@ fn parse_integration_state(
|| !valid_images(&state.original)
|| !valid_images(&state.applied)
{
return Err("接入配置合并状态与当前客户端不匹配,请先手动备份配置".into());
return Err("Access configuration merge state does not match the current client. Back up the configuration manually first".into());
}
Ok(Some(state))
}
fn integration_state_bytes(state: &AgentIntegrationState) -> Result<Vec<u8>, String> {
serde_json::to_vec(state).map_err(|_| "生成接入配置合并状态失败".to_string())
serde_json::to_vec(state).map_err(|_| "Failed to generate access configuration merge state".to_string())
}
fn merge_user_value(
@@ -126,18 +126,18 @@ fn merge_user_image(
return Ok(None);
};
let current_text = std::str::from_utf8(current)
.map_err(|_| format!("配置不是 UTF-8 文本: {}", path_to_string(path)))?;
.map_err(|_| format!("Configuration is not UTF-8 text: {}", path_to_string(path)))?;
let applied_value = parse(
path,
applied
.map(|bytes| std::str::from_utf8(bytes).map_err(|_| "接入状态不是 UTF-8 文本"))
.map(|bytes| std::str::from_utf8(bytes).map_err(|_| "Access state is not UTF-8 text"))
.transpose()?,
)?;
let current_value = parse(path, Some(current_text))?;
let baseline_value = parse(
path,
baseline
.map(|bytes| std::str::from_utf8(bytes).map_err(|_| "原始状态不是 UTF-8 文本"))
.map(|bytes| std::str::from_utf8(bytes).map_err(|_| "Original state is not UTF-8 text"))
.transpose()?,
)?;
let mut merged = merge_user_value(
@@ -240,7 +240,7 @@ pub(crate) fn config_paths(client: &str, home: &Path) -> Result<Vec<PathBuf>, St
}
let client = AgentClient::parse(client)?;
if !client.supported_platform() {
return Err("当前平台不支持此智能体配置".into());
return Err("The current platform does not support this agent configuration".into());
}
Ok(expected_agent_record_paths(
client,
@@ -254,7 +254,7 @@ pub(crate) fn validate_config_path(path: &Path) -> Result<(), String> {
.components()
.any(|c| matches!(c, std::path::Component::ParentDir))
{
return Err("配置路径必须是绝对路径且不能越界".into());
return Err("Configuration path must be absolute and remain within bounds".into());
}
for ancestor in path.ancestors() {
match fs::symlink_metadata(ancestor) {
@@ -267,16 +267,16 @@ pub(crate) fn validate_config_path(path: &Path) -> Result<(), String> {
};
if linked {
return Err(format!(
"配置路径不能包含符号链接: {}",
"Configuration path cannot contain symbolic links: {}",
path_to_string(ancestor)
));
}
if ancestor == path && !meta.is_file() {
return Err("配置路径不是普通文件".into());
return Err("Configuration path is not a regular file".into());
}
}
Err(e) if e.kind() == io::ErrorKind::NotFound => {}
Err(_) => return Err("无法检查配置路径".into()),
Err(_) => return Err("Unable to check configuration path".into()),
}
}
Ok(())
@@ -321,10 +321,10 @@ pub(crate) fn write_config_images(client: &str, images: &Images) -> Result<(), S
write_agent_configuration_file(AgentClient::parse(client)?, path, bytes)?;
}
} else {
fs::remove_file(path).map_err(|_| "删除配置文件失败".to_string())?;
fs::remove_file(path).map_err(|_| "Failed to delete configuration file".to_string())?;
}
if read_agent_bytes(path)? != *bytes {
return Err("配置写后校验失败".into());
return Err("Configuration post-write verification failed".into());
}
}
Ok(())
@@ -401,10 +401,10 @@ fn commit_config_transaction(
.zip(paths)
.any(|((p, _), expected)| p != expected)
{
return Err("配置更新路径不匹配".into());
return Err("Configuration update path mismatch".into());
}
if config_images(paths)? != *before {
return Err("配置已被其他程序修改,请刷新后重试".into());
return Err("Configuration was modified by another program. Refresh and try again".into());
}
let mut previous = before.clone();
let mut target = after.clone();
@@ -500,14 +500,14 @@ fn commit_config_transaction(
}
let all_paths = previous.iter().map(|(p, _)| p.clone()).collect::<Vec<_>>();
if config_images(&all_paths)? != previous {
return Err("配置已被其他程序修改,请刷新后重试".into());
return Err("Configuration was modified by another program. Refresh and try again".into());
}
let mut attempted = Vec::new();
let result = (if checked {
(|| {
for (index, ((path, old), (_, next))) in previous.iter().zip(&target).enumerate() {
if read_agent_bytes(path)? != *old {
return Err("配置已被其他程序修改".into());
return Err("Configuration was modified by another program".into());
}
if old == next {
continue;
@@ -525,7 +525,7 @@ fn commit_config_transaction(
if config_images(&all_paths)? == target {
Ok(())
} else {
Err("配置写后校验失败".into())
Err("Configuration post-write verification failed".into())
}
});
if result.is_err() {
@@ -540,7 +540,7 @@ fn commit_config_transaction(
failed |= writer(client, &vec![previous[index].clone()]).is_err();
}
if failed {
Err("回滚失败".into())
Err("Rollback failed".into())
} else {
Ok(())
}
@@ -551,13 +551,13 @@ fn commit_config_transaction(
if config_images(&all_paths)? == previous {
Ok(())
} else {
Err("回滚校验失败".into())
Err("Rollback verification failed".into())
}
});
return Err(if rollback.is_ok() {
"配置写入失败,已回滚本次修改"
"Configuration write failed; this change was rolled back"
} else {
"配置写入失败且回滚失败,请检查配置并使用手动备份恢复或基础配置模板修复"
"Configuration write and rollback both failed. Check the configuration and restore it from a manual backup or repair it with the base configuration template"
}
.into());
}
@@ -581,7 +581,7 @@ pub(crate) fn validate_client_config_images(client: &str, images: &Images) -> Re
}
for (path, bytes) in images {
if !parse(path, text(bytes.as_deref())?)?.is_object() {
return Err("配置根节点必须是对象".into());
return Err("Configuration root must be an object".into());
}
}
if matches!(client, "opencode" | "openclaw") {
@@ -597,7 +597,7 @@ pub(crate) fn validate_client_config_images(client: &str, images: &Images) -> Re
if let Some(content) = text(bytes.as_deref())? {
serde_json::from_str::<Value>(content.strip_prefix('\u{feff}').unwrap_or(content))
.map_err(|_| {
"配置 JSON 格式错误,请使用手动备份恢复或基础配置模板修复".to_string()
"Invalid configuration JSON. Restore it from a manual backup or repair it with the base configuration template".to_string()
})?;
}
}
@@ -619,7 +619,7 @@ pub(crate) fn prepare_config_updates(
let entry = after
.iter_mut()
.find(|(path, _)| path == &update.path)
.ok_or("配置更新路径不匹配")?;
.ok_or("Configuration update path mismatch")?;
let mut rendered = update.after.clone();
let mut generated = parse(&update.path, Some(&rendered))?;
if !template {
@@ -653,7 +653,7 @@ pub(crate) fn prepare_config_updates(
.iter()
.any(|p| updates.iter().filter(|u| &u.path == p).count() != 1))
{
return Err("基础配置模板必须覆盖整组文件".into());
return Err("Base configuration template must cover the entire file group".into());
}
validate_client_config_images(client, &after)?;
Ok(after)
@@ -809,7 +809,7 @@ pub(crate) fn config_package_operation(
) -> Result<(), String> {
let _guard = AGENT_CONFIG_FILE_LOCK
.lock()
.map_err(|_| "配置文件锁已损坏")?;
.map_err(|_| "Configuration file lock is poisoned")?;
let paths = config_paths(PI_AGENT_ID, home)?;
let before = config_images(&paths)?;
validate_config_images(&before)?;
@@ -818,8 +818,8 @@ pub(crate) fn config_package_operation(
.is_err()
{
return match write_config_images(PI_AGENT_ID, &before) {
Ok(()) => Err("插件操作失败,已回滚本次配置修改".into()),
Err(_) => Err("插件操作失败且配置回滚失败,请检查配置".into()),
Ok(()) => Err("Plugin operation failed; this configuration change was rolled back".into()),
Err(_) => Err("Plugin operation and configuration rollback both failed. Check the configuration".into()),
};
}
Ok(())
@@ -894,7 +894,7 @@ fn validate_unmanaged_preserved(
};
if project(before)? != project(after)? {
return Err(format!(
"更新意外改变了自定义配置,已拒绝写入: {}",
"The update unexpectedly changed custom configuration and was not written: {}",
path_to_string(path)
));
}
@@ -903,7 +903,7 @@ fn validate_unmanaged_preserved(
pub(crate) fn text(bytes: Option<&[u8]>) -> Result<Option<&str>, String> {
bytes
.map(|bytes| std::str::from_utf8(bytes).map_err(|_| "配置不是 UTF-8 文本".to_string()))
.map(|bytes| std::str::from_utf8(bytes).map_err(|_| "Configuration is not UTF-8 text".to_string()))
.transpose()
}
@@ -921,13 +921,13 @@ pub(crate) fn parse(path: &Path, content: Option<&str>) -> Result<Value, String>
};
let value = result.map_err(|_| {
format!(
"{} 配置格式错误,请使用手动备份恢复或基础配置模板修复",
"Invalid {} configuration format. Restore it from a manual backup or repair it with the base configuration template",
path_to_string(path)
)
})?;
// WorkBuddy also saves models.json as a top-level array.
if !value.is_object() && !(path.file_name().is_some_and(|n| n == "models.json") && value.is_array()) {
return Err(format!("{} 配置根节点必须是对象", path_to_string(path)));
return Err(format!("{} configuration root must be an object", path_to_string(path)));
}
Ok(value)
}
+1 -1
View File
@@ -823,7 +823,7 @@ pub(crate) fn stop_windows_matching_processes(
.collect::<Vec<_>>()
.join(", ");
return Err(format!(
"{label} 未能完全关闭; remaining process IDs: {ids}"
"{label} did not close completely; remaining process IDs: {ids}"
));
}
for process_id in remaining {
+13 -13
View File
@@ -112,7 +112,7 @@ pub(crate) fn read_workbuddy_app_version(executable: &Path) -> Option<String> {
pub(crate) fn parse_workbuddy_config(content: Option<&str>) -> Result<Value, String> {
let value = match content {
Some(content) => serde_json::from_str(content.strip_prefix('\u{feff}').unwrap_or(content))
.map_err(|_| "WorkBuddy models.json 格式无效,请使用手动备份恢复或基础配置模板修复")?,
.map_err(|_| "Invalid WorkBuddy models.json format. Restore it from a manual backup or repair it with the base configuration template")?,
None => json!({}),
};
validate_workbuddy_config(&value)?;
@@ -127,14 +127,14 @@ pub(crate) fn validate_workbuddy_config(value: &Value) -> Result<(), String> {
v.as_array()
.is_none_or(|a| a.iter().any(|v| !v.is_string()))
}) {
return Err("WorkBuddy availableModels 必须是字符串数组".into());
return Err("WorkBuddy availableModels must be an array of strings".into());
}
match root.get("models") {
None => None,
Some(v) => Some(v.as_array().ok_or("WorkBuddy models 必须是数组")?),
Some(v) => Some(v.as_array().ok_or("WorkBuddy models must be an array")?),
}
} else {
return Err("WorkBuddy models.json 根节点必须是对象或数组".into());
return Err("WorkBuddy models.json root must be an object or array".into());
};
let mut ids = std::collections::HashSet::new();
for entry in entries.into_iter().flatten() {
@@ -142,9 +142,9 @@ pub(crate) fn validate_workbuddy_config(value: &Value) -> Result<(), String> {
.get("id")
.and_then(Value::as_str)
.filter(|id| !id.trim().is_empty())
.ok_or("WorkBuddy 模型缺少有效 id")?;
.ok_or("WorkBuddy model is missing a valid id")?;
if !ids.insert(workbuddy_request_model(id)) {
return Err("WorkBuddy 存在重复模型 id,请先修复配置".into());
return Err("WorkBuddy contains duplicate model IDs. Fix the configuration first".into());
}
}
Ok(())
@@ -195,7 +195,7 @@ pub(crate) fn build_workbuddy_agent_config(
.is_some_and(|id| workbuddy_request_model(id) == model)
}) {
return Err(
"WorkBuddy 已有同名自定义模型,请先在 WorkBuddy 中修改该模型 ID,或选择其他 CPA 模型"
"WorkBuddy already has a custom model with the same name. Change that model ID in WorkBuddy or select another CPA model"
.into(),
);
}
@@ -217,7 +217,7 @@ pub(crate) fn build_workbuddy_agent_config(
.iter()
.filter(|m| workbuddy_managed_model(m))
{
let id = previous["id"].as_str().ok_or("WorkBuddy 模型缺少有效 id")?;
let id = previous["id"].as_str().ok_or("WorkBuddy model is missing a valid id")?;
let previous_model = workbuddy_request_model(id);
if previous_model == model {
continue;
@@ -245,7 +245,7 @@ pub(crate) fn build_workbuddy_agent_config(
visible.push(json!(model));
}
}
serde_json::to_string_pretty(&root).map_err(|_| "生成 WorkBuddy 配置失败".into())
serde_json::to_string_pretty(&root).map_err(|_| "Failed to generate WorkBuddy configuration".into())
}
fn update_workbuddy_model(
@@ -415,7 +415,7 @@ pub(crate) fn validate_workbuddy_unmanaged_preserved(
value
};
if project(before) != project(after) {
return Err("更新意外改变了 WorkBuddy 自定义配置,已拒绝写入".into());
return Err("The update unexpectedly changed WorkBuddy custom configuration and was not written".into());
}
Ok(())
}
@@ -449,11 +449,11 @@ pub(crate) fn build_restored_workbuddy_config(
}
serde_json::to_string_pretty(&root)
.map(Some)
.map_err(|_| "恢复 WorkBuddy 配置失败".into())
.map_err(|_| "Failed to restore WorkBuddy configuration".into())
}
pub(crate) fn prepare_workbuddy_managed_removal(paths: &[PathBuf]) -> Result<Images, String> {
let path = paths.first().ok_or("WorkBuddy 配置路径不可用")?;
let path = paths.first().ok_or("WorkBuddy configuration path is unavailable")?;
let Some(current) = read_optional_text(path)? else {
return Ok(Vec::new());
};
@@ -467,7 +467,7 @@ pub(crate) fn prepare_workbuddy_managed_removal(paths: &[PathBuf]) -> Result<Ima
{
None
} else {
Some(serde_json::to_vec_pretty(&value).map_err(|_| "生成 WorkBuddy 配置失败")?)
Some(serde_json::to_vec_pretty(&value).map_err(|_| "Failed to generate WorkBuddy configuration")?)
};
Ok(vec![(path.clone(), bytes)])
}
+7 -7
View File
@@ -20,7 +20,7 @@ pub(crate) async fn get_core_status(app: tauri::AppHandle) -> Result<CoreStatus,
)
})
.await
.map_err(|error| format!("内核状态后台任务失败: {error}"))?
.map_err(|error| format!("Kernel status background task failed: {error}"))?
}
pub(crate) fn emit_core_status(app: &tauri::AppHandle, status: &CoreStatus) {
@@ -580,10 +580,10 @@ pub(crate) fn resolve_windows_close_request(
WindowsCloseAction::MinimizeToTray => {
let window = app
.get_webview_window("main")
.ok_or_else(|| "主窗口不存在".to_string())?;
.ok_or_else(|| "Main window does not exist".to_string())?;
window
.hide()
.map_err(|error| format!("隐藏主窗口失败: {error}"))?;
.map_err(|error| format!("Failed to hide main window: {error}"))?;
}
}
@@ -593,7 +593,7 @@ pub(crate) fn resolve_windows_close_request(
pub(crate) fn app_autostart_enabled(app: &tauri::AppHandle) -> Result<bool, String> {
app.autolaunch()
.is_enabled()
.map_err(|error| format!("读取系统开机自启状态失败: {error}"))
.map_err(|error| format!("Failed to read system startup status: {error}"))
}
pub(crate) fn set_app_autostart_enabled(
@@ -604,11 +604,11 @@ pub(crate) fn set_app_autostart_enabled(
if enabled {
manager
.enable()
.map_err(|error| format!("启用开机自启失败: {error}"))
.map_err(|error| format!("Failed to enable launch at startup: {error}"))
} else {
manager
.disable()
.map_err(|error| format!("关闭开机自启失败: {error}"))
.map_err(|error| format!("Failed to disable launch at startup: {error}"))
}
}
@@ -670,7 +670,7 @@ pub(crate) fn save_software_settings(
.flatten();
return Err(match rollback_error {
Some(rollback_error) => {
format!("{error}; 回滚开机自启设置也失败: {rollback_error}")
format!("{error}; failed to roll back startup settings: {rollback_error}")
}
None => error,
});
File diff suppressed because it is too large. Load diff
+8 -8
View File
@@ -52,22 +52,22 @@ fn catalog_state() -> Result<&'static ClaudeCatalog, String> {
fn parse_catalog(content: &str) -> Result<ClaudeCatalog, String> {
let root: Value = serde_json::from_str(content)
.map_err(|error| format!("解析内置 Claude model-catalog.json 失败: {error}"))?;
.map_err(|error| format!("Failed to parse bundled Claude model-catalog.json: {error}"))?;
let root = root
.as_object()
.ok_or_else(|| "内置 Claude model-catalog.json 根节点必须是对象".to_string())?;
.ok_or_else(|| "Bundled Claude model-catalog.json root must be an object".to_string())?;
let fallback_context_window = root
.get("fallback_model")
.and_then(Value::as_object)
.and_then(|model| model.get("context_window"))
.and_then(positive_u64)
.ok_or_else(|| {
"内置 Claude model-catalog.json 缺少有效的 fallback_model.context_window".to_string()
"Bundled Claude model-catalog.json is missing a valid fallback_model.context_window".to_string()
})?;
let models = root
.get("models")
.and_then(Value::as_array)
.ok_or_else(|| "内置 Claude model-catalog.json 必须包含 models 数组".to_string())?;
.ok_or_else(|| "Bundled Claude model-catalog.json must contain a models array".to_string())?;
let mut context_windows = HashMap::with_capacity(models.len());
let mut display_names = HashMap::with_capacity(models.len());
let mut claude_code_effort_levels = HashMap::with_capacity(models.len());
@@ -75,7 +75,7 @@ fn parse_catalog(content: &str) -> Result<ClaudeCatalog, String> {
for (index, model) in models.iter().enumerate() {
let model = model.as_object().ok_or_else(|| {
format!(
"内置 Claude model-catalog.json 第 {} 个模型必须是对象",
"Model {} in bundled Claude model-catalog.json must be an object",
index + 1
)
})?;
@@ -86,14 +86,14 @@ fn parse_catalog(content: &str) -> Result<ClaudeCatalog, String> {
.filter(|value| !value.is_empty())
.ok_or_else(|| {
format!(
"内置 Claude model-catalog.json 第 {} 个模型的 slug 不能为空",
"The slug of model {} in bundled Claude model-catalog.json cannot be empty",
index + 1
)
})?;
let key = normalize_id(slug);
if !model_ids.insert(key.clone()) {
return Err(format!(
"内置 Claude model-catalog.json 模型 slug 大小写重复: {slug}"
"Bundled Claude model-catalog.json contains duplicate model slugs differing only by case: {slug}"
));
}
if let Some(context_window) = model.get("context_window").and_then(positive_u64) {
@@ -194,7 +194,7 @@ mod tests {
}"#,
)
.unwrap_err();
assert!(error.contains("slug 大小写重复"));
assert!(error.contains("duplicate model slugs differing only by case"));
}
#[test]
+33 -33
View File
@@ -104,7 +104,7 @@ pub(crate) fn activate_catalog_json(catalog_json: &str) -> Result<bool, String>
let parsed = parse_sources(catalog_json)?;
let mut state = catalog_state()?
.write()
.map_err(|_| "Codex 模型目录内存锁已损坏".to_string())?;
.map_err(|_| "Codex model catalog memory lock is poisoned".to_string())?;
Ok(state.activate(catalog_json, parsed))
}
@@ -115,14 +115,14 @@ pub(crate) fn validate_catalog_json(catalog_json: &str) -> Result<u64, String> {
pub(crate) fn current_catalog_revision() -> Result<u64, String> {
let state = catalog_state()?
.read()
.map_err(|_| "Codex 模型目录内存锁已损坏".to_string())?;
.map_err(|_| "Codex model catalog memory lock is poisoned".to_string())?;
Ok(state.sources.revision)
}
pub(crate) fn current_catalog_json() -> Result<String, String> {
let state = catalog_state()?
.read()
.map_err(|_| "Codex 模型目录内存锁已损坏".to_string())?;
.map_err(|_| "Codex model catalog memory lock is poisoned".to_string())?;
Ok(state.json.clone())
}
@@ -132,7 +132,7 @@ pub(crate) fn parse_runtime_models(payload: &Value) -> Result<Vec<CodexRuntimeMo
.and_then(Value::as_array)
.or_else(|| payload.get("data").and_then(Value::as_array))
.or_else(|| payload.as_array())
.ok_or_else(|| "Codex 模型列表响应缺少 models 或 data 数组".to_string())?;
.ok_or_else(|| "Codex model list response is missing a models or data array".to_string())?;
let mut seen = HashSet::with_capacity(values.len());
let mut models = Vec::with_capacity(values.len());
@@ -209,7 +209,7 @@ pub(crate) fn parse_runtime_models(payload: &Value) -> Result<Vec<CodexRuntimeMo
}
}
if !values.is_empty() && models.is_empty() {
return Err("Codex 模型列表响应未包含有效的模型 ID".to_string());
return Err("Codex model list response contains no valid model ID".to_string());
}
Ok(models)
}
@@ -239,7 +239,7 @@ pub(crate) fn prepare_catalog(
) -> Result<PreparedCodexCatalog, String> {
let state = catalog_state()?
.read()
.map_err(|_| "Codex 模型目录内存锁已损坏".to_string())?;
.map_err(|_| "Codex model catalog memory lock is poisoned".to_string())?;
prepare_catalog_with_customizations(runtime_models, &state.sources, &state.customizations)
}
@@ -265,11 +265,11 @@ fn parse_sources(catalog_json: &str) -> Result<CatalogSources, String> {
fn parse_fallback_model(fallback_json: &str) -> Result<Map<String, Value>, String> {
let fallback: Value = serde_json::from_str(fallback_json)
.map_err(|error| format!("解析内置 fallback-model.json 失败: {error}"))?;
.map_err(|error| format!("Failed to parse bundled fallback-model.json: {error}"))?;
let fallback = fallback
.as_object()
.cloned()
.ok_or_else(|| "内置 fallback-model.json 根节点必须是对象".to_string())?;
.ok_or_else(|| "Bundled fallback-model.json root must be an object".to_string())?;
validate_model(&fallback, "fallback-model.json", false)?;
Ok(fallback)
}
@@ -279,15 +279,15 @@ fn parse_catalog_sources(
fallback: Map<String, Value>,
) -> Result<CatalogSources, String> {
let root: Value = serde_json::from_str(catalog_json)
.map_err(|error| format!("解析内置 model-catalog.json 失败: {error}"))?;
.map_err(|error| format!("Failed to parse bundled model-catalog.json: {error}"))?;
let root = root
.as_object()
.ok_or_else(|| "内置 model-catalog.json 根节点必须是对象".to_string())?;
.ok_or_else(|| "Bundled model-catalog.json root must be an object".to_string())?;
let revision = match root.get("catalog_revision") {
Some(value) => value
.as_u64()
.ok_or_else(|| "Codex 模型目录 catalog_revision 必须为非负整数".to_string())?,
.ok_or_else(|| "Codex model catalog catalog_revision must be a non-negative integer".to_string())?,
None => 0,
};
@@ -295,20 +295,20 @@ fn parse_catalog_sources(
.get("models")
.and_then(Value::as_array)
.filter(|models| !models.is_empty())
.ok_or_else(|| "内置 model-catalog.json 必须包含非空 models 数组".to_string())?;
.ok_or_else(|| "Bundled model-catalog.json must contain a non-empty models array".to_string())?;
let mut templates = HashMap::with_capacity(values.len());
let mut max_template_priority = 0;
for (order, value) in values.iter().enumerate() {
let model = value
.as_object()
.cloned()
.ok_or_else(|| format!("内置 model-catalog.json 第 {} 个模型必须是对象", order + 1))?;
validate_model(&model, &format!("第 {} 个正式模板", order + 1), true)?;
.ok_or_else(|| format!("Model {} in bundled model-catalog.json must be an object", order + 1))?;
validate_model(&model, &format!("Official template {}", order + 1), true)?;
let slug = string_value(&model, "slug");
let key = normalize_id(&slug);
if templates.contains_key(&key) {
return Err(format!(
"内置 model-catalog.json 模型 slug 大小写重复: {slug}"
"Bundled model-catalog.json contains duplicate model slugs differing only by case: {slug}"
));
}
max_template_priority = max_template_priority.max(priority_value(&model));
@@ -332,13 +332,13 @@ fn parse_catalog_sources(
#[cfg(test)]
fn parse_combined_sources_for_test(catalog_json: &str) -> Result<CatalogSources, String> {
let mut root: Value = serde_json::from_str(catalog_json)
.map_err(|error| format!("解析测试 model-catalog.json 失败: {error}"))?;
.map_err(|error| format!("Failed to parse test model-catalog.json: {error}"))?;
let fallback = root
.as_object_mut()
.and_then(|root| root.remove("fallback_model"))
.and_then(|fallback| fallback.as_object().cloned())
.ok_or_else(|| "测试 model-catalog.json 缺少 fallback_model 对象".to_string())?;
validate_model(&fallback, "测试 fallback_model", false)?;
.ok_or_else(|| "Test model-catalog.json is missing a fallback_model object".to_string())?;
validate_model(&fallback, "Test fallback_model", false)?;
parse_catalog_sources(&root.to_string(), fallback)
}
@@ -348,23 +348,23 @@ fn validate_model(
require_slug: bool,
) -> Result<(), String> {
if require_slug && string_value(model, "slug").is_empty() {
return Err(format!("内置 model-catalog.json {label} 的 slug 不能为空"));
return Err(format!("The slug of {label} in bundled model-catalog.json cannot be empty"));
}
if string_value(model, "base_instructions").is_empty() {
return Err(format!(
"内置 model-catalog.json {label} 的 base_instructions 不能为空"
"The base_instructions of {label} in bundled model-catalog.json cannot be empty"
));
}
validate_required_codex_fields(model, label)?;
let context_window = positive_u64_value(model.get("context_window"))
.ok_or_else(|| format!("内置 model-catalog.json {label} 的 context_window 必须为正数"))?;
.ok_or_else(|| format!("The context_window of {label} in bundled model-catalog.json must be positive"))?;
let max_context_window =
positive_u64_value(model.get("max_context_window")).ok_or_else(|| {
format!("内置 model-catalog.json {label} 的 max_context_window 必须为正数")
format!("The max_context_window of {label} in bundled model-catalog.json must be positive")
})?;
if max_context_window < context_window {
return Err(format!(
"内置 model-catalog.json {label} 的 max_context_window 不能小于 context_window"
"The max_context_window of {label} in bundled model-catalog.json cannot be less than context_window"
));
}
Ok(())
@@ -374,13 +374,13 @@ fn validate_required_codex_fields(model: &Map<String, Value>, label: &str) -> Re
for field in ["display_name", "shell_type", "visibility"] {
if string_value(model, field).is_empty() {
return Err(format!(
"Codex 模型 {label} 的必填字段 {field} 必须是非空字符串"
"Required field {field} of Codex model {label} must be a non-empty string"
));
}
}
for field in ["supported_reasoning_levels", "experimental_supported_tools"] {
if !model.get(field).is_some_and(Value::is_array) {
return Err(format!("Codex 模型 {label} 的必填字段 {field} 必须是数组"));
return Err(format!("Required field {field} of Codex model {label} must be an array"));
}
}
for field in [
@@ -390,7 +390,7 @@ fn validate_required_codex_fields(model: &Map<String, Value>, label: &str) -> Re
] {
if !model.get(field).is_some_and(Value::is_boolean) {
return Err(format!(
"Codex 模型 {label} 的必填字段 {field} 必须是布尔值"
"Required field {field} of Codex model {label} must be a boolean"
));
}
}
@@ -398,26 +398,26 @@ fn validate_required_codex_fields(model: &Map<String, Value>, label: &str) -> Re
.get("priority")
.is_some_and(|value| value.as_i64().is_some() || value.as_u64().is_some())
{
return Err(format!("Codex 模型 {label} 的必填字段 priority 必须是整数"));
return Err(format!("Required field priority of Codex model {label} must be an integer"));
}
if !model
.get("default_reasoning_summary")
.is_some_and(Value::is_string)
{
return Err(format!(
"Codex 模型 {label} 的必填字段 default_reasoning_summary 必须是字符串"
"Required field default_reasoning_summary of Codex model {label} must be a string"
));
}
let truncation = model
.get("truncation_policy")
.and_then(Value::as_object)
.ok_or_else(|| format!("Codex 模型 {label} 缺少 truncation_policy 对象"))?;
.ok_or_else(|| format!("Codex model {label} is missing a truncation_policy object"))?;
if string_value(truncation, "mode").is_empty()
|| !truncation
.get("limit")
.is_some_and(|value| value.as_i64().is_some() || value.as_u64().is_some())
{
return Err(format!("Codex 模型 {label} 的 truncation_policy 无效"));
return Err(format!("Invalid truncation_policy for Codex model {label}"));
}
Ok(())
}
@@ -436,7 +436,7 @@ fn prepare_catalog_with_customizations(
customizations: &customizations::ModelCustomizations,
) -> Result<PreparedCodexCatalog, String> {
if runtime_models.is_empty() {
return Err("CPA 当前没有可写入 Codex 的模型".to_string());
return Err("CPA currently has no models that can be written to Codex".to_string());
}
let mut entries = Vec::with_capacity(runtime_models.len());
@@ -471,7 +471,7 @@ fn prepare_catalog_with_customizations(
}
}
if entries.is_empty() {
return Err("CPA 当前没有有效的 Codex 模型 ID".to_string());
return Err("CPA currently has no valid Codex model ID".to_string());
}
for entry in &mut entries {
@@ -532,7 +532,7 @@ fn prepare_catalog_with_customizations(
.map(|entry| Value::Object(entry.value))
.collect::<Vec<_>>();
let mut json = serde_json::to_string_pretty(&serde_json::json!({ "models": values }))
.map_err(|error| format!("生成 Codex 模型目录失败: {error}"))?;
.map_err(|error| format!("Failed to generate Codex model catalog: {error}"))?;
json.push('\n');
Ok(PreparedCodexCatalog { models, json })
}
+32 -32
View File
@@ -80,7 +80,7 @@ fn editable_configuration(model: &Map<String, Value>) -> Map<String, Value> {
fn validate_configuration(model: &Map<String, Value>) -> Result<(), String> {
for field in model.keys() {
if !EDITABLE_FIELDS.contains(&field.as_str()) {
return Err(format!("不允许修改模型字段 {field}"));
return Err(format!("Model field {field} cannot be modified"));
}
}
for field in [
@@ -96,7 +96,7 @@ fn validate_configuration(model: &Map<String, Value>) -> Result<(), String> {
.as_u64()
.is_some_and(|value| (1..=9_007_199_254_740_991).contains(&value))
{
return Err(format!("{field} 必须是有效的正整数"));
return Err(format!("{field} must be a valid positive integer"));
}
}
}
@@ -105,14 +105,14 @@ fn validate_configuration(model: &Map<String, Value>) -> Result<(), String> {
model.get("max_context_window").and_then(Value::as_u64),
) {
if maximum < context {
return Err("最大上下文窗口不能小于上下文窗口".to_string());
return Err("Maximum context window cannot be smaller than the context window".to_string());
}
if model
.get("auto_compact_token_limit")
.and_then(Value::as_u64)
.is_some_and(|limit| limit > context)
{
return Err("自动压缩阈值不能大于上下文窗口".to_string());
return Err("Automatic compaction threshold cannot exceed the context window".to_string());
}
}
if let Some(value) = model.get("effective_context_window_percent") {
@@ -120,7 +120,7 @@ fn validate_configuration(model: &Map<String, Value>) -> Result<(), String> {
.as_u64()
.is_some_and(|value| (1..=100).contains(&value))
{
return Err("有效上下文比例必须为 1 到 100 的整数".to_string());
return Err("Effective context percentage must be an integer from 1 to 100".to_string());
}
}
for field in ["display_name", "description"] {
@@ -131,61 +131,61 @@ fn validate_configuration(model: &Map<String, Value>) -> Result<(), String> {
if !value.as_str().is_some_and(|text| {
text.len() <= 4_000 && (field == "description" || !text.trim().is_empty())
}) {
return Err(format!("{field} 必须是有效的文本"));
return Err(format!("{field} must be valid text"));
}
}
}
if let Some(value) = model.get("visibility") {
if !matches!(value.as_str(), Some("list" | "hide" | "none")) {
return Err("模型显示状态无效".to_string());
return Err("Invalid model display status".to_string());
}
}
if let Some(value) = model.get("supports_parallel_tool_calls") {
if !value.is_boolean() {
return Err("并行工具调用必须为布尔值".to_string());
return Err("Parallel tool calls must be a boolean".to_string());
}
}
if let Some(value) = model.get("input_modalities") {
let modalities = value.as_array().ok_or("输入类型必须为数组")?;
let modalities = value.as_array().ok_or("Input types must be an array")?;
let mut seen = HashSet::new();
if modalities.is_empty()
|| modalities.iter().any(|value| {
!matches!(value.as_str(), Some("text" | "image")) || !seen.insert(value.as_str())
})
{
return Err("输入类型仅允许不重复的 text 和 image,且至少选择一项".to_string());
return Err("Input types may contain only unique text and image values, with at least one selected".to_string());
}
}
let default = model.get("default_reasoning_level");
if let Some(value) = default {
if !value.is_null() && !value.as_str().is_some_and(is_allowed_reasoning_level) {
return Err("默认思考等级无效".to_string());
return Err("Invalid default reasoning level".to_string());
}
}
if let Some(value) = model.get("supported_reasoning_levels") {
let levels = value.as_array().ok_or("思考等级必须为数组")?;
let levels = value.as_array().ok_or("Reasoning levels must be an array")?;
let mut seen = HashSet::new();
for level in levels {
let effort = level
.get("effort")
.and_then(Value::as_str)
.ok_or("思考等级缺少 effort")?;
.ok_or("Reasoning level is missing effort")?;
if !is_allowed_reasoning_level(effort) || !seen.insert(effort) {
return Err(format!("思考等级无效或重复: {effort}"));
return Err(format!("Invalid or duplicate reasoning level: {effort}"));
}
if !level
.get("description")
.and_then(Value::as_str)
.is_some_and(|text| text.len() <= 4_000)
{
return Err("思考等级说明必须是有效的文本".to_string());
return Err("Reasoning level description must be valid text".to_string());
}
}
if default
.and_then(Value::as_str)
.is_some_and(|effort| !seen.contains(effort))
{
return Err("默认思考等级必须包含在可选等级中".to_string());
return Err("Default reasoning level must be included in the available levels".to_string());
}
}
Ok(())
@@ -199,7 +199,7 @@ pub(super) fn apply_customizations(
if let Some(customization) = customizations.get(&normalize_id(&slug)) {
model.extend(customization.clone());
validate_configuration(&editable_configuration(model))
.map_err(|error| format!("模型 {slug} 的自定义配置无效: {error}"))?;
.map_err(|error| format!("Invalid custom configuration for model {slug}: {error}"))?;
enable_fast_mode(model);
}
Ok(())
@@ -219,9 +219,9 @@ pub(super) fn snapshot_for_state(
let mut models = Vec::new();
for value in root["models"]
.as_array()
.ok_or("模型目录缺少 models 数组")?
.ok_or("Model catalog is missing a models array")?
{
let mut model = value.as_object().cloned().ok_or("模型目录条目必须为对象")?;
let mut model = value.as_object().cloned().ok_or("Model catalog entry must be an object")?;
let slug = string_value(&model, "slug");
let key = normalize_id(&slug);
let defaults = editable_configuration(&model);
@@ -252,7 +252,7 @@ pub(crate) fn editor_snapshot(
) -> Result<CatalogEditorSnapshot, String> {
let state = catalog_state()?
.read()
.map_err(|_| "Codex 模型目录内存锁已损坏")?;
.map_err(|_| "Codex model catalog memory lock is poisoned")?;
snapshot_for_state(runtime_models, &state)
}
@@ -264,7 +264,7 @@ fn customizations_from_request(
return Err("CODEX_MODEL_CATALOG_CHANGED".to_string());
}
if request.models.len() != snapshot.models.len() {
return Err("模型列表已变化,请重新加载后编辑".to_string());
return Err("Model list has changed. Reload it before editing".to_string());
}
let mut seen = HashSet::new();
let mut customizations = BTreeMap::new();
@@ -274,15 +274,15 @@ fn customizations_from_request(
.models
.iter()
.find(|model| normalize_id(&model.slug) == key)
.ok_or_else(|| format!("模型 {} 已不在当前列表中", requested.slug))?;
.ok_or_else(|| format!("Model {} is no longer in the current list", requested.slug))?;
if !seen.insert(key.clone()) {
return Err(format!("模型 {} 重复", requested.slug));
return Err(format!("Duplicate model {}", requested.slug));
}
if requested.configuration.len() != EDITABLE_FIELDS.len() {
return Err(format!("模型 {} 的配置字段不完整", requested.slug));
return Err(format!("Model {} has incomplete configuration fields", requested.slug));
}
validate_configuration(&requested.configuration)
.map_err(|error| format!("模型 {}: {error}", requested.slug))?;
.map_err(|error| format!("Model {}: {error}", requested.slug))?;
let mut changes: Map<String, Value> = requested
.configuration
.iter()
@@ -313,18 +313,18 @@ fn customizations_from_request(
fn decode_customizations(content: &[u8]) -> Result<ModelCustomizations, String> {
if content.len() > crate::MAX_CODEX_MODEL_CATALOG_BYTES {
return Err("Codex 自定义模型配置超过大小限制".to_string());
return Err("Codex custom model configuration exceeds the size limit".to_string());
}
let saved: SavedCustomizations =
serde_json::from_slice(content).map_err(|error| error.to_string())?;
if saved.version != 1 {
return Err("不支持的 Codex 自定义模型配置版本".to_string());
return Err("Unsupported Codex custom model configuration version".to_string());
}
let mut normalized = BTreeMap::new();
for (slug, model) in saved.models {
let key = normalize_id(&slug);
if key.is_empty() || normalized.contains_key(&key) {
return Err("自定义模型 ID 为空或重复".to_string());
return Err("Custom model ID is empty or duplicated".to_string());
}
validate_configuration(&model)?;
normalized.insert(key, model);
@@ -336,12 +336,12 @@ pub(crate) fn load_customizations(path: &Path) -> Result<(), String> {
let content = match std::fs::read(path) {
Ok(content) => content,
Err(error) if error.kind() == std::io::ErrorKind::NotFound => return Ok(()),
Err(error) => return Err(format!("读取 Codex 自定义模型配置失败: {error}")),
Err(error) => return Err(format!("Failed to read Codex custom model configuration: {error}")),
};
let customizations = decode_customizations(&content)?;
let mut state = catalog_state()?
.write()
.map_err(|_| "Codex 模型目录内存锁已损坏")?;
.map_err(|_| "Codex model catalog memory lock is poisoned")?;
state.customizations = customizations;
Ok(())
}
@@ -360,7 +360,7 @@ fn save_for_state(
};
let content = serde_json::to_vec_pretty(&saved).map_err(|error| error.to_string())?;
if content.len() > crate::MAX_CODEX_MODEL_CATALOG_BYTES {
return Err("Codex 自定义模型配置超过大小限制".to_string());
return Err("Codex custom model configuration exceeds the size limit".to_string());
}
crate::write_bytes_atomically(path, &content)?;
state.customizations = saved.models;
@@ -374,7 +374,7 @@ pub(crate) fn save_customizations(
) -> Result<CatalogEditorSnapshot, String> {
let mut state = catalog_state()?
.write()
.map_err(|_| "Codex 模型目录内存锁已损坏")?;
.map_err(|_| "Codex model catalog memory lock is poisoned")?;
save_for_state(path, runtime_models, request, &mut state)
}
@@ -5,10 +5,10 @@ pub(crate) fn apply_configured_context_limits(
content: &str,
) -> Result<(), String> {
let document: serde_norway::Value = serde_norway::from_str(content)
.map_err(|error| format!("解析内核模型上下文配置失败: {error}"))?;
.map_err(|error| format!("Failed to parse kernel model context configuration: {error}"))?;
let root = document
.as_mapping()
.ok_or("内核配置顶层必须是 YAML 映射")?;
.ok_or("Kernel configuration root must be a YAML mapping")?;
let mut limits: HashMap<String, u64> = HashMap::new();
for section in crate::MODEL_ALIAS_CONFIG_SECTIONS {
let Some(providers) =
+107 -107
View File
@@ -197,7 +197,7 @@ pub(crate) async fn list_codex_sessions(
let user_home = app
.path()
.home_dir()
.map_err(|error| format!("无法获取用户目录: {error}"))?;
.map_err(|error| format!("Failed to get user directory: {error}"))?;
let codex_home = resolve_codex_home(&user_home);
let request = request.unwrap_or(ListCodexSessionsRequest {
offset: 0,
@@ -207,7 +207,7 @@ pub(crate) async fn list_codex_sessions(
list_codex_sessions_from_home(&codex_home, request.offset, request.limit)
})
.await
.map_err(|error| format!("读取 Codex 会话任务失败: {error}"))?
.map_err(|error| format!("Failed to read Codex session task: {error}"))?
}
#[tauri::command]
@@ -218,13 +218,13 @@ pub(crate) async fn delete_codex_sessions(
let user_home = app
.path()
.home_dir()
.map_err(|error| format!("无法获取用户目录: {error}"))?;
.map_err(|error| format!("Failed to get user directory: {error}"))?;
let codex_home = resolve_codex_home(&user_home);
tauri::async_runtime::spawn_blocking(move || {
delete_codex_sessions_from_home(&codex_home, request.session_ids)
})
.await
.map_err(|error| format!("删除 Codex 会话任务失败: {error}"))?
.map_err(|error| format!("Failed to delete Codex session task: {error}"))?
}
#[tauri::command]
@@ -234,7 +234,7 @@ pub(crate) async fn repair_codex_session_metadata(
let user_home = app
.path()
.home_dir()
.map_err(|error| format!("无法获取用户目录: {error}"))?;
.map_err(|error| format!("Failed to get user directory: {error}"))?;
let codex_home = resolve_codex_home(&user_home);
let progress_app = app.clone();
tauri::async_runtime::spawn_blocking(move || {
@@ -244,7 +244,7 @@ pub(crate) async fn repair_codex_session_metadata(
})
})
.await
.map_err(|error| format!("恢复 Codex 历史会话任务失败: {error}"))?
.map_err(|error| format!("Failed to restore Codex historical session task: {error}"))?
}
#[tauri::command]
@@ -254,13 +254,13 @@ pub(crate) async fn preview_codex_session_index_cleanup(
let user_home = app
.path()
.home_dir()
.map_err(|error| format!("无法获取用户目录: {error}"))?;
.map_err(|error| format!("Failed to get user directory: {error}"))?;
let codex_home = resolve_codex_home(&user_home);
tauri::async_runtime::spawn_blocking(move || {
preview_session_index_cleanup_from_home(&codex_home)
})
.await
.map_err(|error| format!("预览 Codex 会话索引任务失败: {error}"))?
.map_err(|error| format!("Failed to preview Codex session index task: {error}"))?
}
#[tauri::command]
@@ -271,7 +271,7 @@ pub(crate) async fn apply_codex_session_index_cleanup(
let user_home = app
.path()
.home_dir()
.map_err(|error| format!("无法获取用户目录: {error}"))?;
.map_err(|error| format!("Failed to get user directory: {error}"))?;
let codex_home = resolve_codex_home(&user_home);
tauri::async_runtime::spawn_blocking(move || {
apply_session_index_cleanup_from_home(
@@ -282,7 +282,7 @@ pub(crate) async fn apply_codex_session_index_cleanup(
)
})
.await
.map_err(|error| format!("清理 Codex 会话索引任务失败: {error}"))?
.map_err(|error| format!("Failed to clean up Codex session index task: {error}"))?
}
fn resolve_codex_home(user_home: &Path) -> PathBuf {
@@ -303,10 +303,10 @@ fn open_read_only(path: &Path) -> Result<Connection, String> {
path,
OpenFlags::SQLITE_OPEN_READ_ONLY | OpenFlags::SQLITE_OPEN_NO_MUTEX,
)
.map_err(|error| format!("打开数据库失败 {}: {error}", path.display()))?;
.map_err(|error| format!("Failed to open database {}: {error}", path.display()))?;
connection
.busy_timeout(SQLITE_BUSY_TIMEOUT)
.map_err(|error| format!("设置数据库等待时间失败 {}: {error}", path.display()))?;
.map_err(|error| format!("Failed to set database timeout {}: {error}", path.display()))?;
Ok(connection)
}
@@ -315,10 +315,10 @@ fn open_read_write(path: &Path) -> Result<Connection, String> {
path,
OpenFlags::SQLITE_OPEN_READ_WRITE | OpenFlags::SQLITE_OPEN_NO_MUTEX,
)
.map_err(|error| format!("打开数据库失败 {}: {error}", path.display()))?;
.map_err(|error| format!("Failed to open database {}: {error}", path.display()))?;
connection
.busy_timeout(SQLITE_BUSY_TIMEOUT)
.map_err(|error| format!("设置数据库等待时间失败 {}: {error}", path.display()))?;
.map_err(|error| format!("Failed to set database timeout {}: {error}", path.display()))?;
Ok(connection)
}
@@ -384,7 +384,7 @@ fn discover_database_paths(codex_home: &Path, reference_only: bool) -> (Vec<Path
Ok(true) => has_supported_table = true,
Ok(false) => {}
Err(error) => {
warnings.push(format!("检查数据库结构失败 {}: {error}", path.display()))
warnings.push(format!("Failed to inspect database schema {}: {error}", path.display()))
}
}
}
@@ -403,7 +403,7 @@ fn table_exists(connection: &Connection, table: &str) -> Result<bool, String> {
) {
Ok(()) => Ok(true),
Err(rusqlite::Error::QueryReturnedNoRows) => Ok(false),
Err(error) => Err(format!("读取 SQLite 表清单失败: {error}")),
Err(error) => Err(format!("Failed to read SQLite table list: {error}")),
}
}
@@ -416,12 +416,12 @@ fn table_columns(connection: &Connection, table: &str) -> Result<HashSet<String>
"PRAGMA table_info(\"{}\")",
table.replace('"', "\"\"")
))
.map_err(|error| format!("读取 {table} 表结构失败: {error}"))?;
.map_err(|error| format!("Failed to read {table} table schema: {error}"))?;
let rows = statement
.query_map([], |row| row.get::<_, String>(1))
.map_err(|error| format!("读取 {table} 表结构失败: {error}"))?;
.map_err(|error| format!("Failed to read {table} table schema: {error}"))?;
rows.collect::<rusqlite::Result<HashSet<_>>>()
.map_err(|error| format!("读取 {table} 表结构失败: {error}"))
.map_err(|error| format!("Failed to read {table} table schema: {error}"))
}
fn optional_column<'a>(columns: &HashSet<String>, column: &'a str, fallback: &'a str) -> &'a str {
@@ -556,7 +556,7 @@ fn list_sessions_from_rollouts(codex_home: &Path) -> (Vec<CodexSessionSummary>,
Ok(None) => {}
Err(error) if is_locked_error_message(&error) => {
warnings.push(format!(
"会话文件正在使用,已跳过 {}: {error}",
"Session file is in use; skipped {}: {error}",
path.display()
));
}
@@ -574,7 +574,7 @@ fn read_session_index_metadata(
return Ok(HashMap::new());
}
let content = fs::read_to_string(&path)
.map_err(|error| format!("读取 session_index.jsonl 失败: {error}"))?;
.map_err(|error| format!("Failed to read session_index.jsonl: {error}"))?;
let mut metadata = HashMap::new();
for line in content.lines() {
let Ok(record) = serde_json::from_str::<Value>(line) else {
@@ -617,7 +617,7 @@ fn summarize_rollout_session(
metadata: &HashMap<String, SessionIndexMetadata>,
) -> Result<Option<CodexSessionSummary>, String> {
let file = fs::File::open(path)
.map_err(|error| format!("读取 rollout 失败 {}: {error}", path.display()))?;
.map_err(|error| format!("Failed to read rollout {}: {error}", path.display()))?;
let mut id = None;
let mut title = String::new();
let mut cwd = String::new();
@@ -625,7 +625,7 @@ fn summarize_rollout_session(
let mut updated_at_ms = None;
for line in BufReader::new(file).lines() {
let line =
line.map_err(|error| format!("读取 rollout 失败 {}: {error}", path.display()))?;
line.map_err(|error| format!("Failed to read rollout {}: {error}", path.display()))?;
let Ok(record) = serde_json::from_str::<Value>(&line) else {
continue;
};
@@ -740,13 +740,13 @@ fn list_session_ids_from_database(path: &Path) -> Result<HashSet<String>, String
);
let mut statement = connection
.prepare(&sql)
.map_err(|error| format!("统计会话总数失败 {}: {error}", path.display()))?;
.map_err(|error| format!("Failed to count sessions {}: {error}", path.display()))?;
let rows = statement
.query_map([], |row| row.get::<_, String>(0))
.map_err(|error| format!("统计会话总数失败 {}: {error}", path.display()))?;
.map_err(|error| format!("Failed to count sessions {}: {error}", path.display()))?;
for id in rows
.collect::<rusqlite::Result<Vec<_>>>()
.map_err(|error| format!("统计会话总数失败 {}: {error}", path.display()))?
.map_err(|error| format!("Failed to count sessions {}: {error}", path.display()))?
{
let id = session_identity_key(&id);
if !id.is_empty() {
@@ -790,7 +790,7 @@ fn list_thread_rows(
);
let mut statement = connection
.prepare(&sql)
.map_err(|error| format!("读取会话数据库失败 {}: {error}", path.display()))?;
.map_err(|error| format!("Failed to read session database {}: {error}", path.display()))?;
let rows = statement
.query_map([i64::try_from(limit).unwrap_or(i64::MAX)], |row| {
Ok(CodexSessionSummary {
@@ -803,9 +803,9 @@ fn list_thread_rows(
database_path: path.to_string_lossy().to_string(),
})
})
.map_err(|error| format!("读取会话数据库失败 {}: {error}", path.display()))?;
.map_err(|error| format!("Failed to read session database {}: {error}", path.display()))?;
rows.collect::<rusqlite::Result<Vec<_>>>()
.map_err(|error| format!("读取会话数据库失败 {}: {error}", path.display()))
.map_err(|error| format!("Failed to read session database {}: {error}", path.display()))
}
fn list_automation_rows(
@@ -832,7 +832,7 @@ fn list_automation_rows(
);
let mut statement = connection
.prepare(&sql)
.map_err(|error| format!("读取自动化会话数据库失败 {}: {error}", path.display()))?;
.map_err(|error| format!("Failed to read automation session database {}: {error}", path.display()))?;
let rows = statement
.query_map([i64::try_from(limit).unwrap_or(i64::MAX)], |row| {
let status = row.get::<_, Option<String>>(3)?.unwrap_or_default();
@@ -846,9 +846,9 @@ fn list_automation_rows(
database_path: path.to_string_lossy().to_string(),
})
})
.map_err(|error| format!("读取自动化会话数据库失败 {}: {error}", path.display()))?;
.map_err(|error| format!("Failed to read automation session database {}: {error}", path.display()))?;
rows.collect::<rusqlite::Result<Vec<_>>>()
.map_err(|error| format!("读取自动化会话数据库失败 {}: {error}", path.display()))
.map_err(|error| format!("Failed to read automation session database {}: {error}", path.display()))
}
fn normalize_thread_id(value: &str) -> String {
@@ -875,7 +875,7 @@ fn delete_codex_sessions_from_home(
) -> Result<CodexSessionDeleteBatchResult, String> {
let _guard = CODEX_SESSION_WRITE_LOCK
.lock()
.map_err(|_| "Codex 会话写入锁已损坏".to_string())?;
.map_err(|_| "Codex session write lock is poisoned".to_string())?;
let mut unique = Vec::new();
let mut seen = HashSet::new();
for value in session_ids {
@@ -885,15 +885,15 @@ fn delete_codex_sessions_from_home(
}
}
if unique.is_empty() {
return Err("请至少选择一个有效的会话 ID".to_string());
return Err("Select at least one valid session ID".to_string());
}
if unique.len() > MAX_PAGE_SIZE {
return Err(format!("一次最多删除 {MAX_PAGE_SIZE} 个会话"));
return Err(format!("You can delete up to {MAX_PAGE_SIZE} sessions at a time"));
}
let (database_paths, database_warnings) = discover_database_paths(codex_home, false);
if !database_warnings.is_empty() {
return Err(format!(
"无法安全检查全部 Codex 会话数据库,删除已中止:{}",
"Unable to safely inspect all Codex session databases; deletion aborted: {}",
database_warnings.join(";")
));
}
@@ -929,7 +929,7 @@ fn delete_one_session(
return Ok(CodexSessionDeleteResult {
session_id: session_id.to_string(),
status: "notFound".to_string(),
message: "未在本地数据库中找到该会话".to_string(),
message: "Session not found in the local database".to_string(),
backup_path: None,
});
}
@@ -945,7 +945,7 @@ fn delete_one_session(
session_id: session_id.to_string(),
status: status.to_string(),
message: format!(
"已提交 {committed_databases} 个数据库事务,其余数据库删除失败:{error}"
"Committed {committed_databases} database transactions; deletion failed for the remaining databases: {error}"
),
backup_path: Some(backup_dir.to_string_lossy().to_string()),
});
@@ -971,19 +971,19 @@ fn delete_one_session(
}
}
Ok(None) => rollout_errors.push(format!(
"已保留不在 Codex 会话目录中的 rollout:{}",
"Preserved rollout outside the Codex session directory: {}",
path.display()
)),
Err(error) => rollout_errors.push(error),
}
}
let (status, message) = if rollout_errors.is_empty() {
("deleted", "已删除本地数据库记录和 rollout 文件".to_string())
("deleted", "Deleted local database records and rollout files".to_string())
} else {
(
"partial",
format!(
"数据库记录已删除,但部分 rollout 未删除:{}",
"Database records were deleted, but some rollouts were not: {}",
rollout_errors.join(";")
),
)
@@ -1140,7 +1140,7 @@ fn select_json_rows(
) -> Result<Vec<Value>, String> {
let mut statement = connection
.prepare(sql)
.map_err(|error| format!("准备会话备份查询失败: {error}"))?;
.map_err(|error| format!("Failed to prepare session backup query: {error}"))?;
let columns = statement
.column_names()
.iter()
@@ -1154,9 +1154,9 @@ fn select_json_rows(
}
Ok(Value::Object(value))
})
.map_err(|error| format!("读取会话备份数据失败: {error}"))?;
.map_err(|error| format!("Failed to read session backup data: {error}"))?;
rows.collect::<rusqlite::Result<Vec<_>>>()
.map_err(|error| format!("读取会话备份数据失败: {error}"))
.map_err(|error| format!("Failed to read session backup data: {error}"))
}
fn sql_value_to_json(value: ValueRef<'_>) -> Value {
@@ -1198,7 +1198,7 @@ fn create_operation_directory(
.join(category)
.join(name);
fs::create_dir_all(&directory)
.map_err(|error| format!("创建会话备份目录失败 {}: {error}", directory.display()))?;
.map_err(|error| format!("Failed to create session backup directory {}: {error}", directory.display()))?;
Ok(directory)
}
@@ -1246,7 +1246,7 @@ fn create_delete_backup(
}
fs::create_dir_all(&rollout_directory).map_err(|error| {
format!(
"创建 rollout 备份目录失败 {}: {error}",
"Failed to create rollout backup directory {}: {error}",
rollout_directory.display()
)
})?;
@@ -1259,7 +1259,7 @@ fn create_delete_backup(
);
let target = rollout_directory.join(&file_name);
fs::copy(&validated, &target)
.map_err(|error| format!("备份 rollout 失败 {}: {error}", validated.display()))?;
.map_err(|error| format!("Failed to back up rollout {}: {error}", validated.display()))?;
rollout_files.push(json!({
"originalPath": validated.to_string_lossy(),
"backupPath": format!("rollouts/{file_name}")
@@ -1295,7 +1295,7 @@ fn validated_rollout_path(codex_home: &Path, path: &Path) -> Result<Option<PathB
return Ok(None);
}
let canonical = fs::canonicalize(&candidate)
.map_err(|error| format!("解析 rollout 路径失败 {}: {error}", candidate.display()))?;
.map_err(|error| format!("Failed to resolve rollout path {}: {error}", candidate.display()))?;
for directory in SESSION_DIRS {
let root = codex_home.join(directory);
if let Ok(root) = fs::canonicalize(root) {
@@ -1313,7 +1313,7 @@ fn apply_database_delete_plan(plan: &DatabaseDeletePlan, session_id: &str) -> Re
let mut connection = open_read_write(&plan.path)?;
let transaction = connection
.transaction()
.map_err(|error| format!("开始数据库事务失败 {}: {error}", plan.path.display()))?;
.map_err(|error| format!("Failed to begin database transaction {}: {error}", plan.path.display()))?;
if plan.has_thread_row {
delete_where_if_supported(
&transaction,
@@ -1352,7 +1352,7 @@ fn apply_database_delete_plan(plan: &DatabaseDeletePlan, session_id: &str) -> Re
"UPDATE agent_job_items SET assigned_thread_id = NULL WHERE assigned_thread_id = ?1",
[session_id],
)
.map_err(|error| format!("更新 agent_job_items 失败: {error}"))?;
.map_err(|error| format!("Failed to update agent_job_items: {error}"))?;
}
delete_where_if_supported(&transaction, "threads", &["id"], "id = ?1", &[&session_id])?;
}
@@ -1388,7 +1388,7 @@ fn apply_database_delete_plan(plan: &DatabaseDeletePlan, session_id: &str) -> Re
)?;
transaction
.commit()
.map_err(|error| format!("提交数据库删除失败 {}: {error}", plan.path.display()))
.map_err(|error| format!("Failed to commit database deletion {}: {error}", plan.path.display()))
}
fn delete_where_if_supported(
@@ -1415,12 +1415,12 @@ fn delete_where_if_supported(
params,
)
.map(|_| ())
.map_err(|error| format!("删除 {table} 关联记录失败: {error}"))
.map_err(|error| format!("Failed to delete related records from {table}: {error}"))
}
fn write_json_atomically(path: &Path, value: &Value) -> Result<(), String> {
let bytes =
serde_json::to_vec_pretty(value).map_err(|error| format!("序列化会话备份失败: {error}"))?;
serde_json::to_vec_pretty(value).map_err(|error| format!("Failed to serialize session backup: {error}"))?;
crate::write_bytes_atomically(path, &bytes)
}
@@ -1430,10 +1430,10 @@ fn resolve_current_codex_provider(codex_home: &Path) -> Result<String, String> {
return Ok("openai".to_string());
}
let text = fs::read_to_string(&path)
.map_err(|error| format!("读取 Codex 配置失败 {}: {error}", path.display()))?;
.map_err(|error| format!("Failed to read Codex configuration {}: {error}", path.display()))?;
let document = text
.parse::<toml::Value>()
.map_err(|error| format!("Codex 配置无法解析: {error}"))?;
.map_err(|error| format!("Failed to parse Codex configuration: {error}"))?;
Ok(document
.get("model_provider")
.and_then(toml::Value::as_str)
@@ -1453,11 +1453,11 @@ where
{
let target_provider = target_provider.trim();
if target_provider.is_empty() {
return Err("当前 Codex provider 不能为空".to_string());
return Err("Current Codex provider cannot be empty".to_string());
}
let _guard = CODEX_SESSION_WRITE_LOCK
.lock()
.map_err(|_| "Codex 会话写入锁已损坏".to_string())?;
.map_err(|_| "Codex session write lock is poisoned".to_string())?;
progress(CodexSessionRepairProgress {
phase: "scanning".to_string(),
percent: 10,
@@ -1541,7 +1541,7 @@ where
let _ = crate::write_bytes_atomically(&previous.path, &previous.original);
restore_modified_time(&previous.path, previous.original_mtime);
}
return Err(format!("写入历史会话失败;已尝试回滚:{error}"));
return Err(format!("Failed to write historical session; rollback attempted: {error}"));
}
}
progress(CodexSessionRepairProgress {
@@ -1569,7 +1569,7 @@ where
restore_modified_time(&previous.path, previous.original_mtime);
}
return Err(format!(
"更新会话数据库失败;rollout 已尝试回滚,备份仍保留:{error}"
"Failed to update session database; rollout rollback attempted and backup retained: {error}"
));
}
};
@@ -1608,12 +1608,12 @@ fn collect_rollout_files_recursive(root: &Path, files: &mut Vec<PathBuf>) -> Res
return Ok(());
}
let entries = fs::read_dir(root)
.map_err(|error| format!("扫描会话目录失败 {}: {error}", root.display()))?;
.map_err(|error| format!("Failed to scan session directory {}: {error}", root.display()))?;
for entry in entries {
let entry = entry.map_err(|error| format!("读取会话目录项失败: {error}"))?;
let entry = entry.map_err(|error| format!("Failed to read session directory entry: {error}"))?;
let file_type = entry
.file_type()
.map_err(|error| format!("读取会话目录项类型失败: {error}"))?;
.map_err(|error| format!("Failed to read session directory entry type: {error}"))?;
if file_type.is_symlink() {
continue;
}
@@ -1632,9 +1632,9 @@ fn build_rollout_repair(
target_provider: &str,
) -> Result<Option<RolloutRepair>, String> {
let original =
fs::read(path).map_err(|error| format!("读取 rollout 失败 {}: {error}", path.display()))?;
fs::read(path).map_err(|error| format!("Failed to read rollout {}: {error}", path.display()))?;
let text = String::from_utf8(original.clone())
.map_err(|error| format!("rollout 不是 UTF-8 {}: {error}", path.display()))?;
.map_err(|error| format!("Rollout is not UTF-8 {}: {error}", path.display()))?;
let mut next = String::with_capacity(text.len());
let mut changed = false;
let mut session_meta_count = 0usize;
@@ -1674,7 +1674,7 @@ fn build_rollout_repair(
{
payload.insert("model_provider".to_string(), json!(target_provider));
output = serde_json::to_string(&record)
.map_err(|error| format!("序列化 session_meta 失败: {error}"))?;
.map_err(|error| format!("Failed to serialize session_meta: {error}"))?;
changed = true;
}
}
@@ -1737,9 +1737,9 @@ fn load_projectless_thread_ids(codex_home: &Path) -> Result<HashSet<String>, Str
return Ok(HashSet::new());
}
let value = serde_json::from_str::<Value>(
&fs::read_to_string(&path).map_err(|error| format!("读取 Codex 全局状态失败: {error}"))?,
&fs::read_to_string(&path).map_err(|error| format!("Failed to read Codex global state: {error}"))?,
)
.map_err(|error| format!("解析 Codex 全局状态失败: {error}"))?;
.map_err(|error| format!("Failed to parse Codex global state: {error}"))?;
Ok(value
.get("projectless-thread-ids")
.and_then(Value::as_array)
@@ -1789,7 +1789,7 @@ fn count_sqlite_repairs(
[target_provider],
|row| row.get::<_, i64>(0),
)
.map_err(|error| format!("统计 Provider 迁移行失败: {error}"))?
.map_err(|error| format!("Failed to count provider migration rows: {error}"))?
as usize;
}
if columns.contains("has_user_event") {
@@ -1800,7 +1800,7 @@ fn count_sqlite_repairs(
[id],
|row| row.get::<_, i64>(0),
)
.map_err(|error| format!("统计会话可见性更新行失败: {error}"))?
.map_err(|error| format!("Failed to count session visibility update rows: {error}"))?
as usize;
}
}
@@ -1812,7 +1812,7 @@ fn count_sqlite_repairs(
(id, cwd),
|row| row.get::<_, i64>(0),
)
.map_err(|error| format!("统计项目路径更新行失败: {error}"))?
.map_err(|error| format!("Failed to count project path update rows: {error}"))?
as usize;
}
}
@@ -1821,7 +1821,7 @@ fn count_sqlite_repairs(
match count_result {
Ok(count) => total += count,
Err(error) if is_locked_error_message(&error) => warnings.push(format!(
"数据库正在使用,无法预统计同步行 {}: {error}",
"Database is in use; cannot pre-count synchronization rows {}: {error}",
path.display()
)),
Err(error) => return Err(error),
@@ -1848,7 +1848,7 @@ fn apply_sqlite_repairs(
}
let transaction = connection
.transaction()
.map_err(|error| format!("开始历史会话恢复事务失败 {}: {error}", path.display()))?;
.map_err(|error| format!("Failed to begin historical session restoration transaction {}: {error}", path.display()))?;
let mut updated = 0usize;
if columns.contains("model_provider") {
updated += transaction
@@ -1856,7 +1856,7 @@ fn apply_sqlite_repairs(
"UPDATE threads SET model_provider = ?1 WHERE COALESCE(model_provider, '') <> ?1",
[target_provider],
)
.map_err(|error| format!("更新会话 provider 失败: {error}"))?;
.map_err(|error| format!("Failed to update session provider: {error}"))?;
}
if columns.contains("has_user_event") {
for id in &user_event_ids {
@@ -1865,7 +1865,7 @@ fn apply_sqlite_repairs(
"UPDATE threads SET has_user_event = 1 WHERE id = ?1 AND COALESCE(has_user_event, 0) <> 1",
[id],
)
.map_err(|error| format!("更新会话用户事件标记失败: {error}"))?;
.map_err(|error| format!("Failed to update session user-event flag: {error}"))?;
}
}
if columns.contains("cwd") {
@@ -1875,18 +1875,18 @@ fn apply_sqlite_repairs(
"UPDATE threads SET cwd = ?1 WHERE id = ?2 AND COALESCE(cwd, '') <> ?1",
(cwd, id),
)
.map_err(|error| format!("更新会话项目路径失败: {error}"))?;
.map_err(|error| format!("Failed to update session project path: {error}"))?;
}
}
transaction
.commit()
.map_err(|error| format!("提交历史会话恢复事务失败 {}: {error}", path.display()))?;
.map_err(|error| format!("Failed to commit historical session restoration transaction {}: {error}", path.display()))?;
Ok(updated)
})();
match update_result {
Ok(updated) => total += updated,
Err(error) if is_locked_error_message(&error) => warnings.push(format!(
"数据库正在使用,已跳过本次同步 {}: {error}",
"Database is in use; skipped this synchronization {}: {error}",
path.display()
)),
Err(error) => return Err(error),
@@ -1911,7 +1911,7 @@ fn create_repair_backup(
let source = codex_home.join(name);
if source.is_file() {
fs::copy(&source, directory.join(name))
.map_err(|error| format!("备份 {name} 失败: {error}"))?;
.map_err(|error| format!("Failed to back up {name}: {error}"))?;
}
}
let sqlite_home = resolve_sqlite_home(codex_home);
@@ -1928,10 +1928,10 @@ fn create_repair_backup(
let target = directory.join("databases").join(&relative);
if let Some(parent) = target.parent() {
fs::create_dir_all(parent)
.map_err(|error| format!("创建数据库备份目录失败: {error}"))?;
.map_err(|error| format!("Failed to create database backup directory: {error}"))?;
}
fs::copy(&source, &target)
.map_err(|error| format!("备份数据库失败 {}: {error}", source.display()))?;
.map_err(|error| format!("Failed to back up database {}: {error}", source.display()))?;
database_files.push(relative.to_string_lossy().replace('\\', "/"));
}
}
@@ -1948,10 +1948,10 @@ fn create_repair_backup(
let target = directory.join("rollouts").join(&file_name);
if let Some(parent) = target.parent() {
fs::create_dir_all(parent)
.map_err(|error| format!("创建 rollout 备份目录失败: {error}"))?;
.map_err(|error| format!("Failed to create rollout backup directory: {error}"))?;
}
fs::write(&target, &repair.original)
.map_err(|error| format!("备份 rollout 失败 {}: {error}", repair.path.display()))?;
.map_err(|error| format!("Failed to back up rollout {}: {error}", repair.path.display()))?;
rollout_files.push(json!({
"originalPath": repair.path.to_string_lossy(),
"backupPath": format!("rollouts/{file_name}")
@@ -2002,7 +2002,7 @@ fn encrypted_content_warning(repairs: &[RolloutRepair], target_provider: &str) -
return None;
}
Some(format!(
"部分历史会话包含来自 {} 的 encrypted_content;元数据已同步到 {},但继续或压缩这些会话可能失败。",
"Some historical sessions contain encrypted_content from {}; metadata was synchronized to {}, but continuing or compacting these sessions may fail.",
providers.join("、"),
target_provider
))
@@ -2017,7 +2017,7 @@ fn prune_repair_backups(codex_home: &Path) -> Result<(), String> {
return Ok(());
}
let mut directories = fs::read_dir(&root)
.map_err(|error| format!("读取历史会话恢复备份失败: {error}"))?
.map_err(|error| format!("Failed to read historical session restoration backup: {error}"))?
.filter_map(Result::ok)
.filter(|entry| entry.path().is_dir())
.collect::<Vec<_>>();
@@ -2027,7 +2027,7 @@ fn prune_repair_backups(codex_home: &Path) -> Result<(), String> {
let path = entry.path();
if path.starts_with(&root) {
fs::remove_dir_all(&path).map_err(|error| {
format!("清理旧的历史会话恢复备份失败 {}: {error}", path.display())
format!("Failed to clean up old historical session restoration backup {}: {error}", path.display())
})?;
}
}
@@ -2060,7 +2060,7 @@ fn collect_live_thread_ids(codex_home: &Path) -> Result<HashSet<String>, String>
let text = match fs::read_to_string(&path) {
Ok(text) => text,
Err(error) if is_locked_io_error(&error) => continue,
Err(error) => return Err(format!("读取 rollout 失败 {}: {error}", path.display())),
Err(error) => return Err(format!("Failed to read rollout {}: {error}", path.display())),
};
for line in text.lines() {
let Ok(record) = serde_json::from_str::<Value>(line) else {
@@ -2084,7 +2084,7 @@ fn collect_live_thread_ids(codex_home: &Path) -> Result<HashSet<String>, String>
let (database_paths, database_warnings) = discover_database_paths(codex_home, true);
if !database_warnings.is_empty() {
return Err(format!(
"无法完整核对全部 Codex 数据库,无效会话记录检查已中止:{}",
"Unable to fully verify all Codex databases; invalid session record check aborted: {}",
database_warnings.join(";")
));
}
@@ -2101,13 +2101,13 @@ fn collect_live_thread_ids(codex_home: &Path) -> Result<HashSet<String>, String>
);
let mut statement = connection
.prepare(&sql)
.map_err(|error| format!("读取会话引用失败 {}: {error}", path.display()))?;
.map_err(|error| format!("Failed to read session reference {}: {error}", path.display()))?;
let rows = statement
.query_map([], |row| row.get::<_, String>(0))
.map_err(|error| format!("读取会话引用失败 {}: {error}", path.display()))?;
.map_err(|error| format!("Failed to read session reference {}: {error}", path.display()))?;
for id in rows
.collect::<rusqlite::Result<HashSet<_>>>()
.map_err(|error| format!("读取会话引用失败 {}: {error}", path.display()))?
.map_err(|error| format!("Failed to read session reference {}: {error}", path.display()))?
{
ids.insert(normalize_thread_id(&id));
ids.insert(id);
@@ -2142,9 +2142,9 @@ fn build_session_index_plan(
return Ok(None);
}
let original =
fs::read(path).map_err(|error| format!("读取 session_index.jsonl 失败: {error}"))?;
fs::read(path).map_err(|error| format!("Failed to read session_index.jsonl: {error}"))?;
let original_text = String::from_utf8(original.clone())
.map_err(|error| format!("session_index.jsonl 不是 UTF-8: {error}"))?;
.map_err(|error| format!("session_index.jsonl is not UTF-8: {error}"))?;
let mut seen_candidates = HashSet::new();
let candidates = original_text
.lines()
@@ -2195,16 +2195,16 @@ fn apply_session_index_cleanup_from_home(
) -> Result<SessionIndexCleanupResult, String> {
let _guard = CODEX_SESSION_WRITE_LOCK
.lock()
.map_err(|_| "Codex 会话写入锁已损坏".to_string())?;
.map_err(|_| "Codex session write lock is poisoned".to_string())?;
if require_stopped_app {
ensure_codex_apps_stopped()?;
}
let live_ids = collect_live_thread_ids(codex_home)?;
let path = codex_home.join("session_index.jsonl");
let plan = build_session_index_plan(&path, &live_ids)?
.ok_or_else(|| "session_index.jsonl 不存在,无法清理".to_string())?;
.ok_or_else(|| "session_index.jsonl does not exist and cannot be cleaned".to_string())?;
if plan.snapshot_sha256 != expected_snapshot {
return Err("session_index.jsonl 已在预览后发生变化,请重新预览".to_string());
return Err("session_index.jsonl changed after preview. Preview it again".to_string());
}
let candidate_ids = plan
.candidates
@@ -2221,7 +2221,7 @@ fn apply_session_index_cleanup_from_home(
.iter()
.any(|id| !candidate_ids.contains(id.as_str()))
{
return Err("确认列表已经过期或包含非候选会话,请重新预览".to_string());
return Err("The confirmation list has expired or contains non-candidate sessions. Preview again".to_string());
}
if selected.is_empty() {
return Ok(SessionIndexCleanupResult {
@@ -2239,7 +2239,7 @@ fn apply_session_index_cleanup_from_home(
let backup_dir =
create_operation_directory(codex_home, "session-index-cleanup", "index-cleanup")?;
fs::write(backup_dir.join("session_index.jsonl"), &plan.original)
.map_err(|error| format!("备份 session_index.jsonl 失败: {error}"))?;
.map_err(|error| format!("Failed to back up session_index.jsonl: {error}"))?;
write_json_atomically(
&backup_dir.join("metadata.json"),
&json!({
@@ -2252,10 +2252,10 @@ fn apply_session_index_cleanup_from_home(
}),
)?;
let current = fs::read(&plan.path)
.map_err(|error| format!("写入前重新读取 session_index.jsonl 失败: {error}"))?;
.map_err(|error| format!("Failed to reread session_index.jsonl before writing: {error}"))?;
if current != plan.original {
return Err(format!(
"session_index.jsonl 在写入前再次发生变化;未覆盖新内容,备份位于 {}",
"session_index.jsonl changed again before writing; new content was not overwritten, and the backup is at {}",
backup_dir.display()
));
}
@@ -2265,10 +2265,10 @@ fn apply_session_index_cleanup_from_home(
let expected_after_sha256 = sha256_hex(next.as_bytes());
crate::write_bytes_atomically(&plan.path, next.as_bytes())?;
let written = fs::read(&plan.path)
.map_err(|error| format!("写入后校验 session_index.jsonl 失败: {error}"))?;
.map_err(|error| format!("Failed to verify session_index.jsonl after writing: {error}"))?;
if sha256_hex(&written) != expected_after_sha256 {
return Err(format!(
"session_index.jsonl 写入后 SHA-256 校验失败;原文件备份位于 {}",
"session_index.jsonl SHA-256 verification failed after writing; the original file backup is at {}",
backup_dir.display()
));
}
@@ -2313,7 +2313,7 @@ fn ensure_codex_apps_stopped() -> Result<(), String> {
return Ok(());
}
Err(format!(
"Codex App / ChatGPT 仍在运行(进程:{}),请完全退出后重新预览并清理",
"Codex App / ChatGPT is still running (process: {}). Exit it completely, then preview and clean again",
running
.iter()
.map(u32::to_string)
@@ -2338,10 +2338,10 @@ fn windows_app_process_ids(names: &[&str]) -> Result<Vec<u32>, String> {
.args(["/FI", &filter, "/FO", "CSV", "/NH"])
.creation_flags(CREATE_NO_WINDOW)
.output()
.map_err(|error| format!("无法检查 {name} 进程状态: {error}"))?;
.map_err(|error| format!("Failed to check {name} process status: {error}"))?;
if !output.status.success() {
return Err(format!(
"检查 {name} 进程状态失败: {}",
"Error checking {name} process status: {}",
String::from_utf8_lossy(&output.stderr).trim()
));
}
@@ -2379,7 +2379,7 @@ fn unix_app_process_ids(names: &[&str]) -> Result<Vec<u32>, String> {
let output = Command::new("pgrep")
.args(["-x", name])
.output()
.map_err(|error| format!("无法检查 {name} 进程状态: {error}"))?;
.map_err(|error| format!("Failed to check {name} process status: {error}"))?;
match output.status.code() {
Some(0) => ids.extend(
String::from_utf8_lossy(&output.stdout)
@@ -2389,7 +2389,7 @@ fn unix_app_process_ids(names: &[&str]) -> Result<Vec<u32>, String> {
Some(1) => {}
_ => {
return Err(format!(
"检查 {name} 进程状态失败: {}",
"Error checking {name} process status: {}",
String::from_utf8_lossy(&output.stderr).trim()
));
}
@@ -3020,7 +3020,7 @@ mod tests {
.unwrap();
let error = preview_session_index_cleanup_from_home(&root).unwrap_err();
assert!(error.contains("无效会话记录检查已中止"));
assert!(error.contains("invalid session record check aborted"));
assert!(root.join("session_index.jsonl").is_file());
fs::remove_dir_all(root).unwrap();
}
+10 -10
View File
@@ -51,7 +51,7 @@ fn patch_core_from_gui_config_if_valid(config: &GuiConfigFile) -> Result<(), Str
return Ok(());
}
let content = fs::read_to_string(&path)
.map_err(|error| format!("读取内核配置失败 {}: {error}", path_to_string(&path)))?;
.map_err(|error| format!("Failed to read kernel configuration {}: {error}", path_to_string(&path)))?;
let updated = apply_gui_managed_settings(&content, config)?;
write_yaml_if_changed(&path, &updated).map(|_| ())
}
@@ -60,7 +60,7 @@ fn tracked_configuration_paths(app: &tauri::AppHandle) -> Result<Vec<PathBuf>, S
let home = app
.path()
.home_dir()
.map_err(|error| format!("无法获取用户目录: {error}"))?;
.map_err(|error| format!("Failed to get user directory: {error}"))?;
let mut paths = vec![
gui_config_path()?,
core_install_dir()?.join(CORE_CONFIG_FILE),
@@ -142,12 +142,12 @@ fn handle_configuration_file_changes(
let parsed = (|| -> Result<GuiConfigFile, String> {
let content = fs::read_to_string(tracked_path).map_err(|error| {
format!(
"读取 GUI 配置失败 {}: {error}",
"Failed to read GUI configuration {}: {error}",
path_to_string(tracked_path)
)
})?;
let mut config = toml::from_str::<GuiConfigFile>(&content)
.map_err(|error| format!("解析 GUI 配置失败: {error}"))?;
.map_err(|error| format!("Failed to parse GUI configuration: {error}"))?;
config.allow_lan = !is_loopback_host(&config.host);
config.proxy_url = super::network_proxy::resolve(&config);
validate_gui_config(&config)?;
@@ -178,10 +178,10 @@ fn handle_configuration_file_changes(
if is_core_path(tracked_path) {
let parsed = (|| -> Result<CoreConfigSettings, String> {
let content = fs::read_to_string(tracked_path).map_err(|error| {
format!("读取内核配置失败 {}: {error}", path_to_string(tracked_path))
format!("Failed to read kernel configuration {}: {error}", path_to_string(tracked_path))
})?;
let document = serde_norway::from_str::<serde_norway::Value>(&content)
.map_err(|error| format!("解析内核配置失败: {error}"))?;
.map_err(|error| format!("Failed to parse kernel configuration: {error}"))?;
core_config_settings_from_value(&document)
})();
let settings = match parsed {
@@ -262,7 +262,7 @@ fn ensure_configuration_watch_directories(
}
watcher
.watch(&directory, RecursiveMode::NonRecursive)
.map_err(|error| format!("监控配置目录失败 {}: {error}", path_to_string(&directory)))?;
.map_err(|error| format!("Failed to monitor configuration directory {}: {error}", path_to_string(&directory)))?;
watched_directories.push(directory.clone());
newly_watched.push(directory);
}
@@ -355,7 +355,7 @@ pub(crate) fn start_configuration_file_watcher(app: tauri::AppHandle) -> Result<
let _ = sender.try_send(());
}
})
.map_err(|error| format!("创建配置文件监控器失败: {error}"))?;
.map_err(|error| format!("Failed to create configuration file watcher: {error}"))?;
let mut watched_directories = Vec::new();
ensure_configuration_watch_directories(&mut watcher, &tracked_paths, &mut watched_directories)?;
@@ -379,7 +379,7 @@ pub(crate) fn start_configuration_file_watcher(app: tauri::AppHandle) -> Result<
Err(_) => return,
};
if let Some(error) = changes.error.take() {
eprintln!("配置文件监控错误: {error}");
eprintln!("Configuration file watcher error: {error}");
}
match ensure_configuration_watch_directories(
&mut watcher,
@@ -398,7 +398,7 @@ pub(crate) fn start_configuration_file_watcher(app: tauri::AppHandle) -> Result<
}
}
}
Err(error) => eprintln!("配置目录监控更新失败: {error}"),
Err(error) => eprintln!("Failed to update configuration directory watcher: {error}"),
}
handle_configuration_file_changes(&app, changes.paths, &tracked_paths);
}
+29 -29
View File
@@ -20,18 +20,18 @@ pub(crate) fn validate_model_alias_revision(
revision: Option<&str>,
) -> Result<(), String> {
if revision != Some(model_alias_config_revision(content)?.as_str()) {
return Err("配置已变化,请关闭编辑器并刷新后重试".to_string());
return Err("Configuration changed. Close the editor, refresh, and try again".to_string());
}
Ok(())
}
pub(crate) fn model_alias_config_revision(content: &str) -> Result<String, String> {
let document = serde_norway::from_str::<serde_norway::Value>(content)
.map_err(|error| format!("解析内核 YAML 配置失败: {error}"))?;
.map_err(|error| format!("Failed to parse kernel YAML configuration: {error}"))?;
let mut value = serde_json::to_value(document).map_err(|error| error.to_string())?;
let root = value
.as_object_mut()
.ok_or("内核配置顶层必须是 YAML 映射")?;
.ok_or("Kernel configuration root must be a YAML mapping")?;
if root
.get("oauth-model-alias")
.and_then(serde_json::Value::as_object)
@@ -57,7 +57,7 @@ pub(crate) fn model_alias_edit_context(
.map_err(|error| error.to_string())?;
let root = document
.as_mapping()
.ok_or("内核配置顶层必须是 YAML 映射")?;
.ok_or("Kernel configuration root must be a YAML mapping")?;
Ok(ModelAliasEditContext {
effort: find_thinking_alias_effort(root, alias, &source.protocol),
fast: find_speed_alias_service_tier(root, alias, &source.protocol).as_deref()
@@ -81,7 +81,7 @@ fn editable_model_alias(
("codex-api-key", "Codex API", "codex-api", "codex"),
(
"openai-compatibility",
"OpenAI 兼容",
"OpenAI-compatible",
"openai-compatible",
"openai",
),
@@ -93,7 +93,7 @@ fn editable_model_alias(
};
let providers = providers
.as_sequence()
.ok_or_else(|| format!("{section} 必须是数组"))?;
.ok_or_else(|| format!("{section} must be an array"))?;
for (provider_index, value) in providers.iter().enumerate() {
let Some(value) = value.as_mapping() else {
continue;
@@ -103,7 +103,7 @@ fn editable_model_alias(
};
let models = models
.as_sequence()
.ok_or_else(|| format!("{section}.models 必须是数组"))?;
.ok_or_else(|| format!("{section}.models must be an array"))?;
for (model_index, model) in models.iter().enumerate() {
let Some((upstream, client, display_name)) = configured_model_identity(model)
else {
@@ -142,11 +142,11 @@ fn editable_model_alias(
if let Some(channels) = yaml_mapping_value(root, "oauth-model-alias") {
let channels = channels
.as_mapping()
.ok_or("oauth-model-alias 必须是 YAML 映射")?;
.ok_or("oauth-model-alias must be a YAML mapping")?;
for (channel_name, models) in channels {
let models = models
.as_sequence()
.ok_or("oauth-model-alias 的通道配置必须是数组")?;
.ok_or("oauth-model-alias channel configuration must be an array")?;
for (model_index, model) in models.iter().enumerate() {
let Some((upstream, client, display_name)) = configured_model_identity(model)
else {
@@ -162,7 +162,7 @@ fn editable_model_alias(
let channel = channel_name
.as_str()
.and_then(oauth_alias_channel)
.ok_or("暂不支持编辑此 OAuth 通道的模型别名")?;
.ok_or("Editing model aliases for this OAuth channel is not supported yet")?;
matches.push(EditableModelAlias {
source: ResolvedThinkingAliasSource {
source: ThinkingAliasSource {
@@ -186,7 +186,7 @@ fn editable_model_alias(
}
}
if matching_names != 1 || matches.len() != 1 {
return Err("别名不存在或存在多个同名映射,请刷新并检查配置后重试".to_string());
return Err("Alias does not exist or has multiple mappings with the same name. Refresh, check the configuration, and try again".to_string());
}
Ok(matches.remove(0))
}
@@ -197,10 +197,10 @@ pub(crate) fn resolve_model_alias_edit_source(
definitions: &[OAuthModelDefinitions],
) -> Result<ResolvedThinkingAliasSource, String> {
let document = serde_norway::from_str::<serde_norway::Value>(content)
.map_err(|error| format!("解析内核 YAML 配置失败: {error}"))?;
.map_err(|error| format!("Failed to parse kernel YAML configuration: {error}"))?;
let root = document
.as_mapping()
.ok_or("内核配置顶层必须是 YAML 映射")?;
.ok_or("Kernel configuration root must be a YAML mapping")?;
let mut source = editable_model_alias(root, alias)?.source;
if let ThinkingAliasSourceLocation::Oauth { channel, .. } = source.location {
if let Some(model) = definitions
@@ -229,17 +229,17 @@ pub(crate) fn edit_model_alias_in_yaml(
fast: bool,
) -> Result<String, String> {
if fast && !alias_source_supports_fast(source) {
return Err("Fast 仅支持 OpenAI 兼容 API、Codex API 或 Codex OAuth 模型源".to_string());
return Err("Fast supports only OpenAI-compatible API, Codex API, or Codex OAuth model sources".to_string());
}
let mut document = yaml_serde_edit::YamlValue::parse(content)
.map_err(|error| format!("解析内核 YAML 配置失败: {error}"))?;
.map_err(|error| format!("Failed to parse kernel YAML configuration: {error}"))?;
let mut updated = document.get().clone();
let root = updated
.as_mapping_mut()
.ok_or("内核配置顶层必须是 YAML 映射")?;
.ok_or("Kernel configuration root must be a YAML mapping")?;
let original = editable_model_alias(root, original_alias)?;
if !alias.eq_ignore_ascii_case(original_alias) && configured_model_alias_exists(root, alias) {
return Err(format!("别名模型 {alias} 已存在"));
return Err(format!("Alias model {alias} already exists"));
}
let mut replacement = match &source.location {
ThinkingAliasSourceLocation::ConfigModel {
@@ -254,8 +254,8 @@ pub(crate) fn edit_model_alias_in_yaml(
.and_then(|provider| yaml_mapping_value(provider, "models"))
.and_then(serde_norway::Value::as_sequence)
.and_then(|models| models.get(*model_index))
.ok_or("原模型配置已经变化,请刷新后重试")?;
let (upstream, _, _) = configured_model_identity(model).ok_or("原模型配置格式无效")?;
.ok_or("Source model configuration changed. Refresh and try again")?;
let (upstream, _, _) = configured_model_identity(model).ok_or("Invalid source model configuration format")?;
let same_model = matches!(&original.source.location,
ThinkingAliasSourceLocation::ConfigModel { section: old_section, provider_index: old_provider, .. }
if old_section == section && old_provider == provider_index)
@@ -359,21 +359,21 @@ pub(crate) fn edit_model_alias_in_yaml(
.and_then(serde_norway::Value::as_mapping_mut)
.and_then(|provider| yaml_mapping_value_mut(provider, "models"))
.and_then(serde_norway::Value::as_sequence_mut)
.ok_or("模型提供商已经变化,请刷新后重试")?,
.ok_or("Model provider changed. Refresh and try again")?,
ThinkingAliasSourceLocation::Oauth { channel, .. } => root
.entry(yaml_key("oauth-model-alias"))
.or_insert_with(|| serde_norway::Value::Mapping(Default::default()))
.as_mapping_mut()
.ok_or("oauth-model-alias 必须是 YAML 映射")?
.ok_or("oauth-model-alias must be a YAML mapping")?
.entry(yaml_key(channel))
.or_insert_with(|| serde_norway::Value::Sequence(Vec::new()))
.as_sequence_mut()
.ok_or("oauth-model-alias 的通道配置必须是数组")?,
.ok_or("oauth-model-alias channel configuration must be an array")?,
};
if same_group {
let model = models
.get_mut(original.model_index)
.ok_or("原别名已经变化,请刷新后重试")?;
.ok_or("Original alias changed. Refresh and try again")?;
*model = serde_norway::Value::Mapping(replacement);
} else {
models.push(serde_norway::Value::Mapping(replacement));
@@ -436,7 +436,7 @@ fn edit_alias_payload(
let payload = root
.entry(yaml_key("payload"))
.or_insert_with(|| serde_norway::Value::Mapping(Default::default()));
let payload = payload.as_mapping_mut().ok_or("payload 必须是 YAML 映射")?;
let payload = payload.as_mapping_mut().ok_or("payload must be a YAML mapping")?;
for section in [
"default",
"default-raw",
@@ -449,7 +449,7 @@ fn edit_alias_payload(
};
let rules = rules
.as_sequence_mut()
.ok_or_else(|| format!("payload.{section} 必须是数组"))?;
.ok_or_else(|| format!("payload.{section} must be an array"))?;
let mut result = Vec::new();
for rule in rules.iter() {
let Some(mapping) = rule.as_mapping() else {
@@ -460,7 +460,7 @@ fn edit_alias_payload(
result.push(rule.clone());
continue;
};
let models = models.as_sequence().ok_or("payload.models 必须是数组")?;
let models = models.as_sequence().ok_or("payload.models must be an array")?;
let (mut target, others): (Vec<_>, Vec<_>) =
models.iter().cloned().partition(|model| {
thinking_payload_model_matches(model, original_alias, original_protocol)
@@ -472,7 +472,7 @@ fn edit_alias_payload(
for model in &mut target {
let model = model
.as_mapping_mut()
.ok_or("payload.models 条目必须是映射")?;
.ok_or("payload.models entries must be mappings")?;
model.insert(
yaml_key("name"),
serde_norway::Value::String(alias.to_string()),
@@ -494,7 +494,7 @@ fn edit_alias_payload(
if let Some(params) = yaml_mapping_value_mut(&mut edited, "params") {
let params = params
.as_mapping_mut()
.ok_or("payload.override.params 必须是映射")?;
.ok_or("payload.override.params must be a mapping")?;
let has_effort = ALIAS_EFFORT_KEYS
.iter()
.any(|key| params.contains_key(yaml_key(key)));
@@ -507,7 +507,7 @@ fn edit_alias_payload(
edit.effort.to_string()
} else {
thinking_effort_from_params(params, original_protocol, raw).ok_or(
"此规则的思考配置无法转换,请先在配置中调整后再切换模型协议",
"This rule's reasoning configuration cannot be converted. Adjust it in the configuration before switching model protocols",
)?
};
for key in ALIAS_EFFORT_KEYS {
+8 -8
View File
@@ -8,7 +8,7 @@ fn alias_config_is_unchanged(current: &str, latest: &str) -> Result<bool, String
fn validate_alias_api_access_preserved(current: &str, updated: &str) -> Result<(), String> {
let parse = |content: &str| {
serde_norway::from_str::<serde_norway::Value>(content)
.map_err(|error| format!("解析内核 YAML 配置失败: {error}"))
.map_err(|error| format!("Failed to parse kernel YAML configuration: {error}"))
};
let current = parse(current)?;
let updated = parse(updated)?;
@@ -27,7 +27,7 @@ fn validate_alias_api_access_preserved(current: &str, updated: &str) -> Result<(
}
let providers = value
.as_sequence_mut()
.ok_or_else(|| format!("{section} 必须是数组,已拒绝保存别名"))?;
.ok_or_else(|| format!("{section} must be an array; alias was not saved"))?;
if MODEL_ALIAS_CONFIG_SECTIONS.contains(&section) {
for provider in providers {
if let Some(provider) = provider.as_mapping_mut() {
@@ -39,7 +39,7 @@ fn validate_alias_api_access_preserved(current: &str, updated: &str) -> Result<(
};
if without_models(&current)? != without_models(&updated)? {
return Err(format!(
"别名更新意外改变了 API 接入配置({section}),已拒绝写入"
"The alias update unexpectedly changed API access configuration ({section}); write rejected"
));
}
}
@@ -70,7 +70,7 @@ pub(crate) async fn commit_management_alias_config_changes<T>(
let changes = management_alias_config_changes(current, updated)?;
let latest = fetch_management_config_yaml(config).await?;
if !alias_config_is_unchanged(current, &latest)? {
return Err("配置已变化,请关闭编辑器并刷新后重试".to_string());
return Err("Configuration changed. Close the editor, refresh, and try again".to_string());
}
let result = async {
if let Some(aliases) = changes.oauth_model_aliases.as_ref() {
@@ -85,8 +85,8 @@ pub(crate) async fn commit_management_alias_config_changes<T>(
match result {
Ok(value) => Ok(value),
Err(error) => Err(match restore_management_alias_config(config, current, updated).await {
Ok(()) => format!("保存失败,已恢复原配置,可重试:{error}"),
Err(restore_error) => format!("保存失败:{error};自动恢复失败,配置可能已部分写入,请关闭编辑器并刷新检查:{restore_error}"),
Ok(()) => format!("Save failed. Original configuration was restored; you can retry: {error}"),
Err(restore_error) => format!("Save failed: {error}; automatic restoration failed, and the configuration may be partially written. Close the editor, refresh, and inspect it: {restore_error}"),
}),
}
}
@@ -103,7 +103,7 @@ async fn restore_management_alias_config(
|| (from_current.oauth_model_aliases.is_some()
&& from_updated.oauth_model_aliases.is_some())
{
return Err("检测到其他配置修改,未覆盖这些修改".to_string());
return Err("Other configuration changes were detected and were not overwritten".to_string());
}
let mut errors = Vec::new();
if from_current.update_config_yaml {
@@ -120,7 +120,7 @@ async fn restore_management_alias_config(
return Err(errors.join(";"));
}
if !alias_config_is_unchanged(current, &fetch_management_config_yaml(config).await?)? {
return Err("恢复后配置与原配置不一致".to_string());
return Err("Restored configuration does not match the original configuration".to_string());
}
Ok(())
}
+108 -108
View File
@@ -151,13 +151,13 @@ pub(crate) async fn fetch_active_oauth_alias_channels(
.header(reqwest::header::ACCEPT, "application/json")
.send()
.await
.map_err(|error| format_management_request_error("读取 OAuth 凭据来源失败", &error))?;
.map_err(|error| format_management_request_error("Failed to read OAuth credential sources", &error))?;
let payload = read_management_value(response).await?;
let files = payload
.get("files")
.and_then(serde_json::Value::as_array)
.or_else(|| payload.as_array())
.ok_or_else(|| "OAuth 凭据来源响应缺少 files 数组".to_string())?;
.ok_or_else(|| "OAuth credential source response is missing a files array".to_string())?;
Ok(files
.iter()
.filter(|file| {
@@ -188,10 +188,10 @@ pub(crate) async fn fetch_active_oauth_alias_channels(
pub(crate) fn existing_thinking_alias_model_id(value: &str, label: &str) -> Result<String, String> {
let value = value.trim();
if value.is_empty() {
return Err(format!("{label}不能为空"));
return Err(format!("{label} cannot be empty"));
}
if value.len() > 240 || value.chars().any(char::is_control) {
return Err(format!("{label}格式无效"));
return Err(format!("Invalid {label} format"));
}
Ok(value.to_string())
}
@@ -202,7 +202,7 @@ pub(crate) fn validate_thinking_alias_model_id(value: &str, label: &str) -> Resu
.chars()
.any(|character| character.is_whitespace() || character.is_control())
{
return Err(format!("{label}格式无效,不能包含空白字符"));
return Err(format!("Invalid {label} format; whitespace is not allowed"));
}
Ok(value)
}
@@ -210,17 +210,17 @@ pub(crate) fn validate_thinking_alias_model_id(value: &str, label: &str) -> Resu
pub(crate) fn validate_thinking_alias_effort(value: &str) -> Result<String, String> {
let effort = value.trim().to_ascii_lowercase();
if effort.is_empty() {
return Err("思考强度不能为空".to_string());
return Err("Reasoning effort cannot be empty".to_string());
}
if effort.chars().all(|character| character.is_ascii_digit()) {
return Err("固定思考别名不支持纯数字预算,请输入思考等级名称".to_string());
return Err("Fixed reasoning aliases do not support numeric-only budgets. Enter a reasoning level name".to_string());
}
if effort.len() > 64
|| !effort.chars().all(|character| {
character.is_ascii_alphanumeric() || matches!(character, '-' | '_' | '.')
})
{
return Err("思考强度格式无效,仅支持字母、数字、短横线、下划线和点".to_string());
return Err("Invalid reasoning effort format. Only letters, numbers, hyphens, underscores, and periods are allowed".to_string());
}
Ok(effort)
}
@@ -236,7 +236,7 @@ pub(crate) async fn fetch_management_config_yaml(config: &GuiConfigFile) -> Resu
)
.send()
.await
.map_err(|error| format_management_request_error("读取内核 YAML 配置失败", &error))?;
.map_err(|error| format_management_request_error("Failed to read kernel YAML configuration", &error))?;
read_management_text(response).await
}
@@ -252,7 +252,7 @@ pub(crate) async fn put_management_config_yaml(
.body(content.to_string())
.send()
.await
.map_err(|error| format_management_request_error("保存内核 YAML 配置失败", &error))?;
.map_err(|error| format_management_request_error("Failed to save kernel YAML configuration", &error))?;
read_management_value(response).await.map(|_| ())
}
@@ -267,7 +267,7 @@ pub(crate) async fn put_management_oauth_model_aliases(
.json(aliases)
.send()
.await
.map_err(|error| format_management_request_error("保存 OAuth 模型别名失败", &error))?;
.map_err(|error| format_management_request_error("Failed to save OAuth model alias", &error))?;
read_management_value(response).await.map(|_| ())
}
@@ -283,10 +283,10 @@ pub(crate) fn management_alias_config_changes(
) -> Result<ManagementAliasConfigChanges, String> {
let split = |content: &str| {
let mut document = serde_norway::from_str::<serde_norway::Value>(content)
.map_err(|error| format!("解析内核 YAML 配置失败: {error}"))?;
.map_err(|error| format!("Failed to parse kernel YAML configuration: {error}"))?;
let root = document
.as_mapping_mut()
.ok_or_else(|| "内核配置顶层必须是 YAML 映射".to_string())?;
.ok_or_else(|| "Kernel configuration root must be a YAML mapping".to_string())?;
let oauth_aliases = root
.remove(yaml_key("oauth-model-alias"))
.unwrap_or_else(|| serde_norway::Value::Mapping(serde_norway::Mapping::new()));
@@ -299,7 +299,7 @@ pub(crate) fn management_alias_config_changes(
} else {
Some(
serde_json::to_value(updated_oauth)
.map_err(|error| format!("序列化 OAuth 模型别名失败: {error}"))?,
.map_err(|error| format!("Failed to serialize OAuth model alias: {error}"))?,
)
};
Ok(ManagementAliasConfigChanges {
@@ -323,7 +323,7 @@ pub(crate) fn ensure_claude_desktop_model_aliases_with_codex_oauth_in_yaml(
codex_oauth_models: &[CodexModelDefinition],
) -> Result<String, String> {
let codex_channel = oauth_alias_channel("codex")
.ok_or_else(|| "缺少 Codex OAuth 别名 channel 定义".to_string())?;
.ok_or_else(|| "Codex OAuth alias channel definition is missing".to_string())?;
let definitions = [OAuthModelDefinitions {
channel: codex_channel,
models: codex_oauth_models.to_vec(),
@@ -363,11 +363,11 @@ pub(crate) fn ensure_claude_desktop_model_aliases_with_oauth_definitions_and_rou
routes: [&str; 3],
) -> Result<String, String> {
let mut document = yaml_serde_edit::YamlValue::parse(content)
.map_err(|error| format!("解析内核 YAML 配置失败: {error}"))?;
.map_err(|error| format!("Failed to parse kernel YAML configuration: {error}"))?;
let mut updated = document.get().clone();
let root = updated
.as_mapping_mut()
.ok_or_else(|| "内核配置顶层必须是 YAML 映射".to_string())?;
.ok_or_else(|| "Kernel configuration root must be a YAML mapping".to_string())?;
let sources = root.clone();
if let Some(entries) = &mappings.desktop_models {
@@ -393,7 +393,7 @@ pub(crate) fn ensure_claude_desktop_model_aliases_with_oauth_definitions_and_rou
|| (occupied && !managed_aliases.iter()
.any(|alias| alias.eq_ignore_ascii_case(&entry.alias)))
{
return Err(format!("别名 {} 已被其他模型使用,请更换别名", entry.alias));
return Err(format!("Alias {} is already used by another model. Choose a different alias", entry.alias));
}
}
for alias in managed_aliases {
@@ -467,21 +467,21 @@ pub(crate) fn validate_claude_desktop_entries(
entries: &[ClaudeDesktopModelMapping],
) -> Result<(), String> {
if entries.is_empty() {
return Err("请至少添加一个 Claude Desktop 模型".into());
return Err("Add at least one Claude Desktop model".into());
}
let mut ids = HashSet::new();
for entry in entries {
validate_agent_model(entry.source_or_alias())?;
if entry.source_or_alias().chars().any(char::is_whitespace) {
return Err("原模型 ID 不能包含空白字符".into());
return Err("Source model ID cannot contain whitespace".into());
}
let alias = entry.alias.trim();
if !alias.is_empty() && !alias.eq_ignore_ascii_case(entry.source_or_alias()) && !valid_claude_desktop_alias(alias) {
return Err(format!("Claude Desktop 别名 {alias} 必须以 claude- 开头,只能包含小写字母、数字、连字符和小数点,且不能包含 gpt、grok、gemini、deepseek 等其他模型系列名称"));
return Err(format!("Claude Desktop alias {alias} must start with claude-, may contain only lowercase letters, numbers, hyphens, and periods, and cannot contain other model family names such as gpt, grok, gemini, or deepseek"));
}
let id = entry.model_id();
if !ids.insert(id.to_ascii_lowercase()) {
return Err(format!("Claude Desktop 模型 ID 重复: {id}"));
return Err(format!("Duplicate Claude Desktop model ID: {id}"));
}
}
Ok(())
@@ -556,11 +556,11 @@ pub(crate) fn managed_claude_desktop_aliases(root: &serde_norway::Mapping) -> Ve
#[cfg(test)]
pub(crate) fn remove_managed_claude_model_aliases_in_yaml(content: &str) -> Result<String, String> {
let mut document = yaml_serde_edit::YamlValue::parse(content)
.map_err(|error| format!("解析内核 YAML 配置失败: {error}"))?;
.map_err(|error| format!("Failed to parse kernel YAML configuration: {error}"))?;
let mut updated = document.get().clone();
let root = updated
.as_mapping_mut()
.ok_or_else(|| "内核配置顶层必须是 YAML 映射".to_string())?;
.ok_or_else(|| "Kernel configuration root must be a YAML mapping".to_string())?;
let mut changed = false;
for alias in [
CLAUDE_DESKTOP_OPUS_MODEL_ID,
@@ -589,7 +589,7 @@ pub(crate) fn remove_managed_claude_model_alias(
};
let providers = providers
.as_sequence_mut()
.ok_or_else(|| format!("{section} 必须是数组"))?;
.ok_or_else(|| format!("{section} must be an array"))?;
for provider in providers {
let Some(provider) = provider.as_mapping_mut() else {
continue;
@@ -599,7 +599,7 @@ pub(crate) fn remove_managed_claude_model_alias(
};
let models = models
.as_sequence_mut()
.ok_or_else(|| format!("{section}.models 必须是数组"))?;
.ok_or_else(|| format!("{section}.models must be an array"))?;
let before = models.len();
models.retain(|model| !is_managed_claude_model_alias(model, alias));
changed |= models.len() != before;
@@ -620,7 +620,7 @@ pub(crate) fn remove_existing_claude_model_alias(
};
let providers = providers
.as_sequence_mut()
.ok_or_else(|| format!("{section} 必须是数组"))?;
.ok_or_else(|| format!("{section} must be an array"))?;
for provider in providers {
let Some(provider) = provider.as_mapping_mut() else {
continue;
@@ -630,7 +630,7 @@ pub(crate) fn remove_existing_claude_model_alias(
};
let models = models
.as_sequence_mut()
.ok_or_else(|| format!("{section}.models 必须是数组"))?;
.ok_or_else(|| format!("{section}.models must be an array"))?;
let before = models.len();
models.retain(|model| !occupies_claude_client_alias(model, alias));
changed |= models.len() != before;
@@ -650,14 +650,14 @@ pub(crate) fn remove_oauth_claude_model_alias(
};
let oauth_aliases = oauth_aliases
.as_mapping_mut()
.ok_or_else(|| "oauth-model-alias 必须是 YAML 映射".to_string())?;
.ok_or_else(|| "oauth-model-alias must be a YAML mapping".to_string())?;
let mut changed = false;
let mut empty_channels = Vec::new();
for (channel, entries) in oauth_aliases.iter_mut() {
let channel_name = channel.as_str().unwrap_or("unknown");
let entries = entries
.as_sequence_mut()
.ok_or_else(|| format!("oauth-model-alias.{channel_name} 必须是数组"))?;
.ok_or_else(|| format!("oauth-model-alias.{channel_name} must be an array"))?;
let before = entries.len();
entries.retain(|entry| {
if managed_only {
@@ -850,7 +850,7 @@ pub(crate) fn ensure_claude_desktop_model_alias(
return Ok(());
}
Err(format!(
"无法确定模型 {source_model} 的 CPA 配置来源,无法创建 Claude Desktop 别名 {alias}"
"Unable to determine the CPA configuration source for model {source_model}; cannot create Claude Desktop alias {alias}"
))
}
@@ -978,7 +978,7 @@ fn resolve_claude_desktop_alias_source(
};
let providers = providers
.as_sequence()
.ok_or_else(|| format!("{section} 必须是数组"))?;
.ok_or_else(|| format!("{section} must be an array"))?;
for (provider_index, provider) in providers.iter().enumerate() {
let Some(provider) = provider.as_mapping() else {
continue;
@@ -991,7 +991,7 @@ fn resolve_claude_desktop_alias_source(
};
let models = models
.as_sequence()
.ok_or_else(|| format!("{section}.models 必须是数组"))?;
.ok_or_else(|| format!("{section}.models must be an array"))?;
if let Some(model) = models.iter().find_map(|model| {
let (_, client_model, _) = configured_model_identity(model)?;
if !configured_provider_model_is_enabled(provider, &client_model) {
@@ -1043,7 +1043,7 @@ fn append_claude_desktop_model_alias(
.and_then(serde_norway::Value::as_mapping_mut)
.and_then(|provider| yaml_mapping_value_mut(provider, "models"))
.and_then(serde_norway::Value::as_sequence_mut)
.ok_or("模型来源配置已变化,请刷新后重试")?;
.ok_or("Model source configuration changed. Refresh and try again")?;
(models, model)
}
ClaudeDesktopAliasSource::OAuth { channel, mut model } => {
@@ -1051,12 +1051,12 @@ fn append_claude_desktop_model_alias(
.entry(yaml_key("oauth-model-alias"))
.or_insert_with(|| serde_norway::Value::Mapping(serde_norway::Mapping::new()))
.as_mapping_mut()
.ok_or("oauth-model-alias 必须是 YAML 映射")?;
.ok_or("oauth-model-alias must be a YAML mapping")?;
let models = aliases
.entry(yaml_key(&channel))
.or_insert_with(|| serde_norway::Value::Sequence(Vec::new()))
.as_sequence_mut()
.ok_or_else(|| format!("oauth-model-alias.{channel} 必须是数组"))?;
.ok_or_else(|| format!("oauth-model-alias.{channel} must be an array"))?;
model.insert(yaml_key("fork"), serde_norway::Value::Bool(true));
if oauth_alias_channel(&channel).is_some_and(|channel| channel.force_mapping) {
model.insert(yaml_key("force-mapping"), serde_norway::Value::Bool(true));
@@ -1069,7 +1069,7 @@ fn append_claude_desktop_model_alias(
serde_norway::Value::String(alias.to_string()),
);
let display_name = managed_claude_alias_display_name(alias)
.ok_or_else(|| format!("不支持的 Claude 托管别名: {alias}"))?;
.ok_or_else(|| format!("Unsupported Claude managed alias: {alias}"))?;
model.insert(
yaml_key("display-name"),
serde_norway::Value::String(display_name.to_string()),
@@ -1089,14 +1089,14 @@ pub(crate) fn append_managed_oauth_model_alias(
.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())?;
.ok_or_else(|| "oauth-model-alias must be a YAML mapping".to_string())?;
let channel_aliases = oauth_aliases
.entry(yaml_key(channel))
.or_insert_with(|| serde_norway::Value::Sequence(Vec::new()))
.as_sequence_mut()
.ok_or_else(|| format!("oauth-model-alias.{channel} 必须是数组"))?;
.ok_or_else(|| format!("oauth-model-alias.{channel} must be an array"))?;
let display_name = managed_claude_alias_display_name(alias)
.ok_or_else(|| format!("不支持的 Claude 托管别名: {alias}"))?;
.ok_or_else(|| format!("Unsupported Claude managed alias: {alias}"))?;
let mut alias_mapping = serde_norway::Mapping::new();
alias_mapping.insert(
yaml_key("name"),
@@ -1133,7 +1133,7 @@ pub(crate) async fn fetch_oauth_channel_model_definitions(
.send()
.await
.map_err(|error| {
format_management_request_error(&format!("读取 {channel} OAuth 模型定义失败"), &error)
format_management_request_error(&format!("Failed to read {channel} OAuth model definition"), &error)
})?;
let payload = read_management_value(response).await?;
parse_codex_model_definitions(&payload)
@@ -1229,10 +1229,10 @@ pub(crate) fn resolved_oauth_alias_sources(
capability: AliasSourceCapability,
) -> Result<Vec<ResolvedThinkingAliasSource>, String> {
let document = serde_norway::from_str::<serde_norway::Value>(content)
.map_err(|error| format!("解析内核 YAML 配置失败: {error}"))?;
.map_err(|error| format!("Failed to parse kernel YAML configuration: {error}"))?;
let root = document
.as_mapping()
.ok_or_else(|| "内核配置顶层必须是 YAML 映射".to_string())?;
.ok_or_else(|| "Kernel configuration root must be a YAML mapping".to_string())?;
let mut sources = Vec::new();
collect_config_thinking_alias_sources(
root,
@@ -1246,7 +1246,7 @@ pub(crate) fn resolved_oauth_alias_sources(
collect_config_thinking_alias_sources(
root,
"openai-compatibility",
"OpenAI 兼容",
"OpenAI-compatible",
"openai-compatible",
"openai",
available_models,
@@ -1356,7 +1356,7 @@ pub(crate) fn collect_config_thinking_alias_sources(
};
let providers = providers
.as_sequence()
.ok_or_else(|| format!("{section} 必须是数组"))?;
.ok_or_else(|| format!("{section} must be an array"))?;
for (provider_index, provider) in providers.iter().enumerate() {
let Some(provider) = provider.as_mapping() else {
continue;
@@ -1371,7 +1371,7 @@ pub(crate) fn collect_config_thinking_alias_sources(
thinking_alias_provider_name(provider, fallback_provider, provider_index);
let provider_revision = sha256_bytes(
serde_norway::to_string(provider)
.map_err(|error| format!("读取模型源配置失败: {error}"))?
.map_err(|error| format!("Failed to read model source configuration: {error}"))?
.as_bytes(),
);
let Some(models) = yaml_mapping_value(provider, "models") else {
@@ -1379,7 +1379,7 @@ pub(crate) fn collect_config_thinking_alias_sources(
};
let models = models
.as_sequence()
.ok_or_else(|| format!("{section}.models 必须是数组"))?;
.ok_or_else(|| format!("{section}.models must be an array"))?;
for (model_index, model) in models.iter().enumerate() {
let Some((upstream_model, client_model, display_name)) =
configured_model_identity(model)
@@ -1495,7 +1495,7 @@ pub(crate) fn configured_model_identity(
pub(crate) fn thinking_aliases_from_yaml(content: &str) -> Result<Vec<ThinkingAliasEntry>, String> {
let document = serde_norway::from_str::<serde_norway::Value>(content)
.map_err(|error| format!("解析内核 YAML 配置失败: {error}"))?;
.map_err(|error| format!("Failed to parse kernel YAML configuration: {error}"))?;
thinking_aliases_from_value(&document)
}
@@ -1504,17 +1504,17 @@ pub(crate) fn thinking_aliases_from_value(
) -> Result<Vec<ThinkingAliasEntry>, String> {
let root = document
.as_mapping()
.ok_or_else(|| "内核配置顶层必须是 YAML 映射".to_string())?;
.ok_or_else(|| "Kernel configuration root must be a YAML mapping".to_string())?;
let mut entries = Vec::new();
if let Some(oauth_aliases) = yaml_mapping_value(root, "oauth-model-alias") {
let oauth_aliases = oauth_aliases
.as_mapping()
.ok_or_else(|| "oauth-model-alias 必须是 YAML 映射".to_string())?;
.ok_or_else(|| "oauth-model-alias must be a YAML mapping".to_string())?;
for (channel, channel_aliases) in oauth_aliases {
let channel = channel.as_str().unwrap_or("unknown");
let channel_aliases = channel_aliases
.as_sequence()
.ok_or_else(|| format!("oauth-model-alias.{channel} 必须是数组"))?;
.ok_or_else(|| format!("oauth-model-alias.{channel} must be an array"))?;
let (provider, kind, protocol) = oauth_alias_channel_details(channel);
for entry in channel_aliases {
let Some(mapping) = entry.as_mapping() else {
@@ -1556,7 +1556,7 @@ pub(crate) fn thinking_aliases_from_value(
collect_config_thinking_alias_entries(
root,
"openai-compatibility",
"OpenAI 兼容",
"OpenAI-compatible",
"openai-compatible",
"openai",
&mut entries,
@@ -1592,7 +1592,7 @@ pub(crate) fn thinking_aliases_from_value(
pub(crate) fn speed_aliases_from_yaml(content: &str) -> Result<Vec<SpeedAliasEntry>, String> {
let document = serde_norway::from_str::<serde_norway::Value>(content)
.map_err(|error| format!("解析内核 YAML 配置失败: {error}"))?;
.map_err(|error| format!("Failed to parse kernel YAML configuration: {error}"))?;
speed_aliases_from_value(&document)
}
@@ -1601,17 +1601,17 @@ pub(crate) fn speed_aliases_from_value(
) -> Result<Vec<SpeedAliasEntry>, String> {
let root = document
.as_mapping()
.ok_or_else(|| "内核配置顶层必须是 YAML 映射".to_string())?;
.ok_or_else(|| "Kernel configuration root must be a YAML mapping".to_string())?;
let mut entries = Vec::new();
if let Some(oauth_aliases) = yaml_mapping_value(root, "oauth-model-alias") {
let oauth_aliases = oauth_aliases
.as_mapping()
.ok_or_else(|| "oauth-model-alias 必须是 YAML 映射".to_string())?;
.ok_or_else(|| "oauth-model-alias must be a YAML mapping".to_string())?;
for (channel, channel_aliases) in oauth_aliases {
let channel = channel.as_str().unwrap_or("unknown");
let channel_aliases = channel_aliases
.as_sequence()
.ok_or_else(|| format!("oauth-model-alias.{channel} 必须是数组"))?;
.ok_or_else(|| format!("oauth-model-alias.{channel} must be an array"))?;
let (provider, kind, protocol) = oauth_alias_channel_details(channel);
for entry in channel_aliases {
let Some(mapping) = entry.as_mapping() else {
@@ -1657,7 +1657,7 @@ pub(crate) fn speed_aliases_from_value(
collect_config_speed_alias_entries(
root,
"openai-compatibility",
"OpenAI 兼容",
"OpenAI-compatible",
"openai-compatible",
"openai",
&mut entries,
@@ -1688,7 +1688,7 @@ pub(crate) fn collect_config_thinking_alias_entries(
};
let providers = providers
.as_sequence()
.ok_or_else(|| format!("{section} 必须是数组"))?;
.ok_or_else(|| format!("{section} must be an array"))?;
for (provider_index, provider) in providers.iter().enumerate() {
let Some(provider) = provider.as_mapping() else {
continue;
@@ -1700,7 +1700,7 @@ pub(crate) fn collect_config_thinking_alias_entries(
};
let models = models
.as_sequence()
.ok_or_else(|| format!("{section}.models 必须是数组"))?;
.ok_or_else(|| format!("{section}.models must be an array"))?;
for model in models {
let Some((source_model, alias, _)) = configured_model_identity(model) else {
continue;
@@ -1738,7 +1738,7 @@ pub(crate) fn collect_config_speed_alias_entries(
};
let providers = providers
.as_sequence()
.ok_or_else(|| format!("{section} 必须是数组"))?;
.ok_or_else(|| format!("{section} must be an array"))?;
for (provider_index, provider) in providers.iter().enumerate() {
let Some(provider) = provider.as_mapping() else {
continue;
@@ -1750,7 +1750,7 @@ pub(crate) fn collect_config_speed_alias_entries(
};
let models = models
.as_sequence()
.ok_or_else(|| format!("{section}.models 必须是数组"))?;
.ok_or_else(|| format!("{section}.models must be an array"))?;
for model in models {
let Some((source_model, alias, _)) = configured_model_identity(model) else {
continue;
@@ -1960,7 +1960,7 @@ pub(crate) fn insert_thinking_effort_params(
effort,
),
protocol => {
return Err(format!("暂不支持为 {protocol} 来源强制覆写思考强度"));
return Err(format!("Forcing a reasoning effort override is not yet supported for {protocol} sources"));
}
},
}
@@ -1975,17 +1975,17 @@ pub(crate) fn add_model_alias_to_yaml(
fast: bool,
) -> Result<String, String> {
if fast && !alias_source_supports_fast(source) {
return Err("Fast 仅支持 OpenAI 兼容 API、Codex API 或 Codex OAuth 模型源".to_string());
return Err("Fast supports only OpenAI-compatible API, Codex API, or Codex OAuth model sources".to_string());
}
let mut document = yaml_serde_edit::YamlValue::parse(content)
.map_err(|error| format!("解析内核 YAML 配置失败: {error}"))?;
.map_err(|error| format!("Failed to parse kernel YAML configuration: {error}"))?;
let mut updated = document.get().clone();
let root = updated
.as_mapping_mut()
.ok_or_else(|| "内核配置顶层必须是 YAML 映射".to_string())?;
.ok_or_else(|| "Kernel configuration root must be a YAML mapping".to_string())?;
if configured_model_alias_exists(root, alias) {
return Err(format!("别名模型 {alias} 已存在"));
return Err(format!("Alias model {alias} already exists"));
}
match &source.location {
@@ -2040,12 +2040,12 @@ pub(crate) fn append_alias_payload_override(
.entry(yaml_key("payload"))
.or_insert_with(|| serde_norway::Value::Mapping(serde_norway::Mapping::new()))
.as_mapping_mut()
.ok_or_else(|| "payload 必须是 YAML 映射".to_string())?;
.ok_or_else(|| "payload must be a YAML mapping".to_string())?;
let override_rules = payload
.entry(yaml_key("override"))
.or_insert_with(|| serde_norway::Value::Sequence(Vec::new()))
.as_sequence_mut()
.ok_or_else(|| "payload.override 必须是数组".to_string())?;
.ok_or_else(|| "payload.override must be an array".to_string())?;
let mut model_mapping = serde_norway::Mapping::new();
model_mapping.insert(
@@ -2075,17 +2075,17 @@ pub(crate) fn add_speed_alias_to_yaml(
alias: &str,
) -> Result<String, String> {
if !alias_source_supports_fast(source) {
return Err("Fast 仅支持 OpenAI 兼容 API、Codex API 或 Codex OAuth 模型源".to_string());
return Err("Fast supports only OpenAI-compatible API, Codex API, or Codex OAuth model sources".to_string());
}
let mut document = yaml_serde_edit::YamlValue::parse(content)
.map_err(|error| format!("解析内核 YAML 配置失败: {error}"))?;
.map_err(|error| format!("Failed to parse kernel YAML configuration: {error}"))?;
let mut updated = document.get().clone();
let root = updated
.as_mapping_mut()
.ok_or_else(|| "内核配置顶层必须是 YAML 映射".to_string())?;
.ok_or_else(|| "Kernel configuration root must be a YAML mapping".to_string())?;
if configured_model_alias_exists(root, alias) {
return Err(format!("别名模型 {alias} 已存在"));
return Err(format!("Alias model {alias} already exists"));
}
match &source.location {
@@ -2133,12 +2133,12 @@ pub(crate) fn append_oauth_model_alias(
.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())?;
.ok_or_else(|| "oauth-model-alias must be a YAML mapping".to_string())?;
let channel_aliases = oauth_aliases
.entry(yaml_key(channel))
.or_insert_with(|| serde_norway::Value::Sequence(Vec::new()))
.as_sequence_mut()
.ok_or_else(|| format!("oauth-model-alias.{channel} 必须是数组"))?;
.ok_or_else(|| format!("oauth-model-alias.{channel} must be an array"))?;
let mut alias_mapping = serde_norway::Mapping::new();
alias_mapping.insert(
yaml_key("name"),
@@ -2167,22 +2167,22 @@ pub(crate) fn append_config_thinking_alias(
) -> Result<(), String> {
let providers = yaml_mapping_value_mut(root, section)
.and_then(serde_norway::Value::as_sequence_mut)
.ok_or_else(|| format!("{section} 必须是数组"))?;
.ok_or_else(|| format!("{section} must be an array"))?;
let provider = providers
.get_mut(provider_index)
.and_then(serde_norway::Value::as_mapping_mut)
.ok_or_else(|| "模型提供商已经变化,请刷新后重试".to_string())?;
.ok_or_else(|| "Model provider changed. Refresh and try again".to_string())?;
let models = yaml_mapping_value_mut(provider, "models")
.and_then(serde_norway::Value::as_sequence_mut)
.ok_or_else(|| format!("{section}.models 必须是数组"))?;
.ok_or_else(|| format!("{section}.models must be an array"))?;
let source = models
.get(model_index)
.cloned()
.ok_or_else(|| "原模型已经变化,请刷新后重试".to_string())?;
.ok_or_else(|| "Source model changed. Refresh and try again".to_string())?;
let (_, current_model, _) =
configured_model_identity(&source).ok_or_else(|| "原模型配置格式无效".to_string())?;
configured_model_identity(&source).ok_or_else(|| "Invalid source model configuration format".to_string())?;
if !current_model.eq_ignore_ascii_case(expected_model) {
return Err("原模型已经变化,请刷新后重试".to_string());
return Err("Source model changed. Refresh and try again".to_string());
}
let mut alias_model = source.as_mapping().cloned().unwrap_or_else(|| {
let mut mapping = serde_norway::Mapping::new();
@@ -2223,22 +2223,22 @@ pub(crate) fn append_config_speed_alias(
) -> Result<(), String> {
let providers = yaml_mapping_value_mut(root, section)
.and_then(serde_norway::Value::as_sequence_mut)
.ok_or_else(|| format!("{section} 必须是数组"))?;
.ok_or_else(|| format!("{section} must be an array"))?;
let provider = providers
.get_mut(provider_index)
.and_then(serde_norway::Value::as_mapping_mut)
.ok_or_else(|| "模型提供商已经变化,请刷新后重试".to_string())?;
.ok_or_else(|| "Model provider changed. Refresh and try again".to_string())?;
let models = yaml_mapping_value_mut(provider, "models")
.and_then(serde_norway::Value::as_sequence_mut)
.ok_or_else(|| format!("{section}.models 必须是数组"))?;
.ok_or_else(|| format!("{section}.models must be an array"))?;
let source = models
.get(model_index)
.cloned()
.ok_or_else(|| "原模型已经变化,请刷新后重试".to_string())?;
.ok_or_else(|| "Source model changed. Refresh and try again".to_string())?;
let (_, current_model, _) =
configured_model_identity(&source).ok_or_else(|| "原模型配置格式无效".to_string())?;
configured_model_identity(&source).ok_or_else(|| "Invalid source model configuration format".to_string())?;
if !current_model.eq_ignore_ascii_case(expected_model) {
return Err("原模型已经变化,请刷新后重试".to_string());
return Err("Source model changed. Refresh and try again".to_string());
}
let mut alias_model = source.as_mapping().cloned().unwrap_or_else(|| {
let mut mapping = serde_norway::Mapping::new();
@@ -2281,11 +2281,11 @@ pub(crate) fn remove_thinking_alias_from_yaml_for_channel(
oauth_channel: Option<&str>,
) -> Result<String, String> {
let mut document = yaml_serde_edit::YamlValue::parse(content)
.map_err(|error| format!("解析内核 YAML 配置失败: {error}"))?;
.map_err(|error| format!("Failed to parse kernel YAML configuration: {error}"))?;
let mut updated = document.get().clone();
let root = updated
.as_mapping_mut()
.ok_or_else(|| "内核配置顶层必须是 YAML 映射".to_string())?;
.ok_or_else(|| "Kernel configuration root must be a YAML mapping".to_string())?;
let mut removed = remove_oauth_model_alias(root, alias, oauth_channel)?;
if oauth_channel.is_none() {
removed |= remove_config_model_alias(root, "codex-api-key", alias)?;
@@ -2294,7 +2294,7 @@ pub(crate) fn remove_thinking_alias_from_yaml_for_channel(
removed |= remove_config_model_alias(root, "gemini-api-key", alias)?;
}
if !removed {
return Err(format!("别名模型 {alias} 不存在,请刷新后重试"));
return Err(format!("Alias model {alias} does not exist. Refresh and try again"));
}
let scope = AliasPayloadScope::after_removal(root, alias, oauth_channel);
remove_alias_payload_options(root, alias, &scope)?;
@@ -2312,18 +2312,18 @@ pub(crate) fn remove_speed_alias_from_yaml_for_channel(
oauth_channel: Option<&str>,
) -> Result<String, String> {
let mut document = yaml_serde_edit::YamlValue::parse(content)
.map_err(|error| format!("解析内核 YAML 配置失败: {error}"))?;
.map_err(|error| format!("Failed to parse kernel YAML configuration: {error}"))?;
let mut updated = document.get().clone();
let root = updated
.as_mapping_mut()
.ok_or_else(|| "内核配置顶层必须是 YAML 映射".to_string())?;
.ok_or_else(|| "Kernel configuration root must be a YAML mapping".to_string())?;
let mut removed = remove_oauth_model_alias(root, alias, oauth_channel)?;
if oauth_channel.is_none() {
removed |= remove_config_speed_alias(root, "codex-api-key", "codex", alias)?;
removed |= remove_config_speed_alias(root, "openai-compatibility", "openai", alias)?;
}
if !removed {
return Err(format!("别名模型 {alias} 不存在,请刷新后重试"));
return Err(format!("Alias model {alias} does not exist. Refresh and try again"));
}
let scope = AliasPayloadScope::after_removal(root, alias, oauth_channel);
remove_alias_payload_options(root, alias, &scope)?;
@@ -2340,7 +2340,7 @@ pub(crate) fn remove_oauth_model_alias(
};
let oauth_aliases = oauth_aliases
.as_mapping_mut()
.ok_or_else(|| "oauth-model-alias 必须是 YAML 映射".to_string())?;
.ok_or_else(|| "oauth-model-alias must be a YAML mapping".to_string())?;
let mut removed = false;
let mut empty_channels = Vec::new();
for (channel, entries) in oauth_aliases.iter_mut() {
@@ -2350,7 +2350,7 @@ pub(crate) fn remove_oauth_model_alias(
}
let entries = entries
.as_sequence_mut()
.ok_or_else(|| format!("oauth-model-alias.{channel_name} 必须是数组"))?;
.ok_or_else(|| format!("oauth-model-alias.{channel_name} must be an array"))?;
entries.retain(|entry| {
let matches = entry
.as_mapping()
@@ -2414,7 +2414,7 @@ pub(crate) fn remove_config_model_alias(
};
let providers = providers
.as_sequence_mut()
.ok_or_else(|| format!("{section} 必须是数组"))?;
.ok_or_else(|| format!("{section} must be an array"))?;
let mut removed = false;
for provider in providers {
let Some(provider) = provider.as_mapping_mut() else {
@@ -2425,7 +2425,7 @@ pub(crate) fn remove_config_model_alias(
};
let models = models
.as_sequence_mut()
.ok_or_else(|| format!("{section}.models 必须是数组"))?;
.ok_or_else(|| format!("{section}.models must be an array"))?;
models.retain(|model| {
let matches = configured_model_identity(model)
.map(|(source, model_alias, _)| {
@@ -2453,7 +2453,7 @@ pub(crate) fn remove_config_speed_alias(
};
let providers = providers
.as_sequence_mut()
.ok_or_else(|| format!("{section} 必须是数组"))?;
.ok_or_else(|| format!("{section} must be an array"))?;
let mut removed = false;
for provider in providers {
let Some(provider) = provider.as_mapping_mut() else {
@@ -2464,7 +2464,7 @@ pub(crate) fn remove_config_speed_alias(
};
let models = models
.as_sequence_mut()
.ok_or_else(|| format!("{section}.models 必须是数组"))?;
.ok_or_else(|| format!("{section}.models must be an array"))?;
models.retain(|model| {
let matches = configured_model_identity(model)
.map(|(source, model_alias, _)| {
@@ -2572,14 +2572,14 @@ fn remove_alias_payload_options(
let Some(payload) = yaml_mapping_value_mut(root, "payload") else {
return Ok(());
};
let payload = payload.as_mapping_mut().ok_or("payload 必须是 YAML 映射")?;
let payload = payload.as_mapping_mut().ok_or("payload must be a YAML mapping")?;
for section in ["override", "override-raw"] {
let Some(rules) = yaml_mapping_value_mut(payload, section) else {
continue;
};
let rules = rules
.as_sequence_mut()
.ok_or_else(|| format!("payload.{section} 必须是数组"))?;
.ok_or_else(|| format!("payload.{section} must be an array"))?;
let mut next = Vec::with_capacity(rules.len());
for rule in rules.iter() {
let Some(mapping) = rule.as_mapping() else {
@@ -2606,7 +2606,7 @@ fn remove_alias_payload_options(
};
let models = models
.as_sequence()
.ok_or_else(|| format!("payload.{section}.models 必须是数组"))?;
.ok_or_else(|| format!("payload.{section}.models must be an array"))?;
let (target, others): (Vec<_>, Vec<_>) = models
.iter()
.cloned()
@@ -2649,12 +2649,12 @@ pub(crate) fn render_updated_core_yaml(
let rendered = expand_top_level_flow_style_collections(&document.get_string(), document.get())?;
let rendered = indent_indentationless_yaml_sequences(&rendered);
let validated = serde_norway::from_str::<serde_norway::Value>(&rendered)
.map_err(|error| format!("验证更新后的内核配置失败: {error}"))?;
.map_err(|error| format!("Failed to validate updated kernel configuration: {error}"))?;
if &validated != document.get() {
let path = first_yaml_mismatch_path(document.get(), &validated, &mut Vec::new())
.unwrap_or_else(|| "<unknown>".to_string());
return Err(format!(
"更新后的内核配置与预期值不一致(路径: {path}),已拒绝写入"
"Updated kernel configuration does not match the expected value (path: {path}); write rejected"
));
}
Ok(rendered)
@@ -2666,7 +2666,7 @@ pub(crate) fn expand_top_level_flow_style_collections(
) -> Result<String, String> {
let root = document
.as_mapping()
.ok_or_else(|| "内核配置顶层必须是 YAML 映射".to_string())?;
.ok_or_else(|| "Kernel configuration root must be a YAML mapping".to_string())?;
let mut rendered = content.to_string();
for (key, value) in root {
let Some(key) = key.as_str() else {
@@ -2683,7 +2683,7 @@ pub(crate) fn expand_top_level_flow_style_collections(
let mut wrapper = serde_norway::Mapping::new();
wrapper.insert(yaml_key(key), value.clone());
let block = serde_norway::to_string(&serde_norway::Value::Mapping(wrapper))
.map_err(|error| format!("格式化内核 YAML 配置失败: {error}"))?;
.map_err(|error| format!("Failed to format kernel YAML configuration: {error}"))?;
rendered = replace_top_level_yaml_block(&rendered, key, &block);
}
Ok(rendered)
@@ -2783,13 +2783,13 @@ pub(crate) fn truncate_for_error(value: &str) -> String {
pub(crate) fn open_external_url_inner(app: &tauri::AppHandle, url: &str) -> Result<(), String> {
let url = url.trim();
if url.is_empty() {
return Err("链接为空".to_string());
return Err("Link is empty".to_string());
}
if !(url.starts_with("http://") || url.starts_with("https://")) {
return Err("只允许打开 http/https 链接".to_string());
return Err("Only http/https links can be opened".to_string());
}
app.opener()
.open_url(url, None::<&str>)
.map_err(|err| format!("打开浏览器失败: {err}"))
.map_err(|err| format!("Failed to open browser: {err}"))
}
+16 -16
View File
@@ -63,13 +63,13 @@ pub(crate) fn save_gui_settings(
settings: GuiNetworkSettings,
) -> Result<GuiSettings, String> {
if settings.port == 0 {
return Err("端口必须在 1 到 65535 之间".to_string());
return Err("Port must be between 1 and 65535".to_string());
}
let _refresh_guard = cache
.refresh_lock
.lock()
.map_err(|_| "智能体配置状态刷新锁已损坏".to_string())?;
.map_err(|_| "Agent configuration status refresh lock is poisoned".to_string())?;
let previous = gui_config_state.snapshot()?;
let mut next = previous.clone();
next.port = settings.port;
@@ -87,7 +87,7 @@ pub(crate) fn save_gui_settings(
let rollback_error = patch_core_network_settings(&previous).err();
return Err(match rollback_error {
Some(rollback_error) => {
format!("{error};回滚内核网络配置也失败: {rollback_error}")
format!("{error}; failed to roll back kernel network configuration: {rollback_error}")
}
None => error,
});
@@ -96,7 +96,7 @@ pub(crate) fn save_gui_settings(
if config.port != previous.port {
if let Err(error) = cache.clear() {
eprintln!("清空智能体配置状态缓存失败: {error}");
eprintln!("Failed to clear agent configuration status cache: {error}");
}
}
@@ -110,15 +110,15 @@ pub(crate) fn save_network_routing_settings(
settings: GuiNetworkRoutingSettings,
) -> Result<CoreConfigView, String> {
if settings.port == 0 {
return Err("端口必须在 1 到 65535 之间".to_string());
return Err("Port must be between 1 and 65535".to_string());
}
let routing_session_affinity_ttl =
normalize_optional_config_string(settings.routing_session_affinity_ttl, "会话粘性 TTL")?;
normalize_optional_config_string(settings.routing_session_affinity_ttl, "Session affinity TTL")?;
let _refresh_guard = cache
.refresh_lock
.lock()
.map_err(|_| "智能体配置状态刷新锁已损坏".to_string())?;
.map_err(|_| "Agent configuration status refresh lock is poisoned".to_string())?;
let previous = gui_config_state.snapshot()?;
let mut next = previous.clone();
next.port = settings.port;
@@ -149,7 +149,7 @@ pub(crate) fn save_network_routing_settings(
if config.port != previous.port {
if let Err(error) = cache.clear() {
eprintln!("清空智能体配置状态缓存失败: {error}");
eprintln!("Failed to clear agent configuration status cache: {error}");
}
}
@@ -273,7 +273,7 @@ pub(crate) fn save_core_logging_settings(
settings: CoreLoggingSettingsInput,
) -> Result<CoreConfigView, String> {
if !(1..=3600).contains(&settings.redis_usage_queue_retention_seconds) {
return Err("Redis 用量队列保留时间必须在 1 到 3600 秒之间".to_string());
return Err("Redis usage queue retention must be between 1 and 3600 seconds".to_string());
}
let previous = current_core_config_settings(gui_config_state.inner())?;
@@ -313,7 +313,7 @@ pub(crate) fn add_core_api_key(
.iter()
.any(|existing| existing == &api_key)
{
return Err("该鉴权密钥已经存在".to_string());
return Err("This authentication key already exists".to_string());
}
settings.api_keys.push(api_key);
patch_core_api_keys(&settings.api_keys)?;
@@ -333,13 +333,13 @@ pub(crate) fn replace_core_api_key_value(
let index = api_keys
.iter()
.position(|existing| existing == original_api_key)
.ok_or_else(|| "要编辑的鉴权密钥不存在,请刷新后重试".to_string())?;
.ok_or_else(|| "The authentication key to edit does not exist. Refresh and try again".to_string())?;
if replacement_api_key != original_api_key
&& api_keys
.iter()
.any(|existing| existing == &replacement_api_key)
{
return Err("该鉴权密钥已经存在".to_string());
return Err("This authentication key already exists".to_string());
}
api_keys[index] = replacement_api_key;
Ok(())
@@ -352,7 +352,7 @@ pub(crate) fn remove_core_api_key_value(
let index = api_keys
.iter()
.position(|existing| existing == api_key)
.ok_or_else(|| "要删除的鉴权密钥不存在,请刷新后重试".to_string())?;
.ok_or_else(|| "The authentication key to delete does not exist. Refresh and try again".to_string())?;
api_keys.remove(index);
Ok(())
}
@@ -368,7 +368,7 @@ pub(crate) fn update_core_api_key(
let api_key = api_key.trim().to_string();
let remark = remark.trim().to_string();
if original_api_key.is_empty() {
return Err("要编辑的鉴权密钥不能为空".to_string());
return Err("The authentication key to edit cannot be empty".to_string());
}
validate_core_api_key(&api_key)?;
validate_api_key_remark(&remark)?;
@@ -393,7 +393,7 @@ pub(crate) fn delete_core_api_key(
) -> Result<CoreConfigView, String> {
let api_key = api_key.trim();
if api_key.is_empty() {
return Err("要删除的鉴权密钥不能为空".to_string());
return Err("The authentication key to delete cannot be empty".to_string());
}
let mut settings = current_core_config_settings(gui_config_state.inner())?;
remove_core_api_key_value(&mut settings.api_keys, api_key)?;
@@ -515,7 +515,7 @@ pub(crate) fn set_core_session_affinity_ttl(
gui_config_state: tauri::State<'_, GuiConfigState>,
ttl: String,
) -> Result<CoreConfigView, String> {
let ttl = normalize_optional_config_string(ttl, "会话粘性 TTL")?;
let ttl = normalize_optional_config_string(ttl, "Session affinity TTL")?;
let mut settings = current_core_config_settings(gui_config_state.inner())?;
let previous_ttl = settings.routing_session_affinity_ttl.clone();
settings.routing_session_affinity_ttl = ttl;
+84 -84
View File
@@ -2,23 +2,23 @@ use super::*;
pub(crate) fn validate_core_api_key(api_key: &str) -> Result<(), String> {
if api_key.is_empty() {
return Err("鉴权密钥不能为空".to_string());
return Err("Authentication key cannot be empty".to_string());
}
if !api_key.bytes().all(|byte| (0x21..=0x7e).contains(&byte)) {
return Err("鉴权密钥只能包含 ASCII 可见字符,且不能包含空格".to_string());
return Err("Authentication key may contain only visible ASCII characters and cannot contain spaces".to_string());
}
if is_example_core_api_key(api_key) {
return Err("不能使用内核模板里的示例鉴权密钥".to_string());
return Err("The example authentication key from the kernel template cannot be used".to_string());
}
Ok(())
}
pub(crate) fn validate_api_key_remark(remark: &str) -> Result<(), String> {
if remark.chars().count() > 80 {
return Err("密钥备注不能超过 80 个字符".to_string());
return Err("Key note cannot exceed 80 characters".to_string());
}
if remark.chars().any(char::is_control) {
return Err("密钥备注不能包含换行或控制字符".to_string());
return Err("Key note cannot contain line breaks or control characters".to_string());
}
Ok(())
}
@@ -30,7 +30,7 @@ pub(crate) fn validate_api_access_provider_section(section: &str) -> Result<(),
) {
Ok(())
} else {
Err("API 接入类型无效".to_string())
Err("Invalid API access type".to_string())
}
}
@@ -84,10 +84,10 @@ impl GuiConfigFile {
pub(crate) fn validate_management_secret_key(secret_key: &str) -> Result<(), String> {
if secret_key.chars().count() > 512 {
return Err("管理密钥不能超过 512 个字符".to_string());
return Err("Management key cannot exceed 512 characters".to_string());
}
if secret_key.chars().any(char::is_control) {
return Err("管理密钥不能包含控制字符".to_string());
return Err("Management key cannot contain control characters".to_string());
}
Ok(())
}
@@ -95,13 +95,13 @@ pub(crate) fn validate_management_secret_key(secret_key: &str) -> Result<(), Str
pub(crate) fn validate_strong_management_secret_key(secret_key: &str) -> Result<(), String> {
validate_management_secret_key(secret_key)?;
if secret_key.trim().is_empty() {
return Err("WebUI 密钥不能为空".to_string());
return Err("WebUI key cannot be empty".to_string());
}
if secret_key.trim() == LEGACY_DEFAULT_MANAGEMENT_SECRET_KEY {
return Err("不能继续使用旧版默认 WebUI 密钥 123456".to_string());
return Err("The legacy default WebUI key 123456 cannot be used".to_string());
}
if is_hashed_management_secret_key(secret_key) {
return Err("GUI 配置必须保存可用于管理接口认证的明文 WebUI 密钥".to_string());
return Err("GUI configuration must save a plaintext WebUI key that can authenticate with the management interface".to_string());
}
Ok(())
}
@@ -110,7 +110,7 @@ pub(crate) fn generate_management_secret_key() -> Result<String, String> {
use base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine as _};
let mut random = [0_u8; 32];
getrandom::fill(&mut random).map_err(|error| format!("生成 WebUI 安全密钥失败: {error}"))?;
getrandom::fill(&mut random).map_err(|error| format!("Failed to generate WebUI security key: {error}"))?;
Ok(format!("wui-Aa9_{}", URL_SAFE_NO_PAD.encode(random)))
}
@@ -158,7 +158,7 @@ pub(crate) fn validate_routing_strategy(strategy: &str) -> Result<(), String> {
if matches!(strategy, "round-robin" | "fill-first") {
return Ok(());
}
Err("路由策略只支持 round-robin 或 fill-first".to_string())
Err("Routing strategy supports only round-robin or fill-first".to_string())
}
pub(crate) fn normalize_optional_config_string(
@@ -167,7 +167,7 @@ pub(crate) fn normalize_optional_config_string(
) -> Result<String, String> {
let value = value.trim().to_string();
if value.chars().any(char::is_control) {
return Err(format!("{field_name} 不能包含控制字符"));
return Err(format!("{field_name} cannot contain control characters"));
}
Ok(value)
}
@@ -251,7 +251,7 @@ pub(crate) fn config_update_error_with_rollback(
rollback_error: Option<String>,
) -> String {
match rollback_error {
Some(rollback_error) => format!("{error};回滚内核配置也失败: {rollback_error}"),
Some(rollback_error) => format!("{error}; failed to roll back kernel configuration: {rollback_error}"),
None => error,
}
}
@@ -275,9 +275,9 @@ pub(crate) fn merge_core_config_fields(
let current_value = current
.map(|current| {
let current = serde_norway::from_str::<serde_norway::Value>(current)
.map_err(|err| format!("解析现有内核配置失败,为避免配置丢失已停止写入: {err}"))?;
.map_err(|err| format!("Failed to parse existing kernel configuration; writing stopped to prevent configuration loss: {err}"))?;
if !current.is_mapping() {
return Err("现有内核配置根节点必须是 YAML 映射,已停止写入".to_string());
return Err("Existing kernel configuration root must be a YAML mapping; writing stopped".to_string());
}
Ok(current)
})
@@ -290,9 +290,9 @@ pub(crate) fn merge_core_config_value(
current: Option<serde_norway::Value>,
) -> Result<String, String> {
let template_value = serde_norway::from_str::<serde_norway::Value>(template)
.map_err(|err| format!("解析内核配置模板失败: {err}"))?;
.map_err(|err| format!("Failed to parse kernel configuration template: {err}"))?;
if !template_value.is_mapping() {
return Err("内核配置模板根节点必须是 YAML 映射".to_string());
return Err("Kernel configuration template root must be a YAML mapping".to_string());
}
let mut merged = template_value.clone();
@@ -302,9 +302,9 @@ pub(crate) fn merge_core_config_value(
let rendered = render_yaml_value_changes(template, &template_value, &merged)?;
let rendered_value = serde_norway::from_str::<serde_norway::Value>(&rendered)
.map_err(|err| format!("验证迁移后的内核配置失败: {err}"))?;
.map_err(|err| format!("Failed to validate migrated kernel configuration: {err}"))?;
if !rendered_value.is_mapping() {
return Err("迁移后的内核配置根节点必须是 YAML 映射".to_string());
return Err("Migrated kernel configuration root must be a YAML mapping".to_string());
}
Ok(rendered)
}
@@ -353,26 +353,26 @@ pub(crate) fn patch_core_network_routing_yaml(
document,
"request-retry",
serde_norway::to_value(config.request_retry)
.map_err(|err| format!("序列化请求重试次数失败: {err}"))?,
.map_err(|err| format!("Failed to serialize request retry count: {err}"))?,
)?;
set_core_yaml_top_level_value(
document,
"max-retry-credentials",
serde_norway::to_value(config.max_retry_credentials)
.map_err(|err| format!("序列化最大重试凭据数失败: {err}"))?,
.map_err(|err| format!("Failed to serialize maximum retry credentials: {err}"))?,
)?;
set_core_yaml_top_level_value(
document,
"max-retry-interval",
serde_norway::to_value(config.max_retry_interval)
.map_err(|err| format!("序列化最大重试等待时间失败: {err}"))?,
.map_err(|err| format!("Failed to serialize maximum retry interval: {err}"))?,
)?;
set_core_yaml_nested_value(
document,
"streaming",
"bootstrap-retries",
serde_norway::to_value(config.streaming_bootstrap_retries)
.map_err(|err| format!("序列化流式启动重试次数失败: {err}"))?,
.map_err(|err| format!("Failed to serialize streaming bootstrap retry count: {err}"))?,
)?;
Ok(*document != original)
})
@@ -477,7 +477,7 @@ pub(crate) fn apply_network_settings(
) -> Result<(), String> {
let mapping = document
.as_mapping_mut()
.ok_or_else(|| "内核配置顶层必须是 YAML 映射".to_string())?;
.ok_or_else(|| "Kernel configuration root must be a YAML mapping".to_string())?;
let host = config.host.trim();
mapping.insert(
@@ -486,7 +486,7 @@ pub(crate) fn apply_network_settings(
);
mapping.insert(
serde_norway::Value::String("port".to_string()),
serde_norway::to_value(config.port).map_err(|err| format!("序列化内核端口失败: {err}"))?,
serde_norway::to_value(config.port).map_err(|err| format!("Failed to serialize kernel port: {err}"))?,
);
Ok(())
}
@@ -507,7 +507,7 @@ pub(crate) fn apply_gui_managed_settings(
document,
"port",
serde_norway::to_value(config.port)
.map_err(|err| format!("序列化内核端口失败: {err}"))?,
.map_err(|err| format!("Failed to serialize kernel port: {err}"))?,
)?;
changed |= set_core_yaml_top_level_value(
document,
@@ -533,13 +533,13 @@ pub(crate) fn apply_gui_managed_settings(
document,
"logs-max-total-size-mb",
serde_norway::to_value(config.logs_max_total_size_mb)
.map_err(|err| format!("序列化日志容量限制失败: {err}"))?,
.map_err(|err| format!("Failed to serialize log size limit: {err}"))?,
)?;
changed |= set_core_yaml_top_level_value(
document,
"error-logs-max-files",
serde_norway::to_value(config.error_logs_max_files)
.map_err(|err| format!("序列化错误日志保留数失败: {err}"))?,
.map_err(|err| format!("Failed to serialize error log retention count: {err}"))?,
)?;
changed |= set_core_yaml_top_level_value(
document,
@@ -550,7 +550,7 @@ pub(crate) fn apply_gui_managed_settings(
document,
"redis-usage-queue-retention-seconds",
serde_norway::to_value(config.redis_usage_queue_retention_seconds)
.map_err(|err| format!("序列化 Redis 用量队列保留时间失败: {err}"))?,
.map_err(|err| format!("Failed to serialize Redis usage queue retention: {err}"))?,
)?;
changed |= set_core_yaml_top_level_value(
document,
@@ -601,26 +601,26 @@ pub(crate) fn apply_gui_managed_settings(
document,
"request-retry",
serde_norway::to_value(config.request_retry)
.map_err(|err| format!("序列化请求重试次数失败: {err}"))?,
.map_err(|err| format!("Failed to serialize request retry count: {err}"))?,
)?;
changed |= set_core_yaml_top_level_value(
document,
"max-retry-credentials",
serde_norway::to_value(config.max_retry_credentials)
.map_err(|err| format!("序列化最大重试凭据数失败: {err}"))?,
.map_err(|err| format!("Failed to serialize maximum retry credentials: {err}"))?,
)?;
changed |= set_core_yaml_top_level_value(
document,
"max-retry-interval",
serde_norway::to_value(config.max_retry_interval)
.map_err(|err| format!("序列化最大重试等待时间失败: {err}"))?,
.map_err(|err| format!("Failed to serialize maximum retry interval: {err}"))?,
)?;
changed |= set_core_yaml_nested_value(
document,
"streaming",
"bootstrap-retries",
serde_norway::to_value(config.streaming_bootstrap_retries)
.map_err(|err| format!("序列化流式启动重试次数失败: {err}"))?,
.map_err(|err| format!("Failed to serialize streaming bootstrap retry count: {err}"))?,
)?;
Ok(changed)
})?
@@ -628,14 +628,14 @@ pub(crate) fn apply_gui_managed_settings(
let updated = patch_core_api_keys_yaml(&updated, &gui_api_key_values(&config.api_keys))?;
serde_norway::from_str::<serde_norway::Value>(&updated)
.map_err(|err| format!("验证启动内核配置失败: {err}"))?;
.map_err(|err| format!("Failed to validate kernel startup configuration: {err}"))?;
Ok(updated)
}
pub(crate) fn write_bytes_directly(path: &Path, content: &[u8]) -> Result<(), String> {
let directory = path.parent().unwrap_or_else(|| Path::new("."));
fs::create_dir_all(directory)
.map_err(|error| format!("创建配置目录失败 {}: {error}", path_to_string(directory)))?;
.map_err(|error| format!("Failed to create configuration directory {}: {error}", path_to_string(directory)))?;
let write_result = (|| -> io::Result<()> {
let mut file = fs::OpenOptions::new()
@@ -649,7 +649,7 @@ pub(crate) fn write_bytes_directly(path: &Path, content: &[u8]) -> Result<(), St
file.sync_all()
})();
write_result.map_err(|error| format!("直接写入配置失败 {}: {error}", path_to_string(path)))?;
write_result.map_err(|error| format!("Failed to write configuration directly {}: {error}", path_to_string(path)))?;
remember_software_write(path, content);
Ok(())
}
@@ -660,7 +660,7 @@ pub(crate) fn write_bytes_atomically(path: &Path, content: &[u8]) -> Result<(),
static SEQUENCE: AtomicU64 = AtomicU64::new(0);
let directory = path.parent().unwrap_or_else(|| Path::new("."));
fs::create_dir_all(directory)
.map_err(|error| format!("创建配置目录失败 {}: {error}", path_to_string(directory)))?;
.map_err(|error| format!("Failed to create configuration directory {}: {error}", path_to_string(directory)))?;
let file_name = path
.file_name()
.and_then(|name| name.to_str())
@@ -682,7 +682,7 @@ pub(crate) fn write_bytes_atomically(path: &Path, content: &[u8]) -> Result<(),
if let Err(error) = write_result {
let _ = fs::remove_file(&temporary_path);
return Err(format!(
"原子写入配置失败 {}: {error}",
"Failed to write configuration atomically {}: {error}",
path_to_string(path)
));
}
@@ -882,16 +882,16 @@ fn copy_auth_file_atomically(source: &Path, destination: &Path) -> Result<(), St
Ok(metadata) => {
if metadata.file_type().is_symlink() || !metadata.is_file() {
return Err(format!(
"OAuth 迁移目标不是普通文件: {}",
"OAuth migration target is not a regular file: {}",
path_to_string(destination)
));
}
let source_bytes = fs::read(source).map_err(|error| {
format!("读取旧 OAuth 文件失败 {}: {error}", path_to_string(source))
format!("Failed to read old OAuth file {}: {error}", path_to_string(source))
})?;
let destination_bytes = fs::read(destination).map_err(|error| {
format!(
"读取现有 OAuth 文件失败 {}: {error}",
"Failed to read existing OAuth file {}: {error}",
path_to_string(destination)
)
})?;
@@ -899,14 +899,14 @@ fn copy_auth_file_atomically(source: &Path, destination: &Path) -> Result<(), St
return Ok(());
}
return Err(format!(
"OAuth 迁移目标已存在不同内容,未覆盖: {}",
"OAuth migration target already contains different content and was not overwritten: {}",
path_to_string(destination)
));
}
Err(error) if error.kind() == io::ErrorKind::NotFound => {}
Err(error) => {
return Err(format!(
"检查 OAuth 迁移目标失败 {}: {error}",
"Failed to check OAuth migration target {}: {error}",
path_to_string(destination)
));
}
@@ -918,7 +918,7 @@ fn copy_auth_file_atomically(source: &Path, destination: &Path) -> Result<(), St
let directory = destination.parent().unwrap_or_else(|| Path::new("."));
fs::create_dir_all(directory).map_err(|error| {
format!(
"创建 OAuth 迁移目录失败 {}: {error}",
"Failed to create OAuth migration directory {}: {error}",
path_to_string(directory)
)
})?;
@@ -942,7 +942,7 @@ fn copy_auth_file_atomically(source: &Path, destination: &Path) -> Result<(), St
if let Err(error) = copy_result {
let _ = fs::remove_file(&temporary_path);
return Err(format!(
"复制 OAuth 文件失败 {} -> {}: {error}",
"Failed to copy OAuth file {} -> {}: {error}",
path_to_string(source),
path_to_string(destination)
));
@@ -957,21 +957,21 @@ fn copy_auth_directory(source: &Path, destination: &Path) -> Result<(), String>
Err(error) if error.kind() == io::ErrorKind::NotFound => {
return fs::create_dir_all(destination).map_err(|create_error| {
format!(
"创建 OAuth 目录失败 {}: {create_error}",
"Failed to create OAuth directory {}: {create_error}",
path_to_string(destination)
)
});
}
Err(error) => {
return Err(format!(
"检查旧 OAuth 目录失败 {}: {error}",
"Failed to check old OAuth directory {}: {error}",
path_to_string(source)
));
}
};
if source_metadata.file_type().is_symlink() || !source_metadata.is_dir() {
return Err(format!(
"旧 OAuth 路径不是普通目录: {}",
"Old OAuth path is not a regular directory: {}",
path_to_string(source)
));
}
@@ -979,7 +979,7 @@ fn copy_auth_directory(source: &Path, destination: &Path) -> Result<(), String>
match fs::symlink_metadata(destination) {
Ok(metadata) if metadata.file_type().is_symlink() || !metadata.is_dir() => {
return Err(format!(
"OAuth 迁移目标不是普通目录: {}",
"OAuth migration target is not a regular directory: {}",
path_to_string(destination)
));
}
@@ -987,37 +987,37 @@ fn copy_auth_directory(source: &Path, destination: &Path) -> Result<(), String>
Err(error) if error.kind() == io::ErrorKind::NotFound => {
fs::create_dir_all(destination).map_err(|create_error| {
format!(
"创建 OAuth 迁移目录失败 {}: {create_error}",
"Failed to create OAuth migration directory {}: {create_error}",
path_to_string(destination)
)
})?;
}
Err(error) => {
return Err(format!(
"检查 OAuth 迁移目录失败 {}: {error}",
"Failed to check OAuth migration directory {}: {error}",
path_to_string(destination)
));
}
}
for entry in fs::read_dir(source)
.map_err(|error| format!("读取旧 OAuth 目录失败 {}: {error}", path_to_string(source)))?
.map_err(|error| format!("Failed to read old OAuth directory {}: {error}", path_to_string(source)))?
{
let entry = entry.map_err(|error| {
format!(
"读取旧 OAuth 目录项失败 {}: {error}",
"Failed to read old OAuth directory entry {}: {error}",
path_to_string(source)
)
})?;
let file_type = entry.file_type().map_err(|error| {
format!(
"检查旧 OAuth 目录项失败 {}: {error}",
"Failed to check old OAuth directory entry {}: {error}",
path_to_string(&entry.path())
)
})?;
if file_type.is_symlink() {
return Err(format!(
"OAuth 迁移不接受符号链接: {}",
"OAuth migration does not accept symbolic links: {}",
path_to_string(&entry.path())
));
}
@@ -1028,7 +1028,7 @@ fn copy_auth_directory(source: &Path, destination: &Path) -> Result<(), String>
copy_auth_file_atomically(&entry.path(), &target)?;
} else {
return Err(format!(
"OAuth 迁移不支持该文件类型: {}",
"OAuth migration does not support this file type: {}",
path_to_string(&entry.path())
));
}
@@ -1049,7 +1049,7 @@ pub(crate) fn migrate_auth_dir_from_macos_app_bundle(
let destination = normalize_path_lexically(persistent_auth_dir);
if auth_dir_is_inside_macos_app_bundle(&destination) {
return Err(format!(
"OAuth 持久化目录不能位于 macOS 应用包内: {}",
"OAuth persistence directory cannot be inside a macOS application bundle: {}",
path_to_string(&destination)
));
}
@@ -1069,7 +1069,7 @@ fn migrate_packaged_macos_auth_dir(config: &mut GuiConfigFile) -> Result<bool, S
if migrated {
if let Err(error) = patch_core_auth_dir(&config.auth_dir) {
config.auth_dir = previous_auth_dir;
return Err(format!("更新内核 OAuth 目录失败: {error}"));
return Err(format!("Failed to update kernel OAuth directory: {error}"));
}
}
Ok(migrated)
@@ -1095,7 +1095,7 @@ pub(crate) fn load_or_create_gui_config() -> Result<GuiConfigFile, String> {
let (mut config, presence, mut changed, gui_parse_failed) = if gui_config_exists {
let content = fs::read_to_string(&config_path)
.map_err(|err| format!("读取 GUI 配置失败 {}: {err}", path_to_string(&config_path)))?;
.map_err(|err| format!("Failed to read GUI configuration {}: {err}", path_to_string(&config_path)))?;
match (
toml::from_str::<GuiConfigFile>(&content),
toml::from_str::<GuiConfigPresence>(&content),
@@ -1111,14 +1111,14 @@ pub(crate) fn load_or_create_gui_config() -> Result<GuiConfigFile, String> {
} else if legacy_config_exists {
let content = fs::read_to_string(&legacy_config_path).map_err(|err| {
format!(
"读取旧 GUI 配置失败 {}: {err}",
"Failed to read old GUI configuration {}: {err}",
path_to_string(&legacy_config_path)
)
})?;
let config = serde_yaml::from_str::<GuiConfigFile>(&content)
.map_err(|err| format!("解析旧 GUI 配置失败: {err}"))?;
.map_err(|err| format!("Failed to parse old GUI configuration: {err}"))?;
let presence = serde_yaml::from_str::<GuiConfigPresence>(&content)
.map_err(|err| format!("解析旧 GUI 配置字段失败: {err}"))?;
.map_err(|err| format!("Failed to parse old GUI configuration field: {err}"))?;
(config, presence, true, false)
} else {
(
@@ -1326,14 +1326,14 @@ pub(crate) fn load_or_create_gui_config() -> Result<GuiConfigFile, String> {
}
if management_secret_rotated {
if let Err(error) = patch_core_management_secret_key(&config.management_secret_key) {
eprintln!("更新旧版 CPA WebUI 密钥失败,将在下次启动内核时重试: {error}");
eprintln!("Failed to update legacy CPA WebUI key; will retry the next time the kernel starts: {error}");
}
}
if !config.auth_dir.trim().is_empty() {
let install_dir = core_install_dir()?;
let auth_dir = auth_dir_path_for_core(&config.auth_dir, &install_dir);
fs::create_dir_all(&auth_dir)
.map_err(|error| format!("创建凭证目录失败 {}: {error}", path_to_string(&auth_dir)))?;
.map_err(|error| format!("Failed to create credentials directory {}: {error}", path_to_string(&auth_dir)))?;
}
Ok(config)
}
@@ -1524,7 +1524,7 @@ pub(crate) fn restore_main_window_size(app: &tauri::AppHandle) -> Result<(), Str
};
let window = app
.get_webview_window("main")
.ok_or_else(|| "主窗口不存在,无法恢复窗口尺寸".to_string())?;
.ok_or_else(|| "Main window does not exist; cannot restore window size".to_string())?;
let restored_size = fit_window_size_to_current_monitor(&window, saved_size);
window
@@ -1532,7 +1532,7 @@ pub(crate) fn restore_main_window_size(app: &tauri::AppHandle) -> Result<(), Str
f64::from(restored_size.width),
f64::from(restored_size.height),
))
.map_err(|error| format!("恢复主窗口尺寸失败: {error}"))?;
.map_err(|error| format!("Failed to restore main window size: {error}"))?;
window_size_state.replace(restored_size)
}
@@ -1543,15 +1543,15 @@ pub(crate) fn persist_main_window_size(app: &tauri::AppHandle) -> Result<(), Str
None => {
let window = app
.get_webview_window("main")
.ok_or_else(|| "主窗口不存在,无法保存窗口尺寸".to_string())?;
.ok_or_else(|| "Main window does not exist; cannot save window size".to_string())?;
let physical_size = window
.inner_size()
.map_err(|error| format!("读取主窗口尺寸失败: {error}"))?;
.map_err(|error| format!("Failed to read main window size: {error}"))?;
let scale_factor = window
.scale_factor()
.map_err(|error| format!("读取主窗口缩放比例失败: {error}"))?;
.map_err(|error| format!("Failed to read main window scale factor: {error}"))?;
logical_window_size_from_physical(&physical_size, scale_factor)
.ok_or_else(|| "主窗口尺寸无效,已跳过保存".to_string())?
.ok_or_else(|| "Invalid main window size; save skipped".to_string())?
}
};
@@ -1854,7 +1854,7 @@ pub(crate) fn write_gui_config_to_path(
let content = document.to_string();
toml::from_str::<GuiConfigFile>(&content)
.map_err(|error| format!("验证 GUI 配置失败: {error}"))?;
.map_err(|error| format!("Failed to validate GUI configuration: {error}"))?;
if existing.as_deref() == Some(content.as_str()) {
return Ok(());
}
@@ -1863,19 +1863,19 @@ pub(crate) fn write_gui_config_to_path(
pub(crate) fn validate_gui_config(config: &GuiConfigFile) -> Result<(), String> {
if config.port == 0 {
return Err("GUI 配置端口必须在 1 到 65535 之间".to_string());
return Err("GUI configuration port must be between 1 and 65535".to_string());
}
if config.host.trim().is_empty() || config.host.chars().any(char::is_control) {
return Err("GUI 配置 host 无效".to_string());
return Err("Invalid GUI configuration host".to_string());
}
if config.auth_dir.trim().is_empty() || config.auth_dir.chars().any(char::is_control) {
return Err("凭证目录不能为空或包含控制字符".to_string());
return Err("Credentials directory cannot be empty or contain control characters".to_string());
}
#[cfg(target_os = "macos")]
{
let auth_dir = auth_dir_path_for_core(&config.auth_dir, &core_install_dir()?);
if auth_dir_is_inside_macos_app_bundle(&auth_dir) {
return Err("OAuth 凭证目录不能位于 macOS 应用包内".to_string());
return Err("OAuth credentials directory cannot be inside a macOS application bundle".to_string());
}
}
for entry in &config.api_keys {
@@ -1890,7 +1890,7 @@ pub(crate) fn validate_gui_config(config: &GuiConfigFile) -> Result<(), String>
.chars()
.all(|character| character.is_ascii_hexdigit())
{
return Err("API 接入备注的密钥指纹无效".to_string());
return Err("Invalid key fingerprint in API access note".to_string());
}
if !entry.record_hash.is_empty()
&& (entry.record_hash.len() != 64
@@ -1899,7 +1899,7 @@ pub(crate) fn validate_gui_config(config: &GuiConfigFile) -> Result<(), String>
.chars()
.all(|character| character.is_ascii_hexdigit()))
{
return Err("API 接入备注的条目指纹无效".to_string());
return Err("Invalid entry fingerprint in API access note".to_string());
}
validate_api_key_remark(&entry.remark)?;
}
@@ -1908,11 +1908,11 @@ pub(crate) fn validate_gui_config(config: &GuiConfigFile) -> Result<(), String>
if config.proxy_override {
network_proxy::normalize_optional_proxy_url(&config.proxy_url)?;
} else if config.proxy_url.chars().any(char::is_control) {
return Err("代理 URL 不能包含控制字符".to_string());
return Err("Proxy URL cannot contain control characters".to_string());
}
for url in &config.custom_download_mirrors {
if normalize_custom_download_mirror_url(url).as_deref() != Ok(url.as_str()) {
return Err(format!("自定义下载镜像地址无效: {url}"));
return Err(format!("Invalid custom download mirror URL: {url}"));
}
}
if config.download_source == VersionDownloadSource::Custom
@@ -1920,17 +1920,17 @@ pub(crate) fn validate_gui_config(config: &GuiConfigFile) -> Result<(), String>
.custom_download_mirrors
.contains(&config.active_custom_download_mirror)
{
return Err("当前选择的自定义下载镜像不存在".to_string());
return Err("The selected custom download mirror does not exist".to_string());
}
if config
.routing_session_affinity_ttl
.chars()
.any(char::is_control)
{
return Err("会话粘性 TTL 不能包含控制字符".to_string());
return Err("Session affinity TTL cannot contain control characters".to_string());
}
if !(1..=3600).contains(&config.redis_usage_queue_retention_seconds) {
return Err("Redis 用量队列保留时间必须在 1 到 3600 秒之间".to_string());
return Err("Redis usage queue retention must be between 1 and 3600 seconds".to_string());
}
Ok(())
}
+88 -88
View File
@@ -9,21 +9,21 @@ pub(crate) fn merge_core_config_for_start(
let example_config_path = install_dir.join(CORE_EXAMPLE_CONFIG_FILE);
if !example_config_path.is_file() {
return Err(format!(
"未找到内核配置模板: {}",
"Kernel configuration template not found: {}",
path_to_string(&example_config_path)
));
}
let template = fs::read_to_string(&example_config_path).map_err(|err| {
format!(
"读取内核配置模板失败 {}: {err}",
"Failed to read kernel configuration template {}: {err}",
path_to_string(&example_config_path)
)
})?;
let current = if config_path.is_file() {
Some(fs::read_to_string(&config_path).map_err(|err| {
format!(
"读取现有内核配置失败 {}: {err}",
"Failed to read existing kernel configuration {}: {err}",
path_to_string(&config_path)
)
})?)
@@ -45,7 +45,7 @@ pub(crate) fn patch_core_network_settings(config: &GuiConfigFile) -> Result<(),
}
let content = fs::read_to_string(&config_path)
.map_err(|err| format!("读取内核配置失败 {}: {err}", path_to_string(&config_path)))?;
.map_err(|err| format!("Failed to read kernel configuration {}: {err}", path_to_string(&config_path)))?;
let Some(updated) = patch_core_network_yaml(&content, config)? else {
return Ok(());
};
@@ -60,7 +60,7 @@ pub(crate) fn patch_core_network_routing_settings(config: &GuiConfigFile) -> Res
}
let content = fs::read_to_string(&config_path)
.map_err(|err| format!("读取内核配置失败 {}: {err}", path_to_string(&config_path)))?;
.map_err(|err| format!("Failed to read kernel configuration {}: {err}", path_to_string(&config_path)))?;
let Some(updated) = patch_core_network_routing_yaml(&content, config)? else {
return Ok(());
};
@@ -102,7 +102,7 @@ pub(crate) fn patch_core_session_routing_settings(config: &GuiConfigFile) -> Res
pub(crate) fn lock_core_config_file() -> Result<std::sync::MutexGuard<'static, ()>, String> {
CORE_CONFIG_FILE_LOCK
.lock()
.map_err(|_| "内核配置文件锁已损坏".to_string())
.map_err(|_| "Kernel configuration file lock is poisoned".to_string())
}
pub(crate) fn read_installed_core_config_settings() -> Result<CoreConfigSettings, String> {
@@ -245,7 +245,7 @@ pub(crate) fn core_sensitive_words_settings_from_value(
) -> Result<CoreSensitiveWordsSettings, String> {
let root = document
.as_mapping()
.ok_or_else(|| "内核配置顶层必须是 YAML 映射".to_string())?;
.ok_or_else(|| "Kernel configuration root must be a YAML mapping".to_string())?;
let read_words = |provider: &str| -> Result<Vec<String>, String> {
let Some(section) = yaml_mapping_value(root, provider) else {
return Ok(Vec::new());
@@ -255,7 +255,7 @@ pub(crate) fn core_sensitive_words_settings_from_value(
}
let section = section
.as_mapping()
.ok_or_else(|| format!("{provider} 必须是 YAML 映射"))?;
.ok_or_else(|| format!("{provider} must be a YAML mapping"))?;
let Some(value) = yaml_mapping_value(section, "sensitive-words") else {
return Ok(Vec::new());
};
@@ -264,13 +264,13 @@ pub(crate) fn core_sensitive_words_settings_from_value(
}
value
.as_sequence()
.ok_or_else(|| format!("{provider}.sensitive-words 必须是字符串列表"))?
.ok_or_else(|| format!("{provider}.sensitive-words must be a list of strings"))?
.iter()
.map(|entry| {
entry
.as_str()
.map(str::to_string)
.ok_or_else(|| format!("{provider}.sensitive-words 必须是字符串列表"))
.ok_or_else(|| format!("{provider}.sensitive-words must be a list of strings"))
})
.collect()
};
@@ -291,10 +291,10 @@ pub(crate) fn patch_core_sensitive_words_yaml(
settings: &CoreSensitiveWordsSettings,
) -> Result<Option<String>, String> {
let original = serde_norway::from_str::<serde_norway::Value>(content)
.map_err(|error| format!("解析内核配置失败: {error}"))?;
.map_err(|error| format!("Failed to parse kernel configuration: {error}"))?;
let root = original
.as_mapping()
.ok_or_else(|| "内核配置顶层必须是 YAML 映射".to_string())?;
.ok_or_else(|| "Kernel configuration root must be a YAML mapping".to_string())?;
let mut prepared = content.to_string();
for (provider, words) in [
("antigravity", &settings.antigravity_sensitive_words),
@@ -319,17 +319,17 @@ pub(crate) fn patch_core_sensitive_words_yaml(
} else {
section
.as_mapping()
.ok_or_else(|| format!("{provider} 必须是 YAML 映射"))?
.ok_or_else(|| format!("{provider} must be a YAML mapping"))?
.clone()
};
section.insert(
yaml_key("sensitive-words"),
serde_norway::to_value(words).map_err(|error| format!("序列化敏感词失败: {error}"))?,
serde_norway::to_value(words).map_err(|error| format!("Failed to serialize sensitive words: {error}"))?,
);
let mut replacement = serde_norway::Mapping::new();
replacement.insert(yaml_key(provider), serde_norway::Value::Mapping(section));
let block = serde_norway::to_string(&replacement)
.map_err(|error| format!("序列化内核配置区段失败: {error}"))?;
.map_err(|error| format!("Failed to serialize kernel configuration section: {error}"))?;
prepared = replace_top_level_yaml_block(&prepared, provider, &block);
}
let updated = patch_core_yaml_document(&prepared, |document| {
@@ -340,7 +340,7 @@ pub(crate) fn patch_core_sensitive_words_yaml(
] {
let root = document
.as_mapping()
.ok_or_else(|| "内核配置顶层必须是 YAML 映射".to_string())?;
.ok_or_else(|| "Kernel configuration root must be a YAML mapping".to_string())?;
let section = yaml_mapping_value(root, provider);
let current_words = section
.and_then(serde_norway::Value::as_mapping)
@@ -380,10 +380,10 @@ pub(crate) fn patch_core_sensitive_words_settings(
let _config_guard = lock_core_config_file()?;
let config_path = core_install_dir()?.join(CORE_CONFIG_FILE);
if !config_path.is_file() {
return Err("内核配置尚未生成,请先启动 CPA 内核".to_string());
return Err("Kernel configuration has not been generated. Start the CPA kernel first".to_string());
}
let content = fs::read_to_string(&config_path)
.map_err(|error| format!("读取内核配置失败 {}: {error}", path_to_string(&config_path)))?;
.map_err(|error| format!("Failed to read kernel configuration {}: {error}", path_to_string(&config_path)))?;
if let Some(updated) = patch_core_sensitive_words_yaml(&content, settings)? {
write_yaml_if_changed(&config_path, &updated)?;
}
@@ -398,7 +398,7 @@ pub(crate) fn patch_core_api_keys(api_keys: &[String]) -> Result<(), String> {
}
let content = fs::read_to_string(&config_path)
.map_err(|err| format!("读取内核配置失败 {}: {err}", path_to_string(&config_path)))?;
.map_err(|err| format!("Failed to read kernel configuration {}: {err}", path_to_string(&config_path)))?;
let updated = patch_core_api_keys_yaml(&content, api_keys)?;
write_yaml_if_changed(&config_path, &updated)?;
Ok(())
@@ -458,12 +458,12 @@ pub(crate) fn patch_core_logging_settings(settings: &CoreConfigSettings) -> Resu
(
"logs-max-total-size-mb",
serde_norway::to_value(settings.logs_max_total_size_mb)
.map_err(|err| format!("序列化日志容量限制失败: {err}"))?,
.map_err(|err| format!("Failed to serialize log size limit: {err}"))?,
),
(
"error-logs-max-files",
serde_norway::to_value(settings.error_logs_max_files)
.map_err(|err| format!("序列化错误日志保留数失败: {err}"))?,
.map_err(|err| format!("Failed to serialize error log retention count: {err}"))?,
),
(
"usage-statistics-enabled",
@@ -472,7 +472,7 @@ pub(crate) fn patch_core_logging_settings(settings: &CoreConfigSettings) -> Resu
(
"redis-usage-queue-retention-seconds",
serde_norway::to_value(settings.redis_usage_queue_retention_seconds)
.map_err(|err| format!("序列化 Redis 用量队列保留时间失败: {err}"))?,
.map_err(|err| format!("Failed to serialize Redis usage queue retention: {err}"))?,
),
] {
changed |= set_core_yaml_top_level_value(document, key, value)?;
@@ -538,7 +538,7 @@ where
}
let content = fs::read_to_string(&config_path)
.map_err(|err| format!("读取内核配置失败 {}: {err}", path_to_string(&config_path)))?;
.map_err(|err| format!("Failed to read kernel configuration {}: {err}", path_to_string(&config_path)))?;
let Some(updated) = patch_core_yaml_document(&content, update)? else {
return Ok(());
};
@@ -556,7 +556,7 @@ where
F: FnOnce(&mut serde_norway::Value) -> Result<bool, String>,
{
let original = serde_norway::from_str::<serde_norway::Value>(content)
.map_err(|err| format!("解析内核配置失败: {err}"))?;
.map_err(|err| format!("Failed to parse kernel configuration: {err}"))?;
let mut updated = original.clone();
if !update(&mut updated)? {
return Ok(None);
@@ -640,24 +640,24 @@ pub(crate) fn render_yaml_value_changes(
remaining_changes = yaml_edit_changes;
let file = editable_content
.parse::<yaml_edit::YamlFile>()
.map_err(|err| format!("解析可编辑内核配置失败: {err}"))?;
.map_err(|err| format!("Failed to parse editable kernel configuration: {err}"))?;
let document = file
.document()
.ok_or_else(|| "内核配置没有 YAML 文档".to_string())?;
.ok_or_else(|| "Kernel configuration has no YAML document".to_string())?;
let root = document
.as_mapping()
.ok_or_else(|| "内核配置顶层必须是 YAML 映射".to_string())?;
.ok_or_else(|| "Kernel configuration root must be a YAML mapping".to_string())?;
for change in remaining_changes {
set_yaml_edit_mapping_path(&root, &change.path, &change.value)?;
}
let rendered = file.to_string();
let validated = serde_norway::from_str::<serde_norway::Value>(&rendered)
.map_err(|err| format!("验证更新后的内核配置失败: {err}"))?;
.map_err(|err| format!("Failed to validate updated kernel configuration: {err}"))?;
if validated != *updated {
let path = first_yaml_mismatch_path(updated, &validated, &mut Vec::new())
.unwrap_or_else(|| "<unknown>".to_string());
return Err(format!(
"更新后的内核配置与预期值不一致(路径: {path}),已拒绝写入"
"Updated kernel configuration does not match the expected value (path: {path}); write rejected"
));
}
Ok(rendered)
@@ -787,13 +787,13 @@ pub(crate) fn collect_yaml_value_changes(
) => {
for key in original_mapping.keys() {
if !updated_mapping.contains_key(key) {
return Err("当前内核配置更新不支持删除任意 YAML 字段".to_string());
return Err("The current kernel configuration update does not support deleting arbitrary YAML fields".to_string());
}
}
for (key, updated_value) in updated_mapping {
let key = key
.as_str()
.ok_or_else(|| "内核配置映射键必须是字符串".to_string())?;
.ok_or_else(|| "Kernel configuration mapping keys must be strings".to_string())?;
path.push(key.to_string());
if let Some(original_value) = original_mapping.get(yaml_key(key)) {
collect_yaml_value_changes(original_value, updated_value, path, changes)?;
@@ -807,7 +807,7 @@ pub(crate) fn collect_yaml_value_changes(
}
Ok(())
}
_ if path.is_empty() => Err("内核配置顶层必须保持为 YAML 映射".to_string()),
_ if path.is_empty() => Err("Kernel configuration root must remain a YAML mapping".to_string()),
_ => {
changes.push(YamlValueChange {
path: path.clone(),
@@ -880,7 +880,7 @@ pub(crate) fn insert_yaml_block_mapping_values_at_path(
let mut insertion = String::new();
for (key, value) in entries {
let value = serde_json::to_string(value)
.map_err(|error| format!("序列化内核配置值失败: {error}"))?;
.map_err(|error| format!("Failed to serialize kernel configuration value: {error}"))?;
insertion.push_str(&indent);
insertion.push_str(&render_yaml_mapping_key(key));
insertion.push_str(": ");
@@ -932,18 +932,18 @@ pub(crate) fn replace_yaml_sequence_value(
) -> Result<String, String> {
let file = content
.parse::<yaml_edit::YamlFile>()
.map_err(|err| format!("解析可编辑内核配置失败: {err}"))?;
.map_err(|err| format!("Failed to parse editable kernel configuration: {err}"))?;
let document = file
.document()
.ok_or_else(|| "内核配置没有 YAML 文档".to_string())?;
.ok_or_else(|| "Kernel configuration has no YAML document".to_string())?;
let root = document
.as_mapping()
.ok_or_else(|| "内核配置顶层必须是 YAML 映射".to_string())?;
.ok_or_else(|| "Kernel configuration root must be a YAML mapping".to_string())?;
let node = yaml_edit_node_at_path(&root, path)
.ok_or_else(|| format!("未找到内核配置序列 {}", path.join(".")))?;
.ok_or_else(|| format!("Kernel configuration sequence {} not found", path.join(".")))?;
let sequence = node
.as_sequence()
.ok_or_else(|| format!("内核配置字段 {} 必须是 YAML 序列", path.join(".")))?;
.ok_or_else(|| format!("Kernel configuration field {} must be a YAML sequence", path.join(".")))?;
let range = sequence.byte_range();
let start = range.start as usize;
let end = range.end as usize;
@@ -958,7 +958,7 @@ pub(crate) fn replace_yaml_sequence_value(
let serialized = if prefix.trim().is_empty() {
let serialized = serde_norway::to_string(value)
.map_err(|err| format!("序列化内核配置序列失败: {err}"))?;
.map_err(|err| format!("Failed to serialize kernel configuration sequence: {err}"))?;
let serialized = serialized.trim_end_matches(['\r', '\n']);
let mut indented = String::with_capacity(serialized.len() + prefix.len() * 2);
for (index, line) in serialized.split('\n').enumerate() {
@@ -970,7 +970,7 @@ pub(crate) fn replace_yaml_sequence_value(
}
indented
} else {
serde_json::to_string(value).map_err(|err| format!("序列化内核配置序列失败: {err}"))?
serde_json::to_string(value).map_err(|err| format!("Failed to serialize kernel configuration sequence: {err}"))?
};
Ok(format!(
@@ -1001,7 +1001,7 @@ pub(crate) fn set_yaml_edit_mapping_path(
value: &serde_norway::Value,
) -> Result<(), String> {
let Some((key, remaining)) = path.split_first() else {
return Err("内核配置更新路径不能为空".to_string());
return Err("Kernel configuration update path cannot be empty".to_string());
};
if remaining.is_empty() {
return set_yaml_edit_mapping_value(mapping, key, value);
@@ -1009,7 +1009,7 @@ pub(crate) fn set_yaml_edit_mapping_path(
if let Some(child) = mapping.get(key.as_str()) {
let child = child
.as_mapping()
.ok_or_else(|| format!("内核配置区段 {key} 必须是 YAML 映射"))?;
.ok_or_else(|| format!("Kernel configuration section {key} must be a YAML mapping"))?;
return set_yaml_edit_mapping_path(child, remaining, value);
}
let nested_value = nested_yaml_value_for_path(remaining, value.clone());
@@ -1050,12 +1050,12 @@ pub(crate) fn set_yaml_edit_mapping_value(
if let Some(node) = mapping.get(key) {
if let Some(sequence) = node.as_sequence() {
if sequence.len() != values.len() {
return Err(format!("内核配置序列 {key} 长度变化未被预处理"));
return Err(format!("Length change in kernel configuration sequence {key} was not preprocessed"));
}
for (index, value) in values.iter().enumerate() {
let value = yaml_edit_node_from_value(value)?;
if !sequence.set(index, value) {
return Err(format!("更新内核配置序列 {key}[{index}] 失败"));
return Err(format!("Failed to update kernel configuration sequence {key}[{index}]"));
}
}
return Ok(());
@@ -1088,14 +1088,14 @@ pub(crate) fn yaml_edit_node_from_value(
) -> Result<yaml_edit::YamlNode, String> {
const WRAPPER_KEY: &str = "__cpa_gui_value__";
let value =
serde_json::to_string(value).map_err(|err| format!("序列化内核配置值失败: {err}"))?;
serde_json::to_string(value).map_err(|err| format!("Failed to serialize kernel configuration value: {err}"))?;
let serialized = format!("{WRAPPER_KEY}: {value}\n");
let file = serialized
.parse::<yaml_edit::YamlFile>()
.map_err(|err| format!("解析内核配置值失败: {err}"))?;
.map_err(|err| format!("Failed to parse kernel configuration value: {err}"))?;
file.document()
.and_then(|document| document.get(WRAPPER_KEY))
.ok_or_else(|| "无法构造内核配置值".to_string())
.ok_or_else(|| "Unable to construct kernel configuration value".to_string())
}
pub(crate) fn set_core_yaml_top_level_value(
@@ -1105,7 +1105,7 @@ pub(crate) fn set_core_yaml_top_level_value(
) -> Result<bool, String> {
let root = document
.as_mapping_mut()
.ok_or_else(|| "内核配置顶层必须是 YAML 映射".to_string())?;
.ok_or_else(|| "Kernel configuration root must be a YAML mapping".to_string())?;
let key = yaml_key(key);
if root.get(&key) == Some(&value) {
return Ok(false);
@@ -1122,12 +1122,12 @@ pub(crate) fn set_core_yaml_nested_value(
) -> Result<bool, String> {
let root = document
.as_mapping_mut()
.ok_or_else(|| "内核配置顶层必须是 YAML 映射".to_string())?;
.ok_or_else(|| "Kernel configuration root must be a YAML mapping".to_string())?;
let section = root
.entry(yaml_key(section))
.or_insert_with(|| serde_norway::Value::Mapping(serde_norway::Mapping::new()))
.as_mapping_mut()
.ok_or_else(|| "内核配置区段必须是 YAML 映射".to_string())?;
.ok_or_else(|| "Kernel configuration section must be a YAML mapping".to_string())?;
let key = yaml_key(key);
if section.get(&key) == Some(&value) {
return Ok(false);
@@ -1141,10 +1141,10 @@ pub(crate) fn patch_core_api_keys_yaml(
api_keys: &[String],
) -> Result<String, String> {
let parsed = serde_norway::from_str::<serde_norway::Value>(content)
.map_err(|err| format!("解析内核配置失败: {err}"))?;
.map_err(|err| format!("Failed to parse kernel configuration: {err}"))?;
let root = parsed
.as_mapping()
.ok_or_else(|| "内核配置顶层必须是 YAML 映射".to_string())?;
.ok_or_else(|| "Kernel configuration root must be a YAML mapping".to_string())?;
let has_legacy_api_keys = nested_yaml_value(
root,
&["auth", "providers", "config-api-key", "api-key-entries"],
@@ -1154,10 +1154,10 @@ pub(crate) fn patch_core_api_keys_yaml(
let content = if has_legacy_api_keys {
let file = content
.parse::<yaml_edit::YamlFile>()
.map_err(|err| format!("解析内核配置失败: {err}"))?;
.map_err(|err| format!("Failed to parse kernel configuration: {err}"))?;
let document = file
.document()
.ok_or_else(|| "内核配置没有 YAML 文档".to_string())?;
.ok_or_else(|| "Kernel configuration has no YAML document".to_string())?;
clear_legacy_api_key_paths(&document);
file.to_string()
} else {
@@ -1166,7 +1166,7 @@ pub(crate) fn patch_core_api_keys_yaml(
let block = render_core_api_keys_yaml(api_keys)?;
let updated = replace_top_level_yaml_block(&content, "api-keys", &block);
serde_norway::from_str::<serde_norway::Value>(&updated)
.map_err(|err| format!("验证更新后的内核配置失败: {err}"))?;
.map_err(|err| format!("Failed to validate updated kernel configuration: {err}"))?;
Ok(updated)
}
@@ -1183,7 +1183,7 @@ pub(crate) fn render_core_api_keys_yaml(api_keys: &[String]) -> Result<String, S
.collect(),
);
let serialized = serde_norway::to_string(&sequence)
.map_err(|err| format!("生成内核鉴权密钥配置失败: {err}"))?;
.map_err(|err| format!("Failed to generate kernel authentication key configuration: {err}"))?;
let mut block = String::from("api-keys:\n");
for line in serialized.lines() {
block.push_str(" ");
@@ -1306,13 +1306,13 @@ pub(crate) fn set_yaml_edit_nested_value(
pub(crate) fn read_core_config_document() -> Result<(PathBuf, yaml_serde_edit::YamlValue), String> {
let config_path = core_install_dir()?.join(CORE_CONFIG_FILE);
if !config_path.is_file() {
return Err("内核配置尚未生成,请先启动 CPA 内核".to_string());
return Err("Kernel configuration has not been generated. Start the CPA kernel first".to_string());
}
let content = fs::read_to_string(&config_path)
.map_err(|err| format!("读取内核配置失败 {}: {err}", path_to_string(&config_path)))?;
.map_err(|err| format!("Failed to read kernel configuration {}: {err}", path_to_string(&config_path)))?;
let document = yaml_serde_edit::YamlValue::parse(&content)
.map_err(|err| format!("解析内核配置失败: {err}"))?;
.map_err(|err| format!("Failed to parse kernel configuration: {err}"))?;
Ok((config_path, document))
}
@@ -1321,13 +1321,13 @@ pub(crate) fn core_config_settings_from_value(
) -> Result<CoreConfigSettings, String> {
let root = document
.as_mapping()
.ok_or_else(|| "内核配置顶层必须是 YAML 映射".to_string())?;
.ok_or_else(|| "Kernel configuration root must be a YAML mapping".to_string())?;
let host = yaml_mapping_value(root, "host")
.map(|value| {
value
.as_str()
.map(str::to_string)
.ok_or_else(|| "host 必须是字符串".to_string())
.ok_or_else(|| "host must be a string".to_string())
})
.transpose()?
.unwrap_or_else(|| "127.0.0.1".to_string());
@@ -1337,7 +1337,7 @@ pub(crate) fn core_config_settings_from_value(
.as_u64()
.and_then(|value| u16::try_from(value).ok())
.filter(|value| *value != 0)
.ok_or_else(|| "port 必须是 1 到 65535 之间的整数".to_string())
.ok_or_else(|| "port must be an integer between 1 and 65535".to_string())
})
.transpose()?
.unwrap_or(8317);
@@ -1346,7 +1346,7 @@ pub(crate) fn core_config_settings_from_value(
value
.as_str()
.map(str::to_string)
.ok_or_else(|| "auth-dir 必须是字符串".to_string())
.ok_or_else(|| "auth-dir must be a string".to_string())
})
.transpose()?
.unwrap_or_else(|| OAUTH_DIR_NAME.to_string());
@@ -1354,7 +1354,7 @@ pub(crate) fn core_config_settings_from_value(
.map(|value| {
value
.as_bool()
.ok_or_else(|| "debug 必须是布尔值".to_string())
.ok_or_else(|| "debug must be a boolean".to_string())
})
.transpose()?
.unwrap_or(false);
@@ -1362,7 +1362,7 @@ pub(crate) fn core_config_settings_from_value(
.map(|value| {
value
.as_bool()
.ok_or_else(|| "commercial-mode 必须是布尔值".to_string())
.ok_or_else(|| "commercial-mode must be a boolean".to_string())
})
.transpose()?
.unwrap_or(false);
@@ -1370,7 +1370,7 @@ pub(crate) fn core_config_settings_from_value(
.map(|value| {
value
.as_bool()
.ok_or_else(|| "logging-to-file 必须是布尔值".to_string())
.ok_or_else(|| "logging-to-file must be a boolean".to_string())
})
.transpose()?
.unwrap_or(false);
@@ -1379,7 +1379,7 @@ pub(crate) fn core_config_settings_from_value(
value
.as_u64()
.and_then(|value| u32::try_from(value).ok())
.ok_or_else(|| "logs-max-total-size-mb 必须是非负整数".to_string())
.ok_or_else(|| "logs-max-total-size-mb must be a non-negative integer".to_string())
})
.transpose()?
.unwrap_or(DEFAULT_LOGS_MAX_TOTAL_SIZE_MB);
@@ -1388,7 +1388,7 @@ pub(crate) fn core_config_settings_from_value(
value
.as_u64()
.and_then(|value| u32::try_from(value).ok())
.ok_or_else(|| "error-logs-max-files 必须是非负整数".to_string())
.ok_or_else(|| "error-logs-max-files must be a non-negative integer".to_string())
})
.transpose()?
.unwrap_or(DEFAULT_ERROR_LOGS_MAX_FILES);
@@ -1396,7 +1396,7 @@ pub(crate) fn core_config_settings_from_value(
.map(|value| {
value
.as_bool()
.ok_or_else(|| "usage-statistics-enabled 必须是布尔值".to_string())
.ok_or_else(|| "usage-statistics-enabled must be a boolean".to_string())
})
.transpose()?
.unwrap_or(true);
@@ -1406,7 +1406,7 @@ pub(crate) fn core_config_settings_from_value(
value
.as_u64()
.and_then(|value| u32::try_from(value).ok())
.ok_or_else(|| "redis-usage-queue-retention-seconds 必须是非负整数".to_string())
.ok_or_else(|| "redis-usage-queue-retention-seconds must be a non-negative integer".to_string())
})
.transpose()?
.unwrap_or(DEFAULT_REDIS_USAGE_QUEUE_RETENTION_SECONDS);
@@ -1419,7 +1419,7 @@ pub(crate) fn core_config_settings_from_value(
.map(|value| {
value
.as_bool()
.ok_or_else(|| "request-log 必须是布尔值".to_string())
.ok_or_else(|| "request-log must be a boolean".to_string())
})
.transpose()?
.unwrap_or(false);
@@ -1432,7 +1432,7 @@ pub(crate) fn core_config_settings_from_value(
.map(|value| {
value
.as_bool()
.ok_or_else(|| "plugins.enabled 必须是布尔值".to_string())
.ok_or_else(|| "plugins.enabled must be a boolean".to_string())
})
.transpose()?
.unwrap_or(false);
@@ -1441,7 +1441,7 @@ pub(crate) fn core_config_settings_from_value(
value
.as_str()
.map(str::to_string)
.ok_or_else(|| "routing.strategy 必须是字符串".to_string())
.ok_or_else(|| "routing.strategy must be a string".to_string())
})
.transpose()?
.unwrap_or_else(|| "round-robin".to_string());
@@ -1450,7 +1450,7 @@ pub(crate) fn core_config_settings_from_value(
value
.as_str()
.map(str::to_string)
.ok_or_else(|| "proxy-url 必须是字符串".to_string())
.ok_or_else(|| "proxy-url must be a string".to_string())
})
.transpose()?
.unwrap_or_default();
@@ -1459,7 +1459,7 @@ pub(crate) fn core_config_settings_from_value(
.map(|value| {
value
.as_bool()
.ok_or_else(|| "routing.session-affinity 必须是布尔值".to_string())
.ok_or_else(|| "routing.session-affinity must be a boolean".to_string())
})
.transpose()?
.unwrap_or(false);
@@ -1470,7 +1470,7 @@ pub(crate) fn core_config_settings_from_value(
value
.as_str()
.map(str::to_string)
.ok_or_else(|| "routing.session-affinity-ttl 必须是字符串".to_string())
.ok_or_else(|| "routing.session-affinity-ttl must be a string".to_string())
})
.transpose()?
.unwrap_or_default();
@@ -1478,7 +1478,7 @@ pub(crate) fn core_config_settings_from_value(
.map(|value| {
value
.as_bool()
.ok_or_else(|| "disable-cooling 必须是布尔值".to_string())
.ok_or_else(|| "disable-cooling must be a boolean".to_string())
})
.transpose()?
.unwrap_or(DEFAULT_DISABLE_COOLING);
@@ -1487,7 +1487,7 @@ pub(crate) fn core_config_settings_from_value(
value
.as_u64()
.and_then(|value| u32::try_from(value).ok())
.ok_or_else(|| "request-retry 必须是非负整数".to_string())
.ok_or_else(|| "request-retry must be a non-negative integer".to_string())
})
.transpose()?
.unwrap_or(DEFAULT_REQUEST_RETRY);
@@ -1496,7 +1496,7 @@ pub(crate) fn core_config_settings_from_value(
value
.as_u64()
.and_then(|value| u32::try_from(value).ok())
.ok_or_else(|| "max-retry-credentials 必须是非负整数".to_string())
.ok_or_else(|| "max-retry-credentials must be a non-negative integer".to_string())
})
.transpose()?
.unwrap_or(DEFAULT_MAX_RETRY_CREDENTIALS);
@@ -1505,7 +1505,7 @@ pub(crate) fn core_config_settings_from_value(
value
.as_u64()
.and_then(|value| u32::try_from(value).ok())
.ok_or_else(|| "max-retry-interval 必须是非负整数".to_string())
.ok_or_else(|| "max-retry-interval must be a non-negative integer".to_string())
})
.transpose()?
.unwrap_or(DEFAULT_MAX_RETRY_INTERVAL);
@@ -1514,7 +1514,7 @@ pub(crate) fn core_config_settings_from_value(
value
.as_u64()
.and_then(|value| u32::try_from(value).ok())
.ok_or_else(|| "streaming.bootstrap-retries 必须是非负整数".to_string())
.ok_or_else(|| "streaming.bootstrap-retries must be a non-negative integer".to_string())
})
.transpose()?
.unwrap_or(DEFAULT_STREAMING_BOOTSTRAP_RETRIES);
@@ -1577,7 +1577,7 @@ pub(crate) fn extract_api_key_sequence(
let sequence = value
.as_sequence()
.ok_or_else(|| format!("{field_name} 必须是数组"))?;
.ok_or_else(|| format!("{field_name} must be an array"))?;
sequence
.iter()
.filter_map(extract_api_key_value)
@@ -1602,7 +1602,7 @@ pub(crate) fn extract_api_key_value(value: &serde_norway::Value) -> Option<Resul
}
Some(Err(
"鉴权密钥条目必须是字符串或包含 key 字段的映射".to_string()
"Authentication key entry must be a string or a mapping containing a key field".to_string()
))
}
@@ -1614,7 +1614,7 @@ pub(crate) fn extract_core_management_secret_key(
};
let value = value
.as_str()
.ok_or_else(|| "remote-management.secret-key 必须是字符串".to_string())?
.ok_or_else(|| "remote-management.secret-key must be a string".to_string())?
.trim()
.to_string();
if value.is_empty() {
@@ -1630,7 +1630,7 @@ pub(crate) fn set_core_api_keys(
) -> Result<(), String> {
let root = document
.as_mapping_mut()
.ok_or_else(|| "内核配置顶层必须是 YAML 映射".to_string())?;
.ok_or_else(|| "Kernel configuration root must be a YAML mapping".to_string())?;
root.insert(
yaml_key("api-keys"),
serde_norway::Value::Sequence(
@@ -1675,18 +1675,18 @@ where
T: Into<serde_norway::Value>,
{
if path.len() != 2 {
return Err("内核配置路径无效".to_string());
return Err("Invalid kernel configuration path".to_string());
}
let root = document
.as_mapping_mut()
.ok_or_else(|| "内核配置顶层必须是 YAML 映射".to_string())?;
.ok_or_else(|| "Kernel configuration root must be a YAML mapping".to_string())?;
let section = root
.entry(yaml_key(path[0]))
.or_insert_with(|| serde_norway::Value::Mapping(serde_norway::Mapping::new()));
let section = section
.as_mapping_mut()
.ok_or_else(|| format!("{} 必须是 YAML 映射", path[0]))?;
.ok_or_else(|| format!("{} must be a YAML mapping", path[0]))?;
section.insert(yaml_key(path[1]), value.into());
Ok(())
}
File diff suppressed because it is too large. Load diff
+5 -5
View File
@@ -47,13 +47,13 @@ pub(crate) fn acquire_app_instance_guard_for(
let handle = unsafe { CreateMutexW(std::ptr::null(), 0, wide_name.as_ptr()) };
if handle.is_null() {
return Err(format!(
"创建当前目录的软件实例锁失败: {}",
"Failed to create application instance lock for the current directory: {}",
std::io::Error::last_os_error()
));
}
if unsafe { GetLastError() } == ERROR_ALREADY_EXISTS {
unsafe { CloseHandle(handle) };
return Err("当前 EasyCLIProxyAPI 目录已经有一个软件实例在运行".to_string());
return Err("An application instance is already running in the current EasyCLIProxyAPI directory".to_string());
}
Ok(AppInstanceGuard {
@@ -76,15 +76,15 @@ pub(crate) fn acquire_app_instance_guard_for(
.write(true)
.create(true)
.open(&lock_path)
.map_err(|error| format!("打开当前目录的软件实例锁失败: {error}"))?;
.map_err(|error| format!("Failed to open application instance lock for the current directory: {error}"))?;
let result = unsafe { libc::flock(file.as_raw_fd(), libc::LOCK_EX | libc::LOCK_NB) };
if result != 0 {
let error = std::io::Error::last_os_error();
let raw_error = error.raw_os_error();
if raw_error == Some(libc::EWOULDBLOCK) || raw_error == Some(libc::EAGAIN) {
return Err("当前 EasyCLIProxyAPI 目录已经有一个软件实例在运行".to_string());
return Err("An application instance is already running in the current EasyCLIProxyAPI directory".to_string());
}
return Err(format!("锁定当前 EasyCLIProxyAPI 目录失败: {error}"));
return Err(format!("Failed to lock the current EasyCLIProxyAPI directory: {error}"));
}
Ok(AppInstanceGuard { _file: file })
+65 -109
View File
@@ -13,6 +13,8 @@ mod desktop_theme;
mod instance_lock;
mod management_api;
mod network_proxy;
#[cfg(any(target_os = "macos", target_os = "windows"))]
mod native_i18n;
mod oauth_browser;
mod progress;
mod provider_health;
@@ -133,7 +135,7 @@ const OAUTH_DIR_NAME: &str = "oauth";
const DEFAULT_AUTH_DIR: &str = "../oauth";
const DEFAULT_API_KEY: &str = "123456";
const DEFAULT_AGENT_TERMINAL: &str = "auto";
const DEFAULT_API_KEY_INITIAL_REMARK: &str = "默认密钥";
const DEFAULT_API_KEY_INITIAL_REMARK: &str = "Default key";
const DEFAULT_REQUEST_RETRY: u32 = 3;
const DEFAULT_MAX_RETRY_CREDENTIALS: u32 = 0;
const DEFAULT_MAX_RETRY_INTERVAL: u32 = 30;
@@ -227,9 +229,7 @@ static CONFIG_WRITE_HASHES: LazyLock<Mutex<std::collections::HashMap<PathBuf, St
fn normalize_app_locale(locale: &str) -> &'static str {
let normalized = locale.trim().to_ascii_lowercase();
if normalized.starts_with("en") {
"en"
} else if normalized.starts_with("ja") {
if normalized.starts_with("ja") {
"ja"
} else if normalized == "zh-tw"
|| normalized == "zh-hk"
@@ -237,54 +237,10 @@ fn normalize_app_locale(locale: &str) -> &'static str {
|| normalized.starts_with("zh-hant")
{
"zh-TW"
} else {
} else if normalized.starts_with("zh") {
"zh-CN"
}
}
#[cfg(any(target_os = "macos", target_os = "windows"))]
fn locale_text<'a>(locale: &str, zh_cn: &'a str, en: &'a str) -> &'a str {
match normalize_app_locale(locale) {
"en" => en,
"zh-TW" => match zh_cn {
"打开主界面" => "開啟主介面",
"退出" => "退出",
"内核状态:处理中" => "核心狀態:處理中",
"内核状态:未安装" => "核心狀態:未安裝",
"内核状态:运行中" => "核心狀態:執行中",
"内核状态:已停止" => "核心狀態:已停止",
"处理中..." => "處理中...",
"停止内核" => "停止核心",
"启动内核" => "啟動核心",
"重启内核" => "重新啟動核心",
"EasyCLIProxyAPI · 内核处理中" => "EasyCLIProxyAPI · 核心處理中",
"EasyCLIProxyAPI · 内核未安装" => "EasyCLIProxyAPI · 核心未安裝",
"EasyCLIProxyAPI · 内核运行中" => "EasyCLIProxyAPI · 核心執行中",
"EasyCLIProxyAPI · 内核已停止" => "EasyCLIProxyAPI · 核心已停止",
"EasyCLIProxyAPI · 内核操作失败" => "EasyCLIProxyAPI · 核心操作失敗",
"内核状态:正在检查" => "核心狀態:正在檢查",
_ => zh_cn,
},
"ja" => match zh_cn {
"打开主界面" => "メイン画面を開く",
"退出" => "終了",
"内核状态:处理中" => "コア状態:処理中",
"内核状态:未安装" => "コア状態:未インストール",
"内核状态:运行中" => "コア状態:実行中",
"内核状态:已停止" => "コア状態:停止済み",
"处理中..." => "処理中...",
"停止内核" => "コアを停止",
"启动内核" => "コアを起動",
"重启内核" => "コアを再起動",
"EasyCLIProxyAPI · 内核处理中" => "EasyCLIProxyAPI · コア処理中",
"EasyCLIProxyAPI · 内核未安装" => "EasyCLIProxyAPI · コア未インストール",
"EasyCLIProxyAPI · 内核运行中" => "EasyCLIProxyAPI · コア実行中",
"EasyCLIProxyAPI · 内核已停止" => "EasyCLIProxyAPI · コア停止済み",
"EasyCLIProxyAPI · 内核操作失败" => "EasyCLIProxyAPI · コア操作失敗",
"内核状态:正在检查" => "コア状態:確認中",
_ => zh_cn,
},
_ => zh_cn,
} else {
"en"
}
}
@@ -389,7 +345,7 @@ impl AgentConfigStatusCache {
.filter(|entry| entry.port == port && entry.api_key_sha256 == api_key_sha256)
.map(|entry| entry.statuses.clone())
})
.map_err(|_| "智能体配置状态缓存锁已损坏".to_string())
.map_err(|_| "Agent configuration status cache lock is poisoned".to_string())
}
fn replace(
@@ -401,7 +357,7 @@ impl AgentConfigStatusCache {
let mut current = self
.entry
.lock()
.map_err(|_| "智能体配置状态缓存锁已损坏".to_string())?;
.map_err(|_| "Agent configuration status cache lock is poisoned".to_string())?;
*current = Some(AgentConfigStatusCacheEntry {
port,
api_key_sha256: sha256_bytes(api_key.as_bytes()),
@@ -414,7 +370,7 @@ impl AgentConfigStatusCache {
let mut current = self
.entry
.lock()
.map_err(|_| "智能体配置状态缓存锁已损坏".to_string())?;
.map_err(|_| "Agent configuration status cache lock is poisoned".to_string())?;
*current = None;
Ok(())
}
@@ -698,7 +654,7 @@ impl VersionDownloadSource {
Self::Gitcode => "GitCode",
Self::GhProxy => "gh-proxy.com",
Self::GhFast => "ghfast.top",
Self::Custom => "自定义镜像",
Self::Custom => "Custom mirror",
}
}
@@ -794,7 +750,7 @@ fn version_download_source_candidates(
fn normalize_custom_download_mirror_url(value: &str) -> Result<String, String> {
let mut url =
reqwest::Url::parse(value.trim()).map_err(|error| format!("镜像地址无效: {error}"))?;
reqwest::Url::parse(value.trim()).map_err(|error| format!("Invalid mirror URL: {error}"))?;
if url.scheme() != "https"
|| url.host_str().is_none()
|| url.port().is_some()
@@ -803,7 +759,7 @@ fn normalize_custom_download_mirror_url(value: &str) -> Result<String, String> {
|| url.query().is_some()
|| url.fragment().is_some()
{
return Err("镜像地址必须是无认证、端口、查询参数和片段的 HTTPS 地址".to_string());
return Err("Mirror URL must be an HTTPS URL without authentication, port, query parameters, or fragment".to_string());
}
if !url.path().ends_with('/') {
let path = format!("{}/", url.path());
@@ -924,7 +880,7 @@ where
impl Default for GuiConfigFile {
fn default() -> Self {
Self {
locale: "zh-CN".to_string(),
locale: "en".to_string(),
port: 8317,
allow_lan: false,
host: "127.0.0.1".to_string(),
@@ -1389,7 +1345,7 @@ impl AgentClient {
"antigravity-cli" => Ok(Self::AntigravityCli),
"kimi-code" => Ok(Self::KimiCode),
"grok-build" => Ok(Self::GrokBuild),
_ => Err(format!("不支持的智能体客户端: {value}")),
_ => Err(format!("Unsupported agent client: {value}")),
}
}
@@ -1629,7 +1585,7 @@ impl Default for CoreInstallTask {
Self {
running: false,
cancellable: false,
phase: "空闲".to_string(),
phase: "Idle".to_string(),
downloaded: 0,
total: None,
percent: None,
@@ -1657,10 +1613,10 @@ impl CoreDownloadState {
let mut inner = self
.inner
.lock()
.map_err(|_| "内核安装状态锁已损坏".to_string())?;
.map_err(|_| "Kernel installation status lock is poisoned".to_string())?;
if inner.running {
return Err("已有内核安装任务正在运行".to_string());
return Err("A kernel installation task is already running".to_string());
}
inner.running = true;
@@ -1669,8 +1625,8 @@ impl CoreDownloadState {
running: true,
cancellable: true,
phase: version
.map(|version| format!("准备安装 {version}"))
.unwrap_or_else(|| "准备安装最新版".to_string()),
.map(|version| format!("Preparing to install {version}"))
.unwrap_or_else(|| "Preparing to install the latest version".to_string()),
downloaded: 0,
total: None,
percent: None,
@@ -1738,18 +1694,18 @@ impl CoreDownloadState {
match result {
Ok(result) => {
inner.task.phase = "安装完成".to_string();
inner.task.phase = "Installation complete".to_string();
inner.task.downloaded = 1;
inner.task.total = Some(1);
inner.task.percent = Some(100.0);
inner.task.message = Some(format!("{} 安装完成", result.version));
inner.task.message = Some(format!("{} installation complete", result.version));
inner.task.result = Some(result);
}
Err(error) => {
inner.task.phase = if error.contains("取消") {
"已取消".to_string()
inner.task.phase = if error.to_ascii_lowercase().contains("cancel") {
"Canceled".to_string()
} else {
"安装失败".to_string()
"Installation failed".to_string()
};
inner.task.message = Some(error);
inner.task.result = None;
@@ -1783,9 +1739,9 @@ impl AppUpdateState {
let mut inner = self
.inner
.lock()
.map_err(|_| "应用更新状态锁已损坏".to_string())?;
.map_err(|_| "Application update status lock is poisoned".to_string())?;
if inner.task.running {
return Err("已有应用更新任务正在运行".to_string());
return Err("An application update task is already running".to_string());
}
inner.token = Some(token);
inner.task = AppUpdateTask {
@@ -1801,16 +1757,16 @@ impl AppUpdateState {
let mut inner = self
.inner
.lock()
.map_err(|_| "应用更新状态锁已损坏".to_string())?;
.map_err(|_| "Application update status lock is poisoned".to_string())?;
if !inner.task.running || inner.task.phase != "checking" {
return Err("应用更新未处于检查阶段".to_string());
return Err("Application update is not in the checking stage".to_string());
}
if inner
.token
.as_ref()
.is_none_or(CancellationToken::is_cancelled)
{
return Err("应用更新下载已取消".to_string());
return Err("Application update download canceled".to_string());
}
inner.task = AppUpdateTask {
running: true,
@@ -1875,7 +1831,7 @@ impl CoreProcessState {
fn ensure_active(&self) -> Result<(), String> {
if self.is_shutting_down() {
Err("应用正在退出,已取消内核操作".to_string())
Err("Application is exiting; kernel operation canceled".to_string())
} else {
Ok(())
}
@@ -1921,7 +1877,7 @@ impl CoreProcessState {
let mut adopted = self
.adopted_processes
.lock()
.map_err(|_| "接管的内核进程状态锁已损坏".to_string())?;
.map_err(|_| "Adopted kernel process status lock is poisoned".to_string())?;
*adopted = process_ids
.into_iter()
.filter(|process_id| is_process_alive(*process_id))
@@ -1939,7 +1895,7 @@ impl CoreProcessState {
self.adopted_processes
.lock()
.map(|mut processes| processes.clear())
.map_err(|_| "接管的内核进程状态锁已损坏".to_string())
.map_err(|_| "Adopted kernel process status lock is poisoned".to_string())
}
fn take_adopted_processes(&self) -> Vec<AdoptedCoreProcess> {
@@ -1956,7 +1912,7 @@ impl CoreProcessState {
let mut managed_child = self
.child
.lock()
.map_err(|_| "内核进程状态锁已损坏".to_string())?;
.map_err(|_| "Kernel process status lock is poisoned".to_string())?;
*managed_child = Some(child);
Ok(pid)
}
@@ -1973,14 +1929,14 @@ impl MainWindowSizeState {
self.inner
.lock()
.map(|size| *size)
.map_err(|_| "主窗口尺寸状态锁已损坏".to_string())
.map_err(|_| "Main window size status lock is poisoned".to_string())
}
fn replace(&self, size: SavedWindowSize) -> Result<(), String> {
let mut current = self
.inner
.lock()
.map_err(|_| "主窗口尺寸状态锁已损坏".to_string())?;
.map_err(|_| "Main window size status lock is poisoned".to_string())?;
*current = Some(size);
Ok(())
}
@@ -1997,14 +1953,14 @@ impl GuiConfigState {
self.inner
.lock()
.map(|config| config.clone())
.map_err(|_| "GUI 配置状态锁已损坏".to_string())
.map_err(|_| "GUI configuration status lock is poisoned".to_string())
}
fn replace_external(&self, config: GuiConfigFile) -> Result<(), String> {
let mut current = self
.inner
.lock()
.map_err(|_| "GUI 配置状态锁已损坏".to_string())?;
.map_err(|_| "GUI configuration status lock is poisoned".to_string())?;
*current = config;
Ok(())
}
@@ -2016,7 +1972,7 @@ impl GuiConfigState {
let mut current = self
.inner
.lock()
.map_err(|_| "GUI 配置状态锁已损坏".to_string())?;
.map_err(|_| "GUI configuration status lock is poisoned".to_string())?;
let mut config = current.clone();
apply_core_settings_to_gui_config(&mut config, settings);
apply_external_core_proxy_override(&mut config, settings)?;
@@ -2136,7 +2092,7 @@ impl GuiConfigState {
self.update(|config| {
if let Some(url) = &candidate.custom_url {
if !config.custom_download_mirrors.contains(url) {
return Err("自定义镜像不存在".to_string());
return Err("Custom mirror does not exist".to_string());
}
config.active_custom_download_mirror = url.clone();
}
@@ -2154,7 +2110,7 @@ impl GuiConfigState {
let mut current = self
.inner
.lock()
.map_err(|_| "GUI 配置状态锁已损坏".to_string())?;
.map_err(|_| "GUI configuration status lock is poisoned".to_string())?;
if current.selected_download_candidate() != *expected_source
|| expected_source == fallback_source
{
@@ -2263,7 +2219,7 @@ impl GuiConfigState {
let mut current = self
.inner
.lock()
.map_err(|_| "GUI 配置状态锁已损坏".to_string())?;
.map_err(|_| "GUI configuration status lock is poisoned".to_string())?;
let mut config = current.clone();
update(&mut config)?;
sanitize_gui_config(&mut config)?;
@@ -2385,7 +2341,7 @@ fn main() {
let result = args
.next()
.map(PathBuf::from)
.ok_or_else(|| "应用更新助手缺少描述文件".to_string())
.ok_or_else(|| "Application update helper is missing its descriptor file".to_string())
.and_then(|path| run_portable_update_helper(&path));
if let Err(error) = result {
eprintln!("{error}");
@@ -2409,11 +2365,11 @@ fn main() {
eprintln!("{error}");
let mut config = GuiConfigFile::default();
if let Err(secret_error) = ensure_strong_management_secret(&mut config) {
eprintln!("初始化 WebUI 安全密钥失败: {secret_error}");
eprintln!("Failed to initialize WebUI security key: {secret_error}");
return;
}
if let Err(sanitize_error) = sanitize_gui_config(&mut config) {
eprintln!("初始化固定凭证目录失败: {sanitize_error}");
eprintln!("Failed to initialize fixed credentials directory: {sanitize_error}");
}
config
}
@@ -2463,7 +2419,7 @@ fn main() {
if let Some(observed_size) = observed_size {
let window_size_state = window.state::<MainWindowSizeState>();
if let Err(error) = window_size_state.replace(observed_size) {
eprintln!("记录主窗口尺寸失败: {error}");
eprintln!("Failed to record main window size: {error}");
}
}
});
@@ -2475,7 +2431,7 @@ fn main() {
api.prevent_close();
set_macos_dock_visible(window.app_handle(), false);
if let Err(error) = window.hide() {
eprintln!("隐藏主窗口失败: {error}");
eprintln!("Failed to hide main window: {error}");
set_macos_dock_visible(window.app_handle(), true);
}
}
@@ -2488,7 +2444,7 @@ fn main() {
if let tauri::WindowEvent::CloseRequested { api, .. } = event {
api.prevent_close();
if let Err(error) = window.emit(WINDOWS_CLOSE_REQUEST_EVENT, ()) {
eprintln!("显示 Windows 关闭确认失败: {error}");
eprintln!("Failed to show Windows close confirmation: {error}");
}
}
}
@@ -2497,21 +2453,21 @@ fn main() {
let app = app
.setup(move |app| {
if let Err(error) = network_proxy::refresh(app.state::<GuiConfigState>().inner()) {
eprintln!("读取启动代理设置失败: {error}");
eprintln!("Failed to read startup proxy settings: {error}");
}
if let Err(error) = codex_catalog::validate_embedded_catalog() {
eprintln!("Codex 内置模型目录无效: {error}");
eprintln!("Invalid bundled Codex model catalog: {error}");
}
if let Err(error) = load_codex_model_catalog_override(app.handle()) {
eprintln!("加载 Codex 模型目录更新文件失败,将使用内置目录: {error}");
eprintln!("Failed to load Codex model catalog update file; using bundled catalog: {error}");
}
let catalog_update_app = app.handle().clone();
if let Err(error) = load_codex_model_customizations(app.handle()) {
eprintln!("加载 Codex 自定义模型配置失败: {error}");
eprintln!("Failed to load Codex custom model configuration: {error}");
}
tauri::async_runtime::spawn(async move {
if let Err(error) = update_codex_model_catalog_inner(&catalog_update_app).await {
eprintln!("后台更新 Codex 模型目录失败,继续使用当前目录: {error}");
eprintln!("Failed to update Codex model catalog in the background; continuing with current catalog: {error}");
}
});
if let Err(error) = restore_main_window_size(app.handle()) {
@@ -2524,13 +2480,13 @@ fn main() {
setup_windows_tray(app)?;
if let Err(error) = configure_initial_main_window(app.handle(), start_hidden) {
eprintln!("配置启动窗口状态失败: {error}");
eprintln!("Failed to configure startup window state: {error}");
}
if let Err(error) =
configuration_watcher::start_configuration_file_watcher(app.handle().clone())
{
eprintln!("启动配置文件监控失败: {error}");
eprintln!("Failed to start configuration file watcher: {error}");
}
network_proxy::start_monitor(app.handle().clone());
@@ -2539,7 +2495,7 @@ fn main() {
let usage_app = app.handle().clone();
tauri::async_runtime::spawn_blocking(move || {
if let Err(error) = usage::initialize_usage_storage() {
eprintln!("初始化使用记录目录失败: {error}");
eprintln!("Failed to initialize usage records directory: {error}");
}
usage::start_usage_collector(usage_app);
});
@@ -2553,7 +2509,7 @@ fn main() {
gui_config_state.inner(),
cache.inner(),
) {
eprintln!("后台刷新智能体配置状态失败: {error}");
eprintln!("Failed to refresh agent configuration status in the background: {error}");
}
});
@@ -2582,22 +2538,22 @@ fn main() {
}
match auto_install_bundled_core_if_needed(&core_app) {
Ok(true) => eprintln!("已自动安装或升级至软件内置的 CPA 内核版本"),
Ok(true) => eprintln!("Automatically installed or upgraded to the CPA kernel version bundled with the application"),
Ok(false) => {}
Err(error) => eprintln!("自动安装 CPA 离线内核失败: {error}"),
Err(error) => eprintln!("Failed to automatically install CPA offline kernel: {error}"),
}
let adopted_process_ids = match adopt_existing_core_processes(process_state.inner())
{
Ok(process_ids) => process_ids,
Err(error) => {
eprintln!("扫描并接管当前目录的 CPA 内核失败: {error}");
eprintln!("Failed to scan for and adopt the CPA kernel in the current directory: {error}");
Vec::new()
}
};
if !adopted_process_ids.is_empty() {
eprintln!(
"已接管当前目录中运行的 CPA 内核: PID {}",
"Adopted running CPA kernel in the current directory: PID {}",
adopted_process_ids
.iter()
.map(u32::to_string)
@@ -2606,7 +2562,7 @@ fn main() {
);
} else if should_start_core_on_launch(&config) {
if let Err(error) = start_core_process_inner(process_state.inner(), &config) {
eprintln!("自动启动 CPA 内核失败: {error}");
eprintln!("Failed to automatically start CPA kernel: {error}");
}
}
@@ -2621,7 +2577,7 @@ fn main() {
if let Some(ack_path) = portable_update_ack.as_ref() {
fs::write(ack_path, env!("CARGO_PKG_VERSION").as_bytes())
.map_err(|error| format!("写入应用更新启动确认失败: {error}"))?;
.map_err(|error| format!("Failed to write application update startup confirmation: {error}"))?;
}
Ok(())
@@ -2772,7 +2728,7 @@ fn main() {
return;
}
if let Err(error) = persist_main_window_size(app_handle) {
eprintln!("保存主窗口尺寸失败: {error}");
eprintln!("Failed to save main window size: {error}");
}
app_handle.state::<CoreDownloadState>().cancel();
let app_handle = app_handle.clone();
@@ -2793,7 +2749,7 @@ fn main() {
usage::stop_usage_collector(app_handle);
let deepseek_process_state = app_handle.state::<DeepSeekHarnessProcessState>();
if let Err(error) = stop_managed_deepseek_harness(deepseek_process_state.inner()) {
eprintln!("关闭 DeepSeek Harness 失败: {error}");
eprintln!("Failed to stop DeepSeek Harness: {error}");
}
let gui_config_state = app_handle.state::<GuiConfigState>();
let process_state = app_handle.state::<CoreProcessState>();
+27 -27
View File
@@ -72,11 +72,11 @@ pub(crate) async fn management_request(
"PUT" => reqwest::Method::PUT,
"PATCH" => reqwest::Method::PATCH,
"DELETE" => reqwest::Method::DELETE,
_ => return Err("不支持的管理 API 请求方法".to_string()),
_ => return Err("Unsupported management API request method".to_string()),
};
let path = request.path.trim();
if path.is_empty() || path.contains("://") || path.contains("..") {
return Err("无效的管理 API 路径".to_string());
return Err("Invalid management API path".to_string());
}
let client = management_http_client()?;
@@ -96,7 +96,7 @@ pub(crate) async fn management_request(
let response = builder
.send()
.await
.map_err(|err| format_management_request_error("请求管理 API 失败", &err))?;
.map_err(|err| format_management_request_error("Management API request failed", &err))?;
read_management_value(response).await
}
@@ -108,7 +108,7 @@ pub(crate) async fn upload_auth_file(
) -> Result<serde_json::Value, String> {
let name = name.trim().to_string();
if name.is_empty() || !name.to_ascii_lowercase().ends_with(".json") {
return Err("凭证文件名必须以 .json 结尾".to_string());
return Err("Credential filename must end with .json".to_string());
}
let config = gui_config_state.snapshot()?;
@@ -123,7 +123,7 @@ pub(crate) async fn upload_auth_file(
.body(data)
.send()
.await
.map_err(|err| format_management_request_error("上传凭证文件失败", &err))?;
.map_err(|err| format_management_request_error("Failed to upload credential file", &err))?;
read_management_value(response).await
}
@@ -135,7 +135,7 @@ pub(crate) fn open_auth_files_directory(
let install_dir = core_install_dir()?;
let auth_dir = auth_dir_path_for_core(&config.auth_dir, &install_dir);
fs::create_dir_all(&auth_dir)
.map_err(|error| format!("创建凭证目录失败 {}: {error}", path_to_string(&auth_dir)))?;
.map_err(|error| format!("Failed to create credentials directory {}: {error}", path_to_string(&auth_dir)))?;
open_directory_in_file_manager(&auth_dir)
}
@@ -147,7 +147,7 @@ pub(crate) fn open_core_logs_directory(
let install_dir = core_install_dir()?;
let logs_dir = core_logs_dir_path(&config.auth_dir, &install_dir);
fs::create_dir_all(&logs_dir)
.map_err(|error| format!("创建日志目录失败 {}: {error}", path_to_string(&logs_dir)))?;
.map_err(|error| format!("Failed to create log directory {}: {error}", path_to_string(&logs_dir)))?;
open_directory_in_file_manager(&logs_dir)
}
@@ -169,7 +169,7 @@ fn open_directory_in_file_manager(path: &Path) -> Result<(), String> {
command
.spawn()
.map(|_| ())
.map_err(|error| format!("打开目录失败 {}: {error}", path_to_string(path)))
.map_err(|error| format!("Failed to open directory {}: {error}", path_to_string(path)))
}
#[tauri::command]
@@ -195,7 +195,7 @@ pub(crate) async fn start_oauth_login(
let response = request
.send()
.await
.map_err(|err| format_management_request_error("请求 OAuth 登录链接失败", &err))?;
.map_err(|err| format_management_request_error("Failed to request OAuth login URL", &err))?;
let payload = read_management_json::<OAuthStartApiResponse>(response).await?;
if let Some(error) = payload
.error
@@ -208,7 +208,7 @@ pub(crate) async fn start_oauth_login(
.url
.map(|value| value.trim().to_string())
.filter(|value| !value.is_empty())
.ok_or_else(|| "内核未返回 OAuth 登录链接".to_string())?;
.ok_or_else(|| "Kernel did not return an OAuth login URL".to_string())?;
let state = payload
.state
.map(|value| value.trim().to_string())
@@ -234,7 +234,7 @@ pub(crate) async fn get_oauth_status(
) -> Result<OAuthStatusResult, String> {
let state = state.trim().to_string();
if state.is_empty() {
return Err("OAuth state 不能为空".to_string());
return Err("OAuth state cannot be empty".to_string());
}
let config = gui_config_state.snapshot()?;
let client = management_http_client()?;
@@ -244,7 +244,7 @@ pub(crate) async fn get_oauth_status(
.query(&[("state", state)])
.send()
.await
.map_err(|err| format_management_request_error("查询 OAuth 状态失败", &err))?;
.map_err(|err| format_management_request_error("Failed to query OAuth status", &err))?;
let payload = read_management_json::<OAuthStatusApiResponse>(response).await?;
let status = payload
.status
@@ -269,7 +269,7 @@ pub(crate) async fn submit_oauth_callback(
) -> Result<(), String> {
let redirect_url = redirect_url.trim().to_string();
if redirect_url.is_empty() {
return Err("回调链接不能为空".to_string());
return Err("Callback URL cannot be empty".to_string());
}
let config = gui_config_state.snapshot()?;
let provider_key = normalize_management_oauth_provider(&provider)?;
@@ -284,12 +284,12 @@ pub(crate) async fn submit_oauth_callback(
.json(&body)
.send()
.await
.map_err(|err| format_management_request_error("提交 OAuth 回调失败", &err))?;
.map_err(|err| format_management_request_error("Failed to submit OAuth callback", &err))?;
let status = response.status();
let text = response
.text()
.await
.map_err(|err| format_management_request_error("读取 OAuth 回调响应失败", &err))?;
.map_err(|err| format_management_request_error("Failed to read OAuth callback response", &err))?;
if !status.is_success() {
return Err(format_management_error(status.as_u16(), &text));
}
@@ -304,7 +304,7 @@ pub(crate) fn management_http_client() -> Result<reqwest::Client, String> {
.timeout(Duration::from_secs(30))
.danger_accept_invalid_certs(true)
.build()
.map_err(|err| format_management_request_error("创建管理 API 客户端失败", &err))
.map_err(|err| format_management_request_error("Failed to create management API client", &err))
});
CLIENT.as_ref().cloned().map_err(Clone::clone)
}
@@ -339,14 +339,14 @@ pub(crate) fn format_management_request_error(
pub(crate) fn management_authorization(config: &GuiConfigFile) -> Result<String, String> {
let secret_key = config.management_secret_key.trim();
if secret_key.is_empty() || is_hashed_management_secret_key(secret_key) {
return Err("管理接口不可用:没有可用的明文管理密钥".to_string());
return Err("Management interface unavailable: no plaintext management key is available".to_string());
}
Ok(format!("Bearer {secret_key}"))
}
pub(crate) fn management_endpoint(config: &GuiConfigFile, path: &str) -> Result<String, String> {
if config.port == 0 {
return Err("内核端口无效".to_string());
return Err("Invalid kernel port".to_string());
}
let path = path.trim_start_matches('/');
let origin = core_origin(
@@ -371,7 +371,7 @@ fn normalize_management_oauth_provider(provider: &str) -> Result<String, String>
.bytes()
.all(|byte| byte.is_ascii_lowercase() || byte.is_ascii_digit() || byte == b'-')
{
return Err("无效的 OAuth 提供商".to_string());
return Err("Invalid OAuth provider".to_string());
}
Ok(key)
}
@@ -388,16 +388,16 @@ where
let text = response
.text()
.await
.map_err(|err| format_management_request_error("读取管理 API 响应失败", &err))?;
.map_err(|err| format_management_request_error("Failed to read management API response", &err))?;
if !status.is_success() {
return Err(format_management_error(status.as_u16(), &text));
}
if text.trim().is_empty() {
return Err("管理 API 返回了空响应".to_string());
return Err("Management API returned an empty response".to_string());
}
serde_json::from_str::<T>(&text).map_err(|err| {
format!(
"解析管理 API 响应失败: {err}; body={}",
"Failed to parse management API response: {err}; body={}",
truncate_for_error(&text)
)
})
@@ -410,7 +410,7 @@ pub(crate) async fn read_management_value(
let text = response
.text()
.await
.map_err(|err| format_management_request_error("读取管理 API 响应失败", &err))?;
.map_err(|err| format_management_request_error("Failed to read management API response", &err))?;
if !status.is_success() {
return Err(format_management_error(status.as_u16(), &text));
}
@@ -428,7 +428,7 @@ pub(crate) async fn read_management_text(response: reqwest::Response) -> Result<
let text = response
.text()
.await
.map_err(|err| format_management_request_error("读取管理 API 响应失败", &err))?;
.map_err(|err| format_management_request_error("Failed to read management API response", &err))?;
if !status.is_success() {
return Err(format_management_error(status.as_u16(), &text));
}
@@ -444,15 +444,15 @@ fn format_management_error(status: u16, body: &str) -> String {
{
let message = message.trim();
if !message.is_empty() {
return format!("管理 API 错误 ({status}): {message}");
return format!("Management API error ({status}): {message}");
}
}
}
let body = body.trim();
if body.is_empty() {
format!("管理 API 错误 ({status})")
format!("Management API error ({status})")
} else {
format!("管理 API 错误 ({status}): {}", truncate_for_error(body))
format!("Management API error ({status}): {}", truncate_for_error(body))
}
}
+113
View File
@@ -0,0 +1,113 @@
#[derive(Clone, Copy)]
pub(crate) enum NativeText {
OpenMainWindow,
Quit,
CoreStatusWorking,
CoreStatusNotInstalled,
CoreStatusRunning,
CoreStatusStopped,
CoreStatusChecking,
Working,
StopCore,
StartCore,
RestartCore,
TooltipCoreWorking,
TooltipCoreNotInstalled,
TooltipCoreRunning,
TooltipCoreStopped,
TooltipCoreOperationFailed,
OperationFailed,
}
pub(crate) fn native_text(locale: &str, text: NativeText) -> &'static str {
match super::normalize_app_locale(locale) {
"zh-CN" => match text {
NativeText::OpenMainWindow => "打开主界面",
NativeText::Quit => "退出",
NativeText::CoreStatusWorking => "内核状态:处理中",
NativeText::CoreStatusNotInstalled => "内核状态:未安装",
NativeText::CoreStatusRunning => "内核状态:运行中",
NativeText::CoreStatusStopped => "内核状态:已停止",
NativeText::CoreStatusChecking => "内核状态:正在检查",
NativeText::Working => "处理中...",
NativeText::StopCore => "停止内核",
NativeText::StartCore => "启动内核",
NativeText::RestartCore => "重启内核",
NativeText::TooltipCoreWorking => "EasyCLIProxyAPI · 内核处理中",
NativeText::TooltipCoreNotInstalled => "EasyCLIProxyAPI · 内核未安装",
NativeText::TooltipCoreRunning => "EasyCLIProxyAPI · 内核运行中",
NativeText::TooltipCoreStopped => "EasyCLIProxyAPI · 内核已停止",
NativeText::TooltipCoreOperationFailed => "EasyCLIProxyAPI · 内核操作失败",
NativeText::OperationFailed => "操作失败",
},
"zh-TW" => match text {
NativeText::OpenMainWindow => "開啟主介面",
NativeText::Quit => "退出",
NativeText::CoreStatusWorking => "核心狀態:處理中",
NativeText::CoreStatusNotInstalled => "核心狀態:未安裝",
NativeText::CoreStatusRunning => "核心狀態:執行中",
NativeText::CoreStatusStopped => "核心狀態:已停止",
NativeText::CoreStatusChecking => "核心狀態:正在檢查",
NativeText::Working => "處理中...",
NativeText::StopCore => "停止核心",
NativeText::StartCore => "啟動核心",
NativeText::RestartCore => "重新啟動核心",
NativeText::TooltipCoreWorking => "EasyCLIProxyAPI · 核心處理中",
NativeText::TooltipCoreNotInstalled => "EasyCLIProxyAPI · 核心未安裝",
NativeText::TooltipCoreRunning => "EasyCLIProxyAPI · 核心執行中",
NativeText::TooltipCoreStopped => "EasyCLIProxyAPI · 核心已停止",
NativeText::TooltipCoreOperationFailed => "EasyCLIProxyAPI · 核心操作失敗",
NativeText::OperationFailed => "操作失敗",
},
"ja" => match text {
NativeText::OpenMainWindow => "メイン画面を開く",
NativeText::Quit => "終了",
NativeText::CoreStatusWorking => "コア状態:処理中",
NativeText::CoreStatusNotInstalled => "コア状態:未インストール",
NativeText::CoreStatusRunning => "コア状態:実行中",
NativeText::CoreStatusStopped => "コア状態:停止済み",
NativeText::CoreStatusChecking => "コア状態:確認中",
NativeText::Working => "処理中...",
NativeText::StopCore => "コアを停止",
NativeText::StartCore => "コアを起動",
NativeText::RestartCore => "コアを再起動",
NativeText::TooltipCoreWorking => "EasyCLIProxyAPI · コア処理中",
NativeText::TooltipCoreNotInstalled => "EasyCLIProxyAPI · コア未インストール",
NativeText::TooltipCoreRunning => "EasyCLIProxyAPI · コア実行中",
NativeText::TooltipCoreStopped => "EasyCLIProxyAPI · コア停止済み",
NativeText::TooltipCoreOperationFailed => "EasyCLIProxyAPI · コア操作失敗",
NativeText::OperationFailed => "操作に失敗しました",
},
_ => match text {
NativeText::OpenMainWindow => "Open Main Window",
NativeText::Quit => "Quit",
NativeText::CoreStatusWorking => "Core status: Working",
NativeText::CoreStatusNotInstalled => "Core status: Not installed",
NativeText::CoreStatusRunning => "Core status: Running",
NativeText::CoreStatusStopped => "Core status: Stopped",
NativeText::CoreStatusChecking => "Core status: Checking",
NativeText::Working => "Working...",
NativeText::StopCore => "Stop Core",
NativeText::StartCore => "Start Core",
NativeText::RestartCore => "Restart Core",
NativeText::TooltipCoreWorking => "EasyCLIProxyAPI · Core working",
NativeText::TooltipCoreNotInstalled => "EasyCLIProxyAPI · Core not installed",
NativeText::TooltipCoreRunning => "EasyCLIProxyAPI · Core running",
NativeText::TooltipCoreStopped => "EasyCLIProxyAPI · Core stopped",
NativeText::TooltipCoreOperationFailed => "EasyCLIProxyAPI · Core operation failed",
NativeText::OperationFailed => "Operation failed",
},
}
}
pub(crate) fn native_operation_failed(locale: &str, summary: &str) -> String {
let separator = if super::normalize_app_locale(locale) == "en" {
": "
} else {
":"
};
format!(
"{}{separator}{summary}",
native_text(locale, NativeText::OperationFailed)
)
}
+20 -20
View File
@@ -3,7 +3,7 @@ use super::*;
pub(crate) fn normalize_proxy_url(value: &str) -> Result<String, String> {
let value = value.trim();
let invalid = || {
"代理地址无效,请使用 http://host:port、https://host:port 或 socks5://host:port(不是 PAC 地址)".to_string()
"Invalid proxy address. Use http://host:port, https://host:port, or socks5://host:port (not a PAC address)".to_string()
};
if value.is_empty() || value.chars().any(|c| c.is_control() || c.is_whitespace()) {
return Err(invalid());
@@ -38,7 +38,7 @@ fn fixed_proxy(value: &str, scheme: &str) -> Result<String, String> {
fn parse_detected_proxy(value: &str) -> Result<String, String> {
let value = value.trim().trim_matches('\'').trim_matches('"');
if value.is_empty() {
return Err("系统代理地址为空".to_string());
return Err("System proxy address is empty".to_string());
}
if value.contains("://") {
normalize_proxy_url(value)
@@ -51,7 +51,7 @@ fn parse_detected_proxy(value: &str) -> Result<String, String> {
fn parse_env_proxy(name: &str, value: &str) -> Result<String, String> {
let value = value.trim().trim_matches('\'').trim_matches('"');
if value.is_empty() {
return Err("系统代理地址为空".to_string());
return Err("System proxy address is empty".to_string());
}
if !value.contains("://") && name.eq_ignore_ascii_case("socks_proxy") {
return normalize_proxy_url(&format!("socks5://{value}"));
@@ -90,12 +90,12 @@ fn parse_windows_proxy(value: &str) -> Result<String, String> {
);
}
}
Err("系统代理没有可用的 HTTP / HTTPS / SOCKS 地址".to_string())
Err("System proxy has no available HTTP / HTTPS / SOCKS address".to_string())
}
#[cfg(any(target_os = "windows", test))]
fn pac_error() -> String {
"检测到 PAC / 自动发现代理,暂不支持转换为内核固定代理".to_string()
"PAC / auto-discovery proxy detected; conversion to a fixed kernel proxy is not supported yet".to_string()
}
#[cfg(target_os = "windows")]
@@ -119,7 +119,7 @@ fn detect_system_proxy() -> Result<String, String> {
unsafe {
let mut config: WINHTTP_CURRENT_USER_IE_PROXY_CONFIG = std::mem::zeroed();
if WinHttpGetIEProxyConfigForCurrentUser(&mut config) == 0 {
return Err("无法读取当前用户的 Windows 系统代理".to_string());
return Err("Unable to read Windows system proxy for the current user".to_string());
}
let proxy = read_wide(config.lpszProxy);
let pac = read_wide(config.lpszAutoConfigUrl);
@@ -181,17 +181,17 @@ fn desktop_setting(program: &str, args: &[&str]) -> Result<String, String> {
.stderr(Stdio::null());
let mut child = command
.spawn()
.map_err(|_| format!("无法运行 {program} 读取系统代理"))?;
.map_err(|_| format!("Unable to run {program} to read system proxy"))?;
let deadline = std::time::Instant::now() + Duration::from_millis(400);
loop {
match child.try_wait() {
Ok(Some(status)) => {
if !status.success() {
return Err(format!("{program} 读取系统代理失败"));
return Err(format!("{program} failed to read system proxy"));
}
let output = child
.wait_with_output()
.map_err(|_| "读取系统代理失败".to_string())?;
.map_err(|_| "Failed to read system proxy".to_string())?;
return Ok(String::from_utf8_lossy(&output.stdout).trim().to_string());
}
Ok(None) if std::time::Instant::now() < deadline => {
@@ -200,7 +200,7 @@ fn desktop_setting(program: &str, args: &[&str]) -> Result<String, String> {
_ => {
let _ = child.kill();
let _ = child.wait();
return Err("读取系统代理超时".to_string());
return Err("Timed out while reading system proxy".to_string());
}
}
}
@@ -300,10 +300,10 @@ fn parse_networksetup_proxy(output: &str, scheme: &str) -> Result<String, String
let key = key.trim();
let value = value.trim();
if key.eq_ignore_ascii_case("Enabled") {
enabled = value.eq_ignore_ascii_case("Yes") || value == "是";
} else if key.eq_ignore_ascii_case("Server") || key == "服务器" {
enabled = value.eq_ignore_ascii_case("Yes") || value == "Yes";
} else if key.eq_ignore_ascii_case("Server") || key == "Server" {
host = value.to_string();
} else if key.eq_ignore_ascii_case("Port") || key == "端口" {
} else if key.eq_ignore_ascii_case("Port") || key == "Port" {
port = value.to_string();
}
}
@@ -420,7 +420,7 @@ fn kde_config(key: &str) -> Result<String, String> {
return Ok(value);
}
}
Err("无法读取 KDE 系统代理".to_string())
Err("Unable to read KDE system proxy".to_string())
}
#[cfg(target_os = "linux")]
@@ -511,7 +511,7 @@ fn apply(state: &GuiConfigState) -> Result<GuiConfigFile, String> {
let snapshot = state
.inner
.lock()
.map_err(|_| "代理配置锁已损坏".to_string())?
.map_err(|_| "Proxy configuration lock is poisoned".to_string())?
.clone();
let detected = if snapshot.proxy_override {
None
@@ -521,7 +521,7 @@ fn apply(state: &GuiConfigState) -> Result<GuiConfigFile, String> {
let mut current = state
.inner
.lock()
.map_err(|_| "代理配置锁已损坏".to_string())?;
.map_err(|_| "Proxy configuration lock is poisoned".to_string())?;
let mut next = current.clone();
next.proxy_url = if next.proxy_override {
next.proxy_url.clone()
@@ -567,7 +567,7 @@ pub(crate) fn set_manual(state: &GuiConfigState, proxy_url: String) -> Result<()
let mut current = state
.inner
.lock()
.map_err(|_| "代理配置锁已损坏".to_string())?;
.map_err(|_| "Proxy configuration lock is poisoned".to_string())?;
let mut next = current.clone();
next.proxy_override = !proxy_url.is_empty();
next.proxy_url = if next.proxy_override {
@@ -609,10 +609,10 @@ pub(crate) async fn verify_core_proxy(config: &GuiConfigFile) -> Result<(), Stri
.timeout(Duration::from_secs(3))
.send()
.await
.map_err(|_| "无法更新内核代理,请重启内核后重试".to_string())?;
.map_err(|_| "Unable to update kernel proxy. Restart the kernel and try again".to_string())?;
if !response.status().is_success() {
return Err(
"内核拒绝代理更新,请重启内核或升级内核后重试".to_string()
"Kernel rejected the proxy update. Restart or upgrade the kernel and try again".to_string()
);
}
updated = true;
@@ -624,7 +624,7 @@ pub(crate) async fn verify_core_proxy(config: &GuiConfigFile) -> Result<(), Stri
tokio::time::sleep(Duration::from_millis(200)).await;
}
Err(
"代理配置已保存,但尚未确认内核已应用。请确认内核正在运行,必要时重启内核后重试。"
"Proxy configuration was saved, but the kernel has not confirmed applying it. Make sure the kernel is running and restart it if necessary."
.to_string(),
)
}
+15 -15
View File
@@ -272,7 +272,7 @@ fn persist_provider_health_success(
},
});
if let Err(error) = usage::persist_local_usage_event(app, "desktop_health_check", event) {
eprintln!("保存桌面健康检测使用记录失败: {error}");
eprintln!("Failed to save desktop health check usage record: {error}");
}
}
@@ -290,19 +290,19 @@ pub(crate) async fn provider_health_probe(
request: ProviderHealthProbeRequest,
) -> Result<ProviderHealthProbeResponse, String> {
let url = reqwest::Url::parse(request.url.trim())
.map_err(|error| format!("健康检测地址无效: {error}"))?;
.map_err(|error| format!("Invalid health check URL: {error}"))?;
if !matches!(url.scheme(), "http" | "https") || url.host_str().is_none() {
return Err("健康检测仅支持 HTTP 或 HTTPS 地址".to_string());
return Err("Health checks support only HTTP or HTTPS URLs".to_string());
}
if !matches!(
request.protocol.as_str(),
"openai-chat" | "openai-responses" | "claude" | "gemini"
) {
return Err("不支持的健康检测协议".to_string());
return Err("Unsupported health check protocol".to_string());
}
let endpoint = format!("POST {}", url.path());
if request.data.len() > 64 * 1024 {
return Err("健康检测请求体过大".to_string());
return Err("Health check request body is too large".to_string());
}
let _permit = PROVIDER_HEALTH_SLOTS
@@ -321,14 +321,14 @@ pub(crate) async fn provider_health_probe(
.connect_timeout(Duration::from_secs(10))
.timeout(timeout),
proxy_url,
"创建健康检测客户端失败",
"Failed to create health check client",
)?;
let mut headers = reqwest::header::HeaderMap::new();
for (name, value) in &request.header {
let name = reqwest::header::HeaderName::from_bytes(name.as_bytes())
.map_err(|error| format!("健康检测请求头名称无效: {error}"))?;
.map_err(|error| format!("Invalid health check request header name: {error}"))?;
let value = reqwest::header::HeaderValue::from_str(value)
.map_err(|error| format!("健康检测请求头值无效: {error}"))?;
.map_err(|error| format!("Invalid health check request header value: {error}"))?;
headers.insert(name, value);
}
if !headers.contains_key(reqwest::header::USER_AGENT) {
@@ -345,15 +345,15 @@ pub(crate) async fn provider_health_probe(
.body(request.data.clone())
.send()
.await
.map_err(|error| format!("健康检测请求失败: {error}"))?;
.map_err(|error| format!("Health check request failed: {error}"))?;
let status = response.status();
if !status.is_success() {
let detail = response.text().await.unwrap_or_default();
let detail = detail.trim();
return Err(if detail.is_empty() {
format!("上游返回 HTTP {}", status.as_u16())
format!("Upstream returned HTTP {}", status.as_u16())
} else {
format!("上游返回 HTTP {}: {}", status.as_u16(), detail)
format!("Upstream returned HTTP {}: {}", status.as_u16(), detail)
});
}
let content_type = response
@@ -363,15 +363,15 @@ pub(crate) async fn provider_health_probe(
.unwrap_or("")
.to_ascii_lowercase();
if !provider_health_content_type_is_streaming(&content_type) {
return Err("上游未返回流式响应,无法测量首字延迟".to_string());
return Err("Upstream did not return a streaming response; time to first token cannot be measured".to_string());
}
let mut stream = response.bytes_stream();
let mut received = Vec::new();
while let Some(chunk) = stream.next().await {
let chunk = chunk.map_err(|error| format!("读取健康检测流失败: {error}"))?;
let chunk = chunk.map_err(|error| format!("Failed to read health check stream: {error}"))?;
if received.len().saturating_add(chunk.len()) > MAX_PROVIDER_HEALTH_STREAM_BYTES {
return Err("健康检测在限制范围内未收到模型首字".to_string());
return Err("Health check did not receive the model's first token within the limit".to_string());
}
received.extend_from_slice(&chunk);
let elapsed_ms = started_at.elapsed().as_millis().max(1) as u64;
@@ -397,5 +397,5 @@ pub(crate) async fn provider_health_probe(
});
}
}
Err("健康检测未收到模型首字".to_string())
Err("Health check did not receive the model's first token".to_string())
}
+1 -1
View File
@@ -1632,7 +1632,7 @@ fn deepseek_harness_credentials_reject_legacy_flat_layout() {
)
.unwrap_err();
assert!(error.contains("缺少 version 字段"), "{error}");
assert!(error.contains("missing the version field"), "{error}");
}
#[test]
+2 -2
View File
@@ -664,7 +664,7 @@ fn applied_state_rejects_backup_paths_outside_the_managed_config_set() {
let error = load_agent_applied_state(AgentClient::OpenCode, &home)
.err()
.unwrap();
assert!(error.contains("非预期备份路径"));
assert!(error.contains("unexpected backup path"));
fs::remove_dir_all(home).unwrap();
}
@@ -707,7 +707,7 @@ fn missing_session_backup_is_detected_before_any_file_is_restored() {
restore_agent_applied_state_configuration(AgentClient::Codex, &paths, &state_path, &state)
.err()
.unwrap();
assert!(error.contains("读取智能体备份失败"));
assert!(error.contains("Failed to read agent backup"));
assert_eq!(fs::read_to_string(&config_path).unwrap(), "managed-config");
assert_eq!(
fs::read_to_string(&catalog_path).unwrap(),
+1 -1
View File
@@ -82,7 +82,7 @@ custom_option = "keep-original"
assert!(inspection
.warnings
.iter()
.any(|warning| warning.contains("自动重新同步")));
.any(|warning| warning.contains("resynchronized automatically")));
let updated = update_agent_modification(
AgentClient::Codex,
+10 -8
View File
@@ -200,7 +200,7 @@ async fn alias_save_recovers_failures_before_and_after_writes() {
let core = MockCore::new(CURRENT, failure);
let updated = CURRENT.replace("my-alias", "renamed");
let result = put_management_alias_config_changes(&core.config, CURRENT, &updated).await;
assert!(result.unwrap_err().contains("已恢复原配置"));
assert!(result.unwrap_err().contains("Original configuration was restored"));
let (persisted, requests) = core.finish();
assert_eq!(persisted, yaml_json(CURRENT), "{requests:?}");
assert_eq!(
@@ -223,8 +223,8 @@ async fn alias_save_reports_failed_rollback_without_claiming_restoration() {
)
.await;
let error = result.unwrap_err();
assert!(error.contains("自动恢复失败"));
assert!(error.contains("配置可能已部分写入"));
assert!(error.contains("automatic restoration failed"));
assert!(error.contains("configuration may be partially written"));
let (persisted, _) = core.finish();
assert_eq!(
persisted["oauth-model-alias"]["codex"][0]["alias"],
@@ -242,7 +242,9 @@ async fn alias_save_does_not_rollback_over_external_changes() {
&CURRENT.replace("my-alias", "renamed"),
)
.await;
assert!(result.unwrap_err().contains("检测到其他配置修改"));
assert!(result
.unwrap_err()
.contains("Other configuration changes were detected"));
let (persisted, requests) = core.finish();
assert_eq!(persisted["debug"], true);
assert_eq!(
@@ -263,7 +265,7 @@ async fn alias_save_rejects_stale_snapshot_before_any_write() {
&CURRENT.replace("my-alias", "renamed"),
)
.await;
assert!(result.unwrap_err().contains("配置已变化"));
assert!(result.unwrap_err().contains("Configuration changed"));
let (_, requests) = core.finish();
assert_eq!(requests, ["GET /v0/management/config.yaml"]);
}
@@ -317,7 +319,7 @@ async fn alias_save_can_retry_after_restoration_reformats_yaml() {
put_management_alias_config_changes(&core.config, CURRENT, &updated)
.await
.unwrap_err()
.contains("已恢复原配置")
.contains("Original configuration was restored")
);
let restored = fetch_management_config_yaml(&core.config).await.unwrap();
validate_model_alias_revision(&restored, Some(&context.revision)).unwrap();
@@ -575,7 +577,7 @@ async fn desktop_backup_restore_rejects_core_changes_since_preview() {
execute_restore_plan(&core.config, plan, &preview.preview.revision)
.await
.unwrap_err()
.contains("预览后")
.contains("changed after preview")
);
assert_eq!(
current_desktop_mappings(&fixture.home)
@@ -600,7 +602,7 @@ async fn desktop_backup_restore_rolls_back_core_when_local_files_change() {
assert!(execute_restore_plan(&core.config, plan, &revision)
.await
.unwrap_err()
.contains("已恢复原配置"));
.contains("Original configuration was restored"));
assert_eq!(config_images(&paths).unwrap(), before);
assert_eq!(
current_desktop_mappings(&fixture.home)
+1 -1
View File
@@ -93,7 +93,7 @@ fn gui_config_defaults_are_stable() {
assert!(content.contains("auth-dir = \"../oauth\""));
assert!(content.contains("[[api-keys]]"));
assert!(content.contains("key = \"123456\""));
assert!(content.contains("remark = \"默认密钥\""));
assert!(content.contains("remark = \"Default key\""));
assert!(content.contains("management-secret-key = \"\""));
assert!(content.contains("plugins-enabled = false"));
assert!(content.contains("routing-strategy = \"round-robin\""));
+1 -1
View File
@@ -496,7 +496,7 @@ async fn failed_or_cancelled_refresh_never_downloads_the_previous_offer() {
let (result, _) = tokio::join!(refresh, async {
state.cancel();
});
assert_eq!(result.err().as_deref(), Some("应用更新下载已取消"));
assert_eq!(result.err().as_deref(), Some("Application update download canceled"));
assert!(state.snapshot().target_version.is_none());
let pending = PendingAppUpdate {
version: "v0.3.3".to_string(),
+6 -6
View File
@@ -26,7 +26,7 @@ fn api_key_remarks_follow_matching_core_keys() {
default_api_key_entry(),
GuiApiKeyEntry {
key: "custom-key".to_string(),
remark: "开发环境".to_string(),
remark: "Development environment".to_string(),
},
];
let core_keys = vec!["custom-key".to_string(), "new-key".to_string()];
@@ -34,7 +34,7 @@ fn api_key_remarks_follow_matching_core_keys() {
let merged = merge_core_api_keys_with_gui_metadata(&existing, &core_keys, None);
assert_eq!(gui_api_key_values(&merged), vec!["custom-key", "new-key"]);
assert_eq!(merged[0].remark, "开发环境");
assert_eq!(merged[0].remark, "Development environment");
assert!(merged[1].remark.is_empty());
}
@@ -279,7 +279,7 @@ fn packaged_macos_auth_migration_does_not_overwrite_conflicting_credentials() {
let error = migrate_auth_dir_from_macos_app_bundle(&mut config, &install_dir, &destination)
.unwrap_err();
assert!(error.contains("未覆盖"), "{error}");
assert!(error.contains("not overwritten"), "{error}");
assert_eq!(config.auth_dir, original_auth_dir);
assert_eq!(
fs::read(destination.join("account.json")).unwrap(),
@@ -997,7 +997,7 @@ fn managed_session_settings_use_canonical_yaml_and_preserve_unrelated_content()
#[test]
fn optional_core_strings_are_trimmed_and_reject_control_characters() {
assert_eq!(
normalize_optional_config_string(" socks5://proxy:7890 ".to_string(), "代理 URL")
normalize_optional_config_string(" socks5://proxy:7890 ".to_string(), "Proxy URL")
.unwrap(),
"socks5://proxy:7890"
);
@@ -1005,7 +1005,7 @@ fn optional_core_strings_are_trimmed_and_reject_control_characters() {
normalize_optional_config_string(" 1h ".to_string(), "TTL").unwrap(),
"1h"
);
assert!(normalize_optional_config_string("bad\nvalue".to_string(), "代理 URL").is_err());
assert!(normalize_optional_config_string("bad\nvalue".to_string(), "Proxy URL").is_err());
}
#[test]
@@ -1238,7 +1238,7 @@ fn startup_preserves_all_user_owned_yaml_and_only_applies_gui_managed_values() {
default_api_key_entry(),
GuiApiKeyEntry {
key: "gui-key".to_string(),
remark: "测试密钥".to_string(),
remark: "Test key".to_string(),
},
],
api_access_remarks: Vec::new(),
+6 -6
View File
@@ -121,11 +121,11 @@ fn exiting_rejects_new_core_operations_and_cleans_up_an_in_flight_child() {
assert!(lock_core_operation(&state)
.unwrap_err()
.contains("应用正在退出"));
.contains("Application is exiting"));
assert!(state
.store_child(child)
.unwrap_err()
.contains("应用正在退出"));
.contains("Application is exiting"));
assert!(!is_process_alive(child_id));
assert_eq!(state.managed_pid(), None);
}
@@ -416,7 +416,7 @@ fn successful_core_install_reports_automatic_restart_failure() {
let result = combine_install_and_restart_results(Ok("installed"), Err("port busy".into()));
assert_eq!(
result.unwrap_err(),
"内核已安装,但自动恢复运行失败: port busy"
"Kernel installed, but failed to automatically resume operation: port busy"
);
}
@@ -435,7 +435,7 @@ fn failed_core_install_reports_restart_failure_too() {
);
assert_eq!(
result.unwrap_err(),
"checksum mismatch;自动恢复原内核运行状态也失败: port busy"
"checksum mismatch; failed to automatically restore the previous kernel running state: port busy"
);
}
@@ -534,7 +534,7 @@ fn overlaying_a_core_updates_packaged_files_and_preserves_plugins() {
.unwrap()
.ino(),
original_binary_inode,
"更新后的内核必须使用新 inode"
"The updated kernel must use a new inode"
);
assert!(!staging_dir.exists());
fs::remove_dir_all(root).unwrap();
@@ -959,7 +959,7 @@ fn core_start_log_captures_stdout_and_stderr() {
assert!(command.status().unwrap().success());
let output = fs::read_to_string(&log_path).unwrap();
assert!(output.contains("===== CPA 内核启动"));
assert!(output.contains("===== CPA kernel startup"));
assert!(output.contains("core stdout marker"));
assert!(output.contains("core stderr marker"));
assert!(!output.contains("stale startup output"));
+2 -2
View File
@@ -156,7 +156,7 @@ fn desktop_custom_alias_edits_remove_old_managed_routes_and_preserve_user_routes
assert!(entries.iter().any(|m| m["alias"] == "claude-user-model"));
assert!(!entries.iter().any(|m| m["alias"] == "claude-opus-4-6" || m["alias"] == "claude-sonnet-4-6"));
assert!(ensure_claude_desktop_model_aliases_in_yaml(input,
&custom_mappings(&[("gpt-two", "claude-user-model")]), &models).unwrap_err().contains("已被其他模型使用"));
&custom_mappings(&[("gpt-two", "claude-user-model")]), &models).unwrap_err().contains("already used by another model"));
assert!(ensure_claude_desktop_model_aliases_in_yaml(input,
&custom_mappings(&[("gpt-two", "claude-native-model")]),
&test_agent_models(&["gpt-two", "claude-native-model"])).is_err());
@@ -195,7 +195,7 @@ fn desktop_manual_source_can_create_an_alias_when_missing_from_the_loaded_model_
assert!(routed["openai-compatibility"][0]["models"].as_array().unwrap().iter()
.any(|entry| entry["name"] == model && entry["alias"] == "claude-opus-5"));
assert!(ensure_claude_desktop_model_aliases_in_yaml("{}", &mappings, &[])
.unwrap_err().contains("无法确定模型 manual-model"));
.unwrap_err().contains("CPA configuration source for model manual-model"));
}
#[test]
+3 -3
View File
@@ -565,10 +565,10 @@ fn thinking_alias_effort_accepts_provider_defined_levels() {
#[test]
fn existing_aliases_with_spaces_can_be_loaded_and_deleted() {
assert_eq!(
existing_thinking_alias_model_id(" Codex Auto Review ", "别名模型").unwrap(),
existing_thinking_alias_model_id(" Codex Auto Review ", "Alias model").unwrap(),
"Codex Auto Review"
);
assert!(validate_thinking_alias_model_id("Codex Auto Review", "别名模型").is_err());
assert!(validate_thinking_alias_model_id("Codex Auto Review", "Alias model").is_err());
let content = "codex-api-key:\n - name: provider\n base-url: https://www.loomex.cc\n models:\n - name: codex-auto-review\n alias: Codex Auto Review\n - name: keep-me\n";
let entries = thinking_aliases_from_yaml(content).unwrap();
assert_eq!(entries.len(), 1);
@@ -615,7 +615,7 @@ fn thinking_alias_rejects_duplicate_client_visible_name() {
assert!(
add_model_alias_to_yaml(input, &source, "GPT-5.5-HIGH", "high", false)
.unwrap_err()
.contains("已存在")
.contains("already exists")
);
}
+3 -3
View File
@@ -51,14 +51,14 @@ fn windows_tray_presentation_tracks_core_state_and_busy_actions() {
}
#[test]
fn app_locale_normalization_has_a_stable_chinese_fallback() {
fn app_locale_normalization_has_a_stable_english_fallback() {
assert_eq!(normalize_app_locale("en"), "en");
assert_eq!(normalize_app_locale("en-US"), "en");
assert_eq!(normalize_app_locale("ja-JP"), "ja");
assert_eq!(normalize_app_locale("zh-TW"), "zh-TW");
assert_eq!(normalize_app_locale("zh-Hant-HK"), "zh-TW");
assert_eq!(normalize_app_locale("unsupported"), "zh-CN");
assert_eq!(GuiConfigFile::default().locale, "zh-CN");
assert_eq!(normalize_app_locale("unsupported"), "en");
assert_eq!(GuiConfigFile::default().locale, "en");
}
#[cfg(target_os = "windows")]
+61 -85
View File
@@ -1,4 +1,5 @@
use super::*;
use crate::native_i18n::{native_operation_failed, native_text, NativeText};
#[cfg(target_os = "macos")]
use objc2_app_kit::NSStatusItemBehavior;
@@ -18,7 +19,7 @@ pub(crate) struct MacosTrayClickState {
#[cfg(target_os = "macos")]
pub(crate) fn set_macos_dock_visible(app_handle: &tauri::AppHandle, visible: bool) {
if let Err(error) = app_handle.set_dock_visibility(visible) {
eprintln!("更新 Dock 图标状态失败: {error}");
eprintln!("Failed to update Dock icon visibility: {error}");
}
}
@@ -29,17 +30,17 @@ pub(crate) fn show_main_window_on_main_thread(app_handle: &tauri::AppHandle) {
};
set_macos_dock_visible(app_handle, true);
if let Err(error) = window.show() {
eprintln!("显示主窗口失败: {error}");
eprintln!("Failed to show the main window: {error}");
set_macos_dock_visible(app_handle, false);
return;
}
if window.is_minimized().unwrap_or(false) {
if let Err(error) = window.unminimize() {
eprintln!("恢复主窗口失败: {error}");
eprintln!("Failed to restore the main window: {error}");
}
}
if let Err(error) = window.set_focus() {
eprintln!("聚焦主窗口失败: {error}");
eprintln!("Failed to focus the main window: {error}");
}
}
@@ -54,7 +55,7 @@ pub(crate) fn show_main_window(app_handle: &tauri::AppHandle) {
if let Err(error) = app_handle.clone().run_on_main_thread(move || {
show_main_window_on_main_thread(&app_handle);
}) {
eprintln!("调度主窗口显示失败: {error}");
eprintln!("Failed to schedule showing the main window: {error}");
}
}
@@ -72,7 +73,7 @@ pub(crate) fn show_macos_tray_menu<R: tauri::Runtime>(tray: &TrayIcon<R>) {
});
if let Err(error) = result {
eprintln!("显示托盘菜单失败: {error}");
eprintln!("Failed to show the tray menu: {error}");
}
}
@@ -82,18 +83,18 @@ pub(crate) fn setup_macos_tray(app: &mut tauri::App<tauri::Wry>) -> tauri::Resul
.state::<GuiConfigState>()
.snapshot()
.map(|config| config.locale)
.unwrap_or_else(|_| "zh-CN".to_string());
.unwrap_or_else(|_| "en".to_string());
let open_main_window = MenuItem::with_id(
app,
"open-main-window",
locale_text(&locale, "打开主界面", "Open Main Window"),
native_text(&locale, NativeText::OpenMainWindow),
true,
None::<&str>,
)?;
let quit = MenuItem::with_id(
app,
"quit",
locale_text(&locale, "退出", "Quit"),
native_text(&locale, NativeText::Quit),
true,
None::<&str>,
)?;
@@ -128,7 +129,7 @@ pub(crate) fn setup_macos_tray(app: &mut tauri::App<tauri::Wry>) -> tauri::Resul
let now = Instant::now();
let Ok(mut state) = click_state.lock() else {
eprintln!("读取托盘点击状态失败");
eprintln!("Failed to read tray click state");
return;
};
state.sequence += 1;
@@ -159,7 +160,7 @@ pub(crate) fn setup_macos_tray(app: &mut tauri::App<tauri::Wry>) -> tauri::Resul
}
Ok(_) => false,
Err(_) => {
eprintln!("读取托盘点击状态失败");
eprintln!("Failed to read tray click state");
false
}
};
@@ -220,45 +221,29 @@ pub(crate) fn windows_tray_presentation(
locale: &str,
) -> WindowsTrayPresentation {
let status_text = if busy {
locale_text(locale, "内核状态:处理中", "Core status: Working")
native_text(locale, NativeText::CoreStatusWorking)
} else if !status.installed {
locale_text(locale, "内核状态:未安装", "Core status: Not installed")
native_text(locale, NativeText::CoreStatusNotInstalled)
} else if status.running {
locale_text(locale, "内核状态:运行中", "Core status: Running")
native_text(locale, NativeText::CoreStatusRunning)
} else {
locale_text(locale, "内核状态:已停止", "Core status: Stopped")
native_text(locale, NativeText::CoreStatusStopped)
};
let toggle_text = if busy {
locale_text(locale, "处理中...", "Working...")
native_text(locale, NativeText::Working)
} else if status.running {
locale_text(locale, "停止内核", "Stop Core")
native_text(locale, NativeText::StopCore)
} else {
locale_text(locale, "启动内核", "Start Core")
native_text(locale, NativeText::StartCore)
};
let tooltip = if busy {
locale_text(
locale,
"EasyCLIProxyAPI · 内核处理中",
"EasyCLIProxyAPI · Core working",
)
native_text(locale, NativeText::TooltipCoreWorking)
} else if !status.installed {
locale_text(
locale,
"EasyCLIProxyAPI · 内核未安装",
"EasyCLIProxyAPI · Core not installed",
)
native_text(locale, NativeText::TooltipCoreNotInstalled)
} else if status.running {
locale_text(
locale,
"EasyCLIProxyAPI · 内核运行中",
"EasyCLIProxyAPI · Core running",
)
native_text(locale, NativeText::TooltipCoreRunning)
} else {
locale_text(
locale,
"EasyCLIProxyAPI · 内核已停止",
"EasyCLIProxyAPI · Core stopped",
)
native_text(locale, NativeText::TooltipCoreStopped)
};
WindowsTrayPresentation {
@@ -287,7 +272,7 @@ impl WindowsTrayState {
self.locale
.lock()
.map(|locale| locale.clone())
.unwrap_or_else(|_| "zh-CN".to_string())
.unwrap_or_else(|_| "en".to_string())
}
fn set_locale(&self, locale: &str) {
@@ -298,17 +283,17 @@ impl WindowsTrayState {
let labels = [
(
&self.open_main_window,
locale_text(normalized, "打开主界面", "Open Main Window"),
native_text(normalized, NativeText::OpenMainWindow),
),
(
&self.restart_core_item,
locale_text(normalized, "重启内核", "Restart Core"),
native_text(normalized, NativeText::RestartCore),
),
(&self.quit_item, locale_text(normalized, "退出", "Quit")),
(&self.quit_item, native_text(normalized, NativeText::Quit)),
];
for (item, label) in labels {
if let Err(error) = item.set_text(label) {
eprintln!("更新 Windows 托盘语言失败: {error}");
eprintln!("Failed to update Windows tray language: {error}");
}
}
}
@@ -323,24 +308,22 @@ impl WindowsTrayState {
}
let locale = self.locale();
if let Err(error) = self.status_item.set_text(locale_text(
&locale,
"内核状态:处理中",
"Core status: Working",
)) {
eprintln!("更新 Windows 托盘内核状态失败: {error}");
if let Err(error) = self
.status_item
.set_text(native_text(&locale, NativeText::CoreStatusWorking)) {
eprintln!("Failed to update Windows tray core status: {error}");
}
if let Err(error) =
self.toggle_core_item
.set_text(locale_text(&locale, "处理中...", "Working..."))
.set_text(native_text(&locale, NativeText::Working))
{
eprintln!("更新 Windows 托盘操作文本失败: {error}");
eprintln!("Failed to update Windows tray action text: {error}");
}
if let Err(error) = self.toggle_core_item.set_enabled(false) {
eprintln!("更新 Windows 托盘操作状态失败: {error}");
eprintln!("Failed to update Windows tray action state: {error}");
}
if let Err(error) = self.restart_core_item.set_enabled(false) {
eprintln!("更新 Windows 托盘重启状态失败: {error}");
eprintln!("Failed to update Windows tray restart state: {error}");
}
true
}
@@ -353,25 +336,25 @@ impl WindowsTrayState {
let presentation =
windows_tray_presentation(status, self.busy.load(Ordering::Acquire), &self.locale());
if let Err(error) = self.status_item.set_text(presentation.status_text.clone()) {
eprintln!("更新 Windows 托盘内核状态失败: {error}");
eprintln!("Failed to update Windows tray core status: {error}");
}
if let Err(error) = self
.toggle_core_item
.set_text(presentation.toggle_text.clone())
{
eprintln!("更新 Windows 托盘操作文本失败: {error}");
eprintln!("Failed to update Windows tray action text: {error}");
}
if let Err(error) = self
.toggle_core_item
.set_enabled(presentation.toggle_enabled)
{
eprintln!("更新 Windows 托盘操作状态失败: {error}");
eprintln!("Failed to update Windows tray action state: {error}");
}
if let Err(error) = self
.restart_core_item
.set_enabled(presentation.restart_enabled)
{
eprintln!("更新 Windows 托盘重启状态失败: {error}");
eprintln!("Failed to update Windows tray restart state: {error}");
}
presentation.tooltip
}
@@ -381,14 +364,9 @@ impl WindowsTrayState {
if error.chars().count() > 48 {
summary.push('…');
}
let text = match normalize_app_locale(&self.locale()) {
"en" => format!("Operation failed: {summary}"),
"zh-TW" => format!("操作失敗:{summary}"),
"ja" => format!("操作に失敗しました:{summary}"),
_ => format!("操作失败:{summary}"),
};
let text = native_operation_failed(&self.locale(), &summary);
if let Err(update_error) = self.status_item.set_text(text) {
eprintln!("更新 Windows 托盘错误状态失败: {update_error}");
eprintln!("Failed to update Windows tray error state: {update_error}");
}
}
}
@@ -399,16 +377,16 @@ pub(crate) fn show_windows_main_window(app_handle: &tauri::AppHandle) {
return;
};
if let Err(error) = window.show() {
eprintln!("显示主窗口失败: {error}");
eprintln!("Failed to show the main window: {error}");
return;
}
if window.is_minimized().unwrap_or(false) {
if let Err(error) = window.unminimize() {
eprintln!("恢复主窗口失败: {error}");
eprintln!("Failed to restore the main window: {error}");
}
}
if let Err(error) = window.set_focus() {
eprintln!("聚焦主窗口失败: {error}");
eprintln!("Failed to focus the main window: {error}");
}
}
@@ -417,11 +395,11 @@ pub(crate) fn update_windows_tray_status(app_handle: &tauri::AppHandle, status:
let tooltip = app_handle
.try_state::<WindowsTrayState>()
.map(|tray_state| tray_state.update(status))
.unwrap_or_else(|| windows_tray_presentation(status, false, "zh-CN").tooltip);
.unwrap_or_else(|| windows_tray_presentation(status, false, "en").tooltip);
if let Some(tray) = app_handle.tray_by_id(WINDOWS_TRAY_ID) {
if let Err(error) = tray.set_tooltip(Some(&tooltip)) {
eprintln!("更新 Windows 托盘提示失败: {error}");
eprintln!("Failed to update Windows tray tooltip: {error}");
}
}
}
@@ -440,21 +418,20 @@ pub(crate) fn update_windows_tray_locale(
#[cfg(target_os = "windows")]
pub(crate) fn show_windows_tray_action_error(app_handle: &tauri::AppHandle, error: &str) {
eprintln!("Windows 托盘内核操作失败: {error}");
eprintln!("Windows tray core operation failed: {error}");
let locale = app_handle
.try_state::<WindowsTrayState>()
.map(|state| state.locale())
.unwrap_or_else(|| "zh-CN".to_string());
.unwrap_or_else(|| "en".to_string());
if let Some(tray_state) = app_handle.try_state::<WindowsTrayState>() {
tray_state.show_error(error);
}
if let Some(tray) = app_handle.tray_by_id(WINDOWS_TRAY_ID) {
if let Err(update_error) = tray.set_tooltip(Some(locale_text(
if let Err(update_error) = tray.set_tooltip(Some(native_text(
&locale,
"EasyCLIProxyAPI · 内核操作失败",
"EasyCLIProxyAPI · Core operation failed",
NativeText::TooltipCoreOperationFailed,
))) {
eprintln!("更新 Windows 托盘错误提示失败: {update_error}");
eprintln!("Failed to update Windows tray error tooltip: {update_error}");
}
}
}
@@ -471,12 +448,11 @@ pub(crate) fn run_windows_tray_core_action(
return;
}
if let Some(tray) = app_handle.tray_by_id(WINDOWS_TRAY_ID) {
if let Err(error) = tray.set_tooltip(Some(locale_text(
if let Err(error) = tray.set_tooltip(Some(native_text(
&tray_state.locale(),
"EasyCLIProxyAPI · 内核处理中",
"EasyCLIProxyAPI · Core working",
NativeText::TooltipCoreWorking,
))) {
eprintln!("更新 Windows 托盘处理中提示失败: {error}");
eprintln!("Failed to update Windows tray busy tooltip: {error}");
}
}
@@ -535,39 +511,39 @@ pub(crate) fn setup_windows_tray(app: &mut tauri::App<tauri::Wry>) -> tauri::Res
.state::<GuiConfigState>()
.snapshot()
.map(|config| config.locale)
.unwrap_or_else(|_| "zh-CN".to_string());
.unwrap_or_else(|_| "en".to_string());
let open_main_window = MenuItem::with_id(
app,
WINDOWS_TRAY_OPEN_MENU_ID,
locale_text(&locale, "打开主界面", "Open Main Window"),
native_text(&locale, NativeText::OpenMainWindow),
true,
None::<&str>,
)?;
let status_item = MenuItem::with_id(
app,
WINDOWS_TRAY_STATUS_MENU_ID,
locale_text(&locale, "内核状态:正在检查", "Core status: Checking"),
native_text(&locale, NativeText::CoreStatusChecking),
false,
None::<&str>,
)?;
let toggle_core_item = MenuItem::with_id(
app,
WINDOWS_TRAY_TOGGLE_CORE_MENU_ID,
locale_text(&locale, "启动内核", "Start Core"),
native_text(&locale, NativeText::StartCore),
false,
None::<&str>,
)?;
let restart_core_item = MenuItem::with_id(
app,
WINDOWS_TRAY_RESTART_CORE_MENU_ID,
locale_text(&locale, "重启内核", "Restart Core"),
native_text(&locale, NativeText::RestartCore),
false,
None::<&str>,
)?;
let quit = MenuItem::with_id(
app,
WINDOWS_TRAY_QUIT_MENU_ID,
locale_text(&locale, "退出", "Quit"),
native_text(&locale, NativeText::Quit),
true,
None::<&str>,
)?;
+198 -198
View File
File diff suppressed because it is too large. Load diff
+33 -33
View File
@@ -54,7 +54,7 @@ impl UsageSubscription {
.await?;
let acknowledgement = subscription.read_frame().await?;
if !is_subscription_ack(&acknowledgement) {
return Err("CPA 未确认 usage 订阅".to_string());
return Err("CPA did not confirm the usage subscription".to_string());
}
Ok(subscription)
}
@@ -63,8 +63,8 @@ impl UsageSubscription {
let address = format!("127.0.0.1:{port}");
let stream = timeout(CONNECT_TIMEOUT, TcpStream::connect(&address))
.await
.map_err(|_| format!("连接 CPA usage 订阅超时: {address}"))?
.map_err(|error| format!("连接 CPA usage 订阅失败 {address}: {error}"))?;
.map_err(|_| format!("Timed out connecting to CPA usage subscription: {address}"))?
.map_err(|error| format!("Failed to connect to CPA usage subscription {address}: {error}"))?;
let mut subscription = Self {
stream,
read_buffer: Vec::new(),
@@ -72,8 +72,8 @@ impl UsageSubscription {
subscription.send_command(&["AUTH", management_key]).await?;
match subscription.read_frame().await? {
RespValue::Simple(value) if value.eq_ignore_ascii_case("OK") => {}
RespValue::Error(error) => return Err(format!("CPA usage 订阅认证失败: {error}")),
value => return Err(format!("CPA usage 订阅认证响应无效: {}", value.kind())),
RespValue::Error(error) => return Err(format!("CPA usage subscription authentication failed: {error}")),
value => return Err(format!("Invalid CPA usage subscription authentication response: {}", value.kind())),
}
Ok(subscription)
}
@@ -85,7 +85,7 @@ impl UsageSubscription {
return Ok(payload);
}
if let RespValue::Error(error) = frame {
return Err(format!("CPA usage 订阅返回错误: {error}"));
return Err(format!("CPA usage subscription returned an error: {error}"));
}
}
}
@@ -99,8 +99,8 @@ impl UsageSubscription {
}
timeout(IO_TIMEOUT, self.stream.write_all(&command))
.await
.map_err(|_| "写入 CPA usage 订阅命令超时".to_string())?
.map_err(|error| format!("写入 CPA usage 订阅命令失败: {error}"))
.map_err(|_| "Timed out writing CPA usage subscription command".to_string())?
.map_err(|error| format!("Failed to write CPA usage subscription command: {error}"))
}
async fn read_frame(&mut self) -> Result<RespValue, String> {
@@ -119,7 +119,7 @@ impl UsageSubscription {
)? {
ParseResult::Complete(value, consumed) => {
if consumed > limits.max_frame_bytes {
return Err("CPA usage RESP 响应超过大小限制".to_string());
return Err("CPA usage RESP response exceeds the size limit".to_string());
}
self.read_buffer.drain(..consumed);
return Ok(value);
@@ -127,15 +127,15 @@ impl UsageSubscription {
ParseResult::Incomplete => {}
}
if self.read_buffer.len() >= limits.max_frame_bytes {
return Err("CPA usage RESP 响应超过大小限制".to_string());
return Err("CPA usage RESP response exceeds the size limit".to_string());
}
let mut chunk = [0_u8; 8192];
let read = timeout(IO_TIMEOUT, self.stream.read(&mut chunk))
.await
.map_err(|_| "读取 CPA usage 订阅响应超时".to_string())?
.map_err(|error| format!("读取 CPA usage 订阅响应失败: {error}"))?;
.map_err(|_| "Timed out reading CPA usage subscription response".to_string())?
.map_err(|error| format!("Failed to read CPA usage subscription response: {error}"))?;
if read == 0 {
return Err("CPA usage 订阅连接已关闭".to_string());
return Err("CPA usage subscription connection was closed".to_string());
}
self.read_buffer.extend_from_slice(&chunk[..read]);
}
@@ -150,7 +150,7 @@ pub(super) async fn pull_usage_queue(
) -> Result<Vec<String>, String> {
if count == 0 || count > QUEUE_MAX_ARRAY_LENGTH {
return Err(format!(
"CPA usage 队列批量大小必须在 1..={QUEUE_MAX_ARRAY_LENGTH} 之间"
"CPA usage queue batch size must be between 1..={QUEUE_MAX_ARRAY_LENGTH}"
));
}
let mut connection = UsageSubscription::connect_authenticated(port, management_key).await?;
@@ -167,15 +167,15 @@ pub(super) async fn pull_usage_queue(
.map(|value| {
value
.text()
.ok_or_else(|| "CPA usage 队列包含非文本消息".to_string())
.ok_or_else(|| "CPA usage queue contains a non-text message".to_string())
})
.collect(),
RespValue::Bulk(Some(value)) => String::from_utf8(value)
.map(|value| vec![value])
.map_err(|_| "CPA usage 队列消息不是 UTF-8".to_string()),
.map_err(|_| "CPA usage queue message is not UTF-8".to_string()),
RespValue::Array(None) | RespValue::Bulk(None) => Ok(Vec::new()),
RespValue::Error(error) => Err(format!("CPA usage 队列 LPOP 失败: {error}")),
value => Err(format!("CPA usage 队列 LPOP 响应无效: {}", value.kind())),
RespValue::Error(error) => Err(format!("CPA usage queue LPOP failed: {error}")),
value => Err(format!("Invalid CPA usage queue LPOP response: {}", value.kind())),
}
}
@@ -222,7 +222,7 @@ fn parse_resp_frame_with_limits(
remaining_bulk_bytes: &mut usize,
) -> Result<ParseResult, String> {
if depth > MAX_NESTING_DEPTH {
return Err("CPA usage RESP 响应嵌套过深".to_string());
return Err("CPA usage RESP response nesting is too deep".to_string());
}
let Some(prefix) = input.get(offset).copied() else {
return Ok(ParseResult::Incomplete);
@@ -233,13 +233,13 @@ fn parse_resp_frame_with_limits(
return Ok(ParseResult::Incomplete);
};
let text = String::from_utf8(line.to_vec())
.map_err(|_| "CPA usage RESP 文本响应不是 UTF-8".to_string())?;
.map_err(|_| "CPA usage RESP text response is not UTF-8".to_string())?;
let value = match prefix {
b'+' => RespValue::Simple(text),
b'-' => RespValue::Error(text),
_ => RespValue::Integer(
text.parse::<i64>()
.map_err(|_| "CPA usage RESP 整数响应无效".to_string())?,
.map_err(|_| "Invalid CPA usage RESP integer response".to_string())?,
),
};
Ok(ParseResult::Complete(value, next - offset))
@@ -256,12 +256,12 @@ fn parse_resp_frame_with_limits(
));
}
let length =
usize::try_from(length).map_err(|_| "CPA usage RESP 字符串长度无效".to_string())?;
usize::try_from(length).map_err(|_| "Invalid CPA usage RESP string length".to_string())?;
if length > MAX_BULK_BYTES {
return Err("CPA usage RESP 字符串超过大小限制".to_string());
return Err("CPA usage RESP string exceeds the size limit".to_string());
}
if length > *remaining_bulk_bytes {
return Err("CPA usage RESP 响应超过总字符串大小限制".to_string());
return Err("CPA usage RESP response exceeds the total string size limit".to_string());
}
let data_end = data_start.saturating_add(length);
let frame_end = data_end.saturating_add(2);
@@ -269,7 +269,7 @@ fn parse_resp_frame_with_limits(
return Ok(ParseResult::Incomplete);
}
if input.get(data_end..frame_end) != Some(b"\r\n") {
return Err("CPA usage RESP 字符串结尾无效".to_string());
return Err("Invalid CPA usage RESP string terminator".to_string());
}
*remaining_bulk_bytes -= length;
Ok(ParseResult::Complete(
@@ -286,9 +286,9 @@ fn parse_resp_frame_with_limits(
return Ok(ParseResult::Complete(RespValue::Array(None), next - offset));
}
let length =
usize::try_from(length).map_err(|_| "CPA usage RESP 数组长度无效".to_string())?;
usize::try_from(length).map_err(|_| "Invalid CPA usage RESP array length".to_string())?;
if length > limits.max_array_length {
return Err("CPA usage RESP 数组超过大小限制".to_string());
return Err("CPA usage RESP array exceeds the size limit".to_string());
}
let mut values = Vec::with_capacity(length);
for _ in 0..length {
@@ -311,7 +311,7 @@ fn parse_resp_frame_with_limits(
next - offset,
))
}
_ => Err("CPA usage RESP 响应类型无效".to_string()),
_ => Err("Invalid CPA usage RESP response type".to_string()),
}
}
@@ -321,12 +321,12 @@ fn resp_line(input: &[u8], start: usize) -> Result<Option<(&[u8], usize)>, Strin
};
let Some(relative_end) = remaining.windows(2).position(|pair| pair == b"\r\n") else {
if remaining.len() > MAX_RESP_LINE_BYTES + 1 {
return Err("CPA usage RESP 行超过大小限制".to_string());
return Err("CPA usage RESP line exceeds the size limit".to_string());
}
return Ok(None);
};
if relative_end > MAX_RESP_LINE_BYTES {
return Err("CPA usage RESP 行超过大小限制".to_string());
return Err("CPA usage RESP line exceeds the size limit".to_string());
}
let end = start + relative_end;
Ok(Some((&input[start..end], end + 2)))
@@ -334,9 +334,9 @@ fn resp_line(input: &[u8], start: usize) -> Result<Option<(&[u8], usize)>, Strin
fn parse_resp_length(value: &[u8]) -> Result<i64, String> {
std::str::from_utf8(value)
.map_err(|_| "CPA usage RESP 长度不是 UTF-8".to_string())?
.map_err(|_| "CPA usage RESP length is not UTF-8".to_string())?
.parse::<i64>()
.map_err(|_| "CPA usage RESP 长度无效".to_string())
.map_err(|_| "Invalid CPA usage RESP length".to_string())
}
fn is_subscription_ack(value: &RespValue) -> bool {
@@ -404,7 +404,7 @@ mod tests {
Err(error) => error,
_ => panic!("expected oversized RESP array to fail"),
};
assert!(error.contains("数组超过大小限制"));
assert!(error.contains("RESP array exceeds the size limit"));
}
#[tokio::test]
+3 -3
View File
@@ -193,7 +193,7 @@ function AppContent() {
return;
}
} catch (error) {
console.error('读取关闭行为设置失败', error);
console.error('Failed to read close behavior settings', error);
}
setWindowsClosePrompt((current) =>
@@ -216,7 +216,7 @@ function AppContent() {
}
})
.catch((error) => {
console.error('监听 Windows 关闭确认事件失败', error);
console.error('Failed to listen for Windows close confirmation events', error);
});
return () => {
@@ -252,7 +252,7 @@ function AppContent() {
try {
await invoke('open_external_url', { url: CONTACT_URL });
} catch (error) {
console.error('打开联系我们链接失败', error);
console.error('Failed to open the contact link', error);
}
};
+1 -1
View File
@@ -12,7 +12,7 @@ export class AppErrorBoundary extends Component<Props, State> {
}
componentDidCatch(error: Error, info: ErrorInfo) {
console.error('EasyCLIProxyAPI 渲染异常', error, info.componentStack);
console.error('EasyCLIProxyAPI render failure', error, info.componentStack);
}
render() {
+1 -1
View File
@@ -99,7 +99,7 @@ export function CoreRuntimeProvider({ children }: { children: ReactNode }) {
export function useCoreRuntime() {
const context = useContext(CoreRuntimeContext);
if (!context) {
throw new Error('useCoreRuntime 必须在 CoreRuntimeProvider 内使用');
throw new Error('useCoreRuntime must be used inside CoreRuntimeProvider');
}
return context;
}
+1 -1
View File
@@ -194,7 +194,7 @@ export function CoreUpdateProvider({ children }: { children: ReactNode }) {
export function useCoreUpdate() {
const context = useContext(CoreUpdateContext);
if (!context) {
throw new Error('useCoreUpdate 必须在 CoreUpdateProvider 内使用');
throw new Error('useCoreUpdate must be used inside CoreUpdateProvider');
}
return context;
}
+5 -5
View File
@@ -16,7 +16,7 @@ export const languageOptions: ReadonlyArray<{
const STORAGE_KEY = 'easy-cli-proxy-api.locale';
const resources = { 'zh-CN': zhCN, 'zh-TW': zhTW, ja, en } as const;
let currentLocale: AppLocale = 'zh-CN';
let currentLocale: AppLocale = 'en';
export const supportedLocales: readonly AppLocale[] = ['zh-CN', 'zh-TW', 'ja', 'en'];
@@ -26,7 +26,6 @@ export function getCurrentLocale(): AppLocale {
export function normalizeLocale(value: string | null | undefined): AppLocale {
const normalized = value?.trim().toLowerCase() ?? '';
if (normalized.startsWith('en')) return 'en';
if (normalized.startsWith('ja')) return 'ja';
if (
normalized === 'zh-tw'
@@ -34,11 +33,12 @@ export function normalizeLocale(value: string | null | undefined): AppLocale {
|| normalized === 'zh-mo'
|| normalized.startsWith('zh-hant')
) return 'zh-TW';
return 'zh-CN';
if (normalized.startsWith('zh')) return 'zh-CN';
return 'en';
}
function detectInitialLocale(): AppLocale {
if (typeof window === 'undefined') return 'zh-CN';
if (typeof window === 'undefined') return 'en';
try {
const saved = window.localStorage.getItem(STORAGE_KEY);
if (saved) return normalizeLocale(saved);
@@ -55,7 +55,7 @@ function interpolate(template: string, variables?: MessageVariables): string {
}
export function translate(locale: AppLocale, key: MessageKey, variables?: MessageVariables): string {
return interpolate(resources[locale][key] ?? zhCN[key], variables);
return interpolate(resources[locale][key] ?? en[key], variables);
}
type I18nContextValue = {
+1
View File
@@ -1461,6 +1461,7 @@ export const jaOverrides = {
'kernel.error.port': 'ポートは 1~65535 の範囲で指定してください',
'kernel.error.installTask': 'インストールタスクを読み込めませんでした:{error}',
'kernel.install.installingVersion': '{version} をインストールしています',
'kernel.install.installingLatest': '最新バージョンをインストールしています',
'kernel.phase.preparingDownload': 'ダウンロードを準備中',
'kernel.phase.downloading': 'ダウンロード中',
'kernel.phase.extracting': '展開中',
+1
View File
@@ -1461,6 +1461,7 @@ export const en: Record<MessageKey, string> = {
'kernel.error.port': 'The port must be between 1 and 65535',
'kernel.error.installTask': 'Failed to read the installation task: {error}',
'kernel.install.installingVersion': 'Installing {version}',
'kernel.install.installingLatest': 'Installing the latest version',
'kernel.phase.preparingDownload': 'Preparing Download',
'kernel.phase.downloading': 'Downloading',
'kernel.phase.extracting': 'Extracting',
+1
View File
@@ -1460,6 +1460,7 @@ export const zhCN = {
'kernel.error.port': '端口必须在 1 到 65535 之间',
'kernel.error.installTask': '读取安装任务失败: {error}',
'kernel.install.installingVersion': '正在安装 {version}',
'kernel.install.installingLatest': '正在安装最新版本',
'kernel.phase.preparingDownload': '准备下载',
'kernel.phase.downloading': '下载中',
'kernel.phase.extracting': '解压中',
+10 -10
View File
@@ -35,18 +35,18 @@ function installToolbar(scenario: BrowserMockScenario, delayMs: number) {
const toolbar = document.createElement('aside');
toolbar.id = 'browser-mock-toolbar';
toolbar.setAttribute('aria-label', 'Browser Mock controls');
toolbar.title = '仅在普通浏览器开发模式中显示;Tauri 和正式构建不会启用 Mock。';
toolbar.title = 'Shown only in regular browser development mode; Mock is disabled in Tauri and production builds.';
const badge = document.createElement('strong');
badge.textContent = 'MOCK';
const select = document.createElement('select');
select.setAttribute('aria-label', 'Browser Mock 场景');
select.setAttribute('aria-label', 'Browser Mock scenario');
const scenarios: Array<[BrowserMockScenario, string]> = [
['running', '内核运行'],
['stopped', '内核停止'],
['empty', '未安装内核'],
['error', '请求错误'],
['running', 'Core running'],
['stopped', 'Core stopped'],
['empty', 'Core not installed'],
['error', 'Request error'],
];
scenarios.forEach(([value, label]) => {
const option = document.createElement('option');
@@ -64,7 +64,7 @@ function installToolbar(scenario: BrowserMockScenario, delayMs: number) {
});
const hint = document.createElement('span');
hint.textContent = delayMs > 0 ? `${delayMs} ms` : '浏览器调试';
hint.textContent = delayMs > 0 ? `${delayMs} ms` : 'Browser debugging';
toolbar.append(badge, select, hint);
document.body.append(toolbar);
@@ -74,7 +74,7 @@ function installToolbar(scenario: BrowserMockScenario, delayMs: number) {
export function installBrowserMock(): InstalledBrowserMock | null {
const options = resolveBrowserMockOptions(window.location.search, readStoredScenario());
if (options.mode === 'off') {
console.info('[Browser Mock] 已通过 ?mock=off 禁用。');
console.info('[Browser Mock] Disabled via ?mock=off.');
return null;
}
@@ -98,8 +98,8 @@ export function installBrowserMock(): InstalledBrowserMock | null {
installToolbar(options.mode, options.delayMs);
console.info(
`[Browser Mock] 已启用场景“${options.mode}”${options.delayMs ? `,延迟 ${options.delayMs} ms` : ''}。`,
'使用 ?mock=running|stopped|empty|error 切换,?mock=off 禁用。',
`[Browser Mock] Enabled scenario “${options.mode}”${options.delayMs ? ` with a ${options.delayMs} ms delay` : ''}.`,
'Use ?mock=running|stopped|empty|error to switch scenarios, or ?mock=off to disable.',
);
return { scenario: options.mode, delayMs: options.delayMs };
}
+29 -29
View File
@@ -83,7 +83,7 @@ function createCoreStatus(scenario: BrowserMockScenario) {
currentVersion: installed ? '7.3.15' : null,
installDir: 'C:\\EasyCLIProxyAPI\\cpa-core',
binaryPath: installed ? 'C:\\EasyCLIProxyAPI\\cpa-core\\cli-proxy-api.exe' : null,
message: running ? 'Browser Mock 内核运行中' : installed ? 'Browser Mock 内核已停止' : 'Browser Mock 内核未安装',
message: running ? 'Browser Mock core is running' : installed ? 'Browser Mock core is stopped' : 'Browser Mock core is not installed',
};
}
@@ -138,7 +138,7 @@ function createUsageEvents() {
endpoint: '/v1/responses',
api_key_hash: 'mock-key-hash',
api_key_display: 'sk-mock••••••••2026',
api_key_remark: '浏览器 Mock 密钥',
api_key_remark: 'Browser Mock key',
tokens: {
input_tokens: input,
output_tokens: output,
@@ -186,10 +186,10 @@ function createAgentStatuses() {
pluginInstalled: id === 'pi',
launchTargets: id === 'codex' || id === 'opencode'
? [
{ id: 'app', label: 'Desktop App', detail: 'Browser Mock 桌面入口' },
{ id: 'cli', label: 'CLI', detail: 'Browser Mock 命令行入口' },
{ id: 'app', label: 'Desktop App', detail: 'Browser Mock desktop entry point' },
{ id: 'cli', label: 'CLI', detail: 'Browser Mock command-line entry point' },
]
: [{ id: id === 'claude-desktop' ? 'app' : 'cli', label: '启动', detail: 'Browser Mock 启动目标' }],
: [{ id: id === 'claude-desktop' ? 'app' : 'cli', label: 'Launch', detail: 'Browser Mock launch target' }],
version: `1.${index + 4}.0`,
cliVersion: id === 'claude-desktop' ? null : `1.${index + 4}.0`,
appVersion: id === 'claude-desktop' ? '0.12.8' : null,
@@ -217,7 +217,7 @@ function createAgentStatuses() {
],
}
: null,
warnings: id === 'openclaw' ? ['Mock:尚未应用配置'] : [],
warnings: id === 'openclaw' ? ['Mock: configuration has not been applied'] : [],
error: null,
}));
}
@@ -345,7 +345,7 @@ function createState(scenario: BrowserMockScenario) {
{ key: 'other', label: 'Other', requests: 34, failures: 1, tokens: 333_300 },
],
apiKeys: [
{ key: 'mock-key-hash', label: '浏览器 Mock 密钥', requests: 312, failures: 7, tokens: 2_187_300 },
{ key: 'mock-key-hash', label: 'Browser Mock key', requests: 312, failures: 7, tokens: 2_187_300 },
],
};
return {
@@ -359,8 +359,8 @@ function createState(scenario: BrowserMockScenario) {
},
coreConfig: {
apiKeys: [
{ apiKey: 'sk-browser-mock-2026', remark: '浏览器 Mock 密钥' },
{ apiKey: 'sk-browser-demo-secondary', remark: '备用演示密钥' },
{ apiKey: 'sk-browser-mock-2026', remark: 'Browser Mock key' },
{ apiKey: 'sk-browser-demo-secondary', remark: 'Secondary demo key' },
],
debug: false,
commercialMode: false,
@@ -390,7 +390,7 @@ function createState(scenario: BrowserMockScenario) {
silentStartEnabled: false,
defaultTerminal: 'auto',
availableTerminals: [
{ id: 'auto', label: '自动选择' },
{ id: 'auto', label: 'Automatic selection' },
{ id: 'windows-terminal', label: 'Windows Terminal' },
{ id: 'powershell', label: 'PowerShell' },
],
@@ -512,7 +512,7 @@ function createState(scenario: BrowserMockScenario) {
models: ['gpt-5.2-codex', 'gpt-5.1-codex'].map((slug) => {
const configuration = {
display_name: slug === 'gpt-5.2-codex' ? 'GPT-5.2 Codex' : 'GPT-5.1 Codex',
description: 'Browser Mock 模型目录',
description: 'Browser Mock model catalog',
context_window: 272_000,
max_context_window: 400_000,
effective_context_window_percent: 90,
@@ -545,9 +545,9 @@ function createState(scenario: BrowserMockScenario) {
configured: true,
},
codexSessions: [
{ id: 'mock-session-1', title: '实现 Browser Mock', cwd: 'E:\\projects\\mock-demo', modelProvider: 'cpa-gui', archived: false, updatedAtMs: Date.now() - 20 * 60_000, databasePath: 'C:\\Users\\Mock\\.codex\\state.sqlite' },
{ id: 'mock-session-2', title: 'TypeScript 类型学习', cwd: 'E:\\projects\\typescript-study', modelProvider: 'openai', archived: false, updatedAtMs: Date.now() - 3 * 60 * 60_000, databasePath: 'C:\\Users\\Mock\\.codex\\state.sqlite' },
{ id: 'mock-session-3', title: '旧版界面检查', cwd: 'E:\\projects\\legacy-ui', modelProvider: 'cpa-gui', archived: true, updatedAtMs: Date.now() - 3 * 24 * 60 * 60_000, databasePath: 'C:\\Users\\Mock\\.codex\\state.sqlite' },
{ id: 'mock-session-1', title: 'Implement Browser Mock', cwd: 'E:\\projects\\mock-demo', modelProvider: 'cpa-gui', archived: false, updatedAtMs: Date.now() - 20 * 60_000, databasePath: 'C:\\Users\\Mock\\.codex\\state.sqlite' },
{ id: 'mock-session-2', title: 'Learn TypeScript types', cwd: 'E:\\projects\\typescript-study', modelProvider: 'openai', archived: false, updatedAtMs: Date.now() - 3 * 60 * 60_000, databasePath: 'C:\\Users\\Mock\\.codex\\state.sqlite' },
{ id: 'mock-session-3', title: 'Review legacy UI', cwd: 'E:\\projects\\legacy-ui', modelProvider: 'cpa-gui', archived: true, updatedAtMs: Date.now() - 3 * 24 * 60 * 60_000, databasePath: 'C:\\Users\\Mock\\.codex\\state.sqlite' },
],
};
}
@@ -769,21 +769,21 @@ export function createBrowserMockRuntime(
case 'get_core_status': return clone(state.coreStatus);
case 'start_core_process': {
if (!state.coreStatus.installed) throw new Error('Browser Mock:请先安装内核');
Object.assign(state.coreStatus, { running: true, ready: true, managed: true, starting: false, processId: 42817, message: 'Browser Mock 内核运行中' });
if (!state.coreStatus.installed) throw new Error('Browser Mock: install the core first');
Object.assign(state.coreStatus, { running: true, ready: true, managed: true, starting: false, processId: 42817, message: 'Browser Mock core is running' });
state.guiSettings.runOnStartup = true;
emit('core-status-changed', state.coreStatus);
return clone(state.coreStatus);
}
case 'stop_core_process': {
Object.assign(state.coreStatus, { running: false, ready: false, managed: false, starting: false, processId: null, message: 'Browser Mock 内核已停止' });
Object.assign(state.coreStatus, { running: false, ready: false, managed: false, starting: false, processId: null, message: 'Browser Mock core is stopped' });
state.guiSettings.runOnStartup = false;
emit('core-status-changed', state.coreStatus);
return clone(state.coreStatus);
}
case 'restart_core_process': {
if (!state.coreStatus.installed) throw new Error('Browser Mock:请先安装内核');
Object.assign(state.coreStatus, { running: true, ready: true, managed: true, starting: false, processId: 42818, message: 'Browser Mock 内核已重启' });
if (!state.coreStatus.installed) throw new Error('Browser Mock: install the core first');
Object.assign(state.coreStatus, { running: true, ready: true, managed: true, starting: false, processId: 42818, message: 'Browser Mock core restarted' });
emit('core-status-changed', state.coreStatus);
return clone(state.coreStatus);
}
@@ -853,13 +853,13 @@ export function createBrowserMockRuntime(
downloadedBytes: 18 * 1024 * 1024,
totalBytes: state.appUpdateInfo.downloadSizeBytes,
percent: 38,
message: 'Browser Mock 正在下载更新',
message: 'Browser Mock is downloading an update',
});
emit('app-update-progress', state.appUpdateTask);
return null;
}
case 'cancel_app_update': {
Object.assign(state.appUpdateTask, { running: false, cancellable: false, phase: 'cancelled', message: 'Browser Mock 已取消更新' });
Object.assign(state.appUpdateTask, { running: false, cancellable: false, phase: 'cancelled', message: 'Browser Mock update canceled' });
emit('app-update-progress', state.appUpdateTask);
return null;
}
@@ -873,8 +873,8 @@ export function createBrowserMockRuntime(
installDir: state.coreStatus.installDir,
binaryPath: 'C:\\EasyCLIProxyAPI\\cpa-core\\cli-proxy-api.exe',
};
Object.assign(state.coreStatus, { installed: true, currentVersion: result.version, binaryPath: result.binaryPath, message: 'Browser Mock 内核已安装' });
Object.assign(state.coreInstallTask, { running: false, cancellable: false, phase: 'completed', downloaded: 64 * 1024 * 1024, total: 64 * 1024 * 1024, percent: 100, message: 'Browser Mock 安装完成', result });
Object.assign(state.coreStatus, { installed: true, currentVersion: result.version, binaryPath: result.binaryPath, message: 'Browser Mock core installed' });
Object.assign(state.coreInstallTask, { running: false, cancellable: false, phase: 'completed', downloaded: 64 * 1024 * 1024, total: 64 * 1024 * 1024, percent: 100, message: 'Browser Mock installation complete', result });
emit('core-install-progress', state.coreInstallTask);
emit('core-status-changed', state.coreStatus);
return clone(result);
@@ -915,7 +915,7 @@ export function createBrowserMockRuntime(
case 'provider_health_probe': return { firstTokenLatencyMs: 184, responseLatencyMs: 642 };
case 'list_oauth_browsers': return [
{ id: 'default', label: '系统默认浏览器' },
{ id: 'default', label: 'System default browser' },
{ id: 'chrome', label: 'Google Chrome (Mock)' },
{ id: 'edge', label: 'Microsoft Edge (Mock)' },
];
@@ -940,7 +940,7 @@ export function createBrowserMockRuntime(
case 'get_usage_collector_status': return {
state: state.coreStatus.ready ? 'collecting' : 'waiting-core',
message: state.coreStatus.ready ? 'Browser Mock 正在采集使用记录' : '等待内核启动',
message: state.coreStatus.ready ? 'Browser Mock is collecting usage records' : 'Waiting for the core to start',
lastCollectedAt: new Date().toISOString(),
totalRecords: state.usageOverview.totalRequests,
};
@@ -1152,7 +1152,7 @@ export function createBrowserMockRuntime(
const ids = asArray(request.sessionIds).map(String);
state.codexSessions = state.codexSessions.filter((session) => !ids.includes(session.id));
return {
results: ids.map((id) => ({ sessionId: id, status: 'deleted', message: 'Browser Mock 已删除', backupPath: 'C:\\Users\\Mock\\.codex\\backups\\mock.zip' })),
results: ids.map((id) => ({ sessionId: id, status: 'deleted', message: 'Deleted by Browser Mock', backupPath: 'C:\\Users\\Mock\\.codex\\backups\\mock.zip' })),
deletedCount: ids.length,
failedCount: 0,
};
@@ -1167,15 +1167,15 @@ export function createBrowserMockRuntime(
}
case 'preview_codex_session_index_cleanup': return {
snapshotSha256: 'mock-snapshot-sha256',
candidates: [{ id: 'stale-thread-1', threadName: '已不存在的会话', updatedAt: isoHoursAgo(72) }],
candidates: [{ id: 'stale-thread-1', threadName: 'Missing session', updatedAt: isoHoursAgo(72) }],
};
case 'apply_codex_session_index_cleanup': return { prunedEntries: asArray(asObject(payload.request).threadIds).length, backupPath: 'C:\\Users\\Mock\\.codex\\backups\\index.json' };
case 'get_lan_ipv4': return ['192.168.100.100'];
default:
if (command.startsWith('plugin:window|') || command.startsWith('plugin:webview|')) return null;
console.warn(`[Browser Mock] 尚未实现命令:${command}`, rawPayload);
throw new Error(`Browser Mock 尚未实现命令:${command}`);
console.warn(`[Browser Mock] Command not implemented: ${command}`, rawPayload);
throw new Error(`Browser Mock command not implemented: ${command}`);
}
};
+23 -17
View File
@@ -163,7 +163,7 @@ export function VersionManagementPage() {
}
if (task.message && !task.running) {
showNotice(task.message, task.phase === '安装失败' ? 'error' : 'info');
showNotice(task.message, task.phase === 'Installation failed' ? 'error' : 'info');
}
};
@@ -252,7 +252,7 @@ export function VersionManagementPage() {
setProgress({
running: true,
cancellable: true,
phase: '准备下载',
phase: 'Preparing download',
downloaded: 0,
total: null,
percent: null,
@@ -267,7 +267,7 @@ export function VersionManagementPage() {
setProgress({
running: false,
cancellable: false,
phase: '安装完成',
phase: 'Installation complete',
downloaded: 1,
total: 1,
percent: 100,
@@ -281,11 +281,12 @@ export function VersionManagementPage() {
} catch (error) {
manualInstallInProgressRef.current = false;
const errorMessage = String(error);
showNotice(errorMessage, errorMessage.includes('取消') ? 'info' : 'error');
const cancelled = /cancel/i.test(errorMessage);
showNotice(errorMessage, cancelled ? 'info' : 'error');
setProgress((current) => ({
running: false,
cancellable: false,
phase: errorMessage.includes('取消') ? '已取消' : '安装失败',
phase: cancelled ? 'Canceled' : 'Installation failed',
downloaded: current?.downloaded ?? 0,
total: current?.total ?? null,
percent: current?.percent ?? null,
@@ -444,9 +445,9 @@ export function VersionManagementPage() {
const progressKnown = computedPercent !== null;
const progressPercent = clampPercent(computedPercent ?? 0);
const progressText = progress
? progress.phase === '安装完成'
? progress.phase === 'Installation complete'
? t('kernel.progress.completed')
: progress.phase === '解压中'
: progress.phase === 'Extracting'
? t('kernel.progress.extracting')
: progress.total
? `${formatBytes(progress.downloaded)} / ${formatBytes(progress.total)}`
@@ -457,7 +458,7 @@ export function VersionManagementPage() {
const installDialogTone: MessageType = progress?.result
? 'success'
: progress?.phase === '安装失败'
: progress?.phase === 'Installation failed'
? 'error'
: 'info';
@@ -467,7 +468,7 @@ export function VersionManagementPage() {
: t('kernel.install.titleInstalling')
: progress?.result
? t('kernel.install.titleCompleted')
: progress?.phase === '已取消'
: progress?.phase === 'Canceled'
? t('kernel.install.titleCancelled')
: t('kernel.install.titleFailed');
@@ -895,16 +896,21 @@ function localizeInstallPhase(
t: ReturnType<typeof useI18n>['t'],
) {
const keys = {
'准备下载': 'kernel.phase.preparingDownload',
'下载中': 'kernel.phase.downloading',
'解压中': 'kernel.phase.extracting',
'准备内置内核': 'kernel.phase.preparingBundled',
'安装完成': 'kernel.phase.completed',
'安装失败': 'kernel.phase.failed',
'已取消': 'kernel.phase.cancelled',
'Preparing download': 'kernel.phase.preparingDownload',
Downloading: 'kernel.phase.downloading',
Extracting: 'kernel.phase.extracting',
'Extract bundled kernel': 'kernel.phase.preparingBundled',
'Installation complete': 'kernel.phase.completed',
'Installation failed': 'kernel.phase.failed',
Canceled: 'kernel.phase.cancelled',
} as const;
const key = keys[phase as keyof typeof keys];
return key ? t(key) : phase;
if (key) return t(key);
if (phase === 'Preparing to install the latest version') {
return t('kernel.install.installingLatest');
}
const version = phase.match(/^Preparing to install (.+)$/)?.[1];
return version ? t('kernel.install.installingVersion', { version }) : phase;
}
function clampPercent(percent: number) {
+4 -1
View File
@@ -154,7 +154,10 @@ const formatDateTime = (date: Date): string | undefined => {
}).format(date);
};
export const formatQuotaTimestamp = (value: string | undefined, locale: AppLocale = 'zh-CN'): string => {
export const formatQuotaTimestamp = (
value: string | undefined,
locale: AppLocale = getCurrentLocale(),
): string => {
if (!value) return '—';
const ms = quotaResetInstant(value);
if (ms === undefined) return '—';
+1 -1
View File
@@ -9089,7 +9089,7 @@ textarea:focus-visible {
transition: width 0.3s ease;
}
/* Events Tab (请求明细) */
/* Events tab (request details) */
.usage-events-panel {
display: flex;
width: 100%;
+11 -11
View File
@@ -12,7 +12,7 @@ import {
const notice: AppNotice = {
owner: 'api-page',
source: 'app.nav.api',
message: '接入已启用',
message: 'Connection enabled',
tone: 'success',
};
@@ -31,11 +31,11 @@ describe('统一操作提示', () => {
const first = appNoticeReducer(initialAppNoticeState, { type: 'show', notice });
const next = appNoticeReducer(first, {
type: 'show',
notice: { ...notice, message: '接入已停用' },
notice: { ...notice, message: 'Connection disabled' },
});
expect(next.notice?.message).toBe('接入已停用');
expect(next.notice?.message).toBe('Connection disabled');
expect(next.revision).toBe(2);
expect(first.notice?.message).toBe('接入已启用');
expect(first.notice?.message).toBe('Connection enabled');
});
it('重复的操作结果也产生新的可播报版本', () => {
@@ -76,12 +76,12 @@ describe('统一操作提示', () => {
it('操作结果包含来源、完整消息和可访问的关闭按钮', () => {
const html = renderNotice(appNoticeReducer(initialAppNoticeState, { type: 'show', notice }));
expect(html).toContain('action-feedback inline-notice success');
expect(html).toContain('API 接入');
expect(html).toContain('接入已启用');
expect(html).toContain('API Access');
expect(html).toContain('Connection enabled');
expect(html).toContain('role="status"');
expect(html).toContain('aria-live="polite"');
expect(html).toContain('aria-atomic="true"');
expect(html).toContain('aria-label="关闭操作提示"');
expect(html).toContain('aria-label="Dismiss operation feedback"');
expect(html).not.toContain('config-toast');
expect(html).not.toContain('<footer');
expect(html).not.toContain('app-notice-bar');
@@ -89,19 +89,19 @@ describe('统一操作提示', () => {
it('局部操作可省略重复的模块标题', () => {
const html = renderNotice({ notice: { ...notice, source: undefined }, revision: 1 });
expect(html).toContain('接入已启用');
expect(html).toContain('Connection enabled');
expect(html).not.toContain('action-feedback-source');
});
it('长错误保留全文、换行且转义外部内容', () => {
const message = `失败:${'详细错误'.repeat(160)}\n<script>alert(1)</script>`;
const message = `Failure: ${'Detailed error'.repeat(160)}\n<script>alert(1)</script>`;
const html = renderNotice(appNoticeReducer(initialAppNoticeState, {
type: 'show', notice: { ...notice, message, tone: 'error' },
}));
expect(html).toContain('action-feedback inline-notice error');
expect(html).toContain('role="alert"');
expect(html).toContain('aria-live="assertive"');
expect(html).toContain('详细错误'.repeat(160));
expect(html).toContain('Detailed error'.repeat(160));
expect(html).toContain('\n&lt;script&gt;');
expect(html).not.toContain('<script>');
});
@@ -112,7 +112,7 @@ describe('统一操作提示', () => {
type: 'show', notice: { ...notice, message },
});
expect(shown.notice?.message).toBe(message);
expect(renderNotice(shown)).toContain('&lt;QA&gt; 登录成功');
expect(renderNotice(shown)).toContain('&lt;QA&gt; sign-in succeeded');
for (const locale of supportedLocales) {
expect(translate(locale, message.key, message.variables)).toContain('<QA>');
}
+5 -5
View File
@@ -113,11 +113,11 @@ describe('credential health and cooldowns', () => {
});
it('does not render elapsed timers as healthy, or hide disabled state behind a cooldown', () => {
expect(render({ status: 'active', cooldowns: [record] }, receivedAtMs - 60_000)).toContain('冷却计时已到期');
expect(render({ disabled: true, status: 'error', cooldowns: [record] })).toContain('已停用');
expect(render({ status: 'error', unavailable: true })).toContain('无法确定恢复时间');
expect(render({ status: 'active', cooldowns: null })).toContain('冷却状态未知');
expect(render({ status: 'active', cooldowns: [record] }, receivedAtMs - 60_000)).toContain('Cooldown timer elapsed');
expect(render({ disabled: true, status: 'error', cooldowns: [record] })).toContain('Disabled');
expect(render({ status: 'error', unavailable: true })).toContain('recovery time is unknown');
expect(render({ status: 'active', cooldowns: null })).toContain('Cooldown state is unknown');
const expiredToken = render({ status: 'error', status_message: 'token expired', cooldowns: [record] }, receivedAtMs - 60_000);
expect(expiredToken.slice(0, expiredToken.indexOf('</summary>'))).toContain('访问令牌已过期');
expect(expiredToken.slice(0, expiredToken.indexOf('</summary>'))).toContain('Access token expired');
});
});
+2 -2
View File
@@ -53,10 +53,10 @@ it.each([
const credentialHtml = renderToStaticMarkup(<I18nProvider><AuthFileQuotaPanel file={file} quota={quota} disabled={file.disabled} onRefresh={() => {}} onReset={onReset} /></I18nProvider>);
const quotaHtml = renderToStaticMarkup(<I18nProvider><QuotaCard file={file} quota={quota} onRefresh={() => {}} onReset={onReset} /></I18nProvider>);
const buttonState = (html: string) => {
const button = html.match(/<button[^>]*title="重置额度"[^>]*>重置额度<\/button>/)?.[0];
const button = html.match(/<button[^>]*title="Reset Quota"[^>]*>Reset Quota<\/button>/)?.[0];
return { visible: Boolean(button), disabled: Boolean(button?.includes('disabled=""')) };
};
expect(buttonState(credentialHtml)).toEqual({ visible: values.visible, disabled: values.disabled });
expect(buttonState(credentialHtml)).toEqual(buttonState(quotaHtml));
if (values.loading) expect(credentialHtml).toContain('正在提交重置并刷新额度');
if (values.loading) expect(credentialHtml).toContain('Submitting reset and refreshing quota');
});
+4 -4
View File
@@ -6,13 +6,13 @@ import { I18nProvider } from '../src/i18n';
describe('application confirmation and quota feedback', () => {
it('renders an accessible in-app confirmation with explicit reset and cancel actions', () => {
const html = renderToStaticMarkup(<I18nProvider><ConfirmationDialog title="重置额度" message="确认重置测试账号?" confirmText="确认重置" warning="消耗 1 次重置机会" onDecision={() => {}} /></I18nProvider>);
const html = renderToStaticMarkup(<I18nProvider><ConfirmationDialog title="Reset Quota" message="Reset the test account quota?" confirmText="Confirm Reset" warning="This consumes one reset credit" onDecision={() => {}} /></I18nProvider>);
expect(html).toContain('role="alertdialog"');
expect(html).toContain('aria-modal="true"');
expect(html).toContain('aria-describedby=');
expect(html).toContain('确认重置');
expect(html).toContain('取消');
expect(html).toContain('消耗 1 次重置机会');
expect(html).toContain('Confirm Reset');
expect(html).toContain('Cancel');
expect(html).toContain('This consumes one reset credit');
});
it('keeps reset feedback out of the card layout (message checked in browser)', () => {
+2 -2
View File
@@ -43,8 +43,8 @@ describe('Devin live quota', () => {
{ id: 'weekly', remainingPercent: 81.25, resetAtMs: 1893542400000 },
] });
expect(quotaRowsFor('devin', liveStatus({ dailyQuotaRemainingPercent: 0 }))).toEqual([
{ label: '每日额度', remainingPercent: 0, resetAtMs: undefined },
{ label: '每周额度', remainingPercent: null, resetAtMs: undefined },
{ label: 'Daily quota', remainingPercent: 0, resetAtMs: undefined },
{ label: 'Weekly quota', remainingPercent: null, resetAtMs: undefined },
]);
});
it('does not turn malformed, missing or cached metadata into live quota', () => {
+2 -2
View File
@@ -36,14 +36,14 @@ describe('i18n', () => {
expect(translate('zh-CN', 'authFiles.models.globalDescription', { provider: 'Codex' })).toContain('不影响 API 接入');
});
it('normalizes English variants and keeps Chinese as the fallback', () => {
it('normalizes supported variants and keeps English as the fallback', () => {
expect(normalizeLocale('en-US')).toBe('en');
expect(normalizeLocale('en')).toBe('en');
expect(normalizeLocale('zh-CN')).toBe('zh-CN');
expect(normalizeLocale('zh-TW')).toBe('zh-TW');
expect(normalizeLocale('zh-Hant-HK')).toBe('zh-TW');
expect(normalizeLocale('ja-JP')).toBe('ja');
expect(normalizeLocale('unsupported')).toBe('zh-CN');
expect(normalizeLocale('unsupported')).toBe('en');
});
it('translates messages and interpolates variables', () => {
+1 -1
View File
@@ -15,6 +15,6 @@ describe('apiCallErrorMessage', () => {
});
it('没有错误体时回退到 HTTP 状态', () => {
expect(apiCallErrorMessage({ status_code: 429 })).toBe('上游返回 HTTP 429');
expect(apiCallErrorMessage({ status_code: 429 })).toBe('Upstream returned HTTP 429');
});
});
+1 -1
View File
@@ -186,6 +186,6 @@ describe('credential model exclusion metadata', () => {
expect(() => authFileExcludedRulesFromPayload(payload)).toThrow();
}
expect(() => authFileExcludedRulesFromPayload('{"access_token": "sensitive-token"'))
.toThrow('无法读取凭证文件内容,未加载模型设置');
.toThrow('Could not read the credential file; model settings were not loaded');
});
});
+1 -1
View File
@@ -604,7 +604,7 @@ describe('API 接入配置合并', () => {
expect(parseProviderHeaders('Authorization: Bearer abc:def')).toEqual({
Authorization: 'Bearer abc:def',
});
expect(() => parseProviderHeaders('Invalid header')).toThrow('缺少冒号');
expect(() => parseProviderHeaders('Invalid header')).toThrow('missing a colon');
});
it('把未勾选的上游模型写入排除列表', () => {
+15 -15
View File
@@ -15,12 +15,12 @@ describe('quota card rendering', () => {
status: 'success', rows: [], resetCredits: 2, resetCreditsApplicable: 0,
resetCreditsError: 'temporary failure', subscriptionActiveUntil: '2030-01-01T00:00:00Z',
});
expect(html).toMatch(/<button[^>]*title="重置额度"[^>]*>重置额度<\/button>/);
expect(html).not.toMatch(/<button[^>]*disabled=""[^>]*>重置额度<\/button>/);
expect(html).not.toContain('当前没有适用的重置次数');
expect(html).toContain('当前适用:0 次');
expect(html).toMatch(/<button[^>]*title="Reset Quota"[^>]*>Reset Quota<\/button>/);
expect(html).not.toMatch(/<button[^>]*disabled=""[^>]*>Reset Quota<\/button>/);
expect(html).not.toContain('No applicable reset credits');
expect(html).toContain('Currently applicable: 0');
expect(html).not.toContain('temporary failure');
expect(html).toContain('订阅到期');
expect(html).toContain('Subscription expires');
});
it.each(['error', 'refresh-error'] as const)('重置结果为 %s 时仍可再次点击重置', (status) => {
@@ -28,35 +28,35 @@ describe('quota card rendering', () => {
status: 'success', rows: [], resetCredits: 2,
actionResult: { action: 'reset', status, error: 'temporary failure' },
});
expect(html).toMatch(/<button[^>]*title="重置额度"[^>]*>重置额度<\/button>/);
expect(html).not.toMatch(/<button[^>]*disabled=""[^>]*>重置额度<\/button>/);
expect(html).toMatch(/<button[^>]*title="Reset Quota"[^>]*>Reset Quota<\/button>/);
expect(html).not.toMatch(/<button[^>]*disabled=""[^>]*>Reset Quota<\/button>/);
expect(html).not.toContain('temporary failure');
});
it('xAI 付费账号保留额度说明和刷新入口,不显示可用性测试', () => {
const html = render({ status: 'success', rows: [{ label: '付费 API 账号', remainingPercent: null, detail: '上游未提供剩余额度' }] }, 'xai');
expect(html).toContain('上游未提供剩余额度');
const html = render({ status: 'success', rows: [{ label: 'Paid API account', remainingPercent: null, detail: 'Upstream did not provide remaining quota' }] }, 'xai');
expect(html).toContain('Upstream did not provide remaining quota');
expect(html).not.toContain('real-quota-track');
expect(html).not.toContain('测试可用性');
expect(html).toContain('获取/刷新额度');
expect(html).not.toContain('Test availability');
expect(html).toContain('Fetch or refresh quota');
});
it('xAI 探测成功显示可用说明,100% 额度保留刷新按钮', () => {
const healthy = render({
status: 'success', rows: quotaRowsFor('xai', { mode: 'paid-health' }),
}, 'xai');
expect(healthy).toContain('付费 API 对话可用');
expect(healthy).toContain('Paid API chat is available');
expect(healthy).not.toContain('real-quota-track');
const full = render({
status: 'success', rows: quotaRowsFor('xai', { config: { creditUsagePercent: 0 } }),
}, 'xai');
expect(full).toContain('剩余 100%');
expect(full).toContain('100% remaining');
expect(full).toContain('width:100%');
expect(full).not.toContain('disabled=""');
});
it('缓存中的原始重置时间提供动态提示,过期额度不伪造为已恢复', () => {
const html = render({ status: 'success', rows: [{ label: '5h', remainingPercent: 0, resetAtMs: Date.parse('2020-01-01T00:00:00Z') }] });
expect(html).toContain('重置时间已到,请刷新确认');
expect(html).toContain('剩余 0%');
expect(html).toContain('Reset time reached; refresh to verify');
expect(html).toContain('0% remaining');
});
});
+3 -3
View File
@@ -71,7 +71,7 @@ describe('quota API compatibility', () => {
const result = await loadQuota(codexFile);
expect(result.status).toBe('success');
expect(result.resetCredits).toBe(2);
expect(result.resetCreditsError).toContain('无法识别');
expect(result.resetCreditsError).toContain('unrecognized data');
});
it('Claude profile 查询失败不影响现代 Fable 额度', async () => {
@@ -79,7 +79,7 @@ describe('quota API compatibility', () => {
? { status_code: 500 }
: success({ limits: [{ kind: 'weekly_scoped', percent: 40, scope: { model: { display_name: 'Fable 5' } } }] });
expect(await loadQuota({ name: 'claude.json', provider: 'claude', auth_index: 'c' })).toMatchObject({
status: 'success', rows: [{ label: '7 天 Fable 窗口', remainingPercent: 60 }],
status: 'success', rows: [{ label: '7-day Fable window', remainingPercent: 60 }],
});
});
@@ -141,7 +141,7 @@ describe('xAI quota queries aligned with Management Center', () => {
const result = await loadQuota({ ...file, using_api: true, prefix: 'paid' });
expect(result).toMatchObject({ status: 'success', plan: 'Paid' });
expect(result.rows[0].remainingPercent).toBeNull();
expect(result.rows[0].detail).toContain('付费 API 对话可用');
expect(result.rows[0].detail).toContain('Paid API chat is available');
expect(calls).toHaveLength(2);
expect(calls.map((request) => request.url)).toEqual([
'https://api.x.ai/v1/me', 'https://api.x.ai/v1/chat/completions',
+15 -15
View File
@@ -19,7 +19,7 @@ describe('quotaRowsFor', () => {
});
expect(rows).toHaveLength(1);
expect(rows[0].label).toBe('周限额');
expect(rows[0].label).toBe('Weekly limit');
expect(rows[0].remainingPercent).toBe(3);
expect(rows[0].reset).toBeTruthy();
});
@@ -38,7 +38,7 @@ describe('quotaRowsFor', () => {
},
additional_rate_limits: [
{
limit_name: '代码审查增强',
limit_name: 'Enhanced code review',
rate_limit: {
primary_window: {
used_percent: 30,
@@ -50,9 +50,9 @@ describe('quotaRowsFor', () => {
});
expect(rows.map((row) => row.label)).toEqual([
'5 小时限额',
'月限额',
'代码审查增强 周限额',
'5-hour limit',
'Monthly limit',
'Enhanced code review Weekly limit',
]);
});
@@ -65,7 +65,7 @@ describe('quotaRowsFor', () => {
},
});
expect(rows.map((row) => row.label)).toEqual(['5 小时限额', '周限额']);
expect(rows.map((row) => row.label)).toEqual(['5-hour limit', 'Weekly limit']);
});
it('支持反序窗口且只在有重置时间时推断已耗尽额度', () => {
@@ -76,7 +76,7 @@ describe('quotaRowsFor', () => {
secondaryWindow: { limitWindowSeconds: 18000, resetAfterSeconds: 3600 },
},
});
expect(rows.map((row) => row.label)).toEqual(['5 小时限额', '周限额']);
expect(rows.map((row) => row.label)).toEqual(['5-hour limit', 'Weekly limit']);
expect(rows.map((row) => row.remainingPercent)).toEqual([0, null]);
expect(rows[0].resetAtMs).toBeGreaterThan(Date.now());
});
@@ -168,7 +168,7 @@ describe('quotaRowsFor', () => {
limits: [null, limit(null, true), limit(12, false, 'Fable 5'), limit(64, true), limit(99, true, 'Sonnet')],
});
expect(rows.map((row) => [row.label, row.remainingPercent])).toEqual([
['5 小时窗口', 90], ['7 天 Fable 窗口', 36],
['5-hour window', 90], ['7-day Fable window', 36],
]);
expect(rows[1].resetAtMs).toBe(Date.parse('2030-01-01T00:00:00Z'));
});
@@ -179,7 +179,7 @@ describe('quotaRowsFor', () => {
limits: [{ kind: 'weekly_scoped', percent: null, scope: { model: { display_name: 'Fable' } } }],
});
expect(rows).toHaveLength(1);
expect(rows[0]).toMatchObject({ label: '7 天 Fable 窗口', remainingPercent: 59 });
expect(rows[0]).toMatchObject({ label: '7-day Fable window', remainingPercent: 59 });
});
it('显示 Claude 已启用的额外用量', () => {
@@ -194,7 +194,7 @@ describe('quotaRowsFor', () => {
});
expect(rows.at(-1)).toMatchObject({
label: '额外用量',
label: 'Extra usage',
remainingPercent: 75,
});
expect(rows.at(-1)?.detail).toContain('$12.50');
@@ -228,7 +228,7 @@ describe('quotaRowsFor', () => {
{ window: { duration: 90, timeUnit: 'TIME_UNIT_SECOND' }, detail: { limit: 100, used: 0 } },
],
});
expect(rows.map((row) => row.label)).toEqual(['5 小时窗口', '7 天窗口', '5 小时窗口', '90 秒窗口', '每周额度']);
expect(rows.map((row) => row.label)).toEqual(['5 hours window', '7 days window', '5 hours window', '90 seconds window', 'Weekly quota']);
expect(rows.at(-1)?.remainingPercent).toBeCloseTo(0.5);
expect(rows[0].remainingPercent).toBe(80);
expect(rows[0].resetAtMs).toBeGreaterThan(Date.now());
@@ -266,7 +266,7 @@ describe('quotaRowsFor', () => {
expect(quotaRowsFor('xai', { config: { productUsage: [{ product: 'grok', usagePercent: 20 }] } })[1])
.toMatchObject({ label: 'grok', remainingPercent: 80 });
expect(quotaRowsFor('xai', { mode: 'paid-info' })[0])
.toMatchObject({ label: '付费 API 账号', remainingPercent: null });
.toMatchObject({ label: 'Paid API account', remainingPercent: null });
});
it('xAI 不从月账单借用周窗口的重置时间', () => {
@@ -297,10 +297,10 @@ describe('quotaRowsFor', () => {
},
});
expect(rows.find((row) => row.label === '每周额度')?.remainingPercent).toBe(75);
expect(rows.find((row) => row.label === 'Weekly quota')?.remainingPercent).toBe(75);
expect(rows.find((row) => row.label === 'grok-code')?.remainingPercent).toBe(60);
expect(rows.find((row) => row.label === '月度包含额度')?.remainingPercent).toBe(0);
expect(rows.find((row) => row.label === '按量付费额度')?.remainingPercent).toBe(60);
expect(rows.find((row) => row.label === 'Monthly included quota')?.remainingPercent).toBe(0);
expect(rows.find((row) => row.label === 'On-demand quota')?.remainingPercent).toBe(60);
});
it('xAI 保留 0 和 100 的边界值,缺失用量不显示全额余额', () => {
+1 -1
View File
@@ -61,7 +61,7 @@ describe('思考别名来源', () => {
test('区分同名模型的接入来源', () => {
expect(thinkingAliasSourceKindLabel('codex-oauth')).toBe('Codex OAuth');
expect(thinkingAliasSourceKindLabel('claude-api')).toBe('Claude API');
expect(thinkingAliasSourceKindLabel('openai-compatible')).toBe('OpenAI 兼容');
expect(thinkingAliasSourceKindLabel('openai-compatible')).toBe('OpenAI Compatible');
});
test('仅把内核报告了思考等级的来源标为可覆写', () => {