From 36fc4de73c03cd342115debcf1a28a86707d2d07 Mon Sep 17 00:00:00 2001 From: Jeffery Date: Tue, 25 Aug 2026 14:58:54 +0800 Subject: [PATCH 1/7] =?UTF-8?q?fix(gitea):=20=E8=A3=9C=E9=BD=8A=E7=A8=BD?= =?UTF-8?q?=E6=A0=B8=E7=BC=BA=E5=A4=B1=E4=B8=A6=E4=BF=AE=E6=8E=89=E8=AD=B7?= =?UTF-8?q?=E6=AC=84=E5=A4=B1=E6=95=88?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit What:依 jsc-meta:skill-check 的稽核結果修正技能與工具——補上每個步驟的可檢核完成條件、 把留在內文的標準輸入輸出流程下放 tools/、修正查表與退碼路由造成的誤判。 Why:稽核發現這些缺失會讓技能在實際執行時走錯分支或靜默通過。 完成條件缺漏是最常被違反的一項;退碼誤判與查表錯誤則會讓良性狀況被當成失敗。 How:逐項對照 references/guidelines.md 的審核檢查清單修正,新增的工具都有 documented exit codes,並以真實執行驗證每條路徑。 Who:jsc-meta:skill-check 例行稽核(2026-08-25)。 Co-Authored-By: Claude Opus 5 --- skills/repo-sync/SKILL.md | 17 ++++----- skills/wiki/SKILL.md | 29 ++++---------- tools/check-wiki-rules.sh | 80 ++++++++++++++++++++++----------------- tools/repo-sync.sh | 80 +++++++++++++++++++++++++++++++++++++++ 4 files changed, 142 insertions(+), 64 deletions(-) create mode 100755 tools/repo-sync.sh diff --git a/skills/repo-sync/SKILL.md b/skills/repo-sync/SKILL.md index a611bb3..18f66a1 100644 --- a/skills/repo-sync/SKILL.md +++ b/skills/repo-sync/SKILL.md @@ -1,18 +1,17 @@ --- name: repo-sync -description: Batch-sync all readable repos of a chosen Gitea owner into the working directory. List owners, let the user pick, then clone or update each repo; local changes become a branch, commit, push, and PR to develop or master. Use for workspace bootstrap or bulk refresh; not for a single repo. +description: Batch-sync all readable repos of a chosen Gitea owner into the working directory. List owners, let the user pick, then clone or update each repo through tools/repo-sync.sh; a repo reported dirty goes to jsc-git:pr against the base branch that same script reports. Use for workspace bootstrap or bulk refresh; not for a single repo. --- # repo-sync — batch-sync repositories ## Steps -1. Before calling `tools/gitea.sh`, resolve `GITEA_HOST` and `GITEA_TOKEN` from the current shell environment (also check `tea login list` for a usable login token when `GITEA_TOKEN` is unset). If `GITEA_HOST` is unresolvable, or `GITEA_TOKEN` is unset and no tea login token exists either, ask the user for the missing value per the `jsc-ask:ask` rules before proceeding to any `tools/gitea.sh` call. -2. Run `tools/gitea.sh owners` to list every `{owner}` the user can read. -3. Ask the user which `{owner}` to sync, per the `jsc-ask:ask` rules. Every option states the owner's repo count and impact scope. -4. Run `tools/gitea.sh repos {owner}` to list every readable `{repo}` under that owner. +1. Before calling `tools/gitea.sh`, resolve `GITEA_HOST` and `GITEA_TOKEN` from the current shell environment (also check `tea login list` for a usable login token when `GITEA_TOKEN` is unset). If `GITEA_HOST` is unresolvable, or `GITEA_TOKEN` is unset and no tea login token exists either, ask the user for the missing value per the `jsc-ask:ask` rules before proceeding to any `tools/gitea.sh` call. Done when `GITEA_HOST` holds a value and either `GITEA_TOKEN` or a tea login token is available. +2. Run `tools/gitea.sh owners` to list every `{owner}` the user can read. Done when the command has printed at least one `{owner}`. +3. Ask the user which `{owner}` to sync, per the `jsc-ask:ask` rules. Every option states the owner's repo count and impact scope. Done when the user has named exactly one `{owner}` from that list. +4. Run `tools/gitea.sh repos {owner}` to list every readable `{repo}` under that owner. Done when the command has printed the full `{owner}/{repo}` list for the chosen owner. 5. Sync each `{repo}` one by one. This step **MUST run as a sub agent** (one sub agent per repo): - 1. Missing locally → `git clone` into the working directory (clone URL from `tools/gitea.sh clone-url`). - 2. Present locally → switch to `develop`, else `master` (or the result of `tools/gitea.sh default-branch`), then `git pull`. - 3. Local file changes → create a branch from develop or master, commit via `jsc-git:commit`, push, then open a PR back to develop or master via `jsc-git:pr`. -6. Report the sync result for every repo: cloned, updated, PR created, or the failure reason. + 1. Run `tools/repo-sync.sh {owner}/{repo}`. The script owns the clone-versus-pull decision and the base-branch precedence, so run no `git clone`, `git checkout` or `git pull` by hand, and derive no branch name yourself. Route on its single line of output: `cloned` or `updated` → this repo is done; `dirty {branch}` → go to substep 2; `failed {reason}` → record that reason and stop this repo. Done when exactly one of those four outcomes is recorded for this repo. + 2. `dirty {branch}` → call `jsc-git:pr` with `{branch}` from that same output line as the base, passed through verbatim. It commits, branches, pushes and opens the PR itself, so add none of those steps. Done when `jsc-git:pr` returns the PR URL. +6. Report the sync result for every repo: cloned, updated, PR URL, or the failure reason. Done when every `{repo}` from step 4 carries one of those four results. diff --git a/skills/wiki/SKILL.md b/skills/wiki/SKILL.md index 5b8d7dd..5a54d0b 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 and tools/hash-id. Resolve the wiki repo per page type with JSC_WIKI_REPO_{TYPE} first, then JSC_WIKI_REPO, and ask only when neither is set. Page content is chart-first - prefer mermaid diagrams and markdown tables over plain prose. Used by jsc-ask, jsc-sdlc, and jsc-log for wiki pages, including ERROR pages, not for repo code files. +description: Read or write a Gitea wiki page through tools/gitea.sh and tools/hash-id. Resolve the wiki repo per page type with JSC_WIKI_REPO_{TYPE} first, then JSC_WIKI_REPO, and ask only when neither is set. Page content is chart-first - prefer mermaid diagrams and markdown tables over plain prose. Callers are jsc-ask, jsc-sdlc, jsc-log, and jsc-hooks for its ERROR pages. Use for any wiki page in the skill set; not for repo code files. --- # wiki — read and write Gitea wiki pages @@ -9,11 +9,11 @@ Every wiki operation in the jsc skill set goes through this skill. One entry poi ## Resolve the wiki location -Different page types can live in different `{owner}/{repo}` repos, classified by page-name prefix: `QUESTION`, `PLAN`, `ANALYZE`, `DELIVER`, `MAINTAIN`, `REPO`, `LOG`, `LEARN`, `ERROR`. +Different page types can live in different `{owner}/{repo}` repos, classified by the page-name prefix. -1. Before asking the user, inspect the current shell environment for the needed repo variables and Gitea connection variables: `JSC_WIKI_REPO_{TYPE}`, `JSC_WIKI_REPO`, `GITEA_HOST`, and `GITEA_TOKEN` (also check `tea login list` for a usable login token when `GITEA_TOKEN` is unset). Use inherited shell values first; only ask when the needed repo or connection value cannot be resolved after that check. `GITEA_HOST` and `GITEA_TOKEN` are both covered by this ask-if-unresolvable rule, the same as the wiki-repo variables below. -2. Run `tools/gitea.sh wiki-repo {TYPE}` (TYPE = the page-name prefix). Allowed types are `QUESTION`, `PLAN`, `ANALYZE`, `DELIVER`, `MAINTAIN`, `REPO`, `LOG`, `LEARN`, and `ERROR`. Resolution order is `JSC_WIKI_REPO_{TYPE}` first, then `JSC_WIKI_REPO`. Never borrow another type's repo. -3. On exit 3 (neither is set after env inspection), ask the user for that page type's `{owner}/{repo}` per the `jsc-ask:ask` rules, and suggest setting `JSC_WIKI_REPO_{TYPE}` (can differ per type) or `JSC_WIKI_REPO` (shared default). +1. Before asking the user, inspect the current shell environment for the needed repo variables and Gitea connection variables: `JSC_WIKI_REPO_{TYPE}`, `JSC_WIKI_REPO`, `GITEA_HOST`, and `GITEA_TOKEN` (also check `tea login list` for a usable login token when `GITEA_TOKEN` is unset). Use inherited shell values first; only ask when the needed repo or connection value cannot be resolved after that check. Done when every one of those variables is either resolved from the environment or listed as missing. +2. Run `tools/gitea.sh wiki-repo {TYPE}` (TYPE = the page-name prefix). Allowed types are `QUESTION`, `PLAN`, `ANALYZE`, `DELIVER`, `MAINTAIN`, `REPO`, `LOG`, `LEARN`, and `ERROR`. Resolution order is `JSC_WIKI_REPO_{TYPE}` first, then `JSC_WIKI_REPO`. Never borrow another type's repo. Done when the command has printed exactly one `{owner}/{repo}`, or exited 3 and sent this page type to step 3. +3. On exit 3 (neither is set after env inspection), ask the user for that page type's `{owner}/{repo}` per the `jsc-ask:ask` rules, and suggest setting `JSC_WIKI_REPO_{TYPE}` (can differ per type) or `JSC_WIKI_REPO` (shared default). Done when the user has supplied one `{owner}/{repo}` for that page type. ## Operations @@ -24,20 +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}` (creates or updates automatically) | | page URL | `tools/gitea.sh wiki-url {owner}/{repo} {page}` — the page's absolute URL, taken from the API's `html_url` (exit 4 when the page is missing) | -## Linking between wiki pages - -Two rules, and getting either wrong produces a link that silently points at a page that does not exist. - -**Direction**: Gitea uses the GitHub/Gollum convention — `[[display text|page name]]`, **display text on the LEFT, page name on the RIGHT**. This is the opposite of MediaWiki. Gitea's own source says so (`modules/markup/html_link.go`): *"MediaWiki uses [[link|text]], while GitHub uses [[text|link]] … we prefer GitHub syntax"*. So `[[PLAN_H1234567|我的計畫]]` renders as the text `PLAN_H1234567` linking to a page named 我的計畫 — broken. Write `[[我的計畫|PLAN_H1234567]]`. When display text and page name are the same, use the no-pipe form `[[PLAN_H1234567]]`, which cannot be got wrong. - -**Scope**: `[[...]]` and relative markdown links both resolve **only inside the current wiki**. There is no cross-repo wiki-link syntax. - -| Link | Same wiki? | Use | -| --- | --- | --- | -| Same page type (e.g. `PLAN_CONTENTS` → `PLAN_{HASH}`) | Always — one type, one repo | `[[display\|page]]` or `[[page]]` | -| Different page type (e.g. `LOG_{HASH}` → `PLAN_{HASH}`) | **Only when both types resolve to the same repo** | Absolute URL from `wiki-url`: `[display](https://…/wiki/PLAN_…)` | - -Because each type resolves its own `JSC_WIKI_REPO_{TYPE}`, a cross-type link **must** use the absolute URL — it stays correct whether or not the two types happen to share a repo, so never branch on that. Get the URL from `wiki-url`, never hand-assemble the path. +When writing a page, link same-type pages with the `[[display|page]]` form (display text on the LEFT) and cross-type pages with the absolute URL from `wiki-url`, because `[[...]]` resolves only inside one wiki. Full rules and the direction trap: `references/wiki-links.md`. ## Rules @@ -45,5 +32,5 @@ Because each type resolves its own `JSC_WIKI_REPO_{TYPE}`, a cross-type link **m 2. Use `tools/hash-id` for `{HASH}` values. It returns the first 8 uppercase SHA-1 hex chars, or `H` plus the first 7 chars when the raw hash starts with `0-9`, `A`, `B`, or `C`. 3. To update a contents page (`*_CONTENTS`): `wiki-get` it first, apply the template to append or modify, then `wiki-put` the whole page back. Never overwrite entries owned by others. 4. Write all wiki content in UTF-8 Traditional Chinese, per the STE100 output rule. -5. Prefer visual forms for page content: use mermaid diagrams (flowchart, sequence, gantt, pie) and markdown tables wherever the information allows. Plain running text is the last resort, kept short. -6. Authentication fallback is built into `tools/gitea.sh`: on a missing GITEA_TOKEN or a 401/403 response it retries with the tea CLI login token automatically. But before calling it, resolve `GITEA_HOST` and `GITEA_TOKEN` per rule 1: if `GITEA_HOST` is unresolvable, or `GITEA_TOKEN` is unset and no tea login token exists either, ask the user for the missing value per the `jsc-ask:ask` rules — same decision-tree pattern as the missing-wiki-repo case above — before calling `tools/gitea.sh`. Only report a genuine failure when the user has no answer to give or Gitea itself rejects the request (e.g. a 401/403 even after the tea fallback). +5. 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. `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. diff --git a/tools/check-wiki-rules.sh b/tools/check-wiki-rules.sh index 008f503..194fefc 100755 --- a/tools/check-wiki-rules.sh +++ b/tools/check-wiki-rules.sh @@ -1,11 +1,15 @@ #!/usr/bin/env sh # check-wiki-rules — 驗證 wiki repo 解析與 hash-id 規則。 +# 涵蓋全部九種頁面類型,每種三項:專用變數優先、退回共用變數、不得跨類型代用。 +# 全部通過印 OK 並 exit 0;任一項不符印出差異並 exit 1。 set -eu dir=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd) gitea="$dir/gitea.sh" hash_id="$dir/hash-id" +TYPES='QUESTION PLAN ANALYZE DELIVER MAINTAIN REPO LOG LEARN ERROR' + fail() { printf '%s\n' "$1" >&2 exit 1 @@ -18,7 +22,7 @@ expect_eq() { [ "$got" = "$want" ] || fail "$label: want=$want got=$got" } -check_repo() { +check_repo() { # type label want [VAR=值...] type=$1 label=$2 want=$3 @@ -27,6 +31,19 @@ check_repo() { expect_eq "$got" "$want" "$label" } +check_no_repo() { # type label [VAR=值...]:期望 exit 3 且不印出任何 {owner}/{repo} + type=$1 + label=$2 + shift 2 + if got=$(env -i PATH="${PATH:-/usr/bin:/bin}" "$@" "$gitea" wiki-repo "$type" 2>/dev/null); then + code=0 + else + code=$? + fi + [ "$code" -eq 3 ] || fail "$label: want exit=3 got exit=$code output=$got" + [ -z "$got" ] || fail "$label: want no output got=$got" +} + check_hash() { input=$1 want=$2 @@ -34,47 +51,42 @@ check_hash() { expect_eq "$got" "$want" "hash-id $input" } -check_repo REPO 'REPO specific wins' 'records/REPO' \ - JSC_WIKI_REPO_REPO='records/REPO' \ - JSC_WIKI_REPO_ANALYZE='knowledges/ANALYZE' \ - JSC_WIKI_REPO='shared/wiki' +# 其他八個類型的誘餌值。跨類型代用一旦發生,回傳的就會是 decoy/{別的類型}。 +decoys() { # $1=要排除的類型 + for t in $TYPES; do + [ "$t" = "$1" ] || printf 'JSC_WIKI_REPO_%s=decoy/%s ' "$t" "$t" + done +} -check_repo REPO 'REPO falls back to shared only' 'shared/wiki' \ - JSC_WIKI_REPO_ANALYZE='knowledges/ANALYZE' \ - JSC_WIKI_REPO='shared/wiki' +for ty in $TYPES; do + # 1. 自己的變數贏過共用變數,也贏過其他類型的誘餌。 + check_repo "$ty" "$ty specific wins" "own/$ty" \ + $(decoys "$ty") "JSC_WIKI_REPO_$ty=own/$ty" JSC_WIKI_REPO='shared/wiki' -check_repo ANALYZE 'ANALYZE specific wins' 'knowledges/ANALYZE' \ - JSC_WIKI_REPO_REPO='records/REPO' \ - JSC_WIKI_REPO_ANALYZE='knowledges/ANALYZE' \ - JSC_WIKI_REPO='shared/wiki' + # 2. 自己的變數未設定時,只退回共用變數。 + check_repo "$ty" "$ty falls back to shared only" 'shared/wiki' \ + $(decoys "$ty") JSC_WIKI_REPO='shared/wiki' -check_repo ANALYZE 'ANALYZE falls back to shared only' 'shared/wiki' \ - JSC_WIKI_REPO_REPO='records/REPO' \ - JSC_WIKI_REPO='shared/wiki' + # 3. 自己的變數與共用變數都沒有時,exit 3,不借用別的類型。 + check_no_repo "$ty" "$ty never borrows another type" $(decoys "$ty") +done -check_repo LEARN 'LEARN specific wins' 'lessons/LEARN' \ - JSC_WIKI_REPO_LEARN='lessons/LEARN' \ - JSC_WIKI_REPO='shared/wiki' - -check_repo LEARN 'LEARN falls back to shared only' 'shared/wiki' \ - JSC_WIKI_REPO_LOG='records/LOG' \ - JSC_WIKI_REPO='shared/wiki' - -check_repo DELIVER 'DELIVER specific wins' 'handover/DELIVER' \ - JSC_WIKI_REPO_DELIVER='handover/DELIVER' \ - JSC_WIKI_REPO='shared/wiki' - -check_repo DELIVER 'DELIVER falls back to shared only' 'shared/wiki' \ - JSC_WIKI_REPO_ANALYZE='knowledges/ANALYZE' \ - JSC_WIKI_REPO='shared/wiki' - -check_repo ERROR 'ERROR uses its own repo' 'errors/wiki' \ - JSC_WIKI_REPO_ERROR='errors/wiki' \ - JSC_WIKI_REPO='shared/wiki' +# 未知類型 exit 2,與「設定不足」的 exit 3 分開。 +if got=$(env -i PATH="${PATH:-/usr/bin:/bin}" JSC_WIKI_REPO='shared/wiki' \ + "$gitea" wiki-repo NOSUCH 2>/dev/null); then + code=0 +else + code=$? +fi +[ "$code" -eq 2 ] || fail "unknown type: want exit=2 got exit=$code" +[ -z "$got" ] || fail "unknown type: want no output got=$got" +# hash-id:首碼 0-9/A/B/C 改成 H 加前 7 碼;其餘首碼原樣輸出 8 碼大寫。 check_hash 'case-2' 'H5172CB7' check_hash 'case-11' 'HA9A6662' check_hash 'case-1' 'HB6EC7FD' check_hash 'case-12' 'HCCA8D42' +check_hash 'case-3' 'D3E5AA27' +check_hash 'case-7' 'D794C002' printf '%s\n' 'OK' diff --git a/tools/repo-sync.sh b/tools/repo-sync.sh new file mode 100755 index 0000000..e901384 --- /dev/null +++ b/tools/repo-sync.sh @@ -0,0 +1,80 @@ +#!/usr/bin/env sh +# repo-sync.sh — 把單一存取庫同步到本機。 +# 用法: repo-sync.sh / [target-dir] # target-dir 預設為 +# 目錄不存在 → git clone(clone URL 取自 gitea.sh clone-url) +# 目錄已存在 → git fetch、切到基準分支、git pull --ff-only +# 有未提交變更 → 只回報 dirty 與基準分支,不動分支也不 pull +# 基準分支的唯一優先序(呼叫端不必再判斷,也不要自己再推導一次): +# 1. gitea.sh default-branch /(該分支要在遠端存在) +# 2. 查不到時依序取遠端的 develop、main、master +# 3. 都沒有 → failed no default branch +# 輸出: 恰好一行,cloned、updated、dirty {分支} 或 failed {原因}。 +# 前三種 exit 0;failed exit 1。 +# dirty 會把解析好的基準分支一起帶出來,呼叫端直接拿去當 PR 的 base。 +set -u + +script_dir=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd) +gitea="$script_dir/gitea.sh" + +fail() { # 原因壓成一行,避免呼叫端解析多行輸出 + printf 'failed %s\n' "$(printf '%s' "$1" | tr '\n\r\t' ' ')" + exit 1 +} + +full="${1:-}" +[ -n "$full" ] || fail "usage: repo-sync.sh / [target-dir]" +case "$full" in + */*/*|/*|*/) fail "not owner/repo: $full" ;; + */*) ;; + *) fail "not owner/repo: $full" ;; +esac +repo="${full#*/}" +target="${2:-$repo}" + +remote_has() { # 遠端是否有這個分支 + git -C "$target" rev-parse --verify --quiet "refs/remotes/origin/$1" >/dev/null 2>&1 +} + +if [ ! -e "$target" ]; then + # clone URL 只收 stdout:把 stderr 併進來會讓任何雜訊變成網址的一部分 + err=$(mktemp) + url=$("$gitea" clone-url "$full" 2>"$err") || url='' + if [ -z "$url" ]; then + reason=$(cat "$err"); rm -f "$err" + fail "clone-url failed for $full: ${reason:-no clone URL}" + fi + rm -f "$err" + out=$(git clone "$url" "$target" 2>&1) || fail "clone: $out" + printf 'cloned\n' + exit 0 +fi + +git -C "$target" rev-parse --git-dir >/dev/null 2>&1 || fail "$target is not a git repo" + +dirty=0 +[ -z "$(git -C "$target" status --porcelain 2>/dev/null)" ] || dirty=1 + +# dirty 的存取庫也要解析基準分支:呼叫端拿它當 PR 的 base,自己再推導一次就會 +# 出現第二套優先序。fetch 只更新 refs,不動工作目錄,dirty 時照樣安全;但 dirty +# 時網路失敗不算致命,還是要把 dirty 回報出去,改用本機既有的 remote refs 判斷。 +if ! out=$(git -C "$target" fetch --prune origin 2>&1); then + [ "$dirty" -eq 1 ] || fail "fetch: $out" +fi + +branch=$("$gitea" default-branch "$full" 2>/dev/null) || branch='' +if [ -z "$branch" ] || ! remote_has "$branch"; then + branch='' + for b in develop main master; do + if remote_has "$b"; then branch="$b"; break; fi + done +fi +[ -n "$branch" ] || fail "no default branch" + +if [ "$dirty" -eq 1 ]; then + printf 'dirty %s\n' "$branch" + exit 0 +fi + +out=$(git -C "$target" checkout "$branch" 2>&1) || fail "checkout $branch: $out" +out=$(git -C "$target" pull --ff-only origin "$branch" 2>&1) || fail "pull $branch: $out" +printf 'updated\n' From 3a2eb40bef84ef6bbc1f55f34a25a8ba575321b2 Mon Sep 17 00:00:00 2001 From: Jeffery Date: Tue, 25 Aug 2026 14:58:54 +0800 Subject: [PATCH 2/7] =?UTF-8?q?docs(gitea):=20=E5=90=8C=E6=AD=A5=E6=96=87?= =?UTF-8?q?=E4=BB=B6=E8=88=87=E5=8F=83=E8=80=83=E8=B3=87=E6=96=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit What:更新 README、AGENTS.md、templates 與 references,讓文件敘述與實際行為一致。 Why:稽核發現多處文件與程式行為分歧,違反「每個意義只有單一真實來源」。 How:以實際程式行為為準改寫敘述,重複的規則收成單一來源並以一行指引指過去。 Who:jsc-meta:skill-check 例行稽核(2026-08-25)。 Co-Authored-By: Claude Opus 5 --- README.md | 15 ++++++++++++--- references/wiki-links.md | 24 ++++++++++++++++++++++++ 2 files changed, 36 insertions(+), 3 deletions(-) create mode 100644 references/wiki-links.md diff --git a/README.md b/README.md index a31ddba..4aba7df 100644 --- a/README.md +++ b/README.md @@ -35,12 +35,21 @@ gitea.sh wiki-put / # 自動判斷新建或更新 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-comments <owner>/<repo> <pr-index> # 所有留言(issue/審查/行內),依時間排序 +gitea.sh pr-comments <owner>/<repo> <pr-index> # 印出所有留言(issue 留言、審查評語、行內留言),依時間排序 +gitea.sh pr-depend <owner>/<repo> <pr-index> <dep-owner>/<dep-repo> <dep-index> + # 把 PR 掛上前置 PR 依賴;依賴未關閉前 Gitea 會阻擋合併 +gitea.sh repo-set <owner>/<repo> <description> [website] # 設定 repo 描述與網頁 gitea.sh api <METHOD> <path> [json-file] hash-id <text> # 與 gitea.sh hash-id 相同 +repo-sync.sh <owner>/<repo> [target-dir] # 同步單一存取庫;印出 cloned、updated、dirty {分支} 或 failed {原因} + # 基準分支的優先序只在這支腳本裡;dirty 會把解析好的分支帶出來當 PR 的 base check-wiki-rules.sh # 驗證 wiki repo 解析與 hash fallback 規則 ``` +## 參考資料 + +- `references/wiki-links.md`:寫 wiki 頁才需要的連結規則。同類型用 `[[顯示文字|頁名]]`(顯示文字在左),跨類型用 `wiki-url` 給的絕對網址。 + ## Skills 目錄 呼叫方式:Claude / Antigravity `/jsc-gitea:{name}`;Codex `${name}`;Copilot / Kiro 描述需求自動觸發。 @@ -49,11 +58,11 @@ check-wiki-rules.sh # 驗證 wiki repo 解析與 ha ### `wiki` -Gitea wiki 頁讀寫的統一入口:依頁面類型(QUESTION / PLAN / ANALYZE / DELIVER / MAINTAIN / REPO / LOG / LEARN / ERROR)解析 wiki 所在的 `{owner}/{repo}`,先讀對應的 `JSC_WIKI_REPO_{TYPE}`,再退回 `JSC_WIKI_REPO`,不同類型不可互相代用。頁面內容以圖表優先(mermaid 圖、markdown 表格),純文字為最後手段。 +Gitea wiki 頁讀寫的統一入口:依頁面類型(QUESTION / PLAN / ANALYZE / DELIVER / MAINTAIN / REPO / LOG / LEARN / ERROR)解析 wiki 所在的 `{owner}/{repo}`,先讀對應的 `JSC_WIKI_REPO_{TYPE}`,再退回 `JSC_WIKI_REPO`,不同類型不可互相代用。頁面內容以圖表優先(mermaid 圖、markdown 表格),純文字每節最多三句。 ### `repo-sync` -存取庫批次同步:列出 owner → 使用者選擇 → 逐 repo(sub agent)clone 或切 develop/master 更新;有變更就開分支 commit、push、PR。 +存取庫批次同步:列出 owner → 使用者選擇 → 逐 repo(sub agent)呼叫 `tools/repo-sync.sh` clone 或更新;回報 `dirty {分支}` 的存取庫交給 `jsc-git:pr`,base 直接用腳本帶出來的那個分支。 <!-- JSC-SKILLS:END --> diff --git a/references/wiki-links.md b/references/wiki-links.md new file mode 100644 index 0000000..4b9d57c --- /dev/null +++ b/references/wiki-links.md @@ -0,0 +1,24 @@ +# Wiki 頁之間的連結 + +寫 wiki 頁才需要這份規則。兩條規則任一條寫錯,連結會指向一個不存在的頁,畫面上看不出異常。 + +## 方向:顯示文字在左,頁名在右 + +Gitea 採 GitHub/Gollum 慣例:`[[顯示文字|頁名]]`。方向與 MediaWiki 相反。Gitea 原始碼(`modules/markup/html_link.go`)寫得很清楚: + +> MediaWiki uses [[link|text]], while GitHub uses [[text|link]] … we prefer GitHub syntax + +所以 `[[PLAN_H1234567|我的計畫]]` 會顯示成文字 `PLAN_H1234567`,連到一個叫「我的計畫」的頁——這是壞連結。要寫 `[[我的計畫|PLAN_H1234567]]`。 + +顯示文字與頁名相同時,用不帶豎線的 `[[PLAN_H1234567]]`,這種寫法不會寫錯。 + +## 範圍:`[[...]]` 只在同一個 wiki 內解析 + +`[[...]]` 與 markdown 相對連結都只在目前這個 wiki 內解析。跨存取庫沒有 wiki 連結語法。 + +| 連結 | 同一個 wiki? | 寫法 | +| --- | --- | --- | +| 同頁面類型(例:`PLAN_CONTENTS` → `PLAN_{HASH}`) | 一定同一個:一個類型一個存取庫 | `[[顯示文字\|頁名]]` 或 `[[頁名]]` | +| 不同頁面類型(例:`LOG_{HASH}` → `PLAN_{HASH}`) | **只有兩個類型解析到同一個存取庫時才同一個** | `wiki-url` 給的絕對網址:`[顯示文字](https://…/wiki/PLAN_…)` | + +每個類型各自解析自己的 `JSC_WIKI_REPO_{TYPE}`,所以跨類型連結**一律**用絕對網址:兩個類型剛好同存取庫也照樣正確,不必分兩種寫法。網址一律取自 `tools/gitea.sh wiki-url`,不要自己組路徑。 From 768ae6a9fe0b63586c7ff03e72b4e33935fb8b58 Mon Sep 17 00:00:00 2001 From: Jeffery <jiantw83@yahoo.com> Date: Tue, 25 Aug 2026 14:58:54 +0800 Subject: [PATCH 3/7] =?UTF-8?q?chore(gitea):=20=E4=B8=89=E4=BB=BD=20manife?= =?UTF-8?q?st=20=E5=90=8C=E6=AD=A5=E5=8D=87=E7=89=88=E4=B8=A6=E5=90=8C?= =?UTF-8?q?=E6=AD=A5=20marketplace=20=E6=AD=A3=E6=9C=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit What:三份 plugin manifest 版本同步 bump,兩份 marketplace 檔與 plugins/meta 正本對齊。 Why:準則要求技能異動必須同步升版;marketplace 副本必須與正本完全一致。 How:以 jsc-meta 的 tools/sync-skill-manifest.sh 升版,marketplace 檔由正本複製。 Who:jsc-meta:skill-check 例行稽核(2026-08-25)。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> --- .agents/plugins/marketplace.json | 6 +++--- .claude-plugin/marketplace.json | 6 +++--- .claude-plugin/plugin.json | 2 +- .codex-plugin/plugin.json | 2 +- plugin.json | 2 +- 5 files changed, 9 insertions(+), 9 deletions(-) diff --git a/.agents/plugins/marketplace.json b/.agents/plugins/marketplace.json index a9b4e66..8a2f91b 100644 --- a/.agents/plugins/marketplace.json +++ b/.agents/plugins/marketplace.json @@ -43,7 +43,7 @@ "source": "url", "url": "https://gitea.jsc.idv.tw/plugins/hooks.git" }, - "description": "跨 CLI hooks:STE100 語言強制、工時計時、技能用量記錄" + "description": "跨 CLI hooks:STE100 語言強制、工時計時、技能用量記錄、SDLC 模型鎖、版本前置檢查" }, { "name": "jsc-log", @@ -75,7 +75,7 @@ "source": "url", "url": "https://gitea.jsc.idv.tw/plugins/review.git" }, - "description": "程式碼審查:Refactoring 壞味道六組 + 註解規範 + 淺模組" + "description": "程式碼審查:Refactoring 壞味道六組、註解規範、淺模組" }, { "name": "jsc-sdlc", @@ -83,7 +83,7 @@ "source": "url", "url": "https://gitea.jsc.idv.tw/plugins/sdlc.git" }, - "description": "開發生命週期:規劃/分析/實作/維護(wiki 追蹤)" + "description": "開發生命週期:規劃、分析、實作、維護(wiki 追蹤)" } ] } diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json index a9b4e66..8a2f91b 100644 --- a/.claude-plugin/marketplace.json +++ b/.claude-plugin/marketplace.json @@ -43,7 +43,7 @@ "source": "url", "url": "https://gitea.jsc.idv.tw/plugins/hooks.git" }, - "description": "跨 CLI hooks:STE100 語言強制、工時計時、技能用量記錄" + "description": "跨 CLI hooks:STE100 語言強制、工時計時、技能用量記錄、SDLC 模型鎖、版本前置檢查" }, { "name": "jsc-log", @@ -75,7 +75,7 @@ "source": "url", "url": "https://gitea.jsc.idv.tw/plugins/review.git" }, - "description": "程式碼審查:Refactoring 壞味道六組 + 註解規範 + 淺模組" + "description": "程式碼審查:Refactoring 壞味道六組、註解規範、淺模組" }, { "name": "jsc-sdlc", @@ -83,7 +83,7 @@ "source": "url", "url": "https://gitea.jsc.idv.tw/plugins/sdlc.git" }, - "description": "開發生命週期:規劃/分析/實作/維護(wiki 追蹤)" + "description": "開發生命週期:規劃、分析、實作、維護(wiki 追蹤)" } ] } diff --git a/.claude-plugin/plugin.json b/.claude-plugin/plugin.json index 02ae668..844ddc8 100644 --- a/.claude-plugin/plugin.json +++ b/.claude-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "jsc-gitea", - "version": "0.1.0", + "version": "0.1.2", "description": "Gitea API 工具、Wiki 讀寫與存取庫批次同步", "skills": "./skills", "author": { diff --git a/.codex-plugin/plugin.json b/.codex-plugin/plugin.json index 9b94128..4657e0b 100644 --- a/.codex-plugin/plugin.json +++ b/.codex-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "jsc-gitea", - "version": "0.1.0", + "version": "0.1.2", "description": "Gitea API 工具、Wiki 讀寫與存取庫批次同步", "skills": "./skills" } diff --git a/plugin.json b/plugin.json index 7506aa3..3c54f99 100644 --- a/plugin.json +++ b/plugin.json @@ -1,6 +1,6 @@ { "name": "jsc-gitea", - "version": "0.1.0", + "version": "0.1.2", "description": "Gitea API 工具、Wiki 讀寫與存取庫批次同步", "skills": "./skills/" } From f8123e829b61995e3c10cfbed16b41ceb17f6700 Mon Sep 17 00:00:00 2001 From: Jeffery <jiantw83@yahoo.com> Date: Wed, 26 Aug 2026 09:19:08 +0800 Subject: [PATCH 4/7] =?UTF-8?q?feat(gitea):=20=E6=96=B0=E5=A2=9E=20wiki=20?= =?UTF-8?q?=E8=BD=89=E8=AD=B0=E9=A1=8C=E6=8A=80=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit What:新增 wiki-to-issue 技能,把一頁 wiki 轉成同一個存取庫的議題;配套新增 tools/gitea-link.sh 解析連結、tools/issue.sh 讀寫議題。 Why:wiki 頁要變成可追蹤的工作時,原本得自己拼 API:組 JSON、查標籤 id、挑回應欄位,跳脫一錯就把對外的議題內容寫壞。 How:連結是唯一入口,gitea-link.sh 解不出 wiki 頁就中止,不猜存取庫也不猜頁名;標籤只從既有的挑;站台沒有專案看板 API 時據實回報請使用者手動拖,不假裝關聯成功。 Who:使用 jsc 技能組、需要把 wiki 內容交辦出去的人。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> --- skills/wiki-to-issue/SKILL.md | 23 +++++ tools/gitea-link.sh | 77 +++++++++++++++++ tools/issue.sh | 157 ++++++++++++++++++++++++++++++++++ 3 files changed, 257 insertions(+) create mode 100644 skills/wiki-to-issue/SKILL.md create mode 100755 tools/gitea-link.sh create mode 100755 tools/issue.sh diff --git a/skills/wiki-to-issue/SKILL.md b/skills/wiki-to-issue/SKILL.md new file mode 100644 index 0000000..24dc6b0 --- /dev/null +++ b/skills/wiki-to-issue/SKILL.md @@ -0,0 +1,23 @@ +--- +name: wiki-to-issue +description: Turn one Gitea wiki page into an issue in the same repository. Parse the link with tools/gitea-link.sh, read the page through jsc-gitea:wiki, draft title and body as a sub agent, then create the issue with tools/issue.sh - labels picked from the repository's existing set per the jsc-ask decision tree, project board attached or reported as manual when the site has no board API. A request without a wiki link stops the skill immediately: never guess the repository or the page. Use when a wiki page has to become trackable work; not for issues drafted from scratch, and not for syncing an issue that already exists. +--- + +# wiki-to-issue — a wiki page becomes an issue + +The wiki link is the only input. Everything else — repository, page name, host — comes out of that link. + +## Steps + +1. **Link gate.** Run `tools/gitea-link.sh parse {url}` on the link the user gave. Exit 3, no link in the request, or `kind=issue` (this skill reads wiki pages, not issues) all mean the same thing: **stop and report which one it was**. Never ask for a repository name instead, and never fall back to the working directory's remote — a page written into the wrong repository's issue tracker is public and hard to take back. Completion condition: `kind=wiki`, and `repo`, `page` and `host` are known. +2. Read the page with `jsc-gitea:wiki` (`wiki-get {repo} {page}`). Exit 4 means the page does not exist — stop and report the page name. Take the page's absolute URL from `wiki-url` in the same pass; it goes into the issue body. Completion condition: the page's markdown and its absolute URL are both in hand. +3. **Draft the issue — this step 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. Completion condition: title and body file exist, the body carries the source line, and no `[[...]]` link is left in it. +4. **Labels come from what the repository already has.** Run `tools/issue.sh labels {repo}`, propose the fitting ones 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`. 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. +5. **Project board.** Run `tools/issue.sh projects {repo}`. Exit 3 means this Gitea has no board API: say so plainly, and hand the user the board URL the script printed so they can drag the issue in themselves. A board list comes back: let the user pick one per `jsc-ask:ask` rules, attach it, and report the failure verbatim if the attach call is refused. 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. +6. Create the issue: `tools/issue.sh create {repo} {title} {body-file} [--labels {ids}]`. Report the `index=` and `url=` it prints. Completion condition: the issue URL is reported to the user, together with the labels applied and the board status from step 5. + +## Rules + +- One wiki page, one issue. Splitting a page into several issues is analysis work, not conversion — hand that to `jsc-sdlc:analyze`. +- The issue body stays Traditional Chinese per the STE100 rule, and keeps the source line at the top. +- Creating an issue is an outward-facing action: the title, body, labels and board pick are confirmed with the user before the create call, never after. diff --git a/tools/gitea-link.sh b/tools/gitea-link.sh new file mode 100755 index 0000000..c5e0cc4 --- /dev/null +++ b/tools/gitea-link.sh @@ -0,0 +1,77 @@ +#!/usr/bin/env sh +# gitea-link.sh — 解析 Gitea 的 wiki 頁或議題連結(供 jsc-gitea:wiki-to-issue、html-export 使用)。 +# +# 為什麼要有這支腳本:兩支技能都以「一條連結」為唯一入口,沒有連結就中斷。連結長什麼樣、 +# 哪一段是存取庫、哪一段是頁名,是固定的字串規則,交給模型每次自己拆,拆錯就寫到別的存取庫去。 +# +# 用法: +# gitea-link.sh parse <url> 解析連結,印出可直接 eval 的欄位 +# +# parse 輸出(每行一個 key=value,值已加單引號): +# kind='wiki' repo='owner/repo' page='PAGE_NAME' host='https://gitea.example' +# kind='issue' repo='owner/repo' index='12' host='https://gitea.example' +# +# 認得的形式: +# https://host/{owner}/{repo}/wiki/{page} (page 可含 %XX 編碼,會還原) +# https://host/{owner}/{repo}/wiki/{page}/_edit (尾巴的 _edit、_new 會去掉) +# https://host/{owner}/{repo}/issues/{index} +# https://host/{owner}/{repo}/issues/{index}#issuecomment-123 +# +# 結束碼: 0=解析成功 2=用法錯誤 3=不是認得的 wiki 或議題連結 +# +# 陷阱: +# - 結束碼 3 由呼叫端當成「沒有連結」處理,直接中斷流程,不要退回去猜存取庫或頁名。 +# - 只解析連結本身,不打網路,也不檢查頁面存不存在;那是 wiki-get 與 issue.sh 的事。 +set -eu + +usage() { + echo 'usage: gitea-link.sh parse <url>' >&2 + exit 2 +} + +[ "$#" -eq 2 ] || usage +[ "$1" = parse ] || usage +url="$2" +[ -n "$url" ] || usage + +python3 - "$url" <<'PY' +import re, sys +from urllib.parse import urlsplit, unquote + +url = sys.argv[1].strip() +if not re.match(r'^https?://', url): + print('[jsc][連結解析][ERR]:連結要以 http:// 或 https:// 開頭。', file=sys.stderr) + sys.exit(3) + +u = urlsplit(url) +host = f'{u.scheme}://{u.netloc}' +parts = [p for p in u.path.split('/') if p] +if len(parts) < 4: + print('[jsc][連結解析][ERR]:認不出存取庫與頁面,需要 /{owner}/{repo}/wiki/{page} 或 /{owner}/{repo}/issues/{index}。', file=sys.stderr) + sys.exit(3) + +owner, repo, kind = parts[0], parts[1], parts[2] +rest = parts[3:] + +def out(**kv): + for k, v in kv.items(): + print(f"{k}='{str(v)}'") + +if kind == 'wiki': + while rest and rest[-1] in ('_edit', '_new', '_pages'): + rest.pop() + if not rest: + print('[jsc][連結解析][ERR]:wiki 連結少了頁名。', file=sys.stderr) + sys.exit(3) + page = unquote('/'.join(rest)) + out(kind='wiki', repo=f'{owner}/{repo}', page=page, host=host) +elif kind == 'issues': + idx = rest[0].split('#')[0] + if not idx.isdigit(): + print('[jsc][連結解析][ERR]:議題編號不是數字。', file=sys.stderr) + sys.exit(3) + out(kind='issue', repo=f'{owner}/{repo}', index=idx, host=host) +else: + print(f'[jsc][連結解析][ERR]:只認得 wiki 與 issues 連結,收到「{kind}」。', file=sys.stderr) + sys.exit(3) +PY diff --git a/tools/issue.sh b/tools/issue.sh new file mode 100755 index 0000000..2fab5cd --- /dev/null +++ b/tools/issue.sh @@ -0,0 +1,157 @@ +#!/usr/bin/env sh +# issue.sh — Gitea 議題的讀取與建立(供 jsc-gitea:wiki-to-issue、html-export 使用)。 +# +# 為什麼要有這支腳本:建議題要組 JSON、要把標籤名換成標籤 id、要把回應裡的編號與網址挑出來, +# 全是固定的輸入輸出。交給模型每次自己拼 curl,跳脫字元一錯就把議題內容寫壞,而議題是對外的。 +# +# 用法: +# issue.sh labels <owner>/<repo> 列出既有標籤:id<TAB>name +# issue.sh label-ids <owner>/<repo> <name>[,<name>] 標籤名換成 id(逗號分隔);有名字對不到就 exit 4 +# issue.sh projects <owner>/<repo> 列出專案看板:id<TAB>title;站台沒有這個 API 就 exit 3 +# issue.sh title <owner>/<repo> <index> 印出議題標題 +# issue.sh body <owner>/<repo> <index> 印出議題正文(markdown) +# issue.sh labels-of <owner>/<repo> <index> 印出議題目前的標籤名,一行一個 +# issue.sh create <owner>/<repo> <title> <body-file> [--labels <id>[,<id>]] [--milestone <id>] +# 建立議題。輸出兩行: index=<編號>、url=<議題網址> +# +# 環境變數: 同 gitea.sh(GITEA_HOST、GITEA_TOKEN;token 缺或被拒時退回 tea 登入金鑰)。 +# 結束碼: 0=成功 1=API 失敗 2=用法錯誤 3=站台不支援該 API 4=名稱對不到 id +# +# 陷阱: +# - 正文一律用檔案傳進來,內容維持 UTF-8 與真實換行;不要在參數裡塞 \n。 +# - Gitea 1.27 沒有公開的專案看板 API,projects 會 exit 3。呼叫端要據實回報「請手動把議題拖到看板」, +# 不要假裝已經關聯好——關聯不上跟關聯成功看起來一樣,是最容易被當成完成的一種失敗。 +set -eu + +script_dir=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd) +GITEA="$script_dir/gitea.sh" + +usage() { + cat >&2 <<'EOF' +用法: + issue.sh labels <owner>/<repo> 列出既有標籤:id<TAB>name + issue.sh label-ids <owner>/<repo> <name>[,<name>] 標籤名換成 id + issue.sh projects <owner>/<repo> 列出專案看板;站台不支援就 exit 3 + issue.sh title|body|labels-of <owner>/<repo> <index> + issue.sh create <owner>/<repo> <title> <body-file> [--labels <id>[,<id>]] [--milestone <id>] +結束碼: 0=成功 1=API 失敗 2=用法錯誤 3=站台不支援 4=名稱對不到 id +EOF + exit 2 +} + +[ -f "$GITEA" ] || { echo "[jsc][議題][ERR]:找不到 $GITEA。" >&2; exit 1; } + +valid_repo() { + case "${1:-}" in + */*/*|/*|*/) return 1 ;; + */*) return 0 ;; + *) return 1 ;; + esac +} + +cmd="${1:-}"; [ -n "$cmd" ] || usage +shift || true +repo="${1:-}" +valid_repo "$repo" || { echo "[jsc][議題][ERR]:存取庫須為 {owner}/{repo},收到「${repo:-空值}」。" >&2; exit 2; } +shift + +case "$cmd" in + labels) + "$GITEA" api GET "/repos/$repo/labels?limit=100" | python3 -c ' +import json,sys +for l in json.load(sys.stdin): + print("%s\t%s" % (l["id"], l["name"])) +' ;; + + label-ids) + names="${1:-}"; [ -n "$names" ] || usage + "$GITEA" api GET "/repos/$repo/labels?limit=100" | python3 -c ' +import json,sys +want=[n.strip() for n in sys.argv[1].split(",") if n.strip()] +have={l["name"]: l["id"] for l in json.load(sys.stdin)} +miss=[n for n in want if n not in have] +if miss: + sys.stderr.write("[jsc][議題][ERR]:這些標籤在存取庫裡找不到:" + "、".join(miss) + "\n") + sys.exit(4) +print(",".join(str(have[n]) for n in want)) +' "$names" ;; + + projects) + if out=$("$GITEA" api GET "/repos/$repo/projects" 2>/dev/null); then + printf '%s' "$out" | python3 -c ' +import json,sys +try: + d=json.load(sys.stdin) +except Exception: + sys.exit(3) +if not isinstance(d, list): + sys.exit(3) +for p in d: + print("%s\t%s" % (p.get("id",""), p.get("title") or p.get("name",""))) +' || exit 3 + else + host="${GITEA_HOST:-}" + case "$host" in http://*|https://*) : ;; *) host="https://$host" ;; esac + echo "[jsc][議題][WARN]:這個 Gitea 站台沒有專案看板 API,程式關聯不了。請開 ${host%/}/$repo/projects 手動把議題拖進看板。" >&2 + exit 3 + fi ;; + + title) + idx="${1:-}"; [ -n "$idx" ] || usage + "$GITEA" api GET "/repos/$repo/issues/$idx" | python3 -c ' +import json,sys +print(json.load(sys.stdin).get("title","")) +' ;; + + body) + idx="${1:-}"; [ -n "$idx" ] || usage + "$GITEA" api GET "/repos/$repo/issues/$idx" | python3 -c ' +import json,sys +sys.stdout.write(json.load(sys.stdin).get("body","") or "") +' ;; + + labels-of) + idx="${1:-}"; [ -n "$idx" ] || usage + "$GITEA" api GET "/repos/$repo/issues/$idx" | python3 -c ' +import json,sys +for l in json.load(sys.stdin).get("labels") or []: + print(l.get("name","")) +' ;; + + create) + title="${1:-}"; body_file="${2:-}" + [ -n "$title" ] || usage + [ -n "$body_file" ] || usage + [ -f "$body_file" ] || { echo "[jsc][議題][ERR]:找不到正文檔「$body_file」。" >&2; exit 2; } + shift 2 + labels='' + milestone='' + while [ "$#" -gt 0 ]; do + case "$1" in + --labels) [ "$#" -ge 2 ] || usage; labels="$2"; shift 2 ;; + --milestone) [ "$#" -ge 2 ] || usage; milestone="$2"; shift 2 ;; + *) echo "[jsc][議題][ERR]:不認得的選項「$1」。" >&2; usage ;; + esac + done + payload=$(mktemp) + trap 'rm -f "$payload"' EXIT + python3 -c ' +import json,sys +title, body_file, labels, milestone, out = sys.argv[1:6] +d={"title": title, "body": open(body_file, encoding="utf-8").read()} +if labels: + d["labels"]=[int(x) for x in labels.split(",") if x.strip()] +if milestone: + d["milestone"]=int(milestone) +with open(out, "w", encoding="utf-8") as f: + json.dump(d, f, ensure_ascii=False) +' "$title" "$body_file" "$labels" "$milestone" "$payload" + "$GITEA" api POST "/repos/$repo/issues" "$payload" | python3 -c ' +import json,sys +d=json.load(sys.stdin) +print("index=%s" % (d.get("number") or d.get("index") or "")) +print("url=%s" % (d.get("html_url") or "")) +' ;; + + *) usage ;; +esac From a0c13f807a727f4087a327d161286deefaf42a5a Mon Sep 17 00:00:00 2001 From: Jeffery <jiantw83@yahoo.com> Date: Wed, 26 Aug 2026 09:19:25 +0800 Subject: [PATCH 5/7] =?UTF-8?q?feat(gitea):=20=E6=96=B0=E5=A2=9E=20HTML=20?= =?UTF-8?q?=E5=8C=AF=E5=87=BA=E8=88=87=E7=AF=84=E6=9C=AC=E9=A2=A8=E6=A0=BC?= =?UTF-8?q?=E8=A8=AD=E5=AE=9A?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit What:新增 html-export 與 html-style 兩支技能、tools/html-render.sh 與 tools/html-style.sh 兩支工具,以及六種版型乘五種風格的 HTML 範本。 Why:wiki 頁與議題要拿給 Gitea 以外的人看時,只能複製 markdown;不同類型的文件也該有各自的版面,不是每份都長一樣。 How:版型(report、slide、dashboard、spec、timeline、onepager)決定內容怎麼排,風格(minimal、corporate、dark、print、vivid)決定看起來長怎樣,兩者自由搭配。哪一種頁面套哪一組由設定決定:專案的 .jsc/html-styles 優先,其次 $JSC_HOME/html-styles.conf,對不到退 DEFAULT,再對不到才用內建的 report/minimal。產出是單一 HTML 檔,CSS 與腳本全部內嵌。 Who:需要把 wiki 頁或議題寄給客戶、主管或跨團隊同事的人。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> --- skills/html-export/SKILL.md | 28 +++++ skills/html-style/SKILL.md | 23 ++++ templates/html/base.css | 131 +++++++++++++++++++++ templates/html/base.js | 63 ++++++++++ templates/html/layout/dashboard.html | 52 +++++++++ templates/html/layout/onepager.html | 46 ++++++++ templates/html/layout/report.html | 60 ++++++++++ templates/html/layout/slide.html | 90 ++++++++++++++ templates/html/layout/spec.html | 58 +++++++++ templates/html/layout/timeline.html | 61 ++++++++++ templates/html/style/corporate.css | 23 ++++ templates/html/style/dark.css | 21 ++++ templates/html/style/minimal.css | 21 ++++ templates/html/style/print.css | 30 +++++ templates/html/style/vivid.css | 34 ++++++ tools/html-render.sh | 111 ++++++++++++++++++ tools/html-style.sh | 169 +++++++++++++++++++++++++++ 17 files changed, 1021 insertions(+) create mode 100644 skills/html-export/SKILL.md create mode 100644 skills/html-style/SKILL.md create mode 100644 templates/html/base.css create mode 100644 templates/html/base.js create mode 100644 templates/html/layout/dashboard.html create mode 100644 templates/html/layout/onepager.html create mode 100644 templates/html/layout/report.html create mode 100644 templates/html/layout/slide.html create mode 100644 templates/html/layout/spec.html create mode 100644 templates/html/layout/timeline.html create mode 100644 templates/html/style/corporate.css create mode 100644 templates/html/style/dark.css create mode 100644 templates/html/style/minimal.css create mode 100644 templates/html/style/print.css create mode 100644 templates/html/style/vivid.css create mode 100755 tools/html-render.sh create mode 100755 tools/html-style.sh diff --git a/skills/html-export/SKILL.md b/skills/html-export/SKILL.md new file mode 100644 index 0000000..ec85277 --- /dev/null +++ b/skills/html-export/SKILL.md @@ -0,0 +1,28 @@ +--- +name: html-export +description: Export one Gitea wiki page or issue as a single self-contained HTML file. Parse the link with tools/gitea-link.sh, resolve that kind's layout and style through tools/html-style.sh (kind, then DEFAULT, then the built-in report/minimal), then render with tools/html-render.sh, which puts the markdown through Gitea's own renderer and inlines every asset. A request without a wiki or issue link stops the skill immediately: never guess the repository, the page or the issue number. Use when a page or issue has to leave Gitea as a document; not for choosing which template a kind uses - that is jsc-gitea:html-style. +--- + +# html-export — a wiki page or issue becomes one HTML file + +The link is the only input. The output is one file that opens anywhere, with no external asset. + +## Steps + +1. **Link gate.** Run `tools/gitea-link.sh parse {url}`. Exit 3 or no link in the request: **stop and report it**. Never fall back to the working directory's remote or to a page name the user mentioned in passing. Completion condition: `kind` is `wiki` or `issue`, and `repo` plus `page` or `index` are known. +2. **Work out the kind key** — it decides which template applies: + - wiki page → `WIKI:{prefix}`, where the prefix is the page name up to the first underscore (`ANALYZE_D3F1A2B0` → `WIKI:ANALYZE`; a page with no underscore uses the whole name). + - issue → run `tools/issue.sh labels-of {repo} {index}` and try `ISSUE:{label}` for each label in order; the first one `tools/html-style.sh get` answers with source `project` or `global` wins. No label matches: use `ISSUE:DEFAULT`. + + Completion condition: exactly one kind key is chosen, and you can say which label or prefix produced it. +3. Run `tools/html-style.sh get {key}`. It always prints `layout<TAB>style<TAB>source`. **Read the third column and report it**: `project` or `global` means the user configured this kind; `default` means it fell back to the DEFAULT row; `builtin` means nothing is configured at all and `report`/`minimal` was used. For `default` and `builtin`, tell the user in one line that `jsc-gitea:html-style` can set this kind's own template. Completion condition: layout, style and source are reported before anything is rendered. +4. Fetch the content: `jsc-gitea:wiki` `wiki-get` for a page, or `tools/issue.sh title` plus `tools/issue.sh body` for an issue. Exit 4 (page missing) or an API failure stops the skill with the page name or issue number in the report. Completion condition: the markdown and the document title are in hand. +5. **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. +6. Ask per `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 user has confirmed one output path. +7. Render: `tools/html-render.sh --markdown {file} --title {title} --layout {layout} --style {style} --source-url {absolute URL} --out {path}`. Exit 1 means Gitea's renderer failed — report it and stop, with no half-rendered file left behind. Completion condition: the file exists, and the report names its path, the layout, the style and where that pair came from. + +## Rules + +- One link, one file. Batch export is a loop the caller runs, not something this skill decides on its own. +- The rendered file inlines CSS and scripts on purpose: it is usually sent to someone outside Gitea, and an external asset breaks on their machine. +- The layout and style are never chosen by inspecting the content. The configuration decides, and `jsc-gitea:html-style` owns the configuration. diff --git a/skills/html-style/SKILL.md b/skills/html-style/SKILL.md new file mode 100644 index 0000000..424bddd --- /dev/null +++ b/skills/html-style/SKILL.md @@ -0,0 +1,23 @@ +--- +name: html-style +description: Set which HTML layout and style a kind of Gitea wiki page or issue gets when jsc-gitea:html-export renders it. Offer all six layouts from tools/html-style.sh layouts and all five styles from tools/html-style.sh styles as decision-tree options per jsc-ask, then write the pair with tools/html-style.sh set - either to the project file .jsc/html-styles or to the global $JSC_HOME/html-styles.conf. Use when a kind of page or issue should come out looking different, or when the export reported source builtin or default; not for rendering a page, which is jsc-gitea:html-export. +--- + +# html-style — which template a kind of page gets + +One kind of page, one layout, one style. `jsc-gitea:html-export` reads what this skill writes. + +## Steps + +1. **Settle the kind key.** Show the current configuration with `tools/html-style.sh list` first, then ask per `jsc-ask:ask` rules which kind this run sets. The three shapes are fixed: `WIKI:{page-name prefix}` (`WIKI:PLAN`, `WIKI:ANALYZE`, `WIKI:LOG` …), `ISSUE:{label name}` (`ISSUE:bug`), and `DEFAULT` for everything that matches nothing else. Every option states its impact scope — `DEFAULT` changes every kind that has no row of its own. Completion condition: exactly one key is agreed, and its current value from `tools/html-style.sh get {key}` has been read back with its source column. +2. **Pick the layout — offer all six.** Run `tools/html-style.sh layouts`; it prints each name with its Traditional Chinese description, taken from the template file itself. Present all six as options per `jsc-ask:ask` rules, each with what it does to the content (`report` builds a table of contents beside the text, `slide` turns every `##` into a keyboard-flipped page, `dashboard` turns them into cards, `spec` freezes table headers, `timeline` strings them along a line, `onepager` narrows everything into one printable page). Completion condition: the user has picked one layout name that the script listed. +3. **Pick the style — offer all five.** Run `tools/html-style.sh styles` and present every one it prints (`minimal`, `corporate`, `dark`, `print`, `vivid`) with its description. Never trim the list to a shortlist: the point of this skill is that the user sees the whole set. Completion condition: the user has picked one style name that the script listed. +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]`. Exit 4 means the name is not one of the listed templates — go back to step 2 or 3 rather than editing the 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. + +## Rules + +- Only names the script listed may be written. A key holding a template that does not exist fails at export time, long after the mistake was made. +- Removing a row is `tools/html-style.sh unset {key} [--project|--global]`; after that the kind falls back to `DEFAULT`, then to the built-in `report`/`minimal`. +- New layouts live in `templates/html/layout/{name}.html` and new styles in `templates/html/style/{name}.css`, each starting with a one-line Traditional Chinese comment — that comment is what the option list shows. diff --git a/templates/html/base.css b/templates/html/base.css new file mode 100644 index 0000000..3694166 --- /dev/null +++ b/templates/html/base.css @@ -0,0 +1,131 @@ +/* 共用排版:所有版型與風格都吃這一份,顏色與字型一律走 CSS 變數,由風格檔決定。 */ +*, *::before, *::after { box-sizing: border-box; } + +body { + margin: 0; + background: var(--bg); + color: var(--fg); + font-family: var(--font); + font-size: 16px; + line-height: 1.75; + -webkit-text-size-adjust: 100%; +} + +.page-head { + padding: 2.5rem 0 1.5rem; + border-bottom: 2px solid var(--accent); +} + +.page-head h1 { + margin: 0 0 .35rem; + font-family: var(--font-head); + font-size: 2rem; + line-height: 1.3; + color: var(--head-fg); +} + +.subtitle { margin: 0 0 .5rem; color: var(--muted); font-size: 1.05rem; } +.subtitle:empty { display: none; } + +.meta { + margin: 0; + color: var(--muted); + font-size: .85rem; + display: flex; + flex-wrap: wrap; + gap: 1rem; +} + +.meta a.source { color: var(--accent); text-decoration: none; word-break: break-all; } +.meta a.source:hover { text-decoration: underline; } + +.content { padding: 1.5rem 0 3rem; } + +.content h1, .content h2, .content h3, .content h4 { + font-family: var(--font-head); + color: var(--head-fg); + line-height: 1.35; + margin: 2rem 0 .75rem; +} + +.content h1 { font-size: 1.7rem; } +.content h2 { font-size: 1.4rem; } +.content h3 { font-size: 1.15rem; } +.content h4 { font-size: 1rem; } + +.content p { margin: 0 0 1rem; } +.content ul, .content ol { margin: 0 0 1rem; padding-left: 1.5rem; } +.content li { margin: .25rem 0; } +.content li input[type="checkbox"] { margin-right: .4rem; } + +.content a { color: var(--accent); text-decoration: none; } +.content a:hover { text-decoration: underline; } + +.content blockquote { + margin: 1rem 0; + padding: .6rem 1rem; + border-left: 4px solid var(--accent); + background: var(--card); + color: var(--muted); +} + +.content code { + font-family: var(--font-mono); + font-size: .9em; + background: var(--code-bg); + padding: .15em .4em; + border-radius: 4px; +} + +.content pre { + margin: 0 0 1rem; + padding: 1rem; + overflow-x: auto; + background: var(--code-bg); + border: 1px solid var(--border); + border-radius: var(--radius); +} + +.content pre code { background: none; padding: 0; } + +/* 表格一律可橫向捲動,寬表格不會把整頁撐開 */ +.table-wrap { overflow-x: auto; margin: 0 0 1.25rem; } + +.content table { + border-collapse: collapse; + width: 100%; + font-size: .95rem; +} + +.content th, .content td { + border: 1px solid var(--border); + padding: .5rem .7rem; + text-align: left; + vertical-align: top; +} + +.content th { background: var(--table-head-bg); color: var(--head-fg); font-weight: 600; } +.content tbody tr:nth-child(even) { background: var(--stripe); } + +.content img { max-width: 100%; height: auto; } +.content hr { border: 0; border-top: 1px solid var(--border); margin: 2rem 0; } + +.page-foot { + padding: 1.25rem 0 2rem; + border-top: 1px solid var(--border); + color: var(--muted); + font-size: .8rem; + display: flex; + flex-wrap: wrap; + gap: .75rem; + justify-content: space-between; +} + +.badge { + display: inline-block; + padding: .1rem .5rem; + border: 1px solid var(--border); + border-radius: 999px; + font-size: .75rem; + color: var(--muted); +} diff --git a/templates/html/base.js b/templates/html/base.js new file mode 100644 index 0000000..8d8e22c --- /dev/null +++ b/templates/html/base.js @@ -0,0 +1,63 @@ +// 共用行為:表格加捲動外框、依 h2 切段、產生目錄。各版型自己決定要用哪幾個。 +(function (w) { + 'use strict'; + + // 寬表格包一層可橫捲的外框,整頁就不會被撐開。 + function wrapTables(root) { + root.querySelectorAll('table').forEach(function (t) { + if (t.parentElement && t.parentElement.classList.contains('table-wrap')) return; + var box = document.createElement('div'); + box.className = 'table-wrap'; + t.parentNode.insertBefore(box, t); + box.appendChild(t); + }); + } + + // 依 h2 把內容切成一段一段。h2 之前的內容自成第一段(前言)。 + function splitBySection(root) { + var nodes = Array.prototype.slice.call(root.childNodes); + var sections = []; + var current = null; + + function open(headingText) { + current = document.createElement('section'); + current.className = 'jsc-section'; + current.dataset.title = headingText || ''; + sections.push(current); + } + + nodes.forEach(function (node) { + if (node.nodeType === 1 && node.tagName === 'H2') { + open(node.textContent.trim()); + } else if (!current) { + if (node.nodeType === 3 && !node.textContent.trim()) return; + open(''); + } + current.appendChild(node); + }); + + root.innerHTML = ''; + sections.forEach(function (s) { root.appendChild(s); }); + return sections; + } + + // 依 h2、h3 產生目錄,塞進指定容器。標題沒有 id 就補一個。 + function buildToc(root, target) { + var heads = root.querySelectorAll('h2, h3'); + if (!heads.length) { target.remove(); return; } + var list = document.createElement('ul'); + heads.forEach(function (h, i) { + if (!h.id) h.id = 'sec-' + (i + 1); + var li = document.createElement('li'); + li.className = 'toc-' + h.tagName.toLowerCase(); + var a = document.createElement('a'); + a.href = '#' + h.id; + a.textContent = h.textContent.trim(); + li.appendChild(a); + list.appendChild(li); + }); + target.appendChild(list); + } + + w.jsc = { wrapTables: wrapTables, splitBySection: splitBySection, buildToc: buildToc }; +})(window); diff --git a/templates/html/layout/dashboard.html b/templates/html/layout/dashboard.html new file mode 100644 index 0000000..434fbce --- /dev/null +++ b/templates/html/layout/dashboard.html @@ -0,0 +1,52 @@ +<!-- 看板:每個 h2 一張卡片並排,適合進度、狀態、維護清單 --> +<!doctype html> +<html lang="zh-Hant" data-layout="{{LAYOUT}}" data-style="{{STYLE_NAME}}"> +<head> +<meta charset="utf-8"> +<meta name="viewport" content="width=device-width, initial-scale=1"> +<title>{{TITLE}} + + + + + +
+
+

