From c6bd0d7899537fcb667004eb21eae585247a1814 Mon Sep 17 00:00:00 2001 From: kbdb-cc Date: Mon, 6 Jul 2026 10:51:42 +0000 Subject: [PATCH] =?UTF-8?q?fix(drainer):=20=E5=90=8C=E5=B8=B3=E8=99=9F=20w?= =?UTF-8?q?orker-to-worker=20=E6=94=B9=E8=B5=B0=20Service=20Bindings?= =?UTF-8?q?=EF=BC=88=E4=BF=AE=20CF=201042=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Production bug(本地 miniflare 未抓到、真端點才爆):部署後 drainer 對同帳號 arcrun-code/arcrun-kbdb/kbdb-graph-plugin 做 fetch("https://*.leo21c.workers.dev") → CF 1042(同帳號 worker 互打限制),被 error handler 包成 "not valid JSON"。 修法=比照 cypher-executor(wrangler.toml [[services]] 綁零件): - wrangler.toml 加 [[services]] SVC_CODE→arcrun-code / SVC_KBDB→arcrun-kbdb / SVC_GRAPH→kbdb-graph-plugin;移除 CODE_WORKER_URL/KBDB_URL/GRAPH_URL vars。 - index.ts:所有 code/kbdb/graph 呼叫改 env.SVC_X.fetch()(svc() 輔助);Gitea (git.uncle6.me,非 CF)維持 plain fetch。 驗收(強制對部署後真端點、禁 miniflare):POST /drain?batch=2 回真 JSON、notes 3 卡 冪等全 skip、無 1042;游標前進+回捲;加測試卡→真端點 /webhook 只寫該 delta(entry create 進 live KBDB)→三模式 curl 命中(含 Arcrun#11 修後語意 owner+type 過濾生效)→ 測試卡清除。deploy version 95562b3e。 Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01HJiLCRUU2o3aSpPEzVCt2o --- .../examples/km-wiki-ingest/drainer/README.md | 9 ++++-- .../km-wiki-ingest/drainer/src/index.ts | 32 +++++++++++-------- .../km-wiki-ingest/drainer/wrangler.toml | 19 +++++++---- 3 files changed, 38 insertions(+), 22 deletions(-) diff --git a/registry/examples/km-wiki-ingest/drainer/README.md b/registry/examples/km-wiki-ingest/drainer/README.md index d7d6a33..f5273cc 100644 --- a/registry/examples/km-wiki-ingest/drainer/README.md +++ b/registry/examples/km-wiki-ingest/drainer/README.md @@ -57,10 +57,13 @@ printf '%s' "$GITEA_TOKEN" | npx wrangler secret put GITEA_TOKEN - Secret:若設,同步 `GITEA_WEBHOOK_SECRET` secret(本 Worker 驗 `X-Gitea-Signature` HMAC-SHA256) - Gitea 的 ping event → Worker 回 `{ok:true,pong:true}`(測連通) -## config(wrangler.toml [vars]) +## config -`REPO=Leo/notes REF=main OWNER=leo CARDS_ROOT=system-dev/wiki/cards BATCH_SIZE=2` -`CODE_WORKER_URL / KBDB_URL / GRAPH_URL / GRAPH_KEY`(皆 leo21c live 服務)。 +- **vars**:`REPO=Leo/notes REF=main OWNER=leo CARDS_ROOT=system-dev/wiki/cards GRAPH_KEY=leo BATCH_SIZE=2` +- **Service Bindings(`[[services]]`)**:`SVC_CODE→arcrun-code`、`SVC_KBDB→arcrun-kbdb`、`SVC_GRAPH→kbdb-graph-plugin`。 + ⚠️ 同帳號 worker-to-worker **必須走 Service Binding**(`env.SVC_X.fetch()`)——直接 `fetch("https://arcrun-*.leo21c.workers.dev")` + 會撞 **CF 1042**(同帳號 worker 互打限制)。Gitea(`git.uncle6.me`,非 CF)才用 plain `fetch`。 + 註:`wrangler dev` 本地 miniflare **不強制** 1042,故本地測會假綠 → 一律對部署後真端點驗收。 ## 規模備註(5,083 檔) diff --git a/registry/examples/km-wiki-ingest/drainer/src/index.ts b/registry/examples/km-wiki-ingest/drainer/src/index.ts index 1c0ba65..ef9d548 100644 --- a/registry/examples/km-wiki-ingest/drainer/src/index.ts +++ b/registry/examples/km-wiki-ingest/drainer/src/index.ts @@ -28,15 +28,21 @@ export interface Env { REF: string; // main OWNER: string; // leo(KBDB 租戶 owner_id) CARDS_ROOT: string; // system-dev/wiki/cards - CODE_WORKER_URL: string; // https://arcrun-code.leo21c.workers.dev/ - KBDB_URL: string; // https://arcrun-kbdb.leo21c.workers.dev - GRAPH_URL: string; // https://kbdb-graph-plugin.leo21c.workers.dev + // 同帳號 worker-to-worker 必須走 Service Binding(直接 fetch(*.leo21c.workers.dev) 會撞 CF 1042)。 + SVC_CODE: Fetcher; // → arcrun-code + SVC_KBDB: Fetcher; // → arcrun-kbdb + SVC_GRAPH: Fetcher; // → kbdb-graph-plugin GRAPH_KEY: string; // leo BATCH_SIZE: string; // "2" GITEA_TOKEN: string; // secret:讀 repo GITEA_WEBHOOK_SECRET?: string; // secret(選填):驗 webhook 簽章 } +// Service Binding fetch 輔助:host 是 placeholder(binding 直接路由到目標 worker),path/query/method/body 才重要。 +function svc(binding: Fetcher, path: string, init?: RequestInit): Promise { + return binding.fetch(new Request(`https://svc${path}`, init)); +} + const CURSOR_TYPE = 'ingest_cursor'; const cursorPageName = (env: Env) => `cursor:km_wiki_ingest_drain:${env.REPO}`; @@ -73,7 +79,7 @@ async function giteaRaw(env: Env, relPath: string): Promise { // ── card-processing 核心 ────────────────────────────────────────────────────── async function parseCard(env: Env, md: string, relPath: string): Promise { - const r = await fetch(env.CODE_WORKER_URL, { + const r = await svc(env.SVC_CODE, '/', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ @@ -99,20 +105,20 @@ async function upsertEntry(env: Env, entry: any): Promise<{ action: string; id?: const pageName = entry.page_name; const metadata_json = JSON.stringify(entry.metadata); const newHash = entry.metadata.content_hash; - const lookup = await fetch( - `${env.KBDB_URL}/entries?page_name=${encodeURIComponent(pageName)}&owner_id=${encodeURIComponent(env.OWNER)}`, + const lookup = await svc(env.SVC_KBDB, + `/entries?page_name=${encodeURIComponent(pageName)}&owner_id=${encodeURIComponent(env.OWNER)}`, ); const existing = ((await lookup.json()) as { entries?: any[] }).entries?.[0]; if (existing) { if (metaHash(existing) === newHash) return { action: 'skip', id: existing.id }; - const r = await fetch(`${env.KBDB_URL}/entries/${existing.id}`, { + const r = await svc(env.SVC_KBDB, `/entries/${existing.id}`, { method: 'PATCH', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ content: entry.content, tags_json: JSON.stringify(entry.tags || []), metadata_json }), }); return { action: 'patch', id: existing.id, ...(r.ok ? {} : { action: 'patch-failed' }) }; } - const r = await fetch(`${env.KBDB_URL}/entries`, { + const r = await svc(env.SVC_KBDB, `/entries`, { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ @@ -134,7 +140,7 @@ async function postEnvelopes(env: Env, envelopes: any[]): Promise { for (const envp of envelopes) { // envelopes 已由 code 節點剝除 _-鍵;此處再保險剝一次。 const clean = Object.fromEntries(Object.entries(envp).filter(([k]) => !k.startsWith('_'))); - const r = await fetch(`${env.GRAPH_URL}/triplets/ingest?owner_id=${encodeURIComponent(env.OWNER)}`, { + const r = await svc(env.SVC_GRAPH, `/triplets/ingest?owner_id=${encodeURIComponent(env.OWNER)}`, { method: 'POST', headers: { 'content-type': 'application/json', 'X-Arcrun-API-Key': env.GRAPH_KEY }, body: JSON.stringify(clean), @@ -168,8 +174,8 @@ async function processCard(env: Env, relPath: string): Promise { interface CursorState { last_path: string; cycle: number; processed_total: number; updated_at: number; } async function getCursor(env: Env): Promise<{ id?: string; state: CursorState }> { - const r = await fetch( - `${env.KBDB_URL}/entries?page_name=${encodeURIComponent(cursorPageName(env))}&owner_id=${encodeURIComponent(env.OWNER)}`, + const r = await svc(env.SVC_KBDB, + `/entries?page_name=${encodeURIComponent(cursorPageName(env))}&owner_id=${encodeURIComponent(env.OWNER)}`, ); const e = ((await r.json()) as { entries?: any[] }).entries?.[0]; if (e) { @@ -182,11 +188,11 @@ async function getCursor(env: Env): Promise<{ id?: string; state: CursorState }> async function setCursor(env: Env, cur: { id?: string; state: CursorState }): Promise { const content = JSON.stringify(cur.state); if (cur.id) { - await fetch(`${env.KBDB_URL}/entries/${cur.id}`, { + await svc(env.SVC_KBDB, `/entries/${cur.id}`, { method: 'PATCH', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ content }), }); } else { - await fetch(`${env.KBDB_URL}/entries`, { + await svc(env.SVC_KBDB, `/entries`, { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ entry_type: CURSOR_TYPE, page_name: cursorPageName(env), owner_id: env.OWNER, content, metadata_json: JSON.stringify({ kind: 'ingest_cursor', embed: false }) }), diff --git a/registry/examples/km-wiki-ingest/drainer/wrangler.toml b/registry/examples/km-wiki-ingest/drainer/wrangler.toml index 658b1e3..631eb2d 100644 --- a/registry/examples/km-wiki-ingest/drainer/wrangler.toml +++ b/registry/examples/km-wiki-ingest/drainer/wrangler.toml @@ -3,20 +3,27 @@ main = "src/index.ts" compatibility_date = "2025-02-19" workers_dev = true -# ⚠️ 維持「非-auto」:預設不註冊 cron trigger(不自走排程、不無人值守亂寫)。 -# drain 以 POST /drain 手動觸發(與 scheduled handler 同核心)。 -# 要開自走 Phase 0 backfill:leo 過閘後,取消下面兩行註解再 deploy(scheduled handler 已在 code)。 +# ⚠️ 維持「非-auto」:預設不註冊 cron trigger(不自走排程)。drain 走 POST /drain(與 scheduled 同核心)。 +# 要開自走 Phase 0 backfill:leo 過閘後取消下面兩行註解再 deploy(scheduled handler 已在 code)。 # [triggers] # crons = ["*/5 * * * *"] +# ── Service Bindings(同帳號 worker-to-worker 正解;直接 fetch(*.leo21c.workers.dev) 會撞 CF 1042)── +[[services]] +binding = "SVC_CODE" +service = "arcrun-code" +[[services]] +binding = "SVC_KBDB" +service = "arcrun-kbdb" +[[services]] +binding = "SVC_GRAPH" +service = "kbdb-graph-plugin" + [vars] REPO = "Leo/notes" REF = "main" OWNER = "leo" CARDS_ROOT = "system-dev/wiki/cards" -CODE_WORKER_URL = "https://arcrun-code.leo21c.workers.dev/" -KBDB_URL = "https://arcrun-kbdb.leo21c.workers.dev" -GRAPH_URL = "https://kbdb-graph-plugin.leo21c.workers.dev" GRAPH_KEY = "leo" BATCH_SIZE = "2" # secret(wrangler secret put):GITEA_TOKEN(必),GITEA_WEBHOOK_SECRET(選)