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>
154 lines
7.2 KiB
Python
154 lines
7.2 KiB
Python
#!/usr/bin/env python3
|
||
"""
|
||
考前指紋:**這一筆數據是打在哪一版 kbdb 上量出來的。**
|
||
|
||
立它的理由(總管 2026-08-15,出貨管線 preflight 抓到的):
|
||
kbdb 的出貨成品比源碼舊 3 顆 commit,其中一顆正是 v7 拆表那顆
|
||
⇒ youlin 上那顆 worker 什麼時候變成 v7,取決於出貨走到哪一步。
|
||
⇒ **不標指紋就會量出「模型寫錯了」,而真兇是它打到的那台還在跑舊模型。**
|
||
|
||
兩個獨立來源,**刻意都留**(一個從使用者那條路量、一個從帳號那邊量):
|
||
|
||
① 行為指紋(不需要任何憑證,走 `acr` = 使用者真的會走的那條路)
|
||
建一張拋棄式 sheet,寫一筆 → 回應直接說出它是哪一版:
|
||
`no such table: entry_values` → 舊碼(v7 之前)
|
||
成功 → 新碼(v7 之後)
|
||
🔑 這一支的價值在於**它量的就是受測者會撞到的那個東西**。
|
||
|
||
② 部署指紋(需要該帳號的 CF token,唯讀)
|
||
worker 的 modified_on + etag。它答的是「這顆什麼時候被換過」。
|
||
|
||
⚠️ 兩者都不是 commit sha。**worker 上沒有 sha 可讀**——
|
||
所以這裡誠實地記「行為 + 換過的時間」,不假裝知道它是哪一顆 commit。
|
||
"""
|
||
import argparse
|
||
import json
|
||
import os
|
||
import subprocess
|
||
import time
|
||
import uuid
|
||
|
||
|
||
def behavior_fingerprint(workdir):
|
||
"""走 acr(使用者那條路)問一句:你是 v7 之前還是之後?"""
|
||
name = f"xqfp_{uuid.uuid4().hex[:8]}"
|
||
out = {"probe_sheet": name}
|
||
|
||
def run(args):
|
||
r = subprocess.run(["acr", "kbdb"] + args, cwd=workdir,
|
||
capture_output=True, text=True, timeout=120)
|
||
return r.returncode, (r.stdout + r.stderr).strip()
|
||
|
||
who = subprocess.run(["acr", "whoami"], cwd=workdir,
|
||
capture_output=True, text=True, timeout=60).stdout
|
||
out["whoami"] = [l.strip() for l in who.splitlines() if l.strip()][:6]
|
||
|
||
rc, txt = run(["template", "create", name, "--slots", "a,b"])
|
||
out["template_create"] = {"exit": rc, "tail": txt[-160:]}
|
||
|
||
rc, txt = run(["record", "create", name, "--values", "a=1", "--values", "b=2"])
|
||
out["record_create"] = {"exit": rc, "tail": txt[-200:]}
|
||
|
||
if rc == 0:
|
||
# 🔴 2026-08-16 這一行我寫錯過一次,留著當警示:
|
||
# 原本是「rc==0 ⇒ v7+(關係列模型已上線)」。**寫得進去 ≠ 寫成新形狀。**
|
||
# 那天 acr update 之後 record create 又成功了,我就宣告 v7+;
|
||
# 實際是 `entry_values` 被 migration 重播**復活**,舊碼照樣往那張表寫
|
||
# ⇒ 考卷寫進去的 310 列全在那張死掉的表裡,新模型那邊一列都沒長。
|
||
# ⇒ 行為探針只能證明「寫得進去」,**證明不了寫成什麼形狀**。形狀要另外量。
|
||
out["kbdb_generation"] = "寫得進去,但**形狀未驗**——要看 shape 那一段才算數"
|
||
elif "no such table: entry_values" in txt:
|
||
out["kbdb_generation"] = "pre-v7(舊碼碰新庫:worker 還在寫 entry_values)"
|
||
else:
|
||
out["kbdb_generation"] = "未知(寫入失敗但不是拆表那個原因,看 tail)"
|
||
return out
|
||
|
||
|
||
def deploy_fingerprint(account_id, token, scripts=("arcrun-kbdb", "arcrun-cypher-executor",
|
||
"arcrun-mcp")):
|
||
"""worker 上一次被換掉是什麼時候(唯讀)。"""
|
||
r = subprocess.run(
|
||
["curl", "-s", "--max-time", "25", "-H", f"Authorization: Bearer {token}",
|
||
f"https://api.cloudflare.com/client/v4/accounts/{account_id}/workers/scripts"],
|
||
capture_output=True, text=True, timeout=60).stdout
|
||
try:
|
||
data = json.loads(r).get("result", [])
|
||
except Exception:
|
||
return {"error": "CF API 回應不是 JSON"}
|
||
return {s["id"]: {"modified_on": s.get("modified_on"), "etag": (s.get("etag") or "")[:16]}
|
||
for s in data if s["id"] in scripts}
|
||
|
||
|
||
# 受測者可能讀到教材的地方。**一個都不能漏,漏掉的那份會安靜地教錯。**
|
||
# 2026-08-15 實證:同名的 kbdb-api-wall-guard.sh 有兩份都會開火,
|
||
# 只有 repo 那份被修好(efa64d8),全域 skill 那份還在說
|
||
# 「exactly three core tables: entries / templates / entry_values」。
|
||
MATERIAL_PATHS = [
|
||
"~/.claude/skills/arcrun-kbdb-guardrails",
|
||
".claude/hooks/kbdb-api-wall-guard.sh",
|
||
"arcrun_harness/.claude/skills/arcrun-kbdb-guardrails",
|
||
]
|
||
# 已經死掉的東西:教材裡出現它**而且沒有同時說它死了**,就是還在當現行做法教。
|
||
DEAD_TERMS = ["entry_values"]
|
||
TOMBSTONE_MARKS = ["🪦", "廢除", "已死", "0007", "已被推翻", "提議廢掉", "尚未 confirm"]
|
||
|
||
|
||
def materials_fingerprint(paths=MATERIAL_PATHS):
|
||
"""教材指紋:這一筆數據是在哪一版教材底下量的。
|
||
|
||
🔑 判準不是「有沒有提到死掉的東西」——**歷史要留著**
|
||
(保留歷史而不是抹掉,那是 efa64d8 自己選的做法,對的)。
|
||
判準是「提到它的那一段,有沒有說它死了」。
|
||
"""
|
||
out = {}
|
||
for p in paths:
|
||
real = os.path.expanduser(p)
|
||
if not os.path.exists(real):
|
||
out[p] = {"status": "不存在"}
|
||
continue
|
||
walk = ([real] if os.path.isfile(real)
|
||
else [os.path.join(d, f) for d, _, fs in os.walk(real) for f in fs])
|
||
files = []
|
||
for f in walk:
|
||
if f.endswith((".png", ".jpg", ".pyc", ".bak")):
|
||
continue
|
||
try:
|
||
lines = open(f, encoding="utf-8", errors="ignore").read().splitlines()
|
||
except Exception:
|
||
continue
|
||
stale = []
|
||
for i, line in enumerate(lines):
|
||
if not any(t in line for t in DEAD_TERMS):
|
||
continue
|
||
# 墓碑註記可能寫在前後幾行,看一個小範圍再判
|
||
#(避免把「刻意保留的歷史」誤報成過期教材)
|
||
ctx = "\n".join(lines[max(0, i - 3):i + 4])
|
||
if not any(m in ctx for m in TOMBSTONE_MARKS):
|
||
stale.append(i + 1)
|
||
if stale:
|
||
files.append({"file": f.replace(os.path.expanduser("~"), "~"),
|
||
"stale_lines": stale[:8]})
|
||
out[p] = {"status": "🔴 還在教死掉的東西" if files else "✅ 乾淨", "files": files}
|
||
return out
|
||
|
||
|
||
def main():
|
||
ap = argparse.ArgumentParser()
|
||
ap.add_argument("--workdir", required=True, help="考場資料夾(含 .arcrun.yaml)")
|
||
ap.add_argument("--materials-only", action="store_true", help="只查教材指紋,什麼都不寫")
|
||
ap.add_argument("--account-id", default="")
|
||
ap.add_argument("--token-env", default="CLOUDFLARE_API_TOKEN_YOULIN_CC_USE")
|
||
ap.add_argument("--no-probe", action="store_true", help="只取部署指紋,不寫任何東西")
|
||
a = ap.parse_args()
|
||
|
||
stamp = {"taken_at": time.strftime("%Y-%m-%dT%H:%M:%S%z")}
|
||
if a.account_id and os.environ.get(a.token_env):
|
||
stamp["deploy"] = deploy_fingerprint(a.account_id, os.environ[a.token_env])
|
||
if not a.no_probe:
|
||
stamp["behavior"] = behavior_fingerprint(a.workdir)
|
||
print(json.dumps(stamp, ensure_ascii=False, indent=2))
|
||
|
||
|
||
if __name__ == "__main__":
|
||
main()
|