Skip to content

Commit 861e4ed

Browse files
author
李杰
committed
release: v0.21.3
1 parent 5197a3b commit 861e4ed

45 files changed

Lines changed: 2947 additions & 95 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

CHANGELOG.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,17 @@ All notable changes to Cockpit Tools will be documented in this file.
66

77
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
88

9+
---
10+
## [0.21.3] - 2026-04-13
11+
12+
### Added
13+
- **Windsurf account onboarding now supports email/password sign-in again, including batch import**: the add-account dialog now supports single-account login plus batch import from JSON arrays or delimiter-based text, and failed rows return explicit line-level error feedback while successful logins immediately sync managed account data.
14+
- **Codex account groups now support in-group quick add, direct removal, and group deletion workflows**: users can enter a group as a scoped view, add more accounts from a picker, remove one or many accounts from that group, and delete the group with in-modal confirmation and error feedback.
15+
16+
### Changed
17+
- **Shared Accounts and Codex account pages now expose faster group-entry actions around folder views**: group cards, table rows, and in-group breadcrumb toolbars now provide direct add-account actions, and moving Codex accounts between groups excludes the current source group to avoid no-op targets.
18+
- **Opening the live Codex `config.toml` now goes through the desktop backend instead of frontend path opening**: Quick Settings and the model-provider quick config card now resolve and open the active file through the Tauri opener command for more reliable desktop behavior.
19+
920
---
1021
## [0.21.2] - 2026-04-13
1122

CHANGELOG.zh-CN.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,17 @@
66

