feat(模型標籤閘門): 新增 model-tags.sh 與 reasoning-max 分級,SDLC 階段閘門改由能力標籤判定

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-08-24 16:05:06 +08:00
co-authored by Claude Opus 5
parent f2029d4cd0
commit 519f1163db
3 changed files with 190 additions and 11 deletions
+155
View File
@@ -0,0 +1,155 @@
#!/usr/bin/env sh
# model-tags.sh — 解析「模型能力標籤」與「SDLC 階段必要標籤」,並判定模型是否夠格跑某階段。
#
# 資料來源優先序(前一項可取得就不往下):
# 1. $JSC_HOME/model-tags.tsv(機器可讀快取,可手改,用來補表列以外的模型)
# 2. 本 plugin 的 references/model-tags.md(唯一權威來源,不在腳本內另存一份對照表)
#
# 用法:
# model-tags.sh dump 把對照表印成 TSV,不寫檔
# model-tags.sh sync 把對照表寫進 $JSC_HOME/model-tags.tsv(給 hook 讀)
# model-tags.sh stage {stage} 印出該階段的必要標籤(逗號分隔);階段不限標籤印 any
# model-tags.sh model {model-id} 印出該模型的標籤(逗號分隔);查不到不印
# model-tags.sh gate {stage} {model-id} 判定該模型能否跑該階段
#
# TSV 欄位(三欄,Tab 分隔):
# stage<TAB>{階段}<TAB>{必要標籤以逗號分隔,不限標籤時為 any}
# model<TAB>{模型鍵}<TAB>{能力標籤以逗號分隔}
#
# gate 的輸出與 exit code:
# PASS exit 0 模型具備該階段全部必要標籤
# FAIL:{缺少的標籤} exit 1 模型缺標籤,不得執行該階段
# UNKNOWN-MODEL exit 2 模型不在對照表上,無法判定
# UNKNOWN-STAGE exit 2 階段不在對照表上,無法判定
set -u
HERE=$(dirname "$0")
REF="$HERE/../references/model-tags.md"
JSC_HOME="${JSC_HOME:-$HOME/.jsc}"
TSV="$JSC_HOME/model-tags.tsv"
# 解析 references/model-tags.md 的兩張表格,輸出 TSV。
# 「## 對照表」段落 → model 列;「## SDLC 階段需求」段落 → stage 列。
# 模型欄可能寫成「gpt-5.x / o 系列(codex 預設)」這種人讀格式,
# 因此先砍掉全形括號後的說明,再用「/」切成多個鍵,只留純 ASCII 的鍵。
parse_ref() {
[ -f "$REF" ] || return 0
awk '
function trim(s) { sub(/^[ \t]+/, "", s); sub(/[ \t]+$/, "", s); return s }
# 砍掉第一個全形左括號之後的內容(表中括號都是尾端的補充說明)。
# 一律用 split 切字串,不用 substr 算位移——全形字元的長度在不同 locale 下
# 會被算成 1 個字元或 3 個 byte,位移法會在其中一種 locale 下算錯。
function strip_note(s) { split(s, a, "("); return trim(a[1]) }
# 取全形括號內的文字(例:規劃(plan) → plan)
function paren(s) {
if (split(s, a, "(") < 2) return ""
split(a[2], b, ")")
return trim(b[1])
}
function is_ascii_key(s) { return s ~ /^[A-Za-z0-9._-]+$/ }
function norm_tags(s) { gsub(/[ \t]/, "", s); return s }
/^##[ \t]/ {
if (index($0, "對照表") > 0) sect = "model"
else if (index($0, "SDLC 階段需求") > 0) sect = "stage"
else sect = ""
next
}
sect == "" { next }
/^\|/ {
line = $0
sub(/^\|/, "", line); sub(/\|[ \t]*$/, "", line)
n = split(line, f, "|")
if (n < 2) next
c1 = trim(f[1]); c2 = trim(f[2])
if (c1 == "" || c2 == "") next
if (c1 ~ /^:?-+:?$/) next # 表頭分隔列
if (c1 == "模型" || c1 == "階段" || c1 == "標籤") next # 表頭
if (sect == "stage") {
key = paren(c1)
if (key == "") key = c1
if (!is_ascii_key(key)) next
tags = (index(c2, "任意") > 0) ? "any" : norm_tags(c2)
printf "stage\t%s\t%s\n", key, tags
next
}
tags = norm_tags(c2)
if (tags == "") next
m = split(strip_note(c1), keys, "/")
for (i = 1; i <= m; i++) {
k = trim(keys[i])
if (k == "" || !is_ascii_key(k)) continue
printf "model\t%s\t%s\n", k, tags
}
}
' "$REF"
}
# 取得對照表內容:快取優先,沒有快取才解析 md。
data() {
if [ -s "$TSV" ]; then
cat "$TSV"
else
parse_ref
fi
}
stage_tags() { # $1=階段
data | awk -F'\t' -v s="$1" '$1 == "stage" && $2 == s { print $3; exit }'
}
# 模型鍵與實際模型 id 雙向包含即視為同一家族。
# 例:表列 claude-haiku-4-5 對得上實際 id claude-haiku-4-5-20251001。
# 多筆命中時取最長鍵,避免 claude 這種短鍵蓋掉精確鍵。
model_tags() { # $1=模型 id
data | awk -F'\t' -v m="$1" '
$1 == "model" && (index(m, $2) > 0 || index($2, m) > 0) {
if (length($2) > best_len) { best_len = length($2); best = $3 }
}
END { if (best != "") print best }
'
}
gate() { # $1=階段 $2=模型 id
req=$(stage_tags "$1")
if [ -z "$req" ]; then echo "UNKNOWN-STAGE"; return 2; fi
[ "$req" = "any" ] && { echo "PASS"; return 0; }
have=$(model_tags "$2")
if [ -z "$have" ]; then echo "UNKNOWN-MODEL"; return 2; fi
missing=$(awk -v req="$req" -v have="$have" '
BEGIN {
n = split(have, h, ","); for (i = 1; i <= n; i++) owned[h[i]] = 1
n = split(req, r, ",")
for (i = 1; i <= n; i++) if (!(r[i] in owned)) out = (out == "" ? r[i] : out "," r[i])
print out
}')
if [ -n "$missing" ]; then echo "FAIL:$missing"; return 1; fi
echo "PASS"; return 0
}
usage() {
echo "用法:model-tags.sh dump | sync | stage {階段} | model {模型 id} | gate {階段} {模型 id}" >&2
exit 1
}
case "${1:-}" in
dump) parse_ref; exit 0 ;;
sync)
out=$(parse_ref)
if [ -z "$out" ]; then
echo "[jsc] 解析不到 $REF,未寫出 $TSV" >&2
exit 1
fi
mkdir -p "$JSC_HOME" 2>/dev/null || true
printf '%s\n' "$out" > "$TSV" || exit 1
echo "$TSV"
exit 0 ;;
stage) [ -n "${2:-}" ] || usage; stage_tags "$2"; exit 0 ;;
model) [ -n "${2:-}" ] || usage; model_tags "$2"; exit 0 ;;
gate)
[ -n "${2:-}" ] && [ -n "${3:-}" ] || usage
gate "$2" "$3"; exit $? ;;
*) usage ;;
esac