diff --git a/.claude-plugin/plugin.json b/.claude-plugin/plugin.json index 8bedb93..e9b14f1 100644 --- a/.claude-plugin/plugin.json +++ b/.claude-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "jsc-gitea", - "version": "0.2.2", + "version": "0.2.3", "description": "Gitea API 工具、Wiki 讀寫、議題轉換、HTML 匯出與存取庫批次同步", "skills": "./skills", "author": { diff --git a/.codex-plugin/plugin.json b/.codex-plugin/plugin.json index f799eea..87ea348 100644 --- a/.codex-plugin/plugin.json +++ b/.codex-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "jsc-gitea", - "version": "0.2.2", + "version": "0.2.3", "description": "Gitea API 工具、Wiki 讀寫、議題轉換、HTML 匯出與存取庫批次同步", "skills": "./skills", "jsc": { diff --git a/README.md b/README.md index 258fd24..b1640ac 100644 --- a/README.md +++ b/README.md @@ -34,7 +34,8 @@ gitea.sh wiki-list / gitea.sh wiki-get / # 不存在 exit 4 gitea.sh wiki-put / # 自動判斷新建或更新;會先要求確認 gitea.sh wiki-delete / # 刪除 wiki 頁;與 wiki-put 走同一道確認,頁不存在 exit 4 -gitea.sh wiki-url / # 印出 wiki 頁絕對網址(取自 API 的 html_url);跨存取庫連結用 +gitea.sh wiki-url / # 印出 wiki 頁絕對網址(取自 API 的 html_url) + # 連結一律寫成 [文字](絕對網址),網址就取自這裡 gitea.sh pr-create / <body-file> # 會先要求確認 gitea.sh pr-status <owner>/<repo> <pr-index> # 印出 {state} {merged} {mergeable} gitea.sh pr-get <owner>/<repo> <pr-index> # 印出 PR 的標題、base 分支與描述,供呼叫端比對有沒有差 @@ -80,6 +81,13 @@ migrate-wiki.sh [--apply] [--key <候選鍵>]... # 把舊頁搬到新規則 repo-sync.sh <owner>/<repo> [target-dir] # 同步單一存取庫;印出 cloned、updated、dirty {分支} 或 failed {原因} # 基準分支的優先序只在這支腳本裡;dirty 會把解析好的分支帶出來當 PR 的 base check-wiki-rules.sh # 驗證 wiki repo 解析、hash-id 與頁名樣式規則 +link-check.sh <網址>... # 連結寫進文件之前先驗證連得到;也吃標準輸入,一行一個 + # 每個網址一行「{OK|DEAD|SKIP}<TAB>{網址}<TAB>{說明}」 + # wiki 頁與議題轉成 API 查,其他 Gitea 網址帶金鑰 HEAD,外部網址不帶金鑰 HEAD + # Gitea 一律走 API:私有存取庫的網頁網址對未登入請求一律回 404 + # 結束碼 0=全部連得到、1=有連不到、2=沒給網址、3=有 Gitea 網址但 GITEA_HOST 未設定 + # 7=Gitea 認證失敗(401/403) + # 7 一定要與 1 分開:金鑰失效與「頁不存在」難分辨,混用會把還在的頁整批判成死連結 pr-watch.sh <owner>/<repo> <pr-index> [state-file] # 盯著一支 PR,直到它合併或關閉,不自動逾時退場 # 退出碼 0 = 已合併或關閉、10 = 有新留言要接手、2 = 參數錯誤、3 = 查不到該 PR @@ -107,7 +115,7 @@ html-render.sh --markdown <檔案> --title <標題> --out <輸出檔> [--layout] ## 參考資料 -- `references/wiki-links.md`:寫 wiki 頁才需要的連結規則。目錄頁之間、同型別的內容頁之間用 `[[顯示文字|頁名]]`(顯示文字在左);目錄頁與內容頁之間以及跨型別,一律用 `wiki-url` 給的絕對網址。 +- `references/wiki-links.md`:連結規則。規則 A:一律寫成 `[文字](絕對網址)`,網址取自 `wiki-url`,只有這一種寫法。規則 B:連結寫進文件之前先過 `link-check.sh`,結束碼 0 才寫入。 ## HTML 範本 diff --git a/plugin.json b/plugin.json index b0ee06b..f0b6141 100644 --- a/plugin.json +++ b/plugin.json @@ -1,6 +1,6 @@ { "name": "jsc-gitea", - "version": "0.2.2", + "version": "0.2.3", "description": "Gitea API 工具、Wiki 讀寫、議題轉換、HTML 匯出與存取庫批次同步", "skills": "./skills/", "jsc": { diff --git a/references/behaviors.md b/references/behaviors.md index 2244451..482d0c7 100644 --- a/references/behaviors.md +++ b/references/behaviors.md @@ -7,7 +7,7 @@ | 項目 | 內容 | | --- | --- | | 觸發時機 | 使用者給一條 Gitea wiki 頁連結或議題連結,要把那一頁變成一份離得開 Gitea 的 HTML 檔。請求裡沒有連結就直接停手。不猜存放庫、不猜頁名、不猜議題編號,也不回頭去讀工作目錄的 remote。要改某一類頁面用哪一組範本,走 jsc-gitea:html-style。 | -| 關鍵步驟 | 用 tools/gitea-link.sh parse 解析連結、確認 GITEA_HOST 有值、同一批平行跑三條線、開 sub agent 整理 markdown、用 tools/html-render.sh 產出檔案。三條線分別是:A 取內容,wiki 頁走 jsc-gitea:wiki 的 wiki-get 與 wiki-url,議題走 tools/issue.sh show 一次取回標題、標籤、內文;B 取範本,tools/html-style.sh key 算出 kind key,再用 get 讀出版型、風格、來源三欄;C 問輸出路徑,預設提 ./.jsc/html/{名稱}.html。sub agent 只做兩件事:把 wiki 內部連結換成絕對網址、拿掉個資。 | +| 關鍵步驟 | 用 tools/gitea-link.sh parse 解析連結、確認 GITEA_HOST 有值、同一批平行跑三條線、開 sub agent 整理 markdown、用 tools/html-render.sh 產出檔案。三條線分別是:A 取內容,wiki 頁走 jsc-gitea:wiki 的 wiki-get 與 wiki-url,議題走 tools/issue.sh show 一次取回標題、標籤、內文;B 取範本,tools/html-style.sh key 算出 kind key,再用 get 讀出版型、風格、來源三欄;C 問輸出路徑,預設提 ./.jsc/html/{名稱}.html。sub agent 只做兩件事:把舊頁殘留的 wiki 內部連結換成 [文字](絕對網址)、拿掉個資。 | | 外部呼叫 | tools/gitea-link.sh、tools/html-style.sh、tools/html-render.sh、tools/issue.sh、jsc-gitea:wiki、jsc-ask:ask、Gitea 的 wiki API、Gitea 的議題 API、Gitea 的 markdown 渲染 API。 | | 完成條件 | 檔案已經寫出來。回報裡有檔案路徑、版型名稱、風格名稱,以及這一組是從哪裡來的。來源是 default 或 builtin 時,回報要多一行告訴使用者可以用 jsc-gitea:html-style 設定。渲染失敗就不留半成品檔,直接停手回報。 | | 可驗證跡象 | 使用者確認過的輸出路徑多一個 HTML 檔,預設落在 ./.jsc/html/ 底下。這個檔把 CSS 與 JS 內嵌,不外連任何資源,開起來就是完整的一頁。不寫 wiki 頁、不建議題、不開 PR,來源頁面本身也不動。 | @@ -37,17 +37,17 @@ | 項目 | 內容 | | --- | --- | | 觸發時機 | 技能組裡任何一次 wiki 頁的讀、寫、刪或搬移,都經過這一支。呼叫端有 jsc-ask、jsc-sdlc、jsc-log、jsc-hooks 的 ERROR 頁、jsc-cli 的 CHECK 頁、jsc-meta 的 SKILLSET 與 TOOLING 頁、jsc-assist 的 MONITOR 頁。存放庫裡的程式碼檔案不歸這一支管。 | -| 關鍵步驟 | 確認 GITEA_HOST 有值、用 tools/gitea.sh wiki-repo {TYPE} 解出 {owner}/{repo}、用 tools/hash-id 算頁名要用的 HASH、用 tools/page-name.sh check 驗過頁名、再依動作跑 wiki-list、wiki-get、wiki-put、wiki-delete 或 wiki-url,每一次呼叫都照結束碼表分流。所有 *_CONTENTS 頁一律走型別 CONTENTS,內容頁走自己的型別;存放庫的解法是先讀 JSC_WIKI_REPO_{TYPE}、再讀 JSC_WIKI_REPO、兩個都沒有才問使用者,而且不借用別的頁型的存放庫。目錄頁的整列 upsert 交給 tools/wiki-contents.sh,寫入前一定先 wiki-get 讀回舊內容,只有結束碼 4 才准用範本建新頁;舊頁搬到新規則走 tools/migrate-wiki.sh,不帶 --apply 只印對照表,寫每一個目的地之前也一樣先 wiki-get 讀一次。 | -| 外部呼叫 | tools/gitea.sh 的 wiki-repo、wiki-list、wiki-get、wiki-put、wiki-delete、wiki-url、tools/hash-id、tools/page-name.sh、tools/wiki-contents.sh、tools/migrate-wiki.sh、tools/write-confirm.sh、jsc-ask:ask、Gitea 的 wiki API。 | -| 完成條件 | 讀取動作拿到頁面內容,或拿到一個講得清楚的結束碼。寫入動作通過人工確認、wiki-put 回結束碼 0,而且送出去的是舊內容加上這次的異動,不是整頁覆蓋。結束碼 7 與 8 一律中止整個動作,不建頁、不寫入、不用原參數重試。搬移動作要嘛全部搬完回 0,要嘛把失敗頁、孤兒頁、目的地已有內容的頁、指向被搬頁卻沒被搬的引用方逐條列出來,這四種一律交給人判斷。 | -| 可驗證跡象 | 目標 wiki 存放庫多一頁或改一頁。內容頁的頁名是 {型別}_{40 碼大寫十六進位},目錄頁是 {型別}_CONTENTS 且落在 JSC_WIKI_REPO_CONTENTS 指的那個存放庫。頁面內容是 UTF-8 繁體中文,以 mermaid 圖與 markdown 表格為主,散文每節最多三句;目錄頁與內容頁互指的連結是絕對網址,不是 [[...]]。寫入與刪除前 tools/write-confirm.sh 會各留下一次人工確認。只做讀取的呼叫無寫入跡象,只有回報內容。 | +| 關鍵步驟 | 確認 GITEA_HOST 有值、用 tools/gitea.sh wiki-repo {TYPE} 解出 {owner}/{repo}、用 tools/hash-id 算頁名要用的 HASH、用 tools/page-name.sh check 驗過頁名、再依動作跑 wiki-list、wiki-get、wiki-put、wiki-delete 或 wiki-url,每一次呼叫都照結束碼表分流。所有 *_CONTENTS 頁一律走型別 CONTENTS,內容頁走自己的型別;存放庫的解法是先讀 JSC_WIKI_REPO_{TYPE}、再讀 JSC_WIKI_REPO、兩個都沒有才問使用者,而且不借用別的頁型的存放庫。頁面裡的連結一律寫成 [文字](絕對網址),網址取自 wiki-url,不自行組路徑;wiki-put 之前先把這一頁要放的每一條連結交給 tools/link-check.sh,結束碼 0 才寫,1 就不寫並回報 DEAD 那幾筆,7 停下來回報金鑰問題。目錄頁的整列 upsert 交給 tools/wiki-contents.sh,寫入前一定先 wiki-get 讀回舊內容,只有結束碼 4 才准用範本建新頁;舊頁搬到新規則走 tools/migrate-wiki.sh,不帶 --apply 只印對照表,寫每一個目的地之前也一樣先 wiki-get 讀一次。 | +| 外部呼叫 | tools/gitea.sh 的 wiki-repo、wiki-list、wiki-get、wiki-put、wiki-delete、wiki-url、tools/hash-id、tools/page-name.sh、tools/link-check.sh、tools/wiki-contents.sh、tools/migrate-wiki.sh、tools/write-confirm.sh、jsc-ask:ask、Gitea 的 wiki API 與議題 API。 | +| 完成條件 | 讀取動作拿到頁面內容,或拿到一個講得清楚的結束碼。寫入動作先讓 tools/link-check.sh 回結束碼 0,再通過人工確認、wiki-put 回結束碼 0,而且送出去的是舊內容加上這次的異動,不是整頁覆蓋。連結檢查回 1 就不寫入,回 7 連同整個動作一起中止。wiki-put 的結束碼 7 與 8 一律中止整個動作,不建頁、不寫入、不用原參數重試。搬移動作要嘛全部搬完回 0,要嘛把失敗頁、孤兒頁、目的地已有內容的頁、指向被搬頁卻沒被搬的引用方逐條列出來,這四種一律交給人判斷。 | +| 可驗證跡象 | 目標 wiki 存放庫多一頁或改一頁。內容頁的頁名是 {型別}_{40 碼大寫十六進位},目錄頁是 {型別}_CONTENTS 且落在 JSC_WIKI_REPO_CONTENTS 指的那個存放庫。頁面內容是 UTF-8 繁體中文,以 mermaid 圖與 markdown 表格為主,散文每節最多三句;頁面裡每一條連結都是 [文字](絕對網址),沒有 wiki 內部連結語法,而且每一條在寫入前都被 tools/link-check.sh 判成 OK。寫入與刪除前 tools/write-confirm.sh 會各留下一次人工確認。只做讀取的呼叫無寫入跡象,只有回報內容。 | ## wiki-to-issue | 項目 | 內容 | | --- | --- | | 觸發時機 | 一頁 wiki 要變成一條追得動的議題,而且使用者已經給了那一頁的連結。請求裡沒有 wiki 連結就直接停手,不猜存放庫、不猜頁名。連結指向的是議題也停手。從零起草的議題不走這一支,已經存在的議題要同步也不走這一支。 | -| 關鍵步驟 | 同一批檢查連結與主機、平行跑三條線、依決策樹挑標籤、處理看板、最後建議題。連結用 tools/gitea-link.sh parse 解析,要印出 kind=wiki 才算過;GITEA_HOST 要有值。三條線是:A 讀頁面並開 sub agent 起草標題與內文;B 用 tools/issue.sh labels 取這個存放庫既有的標籤清單;C 用 tools/issue.sh projects 取看板清單。內文開頭放一行「來源:{絕對網址}」,wiki 內部連結換成絕對網址,個資拿掉。標籤只能從既有清單裡挑,確認後用 label-ids 換成 id,缺的標籤不補建。看板 API 不存在時,把腳本印出的看板網址交給使用者自己拖。 | +| 關鍵步驟 | 同一批檢查連結與主機、平行跑三條線、依決策樹挑標籤、處理看板、最後建議題。連結用 tools/gitea-link.sh parse 解析,要印出 kind=wiki 才算過;GITEA_HOST 要有值。三條線是:A 讀頁面並開 sub agent 起草標題與內文;B 用 tools/issue.sh labels 取這個存放庫既有的標籤清單;C 用 tools/issue.sh projects 取看板清單。內文開頭放一行「來源:{絕對網址}」,連結一律寫成 [文字](絕對網址),舊頁殘留的 wiki 內部連結一併換掉,個資拿掉。標籤只能從既有清單裡挑,確認後用 label-ids 換成 id,缺的標籤不補建。看板 API 不存在時,把腳本印出的看板網址交給使用者自己拖。 | | 外部呼叫 | tools/gitea-link.sh、tools/issue.sh 的 labels、projects、label-ids、create、jsc-gitea:wiki、jsc-ask:ask、tools/write-confirm.sh、Gitea 的 wiki API 與議題 API。 | | 完成條件 | 議題已經建立,回報裡有議題網址、實際套上的標籤,以及看板狀態。建不成就明講沒有建成,並交出草稿檔的路徑,讓草稿不會白寫。標題、內文、標籤、看板都在呼叫 create 之前先跟使用者確認過。 | -| 可驗證跡象 | 目標存放庫的議題追蹤器多一條議題,create 會印出 index= 與 url= 兩行。議題內文第一行是「來源:」加上 wiki 頁的絕對網址,內文裡沒有殘留的 wiki 內部連結語法。標籤就是這次確認過的那一組。站台有看板 API 時,看板上多一張卡;沒有時,回報裡留一行待辦說明誰要去補。來源 wiki 頁本身不動。 | +| 可驗證跡象 | 目標存放庫的議題追蹤器多一條議題,create 會印出 index= 與 url= 兩行。議題內文第一行是「來源:」加上 wiki 頁的絕對網址,內文裡每一條連結都是 [文字](絕對網址),沒有殘留的 wiki 內部連結語法。標籤就是這次確認過的那一組。站台有看板 API 時,看板上多一張卡;沒有時,回報裡留一行待辦說明誰要去補。來源 wiki 頁本身不動。 | diff --git a/references/wiki-links.md b/references/wiki-links.md index 4bbbbfa..beb694e 100644 --- a/references/wiki-links.md +++ b/references/wiki-links.md @@ -1,27 +1,62 @@ -# Wiki 頁之間的連結 +# Wiki 頁與文件的連結 -寫 wiki 頁才需要這份規則。兩條規則任一條寫錯,連結會指向一個不存在的頁,畫面上看不出異常。 +寫 wiki 頁、議題或存放庫文件都適用這兩條規則。連結寫錯時畫面上看不出異常,所以規則寫在這裡,不靠當下判斷。 -## 方向:顯示文字在左,頁名在右 +## 規則 A:文字加連結一律寫成 `[文字](絕對網址)` -Gitea 採 GitHub/Gollum 慣例:`[[顯示文字|頁名]]`。方向與 MediaWiki 相反。Gitea 原始碼(`modules/markup/html_link.go`)寫得很清楚: +只有這一種寫法。`[[頁名]]` 與 `[[顯示文字|頁名]]` 全面取消,不再分「同存取庫」與「跨存取庫」兩種寫法。 -> MediaWiki uses [[link|text]], while GitHub uses [[text|link]] … we prefer GitHub syntax +| 情境 | 寫法 | +| --- | --- | +| 目錄頁指向內容頁(例:`PLAN_CONTENTS` → `PLAN_{HASH}`) | `[PLAN_{HASH}](https://…/wiki/PLAN_…)` | +| 內容頁指向目錄頁,或指向別的型別 | `[顯示文字](絕對網址)` | +| 目錄頁之間、同型別的內容頁之間 | `[顯示文字](絕對網址)` | +| 議題、PR、存放庫檔案 | `[顯示文字](絕對網址)` | -所以 `[[PLAN_CONTENTS|我的計畫]]` 會顯示成文字 `PLAN_CONTENTS`,連到一個叫「我的計畫」的頁——這是壞連結。要寫 `[[我的計畫|PLAN_CONTENTS]]`。 +網址一律取自 `tools/gitea.sh wiki-url {owner}/{repo} {page}`,它讀 API 回應的 `html_url`。不要自己組路徑:頁名有大小寫與編碼規則,手組的路徑看起來像對的,點下去是死的。 -顯示文字與頁名相同時,用不帶豎線的 `[[PLAN_CONTENTS]]`,這種寫法不會寫錯。 +## 為什麼取消 `[[...]]` -## 範圍:`[[...]]` 只在同一個 wiki 內解析 +三個理由,每一個都足以單獨取消它。 -`[[...]]` 與 markdown 相對連結都只在目前這個 wiki 內解析。跨存取庫沒有 wiki 連結語法。 +1. `[[...]]` 只在目前這個 wiki 內解析。跨存取庫沒有這種語法,連結會落在自己這個 wiki 的同名頁上。 +2. 寫錯不會報錯。畫面上是一段普通文字或一條死連結,巡不到也修不了。 +3. 目錄頁住在 CONTENTS 專用存取庫,內容頁住在自己型別的存取庫。兩種寫法並存,就得逐處判斷兩端各自解到哪一個存取庫。統一成一種,這個判斷整個消失。 -目錄頁全部住在 CONTENTS 專用存取庫,內容頁住在自己型別的存取庫。所以「同型別」不再等於「同存取庫」,判斷要看兩端各自解析到哪一個存取庫。 +另外還有一項成本:Gitea 的 markdown 渲染端點不吃 wiki 情境,`[[...]]` 會原樣輸出成字面括號。匯出成 HTML 或轉成議題時,每一條都得先換成絕對網址。一律寫絕對網址就沒有這道轉換。 -| 連結 | 同一個 wiki? | 寫法 | +搬移舊頁時 `tools/migrate-wiki.sh` 仍會改寫舊頁裡殘留的 `[[...]]`,那是清理既有內容,不是允許新寫。 + +## 規則 B:連結先驗證連得到,才寫進文件 + +寫入前,把每一個要放進頁面的連結交給 `tools/link-check.sh`。結束碼 0 才 `wiki-put`。 + +``` +link-check.sh {網址}... +printf '%s\n' {網址}... | link-check.sh +``` + +每個網址印一行,三欄以 TAB 分隔:`{OK|DEAD|SKIP}<TAB>{網址}<TAB>{說明}`。 + +驗證方式依網址種類分流: + +| 網址種類 | 驗證方式 | +| --- | --- | +| Gitea wiki 頁(`{GITEA_HOST}/{owner}/{repo}/wiki/{頁名}`) | 轉成 API `/repos/{owner}/{repo}/wiki/page/{頁名}` | +| Gitea 議題(`…/issues/{編號}`) | API `/repos/{owner}/{repo}/issues/{編號}` | +| 其他 Gitea 網址 | HTTP HEAD,帶金鑰 | +| 非 Gitea 的外部網址 | HTTP HEAD,不帶金鑰 | + +結束碼: + +| 碼 | 意義 | 呼叫端該做的事 | | --- | --- | --- | -| 目錄頁之間(例:`PLAN_CONTENTS` → `LOG_CONTENTS`) | 一定同一個:目錄頁都在 CONTENTS 存取庫 | `[[顯示文字\|頁名]]` 或 `[[頁名]]` | -| 同型別的內容頁之間(例:`PLAN_{HASH}` → 另一頁 `PLAN_{HASH}`) | 一定同一個:一個型別一個存取庫 | `[[顯示文字\|頁名]]` 或 `[[頁名]]` | -| 目錄頁與內容頁之間,以及跨型別(例:`PLAN_CONTENTS` → `PLAN_{HASH}`、`LOG_{HASH}` → `PLAN_{HASH}`) | **不保證**:兩端各自解析,可能不同 | `wiki-url` 給的絕對網址:`[顯示文字](https://…/wiki/PLAN_…)` | +| 0 | 全部連得到 | 才可以寫入 | +| 1 | 至少一筆連不到 | 不得寫入,回報 DEAD 那幾筆 | +| 2 | 用法錯誤:一個網址都沒給 | 補上參數再呼叫 | +| 3 | 清單裡有 Gitea 網址,但 `GITEA_HOST` 未設定 | 先設定再呼叫,不得跳過驗證 | +| 7 | Gitea 認證失敗(401、403) | 停下來回報金鑰問題 | -目錄頁解 `JSC_WIKI_REPO_CONTENTS`,內容頁解自己的 `JSC_WIKI_REPO_{TYPE}`,兩端各解各的。只設 `JSC_WIKI_REPO` 時兩端會落在同一個存取庫,多設一個型別變數就分開了。所以這兩種連結**一律**用絕對網址:兩邊剛好同存取庫也照樣正確,不必分兩種寫法,也不必跟著環境變數改寫法。網址一律取自 `tools/gitea.sh wiki-url`,不要自己組路徑。 +Gitea 連結一律走 API,不看網頁狀態碼。私有存取庫的網頁網址對未登入請求一律回 404,用網頁狀態碼判斷會把好連結判成壞連結,接著整批砍掉還在的頁。 + +第 7 碼與第 1 碼分開的理由一樣:金鑰失效時,Gitea 對私有存取庫的回應與「頁不存在」難以分辨。兩者混用,一次金鑰過期就把整批還在的頁判成死連結,接著這些頁會被當成壞連結刪掉或改寫。 diff --git a/skills/html-export/SKILL.md b/skills/html-export/SKILL.md index 4f07165..cb631ab 100644 --- a/skills/html-export/SKILL.md +++ b/skills/html-export/SKILL.md @@ -17,7 +17,7 @@ The link is the only input. The output is one file that opens anywhere, with no - **Track C — destination.** Ask per the `jsc-ask:ask` rules where the file goes, proposing `./.jsc/html/{page-or-issue}.html`. State the impact scope: a path inside a repository gets committed unless it is ignored. Completion condition: the markdown and the document title are in hand, exactly one kind key is chosen with the reason that produced it, the layout, style and source are reported, and the user has confirmed one output path. -4. **Prepare the markdown — this step MUST run as a sub agent.** Convert `[[display|page]]` wiki links to absolute URLs from `wiki-url`; the renderer does not resolve them, so they would ship as literal brackets. Strip personal data — an exported file travels further than the page it came from. Leave everything else exactly as written; this step never rewrites the content. Completion condition: no `[[...]]` remains, and the diff against the source is limited to link conversion and personal-data removal. +4. **Prepare the markdown — this step MUST run as a sub agent.** Links are written as `[text](absolute URL)`, so pages that follow the current rule need no conversion. An older page can still carry a wiki-internal link: turn it into an absolute URL from `wiki-url`, because the renderer does not resolve it and it would ship as literal brackets. Strip personal data — an exported file travels further than the page it came from. Leave everything else exactly as written; this step never rewrites the content. Completion condition: every link in the file is `[text](absolute URL)`, and the diff against the source is limited to link conversion and personal-data removal. 5. Render: `tools/html-render.sh --markdown {file} --title {title} --layout {layout} --style {style} --source-url {absolute URL} --out {path}`. Route every exit code: 0 → the path it printed is the finished file; 1 → Gitea's renderer or the write failed, so report it and stop, with no half-rendered file left behind; 2 → a usage error or a missing markdown file, so fix the arguments and call again; 4 → the layout or style template file is gone, so report which pair was asked for and send the user to `jsc-gitea:html-style` rather than editing the configuration by hand. Completion condition: the file exists, and the report names its path, the layout, the style and where that pair came from. ## Rules diff --git a/skills/wiki-to-issue/SKILL.md b/skills/wiki-to-issue/SKILL.md index 9b6f278..c66dc93 100644 --- a/skills/wiki-to-issue/SKILL.md +++ b/skills/wiki-to-issue/SKILL.md @@ -15,11 +15,11 @@ The wiki link is the only input. Everything else — repository, page name, host Completion condition: the parse printed `kind=wiki` with `repo`, `page` and `host` known, **and** `GITEA_HOST` holds a value — both, or the run has stopped with the reason named. 2. **Run these three tracks at the same time.** The label list and the board list depend on the repository only, not on the page, so they start in the same batch as the read rather than queueing behind the draft. - - **Track A — read and draft.** Read the page with `jsc-gitea:wiki` (`wiki-get {repo} {page}`) and take its absolute URL from `wiki-url` in the same pass. Route the exit codes by that skill's table: 4 means the page does not exist, 7 means the key is invalid or lacks permission, 8 is any other API failure — all three stop this skill with the page name in the report. **Drafting MUST run as a sub agent.** Title: the page's first heading, or the page name when it has none. Body: the page content in Traditional Chinese, opening with a 「來源:{絕對網址}」 line so the issue points back at the wiki. Convert `[[display|page]]` links to absolute URLs (`wiki-url`), because `[[...]]` resolves only inside a wiki. Drop personal data — an issue is read by more people than a wiki page. + - **Track A — read and draft.** Read the page with `jsc-gitea:wiki` (`wiki-get {repo} {page}`) and take its absolute URL from `wiki-url` in the same pass. Route the exit codes by that skill's table: 4 means the page does not exist, 7 means the key is invalid or lacks permission, 8 is any other API failure — all three stop this skill with the page name in the report. **Drafting MUST run as a sub agent.** Title: the page's first heading, or the page name when it has none. Body: the page content in Traditional Chinese, opening with a 「來源:{絕對網址}」 line so the issue points back at the wiki. Links are written as `[文字](絕對網址)`; a wiki-internal link left over from an older page becomes an absolute URL from `wiki-url`, because that form resolves only inside a wiki and an issue is not one. Drop personal data — an issue is read by more people than a wiki page. - **Track B — label list.** Run `tools/issue.sh labels {repo}`. Exit 1 means the label list could not be read: stop and report it, because the alternative is inventing labels. Exit 2 is a usage error — fix the arguments and call again. - **Track C — board list.** Run `tools/issue.sh projects {repo}`. Exit 3 means this Gitea has no board API — keep the board URL the script printed for step 4. Exit 1 means the call failed for another reason: report it and treat the board link as outstanding. Exit 2 is a usage error — fix the arguments and call again. - Completion condition: the title and body file exist with the source line and no `[[...]]` left in the body, the repository's label list is in hand or the run has stopped, and the board list is either in hand or recorded as unavailable. + Completion condition: the title and body file exist with the source line and every link in the body written as `[文字](絕對網址)`, the repository's label list is in hand or the run has stopped, and the board list is either in hand or recorded as unavailable. 3. **Labels come from what the repository already has.** Propose the fitting ones from track B's list with a reason each, and confirm per `jsc-ask:ask` rules — every option states its impact scope (a label drives filters and board rules, so a wrong one routes the work to the wrong queue). Turn the confirmed names into ids with `tools/issue.sh label-ids {repo} {names}`. Exit 4 means a name is not in the repository: go back to the list and pick again, never create the label to make the command pass. Exit 1 means the call failed — report it and stop. An empty label list, or nothing fitting: ask whether to create the issue with no label, and record that answer. **Never invent a label that the repository does not have.** Completion condition: the user has confirmed a label set — possibly empty — and its ids are resolved. 4. **Project board.** Track C returned a board list: let the user pick one per `jsc-ask:ask` rules, attach it, and report the failure verbatim if the attach call is refused. Track C exited 3: say plainly that this Gitea has no board API, and hand the user the board URL the script printed so they can drag the issue in themselves. Completion condition: the issue is either attached to a board, or the report states in one line that the board link is still outstanding and who has to do it. 5. Create the issue: `tools/issue.sh create {repo} {title} {body-file} [--labels {ids}]`. The script asks for confirmation before it writes, so expect that prompt and hand the user the title, the labels and the board it is about to apply. Exit 0: report the `index=` and `url=` it prints. Exit 1 means no issue was created — report that plainly, and hand back the path of the drafted body file so the draft is not lost. Exit 2 is a usage error, usually a body file that is not there — fix the arguments and call again. Completion condition: the issue URL is reported to the user together with the labels applied and the board status from step 4, or the report states that no issue was created and where the draft is. diff --git a/skills/wiki/SKILL.md b/skills/wiki/SKILL.md index 5175837..27f2d3d 100644 --- a/skills/wiki/SKILL.md +++ b/skills/wiki/SKILL.md @@ -1,6 +1,6 @@ --- name: wiki -description: Read or write a Gitea wiki page through tools/gitea.sh, tools/hash-id and tools/page-name.sh. Resolve the wiki repo per page type with JSC_WIKI_REPO_{TYPE} first, then JSC_WIKI_REPO, and ask only when neither is set - every *_CONTENTS page resolves through type CONTENTS and is updated with tools/wiki-contents.sh, and {HASH} is the full 40-char uppercase SHA-1 from tools/hash-id. Page content is chart-first - prefer mermaid diagrams and markdown tables over plain prose. Callers are jsc-ask, jsc-sdlc, jsc-log, jsc-hooks (ERROR), jsc-cli (CHECK), jsc-meta (SKILLSET, TOOLING) and jsc-assist (MONITOR). Use for any wiki page in the skill set; not for repo code files. +description: Read or write a Gitea wiki page through tools/gitea.sh, tools/hash-id and tools/page-name.sh. Resolve the wiki repo per page type with JSC_WIKI_REPO_{TYPE} first, then JSC_WIKI_REPO, and ask only when neither is set - every *_CONTENTS page resolves through type CONTENTS and is updated with tools/wiki-contents.sh, and {HASH} is the full 40-char uppercase SHA-1 from tools/hash-id. Page content is chart-first - prefer mermaid diagrams and markdown tables over plain prose, and every link is written as [text](absolute URL) that tools/link-check.sh passed before the write. Callers are jsc-ask, jsc-sdlc, jsc-log, jsc-hooks (ERROR), jsc-cli (CHECK), jsc-meta (SKILLSET, TOOLING) and jsc-assist (MONITOR). Use for any wiki page in the skill set; not for repo code files. --- # wiki — read and write Gitea wiki pages @@ -27,8 +27,9 @@ Different page types can live in different `{owner}/{repo}` repos, classified by | update a contents page | `tools/wiki-contents.sh upsert {TYPE} {key-col} {key} {row-file} [template-file]` — resolves the CONTENTS repo, replaces the row whose key column matches, appends when none does, writes the whole page back. `{key-col}` is the column's 1-based position number, not the column name | | check a page name | `tools/page-name.sh check {page}` — the single source of the page-name pattern; `tools/page-name.sh regex` prints it | | move pages to the current rules | `tools/migrate-wiki.sh [--apply] [--key {key}]...` — prints the mapping table and the orphan list; writes only with `--apply` | +| check links before a write | `tools/link-check.sh {url}...` — prints `{OK\|DEAD\|SKIP}<TAB>{url}<TAB>{reason}` per URL; exit 0 means every link is reachable | -Contents pages and content pages no longer share a repo. Link between them — and between any two different types — with the absolute URL from `wiki-url`. Keep `[[display|page]]` (display text on the LEFT) for two pages that resolve to the same repo: contents page to contents page, or same-type content page to same-type content page. Full rules and the direction trap: `references/wiki-links.md`. +Every link in a page is written as `[text](absolute URL)`, and the URL comes from `wiki-url` — one form for every target, inside this wiki or not. Every link goes through `tools/link-check.sh` before the page is written. Full rules, and why `[[...]]` was dropped: `references/wiki-links.md`. ## Exit codes @@ -40,7 +41,7 @@ Route every `tools/gitea.sh` call in this skill on its exit code. A code with no | 2 | usage error, or a page type outside the allowed list | fix the arguments, then call again; never repeat the same call unchanged | | 3 | `wiki-repo`: neither `JSC_WIKI_REPO_{TYPE}` nor `JSC_WIKI_REPO` is set | go to step 3 and ask | | 4 | HTTP 404: `wiki-get` and `wiki-url` found no such page, or `wiki-delete` found nothing to delete | for a read the caller expects to succeed, stop and report the page name; this is the **only** code that opens the create path of rule 4 — write the page from the template instead of appending. For `wiki-delete` it means the page is already gone: report it and move on, do not retry | -| 5 | `wiki-url`: the page exists but the API returned no `html_url` | stop and report it. Link inside the same wiki with `[[display\|page]]`; a cross-repo link has no absolute URL to point at, so do not fabricate one | +| 5 | `wiki-url`: the page exists but the API returned no `html_url` | stop and report it. There is no second link form to fall back on, and a hand-built path is not a substitute — never fabricate the URL | | 7 | HTTP 401 or 403 after the tea-token retry: the key is invalid or lacks permission | **stop the whole operation and report the key problem.** Never read this as an empty or missing page, and never take the create path of rule 4: writing a fresh page over one you could not read destroys the record that is still there | | 8 | any other API failure, HTTP status in the message | stop and report that status; call again only after the cause is fixed | @@ -63,6 +64,11 @@ Every code below gets its own branch. Nothing here is retried unchanged. | | 4 | the page is not there and no template was given | supply the template for that page type, then call again | | | 7 | the key is invalid or lacks permission | stop the whole operation and report the key problem; create no page | | | 8 | any other API failure | stop and report the status | +| `tools/link-check.sh` | 0 | every link is reachable | write the page; this is the only code that opens `wiki-put` | +| | 1 | at least one link is dead | do not write. Report the `DEAD` rows verbatim, fix or drop those links, then check again | +| | 2 | usage error: no URL was given | fix the arguments, then call again; never skip the check because the list looked empty | +| | 3 | the list holds a Gitea URL but `GITEA_HOST` is not set | set `GITEA_HOST` and call again. Never write the page unchecked | +| | 7 | HTTP 401 or 403: the key is invalid or lacks permission | stop the whole operation and report the key problem. Those pages are not dead — treating them as dead deletes or rewrites links to pages that are still there | | `tools/migrate-wiki.sh` | 0 | every page moved, or the preview found nothing to move | report the mapping table | | | 1 | at least one page failed to move | report the failure list; the old pages of the failed entries stay in place | | | 2 | usage error, including an unconfigured CONTENTS repo | fix the arguments or set `JSC_WIKI_REPO_CONTENTS`, then call again | @@ -81,4 +87,6 @@ Every code below gets its own branch. Nothing here is retried unchanged. 5. Write all wiki content in UTF-8 Traditional Chinese, per the STE100 output rule. 6. Prefer visual forms for page content: use mermaid diagrams (flowchart, sequence, gantt, pie) and markdown tables wherever the information allows. Prose is capped at 3 sentences per section, and a sentence stays only when neither a mermaid diagram nor a markdown table can carry the same information. 7. `tools/gitea.sh` retries once with the tea CLI login token when `GITEA_TOKEN` is missing or the response is 401/403. Report a failure only after that retry also fails. -8. `wiki-delete` removes a page for good. Call it only from `tools/migrate-wiki.sh --apply`, or when the user has asked for that exact page to go. In a migration the delete comes last: write the new page, read it back, then delete the old one. +8. **Rule A — every link is written as `[text](absolute URL)`.** That is the only form. `[[page]]` and `[[display|page]]` are gone, and there is no longer a same-repo case that keeps them. The URL always comes from `tools/gitea.sh wiki-url {owner}/{repo} {page}`, never from a path built by hand. Why: `[[...]]` resolves only inside the current wiki, so a cross-repo link silently lands on a same-named page in this one — and it fails as plain text or a dead link, with nothing to catch it. Contents pages and content pages already live in different repos, so keeping two forms would mean judging, link by link, which repo each end resolves to. +9. **Rule B — check every link before the write.** Run `tools/link-check.sh {url}...` over every link that is going into the page. Exit 0 is the only code that opens `wiki-put`. Exit 1 means at least one link is dead: write nothing, and hand the caller the `DEAD` rows. Exit 7 means the key failed, not that the pages are gone — stop and report the key problem. Checking after the write is not the same thing: the dead link is already published, and the next reader follows it. +10. `wiki-delete` removes a page for good. Call it only from `tools/migrate-wiki.sh --apply`, or when the user has asked for that exact page to go. In a migration the delete comes last: write the new page, read it back, then delete the old one. diff --git a/tools/gitea.sh b/tools/gitea.sh index 32251bc..b96ff90 100755 --- a/tools/gitea.sh +++ b/tools/gitea.sh @@ -13,7 +13,8 @@ # gitea.sh wiki-get <owner>/<repo> <page> # 印出 wiki 頁 markdown;不存在時 exit 4 # gitea.sh wiki-put <owner>/<repo> <page> <file> # 建立或更新 wiki 頁(內容取自檔案) # gitea.sh wiki-delete <owner>/<repo> <page> # 刪除 wiki 頁;與 wiki-put 走同一道人工確認 -# gitea.sh wiki-url <owner>/<repo> <page> # 印出 wiki 頁絕對網址(取自 API 的 html_url);跨存取庫連結用 +# gitea.sh wiki-url <owner>/<repo> <page> # 印出 wiki 頁絕對網址(取自 API 的 html_url) +# 連結一律寫成 [文字](絕對網址),網址就取自這裡,不自行組路徑 # gitea.sh pr-create <owner>/<repo> <head> <base> <title> <body-file> # 建立 PR,印出 PR URL # gitea.sh pr-status <owner>/<repo> <pr-index> # 印出「{state} {merged} {mergeable}」 # gitea.sh pr-get <owner>/<repo> <pr-index> # 印出 PR 的標題、base 分支與描述,供比對用 @@ -243,8 +244,8 @@ sys.stdout.write(base64.b64decode(d.get("content_base64","")).decode("utf-8")) ' ;; wiki-url) # 印出 wiki 頁的絕對網址,取自 API 回應的 html_url,不自行組路徑。 - # 跨存取庫連結(例如 LOG 頁連到 PLAN 頁,而兩者的 JSC_WIKI_REPO_{TYPE} 不同) - # 只有絕對網址會通:[[頁名]] 與 markdown 相對連結都只在同一個 wiki 內解析。 + # 連結一律寫成 [文字](絕對網址),網址全部取自這裡:只有絕對網址在哪裡都解得到, + # wiki 內部連結語法與 markdown 相對連結都只在同一個 wiki 內解析,跨存取庫就落到別頁去。 or="${1:?owner/repo required}"; page="${2:?page required}" if ! out=$(req GET "/repos/$or/wiki/page/$page" 2>/dev/null); then case "$(req_code)" in diff --git a/tools/link-check.sh b/tools/link-check.sh new file mode 100755 index 0000000..4e00e27 --- /dev/null +++ b/tools/link-check.sh @@ -0,0 +1,231 @@ +#!/usr/bin/env sh +# link-check.sh — 連結可達性檢查(連結寫進任何文件之前先跑這一支)。 +# 用法: +# link-check.sh <網址>... +# printf '%s\n' <網址>... | link-check.sh # 沒給參數就從標準輸入一行一個讀 +# +# 輸出: 每個網址一行,三欄以 TAB 分隔 +# {OK|DEAD|SKIP}<TAB>{網址}<TAB>{說明} +# OK = 連得到 +# DEAD = 連不到,呼叫端不得把它寫進文件 +# SKIP = 沒有可查的端點(mailto:、錨點、相對路徑),不影響結束碼 +# +# 驗證方式依網址種類分流: +# Gitea wiki 頁(<GITEA_HOST>/<owner>/<repo>/wiki/<頁名>) -> API /repos/<owner>/<repo>/wiki/page/<頁名> +# Gitea 議題(…/issues/<編號>) -> API /repos/<owner>/<repo>/issues/<編號> +# 其他 Gitea 網址 -> HTTP HEAD,帶金鑰 +# 非 Gitea 的外部網址 -> HTTP HEAD,不帶金鑰 +# +# 規則: +# - Gitea 一律走 API,不看網頁狀態碼。私有存取庫的網頁網址對未登入請求一律回 404, +# 用網頁狀態碼判斷會把好連結判成壞連結,接著整批砍掉還在的頁。 +# - 外部網址不帶金鑰。金鑰是這個站台的憑證,送去別的主機就是憑證外洩。 +# - 這支只回報,不改任何檔案。呼叫端拿到結束碼 0 才可以把連結寫進文件。 +# +# 環境變數: GITEA_HOST(例 https://gitea.jsc.idv.tw)、GITEA_TOKEN +# GITEA_TOKEN 未設定時退回 tea CLI 的登入金鑰(~/.config/tea/config.yml)。 +# 優先取 url 與 GITEA_HOST 同主機的登入:tea 可以同時登入多個站台,只看 default +# 會把 A 站的金鑰送去 B 站,那是憑證外洩,不是單純取錯值。 +# +# 結束碼: 0=全部連得到 1=至少一筆連不到 2=用法錯誤(一個網址都沒給) +# 3=清單裡有 Gitea 網址,但 GITEA_HOST 未設定 7=Gitea 認證失敗(HTTP 401、403) +# 7 一定要與 1 分開:金鑰失效時,Gitea 對私有存取庫的回應與「頁不存在」難以分辨。 +# 兩者混用,一次金鑰過期就把整批還在的頁判成死連結,接著這些頁會被當成壞連結刪掉或改寫。 +set -eu + +TAB=$(printf '\t') + +emit() { # <狀態> <網址> <說明> + printf '%s%s%s%s%s\n' "$1" "$TAB" "$2" "$TAB" "$3" +} + +host_of() { # <網址> -> 只留主機名,轉小寫 + printf '%s' "$1" | sed 's#^[a-zA-Z]*://##; s#/.*##; s#^.*@##; s#:[0-9]*$##' | tr 'A-Z' 'a-z' +} + +path_of() { # <網址> -> 去掉協定、主機、查詢字串與錨點之後的路徑,頭尾不留斜線 + printf '%s' "$1" | sed 's#^[a-zA-Z]*://[^/]*##; s/[?#].*$//; s#^/##; s#/$##' +} + +seg() { # <路徑> <序號> -> 第 n 段 + printf '%s' "$1" | cut -d/ -f"$2" +} + +nseg() { # <路徑> -> 段數 + printf '%s' "$1" | awk -F/ '{print NF}' +} + +looks_like_gitea_path() { # <路徑> -> 形狀像 wiki 頁或議題就回 0 + _p="$1" + [ "$(nseg "$_p")" -ge 4 ] || return 1 + case "$(seg "$_p" 3)" in + wiki|issues) return 0 ;; + *) return 1 ;; + esac +} + +resolve_token() { # 印出這次要用的金鑰;一把都沒有就印空字串 + if [ -n "${GITEA_TOKEN:-}" ]; then printf '%s' "$GITEA_TOKEN"; return 0; fi + cfg="${HOME:-}/.config/tea/config.yml" + [ -f "$cfg" ] || return 0 + awk -v want="$GITEA_HOST_ONLY" ' + function flush() { + if (tok == "") return + if (first == "") first = tok + if (want != "" && host == want) match_tok = tok + if (def) def_tok = tok + } + /^ *- / { flush(); tok=""; host=""; def=0 } + /^ *token: */ { line=$0; sub(/^ *token: */,"",line); gsub(/"/,"",line); tok=line } + /^ *url: */ { line=$0; sub(/^ *url: */,"",line); gsub(/"/,"",line) + sub(/^https?:\/\//,"",line); sub(/\/.*$/,"",line); host=line } + /^ *default: *true/ { def=1 } + END { + flush() + if (match_tok != "") { printf "%s", match_tok; exit } + if (def_tok != "") printf "%s", def_tok + else printf "%s", first + } + ' "$cfg" +} + +http_status() { # <HEAD|GET> <網址> [金鑰] -> 印出 HTTP 狀態碼,連不上印 000 + _m="$1"; _u="$2"; _t="${3:-}" + # HEAD 走 -I,不走 -X HEAD:後者讓 curl 等一個永遠不會來的內文,整支腳本卡在那裡。 + # GET 退讓只要第一個位元組(-r 0-0),拿的是狀態碼,不必把整份內容抓回來。 + if [ "$_m" = HEAD ]; then set -- -I; else set -- -r 0-0; fi + # 連線與總時間分兩個上限。主機根本不通時,5 秒就收工;主機活著但慢,才讓它用滿 20 秒。 + # 只設總時間的話,一頁十條連結碰上不通的主機要等三分多鐘,呼叫端會以為整支腳本掛了。 + if [ -n "$_t" ]; then + _c=$(curl -sS -o /dev/null -L --connect-timeout 5 --max-time 20 -w '%{http_code}' "$@" \ + -H "Authorization: token $_t" "$_u" 2>/dev/null || true) + else + _c=$(curl -sS -o /dev/null -L --connect-timeout 5 --max-time 20 -w '%{http_code}' "$@" "$_u" 2>/dev/null || true) + fi + case "$_c" in ''|*[!0-9]*) _c=000 ;; esac + printf '%s' "$_c" +} + +api_status() { # <API 路徑> -> 印出 HTTP 狀態碼,連不上印 000 + _c=$(curl -sS -o /dev/null --connect-timeout 5 --max-time 20 -w '%{http_code}' \ + -H "Authorization: token $TOKEN" "$API$1" 2>/dev/null || true) + case "$_c" in ''|*[!0-9]*) _c=000 ;; esac + printf '%s' "$_c" +} + +auth_stop() { # <網址> -> 印出這一筆,回報金鑰問題並中止 + emit SKIP "$1" "Gitea 認證失敗(HTTP $2),無法判定" + echo "[jsc][連結檢查][ERR]:Gitea 金鑰失效或權限不足(HTTP $2)。請換一支有效的 GITEA_TOKEN,或重新 tea login 之後再跑一次;在那之前不要把任何連結判成死連結。" >&2 + exit 7 +} + +# ---- 收集網址 ---- +list=$(mktemp) +raw=$(mktemp) +trap 'rm -f "$list" "$raw"' EXIT +if [ "$#" -gt 0 ]; then + printf '%s\n' "$@" > "$raw" +else + cat > "$raw" +fi +# 先去掉行尾空白,再丟掉空白行:清單多半是別的指令產出的,尾巴常帶一行空的, +# 那一行會被當成一個網址,最後回一個沒人看得懂的 DEAD。 +sed 's/[[:space:]]*$//' "$raw" | grep -v '^[[:space:]]*$' > "$list" || true +if [ ! -s "$list" ]; then + echo 'usage: link-check.sh <網址>... ;或用標準輸入一行一個網址' >&2 + exit 2 +fi + +# ---- Gitea 站台設定 ---- +GITEA_HOST_ONLY=$(host_of "${GITEA_HOST:-}") +if [ -z "$GITEA_HOST_ONLY" ]; then + # GITEA_HOST 沒設定就認不出哪些是 Gitea 網址,只能看路徑形狀。認出一筆就整批停下來: + # 少了主機設定,Gitea 連結只驗得到網頁狀態碼,私有存取庫會全部被判成死連結。 + while IFS= read -r u; do + if looks_like_gitea_path "$(path_of "$u")"; then + echo "[jsc][連結檢查][ERR]:清單裡有 Gitea 網址($u),但 GITEA_HOST 未設定。請先設定 GITEA_HOST 再跑一次,不得跳過驗證。" >&2 + exit 3 + fi + done < "$list" +fi + +case "${GITEA_HOST:-}" in + http://*|https://*) HOST="${GITEA_HOST:-}" ;; + '') HOST='' ;; + *) HOST="https://${GITEA_HOST}" ;; +esac +API='' +[ -z "$HOST" ] || API="${HOST%/}/api/v1" +TOKEN=$(resolve_token) + +# ---- 逐筆檢查 ---- +dead=0 +while IFS= read -r url; do + case "$url" in + http://*|https://*) ;; + *) + emit SKIP "$url" "不是 http 或 https 網址,沒有可查的端點" + continue ;; + esac + + uhost=$(host_of "$url") + upath=$(path_of "$url") + + if [ -n "$GITEA_HOST_ONLY" ] && [ "$uhost" = "$GITEA_HOST_ONLY" ]; then + owner=$(seg "$upath" 1); repo=$(seg "$upath" 2); kind=$(seg "$upath" 3) + rest='' + [ "$(nseg "$upath")" -lt 4 ] || rest=$(printf '%s' "$upath" | cut -d/ -f4-) + # 編輯畫面的網址尾巴指的還是同一頁,去掉再查。 + rest=$(printf '%s' "$rest" | sed 's#/_edit$##; s#/_new$##; s#/_pages$##') + + if [ "$kind" = wiki ] && [ -n "$rest" ]; then + code=$(api_status "/repos/$owner/$repo/wiki/page/$rest") + case "$code" in + 2*) emit OK "$url" "wiki 頁存在(API HTTP $code)" ;; + 401|403) auth_stop "$url" "$code" ;; + 404) emit DEAD "$url" "wiki 頁不存在(API HTTP 404)"; dead=1 ;; + *) emit DEAD "$url" "wiki API 回 HTTP $code"; dead=1 ;; + esac + continue + fi + + if [ "$kind" = issues ] && [ -n "$rest" ]; then + idx=$(printf '%s' "$rest" | cut -d/ -f1) + case "$idx" in + ''|*[!0-9]*) + emit DEAD "$url" "議題編號不是數字:$idx"; dead=1; continue ;; + esac + code=$(api_status "/repos/$owner/$repo/issues/$idx") + case "$code" in + 2*) emit OK "$url" "議題存在(API HTTP $code)" ;; + 401|403) auth_stop "$url" "$code" ;; + 404) emit DEAD "$url" "議題不存在(API HTTP 404)"; dead=1 ;; + *) emit DEAD "$url" "議題 API 回 HTTP $code"; dead=1 ;; + esac + continue + fi + + code=$(http_status HEAD "$url" "$TOKEN") + case "$code" in + 2*|3*) emit OK "$url" "Gitea 網址可達(HEAD HTTP $code)" ;; + 401|403) auth_stop "$url" "$code" ;; + 000) emit DEAD "$url" "連不上 Gitea 主機(沒有回應)"; dead=1 ;; + *) emit DEAD "$url" "Gitea 網址回 HTTP $code"; dead=1 ;; + esac + continue + fi + + # 外部網址:不帶金鑰。HEAD 被擋掉時再用 GET 試一次,有些站台只擋 HEAD。 + code=$(http_status HEAD "$url") + case "$code" in + 2*|3*) ;; + *) code=$(http_status GET "$url") ;; + esac + case "$code" in + 2*|3*) emit OK "$url" "外部網址可達(HTTP $code)" ;; + 000) emit DEAD "$url" "連不上主機(沒有回應)"; dead=1 ;; + *) emit DEAD "$url" "外部網址回 HTTP $code"; dead=1 ;; + esac +done < "$list" + +exit "$dead"