fix(drainer): 同帳號 worker-to-worker 改走 Service Bindings(修 CF 1042)
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 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HJiLCRUU2o3aSpPEzVCt2o
This commit is contained in:
@@ -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)
|
- Secret:若設,同步 `GITEA_WEBHOOK_SECRET` secret(本 Worker 驗 `X-Gitea-Signature` HMAC-SHA256)
|
||||||
- Gitea 的 ping event → Worker 回 `{ok:true,pong:true}`(測連通)
|
- 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`
|
- **vars**:`REPO=Leo/notes REF=main OWNER=leo CARDS_ROOT=system-dev/wiki/cards GRAPH_KEY=leo BATCH_SIZE=2`
|
||||||
`CODE_WORKER_URL / KBDB_URL / GRAPH_URL / GRAPH_KEY`(皆 leo21c live 服務)。
|
- **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 檔)
|
## 規模備註(5,083 檔)
|
||||||
|
|
||||||
|
|||||||
@@ -28,15 +28,21 @@ export interface Env {
|
|||||||
REF: string; // main
|
REF: string; // main
|
||||||
OWNER: string; // leo(KBDB 租戶 owner_id)
|
OWNER: string; // leo(KBDB 租戶 owner_id)
|
||||||
CARDS_ROOT: string; // system-dev/wiki/cards
|
CARDS_ROOT: string; // system-dev/wiki/cards
|
||||||
CODE_WORKER_URL: string; // https://arcrun-code.leo21c.workers.dev/
|
// 同帳號 worker-to-worker 必須走 Service Binding(直接 fetch(*.leo21c.workers.dev) 會撞 CF 1042)。
|
||||||
KBDB_URL: string; // https://arcrun-kbdb.leo21c.workers.dev
|
SVC_CODE: Fetcher; // → arcrun-code
|
||||||
GRAPH_URL: string; // https://kbdb-graph-plugin.leo21c.workers.dev
|
SVC_KBDB: Fetcher; // → arcrun-kbdb
|
||||||
|
SVC_GRAPH: Fetcher; // → kbdb-graph-plugin
|
||||||
GRAPH_KEY: string; // leo
|
GRAPH_KEY: string; // leo
|
||||||
BATCH_SIZE: string; // "2"
|
BATCH_SIZE: string; // "2"
|
||||||
GITEA_TOKEN: string; // secret:讀 repo
|
GITEA_TOKEN: string; // secret:讀 repo
|
||||||
GITEA_WEBHOOK_SECRET?: string; // secret(選填):驗 webhook 簽章
|
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<Response> {
|
||||||
|
return binding.fetch(new Request(`https://svc${path}`, init));
|
||||||
|
}
|
||||||
|
|
||||||
const CURSOR_TYPE = 'ingest_cursor';
|
const CURSOR_TYPE = 'ingest_cursor';
|
||||||
const cursorPageName = (env: Env) => `cursor:km_wiki_ingest_drain:${env.REPO}`;
|
const cursorPageName = (env: Env) => `cursor:km_wiki_ingest_drain:${env.REPO}`;
|
||||||
|
|
||||||
@@ -73,7 +79,7 @@ async function giteaRaw(env: Env, relPath: string): Promise<string> {
|
|||||||
|
|
||||||
// ── card-processing 核心 ──────────────────────────────────────────────────────
|
// ── card-processing 核心 ──────────────────────────────────────────────────────
|
||||||
async function parseCard(env: Env, md: string, relPath: string): Promise<any> {
|
async function parseCard(env: Env, md: string, relPath: string): Promise<any> {
|
||||||
const r = await fetch(env.CODE_WORKER_URL, {
|
const r = await svc(env.SVC_CODE, '/', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'content-type': 'application/json' },
|
headers: { 'content-type': 'application/json' },
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
@@ -99,20 +105,20 @@ async function upsertEntry(env: Env, entry: any): Promise<{ action: string; id?:
|
|||||||
const pageName = entry.page_name;
|
const pageName = entry.page_name;
|
||||||
const metadata_json = JSON.stringify(entry.metadata);
|
const metadata_json = JSON.stringify(entry.metadata);
|
||||||
const newHash = entry.metadata.content_hash;
|
const newHash = entry.metadata.content_hash;
|
||||||
const lookup = await fetch(
|
const lookup = await svc(env.SVC_KBDB,
|
||||||
`${env.KBDB_URL}/entries?page_name=${encodeURIComponent(pageName)}&owner_id=${encodeURIComponent(env.OWNER)}`,
|
`/entries?page_name=${encodeURIComponent(pageName)}&owner_id=${encodeURIComponent(env.OWNER)}`,
|
||||||
);
|
);
|
||||||
const existing = ((await lookup.json()) as { entries?: any[] }).entries?.[0];
|
const existing = ((await lookup.json()) as { entries?: any[] }).entries?.[0];
|
||||||
if (existing) {
|
if (existing) {
|
||||||
if (metaHash(existing) === newHash) return { action: 'skip', id: existing.id };
|
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',
|
method: 'PATCH',
|
||||||
headers: { 'content-type': 'application/json' },
|
headers: { 'content-type': 'application/json' },
|
||||||
body: JSON.stringify({ content: entry.content, tags_json: JSON.stringify(entry.tags || []), metadata_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' }) };
|
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',
|
method: 'POST',
|
||||||
headers: { 'content-type': 'application/json' },
|
headers: { 'content-type': 'application/json' },
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
@@ -134,7 +140,7 @@ async function postEnvelopes(env: Env, envelopes: any[]): Promise<any[]> {
|
|||||||
for (const envp of envelopes) {
|
for (const envp of envelopes) {
|
||||||
// envelopes 已由 code 節點剝除 _-鍵;此處再保險剝一次。
|
// envelopes 已由 code 節點剝除 _-鍵;此處再保險剝一次。
|
||||||
const clean = Object.fromEntries(Object.entries(envp).filter(([k]) => !k.startsWith('_')));
|
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',
|
method: 'POST',
|
||||||
headers: { 'content-type': 'application/json', 'X-Arcrun-API-Key': env.GRAPH_KEY },
|
headers: { 'content-type': 'application/json', 'X-Arcrun-API-Key': env.GRAPH_KEY },
|
||||||
body: JSON.stringify(clean),
|
body: JSON.stringify(clean),
|
||||||
@@ -168,8 +174,8 @@ async function processCard(env: Env, relPath: string): Promise<any> {
|
|||||||
interface CursorState { last_path: string; cycle: number; processed_total: number; updated_at: number; }
|
interface CursorState { last_path: string; cycle: number; processed_total: number; updated_at: number; }
|
||||||
|
|
||||||
async function getCursor(env: Env): Promise<{ id?: string; state: CursorState }> {
|
async function getCursor(env: Env): Promise<{ id?: string; state: CursorState }> {
|
||||||
const r = await fetch(
|
const r = await svc(env.SVC_KBDB,
|
||||||
`${env.KBDB_URL}/entries?page_name=${encodeURIComponent(cursorPageName(env))}&owner_id=${encodeURIComponent(env.OWNER)}`,
|
`/entries?page_name=${encodeURIComponent(cursorPageName(env))}&owner_id=${encodeURIComponent(env.OWNER)}`,
|
||||||
);
|
);
|
||||||
const e = ((await r.json()) as { entries?: any[] }).entries?.[0];
|
const e = ((await r.json()) as { entries?: any[] }).entries?.[0];
|
||||||
if (e) {
|
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<void> {
|
async function setCursor(env: Env, cur: { id?: string; state: CursorState }): Promise<void> {
|
||||||
const content = JSON.stringify(cur.state);
|
const content = JSON.stringify(cur.state);
|
||||||
if (cur.id) {
|
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 }),
|
method: 'PATCH', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ content }),
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
await fetch(`${env.KBDB_URL}/entries`, {
|
await svc(env.SVC_KBDB, `/entries`, {
|
||||||
method: 'POST', headers: { 'content-type': 'application/json' },
|
method: 'POST', headers: { 'content-type': 'application/json' },
|
||||||
body: JSON.stringify({ entry_type: CURSOR_TYPE, page_name: cursorPageName(env), owner_id: env.OWNER, content,
|
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 }) }),
|
metadata_json: JSON.stringify({ kind: 'ingest_cursor', embed: false }) }),
|
||||||
|
|||||||
@@ -3,20 +3,27 @@ main = "src/index.ts"
|
|||||||
compatibility_date = "2025-02-19"
|
compatibility_date = "2025-02-19"
|
||||||
workers_dev = true
|
workers_dev = true
|
||||||
|
|
||||||
# ⚠️ 維持「非-auto」:預設不註冊 cron trigger(不自走排程、不無人值守亂寫)。
|
# ⚠️ 維持「非-auto」:預設不註冊 cron trigger(不自走排程)。drain 走 POST /drain(與 scheduled 同核心)。
|
||||||
# drain 以 POST /drain 手動觸發(與 scheduled handler 同核心)。
|
# 要開自走 Phase 0 backfill:leo 過閘後取消下面兩行註解再 deploy(scheduled handler 已在 code)。
|
||||||
# 要開自走 Phase 0 backfill:leo 過閘後,取消下面兩行註解再 deploy(scheduled handler 已在 code)。
|
|
||||||
# [triggers]
|
# [triggers]
|
||||||
# crons = ["*/5 * * * *"]
|
# 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]
|
[vars]
|
||||||
REPO = "Leo/notes"
|
REPO = "Leo/notes"
|
||||||
REF = "main"
|
REF = "main"
|
||||||
OWNER = "leo"
|
OWNER = "leo"
|
||||||
CARDS_ROOT = "system-dev/wiki/cards"
|
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"
|
GRAPH_KEY = "leo"
|
||||||
BATCH_SIZE = "2"
|
BATCH_SIZE = "2"
|
||||||
# secret(wrangler secret put):GITEA_TOKEN(必),GITEA_WEBHOOK_SECRET(選)
|
# secret(wrangler secret put):GITEA_TOKEN(必),GITEA_WEBHOOK_SECRET(選)
|
||||||
|
|||||||
Reference in New Issue
Block a user