雲端第一則 prompt 的主線是現在這條:兩份主線檔都不在時 refresh 去 Gitea 讀 InkStoneCo main 那份一次(inkstone/ISEP#140)
薄殼 SessionStart 跑在 bootstrap.sh 之前,InkStoneCo/ 還沒 clone、家目錄空 ⇒ 兩份都不在 ⇒ countdown.py 退到「期限最近的 milestone」⇒ 第一則 prompt 印逾期 341 小時的舊線。機制沒壞,是順序。 - hooks/lib/mainline.py:第三個來源 fetch_gitea()(本檔唯一會打網路的函式,只有 SessionStart 的 `mainline refresh` 叫它;load()/line() 照舊零網路)。優先序寫進檔頭:家目錄 > repo 那份 > Gitea 那份。 匿名優先,匿名被拒(401/403/404)且拿得到 token 才帶 token 再問一次;讀不到回 None,逾時有上限。 - scripts/mainline refresh:兩份都不在 ⇒ fetch_gitea 一次、只寫家目錄(不 mirror);token 拿不到不炸、不抱怨。 - hooks/tests/mainline-gitea-fallback.test.sh:31 條,假 Gitea 有 public/private/500/garbage/notitle/hang 六種回法。 假設(票上紅線寫「一律匿名不帶 token」,其前提「Gitea 匿名讀得到」實測為假): inkstone/InkStoneCo 在 Gitea 是 private——匿名打 raw/contents/repo API 都 404,ISEP 同端點 200。 ⇒ 照字面做這張票在真環境永遠讀不到。改成匿名優先、被拒才帶 token(同一條 SessionStart 鏈上 countdown-milestone-refresh.sh 本來就帶 GITEA_TOKEN_CLAUDE_CODE 打 Gitea;D20 管的是 GitHub)。 真跑:帶 token 2.06s 讀到 inkstone/Arcrun#48;拔 token rc 0、零檔案、「現在沒有主線」。 版本:待總管定版。 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0178ef1fGw3XeZtpN7LaZrm4
This commit is contained in:
+101
-5
@@ -27,10 +27,21 @@ Gitea 的 milestone 不能跨 repo,而同一條線常常同時開在 5 個 rep
|
||||
· **成員判定**= 票所屬 milestone 的**標題**與主線標題相同(不分 repo)
|
||||
⇒ 「同名跨 repo」照舊能用,而「現在在做哪一條」仍然只有一個答案。
|
||||
|
||||
── 這支從不打網路 ────────────────────────────────────────────────
|
||||
它被 `UserPromptSubmit` 用(每一則訊息的關鍵路徑)。在那裡打 HTTP = 每講一句話
|
||||
── `load()`/`line()` 從不打網路 ─────────────────────────────────
|
||||
它們被 `UserPromptSubmit` 用(每一則訊息的關鍵路徑)。在那裡打 HTTP = 每講一句話
|
||||
都先等一次網路。網路那一半全部在 `scripts/mainline`(set/refresh/adopt),
|
||||
本支只讀寫 state 檔。讀不到 ⇒ 回「沒有主線」,**不編一條出來**。
|
||||
本支的讀寫只碰 state 檔。讀不到 ⇒ 回「沒有主線」,**不編一條出來**。
|
||||
唯一的例外是 `fetch_gitea()`(inkstone/ISEP#140,見下方第三個來源那段):
|
||||
它會打一次 Gitea,但**只有 SessionStart 的 `scripts/mainline refresh` 叫它**,
|
||||
`load()`/`line()` 永遠不叫——所以每一則訊息的路徑上仍然沒有網路。
|
||||
|
||||
── 三個來源,優先序寫死:家目錄 > repo 那份 > Gitea main 那份 ───────
|
||||
① 家目錄 `~/.claude/isep-countdown/mainline.json`(這台機器上 refresh 過、有進度)
|
||||
② repo 那份 `InkStoneCo/system-dev/mainline.json`(隨 repo 走,inkstone/ISEP#130)
|
||||
③ Gitea 上 `inkstone/InkStoneCo` main 的同一個檔(inkstone/ISEP#140)
|
||||
①② 之間誰算數看 `set_at`(見 `load()`);③ **只在 ①② 都不存在時**被問一次,
|
||||
抓到就寫進 ①,之後跟 ① 一樣對待。③ 讀的就是 ② 那個檔,只是從 Gitea 拿而不是從
|
||||
工作樹拿——它不是第二套主線來源。
|
||||
"""
|
||||
import json
|
||||
import os
|
||||
@@ -106,15 +117,100 @@ def repo_path(for_write=False):
|
||||
return None
|
||||
|
||||
|
||||
def _valid(d):
|
||||
"""一份主線檔長什麼樣才算數:dict、而且有 title。三個來源用同一把尺。"""
|
||||
return d if (isinstance(d, dict) and d.get("title")) else None
|
||||
|
||||
|
||||
def _read(p):
|
||||
try:
|
||||
with open(p) as f:
|
||||
d = json.load(f)
|
||||
except Exception:
|
||||
return None
|
||||
if not isinstance(d, dict) or not d.get("title"):
|
||||
return _valid(d)
|
||||
|
||||
|
||||
# ── Gitea main 上的那一份(inkstone/ISEP#140)────────────────────────
|
||||
#
|
||||
# 雲端薄殼 `youlinhsieh/inkstoneco` 的 SessionStart 跑在 `bootstrap.sh` 之前:那時
|
||||
# `InkStoneCo/` 還沒 clone,家目錄又是空的 ⇒ 上面兩份都不存在 ⇒ `countdown.py` 退到
|
||||
# 第二基準「期限最近的 milestone」⇒ 第一則 prompt 印出一條兩週前的舊線
|
||||
# (票上實測:`🔴 主線 把管理這條線做對 已逾期 341 小時`+`🎯 現在沒有主線`)。
|
||||
# 機制沒壞,是順序。而「哪一條是主線」這個指標本來就在 Gitea 上讀得到。
|
||||
#
|
||||
# 規矩(跟上面「repo 裡的那一份」那段接起來,不另立一套):
|
||||
# · 只在家目錄與 repo 那份**都不存在**時問 Gitea,問一次;任一份在場就不問。
|
||||
# · 只有 SessionStart 的 `scripts/mainline refresh` 會叫 `fetch_gitea()`;抓到就
|
||||
# `save()` 進家目錄(不 mirror:雲端沒有 InkStoneCo/ 可寫,本機也不弄髒工作樹)。
|
||||
# bootstrap 之後 repo 那份出現時,兩份 `set_at` 相同 ⇒ 家目錄那份算數(它有 refresh 過的進度)。
|
||||
# · 匿名優先。匿名被拒(401/403/404)且環境拿得到 Gitea token 才帶 token 再問一次
|
||||
# ——`inkstone/InkStoneCo` 在 Gitea 是 **private**(2026-09-07 實測:匿名打 raw/
|
||||
# contents/repo API 都 404),沒 token 就真的讀不到。
|
||||
# · 讀不到(斷網/5xx/不是 JSON/沒 title/逾時)⇒ 回 None,退回現行行為,不報錯。
|
||||
# 逾時有上限(`ISEP_MAINLINE_GITEA_TIMEOUT`,預設 4 秒,最多問兩次)。
|
||||
# · 測試用:`ISEP_MAINLINE_GITEA_URL=none` 把這個來源整個拿掉;`TICKET_HOST` 換主機
|
||||
# (既有測試都把它指到連不上的位址,所以它們照舊離線)。
|
||||
GITEA_HOST_DEFAULT = "https://git.uncle6.me"
|
||||
GITEA_REPO = "inkstone/InkStoneCo"
|
||||
GITEA_REF = "main"
|
||||
|
||||
|
||||
def gitea_url() -> str:
|
||||
forced = os.environ.get("ISEP_MAINLINE_GITEA_URL", "").strip()
|
||||
if forced == "none":
|
||||
return ""
|
||||
if forced:
|
||||
return forced
|
||||
host = (os.environ.get("TICKET_HOST", "").strip() or GITEA_HOST_DEFAULT).rstrip("/")
|
||||
return "%s/api/v1/repos/%s/raw/%s?ref=%s" % (
|
||||
host, GITEA_REPO, REPO_REL.replace(os.sep, "/"), GITEA_REF)
|
||||
|
||||
|
||||
def fetch_gitea(token="", timeout=None):
|
||||
"""去 Gitea 讀 `inkstone/InkStoneCo` main 上的 `system-dev/mainline.json`。
|
||||
|
||||
回一份跟 `_read()` 同樣通過 `_valid()` 的 dict,或 None。**永遠不 raise。**
|
||||
`token`:字串,或一個回字串的函式(只在匿名被拒時才會被叫)。
|
||||
🔴 這是本檔唯一會打網路的函式,只准 SessionStart 的 `scripts/mainline refresh` 叫;
|
||||
`load()`/`line()` 走的是每一則訊息的關鍵路徑,不准碰它。
|
||||
"""
|
||||
url = gitea_url()
|
||||
if not url:
|
||||
return None
|
||||
if timeout is None:
|
||||
try:
|
||||
timeout = float(os.environ.get("ISEP_MAINLINE_GITEA_TIMEOUT", "") or 4)
|
||||
except Exception:
|
||||
timeout = 4.0
|
||||
import urllib.error
|
||||
import urllib.request
|
||||
|
||||
def get(headers):
|
||||
req = urllib.request.Request(url, headers=headers)
|
||||
with urllib.request.urlopen(req, timeout=timeout) as r:
|
||||
return r.read().decode("utf-8", "replace")
|
||||
|
||||
try:
|
||||
text = get({}) # 匿名優先(D20 的形狀:讀不帶憑證)
|
||||
except urllib.error.HTTPError as e:
|
||||
if e.code not in (401, 403, 404):
|
||||
return None
|
||||
# `token` 可以是字串,也可以是「被拒了才去拿」的函式——拿 token 可能要掃 git remote、
|
||||
# 可能拿不到而想開口抱怨,匿名讀得到的時候根本不該走到那裡。
|
||||
tok = token() if callable(token) else token
|
||||
if not tok:
|
||||
return None
|
||||
try:
|
||||
text = get({"Authorization": "token " + tok}) # private repo:帶 token 再問一次
|
||||
except Exception:
|
||||
return None
|
||||
except Exception:
|
||||
return None
|
||||
try:
|
||||
return _valid(json.loads(text))
|
||||
except Exception:
|
||||
return None
|
||||
return d
|
||||
|
||||
|
||||
def load():
|
||||
|
||||
Reference in New Issue
Block a user