feat(scene): complete automation runtime #11

Merged
KremeCN merged 9 commits from feat/scene-automation-runtime into main 2026-08-28 06:39:31 +08:00
Owner

Summary

  • persist scenes and trigger state in Room with a database migration and one-time default seeding
  • execute enabled automatic scenes from real device readings with edge-triggered thresholds, conditions, hysteresis, and time occurrence deduplication
  • schedule foreground polling and WorkManager background evaluation, including reconciliation after scene changes
  • allow automatic commands to be run manually for testing while bypassing triggers and conditions
  • send real system notifications with runtime permission handling and accurate failure reporting
  • move Other Options & Appearance to the bottom of the command editor

Verification

  • ./gradlew.bat :android:app:assembleDebug --no-daemon
  • installed and exercised on mulator-5554
  • verified manual automatic-command execution and Activity records
  • verified notification permission flow, posted notification, and successful Activity records

Automated tests were not run per the requested iteration workflow.

## Summary - persist scenes and trigger state in Room with a database migration and one-time default seeding - execute enabled automatic scenes from real device readings with edge-triggered thresholds, conditions, hysteresis, and time occurrence deduplication - schedule foreground polling and WorkManager background evaluation, including reconciliation after scene changes - allow automatic commands to be run manually for testing while bypassing triggers and conditions - send real system notifications with runtime permission handling and accurate failure reporting - move Other Options & Appearance to the bottom of the command editor ## Verification - ./gradlew.bat :android:app:assembleDebug --no-daemon - installed and exercised on mulator-5554 - verified manual automatic-command execution and Activity records - verified notification permission flow, posted notification, and successful Activity records Automated tests were not run per the requested iteration workflow.
KremeCN added 1 commit 2026-08-28 01:40:09 +08:00
pr-agent-bot bot reviewed 2026-08-28 01:43:47 +08:00
pr-agent-bot bot left a comment

Inline comment

Inline comment
@@ -40,3 +40,4 @@
kapt(libs.hilt.compiler)
implementation(libs.androidx.work.runtime.ktx)
implementation(libs.androidx.hilt.work)
implementation(platform(libs.compose.bom))

处理器缺失

这里只加入了 AndroidX Hilt Work 运行库,但 @HiltWorker 还需要在声明 Worker 的模块中配置 AndroidX Hilt 编译器。否则不会生成并注册 Worker assisted factory,WorkManager 无法通过 HiltWorkerFactory 实例化 AutomationPollWorker,后台自动化任务会在运行时失败;应增加对应的 kapt 或 ksp 编译器依赖。

**处理器缺失** 这里只加入了 AndroidX Hilt Work 运行库,但 `@HiltWorker` 还需要在声明 Worker 的模块中配置 AndroidX Hilt 编译器。否则不会生成并注册 Worker assisted factory,WorkManager 无法通过 `HiltWorkerFactory` 实例化 `AutomationPollWorker`,后台自动化任务会在运行时失败;应增加对应的 `kapt` 或 `ksp` 编译器依赖。 <!-- pr-agent-dedup: df5ab63303b0 --> <!-- pr-agent-key-issue-location: f5a5ce5a7abc -->
pr-agent-bot bot reviewed 2026-08-28 01:43:47 +08:00
pr-agent-bot bot left a comment

Inline comment

