c2638668e3
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>
114 lines
4.7 KiB
Python
114 lines
4.7 KiB
Python
#!/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()
|