From 2097df7175c03402aae2873952861a8de39b7140 Mon Sep 17 00:00:00 2001 From: yougeqiu Date: Fri, 25 Sep 2026 23:04:49 +0800 Subject: [PATCH] fix(devin): probe %APPDATA%\devin so Windows sessions are indexed by default MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit DevinAdapter::new() only probed XDG (~/.local/share/devin), the XDG env override and macOS' ~/Library/Application Support/devin. Devin Desktop on Windows keeps its sessions.db at %APPDATA%\devin\cli\sessions.db, so the default instance found no library and Devin sessions never showed up — only a manually added custom location worked around it. Add the AppData/Roaming candidate (derived from home_dir(), same convention as cursor_ide so WAKE_HOME redirection keeps working). The candidate is probed unconditionally like app_support, which keeps the XDG form first in the order and makes the resolution testable on every platform. --- crates/wake-core/src/adapters/devin.rs | 14 ++-- crates/wake-core/tests/devin_default_root.rs | 67 ++++++++++++++++++++ 2 files changed, 77 insertions(+), 4 deletions(-) create mode 100644 crates/wake-core/tests/devin_default_root.rs diff --git a/crates/wake-core/src/adapters/devin.rs b/crates/wake-core/src/adapters/devin.rs index a27392f..ba77c27 100644 --- a/crates/wake-core/src/adapters/devin.rs +++ b/crates/wake-core/src/adapters/devin.rs @@ -2,9 +2,11 @@ //! `<数据根>/cli/sessions.db` 全明文 SQLite。数据根服从 XDG—— //! `$XDG_DATA_HOME/devin`,缺省 `~/.local/share/devin`(macOS 同样走 XDG //! 形态,实测);`~/Library/Application Support/devin` 是桌面 Electron 壳的 -//! profile 目录,作为历史候选一并探。候选按序取第一个真有库文件的,都没有 -//! 时回落 XDG 语义下的默认位置(env 候选里真有库才采信,存在但空的目录 -//! 不能遮掉默认根——与其他 adapter 同一约定)。 +//! profile 目录,作为历史候选一并探;Windows 的 Devin Desktop 把库放在 +//! `%APPDATA%\devin`(从 `home_dir()` 派生,与 cursor_ide 同一约定:不走 +//! `dirs::config_dir()`,否则 `WAKE_HOME` 改道对它无效),同样探。候选按序取 +//! 第一个真有库文件的,都没有时回落 XDG 语义下的默认位置(env 候选里真有库 +//! 才采信,存在但空的目录不能遮掉默认根——与其他 adapter 同一约定)。 //! //! `sessions(id,title,working_directory,model,agent_mode,created_at, //! last_activity_at,hidden,main_chain_id,…)` + `message_nodes(row_id, @@ -108,9 +110,13 @@ impl DevinAdapter { .join("Library") .join("Application Support") .join("devin"); + // Windows 的 Devin Desktop: %APPDATA%\devin。Mac/Linux 上该目录不会 + // 存在,无条件作候选无害(与 app_support 同理),也让三平台的契约测试 + // 都能覆盖这条解析 + let appdata = home.join("AppData").join("Roaming").join("devin"); let hit = xdg .iter() - .chain([&local, &app_support]) + .chain([&local, &app_support, &appdata]) .find(|dir| db_in(dir).is_file()) .cloned(); let root = hit.unwrap_or_else(|| xdg.unwrap_or(local)); diff --git a/crates/wake-core/tests/devin_default_root.rs b/crates/wake-core/tests/devin_default_root.rs new file mode 100644 index 0000000..eaec6cb --- /dev/null +++ b/crates/wake-core/tests/devin_default_root.rs @@ -0,0 +1,67 @@ +//! Devin 默认数据根的解析契约。`DevinAdapter::new()` 的候选链 +//! (XDG → `~/.local/share` → macOS Application Support → Windows +//! `%APPDATA%`)依赖进程级 `WAKE_HOME`/`XDG_DATA_HOME`,因此整个文件只有 +//! 一个用例、串行覆盖两种布局(与 mcp_stdio/remote_sync 同一约定): +//! 1. 只有 `%APPDATA%\devin\cli\sessions.db`(真实 Windows 机器的形态)时, +//! 默认解析必须落到它——此前 Windows 上没有任何候选,整家会话空列; +//! 2. XDG 形态(`~/.local/share/devin`)同时存在时仍按序优先(跨平台回归)。 + +use std::fs; +use wake_core::adapters::devin::DevinAdapter; +use wake_core::adapters::AgentAdapter; + +mod common; + +#[test] +fn devin_default_root_resolves_windows_appdata() { + // 独立假家:进程级 env 之外不受 adapter_contracts 的共享 fixture 影响 + let home = tempfile::Builder::new() + .prefix("wake-devin-default-root-") + .tempdir() + .expect("create fake home"); + let appdata_dir = home + .path() + .join("AppData") + .join("Roaming") + .join("devin") + .join("cli"); + fs::create_dir_all(&appdata_dir).expect("mkdir AppData devin cli"); + let appdata_db = appdata_dir.join("sessions.db"); + common::build_devin_db(&appdata_db); + + // 老家目录清理 + 改道,先只给 AppData 一份(Windows 真机形态) + std::env::set_var("WAKE_HOME", home.path()); + std::env::set_var("HOME", home.path()); + std::env::remove_var("XDG_DATA_HOME"); + + let adapter = DevinAdapter::new(); + assert_eq!( + adapter.data_roots(), + vec![appdata_db.clone()], + "只有 %APPDATA%\\devin 一份库时,默认解析必须命中它" + ); + let ids: Vec = adapter + .list_session_files() + .expect("devin list") + .into_iter() + .map(|r| r.native_id) + .collect(); + assert_eq!(ids, vec!["dv-0001", "dv-0002"], "AppData 布局整家可列"); + + // XDG 形态出现后仍按候选序优先:老平台用户的会话不能换根消失 + let xdg_dir = home + .path() + .join(".local") + .join("share") + .join("devin") + .join("cli"); + fs::create_dir_all(&xdg_dir).expect("mkdir XDG devin cli"); + let xdg_db = xdg_dir.join("sessions.db"); + common::build_devin_db(&xdg_db); + let adapter = DevinAdapter::new(); + assert_eq!( + adapter.data_roots(), + vec![xdg_db], + "XDG 与 AppData 并存时按候选序取 XDG 形态" + ); +}