diff --git a/android/app/src/main/java/cn/auto/agent/core/executor/TaskExecutor.kt b/android/app/src/main/java/cn/auto/agent/core/executor/TaskExecutor.kt index 7112b10..02a3ac2 100644 --- a/android/app/src/main/java/cn/auto/agent/core/executor/TaskExecutor.kt +++ b/android/app/src/main/java/cn/auto/agent/core/executor/TaskExecutor.kt @@ -3,6 +3,7 @@ package cn.auto.agent.core.executor import cn.auto.agent.core.model.ActionType import cn.auto.agent.core.model.AgentTask import cn.auto.agent.core.model.NodeSelector +import cn.auto.agent.core.model.ResultCode import cn.auto.agent.core.model.TaskExecutionResult data class UiNodeRef(val opaqueId: Any, val text: String?) @@ -25,20 +26,32 @@ class TaskExecutor( val output = linkedMapOf>() task.steps.forEach { step -> val deadline = now() + step.timeoutMs + var lastCode = ResultCode.TARGET_NOT_FOUND var lastFailure = "控件未出现" - while (now() <= deadline) { + var completed = false + while (!completed) { if (driver.currentPackage() != step.packageName) { + lastCode = ResultCode.PACKAGE_MISMATCH lastFailure = "当前应用与步骤不匹配" } else if (step.activityName != null && driver.currentActivity() != step.activityName) { + lastCode = ResultCode.ACTIVITY_MISMATCH lastFailure = "当前 Activity 与步骤不匹配" } else if (step.action == ActionType.BACK) { - if (driver.back()) break - lastFailure = "返回操作失败" + if (driver.back()) { + completed = true + } else { + lastCode = ResultCode.ACTION_FAILED + lastFailure = "返回操作失败" + } } else { val matches = driver.find(requireNotNull(step.selector)) when { - matches.size > 1 -> return failure(step.id, "TASK_AMBIGUOUS", "控件匹配到多个节点", output) - matches.isEmpty() -> lastFailure = "控件未出现" + matches.size > 1 -> + return failure(step.id, ResultCode.TARGET_AMBIGUOUS, "控件匹配到多个节点", output) + matches.isEmpty() -> { + lastCode = ResultCode.TARGET_NOT_FOUND + lastFailure = "控件未出现" + } else -> { val node = matches.single() val successful = when (step.action) { @@ -50,21 +63,26 @@ class TaskExecutor( } != null ActionType.BACK -> error("handled above") } - if (successful) break - lastFailure = "操作执行失败" + if (successful) { + completed = true + } else { + lastCode = ResultCode.ACTION_FAILED + lastFailure = "操作执行失败" + } } } } + if (completed) break if (now() >= deadline) { if (step.optional) break - return failure(step.id, "TASK_NOT_MATCHED", lastFailure, output) + return failure(step.id, lastCode, lastFailure, output) } pause(minOf(100, (deadline - now()).coerceAtLeast(1))) } } - return TaskExecutionResult(true, "OK", "任务执行完成", output.mapValues { it.value.toList() }) + return TaskExecutionResult(true, ResultCode.OK, "任务执行完成", output.mapValues { it.value.toList() }) } - private fun failure(id: String, code: String, detail: String, output: Map>) = + private fun failure(id: String, code: ResultCode, detail: String, output: Map>) = TaskExecutionResult(false, code, "步骤 $id 失败:$detail", output) } diff --git a/android/app/src/main/java/cn/auto/agent/core/model/TaskModels.kt b/android/app/src/main/java/cn/auto/agent/core/model/TaskModels.kt index f01aa7c..40550df 100644 --- a/android/app/src/main/java/cn/auto/agent/core/model/TaskModels.kt +++ b/android/app/src/main/java/cn/auto/agent/core/model/TaskModels.kt @@ -3,6 +3,19 @@ package cn.auto.agent.core.model enum class TaskStatus { RECEIVED, RUNNING, SUCCEEDED, FAILED } enum class ActionType { WAIT, CLICK, INPUT, BACK, EXTRACT_TEXT } +/** + * 任务结果码。成功时恒为 [OK],是否成功只看 [TaskExecutionResult.successful]。 + * 只列出执行器当前真正会产生的码;新增动作时同步扩充,不预留未实现的码。 + */ +enum class ResultCode { + OK, + PACKAGE_MISMATCH, + ACTIVITY_MISMATCH, + TARGET_NOT_FOUND, + TARGET_AMBIGUOUS, + ACTION_FAILED, +} + data class NodeSelector( val resourceId: String? = null, val text: String? = null, @@ -36,7 +49,7 @@ data class AgentTask( data class TaskExecutionResult( val successful: Boolean, - val code: String, + val code: ResultCode, val message: String, val extracted: Map> = emptyMap(), ) diff --git a/android/app/src/test/java/cn/auto/agent/TaskExecutorTest.kt b/android/app/src/test/java/cn/auto/agent/TaskExecutorTest.kt index b06ab5f..7d8f7be 100644 --- a/android/app/src/test/java/cn/auto/agent/TaskExecutorTest.kt +++ b/android/app/src/test/java/cn/auto/agent/TaskExecutorTest.kt @@ -4,6 +4,7 @@ import cn.auto.agent.core.executor.ActionDriver import cn.auto.agent.core.executor.TaskExecutor import cn.auto.agent.core.executor.UiNodeRef import cn.auto.agent.core.model.NodeSelector +import cn.auto.agent.core.model.ResultCode import cn.auto.agent.core.protocol.TaskParser import org.junit.Assert.assertEquals import org.junit.Assert.assertTrue @@ -37,6 +38,6 @@ class TaskExecutorTest { override fun back() = true } val result = TaskExecutor(driver).execute(TaskParser.parse(TaskParserTest.taskJson("phone-01"))) - assertEquals("TASK_AMBIGUOUS", result.code) + assertEquals(ResultCode.TARGET_AMBIGUOUS, result.code) } } diff --git a/android/app/src/test/java/cn/auto/agent/TaskExecutorTimeoutTest.kt b/android/app/src/test/java/cn/auto/agent/TaskExecutorTimeoutTest.kt new file mode 100644 index 0000000..7224093 --- /dev/null +++ b/android/app/src/test/java/cn/auto/agent/TaskExecutorTimeoutTest.kt @@ -0,0 +1,143 @@ +package cn.auto.agent + +import cn.auto.agent.core.executor.ActionDriver +import cn.auto.agent.core.executor.TaskExecutor +import cn.auto.agent.core.executor.UiNodeRef +import cn.auto.agent.core.model.NodeSelector +import cn.auto.agent.core.model.ResultCode +import cn.auto.agent.core.protocol.TaskParser +import org.junit.Assert.assertEquals +import org.junit.Assert.assertFalse +import org.junit.Assert.assertTrue +import org.junit.Test + +/** 超时语义:步骤在 timeoutMs 内没有成功时必须给出确定结果,不能被当成成功。 */ +class TaskExecutorTimeoutTest { + private var clock = 0L + + /** overshootMs 模拟 Thread.sleep 的实际超调:真实设备上唤醒时间总是略晚于请求时间。 */ + private fun executor(driver: ActionDriver, overshootMs: Long = 0) = + TaskExecutor(driver, now = { clock }, pause = { clock += it + overshootMs }) + + @Test + fun failsWhenTargetNeverAppears() { + var attempts = 0 + val result = executor(driver(find = { attempts++; emptyList() })) + .execute(TaskParser.parse(clickTask())) + assertFalse(result.successful) + assertEquals(ResultCode.TARGET_NOT_FOUND, result.code) + assertTrue("应在超时前重复查询", attempts > 1) + } + + @Test + fun failsWhenSleepOvershootsDeadline() { + val result = executor(driver(find = { emptyList() }), overshootMs = 5) + .execute(TaskParser.parse(clickTask())) + assertFalse("轮询唤醒晚于截止时间时不能被当作成功", result.successful) + assertEquals(ResultCode.TARGET_NOT_FOUND, result.code) + } + + @Test + fun failsWhenActionKeepsReturningFalse() { + val result = executor(driver(find = { listOf(UiNodeRef("node", "登录")) }, click = { false })) + .execute(TaskParser.parse(clickTask())) + assertFalse(result.successful) + assertEquals(ResultCode.ACTION_FAILED, result.code) + } + + @Test + fun failsWhenPackageNeverMatches() { + val result = executor(driver(currentPackage = "com.other.app")) + .execute(TaskParser.parse(clickTask())) + assertFalse(result.successful) + assertEquals(ResultCode.PACKAGE_MISMATCH, result.code) + } + + @Test + fun skipsOptionalStepOnTimeout() { + val result = executor(driver(find = { emptyList() })) + .execute(TaskParser.parse(clickTask(optional = true))) + assertTrue(result.successful) + assertEquals(ResultCode.OK, result.code) + } + + @Test + fun succeedsWhenTargetAppearsBeforeDeadline() { + var attempts = 0 + var clicked = 0 + val result = executor( + driver( + find = { if (attempts++ < 3) emptyList() else listOf(UiNodeRef("node", "登录")) }, + click = { clicked++; true }, + ), + ).execute(TaskParser.parse(clickTask())) + assertTrue(result.successful) + assertEquals(1, clicked) + } + + @Test + fun keepsExtractedOutputOnFailure() { + var attempts = 0 + val result = executor( + driver(find = { if (attempts++ == 0) listOf(UiNodeRef("node", "已完成")) else emptyList() }), + ).execute(TaskParser.parse(extractThenClickTask())) + assertFalse(result.successful) + assertEquals(listOf("已完成"), result.extracted["status"]) + } + + private fun driver( + currentPackage: String = "com.example.target", + find: () -> List = { emptyList() }, + click: () -> Boolean = { true }, + ) = object : ActionDriver { + override fun currentPackage() = currentPackage + override fun currentActivity(): String? = null + override fun find(selector: NodeSelector) = find() + override fun click(node: UiNodeRef) = click() + override fun input(node: UiNodeRef, value: String) = true + override fun back() = true + } + + private fun clickTask(optional: Boolean = false) = """ + { + "schemaVersion": 1, + "taskId": "task-timeout", + "revision": 1, + "assignedAgentName": "phone-01", + "steps": [{ + "id": "click-login", + "action": "CLICK", + "packageName": "com.example.target", + "selector": {"resourceId": "com.example.target:id/login"}, + "timeoutMs": 1000, + "optional": $optional + }] + } + """.trimIndent() + + private fun extractThenClickTask() = """ + { + "schemaVersion": 1, + "taskId": "task-extract", + "revision": 1, + "assignedAgentName": "phone-01", + "steps": [ + { + "id": "read-status", + "action": "EXTRACT_TEXT", + "packageName": "com.example.target", + "selector": {"resourceId": "com.example.target:id/status"}, + "outputField": "status", + "timeoutMs": 1000 + }, + { + "id": "click-login", + "action": "CLICK", + "packageName": "com.example.target", + "selector": {"resourceId": "com.example.target:id/login"}, + "timeoutMs": 1000 + } + ] + } + """.trimIndent() +} diff --git a/docs/agent/actions/README.md b/docs/agent/actions/README.md index f2efdc9..ffc489b 100644 --- a/docs/agent/actions/README.md +++ b/docs/agent/actions/README.md @@ -20,6 +20,29 @@ 当前代码中的动作只有 `WAIT`、`CLICK`、`INPUT`、`BACK` 和 `EXTRACT_TEXT`。 +## 结果码总表 + +结果码在这里统一定义,动作文档只引用不重复定义。事实来源是 [`ResultCode`](../../../android/app/src/main/java/cn/auto/agent/core/model/TaskModels.kt)。 + +**是否成功只看 `successful`;成功时 `code` 恒为 `OK`。** 不使用 `CLICKED`、`INPUT_SENT`、`OPEN_SENT`、`ALREADY_SELECTED` 这类“成功但不是 OK”的码——否则调用方无法用 `code` 判断成败。需要区分成功的细节时,用独立的 `detail` 字段,不占用 `code`。 + +| code | 状态 | 含义 | +|---|---|---| +| `OK` | **[已实现]** | 步骤或任务成功 | +| `PACKAGE_MISMATCH` | **[已实现]** | 超时内前台包始终与 `packageName` 不符 | +| `ACTIVITY_MISMATCH` | **[已实现]** | 超时内前台 Activity 始终与 `activityName` 不符 | +| `TARGET_NOT_FOUND` | **[已实现]** | 超时内 selector 没有匹配到节点 | +| `TARGET_AMBIGUOUS` | **[已实现]** | selector 匹配多个节点,立即失败 | +| `ACTION_FAILED` | **[已实现]** | 目标已找到,但动作在超时内始终未成功 | +| `TARGET_NOT_INTERACTABLE` | **[提案]** | 目标不可见、禁用或不可编辑 | +| `VERIFICATION_FAILED` | **[提案]** | 动作已分发,但 `after` 或回读验证未通过 | +| `TASK_INVALID` | **[提案]** | 任务解析失败 | +| `UNSUPPORTED_ON_DEVICE` | **[提案]** | 当前系统版本或服务配置不支持该动作 | + +超时不单独设码:超时体现为“超时内没能成功”的那个具体原因(`TARGET_NOT_FOUND`、`ACTION_FAILED` 等),不再为每个动作定义 `WAIT_TIMEOUT`、`SCROLL_TIMEOUT` 这类同义码。 + +新增动作时,先在 `ResultCode` 与本表中登记,再在动作文档引用。 + ## 统一规则 每个 UI 动作遵循同一条简单流程: diff --git a/docs/agent/actions/back.md b/docs/agent/actions/back.md index 1f5f17c..842f820 100644 --- a/docs/agent/actions/back.md +++ b/docs/agent/actions/back.md @@ -60,11 +60,9 @@ after 超时不会自动再次返回。连续返回需要编写多个 BACK 步 ## 4. 结果码 -- `BACK_SENT`:系统接受返回请求,未声明 after。 -- `OK`:返回后 after 满足。 -- `PRECONDITION_FAILED` -- `ACTION_FAILED` -- `VERIFICATION_FAILED` +见[结果码总表](README.md#结果码总表)。BACK 使用 `OK`、`ACTION_FAILED` 和 `VERIFICATION_FAILED`。 + +未声明 after 时,成功只表示系统接受了返回请求。before 不满足按 `ACTION_FAILED` 之外的上下文码处理(`PACKAGE_MISMATCH` 或 `TARGET_NOT_FOUND`)。 ## 5. 测试 diff --git a/docs/agent/actions/click.md b/docs/agent/actions/click.md index 434324b..d8fff01 100644 --- a/docs/agent/actions/click.md +++ b/docs/agent/actions/click.md @@ -75,13 +75,9 @@ ## 4. 结果码 -- `CLICKED`:点击已分发,未声明 after。 -- `OK`:点击已分发且 after 满足。 -- `TARGET_NOT_FOUND` -- `TARGET_AMBIGUOUS` -- `TARGET_NOT_INTERACTABLE` -- `ACTION_FAILED` -- `VERIFICATION_FAILED` +见[结果码总表](README.md#结果码总表)。CLICK 使用 `OK`、`TARGET_NOT_FOUND`、`TARGET_AMBIGUOUS`、`TARGET_NOT_INTERACTABLE`、`ACTION_FAILED` 和 `VERIFICATION_FAILED`。 + +未声明 after 时,成功只表示点击已分发,不表示页面已变化。 ## 5. 测试 diff --git a/docs/agent/actions/extract.md b/docs/agent/actions/extract.md index 4d27044..f6977be 100644 --- a/docs/agent/actions/extract.md +++ b/docs/agent/actions/extract.md @@ -79,13 +79,9 @@ EXTRACT 不点击、不滚动、不读取未声明字段,也不保存完整控 ## 4. 结果码 -- `OK` -- `TARGET_NOT_FOUND` -- `TARGET_AMBIGUOUS` -- `FIELD_NOT_AVAILABLE` -- `CONVERSION_FAILED` -- `OUTPUT_EMPTY` -- `EXTRACT_TIMEOUT` +见[结果码总表](README.md#结果码总表)。EXTRACT 使用 `OK`、`TARGET_NOT_FOUND`、`TARGET_AMBIGUOUS` 和 `ACTION_FAILED`(文本为空、字段缺失或类型转换失败)。 + +字段缺失与类型转换失败是否需要独立码,等 EXTRACT 实现时按实际排查需要决定,不提前登记。 ## 5. 兼容方式 diff --git a/docs/agent/actions/input.md b/docs/agent/actions/input.md index bdcca5e..53a7d0e 100644 --- a/docs/agent/actions/input.md +++ b/docs/agent/actions/input.md @@ -75,14 +75,9 @@ ## 4. 结果码 -- `INPUT_SENT`:输入已分发但未回读验证。 -- `OK`:输入验证通过。 -- `INPUT_VALUE_INVALID` -- `TARGET_NOT_FOUND` -- `TARGET_AMBIGUOUS` -- `TARGET_NOT_EDITABLE` -- `ACTION_FAILED` -- `VERIFICATION_FAILED` +见[结果码总表](README.md#结果码总表)。INPUT 使用 `OK`、`TARGET_NOT_FOUND`、`TARGET_AMBIGUOUS`、`TARGET_NOT_INTERACTABLE`(含不可编辑)、`ACTION_FAILED` 和 `VERIFICATION_FAILED`。 + +`verify` 为 `NONE` 时,成功只表示系统接受了输入请求,不表示文本已正确写入。 ## 5. 测试 diff --git a/docs/agent/actions/open.md b/docs/agent/actions/open.md index b3a9a5c..47c9320 100644 --- a/docs/agent/actions/open.md +++ b/docs/agent/actions/open.md @@ -63,14 +63,9 @@ OPEN 不写入任何具体 App 的页面文案、深链或恢复流程;这些 ## 4. 结果码 -- `OPEN_ALREADY_FOREGROUND` -- `OPEN_SENT`:目标包已进入前台,未声明 after。 -- `OK`:目标包和 after 都满足。 -- `TARGET_NOT_INSTALLED` -- `NO_HANDLER` -- `ACTION_FAILED` -- `PACKAGE_MISMATCH` -- `VERIFICATION_FAILED` +见[结果码总表](README.md#结果码总表)。OPEN 使用 `OK`、`PACKAGE_MISMATCH`、`ACTION_FAILED`(未安装、无处理程序或 Intent 分发失败)和 `VERIFICATION_FAILED`。 + +“已在前台”“已进入前台但未验证”属于成功的细节,放在 `detail` 而不是 `code`。未安装与无处理程序是否需要独立码,等实现时按排查需要决定。 ## 5. 测试 diff --git a/docs/agent/actions/screenshot.md b/docs/agent/actions/screenshot.md index 365c960..869ffe5 100644 --- a/docs/agent/actions/screenshot.md +++ b/docs/agent/actions/screenshot.md @@ -68,14 +68,9 @@ Android 无障碍截图从 API 30 开始可用;服务配置还需要声明对 ## 4. 结果码 -- `OK` -- `UNSUPPORTED_ON_DEVICE` -- `TARGET_NOT_FOUND` -- `TARGET_AMBIGUOUS` -- `CAPTURE_FAILED` -- `CROP_INVALID` -- `IMAGE_TOO_LARGE` -- `STORE_FAILED` +见[结果码总表](README.md#结果码总表)。SCREENSHOT 使用 `OK`、`UNSUPPORTED_ON_DEVICE`、`TARGET_NOT_FOUND`、`TARGET_AMBIGUOUS` 和 `ACTION_FAILED`(截图、裁剪、超限或保存失败)。 + +截图各阶段的失败是否需要拆成独立码,等实现时按排查需要决定。 ## 5. 测试 diff --git a/docs/agent/actions/scroll.md b/docs/agent/actions/scroll.md index 7d4cf2e..ea85d70 100644 --- a/docs/agent/actions/scroll.md +++ b/docs/agent/actions/scroll.md @@ -64,14 +64,9 @@ SCROLL 是统一滚动动作。当前代码尚未实现。 ## 4. 结果码 -- `OK` -- `SCROLL_SENT`:方向滚动成功。 -- `TARGET_NOT_FOUND` -- `CONTAINER_NOT_FOUND` -- `CONTAINER_AMBIGUOUS` -- `EDGE_REACHED` -- `ACTION_FAILED` -- `SCROLL_TIMEOUT` +见[结果码总表](README.md#结果码总表)。SCROLL 使用 `OK`、`TARGET_NOT_FOUND`(含容器未找到)、`TARGET_AMBIGUOUS`(容器不唯一)和 `ACTION_FAILED`。 + +“到达边缘仍未找到目标”属于 `TARGET_NOT_FOUND` 的一种原因,写在 `message` 里即可,不单独设码。 ## 5. 测试 diff --git a/docs/agent/actions/select-option.md b/docs/agent/actions/select-option.md index d6f2341..9a1cdae 100644 --- a/docs/agent/actions/select-option.md +++ b/docs/agent/actions/select-option.md @@ -49,14 +49,9 @@ SELECT_OPTION 是“查找选项、必要时滚动、点击并验证选中”的 ## 3. 结果码 -- `ALREADY_SELECTED` -- `OK` -- `OPTION_NOT_FOUND` -- `OPTION_AMBIGUOUS` -- `CONTAINER_NOT_FOUND` -- `ACTION_FAILED` -- `VERIFICATION_FAILED` -- `SELECT_TIMEOUT` +见[结果码总表](README.md#结果码总表)。SELECT_OPTION 使用 `OK`、`TARGET_NOT_FOUND`、`TARGET_AMBIGUOUS`、`ACTION_FAILED` 和 `VERIFICATION_FAILED`。 + +“已经处于选中状态”属于成功的细节,放在 `detail` 而不是 `code`。 ## 4. 测试 diff --git a/docs/agent/actions/wait.md b/docs/agent/actions/wait.md index e8cc166..9d04734 100644 --- a/docs/agent/actions/wait.md +++ b/docs/agent/actions/wait.md @@ -22,8 +22,8 @@ - 约每 100 毫秒查询一次。 - selector 匹配一个节点时成功。 - 没有匹配时等待到超时。 -- 多个匹配时立即返回 `TASK_AMBIGUOUS`。 -- 超时返回 `TASK_NOT_MATCHED`;optional=true 时继续下一步。 +- 多个匹配时立即返回 `TARGET_AMBIGUOUS`。 +- 超时返回 `TARGET_NOT_FOUND`;optional=true 时跳过并继续下一步。 ## 2. Condition 扩展 **[提案]** @@ -68,12 +68,9 @@ ## 4. 结果码 -- `OK`:条件满足。 -- `WAIT_TIMEOUT`:超时未满足。 -- `TARGET_AMBIGUOUS`:要求 ONE 但匹配多个。 -- `CONDITION_ERROR`:条件无法正常求值。 +见[结果码总表](README.md#结果码总表)。WAIT 使用 `OK`、`TARGET_NOT_FOUND`(超时未满足)、`TARGET_AMBIGUOUS`(要求 ONE 但匹配多个)以及上下文码 `PACKAGE_MISMATCH`、`ACTIVITY_MISMATCH`。 -v1 对外仍可映射为现有 `OK`、`TASK_AMBIGUOUS` 和 `TASK_NOT_MATCHED`。 +Condition 无法求值时需要一个独立码,等 Condition 实现时再登记。 ## 5. 测试 diff --git a/docs/agent/execution.md b/docs/agent/execution.md index 4651035..77eaaa4 100644 --- a/docs/agent/execution.md +++ b/docs/agent/execution.md @@ -10,8 +10,10 @@ 2. 等待 `packageName` 和可选 `activityName` 匹配。 3. BACK 直接调用全局返回;其他动作查询 selector。 4. 没有目标时继续等待,多个目标立即失败。 -5. 执行动作;成功后进入下一步。 -6. 到达超时时,optional 步骤跳过,其他步骤停止任务。 +5. 执行动作;成功后立即结束本步并进入下一步。 +6. 每次尝试结束后判定截止时间:到达超时时 optional 步骤跳过,其他步骤停止任务。 + +超时判定在每轮尝试之后进行,因此轮询唤醒晚于截止时间不会让失败的步骤被记为成功。多目标(`TARGET_AMBIGUOUS`)立即停止任务,不受 `optional` 影响。 当前结果: @@ -26,13 +28,9 @@ } ~~~ -当前结果码只有: +结果码见[结果码总表](actions/README.md#结果码总表)。当前执行器会产生 `OK`、`PACKAGE_MISMATCH`、`ACTIVITY_MISMATCH`、`TARGET_NOT_FOUND`、`TARGET_AMBIGUOUS` 和 `ACTION_FAILED`。 -| code | 含义 | -|---|---| -| `OK` | 任务执行完成 | -| `TASK_AMBIGUOUS` | selector 匹配多个节点 | -| `TASK_NOT_MATCHED` | 包名、Activity、目标或动作在超时内未成功 | +是否成功只看 `successful`,成功时 `code` 恒为 `OK`;`message` 只用于排障,不参与流程判断。 ## 2. 简单步骤结果 **[提案]** @@ -60,16 +58,7 @@ 暂不引入 phase、effect、transition 和独立结果协议版本。以后确实需要断点恢复或复杂流程审计时再扩展。 -建议逐步细分以下 code: - -- `TASK_INVALID` -- `PACKAGE_MISMATCH` -- `ACTIVITY_MISMATCH` -- `TARGET_NOT_FOUND` -- `TARGET_AMBIGUOUS` -- `ACTION_FAILED` -- `VERIFICATION_FAILED` -- `UNSUPPORTED_ON_DEVICE` +结果码的取值与新增规则统一见[结果码总表](actions/README.md#结果码总表)。 ## 3. 动作执行规则 diff --git a/docs/agent/protocol.md b/docs/agent/protocol.md index 1d789f3..18ebe79 100644 --- a/docs/agent/protocol.md +++ b/docs/agent/protocol.md @@ -66,9 +66,10 @@ - 步骤线性执行。 - 包名、Activity 或目标控件不匹配时,在 `timeoutMs` 内继续等待。 -- 默认要求 selector 只匹配一个节点;多个节点立即返回 `TASK_AMBIGUOUS`。 -- 普通失败当前统一返回 `TASK_NOT_MATCHED`。 -- 非 optional 步骤失败后停止任务;optional 步骤超时后继续。 +- 默认要求 selector 只匹配一个节点;多个节点立即返回 `TARGET_AMBIGUOUS`。 +- 失败返回具体原因码:`PACKAGE_MISMATCH`、`ACTIVITY_MISMATCH`、`TARGET_NOT_FOUND`、`TARGET_AMBIGUOUS` 或 `ACTION_FAILED`。 +- 步骤在 `timeoutMs` 内没有成功时给出确定结果:非 optional 步骤停止任务并返回对应的原因码,optional 步骤跳过并继续。 +- `optional` 只跳过“超时类失败”(上下文不匹配、目标未出现、动作未成功);`TARGET_AMBIGUOUS` 立即停止任务,不受 `optional` 影响。 - 当前只搜索 `rootInActiveWindow`。 已知差距: @@ -86,6 +87,7 @@ - 默认唯一匹配,不自动选择第一个。 - 所有等待、滚动和重试都有总超时或最大次数。 - 动作结果使用结构化 code,不依赖 message 文本判断流程。 +- 是否成功只看 `successful`,成功时 `code` 恒为 `OK`。成功的细节(点击已分发但未验证、App 已在前台等)放在独立的 `detail` 字段,不占用 `code`。 - 页面文字、资源 ID 和成功条件保留在任务或目标 App 适配层。 ## 5. 后续扩展 diff --git a/docs/agent/roadmap.md b/docs/agent/roadmap.md index a8cafc9..7af00f1 100644 --- a/docs/agent/roadmap.md +++ b/docs/agent/roadmap.md @@ -16,10 +16,11 @@ ### 1. 修正现有 v1 -- 未知或不适用字段明确报错,不再静默忽略。 +- ~~将失败原因从单一 `TASK_NOT_MATCHED` 细分~~ 已完成,见[结果码总表](actions/README.md#结果码总表)。 +- 未知或不适用字段明确报错,不再静默忽略。**但这一项与“设备上报能力”是一对,必须一起做或都先不做**:严格校验一旦上线,下发端就必须知道设备支持哪些字段,否则新服务端发给旧设备的任务会整体解析失败,且事先无法预判。设备数量可控、能统一升级时,两件都可以推迟。 - 将“等待目标”和“执行动作”分开;`CLICK`、`INPUT`、`BACK` 不因驱动返回 `false` 而重复分发。 -- 将 `TASK_NOT_MATCHED` 逐步细分为包名不符、Activity 不符、目标未找到和动作失败。 -- 明确 `optional` 只跳过哪些失败。 +- ~~步骤超时必须给出确定结果,不能因轮询唤醒晚于截止时间被记为成功。~~ 已修正,回归测试见 `TaskExecutorTimeoutTest`。 +- ~~明确 `optional` 只跳过哪些失败。~~ 已在[任务协议](protocol.md#3-当前执行语义)写明:只跳过超时类失败。 - 查询控件时补充可见、启用和有效边界判断,并正确释放节点。 - 让任务保存返回“新增、重复或失败”,同步统计不再混淆。 @@ -58,6 +59,11 @@ - 真机测试:只覆盖本次涉及的 Android 版本、目标 App 页面和输入法。 - 文档测试:相对链接有效,JSON 示例能解析。 +## 已知会推迟的设计 + +- **设备能力上报**(`schemaVersion`、支持的动作与选择器字段):只在“多台设备版本不一致 + 服务端自动生成任务”时才有价值。与上面的未知字段严格校验绑定。 +- **意外弹窗处理**:权限弹窗、更新提示和插屏是真实失败的主因,但当前 `BRANCH`/`GOTO` 只许向前、`onFailure` 不提供重试,缺少全局处理位置。等第一个真实目标 App 跑起来、拿到实际弹窗形态后再设计(预期形态:任务级 `interrupts`,一组“条件 → 一个消除动作”,带单条与全任务触发次数上限,且不重置步骤 `timeoutMs`)。在那之前若用大量 `optional` 步骤穷举兜底,这些任务将来需要重写;成规模出现时就是该做这件事的信号。 + ## 等外部信息明确后再设计 - 服务端任务接口与结果提交格式。