iT邦幫忙

2026 iThome 鐵人賽

DAY 14
0
AI 自動化

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

[Day 14] 手工組裝產線 1:宣告式設定檔

  • 分享至 

  • xImage
  •  

前六天的「產線實作」陸續把定位元件、狀態注入、時機與構圖、標註、標號、遮蔽這些單點技術都做出來了,但它們現在還是散落在各自的腳本裡。接下來要把產線組裝起來,這件事分兩步:先用一份設定檔把這些技術收攏起來,再靠 runner 真正執行它,今天先處理第一步。

這份設定檔就是整條產線的心臟。

直接讓 AI 寫腳本就好,不好嗎?

如果是直接請 AI 寫一支 TypeScript 腳本,從啟動 App、操作、截圖到輸出一手包辦,看起來確實很方便。但是,這個腳本基本上會是一個上百行的程式碼,每次有版本更新的時候,AI 給你的往往是一份幾乎全新的版本 (或許脈絡一致,但很難一眼看出),而不是針對變動處的小修改。

結果就是 diff 整片都是紅綠、沒有人 review 得動,最後大家只能賭它是對的,沒有人真的知道這次重新生成到底改了什麼。

(我知道這件事不是不可能,只是有點辛苦XD)

這就是「宣告式」的意思:前面那支 TypeScript 腳本是程序式的,一行一行寫「怎麼做」。設定檔則是純資料,只描述「要什麼」 (e.g. 要等哪個元件、截哪一塊、標哪幾個框),本身不會執行任何動作,怎麼跑、跑幾次都交給明天的 runner 決定。

改成宣告式的設定檔之後,有四個關鍵性質:

  • 可 diff:新增一章通常只是多十幾行,git diff 一眼就看得出這次加了什麼。
  • 可局部重跑:只改了某一章就只重跑那一章,不用整本重來 (這件事明天會展開)。
  • 可驗證:用 JSON Schema (或其他類似的工具) 來避免不合法的內容,擋掉 AI 幻覺出來、根本不存在的 action。
  • 可轉換:同一份描述除了產手冊,理論上也能拿去產 smoke test、甚至產教學影片,因為它描述的是「操作序列」這個抽象概念,而不是綁死在某一種輸出格式上。

三層結構

整份設定檔分成三層:

  • profile:這次要產哪一個版本的手冊,產品線、語言、版本號。
  • bootstrap:全域前置設定,例如狀態注入、視窗尺寸,也就是每一章開始前都要準備好的環境。
  • chapters:章節,每一章底下是一連串的 steps

前兩層寫在 manifest/manual.yaml

profile: demo-stream-app
version: 'v1.2.0'
locale: zh-Hant

bootstrap:
  viewport: { width: 1600, height: 900, deviceScaleFactor: 2 }
  # 狀態注入在第一次 navigation 之前發生(Day 09)
  storage:
    locale: zh-Hant
    role: operator
    layout: '{"mode":"2x2","cells":[null,null,null,null]}'
  clock: '2025-09-01T09:00:00'
  disableAnimations: true

章節則是一章一個檔案 (manifest/{order}-{id}.yaml),而不是全部塞在同一份陣列裡。這樣做是為了讓 diff 跟重跑的單位都是「一章」,新增章節不會動到別人的檔案,三個人同時改三章也不會撞在一起。

前兩天為了聚焦在單一步驟,設定檔片段都是用 JSON 寫的,完整的 manifest 則是 YAML。原因很單純:YAML 可以寫註解,而註解正是「為什麼這一步要等這個元件」這種資訊的家。

章節設定檔

以範例 App 的「即時監控」這一章為例:

id: live-monitor
title: 即時監控畫面
order: 20

steps:
  # 等骨架屏消失、清單出現才開始(Day 10)
  - { action: waitFor, testid: camera-list-skeleton, state: detached }
  - { action: waitFor, testid: camera-list }

  - action: screenshot
    name: live-monitor-01
    clip: { testid: monitor-page, padding: 12 }
    annotate:
      - { key: search, testid: camera-search, legend: 攝影機搜尋框 }
      - { key: layout, testid: grid-layout-group, legend: 版面切換 }
      - { key: save, testid: grid-preset-save, legend: 儲存目前版面 }

  - { action: fill, testid: camera-search, text: lobby }
  - { action: dblclick, testid: camera-row_lobby-01 }

  - action: screenshot
    name: live-monitor-02
    clip: { testid: grid-panel, padding: 12 }
    annotate:
      - { key: cell, testid: grid-cell_1, legend: 剛填入的格子 }
      - { key: remove, testid: grid-cell-remove_1, legend: 清除此格 }

這份 YAML 丟給 runner,跑出來的就是這兩張圖:

第一張截圖:整個監控頁,標上搜尋框、版面切換、儲存版面

第二張截圖:雙擊 Lobby-01 之後,格 1 被填入,時間戳停在 09:00:00

