chore: unify code formatting and remove unused code

- Apply cargo fmt to Rust code with multiline error handling
- Apply Prettier formatting to TypeScript code with trailing commas
- Unify #[allow(non_snake_case)] attribute formatting
- Remove unused ProviderNotFound error variant from error.rs
- Add vitest-report.json to .gitignore to exclude test artifacts
- Optimize readability of error handling chains with vertical alignment

All tests passing: 22 Rust tests + 126 frontend tests
This commit is contained in:
Jason
2025-11-05 23:17:34 +08:00
parent 4f4c1e4ed7
commit d6fa0060fb
15 changed files with 344 additions and 195 deletions

1
.gitignore vendored
View File

@@ -11,3 +11,4 @@ CLAUDE.md
AGENTS.md
/.claude
/.vscode
vitest-report.json

View File

@@ -73,8 +73,7 @@ pub async fn open_config_folder(handle: AppHandle, app: String) -> Result<bool,
#[tauri::command]
pub async fn pick_directory(
app: AppHandle,
#[allow(non_snake_case)]
defaultPath: Option<String>,
#[allow(non_snake_case)] defaultPath: Option<String>,
) -> Result<Option<String>, String> {
let initial = defaultPath
.map(|p| p.trim().to_string())

View File

@@ -12,8 +12,7 @@ use crate::store::AppState;
/// 导出配置文件
#[tauri::command]
pub async fn export_config_to_file(
#[allow(non_snake_case)]
filePath: String
#[allow(non_snake_case)] filePath: String,
) -> Result<Value, String> {
tauri::async_runtime::spawn_blocking(move || {
let target_path = PathBuf::from(&filePath);
@@ -32,8 +31,7 @@ pub async fn export_config_to_file(
/// 从文件导入配置
#[tauri::command]
pub async fn import_config_from_file(
#[allow(non_snake_case)]
filePath: String,
#[allow(non_snake_case)] filePath: String,
state: State<'_, AppState>,
) -> Result<Value, String> {
let (new_config, backup_id) = tauri::async_runtime::spawn_blocking(move || {
@@ -81,8 +79,7 @@ pub async fn sync_current_providers_live(state: State<'_, AppState>) -> Result<V
#[tauri::command]
pub async fn save_file_dialog<R: tauri::Runtime>(
app: tauri::AppHandle<R>,
#[allow(non_snake_case)]
defaultName: String,
#[allow(non_snake_case)] defaultName: String,
) -> Result<Option<String>, String> {
let dialog = app.dialog();
let result = dialog

View File

@@ -1,8 +1,8 @@
#![allow(non_snake_case)]
use crate::init_status::InitErrorPayload;
use tauri::AppHandle;
use tauri_plugin_opener::OpenerExt;
use crate::init_status::InitErrorPayload;
/// 打开外部链接
#[tauri::command]

View File

@@ -112,8 +112,7 @@ pub fn import_default_config(state: State<'_, AppState>, app: String) -> Result<
#[tauri::command]
pub async fn queryProviderUsage(
state: State<'_, AppState>,
#[allow(non_snake_case)]
providerId: String, // 使用 camelCase 匹配前端
#[allow(non_snake_case)] providerId: String, // 使用 camelCase 匹配前端
app: String,
) -> Result<crate::provider::UsageResult, String> {
let app_type = AppType::from_str(&app).map_err(|e| e.to_string())?;
@@ -127,16 +126,12 @@ pub async fn queryProviderUsage(
#[tauri::command]
pub async fn testUsageScript(
state: State<'_, AppState>,
#[allow(non_snake_case)]
providerId: String,
#[allow(non_snake_case)] providerId: String,
app: String,
#[allow(non_snake_case)]
scriptCode: String,
#[allow(non_snake_case)] scriptCode: String,
timeout: Option<u64>,
#[allow(non_snake_case)]
accessToken: Option<String>,
#[allow(non_snake_case)]
userId: Option<String>,
#[allow(non_snake_case)] accessToken: Option<String>,
#[allow(non_snake_case)] userId: Option<String>,
) -> Result<crate::provider::UsageResult, String> {
let app_type = AppType::from_str(&app).map_err(|e| e.to_string())?;
ProviderService::test_usage_script(
@@ -163,8 +158,7 @@ pub fn read_live_provider_settings(app: String) -> Result<serde_json::Value, Str
#[tauri::command]
pub async fn test_api_endpoints(
urls: Vec<String>,
#[allow(non_snake_case)]
timeoutSecs: Option<u64>,
#[allow(non_snake_case)] timeoutSecs: Option<u64>,
) -> Result<Vec<EndpointLatency>, String> {
SpeedtestService::test_endpoints(urls, timeoutSecs)
.await
@@ -176,8 +170,7 @@ pub async fn test_api_endpoints(
pub fn get_custom_endpoints(
state: State<'_, AppState>,
app: String,
#[allow(non_snake_case)]
providerId: String,
#[allow(non_snake_case)] providerId: String,
) -> Result<Vec<crate::settings::CustomEndpoint>, String> {
let app_type = AppType::from_str(&app).map_err(|e| e.to_string())?;
ProviderService::get_custom_endpoints(state.inner(), app_type, &providerId)
@@ -189,8 +182,7 @@ pub fn get_custom_endpoints(
pub fn add_custom_endpoint(
state: State<'_, AppState>,
app: String,
#[allow(non_snake_case)]
providerId: String,
#[allow(non_snake_case)] providerId: String,
url: String,
) -> Result<(), String> {
let app_type = AppType::from_str(&app).map_err(|e| e.to_string())?;
@@ -203,8 +195,7 @@ pub fn add_custom_endpoint(
pub fn remove_custom_endpoint(
state: State<'_, AppState>,
app: String,
#[allow(non_snake_case)]
providerId: String,
#[allow(non_snake_case)] providerId: String,
url: String,
) -> Result<(), String> {
let app_type = AppType::from_str(&app).map_err(|e| e.to_string())?;
@@ -217,8 +208,7 @@ pub fn remove_custom_endpoint(
pub fn update_endpoint_last_used(
state: State<'_, AppState>,
app: String,
#[allow(non_snake_case)]
providerId: String,
#[allow(non_snake_case)] providerId: String,
url: String,
) -> Result<(), String> {
let app_type = AppType::from_str(&app).map_err(|e| e.to_string())?;

View File

@@ -39,4 +39,3 @@ mod tests {
assert_eq!(got.error, payload.error);
}
}

View File

@@ -142,19 +142,31 @@ impl ProviderService {
.and_then(|v| v.as_str())
.map(|s| s.to_string());
let target_haiku = current_haiku.or_else(|| small_fast.clone()).or_else(|| model.clone());
let target_sonnet = current_sonnet.or_else(|| model.clone()).or_else(|| small_fast.clone());
let target_opus = current_opus.or_else(|| model.clone()).or_else(|| small_fast.clone());
let target_haiku = current_haiku
.or_else(|| small_fast.clone())
.or_else(|| model.clone());
let target_sonnet = current_sonnet
.or_else(|| model.clone())
.or_else(|| small_fast.clone());
let target_opus = current_opus
.or_else(|| model.clone())
.or_else(|| small_fast.clone());
if env.get("ANTHROPIC_DEFAULT_HAIKU_MODEL").is_none() {
if let Some(v) = target_haiku {
env.insert("ANTHROPIC_DEFAULT_HAIKU_MODEL".to_string(), Value::String(v));
env.insert(
"ANTHROPIC_DEFAULT_HAIKU_MODEL".to_string(),
Value::String(v),
);
changed = true;
}
}
if env.get("ANTHROPIC_DEFAULT_SONNET_MODEL").is_none() {
if let Some(v) = target_sonnet {
env.insert("ANTHROPIC_DEFAULT_SONNET_MODEL".to_string(), Value::String(v));
env.insert(
"ANTHROPIC_DEFAULT_SONNET_MODEL".to_string(),
Value::String(v),
);
changed = true;
}
}
@@ -619,16 +631,13 @@ impl ProviderService {
let manager = cfg
.get_manager_mut(&app_type)
.ok_or_else(|| Self::app_not_found(&app_type))?;
let provider = manager
.providers
.get_mut(provider_id)
.ok_or_else(|| {
AppError::localized(
"provider.not_found",
format!("供应商不存在: {}", provider_id),
format!("Provider not found: {}", provider_id),
)
})?;
let provider = manager.providers.get_mut(provider_id).ok_or_else(|| {
AppError::localized(
"provider.not_found",
format!("供应商不存在: {}", provider_id),
format!("Provider not found: {}", provider_id),
)
})?;
let meta = provider.meta.get_or_insert_with(ProviderMeta::default);
let endpoint = CustomEndpoint {
@@ -737,19 +746,21 @@ impl ProviderService {
{
Ok(data) => {
let usage_list: Vec<UsageData> = if data.is_array() {
serde_json::from_value(data)
.map_err(|e| AppError::localized(
serde_json::from_value(data).map_err(|e| {
AppError::localized(
"usage_script.data_format_error",
format!("数据格式错误: {}", e),
format!("Data format error: {}", e)
))?
format!("Data format error: {}", e),
)
})?
} else {
let single: UsageData = serde_json::from_value(data)
.map_err(|e| AppError::localized(
let single: UsageData = serde_json::from_value(data).map_err(|e| {
AppError::localized(
"usage_script.data_format_error",
format!("数据格式错误: {}", e),
format!("Data format error: {}", e)
))?;
format!("Data format error: {}", e),
)
})?;
vec![single]
};
@@ -766,7 +777,11 @@ impl ProviderService {
let msg = match err {
AppError::Localized { zh, en, .. } => {
if lang == "en" { en } else { zh }
if lang == "en" {
en
} else {
zh
}
}
other => other.to_string(),
};
@@ -791,17 +806,13 @@ impl ProviderService {
let manager = config
.get_manager(&app_type)
.ok_or_else(|| Self::app_not_found(&app_type))?;
let provider = manager
.providers
.get(provider_id)
.cloned()
.ok_or_else(|| {
AppError::localized(
"provider.not_found",
format!("供应商不存在: {}", provider_id),
format!("Provider not found: {}", provider_id),
)
})?;
let provider = manager.providers.get(provider_id).cloned().ok_or_else(|| {
AppError::localized(
"provider.not_found",
format!("供应商不存在: {}", provider_id),
format!("Provider not found: {}", provider_id),
)
})?;
let (script_code, timeout, access_token, user_id) = {
let usage_script = provider
.meta
@@ -861,17 +872,13 @@ impl ProviderService {
let manager = config
.get_manager(&app_type)
.ok_or_else(|| Self::app_not_found(&app_type))?;
manager
.providers
.get(provider_id)
.cloned()
.ok_or_else(|| {
AppError::localized(
"provider.not_found",
format!("供应商不存在: {}", provider_id),
format!("Provider not found: {}", provider_id),
)
})?
manager.providers.get(provider_id).cloned().ok_or_else(|| {
AppError::localized(
"provider.not_found",
format!("供应商不存在: {}", provider_id),
format!("Provider not found: {}", provider_id),
)
})?
};
let (api_key, base_url) = Self::extract_credentials(&provider, &app_type)?;
@@ -1206,12 +1213,13 @@ impl ProviderService {
.unwrap_or("");
let base_url = if config_toml.contains("base_url") {
let re = Regex::new(r#"base_url\s*=\s*["']([^"']+)["']"#)
.map_err(|e| AppError::localized(
let re = Regex::new(r#"base_url\s*=\s*["']([^"']+)["']"#).map_err(|e| {
AppError::localized(
"provider.regex_init_failed",
format!("正则初始化失败: {}", e),
format!("Failed to initialize regex: {}", e)
))?;
format!("Failed to initialize regex: {}", e),
)
})?;
re.captures(config_toml)
.and_then(|caps| caps.get(1))
.map(|m| m.as_str().to_string())
@@ -1239,7 +1247,7 @@ impl ProviderService {
AppError::localized(
"provider.app_not_found",
format!("应用类型不存在: {:?}", app_type),
format!("App type not found: {:?}", app_type)
format!("App type not found: {:?}", app_type),
)
}
@@ -1265,17 +1273,13 @@ impl ProviderService {
));
}
manager
.providers
.get(provider_id)
.cloned()
.ok_or_else(|| {
AppError::localized(
"provider.not_found",
format!("供应商不存在: {}", provider_id),
format!("Provider not found: {}", provider_id),
)
})?
manager.providers.get(provider_id).cloned().ok_or_else(|| {
AppError::localized(
"provider.not_found",
format!("供应商不存在: {}", provider_id),
format!("Provider not found: {}", provider_id),
)
})?
};
match app_type {

View File

@@ -101,11 +101,13 @@ impl SpeedtestService {
.redirect(reqwest::redirect::Policy::limited(5))
.user_agent("cc-switch-speedtest/1.0")
.build()
.map_err(|e| AppError::localized(
"speedtest.client_create_failed",
format!("创建 HTTP 客户端失败: {e}"),
format!("Failed to create HTTP client: {e}")
))
.map_err(|e| {
AppError::localized(
"speedtest.client_create_failed",
format!("创建 HTTP 客户端失败: {e}"),
format!("Failed to create HTTP client: {e}"),
)
})
}
fn sanitize_timeout(timeout_secs: Option<u64>) -> u64 {

View File

@@ -30,80 +30,171 @@ pub async fn execute_usage_script(
// 2. 在独立作用域中提取 request 配置(确保 Runtime/Context 在 await 前释放)
let request_config = {
let runtime =
Runtime::new().map_err(|e| AppError::localized("usage_script.runtime_create_failed", format!("创建 JS 运行时失败: {}", e), format!("Failed to create JS runtime: {}", e)))?;
let context = Context::full(&runtime)
.map_err(|e| AppError::localized("usage_script.context_create_failed", format!("创建 JS 上下文失败: {}", e), format!("Failed to create JS context: {}", e)))?;
let runtime = Runtime::new().map_err(|e| {
AppError::localized(
"usage_script.runtime_create_failed",
format!("创建 JS 运行时失败: {}", e),
format!("Failed to create JS runtime: {}", e),
)
})?;
let context = Context::full(&runtime).map_err(|e| {
AppError::localized(
"usage_script.context_create_failed",
format!("创建 JS 上下文失败: {}", e),
format!("Failed to create JS context: {}", e),
)
})?;
context.with(|ctx| {
// 执行用户代码,获取配置对象
let config: rquickjs::Object = ctx
.eval(replaced.clone())
.map_err(|e| AppError::localized("usage_script.config_parse_failed", format!("解析配置失败: {}", e), format!("Failed to parse config: {}", e)))?;
let config: rquickjs::Object = ctx.eval(replaced.clone()).map_err(|e| {
AppError::localized(
"usage_script.config_parse_failed",
format!("解析配置失败: {}", e),
format!("Failed to parse config: {}", e),
)
})?;
// 提取 request 配置
let request: rquickjs::Object = config
.get("request")
.map_err(|e| AppError::localized("usage_script.request_missing", format!("缺少 request 配置: {}", e), format!("Missing request config: {}", e)))?;
let request: rquickjs::Object = config.get("request").map_err(|e| {
AppError::localized(
"usage_script.request_missing",
format!("缺少 request 配置: {}", e),
format!("Missing request config: {}", e),
)
})?;
// 将 request 转换为 JSON 字符串
let request_json: String = ctx
.json_stringify(request)
.map_err(|e| AppError::localized("usage_script.request_serialize_failed", format!("序列化 request 失败: {}", e), format!("Failed to serialize request: {}", e)))?
.ok_or_else(|| AppError::localized("usage_script.serialize_none", "序列化返回 None", "Serialization returned None"))?
.map_err(|e| {
AppError::localized(
"usage_script.request_serialize_failed",
format!("序列化 request 失败: {}", e),
format!("Failed to serialize request: {}", e),
)
})?
.ok_or_else(|| {
AppError::localized(
"usage_script.serialize_none",
"序列化返回 None",
"Serialization returned None",
)
})?
.get()
.map_err(|e| AppError::localized("usage_script.get_string_failed", format!("获取字符串失败: {}", e), format!("Failed to get string: {}", e)))?;
.map_err(|e| {
AppError::localized(
"usage_script.get_string_failed",
format!("获取字符串失败: {}", e),
format!("Failed to get string: {}", e),
)
})?;
Ok::<_, AppError>(request_json)
})?
}; // Runtime 和 Context 在这里被 drop
// 3. 解析 request 配置
let request: RequestConfig = serde_json::from_str(&request_config)
.map_err(|e| AppError::localized("usage_script.request_format_invalid", format!("request 配置格式错误: {}", e), format!("Invalid request config format: {}", e)))?;
let request: RequestConfig = serde_json::from_str(&request_config).map_err(|e| {
AppError::localized(
"usage_script.request_format_invalid",
format!("request 配置格式错误: {}", e),
format!("Invalid request config format: {}", e),
)
})?;
// 4. 发送 HTTP 请求
let response_data = send_http_request(&request, timeout_secs).await?;
// 5. 在独立作用域中执行 extractor确保 Runtime/Context 在函数结束前释放)
let result: Value = {
let runtime =
Runtime::new().map_err(|e| AppError::localized("usage_script.runtime_create_failed", format!("创建 JS 运行时失败: {}", e), format!("Failed to create JS runtime: {}", e)))?;
let context = Context::full(&runtime)
.map_err(|e| AppError::localized("usage_script.context_create_failed", format!("创建 JS 上下文失败: {}", e), format!("Failed to create JS context: {}", e)))?;
let runtime = Runtime::new().map_err(|e| {
AppError::localized(
"usage_script.runtime_create_failed",
format!("创建 JS 运行时失败: {}", e),
format!("Failed to create JS runtime: {}", e),
)
})?;
let context = Context::full(&runtime).map_err(|e| {
AppError::localized(
"usage_script.context_create_failed",
format!("创建 JS 上下文失败: {}", e),
format!("Failed to create JS context: {}", e),
)
})?;
context.with(|ctx| {
// 重新 eval 获取配置对象
let config: rquickjs::Object = ctx
.eval(replaced.clone())
.map_err(|e| AppError::localized("usage_script.config_reparse_failed", format!("重新解析配置失败: {}", e), format!("Failed to re-parse config: {}", e)))?;
let config: rquickjs::Object = ctx.eval(replaced.clone()).map_err(|e| {
AppError::localized(
"usage_script.config_reparse_failed",
format!("重新解析配置失败: {}", e),
format!("Failed to re-parse config: {}", e),
)
})?;
// 提取 extractor 函数
let extractor: Function = config
.get("extractor")
.map_err(|e| AppError::localized("usage_script.extractor_missing", format!("缺少 extractor 函数: {}", e), format!("Missing extractor function: {}", e)))?;
let extractor: Function = config.get("extractor").map_err(|e| {
AppError::localized(
"usage_script.extractor_missing",
format!("缺少 extractor 函数: {}", e),
format!("Missing extractor function: {}", e),
)
})?;
// 将响应数据转换为 JS 值
let response_js: rquickjs::Value = ctx
.json_parse(response_data.as_str())
.map_err(|e| AppError::localized("usage_script.response_parse_failed", format!("解析响应 JSON 失败: {}", e), format!("Failed to parse response JSON: {}", e)))?;
let response_js: rquickjs::Value =
ctx.json_parse(response_data.as_str()).map_err(|e| {
AppError::localized(
"usage_script.response_parse_failed",
format!("解析响应 JSON 失败: {}", e),
format!("Failed to parse response JSON: {}", e),
)
})?;
// 调用 extractor(response)
let result_js: rquickjs::Value = extractor
.call((response_js,))
.map_err(|e| AppError::localized("usage_script.extractor_exec_failed", format!("执行 extractor 失败: {}", e), format!("Failed to execute extractor: {}", e)))?;
let result_js: rquickjs::Value = extractor.call((response_js,)).map_err(|e| {
AppError::localized(
"usage_script.extractor_exec_failed",
format!("执行 extractor 失败: {}", e),
format!("Failed to execute extractor: {}", e),
)
})?;
// 转换为 JSON 字符串
let result_json: String = ctx
.json_stringify(result_js)
.map_err(|e| AppError::localized("usage_script.result_serialize_failed", format!("序列化结果失败: {}", e), format!("Failed to serialize result: {}", e)))?
.ok_or_else(|| AppError::localized("usage_script.serialize_none", "序列化返回 None", "Serialization returned None"))?
.map_err(|e| {
AppError::localized(
"usage_script.result_serialize_failed",
format!("序列化结果失败: {}", e),
format!("Failed to serialize result: {}", e),
)
})?
.ok_or_else(|| {
AppError::localized(
"usage_script.serialize_none",
"序列化返回 None",
"Serialization returned None",
)
})?
.get()
.map_err(|e| AppError::localized("usage_script.get_string_failed", format!("获取字符串失败: {}", e), format!("Failed to get string: {}", e)))?;
.map_err(|e| {
AppError::localized(
"usage_script.get_string_failed",
format!("获取字符串失败: {}", e),
format!("Failed to get string: {}", e),
)
})?;
// 解析为 serde_json::Value
serde_json::from_str(&result_json)
.map_err(|e| AppError::localized("usage_script.json_parse_failed", format!("JSON 解析失败: {}", e), format!("JSON parse failed: {}", e)))
serde_json::from_str(&result_json).map_err(|e| {
AppError::localized(
"usage_script.json_parse_failed",
format!("JSON 解析失败: {}", e),
format!("JSON parse failed: {}", e),
)
})
})?
}; // Runtime 和 Context 在这里被 drop
@@ -131,20 +222,23 @@ async fn send_http_request(config: &RequestConfig, timeout_secs: u64) -> Result<
let client = Client::builder()
.timeout(Duration::from_secs(timeout))
.build()
.map_err(|e| AppError::localized("usage_script.client_create_failed", format!("创建客户端失败: {}", e), format!("Failed to create client: {}", e)))?;
// 严格校验 HTTP 方法,非法值不回退为 GET
let method: reqwest::Method = config
.method
.parse()
.map_err(|_| {
.map_err(|e| {
AppError::localized(
"usage_script.invalid_http_method",
format!("不支持的 HTTP 方法: {}", config.method),
format!("Unsupported HTTP method: {}", config.method),
"usage_script.client_create_failed",
format!("创建客户端失败: {}", e),
format!("Failed to create client: {}", e),
)
})?;
// 严格校验 HTTP 方法,非法值不回退为 GET
let method: reqwest::Method = config.method.parse().map_err(|_| {
AppError::localized(
"usage_script.invalid_http_method",
format!("不支持的 HTTP 方法: {}", config.method),
format!("Unsupported HTTP method: {}", config.method),
)
})?;
let mut req = client.request(method.clone(), &config.url);
// 添加请求头
@@ -158,16 +252,22 @@ async fn send_http_request(config: &RequestConfig, timeout_secs: u64) -> Result<
}
// 发送请求
let resp = req
.send()
.await
.map_err(|e| AppError::localized("usage_script.request_failed", format!("请求失败: {}", e), format!("Request failed: {}", e)))?;
let resp = req.send().await.map_err(|e| {
AppError::localized(
"usage_script.request_failed",
format!("请求失败: {}", e),
format!("Request failed: {}", e),
)
})?;
let status = resp.status();
let text = resp
.text()
.await
.map_err(|e| AppError::localized("usage_script.read_response_failed", format!("读取响应失败: {}", e), format!("Failed to read response: {}", e)))?;
let text = resp.text().await.map_err(|e| {
AppError::localized(
"usage_script.read_response_failed",
format!("读取响应失败: {}", e),
format!("Failed to read response: {}", e),
)
})?;
if !status.is_success() {
let preview = if text.len() > 200 {
@@ -175,7 +275,11 @@ async fn send_http_request(config: &RequestConfig, timeout_secs: u64) -> Result<
} else {
text.clone()
};
return Err(AppError::localized("usage_script.http_error", format!("HTTP {} : {}", status, preview), format!("HTTP {} : {}", status, preview)));
return Err(AppError::localized(
"usage_script.http_error",
format!("HTTP {} : {}", status, preview),
format!("HTTP {} : {}", status, preview),
));
}
Ok(text)
@@ -186,11 +290,20 @@ fn validate_result(result: &Value) -> Result<(), AppError> {
// 如果是数组,验证每个元素
if let Some(arr) = result.as_array() {
if arr.is_empty() {
return Err(AppError::localized("usage_script.empty_array", "脚本返回的数组不能为空", "Script returned empty array"));
return Err(AppError::localized(
"usage_script.empty_array",
"脚本返回的数组不能为空",
"Script returned empty array",
));
}
for (idx, item) in arr.iter().enumerate() {
validate_single_usage(item)
.map_err(|e| AppError::localized("usage_script.array_validation_failed", format!("数组索引[{}]验证失败: {}", idx, e), format!("Validation failed at index [{}]: {}", idx, e)))?;
validate_single_usage(item).map_err(|e| {
AppError::localized(
"usage_script.array_validation_failed",
format!("数组索引[{}]验证失败: {}", idx, e),
format!("Validation failed at index [{}]: {}", idx, e),
)
})?;
}
return Ok(());
}
@@ -201,46 +314,82 @@ fn validate_result(result: &Value) -> Result<(), AppError> {
/// 验证单个用量数据对象
fn validate_single_usage(result: &Value) -> Result<(), AppError> {
let obj = result
.as_object()
.ok_or_else(|| AppError::localized("usage_script.must_return_object", "脚本必须返回对象或对象数组", "Script must return object or array of objects"))?;
let obj = result.as_object().ok_or_else(|| {
AppError::localized(
"usage_script.must_return_object",
"脚本必须返回对象或对象数组",
"Script must return object or array of objects",
)
})?;
// 所有字段均为可选,只进行类型检查
if obj.contains_key("isValid")
&& !result["isValid"].is_null()
&& !result["isValid"].is_boolean()
{
return Err(AppError::localized("usage_script.isvalid_type_error", "isValid 必须是布尔值或 null", "isValid must be boolean or null"));
return Err(AppError::localized(
"usage_script.isvalid_type_error",
"isValid 必须是布尔值或 null",
"isValid must be boolean or null",
));
}
if obj.contains_key("invalidMessage")
&& !result["invalidMessage"].is_null()
&& !result["invalidMessage"].is_string()
{
return Err(AppError::localized("usage_script.invalidmessage_type_error", "invalidMessage 必须是字符串或 null", "invalidMessage must be string or null"));
return Err(AppError::localized(
"usage_script.invalidmessage_type_error",
"invalidMessage 必须是字符串或 null",
"invalidMessage must be string or null",
));
}
if obj.contains_key("remaining")
&& !result["remaining"].is_null()
&& !result["remaining"].is_number()
{
return Err(AppError::localized("usage_script.remaining_type_error", "remaining 必须是数字或 null", "remaining must be number or null"));
return Err(AppError::localized(
"usage_script.remaining_type_error",
"remaining 必须是数字或 null",
"remaining must be number or null",
));
}
if obj.contains_key("unit") && !result["unit"].is_null() && !result["unit"].is_string() {
return Err(AppError::localized("usage_script.unit_type_error", "unit 必须是字符串或 null", "unit must be string or null"));
return Err(AppError::localized(
"usage_script.unit_type_error",
"unit 必须是字符串或 null",
"unit must be string or null",
));
}
if obj.contains_key("total") && !result["total"].is_null() && !result["total"].is_number() {
return Err(AppError::localized("usage_script.total_type_error", "total 必须是数字或 null", "total must be number or null"));
return Err(AppError::localized(
"usage_script.total_type_error",
"total 必须是数字或 null",
"total must be number or null",
));
}
if obj.contains_key("used") && !result["used"].is_null() && !result["used"].is_number() {
return Err(AppError::localized("usage_script.used_type_error", "used 必须是数字或 null", "used must be number or null"));
return Err(AppError::localized(
"usage_script.used_type_error",
"used 必须是数字或 null",
"used must be number or null",
));
}
if obj.contains_key("planName")
&& !result["planName"].is_null()
&& !result["planName"].is_string()
{
return Err(AppError::localized("usage_script.planname_type_error", "planName 必须是字符串或 null", "planName must be string or null"));
return Err(AppError::localized(
"usage_script.planname_type_error",
"planName 必须是字符串或 null",
"planName must be string or null",
));
}
if obj.contains_key("extra") && !result["extra"].is_null() && !result["extra"].is_string() {
return Err(AppError::localized("usage_script.extra_type_error", "extra 必须是字符串或 null", "extra must be string or null"));
return Err(AppError::localized(
"usage_script.extra_type_error",
"extra 必须是字符串或 null",
"extra must be string or null",
));
}
Ok(())

View File

@@ -240,8 +240,8 @@ fn provider_service_switch_missing_provider_returns_error() {
let err = ProviderService::switch(&state, AppType::Claude, "missing")
.expect_err("switching missing provider should fail");
match err {
AppError::ProviderNotFound(id) => assert_eq!(id, "missing"),
other => panic!("expected ProviderNotFound, got {other:?}"),
AppError::Localized { key, .. } => assert_eq!(key, "provider.not_found"),
other => panic!("expected Localized error for provider not found, got {other:?}"),
}
}

View File

@@ -333,7 +333,7 @@ const UsagePlanItem: React.FC<{ data: UsageData }> = ({ data }) => {
function formatRelativeTime(
timestamp: number,
now: number,
t: (key: string, options?: { count?: number }) => string
t: (key: string, options?: { count?: number }) => string,
): string {
const diff = Math.floor((now - timestamp) / 1000); // 秒

View File

@@ -33,7 +33,9 @@ const TEMPLATE_KEYS = {
} as const;
// 生成预设模板的函数(支持国际化)
const generatePresetTemplates = (t: (key: string) => string): Record<string, string> => ({
const generatePresetTemplates = (
t: (key: string) => string,
): Record<string, string> => ({
[TEMPLATE_KEYS.CUSTOM]: `({
request: {
url: "",
@@ -135,7 +137,7 @@ const UsageScriptModal: React.FC<UsageScriptModalProps> = ({
return TEMPLATE_KEYS.NEW_API;
}
return null;
}
},
);
const handleSave = () => {
@@ -165,7 +167,7 @@ const UsageScriptModal: React.FC<UsageScriptModalProps> = ({
script.code,
script.timeout,
script.accessToken,
script.userId
script.userId,
);
if (result.success && result.data && result.data.length > 0) {
// 显示所有套餐数据
@@ -183,7 +185,7 @@ const UsageScriptModal: React.FC<UsageScriptModalProps> = ({
`${t("usageScript.testFailed")}: ${result.error || t("endpointTest.noResult")}`,
{
duration: 5000,
}
},
);
}
} catch (error: any) {
@@ -191,7 +193,7 @@ const UsageScriptModal: React.FC<UsageScriptModalProps> = ({
`${t("usageScript.testFailed")}: ${error?.message || t("common.unknown")}`,
{
duration: 5000,
}
},
);
} finally {
setTesting(false);
@@ -215,7 +217,7 @@ const UsageScriptModal: React.FC<UsageScriptModalProps> = ({
`${t("usageScript.formatFailed")}: ${error?.message || t("jsonEditor.invalidJson")}`,
{
duration: 3000,
}
},
);
}
};

View File

@@ -331,26 +331,34 @@ export function ProviderForm({
// 处理 meta 字段:基于 draftCustomEndpoints 生成 custom_endpoints
// 注意:不使用 customEndpointsMap因为它包含了候选端点预设、Base URL 等)
// 而我们只需要保存用户真正添加的自定义端点
const customEndpointsToSave: Record<string, import("@/types").CustomEndpoint> | null =
const customEndpointsToSave: Record<
string,
import("@/types").CustomEndpoint
> | null =
draftCustomEndpoints.length > 0
? draftCustomEndpoints.reduce((acc, url) => {
// 尝试从 initialData.meta 中获取原有的端点元数据(保留 addedAt 和 lastUsed
const existing = initialData?.meta?.custom_endpoints?.[url];
if (existing) {
acc[url] = existing;
} else {
// 新端点:使用当前时间戳
const now = Date.now();
acc[url] = { url, addedAt: now, lastUsed: undefined };
}
return acc;
}, {} as Record<string, import("@/types").CustomEndpoint>)
? draftCustomEndpoints.reduce(
(acc, url) => {
// 尝试从 initialData.meta 中获取原有的端点元数据(保留 addedAt 和 lastUsed
const existing = initialData?.meta?.custom_endpoints?.[url];
if (existing) {
acc[url] = existing;
} else {
// 新端点:使用当前时间戳
const now = Date.now();
acc[url] = { url, addedAt: now, lastUsed: undefined };
}
return acc;
},
{} as Record<string, import("@/types").CustomEndpoint>,
)
: null;
// 检测是否需要清空端点(重要:区分"用户清空端点"和"用户没有修改端点"
const hadEndpoints = initialData?.meta?.custom_endpoints &&
Object.keys(initialData.meta.custom_endpoints).length > 0;
const needsClearEndpoints = hadEndpoints && draftCustomEndpoints.length === 0;
const hadEndpoints =
initialData?.meta?.custom_endpoints &&
Object.keys(initialData.meta.custom_endpoints).length > 0;
const needsClearEndpoints =
hadEndpoints && draftCustomEndpoints.length === 0;
// 如果用户明确清空了端点,传递空对象(而不是 null让后端知道要删除
const mergedMeta = needsClearEndpoints

View File

@@ -121,9 +121,7 @@ requires_openai_auth = true`,
"https://www.dmxapi.cn/v1",
"gpt-5-codex",
),
endpointCandidates: [
"https://www.dmxapi.cn/v1",
],
endpointCandidates: ["https://www.dmxapi.cn/v1"],
},
{
name: "PackyCode",

View File

@@ -33,7 +33,7 @@ export const usageApi = {
scriptCode: string,
timeout?: number,
accessToken?: string,
userId?: string
userId?: string,
): Promise<UsageResult> {
try {
return await invoke("testUsageScript", {