Skip to content

fix: preserve failed action details#1320

Open
zitonwei wants to merge 3 commits into
MaaXYZ:mainfrom
zitonwei:fix-action-failed-detail
Open

fix: preserve failed action details#1320
zitonwei wants to merge 3 commits into
MaaXYZ:mainfrom
zitonwei:fix-action-failed-detail

Conversation

@zitonwei
Copy link
Copy Markdown

@zitonwei zitonwei commented May 9, 2026

Fixes #1258

Summary

This PR fixes missing action_details in Node.Action.Failed callbacks when an action exits through an early failure path.

Previously, some failed actions returned an empty ActionResult, which left action_id as MaaInvalidId. As a result, the failed action detail written to runtime cache and emitted through callbacks could lose useful metadata.

Now Actuator::run() fills a failed ActionResult with the current action id, node name, action type, hit box, and success=false before storing it.

Validation

  • cmake --build build-make --target MaaFramework -j 8
  • cmake --build build-make --target PipelineTesting -j 8
  • ./build-make/bin/PipelineTesting ./test/TestingDataSet
  • git diff --check

Summary by Sourcery

确保失败的操作始终记录并发出详细的失败元数据,而不是返回空结果。

错误修复:

  • 当某个操作返回空结果时,在 Actuator::run() 中填充一个默认的失败 ActionResult,以便在运行时缓存和回调中保留失败操作的详细信息。

测试:

  • 扩展 RunWithoutFile pipeline 测试,发布一个失败的操作,捕获 Node.Action.Failed 上下文消息,并断言 action_idnameactionsuccess 字段被正确填充。
Original summary in English

Summary by Sourcery

Ensure failed actions always record and emit detailed failure metadata instead of returning an empty result.

Bug Fixes:

  • Populate a default failure ActionResult in Actuator::run() when an action returns an empty result so failed action details are preserved in the runtime cache and callbacks.

Tests:

  • Extend the RunWithoutFile pipeline test to post a failing action, capture the Node.Action.Failed context message, and assert that action_id, name, action, and success fields are correctly populated.

Copy link
Copy Markdown
Contributor

@sourcery-ai sourcery-ai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - 我发现了两个问题,并给出了一些高层次的反馈:

  • Actuator::run 中,当 action_idMaaInvalidId 时,后备分支会完全替换整个 ActionResult;如果调用方可能在失败结果上填充其他字段,建议只填补缺失的字段(例如 action_idnameactionboxsuccess),而不是丢弃已有的详细信息。
  • 新增的 RunWithoutFile 测试只断言了 action_details 中少数字段的存在;如果下游使用者也依赖命中框(hit box),建议同时校验期望的 box 字段存在且不是默认值,以防在保留几何信息方面出现回归。
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
-`Actuator::run` 中,当 `action_id``MaaInvalidId` 时,后备分支会完全替换整个 `ActionResult`;如果调用方可能在失败结果上填充其他字段,建议只填补缺失的字段(例如 `action_id``name``action``box``success`),而不是丢弃已有的详细信息。
- 新增的 `RunWithoutFile` 测试只断言了 `action_details` 中少数字段的存在;如果下游使用者也依赖命中框(hit box),建议同时校验期望的 `box` 字段存在且不是默认值,以防在保留几何信息方面出现回归。

## Individual Comments

