From 88b7fbf4093cf0b2800fae0b46a31f55b1b9ff6c Mon Sep 17 00:00:00 2001 From: kbdb-cc Date: Mon, 6 Jul 2026 09:13:03 +0000 Subject: [PATCH] =?UTF-8?q?feat(km-wiki-ingest):=20=E6=96=B0=E5=A2=9E?= =?UTF-8?q?=E7=84=A1=E4=BA=BA=E5=80=BC=E5=AE=88=20drainer=20runtime?= =?UTF-8?q?=EF=BC=88cursor=20+=20cron=20+=20Gitea=20webhook=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Phase b:把 ingest 從手動直驅升級成真無人值守。drainer/ 是 workflow km_wiki_ingest_drain 的可部署執行體(Cloudflare Worker),復用已 live 的 arcrun-code/kbdb/graph、不改 cypher-executor。 - 兩觸發共用 processCard 核心:(A) cron drain(Gitea tree 列卡→游標後取一小批→ 逐卡處理→進游標→到底回捲;撐 5,083 檔);(B) Gitea push webhook→只處理 delta。 - 游標存 KBDB 一個 ingest_cursor entry(走 API、零建表)。冪等:未改卡 entry (page_name+content_hash)/triplet(uri+hash) 皆 skip,只有新/改卡真寫。 - src/usercode.ts 由 workflow.yaml parse_card.code 抽出(單一真相源 card-to-envelope.mjs)。 - 維持非-auto:wrangler.toml 的 cron trigger 預設註解(不自走),drain 走 POST /drain。 scheduled handler 已在 code,leo 過閘後取消註解即開自走。README 記部署/secret/webhook 設定。 已部署 leo21c:arcrun-km-wiki-drainer.leo21c.workers.dev。以 wrangler dev(本地 miniflare + .dev.vars token,非 secret 寫入)對 live 服務驗證:cron drain 冪等全 skip、游標前進+回捲、 webhook delta 只寫變更卡、三模式命中新卡。GITEA_TOKEN secret 需 leo 於互動模式設(auto 擋 secret 寫入)。 Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01HJiLCRUU2o3aSpPEzVCt2o --- .../examples/km-wiki-ingest/description.md | 2 +- .../km-wiki-ingest/drainer/.gitignore | 3 + .../examples/km-wiki-ingest/drainer/README.md | 70 + .../km-wiki-ingest/drainer/package-lock.json | 1528 +++++++++++++++++ .../km-wiki-ingest/drainer/package.json | 13 + .../km-wiki-ingest/drainer/src/index.ts | 293 ++++ .../km-wiki-ingest/drainer/src/usercode.ts | 4 + .../km-wiki-ingest/drainer/tsconfig.json | 14 + .../km-wiki-ingest/drainer/wrangler.toml | 22 + 9 files changed, 1948 insertions(+), 1 deletion(-) create mode 100644 registry/examples/km-wiki-ingest/drainer/.gitignore create mode 100644 registry/examples/km-wiki-ingest/drainer/README.md create mode 100644 registry/examples/km-wiki-ingest/drainer/package-lock.json create mode 100644 registry/examples/km-wiki-ingest/drainer/package.json create mode 100644 registry/examples/km-wiki-ingest/drainer/src/index.ts create mode 100644 registry/examples/km-wiki-ingest/drainer/src/usercode.ts create mode 100644 registry/examples/km-wiki-ingest/drainer/tsconfig.json create mode 100644 registry/examples/km-wiki-ingest/drainer/wrangler.toml diff --git a/registry/examples/km-wiki-ingest/description.md b/registry/examples/km-wiki-ingest/description.md index da1660c..9f066f6 100644 --- a/registry/examples/km-wiki-ingest/description.md +++ b/registry/examples/km-wiki-ingest/description.md @@ -67,7 +67,7 @@ self-test 合成超大卡(不分段 est=114 會炸)→ 自動分 4 段、每 ## 待 live 部署 + 寫入(總管過 leo 閘用) 1. **部署通用 `code` 零件**(Arcrun#10 分支 `feat/issue-10-code-component`,已就緒):`cd registry/components/code && npm install && npx wrangler deploy`(→ `code.arcrun.dev`)+ `register-component.sh code`。本 workflow 的 parse_card 以 `component: code` 引用它,解析 JS 已內聯在 workflow.yaml(= `lib/card-to-envelope.mjs` 邏輯)。不再部署 domain 零件 `km_wiki_card_parse`。 -2. **部署 workflow**:`km_wiki_ingest_drain`(cron drain);`wrangler` 直推 leo21c(**禁 `acr update`**——codeload 綁 GitHub 假綠,Arcrun#4)。 +2. **部署 workflow runtime**:`km_wiki_ingest_drain` 的**無人值守執行體= `drainer/`**(Cloudflare Worker:cron drain + Gitea webhook delta,游標存 KBDB、復用 code/kbdb/graph);`wrangler` 直推 leo21c(**禁 `acr update`**)。已 live:`arcrun-km-wiki-drainer.leo21c.workers.dev`。部署/觸發/webhook 設定見 `drainer/README.md`。(workflow.yaml 為宣告式 spec;drainer 為其可執行 runtime。) 3. **注入環境變數**(不放 repo):`repo=Leo/notes ref=main gitea_token kbdb_url=https://arcrun-kbdb.leo21c.workers.dev kbdb_api_key graph_url graph_api_key=leo`。`CLOUDFLARE_ACCOUNT_ID=leo21c`(別讓官方 58309b 污染)。 4. **entry 寫入路徑確認**:若 `kbdb_upsert_block` 尚不透傳 `metadata_json`(需 `embed:true`/`content_hash`),entry 改用 `http_request` 直打 base `POST/PATCH /entries` 帶 `body_json.metadata_json`。 5. **預期寫入量(Leo/notes 現況 3 卡)**:3 entries + 15 triplets + 16 node records(去重後更少);分 3 次 graph 呼叫(每次 ≤ 33 subrequest)+ 3 次 entry upsert。全庫鋪開時照 cron 一卡一 tick 慢推。 diff --git a/registry/examples/km-wiki-ingest/drainer/.gitignore b/registry/examples/km-wiki-ingest/drainer/.gitignore new file mode 100644 index 0000000..cf53edc --- /dev/null +++ b/registry/examples/km-wiki-ingest/drainer/.gitignore @@ -0,0 +1,3 @@ +.dev.vars +node_modules/ +.wrangler/ diff --git a/registry/examples/km-wiki-ingest/drainer/README.md b/registry/examples/km-wiki-ingest/drainer/README.md new file mode 100644 index 0000000..d7d6a33 --- /dev/null +++ b/registry/examples/km-wiki-ingest/drainer/README.md @@ -0,0 +1,70 @@ +# arcrun-km-wiki-drainer —— km_wiki_ingest_drain 的無人值守 runtime + +`workflow.yaml` 是宣告式 spec;**本 Worker 是它可真部署、有游標/觸發的執行體**。 +復用已 live 的零件與服務,**不改 cypher-executor**: + +``` +Gitea 抓卡 md → arcrun-code 零件(sandbox inline JS 解析)→ /entries 冪等 upsert(embed:true) + → graph /triplets/ingest(server 側 per-source 冪等) +``` + +- Live:`https://arcrun-km-wiki-drainer.leo21c.workers.dev` +- 解析 JS=`src/usercode.ts`(由 `workflow.yaml` 的 `parse_card.code` 抽出,單一真相源 `lib/card-to-envelope.mjs`)。 + +## 兩觸發模式(共用同一 `processCard` 核心) + +| 模式 | 觸發 | 行為 | +|---|---|---| +| **Phase 0 cron drain** | cron tick(見「開自走」)| `drainBatch(BATCH_SIZE)`:Gitea tree 列卡 → 游標之後取一小批 → 逐卡 `processCard` → 進游標(到底**回捲**重掃)。壓 CF subrequest 下、撐得住 5,083 檔(每 tick 小批)。 | +| **穩態 webhook** | Gitea push webhook → `POST /webhook` | 只處理本次 commit 動到的 `system-dev/wiki/cards/**.md`(delta),不重掃全庫。**Gitea→Cloudflare,非 GitHub Actions(不觸 D20)**。 | + +**冪等**:未改卡的 entry(`page_name`+`content_hash`)與 triplet(`source.uri`+`content_hash`)皆 skip;只有新/改卡真寫。游標存 KBDB 一個 `ingest_cursor` entry(走 API、零建表)。 + +## HTTP 路由 + +| 方法 | 路徑 | 用途 | +|---|---|---| +| GET | `/` | 健康 | +| GET | `/cursor` | 看游標狀態 | +| POST | `/drain?batch=N` | 手動觸發一批 drain(= 一個 cron tick;驗收/補跑用) | +| POST | `/webhook` | Gitea push webhook 入口(delta) | +| POST | `/cursor/reset` | 游標歸零(重新全 drain) | + +## ⚠️ 非-auto(預設) + +`wrangler.toml` 的 `[triggers] crons` **預設註解掉**——不自走排程、不無人值守亂寫。 +drain 以 `POST /drain` 手動觸發(與 cron `scheduled` handler 同核心)。 +要開自走 Phase 0 backfill:leo 過閘後取消 `wrangler.toml` 那兩行註解再 deploy。 + +## 部署(wrangler 直推 leo21c,禁 acr update) + +```bash +cd registry/examples/km-wiki-ingest/drainer +npm install +npx wrangler deploy +# ★ 必須:設 Gitea 讀取權(Leo/notes 為 private repo)——此步需 leo 在互動模式跑(secret 寫入) +printf '%s' "$GITEA_TOKEN" | npx wrangler secret put GITEA_TOKEN +# 選填:webhook 簽章驗證 +# printf '%s' "$WEBHOOK_SECRET" | npx wrangler secret put GITEA_WEBHOOK_SECRET +``` + +## Gitea webhook 設定(穩態 delta) + +`Leo/notes` → Settings → Webhooks → Add Webhook → Gitea: +- Target URL:`https://arcrun-km-wiki-drainer.leo21c.workers.dev/webhook` +- HTTP Method:POST,Content-Type:`application/json` +- Trigger:Push events(可只限 `main`) +- Secret:若設,同步 `GITEA_WEBHOOK_SECRET` secret(本 Worker 驗 `X-Gitea-Signature` HMAC-SHA256) +- Gitea 的 ping event → Worker 回 `{ok:true,pong:true}`(測連通) + +## config(wrangler.toml [vars]) + +`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 服務)。 + +## 規模備註(5,083 檔) + +- 列卡走 Gitea `git/trees?recursive=true`(一次全樹);若 `truncated`,回應標記 `truncated:true`, + 屆時換 tree 分頁列法(現況 notes 卡數小、不截斷)。 +- 每 tick 只 `BATCH_SIZE` 張,游標續傳;到底回捲 → 全庫變更靠冪等於下一輪捕捉; + 即時變更走 webhook。 diff --git a/registry/examples/km-wiki-ingest/drainer/package-lock.json b/registry/examples/km-wiki-ingest/drainer/package-lock.json new file mode 100644 index 0000000..66b3964 --- /dev/null +++ b/registry/examples/km-wiki-ingest/drainer/package-lock.json @@ -0,0 +1,1528 @@ +{ + "name": "arcrun-km-wiki-drainer", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "arcrun-km-wiki-drainer", + "version": "0.1.0", + "devDependencies": { + "@cloudflare/workers-types": "^4.20250408.0", + "typescript": "^5.7.0", + "wrangler": "^4.0.0" + } + }, + "node_modules/@cloudflare/kv-asset-handler": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/@cloudflare/kv-asset-handler/-/kv-asset-handler-0.5.0.tgz", + "integrity": "sha512-jxQYkj8dSIzc0cD6cMMNdOc1UVjqSqu8BZdor5s8cGjW2I8BjODt/kWPVdY+u9zj3ms75Q5qaZgnxUad83+eAg==", + "dev": true, + "license": "MIT OR Apache-2.0", + "engines": { + "node": ">=22.0.0" + } + }, + "node_modules/@cloudflare/unenv-preset": { + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/@cloudflare/unenv-preset/-/unenv-preset-2.16.1.tgz", + "integrity": "sha512-ECxObrMfyTl5bhQf/lZCXwo5G6xX9IAUo+nDMKK4SZ8m4Jvvxp52vilxyySSWh2YTZz8+HQ07qGH/2rEom1vDw==", + "dev": true, + "license": "MIT OR Apache-2.0", + "peerDependencies": { + "unenv": "2.0.0-rc.24", + "workerd": ">1.20260305.0 <2.0.0-0" + }, + "peerDependenciesMeta": { + "workerd": { + "optional": true + } + } + }, + "node_modules/@cloudflare/workerd-darwin-64": { + "version": "1.20260701.1", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-darwin-64/-/workerd-darwin-64-1.20260701.1.tgz", + "integrity": "sha512-Zd9Y1bah6DwwBN2RW8vJohffQrIUazb8UXnqSNecOxM+jJLhUuvv5IOG8dbHcV83TyZAubea6gsQXo2yH1lDdw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=16" + } + }, + "node_modules/@cloudflare/workerd-darwin-arm64": { + "version": "1.20260701.1", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-darwin-arm64/-/workerd-darwin-arm64-1.20260701.1.tgz", + "integrity": "sha512-yBLsjS1qCWqFyCY37qRUrYfzHHvMGvjh8zRKJ6MvUivYDhkZTzqduppK38FoqYvayLJ5KbcxH7zo5rkxGqbsaA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=16" + } + }, + "node_modules/@cloudflare/workerd-linux-64": { + "version": "1.20260701.1", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-linux-64/-/workerd-linux-64-1.20260701.1.tgz", + "integrity": "sha512-vMfqSIMfoo4xmZXEuUVqLpSFS921YKjiR9q7kDXPi6Vld1PK74UHg9LZuBavT2KSyemHUCTpj9y/4JSYOEyQbQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16" + } + }, + "node_modules/@cloudflare/workerd-linux-arm64": { + "version": "1.20260701.1", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-linux-arm64/-/workerd-linux-arm64-1.20260701.1.tgz", + "integrity": "sha512-HRfwbKU2pK44V2NhoM0+iH0JJSj7nQ9Wv13ifIiGYCmTtDL8/zKtEhX7kQ3D4Vy/Cpjhttl0FkfqXj1aqLDPPg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16" + } + }, + "node_modules/@cloudflare/workerd-windows-64": { + "version": "1.20260701.1", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-windows-64/-/workerd-windows-64-1.20260701.1.tgz", + "integrity": "sha512-ngxCiIN9s/fM2o1IBMD0o1/mcXrv2NJVdyznh51UH8sQuvrTrXvV2nM0Uj/qU2wMwF6prgNBcdcd7AZeZGiBQA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=16" + } + }, + "node_modules/@cloudflare/workers-types": { + "version": "4.20260702.1", + "resolved": "https://registry.npmjs.org/@cloudflare/workers-types/-/workers-types-4.20260702.1.tgz", + "integrity": "sha512-mOhf5TUEB1m2vPrxtqoIGfz0fUC9xyxRDx5gWHy5s+OCo6dcV+g7wI1R7gYCMFohhqF/2y2xeKVwMwCJjfn/WA==", + "dev": true, + "license": "MIT OR Apache-2.0" + }, + "node_modules/@cspotcode/source-map-support": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", + "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "0.3.9" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.11.2", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.2.tgz", + "integrity": "sha512-kyOl3X0DuTiT1h2ft8r2fYO8JYtU9a9Xis/zBSiGArNaagCOWx90N1k2wxp18czFDH+OgcWGb5ZP/XMt3dcyPA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.1.tgz", + "integrity": "sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.1.tgz", + "integrity": "sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.1.tgz", + "integrity": "sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.1.tgz", + "integrity": "sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.1.tgz", + "integrity": "sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.1.tgz", + "integrity": "sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.1.tgz", + "integrity": "sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.1.tgz", + "integrity": "sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.1.tgz", + "integrity": "sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.1.tgz", + "integrity": "sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.1.tgz", + "integrity": "sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.1.tgz", + "integrity": "sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.1.tgz", + "integrity": "sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.1.tgz", + "integrity": "sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.1.tgz", + "integrity": "sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.1.tgz", + "integrity": "sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.1.tgz", + "integrity": "sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.1.tgz", + "integrity": "sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.1.tgz", + "integrity": "sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.1.tgz", + "integrity": "sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.1.tgz", + "integrity": "sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.1.tgz", + "integrity": "sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.1.tgz", + "integrity": "sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.1.tgz", + "integrity": "sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.1.tgz", + "integrity": "sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.1.tgz", + "integrity": "sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@img/colour": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.1.0.tgz", + "integrity": "sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@img/sharp-darwin-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.34.5.tgz", + "integrity": "sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-arm64": "1.2.4" + } + }, + "node_modules/@img/sharp-darwin-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.34.5.tgz", + "integrity": "sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-x64": "1.2.4" + } + }, + "node_modules/@img/sharp-libvips-darwin-arm64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.2.4.tgz", + "integrity": "sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-darwin-x64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.2.4.tgz", + "integrity": "sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.2.4.tgz", + "integrity": "sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.2.4.tgz", + "integrity": "sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-ppc64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.2.4.tgz", + "integrity": "sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-riscv64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.2.4.tgz", + "integrity": "sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-s390x": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.2.4.tgz", + "integrity": "sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-x64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.2.4.tgz", + "integrity": "sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-arm64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.2.4.tgz", + "integrity": "sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-x64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.2.4.tgz", + "integrity": "sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-linux-arm": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.34.5.tgz", + "integrity": "sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.34.5.tgz", + "integrity": "sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm64": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-ppc64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.34.5.tgz", + "integrity": "sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-ppc64": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-riscv64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.34.5.tgz", + "integrity": "sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-riscv64": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-s390x": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.34.5.tgz", + "integrity": "sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-s390x": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.34.5.tgz", + "integrity": "sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-x64": "1.2.4" + } + }, + "node_modules/@img/sharp-linuxmusl-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.34.5.tgz", + "integrity": "sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-arm64": "1.2.4" + } + }, + "node_modules/@img/sharp-linuxmusl-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.34.5.tgz", + "integrity": "sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-x64": "1.2.4" + } + }, + "node_modules/@img/sharp-wasm32": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.34.5.tgz", + "integrity": "sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==", + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", + "optional": true, + "dependencies": { + "@emnapi/runtime": "^1.7.0" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.34.5.tgz", + "integrity": "sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-ia32": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.34.5.tgz", + "integrity": "sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.34.5.tgz", + "integrity": "sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", + "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + }, + "node_modules/@poppinss/colors": { + "version": "4.1.6", + "resolved": "https://registry.npmjs.org/@poppinss/colors/-/colors-4.1.6.tgz", + "integrity": "sha512-H9xkIdFswbS8n1d6vmRd8+c10t2Qe+rZITbbDHHkQixH5+2x1FDGmi/0K+WgWiqQFKPSlIYB7jlH6Kpfn6Fleg==", + "dev": true, + "license": "MIT", + "dependencies": { + "kleur": "^4.1.5" + } + }, + "node_modules/@poppinss/dumper": { + "version": "0.6.5", + "resolved": "https://registry.npmjs.org/@poppinss/dumper/-/dumper-0.6.5.tgz", + "integrity": "sha512-NBdYIb90J7LfOI32dOewKI1r7wnkiH6m920puQ3qHUeZkxNkQiFnXVWoE6YtFSv6QOiPPf7ys6i+HWWecDz7sw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@poppinss/colors": "^4.1.5", + "@sindresorhus/is": "^7.0.2", + "supports-color": "^10.0.0" + } + }, + "node_modules/@poppinss/exception": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@poppinss/exception/-/exception-1.2.3.tgz", + "integrity": "sha512-dCED+QRChTVatE9ibtoaxc+WkdzOSjYTKi/+uacHWIsfodVfpsueo3+DKpgU5Px8qXjgmXkSvhXvSCz3fnP9lw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@sindresorhus/is": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-7.2.0.tgz", + "integrity": "sha512-P1Cz1dWaFfR4IR+U13mqqiGsLFf1KbayybWwdd2vfctdV6hDpUkgCY0nKOLLTMSoRd/jJNjtbqzf13K8DCCXQw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sindresorhus/is?sponsor=1" + } + }, + "node_modules/@speed-highlight/core": { + "version": "1.2.17", + "resolved": "https://registry.npmjs.org/@speed-highlight/core/-/core-1.2.17.tgz", + "integrity": "sha512-Z92FwKpCtfaW1V0jTU/fh3QzYEZN8wDwrzRIBoADCJfn4mJCNcJN/XegifX7BDrQ8/h9Xh/JnbyMchL0FqXrkg==", + "dev": true, + "license": "CC0-1.0" + }, + "node_modules/blake3-wasm": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/blake3-wasm/-/blake3-wasm-2.1.5.tgz", + "integrity": "sha512-F1+K8EbfOZE49dtoPtmxUQrpXaBIl3ICvasLh+nJta0xkz+9kF/7uet9fLnwKqhDrmj6g+6K3Tw9yQPUg2ka5g==", + "dev": true, + "license": "MIT" + }, + "node_modules/cookie": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz", + "integrity": "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/error-stack-parser-es": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/error-stack-parser-es/-/error-stack-parser-es-1.0.5.tgz", + "integrity": "sha512-5qucVt2XcuGMcEGgWI7i+yZpmpByQ8J1lHhcL7PwqCwu9FPP3VUXzT4ltHe5i2z9dePwEHcDVOAfSnHsOlCXRA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/esbuild": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.1.tgz", + "integrity": "sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.28.1", + "@esbuild/android-arm": "0.28.1", + "@esbuild/android-arm64": "0.28.1", + "@esbuild/android-x64": "0.28.1", + "@esbuild/darwin-arm64": "0.28.1", + "@esbuild/darwin-x64": "0.28.1", + "@esbuild/freebsd-arm64": "0.28.1", + "@esbuild/freebsd-x64": "0.28.1", + "@esbuild/linux-arm": "0.28.1", + "@esbuild/linux-arm64": "0.28.1", + "@esbuild/linux-ia32": "0.28.1", + "@esbuild/linux-loong64": "0.28.1", + "@esbuild/linux-mips64el": "0.28.1", + "@esbuild/linux-ppc64": "0.28.1", + "@esbuild/linux-riscv64": "0.28.1", + "@esbuild/linux-s390x": "0.28.1", + "@esbuild/linux-x64": "0.28.1", + "@esbuild/netbsd-arm64": "0.28.1", + "@esbuild/netbsd-x64": "0.28.1", + "@esbuild/openbsd-arm64": "0.28.1", + "@esbuild/openbsd-x64": "0.28.1", + "@esbuild/openharmony-arm64": "0.28.1", + "@esbuild/sunos-x64": "0.28.1", + "@esbuild/win32-arm64": "0.28.1", + "@esbuild/win32-ia32": "0.28.1", + "@esbuild/win32-x64": "0.28.1" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/kleur": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/kleur/-/kleur-4.1.5.tgz", + "integrity": "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/miniflare": { + "version": "4.20260701.0", + "resolved": "https://registry.npmjs.org/miniflare/-/miniflare-4.20260701.0.tgz", + "integrity": "sha512-L6eAAi6IKtyb/7J6L+YsH2vb1yBrJWKRXI293JYDiMl70+6nncdAgigex58w6WBd+CwvdMsqOyNyGs95Op5gWQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@cspotcode/source-map-support": "0.8.1", + "sharp": "0.34.5", + "undici": "7.28.0", + "workerd": "1.20260701.1", + "ws": "8.21.0", + "youch": "4.1.0-beta.10" + }, + "bin": { + "miniflare": "bootstrap.js" + }, + "engines": { + "node": ">=22.0.0" + } + }, + "node_modules/path-to-regexp": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-6.3.0.tgz", + "integrity": "sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" + }, + "node_modules/semver": { + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/sharp": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.5.tgz", + "integrity": "sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==", + "dev": true, + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "@img/colour": "^1.0.0", + "detect-libc": "^2.1.2", + "semver": "^7.7.3" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-darwin-arm64": "0.34.5", + "@img/sharp-darwin-x64": "0.34.5", + "@img/sharp-libvips-darwin-arm64": "1.2.4", + "@img/sharp-libvips-darwin-x64": "1.2.4", + "@img/sharp-libvips-linux-arm": "1.2.4", + "@img/sharp-libvips-linux-arm64": "1.2.4", + "@img/sharp-libvips-linux-ppc64": "1.2.4", + "@img/sharp-libvips-linux-riscv64": "1.2.4", + "@img/sharp-libvips-linux-s390x": "1.2.4", + "@img/sharp-libvips-linux-x64": "1.2.4", + "@img/sharp-libvips-linuxmusl-arm64": "1.2.4", + "@img/sharp-libvips-linuxmusl-x64": "1.2.4", + "@img/sharp-linux-arm": "0.34.5", + "@img/sharp-linux-arm64": "0.34.5", + "@img/sharp-linux-ppc64": "0.34.5", + "@img/sharp-linux-riscv64": "0.34.5", + "@img/sharp-linux-s390x": "0.34.5", + "@img/sharp-linux-x64": "0.34.5", + "@img/sharp-linuxmusl-arm64": "0.34.5", + "@img/sharp-linuxmusl-x64": "0.34.5", + "@img/sharp-wasm32": "0.34.5", + "@img/sharp-win32-arm64": "0.34.5", + "@img/sharp-win32-ia32": "0.34.5", + "@img/sharp-win32-x64": "0.34.5" + } + }, + "node_modules/supports-color": { + "version": "10.2.2", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-10.2.2.tgz", + "integrity": "sha512-SS+jx45GF1QjgEXQx4NJZV9ImqmO2NPz5FNsIHrsDjh2YsHnawpan7SNQ1o8NuhrbHZy9AZhIoCUiCeaW/C80g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true, + "license": "0BSD", + "optional": true + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-7.28.0.tgz", + "integrity": "sha512-cRZYrTDwWznlnRiPjggAGxZXanty6M8RV1ff8Wm4LWXBp7/IG8v5DnOm74DtUBp9OONpK75YlPnIjQqX0dBDtA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20.18.1" + } + }, + "node_modules/unenv": { + "version": "2.0.0-rc.24", + "resolved": "https://registry.npmjs.org/unenv/-/unenv-2.0.0-rc.24.tgz", + "integrity": "sha512-i7qRCmY42zmCwnYlh9H2SvLEypEFGye5iRmEMKjcGi7zk9UquigRjFtTLz0TYqr0ZGLZhaMHl/foy1bZR+Cwlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "pathe": "^2.0.3" + } + }, + "node_modules/workerd": { + "version": "1.20260701.1", + "resolved": "https://registry.npmjs.org/workerd/-/workerd-1.20260701.1.tgz", + "integrity": "sha512-uF813NG09JwNRRUfJ0zBomyTslSPM810dMj9LVvkQ7RAkLrQLzAlPU8Xh/3dIqZDo2bfd7tChbf2PtqLRARRJQ==", + "dev": true, + "hasInstallScript": true, + "license": "Apache-2.0", + "bin": { + "workerd": "bin/workerd" + }, + "engines": { + "node": ">=16" + }, + "optionalDependencies": { + "@cloudflare/workerd-darwin-64": "1.20260701.1", + "@cloudflare/workerd-darwin-arm64": "1.20260701.1", + "@cloudflare/workerd-linux-64": "1.20260701.1", + "@cloudflare/workerd-linux-arm64": "1.20260701.1", + "@cloudflare/workerd-windows-64": "1.20260701.1" + } + }, + "node_modules/wrangler": { + "version": "4.107.0", + "resolved": "https://registry.npmjs.org/wrangler/-/wrangler-4.107.0.tgz", + "integrity": "sha512-fw69ThymNitZ0oIEBU2yNeq3kK59UKz/jyA3udwRrQIAIsxX57q5qLOpPTN7qc5t8n9pnUeofe0uxtMuhQZW8w==", + "dev": true, + "license": "MIT OR Apache-2.0", + "dependencies": { + "@cloudflare/kv-asset-handler": "0.5.0", + "@cloudflare/unenv-preset": "2.16.1", + "blake3-wasm": "2.1.5", + "esbuild": "0.28.1", + "miniflare": "4.20260701.0", + "path-to-regexp": "6.3.0", + "unenv": "2.0.0-rc.24", + "workerd": "1.20260701.1" + }, + "bin": { + "cf-wrangler": "bin/cf-wrangler.js", + "wrangler": "bin/wrangler.js", + "wrangler2": "bin/wrangler.js" + }, + "engines": { + "node": ">=22.0.0" + }, + "optionalDependencies": { + "fsevents": "2.3.3" + }, + "peerDependencies": { + "@cloudflare/workers-types": "^4.20260701.1" + }, + "peerDependenciesMeta": { + "@cloudflare/workers-types": { + "optional": true + } + } + }, + "node_modules/ws": { + "version": "8.21.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.0.tgz", + "integrity": "sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/youch": { + "version": "4.1.0-beta.10", + "resolved": "https://registry.npmjs.org/youch/-/youch-4.1.0-beta.10.tgz", + "integrity": "sha512-rLfVLB4FgQneDr0dv1oddCVZmKjcJ6yX6mS4pU82Mq/Dt9a3cLZQ62pDBL4AUO+uVrCvtWz3ZFUL2HFAFJ/BXQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@poppinss/colors": "^4.1.5", + "@poppinss/dumper": "^0.6.4", + "@speed-highlight/core": "^1.2.7", + "cookie": "^1.0.2", + "youch-core": "^0.3.3" + } + }, + "node_modules/youch-core": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/youch-core/-/youch-core-0.3.3.tgz", + "integrity": "sha512-ho7XuGjLaJ2hWHoK8yFnsUGy2Y5uDpqSTq1FkHLK4/oqKtyUU1AFbOOxY4IpC9f0fTLjwYbslUz0Po5BpD1wrA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@poppinss/exception": "^1.2.2", + "error-stack-parser-es": "^1.0.5" + } + } + } +} diff --git a/registry/examples/km-wiki-ingest/drainer/package.json b/registry/examples/km-wiki-ingest/drainer/package.json new file mode 100644 index 0000000..c1483ff --- /dev/null +++ b/registry/examples/km-wiki-ingest/drainer/package.json @@ -0,0 +1,13 @@ +{ + "name": "arcrun-km-wiki-drainer", + "version": "0.1.0", + "private": true, + "type": "module", + "description": "km_wiki_ingest_drain 的無人值守 runtime(cron drain + Gitea webhook delta)", + "scripts": { "deploy": "wrangler deploy" }, + "devDependencies": { + "@cloudflare/workers-types": "^4.20250408.0", + "typescript": "^5.7.0", + "wrangler": "^4.0.0" + } +} diff --git a/registry/examples/km-wiki-ingest/drainer/src/index.ts b/registry/examples/km-wiki-ingest/drainer/src/index.ts new file mode 100644 index 0000000..1c0ba65 --- /dev/null +++ b/registry/examples/km-wiki-ingest/drainer/src/index.ts @@ -0,0 +1,293 @@ +/** + * arcrun km_wiki_ingest_drainer —— 無人值守卡片 ingest 編排 Worker(Phase b) + * + * 這是 workflow `km_wiki_ingest_drain` 的「具體 runtime」:workflow.yaml 是宣告式 spec, + * 本 Worker 是可真部署、有游標/觸發的執行體。復用已 live 的零件與服務,不動 cypher-executor: + * Gitea 抓卡 md → arcrun-code 零件(sandbox inline JS 解析)→ /entries 冪等 upsert(embed:true) + * → graph /triplets/ingest(server 側 per-source 冪等)。 + * + * 兩觸發模式共用同一 card-processing 核心(processCard): + * (A) Phase 0 cron drain:cron tick → drainBatch(BATCH_SIZE):列卡(Gitea tree recursive)→ + * 游標(存 KBDB 一個 cursor entry)之後取一小批 → 逐卡 processCard → 進游標(到底回捲)。 + * 冪等:未改卡的 entry(content_hash)/triplet(uri+hash) 皆 skip,只有新/改卡真寫。 + * (B) 穩態 webhook:Gitea push webhook(非 GitHub Actions,不觸 D20)→ 只處理 delta(本次 + * commit 動到的 system-dev/wiki/cards/**.md)→ 逐卡 processCard。不重掃全庫。 + * + * HTTP 路由(fetch): + * GET / 健康 + * GET /cursor 看游標狀態 + * POST /drain 手動觸發一批 drain(= 一個 cron tick;驗收/補跑用) + * POST /webhook Gitea push webhook 入口(delta 模式) + * POST /cursor/reset 游標歸零(重新全 drain 用) + */ + +import { CARD_TO_ENVELOPE_USERCODE } from './usercode'; + +export interface Env { + REPO: string; // Leo/notes + 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 + GRAPH_KEY: string; // leo + BATCH_SIZE: string; // "2" + GITEA_TOKEN: string; // secret:讀 repo + GITEA_WEBHOOK_SECRET?: string; // secret(選填):驗 webhook 簽章 +} + +const CURSOR_TYPE = 'ingest_cursor'; +const cursorPageName = (env: Env) => `cursor:km_wiki_ingest_drain:${env.REPO}`; + +// ── Gitea ──────────────────────────────────────────────────────────────────── +const giteaApi = (env: Env, path: string) => `https://git.uncle6.me/api/v1/repos/${env.REPO}${path}`; + +/** 列出 CARDS_ROOT 下所有卡片(git tree recursive,排除 00-INDEX/.gitkeep),回 [{path, sha}] 已排序。 */ +async function listCards(env: Env): Promise<{ path: string; sha: string }[]> { + const r = await fetch(giteaApi(env, `/git/trees/${encodeURIComponent(env.REF)}?recursive=true`), { + headers: { Authorization: `token ${env.GITEA_TOKEN}` }, + }); + if (!r.ok) throw new Error(`gitea tree ${r.status}: ${(await r.text()).slice(0, 200)}`); + const data = (await r.json()) as { tree?: { path: string; type: string; sha: string }[]; truncated?: boolean }; + const root = env.CARDS_ROOT.replace(/\/$/, '') + '/'; + const cards = (data.tree || []) + .filter((e) => e.type === 'blob' && e.path.startsWith(root) && e.path.endsWith('.md')) + .filter((e) => { + const base = e.path.split('/').pop() || ''; + return base !== '.gitkeep' && !base.startsWith('00-INDEX'); + }) + .map((e) => ({ path: e.path, sha: e.sha })) + .sort((a, b) => (a.path < b.path ? -1 : a.path > b.path ? 1 : 0)); + // truncated:5,083 檔規模 Gitea tree 可能截斷 → 記錄以便換分頁列法(現況 notes 卡數小,不截斷)。 + (cards as unknown as { truncated?: boolean }).truncated = data.truncated === true; + return cards; +} + +async function giteaRaw(env: Env, relPath: string): Promise { + const url = giteaApi(env, `/raw/${relPath.split('/').map(encodeURIComponent).join('/')}?ref=${encodeURIComponent(env.REF)}`); + const r = await fetch(url, { headers: { Authorization: `token ${env.GITEA_TOKEN}` } }); + if (!r.ok) throw new Error(`gitea raw ${relPath} ${r.status}`); + return r.text(); +} + +// ── card-processing 核心 ────────────────────────────────────────────────────── +async function parseCard(env: Env, md: string, relPath: string): Promise { + const r = await fetch(env.CODE_WORKER_URL, { + method: 'POST', + headers: { 'content-type': 'application/json' }, + body: JSON.stringify({ + code: CARD_TO_ENVELOPE_USERCODE, + input: { md, relPath, repo: env.REPO, opts: { budget: 40 } }, + limits: { timeout_ms: 3000, max_output_bytes: 4 * 1024 * 1024 }, + }), + }); + const out = (await r.json()) as { success: boolean; data?: any; error?: string }; + if (!out.success) throw new Error(`code 零件解析失敗 ${relPath}: ${out.error}`); + return out.data; // { entry, envelopes, meta, nodeCount, tripletCount } +} + +function metaHash(entry: any): string | undefined { + try { + return typeof entry.metadata_json === 'string' ? JSON.parse(entry.metadata_json).content_hash : undefined; + } catch { + return undefined; + } +} + +async function upsertEntry(env: Env, entry: any): Promise<{ action: string; id?: string }> { + 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 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}`, { + 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`, { + method: 'POST', + headers: { 'content-type': 'application/json' }, + body: JSON.stringify({ + entry_type: entry.entry_type, + content: entry.content, + page_name: pageName, + owner_id: env.OWNER, + tags_json: JSON.stringify(entry.tags || []), + source: entry.metadata.source, + metadata_json, + }), + }); + const out = (await r.json()) as { entry?: { id?: string } }; + return { action: 'create', id: out.entry?.id }; +} + +async function postEnvelopes(env: Env, envelopes: any[]): Promise { + const results = []; + 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)}`, { + method: 'POST', + headers: { 'content-type': 'application/json', 'X-Arcrun-API-Key': env.GRAPH_KEY }, + body: JSON.stringify(clean), + }); + const text = await r.text(); + let body: any; + try { body = JSON.parse(text); } catch { body = text; } + results.push({ status: r.status, uri: (envp as any).source?.uri, triplets: (envp as any).triplets?.length, body }); + } + return results; +} + +/** 一張卡的完整處理(冪等):fetch → parse → upsert entry → post envelopes。 */ +async function processCard(env: Env, relPath: string): Promise { + const md = await giteaRaw(env, relPath); + const plan = await parseCard(env, md, relPath); + const entry = await upsertEntry(env, plan.entry); + const envelopes = await postEnvelopes(env, plan.envelopes); + const wrote = entry.action !== 'skip' || envelopes.some((e) => e.body && e.body.skipped === false); + return { + relPath, + page_name: plan.entry.page_name, + content_hash: (plan.entry.metadata.content_hash || '').slice(0, 12), + entry: entry.action, + envelopes: envelopes.map((e) => ({ status: e.status, triplets: e.triplets, skipped: e.body?.skipped, ingested: e.body?.ingested })), + wrote, + }; +} + +// ── 游標(存 KBDB 一個 ingest_cursor entry;不 embed)───────────────────────── +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 e = ((await r.json()) as { entries?: any[] }).entries?.[0]; + if (e) { + try { return { id: e.id, state: JSON.parse(e.content) }; } catch { /* fallthrough */ } + return { id: e.id, state: { last_path: '', cycle: 0, processed_total: 0, updated_at: 0 } }; + } + return { state: { last_path: '', cycle: 0, processed_total: 0, updated_at: 0 } }; +} + +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}`, { + method: 'PATCH', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ content }), + }); + } else { + await fetch(`${env.KBDB_URL}/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 }) }), + }); + } +} + +/** cron / 手動觸發:drain 一小批。游標之後取 BATCH 張;到底回捲(continuous drain)。 */ +async function drainBatch(env: Env, batchSize: number): Promise { + const cards = await listCards(env); + const truncated = (cards as unknown as { truncated?: boolean }).truncated; + const cur = await getCursor(env); + let idx = cards.findIndex((c) => c.path > cur.state.last_path); + let wrapped = false; + if (idx < 0) { idx = 0; wrapped = true; } // 到底 → 回捲重掃(靠冪等,未改卡 cheap skip) + const batch = cards.slice(idx, idx + batchSize); + const results = []; + for (const c of batch) results.push(await processCard(env, c.path)); + const newLast = batch.length ? batch[batch.length - 1].path : cur.state.last_path; + cur.state = { + last_path: newLast, + cycle: cur.state.cycle + (wrapped ? 1 : 0), + processed_total: cur.state.processed_total + batch.length, + updated_at: Math.floor(Date.now() / 1000), + }; + await setCursor(env, cur); + return { + mode: 'cron-drain', total_cards: cards.length, truncated: !!truncated, wrapped, + batch: batch.map((c) => c.path), results, + wrote: results.filter((r) => r.wrote).length, skipped: results.filter((r) => !r.wrote).length, + cursor: cur.state, + }; +} + +// ── webhook(Gitea push → delta)─────────────────────────────────────────────── +async function verifyGiteaSig(env: Env, raw: string, sig: string | null): Promise { + if (!env.GITEA_WEBHOOK_SECRET) return true; // 未設 secret → 不驗(leo 自有 repo) + if (!sig) return false; + const key = await crypto.subtle.importKey('raw', new TextEncoder().encode(env.GITEA_WEBHOOK_SECRET), + { name: 'HMAC', hash: 'SHA-256' }, false, ['sign']); + const mac = await crypto.subtle.sign('HMAC', key, new TextEncoder().encode(raw)); + const hex = [...new Uint8Array(mac)].map((b) => b.toString(16).padStart(2, '0')).join(''); + return hex === sig; +} + +async function handleWebhook(env: Env, raw: string): Promise { + const payload = JSON.parse(raw) as { commits?: { added?: string[]; modified?: string[]; removed?: string[] }[] }; + const root = env.CARDS_ROOT.replace(/\/$/, '') + '/'; + const isCard = (p: string) => p.startsWith(root) && p.endsWith('.md') && !(p.split('/').pop() || '').startsWith('00-INDEX'); + const changed = new Set(); + const removed = new Set(); + for (const cm of payload.commits || []) { + for (const p of [...(cm.added || []), ...(cm.modified || [])]) if (isCard(p)) changed.add(p); + for (const p of cm.removed || []) if (isCard(p)) removed.add(p); + } + const results = []; + for (const p of changed) results.push(await processCard(env, p)); + return { + mode: 'webhook-delta', changed: [...changed], removed: [...removed], + note: removed.size ? 'removed 卡的清理未實作(graph 有 deprecate 機制,另議)' : undefined, + results, wrote: results.filter((r) => r.wrote).length, + }; +} + +// ── Worker 入口 ──────────────────────────────────────────────────────────────── +export default { + async scheduled(_event: ScheduledEvent, env: Env, ctx: ExecutionContext): Promise { + ctx.waitUntil(drainBatch(env, Number(env.BATCH_SIZE || '2')).then(() => {}).catch(() => {})); + }, + + async fetch(req: Request, env: Env, _ctx: ExecutionContext): Promise { + const url = new URL(req.url); + const json = (o: unknown, s = 200) => new Response(JSON.stringify(o, null, 2), { status: s, headers: { 'content-type': 'application/json' } }); + try { + if (req.method === 'GET' && url.pathname === '/') { + return json({ ok: true, service: 'km_wiki_ingest_drainer', repo: env.REPO, cards_root: env.CARDS_ROOT, batch: env.BATCH_SIZE }); + } + if (req.method === 'GET' && url.pathname === '/cursor') { + return json(await getCursor(env)); + } + if (req.method === 'POST' && url.pathname === '/cursor/reset') { + const cur = await getCursor(env); + cur.state = { last_path: '', cycle: 0, processed_total: 0, updated_at: Math.floor(Date.now() / 1000) }; + await setCursor(env, cur); + return json({ ok: true, cursor: cur.state }); + } + if (req.method === 'POST' && url.pathname === '/drain') { + const n = Number(url.searchParams.get('batch') || env.BATCH_SIZE || '2'); + return json(await drainBatch(env, n)); + } + if (req.method === 'POST' && url.pathname === '/webhook') { + const raw = await req.text(); + const ok = await verifyGiteaSig(env, raw, req.headers.get('X-Gitea-Signature')); + if (!ok) return json({ error: 'bad signature' }, 401); + // Gitea ping event(測試)→ 回 pong + if (req.headers.get('X-Gitea-Event') === 'ping') return json({ ok: true, pong: true }); + return json(await handleWebhook(env, raw)); + } + return json({ error: 'not found' }, 404); + } catch (e) { + return json({ error: e instanceof Error ? e.message : String(e) }, 500); + } + }, +}; diff --git a/registry/examples/km-wiki-ingest/drainer/src/usercode.ts b/registry/examples/km-wiki-ingest/drainer/src/usercode.ts new file mode 100644 index 0000000..2fb6367 --- /dev/null +++ b/registry/examples/km-wiki-ingest/drainer/src/usercode.ts @@ -0,0 +1,4 @@ +// 自動由 workflow.yaml 的 parse_card.code 抽出(單一真相源: lib/card-to-envelope.mjs)。 +// 這段 inline JS 餵給 arcrun-code 零件沙箱執行: 讀 input{md,relPath,repo,opts} -> 回 {entry,envelopes,meta}。 +// envelopes 已剝除 _-診斷鍵(graph strict schema 相容)。 +export const CARD_TO_ENVELOPE_USERCODE = "// km-wiki-ingest — 機械式卡片→(entry + triplet envelope) 轉換核心(無 LLM、純函式)\n// ---------------------------------------------------------------------------\n// 取代舊 `kbdb-ingest-plugin/scripts/ingest-cli.mjs` 的 raw→Haiku 路:\n// 舊路 = 讀裸筆記 → 呼叫 Haiku 萃 (s,p,o) → envelope(有 LLM、非決定性、耗 token)。\n// 新路 = 讀「已精耕卡片」(`system-dev/wiki/cards/**/*.md`)→ 直接解析卡片內既有的\n// `## 實體`(節點)、`## 關聯` 的 typed-edge(`A >> 關係 >> B`)與 `[[wikilink]]`\n// → entry + triplet envelope。純機械、決定性、零 token。\n//\n// 這支=通用 `code` 零件(Arcrun#10,sandbox inline JS)承載的解析邏輯本體。\n// workflow.yaml 的 parse_card 節點把本檔的 planCard 邏輯內聯進 code 零件的 config\n// (去 import/export、raw NUL 分隔符改 u0000 escape、改用 code 沙箱注入的 sha256);\n// 不再鑄 domain 零件 km_wiki_card_parse(Arcrun#10 裁定:一次性解析走通用逃生口)。\n// 本檔續留作「該內聯 JS 的權威來源 + 可單元測試的參考實作」(純函式、stdin→stdout JSON、無 fs/網路)。\n//\n// 對齊契約:kbdb-ingest-plugin/contracts/ingest-candidate.json(envelope 形狀 / 禁止欄位)。\n// 對齊頂層 SDD:卡片→entry(metadata.embed=true,走 base API)、wikilink→triplet(走 graph)。\n//\n// 鐵律:不碰儲存、不算向量、不建表。這支只「產出將寫入什麼」,實際 HTTP 由 workflow 打。\n\n// (import 移除:code 沙箱提供注入的 sha256 builtin)\n// --- CF subrequest 預算(防「Too many subrequests by single Worker invocation」,07_01 根因)---\n//\n// graph worker 處理一次 POST /triplets/ingest 時,對 base 的每次 fetch = 1 subrequest。\n// 精確拆帳(讀 kbdb-graph-plugin/src/actions/triplet-ingest.ts + triplet-crud.ts + templates.ts):\n// ingestEnvelope = ensurePluginTemplates(3) + listRecordsByTemplate(1)\n// + Σ triplet [ createTriplet → ensurePluginTemplates(3) + createRecord(1) = 4 ]\n// + persistNodes [ ensurePluginTemplates(3) + Σ node createRecord(1) ]\n// + Σ deprecated updateRecord(1)\n// ⟹ subreq(envelope) = 7 + 4*N_triplets + M_nodes + D_deprecated\n//\n// 07_01 實測炸點:N=11, M=10, D=0 → 7+44+10 = 61 > 50(CF 免費/bundled 上限)→ 炸半殘。\n//\n// 對策 = 「一卡一 tick、每 envelope 壓在預算下、超大檔以 source_uri anchor 分段」。\nconst SUBREQ_CEILING = 50; // CF 單次 Worker invocation subrequest 硬上限(bundled)\nconst SUBREQ_BUDGET = 40; // 我們的目標上限(留 10 給 D_deprecated 等變動)\n\n/** 精確估算「一個 envelope 打進 graph /triplets/ingest」會在 graph worker 內產生幾個 subrequest。 */\nfunction estimateEnvelopeSubrequests(nTriplets, mNodes, dDeprecated = 0) {\n return 7 + 4 * nTriplets + mNodes + dDeprecated;\n}\n\n// --- sha256(content_hash 冪等鍵)---\n// (sha256 移除:使用 code 沙箱注入的 curated builtin sha256)\n\n// --- frontmatter 解析(極簡 YAML:只吃我們卡片用到的 tags / gloss / pipeline_candidate)---\nfunction parseFrontmatter(md) {\n const m = md.match(/^---\\n([\\s\\S]*?)\\n---\\n?/);\n if (!m) return { data: {}, body: md };\n const body = md.slice(m[0].length);\n const data = {};\n for (const line of m[1].split('\\n')) {\n const kv = line.match(/^([A-Za-z_][\\w-]*):\\s*(.*)$/);\n if (!kv) continue;\n const key = kv[1];\n let val = kv[2].trim();\n if (val.startsWith('[') && val.endsWith(']')) {\n // inline list: [a, b, c]\n data[key] = val.slice(1, -1).split(',').map((s) => s.trim()).filter(Boolean);\n } else if (val === 'true' || val === 'false') {\n data[key] = val === 'true';\n } else {\n data[key] = val;\n }\n }\n return { data, body };\n}\n\n// --- 取某個 `## 標題` / `### 標題` 區塊的內文(到下一個同級或更高級標題為止)---\nfunction sectionBody(md, heading) {\n // heading 例:'## 實體'、'### 內文知識關係'\n const level = heading.match(/^#+/)[0].length;\n const lines = md.split('\\n');\n const out = [];\n let inSec = false;\n for (const line of lines) {\n const h = line.match(/^(#+)\\s+(.*)$/);\n if (h) {\n const thisLevel = h[1].length;\n if (inSec) {\n // 遇到同級或更高級標題 → 區塊結束\n if (thisLevel <= level) break;\n }\n // 標題文字「開頭相符」即算命中(容忍標題後帶括號補述)\n if (!inSec && thisLevel === level && line.replace(/^#+\\s+/, '').startsWith(heading.replace(/^#+\\s+/, ''))) {\n inSec = true;\n continue;\n }\n }\n if (inSec) out.push(line);\n }\n return out.join('\\n');\n}\n\n// --- 實體行解析:`- **正規名**(別名1/別名2)— 描述`(別名、描述皆選填)---\nfunction parseEntities(md) {\n const sec = sectionBody(md, '## 實體');\n const entities = [];\n for (const raw of sec.split('\\n')) {\n const line = raw.trim();\n if (!line.startsWith('- ')) continue;\n if (line.startsWith('- >') || line.startsWith('> ')) continue; // 跳過引言說明行\n const m = line.match(/^- \\*\\*(.+?)\\*\\*(?:((.+?)))?\\s*(?:[—–\\-]\\s*(.*))?$/);\n if (!m) continue;\n const name = m[1].trim();\n if (!name) continue;\n const aliases = m[2]\n ? m[2].split(/[//、,]/).map((s) => s.trim()).filter((s) => s && s !== name)\n : [];\n const gloss = (m[3] || '').trim();\n entities.push({ name, aliases, gloss });\n }\n return entities;\n}\n\n// --- typed-edge 行解析:`A >> 謂詞 >> B`(端點可為裸實體名或 [[wikilink]])---\nfunction parseTypedEdges(sectionText) {\n const edges = [];\n for (const raw of (sectionText || '').split('\\n')) {\n const line = raw.trim();\n if (!line.startsWith('- ')) continue;\n const body = line.slice(2).trim();\n if (body.startsWith('(') || body.startsWith('(')) continue; // 「(暫無…)」占位行\n const parts = body.split('>>');\n if (parts.length !== 3) continue;\n const subject = stripWikilink(parts[0].trim());\n const predicate = parts[1].trim();\n const object = stripWikilink(parts[2].trim());\n if (!subject || !predicate || !object) continue;\n edges.push({ subject, predicate, object });\n }\n return edges;\n}\n\n// [[notes/00-INDEX]] → notes/00-INDEX ;純字串則原樣回。\nfunction stripWikilink(s) {\n const m = s.match(/^\\[\\[(.+?)\\]\\]$/);\n return m ? m[1].trim() : s;\n}\n\n// --- 抽所有 inline [[wikilink]](含 header 的 ← [[notes/00-INDEX]] 與內文)---\nfunction extractInlineWikilinks(md) {\n const out = [];\n const re = /\\[\\[(.+?)\\]\\]/g;\n let m;\n while ((m = re.exec(md)) !== null) out.push(m[1].trim());\n return out;\n}\n\n// --- 卡片 canonical id:以檔名(去副檔名)為準,對齊 `## 卡片關係` 用的 [[基名]] 慣例 ---\nfunction cardCanonical(relPath) {\n const base = relPath.split('/').pop().replace(/\\.md$/, '');\n return base;\n}\n\n/**\n * 解析一張卡片 → { entry, nodes, triplets, meta }(尚未分段的原始產物)。\n * relPath:卡片相對 repo 根路徑(如 system-dev/wiki/cards/notes/Xxx.md)。\n * repo:如 'Leo/notes'。\n */\nfunction parseCard(md, relPath, repo = 'Leo/notes') {\n const { data: fm } = parseFrontmatter(md);\n const canonical = cardCanonical(relPath);\n const titleMatch = md.match(/^#\\s+(.+)$/m);\n const title = titleMatch ? titleMatch[1].trim() : canonical;\n\n // 1) 節點:## 實體 的正規名 + 別名 + gloss。\n const entities = parseEntities(md);\n\n // 2) 邊:內文知識關係(實體↔實體)+ 卡片關係(卡↔卡)+ inline wikilink(卡→卡 導覽/引用)。\n const intraEdges = parseTypedEdges(sectionBody(md, '### 內文知識關係'))\n .map((e) => ({ ...e, confidence: 1.0 }));\n const cardEdges = parseTypedEdges(sectionBody(md, '### 卡片關係'))\n .map((e) => ({ ...e, confidence: 1.0 }));\n\n // inline wikilink(← [[notes/00-INDEX]] 等)→ 卡→卡「連結至」邊,去重、排除自環與已被 typed 邊覆蓋者。\n const typedPairs = new Set(\n [...cardEdges].map((e) => `${e.subject}\\u0000${e.object}`),\n );\n const seenRef = new Set();\n const refEdges = [];\n for (const target of extractInlineWikilinks(md)) {\n const t = stripWikilink(target);\n if (t === canonical || t === title) continue; // 自環\n if (typedPairs.has(`${canonical}\\u0000${t}`)) continue; // 已有明確謂詞邊\n const key = `${canonical}\\u0000${t}`;\n if (seenRef.has(key)) continue;\n seenRef.add(key);\n refEdges.push({ subject: canonical, predicate: '連結至', object: t, confidence: 0.5 });\n }\n\n const triplets = [...intraEdges, ...cardEdges, ...refEdges];\n\n // 3) 節點清單:卡片本身(canonical,帶 frontmatter gloss)+ 內文實體。\n // 卡對卡邊指到的「別張卡」不在此補 node —— 那張卡自己被 ingest 時會補自己的 node。\n const nodes = [];\n const seenNode = new Set();\n const pushNode = (n) => {\n const k = n.name.toLowerCase();\n if (!n.name || seenNode.has(k)) return;\n seenNode.add(k);\n nodes.push(n);\n };\n pushNode({ name: canonical, gloss: fm.gloss || '', aliases: title && title !== canonical ? [title] : [] });\n for (const e of entities) pushNode({ name: e.name, gloss: e.gloss, aliases: e.aliases });\n\n return {\n entry: {\n // base POST /entries(或 kbdb_upsert_block)用。metadata.embed=true → 語意可搜。\n page_name: `wikicard:${repo}/${canonical}`, // idempotency key(穩定)\n entry_type: 'wiki_card',\n content: md, // 卡片全文逐字(embed 對象)\n tags: Array.isArray(fm.tags) ? fm.tags : [],\n metadata: {\n embed: true, // ★ base embed 模組讀此旗標\n source: `gitea:${repo}@${relPath}`,\n content_hash: sha256(md),\n kind: 'wiki_card',\n repo,\n canonical,\n pipeline_candidate: fm.pipeline_candidate === true,\n },\n },\n nodes,\n triplets,\n meta: { canonical, title, relPath, repo, contentHash: sha256(md) },\n };\n}\n\n/**\n * 把一張卡片的 (nodes, triplets) 打包成「一個或多個」ingest envelope,\n * 使每個 envelope 打進 graph 後的 subrequest 都 ≤ SUBREQ_BUDGET。\n *\n * 分段規則(對應頂層 SDD R4 / issue #8 第4點):\n * - 單 envelope 夠塞(7+4N+M ≤ budget)→ 不分段,uri = 基 uri(無 anchor)。\n * - 需分段 → 每段 uri = `<基uri>#seg{NN}`、anchor = `seg{NN}`。\n * 每段是「獨立 source_uri」→ 各自獨立冪等,繞開 graph 的 per-source content_hash 整包 skip\n * (否則同 uri 第 2 段起會被 line 65 的 content_hash 命中而整包跳過)。\n * - 節點只放進「第一個引用到它的段」,跨段不重送(避免 graph persistNodes 重建 entity record)。\n */\nfunction planEnvelopes(parsed, opts = {}) {\n const budget = opts.budget ?? SUBREQ_BUDGET;\n const repo = parsed.meta.repo;\n const relPath = parsed.meta.relPath;\n const baseUri = `gitea:${repo}@${relPath}`;\n const contentHash = parsed.meta.contentHash;\n const commit = opts.commit;\n\n const extractor = {\n model: opts.extractorModel ?? 'mechanical/km-wiki-card-parse@1',\n tier: 'deep', // 人工精耕卡=deep(決定性、非淺萃)\n extracted_at: Math.floor(Date.now() / 1000),\n };\n\n const nodeByName = new Map(parsed.nodes.map((n) => [n.name, n]));\n\n // 貪婪打包:逐條 triplet 累進,段成本 = 7 + 4*(段內邊數) + (段內首見節點數)。\n const segments = [];\n let cur = null;\n const startSeg = () => {\n cur = { triplets: [], nodeNames: new Set() };\n segments.push(cur);\n };\n const segCost = (seg, extraEdges = 0, extraNodes = 0) =>\n estimateEnvelopeSubrequests(seg.triplets.length + extraEdges, seg.nodeNames.size + extraNodes);\n\n startSeg();\n for (const t of parsed.triplets) {\n // 這條邊會新引入哪些節點(subject/object 命中 nodeByName 且本段尚未收)\n const cand = [t.subject, t.object].filter(\n (nm) => nodeByName.has(nm) && !cur.nodeNames.has(nm) && !anySegHas(segments, cur, nm),\n );\n // 放得下?(含新增這條邊 + 新引入節點)\n if (cur.triplets.length > 0 && segCost(cur, 1, cand.length) > budget) {\n startSeg();\n }\n cur.triplets.push(t);\n for (const nm of [t.subject, t.object]) {\n if (nodeByName.has(nm) && !anySegHas(segments, null, nm)) cur.nodeNames.add(nm);\n }\n }\n\n const multi = segments.length > 1;\n const envelopes = segments.map((seg, i) => {\n const anchor = multi ? `seg${String(i + 1).padStart(2, '0')}` : undefined;\n const uri = multi ? `${baseUri}#${anchor}` : baseUri;\n const nodes = [...seg.nodeNames].map((nm) => {\n const n = nodeByName.get(nm);\n const out = { name: n.name };\n if (n.gloss) out.gloss = n.gloss;\n if (n.aliases && n.aliases.length) out.aliases = n.aliases;\n out.embed = true;\n return out;\n });\n const source = { uri, content_hash: contentHash };\n if (anchor) source.anchor = anchor;\n if (commit) source.commit = commit;\n return {\n source,\n extractor,\n nodes,\n triplets: seg.triplets.map((t) => ({\n subject: t.subject,\n predicate: t.predicate,\n object: t.object,\n confidence: t.confidence ?? 1.0,\n })),\n _estSubrequests: estimateEnvelopeSubrequests(seg.triplets.length, seg.nodeNames.size),\n };\n });\n\n // triplets≥1 是契約硬性;無邊的卡不產 envelope(仍會建 entry)。\n return envelopes.filter((e) => e.triplets.length >= 1);\n}\n\nfunction anySegHas(segments, exclude, name) {\n for (const s of segments) {\n if (s === exclude) continue;\n if (s.nodeNames.has(name)) return true;\n }\n return false;\n}\n\n/** 一張卡片 → 完整 ingest 計畫(entry + envelopes)。planCard = parseCard + planEnvelopes。 */\nfunction planCard(md, relPath, repo = 'Leo/notes', opts = {}) {\n const parsed = parseCard(md, relPath, repo);\n const envelopes = planEnvelopes(parsed, opts);\n return { entry: parsed.entry, envelopes, meta: parsed.meta, nodeCount: parsed.nodes.length, tripletCount: parsed.triplets.length };\n}\n\n// graph /triplets/ingest 是 strict schema,拒絕未知鍵。planEnvelopes 於 envelope 上掛的\n// 診斷鍵 _estSubrequests 不在 ingest-candidate 契約內 → post 前剝除所有 _-前綴鍵,\n// 讓 post_one_envelope 的 body_json={{envelope}} 為契約乾淨 payload。\n// (leo21c 實測:不剝除會回 422 unrecognized_keys \"_estSubrequests\"。)\nconst __plan = planCard(input.md, input.relPath, input.repo, input.opts || {});\n__plan.envelopes = __plan.envelopes.map(function (e) {\n const clean = {};\n for (const k in e) { if (k.charAt(0) !== '_') clean[k] = e[k]; }\n return clean;\n});\nreturn __plan;\n"; diff --git a/registry/examples/km-wiki-ingest/drainer/tsconfig.json b/registry/examples/km-wiki-ingest/drainer/tsconfig.json new file mode 100644 index 0000000..e71bb9a --- /dev/null +++ b/registry/examples/km-wiki-ingest/drainer/tsconfig.json @@ -0,0 +1,14 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "ES2022", + "moduleResolution": "Bundler", + "lib": ["ES2022"], + "types": ["@cloudflare/workers-types"], + "strict": true, + "skipLibCheck": true, + "noEmit": true, + "resolveJsonModule": true + }, + "include": ["src/**/*.ts"] +} diff --git a/registry/examples/km-wiki-ingest/drainer/wrangler.toml b/registry/examples/km-wiki-ingest/drainer/wrangler.toml new file mode 100644 index 0000000..658b1e3 --- /dev/null +++ b/registry/examples/km-wiki-ingest/drainer/wrangler.toml @@ -0,0 +1,22 @@ +name = "arcrun-km-wiki-drainer" +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)。 +# [triggers] +# crons = ["*/5 * * * *"] + +[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(選)