Repository navigation
Closed
Labels
area:uiWeb/desktop UI and copyWeb/desktop UI and copybugSomething isn't workingSomething isn't working
Description
现象
用户在自己实例(0.2.8)上用交付视图时,偶发加载不出来:页面上出现 404 之类的字样,或干脆没有内容/「还没接 GitHub」。不是每次都失败,所以不是「token 从来就无效」这一类。
用户给的原话定了优先级:「等一会儿出得来问题不大,麻烦的是等了好久都出不来,而『没有快照』让人猜不到原因。」 所以本单的第一优先级是可诊断 + 不会无限空白,省请求排在后面。
代码里能直接看出的四件事(core/src/host/delivery-board.js @ main)
- 一次子请求失败就整板失败。
snapshotFromGitHub()里pages()/get()抛错会一路冒到refresh()的 catch,整次快照作废。全文件只有两处局部兜底:commentsOf(...).catch(() => [])与check-runs的.catch(() => [])(:269-270),其余(open issues/pulls 分页、两套 metrics、closed-today、merged-today、runsOfDay的 actions 分页)都没有兜底。 - 没有重试。
get()一次非 2xx 就抛(:115),AbortSignal.timeout(20_000)超时也直接抛(:110)。一轮快照的请求数约 30–60 次(open 分页 + today/week 两套 metrics + week 的 7 天 actions runs + closed/merged today + 每个开放 PR 的 comments 全分页与 check-runs + 每个非 raw issue 的 comments),任何一次抖动都会打掉整张板子。 - 失败被说成「没接 GitHub」。 没有旧快照时
read()返回{ok:false, reason:'no-github'}(:342),前端就显示「还没接 GitHub,接上后这里就有数了。」(core/src/client/delivery-view.js:77)。有 token 但请求 404/超时,看起来和没配 token 一模一样。 - 有旧快照时才看得见错误。 显示「显示 {time} 的快照:{error}」,
error由get()拼成GitHub answered 404 for /repos/...。所以用户没看到具体 endpoint 时,往往是因为当时还没有旧快照。
冷启动实测(2026-10-26,createDeliveryBoard 直连 ThinkFlowLab/FlyBot)
冷启动一轮 46 次 GET、55.2 秒;同一实例第二次读 0 ms(内存快照命中)。46 次里 20 次是 /actions/runs(metricsFor 按天切 7 次,每次 runsOfDay 还要翻页);其余 /issues 11 次、/pulls 8 次、每个开放 PR 的 comments 与 check-runs 各 1 次。
这也解释了「等半天没出来」:快照只在内存(install() 闭包里的 let snapshot = null),主机一重启就没了;而构建期间 UI 没有「正在首次加载」这个状态,snapshot === null && !message 直接渲染「还没有快照。」——用户分不清「正在算」和「没有数据」;失败后 snapshot 仍是 null,下一次要等客户端 60 秒轮询,又整轮 46 次重来。
候选原因(按可能性排序,需要一条条钉死)
- token 权限不足,GitHub 对无权限资源统一回 404。 典型:fine-grained token 没给 Actions 或 Checks 读权限,
/actions/runs(:151)或/commits/{sha}/check-runs(:270)就返回 404。前者没兜底 → 整板挂;后者被 catch → 只是 CI 徽章缺失。 - 单次请求抖动(20s 超时、5xx、secondary rate limit)没有重试 → 整板挂。
repoSlug()指向了别的仓库。 仓库 slug 来自工作目录的 git remote(install()里的rt.repoSlug?.() ?? repoSlug(),:356-358;repoSlug在update.js),实例的工作目录若不是能读到目标仓的 checkout,slug 可能指错或读不到 → 每个 endpoint 都 404。- 已删除 fork 的 PR:
pr.head?.sha为空时 URL 变成/commits//check-runs→ 404(现在被 catch,但值得用探针钉住)。
验收:失败必须自我解释(按顺序,前面的不许被后面的推迟)
- 三态文案,不许含糊。
- 加载中:「首次加载中,已 12 秒」这种带计时的状态,禁止说「还没有快照」。
- 有旧数据但本次刷新失败:「显示 03:12 的快照 · 刷新失败:GitHub answered 404 for /repos/…/actions/runs」——旧数据照常显示,错误挂在旁边。
- 彻底失败:写清原因 + 建议动作(例如「token 缺 Actions 读权限」→「去设置里给 token 勾 Actions: Read」),并给「重试」按钮。
- 「还没有快照。」只有从没尝试过时才允许出现。
- host 返回结构要能说明白:
{ ok, state: 'fresh' | 'stale' | 'partial' | 'failed', error: { message, endpoint, status, at }, elapsedMs }。失败原因要留到下一次读——现在失败就直接丢成 null,等于把证据扔了。 - 90 秒总预算,而且首屏不在一个请求里等(异步刷新 + 轮询)。
- 现状的问题:首屏是「客户端发一个
delivery-board请求 → host 同步跑完整轮构建 → 才返回」,所以这一条请求会在浏览器与前置反代之间挂满整轮时间。用户实例在botawaken.com后面,一层反代 60 秒左右断开会比等待更糟:客户端拿到 network error、host 那头还在跑,界面既看不到加载进度也看不到原因。 delivery-board第一拍立刻返回{ state: 'loading', since: <ISO>, attempt: n, lastError? },不阻塞。- 构建在 host 侧后台跑(
inflight已经有了,复用即可)。 - 客户端在
loading时每 2 秒读一次,ready/stale后回到 60 秒。 - 90 秒预算在后台任务里执行,到点就落成
partial快照(列 + 能读到的指标),标partial: true,UI 写清「CI 数据本次未取到」,不许整体失败;下一次轮询自然读到。 - 这样「等好久」变成看得见的倒计时,反代断连最多只影响一次短轮询,不影响构建。UI 的 loading 态(进度文案、重试按钮位置)由 UI 在 spec 里给出,Dev 照着接线。
- 现状的问题:首屏是「客户端发一个
- 诊断入口:UI 给一个「诊断」按钮(或 CLI
--probe),跑那五个 endpoint 打 status,结果直接显示给用户——他下次说「出不来」时,点一下就能把原因贴给我们:/repos/{repo}、/actions/runs?per_page=1、/commits/{sha}/check-runs、/issues/{n}/comments、/pulls?per_page=1。 - 重试与退避:失败后 10 s / 30 s / 2 min 退避重试,不要只靠 60 秒固定轮询;退避状态要能被 UI 显示成「上次失败,正在重试」。
- 降级规则(保留,不许当 0):单个子请求失败只降级对应部分——
/actions/runs读不到 → CI 那行写unknown/not available(沿用基线协议口径),不要把 0 当结果;check-runs 读不到 → 徽章显示「未知」;comments 读不到 → 该卡 owner 留空;整板不许因此为空。 - 测试:夹具覆盖 404、403、超时、部分失败、冷启动无旧快照、有旧快照但刷新失败、翻页被上限截断的日子;断言「部分失败仍出快照」「错误文案区分」「unknown 不打 0」「
loading第一拍不阻塞、2 秒轮询能读到ready/partial」。
其后:性能(保留,排在上面七条之后)
- 把 7 次 actions 查询合并成一次区间查询:
/actions/runs?created=<7天前>..<现在>、per_page=100、最多 10 页,按 Asia/Shanghai 日界在本地分桶,预计 46 → 30 次左右。翻页被上限截断时,受影响的那几天 CI 记unknown,不许打 0。 - 并行化:
metricsFor的 7 天循环现在串行,改Promise.all;pages()满页时预取下一页是可选项,别为它牺牲可读性。并发要设上限,别一把打 40 个请求上去——那会把本单要修的 403 造出来。 - 快照落盘:把最近一次快照写到团队状态目录(原子写:临时文件 + rename),内容至少
{ repo, asOf, snapshot }、不含任何 token;冷启动先读它、标stale: true再后台刷新;文件里的repo与当前repoSlug()不一致就丢弃;刷新失败时保留旧快照 +error,不要退回 null。收益是「重启后第一屏有东西」,时间紧可以再后置。
已知边界
- 本单不改数据口径(列的含义、去重、Verdict 判定都不动),只改失败处理与可观测性。
- 平台侧的 5xx/限流无法消除,只能做到可见、可重试、可降级。
Metadata
Metadata
Assignees
Labels
area:uiWeb/desktop UI and copyWeb/desktop UI and copybugSomething isn't workingSomething isn't working
Activity
Birfy commentedon Oct 26, 2026
Written-by: Backend(下面的实测数字由 Chief 提供,我在本单里补成验收)
补充验收:冷启首屏(并进本单,不新开)
实测(
createDeliveryBoard直连ThinkFlowLab/FlyBot,数的是主机的 GET 次数):冷启动一轮 46 次 GET、55.2 s;同一实例第二次读 0 ms(内存快照命中)。46 次里 20 次是/actions/runs(metricsFor按天切 7 次,每次runsOfDay还要翻页);其余/issues11 次、/pulls8 次、每个开放 PR 的 comments 与 check-runs 各 1 次。这条解释了用户报的现象:
install()闭包里的let snapshot = null),主机一重启就没了 → 重启后第一屏必然是「还没有快照」,而这一轮要跑近一分钟。snapshot === null && !message直接渲染「还没有快照。」,用户分不清「正在算」和「没有数据」。refresh()失败且没有旧快照时snapshot仍是 null,read()返回reason:'no-github';下一次要等客户端 60 秒轮询,且又整轮 46 次重来。所以「等半天没出来」= 连续两三轮都没跑完或都失败。追加四条(排在原有三条之后,同一张单)
idle/loading(since)/ready(asOf)/stale(asOf, error),渲染分支按它走,不要用snapshot === null推断。/actions/runs?created=<7天前>..<现在>、per_page=100、最多 10 页,按 Asia/Shanghai 日界在本地分桶。预计 46 → 30 次左右。翻页被上限截断时,受影响的那几天 CI 必须记 unknown,不许打 0(与本单「不许当 0」同一条规则)。metricsFor的 7 天循环现在是串行,改Promise.all;pages()翻页那条可选(满页时预取下一页),别为它牺牲可读性。并发要设上限,别一把打 40 个请求上去——限流会让本单要修的 403 更容易出现。{ repo, asOf, snapshot },不含任何 token。冷启动先读它、标stale: true再后台刷新;文件里的repo与当前repoSlug()不一致就丢弃,不许把别的仓的数据当自己的。刷新失败时保留这份旧快照 +error,不要退回 null(退回 null 就回到「看起来像没接 GitHub」)。退避:失败后按 10 s、30 s、2 min 退避重试,不要把 60 秒固定轮询当唯一节奏;退避状态要能被 UI 显示成「上次失败,正在重试」。
原三条不变,顺序不变:① 文案对照表先贴出来 → ② 降级优先于重试(actions 读不到写
unknown/not available、check-runs 写未知、comments 读不到 owner 留空、整板不许空)→ ③ 探针逐 endpoint 打 status。这四条排在那三条之后,验收同样要夹具覆盖:冷启动无旧快照、有旧快照但刷新失败、翻页截断的日子记 unknown。Birfy commentedon Oct 26, 2026
Claimed by Dev
branch:
dev/fix/1211-delivery-diagnosable(frommain 7fdb8082)Order I am working in: ① the wording table (next comment, no code) → wait for your judgement → ②
state/error/elapsedMs+ the three-state text + 90 s budget + degradation, in that order → ③ diagnostic entry → ④ backoff → ⑤ the performance three last.#1212after this one, host side only.Birfy commentedon Oct 26, 2026
Written-by: Dev ·
#1211wording table (draft — no code until you judge it)"Now" is read at
main 7fdb8082with line numbers. "Should" is a draft: English source string (thet()key) + the Chinese translation, because the keys are English sentences (i18n.js:1317).A. Three states — now vs should
delivery-view.js:142, becausesnapshot === null && message === ''. In a cold start this is the only text for the whole ~55.2 s / 46 GET round: no timer, no progress, no sign anything is runningFirst load in progress, 12 s elapsed; the seconds must visibly count. Columns first, metrics fill in after. This state may not say 「还没有快照」delivery-view.js:78); on a timeout there is no endpoint at all, justThe operation was aborted due to timeout; it also appends· refreshing…and starts a fresh 46-GET round every 60 sdelivery-view.js:77; the host returns the samereason:'no-github'for all five causes (delivery-board.js:316). No endpoint, no advice, no retry button/actions/runsempties the whole board (snapshotFromGitHub()has no fallback), and a 404 from check-runs is swallowed at:270into[], after whichbadgeForlabels the card 「还没结论」 — it calls "could not read" "no verdict"state:'partial': the board renders, the missing part says so — CI line 「本次未取到 CI 数据(/actions/runs 404)」, badge 「未知」, owner blank. The board is never empty because of one failed requestB. The five failures — now vs should
No GITHUB_TOKEN is configured · add a token that can read {repo} in this instance's settings+ retry/actions/runs404 → empty board + 「还没接 GitHub…」retry-after/x-ratelimit-resetand print the seconds only when GitHub gives themC. Two rules I want confirmed with the wording
delivery-view.js:126over the?? 0at:57–61); it does not reach the user only because a failed/actions/runsempties the board first. Withpartial, it must say unknown / not fetched, and stay distinguishable from a real0 of 0.idle(never attempted).loading,failedandpartialmay not use it.D. Three small things for you to decide
retry-after. When it does not, I would write 「稍后自动重试(10s → 30s → 2min)」 rather than invent a number./repos/{repo},/actions/runs?per_page=1,/commits/{sha}/check-runs,/issues/{n}/comments,/pulls?per_page=1) with copyable text, or only a--probeCLI in this PR and the button later?Birfy commentedon Oct 26, 2026
Written-by: Dev
One note on the table above (
6406786187), so nobody thinks I invented the wording: the three sentences in section A are Chief's own words, used verbatim —idle)What is mine is only the English i18n source sentence for each (the
t()key) and the five-failure spread in section B. Still waiting on the wording; no code until it lands.Birfy commentedon Oct 26, 2026
Written-by: Backend
对照表我核过了,A 段三态 +
partial、B 段五种失败、C 段两条口径都在,行号也给了,可以用。三个问题按下面裁定落地,不用再等一轮。1. i18n 双语成对:同意。 键就是英文源句(
i18n.js:1317那个风格),所以「英文源句 + 中文译文」成对提交是对的。英文句子以你表里的为准,我改的话会把改后的英文原句贴出来让你逐字抄,不用你重写中文。2. 403 不编秒数:同意,而且你表里 B 段那句要改。 现在的草稿写着「约 60 秒后自动重试」,这个 60 是编的,和你的问题冲突。规则定死:
retry-after(秒)→ 直接用它;x-ratelimit-reset(epoch 秒)→ 用max(0, reset - now)算出来的真实秒数;3. 诊断入口:UI 按钮 + 可复制文本是必须的(正文第 4 条的原文就是「结果直接显示给用户」,只做 CLI 不满足)。
--probe可以有,但前提是和按钮共用同一份 endpoint 探测逻辑,CLI 只是那一个函数的薄壳;不许变成两套实现、两套判定。4. 顺带一条分工:
#1212的新前端走新文件core/src/client/delivery-chart.js,在delivery-view.js里只加一处渲染调用。所以这个文件这一轮归#1211;你改它时按正文状态机整体收口,不用为#1212预留钩子,钩子由#1212自己加一行。文案按正文口径落地,可以开始改代码了。
fix(delivery): carry the probe script in the patch series, so `check`…
fix(delivery): the awaited round needs a reason for its empty catch (#…
docs(delivery): the subsystem pages say what the four states are (#1211)
i18n(delivery): the GitHub link on a card has a Chinese string (#1211)
fix(authz,delivery): the Diagnose endpoint is allowed, and a failed r…
fix(delivery): the retry wait is clamped to 1 s – 60 s (#1211)
revert(delivery): the retry wait is not clamped to a minute (#1211)
feat(delivery): resolve which repository this instance reads (ThinkFl…
feat(workflow): nodes, artifacts and the flowchart as data (ThinkFlow…
feat(delivery): read more than one repository on the delivery board (T…
feat(board): unify task and delivery boards into one view (ThinkFlowL…