### Comment 1
<location path="source/MaaFramework/Task/Component/Actuator.cpp" line_range="139" />
<code_context>
+        result = ActionResult {
+            .action_id = action_id_,
+            .name = pipeline_data.name,
+            .action = action_iter == kTypeNameMap.end() ? std::string() : action_iter->second,
+            .box = reco_hit,
+            .success = false,
</code_context>
<issue_to_address>
**suggestion:** 使用空字符串来表示未知的 action 类型可能会降低可调试性。

当在 `kTypeNameMap` 中找不到该类型时,`action` 会变成空字符串,这会让日志和诊断信息不够直观。建议使用更有意义的后备值(例如 `"<unknown>"``std::to_string(static_cast<int>(pipeline_data.action_type))`),这样下游使用者仍然可以识别出触发该动作的原始类型。

建议实现:

```cpp
            .action = action_iter == kTypeNameMap.end()
                          ? std::string("<unknown:") +
                                std::to_string(static_cast<int>(pipeline_data.action_type)) + ">"
                          : action_iter->second,

```

1. 确保在当前翻译单元中已包含 `<string>` 头文件(如果尚未包含),因为现在依赖 `std::string` 和 `std::to_string`。
2. 如果代码库中存在集中式的日志/格式化辅助函数(例如自定义的 `Format` 或类 `fmt` 的工具),为保持与现有约定一致,可能希望使用这些工具而不是手动拼接字符串。
</issue_to_address>

### Comment 2
<location path="test/pipeline/module/RunWithoutFile.cpp" line_range="94-100" />
<code_context>
+
+        MaaTaskerRemoveContextSink(tasker_handle, sink_id);
+
+        if (failed_id == MaaInvalidId || !capture.seen || capture.action_id == MaaInvalidId || capture.name.empty()
+            || capture.action != "Click" || capture.success) {
+            std::cout << "Failed to preserve action detail on failed action" << std::endl;
+            return false;
</code_context>
<issue_to_address>
**suggestion (testing):** 加强断言,以验证捕获的 `action_id` 与传入的 `failed_id` 一致。

当前检查只验证了 `capture.action_id` 被设置,而没有验证它是否与 `failed_id` 相匹配。为更好地覆盖这个回归场景,请增加断言 `capture.action_id == failed_id`(并在失败条件与日志信息中体现这一点),这样当捕获到了错误的动作详情时测试也会失败,而不仅仅是在信息缺失时才失败。

```suggestion
        MaaTaskerRemoveContextSink(tasker_handle, sink_id);

        if (failed_id == MaaInvalidId || !capture.seen || capture.action_id == MaaInvalidId || capture.name.empty()
            || capture.action != "Click" || capture.success || capture.action_id != failed_id) {
            std::cout << "Failed to preserve or correctly associate action detail on failed action" << std::endl;
            return false;
        }
```
</issue_to_address>

Sourcery 对开源项目免费——如果你觉得这些评论有帮助,欢迎分享 ✨
帮我变得更有用!请对每条评论点击 👍 或 👎,我会根据你的反馈改进后续的评审。
Original comment in English

Hey - I've found 2 issues, and left some high level feedback:

  • In Actuator::run, the fallback branch replaces the entire ActionResult when action_id is MaaInvalidId; if callers may populate other fields on a failed result, consider only filling missing fields (e.g., action_id, name, action, box, success) instead of discarding any existing detail.
  • The new RunWithoutFile test only asserts presence of a few fields from action_details; if consumers rely on the hit box as well, consider also validating that the expected box is present and non-default to catch regressions in preserved geometry.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- In `Actuator::run`, the fallback branch replaces the entire `ActionResult` when `action_id` is `MaaInvalidId`; if callers may populate other fields on a failed result, consider only filling missing fields (e.g., `action_id`, `name`, `action`, `box`, `success`) instead of discarding any existing detail.
- The new `RunWithoutFile` test only asserts presence of a few fields from `action_details`; if consumers rely on the hit box as well, consider also validating that the expected `box` is present and non-default to catch regressions in preserved geometry.

## Individual Comments

### Comment 1
<location path="source/MaaFramework/Task/Component/Actuator.cpp" line_range="139" />
<code_context>
+        result = ActionResult {
+            .action_id = action_id_,
+            .name = pipeline_data.name,
+            .action = action_iter == kTypeNameMap.end() ? std::string() : action_iter->second,
+            .box = reco_hit,
+            .success = false,
</code_context>
<issue_to_address>
**suggestion:** Using an empty string for unknown action types may reduce debuggability.

When the type isn’t found in `kTypeNameMap`, `action` becomes an empty string, which makes logs and diagnostics less informative. Prefer a meaningful fallback (e.g. `"<unknown>"` or `std::to_string(static_cast<int>(pipeline_data.action_type))`) so downstream consumers can still identify the originating action type.

Suggested implementation:

```cpp
            .action = action_iter == kTypeNameMap.end()
                          ? std::string("<unknown:") +
                                std::to_string(static_cast<int>(pipeline_data.action_type)) + ">"
                          : action_iter->second,

```

1. Ensure `<string>` is included in this translation unit if it is not already, as we now rely on `std::string` and `std::to_string`.
2. If your codebase has a centralized logging/formatting helper (e.g., a custom `Format` or `fmt`-like utility), you may want to use that instead of manual string concatenation, to be consistent with existing conventions.
</issue_to_address>

### Comment 2
<location path="test/pipeline/module/RunWithoutFile.cpp" line_range="94-100" />
<code_context>
+
+        MaaTaskerRemoveContextSink(tasker_handle, sink_id);
+
+        if (failed_id == MaaInvalidId || !capture.seen || capture.action_id == MaaInvalidId || capture.name.empty()
+            || capture.action != "Click" || capture.success) {
+            std::cout << "Failed to preserve action detail on failed action" << std::endl;
+            return false;
</code_context>
<issue_to_address>
**suggestion (testing):** Strengthen the assertion to verify the captured `action_id` matches the posted `failed_id`.

The current check only verifies that `capture.action_id` is set, not that it matches the `failed_id`. To better cover the regression, please assert `capture.action_id == failed_id` (and reflect this in the failure condition / log message) so the test fails if the wrong action detail is captured, not just when it’s missing.

```suggestion
        MaaTaskerRemoveContextSink(tasker_handle, sink_id);

        if (failed_id == MaaInvalidId || !capture.seen || capture.action_id == MaaInvalidId || capture.name.empty()
            || capture.action != "Click" || capture.success || capture.action_id != failed_id) {
            std::cout << "Failed to preserve or correctly associate action detail on failed action" << std::endl;
            return false;
        }
```
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment thread source/MaaFramework/Task/Component/Actuator.cpp Outdated
Comment on lines +94 to +100
MaaTaskerRemoveContextSink(tasker_handle, sink_id);

if (failed_id == MaaInvalidId || !capture.seen || capture.action_id == MaaInvalidId || capture.name.empty()
|| capture.action != "Click" || capture.success) {
std::cout << "Failed to preserve action detail on failed action" << std::endl;
return false;
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion (testing): 加强断言,以验证捕获的 action_id 与传入的 failed_id 一致。

当前检查只验证了 capture.action_id 被设置,而没有验证它是否与 failed_id 相匹配。为更好地覆盖这个回归场景,请增加断言 capture.action_id == failed_id(并在失败条件与日志信息中体现这一点),这样当捕获到了错误的动作详情时测试也会失败,而不仅仅是在信息缺失时才失败。

Suggested change
MaaTaskerRemoveContextSink(tasker_handle, sink_id);
if (failed_id == MaaInvalidId || !capture.seen || capture.action_id == MaaInvalidId || capture.name.empty()
|| capture.action != "Click" || capture.success) {
std::cout << "Failed to preserve action detail on failed action" << std::endl;
return false;
}
MaaTaskerRemoveContextSink(tasker_handle, sink_id);
if (failed_id == MaaInvalidId || !capture.seen || capture.action_id == MaaInvalidId || capture.name.empty()
|| capture.action != "Click" || capture.success || capture.action_id != failed_id) {
std::cout << "Failed to preserve or correctly associate action detail on failed action" << std::endl;
return false;
}
Original comment in English

suggestion (testing): Strengthen the assertion to verify the captured action_id matches the posted failed_id.

The current check only verifies that capture.action_id is set, not that it matches the failed_id. To better cover the regression, please assert capture.action_id == failed_id (and reflect this in the failure condition / log message) so the test fails if the wrong action detail is captured, not just when it’s missing.

Suggested change
MaaTaskerRemoveContextSink(tasker_handle, sink_id);
if (failed_id == MaaInvalidId || !capture.seen || capture.action_id == MaaInvalidId || capture.name.empty()
|| capture.action != "Click" || capture.success) {
std::cout << "Failed to preserve action detail on failed action" << std::endl;
return false;
}
MaaTaskerRemoveContextSink(tasker_handle, sink_id);
if (failed_id == MaaInvalidId || !capture.seen || capture.action_id == MaaInvalidId || capture.name.empty()
|| capture.action != "Click" || capture.success || capture.action_id != failed_id) {
std::cout << "Failed to preserve or correctly associate action detail on failed action" << std::endl;
return false;
}

@zitonwei
Copy link
Copy Markdown
Author

zitonwei commented May 9, 2026

I checked the failing CI jobs. They seem to fail in the Node binding preparation step before reaching this C++ change:

pnpm requires Node.js >= 22.13, while the workflow is using Node.js 20.20.2, which causes ERR_UNKNOWN_BUILTIN_MODULE: node:sqlite.

This PR only changes Actuator.cpp and RunWithoutFile.cpp, and local validation passed:

  • cmake --build build-make --target PipelineTesting -j 8
  • ./build-make/bin/PipelineTesting ./test/TestingDataSet
  • git diff --check

Please let me know if you would like me to adjust anything in this PR.

Copy link
Copy Markdown
Member

@MistEO MistEO left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 partial-fill 的思路对,测试也挺到位。CI 挂的是 node 版本问题,#1322 合了就恢复

@zitonwei
Copy link
Copy Markdown
Author

您好,感谢之前的 review 和 approval。

我看到 #1322 现在已经关闭了,这个 PR 目前还处于 open 状态,并且还有一些 CI job 没有通过。我这边看日志感觉剩余失败项仍然不像是这个 C++ 改动引起的,不过想确认一下:这个 PR 目前还需要我这边继续调整什么吗?

谢谢!

@MistEO
Copy link
Copy Markdown
Member

MistEO commented May 25, 2026

上面是机器人回复的

啥情况会进来一个 InvalidId?

@zitonwei
Copy link
Copy Markdown
Author

抱歉,上面 CI 那段我理解偏了。

这里的 InvalidId 指的是 Actuator::run() 里具体 action 函数返回了默认构造的 ActionResult {},不是 MaaTaskerPostAction 返回的 task id invalid。

一个可复现的场景是 Click 的 target 解析失败:例如我测试里用的 MaaTaskerPostAction(tasker, "Click", R"({"target":[0,0,0,0]})", box, "{}")。它会走到 Actuator::click() 里的:

auto target_rect = helper_.get_target_rect(param.target, box);
if (target_rect.empty()) {
    LogError << "failed to get target rect" << VAR(name);
    return { };
}

这个 {} 里的 action_id 就是 MaaInvalidId。之后 TaskBase::run_action() 还是会把这个 result 放进 Node.Action.Failedaction_details,所以失败回调里就拿不到有效的 action_id/name/action/box,同时 RuntimeCache::set_action_detail() 也会因为 uid invalid 跳过。

这个 PR 的处理就是在这种 action 内部已经确认失败、但还没生成完整 ActionResult 的路径上,用当前 Actuator 的 id 和 pipeline 信息补齐失败详情。除了 Click target 为空,类似 controller 为空、部分参数校验失败等提前 return {} 的 action 路径也会有同样问题。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Node.Action.Failed 在部分 Action 失败路径中丢失 action_details 关键信息

2 participants