iT邦幫忙

2026 iThome 鐵人賽

DAY 19
0

! 本篇文章將會介紹 iT 邦幫忙發文實戰(上):編輯器解析,期望大家都能在按下自動化開關之前,先把目標網頁的結構看個清楚 :D

昨天預告的「最終戰場開箱」今天兌現。前 18 天,Agent 已經學會寫文章(d03–d07)、被排程叫起床(d09–d12)、還會主動回報戰況(d13);瀏覽器的基本功——填表、等待、截圖驗證——也在 d15–d18 練完了一輪。萬事俱備,只欠把這些功夫全部砸在 iThelp 的發文頁上。但我有個鐵律:自動化之前,先當一分鐘的真人——手動走一次流程、把頁面結構摸清楚,才知道要跟哪些元件打交道。所以今天不按任何按鈕,先當偵察兵,把發文頁拆成三塊來看:入口與表單結構、markdown 編輯器、分類與 tag。明天再把拆下來的零件組裝成 publish.sh。

本篇目標

讀完這篇你會學到:

  • 鐵人賽發文入口跟一般發文差在哪,以及那個 hidden input article_type 為什麼是防呆關鍵
  • iThelp 的 markdown 模式其實是 SimpleMDE(底層 CodeMirror),以及為什麼「直接 fill」會失效
  • 分類與 tag 的選擇行為:select2 的搜尋式互動、tag 上限、前綴匹配的坑

環境準備

  • d16 的 profile 綁定(iThelp 登入狀態常駐)與 d18 的存證資料夾
  • 開賽前跑過一次校準,把發文頁的 accessibility tree 存下來
# 沿用的綁定函式(d16)
ab() { agent-browser --session ithelp --profile "$HOME/.agent-browser-profiles/ithelp" "$@"; }

# 開賽前校準:開發文頁、輸出 snapshot 與截圖(scripts/calibrate.sh 的 probe 子指令)
./scripts/calibrate.sh probe
# 產出:state/snapshot-*.txt(accessibility tree)、state/screenshot-*.png
# 之後找 selector 全靠這份事實,不要憑想像猜

主要內容

步驟一:入口解析——從「鐵人發文」到那個 9571

iThelp 有兩種發文入口,長得像但身世完全不同:

  • 一般技術文:https://ithelp.ithome.com.tw/articles/create?group=tech,進去要自己選分類群組
  • 鐵人賽專用:從首頁「鐵人發文」按鈕 → modal 裡點自己的系列,落到 https://ithelp.ithome.com.tw/2026ironman/create/9571——網址尾端的 9571 就是系列 id

鐵人入口有三個行為差異,每一個都影響自動化設計:

  1. 表單多一個 hidden input article_type=ironman(一般文沒有)——這是文章計入賽事的識別
  2. 自動附加「18th鐵人賽」tag,不用(也沒法)自己掛
  3. 入口會重用最新的空草稿(含你以前手動留下的一般草稿),沒有空草稿才新建

第 3 點聽起來是貼心功能,對全自動系統卻是地雷:哪天入口重用到一篇一般草稿,文章發出去就不計賽事,等於白發。所以 publish.sh 的第一道守衛就是驗明正身:

# 鐵人賽模式確認:入口必須產出 article_type=ironman 的草稿(節錄自 publish.sh)
ATYPE=$("${AB[@]}" eval '(document.querySelector("input[name=article_type]")?.value||"none")' 2>/dev/null | tr -d '"')
if [[ "$ATYPE" != "ironman" ]]; then
  die "此草稿 article_type=${ATYPE}(應為 ironman)——發文入口可能有誤,中止以免白發"
fi

順帶記一個會影響明天成敗的事實:這個頁面的 <form> 裡其實只有 _token 和 _method 兩個欄位,送出根本不是傳統 form submit,而是前端 JS 發 XHR。意思是——按了送出之後,頁面不會給你任何「成功」的表單回饋,成敗得另想辦法判斷。先記著,明天細講。

步驟二:markdown 模式——SimpleMDE 的偷天換日

小小小測驗:你知道發文頁那個看似平平無奇的內文輸入框,其實早就被偷天換日過了嗎?
答案:HTML 原始碼裡只有一個 textarea[name="description"],但頁面載入時 SimpleMDE 把它藏起來,換上自己的 CodeMirror 編輯器。你看到的編輯區根本不是那個 textarea——直接對 textarea fill,是寫給空氣看的。

校準時從 snapshot 拼出來的發文頁解剖圖,先奉上:

發文頁(2026ironman/create/9571)
├── input.post-header__title          # 標題(name=subject),普通 input,fill 就好
├── textarea[name="description"]      # 名義上的內文欄(被 SimpleMDE 藏起來的影子)
│   └── .CodeMirror                   # 真身:CodeMirror 實例,markdown 模式在這裡
├── .select2-search__field            # tag 搜尋框(select2,不是原生 <select>)
│   └── .select2-selection__choice    # 已掛上的 tag 們
├── input[name=article_type]          # hidden:鐵人草稿 = ironman(防呆檢查點)
└── button.save-group__btn            # 「儲存草稿」
    └── ▲ .save-group__dropdown-toggle  # 旁邊的 ▲ 展開下拉選單
        └── ...--publish              # 「發表文章」其實藏在這個下拉裡

