Wave 3: 补齐九个适配器(Qwen/CodeBuddy/Cline/Copilot/Crush/Goose/Aider/Cursor/Warp)
Co-authored-by: multica-agent <github@multica.ai>
This commit is contained in:
@@ -14,9 +14,10 @@ pub use error::ConfigError;
|
||||
use std::path::PathBuf;
|
||||
|
||||
/// 解析适配器声明的配置文件路径:展开 `~`(用户主目录),
|
||||
/// 以及平台变量(Windows `%VAR%` / `${VAR}`,如 `%APPDATA%`)。
|
||||
/// 并把 Windows 路径分隔符统一处理(YAML 里写的是 `~/.codex/...`)。
|
||||
pub fn resolve_path(raw: &str) -> PathBuf {
|
||||
let expanded = expand_home(raw);
|
||||
let expanded = expand_env_vars(&expand_home(raw));
|
||||
PathBuf::from(expanded)
|
||||
}
|
||||
|
||||
@@ -34,6 +35,65 @@ pub fn expand_home(raw: &str) -> String {
|
||||
raw.to_string()
|
||||
}
|
||||
|
||||
/// 展开平台环境变量:`%VAR%`(Windows)与 `${VAR}`。
|
||||
/// 未定义或非法变量名保持原样,不做静默吞并。
|
||||
pub fn expand_env_vars(raw: &str) -> String {
|
||||
// 先用 ${VAR} 处理(避免与 %VAR% 互相干扰)
|
||||
let mut out = String::with_capacity(raw.len());
|
||||
let bytes: Vec<char> = raw.chars().collect();
|
||||
let mut i = 0;
|
||||
while i < bytes.len() {
|
||||
if bytes[i] == '%' {
|
||||
if let Some((name, end)) = take_until(&bytes, i + 1, '%') {
|
||||
if is_valid_var_name(&name) {
|
||||
if let Ok(v) = std::env::var(&name) {
|
||||
out.push_str(&v);
|
||||
i = end + 1;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if bytes[i] == '$' && i + 1 < bytes.len() && bytes[i + 1] == '{' {
|
||||
if let Some((name, end)) = take_until(&bytes, i + 2, '}') {
|
||||
if is_valid_var_name(&name) {
|
||||
if let Ok(v) = std::env::var(&name) {
|
||||
out.push_str(&v);
|
||||
i = end + 1;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
out.push(bytes[i]);
|
||||
i += 1;
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// 从 `from` 开始找 `close`,返回 (变量名, close 下标)。找不到返回 None。
|
||||
fn take_until(chars: &[char], from: usize, close: char) -> Option<(String, usize)> {
|
||||
let mut end = from;
|
||||
while end < chars.len() {
|
||||
if chars[end] == close {
|
||||
let name: String = chars[from..end].iter().collect();
|
||||
return Some((name, end));
|
||||
}
|
||||
end += 1;
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
/// 环境变量名合法性:`[A-Za-z_][A-Za-z0-9_]*`(与常见 shell/OS 约定一致)。
|
||||
fn is_valid_var_name(name: &str) -> bool {
|
||||
let mut chars = name.chars();
|
||||
match chars.next() {
|
||||
Some(c) if c.is_ascii_alphabetic() || c == '_' => {}
|
||||
_ => return false,
|
||||
}
|
||||
chars.all(|c| c.is_ascii_alphanumeric() || c == '_')
|
||||
}
|
||||
|
||||
/// 用户主目录(含路径分隔符兜底)。
|
||||
pub fn home_dir() -> String {
|
||||
#[cfg(windows)]
|
||||
@@ -63,4 +123,20 @@ mod tests {
|
||||
fn plain_path_unchanged() {
|
||||
assert_eq!(expand_home("/etc/codex/config.toml"), "/etc/codex/config.toml");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn expand_env_vars_windows_and_braces() {
|
||||
std::env::set_var("AGENTDOCK_TEST_VAR", "C:\\Users\\Test");
|
||||
assert_eq!(
|
||||
expand_env_vars("%AGENTDOCK_TEST_VAR%\\block\\goose"),
|
||||
"C:\\Users\\Test\\block\\goose"
|
||||
);
|
||||
assert_eq!(
|
||||
expand_env_vars("${AGENTDOCK_TEST_VAR}/x"),
|
||||
"C:\\Users\\Test/x"
|
||||
);
|
||||
// 未定义变量保持原样
|
||||
assert_eq!(expand_env_vars("%AGENTDOCK_NOT_SET_VAR%\\x"), "%AGENTDOCK_NOT_SET_VAR%\\x");
|
||||
std::env::remove_var("AGENTDOCK_TEST_VAR");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1534,6 +1534,31 @@ configuration:
|
||||
}
|
||||
}
|
||||
|
||||
/// 真机检测记录:对 Wave 3 九个新工具逐项 detect 并打印结果(自测记录用)。
|
||||
/// 运行:cargo test -p agentdock-core real_machine_detect_wave3 -- --ignored --nocapture
|
||||
#[test]
|
||||
#[ignore]
|
||||
fn real_machine_detect_wave3() {
|
||||
let dir = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("../../adapters");
|
||||
let adapters = load_adapters(&dir).expect("适配器目录应可加载");
|
||||
let nine = ["qwen", "codebuddy", "cline", "copilot", "crush", "goose", "aider", "cursor", "warp"];
|
||||
for id in nine {
|
||||
let Some(adapter) = adapters.iter().find(|a| a.id == id) else {
|
||||
panic!("缺少 {id}");
|
||||
};
|
||||
let r = detect_one(adapter);
|
||||
println!(
|
||||
"{} ({}): status={} version={:?} exe={:?}",
|
||||
id, adapter.name_zh, r.status, r.version, r.executable
|
||||
);
|
||||
assert!([
|
||||
"installed", "not_installed", "not_in_path", "permission_denied",
|
||||
"exec_failed", "version_unparseable",
|
||||
]
|
||||
.contains(&r.status.as_str()));
|
||||
}
|
||||
}
|
||||
|
||||
/// 真机安装/卸载往返:gemini(npm 渠道,用户目录、可逆)。
|
||||
/// 运行:cargo test -p agentdock-core real_machine_install_uninstall_gemini -- --ignored --nocapture
|
||||
#[test]
|
||||
|
||||
@@ -22,6 +22,21 @@ pub fn runtime_for_prog(prog: &str) -> Option<&'static str> {
|
||||
}
|
||||
}
|
||||
|
||||
/// 运行时 id → 中文显示名(与前端 RuntimeInstallModal 的 RUNTIME_LABELS 对齐)。
|
||||
/// 「node」统一显示为「Node.js」,避免人话提示里出现 id 与按钮文案不一致。
|
||||
pub fn runtime_label_zh(id: &str) -> &'static str {
|
||||
match id {
|
||||
"node" => "Node.js",
|
||||
"python" => "Python",
|
||||
"git" => "Git",
|
||||
"winget" => "winget",
|
||||
"uv" => "uv",
|
||||
"choco" => "choco",
|
||||
"scoop" => "scoop",
|
||||
_ => "运行时",
|
||||
}
|
||||
}
|
||||
|
||||
/// 主入口:结合 spawn 错误与 stderr 尾部文本,产出结构化错误提示。
|
||||
/// `spawn_err` 为 `Command::spawn` 的 io::Error(None 表示进程已启动但退出非 0)。
|
||||
pub fn map_exec_error(prog: &str, spawn_err: Option<&std::io::Error>, stderr_tail: &str) -> ErrorHint {
|
||||
@@ -33,7 +48,7 @@ pub fn map_exec_error(prog: &str, spawn_err: Option<&std::io::Error>, stderr_tai
|
||||
ErrorKind::NotFound => {
|
||||
let missing = runtime_for_prog(prog);
|
||||
let friendly = match missing {
|
||||
Some(r) => format!("未找到 {prog} 命令:需要先安装 {r}(可在下方「本机环境」区一键安装)"),
|
||||
Some(r) => format!("未找到 {prog} 命令:需要先安装 {}(可在下方「本机环境」区一键安装)", runtime_label_zh(r)),
|
||||
None => format!("未找到 {prog} 命令:请确认 {prog} 已安装并加入 PATH"),
|
||||
};
|
||||
return ErrorHint {
|
||||
@@ -102,7 +117,17 @@ mod tests {
|
||||
let h = map_exec_error("npm", Some(¬_found()), "");
|
||||
assert_eq!(h.code, "program_not_found");
|
||||
assert_eq!(h.missing_runtime.as_deref(), Some("node"));
|
||||
assert!(h.friendly_zh.contains("Node") || h.friendly_zh.contains("node"));
|
||||
// 文案统一为「Node.js」(与按钮/运行时中文名一致,而非裸 id「node」)
|
||||
assert!(h.friendly_zh.contains("Node.js"), "人话提示应显示 Node.js:{}", h.friendly_zh);
|
||||
assert!(!h.friendly_zh.contains("安装 node"), "不应出现裸 id「node」:{}", h.friendly_zh);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn runtime_label_zh_maps_node() {
|
||||
assert_eq!(runtime_label_zh("node"), "Node.js");
|
||||
assert_eq!(runtime_label_zh("python"), "Python");
|
||||
assert_eq!(runtime_label_zh("git"), "Git");
|
||||
assert_eq!(runtime_label_zh("winget"), "winget");
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
Reference in New Issue
Block a user