feat(agents): add Antigravity IDE and CLI support
- Add separate configuration entries for IDE and CLI - Integrate Gemini-compatible routing and scoped credentials - Support detection, launch, backup, restore, and IDE restart - Add localized guidance and configuration, UI, and native integration tests
This commit is contained in:
1 parent
3545941414
commit
615188b390
23 files changed
+1115
-11
No files matched your search
@@ -100,12 +100,16 @@ OpenAI、Claude、Gemini、およびその他の互換形式の間で変換で
|
||||
- Pi(CLIProxyAPI provider 拡張機能)
|
||||
- ZCode
|
||||
- WorkBuddy / WorkBuddy AI
|
||||
- Antigravity IDE
|
||||
- Antigravity CLI
|
||||
- Kimi Code
|
||||
- Grok Build
|
||||
|
||||
対応クライアントでは、利用可能なモデルカタログの同期、デフォルトモデルの選択、管理設定を適用する前の
|
||||
元設定のバックアップ、以前の設定への復元、利用可能なデスクトップまたは CLI エントリーポイントの起動ができます。
|
||||
|
||||
Antigravity IDE と Antigravity CLI は設定ファイルが異なるため、個別のエントリーとして表示されます。どちらも CPA の Gemini 互換 API に接続します。IDE は設定の適用・解除後に再起動してください。IDE 内のモデル名表示は変わりませんが、リクエストには CPA で選択したモデルが使われます。CLI は CPA から起動すると、そのプロセスに接続先と API キーが渡されます。`agy` を直接実行する場合は環境変数を自分で設定してください。
|
||||
|
||||
## その他の機能
|
||||
|
||||
- コア設定、API Key、リモート管理用認証情報、ルーティング戦略の管理。
|
||||
|
||||
@@ -118,6 +118,8 @@ proxy. Supported clients include:
|
||||
- Pi (with the CLIProxyAPI provider extension)
|
||||
- ZCode
|
||||
- WorkBuddy / WorkBuddy AI
|
||||
- Antigravity IDE
|
||||
- Antigravity CLI
|
||||
- Kimi Code
|
||||
- Grok Build
|
||||
|
||||
@@ -125,6 +127,8 @@ For supported clients, the application can synchronize the available model catal
|
||||
default model, back up the original configuration before applying managed settings, and restore the
|
||||
previous configuration.
|
||||
|
||||
Antigravity IDE and Antigravity CLI have separate entries because they use different configuration files. Both connect through CPA's Gemini-compatible API. Restart the IDE after applying or disabling settings; its model labels stay unchanged, while requests use the model selected in CPA. Launch the CLI from CPA to supply its endpoint and API key for that process. Running `agy` directly requires setting those environment variables yourself.
|
||||
|
||||
## Additional Capabilities
|
||||
|
||||
- Manage core settings, API keys, remote management credentials, and routing strategy.
|
||||
|
||||
@@ -109,6 +109,8 @@ API 接入页面按照协议或 Provider 管理上游 API 凭证和服务地址
|
||||
- Pi(通过 CLIProxyAPI provider 插件)
|
||||
- ZCode
|
||||
- WorkBuddy / WorkBuddy AI
|
||||
- Antigravity IDE
|
||||
- Antigravity CLI
|
||||
- Kimi Code
|
||||
- Grok Build
|
||||
|
||||
@@ -119,6 +121,8 @@ WorkBuddy 接入沿用模型选择、更新配置、关闭配置修改、手动
|
||||
支持 WorkBuddy 与 WorkBuddy AI 的 `models.json` 对象及数组格式,并保留已有自定义模型。
|
||||
应用后需在 WorkBuddy 内选择 `CPA:模型名`;已有会话不会自动切换模型,未显示时可重启 App。
|
||||
|
||||
Antigravity IDE 与 Antigravity CLI 使用不同的配置文件,因此提供独立入口,均通过 CPA 的 Gemini 兼容接口连接。IDE 应用或关闭配置后需要重启;IDE 内的模型标签保持不变,实际请求使用 CPA 中选定的模型。CLI 请从 CPA 启动,以便为该进程注入接口地址和密钥;直接运行 `agy` 时需自行设置环境变量。
|
||||
|
||||
## 其他功能
|
||||
|
||||
- 管理内核配置、API Key、远程管理凭证和路由策略。
|
||||
|
||||
@@ -58,6 +58,7 @@ windows-sys = { version = "0.59", features = [
|
||||
"Win32_Storage_FileSystem",
|
||||
"Win32_System_Diagnostics_ToolHelp",
|
||||
"Win32_System_Environment",
|
||||
"Win32_System_Console",
|
||||
"Win32_System_JobObjects",
|
||||
"Win32_System_Registry",
|
||||
"Win32_System_Threading",
|
||||
|
||||
@@ -11,6 +11,7 @@ mod state;
|
||||
mod templates;
|
||||
mod transactions;
|
||||
mod workbuddy;
|
||||
mod antigravity;
|
||||
#[cfg(target_os = "windows")]
|
||||
mod windows_probe;
|
||||
pub(crate) use backups::*;
|
||||
@@ -24,5 +25,6 @@ pub(crate) use state::*;
|
||||
pub(crate) use templates::*;
|
||||
pub(crate) use transactions::*;
|
||||
pub(crate) use workbuddy::*;
|
||||
pub(crate) use antigravity::*;
|
||||
#[cfg(target_os = "windows")]
|
||||
pub(crate) use windows_probe::*;
|
||||
@@ -0,0 +1,539 @@
|
||||
use super::*;
|
||||
use serde_json::{json, Value};
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
|
||||
pub(crate) const ANTIGRAVITY_CONNECTION_FILE: &str = "cpa-connection.json";
|
||||
const CLI_MODEL_LABEL: &str = "CPA (EasyCLIProxyAPI)";
|
||||
const IDE_BINARY_KEY: &str = "codeiumDev.languageServerBinaryPath";
|
||||
const IDE_ENV_KEY: &str = "codeiumDev.languageServerEnv";
|
||||
const IDE_SERVER_ENV: &str = "CPA_ANTIGRAVITY_LANGUAGE_SERVER";
|
||||
const IDE_MODEL_ENV: &str = "CPA_ANTIGRAVITY_MODEL";
|
||||
|
||||
pub(crate) fn antigravity_config_paths(client: AgentClient, home: &Path) -> Vec<PathBuf> {
|
||||
if client == AgentClient::AntigravityCli {
|
||||
let directory = home.join(".gemini/antigravity-cli");
|
||||
return vec![
|
||||
directory.join("settings.json"),
|
||||
directory.join(ANTIGRAVITY_CONNECTION_FILE),
|
||||
];
|
||||
}
|
||||
#[cfg(target_os = "windows")]
|
||||
let directory =
|
||||
agent_configuration_environment("APPDATA").unwrap_or_else(|| home.join("AppData/Roaming"));
|
||||
#[cfg(target_os = "macos")]
|
||||
let directory = home.join("Library/Application Support");
|
||||
#[cfg(not(any(target_os = "windows", target_os = "macos")))]
|
||||
let directory = agent_configuration_environment("XDG_CONFIG_HOME")
|
||||
.filter(|path| path.is_absolute())
|
||||
.unwrap_or_else(|| home.join(".config"));
|
||||
let current = directory.join("Antigravity IDE/User/settings.json");
|
||||
let legacy = directory.join("Antigravity/User/settings.json");
|
||||
vec![if !current.exists() && legacy.exists() {
|
||||
legacy
|
||||
} else {
|
||||
current
|
||||
}]
|
||||
}
|
||||
|
||||
pub(crate) fn find_antigravity_cli(home: &Path) -> Option<PathBuf> {
|
||||
#[cfg(target_os = "windows")]
|
||||
let native = agent_configuration_environment("LOCALAPPDATA")
|
||||
.unwrap_or_else(|| home.join("AppData/Local"))
|
||||
.join("agy/bin/agy.exe");
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
let native = home.join(".local/bin/agy");
|
||||
if native.is_file() {
|
||||
Some(native)
|
||||
} else {
|
||||
find_named_agent_executable(home, &["agy"])
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn find_antigravity_ide(home: &Path) -> Option<PathBuf> {
|
||||
let mut candidates = Vec::new();
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
let local = agent_configuration_environment("LOCALAPPDATA")
|
||||
.unwrap_or_else(|| home.join("AppData/Local"));
|
||||
let mut roots = vec![local.join("Programs")];
|
||||
for name in ["ProgramFiles", "ProgramFiles(x86)"] {
|
||||
if let Some(root) = agent_configuration_environment(name) {
|
||||
roots.push(root);
|
||||
}
|
||||
}
|
||||
for root in roots {
|
||||
candidates.push(root.join("Antigravity IDE/Antigravity IDE.exe"));
|
||||
candidates.push(root.join("Antigravity/Antigravity.exe"));
|
||||
}
|
||||
}
|
||||
#[cfg(target_os = "macos")]
|
||||
for root in [PathBuf::from("/Applications"), home.join("Applications")] {
|
||||
for name in ["Antigravity IDE", "Antigravity"] {
|
||||
candidates.push(root.join(format!("{name}.app/Contents/MacOS/Electron")));
|
||||
}
|
||||
}
|
||||
#[cfg(not(any(target_os = "windows", target_os = "macos")))]
|
||||
{
|
||||
candidates.extend([
|
||||
PathBuf::from("/usr/share/antigravity-ide/antigravity-ide"),
|
||||
PathBuf::from("/opt/antigravity-ide/antigravity-ide"),
|
||||
PathBuf::from("/usr/share/antigravity/antigravity"),
|
||||
home.join(".local/share/antigravity-ide/antigravity-ide"),
|
||||
]);
|
||||
}
|
||||
let found = candidates.into_iter().find(|p| p.is_file());
|
||||
#[cfg(all(target_os = "windows", not(test)))]
|
||||
let found = found.or_else(find_windows_registered_antigravity_ide);
|
||||
found
|
||||
}
|
||||
|
||||
fn antigravity_resources(executable: &Path) -> Option<PathBuf> {
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
Some(executable.parent()?.parent()?.join("Resources/app"))
|
||||
}
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
{
|
||||
Some(executable.parent()?.join("resources/app"))
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn read_antigravity_ide_version(executable: &Path) -> Option<String> {
|
||||
let value: Value = serde_json::from_str(
|
||||
&fs::read_to_string(antigravity_resources(executable)?.join("product.json")).ok()?,
|
||||
)
|
||||
.ok()?;
|
||||
value
|
||||
.get("ideVersion")
|
||||
.and_then(Value::as_str)
|
||||
.map(str::to_string)
|
||||
}
|
||||
|
||||
fn antigravity_language_server(executable: &Path) -> Option<PathBuf> {
|
||||
let platform = if cfg!(target_os = "windows") {
|
||||
"windows"
|
||||
} else if cfg!(target_os = "macos") {
|
||||
"macos"
|
||||
} else {
|
||||
"linux"
|
||||
};
|
||||
let arch = if cfg!(target_arch = "aarch64") {
|
||||
"arm"
|
||||
} else {
|
||||
"x64"
|
||||
};
|
||||
let suffix = if cfg!(target_os = "windows") {
|
||||
".exe"
|
||||
} else {
|
||||
""
|
||||
};
|
||||
let server = antigravity_resources(executable)?.join(format!(
|
||||
"extensions/antigravity/bin/language_server_{platform}_{arch}{suffix}"
|
||||
));
|
||||
server.is_file().then_some(server)
|
||||
}
|
||||
|
||||
fn object_at<'a>(root: &'a mut Value, keys: &[&str]) -> Result<&'a mut Value, String> {
|
||||
let mut value = root;
|
||||
for key in keys {
|
||||
if !value.is_object() {
|
||||
return Err("Antigravity 配置字段必须是对象".into());
|
||||
}
|
||||
value = value
|
||||
.as_object_mut()
|
||||
.unwrap()
|
||||
.entry((*key).to_string())
|
||||
.or_insert_with(|| json!({}));
|
||||
}
|
||||
if !value.is_object() {
|
||||
return Err("Antigravity 配置字段必须是对象".into());
|
||||
}
|
||||
Ok(value)
|
||||
}
|
||||
|
||||
pub(crate) fn build_antigravity_config(
|
||||
client: AgentClient,
|
||||
path: &Path,
|
||||
existing: Option<&str>,
|
||||
origin: &str,
|
||||
api_key: &str,
|
||||
model: &str,
|
||||
server: Option<&Path>,
|
||||
) -> Result<String, String> {
|
||||
let mut root = parse(path, existing)?;
|
||||
if !root.is_object() {
|
||||
return Err("Antigravity 配置根节点必须是对象".into());
|
||||
}
|
||||
if client == AgentClient::AntigravityIde {
|
||||
let server = server.ok_or("未找到 Antigravity IDE 语言服务,请安装 IDE 后重新检测")?;
|
||||
let helper = env::current_exe().map_err(|_| "无法定位 CPA 启动适配程序")?;
|
||||
let environment = object_at(&mut root, &[IDE_ENV_KEY])?;
|
||||
environment[IDE_SERVER_ENV] = json!(server);
|
||||
environment[IDE_MODEL_ENV] = json!(model);
|
||||
environment["GEMINI_API_KEY"] = json!(api_key);
|
||||
environment["GOOGLE_GEMINI_BASE_URL"] = json!(origin);
|
||||
root[IDE_BINARY_KEY] = json!(helper);
|
||||
} else if path
|
||||
.file_name()
|
||||
.is_some_and(|n| n == ANTIGRAVITY_CONNECTION_FILE)
|
||||
{
|
||||
root["provider"] = json!(MANAGED_AGENT_PROVIDER_ID);
|
||||
root["baseUrl"] = json!(origin);
|
||||
root["apiKey"] = json!(api_key);
|
||||
root["model"] = json!(model);
|
||||
} else {
|
||||
let entry = object_at(
|
||||
&mut root,
|
||||
&["customModelsConfig", "customModels", CLI_MODEL_LABEL],
|
||||
)?;
|
||||
entry["modelName"] = json!(model);
|
||||
root["modelProvider"] = json!("gemini");
|
||||
}
|
||||
render(path, &root)
|
||||
}
|
||||
|
||||
pub(crate) fn build_antigravity_updates(
|
||||
client: AgentClient,
|
||||
home: &Path,
|
||||
origin: &str,
|
||||
api_key: &str,
|
||||
model: &str,
|
||||
template: bool,
|
||||
) -> Result<Vec<AgentFileUpdate>, String> {
|
||||
let server = if client == AgentClient::AntigravityIde {
|
||||
find_antigravity_ide(home)
|
||||
.as_deref()
|
||||
.and_then(antigravity_language_server)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
antigravity_config_paths(client, home)
|
||||
.into_iter()
|
||||
.map(|path| {
|
||||
let before = if template {
|
||||
None
|
||||
} else {
|
||||
read_optional_text(&path)?
|
||||
};
|
||||
let after = build_antigravity_config(
|
||||
client,
|
||||
&path,
|
||||
before.as_deref(),
|
||||
origin,
|
||||
api_key,
|
||||
model,
|
||||
server.as_deref(),
|
||||
)?;
|
||||
Ok(AgentFileUpdate { path, after })
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn managed_fields(client: AgentClient, path: &Path) -> Vec<Vec<&'static str>> {
|
||||
if client == AgentClient::AntigravityIde {
|
||||
vec![
|
||||
vec![IDE_BINARY_KEY],
|
||||
vec![IDE_ENV_KEY, IDE_SERVER_ENV],
|
||||
vec![IDE_ENV_KEY, IDE_MODEL_ENV],
|
||||
vec![IDE_ENV_KEY, "GEMINI_API_KEY"],
|
||||
vec![IDE_ENV_KEY, "GOOGLE_GEMINI_BASE_URL"],
|
||||
]
|
||||
} else if path
|
||||
.file_name()
|
||||
.is_some_and(|n| n == ANTIGRAVITY_CONNECTION_FILE)
|
||||
{
|
||||
["provider", "baseUrl", "apiKey", "model"]
|
||||
.into_iter()
|
||||
.map(|k| vec![k])
|
||||
.collect()
|
||||
} else {
|
||||
vec![
|
||||
vec!["modelProvider"],
|
||||
vec![
|
||||
"customModelsConfig",
|
||||
"customModels",
|
||||
CLI_MODEL_LABEL,
|
||||
"modelName",
|
||||
],
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
fn field<'a>(root: &'a Value, keys: &[&str]) -> Option<&'a Value> {
|
||||
keys.iter().try_fold(root, |v, k| v.get(*k))
|
||||
}
|
||||
|
||||
fn replace_field(root: &mut Value, keys: &[&str], value: Option<&Value>) {
|
||||
let Some(map) = root.as_object_mut() else {
|
||||
return;
|
||||
};
|
||||
if keys.len() == 1 {
|
||||
if let Some(value) = value {
|
||||
map.insert(keys[0].into(), value.clone());
|
||||
} else {
|
||||
map.remove(keys[0]);
|
||||
}
|
||||
} else {
|
||||
if value.is_some() {
|
||||
map.entry(keys[0]).or_insert_with(|| json!({}));
|
||||
}
|
||||
if let Some(child) = map.get_mut(keys[0]) {
|
||||
replace_field(child, &keys[1..], value);
|
||||
if child.as_object().is_some_and(|m| m.is_empty()) {
|
||||
map.remove(keys[0]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn restore_antigravity_config(
|
||||
client: AgentClient,
|
||||
path: &Path,
|
||||
current: &str,
|
||||
original: Option<&str>,
|
||||
) -> Result<Option<String>, String> {
|
||||
let mut root = parse(path, Some(current))?;
|
||||
let before = parse(path, original)?;
|
||||
for keys in managed_fields(client, path) {
|
||||
replace_field(&mut root, &keys, field(&before, &keys));
|
||||
}
|
||||
if original.is_none() && root.as_object().is_some_and(|m| m.is_empty()) {
|
||||
Ok(None)
|
||||
} else {
|
||||
render(path, &root).map(Some)
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn antigravity_has_marker(
|
||||
client: AgentClient,
|
||||
paths: &[PathBuf],
|
||||
) -> Result<bool, String> {
|
||||
let path = if client == AgentClient::AntigravityIde {
|
||||
&paths[0]
|
||||
} else {
|
||||
paths.get(1).ok_or("Antigravity CLI 配置路径缺失")?
|
||||
};
|
||||
let root = parse(path, read_optional_text(path)?.as_deref())?;
|
||||
Ok(if client == AgentClient::AntigravityIde {
|
||||
field(&root, &[IDE_ENV_KEY, IDE_SERVER_ENV]).is_some()
|
||||
} else {
|
||||
root.get("provider").and_then(Value::as_str) == Some(MANAGED_AGENT_PROVIDER_ID)
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn inspect_antigravity_config(
|
||||
client: AgentClient,
|
||||
paths: &[PathBuf],
|
||||
port: u16,
|
||||
api_key: &str,
|
||||
) -> Result<(bool, Option<String>), String> {
|
||||
let settings = parse(&paths[0], read_optional_text(&paths[0])?.as_deref())?;
|
||||
let origin = managed_core_loopback_origin(port);
|
||||
if client == AgentClient::AntigravityIde {
|
||||
let environment = settings.get(IDE_ENV_KEY).unwrap_or(&Value::Null);
|
||||
let model = environment
|
||||
.get(IDE_MODEL_ENV)
|
||||
.and_then(Value::as_str)
|
||||
.filter(|s| !s.is_empty())
|
||||
.map(str::to_string);
|
||||
let helper = env::current_exe().ok();
|
||||
let configured = model.is_some()
|
||||
&& settings
|
||||
.get(IDE_BINARY_KEY)
|
||||
.and_then(Value::as_str)
|
||||
.map(Path::new)
|
||||
== helper.as_deref()
|
||||
&& environment
|
||||
.get(IDE_SERVER_ENV)
|
||||
.and_then(Value::as_str)
|
||||
.is_some_and(|p| Path::new(p).is_file())
|
||||
&& environment.get("GEMINI_API_KEY").and_then(Value::as_str) == Some(api_key)
|
||||
&& environment
|
||||
.get("GOOGLE_GEMINI_BASE_URL")
|
||||
.and_then(Value::as_str)
|
||||
== Some(&origin);
|
||||
Ok((configured, model))
|
||||
} else {
|
||||
let path = paths.get(1).ok_or("Antigravity CLI 配置路径缺失")?;
|
||||
let connection = parse(path, read_optional_text(path)?.as_deref())?;
|
||||
let model = connection
|
||||
.get("model")
|
||||
.and_then(Value::as_str)
|
||||
.filter(|s| !s.is_empty())
|
||||
.map(str::to_string);
|
||||
let configured = model.is_some()
|
||||
&& settings.get("modelProvider").and_then(Value::as_str) == Some("gemini")
|
||||
&& field(
|
||||
&settings,
|
||||
&[
|
||||
"customModelsConfig",
|
||||
"customModels",
|
||||
CLI_MODEL_LABEL,
|
||||
"modelName",
|
||||
],
|
||||
)
|
||||
.and_then(Value::as_str)
|
||||
== model.as_deref()
|
||||
&& connection.get("provider").and_then(Value::as_str)
|
||||
== Some(MANAGED_AGENT_PROVIDER_ID)
|
||||
&& connection.get("baseUrl").and_then(Value::as_str) == Some(&origin)
|
||||
&& connection.get("apiKey").and_then(Value::as_str) == Some(api_key);
|
||||
Ok((configured, model))
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn prepare_antigravity_removal(
|
||||
client: AgentClient,
|
||||
paths: &[PathBuf],
|
||||
) -> Result<Images, String> {
|
||||
if !antigravity_has_marker(client, paths)? {
|
||||
return Ok(Vec::new());
|
||||
}
|
||||
paths
|
||||
.iter()
|
||||
.map(|path| {
|
||||
let value = read_optional_text(path)?
|
||||
.map(|current| restore_antigravity_config(client, path, ¤t, None))
|
||||
.transpose()?
|
||||
.flatten();
|
||||
Ok((path.clone(), value.map(String::into_bytes)))
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub(crate) fn antigravity_cli_helper_arguments(home: &Path) -> Vec<String> {
|
||||
vec!["--cpa-antigravity-cli".into(), path_to_string(home)]
|
||||
}
|
||||
|
||||
pub(crate) fn antigravity_helper_requested(args: &[std::ffi::OsString]) -> bool {
|
||||
args.get(1).is_some_and(|a| a == "--cpa-antigravity-cli")
|
||||
|| (env::var_os(IDE_SERVER_ENV).is_some()
|
||||
&& args.iter().skip(1).any(|a| {
|
||||
a == "--extension_server_port"
|
||||
|| a.to_string_lossy().starts_with("--extension_server_port=")
|
||||
}))
|
||||
}
|
||||
|
||||
fn antigravity_helper_command(args: &[std::ffi::OsString]) -> Result<Command, String> {
|
||||
let cli = args.get(1).is_some_and(|a| a == "--cpa-antigravity-cli");
|
||||
let mut command = if cli {
|
||||
let home = args
|
||||
.get(2)
|
||||
.map(PathBuf::from)
|
||||
.ok_or("Antigravity CLI 启动参数缺少用户目录")?;
|
||||
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());
|
||||
}
|
||||
let key = connection
|
||||
.get("apiKey")
|
||||
.and_then(Value::as_str)
|
||||
.filter(|s| !s.is_empty())
|
||||
.ok_or("Antigravity CLI 密钥缺失")?;
|
||||
let url = connection
|
||||
.get("baseUrl")
|
||||
.and_then(Value::as_str)
|
||||
.filter(|s| !s.trim().is_empty())
|
||||
.ok_or("Antigravity CLI 地址缺失")?;
|
||||
let model = connection
|
||||
.get("model")
|
||||
.and_then(Value::as_str)
|
||||
.filter(|s| !s.trim().is_empty())
|
||||
.ok_or("Antigravity CLI 模型缺失")?;
|
||||
let settings = parse(&paths[0], read_optional_text(&paths[0])?.as_deref())?;
|
||||
if settings.get("modelProvider").and_then(Value::as_str) != Some("gemini")
|
||||
|| field(
|
||||
&settings,
|
||||
&[
|
||||
"customModelsConfig",
|
||||
"customModels",
|
||||
CLI_MODEL_LABEL,
|
||||
"modelName",
|
||||
],
|
||||
)
|
||||
.and_then(Value::as_str)
|
||||
!= Some(model)
|
||||
{
|
||||
return Err("Antigravity CLI 配置已变化,请重新应用配置".into());
|
||||
}
|
||||
let executable = find_antigravity_cli(&home).ok_or("未找到 Antigravity CLI")?;
|
||||
let mut command = Command::new(executable);
|
||||
command
|
||||
.arg(format!("--gemini_dir={}", home.join(".gemini").display()))
|
||||
.arg(format!("--model={CLI_MODEL_LABEL}"))
|
||||
.args(args.iter().skip(3))
|
||||
.env("GEMINI_API_KEY", key)
|
||||
.env("GOOGLE_GEMINI_BASE_URL", url);
|
||||
command
|
||||
} else {
|
||||
let executable = env::var_os(IDE_SERVER_ENV)
|
||||
.map(PathBuf::from)
|
||||
.ok_or("Antigravity IDE 语言服务路径缺失")?;
|
||||
if Some(&executable) == env::current_exe().ok().as_ref() {
|
||||
return Err("Antigravity IDE 语言服务路径不能指向 CPA".into());
|
||||
}
|
||||
let model = env::var(IDE_MODEL_ENV).map_err(|_| "Antigravity IDE 模型缺失")?;
|
||||
if model.trim().is_empty() {
|
||||
return Err("Antigravity IDE 模型缺失".into());
|
||||
}
|
||||
let mut command = Command::new(executable);
|
||||
command
|
||||
.args(args.iter().skip(1))
|
||||
.arg("--model_api_client_type=gemini")
|
||||
.arg(format!("--override_model_name={model}"));
|
||||
command
|
||||
};
|
||||
command
|
||||
.env_remove("GOOGLE_API_KEY")
|
||||
.env_remove(IDE_SERVER_ENV)
|
||||
.env_remove(IDE_MODEL_ENV);
|
||||
Ok(command)
|
||||
}
|
||||
|
||||
pub(crate) fn run_antigravity_helper(args: &[std::ffi::OsString]) -> Result<i32, String> {
|
||||
let mut command = antigravity_helper_command(args)?;
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
use std::os::windows::process::CommandExt;
|
||||
if args.get(1).is_some_and(|a| a == "--cpa-antigravity-cli") {
|
||||
unsafe {
|
||||
windows_sys::Win32::System::Console::AttachConsole(u32::MAX);
|
||||
}
|
||||
} else {
|
||||
command.creation_flags(0x0800_0000);
|
||||
}
|
||||
}
|
||||
#[cfg(unix)]
|
||||
{
|
||||
use std::os::unix::process::CommandExt;
|
||||
return Err(format!("启动 Antigravity 失败: {}", command.exec()));
|
||||
}
|
||||
#[cfg(not(unix))]
|
||||
{
|
||||
let mut child = command
|
||||
.spawn()
|
||||
.map_err(|error| format!("启动 Antigravity 失败: {error}"))?;
|
||||
#[cfg(target_os = "windows")]
|
||||
let job = match attach_child_to_windows_job(&child) {
|
||||
Ok(job) => job,
|
||||
Err(error) => {
|
||||
let _ = child.kill();
|
||||
let _ = child.wait();
|
||||
return Err(error);
|
||||
}
|
||||
};
|
||||
let result = child.wait();
|
||||
#[cfg(target_os = "windows")]
|
||||
close_windows_handle(job);
|
||||
if result.is_err() {
|
||||
let _ = child.kill();
|
||||
let _ = child.wait();
|
||||
}
|
||||
result
|
||||
.map(|s| s.code().unwrap_or(1))
|
||||
.map_err(|error| format!("等待 Antigravity 退出失败: {error}"))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,338 @@
|
||||
use super::*;
|
||||
|
||||
struct Home(PathBuf);
|
||||
impl Home {
|
||||
fn new() -> Self {
|
||||
let path = env::temp_dir().join(format!(
|
||||
"cpa-antigravity-test-{}-{}",
|
||||
std::process::id(),
|
||||
SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.unwrap()
|
||||
.as_nanos()
|
||||
));
|
||||
fs::create_dir_all(&path).unwrap();
|
||||
Self(path)
|
||||
}
|
||||
fn save(&self, path: &Path, value: &Value) {
|
||||
assert!(path.starts_with(&self.0));
|
||||
fs::create_dir_all(path.parent().unwrap()).unwrap();
|
||||
fs::write(path, serde_json::to_vec(value).unwrap()).unwrap();
|
||||
}
|
||||
fn apply(&self, client: AgentClient, model: &str) {
|
||||
let paths = config_paths(client.id(), &self.0).unwrap();
|
||||
let before = config_images(&paths).unwrap();
|
||||
let server = self.0.join("language_server.exe");
|
||||
fs::write(&server, b"fixture").unwrap();
|
||||
let updates = paths
|
||||
.iter()
|
||||
.map(|path| AgentFileUpdate {
|
||||
path: path.clone(),
|
||||
after: build_antigravity_config(
|
||||
client,
|
||||
path,
|
||||
read_optional_text(path).unwrap().as_deref(),
|
||||
"http://127.0.0.1:8317",
|
||||
"test-key",
|
||||
model,
|
||||
Some(&server),
|
||||
)
|
||||
.unwrap(),
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
config_updates(
|
||||
client.id(),
|
||||
&self.0,
|
||||
&before,
|
||||
&updates,
|
||||
"update",
|
||||
Some(model.into()),
|
||||
None,
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
inspect_antigravity_config(client, &paths, 8317, "test-key").unwrap(),
|
||||
(true, Some(model.into()))
|
||||
);
|
||||
}
|
||||
}
|
||||
impl Drop for Home {
|
||||
fn drop(&mut self) {
|
||||
let _ = fs::remove_dir_all(&self.0);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn antigravity_clients_have_independent_paths_and_launch_targets() {
|
||||
let home = Home::new();
|
||||
let ide = antigravity_config_paths(AgentClient::AntigravityIde, &home.0);
|
||||
let cli = antigravity_config_paths(AgentClient::AntigravityCli, &home.0);
|
||||
assert!(ide
|
||||
.iter()
|
||||
.all(|p| !cli.contains(p) && p.starts_with(&home.0)));
|
||||
assert_eq!(cli[0], home.0.join(".gemini/antigravity-cli/settings.json"));
|
||||
for (client, target) in [
|
||||
(AgentClient::AntigravityIde, "app"),
|
||||
(AgentClient::AntigravityCli, "cli"),
|
||||
] {
|
||||
let targets = agent_launch_targets(client, Some(Path::new("fixture")), None, false);
|
||||
assert_eq!(targets.len(), 1);
|
||||
assert_eq!(targets[0].id, target);
|
||||
}
|
||||
assert!(!should_probe_primary_agent_executable_version(
|
||||
AgentClient::AntigravityIde
|
||||
));
|
||||
assert!(should_probe_primary_agent_executable_version(
|
||||
AgentClient::AntigravityCli
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn antigravity_updates_and_disable_preserve_external_settings() {
|
||||
for client in [AgentClient::AntigravityIde, AgentClient::AntigravityCli] {
|
||||
let home = Home::new();
|
||||
let paths = config_paths(client.id(), &home.0).unwrap();
|
||||
let original = if client == AgentClient::AntigravityIde {
|
||||
json!({"editor.fontSize": 15, IDE_BINARY_KEY: "original-server", IDE_ENV_KEY: {"KEEP":"original", "GEMINI_API_KEY":"original-key"}})
|
||||
} else {
|
||||
json!({"modelProvider":"original", "colorScheme":"dark", "customModelsConfig":{"customModels":{"User Model":{"modelName":"user"},CLI_MODEL_LABEL:{"modelName":"original", "maxTokens":123}}}})
|
||||
};
|
||||
home.save(&paths[0], &original);
|
||||
home.apply(client, "model-one");
|
||||
let applied = config_images(&paths).unwrap();
|
||||
home.apply(client, "model-one");
|
||||
assert_eq!(config_images(&paths).unwrap(), applied);
|
||||
let mut current =
|
||||
parse(&paths[0], read_optional_text(&paths[0]).unwrap().as_deref()).unwrap();
|
||||
current["user-added"] = json!({"keep":[1,2]});
|
||||
home.save(&paths[0], ¤t);
|
||||
home.apply(client, "model-two");
|
||||
assert!(
|
||||
!inspect_antigravity_config(client, &paths, 8318, "test-key")
|
||||
.unwrap()
|
||||
.0
|
||||
);
|
||||
assert!(
|
||||
!inspect_antigravity_config(client, &paths, 8317, "other-key")
|
||||
.unwrap()
|
||||
.0
|
||||
);
|
||||
clear_agent_managed_configuration(client, &home.0, 8317).unwrap();
|
||||
let restored = parse(&paths[0], read_optional_text(&paths[0]).unwrap().as_deref()).unwrap();
|
||||
let mut expected = original;
|
||||
expected["user-added"] = json!({"keep":[1,2]});
|
||||
assert_eq!(restored, expected);
|
||||
if client == AgentClient::AntigravityCli {
|
||||
assert!(!paths[1].exists());
|
||||
}
|
||||
assert!(!antigravity_has_marker(client, &paths).unwrap());
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn antigravity_manual_backup_restores_only_its_client() {
|
||||
let home = Home::new();
|
||||
for client in [AgentClient::AntigravityIde, AgentClient::AntigravityCli] {
|
||||
home.apply(client, "model-one");
|
||||
}
|
||||
let ide_paths = config_paths("antigravity-ide", &home.0).unwrap();
|
||||
let cli_paths = config_paths("antigravity-cli", &home.0).unwrap();
|
||||
let ide = config_images(&ide_paths).unwrap();
|
||||
let cli = config_images(&cli_paths).unwrap();
|
||||
let backup = create_backup("antigravity-cli", &home.0).unwrap();
|
||||
home.apply(AgentClient::AntigravityCli, "model-two");
|
||||
test_restore_backup(AgentClient::AntigravityCli, &home.0, &backup.id);
|
||||
assert_eq!(config_images(&cli_paths).unwrap(), cli);
|
||||
clear_agent_managed_configuration(AgentClient::AntigravityCli, &home.0, 8317).unwrap();
|
||||
assert_eq!(config_images(&ide_paths).unwrap(), ide);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn antigravity_rejects_malformed_owned_containers() {
|
||||
let path = Path::new("settings.json");
|
||||
for (client, input) in [
|
||||
(
|
||||
AgentClient::AntigravityCli,
|
||||
json!({"customModelsConfig":[]}),
|
||||
),
|
||||
(
|
||||
AgentClient::AntigravityCli,
|
||||
json!({"customModelsConfig":{"customModels":false}}),
|
||||
),
|
||||
(AgentClient::AntigravityIde, json!({IDE_ENV_KEY:"invalid"})),
|
||||
] {
|
||||
assert!(build_antigravity_config(
|
||||
client,
|
||||
path,
|
||||
Some(&input.to_string()),
|
||||
"http://127.0.0.1:8317",
|
||||
"key",
|
||||
"model",
|
||||
Some(Path::new("server"))
|
||||
)
|
||||
.is_err());
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn antigravity_cli_template_covers_settings_and_credentials() {
|
||||
let home = Home::new();
|
||||
let updates = build_agent_template_updates(AgentDefaultConfiguration {
|
||||
client: AgentClient::AntigravityCli,
|
||||
home: &home.0,
|
||||
port: 8317,
|
||||
api_key: "key",
|
||||
model: "model",
|
||||
models: &[],
|
||||
codex_catalog: None,
|
||||
oauth_configuration: false,
|
||||
claude_code_model_mappings: None,
|
||||
claude_desktop_model_mappings: None,
|
||||
})
|
||||
.unwrap();
|
||||
assert_eq!(updates.len(), 2);
|
||||
let paths = config_paths("antigravity-cli", &home.0).unwrap();
|
||||
prepare_config_updates(
|
||||
"antigravity-cli",
|
||||
&paths,
|
||||
&config_images(&paths).unwrap(),
|
||||
&updates,
|
||||
true,
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn antigravity_cli_helper_uses_managed_credentials_and_model() {
|
||||
let home = Home::new();
|
||||
home.apply(AgentClient::AntigravityCli, "model-one");
|
||||
let binary = if cfg!(windows) {
|
||||
home.0.join("AppData/Local/agy/bin/agy.exe")
|
||||
} else {
|
||||
home.0.join(".local/bin/agy")
|
||||
};
|
||||
fs::create_dir_all(binary.parent().unwrap()).unwrap();
|
||||
fs::write(&binary, []).unwrap();
|
||||
let args = [
|
||||
"cpa".into(),
|
||||
"--cpa-antigravity-cli".into(),
|
||||
home.0.clone().into_os_string(),
|
||||
"--print=hello".into(),
|
||||
];
|
||||
let command = antigravity_helper_command(&args).unwrap();
|
||||
assert_eq!(Path::new(command.get_program()), binary.as_path());
|
||||
assert_eq!(
|
||||
command.get_args().collect::<Vec<_>>(),
|
||||
[
|
||||
std::ffi::OsStr::new(&format!(
|
||||
"--gemini_dir={}",
|
||||
home.0.join(".gemini").display()
|
||||
)),
|
||||
std::ffi::OsStr::new(&format!("--model={CLI_MODEL_LABEL}")),
|
||||
std::ffi::OsStr::new("--print=hello"),
|
||||
]
|
||||
);
|
||||
let variables = command
|
||||
.get_envs()
|
||||
.collect::<std::collections::HashMap<_, _>>();
|
||||
assert_eq!(
|
||||
variables[std::ffi::OsStr::new("GEMINI_API_KEY")],
|
||||
Some(std::ffi::OsStr::new("test-key"))
|
||||
);
|
||||
assert_eq!(
|
||||
variables[std::ffi::OsStr::new("GOOGLE_GEMINI_BASE_URL")],
|
||||
Some(std::ffi::OsStr::new("http://127.0.0.1:8317"))
|
||||
);
|
||||
assert_eq!(variables[std::ffi::OsStr::new("GOOGLE_API_KEY")], None);
|
||||
let paths = antigravity_config_paths(AgentClient::AntigravityCli, &home.0);
|
||||
let valid = fs::read_to_string(&paths[1]).unwrap();
|
||||
for key in ["apiKey", "baseUrl", "model", "provider"] {
|
||||
let mut connection: Value = serde_json::from_str(&valid).unwrap();
|
||||
connection[key] = json!("");
|
||||
home.save(&paths[1], &connection);
|
||||
assert!(antigravity_helper_command(&args).is_err(), "{key}");
|
||||
}
|
||||
fs::write(&paths[1], valid).unwrap();
|
||||
home.save(&paths[0], &json!({"modelProvider":"other"}));
|
||||
assert!(antigravity_helper_command(&args).is_err());
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
#[test]
|
||||
fn antigravity_ide_discovery_and_template_use_native_server() {
|
||||
let home = Home::new();
|
||||
let install = home.0.join("AppData/Local/Programs/Antigravity IDE");
|
||||
let executable = install.join("Antigravity IDE.exe");
|
||||
let resources = install.join("resources/app");
|
||||
let server = resources.join("extensions/antigravity/bin/language_server_windows_x64.exe");
|
||||
fs::create_dir_all(server.parent().unwrap()).unwrap();
|
||||
fs::write(&server, []).unwrap();
|
||||
fs::write(&executable, []).unwrap();
|
||||
home.save(
|
||||
&resources.join("product.json"),
|
||||
&json!({"ideVersion":"2.5.5"}),
|
||||
);
|
||||
assert_eq!(find_antigravity_ide(&home.0), Some(executable.clone()));
|
||||
assert_eq!(
|
||||
read_antigravity_ide_version(&executable).as_deref(),
|
||||
Some("2.5.5")
|
||||
);
|
||||
assert_eq!(
|
||||
antigravity_language_server(&executable),
|
||||
Some(server.clone())
|
||||
);
|
||||
let updates = build_agent_template_updates(AgentDefaultConfiguration {
|
||||
client: AgentClient::AntigravityIde,
|
||||
home: &home.0,
|
||||
port: 8317,
|
||||
api_key: "test-key",
|
||||
model: "model-one",
|
||||
models: &[],
|
||||
codex_catalog: None,
|
||||
oauth_configuration: false,
|
||||
claude_code_model_mappings: None,
|
||||
claude_desktop_model_mappings: None,
|
||||
})
|
||||
.unwrap();
|
||||
assert_eq!(updates.len(), 1);
|
||||
let value = parse(&updates[0].path, Some(&updates[0].after)).unwrap();
|
||||
assert_eq!(
|
||||
Path::new(value[IDE_ENV_KEY][IDE_SERVER_ENV].as_str().unwrap()),
|
||||
server.as_path()
|
||||
);
|
||||
assert_eq!(value[IDE_ENV_KEY][IDE_MODEL_ENV], "model-one");
|
||||
assert_eq!(value[IDE_BINARY_KEY], json!(env::current_exe().unwrap()));
|
||||
fs::remove_file(&server).unwrap();
|
||||
assert!(build_antigravity_updates(
|
||||
AgentClient::AntigravityIde,
|
||||
&home.0,
|
||||
"http://127.0.0.1:8317",
|
||||
"key",
|
||||
"model",
|
||||
false
|
||||
)
|
||||
.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn antigravity_removal_without_history_keeps_unmanaged_settings() {
|
||||
for client in [AgentClient::AntigravityIde, AgentClient::AntigravityCli] {
|
||||
let home = Home::new();
|
||||
home.apply(client, "model-one");
|
||||
let paths = antigravity_config_paths(client, &home.0);
|
||||
let mut settings =
|
||||
parse(&paths[0], read_optional_text(&paths[0]).unwrap().as_deref()).unwrap();
|
||||
settings["externalSetting"] = json!(42);
|
||||
home.save(&paths[0], &settings);
|
||||
let removals = prepare_antigravity_removal(client, &paths).unwrap();
|
||||
let restored = parse(
|
||||
&paths[0],
|
||||
Some(std::str::from_utf8(removals[0].1.as_ref().unwrap()).unwrap()),
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(restored, json!({"externalSetting":42}));
|
||||
if client == AgentClient::AntigravityCli {
|
||||
assert!(removals[1].1.is_none());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -30,6 +30,8 @@ pub(crate) fn inspect_agent_config_statuses(
|
||||
AgentStatusDetectionTarget::Client(AgentClient::DeepSeekHarness),
|
||||
AgentStatusDetectionTarget::Client(AgentClient::ZCode),
|
||||
AgentStatusDetectionTarget::Client(AgentClient::WorkBuddy),
|
||||
AgentStatusDetectionTarget::Client(AgentClient::AntigravityIde),
|
||||
AgentStatusDetectionTarget::Client(AgentClient::AntigravityCli),
|
||||
AgentStatusDetectionTarget::Client(AgentClient::KimiCode),
|
||||
AgentStatusDetectionTarget::Client(AgentClient::GrokBuild),
|
||||
AgentStatusDetectionTarget::PiProvider,
|
||||
|
||||
@@ -187,6 +187,7 @@ pub(crate) fn build_agent_updates_with_oauth(
|
||||
},
|
||||
])
|
||||
}
|
||||
AgentClient::AntigravityIde | AgentClient::AntigravityCli => build_antigravity_updates(client, home, &root_base, api_key, model, false),
|
||||
AgentClient::WorkBuddy => {
|
||||
let before = read_optional_text(&paths[0])?;
|
||||
Ok(vec![AgentFileUpdate {
|
||||
@@ -550,6 +551,9 @@ pub(crate) fn write_agent_configuration_file(
|
||||
{
|
||||
return write_codex_private_file(path, content);
|
||||
}
|
||||
if matches!(client, AgentClient::AntigravityIde | AgentClient::AntigravityCli) {
|
||||
return write_codex_private_file(path, content);
|
||||
}
|
||||
if client == AgentClient::DeepSeekHarness {
|
||||
let owner_only = path.file_name().and_then(|name| name.to_str())
|
||||
== Some(DEEPSEEK_HARNESS_CREDENTIALS_FILE);
|
||||
@@ -1629,6 +1633,7 @@ pub(crate) fn prepare_agent_managed_removal(
|
||||
AgentClient::DeepSeekHarness => prepare_deepseek_harness_managed_removal(paths),
|
||||
AgentClient::ZCode => prepare_zcode_managed_removal(paths),
|
||||
AgentClient::WorkBuddy => prepare_workbuddy_managed_removal(paths),
|
||||
AgentClient::AntigravityIde | AgentClient::AntigravityCli => prepare_antigravity_removal(client, paths),
|
||||
AgentClient::KimiCode => prepare_kimi_code_managed_removal(paths),
|
||||
AgentClient::GrokBuild => prepare_grok_build_managed_removal(paths),
|
||||
}
|
||||
@@ -2667,6 +2672,7 @@ pub(crate) fn build_agent_session_restored_bytes_with_preference(
|
||||
}
|
||||
AgentClient::ZCode => build_restored_zcode_config(current, original)?,
|
||||
AgentClient::WorkBuddy => build_restored_workbuddy_config(current, original)?,
|
||||
AgentClient::AntigravityIde | AgentClient::AntigravityCli => restore_antigravity_config(client, path, current, original)?,
|
||||
AgentClient::KimiCode => build_restored_kimi_code_config(current, original)?,
|
||||
AgentClient::GrokBuild => build_restored_grok_build_config(current, original)?,
|
||||
};
|
||||
|
||||
@@ -48,6 +48,7 @@ pub(crate) fn agent_config_paths(client: AgentClient, home: &Path) -> Vec<PathBu
|
||||
]
|
||||
}
|
||||
AgentClient::WorkBuddy => vec![workbuddy_home(home).join("models.json")],
|
||||
AgentClient::AntigravityIde | AgentClient::AntigravityCli => antigravity_config_paths(client, home),
|
||||
AgentClient::ZCode => vec![
|
||||
home.join(".zcode/v2").join(ZCODE_CONFIG_FILE),
|
||||
home.join(".zcode/cli").join(ZCODE_CONFIG_FILE),
|
||||
@@ -982,6 +983,7 @@ pub(crate) fn inspect_agent_config(
|
||||
AgentClient::DeepSeekHarness => read_deepseek_harness_profile_version(home),
|
||||
AgentClient::ZCode => executable.as_deref().and_then(read_zcode_app_version),
|
||||
AgentClient::WorkBuddy => executable.as_deref().and_then(read_workbuddy_app_version),
|
||||
AgentClient::AntigravityIde => executable.as_deref().and_then(read_antigravity_ide_version),
|
||||
_ => None,
|
||||
};
|
||||
let version = cli_version.clone().or_else(|| app_version.clone());
|
||||
@@ -1067,13 +1069,13 @@ pub(crate) fn agent_installation_detected(
|
||||
version.is_some()
|
||||
|| (matches!(
|
||||
client,
|
||||
AgentClient::ClaudeDesktop | AgentClient::OpenCode | AgentClient::ZCode | AgentClient::WorkBuddy
|
||||
AgentClient::ClaudeDesktop | AgentClient::OpenCode | AgentClient::ZCode | AgentClient::WorkBuddy | AgentClient::AntigravityIde
|
||||
) && executable_found)
|
||||
|| app_installed
|
||||
}
|
||||
|
||||
pub(crate) fn should_probe_primary_agent_executable_version(client: AgentClient) -> bool {
|
||||
!matches!(client, AgentClient::ClaudeDesktop | AgentClient::ZCode | AgentClient::WorkBuddy)
|
||||
!matches!(client, AgentClient::ClaudeDesktop | AgentClient::ZCode | AgentClient::WorkBuddy | AgentClient::AntigravityIde)
|
||||
}
|
||||
|
||||
pub(crate) fn agent_launch_targets(
|
||||
@@ -1127,7 +1129,7 @@ pub(crate) fn agent_launch_targets(
|
||||
});
|
||||
}
|
||||
}
|
||||
AgentClient::ZCode | AgentClient::WorkBuddy => {
|
||||
AgentClient::ZCode | AgentClient::WorkBuddy | AgentClient::AntigravityIde => {
|
||||
if let Some(executable) = executable {
|
||||
targets.push(AgentLaunchTarget {
|
||||
id: "app".to_string(),
|
||||
@@ -1264,6 +1266,7 @@ pub(crate) fn inspect_agent_managed_config(
|
||||
false,
|
||||
))
|
||||
}
|
||||
AgentClient::AntigravityIde | AgentClient::AntigravityCli => inspect_antigravity_config(client, paths, port, api_key).map(|(configured, model)| (configured, model, false)),
|
||||
AgentClient::WorkBuddy => inspect_workbuddy_agent_config(&paths[0], port, api_key)
|
||||
.map(|(configured, model)| (configured, model, false)),
|
||||
AgentClient::KimiCode => inspect_kimi_code_agent_config(&paths[0], port, api_key)
|
||||
@@ -1473,6 +1476,7 @@ pub(crate) fn agent_has_managed_marker(
|
||||
}
|
||||
AgentClient::DeepSeekHarness => deepseek_harness_has_managed_marker(paths),
|
||||
AgentClient::WorkBuddy => workbuddy_has_managed_marker(&paths[0]),
|
||||
AgentClient::AntigravityIde | AgentClient::AntigravityCli => antigravity_has_marker(client, paths),
|
||||
AgentClient::ZCode => {
|
||||
let prefix = format!("{MANAGED_AGENT_PROVIDER_ID}/");
|
||||
for path in paths {
|
||||
@@ -2389,6 +2393,8 @@ pub(crate) fn find_windows_codex_app_executable(home: &Path) -> Option<PathBuf>
|
||||
}
|
||||
|
||||
pub(crate) fn find_agent_executable(client: AgentClient, home: &Path) -> Option<PathBuf> {
|
||||
if client == AgentClient::AntigravityIde { return find_antigravity_ide(home); }
|
||||
if client == AgentClient::AntigravityCli { return find_antigravity_cli(home); }
|
||||
if client == AgentClient::ClaudeDesktop {
|
||||
return find_claude_desktop_executable(home);
|
||||
}
|
||||
|
||||
@@ -181,6 +181,19 @@ pub(crate) fn launch_agent(
|
||||
let requested_target = requested_target.unwrap_or(default_target);
|
||||
match (client, requested_target) {
|
||||
(AgentClient::ClaudeDesktop, "app") => launch_claude_desktop(&home),
|
||||
(AgentClient::AntigravityIde, "app") => {
|
||||
let executable = find_antigravity_ide(&home).ok_or("未找到 Antigravity IDE")?;
|
||||
launch_desktop_agent(&executable, client.name())
|
||||
}
|
||||
(AgentClient::AntigravityCli, "cli")
|
||||
if antigravity_has_marker(client, &agent_config_paths(client, &home))? => {
|
||||
if !status.configured {
|
||||
return Err("Antigravity CLI 配置与 CPA 不一致,请重新应用配置".into());
|
||||
}
|
||||
let executable = env::current_exe().map_err(|_| "无法定位 CPA 启动适配程序")?;
|
||||
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())?;
|
||||
@@ -193,7 +206,7 @@ pub(crate) fn launch_agent(
|
||||
}
|
||||
(AgentClient::Codex, "app") => launch_codex_desktop(&home),
|
||||
(AgentClient::OpenCode, "app") => launch_opencode_desktop(&home),
|
||||
(AgentClient::ClaudeDesktop | AgentClient::ZCode | AgentClient::WorkBuddy, "cli") => {
|
||||
(AgentClient::ClaudeDesktop | AgentClient::ZCode | AgentClient::WorkBuddy | AgentClient::AntigravityIde, "cli") => {
|
||||
Err(format!("{} 不支持 CLI 启动方式", client.name()))
|
||||
}
|
||||
(_, "cli") => {
|
||||
@@ -701,6 +714,7 @@ pub(crate) async fn restart_agent_app(app: tauri::AppHandle, client: String) ->
|
||||
| AgentClient::ClaudeDesktop
|
||||
| AgentClient::ZCode
|
||||
| AgentClient::WorkBuddy
|
||||
| AgentClient::AntigravityIde
|
||||
) {
|
||||
return Err(format!("{} 不支持桌面应用重启", client.name()));
|
||||
}
|
||||
@@ -747,6 +761,7 @@ fn find_desktop_restart_target(
|
||||
AgentClient::WorkBuddy => {
|
||||
find_workbuddy_desktop_executable(home).map(DesktopAppTarget::Application)
|
||||
}
|
||||
AgentClient::AntigravityIde => find_antigravity_ide(home).map(DesktopAppTarget::Application),
|
||||
AgentClient::ClaudeDesktop => {
|
||||
let executable =
|
||||
find_claude_desktop_executable(home).map(DesktopAppTarget::Application);
|
||||
|
||||
@@ -727,6 +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::AntigravityIde | AgentClient::AntigravityCli => Err("Antigravity 模板需要安装和用户目录信息".into()),
|
||||
AgentClient::WorkBuddy => Ok(vec![build_workbuddy_agent_config(
|
||||
None, &openai_base, api_key, model, models,
|
||||
)?]),
|
||||
@@ -1540,6 +1541,9 @@ pub(crate) fn build_agent_template_updates(
|
||||
claude_code_model_mappings,
|
||||
claude_desktop_model_mappings,
|
||||
} = request;
|
||||
if matches!(client, AgentClient::AntigravityIde | AgentClient::AntigravityCli) {
|
||||
return build_antigravity_updates(client, home, &managed_core_loopback_origin(port), api_key, model, true);
|
||||
}
|
||||
let paths = agent_config_paths(client, home);
|
||||
let contents = fresh_agent_contents_with_oauth(
|
||||
client,
|
||||
|
||||
@@ -353,6 +353,19 @@ pub(crate) fn find_windows_registered_zcode_executable() -> Option<PathBuf> {
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(not(test))]
|
||||
pub(crate) fn find_windows_registered_antigravity_ide() -> Option<PathBuf> {
|
||||
for executable in ["Antigravity IDE.exe", "Antigravity.exe"] {
|
||||
if let Some(path) = collect_windows_desktop_registrations(executable, |name| {
|
||||
name == "Antigravity IDE" || name.starts_with("Antigravity IDE (")
|
||||
|| name == "Antigravity" || name.starts_with("Antigravity (")
|
||||
}).into_iter().find_map(|r| parse_windows_desktop_registration(r.kind, &r.value, executable)) {
|
||||
return Some(path);
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
#[cfg(not(test))]
|
||||
pub(crate) fn find_windows_registered_workbuddy_executable() -> Option<PathBuf> {
|
||||
for executable in ["WorkBuddyAI.exe", "WorkBuddy.exe"] {
|
||||
|
||||
@@ -75,6 +75,8 @@ fn tracked_configuration_paths(app: &tauri::AppHandle) -> Result<Vec<PathBuf>, S
|
||||
AgentClient::DeepSeekHarness,
|
||||
AgentClient::ZCode,
|
||||
AgentClient::WorkBuddy,
|
||||
AgentClient::AntigravityIde,
|
||||
AgentClient::AntigravityCli,
|
||||
AgentClient::KimiCode,
|
||||
AgentClient::GrokBuild,
|
||||
] {
|
||||
|
||||
@@ -1363,6 +1363,8 @@ enum AgentClient {
|
||||
DeepSeekHarness,
|
||||
ZCode,
|
||||
WorkBuddy,
|
||||
AntigravityIde,
|
||||
AntigravityCli,
|
||||
KimiCode,
|
||||
GrokBuild,
|
||||
}
|
||||
@@ -1386,6 +1388,8 @@ impl AgentClient {
|
||||
"deepseek-harness" => Ok(Self::DeepSeekHarness),
|
||||
"zcode" => Ok(Self::ZCode),
|
||||
"workbuddy" => Ok(Self::WorkBuddy),
|
||||
"antigravity-ide" => Ok(Self::AntigravityIde),
|
||||
"antigravity-cli" => Ok(Self::AntigravityCli),
|
||||
"kimi-code" => Ok(Self::KimiCode),
|
||||
"grok-build" => Ok(Self::GrokBuild),
|
||||
_ => Err(format!("不支持的智能体客户端: {value}")),
|
||||
@@ -1403,6 +1407,8 @@ impl AgentClient {
|
||||
Self::DeepSeekHarness => "deepseek-harness",
|
||||
Self::ZCode => "zcode",
|
||||
Self::WorkBuddy => "workbuddy",
|
||||
Self::AntigravityIde => "antigravity-ide",
|
||||
Self::AntigravityCli => "antigravity-cli",
|
||||
Self::KimiCode => "kimi-code",
|
||||
Self::GrokBuild => "grok-build",
|
||||
}
|
||||
@@ -1419,6 +1425,8 @@ impl AgentClient {
|
||||
Self::DeepSeekHarness => "DeepSeek Harness",
|
||||
Self::ZCode => "ZCode",
|
||||
Self::WorkBuddy => "WorkBuddy",
|
||||
Self::AntigravityIde => "Antigravity IDE",
|
||||
Self::AntigravityCli => "Antigravity CLI",
|
||||
Self::KimiCode => "Kimi Code",
|
||||
Self::GrokBuild => "Grok Build",
|
||||
}
|
||||
@@ -1444,6 +1452,8 @@ impl AgentClient {
|
||||
Self::DeepSeekHarness => &["dsh"],
|
||||
Self::ZCode => &["zcode"],
|
||||
Self::WorkBuddy => &[],
|
||||
Self::AntigravityIde => &[],
|
||||
Self::AntigravityCli => &["agy"],
|
||||
Self::KimiCode => &["kimi"],
|
||||
Self::GrokBuild => &["grok"],
|
||||
}
|
||||
@@ -2341,6 +2351,14 @@ struct GithubAsset {
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let helper_args = env::args_os().collect::<Vec<_>>();
|
||||
if antigravity_helper_requested(&helper_args) {
|
||||
let code = run_antigravity_helper(&helper_args).unwrap_or_else(|error| {
|
||||
eprintln!("{error}");
|
||||
1
|
||||
});
|
||||
std::process::exit(code);
|
||||
}
|
||||
let mut args = env::args_os();
|
||||
while let Some(argument) = args.next() {
|
||||
if argument == "--portable-update-helper" {
|
||||
|
||||
@@ -14,6 +14,8 @@ fn configuration_paths_ignore_inherited_environment() {
|
||||
"deepseek-harness",
|
||||
"zcode",
|
||||
"workbuddy",
|
||||
"antigravity-ide",
|
||||
"antigravity-cli",
|
||||
"kimi-code",
|
||||
"grok-build",
|
||||
"pi",
|
||||
@@ -36,6 +38,7 @@ fn configuration_paths_ignore_inherited_environment() {
|
||||
command.env("CPA_PATH_ISOLATION_CHILD", "1");
|
||||
for variable in [
|
||||
"LOCALAPPDATA",
|
||||
"APPDATA",
|
||||
"XDG_CONFIG_HOME",
|
||||
"CODEX_HOME",
|
||||
"HERMES_HOME",
|
||||
|
||||
@@ -937,6 +937,10 @@ export const jaOverrides = {
|
||||
'agents.description.openclaw': 'CPA モデルプロバイダーと既定モデルを使用',
|
||||
'agents.description.hermes': 'CPA カスタムプロバイダーを使用',
|
||||
'agents.description.deepseekHarness': 'Harness のネイティブ DeepSeek 設定と分離された OpenAI 互換ルートで CPA を使用',
|
||||
'agents.description.antigravityIde': 'CPA の Gemini 互換 API で Antigravity IDE を設定',
|
||||
'agents.description.antigravityCli': 'CPA の Gemini 互換 API で Antigravity CLI を設定',
|
||||
'agents.modify.antigravityIdeHint': '設定の適用・解除後は IDE を再起動してください。リクエストにはここで選択した CPA モデルが使用され、IDE 内のモデル表示名は変わりません。初回は IDE へのログインが必要です。',
|
||||
'agents.modify.antigravityCliHint': '設定適用後はこのページの起動ボタンを使用してください。CPA が CLI プロセスに接続先とキーを渡します。agy を直接実行する場合は環境変数の設定が必要です。',
|
||||
'agents.description.workbuddy': 'CPA の OpenAI Chat Completions API で WorkBuddy のカスタムモデルを設定',
|
||||
'agents.modify.workbuddyHint': '適用後、WorkBuddy で「CPA:モデル名」を選択してください。表示されない場合は App を再起動してください。既存の会話のモデルは自動で切り替わりません。',
|
||||
'agents.description.zcode': 'CPA の Anthropic Messages API と動的モデルカタログを使用',
|
||||
|
||||
@@ -937,6 +937,10 @@ export const en: Record<MessageKey, string> = {
|
||||
'agents.description.openclaw': 'Uses the CPA model provider and default model',
|
||||
'agents.description.hermes': 'Uses the CPA custom provider',
|
||||
'agents.description.deepseekHarness': 'Uses CPA through an isolated, OpenAI-compatible Harness route',
|
||||
'agents.description.antigravityIde': 'Configure Antigravity IDE with CPA’s Gemini-compatible API',
|
||||
'agents.description.antigravityCli': 'Configure Antigravity CLI with CPA’s Gemini-compatible API',
|
||||
'agents.modify.antigravityIdeHint': 'Restart the IDE after applying or disabling configuration. Requests use the CPA model selected here; model labels inside the IDE stay unchanged. Initial IDE sign-in is still required.',
|
||||
'agents.modify.antigravityCliHint': 'Use Launch on this page after applying configuration. CPA supplies the endpoint and key to that CLI process. Running agy directly requires setting the environment variables yourself.',
|
||||
'agents.description.workbuddy': 'Configures a WorkBuddy custom model using the CPA OpenAI Chat Completions API',
|
||||
'agents.modify.workbuddyHint': 'After applying, select “CPA:model name” in WorkBuddy. Restart the app if it is missing; existing conversations keep their selected model.',
|
||||
'agents.description.zcode': 'Uses the CPA Anthropic Messages API and dynamic model catalog',
|
||||
|
||||
@@ -936,6 +936,10 @@ export const zhCN = {
|
||||
'agents.description.openclaw': '使用 CPA 模型供应商和默认模型',
|
||||
'agents.description.hermes': '使用 CPA 自定义提供方',
|
||||
'agents.description.deepseekHarness': '通过通用 OpenAI 兼容路由使用 CPA,并与 Harness 原生 DeepSeek 配置隔离',
|
||||
'agents.description.antigravityIde': '使用 CPA 的 Gemini 兼容接口配置 Antigravity IDE',
|
||||
'agents.description.antigravityCli': '使用 CPA 的 Gemini 兼容接口配置 Antigravity CLI',
|
||||
'agents.modify.antigravityIdeHint': '应用或关闭配置后请重启 IDE。模型请求使用此处选择的 CPA 模型;IDE 内的模型标签保持原样。首次使用仍需完成 IDE 登录。',
|
||||
'agents.modify.antigravityCliHint': '应用后请使用此页的启动按钮,CPA 会为本次 CLI 进程注入地址和密钥。直接运行 agy 需要自行设置环境变量。',
|
||||
'agents.description.workbuddy': '使用 CPA 的 OpenAI Chat Completions 接口配置 WorkBuddy 自定义模型',
|
||||
'agents.modify.workbuddyHint': '应用后,在 WorkBuddy 的模型选择器中选择「CPA:模型名」。若未显示,请重启 App;已有会话的模型不会自动切换。',
|
||||
'agents.description.zcode': '使用 CPA 的 Anthropic Messages 接口和动态模型目录',
|
||||
|
||||
@@ -37,6 +37,7 @@ import opencodeIcon from '../assets/icons/opencode.svg';
|
||||
import piIcon from '../assets/icons/pi-logo-on-light.svg';
|
||||
import zcodeIcon from '../assets/icons/zcode.png';
|
||||
import workbuddyIcon from '../assets/icons/workbuddy.png';
|
||||
import antigravityIcon from '../assets/icons/antigravity.svg';
|
||||
import {
|
||||
agentModelAlias,
|
||||
filterAgentModels,
|
||||
@@ -81,6 +82,8 @@ type AgentClientId =
|
||||
| 'deepseek-harness'
|
||||
| 'zcode'
|
||||
| 'workbuddy'
|
||||
| 'antigravity-ide'
|
||||
| 'antigravity-cli'
|
||||
| 'kimi-code'
|
||||
| 'grok-build'
|
||||
| 'pi';
|
||||
@@ -230,7 +233,7 @@ type AgentDefinition = {
|
||||
name: string;
|
||||
icon?: string;
|
||||
Icon?: ComponentType<{ size?: number; 'aria-hidden'?: boolean }>;
|
||||
descriptionKey: 'agents.description.claudeCode' | 'agents.description.claudeDesktop' | 'agents.description.codex' | 'agents.description.opencode' | 'agents.description.openclaw' | 'agents.description.hermes' | 'agents.description.deepseekHarness' | 'agents.description.zcode' | 'agents.description.workbuddy' | 'agents.description.kimiCode' | 'agents.description.grokBuild' | 'agents.description.pi';
|
||||
descriptionKey: 'agents.description.claudeCode' | 'agents.description.claudeDesktop' | 'agents.description.codex' | 'agents.description.opencode' | 'agents.description.openclaw' | 'agents.description.hermes' | 'agents.description.deepseekHarness' | 'agents.description.zcode' | 'agents.description.workbuddy' | 'agents.description.antigravityIde' | 'agents.description.antigravityCli' | 'agents.description.kimiCode' | 'agents.description.grokBuild' | 'agents.description.pi';
|
||||
};
|
||||
|
||||
type AgentSubpageId = 'core' | 'management' | 'sessions';
|
||||
@@ -284,6 +287,8 @@ const agentDefinitions: AgentDefinition[] = [
|
||||
icon: grokIcon,
|
||||
descriptionKey: 'agents.description.grokBuild',
|
||||
},
|
||||
{ id: 'antigravity-ide', name: 'Antigravity IDE', icon: antigravityIcon, descriptionKey: 'agents.description.antigravityIde' },
|
||||
{ id: 'antigravity-cli', name: 'Antigravity CLI', icon: antigravityIcon, descriptionKey: 'agents.description.antigravityCli' },
|
||||
{
|
||||
id: 'workbuddy',
|
||||
name: 'WorkBuddy',
|
||||
@@ -1218,6 +1223,8 @@ export function AgentsPage({ embedded = false, onConfigurationApplied }: AgentsP
|
||||
const modificationDescription = activeStatus?.modificationState === 'invalid'
|
||||
? t('agents.modify.invalid')
|
||||
: nativeOauth ? t('agents.nativeOAuth.activeHint')
|
||||
: selected === 'antigravity-ide' ? t('agents.modify.antigravityIdeHint')
|
||||
: selected === 'antigravity-cli' ? t('agents.modify.antigravityCliHint')
|
||||
: selected === 'workbuddy'
|
||||
? t('agents.modify.workbuddyHint')
|
||||
: selected === 'zcode'
|
||||
@@ -1741,7 +1748,7 @@ export function AgentsPage({ embedded = false, onConfigurationApplied }: AgentsP
|
||||
};
|
||||
|
||||
const restartDesktopApp = async () => {
|
||||
if (!['codex', 'opencode', 'claude-desktop', 'zcode', 'workbuddy'].includes(selected)) return;
|
||||
if (!['codex', 'opencode', 'claude-desktop', 'zcode', 'workbuddy', 'antigravity-ide'].includes(selected)) return;
|
||||
setBusyAction('restart-app');
|
||||
setLaunchError('');
|
||||
try {
|
||||
@@ -2436,7 +2443,7 @@ export function AgentsPage({ embedded = false, onConfigurationApplied }: AgentsP
|
||||
<MessageNotice tone="success" message={!configurationErrorMessage ? configurationNotice || clearNotice : null}
|
||||
onDismiss={() => { setConfigurationNotice(''); setClearNotice(''); }} />
|
||||
{activeSubpage === 'core' ? <AgentRunControls name={activeDefinition.name} dualTargets={hasIndependentCliAndApp}
|
||||
desktop={hasIndependentCliAndApp || selected === 'claude-desktop' || selected === 'zcode' || selected === 'workbuddy'}
|
||||
desktop={hasIndependentCliAndApp || selected === 'claude-desktop' || selected === 'zcode' || selected === 'workbuddy' || selected === 'antigravity-ide'}
|
||||
targets={activeLaunchTargets} enabled={launchEnabled} busyAction={busyAction}
|
||||
harness={isDeepSeekHarnessClient ? deepSeekHarnessProcessStatus : null}
|
||||
onLaunch={(target) => void launchAgent(target)} onRestart={() => void restartDesktopApp()}
|
||||
|
||||
@@ -66,7 +66,7 @@ const path = require('node:path');
|
||||
assert.equal(await tab('基础配置').getAttribute('aria-selected'), 'true');
|
||||
assert.equal(await tab('会话管理').count(), 0);
|
||||
|
||||
for (const client of ['claude-desktop', 'zcode', 'workbuddy', 'opencode']) {
|
||||
for (const client of ['claude-desktop', 'zcode', 'workbuddy', 'antigravity-ide', 'opencode']) {
|
||||
await open(mode + 'client=' + client + '&app-only');
|
||||
assert.ok(await button('重启 App').isEnabled());
|
||||
await button('重启 App').click();
|
||||
@@ -80,7 +80,7 @@ const path = require('node:path');
|
||||
await open(mode + 'client=kimi-code');
|
||||
assert.equal(await button('重启 App').count(), 0);
|
||||
|
||||
for (const client of ['claude-code', 'claude-desktop', 'opencode', 'openclaw', 'hermes', 'deepseek-harness', 'zcode', 'workbuddy', 'kimi-code', 'grok-build']) {
|
||||
for (const client of ['claude-code', 'claude-desktop', 'opencode', 'openclaw', 'hermes', 'deepseek-harness', 'zcode', 'workbuddy', 'antigravity-ide', 'antigravity-cli', 'kimi-code', 'grok-build']) {
|
||||
await open(mode + 'client=' + client);
|
||||
assert.ok(await button('关闭配置修改').isEnabled());
|
||||
await tab('配置管理').click();
|
||||
|
||||
@@ -0,0 +1,124 @@
|
||||
import argparse
|
||||
import http.server
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import socket
|
||||
import subprocess
|
||||
import tempfile
|
||||
import threading
|
||||
import time
|
||||
import urllib.request
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description="Probe CPA launch helpers with installed Antigravity binaries and a local mock API.")
|
||||
parser.add_argument("--cpa", type=Path, required=True)
|
||||
parser.add_argument("--cli", type=Path, required=True)
|
||||
parser.add_argument("--language-server", type=Path, required=True)
|
||||
args = parser.parse_args()
|
||||
for path in (args.cpa, args.cli, args.language_server):
|
||||
if not path.is_file():
|
||||
parser.error(f"Binary not found: {path}")
|
||||
cpa = str(args.cpa.resolve())
|
||||
answer = "CPA integration probe OK"
|
||||
seen = []
|
||||
|
||||
class Handler(http.server.BaseHTTPRequestHandler):
|
||||
def log_message(self, *args):
|
||||
pass
|
||||
|
||||
def do_POST(self):
|
||||
self.rfile.read(int(self.headers.get("Content-Length", "0")))
|
||||
seen.append((self.path, self.headers.get("x-goog-api-key")))
|
||||
selected = "/models/cpa-test-model:" in self.path
|
||||
self.send_response(200 if selected else 404)
|
||||
self.send_header("Content-Type", "text/event-stream" if selected else "application/json")
|
||||
self.end_headers()
|
||||
if selected:
|
||||
body = {"candidates": [{"content": {"role": "model", "parts": [{"text": answer}]}, "finishReason": "STOP"}],
|
||||
"usageMetadata": {"promptTokenCount": 1, "candidatesTokenCount": 5, "totalTokenCount": 6}}
|
||||
self.wfile.write(("data: " + json.dumps(body) + "\n\n").encode())
|
||||
else:
|
||||
self.wfile.write(b'{"error":{"code":404,"message":"Model unavailable","status":"NOT_FOUND"}}')
|
||||
|
||||
server = http.server.ThreadingHTTPServer(("127.0.0.1", 0), Handler)
|
||||
threading.Thread(target=server.serve_forever, daemon=True).start()
|
||||
try:
|
||||
with tempfile.TemporaryDirectory(prefix="cpa-antigravity-native-") as directory:
|
||||
root = Path(directory)
|
||||
gemini = root / ".gemini"
|
||||
settings = gemini / "antigravity-cli"
|
||||
settings.mkdir(parents=True)
|
||||
(settings / "settings.json").write_text(json.dumps({"modelProvider": "gemini", "enableTelemetry": False,
|
||||
"customModelsConfig": {"customModels": {"CPA (EasyCLIProxyAPI)": {"modelName": "cpa-test-model"}}}}))
|
||||
endpoint = f"http://127.0.0.1:{server.server_port}"
|
||||
(settings / "cpa-connection.json").write_text(json.dumps({"provider": "cpa-gui", "baseUrl": endpoint,
|
||||
"apiKey": "cpa-probe-key", "model": "cpa-test-model"}))
|
||||
env = dict(os.environ, HOME=directory, USERPROFILE=directory, APPDATA=str(root / "roaming"),
|
||||
LOCALAPPDATA=str(root / "local"), XDG_CONFIG_HOME=str(root / "config"),
|
||||
PATH=str(args.cli.resolve().parent) + os.pathsep + os.environ.get("PATH", ""),
|
||||
GOOGLE_API_KEY="must-be-removed", GEMINI_API_KEY="must-be-replaced", GOOGLE_GEMINI_BASE_URL="http://127.0.0.1:1")
|
||||
flags = subprocess.CREATE_NO_WINDOW if os.name == "nt" else 0
|
||||
result = subprocess.run([cpa, "--cpa-antigravity-cli", directory, f"--print=Say exactly {answer}. Do not use tools.",
|
||||
"--print-timeout=15s"], env=env, cwd=root, capture_output=True, timeout=30, creationflags=flags)
|
||||
assert result.returncode == 0, result.stderr.decode(errors="replace")
|
||||
assert answer in result.stdout.decode(errors="replace"), result.stdout
|
||||
assert any("/models/cpa-test-model:" in path for path, _ in seen), seen
|
||||
assert all(key == "cpa-probe-key" for _, key in seen), seen
|
||||
print("PASS CLI: selected model, scoped credentials, response, unavailable auxiliary model")
|
||||
seen.clear()
|
||||
|
||||
env.update(CPA_ANTIGRAVITY_LANGUAGE_SERVER=str(args.language_server.resolve()),
|
||||
CPA_ANTIGRAVITY_MODEL="cpa-test-model", GEMINI_API_KEY="cpa-probe-key", GOOGLE_GEMINI_BASE_URL=endpoint)
|
||||
with socket.socket() as sock:
|
||||
sock.bind(("127.0.0.1", 0))
|
||||
port = sock.getsockname()[1]
|
||||
|
||||
def rpc(method, payload):
|
||||
request = urllib.request.Request(f"http://127.0.0.1:{port}/exa.language_server_pb.LanguageServerService/{method}",
|
||||
data=json.dumps(payload).encode(), headers={"Content-Type": "application/json",
|
||||
"X-Codeium-Csrf-Token": "cpa-probe", "Connect-Protocol-Version": "1"})
|
||||
with urllib.request.urlopen(request, timeout=15) as response:
|
||||
return json.load(response)
|
||||
|
||||
with (root / "server.log").open("wb") as log:
|
||||
proc = subprocess.Popen([cpa, "--extension_server_port", "0", f"--gemini_dir={gemini}", "--headless",
|
||||
"--disable_telemetry", f"--http_server_port={port}", "--csrf_token=cpa-probe"], env=env, cwd=root,
|
||||
stdin=subprocess.PIPE, stdout=log, stderr=log, creationflags=flags)
|
||||
try:
|
||||
proc.stdin.write(b"\x0a\x09cpa-probe")
|
||||
proc.stdin.close()
|
||||
for _ in range(100):
|
||||
try:
|
||||
with socket.create_connection(("127.0.0.1", port), timeout=0.1):
|
||||
break
|
||||
except OSError:
|
||||
assert proc.poll() is None, (root / "server.log").read_text(errors="replace")
|
||||
time.sleep(0.1)
|
||||
model = "MODEL_GOOGLE_GEMINI_2_5_PRO"
|
||||
cascade = rpc("StartCascade", {"workspaceUris": [root.as_uri()], "source": 1, "requestedModel": model})["cascadeId"]
|
||||
rpc("SendUserCascadeMessage", {"cascadeId": cascade, "items": [{"text": "Say hello. Do not use tools."}],
|
||||
"blocking": True, "cascadeConfig": {"plannerConfig": {"planModel": model}}})
|
||||
steps = rpc("GetCascadeTrajectory", {"cascadeId": cascade})["trajectory"]["steps"]
|
||||
assert any(step.get("plannerResponse", {}).get("response") == answer for step in steps), steps
|
||||
assert seen and all("/models/cpa-test-model:" in path and key == "cpa-probe-key" for path, key in seen), seen
|
||||
finally:
|
||||
proc.terminate()
|
||||
proc.wait(timeout=5)
|
||||
for _ in range(50):
|
||||
try:
|
||||
with socket.create_connection(("127.0.0.1", port), timeout=0.1):
|
||||
time.sleep(0.1)
|
||||
except OSError:
|
||||
break
|
||||
else:
|
||||
raise AssertionError("Native language server survived CPA helper termination")
|
||||
print("PASS IDE: stdin forwarding, selected model, scoped credentials, response, child cleanup")
|
||||
finally:
|
||||
server.shutdown()
|
||||
server.server_close()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Vendored
+2
-2
@@ -9,7 +9,7 @@ if(params.has('reset-selections'))localStorage.removeItem('cpa-gui.agent-model-s
|
||||
localStorage.setItem('easy-cli-proxy-api.locale',params.get('locale') || 'zh-CN');
|
||||
document.documentElement.dataset.theme = params.get('theme') || 'light';
|
||||
localStorage.setItem('cpa-gui.agent-selected-client.v1', params.get('client') || 'codex');
|
||||
const ids = ['claude-code','claude-desktop','codex','opencode','openclaw','hermes','deepseek-harness','workbuddy','zcode','kimi-code','grok-build','pi'];
|
||||
const ids = ['claude-code','claude-desktop','codex','opencode','openclaw','hermes','deepseek-harness','antigravity-ide','antigravity-cli','workbuddy','zcode','kimi-code','grok-build','pi'];
|
||||
let count=0; let backupCount=0; const backups:any[]=[]; let currentModel=params.has('fresh')?null:'gpt-one';
|
||||
let nativeOauth=params.has('native-oauth'); let nativeSwitchCount=0; let codexClosed=false; let closeCount=0;
|
||||
let currentOauth=false; const currentMappings:Record<string,any>={};
|
||||
@@ -35,7 +35,7 @@ mockIPC(async (cmd,args:any) => {
|
||||
calls.push({cmd,args});
|
||||
if(cmd==='plugin:event|listen') return 1;
|
||||
if(cmd==='plugin:event|unlisten'||cmd==='set_app_locale') return null;
|
||||
if(cmd==='get_agent_config_statuses'||cmd==='refresh_agent_config_statuses') return ids.map(id=>({id,name:id,supportedPlatform:true,installed:!params.has('not-installed'),pluginInstalled:!params.has('no-plugin'),launchTargets:params.has('not-installed')?[]:['claude-desktop','zcode','workbuddy'].includes(id)?[{id:'app',label:id,detail:'test desktop'}]:['codex','opencode'].includes(id)&&!params.has('cli-only')?[...(params.has('app-only')?[]:[{id:'cli',label:'CLI',detail:'test CLI'}]),{id:'app',label:'APP',detail:'test desktop'}]:[{id:'cli',label:'CLI',detail:'test CLI'}],version:'1.0',cliVersion:'1.0',appVersion:null,pluginVersion:'1.0',configValid:params.get('state')!=='invalid',connectionState:params.get('state') || (currentModel?'configured':'not-configured'),configured:!!currentModel,configurationSynchronized:!!currentModel,currentModel,oauthConfiguration:id==='codex'&¤tOauth,codexNativeOauth:id==='codex'&&nativeOauth,modificationEnabled:!!currentModel,modificationState:currentModel?'applied':'unconfigured',backupAvailable:false,appliedModel:currentModel,claudeCodeModelMappings:id==='claude-code'?currentMappings[id]??null:null,claudeDesktopModelMappings:id==='claude-desktop'?currentMappings[id]??null:null,warnings:[],error:null})).map(status=>status.id==='codex'&&(nativeOauth||codexClosed)?{...status,configured:false,connectionState:'not-configured',configurationSynchronized:false,currentModel:null,appliedModel:null,modificationEnabled:false,modificationState:'unconfigured',oauthConfiguration:false}:status);
|
||||
if(cmd==='get_agent_config_statuses'||cmd==='refresh_agent_config_statuses') return ids.map(id=>({id,name:id,supportedPlatform:true,installed:!params.has('not-installed'),pluginInstalled:!params.has('no-plugin'),launchTargets:params.has('not-installed')?[]:['claude-desktop','zcode','workbuddy','antigravity-ide'].includes(id)?[{id:'app',label:id,detail:'test desktop'}]:['codex','opencode'].includes(id)&&!params.has('cli-only')?[...(params.has('app-only')?[]:[{id:'cli',label:'CLI',detail:'test CLI'}]),{id:'app',label:'APP',detail:'test desktop'}]:[{id:'cli',label:'CLI',detail:'test CLI'}],version:'1.0',cliVersion:'1.0',appVersion:null,pluginVersion:'1.0',configValid:params.get('state')!=='invalid',connectionState:params.get('state') || (currentModel?'configured':'not-configured'),configured:!!currentModel,configurationSynchronized:!!currentModel,currentModel,oauthConfiguration:id==='codex'&¤tOauth,codexNativeOauth:id==='codex'&&nativeOauth,modificationEnabled:!!currentModel,modificationState:currentModel?'applied':'unconfigured',backupAvailable:false,appliedModel:currentModel,claudeCodeModelMappings:id==='claude-code'?currentMappings[id]??null:null,claudeDesktopModelMappings:id==='claude-desktop'?currentMappings[id]??null:null,warnings:[],error:null})).map(status=>status.id==='codex'&&(nativeOauth||codexClosed)?{...status,configured:false,connectionState:'not-configured',configurationSynchronized:false,currentModel:null,appliedModel:null,modificationEnabled:false,modificationState:'unconfigured',oauthConfiguration:false}:status);
|
||||
if(cmd==='close_codex_config_modification') {
|
||||
closeCount++;
|
||||
if(params.has('fail-close')&&closeCount===1)throw new Error('模拟关闭失败,已回滚');
|
||||
|
||||
Reference in new issue
Block a user