內建式 Bug / Issue 回報服務。使用者在自己的網站上圈選畫面回報,Claude Code 直接讀檔修復。
先到後台的專案頁建立專案, 並在 allowedOrigins 欄位填入你的網站網址,然後貼這兩段:
allowedOrigins 具體要填什麼?key(例:taione)、名稱、authMode, 並在 allowedOrigins(逗號分隔) 欄位填入網址;按「建立」。 填的是 origin,也就是「協定 + 網域 + port」, 不含路徑,也不要結尾斜線:
https://taione.com, http://localhost:3100https://taione.com/(多了結尾斜線)https://taione.com/app(帶了路徑)taione.com(缺協定) 幾個容易漏掉的情況:開發機與正式站是不同的 origin,兩個都要填; http://localhost:3100 與 http://127.0.0.1:3100 也算不同的 origin; 換 port 就要再加一筆。漏填時 widget 會完全連不上服務, 瀏覽器 console 會出現 CORS 相關錯誤 —— 那就是這一格沒對上。
<script type="module"
src="https://issues.example.com/w.js"
data-project="your-project"></script>
<script type="module">
window.IssueSystem.identify({ id: user.id, name: user.name, role: user.role })
</script> 以上兩步就能回報。identify() 在框架的 plugin 裡呼叫即可, 比 widget 載入晚沒關係 —— API 會等它。 選配:provideState() 送出應用狀態、 @taylor/issue-system-widget/vite 的 plugin 提供原始碼位置對應。
src 不要加任何 query 參數(例如 ?v=20260911)/w.js 本身就帶 Cache-Control: public, no-cache, must-revalidate, 每次載入都會用 ETag 回來驗一次:改版一定拿得到新的,沒變就回 304 幾乎不耗流量。 不需要自己加 cache-bust 參數。
加了反而有代價:瀏覽器的 module map 以完整網址為 key, w.js?v=1 與 widget 內部 chunk 引用的 w.js 會被當成兩個不同的模組,各下載、各執行一次。 widget 內建守衛會忽略第二套、不會壞掉,但那份下載與解析是白花的。
src="https://issues.example.com/w.js"src="https://issues.example.com/w.js?v=20260911" widget 要有使用者身分才能運作。登入頁上還沒有登入者, identify() 無從呼叫,訪客看到浮動按鈕、點下去卻只會得到 「尚未提供使用者身分」—— 對還沒登入的人來說是純粹的干擾。
所以不要把這段 <script> 放在全站共用的 <head>, 改放在登入後才會套用的版型(layout)裡,或在框架的 plugin 裡判斷登入狀態後再動態插入。 同一個道理也適用於註冊頁、忘記密碼頁、錯誤頁這類還沒有身分的畫面。
這樣做還順便解決了時序問題:widget 一載入,登入者資訊就已經在手上, identify() 可以立刻呼叫。
做不到的話有另一條路。有些框架把腳本延到登入後注入反而會壞掉 —— widget 載入完成就會自己建 controller、發出第一批 API 請求,而那些請求要等 identify();延後注入會讓兩者時序反過來,widget 卡在 「尚未提供使用者身分」且不會自己重試。這種情況請照原樣在早期注入, 改用下面的 data-armed 控制誰看得到。
正式機通常不能把 <script> 宣告拿掉(那等於每次調整授權都要改部署內容), 但也不該讓每個訪客都看到浮動按鈕、都被攔截 console 與網路請求。 做法是留著宣告,只切換一個屬性。
三層防線,各自只管一件事:
data-armed(script 屬性)—— 只管隱私:w.js 要不要攔截。 它刻意不需要可信:偽造的人只是讓自己的瀏覽器錄自己的資料, 送出時會被擋下,而且會在第一次向伺服器問話後就停止錄製並銷毀已錄內容。 role 寫在 JWT 裡,前端改不了。 reportGate / reportRoles(後台專案設定)—— 管「你能不能回報」。 真正的門在這裡。 原則是前端 fail-open、後端 fail-closed:屬性漏掉頂多多錄一點自己的資料, 真正的授權永遠由伺服器決定。
上線時依序做這六件事:
authMode 改成 signed。 (reportGate=roles 強制要搭配 signed —— trusted 模式的 role 是前端 identify() 自己填的,拿它當授權依據等於沒有授權。) role 由你自己的 session 決定。 data-token-url 指向那支 endpoint。data-armed,值由「這個 session 的人該不該回報」決定。 reportGate 設成 roles,並在 reportRoles 填上可回報的 role。data-button="false" —— 畫面上完全看不到痕跡, 授權者用熱鍵(預設 ctrl+shift+b)叫出來。 第 1 與第 2、3 步要一起上。只切 signed 而還沒有 token endpoint 的話, widget 會拿不到 token 而整個失效(/api/auth/session 對 signed 專案一律回 403)。
<script type="module" src="https://issues.example.com/w.js" data-project="your-project" data-armed="true" data-token-url="/api/issue-token" data-button="false"></script>
data-armed 必須在 w.js 執行之前就定案。 SSR 直接輸出屬性最單純;如果你是在 plugin 裡動態建 script 標籤, 注入的當下設 script.dataset.armed = String(canReport) 一樣可以 —— 不行的是「先載入、之後再用 JS 告知資格」。 那會讓 collector 必須等伺服器回答才啟動,首屏的 console 錯誤與第一批 API 請求 全部錄不到,而「一進頁面就壞掉」恰好是最需要診斷的情況。
屬性缺少時視為 armed(與 data-button 同一個寫法), 所以現有整合不加這個屬性行為完全不變。代價是正式機忘了輸出就等於全員錄製 —— 但失效範圍有限:伺服器端的 reportGate 會讓 widget 在第一次問話後立刻停下。
npx issue-system auth --server https://issues.example.com --project your-project --token <PAT> npx issue-system pull npx issue-system verify <issue-id> --summary "改了什麼" --url http://localhost:3000
PAT 在後台的 CLI Token 頁面建立。