77
格式参考 [Keep a Changelog](https://keepachangelog.com/zh-CN/1.0.0/)
88

9+
---
10+
## [0.21.3] - 2026-04-13
11+
12+
### 新增
13+
- **Windsurf 账号接入现已重新支持邮箱密码登录,并补充批量导入**:添加账号弹框现在既支持单个账号邮箱密码登录,也支持通过 JSON 数组或按分隔符逐行文本批量导入;批量失败项会返回明确的行级错误提示,成功登录后会立即同步受管账号数据。
14+
- **Codex 账号分组现已支持组内快速加号、直接移出与删除分组流程**:可直接进入某个分组范围查看账号,通过选择器继续补充账号,按单个或批量方式把账号移出该分组,并在弹框内完成删除确认与错误提示。
15+
16+
### 变更
17+
- **聚合账号页与 Codex 账号页现已补齐围绕分组视图的快捷操作入口**:分组卡片、表格行以及组内面包屑工具栏现在都提供直接加账号入口;在 Codex 分组间移动账号时,也会自动排除当前来源分组,避免出现无效目标。
18+
- **打开当前生效的 Codex `config.toml` 现已统一改走桌面端后端命令**:快捷设置与模型供应商快捷配置卡片不再依赖前端直接按路径打开文件,而是通过 Tauri opener 命令解析并打开当前实际生效的配置文件。
19+
920
---
1021
## [0.21.2] - 2026-04-13
1122

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "cockpit-tools",
33
"private": true,
4-
"version": "0.21.2",
4+
"version": "0.21.3",
55
"type": "module",
66
"scripts": {
77
"dev": "vite",

src-tauri/Cargo.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src-tauri/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "cockpit-tools"
3-
version = "0.21.2"
3+
version = "0.21.3"
44
description = "Cockpit Tools"
55
authors = ["jlcodes"]
66
license = "CC-BY-NC-SA-4.0"

src-tauri/src/commands/codex.rs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ use crate::modules::{
88
use std::sync::atomic::{AtomicBool, Ordering};
99
use tauri::AppHandle;
1010
use tauri::Emitter;
11+
use tauri_plugin_opener::OpenerExt;
1112

1213
static CODEX_POST_REFRESH_CHECK_IN_PROGRESS: AtomicBool = AtomicBool::new(false);
1314

@@ -29,6 +30,18 @@ pub fn get_codex_config_toml_path() -> Result<String, String> {
2930
Ok(path.to_string_lossy().to_string())
3031
}
3132

33+
#[tauri::command]
34+
pub fn open_codex_config_toml(app: AppHandle) -> Result<(), String> {
35+
let path = codex_account::get_codex_home().join("config.toml");
36+
if !path.exists() {
37+
return Err(format!("未找到 Codex config.toml 文件: {}", path.display()));
38+
}
39+
40+
app.opener()
41+
.open_path(path.to_string_lossy().to_string(), None::<String>)
42+
.map_err(|e| format!("打开 Codex config.toml 失败: {}", e))
43+
}
44+
3245
#[tauri::command]
3346
pub fn get_codex_quick_config() -> Result<CodexQuickConfig, String> {
3447
codex_account::load_current_quick_config()

src-tauri/src/commands/system.rs

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -455,7 +455,9 @@ pub fn save_auto_backup_settings(
455455
}
456456

457457
#[tauri::command]
458-
pub fn update_auto_backup_last_run(last_backup_at: Option<String>) -> Result<AutoBackupSettings, String> {
458+
pub fn update_auto_backup_last_run(
459+
last_backup_at: Option<String>,
460+
) -> Result<AutoBackupSettings, String> {
459461
let current = config::get_user_config();
460462
let normalized_last_backup_at = last_backup_at.and_then(|value| {
461463
let trimmed = value.trim().to_string();
@@ -508,7 +510,8 @@ pub fn list_auto_backup_files() -> Result<Vec<AutoBackupFileEntry>, String> {
508510
Some(name) if name.ends_with(".json") => name.to_string(),
509511
_ => continue,
510512
};
511-
let metadata = fs::metadata(&path).map_err(|err| format!("读取备份文件信息失败: {}", err))?;
513+
let metadata =
514+
fs::metadata(&path).map_err(|err| format!("读取备份文件信息失败: {}", err))?;
512515
files.push(AutoBackupFileEntry {
513516
file_name,
514517
path: path.to_string_lossy().to_string(),
@@ -547,7 +550,9 @@ pub fn cleanup_auto_backup_files(retention_days: i32) -> Result<Vec<String>, Str
547550
let normalized_retention_days = config::sanitize_auto_backup_retention_days(retention_days);
548551
let now = SystemTime::now();
549552
let cutoff = now
550-
.checked_sub(Duration::from_secs(normalized_retention_days as u64 * 24 * 60 * 60))
553+
.checked_sub(Duration::from_secs(
554+
normalized_retention_days as u64 * 24 * 60 * 60,
555+
))
551556
.unwrap_or(now);
552557

553558
let mut deleted = Vec::new();
@@ -562,7 +567,8 @@ pub fn cleanup_auto_backup_files(retention_days: i32) -> Result<Vec<String>, Str
562567
Some(name) if name.ends_with(".json") => name.to_string(),
563568
_ => continue,
564569
};
565-
let metadata = fs::metadata(&path).map_err(|err| format!("读取备份文件信息失败: {}", err))?;
570+
let metadata =
571+
fs::metadata(&path).map_err(|err| format!("读取备份文件信息失败: {}", err))?;
566572
let modified = match metadata.modified() {
567573
Ok(value) => value,
568574
Err(_) => continue,

src-tauri/src/commands/windsurf.rs

Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,33 @@
11
use std::time::Instant;
2+
use serde::{Deserialize, Serialize};
23
use tauri::{AppHandle, Emitter};
34

45
use crate::models::windsurf::{WindsurfAccount, WindsurfOAuthStartResponse};
56
use crate::modules::{logger, windsurf_account, windsurf_oauth};
67

8+
#[derive(Debug, Deserialize)]
9+
pub struct WindsurfPasswordCredentialInput {
10+
pub email: String,
11+
pub password: String,
12+
#[serde(default)]
13+
pub source_line: Option<usize>,
14+
}
15+
16+
#[derive(Debug, Serialize)]
17+
pub struct WindsurfPasswordCredentialFailure {
18+
pub email: String,
19+
pub error: String,
20+
pub source_line: Option<usize>,
21+
}
22+
23+
#[derive(Debug, Serialize)]
24+
pub struct WindsurfPasswordBatchResult {
25+
pub accounts: Vec<WindsurfAccount>,
26+
pub success_count: usize,
27+
pub failed_count: usize,
28+
pub failures: Vec<WindsurfPasswordCredentialFailure>,
29+
}
30+
731
async fn refresh_windsurf_account_after_login(account: WindsurfAccount) -> WindsurfAccount {
832
let account_id = account.id.clone();
933
match windsurf_account::refresh_account_token(&account_id).await {
@@ -18,6 +42,14 @@ async fn refresh_windsurf_account_after_login(account: WindsurfAccount) -> Winds
1842
}
1943
}
2044

45+
async fn add_windsurf_account_from_password(
46+
email: &str,
47+
password: &str,
48+
) -> Result<WindsurfAccount, String> {
49+
let payload = windsurf_oauth::build_payload_from_password(email, password).await?;
50+
windsurf_account::upsert_account(payload)
51+
}
52+
2153
#[tauri::command]
2254
pub fn list_windsurf_accounts() -> Result<Vec<WindsurfAccount>, String> {
2355
windsurf_account::list_accounts_checked()
@@ -186,6 +218,103 @@ pub async fn add_windsurf_account_with_token(
186218
Ok(account)
187219
}
188220

221+
#[tauri::command]
222+
pub async fn add_windsurf_account_with_password(
223+
app: AppHandle,
224+
email: String,
225+
password: String,
226+
) -> Result<WindsurfAccount, String> {
227+
logger::log_info("[Windsurf Command] 邮箱密码登录开始");
228+
let account = add_windsurf_account_from_password(&email, &password).await?;
229+
logger::log_info(&format!(
230+
"[Windsurf Command] 邮箱密码登录成功: account_id={}, login={}",
231+
account.id, account.github_login
232+
));
233+
let _ = crate::modules::tray::update_tray_menu(&app);
234+
Ok(account)
235+
}
236+
237+
#[tauri::command]
238+
pub async fn add_windsurf_accounts_with_password(
239+
app: AppHandle,
240+
credentials: Vec<WindsurfPasswordCredentialInput>,
241+
) -> Result<WindsurfPasswordBatchResult, String> {
242+
if credentials.is_empty() {
243+
return Err("请先提供至少一组邮箱和密码".to_string());
244+
}
245+
246+
let started_at = Instant::now();
247+
logger::log_info(&format!(
248+
"[Windsurf Command] 批量邮箱密码登录开始: count={}",
249+
credentials.len()
250+
));
251+
252+
let mut accounts = Vec::new();
253+
let mut failures = Vec::new();
254+
255+
for item in credentials {
256+
let email = item.email.trim().to_string();
257+
let password = item.password;
258+
if email.is_empty() || password.is_empty() {
259+
failures.push(WindsurfPasswordCredentialFailure {
260+
email,
261+
error: "邮箱和密码不能为空".to_string(),
262+
source_line: item.source_line,
263+
});
264+
continue;
265+
}
266+
267+
match add_windsurf_account_from_password(&email, &password).await {
268+
Ok(account) => {
269+
logger::log_info(&format!(
270+
"[Windsurf Command] 批量邮箱密码登录成功: account_id={}, login={}",
271+
account.id, account.github_login
272+
));
273+
accounts.push(account);
274+
}
275+
Err(error) => {
276+
logger::log_warn(&format!(
277+
"[Windsurf Command] 批量邮箱密码登录失败: email={}, error={}",
278+
email, error
279+
));
280+
failures.push(WindsurfPasswordCredentialFailure {
281+
email,
282+
error,
283+
source_line: item.source_line,
284+
});
285+
}
286+
}
287+
}
288+
289+
if !accounts.is_empty() {
290+
let _ = crate::modules::tray::update_tray_menu(&app);
291+
}
292+
293+
let success_count = accounts.len();
294+
let failed_count = failures.len();
295+
if failed_count == 0 {
296+
logger::log_info(&format!(
297+
"[Windsurf Command] 批量邮箱密码登录完成: success={}, elapsed={}ms",
298+
success_count,
299+
started_at.elapsed().as_millis()
300+
));
301+
} else {
302+
logger::log_warn(&format!(
303+
"[Windsurf Command] 批量邮箱密码登录完成(部分失败): success={}, failed={}, elapsed={}ms",
304+
success_count,
305+
failed_count,
306+
started_at.elapsed().as_millis()
307+
));
308+
}
309+
310+
Ok(WindsurfPasswordBatchResult {
311+
accounts,
312+
success_count,
313+
failed_count,
314+
failures,
315+
})
316+
}
317+
189318
#[tauri::command]
190319
pub async fn update_windsurf_account_tags(
191320
account_id: String,

src-tauri/src/lib.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -351,6 +351,7 @@ pub fn run() {
351351
commands::codex::list_codex_accounts,
352352
commands::codex::get_current_codex_account,
353353
commands::codex::get_codex_config_toml_path,
354+
commands::codex::open_codex_config_toml,
354355
commands::codex::get_codex_quick_config,
355356
commands::codex::save_codex_quick_config,
356357
commands::codex::refresh_codex_account_profile,
@@ -430,6 +431,8 @@ pub fn run() {
430431
commands::windsurf::windsurf_oauth_submit_callback_url,
431432
commands::windsurf::windsurf_oauth_login_cancel,
432433
commands::windsurf::add_windsurf_account_with_token,
434+
commands::windsurf::add_windsurf_account_with_password,
435+
commands::windsurf::add_windsurf_accounts_with_password,
433436
commands::windsurf::update_windsurf_account_tags,
434437
commands::windsurf::get_windsurf_accounts_index_path,
435438
commands::windsurf::inject_windsurf_to_vscode,

src-tauri/src/modules/config.rs

Lines changed: 12 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1167,7 +1167,10 @@ pub fn load_user_config() -> Result<UserConfig, String> {
11671167
);
11681168
}
11691169
if !obj.contains_key("auto_backup_last_backup_at") {
1170-
obj.insert("auto_backup_last_backup_at".to_string(), serde_json::Value::Null);
1170+
obj.insert(
1171+
"auto_backup_last_backup_at".to_string(),
1172+
serde_json::Value::Null,
1173+
);
11711174
}
11721175

11731176
if !obj.contains_key("report_enabled") {
@@ -1450,16 +1453,14 @@ pub fn load_user_config() -> Result<UserConfig, String> {
14501453
config.auto_backup_include_config = include_config;
14511454
config.auto_backup_retention_days =
14521455
sanitize_auto_backup_retention_days(config.auto_backup_retention_days);
1453-
config.auto_backup_last_backup_at = config
1454-
.auto_backup_last_backup_at
1455-
.and_then(|value| {
1456-
let trimmed = value.trim().to_string();
1457-
if trimmed.is_empty() {
1458-
None
1459-
} else {
1460-
Some(trimmed)
1461-
}
1462-
});
1456+
config.auto_backup_last_backup_at = config.auto_backup_last_backup_at.and_then(|value| {
1457+
let trimmed = value.trim().to_string();
1458+
if trimmed.is_empty() {
1459+
None
1460+
} else {
1461+
Some(trimmed)
1462+
}
1463+
});
14631464

14641465
Ok(config)
14651466
}

0 commit comments

Comments
 (0)