iT邦幫忙

2026 iThome 鐵人賽

DAY 15
0
AI 自動化

用 AI Agent 打造你的產品使用手冊產線系列 第 15

[Day 15] 手工組裝產線 2:runner

  • 分享至 

  • xImage
  •  

昨天把 manifest 的三層結構(profile / bootstrap / chapters)定下來了,但那份 YAML 自己不會動。今天要處理讀進這份設定檔、真正操作 App、把截圖吐出來的角色:runner。

好的 runner 應該要具備的功能

一份完整的手冊 manifest,章節數可能上看二、三十章,跑一次要十幾分鐘。我覺得,一個好的 runner 要做的不會只是照著 manifest 從頭執行到尾,也需要這兩件事:

  1. 可以在跑到一半失敗時,留下必要的錯誤訊息
  2. 可以做到只執行部分章節

章節之間要不要重啟 App?

在寫執行迴圈之前,有一個值得討論的決定:

每一章開始前,要不要重新啟動一次 App,回到乾淨狀態?

這是一個經典的取捨問題,要不要冒著狀態污染的風險,去追求更快的速度?這在自動化測試領域應該也是會被討論到的問題。

我的想法是,通常這個腳本是不會太頻繁地使用的,不需要為了省那幾分鐘而冒這麼大的風險。我想要的是可以「穩定」地自動化建立使用手冊,因此,我預設每一個章節都從乾淨狀態重新開始,要重新啟動、重新注入狀態,才開始該章節的截圖步驟。

--chapter 局部重跑

有了「每章獨立開機」這個前提,局部重跑就只是把迴圈換成單一章節而已:

npm run manual                             # 整本重跑
npm run manual -- --chapter live-monitor   # 只重跑這一章

只要設計一個可以指定章節的參數就好,上面只是其中一種範例。

失敗時要留下什麼

失敗當下,runner 應該自動把除錯需要的東西存下來,讓人不用重跑一次就能看懂發生了什麼事:

  • 失敗當下的整頁截圖 (不是裁切後的,是完整畫面)
  • 當時的 DOM dump (方便回頭確認元件實際的狀態與結構)
  • 該次失敗發生在第幾個 step (step index)
  • 當下畫面上所有可用的 data-testid 清單
try {
  await runStep(page, chapter.steps[i], dir)
} catch (cause) {
  const dir = `output/failures/${chapter.id}`
  await page.screenshot({ path: `${dir}/failure.png`, fullPage: true })
  await writeFile(`${dir}/failure.html`, await page.content())

  throw new RunnerError({ chapter: chapter.id, stepIndex: i, step: chapter.steps[i], cause })
}

這幾樣東西最後要合成一則錯誤訊息,而這則訊息除了給人類當參考之外,主要是給 AI Agent 看的 (之後再討論)。以下就拿範例專案實際跑一次,看看這則訊息長什麼樣、又能幫我們做到什麼。

用範例 App 進行 Demo:整本 → 失敗 → 定位 → 只重跑一章

總覺得還是要有一個實際範例比較有說服力,大家也才能對這個腳本比較有畫面。因此,以下設計了一個簡單範例:用 runner 去執行整本 manifest 截圖,但遇到錯誤,接著排查出原因並進行修正,最後重新跑對應章節的腳本 (而不是整本)

範例專案的 manifest 目前有四章,照 order 排是 overview / live-monitor / layout-preset / settings。直接整本跑:

整本執行的終端機輸出,前兩章通過,第三章 layout-preset 失敗

$ npm run manual -- --mode web

manifest: demo-stream-app v1.2.0  共 4 章  [web]

▶ [1/4] overview  介面總覽
✔ [1/4] overview  3 steps / 1 shot  1.3s

▶ [2/4] live-monitor  即時監控畫面
✔ [2/4] live-monitor  6 steps / 2 shots  1.7s