{{TITLE}}

+

{{SUBTITLE}}

+

{{SOURCE}}產生時間:{{GENERATED}}

+
+
{{CONTENT}}
+
+ {{LAYOUT}}/{{STYLE_NAME}} + 本頁由 jsc-gitea:html-export 產生 +
+
+ + + + diff --git a/templates/html/layout/onepager.html b/templates/html/layout/onepager.html new file mode 100644 index 0000000..8fad63a --- /dev/null +++ b/templates/html/layout/onepager.html @@ -0,0 +1,46 @@ + + + + + + +{{TITLE}} + + + + + +
+
+

{{TITLE}}

+

{{SUBTITLE}}

+

{{SOURCE}}產生時間:{{GENERATED}}

+
+
{{CONTENT}}
+
+ {{LAYOUT}}/{{STYLE_NAME}} + 本頁由 jsc-gitea:html-export 產生 +
+
+ + + + diff --git a/templates/html/layout/report.html b/templates/html/layout/report.html new file mode 100644 index 0000000..5abd440 --- /dev/null +++ b/templates/html/layout/report.html @@ -0,0 +1,60 @@ + + + + + + +{{TITLE}} + + + + + +
+
+

{{TITLE}}

+

{{SUBTITLE}}

+

{{SOURCE}}產生時間:{{GENERATED}}

