diff --git a/src-tauri/src/agents/commands.rs b/src-tauri/src/agents/commands.rs index bb93e7d..d01bb5b 100644 --- a/src-tauri/src/agents/commands.rs +++ b/src-tauri/src/agents/commands.rs @@ -1257,7 +1257,8 @@ pub(crate) fn validate_agent_can_enable( )); } let detection = inspect_agent_config(client, home, port, api_key); - if !detection.installed { + let workbuddy_config_available = client == AgentClient::WorkBuddy && detection.config_exists; + if !detection.installed && !workbuddy_config_available { return Err(format!("{} is not installed", client.name())); } Ok(()) diff --git a/src-tauri/src/agents/discovery.rs b/src-tauri/src/agents/discovery.rs index 6b466a4..855d075 100644 --- a/src-tauri/src/agents/discovery.rs +++ b/src-tauri/src/agents/discovery.rs @@ -1003,7 +1003,12 @@ pub(crate) fn inspect_agent_config( if !client.supported_platform() { warnings.push("当前平台不支持 Claude Desktop 3P 配置".to_string()); } else if !installed && config_exists { - warnings.push("只检测到配置文件,未检测到客户端".to_string()); + warnings.push(if client == AgentClient::WorkBuddy { + "只检测到 WorkBuddy 配置文件,仍可编辑配置并接入 CPA;启动功能不可用" + .to_string() + } else { + "只检测到配置文件,未检测到客户端".to_string() + }); } if let Some(message) = error.as_ref() { warnings.push(message.clone()); diff --git a/src-tauri/src/agents/workbuddy/tests.rs b/src-tauri/src/agents/workbuddy/tests.rs index e3059de..dbe2ec6 100644 --- a/src-tauri/src/agents/workbuddy/tests.rs +++ b/src-tauri/src/agents/workbuddy/tests.rs @@ -181,6 +181,31 @@ fn workbuddy_status_checks_endpoint_key_visibility_and_tool_support() { ); } +#[test] +fn workbuddy_config_file_allows_cpa_connection_without_detected_application() { + let home = Home::new(); + home.save(&json!({})); + + let status = inspect_agent_config(AgentClient::WorkBuddy, &home.0, 8317, "test-key"); + assert!(!status.installed); + assert!(status.config_exists); + assert!(status.config_valid); + assert!(status.launch_targets.is_empty()); + assert!(validate_agent_can_enable( + AgentClient::WorkBuddy, + &home.0, + 8317, + "test-key" + ) + .is_ok()); + + home.apply("gpt-one").unwrap(); + let connected = inspect_agent_config(AgentClient::WorkBuddy, &home.0, 8317, "test-key"); + assert!(!connected.installed); + assert!(connected.configured); + assert_eq!(connected.current_model.as_deref(), Some("gpt-one")); +} + #[test] fn workbuddy_visibility_restores_original_references_and_keeps_user_edits() { for external_edit in [false, true] { diff --git a/src/pages/AgentsPage.tsx b/src/pages/AgentsPage.tsx index 1a883ef..8619ab1 100644 --- a/src/pages/AgentsPage.tsx +++ b/src/pages/AgentsPage.tsx @@ -102,6 +102,7 @@ type AgentConfigStatus = { cliVersion: string | null; appVersion: string | null; pluginVersion: string | null; + configExists: boolean; configValid: boolean; configured: boolean; connectionState: 'configured' | 'not-configured' | 'needs-update' | 'invalid'; @@ -1196,9 +1197,12 @@ export function AgentsPage({ embedded = false, onConfigurationApplied }: AgentsP && claudeModelMappingsDraft.autoCompactPct >= 1 && claudeModelMappingsDraft.autoCompactPct <= 100 ); - const canEnable = Boolean( + const canConfigureActiveClient = Boolean( activeStatus?.supportedPlatform - && activeStatus.installed + && (activeStatus.installed || (selected === 'workbuddy' && activeStatus.configExists)), + ); + const canEnable = Boolean( + canConfigureActiveClient && !modelLoading && (isClaudeModelMappingClient ? claudeMappingsReady && claudeCodeRuntimeSettingsReady @@ -2091,7 +2095,7 @@ export function AgentsPage({ embedded = false, onConfigurationApplied }: AgentsP value={isClaudeModelMappingClient ? claudeModelMappingsDraft.sonnet : selectedModel} loading={modelLoading} error={modelError} - disabled={busy || !activeStatus?.installed || !activeStatus.supportedPlatform} + disabled={busy || !canConfigureActiveClient} onChange={selectEmbeddedModel} onRefresh={refreshModels} /> @@ -2187,7 +2191,7 @@ export function AgentsPage({ embedded = false, onConfigurationApplied }: AgentsP value={selectedModel} loading={modelLoading} error={modelError} - disabled={busy || !activeStatus?.installed || !activeStatus.supportedPlatform} + disabled={busy || !canConfigureActiveClient} onChange={selectModel} onRefresh={refreshModels} /> diff --git a/tests/agent-management-ui.cjs b/tests/agent-management-ui.cjs index 71d5c3e..524aef3 100644 --- a/tests/agent-management-ui.cjs +++ b/tests/agent-management-ui.cjs @@ -141,6 +141,14 @@ 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'); await open(mode + 'client=kimi-code'); assert.equal(await button('重启 App').count(), 0); diff --git a/tests/fixtures/agent-backups.tsx b/tests/fixtures/agent-backups.tsx index a8111b7..a2dc5bd 100644 --- a/tests/fixtures/agent-backups.tsx +++ b/tests/fixtures/agent-backups.tsx @@ -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')&&!(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==='close_codex_config_modification') { closeCount++; if(params.has('fail-close')&&closeCount===1)throw new Error('模拟关闭失败,已回滚');