# Async Task UI

> 背景任務與多工介面紀律——任何「使用者按下開始、系統在背景長時間工作」的介面(報表產出、批次處理、匯入匯出、長查詢、AI 生成)怎麼做到不騙人、不失控、不還魂。用於設計或修改任務型 UI:狀態機先於介面、終態不可逆、完成未領取進收件匣不自動佔版面、視圖與任務分離(伺服器為真相源)、送出必冪等、並行語意顯性化、測試必含髒環境。背景任務 UI 的失敗模式極其固定:重複送出、進度假死、重整丟任務、歷史任務還魂、完成了卻找不到結果——全部有標準解。

- Skill: `poloplay0114/async-task-ui-2` (Agent Skill)
- Install (CLI): `npx skillmds@latest add poloplay0114/async-task-ui-2`
- Raw SKILL.md: https://api.skillmd.com/api/skills/poloplay0114/async-task-ui-2/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Productivity
- Author: poloplay0114 (https://skillmd.com/u/poloplay0114)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/poloplay0114/async-task-ui-2

---


# 背景任務與多工介面紀律(Async Task UI)

## 什麼時候用

任何時候介面上出現「開始」按鈕、而工作要跑超過幾秒:報表/文件產出、批次匯入、長時間查詢、
AI 生成、檔案轉換。這類介面的物理是固定的:**HTTP 是無狀態的、前端是會死的(重整/關頁/斷線)、
使用者是會亂點的**——而任務必須活得比這三者都久。業界對此有一套高度一致的標準骨架
(送出→立即回任務 ID→背景執行→狀態端點輪詢→終態→領取結果);這份技能是那套骨架
加上真實踩雷換來的鐵則。失敗模式極其固定,標準解也是——不需要發明,需要的是不偏離。

---

## 核心法則

### 法則 1:狀態機先於介面——先定生死,再畫畫面

動手寫任何任務 UI 元件之前,先把任務狀態機定下來:最小集
`queued(排隊)→ running(執行中)→ succeeded / failed / cancelled(終態)`,
外加正交的 `delivered(已領取)`旗標。
- **狀態轉移寫成一張表入碼**(哪個狀態能到哪個狀態),禁止散落在各處的 if;
  轉移表配測試——非法轉移會吠。
- 進度百分比、預估時間都是裝飾,**狀態才是骨架**;先有誠實的狀態,才配有進度條
  (給不出誠實的百分比就別給,狀態文字比假進度可信)。
- 沒先定狀態機就寫介面=每個元件各自發明任務的生死,之後每顆 bug 都是「兩處對任務
  狀態的理解不一致」。

### 法則 2:終態不可逆——完成的任務進收件匣,絕不還魂

終態(succeeded/failed/cancelled)是單行道:**任何自動機制(重連、輪詢、恢復)
永不把終態任務重新開成工作視圖**。
- 完成而未領取(succeeded 且未 delivered)的任務 → **收件匣/任務中心**:被動列出,
  使用者點了才開;絕不自動佔用分頁/版面。
- 歷史殘留任務(幾天前的、測試留下的)一律歸收件匣;「載入時把所有找得到的任務
  都開成分頁」是本技能最經典的案發現場(見下)。
- 收件匣同時解掉「任務完成時使用者不在場」的交付問題——結果有家可歸,不必賭
  使用者的視窗還開著。

### 法則 3:視圖與任務分離——伺服器是真相源,前端只是望遠鏡

任務的生死存亡**絕不繫於任何視窗/分頁/前端物件的存活**:
- 送出後任務屬於伺服器;前端拿 task_id 輪詢狀態端點——關頁、重整、斷線,任務照跑。
- **重整=望遠鏡重新對焦**:載入時查詢「進行中/排隊中」任務、把視圖掛回去;
  不是任務重生、更不是任務死亡。
- 重連機制只認**非終態**(法則 2);且「掛回」=更新既有視圖或掛回既有容器,
  **絕不因輪詢撈到東西就開新版面**——視圖的誕生只有兩個合法來源:使用者動作、
  載入時掛回真正在跑的任務。
- 多視圖(多分頁)時,每個視圖各持自己的狀態容器(per-tab context),
  禁全域單例——全域狀態跨 await 交換必有 re-entrancy 撞況。

### 法則 4:冪等是送出的義務——連點、重試、重載都不得生出第二份工作

背景工作天生會被觸發多次(連點、網路重試、頁面重載後再按)——**同一份邏輯工作,
跑幾次都只能算一次**:
- 前端:送出即禁用按鈕/去抖(第一道,擋手速)。
- 服務端:冪等鍵(內容鍵或請求鍵)——同鍵重複送達=回同一個 task_id,不開新任務
  (第二道,擋一切前端擋不住的)。兩道都要,只有前端的=沒有。
- 取消也要冪等:連按取消無害;跑中取消走 `cancel_requested` 中間態(UI 顯「取消中…」),
  由工作端在檢查點收斂到 cancelled——不假裝立即死,也不讓使用者狂點。

### 法則 5:並行語意顯性化——不讓使用者猜,空白本身就是缺陷

能不能再開一個?排在第幾?上限幾個?——這些問題的答案必須**寫在介面上**:
- 有佇列就顯示順位(「排隊中,前面 N 筆」),不顯示假的「執行中 0%」。
- 有上限就在達上限時**擋下並講明**(「已達上限 N,需先關閉一個」),不是默默失敗。
- 使用者對背景任務的核心焦慮是掌控感:重整安不安全、能不能平行、系統到底更新了沒
  ——UI 不明講,使用者就開始亂試,亂試就撞出法則 4 要擋的事。
- 一個新開的工作區必須立即可用或明講為何不可用;**渲染成空白讓人猜=缺陷**,
  不是「還沒做完的功能」。

### 法則 6:髒環境是預設——乾淨環境全綠≠真環境全綠

任務型 UI 的測試 fixture 必含**兩軌**:乾淨環境 + 髒環境(預置歷史終態任務
N 筆+進行中 1 筆再載入)。
- 真實使用者的環境永遠帶著歷史:昨天的任務、上週的殘留、測到一半的孤兒。
  只在乾淨環境起跑的測試,對「載入」這個最日常的動作是零覆蓋。
- 髒環境斷言最小集:載入後恰好掛回「真正在跑的」、終態 0 自動復活、
  收件匣列出全部未領取。
- 交付前的自走驗收,環境=**真實使用者環境的等價複本**(含全部殘留),
  乾淨環境自走=沒自走。

---

## 本專案案發現場(佐證,非通用必需)

- **歷史任務還魂(法則 2/3/6 同場)**:某報表系統為「重整後恢復」加了重連機制,
  但未過濾任務狀態、且輪詢每撈到一筆就開一個新分頁——使用者僅上傳一份檔案,
  數天前的已完成批次逐一自己「開門走進來」佔滿分頁列。三重違規:終態被復活(法則 2)、
  輪詢開新版面(法則 3)、旅程測試全部乾淨環境起跑所以全綠(法則 6)。
  修法=狀態機補終態+完成未領取進收件匣+重連只認非終態+髒環境測試雙軌。
- **完成了卻找不到結果(法則 1/3)**:多份批次寫結果到 多份結果容器,交付視窗
  卻讀單份的 state['result'] → 進度打了 ✓、開窗 404。根因=沒有統一狀態機,
  單份/多份各自發明了「完成」的形狀。5 份成品其實好端端躺在磁碟——壞的是門牌,
  不是房子;但對使用者而言「拿不到=沒做出來」,嚴重度按後者計。
- **全域狀態互踩(法則 3)**:多分頁共用模組全域變數+單一 DOM 容器,
  切分頁互相 clobber、跑中視圖消失;計時器跨 await 交換全域=re-entrancy。
  修法=per-tab context 穿透,狀態綁分頁不綁模組。
- **連點防護(法則 4)**:「開始」連按三次=三份任務。修法=前端去抖+服務端冪等,
  連送 3 次只 1 次執行,測試鎖住。
- **姊妹技能**:狀態機/終態的驗證思路同 verification-discipline(獨立期望值、
  破壞測試會吠);「先查外部再開工」的時點紀律見 engineering-economy 法則 1;
  交付站/旅程級驗收見 ui-station-delivery;髒環境雙軌與快取還魂的資料層對應
  見 cache-discipline(壞 reader 種的快取結構性作廢)。

