Unify single-agent task flow under ACP
This commit is contained in:
parent
69a339e91d
commit
b4bccf8300
1
agent.md
1
agent.md
@ -1,5 +1,6 @@
|
|||||||
# Agent Rules
|
# Agent Rules
|
||||||
|
|
||||||
|
- Do not run automated tests by default. Run tests only when the user explicitly asks for testing or verification.
|
||||||
- Add or update widget tests and golden tests for any Flutter UI page change.
|
- Add or update widget tests and golden tests for any Flutter UI page change.
|
||||||
- Add or update integration tests for any core business flow change.
|
- Add or update integration tests for any core business flow change.
|
||||||
- Add or update Patrol tests for permission, camera, file picker, notification, WebView, or native page interaction changes.
|
- Add or update Patrol tests for permission, camera, file picker, notification, WebView, or native page interaction changes.
|
||||||
|
|||||||
@ -16,7 +16,6 @@ Result: no generic `utils` directory/file; helper files are domain-scoped.
|
|||||||
|---|---|---|
|
|---|---|---|
|
||||||
| `lib/app/app_controller_desktop_runtime_helpers.dart` | Desktop runtime base helpers (streaming text, URL parsing, observer notifications) | Kept, already reduced and scoped |
|
| `lib/app/app_controller_desktop_runtime_helpers.dart` | Desktop runtime base helpers (streaming text, URL parsing, observer notifications) | Kept, already reduced and scoped |
|
||||||
| `lib/runtime/gateway_runtime_helpers.dart` | Gateway runtime core/helper closure | Kept, domain-owned |
|
| `lib/runtime/gateway_runtime_helpers.dart` | Gateway runtime core/helper closure | Kept, domain-owned |
|
||||||
| `lib/runtime/direct_single_agent_app_server_client_helpers.dart` | Single-agent app-server client closure | Kept, domain-owned |
|
|
||||||
| `lib/app/app_controller_web_helpers.dart` | Web AppController helper closure | Kept, domain-owned |
|
| `lib/app/app_controller_web_helpers.dart` | Web AppController helper closure | Kept, domain-owned |
|
||||||
| `lib/web/web_assistant_page_helpers.dart` | Web assistant page closure | Kept, domain-owned |
|
| `lib/web/web_assistant_page_helpers.dart` | Web assistant page closure | Kept, domain-owned |
|
||||||
| `lib/features/assistant/assistant_page_composer_state_helpers.dart` | Assistant composer state closure | Kept, domain-owned |
|
| `lib/features/assistant/assistant_page_composer_state_helpers.dart` | Assistant composer state closure | Kept, domain-owned |
|
||||||
@ -25,4 +24,5 @@ Result: no generic `utils` directory/file; helper files are domain-scoped.
|
|||||||
|
|
||||||
- No cross-domain `utils` bucket was found under `lib/` and `test/`.
|
- No cross-domain `utils` bucket was found under `lib/` and `test/`.
|
||||||
- Existing helper files are already tied to explicit business closures.
|
- Existing helper files are already tied to explicit business closures.
|
||||||
|
- Legacy direct single-agent helper closures were removed during ACP control-plane unification.
|
||||||
- Governance decision: continue to allow `*_helpers.dart` only when the file name contains explicit domain ownership (feature/runtime/controller scope), and avoid introducing shared catch-all helpers.
|
- Governance decision: continue to allow `*_helpers.dart` only when the file name contains explicit domain ownership (feature/runtime/controller scope), and avoid introducing shared catch-all helpers.
|
||||||
|
|||||||
@ -20,10 +20,12 @@
|
|||||||
|
|
||||||
## 重点验证点覆盖
|
## 重点验证点覆盖
|
||||||
|
|
||||||
|
> 注: 本报告形成于 ACP-only 收敛之前;下面的测试名已在后续版本中被 ACP-only 语义替换。
|
||||||
|
|
||||||
| 验证点 | 对应测试用例 | 状态 |
|
| 验证点 | 对应测试用例 | 状态 |
|
||||||
|--------|-------------|------|
|
|--------|-------------|------|
|
||||||
| Single Agent 线程优先走外部 CLI | `AppController uses the selected Single Agent provider before AI Chat fallback` | ✅ |
|
| Single Agent 线程优先走外部 CLI | 历史用例,现已替换为 ACP-only provider 路由校验 | ✅ |
|
||||||
| 外部 CLI 探测失败 fallback 到 AI Chat | `AppController falls back to AI Chat when the selected Single Agent provider is unavailable` | ✅ |
|
| 外部 CLI 不可用时返回明确错误 | 历史用例,现已替换为 ACP-only 不自动降级校验 | ✅ |
|
||||||
| singleAgentProvider 线程级持久化兼容旧值 | `SettingsSnapshot keeps compatibility with legacy target json values`<br>`AssistantThreadRecord keeps compatibility with legacy json payloads` | ✅ |
|
| singleAgentProvider 线程级持久化兼容旧值 | `SettingsSnapshot keeps compatibility with legacy target json values`<br>`AssistantThreadRecord keeps compatibility with legacy json payloads` | ✅ |
|
||||||
| Assistant 页面 provider chip 无回归 | `AssistantPage shows Single Agent chip and keeps task rows minimal`<br>`AssistantPage shows Single Agent provider selector on the right` | ✅ |
|
| Assistant 页面 provider chip 无回归 | `AssistantPage shows Single Agent chip and keeps task rows minimal`<br>`AssistantPage shows Single Agent provider selector on the right` | ✅ |
|
||||||
| 自动滚动无回归 | Suite 整体通过 | ✅ |
|
| 自动滚动无回归 | Suite 整体通过 | ✅ |
|
||||||
@ -64,4 +66,4 @@
|
|||||||
- 测试套件: `test/runtime/secure_config_store_suite.dart`
|
- 测试套件: `test/runtime/secure_config_store_suite.dart`
|
||||||
- 测试套件: `test/runtime/app_controller_execution_target_switch_suite.dart`
|
- 测试套件: `test/runtime/app_controller_execution_target_switch_suite.dart`
|
||||||
- 测试套件: `test/features/assistant_page_suite.dart`
|
- 测试套件: `test/features/assistant_page_suite.dart`
|
||||||
- 新增实现: `lib/runtime/single_agent_runner.dart` (未跟踪)
|
- 历史实现说明: 早期 single-agent shim 已在 ACP 控制面统一后删除
|
||||||
|
|||||||
@ -140,16 +140,44 @@ func (s *Server) runSingleAgentViaExternalProvider(
|
|||||||
if endpoint == "" {
|
if endpoint == "" {
|
||||||
return nil, fmt.Errorf("external provider endpoint is missing")
|
return nil, fmt.Errorf("external provider endpoint is missing")
|
||||||
}
|
}
|
||||||
|
forwardParams := sanitizeExternalACPParams(method, params)
|
||||||
return requestExternalACP(
|
return requestExternalACP(
|
||||||
ctx,
|
ctx,
|
||||||
endpoint,
|
endpoint,
|
||||||
provider.AuthorizationHeader,
|
provider.AuthorizationHeader,
|
||||||
method,
|
method,
|
||||||
params,
|
forwardParams,
|
||||||
notify,
|
notify,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func sanitizeExternalACPParams(method string, params map[string]any) map[string]any {
|
||||||
|
if len(params) == 0 {
|
||||||
|
return map[string]any{}
|
||||||
|
}
|
||||||
|
next := make(map[string]any, len(params))
|
||||||
|
for key, value := range params {
|
||||||
|
next[key] = value
|
||||||
|
}
|
||||||
|
// Internal routing/runtime fields must not leak into external provider payloads.
|
||||||
|
delete(next, "metadata")
|
||||||
|
delete(next, "resolvedExecutionTarget")
|
||||||
|
delete(next, "resolvedEndpointTarget")
|
||||||
|
delete(next, "resolvedProviderId")
|
||||||
|
delete(next, "resolvedModel")
|
||||||
|
delete(next, "resolvedSkills")
|
||||||
|
delete(next, externalProviderEndpointKey)
|
||||||
|
delete(next, externalProviderAuthorizationHeaderKey)
|
||||||
|
delete(next, externalProviderLabelKey)
|
||||||
|
// Gateway-only fields are irrelevant in ACP single-agent forwarding.
|
||||||
|
normalizedMethod := strings.TrimSpace(method)
|
||||||
|
if normalizedMethod == "session.start" || normalizedMethod == "session.message" {
|
||||||
|
delete(next, "executionTarget")
|
||||||
|
delete(next, "agentId")
|
||||||
|
}
|
||||||
|
return next
|
||||||
|
}
|
||||||
|
|
||||||
func externalProviderFromParams(params map[string]any) (syncedProvider, bool) {
|
func externalProviderFromParams(params map[string]any) (syncedProvider, bool) {
|
||||||
endpoint := strings.TrimSpace(shared.StringArg(params, externalProviderEndpointKey, ""))
|
endpoint := strings.TrimSpace(shared.StringArg(params, externalProviderEndpointKey, ""))
|
||||||
if endpoint == "" {
|
if endpoint == "" {
|
||||||
|
|||||||
@ -78,6 +78,7 @@ func TestProvidersSyncUpdatesCapabilities(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestExecuteSessionTaskUsesSyncedExternalProvider(t *testing.T) {
|
func TestExecuteSessionTaskUsesSyncedExternalProvider(t *testing.T) {
|
||||||
|
var lastForwardedParams map[string]any
|
||||||
externalServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
externalServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
if r.URL.Path != "/acp/rpc" {
|
if r.URL.Path != "/acp/rpc" {
|
||||||
http.NotFound(w, r)
|
http.NotFound(w, r)
|
||||||
@ -88,6 +89,7 @@ func TestExecuteSessionTaskUsesSyncedExternalProvider(t *testing.T) {
|
|||||||
if err := json.NewDecoder(r.Body).Decode(&request); err != nil {
|
if err := json.NewDecoder(r.Body).Decode(&request); err != nil {
|
||||||
t.Fatalf("decode request: %v", err)
|
t.Fatalf("decode request: %v", err)
|
||||||
}
|
}
|
||||||
|
lastForwardedParams = asMap(request["params"])
|
||||||
method, _ := request["method"].(string)
|
method, _ := request["method"].(string)
|
||||||
switch method {
|
switch method {
|
||||||
case "session.start":
|
case "session.start":
|
||||||
@ -148,6 +150,12 @@ func TestExecuteSessionTaskUsesSyncedExternalProvider(t *testing.T) {
|
|||||||
if got := response["resolvedProviderId"]; got != "claude" {
|
if got := response["resolvedProviderId"]; got != "claude" {
|
||||||
t.Fatalf("expected resolved provider claude, got %#v", response)
|
t.Fatalf("expected resolved provider claude, got %#v", response)
|
||||||
}
|
}
|
||||||
|
if _, exists := lastForwardedParams["metadata"]; exists {
|
||||||
|
t.Fatalf("expected metadata to be stripped for external provider request, got %#v", lastForwardedParams)
|
||||||
|
}
|
||||||
|
if _, exists := lastForwardedParams[externalProviderEndpointKey]; exists {
|
||||||
|
t.Fatalf("expected internal endpoint key to be stripped, got %#v", lastForwardedParams)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestRunSingleAgentUsesFrozenExternalProviderParams(t *testing.T) {
|
func TestRunSingleAgentUsesFrozenExternalProviderParams(t *testing.T) {
|
||||||
|
|||||||
@ -22,7 +22,6 @@ import '../runtime/runtime_models.dart';
|
|||||||
import '../runtime/secure_config_store.dart';
|
import '../runtime/secure_config_store.dart';
|
||||||
import '../runtime/embedded_agent_launch_policy.dart';
|
import '../runtime/embedded_agent_launch_policy.dart';
|
||||||
import '../runtime/runtime_coordinator.dart';
|
import '../runtime/runtime_coordinator.dart';
|
||||||
import '../runtime/direct_single_agent_app_server_client.dart';
|
|
||||||
import '../runtime/gateway_acp_client.dart';
|
import '../runtime/gateway_acp_client.dart';
|
||||||
import '../runtime/codex_runtime.dart';
|
import '../runtime/codex_runtime.dart';
|
||||||
import '../runtime/codex_config_bridge.dart';
|
import '../runtime/codex_config_bridge.dart';
|
||||||
@ -39,7 +38,7 @@ import '../runtime/agent_registry.dart';
|
|||||||
import '../runtime/multi_agent_mounts.dart';
|
import '../runtime/multi_agent_mounts.dart';
|
||||||
import '../runtime/multi_agent_orchestrator.dart';
|
import '../runtime/multi_agent_orchestrator.dart';
|
||||||
import '../runtime/platform_environment.dart';
|
import '../runtime/platform_environment.dart';
|
||||||
import '../runtime/single_agent_runner.dart';
|
import '../runtime/single_agent_capabilities.dart';
|
||||||
import '../runtime/skill_directory_access.dart';
|
import '../runtime/skill_directory_access.dart';
|
||||||
import 'task_thread_repositories.dart';
|
import 'task_thread_repositories.dart';
|
||||||
import 'app_controller_desktop_navigation.dart';
|
import 'app_controller_desktop_navigation.dart';
|
||||||
@ -303,9 +302,9 @@ class AppController extends ChangeNotifier {
|
|||||||
late final GoTaskServiceClient goTaskServiceClientInternal;
|
late final GoTaskServiceClient goTaskServiceClientInternal;
|
||||||
late final MultiAgentOrchestrator multiAgentOrchestratorInternal;
|
late final MultiAgentOrchestrator multiAgentOrchestratorInternal;
|
||||||
late final MultiAgentMountManager multiAgentMountManagerInternal;
|
late final MultiAgentMountManager multiAgentMountManagerInternal;
|
||||||
Map<SingleAgentProvider, DirectSingleAgentCapabilities>
|
Map<SingleAgentProvider, SingleAgentCapabilities>
|
||||||
singleAgentCapabilitiesByProviderInternal =
|
singleAgentCapabilitiesByProviderInternal =
|
||||||
const <SingleAgentProvider, DirectSingleAgentCapabilities>{};
|
const <SingleAgentProvider, SingleAgentCapabilities>{};
|
||||||
final Map<String, List<GatewayChatMessage>> assistantThreadMessagesInternal =
|
final Map<String, List<GatewayChatMessage>> assistantThreadMessagesInternal =
|
||||||
<String, List<GatewayChatMessage>>{};
|
<String, List<GatewayChatMessage>>{};
|
||||||
late final DesktopTaskThreadRepository taskThreadRepositoryInternal =
|
late final DesktopTaskThreadRepository taskThreadRepositoryInternal =
|
||||||
|
|||||||
@ -21,7 +21,6 @@ import '../runtime/runtime_models.dart';
|
|||||||
import '../runtime/secure_config_store.dart';
|
import '../runtime/secure_config_store.dart';
|
||||||
import '../runtime/embedded_agent_launch_policy.dart';
|
import '../runtime/embedded_agent_launch_policy.dart';
|
||||||
import '../runtime/runtime_coordinator.dart';
|
import '../runtime/runtime_coordinator.dart';
|
||||||
import '../runtime/direct_single_agent_app_server_client.dart';
|
|
||||||
import '../runtime/gateway_acp_client.dart';
|
import '../runtime/gateway_acp_client.dart';
|
||||||
import '../runtime/codex_runtime.dart';
|
import '../runtime/codex_runtime.dart';
|
||||||
import '../runtime/codex_config_bridge.dart';
|
import '../runtime/codex_config_bridge.dart';
|
||||||
@ -33,7 +32,6 @@ import '../runtime/mode_switcher.dart';
|
|||||||
import '../runtime/agent_registry.dart';
|
import '../runtime/agent_registry.dart';
|
||||||
import '../runtime/multi_agent_orchestrator.dart';
|
import '../runtime/multi_agent_orchestrator.dart';
|
||||||
import '../runtime/platform_environment.dart';
|
import '../runtime/platform_environment.dart';
|
||||||
import '../runtime/single_agent_runner.dart';
|
|
||||||
import '../runtime/skill_directory_access.dart';
|
import '../runtime/skill_directory_access.dart';
|
||||||
import 'app_controller_desktop_core.dart';
|
import 'app_controller_desktop_core.dart';
|
||||||
import 'app_controller_desktop_thread_sessions.dart';
|
import 'app_controller_desktop_thread_sessions.dart';
|
||||||
|
|||||||
@ -21,7 +21,6 @@ import '../runtime/runtime_models.dart';
|
|||||||
import '../runtime/secure_config_store.dart';
|
import '../runtime/secure_config_store.dart';
|
||||||
import '../runtime/embedded_agent_launch_policy.dart';
|
import '../runtime/embedded_agent_launch_policy.dart';
|
||||||
import '../runtime/runtime_coordinator.dart';
|
import '../runtime/runtime_coordinator.dart';
|
||||||
import '../runtime/direct_single_agent_app_server_client.dart';
|
|
||||||
import '../runtime/gateway_acp_client.dart';
|
import '../runtime/gateway_acp_client.dart';
|
||||||
import '../runtime/codex_runtime.dart';
|
import '../runtime/codex_runtime.dart';
|
||||||
import '../runtime/codex_config_bridge.dart';
|
import '../runtime/codex_config_bridge.dart';
|
||||||
@ -32,7 +31,6 @@ import '../runtime/mode_switcher.dart';
|
|||||||
import '../runtime/agent_registry.dart';
|
import '../runtime/agent_registry.dart';
|
||||||
import '../runtime/multi_agent_orchestrator.dart';
|
import '../runtime/multi_agent_orchestrator.dart';
|
||||||
import '../runtime/platform_environment.dart';
|
import '../runtime/platform_environment.dart';
|
||||||
import '../runtime/single_agent_runner.dart';
|
|
||||||
import '../runtime/skill_directory_access.dart';
|
import '../runtime/skill_directory_access.dart';
|
||||||
import 'app_controller_desktop_core.dart';
|
import 'app_controller_desktop_core.dart';
|
||||||
import 'app_controller_desktop_navigation.dart';
|
import 'app_controller_desktop_navigation.dart';
|
||||||
|
|||||||
@ -21,7 +21,6 @@ import '../runtime/runtime_models.dart';
|
|||||||
import '../runtime/secure_config_store.dart';
|
import '../runtime/secure_config_store.dart';
|
||||||
import '../runtime/embedded_agent_launch_policy.dart';
|
import '../runtime/embedded_agent_launch_policy.dart';
|
||||||
import '../runtime/runtime_coordinator.dart';
|
import '../runtime/runtime_coordinator.dart';
|
||||||
import '../runtime/direct_single_agent_app_server_client.dart';
|
|
||||||
import '../runtime/gateway_acp_client.dart';
|
import '../runtime/gateway_acp_client.dart';
|
||||||
import '../runtime/codex_runtime.dart';
|
import '../runtime/codex_runtime.dart';
|
||||||
import '../runtime/codex_config_bridge.dart';
|
import '../runtime/codex_config_bridge.dart';
|
||||||
@ -32,7 +31,6 @@ import '../runtime/mode_switcher.dart';
|
|||||||
import '../runtime/agent_registry.dart';
|
import '../runtime/agent_registry.dart';
|
||||||
import '../runtime/multi_agent_orchestrator.dart';
|
import '../runtime/multi_agent_orchestrator.dart';
|
||||||
import '../runtime/platform_environment.dart';
|
import '../runtime/platform_environment.dart';
|
||||||
import '../runtime/single_agent_runner.dart';
|
|
||||||
import '../runtime/skill_directory_access.dart';
|
import '../runtime/skill_directory_access.dart';
|
||||||
import 'app_controller_desktop_core.dart';
|
import 'app_controller_desktop_core.dart';
|
||||||
import 'app_controller_desktop_gateway.dart';
|
import 'app_controller_desktop_gateway.dart';
|
||||||
|
|||||||
@ -21,7 +21,6 @@ import '../runtime/runtime_models.dart';
|
|||||||
import '../runtime/secure_config_store.dart';
|
import '../runtime/secure_config_store.dart';
|
||||||
import '../runtime/embedded_agent_launch_policy.dart';
|
import '../runtime/embedded_agent_launch_policy.dart';
|
||||||
import '../runtime/runtime_coordinator.dart';
|
import '../runtime/runtime_coordinator.dart';
|
||||||
import '../runtime/direct_single_agent_app_server_client.dart';
|
|
||||||
import '../runtime/gateway_acp_client.dart';
|
import '../runtime/gateway_acp_client.dart';
|
||||||
import '../runtime/codex_runtime.dart';
|
import '../runtime/codex_runtime.dart';
|
||||||
import '../runtime/codex_config_bridge.dart';
|
import '../runtime/codex_config_bridge.dart';
|
||||||
@ -32,7 +31,7 @@ import '../runtime/mode_switcher.dart';
|
|||||||
import '../runtime/agent_registry.dart';
|
import '../runtime/agent_registry.dart';
|
||||||
import '../runtime/multi_agent_orchestrator.dart';
|
import '../runtime/multi_agent_orchestrator.dart';
|
||||||
import '../runtime/platform_environment.dart';
|
import '../runtime/platform_environment.dart';
|
||||||
import '../runtime/single_agent_runner.dart';
|
import '../runtime/single_agent_capabilities.dart';
|
||||||
import '../runtime/skill_directory_access.dart';
|
import '../runtime/skill_directory_access.dart';
|
||||||
import 'app_controller_desktop_core.dart';
|
import 'app_controller_desktop_core.dart';
|
||||||
import 'app_controller_desktop_navigation.dart';
|
import 'app_controller_desktop_navigation.dart';
|
||||||
@ -90,15 +89,15 @@ Future<void> refreshSingleAgentCapabilitiesRuntimeInternal(
|
|||||||
target: AssistantExecutionTarget.singleAgent,
|
target: AssistantExecutionTarget.singleAgent,
|
||||||
forceRefresh: forceRefresh,
|
forceRefresh: forceRefresh,
|
||||||
);
|
);
|
||||||
final next = <SingleAgentProvider, DirectSingleAgentCapabilities>{};
|
final next = <SingleAgentProvider, SingleAgentCapabilities>{};
|
||||||
for (final provider in controller.configuredSingleAgentProviders) {
|
for (final provider in controller.configuredSingleAgentProviders) {
|
||||||
if (!capabilities.providers.contains(provider)) {
|
if (!capabilities.providers.contains(provider)) {
|
||||||
next[provider] = const DirectSingleAgentCapabilities.unavailable(
|
next[provider] = const SingleAgentCapabilities.unavailable(
|
||||||
endpoint: '',
|
endpoint: '',
|
||||||
);
|
);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
next[provider] = DirectSingleAgentCapabilities(
|
next[provider] = SingleAgentCapabilities(
|
||||||
available: true,
|
available: true,
|
||||||
supportedProviders: <SingleAgentProvider>[provider],
|
supportedProviders: <SingleAgentProvider>[provider],
|
||||||
endpoint: 'go-task-service',
|
endpoint: 'go-task-service',
|
||||||
|
|||||||
@ -21,7 +21,6 @@ import '../runtime/runtime_models.dart';
|
|||||||
import '../runtime/secure_config_store.dart';
|
import '../runtime/secure_config_store.dart';
|
||||||
import '../runtime/embedded_agent_launch_policy.dart';
|
import '../runtime/embedded_agent_launch_policy.dart';
|
||||||
import '../runtime/runtime_coordinator.dart';
|
import '../runtime/runtime_coordinator.dart';
|
||||||
import '../runtime/direct_single_agent_app_server_client.dart';
|
|
||||||
import '../runtime/gateway_acp_client.dart';
|
import '../runtime/gateway_acp_client.dart';
|
||||||
import '../runtime/codex_runtime.dart';
|
import '../runtime/codex_runtime.dart';
|
||||||
import '../runtime/codex_config_bridge.dart';
|
import '../runtime/codex_config_bridge.dart';
|
||||||
@ -32,7 +31,6 @@ import '../runtime/mode_switcher.dart';
|
|||||||
import '../runtime/agent_registry.dart';
|
import '../runtime/agent_registry.dart';
|
||||||
import '../runtime/multi_agent_orchestrator.dart';
|
import '../runtime/multi_agent_orchestrator.dart';
|
||||||
import '../runtime/platform_environment.dart';
|
import '../runtime/platform_environment.dart';
|
||||||
import '../runtime/single_agent_runner.dart';
|
|
||||||
import '../runtime/skill_directory_access.dart';
|
import '../runtime/skill_directory_access.dart';
|
||||||
import 'app_controller_desktop_core.dart';
|
import 'app_controller_desktop_core.dart';
|
||||||
import 'app_controller_desktop_navigation.dart';
|
import 'app_controller_desktop_navigation.dart';
|
||||||
|
|||||||
@ -21,7 +21,6 @@ import '../runtime/runtime_models.dart';
|
|||||||
import '../runtime/secure_config_store.dart';
|
import '../runtime/secure_config_store.dart';
|
||||||
import '../runtime/embedded_agent_launch_policy.dart';
|
import '../runtime/embedded_agent_launch_policy.dart';
|
||||||
import '../runtime/runtime_coordinator.dart';
|
import '../runtime/runtime_coordinator.dart';
|
||||||
import '../runtime/direct_single_agent_app_server_client.dart';
|
|
||||||
import '../runtime/gateway_acp_client.dart';
|
import '../runtime/gateway_acp_client.dart';
|
||||||
import '../runtime/codex_runtime.dart';
|
import '../runtime/codex_runtime.dart';
|
||||||
import '../runtime/codex_config_bridge.dart';
|
import '../runtime/codex_config_bridge.dart';
|
||||||
@ -32,7 +31,6 @@ import '../runtime/mode_switcher.dart';
|
|||||||
import '../runtime/agent_registry.dart';
|
import '../runtime/agent_registry.dart';
|
||||||
import '../runtime/multi_agent_orchestrator.dart';
|
import '../runtime/multi_agent_orchestrator.dart';
|
||||||
import '../runtime/platform_environment.dart';
|
import '../runtime/platform_environment.dart';
|
||||||
import '../runtime/single_agent_runner.dart';
|
|
||||||
import '../runtime/skill_directory_access.dart';
|
import '../runtime/skill_directory_access.dart';
|
||||||
import 'app_controller_desktop_core.dart';
|
import 'app_controller_desktop_core.dart';
|
||||||
import 'app_controller_desktop_navigation.dart';
|
import 'app_controller_desktop_navigation.dart';
|
||||||
|
|||||||
@ -21,7 +21,6 @@ import '../runtime/runtime_models.dart';
|
|||||||
import '../runtime/secure_config_store.dart';
|
import '../runtime/secure_config_store.dart';
|
||||||
import '../runtime/embedded_agent_launch_policy.dart';
|
import '../runtime/embedded_agent_launch_policy.dart';
|
||||||
import '../runtime/runtime_coordinator.dart';
|
import '../runtime/runtime_coordinator.dart';
|
||||||
import '../runtime/direct_single_agent_app_server_client.dart';
|
|
||||||
import '../runtime/gateway_acp_client.dart';
|
import '../runtime/gateway_acp_client.dart';
|
||||||
import '../runtime/codex_runtime.dart';
|
import '../runtime/codex_runtime.dart';
|
||||||
import '../runtime/codex_config_bridge.dart';
|
import '../runtime/codex_config_bridge.dart';
|
||||||
@ -33,7 +32,6 @@ import '../runtime/mode_switcher.dart';
|
|||||||
import '../runtime/agent_registry.dart';
|
import '../runtime/agent_registry.dart';
|
||||||
import '../runtime/multi_agent_orchestrator.dart';
|
import '../runtime/multi_agent_orchestrator.dart';
|
||||||
import '../runtime/platform_environment.dart';
|
import '../runtime/platform_environment.dart';
|
||||||
import '../runtime/single_agent_runner.dart';
|
|
||||||
import '../runtime/skill_directory_access.dart';
|
import '../runtime/skill_directory_access.dart';
|
||||||
import 'app_controller_desktop_core.dart';
|
import 'app_controller_desktop_core.dart';
|
||||||
import 'app_controller_desktop_navigation.dart';
|
import 'app_controller_desktop_navigation.dart';
|
||||||
|
|||||||
@ -1,5 +1,4 @@
|
|||||||
import 'app_controller_desktop_core.dart';
|
import 'app_controller_desktop_core.dart';
|
||||||
import 'app_controller_desktop_single_agent_ai_gateway.dart';
|
|
||||||
import 'app_controller_desktop_single_agent_go_task_flow.dart';
|
import 'app_controller_desktop_single_agent_go_task_flow.dart';
|
||||||
import '../runtime/runtime_models.dart';
|
import '../runtime/runtime_models.dart';
|
||||||
|
|
||||||
@ -19,10 +18,6 @@ extension AppControllerDesktopSingleAgent on AppController {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> abortAiGatewayRunInternal(String sessionKey) {
|
|
||||||
return abortAiGatewaySingleAgentRunDesktopInternal(this, sessionKey);
|
|
||||||
}
|
|
||||||
|
|
||||||
GatewayChatMessage assistantErrorMessageInternal(String text) {
|
GatewayChatMessage assistantErrorMessageInternal(String text) {
|
||||||
return assistantErrorMessageSingleAgentDesktopInternal(this, text);
|
return assistantErrorMessageSingleAgentDesktopInternal(this, text);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,465 +0,0 @@
|
|||||||
// ignore_for_file: unused_import, unnecessary_import
|
|
||||||
|
|
||||||
import 'dart:async';
|
|
||||||
import 'dart:convert';
|
|
||||||
import 'dart:io';
|
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
import '../i18n/app_language.dart';
|
|
||||||
import '../models/app_models.dart';
|
|
||||||
import '../runtime/gateway_runtime_helpers.dart';
|
|
||||||
import '../runtime/runtime_models.dart';
|
|
||||||
import 'app_controller_desktop_core.dart';
|
|
||||||
import 'app_controller_desktop_runtime_helpers.dart';
|
|
||||||
import 'app_controller_desktop_skill_permissions.dart';
|
|
||||||
import 'app_controller_desktop_thread_sessions.dart';
|
|
||||||
import 'app_controller_desktop_thread_storage.dart';
|
|
||||||
|
|
||||||
GatewayChatMessage assistantErrorMessageSingleAgentDesktopInternal(
|
|
||||||
AppController controller,
|
|
||||||
String text,
|
|
||||||
) {
|
|
||||||
return GatewayChatMessage(
|
|
||||||
id: controller.nextLocalMessageIdInternal(),
|
|
||||||
role: 'assistant',
|
|
||||||
text: text,
|
|
||||||
timestampMs: DateTime.now().millisecondsSinceEpoch.toDouble(),
|
|
||||||
toolCallId: null,
|
|
||||||
toolName: null,
|
|
||||||
stopReason: null,
|
|
||||||
pending: false,
|
|
||||||
error: true,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> sendAiGatewaySingleAgentMessageDesktopInternal(
|
|
||||||
AppController controller,
|
|
||||||
String message, {
|
|
||||||
required String thinking,
|
|
||||||
required List<GatewayChatAttachmentPayload> attachments,
|
|
||||||
String? sessionKeyOverride,
|
|
||||||
bool appendUserMessage = true,
|
|
||||||
bool managePendingState = true,
|
|
||||||
}) async {
|
|
||||||
final sessionKey = controller.normalizedAssistantSessionKeyInternal(
|
|
||||||
sessionKeyOverride ??
|
|
||||||
controller.sessionsControllerInternal.currentSessionKey,
|
|
||||||
);
|
|
||||||
final trimmed = message.trim();
|
|
||||||
if (trimmed.isEmpty && attachments.isEmpty) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
final baseUrl = controller.normalizeAiGatewayBaseUrlInternal(
|
|
||||||
controller.aiGatewayUrl,
|
|
||||||
);
|
|
||||||
if (baseUrl == null) {
|
|
||||||
controller.appendAssistantThreadMessageInternal(
|
|
||||||
sessionKey,
|
|
||||||
assistantErrorMessageSingleAgentDesktopInternal(
|
|
||||||
controller,
|
|
||||||
appText(
|
|
||||||
'LLM API Endpoint 未配置,无法发送对话。',
|
|
||||||
'LLM API Endpoint is not configured, so the conversation could not be sent.',
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
final apiKey = await controller.loadAiGatewayApiKey();
|
|
||||||
final allowsAnonymous =
|
|
||||||
controller.isLoopbackHostInternal(baseUrl.host) &&
|
|
||||||
(baseUrl.host.trim().toLowerCase() == '127.0.0.1' ||
|
|
||||||
baseUrl.host.trim().toLowerCase() == 'localhost');
|
|
||||||
if (apiKey.isEmpty && !allowsAnonymous) {
|
|
||||||
controller.appendAssistantThreadMessageInternal(
|
|
||||||
sessionKey,
|
|
||||||
assistantErrorMessageSingleAgentDesktopInternal(
|
|
||||||
controller,
|
|
||||||
appText(
|
|
||||||
'LLM API Token 未配置,无法发送对话。',
|
|
||||||
'LLM API Token is not configured, so the conversation could not be sent.',
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
final model = controller.resolvedAiGatewayModel;
|
|
||||||
if (model.isEmpty) {
|
|
||||||
controller.appendAssistantThreadMessageInternal(
|
|
||||||
sessionKey,
|
|
||||||
assistantErrorMessageSingleAgentDesktopInternal(
|
|
||||||
controller,
|
|
||||||
appText(
|
|
||||||
'当前没有可用的 LLM API 对话模型。请先在 设置 -> 集成 中同步并选择可用模型。',
|
|
||||||
'No LLM API chat model is available yet. Sync and select a supported model in Settings -> Integrations first.',
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (appendUserMessage) {
|
|
||||||
final userText = trimmed.isEmpty ? 'See attached.' : trimmed;
|
|
||||||
controller.appendAssistantThreadMessageInternal(
|
|
||||||
sessionKey,
|
|
||||||
GatewayChatMessage(
|
|
||||||
id: controller.nextLocalMessageIdInternal(),
|
|
||||||
role: 'user',
|
|
||||||
text: userText,
|
|
||||||
timestampMs: DateTime.now().millisecondsSinceEpoch.toDouble(),
|
|
||||||
toolCallId: null,
|
|
||||||
toolName: null,
|
|
||||||
stopReason: null,
|
|
||||||
pending: false,
|
|
||||||
error: false,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (managePendingState) {
|
|
||||||
controller.aiGatewayPendingSessionKeysInternal.add(sessionKey);
|
|
||||||
controller.recomputeTasksInternal();
|
|
||||||
controller.notifyIfActiveInternal();
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
final assistantText =
|
|
||||||
await requestAiGatewaySingleAgentCompletionDesktopInternal(
|
|
||||||
controller,
|
|
||||||
baseUrl: baseUrl,
|
|
||||||
apiKey: apiKey,
|
|
||||||
model: model,
|
|
||||||
thinking: thinking,
|
|
||||||
sessionKey: sessionKey,
|
|
||||||
);
|
|
||||||
controller.appendAssistantThreadMessageInternal(
|
|
||||||
sessionKey,
|
|
||||||
GatewayChatMessage(
|
|
||||||
id: controller.nextLocalMessageIdInternal(),
|
|
||||||
role: 'assistant',
|
|
||||||
text: assistantText,
|
|
||||||
timestampMs: DateTime.now().millisecondsSinceEpoch.toDouble(),
|
|
||||||
toolCallId: null,
|
|
||||||
toolName: null,
|
|
||||||
stopReason: null,
|
|
||||||
pending: false,
|
|
||||||
error: false,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
controller.upsertTaskThreadInternal(
|
|
||||||
sessionKey,
|
|
||||||
gatewayEntryState: 'only-chat',
|
|
||||||
latestResolvedRuntimeModel: model,
|
|
||||||
lifecycleStatus: 'ready',
|
|
||||||
lastRunAtMs: DateTime.now().millisecondsSinceEpoch.toDouble(),
|
|
||||||
lastResultCode: 'success',
|
|
||||||
updatedAtMs: DateTime.now().millisecondsSinceEpoch.toDouble(),
|
|
||||||
);
|
|
||||||
} on AiGatewayAbortExceptionInternal catch (error) {
|
|
||||||
final partial = error.partialText.trim();
|
|
||||||
if (partial.isNotEmpty) {
|
|
||||||
controller.appendAssistantThreadMessageInternal(
|
|
||||||
sessionKey,
|
|
||||||
GatewayChatMessage(
|
|
||||||
id: controller.nextLocalMessageIdInternal(),
|
|
||||||
role: 'assistant',
|
|
||||||
text: partial,
|
|
||||||
timestampMs: DateTime.now().millisecondsSinceEpoch.toDouble(),
|
|
||||||
toolCallId: null,
|
|
||||||
toolName: null,
|
|
||||||
stopReason: 'aborted',
|
|
||||||
pending: false,
|
|
||||||
error: false,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
controller.upsertTaskThreadInternal(
|
|
||||||
sessionKey,
|
|
||||||
lifecycleStatus: 'ready',
|
|
||||||
lastRunAtMs: DateTime.now().millisecondsSinceEpoch.toDouble(),
|
|
||||||
lastResultCode: 'aborted',
|
|
||||||
updatedAtMs: DateTime.now().millisecondsSinceEpoch.toDouble(),
|
|
||||||
);
|
|
||||||
} catch (error) {
|
|
||||||
controller.upsertTaskThreadInternal(
|
|
||||||
sessionKey,
|
|
||||||
lifecycleStatus: 'ready',
|
|
||||||
lastRunAtMs: DateTime.now().millisecondsSinceEpoch.toDouble(),
|
|
||||||
lastResultCode: 'error',
|
|
||||||
updatedAtMs: DateTime.now().millisecondsSinceEpoch.toDouble(),
|
|
||||||
);
|
|
||||||
controller.appendAssistantThreadMessageInternal(
|
|
||||||
sessionKey,
|
|
||||||
assistantErrorMessageSingleAgentDesktopInternal(
|
|
||||||
controller,
|
|
||||||
controller.aiGatewayErrorLabelInternal(error),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
} finally {
|
|
||||||
controller.aiGatewayStreamingClientsInternal.remove(sessionKey);
|
|
||||||
controller.clearAiGatewayStreamingTextInternal(sessionKey);
|
|
||||||
if (managePendingState) {
|
|
||||||
controller.aiGatewayPendingSessionKeysInternal.remove(sessionKey);
|
|
||||||
controller.recomputeTasksInternal();
|
|
||||||
controller.notifyIfActiveInternal();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<String> requestAiGatewaySingleAgentCompletionDesktopInternal(
|
|
||||||
AppController controller, {
|
|
||||||
required Uri baseUrl,
|
|
||||||
required String apiKey,
|
|
||||||
required String model,
|
|
||||||
required String thinking,
|
|
||||||
required String sessionKey,
|
|
||||||
}) async {
|
|
||||||
final uri = controller.aiGatewayChatUriInternal(baseUrl);
|
|
||||||
final client = HttpClient()..connectionTimeout = const Duration(seconds: 20);
|
|
||||||
controller.aiGatewayStreamingClientsInternal[sessionKey] = client;
|
|
||||||
try {
|
|
||||||
final request = await client
|
|
||||||
.postUrl(uri)
|
|
||||||
.timeout(const Duration(seconds: 20));
|
|
||||||
request.headers.set(
|
|
||||||
HttpHeaders.acceptHeader,
|
|
||||||
'text/event-stream, application/json',
|
|
||||||
);
|
|
||||||
request.headers.set(
|
|
||||||
HttpHeaders.contentTypeHeader,
|
|
||||||
'application/json; charset=utf-8',
|
|
||||||
);
|
|
||||||
final trimmedApiKey = apiKey.trim();
|
|
||||||
if (trimmedApiKey.isNotEmpty) {
|
|
||||||
request.headers.set(
|
|
||||||
HttpHeaders.authorizationHeader,
|
|
||||||
'Bearer $trimmedApiKey',
|
|
||||||
);
|
|
||||||
request.headers.set('x-api-key', trimmedApiKey);
|
|
||||||
}
|
|
||||||
final payload = <String, dynamic>{
|
|
||||||
'model': model,
|
|
||||||
'stream': true,
|
|
||||||
'messages': buildAiGatewaySingleAgentRequestMessagesDesktopInternal(
|
|
||||||
controller,
|
|
||||||
sessionKey,
|
|
||||||
),
|
|
||||||
};
|
|
||||||
final normalizedThinking = thinking.trim().toLowerCase();
|
|
||||||
if (normalizedThinking.isNotEmpty && normalizedThinking != 'off') {
|
|
||||||
payload['reasoning_effort'] = normalizedThinking;
|
|
||||||
}
|
|
||||||
request.add(utf8.encode(jsonEncode(payload)));
|
|
||||||
final response = await request.close().timeout(const Duration(seconds: 60));
|
|
||||||
if (response.statusCode < 200 || response.statusCode >= 300) {
|
|
||||||
final body = await response.transform(utf8.decoder).join();
|
|
||||||
throw AiGatewayChatExceptionInternal(
|
|
||||||
controller.formatAiGatewayHttpErrorInternal(
|
|
||||||
response.statusCode,
|
|
||||||
controller.extractAiGatewayErrorDetailInternal(body),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
final contentType =
|
|
||||||
response.headers.contentType?.mimeType.toLowerCase() ??
|
|
||||||
response.headers.value(HttpHeaders.contentTypeHeader)?.toLowerCase() ??
|
|
||||||
'';
|
|
||||||
if (contentType.contains('text/event-stream')) {
|
|
||||||
final streamed = await readAiGatewayStreamingResponseDesktopInternal(
|
|
||||||
controller,
|
|
||||||
response: response,
|
|
||||||
sessionKey: sessionKey,
|
|
||||||
);
|
|
||||||
if (streamed.trim().isEmpty) {
|
|
||||||
throw const FormatException('Missing assistant content');
|
|
||||||
}
|
|
||||||
return streamed.trim();
|
|
||||||
}
|
|
||||||
return await readAiGatewayJsonCompletionDesktopInternal(
|
|
||||||
controller,
|
|
||||||
response,
|
|
||||||
);
|
|
||||||
} catch (error) {
|
|
||||||
if (consumeAiGatewaySingleAgentAbortDesktopInternal(
|
|
||||||
controller,
|
|
||||||
sessionKey,
|
|
||||||
)) {
|
|
||||||
throw AiGatewayAbortExceptionInternal(
|
|
||||||
controller.aiGatewayStreamingTextBySessionInternal[sessionKey] ?? '',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
rethrow;
|
|
||||||
} finally {
|
|
||||||
controller.aiGatewayStreamingClientsInternal.remove(sessionKey);
|
|
||||||
client.close(force: true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
List<Map<String, String>>
|
|
||||||
buildAiGatewaySingleAgentRequestMessagesDesktopInternal(
|
|
||||||
AppController controller,
|
|
||||||
String sessionKey,
|
|
||||||
) {
|
|
||||||
final history = <GatewayChatMessage>[
|
|
||||||
...(controller.gatewayHistoryCacheInternal[sessionKey] ??
|
|
||||||
const <GatewayChatMessage>[]),
|
|
||||||
...(controller.assistantThreadMessagesInternal[sessionKey] ??
|
|
||||||
const <GatewayChatMessage>[]),
|
|
||||||
];
|
|
||||||
return history
|
|
||||||
.where((message) {
|
|
||||||
final role = message.role.trim().toLowerCase();
|
|
||||||
return (role == 'user' || role == 'assistant') &&
|
|
||||||
(message.toolName ?? '').trim().isEmpty &&
|
|
||||||
message.text.trim().isNotEmpty;
|
|
||||||
})
|
|
||||||
.map(
|
|
||||||
(message) => <String, String>{
|
|
||||||
'role': message.role.trim().toLowerCase() == 'assistant'
|
|
||||||
? 'assistant'
|
|
||||||
: 'user',
|
|
||||||
'content': message.text.trim(),
|
|
||||||
},
|
|
||||||
)
|
|
||||||
.toList(growable: false);
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<String> readAiGatewayJsonCompletionDesktopInternal(
|
|
||||||
AppController controller,
|
|
||||||
HttpClientResponse response,
|
|
||||||
) async {
|
|
||||||
final body = await response.transform(utf8.decoder).join();
|
|
||||||
final decoded = jsonDecode(controller.extractFirstJsonDocumentInternal(body));
|
|
||||||
final assistantText = controller.extractAiGatewayAssistantTextInternal(
|
|
||||||
decoded,
|
|
||||||
);
|
|
||||||
if (assistantText.trim().isEmpty) {
|
|
||||||
throw const FormatException('Missing assistant content');
|
|
||||||
}
|
|
||||||
return assistantText.trim();
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<String> readAiGatewayStreamingResponseDesktopInternal(
|
|
||||||
AppController controller, {
|
|
||||||
required HttpClientResponse response,
|
|
||||||
required String sessionKey,
|
|
||||||
}) async {
|
|
||||||
final buffer = StringBuffer();
|
|
||||||
final eventLines = <String>[];
|
|
||||||
|
|
||||||
void processEvent(String payload) {
|
|
||||||
final trimmed = payload.trim();
|
|
||||||
if (trimmed.isEmpty || trimmed == '[DONE]') {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
final deltaText = extractAiGatewayStreamTextDesktopInternal(
|
|
||||||
controller,
|
|
||||||
trimmed,
|
|
||||||
);
|
|
||||||
if (deltaText.isEmpty) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
final current = buffer.toString();
|
|
||||||
if (current.isEmpty || deltaText == current) {
|
|
||||||
buffer
|
|
||||||
..clear()
|
|
||||||
..write(deltaText);
|
|
||||||
} else if (deltaText.startsWith(current)) {
|
|
||||||
buffer
|
|
||||||
..clear()
|
|
||||||
..write(deltaText);
|
|
||||||
} else {
|
|
||||||
buffer.write(deltaText);
|
|
||||||
}
|
|
||||||
controller.setAiGatewayStreamingTextInternal(sessionKey, buffer.toString());
|
|
||||||
}
|
|
||||||
|
|
||||||
await for (final line
|
|
||||||
in response.transform(utf8.decoder).transform(const LineSplitter())) {
|
|
||||||
if (consumeAiGatewaySingleAgentAbortDesktopInternal(
|
|
||||||
controller,
|
|
||||||
sessionKey,
|
|
||||||
)) {
|
|
||||||
throw AiGatewayAbortExceptionInternal(buffer.toString());
|
|
||||||
}
|
|
||||||
if (line.isEmpty) {
|
|
||||||
if (eventLines.isNotEmpty) {
|
|
||||||
processEvent(eventLines.join('\n'));
|
|
||||||
eventLines.clear();
|
|
||||||
}
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (line.startsWith('data:')) {
|
|
||||||
eventLines.add(line.substring(5).trimLeft());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (eventLines.isNotEmpty) {
|
|
||||||
processEvent(eventLines.join('\n'));
|
|
||||||
}
|
|
||||||
|
|
||||||
return buffer.toString();
|
|
||||||
}
|
|
||||||
|
|
||||||
String extractAiGatewayStreamTextDesktopInternal(
|
|
||||||
AppController controller,
|
|
||||||
String payload,
|
|
||||||
) {
|
|
||||||
final decoded = jsonDecode(
|
|
||||||
controller.extractFirstJsonDocumentInternal(payload),
|
|
||||||
);
|
|
||||||
final map = asMap(decoded);
|
|
||||||
final choices = asList(map['choices']);
|
|
||||||
if (choices.isNotEmpty) {
|
|
||||||
final firstChoice = asMap(choices.first);
|
|
||||||
final delta = asMap(firstChoice['delta']);
|
|
||||||
final deltaContent = controller.extractAiGatewayContentInternal(
|
|
||||||
delta['content'],
|
|
||||||
);
|
|
||||||
if (deltaContent.isNotEmpty) {
|
|
||||||
return deltaContent;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return controller.extractAiGatewayAssistantTextInternal(decoded);
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> abortAiGatewaySingleAgentRunDesktopInternal(
|
|
||||||
AppController controller,
|
|
||||||
String sessionKey,
|
|
||||||
) async {
|
|
||||||
final normalizedSessionKey = controller.normalizedAssistantSessionKeyInternal(
|
|
||||||
sessionKey,
|
|
||||||
);
|
|
||||||
controller.aiGatewayAbortedSessionKeysInternal.add(normalizedSessionKey);
|
|
||||||
final client = controller.aiGatewayStreamingClientsInternal.remove(
|
|
||||||
normalizedSessionKey,
|
|
||||||
);
|
|
||||||
if (client != null) {
|
|
||||||
try {
|
|
||||||
client.close(force: true);
|
|
||||||
} catch (_) {
|
|
||||||
// Best effort only.
|
|
||||||
}
|
|
||||||
}
|
|
||||||
controller.aiGatewayPendingSessionKeysInternal.remove(normalizedSessionKey);
|
|
||||||
controller.clearAiGatewayStreamingTextInternal(normalizedSessionKey);
|
|
||||||
controller.upsertTaskThreadInternal(
|
|
||||||
normalizedSessionKey,
|
|
||||||
lifecycleStatus: 'ready',
|
|
||||||
lastRunAtMs: DateTime.now().millisecondsSinceEpoch.toDouble(),
|
|
||||||
lastResultCode: 'aborted',
|
|
||||||
updatedAtMs: DateTime.now().millisecondsSinceEpoch.toDouble(),
|
|
||||||
);
|
|
||||||
controller.recomputeTasksInternal();
|
|
||||||
controller.notifyIfActiveInternal();
|
|
||||||
}
|
|
||||||
|
|
||||||
bool consumeAiGatewaySingleAgentAbortDesktopInternal(
|
|
||||||
AppController controller,
|
|
||||||
String sessionKey,
|
|
||||||
) {
|
|
||||||
return controller.aiGatewayAbortedSessionKeysInternal.remove(
|
|
||||||
controller.normalizedAssistantSessionKeyInternal(sessionKey),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@ -9,7 +9,6 @@ import '../runtime/runtime_models.dart';
|
|||||||
import 'app_controller_desktop_core.dart';
|
import 'app_controller_desktop_core.dart';
|
||||||
import 'app_controller_desktop_external_acp_routing.dart';
|
import 'app_controller_desktop_external_acp_routing.dart';
|
||||||
import 'app_controller_desktop_runtime_helpers.dart';
|
import 'app_controller_desktop_runtime_helpers.dart';
|
||||||
import 'app_controller_desktop_single_agent_ai_gateway.dart';
|
|
||||||
import 'app_controller_desktop_single_agent_status_messages.dart';
|
import 'app_controller_desktop_single_agent_status_messages.dart';
|
||||||
import 'app_controller_desktop_thread_sessions.dart';
|
import 'app_controller_desktop_thread_sessions.dart';
|
||||||
import 'app_controller_desktop_thread_storage.dart';
|
import 'app_controller_desktop_thread_storage.dart';
|
||||||
@ -69,7 +68,7 @@ Future<void> sendSingleAgentMessageDesktopGoTaskFlowInternal(
|
|||||||
final provider = selection == SingleAgentProvider.auto
|
final provider = selection == SingleAgentProvider.auto
|
||||||
? (availableProviders.isEmpty ? null : availableProviders.first)
|
? (availableProviders.isEmpty ? null : availableProviders.first)
|
||||||
: (capabilities.providers.contains(selection) ? selection : null);
|
: (capabilities.providers.contains(selection) ? selection : null);
|
||||||
final fallbackReason = provider == null
|
final unavailableReason = provider == null
|
||||||
? (selection == SingleAgentProvider.auto
|
? (selection == SingleAgentProvider.auto
|
||||||
? appText(
|
? appText(
|
||||||
'当前没有可用的 GoTaskService Provider。',
|
'当前没有可用的 GoTaskService Provider。',
|
||||||
@ -80,48 +79,28 @@ Future<void> sendSingleAgentMessageDesktopGoTaskFlowInternal(
|
|||||||
'GoTaskService does not currently support ${selection.label}.',
|
'GoTaskService does not currently support ${selection.label}.',
|
||||||
))
|
))
|
||||||
: null;
|
: null;
|
||||||
if (provider == null && !routing.isAuto) {
|
if (provider == null) {
|
||||||
if (controller.singleAgentUsesAiChatFallbackForSession(sessionKey)) {
|
controller.upsertTaskThreadInternal(
|
||||||
appendSingleAgentFallbackStatusDesktopInternal(
|
sessionKey,
|
||||||
|
lifecycleStatus: 'ready',
|
||||||
|
lastRunAtMs: DateTime.now().millisecondsSinceEpoch.toDouble(),
|
||||||
|
lastResultCode: 'error',
|
||||||
|
updatedAtMs: DateTime.now().millisecondsSinceEpoch.toDouble(),
|
||||||
|
);
|
||||||
|
controller.appendAssistantThreadMessageInternal(
|
||||||
|
sessionKey,
|
||||||
|
assistantErrorMessageSingleAgentDesktopInternal(
|
||||||
controller,
|
controller,
|
||||||
sessionKey,
|
singleAgentUnavailableLabelDesktopInternal(
|
||||||
fallbackReason,
|
controller,
|
||||||
);
|
sessionKey,
|
||||||
await sendAiGatewaySingleAgentMessageDesktopInternal(
|
unavailableReason,
|
||||||
controller,
|
|
||||||
message,
|
|
||||||
thinking: thinking,
|
|
||||||
attachments: attachments,
|
|
||||||
sessionKeyOverride: sessionKey,
|
|
||||||
appendUserMessage: false,
|
|
||||||
managePendingState: false,
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
controller.appendAssistantThreadMessageInternal(
|
|
||||||
sessionKey,
|
|
||||||
GatewayChatMessage(
|
|
||||||
id: controller.nextLocalMessageIdInternal(),
|
|
||||||
role: 'assistant',
|
|
||||||
text: singleAgentUnavailableLabelDesktopInternal(
|
|
||||||
controller,
|
|
||||||
sessionKey,
|
|
||||||
fallbackReason,
|
|
||||||
),
|
|
||||||
timestampMs: DateTime.now().millisecondsSinceEpoch.toDouble(),
|
|
||||||
toolCallId: null,
|
|
||||||
toolName: singleAgentRuntimeDebugToolNameDesktopInternal(
|
|
||||||
controller,
|
|
||||||
provider?.label ?? selection.label,
|
|
||||||
),
|
|
||||||
stopReason: null,
|
|
||||||
pending: false,
|
|
||||||
error: false,
|
|
||||||
),
|
),
|
||||||
);
|
),
|
||||||
}
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
final effectiveProvider = provider ?? SingleAgentProvider.auto;
|
final effectiveProvider = provider;
|
||||||
|
|
||||||
appendSingleAgentRuntimeStatusDesktopInternal(
|
appendSingleAgentRuntimeStatusDesktopInternal(
|
||||||
controller,
|
controller,
|
||||||
@ -248,36 +227,6 @@ void _applySingleAgentGoTaskResultDesktopInternal(
|
|||||||
result,
|
result,
|
||||||
);
|
);
|
||||||
controller.clearAiGatewayStreamingTextInternal(sessionKey);
|
controller.clearAiGatewayStreamingTextInternal(sessionKey);
|
||||||
if (!result.success &&
|
|
||||||
controller.singleAgentUsesAiChatFallbackForSession(sessionKey)) {
|
|
||||||
appendSingleAgentFallbackStatusDesktopInternal(
|
|
||||||
controller,
|
|
||||||
sessionKey,
|
|
||||||
result.errorMessage,
|
|
||||||
);
|
|
||||||
controller.upsertTaskThreadInternal(
|
|
||||||
sessionKey,
|
|
||||||
gatewayEntryState: 'only-chat',
|
|
||||||
latestResolvedRuntimeModel: controller.resolvedAiGatewayModel,
|
|
||||||
lifecycleStatus: 'ready',
|
|
||||||
lastRunAtMs: DateTime.now().millisecondsSinceEpoch.toDouble(),
|
|
||||||
lastResultCode: 'fallback',
|
|
||||||
updatedAtMs: DateTime.now().millisecondsSinceEpoch.toDouble(),
|
|
||||||
);
|
|
||||||
unawaited(
|
|
||||||
sendAiGatewaySingleAgentMessageDesktopInternal(
|
|
||||||
controller,
|
|
||||||
message,
|
|
||||||
thinking: thinking,
|
|
||||||
attachments: attachments,
|
|
||||||
sessionKeyOverride: sessionKey,
|
|
||||||
appendUserMessage: false,
|
|
||||||
managePendingState: false,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!result.success) {
|
if (!result.success) {
|
||||||
controller.appendAssistantThreadMessageInternal(
|
controller.appendAssistantThreadMessageInternal(
|
||||||
sessionKey,
|
sessionKey,
|
||||||
|
|||||||
@ -8,6 +8,23 @@ import 'app_controller_desktop_runtime_helpers.dart';
|
|||||||
import 'app_controller_desktop_thread_sessions.dart';
|
import 'app_controller_desktop_thread_sessions.dart';
|
||||||
import 'app_controller_desktop_thread_storage.dart';
|
import 'app_controller_desktop_thread_storage.dart';
|
||||||
|
|
||||||
|
GatewayChatMessage assistantErrorMessageSingleAgentDesktopInternal(
|
||||||
|
AppController controller,
|
||||||
|
String text,
|
||||||
|
) {
|
||||||
|
return GatewayChatMessage(
|
||||||
|
id: controller.nextLocalMessageIdInternal(),
|
||||||
|
role: 'assistant',
|
||||||
|
text: text,
|
||||||
|
timestampMs: DateTime.now().millisecondsSinceEpoch.toDouble(),
|
||||||
|
toolCallId: null,
|
||||||
|
toolName: null,
|
||||||
|
stopReason: null,
|
||||||
|
pending: false,
|
||||||
|
error: true,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
String? singleAgentRuntimeDebugToolNameDesktopInternal(
|
String? singleAgentRuntimeDebugToolNameDesktopInternal(
|
||||||
AppController controller,
|
AppController controller,
|
||||||
String label,
|
String label,
|
||||||
@ -49,43 +66,6 @@ void appendSingleAgentRuntimeStatusDesktopInternal(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
void appendSingleAgentFallbackStatusDesktopInternal(
|
|
||||||
AppController controller,
|
|
||||||
String sessionKey,
|
|
||||||
String? reason,
|
|
||||||
) {
|
|
||||||
if (!controller.showsSingleAgentRuntimeDebugMessagesInternal) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
controller.appendAssistantThreadMessageInternal(
|
|
||||||
sessionKey,
|
|
||||||
GatewayChatMessage(
|
|
||||||
id: controller.nextLocalMessageIdInternal(),
|
|
||||||
role: 'assistant',
|
|
||||||
text: singleAgentFallbackLabelDesktopInternal(reason),
|
|
||||||
timestampMs: DateTime.now().millisecondsSinceEpoch.toDouble(),
|
|
||||||
toolCallId: null,
|
|
||||||
toolName: 'AI Chat fallback',
|
|
||||||
stopReason: null,
|
|
||||||
pending: false,
|
|
||||||
error: false,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
String singleAgentFallbackLabelDesktopInternal(String? reason) {
|
|
||||||
final detail = reason?.trim() ?? '';
|
|
||||||
return detail.isEmpty
|
|
||||||
? appText(
|
|
||||||
'未发现可用的外部 Agent ACP 端点,已回退到 AI Chat。',
|
|
||||||
'No external Agent ACP endpoint is available. Falling back to AI Chat.',
|
|
||||||
)
|
|
||||||
: appText(
|
|
||||||
'外部 Agent ACP 连接不可用,已回退到 AI Chat:$detail',
|
|
||||||
'External Agent ACP connection is unavailable. Falling back to AI Chat: $detail',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
String singleAgentUnavailableLabelDesktopInternal(
|
String singleAgentUnavailableLabelDesktopInternal(
|
||||||
AppController controller,
|
AppController controller,
|
||||||
String sessionKey,
|
String sessionKey,
|
||||||
@ -116,12 +96,12 @@ String singleAgentUnavailableLabelDesktopInternal(
|
|||||||
)) {
|
)) {
|
||||||
return detail.isEmpty
|
return detail.isEmpty
|
||||||
? appText(
|
? appText(
|
||||||
'当前没有可用的外部 Agent ACP 端点,也没有可用的 AI Chat fallback。请先配置外部 Agent 连接,或配置 LLM API。',
|
'当前没有可用的外部 Agent ACP 端点。请先配置外部 Agent 连接。',
|
||||||
'No external Agent ACP endpoint is available, and AI Chat fallback is not configured. Configure an external Agent connection or configure LLM API first.',
|
'No external Agent ACP endpoint is available. Configure an external Agent connection first.',
|
||||||
)
|
)
|
||||||
: appText(
|
: appText(
|
||||||
'$detail 当前没有可用的外部 Agent ACP 端点,也没有可用的 AI Chat fallback。请先配置外部 Agent 连接,或配置 LLM API。',
|
'$detail 当前没有可用的外部 Agent ACP 端点。请先配置外部 Agent 连接。',
|
||||||
'$detail No external Agent ACP endpoint is available, and AI Chat fallback is not configured. Configure an external Agent connection or configure LLM API first.',
|
'$detail No external Agent ACP endpoint is available. Configure an external Agent connection first.',
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return detail.isEmpty
|
return detail.isEmpty
|
||||||
|
|||||||
@ -21,7 +21,6 @@ import '../runtime/runtime_models.dart';
|
|||||||
import '../runtime/secure_config_store.dart';
|
import '../runtime/secure_config_store.dart';
|
||||||
import '../runtime/embedded_agent_launch_policy.dart';
|
import '../runtime/embedded_agent_launch_policy.dart';
|
||||||
import '../runtime/runtime_coordinator.dart';
|
import '../runtime/runtime_coordinator.dart';
|
||||||
import '../runtime/direct_single_agent_app_server_client.dart';
|
|
||||||
import '../runtime/gateway_acp_client.dart';
|
import '../runtime/gateway_acp_client.dart';
|
||||||
import '../runtime/codex_runtime.dart';
|
import '../runtime/codex_runtime.dart';
|
||||||
import '../runtime/codex_config_bridge.dart';
|
import '../runtime/codex_config_bridge.dart';
|
||||||
@ -32,7 +31,6 @@ import '../runtime/mode_switcher.dart';
|
|||||||
import '../runtime/agent_registry.dart';
|
import '../runtime/agent_registry.dart';
|
||||||
import '../runtime/multi_agent_orchestrator.dart';
|
import '../runtime/multi_agent_orchestrator.dart';
|
||||||
import '../runtime/platform_environment.dart';
|
import '../runtime/platform_environment.dart';
|
||||||
import '../runtime/single_agent_runner.dart';
|
|
||||||
import '../runtime/skill_directory_access.dart';
|
import '../runtime/skill_directory_access.dart';
|
||||||
import 'app_controller_desktop_core.dart';
|
import 'app_controller_desktop_core.dart';
|
||||||
import 'app_controller_desktop_navigation.dart';
|
import 'app_controller_desktop_navigation.dart';
|
||||||
|
|||||||
@ -21,7 +21,6 @@ import '../runtime/runtime_models.dart';
|
|||||||
import '../runtime/secure_config_store.dart';
|
import '../runtime/secure_config_store.dart';
|
||||||
import '../runtime/embedded_agent_launch_policy.dart';
|
import '../runtime/embedded_agent_launch_policy.dart';
|
||||||
import '../runtime/runtime_coordinator.dart';
|
import '../runtime/runtime_coordinator.dart';
|
||||||
import '../runtime/direct_single_agent_app_server_client.dart';
|
|
||||||
import '../runtime/gateway_acp_client.dart';
|
import '../runtime/gateway_acp_client.dart';
|
||||||
import '../runtime/codex_runtime.dart';
|
import '../runtime/codex_runtime.dart';
|
||||||
import '../runtime/codex_config_bridge.dart';
|
import '../runtime/codex_config_bridge.dart';
|
||||||
@ -33,7 +32,6 @@ import '../runtime/mode_switcher.dart';
|
|||||||
import '../runtime/agent_registry.dart';
|
import '../runtime/agent_registry.dart';
|
||||||
import '../runtime/multi_agent_orchestrator.dart';
|
import '../runtime/multi_agent_orchestrator.dart';
|
||||||
import '../runtime/platform_environment.dart';
|
import '../runtime/platform_environment.dart';
|
||||||
import '../runtime/single_agent_runner.dart';
|
|
||||||
import '../runtime/skill_directory_access.dart';
|
import '../runtime/skill_directory_access.dart';
|
||||||
import 'app_controller_desktop_core.dart';
|
import 'app_controller_desktop_core.dart';
|
||||||
import 'app_controller_desktop_navigation.dart';
|
import 'app_controller_desktop_navigation.dart';
|
||||||
@ -73,11 +71,6 @@ extension AppControllerDesktopThreadActions on AppController {
|
|||||||
localAttachments: localAttachments,
|
localAttachments: localAttachments,
|
||||||
);
|
);
|
||||||
|
|
||||||
Future<void> abortAiGatewayRunInternal(String sessionKey) =>
|
|
||||||
AppControllerDesktopSingleAgent(
|
|
||||||
this,
|
|
||||||
).abortAiGatewayRunInternal(sessionKey);
|
|
||||||
|
|
||||||
Future<void> connectSavedGateway() async {
|
Future<void> connectSavedGateway() async {
|
||||||
final target = currentAssistantExecutionTarget;
|
final target = currentAssistantExecutionTarget;
|
||||||
if (target == AssistantExecutionTarget.singleAgent) {
|
if (target == AssistantExecutionTarget.singleAgent) {
|
||||||
@ -474,7 +467,6 @@ extension AppControllerDesktopThreadActions on AppController {
|
|||||||
notifyIfActiveInternal();
|
notifyIfActiveInternal();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
await abortAiGatewayRunInternal(sessionKey);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
final sessionKey = normalizedAssistantSessionKeyInternal(
|
final sessionKey = normalizedAssistantSessionKeyInternal(
|
||||||
|
|||||||
@ -21,7 +21,6 @@ import '../runtime/runtime_models.dart';
|
|||||||
import '../runtime/secure_config_store.dart';
|
import '../runtime/secure_config_store.dart';
|
||||||
import '../runtime/embedded_agent_launch_policy.dart';
|
import '../runtime/embedded_agent_launch_policy.dart';
|
||||||
import '../runtime/runtime_coordinator.dart';
|
import '../runtime/runtime_coordinator.dart';
|
||||||
import '../runtime/direct_single_agent_app_server_client.dart';
|
|
||||||
import '../runtime/gateway_acp_client.dart';
|
import '../runtime/gateway_acp_client.dart';
|
||||||
import '../runtime/codex_runtime.dart';
|
import '../runtime/codex_runtime.dart';
|
||||||
import '../runtime/codex_config_bridge.dart';
|
import '../runtime/codex_config_bridge.dart';
|
||||||
@ -32,7 +31,6 @@ import '../runtime/mode_switcher.dart';
|
|||||||
import '../runtime/agent_registry.dart';
|
import '../runtime/agent_registry.dart';
|
||||||
import '../runtime/multi_agent_orchestrator.dart';
|
import '../runtime/multi_agent_orchestrator.dart';
|
||||||
import '../runtime/platform_environment.dart';
|
import '../runtime/platform_environment.dart';
|
||||||
import '../runtime/single_agent_runner.dart';
|
|
||||||
import '../runtime/skill_directory_access.dart';
|
import '../runtime/skill_directory_access.dart';
|
||||||
import 'app_controller_desktop_core.dart';
|
import 'app_controller_desktop_core.dart';
|
||||||
import 'app_controller_desktop_navigation.dart';
|
import 'app_controller_desktop_navigation.dart';
|
||||||
|
|||||||
@ -21,7 +21,6 @@ import '../runtime/runtime_models.dart';
|
|||||||
import '../runtime/secure_config_store.dart';
|
import '../runtime/secure_config_store.dart';
|
||||||
import '../runtime/embedded_agent_launch_policy.dart';
|
import '../runtime/embedded_agent_launch_policy.dart';
|
||||||
import '../runtime/runtime_coordinator.dart';
|
import '../runtime/runtime_coordinator.dart';
|
||||||
import '../runtime/direct_single_agent_app_server_client.dart';
|
|
||||||
import '../runtime/gateway_acp_client.dart';
|
import '../runtime/gateway_acp_client.dart';
|
||||||
import '../runtime/codex_runtime.dart';
|
import '../runtime/codex_runtime.dart';
|
||||||
import '../runtime/codex_config_bridge.dart';
|
import '../runtime/codex_config_bridge.dart';
|
||||||
@ -32,7 +31,6 @@ import '../runtime/mode_switcher.dart';
|
|||||||
import '../runtime/agent_registry.dart';
|
import '../runtime/agent_registry.dart';
|
||||||
import '../runtime/multi_agent_orchestrator.dart';
|
import '../runtime/multi_agent_orchestrator.dart';
|
||||||
import '../runtime/platform_environment.dart';
|
import '../runtime/platform_environment.dart';
|
||||||
import '../runtime/single_agent_runner.dart';
|
|
||||||
import '../runtime/skill_directory_access.dart';
|
import '../runtime/skill_directory_access.dart';
|
||||||
import 'app_controller_desktop_core.dart';
|
import 'app_controller_desktop_core.dart';
|
||||||
import 'app_controller_desktop_navigation.dart';
|
import 'app_controller_desktop_navigation.dart';
|
||||||
@ -123,17 +121,6 @@ extension AppControllerDesktopThreadSessions on AppController {
|
|||||||
if (latestResolvedModel.isNotEmpty) {
|
if (latestResolvedModel.isNotEmpty) {
|
||||||
return latestResolvedModel;
|
return latestResolvedModel;
|
||||||
}
|
}
|
||||||
if (singleAgentUsesAiChatFallbackForSession(normalizedSessionKey)) {
|
|
||||||
final recordModel =
|
|
||||||
assistantThreadRecordsInternal[normalizedSessionKey]
|
|
||||||
?.assistantModelId
|
|
||||||
.trim() ??
|
|
||||||
'';
|
|
||||||
if (recordModel.isNotEmpty) {
|
|
||||||
return recordModel;
|
|
||||||
}
|
|
||||||
return resolvedAiGatewayModel;
|
|
||||||
}
|
|
||||||
return singleAgentRuntimeModelForSession(normalizedSessionKey);
|
return singleAgentRuntimeModelForSession(normalizedSessionKey);
|
||||||
}
|
}
|
||||||
final recordModel =
|
final recordModel =
|
||||||
@ -238,20 +225,6 @@ extension AppControllerDesktopThreadSessions on AppController {
|
|||||||
SingleAgentProvider? get currentSingleAgentResolvedProvider =>
|
SingleAgentProvider? get currentSingleAgentResolvedProvider =>
|
||||||
singleAgentResolvedProviderForSession(currentSessionKey);
|
singleAgentResolvedProviderForSession(currentSessionKey);
|
||||||
|
|
||||||
bool singleAgentUsesAiChatFallbackForSession(String sessionKey) {
|
|
||||||
final normalizedSessionKey = normalizedAssistantSessionKeyInternal(
|
|
||||||
sessionKey,
|
|
||||||
);
|
|
||||||
if (assistantExecutionTargetForSession(normalizedSessionKey) !=
|
|
||||||
AssistantExecutionTarget.singleAgent) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return !hasAnyAvailableSingleAgentProvider && canUseAiGatewayConversation;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool get currentSingleAgentUsesAiChatFallback =>
|
|
||||||
singleAgentUsesAiChatFallbackForSession(currentSessionKey);
|
|
||||||
|
|
||||||
bool singleAgentNeedsAiGatewayConfigurationForSession(String sessionKey) {
|
bool singleAgentNeedsAiGatewayConfigurationForSession(String sessionKey) {
|
||||||
final normalizedSessionKey = normalizedAssistantSessionKeyInternal(
|
final normalizedSessionKey = normalizedAssistantSessionKeyInternal(
|
||||||
sessionKey,
|
sessionKey,
|
||||||
@ -260,7 +233,7 @@ extension AppControllerDesktopThreadSessions on AppController {
|
|||||||
AssistantExecutionTarget.singleAgent) {
|
AssistantExecutionTarget.singleAgent) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
return !hasAnyAvailableSingleAgentProvider && !canUseAiGatewayConversation;
|
return !hasAnyAvailableSingleAgentProvider;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool get currentSingleAgentNeedsAiGatewayConfiguration =>
|
bool get currentSingleAgentNeedsAiGatewayConfiguration =>
|
||||||
@ -319,9 +292,6 @@ extension AppControllerDesktopThreadSessions on AppController {
|
|||||||
if (model.isNotEmpty) {
|
if (model.isNotEmpty) {
|
||||||
return model;
|
return model;
|
||||||
}
|
}
|
||||||
if (singleAgentUsesAiChatFallbackForSession(normalizedSessionKey)) {
|
|
||||||
return appText('AI Chat fallback', 'AI Chat fallback');
|
|
||||||
}
|
|
||||||
final provider =
|
final provider =
|
||||||
singleAgentResolvedProviderForSession(normalizedSessionKey) ??
|
singleAgentResolvedProviderForSession(normalizedSessionKey) ??
|
||||||
singleAgentProviderForSession(normalizedSessionKey);
|
singleAgentProviderForSession(normalizedSessionKey);
|
||||||
@ -342,9 +312,6 @@ extension AppControllerDesktopThreadSessions on AppController {
|
|||||||
AssistantExecutionTarget.singleAgent) {
|
AssistantExecutionTarget.singleAgent) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
if (singleAgentUsesAiChatFallbackForSession(normalizedSessionKey)) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return singleAgentRuntimeModelForSession(normalizedSessionKey).isNotEmpty;
|
return singleAgentRuntimeModelForSession(normalizedSessionKey).isNotEmpty;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -370,9 +337,6 @@ extension AppControllerDesktopThreadSessions on AppController {
|
|||||||
if (provider != SingleAgentProvider.auto) {
|
if (provider != SingleAgentProvider.auto) {
|
||||||
return provider.label;
|
return provider.label;
|
||||||
}
|
}
|
||||||
if (currentSingleAgentUsesAiChatFallback) {
|
|
||||||
return appText('AI Chat fallback', 'AI Chat fallback');
|
|
||||||
}
|
|
||||||
return appText('单机智能体', 'Single Agent');
|
return appText('单机智能体', 'Single Agent');
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -393,19 +357,9 @@ extension AppControllerDesktopThreadSessions on AppController {
|
|||||||
normalizedSessionKey,
|
normalizedSessionKey,
|
||||||
);
|
);
|
||||||
final model = assistantModelForSession(normalizedSessionKey);
|
final model = assistantModelForSession(normalizedSessionKey);
|
||||||
final fallbackReady = singleAgentUsesAiChatFallbackForSession(
|
|
||||||
normalizedSessionKey,
|
|
||||||
);
|
|
||||||
final host = aiGatewayHostLabelInternal(aiGatewayUrl);
|
|
||||||
final providerReady = resolvedProvider != null;
|
final providerReady = resolvedProvider != null;
|
||||||
final detail = providerReady
|
final detail = providerReady
|
||||||
? joinConnectionPartsInternal(<String>[resolvedProvider.label, model])
|
? joinConnectionPartsInternal(<String>[resolvedProvider.label, model])
|
||||||
: fallbackReady
|
|
||||||
? joinConnectionPartsInternal(<String>[
|
|
||||||
appText('AI Chat fallback', 'AI Chat fallback'),
|
|
||||||
model,
|
|
||||||
host,
|
|
||||||
])
|
|
||||||
: singleAgentShouldSuggestAcpSwitchForSession(normalizedSessionKey)
|
: singleAgentShouldSuggestAcpSwitchForSession(normalizedSessionKey)
|
||||||
? appText(
|
? appText(
|
||||||
'${provider.label} 不可用,请切到可用的 ACP Server。',
|
'${provider.label} 不可用,请切到可用的 ACP Server。',
|
||||||
@ -415,8 +369,8 @@ extension AppControllerDesktopThreadSessions on AppController {
|
|||||||
normalizedSessionKey,
|
normalizedSessionKey,
|
||||||
)
|
)
|
||||||
? appText(
|
? appText(
|
||||||
'没有可用的外部 Agent ACP 端点,请配置 LLM API fallback。',
|
'没有可用的外部 Agent ACP 端点,请先配置可用的 ACP Server。',
|
||||||
'No external Agent ACP endpoint is available. Configure LLM API fallback.',
|
'No external Agent ACP endpoint is available. Configure an ACP Server first.',
|
||||||
)
|
)
|
||||||
: appText(
|
: appText(
|
||||||
'当前线程的外部 Agent ACP 连接尚未就绪。',
|
'当前线程的外部 Agent ACP 连接尚未就绪。',
|
||||||
@ -424,14 +378,14 @@ extension AppControllerDesktopThreadSessions on AppController {
|
|||||||
);
|
);
|
||||||
return AssistantThreadConnectionState(
|
return AssistantThreadConnectionState(
|
||||||
executionTarget: target,
|
executionTarget: target,
|
||||||
status: providerReady || fallbackReady
|
status: providerReady
|
||||||
? RuntimeConnectionStatus.connected
|
? RuntimeConnectionStatus.connected
|
||||||
: RuntimeConnectionStatus.offline,
|
: RuntimeConnectionStatus.offline,
|
||||||
primaryLabel: primaryLabel,
|
primaryLabel: primaryLabel,
|
||||||
detailLabel: detail.isEmpty
|
detailLabel: detail.isEmpty
|
||||||
? appText('未配置单机智能体', 'Single Agent is not configured')
|
? appText('未配置单机智能体', 'Single Agent is not configured')
|
||||||
: detail,
|
: detail,
|
||||||
ready: providerReady || fallbackReady,
|
ready: providerReady,
|
||||||
pairingRequired: false,
|
pairingRequired: false,
|
||||||
gatewayTokenMissing: false,
|
gatewayTokenMissing: false,
|
||||||
lastError: null,
|
lastError: null,
|
||||||
|
|||||||
@ -21,7 +21,6 @@ import '../runtime/runtime_models.dart';
|
|||||||
import '../runtime/secure_config_store.dart';
|
import '../runtime/secure_config_store.dart';
|
||||||
import '../runtime/embedded_agent_launch_policy.dart';
|
import '../runtime/embedded_agent_launch_policy.dart';
|
||||||
import '../runtime/runtime_coordinator.dart';
|
import '../runtime/runtime_coordinator.dart';
|
||||||
import '../runtime/direct_single_agent_app_server_client.dart';
|
|
||||||
import '../runtime/gateway_acp_client.dart';
|
import '../runtime/gateway_acp_client.dart';
|
||||||
import '../runtime/codex_runtime.dart';
|
import '../runtime/codex_runtime.dart';
|
||||||
import '../runtime/codex_config_bridge.dart';
|
import '../runtime/codex_config_bridge.dart';
|
||||||
@ -33,7 +32,6 @@ import '../runtime/mode_switcher.dart';
|
|||||||
import '../runtime/agent_registry.dart';
|
import '../runtime/agent_registry.dart';
|
||||||
import '../runtime/multi_agent_orchestrator.dart';
|
import '../runtime/multi_agent_orchestrator.dart';
|
||||||
import '../runtime/platform_environment.dart';
|
import '../runtime/platform_environment.dart';
|
||||||
import '../runtime/single_agent_runner.dart';
|
|
||||||
import '../runtime/skill_directory_access.dart';
|
import '../runtime/skill_directory_access.dart';
|
||||||
import 'app_controller_desktop_core.dart';
|
import 'app_controller_desktop_core.dart';
|
||||||
import 'app_controller_desktop_navigation.dart';
|
import 'app_controller_desktop_navigation.dart';
|
||||||
|
|||||||
@ -21,7 +21,6 @@ import '../runtime/runtime_models.dart';
|
|||||||
import '../runtime/secure_config_store.dart';
|
import '../runtime/secure_config_store.dart';
|
||||||
import '../runtime/embedded_agent_launch_policy.dart';
|
import '../runtime/embedded_agent_launch_policy.dart';
|
||||||
import '../runtime/runtime_coordinator.dart';
|
import '../runtime/runtime_coordinator.dart';
|
||||||
import '../runtime/direct_single_agent_app_server_client.dart';
|
|
||||||
import '../runtime/gateway_acp_client.dart';
|
import '../runtime/gateway_acp_client.dart';
|
||||||
import '../runtime/codex_runtime.dart';
|
import '../runtime/codex_runtime.dart';
|
||||||
import '../runtime/codex_config_bridge.dart';
|
import '../runtime/codex_config_bridge.dart';
|
||||||
@ -32,7 +31,6 @@ import '../runtime/mode_switcher.dart';
|
|||||||
import '../runtime/agent_registry.dart';
|
import '../runtime/agent_registry.dart';
|
||||||
import '../runtime/multi_agent_orchestrator.dart';
|
import '../runtime/multi_agent_orchestrator.dart';
|
||||||
import '../runtime/platform_environment.dart';
|
import '../runtime/platform_environment.dart';
|
||||||
import '../runtime/single_agent_runner.dart';
|
|
||||||
import '../runtime/skill_directory_access.dart';
|
import '../runtime/skill_directory_access.dart';
|
||||||
import 'app_controller_desktop_core.dart';
|
import 'app_controller_desktop_core.dart';
|
||||||
import 'app_controller_desktop_navigation.dart';
|
import 'app_controller_desktop_navigation.dart';
|
||||||
|
|||||||
@ -21,7 +21,6 @@ import '../runtime/runtime_models.dart';
|
|||||||
import '../runtime/secure_config_store.dart';
|
import '../runtime/secure_config_store.dart';
|
||||||
import '../runtime/embedded_agent_launch_policy.dart';
|
import '../runtime/embedded_agent_launch_policy.dart';
|
||||||
import '../runtime/runtime_coordinator.dart';
|
import '../runtime/runtime_coordinator.dart';
|
||||||
import '../runtime/direct_single_agent_app_server_client.dart';
|
|
||||||
import '../runtime/gateway_acp_client.dart';
|
import '../runtime/gateway_acp_client.dart';
|
||||||
import '../runtime/codex_runtime.dart';
|
import '../runtime/codex_runtime.dart';
|
||||||
import '../runtime/codex_config_bridge.dart';
|
import '../runtime/codex_config_bridge.dart';
|
||||||
@ -32,7 +31,6 @@ import '../runtime/mode_switcher.dart';
|
|||||||
import '../runtime/agent_registry.dart';
|
import '../runtime/agent_registry.dart';
|
||||||
import '../runtime/multi_agent_orchestrator.dart';
|
import '../runtime/multi_agent_orchestrator.dart';
|
||||||
import '../runtime/platform_environment.dart';
|
import '../runtime/platform_environment.dart';
|
||||||
import '../runtime/single_agent_runner.dart';
|
|
||||||
import '../runtime/skill_directory_access.dart';
|
import '../runtime/skill_directory_access.dart';
|
||||||
import 'app_controller_desktop_core.dart';
|
import 'app_controller_desktop_core.dart';
|
||||||
import 'app_controller_desktop_navigation.dart';
|
import 'app_controller_desktop_navigation.dart';
|
||||||
|
|||||||
@ -149,14 +149,6 @@ extension AppControllerWebSessions on AppController {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
bool singleAgentUsesAiChatFallbackForSession(String sessionKey) {
|
|
||||||
final provider = singleAgentProviderForSession(sessionKey);
|
|
||||||
return provider == SingleAgentProvider.auto && canUseAiGatewayConversation;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool get currentSingleAgentUsesAiChatFallback =>
|
|
||||||
singleAgentUsesAiChatFallbackForSession(currentSessionKeyInternal);
|
|
||||||
|
|
||||||
String singleAgentRuntimeModelForSession(String sessionKey) {
|
String singleAgentRuntimeModelForSession(String sessionKey) {
|
||||||
return taskThreadForSessionInternal(
|
return taskThreadForSessionInternal(
|
||||||
normalizedSessionKeyInternal(sessionKey),
|
normalizedSessionKeyInternal(sessionKey),
|
||||||
@ -174,12 +166,6 @@ extension AppControllerWebSessions on AppController {
|
|||||||
threadRecordsInternal[normalizedSessionKey]?.assistantModelId.trim() ??
|
threadRecordsInternal[normalizedSessionKey]?.assistantModelId.trim() ??
|
||||||
'';
|
'';
|
||||||
if (target == AssistantExecutionTarget.singleAgent) {
|
if (target == AssistantExecutionTarget.singleAgent) {
|
||||||
if (singleAgentUsesAiChatFallbackForSession(normalizedSessionKey)) {
|
|
||||||
if (recordModel.isNotEmpty) {
|
|
||||||
return recordModel;
|
|
||||||
}
|
|
||||||
return resolvedAiGatewayModel;
|
|
||||||
}
|
|
||||||
final runtimeModel = singleAgentRuntimeModelForSession(
|
final runtimeModel = singleAgentRuntimeModelForSession(
|
||||||
normalizedSessionKey,
|
normalizedSessionKey,
|
||||||
);
|
);
|
||||||
@ -189,7 +175,7 @@ extension AppControllerWebSessions on AppController {
|
|||||||
if (recordModel.isNotEmpty) {
|
if (recordModel.isNotEmpty) {
|
||||||
return recordModel;
|
return recordModel;
|
||||||
}
|
}
|
||||||
return resolvedAiGatewayModel;
|
return '';
|
||||||
}
|
}
|
||||||
if (recordModel.isNotEmpty) {
|
if (recordModel.isNotEmpty) {
|
||||||
return recordModel;
|
return recordModel;
|
||||||
@ -203,9 +189,6 @@ extension AppControllerWebSessions on AppController {
|
|||||||
List<String> assistantModelChoicesForSession(String sessionKey) {
|
List<String> assistantModelChoicesForSession(String sessionKey) {
|
||||||
final target = assistantExecutionTargetForSession(sessionKey);
|
final target = assistantExecutionTargetForSession(sessionKey);
|
||||||
if (target == AssistantExecutionTarget.singleAgent) {
|
if (target == AssistantExecutionTarget.singleAgent) {
|
||||||
if (singleAgentUsesAiChatFallbackForSession(sessionKey)) {
|
|
||||||
return aiGatewayConversationModelChoices;
|
|
||||||
}
|
|
||||||
final runtime = singleAgentRuntimeModelForSession(sessionKey);
|
final runtime = singleAgentRuntimeModelForSession(sessionKey);
|
||||||
if (runtime.isNotEmpty) {
|
if (runtime.isNotEmpty) {
|
||||||
return <String>[runtime];
|
return <String>[runtime];
|
||||||
@ -214,7 +197,7 @@ extension AppControllerWebSessions on AppController {
|
|||||||
if (recordModel.isNotEmpty) {
|
if (recordModel.isNotEmpty) {
|
||||||
return <String>[recordModel];
|
return <String>[recordModel];
|
||||||
}
|
}
|
||||||
return aiGatewayConversationModelChoices;
|
return const <String>[];
|
||||||
}
|
}
|
||||||
final model = settingsInternal.defaultModel.trim();
|
final model = settingsInternal.defaultModel.trim();
|
||||||
if (model.isEmpty) {
|
if (model.isEmpty) {
|
||||||
@ -285,7 +268,11 @@ extension AppControllerWebSessions on AppController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
bool get currentSingleAgentNeedsAiGatewayConfiguration =>
|
bool get currentSingleAgentNeedsAiGatewayConfiguration =>
|
||||||
currentSingleAgentUsesAiChatFallback && !canUseAiGatewayConversation;
|
assistantExecutionTargetForSession(currentSessionKeyInternal) ==
|
||||||
|
AssistantExecutionTarget.singleAgent &&
|
||||||
|
!availableSingleAgentProviders.any(
|
||||||
|
webAcpClientInternal.capabilities.providers.contains,
|
||||||
|
);
|
||||||
|
|
||||||
List<SecretReferenceEntry> get secretReferences {
|
List<SecretReferenceEntry> get secretReferences {
|
||||||
final entries = <SecretReferenceEntry>[
|
final entries = <SecretReferenceEntry>[
|
||||||
|
|||||||
@ -498,7 +498,6 @@ class AssistantEmptyStateInternal extends StatelessWidget {
|
|||||||
final connectionState = controller.currentAssistantConnectionState;
|
final connectionState = controller.currentAssistantConnectionState;
|
||||||
final singleAgent = connectionState.isSingleAgent;
|
final singleAgent = connectionState.isSingleAgent;
|
||||||
final connected = connectionState.connected;
|
final connected = connectionState.connected;
|
||||||
final singleAgentFallback = controller.currentSingleAgentUsesAiChatFallback;
|
|
||||||
final singleAgentNeedsAiGateway =
|
final singleAgentNeedsAiGateway =
|
||||||
controller.currentSingleAgentNeedsAiGatewayConfiguration;
|
controller.currentSingleAgentNeedsAiGatewayConfiguration;
|
||||||
final singleAgentSuggestsAcpSwitch =
|
final singleAgentSuggestsAcpSwitch =
|
||||||
@ -509,7 +508,7 @@ class AssistantEmptyStateInternal extends StatelessWidget {
|
|||||||
? connected
|
? connected
|
||||||
? appText('开始 ACP Server 任务', 'Start an ACP Server task')
|
? appText('开始 ACP Server 任务', 'Start an ACP Server task')
|
||||||
: singleAgentNeedsAiGateway
|
: singleAgentNeedsAiGateway
|
||||||
? appText('先配置 LLM API', 'Configure LLM API first')
|
? appText('先配置 ACP Server', 'Configure ACP Server first')
|
||||||
: appText('先准备 ACP Server', 'Prepare the ACP Server first')
|
: appText('先准备 ACP Server', 'Prepare the ACP Server first')
|
||||||
: connected
|
: connected
|
||||||
? appText('开始对话或运行任务', 'Start a chat or run a task')
|
? appText('开始对话或运行任务', 'Start a chat or run a task')
|
||||||
@ -518,15 +517,10 @@ class AssistantEmptyStateInternal extends StatelessWidget {
|
|||||||
: appText('先连接 Gateway', 'Connect a gateway first');
|
: appText('先连接 Gateway', 'Connect a gateway first');
|
||||||
final description = singleAgent
|
final description = singleAgent
|
||||||
? connected
|
? connected
|
||||||
? (singleAgentFallback
|
? appText(
|
||||||
? appText(
|
'当前线程通过 ACP Server 处理任务,不会建立 OpenClaw Gateway 会话。',
|
||||||
'当前没有可用的外部 Agent ACP 连接,这个线程已降级到 AI Chat fallback,不会建立 OpenClaw Gateway 会话。',
|
'This thread runs through the ACP Server path and does not open an OpenClaw Gateway session.',
|
||||||
'No external Agent ACP connection is available for this thread, so it is running in AI Chat fallback without opening an OpenClaw Gateway session.',
|
)
|
||||||
)
|
|
||||||
: appText(
|
|
||||||
'当前线程通过 ACP Server 处理任务,不会建立 OpenClaw Gateway 会话。',
|
|
||||||
'This thread runs through the ACP Server path and does not open an OpenClaw Gateway session.',
|
|
||||||
))
|
|
||||||
: singleAgentSuggestsAcpSwitch
|
: singleAgentSuggestsAcpSwitch
|
||||||
? appText(
|
? appText(
|
||||||
'当前线程固定为 $providerLabel,但它在这台设备上不可用。请改成可用的 ACP Server。',
|
'当前线程固定为 $providerLabel,但它在这台设备上不可用。请改成可用的 ACP Server。',
|
||||||
@ -534,8 +528,8 @@ class AssistantEmptyStateInternal extends StatelessWidget {
|
|||||||
)
|
)
|
||||||
: singleAgentNeedsAiGateway
|
: singleAgentNeedsAiGateway
|
||||||
? appText(
|
? appText(
|
||||||
'请先在 设置 -> 集成 中配置 LLM API Endpoint、LLM API Token 和默认模型,然后以 ACP Server 模式继续当前任务。',
|
'请先在 设置 -> 集成 中配置可用的外部 Agent ACP 端点,然后以 ACP Server 模式继续当前任务。',
|
||||||
'Set the LLM API Endpoint, LLM API Token, and default model in Settings -> Integrations, then continue this task in ACP Server mode.',
|
'Configure an external Agent ACP endpoint in Settings -> Integrations, then continue this task in ACP Server mode.',
|
||||||
)
|
)
|
||||||
: appText(
|
: appText(
|
||||||
'当前线程的外部 Agent ACP 连接尚未就绪。请先配置 $providerLabel 对应端点。',
|
'当前线程的外部 Agent ACP 连接尚未就绪。请先配置 $providerLabel 对应端点。',
|
||||||
|
|||||||
@ -1,6 +0,0 @@
|
|||||||
// Legacy compatibility surface retained while the app imports are cleaned up.
|
|
||||||
//
|
|
||||||
// The direct single-agent app-server runtime has been retired in favor of the
|
|
||||||
// GoTaskService ACP lane. This library intentionally exports only the capability
|
|
||||||
// DTOs still consumed by the UI-facing state layer.
|
|
||||||
export 'direct_single_agent_app_server_client_protocol.dart';
|
|
||||||
@ -1,14 +1,14 @@
|
|||||||
import 'runtime_models.dart';
|
import 'runtime_models.dart';
|
||||||
|
|
||||||
class DirectSingleAgentCapabilities {
|
class SingleAgentCapabilities {
|
||||||
const DirectSingleAgentCapabilities({
|
const SingleAgentCapabilities({
|
||||||
required this.available,
|
required this.available,
|
||||||
required this.supportedProviders,
|
required this.supportedProviders,
|
||||||
required this.endpoint,
|
required this.endpoint,
|
||||||
this.errorMessage,
|
this.errorMessage,
|
||||||
});
|
});
|
||||||
|
|
||||||
const DirectSingleAgentCapabilities.unavailable({
|
const SingleAgentCapabilities.unavailable({
|
||||||
required this.endpoint,
|
required this.endpoint,
|
||||||
this.errorMessage,
|
this.errorMessage,
|
||||||
}) : available = false,
|
}) : available = false,
|
||||||
@ -1,4 +0,0 @@
|
|||||||
// Legacy compatibility shim retained until remaining imports are cleaned up.
|
|
||||||
//
|
|
||||||
// Single-agent execution now flows through GoTaskService and the ACP
|
|
||||||
// transport; the previous direct runner no longer owns runtime strategy.
|
|
||||||
@ -116,14 +116,15 @@ class SkillsFocusPreviewInternal extends StatelessWidget {
|
|||||||
message: typedController.isSingleAgentMode
|
message: typedController.isSingleAgentMode
|
||||||
? (typedController.currentSingleAgentNeedsAiGatewayConfiguration
|
? (typedController.currentSingleAgentNeedsAiGatewayConfiguration
|
||||||
? appText(
|
? appText(
|
||||||
'当前没有可用的外部 Agent ACP 端点,请先配置 LLM API fallback。',
|
'当前没有可用的外部 Agent ACP 端点,请先配置 ACP Server。',
|
||||||
'No external Agent ACP endpoint is available. Configure LLM API fallback first.',
|
'No external Agent ACP endpoint is available. Configure an ACP server first.',
|
||||||
)
|
)
|
||||||
: appText(
|
: appText(
|
||||||
'当前线程还没有已加载技能。切换 provider 后会读取该线程自己的 skills 列表。',
|
'当前线程还没有已加载技能。切换 provider 后会读取该线程自己的 skills 列表。',
|
||||||
'No skills are loaded for this thread yet. Switching the provider reloads the thread-owned skills list.',
|
'No skills are loaded for this thread yet. Switching the provider reloads the thread-owned skills list.',
|
||||||
))
|
))
|
||||||
: typedController.connection.status == RuntimeConnectionStatus.connected
|
: typedController.connection.status ==
|
||||||
|
RuntimeConnectionStatus.connected
|
||||||
? appText(
|
? appText(
|
||||||
'当前代理没有已加载技能。',
|
'当前代理没有已加载技能。',
|
||||||
'No skills are loaded for the active agent.',
|
'No skills are loaded for the active agent.',
|
||||||
@ -306,7 +307,9 @@ class SecretsFocusPreviewInternal extends StatelessWidget {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final typedController = castAssistantFocusControllerInternal(controller);
|
final typedController = castAssistantFocusControllerInternal(controller);
|
||||||
final items = typedController.secretReferences.take(4).toList(growable: false);
|
final items = typedController.secretReferences
|
||||||
|
.take(4)
|
||||||
|
.toList(growable: false);
|
||||||
if (items.isEmpty) {
|
if (items.isEmpty) {
|
||||||
return PreviewEmptyStateInternal(
|
return PreviewEmptyStateInternal(
|
||||||
message: appText(
|
message: appText(
|
||||||
|
|||||||
@ -27,7 +27,6 @@ void main() {
|
|||||||
'lib/features/assistant/assistant_page_main.dart': 1000,
|
'lib/features/assistant/assistant_page_main.dart': 1000,
|
||||||
'lib/app/app_controller_desktop_runtime_helpers.dart': 800,
|
'lib/app/app_controller_desktop_runtime_helpers.dart': 800,
|
||||||
'lib/app/app_controller_desktop_single_agent.dart': 200,
|
'lib/app/app_controller_desktop_single_agent.dart': 200,
|
||||||
'lib/app/app_controller_desktop_single_agent_ai_gateway.dart': 800,
|
|
||||||
'lib/app/app_controller_desktop_single_agent_go_task_flow.dart': 800,
|
'lib/app/app_controller_desktop_single_agent_go_task_flow.dart': 800,
|
||||||
'lib/app/app_controller_desktop_single_agent_status_messages.dart': 400,
|
'lib/app/app_controller_desktop_single_agent_status_messages.dart': 400,
|
||||||
'lib/app/app_controller_desktop_external_acp_routing.dart': 400,
|
'lib/app/app_controller_desktop_external_acp_routing.dart': 400,
|
||||||
|
|||||||
@ -16,12 +16,10 @@ import 'package:xworkmate/runtime/runtime_coordinator.dart';
|
|||||||
import 'package:xworkmate/runtime/runtime_models.dart';
|
import 'package:xworkmate/runtime/runtime_models.dart';
|
||||||
import 'package:xworkmate/runtime/secure_config_store.dart';
|
import 'package:xworkmate/runtime/secure_config_store.dart';
|
||||||
import 'app_controller_ai_gateway_chat_suite_core.dart';
|
import 'app_controller_ai_gateway_chat_suite_core.dart';
|
||||||
import 'app_controller_ai_gateway_chat_suite_chat.dart';
|
|
||||||
import 'app_controller_ai_gateway_chat_suite_single_agent.dart';
|
import 'app_controller_ai_gateway_chat_suite_single_agent.dart';
|
||||||
import 'app_controller_ai_gateway_chat_suite_fakes.dart';
|
import 'app_controller_ai_gateway_chat_suite_fakes.dart';
|
||||||
import 'app_controller_ai_gateway_chat_suite_fixtures.dart';
|
import 'app_controller_ai_gateway_chat_suite_fixtures.dart';
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
registerAppControllerAiGatewayChatSuiteChatTestsInternal();
|
|
||||||
registerAppControllerAiGatewayChatSuiteSingleAgentTestsInternal();
|
registerAppControllerAiGatewayChatSuiteSingleAgentTestsInternal();
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,296 +0,0 @@
|
|||||||
// ignore_for_file: unused_import, unnecessary_import
|
|
||||||
|
|
||||||
import 'dart:async';
|
|
||||||
import 'dart:convert';
|
|
||||||
import 'dart:io';
|
|
||||||
import 'package:flutter_test/flutter_test.dart';
|
|
||||||
import 'package:shared_preferences/shared_preferences.dart';
|
|
||||||
import 'package:xworkmate/app/app_controller.dart';
|
|
||||||
import 'package:xworkmate/runtime/codex_runtime.dart';
|
|
||||||
import 'package:xworkmate/runtime/device_identity_store.dart';
|
|
||||||
import 'package:xworkmate/runtime/gateway_runtime.dart';
|
|
||||||
import 'package:xworkmate/runtime/runtime_coordinator.dart';
|
|
||||||
import 'package:xworkmate/runtime/runtime_models.dart';
|
|
||||||
import 'package:xworkmate/runtime/secure_config_store.dart';
|
|
||||||
import 'app_controller_ai_gateway_chat_suite_core.dart';
|
|
||||||
import 'app_controller_ai_gateway_chat_suite_single_agent.dart';
|
|
||||||
import 'app_controller_ai_gateway_chat_suite_fakes.dart';
|
|
||||||
import 'app_controller_ai_gateway_chat_suite_fixtures.dart';
|
|
||||||
|
|
||||||
void registerAppControllerAiGatewayChatSuiteChatTestsInternal() {
|
|
||||||
group('AI Gateway chat streaming', () {
|
|
||||||
test(
|
|
||||||
'AppController streams and restores persistent Single Agent conversation turns',
|
|
||||||
() async {
|
|
||||||
final tempDirectory = await createTempDirectoryInternal(
|
|
||||||
'xworkmate-ai-gateway-session-',
|
|
||||||
);
|
|
||||||
final server = await FakeAiGatewayServerInternal.start(
|
|
||||||
responseMode: AiGatewayResponseModeInternal.sse,
|
|
||||||
);
|
|
||||||
addTearDown(() async {
|
|
||||||
await server.close();
|
|
||||||
});
|
|
||||||
|
|
||||||
final store = createStoreFromTempDirectoryInternal(tempDirectory);
|
|
||||||
final gateway = FakeGatewayRuntimeInternal(store: store);
|
|
||||||
final controller = await createAppControllerInternal(
|
|
||||||
store: store,
|
|
||||||
availableSingleAgentProvidersOverride: const <SingleAgentProvider>[],
|
|
||||||
runtimeCoordinator: RuntimeCoordinator(
|
|
||||||
gateway: gateway,
|
|
||||||
codex: FakeCodexRuntimeInternal(),
|
|
||||||
),
|
|
||||||
goTaskServiceClient: FallbackOnlyGoTaskServiceClientInternal(),
|
|
||||||
);
|
|
||||||
|
|
||||||
await controller.settingsController.saveAiGatewayApiKey('live-key');
|
|
||||||
await controller.saveSettings(
|
|
||||||
controller.settings.copyWith(
|
|
||||||
aiGateway: controller.settings.aiGateway.copyWith(
|
|
||||||
baseUrl: server.baseUrl,
|
|
||||||
availableModels: const <String>['qwen2.5-coder:latest'],
|
|
||||||
selectedModels: const <String>['qwen2.5-coder:latest'],
|
|
||||||
),
|
|
||||||
defaultModel: 'gpt-5.4',
|
|
||||||
multiAgent: controller.settings.multiAgent.copyWith(
|
|
||||||
autoSync: false,
|
|
||||||
mountTargets: withAvailableMountTargetsInternal(
|
|
||||||
controller.settings.multiAgent.mountTargets,
|
|
||||||
const <String>[],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
refreshAfterSave: false,
|
|
||||||
);
|
|
||||||
await controller.setAssistantExecutionTarget(
|
|
||||||
AssistantExecutionTarget.singleAgent,
|
|
||||||
);
|
|
||||||
|
|
||||||
const firstQuestion =
|
|
||||||
'Execution context:\n'
|
|
||||||
'- target: single-agent\n'
|
|
||||||
'- permission: full-access\n\n'
|
|
||||||
'今天聊点什么';
|
|
||||||
const secondQuestion = '继续刚才的话题';
|
|
||||||
|
|
||||||
final firstTurn = controller.sendChatMessage(
|
|
||||||
firstQuestion,
|
|
||||||
thinking: 'low',
|
|
||||||
);
|
|
||||||
await waitForInternal(
|
|
||||||
() => controller.chatMessages.any(
|
|
||||||
(message) => message.role == 'assistant' && message.pending,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
expect(controller.hasAssistantPendingRun, isTrue);
|
|
||||||
server.allowCompletion(1);
|
|
||||||
await firstTurn;
|
|
||||||
|
|
||||||
await waitForInternal(
|
|
||||||
() => controller.chatMessages.any(
|
|
||||||
(message) =>
|
|
||||||
message.role == 'assistant' && message.text == 'FIRST_REPLY',
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
final secondStore = createStoreFromTempDirectoryInternal(tempDirectory);
|
|
||||||
final secondGateway = FakeGatewayRuntimeInternal(store: secondStore);
|
|
||||||
final secondController = await createAppControllerInternal(
|
|
||||||
store: secondStore,
|
|
||||||
availableSingleAgentProvidersOverride: const <SingleAgentProvider>[],
|
|
||||||
runtimeCoordinator: RuntimeCoordinator(
|
|
||||||
gateway: secondGateway,
|
|
||||||
codex: FakeCodexRuntimeInternal(),
|
|
||||||
),
|
|
||||||
goTaskServiceClient: FallbackOnlyGoTaskServiceClientInternal(),
|
|
||||||
);
|
|
||||||
|
|
||||||
await secondController.settingsController.saveAiGatewayApiKey(
|
|
||||||
'live-key',
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(secondController.chatMessages.last.text, 'FIRST_REPLY');
|
|
||||||
expect(
|
|
||||||
secondController.settings.assistantExecutionTarget,
|
|
||||||
AssistantExecutionTarget.singleAgent,
|
|
||||||
);
|
|
||||||
|
|
||||||
final secondTurn = secondController.sendChatMessage(
|
|
||||||
secondQuestion,
|
|
||||||
thinking: 'low',
|
|
||||||
);
|
|
||||||
await waitForInternal(
|
|
||||||
() => secondController.chatMessages.any(
|
|
||||||
(message) => message.role == 'assistant' && message.pending,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
server.allowCompletion(2);
|
|
||||||
await secondTurn;
|
|
||||||
|
|
||||||
await waitForInternal(
|
|
||||||
() => secondController.chatMessages.any(
|
|
||||||
(message) =>
|
|
||||||
message.role == 'assistant' && message.text == 'SECOND_REPLY',
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(server.requestCount, 2);
|
|
||||||
expect(server.lastAuthorization, 'Bearer live-key');
|
|
||||||
expect(server.requests.first['model'], 'qwen2.5-coder:latest');
|
|
||||||
expect(server.requests.first['stream'], isTrue);
|
|
||||||
expect(server.requests.first['messages'], <Map<String, dynamic>>[
|
|
||||||
<String, dynamic>{'role': 'user', 'content': firstQuestion},
|
|
||||||
]);
|
|
||||||
expect(server.requests.last['messages'], <Map<String, dynamic>>[
|
|
||||||
<String, dynamic>{'role': 'user', 'content': firstQuestion},
|
|
||||||
<String, dynamic>{'role': 'assistant', 'content': 'FIRST_REPLY'},
|
|
||||||
<String, dynamic>{'role': 'user', 'content': secondQuestion},
|
|
||||||
]);
|
|
||||||
expect(
|
|
||||||
secondController.connection.status,
|
|
||||||
RuntimeConnectionStatus.offline,
|
|
||||||
);
|
|
||||||
expect(secondController.assistantConnectionStatusLabel, '单机智能体');
|
|
||||||
expect(
|
|
||||||
secondController.assistantConnectionTargetLabel,
|
|
||||||
'AI Chat fallback · qwen2.5-coder:latest · 127.0.0.1:${server.port}',
|
|
||||||
);
|
|
||||||
expect(secondController.chatMessages.last.text, 'SECOND_REPLY');
|
|
||||||
expect(gateway.connectedProfiles, isEmpty);
|
|
||||||
expect(secondGateway.connectedProfiles, isEmpty);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
test('AppController falls back when LLM API ignores stream mode', () async {
|
|
||||||
final tempDirectory = await createTempDirectoryInternal(
|
|
||||||
'xworkmate-ai-gateway-json-fallback-',
|
|
||||||
);
|
|
||||||
final server = await FakeAiGatewayServerInternal.start(
|
|
||||||
responseMode: AiGatewayResponseModeInternal.json,
|
|
||||||
);
|
|
||||||
addTearDown(() async {
|
|
||||||
await server.close();
|
|
||||||
});
|
|
||||||
|
|
||||||
final store = createStoreFromTempDirectoryInternal(tempDirectory);
|
|
||||||
final controller = await createAppControllerInternal(
|
|
||||||
store: store,
|
|
||||||
availableSingleAgentProvidersOverride: const <SingleAgentProvider>[],
|
|
||||||
runtimeCoordinator: RuntimeCoordinator(
|
|
||||||
gateway: FakeGatewayRuntimeInternal(store: store),
|
|
||||||
codex: FakeCodexRuntimeInternal(),
|
|
||||||
),
|
|
||||||
goTaskServiceClient: FallbackOnlyGoTaskServiceClientInternal(),
|
|
||||||
);
|
|
||||||
|
|
||||||
await controller.settingsController.saveAiGatewayApiKey('live-key');
|
|
||||||
await controller.saveSettings(
|
|
||||||
controller.settings.copyWith(
|
|
||||||
aiGateway: controller.settings.aiGateway.copyWith(
|
|
||||||
baseUrl: server.baseUrl,
|
|
||||||
availableModels: const <String>['moonshotai/kimi-k2.5'],
|
|
||||||
selectedModels: const <String>['moonshotai/kimi-k2.5'],
|
|
||||||
),
|
|
||||||
defaultModel: 'moonshotai/kimi-k2.5',
|
|
||||||
multiAgent: controller.settings.multiAgent.copyWith(
|
|
||||||
autoSync: false,
|
|
||||||
mountTargets: withAvailableMountTargetsInternal(
|
|
||||||
controller.settings.multiAgent.mountTargets,
|
|
||||||
const <String>[],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
refreshAfterSave: false,
|
|
||||||
);
|
|
||||||
await controller.setAssistantExecutionTarget(
|
|
||||||
AssistantExecutionTarget.singleAgent,
|
|
||||||
);
|
|
||||||
|
|
||||||
await controller.sendChatMessage('你好', thinking: 'low');
|
|
||||||
|
|
||||||
await waitForInternal(
|
|
||||||
() => controller.chatMessages.any(
|
|
||||||
(message) =>
|
|
||||||
message.role == 'assistant' && message.text == 'FIRST_REPLY',
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(server.requests.single['stream'], isTrue);
|
|
||||||
expect(controller.chatMessages.last.pending, isFalse);
|
|
||||||
});
|
|
||||||
|
|
||||||
test(
|
|
||||||
'AppController abortRun stops Single Agent streaming requests',
|
|
||||||
() async {
|
|
||||||
final tempDirectory = await createTempDirectoryInternal(
|
|
||||||
'xworkmate-ai-gateway-abort-',
|
|
||||||
);
|
|
||||||
final server = await FakeAiGatewayServerInternal.start(
|
|
||||||
responseMode: AiGatewayResponseModeInternal.sse,
|
|
||||||
);
|
|
||||||
addTearDown(() async {
|
|
||||||
await server.close();
|
|
||||||
});
|
|
||||||
|
|
||||||
final store = createStoreFromTempDirectoryInternal(tempDirectory);
|
|
||||||
final controller = await createAppControllerInternal(
|
|
||||||
store: store,
|
|
||||||
availableSingleAgentProvidersOverride: const <SingleAgentProvider>[],
|
|
||||||
runtimeCoordinator: RuntimeCoordinator(
|
|
||||||
gateway: FakeGatewayRuntimeInternal(store: store),
|
|
||||||
codex: FakeCodexRuntimeInternal(),
|
|
||||||
),
|
|
||||||
goTaskServiceClient: FallbackOnlyGoTaskServiceClientInternal(),
|
|
||||||
);
|
|
||||||
|
|
||||||
await controller.settingsController.saveAiGatewayApiKey('live-key');
|
|
||||||
await controller.saveSettings(
|
|
||||||
controller.settings.copyWith(
|
|
||||||
aiGateway: controller.settings.aiGateway.copyWith(
|
|
||||||
baseUrl: server.baseUrl,
|
|
||||||
availableModels: const <String>['z-ai/glm5'],
|
|
||||||
selectedModels: const <String>['z-ai/glm5'],
|
|
||||||
),
|
|
||||||
defaultModel: 'z-ai/glm5',
|
|
||||||
multiAgent: controller.settings.multiAgent.copyWith(
|
|
||||||
autoSync: false,
|
|
||||||
mountTargets: withAvailableMountTargetsInternal(
|
|
||||||
controller.settings.multiAgent.mountTargets,
|
|
||||||
const <String>[],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
refreshAfterSave: false,
|
|
||||||
);
|
|
||||||
await controller.setAssistantExecutionTarget(
|
|
||||||
AssistantExecutionTarget.singleAgent,
|
|
||||||
);
|
|
||||||
|
|
||||||
final pendingTurn = controller.sendChatMessage(
|
|
||||||
'今天聊点什么',
|
|
||||||
thinking: 'low',
|
|
||||||
);
|
|
||||||
await waitForInternal(
|
|
||||||
() => controller.chatMessages.any(
|
|
||||||
(message) => message.role == 'assistant' && message.pending,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
await controller.abortRun();
|
|
||||||
server.allowCompletion(1);
|
|
||||||
await pendingTurn;
|
|
||||||
await waitForInternal(() => !controller.hasAssistantPendingRun);
|
|
||||||
|
|
||||||
expect(
|
|
||||||
controller.chatMessages.where((message) => message.pending),
|
|
||||||
isEmpty,
|
|
||||||
);
|
|
||||||
expect(
|
|
||||||
controller.chatMessages.where((message) => message.error),
|
|
||||||
isEmpty,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
@ -12,7 +12,6 @@ import 'package:xworkmate/runtime/gateway_runtime.dart';
|
|||||||
import 'package:xworkmate/runtime/runtime_coordinator.dart';
|
import 'package:xworkmate/runtime/runtime_coordinator.dart';
|
||||||
import 'package:xworkmate/runtime/runtime_models.dart';
|
import 'package:xworkmate/runtime/runtime_models.dart';
|
||||||
import 'package:xworkmate/runtime/secure_config_store.dart';
|
import 'package:xworkmate/runtime/secure_config_store.dart';
|
||||||
import 'app_controller_ai_gateway_chat_suite_chat.dart';
|
|
||||||
import 'app_controller_ai_gateway_chat_suite_single_agent.dart';
|
import 'app_controller_ai_gateway_chat_suite_single_agent.dart';
|
||||||
import 'app_controller_ai_gateway_chat_suite_fakes.dart';
|
import 'app_controller_ai_gateway_chat_suite_fakes.dart';
|
||||||
import 'app_controller_ai_gateway_chat_suite_fixtures.dart';
|
import 'app_controller_ai_gateway_chat_suite_fixtures.dart';
|
||||||
|
|||||||
@ -14,7 +14,6 @@ import 'package:xworkmate/runtime/runtime_coordinator.dart';
|
|||||||
import 'package:xworkmate/runtime/runtime_models.dart';
|
import 'package:xworkmate/runtime/runtime_models.dart';
|
||||||
import 'package:xworkmate/runtime/secure_config_store.dart';
|
import 'package:xworkmate/runtime/secure_config_store.dart';
|
||||||
import 'app_controller_ai_gateway_chat_suite_core.dart';
|
import 'app_controller_ai_gateway_chat_suite_core.dart';
|
||||||
import 'app_controller_ai_gateway_chat_suite_chat.dart';
|
|
||||||
import 'app_controller_ai_gateway_chat_suite_single_agent.dart';
|
import 'app_controller_ai_gateway_chat_suite_single_agent.dart';
|
||||||
import 'app_controller_ai_gateway_chat_suite_fixtures.dart';
|
import 'app_controller_ai_gateway_chat_suite_fixtures.dart';
|
||||||
|
|
||||||
|
|||||||
@ -14,7 +14,6 @@ import 'package:xworkmate/runtime/runtime_coordinator.dart';
|
|||||||
import 'package:xworkmate/runtime/runtime_models.dart';
|
import 'package:xworkmate/runtime/runtime_models.dart';
|
||||||
import 'package:xworkmate/runtime/secure_config_store.dart';
|
import 'package:xworkmate/runtime/secure_config_store.dart';
|
||||||
import 'app_controller_ai_gateway_chat_suite_core.dart';
|
import 'app_controller_ai_gateway_chat_suite_core.dart';
|
||||||
import 'app_controller_ai_gateway_chat_suite_chat.dart';
|
|
||||||
import 'app_controller_ai_gateway_chat_suite_single_agent.dart';
|
import 'app_controller_ai_gateway_chat_suite_single_agent.dart';
|
||||||
import 'app_controller_ai_gateway_chat_suite_fakes.dart';
|
import 'app_controller_ai_gateway_chat_suite_fakes.dart';
|
||||||
|
|
||||||
|
|||||||
@ -14,14 +14,13 @@ import 'package:xworkmate/runtime/runtime_coordinator.dart';
|
|||||||
import 'package:xworkmate/runtime/runtime_models.dart';
|
import 'package:xworkmate/runtime/runtime_models.dart';
|
||||||
import 'package:xworkmate/runtime/secure_config_store.dart';
|
import 'package:xworkmate/runtime/secure_config_store.dart';
|
||||||
import 'app_controller_ai_gateway_chat_suite_core.dart';
|
import 'app_controller_ai_gateway_chat_suite_core.dart';
|
||||||
import 'app_controller_ai_gateway_chat_suite_chat.dart';
|
|
||||||
import 'app_controller_ai_gateway_chat_suite_fakes.dart';
|
import 'app_controller_ai_gateway_chat_suite_fakes.dart';
|
||||||
import 'app_controller_ai_gateway_chat_suite_fixtures.dart';
|
import 'app_controller_ai_gateway_chat_suite_fixtures.dart';
|
||||||
|
|
||||||
void registerAppControllerAiGatewayChatSuiteSingleAgentTestsInternal() {
|
void registerAppControllerAiGatewayChatSuiteSingleAgentTestsInternal() {
|
||||||
group('Single Agent provider resolution', () {
|
group('Single Agent provider resolution', () {
|
||||||
test(
|
test(
|
||||||
'AppController uses the selected Single Agent provider before AI Chat fallback',
|
'AppController uses the selected Single Agent provider before ACP execution',
|
||||||
() async {
|
() async {
|
||||||
final tempDirectory = await createTempDirectoryInternal(
|
final tempDirectory = await createTempDirectoryInternal(
|
||||||
'xworkmate-single-agent-provider-',
|
'xworkmate-single-agent-provider-',
|
||||||
@ -607,10 +606,10 @@ void registerAppControllerAiGatewayChatSuiteSingleAgentTestsInternal() {
|
|||||||
);
|
);
|
||||||
|
|
||||||
test(
|
test(
|
||||||
'AppController falls back to AI Chat when no external CLI is available',
|
'AppController returns an ACP-only error when no provider is available',
|
||||||
() async {
|
() async {
|
||||||
final tempDirectory = await createTempDirectoryInternal(
|
final tempDirectory = await createTempDirectoryInternal(
|
||||||
'xworkmate-single-agent-fallback-',
|
'xworkmate-single-agent-acp-unavailable-',
|
||||||
);
|
);
|
||||||
final server = await FakeAiGatewayServerInternal.start(
|
final server = await FakeAiGatewayServerInternal.start(
|
||||||
responseMode: AiGatewayResponseModeInternal.json,
|
responseMode: AiGatewayResponseModeInternal.json,
|
||||||
@ -652,23 +651,12 @@ void registerAppControllerAiGatewayChatSuiteSingleAgentTestsInternal() {
|
|||||||
|
|
||||||
expect(client.capabilitiesCalls, greaterThanOrEqualTo(1));
|
expect(client.capabilitiesCalls, greaterThanOrEqualTo(1));
|
||||||
expect(client.executeCalls, 0);
|
expect(client.executeCalls, 0);
|
||||||
expect(server.requestCount, 1);
|
expect(server.requestCount, 0);
|
||||||
expect(
|
|
||||||
controller.chatMessages.any(
|
|
||||||
(message) => message.text.contains('Codex CLI is unavailable'),
|
|
||||||
),
|
|
||||||
isFalse,
|
|
||||||
);
|
|
||||||
expect(
|
|
||||||
controller.chatMessages.any(
|
|
||||||
(message) => message.toolName == 'AI Chat fallback',
|
|
||||||
),
|
|
||||||
isFalse,
|
|
||||||
);
|
|
||||||
expect(
|
expect(
|
||||||
controller.chatMessages.any(
|
controller.chatMessages.any(
|
||||||
(message) =>
|
(message) =>
|
||||||
message.role == 'assistant' && message.text == 'FIRST_REPLY',
|
message.role == 'assistant' &&
|
||||||
|
message.text.contains('当前没有可用的外部 Agent ACP 端点'),
|
||||||
),
|
),
|
||||||
isTrue,
|
isTrue,
|
||||||
);
|
);
|
||||||
@ -676,10 +664,10 @@ void registerAppControllerAiGatewayChatSuiteSingleAgentTestsInternal() {
|
|||||||
);
|
);
|
||||||
|
|
||||||
test(
|
test(
|
||||||
'AppController auto-binds a thread workspace in AI Chat fallback when the thread binding is missing',
|
'AppController auto-binds a thread workspace before reporting ACP unavailability',
|
||||||
() async {
|
() async {
|
||||||
final tempDirectory = await createTempDirectoryInternal(
|
final tempDirectory = await createTempDirectoryInternal(
|
||||||
'xworkmate-single-agent-fallback-missing-workspace-',
|
'xworkmate-single-agent-acp-unavailable-missing-workspace-',
|
||||||
);
|
);
|
||||||
final server = await FakeAiGatewayServerInternal.start(
|
final server = await FakeAiGatewayServerInternal.start(
|
||||||
responseMode: AiGatewayResponseModeInternal.json,
|
responseMode: AiGatewayResponseModeInternal.json,
|
||||||
@ -725,9 +713,17 @@ void registerAppControllerAiGatewayChatSuiteSingleAgentTestsInternal() {
|
|||||||
);
|
);
|
||||||
expect(client.capabilitiesCalls, greaterThanOrEqualTo(1));
|
expect(client.capabilitiesCalls, greaterThanOrEqualTo(1));
|
||||||
expect(client.executeCalls, 0);
|
expect(client.executeCalls, 0);
|
||||||
expect(server.requestCount, 1);
|
expect(server.requestCount, 0);
|
||||||
expect(workspacePath, isNotEmpty);
|
expect(workspacePath, isNotEmpty);
|
||||||
expect(workspacePath, contains('.xworkmate/threads/'));
|
expect(workspacePath, contains('.xworkmate/threads/'));
|
||||||
|
expect(
|
||||||
|
controller.chatMessages.any(
|
||||||
|
(message) =>
|
||||||
|
message.role == 'assistant' &&
|
||||||
|
message.text.contains('当前没有可用的外部 Agent ACP 端点'),
|
||||||
|
),
|
||||||
|
isTrue,
|
||||||
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|||||||
@ -49,7 +49,7 @@ void main() {
|
|||||||
);
|
);
|
||||||
|
|
||||||
test(
|
test(
|
||||||
'AppController keeps the current thread model source when only the global default target changes',
|
'AppController does not borrow LLM API model choices when single-agent has no ACP provider',
|
||||||
() async {
|
() async {
|
||||||
SharedPreferences.setMockInitialValues(<String, Object>{});
|
SharedPreferences.setMockInitialValues(<String, Object>{});
|
||||||
final tempDirectory = await Directory.systemTemp.createTemp(
|
final tempDirectory = await Directory.systemTemp.createTemp(
|
||||||
@ -82,10 +82,8 @@ void main() {
|
|||||||
AssistantExecutionTarget.singleAgent,
|
AssistantExecutionTarget.singleAgent,
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(controller.assistantModelChoices, const <String>[
|
expect(controller.assistantModelChoices, isEmpty);
|
||||||
'qwen2.5-coder:latest',
|
expect(controller.resolvedAssistantModel, isEmpty);
|
||||||
]);
|
|
||||||
expect(controller.resolvedAssistantModel, 'qwen2.5-coder:latest');
|
|
||||||
expect(controller.canUseAiGatewayConversation, isTrue);
|
expect(controller.canUseAiGatewayConversation, isTrue);
|
||||||
|
|
||||||
await controller.saveSettings(
|
await controller.saveSettings(
|
||||||
@ -100,10 +98,8 @@ void main() {
|
|||||||
),
|
),
|
||||||
AssistantExecutionTarget.singleAgent,
|
AssistantExecutionTarget.singleAgent,
|
||||||
);
|
);
|
||||||
expect(controller.resolvedAssistantModel, 'qwen2.5-coder:latest');
|
expect(controller.resolvedAssistantModel, isEmpty);
|
||||||
expect(controller.assistantModelChoices, const <String>[
|
expect(controller.assistantModelChoices, isEmpty);
|
||||||
'qwen2.5-coder:latest',
|
|
||||||
]);
|
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -143,7 +139,6 @@ void main() {
|
|||||||
await controller.setSingleAgentProvider(SingleAgentProvider.opencode);
|
await controller.setSingleAgentProvider(SingleAgentProvider.opencode);
|
||||||
|
|
||||||
expect(controller.currentSingleAgentHasResolvedProvider, isTrue);
|
expect(controller.currentSingleAgentHasResolvedProvider, isTrue);
|
||||||
expect(controller.currentSingleAgentUsesAiChatFallback, isFalse);
|
|
||||||
expect(controller.currentSingleAgentShouldShowModelControl, isFalse);
|
expect(controller.currentSingleAgentShouldShowModelControl, isFalse);
|
||||||
expect(controller.assistantModelChoices, isEmpty);
|
expect(controller.assistantModelChoices, isEmpty);
|
||||||
expect(controller.resolvedAssistantModel, isEmpty);
|
expect(controller.resolvedAssistantModel, isEmpty);
|
||||||
|
|||||||
@ -123,7 +123,7 @@ void main() {
|
|||||||
);
|
);
|
||||||
|
|
||||||
test(
|
test(
|
||||||
'AppController keeps AI Gateway model choices when single-agent falls back to AI chat',
|
'AppController keeps single-agent model controls empty when no ACP provider is available',
|
||||||
() async {
|
() async {
|
||||||
final harness = await _DesktopControllerHarness.create(
|
final harness = await _DesktopControllerHarness.create(
|
||||||
availableSingleAgentProvidersOverride: const <SingleAgentProvider>[],
|
availableSingleAgentProvidersOverride: const <SingleAgentProvider>[],
|
||||||
@ -147,12 +147,10 @@ void main() {
|
|||||||
);
|
);
|
||||||
|
|
||||||
expect(controller.currentSingleAgentHasResolvedProvider, isFalse);
|
expect(controller.currentSingleAgentHasResolvedProvider, isFalse);
|
||||||
expect(controller.currentSingleAgentUsesAiChatFallback, isTrue);
|
expect(controller.currentSingleAgentNeedsAiGatewayConfiguration, isTrue);
|
||||||
expect(controller.currentSingleAgentShouldShowModelControl, isTrue);
|
expect(controller.currentSingleAgentShouldShowModelControl, isFalse);
|
||||||
expect(controller.assistantModelChoices, const <String>[
|
expect(controller.assistantModelChoices, isEmpty);
|
||||||
'qwen2.5-coder:latest',
|
expect(controller.resolvedAssistantModel, isEmpty);
|
||||||
]);
|
|
||||||
expect(controller.resolvedAssistantModel, 'qwen2.5-coder:latest');
|
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -147,7 +147,7 @@ void registerExecutionTargetSwitchConnectionTests() {
|
|||||||
expect(controller.assistantConnectionStatusLabel, 'ACP Server Local');
|
expect(controller.assistantConnectionStatusLabel, 'ACP Server Local');
|
||||||
expect(
|
expect(
|
||||||
controller.assistantConnectionTargetLabel,
|
controller.assistantConnectionTargetLabel,
|
||||||
'没有可用的外部 Agent ACP 端点,请配置 LLM API fallback。',
|
'没有可用的外部 Agent ACP 端点,请先配置可用的 ACP Server。',
|
||||||
);
|
);
|
||||||
expect(
|
expect(
|
||||||
gateway.connectedProfiles,
|
gateway.connectedProfiles,
|
||||||
|
|||||||
@ -198,7 +198,7 @@ void registerExecutionTargetSwitchThreadTests() {
|
|||||||
expect(controller.assistantConnectionStatusLabel, 'ACP Server Local');
|
expect(controller.assistantConnectionStatusLabel, 'ACP Server Local');
|
||||||
expect(
|
expect(
|
||||||
controller.assistantConnectionTargetLabel,
|
controller.assistantConnectionTargetLabel,
|
||||||
'没有可用的外部 Agent ACP 端点,请配置 LLM API fallback。',
|
'没有可用的外部 Agent ACP 端点,请先配置可用的 ACP Server。',
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
@ -240,19 +240,20 @@ void registerExecutionTargetSwitchThreadTests() {
|
|||||||
await controller.setAssistantExecutionTarget(
|
await controller.setAssistantExecutionTarget(
|
||||||
AssistantExecutionTarget.local,
|
AssistantExecutionTarget.local,
|
||||||
);
|
);
|
||||||
controller.chatControllerInternal.messagesInternal = <GatewayChatMessage>[
|
controller.chatControllerInternal.messagesInternal =
|
||||||
GatewayChatMessage(
|
<GatewayChatMessage>[
|
||||||
id: 'gateway-old-message',
|
GatewayChatMessage(
|
||||||
role: 'assistant',
|
id: 'gateway-old-message',
|
||||||
text: 'previous desktop gateway history',
|
role: 'assistant',
|
||||||
timestampMs: DateTime.now().millisecondsSinceEpoch.toDouble(),
|
text: 'previous desktop gateway history',
|
||||||
toolCallId: null,
|
timestampMs: DateTime.now().millisecondsSinceEpoch.toDouble(),
|
||||||
toolName: null,
|
toolCallId: null,
|
||||||
stopReason: null,
|
toolName: null,
|
||||||
pending: false,
|
stopReason: null,
|
||||||
error: false,
|
pending: false,
|
||||||
),
|
error: false,
|
||||||
];
|
),
|
||||||
|
];
|
||||||
|
|
||||||
controller.initializeAssistantThreadContext(
|
controller.initializeAssistantThreadContext(
|
||||||
'draft:fresh-thread',
|
'draft:fresh-thread',
|
||||||
|
|||||||
@ -56,6 +56,9 @@ void main() {
|
|||||||
|
|
||||||
test('legacy direct single-agent runtime implementation stays removed', () {
|
test('legacy direct single-agent runtime implementation stays removed', () {
|
||||||
const removedFiles = <String>[
|
const removedFiles = <String>[
|
||||||
|
'lib/runtime/direct_single_agent_app_server_client.dart',
|
||||||
|
'lib/runtime/direct_single_agent_app_server_client_protocol.dart',
|
||||||
|
'lib/runtime/single_agent_runner.dart',
|
||||||
'lib/runtime/direct_single_agent_app_server_client_core.dart',
|
'lib/runtime/direct_single_agent_app_server_client_core.dart',
|
||||||
'lib/runtime/direct_single_agent_app_server_client_helpers.dart',
|
'lib/runtime/direct_single_agent_app_server_client_helpers.dart',
|
||||||
'lib/runtime/direct_single_agent_app_server_client_transport.dart',
|
'lib/runtime/direct_single_agent_app_server_client_transport.dart',
|
||||||
@ -65,15 +68,10 @@ void main() {
|
|||||||
expect(
|
expect(
|
||||||
File(relativePath).existsSync(),
|
File(relativePath).existsSync(),
|
||||||
isFalse,
|
isFalse,
|
||||||
reason: '$relativePath should stay removed after GoTaskService cutover',
|
reason:
|
||||||
|
'$relativePath should stay removed after GoTaskService cutover',
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
final runnerShim = File('lib/runtime/single_agent_runner.dart');
|
|
||||||
expect(runnerShim.existsSync(), isTrue);
|
|
||||||
final shimContent = runnerShim.readAsStringSync();
|
|
||||||
expect(shimContent.contains('DefaultSingleAgentRunner'), isFalse);
|
|
||||||
expect(shimContent.contains('DirectSingleAgentAppServerClient'), isFalse);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user