From 9ec18b8a94fc04d4f003ce0cf7366bbcfdb6071f Mon Sep 17 00:00:00 2001 From: Jeffery Date: Wed, 2 Sep 2026 14:27:18 +0800 Subject: [PATCH 1/9] =?UTF-8?q?feat(link):=20=E9=80=A3=E7=B5=90=E4=B8=80?= =?UTF-8?q?=E5=BE=8B=E5=AF=AB=E6=88=90=20[=E6=96=87=E5=AD=97](=E7=B5=95?= =?UTF-8?q?=E5=B0=8D=E7=B6=B2=E5=9D=80)=EF=BC=8C=E5=AF=AB=E5=85=A5?= =?UTF-8?q?=E5=89=8D=E5=85=88=E9=A9=97=E8=AD=89=E9=80=A3=E5=BE=97=E5=88=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 取消 [[頁名]] 與 [[顯示文字|頁名]] 兩種同 wiki 寫法,不再分「同存取庫」與 「跨存取庫」兩條規則。那種寫法只在自己那個 wiki 內解析,寫錯不報錯,畫面上 看起來像普通文字或死連結,巡不到也修不了。 連結寫進頁面前先過 jsc-gitea 的 link-check.sh,結束碼 0 才寫。驗證一律走 API, 不看網頁狀態碼:私有存取庫的網頁網址對未登入請求一律回 404,拿狀態碼判會把 好連結判成壞的。認證失敗回 7,與死連結的 1 分開,免得金鑰一過期就把還在的頁 整批判死。 --- README.md | 12 +- references/behaviors.md | 14 +-- references/wiki-links.md | 65 +++++++--- skills/html-export/SKILL.md | 2 +- skills/wiki-to-issue/SKILL.md | 4 +- skills/wiki/SKILL.md | 16 ++- tools/gitea.sh | 7 +- tools/link-check.sh | 231 ++++++++++++++++++++++++++++++++++ 8 files changed, 317 insertions(+), 34 deletions(-) create mode 100755 tools/link-check.sh 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/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" From b59782d0931143f865ca2965ab679bf119289ad5 Mon Sep 17 00:00:00 2001 From: Jeffery <jiantw83@yahoo.com> Date: Wed, 2 Sep 2026 14:27:18 +0800 Subject: [PATCH 2/9] =?UTF-8?q?chore(plugin=20=E7=89=88=E6=9C=AC):=20?= =?UTF-8?q?=E4=B8=89=E4=BB=BD=20manifest=20=E5=8D=87=E7=89=88=E8=87=B3=200?= =?UTF-8?q?.2.3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .claude-plugin/plugin.json | 2 +- .codex-plugin/plugin.json | 2 +- plugin.json | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) 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/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": { From f69b4b6f8595495af7f42a0b4495688fc1143dda Mon Sep 17 00:00:00 2001 From: Jeffery <jiantw83@yahoo.com> Date: Wed, 2 Sep 2026 16:01:15 +0800 Subject: [PATCH 3/9] =?UTF-8?q?feat(=E7=8B=80=E6=85=8B=E5=9B=9E=E5=A0=B1):?= =?UTF-8?q?=20=E6=94=B6=E5=B0=BE=E5=AF=AB=E4=B8=80=E7=AD=86=20skill-end=20?= =?UTF-8?q?=E4=BA=8B=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 現行紀錄只記「被叫用」,沒有成敗也沒有結束碼。跑完整輪的技能與開場就 中止的技能,在紀錄裡長得一模一樣。 start 由技能用量 hook 順手發,不必改技能文件。end 只能由技能自己在收尾 步驟寫——hook 接在技能工具呼叫上,而實際工作發生在之後的模型輪次,它在 原理上看不到成敗。有 start 沒有配對的 end,就是那一輪中止了。 status 五選一,每支技能各自寫明什麼情況選哪一個。找不到回報腳本就安靜 跳過,回報失敗一律不改變技能自己的結論。 --- references/behaviors.md | 40 +++++++++++++++++------------------ skills/html-export/SKILL.md | 15 +++++++++++++ skills/html-style/SKILL.md | 15 +++++++++++++ skills/repo-sync/SKILL.md | 15 +++++++++++++ skills/wiki-to-issue/SKILL.md | 16 ++++++++++++++ skills/wiki/SKILL.md | 20 ++++++++++++++++++ 6 files changed, 101 insertions(+), 20 deletions(-) diff --git a/references/behaviors.md b/references/behaviors.md index 482d0c7..fbea735 100644 --- a/references/behaviors.md +++ b/references/behaviors.md @@ -7,47 +7,47 @@ | 項目 | 內容 | | --- | --- | | 觸發時機 | 使用者給一條 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、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,來源頁面本身也不動。 | +| 關鍵步驟 | 用 tools/gitea-link.sh parse 解析連結、確認 GITEA_HOST 有值、同一批平行跑三條線、開 sub agent 整理 markdown、用 tools/html-render.sh 產出檔案,最後呼叫 jsc-hooks/tools/report-status.sh skill-end jsc-gitea:html-export 寫下這一輪的結果。三條線分別是: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 內部連結換成 [文字](絕對網址)、拿掉個資。收尾那一筆走每一條出口,連停在連結閘門那一條也要寫;status 五選一,檔案產出得完整是 ok,主機沒值就停是 blocked,讀頁或渲染中途壞掉是 failed,讀不到標籤而改用沒帶標籤的範本是 degraded,請求裡沒有連結或使用者中止是 aborted。腳本不在這台機器就安靜跳過,回報失敗不得改變這支技能的結果。 | +| 外部呼叫 | tools/gitea-link.sh、tools/html-style.sh、tools/html-render.sh、tools/issue.sh、jsc-gitea:wiki、jsc-ask:ask、jsc-hooks/tools/report-status.sh skill-end、Gitea 的 wiki API、Gitea 的議題 API、Gitea 的 markdown 渲染 API。 | +| 完成條件 | 檔案已經寫出來。回報裡有檔案路徑、版型名稱、風格名稱,以及這一組是從哪裡來的。來源是 default 或 builtin 時,回報要多一行告訴使用者可以用 jsc-gitea:html-style 設定。渲染失敗就不留半成品檔,直接停手回報。這一輪還要留下一筆 skill-end 事件,或是腳本不在而略過,兩者都算收好;略過不影響這支技能的結束碼。 | +| 可驗證跡象 | 使用者確認過的輸出路徑多一個 HTML 檔,預設落在 ./.jsc/html/ 底下。這個檔把 CSS 與 JS 內嵌,不外連任何資源,開起來就是完整的一頁。$JSC_HOME/usage/events.jsonl 會多一筆 {kind:skill,phase:end} 事件,name 是 jsc-gitea:html-export,status 與 exit 就是這一輪的結果。不寫 wiki 頁、不建議題、不開 PR,來源頁面本身也不動。 | ## html-style | 項目 | 內容 | | --- | --- | | 觸發時機 | 某一類 wiki 頁或議題匯出時要換版型、換風格。或是 jsc-gitea:html-export 回報來源是 builtin 或 default,要補上這一類自己的設定。只是要出一份檔案,走 jsc-gitea:html-export。 | -| 關鍵步驟 | 用 tools/html-style.sh list 列出目前設定、依決策樹敲定一個 kind key、用 layouts 列出全部六種版型讓使用者挑、用 styles 列出全部五種風格讓使用者挑、問這次寫專案還是寫全機、用 set 寫進設定檔、再用 get 讀回來核對來源欄。kind key 只有三種形狀:WIKI:{頁名前綴}、ISSUE:{標籤名}、DEFAULT。版型與風格一律整份列出,不先篩短清單。 | -| 外部呼叫 | tools/html-style.sh 的 list、get、layouts、styles、set、unset 子命令、jsc-ask:ask。不打任何 Gitea API。 | -| 完成條件 | set 回結束碼 0,並印出它寫的那個檔案。get 讀回來的來源欄與這次選的範圍一致:選 --project 就顯示 project,選 --global 就顯示 global。寫進去的版型名與風格名,都要是腳本列過的名字。 | -| 可驗證跡象 | 設定檔多一列 {種類}={版型},{風格},例如 WIKI:PLAN=report,corporate。選 --project 改的是工作目錄的 ./.jsc/html-styles,這個檔會跟著存放庫一起提交。選 --global 改的是 $JSC_HOME/html-styles.conf,JSC_HOME 預設 ~/.jsc。兩個檔都握有同一個 key 時,專案檔贏。不產 HTML 檔、不動 Gitea。 | +| 關鍵步驟 | 用 tools/html-style.sh list 列出目前設定、依決策樹敲定一個 kind key、用 layouts 列出全部六種版型讓使用者挑、用 styles 列出全部五種風格讓使用者挑、問這次寫專案還是寫全機、用 set 寫進設定檔、再用 get 讀回來核對來源欄,最後呼叫 jsc-hooks/tools/report-status.sh skill-end jsc-gitea:html-style 寫下這一輪的結果。kind key 只有三種形狀:WIKI:{頁名前綴}、ISSUE:{標籤名}、DEFAULT。版型與風格一律整份列出,不先篩短清單。收尾那一筆走每一條出口,連停在範本目錄不見那一條也要寫;status 五選一,寫進去又讀回來對得上是 ok,範本目錄不見而列不出名字、整支停在問問題之前是 blocked,set 回 1、2 或 4 沒寫成是 failed,set 回 0 但讀回來的來源欄與這次選的範圍不同是 degraded,使用者在四個問題任何一關停手是 aborted。腳本不在這台機器就安靜跳過,回報失敗不得改變這支技能的結果。 | +| 外部呼叫 | tools/html-style.sh 的 list、get、layouts、styles、set、unset 子命令、jsc-ask:ask、jsc-hooks/tools/report-status.sh skill-end。不打任何 Gitea API。 | +| 完成條件 | set 回結束碼 0,並印出它寫的那個檔案。get 讀回來的來源欄與這次選的範圍一致:選 --project 就顯示 project,選 --global 就顯示 global。寫進去的版型名與風格名,都要是腳本列過的名字。這一輪還要留下一筆 skill-end 事件,或是腳本不在而略過,兩者都算收好;略過不影響這支技能的結束碼。 | +| 可驗證跡象 | 設定檔多一列 {種類}={版型},{風格},例如 WIKI:PLAN=report,corporate。選 --project 改的是工作目錄的 ./.jsc/html-styles,這個檔會跟著存放庫一起提交。選 --global 改的是 $JSC_HOME/html-styles.conf,JSC_HOME 預設 ~/.jsc。兩個檔都握有同一個 key 時,專案檔贏。$JSC_HOME/usage/events.jsonl 會多一筆 {kind:skill,phase:end} 事件,name 是 jsc-gitea:html-style,status 與 exit 就是這一輪的結果。不產 HTML 檔、不動 Gitea。 | ## repo-sync | 項目 | 內容 | | --- | --- | | 觸發時機 | 要把某一個 Gitea 擁有者底下讀得到的存放庫,一次全部拉到工作目錄。開新工作環境、或整批更新既有存放庫時用。只同步一個存放庫時不用這支。 | -| 關鍵步驟 | 用 tools/gitea.sh owners 列出讀得到的擁有者、請使用者挑一個、用 tools/gitea.sh repos 列出該擁有者底下的存放庫、同一批平行開 sub agent 一個存放庫一個、每個 sub agent 跑 tools/repo-sync.sh 並依它那一行輸出分流、最後把每個存放庫的結果彙整回報。分流有四種:cloned 與 updated 就算完成,dirty {分支} 把那個分支當 base 交給 jsc-git:pr,failed {原因} 記下原因並停掉這個存放庫。複製或拉取的判斷、基準分支的優先序,都由腳本決定,不自己下 git clone、git checkout、git pull。 | -| 外部呼叫 | tools/gitea.sh 的 owners、repos、clone-url、tools/repo-sync.sh、jsc-git:pr、jsc-ask:ask、Gitea 的擁有者與存放庫清單 API,以及腳本內部的 git clone、git fetch、git pull。 | -| 完成條件 | 清單上的每一個存放庫都拿到一種結果:cloned、updated、一列 PR 表格,或失敗原因。一個存放庫失敗不取消其他存放庫。有多條 PR 時,全部併進同一張表。 | -| 可驗證跡象 | 工作目錄底下多出或更新了各存放庫的目錄。新的是 git clone 的結果,既有的已經快轉到基準分支。原本有未提交變更的存放庫,會多一條推上去的分支,以及一條開在 Gitea 上的 PR,PR 網址寫在回報表格裡。不寫 wiki 頁、不建議題。 | +| 關鍵步驟 | 用 tools/gitea.sh owners 列出讀得到的擁有者、請使用者挑一個、用 tools/gitea.sh repos 列出該擁有者底下的存放庫、同一批平行開 sub agent 一個存放庫一個、每個 sub agent 跑 tools/repo-sync.sh 並依它那一行輸出分流、把每個存放庫的結果彙整回報,最後呼叫 jsc-hooks/tools/report-status.sh skill-end jsc-gitea:repo-sync 寫下這一輪的結果。分流有四種:cloned 與 updated 就算完成,dirty {分支} 把那個分支當 base 交給 jsc-git:pr,failed {原因} 記下原因並停掉這個存放庫。複製或拉取的判斷、基準分支的優先序,都由腳本決定,不自己下 git clone、git checkout、git pull。收尾那一筆走每一條出口,連停在列不出擁有者那一條也要寫;status 五選一,每個存放庫都同步完是 ok,主機或金鑰沒值、或這把金鑰一個擁有者都讀不到而整輪沒動到任何存放庫是 blocked,列清單回 7 或 8、或每個存放庫都 failed 是 failed,有的成功有的 failed 是 degraded,使用者沒挑擁有者或中途停手是 aborted。detail 只放筆數,逐個存放庫的清單留在回報裡。腳本不在這台機器就安靜跳過,回報失敗不得改變這支技能的結果。 | +| 外部呼叫 | tools/gitea.sh 的 owners、repos、clone-url、tools/repo-sync.sh、jsc-git:pr、jsc-ask:ask、jsc-hooks/tools/report-status.sh skill-end、Gitea 的擁有者與存放庫清單 API,以及腳本內部的 git clone、git fetch、git pull。 | +| 完成條件 | 清單上的每一個存放庫都拿到一種結果:cloned、updated、一列 PR 表格,或失敗原因。一個存放庫失敗不取消其他存放庫。有多條 PR 時,全部併進同一張表。這一輪還要留下一筆 skill-end 事件,或是腳本不在而略過,兩者都算收好;略過不影響這支技能的結束碼。 | +| 可驗證跡象 | 工作目錄底下多出或更新了各存放庫的目錄。新的是 git clone 的結果,既有的已經快轉到基準分支。原本有未提交變更的存放庫,會多一條推上去的分支,以及一條開在 Gitea 上的 PR,PR 網址寫在回報表格裡。$JSC_HOME/usage/events.jsonl 會多一筆 {kind:skill,phase:end} 事件,name 是 jsc-gitea:repo-sync,status 與 exit 就是這一輪的結果。不寫 wiki 頁、不建議題。 | ## wiki | 項目 | 內容 | | --- | --- | | 觸發時機 | 技能組裡任何一次 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、兩個都沒有才問使用者,而且不借用別的頁型的存放庫。頁面裡的連結一律寫成 [文字](絕對網址),網址取自 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 會各留下一次人工確認。只做讀取的呼叫無寫入跡象,只有回報內容。 | +| 關鍵步驟 | 確認 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 讀一次。收尾一律呼叫 jsc-hooks/tools/report-status.sh skill-end jsc-gitea:wiki 寫下這一輪的結果,走每一條出口,連停在主機閘門那一條也要寫;這支被幾乎每支技能呼叫,只回報這一次 wiki 動作的成敗,不回報呼叫端自己的結果。status 五選一,讀到頁面或寫入落地是 ok,主機沒值、或 wiki-repo 回 3 而使用者沒給 {owner}/{repo}、整輪沒讀也沒寫是 blocked,金鑰失效那個結束碼 7 算 failed 不算頁面不存在、結束碼 8 與 link-check 回 1 擋下寫入也是 failed,內容頁寫成功但目錄頁沒更新(wiki-contents.sh 回 3 或 1)是 degraded、搬移只搬掉一部分也是 degraded,人工確認被否決或頁型不在允許清單而停在 wiki-repo 回 2 是 aborted。detail 只放頁名,不放頁面內容。腳本不在這台機器就安靜跳過,回報失敗不得改變回給呼叫端的結果。 | +| 外部呼叫 | 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、jsc-hooks/tools/report-status.sh skill-end、Gitea 的 wiki API 與議題 API。 | +| 完成條件 | 讀取動作拿到頁面內容,或拿到一個講得清楚的結束碼。這一輪還要留下一筆 skill-end 事件,或是腳本不在而略過,兩者都算收好;略過不影響回給呼叫端的結束碼。寫入動作先讓 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 會各留下一次人工確認。$JSC_HOME/usage/events.jsonl 會多一筆 {kind:skill,phase:end} 事件,name 是 jsc-gitea:wiki,status 與 exit 就是這一次 wiki 動作的結果。只做讀取的呼叫沒有 wiki 寫入跡象,只有回報內容與那一筆事件。 | ## 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、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 頁本身不動。 | +| 關鍵步驟 | 同一批檢查連結與主機、平行跑三條線、依決策樹挑標籤、處理看板、最後建議題。連結用 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 不存在時,把腳本印出的看板網址交給使用者自己拖。最後呼叫 jsc-hooks/tools/report-status.sh skill-end jsc-gitea:wiki-to-issue 寫下這一輪的結果,走每一條出口,連停在連結閘門那一條也要寫;status 五選一,議題建好且看板也掛上是 ok,主機沒值就停、整輪沒讀頁也沒起草是 blocked,讀頁回 4、7、8 或取標籤回 1 或 create 回 1 是 failed,議題建好但看板沒掛上(projects 回 3 或掛看板被拒)是 degraded,請求裡沒有 wiki 連結、parse 回 3、連結指向議題,或使用者否決 create 的人工確認是 aborted。detail 放議題編號,不放內文。腳本不在這台機器就安靜跳過,回報失敗不得改變這支技能的結果。 | +| 外部呼叫 | tools/gitea-link.sh、tools/issue.sh 的 labels、projects、label-ids、create、jsc-gitea:wiki、jsc-ask:ask、tools/write-confirm.sh、jsc-hooks/tools/report-status.sh skill-end、Gitea 的 wiki API 與議題 API。 | +| 完成條件 | 議題已經建立,回報裡有議題網址、實際套上的標籤,以及看板狀態。建不成就明講沒有建成,並交出草稿檔的路徑,讓草稿不會白寫。標題、內文、標籤、看板都在呼叫 create 之前先跟使用者確認過。這一輪還要留下一筆 skill-end 事件,或是腳本不在而略過,兩者都算收好;略過不影響這支技能的結束碼。 | +| 可驗證跡象 | 目標存放庫的議題追蹤器多一條議題,create 會印出 index= 與 url= 兩行。$JSC_HOME/usage/events.jsonl 會多一筆 {kind:skill,phase:end} 事件,name 是 jsc-gitea:wiki-to-issue,status 與 exit 就是這一輪的結果。議題內文第一行是「來源:」加上 wiki 頁的絕對網址,內文裡每一條連結都是 [文字](絕對網址),沒有殘留的 wiki 內部連結語法。標籤就是這次確認過的那一組。站台有看板 API 時,看板上多一張卡;沒有時,回報裡留一行待辦說明誰要去補。來源 wiki 頁本身不動。 | diff --git a/skills/html-export/SKILL.md b/skills/html-export/SKILL.md index cb631ab..9722a3c 100644 --- a/skills/html-export/SKILL.md +++ b/skills/html-export/SKILL.md @@ -19,6 +19,21 @@ The link is the only input. The output is one file that opens anywhere, with no 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.** 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. +6. **Record how the run ended.** This is the last thing this skill does, and it runs on every path out of the skill — including the ones that stop at step 1. Call + + `jsc-hooks/tools/report-status.sh skill-end jsc-gitea:html-export {status} {exit code} [detail]` + + `{exit code}` is the exit code of whatever decided the outcome, and `0` when nothing failed. `{detail}` is one short line, no more than 200 characters. **If the script is not on this machine, skip this step in silence and finish the run as it stood** — missing infrastructure is not a failure, and a reporting call may never change what this skill returns or reports. + + | status | When this skill uses it | + | --- | --- | + | `ok` | The file was rendered and the report names its path, layout, style and source | + | `blocked` | The host gate of step 2 stopped the run: `GITEA_HOST` holds no value and the user gave none, so nothing was read and nothing was rendered | + | `failed` | The work started and broke: `wiki-get` returned 7 or 8, `issue.sh show` returned 1, or `html-render.sh` returned 1, 2 or 4. Nothing usable came out | + | `degraded` | The file was rendered, but part of the run did not hold — track B could not read the labels (exit 1) so the template was picked without them, and the export used a template the configuration did not choose | + | `aborted` | The premise did not hold, so the skill stopped on its own: the request carried no wiki or issue link, or `gitea-link.sh parse` returned 3. Also used when the user stops the run at step 3's destination question | + + Completion condition: exactly one `skill-end` line was recorded for this run, or the script was absent and the run finished without it. ## Rules diff --git a/skills/html-style/SKILL.md b/skills/html-style/SKILL.md index 219db95..27c0954 100644 --- a/skills/html-style/SKILL.md +++ b/skills/html-style/SKILL.md @@ -15,6 +15,21 @@ One kind of page, one layout, one style. `jsc-gitea:html-export` reads what this 4. **Pick the scope.** Ask per `jsc-ask:ask` rules: `--project` writes `./.jsc/html-styles`, which only applies inside this working directory and is committed with the repository; `--global` writes `$JSC_HOME/html-styles.conf`, which follows the user across every project on this machine. State that the project file wins whenever both hold the same key. Completion condition: the user has picked one scope. 5. Write it: `tools/html-style.sh set {key} {layout} {style} [--project|--global]`. Route every exit code: 0 → the file it printed now holds the pair; 1 → the settings file's directory could not be created, so report the path and stop, since nothing was written; 2 → a usage error, such as a missing name or a scope flag that is neither `--project` nor `--global`, so fix the arguments and call again; 4 → the layout or style name has no template file, so go back to step 2 or step 3 rather than editing the settings file by hand. Completion condition: the script exits 0 and prints the file it wrote. 6. Read it back with `tools/html-style.sh get {key}` and report the resolved layout, style and source. Completion condition: the source column shows `project` or `global`, matching the scope chosen in step 4. +7. **Record how the run ended.** This is the last thing this skill does, and it runs on every path out of the skill, the ones that stop at step 2 included. Call + + `jsc-hooks/tools/report-status.sh skill-end jsc-gitea:html-style {status} {exit code} [detail]` + + `{exit code}` is the exit code of whatever decided the outcome, and `0` when nothing failed. `{detail}` is one short line, no more than 200 characters. **If the script is not on this machine, skip this step in silence and finish the run as it stood** — missing infrastructure is not a failure, and a reporting call may never change what this skill returns or reports. + + | status | When this skill uses it | + | --- | --- | + | `ok` | `set` exited 0 and step 6 read the pair back with the source column matching the scope that was chosen | + | `blocked` | The template directory is missing, so `layouts` or `styles` listed nothing. There is no name to offer and no pair to write, so the run stops before any question and the settings file is untouched | + | `failed` | The write itself broke: `set` returned 1 because the settings directory could not be created, 2 on a malformed call, or 4 because the layout or style has no template file. Nothing was written | + | `degraded` | `set` exited 0, but step 6 read back a different source than the scope chosen in step 4 — usually a project file holding the same key and winning over a global write. The pair is on disk, yet the export will still resolve to another one | + | `aborted` | The user stopped at one of the four questions — the kind key, the layout, the style or the scope — so nothing was written | + + Completion condition: exactly one `skill-end` line was recorded for this run, or the script was absent and the run finished without it. ## Rules diff --git a/skills/repo-sync/SKILL.md b/skills/repo-sync/SKILL.md index dbf308d..32b94be 100644 --- a/skills/repo-sync/SKILL.md +++ b/skills/repo-sync/SKILL.md @@ -16,3 +16,18 @@ description: Batch-sync all readable repos of a chosen Gitea owner into the work Done when every repo's sub agent has returned one of those outcomes; one repo failing never cancels the others. 5. Report the sync result for every repo: cloned, updated, PR table row, or the failure reason. Done when every `{repo}` from step 3 carries one of those four results, and all PR rows share one table when more than one PR exists. +6. **Record how the run ended.** This is the last thing this skill does, and it runs on every path out of the skill, the ones that stop at step 1 included. Call + + `jsc-hooks/tools/report-status.sh skill-end jsc-gitea:repo-sync {status} {exit code} [detail]` + + `{exit code}` is the exit code of whatever decided the outcome, and `0` when nothing failed. `{detail}` is one short line, no more than 200 characters — the repo counts fit there, the per-repo list does not. **If the script is not on this machine, skip this step in silence and finish the run as it stood** — missing infrastructure is not a failure, and a reporting call may never change what this skill returns or reports. + + | status | When this skill uses it | + | --- | --- | + | `ok` | Every repo from step 3 came back `cloned`, `updated`, or dirty with its PR opened | + | `blocked` | Nothing could be listed, so no repo was touched: `GITEA_HOST` or `GITEA_TOKEN` was required and the user gave none, or `owners` exited 0 with no owner this key can read | + | `failed` | The listing broke mid-run — `owners` or `repos` returned 7 or 8 — or every repo in step 4 came back `failed`. No repo reached the working directory | + | `degraded` | Some repos synced and some did not: at least one `failed {reason}` next to at least one `cloned`, `updated` or PR row. One repo failing never cancels the others, so the run finishes with part of the workspace missing | + | `aborted` | The user named no owner at step 2, or stopped the run before step 4 started | + + Completion condition: exactly one `skill-end` line was recorded for this run, or the script was absent and the run finished without it. diff --git a/skills/wiki-to-issue/SKILL.md b/skills/wiki-to-issue/SKILL.md index c66dc93..5bf9fd3 100644 --- a/skills/wiki-to-issue/SKILL.md +++ b/skills/wiki-to-issue/SKILL.md @@ -24,6 +24,22 @@ The wiki link is the only input. Everything else — repository, page name, host 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. +6. **Record how the run ended.** This is the last thing this skill does, and it runs on every path out of the skill, the ones that stop at step 1 included. Call + + `jsc-hooks/tools/report-status.sh skill-end jsc-gitea:wiki-to-issue {status} {exit code} [detail]` + + `{exit code}` is the exit code of whatever decided the outcome, and `0` when nothing failed. `{detail}` is one short line, no more than 200 characters — the issue index fits there, the issue body does not. **If the script is not on this machine, skip this step in silence and finish the run as it stood** — missing infrastructure is not a failure, and a reporting call may never change what this skill returns or reports. + + | status | When this skill uses it | + | --- | --- | + | `ok` | `issue.sh create` exited 0, and the report carries the issue URL, the labels applied and a board that is attached | + | `blocked` | The host gate stopped the run: `GITEA_HOST` holds no value and the user gave none, so the page was never read and no issue was drafted | + | `failed` | The work started and broke: `wiki-get` returned 4, 7 or 8, `issue.sh labels` returned 1 so no label could be picked without inventing one, or `issue.sh create` returned 1 and no issue exists. Report the draft path in `{detail}` when the create failed | + | `degraded` | The issue was created, but part of it stays outstanding — track C exited 3 because this Gitea has no board API, or the attach call was refused, so the report hands the board link back to the user to drag in by hand. The issue is real, its place on the board is not | + | `aborted` | The premise did not hold, so the skill stopped on its own: the request carried no wiki link, `gitea-link.sh parse` returned 3, or the link parsed as `kind=issue`. Also used when the user refuses the confirmation `issue.sh create` asks for, so nothing was written | + + Completion condition: exactly one `skill-end` line was recorded for this run, or the script was absent and the run finished without it. + ## Rules - One wiki page, one issue. Splitting a page into several issues is analysis work, not conversion — hand that to `jsc-sdlc:analyze`. diff --git a/skills/wiki/SKILL.md b/skills/wiki/SKILL.md index 27f2d3d..7bba34a 100644 --- a/skills/wiki/SKILL.md +++ b/skills/wiki/SKILL.md @@ -74,6 +74,26 @@ Every code below gets its own branch. Nothing here is retried unchanged. | | 2 | usage error, including an unconfigured CONTENTS repo | fix the arguments or set `JSC_WIKI_REPO_CONTENTS`, then call again | | | 3 | something needs manual handling: an orphan page, a page that links to a moved page without being moved itself, or a destination page that already holds content | report those lists and hand them to the user; guess no key, and rewrite no link the script left alone | +## Close the run + +**Record how the run ended.** This is the last thing this skill does, and it runs on every path out of the skill, the ones that stop at the host gate included. Call + +`jsc-hooks/tools/report-status.sh skill-end jsc-gitea:wiki {status} {exit code} [detail]` + +`{exit code}` is the exit code of whatever decided the outcome — the `gitea.sh`, `link-check.sh` or `wiki-contents.sh` call that ruled the run — and `0` when nothing failed. `{detail}` is one short line, no more than 200 characters; put the page name there, never the page content. **If the script is not on this machine, skip this step in silence and finish the run as it stood** — missing infrastructure is not a failure, and a reporting call may never change what this skill returns to its caller. + +This skill is called by almost every other one, so its status is what the caller reads back. Report the status of this wiki operation only, never the caller's own outcome. + +| status | When this skill uses it | +| --- | --- | +| `ok` | The read returned the page, or the write landed: `link-check.sh` exited 0, the confirmation was given, and `wiki-put` exited 0 | +| `blocked` | The location could not be resolved, so nothing was read and nothing was written: `GITEA_HOST` holds no value and the user gave none, or `wiki-repo` exited 3 and the user supplied no `{owner}/{repo}` for that page type | +| `failed` | The operation ran and broke. **Exit 7 belongs here**: the key is invalid or lacks permission, so the whole operation stopped, and that is a failure, never an absent page. Exit 8, a `wiki-put` that did not land, a `link-check.sh` exit 1 that refused the write, and a `hash-id` or `page-name.sh` rejection all sit here too | +| `degraded` | The content page landed and the contents page did not — `wiki-put` on `{TYPE}_{HASH}` exited 0, then `wiki-contents.sh upsert` exited 3 with no CONTENTS repo configured, or exited 1 on a page holding no markdown table. The record exists but nothing indexes it, so the next reader will not find it. A migration that moved some pages and left orphans or occupied destinations behind sits here as well | +| `aborted` | The premise did not hold or the user stopped it: `write-confirm.sh` was refused before a write or a delete, or the caller asked for a page type outside the allowed list and the run stopped at `wiki-repo` exit 2 | + +Completion condition: exactly one `skill-end` line was recorded for this run, or the script was absent and the run finished without it. + ## Rules 1. Page names must follow the wiki naming table in the skill guidelines (see `jsc-meta/references/guidelines.md`). Check any page name you build with `tools/page-name.sh check {page}` before it reaches an API call. From a1a00eae74e84dd78a48d325f8a4f64bcb79d357 Mon Sep 17 00:00:00 2001 From: Jeffery <jiantw83@yahoo.com> Date: Wed, 2 Sep 2026 16:01:15 +0800 Subject: [PATCH 4/9] =?UTF-8?q?chore(plugin=20=E7=89=88=E6=9C=AC):=20?= =?UTF-8?q?=E4=B8=89=E4=BB=BD=20manifest=20=E5=8D=87=E7=89=88=E8=87=B3=200?= =?UTF-8?q?.2.4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .claude-plugin/plugin.json | 2 +- .codex-plugin/plugin.json | 2 +- plugin.json | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.claude-plugin/plugin.json b/.claude-plugin/plugin.json index e9b14f1..a9c234f 100644 --- a/.claude-plugin/plugin.json +++ b/.claude-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "jsc-gitea", - "version": "0.2.3", + "version": "0.2.4", "description": "Gitea API 工具、Wiki 讀寫、議題轉換、HTML 匯出與存取庫批次同步", "skills": "./skills", "author": { diff --git a/.codex-plugin/plugin.json b/.codex-plugin/plugin.json index 87ea348..19cf779 100644 --- a/.codex-plugin/plugin.json +++ b/.codex-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "jsc-gitea", - "version": "0.2.3", + "version": "0.2.4", "description": "Gitea API 工具、Wiki 讀寫、議題轉換、HTML 匯出與存取庫批次同步", "skills": "./skills", "jsc": { diff --git a/plugin.json b/plugin.json index f0b6141..c2d3810 100644 --- a/plugin.json +++ b/plugin.json @@ -1,6 +1,6 @@ { "name": "jsc-gitea", - "version": "0.2.3", + "version": "0.2.4", "description": "Gitea API 工具、Wiki 讀寫、議題轉換、HTML 匯出與存取庫批次同步", "skills": "./skills/", "jsc": { From 2e8712126fb86621fdccd83b0fb6fdd95452c63b Mon Sep 17 00:00:00 2001 From: Jeffery <jiantw83@yahoo.com> Date: Wed, 2 Sep 2026 17:22:06 +0800 Subject: [PATCH 5/9] =?UTF-8?q?refactor(wiki-contents):=20=E7=9B=AE?= =?UTF-8?q?=E9=8C=84=E9=A0=81=E6=94=B9=E6=88=90=20H2=20=E5=8D=80=E5=A1=8A?= =?UTF-8?q?=20upsert=EF=BC=8C=E8=88=8A=E8=A1=A8=E6=A0=BC=E8=87=AA=E5=8B=95?= =?UTF-8?q?=E8=BD=89=E6=AA=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 目錄頁的一筆紀錄從 markdown 表格的一列,改成一個 H2 區塊:標題就是這一筆 對應內容頁的頁名,欄位變成標題底下一層的條列「- {欄位名}:{值}」。upsert 換掉標題相同那一塊,找不到就附加到頁尾。 表格一列擠著所有欄位,欄位一多就超出可讀寬度,得橫向捲才看得完;換行之後 也分不出哪幾格屬於同一筆。條列沒有寬度上限,一筆看得完整。 讀到的舊頁還是表格,就先整頁轉成區塊再在轉好的頁面上做 upsert,一頁同時 有表格與區塊也照樣接得起來。轉檔取標題只看持有身分那一欄:有連結取網址 最後一段路徑並解掉百分號編碼,沒連結取純文字——連結的顯示文字常常是計畫 或工作包名稱而不是頁名,拿它當標題會跟呼叫端傳進來的鍵對不上,同一筆長出 第二個區塊,舊的那塊從此再也更新不到。那一欄取不出鍵就整支擋下來,不猜 標題。轉檔那一次有給範本,就連 H1 與引言一起換成範本那一份,舊引言否則 會一直講「一列一筆」;頁面已是條列時只更新自己那一筆,不動引言。組頁邏輯 另外拆出 format 子命令,離線驗證才叫得到,不必打 API。 功能範圍:目錄頁版面改版,涵蓋所有以 _CONTENTS 結尾的頁面與每一支寫目錄 頁的技能。 --- tools/wiki-contents.sh | 365 ++++++++++++++++++++++++++++++----------- 1 file changed, 272 insertions(+), 93 deletions(-) diff --git a/tools/wiki-contents.sh b/tools/wiki-contents.sh index b352a7a..23abed9 100755 --- a/tools/wiki-contents.sh +++ b/tools/wiki-contents.sh @@ -1,25 +1,48 @@ #!/usr/bin/env sh -# wiki-contents.sh — 目錄頁(*_CONTENTS)的表格列 upsert。 +# wiki-contents.sh — 目錄頁(*_CONTENTS)的區塊 upsert。 # -# 為什麼要有這支腳本:目錄頁的「找同一列就取代、找不到就附加」原本靠模型照 +# 為什麼要有這支腳本:目錄頁的「找同一筆就取代、找不到就附加」原本靠模型照 # SKILL.md 手工做,十四個目錄頁只有一處寫成程式。同一段判斷做十四次,錯一次 # 就少一筆紀錄。抽成一支,讀舊頁、比對鍵、整頁寫回只有一種做法。 # +# 版面:一筆紀錄一個 H2 區塊。H2 標題就是這一筆的鍵,寫成對應內容頁的頁名;欄位是 +# 標題底下一層條列,一行一條「- {欄位名}:{值}」。目錄頁上不留 markdown 表格。 +# # 用法: -# wiki-contents.sh upsert <TYPE> <key-col> <key> <row-file> [template-file] +# wiki-contents.sh upsert <TYPE> <key-col> <key> <entry-file> [template-file] # TYPE 頁型,決定頁名 {TYPE}_CONTENTS -# key-col 鍵在表格第幾欄,1 起算 -# key 鍵值,用來找既有列 -# row-file 整列 markdown 表格列的檔案 -# template-file 選用。頁不存在時用它建新頁 +# key-col 只有舊頁還是表格時才用得到:舊表格裡持有這一筆身分的欄位序號, +# 1 起算。轉檔時該欄格子有連結就取網址最後一段路徑當 H2 標題, +# 沒有連結才取格子純文字。頁面已經是條列格式時完全忽略這個參數 +# key 這一筆的 H2 標題文字,也就是內容頁頁名。用來找既有區塊 +# entry-file 整個 H2 區塊的 markdown:「## {key}」那一行、空行、各條條列 +# template-file 選用。頁不存在時用它建新頁;舊頁還是表格而要自動轉檔時, +# 也用它的 H1 與「>」引言取代舊頁那一份 +# +# wiki-contents.sh format <key-col> <key> <entry-file> <old-file> <new-file> [template-file|--fresh] +# 只做文字處理,不碰 API,把結果寫進 new-file 並印出 updated 或 added。 +# 轉檔與 upsert 的判斷只有這一份,離線驗證餵檔案給它就好,不必打 API。 +# 第六個參數給 --fresh 代表 old-file 是範本,要剝掉示範資料;給檔案路徑 +# 則等同 upsert 的 template-file,只在轉檔那一次用來換掉引言。 # # 規則: # 目錄頁一律住在 CONTENTS 專用存取庫,所以存取庫走 wiki-repo CONTENTS, # 不走各自的頁型。 +# 舊頁還是 markdown 表格時,先整頁轉成 H2 區塊再做 upsert;一頁同時有表格與區塊, +# 表格轉出來的區塊接在既有區塊後面。三種舊頁狀態都不得毀掉別人那一筆。 +# 轉檔取 H2 標題只看身分欄那一格:有連結就取網址最後一段路徑,網址經過百分號編碼 +# 就先解碼;沒有連結就取純文字,去掉反引號與頭尾空白。取到什麼就用什麼,不驗頁名樣式。 +# 找「## {key}」:標題文字去頭尾空白後完全相等才算命中。命中就換掉整塊,從那一行 +# 到下一個「## 」之前或檔尾;沒命中就附加到最後一個區塊之後。 # 只有 wiki-get 回 4 才准建新頁。回 7 或 8 一律中止:把金鑰失效讀成 # 「頁面不存在」,就會拿新範本蓋掉活著的頁,舊紀錄整份沒了。 # 這條規則的正本在 skills/wiki/SKILL.md 的 Rules 第 4 條。 -# 結束碼: 0=已更新或已新增 1=寫入失敗 2=用法錯誤 3=CONTENTS 存取庫未設定 +# 建新頁時剝掉範本的示範資料,只留 H1 與「>」引言。 +# 轉檔那一次若呼叫端給了範本,連引言一起換成範本那一份:轉檔只搬表格不動散文, +# 舊引言會一直講「每個存取庫一列」這種只對表格成立的話,誤導之後讀的人。 +# 頁面已經是條列格式、不需要轉檔時引言原樣不動,那時呼叫端只是更新自己那一筆, +# 沒有理由改別人寫的散文;沒給範本也保留舊引言,因為沒有正本可換。 +# 結束碼: 0=已更新或已新增 1=組不出頁面內容或寫入失敗 2=用法錯誤 3=CONTENTS 存取庫未設定 # 4=頁不存在且沒給範本 7=金鑰失效或權限不足 8=其他 API 失敗 set -eu @@ -28,18 +51,253 @@ gitea="$dir/gitea.sh" page_name="$dir/page-name.sh" usage() { - echo 'usage: wiki-contents.sh upsert <TYPE> <key-col> <key> <row-file> [template-file]' >&2 + echo 'usage: wiki-contents.sh upsert <TYPE> <key-col> <key> <entry-file> [template-file]' >&2 + echo ' wiki-contents.sh format <key-col> <key> <entry-file> <old-file> <new-file> [template-file|--fresh]' >&2 exit 2 } -[ "${1-}" = upsert ] || usage +check_keycol() { + case "$1" in + ''|*[!0-9]*) echo "key-col must be a positive integer: $1" >&2; exit 2 ;; + esac + [ "$1" -ge 1 ] || { echo "key-col must be a positive integer: $1" >&2; exit 2; } +} + +# 轉檔與 upsert 只有這一份實作。upsert 與 format 共用它,離線驗證跑的就是正式路徑那一段。 +# 參數:舊頁檔 區塊檔 鍵欄序號 鍵 輸出檔 是否為新建(1 或 0) 範本檔(沒有就給空字串) +render() { + python3 - "$1" "$2" "$3" "$4" "$5" "$6" "$7" <<'PY' +import re +import sys +from urllib.parse import unquote + +old_path, entry_path, keycol, key, new_path, fresh, template_path = sys.argv[1:8] +keycol = int(keycol) +fresh = fresh == '1' +key = key.strip() + +lines = open(old_path, encoding='utf-8').read().split('\n') +entry = open(entry_path, encoding='utf-8').read().strip('\n') + + +def cells(line): + s = line.strip() + if not s.startswith('|'): + return None + s = s[1:] + if s.endswith('|'): + s = s[:-1] + return [c.strip() for c in s.split('|')] + + +def is_sep(cs): + return bool(cs) and all(c and set(c) <= set('-: ') for c in cs) + + +def plain(v): + # 欄名只留文字。留著連結語法或反引號,條列的欄位名就跟頁面上寫的不一樣。 + v = re.sub(r'\[\[([^\]|]*)\|([^\]]*)\]\]', r'\1', v) + v = re.sub(r'\[\[([^\]]*)\]\]', r'\1', v) + v = re.sub(r'\[([^\]]*)\]\([^)]*\)', r'\1', v) + return v.replace('`', '').strip() + + +def last_segment(url): + """取網址最後一段路徑,也就是 .../wiki/{頁名} 的頁名。""" + u = url.strip().split('#', 1)[0].split('?', 1)[0] + parts = [p for p in u.split('/') if p] + seg = parts[-1] if parts else '' + # 頁名有空白或中文時網址會被百分號編碼,解碼後才是頁面上看到的頁名。 + return unquote(seg).replace('`', '').strip() + + +def title_from_cell(v): + # 身分欄的連結文字常常不是頁名,是工作包名稱或計畫名稱;真正的頁名在網址最後一段。 + # 拿連結文字當 H2 標題,就跟呼叫端傳進來的鍵對不上,同一筆會長出第二個區塊。 + m = re.search(r'\[[^\]]*\]\(([^)]*)\)', v) + if m: + return last_segment(m.group(1)) + # wiki 連結 [[頁名|文字]] 的目標寫在前半段,那一段就是頁名。 + m = re.search(r'\[\[([^\]|]*)(?:\|[^\]]*)?\]\]', v) + if m: + return m.group(1).replace('`', '').strip() + # 沒有連結就是純文字身分欄,例如 {owner}/{repo}。取到什麼就用什麼,不判形狀。 + return v.replace('`', '').strip() + + +def split_tables(src): + """把每一段 markdown 表格從行清單裡拿掉。回傳剩下的行與各表格的列。""" + rest = [] + tables = [] + i = 0 + fence = False + while i < len(src): + line = src[i] + # 程式碼圍欄裡的「|」是內容不是表格。mermaid 圖與範例被當表格拆掉,引言就毀了。 + if line.lstrip().startswith('```'): + fence = not fence + rest.append(line) + i += 1 + continue + if not fence and cells(line) is not None: + j = i + while j < len(src) and cells(src[j]) is not None: + j += 1 + rows = [cells(x) for x in src[i:j]] + if len(rows) >= 2 and is_sep(rows[1]): + tables.append(rows) + else: + # 沒有分隔列就不是表格,原樣留著。 + rest.extend(src[i:j]) + i = j + continue + rest.append(line) + i += 1 + return rest, tables + + +def table_blocks(rows): + """一列一個 H2 區塊,欄位順序照表頭從左到右。""" + head = rows[0] + out = [] + for cs in rows[2:]: + if is_sep(cs): + continue + if not any(c for c in cs): + continue + title = title_from_cell(cs[keycol - 1]) if len(cs) >= keycol else '' + if not title: + # 取不出身分就不猜標題。猜錯的標題比不到任何鍵,之後每次 upsert 都在它旁邊 + # 再長一筆;停下來讓人看那一列,比留一筆對不上的紀錄安全。 + sys.stderr.write( + '[jsc][gitea][ERR]:表格有一列取不出第 %d 欄的鍵,轉不成區塊。\n' % keycol) + raise SystemExit(1) + body = [] + for n, name in enumerate(head): + label = plain(name) or ('欄位%d' % (n + 1)) + value = cs[n].strip() if n < len(cs) else '' + body.append('- %s:%s' % (label, value)) + out.append('## %s\n\n%s' % (title, '\n'.join(body))) + return out + + +def split_blocks(src): + """切成引言與各 H2 區塊。第一個「## 」之前的都是引言。""" + pre = [] + blocks = [] + cur = None + fence = False + for line in src: + if line.lstrip().startswith('```'): + fence = not fence + if not fence and line.startswith('## '): + cur = [line] + blocks.append(cur) + continue + (cur if cur is not None else pre).append(line) + return pre, ['\n'.join(b).strip('\n') for b in blocks] + + +def preamble(path): + """取一份檔案第一個「## 」之前的內容,也就是 H1 加「>」引言那一段。""" + src = open(path, encoding='utf-8').read().split('\n') + # 先拆掉表格:範本若在引言之前放了示範表格,照搬進去就等於在目錄頁上留下表格。 + head, _ = split_blocks(split_tables(src)[0]) + return head + + +rest, tables = split_tables(lines) +pre, blocks = split_blocks(rest) + +# 範本的示範區塊會被當成真的一筆。照抄進新頁,那一筆就永遠留著,之後每次 upsert 都 +# 比不到它的鍵而跳過,正式頁上多出一筆指向不存在的頁的死紀錄。所以建新頁只留引言。 +if fresh: + blocks = [] +else: + # 有表格就代表這一頁還是舊版面,這一次要轉檔。 + converting = bool(tables) + for rows in tables: + blocks.extend(table_blocks(rows)) + # 轉檔只搬表格、不動散文,舊引言就會一直講只對表格成立的話。範本的引言是正本, + # 轉檔正好是換掉它的時機。不轉檔就不動引言:那時呼叫端只是更新自己那一筆。 + if converting and template_path: + tpl_pre = preamble(template_path) + # 範本沒有引言時保留舊的,換成空白等於把 H1 也弄掉。 + if '\n'.join(tpl_pre).strip(): + pre = tpl_pre + +# 鍵就是標題,所以標題一律重寫成 key。兩者不一致的話,這一筆下一次就找不回來。 +body = entry.split('\n') +if body and body[0].startswith('## '): + body = body[1:] +while body and not body[0].strip(): + body = body[1:] +block = '## %s' % key +if body: + block += '\n\n' + '\n'.join(body).strip('\n') + +hit = -1 +for i, b in enumerate(blocks): + if b.split('\n', 1)[0][3:].strip() == key: + hit = i + break + +if hit >= 0: + blocks[hit] = block + action = 'updated' +else: + blocks.append(block) + action = 'added' + +head = '\n'.join(pre).strip('\n') +parts = ([head] if head else []) + blocks +text = '\n\n'.join(parts) +if not text.strip(): + sys.stderr.write('[jsc][gitea][ERR]:組不出頁面內容,不寫入。\n') + raise SystemExit(1) + +open(new_path, 'w', encoding='utf-8').write(text + '\n') +print(action) +PY +} + +cmd="${1-}" +[ "$cmd" = upsert ] || [ "$cmd" = format ] || usage shift + +if [ "$cmd" = format ]; then + [ "$#" -ge 5 ] && [ "$#" -le 6 ] || usage + keycol="$1" + key="$2" + entryfile="$3" + oldfile="$4" + newfile="$5" + fresh=0 + template='' + if [ "$#" -eq 6 ]; then + if [ "$6" = --fresh ]; then + fresh=1 + else + template="$6" + fi + fi + check_keycol "$keycol" + [ -n "$key" ] || { echo 'key required' >&2; exit 2; } + [ -f "$entryfile" ] || { echo "找不到區塊檔案: $entryfile" >&2; exit 2; } + [ -f "$oldfile" ] || { echo "找不到舊頁檔案: $oldfile" >&2; exit 2; } + [ -z "$template" ] || [ -f "$template" ] || { echo "找不到範本檔: $template" >&2; exit 2; } + rc=0 + render "$oldfile" "$entryfile" "$keycol" "$key" "$newfile" "$fresh" "$template" || rc=$? + [ "$rc" -eq 0 ] || exit 1 + exit 0 +fi + [ "$#" -ge 4 ] && [ "$#" -le 5 ] || usage type=$(printf '%s' "${1-}" | tr a-z A-Z) keycol="${2-}" key="${3-}" -rowfile="${4-}" +entryfile="${4-}" template="${5-}" [ -n "$type" ] || usage @@ -48,12 +306,9 @@ page="${type}_CONTENTS" # 它連 CONTENTS 一起擋掉——CONTENTS 只用來解存取庫,沒有 CONTENTS_CONTENTS 這一頁。 sh "$page_name" check "$page" >/dev/null 2>&1 || { echo "不能用來組目錄頁頁名的頁型: $type" >&2; usage; } -case "$keycol" in - ''|*[!0-9]*) echo "key-col must be a positive integer: $keycol" >&2; exit 2 ;; -esac -[ "$keycol" -ge 1 ] || { echo "key-col must be a positive integer: $keycol" >&2; exit 2; } +check_keycol "$keycol" [ -n "$key" ] || { echo 'key required' >&2; exit 2; } -[ -f "$rowfile" ] || { echo "找不到列檔案: $rowfile" >&2; exit 2; } +[ -f "$entryfile" ] || { echo "找不到區塊檔案: $entryfile" >&2; exit 2; } [ -z "$template" ] || [ -f "$template" ] || { echo "找不到範本檔: $template" >&2; exit 2; } # 存取庫解析失敗照原碼傳出去:3 是「沒設定」,2 是型別不認得,兩者處置不同。 @@ -88,83 +343,7 @@ case "$rc" in esac rc=0 -action=$(python3 - "$old" "$rowfile" "$keycol" "$key" "$new" "$fresh" <<'PY' -import sys - -old_path, row_path, keycol, key, new_path, fresh = sys.argv[1:7] -keycol = int(keycol) -fresh = fresh == '1' - -lines = open(old_path, encoding='utf-8').read().split('\n') -row = open(row_path, encoding='utf-8').read().strip('\n') - - -def cells(line): - s = line.strip() - if not s.startswith('|'): - return None - s = s[1:] - if s.endswith('|'): - s = s[:-1] - return [c.strip() for c in s.split('|')] - - -def is_sep(cs): - return bool(cs) and all(c and set(c) <= set('-: ') for c in cs) - - -# 範本表格的示範列落在分隔列之後,會被當成真的資料列。照抄進新頁,那一列就永遠留著, -# 之後每次 upsert 都比不到它的鍵而跳過,正式頁上多出一條指向不存在的頁的死連結。 -# 所以建新頁時剝掉分隔列之後的所有資料列,只留標題、說明、表頭與分隔列。 -if fresh: - kept = [] - passed_sep = False - for line in lines: - cs = cells(line) - if cs is None: - kept.append(line) - continue - if is_sep(cs): - passed_sep = True - kept.append(line) - continue - if passed_sep: - continue - kept.append(line) - lines = kept - -# 分隔列之前的都是表頭。從分隔列之後才開始比對鍵,表頭第一欄剛好等於鍵時才不會被改掉。 -seen_sep = False -hit = -1 -last_row = -1 -for i, line in enumerate(lines): - cs = cells(line) - if cs is None: - continue - if is_sep(cs): - seen_sep = True - last_row = i - continue - if not seen_sep: - continue - last_row = i - if hit < 0 and len(cs) >= keycol and cs[keycol - 1] == key: - hit = i - -if hit >= 0: - lines[hit] = row - action = 'updated' -elif last_row >= 0: - lines.insert(last_row + 1, row) - action = 'added' -else: - sys.stderr.write('[jsc][gitea][ERR]:頁面裡找不到 markdown 表格,無處可放這一列。\n') - raise SystemExit(1) - -open(new_path, 'w', encoding='utf-8').write('\n'.join(lines)) -print(action) -PY -) || rc=$? +action=$(render "$old" "$entryfile" "$keycol" "$key" "$new" "$fresh" "$template") || rc=$? # 整不出正確的頁就不要送出去。組不出內容跟送出失敗一樣寫不進去,共用結束碼 1。 [ "$rc" -eq 0 ] || exit 1 From c7c14445e6549ce79a6bdddd97c57edff588c147 Mon Sep 17 00:00:00 2001 From: Jeffery <jiantw83@yahoo.com> Date: Wed, 2 Sep 2026 17:22:18 +0800 Subject: [PATCH 6/9] =?UTF-8?q?test(check-contents-format):=20=E6=96=B0?= =?UTF-8?q?=E5=A2=9E=E7=9B=AE=E9=8C=84=E9=A0=81=E5=8D=80=E5=A1=8A=E6=A0=BC?= =?UTF-8?q?=E5=BC=8F=E7=9A=84=E9=9B=A2=E7=B7=9A=E9=A9=97=E8=AD=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 新增一支只讀寫暫存檔的驗證腳本,全程走 format 子命令,涵蓋五種舊頁狀態: 純表格、已是條列且鍵命中、已是條列且鍵未命中、表格與區塊混合、用範本建 新頁。另外驗身分欄取標題的四種情形與引言的五種情形。 轉檔與 upsert 的錯法都是靜默的:標題取錯只會多長一個區塊,引言沒換掉只是 說明過期,兩種都不會報錯,要等到線上頁面壞掉才看得出來。手動打 API 驗又 會在正式頁上留下試出來的垃圾紀錄。 驗證只比對輸入與輸出檔,不碰網路,任何機器上都跑得完。除了正常流程還特別 釘住三件事:表格取不出鍵那一列要擋下來不猜標題、轉檔後拿結果重跑同一筆 必須一字不變、區塊檔沒帶標題也照樣補上。任一項不符就印出期望值與實際值 之後立刻停住,不續跑其餘項目。 功能範圍:目錄頁版面改版的迴歸防護。 --- tools/check-contents-format.sh | 518 +++++++++++++++++++++++++++++++++ 1 file changed, 518 insertions(+) create mode 100755 tools/check-contents-format.sh diff --git a/tools/check-contents-format.sh b/tools/check-contents-format.sh new file mode 100755 index 0000000..f495ff4 --- /dev/null +++ b/tools/check-contents-format.sh @@ -0,0 +1,518 @@ +#!/usr/bin/env sh +# check-contents-format.sh — 驗證目錄頁的區塊轉檔與 upsert 規則。 +# 全部走 wiki-contents.sh 的 format 子命令,只讀寫暫存檔,不打任何 API。 +# 涵蓋五種舊頁狀態:純表格、已是條列且鍵命中、已是條列且鍵未命中、表格與區塊混合、 +# 用範本建新頁。另外驗三件事:表格取不出鍵那一列要擋下來不猜標題、轉檔後重跑同一筆 +# 結果一字不變、區塊檔沒帶標題也照樣補上。 +# 轉檔取 H2 標題另外驗四種身分欄:連結文字不是頁名、連結文字剛好等於頁名、純文字沒有 +# 連結、網址帶百分號編碼。四種都要取到真正的頁名,呼叫端那一筆才會是 updated。 +# 引言另外驗五種情形:轉檔且有範本要換成範本那一份、轉檔但沒範本保留舊的、已是條列 +# 加了範本也不准動引言、--fresh 建新頁照舊、拿轉檔結果重跑引言不再變動。 +# 全部通過印 OK 並 exit 0;任一項不符印出 want 與 got 並 exit 1。 +# 結束碼: 0=全部通過,stdout 印 OK +# 1=有一項不符,stderr 印出 {項目}: want=… got=… 之後立刻停住,不續跑其餘項目。 +# 只有 0 與 1 兩種;這支不吃參數,也沒有用法錯誤那條路。 +set -eu + +dir=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd) +contents="$dir/wiki-contents.sh" + +work=$(mktemp -d) +trap 'rm -rf "$work"' EXIT + +fail() { + printf '%s\n' "$1" >&2 + exit 1 +} + +expect_eq() { # got want label + [ "$1" = "$2" ] || fail "$(printf '%s: want=\n%s\ngot=\n%s' "$3" "$2" "$1")" +} + +# run <label> <keycol> <key> <fresh|keep> [範本檔] — 讀 $work/old 與 $work/entry, +# 結果留在 $work/new,動作留在 $work/action。 +run() { + rc=0 + if [ "$4" = fresh ]; then + sh "$contents" format "$2" "$3" "$work/entry" "$work/old" "$work/new" --fresh > "$work/action" || rc=$? + elif [ -n "${5-}" ]; then + sh "$contents" format "$2" "$3" "$work/entry" "$work/old" "$work/new" "$5" > "$work/action" || rc=$? + else + sh "$contents" format "$2" "$3" "$work/entry" "$work/old" "$work/new" > "$work/action" || rc=$? + fi + [ "$rc" -eq 0 ] || fail "$1: want exit=0 got exit=$rc" +} + +# ---- 1. 純表格舊頁:整頁轉成區塊,再換掉鍵相同那一筆 ---- +cat > "$work/old" <<'EOF' +# 日誌目錄 + +> 由 `jsc-log:worklog` 維護。一個區塊代表一個日誌頁。 + +| 日誌頁 | 存取庫 | 條目數 | +| --- | --- | --- | +| [LOG_AAA](https://example.test/wiki/LOG_AAA) | plugins/meta | 3 | +| [LOG_BBB](https://example.test/wiki/LOG_BBB) | plugins/ask | 5 | +EOF +cat > "$work/entry" <<'EOF' +## LOG_BBB + +- 日誌頁:[LOG_BBB](https://example.test/wiki/LOG_BBB) +- 存取庫:plugins/ask +- 條目數:9 +EOF +run 'table page' 1 LOG_BBB keep +expect_eq "$(cat "$work/action")" updated 'table page action' +expect_eq "$(cat "$work/new")" '# 日誌目錄 + +> 由 `jsc-log:worklog` 維護。一個區塊代表一個日誌頁。 + +## LOG_AAA + +- 日誌頁:[LOG_AAA](https://example.test/wiki/LOG_AAA) +- 存取庫:plugins/meta +- 條目數:3 + +## LOG_BBB + +- 日誌頁:[LOG_BBB](https://example.test/wiki/LOG_BBB) +- 存取庫:plugins/ask +- 條目數:9' 'table page' + +# ---- 2. 已是條列,鍵命中:只換那一塊,別人那一筆一字不動 ---- +cat > "$work/old" <<'EOF' +# 日誌目錄 + +> 引言。 + +## LOG_AAA + +- 存取庫:plugins/meta +- 條目數:3 + +## LOG_BBB + +- 存取庫:plugins/ask +- 條目數:5 +EOF +cat > "$work/entry" <<'EOF' +## LOG_AAA + +- 存取庫:plugins/meta +- 條目數:7 +EOF +run 'list hit' 1 LOG_AAA keep +expect_eq "$(cat "$work/action")" updated 'list hit action' +expect_eq "$(cat "$work/new")" '# 日誌目錄 + +> 引言。 + +## LOG_AAA + +- 存取庫:plugins/meta +- 條目數:7 + +## LOG_BBB + +- 存取庫:plugins/ask +- 條目數:5' 'list hit' + +# ---- 3. 已是條列,鍵未命中:附加到頁尾,前面的區塊照舊 ---- +cat > "$work/entry" <<'EOF' +## LOG_CCC + +- 存取庫:plugins/git +- 條目數:1 +EOF +run 'list miss' 1 LOG_CCC keep +expect_eq "$(cat "$work/action")" added 'list miss action' +expect_eq "$(cat "$work/new")" '# 日誌目錄 + +> 引言。 + +## LOG_AAA + +- 存取庫:plugins/meta +- 條目數:3 + +## LOG_BBB + +- 存取庫:plugins/ask +- 條目數:5 + +## LOG_CCC + +- 存取庫:plugins/git +- 條目數:1' 'list miss' + +# ---- 4. 表格與區塊混合:表格轉出來的區塊接在既有區塊後面 ---- +cat > "$work/old" <<'EOF' +# 日誌目錄 + +> 引言。 + +## LOG_AAA + +- 存取庫:plugins/meta +- 條目數:3 + +| 日誌頁 | 存取庫 | +| --- | --- | +| [LOG_BBB](https://example.test/wiki/LOG_BBB) | plugins/ask | +EOF +cat > "$work/entry" <<'EOF' +## LOG_CCC + +- 日誌頁:[LOG_CCC](https://example.test/wiki/LOG_CCC) +- 存取庫:plugins/git +EOF +run 'mixed page' 1 LOG_CCC keep +expect_eq "$(cat "$work/action")" added 'mixed page action' +expect_eq "$(cat "$work/new")" '# 日誌目錄 + +> 引言。 + +## LOG_AAA + +- 存取庫:plugins/meta +- 條目數:3 + +## LOG_BBB + +- 日誌頁:[LOG_BBB](https://example.test/wiki/LOG_BBB) +- 存取庫:plugins/ask + +## LOG_CCC + +- 日誌頁:[LOG_CCC](https://example.test/wiki/LOG_CCC) +- 存取庫:plugins/git' 'mixed page' + +# ---- 5. 用範本建新頁:示範區塊與示範表格都要剝掉,只留 H1 與引言 ---- +cat > "$work/old" <<'EOF' +# 日誌目錄 + +> 由 `jsc-log:worklog` 維護。一個區塊代表一個日誌頁。 + +## {日誌頁頁名} + +- 存取庫:{owner}/{repo} +- 條目數:{數字} +EOF +cat > "$work/entry" <<'EOF' +## LOG_AAA + +- 存取庫:plugins/meta +- 條目數:3 +EOF +run 'fresh page' 1 LOG_AAA fresh +expect_eq "$(cat "$work/action")" added 'fresh page action' +expect_eq "$(cat "$work/new")" '# 日誌目錄 + +> 由 `jsc-log:worklog` 維護。一個區塊代表一個日誌頁。 + +## LOG_AAA + +- 存取庫:plugins/meta +- 條目數:3' 'fresh page' + +# ---- 6. 表格那一列取不出鍵:擋下來回 1,不猜標題 ---- +cat > "$work/old" <<'EOF' +# 日誌目錄 + +> 引言。 + +| 日誌頁 | 存取庫 | +| --- | --- | +| | plugins/ask | +EOF +cat > "$work/entry" <<'EOF' +## LOG_AAA + +- 存取庫:plugins/meta +EOF +if sh "$contents" format 1 LOG_AAA "$work/entry" "$work/old" "$work/new" >/dev/null 2>&1; then + code=0 +else + code=$? +fi +[ "$code" -eq 1 ] || fail "keyless table row: want exit=1 got exit=$code" + +# ---- 7. 轉檔後重跑同一筆:結果一字不變,程式碼圍欄不被當成表格拆掉 ---- +cat > "$work/old" <<'EOF' +# 監控目錄 + +> 引言。 + +```mermaid +flowchart LR + A --> B +``` + +| 監控頁 | 主機 | +| --- | --- | +| `MONITOR_AAA` | myhost | +EOF +cat > "$work/entry" <<'EOF' +## MONITOR_AAA + +- 監控頁:[MONITOR_AAA](https://example.test/wiki/MONITOR_AAA) +- 主機:myhost +EOF +run 'converted once' 1 MONITOR_AAA keep +expect_eq "$(cat "$work/action")" updated 'converted once action' +expect_eq "$(cat "$work/new")" '# 監控目錄 + +> 引言。 + +```mermaid +flowchart LR + A --> B +``` + +## MONITOR_AAA + +- 監控頁:[MONITOR_AAA](https://example.test/wiki/MONITOR_AAA) +- 主機:myhost' 'converted once' + +cp "$work/new" "$work/old" +run 'converted twice' 1 MONITOR_AAA keep +expect_eq "$(cat "$work/action")" updated 'converted twice action' +expect_eq "$(cat "$work/new")" "$(cat "$work/old")" 'converted twice' + +# ---- 8. 區塊檔沒帶「## 」標題:照樣補上,鍵就是標題 ---- +cat > "$work/old" <<'EOF' +# 監控目錄 + +> 引言。 +EOF +cat > "$work/entry" <<'EOF' +- 主機:myhost +EOF +run 'headless entry' 1 MONITOR_BBB keep +expect_eq "$(cat "$work/action")" added 'headless entry action' +expect_eq "$(cat "$work/new")" '# 監控目錄 + +> 引言。 + +## MONITOR_BBB + +- 主機:myhost' 'headless entry' + +# ---- 9. 連結文字不是頁名:標題取網址最後一段,這一筆算 updated 不是 added ---- +# 頁名一律走變數帶進來。這支腳本的每一行「## 開頭」都會被註解掃描器當成註解讀, +# 頁名直接寫在那種行上就會被判成註解夾帶頁面編號。 +page='ANALYZE_20260821_100552_104F0709' +cat > "$work/old" <<EOF +# 分析目錄 + +> 引言。 + +| 計畫名稱 | 分析頁 | HASH | +| --- | --- | --- | +| 某計畫 | [假 CLI 核心程式庫](https://example.test/knowledges/ANALYZE/wiki/$page) | 104F0709 | +EOF +printf '## %s\n\n- 計畫名稱:某計畫\n' "$page" > "$work/entry" +run 'link text differs' 2 "$page" keep +expect_eq "$(cat "$work/action")" updated 'link text differs action' +expect_eq "$(cat "$work/new")" "$(printf '# 分析目錄\n\n> 引言。\n\n## %s\n\n- 計畫名稱:某計畫' "$page")" 'link text differs' + +# ---- 10. 轉檔後重跑同一筆:仍是 updated,不多出區塊 ---- +cp "$work/new" "$work/old" +run 'link text differs twice' 2 "$page" keep +expect_eq "$(cat "$work/action")" updated 'link text differs twice action' +expect_eq "$(cat "$work/new")" "$(cat "$work/old")" 'link text differs twice' + +# ---- 11. 連結文字剛好等於頁名:結果與連結文字不是頁名時一致 ---- +page='PLAN_104F0709' +cat > "$work/old" <<EOF +# 計畫目錄 + +> 引言。 + +| 計畫頁 | 狀態 | +| --- | --- | +| [$page](https://example.test/knowledges/PLAN/wiki/$page) | 進行中 | +EOF +printf '## %s\n\n- 狀態:已完成\n' "$page" > "$work/entry" +run 'link text equals page' 1 "$page" keep +expect_eq "$(cat "$work/action")" updated 'link text equals page action' +expect_eq "$(cat "$work/new")" "$(printf '# 計畫目錄\n\n> 引言。\n\n## %s\n\n- 狀態:已完成' "$page")" 'link text equals page' + +# ---- 12. 身分欄是純文字沒有連結:標題就是那段文字 ---- +page='plugins/meta' +cat > "$work/old" <<EOF +# 維護目錄 + +> 引言。 + +| 存取庫 | 維護期限 | +| --- | --- | +| \`$page\` | 2026-12-31 | +EOF +printf '## %s\n\n- 維護期限:2027-06-30\n' "$page" > "$work/entry" +run 'plain identity cell' 1 "$page" keep +expect_eq "$(cat "$work/action")" updated 'plain identity cell action' +expect_eq "$(cat "$work/new")" "$(printf '# 維護目錄\n\n> 引言。\n\n## %s\n\n- 維護期限:2027-06-30' "$page")" 'plain identity cell' + +# ---- 13. 網址帶百分號編碼:解碼後取最後一段 ---- +page='ANALYZE_中文' +cat > "$work/old" <<'EOF' +# 分析目錄 + +> 引言。 + +| 分析頁 | HASH | +| --- | --- | +| [某工作包](https://example.test/knowledges/ANALYZE/wiki/ANALYZE_%E4%B8%AD%E6%96%87) | 104F0709 | +EOF +printf '## %s\n\n- HASH:104F0709\n' "$page" > "$work/entry" +run 'percent encoded url' 1 "$page" keep +expect_eq "$(cat "$work/action")" updated 'percent encoded url action' +expect_eq "$(cat "$work/new")" "$(printf '# 分析目錄\n\n> 引言。\n\n## %s\n\n- HASH:104F0709' "$page")" 'percent encoded url' + +# ---- 14. 轉檔且有範本:引言換成範本那一份,紀錄區塊一筆不少、順序照舊 ---- +# 範本的示範區塊不得混進來,所以範本檔也放一個示範區塊。 +cat > "$work/tpl" <<'EOF' +# 日誌目錄 + +> 由 `jsc-log:worklog` 維護。每個存取庫一個區塊;頁面上不留 markdown 表格。 +> +> 版面:H1 頁名、這段引言,然後一筆紀錄一個 H2 區塊。 + +## {日誌頁頁名} + +- 存取庫:{owner}/{repo} +- 條目數:{數字} +EOF +cat > "$work/old" <<'EOF' +# 日誌目錄 + +> 由 `jsc-log:worklog` 維護。每個存取庫一列;`{HASH}` 為 SHA-1 前 8 碼。 + +| 日誌頁 | 存取庫 | 條目數 | +| --- | --- | --- | +| [LOG_AAA](https://example.test/wiki/LOG_AAA) | plugins/meta | 3 | +| [LOG_BBB](https://example.test/wiki/LOG_BBB) | plugins/ask | 5 | +EOF +cat > "$work/entry" <<'EOF' +## LOG_BBB + +- 日誌頁:[LOG_BBB](https://example.test/wiki/LOG_BBB) +- 存取庫:plugins/ask +- 條目數:9 +EOF +run 'converting with template' 1 LOG_BBB keep "$work/tpl" +expect_eq "$(cat "$work/action")" updated 'converting with template action' +expect_eq "$(cat "$work/new")" '# 日誌目錄 + +> 由 `jsc-log:worklog` 維護。每個存取庫一個區塊;頁面上不留 markdown 表格。 +> +> 版面:H1 頁名、這段引言,然後一筆紀錄一個 H2 區塊。 + +## LOG_AAA + +- 日誌頁:[LOG_AAA](https://example.test/wiki/LOG_AAA) +- 存取庫:plugins/meta +- 條目數:3 + +## LOG_BBB + +- 日誌頁:[LOG_BBB](https://example.test/wiki/LOG_BBB) +- 存取庫:plugins/ask +- 條目數:9' 'converting with template' +expect_eq "$(grep -c '^## ' "$work/new")" 2 'converting with template block count' + +# ---- 15. 轉檔但沒給範本:沒有正本可換,引言保留舊的 ---- +run 'converting without template' 1 LOG_BBB keep +expect_eq "$(cat "$work/action")" updated 'converting without template action' +expect_eq "$(cat "$work/new")" '# 日誌目錄 + +> 由 `jsc-log:worklog` 維護。每個存取庫一列;`{HASH}` 為 SHA-1 前 8 碼。 + +## LOG_AAA + +- 日誌頁:[LOG_AAA](https://example.test/wiki/LOG_AAA) +- 存取庫:plugins/meta +- 條目數:3 + +## LOG_BBB + +- 日誌頁:[LOG_BBB](https://example.test/wiki/LOG_BBB) +- 存取庫:plugins/ask +- 條目數:9' 'converting without template' + +# ---- 16. 已是條列還給了範本:不必轉檔,引言一字不動 ---- +# 這一筆只是更新自己那一塊,沒有理由改掉別人寫的散文。 +cat > "$work/old" <<'EOF' +# 日誌目錄 + +> 這段引言是頁面主人自己寫的,不准被範本蓋掉。 + +## LOG_AAA + +- 存取庫:plugins/meta +- 條目數:3 +EOF +cat > "$work/entry" <<'EOF' +## LOG_AAA + +- 存取庫:plugins/meta +- 條目數:7 +EOF +run 'list page with template' 1 LOG_AAA keep "$work/tpl" +expect_eq "$(cat "$work/action")" updated 'list page with template action' +expect_eq "$(cat "$work/new")" '# 日誌目錄 + +> 這段引言是頁面主人自己寫的,不准被範本蓋掉。 + +## LOG_AAA + +- 存取庫:plugins/meta +- 條目數:7' 'list page with template' + +# ---- 17. --fresh 建新頁:整份用範本,示範區塊剝掉,行為與加了範本參數之前一樣 ---- +cp "$work/tpl" "$work/old" +cat > "$work/entry" <<'EOF' +## LOG_AAA + +- 存取庫:plugins/meta +- 條目數:3 +EOF +run 'fresh page with template intro' 1 LOG_AAA fresh +expect_eq "$(cat "$work/action")" added 'fresh page with template intro action' +expect_eq "$(cat "$work/new")" '# 日誌目錄 + +> 由 `jsc-log:worklog` 維護。每個存取庫一個區塊;頁面上不留 markdown 表格。 +> +> 版面:H1 頁名、這段引言,然後一筆紀錄一個 H2 區塊。 + +## LOG_AAA + +- 存取庫:plugins/meta +- 條目數:3' 'fresh page with template intro' + +# ---- 18. 拿轉檔結果再跑同一筆:已經沒有表格,引言不再變動 ---- +cat > "$work/old" <<'EOF' +# 日誌目錄 + +> 由 `jsc-log:worklog` 維護。每個存取庫一列;`{HASH}` 為 SHA-1 前 8 碼。 + +| 日誌頁 | 存取庫 | 條目數 | +| --- | --- | --- | +| [LOG_AAA](https://example.test/wiki/LOG_AAA) | plugins/meta | 3 | +| [LOG_BBB](https://example.test/wiki/LOG_BBB) | plugins/ask | 5 | +EOF +cat > "$work/entry" <<'EOF' +## LOG_BBB + +- 日誌頁:[LOG_BBB](https://example.test/wiki/LOG_BBB) +- 存取庫:plugins/ask +- 條目數:9 +EOF +run 'template intro once' 1 LOG_BBB keep "$work/tpl" +cp "$work/new" "$work/old" +run 'template intro twice' 1 LOG_BBB keep "$work/tpl" +expect_eq "$(cat "$work/action")" updated 'template intro twice action' +expect_eq "$(cat "$work/new")" "$(cat "$work/old")" 'template intro twice' + +printf '%s\n' 'OK' From 9942cf2506a02749af11cfce5ae3cb7a7ec2bedf Mon Sep 17 00:00:00 2001 From: Jeffery <jiantw83@yahoo.com> Date: Wed, 2 Sep 2026 17:22:24 +0800 Subject: [PATCH 7/9] =?UTF-8?q?fix(migrate-wiki):=20=E5=80=99=E9=81=B8?= =?UTF-8?q?=E9=8D=B5=E5=85=BC=E6=94=B6=E6=A2=9D=E5=88=97=E8=88=87=E8=A1=A8?= =?UTF-8?q?=E6=A0=BC=E5=85=A9=E7=A8=AE=E7=9B=AE=E9=8C=84=E9=A0=81=E5=BD=A2?= =?UTF-8?q?=E7=8B=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 搬頁時蒐集候選鍵的那一段,原本只讀目錄頁表格的存取庫、主機、帳號、工具、 期間五欄。現在改成 H2 區塊底下的「- {欄位名}:{值}」也照樣讀,表格的欄位 維持原樣收下,兩種形狀都認得。 目錄頁已經改成條列,還沒轉檔的線上舊頁卻仍是表格,同一輪搬頁會同時遇到 兩種。只讀表格的話,條列頁一個候選鍵都收不到,那些頁的舊頁名就對不到新 頁名,搬頁會把它們當成沒有對應而漏掉。 讀取改成逐行判斷:碰到「## 」就把上一個區塊收掉並開新的一筆,區塊內的 條列按欄位名對照收值。欄位值各自算一個候選鍵,再依固定欄位順序串一個組合 鍵,跟表格那一路的產出規則完全一致。條列的冒號正本寫全形,半形也一併收, 舊頁手寫的那幾條才不會整條漏掉。 功能範圍:目錄頁版面改版的搬頁相容。 --- tools/migrate-wiki.sh | 56 +++++++++++++++++++++++++++++++++++++------ 1 file changed, 49 insertions(+), 7 deletions(-) diff --git a/tools/migrate-wiki.sh b/tools/migrate-wiki.sh index 7f19f49..db45b08 100755 --- a/tools/migrate-wiki.sh +++ b/tools/migrate-wiki.sh @@ -14,7 +14,9 @@ # --key 可重複,補充自動蒐集不到的候選鍵。 # # 候選鍵來源: -# 目錄頁表格的存取庫、主機、帳號、工具、期間欄,以及這幾欄依序串成的組合鍵。 +# 目錄頁每一筆的存取庫、主機、帳號、工具、期間,以及這幾項依序串成的組合鍵。 +# 目錄頁是 H2 區塊加條列的形狀,所以讀「- {欄位名}:{值}」那幾條;還沒轉檔的舊頁 +# 仍是 markdown 表格,所以表格的欄位也照樣讀,兩種形狀都收。 # 內容頁的 H1 標題(CHECK 頁的 H1 直接就是 {主機}/{帳號})。 # --key 補充的候選。 # @@ -136,7 +138,7 @@ for ty in $TYPES; do done # ---- 第二輪:蒐集候選鍵 ---- -# 目錄頁表格的欄位值,以及依序串成的組合鍵 +# 目錄頁每一筆的欄位值,以及依序串成的組合鍵 while IFS="$TAB" read -r orepo opage nrepo npage <&3; do [ -n "$opage" ] || continue case "$opage" in *_CONTENTS) ;; *) continue ;; esac @@ -174,9 +176,49 @@ def plain(v): idx = {} seen_sep = False out = [] +block = None + + +def take(found): + """一筆紀錄收到的欄位值,各自算一個候選鍵,依 WANT 順序再串一個組合鍵。""" + if not found: + return + parts = [] + for w in WANT: + v = found.get(w) + if not v: + continue + out.append(v) + parts.append(v) + if len(parts) > 1: + out.append('/'.join(parts)) + + for line in lines: + # 目錄頁一筆一個 H2 區塊,欄位在標題底下一條一條列。 + if line.startswith('## '): + take(block) + block = {} + idx, seen_sep = {}, False + continue cs = cells(line) if cs is None: + if block is not None: + s = line.strip() + if s.startswith('- '): + body = s[2:] + # 正本寫全形冒號;半形也收,舊頁手寫的那幾條才不會整條漏掉。 + for mark in (':', ':'): + if mark in body: + label, value = body.split(mark, 1) + label = plain(label) + for w in WANT: + if w in label: + v = plain(value) + if v: + block[w] = v + break + break idx, seen_sep = {}, False continue if is_sep(cs): @@ -193,7 +235,7 @@ for line in lines: continue if not idx: continue - parts = [] + found = {} for w in WANT: i = idx.get(w) if i is None or i >= len(cs): @@ -201,10 +243,10 @@ for line in lines: v = plain(cs[i]) if not v: continue - out.append(v) - parts.append(v) - if len(parts) > 1: - out.append('/'.join(parts)) + found[w] = v + take(found) + +take(block) for v in out: print(v) From 89463cd2632bdeb6aa7614fd60522e282273c815 Mon Sep 17 00:00:00 2001 From: Jeffery <jiantw83@yahoo.com> Date: Wed, 2 Sep 2026 17:22:29 +0800 Subject: [PATCH 8/9] =?UTF-8?q?docs(wiki):=20=E5=90=8C=E6=AD=A5=E7=9B=AE?= =?UTF-8?q?=E9=8C=84=E9=A0=81=E6=A2=9D=E5=88=97=E6=A0=BC=E5=BC=8F=E7=9A=84?= =?UTF-8?q?=E6=95=98=E8=BF=B0=E8=88=87=E5=B7=A5=E5=85=B7=E7=94=A8=E6=B3=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit README 的工具用法、技能行為清單、連結規則與 wiki 技能本文,全部改寫成 目錄頁的區塊格式:參數名從整列改成整個區塊、key-col 的用途縮回只給轉檔 用、補上取標題與換引言的判準,並登錄新增的驗證腳本。 文件與工具講的不是同一件事,呼叫端就會照舊敘述傳整列的表格文字進去,寫出 半表格半條列的頁面。key-col 的語意變動最容易誤解——它從「要換掉哪一欄」 變成「轉檔時哪一欄持有身分」,敘述不改就會被填成別的欄位。 目錄頁條列、內容頁維持圖表優先,這個區分在每一份文件裡都寫明,避免把改版 範圍誤讀成整個 wiki。結束碼說明一併對回工具現況。 功能範圍:目錄頁版面改版的文件同步。 --- README.md | 22 ++++++++++++++++------ references/behaviors.md | 10 +++++----- references/wiki-links.md | 2 +- skills/wiki/SKILL.md | 14 +++++++------- 4 files changed, 29 insertions(+), 19 deletions(-) diff --git a/README.md b/README.md index b1640ac..cb7b9f1 100644 --- a/README.md +++ b/README.md @@ -66,13 +66,22 @@ page-name.sh regex | check <page> # 頁名樣式的唯一正本 # 8 碼與 H 加 7 碼留給尚未遷移的舊頁;舊演算法多數情況會加 H,兩種都要收 # 前綴只收十四種內容型別。CONTENTS 只解存取庫,沒有 CONTENTS_CONTENTS 這一頁 # 結束碼 0=合法、1=不合法、2=用法錯誤 -wiki-contents.sh upsert <TYPE> <key-col> <key> <row-file> [template-file] - # 目錄頁的整列 upsert:解 CONTENTS 存取庫、讀舊頁、換掉鍵相同那一列或附加到表尾、整頁寫回 - # key-col 是 1 起算的欄位序號,不是欄位名稱 - # 用範本建新頁時剝掉分隔列之後的示範列,正式頁上不留佔位的死連結 - # 結束碼 0=已更新或已新增、1=寫入失敗、2=用法錯誤、3=CONTENTS 存取庫未設定 +wiki-contents.sh upsert <TYPE> <key-col> <key> <entry-file> [template-file] + # 目錄頁的區塊 upsert:解 CONTENTS 存取庫、讀舊頁、換掉「## {key}」那一塊或附加到頁尾、整頁寫回 + # 目錄頁一筆一個 H2 區塊:標題就是內容頁頁名,欄位是底下一層條列「- {欄位名}:{值}」 + # entry-file 放整個 H2 區塊;舊頁還是表格時先整頁轉成區塊,再做 upsert + # key-col 只給轉檔用:舊表格裡持有身分那一欄的序號,1 起算,頁面已是條列就忽略 + # 轉檔的 H2 標題只看那一格:有連結取網址最後一段路徑(百分號編碼先解碼),沒連結取純文字 + # 連結文字常常是工作包或計畫名稱不是頁名;拿它當標題會跟呼叫端的鍵對不上,同一筆長出第二個區塊 + # 轉檔那一次有給範本,就連 H1 與「>」引言一起換成範本那一份:只搬表格不動散文,舊引言會一直講「一列一筆」 + # 頁面已是條列就不動引言,那時只是更新自己那一筆;沒給範本也保留舊引言,沒有正本可換 + # 用範本建新頁時剝掉示範區塊,只留 H1 與引言,正式頁上不留佔位的死紀錄 + # 結束碼 0=已更新或已新增、1=組不出頁面內容或寫入失敗、2=用法錯誤、3=CONTENTS 存取庫未設定 # 4=頁不存在且沒給範本、7=金鑰失效或權限不足、8=其他 API 失敗 # 只有 4 才准建新頁;7 與 8 一律中止,不得當成「頁面不存在」 +wiki-contents.sh format <key-col> <key> <entry-file> <old-file> <new-file> [template-file|--fresh] + # 同一份轉檔與 upsert 判斷,只讀寫檔案、不碰 API,供離線驗證用 + # 第六個參數給 --fresh 是「old-file 就是範本」,給路徑則等同 upsert 的範本檔 migrate-wiki.sh [--apply] [--key <候選鍵>]... # 把舊頁搬到新規則:目錄頁換存取庫、內容頁換頁名 # 只做正推配對,配不上的一律列成孤兒,不猜;不帶 --apply 只印對照表 # 寫目的地之前先讀:只有 4 才准寫,0 列成需人工確認且不覆蓋,7 與 8 中止 @@ -81,6 +90,7 @@ 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 與頁名樣式規則 +check-contents-format.sh # 驗證目錄頁的區塊轉檔與 upsert 規則;全走 format 子命令,不打 API link-check.sh <網址>... # 連結寫進文件之前先驗證連得到;也吃標準輸入,一行一個 # 每個網址一行「{OK|DEAD|SKIP}<TAB>{網址}<TAB>{說明}」 # wiki 頁與議題轉成 API 查,其他 Gitea 網址帶金鑰 HEAD,外部網址不帶金鑰 HEAD @@ -150,7 +160,7 @@ html-render.sh --markdown <檔案> --title <標題> --out <輸出檔> [--layout] ### `wiki` -Gitea wiki 頁讀寫的統一入口:依頁面類型(QUESTION / PLAN / ANALYZE / DELIVER / MAINTAIN / REPO / LOG / LEARN / ERROR / CHECK / REPORT / SKILLSET / TOOLING / MONITOR / CONTENTS)解析 wiki 所在的 `{owner}/{repo}`,先讀對應的 `JSC_WIKI_REPO_{TYPE}`,再退回 `JSC_WIKI_REPO`,不同類型不可互相代用。所有 `*_CONTENTS` 頁一律走 `CONTENTS` 這個型別,內容頁走自己的型別;目錄頁的整列 upsert 交給 `tools/wiki-contents.sh`。頁面內容以圖表優先(mermaid 圖、markdown 表格),純文字每節最多三句。 +Gitea wiki 頁讀寫的統一入口:依頁面類型(QUESTION / PLAN / ANALYZE / DELIVER / MAINTAIN / REPO / LOG / LEARN / ERROR / CHECK / REPORT / SKILLSET / TOOLING / MONITOR / CONTENTS)解析 wiki 所在的 `{owner}/{repo}`,先讀對應的 `JSC_WIKI_REPO_{TYPE}`,再退回 `JSC_WIKI_REPO`,不同類型不可互相代用。所有 `*_CONTENTS` 頁一律走 `CONTENTS` 這個型別,內容頁走自己的型別;目錄頁的區塊 upsert 交給 `tools/wiki-contents.sh`,一筆紀錄一個 H2 區塊,標題就是內容頁頁名,欄位是底下一層條列。內容頁的內容以圖表優先(mermaid 圖、markdown 表格),純文字每節最多三句;目錄頁不放表格。 ### `repo-sync` diff --git a/references/behaviors.md b/references/behaviors.md index fbea735..eb527a1 100644 --- a/references/behaviors.md +++ b/references/behaviors.md @@ -36,11 +36,11 @@ | 項目 | 內容 | | --- | --- | -| 觸發時機 | 技能組裡任何一次 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、兩個都沒有才問使用者,而且不借用別的頁型的存放庫。頁面裡的連結一律寫成 [文字](絕對網址),網址取自 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 讀一次。收尾一律呼叫 jsc-hooks/tools/report-status.sh skill-end jsc-gitea:wiki 寫下這一輪的結果,走每一條出口,連停在主機閘門那一條也要寫;這支被幾乎每支技能呼叫,只回報這一次 wiki 動作的成敗,不回報呼叫端自己的結果。status 五選一,讀到頁面或寫入落地是 ok,主機沒值、或 wiki-repo 回 3 而使用者沒給 {owner}/{repo}、整輪沒讀也沒寫是 blocked,金鑰失效那個結束碼 7 算 failed 不算頁面不存在、結束碼 8 與 link-check 回 1 擋下寫入也是 failed,內容頁寫成功但目錄頁沒更新(wiki-contents.sh 回 3 或 1)是 degraded、搬移只搬掉一部分也是 degraded,人工確認被否決或頁型不在允許清單而停在 wiki-repo 回 2 是 aborted。detail 只放頁名,不放頁面內容。腳本不在這台機器就安靜跳過,回報失敗不得改變回給呼叫端的結果。 | -| 外部呼叫 | 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、jsc-hooks/tools/report-status.sh skill-end、Gitea 的 wiki API 與議題 API。 | -| 完成條件 | 讀取動作拿到頁面內容,或拿到一個講得清楚的結束碼。這一輪還要留下一筆 skill-end 事件,或是腳本不在而略過,兩者都算收好;略過不影響回給呼叫端的結束碼。寫入動作先讓 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 會各留下一次人工確認。$JSC_HOME/usage/events.jsonl 會多一筆 {kind:skill,phase:end} 事件,name 是 jsc-gitea:wiki,status 與 exit 就是這一次 wiki 動作的結果。只做讀取的呼叫沒有 wiki 寫入跡象,只有回報內容與那一筆事件。 | +| 觸發時機 | 技能組裡任何一次 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、兩個都沒有才問使用者,而且不借用別的頁型的存放庫。頁面裡的連結一律寫成 [文字](絕對網址),網址取自 wiki-url,不自行組路徑;wiki-put 之前先把這一頁要放的每一條連結交給 tools/link-check.sh,結束碼 0 才寫,1 就不寫並回報 DEAD 那幾筆,7 停下來回報金鑰問題。內容頁的內容以圖表優先,目錄頁不放表格:一筆紀錄一個 H2 區塊,標題就是那一筆對應的內容頁頁名,欄位是標題底下一層條列「- {欄位名}:{值}」。目錄頁的區塊 upsert 交給 tools/wiki-contents.sh,參數是 upsert {TYPE} {key-col} {key} {區塊檔} [範本檔]:它先 wiki-get 讀回舊內容,舊頁還是 markdown 表格就整頁轉成區塊,再找「## {key}」,命中換掉整塊、沒命中附加到頁尾,最後整頁寫回;key-col 只給轉檔認舊表格的身分欄用,頁面已是條列就忽略;轉檔的 H2 標題只看身分欄那一格,有連結就取網址最後一段路徑、百分號編碼先解碼,沒連結才取格子純文字,取到什麼就用什麼,不拿頁名樣式去驗;轉檔那一次呼叫端有給範本,就把第一個「## 」之前的 H1 與「>」引言換成範本那一段,範本的示範區塊不得混進來,因為轉檔只搬表格不動散文,舊引言會一直講「每個存取庫一列」這種只對表格成立的話,頁面已是條列或沒給範本則引言原樣不動。只有結束碼 4 才准用範本建新頁,7 與 8 一律中止;建新頁時範本的示範區塊要剝掉。舊頁搬到新規則走 tools/migrate-wiki.sh,不帶 --apply 只印對照表,寫每一個目的地之前也一樣先 wiki-get 讀一次。收尾一律呼叫 jsc-hooks/tools/report-status.sh skill-end jsc-gitea:wiki 寫下這一輪的結果,走每一條出口,連停在主機閘門那一條也要寫;這支被幾乎每支技能呼叫,只回報這一次 wiki 動作的成敗,不回報呼叫端自己的結果。status 五選一,讀到頁面或寫入落地是 ok,主機沒值、或 wiki-repo 回 3 而使用者沒給 {owner}/{repo}、整輪沒讀也沒寫是 blocked,金鑰失效那個結束碼 7 算 failed 不算頁面不存在、結束碼 8 與 link-check 回 1 擋下寫入也是 failed,內容頁寫成功但目錄頁沒更新(wiki-contents.sh 回 3,或回 1 組不出頁面內容、寫入沒落地)是 degraded、搬移只搬掉一部分也是 degraded,人工確認被否決或頁型不在允許清單而停在 wiki-repo 回 2 是 aborted。detail 只放頁名,不放頁面內容。腳本不在這台機器就安靜跳過,回報失敗不得改變回給呼叫端的結果。 | +| 外部呼叫 | 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 的 upsert 與 format、tools/migrate-wiki.sh、tools/write-confirm.sh、jsc-ask:ask、jsc-hooks/tools/report-status.sh skill-end、Gitea 的 wiki API 與議題 API。 | +| 完成條件 | 讀取動作拿到頁面內容,或拿到一個講得清楚的結束碼。這一輪還要留下一筆 skill-end 事件,或是腳本不在而略過,兩者都算收好;略過不影響回給呼叫端的結束碼。寫入動作先讓 tools/link-check.sh 回結束碼 0,再通過人工確認、wiki-put 回結束碼 0,而且送出去的是舊內容加上這次的異動,不是整頁覆蓋。目錄頁的異動只動自己那一個 H2 區塊:別人那幾筆一字不變,頁面上不留 markdown 表格,也不出現兩個同名的 H2 標題;只有從表格轉成區塊那一次,且呼叫端有給範本,引言才會換成範本那一份,其餘情況引言與別人那幾筆一起一字不動。連結檢查回 1 就不寫入,回 7 連同整個動作一起中止。wiki-put 的結束碼 7 與 8 一律中止整個動作,不建頁、不寫入、不用原參數重試。搬移動作要嘛全部搬完回 0,要嘛把失敗頁、孤兒頁、目的地已有內容的頁、指向被搬頁卻沒被搬的引用方逐條列出來,這四種一律交給人判斷。 | +| 可驗證跡象 | 目標 wiki 存放庫多一頁或改一頁。內容頁的頁名是 {型別}_{40 碼大寫十六進位},目錄頁是 {型別}_CONTENTS 且落在 JSC_WIKI_REPO_CONTENTS 指的那個存放庫。頁面內容是 UTF-8 繁體中文;內容頁以 mermaid 圖與 markdown 表格為主,散文每節最多三句。目錄頁只有三段:H1 頁名、「>」引言、一筆一個 H2 區塊,區塊之間空一行,H2 與第一條條列之間也空一行,H2 標題就是內容頁頁名而且不帶連結。頁面裡每一條連結都是 [文字](絕對網址),沒有 wiki 內部連結語法,而且每一條在寫入前都被 tools/link-check.sh 判成 OK。wiki-contents.sh 印出一行 updated 或 added 加上 {owner}/{repo}/{頁名}。寫入與刪除前 tools/write-confirm.sh 會各留下一次人工確認。$JSC_HOME/usage/events.jsonl 會多一筆 {kind:skill,phase:end} 事件,name 是 jsc-gitea:wiki,status 與 exit 就是這一次 wiki 動作的結果。只做讀取的呼叫沒有 wiki 寫入跡象,只有回報內容與那一筆事件。 | ## wiki-to-issue diff --git a/references/wiki-links.md b/references/wiki-links.md index beb694e..adcf705 100644 --- a/references/wiki-links.md +++ b/references/wiki-links.md @@ -8,7 +8,7 @@ | 情境 | 寫法 | | --- | --- | -| 目錄頁指向內容頁(例:`PLAN_CONTENTS` → `PLAN_{HASH}`) | `[PLAN_{HASH}](https://…/wiki/PLAN_…)` | +| 目錄頁指向內容頁(例:`PLAN_CONTENTS` → `PLAN_{HASH}`):連結寫在該筆 H2 區塊的條列裡,H2 標題本身只放頁名,不放連結 | `- 計畫頁:[PLAN_{HASH}](https://…/wiki/PLAN_…)` | | 內容頁指向目錄頁,或指向別的型別 | `[顯示文字](絕對網址)` | | 目錄頁之間、同型別的內容頁之間 | `[顯示文字](絕對網址)` | | 議題、PR、存放庫檔案 | `[顯示文字](絕對網址)` | diff --git a/skills/wiki/SKILL.md b/skills/wiki/SKILL.md index 7bba34a..61f78cd 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, 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. +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 upserted by tools/wiki-contents.sh as one H2 block per record, and {HASH} is the full 40-char uppercase SHA-1 from tools/hash-id. A contents page carries no markdown table - one H2 block per record, headed by that record's content-page name with one bullet per field - while a content page is chart-first, preferring 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 @@ -24,7 +24,7 @@ Different page types can live in different `{owner}/{repo}` repos, classified by | write page | write the content to a temp file first, then `tools/gitea.sh wiki-put {owner}/{repo} {page} {file}` (asks for confirmation first, then creates or updates) | | delete page | `tools/gitea.sh wiki-delete {owner}/{repo} {page}` — asks for the same confirmation as a write. Only a migration or an explicit user request may call it | | page URL | `tools/gitea.sh wiki-url {owner}/{repo} {page}` — the page's absolute URL, taken from the API's `html_url` | -| 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 | +| update a contents page | `tools/wiki-contents.sh upsert {TYPE} {key-col} {key} {entry-file} [template-file]` — resolves the CONTENTS repo, replaces the `## {key}` block, appends when the page holds no such heading, writes the whole page back. `{entry-file}` is that record's whole H2 block. `{key-col}` only matters while the old page is still a markdown table: it is the 1-based position of the column that carries the record's identity, and the script converts such a page to H2 blocks before the upsert, taking each heading from that cell's link URL — its last path segment, percent-decoded — or from the cell's plain text when the cell holds no link. On that conversion, a supplied `{template-file}` also replaces the page's H1 and `>` intro with the template's own, because a conversion moves the table alone and the old intro keeps describing rows; an already-converted page keeps its intro untouched, and so does a conversion run without a template | | 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 | @@ -57,8 +57,8 @@ Every code below gets its own branch. Nothing here is retried unchanged. | `tools/page-name.sh` | 0 | the page name follows the pattern | continue with that page name | | | 1 | the page name breaks the pattern | stop and report the name; build the correct one instead of writing to a wrong page | | | 2 | usage error | fix the arguments, then call again | -| `tools/wiki-contents.sh` | 0 | the row was updated or added | report which of the two, and the page | -| | 1 | the write failed, or the page held no markdown table | stop and report; fix the page or the row before calling again | +| `tools/wiki-contents.sh` | 0 | the block was updated or added | report which of the two, and the page | +| | 1 | the page content could not be built, or the write failed | stop and report; fix the page or the entry before calling again. A missing `## {key}` is not this code — that path appends | | | 2 | usage error, or an unknown page type | fix the arguments, then call again | | | 3 | the CONTENTS repo is not configured | go to step 3 and ask for `JSC_WIKI_REPO_CONTENTS` | | | 4 | the page is not there and no template was given | supply the template for that page type, then call again | @@ -89,7 +89,7 @@ This skill is called by almost every other one, so its status is what the caller | `ok` | The read returned the page, or the write landed: `link-check.sh` exited 0, the confirmation was given, and `wiki-put` exited 0 | | `blocked` | The location could not be resolved, so nothing was read and nothing was written: `GITEA_HOST` holds no value and the user gave none, or `wiki-repo` exited 3 and the user supplied no `{owner}/{repo}` for that page type | | `failed` | The operation ran and broke. **Exit 7 belongs here**: the key is invalid or lacks permission, so the whole operation stopped, and that is a failure, never an absent page. Exit 8, a `wiki-put` that did not land, a `link-check.sh` exit 1 that refused the write, and a `hash-id` or `page-name.sh` rejection all sit here too | -| `degraded` | The content page landed and the contents page did not — `wiki-put` on `{TYPE}_{HASH}` exited 0, then `wiki-contents.sh upsert` exited 3 with no CONTENTS repo configured, or exited 1 on a page holding no markdown table. The record exists but nothing indexes it, so the next reader will not find it. A migration that moved some pages and left orphans or occupied destinations behind sits here as well | +| `degraded` | The content page landed and the contents page did not — `wiki-put` on `{TYPE}_{HASH}` exited 0, then `wiki-contents.sh upsert` exited 3 with no CONTENTS repo configured, or exited 1 because the page content could not be built or the write did not land. The record exists but nothing indexes it, so the next reader will not find it. A migration that moved some pages and left orphans or occupied destinations behind sits here as well | | `aborted` | The premise did not hold or the user stopped it: `write-confirm.sh` was refused before a write or a delete, or the caller asked for a page type outside the allowed list and the run stopped at `wiki-repo` exit 2 | Completion condition: exactly one `skill-end` line was recorded for this run, or the script was absent and the run finished without it. @@ -98,14 +98,14 @@ Completion condition: exactly one `skill-end` line was recorded for this run, or 1. Page names must follow the wiki naming table in the skill guidelines (see `jsc-meta/references/guidelines.md`). Check any page name you build with `tools/page-name.sh check {page}` before it reaches an API call. 2. Use `tools/hash-id` for `{HASH}` values. It returns the full 40 uppercase SHA-1 hex chars — no truncation, no prefix. Never compute a hash by hand: a hand-made page name lands the content on a page nobody else reads. -3. To update a contents page (`*_CONTENTS`), run `tools/wiki-contents.sh upsert {TYPE} {key-col} {key} {row-file} [template-file]`, where `{key-col}` is the column's 1-based position number, not the column name. It reads the page, replaces the row whose key column equals the key, appends the row when no line matches, and writes the whole page back through the same confirmation. Never overwrite entries owned by others. Whether the page may be created from the template instead is decided by rule 4, and by nothing else. +3. **A contents page (`*_CONTENTS`) is one H2 block per record, never a table.** The H2 heading is that record's key, written as the content page's own name (`{TYPE}_{HASH}`) — no link, no URL, no prefix, no date. Every field is one bullet under it, `- {field}:{value}`, full-width colon, one bullet per field including the key's own. Update it with `tools/wiki-contents.sh upsert {TYPE} {key-col} {key} {entry-file} [template-file]`, where `{entry-file}` holds that whole H2 block. The script reads the page, converts a page still holding a markdown table into H2 blocks first, replaces the block whose heading equals `{key}`, appends the block when no heading matches, and writes the whole page back through the same confirmation. `{key-col}` is used only by that conversion: it is the 1-based position of the old table's identity column, and it is ignored once the page is already in block form. That conversion takes the heading from the identity cell's markdown link — the URL's last path segment, percent-decoded, because the link text is often a work-package or plan name rather than the page name — and from the cell's plain text, backticks stripped, only when the cell holds no link; it never checks the result against the page-name pattern, since old timestamped names, new 40-char names, and plain `{owner}/{repo}` identities all appear online. **That conversion is also the one moment the intro may be rewritten:** when `{template-file}` is given, everything before the first `## ` — the H1, the `>` intro, the blank lines between them — is replaced by the template's own preamble, taken the same way and carrying none of the template's demo blocks. Why: the conversion moves the table and leaves the prose, so an intro still saying "one row per repository" outlives the rows it describes, and the template holds the canonical wording. A page already in block form keeps its intro exactly as its owner wrote it — that call only updates its own record — and a conversion run without a template keeps the old intro, there being no canonical copy to install. Never overwrite entries owned by others. Whether the page may be created from the template instead is decided by rule 4, and by nothing else. 4. **Only exit 4 means the page is not there yet — this rule binds every "create it if it does not exist" path, without exception.** It is not limited to contents pages: a content page (`*_{HASH}`), a work log, an error page, a report, any page at all, follows the same branch. - **Correct branch.** Read the page with `wiki-get`. Exit 0 means the page exists, so append or modify the content that came back and `wiki-put` the whole page. Exit 4 (HTTP 404) is the one and only code that permits creating a new page from the template. - **Exit 7 and exit 8 abort.** Exit 7 (HTTP 401 or 403) and exit 8 (any other API failure) both mean the old content is unknown, never that the page is missing. Stop the operation and report the exit code with its cause. Create no page, write nothing, and do not retry the same call unchanged. - **Why.** Wiki writes in this skill set are append-not-overwrite, and that semantics rests entirely on reading the old page back first. Reading a 401 as a 404 makes the caller believe it holds a brand-new page and `wiki-put` a fresh template over a live one, and the whole earlier record is gone — the write carries no merge and no backup. - `tools/wiki-contents.sh` implements exactly this branch for contents pages and reports the same codes. 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. +6. **Chart-first applies to content pages (`*_{HASH}`) only.** On a content page, prefer visual forms: 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. **A contents page (`*_CONTENTS`) always takes the H2-heading-plus-bullets form of rule 3 instead** — no table, and no diagram, whatever the field count. Why: the heading is the key the upsert matches on, so the layout is fixed by the tool, not by which form reads better. 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. **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. From a229964dfe429e6911d007f8b68b1d36de49a2e6 Mon Sep 17 00:00:00 2001 From: Jeffery <jiantw83@yahoo.com> Date: Wed, 2 Sep 2026 17:22:35 +0800 Subject: [PATCH 9/9] =?UTF-8?q?chore(plugin):=20=E4=B8=89=E4=BB=BD=20manif?= =?UTF-8?q?est=20=E5=8D=87=E7=89=88=E8=87=B3=200.2.5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 三份 plugin manifest 的版本號同步升到 0.2.5。 目錄頁格式改動了工具的對外行為,其餘七個 domain 的範本要對上這一版才組得 出正確的區塊。版本號沒動,版本守衛就看不出機器上裝的是舊版工具。 三份一起改,維持既有的同版策略。 功能範圍:目錄頁版面改版的版本標記。 --- .claude-plugin/plugin.json | 2 +- .codex-plugin/plugin.json | 2 +- plugin.json | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.claude-plugin/plugin.json b/.claude-plugin/plugin.json index a9c234f..28d05ca 100644 --- a/.claude-plugin/plugin.json +++ b/.claude-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "jsc-gitea", - "version": "0.2.4", + "version": "0.2.5", "description": "Gitea API 工具、Wiki 讀寫、議題轉換、HTML 匯出與存取庫批次同步", "skills": "./skills", "author": { diff --git a/.codex-plugin/plugin.json b/.codex-plugin/plugin.json index 19cf779..e895b09 100644 --- a/.codex-plugin/plugin.json +++ b/.codex-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "jsc-gitea", - "version": "0.2.4", + "version": "0.2.5", "description": "Gitea API 工具、Wiki 讀寫、議題轉換、HTML 匯出與存取庫批次同步", "skills": "./skills", "jsc": { diff --git a/plugin.json b/plugin.json index c2d3810..85c8389 100644 --- a/plugin.json +++ b/plugin.json @@ -1,6 +1,6 @@ { "name": "jsc-gitea", - "version": "0.2.4", + "version": "0.2.5", "description": "Gitea API 工具、Wiki 讀寫、議題轉換、HTML 匯出與存取庫批次同步", "skills": "./skills/", "jsc": {