--- name: spec-gitea description: JSC plugins 共用「Gitea 工具規範」:tea 或 Gitea REST API + GITEA_TOKEN 的工具選擇與可用性檢查、token 機密保護(不 echo、遮蔽、不落地)、不依賴 jq、API 呼叫慣例(分頁完整讀取、UTF-8 JSON body、實際換行)、gitea 主機決定順序。當其他 skill 內文引用 spec-gitea 或 /jsc-shared:spec-gitea、或執行任何需存取 Gitea 的 JSC skill 時載入此 skill。單獨被使用者呼叫時,直接說明本規範內容。 --- # spec-gitea — 共用 Gitea 工具規範 所有 JSC skills 存取 Gitea(議題、留言、PR、repo 清單)時,一律遵守以下規範。 ## 工具選擇(`tea` 或 `api`) 使用者已明確選定工具(`--tool` 或對話中已選)時**跳過詢問**,只做該工具的可用性驗證。否則: 1. 檢查 `tea` 是否存在:`command -v tea`;存在則執行 `tea login list` 記錄可用 login 與 host(失敗記錄原因,不中止)。 2. 檢查 `GITEA_TOKEN` 是否已設定,**只輸出「已設定/未設定」**,不得輸出 token 內容: ```bash [ -n "${GITEA_TOKEN}" ] && echo "GITEA_TOKEN 已設定" || echo "GITEA_TOKEN 未設定" ``` 3. 以表格呈現檢查結果後詢問使用者要用哪一種: | 選項 | 可選條件 | 後續使用方式 | | --- | --- | --- | | `tea` | `tea` 可執行且目標 host 有對應 login | 命令一律帶 `--login --repo /` | | `api` | `GITEA_TOKEN` 已設定 | Gitea REST API + `curl`,標頭 `Authorization: token $GITEA_TOKEN` | 4. 兩種方式都不可用 → 回報缺少 `tea login` 或 `GITEA_TOKEN` 並停止;**不要請使用者把 token 貼進對話**。 5. 多筆來源分屬不同 host:選 `tea` 須確認每個 host 都有對應 login;選 `api` 須同一個 token 可存取全部,否則回報權限不足並停止。 ## Token 機密保護(極重要) - token 一律**從環境變數讀取**(`$GITEA_TOKEN`),**絕不**寫死在 skill、commit、PR、議題、log 或任何輸出。 - **不可** echo 含 token 的指令或 URL;顯示給使用者的指令/錯誤訊息一律**遮蔽 token**(以 `***` 取代);檢查時只輸出「已設定/未設定」。 - 帶 token 的 URL(clone/push)用變數帶入、**不可印出**;token 用完即棄,不寫進 git remote 設定、不落地。clone 完成後把 origin 還原成不含 token 的乾淨 URL: ```bash git -C "${dest}" remote set-url origin "https:////.git" ``` - 流程若可能使對話內文殘留 token(push/API 呼叫),完成後提醒使用者清除對話(Claude Code:`/clear`),並先確認輸出與 log 無明文 token。 ### token 解析優先序 完整優先序(依序取第一個驗證成功者): 1. 呼叫端工具自訂的專用環境變數(若有,例如 persona 的 `PERSONA_GITEA_TOKEN`)。 2. `$GITEA_TOKEN`。 3. tea 設定檔(`~/.config/tea/config.yml` 或 `~/.tea/config.yml`)中對應 host 的 token。 4. `~/.git-credentials`(`credential.helper=store`)中對應 host 的密碼。 每個候選都必須用 `GET /repos//` 之類的輕量請求實際驗證可用(HTTP 200 才採用),驗證失敗就換下一個候選;全部候選都失敗,才回報錯誤並停止。 各工具可以只實作其中適用的子集(例如純粹用 git `http.extraHeader` 認證的工具可以不需要 tea 設定檔/git-credentials 這兩層 fallback),但已實作的部分不可打亂這個順序,且專用變數只能插在最前面,不能插在中間。 參考實作:`doc/scripts/worklog/wiki_api.py` 的 `resolve_token()` 依「`GITEA_TOKEN` → tea 設定檔 → git-credentials」順序逐一以 API 驗證;`persona/scripts/persona-gitea.mjs` 的 `giteaEnv()` 則示範了「專用變數插在最前面」——優先取 `PERSONA_GITEA_TOKEN`,其次才是 `GITEA_TOKEN`。 ### 機密遮蔽實作 任何會把 Gitea API 回應內容、git stderr/stdout、或組出的錯誤訊息顯示給使用者/寫進 log 的地方,一律先套用下列遮蔽規則。 | 規則 | 用途 | | --- | --- | | `[A-Za-z0-9_-]*:[A-Za-z0-9_-]{16,}@` → 取代為 `***@` | URL 內嵌憑證 user:token@ | | `\b[0-9a-f]{40}\b` → `***` | Gitea 40 字元 token | | `\bgh[pousr]_[A-Za-z0-9_]{16,}\b` → `***` | GitHub token | | `\bsk-[A-Za-z0-9\-_]{16,}\b` → `***` | API key | | `(?i)\b(token\|password\|passwd\|pwd\|secret\|api[_-]?key)\b\s*[:=]\s*\S+` → `\1=***` | key=value 形式機密 | | `(?i)Authorization:\s*(token\|bearer)\s+\S+` → `Authorization: \1 ***` | Authorization 標頭 | | `[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}` → `***` | Email | | `\b09\d{2}[-\s]?\d{3}[-\s]?\d{3}\b` → `***` | 台灣手機 | | `\b[A-Z][12]\d{8}\b` → `***` | 台灣身分證字號 | (來源:`doc/scripts/worklog/transcript.py` 的 `REDACT_PATTERNS`,逐條照抄。) 這 9 條規則的**唯一權威資料來源**為 `shared/scripts/lib/redact-patterns.json`(Python/JavaScript/Bash 三語言共用),上表僅供閱讀對照,異動一律先改該 JSON 檔再回頭同步本表,避免文件與 JSON 各自漂移。 任何存取 Gitea 的 JSC 腳本/skill,其錯誤輸出路徑都必須套用這 9 條規則(或功能對等實作),而不只是遮蔽 token 字面值。 ## 不依賴 `jq`(環境未必安裝) - 解析 JSON 用 `tea` 的結構化輸出(`--output csv`/`--fields`),或把原始 JSON 直接交給助理/subagent 解析,**不要 pipe 到 `jq`**。 ## API 呼叫慣例 - API base:`https:///api/v1`(repo 層:`https:///api/v1/repos//`)。 - 標頭:`Authorization: token $GITEA_TOKEN`。 - **分頁必須完整讀取**:持續累加 `page` 直到回傳筆數 `< limit`(或回空陣列)為止,不可只取第一頁。 - 寫入(議題描述/留言/PR body)以 **UTF-8 JSON body** 帶入;wiki 寫入則使用 `content_base64`,值必須是 wiki Markdown 內容先以 UTF-8 編碼再 base64 編碼的結果,不能用 `content`、不能用 `@file` 形式。換行必須是**實際換行**,不可讓內容顯示字面 `\n`(編碼細節見 `/jsc-shared:spec-output`)。 - API 失敗(401/403/網路錯誤)→ 回報錯誤(**遮蔽 token**)並停止;401/403 多半是 token 失效或權限不足。 - 版本相依端點(project/column/dependency 等)先以 GET 探測(404/501 視為不支援),**不得對未確認存在的端點做寫入**。 ### Wiki 頁名轉義規則 Gitea wiki 的「title」與實際存放用的「sub_url/檔名」不是同一個字串,Gitea 會對 title 做內部轉義;這個轉義演算法未完全公開,**不可自行用字串取代規則反推**。已知規律與收斂 後的共通作法如下: 1. 已觀測到的規律:title 中的**空白**在 sub_url/檔名中會對應成 `-`;但字面上的 `-` 本身另有轉義形式(例如 title `Worklog-2026-07-W4`(本身含 `-`、不含空白)對應到的 sub_url 觀測值為 `Worklog-2026-07-W4.-`)。也就是「title→sub_url」只有空白這一條 規則可靠,`-`/`.-` 等其他符號的精確對應**不可靠、不要照抄硬編碼**。 2. **查表優先於猜測轉義規則**:任何需要「用 title 找到實際頁面路徑」的操作(讀取、 更新、刪除既有頁面),一律先呼叫 `GET /repos///wiki/pages`(分頁完整 讀取,見「API 呼叫慣例」)列出全部頁面,比對 `title` 欄位取得真正的 `sub_url`, 再用該 `sub_url` 組出 `/wiki/page/`。查不到才退回把 title 本身當作 sub_url 使用(新頁尚未建立時的合理退路)。 3. **建立新頁不必自行轉義**:`POST /wiki/new` 直接帶完整、人類可讀的 title 字串即可 (含空白與符號皆可),轉義是 Gitea 伺服器端完成的,呼叫端不用預先處理。 4. **wiki 讀回驗證以正文為準**:寫入後一律再呼叫 `GET /repos///wiki/page/` 讀回內容驗證,不可只確認建立成功;若回應含 `content_base64`,以 base64 解碼後的 Markdown 做比對;若回應格式不同,依官方 API 文件與實際回應欄位抽取正文後再比對。 5. 若寫入策略是透過 **git clone/push** 直接操作 wiki repo 產生 `.md` 檔(而非呼叫 REST API),則不必還原 Gitea 的內部轉義:改由呼叫端自建一份「工作路徑 → 儲存 檔名」manifest(例如 `_paths.json`),寫入時查 manifest 決定檔名、讀回時查 manifest 還原原始路徑,全程不依賴、也不猜測 Gitea 從檔名反推 title 的規則。 6. 兩種寫入策略(REST API 查表 vs git clone/push + 自建 manifest)各有各的理由, 不合併;但上述查表優先、不猜測轉義的原則對兩者都適用。 參考實作:`doc/scripts/worklog/wiki_api.py` 的 `resolve_sub_url()` 走 REST API 查表 (規則 2);`persona/scripts/persona-gitea.mjs` 的 `wikiName()` 走 git clone/push + `_paths.json` manifest(規則 4)。 ## gitea 主機決定順序 本節為唯一權威順序,其他 skill 若需描述 host 決定流程,只能引用本節,不可另行複述或改動順序。 依序決定(取第一個成功者): 1. 參數 `--host <主機>`。 2. 環境變數 `$GITEA_HOST`(若有)。 3. 目前工作目錄是 git repo 且 `git remote get-url origin` 指向某 gitea 主機 → 取該 host。 4. 以上皆無 → **詢問使用者**,不臆測。 主機僅取 host 部分(如 `gitea.jsc.idv.tw`)。