▶ [3/4] layout-preset  儲存版面設定
✖ [3/4] layout-preset  step 4 失敗:{"action":"screenshot","name":"layout-preset-01"}
找不到 annotate[1].testid「grid-cell-fps_1」
  目前畫面上有 150 個 testid,其中 grid-cell* 開頭的有:
    grid-cell_1 / grid-cell-view_1 / grid-cell-det_1a / grid-cell-det_1b /
    grid-cell-name_1 / grid-cell-timestamp_1 / grid-cell-remove_1 / grid-cell_2
  失敗當下的畫面:output/failures/layout-preset/failure.png
  失敗當下的 DOM:output/failures/layout-preset/failure.html
  修好之後只要重跑這一章:npm run manual -- --chapter layout-preset
  尚未執行的章節:settings

不用打開瀏覽器,這則訊息已經交代了四件事:

  1. 失敗在 layout-preset 的第 4 步,那一步是 screenshot,名字叫 layout-preset-01
  2. 找不到的不是這一步本身的 testid,而是 annotate[1].testid——是標註欄位出問題,不是操作出問題。
  3. 當下畫面有 150 個 testid,grid-cell* 開頭的那一群列出來了:grid-cell_1grid-cell-view_1grid-cell-name_1grid-cell-timestamp_1……都在,就是沒有 grid-cell-fps_1
  4. 前兩章的產物已經好了,settings 這章根本還沒開始跑。

第 3 點是關鍵。有了候選清單,可以立刻分辨兩種完全不同的狀況:

  • 候選裡有一個長得幾乎一樣的(例如 grid-preset-sav 對上清單裡的 grid-preset-save)——那就是打錯字,把字補回去就好。
  • 同一群的其他成員都在,就缺這一個——那不是打錯字,是這個元件在當下的畫面狀態根本沒被渲染出來。

這次顯然是後者。打開留下來的 failure.png 就確認了:

失敗當下的整頁截圖,4×4 版面下格子底部只剩名稱與時間戳

4×4 版面下,格子底部的資訊列只剩下攝影機名稱跟時間戳,偵測到 N 個物件NN fps 那兩段不見了——因為格子寬度不夠時,這兩個元素會被整個移除(不是隱藏,是真的不在 DOM 裡)。所以「grid-cell-fps_1 在 2×2 存在」跟「在 4×4 不存在」兩件事同時都是對的,錯的是 manifest 要它在 4×4 的畫面上被標註。

候選清單這件事實作上有個小細節很值得注意:前綴要從最長的開始試grid-cell-fps_1 找不到時,如果只用第一段 grid 當前綴,150 個 testid 裡光是工具列就佔滿了前八個(grid-panel / grid-toolbar / grid-layout_1x1……),最有用的 grid-cell-* 一個都看不到。從 grid-cell-fps_1 開始逐段往回退到 grid-cell,列出來的才是真正該比對的那一群。

修法:改步驟

問題不在 runner,在 manifest 的步驟順序——這張圖要標 fps,就只能在 2×2 拍。把截圖那一步搬到切版面之前就好:

   # 先填一格,版面才不是空的
   - { action: dblclick, testid: camera-row_gate-a }

-  - { action: click, testid: grid-layout_4x4 }
-
+  # grid-cell-fps_{n} 在 3×3 以上會被精簡掉,這張圖只能在 2×2 拍
   - action: screenshot
     name: layout-preset-01
     clip: { testid: grid-view, padding: 12 }
@@
       - { key: fps, testid: grid-cell-fps_1, legend: 即時影格率 }

+  - { action: click, testid: grid-layout_4x4 }
+
   - { action: click, testid: grid-preset-save }

這就是 Day 14 講「可 diff」的實際樣子:改動是一個步驟的位置,加上一行說明為什麼,review 的人不需要在腦中模擬整段流程。

只重跑這一章

只重跑 layout-preset 一章的終端機輸出

$ npm run manual -- --chapter layout-preset --mode web

manifest: demo-stream-app v1.2.0  只跑 1 章(--chapter layout-preset)  [web]

▶ [1/1] layout-preset  儲存版面設定
✔ [1/1] layout-preset  10 steps / 2 shots  1.5s