Inline comment
@@ -69,6 +93,7 @@ class SceneTriggerEvaluator {
result: DomainResult<TemperatureHumidityReading>?,

跨日漏触发

时间触发器始终基于当前日期构造计划时间,因此午夜后执行时无法匹配前一天的计划。例如周一 23:50 的任务被 WorkManager 延迟到周二 00:05 后,计划时间会被计算为周二 23:50,并且星期也按周二判断,导致本应处于宽限窗口内的任务永久漏执行。

**跨日漏触发** 时间触发器始终基于当前日期构造计划时间,因此午夜后执行时无法匹配前一天的计划。例如周一 23:50 的任务被 WorkManager 延迟到周二 00:05 后,计划时间会被计算为周二 23:50,并且星期也按周二判断,导致本应处于宽限窗口内的任务永久漏执行。 <!-- pr-agent-dedup: 24d013e675d0 --> <!-- pr-agent-key-issue-location: 9f6d81f0caab -->
pr-agent-bot bot reviewed 2026-08-28 01:43:47 +08:00
pr-agent-bot bot left a comment

Inline comment

Inline comment
@@ -26,5 +26,5 @@
sessionCoordinator.observeActiveSession(),
) { active, session -> active to session }.collectLatest { (active, session) ->
previousProfileId?.takeIf { it != active?.id }?.let { scheduler.cancel(it) }
previousProfileId = active?.id
if (active != null && session?.profileId == active.id) {

后台轮询

分钟级轮询启动后只依赖应用级协程作用域,没有与 Activity 或进程前台生命周期绑定。用户将应用切到后台,甚至仅由 WorkManager 拉起应用进程后,该循环仍会继续读取设备并评估场景,与 WorkManager 后台任务重复运行并产生持续的网络和电量开销;应在进程进入后台时停止该循环。

**后台轮询** 分钟级轮询启动后只依赖应用级协程作用域,没有与 Activity 或进程前台生命周期绑定。用户将应用切到后台,甚至仅由 WorkManager 拉起应用进程后,该循环仍会继续读取设备并评估场景,与 WorkManager 后台任务重复运行并产生持续的网络和电量开销;应在进程进入后台时停止该循环。 <!-- pr-agent-dedup: aa2efc5bb6a5 --> <!-- pr-agent-key-issue-location: ef1c3147c48c -->
pr-agent-bot bot reviewed 2026-08-28 01:43:47 +08:00
pr-agent-bot bot left a comment

PR Reviewer Guide 🔍

Here are some key observations to aid the review process:

⏱️ Estimated effort to review: 5 🔵🔵🔵🔵🔵
🧪 No relevant tests
🔒 No security concerns identified
## PR Reviewer Guide 🔍 Here are some key observations to aid the review process: <table> <tr><td>⏱️&nbsp;<strong>Estimated effort to review</strong>: 5 🔵🔵🔵🔵🔵</td></tr> <tr><td>🧪&nbsp;<strong>No relevant tests</strong></td></tr> <tr><td>🔒&nbsp;<strong>No security concerns identified</strong></td></tr> </table>
KremeCN added 1 commit 2026-08-28 02:00:33 +08:00
pr-agent-bot bot reviewed 2026-08-28 02:03:47 +08:00
pr-agent-bot bot left a comment

Inline comment

Inline comment
@@ -70,3 +98,4 @@
operator: ComparisonOperator,
threshold: Double,
hysteresis: Double = 0.0,
value: (TemperatureHumidityReading) -> String?,

提前触发

当轮询发生在计划时间前不足一分钟时,负的 Duration 经 toMinutes() 向零截断为 0,仍会通过时间窗口判断。例如 09:59:30 会提前执行 10:00 的规则并占用该 occurrence,真正到点时不再执行。应先排除负时长或改用秒级比较。

**提前触发** 当轮询发生在计划时间前不足一分钟时,负的 `Duration` 经 `toMinutes()` 向零截断为 0,仍会通过时间窗口判断。例如 09:59:30 会提前执行 10:00 的规则并占用该 occurrence,真正到点时不再执行。应先排除负时长或改用秒级比较。 <!-- pr-agent-dedup: 3629966bd0b7 --> <!-- pr-agent-key-issue-location: d1ec98b89700 -->
pr-agent-bot bot reviewed 2026-08-28 02:03:47 +08:00
pr-agent-bot bot left a comment

Inline comment

Inline comment
@@ -91,5 +91,5 @@
}
}
}
if (!newlyMatched) continue
if (conditionEvaluator.evaluate(scene.conditions, snapshot, now) !is ConditionsEvaluation.Satisfied) {

边沿丢失

阈值刚越界但条件不满足时,这里会把刚写入的匹配状态重置为 false。如果阈值之后一直保持越界,只要条件在后续轮询中变为满足,场景就会在没有新阈值边沿的情况下执行,破坏边沿触发语义。条件失败后应保留已匹配状态,或显式建模待处理事件。

**边沿丢失** 阈值刚越界但条件不满足时,这里会把刚写入的匹配状态重置为 `false`。如果阈值之后一直保持越界,只要条件在后续轮询中变为满足,场景就会在没有新阈值边沿的情况下执行,破坏边沿触发语义。条件失败后应保留已匹配状态,或显式建模待处理事件。 <!-- pr-agent-dedup: a7138467d563 --> <!-- pr-agent-key-issue-location: 5ff8a986e04d -->
pr-agent-bot bot reviewed 2026-08-28 02:03:47 +08:00
pr-agent-bot bot left a comment

Inline comment

Inline comment
@@ -59,5 +59,5 @@
return runCatching {
if (requireExisting && dao.getScene(profileId.value, scene.id.value) == null) throw IllegalArgumentException("Scene not found")
if (!requireExisting && dao.getScene(profileId.value, scene.id.value) != null) throw IllegalArgumentException("Scene already exists")
dao.upsertScene(scene.toEntity())
dao.deleteTriggerStates(profileId.value, scene.id.value)

意外重触发

每次更新场景都会无条件删除全部触发器状态。用户仅修改名称、图标或动作时,如果某个阈值当前仍处于匹配状态,下一轮评估会再次识别为从未匹配到匹配并重复执行自动化。应只清理已删除或定义发生变化的触发器状态,并保留未修改触发器的状态。

**意外重触发** 每次更新场景都会无条件删除全部触发器状态。用户仅修改名称、图标或动作时,如果某个阈值当前仍处于匹配状态,下一轮评估会再次识别为从未匹配到匹配并重复执行自动化。应只清理已删除或定义发生变化的触发器状态,并保留未修改触发器的状态。 <!-- pr-agent-dedup: 084401ee6ca9 --> <!-- pr-agent-key-issue-location: f6d7241cf954 -->
pr-agent-bot bot reviewed 2026-08-28 02:03:47 +08:00
pr-agent-bot bot left a comment

PR Reviewer Guide 🔍

Here are some key observations to aid the review process:

⏱️ Estimated effort to review: 5 🔵🔵🔵🔵🔵
🧪 No relevant tests
🔒 No security concerns identified
## PR Reviewer Guide 🔍 Here are some key observations to aid the review process: <table> <tr><td>⏱️&nbsp;<strong>Estimated effort to review</strong>: 5 🔵🔵🔵🔵🔵</td></tr> <tr><td>🧪&nbsp;<strong>No relevant tests</strong></td></tr> <tr><td>🔒&nbsp;<strong>No security concerns identified</strong></td></tr> </table>
KremeCN added 1 commit 2026-08-28 02:17:59 +08:00
pr-agent-bot bot reviewed 2026-08-28 02:23:55 +08:00
pr-agent-bot bot left a comment

Inline comment

Inline comment
@@ -31,5 +31,5 @@
profileStore.observeActiveProfile(),
sessionCoordinator.observeActiveSession(),
isForeground,
) { active, session, foreground -> Triple(active, session, foreground) }
.collectLatest { (active, session, foreground) ->

执行中断

collectLatest 将前台状态监听和场景评估放在同一个可取消块中。应用在场景执行期间进入后台时,isForeground 的变化会取消正在运行的 engine.evaluate();此时触发状态或定时 occurrence 已经持久化,包含延迟或慢设备动作的场景可能只执行一部分,后续 worker 也不会重试该次触发。应只停止后续轮询,并允许当前评估完成。

**执行中断** `collectLatest` 将前台状态监听和场景评估放在同一个可取消块中。应用在场景执行期间进入后台时,`isForeground` 的变化会取消正在运行的 `engine.evaluate()`;此时触发状态或定时 occurrence 已经持久化,包含延迟或慢设备动作的场景可能只执行一部分,后续 worker 也不会重试该次触发。应只停止后续轮询,并允许当前评估完成。 <!-- pr-agent-dedup: bca7d5c309ee --> <!-- pr-agent-key-issue-location: 3417d4aebe9e -->
pr-agent-bot bot reviewed 2026-08-28 02:23:55 +08:00
pr-agent-bot bot left a comment

Inline comment

Inline comment
@@ -64,5 +64,5 @@
private fun evaluateOne(trigger: SceneTrigger, snapshot: AutomationSnapshot, now: LocalDateTime): TriggerEvaluation? = when (trigger) {
is SceneTrigger.Time -> {
val time = parseTime(trigger.timeOfDay) ?: return TriggerEvaluation.Unavailable(DomainError.InvalidRequest(detail = "Invalid trigger time"))
val scheduled = listOf(now.toLocalDate(), now.toLocalDate().minusDays(1))

定时漏触发

时间触发仅接受最多晚到 20 分钟,但 WorkManager 的一次性和周期任务在 Doze、后台限制或系统繁忙时可能被推迟更久。任务最终运行时该 occurrence 会被判定为不匹配,并直接安排下一天或下一周,导致本次触发永久丢失。应持久化或传递预定 occurrence,避免以固定宽限窗口判断任务是否仍有效。

**定时漏触发** 时间触发仅接受最多晚到 20 分钟,但 WorkManager 的一次性和周期任务在 Doze、后台限制或系统繁忙时可能被推迟更久。任务最终运行时该 occurrence 会被判定为不匹配,并直接安排下一天或下一周,导致本次触发永久丢失。应持久化或传递预定 occurrence,避免以固定宽限窗口判断任务是否仍有效。 <!-- pr-agent-dedup: e523193f135d --> <!-- pr-agent-key-issue-location: 83ed580ab3fd -->
pr-agent-bot bot reviewed 2026-08-28 02:23:56 +08:00
pr-agent-bot bot left a comment

Inline comment

Inline comment
@@ -36,7 +36,7 @@ class ProfileAutomationScheduler(
override suspend fun reconcile(profileId: ProviderProfileId): DomainResult<Unit> {
val active = profileStore.activeProfile()
if (active?.id != profileId) return DomainResult.Failure(DomainError.InvalidRequest(detail = "Provider profile is not active"))
return when (val validity = sessionGateway.validateSession(profileId)) {

会话错配

validateActiveSession() 只验证当前活动会话是否有效,没有确认其 profileId 与待调度的 profileId 一致。切换配置后旧会话仍有效的窗口内,新配置可能被错误调度;worker 也采用相同验证方式,可能在旧会话上下文中评估新配置。应恢复按配置验证,或显式核对活动会话归属。

**会话错配** `validateActiveSession()` 只验证当前活动会话是否有效,没有确认其 `profileId` 与待调度的 `profileId` 一致。切换配置后旧会话仍有效的窗口内,新配置可能被错误调度;worker 也采用相同验证方式,可能在旧会话上下文中评估新配置。应恢复按配置验证,或显式核对活动会话归属。 <!-- pr-agent-dedup: ebc3f0b1da28 --> <!-- pr-agent-key-issue-location: d5dbad8484e3 -->
pr-agent-bot bot reviewed 2026-08-28 02:23:56 +08:00
pr-agent-bot bot left a comment

PR Reviewer Guide 🔍

Here are some key observations to aid the review process:

⏱️ Estimated effort to review: 5 🔵🔵🔵🔵🔵
🧪 PR contains tests
🔒 Security concerns

会话隔离:调度器和 worker 未验证活动会话的 profileId 与待执行配置一致。配置切换期间可能在旧会话上下文中评估新配置;如果下游网关依赖当前会话,这可能导致自动化命令被发送到错误的账户或设备。

## PR Reviewer Guide 🔍 Here are some key observations to aid the review process: <table> <tr><td>⏱️&nbsp;<strong>Estimated effort to review</strong>: 5 🔵🔵🔵🔵🔵</td></tr> <tr><td>🧪&nbsp;<strong>PR contains tests</strong></td></tr> <tr><td>🔒&nbsp;<strong>Security concerns</strong><br><br> 会话隔离:调度器和 worker 未验证活动会话的 `profileId` 与待执行配置一致。配置切换期间可能在旧会话上下文中评估新配置;如果下游网关依赖当前会话,这可能导致自动化命令被发送到错误的账户或设备。</td></tr> </table>
KremeCN added 1 commit 2026-08-28 02:36:47 +08:00
pr-agent-bot bot reviewed 2026-08-28 02:43:40 +08:00
pr-agent-bot bot left a comment

Inline comment

Inline comment
@@ -84,5 +84,5 @@
is DomainResult.Failure -> return state
is DomainResult.Success -> state.value
}
}
val evaluation = if (

过期触发

scheduledOccurrence 只根据场景和触发器 ID 强制判定命中,没有校验计划时间与当前时间。WorkManager 因关机、Doze 或会话重试而延迟数小时甚至数天后,过期的定时规则仍会执行;应携带计划时间戳并应用宽限窗口,过期时仅重排下一次任务。

**过期触发** `scheduledOccurrence` 只根据场景和触发器 ID 强制判定命中,没有校验计划时间与当前时间。WorkManager 因关机、Doze 或会话重试而延迟数小时甚至数天后,过期的定时规则仍会执行;应携带计划时间戳并应用宽限窗口,过期时仅重排下一次任务。 <!-- pr-agent-dedup: 5194bf61fc8d --> <!-- pr-agent-key-issue-location: beb64bf775b3 -->
pr-agent-bot bot reviewed 2026-08-28 02:43:40 +08:00
pr-agent-bot bot left a comment

Inline comment

Inline comment
@@ -34,5 +34,5 @@
) { active, session -> active to session }
.collectLatest { (active, session) ->
previousProfileId?.takeIf { it != active?.id }?.let { scheduler.cancel(it) }
previousProfileId = active?.id
if (active != null && session?.profileId == active.id) {

会话校验

scheduler.reconcile 的结果被忽略,随后仅凭仍存在同一 profile 的本地 session 就启动无限评估循环。若会话校验返回无效或失败但 session 流尚未清空,前台仍会执行时间或通知类自动化,并持续尝试需要鉴权的动作;应仅在确认会话有效后进入评估循环。

**会话校验** `scheduler.reconcile` 的结果被忽略,随后仅凭仍存在同一 profile 的本地 session 就启动无限评估循环。若会话校验返回无效或失败但 session 流尚未清空,前台仍会执行时间或通知类自动化,并持续尝试需要鉴权的动作;应仅在确认会话有效后进入评估循环。 <!-- pr-agent-dedup: 2b73143ba705 --> <!-- pr-agent-key-issue-location: 19c0e00c1367 -->
pr-agent-bot bot reviewed 2026-08-28 02:43:40 +08:00
pr-agent-bot bot left a comment

Inline comment

Inline comment
@@ -27,5 +27,5 @@
val profileId = ProviderProfileId(rawProfileId)
if (profileStore.activeProfile()?.id != profileId) return Result.success()
sessionCoordinator.bootstrap()
return when (val validity = sessionCoordinator.validateSession(profileId)) {

无效重试

明确的 SessionValidity.INVALID 被当作可恢复错误返回 Result.retry(),会让周期任务和一次性定时任务无限退避重试。例如凭据过期且应用长期未打开时,Worker 会持续唤醒并验证会话;应取消该 profile 的自动化并返回成功,仅对真正的暂时性失败重试。

**无效重试** 明确的 `SessionValidity.INVALID` 被当作可恢复错误返回 `Result.retry()`,会让周期任务和一次性定时任务无限退避重试。例如凭据过期且应用长期未打开时,Worker 会持续唤醒并验证会话;应取消该 profile 的自动化并返回成功,仅对真正的暂时性失败重试。 <!-- pr-agent-dedup: 11bf0ce54e11 --> <!-- pr-agent-key-issue-location: 5f0026961b14 -->
pr-agent-bot bot reviewed 2026-08-28 02:43:41 +08:00
pr-agent-bot bot left a comment

PR Reviewer Guide 🔍

Here are some key observations to aid the review process:

⏱️ Estimated effort to review: 5 🔵🔵🔵🔵🔵
🧪 PR contains tests
🔒 No security concerns identified
## PR Reviewer Guide 🔍 Here are some key observations to aid the review process: <table> <tr><td>⏱️&nbsp;<strong>Estimated effort to review</strong>: 5 🔵🔵🔵🔵🔵</td></tr> <tr><td>🧪&nbsp;<strong>PR contains tests</strong></td></tr> <tr><td>🔒&nbsp;<strong>No security concerns identified</strong></td></tr> </table>
KremeCN added 1 commit 2026-08-28 02:53:14 +08:00
pr-agent-bot bot reviewed 2026-08-28 02:59:57 +08:00
pr-agent-bot bot left a comment

Inline comment

Inline comment
@@ -85,5 +85,5 @@
is DomainResult.Failure -> return state
is DomainResult.Success -> state.value
}
}
val evaluation = if (

定时遗漏

scheduledOccurrence 仅在计划时间后的 20 分钟内有效,但 WorkManager 在 Doze、省电模式或系统繁忙时可能延迟更久。此时本次 occurrence 会被判定为未匹配,后续协调又会直接安排下一次,导致当前定时自动化永久漏执行。应依赖 occurrence 去重接受迟到任务,或改用能满足该时效要求的调度机制。

**定时遗漏** `scheduledOccurrence` 仅在计划时间后的 20 分钟内有效,但 WorkManager 在 Doze、省电模式或系统繁忙时可能延迟更久。此时本次 occurrence 会被判定为未匹配,后续协调又会直接安排下一次,导致当前定时自动化永久漏执行。应依赖 occurrence 去重接受迟到任务,或改用能满足该时效要求的调度机制。 <!-- pr-agent-dedup: b30e71db5b2b --> <!-- pr-agent-key-issue-location: bf8dfa560fb0 -->
pr-agent-bot bot reviewed 2026-08-28 02:59:58 +08:00
pr-agent-bot bot left a comment

Inline comment

Inline comment
@@ -32,5 +32,5 @@
) {
return
}
val permissionState = getSharedPreferences(NOTIFICATION_PERMISSION_STATE, MODE_PRIVATE)

权限锁死

KEY_PERMISSION_REQUESTED 在启动授权弹窗前就被永久置为 true。如果用户首次拒绝或关闭弹窗,即使系统仍允许再次申请,应用后续也不会再请求权限,通知动作将持续失败。应根据授权结果和 rationale 状态允许重试,或提供明确的系统设置入口。

**权限锁死** `KEY_PERMISSION_REQUESTED` 在启动授权弹窗前就被永久置为 `true`。如果用户首次拒绝或关闭弹窗,即使系统仍允许再次申请,应用后续也不会再请求权限,通知动作将持续失败。应根据授权结果和 rationale 状态允许重试,或提供明确的系统设置入口。 <!-- pr-agent-dedup: 1374868344cf --> <!-- pr-agent-key-issue-location: 53ad2e88bced -->
pr-agent-bot bot reviewed 2026-08-28 02:59:58 +08:00
pr-agent-bot bot left a comment

Inline comment

Inline comment
@@ -39,4 +51,4 @@
)
}
postNotification()
DomainResult.Success(Unit)

通知覆盖

notify 的 ID 只由 profileId 生成,因此同一配置下的所有场景和多个通知动作都会更新同一条系统通知。短时间连续触发时,先前通知会被覆盖,用户只能看到最后一条;应为每次事件或动作生成唯一 ID。

**通知覆盖** `notify` 的 ID 只由 `profileId` 生成,因此同一配置下的所有场景和多个通知动作都会更新同一条系统通知。短时间连续触发时,先前通知会被覆盖,用户只能看到最后一条;应为每次事件或动作生成唯一 ID。 <!-- pr-agent-dedup: c844385a2f3e --> <!-- pr-agent-key-issue-location: 737a0caa82af -->
pr-agent-bot bot reviewed 2026-08-28 02:59:58 +08:00
pr-agent-bot bot left a comment

PR Reviewer Guide 🔍

Here are some key observations to aid the review process:

⏱️ Estimated effort to review: 5 🔵🔵🔵🔵🔵
🧪 No relevant tests
🔒 No security concerns identified
## PR Reviewer Guide 🔍 Here are some key observations to aid the review process: <table> <tr><td>⏱️&nbsp;<strong>Estimated effort to review</strong>: 5 🔵🔵🔵🔵🔵</td></tr> <tr><td>🧪&nbsp;<strong>No relevant tests</strong></td></tr> <tr><td>🔒&nbsp;<strong>No security concerns identified</strong></td></tr> </table>
KremeCN added 1 commit 2026-08-28 03:10:39 +08:00
pr-agent-bot bot reviewed 2026-08-28 03:17:31 +08:00
pr-agent-bot bot left a comment

Inline comment

Inline comment
@@ -120,5 +120,5 @@
if (!newlyMatched) continue
if (conditionEvaluator.evaluate(scene.conditions, snapshot, now) !is ConditionsEvaluation.Satisfied) {
continue
}
if (matchedTimeTriggers.isNotEmpty()) {

触发合并

当同一场景的多个时间触发器在一次评估中同时到期时,所有 occurrence 都会被成功占用,但场景只执行一次。例如后台任务在 10:15 才处理 10:00 和 10:10 的触发器时,两个去重键都会被记录,第二次计划执行将永久丢失。应按成功占用的 occurrence 分别执行,或仅占用本次实际执行对应的 occurrence。

**触发合并** 当同一场景的多个时间触发器在一次评估中同时到期时,所有 occurrence 都会被成功占用,但场景只执行一次。例如后台任务在 10:15 才处理 10:00 和 10:10 的触发器时,两个去重键都会被记录,第二次计划执行将永久丢失。应按成功占用的 occurrence 分别执行,或仅占用本次实际执行对应的 occurrence。 <!-- pr-agent-dedup: cc702268df73 --> <!-- pr-agent-key-issue-location: c943741e1e43 -->
pr-agent-bot bot reviewed 2026-08-28 03:17:31 +08:00
pr-agent-bot bot left a comment

Inline comment

Inline comment
@@ -38,5 +38,5 @@
private fun timeWorkName(profileId: ProviderProfileId) = "automation-time:${profileId.value}"
private fun scheduleNextTimeTrigger(profileId: ProviderProfileId, scenes: List<Scene>) {
val now = LocalDateTime.now()
val next = scenes.flatMap { scene ->

计划遗漏

整个配置档只保存最早的一个时间 occurrence。若 Doze 或电量限制使任务延迟超过二十分钟,期间其他已到期的时间触发器既没有独立的计划任务,也已超出普通评估的宽限期;重新协调时又只会查找未来时间,因此这些执行会被永久跳过。应为每个 occurrence 建立唯一任务,或持久化并依次处理全部待执行 occurrence。

**计划遗漏** 整个配置档只保存最早的一个时间 occurrence。若 Doze 或电量限制使任务延迟超过二十分钟,期间其他已到期的时间触发器既没有独立的计划任务,也已超出普通评估的宽限期;重新协调时又只会查找未来时间,因此这些执行会被永久跳过。应为每个 occurrence 建立唯一任务,或持久化并依次处理全部待执行 occurrence。 <!-- pr-agent-dedup: 44f30beb57cd --> <!-- pr-agent-key-issue-location: bdcee4cf7c5b -->
pr-agent-bot bot reviewed 2026-08-28 03:17:31 +08:00
pr-agent-bot bot left a comment

Inline comment

Inline comment
@@ -38,5 +38,5 @@
) {
return
}
val permissionState = getSharedPreferences(NOTIFICATION_PERMISSION_STATE, MODE_PRIVATE)

权限误判

系统权限弹窗启动前就将请求状态持久化,而后续判断会把“已请求且无需显示说明”当作不可再请求。若用户直接关闭弹窗,或进程在结果回调前终止,权限仍未决定且 rationale 仍为 false,但之后的启动将永久停止请求,通知只能由用户手动进入系统设置开启。应仅在获得明确结果后记录最终状态,或单独区分请求中、拒绝和永久拒绝。

**权限误判** 系统权限弹窗启动前就将请求状态持久化,而后续判断会把“已请求且无需显示说明”当作不可再请求。若用户直接关闭弹窗,或进程在结果回调前终止,权限仍未决定且 rationale 仍为 false,但之后的启动将永久停止请求,通知只能由用户手动进入系统设置开启。应仅在获得明确结果后记录最终状态,或单独区分请求中、拒绝和永久拒绝。 <!-- pr-agent-dedup: 19f8657c67ec --> <!-- pr-agent-key-issue-location: 7014529e85c4 -->
pr-agent-bot bot reviewed 2026-08-28 03:17:31 +08:00
pr-agent-bot bot left a comment

PR Reviewer Guide 🔍

Here are some key observations to aid the review process:

⏱️ Estimated effort to review: 5 🔵🔵🔵🔵🔵
🧪 No relevant tests
🔒 No security concerns identified
## PR Reviewer Guide 🔍 Here are some key observations to aid the review process: <table> <tr><td>⏱️&nbsp;<strong>Estimated effort to review</strong>: 5 🔵🔵🔵🔵🔵</td></tr> <tr><td>🧪&nbsp;<strong>No relevant tests</strong></td></tr> <tr><td>🔒&nbsp;<strong>No security concerns identified</strong></td></tr> </table>
KremeCN added 1 commit 2026-08-28 03:28:13 +08:00
pr-agent-bot bot reviewed 2026-08-28 03:37:30 +08:00
pr-agent-bot bot left a comment

Inline comment

Inline comment
@@ -117,5 +117,5 @@
}
}
}
if (!newlyMatched) continue
if (conditionEvaluator.evaluate(scene.conditions, snapshot, now) !is ConditionsEvaluation.Satisfied) {

触发延迟

时间触发的 occurrence 仅在条件满足后才被认领。如果 08:00 已完成评估但条件不满足,该 occurrence 在宽限期内仍保持未消费;条件于 08:10 变为满足时,同一个 08:00 规则会被延迟执行。非时间触发会先持久化边沿,因此两类触发器的条件语义不一致。应在条件明确不满足时消费或记录该 occurrence,并仅对条件数据不可用的情况保留重试。

**触发延迟** 时间触发的 occurrence 仅在条件满足后才被认领。如果 08:00 已完成评估但条件不满足,该 occurrence 在宽限期内仍保持未消费;条件于 08:10 变为满足时,同一个 08:00 规则会被延迟执行。非时间触发会先持久化边沿,因此两类触发器的条件语义不一致。应在条件明确不满足时消费或记录该 occurrence,并仅对条件数据不可用的情况保留重试。 <!-- pr-agent-dedup: f4cab2f239a0 --> <!-- pr-agent-key-issue-location: e0ecaad0e2a9 -->
pr-agent-bot bot reviewed 2026-08-28 03:37:30 +08:00
pr-agent-bot bot left a comment

Inline comment

Inline comment
@@ -55,5 +55,5 @@
private suspend fun save(profileId: ProviderProfileId, scene: Scene, requireExisting: Boolean): DomainResult<Scene> {
if (scene.profileId != profileId || scene.actions.any { it.profileId != profileId }) {
return DomainResult.Failure(DomainError.InvalidRequest(detail = "Scene profile mismatch"))
}
return runCatching {

更新竞态

场景写入和旧触发状态清理是多个独立 DAO 操作,前台轮询或 Worker 可在两者之间读取新场景和旧状态。编辑一个保留相同 ID 的阈值触发器时,引擎可能基于混合状态执行并写入新边沿,随后清理又将其删除,导致下次轮询重复执行或丢失边沿。应将读取、场景更新和触发状态清理放入同一个 Room 事务。

**更新竞态** 场景写入和旧触发状态清理是多个独立 DAO 操作,前台轮询或 Worker 可在两者之间读取新场景和旧状态。编辑一个保留相同 ID 的阈值触发器时,引擎可能基于混合状态执行并写入新边沿,随后清理又将其删除,导致下次轮询重复执行或丢失边沿。应将读取、场景更新和触发状态清理放入同一个 Room 事务。 <!-- pr-agent-dedup: a4b099355e82 --> <!-- pr-agent-key-issue-location: c376684e873a -->
pr-agent-bot bot reviewed 2026-08-28 03:37:30 +08:00
pr-agent-bot bot left a comment

Inline comment

Inline comment
@@ -39,5 +39,5 @@
previousProfileId = active?.id
if (active != null && session?.profileId == active.id) {
while (true) {
isForeground.first { it }
when (val validity = sessionCoordinator.validateSession(active.id)) {

重复调度

有效会话在前台时,每轮 60 秒轮询都会执行完整的调度协调。其下游会更新周期任务、取消全部时间触发任务并逐个重建,即使场景没有任何变化;场景或时间触发器较多时会持续产生 WorkManager 数据库写入和任务取消,并干扰临近执行时间的任务。调度协调应仅由配置、资料或会话变化触发,轮询循环只需执行引擎评估。

**重复调度** 有效会话在前台时,每轮 60 秒轮询都会执行完整的调度协调。其下游会更新周期任务、取消全部时间触发任务并逐个重建,即使场景没有任何变化;场景或时间触发器较多时会持续产生 WorkManager 数据库写入和任务取消,并干扰临近执行时间的任务。调度协调应仅由配置、资料或会话变化触发,轮询循环只需执行引擎评估。 <!-- pr-agent-dedup: 49f38f37f27d --> <!-- pr-agent-key-issue-location: 228b2c82db3e -->
pr-agent-bot bot reviewed 2026-08-28 03:37:30 +08:00
pr-agent-bot bot left a comment

PR Reviewer Guide 🔍

Here are some key observations to aid the review process:

⏱️ Estimated effort to review: 5 🔵🔵🔵🔵🔵
🧪 No relevant tests
🔒 No security concerns identified
## PR Reviewer Guide 🔍 Here are some key observations to aid the review process: <table> <tr><td>⏱️&nbsp;<strong>Estimated effort to review</strong>: 5 🔵🔵🔵🔵🔵</td></tr> <tr><td>🧪&nbsp;<strong>No relevant tests</strong></td></tr> <tr><td>🔒&nbsp;<strong>No security concerns identified</strong></td></tr> </table>
KremeCN added 1 commit 2026-08-28 03:45:08 +08:00
pr-agent-bot bot reviewed 2026-08-28 03:51:18 +08:00
pr-agent-bot bot left a comment

Inline comment

Inline comment
@@ -25,3 +37,4 @@
workManager.cancelUniqueWork(workName(profileId))
workManager.cancelAllWorkByTag(timeWorkTag(profileId))
return DomainResult.Success(Unit)
}

任务中断

replace 每次协调都会按标签取消全部定时任务,这也会停止已经处于运行状态的 worker。若用户在定时场景执行期间保存、启用或删除任意场景,当前执行可能在触发记录已被占用后遭到取消,导致动作只执行一部分且无法再次触发。应仅替换尚未运行的调度,或避免取消正在运行的工作。

**任务中断** `replace` 每次协调都会按标签取消全部定时任务,这也会停止已经处于运行状态的 worker。若用户在定时场景执行期间保存、启用或删除任意场景,当前执行可能在触发记录已被占用后遭到取消,导致动作只执行一部分且无法再次触发。应仅替换尚未运行的调度,或避免取消正在运行的工作。 <!-- pr-agent-dedup: ecf27d5946b4 --> <!-- pr-agent-key-issue-location: 8c2cba4abb4d -->
pr-agent-bot bot reviewed 2026-08-28 03:51:19 +08:00
pr-agent-bot bot left a comment

Inline comment

Inline comment
@@ -56,5 +56,5 @@
override suspend fun evaluate(
profileId: ProviderProfileId,
scheduledOccurrence: ScheduledAutomationOccurrence?,
): DomainResult<Int> = evaluationMutex.withLock {

执行阻塞

evaluationMutex 覆盖了整个评估过程,其中还会等待场景动作执行完成。包含 Delay 或慢速设备 I/O 的场景会长期占用这个全局锁,阻塞前台轮询和其他 WorkManager 评估,可能造成触发窗口错过。应在完成快照评估和原子占用后释放锁,再执行动作。

**执行阻塞** `evaluationMutex` 覆盖了整个评估过程,其中还会等待场景动作执行完成。包含 `Delay` 或慢速设备 I/O 的场景会长期占用这个全局锁,阻塞前台轮询和其他 WorkManager 评估,可能造成触发窗口错过。应在完成快照评估和原子占用后释放锁,再执行动作。 <!-- pr-agent-dedup: 75e5331a6c95 --> <!-- pr-agent-key-issue-location: 42b5e002dc41 -->
pr-agent-bot bot reviewed 2026-08-28 03:51:19 +08:00
pr-agent-bot bot left a comment

Inline comment

Inline comment
@@ -48,5 +48,5 @@
val now = LocalDateTime.now()
val scheduledAt = nextOccurrence(trigger, now) ?: return
val delay = Duration.between(now.atZone(ZoneId.systemDefault()).toInstant(), scheduledAt.atZone(ZoneId.systemDefault()).toInstant())
val scheduledAtInstant = scheduledAt.atZone(ZoneId.systemDefault()).toInstant()
val maximumExpiry = scheduledAtInstant.plus(MAXIMUM_OCCURRENCE_LATENESS)

过期执行

定时触发的有效期最长被设置为 24 小时,而引擎会在整个有效期内接受该触发。设备休眠或 WorkManager 延迟数小时后,“定时开关设备”等动作仍可能在接近下一次计划时间时执行,与前台评估采用的 20 分钟宽限期也不一致。应将有效期限制为明确且较短的迟到窗口。

**过期执行** 定时触发的有效期最长被设置为 24 小时,而引擎会在整个有效期内接受该触发。设备休眠或 WorkManager 延迟数小时后,“定时开关设备”等动作仍可能在接近下一次计划时间时执行,与前台评估采用的 20 分钟宽限期也不一致。应将有效期限制为明确且较短的迟到窗口。 <!-- pr-agent-dedup: a69636e81e58 --> <!-- pr-agent-key-issue-location: b2e10030dd23 -->
pr-agent-bot bot reviewed 2026-08-28 03:51:19 +08:00
pr-agent-bot bot left a comment

PR Reviewer Guide 🔍

Here are some key observations to aid the review process:

⏱️ Estimated effort to review: 5 🔵🔵🔵🔵🔵
🧪 No relevant tests
🔒 No security concerns identified
## PR Reviewer Guide 🔍 Here are some key observations to aid the review process: <table> <tr><td>⏱️&nbsp;<strong>Estimated effort to review</strong>: 5 🔵🔵🔵🔵🔵</td></tr> <tr><td>🧪&nbsp;<strong>No relevant tests</strong></td></tr> <tr><td>🔒&nbsp;<strong>No security concerns identified</strong></td></tr> </table>
KremeCN added 1 commit 2026-08-28 04:02:42 +08:00
pr-agent-bot bot reviewed 2026-08-28 04:10:03 +08:00
pr-agent-bot bot left a comment

Inline comment

Inline comment
@@ -47,5 +47,5 @@
val now = LocalDateTime.now()
val scheduledAt = nextOccurrence(trigger, now) ?: return
val delay = Duration.between(now.atZone(ZoneId.systemDefault()).toInstant(), scheduledAt.atZone(ZoneId.systemDefault()).toInstant())
val scheduledAtInstant = scheduledAt.atZone(ZoneId.systemDefault()).toInstant()
val maximumExpiry = scheduledAtInstant.plus(MAXIMUM_OCCURRENCE_LATENESS)

延迟执行

时间触发任务的有效期被放宽到最多 24 小时或下一次发生前。设备关机、处于 Doze 或 WorkManager 严重延迟时,早晨的设备控制场景可能在当晚甚至次日才执行。应使用与前台评估一致的短宽限期,并让过期任务只安排下一次触发而不执行动作。

**延迟执行** 时间触发任务的有效期被放宽到最多 24 小时或下一次发生前。设备关机、处于 Doze 或 WorkManager 严重延迟时,早晨的设备控制场景可能在当晚甚至次日才执行。应使用与前台评估一致的短宽限期,并让过期任务只安排下一次触发而不执行动作。 <!-- pr-agent-dedup: 9c4e97f865b9 --> <!-- pr-agent-key-issue-location: 33287255f21a -->
pr-agent-bot bot reviewed 2026-08-28 04:10:03 +08:00
pr-agent-bot bot left a comment

Inline comment

Inline comment
@@ -25,3 +36,4 @@
workManager.cancelUniqueWork(workName(profileId))
workManager.cancelAllWorkByTag(timeWorkTag(profileId))
return DomainResult.Success(Unit)
}

任务残留

replace 只添加当前时间触发器的任务;只要该配置仍有任一自动场景,就不会取消已删除、禁用或改时的旧任务。反复编辑会累积旧 worker,它们到点后仍会校验会话并评估整个配置,产生不必要的后台 I/O 和设备请求。替换计划时应清理或比对旧时间任务。

**任务残留** `replace` 只添加当前时间触发器的任务;只要该配置仍有任一自动场景,就不会取消已删除、禁用或改时的旧任务。反复编辑会累积旧 worker,它们到点后仍会校验会话并评估整个配置,产生不必要的后台 I/O 和设备请求。替换计划时应清理或比对旧时间任务。 <!-- pr-agent-dedup: 20f0c54dcf7a --> <!-- pr-agent-key-issue-location: eb9820b6b300 -->
pr-agent-bot bot reviewed 2026-08-28 04:10:04 +08:00
pr-agent-bot bot left a comment

Inline comment

Inline comment
@@ -399,2 +402,4 @@
current.copy(isSaving = false, isSaved = true)
}
is DomainResult.Failure -> current.copy(
isSaving = false,

失败吞掉

场景持久化后,reconcile 的失败结果被忽略,界面仍直接标记保存成功。若用户在没有现有周期任务时创建首个自动场景,而会话校验或调度存储发生瞬时失败,该场景将没有后台任务且不会自动重试,直至重启或再次修改。应记录失败并安排重试或暴露调度失败状态。

**失败吞掉** 场景持久化后,`reconcile` 的失败结果被忽略,界面仍直接标记保存成功。若用户在没有现有周期任务时创建首个自动场景,而会话校验或调度存储发生瞬时失败,该场景将没有后台任务且不会自动重试,直至重启或再次修改。应记录失败并安排重试或暴露调度失败状态。 <!-- pr-agent-dedup: 1a79d15dad7a --> <!-- pr-agent-key-issue-location: 256a8d28a01c -->
pr-agent-bot bot reviewed 2026-08-28 04:10:04 +08:00
pr-agent-bot bot left a comment

PR Reviewer Guide 🔍

Here are some key observations to aid the review process:

⏱️ Estimated effort to review: 5 🔵🔵🔵🔵🔵
🧪 No relevant tests
🔒 No security concerns identified
## PR Reviewer Guide 🔍 Here are some key observations to aid the review process: <table> <tr><td>⏱️&nbsp;<strong>Estimated effort to review</strong>: 5 🔵🔵🔵🔵🔵</td></tr> <tr><td>🧪&nbsp;<strong>No relevant tests</strong></td></tr> <tr><td>🔒&nbsp;<strong>No security concerns identified</strong></td></tr> </table>
Author
Owner

/review

/review
pr-agent-bot bot reviewed 2026-08-28 04:32:37 +08:00
pr-agent-bot bot left a comment

Inline comment

Inline comment
@@ -146,5 +146,5 @@
continue
}
ConditionsEvaluation.Satisfied -> Unit
}
var executionCount = if (newlyMatchedStateTriggers.isEmpty()) 0 else 1

重复执行

当状态触发器与时间触发器在同一轮同时命中,或多个时间触发器同时命中时,executionCount 会大于 1,导致同一场景的全部动作被重复执行。插座、红外等非幂等动作可能因此连续执行多次;同一轮评估中只应将场景加入一次,除非明确要求按触发器分别执行。

**重复执行** 当状态触发器与时间触发器在同一轮同时命中,或多个时间触发器同时命中时,`executionCount` 会大于 1,导致同一场景的全部动作被重复执行。插座、红外等非幂等动作可能因此连续执行多次;同一轮评估中只应将场景加入一次,除非明确要求按触发器分别执行。 <!-- pr-agent-dedup: c9d00b09d401 --> <!-- pr-agent-key-issue-location: e0ca46a9d596 -->
pr-agent-bot bot reviewed 2026-08-28 04:32:37 +08:00
pr-agent-bot bot left a comment

Inline comment

Inline comment
@@ -25,3 +36,4 @@
workManager.cancelUniqueWork(workName(profileId))
workManager.cancelAllWorkByTag(timeWorkTag(profileId))
return DomainResult.Success(Unit)
}

任务泄漏

replace 只为当前时间触发器添加任务,却不取消已删除、禁用或修改的触发器对应的旧一次性任务。只要仍有其他自动场景,旧任务就会一直保留到执行;反复编辑时间触发器会累积后台任务,并在触发时产生多余的会话验证、设备读取和场景评估。

**任务泄漏** `replace` 只为当前时间触发器添加任务,却不取消已删除、禁用或修改的触发器对应的旧一次性任务。只要仍有其他自动场景,旧任务就会一直保留到执行;反复编辑时间触发器会累积后台任务,并在触发时产生多余的会话验证、设备读取和场景评估。 <!-- pr-agent-dedup: 1812827f9fe7 --> <!-- pr-agent-key-issue-location: 8f72625368dd -->
pr-agent-bot bot reviewed 2026-08-28 04:32:38 +08:00
pr-agent-bot bot left a comment

Inline comment

Inline comment
@@ -37,5 +37,5 @@
.collectLatest { (active, session) ->
previousProfileId?.takeIf { it != active?.id }?.let { scheduler.cancel(it) }
previousProfileId = active?.id
if (active != null && session?.profileId == active.id) {
var scheduleReady = false

漏调度

初始协调成功后,scheduleReady 会永久保持为 true,前台循环不再重新协调。若之后创建或启停场景时的单次 reconcile 因会话或存储瞬时失败,失败结果又被调用方忽略,后台计划将持续过期;创建首个自动场景时甚至不会产生 WorkManager 任务,直到进程或会话重启。

**漏调度** 初始协调成功后,`scheduleReady` 会永久保持为 true,前台循环不再重新协调。若之后创建或启停场景时的单次 `reconcile` 因会话或存储瞬时失败,失败结果又被调用方忽略,后台计划将持续过期;创建首个自动场景时甚至不会产生 WorkManager 任务,直到进程或会话重启。 <!-- pr-agent-dedup: 02d0c47771c5 --> <!-- pr-agent-key-issue-location: d3d969c486b0 -->
pr-agent-bot bot reviewed 2026-08-28 04:32:38 +08:00
pr-agent-bot bot left a comment

PR Reviewer Guide 🔍

Here are some key observations to aid the review process:

⏱️ Estimated effort to review: 5 🔵🔵🔵🔵🔵
🧪 No relevant tests
🔒 No security concerns identified
## PR Reviewer Guide 🔍 Here are some key observations to aid the review process: <table> <tr><td>⏱️&nbsp;<strong>Estimated effort to review</strong>: 5 🔵🔵🔵🔵🔵</td></tr> <tr><td>🧪&nbsp;<strong>No relevant tests</strong></td></tr> <tr><td>🔒&nbsp;<strong>No security concerns identified</strong></td></tr> </table>
KremeCN merged commit 4d4dd2893e into main 2026-08-28 06:39:31 +08:00
KremeCN deleted branch feat/scene-automation-runtime 2026-08-28 06:39:31 +08:00
Sign in to join this conversation.
No Reviewers
No labels
2 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: Flagship/abox-mgr#11