feat(gitea): 新增 wiki 轉議題技能
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>
This commit is contained in:
Executable
+77
@@ -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
|
||||
Executable
+157
@@ -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
|
||||
Reference in New Issue
Block a user