完成 1 章,總共 2.2s -> screenshots/

2.2 秒結束,另外三章的產物完全沒有被碰到,失敗現場也一併被清掉了:

screenshots/                     screenshots/
├─ overview-01.png               ├─ overview-01.png
├─ live-monitor-01.png     →     ├─ live-monitor-01.png
└─ live-monitor-02.png           ├─ live-monitor-02.png
                                 ├─ layout-preset-01.png
output/failures/                 └─ layout-preset-02.png
└─ layout-preset/
   ├─ failure.png                output/failures/
   └─ failure.html               └─ (空的)

這一段是真的跑出來的,不是示意。範例專案的 chore/day15 分支把「還沒修」與「修好」排成兩個 commit,README 有完整的重現步驟——取回修正前的那一版 manifest、跑到失敗、再重跑那一章。

關於腳本的產物

檔名規則要跟 Day 05 定下的命名規則一致 (docs/{order}-{id}.mdscreenshots/{name}.png);輸出目錄結構固定,方便後續 pandoc 合併與比對。每次執行前,要不要清空舊產物,也需要一個明確的預設值——通常是清空,避免舊產物殘留造成混淆,除非明確指定要保留基準線截圖用於比對 (例如要對照改版前後的畫面差異時)。

清空的單位是一章,不是整個輸出目錄。上面那次局部重跑只清掉 layout-preset-* 這幾個檔案,這是局部重跑之所以有意義的另一半:如果每次執行都把整個目錄砍掉重建,那 --chapter 就只剩下「跑得比較快」,其他三章的產物一樣得重來一次才拿得回來。

值得一提的是,截圖是扁平地全部放在 screenshots/ 底下,沒有按章節分資料夾,卻仍然刪得乾淨。這是因為檔名前綴就是章節 id。這正是 Day 05 那套命名約定在發揮作用:產線各段之間靠檔名對齊,不需要額外維護一份索引檔來記錄「哪些圖屬於哪一章」。

經驗分享

平行化

跑一份大型 manifest 時,很自然會想:

這些章節之間互不依賴,為什麼不平行跑,加快速度?

這件事理論上是可行的,但前提是系統的資源要夠多,同時開多個瀏覽器或是 Electron 其實也會消耗不少系統資源,可能會影響系統穩定性 (e.g. 變 Lag XD),進而導致截圖可能會發生預期之外的失敗。

對使用手冊的建立來說,穩定性應該是第一優先的,因此,除非真的很確定系統資源足夠,不然我會建議還是一個一個章節執行就好。

這個取捨,恰好跟 E2E 測試的常見取捨完全相反。測試通常都會要求快、要求平行執行,好讓 CI 回饋週期短

錯誤訊息要把下一個指令也寫出來

上面那則錯誤訊息的最後兩行,是把「修好之後只要重跑這一章」的完整指令、以及「尚未執行的章節」直接印出來。這不只是貼心,更重要的是方便之後 AI agent 接手修 manifest 之後,可以直接用錯誤訊息中所提示的指令,不必自己拼湊。

小結

到目前為止,應該可以算做 Part 1。

前面鋪陳了動機和需求,接著介紹比較基礎 無聊 的各種實作細節,這兩天開始介紹 manifest 描述「要什麼」、runner 決定「怎麼跑」,兩篇加起來才是一套完整的截圖工具。局部重跑靠的是每章獨立開機、獨立可執行;失敗時留下的截圖、DOM、testid 清單,則是讓後續 AI agent 上場時有線索可以操作。

明天開始進入 Part 2,讓 AI Agent 進場了。

今天那次失敗,是人讀著錯誤訊息把 manifest 修好的,之後就可以換 AI Agent 讀同一則訊息,讓他自己搞定。


上一篇
[Day 14] 手工組裝產線 1:宣告式設定檔
下一篇
[Day 16] 自動組裝產線 1:準備讓 AI Agent 寫設定檔
系列文
用 AI Agent 打造你的產品使用手冊產線17
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言