桌面一闪而过的黑框 + 打字被打断:诊断与根治
云端副本(可直接阅读): https://jebbnhdcmgptasjiuvjc.supabase.co/storage/v1/object/public/dsh/04-flash-window/flash-window-fix.md 工具包下载: https://jebbnhdcmgptasjiuvjc.supabase.co/storage/v1/object/public/dsh/04-flash-window/flash-tools.zip 正式分类页(
04-桌面闪窗排障)已登记进 cloud-map,等 Netlify 额度恢复(2026-12-01)自动补部署。记录 2026-11-03 与 2026-11-07 两次同一症状的实例、定位手法、修复规范与验证标准。 适用:Windows 10/11,任务计划程序里以「当前登录用户」身份运行的周期任务。
1. 症状
- 使用中屏幕突然闪出一个程序界面(一个方框/黑框),不到半秒消失。
- 键盘输入被打断:正在打的字跑到别的窗口或丢失,光标跳走。
- 周期性复发:分钟级间隔(本例 5 分钟),或者在某些动作后出现。
- 任务管理器里看不到它 —— 因为进程活得很短(几十~几百毫秒)。
2. 机制(为什么会闪、为什么会抢焦点)
- 任务计划程序(Task Scheduler 服务,svchost)以
InteractiveToken身份启动任务时, 子进程运行在当前登录用户的可交互桌面(winsta0\default)。 - 如果子进程是控制台子系统程序(
python.exe、powershell.exe、pwsh.exe、cmd.exe、node.exe、cscript.exe…),Windows 会给它分配一个 ConHost 控制台窗口 (窗口类ConsoleWindowClass,典型尺寸 993×839 / 993×519)。 - 这个控制台窗口创建时是可见的,并且会被激活到前台 → 焦点从你正在打字的窗口被抢走。 任务跑完,进程退出,窗口消失,焦点还回去 —— 看起来就是「闪了一下」。
- 关键:这些都不是脚本的问题,是启动方式的问题。所以:
-WindowStyle Hidden(powershell 参数)没用:它只能隐藏脚本宿主自己的窗口引用, ConHost 窗口仍然先创建、先显示、先抢焦点。- 任务设置里的
Hidden = True也没用:那只是「不显示任务栏按钮」,不影响控制台窗口。 - 唯一的正解是让创建进程的那一方用
SW_HIDE启动:由 GUI 子系统的wscript.exe完成启动,它本身没有控制台,再用WScript.Shell.Run(cmd, 0, False)以隐藏、不激活方式拉起子进程。
3. 两次实例时间线
实例 A:2026-11-03(7 个任务)
| 时间 | 任务 | 抢占焦点 |
|---|---|---|
| 13:58:16 | DSH-Quota-Turns(每 2 分钟) | 1.07 s |
| 14:00:16 | DSH-Quota-Turns | 1.03 s |
| 14:00:40 | 额度记账本-账本同步(每 15 分钟) | 8.06 s |
修复:把 7 个交互式任务的动作统一改成
wscript.exe //nologo hidden-launch.vbs "<原程序>" "<原参数>"。
验证:两个自然周期 + 手动触发全部返回码 0、零窗口事件、脚本输出仍正常写入。
实例 B:2026-11-07(回归,单点)
- 09:57:40 新建了计划任务
Cloud Sync(云端自动同步,每 5 分钟),动作是...\python\python.exe "D:\cloud-sync\cloud_sync.py" --once—— 直接启动控制台程序, 没走hidden-launch.vbs。 - 结果:每 5 分钟闪一次 python 控制台窗口,抢焦点 ~150 ms,打字被打断。 因为 build 里 Netlify 返回 403(额度超限)需要不断重试,窗口出现得更勤。
探针抓到的原始证据(flash-probe.log,窗口事件钩子 + 心跳):
10:14:44.600 FG vis=True 993x519 PID=13548 python parent=2536(svchost) ConsoleWindowClass ...python.exe
10:14:44.633 EV32770 vis=True 993x519 PID=13548 python parent=2536(svchost) ConsoleWindowClass ...python.exe
10:14:44.747 FG vis=True 1296x828 PID=4136 DeepSeek Harness parent=9744(explorer) Chrome_WidgetWin_1
读法:44.600 抢走前台 → 44.747 焦点回到原窗口,147 ms 的中断;父进程是 svchost
(= 任务计划服务),窗口类是 ConsoleWindowClass——三要素齐全,无需再猜。
修复后同一手法验证:
10:15:27 (schtasks /run "Cloud Sync")
探针日志:零 FG / 零 ConsoleWindowClass 事件
sync.log:10:15:27 两行新日志 → 脚本确实执行了
LastTaskResult:0
修复后自然周期(两个,探针共 143 次心跳持续覆盖):
10:19:19 探针零窗口事件;sync.log 10:19:41 有新行;返回码 0
10:24:24 探针零窗口事件;sync.log 10:24:40 有新行;返回码 0
4. 定位流程(5 分钟出结论)
# ① 列出所有「以当前登录用户身份运行」的计划任务及其动作
$out = foreach ($t in Get-ScheduledTask) {
try { $x = [xml](Export-ScheduledTask -TaskName $t.TaskName -TaskPath $t.TaskPath) } catch { continue }
$p = $x.Task.Principals.Principal
[pscustomobject]@{
Task = $t.TaskName; Logon = $p.LogonType; Hidden = $t.Settings.Hidden
Action = ($t.Actions | ForEach-Object { "$($_.Execute) $($_.Arguments)" }) -join ' ;; '
}
}
$out | Where-Object Logon -eq 'InteractiveToken' | Format-Table -Wrap
# ② 看谁是控制台程序、且没被 wscript 包着 → 就是嫌疑
# ③ 看它的触发周期是否对得上你被打断的节奏
Get-ScheduledTask -TaskName '<嫌疑任务>' | Get-ScheduledTaskInfo |
Select-Object LastRunTime, NextRunTime, LastTaskResult
# ③' 不确定第几次、什么时候:跑探针抓窗口(见工具包)
探针(flash-probe.ps1)是这套流程的核心:用 SetWinEventHook
(EVENT_SYSTEM_FOREGROUND=0x0003 + EVENT_OBJECT_SHOW=0x8002)记录每一次「窗口显示」和
「前台切换」,字段包括时间(毫秒)、PID、进程名、父进程、窗口类、标题、是否可见。
它每 5 秒写一行 BEAT 心跳 —— 有 BEAT 才证明探针活着,零事件才算「真的没闪」(这是上次踩过的坑:
探针被父进程杀掉后日志只剩 START,看起来像「零事件」,其实什么都没在监听)。
启动方式:用 Win32_Process.Create 或计划任务起 flash-probe-launch.vbs(wscript 隐藏启动、脱钩),
不要直接 Start-Process(会话结束可能被连带终止)。
5. 修复规范(新增任务前必须遵守)
规矩:凡是以交互身份运行、且会周期性/频繁执行的计划任务,一律走 hidden-launch 启动器。
动作: C:\Windows\System32\wscript.exe
参数: //nologo "D:\Backup\Documents\deepseek-harness\default-workspace\03-焦点闪窗\focus-watch\hidden-launch.vbs" "<原可执行文件>" "<原参数(内部引号写成双写 "")>"
工作目录: 保留原来的
身份: 不变(Administrator / InteractiveToken)
现役已改造清单(保持原触发频率与脚本行为):
| 任务 | 周期 |
|---|---|
| DSH-Quota-Turns | 每 2 分钟 |
| Cloud Sync | 每 5 分钟(2026-11-07 加入) |
| 额度记账本-账本同步 | 每 15 分钟 |
| DSH Archive Reaper / DSH-Quota-Logger / dsh-remote agent | 登录常驻 |
| Mihomo WARP Proxy / Ollama Server | 登录启动 |
写新任务时的自检(30 秒):
powershell -NoProfile -ExecutionPolicy Bypass -File `
'D:\Backup\Documents\deepseek-harness\default-workspace\03-焦点闪窗\focus-watch\fix-flash-window.ps1' -DryRun
# 输出 "OK:没有以交互身份直启控制台程序的计划任务" 才算干净
# 要它自动改:把 -DryRun 换成 -Fix(会先备份 XML 到 focus-watch\backup\)
6. 验证标准(别只改完就算)
- 手动触发:
schtasks /run /tn "<任务名>",同时探针在跑 → 日志里应没有ConsoleWindowClass/FG记录。 - 功能没坏:任务自己写的日志(如
D:\cloud-sync\sync.log)在触发时刻有新行;LastTaskResult = 0。 - 自然周期 ×2–3:等 10 分钟,让周期自己跑两三次,再确认零窗口事件。
- 心跳覆盖:确认探针日志的
BEAT时间跨度覆盖了这些触发点。
7. 回滚
原任务 XML 全在 focus-watch\backup\(实例 A:7 个任务;实例 B:Cloud Sync.xml.bak-20261107-101500)。
schtasks /create /tn "<任务名>" /xml "<备份文件>" /f
8. 常见误判(别浪费时间)
mshta弹出「额度记账本」:是桌面快捷方式(QuotaViewer.hta)被打开,不是故障。- SYSTEM 身份任务(如 WARP Proxy Watchdog):跑在 session 0,没有桌面,不会闪窗。
- GUI 子系统程序(
pythonw.exe、mshta.exe、wscript.exe、explorer.exe):本身不创建控制台, 一般不会因「窗口类 ConsoleWindowClass」抢焦点;但它们自己可能弹 UI(如更新器)。 OpenProcess看不到进程:闪窗进程活得极短,任务管理器/Process Explorer 都容易错过; 用探针的事件钩子才能抓到。
9. 工具包清单(同目录 source/flash-tools.zip)
| 文件 | 作用 |
|---|---|
flash-probe.ps1 |
窗口/前台事件探针(含 5 秒心跳),参数 -Seconds -Log |
flash-probe-launch.vbs |
隐藏、脱钩启动探针(wscript,无控制台) |
fix-flash-window.ps1 |
巡检 -DryRun / 批量修复 -Fix(自动备份 XML) |
hidden-launch.vbs |
通用隐藏启动器(本次修复的核心组件) |
flash-probe.log |
2026-11-07 抓到的现场证据 |
FIX-NOTES.md |
2026-11-03 第一次实例的原始记录 |
10. 一句话总结
控制台程序不能由任务计划直接以交互身份启动——-WindowStyle Hidden 和 Settings.Hidden 都拦不住
ConHost 抢焦点;必须让 wscript.exe + hidden-launch.vbs 用 SW_HIDE 去拉起它。
新增任务后跑一次 fix-flash-window.ps1 -DryRun,就能避免同一症状第三次回归。