前面六天的東西在這裡全部就定位了:clock 凍結了時間戳、disableAnimations 停掉了偵測框的飄移、waitFor ... detached 等掉了骨架屏、clip 決定構圖、annotate 畫框標號、legend 之後會渲染成圖片下方的表格。

幾個刻意的設計決定

1. 只收 testid,不收任意 selector

看起來少了彈性,但這是刻意的:selector 一旦開放成自由字串,AI 就會開始寫 .camera-list > div:nth-child(3) 這種東西,Day 08 好不容易建立的穩定性就白費了。限制成 testid 之後,驗證時還可以直接拿 TESTID.md 對照,這是自由字串做不到的。

2. 沒有條件判斷、迴圈、變數

這是整份設計裡比較反直覺的一條。工程師的本能是「加個 if、加個 for 不是很方便嗎」,但一旦有了這些,人 review 的時候不再是看過 diff 就懂全貌,而是要在腦中模擬所有分支。

3. order 獨立於檔案順序

章節順序由 order 欄位決定,用 10 的倍數編號,中間留白。之後要在第 20 章跟第 30 章之間插一章,直接用 25,不需要動到其他任何檔案。

驗證要跑在最前面

設定檔的價值有一半來自「它可以被機器檢查」。驗證分兩個階段,第一個階段在 App 啟動之前就跑完:

Error: manifest 驗證失敗(2 個問題):
  - live-monitor.steps[1]: 不存在的 action「type」,可用的有:click / dblclick / fill / waitFor / wait / scroll / hover / dismiss / screenshot
  - live-monitor.steps[2]: action: screenshot 缺少必填欄位 name

這種錯誤根本不需要打開瀏覽器就知道,所以也不該等到產線跑到第 18 章才爆掉。

第二個階段是執行期,這時候的錯誤訊息是寫給 AI agent 看的,所以不能只說「找不到元素」:

[live-monitor] step 1 失敗:{"action":"fill","testid":"camera-searchbox","text":"lobby"}
找不到 testid「camera-searchbox」
  目前畫面上有 119 個 testid,其中 camera* 開頭的有:
    camera-panel / camera-panel-title / camera-add / camera-search / camera-hint / camera-list / camera-row_gate-a / camera-state_gate-a
  失敗當下的畫面:output/day14/failure.png

把候選清單跟失敗當下的畫面一起吐出來,agent 才有辦法自己把 camera-searchbox 修成 camera-search,不然它就只能重猜一次。這件事在後面提到讓 AI 寫設定檔的時候變得非常關鍵。

經驗分享

1. YAML 的型別陷阱

YAML 的自動型別推斷很方便,但也很會惹事:

  • noyesonoff 在 YAML 1.1 下會被解析成布林值,本意是字串的話一定要加引號。
  • 版本號 1.20 沒加引號會變成數字 1.2,尾端的零直接消失。
  • 中文字串裡的冒號如果後面接了空白 (例如 說明: 這是範例),會被誤判成 key-value。

因此,任何看起來像數字、布林值,或含有特殊符號的字串,一律加引號,避免不必要的麻煩。

2. 動詞集裡本來就該有「允許失敗」的動詞

動詞集要小,但小不等於每個動詞的行為都一樣嚴格。dismiss (關掉可能殘留的對話框) 就是一個天生允許找不到目標的動詞——上一章結束時對話框不一定還開著,硬要找到才繼續反而會卡住整條產線。

這跟 Day 13 的 redact 剛好是一對:同樣是「找不到目標」,dismiss 該跳過,redact 該中止。容錯策略是動詞語意的一部分,不是全域設定,所以它應該寫死在動詞的定義裡,而不是開一個 optional: true 讓每個人自己填——那個欄位一旦存在,遲早有人會加在 redact 上。

3. manifest 是契約,改 testid 要一起改

Day 08 說 data-testid 是手冊產線與 E2E 測試的共同契約,manifest 出現之後,這份契約多了一個真實的引用者。testid 改名時如果沒有同步更新 manifest,下次重跑就會在那一步停下來。

好消息是,這個失敗是立刻可見的:驗證會直接指著出問題的那一步,而不是默默產出一張怪怪的圖。這也是為什麼 validate 值得放進 CI——它等於幫 testid 這份契約補上了一個守門員。

小結

今天這份設定檔自己不會動,它做的事情是把前六天的技術「說清楚要用在哪一步」,真正讓它動起來的是明天的 runner。

設定檔負責描述,runner 負責執行,兩篇加起來才是一套完整的截圖工具。

不過這裡的「完整」有兩點要先講清楚:

  1. 它產出的是截圖,不是手冊。
  2. 它從頭到尾沒有 AI。

明天要處理執行這份設定檔的 runner:怎麼做到局部重跑、以及失敗的時候該留下哪些資訊。


上一篇
[Day 13] 產線實作 6:遮蔽敏感資訊
下一篇
[Day 15] 手工組裝產線 2:runner
系列文
用 AI Agent 打造你的產品使用手冊產線17
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言