Files
cli/tools/model-config.sh
jiantw83 f2029d4cd0 feat(cli): 新增 model-config.sh 的 resolve 子指令
What:
在 `tools/model-config.sh` 新增 `resolve {stage}` 子指令。這個指令會取出某個 SDLC 階段的模型鏈(跟 `get {stage}` 用同樣的解析規則:先讀專案的 `.jsc/models`,找不到再讀 `$JSC_HOME/models.conf`),然後印出鏈中第一個模型名稱,當作目前這個 CLI 可用的模型。同時新增 `current_cli()` 這個輔助函式,透過各家 CLI 的環境變數,判斷目前是在 claude、codex、copilot、antigravity、還是 kiro 底下執行。這個版本先把 `current_cli()` 的結果記錄下來,還沒有拿它去過濾模型鏈中不可用的模型,這件事留到之後再做,因為要判斷某個模型是否可用,需要 LLM 層級的判斷,不是單純的 shell script 能做到的。之後的計畫是拿 `current_cli()` 的結果,對照 `references/model-tags.md` 和 `detect-clis.sh`,跳過模型鏈裡不可用的模型。另外,把 README.md 裡 `tools/model-config.sh` 的工具說明表格,加上新的 `resolve {stage}` 子指令說明。版本號也從 0.0.2 升到 0.0.3,同步改了 `plugin.json`、`.claude-plugin/plugin.json`、`.codex-plugin/plugin.json` 這三份清單檔,這是這個 repo 的慣例:新增功能時要一併升版號。

Why:
jsc-meta:skill-check 這次稽核發現,jsc-sdlc 底下 plan、analyze、implement、maintain 這四個階段的技能,各自都要幫自己的階段挑一個模型,來做 gate check。如果沒有 `resolve` 這個子指令,這四個技能就要各自在技能說明裡,重複寫一次「讀模型鏈、選一個候補模型」的邏輯。這樣容易寫錯,也難維護。

How:
在既有的 `get`/`list` 邏輯基礎上,加一個 `resolve` 子指令,直接回傳模型鏈的第一個模型。同時加上 `current_cli()`,先把偵測 CLI 的能力做出來,但先不接上過濾邏輯,用註解說明後續要怎麼接。文件和版本號一併更新。

Who:
這個功能是給 jsc-sdlc 的四個階段技能(plan、analyze、implement、maintain)用的。它們可以直接呼叫 `resolve {stage}`,取得一個可用的模型名稱,不用各自重複寫模型鏈的判斷邏輯。
2026-08-24 14:49:28 +08:00

