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

> 云端副本（可直接阅读）：
> 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. 机制（为什么会闪、为什么会抢焦点）

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. 关键：**这些都不是脚本的问题，是启动方式的问题**。所以：
   - `-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 分钟出结论）

```powershell
# ① 列出所有「以当前登录用户身份运行」的计划任务及其动作
$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 启动器。**

```text
动作: 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
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`）。

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