SimpleMDE 是一套輕量 markdown 編輯器:工具列的粗體、連結、code 按鈕,實際行為就是幫你插入對應的 markdown 語法;編輯區本體由 CodeMirror 接手渲染。對人類是所見即所得的糖,對自動化則是一紙合約,三個條款:

  1. markdown 進、markdown 出:編輯器不會改寫你的語法,餵什麼存什麼——所以生成端寫 markdown、發文端直接餵,全系列語法零轉換
  2. fill 對它無效:CodeMirror 不是普通 textarea,得走它的 API
  3. .CodeMirror 是唯一真身:定位就抓這個 class
# 主路線:用 CodeMirror API 直接 setValue,整篇 markdown 一次進場(節錄自 publish.sh)
BODY_JSON=$(jq -Rs . "$BODY_FILE")   # markdown 檔先變成合法 JSON 字串
"${AB[@]}" eval "(function(){var cm=document.querySelector('.CodeMirror');
  if(!cm||!cm.CodeMirror){return false}
  cm.CodeMirror.setValue(${BODY_JSON});
  return cm.CodeMirror.getValue().length})()"   # 回傳字數 = 填入成功的佐證

# fallback:click 編輯區 + keyboard type(模擬真人打字,慢但通用)
"${AB[@]}" click 'textarea[name="description"]'
"${AB[@]}" keyboard type "$(cat "$BODY_FILE")"

順帶對手動發文的讀者說一句:這也解釋了為什麼可以直接把 markdown 原文貼進 iThelp 編輯器——工具列只是糖,語法才是本體。本系列從 generate.sh 剝掉 frontmatter 後的內文,就是一路 markdown 到底、原封不動進編輯器的。

步驟三:分類與 tag——select2 的遊戲規則

分類這關,鐵人入口幫你省掉了:一般文要自己挑分類群組,鐵人入口則直接把文章掛在你的系列底下。所以腳本要處理的只剩 tag——而 tag 的互動元件是 select2,一個搜尋式下拉,不是原生 <select>,select 類指令對它完全無效。正確玩法是「逐個搜尋 + Enter 收貨」:

# select2 的正確玩法:填搜尋框 → 等下拉選項 → Enter(節錄自 publish.sh)
IFS=',' read -ra TAGARR <<<"${TAGS}"   # TAGS="自動化,AI Agent,opencode"
for t in "${TAGARR[@]}"; do
  "${AB[@]}" fill '.select2-search__field' "$t"
  sleep 1                              # 等匹配選項浮出來
  "${AB[@]}" press Enter
done

三個坑先講在前面。第一,前綴匹配:搜尋「自動化」按 Enter,掛上來的會是下拉第一個匹配項「自動化測試」——想精確命中,得看著下拉清單點目標。第二,上限 5 個:鐵人流程自動加的「18th鐵人賽」也佔名額,自己想塞的其實只剩 4 個。第三,掛上不等於選對——這就輪到 d18 的三層驗證出場了,填完要讀出來對帳:

# 驗證:列出實際掛上的 tag,跟預期比對(節錄自 publish.sh)
"${AB[@]}" eval 'JSON.stringify([...document.querySelectorAll(".select2-selection__choice")].map(function(e){return e.textContent.replace(/[×✕]/g,"").trim()}))'
# 輸出範例:["18th鐵人賽","自動化測試","opencode"]

明天的 publish.sh 會把這段變成制式的 tag 驗證:缺了就發 Discord 警告,但不擋發佈——tag 少一個,不值得讓整篇文章卡關,這種「警告但放行」的分級就是 d12 講的防禦工事的縮影。

常見問題 / 踩坑記錄

  • Q:對內文 textarea fill 明明回報成功,存草稿後內文卻是空的?
    A:你在跟影子打交道。CodeMirror 把真 textarea 藏起來了,寫進 textarea 的值不會同步到編輯器。改走 cm.CodeMirror.setValue();API 抓不到再 fallback 成 click + keyboard type。
  • Q:headless 模式開發文頁,直接被伺服器 403?
    A:iThelp 對無頭瀏覽器有伺服器端偵測,實測必須 --headed 帶真 Chrome 才放行。而且這只是第一關——發佈動作後面還有 Cloudflare Turnstile,需要「真 Chrome + 停用 AutomationControlled + stealth init script」三件套,明天發佈實戰一併細講。
  • Q:入口開出來的草稿,綁到一篇不知道哪來的舊文?
    A:入口會重用「最新的空草稿」,包括你手動發文失敗留下的殘骸。開賽前把雜草清乾淨,並靠 article_type 防呆守門——綁錯直接中止不發。寧可漏發一篇,不要白發一篇。

小結

  • 鐵人入口與一般入口的差異:article_type=ironman、自動掛賽事 tag、草稿重用——入口選錯,一切白工
  • markdown 模式 = SimpleMDE/CodeMirror:markdown 進出零轉換是禮物,fill 失效是代價,setValue 是正解
  • tag 是 select2 不是 <select>:搜尋 + Enter、上限 5 個、前綴匹配會送你意想不到的 tag
  • 偵察先於進攻:calibrate probe 的 snapshot 是所有 selector 的事實來源,自動化前先當一分鐘真人

明日預告

下一篇我們要介紹「iT 邦幫忙發文實戰(下):一鍵全自動發文」,把今天拆解的所有零件組裝成 publish.sh:填表與驗證、dry-run 與 live 的模式切換、發佈重試與存證,一鍵到底,敬請期待!

參考資料:

有任何疑問但沒有 iT 邦幫忙帳號,或是想匿名提問?
歡迎到 https://dev.benben.me/q/Z5442T 提問或加油打氣,沒意外的話會在完賽之後一起回答 :D


上一篇
18 截圖驗證:眼見為憑的自動化驗收
系列文
自我耍廢組:全自動化の鐵人 共 19 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言