+
+
+ +
{{CONTENT}}
+
+
+ {{LAYOUT}}/{{STYLE_NAME}} + 本頁由 jsc-gitea:html-export 產生 +
+
+ + + + diff --git a/templates/html/layout/slide.html b/templates/html/layout/slide.html new file mode 100644 index 0000000..c2ced90 --- /dev/null +++ b/templates/html/layout/slide.html @@ -0,0 +1,90 @@ + + + + + + +{{TITLE}} + + + + + +
+
+

{{TITLE}}

+

{{SUBTITLE}}

+

{{SOURCE}}產生時間:{{GENERATED}}

+
+
{{CONTENT}}
+
+ + + {{LAYOUT}}/{{STYLE_NAME}} + +
+
+ + + + diff --git a/templates/html/layout/spec.html b/templates/html/layout/spec.html new file mode 100644 index 0000000..5b6dd1d --- /dev/null +++ b/templates/html/layout/spec.html @@ -0,0 +1,58 @@ + + + + + + +{{TITLE}} + + + + + +
+
+

{{TITLE}}

+

{{SUBTITLE}}

+

{{SOURCE}}產生時間:{{GENERATED}}

+
+
+ +
{{CONTENT}}
+
+
+ {{LAYOUT}}/{{STYLE_NAME}} + 本頁由 jsc-gitea:html-export 產生 +
+
+ + + + diff --git a/templates/html/layout/timeline.html b/templates/html/layout/timeline.html new file mode 100644 index 0000000..18965a9 --- /dev/null +++ b/templates/html/layout/timeline.html @@ -0,0 +1,61 @@ + + + + + + +{{TITLE}} + + + + + +
+
+

