feat(agents): allow CPA configuration for config-only clients
This commit is contained in:
1 parent
b0f69877f8
commit
286f4bbe1b
6 files changed
+98
-23
No files matched your search
@@ -896,6 +896,24 @@ fn pi_template_only_writes_configuration_and_package_references() {
|
||||
assert_eq!(list_backups("pi", &home.0).unwrap().versions.len(), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pi_config_file_allows_cpa_connection_without_detected_cli() {
|
||||
let home = Home::new();
|
||||
let paths = config_paths("pi", &home.0).unwrap();
|
||||
save(&paths[0], "{}\n");
|
||||
|
||||
let before = inspect_pi_provider_status(&home.0, 8317, "test-key");
|
||||
assert!(before.config_exists);
|
||||
assert!(!before.plugin_installed);
|
||||
|
||||
configure_pi_provider_without_cli_inner(&home.0, 8317, "test-key", "gpt-one").unwrap();
|
||||
|
||||
let connected = inspect_pi_provider_status(&home.0, 8317, "test-key");
|
||||
assert!(connected.configured);
|
||||
assert!(connected.plugin_installed);
|
||||
assert_eq!(connected.current_model.as_deref(), Some("gpt-one"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn desktop_setup_repairs_legacy_names_without_changing_other_profiles() {
|
||||
if !AgentClient::ClaudeDesktop.supported_platform() {
|
||||
|
||||
@@ -202,14 +202,23 @@ pub(crate) async fn install_pi_provider(
|
||||
.home_dir()
|
||||
.map_err(|error| format!("无法获取用户目录: {error}"))?;
|
||||
let config = gui_config_state.snapshot()?;
|
||||
let executable = find_pi_executable(&home)
|
||||
.ok_or_else(|| "未检测到 Pi CLI,请先安装 Pi 并确保 pi 命令在 PATH 中".to_string())?;
|
||||
let executable = find_pi_executable(&home);
|
||||
if executable.is_none()
|
||||
&& !inspect_pi_provider_status(&home, config.port, effective_agent_api_key(&config))
|
||||
.config_exists
|
||||
{
|
||||
return Err("未检测到 Pi CLI 或 Pi 配置文件".to_string());
|
||||
}
|
||||
let model = resolve_pi_default_model(&config, &model).await?;
|
||||
let port = config.port;
|
||||
let api_key = effective_agent_api_key(&config).to_string();
|
||||
let proxy_url = config.proxy_url.clone();
|
||||
let result = tauri::async_runtime::spawn_blocking(move || {
|
||||
install_pi_provider_inner(&home, &executable, port, &api_key, &model, &proxy_url)
|
||||
if let Some(executable) = executable {
|
||||
install_pi_provider_inner(&home, &executable, port, &api_key, &model, &proxy_url)
|
||||
} else {
|
||||
configure_pi_provider_without_cli_inner(&home, port, &api_key, &model)
|
||||
}
|
||||
})
|
||||
.await
|
||||
.map_err(|error| format!("安装 Pi CLIProxyAPI provider 任务失败: {error}"))??;
|
||||
@@ -1257,8 +1266,7 @@ pub(crate) fn validate_agent_can_enable(
|
||||
));
|
||||
}
|
||||
let detection = inspect_agent_config(client, home, port, api_key);
|
||||
let workbuddy_config_available = client == AgentClient::WorkBuddy && detection.config_exists;
|
||||
if !detection.installed && !workbuddy_config_available {
|
||||
if !detection.installed && !detection.config_exists {
|
||||
return Err(format!("{} is not installed", client.name()));
|
||||
}
|
||||
Ok(())
|
||||
|
||||
@@ -307,6 +307,20 @@ pub(crate) fn build_pi_provider_settings(
|
||||
let object = root
|
||||
.as_object_mut()
|
||||
.ok_or_else(|| "Pi settings.json 根节点必须是 JSON 对象".to_string())?;
|
||||
let packages = object
|
||||
.entry("packages")
|
||||
.or_insert_with(|| serde_json::json!([]))
|
||||
.as_array_mut()
|
||||
.ok_or_else(|| "Pi settings.json packages 必须是数组".to_string())?;
|
||||
if !packages.iter().any(|package| {
|
||||
package.as_str().is_some_and(pi_package_source_matches)
|
||||
|| package
|
||||
.get("source")
|
||||
.and_then(serde_json::Value::as_str)
|
||||
.is_some_and(pi_package_source_matches)
|
||||
}) {
|
||||
packages.push(serde_json::json!(PI_CLIPROXYAPI_PACKAGE));
|
||||
}
|
||||
object.insert(
|
||||
"defaultProvider".to_string(),
|
||||
serde_json::json!(PI_CLIPROXYAPI_PROVIDER_ID),
|
||||
@@ -389,8 +403,10 @@ pub(crate) fn inspect_pi_provider_status(
|
||||
"Pi 插件配置不完整,请使用“应用配置”重新写入凭据、默认 provider 和默认模型".to_string(),
|
||||
);
|
||||
}
|
||||
if executable.is_none() && plugin_installed {
|
||||
warnings.push("已找到 Pi 插件配置,但未检测到 Pi CLI 命令".to_string());
|
||||
if executable.is_none() && config_exists {
|
||||
warnings.push(
|
||||
"只检测到 Pi 配置文件,仍可编辑配置并接入 CPA;启动功能不可用".to_string(),
|
||||
);
|
||||
}
|
||||
let modification_state = if configured {
|
||||
"applied"
|
||||
@@ -476,6 +492,36 @@ pub(crate) fn install_pi_provider_inner(
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
pub(crate) fn configure_pi_provider_without_cli_inner(
|
||||
home: &Path,
|
||||
port: u16,
|
||||
api_key: &str,
|
||||
default_model: &str,
|
||||
) -> Result<AgentConfigActionResult, String> {
|
||||
if port == 0 {
|
||||
return Err("内核端口无效".to_string());
|
||||
}
|
||||
if api_key.trim().is_empty() {
|
||||
return Err("EasyCLIProxyAPI 没有可用的 API key".to_string());
|
||||
}
|
||||
let settings_path = pi_provider_settings_path(home);
|
||||
config_package_operation(home, "plugin-config", || {
|
||||
let settings = if settings_path.is_file() {
|
||||
fs::read_to_string(&settings_path).map_err(|error| {
|
||||
format!(
|
||||
"读取 Pi settings.json 失败 {}: {error}",
|
||||
path_to_string(&settings_path)
|
||||
)
|
||||
})?
|
||||
} else {
|
||||
"{}".to_string()
|
||||
};
|
||||
let rendered = build_pi_provider_settings(&settings, default_model)?;
|
||||
write_bytes_directly(&settings_path, rendered.as_bytes())
|
||||
})?;
|
||||
repair_pi_provider_inner(home, port, api_key, default_model)
|
||||
}
|
||||
|
||||
pub(crate) fn repair_pi_provider_inner(
|
||||
home: &Path,
|
||||
port: u16,
|
||||
@@ -1003,12 +1049,9 @@ pub(crate) fn inspect_agent_config(
|
||||
if !client.supported_platform() {
|
||||
warnings.push("当前平台不支持 Claude Desktop 3P 配置".to_string());
|
||||
} else if !installed && config_exists {
|
||||
warnings.push(if client == AgentClient::WorkBuddy {
|
||||
"只检测到 WorkBuddy 配置文件,仍可编辑配置并接入 CPA;启动功能不可用"
|
||||
.to_string()
|
||||
} else {
|
||||
"只检测到配置文件,未检测到客户端".to_string()
|
||||
});
|
||||
warnings.push(
|
||||
"只检测到配置文件,仍可编辑配置并接入 CPA;启动功能不可用".to_string(),
|
||||
);
|
||||
}
|
||||
if let Some(message) = error.as_ref() {
|
||||
warnings.push(message.clone());
|
||||
|
||||
@@ -1199,7 +1199,7 @@ export function AgentsPage({ embedded = false, onConfigurationApplied }: AgentsP
|
||||
);
|
||||
const canConfigureActiveClient = Boolean(
|
||||
activeStatus?.supportedPlatform
|
||||
&& (activeStatus.installed || (selected === 'workbuddy' && activeStatus.configExists)),
|
||||
&& (activeStatus.installed || activeStatus.configExists),
|
||||
);
|
||||
const canEnable = Boolean(
|
||||
canConfigureActiveClient
|
||||
|
||||
@@ -141,14 +141,15 @@ const path = require('node:path');
|
||||
await open(mode + 'not-installed');
|
||||
assert.ok(await button('启动 App').isDisabled());
|
||||
assert.ok(await button('重启 App').isDisabled());
|
||||
await open(mode + 'client=workbuddy&config-only&fresh');
|
||||
assert.ok(await page.locator('.agent-model-trigger').isEnabled());
|
||||
assert.ok(await button('一键接入').isEnabled());
|
||||
assert.ok(await button('启动 App').isDisabled());
|
||||
assert.ok(await button('重启 App').isDisabled());
|
||||
await button('一键接入').click();
|
||||
await page.waitForFunction(() => window.fixtureCalls.some(call => call.cmd === 'update_agent_config'));
|
||||
assert.deepEqual((await calls('update_agent_config'))[0].args.client, 'workbuddy');
|
||||
for (const client of ['workbuddy', 'openclaw']) {
|
||||
await open(mode + `client=${client}&config-only=${client}&fresh`);
|
||||
assert.ok(await page.locator('.agent-model-trigger').isEnabled());
|
||||
assert.ok(await button('一键接入').isEnabled());
|
||||
assert.ok(await page.locator('.agent-launch-actions button').first().isDisabled());
|
||||
await button('一键接入').click();
|
||||
await page.waitForFunction(() => window.fixtureCalls.some(call => call.cmd === 'update_agent_config'));
|
||||
assert.deepEqual((await calls('update_agent_config'))[0].args.client, client);
|
||||
}
|
||||
await open(mode + 'client=kimi-code');
|
||||
assert.equal(await button('重启 App').count(), 0);
|
||||
|
||||
@@ -199,6 +200,11 @@ const path = require('node:path');
|
||||
assert.equal(await button('手动备份').count(), 0);
|
||||
await open(mode + 'client=pi&no-plugin');
|
||||
assert.ok(await button('安装 Pi 插件').isEnabled());
|
||||
await open(mode + 'client=pi&config-only=pi&no-plugin&fresh');
|
||||
assert.ok(await button('安装 Pi 插件').isEnabled());
|
||||
assert.ok(await button('启动 CLI').isDisabled());
|
||||
await button('安装 Pi 插件').click();
|
||||
await page.waitForFunction(() => window.fixtureCalls.some(call => call.cmd === 'install_pi_provider'));
|
||||
|
||||
await open(mode + 'client=deepseek-harness&running');
|
||||
await button('重启 Web').click();
|
||||
|
||||
Vendored
+1
-1
@@ -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')&&!(params.has('config-only')&&id==='workbuddy'),pluginInstalled:!params.has('no-plugin'),launchTargets:params.has('not-installed')||(params.has('config-only')&&id==='workbuddy')?[]:['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',configExists:!params.has('not-installed')||(params.has('config-only')&&id==='workbuddy'),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')&¶ms.get('config-only')!==id,pluginInstalled:!params.has('no-plugin'),launchTargets:params.has('not-installed')||params.get('config-only')===id?[]:['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',configExists:!params.has('not-installed')||params.get('config-only')===id,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