123 lines
4.4 KiB
Bash
Executable File
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env sh
# model-config.sh — 解析 SDLC 各階段的指定模型鏈(角色式路由)。
# 設定格式(一行一階段):stage=model[,fallback1[,fallback2...]]
# 「#」開頭為註解,空白行忽略。階段限定:plan、analyze、implement、maintain。
# 解析順序:
# 1. 目前工作目錄的 ./.jsc/models(專案覆寫)
# 2. $JSC_HOME/models.conf(JSC_HOME 預設 ~/.jsc)
# 專案檔有設定的階段完全覆寫全域設定;其餘階段仍讀全域檔。
# 用法:
# model-config.sh get {stage} 印出該階段解析後的模型鏈(逗號分隔);未設定不印。兩種情況都 exit 0。
# model-config.sh list 每階段一行:stage<TAB>chain<TAB>source(project 或 global);未設定的 chain 與 source 印「-」。
# model-config.sh resolve {stage} 印出該階段「目前 CLI 可用的第一個模型」單一名稱;鏈為空或無法判定時不印,exit 0。
set -u
JSC_HOME="${JSC_HOME:-$HOME/.jsc}"
PROJECT_FILE=./.jsc/models
GLOBAL_FILE="$JSC_HOME/models.conf"
STAGES="plan analyze implement maintain"
# 取出單一設定檔裡某階段的模型鏈。
# 同階段出現多行時取最後一行,結果才有確定性。
read_chain() { # $1=設定檔 $2=階段
[ -f "$1" ] || return 0
awk -F= -v stage="$2" '
{ sub(/\r$/, "") } # 去掉 CR
/^[ \t]*#/ { next } # 註解行
/^[ \t]*$/ { next } # 空白行
index($0, "=") == 0 { next }
{
key = $1
gsub(/[ \t]/, "", key)
if (key != stage) next
val = substr($0, index($0, "=") + 1)
gsub(/[ \t]/, "", val)
if (val != "") chain = val
}
END { if (chain != "") print chain }
' "$1"
}
# 解析單一階段:專案檔優先,該階段沒設定才退回全域檔。
# 輸出:chain<TAB>source;兩檔都沒設定時不輸出。
resolve() { # $1=階段
chain=$(read_chain "$PROJECT_FILE" "$1")
if [ -n "$chain" ]; then
printf '%s\tproject\n' "$chain"
return 0
fi
chain=$(read_chain "$GLOBAL_FILE" "$1")
if [ -n "$chain" ]; then
printf '%s\tglobal\n' "$chain"
fi
return 0
}
# 判斷目前是哪個 CLI 在跑(盡量而為,非必要)。
# 依 CLI 官方常見環境變數判斷;偵測不到就回傳 unknown。
current_cli() {
[ -n "${CLAUDECODE:-}" ] && { echo claude; return 0; }
[ -n "${CODEX_SANDBOX:-}${CODEX_HOME:-}" ] && { echo codex; return 0; }
[ -n "${COPILOT_AGENT:-}" ] && { echo copilot; return 0; }
[ -n "${ANTIGRAVITY_SESSION:-}" ] && { echo antigravity; return 0; }
[ -n "${KIRO_SESSION:-}" ] && { echo kiro; return 0; }
echo unknown
}
# resolve 子指令:印出「目前 CLI 可用的第一個模型」單一名稱。
# 簡化說明:這個 repo 目前沒有「列出某 CLI 實際安裝/可用模型」的 shell 級機制
# ——現有的 skills/models 是靠 sub agent 讀各 CLI 的設定檔(~/.claude/settings.json、
# ~/.codex/config.toml 等),屬於 LLM 才能做的判讀,沒辦法在這支 POSIX sh 腳本裡重現。
# 因此這裡只用 current_cli() 抓到的「目前 CLI 名稱」這個既有訊號做記錄,
# 實際判斷可用性一律簡化為「直接取模型鏈的第一個模型」,不逐一檢查該模型是否真的能用。
# 之後若要做到依 CLI 實際可用模型過濾,可以在這裡比對 current_cli 的結果與
# references/model-tags.md/detect-clis.sh 的輸出,逐一嘗試鏈上模型直到找到可用的。
resolve_first_usable() { # $1=階段
chain=$(resolve "$1" | cut -f1)
[ -n "$chain" ] || return 0
cli=$(current_cli)
: "目前偵測到的 CLI:$cli(本版本尚未用來篩選模型,見上方簡化說明)"
printf '%s\n' "$chain" | cut -d, -f1
return 0
}
usage() {
echo "用法:model-config.sh get {plan|analyze|implement|maintain} | list | resolve {plan|analyze|implement|maintain}" >&2
exit 1
}
case "${1:-}" in
get)
stage="${2:-}"
case " $STAGES " in
*" $stage "*) ;;
*) usage ;;
esac
resolve "$stage" | cut -f1
exit 0
;;
resolve)
stage="${2:-}"
case " $STAGES " in
*" $stage "*) ;;
*) usage ;;
esac
resolve_first_usable "$stage"
exit 0
;;
list)
for stage in $STAGES; do
line=$(resolve "$stage")
if [ -n "$line" ]; then
printf '%s\t%s\n' "$stage" "$line"
else
printf '%s\t-\t-\n' "$stage"
fi
done
exit 0
;;
*)
usage
;;
esac