Files
shared/skills/spec-gitea/SKILL.md
T

134 lines
9.4 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
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 <name> --repo <owner>/<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://<host>/<owner>/<repo>.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/<owner>/<repo>` 之類的輕量請求實際驗證可用(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://<host>/api/v1`(repo 層:`https://<host>/api/v1/repos/<owner>/<repo>`)。
- 標頭:`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/<owner>/<repo>/wiki/pages`(分頁完整
讀取,見「API 呼叫慣例」)列出全部頁面,比對 `title` 欄位取得真正的 `sub_url`,
再用該 `sub_url` 組出 `/wiki/page/<sub_url>`。查不到才退回把 title 本身當作
sub_url 使用(新頁尚未建立時的合理退路)。
3. **建立新頁不必自行轉義**:`POST /wiki/new` 直接帶完整、人類可讀的 title 字串即可
(含空白與符號皆可),轉義是 Gitea 伺服器端完成的,呼叫端不用預先處理。
4. **wiki 讀回驗證以正文為準**:寫入後一律再呼叫 `GET /repos/<owner>/<repo>/wiki/page/<sub_url>` 讀回內容驗證,不可只確認建立成功;若回應含 `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`)。