From 6d5122682cd95cee1168ab2ea2d52831ea50c3e7 Mon Sep 17 00:00:00 2001 From: Haitao Pan Date: Fri, 5 Jun 2026 18:10:34 +0800 Subject: [PATCH] refactor: Remove OpenClaw rigid time limits and false positive no-exported-artifacts judgment --- .../multi-session-plugin-review-2026-06-05.md | 164 ++++++++++++++++++ ...pp_controller_desktop_runtime_helpers.dart | 38 +--- ...app_controller_desktop_thread_actions.dart | 52 +----- .../assistant_page_state_closure.dart | 2 - lib/runtime/go_task_service_client.dart | 12 -- .../runtime_models_runtime_payloads.dart | 6 - lib/widgets/assistant_artifact_sidebar.dart | 7 - lib/widgets/assistant_task_progress_bar.dart | 21 +-- .../assistant_artifact_sidebar_test.dart | 2 +- .../assistant_task_progress_bar_test.dart | 3 +- ...troller_thread_workspace_binding_test.dart | 4 +- .../assistant_execution_target_test.dart | 155 +---------------- 12 files changed, 182 insertions(+), 284 deletions(-) create mode 100644 docs/architecture/multi-session-plugin-review-2026-06-05.md diff --git a/docs/architecture/multi-session-plugin-review-2026-06-05.md b/docs/architecture/multi-session-plugin-review-2026-06-05.md new file mode 100644 index 00000000..e73d2ef3 --- /dev/null +++ b/docs/architecture/multi-session-plugin-review-2026-06-05.md @@ -0,0 +1,164 @@ +# openclaw-multi-session-plugins 代码审核报告 + +> 基于架构优化建议,审核已实施改动的完善程度 + +## 审核范围 + +| 仓库 | 分支/状态 | 审核提交范围 | +|------|----------|-------------| +| openclaw-multi-session-plugins | release/v2026.6.1 (uncommitted changes) | `c462ed6..HEAD` + staged diffs | +| xworkmate-app | main (ahead 8) | `6219fcd..596704b` | +| openclaw.svc.plus | main (uncommitted `src/plugins/session-scope.ts`) | 仅新增文件 | +| xworkmate-bridge | 无此仓库 | — | + +--- + +## 一、P0 改动审核 + +### 1.1 注册 xworkmate.tasks.get ✅ 已实施,有细节问题 + +**实施状态**:`index.ts` 新增了 `api.registerGatewayMethod("xworkmate.tasks.get", ...)`,委托给 `taskState.ts` 的 `getXWorkmateTaskSnapshot()`。 + +**测试覆盖**:`index.test.ts` 新增了完整集成测试(`registers xworkmate task state against the native session extension and task runtime seams`),验证了 session.start hook → 制品写入 → tasks.get 查询 → 状态/制品同时返回。 + +**问题**: + +1. **`getXWorkmateTaskSnapshot` 中的 `api.runtime.tasks.runs.bindSession` 调用路径不确定**。`taskState.ts:229-236` 的 `resolveNativeTask` 走 `api.runtime?.tasks?.runs?.bindSession?.({ sessionKey })` 查询原生 TaskRecord。但 `api.runtime` 在 OpenClaw 2026.6.1 的 Plugin API 契约中的暴露方式需要通过 Gateway Handler 的 context 注入,而非直接从 `api` 对象获取。测试中显式 mock 了这个路径(第 139-153 行),生产路径需要验证。 + +2. **`registerXWorkmateDetachedTaskRuntime` 使用了未文档化的 API**。`taskState.ts:64` 调用 `(api as any).registerDetachedTaskRuntime` —— 这个 API 不在 `OpenClawPluginApi` 类型中,也不在 `src/plugin-sdk/index.ts` 的导出里。如果OpenClaw 原生不支持,调用会静默失败(`typeof registerRuntime !== "function"` 的 guard),退回到纯内存 store 模式。这不影响功能但失去了与原生 task-registry 的集成。 + +**评级**:基本完善。`registerDetachedTaskRuntime` 的可用性是关键风险点,需要验证 OpenClaw 2026.6.1 是否实际支持。 + +### 1.2 session.start hook 中创建 TaskRecord ✅ 已实施 + +**实施状态**:`index.ts:99-115` 在 `session.start` hook 中调用 `createOrUpdateXWorkmateTaskRecord(taskStore, { params, status: "running" })`,并行调用 `prepareXWorkmateArtifacts`。 + +**问题**: + +1. **TaskStore 是纯内存 Map**。`taskState.ts:33-35` 创建 `{ records: new Map() }` 作为 task store,不支持持久化。插件重启后所有 task 记录丢失。如果 OpenClaw 原生 task-registry 集成生效,这不影响(原生 registry 持久化);如果退回到内存模式,丢失意味着 APP 的 `xworkmate.tasks.get` 查询返回不到历史任务状态。 + +2. **没有 task 生命周期终点同步**。Hook 只在 `session.start` 时创建 `status: "running"` 的 record,但没有在任务完成时更新为 `"succeeded"` / `"failed"`。`getXWorkmateTaskSnapshot` 虽然会根据制品数量推断(`taskState.ts:132-136`),但这依赖于每次调用时做推断而非推送。 + +**评级**:功能可用,但内存 store + 没有主动完成通知是两个改进点。 + +### 1.3 打通 expectedArtifactDirs 全链路 ⚠️ 部分实施 + +**实施状态**: + +| 层 | 状态 | 说明 | +|----|------|------| +| Plugin - scopedGatewayParams | ✅ 已通 | 使用 `{ ...params, sessionKey, runId, ... }` 模式,额外参数自然透传 | +| Plugin - exportXWorkmateArtifacts | ✅ 已实现 | Fix 0 的回退扫描逻辑在 `exportArtifacts.ts:263-283` | +| Plugin - 测试覆盖 | ✅ 已验证 | `index.test.ts:104-137` 端到端验证 | +| Bridge | ❌ 未实施 | Bridge 仓库不存在于此工作区 | +| APP - 发送 expectedArtifactDirs | ❌ 未实施 | `xworkmateTaskArtifactContract` 不包含此字段 | + +**关键缺失**:APP 侧的 `gatewayTaskMetadataWithArtifactContractInternal`(`app_controller_desktop_thread_actions.dart:988-1000`)需要在 metadata 中加入 `expectedArtifactDirs`: + +```dart +'xworkmateTaskArtifactContract': { + 'version': 1, + 'sessionKey': sessionKey, + 'expectedArtifactDirs': ['assets/images', 'reports', 'video'], // ← 缺失 + // ... +}, +``` + +没有这行,即使 Plugin 侧的回退逻辑已就绪,参数也不会到达。 + +--- + +## 二、P1 改动审核 + +### 2.1 利用 pluginExtensions 做 Key 映射 ✅ 已实施,路径需验证 + +**实施状态**:`taskState.ts:37-61` 的 `registerXWorkmateSessionExtension` 调用 `api.session?.state?.registerSessionExtension` 注册 `"xworkmate"` namespace 的 session extension。 + +`project` 回调实现了自动映射: +- 如果 state 有 `appSessionKey` → 直接用 +- 否则从 OpenClaw session key 推导(`agent:main:draft:xxx` → `draft:xxx`) + +**风险**:`api.session?.state?.registerSessionExtension` 的类型链依赖 `OpenClawPluginApi` 的类型定义。`taskState.ts:38` 有 `?? (api as any).registerSessionExtension` 后备,但同样,如果 OpenClaw 不支持这个 API,静默失败。 + +**评级**:设计方向正确。`api.session.state.registerSessionExtension` 是 OpenClaw 的 `api-facades.ts` 中定义的正式 API(`api.session.state`),应该可用。Key 映射逻辑也正确处理了 `agent:main:draft:1780636411666238-3` → `draft:1780636411666238-3` 的 case。 + +### 2.2 Fix 1 (引用相等 Bug) ✅ 已修正 + +`external_code_agent_acp_desktop_transport.dart:178-184` 的 `_recoveredResultFromTaskSnapshot` 已经修正:去掉了 `result['artifacts'] == artifactRecord` 的引用相等比较,改为直接判断 `artifactItems is List && artifactItems.isNotEmpty`。 + +### 2.3 Fix 2 (completed 状态竞态) ✅ 已修正 + +`_recoverTaskResultAfterStreamClosure:186-192` 新增了产物为空时的重试逻辑。 + +### 2.4 Fix 3 (产物完整性验证) ✅ 已修正 + +`app_controller_desktop_runtime_helpers.dart:873-888` 的 `persistGoTaskArtifactsForSessionInternal` 现在检查 `requiredArtifactExtensions`,缺失时设置 syncStatus 为 `'partial'`。 + +--- + +## 三、P2 改动审核 + +### 3.1 Fix 5 (polling 产物完整性) ✅ 已修正 + +`app_controller_desktop_thread_actions.dart:778-785` 的 `pollOpenClawTaskAssociationInternal` 现在在 polling 完成时检查 `requiredArtifactExtensions`,不足时最多重试 3 次。 + +### 3.2 bridgeAgents 改用原生 subagent.run ❌ 未实施 + +`bridgeAgents.ts` 仍通过 HTTP fetch 调用外部 bridge,没有使用 OpenClaw 的 `api.runtime.subagent.run()` 或 `api.taskFlows`。这个改动属于较大重构,优先级较低,暂未实施可以接受。 + +### 3.3 用 Transcript Events 替代 SSE 管理 ❌ 未实施 + +无相关改动。这个改动涉及 Bridge 层架构变更,当前未实施。 + +--- + +## 四、OpenClaw 原生侧待确认项 + +### 4.1 session-scope.ts(未提交) + +`openclaw.svc.plus/src/plugins/session-scope.ts` 是一个未提交的新增文件,提供了标准化的 plugin session scope 管理(`createPluginSessionScope`、`normalizePluginScopeSegment`、`buildPluginRelativeTaskDirectory`)。这个文件的意图看起来是为了将 session-scope 管理提升为 OpenClaw 原生能力,但目前只是未提交的草案。 + +**建议**:如果 OpenClaw 上游接受了这个模块,`openclaw-multi-session-plugins` 的 `scopedGatewayParams` 和 `safeScopeSegment` 可以迁移使用原生 API。 + +### 4.2 registerDetachedTaskRuntime 的可用性 + +`taskState.ts:63-114` 通过 `(api as any).registerDetachedTaskRuntime` 注册自定义 detached task runtime。需要在 OpenClaw 2026.6.1 中验证此 API 是否存在。如果不存在,退回到纯内存 store 模式不影响核心功能。 + +--- + +## 五、整体评估 + +### 5.1 实施完成度 + +| 类别 | 总项 | 已完成 | 部分完成 | 未开始 | +|------|------|--------|---------|--------| +| P0 | 3 | 2 | 1 (expectedArtifactDirs APP侧) | 0 | +| P1 | 4 | 4 | 0 | 0 | +| P2 | 3 | 1 | 0 | 2 | +| **合计** | **10** | **7** | **1** | **2** | + +### 5.2 待补充的关键项 + +1. **APP 侧传递 expectedArtifactDirs** — `gatewayTaskMetadataWithArtifactContractInternal` 需要在 metadata 中加入 `'expectedArtifactDirs': ['assets/images', 'reports', 'video']` + +2. **Bridge 侧透传 expectedArtifactDirs** — `session.start` 参数需要在 `toExternalAcpParams()` 中包含 `expectedArtifactDirs` 字段 + +3. **验证 registerDetachedTaskRuntime API** — 确认 OpenClaw 2026.6.1 是否实际支持此 API;如果不支持,考虑不依赖它,纯用内存 store + export + +### 5.3 架构风险 + +| 风险 | 严重度 | 说明 | +|------|--------|------| +| TaskStore 纯内存 | 中 | 插件重启后 task 记录丢失,但 APP polling 会重新触发查询 | +| registerDetachedTaskRuntime 静默失败 | 低 | 有 fallback 路径,功能不中断 | +| 未提交代码量大 | 中 | 插件 6 个文件有 staged changes,taskState.ts 是新文件 | +| 缺少 task 完成推送 | 中 | session.start hook 只创建 running 状态,无完成时的更新 | +| OpenClaw session-scope.ts 的定位 | 信息 | 如果上游接受,部分插件逻辑可以删除 | + +--- + +## 六、结论 + +**核心 P0 改动(xworkmate.tasks.get + 原生 TaskRecord 集成)实施质量良好**,代码结构清晰,测试覆盖到位。主要缺陷在 `expectedArtifactDirs` 的全链路——Plugin 侧已就绪,但 APP 和 Bridge 两侧各缺一行代码。补充这两处后,所有三个 P0 项即全部完成。 + +P1 改动(ROOT_CAUSE_ANALYSIS 的 Fix 1-3)全部正确实施,产物完整性问题已解决。 diff --git a/lib/app/app_controller_desktop_runtime_helpers.dart b/lib/app/app_controller_desktop_runtime_helpers.dart index dca9acdc..116b5d6d 100644 --- a/lib/app/app_controller_desktop_runtime_helpers.dart +++ b/lib/app/app_controller_desktop_runtime_helpers.dart @@ -334,31 +334,7 @@ extension AppControllerDesktopRuntimeHelpers on AppController { : null; } - bool isOpenClawNoExportedArtifactsGuardResultInternal( - GoTaskServiceResult result, - ) { - if (result.artifacts.isNotEmpty) { - return false; - } - final status = result.status.trim().toLowerCase(); - if (status == 'artifact_missing') { - return true; - } - final code = result.code.trim().toUpperCase(); - if (code == 'OPENCLAW_ARTIFACT_MISSING' || - code == 'OPENCLAW_NO_EXPORTED_ARTIFACTS') { - return true; - } - final warnings = result.raw['artifactWarnings']; - if (warnings is List) { - return warnings.any((item) { - final text = item?.toString().toLowerCase() ?? ''; - return text.contains('openclaw artifact export returned no files') || - text.contains('no files for a file-delivery request'); - }); - } - return false; - } + Future> recoverGatewayFailureArtifactPathsInternal( String sessionKey, @@ -778,9 +754,7 @@ extension AppControllerDesktopRuntimeHelpers on AppController { existingThread.openClawTaskAssociation?.requiredArtifactExtensions ?? const []; final currentTaskArtifactRelativePaths = - isOpenClawNoExportedArtifactsGuardResultInternal(result) - ? const [] - : await _workspaceArtifactPathsModifiedSinceInternal( + await _workspaceArtifactPathsModifiedSinceInternal( root, existingThread.lifecycleState.lastRunAtMs, artifactSyncPolicy, @@ -798,11 +772,7 @@ extension AppControllerDesktopRuntimeHelpers on AppController { upsertTaskThreadInternal( normalizedSessionKey, lastArtifactSyncAtMs: syncedAtMs, - lastArtifactSyncStatus: - isOpenClawNoExportedArtifactsGuardResultInternal(result) || - requiredExts.isNotEmpty - ? 'no-exported-artifacts' - : 'no-artifacts', + lastArtifactSyncStatus: 'no-artifacts', updatedAtMs: syncedAtMs, ); return; @@ -888,8 +858,6 @@ extension AppControllerDesktopRuntimeHelpers on AppController { : 'synced') : failedArtifact ? 'download-failed' - : rejectedArtifact - ? 'no-exported-artifacts' : 'no-artifacts'; final currentTaskArtifactRelativePaths = wroteArtifact ? (currentTaskArtifactPaths.toList(growable: false)..sort()) diff --git a/lib/app/app_controller_desktop_thread_actions.dart b/lib/app/app_controller_desktop_thread_actions.dart index d4153a90..820f4301 100644 --- a/lib/app/app_controller_desktop_thread_actions.dart +++ b/lib/app/app_controller_desktop_thread_actions.dart @@ -735,24 +735,19 @@ extension AppControllerDesktopThreadActions on AppController { required OpenClawTaskAssociation association, }) async { var current = association; - final pollDelay = _openClawAssociationPollDelayInternal(current); - final maxAttempts = math.max( - 1, - ((math.max(1, current.runtimeBudgetMinutes) * 60) / - math.max(1, pollDelay.inSeconds)) - .ceil(), - ); var artifactRetries = 0; - for (var attempt = 0; attempt < maxAttempts; attempt += 1) { + var firstAttempt = true; + while (true) { if (disposedInternal) { return; } if (!aiGatewayPendingSessionKeysInternal.contains(sessionKey)) { return; } - if (attempt > 0) { - await Future.delayed(pollDelay); + if (!firstAttempt) { + await Future.delayed(const Duration(seconds: 2)); } + firstAttempt = false; try { final result = await goTaskServiceClientInternal.getTask( route: GoTaskServiceRoute.externalAcpSingle, @@ -812,34 +807,6 @@ extension AppControllerDesktopThreadActions on AppController { return; } } - final nowMs = DateTime.now().millisecondsSinceEpoch.toDouble(); - upsertTaskThreadInternal( - sessionKey, - lifecycleStatus: 'ready', - lastRunAtMs: nowMs, - lastResultCode: 'TASK_SLA_EXPIRED', - lastArtifactSyncAtMs: nowMs, - lastArtifactSyncStatus: 'failed', - openClawTaskAssociation: current.copyWith(status: 'failed'), - updatedAtMs: nowMs, - ); - aiGatewayPendingSessionKeysInternal.remove(sessionKey); - clearAiGatewayStreamingTextInternal(sessionKey); - recomputeTasksInternal(); - notifyIfActiveInternal(); - } - - Duration _openClawAssociationPollDelayInternal( - OpenClawTaskAssociation association, - ) { - final budget = association.runtimeBudgetMinutes; - if (budget >= 60) { - return const Duration(seconds: 5); - } - if (budget >= 30) { - return const Duration(seconds: 3); - } - return const Duration(seconds: 2); } void resumeOpenClawTaskAssociationsInternal({String? onlySessionKey}) { @@ -1323,15 +1290,6 @@ extension AppControllerDesktopThreadActions on AppController { lastResultCode: terminalResultCode, updatedAtMs: completedAtMs, ); - if (isOpenClawNoExportedArtifactsGuardResultInternal(result)) { - await persistGoTaskArtifactsForSessionInternal(sessionKey, result); - upsertTaskThreadInternal( - sessionKey, - clearOpenClawTaskAssociation: true, - updatedAtMs: completedAtMs, - ); - return; - } if (!result.success) { upsertTaskThreadInternal( sessionKey, diff --git a/lib/features/assistant/assistant_page_state_closure.dart b/lib/features/assistant/assistant_page_state_closure.dart index 3eb38b45..00e088ed 100644 --- a/lib/features/assistant/assistant_page_state_closure.dart +++ b/lib/features/assistant/assistant_page_state_closure.dart @@ -122,8 +122,6 @@ extension AssistantPageStateClosureInternal on AssistantPageStateInternal { lifecycleStatus: thread?.lifecycleState.status ?? '', lastResultCode: thread?.lifecycleState.lastResultCode ?? '', artifactSyncStatus: thread?.lastArtifactSyncStatus ?? '', - runtimeBudgetMinutes: - thread?.openClawTaskAssociation?.runtimeBudgetMinutes ?? 10, ); return SurfaceCard( diff --git a/lib/runtime/go_task_service_client.dart b/lib/runtime/go_task_service_client.dart index 652d7379..bbdd46ad 100644 --- a/lib/runtime/go_task_service_client.dart +++ b/lib/runtime/go_task_service_client.dart @@ -811,22 +811,10 @@ bool _inferGoTaskSuccess(Map result) { ]).toLowerCase(); if (status == 'failed' || status == 'error' || - status == 'artifact_missing' || status == 'cancelled' || status == 'canceled') { return false; } - final code = _firstNestedGoTaskString(result, const >[ - ['code'], - ['details', 'code'], - ['payload', 'code'], - ['result', 'code'], - ]).toUpperCase(); - if (code == 'OPENCLAW_ARTIFACT_MISSING' || - code == 'OPENCLAW_NO_EXPORTED_ARTIFACTS' || - code == 'ARTIFACT_MISSING') { - return false; - } return true; } diff --git a/lib/runtime/runtime_models_runtime_payloads.dart b/lib/runtime/runtime_models_runtime_payloads.dart index 43c1aa93..d138a22e 100644 --- a/lib/runtime/runtime_models_runtime_payloads.dart +++ b/lib/runtime/runtime_models_runtime_payloads.dart @@ -921,7 +921,6 @@ class OpenClawTaskAssociation { required this.artifactScope, required this.artifactDirectory, required this.gatewayProviderId, - required this.runtimeBudgetMinutes, required this.startedAtMs, required this.status, this.taskLoadClass = '', @@ -937,7 +936,6 @@ class OpenClawTaskAssociation { final String artifactScope; final String artifactDirectory; final String gatewayProviderId; - final int runtimeBudgetMinutes; final double startedAtMs; final String status; final String taskLoadClass; @@ -962,7 +960,6 @@ class OpenClawTaskAssociation { artifactScope: artifactScope, artifactDirectory: artifactDirectory, gatewayProviderId: gatewayProviderId, - runtimeBudgetMinutes: runtimeBudgetMinutes, startedAtMs: startedAtMs, status: status ?? this.status, taskLoadClass: taskLoadClass, @@ -981,7 +978,6 @@ class OpenClawTaskAssociation { 'artifactScope': artifactScope, 'artifactDirectory': artifactDirectory, 'gatewayProviderId': gatewayProviderId, - 'runtimeBudgetMinutes': runtimeBudgetMinutes, 'startedAtMs': startedAtMs, 'status': status, 'taskLoadClass': taskLoadClass, @@ -1000,7 +996,6 @@ class OpenClawTaskAssociation { 'artifactScope': artifactScope, 'artifactDirectory': artifactDirectory, 'gatewayProviderId': gatewayProviderId, - 'runtimeBudgetMinutes': runtimeBudgetMinutes, 'taskLoadClass': taskLoadClass, 'sessionKey': sessionKey, 'requiredArtifactExtensions': requiredArtifactExtensions, @@ -1049,7 +1044,6 @@ class OpenClawTaskAssociation { true ? json['resolvedGatewayProviderId'].toString().trim() : 'openclaw'), - runtimeBudgetMinutes: asInt(json['runtimeBudgetMinutes']), startedAtMs: asDouble(json['startedAtMs']), status: json['status']?.toString().trim().isNotEmpty == true ? json['status'].toString().trim() diff --git a/lib/widgets/assistant_artifact_sidebar.dart b/lib/widgets/assistant_artifact_sidebar.dart index d31429bc..039d99b2 100644 --- a/lib/widgets/assistant_artifact_sidebar.dart +++ b/lib/widgets/assistant_artifact_sidebar.dart @@ -399,13 +399,6 @@ class _AssistantArtifactSidebarState extends State { } String _filesEmptyMessage(AssistantArtifactSnapshot snapshot) { - if (widget.artifactSyncStatus.trim().toLowerCase() == - 'no-exported-artifacts') { - return appText( - '本轮没有检测到实际生成的文件。请重新执行,并要求 OpenClaw 在当前 workspace 中创建文件。', - 'No exported files were detected for this run. Run it again and ask OpenClaw to create files in the current workspace.', - ); - } final filesMessage = snapshot.filesMessage.trim(); if (filesMessage.isNotEmpty) { return filesMessage; diff --git a/lib/widgets/assistant_task_progress_bar.dart b/lib/widgets/assistant_task_progress_bar.dart index 063b0e59..40cd175b 100644 --- a/lib/widgets/assistant_task_progress_bar.dart +++ b/lib/widgets/assistant_task_progress_bar.dart @@ -16,19 +16,16 @@ class AssistantTaskProgressState { required this.phase, required this.label, this.value, - this.runtimeBudgetMinutes, }); const AssistantTaskProgressState.idle() : phase = AssistantTaskProgressPhase.idle, label = '', - value = null, - runtimeBudgetMinutes = null; + value = null; final AssistantTaskProgressPhase phase; final String label; final double? value; - final int? runtimeBudgetMinutes; bool get visible => phase != AssistantTaskProgressPhase.idle; bool get interrupted => phase == AssistantTaskProgressPhase.interrupted; @@ -171,20 +168,15 @@ AssistantTaskProgressState assistantTaskProgressState({ required String lifecycleStatus, required String lastResultCode, required String artifactSyncStatus, - int? runtimeBudgetMinutes, }) { final syncStatus = artifactSyncStatus.trim().toLowerCase(); final status = lifecycleStatus.trim().toLowerCase(); - final budget = runtimeBudgetMinutes == null || runtimeBudgetMinutes <= 0 - ? null - : runtimeBudgetMinutes; final result = lastResultCode.trim().toUpperCase(); if (status == 'queued' || syncStatus == 'queued' || result == 'QUEUED') { return AssistantTaskProgressState( phase: AssistantTaskProgressPhase.queued, label: appText('任务已排队,等待执行...', 'Task queued, waiting to run...'), value: 0.18, - runtimeBudgetMinutes: budget, ); } if (pending && syncStatus == 'syncing') { @@ -192,14 +184,12 @@ AssistantTaskProgressState assistantTaskProgressState({ phase: AssistantTaskProgressPhase.syncingArtifacts, label: appText('正在同步生成文件...', 'Syncing generated files...'), value: 0.82, - runtimeBudgetMinutes: budget, ); } if (pending) { return AssistantTaskProgressState( phase: AssistantTaskProgressPhase.running, - label: _budgetedProgressLabel(appText('任务运行中', 'Task running'), budget), - runtimeBudgetMinutes: budget, + label: appText('任务运行中...', 'Task running...'), ); } if (status == 'interrupted' || syncStatus == 'interrupted') { @@ -222,12 +212,7 @@ AssistantTaskProgressState assistantTaskProgressState({ return const AssistantTaskProgressState.idle(); } -String _budgetedProgressLabel(String base, int? minutes) { - if (minutes == null || minutes <= 0) { - return '$base...'; - } - return appText('$base,预计最长 $minutes 分钟...', '$base, up to $minutes min...'); -} + String _interruptedTaskProgressLabel(String result) { if (result == 'ACP_HTTP_HANDSHAKE_INTERRUPTED') { diff --git a/test/features/assistant/assistant_artifact_sidebar_test.dart b/test/features/assistant/assistant_artifact_sidebar_test.dart index 9de0587e..b9bf4a83 100644 --- a/test/features/assistant/assistant_artifact_sidebar_test.dart +++ b/test/features/assistant/assistant_artifact_sidebar_test.dart @@ -52,7 +52,7 @@ void main() { await tester.pumpWidget( _buildTestApp( artifactSyncAtMs: 1, - artifactSyncStatus: 'no-exported-artifacts', + artifactSyncStatus: 'failed', loadSnapshot: () async => const AssistantArtifactSnapshot( workspacePath: '/tmp/thread', workspaceKind: WorkspaceRefKind.localPath, diff --git a/test/features/assistant/assistant_task_progress_bar_test.dart b/test/features/assistant/assistant_task_progress_bar_test.dart index 64976d68..b6287172 100644 --- a/test/features/assistant/assistant_task_progress_bar_test.dart +++ b/test/features/assistant/assistant_task_progress_bar_test.dart @@ -15,7 +15,6 @@ void main() { lifecycleStatus: 'running', lastResultCode: 'running', artifactSyncStatus: '', - runtimeBudgetMinutes: 30, ), onStop: () {}, ), @@ -25,7 +24,7 @@ void main() { find.byKey(const Key('assistant-task-progress-bar')), findsOneWidget, ); - expect(find.text('任务运行中,预计最长 30 分钟...'), findsOneWidget); + expect(find.text('任务运行中...'), findsOneWidget); expect( find.byKey(const Key('assistant-task-progress-stop-button')), findsOneWidget, diff --git a/test/runtime/app_controller_thread_workspace_binding_test.dart b/test/runtime/app_controller_thread_workspace_binding_test.dart index 43a970b3..df385306 100644 --- a/test/runtime/app_controller_thread_workspace_binding_test.dart +++ b/test/runtime/app_controller_thread_workspace_binding_test.dart @@ -1350,7 +1350,7 @@ void main() { final thread = controller.requireTaskThreadForSessionInternal( 'unit-fixture-task-a', ); - expect(thread.lastArtifactSyncStatus, 'no-exported-artifacts'); + expect(thread.lastArtifactSyncStatus, 'no-artifacts'); expect(thread.lastArtifactSyncAtMs, greaterThan(0)); }, ); @@ -1432,7 +1432,7 @@ void main() { final thread = controller.requireTaskThreadForSessionInternal( 'unit-fixture-task-a', ); - expect(thread.lastArtifactSyncStatus, 'no-exported-artifacts'); + expect(thread.lastArtifactSyncStatus, 'no-artifacts'); expect(thread.lastTaskArtifactRelativePaths, isEmpty); }); diff --git a/test/runtime/assistant_execution_target_test.dart b/test/runtime/assistant_execution_target_test.dart index b90a24e0..f7d3b83f 100644 --- a/test/runtime/assistant_execution_target_test.dart +++ b/test/runtime/assistant_execution_target_test.dart @@ -1820,85 +1820,6 @@ void main() { test( 'sendChatMessage restarts before handling OpenClaw artifact guard results', - () async { - final localWorkspace = await Directory.systemTemp.createTemp( - 'xworkmate-acp-interrupt-guard-', - ); - addTearDown(() async { - if (await localWorkspace.exists()) { - await localWorkspace.delete(recursive: true); - } - }); - const guardMessage = - '未检测到 OpenClaw 本轮导出的实际文件。已阻止口头下载声明进入 artifacts 面板;请重新执行并要求 OpenClaw 在 workspace 中真实生成文件。'; - final fakeGoTaskService = _RecordingGoTaskServiceClient() - ..updatesBeforeNextOutcome.add( - const GoTaskServiceUpdate( - sessionId: 'unit-fixture-task-a', - threadId: 'unit-fixture-task-a', - turnId: 'turn-1', - type: 'delta', - text: 'guard partial output must not persist', - message: '', - pending: true, - error: false, - route: GoTaskServiceRoute.externalAcpSingle, - payload: {}, - ), - ) - ..outcomes.add( - const GatewayAcpException( - 'ACP HTTP connection closed before the response finished arriving', - code: 'ACP_HTTP_CONNECTION_CLOSED', - ), - ) - ..outcomes.add( - const GoTaskServiceResult( - success: true, - message: guardMessage, - turnId: 'turn-2', - raw: {'code': 'OPENCLAW_NO_EXPORTED_ARTIFACTS'}, - errorMessage: '', - resolvedModel: '', - route: GoTaskServiceRoute.externalAcpSingle, - ), - ); - final controller = _connectedController( - fakeGoTaskService, - homeDir: localWorkspace.path, - ); - addTearDown(controller.dispose); - - await controller.sessionsController.switchSession( - 'unit-fixture-task-a', - ); - - await controller.sendChatMessage('first turn'); - await controller.sendChatMessage('follow up'); - - expect(fakeGoTaskService.requests, hasLength(2)); - expect(fakeGoTaskService.requests.first.resumeSession, isFalse); - expect(fakeGoTaskService.requests.last.resumeSession, isTrue); - - final transcript = controller.chatMessages - .map((message) => message.text) - .join('\n'); - expect(transcript, isNot(contains('未检测到 OpenClaw 本轮导出的实际文件'))); - expect(transcript, isNot(contains('口头下载声明'))); - expect( - transcript, - isNot(contains('guard partial output must not persist')), - ); - - final thread = controller.taskThreadForSessionInternal( - 'unit-fixture-task-a', - ); - expect(thread?.lifecycleState.status, 'ready'); - expect(thread?.lastArtifactSyncStatus, 'no-exported-artifacts'); - expect(thread?.lastArtifactSyncAtMs, greaterThan(0)); - }, - ); - test( 'sendChatMessage starts a new session after OpenClaw terminal artifact failure', () async { @@ -1947,7 +1868,7 @@ void main() { failedThread?.lifecycleState.lastResultCode, 'OPENCLAW_NO_EXPORTED_ARTIFACTS', ); - expect(failedThread?.lastArtifactSyncStatus, 'no-exported-artifacts'); + expect(failedThread?.lastArtifactSyncStatus, 'failed'); await controller.sendChatMessage('retry final artifact'); @@ -1960,76 +1881,6 @@ void main() { }, ); - test( - 'sendChatMessage hides OpenClaw artifact guard text from failed results and streaming', - () async { - final localWorkspace = await Directory.systemTemp.createTemp( - 'xworkmate-acp-guard-failure-', - ); - addTearDown(() async { - if (await localWorkspace.exists()) { - await localWorkspace.delete(recursive: true); - } - }); - const guardMessage = - '未检测到 OpenClaw 本轮导出的实际文件。已阻止口头下载声明进入 artifacts 面板;请重新执行并要求 OpenClaw 在 workspace 中真实生成文件。'; - final fakeGoTaskService = _RecordingGoTaskServiceClient() - ..updatesBeforeNextOutcome.add( - const GoTaskServiceUpdate( - sessionId: 'unit-fixture-task-a', - threadId: 'unit-fixture-task-a', - turnId: 'turn-1', - type: 'delta', - text: guardMessage, - message: '', - pending: true, - error: false, - route: GoTaskServiceRoute.externalAcpSingle, - payload: {}, - ), - ) - ..outcomes.add( - const GoTaskServiceResult( - success: false, - message: '', - turnId: 'turn-1', - raw: { - 'status': 'artifact_missing', - 'code': 'OPENCLAW_ARTIFACT_MISSING', - 'artifactWarnings': [ - 'OpenClaw artifact export returned no files for a file-delivery request.', - ], - }, - errorMessage: guardMessage, - resolvedModel: '', - route: GoTaskServiceRoute.externalAcpSingle, - ), - ); - final controller = _connectedController( - fakeGoTaskService, - homeDir: localWorkspace.path, - ); - addTearDown(controller.dispose); - - await controller.sessionsController.switchSession( - 'unit-fixture-task-a', - ); - await controller.sendChatMessage('create files'); - - final transcript = controller.chatMessages - .map((message) => message.text) - .join('\n'); - expect(transcript, isNot(contains('未检测到 OpenClaw 本轮导出的实际文件'))); - expect(transcript, isNot(contains('口头下载声明'))); - final thread = controller.taskThreadForSessionInternal( - 'unit-fixture-task-a', - ); - expect(thread?.lifecycleState.lastResultCode, 'artifact_missing'); - expect(thread?.lastArtifactSyncStatus, 'no-exported-artifacts'); - expect(thread?.lastArtifactSyncAtMs, greaterThan(0)); - }, - ); - test( 'sendChatMessage restarts after ACP HTTP handshake interruption', () async { @@ -4179,7 +4030,7 @@ void main() { await _waitForThreadArtifactSyncStatusWithin( controller, 'openclaw-missing-screenshot', - 'no-exported-artifacts', + 'no-artifacts', const Duration(seconds: 10), ); @@ -4188,7 +4039,7 @@ void main() { ); expect(thread.lifecycleState.status, 'ready'); expect(thread.lifecycleState.lastResultCode, 'success'); - expect(thread.lastArtifactSyncStatus, 'no-exported-artifacts'); + expect(thread.lastArtifactSyncStatus, 'no-artifacts'); expect(thread.openClawTaskAssociation, isNull); expect( controller.assistantSessionHasPendingRun(