{{TITLE}}

+

{{SUBTITLE}}

+

{{SOURCE}}產生時間:{{GENERATED}}

+
+
{{CONTENT}}
+
+ {{LAYOUT}}/{{STYLE_NAME}} + 本頁由 jsc-gitea:html-export 產生 +
+
+ + + + diff --git a/templates/html/style/corporate.css b/templates/html/style/corporate.css new file mode 100644 index 0000000..dcac7ea --- /dev/null +++ b/templates/html/style/corporate.css @@ -0,0 +1,23 @@ +/* 商務:深藍主色、表頭反白、正式對外用 */ +:root { + --bg: #ffffff; + --fg: #22272e; + --head-fg: #0b3358; + --muted: #5c6b7a; + --accent: #0b5fa5; + --border: #c9d6e2; + --card: #eef4fa; + --code-bg: #eef2f6; + --table-head-bg: #0b3358; + --stripe: #f4f8fc; + --radius: 6px; + --shadow: 0 1px 2px rgba(11, 51, 88, .12); + --font: "Noto Sans TC", "PingFang TC", "Microsoft JhengHei", system-ui, sans-serif; + --font-head: var(--font); + --font-mono: "JetBrains Mono", "Cascadia Mono", Consolas, monospace; +} + +.page-head { border-bottom-width: 3px; } +.content th { color: #ffffff; letter-spacing: .03em; } +.content h2 { border-left: 5px solid var(--accent); padding-left: .6rem; } +.content table { box-shadow: var(--shadow); } diff --git a/templates/html/style/dark.css b/templates/html/style/dark.css new file mode 100644 index 0000000..bcdf929 --- /dev/null +++ b/templates/html/style/dark.css @@ -0,0 +1,21 @@ +/* 深色:深底亮字,長時間閱讀與投影機環境 */ +:root { + --bg: #11161c; + --fg: #d7dee6; + --head-fg: #f2f6fa; + --muted: #8b98a6; + --accent: #56a8f5; + --border: #2b3540; + --card: #1a2129; + --code-bg: #1c242d; + --table-head-bg: #1f2932; + --stripe: #161d24; + --radius: 6px; + --shadow: 0 1px 3px rgba(0, 0, 0, .5); + --font: "Noto Sans TC", "PingFang TC", "Microsoft JhengHei", system-ui, sans-serif; + --font-head: var(--font); + --font-mono: "JetBrains Mono", "Cascadia Mono", Consolas, monospace; +} + +.content pre { border-color: #2b3540; } +.content h2 { border-bottom: 1px solid var(--border); padding-bottom: .3rem; } diff --git a/templates/html/style/minimal.css b/templates/html/style/minimal.css new file mode 100644 index 0000000..c492018 --- /dev/null +++ b/templates/html/style/minimal.css @@ -0,0 +1,21 @@ +/* 極簡:白底、細線、無襯線,資訊密度優先 */ +:root { + --bg: #ffffff; + --fg: #1f2328; + --head-fg: #0d1117; + --muted: #6a737d; + --accent: #2f6f9f; + --border: #d8dee4; + --card: #f6f8fa; + --code-bg: #f2f4f7; + --table-head-bg: #f6f8fa; + --stripe: #fbfcfd; + --radius: 4px; + --shadow: none; + --font: "Noto Sans TC", "PingFang TC", "Microsoft JhengHei", system-ui, sans-serif; + --font-head: var(--font); + --font-mono: "JetBrains Mono", "Cascadia Mono", Consolas, monospace; +} + +.page-head { border-bottom-width: 1px; } +.content h2 { border-bottom: 1px solid var(--border); padding-bottom: .3rem; } diff --git a/templates/html/style/print.css b/templates/html/style/print.css new file mode 100644 index 0000000..a404b4a --- /dev/null +++ b/templates/html/style/print.css @@ -0,0 +1,30 @@ +/* 印刷:襯線字、A4 邊界、去掉陰影,列印或轉 PDF 用 */ +:root { + --bg: #ffffff; + --fg: #1b1b1b; + --head-fg: #000000; + --muted: #55555f; + --accent: #4a4a4a; + --border: #b8b8b8; + --card: #f4f4f2; + --code-bg: #f2f2f0; + --table-head-bg: #ececeb; + --stripe: #f9f9f8; + --radius: 0; + --shadow: none; + --font: "Noto Serif TC", "Songti TC", "PMingLiU", Georgia, serif; + --font-head: var(--font); + --font-mono: "JetBrains Mono", Consolas, monospace; +} + +@page { size: A4; margin: 20mm 18mm; } + +body { font-size: 15px; line-height: 1.85; } +.page-head { border-bottom: 1px solid var(--border); } +.content h2 { page-break-after: avoid; } +.content table, .content pre, .content blockquote { page-break-inside: avoid; } + +@media print { + .meta a.source { color: var(--fg); } + .page-foot { border-top: 1px solid var(--border); } +} diff --git a/templates/html/style/vivid.css b/templates/html/style/vivid.css new file mode 100644 index 0000000..c63e2f9 --- /dev/null +++ b/templates/html/style/vivid.css @@ -0,0 +1,34 @@ +/* 明亮:高彩度、圓角卡片、漸層標題,簡報與對內宣達用 */ +:root { + --bg: #fdfbff; + --fg: #241f2e; + --head-fg: #4c1d95; + --muted: #6d6480; + --accent: #7c3aed; + --border: #e2d9f5; + --card: #f6f1ff; + --code-bg: #f1ecfd; + --table-head-bg: #ede4ff; + --stripe: #faf7ff; + --radius: 12px; + --shadow: 0 2px 10px rgba(124, 58, 237, .12); + --font: "Noto Sans TC", "PingFang TC", "Microsoft JhengHei", system-ui, sans-serif; + --font-head: var(--font); + --font-mono: "JetBrains Mono", "Cascadia Mono", Consolas, monospace; +} + +.page-head { + border-bottom: 0; + background: linear-gradient(135deg, #7c3aed 0%, #e0378f 100%); + color: #ffffff; + border-radius: var(--radius); + padding: 2rem 1.5rem; + box-shadow: var(--shadow); +} + +.page-head h1, .page-head .subtitle, .page-head .meta { color: #ffffff; } +.page-head .meta a.source { color: #ffffff; text-decoration: underline; } + +.content h2 { border-left: 6px solid var(--accent); padding-left: .6rem; } +.content table { border-radius: var(--radius); overflow: hidden; box-shadow: var(--shadow); } +.content blockquote { border-radius: var(--radius); } diff --git a/tools/html-render.sh b/tools/html-render.sh new file mode 100755 index 0000000..f96bea0 --- /dev/null +++ b/tools/html-render.sh @@ -0,0 +1,111 @@ +#!/usr/bin/env sh +# html-render.sh — 把 markdown 套上版型與風格,輸出單一 HTML 檔(供 jsc-gitea:html-export 使用)。 +# +# 為什麼要有這支腳本:markdown 轉 HTML 交給 Gitea 自己渲染(gitea.sh markdown),出來的排版才跟 +# wiki、議題頁看到的一致;版型與風格則是固定的字串替換。兩件事都有標準輸入輸出,不必每次重寫。 +# +# 用法: +# html-render.sh --markdown <檔案> --title <標題> --out <輸出檔> +# [--subtitle <副標>] [--layout <版型>] [--style <風格>] +# [--source-url <來源網址>] +# +# --layout 預設 report,--style 預設 minimal。可用清單見 html-style.sh layouts / styles。 +# +# 輸出: 寫出 --out 指定的 HTML 檔,並在標準輸出印出該檔路徑。 +# 結束碼: 0=成功 1=渲染或寫檔失敗 2=用法錯誤 4=找不到版型或風格範本 +# +# 陷阱: +# - HTML 是單一檔案,CSS 直接內嵌,不外連任何資源;產出物常常是寄給別人看的,外連在對方那裡會破圖。 +# - markdown 渲染走 Gitea API。連不上就失敗收場,不自己拼一套半套的轉換——半套轉換出來的表格 +# 跟 wiki 上看到的不一樣,比失敗更難發現。 +# - 渲染端點不吃 wiki 情境,`[[頁名]]` 這種 wiki 內部連結會原樣留著。呼叫端要先換成絕對網址, +# 產出的 HTML 才連得回去。 +set -eu + +script_dir=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd) +plugin_root="${CLAUDE_PLUGIN_ROOT:-$script_dir/..}" +TPL="$plugin_root/templates/html" +GITEA="$script_dir/gitea.sh" + +usage() { + cat >&2 <<'EOF' +用法: + html-render.sh --markdown <檔案> --title <標題> --out <輸出檔> + [--subtitle <副標>] [--layout <版型>] [--style <風格>] + [--source-url <來源網址>] +結束碼: 0=成功 1=渲染或寫檔失敗 2=用法錯誤 4=找不到版型或風格範本 +EOF + exit 2 +} + +md=''; title=''; out=''; subtitle=''; layout=report; style=minimal; source_url='' +while [ "$#" -gt 0 ]; do + case "$1" in + --markdown) [ "$#" -ge 2 ] || usage; md="$2"; shift 2 ;; + --title) [ "$#" -ge 2 ] || usage; title="$2"; shift 2 ;; + --out) [ "$#" -ge 2 ] || usage; out="$2"; shift 2 ;; + --subtitle) [ "$#" -ge 2 ] || usage; subtitle="$2"; shift 2 ;; + --layout) [ "$#" -ge 2 ] || usage; layout="$2"; shift 2 ;; + --style) [ "$#" -ge 2 ] || usage; style="$2"; shift 2 ;; + --source-url) [ "$#" -ge 2 ] || usage; source_url="$2"; shift 2 ;; + *) echo "[jsc][HTML 產生][ERR]:不認得的選項「$1」。" >&2; usage ;; + esac +done + +[ -n "$md" ] && [ -n "$title" ] && [ -n "$out" ] || usage +[ -f "$md" ] || { echo "[jsc][HTML 產生][ERR]:找不到 markdown 檔「$md」。" >&2; exit 2; } + +layout_file="$TPL/layout/$layout.html" +style_file="$TPL/style/$style.css" +[ -f "$layout_file" ] || { echo "[jsc][HTML 產生][ERR]:找不到版型範本「$layout_file」。" >&2; exit 4; } +[ -f "$style_file" ] || { echo "[jsc][HTML 產生][ERR]:找不到風格範本「$style_file」。" >&2; exit 4; } + +# markdown -> HTML 片段:交給 Gitea 自己渲染,排版才跟站上一致。 +fragment=$(mktemp) +trap 'rm -f "$fragment"' EXIT + +if ! "$GITEA" markdown "$md" > "$fragment" 2>/dev/null || [ ! -s "$fragment" ]; then + echo '[jsc][HTML 產生][ERR]:Gitea 的 markdown 渲染失敗,這次不出檔。請確認 GITEA_HOST 與權杖後重跑。' >&2 + exit 1 +fi + +python3 - "$layout_file" "$style_file" "$fragment" "$out" "$title" "$subtitle" "$source_url" "$layout" "$style" "$TPL" <<'PY' +import html, sys, datetime, os + +layout_file, style_file, frag_file, out_file, title, subtitle, source_url, layout, style, tpl_dir = sys.argv[1:11] + +def read(p): + with open(p, encoding='utf-8') as f: + return f.read() + +page = read(layout_file) +css = read(style_file) +base_css = read(os.path.join(tpl_dir, 'base.css')) +base_js = read(os.path.join(tpl_dir, 'base.js')) +content = read(frag_file) +generated = datetime.datetime.now().astimezone().strftime('%Y-%m-%d %H:%M') + +source_html = '' +if source_url: + safe = html.escape(source_url, quote=True) + source_html = '來源:%s' % (safe, safe) + +for key, value in ( + ('{{TITLE}}', html.escape(title)), + ('{{SUBTITLE}}', html.escape(subtitle)), + ('{{BASE}}', base_css), + ('{{BASE_JS}}', base_js), + ('{{STYLE}}', css), + ('{{CONTENT}}', content), + ('{{SOURCE}}', source_html), + ('{{GENERATED}}', generated), + ('{{LAYOUT}}', html.escape(layout)), + ('{{STYLE_NAME}}', html.escape(style)), +): + page = page.replace(key, value) + +with open(out_file, 'w', encoding='utf-8') as f: + f.write(page) +PY + +printf '%s\n' "$out" diff --git a/tools/html-style.sh b/tools/html-style.sh new file mode 100755 index 0000000..af20e74 --- /dev/null +++ b/tools/html-style.sh @@ -0,0 +1,169 @@ +#!/usr/bin/env sh +# html-style.sh — 「哪一種 wiki 頁或議題,用哪一種版型與風格出 HTML」的設定(供 jsc-gitea:html-style、html-export 使用)。 +# +# 設定格式(一行一筆):{種類}={版型},{風格} +# 種類:WIKI:{頁名前綴}(例 WIKI:PLAN)、ISSUE:{標籤名}(例 ISSUE:bug)、DEFAULT(都對不到時用) +# 「#」開頭為註解,空白行忽略。同一種類出現多行時取最後一行。 +# 解析順序: +# 1. 目前工作目錄的 ./.jsc/html-styles(專案覆寫) +# 2. $JSC_HOME/html-styles.conf(JSC_HOME 預設 ~/.jsc) +# 3. 種類對不到就退 DEFAULT,DEFAULT 也沒有才用內建預設 report,minimal +# +# 用法: +# html-style.sh get <種類> 印出 版型風格來源(project/global/default/builtin) +# html-style.sh set <種類> <版型> <風格> [--project|--global] 寫入設定(預設 --global) +# html-style.sh unset <種類> [--project|--global] 移除設定 +# html-style.sh list 印出合併後的所有設定:種類版型風格來源 +# html-style.sh layouts 列出可用版型:名稱繁中說明 +# html-style.sh styles 列出可用風格:名稱繁中說明 +# +# 結束碼: 0=成功 2=用法錯誤 4=版型或風格沒有對應範本檔 +# +# 陷阱: +# - get 永遠印得出一組值:對不到就退 DEFAULT,再對不到就退內建預設。呼叫端不必自己準備退路, +# 但要看第三欄,才知道這組值是使用者設的還是撿來的。 +# - set 會先確認範本檔真的存在,擋掉打錯字的版型或風格;設定寫得進去、出圖卻失敗最難查。 +set -u + +script_dir=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd) +plugin_root="${CLAUDE_PLUGIN_ROOT:-$script_dir/..}" +TPL="$plugin_root/templates/html" +JSC_HOME="${JSC_HOME:-$HOME/.jsc}" +PROJECT_FILE=./.jsc/html-styles +GLOBAL_FILE="$JSC_HOME/html-styles.conf" +BUILTIN_LAYOUT=report +BUILTIN_STYLE=minimal + +usage() { + cat >&2 <<'EOF' +用法: + html-style.sh get <種類> + html-style.sh set <種類> <版型> <風格> [--project|--global] + html-style.sh unset <種類> [--project|--global] + html-style.sh list | layouts | styles +種類: WIKI:{頁名前綴}、ISSUE:{標籤名}、DEFAULT +結束碼: 0=成功 2=用法錯誤 4=版型或風格沒有對應範本檔 +EOF + exit 2 +} + +read_value() { # $1=設定檔 $2=種類 -> 「版型,風格」 + [ -f "$1" ] || return 0 + awk -F= -v want="$2" ' + { sub(/\r$/, "") } + /^[ \t]*#/ { next } + /^[ \t]*$/ { next } + index($0, "=") == 0 { next } + { + key = $1 + gsub(/[ \t]/, "", key) + if (key != want) next + val = substr($0, index($0, "=") + 1) + gsub(/[ \t]/, "", val) + if (val != "") v = val + } + END { if (v != "") print v } + ' "$1" +} + +resolve() { # $1=種類 -> 版型風格來源 + for _f in "$PROJECT_FILE:project" "$GLOBAL_FILE:global"; do + _file=${_f%:*}; _src=${_f##*:} + _v=$(read_value "$_file" "$1") + if [ -n "$_v" ]; then + printf '%s\t%s\t%s\n' "${_v%%,*}" "${_v##*,}" "$_src" + return 0 + fi + done + if [ "$1" != DEFAULT ]; then + _d=$(resolve DEFAULT) + _src=$(printf '%s' "$_d" | cut -f3) + # DEFAULT 自己也沒設定時,來源照實說是 builtin,不要蓋成 default + [ "$_src" = builtin ] || _src=default + printf '%s\t%s\n' "$(printf '%s' "$_d" | cut -f1,2)" "$_src" + return 0 + fi + printf '%s\t%s\tbuiltin\n' "$BUILTIN_LAYOUT" "$BUILTIN_STYLE" +} + +# 範本檔第一行註解就是繁中說明:版型放在 ,風格放在 /* 說明 */。 +describe() { # $1=檔案 + head -n1 "$1" 2>/dev/null | sed 's///; s|/\*[[:space:]]*||; s|[[:space:]]*\*/||' +} + +list_layouts() { + for f in "$TPL"/layout/*.html; do + [ -f "$f" ] || continue + name=$(basename "$f" .html) + printf '%s\t%s\n' "$name" "$(describe "$f")" + done +} + +list_styles() { + for f in "$TPL"/style/*.css; do + [ -f "$f" ] || continue + name=$(basename "$f" .css) + printf '%s\t%s\n' "$name" "$(describe "$f")" + done +} + +write_kv() { # $1=檔案 $2=種類 $3=值 + dir=$(dirname "$1") + mkdir -p "$dir" || { echo "[jsc][HTML 設定][ERR]:建不出目錄「$dir」。" >&2; exit 1; } + tmp="$1.tmp.$$" + { [ -f "$1" ] && grep -v "^[[:space:]]*$2[[:space:]]*=" "$1" || true; } > "$tmp" + [ -n "$3" ] && printf '%s=%s\n' "$2" "$3" >> "$tmp" + mv "$tmp" "$1" +} + +cmd="${1:-}"; [ -n "$cmd" ] || usage +shift || true + +case "$cmd" in + layouts) list_layouts; exit 0 ;; + styles) list_styles; exit 0 ;; + list) + keys=$( { [ -f "$PROJECT_FILE" ] && cut -d= -f1 "$PROJECT_FILE" || true + [ -f "$GLOBAL_FILE" ] && cut -d= -f1 "$GLOBAL_FILE" || true; } \ + | sed 's/^[[:space:]]*//; s/[[:space:]]*$//' | grep -v '^#' | grep -v '^$' | sort -u) + [ -n "$keys" ] || { printf 'DEFAULT\t%s\t%s\tbuiltin\n' "$BUILTIN_LAYOUT" "$BUILTIN_STYLE"; exit 0; } + printf '%s\n' "$keys" | while IFS= read -r k; do + printf '%s\t%s\n' "$k" "$(resolve "$k")" + done + exit 0 ;; + get) + key="${1:-}"; [ -n "$key" ] || usage + resolve "$key" + exit 0 ;; + set) + key="${1:-}"; layout="${2:-}"; style="${3:-}" + [ -n "$key" ] && [ -n "$layout" ] && [ -n "$style" ] || usage + shift 3 + target="$GLOBAL_FILE"; scope=global + case "${1:-}" in + --project) target="$PROJECT_FILE"; scope=project ;; + --global|'') ;; + *) usage ;; + esac + [ -f "$TPL/layout/$layout.html" ] || { + echo "[jsc][HTML 設定][ERR]:沒有版型「$layout」。可用:$(list_layouts | cut -f1 | tr '\n' ' ')" >&2; exit 4; } + [ -f "$TPL/style/$style.css" ] || { + echo "[jsc][HTML 設定][ERR]:沒有風格「$style」。可用:$(list_styles | cut -f1 | tr '\n' ' ')" >&2; exit 4; } + write_kv "$target" "$key" "$layout,$style" + printf '已寫入 %s:%s=%s,%s(%s)\n' "$target" "$key" "$layout" "$style" "$scope" + exit 0 ;; + unset) + key="${1:-}"; [ -n "$key" ] || usage + shift + target="$GLOBAL_FILE" + case "${1:-}" in + --project) target="$PROJECT_FILE" ;; + --global|'') ;; + *) usage ;; + esac + [ -f "$target" ] || { echo "找不到設定檔:$target" >&2; exit 0; } + write_kv "$target" "$key" '' + printf '已移除 %s 的 %s\n' "$target" "$key" + exit 0 ;; + *) usage ;; +esac From dc17907554d84d8612a1d5385d7677191c1a5787 Mon Sep 17 00:00:00 2001 From: Jeffery Date: Wed, 26 Aug 2026 09:19:25 +0800 Subject: [PATCH 6/7] =?UTF-8?q?chore(gitea):=20markdown=20=E6=B8=B2?= =?UTF-8?q?=E6=9F=93=E5=AD=90=E5=91=BD=E4=BB=A4=E8=88=87=E6=96=87=E4=BB=B6?= =?UTF-8?q?=E3=80=81manifest=20=E5=90=8C=E6=AD=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit What:gitea.sh 新增 markdown 子命令與可切換的 Content-Type,README 補上新工具、新技能與 HTML 範本說明,三份 manifest 同步升版到 0.1.3。 Why:markdown 轉 HTML 要交給 Gitea 自己渲染,排版才跟 wiki、議題頁一致;但 /markdown 端點在 Gitea 1.27 回 200 卻是空內容,看起來像成功。 How:改走吃純文字的 /markdown/raw,req 送出的 Content-Type 改由 REQ_CONTENT_TYPE 決定,預設仍是 application/json。代價寫進註解:raw 端點不吃 wiki 情境,[[頁名]] 要由呼叫端先換成絕對網址。 Who:所有透過 jsc-gitea 產生文件的技能。 Co-Authored-By: Claude Opus 5 --- .claude-plugin/plugin.json | 2 +- .codex-plugin/plugin.json | 2 +- README.md | 49 ++++++++++++++++++++++++++++++++++++++ plugin.json | 2 +- tools/gitea.sh | 17 +++++++++++-- 5 files changed, 67 insertions(+), 5 deletions(-) diff --git a/.claude-plugin/plugin.json b/.claude-plugin/plugin.json index 844ddc8..2c5ea67 100644 --- a/.claude-plugin/plugin.json +++ b/.claude-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "jsc-gitea", - "version": "0.1.2", + "version": "0.1.3", "description": "Gitea API 工具、Wiki 讀寫與存取庫批次同步", "skills": "./skills", "author": { diff --git a/.codex-plugin/plugin.json b/.codex-plugin/plugin.json index 4657e0b..8363c08 100644 --- a/.codex-plugin/plugin.json +++ b/.codex-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "jsc-gitea", - "version": "0.1.2", + "version": "0.1.3", "description": "Gitea API 工具、Wiki 讀寫與存取庫批次同步", "skills": "./skills" } diff --git a/README.md b/README.md index 4aba7df..8e68ee6 100644 --- a/README.md +++ b/README.md @@ -39,6 +39,7 @@ gitea.sh pr-comments / # 印出所有留言(issue gitea.sh pr-depend / / # 把 PR 掛上前置 PR 依賴;依賴未關閉前 Gitea 會阻擋合併 gitea.sh repo-set / [website] # 設定 repo 描述與網頁 +gitea.sh markdown # markdown 檔渲染成 HTML 片段(走 /markdown/raw) gitea.sh api [json-file] hash-id # 與 gitea.sh hash-id 相同 repo-sync.sh / [target-dir] # 同步單一存取庫;印出 cloned、updated、dirty {分支} 或 failed {原因} @@ -46,10 +47,46 @@ repo-sync.sh / [target-dir] # 同步單一存取庫;印 check-wiki-rules.sh # 驗證 wiki repo 解析與 hash fallback 規則 ``` +議題與 HTML 產出: + +``` +gitea-link.sh parse # 解析 wiki 或議題連結;不是這兩種就 exit 3(呼叫端據此中止) +issue.sh labels|label-ids|projects / +issue.sh title|body|labels-of / +issue.sh create / <body-file> [--labels <ids>] [--milestone <id>] +html-style.sh get|set|unset|list|layouts|styles # 種類對版型與風格的設定 +html-render.sh --markdown <檔案> --title <標題> --out <輸出檔> [--layout] [--style] [--subtitle] [--source-url] +``` + ## 參考資料 - `references/wiki-links.md`:寫 wiki 頁才需要的連結規則。同類型用 `[[顯示文字|頁名]]`(顯示文字在左),跨類型用 `wiki-url` 給的絕對網址。 +## HTML 範本 + +版型(`templates/html/layout/*.html`)決定內容怎麼排,風格(`templates/html/style/*.css`)決定看起來長怎樣。兩者自由搭配,六乘五共三十種。 + +| 版型 | 內容排法 | +| --- | --- | +| `report` | 左側目錄加章節內文,長文件用 | +| `slide` | 一個 `##` 一張投影片,鍵盤左右鍵翻頁 | +| `dashboard` | 每個 `##` 一張卡片並排 | +| `spec` | 表格表頭固定、程式碼區塊放大,API 文件用 | +| `timeline` | 每個 `##` 一個節點串成一條線 | +| `onepager` | 窄欄單頁,印出來剛好一頁 | + +| 風格 | 視覺 | +| --- | --- | +| `minimal` | 白底細線、無襯線,資訊密度優先 | +| `corporate` | 深藍主色、表頭反白,正式對外 | +| `dark` | 深底亮字 | +| `print` | 襯線字、A4 邊界,列印或轉 PDF | +| `vivid` | 高彩度、圓角卡片、漸層標題 | + +哪一種頁面套哪一組,由 `html-style.sh` 的設定決定:專案的 `./.jsc/html-styles` 優先,其次 `$JSC_HOME/html-styles.conf`,種類對不到就退 `DEFAULT`,再對不到才用內建的 `report`/`minimal`。設定的 key 是 `WIKI:{頁名前綴}`、`ISSUE:{標籤名}` 或 `DEFAULT`。 + +自訂範本:版型放進 `templates/html/layout/`,風格放進 `templates/html/style/`,檔案第一行寫一句繁中說明——那句話就是技能問使用者時顯示的選項說明。版型檔可用的佔位有 `{{TITLE}}`、`{{SUBTITLE}}`、`{{CONTENT}}`、`{{BASE}}`、`{{BASE_JS}}`、`{{STYLE}}`、`{{SOURCE}}`、`{{GENERATED}}`、`{{LAYOUT}}`、`{{STYLE_NAME}}`。 + ## Skills 目錄 呼叫方式:Claude / Antigravity `/jsc-gitea:{name}`;Codex `${name}`;Copilot / Kiro 描述需求自動觸發。 @@ -64,6 +101,18 @@ Gitea wiki 頁讀寫的統一入口:依頁面類型(QUESTION / PLAN / ANALYZ 存取庫批次同步:列出 owner → 使用者選擇 → 逐 repo(sub agent)呼叫 `tools/repo-sync.sh` clone 或更新;回報 `dirty {分支}` 的存取庫交給 `jsc-git:pr`,base 直接用腳本帶出來的那個分支。 +### `html-export` + +把一頁 wiki 或一筆議題輸出成單一 HTML 檔:解析連結 → 判斷種類 → 查該種類的版型與風格 → 用 Gitea 自己的 markdown 渲染出圖。CSS 與腳本全部內嵌,檔案拿到哪裡都打得開。**沒有連結就直接中止**,不猜存取庫、不猜頁名、不猜議題編號。 + +### `html-style` + +設定「哪一種 wiki 頁或議題,出 HTML 時用哪一種版型與風格」:六種版型與五種風格全部列給使用者選,再寫進專案的 `.jsc/html-styles` 或全域的 `$JSC_HOME/html-styles.conf`。`html-export` 讀的就是這份設定。 + +### `wiki-to-issue` + +把一頁 wiki 轉成同一個存取庫的議題:讀頁面 → sub agent 起草標題與正文(開頭附來源連結)→ 從既有標籤挑合適的 → 關聯專案看板 → 建立議題。標籤只從存取庫既有的挑,不自己發明;站台沒有看板 API 時據實回報請使用者手動拖,不假裝關聯成功。**沒有連結就直接中止**。 + <!-- JSC-SKILLS:END --> ## 環境變數 diff --git a/plugin.json b/plugin.json index 3c54f99..0b089b1 100644 --- a/plugin.json +++ b/plugin.json @@ -1,6 +1,6 @@ { "name": "jsc-gitea", - "version": "0.1.2", + "version": "0.1.3", "description": "Gitea API 工具、Wiki 讀寫與存取庫批次同步", "skills": "./skills/" } diff --git a/tools/gitea.sh b/tools/gitea.sh index 45bf432..f311640 100755 --- a/tools/gitea.sh +++ b/tools/gitea.sh @@ -19,6 +19,7 @@ # gitea.sh pr-depend <owner>/<repo> <pr-index> <dep-owner>/<dep-repo> <dep-index> # 把 PR 掛上前置 PR 依賴;依賴未關閉前 Gitea 會阻擋合併 # gitea.sh repo-set <owner>/<repo> <description> [website] # 設定 repo 描述與網頁 +# gitea.sh markdown <file> # markdown 檔渲染成 HTML 片段(走 /markdown/raw) # gitea.sh api <METHOD> <path> [json-file] # 原始 API 呼叫(path 以 /repos/... 起始) # 環境變數: GITEA_HOST(例 https://gitea.jsc.idv.tw)、GITEA_TOKEN # GITEA_TOKEN 未設定,或請求遇 401/403 時,自動退回 tea CLI 的登入 token @@ -95,13 +96,16 @@ fi case "$GITEA_HOST" in http://*|https://*) HOST="$GITEA_HOST" ;; *) HOST="https://$GITEA_HOST" ;; esac API="${HOST%/}/api/v1" -req() { # METHOD path [json-file] -> body(HTTP >= 400 時 exit 4;401/403 以 tea token 重試一次) +req() { # METHOD path [body-file] -> body(HTTP >= 400 時 exit 4;401/403 以 tea token 重試一次) + # 送出的 Content-Type 由 REQ_CONTENT_TYPE 決定,預設 application/json; + # /markdown/raw 這種吃純文字的端點要先改成 text/plain 再呼叫。 method="$1"; path="$2"; body_file="${3:-}" + ctype="${REQ_CONTENT_TYPE:-application/json}" retried=0 while :; do if [ -n "$body_file" ]; then out=$(curl -sS -w '\n%{http_code}' -X "$method" \ - -H "Authorization: token $GITEA_TOKEN" -H "Content-Type: application/json" \ + -H "Authorization: token $GITEA_TOKEN" -H "Content-Type: $ctype" \ --data-binary "@$body_file" "$API$path") else out=$(curl -sS -w '\n%{http_code}' -X "$method" \ @@ -279,6 +283,15 @@ print(json.dumps(b)) fi rm -f "$tmp" echo "OK $or" ;; + markdown) + # markdown 檔 -> HTML 片段。走 /markdown/raw(吃純文字)而不是 /markdown: + # 後者在 Gitea 1.27 回 200 但內容是空的,看起來像成功,其實什麼都沒渲染。 + # 代價:raw 端點不吃 context,wiki 的 [[頁名]] 內部連結不會變成連結, + # 呼叫端要先把它換成絕對網址。 + file="${1:?markdown file}" + [ -f "$file" ] || { echo "找不到 markdown 檔: $file" >&2; exit 2; } + REQ_CONTENT_TYPE='text/plain' + req POST "/markdown/raw" "$file" ;; api) method="${1:?METHOD}"; path="${2:?path}"; body="${3:-}" req "$method" "$path" $body ;; From dbe800d7a7c9928a0241b7e4a06a48f1ff48d8f8 Mon Sep 17 00:00:00 2001 From: Jeffery <jiantw83@yahoo.com> Date: Wed, 26 Aug 2026 09:22:00 +0800 Subject: [PATCH 7/7] =?UTF-8?q?chore(gitea):=20manifest=20=E6=8F=8F?= =?UTF-8?q?=E8=BF=B0=E8=A3=9C=E4=B8=8A=E8=AD=B0=E9=A1=8C=E8=BD=89=E6=8F=9B?= =?UTF-8?q?=E8=88=87=20HTML=20=E5=8C=AF=E5=87=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit What:三份 plugin manifest 的 description 補上這次新增的兩類能力。 Why:description 是各 CLI 顯示這個 plugin 用途的依據,少了新能力就找不到人用。 How:三份同步改成同一句;marketplace 正本的條目描述留給下一次 skill-check 統一處理,因為那份要同時改到十一個存取庫的副本。 Who:在 CLI 裡挑 plugin 的人。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> --- .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 2c5ea67..9dbc225 100644 --- a/.claude-plugin/plugin.json +++ b/.claude-plugin/plugin.json @@ -1,7 +1,7 @@ { "name": "jsc-gitea", "version": "0.1.3", - "description": "Gitea API 工具、Wiki 讀寫與存取庫批次同步", + "description": "Gitea API 工具、Wiki 讀寫、議題轉換、HTML 匯出與存取庫批次同步", "skills": "./skills", "author": { "name": "JSC" diff --git a/.codex-plugin/plugin.json b/.codex-plugin/plugin.json index 8363c08..d6a7060 100644 --- a/.codex-plugin/plugin.json +++ b/.codex-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "jsc-gitea", "version": "0.1.3", - "description": "Gitea API 工具、Wiki 讀寫與存取庫批次同步", + "description": "Gitea API 工具、Wiki 讀寫、議題轉換、HTML 匯出與存取庫批次同步", "skills": "./skills" } diff --git a/plugin.json b/plugin.json index 0b089b1..312ee78 100644 --- a/plugin.json +++ b/plugin.json @@ -1,6 +1,6 @@ { "name": "jsc-gitea", "version": "0.1.3", - "description": "Gitea API 工具、Wiki 讀寫與存取庫批次同步", + "description": "Gitea API 工具、Wiki 讀寫、議題轉換、HTML 匯出與存取庫批次同步", "skills": "./skills/" }