diff --git a/src/lib/kbdb-client.ts b/src/lib/kbdb-client.ts index b61e682..391d790 100644 --- a/src/lib/kbdb-client.ts +++ b/src/lib/kbdb-client.ts @@ -36,6 +36,12 @@ export class KbdbClient { constructor( private readonly baseUrl: string, private readonly token?: string, + // 2026-07-03 補跑實測發現的坑:Cloudflare 會擋 Worker → 另一個 *.workers.dev + // Worker 的直連 fetch(error code 1042,loop-prevention on shared workers.dev zone)。 + // self-hosted 帳號通常沒有自訂域名可用,正規解法 = Service Binding + // (wrangler.toml `[[services]]`),由 CF 內部直接路由、不經公開網路。 + // 有綁定時優先走它;沒有(例如本地 dev/mock)則 fallback 回全域 fetch。 + private readonly fetcher?: { fetch: typeof fetch }, ) { if (!baseUrl) { throw new Error('KBDB_BASE_URL 未設定:插件需指向基本盤 API(不可直連 D1)'); @@ -46,7 +52,8 @@ export class KbdbClient { const headers: Record = { 'Content-Type': 'application/json' }; if (this.token) headers['Authorization'] = `Bearer ${this.token}`; - const res = await fetch(this.baseUrl.replace(/\/$/, '') + path, { + const doFetch = this.fetcher ? this.fetcher.fetch.bind(this.fetcher) : fetch; + const res = await doFetch(this.baseUrl.replace(/\/$/, '') + path, { method, headers, body: body === undefined ? undefined : JSON.stringify(body), @@ -113,23 +120,34 @@ export class KbdbClient { // --- templates(= 替代建表;插件要新類型只能建 template) --- async ensureTemplate(name: string, slots: string[], description?: string): Promise { - const existing = await this.req<{ id?: string; slots?: string[] } | { error: string }>( + // 2026-07-03 補跑實測發現的坑:base 的 GET /templates/:name 回傳是 + // { success, template: { id, slots_json: "[...]" } }(包一層 + slots 是 JSON 字串), + // 不是原本假設的 { id, slots } 平鋪陣列。誤判「不存在」會導致對已存在的 name 重複 + // POST /templates,而 base 對重名衝突沒有回優雅的 409,是直接 500(見 kbdb-graph 實測)。 + const existing = await this.req<{ template?: { id: string; slots_json?: string } }>( 'GET', `/templates/${encodeURIComponent(name)}`, ).catch(() => null); + const tpl = existing?.template; // 全新 template → 建。 - if (!existing || !(existing as any).id) { + if (!tpl || !tpl.id) { await this.req('POST', '/templates', { name, slots, description, created_by: 'kbdb-graph' }); return; } // 既有 template → 補缺 slot(不 early-return;否則 seed 後新增的 slot 永遠進不來)。 // 走 base PATCH /templates/:id 增 slot;既有環境免另跑遷移腳本即收斂。 - const have = new Set((existing as any).slots ?? []); + let haveList: string[] = []; + try { + haveList = JSON.parse(tpl.slots_json ?? '[]'); + } catch { + haveList = []; + } + const have = new Set(haveList); const missing = slots.filter((s) => !have.has(s)); if (missing.length === 0) return; - await this.req('PATCH', `/templates/${encodeURIComponent((existing as any).id)}`, { + await this.req('PATCH', `/templates/${encodeURIComponent(tpl.id)}`, { slots: [...have, ...missing], }); } @@ -175,7 +193,13 @@ function qs(params: Record): string { return parts.length ? `?${parts.join('&')}` : ''; } -/** 從 Bindings 建 client。KBDB_BASE_URL 未設時拋錯(不准 fallback 直連 D1)。 */ -export function makeKbdbClient(env: { KBDB_BASE_URL?: string; KBDB_INTERNAL_TOKEN?: string }): KbdbClient { - return new KbdbClient(env.KBDB_BASE_URL ?? '', env.KBDB_INTERNAL_TOKEN); +/** 從 Bindings 建 client。KBDB_BASE_URL 未設時拋錯(不准 fallback 直連 D1)。 + * 有 KBDB_BASE_SVC(service binding)時優先走它,繞開 workers.dev→workers.dev 的 + * CF error 1042 封鎖;沒有就退回全域 fetch(本地 dev / 已有自訂域名時仍可用)。 */ +export function makeKbdbClient(env: { + KBDB_BASE_URL?: string; + KBDB_INTERNAL_TOKEN?: string; + KBDB_BASE_SVC?: { fetch: typeof fetch }; +}): KbdbClient { + return new KbdbClient(env.KBDB_BASE_URL ?? '', env.KBDB_INTERNAL_TOKEN, env.KBDB_BASE_SVC); } diff --git a/src/types.ts b/src/types.ts index 30ac9f0..1fd779b 100644 --- a/src/types.ts +++ b/src/types.ts @@ -6,6 +6,11 @@ export type Bindings = { KBDB_BASE_URL?: string; // 基本盤 arcrun/kbdb API 網址(leo: 可設定,先留空) KBDB_INGEST_URL?: string; // ingest 服務網址(refresh 代轉對象;T4 就緒前留空) KBDB_INTERNAL_TOKEN?: string; + // Service Binding(wrangler.toml [[services]])→ 直連基本盤 worker,繞開 + // CF error 1042(Worker 不能公開 fetch 另一個 *.workers.dev Worker)。 + // 2026-07-03 補跑實測發現:self-hosted 帳號常無自訂域名,KBDB_BASE_URL 單靠公開 + // fetch 在 workers.dev 對 workers.dev 場景會被 CF 擋,故新增此綁定作為正規解法。 + KBDB_BASE_SVC?: Fetcher; ENVIRONMENT: string; API_KEY?: string; };