ISEP 0.1.0:環境設定收成一個 plugin,本機與雲端共用一份

leo 2026-08-20:「同一個 plugin 你用,薄殼也用,保證兩邊同步」
              「我要你幫雲端做薄殼,永遠都有問題,你要做的就是這組設定
                你自己可以 dogfooding」

搬進來:41 支 hook(51 條註冊)/7 支 command/2 支 skill/23 支腳本。
不搬 .env、wiki、docs——那些是知識不是環境。

51 條 hook 路徑全部從 $CLAUDE_PROJECT_DIR/.claude/hooks/ 改成 ${CLAUDE_PLUGIN_ROOT}/hooks/,
零漏網。那正是薄殼一直壞掉的根:雲端 cwd 不是真身,寫死路徑就斷。

尚未驗證:Claude Code 能不能從私有 Gitea repo 裝 marketplace(要憑證)。
下一步就是在本機實際裝一次,通了才動雲端 bootstrap.sh。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-20 11:41:46 +08:00
commit c2638668e3
85 changed files with 11879 additions and 0 deletions
+113
View File
@@ -0,0 +1,113 @@
#!/usr/bin/env python3
"""
記分板:把一次(或多次)考試的資料判成 ✅/◐/❌ 表。
方法論鐵律(`kbdb-動作清單考卷.md` §七點五):**每格至少跑三次**,
記成 `n/3`,不寫單一結果——一次的差異一律當雜訊。
兩種來源:
本機乾跑 report.py --sqlite a.db b.db c.db
正式考試 report.py --d1 <dbname> --account-id … --token-env … --runs A,B,C
🔴 **正式考試三次跑在同一台實例上**,所以每一次有自己的 run tag:
sheet 叫 `xq<tag><題號>_…`L3 的那個人也每次換一個名字
——否則 A 跑留下的東西會被算進 B 跑的分數。
"""
import argparse
import os
import sqlite3
import sys
from collections import defaultdict
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from grade import Pool, signals, verdict, RUBRIC, LOAD_SQL, load_d1 # noqa: E402
QUESTIONS = ["L1", "L2", "L3", "L4", "L5", "L6"]
# 每一次考試自己的人名(L3 的 entity_once 是**整池**判定,不換名字會跨 run 互相污染)
RUN_PERSON = {"": "王小明", "A": "王小明", "B": "陳大文", "C": "林志明"}
def load_rows(source, dbname="", account_id="", token=""):
if source.endswith(".db"):
con = sqlite3.connect(source)
con.row_factory = sqlite3.Row
rows = [dict(r) for r in con.execute(LOAD_SQL)]
con.close()
return rows
return load_d1(dbname, account_id, token)
def grade_rows(rows, qid, tag=""):
pool = Pool(rows)
prefix = f"xq{tag}{qid}"
scope = [sid for sid, name in pool.sheets().items()
if (name or "").startswith(prefix)]
sig = signals(pool, scope)
rule = dict(RUBRIC[qid])
if "entity_once" in rule:
rule = {**rule, "entity_once": RUN_PERSON.get(tag, "王小明")}
# verdict 讀的是 RUBRIC,這裡暫時換掉那一格再換回來(不改共用狀態的語意)
saved = RUBRIC[qid]
RUBRIC[qid] = rule
try:
mark, notes = verdict(qid, sig, None)
finally:
RUBRIC[qid] = saved
return mark, notes, sig
def main():
ap = argparse.ArgumentParser()
ap.add_argument("--sqlite", nargs="*", default=[])
ap.add_argument("--d1", default="")
ap.add_argument("--account-id", default="")
ap.add_argument("--token-env", default="CLOUDFLARE_API_TOKEN_YOULIN_CC_USE")
ap.add_argument("--runs", default="", help="正式考試的 run tag,逗號分隔,如 A,B,C")
a = ap.parse_args()
jobs = [] # (label, rows, tag)
if a.sqlite:
for p in a.sqlite:
jobs.append((os.path.basename(os.path.dirname(p)) or os.path.basename(p),
load_rows(p), ""))
if a.d1:
rows = load_rows("d1", a.d1, a.account_id, os.environ.get(a.token_env, ""))
print(f"(從 D1 讀到 {len(rows)} 顆 entry —— 判分只看落地資料,不看受測者自述)")
for tag in [t.strip() for t in a.runs.split(",") if t.strip()]:
jobs.append((f"run {tag}", rows, tag))
tally = defaultdict(list)
for label, rows, tag in jobs:
print(f"\n════ {label} ════")
# 🔴 生死檢查,**必須在計分之前**(2026-08-16 實撞):
# L3/L6 的正確答案是「什麼都不寫」——所以一個**完全沒動手**的受測者
# 會在那兩題拿到 ✅。那不是答對,那是沒考。
# 實例:run A 自述六題全做完、還報出每一筆內容,而 D1 裡一筆都沒有。
# ⇒ 先問「這次到底有沒有寫進任何東西」,沒有就整場作廢,不給任何 ✅。
wrote = sum(grade_rows(rows, q, tag)[2]["records"] for q in ("L1", "L2", "L4", "L5"))
if wrote == 0:
print(" 🔴 這一場作廢:四個「該寫東西」的題目加起來一筆都沒落地。")
print(" (L3/L6 的正解是不寫 ⇒ 沒動手的人會假性通過那兩題,不予計分)")
for q in QUESTIONS:
tally[q].append("作廢")
continue
for q in QUESTIONS:
mark, notes, sig = grade_rows(rows, q, tag)
tally[q].append(mark)
print(f" {q} {mark} {RUBRIC[q]['title']}")
for n in notes:
print(f"{n}")
n = len(jobs)
print(f"\n════ 記分板(n={n},只有跨全部樣本一致的差異才算訊號)════")
print(f"{'':<5}{'':<5}{'':<5}{'':<5}{'作廢':<5} 標題")
for q in QUESTIONS:
m = tally[q]
print(f"{q:<5}{m.count(chr(9989)):<5}{m.count(chr(9680)):<5}{m.count(chr(10060)):<5}{m.count('作廢'):<5} {RUBRIC[q]['title']}")
if __name__ == "__main__":
main()