桌面一闪而过的黑框 + 打字被打断:诊断与根治

云端副本(可直接阅读): 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. 症状

2. 机制(为什么会闪、为什么会抢焦点)

  1. 任务计划程序(Task Scheduler 服务,svchost)以 InteractiveToken 身份启动任务时, 子进程运行在当前登录用户的可交互桌面(winsta0\default)。
  2. 如果子进程是控制台子系统程序(python.exe、powershell.exe、pwsh.exe、cmd.exe、 node.exe、cscript.exe…),Windows 会给它分配一个 ConHost 控制台窗口 (窗口类 ConsoleWindowClass,典型尺寸 993×839 / 993×519)。
  3. 这个控制台窗口创建时是可见的,并且会被激活到前台 → 焦点从你正在打字的窗口被抢走。 任务跑完,进程退出,窗口消失,焦点还回去 —— 看起来就是「闪了一下」。
  4. 关键:这些都不是脚本的问题,是启动方式的问题。所以:
  5. -WindowStyle Hidden(powershell 参数)没用:它只能隐藏脚本宿主自己的窗口引用, ConHost 窗口仍然先创建、先显示、先抢焦点。
  6. 任务设置里的 Hidden = True 也没用:那只是「不显示任务栏按钮」,不影响控制台窗口。
  7. 唯一的正解是让创建进程的那一方用 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(回归,单点)

探针抓到的原始证据(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. 验证标准(别只改完就算)

  1. 手动触发:schtasks /run /tn "<任务名>",同时探针在跑 → 日志里应没有 ConsoleWindowClass/FG 记录。
  2. 功能没坏:任务自己写的日志(如 D:\cloud-sync\sync.log)在触发时刻有新行;LastTaskResult = 0。
  3. 自然周期 ×2–3:等 10 分钟,让周期自己跑两三次,再确认零窗口事件。
  4. 心跳覆盖:确认探针日志的 BEAT 时间跨度覆盖了这些触发点。

7. 回滚

原任务 XML 全在 focus-watch\backup\(实例 A:7 个任务;实例 B:Cloud Sync.xml.bak-20261107-101500)。

schtasks /create /tn "<任务名>" /xml "<备份文件>" /f

8. 常见误判(别浪费时间)

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,就能避免同一症状第三次回归。

generated by cloud_sync · 2026-11-14 03:02