系統與資料流
每天一次,排程 Worker 收集社群動態、整理成 thread,並把不可變的快照發布到 R2;靜態網站再透過小型唯讀 API 讀取這份快照。點選任一步驟,可以看到它做什麼、上次執行的數字,以及實作它的檔案。
流程圖
1 · 收集
2 · 處理
3 · 儲存
4 · 應用
每日發布 cron
每個環境各有一個 Worker 與自己的 cron,Dev 與 Prod 不共用狀態。發布原本每小時一次,2026-10-13 起改為每天一次以降低 R2 成本(Spec 018)。
- Dev
7 22 * * *(台北時間 06:07)- Prod
37 22 * * *- 取捨
- 資料最多約 24 小時舊;要更早發布只能用需要 token 的手動觸發。
來源: apps/data-publisher-worker/wrangler.development.jsonc, apps/data-publisher-worker/wrangler.production.jsonc, docs/specs/018-daily-publication/spec.md
週報 cron 僅 Dev
Dev 發布後 40 分鐘,週報讀取剛發布的快照。若發布還在跑,就延後 15 分鐘,最多 4 次。
來源: docs/specs/014-topic-digest/spec.md, docs/runbooks/cloudflare-data-publisher.md
追趕與重試
某個來源達到單次上限的 run,5 分鐘後再跑一次(最多 5 次);失敗的 run 在 10、30、60 分鐘後重試。一次觸發最多造成 9 次發布。
來源: apps/data-publisher-worker/src/run-schedule.ts, docs/specs/018-daily-publication/spec.md
GitHub PR 與 issue
用 GitHub REST API 依更新時間游標讀取,每個 repo 每次最多 200 筆,其餘由後續的追趕 run 補上。
來源: docs/feature-map.md, docs/specs/004-replayable-github-feed/spec.md
Jira(KAFKA 專案)
匿名讀取 Apache Jira 搜尋 API,時間視窗依上次成功至今的間隔放大。
來源: packages/reference-pipeline/src/kafka-connectors.ts, docs/specs/012-kafka-mailing-list-jira/spec.md
Kafka dev@ 郵件列表
KIP 的討論與投票都在這裡。主旨以 [jira] 或 [PR] 開頭的信會被略過,因為這些已由 Jira 與 GitHub 收集。
- 共同規則
- 5xx 或 429 重試一次;回應超過 4 MiB 時該來源以
too-large失敗;一個來源失敗不會擋住其他來源。
來源: packages/reference-pipeline/src/kafka-connectors.ts, docs/specs/012-kafka-mailing-list-jira/spec.md
GitHub PR review 僅 Dev
誰在何時 review 了什麼,不含留言內容;用於貢獻者頁面。
PipelineState
保存每個來源的游標、目前階段與上一次 run;程式中途掛掉時,會從最後提交的游標續跑,而不是從頭開始。
來源: apps/data-publisher-worker/src/index.ts, docs/architecture/decisions/0014-publish-per-source-with-independent-cursors.md
讀取來源
依游標逐一讀取每個來源。
整理成 thread
確定性的 TypeScript materializer 把紀錄整理成 thread,並從標題與內文抽出 KIP-1150 這類編號。相同輸入一定得到相同輸出。
來源: packages/reference-pipeline/src/materializer.ts, docs/architecture/decisions/0006-use-typescript-reference-materializer-before-flink.md
寫出 Feed release
不可變的快照:一份列表加上每個 thread 的詳細物件。詳細物件以內容的 SHA-256 為鍵只存一次,之後的 release 直接重用(內容定址)。
- 最近一次
- Dev 新增 791、重用 7,796 個物件;Prod 新增 378、重用 7,596 個
來源: packages/serving-contract/src/r2.ts, docs/specs/008-content-addressed-details/spec.md
建立搜尋索引
把每個 thread 切成段落,一次算好語料統計,寫出 BM25 分片與記錄每個詞在哪些分片的 terms.json。詳見搜尋。
- 最近一次
- Dev 9,764 段、11 個分片;Prod 7,118 段、8 個分片
來源: packages/serving-contract/src/search-r2.ts, docs/specs/013-search-streaming/spec.md
切換指標、記錄證據
先寫物件,最後才更新小小的 current.json 指標,所以讀者看到的一定是完整的舊版或新版,不會看到一半。接著記錄這次寫了什麼、花了多少。
- 每次 R2 請求
- Dev Class A 794 / Class B 8,590;Prod 381 / 7,977
- 不變式
- Class A ≤ 新物件數 + 3
來源: apps/data-publisher-worker/src/pipeline.ts, docs/specs/006-environment-isolated-r2-publication/spec.md
追加活動紀錄 僅 Dev
為貢獻者頁面寫入每人每月的活動檔(PEOPLE_ACTIVITY=log@1)。
產生段落向量 僅 Dev
把新的搜尋段落轉成向量供語意搜尋使用,每天上限 2,500 估計 neurons、400 次呼叫。
每週主題週報 僅 Dev
分類、摘要、驗證並翻譯過去 7 天的動態。詳見週報。
指標
唯一會變動的物件;它指向的內容都不可變,所以回復只要改指標。
Feed release 與詳細物件
每次 run 一個 release;詳細物件依內容雜湊在各 release 間共用。
來源: docs/architecture/decisions/0013-share-detail-objects-by-content-digest.md
搜尋 release
與 feed 分開版本化,排序演算法可以獨立更換。
來源: docs/architecture/decisions/0007-publish-independent-versioned-search-releases.md
週報物件 僅 Dev
某一份來源 release 的英文週報與繁體中文翻譯。
內部 R2 物件
每次發布的證據,以及 Dev 上的 review 與活動月檔;不直接對外提供。
Vectorize 向量索引 僅 Dev
Cloudflare 的向量資料庫,放在可替換的介面後面,不成為對外契約。
搜尋查詢閘門 僅 Dev
每個語意查詢都要經過的 Durable Object 帳本,讓公開搜尋框不會超支;超過預算就只回傳關鍵字結果。
Feed 與詳細內容
先讀指標,再讀 release,再讀一個詳細物件。每個 200 回應快取 30 秒、stale-while-revalidate 120 秒。瀏覽器從不直接碰 R2。
來源: apps/web/functions/api, docs/architecture/decisions/0004-use-pages-functions-and-r2-serving-projections.md
搜尋
讀 current.json、manifest、terms.json,再只讀查詢詞出現的分片。
週報與人員 Prod 404
因為 Prod 未開啟這些功能,所以在 Prod 回 404。
This week Dev 有內容
週報:標題、重點、KIP 提案、主題卡片、收合的例行維護。
所有 thread 與搜尋
thread 列表與同一畫面的搜尋,以及每個 thread 的詳細頁(完整時間軸與來源連結)。
資料新鮮度
每個畫面都顯示資料多舊,發布器卡住時不必看 log 也看得出來。
來源: apps/web/src/freshness.ts, docs/specs/021-publication-overdue/spec.md
/health
公開、唯讀的上次執行狀態:各來源是否成功、寫了多少物件、R2 請求數、搜尋索引大小、今天的 AI 用量。不會回傳任何憑證。
verify:health
版本庫內的指令,只用 GET 比對 /health 與網站實際提供的資料,可對 Dev 或 Prod 執行。
執行數字取自 2026-10-20 讀取的 Dev 與 Prod 發布器公開 /health(最近一次執行:2026-10-19 22:07 與 22:37 UTC)。
四個階段
- 收集。 各環境的 cron 觸發發布器。它從 Durable Object 裡保存的各來源游標讀取 GitHub、Jira 與 Kafka dev@ 郵件列表;一個來源失敗不會擋住其他來源,程式中途掛掉也會從停下的地方續跑(ADR-0014)。
- 處理。 apps/data-publisher-worker/src/pipeline.ts 的
runDataPublication用確定性的規則整理 thread(發布流程中不使用語言模型),寫出 Feed release,並建立搜尋索引。 - 儲存。 R2 存放不可變的 release 與內容定址的詳細物件;只有小小的
current.json指標會變,而且最後才寫。所以讀者看到的一定是一份完整的 release,回復也只要改指標(Spec 006、Spec 008)。 - 應用。 Pages Functions 讀 R2、回傳快取 30 秒的 JSON,由 Vue 網站呈現。部署程式碼不會發布資料:資料只來自 Worker(docs/operations/release.md)。
Dev 與 Prod
兩個環境完全隔離:Worker、Durable Object、R2 bucket 與 Pages 專案各自獨立,也不共用任何寫入憑證(ADR-0012)。
| 項目 | Dev | Prod |
|---|---|---|
| 網站 | oss-knowledge-base-dev.pages.dev | oss-knowledge-base.pages.dev |
| 部署方式 | 每次 push 到 main、所有 CI 工作通過後部署,接著對已部署的網站跑端對端測試 | 從 main 可達的 v* tag(目前 v0.3.1,2026-10-19) |
| 發布 cron | 22:07 UTC | 22:37 UTC |
關鍵字搜尋 bm25-reference@3 | 開 | 開 |
| 每週週報 | 開(22:47 UTC) | 關 |
| 語意混合搜尋 | 開(SEARCH_SEMANTIC=rrf-group@2) | 關 |
| 貢獻者頁面、review 收集 | 開 | 關 |
每個只在 Dev 的功能都是一個在 Prod 未設定的開關,列在 docs/feature-map.md。新行為合併時開關是關的,再用另一個小變更逐一環境開啟;要回復時改設定即可,不必 revert(docs/process/workflow.md)。
為什麼這樣設計
- 快照,而不是即時資料庫。 每天一份不可變的 release,從物件儲存提供既便宜又容易重現,也讓每句摘要都能引用固定的紀錄。目標的串流架構(Fluss、Flink、Iceberg)已在 ADR-0001 決定但尚未實作,目前只有相容性驗證(Spec 007)。
- 先規則,後模型。 thread 整理與編號抽取都是確定性的 TypeScript(ADR-0006);語言模型只在發布之後用於週報與語意搜尋,而且各有每日預算。
- 資料過期要看得見。 Dev 曾經約 49 小時沒有發布卻沒人發現(Spec 011)。現在每個畫面都顯示資料多舊:超過 24 小時 30 分標為逾時,超過 27 小時標為過期(apps/web/src/freshness.ts)。