diff --git a/doc/API.md b/doc/API.md
index 193bfd4..8e57a71 100644
--- a/doc/API.md
+++ b/doc/API.md
@@ -321,7 +321,7 @@
| `POST /api/stop_all` | — | 停止全部 |
| `POST /api/device/clear_error` | `{"serial":"…"}` | 运行中/重试等待中拒绝清理 |
| `POST /api/device/clear_all_errors` | — | 跳过 running/connecting |
-| `POST /api/device/screen_all` | `{"mode":"on"\|"off", "serials":[…]}` | 不传 serials 则对全部在线设备 |
+| `POST /api/device/screen_all` | `{"mode":"on"\|"off", "serials":[…]}` | 不传 serials 则对全部在线设备。**熄屏是单向的**(`KEYCODE_SLEEP`),不会把已息屏的设备唤醒;亮屏用 `KEYCODE_WAKEUP` + `dismiss-keyguard` |
| `POST /api/scan_foreground` | — | 已扫描中时返回 `{"ok":false}`(**HTTP 200**) |
| `POST /api/device/locate` | `{"serial":"…","show":true}` | `show=true` 时在设备上打开 `/locate` |
| `POST /api/device/locate/stop` | `{"serial":"…"}` | 结束定位 |
diff --git a/doc/TASK_DEV.md b/doc/TASK_DEV.md
index b7fc697..106d8ea 100644
--- a/doc/TASK_DEV.md
+++ b/doc/TASK_DEV.md
@@ -118,7 +118,7 @@ from .generic import task # 触发 @register_task(当前唯一任务类型)
| 2 | `stop_app` | 结束 App | `package` | — | — | `app_stop`(`am force-stop`,下次打开是冷启动) |
| 3 | `screen_on` | 亮屏 | — | — | — | `d.unlock()`(息屏时唤醒并滑动解锁) |
| 4 | `screen_off` | 息屏 | — | — | — | `d.screen_off()` |
-| 5 | `keep_screen` | 保持亮屏 | `mode`("on") | — | — | `mode="off"` → 恢复自动息屏;否则 `svc power stayon true`(充电时常亮,适合长任务) |
+| 5 | `keep_screen` | 保持亮屏 | `mode`("on") | — | — | `mode="off"` → 恢复自动息屏;否则**把系统息屏超时顶到最大**(`screen_off_timeout=2147483647`)+ `svc power stayon true` + 唤醒一次。⚠️ 只发 `svc power stayon true` 对**没插充电器**的设备(走 WiFi 的那些)**完全无效**——它管的是"充电时屏幕常亮",2026-09-14 就是因此被反馈"保持亮屏没用" |
| 6 | `key_event` | 按键 | `key`("back") | — | — | `d.press(key)`;**值直传,无白名单校验** |
| 7 | `swipe` | 滑动 | `direction`("up") | `duration_min`(0.25)、`duration_max`(0.50) | — | 时长取随机值;up = 中轴从 0.8h → 0.2h(down 反向,left/right 同理);**非法 direction 静默不滑** |
| 8 | `swipe_until` | 滑动直到元素 | `selector_type`、`selector_value` | `direction`("up")、`max_swipes`(8)、`click_when_found`(True) | — | 每轮滑动后 sleep 0.5s;命中即(可选)点击并返回 True;**只有 `direction="down"` 才是向下滑**,其余按上滑 |
diff --git a/static/admin/editor.js b/static/admin/editor.js
index 2b6b057..257df75 100644
--- a/static/admin/editor.js
+++ b/static/admin/editor.js
@@ -291,7 +291,7 @@ var _stepEditor={
h+='
息屏。任务结束后用,避免长时间亮屏导致烧屏/发热
';
}else if(step.type==='keep_screen'){
h+='';
h+='长任务(如养号看视频几小时)放"保持亮屏"防屏幕超时熄灭;任务结束前放"恢复自动息屏"
';
}else if(step.type==='swipe'){
diff --git a/tasks/generic/task.py b/tasks/generic/task.py
index c859417..b751509 100644
--- a/tasks/generic/task.py
+++ b/tasks/generic/task.py
@@ -45,6 +45,13 @@ _log = get_logger("task.generic")
_LEGACY_IDX_XPATH = re.compile(r'^(//\*\[@[^\]]+\])\[(\d+)\](.*)$')
+# ---- 「保持亮屏」用的系统设置 ----
+# 系统息屏超时(毫秒):保持亮屏时顶到最大,恢复时写回原值
+_SCREEN_TIMEOUT_MAX = "2147483647"
+_SCREEN_TIMEOUT_DEFAULT = "600000" # 兜底恢复值:10 分钟(原值记不住时用)
+_SCREEN_TIMEOUT_BACKUP = {} # serial -> 原始 screen_off_timeout
+
+
def _norm_legacy_xpath(sel_val):
"""把历史 `//*[@attr=…][k]` 纠正为 `(//*[@attr=…])[k]`(只改整体前缀,保留后续子路径)。
@@ -228,17 +235,35 @@ class GenericStepsWorker(BaseWorker):
def _exec_keep_screen(self, d, params, depth=0):
"""保持亮屏 / 恢复自动息屏。
- 长任务(养号看视频数小时)时屏幕会按系统超时自动熄灭导致任务无法执行,
- 用 svc power stayon 让充电时屏幕常亮;任务结束前用 mode=off 恢复自动息屏。
+ ⚠️ 只用 `svc power stayon true` **不管用**:它管的是「**充电时**屏幕常亮」
+ (= stay_on_while_plugged_in)。设备走 WiFi 跑任务、没插充电器时它完全不起作用
+ ——以前"保持亮屏"看着像没生效就是这个原因(2026-09-14 用户反馈)。
+
+ 真正管用的是把系统**息屏超时**顶到最大(`screen_off_timeout`):不充电也不会
+ 自动黑屏。进入时记下原值,`mode=off` 写回;进程重启丢了记录就写回常规的 10 分钟。
"""
mode = params.get("mode", "on")
try:
if mode == "off":
d.shell("svc power stayon false")
- _log.info(f"[{self.serial}] 恢复自动息屏")
+ orig = _SCREEN_TIMEOUT_BACKUP.pop(self.serial, _SCREEN_TIMEOUT_DEFAULT)
+ d.shell(f"settings put system screen_off_timeout {orig}")
+ _log.info(f"[{self.serial}] 恢复自动息屏(超时 {orig}ms)")
else:
- d.shell("svc power stayon true")
- _log.info(f"[{self.serial}] 保持亮屏(充电时屏幕常亮)")
+ cur = ""
+ try:
+ cur = (d.shell("settings get system screen_off_timeout") or "").strip()
+ except Exception:
+ pass
+ if self.serial not in _SCREEN_TIMEOUT_BACKUP:
+ # 没设过时 `settings get` 会返回 "null"(走系统默认)——别把 null 写回去
+ _SCREEN_TIMEOUT_BACKUP[self.serial] = \
+ cur if cur.isdigit() else _SCREEN_TIMEOUT_DEFAULT
+ d.shell("svc power stayon true") # 充电场景(顺带)
+ d.shell(f"settings put system screen_off_timeout {_SCREEN_TIMEOUT_MAX}")
+ d.shell("input keyevent 224") # 立刻唤醒(KEYCODE_WAKEUP,单向)
+ _log.info(f"[{self.serial}] 保持亮屏(息屏超时→最大,原值 "
+ f"{_SCREEN_TIMEOUT_BACKUP.get(self.serial)}ms)")
except Exception as e:
_log.warning(f"[{self.serial}] keep_screen({mode}) 异常: {e}")
diff --git a/web/monitor.py b/web/monitor.py
index 144d548..7bf07d6 100644
--- a/web/monitor.py
+++ b/web/monitor.py
@@ -239,8 +239,11 @@ def api_device_screen_all():
if ":" in serial:
adb_connect(serial)
if mode == "off":
- subprocess.run([ADB_PATH, "-s", serial, "shell", "input", "keyevent", "26"],
- capture_output=True, timeout=15) # KEYCODE_POWER 息屏
+ # KEYCODE_SLEEP(223) 是**单向**熄屏;不能用 KEYCODE_POWER(26)——
+ # 那是电源键**开关**,对已经息屏的设备会把屏幕点亮(2026-09-14 用户反馈:
+ # "一键息屏"把睡着的设备唤醒了,看着像按钮反了)
+ subprocess.run([ADB_PATH, "-s", serial, "shell", "input", "keyevent", "223"],
+ capture_output=True, timeout=15)
else:
subprocess.run([ADB_PATH, "-s", serial, "shell", "input", "keyevent", "224"],
capture_output=True, timeout=15) # WAKEUP