fix(kbdb-client): ensureTemplate 誤判「不存在」導致重複建 template → base 500
補跑實測(POST /triplets/ingest 端到端跑不過)發現的真契約漂移,非單純部署坑:
- ensureTemplate() 假設 GET /templates/:name 回傳 {id, slots} 平鋪陣列,
但 base(arcrun-kbdb)實際回 {success, template:{id, slots_json:"[...]"}}
(多包一層 + slots 是 JSON 字串)。existing.id 永遠 undefined → 一律走「不存在」
分支重複 POST 同名 template → base 對重名衝突沒有優雅的 409,直接 500,
ingest 端到端全斷。
- 改為解 existing.template + JSON.parse(slots_json),修好後 POST /triplets/ingest
→ GET /triplets 端到端打通(見部署驗證 curl 記錄)。
同時讓 KbdbClient 支援可選 fetcher(service binding 的 Fetcher),
搭配上一個 commit 的 KBDB_BASE_SVC binding 繞開 workers.dev 互連封鎖;
沒有 binding 時(本地 dev/mock)仍 fallback 回全域 fetch,不影響既有單元測試。
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
+32
-8
@@ -36,6 +36,12 @@ export class KbdbClient {
|
|||||||
constructor(
|
constructor(
|
||||||
private readonly baseUrl: string,
|
private readonly baseUrl: string,
|
||||||
private readonly token?: 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) {
|
if (!baseUrl) {
|
||||||
throw new Error('KBDB_BASE_URL 未設定:插件需指向基本盤 API(不可直連 D1)');
|
throw new Error('KBDB_BASE_URL 未設定:插件需指向基本盤 API(不可直連 D1)');
|
||||||
@@ -46,7 +52,8 @@ export class KbdbClient {
|
|||||||
const headers: Record<string, string> = { 'Content-Type': 'application/json' };
|
const headers: Record<string, string> = { 'Content-Type': 'application/json' };
|
||||||
if (this.token) headers['Authorization'] = `Bearer ${this.token}`;
|
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,
|
method,
|
||||||
headers,
|
headers,
|
||||||
body: body === undefined ? undefined : JSON.stringify(body),
|
body: body === undefined ? undefined : JSON.stringify(body),
|
||||||
@@ -113,23 +120,34 @@ export class KbdbClient {
|
|||||||
// --- templates(= 替代建表;插件要新類型只能建 template) ---
|
// --- templates(= 替代建表;插件要新類型只能建 template) ---
|
||||||
|
|
||||||
async ensureTemplate(name: string, slots: string[], description?: string): Promise<void> {
|
async ensureTemplate(name: string, slots: string[], description?: string): Promise<void> {
|
||||||
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',
|
'GET',
|
||||||
`/templates/${encodeURIComponent(name)}`,
|
`/templates/${encodeURIComponent(name)}`,
|
||||||
).catch(() => null);
|
).catch(() => null);
|
||||||
|
const tpl = existing?.template;
|
||||||
|
|
||||||
// 全新 template → 建。
|
// 全新 template → 建。
|
||||||
if (!existing || !(existing as any).id) {
|
if (!tpl || !tpl.id) {
|
||||||
await this.req('POST', '/templates', { name, slots, description, created_by: 'kbdb-graph' });
|
await this.req('POST', '/templates', { name, slots, description, created_by: 'kbdb-graph' });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 既有 template → 補缺 slot(不 early-return;否則 seed 後新增的 slot 永遠進不來)。
|
// 既有 template → 補缺 slot(不 early-return;否則 seed 後新增的 slot 永遠進不來)。
|
||||||
// 走 base PATCH /templates/:id 增 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));
|
const missing = slots.filter((s) => !have.has(s));
|
||||||
if (missing.length === 0) return;
|
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],
|
slots: [...have, ...missing],
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -175,7 +193,13 @@ function qs(params: Record<string, string | number | undefined>): string {
|
|||||||
return parts.length ? `?${parts.join('&')}` : '';
|
return parts.length ? `?${parts.join('&')}` : '';
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 從 Bindings 建 client。KBDB_BASE_URL 未設時拋錯(不准 fallback 直連 D1)。 */
|
/** 從 Bindings 建 client。KBDB_BASE_URL 未設時拋錯(不准 fallback 直連 D1)。
|
||||||
export function makeKbdbClient(env: { KBDB_BASE_URL?: string; KBDB_INTERNAL_TOKEN?: string }): KbdbClient {
|
* 有 KBDB_BASE_SVC(service binding)時優先走它,繞開 workers.dev→workers.dev 的
|
||||||
return new KbdbClient(env.KBDB_BASE_URL ?? '', env.KBDB_INTERNAL_TOKEN);
|
* 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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,11 @@ export type Bindings = {
|
|||||||
KBDB_BASE_URL?: string; // 基本盤 arcrun/kbdb API 網址(leo: 可設定,先留空)
|
KBDB_BASE_URL?: string; // 基本盤 arcrun/kbdb API 網址(leo: 可設定,先留空)
|
||||||
KBDB_INGEST_URL?: string; // ingest 服務網址(refresh 代轉對象;T4 就緒前留空)
|
KBDB_INGEST_URL?: string; // ingest 服務網址(refresh 代轉對象;T4 就緒前留空)
|
||||||
KBDB_INTERNAL_TOKEN?: string;
|
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;
|
ENVIRONMENT: string;
|
||||||
API_KEY?: string;
|
API_KEY?: string;
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user