diff --git a/.agents/specs/arcrun/arcrun.md b/.agents/specs/arcrun/arcrun.md index 66aaf08..149b355 100644 --- a/.agents/specs/arcrun/arcrun.md +++ b/.agents/specs/arcrun/arcrun.md @@ -69,8 +69,205 @@ P0 全部清除才啟動封測。 | 6 | acr creds push 實測 | ✅ 完成 | /register 回傳 encryption_key,acr init 自動存入 config(CLI 1.0.9)| | 7 | Google Sheets 真實寫入 | ⚠️ 部分驗證 | credential 注入已驗證;實際 Sheets 寫入需真實 OAuth token | | 8 | 第三方服務認證 recipe | ✅ 完成 | 20 個服務(Notion/Slack/GitHub/OpenAI 等),CLI 1.1.0 | +| **9** | **cypher-executor outbound HTTP fetch 全失效** | ✅ **已解決 2026-05-13**(CF 同 zone 自循環死鎖,改走 workers.dev)| 詳見下方專段 | +| **10** | **multi-node chain context propagation 漏失** | ✅ **已解決 2026-05-13**(ON_SUCCESS/ON_FAIL/IF/ON_CLICK 沒 spread baseCtx)| 詳見下方專段 | -**目前狀況**:P0 全部完成。Google Sheets 實際寫入可由封測者用真實 token 驗證,不阻塞啟動。 +**目前狀況**:P0 全部解決。 +- #9 修復方式:component worker URL 從 `*.arcrun.dev`(同 cypher zone)改走 `arcrun-{name}.{WORKER_SUBDOMAIN}.workers.dev`(避開同 zone 自循環) +- #10 修復方式:4 個 edge type 補 `{...baseCtx, ...result}`,跟 PIPE/FOREACH 一致 + +兩個 P0 解完 mira 7 節點 workflow 端對端通(含真 Claude 16 秒呼叫)。 + +--- + +### ✅ P0 #9(2026-05-13 已解決):cypher-executor outbound fetch 全失效 + +**完整事件報告(含誤判路徑)**:[docs/incidents/2026-05-13-cypher-outbound-522.md](../../../docs/incidents/2026-05-13-cypher-outbound-522.md) + +**修復方式**:cypher-executor fetch component worker 從 `*.arcrun.dev`(同 zone)改走 `arcrun-{name}.{WORKER_SUBDOMAIN}.workers.dev`。對外 `cypher.arcrun.dev` 不變,用戶 0 感知。 + +**改動檔案**(2026-05-13): +- `cypher-executor/src/lib/component-loader.ts`:`wasmWorkerUrl(canonicalId, subdomain)` 簽名加 subdomain 參數 + URL pattern 改 workers.dev +- `cypher-executor/src/actions/auth-dispatcher.ts`:同步新簽名 +- `cypher-executor/src/types.ts`:`Bindings` 加 `WORKER_SUBDOMAIN: string` +- `cypher-executor/wrangler.toml`:`[vars]` 加 `WORKER_SUBDOMAIN = "uncle6-me"` +- 5 個 component worker 在 dashboard 啟用 workers.dev URL(kbdb-get / kbdb-ingest / kbdb-create-block / kbdb-patch-block / claude-api,**未來新 component 也都要開**) + +**驗證**:cypher-executor → kbdb-get / claude-api 從 522 → 200。mira `acr run wiki_synthesis` 5 節點 workflow 跑通前 3 節點(kbdb_get chain)。 + +**Self-hosted fork 注意**:必須改 `wrangler.toml [vars] WORKER_SUBDOMAIN` 為自己的 CF 帳號 subdomain,並把所有 component worker 在 dashboard 啟用 workers.dev URL。 + +--- + +### ✅ P0 #10(2026-05-13 已解決):multi-node chain context propagation 漏失 + +**現象**:cypher binding workflow 從第 2 個節點開始,原始 input context(top-level `api_key` / `mira_token` 等)丟失,下游節點 `{{api_key}}` 模板原文未替換傳給零件 → 401 Unauthorized 或類似錯。 + +**測試重現**: + +```yaml +flow: + - "input >> ON_SUCCESS >> n1" + - "n1 >> ON_SUCCESS >> n2" +config: + n1: { component: kbdb_get, api_key: "{{api_key}}", block_id: "{{b1}}" } + n2: { component: kbdb_get, api_key: "{{api_key}}", block_id: "{{b2}}" } +context: { api_key: "ak_xxx", b1: "...", b2: "..." } +``` + +n1 收到 ctx 含 `api_key / b1 / b2` ✓ → 跑通。 +n2 收到的 ctx 只有 `n1.output spread`(blocks/count/success/block_id),**`api_key / b1 / b2` 不見**,`{{api_key}}` 原文傳到零件回 401。 + +**根因**:`graph-executor.ts` 在 PIPE / FOREACH 邊類型已修「baseCtx ∪ result」,但 **ON_SUCCESS / ON_FAIL / IF / ON_CLICK 四個 edge type 沒套同模式**,直接把 `result` 當下游 ctx 傳,丟掉原始 context。 + +**修法**(`cypher-executor/src/graph-executor.ts` line 407 / 415 / 423 / 472): + +```typescript +// 改前 +result = await this.executeNode(nextNode, graph, result, ...); + +// 改後(同 PIPE/FOREACH 模式) +const baseCtx = (typeof context === 'object' && context !== null) ? context as Record : {}; +const baseResult = (typeof result === 'object' && result !== null) ? result as Record : {}; +const mergedCtx = { ...baseCtx, ...baseResult }; +result = await this.executeNode(nextNode, graph, mergedCtx, ...); +``` + +**驗證**:mira `acr run wiki_synthesis` 7 節點 workflow 端對端跑通(16 秒,含真 Claude 呼叫)。每個節點都拿到正確 `api_key` 不再 401。 + +**歷史脈絡**:類似問題 2026-05-07 commit e8fca33 在 FOREACH edge 已修一次("FOREACH preserves outer context"),但當時沒同步處理另外 4 個 edge type。本次補完。 + +--- + +### ✅ P0 #10 補完三個衍生問題(2026-05-13 晚 ~ 2026-05-14) + +P0 #10 修完後 mira 嘗試做 wiki 多段結構,又踩出三個 cypher binding 設計缺陷。**都是同一天解掉**。 + +#### A. interpolateData() 不遞迴 nested object + +**現象**:`set` / `kbdb_create_block` 的 `values: { text: "{{classify.data.text}}" }`、`tags_json: ["facet:{{paragraph.facet}}"]` 等 nested config 內的 `{{x}}` 不被替換,原文傳給零件。 + +**根因**:`interpolateData()` 只 iterate top-level,對非 string 值(object / array)直接 pass-through 不下沉。 + +**修法**:拆 `interpolateString` + `interpolateValue`(遞迴 object / array),`interpolateData` 改 call `interpolateValue`。 + +**測試**:`set values: { text: "hello {{name}}", arr: ["item {{name}}"] }` 帶 `name=world` → 全展開。 + +#### B. ctx 沒存上游 output 的 node id namespace + +**現象**:`{{classify.data.text}}` 找不到上游 classify 的 output;只能用 `{{data.text}}`(直接 spread 取),但會被下個節點覆蓋,多節點 chain 用不了。 + +**根因**:`propagateCtx` 只把上游 result spread 進 ctx,沒額外存 `[node.id]: result`。 + +**修法**:`propagateCtx` 改回傳 `{ ...baseCtx, ...baseResult, [upstreamNodeId]: upstreamResult }`。讓下游能用 `{{node_id.data.text}}` 從 namespace 取,永不被覆蓋。 + +**測試**:5 節點 chain 用 `{{load_schema.blocks.0.content}}` / `{{classify.data.text}}` 全展開。 + +#### C. FOREACH 找 iterable 只看 result,不看 ctx + 不看 nested + +**現象**:mira wiki_synthesis 雙重 FOREACH(外層 `對每個 paragraph`、內層 `對每個 triplet`),外層 OK,內層跑 0 次。 + +**根因 (C1)**:`getIterableFromContext(result, key)` 只看當前節點 output。`result` 是 `create_paragraph` output(`{data, success}`),不含 paragraphs。但 `paragraphs` 早就在 ctx 從 classify spread 來。 + +**根因 (C2)**:當外層 FOREACH 把 `paragraph` item 注入 ctx,內層 FOREACH 要找 `paragraph.triplets`。`getIterableFromContext` 只看 top-level,看不到 `paragraph` 物件裡的 `triplets`。 + +**修法**: +- (C1) FOREACH `result` 找不到 iterable → fallback 找 `context` +- (C2) `getIterableFromContext` 加一輪「掃 ctx 內每個 object 找 nested key」 + +**測試**:mira wiki_synthesis 3 層樹(wiki-page → paragraphs → triplets)端對端跑通,KBDB 內驗證 `物理 AI` wiki 有 2 段 paragraph + 4 個 triplet,parent_id 正確接到對應 paragraph。 + +#### Edge type 一致化 + +抽 `propagateCtx(context, result, upstreamNodeId)` helper,5 個 edge type(PIPE / ON_SUCCESS / ON_FAIL / IF / ON_CLICK / FOREACH)全部用同一 function 組下游 ctx。**未來新 edge type 必須用這 helper**,避免再漏。 + +#### CLI validator 同步 + +`cli/src/lib/yaml-parser.ts` validateRelations 加 regex 支援 `對每個 X` / `FOREACH X` 迭代器命名(之前 validator 字串完全比對擋住,但 graph-builder 執行端早已支援)。 + +--- + +### 三-A、P1 待改進(不擋封測,但 mira 已踩到) + +#### P1 #1:workflow 缺 IF/branch 能力(2026-05-14 mira 7B.3f 提出) + +**現象**:mira 想做「找有則 PATCH 沒則 CREATE」(index-entry upsert),arcrun 目前只有 `ON_SUCCESS` + `對每個 X`(FOREACH)+ 已存在但壞掉的 `if_control`(見已知限制 #1),沒有 `>> ON_TRUE >>` / `>> ON_FALSE >>` 條件路由。 + +**短期 workaround**(已採用,2026-05-14):建 `kbdb_upsert_block` 零件,把分支邏輯封進零件內部(GET by page_name → 找到 PATCH 沒找到 POST)。caller 看到的是單純的 upsert 介面。 + +**長期解**:升 `if_control` false branch 路由 / 加 `>> ON_TRUE >>` edge type,讓 workflow 層可表達分支。對未來所有「找則改否則建」/「條件分流」場景都會撞到,不只 mira。 + +**位置**:cypher-executor/src/graph-executor.ts edge type 處理(5 個 edge type 抽出 `propagateCtx` 後新增 IF 應該不難)+ cli/src/lib/yaml-parser.ts validator。 + +--- + +### 三-B、新零件加入紀錄 + +| 日期 | 零件 | 動機 | 對應 SDD | +|---|---|---|---| +| 2026-05-14 | `kbdb_upsert_block` | mira 7B.3f index-entry per-entity upsert,繞過 workflow 缺 IF/branch 能力(P1 #1)。內部 GET by page_name → 找到 PATCH 沒找到 POST。page_name 當 idempotency key。 | polaris/mira/.agents/specs/mira-app/design.md §3.5.12.4.1 | + +--- + +### 原 P0 #9 調查紀錄(保留作歷史參考) + +**現象**:cypher-executor 的 `makeHttpRunner` (`cypher-executor/src/lib/component-loader.ts:142`) 對任何 outbound URL fetch 都回 CF **522 (origin timeout, ~1000ms)**。 + +**測試矩陣**(mira repo `polaris/mira/arcrun/wiki_synthesis.yaml` 端對端壓測時發現): + +| 路徑 | 結果 | 證明 | +|---|---|---| +| 本機 curl → kbdb-get.arcrun.dev | 200 (22ms) | KBDB worker 本身健康 | +| cypher-executor → kbdb-get.arcrun.dev (HTTP) | **522** (1002ms) | outbound HTTP fetch 壞 | +| cypher-executor → claude-api.arcrun.dev (HTTP) | **522** | 同 zone 也壞 | +| cypher-executor → httpbin.org (外部) | **522** | 不只是 same-account loop | +| cypher-executor → string_ops (Service Binding) | 200 ✅ | SVC_* 路徑正常 | +| acr run hello (built-in via SB) | ✅ | hello.yaml 仍跑得通 | + +**衍生小 bug**:`set` 零件 input schema 變了(要 `assignments` 陣列或 `values` 物件,不是 `value`)。tests/arcrun-test/hello.yaml 跑 string_ops 沒踩到。 + +**影響範圍(封測啟動阻擋)**: + +- 任何用戶 workflow 含 outbound HTTP 都壞: + - 用戶 `acr recipe push` 後 trigger → 打外部 API 全 522(推翻 P0 #2 4/18 紀錄) + - 用戶要存資料進 arcrun 內建 KBDB → 522(mira 7B.3c 卡這裡) + - 任何 auth primitive 走獨立 Worker URL 路徑 → 522 + - Google Sheets 寫入 → 522(推翻 P0 #7「待真實 OAuth 驗證」評估,根本還沒到 OAuth 步驟就壞) +- 只有「全內建邏輯零件 + 純 service binding」workflow 還能跑 + +**根因(2026-05-13 確認):5/8-5/9 9 次 manual `wrangler deploy` 把含 WIP bug 的 cypher-executor 推上 prod** + +調查路徑(按時序): + +1. 一開始懷疑 free tier CPU cap(10ms / invocation)→ 對照測試「5 節點 SB chain」跑了 2.2 秒卻通過,**推翻** +2. 換懷疑 CF zone 規則 / Bot Fight Mode → dashboard bindings 乾淨無攔截,**推翻** +3. 用戶補繳費恢復 Workers Paid → 重測仍全 522,**徹底排除付費假設** +4. 看 dashboard Version History:5/8 4 次 + 5/9 5 次 = **9 次 manual `wrangler deploy by uncle6.me`** +5. 對照 GitHub Actions:4/24 後完全沒 deploy(最後是 commit e222116 `fix(wasi-shim)`) +6. 對照 git:本機 main **領先 origin/main 3 commits 未 push**,含: + - `497f92a feat(arcrun): recipe system + resumable workflow + component registry canon` + - `e8fca33 feat(cypher): 3-node wiki workflow end-to-end (FOREACH + nested interp + unified parsing)` + - `519423c feat(arcrun): mira wiki page with tag filter + accumulated WIP`(自描含 `cypher-executor: auth-dispatcher / wasi-shim adjustments (WIP)`) + +**結論**:那 9 次 manual deploy 把上面 3 個 unpushed commit 的 cypher-executor 改動推上 prod,其中至少一個改動破壞了 outbound fetch(最可能是 519423c WIP 內的 wasi-shim / auth-dispatcher 改動)。GitHub Actions 因為沒 push 沒跑,CI 沒 catch,4/18-4/24 那段 SDD「驗證通過」的紀錄是 truth,現在 prod 是壞的版本。 + +**為何 SB 路徑沒事 / HTTP 路徑全死**:SB 走 cypher-executor 內部 service binding API(`env.SVC_X.fetch()`),不經過 outbound HTTP code path。HTTP 路徑走 `makeHttpRunner` (component-loader.ts:142) 的 `fetch(url, ...)`,這條路被 WIP code 弄壞。具體壞在哪要 diff 那 3 個 commit 的 cypher-executor 改動才知道。 + +**驗證 wrangler tail 證據**:trigger 任何 outbound HTTP 的 graph,cypher-executor 自己 `wallTime: 497ms, cpuTime: 2ms, outcome: ok`、無 logs、無 exceptions。代表 cypher-executor 把「fetch 失敗的 522 response」當作 component 正常輸出包回 client,自己沒撞任何錯。 + +**解法(三選一)**: + +- **A. Rollback prod 到 4/24 的 e222116** — CF dashboard → arcrun-cypher-executor → Deployments → 找 4/24 那筆 → Rollback。5 分鐘恢復 outbound fetch,丟失 wiki workflow / recipe / resumable 等 cypher 端 WIP 改動(但前端、registry components、KBDB blocks 都不丟,因為它們是別的 worker / 別的儲存)。**richblack 操作。** +- **B. Diff 3 個 unpushed commit 找出壞掉的改動修掉** — 不丟功能,但要動 src code 走 SDD 協議,30min - 數小時。 +- **C. 架構切換**(mira 老闆 2026-05-13 提的):sub-workflow 自殺交棒模式,cypher-executor 不再做集中 graph executor。從根本繞開「cypher-executor 一個 invocation 跑長 graph」這條脆弱路徑。一勞永逸但是大改。 + +**衍生小 bug 仍要修**(跟付費無關):`set` 零件 input schema 變了(要 `assignments` 陣列或 `values` 物件,不是 `value`)。要嘛 update set 零件 contract 容錯,要嘛文件化新 schema。 + +**為什麼這直接擋封測**: + +封測場景 Step 6「網頁 POST → 結果存 Google Sheets」走 google_sheets 零件 (HTTP outbound to googleapis.com)。如果 cypher-executor outbound 全壞,**封測者跑任何含外部 API 的 workflow 都會 522**,不是「Google Sheets 實際寫入未驗證」級別的小事。 + +**也直接擋 mira**:[polaris/mira/.agents/specs/mira-app/tasks.md] 7B.3c-f(wiki 合成 workflow)卡這裡。 --- @@ -84,7 +281,7 @@ P0 全部清除才啟動封測。 ## 五、已知限制(封測期間不修) -1. `if_control` false branch 不路由(條件 false 時後續節點不執行) +1. `if_control` false branch 不路由(條件 false 時後續節點不執行)→ 升級計畫見 P1 #1,2026-05-14 mira 用 `kbdb_upsert_block` workaround 2. 多節點 context 不自動解包(上游輸出 flat merge,下游需從 `data.result` 取值) 3. 用戶自製邏輯零件(Phase 5)封測後才實作 diff --git a/.component-builds/kbdb_upsert_block/package.json b/.component-builds/kbdb_upsert_block/package.json new file mode 100644 index 0000000..45e9118 --- /dev/null +++ b/.component-builds/kbdb_upsert_block/package.json @@ -0,0 +1,14 @@ +{ + "name": "arcrun-kbdb-upsert-block", + "version": "1.0.0", + "private": true, + "type": "module", + "dependencies": { + "hono": "^4.7.0" + }, + "devDependencies": { + "@cloudflare/workers-types": "^4.20250408.0", + "typescript": "^5.4.0", + "wrangler": "^4.0.0" + } +} diff --git a/.component-builds/kbdb_upsert_block/pnpm-lock.yaml b/.component-builds/kbdb_upsert_block/pnpm-lock.yaml new file mode 100644 index 0000000..b8ea2c1 --- /dev/null +++ b/.component-builds/kbdb_upsert_block/pnpm-lock.yaml @@ -0,0 +1,898 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + dependencies: + hono: + specifier: ^4.7.0 + version: 4.12.18 + devDependencies: + '@cloudflare/workers-types': + specifier: ^4.20250408.0 + version: 4.20260511.1 + typescript: + specifier: ^5.4.0 + version: 5.9.3 + wrangler: + specifier: ^4.0.0 + version: 4.90.1(@cloudflare/workers-types@4.20260511.1) + +packages: + + '@cloudflare/kv-asset-handler@0.5.0': + resolution: {integrity: sha512-jxQYkj8dSIzc0cD6cMMNdOc1UVjqSqu8BZdor5s8cGjW2I8BjODt/kWPVdY+u9zj3ms75Q5qaZgnxUad83+eAg==} + engines: {node: '>=22.0.0'} + + '@cloudflare/unenv-preset@2.16.1': + resolution: {integrity: sha512-ECxObrMfyTl5bhQf/lZCXwo5G6xX9IAUo+nDMKK4SZ8m4Jvvxp52vilxyySSWh2YTZz8+HQ07qGH/2rEom1vDw==} + peerDependencies: + unenv: 2.0.0-rc.24 + workerd: '>1.20260305.0 <2.0.0-0' + peerDependenciesMeta: + workerd: + optional: true + + '@cloudflare/workerd-darwin-64@1.20260508.1': + resolution: {integrity: sha512-IT3r6VgiSwIesL4AJbxjgxvIxwWZqM7BKkhYAzOKHl4GF2M0TxeOahUIXd+CYXVZgHX8ceEg+MXbEehPelJyNg==} + engines: {node: '>=16'} + cpu: [x64] + os: [darwin] + + '@cloudflare/workerd-darwin-arm64@1.20260508.1': + resolution: {integrity: sha512-JTVsisOJPcNKw0qovPjqyBWYahfdhUh7/9NICiG5wxaEQ45PYKdoqNq0hOAAIqvqoxsKZBvTgcPTJREPqk7avA==} + engines: {node: '>=16'} + cpu: [arm64] + os: [darwin] + + '@cloudflare/workerd-linux-64@1.20260508.1': + resolution: {integrity: sha512-zO38pCc27YlsZiPYcaZnosy0/t7abXrRU3VEO1oKfUvnaCpHgphDG+VsrmHL+kntda6hrtNwg2jLeMAqqIjnjw==} + engines: {node: '>=16'} + cpu: [x64] + os: [linux] + + '@cloudflare/workerd-linux-arm64@1.20260508.1': + resolution: {integrity: sha512-XhJa780Ia6MNIrtxn/ruZHS79b9pu5EKPfRNReaUqxy8erPT2fs93axMfFoS9kIkcaRRj/1TOUKcTeAMoywY7w==} + engines: {node: '>=16'} + cpu: [arm64] + os: [linux] + + '@cloudflare/workerd-windows-64@1.20260508.1': + resolution: {integrity: sha512-QdDOK3B/Ul1s3QmIwDrFyx9230to6LsNmWcVR8w+TYjNZuRPzqQBgusp78LO7MlqCoEl9dvIcN00jkJnLtBSfw==} + engines: {node: '>=16'} + cpu: [x64] + os: [win32] + + '@cloudflare/workers-types@4.20260511.1': + resolution: {integrity: sha512-FA+si7cOq9i/gtCHhIc0XJL0l1F/ApF+m00752Aj7WZFJrj3ZulT2T8/+rT3BabMT0QEnqFEGIqCgrmqhgEfMg==} + + '@cspotcode/source-map-support@0.8.1': + resolution: {integrity: sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==} + engines: {node: '>=12'} + + '@emnapi/runtime@1.10.0': + resolution: {integrity: sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==} + + '@esbuild/aix-ppc64@0.27.3': + resolution: {integrity: sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + + '@esbuild/android-arm64@0.27.3': + resolution: {integrity: sha512-YdghPYUmj/FX2SYKJ0OZxf+iaKgMsKHVPF1MAq/P8WirnSpCStzKJFjOjzsW0QQ7oIAiccHdcqjbHmJxRb/dmg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm@0.27.3': + resolution: {integrity: sha512-i5D1hPY7GIQmXlXhs2w8AWHhenb00+GxjxRncS2ZM7YNVGNfaMxgzSGuO8o8SJzRc/oZwU2bcScvVERk03QhzA==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + + '@esbuild/android-x64@0.27.3': + resolution: {integrity: sha512-IN/0BNTkHtk8lkOM8JWAYFg4ORxBkZQf9zXiEOfERX/CzxW3Vg1ewAhU7QSWQpVIzTW+b8Xy+lGzdYXV6UZObQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + + '@esbuild/darwin-arm64@0.27.3': + resolution: {integrity: sha512-Re491k7ByTVRy0t3EKWajdLIr0gz2kKKfzafkth4Q8A5n1xTHrkqZgLLjFEHVD+AXdUGgQMq+Godfq45mGpCKg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-x64@0.27.3': + resolution: {integrity: sha512-vHk/hA7/1AckjGzRqi6wbo+jaShzRowYip6rt6q7VYEDX4LEy1pZfDpdxCBnGtl+A5zq8iXDcyuxwtv3hNtHFg==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + + '@esbuild/freebsd-arm64@0.27.3': + resolution: {integrity: sha512-ipTYM2fjt3kQAYOvo6vcxJx3nBYAzPjgTCk7QEgZG8AUO3ydUhvelmhrbOheMnGOlaSFUoHXB6un+A7q4ygY9w==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.27.3': + resolution: {integrity: sha512-dDk0X87T7mI6U3K9VjWtHOXqwAMJBNN2r7bejDsc+j03SEjtD9HrOl8gVFByeM0aJksoUuUVU9TBaZa2rgj0oA==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + + '@esbuild/linux-arm64@0.27.3': + resolution: {integrity: sha512-sZOuFz/xWnZ4KH3YfFrKCf1WyPZHakVzTiqji3WDc0BCl2kBwiJLCXpzLzUBLgmp4veFZdvN5ChW4Eq/8Fc2Fg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm@0.27.3': + resolution: {integrity: sha512-s6nPv2QkSupJwLYyfS+gwdirm0ukyTFNl3KTgZEAiJDd+iHZcbTPPcWCcRYH+WlNbwChgH2QkE9NSlNrMT8Gfw==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-ia32@0.27.3': + resolution: {integrity: sha512-yGlQYjdxtLdh0a3jHjuwOrxQjOZYD/C9PfdbgJJF3TIZWnm/tMd/RcNiLngiu4iwcBAOezdnSLAwQDPqTmtTYg==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-loong64@0.27.3': + resolution: {integrity: sha512-WO60Sn8ly3gtzhyjATDgieJNet/KqsDlX5nRC5Y3oTFcS1l0KWba+SEa9Ja1GfDqSF1z6hif/SkpQJbL63cgOA==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-mips64el@0.27.3': + resolution: {integrity: sha512-APsymYA6sGcZ4pD6k+UxbDjOFSvPWyZhjaiPyl/f79xKxwTnrn5QUnXR5prvetuaSMsb4jgeHewIDCIWljrSxw==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-ppc64@0.27.3': + resolution: {integrity: sha512-eizBnTeBefojtDb9nSh4vvVQ3V9Qf9Df01PfawPcRzJH4gFSgrObw+LveUyDoKU3kxi5+9RJTCWlj4FjYXVPEA==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-riscv64@0.27.3': + resolution: {integrity: sha512-3Emwh0r5wmfm3ssTWRQSyVhbOHvqegUDRd0WhmXKX2mkHJe1SFCMJhagUleMq+Uci34wLSipf8Lagt4LlpRFWQ==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-s390x@0.27.3': + resolution: {integrity: sha512-pBHUx9LzXWBc7MFIEEL0yD/ZVtNgLytvx60gES28GcWMqil8ElCYR4kvbV2BDqsHOvVDRrOxGySBM9Fcv744hw==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-x64@0.27.3': + resolution: {integrity: sha512-Czi8yzXUWIQYAtL/2y6vogER8pvcsOsk5cpwL4Gk5nJqH5UZiVByIY8Eorm5R13gq+DQKYg0+JyQoytLQas4dA==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + + '@esbuild/netbsd-arm64@0.27.3': + resolution: {integrity: sha512-sDpk0RgmTCR/5HguIZa9n9u+HVKf40fbEUt+iTzSnCaGvY9kFP0YKBWZtJaraonFnqef5SlJ8/TiPAxzyS+UoA==} + engines: {node: '>=18'} + cpu: [arm64] + os: [netbsd] + + '@esbuild/netbsd-x64@0.27.3': + resolution: {integrity: sha512-P14lFKJl/DdaE00LItAukUdZO5iqNH7+PjoBm+fLQjtxfcfFE20Xf5CrLsmZdq5LFFZzb5JMZ9grUwvtVYzjiA==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + + '@esbuild/openbsd-arm64@0.27.3': + resolution: {integrity: sha512-AIcMP77AvirGbRl/UZFTq5hjXK+2wC7qFRGoHSDrZ5v5b8DK/GYpXW3CPRL53NkvDqb9D+alBiC/dV0Fb7eJcw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + + '@esbuild/openbsd-x64@0.27.3': + resolution: {integrity: sha512-DnW2sRrBzA+YnE70LKqnM3P+z8vehfJWHXECbwBmH/CU51z6FiqTQTHFenPlHmo3a8UgpLyH3PT+87OViOh1AQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + + '@esbuild/openharmony-arm64@0.27.3': + resolution: {integrity: sha512-NinAEgr/etERPTsZJ7aEZQvvg/A6IsZG/LgZy+81wON2huV7SrK3e63dU0XhyZP4RKGyTm7aOgmQk0bGp0fy2g==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openharmony] + + '@esbuild/sunos-x64@0.27.3': + resolution: {integrity: sha512-PanZ+nEz+eWoBJ8/f8HKxTTD172SKwdXebZ0ndd953gt1HRBbhMsaNqjTyYLGLPdoWHy4zLU7bDVJztF5f3BHA==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + + '@esbuild/win32-arm64@0.27.3': + resolution: {integrity: sha512-B2t59lWWYrbRDw/tjiWOuzSsFh1Y/E95ofKz7rIVYSQkUYBjfSgf6oeYPNWHToFRr2zx52JKApIcAS/D5TUBnA==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-ia32@0.27.3': + resolution: {integrity: sha512-QLKSFeXNS8+tHW7tZpMtjlNb7HKau0QDpwm49u0vUp9y1WOF+PEzkU84y9GqYaAVW8aH8f3GcBck26jh54cX4Q==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-x64@0.27.3': + resolution: {integrity: sha512-4uJGhsxuptu3OcpVAzli+/gWusVGwZZHTlS63hh++ehExkVT8SgiEf7/uC/PclrPPkLhZqGgCTjd0VWLo6xMqA==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + + '@img/colour@1.1.0': + resolution: {integrity: sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==} + engines: {node: '>=18'} + + '@img/sharp-darwin-arm64@0.34.5': + resolution: {integrity: sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm64] + os: [darwin] + + '@img/sharp-darwin-x64@0.34.5': + resolution: {integrity: sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [darwin] + + '@img/sharp-libvips-darwin-arm64@1.2.4': + resolution: {integrity: sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g==} + cpu: [arm64] + os: [darwin] + + '@img/sharp-libvips-darwin-x64@1.2.4': + resolution: {integrity: sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==} + cpu: [x64] + os: [darwin] + + '@img/sharp-libvips-linux-arm64@1.2.4': + resolution: {integrity: sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@img/sharp-libvips-linux-arm@1.2.4': + resolution: {integrity: sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==} + cpu: [arm] + os: [linux] + libc: [glibc] + + '@img/sharp-libvips-linux-ppc64@1.2.4': + resolution: {integrity: sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@img/sharp-libvips-linux-riscv64@1.2.4': + resolution: {integrity: sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==} + cpu: [riscv64] + os: [linux] + libc: [glibc] + + '@img/sharp-libvips-linux-s390x@1.2.4': + resolution: {integrity: sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@img/sharp-libvips-linux-x64@1.2.4': + resolution: {integrity: sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@img/sharp-libvips-linuxmusl-arm64@1.2.4': + resolution: {integrity: sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@img/sharp-libvips-linuxmusl-x64@1.2.4': + resolution: {integrity: sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==} + cpu: [x64] + os: [linux] + libc: [musl] + + '@img/sharp-linux-arm64@0.34.5': + resolution: {integrity: sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@img/sharp-linux-arm@0.34.5': + resolution: {integrity: sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm] + os: [linux] + libc: [glibc] + + '@img/sharp-linux-ppc64@0.34.5': + resolution: {integrity: sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@img/sharp-linux-riscv64@0.34.5': + resolution: {integrity: sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [riscv64] + os: [linux] + libc: [glibc] + + '@img/sharp-linux-s390x@0.34.5': + resolution: {integrity: sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@img/sharp-linux-x64@0.34.5': + resolution: {integrity: sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@img/sharp-linuxmusl-arm64@0.34.5': + resolution: {integrity: sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@img/sharp-linuxmusl-x64@0.34.5': + resolution: {integrity: sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [linux] + libc: [musl] + + '@img/sharp-wasm32@0.34.5': + resolution: {integrity: sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [wasm32] + + '@img/sharp-win32-arm64@0.34.5': + resolution: {integrity: sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm64] + os: [win32] + + '@img/sharp-win32-ia32@0.34.5': + resolution: {integrity: sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [ia32] + os: [win32] + + '@img/sharp-win32-x64@0.34.5': + resolution: {integrity: sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [win32] + + '@jridgewell/resolve-uri@3.1.2': + resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} + engines: {node: '>=6.0.0'} + + '@jridgewell/sourcemap-codec@1.5.5': + resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} + + '@jridgewell/trace-mapping@0.3.9': + resolution: {integrity: sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==} + + '@poppinss/colors@4.1.6': + resolution: {integrity: sha512-H9xkIdFswbS8n1d6vmRd8+c10t2Qe+rZITbbDHHkQixH5+2x1FDGmi/0K+WgWiqQFKPSlIYB7jlH6Kpfn6Fleg==} + + '@poppinss/dumper@0.6.5': + resolution: {integrity: sha512-NBdYIb90J7LfOI32dOewKI1r7wnkiH6m920puQ3qHUeZkxNkQiFnXVWoE6YtFSv6QOiPPf7ys6i+HWWecDz7sw==} + + '@poppinss/exception@1.2.3': + resolution: {integrity: sha512-dCED+QRChTVatE9ibtoaxc+WkdzOSjYTKi/+uacHWIsfodVfpsueo3+DKpgU5Px8qXjgmXkSvhXvSCz3fnP9lw==} + + '@sindresorhus/is@7.2.0': + resolution: {integrity: sha512-P1Cz1dWaFfR4IR+U13mqqiGsLFf1KbayybWwdd2vfctdV6hDpUkgCY0nKOLLTMSoRd/jJNjtbqzf13K8DCCXQw==} + engines: {node: '>=18'} + + '@speed-highlight/core@1.2.15': + resolution: {integrity: sha512-BMq1K3DsElxDWawkX6eLg9+CKJrTVGCBAWVuHXVUV2u0s2711qiChLSId6ikYPfxhdYocLNt3wWwSvDiTvFabw==} + + blake3-wasm@2.1.5: + resolution: {integrity: sha512-F1+K8EbfOZE49dtoPtmxUQrpXaBIl3ICvasLh+nJta0xkz+9kF/7uet9fLnwKqhDrmj6g+6K3Tw9yQPUg2ka5g==} + + cookie@1.1.1: + resolution: {integrity: sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==} + engines: {node: '>=18'} + + detect-libc@2.1.2: + resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} + engines: {node: '>=8'} + + error-stack-parser-es@1.0.5: + resolution: {integrity: sha512-5qucVt2XcuGMcEGgWI7i+yZpmpByQ8J1lHhcL7PwqCwu9FPP3VUXzT4ltHe5i2z9dePwEHcDVOAfSnHsOlCXRA==} + + esbuild@0.27.3: + resolution: {integrity: sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg==} + engines: {node: '>=18'} + hasBin: true + + fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + + hono@4.12.18: + resolution: {integrity: sha512-RWzP96k/yv0PQfyXnWjs6zot20TqfpfsNXhOnev8d1InAxubW93L11/oNUc3tQqn2G0bSdAOBpX+2uDFHV7kdQ==} + engines: {node: '>=16.9.0'} + + kleur@4.1.5: + resolution: {integrity: sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==} + engines: {node: '>=6'} + + miniflare@4.20260508.0: + resolution: {integrity: sha512-h3aG+PA8jEH76V4ZtBAbs3g7kjMfHJUF8hPvxeeajLTKwir+G+dqfBODg5yF9MT29LqrZKCRQRqzfHPWX4kCIg==} + engines: {node: '>=22.0.0'} + hasBin: true + + path-to-regexp@6.3.0: + resolution: {integrity: sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==} + + pathe@2.0.3: + resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} + + semver@7.8.0: + resolution: {integrity: sha512-AcM7dV/5ul4EekoQ29Agm5vri8JNqRyj39o0qpX6vDF2GZrtutZl5RwgD1XnZjiTAfncsJhMI48QQH3sN87YNA==} + engines: {node: '>=10'} + hasBin: true + + sharp@0.34.5: + resolution: {integrity: sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + + supports-color@10.2.2: + resolution: {integrity: sha512-SS+jx45GF1QjgEXQx4NJZV9ImqmO2NPz5FNsIHrsDjh2YsHnawpan7SNQ1o8NuhrbHZy9AZhIoCUiCeaW/C80g==} + engines: {node: '>=18'} + + tslib@2.8.1: + resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + + typescript@5.9.3: + resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} + engines: {node: '>=14.17'} + hasBin: true + + undici@7.24.8: + resolution: {integrity: sha512-6KQ/+QxK49Z/p3HO6E5ZCZWNnCasyZLa5ExaVYyvPxUwKtbCPMKELJOqh7EqOle0t9cH/7d2TaaTRRa6Nhs4YQ==} + engines: {node: '>=20.18.1'} + + unenv@2.0.0-rc.24: + resolution: {integrity: sha512-i7qRCmY42zmCwnYlh9H2SvLEypEFGye5iRmEMKjcGi7zk9UquigRjFtTLz0TYqr0ZGLZhaMHl/foy1bZR+Cwlw==} + + workerd@1.20260508.1: + resolution: {integrity: sha512-VlnjyH3AjVddpSK7J54nsCVgf8i2733pl8GjKttfNi7vN/hEjjAk20d2b1nDToOLKvRQpTewRnVkqaaeGHCaAw==} + engines: {node: '>=16'} + hasBin: true + + wrangler@4.90.1: + resolution: {integrity: sha512-u2KrieKSMfRM0toTst/CfDtcRraeoVjmcExcMWgILM/ytq3qcDhuOAULoZSyPHzma43lfLJy1BC544drFyqe1A==} + engines: {node: '>=22.0.0'} + hasBin: true + peerDependencies: + '@cloudflare/workers-types': ^4.20260508.1 + peerDependenciesMeta: + '@cloudflare/workers-types': + optional: true + + ws@8.18.0: + resolution: {integrity: sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==} + 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 + + youch-core@0.3.3: + resolution: {integrity: sha512-ho7XuGjLaJ2hWHoK8yFnsUGy2Y5uDpqSTq1FkHLK4/oqKtyUU1AFbOOxY4IpC9f0fTLjwYbslUz0Po5BpD1wrA==} + + youch@4.1.0-beta.10: + resolution: {integrity: sha512-rLfVLB4FgQneDr0dv1oddCVZmKjcJ6yX6mS4pU82Mq/Dt9a3cLZQ62pDBL4AUO+uVrCvtWz3ZFUL2HFAFJ/BXQ==} + +snapshots: + + '@cloudflare/kv-asset-handler@0.5.0': {} + + '@cloudflare/unenv-preset@2.16.1(unenv@2.0.0-rc.24)(workerd@1.20260508.1)': + dependencies: + unenv: 2.0.0-rc.24 + optionalDependencies: + workerd: 1.20260508.1 + + '@cloudflare/workerd-darwin-64@1.20260508.1': + optional: true + + '@cloudflare/workerd-darwin-arm64@1.20260508.1': + optional: true + + '@cloudflare/workerd-linux-64@1.20260508.1': + optional: true + + '@cloudflare/workerd-linux-arm64@1.20260508.1': + optional: true + + '@cloudflare/workerd-windows-64@1.20260508.1': + optional: true + + '@cloudflare/workers-types@4.20260511.1': {} + + '@cspotcode/source-map-support@0.8.1': + dependencies: + '@jridgewell/trace-mapping': 0.3.9 + + '@emnapi/runtime@1.10.0': + dependencies: + tslib: 2.8.1 + optional: true + + '@esbuild/aix-ppc64@0.27.3': + optional: true + + '@esbuild/android-arm64@0.27.3': + optional: true + + '@esbuild/android-arm@0.27.3': + optional: true + + '@esbuild/android-x64@0.27.3': + optional: true + + '@esbuild/darwin-arm64@0.27.3': + optional: true + + '@esbuild/darwin-x64@0.27.3': + optional: true + + '@esbuild/freebsd-arm64@0.27.3': + optional: true + + '@esbuild/freebsd-x64@0.27.3': + optional: true + + '@esbuild/linux-arm64@0.27.3': + optional: true + + '@esbuild/linux-arm@0.27.3': + optional: true + + '@esbuild/linux-ia32@0.27.3': + optional: true + + '@esbuild/linux-loong64@0.27.3': + optional: true + + '@esbuild/linux-mips64el@0.27.3': + optional: true + + '@esbuild/linux-ppc64@0.27.3': + optional: true + + '@esbuild/linux-riscv64@0.27.3': + optional: true + + '@esbuild/linux-s390x@0.27.3': + optional: true + + '@esbuild/linux-x64@0.27.3': + optional: true + + '@esbuild/netbsd-arm64@0.27.3': + optional: true + + '@esbuild/netbsd-x64@0.27.3': + optional: true + + '@esbuild/openbsd-arm64@0.27.3': + optional: true + + '@esbuild/openbsd-x64@0.27.3': + optional: true + + '@esbuild/openharmony-arm64@0.27.3': + optional: true + + '@esbuild/sunos-x64@0.27.3': + optional: true + + '@esbuild/win32-arm64@0.27.3': + optional: true + + '@esbuild/win32-ia32@0.27.3': + optional: true + + '@esbuild/win32-x64@0.27.3': + optional: true + + '@img/colour@1.1.0': {} + + '@img/sharp-darwin-arm64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-darwin-arm64': 1.2.4 + optional: true + + '@img/sharp-darwin-x64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-darwin-x64': 1.2.4 + optional: true + + '@img/sharp-libvips-darwin-arm64@1.2.4': + optional: true + + '@img/sharp-libvips-darwin-x64@1.2.4': + optional: true + + '@img/sharp-libvips-linux-arm64@1.2.4': + optional: true + + '@img/sharp-libvips-linux-arm@1.2.4': + optional: true + + '@img/sharp-libvips-linux-ppc64@1.2.4': + optional: true + + '@img/sharp-libvips-linux-riscv64@1.2.4': + optional: true + + '@img/sharp-libvips-linux-s390x@1.2.4': + optional: true + + '@img/sharp-libvips-linux-x64@1.2.4': + optional: true + + '@img/sharp-libvips-linuxmusl-arm64@1.2.4': + optional: true + + '@img/sharp-libvips-linuxmusl-x64@1.2.4': + optional: true + + '@img/sharp-linux-arm64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linux-arm64': 1.2.4 + optional: true + + '@img/sharp-linux-arm@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linux-arm': 1.2.4 + optional: true + + '@img/sharp-linux-ppc64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linux-ppc64': 1.2.4 + optional: true + + '@img/sharp-linux-riscv64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linux-riscv64': 1.2.4 + optional: true + + '@img/sharp-linux-s390x@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linux-s390x': 1.2.4 + optional: true + + '@img/sharp-linux-x64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linux-x64': 1.2.4 + optional: true + + '@img/sharp-linuxmusl-arm64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linuxmusl-arm64': 1.2.4 + optional: true + + '@img/sharp-linuxmusl-x64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linuxmusl-x64': 1.2.4 + optional: true + + '@img/sharp-wasm32@0.34.5': + dependencies: + '@emnapi/runtime': 1.10.0 + optional: true + + '@img/sharp-win32-arm64@0.34.5': + optional: true + + '@img/sharp-win32-ia32@0.34.5': + optional: true + + '@img/sharp-win32-x64@0.34.5': + optional: true + + '@jridgewell/resolve-uri@3.1.2': {} + + '@jridgewell/sourcemap-codec@1.5.5': {} + + '@jridgewell/trace-mapping@0.3.9': + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.5.5 + + '@poppinss/colors@4.1.6': + dependencies: + kleur: 4.1.5 + + '@poppinss/dumper@0.6.5': + dependencies: + '@poppinss/colors': 4.1.6 + '@sindresorhus/is': 7.2.0 + supports-color: 10.2.2 + + '@poppinss/exception@1.2.3': {} + + '@sindresorhus/is@7.2.0': {} + + '@speed-highlight/core@1.2.15': {} + + blake3-wasm@2.1.5: {} + + cookie@1.1.1: {} + + detect-libc@2.1.2: {} + + error-stack-parser-es@1.0.5: {} + + esbuild@0.27.3: + optionalDependencies: + '@esbuild/aix-ppc64': 0.27.3 + '@esbuild/android-arm': 0.27.3 + '@esbuild/android-arm64': 0.27.3 + '@esbuild/android-x64': 0.27.3 + '@esbuild/darwin-arm64': 0.27.3 + '@esbuild/darwin-x64': 0.27.3 + '@esbuild/freebsd-arm64': 0.27.3 + '@esbuild/freebsd-x64': 0.27.3 + '@esbuild/linux-arm': 0.27.3 + '@esbuild/linux-arm64': 0.27.3 + '@esbuild/linux-ia32': 0.27.3 + '@esbuild/linux-loong64': 0.27.3 + '@esbuild/linux-mips64el': 0.27.3 + '@esbuild/linux-ppc64': 0.27.3 + '@esbuild/linux-riscv64': 0.27.3 + '@esbuild/linux-s390x': 0.27.3 + '@esbuild/linux-x64': 0.27.3 + '@esbuild/netbsd-arm64': 0.27.3 + '@esbuild/netbsd-x64': 0.27.3 + '@esbuild/openbsd-arm64': 0.27.3 + '@esbuild/openbsd-x64': 0.27.3 + '@esbuild/openharmony-arm64': 0.27.3 + '@esbuild/sunos-x64': 0.27.3 + '@esbuild/win32-arm64': 0.27.3 + '@esbuild/win32-ia32': 0.27.3 + '@esbuild/win32-x64': 0.27.3 + + fsevents@2.3.3: + optional: true + + hono@4.12.18: {} + + kleur@4.1.5: {} + + miniflare@4.20260508.0: + dependencies: + '@cspotcode/source-map-support': 0.8.1 + sharp: 0.34.5 + undici: 7.24.8 + workerd: 1.20260508.1 + ws: 8.18.0 + youch: 4.1.0-beta.10 + transitivePeerDependencies: + - bufferutil + - utf-8-validate + + path-to-regexp@6.3.0: {} + + pathe@2.0.3: {} + + semver@7.8.0: {} + + sharp@0.34.5: + dependencies: + '@img/colour': 1.1.0 + detect-libc: 2.1.2 + semver: 7.8.0 + 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 + + supports-color@10.2.2: {} + + tslib@2.8.1: + optional: true + + typescript@5.9.3: {} + + undici@7.24.8: {} + + unenv@2.0.0-rc.24: + dependencies: + pathe: 2.0.3 + + workerd@1.20260508.1: + optionalDependencies: + '@cloudflare/workerd-darwin-64': 1.20260508.1 + '@cloudflare/workerd-darwin-arm64': 1.20260508.1 + '@cloudflare/workerd-linux-64': 1.20260508.1 + '@cloudflare/workerd-linux-arm64': 1.20260508.1 + '@cloudflare/workerd-windows-64': 1.20260508.1 + + wrangler@4.90.1(@cloudflare/workers-types@4.20260511.1): + dependencies: + '@cloudflare/kv-asset-handler': 0.5.0 + '@cloudflare/unenv-preset': 2.16.1(unenv@2.0.0-rc.24)(workerd@1.20260508.1) + blake3-wasm: 2.1.5 + esbuild: 0.27.3 + miniflare: 4.20260508.0 + path-to-regexp: 6.3.0 + unenv: 2.0.0-rc.24 + workerd: 1.20260508.1 + optionalDependencies: + '@cloudflare/workers-types': 4.20260511.1 + fsevents: 2.3.3 + transitivePeerDependencies: + - bufferutil + - utf-8-validate + + ws@8.18.0: {} + + youch-core@0.3.3: + dependencies: + '@poppinss/exception': 1.2.3 + error-stack-parser-es: 1.0.5 + + youch@4.1.0-beta.10: + dependencies: + '@poppinss/colors': 4.1.6 + '@poppinss/dumper': 0.6.5 + '@speed-highlight/core': 1.2.15 + cookie: 1.1.1 + youch-core: 0.3.3 diff --git a/.component-builds/kbdb_upsert_block/src/index.ts b/.component-builds/kbdb_upsert_block/src/index.ts new file mode 100644 index 0000000..4a6bc80 --- /dev/null +++ b/.component-builds/kbdb_upsert_block/src/index.ts @@ -0,0 +1,75 @@ +/** + * arcrun WASM 零件 Worker (kbdb_upsert_block) + * POST / → JSON input → WASM (WASI preview1) → JSON output + * SDD: polaris/mira/.agents/specs/mira-app/design.md §3.5.12.4.1 + * matrix/arcrun/.agents/specs/arcrun/arcrun.md 三-B 新零件加入紀錄 + */ + +import componentWasm from '../component.wasm' assert { type: 'webassembly' }; +import { Hono } from 'hono'; +import { cors } from 'hono/cors'; +import { createWasiShim, type WasiHostFunctions } from '../../../cypher-executor/src/lib/wasi-shim'; + +const app = new Hono(); +app.use('*', cors()); + +app.get('/', (c) => c.json({ ok: true, component: 'kbdb_upsert_block' })); + +app.post('/', async (c) => { + let input: unknown; + try { + input = await c.req.json(); + } catch { + return c.json({ success: false, error: 'request body must be JSON' }, 400); + } + + try { + const result = await runWasm(input); + return c.json(result); + } catch (e) { + return c.json( + { success: false, error: e instanceof Error ? e.message : String(e) }, + 500, + ); + } +}); + +export default app; + +async function runWasm(input: unknown): Promise { + const hostFunctions: WasiHostFunctions = { + http_request: async (url, method, headersJson, body) => { + const headers: Record = {}; + if (headersJson) { + try { + const parsed = JSON.parse(headersJson); + if (parsed && typeof parsed === 'object') { + for (const [k, v] of Object.entries(parsed as Record)) { + if (typeof v === 'string') headers[k] = v; + } + } + } catch {} + } + const init: RequestInit = { method, headers }; + if (body && method.toUpperCase() !== 'GET' && method.toUpperCase() !== 'HEAD') { + init.body = body; + } + const res = await fetch(url, init); + return await res.text(); + }, + }; + + const shim = createWasiShim(JSON.stringify(input), hostFunctions); + const instance = await WebAssembly.instantiate( + componentWasm as WebAssembly.Module, + shim.imports, + ); + shim.setMemory(instance.exports.memory as WebAssembly.Memory); + await shim.run(instance); + + const stdout = shim.getStdout().trim(); + const stderr = shim.getStderr().trim(); + if (stderr) console.error('[kbdb_upsert_block wasm stderr]', stderr); + if (!stdout) throw new Error('WASM component produced no output'); + return JSON.parse(stdout); +} diff --git a/.component-builds/kbdb_upsert_block/tsconfig.json b/.component-builds/kbdb_upsert_block/tsconfig.json new file mode 100644 index 0000000..b65fda7 --- /dev/null +++ b/.component-builds/kbdb_upsert_block/tsconfig.json @@ -0,0 +1,11 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "ES2022", + "moduleResolution": "bundler", + "lib": ["ES2022"], + "types": ["@cloudflare/workers-types"], + "strict": true, + "noEmit": true + } +} diff --git a/.component-builds/kbdb_upsert_block/wrangler.toml b/.component-builds/kbdb_upsert_block/wrangler.toml new file mode 100644 index 0000000..79d0080 --- /dev/null +++ b/.component-builds/kbdb_upsert_block/wrangler.toml @@ -0,0 +1,11 @@ +name = "arcrun-kbdb-upsert-block" +main = "src/index.ts" +compatibility_date = "2025-02-19" +compatibility_flags = ["nodejs_compat"] + +[vars] +COMPONENT_ID = "kbdb_upsert_block" + +[[routes]] +pattern = "kbdb-upsert-block.arcrun.dev/*" +zone_name = "arcrun.dev" diff --git a/registry/components/kbdb_upsert_block/component.contract.yaml b/registry/components/kbdb_upsert_block/component.contract.yaml new file mode 100644 index 0000000..533ae3f --- /dev/null +++ b/registry/components/kbdb_upsert_block/component.contract.yaml @@ -0,0 +1,90 @@ +canonical_id: "kbdb_upsert_block" +display_name: "KBDB Upsert Block" +category: "data" +version: "v1" +wasi_target: "preview1" +stability: "floating" +runtime_compat: + - "cf-workers" + - "workerd" + - "wazero" +constraints: + max_size_kb: 2048 + max_cold_start_ms: 50 + no_network_syscall: false + no_filesystem_syscall: true + io_model: "stdin_stdout_json" +input_schema: + type: object + required: [api_key, page_name, content] + properties: + api_key: + type: string + description: KBDB partner key(ak_xxx) + page_name: + type: string + description: 當 idempotency key。內部用 GET /blocks?page_name= 查找。 + content: + type: string + description: block 內容(PATCH 時覆寫,CREATE 時新建) + type: + type: string + description: block type(建立時用,PATCH 時忽略) + parent_id: + type: string + description: 父 block id(建立時用,PATCH 時忽略) + user_id: + type: string + description: 建立時帶入 + lookup 時用來 filter(同 page_name 多 user 共存場景) + source: + type: string + description: 來源標記 + tags_json: + type: string + description: tags JSON 字串(PATCH 時轉 array、CREATE 時直傳) + kbdb_url: + type: string + description: KBDB API base(預設 https://kbdb.finally.click) +output_schema: + type: object + properties: + success: + type: boolean + action: + type: string + enum: [created, patched] + description: 實際做了哪個動作 + data: + type: object + description: KBDB 回傳(含 block id 等) + error: + type: string + phase: + type: string + enum: [lookup, patch, create] + description: 出錯在哪個階段 +gherkin_tests: + - scenario: "缺 page_name" + given: '{"api_key":"ak_x","content":"hi"}' + then_contains: '"success":false' + - scenario: "建立新 block" + given: '{"api_key":"ak_x","page_name":"new-page-uniq","content":"hello"}' + then_contains: '"action":"created"' + - scenario: "PATCH 既有 block" + given: '{"api_key":"ak_x","page_name":"existing-page","content":"updated"}' + then_contains: '"action":"patched"' +tags: [data, storage, kbdb, upsert, primitive, idempotent] +description: | + Upsert:用 page_name 當 idempotency key。內部 GET 找有沒有同 page_name 的 block, + 找到就 PATCH 不到就 POST 新建。解 arcrun workflow 缺 IF/branch 能力的缺口 + (arcrun.md P1 #1)。mira 7B.3f index-entry per-entity 維護是第一個使用者。 +config_example: | + upsert_index_entry: + api_key: "{{api_key}}" + page_name: "index-{{entity}}" + parent_id: "{{mira_wiki_index_entities_id}}" + type: "index-entry" + user_id: "inkstone_mira_tools" + source: "ai-canon-wiki" + content: "{{compose_index_entry.data.text}}" + tags_json: '["mira-wiki", "ai-generated", "index"]' diff --git a/registry/components/kbdb_upsert_block/go.mod b/registry/components/kbdb_upsert_block/go.mod new file mode 100644 index 0000000..9c91451 --- /dev/null +++ b/registry/components/kbdb_upsert_block/go.mod @@ -0,0 +1,3 @@ +module kbdb_upsert_block + +go 1.21 diff --git a/registry/components/kbdb_upsert_block/main.go b/registry/components/kbdb_upsert_block/main.go new file mode 100644 index 0000000..29b7453 --- /dev/null +++ b/registry/components/kbdb_upsert_block/main.go @@ -0,0 +1,271 @@ +// kbdb_upsert_block — 用 page_name 當 idempotency key 做 upsert +// 內部:GET /blocks?page_name=X → user_id filter → 找到 PATCH /blocks/:id 沒找到 POST /blocks +// 解 arcrun workflow 沒 IF/branch 能力的缺口(arcrun.md P1 #1) +// 對應 SDD:polaris/mira/.agents/specs/mira-app/design.md §3.5.12.4.1 +// +//go:build tinygo + +package main + +import ( + "encoding/json" + "io" + "os" + "strconv" + "unsafe" +) + +//go:wasmimport u6u http_request +func hostHttpRequest( + urlPtr uintptr, urlLen uint32, + methodPtr uintptr, methodLen uint32, + headersPtr uintptr, headersLen uint32, + bodyPtr uintptr, bodyLen uint32, + outPtr uintptr, outLenPtr uintptr, +) uint32 + +type Input struct { + KBDBUrl string `json:"kbdb_url"` // optional + APIKey string `json:"api_key"` // 必填 + PageName string `json:"page_name"` // 必填,當 idempotency key + Content string `json:"content"` // 必填 + Type string `json:"type"` // optional(建立時用,PATCH 時忽略) + ParentID string `json:"parent_id"` // optional(建立時用,PATCH 時忽略) + UserID string `json:"user_id"` // optional(建立時用 + lookup filter) + Source string `json:"source"` // optional + TagsJSON string `json:"tags_json"` // optional(完整覆寫) +} + +var dummy [1]byte + +func safePtr(b []byte) (uintptr, uint32) { + if len(b) == 0 { + return uintptr(unsafe.Pointer(&dummy[0])), 0 + } + return uintptr(unsafe.Pointer(&b[0])), uint32(len(b)) +} + +func writeError(msg string) { + out, _ := json.Marshal(map[string]interface{}{"success": false, "error": msg}) + os.Stdout.Write(out) +} + +func writeResult(action string, data map[string]interface{}) { + out, _ := json.Marshal(map[string]interface{}{ + "success": true, + "action": action, + "data": data, + }) + os.Stdout.Write(out) +} + +// urlEncode:跟 kbdb_get 一致,避免引入 net/url +func urlEncode(s string) string { + var out []byte + for i := 0; i < len(s); i++ { + c := s[i] + if (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') || + c == '-' || c == '_' || c == '.' || c == '~' { + out = append(out, c) + } else { + const hex = "0123456789ABCDEF" + out = append(out, '%', hex[c>>4], hex[c&0x0f]) + } + } + return string(out) +} + +func httpCall(method, url string, headers map[string]string, body []byte) ([]byte, uint32) { + headersBytes, _ := json.Marshal(headers) + urlBytes := []byte(url) + methodBytes := []byte(method) + + outBuf := make([]byte, 1<<20) // 1MB + var outLen uint32 + + urlPtr, urlLen := safePtr(urlBytes) + methodPtr, methodLen := safePtr(methodBytes) + headersPtr, headersLenU := safePtr(headersBytes) + bodyPtr, bodyLenU := safePtr(body) + + result := hostHttpRequest( + urlPtr, urlLen, + methodPtr, methodLen, + headersPtr, headersLenU, + bodyPtr, bodyLenU, + uintptr(unsafe.Pointer(&outBuf[0])), uintptr(unsafe.Pointer(&outLen)), + ) + return outBuf[:outLen], result +} + +func main() { + raw, err := io.ReadAll(os.Stdin) + if err != nil { + writeError("failed to read stdin: " + err.Error()) + return + } + + var input Input + if err := json.Unmarshal(raw, &input); err != nil { + writeError("invalid input JSON: " + err.Error()) + return + } + + if input.APIKey == "" { + writeError("api_key 必填") + return + } + if input.PageName == "" { + writeError("page_name 必填(upsert 的 idempotency key)") + return + } + if input.Content == "" { + writeError("content 必填") + return + } + + kbdbURL := input.KBDBUrl + if kbdbURL == "" { + kbdbURL = "https://kbdb.finally.click" + } + + headers := map[string]string{ + "Authorization": "Bearer " + input.APIKey, + } + + // ── Step 1:lookup by page_name ──────────────────────────────────── + lookupURL := kbdbURL + "/blocks?page_name=" + urlEncode(input.PageName) + + "&limit=" + strconv.Itoa(10) + lookupResp, callResult := httpCall("GET", lookupURL, headers, nil) + if callResult != 0 { + writeError("KBDB lookup failed (host_http_request returned non-zero)") + return + } + + var lookupParsed struct { + Blocks []map[string]interface{} `json:"blocks"` + Count int `json:"count"` + Error interface{} `json:"error"` + } + if err := json.Unmarshal(lookupResp, &lookupParsed); err != nil { + writeError("KBDB lookup returned non-JSON: " + string(lookupResp)) + return + } + if lookupParsed.Error != nil { + errBytes, _ := json.Marshal(map[string]interface{}{ + "success": false, + "error": lookupParsed.Error, + "phase": "lookup", + }) + os.Stdout.Write(errBytes) + return + } + + // ── Step 2:找符合 user_id 的第一筆 ────────────────────────────── + var existing map[string]interface{} + for _, b := range lookupParsed.Blocks { + if input.UserID == "" { + existing = b + break + } + if uid, ok := b["user_id"].(string); ok && uid == input.UserID { + existing = b + break + } + } + + // ── Step 3:分支寫入 ─────────────────────────────────────────────── + postHeaders := map[string]string{ + "Content-Type": "application/json", + "Authorization": "Bearer " + input.APIKey, + } + + if existing != nil { + // PATCH 路徑 + existingID, _ := existing["id"].(string) + if existingID == "" { + writeError("lookup 找到 block 但 id 為空") + return + } + + patchBody := make(map[string]interface{}) + patchBody["content"] = input.Content + if input.Source != "" { + patchBody["source"] = input.Source + } + if input.TagsJSON != "" { + // PATCH endpoint 用 tags array 不是 tags_json string + var tagsArr []string + if err := json.Unmarshal([]byte(input.TagsJSON), &tagsArr); err == nil { + patchBody["tags"] = tagsArr + } + } + patchBodyBytes, _ := json.Marshal(patchBody) + + patchURL := kbdbURL + "/blocks/" + existingID + patchResp, callResult := httpCall("PATCH", patchURL, postHeaders, patchBodyBytes) + if callResult != 0 { + writeError("KBDB PATCH failed (host_http_request returned non-zero)") + return + } + var patchParsed map[string]interface{} + if err := json.Unmarshal(patchResp, &patchParsed); err != nil { + writeError("KBDB PATCH returned non-JSON: " + string(patchResp)) + return + } + if _, hasErr := patchParsed["error"]; hasErr { + errBytes, _ := json.Marshal(map[string]interface{}{ + "success": false, + "error": patchParsed["error"], + "phase": "patch", + }) + os.Stdout.Write(errBytes) + return + } + writeResult("patched", patchParsed) + return + } + + // CREATE 路徑 + postBody := make(map[string]interface{}) + postBody["content"] = input.Content + postBody["page_name"] = input.PageName + if input.Type != "" { + postBody["type"] = input.Type + } + if input.ParentID != "" { + postBody["parent_id"] = input.ParentID + } + if input.UserID != "" { + postBody["user_id"] = input.UserID + } + if input.Source != "" { + postBody["source"] = input.Source + } + if input.TagsJSON != "" { + postBody["tags_json"] = input.TagsJSON + } + postBodyBytes, _ := json.Marshal(postBody) + + postURL := kbdbURL + "/blocks" + postResp, callResult := httpCall("POST", postURL, postHeaders, postBodyBytes) + if callResult != 0 { + writeError("KBDB POST failed (host_http_request returned non-zero)") + return + } + var postParsed map[string]interface{} + if err := json.Unmarshal(postResp, &postParsed); err != nil { + writeError("KBDB POST returned non-JSON: " + string(postResp)) + return + } + if _, hasErr := postParsed["error"]; hasErr { + errBytes, _ := json.Marshal(map[string]interface{}{ + "success": false, + "error": postParsed["error"], + "phase": "create", + }) + os.Stdout.Write(errBytes) + return + } + writeResult("created", postParsed) +}