DEVELOPMENT.md

開發者筆記

給要改這支擴充功能程式碼的人看的——單純想安裝使用的話看 README.md 就好。

本機開發(載入未封裝的擴充功能)

  1. Chrome 網址列輸入 chrome://extensions。
  2. 右上角開啟「開發人員模式」。
  3. 點「載入未封裝項目」,選擇這個資料夾(answerhub-extension/)。
  4. 改完程式碼後,回到 chrome://extensions 按重新整理即可套用——背景 service worker 跟一般分頁是分開重新整理的,改到 background.js 一定要點這裡的重新整理圖示,不能只 重新整理 netholiday 分頁,不然會發生「側邊欄是新版、背景還在跑舊版」的狀況(v0.7.1/ v0.7.2 真的發生過,見下方)。

架構

選擇器可信度

科目卡片(.icon-item[name="N"],pass 屬性判斷是否已完成)和送出按鈕(#examOver)是 照使用者提供的真實頁面 HTML 對過的。科目列表畫面跟題目畫面的網址(exam.action / answer.action,content.js 的 currentPage())也是使用者從真實網址列直接回報的。其餘 選擇器(#topic、#answer .radio、span.assign-num)還是照 NetHoAuto.py(這支擴充功能 的 Python + Selenium 前身,見下方)的紀錄推測、沒有對照真實頁面驗證過——如果換題/換科目/ 送出沒有照預期動作,先假設是這些沒驗證過的選擇器出問題。

也是照使用者提供的真實頁面確認過:數學題常常整題是一張圖(#topic 底下只有 <p><img></p>,公式沒辦法用純文字表示)。findQuestionElement() 原本要求元素要有文字才 算「找到」,純圖片的 #topic 因此被判定成「沒找到」,整段邏輯連進都進不去——不只不查答 案,連換題、等級 4 的猜都不會發生,遇到圖片題就卡住不動了。現在改成有文字或有 <img> 就算找到,questionText 抓不到文字時改用圖片的 src(解析後的絕對網址)當題目文字送去 查——不是新發明的做法,是 NetHoAuto.py 對這種情況本來就有的做法 (question_text = image_url # 使用圖片連結作為題目內容)。

這個網址一度對 AnswerHub 自己這邊保證查不到:apps-siao-ai 的 lib/answerhub/question-key.ts 會把題目文字裡的 http(s):// 網址整段拿掉才算 key(原始語 料裡的圖片題是「真正的文字敘述 + 附帶的圖片連結」,連結只是附加資訊,拿掉沒差)。一個 「整題就是網址」的題目拿掉網址後剩空字串,key 變成 null,不可能對到任何一筆資料——不 是沒收錄,是這個查詢方式對 AnswerHub 舊版的比對邏輯來說結構性地不可能成立。apps-siao-ai 的 answer-lookup 路由(2026-08-26 之後)已經修好:questionKey() 回傳 null 時,改用 imageIdsInQuestion() 抓出的圖片 uuid 去比對 question 欄位(爬蟲存資料時這個欄位的網址 沒被拿掉),真的查得到資料——所以這支擴充功能不用再自己繞過 AnswerHub 這段,兩邊(自己 站、敵方站)都照原本的優先順序查。真的連文字帶圖片都沒有的情況(例如純語音題)才會兩邊 都略過查詢,但換題、猜答案照常跑。

這是 NetHoHpRdlAuto(Python + Selenium 版本)的重寫版,選擇器與比對邏輯是參考它的既有決策,不是逐行照搬——瀏覽器擴充功能直接看 得到真實渲染後的 DOM,不需要 Selenium 版本裡大量的 fallback/重試機制。Python 版本維持獨 立、不受這次重寫影響。

排查問題

側邊欄的「即時動態」是第一個該看的地方(不用開 DevTools)。看不夠細的話,在 netholiday.kh.edu.tw 頁面按 F12 打開 DevTools 的 Console,篩選 [AnswerHub] 看到一樣的 內容加上結構化資料(物件、陣列)。

敵方站(netholiday.reh.tw)那個背景分頁查完會自動關閉,幾乎來不及開它自己的 DevTools, 所以查詢過程也會廣播到側邊欄的即時動態(前綴 [bg]/[rival]),同時記在 background service worker 自己的 console——去 chrome://extensions、這個擴充功能卡片、點 「service worker」開發者工具連結,篩 [AnswerHub:background] 看 「own site: not-found → 開分頁 → rival site: found/not-found」這條流程。分頁頁面本身 的操作細節(點了沒、撈到幾個候選答案)在 [AnswerHub:rival-site],只有真的手動切到那個 分頁、在它關閉前打開 DevTools 才看得到,比較難重現,平常排查就先看側邊欄或 background 那邊的摘要。

額度、金鑰測試、敵方站查詢分別是三套獨立的計數機制,搞混會誤判問題:

測試

比對邏輯(similarity.js)是唯一抽出來、脫離瀏覽器環境也能測試的部分 (content.js/background.js/sidepanel.js 直接依賴 chrome.* API 與真實 DOM,沒有 另外的測試 runner 之前先靠手動載入驗證)。hash-consistency.test.js 另外鎖住 background.js 跟 scripts/compute-hash.js 的雜湊合併分隔符一定要一致——這兩個曾經真 的兜不起來過(見下方發布 checklist 的附註),不是靠眼睛看出來的,是靠這條測試。

node --test

發布 checklist

每次改完 manifest.json/background.js/content.js/sidepanel.html/sidepanel.js/shared.css/similarity.js/netholiday-reh-scraper.js 任一個之後想發新版:

  1. 視改動大小手動 bump manifest.json 的 version。
  2. 執行 node scripts/compute-hash.js,拿到 {version, hash}。
  3. 把這個值貼進 apps-siao-ai repo 的 lib/answerhub/extension-version.ts(/answerhub/api/extension-version 路由讀這個 檔案),commit、push、重新部署 answerhub 容器。
  4. 執行 node scripts/package.js,把新的 dist/answerhub-extension.zip 複製到 apps-siao-ai repo 的 public/downloads/answerhub-extension.zip,一起 commit、push、 重新部署——這是使用者實際下載的檔案,忘記這步等於使用者永遠拿到舊版。
  5. commit、push 這個 repo。
  6. 順序不能反——extension-version 端點要先更新,不然舊版使用者短暫看到「版本正常」的 錯誤訊息。

v0.4.1/v0.4.2 曾經回報「版本一直顯示過期」,兩次都誤判成快取問題。 真正原因是 background.js 的 computeSelfHash 拿 "\x00" 當合併分隔符,scripts/compute-hash.js 用的是空格 " ",兩邊從 v0.2.0 起就沒對上過。已經修好,而且 hash-consistency.test.js 會在 node --test 時直接比對兩個檔案的分隔符——如果你改動任一邊的 .join(...) 呼叫, 先跑一次測試再發布。

發布通路

這個 repo 推到 SiaoHub(https://git.siao.ai/siao/answerhub-extension)——repo 頁面本身 公開可看,但 SiaoHub 目前匿名 clone 需要登入才行(設計決定,不是 bug),也沒有 「Download ZIP」功能。所以使用者實際下載走的是 apps.siao.ai 上的靜態檔案 (public/downloads/answerhub-extension.zip),不是 git.siao.ai——上面第 4 步就是在 講這個。這支 repo 本身還是繼續推到 SiaoHub,作為原始碼版本控制與紀錄,只是不是使用者下載 的入口。