42 Commits

Author SHA1 Message Date
uncle6me-web 222a382d49 fix(#12): MCP service binding 用 [[services]] array-of-tables(修 self-hosted CYPHER_EXECUTOR 缺失)
根因(非總管原假設的 strip 誤清):mcp/wrangler.toml 用 inline `services = [...]` 且在
[vars] table 之後 → TOML 把它吸成 `vars.services`(普通 env var 陣列)而非頂層 service
bindings → wrangler 看不到 CYPHER_EXECUTOR/KBDB/COMPONENT_REGISTRY binding。
self-hosted 部署 injectMultiTenant 往 [vars] 注入 MULTI_TENANT 後此問題暴露
(MCP 報 "CYPHER_EXECUTOR service binding not configured")。

修法:inline → [[services]] array-of-tables(獨立頂層 table,不受 [vars] 影響,
對齊官方 cypher-executor/wrangler.toml 慣例)。

本地驗證(wrangler deploy --dry-run,模擬 self-hosted 注入 MULTI_TENANT):
- 修法後:env.CYPHER_EXECUTOR/COMPONENT_REGISTRY/KBDB 三 binding 都在 
- 對照舊 inline:binding 全消失、變成 env.services(一個 JSON env var) 坐實根因

⚠️ 端到端待 leo21c acr update 重部 MCP(CC 跑不了 leo21c 部署)。修好後
MCP validate/recipe_search 不再報 binding not configured;連帶解鎖 #11 self-hosted
u6u_list/search_workflows + mira #6 門鈴 + acr push 端到端。#12 留 open 待端到端。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-27 23:32:16 +08:00
uncle6me-web 5d38b599fd feat(#11): CLI/MCP 薄殼對齊 — P0 run 死端點修 + P1 list 同源 + R4 防複發機制
P0 CLI run 改打 /webhooks/named/:name/trigger 真端點(原打 /webhooks/<name> 死端點 404)。
P1 CLI/MCP list 收斂到 GET /webhooks/named(KV 同源):
   - CLI list 停 CfKvClient 直連 KV,順手修 key 前綴 bug(原讀 workflow: 對不上部署的 {apiKey}:wf:)
     + self-hosted 不再需 CF API token。
   - MCP u6u_list_workflows 從讀 KBDB record 改讀 GET /webhooks/named(registry 簽名加 partnerToken)。
R4 防複發機制:
   - cli-mcp-capability-matrix.md(13 能力對照,docs/ gitignored 不進此 commit,僅本機)
   - thin-shell-smoke.sh(對真端點斷言非 404,本機手動跑非 CI/cron)
   - 機制自驗:注入故意死端點當場攔下、exit 1。

依賴關係:本批依賴 #8(webhooks-named GET 補 description/created_at 欄位、search/backfill 端點),
故疊在 feat/issue-8 branch 上、作獨立 commit。

⚠️ tsc 綠 = code done 非完成。端到端待 leo21c(CLI/MCP 真打通 200 非 404、smoke 對已部署 prod
全綠——目前 smoke 揭 #8 search/backfill 在 prod 仍 404=未部署)。P2 validate 收斂待 #10、
tag resource_id 語意債待方向①。#11 留 open。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-27 19:36:58 +08:00
uncle6me-web 558e80b4da chore(wiki): wiki-init 補骨架 + system-dev-template 安裝/更新腳本
wiki 已初始化過(push 檔活躍維護),本次補從沒建的 pull 層 + arcrun 化範本:
- cards/decisions/ 14 張決策原子卡(含 gloss/實體/typed-edge 三元組):
  從 decisions-summary 全量改寫 13 + 新增「薄殼規則晚於實作-MCP漂移是歷史債」1
- TAXONOMY 從 PKM 範本換成 arcrun 軸(子系統 零件架構/cypher/credential/recipe/kbdb/
  薄殼/部署/平台原則 + 形態 架構決策/踩坑/機制說明/禁令/案例經驗)
- principles 填 13 條跨全局原則(從 rules/ + mindset 蒸餾)
- INDEX 真實視圖(子系統角度 + 決策角度,指向 cards)
- system-dev/scripts/ + scripts/ install/update 安裝腳本(template 接入)

純基建/文檔,無業務 code(功能 code 見前一 commit)。
raw source(docs/)0 異動、wiki 卡際連結無斷鏈。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-27 17:53:37 +08:00
uncle6me-web 934b9265d9 feat: KBDB self-hosted 查詢 + embed 模組 + thin-shell 收窄 + search_workflow(code done 待端到端)
按 issue 分段標明(檔 #5/#8 改動交疊處無法乾淨拆檔,故併一個 commit):

#4 thin-shell §3.1 自力救濟階梯 + code-node 規則(純文檔/規則,code-node 零件未實作)
#5 KBDB source filter(json_extract metadata_json 零建表)+ 能力對照;documents 聚合與
   DELETE proxy 部分擱置等頂層 T8
#7 base embed 模組(kbdb/src/embed.ts)+ vectorize 開關(deploy/config/wrangler.toml 註解範本)
   + 語義查詢降級閉環(mode=semantic 未開→LIKE+capability_hint)
#8 部分(workflow-discovery):
   - KBDB /entries/search 加 base 通用 entry_type filter(entry-crud/embed/route/kbdb-proxy 透傳)
   - /webhooks/named 強制 description(空→400,訊息要求操盤 AI 據實寫一句)
   - 部署雙寫 entry_type=workflow embeddable entry(waitUntil 非阻塞,供 search)
   - cypher GET /workflows/search + MCP u6u_search_workflows(優先語意、降級 hint)
   - cypher POST /workflows/backfill-search-entries(無 desc 列出不編造)
   - GET /webhooks/named 補回 description/created_at 欄位(為 list 來源收斂備)

⚠️ tsc 綠 = code done,非完成(mindset §7 禁假綠):
- #7/#8 端到端待 leo21c 部署驗(Vectorize 需官方憑證、CC 跑不了)
- #8 ①-a(MCP deploy 改打 /webhooks/named)未做、MCP deploy 那半仍 404
- #8 端到端(強制填擋空/語義命中/租戶隔離/降級 hint)未驗

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-27 17:52:52 +08:00
uncle6me-web 013b55e97e chore(gitignore): 排除 D1 備份/匯出 *.sql(含整庫全量資料=機敏,防誤 commit)
issue #3 官方庫誤寫清理時 wrangler d1 export 出 45MB 整庫快照,
發現 .gitignore 沒排除 .sql → 隱患:D1 匯出含全量資料若忘刪可能誤 commit。
補 *.sql / backup-*.sql 規則防患。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-25 19:42:05 +08:00
uncle6me-web b090695414 chore(release): cli 1.3.13(npm publish issue #2 KBDB_BASE_URL 注入修復)
deploy 不只 git——self-hosted 用戶裝 npm 套件,修復要 publish 才到用戶手上。
自動 patch bump 1.3.12→1.3.13 + CHANGELOG 記錄(同 local-deploy.sh §6 邏輯)。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-24 12:35:50 +08:00
uncle6me-web ba00b98038 Merge: fix self-hosted KBDB_BASE_URL injection (issue #2)
self-hosted cypher 不再 fallback 到官方 kbdb;注入用戶自己帳號的 arcrun-kbdb。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-24 12:28:00 +08:00
uncle6me-web 9c4333defb fix(cli): self-hosted 注入 cypher KBDB_BASE_URL 指向用戶自己的 kbdb(issue #2)
injectWranglerConfig 的 self-hosted 分支原本只注 database_id / MULTI_TENANT,
漏了 KBDB_BASE_URL → cypher /kbdb/* 一律指向官方 arcrun-kbdb.uncle6-me,
self-hosted 用戶資料默默寫進官方庫(隔離破損)。

比照既有注入模式,用 ctx.workerSubdomain 把 KBDB_BASE_URL 就地改寫成
arcrun-kbdb.<subdomain>.workers.dev。init 與 update 共用此注入點,一處修兩條路。

驗證:tsc --noEmit 通過;真實 cypher toml 注入 subdomain=leo21c →
KBDB_BASE_URL = "https://arcrun-kbdb.leo21c.workers.dev"。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-24 12:26:36 +08:00
uncle6me-web 4d6e77ff2d chore(release): cli 1.3.12(local-deploy 自動 bump,含 block.md 三盲點重寫)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-15 13:20:29 +08:00
uncle6me-web b1e302b3b5 fix(kbdb): cypher proxy 補 /kbdb/entries CRUD + report_feedback 改打 /entries
kbdb-base Phase 9.6/9.7(HANDOFF §2 缺口① + §3b 連帶):

- 9.6 cypher kbdb-proxy 補 /kbdb/entries CRUD(POST/GET list/GET :id/PATCH :id)
  純轉發到 KBDB 基本盤 /entries,解鎖 mira _kbdb_client.py 主線遷移。
  租戶隔離同 9.5:寫入注入 owner_id、list 強制本租戶過濾、PATCH 剝 owner_id。
  刻意不開 DELETE(基本盤 delete 無 owner 檢查 → 跨租戶刪除風險)。
- 9.7 arcrun_report_feedback 從死 route /blocks 改打基本盤 /entries
  (entry_type=agent-feedback)。9.4 漏網的同類修;基本盤無 /blocks → 原本 404 假紅。

順帶(HANDOFF §6 harness 表達優化):
- 重寫 cli/harness/CLAUDE.block.md 補三盲點(recipe 是公共投稿 / 缺能力補 API 不拼裝 /
  自製零件退場路徑),目標 Haiku 級 CC 讀懂。
- README 零件 vs recipe 段對齊同三點。

cypher + mcp tsc exit 0。端到端 smoke test 隨後。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-15 13:06:58 +08:00
uncle6me-web 8f4c5dbe59 chore(release): cli 1.3.11 版本檔同步(含 MULTI_TENANT 注入修法)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-14 22:46:49 +08:00
uncle6me-web 642b61dc9f fix(cli): deploy 注入 MULTI_TENANT=false 到 self-hosted worker(修 MCP 401 注入缺口)
根因(非 code bug):partner-auth.ts MULTI_TENANT 分支邏輯對,但部署沒注入
→ worker c.env.MULTI_TENANT===undefined → 走 partner-key → self-hosted 401。
mcp/wrangler.toml 的 MULTI_TENANT 原是註解掉的,injectWranglerConfig 注了
KV/WORKER_SUBDOMAIN/D1 卻漏 MULTI_TENANT。只取消註解不夠(只修手動 fork,
沒修 acr update 自動部署這條 mira 走的路)。

修法(方案①,注 vars 非 secret,self-hosted 零填寫):
- deploy.ts:DeployContext 加 selfHosted;新增 injectMultiTenant(active/註解/無行三態
  → 加進 [vars]);injectWranglerConfig 在 selfHosted 時呼叫。
- init.ts:deployCtx selfHosted:true(本就是 --self-hosted 分支)。
- update.ts:ctx selfHosted = mode==='self-hosted' || multi_tenant===false(mira 走這條)。
- mcp/wrangler.toml:# [vars] 改 active [vars](官方不含 MULTI_TENANT=多租戶;
  注入加行在 [vars] 下,結構正確)。

本地驗注入(真實 export 函式 dry-run):mcp/cypher 注入後各 1 行 active
MULTI_TENANT="false" 在 active [vars] 下 → PASS。cli tsc exit 0。
端到端交棒 mira:leo21c 重跑 acr update → curl Bearer leo /mcp 應 200。
SDD: mcp-account-source.md §5.5.1。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-14 22:45:48 +08:00
uncle6me-web b932d96a88 chore(release): cli 1.3.10 版本檔同步(deploy 腳本自動 bump)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-14 22:18:39 +08:00
uncle6me-web eeafd5c094 fix(kbdb): searchByTemplate 真按 owner_id 過濾(修租戶隔離洩漏)
煙霧測試發現 searchByTemplate 的 `if (rec && (!owner_id || true))` 是 stub,
`|| true` 讓 owner_id 過濾失效 → 任何租戶查同 template 看得到別人的 record。
改:給 owner_id 時 JOIN entries 在 SQL 限定 e.owner_id(record 歸屬存底層
entries.owner_id,createRecord 寫入時帶);沒給才不限(內部/全域查詢)。

cypher KBDB proxy 強制注入 owner_id,故端到端隔離靠這條 SQL 落地。
searchEntries 早已正確按 owner_id 過濾,無此 bug。kbdb tsc exit 0。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-14 22:18:17 +08:00
uncle6me-web a410af0b6c fix(cypher): KBDB proxy 指向現役 arcrun-kbdb(舊 fallback kbdb.finally.click 已死)
煙霧測試發現 proxy fallback 寫死舊的 kbdb.finally.click(inkstone 遺留、
回 Missing token),非現役 KBDB。改:
- kbdb-proxy.ts fallback → arcrun-kbdb.uncle6-me.workers.dev(現役、無 auth)
- cypher wrangler.toml [vars] KBDB_BASE_URL 明設現役 URL(self-host fork 覆蓋自己的)

現役 arcrun-kbdb 無 auth middleware,不需 KBDB_INTERNAL_TOKEN。
cli 版本檔 1.3.8→1.3.9(deploy 腳本自動 bump,含 acr kbdb 命令)。

cypher tsc exit 0。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-14 22:16:18 +08:00
uncle6me-web 886a8e31d0 feat(kbdb,mcp): KBDB 資料層薄殼 + self-hosted MCP 認證 + cypher KBDB proxy
三件一條鏈(HANDOFF §2/§3b,kbdb-base Phase 9):

A. KBDB MCP 薄殼(9.1):mcp/src/tools/kbdb_data.ts 6 工具
   template/record/query/search,調基本盤 API。鐵律:不給建表/SQL,只 template+slot。

B. MCP self-hosted 認證 401(mcp-account-source §5.5):
   - partner-auth.ts:MULTI_TENANT=false 時 Bearer 明碼直接當 org_namespace,
     繞 KBDB partner 驗證(對齊 cypher 的 opaque-key 模型)。官方 SaaS 行為不變、共用同碼。
   - mcp-setup.ts:把 namespace/api_key 寫進 .mcp.json headers.Authorization。
   - 新增 self-hosted vs SaaS 分支單測(9 tests 綠)。

C. cypher KBDB proxy(9.5)+ CLI 薄殼(9.2):
   - routes/kbdb-proxy.ts 純轉發 /kbdb/* → KBDB 基本盤(KBDB_BASE_URL HTTP fetch,
     不新增 service binding)。讓 CLI(只認證到 cypher)能達獨立 KBDB worker。
   - 租戶隔離:X-Arcrun-API-Key 自動當 owner_id 注入 records/entries(強制覆寫防跨租戶);
     templates 全域共享(虛擬表定義是 schema 非資料)。
   - cli/src/commands/kbdb.ts:acr kbdb template/record/query/search,與 MCP kbdb_* 同能力。
   - kbdb base:entries 加 page_name 過濾(9.3)。

cypher + cli + mcp tsc exit 0。未驗收:端到端需 deploy + KBDB_BASE_URL 可達後實測。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-14 22:12:32 +08:00
uncle6me-web b9bf3ec3d5 fix(mcp,kbdb): LI M3 skills/examples 改打基本盤 /entries(修死 route 假綠)
skills/examples 整條從舊 v3 /blocks /search 改打 KBDB 基本盤 /entries
(entry_type 對應)。5 個已上線 MCP 工具原本對死 route 回 404(假綠),
現修正;sync-registry-to-kbdb.py 改打 /entries idempotent upsert。
誠實降級:基本盤無語義 search → LIKE 關鍵字(embed 模組上線再換回語義)。
順手 gitignore scripts/__pycache__/。

對應 kbdb-base tasks 9.4 / llm-interface M3.2/M3.4。mcp + kbdb tsc exit 0。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-14 22:12:11 +08:00
uncle6me-web ef1f789525 docs(readme): acr update 補 --force flag + 未變動跳過說明(CLI 1.3.7/1.3.8)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-13 16:02:05 +08:00
uncle6me-web e5b1ce5420 chore(release): cli 1.3.8 版本檔同步 2026-06-13 14:55:08 +08:00
uncle6me-web b778086f4a perf(self-hosted): acr update 共享一次 install 取代 23 個 worker 各裝 324MB
壓測 2026-06-12 真因:每個 worker 各 pnpm install ~324MB node_modules(23× 重複,
全是 hono+wrangler)→ 好幾分鐘。改成 tarball root 裝一次(hono+wrangler+tier2 額外
zod/mcp-sdk/yaml),各 worker 靠 node 往上 resolve(dry-run 驗證 tier1+tier2 都 bundle 成功)。
207MB×1 取代 324MB×23。疊加 manifest 跳過 → 第二次 update 近乎瞬間。
共享失敗自動退回各 worker 自裝(不破壞既有路徑)。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-13 14:45:30 +08:00
uncle6me-web f336fb2fca chore(release): cli 1.3.7 版本檔同步 2026-06-13 13:40:34 +08:00
uncle6me-web b44adda6d2 feat(self-hosted): acr update 內容指紋跳過未變動 worker(22 個沒變的不再白跑)+ --force 強制全部重部
壓測 2026-06-12:22/23 成功後重跑仍全部 pnpm install + wrangler deploy。
manifest 存 ~/.arcrun/deploy-manifest.json,指紋含注入後內容+accountId(換帳號/KV 自動重部),
只在成功後記錄(失敗者下次必重試)。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-13 13:35:15 +08:00
uncle6me-web 5a8d27673b chore(release): cli 1.3.6 版本檔同步
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-12 23:30:25 +08:00
uncle6me-web 6e92ca0372 fix(self-hosted): acr update 10/23 部署失敗根因——pnpm 目錄補 commit pnpm-workspace.yaml(ERR_PNPM_IGNORED_BUILDS)+ CLI 失敗帶 stderr 尾段
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-12 23:25:46 +08:00
uncle6me-web 38a3cccbee chore(release): cli package-lock 1.3.5 版本同步
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-12 21:45:39 +08:00
uncle6me-web b17d0080ee chore(release): cli 1.3.5 + local-deploy.sh 全形字緊鄰 $VAR 的 set -u crash 修復(同 8346596 根因)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-12 20:19:52 +08:00
uncle6me-web 35cdda7061 fix(cli): acr update 假綠與沉默修復——部署部分失敗必須印出、deploy 迴圈逐 worker 進度、migrate 錯誤印 server 回應
- update.ts: result.message 含失敗清單時不再被「✓ 部署完成」蓋掉(404 重跑 3 次找不到根因的元兇)
- update.ts: migrate-cron-index 非 2xx 印 response body 前 200 字 + 404/500 含義提示
- deploy.ts: downloadAndDeploy 逐 worker 印 [i/N] 進度(原本 20+ worker 靜默部署像當機)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-11 08:04:22 +08:00
uncle6me-web 1af7655ac6 fix(mcp-self-hosted): KBDB binding 指向不存在的 service + kbdb worker 未部署
self-hosted MCP failed 根因(壓測 2026-06-10):
1. mcp/wrangler.toml: KBDB service binding 綁 'inkstone-kbdb-api'(全 repo 僅此一處,
   不存在的舊名)→ 改成實際 worker 名 'arcrun-kbdb'(官方+self-hosted 共用)。
2. deploy.ts: self-hosted 部署清單漏了 kbdb worker(只有 cypher/registry/mcp)→
   D1 arcrun-kbdb 有建但 worker 本體沒部署 → KBDB binding 指向不存在 service →
   partnerAuthMiddleware 每個 MCP 認證請求都 throw。加 'kbdb' 進部署清單。
D1 id 注入已涵蓋(injectWranglerConfig 跑 tier2,kbdb 現在在 tier2)。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-11 00:08:25 +08:00
uncle6me-web eba87b9ea9 fix(deploy): release-check 全形括號 set-u crash + pnpm allowBuilds 範本未填
- check-release.sh: 5 處 $VAR( → ${VAR}(,修 set -u unbound var crash
  (全形括號 U+FF09 緊貼變數名被 bash 併入變數名 → 非零退出 → 誤判 git 未同步擋 deploy)
- cypher-executor/ + auth_static_key/ pnpm-workspace.yaml: allowBuilds 改真布林
  (原 'set this to true or false' 範本 → pnpm install 拒絕 → deploy 掛)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-10 23:14:57 +08:00
uncle6me-web 8346596ceb fix(release-check): brace-delimit $VAR before full-width parens (set -u unbound var crash)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-10 17:31:28 +08:00
uncle6me-web 2ff51f00ca chore(cli): acr update 下載/解壓進度提示(chalk gray)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-10 16:46:58 +08:00
uncle6me-web da84425d25 feat(credential-injection): {{credential.X}} 用戶面語法(credential-primitives §8)
壓測 401 根因:{{credential.X}} 系統沒實裝,三條 template 展開路徑都不認
credential. namespace → 注入空值 → 目標 API 401(test_arcrun/5 Haiku 實證)。

修法(design §8,richblack 確認方向 B「讓 {{credential.X}} 真的能用」):
- auth_static_key 加 resolve_credentials action:給 names → WASM 內 kv_get +
  crypto_decrypt → 回明文 map(不查 recipe、缺則誠實報錯)
- auth-dispatcher 加 resolveCredentialRefs:遞迴偵測 {{credential.X}} → 交 WASM
  解密 → 回填(無 ref 則零開銷不打 WASM)
- graph-executor 在 node.data interpolate 後呼叫,不碰 ENCRYPTION_KEY(rule 02 §2.2)

解密全程在 WASM,TS 只偵測+回填。tinygo build OK + tsc 0 + §2.2 自檢綠。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-10 13:55:31 +08:00
uncle6me-web 465c505000 fix(execution-truth): 修系統對 401 假綠根因 + acr run self-hosted + D1-in-update
Haiku 自主壓測(test_arcrun/5)暴露的真 bug,逐一修復:

1. 假綠根因:http_request host function 丟掉 HTTP status code(main.go:112 架構債)
   → 非 2xx(如 Notion 401)被判 success → 引擎自己對失敗報成功。
   修:host fn 非 2xx 回 {error,status,body} envelope,既有判定鏈正確識別。
   http_request/claude_api/kbdb_upsert_block/km_writer 已修(4 worker deploy);
   auth_service_account 自有 OAuth 判定不套。

2. acr run self-hosted:原一律走 /webhooks/<name>(需先 push)→ 沒 push 回 404 純文字
   → res.json() 爆假錯誤。修:本機有 YAML 走玩法一 /cypher/execute 直接執行(三模式一致)
   + res.ok 擋非 2xx + findWorkflowYaml 容忍 .yaml 副檔名。

3. D1-in-update:D1 只在 init 建一次,update 漏建 → token 補權限後無冪等補建路徑。
   修:update 也 ensureD1Database(已驗證 D1 建起 count:1)。

4. CF token 教學漏 D1:llms.txt/.env.example 加「Account/D1/Edit」必勾 + init/preflight
   訊息指明 token 缺 D1 權限的修法。

CLI 1.3.4 publish。Haiku 壓測結論:onboarding 治好(裝+init 沒跳過、建 recipe 不建零件),
但仍會假綠(curl 繞過/D1 沒建謊報)→ 印證執行真相要系統能驗、不信 AI 自報。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-09 22:12:09 +08:00
uncle6me-web c152f5fc1d feat(onboarding+kbdb): 8.P0 cron 止血 + §7.8 onboarding + .env.example 範本
kbdb-base 8.P0:scheduled.ts cron 每分鐘 KV list → 單一 key get(lib/cron-index.ts);
  webhooks-named 維護單 key + 一次性 migrate-cron-index;acr update 自動遷移。1440 list/日 → 0。

self-hosted-init §7.8 onboarding:
  P0 init 偵測+裝完驗收(lib/preflight.ts,pip 式,冪等)
  P1 acr whoami(+--json)+ MCP arcrun_whoami(AI 不繞 CLI 猜帳號)
  P2 mcp-setup 寫完印「請重啟 client」
  P3(部分)repo .env.example 範本(每格白話說明、值留空)+ llms.txt 教 AI 幫用戶 cp 建 .env

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-09 19:15:51 +08:00
uncle6me-web c5d8924fb2 feat(harness): 冷啟動環境強制檢查(exit 2 擋住)
- pre-cold-startup-check.sh:5 項環境檢查(config / api_key / .mcp.json / acr / D1)
- init.ts:init 末尾調用 hook,失敗直接 exit 2
- 機制強制,不靠提醒 — 環境檢查失敗用戶必須修復後重跑 acr init

解決 Haiku 假綠問題(mistakes.md §11 對應修復)

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-06-08 15:40:04 +08:00
uncle6me-web 9438d1f351 chore: 完整 wiki 系統 + docs 文件遷移 + test_arcrun/4 任務框架
- docs/ 6 層分類結構建立(vision/architecture/specs/guides/records/user)
- .agents/specs/ 遷移到 docs/3-specs/,已清空原位置
- .claude/wiki/ 核心文檔:mistakes.md / decisions-summary.md / status.md / INDEX.md
- CLAUDE.md 更新:SDD 路徑改為 docs/3-specs/,加 wiki 讀取順序表
- test_arcrun/4/ 新建任務框架(TASK/PROMPT/異常記錄模板)
- 長期記憶系統就緒,防止 LLM 遺忘決策和重複錯誤

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-06-08 15:36:22 +08:00
uncle6me-web d617793325 chore(cli): bump 1.3.3 (MCP self-hosted discovery) 2026-06-08 13:45:05 +08:00
uncle6me-web d84d6fc0ec fix(mcp): POST /mcp route (not /mcp/mcp)
basePath = '/mcp' + app.post("/") = POST /mcp
之前是 app.post("/mcp") 導致 /mcp/mcp 雙層,發請求到 /mcp 只得 404。

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-06-08 13:44:33 +08:00
uncle6me-web 2b46bea764 fix(cli): MCP self-hosted discovery + config setup
- deploy.ts: discoverWorkerDirs 掃 mcp worker + 回傳 mcpUrl(對內 /mcp 端點)
- init.ts: initSelfHosted 寫入 config.mcp_url + 重跑 cmdMcpSetup
- config.ts: DEFAULT_MCP_URL 補 /mcp 後綴

Fixes mcp-account-source §3:self-hosted .mcp.json 必須指自己的 mcp worker,避免連官方失敗。

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-06-08 13:23:02 +08:00
uncle6me-web 5daeede45f chore(cli): package-lock 版本同步 1.3.2
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-07 16:34:51 +08:00
uncle6me-web 36d7492464 fix(kbdb): 填官方 prod D1 database_id(arcrun-kbdb)+ cli bump 1.3.2
kbdb/wrangler.toml placeholder → 官方 D1 id(self-hosted deploy.ts 注入會覆蓋,安全)。
deploy --all 時 cli 自動 bump 1.3.1→1.3.2 + CHANGELOG。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-07 16:34:30 +08:00
uncle6me-web ad5a83d3d8 merge: kbdb recipe 公庫/私庫雙向機制 + UUID 身份(壓測前)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-07 16:21:01 +08:00
98 changed files with 6867 additions and 414 deletions
@@ -0,0 +1,8 @@
allowBuilds:
esbuild: true
sharp: true
workerd: true
onlyBuiltDependencies:
- esbuild
- sharp
- workerd
Binary file not shown.
+898
View File
@@ -0,0 +1,898 @@
lockfileVersion: '9.0'
settings:
autoInstallPeers: true
excludeLinksFromLockfile: false
importers:
.:
dependencies:
hono:
specifier: ^4.7.0
version: 4.12.25
devDependencies:
'@cloudflare/workers-types':
specifier: ^4.20250408.0
version: 4.20260609.1
typescript:
specifier: ^5.4.0
version: 5.9.3
wrangler:
specifier: ^4.0.0
version: 4.98.0(@cloudflare/workers-types@4.20260609.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.20260603.1':
resolution: {integrity: sha512-cEXDWu6V3ZrpmwWkM4OJE9AeXjdAgOY5rh8EHhcBVCuP5rxnzUbPzLtrVOHx0UUUAcCrFq0Xsa6mZKL1VUZsKQ==}
engines: {node: '>=16'}
cpu: [x64]
os: [darwin]
'@cloudflare/workerd-darwin-arm64@1.20260603.1':
resolution: {integrity: sha512-uBPK4LaWJNbbCYwPnUAehlHbbVulhVZPZsdcAhBPfZhHb3QAuAEPAQepO/P67R3V6Cni4YGx1fLbL8A5wwoaNA==}
engines: {node: '>=16'}
cpu: [arm64]
os: [darwin]
'@cloudflare/workerd-linux-64@1.20260603.1':
resolution: {integrity: sha512-ht9l6/8Tk7Rp6kA4S9oFZ4X8u0VjnnFdmU/6B3fnABYKREYTKh2RdOqXqXxcp5eNJseireKnWik/hQOPK1CutQ==}
engines: {node: '>=16'}
cpu: [x64]
os: [linux]
'@cloudflare/workerd-linux-arm64@1.20260603.1':
resolution: {integrity: sha512-LJZ6x00rAjSrobV4m0ZW0TpH5ilBbKcWBzlH+y+KOUsIE/CpTuhAzKV43TbSnFLRX5+jrWKiz2v0hO91lPXy6A==}
engines: {node: '>=16'}
cpu: [arm64]
os: [linux]
'@cloudflare/workerd-windows-64@1.20260603.1':
resolution: {integrity: sha512-DvwqkXMAJRPoDN4PxapAwhlz/6ouD+6R1ttbAEK3cWD/QBvFF5STx7Ds/9Irf+rBly3np3uHWkeX+wZnNFEuzA==}
engines: {node: '>=16'}
cpu: [x64]
os: [win32]
'@cloudflare/workers-types@4.20260609.1':
resolution: {integrity: sha512-krGHtwSApCFBjTe1NTx/TFQ0P5i/bHGQOqCPnCLssb8rOKaAG4JkPFJZsossr0z/ZTMnpP2Tid5jWju+/i0hCA==}
'@cspotcode/source-map-support@0.8.1':
resolution: {integrity: sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==}
engines: {node: '>=12'}
'@emnapi/runtime@1.11.0':
resolution: {integrity: sha512-55coeOFKHv1ywEcUXJtWU5f+Jr/W5tZDvZig8DLKSwUN1JpROQ4rk/SNOQiFWmaR/VKF4zuFyW1B8JduOSv6Pg==}
'@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.16':
resolution: {integrity: sha512-yNm/fYEcnpRjYduLMaddTK9XKYil6xB88+qFg79ZdZhHu1PadfoQmFW7pVTx7FZqMBNcUuThiAhxhENgtAO2/w==}
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.25:
resolution: {integrity: sha512-2NFaIyNVgJmBs/ecmtGzlmluTFs5cHEWGTdu0t1HBwYzoGXOL5nUQBRMXsXWla5i4KkG//QMzVP88m1+I3fdAQ==}
engines: {node: '>=16.9.0'}
kleur@4.1.5:
resolution: {integrity: sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==}
engines: {node: '>=6'}
miniflare@4.20260603.0:
resolution: {integrity: sha512-+kMQYB82gC8MPOuojHur3icQsUeZUEJ+Sphuo5rVC3Ri9txBLAW/mH33b9OVrpmkogQeaaqPS4tPtugJZhk5Kw==}
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.3:
resolution: {integrity: sha512-wnilbGyMxzbY7dNOl7jpKbLSjcfeweJWU5j4+u5qW+6/wuGD9KzIGOyZnQVSBM9E7DtWaaH3CyHkppYrKYoxwg==}
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.20260603.1:
resolution: {integrity: sha512-NPcbhI1++CS+fnELyXtsIR52en+5kwr/OrKeiQeYXGy10HxmPdsQBv9N+DU7hJIOOmBHhOGAAsoGDjyiQ2YCaA==}
engines: {node: '>=16'}
hasBin: true
wrangler@4.98.0:
resolution: {integrity: sha512-cXfFUuF4rMIvE0hiMnXjEAB27ERryaCgquBJdUoPIjFzYYE1rbRdMUkEdQ18qDPUtsPvhJdqxLntixT9OfSzQw==}
engines: {node: '>=22.0.0'}
hasBin: true
peerDependencies:
'@cloudflare/workers-types': ^4.20260603.1
peerDependenciesMeta:
'@cloudflare/workers-types':
optional: true
ws@8.20.1:
resolution: {integrity: sha512-It4dO0K5v//JtTXuPkfEOaI3uUN87iYPnqo/ZzqCoG3g8uhA66QUMs/SrM0YK7/NAu+r4LMh/9dq2A7k+rHs+w==}
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.20260603.1)':
dependencies:
unenv: 2.0.0-rc.24
optionalDependencies:
workerd: 1.20260603.1
'@cloudflare/workerd-darwin-64@1.20260603.1':
optional: true
'@cloudflare/workerd-darwin-arm64@1.20260603.1':
optional: true
'@cloudflare/workerd-linux-64@1.20260603.1':
optional: true
'@cloudflare/workerd-linux-arm64@1.20260603.1':
optional: true
'@cloudflare/workerd-windows-64@1.20260603.1':
optional: true
'@cloudflare/workers-types@4.20260609.1': {}
'@cspotcode/source-map-support@0.8.1':
dependencies:
'@jridgewell/trace-mapping': 0.3.9
'@emnapi/runtime@1.11.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.11.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.16': {}
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.25: {}
kleur@4.1.5: {}
miniflare@4.20260603.0:
dependencies:
'@cspotcode/source-map-support': 0.8.1
sharp: 0.34.5
undici: 7.24.8
workerd: 1.20260603.1
ws: 8.20.1
youch: 4.1.0-beta.10
transitivePeerDependencies:
- bufferutil
- utf-8-validate
path-to-regexp@6.3.0: {}
pathe@2.0.3: {}
semver@7.8.3: {}
sharp@0.34.5:
dependencies:
'@img/colour': 1.1.0
detect-libc: 2.1.2
semver: 7.8.3
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.20260603.1:
optionalDependencies:
'@cloudflare/workerd-darwin-64': 1.20260603.1
'@cloudflare/workerd-darwin-arm64': 1.20260603.1
'@cloudflare/workerd-linux-64': 1.20260603.1
'@cloudflare/workerd-linux-arm64': 1.20260603.1
'@cloudflare/workerd-windows-64': 1.20260603.1
wrangler@4.98.0(@cloudflare/workers-types@4.20260609.1):
dependencies:
'@cloudflare/kv-asset-handler': 0.5.0
'@cloudflare/unenv-preset': 2.16.1(unenv@2.0.0-rc.24)(workerd@1.20260603.1)
blake3-wasm: 2.1.5
esbuild: 0.27.3
miniflare: 4.20260603.0
path-to-regexp: 6.3.0
unenv: 2.0.0-rc.24
workerd: 1.20260603.1
optionalDependencies:
'@cloudflare/workers-types': 4.20260609.1
fsevents: 2.3.3
transitivePeerDependencies:
- bufferutil
- utf-8-validate
ws@8.20.1: {}
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.16
cookie: 1.1.1
youch-core: 0.3.3
@@ -0,0 +1,8 @@
allowBuilds:
esbuild: true
sharp: true
workerd: true
onlyBuiltDependencies:
- esbuild
- sharp
- workerd
+7 -1
View File
@@ -72,7 +72,13 @@ async function runWasm(input: unknown): Promise<unknown> {
init.body = body;
}
const res = await fetch(url, init);
return await res.text();
const text = await res.text();
// 修架構債(同 http_request):非 2xx 包成帶 "error" key 的 envelope
// 讓 WASM 端既有的 error 判定正確識別失敗(原本只回 body 丟掉 status → 4xx 被判 success)。
if (!res.ok) {
return JSON.stringify({ error: `HTTP ${res.status}`, status: res.status, body: text });
}
return text;
},
};
@@ -0,0 +1,8 @@
allowBuilds:
esbuild: true
sharp: true
workerd: true
onlyBuiltDependencies:
- esbuild
- sharp
- workerd
@@ -0,0 +1,8 @@
allowBuilds:
esbuild: true
sharp: true
workerd: true
onlyBuiltDependencies:
- esbuild
- sharp
- workerd
+13 -1
View File
@@ -61,7 +61,19 @@ async function runWasm(input: unknown): Promise<unknown> {
init.body = body;
}
const res = await fetch(url, init);
return await res.text();
const text = await res.text();
// 修架構債(main.go:112):host function 原本只回 body,丟掉 HTTP status code
// 導致 4xx/5xx(如 Notion 401)被零件判成 success → 引擎對失敗報告成功(系統假綠根因)。
// 修法:非 2xx 包成帶 "error" key 的 envelope,讓所有消費零件既有的 parsed["error"] 判定
// 鏈正確識別失敗(2xx 維持原樣回 body 原文,向後相容不破壞 happy path)。
if (!res.ok) {
return JSON.stringify({
error: `HTTP ${res.status}`,
status: res.status,
body: text,
});
}
return text;
},
};
@@ -0,0 +1,8 @@
allowBuilds:
esbuild: true
sharp: true
workerd: true
onlyBuiltDependencies:
- esbuild
- sharp
- workerd
@@ -55,7 +55,13 @@ async function runWasm(input: unknown): Promise<unknown> {
init.body = body;
}
const res = await fetch(url, init);
return await res.text();
const text = await res.text();
// 修架構債(同 http_request):非 2xx 包成帶 "error" key 的 envelope
// 讓 WASM 端既有的 error 判定正確識別失敗(原本只回 body 丟掉 status → 4xx 被判 success)。
if (!res.ok) {
return JSON.stringify({ error: `HTTP ${res.status}`, status: res.status, body: text });
}
return text;
},
};
@@ -0,0 +1,8 @@
allowBuilds:
esbuild: true
sharp: true
workerd: true
onlyBuiltDependencies:
- esbuild
- sharp
- workerd
+7 -1
View File
@@ -58,7 +58,13 @@ async function runWasm(input: unknown): Promise<unknown> {
init.body = body;
}
const res = await fetch(url, init);
return await res.text();
const text = await res.text();
// 修架構債(同 http_request):非 2xx 包成帶 "error" key 的 envelope
// 讓 WASM 端既有的 error 判定正確識別失敗(原本只回 body 丟掉 status → 4xx 被判 success)。
if (!res.ok) {
return JSON.stringify({ error: `HTTP ${res.status}`, status: res.status, body: text });
}
return text;
},
};
@@ -0,0 +1,8 @@
allowBuilds:
esbuild: true
sharp: true
workerd: true
onlyBuiltDependencies:
- esbuild
- sharp
- workerd
+57
View File
@@ -0,0 +1,57 @@
# ───────────────────────────────────────────────────────────────────────────
# arcrun self-hosted .env 範本
#
# 用法(AI 操盤手會幫你做):把這個檔複製成 .env,然後照下面說明,
# 一格一格把「=」右邊填上。左邊的名稱(KEY)不要改。
# cp .env.example .env
#
# 這個 .env 只放在你自己電腦/專案,已被 .gitignore 排除,不會上傳。
# ───────────────────────────────────────────────────────────────────────────
# ── ① Cloudflare(最基礎,這兩格沒填,下面什麼都跑不了)────────────────────────
#
# arcrun 跑在「你自己的 Cloudflare」上(免費額度即可,不必綁信用卡)。
# 你要先有一個 Cloudflare 帳號,然後拿兩串東西貼回來:
#
# 1) 帳號代碼(Account ID):
# 登入 https://dash.cloudflare.com → 右側欄就有「Account ID」→ 複製貼到下面。
#
# 2) 金鑰(API Token):
# https://dash.cloudflare.com/profile/api-tokens → Create Custom Token →
# 勾三組權限(缺一不可):
# · Account / Workers Scripts / Edit
# · Account / Workers KV Storage / Edit
# · Account / D1 / Edit ← 必勾!arcrun 用 D1 存 workflow/recipe
# 漏勾會在 acr init 建 D1 時報 Authentication error
# → 建立後複製那串 token 貼到下面。(不需要 R2、不需要綁卡,D1 也在免費額度。)
#
CLOUDFLARE_ACCOUNT_ID=
CLOUDFLARE_API_TOKEN=
# ── ② 身份與加密(自架單人用,這兩格你自己決定/保管)──────────────────────────
#
# NAMESPACE:你的資料分區標籤。隨便取個英數小名即可(例:leo、myteam)。
# 這不是密碼,只是用來分隔你的資料。
#
# ENCRYPTION_KEY:你的 credential 加密金鑰,64 個以上的 hex 字元。你自己保管。
# 不會的話,AI 可以幫你產一串:
# node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"
# ⚠️ 這串忘了 = 你之前上傳加密的 credential 就解不開了,請留底。
# (安裝完還要把「同一串」設進你的 worker,acr init 會印確切指令給你跟著做。)
#
NAMESPACE=
ENCRYPTION_KEY=
# ── ③ 各服務的 token(要連哪個服務才填哪個;可之後再加)────────────────────────
#
# 連外部服務(Notion、Gmail、Telegram…)的 token 放這裡,給 AI 幫你
# 透過 `acr creds push` 加密上傳(不會明文留在雲端)。要連什麼就加一行。
#
# 例:連 Notion → 去 https://www.notion.com/my-integrations 建一個 integration、
# 拿它的 token(ntn_… 開頭),把你要讀的 database 分享給這個 integration
# 然後填在下面:
#
# NOTION_INTEGRATION_TOKEN=
+6
View File
@@ -16,6 +16,8 @@ credentials.yaml
~/.arcrun/
.env
.env.*
# 範本(無值,需進 repo 給 self-host 用戶 cp 成 .env)——必須放行
!.env.example
# 任何測試/真實憑證一律不進 repo2026-06-03:曾誤 commit GCP SA 金鑰被 GitHub push protection 擋)
docs/test_credentials/
*.sa.json
@@ -45,3 +47,7 @@ CONTRIBUTING.md
ruvector.db
**/.swarm/
**/ruvector.db
scripts/__pycache__/
# D1 備份/匯出(wrangler d1 export 產物,含整庫全量資料=機敏,絕不 commit)
*.sql
backup-*.sql
+8 -1
View File
@@ -76,9 +76,15 @@ arcrun 只有兩種東西,分清楚就不會做歪:
- recipe = `http_request` + 一組固定設定(endpoint / method / headers / body 模板)。
- 要打 Notion / Slack / 你自己的 API**寫一個 recipe**,不是做一個零件。
- recipe 是純文字、不用 deploy、改一次零成本。AI 讀得懂 API 文件就能幫你組出 recipe。
- **recipe 是公共資產,發現缺的就做一個投稿。** `acr recipe push` = 投稿到公共庫(公開/私有只是屬性,不是兩條路)。沒有現成 recipe 打你要的 API?**自己寫一個 push 上去**——這是被鼓勵的,不必問人,下一個人也能用。
> **一句話判準**:要打一個固定的外部 endpoint → **recipe**。要做流程控制 / 資料處理 / 通用 HTTP → 用既有**零件**。其他大部分情況 → 直接寫**工作流**把它們串起來。
### 兩條容易誤解的紅線(AI 操盤手特別注意)
- **缺一個能力 → 補 API,不准用 recipe / 多步工作流拼裝。** 口訣:「這段邏輯換個介面(CLI→MCP)要不要重寫?要 → 它是『能力』,該長在 API。」例:缺 `upsert` 就去 API 加 `upsert` endpoint,**不要**在工作流裡拼「先查、沒有再建」、也不要寫 recipe 假裝補上。recipe 只負責「打一個固定外部 API」,不是用來補 arcrun 自己缺的能力。
- **已經有自製零件的,讓它退場。** `claude_api` 之類「工作流回頭叫 LLM」→ 刪掉,AI 判斷由操盤的 CC 自己做;`kbdb_*` 資料存取 → 改走 `acr kbdb` / `kbdb_*` MCP 工具;純打固定外部 API 的假零件 → 改寫成 recipe 投稿。
---
## 快速開始(Self-hosted
@@ -304,7 +310,8 @@ acr list 列出已部署的 workflow
acr logs <name> 查看執行記錄
acr mcp-setup 在當前資料夾寫 .mcp.json,讓 Claude Code 連對的 arcrun MCP
acr config --where 顯示目前生效設定與來源層(避免在錯資料夾用錯帳號)
acr update self-hosted:拉新版零件/引擎並重新部署到你的 CF
acr update self-hosted:拉新版零件/引擎並重新部署(未變動的 worker 自動跳過)
acr update --force 強制重部所有 worker(忽略未變動跳過快取)
```
> 給 AI 操盤手:開始前讀 `.claude/rules/06-mindset.md`(或 arcrun-mindset Skill)——它說明 arcrun 的世界觀(工作流是 default、零件稀有且不自製、一切外部 API 皆 recipe),讓你一開始就走在正路上。
-61
View File
@@ -1,61 +0,0 @@
# RELEASE-CHECKLIST — 出貨清單(沒有 GitHub Actions,每個 target 分開推,照順序不漏)
> 為什麼要這份:沒有 CI,deploy 是「一個個分開」的動作(git / CF workers / npm CLI)。
> 漏任一步就會「有些新有些舊」——壓測踩過兩次:
> - 第一次:CLI 改了但 npm 沒發 → 用戶 npm 裝到舊 CLI。
> - 第二次(階段 6):cypher 改了但**沒推 main** → `acr init` 從 origin/main codeload 抓到**舊 worker** → 薄殼打不存在的 APIseed 404)。
>
> 核心鐵則:**self-hosted `acr init` 從 `origin/main` 抓 worker 源。所以「git push main」必須在「部署」之前。**
> 順序錯了 = deploy 出去的 prod 是新的,但 self-hosted 用戶裝到的是舊的。
---
## 正確順序(照做不會忘)
### 0. 改完 code,先驗證
- [ ] 三端 typecheck 綠:`cd cli && npx tsc --noEmit``cd cypher-executor && npx tsc --noEmit``cd mcp && npx tsc --noEmit`
- [ ] 動到的 .sh`bash -n scripts/<檔>.sh`
### 1. ⬆️ 先 git commit + push**必須在 deploy 之前**
- [ ] `git add -A`(確認 `.env` / secret 沒被加:`git diff --cached --name-only | grep -iE '\.env|secret|token'` 應空)
- [ ] `git commit -m "..."`
- [ ] `git push origin main`
- 理由:self-hosted 從 `origin/main` codeload 抓 worker。沒先 push → 用戶抓到舊碼。
### 2. ✅ 跑出貨前檢查(會擋住「git 沒同步」)
- [ ] `bash scripts/check-release.sh` → 必須全綠(含「0. Git 同步」段)。
- 紅燈「領先 origin/main N commit 未 push」= 回步驟 1。
- 此腳本 git 未同步會 `exit 1`,是 deploy 的前置閘。
### 3. 🚀 deployworker + CLI npm
- [ ] Node ≥ 20(本機若預設舊版:`export PATH="$HOME/.nvm/versions/node/v22.21.0/bin:$PATH"`
- [ ] `bash scripts/local-deploy.sh --all`(或不帶 `--all` 只 deploy diff
- 此腳本**會先自動跑步驟 2 的 git 閘**;未過直接拒絕 deploy(要強推設 `SKIP_GIT_CHECK=true`,自負風險)。
- worker 走 `wrangler deploy`CLI 走 `npm publish`(版本未 bump 會自動 patch +1 + 寫 CHANGELOG)。
- npm publish 需 `npm login``.env``NPM_API_TOKEN`authToken)。
### 4. 🔁 deploy 後線上驗證(確認新碼真的上去了)
- [ ] `curl https://cypher.arcrun.dev/health` → 200
- [ ] 改了 cypher 路由時,**實際打那條新路由**確認存在(例:`curl -X POST https://cypher.arcrun.dev/init/seed``curl https://cypher.arcrun.dev/recipes` 應非空)。
← 這步就是階段 6 的教訓:別只看「部署成功」,要打新端點確認。
- [ ] 改了 CLI`npm view arcrun version` == `cli/package.json` version。
- [ ] landing 有改:確認 arcrun.dev 更新。
### 5. 📣 通知 / 收尾
- [ ] 若是回應壓測:到壓測報告加「開發者回覆」+ 請壓測者重跑。
---
## 一眼對照表:每個 target 怎麼推、誰依賴它
| Target | 推法 | 誰依賴「它在 origin/main」 |
|---|---|---|
| **git origin/main** | `git push origin main` | **self-hosted `acr init` codeload 抓這裡的 worker 源** → 必須最先 |
| CF workers26 個含 mcp | `local-deploy.sh`wrangler deploy | 平台 prodself-hosted 自己 deploy |
| CLInpm `arcrun` | `local-deploy.sh` 第 6 段 / `cd cli && npm publish` | 用戶 `npm i -g arcrun` |
| landingarcrun.dev | `cd landing && wrangler pages deploy` | 訪客 |
## 常見漏失(自我檢查)
- ❌ 「我 deploy 了 prod cypher 但忘了 push main」→ self-hosted 用戶 init 抓舊碼。**先 push 再 deploy。**
- ❌ 「改了 CLI 但版本沒 bump」→ npm publish 跳過(同版)。`local-deploy.sh` 會自動 bump,但手動 publish 時要記得。
- ❌ 「改了 cypher 路由只看到『部署成功』就收工」→ 要實際 curl 新路由確認(步驟 4)。
+8
View File
@@ -0,0 +1,8 @@
allowBuilds:
esbuild: true
sharp: true
workerd: true
onlyBuiltDependencies:
- esbuild
- sharp
- workerd
+40
View File
@@ -1,5 +1,45 @@
# arcrun CLI Changelog
## 1.3.13 — 2026-06-24
- Merge: fix self-hosted KBDB_BASE_URL injection (issue #2)
## 1.3.12 — 2026-06-15
- fix(kbdb): cypher proxy 補 /kbdb/entries CRUD + report_feedback 改打 /entries
## 1.3.11 — 2026-06-14
- fix(cli): deploy 注入 MULTI_TENANT=false 到 self-hosted worker(修 MCP 401 注入缺口)
## 1.3.10 — 2026-06-14
- fix(cypher): KBDB proxy 指向現役 arcrun-kbdb(舊 fallback kbdb.finally.click 已死)
## 1.3.9 — 2026-06-14
- feat(kbdb,mcp): KBDB 資料層薄殼 + self-hosted MCP 認證 + cypher KBDB proxy
## 1.3.8 — 2026-06-13
- perf(self-hosted): acr update 共享一次 install 取代 23 個 worker 各裝 324MB
## 1.3.7 — 2026-06-13
- feat(self-hosted): acr update 內容指紋跳過未變動 worker(22 個沒變的不再白跑)+ --force 強制全部重部
## 1.3.6 — 2026-06-12
- fix(self-hosted): acr update 10/23 部署失敗根因——pnpm 目錄補 commit pnpm-workspace.yamlERR_PNPM_IGNORED_BUILDS+ CLI 失敗帶 stderr 尾段
## 1.3.5 — 2026-06-11
- fix(cli): acr update 假綠與沉默修復——部署部分失敗必須印出、deploy 迴圈逐 worker 進度、migrate 錯誤印 server 回應
## 1.3.2 — 2026-06-07
- merge: kbdb recipe 公庫/私庫雙向機制 + UUID 身份(壓測前)
> 由 `scripts/local-deploy.sh` 在 deploy 時自動維護(version bump + 此檔記錄)。
> 也可手動編輯補充細節。最新在最上。
+32 -6
View File
@@ -1,13 +1,39 @@
<!-- arcrun-harness:start -->
## 這個專案用 arcrun 做自動化
當需要「串服務 / 排程 / 打 API / 資料處理自動化」時
當需要「串服務 / 排程 / 打 API / 資料處理自動化」時,用 arcrun 工作流,**不要自己寫 Python/Node 一次性腳本**。arcrun 工作流是純文字、可複用、跑在使用者自己的 Cloudflare。
- **用 arcrun 工作流,不要自己寫 Python/Node 一次性腳本。** arcrun 工作流是純文字、可複用、跑在使用者自己的 Cloudflare。選 arcrun 就是不想要每次重刻的腳本。
- **打外部 API → 寫 recipe**`acr recipe push`),不要自刻 HTTP client。recipe = http_request + 固定設定的 YAML,不用部署。
- **先查能力再動手**`acr parts`(看可用零件)、`acr auth-recipe list`(看支援的認證服務)。
- **不要自製零件**WASM component)——零件由 arcrun 維護、走 GitHub PR。你能擴充的是 recipe + 工作流。
- **需要 AI 判斷/轉換時,你自己做**,再呼叫工作流做確定性的下一步。不要在工作流裡放零件回頭呼叫 LLM。
arcrun 只有三種東西,先分清楚就不會做歪:
| 東西 | 是什麼 | 你能做的 |
|---|---|---|
| **工作流(workflow** | 把零件/recipe 串起來的純文字流程 | **預設就寫這個**,自由寫 |
| **recipe** | 打「一個固定外部 API」的設定(http_request + endpoint/header/body 模板) | 自由寫、**而且該投稿**(見下) |
| **零件(component** | WASM 程式(流程控制 / 資料處理 / http_request / auth),固定一小套 | **你不自製**,由維護者管,走 GitHub PR |
> **一句話判準**:打一個固定外部 endpoint → 寫 **recipe**;流程控制 / 資料處理 / 通用 HTTP → 用既有**零件**;其他 → 寫**工作流**串起來。
### 三個最常踩的坑(讀懂這三條,就不會像前人誤解四輪)
**① recipe 是公共資產,發現缺的就自己做一個投稿,不必問人。**
`acr recipe push` = 把 recipe **投稿到公共庫**,不是存私人腳本。公開/私有只是 recipe 的一個屬性(同一身份可有公私版本並存),不是兩條不同的路。
→ 所以「想打某個 API 但沒有現成 recipe」時,**正解是自己寫一個 recipe 並 push 上去**(會 inject credential、push 時幫你檢查打不打得通)。這是被鼓勵的,別誤以為要自用、不上傳。
**② 缺一個能力 → 去補 APIcypher endpoint),不准用 recipe / 多步工作流拼裝出來。**
判準口訣:**「這段邏輯換一個介面(CLI→MCP)要不要重寫?要重寫 → 它是『能力』,該長在 API。」**
- ❌ 缺 `upsert` → 在工作流裡拼「先查、沒有再建」、或寫個 recipe 假裝補上。
- ✅ 缺 `upsert` → 去 API 加一個 `upsert` endpointCLI/MCP/recipe 都呼叫它。
recipe 只負責「打一個固定外部 API」這件單純事;它不是用來補 arcrun 自己缺的能力的。缺能力就回報 / 補在 API,不要繞。
**③ 已經有自製零件(例如 mira 的那幾個)→ 讓它退場,別再加新的。**
你不該自製零件;既有的自製零件要往這三條退場:
- `claude_api` 之類「工作流回頭叫 LLM」→ **刪掉**,需要 AI 判斷時是**你(操盤的 CC)自己做**,再叫工作流做確定性的下一步。arcrun 是 AI 用的工具,不是工具回頭用 AI。
- `kbdb_*` 之類資料存取 → 改走已備好的 **`acr kbdb` 薄殼 / `kbdb_*` MCP 工具**template + record 模型),不要當零件。
- 純粹打某個固定外部 API 的假零件 → **改寫成 recipe** 投稿(見①)。
### 其餘鐵律
- **先查能力再動手**`acr parts`(看可用零件)、`acr auth-recipe list`(看支援的認證服務)、`acr kbdb`(資料存取)。
- **暴露資料要人類同意**:部署對外 webhook / push recipe 會讓東西可被外部呼叫 → 停下來讓使用者明示同意,不替他決定公開。
- **誠實**:沒打通就誠實說(缺 credential 標「未驗收:缺 X」),不假裝成功;完成以 HTTP 2xx / trace 為證,不口頭宣布。
+2 -2
View File
@@ -1,12 +1,12 @@
{
"name": "arcrun",
"version": "1.3.1",
"version": "1.3.13",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "arcrun",
"version": "1.3.1",
"version": "1.3.13",
"license": "MIT",
"dependencies": {
"chalk": "^5.3.0",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "arcrun",
"version": "1.3.1",
"version": "1.3.13",
"description": "AI Workflow CLI for arcrun — self-host WASM-based AI workflows on your own Cloudflare",
"bin": {
"acr": "dist/index.js"
+73 -7
View File
@@ -12,12 +12,12 @@ import { CfAccountClient } from '../lib/cf-api.js';
import {
REQUIRED_KV_NAMESPACES,
SECRET_TARGET_WORKERS,
wranglerAvailable,
downloadAndDeploy,
type DeployContext,
} from '../lib/deploy.js';
import { cmdInstallHarness } from './install-harness.js';
import { cmdMcpSetup } from './mcp-setup.js';
import { detectEnvironment, printPreflight, verifyInstall } from '../lib/preflight.js';
const ARCRUN_REGISTER_URL = 'https://cypher.arcrun.dev/register';
@@ -65,6 +65,22 @@ export async function cmdInit(options: InitOptions): Promise<void> {
} catch (e) {
console.log(chalk.gray(` .mcp.json 略過:${e instanceof Error ? e.message : e};可稍後跑 acr mcp-setup`));
}
// 冷啟動環境檢查(harness:機制強制,不靠提醒)。
// 失敗會 exit 2,用戶必須修復後重跑 acr init。
console.log('');
try {
const { execSync } = await import('child_process');
const hookPath = new URL('../../../.claude/hooks/pre-cold-startup-check.sh', import.meta.url);
execSync(`bash "${hookPath.pathname}"`, { stdio: 'inherit' });
} catch (e) {
if ((e as any)?.status === 2) {
// exit 2 = 環境檢查失敗,停下,不繼續
process.exit(2);
}
// 其他錯誤(hook 不存在等)只警告,不擋 init
console.log(chalk.gray(` (環境檢查略過,可手動跑 bash .claude/hooks/pre-cold-startup-check.sh`));
}
}
async function initLocal(): Promise<void> {
@@ -148,10 +164,13 @@ async function initSelfHosted(
console.log(chalk.gray(' Self-hosted 模式:自動部署整套 arcrun 到你的 Cloudflare 帳號\n'));
console.log(chalk.gray(' 你只需提供 CF Account ID + API Token,其餘 CLI 自動完成。\n'));
// 前置:wranglerCF CLI
if (!wranglerAvailable()) {
console.log(chalk.yellow(' ✗ 找不到 wranglerCloudflare CLI)。'));
console.log(chalk.yellow(' 請先安裝:npm i -g wrangler,然後重新執行 acr init --self-hosted\n'));
// §7.8 P0:偵測先於動作(pip 式)——先看環境有什麼,缺前置就停下給補救指令,
// 不假設齊備直接動手(test_arcrun/4:D1 缺了 AI 跑去讀原始碼自己想辦法的災難)。
const pre = detectEnvironment();
printPreflight('環境偵測(安裝前)', pre.items);
if (pre.fatal) {
console.log(chalk.yellow('\n ✗ 缺少必要前置(見上方 →)。補齊後重新執行 acr init --self-hosted。'));
console.log(chalk.gray(' init 冪等:補好重跑,已就緒的會自動跳過。\n'));
process.exit(1);
}
@@ -209,7 +228,15 @@ async function initSelfHosted(
d1DatabaseId = await cf.ensureD1Database('arcrun-kbdb');
console.log(chalk.green(' ✓'));
} catch (e) {
console.log(chalk.yellow(`\n ⚠ D1 build failed (${e instanceof Error ? e.message : e}); KBDB Base 暫不可用,可 acr update 重試`));
const em = e instanceof Error ? e.message : String(e);
console.log(chalk.yellow(`\n ⚠ D1 build failed (${em})`));
if (/auth/i.test(em)) {
// 最常見根因:CF token 沒勾 D1 權限(KV/Worker 建得起來但 D1 報 Authentication error)。
console.log(chalk.yellow(' 多半是 CF token 缺 D1 權限 → 去 token 補勾「Account / D1 / Edit」'));
console.log(chalk.gray(' 重產 token 填回 .env 後跑 acr update。D1 存 workflow/recipe,沒它後續會受限。'));
} else {
console.log(chalk.gray(' KBDB Base 暫不可用,可 acr update 重試。'));
}
}
// 3. 查 workers.dev subdomaincypher-executor WORKER_SUBDOMAIN 用)
@@ -221,12 +248,27 @@ async function initSelfHosted(
console.log(chalk.yellow(` ⚠ 查 subdomain 失敗(${e instanceof Error ? e.message : e}),稍後可手動補`));
}
// 3.5 語義查詢開關(issue #7 / T2.4):問用戶要不要開(預設關,free-tier 友善)。
// 開 → deploy 建 CF Vectorize index + 注入 binding。關 → base 維持 LIKE keyword,零花費。
// 之後想開:跟 CC 說「幫我開語義查詢」或設 kbdb_embed:true + acr update(不必重 init)。
const embedAns = (await prompt(
rl,
'要開語義查詢嗎?(KBDB 加 AI 向量搜尋;用 CF Vectorize,可能多花費;預設關,之後可隨時開) [y/N]',
)).trim().toLowerCase();
const kbdbEmbed = embedAns === 'y' || embedAns === 'yes';
if (kbdbEmbed) console.log(chalk.gray(' → 已選開語義查詢:部署時會建 Vectorize index。'));
// 4. 下載 repo 部署物(含預編譯 wasm+ 注入 KV id + wrangler deploy 全部 Worker
console.log(chalk.gray('\n → 下載部署物 + 部署 Worker(從 GitHub 拉預編譯 wasm,用你的 CF token 部署)...'));
const deployCtx: DeployContext = { accountId, apiToken: cfApiToken, workerSubdomain, kvNamespaceIds, d1DatabaseId };
// selfHosted: true → deploy 注入 MULTI_TENANT="false"mcp-account-source §5.5,修 MCP 401)。
// init.ts 這條本就是 --self-hosted 分支(config.mode 稍後寫 'self-hosted')。
const deployCtx: DeployContext = { accountId, apiToken: cfApiToken, workerSubdomain, kvNamespaceIds, d1DatabaseId, selfHosted: true, kbdbEmbed };
const deploy = await downloadAndDeploy(deployCtx);
const cypherUrl = deploy.cypherExecutorUrl
?? (workerSubdomain ? `https://arcrun-cypher-executor.${workerSubdomain}.workers.dev` : '');
// self-hosted 自己的 MCP worker URLmcp-account-source §3.mcp.json 指自己,不 fallback 官方)。
const mcpUrl = deploy.mcpUrl
?? (workerSubdomain ? `https://arcrun-mcp.${workerSubdomain}.workers.dev/mcp` : '');
// 誠實回報部署結果;但**不**用「全部成功」字串 gate 後續 seed(壓測 §4.1
// registry 一個無關 worker 失敗就連坐讓 seed 永遠被跳過)。seed 只看 cypher-executor 是否可達。
const deployFullyOk = /全部成功/.test(deploy.message);
@@ -238,19 +280,43 @@ async function initSelfHosted(
cloudflare_account_id: accountId,
cf_api_token: cfApiToken,
cypher_executor_url: cypherUrl,
mcp_url: mcpUrl || undefined, // 指自己的 MCPmcp-account-source §3),無 subdomain 才留空 fallback 官方
webhooks_kv_namespace_id: kvNamespaceIds['WEBHOOKS'],
credentials_kv_namespace_id: kvNamespaceIds['CREDENTIALS_KV'],
multi_tenant: false,
kbdb_embed: kbdbEmbed, // 語義查詢開關(issue #7);存進 config 讓後續 acr update 維持一致
};
saveConfig(config);
createCredentialsYamlIfMissing();
// 6.5 config 寫好後重寫 .mcp.json,讓它指向「自己的」MCPinit 開頭的 cmdMcpSetup 在 config 前跑,
// 那時 mcp_url 還沒設 → 會 fallback 官方;這裡 config 已含自己的 mcp_url,重跑一次蓋成自己的)。
try {
cmdMcpSetup();
} catch (e) {
console.log(chalk.gray(` .mcp.json 重寫略過:${e instanceof Error ? e.message : e}`));
}
// 6. seed recipe(薄殼:呼叫 API 的 /init/seed 一次,由 API 灌 API recipe + auth recipe)。
// 只要 cypher-executor 可達就 seed——不被無關 worker(registry)的失敗連坐(壓測 §4.1)。
if (cypherUrl) {
await callSeedEndpoint(cypherUrl);
}
// §7.8 P0 裝完驗收:實查 CFKV/D1+ 打 cypher /health 確認真就緒,缺哪項明確報哪項
// + 給一鍵補裝指令(不靜默印灰字)。假綠零容忍(mindset §7):看實際狀態,非看 config 寫了沒。
const verify = await verifyInstall({
cf,
requiredKv: REQUIRED_KV_NAMESPACES,
expectD1Name: d1DatabaseId ? 'arcrun-kbdb' : undefined,
cypherUrl,
});
printPreflight('安裝驗收(裝完檢查)', verify.items);
if (!verify.allOk) {
console.log(chalk.yellow('\n ⚠ 部分項目未就緒(見上方 →)。多數可跑 acr update 冪等補裝。'));
console.log(chalk.gray(' (worker 剛部署可能需數十秒生效,可稍候再跑 acr update 重驗。)'));
}
// 結果回報(誠實:部分失敗時明說,不假綠 — mindset §7)
console.log(chalk.green(`\n ✓ Cloudflare 資源就緒(${REQUIRED_KV_NAMESPACES.length} KV,免費額度即可,無需綁卡)`));
console.log(chalk.green(' ✓ 設定寫入 ~/.arcrun/config.yaml'));
+123
View File
@@ -0,0 +1,123 @@
/**
* acr kbdb — KBDB 資料層 CLI 薄殼(kbdb-base Phase 9.2HANDOFF §2
*
* acr kbdb template create <name> --slots a,b,c 建 template(虛擬表定義)
* acr kbdb template list 列出所有 template
* acr kbdb record create <template> --values k=v… 填一筆 record
* acr kbdb record get <record_id> 取單筆 record
* acr kbdb query <template> 列某 template 下本租戶的 records
* acr kbdb search <q> 關鍵字搜尋(本租戶 LIKE)
*
* 薄殼鐵律(rule 07 §5):能力長在基本盤 API,CLI 只做介面轉換 + 暴露,無業務邏輯。
* 透過 cypher 的 KBDB proxy/kbdb/*)達 KBDB——CLI 本來就只認證到 cypherX-Arcrun-API-Key),
* 與 MCP 薄殼(kbdb_data.ts)同一組基本盤能力,差異只來自介面慣例(rule 07 §3.4)。
* KBDB 鐵律:只 template/slot,無建表/SQLproxy 端也不暴露)。
*/
import chalk from 'chalk';
import ora from 'ora';
import { loadConfig, getCypherExecutorUrl } from '../lib/config.js';
/** 取身份 + cypher URL,缺 api_key/namespace 直接退出(與 recipe.ts 一致)。 */
function ctx(): { url: string; apiKey: string } {
const config = loadConfig();
if (!config.api_key) {
console.error(chalk.red('缺少 API Key / namespace,請先執行 acr init(或設 NAMESPACE'));
process.exit(1);
}
return { url: getCypherExecutorUrl(config), apiKey: config.api_key };
}
/** 統一 fetch + JSON 解析 + 失敗印錯(薄殼只暴露,不含業務邏輯)。 */
async function call(method: string, path: string, body?: unknown): Promise<unknown> {
const { url, apiKey } = ctx();
const res = await fetch(`${url}${path}`, {
method,
headers: { 'Content-Type': 'application/json', 'X-Arcrun-API-Key': apiKey },
body: body ? JSON.stringify(body) : undefined,
});
const text = await res.text();
let data: unknown;
try { data = text ? JSON.parse(text) : {}; } catch { data = { raw: text }; }
if (!res.ok) {
console.error(chalk.red(`KBDB 失敗(HTTP ${res.status}):${text || '(空回應)'}`));
process.exit(1);
}
return data;
}
/** 把 ["k=v","x=y"] 轉成 { k:"v", x:"y" }。 */
function parseKeyVals(pairs: string[]): Record<string, string> {
const out: Record<string, string> = {};
for (const p of pairs) {
const i = p.indexOf('=');
if (i < 0) {
console.error(chalk.red(`--values 格式須為 key=value,收到:"${p}"`));
process.exit(1);
}
out[p.slice(0, i)] = p.slice(i + 1);
}
return out;
}
// ── template ───────────────────────────────────────────────────────────────────
export async function cmdKbdbTemplateCreate(name: string, opts: { slots?: string }): Promise<void> {
const slots = (opts.slots ?? '').split(',').map(s => s.trim()).filter(Boolean);
if (slots.length === 0) {
console.error(chalk.red('需要 --slots a,b,c(至少一個欄位名)'));
process.exit(1);
}
const spinner = ora(`建 template "${name}"`).start();
const data = await call('POST', '/kbdb/templates', { name, slots });
spinner.succeed(chalk.green(`template "${name}" 已建(slots: ${slots.join(', ')}`));
console.log(chalk.gray(JSON.stringify(data, null, 2)));
}
export async function cmdKbdbTemplateList(): Promise<void> {
const data = await call('GET', '/kbdb/templates') as { templates?: Array<{ name: string; slots_json?: string }>; count?: number };
const list = data.templates ?? [];
if (list.length === 0) {
console.log(chalk.yellow('(尚無 template,用 acr kbdb template create <name> --slots … 建一個)'));
return;
}
console.log(chalk.bold(`\n ${list.length} 個 template\n`));
for (const t of list) {
console.log(` ${chalk.cyan(t.name)} ${chalk.gray(t.slots_json ?? '')}`);
}
console.log();
}
// ── record ───────────────────────────────────────────────────────────────────
export async function cmdKbdbRecordCreate(template: string, opts: { values?: string[] }): Promise<void> {
const values = parseKeyVals(opts.values ?? []);
if (Object.keys(values).length === 0) {
console.error(chalk.red('需要 --values slot=內容(可重複),如 --values name=Leo --values email=x@y.com'));
process.exit(1);
}
const spinner = ora(`填 recordtemplate "${template}"`).start();
const data = await call('POST', '/kbdb/records', { template, values });
spinner.succeed(chalk.green(`record 已存入 template "${template}"`));
console.log(chalk.gray(JSON.stringify(data, null, 2)));
}
export async function cmdKbdbRecordGet(recordId: string): Promise<void> {
const data = await call('GET', `/kbdb/records/${encodeURIComponent(recordId)}`);
console.log(chalk.gray(JSON.stringify(data, null, 2)));
}
// ── query / search ─────────────────────────────────────────────────────────────
export async function cmdKbdbQuery(template: string): Promise<void> {
const data = await call('GET', `/kbdb/records/by-template/${encodeURIComponent(template)}`) as { records?: unknown[]; count?: number };
const recs = data.records ?? [];
console.log(chalk.bold(`\n template "${template}" 下 ${recs.length} 筆 record(本租戶):\n`));
console.log(chalk.gray(JSON.stringify(recs, null, 2)));
}
export async function cmdKbdbSearch(q: string): Promise<void> {
const data = await call('GET', `/kbdb/search?q=${encodeURIComponent(q)}`) as { entries?: unknown[]; count?: number; mode?: string };
const hits = data.entries ?? [];
console.log(chalk.bold(`\n "${q}" 命中 ${hits.length} 筆(mode: ${data.mode ?? 'keyword'},本租戶):\n`));
console.log(chalk.gray(JSON.stringify(hits, null, 2)));
}
+26 -42
View File
@@ -1,68 +1,52 @@
/**
* acr list — 列出 USER_KV 中所有已上傳的 workflow
* acr list — 列出已部署的 workflow
*
* thin-shell-alignment P1issue #11):原本直連 CF KV 讀 `workflow:` 前綴(CfKvClient),
* 有兩個漂移:① 前綴對不上(部署寫 `{apiKey}:wf:` → 永遠列不到新部署)② 薄殼直碰底層儲存
* self-hosted 用戶需 CF API token 才能 list)。改走 cypher `GET /webhooks/named`KV 源,與
* MCP u6u_list_workflows 同源),薄殼不再直連 KV、key 前綴 bug 自然消失。
*/
import chalk from 'chalk';
import ora from 'ora';
import { loadConfig } from '../lib/config.js';
import { CfKvClient } from '../lib/cf-api.js';
import { loadConfig, getCypherExecutorUrl } from '../lib/config.js';
export async function cmdList(): Promise<void> {
const config = loadConfig();
const executorUrl = getCypherExecutorUrl(config);
if (!config.cloudflare_account_id || !config.cf_api_token) {
console.error(chalk.red('缺少 Cloudflare 設定,請執行 acr init'));
process.exit(1);
}
const namespaceId = config.mode === 'standard'
? config.user_kv_namespace_id!
: config.webhooks_kv_namespace_id!;
if (!namespaceId) {
console.error(chalk.red('缺少 KV Namespace ID,請執行 acr init'));
process.exit(1);
}
const kv = new CfKvClient({
accountId: config.cloudflare_account_id,
namespaceId,
apiToken: config.cf_api_token,
});
const headers: Record<string, string> = { 'Content-Type': 'application/json' };
if (config.api_key) headers['X-Arcrun-API-Key'] = config.api_key;
const spinner = ora('讀取 workflow 清單').start();
try {
const keys = await kv.list('workflow:');
const res = await fetch(`${executorUrl}/webhooks/named`, { method: 'GET', headers });
if (!res.ok) {
spinner.fail(chalk.red(`讀取失敗(HTTP ${res.status}):${(await res.text()).slice(0, 200)}`));
process.exit(1);
}
const data = await res.json() as {
workflows: Array<{ name: string; description?: string; created_at?: string }>;
};
spinner.stop();
if (keys.length === 0) {
const workflows = data.workflows ?? [];
if (workflows.length === 0) {
console.log(chalk.yellow('\n 沒有已部署的 workflow。執行 acr push <workflow.yaml> 部署第一個。\n'));
return;
}
console.log(chalk.bold(`\n 已部署 ${keys.length} 個 workflow\n`));
console.log(chalk.bold(`\n 已部署 ${workflows.length} 個 workflow\n`));
for (const key of keys) {
const name = key.name.replace('workflow:', '');
// 嘗試讀取 workflow 定義取得 created_at
try {
const raw = await kv.get(key.name);
if (raw) {
const def = JSON.parse(raw) as { name: string; description?: string; created_at?: string };
const date = def.created_at ? new Date(def.created_at).toLocaleString('zh-TW') : '未知';
const desc = def.description ? chalk.gray(`${def.description}`) : '';
console.log(`${chalk.cyan(name.padEnd(25))} ${date}${desc}`);
} else {
console.log(`${chalk.cyan(name)}`);
}
} catch {
console.log(`${chalk.cyan(name)}`);
}
for (const wf of workflows) {
const date = wf.created_at ? new Date(wf.created_at).toLocaleString('zh-TW') : '未知';
const desc = wf.description ? chalk.gray(`${wf.description}`) : '';
console.log(`${chalk.cyan(wf.name.padEnd(25))} ${date}${desc}`);
}
console.log('');
} catch (e) {
spinner.fail(chalk.red(`KV 讀取失敗:${e instanceof Error ? e.message : e}`));
spinner.fail(chalk.red(`讀取失敗:${e instanceof Error ? e.message : e}`));
process.exit(1);
}
}
+22 -2
View File
@@ -18,6 +18,7 @@ import { loadConfig, getMcpUrl, DEFAULT_MCP_URL } from '../lib/config.js';
interface McpServerEntry {
type: 'http';
url: string;
headers?: Record<string, string>;
}
interface McpJson {
mcpServers?: Record<string, McpServerEntry | Record<string, unknown>>;
@@ -43,15 +44,34 @@ export function cmdMcpSetup(): void {
}
}
// 身份寫進 .mcp.json headersHANDOFF §3b ②):裸 .mcp.json 不送任何 header
// MCP partner-auth 收不到 token → 一律 401。與 CLI 同一份身份來源(rule 07 §4):
// self-hosted → api_key 欄位存的是 namespace 明碼(config.ts NAMESPACE→api_key),
// MCPMULTI_TENANT=false)把 Bearer 當 org_namespace,與 cypher 的 X-Arcrun-API-Key 對齊。
// standard → api_key 是平台 ak_(多租戶 MCP 仍走 partner 驗證;ak_ 不是 pk_live 的話平台側自理)。
// 不重實作身份解析:直接用 loadConfig() 已解析好的 api_key(薄殼只暴露,不自造邏輯)。
const identity = config.api_key && config.api_key.trim() !== '' ? config.api_key.trim() : undefined;
const entry: McpServerEntry = { type: 'http', url: mcpUrl };
if (identity) entry.headers = { Authorization: `Bearer ${identity}` };
if (!doc.mcpServers || typeof doc.mcpServers !== 'object') doc.mcpServers = {};
doc.mcpServers[SERVER_KEY] = { type: 'http', url: mcpUrl };
doc.mcpServers[SERVER_KEY] = entry;
writeFileSync(target, JSON.stringify(doc, null, 2) + '\n', 'utf8');
console.log(chalk.green(`\n ✓ 已寫入 ${target}`));
console.log(chalk.gray(` arcrun MCP → ${mcpUrl}`));
if (identity) {
console.log(chalk.gray(` 身份 → Authorization: Bearer ${identity.slice(0, 6)}…(${config.mode === 'self-hosted' ? 'self-hosted namespace 明碼' : 'api_key'}`));
} else {
console.log(chalk.yellow(` ⚠ config 無 api_key/namespace → .mcp.json 不帶身份;self-hosted MCP 會 401。先 acr init 或設 NAMESPACE 再重跑。`));
}
if (mcpUrl === DEFAULT_MCP_URL && (!config.mcp_url || config.mcp_url.trim() === '')) {
console.log(chalk.gray(` (用平台預設;要連自己/客戶的 MCP,在 config 設 mcp_url 或 ARCRUN_MCP_URL env,再重跑)`));
}
console.log(chalk.gray(` Claude Code 進此資料夾會自動連這台 MCP。切帳號 = 在對應資料夾重跑 acr mcp-setup。\n`));
console.log(chalk.gray(` Claude Code 進此資料夾會自動連這台 MCP。切帳號 = 在對應資料夾重跑 acr mcp-setup。`));
// §7.8 P2 D3project scope MCP 寫進 .mcp.json 後**不會即時生效**,要重啟 IDE/client 才載入。
// 不提示 → 用戶開了 MCP 工具卻發現用不了,以為壞了(D3 撞牆)。明說「請重啟」引導,不讓人誤判。
console.log(chalk.yellow(`\n ⚠ 請重啟 IDE / Claude Code clientproject scope MCP 才會載入生效。`));
console.log(chalk.gray(` (重啟後若仍未出現 arcrun 工具,確認該 client 已「信任此工作區」。)\n`));
}
+41 -14
View File
@@ -37,14 +37,14 @@ export async function cmdRun(workflowName: string, options: RunOptions): Promise
const headers: Record<string, string> = { 'Content-Type': 'application/json' };
if (config.api_key) headers['X-Arcrun-API-Key'] = config.api_key;
// ── 玩法一:Standard 模式,YAML 在本機,帶著打 /cypher/execute ──────────────
if (config.mode === 'standard' || config.mode === 'local') {
const yamlPath = findWorkflowYaml(workflowName);
if (!yamlPath) {
console.error(chalk.red(`找不到 ${workflowName}.yaml(在目前目錄或子目錄尋找)`));
console.error(chalk.gray('玩法二(已 push 到 KV)請改用 Self-hosted 模式'));
process.exit(1);
}
// ── 玩法一:本機有 YAML → 直接帶著打 /cypher/execute(不需先 push──────────────
// 2026-06-09 修:原本只有 standard/local 走這條,self-hosted 一律走玩法二(/webhooks/<name>
// 需先 push 到 KV)。導致 self-hosted 用戶(如壓測 Haiku)有本機 YAML 卻 acr run 直接打
// /webhooks/<name> → 沒 push = 404 純文字 → res.json() 爆「Unexpected non-whitespace...」假錯誤。
// 正解:只要本機找得到 YAML 就走玩法一直接執行(三模式一致);找不到才退玩法二(按名字打已 push 的)。
const localYamlPath = findWorkflowYaml(workflowName);
if (localYamlPath) {
const yamlPath = localYamlPath;
let workflow;
try {
@@ -71,6 +71,14 @@ export async function cmdRun(workflowName: string, options: RunOptions): Promise
}),
});
// 非 2xx 先擋:直接 res.json() 對 404「Not Found」這種純文字會爆出誤導的
// 「Unexpected non-whitespace character after JSON」。看 res.ok 給人話。
if (!res.ok) {
const body = await res.text();
spinner.fail(chalk.red(`執行失敗(HTTP ${res.status}):${body.slice(0, 200)}`));
process.exit(1);
}
const data = await res.json() as {
success: boolean;
data?: unknown;
@@ -88,15 +96,31 @@ export async function cmdRun(workflowName: string, options: RunOptions): Promise
return;
}
// ── 玩法二:Self-hostedworkflow 已存在 KV,打 /webhooks/{name} ─────────────
// ── 玩法二:本機沒這個 YAML → 按名字打已 push 到 KV 的 workflow ───────────────
// thin-shell-alignment P0issue #11):原打 /webhooks/${name} 是死端點(404)。
// 真端點是 /webhooks/named/:name/triggerwebhooks-named.ts:279X-Arcrun-API-Key header)。
const spinner = ora(`執行 workflow "${workflowName}"`).start();
try {
const res = await fetch(`${executorUrl}/webhooks/${workflowName}`, {
const res = await fetch(`${executorUrl}/webhooks/named/${workflowName}/trigger`, {
method: 'POST',
headers,
body: JSON.stringify(inputContext),
});
// 非 2xx 先擋(同玩法一):404 純文字別硬 res.json()。404 多半是「還沒 push」。
if (!res.ok) {
if (res.status === 404) {
spinner.fail(chalk.red(`找不到已部署的 workflow "${workflowName}"`));
console.error(chalk.gray(` 本機也沒有 ${workflowName}.yaml。請確認:`));
console.error(chalk.gray(` ① 本機有 YAML → 在該檔所在目錄跑 acr run(會直接執行,不需先 push)`));
console.error(chalk.gray(` ② 要跑已部署的 → 先 acr push <file>.yaml 再 acr run <name>`));
} else {
const body = await res.text();
spinner.fail(chalk.red(`執行失敗(HTTP ${res.status}):${body.slice(0, 200)}`));
}
process.exit(1);
}
const data = await res.json() as {
success: boolean;
data?: unknown;
@@ -116,11 +140,14 @@ export async function cmdRun(workflowName: string, options: RunOptions): Promise
// ── helpers ──────────────────────────────────────────────────────────────────
function findWorkflowYaml(name: string): string | null {
// 容忍使用者直接給含副檔名的檔名(acr run foo.yaml)——剝掉再補,避免找成 foo.yaml.yaml。
const base = name.replace(/\.(ya?ml)$/i, '');
const candidates = [
`${name}.yaml`,
`${name}.yml`,
`workflows/${name}.yaml`,
`workflows/${name}.yml`,
name, // 原樣(已含副檔名或本就是路徑)
`${base}.yaml`,
`${base}.yml`,
`workflows/${base}.yaml`,
`workflows/${base}.yml`,
];
for (const p of candidates) {
if (existsSync(p)) return p;
+55 -2
View File
@@ -20,7 +20,7 @@ import {
type DeployContext,
} from '../lib/deploy.js';
export async function cmdUpdate(): Promise<void> {
export async function cmdUpdate(opts: { force?: boolean } = {}): Promise<void> {
const config = loadConfig();
if (config.mode !== 'self-hosted') {
@@ -57,17 +57,48 @@ export async function cmdUpdate(): Promise<void> {
process.exit(1);
}
// D1KBDB Base)冪等補建——之前只在 init 建,update 漏了,導致「init 時 D1 失敗(如 token 缺權限)
// → 補好權限後沒有任何指令會補建 D1」(壓測 2026-06-09:D1 一直建不起來的真根因)。
// update 既是「冪等重部署」就該與 init 一致把 D1 也 ensure 上。
let d1DatabaseId = '';
try {
process.stdout.write(chalk.gray(' → D1 arcrun-kbdb(冪等)...'));
d1DatabaseId = await cf.ensureD1Database('arcrun-kbdb');
console.log(chalk.green(' ✓'));
} catch (e) {
const em = e instanceof Error ? e.message : String(e);
console.log(chalk.yellow(`${em}`));
if (/auth/i.test(em)) {
console.log(chalk.yellow(' CF token 缺 D1 權限 → 補勾「Account / D1 / Edit」重產 token 填回 .env 再 acr update'));
}
}
const ctx: DeployContext = {
accountId: config.cloudflare_account_id,
apiToken: config.cf_api_token,
workerSubdomain: extractSubdomain(config.cypher_executor_url),
kvNamespaceIds,
d1DatabaseId: d1DatabaseId || undefined,
// self-hosted → 注入 MULTI_TENANT="false"mcp-account-source §5.5,修 acr update 部署的 MCP 401)。
// config 源頭:init 寫 multi_tenant:false + mode:'self-hosted'。acr update 只在 self-hosted 跑。
selfHosted: config.mode === 'self-hosted' || config.multi_tenant === false,
// 語義查詢開關(issue #7):config.kbdb_embed:true → 部署建 Vectorize index + 注入 binding。
// 這也是「CC 幫開」的落地路徑:CC 寫 kbdb_embed:true 進 config → acr update redeploy 即生效。
kbdbEmbed: config.kbdb_embed === true,
};
const result = await downloadAndDeploy(ctx);
const result = await downloadAndDeploy(ctx, 'main', { force: opts.force });
if (result.implemented) {
// message 含部分失敗清單(「部署 X/Y 成功,N 失敗:✗ ...」)——必須印出來,
// 否則 worker 失敗被綠勾蓋掉(假綠):cypher 沒部上 → 後面 migrate 打舊 worker 404
// 用戶重跑 N 次都不知道根因(壓測 2026-06-11 實證)。
if (result.message?.includes('失敗')) {
console.log(chalk.yellow(`\n ⚠ 部署部分失敗:`));
console.log(chalk.yellow(' ' + result.message.split('\n').join('\n ')));
} else {
console.log(chalk.green('\n ✓ 部署完成'));
}
// 重跑 seed(薄殼:呼叫 API /init/seed;冪等,覆寫既有)。
// 修壓測 §4.1.3「update 不做 seed,但 init 提示說 update 會重試 seed」的矛盾。
const cypherUrl = config.cypher_executor_url
@@ -84,6 +115,28 @@ export async function cmdUpdate(): Promise<void> {
} catch (e) {
console.log(chalk.yellow(` ⚠ seed 失敗(${e instanceof Error ? e.message : e}`));
}
// kbdb-base 8.P0:一次性把舊的 per-key cron-idx:{apiKey}:{name} 折進單一 cron-idx:_all。
// 部署 8.P0 後既有 cron workflow 若不重 push 會停擺(scheduled 只讀新集中 key)→ 這裡冪等補上。
// 冪等、不刪舊 key、失敗不致命(重跑 acr update 會再試)。
process.stdout.write(chalk.gray(' → 遷移 cron index(舊 per-key → 集中 key,冪等)...'));
try {
const res = await fetch(`${cypherUrl}/webhooks/named/migrate-cron-index`, { method: 'POST' });
const rawText = await res.text();
let body: { success?: boolean; migrated?: number; skipped?: number; errors?: string[] } | null = null;
try { body = JSON.parse(rawText); } catch { /* 非 JSON(如 CF 錯誤頁)→ 用原文 */ }
if (res.ok && body?.success) {
console.log(chalk.green(` ✓ migrated ${body.migrated ?? 0}, skipped ${body.skipped ?? 0}`));
} else {
// 印 server 回的錯誤內容(截前 200 字)——只回 HTTP status 沒人能診斷
// (壓測 2026-06-11404→500 重跑 3 次都看不到根因)。
const detail = (body?.errors?.join('; ') ?? rawText).slice(0, 200);
console.log(chalk.yellow(` ⚠ HTTP ${res.status}${detail ? `${detail}` : ''}`));
console.log(chalk.yellow(' 404 = cypher worker 還是舊版(看上方部署是否有失敗);500 = server 端錯誤(看上行錯誤內容)'));
}
} catch (e) {
console.log(chalk.yellow(` ⚠ cron index 遷移失敗(${e instanceof Error ? e.message : e}`));
}
}
console.log('');
} else {
+77
View File
@@ -0,0 +1,77 @@
/**
* acr whoami — 一眼看「我現在是誰、連哪台、帳號從哪層來」(§7.8 P1,D2 修法)。
*
* D2 根因(self-hosted-init.md §7.8):config 分層**已實作**config.ts),但 AI 不用 CLI 讀帳號、
* 自己 curl 猜全域帳號 URL → 打到錯帳號。治本不是再改 config,是給 AI 一個無腦入口:
* 「問 CLI 拿正確身份,別自己 curl 猜」。與 MCP arcrun_whoami 對齊(薄殼一致,rule 07 §5)。
*
* 與 acr config 區別:config 印完整欄位表(人用、含敏感欄位遮罩);whoami 印精煉摘要
* mode / 連哪台 cypher / 帳號來源層),AI 讀完就知道該用哪個帳號、打哪個 URL,不必再猜。
*
* --json:給 AI/MCP 結構化讀取(薄殼一致)。
*/
import chalk from 'chalk';
import {
loadConfig,
resolveConfigSources,
getCypherExecutorUrl,
getMcpUrl,
activeProjectConfigPath,
type ConfigSource,
} from '../lib/config.js';
const SOURCE_LABEL: Record<ConfigSource, string> = {
env: 'env 變數',
project: '專案層 .arcrun.yaml',
global: '全域 ~/.arcrun/config.yaml',
default: '預設值',
};
/** 帳號身份欄位(self-hosted=api_key 即 NAMESPACE 分區標籤;standard=平台 api_key)。*/
function identitySource(): ConfigSource {
const row = resolveConfigSources().find((r) => r.field === 'api_key');
return row?.source ?? 'default';
}
function maskKey(v?: string): string {
if (!v) return '(未設)';
return v.length > 8 ? `${v.slice(0, 8)}` : v;
}
export async function cmdWhoami(options: { json?: boolean }): Promise<void> {
const config = loadConfig();
const cypherUrl = getCypherExecutorUrl(config);
const mcpUrl = getMcpUrl(config);
const accountSource = identitySource();
const projectPath = activeProjectConfigPath();
if (options.json) {
// 結構化輸出(AI/MCP 對齊用)。敏感值不全印。
console.log(JSON.stringify({
mode: config.mode,
account: maskKey(config.api_key),
account_source: accountSource,
cypher_executor_url: cypherUrl,
mcp_url: mcpUrl,
cloudflare_account_id: config.cloudflare_account_id ?? null,
project_config: projectPath ?? null,
}, null, 2));
return;
}
console.log(chalk.bold('\n arcrun — 目前身份\n'));
console.log(` ${chalk.cyan('模式')} ${config.mode}`);
console.log(` ${chalk.cyan('帳號')} ${maskKey(config.api_key)} ${chalk.gray(`${SOURCE_LABEL[accountSource]}`)}`);
console.log(` ${chalk.cyan('連哪台')} ${cypherUrl}`);
console.log(` ${chalk.cyan('MCP')} ${mcpUrl}`);
if (config.mode === 'self-hosted' && config.cloudflare_account_id) {
console.log(` ${chalk.cyan('CF 帳號')} ${config.cloudflare_account_id}`);
}
console.log('');
if (projectPath) {
console.log(chalk.gray(` 此資料夾用專案層設定:${projectPath}`));
} else {
console.log(chalk.gray(' 此資料夾用全域設定(無 .arcrun.yaml'));
}
console.log(chalk.gray(' → 部署/觸發前用這個帳號,別自己 curl 全域 URL 猜帳號。\n'));
}
+49 -1
View File
@@ -12,6 +12,7 @@ import { fileURLToPath } from 'node:url';
import { dirname, join } from 'node:path';
import { cmdInit } from './commands/init.js';
import { cmdConfig } from './commands/config.js';
import { cmdWhoami } from './commands/whoami.js';
import { cmdCredsPush } from './commands/creds.js';
import { cmdPush } from './commands/push.js';
import { cmdRun } from './commands/run.js';
@@ -23,6 +24,14 @@ import { cmdLogs } from './commands/logs.js';
import { cmdUpdate } from './commands/update.js';
import { cmdInstallHarness } from './commands/install-harness.js';
import { cmdMcpSetup } from './commands/mcp-setup.js';
import {
cmdKbdbTemplateCreate,
cmdKbdbTemplateList,
cmdKbdbRecordCreate,
cmdKbdbRecordGet,
cmdKbdbQuery,
cmdKbdbSearch,
} from './commands/kbdb.js';
import { cmdAuthRecipeList, cmdAuthRecipeInfo, cmdAuthRecipeScaffold } from './commands/auth-recipe.js';
const program = new Command();
@@ -62,6 +71,13 @@ program
.option('--where', '顯示每個設定值來自哪一層(env > 專案層 .arcrun.yaml > 全域)')
.action((options: { where?: boolean }) => cmdConfig(options));
// acr whoami [--json]:一眼看當前身份(mode / 連哪台 cypher / 帳號來源層)。§7.8 P1 D2 修法。
program
.command('whoami')
.description('顯示目前生效的身份(帳號、連哪台 cypher、來源層)——AI 別自己 curl 猜帳號')
.option('--json', '結構化輸出(給 AI / 腳本讀取)')
.action((options: { json?: boolean }) => cmdWhoami(options));
// acr creds push [credentials.yaml]
const credsCmd = program.command('creds').description('Credential 管理');
credsCmd
@@ -152,6 +168,37 @@ authRecipeCmd
.description('輸出 credentials.yaml 範本 + workflow.yaml 使用範例')
.action((service: string) => cmdAuthRecipeScaffold(service));
// acr kbdb — KBDB 資料層薄殼(kbdb-base 9.2,透過 cypher KBDB proxy;與 MCP kbdb_* 同能力)
const kbdbCmd = program.command('kbdb').description('KBDB 資料層(template/record/query/search;不建表、不寫 SQL');
const kbdbTemplateCmd = kbdbCmd.command('template').description('template = 虛擬表定義(name + slots');
kbdbTemplateCmd
.command('create <name>')
.description('建一個 template(虛擬表定義),如 --slots name,email,phone')
.requiredOption('--slots <list>', '欄位名清單,逗號分隔,如 name,email,phone')
.action((name: string, opts: { slots: string }) => cmdKbdbTemplateCreate(name, opts));
kbdbTemplateCmd
.command('list')
.description('列出所有 template')
.action(() => cmdKbdbTemplateList());
const kbdbRecordCmd = kbdbCmd.command('record').description('record = 依 template 填的一筆資料');
kbdbRecordCmd
.command('create <template>')
.description('填一筆 record,如 --values name=Leo --values email=x@y.com(可重複)')
.option('--values <pair...>', 'slot=內容(可重複)')
.action((template: string, opts: { values?: string[] }) => cmdKbdbRecordCreate(template, opts));
kbdbRecordCmd
.command('get <record_id>')
.description('取單筆 record 全文')
.action((recordId: string) => cmdKbdbRecordGet(recordId));
kbdbCmd
.command('query <template>')
.description('列某 template 下本租戶的所有 record')
.action((template: string) => cmdKbdbQuery(template));
kbdbCmd
.command('search <q>')
.description('關鍵字搜尋本租戶內容(LIKE,基本盤)')
.action((q: string) => cmdKbdbSearch(q));
// acr list
program
.command('list')
@@ -168,7 +215,8 @@ program
program
.command('update')
.description('self-hosted:拉新 release 並重新部署到你的 Cloudflare')
.action(() => cmdUpdate());
.option('--force', '強制重部所有 worker(忽略未變動跳過快取)')
.action((opts: { force?: boolean }) => cmdUpdate({ force: opts.force }));
// acr install-harness(把 arcrun 的 CC harness 裝進當前專案)
program
+13 -1
View File
@@ -28,6 +28,12 @@ export interface ArcrunConfig {
// SDD: sdk-and-website/mcp-account-source.md
mcp_url?: string;
multi_tenant?: boolean;
// 語義查詢開關(issue #7 / SDD T2.4self-hosted 從零做)。
// true → deploy 時建 CF Vectorize index 並注入 kbdb worker 的 [[vectorize]]+[ai] binding
// kbdb embed 模組啟用(寫入時對標記 embed 的 entry embed、search 支援 mode=semantic)。
// 未設/false → base 維持 LIKE keywordfree-tier 友善,不建 index、不花費)。
// 開法:設 kbdb_embed:true → redeployacr update)。「CC 幫開」=CC 寫此欄 true + 跑 acr update。
kbdb_embed?: boolean;
// 資料外流警示:本機記住「已同意暴露 / 選擇不再警示」的資源,避免每次 push 重問(§3 首次問記住)。
// key 格式:`{kind}:{resourceName}`(如 "webhook:contacts_lookup" / "recipe:kbdb_get")。
// 注意:這只是 CLI 端 UX(不重問);server 端獨立存法律憑證並強制(防 CLI 被繞過)。
@@ -65,7 +71,8 @@ const ENV_MAP: Record<string, keyof ArcrunConfig> = {
* 平台預設 MCP URLmcp_url 未設時的 fallbackSaaS 用戶用)。
* MCP 搬進 arcrun 主庫後改用 arcrun.dev zonemcp/wrangler.toml route = mcp.arcrun.dev)。
*/
export const DEFAULT_MCP_URL = 'https://mcp.arcrun.dev';
// MCP streamable-http 端點是 /mcp(根路徑 404)。少了 /mcp → client 連線 Failed。
export const DEFAULT_MCP_URL = 'https://mcp.arcrun.dev/mcp';
/**
* 公庫 URLrecipe pull/search/submit-p 的對象,kbdb-base §7.5)。
@@ -159,6 +166,11 @@ function readEnvOverrides(): Partial<ArcrunConfig> {
(out as Record<string, unknown>)[field] = v;
}
}
// bool 開關(issue #7):env 可選覆蓋,'true'/'1' → true。
const embedEnv = process.env.ARCRUN_KBDB_EMBED;
if (embedEnv !== undefined && embedEnv !== '') {
out.kbdb_embed = embedEnv === 'true' || embedEnv === '1';
}
return out;
}
+267 -19
View File
@@ -10,8 +10,58 @@
import { execFileSync } from 'node:child_process';
import { mkdtempSync, readFileSync, writeFileSync, existsSync, readdirSync, statSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { tmpdir, homedir } from 'node:os';
import { join } from 'node:path';
import { createHash } from 'node:crypto';
import chalk from 'chalk';
/** 部署狀態 manifest:記錄上次成功部署每個 worker 的內容指紋(content hash),
* 讓 acr update 跳過未變動的 worker(壓測 2026-06-1222/23 成功後重跑仍全部
* pnpm install + wrangler deploy22 個沒變的白跑)。存 ~/.arcrun/。
* 指紋含 wrangler.toml 注入後的內容 → 換帳號/KV 會變更指紋 → 自動重部,不會誤跳。*/
const MANIFEST_PATH = join(homedir(), '.arcrun', 'deploy-manifest.json');
function loadManifest(): Record<string, string> {
try {
return JSON.parse(readFileSync(MANIFEST_PATH, 'utf8')) as Record<string, string>;
} catch {
return {};
}
}
function saveManifest(m: Record<string, string>): void {
try {
writeFileSync(MANIFEST_PATH, JSON.stringify(m, null, 2));
} catch {
/* manifest 寫失敗不致命:下次全部重部(退化成舊行為,不會錯,只是慢) */
}
}
/** 算一個 worker 目錄的內容指紋:遞迴 hash 所有檔案(排除 node_modules),
* 加上 accountId(換帳號要重部)。檔案路徑相對化後排序 → 跨機器/temp 目錄穩定。*/
function dirContentHash(dir: string, accountId: string): string {
const h = createHash('sha256');
h.update(accountId);
const walk = (d: string, rel: string): void => {
let entries: string[];
try { entries = readdirSync(d).sort(); } catch { return; }
for (const name of entries) {
if (name === 'node_modules' || name === '.git') continue;
const full = join(d, name);
const relPath = rel ? `${rel}/${name}` : name;
let st;
try { st = statSync(full); } catch { continue; }
if (st.isDirectory()) {
walk(full, relPath);
} else {
h.update(relPath);
try { h.update(readFileSync(full)); } catch { /* skip unreadable */ }
}
}
};
walk(dir, '');
return h.digest('hex');
}
/** GitHub repocodeload tarball 來源)。fork 者改這裡或用 ARCRUN_REPO env。
* 注意:repo 名大小寫敏感(codeload 路徑需完全一致)。*/
@@ -48,11 +98,23 @@ export interface DeployContext {
workerSubdomain: string;
kvNamespaceIds: Record<string, string>; // title → id
d1DatabaseId?: string; // KBDB Base D1 (arcrun-kbdb); injected into kbdb wrangler.toml
// self-hosted 單租戶旗標。trueself-hosted)→ 注入 MULTI_TENANT="false" 到 worker [vars]
// 讓 MCP partner-auth 走 namespace 明碼分支(mcp-account-source §5.5)。
// 未設 / false → 不注入(官方 SaaS 多租戶,行為不變)。
selfHosted?: boolean;
// 語義查詢開關(issue #7 / SDD T2.4)。true → 部署前建 CF Vectorize index 並注入 kbdb worker 的
// [[vectorize]]+[ai] binding(取消 wrangler.toml 註解段)→ embed 模組啟用。未設/false → 不建、不注入,
// base 維持 LIKE keywordfree-tier 友善)。
kbdbEmbed?: boolean;
}
/** Vectorize index 名(kbdb embed 模組用)。bge-base-en-v1.5 = 768 維、cosine。 */
export const KBDB_VECTORIZE_INDEX = 'arcrun-kbdb-embed';
export interface DeployResult {
implemented: boolean;
cypherExecutorUrl?: string;
mcpUrl?: string; // self-hosted 自己的 MCP worker URLmcp-account-source §3
message: string;
}
@@ -80,7 +142,11 @@ export function wranglerAvailable(): boolean {
* @param ctx 部署上下文
* @param ref git refbranch / tag),預設 mainacr update 可帶 tag
*/
export async function downloadAndDeploy(ctx: DeployContext, ref = 'main'): Promise<DeployResult> {
export async function downloadAndDeploy(
ctx: DeployContext,
ref = 'main',
opts: { force?: boolean } = {},
): Promise<DeployResult> {
// 1. 下載 + 解壓 codeload tarball
let root: string;
try {
@@ -98,18 +164,89 @@ export async function downloadAndDeploy(ctx: DeployContext, ref = 'main'): Promi
return { implemented: true, message: `部署物中找不到任何 wrangler.tomlroot=${root})。` };
}
// 3. 對每個 worker:注入 KV id+ cypher WORKER_SUBDOMAIN)→ wrangler deploy。tier1 先 tier2 後。
// 2.5 共享依賴:23 個 component worker 的 runtime dep 全是 hono、devDep 全含 wrangler
// 舊版每個 worker 各 install 一份 ~324MB node_modules23× 重複,壓測 2026-06-12 慢的真因)。
// 改成在 tarball root 裝「一次」hono+wranglercomponent 目錄靠 node 往上 resolve(已驗證可行)。
// → 23×4.4s install 變 1×17s。失敗不致命:退回各 worker 自裝(runWranglerDeploy 仍有 fallback)。
let sharedBin = '';
try {
process.stdout.write(chalk.gray(' → 安裝共享部署依賴(一次,取代每個 worker 各裝)...'));
// 含全部 worker 的 runtime depstier1 component 只要 honotier2 cypher/registry/mcp/kbdb
// 另需 zod / @hono/zod-openapi / @modelcontextprotocol/sdk / js-yaml / yaml)→ 全裝 root
// 各 worker 往上 resolveesbuild bundle 找得到。漏一個會讓該 worker deploy 失敗,故寧可多列。
writeFileSync(
join(root, 'package.json'),
JSON.stringify({ name: 'arcrun-deploy-shared', private: true, type: 'module',
dependencies: {
hono: '^4.7.0', wrangler: '^4.0.0', zod: '^3.23.0',
'@hono/zod-openapi': '^0.18.0', '@modelcontextprotocol/sdk': '^1.0.0',
'js-yaml': '^4.1.0', yaml: '^2.4.0',
} }),
);
execFileSync('npm', ['install', '--no-audit', '--no-fund'],
{ cwd: root, stdio: ['ignore', 'ignore', 'pipe'] });
sharedBin = join(root, 'node_modules', '.bin', 'wrangler');
console.log(existsSync(sharedBin) ? chalk.green(' ✓') : chalk.yellow(' ⚠ 退回各 worker 自裝'));
if (!existsSync(sharedBin)) sharedBin = '';
} catch (e) {
const tail = (e as { stderr?: Buffer }).stderr?.toString().trim().split('\n').slice(-2).join(' | ').slice(0, 200) ?? '';
console.log(chalk.yellow(` ⚠ 共享安裝失敗,退回各 worker 自裝${tail ? `${tail}` : ''}`));
}
const failures: string[] = [];
// 2.6 語義查詢(issue #7 / T2.4):開 kbdb_embed → 先確保 Vectorize index 存在(REST,冪等),
// 再由 injectWranglerConfig 取消 kbdb toml 的 [[vectorize]]+[ai] 註解 → embed 模組上線。
// 失敗不致命(收進 failuresbase 仍可部署、維持 keyword)。
if (ctx.kbdbEmbed) {
try {
process.stdout.write(chalk.gray(' → 開語義查詢:確保 Vectorize index 存在...'));
await ensureVectorizeIndex(ctx);
console.log(chalk.green(' ✓'));
} catch (e) {
console.log(chalk.yellow(' ⚠'));
failures.push(`Vectorize index (${KBDB_VECTORIZE_INDEX}): ${e instanceof Error ? e.message : String(e)}`);
}
}
// 3. 對每個 worker:注入 KV id+ cypher WORKER_SUBDOMAIN)→ wrangler deploy。tier1 先 tier2 後。
// 逐 worker 串流進度(每個含 pnpm install + wrangler deploy,沉默會讓人以為卡住——
// 壓測 2026-06-11 richblack 觀察:「D1 ✓」後停很久其實在這個迴圈靜默部署 20+ worker)。
const allDirs = [...tier1, ...tier2];
let deployed = 0;
for (const dir of [...tier1, ...tier2]) {
let skipped = 0;
// 內容指紋 manifest:未變動且上次成功的 worker 跳過(key 用 worker 名,不用 temp 絕對路徑)。
// --force 清空 manifest → 全部重部。
const manifest = opts.force ? {} : loadManifest();
console.log(chalk.gray(` → 部署 ${allDirs.length} 個 worker(未變動者跳過,依序進行)...`));
for (let i = 0; i < allDirs.length; i++) {
const dir = allDirs[i];
const tomlPath = join(dir, 'wrangler.toml');
const label = dir.replace(/^.*\.component-builds\//, '').replace(/^.*\//, '');
process.stdout.write(chalk.gray(` [${i + 1}/${allDirs.length}] ${label} ...`));
try {
injectWranglerConfig(tomlPath, ctx);
runWranglerDeploy(dir, ctx);
deployed++;
} catch (e) {
failures.push(`${dir}: ${e instanceof Error ? e.message : String(e)}`);
// 注入後算指紋:與 manifest 比,相同 = 上次成功部過且內容沒變 → 跳過。
const hash = dirContentHash(dir, ctx.accountId);
if (manifest[label] === hash) {
skipped++;
console.log(chalk.gray(' ⊘ 未變動,跳過'));
continue;
}
runWranglerDeploy(dir, ctx, sharedBin);
manifest[label] = hash; // 只在成功後記錄 → 失敗者下次必重試
saveManifest(manifest);
deployed++;
console.log(chalk.green(' ✓'));
} catch (e) {
delete manifest[label]; // 失敗 → 清掉舊指紋,確保下次重部
saveManifest(manifest);
failures.push(`${dir}: ${e instanceof Error ? e.message : String(e)}`);
console.log(chalk.yellow(' ⚠'));
}
}
if (skipped > 0) {
console.log(chalk.gray(` ${skipped} 個未變動已跳過;要強制全部重部跑 acr update --force`));
}
// 3.5 KBDB Base: D1 建好後套 migration(建三表 + recipe_stat seed)。
@@ -132,11 +269,17 @@ export async function downloadAndDeploy(ctx: DeployContext, ref = 'main'): Promi
const cypherExecutorUrl = ctx.workerSubdomain
? `https://arcrun-cypher-executor.${ctx.workerSubdomain}.workers.dev`
: undefined;
// self-hosted 自己的 MCP worker URLmcp-account-source §3.mcp.json 指自己)。
// 端點是 /mcpstreamable http;根路徑 404)。仿 cypher 用 WORKER_SUBDOMAIN 組。
const mcpUrl = ctx.workerSubdomain
? `https://arcrun-mcp.${ctx.workerSubdomain}.workers.dev/mcp`
: undefined;
if (failures.length > 0) {
return {
implemented: true,
cypherExecutorUrl,
mcpUrl,
message:
`部署 ${deployed}/${tier1.length + tier2.length} 成功,${failures.length} 失敗(誠實回報,未假綠):\n` +
failures.map(f => `${f}`).join('\n'),
@@ -146,6 +289,7 @@ export async function downloadAndDeploy(ctx: DeployContext, ref = 'main'): Promi
return {
implemented: true,
cypherExecutorUrl,
mcpUrl,
message: `部署完成:${deployed} 個 Worker 全部成功。`,
};
}
@@ -174,13 +318,43 @@ async function applyD1Migration(ctx: DeployContext, sql: string): Promise<void>
}
}
/**
* 確保 KBDB embed 用的 Vectorize index 存在(issue #7 / T2.4)。
* REST `POST /accounts/{id}/vectorize/v2/indexes`dimensions=768/metric=cosine,對齊 bge-base-en-v1.5)。
* 冪等:已存在(CF 回「already exists」類錯)視為成功,不報錯。用 init 已驗的 apiToken+accountId。
*/
async function ensureVectorizeIndex(ctx: DeployContext): Promise<void> {
const url = `https://api.cloudflare.com/client/v4/accounts/${ctx.accountId}/vectorize/v2/indexes`;
const res = await fetch(url, {
method: 'POST',
headers: { Authorization: `Bearer ${ctx.apiToken}`, 'Content-Type': 'application/json' },
body: JSON.stringify({
name: KBDB_VECTORIZE_INDEX,
config: { dimensions: 768, metric: 'cosine' },
description: 'arcrun KBDB optional embed module (issue #7)',
}),
signal: AbortSignal.timeout(60_000),
});
if (res.ok) return;
// 冪等:已存在 → 視為成功(CF 回 409 或 errors 含 already exists / duplicate)。
const json = (await res.json().catch(() => null)) as
| { success?: boolean; errors?: Array<{ message?: string; code?: number }> }
| null;
const msg = (json?.errors?.map(e => e.message).filter(Boolean).join('; ') || `HTTP ${res.status}`).toLowerCase();
if (res.status === 409 || /already exists|duplicate|conflict/.test(msg)) return;
throw new Error(msg);
}
/** 下載 codeload tarball 解壓到暫存目錄,回傳解壓出的 repo root 路徑。*/
async function downloadRepoTarball(ref: string): Promise<string> {
const url = `https://codeload.github.com/${ARCRUN_REPO}/tar.gz/${ref}`;
console.log(chalk.gray(` → 從 GitHub 下載最新版本(${ARCRUN_REPO}@${ref},約 1030 秒,視網速)...`));
const res = await fetch(url, { signal: AbortSignal.timeout(120_000) });
if (!res.ok) throw new Error(`codeload HTTP ${res.status}${url}`);
const buf = Buffer.from(await res.arrayBuffer());
const sizeMB = (buf.length / 1024 / 1024).toFixed(1);
console.log(chalk.gray(` → 下載完成(${sizeMB} MB),解壓中...`));
const dir = mkdtempSync(join(tmpdir(), 'arcrun-deploy-'));
const tarPath = join(dir, 'repo.tar.gz');
writeFileSync(tarPath, buf);
@@ -211,7 +385,12 @@ function discoverWorkerDirs(root: string): { tier1: string[]; tier2: string[] }
}
}
}
for (const name of ['cypher-executor', 'registry']) {
// self-hosted 也部署自己的 MCP workermcp-account-source §5ccodeload 主庫即得 MCP
// .mcp.json 指自己的 mcp 而非官方 mcp.arcrun.dev)。
// kbdbMCP 的 partnerAuthMiddleware 透過 KBDB service binding 打 arcrun-kbdb workermcp/wrangler.toml)。
// D1 arcrun-kbdb 已由 init/update 建好,但 worker 本體要一併部署,否則 binding 指向不存在的 service
// → 每個 MCP 認證請求都 throwself-hosted MCP failed 根因,2026-06-10)。
for (const name of ['cypher-executor', 'registry', 'kbdb', 'mcp']) {
const dir = join(root, name);
if (existsSync(join(dir, 'wrangler.toml'))) tier2.push(dir);
}
@@ -262,11 +441,68 @@ function injectWranglerConfig(tomlPath: string, ctx: DeployContext): void {
);
}
// self-hosted:注入 MULTI_TENANT="false" 到 [vars]mcp-account-source §5.5)。
// 修「部署沒注入 → worker c.env.MULTI_TENANT===undefined → MCP 走 partner-key → 401」。
// 只對有 [vars] 的 workermcp / cypher-executor)生效;其餘無 [vars] 的不動。
if (ctx.selfHosted) {
toml = injectMultiTenant(toml);
// self-hosted:把 cypher 的 KBDB_BASE_URL 從官方 arcrun-kbdb.uncle6-me 改成用戶自己帳號的
// arcrun-kbdb.<subdomain>.workers.devissue #2)。比照 database_id / MULTI_TENANT 注入模式。
// 漏這一個 → cypher /kbdb/* fallback 到官方 kbdb workerself-hosted 資料寫進官方庫(隔離破損)。
if (ctx.workerSubdomain) {
toml = toml.replace(
/(KBDB_BASE_URL\s*=\s*")[^"]*(")/,
`$1https://arcrun-kbdb.${ctx.workerSubdomain}.workers.dev$2`,
);
}
}
toml = stripOfficialOnlyBindings(toml);
// 語義查詢(issue #7 / T2.4):開 kbdb_embed → 取消 kbdb toml 的 [[vectorize]]+[ai] 註解段(注入 active binding)。
// **必須在 stripOfficialOnlyBindings 之後**strip 會移除 [ai] 區塊(官方專屬),若先注入會被它清掉。
// 只對含該註解段的 toml= kbdb)生效;其餘 worker toml 無此段,replace 不命中、不動。
// 未開 → 維持註解 → worker env 無 VECTORIZE/AI → embedEnabled()=false → base keyword(不花費)。
if (ctx.kbdbEmbed) {
toml = toml.replace(
/# (\[\[vectorize\]\])\n# (binding = "VECTORIZE")\n# (index_name = "[^"]*")/,
'$1\n$2\n$3',
);
toml = toml.replace(/# (\[ai\])\n# (binding = "AI")/, '$1\n$2');
}
writeFileSync(tomlPath, toml, 'utf8');
}
/**
* self-hosted:確保 worker [vars] 有 `MULTI_TENANT = "false"`。處理三種既有狀態:
* 1. 已有 active `MULTI_TENANT = "..."` → 改成 "false"
* 2. 有註解的 `# MULTI_TENANT = "false"`mcp/cypher toml 預設這樣)→ 取消註解
* 3. 無此行但有 `[vars]` → 在 [vars] header 下一行加進去
* 4. 無 `[vars]`(該 worker 不吃此 var)→ 不動
* 純文字操作,與 WORKER_SUBDOMAIN/KV 注入同層級(mcp-account-source §5.5)。
*/
export function injectMultiTenant(toml: string): string {
// 1. 已有 active 行 → 設 false
if (/^\s*MULTI_TENANT\s*=/m.test(toml)) {
return toml.replace(/^(\s*MULTI_TENANT\s*=\s*")[^"]*(".*)$/m, `$1false$2`);
}
// 2. 註解掉的行 → 取消註解(保留原縮排)
if (/^\s*#\s*MULTI_TENANT\s*=/m.test(toml)) {
return toml.replace(/^(\s*)#\s*(MULTI_TENANT\s*=\s*)"[^"]*"(.*)$/m, `$1$2"false"$3`);
}
// 3. 有 [vars] → 在其後插入
if (/^\s*\[vars\]\s*$/m.test(toml)) {
return toml.replace(
/^(\s*\[vars\]\s*)$/m,
`$1\nMULTI_TENANT = "false" # self-hosted 單租戶(acr update 注入,mcp-account-source §5.5`,
);
}
// 4. 無 [vars] → 不動(該 worker 不用此 var
return toml;
}
/**
* 移除 self-hosted fork 帳號沒有、會導致 wrangler deploy 失敗的官方專屬 TOML 區塊:
* - `[[routes]]`(含 pattern/zone_name):fork 沒有 arcrun.dev zone
@@ -303,22 +539,34 @@ export function stripOfficialOnlyBindings(toml: string): string {
return out.join('\n');
}
/** 在 worker 目錄跑 wrangler deploy(用用戶的 CF token + account)。*/
function runWranglerDeploy(dir: string, ctx: DeployContext): void {
// 先裝依賴(cypher-executor/registry 是 TSwrangler 內建 esbuild bundle 需 node_modules
if (existsSync(join(dir, 'package.json'))) {
/** 在 worker 目錄跑 wrangler deploy(用用戶的 CF token + account)。
* sharedBinroot 共享 wrangler binary 路徑(見 downloadAndDeploy 2.5)。有則用它且**跳過本地 install**
* deps 從 root node_modules 往上 resolve);空字串則退回舊行為(各 worker 自裝)。*/
function runWranglerDeploy(dir: string, ctx: DeployContext, sharedBin = ''): void {
if (!sharedBin && existsSync(join(dir, 'package.json'))) {
// fallback:共享安裝失敗時才走這條,各 worker 自裝
const installer = existsSync(join(dir, 'pnpm-lock.yaml'))
? ['pnpm', 'install', '--frozen-lockfile']
: ['npm', 'install', '--no-audit', '--no-fund'];
execFileSync(installer[0], installer.slice(1), { cwd: dir, stdio: 'ignore' });
runStep(installer[0], installer.slice(1), dir, process.env);
}
execFileSync('wrangler', ['deploy'], {
cwd: dir,
stdio: 'ignore',
env: {
const wranglerCmd = sharedBin || 'wrangler';
runStep(wranglerCmd, ['deploy'], dir, {
...process.env,
CLOUDFLARE_API_TOKEN: ctx.apiToken,
CLOUDFLARE_ACCOUNT_ID: ctx.accountId,
},
});
}
/** 跑一個部署步驟,失敗時把 stderr 尾段帶進錯誤訊息——stdio ignore 會吞掉真因,
* 用戶只看到「Command failed: pnpm install」無從診斷(壓測 2026-06-12
* ERR_PNPM_IGNORED_BUILDS 被吞,10/23 失敗查不到原因)。*/
function runStep(cmd: string, args: string[], dir: string, env: NodeJS.ProcessEnv): void {
try {
execFileSync(cmd, args, { cwd: dir, stdio: ['ignore', 'ignore', 'pipe'], env });
} catch (e) {
const stderr = (e as { stderr?: Buffer }).stderr?.toString().trim() ?? '';
const tail = stderr.split('\n').slice(-3).join(' | ').slice(0, 300);
throw new Error(`${cmd} ${args.join(' ')} 失敗${tail ? `${tail}` : ''}`);
}
}
+138
View File
@@ -0,0 +1,138 @@
/**
* preflight.ts — self-hosted 安裝的「偵測先於動作 + 裝完驗收」(§7.8 P0,pip 式)。
*
* 核心判準(self-hosted-init.md §7.8):
* - **偵測先於動作**init 先檢查各前置(node / wrangler / CF 可達),缺的才裝、有的跳過。
* 不是假設齊備直接動手 → 缺一個就卡(test_arcrun/4 的 D1 大跑去讀原始碼自己想辦法)。
* - **裝完驗收**:部署後逐項確認(KV / D1 / migration / cypher 可達),缺哪項明確報哪項
* + 給一鍵補裝指令。不是靜默印灰字(原本 harness/MCP 失敗只 console.log 灰字,用戶不知道)。
* - **冪等**:重跑檢查後「什麼也沒動」(ensureKvNamespace / ensureD1Database 本就冪等)。
*
* 本檔只做「偵測 + 報告」,不自己建資源(建資源仍走 cf-api 的 ensure*,由 init 編排)。
*/
import { execFileSync } from 'node:child_process';
import chalk from 'chalk';
import type { CfAccountClient } from './cf-api.js';
export interface PreflightItem {
name: string;
ok: boolean;
detail?: string;
/** 缺漏時給用戶的一鍵補救指令(沒有則留空)。*/
fix?: string;
}
/** node 是否可用 + 版本(init 本身是 node 跑的,能跑到這裡 node 必在,但仍印版本供診斷)。*/
function detectNode(): PreflightItem {
try {
const v = execFileSync('node', ['--version'], { stdio: ['ignore', 'pipe', 'ignore'] })
.toString().trim();
return { name: 'node', ok: true, detail: v };
} catch {
return { name: 'node', ok: false, fix: '安裝 Node.js 18+https://nodejs.org' };
}
}
/** wranglerCF CLI)是否可用 + 版本。self-hosted 部署的硬前置。*/
function detectWrangler(): PreflightItem {
try {
const v = execFileSync('wrangler', ['--version'], { stdio: ['ignore', 'pipe', 'ignore'] })
.toString().trim();
return { name: 'wrangler', ok: true, detail: v };
} catch {
return { name: 'wrangler', ok: false, fix: 'npm i -g wrangler' };
}
}
/**
* 安裝前偵測(pip 式:先看環境有什麼)。
* CF 憑證可達由呼叫端用 CfAccountClient.verifyAccess 接著驗(需要 token,不在這層)。
* 回傳所有項目 + 是否有 fatal 缺漏(node/wrangler 缺 = 無法繼續)。
*/
export function detectEnvironment(): { items: PreflightItem[]; fatal: boolean } {
const items = [detectNode(), detectWrangler()];
const fatal = items.some((i) => !i.ok);
return { items, fatal };
}
/** 印一組偵測結果(✓/✗ + 版本 + 補救指令)。*/
export function printPreflight(title: string, items: PreflightItem[]): void {
console.log(chalk.bold(`\n ${title}`));
for (const it of items) {
if (it.ok) {
console.log(chalk.green(`${it.name}`) + (it.detail ? chalk.gray(` ${it.detail}`) : ''));
} else {
console.log(chalk.yellow(`${it.name}`) + (it.detail ? chalk.gray(` ${it.detail}`) : ''));
if (it.fix) console.log(chalk.gray(`${it.fix}`));
}
}
}
/**
* 裝完驗收:逐項確認 self-hosted 環境真的就緒(§7.8 D1 根因:安裝不偵測,缺了不報)。
* 各項以「實際查 CF / 打 cypher」確認,非看 config 有沒有寫——避免假綠(mindset §7)。
*
* @returns items(每項 ok + detail/fix)。呼叫端依 allOk 決定是否 exit 非零 / 印補裝指引。
*/
export async function verifyInstall(opts: {
cf: CfAccountClient;
requiredKv: readonly string[];
expectD1Name?: string;
cypherUrl?: string;
}): Promise<{ items: PreflightItem[]; allOk: boolean }> {
const items: PreflightItem[] = [];
// KV:實查 CF 上現有 namespace,比對必需清單
try {
const existing = await opts.cf.listKvNamespaces();
const have = new Set(existing.keys());
const missing = opts.requiredKv.filter((t) => !have.has(t));
items.push(
missing.length === 0
? { name: `KV namespaces (${opts.requiredKv.length})`, ok: true }
: { name: 'KV namespaces', ok: false, detail: `${missing.join(', ')}`, fix: 'acr update(冪等重建)' },
);
} catch (e) {
items.push({ name: 'KV namespaces', ok: false, detail: msg(e), fix: 'acr update' });
}
// D1:實查 CF 上是否有該庫
if (opts.expectD1Name) {
try {
const dbs = await opts.cf.listD1Databases();
items.push(
dbs.has(opts.expectD1Name)
? { name: `D1 ${opts.expectD1Name}`, ok: true }
: { name: `D1 ${opts.expectD1Name}`, ok: false, detail: '不存在', fix: 'CF token 補勾「Account / D1 / Edit」權限 → 重產 token 填回 .env → acr update' },
);
} catch (e) {
// D1 建失敗最常見根因:CF token 沒勾 D1 權限(KV/Worker 能建但 D1 報 Authentication error)。
const m = msg(e);
const fix = /auth/i.test(m)
? 'token 缺 D1 權限:CF token 補勾「Account / D1 / Edit」→ 重產 token 填回 .env → acr update'
: 'acr update(冪等重試)';
items.push({ name: `D1 ${opts.expectD1Name}`, ok: false, detail: m, fix });
}
}
// cypher-executor 可達(打 /health,不只看 config 有 URL
if (opts.cypherUrl) {
try {
const res = await fetch(`${opts.cypherUrl}/health`, { method: 'GET' });
items.push(
res.ok
? { name: 'cypher-executor 可達', ok: true, detail: opts.cypherUrl }
: { name: 'cypher-executor 可達', ok: false, detail: `HTTP ${res.status} @ ${opts.cypherUrl}`, fix: 'acr update(重部署)' },
);
} catch (e) {
items.push({ name: 'cypher-executor 可達', ok: false, detail: msg(e), fix: 'acr update(重部署);或 worker 剛部署稍候再試' });
}
}
return { items, allOk: items.every((i) => i.ok) };
}
function msg(e: unknown): string {
return e instanceof Error ? e.message : String(e);
}
+8
View File
@@ -0,0 +1,8 @@
allowBuilds:
esbuild: true
sharp: true
workerd: true
onlyBuiltDependencies:
- esbuild
- sharp
- workerd
@@ -105,3 +105,85 @@ export async function tryAuthDispatch(
_auth_path: result.auth_path ?? {},
};
}
// ── 用戶面 {{credential.NAME}} 注入(design §8)────────────────────────────────
/** 匹配 {{credential.NAME}}NAME 為 word 字元) */
const CREDENTIAL_REF = /\{\{credential\.(\w+)\}\}/g;
/** 遞迴收集任意值(string / 物件 / 陣列)裡所有 {{credential.NAME}} 的 NAME */
function collectCredentialNames(value: unknown, out: Set<string>): void {
if (typeof value === 'string') {
for (const m of value.matchAll(CREDENTIAL_REF)) out.add(m[1]);
} else if (Array.isArray(value)) {
for (const v of value) collectCredentialNames(v, out);
} else if (value && typeof value === 'object') {
for (const v of Object.values(value as Record<string, unknown>)) collectCredentialNames(v, out);
}
}
/** 遞迴把 {{credential.NAME}} 替換成 resolved[NAME](未知 name 原樣保留) */
function replaceCredentialRefs(value: unknown, resolved: Record<string, string>): unknown {
if (typeof value === 'string') {
return value.replace(CREDENTIAL_REF, (orig, name: string) =>
Object.prototype.hasOwnProperty.call(resolved, name) ? resolved[name] : orig,
);
}
if (Array.isArray(value)) return value.map((v) => replaceCredentialRefs(v, resolved));
if (value && typeof value === 'object') {
const out: Record<string, unknown> = {};
for (const [k, v] of Object.entries(value as Record<string, unknown>)) {
out[k] = replaceCredentialRefs(v, resolved);
}
return out;
}
return value;
}
/**
* 展開節點 data 裡用戶寫的 `{{credential.NAME}}`design §8)。
*
* 嚴格邊界(rule 02 §2.2):本函式**不解密**。偵測到 {{credential.X}} 後,把 names 交給
* auth_static_key WASM 的 `resolve_credentials` actionWASM 內 kv_get + crypto_decrypt),
* 拿回明文後只做字串回填。ENCRYPTION_KEY 永不經此處。
*
* - 無 {{credential.}} → 原樣回傳(不打 WASM,零開銷)
* - 解密失敗 / 缺 credential → throw(誠實報錯,不假綠)
*/
export async function resolveCredentialRefs(
data: Record<string, unknown>,
env: Bindings,
apiKey: string,
): Promise<Record<string, unknown>> {
const names = new Set<string>();
collectCredentialNames(data, names);
if (names.size === 0) return data;
const url = wasmWorkerUrl('auth_static_key', env.WORKER_SUBDOMAIN);
const res = await fetch(url, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
action: 'resolve_credentials',
api_key: apiKey,
names: [...names],
}),
});
if (!res.ok) {
const text = await res.text().catch(() => '');
throw new Error(`credential resolve 回傳 ${res.status}: ${text.slice(0, 200)}`);
}
const result = (await res.json().catch(() => null)) as {
success?: boolean;
error?: string;
credentials?: Record<string, string>;
} | null;
if (!result || result.success === false) {
throw new Error(`credential resolve 失敗: ${result?.error ?? '未知錯誤'}`);
}
return replaceCredentialRefs(data, result.credentials ?? {}) as Record<string, unknown>;
}
+8 -1
View File
@@ -2,7 +2,7 @@
import type { ExecutionGraph, GraphNode, TraceStep, ComponentRunner, KVContextStore, EdgeType, Bindings } from './types';
import { kvSetNodeOutput, kvGetNodeOutput, ExecutionError, WorkflowPaused } from './types';
import { injectCredentials } from './actions/credential-injector';
import { tryAuthDispatch } from './actions/auth-dispatcher';
import { tryAuthDispatch, resolveCredentialRefs } from './actions/auth-dispatcher';
import { expandPromptRecipe } from './lib/recipe-expander';
import { resolveRecipe } from './routes/recipes';
import { persistPausedRun, isResumablePending, parseRecipeOutput } from './lib/paused-runs';
@@ -245,6 +245,13 @@ export class GraphExecutor {
...resolvedData,
};
// 用戶面 {{credential.NAME}} 展開(design §8):偵測 node.data 裡用戶寫的
// {{credential.X}} → 交 auth_static_key WASM resolve_credentials 解密回填。
// 解密在 WASMrule 02 §2.2),此處只偵測+回填,不碰 ENCRYPTION_KEY。
if (this.env && this.apiKey) {
mergedContext = await resolveCredentialRefs(mergedContext, this.env, this.apiKey);
}
// Resumable workflow callback_url 注入(SDD: resumable-workflow/design.md §2.2
// claude_api 容器拿到後會透傳給 Mira daemondaemon task 完成時 POST 進來
// hostname 暫從 PUBLIC_BASE_URL 取,沒設則用 cypher.arcrun.dev 預設
+2
View File
@@ -20,6 +20,7 @@ import { authRouter } from './routes/auth';
import { resumeRouter } from './routes/resume';
import { executionsRouter } from './routes/executions';
import { initSeedRouter } from './routes/init-seed';
import { kbdbProxyRouter } from './routes/kbdb-proxy';
const app = new Hono<{ Bindings: Bindings }>();
@@ -48,6 +49,7 @@ app.route('/', authRouter);
app.route('/', resumeRouter);
app.route('/', executionsRouter); // LI SDD M2.1: /executions/* + /workflows/:name/executions
app.route('/', initSeedRouter); // 薄殼原則:seed recipe 是 API 行為(rule 07,壓測 §4.1
app.route('/', kbdbProxyRouter); // kbdb-base 9.5KBDB 資料層 proxy(讓 CLI 透過 cypher 達 KBDB,純轉發)
// Worker 導出(fetch + scheduled
// scheduled handler 對應 wrangler.toml [triggers].crons,每分鐘 tick
+70
View File
@@ -0,0 +1,70 @@
/**
* Cron index — 單一固定 key 模型(kbdb-base 8.P0 止血)。
*
* 背景:原本每個 cron workflow 寫一筆 `cron-idx:{apiKey}:{name}`scheduled() 每分鐘
* `WEBHOOKS.list({prefix:'cron-idx:'})` 一次 = 1440 list/日,單獨就爆 CF KV 免費 list 上限(1000/日)。
*
* 解法(SDD §8.2):所有 cron workflow 的 cron_expr 集中存進**單一固定 key** `cron-idx:_all`。
* scheduled() 每分鐘只 `get` 一次(KV get 免費額度 100K/日,遠夠)→ list 次數歸零。
* acr pushwebhooks-named POST/ delete 時對這個 key 做 read-modify-write 維護。
*
* 結構:{ [ "{apiKey}:{name}" ]: cron_expr }
* key 用 `{apiKey}:{name}` 維持多租戶隔離(scheduled 觸發時拆回 apiKey/name 去讀完整 record)。
*/
// KVNamespace 用全域 ambient 型別(與 types.ts 一致,不從 @cloudflare/workers-types import
// 以免產生第二個不相容的 KVNamespace 型別)。
/** 單一固定索引 key — 全租戶共用一筆,scheduled() 只 get 這個 */
export const CRON_INDEX_KEY = 'cron-idx:_all';
/** 索引內容:entryKey"{apiKey}:{name}")→ cron_expr */
export type CronIndex = Record<string, string>;
/** 組出索引 entry 的 keyapiKey + name),含 ':' 也安全:split 時 name 用 slice 還原 */
export function cronEntryKey(apiKey: string, name: string): string {
return `${apiKey}:${name}`;
}
/** 從 entryKey 拆回 { apiKey, name }name 可能含 ':',取第一個 ':' 後全部為 name */
export function parseCronEntryKey(entryKey: string): { apiKey: string; name: string } | null {
const idx = entryKey.indexOf(':');
if (idx <= 0) return null;
return { apiKey: entryKey.slice(0, idx), name: entryKey.slice(idx + 1) };
}
/** 讀整個 cron index(單次 get,不 list */
export async function readCronIndex(kv: KVNamespace): Promise<CronIndex> {
const raw = await kv.get(CRON_INDEX_KEY, 'text');
if (!raw) return {};
try {
const parsed = JSON.parse(raw);
return parsed && typeof parsed === 'object' ? (parsed as CronIndex) : {};
} catch {
return {};
}
}
/**
* upsert / 移除單筆 cron entryread-modify-write 單一 key)。
* @param cronExpr - 有值=upsertnull/undefined=移除(push 改掉 cron 後清乾淨)
*/
export async function updateCronIndexEntry(
kv: KVNamespace,
apiKey: string,
name: string,
cronExpr: string | null | undefined,
): Promise<void> {
const index = await readCronIndex(kv);
const entryKey = cronEntryKey(apiKey, name);
if (cronExpr) {
if (index[entryKey] === cronExpr) return; // 無變化,不浪費一次 put
index[entryKey] = cronExpr;
} else {
if (!(entryKey in index)) return; // 本來就沒有,不浪費一次 put
delete index[entryKey];
}
await kv.put(CRON_INDEX_KEY, JSON.stringify(index));
}
+200
View File
@@ -0,0 +1,200 @@
/**
* KBDB 資料層 proxykbdb-base Phase 9.5HANDOFF §2 + §3b 後續)
*
* 為什麼存在:CLI 是 client,只認證到 cypher-executorX-Arcrun-API-Key),達不到獨立的
* KBDB workerMCP 走內部 service binding 可達,CLI 不行)。故在 cypher 開一條 proxy
* 讓 CLI 薄殼(acr kbdb *)透過「它本來就連的 cypher」打 KBDB 基本盤 API。
*
* 薄殼鐵律(rule 07):本檔是 **proxy**,純轉發到 KBDB 基本盤 HTTP API
* 無業務邏輯、不寫 SQL、不建表、不直連 D1。能力真身在 KBDB 基本盤(kbdb/src/routes/*)。
*
* KBDB 鐵律(leo 2026-06-14):只暴露 template/record/query/search**不開建表/SQL**。
*
* 租戶隔離(leo 2026-06-14 拍板,選項①):
* - X-Arcrun-API-Keynamespace/api_key)→ 自動當 owner_id 注入 records/entries 的寫入與查詢。
* 不同 namespace 的資料互相看不到。與 cypher 其他端點同身份模型。
* - **templates 全域共享**(虛擬表定義是 schema 不是資料;類 Supabase 的表結構大家共用)→ 不注入 owner_id。
*
* cypher→KBDB 連法沿用既有慣例(webhook-handlers.ts / recipes.ts):
* KBDB_BASE_URL HTTP fetch + 選用 KBDB_INTERNAL_TOKEN Bearer。**不新增 service binding**rule 02 §3.1)。
*/
import { Hono } from 'hono';
import type { Bindings } from '../types';
export const kbdbProxyRouter = new Hono<{ Bindings: Bindings }>();
/**
* KBDB 基本盤 base URL + internal headers。
* fallback 指**現役** arcrun-kbdbworkers.dev,無 auth、不需 token)——
* 不沿用 webhook-handlers.ts 的舊 fallback kbdb.finally.clickinkstone 遺留、已死、要 token)。
* KBDB_BASE_URL 可覆蓋(self-hosted fork 指自己的 KBDB)。
*/
function kbdbBase(env: Bindings): { base: string; headers: Record<string, string> } {
const base = (env.KBDB_BASE_URL ?? 'https://arcrun-kbdb.uncle6-me.workers.dev').replace(/\/$/, '');
const headers: Record<string, string> = { 'Content-Type': 'application/json' };
if (env.KBDB_INTERNAL_TOKEN) headers['Authorization'] = `Bearer ${env.KBDB_INTERNAL_TOKEN}`;
return { base, headers };
}
/** 取租戶身份(owner_id)。缺 header → 401(與 cypher 其他資料端點一致)。 */
function tenant(c: { req: { header: (k: string) => string | undefined } }): string | null {
return c.req.header('X-Arcrun-API-Key') ?? null;
}
const NEED_KEY = { error: '缺少 X-Arcrun-API-Key header' } as const;
// ── templates(全域共享,不注入 owner_id)──────────────────────────────────────
// POST /kbdb/templates — 建 templatename + slots)。鐵律:這是「虛擬表定義」非建真表。
kbdbProxyRouter.post('/kbdb/templates', async (c) => {
const owner = tenant(c);
if (!owner) return c.json(NEED_KEY, 401);
const body = await c.req.json().catch(() => null);
if (!body || !body.name || !Array.isArray(body.slots)) {
return c.json({ error: 'name 與 slots[] 必填' }, 400);
}
const { base, headers } = kbdbBase(c.env);
const res = await fetch(`${base}/templates`, {
method: 'POST',
headers,
// created_by 帶上租戶當溯源,但 template 本身全域可見可用
body: JSON.stringify({ name: body.name, slots: body.slots, description: body.description, created_by: owner }),
});
return new Response(res.body, { status: res.status, headers: { 'Content-Type': 'application/json' } });
});
// GET /kbdb/templates — 列出所有 template(全域)。
kbdbProxyRouter.get('/kbdb/templates', async (c) => {
if (!tenant(c)) return c.json(NEED_KEY, 401);
const { base, headers } = kbdbBase(c.env);
const res = await fetch(`${base}/templates`, { headers });
return new Response(res.body, { status: res.status, headers: { 'Content-Type': 'application/json' } });
});
// GET /kbdb/templates/:idOrName — 取單一 template。
kbdbProxyRouter.get('/kbdb/templates/:idOrName', async (c) => {
if (!tenant(c)) return c.json(NEED_KEY, 401);
const { base, headers } = kbdbBase(c.env);
const res = await fetch(`${base}/templates/${encodeURIComponent(c.req.param('idOrName'))}`, { headers });
return new Response(res.body, { status: res.status, headers: { 'Content-Type': 'application/json' } });
});
// ── records(以租戶 namespace 為 owner_id 隔離)────────────────────────────────
// POST /kbdb/records — 填一筆 recordtemplate + values)。owner_id 自動注入。
kbdbProxyRouter.post('/kbdb/records', async (c) => {
const owner = tenant(c);
if (!owner) return c.json(NEED_KEY, 401);
const body = await c.req.json().catch(() => null);
if (!body || !body.template || !body.values) {
return c.json({ error: 'template 與 values 必填' }, 400);
}
const { base, headers } = kbdbBase(c.env);
const res = await fetch(`${base}/records`, {
method: 'POST',
headers,
// 強制以租戶身份隔離:忽略 caller 自帶 owner_id,一律用 header 身份(防跨租戶寫入)
body: JSON.stringify({ template: body.template, values: body.values, owner_id: owner }),
});
return new Response(res.body, { status: res.status, headers: { 'Content-Type': 'application/json' } });
});
// GET /kbdb/records/by-template/:template — 列某 template 下「本租戶」的 records。
kbdbProxyRouter.get('/kbdb/records/by-template/:template', async (c) => {
const owner = tenant(c);
if (!owner) return c.json(NEED_KEY, 401);
const { base, headers } = kbdbBase(c.env);
const res = await fetch(
`${base}/records/by-template/${encodeURIComponent(c.req.param('template'))}?owner_id=${encodeURIComponent(owner)}`,
{ headers },
);
return new Response(res.body, { status: res.status, headers: { 'Content-Type': 'application/json' } });
});
// GET /kbdb/records/:recordId — 取單筆 record。
kbdbProxyRouter.get('/kbdb/records/:recordId', async (c) => {
if (!tenant(c)) return c.json(NEED_KEY, 401);
const { base, headers } = kbdbBase(c.env);
const res = await fetch(`${base}/records/${encodeURIComponent(c.req.param('recordId'))}`, { headers });
return new Response(res.body, { status: res.status, headers: { 'Content-Type': 'application/json' } });
});
// ── search(限本租戶範圍內)────────────────────────────────────────────────────
// GET /kbdb/search?q=&entry_type=&source=&mode= — entries 搜尋,限本租戶 owner_id。
// 透傳 entry_typebase 通用 filterworkflow-discovery Q4/ source / mode 給 KBDB /entries/search。
kbdbProxyRouter.get('/kbdb/search', async (c) => {
const owner = tenant(c);
if (!owner) return c.json(NEED_KEY, 401);
const q = c.req.query('q');
if (!q) return c.json({ error: 'q 必填' }, 400);
const { base, headers } = kbdbBase(c.env);
const params = new URLSearchParams({ q, owner_id: owner });
for (const k of ['entry_type', 'source', 'mode']) {
const v = c.req.query(k);
if (v) params.set(k, v);
}
const res = await fetch(`${base}/entries/search?${params.toString()}`, { headers });
return new Response(res.body, { status: res.status, headers: { 'Content-Type': 'application/json' } });
});
// ── entries(原子資料 / 樹節點,以租戶 namespace 為 owner_id 隔離)─────────────────
//
// kbdb-base 9.6:基本盤 /entries CRUD 的 proxyHANDOFF §2 缺口①,mira _kbdb_client.py 遷移目標)。
// 租戶隔離同 records(選項①):寫入強制注入 owner_id、list 強制以本租戶 owner_id 過濾;
// by-id 沿用既有 records by-id 慣例(require-key,不額外做 owner 比對——與本檔其他 by-id 端點一致)。
// POST /kbdb/entries — 建一個 entryentry_type 必填,如 block/value/project/workflow)。owner_id 自動注入。
kbdbProxyRouter.post('/kbdb/entries', async (c) => {
const owner = tenant(c);
if (!owner) return c.json(NEED_KEY, 401);
const body = await c.req.json().catch(() => null);
if (!body || !body.entry_type) return c.json({ error: 'entry_type 必填' }, 400);
const { base, headers } = kbdbBase(c.env);
const res = await fetch(`${base}/entries`, {
method: 'POST',
headers,
// 強制以租戶身份隔離:忽略 caller 自帶 owner_id,一律用 header 身份(防跨租戶寫入)
body: JSON.stringify({ ...body, owner_id: owner }),
});
return new Response(res.body, { status: res.status, headers: { 'Content-Type': 'application/json' } });
});
// GET /kbdb/entries — listfilters: entry_type / parent_id / page_name / limit / offset)。
// owner_id 強制覆寫成本租戶(防跨租戶讀;caller 不能查別人的 owner_id)。
kbdbProxyRouter.get('/kbdb/entries', async (c) => {
const owner = tenant(c);
if (!owner) return c.json(NEED_KEY, 401);
const { base, headers } = kbdbBase(c.env);
const params = new URLSearchParams();
params.set('owner_id', owner); // 強制本租戶,不接受 caller 覆寫
for (const k of ['entry_type', 'parent_id', 'page_name', 'source', 'limit', 'offset']) {
const v = c.req.query(k);
if (v) params.set(k, v);
}
const res = await fetch(`${base}/entries?${params.toString()}`, { headers });
return new Response(res.body, { status: res.status, headers: { 'Content-Type': 'application/json' } });
});
// GET /kbdb/entries/:id — 取單筆 entry。
kbdbProxyRouter.get('/kbdb/entries/:id', async (c) => {
if (!tenant(c)) return c.json(NEED_KEY, 401);
const { base, headers } = kbdbBase(c.env);
const res = await fetch(`${base}/entries/${encodeURIComponent(c.req.param('id'))}`, { headers });
return new Response(res.body, { status: res.status, headers: { 'Content-Type': 'application/json' } });
});
// PATCH /kbdb/entries/:id — 更新單筆 entry。owner_id 不可被改(剝除 caller 自帶的 owner_id)。
kbdbProxyRouter.patch('/kbdb/entries/:id', async (c) => {
if (!tenant(c)) return c.json(NEED_KEY, 401);
const body = await c.req.json().catch(() => ({}));
// 不讓 patch 改 owner_id(防把別人的資料認領過來或踢給別人)
const { owner_id: _drop, ...patch } = body ?? {};
const { base, headers } = kbdbBase(c.env);
const res = await fetch(`${base}/entries/${encodeURIComponent(c.req.param('id'))}`, {
method: 'PATCH',
headers,
body: JSON.stringify(patch),
});
return new Response(res.body, { status: res.status, headers: { 'Content-Type': 'application/json' } });
});
+171 -20
View File
@@ -28,6 +28,7 @@ import { executeWebhookGraph } from '../actions/webhook-handlers';
import { writeExecutionVerdict } from '../actions/execution-logger';
import type { GraphNode } from '../types';
import { extractCronExpr } from '../lib/cron-match';
import { updateCronIndexEntry, CRON_INDEX_KEY } from '../lib/cron-index';
import { recordTelemetry } from '../lib/telemetry';
import { checkExposureConsent, resolveConsentForRecord } from '../lib/exposure-consent';
import type { ExposureConsent } from '../lib/exposure-consent';
@@ -52,9 +53,42 @@ function kvKey(apiKey: string, name: string): string {
return `${apiKey}:wf:${name}`;
}
/** 輕量 cron index entry — scheduled() 只列這個 prefix(每分鐘 tick 不掃全量 KV*/
function cronIndexKey(apiKey: string, name: string): string {
return `cron-idx:${apiKey}:${name}`;
/**
* workflow-discovery R2/Phase 2.1:部署時雙寫一個 embeddable entry 到 KBDB,讓 workflow 可被語意搜尋。
*
* 雙寫(design 方案 C):WEBHOOKS KV record 照舊(list/get/trigger 不動),另寫 entry_type=workflow 的
* entry 供 search。owner_id = api_key(租戶隔離,與 kbdb-proxy 同身份模型)。
* content = description(被 embed 的主體);metadata.embed:true → 命中 #7 精耕條件進 Vectorize(模組開時)。
*
* 非阻塞 + 失敗不致命(waitUntil + catch):search 可發現性是加值,不該擋部署成功(對齊 #7 embedOnWrite 慣例)。
* KBDB 連法沿用既有慣例(KBDB_BASE_URL fetch + 選用 token),不新增 service bindingrule 02 §3.1)。
*/
async function writeWorkflowSearchEntry(
env: Bindings,
apiKey: string,
name: string,
description: string,
workflowId?: string,
): Promise<void> {
const base = (env.KBDB_BASE_URL ?? 'https://arcrun-kbdb.uncle6-me.workers.dev').replace(/\/$/, '');
const headers: Record<string, string> = { 'Content-Type': 'application/json' };
if (env.KBDB_INTERNAL_TOKEN) headers['Authorization'] = `Bearer ${env.KBDB_INTERNAL_TOKEN}`;
await fetch(`${base}/entries`, {
method: 'POST',
headers,
body: JSON.stringify({
entry_type: 'workflow',
owner_id: apiKey, // 租戶隔離(與 kbdb-proxy 同身份)
page_name: name,
content: description, // 被 embed / LIKE 命中的主體
// KBDB createEntry 吃 metadata_jsonTEXT),embed.ts isEmbeddable 讀 metadata_json.embed === true。
metadata_json: JSON.stringify({
embed: true, // #7 精耕開關:標 true 才進 Vectorize
workflow_name: name,
workflow_id: workflowId ?? name,
}),
}),
});
}
// POST /webhooks/named — 部署(acr push 呼叫)
@@ -76,6 +110,16 @@ webhooksNamedRouter.post('/webhooks/named', async (c) => {
return c.json({ error: '缺少必要欄位:name, graph' }, 400);
}
// workflow-discovery R1description 強制非空(供語意搜尋,工作流可被發現)。
// 定位(Q2 定案):要求操盤的 AI 據實寫一句「這工作流能做什麼」,非逼 low-code 用戶手填、
// 非介面層機械塞佔位。空 → 擋下,由操盤 CC 據實補一句再部署(用戶可改)。
if (typeof body.description !== 'string' || body.description.trim() === '') {
return c.json({
error: 'description 必填:請操盤的 AI 據實寫一句「這工作流能做什麼」(如「呼叫可 Upsert Google Sheets」),用戶可再改。供語意搜尋用,不是寫文章。',
requires: 'description',
}, 400);
}
const name = body.name.trim();
if (!/^[\w-]+$/.test(name)) {
return c.json({ error: 'workflow name 只能包含英文字母、數字、底線和連字號' }, 400);
@@ -97,7 +141,7 @@ webhooksNamedRouter.post('/webhooks/named', async (c) => {
name,
graph: body.graph,
config: body.config,
description: typeof body.description === 'string' ? body.description : '',
description: body.description.trim(), // R1:已驗非空(見上),存 trim 後的值
created_at: new Date().toISOString(),
cron_expr: cronExpr ?? undefined,
// 法律憑證:存人類明示同意(本次新同意或沿用既有)
@@ -107,12 +151,15 @@ webhooksNamedRouter.post('/webhooks/named', async (c) => {
const start = Date.now();
await c.env.WEBHOOKS.put(kvKey(apiKey, name), JSON.stringify(record));
// 維護 cron index:有 cron_expr 就寫 / 沒有就刪除(避免 push 改 yaml 拿掉 cron 後殘留)
if (cronExpr) {
await c.env.WEBHOOKS.put(cronIndexKey(apiKey, name), JSON.stringify({ cron_expr: cronExpr }));
} else {
await c.env.WEBHOOKS.delete(cronIndexKey(apiKey, name));
}
// 維護單一 cron index key8.P0:有 cron_expr 就 upsert / 沒有就移除
// (避免 push 改 yaml 拿掉 cron 後殘留)。scheduled() 每分鐘只 get 這一個 key。
await updateCronIndexEntry(c.env.WEBHOOKS, apiKey, name, cronExpr);
// workflow-discovery Phase 2.1:雙寫 embeddable search-entry(讓此 workflow 可被語意搜尋)。
// 非阻塞(waitUntil)+ 失敗不致命(catch):可發現性是加值,不擋部署成功(對齊 #7 embedOnWrite 慣例)。
c.executionCtx.waitUntil(
writeWorkflowSearchEntry(c.env, apiKey, name, record.description).catch(() => {}),
);
// Implicit telemetry (LI M1.2)
recordTelemetry(c.env, apiKey, {
@@ -131,6 +178,103 @@ webhooksNamedRouter.post('/webhooks/named', async (c) => {
}, 201);
});
// GET /workflows/search?q=&mode= — workflow-discovery R2:語意搜尋本租戶的工作流。
// 轉發 KBDB /entries/search(限 entry_type=workflow + 本租戶 owner_id)。優先語意、未開 Vectorize
// 降級 keyword + capability_hintKBDB 端已實作 #7 閉環,本端純轉發 + 注 entry_type/owner_id)。
// 形態對齊 u6u_search_components:自然語言 q 進、結果 + capability_hint 出。flag 安全:AI 主動 pull,無輪詢。
webhooksNamedRouter.get('/workflows/search', async (c) => {
const apiKey = c.req.header('X-Arcrun-API-Key');
if (!apiKey) return c.json({ error: '缺少 X-Arcrun-API-Key header' }, 401);
const q = c.req.query('q');
if (!q) return c.json({ error: 'q 必填:用自然語言描述要找的工作流(如「把資料寫進 Google Sheets」)' }, 400);
// 預設優先語意;caller 傳 mode=keyword 才強制關鍵字。KBDB 端未開 Vectorize 會自動降級。
const mode = c.req.query('mode') === 'keyword' ? 'keyword' : 'semantic';
const base = (c.env.KBDB_BASE_URL ?? 'https://arcrun-kbdb.uncle6-me.workers.dev').replace(/\/$/, '');
const headers: Record<string, string> = { 'Content-Type': 'application/json' };
if (c.env.KBDB_INTERNAL_TOKEN) headers['Authorization'] = `Bearer ${c.env.KBDB_INTERNAL_TOKEN}`;
const params = new URLSearchParams({
q,
owner_id: apiKey, // 租戶隔離(只搜本租戶的 workflow)
entry_type: 'workflow', // base 通用 filterQ4),只回 workflow entry
mode,
});
const res = await fetch(`${base}/entries/search?${params.toString()}`, { headers });
return new Response(res.body, { status: res.status, headers: { 'Content-Type': 'application/json' } });
});
// POST /workflows/backfill-search-entries — workflow-discovery R3:把既有 workflow 補成可搜的 search-entry。
// 有 description 的 → 補寫 entry(讓它們可被 u6u_search_workflows 搜到);無 description 的 → 列出待 re-deploy。
// 誠實:不自動編造 description(無 desc 的只列出、不假裝)。flag 安全:人/AI 主動呼叫一次,非 cron/輪詢。
webhooksNamedRouter.post('/workflows/backfill-search-entries', async (c) => {
const apiKey = c.req.header('X-Arcrun-API-Key');
if (!apiKey) return c.json({ error: '缺少 X-Arcrun-API-Key header' }, 401);
const prefix = `${apiKey}:wf:`;
const list = await c.env.WEBHOOKS.list({ prefix });
const backfilled: string[] = [];
const needsDescription: string[] = [];
const errors: string[] = [];
for (const k of list.keys) {
const name = k.name.slice(prefix.length);
const raw = await c.env.WEBHOOKS.get(k.name, 'text');
if (!raw) continue;
const rec = JSON.parse(raw) as NamedWorkflowRecord;
const desc = rec.description?.trim();
if (!desc) {
// 不自動編造:無 description 的列出來,請操盤 CC re-deploy 時據實補(誠實,mindset §7)。
needsDescription.push(name);
continue;
}
try {
await writeWorkflowSearchEntry(c.env, apiKey, name, desc);
backfilled.push(name);
} catch (e) {
errors.push(`${name}: ${e instanceof Error ? e.message : String(e)}`);
}
}
return c.json({
backfilled,
backfilled_count: backfilled.length,
needs_description: needsDescription,
needs_description_count: needsDescription.length,
errors,
hint: needsDescription.length > 0
? `${needsDescription.length} 個工作流缺 description 無法被搜尋。請操盤的 AI re-deploy 它們時據實補一句「能做什麼」(不自動編造)。`
: undefined,
});
});
// POST /webhooks/named/migrate-cron-index — 一次性 migration8.P0):把舊的 per-key
// cron-idx:{apiKey}:{name} 折進單一 cron-idx:_all(這裡才 list 一次,非每分鐘 tick)。
// 增量寫、不刪舊 key(重跑安全、冪等)。部署 8.P0 後跑一次,讓既有 cron workflow 不漏掉。
// 必須在 /:name/trigger 之前註冊,否則 :name 會攔截 "migrate-cron-index"。
webhooksNamedRouter.post('/webhooks/named/migrate-cron-index', async (c) => {
const list = await c.env.WEBHOOKS.list({ prefix: 'cron-idx:' });
let migrated = 0, skipped = 0;
const errors: string[] = [];
for (const k of list.keys) {
if (k.name === CRON_INDEX_KEY) { skipped++; continue; } // 跳過新的集中 key 自己
const parts = k.name.split(':'); // cron-idx:{apiKey}:{name}
if (parts.length < 3) { skipped++; continue; }
const apiKey = parts[1];
const name = parts.slice(2).join(':');
try {
const raw = await c.env.WEBHOOKS.get(k.name, 'text');
if (!raw) { skipped++; continue; }
const idx = JSON.parse(raw) as { cron_expr?: string };
if (!idx.cron_expr) { skipped++; continue; }
await updateCronIndexEntry(c.env.WEBHOOKS, apiKey, name, idx.cron_expr);
migrated++;
} catch (e) {
errors.push(`${k.name}: ${e instanceof Error ? e.message : String(e)}`);
}
}
return c.json({ success: errors.length === 0, migrated, skipped, errors });
});
// POST /webhooks/named/:name/trigger — 觸發執行(api_key 走 header;標準/向後相容)
webhooksNamedRouter.post('/webhooks/named/:name/trigger', async (c) => {
const apiKey = c.req.header('X-Arcrun-API-Key');
@@ -220,16 +364,23 @@ webhooksNamedRouter.get('/webhooks/named', async (c) => {
const prefix = `${apiKey}:wf:`;
const list = await c.env.WEBHOOKS.list({ prefix });
const workflows = list.keys.map(k => {
const name = k.name.slice(prefix.length);
return { name };
});
// workflow-discovery 方向①:list 回完整欄位(description/created_at),讓 MCP u6u_list_workflows
// 改讀本端點時欄位齊(取代舊的讀 workflow_metadata record)。需 get 每個 record 取 description。
const baseUrl = new URL(c.req.url).origin;
const result = workflows.map(w => ({
name: w.name,
webhook_url: `${baseUrl}/webhooks/named/${w.name}/trigger`,
}));
const result = await Promise.all(
list.keys.map(async (k) => {
const name = k.name.slice(prefix.length);
const raw = await c.env.WEBHOOKS.get(k.name, 'text');
const rec = raw ? (JSON.parse(raw) as NamedWorkflowRecord) : null;
return {
name,
description: rec?.description ?? '',
created_at: rec?.created_at ?? '',
cron_expr: rec?.cron_expr,
webhook_url: `${baseUrl}/webhooks/named/${name}/trigger`,
};
}),
);
return c.json({ workflows: result, total: result.length });
});
@@ -248,6 +399,6 @@ webhooksNamedRouter.delete('/webhooks/named/:name', async (c) => {
}
await c.env.WEBHOOKS.delete(kvKey(apiKey, name));
await c.env.WEBHOOKS.delete(cronIndexKey(apiKey, name));
await updateCronIndexEntry(c.env.WEBHOOKS, apiKey, name, null);
return c.json({ deleted: true, name });
});
+19 -22
View File
@@ -2,17 +2,21 @@
* scheduled() handler wrangler.toml [triggers].crons
*
*
* 1. WEBHOOKS KV webhook:{api_key}:{name} key
* 2. workflow cron_expracr push cron record.cron_expr
* 3. cronMatch() event.scheduledTimeUTC
* 1. get cron indexcron-idx:_all cron workflow cron_expr
* 2. cron_expr event.scheduledTimeUTC
* 3. workflow record{apiKey}:wf:{name}
* 4. executeWebhookGraph waitUntil
*
* SDD: arcrun.md -A P1 #3
* 8.P0 SDD §8.2 WEBHOOKS.list('cron-idx:') = 1440 list/ KV
* key get list
*
* SDD: arcrun.md -A P1 #3 / kbdb-base §8.2
*/
import type { ExecutionContext, ScheduledController } from '@cloudflare/workers-types';
import type { Bindings } from './types';
import { cronMatch } from './lib/cron-match';
import { readCronIndex, parseCronEntryKey } from './lib/cron-index';
import { executeWebhookGraph } from './actions/webhook-handlers';
type StoredWorkflowRecord = {
@@ -29,25 +33,18 @@ export async function handleScheduled(
const now = new Date(controller.scheduledTime);
console.log('[scheduled] tick', now.toISOString(), 'controller.cron=', controller.cron);
// 只列 cron-idx: prefix,輕量 — acr push 時為 cron-tagged workflow 額外寫一筆 index
// 主 workflow record 仍在 {apiKey}:wf:{name},需要時再 get
const list = await env.WEBHOOKS.list({ prefix: 'cron-idx:' });
// 8.P0:單次 get 集中索引(取代每分鐘 list),主 workflow record 仍在 {apiKey}:wf:{name}
const index = await readCronIndex(env.WEBHOOKS);
const entries = Object.entries(index);
let triggered = 0;
for (const entry of list.keys) {
// key = cron-idx:{api_key}:{name}
const parts = entry.name.split(':');
if (parts.length < 3) continue;
const apiKey = parts[1];
const name = parts.slice(2).join(':'); // name 可能含 ':'(雖然 push handler 已用 /^[\w-]+$/ 擋)
for (const [entryKey, cronExpr] of entries) {
const parsed = parseCronEntryKey(entryKey);
if (!parsed) continue;
const { apiKey, name } = parsed;
// 從 cron-idx 拿 cron_expr(輕量)
const idxRaw = await env.WEBHOOKS.get(entry.name, 'text');
if (!idxRaw) continue;
let idx: { cron_expr?: string };
try { idx = JSON.parse(idxRaw); } catch { continue; }
if (!idx.cron_expr) continue;
if (!cronMatch(idx.cron_expr, now)) continue;
if (!cronExpr) continue;
if (!cronMatch(cronExpr, now)) continue;
// 匹配才去讀完整 workflow record
const wfKey = `${apiKey}:wf:${name}`;
@@ -60,7 +57,7 @@ export async function handleScheduled(
try { record = JSON.parse(wfRaw) as StoredWorkflowRecord; } catch { continue; }
triggered++;
console.log('[scheduled] trigger', name, 'apiKey=', apiKey.slice(0, 12) + '...', 'cron=', idx.cron_expr);
console.log('[scheduled] trigger', name, 'apiKey=', apiKey.slice(0, 12) + '...', 'cron=', cronExpr);
// 把 apiKey 也放進 triggerContext,讓 workflow 內節點能用 {{api_key}}(跟 webhook trigger 慣例一致)
const triggerContext = {
api_key: apiKey,
@@ -75,5 +72,5 @@ export async function handleScheduled(
),
);
}
console.log(`[scheduled] scanned ${list.keys.length} cron-idx entries, ${triggered} triggered`);
console.log(`[scheduled] scanned ${entries.length} cron-idx entries, ${triggered} triggered`);
}
+5
View File
@@ -113,6 +113,11 @@ ENVIRONMENT = "production"
# Self-hosted fork:改成自己的 CF 帳號 subdomainWorkers & Pages → 你的帳號 → subdomain settings
WORKER_SUBDOMAIN = "uncle6-me"
# KBDB 基本盤對外 URLcypher→KBDBproxy /kbdb/*、recipe-stats、recipe fragment)。
# 現役 = arcrun-kbdbworkers.dev,無 auth、不需 token)。舊的 kbdb.finally.click 是 inkstone 遺留已死。
# Self-hosted fork:改成自己部署的 arcrun-kbdb.<你的subdomain>.workers.dev。
KBDB_BASE_URL = "https://arcrun-kbdb.uncle6-me.workers.dev"
[[routes]]
pattern = "cypher.arcrun.dev/*"
zone_name = "arcrun.dev"
+8
View File
@@ -0,0 +1,8 @@
allowBuilds:
esbuild: true
sharp: true
workerd: true
onlyBuiltDependencies:
- esbuild
- sharp
- workerd
+15 -1
View File
@@ -56,6 +56,8 @@ export interface ListEntriesFilter {
entry_type?: string;
owner_id?: string;
parent_id?: string;
page_name?: string; // exact-match lookup (e.g. skill-/example- idempotency key)
source?: string; // filter by metadata_json.$.source (ingest envelope source.uri). issue #5.1
limit?: number;
offset?: number;
}
@@ -66,6 +68,10 @@ export async function listEntries(db: D1Database, f: ListEntriesFilter = {}): Pr
if (f.entry_type) { conds.push('entry_type = ?'); params.push(f.entry_type); }
if (f.owner_id) { conds.push('owner_id = ?'); params.push(f.owner_id); }
if (f.parent_id) { conds.push('parent_id = ?'); params.push(f.parent_id); }
if (f.page_name) { conds.push('page_name = ?'); params.push(f.page_name); }
// source is queryable via SQLite json_extract on the existing metadata_json TEXT column —
// no new column / no migration (表不變鐵律). Per issue #5.1 (頂層化 source 成可查 filter).
if (f.source) { conds.push("json_extract(metadata_json, '$.source') = ?"); params.push(f.source); }
const where = conds.length ? `WHERE ${conds.join(' AND ')}` : '';
const limit = Math.min(f.limit ?? 100, 1000);
const offset = f.offset ?? 0;
@@ -105,10 +111,18 @@ export async function deleteEntry(db: D1Database, id: string): Promise<void> {
}
// D1 LIKE keyword search (base; semantic search is the optional embed module).
export async function searchEntries(db: D1Database, q: string, owner_id?: string, limit = 50): Promise<Entry[]> {
// entry_type: optional base filter (generic — caller passes any type, base stays type-agnostic).
export async function searchEntries(
db: D1Database,
q: string,
owner_id?: string,
entry_type?: string,
limit = 50,
): Promise<Entry[]> {
const conds = ['content LIKE ?'];
const params: unknown[] = [`%${q}%`];
if (owner_id) { conds.push('owner_id = ?'); params.push(owner_id); }
if (entry_type) { conds.push('entry_type = ?'); params.push(entry_type); }
const res = await db
.prepare(`SELECT * FROM entries WHERE ${conds.join(' AND ')} ORDER BY updated_at DESC LIMIT ?`)
.bind(...params, Math.min(limit, 200))
+63 -3
View File
@@ -88,6 +88,52 @@ export async function createRecord(db: D1Database, input: CreateRecordInput): Pr
return { record_id: recordId, template_id: tpl.id, values: input.values };
}
// Update an existing record's slot values (mira-dissolve T2.1, issue #6).
// "Deprecate by flipping a slot value" — base append-only is NOT broken: we change the
// underlying entries.content of the slot's entry, we do not alter table structure / add columns / delete rows.
// - slot already on the record → UPDATE the linked entries.content.
// - slot valid for the record's template but not yet present → create entry + entry_value (idempotent grow).
// - slot not in the template's slots_json → reject (records must stay template-shaped).
// Returns null if the record does not exist.
export async function updateRecord(
db: D1Database,
recordId: string,
values: Record<string, string>,
): Promise<RecordResult | null> {
// Existing slot → entry_id + template_id for this record.
const evRes = await db
.prepare(`SELECT slot_name, entry_id, template_id FROM entry_values WHERE record_id = ?`)
.bind(recordId)
.all<{ slot_name: string; entry_id: string; template_id: string }>();
const evRows = evRes.results ?? [];
if (evRows.length === 0) return null; // record does not exist
const templateId = evRows[0].template_id;
const slotToEntry = new Map(evRows.map((r) => [r.slot_name, r.entry_id]));
const tpl = await getTemplate(db, templateId);
const allowed: string[] = tpl ? JSON.parse(tpl.slots_json) : [...slotToEntry.keys()];
for (const [slot, content] of Object.entries(values)) {
if (!allowed.includes(slot)) {
throw new Error(`slot not in template: ${slot}`);
}
const entryId = slotToEntry.get(slot);
if (entryId) {
// flip the slot value: update the linked entry's content (table structure untouched)
await db.prepare(`UPDATE entries SET content = ?, updated_at = unixepoch() WHERE id = ?`).bind(content, entryId).run();
} else {
// valid template slot not yet on this record → grow it (create entry + link)
const entry = await createEntry(db, { content, entry_type: 'value' });
await db
.prepare(`INSERT INTO entry_values (id, record_id, template_id, slot_name, entry_id) VALUES (?, ?, ?, ?, ?)`)
.bind(uid('ev'), recordId, templateId, slot, entry.id)
.run();
}
}
return getRecord(db, recordId);
}
export async function getRecord(db: D1Database, recordId: string): Promise<RecordResult | null> {
const res = await db
.prepare(
@@ -107,14 +153,28 @@ export async function getRecord(db: D1Database, recordId: string): Promise<Recor
export async function searchByTemplate(db: D1Database, template: string, owner_id?: string, limit = 100): Promise<RecordResult[]> {
const tpl = await getTemplate(db, template);
if (!tpl) return [];
const res = await db
// owner_id 過濾在 SQL 做:record 的歸屬存在底層 entries.owner_idcreateRecord 寫入時帶)。
// 給了 owner_id → JOIN entries 限定該 owner(租戶隔離,cypher proxy 強制注入);
// 沒給 → 不限(內部/全域查詢)。先前 `|| true` 是 stub,會洩漏跨租戶資料(2026-06-14 修)。
const cap = Math.min(limit, 500);
const res = owner_id
? await db
.prepare(
`SELECT DISTINCT ev.record_id as record_id FROM entry_values ev
JOIN entries e ON ev.entry_id = e.id
WHERE ev.template_id = ? AND e.owner_id = ?
ORDER BY ev.created_at DESC LIMIT ?`,
)
.bind(tpl.id, owner_id, cap)
.all<{ record_id: string }>()
: await db
.prepare(`SELECT DISTINCT record_id FROM entry_values WHERE template_id = ? ORDER BY created_at DESC LIMIT ?`)
.bind(tpl.id, Math.min(limit, 500))
.bind(tpl.id, cap)
.all<{ record_id: string }>();
const out: RecordResult[] = [];
for (const { record_id } of res.results ?? []) {
const rec = await getRecord(db, record_id);
if (rec && (!owner_id || true)) out.push(rec);
if (rec) out.push(rec);
}
return out;
}
+119
View File
@@ -0,0 +1,119 @@
// KBDB optional embed module (issue #7 / mira-dissolve SDD T2.4).
//
// 鐵律對齊:
// - embedding 屬 **base 的 optional 模組**(非 graph/ingest)。CF 內建(Vectorize+AI),程式薄。
// - **不拆 repobinding 開/關**:有 env.VECTORIZE + env.AI 才啟用;沒有 → base 維持 LIKE keywordAPI 不變。
// - 不動三表結構(只標既有 entries.is_embedded / content_hash bookkeeping 欄;那些 base 從不讀,embed 才寫)。
// - 不對每個 block 地毯式 embed(精耕,非 RAG 一股腦灌):只 embed「被標記為 embeddable」的 entry
// wiki 段落 + graph node gloss)。標記方式=寫入時 metadata_json.embed === truecaller 顯式標)。
//
// 為何用 metadata flag 而非 entry_type 白名單:base 不該寫死「哪些 entry_type 該 embed」(那是上游語意,
// 會讓 base 知道 wiki/graph 概念,破壞解耦)。改由 caller(wiki/gloss 寫入端)顯式標 embed:true
// base 只認這個通用旗標 → base 維持對內容語意無知。
import type { Bindings, Entry } from './types';
const EMBED_MODEL = '@cf/baai/bge-base-en-v1.5'; // 768-dim,與 Vectorize index dimensions=768 對齊
/** embed 模組是否啟用(binding 都在才算開)。base 一切 embed 動作先過這關。 */
export function embedEnabled(env: Bindings): boolean {
return !!(env.VECTORIZE && env.AI);
}
/** 一段文字 → 768 維向量(Workers AI bge)。空字串回 null(不 embed)。 */
async function embedText(env: Bindings, text: string): Promise<number[] | null> {
const t = (text ?? '').trim();
if (!t || !env.AI) return null;
const res = (await env.AI.run(EMBED_MODEL, { text: [t] })) as { data: number[][] };
return res?.data?.[0] ?? null;
}
/**
* embedembed-on-write#5 4
* - no-opbase
* - embed embeddable entrymetadata_json.embed === true
* fire-and-forget caller waitUntil embed embed
* embed caller is_embedded
*/
export async function embedOnWrite(env: Bindings, entry: Entry): Promise<boolean> {
if (!embedEnabled(env)) return false;
if (!isEmbeddable(entry)) return false;
const vec = await embedText(env, entry.content ?? '');
if (!vec) return false;
await env.VECTORIZE!.upsert([
{
id: entry.id,
values: vec,
// metadata 走 indexed 範圍:owner_id(租戶隔離)、entry_type、source#5.1 過濾與語義共用)。
metadata: {
owner_id: entry.owner_id ?? '',
entry_type: entry.entry_type,
source: readSource(entry) ?? '',
},
},
]);
// 標記 bookkeeping(既有欄,base 不讀、僅供「已 embed」可查)。不動表結構。
await env.DB.prepare('UPDATE entries SET is_embedded = 1 WHERE id = ?').bind(entry.id).run();
return true;
}
/** entry 是否該被 embedcaller 在 metadata_json 標 embed:true(精耕,非地毯式)。 */
function isEmbeddable(entry: Entry): boolean {
const meta = parseMeta(entry.metadata_json);
return meta?.embed === true;
}
function readSource(entry: Entry): string | null {
const meta = parseMeta(entry.metadata_json);
const s = meta?.source;
return typeof s === 'string' ? s : null;
}
function parseMeta(json: string | null): Record<string, unknown> | null {
if (!json) return null;
try {
const p = JSON.parse(json);
return p && typeof p === 'object' ? (p as Record<string, unknown>) : null;
} catch {
return null;
}
}
export interface SemanticHit {
id: string;
score: number;
owner_id?: string;
entry_type?: string;
source?: string;
}
/**
* mode:'semantic' nullcaller keyword +
* owner_id / source / entry_type Vectorize metadata filterentry_type index upsert metadata
* entry_type base filtercaller typebase
*/
export async function semanticSearch(
env: Bindings,
q: string,
opts: { owner_id?: string; source?: string; entry_type?: string; topK?: number } = {},
): Promise<SemanticHit[] | null> {
if (!embedEnabled(env)) return null;
const vec = await embedText(env, q);
if (!vec) return [];
const filter: Record<string, string> = {};
if (opts.owner_id) filter.owner_id = opts.owner_id;
if (opts.source) filter.source = opts.source;
if (opts.entry_type) filter.entry_type = opts.entry_type;
const res = await env.VECTORIZE!.query(vec, {
topK: Math.min(opts.topK ?? 20, 100),
returnMetadata: 'indexed',
...(Object.keys(filter).length ? { filter } : {}),
});
return (res.matches ?? []).map((m) => ({
id: m.id,
score: m.score,
owner_id: m.metadata?.owner_id as string | undefined,
entry_type: m.metadata?.entry_type as string | undefined,
source: m.metadata?.source as string | undefined,
}));
}
+49 -4
View File
@@ -1,4 +1,4 @@
// Entries route — atomic data + tree (project/workflow). Base, no embed/triplet.
// Entries route — atomic data + tree (project/workflow). Base; embed is OPTIONAL (issue #7).
import { Hono } from 'hono';
import type { Bindings } from '../types';
import {
@@ -9,6 +9,7 @@ import {
deleteEntry,
searchEntries,
} from '../actions/entry-crud';
import { embedEnabled, embedOnWrite, semanticSearch } from '../embed';
export const entryRoutes = new Hono<{ Bindings: Bindings }>();
@@ -17,27 +18,63 @@ entryRoutes.post('/', async (c) => {
const body = await c.req.json().catch(() => null);
if (!body || !body.entry_type) return c.json({ success: false, error: 'entry_type required' }, 400);
const entry = await createEntry(c.env.DB, body);
// embed-on-write (#7 / #5 第4點):模組開 + entry 標 embed:true 才做;fire-and-forget,不阻塞回應、失敗不致命。
if (embedEnabled(c.env)) c.executionCtx.waitUntil(embedOnWrite(c.env, entry).catch(() => {}));
return c.json({ success: true, entry });
});
// GET /entries — list with filters (entry_type, owner_id, parent_id)
// GET /entries — list with filters (entry_type, owner_id, parent_id, page_name, source)
// e.g. list workflows under a project: ?parent_id=PROJECT&entry_type=workflow
// e.g. get one by idempotency key: ?page_name=skill-rag_with_arcrun
// e.g. filter by ingest source: ?source=logseq://vault/foo.md (issue #5.1)
entryRoutes.get('/', async (c) => {
const entries = await listEntries(c.env.DB, {
entry_type: c.req.query('entry_type') || undefined,
owner_id: c.req.query('owner_id') || undefined,
parent_id: c.req.query('parent_id') || undefined,
page_name: c.req.query('page_name') || undefined,
source: c.req.query('source') || undefined,
limit: c.req.query('limit') ? Number(c.req.query('limit')) : undefined,
offset: c.req.query('offset') ? Number(c.req.query('offset')) : undefined,
});
return c.json({ success: true, entries, count: entries.length });
});
// GET /entries/search?q=...&owner_id=... — D1 LIKE keyword search (base)
// GET /entries/search?q=...&owner_id=...&source=...&entry_type=...&mode=keyword|semantic
// - mode=keyword(預設):D1 LIKEbase,永遠可用)。
// - mode=semantic:需 embed 模組開(Vectorize+AI binding)。未開 → 降級 keyword + capability_hint 告知缺能力(#7 發現閉環)。
// - entry_typebase 通用 filtercaller 傳任意 type,如 workflowbase 不寫死語意,workflow-discovery Q4)。
entryRoutes.get('/search', async (c) => {
const q = c.req.query('q');
if (!q) return c.json({ success: false, error: 'q required' }, 400);
const entries = await searchEntries(c.env.DB, q, c.req.query('owner_id') || undefined);
const owner_id = c.req.query('owner_id') || undefined;
const source = c.req.query('source') || undefined;
const entry_type = c.req.query('entry_type') || undefined;
const mode = c.req.query('mode') === 'semantic' ? 'semantic' : 'keyword';
if (mode === 'semantic') {
const hits = await semanticSearch(c.env, q, { owner_id, source, entry_type });
if (hits === null) {
// 模組沒開:誠實降級 keyword + 告知「叫 CC 幫你開 vectorize」(不假裝有語義)。
const entries = await searchEntries(c.env.DB, q, owner_id, entry_type);
return c.json({
success: true,
entries,
count: entries.length,
mode: 'keyword',
requested_mode: 'semantic',
capability_hint:
'語義查詢需先開 vectorizeembed 模組)。叫 CC「幫我開語義查詢」即可(設 kbdb_embed:true + redeploy)。本次已降級關鍵字搜尋。',
});
}
// hydrate vector hits → 完整 entry(保持回應形狀與 keyword 一致)。
const entries = (await Promise.all(hits.map((h) => getEntry(c.env.DB, h.id)))).filter(
(e): e is NonNullable<typeof e> => e !== null,
);
return c.json({ success: true, entries, count: entries.length, mode: 'semantic' });
}
const entries = await searchEntries(c.env.DB, q, owner_id, entry_type);
return c.json({ success: true, entries, count: entries.length, mode: 'keyword' });
});
@@ -53,11 +90,19 @@ entryRoutes.patch('/:id', async (c) => {
const body = await c.req.json().catch(() => ({}));
const entry = await updateEntry(c.env.DB, c.req.param('id'), body);
if (!entry) return c.json({ success: false, error: 'not found' }, 404);
// 內容改了 → 重 embed(保持向量新鮮)。embedOnWrite 內部自會檢查模組開 + entry 是否 embeddable。
if (embedEnabled(c.env) && body.content !== undefined) {
c.executionCtx.waitUntil(embedOnWrite(c.env, entry).catch(() => {}));
}
return c.json({ success: true, entry });
});
// DELETE /entries/:id
entryRoutes.delete('/:id', async (c) => {
// 模組開 → 連帶刪向量(避免孤兒向量)。失敗不致命。
if (embedEnabled(c.env)) {
c.executionCtx.waitUntil(c.env.VECTORIZE!.deleteByIds([c.req.param('id')]).then(() => {}).catch(() => {}));
}
await deleteEntry(c.env.DB, c.req.param('id'));
return c.json({ success: true });
});
+17 -1
View File
@@ -1,7 +1,7 @@
// Records route — structured records (entry_values composed by a template).
import { Hono } from 'hono';
import type { Bindings } from '../types';
import { createRecord, getRecord, searchByTemplate } from '../actions/record-crud';
import { createRecord, getRecord, searchByTemplate, updateRecord } from '../actions/record-crud';
export const recordRoutes = new Hono<{ Bindings: Bindings }>();
@@ -31,3 +31,19 @@ recordRoutes.get('/:recordId', async (c) => {
if (!rec) return c.json({ success: false, error: 'not found' }, 404);
return c.json({ success: true, record: rec });
});
// PATCH /records/:recordId — { values:{slot:content} } update existing record slot values
// (mira-dissolve T2.1 / issue #6; deprecate = flip a slot value, append-only tables untouched).
recordRoutes.patch('/:recordId', async (c) => {
const body = await c.req.json().catch(() => null);
if (!body || !body.values || typeof body.values !== 'object') {
return c.json({ success: false, error: 'values required' }, 400);
}
try {
const rec = await updateRecord(c.env.DB, c.req.param('recordId'), body.values);
if (!rec) return c.json({ success: false, error: 'not found' }, 404);
return c.json({ success: true, record: rec });
} catch (e) {
return c.json({ success: false, error: e instanceof Error ? e.message : String(e) }, 400);
}
});
+5
View File
@@ -4,6 +4,11 @@
export type Bindings = {
DB: D1Database;
ENVIRONMENT: string;
// Optional embed module (issue #7 / SDD T2.4). Present ONLY when the self-host opened
// semantic search (kbdb_embed:true → deploy injects [[vectorize]] + [ai]). Base never
// requires them; code checks `if (env.VECTORIZE && env.AI)` before touching embed.
VECTORIZE?: VectorizeIndex;
AI?: Ai;
};
export type EntryType =
+14 -1
View File
@@ -10,7 +10,20 @@ compatibility_flags = ["nodejs_compat"]
[[d1_databases]]
binding = "DB"
database_name = "arcrun-kbdb"
database_id = "placeholder-replaced-by-init"
database_id = "0c580910-e00b-4f8e-9c57-ac54ea52242f" # 官方 prod D1arcrun-kbdb);self-hosted deploy.ts 會注入用戶自己的 id 覆蓋
[vars]
ENVIRONMENT = "production"
# ── Optional embed module (issue #7 / SDD T2.4) ────────────────────────────────
# Base 預設不開(free-tier 友善)。self-host 開語義查詢時,deploy.ts 偵測 config kbdb_embed:true
# → 取消下面兩段註解(注入 active binding)並 `wrangler vectorize create arcrun-kbdb-embed
# --dimensions=768 --metric=cosine`bge-base-en-v1.5 = 768 維)。官方帳號同理由 deploy 注入。
# 沒有這兩個 binding 時,kbdb/src/embed.ts 的 embedEnabled() 回 false → 維持 LIKE keyword、API 不變。
#
# [[vectorize]]
# binding = "VECTORIZE"
# index_name = "arcrun-kbdb-embed"
#
# [ai]
# binding = "AI"
+17 -8
View File
@@ -30,14 +30,23 @@ npm i -g arcrun && acr install-harness
1. `npm i -g arcrun && acr install-harness`(裝 CLI + 防護)
2. `npm i -g wrangler`Cloudflare CLI,部署用)
3. **帶使用者拿 Cloudflare 的兩串憑證**(唯一只能他做的事)——
用白話照抄式引導,**不要對使用者講 KV / Worker / R2 / zone 等術語**(他多半聽不懂,也不需要懂):
- 帳號代碼(Account ID):登入 https://dash.cloudflare.com 右側欄複製
- 金鑰(API Token):https://dash.cloudflare.com/profile/api-tokens → Create Custom Token →
照抄勾**兩組**權限(Account/Workers Scripts/Edit、Account/Workers KV Storage/Edit
→ 複製產生的 token。(不需要 R2、不需要綁信用卡——只用 Workers + KV 免費額度。)
- 叫使用者把這兩串貼給你。
4. `acr init --self-hosted`(你幫跑,貼使用者的兩串)—— 自動建資源、部署、seed、寫 .mcp.jsonMCP 連線)。
3. **建 .env 並帶使用者填憑證**(憑證是唯一只有他能拿的東西,但「建檔」由你做):
- **你做**`cp .env.example .env`repo 根有 `.env.example` 範本,左邊 KEY 名都寫好了、
每格上面有白話說明)。使用者只需要找到值、填到「=」右邊
- **帶使用者拿值**——用白話照抄式引導,**不要講 KV / Worker / R2 / zone 等術語**(他聽不懂也不需懂):
- 帳號代碼(`CLOUDFLARE_ACCOUNT_ID`):登入 https://dash.cloudflare.com 右側欄複製。
- 金鑰(`CLOUDFLARE_API_TOKEN`):https://dash.cloudflare.com/profile/api-tokens → Create Custom Token →
照抄勾**三組**權限:
· Account / Workers Scripts / Edit
· Account / Workers KV Storage / Edit
· Account / D1 / Edit ← **必勾**arcrun 用 D1 存 workflow/recipe;漏勾會 init 時 D1 建失敗(Authentication error
→ 複製產生的 token。(不需要 R2、不需要綁信用卡——D1 也在免費額度,不綁卡。)
- `NAMESPACE`:隨便取個英數小名(非密碼)。`ENCRYPTION_KEY`:你可幫他產
`node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"`)。
- 使用者把值貼進 .env(或貼給你、你幫他填進對應格)。**CLOUDFLARE 兩格沒填,後面什麼都跑不了。**
- 連外部服務(如 Notion)的 token 也填進 .env 的 ③ 區,之後 `acr creds push` 加密上傳。
4. `acr init --self-hosted`(你幫跑,讀 .env 的 CF 憑證)—— 自動建資源、部署、seed、寫 .mcp.jsonMCP 連線)。
跑完會印「安裝驗收」逐項 ✓/✗;有 ✗ 照它給的指令補(多數 `acr update` 冪等重試)。
5. 跑完照提示 `wrangler secret put ENCRYPTION_KEY`CLI 會印確切指令)。
6. 把使用者需求拆成 workflow → `acr push`。完成給客觀證據(HTTP 2xx / trace)。
+8
View File
@@ -0,0 +1,8 @@
allowBuilds:
esbuild: true
sharp: true
workerd: true
onlyBuiltDependencies:
- esbuild
- sharp
- workerd
+1 -1
View File
@@ -233,7 +233,7 @@ app.options("/mcp", (c) => {
});
});
app.post("/mcp", partnerAuthMiddleware, async (c) => {
app.post("/", partnerAuthMiddleware, async (c) => {
const orgNamespace = c.get("org_namespace");
const partnerToken = c.get("partner_token");
return handleMcpRequest(c.req.raw, c.env, orgNamespace, partnerToken);
+13
View File
@@ -11,6 +11,19 @@ export async function partnerAuthMiddleware(
}
const token = authHeader.slice(7);
// Self-hosted 單租戶(MULTI_TENANT=false):Bearer 帶的是 namespace 明碼,不是平台 partner key。
// 與 cypher-executor 一致——cypher 把 X-Arcrun-API-Key 當「不驗證的 opaque 分區 key」(namespace
// 是明碼分區標籤非密碼,mindset §3 arcrun 不做授權判斷)。故 self-hosted 模式不打 KBDB partner
// 驗證,直接把 token 當 org_namespace。SDD: mcp-account-source.mdHANDOFF §3b。
if (c.env.MULTI_TENANT === 'false') {
c.set('org_namespace', token);
c.set('partner_token', token); // 下游轉發給 cypher 當 X-Arcrun-API-Key(與 CLI 同一份身份)
await next();
return;
}
// 官方 SaaSMULTI_TENANT 未設 / "true"):維持 partner-key 驗證(行為不變)。
const resp = await c.env.KBDB.fetch(
`http://kbdb/partners/${encodeURIComponent(token)}/info`,
{
+14 -10
View File
@@ -51,11 +51,12 @@ export function registerReportFeedback(server: McpServer, env: Env, orgNamespace
return { content: [{ type: "text", text: "Error: KBDB service binding unavailable" }], isError: true };
}
const blockBody = {
api_key: env.PLATFORM_API_KEY || undefined, // 若 platform key 在,聚集;否則用用戶 namespace
type: "agent-feedback",
source: "mcp-tool-call",
user_id: orgNamespace,
// kbdb-base 9.7:寫進基本盤 entriesentry_type=agent-feedback)。
// 舊版打 v3 死 route /blocks(基本盤只 mount entries/templates/records)→ 404 假紅,已改。
// owner_id = 用戶 namespaceself-hosted 單租戶聚集)。基本盤無 source/api_key 欄 → 併入 metadata。
const entryBody = {
entry_type: "agent-feedback",
owner_id: orgNamespace,
content: description,
metadata_json: JSON.stringify({
issue_type,
@@ -64,6 +65,7 @@ export function registerReportFeedback(server: McpServer, env: Env, orgNamespace
blocked: blocked ?? false,
suggested_fix,
agent_user_agent,
source: "mcp-tool-call",
reported_at: new Date().toISOString(),
}),
tags_json: JSON.stringify([
@@ -74,11 +76,11 @@ export function registerReportFeedback(server: McpServer, env: Env, orgNamespace
]),
};
// 走 KBDB service binding(既有 pattern
const createResp = await kbdbFetch(env, `/blocks`, {
// 走 KBDB service binding 打基本盤 /entries(薄殼模式不變
const createResp = await kbdbFetch(env, `/entries`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(blockBody),
body: JSON.stringify(entryBody),
});
if (!createResp.ok) {
@@ -92,7 +94,7 @@ export function registerReportFeedback(server: McpServer, env: Env, orgNamespace
error_code: "kbdb_write_failed",
human_message: `回饋寫入 KBDB 失敗:HTTP ${createResp.status}`,
next_actions: [
"確認 KBDB 服務在線(試 https://kbdb-get.arcrun.dev/health",
"確認 KBDB 服務在線(KBDB worker /health",
"若持續失敗,可暫先在本地記下回饋,稍後重試",
],
detail: errBody.slice(0, 200),
@@ -114,7 +116,9 @@ export function registerReportFeedback(server: McpServer, env: Env, orgNamespace
data: {
reported: true,
issue_type,
block_id: (data as { id?: string } | null)?.id,
// 基本盤 /entries 回 { success, entry };舊 /blocks 回 { id } → 兩種都容忍
entry_id: (data as { entry?: { id?: string }; id?: string } | null)?.entry?.id
?? (data as { id?: string } | null)?.id,
},
hints: [
issue_type === "success_story"
+62 -50
View File
@@ -1,18 +1,27 @@
/**
* Skills + Examples lookup MCP tools LI SDD M3.2
* Skills + Examples lookup MCP tools LI SDD M3.2 / M3.4
*
* .agents/specs/llm-interface/ Milestone 3.2 + 3.4
* docs/3-specs/llm-interface/ Milestone 3.2 + 3.4
*
* - arcrun_list_skills KBDB type=agent-skill
* - arcrun_list_skills KBDB entry_type=agent-skill
* - arcrun_get_skill slug skill markdown
* - arcrun_list_examples KBDB type=workflow-example
* - arcrun_list_examples KBDB entry_type=workflow-example
* - arcrun_get_example slug example yaml + description + tags
* - arcrun_search_examples use case example
* - arcrun_search_examples use case example
*
* Skills / examples arcrun/scripts/sync-registry-to-kbdb.py
* arcrun/registry/{skills,examples} KBDB
*
* KBDB service binding pattern cypher-executor
*
* 2026-06-14 KBDB entries/templates/records v3 blocks
* search /blocks /search /entries
* - entry_type blocks type entries entry_type/page_name/tags_json/metadata_json
* - GET /blocks?type=X GET /entries?entry_type=X
* - GET /blocks?page_name=Y GET /entries?page_name=Ybase listEntries page_name
* - POST /search GET /entries/search?q=D1 LIKE
* search_examples embed kbdb-base Phase 1
*
*/
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
@@ -21,29 +30,30 @@ import type { Env } from "../types.js";
import { kbdbFetch } from "../lib/kbdb-client.js";
import { errorResponse, successResponse } from "../lib/cypher-client.js";
// 基本盤 entries row(與舊 v3 block 欄位 1:1,差別只在 type→entry_type
interface KbdbBlock {
id: string;
page_name?: string | null;
content?: string | null;
type?: string;
entry_type?: string;
tags_json?: string;
metadata_json?: string | null;
source?: string | null;
updated_at?: number;
}
async function kbdbList(env: Env, type: string, limit = 100): Promise<KbdbBlock[]> {
const resp = await kbdbFetch(env, `/blocks?type=${type}&limit=${limit}`);
if (!resp.ok) throw new Error(`KBDB list type=${type} HTTP ${resp.status}`);
const data = await resp.json<{ blocks?: KbdbBlock[] }>();
return data.blocks ?? [];
async function kbdbList(env: Env, entryType: string, limit = 100): Promise<KbdbBlock[]> {
const resp = await kbdbFetch(env, `/entries?entry_type=${encodeURIComponent(entryType)}&limit=${limit}`);
if (!resp.ok) throw new Error(`KBDB list entry_type=${entryType} HTTP ${resp.status}`);
const data = await resp.json<{ entries?: KbdbBlock[] }>();
return data.entries ?? [];
}
async function kbdbGetByPageName(env: Env, pageName: string): Promise<KbdbBlock | null> {
const resp = await kbdbFetch(env, `/blocks?page_name=${encodeURIComponent(pageName)}&limit=1`);
const resp = await kbdbFetch(env, `/entries?page_name=${encodeURIComponent(pageName)}&limit=1`);
if (!resp.ok) return null;
const data = await resp.json<{ blocks?: KbdbBlock[] }>();
return data.blocks?.[0] ?? null;
const data = await resp.json<{ entries?: KbdbBlock[] }>();
return data.entries?.[0] ?? null;
}
function parseTags(tagsJson?: string): string[] {
@@ -234,54 +244,55 @@ export function registerGetExample(server: McpServer, env: Env) {
export function registerSearchExamples(server: McpServer, env: Env) {
server.tool(
"arcrun_search_examples",
"用自然語言 use case 搜 workflow examples回最相關 N 個。內部走 KBDB semantic searchembedding 比對)+ tag 過濾。",
"用 use case 關鍵字搜 workflow examples回最相關 N 個。" +
"注意:基本盤目前是 D1 LIKE 關鍵字搜尋(非語義 embedding;語義是 kbdb-base Phase 1 的 embed 模組,尚未上)。" +
"→ 用具體詞('email'、'cron'、'rag')比整句自然語言命中率高。也會比對 slug/tag。",
{
query: z.string().min(3).describe("use case 描述,例如 '每天早上發 email 摘要' / 'RAG 從文件回答問題'"),
query: z.string().min(2).describe("use case 關鍵字,例如 'email 摘要' / 'cron 排程' / 'rag'。基本盤是關鍵字非語義,用詞要具體"),
top_k: z.number().int().min(1).max(20).optional().describe("回幾個結果(預設 5"),
},
async ({ query, top_k }) => {
try {
const k = top_k ?? 5;
// KBDB /search 是 unified semantic search(既有),filter type=workflow-example
const resp = await kbdbFetch(env, `/search`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
query,
topK: k * 3, // overfetch 後 filter type
}),
});
const q = query.trim();
if (!resp.ok) {
return errorResponse(
"fetch_failed",
`KBDB search HTTP ${resp.status}`,
["稍後重試", "改用 arcrun_list_examples(tag=...) 過濾"],
await resp.text().catch(() => ""),
);
// 基本盤無語義 search:撈全部 workflow-example,用 query 對 content/slug/tag 做關鍵字比對排序。
// examples 只有 ~10 筆,client 端過濾零負擔;embed 模組上線後可改打語義 search)
const blocks = await kbdbList(env, "workflow-example", 200);
const ql = q.toLowerCase();
const terms = ql.split(/\s+/).filter(Boolean);
const scored = blocks
.map((b) => {
const slug = b.page_name?.replace(/^example-/, "") ?? "";
const tags = parseTags(b.tags_json);
const hay = `${slug} ${tags.join(" ")} ${(b.content ?? "")}`.toLowerCase();
// 每個 term 命中 +1slug/tag 命中額外加權
let score = 0;
for (const t of terms) {
if (hay.includes(t)) score += 1;
if (slug.toLowerCase().includes(t)) score += 2;
if (tags.some((tag) => tag.toLowerCase().includes(t))) score += 2;
}
return { b, slug, tags, score };
})
.filter((r) => r.score > 0)
.sort((a, b) => b.score - a.score)
.slice(0, k);
const data = await resp.json<{ results?: Array<{ block?: KbdbBlock; score?: number }> }>();
const all = data.results ?? [];
const examples = all
.filter((r) => r.block?.type === "workflow-example")
.slice(0, k)
.map((r) => {
const b = r.block!;
return {
slug: b.page_name?.replace(/^example-/, "") ?? "",
page_name: b.page_name,
const examples = scored.map((r) => ({
slug: r.slug,
page_name: r.b.page_name,
score: r.score,
tags: parseTags(b.tags_json),
preview: (b.content ?? "").slice(0, 200),
};
});
tags: r.tags,
preview: (r.b.content ?? "").slice(0, 200),
}));
if (examples.length === 0) {
return successResponse(
{ count: 0, examples: [], query },
{ count: 0, examples: [], query: q },
[
"沒命中。可能 KBDB /search 還在等 embedding 建好(剛 sync 完要 1-5 分鐘",
"關鍵字沒命中(基本盤是 LIKE 非語義,換更具體/不同的詞再試",
"改用 arcrun_list_examples(tag='...') 走 tag 過濾",
"或 arcrun_list_examples() 看全部清單自己挑",
],
@@ -289,10 +300,11 @@ export function registerSearchExamples(server: McpServer, env: Env) {
}
return successResponse(
{ count: examples.length, examples, query },
{ count: examples.length, examples, query: q, search_mode: "keyword" },
[
"call arcrun_get_example(slug) 拿完整 YAML",
"score 高 = 跟你 query 更相關",
"score 高 = 關鍵字命中越多(slug/tag 命中加權)",
"search_mode:keyword — 基本盤無語義,命中靠字面;換具體詞可改善",
],
);
} catch (e) {
+34
View File
@@ -0,0 +1,34 @@
/**
* arcrun_whoami MCP §7.8 P1 D2 CLI acr whoami
*
* D2 self-hosted-init.md §7.8AI curl
* AI CLI acr whoamiMCP rule 07 §5
* AI MCP curl
*
* MCP orgNamespace+ cypher binding
*/
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { Env } from "../types.js";
export function registerWhoami(server: McpServer, env: Env, orgNamespace: string) {
server.tool(
"arcrun_whoami",
"回報這個 MCP 連線目前生效的身份:綁哪個帳號 / namespace、cypher 連向哪。" +
"部署 / 觸發 / 查 workflow 前先 call 此 tool 確認帳號,**不要自己 curl 猜帳號 URL**(會打到錯帳號)。",
{},
async () => {
// 薄殼:MCP 透過 service bindingCYPHER_EXECUTOR)連 cypherbinding 本身決定連哪台;
// 身份來自啟動時解析的 orgNamespace(綁哪個帳號的資料分區)。這裡只如實回報,不做推斷。
const identity = {
account_namespace: orgNamespace || "(未設)",
cypher: "service-binding:CYPHER_EXECUTOR",
kbdb: "service-binding:KBDB",
note:
"此 MCP 已綁定上述帳號。部署/觸發/查詢都走這個身份;勿自行 curl 其他 URL 猜帳號。",
};
return {
content: [{ type: "text" as const, text: JSON.stringify(identity, null, 2) }],
};
},
);
}
+211
View File
@@ -0,0 +1,211 @@
/**
* KBDB MCP kbdb-base Phase 9.1HANDOFF §2
*
* rule 07 §5 APIMCP +
* kbdbFetchKBDB service binding HTTP APIkbdb/src/routes/*
*
* KBDB leo 2026-06-14 DECISION-kbdb-v3-baseplane.md
* - ** / SQL tool**
* - AI templatename+slots+ recordslotcontent
* Supabase schema template/slot CREATE TABLE
* - 調 HTTP API D1 SQL
*
* API kbdb/src/routes
* POST /templates { name, slots[], description?, created_by? } { template }
* GET /templates { templates[], count }
* GET /templates/:idOrName { template }
* POST /records { template, values:{slot:content}, owner_id? } { record }
* GET /records/by-template/:t ?owner_id= { records[], count }
* GET /records/:recordId { record }
* GET /entries/search ?q=&owner_id= { entries[], count, mode:'keyword' }
*/
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { z } from "zod";
import type { Env } from "../types.js";
import { kbdbFetch } from "../lib/kbdb-client.js";
import { errorResponse, successResponse } from "../lib/cypher-client.js";
/** 註冊全部 KBDB 資料層工具(kbdb-base Phase 9.1)。不含建表/SQL tool(鐵律)。 */
export function registerAllKbdbDataTools(server: McpServer, env: Env) {
registerCreateTemplate(server, env);
registerListTemplates(server, env);
registerCreateRecord(server, env);
registerGetRecord(server, env);
registerQuery(server, env);
registerSearch(server, env);
}
/**
* kbdb_create_template template= /
* AI API template + slots
*/
export function registerCreateTemplate(server: McpServer, env: Env) {
server.tool(
"kbdb_create_template",
"建一個 KBDB template(萬用表裡的一種資料形狀,類 Supabase 的虛擬表)。KBDB 不能建真的資料表——" +
"要存「新類型」的結構化資料時,就建一個 template 並用 slots 列出它的欄位名,之後用 kbdb_create_record 填值。" +
"例:name='contact', slots=['name','email','phone']。",
{
name: z.string().min(1).describe("template 名稱(唯一識別,之後填 record 用這個名字),如 'contact' / 'note'"),
slots: z.array(z.string().min(1)).min(1).describe("欄位名清單,如 ['name','email','phone']"),
description: z.string().optional().describe("這個 template 用途的簡述(選填)"),
created_by: z.string().optional().describe("建立者標記(選填)"),
},
async ({ name, slots, description, created_by }) => {
try {
const res = await kbdbFetch(env, "/templates", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ name, slots, description, created_by }),
});
if (!res.ok) {
return errorResponse("create_template_failed", `建 template 失敗`, ["檢查 name 是否重複", "確認 slots 是非空字串陣列"], await res.text().catch(() => ""));
}
const data = await res.json();
return successResponse(data, [
`template「${name}」已建。用 kbdb_create_record(template='${name}', values={...}) 填一筆資料`,
]);
} catch (e) {
return errorResponse("internal_error", e instanceof Error ? e.message : String(e), ["稍後重試"]);
}
},
);
}
/** kbdb_list_templates — 列出所有已建的 template(看有哪些資料形狀可用)。 */
export function registerListTemplates(server: McpServer, env: Env) {
server.tool(
"kbdb_list_templates",
"列出 KBDB 裡所有 template(已定義的資料形狀)。要存資料前先看有沒有現成 template 可用,沒有再 kbdb_create_template。",
{},
async () => {
try {
const res = await kbdbFetch(env, "/templates");
if (!res.ok) return errorResponse("list_templates_failed", `列 template 失敗`, ["稍後重試"], await res.text().catch(() => ""));
const data = await res.json();
return successResponse(data, ["每個 template 的 slots_json 是它的欄位清單", "填資料用 kbdb_create_record"]);
} catch (e) {
return errorResponse("internal_error", e instanceof Error ? e.message : String(e), ["稍後重試"]);
}
},
);
}
/** kbdb_create_record — 依某 template 填一筆 recordslot → 內容)。 */
export function registerCreateRecord(server: McpServer, env: Env) {
server.tool(
"kbdb_create_record",
"依某 template 填一筆 record(一列資料)。values 是 {slot名: 內容}slot 名要對得上 template 的 slots。" +
"template 不存在會失敗——先 kbdb_list_templates 確認,或 kbdb_create_template 建一個。",
{
template: z.string().min(1).describe("template 的 name 或 id"),
values: z.record(z.string()).describe("欄位內容 {slot名: 字串內容},如 {name:'Leo', email:'leo@x.com'}"),
owner_id: z.string().optional().describe("資料歸屬標記(選填,如專案 id / 用戶 id)"),
},
async ({ template, values, owner_id }) => {
try {
const res = await kbdbFetch(env, "/records", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ template, values, owner_id }),
});
if (!res.ok) {
return errorResponse("create_record_failed", `填 record 失敗`, [
`確認 template「${template}」存在(kbdb_list_templates`,
"values 的 slot 名要對得上 template 的 slots",
], await res.text().catch(() => ""));
}
const data = await res.json();
return successResponse(data, [`已存入。用 kbdb_query(template='${template}') 列出此 template 的所有 record`]);
} catch (e) {
return errorResponse("internal_error", e instanceof Error ? e.message : String(e), ["稍後重試"]);
}
},
);
}
/** kbdb_get_record — 用 record_id 取單筆 record。 */
export function registerGetRecord(server: McpServer, env: Env) {
server.tool(
"kbdb_get_record",
"用 record_id 取一筆 record 的所有欄位內容。record_id 從 kbdb_create_record 回傳或 kbdb_query 列出取得。",
{
record_id: z.string().min(1).describe("record 的 idrec_xxx"),
},
async ({ record_id }) => {
try {
const res = await kbdbFetch(env, `/records/${encodeURIComponent(record_id)}`);
if (res.status === 404) return errorResponse("not_found", `record「${record_id}」不存在`, ["確認 record_id 正確", "用 kbdb_query 列出某 template 的 record 取 id"]);
if (!res.ok) return errorResponse("get_record_failed", `取 record 失敗`, ["稍後重試"], await res.text().catch(() => ""));
const data = await res.json();
return successResponse(data);
} catch (e) {
return errorResponse("internal_error", e instanceof Error ? e.message : String(e), ["稍後重試"]);
}
},
);
}
/** kbdb_query — 列出某 template 底下的所有 record(結構化查詢)。 */
export function registerQuery(server: McpServer, env: Env) {
server.tool(
"kbdb_query",
"列出某 template 底下的所有 record(結構化查詢,按 template 取整批資料)。要按關鍵字找內容用 kbdb_search。",
{
template: z.string().min(1).describe("template 的 name 或 id"),
owner_id: z.string().optional().describe("只取某歸屬的 record(選填)"),
},
async ({ template, owner_id }) => {
try {
const path = `/records/by-template/${encodeURIComponent(template)}` + (owner_id ? `?owner_id=${encodeURIComponent(owner_id)}` : "");
const res = await kbdbFetch(env, path);
if (!res.ok) return errorResponse("query_failed", `查詢 record 失敗`, [`確認 template「${template}」存在`], await res.text().catch(() => ""));
const data = await res.json();
return successResponse(data, ["用 kbdb_get_record(record_id) 取單筆全文", "按關鍵字找內容改用 kbdb_search"]);
} catch (e) {
return errorResponse("internal_error", e instanceof Error ? e.message : String(e), ["稍後重試"]);
}
},
);
}
/**
* kbdb_search entries mode=keywordD1 LIKE semantic embed
* / KBDB MCP RAGissue #7 / D17
* mode=semantic vectorize base keyword + capability_hint CC
*/
export function registerSearch(server: McpServer, env: Env) {
server.tool(
"kbdb_search",
"搜尋 KBDB 內容。mode='keyword'(預設,D1 LIKE 關鍵字,基本盤永遠可用)或 'semantic'AI 向量語義搜尋," +
"需先開 embed 模組)。語義沒開時會自動降級關鍵字並告訴你怎麼開。要按 template 取整批結構化資料用 kbdb_query。",
{
q: z.string().min(1).describe("搜尋關鍵字 / 語義查詢句"),
owner_id: z.string().optional().describe("限定某歸屬範圍內搜(選填)"),
source: z.string().optional().describe("只搜某來源(ingest source.uri,選填)"),
mode: z.enum(["keyword", "semantic"]).optional().describe("keyword(預設)或 semantic(需開 vectorize"),
},
async ({ q, owner_id, source, mode }) => {
try {
const qs = new URLSearchParams({ q });
if (owner_id) qs.set("owner_id", owner_id);
if (source) qs.set("source", source);
if (mode) qs.set("mode", mode);
const res = await kbdbFetch(env, `/entries/search?${qs.toString()}`);
if (!res.ok) return errorResponse("search_failed", `搜尋失敗`, ["稍後重試"], await res.text().catch(() => ""));
const data = (await res.json()) as { mode?: string; capability_hint?: string };
// base 回 capability_hint → 語義沒開、已降級 keyword。把它當 next-step 傳給 AI(發現閉環)。
const hints =
data.capability_hint
? [data.capability_hint, "要開:跟用戶確認後,CC 可代開(寫 config kbdb_embed:true + acr update"]
: data.mode === "semantic"
? ["mode:semantic = AI 向量語義搜尋"]
: ["mode:keyword = D1 LIKE(基本盤)", "想要語義搜尋:mode='semantic'(需先開 vectorize"];
return successResponse(data, hints);
} catch (e) {
return errorResponse("internal_error", e instanceof Error ? e.message : String(e), ["稍後重試"]);
}
},
);
}
+10 -1
View File
@@ -6,6 +6,7 @@ import { registerDeployWorkflow } from "./u6u_deploy_workflow.js";
import { registerPublishComponent } from "./u6u_publish_component.js";
import { registerListWorkflows } from "./u6u_list_workflows.js";
import { registerGetWorkflow } from "./u6u_get_workflow.js";
import { registerSearchWorkflows } from "./u6u_search_workflows.js";
import { registerListComponents } from "./u6u_list_components.js";
import { registerGetComponent } from "./u6u_get_component.js";
import { registerGetComponentGuide } from "./u6u_get_component_guide.js";
@@ -20,14 +21,17 @@ import { registerAllIntrospectionTools } from "./arcrun_introspection.js";
import { registerAllWorkflowCrudTools } from "./arcrun_workflow_crud.js";
import { registerAllSkillExampleTools } from "./arcrun_skills_examples.js";
import { registerAllRecipeTools } from "./arcrun_recipe.js";
import { registerAllKbdbDataTools } from "./kbdb_data.js";
import { registerWhoami } from "./arcrun_whoami.js";
export function registerAllTools(server: McpServer, env: Env, orgNamespace: string, partnerToken: string) {
registerSearchComponents(server, env, orgNamespace);
registerExecuteWorkflow(server, env, orgNamespace, partnerToken);
registerDeployWorkflow(server, env, orgNamespace);
registerPublishComponent(server, env, orgNamespace);
registerListWorkflows(server, env, orgNamespace);
registerListWorkflows(server, env, orgNamespace, partnerToken); // thin-shell-alignment P1: list 改讀 /webhooks/named
registerGetWorkflow(server, env, orgNamespace);
registerSearchWorkflows(server, env, orgNamespace, partnerToken); // workflow-discovery R2
registerListComponents(server, env, orgNamespace);
registerGetComponent(server, env, orgNamespace);
registerGetComponentGuide(server, env, orgNamespace);
@@ -49,4 +53,9 @@ export function registerAllTools(server: McpServer, env: Env, orgNamespace: stri
registerAllSkillExampleTools(server, env);
// kbdb-base §7.5.i: recipe 公庫/私庫工具(與 CLI 六能力對齊,rule 07 §5 MCP 不落後)
registerAllRecipeTools(server, env);
// kbdb-base Phase 9.1: KBDB 資料層薄殼(template/record/query/searchHANDOFF §2
// 鐵律:不提供建表/SQL toolAI 只有 template+slot 可用(類 Supabase 萬用表)
registerAllKbdbDataTools(server, env);
// §7.8 P1 D2: whoami(與 CLI acr whoami 對齊,AI 不繞 CLI 自己 curl 猜帳號)
registerWhoami(server, env, orgNamespace);
}
+34 -11
View File
@@ -3,33 +3,56 @@ import { z } from "zod";
import { Env } from "../types.js";
import { kbdbFetch } from "../lib/kbdb-client.js";
export function registerListWorkflows(server: McpServer, env: Env, orgNamespace: string) {
/**
* u6u_list_workflows
*
* thin-shell-alignment P1issue #11 KBDB `/records?template=workflow_metadata`
* CLI `acr list` KV 西 cypher `GET /webhooks/named`
* KV CLI job list KVsearch KBDB entry
* tag KBDB resource_tag workflow
*/
export function registerListWorkflows(server: McpServer, env: Env, orgNamespace: string, partnerToken: string) {
server.tool(
"u6u_list_workflows",
"列出當前命名空間下所有已部署的工作流,可選擇按 tag 篩選。",
{ tag: z.string().optional().describe("按 tag 名稱篩選(選填)") },
async ({ tag }) => {
try {
if (!env.KBDB) {
return { content: [{ type: "text", text: "Error: KBDB service binding unavailable" }], isError: true };
if (!env.CYPHER_EXECUTOR) {
return { content: [{ type: "text", text: "Error: CYPHER_EXECUTOR service binding is not configured." }], isError: true };
}
let workflowIds: string[] | null = null;
// tag 過濾(選填):先從 KBDB resource_tag 查出該 tag 下的 workflow_id 白名單。
let tagFilter: Set<string> | null = null;
if (tag) {
if (!env.KBDB) {
return { content: [{ type: "text", text: "Error: KBDB service binding unavailable (tag 過濾需要)" }], isError: true };
}
const tagResp = await kbdbFetch(env, `/records/search?template=resource_tag&user_id=${encodeURIComponent(orgNamespace)}&tag_name=${encodeURIComponent(tag)}&resource_type=workflow`);
if (!tagResp.ok) {
return { content: [{ type: "text", text: `Error querying tags: ${await tagResp.text()}` }], isError: true };
}
const tagData = await tagResp.json<{ records: Array<{ slots: { resource_id: string } }> }>();
workflowIds = tagData.records.map(r => r.slots.resource_id);
if (workflowIds.length === 0) return { content: [{ type: "text", text: JSON.stringify([], null, 2) }] };
tagFilter = new Set(tagData.records.map(r => r.slots.resource_id));
if (tagFilter.size === 0) return { content: [{ type: "text", text: JSON.stringify([], null, 2) }] };
}
const resp = await kbdbFetch(env, `/records/search?template=workflow_metadata&user_id=${encodeURIComponent(orgNamespace)}`);
// 主清單:讀 cypher GET /webhooks/namedKV 源,與 CLI acr list 同源)。
const resp = await env.CYPHER_EXECUTOR.fetch("http://cypher-executor/webhooks/named", {
method: "GET",
headers: { "X-Arcrun-API-Key": partnerToken },
});
if (!resp.ok) {
return { content: [{ type: "text", text: `Error querying workflows: ${await resp.text()}` }], isError: true };
return { content: [{ type: "text", text: `Error listing workflows: ${await resp.text()}` }], isError: true };
}
const data = await resp.json<{ records: Array<{ slots: { workflow_id: string; name: string; deployed_at: string; org_namespace: string } }> }>();
let workflows = data.records.map(r => r.slots);
if (workflowIds !== null) workflows = workflows.filter(w => workflowIds!.includes(w.workflow_id));
const data = await resp.json<{ workflows: Array<{ name: string; description?: string; created_at?: string; webhook_url?: string }> }>();
let workflows = data.workflows ?? [];
// tag 過濾用 name 比對(KV 源主鍵=name)。
// ⚠️ 語意債(SDD §4 記):舊 tag 的 resource_id 可能存 UUID(舊 workflow_metadata workflow_id
// 或 name,語意不明確。方向①收斂到 KV(主鍵=name)後,resource_id 應統一為 name。
// 過渡期先用 name 比對(KV 源唯一鍵);舊用 UUID tag 的會在 backfill/re-tag 後對齊。待總管確認 tag 收斂。
if (tagFilter !== null) workflows = workflows.filter(w => tagFilter!.has(w.name));
return { content: [{ type: "text", text: JSON.stringify(workflows, null, 2) }] };
} catch (error) {
return { content: [{ type: "text", text: `Internal Error: ${error instanceof Error ? error.message : String(error)}` }], isError: true };
+86
View File
@@ -0,0 +1,86 @@
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { z } from "zod";
import { Env } from "../types.js";
/**
* u6u_search_workflows workflow-discovery R2
*
* AI
* cypher GET /workflows/search KBDB /entries/searchentry_type=workflow +
* KBDB Vectorize + capability_hint
*
* rule 07 + + u6u_search_components
* flag AI call /
*/
export function registerSearchWorkflows(
server: McpServer,
env: Env,
orgNamespace: string,
partnerToken: string,
) {
server.tool(
"u6u_search_workflows",
"用自然語言找現成的工作流(先查有沒有現成的能做這件事,找到就用,別重造)。例如:「把資料寫進 Google Sheets」、「每天抓 RSS 發通知」、「webhook 轉發到別的 API」。回傳本帳號下符合的工作流清單。",
{
query: z.string().describe("自然語言描述要找的工作流,如「把資料寫進 Google Sheets」"),
},
async ({ query }) => {
try {
if (!env.CYPHER_EXECUTOR) {
return {
content: [{ type: "text", text: "Error: CYPHER_EXECUTOR service binding is not configured." }],
isError: true,
};
}
const response = await env.CYPHER_EXECUTOR.fetch(
`http://cypher-executor/workflows/search?q=${encodeURIComponent(query)}`,
{ method: "GET", headers: { "X-Arcrun-API-Key": partnerToken } },
);
if (!response.ok) {
const errorText = await response.text();
return {
content: [{ type: "text", text: `Search failed: ${errorText}` }],
isError: true,
};
}
const result = await response.json() as {
entries?: Array<{ page_name?: string; content?: string }>;
count?: number;
mode?: string;
capability_hint?: string;
};
const entries = result.entries ?? [];
const count = result.count ?? entries.length;
if (count === 0) {
const hint = result.capability_hint ? `\n\n${result.capability_hint}` : "";
return {
content: [{
type: "text",
text: `找不到符合「${query}」的現成工作流。可以用 u6u_deploy_workflow 部署一個新的。${hint}`,
}],
};
}
// capability_hint 透傳給 AI:未開語義時 AI 看到就能主動問用戶要不要開 Vectorize(R2.3 閉環)。
const hintLine = result.capability_hint
? `\n\n⚠️ ${result.capability_hint}`
: "";
return {
content: [{
type: "text",
text: `找到 ${count} 個工作流(mode: ${result.mode ?? "keyword"}):\n${JSON.stringify(entries, null, 2)}${hintLine}`,
}],
};
} catch (error) {
return {
content: [{ type: "text", text: `Internal Error: ${error instanceof Error ? error.message : String(error)}` }],
isError: true,
};
}
}
);
}
+6
View File
@@ -9,6 +9,12 @@ export interface Env {
// 設了會把 agent-feedback / agent-telemetry block 都寫到 platform user_id 下;
// 沒設則 fallback 寫進 user 自己的 namespace
PLATFORM_API_KEY?: string;
// Self-hosted 單租戶模式旗標(與 cypher-executor 同名同義)。
// "false" = self-hostedBearer 帶的是 namespace 明碼(非平台 partner key),
// 不打 KBDB partner 驗證,直接當 org_namespace(對齊 cypher 的 opaque-key 模型)。
// 未設 / "true" = 官方 SaaS:維持 partner-key 驗證(行為完全不變)。
// SDD: sdk-and-website/mcp-account-source.mdHANDOFF §3b。
MULTI_TENANT?: string;
}
export interface ToolContext {
+38
View File
@@ -40,3 +40,41 @@ describe("partner-auth: KBDB response validation", () => {
expect(info.org_namespace).toBe("org-a");
});
});
// HANDOFF §3b / mcp-account-source.md §5.5self-hostedMULTI_TENANT=false)下
// Bearer 帶的是 namespace 明碼,不打 KBDB partner 驗證,直接當 org_namespace。
// 與 cypher-executor 的 opaque-key 模型對齊(X-Arcrun-API-Key 不驗證直接當分區 key)。
function resolveNamespace(
multiTenant: string | undefined,
token: string,
validatePartner: (t: string) => { valid: boolean; org_namespace: string },
): { ok: boolean; org_namespace?: string } {
if (multiTenant === "false") {
// self-hostedBearer 明碼即 namespace,繞 partner 驗證
return { ok: true, org_namespace: token };
}
// SaaS:維持 partner-key 驗證(行為不變)
const info = validatePartner(token);
return info.valid ? { ok: true, org_namespace: info.org_namespace } : { ok: false };
}
describe("partner-auth: self-hosted (MULTI_TENANT=false) bypasses partner validation", () => {
const partnerValidatorThatAlwaysRejects = () => ({ valid: false, org_namespace: "" });
it("self-hosted: namespace 明碼直接當 org_namespace,不打 partner 驗證", () => {
const r = resolveNamespace("false", "leo", partnerValidatorThatAlwaysRejects);
expect(r.ok).toBe(true);
expect(r.org_namespace).toBe("leo");
});
it("SaaS (未設 MULTI_TENANT):仍走 partner 驗證,明碼被擋", () => {
const r = resolveNamespace(undefined, "leo", partnerValidatorThatAlwaysRejects);
expect(r.ok).toBe(false);
});
it("SaaS:合法 partner key 通過並取 org_namespace", () => {
const r = resolveNamespace("true", "pk_live_x", () => ({ valid: true, org_namespace: "org-a" }));
expect(r.ok).toBe(true);
expect(r.org_namespace).toBe("org-a");
});
});
+26 -9
View File
@@ -4,15 +4,32 @@ compatibility_date = "2024-11-27"
compatibility_flags = [ "nodejs_compat" ]
workers_dev = true # 對齊 arcrun 部署慣例(rule 05):deploy 掃描自動啟用 workers.dev URL
# Service Bindings
# 2026-05-07COMPONENT_REGISTRY 從 inkstone-component-registry 改為 arcrun-registry
# 原因:舊的 inkstone-component-registry 期望不同 query 參數名,MCP search 失敗。
# 新的 arcrun-registryregistry.arcrun.dev)才是現役。
services = [
{ binding = "COMPONENT_REGISTRY", service = "arcrun-registry" },
{ binding = "CYPHER_EXECUTOR", service = "arcrun-cypher-executor" },
{ binding = "KBDB", service = "inkstone-kbdb-api" }
]
# ── 租戶模式(self-hosted fork 必看)─────────────────────────────────────────────
# 官方 SaaS[vars] 不含 MULTI_TENANT(預設多租戶)→ MCP 走 partner-key 驗證(pk_live)。
# self-hosted 單租戶:acr init/update 部署時 **自動注入** MULTI_TENANT="false" 進此 [vars]
# cli/src/lib/deploy.ts injectMultiTenant,依 config.mode='self-hosted')→ MCP 接受 Bearer =
# namespace 明碼,不打 KBDB partner 驗證,直接當 org_namespace(與 cypher 的 MULTI_TENANT=false 對齊)。
# 用戶零填寫(不必手動取消註解)。手動 fork 不走 CLI 者,自行在此加 MULTI_TENANT = "false"。
# SDD: sdk-and-website/mcp-account-source.md §5.5HANDOFF §3b。
[vars]
# Service Bindingsissue #12:用 [[services]] array-of-tables,不用 services=[...] inline
# ⚠️ 為何不能用 inline `services = [...]`:它在 [vars] table 之後 → TOML 會把它吸成
# `vars.services`(普通 var 陣列)而非頂層 service bindings → wrangler 看不到 binding。
# self-hosted 部署注入 MULTI_TENANT 進 [vars] 後此問題暴露(MCP 報 CYPHER_EXECUTOR not configured)。
# array-of-tables `[[services]]` 是獨立頂層 table,不受 [vars] 影響(對齊官方 cypher-executor/wrangler.toml)。
# 2026-05-07COMPONENT_REGISTRY 從 inkstone-component-registry 改為 arcrun-registry(現役)。
[[services]]
binding = "COMPONENT_REGISTRY"
service = "arcrun-registry"
[[services]]
binding = "CYPHER_EXECUTOR"
service = "arcrun-cypher-executor"
[[services]]
binding = "KBDB"
service = "arcrun-kbdb"
# Route — MCP 搬進 arcrun 主庫後改用 arcrun.dev zone(與其他 worker 一致)。
# 舊的 studio.finally.click 是 inkstone 平台 zonearcrun 帳號沒有該 zone → 部署 route 失敗。
+58 -1
View File
@@ -47,6 +47,9 @@ type Input struct {
APIKey string `json:"api_key"`
Service string `json:"service"`
Request json.RawMessage `json:"request,omitempty"`
// Namesresolve_credentials action 用——要解密的 credential 名稱清單
// (用戶在 workflow node.data 寫 {{credential.NAME}} 時,graph-executor 收集後傳入)。
Names []string `json:"names,omitempty"`
}
type SecretRequirement struct {
@@ -96,12 +99,20 @@ func main() {
writeError("api_key 必填")
return
}
// resolve_credentials:用戶面 {{credential.NAME}} 入口。不查 recipe、不要求 service
// 直接給 names 解密回明文。在 service 必填檢查之前分流(只有 authenticate 才需要 recipe)。
if input.Action == "resolve_credentials" {
handleResolveCredentials(input)
return
}
if input.Service == "" {
writeError("service 必填")
return
}
if input.Action != "" && input.Action != "authenticate" {
writeError("auth_static_key 僅支援 action=authenticate")
writeError("auth_static_key 僅支援 action=authenticate / resolve_credentials")
return
}
@@ -195,6 +206,52 @@ func main() {
os.Stdout.Write(out)
}
// handleResolveCredentials 處理用戶面 {{credential.NAME}} 入口:
// 對每個 name 讀 {api_key}:cred:{name} + 解密,回傳明文 map。
// 不查 auth recipe(與 authenticate 分流)。缺任一 name → success:false + error 指明(不假綠)。
func handleResolveCredentials(input Input) {
if len(input.Names) == 0 {
writeError("resolve_credentials 需要 names(要解密的 credential 名稱清單)")
return
}
credentials := make(map[string]string, len(input.Names))
for _, name := range input.Names {
if name == "" {
continue
}
kvKey := input.APIKey + ":cred:" + name
encJSON, s := kvGet(kvKey)
if s == 2 {
writeError("缺少 credential: " + name + "。修復: 編輯 credentials.yaml 後執行 acr creds push")
return
}
if s != 0 {
writeError("kv_get 失敗(credential " + name + ")")
return
}
var rec EncryptedRecord
if err := json.Unmarshal([]byte(encJSON), &rec); err != nil {
writeError("credential " + name + " 格式錯誤: " + err.Error())
return
}
plaintext, ok := cryptoDecrypt(rec.Encrypted, rec.IV)
if !ok {
writeError("credential " + name + " 解密失敗")
return
}
credentials[name] = plaintext
}
out, _ := json.Marshal(map[string]interface{}{
"success": true,
"credentials": credentials,
})
os.Stdout.Write(out)
}
// ── helpers ──────────────────────────────────────────────────────────────────
func writeError(msg string) {
+10 -4
View File
@@ -108,10 +108,16 @@ func main() {
responseStr := string(outBuf[:outLen])
// 2026-05-14偵測 JSON `{"error":"..."}` 模式視為 4xx 失敗
// 理由:host function 沒回 HTTP status code(架構債),先用 body 啟發式 catch。
// 標準 APIcypher-executor / KBDB / 多數 REST)失敗時都回 {"error":...} JSON。
// 對應 SDD: arcrun.md 三-A P1 #4「http_request status code 缺乏 surface」
// 偵測 JSON `{"error":"..."}` 模式視為失敗
// 2026-06-09 修架構債:host function.component-builds/http_request/src/index.ts)現在對非 2xx
// 回 envelope `{"error":"HTTP <status>","status":<code>,"body":<原文>}`——故此處 parsed["error"]
// 能正確 catch 所有 4xx/5xx(含 Notion 401 那種 body 用 {"object":"error"} 不帶 error key 的)
// 之前 host fn 只回 body 原文丟掉 status → 401 被判 success(系統假綠根因,已修)。
// 註:claude_api/kbdb_upsert_block/km_writer 已同樣修(非 2xx 回 error envelope)。
// auth_service_account 不套此 envelope——它 main.go 自己解析 OAuth token 回應的
// {access_token,error,error_description}access_token 空即視為失敗,已有自己的判定,
// 套 envelope 反而會丟掉 error_description 破壞 token exchange 錯誤處理。
// 待辦:4 份 inline host fn 最好抽成共用 helper(dedup,目前複製貼上)。
var parsed map[string]interface{}
if err := json.Unmarshal([]byte(responseStr), &parsed); err == nil {
if errVal, ok := parsed["error"]; ok && errVal != nil {
+8
View File
@@ -0,0 +1,8 @@
allowBuilds:
esbuild: true
sharp: true
workerd: true
onlyBuiltDependencies:
- esbuild
- sharp
- workerd
+5 -5
View File
@@ -49,7 +49,7 @@ if git rev-parse --git-dir >/dev/null 2>&1; then
err "$BRANCH 領先 origin/$BRANCH $AHEAD 個 commit 未 push → **deploy 前先 git push**(否則 self-hosted 裝到舊 worker,重演壓測階段 6 的 seed 404)"
GIT_BLOCK=1
else
warn "$BRANCH vs origin/$BRANCHahead=$AHEAD behind=$BEHIND(確認狀態)"
warn "${BRANCH} vs origin/${BRANCH}ahead=${AHEAD} behind=${BEHIND}(確認狀態)"
fi
else
info "非 git repo,跳過 git 同步檢查"
@@ -77,7 +77,7 @@ fi
if [ "$LOCAL_CLI" = "$REMOTE_CLI" ]; then
ok "CLI 版本同步:本機 $LOCAL_CLI = npm $REMOTE_CLI"
elif [ "$REMOTE_CLI" = "無法查線上" ] || [ "$REMOTE_CLI" = "無 npm CLI 可查" ]; then
info "本機 CLI $LOCAL_CLI$REMOTE_CLI"
info "本機 CLI ${LOCAL_CLI}${REMOTE_CLI}"
else
warn "CLI 版本漂移:本機 $LOCAL_CLI ≠ npm $REMOTE_CLI"
warn "→ 跑 scripts/local-deploy.sh(含 cli/ 變動時)會 npm publish;或手動 cd cli && npm publish"
@@ -88,7 +88,7 @@ if git rev-parse --git-dir >/dev/null 2>&1; then
LAST_CLI_PKG_CHANGE=$(git log -1 --format=%h -- cli/package.json 2>/dev/null || echo "")
LAST_CLI_SRC_CHANGE=$(git log -1 --format=%h -- cli/src 2>/dev/null || echo "")
if [ -n "$LAST_CLI_SRC_CHANGE" ] && [ "$LAST_CLI_SRC_CHANGE" != "$LAST_CLI_PKG_CHANGE" ]; then
info "cli/src 最後變動($LAST_CLI_SRC_CHANGE)晚於 package.json$LAST_CLI_PKG_CHANGE)→ 確認是否需 bump version"
info "cli/src 最後變動(${LAST_CLI_SRC_CHANGE})晚於 package.json${LAST_CLI_PKG_CHANGE})→ 確認是否需 bump version"
fi
fi
@@ -100,7 +100,7 @@ if command -v curl >/dev/null 2>&1; then
if [ "$CODE" = "200" ]; then
ok "https://cypher.arcrun.dev/health → 200"
else
warn "https://cypher.arcrun.dev/health → $CODE(部署中或不可達?)"
warn "https://cypher.arcrun.dev/health → ${CODE}(部署中或不可達?)"
fi
else
info "無 curl,跳過線上健康檢查"
@@ -111,7 +111,7 @@ echo ""
echo "【MCP serverarcrun/mcp/,已進主庫)】"
if [ -d mcp ] && [ -f mcp/wrangler.toml ]; then
MCP_NAME=$(grep '^name' mcp/wrangler.toml | head -1 | sed -E 's/^name[[:space:]]*=[[:space:]]*"([^"]*)".*/\1/')
ok "arcrun/mcp/ 存在(worker: $MCP_NAME);由 local-deploy.sh 一併 wrangler deploy"
ok "arcrun/mcp/ 存在(worker: ${MCP_NAME});由 local-deploy.sh 一併 wrangler deploy"
info "薄殼一致性:MCP 工具集應對齊 cypher-executor 最新 APIrule 07"
info "用戶連哪台 MCP 由 .mcp.json 決定(acr mcp-setup 依 config mcp_url 產)"
else
+431
View File
@@ -0,0 +1,431 @@
#!/bin/bash
# system-dev-template installer
# 已有專案接入腳本——只建立缺少的東西,已有的一律不動。
#
# 模組化安裝:
# --wiki 只裝 LLM Wiki(記憶系統 + 機敏防護)
# --sdd 只裝 SDD 系統(動 code 前必須有 design.md
# --all 兩個都裝(預設)
# 無參數 互動式詢問
#
# 為什麼留在同一個 repo 用參數選,而不是 fork:
# 使用者多半非專業,最怕「我要去哪個 repo」。一個入口 + 選單最友善。
# 等未來功能多到 3+ 個再演進成「模板組合器」。模組邊界先在這裡劃好。
set -euo pipefail
# ── i18n:依 locale 選語言,預設英文 ──────────────────
# 為什麼預設英文:curl | bash 常是 LANG=C,外國人預設就該看得懂;
# 台灣使用者 locale 多為 zh_TW,會自動切回繁中。
case "${LC_ALL:-${LC_MESSAGES:-${LANG:-}}}" in
zh*|*Hant*|*Hans*) IS_ZH="yes" ;;
*) IS_ZH="no" ;;
esac
# t "中文" "English" → 依語系印出對應字串
t() { if [ "$IS_ZH" = "yes" ]; then printf '%s\n' "$1"; else printf '%s\n' "$2"; fi; }
# tn = 不換行版(給 prompt 用)
tn() { if [ "$IS_ZH" = "yes" ]; then printf '%s' "$1"; else printf '%s' "$2"; fi; }
REPO_URL="https://raw.githubusercontent.com/uncle6me-web/system-dev-template/main/template"
CREATED=()
SKIPPED=()
# ── 解析模組參數 ──────────────────────────────────
MODULE=""
for arg in "$@"; do
case "$arg" in
--wiki|--wiki-only) MODULE="wiki" ;;
--sdd|--sdd-only) MODULE="sdd" ;;
--all) MODULE="all" ;;
-h|--help)
if [ "$IS_ZH" = "yes" ]; then
cat <<'HELP'
用法:install.sh [--wiki | --sdd | --all]
--wiki 只裝 LLM WikiCC 記憶系統 + 機敏防護)
--sdd 只裝 SDD 系統(動 code 前強制要有設計文件)
--all 兩個都裝(預設)
無參數 互動式詢問要裝哪個
HELP
else
cat <<'HELP'
Usage: install.sh [--wiki | --sdd | --all]
--wiki Install LLM Wiki only (CC memory system + secret protection)
--sdd Install SDD system only (require a design doc before touching code)
--all Install both (default)
no flag Interactively ask which to install
HELP
fi
exit 0 ;;
esac
done
echo ""
echo "🔧 system-dev-template installer"
echo "================================="
t "只建立缺少的目錄和檔案,已有的不動。" \
"Only creates missing dirs and files; never touches what already exists."
echo ""
# ── 無參數 → 互動式詢問(給非專業使用者)──────────
if [ -z "$MODULE" ]; then
if [ -t 0 ]; then
t "要安裝哪一塊?" "Which part do you want to install?"
t " 1) LLM Wiki —— 讓 CC 記住決策、不重複犯錯(含機敏防護)" \
" 1) LLM Wiki — let CC remember decisions and avoid repeating mistakes (with secret protection)"
t " 2) SDD —— 動 code 前強制先有設計文件" \
" 2) SDD — require a design doc before touching code"
t " 3) 兩個都裝(推薦)" " 3) Install both (recommended)"
echo ""
tn "請輸入 1 / 2 / 3 [預設 3]" "Enter 1 / 2 / 3 [default 3]: "
read -r choice || choice=3
case "$choice" in
1) MODULE="wiki" ;;
2) MODULE="sdd" ;;
*) MODULE="all" ;;
esac
else
# 非互動環境(如 curl | bash 無 tty)→ 預設全裝
MODULE="all"
fi
fi
WANT_WIKI=false
WANT_SDD=false
case "$MODULE" in
wiki) WANT_WIKI=true ;;
sdd) WANT_SDD=true ;;
all) WANT_WIKI=true; WANT_SDD=true ;;
esac
echo ""
t "📦 安裝模組:$MODULE" "📦 Module: $MODULE"
echo ""
# ── 偵測 vault 類型 → 決定 raw source(原始文件)路徑 ──────────
# 為什麼:這個模板原本假設「原始文件在 docs/」,但 Logseq / Obsidian
# 這種 PKM vault 有自己的目錄慣例,整理時不能照 docs/ 那套搬動,
# 否則會破壞 vault 結構、讓筆記變不可讀。
# 偵測結果寫進 CLAUDE.md,讓 CC 和未來的 Cowork skill 都知道
# 「該讀/該整理哪裡」而不是亂動。
# 必須在建立 CLAUDE.md 之前跑完。
VAULT_TYPE=""
RAW_SOURCE=""
IS_VAULT="no" # 只有 logseq/obsidian 這種「筆記軟體 vault」才算 yes
if [ -d "logseq" ]; then
VAULT_TYPE="logseq"
RAW_SOURCE="pages/, journals/"
IS_VAULT="yes"
elif [ -d ".obsidian" ]; then
VAULT_TYPE="obsidian"
RAW_SOURCE="$(tn './ (整個 vault 根目錄的 .md' './ (all .md under the vault root)')"
IS_VAULT="yes"
else
VAULT_TYPE="docs"
RAW_SOURCE="docs/"
fi
# 偵測到是筆記 vault → 出聲告訴使用者「我看到了,會小心、不破壞你的筆記結構」。
# 不是筆記(一般開發案等)→ 不囉嗦,默默把 docs/ 當原始文件夾安裝完成。
if [ "$IS_VAULT" = "yes" ]; then
t "🗂️ 偵測到 ${VAULT_TYPE} 筆記庫 → 原始文件:${RAW_SOURCE}" \
"🗂️ Detected a ${VAULT_TYPE} note vault → raw source: ${RAW_SOURCE}"
t " (會保留你筆記軟體的目錄/檔名結構,不搬動、不改名)" \
" (your note app's directory/file structure is preserved — nothing is moved or renamed)"
echo ""
fi
# 把「raw source 宣告區塊」吐出來,給新建的 CLAUDE.md append 或
# 給已存在的 CLAUDE.md 當手動補貼的提示。內容對 CC / Cowork 都是
# 機器可讀的指令(明確路徑 + 不可破壞 vault 結構的約束)。
# 寫進 CLAUDE.md 的 raw source 宣告區塊。給人也給 AI 看:
# 依 locale 只寫「一種語言」進 CLAUDE.md(雙語會讓每個 session 的 context 更滿)。
emit_raw_source_block() {
local source_kind
if [ "$IS_ZH" = "yes" ]; then
if [ "$IS_VAULT" = "yes" ]; then source_kind="${VAULT_TYPE} 筆記庫"
else source_kind="一般專案(原始文件放 raw source 路徑)"; fi
cat <<BLOCK
---
## 原始文件空間(raw source
> 安裝時偵測到的來源型態:**${source_kind}**
> CC 與 Cowork 整理/讀取「人寫的原始文件」時,**只在這裡找、只在這裡動**。
| 項目 ||
|------|----|
| 來源型態 | \`${source_kind}\` |
| raw source | \`${RAW_SOURCE}\` |
**約束(CC 與 Cowork 都必須遵守)**
- 整理 wiki/知識時,原始文件**一律從上方 raw source 路徑讀取**,不要假設是 \`docs/\`
BLOCK
if [ "$IS_VAULT" = "yes" ]; then
cat <<BLOCK
- 這是 **${VAULT_TYPE} 筆記庫**:保留它原本的目錄與檔名慣例,**不得搬動、改名、重新分類** \`.md\` 檔,
以免破壞筆記軟體結構造成筆記不可讀。整理只在 \`.claude/wiki/\` 產出,**不動 raw source 本身**。
BLOCK
fi
else
if [ "$IS_VAULT" = "yes" ]; then source_kind="${VAULT_TYPE} note vault"
else source_kind="regular project (raw source lives at the path below)"; fi
cat <<BLOCK
---
## Raw source space
> Source type detected at install time: **${source_kind}**
> When CC and Cowork curate/read human-written raw source, **look only here and act only here**.
| Item | Value |
|------|-------|
| Source type | \`${source_kind}\` |
| raw source | \`${RAW_SOURCE}\` |
**Constraints (both CC and Cowork must obey)**
- When curating the wiki/knowledge, **always read raw source from the path above** — don't assume \`docs/\`.
BLOCK
if [ "$IS_VAULT" = "yes" ]; then
cat <<BLOCK
- This is a **${VAULT_TYPE} note vault**: keep its original directory and file-naming conventions. **Do not move, rename, or re-classify** \`.md\` files,
or you'll break the note-app structure and make notes unreadable. Curation output goes only into \`.claude/wiki/\`; **never touch the raw source itself**.
BLOCK
fi
fi
}
# ── 工具函式 ──────────────────────────────────────
create_dir() {
if [ ! -d "$1" ]; then
mkdir -p "$1"
CREATED+=("$1/")
else
SKIPPED+=("$1/ $(tn '(已存在)' '(already exists)')")
fi
}
download_if_missing() {
local dest="$1" src="$2"
if [ ! -f "$dest" ]; then
mkdir -p "$(dirname "$dest")"
curl -sSL "$src" -o "$dest"
CREATED+=("$dest")
else
SKIPPED+=("$dest $(tn '(已存在,跳過)' '(already exists, skipped)')")
fi
}
# ── 共用結構(兩個模組都需要 docs 分類 + .claude)──
create_dir "docs/1-vision"
create_dir "docs/2-architecture/decisions"
create_dir "docs/4-guides"
create_dir "docs/5-records/incidents"
create_dir "docs/5-records/test-reports"
create_dir "docs/6-user"
create_dir ".claude/commands"
create_dir ".claude/hooks"
download_if_missing "docs/README.md" "$REPO_URL/docs/README.md"
# ── WIKI 模組 ─────────────────────────────────────
if $WANT_WIKI; then
create_dir ".claude/wiki"
download_if_missing ".claude/wiki/INDEX.md" "$REPO_URL/.claude/wiki/INDEX.md"
download_if_missing ".claude/wiki/TAXONOMY.md" "$REPO_URL/.claude/wiki/TAXONOMY.md"
download_if_missing ".claude/wiki/status.md" "$REPO_URL/.claude/wiki/status.md"
download_if_missing ".claude/wiki/mistakes.md" "$REPO_URL/.claude/wiki/mistakes.md"
download_if_missing ".claude/wiki/decisions-summary.md" "$REPO_URL/.claude/wiki/decisions-summary.md"
download_if_missing ".claude/wiki/.wikiignore" "$REPO_URL/.claude/wiki/.wikiignore"
download_if_missing ".claude/commands/wiki-init.md" "$REPO_URL/.claude/commands/wiki-init.md"
download_if_missing ".claude/commands/wiki-capture.md" "$REPO_URL/.claude/commands/wiki-capture.md"
download_if_missing ".claude/commands/wiki-update.md" "$REPO_URL/.claude/commands/wiki-update.md"
download_if_missing ".claude/commands/wiki-recall.md" "$REPO_URL/.claude/commands/wiki-recall.md"
# wiki 相關 hooks:接關 + 機敏掃描
download_if_missing ".claude/hooks/session-start-recall.sh" "$REPO_URL/.claude/hooks/session-start-recall.sh"
download_if_missing ".claude/hooks/wiki-secret-scan.sh" "$REPO_URL/.claude/hooks/wiki-secret-scan.sh"
# Coworkclaude.ai)整理 wiki 用的 skill:與 CC 的 /wiki-init 共用同一套規則
# (含 typed-edge、frontmatter 標籤、gloss)。沒這支 → claude.ai 來掃時身上沒規則。
download_if_missing "docs/SKILL.md" "$REPO_URL/docs/SKILL.md"
fi
# ── SDD 模組 ──────────────────────────────────────
if $WANT_SDD; then
create_dir "docs/3-specs"
download_if_missing "docs/3-specs/TEMPLATE-sdd/design.md" "$REPO_URL/docs/3-specs/TEMPLATE-sdd/design.md"
download_if_missing "docs/3-specs/TEMPLATE-sdd/tasks.md" "$REPO_URL/docs/3-specs/TEMPLATE-sdd/tasks.md"
download_if_missing "docs/2-architecture/decisions/TEMPLATE-adr.md" "$REPO_URL/docs/2-architecture/decisions/TEMPLATE-adr.md"
download_if_missing ".claude/commands/sdd-check.md" "$REPO_URL/.claude/commands/sdd-check.md"
download_if_missing ".claude/hooks/sdd-guard.sh" "$REPO_URL/.claude/hooks/sdd-guard.sh"
fi
# ── 共用 hook:專案自訂禁令骨架(預設停用)────────
download_if_missing ".claude/hooks/pre-write-guard.sh" "$REPO_URL/.claude/hooks/pre-write-guard.sh"
# ── 共用指引:GitHub issue 處理(讀/回普世,跨 repo 發要先問,禁自動輪詢)──
download_if_missing ".claude/commands/issue-handle.md" "$REPO_URL/.claude/commands/issue-handle.md"
chmod +x .claude/hooks/*.sh 2>/dev/null || true
# ── 依模組產生 settings.json 的 hooks 區塊 ────────
# settings.json 因模組而異,不能直接下載單一靜態檔,改條件組裝。
build_hooks_json() {
local session_hooks="" pretool_hooks=""
if $WANT_WIKI; then
session_hooks='{ "type": "command", "command": ".claude/hooks/session-start-recall.sh" }'
fi
# PreToolUse 依模組疊加
local pt=()
$WANT_SDD && pt+=('{ "type": "command", "command": ".claude/hooks/sdd-guard.sh" }')
pt+=('{ "type": "command", "command": ".claude/hooks/pre-write-guard.sh" }')
$WANT_WIKI && pt+=('{ "type": "command", "command": ".claude/hooks/wiki-secret-scan.sh" }')
local IFS=,
pretool_hooks="${pt[*]}"
printf '{\n "hooks": {\n'
if [ -n "$session_hooks" ]; then
printf ' "SessionStart": [\n { "matcher": "startup|resume|clear",\n "hooks": [ %s ] }\n ],\n' "$session_hooks"
fi
printf ' "PreToolUse": [\n { "matcher": "Write|Edit",\n "hooks": [ %s ] }\n ]\n' "$pretool_hooks"
printf ' }\n}\n'
}
if [ ! -f ".claude/settings.json" ]; then
build_hooks_json > .claude/settings.json
CREATED+=(".claude/settings.json $(tn "(依 $MODULE 模組產生)" "(generated for module: $MODULE)")")
else
SKIPPED+=(".claude/settings.json $(tn '(已存在,請手動合併 hooks)' '(already exists — merge hooks manually)')")
fi
# ── CLAUDE.md:只在完全不存在時建立 ────────────────
# 新建時把偵測到的 raw source 宣告 append 進去(在建立的當下寫入,
# 不回頭改使用者既有的 CLAUDE.md,維持「已有不覆蓋」原則)。
if [ ! -f "CLAUDE.md" ]; then
download_if_missing "CLAUDE.md" "$REPO_URL/CLAUDE.md"
if [ -f "CLAUDE.md" ]; then
emit_raw_source_block >> CLAUDE.md
CREATED+=("CLAUDE.md $(tn "← 已寫入 raw source 宣告(${VAULT_TYPE}" "← raw source declaration written (${VAULT_TYPE})")")
fi
else
SKIPPED+=("CLAUDE.md $(tn '(已存在,請手動加入對應區塊)' '(already exists — add the block manually)')")
fi
# ── 輸出結果 ──────────────────────────────────────
echo ""
t "✅ 建立了:" "✅ Created:"
# 注意:macOS bash 3.2 在 set -u 下展開「空陣列」會炸 unbound variable
# 所以這裡先確認有元素才展開(SKIPPED 區塊在下方本來就有守,CREATED 補上)。
if [ ${#CREATED[@]} -gt 0 ]; then
for item in "${CREATED[@]}"; do echo " + $item"; done
fi
if [ ${#SKIPPED[@]} -gt 0 ]; then
echo ""
t "⚠️ 跳過(已存在):" "⚠️ Skipped (already exists):"
for item in "${SKIPPED[@]}"; do echo " - $item"; done
fi
echo ""
echo "─────────────────────────────────"
# CLAUDE.md 已存在 → 依模組提醒手動加區塊
if [ -f "CLAUDE.md" ]; then
if ! grep -q "raw source" CLAUDE.md; then
echo ""
t "📌 CLAUDE.md 已存在但缺少 raw source 宣告。" \
"📌 CLAUDE.md exists but lacks a raw source declaration."
t " 請手動把以下區塊貼進去,讓 CC 與 Cowork 知道原始文件在哪、不要亂動既有結構:" \
" Paste the block below in so CC and Cowork know where the raw source is and won't disturb your structure:"
emit_raw_source_block | sed 's/^/ /'
fi
if $WANT_WIKI && ! grep -q "wiki/status.md" CLAUDE.md; then
echo ""
t "📌 CLAUDE.md 已存在但缺少 wiki 讀取順序,請手動加入:" \
"📌 CLAUDE.md exists but lacks the wiki reading order — please add it manually:"
echo ""
if [ "$IS_ZH" = "yes" ]; then
cat <<'SNIP'
## Wiki 讀取順序
| 檔案 | 時機 | 用途 |
|------|------|------|
| `.claude/wiki/status.md` | session 開始第一件事 | 當前進度 |
| `.claude/wiki/mistakes.md` | 做新功能前 | 已知誤解 |
| `.claude/wiki/decisions-summary.md` | 設計判斷時 | 架構決策 |
SNIP
else
cat <<'SNIP'
## Wiki reading order
| File | When | Purpose |
|------|------|---------|
| `.claude/wiki/status.md` | first thing at session start | current progress |
| `.claude/wiki/mistakes.md` | before building a new feature | known misconceptions |
| `.claude/wiki/decisions-summary.md` | when making design calls | architecture decisions |
SNIP
fi
fi
if $WANT_SDD && ! grep -q "docs/3-specs" CLAUDE.md; then
echo ""
t "📌 CLAUDE.md 已存在但缺少 SDD 鐵律,請手動加入:" \
"📌 CLAUDE.md exists but lacks the SDD iron rule — please add it manually:"
echo ""
if [ "$IS_ZH" = "yes" ]; then
cat <<'SNIP'
## 絕對鐵律
1. 任何 code 變動前必須有對應 SDDdocs/3-specs/[子系統]/design.md
找不到 → 停手問負責人,不要自行建立。
SNIP
else
cat <<'SNIP'
## Iron rule
1. Every code change must have a matching SDD (docs/3-specs/[subsystem]/design.md).
Not found → stop and ask the owner; do not create one on your own.
SNIP
fi
fi
fi
# settings.json 已存在 → 依模組提醒要合併哪些 hook
if [ -f ".claude/settings.json" ]; then
MISSING_HOOKS=()
$WANT_WIKI && ! grep -q "session-start-recall.sh" .claude/settings.json && MISSING_HOOKS+=("SessionStart: session-start-recall.sh")
$WANT_WIKI && ! grep -q "wiki-secret-scan.sh" .claude/settings.json && MISSING_HOOKS+=("PreToolUse(Write|Edit): wiki-secret-scan.sh")
$WANT_SDD && ! grep -q "sdd-guard.sh" .claude/settings.json && MISSING_HOOKS+=("PreToolUse(Write|Edit): sdd-guard.sh")
if [ ${#MISSING_HOOKS[@]} -gt 0 ]; then
echo ""
t "📌 .claude/settings.json 已存在,請手動把以下 hooks 合併進去(保留既有設定):" \
"📌 .claude/settings.json exists — merge the hooks below in manually (keep your existing settings):"
for h in "${MISSING_HOOKS[@]}"; do echo "$h"; done
fi
fi
# pre-write-guard 是空殼,提醒它預設不攔(避免「以為有保護其實沒有」的安全錯覺)
echo ""
t "️ .claude/hooks/pre-write-guard.sh 是「按需手填的空插槽」,預設不攔任何東西。" \
"️ .claude/hooks/pre-write-guard.sh is an empty slot to fill on demand — by default it blocks nothing."
t " 需要專案禁令?最簡單是叫你的 CC 寫一支貼合的 guard hook(比範本表達力強);" \
" Need project-specific bans? Easiest is to ask your CC to write a tailored guard hook (more expressive than the template);"
t " 或自己填 FORBIDDEN_PATTERNS 並到 settings.json 掛上才會生效。" \
" or fill in FORBIDDEN_PATTERNS yourself and wire it into settings.json to take effect."
echo ""
t "🚀 下一步:" "🚀 Next steps:"
if $WANT_WIKI; then
t " 在 Claude Code 對話裡執行 /wiki-init" \
" In a Claude Code conversation, run /wiki-init"
t " CC 會掃描現有文件、套用 .wikiignore、建立 wiki。" \
" CC will scan your existing docs, apply .wikiignore, and build the wiki."
fi
if $WANT_SDD; then
t " 動 code 前先在 docs/3-specs/[子系統]/ 建 design.md(可用 /sdd-check 協助)" \
" Before touching code, create design.md under docs/3-specs/[subsystem]/ (use /sdd-check to help)"
fi
t " GitHub issueCC 可直接 /issue-handle 讀回自己 repo 的 issue(禁自動輪詢)" \
" GitHub issues: CC can use /issue-handle to read issues from its own repo (no auto-polling)"
echo ""
Regular → Executable
+2 -2
View File
@@ -275,7 +275,7 @@ if [[ "${DRY_RUN:-false}" != "true" ]]; then
if [[ -f "$CHANGELOG" ]]; then tail -n +2 "$CHANGELOG"; fi
} > "$TMP_CL"
mv "$TMP_CL" "$CHANGELOG"
echo " · 已 bump → $NEW_V,並記錄進 $CHANGELOG(記得 commit 這兩個檔)"
echo " · 已 bump → ${NEW_V},並記錄進 ${CHANGELOG}(記得 commit 這兩個檔)"
fi
# 優先用 .env 的 NPM_API_TOKENauthToken)——互動 npm login 常因 publish 政策 403。
@@ -296,7 +296,7 @@ if [[ "${DRY_RUN:-false}" != "true" ]]; then
echo " 📦 publish arcrun $REMOTE_V$LOCAL_V ..."
if (cd cli && npm run build >/dev/null 2>&1 && \
NPM_CONFIG_USERCONFIG="${PUB_RC:-$HOME/.npmrc}" npm publish --access public 2>&1 | tail -3); then
echo " ✅ npm publish 完成(arcrun@$LOCAL_V"
echo " ✅ npm publish 完成(arcrun@${LOCAL_V}"
else
echo " ❌ npm publish 失敗"
FAILED+=("cli:npm-publish")
+91 -88
View File
@@ -4,27 +4,31 @@
對應 LI SDD M3.4examples / skills git source of truth
KBDB AI 搜尋 / get query-friendly mirror
KBDB block
- examples type=workflow-example
2026-06-14 重寫KBDB 降基本盤後三表 entries/templates/records v3 blocks
kbdb-upsert-block 零件 worker原打 https://kbdb-upsert-block.arcrun.dev/ 全失效
改打基本盤 KBDB Worker /entries
- examples entry_type=workflow-example
content = workflow.yaml 全文
metadata_json = { description, tags }
tags_json = [...tags.json]
page_name = example-{slug} (idempotency key重複 sync upsert)
- skills type=agent-skill
metadata_json = { slug, description_md, tags }
tags_json = ["workflow-example", "example:{slug}", *tags]
page_name = example-{slug} (idempotency key)
- skills entry_type=agent-skill
content = {slug}.md 全文
page_name = skill-{slug} (idempotency key)
metadata_json = { slug, title }
tags_json = ["agent-skill", "skill:{slug}"]
page_name = skill-{slug} (idempotency key)
基本盤無 upsert 端點 本腳本自己做 idempotencyGET ?page_name= 找到則 PATCH /entries/:id
否則 POST /entries這是 ops 同步腳本 CLI/MCP 薄殼自行編排不違反 rule 07 薄殼原則
執行
cd matrix/arcrun
python3 scripts/sync-registry-to-kbdb.py # 上傳所有
KBDB_BASE_URL=https://arcrun-kbdb.<subdomain>.workers.dev python3 scripts/sync-registry-to-kbdb.py
python3 scripts/sync-registry-to-kbdb.py --dry-run # 只 list 不寫
需求
- mira tools/_kbdb_client.py 風格 (urllib + ak_)
- ARCRUN_API_KEY .env env var
- kbdb-*.arcrun.dev 零件 worker endpoints (符合 mira CLAUDE.md §1.7)
設定
- KBDB_BASE_URL KBDB 基本盤 Worker base URL必填無預設避免誤打到別的環境
- KBDB_OWNER_ID 資料歸屬標記選填預設 'registry'基本盤多租戶用 owner_id
"""
import argparse
@@ -39,42 +43,30 @@ ARCRUN_ROOT = Path(__file__).resolve().parent.parent
EXAMPLES_DIR = ARCRUN_ROOT / "registry" / "examples"
SKILLS_DIR = ARCRUN_ROOT / "registry" / "skills"
KBDB_UPSERT_URL = "https://kbdb-upsert-block.arcrun.dev/"
USER_AGENT = "arcrun-registry-sync/1.0"
USER_ID = "inkstone_platform_registry" # 需符合 KBDB partner namespace prefixinkstone_*
USER_AGENT = "arcrun-registry-sync/2.0"
OWNER_ID = os.environ.get("KBDB_OWNER_ID", "registry")
SOURCE = "registry-git-sync"
def get_api_key() -> str:
"""從 env var 或 polaris/mira/.env 取 ARCRUN_API_KEY"""
key = os.environ.get("ARCRUN_API_KEY", "")
if key:
return key
# fallback:找 polaris/mira/.envleo 既有約定位置)
mira_env = ARCRUN_ROOT.parent.parent / "polaris" / "mira" / ".env"
if mira_env.exists():
for line in mira_env.read_text(encoding="utf-8").splitlines():
line = line.strip()
if line.startswith("ARCRUN_API_KEY="):
return line.split("=", 1)[1].strip()
def get_base_url() -> str:
"""KBDB 基本盤 Worker base URL。無預設(避免誤打環境)"""
url = os.environ.get("KBDB_BASE_URL", "").rstrip("/")
if url:
return url
raise SystemExit(
"ARCRUN_API_KEY 未設定。export ARCRUN_API_KEY=ak_... 或加到 polaris/mira/.env"
"KBDB_BASE_URL 未設定。\n"
" export KBDB_BASE_URL=https://arcrun-kbdb.<subdomain>.workers.dev\n"
" self-hosted 用自己部署的 KBDB Worker URL"
)
def kbdb_upsert(api_key: str, payload: dict, dry_run: bool) -> dict:
"""POST kbdb-upsert-block.arcrun.dev — page_name 當 idempotency key"""
if dry_run:
return {"dry_run": True, "would_upsert": payload.get("page_name")}
data = json.dumps(payload, ensure_ascii=False).encode("utf-8")
def _req(method: str, url: str, payload: dict | None = None) -> dict:
data = json.dumps(payload, ensure_ascii=False).encode("utf-8") if payload is not None else None
req = urllib.request.Request(
KBDB_UPSERT_URL,
url,
data=data,
headers={
"Content-Type": "application/json",
"User-Agent": USER_AGENT,
},
method="POST",
headers={"Content-Type": "application/json", "User-Agent": USER_AGENT},
method=method,
)
try:
with urllib.request.urlopen(req, timeout=30) as resp:
@@ -82,10 +74,43 @@ def kbdb_upsert(api_key: str, payload: dict, dry_run: bool) -> dict:
except urllib.error.HTTPError as e:
body = e.read().decode("utf-8", errors="replace")
return {"error": f"HTTP {e.code}: {body[:200]}"}
except urllib.error.URLError as e:
return {"error": f"URL error: {e}"}
def sync_examples(api_key: str, dry_run: bool) -> tuple[int, int]:
"""同步 registry/examples/{slug}/ 進 KBDB"""
def find_entry_id_by_page_name(base_url: str, page_name: str) -> str | None:
"""GET /entries?page_name= → 回既有 entry ididempotency 用),無則 None。"""
from urllib.parse import quote
res = _req("GET", f"{base_url}/entries?page_name={quote(page_name)}&limit=1")
if "error" in res:
return None
entries = res.get("entries") or []
return entries[0].get("id") if entries else None
def upsert_entry(base_url: str, payload: dict, dry_run: bool) -> dict:
"""page_name 當 idempotency key:找到則 PATCH /entries/:id,否則 POST /entries。"""
page_name = payload.get("page_name")
if dry_run:
existing = None if base_url == "DRY" else find_entry_id_by_page_name(base_url, page_name)
return {"dry_run": True, "would": "patch" if existing else "post", "page_name": page_name}
existing_id = find_entry_id_by_page_name(base_url, page_name)
if existing_id:
# PATCH 只送可變欄位(entry_type/page_name 不變)
patch = {k: payload[k] for k in ("content", "tags_json", "metadata_json") if k in payload}
res = _req("PATCH", f"{base_url}/entries/{existing_id}", patch)
if "error" not in res:
res.setdefault("action", "update")
return res
res = _req("POST", f"{base_url}/entries", payload)
if "error" not in res:
res.setdefault("action", "create")
return res
def sync_examples(base_url: str, dry_run: bool) -> tuple[int, int]:
"""同步 registry/examples/{slug}/ 進 KBDBentry_type=workflow-example"""
if not EXAMPLES_DIR.exists():
print(f"⚠️ {EXAMPLES_DIR} 不存在,跳過 examples 同步")
return 0, 0
@@ -104,50 +129,36 @@ def sync_examples(api_key: str, dry_run: bool) -> tuple[int, int]:
continue
yaml_content = workflow_yaml.read_text(encoding="utf-8")
description = (
description_md.read_text(encoding="utf-8") if description_md.exists() else ""
)
tags = (
json.loads(tags_json.read_text(encoding="utf-8")) if tags_json.exists() else []
)
description = description_md.read_text(encoding="utf-8") if description_md.exists() else ""
tags = json.loads(tags_json.read_text(encoding="utf-8")) if tags_json.exists() else []
# content = workflow YAML(讓 AI semantic search 命中 YAML 內容)
# metadata_json = description + tags 結構化
payload = {
"api_key": api_key,
"type": "workflow-example",
"entry_type": "workflow-example",
"page_name": f"example-{slug}",
"source": SOURCE,
"user_id": USER_ID,
"owner_id": OWNER_ID,
"content": yaml_content,
"metadata_json": json.dumps(
{
"slug": slug,
"description_md": description,
"tags": tags,
},
{"slug": slug, "description_md": description, "tags": tags, "source": SOURCE},
ensure_ascii=False,
),
"tags_json": json.dumps(
["workflow-example", f"example:{slug}", *tags],
ensure_ascii=False,
["workflow-example", f"example:{slug}", *tags], ensure_ascii=False
),
}
result = kbdb_upsert(api_key, payload, dry_run)
result = upsert_entry(base_url, payload, dry_run)
if "error" in result:
print(f"{slug}: {result['error']}")
fail += 1
else:
action = result.get("data", {}).get("action", "?") if isinstance(result.get("data"), dict) else "?"
print(f"{slug}{action}")
print(f"{slug}{result.get('action', 'dry-run:' + result.get('would', '?'))}")
ok += 1
return ok, fail
def sync_skills(api_key: str, dry_run: bool) -> tuple[int, int]:
"""同步 registry/skills/*.md 進 KBDB"""
def sync_skills(base_url: str, dry_run: bool) -> tuple[int, int]:
"""同步 registry/skills/*.md 進 KBDBentry_type=agent-skill"""
if not SKILLS_DIR.exists():
print(f"⚠️ {SKILLS_DIR} 不存在,跳過 skills 同步")
return 0, 0
@@ -159,7 +170,6 @@ def sync_skills(api_key: str, dry_run: bool) -> tuple[int, int]:
slug = md_file.stem
content = md_file.read_text(encoding="utf-8")
# 簡單抓首行 # X 當 title
title = slug
for line in content.splitlines():
line = line.strip()
@@ -168,44 +178,37 @@ def sync_skills(api_key: str, dry_run: bool) -> tuple[int, int]:
break
payload = {
"api_key": api_key,
"type": "agent-skill",
"entry_type": "agent-skill",
"page_name": f"skill-{slug}",
"source": SOURCE,
"user_id": USER_ID,
"owner_id": OWNER_ID,
"content": content,
"metadata_json": json.dumps(
{"slug": slug, "title": title},
ensure_ascii=False,
),
"tags_json": json.dumps(
["agent-skill", f"skill:{slug}"],
ensure_ascii=False,
{"slug": slug, "title": title, "source": SOURCE}, ensure_ascii=False
),
"tags_json": json.dumps(["agent-skill", f"skill:{slug}"], ensure_ascii=False),
}
result = kbdb_upsert(api_key, payload, dry_run)
result = upsert_entry(base_url, payload, dry_run)
if "error" in result:
print(f"{slug}: {result['error']}")
fail += 1
else:
action = result.get("data", {}).get("action", "?") if isinstance(result.get("data"), dict) else "?"
print(f"{slug}{action}")
print(f"{slug}{result.get('action', 'dry-run:' + result.get('would', '?'))}")
ok += 1
return ok, fail
def main():
p = argparse.ArgumentParser(description="Sync registry/examples + skills → KBDB")
p = argparse.ArgumentParser(description="Sync registry/examples + skills → KBDB base (/entries)")
p.add_argument("--dry-run", action="store_true", help="只 list 不寫")
p.add_argument("--examples-only", action="store_true")
p.add_argument("--skills-only", action="store_true")
args = p.parse_args()
api_key = get_api_key()
print(f"🔑 api_key: {api_key[:12]}... (len={len(api_key)})")
print(f"📂 root: {ARCRUN_ROOT}")
base_url = "DRY" if args.dry_run and not os.environ.get("KBDB_BASE_URL") else get_base_url()
print(f"🌐 KBDB base: {base_url}")
print(f"📂 root: {ARCRUN_ROOT} (owner_id={OWNER_ID})")
if args.dry_run:
print("(dry-run,不實際寫 KBDB)")
print()
@@ -214,13 +217,13 @@ def main():
skills_ok = skills_fail = 0
if not args.skills_only:
print("📋 Syncing examples → type=workflow-example ...")
examples_ok, examples_fail = sync_examples(api_key, args.dry_run)
print("📋 Syncing examples → entry_type=workflow-example ...")
examples_ok, examples_fail = sync_examples(base_url, args.dry_run)
print(f" examples: {examples_ok} ok / {examples_fail} fail\n")
if not args.examples_only:
print("📋 Syncing skills → type=agent-skill ...")
skills_ok, skills_fail = sync_skills(api_key, args.dry_run)
print("📋 Syncing skills → entry_type=agent-skill ...")
skills_ok, skills_fail = sync_skills(base_url, args.dry_run)
print(f" skills: {skills_ok} ok / {skills_fail} fail\n")
total_fail = examples_fail + skills_fail
+97
View File
@@ -0,0 +1,97 @@
#!/bin/bash
# thin-shell-smoke.sh — 薄殼防複發機制層 2thin-shell-alignment SDD / issue #11 R4
#
# 目的:讓「薄殼打了不存在的 server 端點」(死端點假綠)當場現形。
# 對 CLI/MCP 各能力對應的 cypher-executor 端點打一次,斷言 **非 404**。
# 404 = route 不存在 = 死端點 → 紅燈。其他狀態(401/400/200…)= 端點存在 → 綠。
#
# ⚠️ flag 紅線(SDD C2/R4.2):本機/手動跑,**非 CI、非 cron、非輪詢**。
# 宣稱「CLI/MCP 對齊/完成」前手動跑一次即可。對齊「執行鏈路不依賴 CI」鐵律。
#
# 用法:
# CYPHER_URL=https://cypher.arcrun.dev ./scripts/thin-shell-smoke.sh
# self-hostedCYPHER_URL=https://<你的 cypher>.workers.dev ...
# 不帶 API key 也能驗端點存在性(401 仍算「端點活著」)。帶 key 可更深驗:
# ARCRUN_API_KEY=xxx CYPHER_URL=... ./scripts/thin-shell-smoke.sh
set -o pipefail
CYPHER_URL="${CYPHER_URL:-https://cypher.arcrun.dev}"
CYPHER_URL="${CYPHER_URL%/}"
API_KEY="${ARCRUN_API_KEY:-}"
PASS=0
FAIL=0
declare -a DEAD=()
# probe METHOD PATH LABEL
# 斷言端點非 404。404 → 死端點(紅)。連線失敗 → 紅(但標明是網路非死端點)。
probe() {
method="$1"; path="$2"; label="$3"
url="${CYPHER_URL}${path}"
if [ "$method" = "POST" ]; then
if [ -n "$API_KEY" ]; then
code=$(curl -s -o /dev/null -w '%{http_code}' --max-time 15 -X POST -H "X-Arcrun-API-Key: ${API_KEY}" -H 'Content-Type: application/json' -d '{}' "$url" 2>/dev/null)
else
code=$(curl -s -o /dev/null -w '%{http_code}' --max-time 15 -X POST -H 'Content-Type: application/json' -d '{}' "$url" 2>/dev/null)
fi
else
if [ -n "$API_KEY" ]; then
code=$(curl -s -o /dev/null -w '%{http_code}' --max-time 15 -H "X-Arcrun-API-Key: ${API_KEY}" "$url" 2>/dev/null)
else
code=$(curl -s -o /dev/null -w '%{http_code}' --max-time 15 "$url" 2>/dev/null)
fi
fi
code="${code:-000}"
if [ "$code" = "000" ]; then
echo " ⚠️ $label — 連線失敗(網路/DNS,非死端點判定):$method $path"
FAIL=$((FAIL+1))
elif [ "$code" = "404" ]; then
echo "$label — 死端點 404$method $path"
DEAD+=("$label: $method $path")
FAIL=$((FAIL+1))
else
echo "$label — 端點存在(HTTP ${code}):$method $path"
PASS=$((PASS+1))
fi
}
echo "=== thin-shell smoke:對 ${CYPHER_URL} 驗端點存在性(斷言非 404==="
echo ""
echo "── 薄殼核心能力(CLI/MCP 共用端點)──"
# 部署:CLI acr push / MCP u6u_deploy_workflow(方向①收斂後同此)
probe POST /webhooks/named "deploy(push)"
# 執行已部署:CLI acr run <name> / MCP u6u_execute_workflow#11 P0 修的)
probe POST /webhooks/named/__smoke__/trigger "run(trigger)"
# 執行本機 YAML/cypher/execute
probe POST /cypher/execute "execute"
# listCLI acr list / MCP u6u_list_workflows#11 P1 收斂的)
probe GET /webhooks/named "list"
# search workflowMCP u6u_search_workflows#8 新增)
probe GET "/workflows/search?q=smoke" "search_workflow"
# 驗證:MCP arcrun_validate_yaml
probe POST /validate "validate"
# backfill search entries#8
probe POST /workflows/backfill-search-entries "backfill"
echo ""
echo "── 其他薄殼端點 ──"
probe GET /me "whoami"
probe POST /credentials "creds_push"
probe POST /recipes "recipe"
probe GET /kbdb/templates "kbdb_templates"
probe POST /cypher/search "validate_local_graph(cypher/search)"
echo ""
echo "=== 結果:${PASS} 端點存在 / ${FAIL} 異常 ==="
if [ ${#DEAD[@]} -gt 0 ]; then
echo ""
echo "🔴 死端點(route 不存在,薄殼打了會 404):"
for d in "${DEAD[@]}"; do echo " - $d"; done
echo ""
echo "→ 修法:對照 cli-mcp-capability-matrix.md,把薄殼改打存在的 route,或補 server route。"
exit 1
fi
echo "✅ 無死端點。"
+234
View File
@@ -0,0 +1,234 @@
#!/bin/bash
# system-dev-template updater
# 已安裝舊版的人,一鍵更新到新版。
#
# 核心安全原則:只覆蓋「模板/邏輯檔」,絕不碰「使用者資料檔」。
# ✅ 可覆蓋:hooks/*.sh、commands/*.md、TEMPLATE-*、wiki/INDEX.md
# ——這些由模板維護,使用者不會手改,新版直接換掉。
# 🔒 絕不碰:wiki/status.md、mistakes.md、decisions-summary.md、TAXONOMY.md、.wikiignore、
# settings.json、CLAUDE.md
# ——這些是使用者自己填的內容,覆蓋=清空他的記憶與設定。
#
# 「第一次更新」的雞生蛋問題:
# 舊版本機沒有 update.sh。所以第一次靠 README 那行 curl 從遠端抓這支腳本來跑。
# 跑完它會把自己也更新進 scripts/update.sh,之後就能直接跑本機的 `bash scripts/update.sh`。
set -euo pipefail
# ── i18n:依 locale 選語言,預設英文(curl | bash 常為 LANG=C)──
case "${LC_ALL:-${LC_MESSAGES:-${LANG:-}}}" in
zh*|*Hant*|*Hans*) IS_ZH="yes" ;;
*) IS_ZH="no" ;;
esac
t() { if [ "$IS_ZH" = "yes" ]; then printf '%s\n' "$1"; else printf '%s\n' "$2"; fi; }
tn() { if [ "$IS_ZH" = "yes" ]; then printf '%s' "$1"; else printf '%s' "$2"; fi; }
REPO_RAW="https://raw.githubusercontent.com/uncle6me-web/system-dev-template/main"
TEMPLATE_URL="$REPO_RAW/template"
UPDATED=()
KEPT=()
NEW=()
TEMPLATED=()
# ── 版本比對:先看本機 vs 遠端,給使用者「值不值得更新」的判斷 ──
LOCAL_VER="$(tn '(未知)' '(unknown)')"
[ -f ".claude/VERSION" ] && LOCAL_VER="$(tr -d '[:space:]' < .claude/VERSION)"
REMOTE_VER="$(curl -sSL "$TEMPLATE_URL/.claude/VERSION" 2>/dev/null | tr -d '[:space:]' || echo '')"
echo ""
echo "🔄 system-dev-template updater"
echo "================================="
t " 本機版本:${LOCAL_VER}" " Local version: ${LOCAL_VER}"
t " 最新版本:${REMOTE_VER:-取不到(檢查網路)}" \
" Latest version: ${REMOTE_VER:-unavailable (check network)}"
echo ""
if [ -z "$REMOTE_VER" ]; then
t "❌ 取不到遠端版本,可能是網路問題。請稍後再試。" \
"❌ Could not fetch the remote version (likely a network issue). Please try again later."
exit 1
fi
if [ "$LOCAL_VER" = "$REMOTE_VER" ]; then
t "✅ 已是最新版(${LOCAL_VER}),不需更新。" \
"✅ Already up to date (${LOCAL_VER}), nothing to update."
t " (仍會同步模板邏輯檔,確保 hooks/commands 與最新一致。)" \
" (Template logic files will still be synced to keep hooks/commands in line with the latest.)"
echo ""
fi
# ── 工具函式 ───────────────────────────────────────
# 覆蓋更新:模板/邏輯檔,無條件抓最新版蓋掉。
update_file() {
local dest="$1" src="$2"
mkdir -p "$(dirname "$dest")"
if [ -f "$dest" ]; then
if curl -sSL "$src" -o "$dest.tmp" 2>/dev/null && [ -s "$dest.tmp" ]; then
if cmp -s "$dest" "$dest.tmp"; then
rm -f "$dest.tmp" # 內容相同,不算更新
else
mv "$dest.tmp" "$dest"
UPDATED+=("$dest")
fi
else
rm -f "$dest.tmp"
t " ⚠️ 抓取失敗,保留原檔:$dest" " ⚠️ Download failed, keeping the original: $dest"
fi
else
if curl -sSL "$src" -o "$dest" 2>/dev/null && [ -s "$dest" ]; then
NEW+=("$dest") # 新功能:舊版沒有的檔
else
rm -f "$dest"
t " ⚠️ 抓取失敗:$dest" " ⚠️ Download failed: $dest"
fi
fi
}
# 保留:使用者資料檔,只記錄「有保留」,永遠不動。
keep_file() {
[ -f "$1" ] && KEPT+=("$1") || true
}
# 客製檔:使用者一定會手填內容(如 pre-write-guard.sh)。
# - 已存在 → 絕不覆蓋,但把最新模板版抓到 <檔名>.template.sh 旁邊,供使用者自行 diff 採納。
# - 不存在 → 視同新檔,直接抓本體(第一次安裝才會走這條)。
keep_with_template() {
local dest="$1" src="$2"
if [ -f "$dest" ]; then
KEPT+=("$dest")
local tmpl="${dest%.sh}.template.sh"
if curl -sSL "$src" -o "$tmpl.tmp" 2>/dev/null && [ -s "$tmpl.tmp" ]; then
if [ -f "$tmpl" ] && cmp -s "$tmpl" "$tmpl.tmp"; then
rm -f "$tmpl.tmp" # 模板版沒變,不重複提示
else
mv "$tmpl.tmp" "$tmpl"
TEMPLATED+=("$tmpl")
fi
else
rm -f "$tmpl.tmp"
fi
else
update_file "$dest" "$src" # 還沒裝過 → 當新檔處理
fi
}
# ── 偵測已安裝哪些模組(依現有檔案判斷,更新只動已裝的)──
HAS_WIKI=false
HAS_SDD=false
[ -d ".claude/wiki" ] && HAS_WIKI=true
if [ -f ".claude/hooks/sdd-guard.sh" ] || [ -d "docs/3-specs/TEMPLATE-sdd" ]; then HAS_SDD=true; fi
t "📦 偵測到已安裝模組:" "📦 Detected installed modules:"
$HAS_WIKI && echo " • LLM Wiki"
$HAS_SDD && echo " • SDD"
{ $HAS_WIKI || $HAS_SDD; } || \
t " (未偵測到任何模組——這裡可能還沒安裝,請改跑 install.sh" \
" (No modules detected — nothing installed here yet; run install.sh instead.)"
echo ""
# ── 客製檔:使用者手填的 guardrail,永不覆蓋(issue #3)──
# pre-write-guard.sh 的定位是「空白客製模板,使用者沒配置前不提供保護」(CHANGELOG 1.2.0)。
# 下游通常已塞滿自己的 enforcement,直接覆蓋=無聲關掉整套 guardrail。
# 改為:保留原檔不動,新版範本另存 pre-write-guard.template.sh,由使用者自行 diff 採納。
keep_with_template ".claude/hooks/pre-write-guard.sh" "$TEMPLATE_URL/.claude/hooks/pre-write-guard.sh"
# ── 模板/邏輯檔:覆蓋更新 ──────────────────────────
# 共用 hook 與指引
update_file ".claude/commands/issue-handle.md" "$TEMPLATE_URL/.claude/commands/issue-handle.md"
update_file ".claude/VERSION" "$TEMPLATE_URL/.claude/VERSION"
if $HAS_WIKI; then
# wiki 的「邏輯檔」:導航與 hooks,可覆蓋
update_file ".claude/wiki/INDEX.md" "$TEMPLATE_URL/.claude/wiki/INDEX.md"
update_file ".claude/hooks/session-start-recall.sh" "$TEMPLATE_URL/.claude/hooks/session-start-recall.sh"
update_file ".claude/hooks/wiki-secret-scan.sh" "$TEMPLATE_URL/.claude/hooks/wiki-secret-scan.sh"
update_file ".claude/commands/wiki-init.md" "$TEMPLATE_URL/.claude/commands/wiki-init.md"
update_file ".claude/commands/wiki-capture.md" "$TEMPLATE_URL/.claude/commands/wiki-capture.md"
update_file ".claude/commands/wiki-update.md" "$TEMPLATE_URL/.claude/commands/wiki-update.md"
update_file ".claude/commands/wiki-recall.md" "$TEMPLATE_URL/.claude/commands/wiki-recall.md"
# Coworkclaude.ai)的 wiki 整理 skill:規則檔,可覆蓋
update_file "docs/SKILL.md" "$TEMPLATE_URL/docs/SKILL.md"
# wiki 的「使用者資料」:絕不碰
keep_file ".claude/wiki/status.md"
keep_file ".claude/wiki/mistakes.md"
keep_file ".claude/wiki/decisions-summary.md"
keep_file ".claude/wiki/TAXONOMY.md"
keep_file ".claude/wiki/.wikiignore"
fi
if $HAS_SDD; then
# SDD 範本與 hook:可覆蓋
update_file "docs/3-specs/TEMPLATE-sdd/design.md" "$TEMPLATE_URL/docs/3-specs/TEMPLATE-sdd/design.md"
update_file "docs/3-specs/TEMPLATE-sdd/tasks.md" "$TEMPLATE_URL/docs/3-specs/TEMPLATE-sdd/tasks.md"
update_file "docs/2-architecture/decisions/TEMPLATE-adr.md" "$TEMPLATE_URL/docs/2-architecture/decisions/TEMPLATE-adr.md"
update_file ".claude/commands/sdd-check.md" "$TEMPLATE_URL/.claude/commands/sdd-check.md"
update_file ".claude/hooks/sdd-guard.sh" "$TEMPLATE_URL/.claude/hooks/sdd-guard.sh"
fi
# ── 自我更新:把最新的 update.sh 也抓下來(含 install.sh)──
# 這兩支在 main/scripts/ 下,不在 template/。
update_file "scripts/update.sh" "$REPO_RAW/scripts/update.sh"
update_file "scripts/install.sh" "$REPO_RAW/scripts/install.sh"
chmod +x .claude/hooks/*.sh scripts/*.sh 2>/dev/null || true
# ── 使用者資料檔:絕不碰,但提醒「設定可能有新欄位要手動補」──
keep_file ".claude/settings.json"
keep_file "CLAUDE.md"
# ── 結果輸出 ───────────────────────────────────────
echo ""
echo "─────────────────────────────────"
if [ ${#NEW[@]} -gt 0 ]; then
echo ""
t "🆕 新功能(舊版沒有,已加入):" "🆕 New features (absent in the old version, now added):"
for f in "${NEW[@]}"; do echo " + $f"; done
fi
if [ ${#UPDATED[@]} -gt 0 ]; then
echo ""
t "⬆️ 已更新(覆蓋成新版):" "⬆️ Updated (overwritten with the new version):"
for f in "${UPDATED[@]}"; do echo " ~ $f"; done
fi
if [ ${#NEW[@]} -eq 0 ] && [ ${#UPDATED[@]} -eq 0 ]; then
echo ""
t "✨ 模板邏輯檔已全部最新,無需變動。" \
"✨ All template logic files are already up to date — no changes needed."
fi
if [ ${#KEPT[@]} -gt 0 ]; then
echo ""
t "🔒 完整保留(你的內容/設定,從未碰過):" \
"🔒 Fully preserved (your content/settings, never touched):"
for f in "${KEPT[@]}"; do echo " = $f"; done
fi
if [ ${#TEMPLATED[@]} -gt 0 ]; then
echo ""
t "📋 客製檔有新版範本(你的原檔沒動,新版另存旁邊,請自行 diff 採納):" \
"📋 Custom files have a new template version (your original is untouched; the new one is saved alongside — diff and adopt as you like):"
for f in "${TEMPLATED[@]}"; do
echo "$f"
t " 比對:diff \"${f%.template.sh}.sh\" \"$f\"" \
" compare: diff \"${f%.template.sh}.sh\" \"$f\""
done
fi
# ── settings.json 提醒:新模組 hook 可能要手動補 ──
if [ -f ".claude/settings.json" ]; then
MISSING=()
$HAS_WIKI && ! grep -q "session-start-recall.sh" .claude/settings.json && MISSING+=("SessionStart: session-start-recall.sh")
$HAS_WIKI && ! grep -q "wiki-secret-scan.sh" .claude/settings.json && MISSING+=("PreToolUse(Write|Edit): wiki-secret-scan.sh")
$HAS_SDD && ! grep -q "sdd-guard.sh" .claude/settings.json && MISSING+=("PreToolUse(Write|Edit): sdd-guard.sh")
if [ ${#MISSING[@]} -gt 0 ]; then
echo ""
t "📌 settings.json 是你的設定(沒動),但偵測到缺以下 hook,請手動補上:" \
"📌 settings.json is yours (untouched), but these hooks are missing — please add them manually:"
for h in "${MISSING[@]}"; do echo "$h"; done
fi
fi
echo ""
t "🚀 更新完成:${LOCAL_VER}${REMOTE_VER}" "🚀 Update complete: ${LOCAL_VER}${REMOTE_VER}"
t " 下次更新直接跑:bash scripts/update.sh" " Next time, just run: bash scripts/update.sh"
t " 改了什麼看:CHANGELOG.md" " See what changed: CHANGELOG.md"
echo ""
+1
View File
@@ -0,0 +1 @@
1.12.0
+472
View File
@@ -0,0 +1,472 @@
#!/bin/bash
# system-dev-template installer
# 已有專案接入腳本——只建立缺少的東西,已有的一律不動。
#
# 模組化安裝:
# --wiki 只裝 LLM Wiki(記憶系統 + 機敏防護)
# --sdd 只裝 SDD 系統(動 code 前必須有 design.md
# --all 兩個都裝(預設)
# 無參數 互動式詢問
#
# 為什麼留在同一個 repo 用參數選,而不是 fork:
# 使用者多半非專業,最怕「我要去哪個 repo」。一個入口 + 選單最友善。
# 等未來功能多到 3+ 個再演進成「模板組合器」。模組邊界先在這裡劃好。
set -euo pipefail
# ── i18n:依 locale 選語言,預設英文 ──────────────────
# 為什麼預設英文:curl | bash 常是 LANG=C,外國人預設就該看得懂;
# 台灣使用者 locale 多為 zh_TW,會自動切回繁中。
case "${LC_ALL:-${LC_MESSAGES:-${LANG:-}}}" in
zh*|*Hant*|*Hans*) IS_ZH="yes" ;;
*) IS_ZH="no" ;;
esac
# t "中文" "English" → 依語系印出對應字串
t() { if [ "$IS_ZH" = "yes" ]; then printf '%s\n' "$1"; else printf '%s\n' "$2"; fi; }
# tn = 不換行版(給 prompt 用)
tn() { if [ "$IS_ZH" = "yes" ]; then printf '%s' "$1"; else printf '%s' "$2"; fi; }
REPO_URL="https://raw.githubusercontent.com/uncle6me-web/system-dev-template/main/template"
# install.sh / update.sh 住在 main/scripts/(不在 template/)。
SCRIPTS_URL="https://raw.githubusercontent.com/uncle6me-web/system-dev-template/main/scripts"
CREATED=()
SKIPPED=()
# ── 解析模組參數 ──────────────────────────────────
MODULE=""
for arg in "$@"; do
case "$arg" in
--wiki|--wiki-only) MODULE="wiki" ;;
--sdd|--sdd-only) MODULE="sdd" ;;
--all) MODULE="all" ;;
-h|--help)
if [ "$IS_ZH" = "yes" ]; then
cat <<'HELP'
用法:install.sh [--wiki | --sdd | --all]
--wiki 只裝 LLM WikiCC 記憶系統 + 機敏防護)
--sdd 只裝 SDD 系統(動 code 前強制要有設計文件)
--all 兩個都裝(預設)
無參數 互動式詢問要裝哪個
HELP
else
cat <<'HELP'
Usage: install.sh [--wiki | --sdd | --all]
--wiki Install LLM Wiki only (CC memory system + secret protection)
--sdd Install SDD system only (require a design doc before touching code)
--all Install both (default)
no flag Interactively ask which to install
HELP
fi
exit 0 ;;
esac
done
echo ""
echo "🔧 system-dev-template installer"
echo "================================="
t "只建立缺少的目錄和檔案,已有的不動。" \
"Only creates missing dirs and files; never touches what already exists."
echo ""
# ── 無參數 → 互動式詢問(給非專業使用者)──────────
if [ -z "$MODULE" ]; then
if [ -t 0 ]; then
t "要安裝哪一塊?" "Which part do you want to install?"
t " 1) LLM Wiki —— 讓 CC 記住決策、不重複犯錯(含機敏防護)" \
" 1) LLM Wiki — let CC remember decisions and avoid repeating mistakes (with secret protection)"
t " 2) SDD —— 動 code 前強制先有設計文件" \
" 2) SDD — require a design doc before touching code"
t " 3) 兩個都裝(推薦)" " 3) Install both (recommended)"
echo ""
tn "請輸入 1 / 2 / 3 [預設 3]" "Enter 1 / 2 / 3 [default 3]: "
read -r choice || choice=3
case "$choice" in
1) MODULE="wiki" ;;
2) MODULE="sdd" ;;
*) MODULE="all" ;;
esac
else
# 非互動環境(如 curl | bash 無 tty)→ 預設全裝
MODULE="all"
fi
fi
WANT_WIKI=false
WANT_SDD=false
case "$MODULE" in
wiki) WANT_WIKI=true ;;
sdd) WANT_SDD=true ;;
all) WANT_WIKI=true; WANT_SDD=true ;;
esac
echo ""
t "📦 安裝模組:$MODULE" "📦 Module: $MODULE"
echo ""
# ── 重複安裝防呆(1.10.1):install 只管「全新安裝」,一切後續歸 update ──
# 判準是「裝過沒」,不分新版舊版:
# - 新結構 system-dev/ 已存在,或
# - 舊結構 .claude/wiki/ 或 .claude/VERSION 存在(裝過舊版、待遷移)
# 裝過了還跑 install → 會重複建範本、甚至跟真資料並存(先 install 建空殼,遷移就被擋)。
# 正解:偵測到裝過 → 不動任何東西,導去 update(更新/遷移/補新檔都由它處理)。
if [ -d "system-dev" ] || [ -d ".claude/wiki" ] || [ -f ".claude/VERSION" ]; then
t "🛑 偵測到這個專案已經安裝過 system-dev-template。" \
"🛑 system-dev-template is already installed in this project."
t " 後續的更新、遷移、補新檔,一律由「更新腳本」處理(不要重跑 install):" \
" All updates, migrations, and new-file additions are handled by the UPDATER (don't re-run install):"
echo ""
echo " curl -sSL https://raw.githubusercontent.com/uncle6me-web/system-dev-template/main/scripts/update.sh | bash"
echo ""
t " (重跑 install 可能建出空白範本、跟你的真資料並存,故在此停止。)" \
" (Re-running install could create empty templates alongside your real data, so it stops here.)"
exit 0
fi
# ── 偵測 vault 類型 → 決定 raw source(原始文件)路徑 ──────────
# 為什麼:這個模板原本假設「原始文件在 docs/」,但 Logseq / Obsidian
# 這種 PKM vault 有自己的目錄慣例,整理時不能照 docs/ 那套搬動,
# 否則會破壞 vault 結構、讓筆記變不可讀。
# 偵測結果寫進 CLAUDE.md,讓 CC 和未來的 Cowork skill 都知道
# 「該讀/該整理哪裡」而不是亂動。
# 必須在建立 CLAUDE.md 之前跑完。
VAULT_TYPE=""
RAW_SOURCE=""
IS_VAULT="no" # 只有 logseq/obsidian 這種「筆記軟體 vault」才算 yes
if [ -d "logseq" ]; then
VAULT_TYPE="logseq"
RAW_SOURCE="pages/, journals/"
IS_VAULT="yes"
elif [ -d ".obsidian" ]; then
VAULT_TYPE="obsidian"
RAW_SOURCE="$(tn './ (整個 vault 根目錄的 .md' './ (all .md under the vault root)')"
IS_VAULT="yes"
else
VAULT_TYPE="docs"
RAW_SOURCE="docs/"
fi
# 偵測到是筆記 vault → 出聲告訴使用者「我看到了,會小心、不破壞你的筆記結構」。
# 不是筆記(一般開發案等)→ 不囉嗦,默默把 docs/ 當原始文件夾安裝完成。
if [ "$IS_VAULT" = "yes" ]; then
t "🗂️ 偵測到 ${VAULT_TYPE} 筆記庫 → 原始文件:${RAW_SOURCE}" \
"🗂️ Detected a ${VAULT_TYPE} note vault → raw source: ${RAW_SOURCE}"
t " (會保留你筆記軟體的目錄/檔名結構,不搬動、不改名)" \
" (your note app's directory/file structure is preserved — nothing is moved or renamed)"
echo ""
fi
# 把「raw source 宣告區塊」吐出來,給新建的 CLAUDE.md append 或
# 給已存在的 CLAUDE.md 當手動補貼的提示。內容對 CC / Cowork 都是
# 機器可讀的指令(明確路徑 + 不可破壞 vault 結構的約束)。
# 寫進 CLAUDE.md 的 raw source 宣告區塊。給人也給 AI 看:
# 依 locale 只寫「一種語言」進 CLAUDE.md(雙語會讓每個 session 的 context 更滿)。
emit_raw_source_block() {
local source_kind
if [ "$IS_ZH" = "yes" ]; then
if [ "$IS_VAULT" = "yes" ]; then source_kind="${VAULT_TYPE} 筆記庫"
else source_kind="一般專案(原始文件放 raw source 路徑)"; fi
cat <<BLOCK
---
## 原始文件空間(raw source
> 安裝時偵測到的來源型態:**${source_kind}**
> CC 與 Cowork 整理/讀取「人寫的原始文件」時,**只在這裡找、只在這裡動**。
| 項目 ||
|------|----|
| 來源型態 | \`${source_kind}\` |
| raw source | \`${RAW_SOURCE}\` |
**約束(CC 與 Cowork 都必須遵守)**
- 整理 wiki/知識時,原始文件**一律從上方 raw source 路徑讀取**,不要假設是 \`docs/\`
BLOCK
if [ "$IS_VAULT" = "yes" ]; then
cat <<BLOCK
- 這是 **${VAULT_TYPE} 筆記庫**:保留它原本的目錄與檔名慣例,**不得搬動、改名、重新分類** \`.md\` 檔,
以免破壞筆記軟體結構造成筆記不可讀。整理只在 \`system-dev/wiki/\` 產出,**不動 raw source 本身**。
BLOCK
fi
else
if [ "$IS_VAULT" = "yes" ]; then source_kind="${VAULT_TYPE} note vault"
else source_kind="regular project (raw source lives at the path below)"; fi
cat <<BLOCK
---
## Raw source space
> Source type detected at install time: **${source_kind}**
> When CC and Cowork curate/read human-written raw source, **look only here and act only here**.
| Item | Value |
|------|-------|
| Source type | \`${source_kind}\` |
| raw source | \`${RAW_SOURCE}\` |
**Constraints (both CC and Cowork must obey)**
- When curating the wiki/knowledge, **always read raw source from the path above** — don't assume \`docs/\`.
BLOCK
if [ "$IS_VAULT" = "yes" ]; then
cat <<BLOCK
- This is a **${VAULT_TYPE} note vault**: keep its original directory and file-naming conventions. **Do not move, rename, or re-classify** \`.md\` files,
or you'll break the note-app structure and make notes unreadable. Curation output goes only into \`system-dev/wiki/\`; **never touch the raw source itself**.
BLOCK
fi
fi
}
# ── 工具函式 ──────────────────────────────────────
create_dir() {
if [ ! -d "$1" ]; then
mkdir -p "$1"
CREATED+=("$1/")
else
SKIPPED+=("$1/ $(tn '(已存在)' '(already exists)')")
fi
}
download_if_missing() {
local dest="$1" src="$2"
if [ ! -f "$dest" ]; then
mkdir -p "$(dirname "$dest")"
curl -sSL "$src" -o "$dest"
CREATED+=("$dest")
else
SKIPPED+=("$dest $(tn '(已存在,跳過)' '(already exists, skipped)')")
fi
}
# ── 共用結構 ──────────────────────────────────────
# 工具自己的文件骨架收進 system-dev/docs/(不污染用戶根目錄、不跟用戶自己的 docs/ 混)。
# 注意語義分離:這裡的 system-dev/docs/ 是「工具文件」;用戶的 raw source(原始文件)
# 另有其處(見上方 vault 偵測),工具只讀、不搬。
# .claude/ 只留 CC 死綁的 commands/ + hooks/,工具資料一律不放這。
create_dir "system-dev/docs/1-vision"
create_dir "system-dev/docs/2-architecture/decisions"
create_dir "system-dev/docs/4-guides"
create_dir "system-dev/docs/5-records/incidents"
create_dir "system-dev/docs/5-records/test-reports"
create_dir "system-dev/docs/6-user"
create_dir ".claude/commands"
create_dir ".claude/hooks"
download_if_missing "system-dev/docs/README.md" "$REPO_URL/system-dev/docs/README.md"
# 工具版號:放 system-dev/,不寄生 .claude/。
download_if_missing "system-dev/VERSION" "$REPO_URL/system-dev/VERSION"
# ── WIKI 模組 ─────────────────────────────────────
# wiki 是工具資料 → 放 system-dev/wiki/(不放 .claude/)。
# commands/ 與 hooks/ 是 CC 機制檔 → 維持 .claude/。
if $WANT_WIKI; then
create_dir "system-dev/wiki"
download_if_missing "system-dev/wiki/INDEX.md" "$REPO_URL/system-dev/wiki/INDEX.md"
download_if_missing "system-dev/wiki/TAXONOMY.md" "$REPO_URL/system-dev/wiki/TAXONOMY.md"
download_if_missing "system-dev/wiki/status.md" "$REPO_URL/system-dev/wiki/status.md"
download_if_missing "system-dev/wiki/mistakes.md" "$REPO_URL/system-dev/wiki/mistakes.md"
download_if_missing "system-dev/wiki/principles.md" "$REPO_URL/system-dev/wiki/principles.md"
download_if_missing "system-dev/wiki/.wikiignore" "$REPO_URL/system-dev/wiki/.wikiignore"
# wiki 改寫產物(AI 自讀定稿卡片)的正式落點:由工具建好,不靠用戶自救。
create_dir "system-dev/wiki/cards"
[ -f "system-dev/wiki/cards/.gitkeep" ] || { : > "system-dev/wiki/cards/.gitkeep"; CREATED+=("system-dev/wiki/cards/.gitkeep"); }
download_if_missing ".claude/commands/wiki-init.md" "$REPO_URL/.claude/commands/wiki-init.md"
download_if_missing ".claude/commands/wiki-capture.md" "$REPO_URL/.claude/commands/wiki-capture.md"
download_if_missing ".claude/commands/wiki-update.md" "$REPO_URL/.claude/commands/wiki-update.md"
download_if_missing ".claude/commands/wiki-recall.md" "$REPO_URL/.claude/commands/wiki-recall.md"
# wiki 相關 hooks:接關 + 機敏掃描
download_if_missing ".claude/hooks/session-start-recall.sh" "$REPO_URL/.claude/hooks/session-start-recall.sh"
download_if_missing ".claude/hooks/wiki-secret-scan.sh" "$REPO_URL/.claude/hooks/wiki-secret-scan.sh"
# Coworkclaude.ai)整理 wiki 用的 skill:與 CC 的 /wiki-init 共用同一套規則
# (含 typed-edge、frontmatter 標籤、gloss)。沒這支 → claude.ai 來掃時身上沒規則。
download_if_missing "system-dev/docs/SKILL.md" "$REPO_URL/system-dev/docs/SKILL.md"
fi
# ── SDD 模組 ──────────────────────────────────────
if $WANT_SDD; then
create_dir "system-dev/docs/3-specs"
download_if_missing "system-dev/docs/3-specs/TEMPLATE-sdd/design.md" "$REPO_URL/system-dev/docs/3-specs/TEMPLATE-sdd/design.md"
download_if_missing "system-dev/docs/3-specs/TEMPLATE-sdd/tasks.md" "$REPO_URL/system-dev/docs/3-specs/TEMPLATE-sdd/tasks.md"
download_if_missing "system-dev/docs/2-architecture/decisions/TEMPLATE-adr.md" "$REPO_URL/system-dev/docs/2-architecture/decisions/TEMPLATE-adr.md"
download_if_missing ".claude/commands/sdd-check.md" "$REPO_URL/.claude/commands/sdd-check.md"
download_if_missing ".claude/hooks/sdd-guard.sh" "$REPO_URL/.claude/hooks/sdd-guard.sh"
fi
# ── 安裝/更新腳本:一開始就放進 system-dev/scripts/ ──
# 為什麼一開始就裝:之後要更新,用戶(或 CC)直接 `bash system-dev/scripts/update.sh`
# 不必每次都記那串 curl。腳本來源在 main/scripts/(不在 template/)。
create_dir "system-dev/scripts"
download_if_missing "system-dev/scripts/install.sh" "$SCRIPTS_URL/install.sh"
download_if_missing "system-dev/scripts/update.sh" "$SCRIPTS_URL/update.sh"
# ── 共用 hook:專案自訂禁令骨架(預設停用)────────
download_if_missing ".claude/hooks/pre-write-guard.sh" "$REPO_URL/.claude/hooks/pre-write-guard.sh"
# ── 共用指引:GitHub issue 處理(讀/回普世,跨 repo 發要先問,禁自動輪詢)──
download_if_missing ".claude/commands/issue-handle.md" "$REPO_URL/.claude/commands/issue-handle.md"
chmod +x .claude/hooks/*.sh 2>/dev/null || true
# ── 依模組產生 settings.json 的 hooks 區塊 ────────
# settings.json 因模組而異,不能直接下載單一靜態檔,改條件組裝。
build_hooks_json() {
local session_hooks="" pretool_hooks=""
if $WANT_WIKI; then
session_hooks='{ "type": "command", "command": ".claude/hooks/session-start-recall.sh" }'
fi
# PreToolUse 依模組疊加
local pt=()
$WANT_SDD && pt+=('{ "type": "command", "command": ".claude/hooks/sdd-guard.sh" }')
pt+=('{ "type": "command", "command": ".claude/hooks/pre-write-guard.sh" }')
$WANT_WIKI && pt+=('{ "type": "command", "command": ".claude/hooks/wiki-secret-scan.sh" }')
local IFS=,
pretool_hooks="${pt[*]}"
printf '{\n "hooks": {\n'
if [ -n "$session_hooks" ]; then
printf ' "SessionStart": [\n { "matcher": "startup|resume|clear",\n "hooks": [ %s ] }\n ],\n' "$session_hooks"
fi
printf ' "PreToolUse": [\n { "matcher": "Write|Edit",\n "hooks": [ %s ] }\n ]\n' "$pretool_hooks"
printf ' }\n}\n'
}
if [ ! -f ".claude/settings.json" ]; then
build_hooks_json > .claude/settings.json
CREATED+=(".claude/settings.json $(tn "(依 $MODULE 模組產生)" "(generated for module: $MODULE)")")
else
SKIPPED+=(".claude/settings.json $(tn '(已存在,請手動合併 hooks)' '(already exists — merge hooks manually)')")
fi
# ── CLAUDE.md:只在完全不存在時建立 ────────────────
# 新建時把偵測到的 raw source 宣告 append 進去(在建立的當下寫入,
# 不回頭改使用者既有的 CLAUDE.md,維持「已有不覆蓋」原則)。
if [ ! -f "CLAUDE.md" ]; then
download_if_missing "CLAUDE.md" "$REPO_URL/CLAUDE.md"
if [ -f "CLAUDE.md" ]; then
emit_raw_source_block >> CLAUDE.md
CREATED+=("CLAUDE.md $(tn "← 已寫入 raw source 宣告(${VAULT_TYPE}" "← raw source declaration written (${VAULT_TYPE})")")
fi
else
SKIPPED+=("CLAUDE.md $(tn '(已存在,請手動加入對應區塊)' '(already exists — add the block manually)')")
fi
# ── 輸出結果 ──────────────────────────────────────
echo ""
t "✅ 建立了:" "✅ Created:"
# 注意:macOS bash 3.2 在 set -u 下展開「空陣列」會炸 unbound variable
# 所以這裡先確認有元素才展開(SKIPPED 區塊在下方本來就有守,CREATED 補上)。
if [ ${#CREATED[@]} -gt 0 ]; then
for item in "${CREATED[@]}"; do echo " + $item"; done
fi
if [ ${#SKIPPED[@]} -gt 0 ]; then
echo ""
t "⚠️ 跳過(已存在):" "⚠️ Skipped (already exists):"
for item in "${SKIPPED[@]}"; do echo " - $item"; done
fi
echo ""
echo "─────────────────────────────────"
# CLAUDE.md 已存在 → 依模組提醒手動加區塊
if [ -f "CLAUDE.md" ]; then
if ! grep -q "raw source" CLAUDE.md; then
echo ""
t "📌 CLAUDE.md 已存在但缺少 raw source 宣告。" \
"📌 CLAUDE.md exists but lacks a raw source declaration."
t " 請手動把以下區塊貼進去,讓 CC 與 Cowork 知道原始文件在哪、不要亂動既有結構:" \
" Paste the block below in so CC and Cowork know where the raw source is and won't disturb your structure:"
emit_raw_source_block | sed 's/^/ /'
fi
if $WANT_WIKI && ! grep -q "wiki/status.md" CLAUDE.md; then
echo ""
t "📌 CLAUDE.md 已存在但缺少 wiki 讀取順序,請手動加入:" \
"📌 CLAUDE.md exists but lacks the wiki reading order — please add it manually:"
echo ""
if [ "$IS_ZH" = "yes" ]; then
cat <<'SNIP'
## Wiki 讀取順序(pushhook 開 session 自動注入)
| 檔案 | 時機 | 用途 |
|------|------|------|
| `system-dev/wiki/status.md` | session 開始第一件事 | 當前進度 |
| `system-dev/wiki/principles.md` | 設計任何東西前 | 跨全局原則,必服從 |
| `system-dev/wiki/mistakes.md` | 做新功能前 | 已知踩坑 |
SNIP
else
cat <<'SNIP'
## Wiki reading order (push: auto-injected at session start)
| File | When | Purpose |
|------|------|---------|
| `system-dev/wiki/status.md` | first thing at session start | current progress |
| `system-dev/wiki/principles.md` | before designing anything | global principles, must obey |
| `system-dev/wiki/mistakes.md` | before building a new feature | known pitfalls |
SNIP
fi
fi
if $WANT_SDD && ! grep -q "system-dev/docs/3-specs" CLAUDE.md; then
echo ""
t "📌 CLAUDE.md 已存在但缺少 SDD 鐵律,請手動加入:" \
"📌 CLAUDE.md exists but lacks the SDD iron rule — please add it manually:"
echo ""
if [ "$IS_ZH" = "yes" ]; then
cat <<'SNIP'
## 絕對鐵律
1. 任何 code 變動前必須有對應 SDDsystem-dev/docs/3-specs/[子系統]/design.md
找不到 → 停手問負責人,不要自行建立。
SNIP
else
cat <<'SNIP'
## Iron rule
1. Every code change must have a matching SDD (system-dev/docs/3-specs/[subsystem]/design.md).
Not found → stop and ask the owner; do not create one on your own.
SNIP
fi
fi
fi
# settings.json 已存在 → 依模組提醒要合併哪些 hook
if [ -f ".claude/settings.json" ]; then
MISSING_HOOKS=()
$WANT_WIKI && ! grep -q "session-start-recall.sh" .claude/settings.json && MISSING_HOOKS+=("SessionStart: session-start-recall.sh")
$WANT_WIKI && ! grep -q "wiki-secret-scan.sh" .claude/settings.json && MISSING_HOOKS+=("PreToolUse(Write|Edit): wiki-secret-scan.sh")
$WANT_SDD && ! grep -q "sdd-guard.sh" .claude/settings.json && MISSING_HOOKS+=("PreToolUse(Write|Edit): sdd-guard.sh")
if [ ${#MISSING_HOOKS[@]} -gt 0 ]; then
echo ""
t "📌 .claude/settings.json 已存在,請手動把以下 hooks 合併進去(保留既有設定):" \
"📌 .claude/settings.json exists — merge the hooks below in manually (keep your existing settings):"
for h in "${MISSING_HOOKS[@]}"; do echo "$h"; done
fi
fi
# pre-write-guard 是空殼,提醒它預設不攔(避免「以為有保護其實沒有」的安全錯覺)
echo ""
t "️ .claude/hooks/pre-write-guard.sh 是「按需手填的空插槽」,預設不攔任何東西。" \
"️ .claude/hooks/pre-write-guard.sh is an empty slot to fill on demand — by default it blocks nothing."
t " 需要專案禁令?最簡單是叫你的 CC 寫一支貼合的 guard hook(比範本表達力強);" \
" Need project-specific bans? Easiest is to ask your CC to write a tailored guard hook (more expressive than the template);"
t " 或自己填 FORBIDDEN_PATTERNS 並到 settings.json 掛上才會生效。" \
" or fill in FORBIDDEN_PATTERNS yourself and wire it into settings.json to take effect."
echo ""
t "🚀 下一步:" "🚀 Next steps:"
if $WANT_WIKI; then
t " 在 Claude Code 對話裡執行 /wiki-init" \
" In a Claude Code conversation, run /wiki-init"
t " CC 會掃描現有文件、套用 .wikiignore、建立 wiki。" \
" CC will scan your existing docs, apply .wikiignore, and build the wiki."
fi
if $WANT_SDD; then
t " 動 code 前先在 system-dev/docs/3-specs/[子系統]/ 建 design.md(可用 /sdd-check 協助)" \
" Before touching code, create design.md under system-dev/docs/3-specs/[subsystem]/ (use /sdd-check to help)"
fi
t " GitHub issueCC 可直接 /issue-handle 讀回自己 repo 的 issue(禁自動輪詢)" \
" GitHub issues: CC can use /issue-handle to read issues from its own repo (no auto-polling)"
echo ""
+332
View File
@@ -0,0 +1,332 @@
#!/bin/bash
# system-dev-template updater
# 已安裝舊版的人,一鍵更新到新版。
#
# 核心安全原則:只覆蓋「模板/邏輯檔」,絕不碰「使用者資料檔」。
# ✅ 可覆蓋:hooks/*.sh、commands/*.md、TEMPLATE-*、wiki/INDEX.md
# ——這些由模板維護,使用者不會手改,新版直接換掉。
# 🔒 絕不碰:wiki/status.md、mistakes.md、decisions-summary.md、TAXONOMY.md、.wikiignore、
# settings.json、CLAUDE.md
# ——這些是使用者自己填的內容,覆蓋=清空他的記憶與設定。
#
# 「第一次更新」的雞生蛋問題:
# 舊版本機沒有 update.sh。所以第一次靠 README 那行 curl 從遠端抓這支腳本來跑。
# 跑完它會把自己也更新進 scripts/update.sh,之後就能直接跑本機的 `bash scripts/update.sh`。
set -euo pipefail
# ── i18n:依 locale 選語言,預設英文(curl | bash 常為 LANG=C)──
case "${LC_ALL:-${LC_MESSAGES:-${LANG:-}}}" in
zh*|*Hant*|*Hans*) IS_ZH="yes" ;;
*) IS_ZH="no" ;;
esac
t() { if [ "$IS_ZH" = "yes" ]; then printf '%s\n' "$1"; else printf '%s\n' "$2"; fi; }
tn() { if [ "$IS_ZH" = "yes" ]; then printf '%s' "$1"; else printf '%s' "$2"; fi; }
REPO_RAW="https://raw.githubusercontent.com/uncle6me-web/system-dev-template/main"
TEMPLATE_URL="$REPO_RAW/template"
UPDATED=()
KEPT=()
NEW=()
TEMPLATED=()
MIGRATED=()
COEXIST=()
# ── 版本比對:先看本機 vs 遠端,給使用者「值不值得更新」的判斷 ──
# VERSION 新位置在 system-dev/,舊位置在 .claude/(1.8.x 以前)。優先讀新、回退舊。
LOCAL_VER="$(tn '(未知)' '(unknown)')"
if [ -f "system-dev/VERSION" ]; then
LOCAL_VER="$(tr -d '[:space:]' < system-dev/VERSION)"
elif [ -f ".claude/VERSION" ]; then
LOCAL_VER="$(tr -d '[:space:]' < .claude/VERSION)"
fi
REMOTE_VER="$(curl -sSL "$TEMPLATE_URL/system-dev/VERSION" 2>/dev/null | tr -d '[:space:]' || echo '')"
# 容錯:curl 對 404 會把「404:NotFound」當內容輸出(非空),舊版誤把它寫進 VERSION。
# 這裡驗證必須像版號(X.Y.Z),否則一律視為取不到,避免污染 VERSION 檔。
case "$REMOTE_VER" in
[0-9]*.[0-9]*.[0-9]*) : ;; # 形如 1.9.0 → 合法
*) REMOTE_VER="" ;; # 404 / HTML 錯誤頁 / 其他 → 當作沒抓到
esac
echo ""
echo "🔄 system-dev-template updater"
echo "================================="
t " 本機版本:${LOCAL_VER}" " Local version: ${LOCAL_VER}"
t " 最新版本:${REMOTE_VER:-取不到(檢查網路)}" \
" Latest version: ${REMOTE_VER:-unavailable (check network)}"
echo ""
if [ -z "$REMOTE_VER" ]; then
t "❌ 取不到遠端版本,可能是網路問題。請稍後再試。" \
"❌ Could not fetch the remote version (likely a network issue). Please try again later."
exit 1
fi
if [ "$LOCAL_VER" = "$REMOTE_VER" ]; then
t "✅ 已是最新版(${LOCAL_VER}),不需更新。" \
"✅ Already up to date (${LOCAL_VER}), nothing to update."
t " (仍會同步模板邏輯檔,確保 hooks/commands 與最新一致。)" \
" (Template logic files will still be synced to keep hooks/commands in line with the latest.)"
echo ""
fi
# ── 結構遷移(1.9.0):舊版把 wiki/VERSION 放 .claude/、工具 docs 放根 docs/ ──
# 新版一律收進 system-dev/。這裡冪等遷移:偵測舊位置 → 搬到 system-dev/,已搬過則略過。
# 必須在「模組偵測」之前跑(偵測靠目錄存在與否判斷,搬完才看得到新位置)。
#
# 安全原則:
# - wiki 整包搬(含 cards/ 與可能的 wiki/.git),用 mv 保留內含 .git。
# - docs 只搬「工具自己鋪的白名單」子目錄;用戶自填在 docs/ 的其他內容一律不動。
# - 目的地已存在同名 → 不覆蓋(保留用戶在新位置的東西),略過該項。
migrate_dir() { # $1=舊路徑 $2=新路徑
local from="$1" to="$2"
[ -e "$from" ] || return 0 # 舊的不存在 → 無需遷移
if [ -e "$to" ]; then
# 目的地已存在。兩種可能:
# (a) 已遷移過 → 舊位置不該還在;冪等略過即可。
# (b) 用戶先 install 建了空殼 → 舊位置仍有真資料,現在「並存」。
# 不能靜默跳過 (b),也絕不自動合併(覆蓋風險)。→ 記為「並存待合併」,警告。
COEXIST+=("$from$to")
return 0
fi
mkdir -p "$(dirname "$to")"
if mv "$from" "$to" 2>/dev/null; then
MIGRATED+=("$from$to")
fi
}
# 任一舊位置還在 → 需要遷移(遷移本身冪等:已搬的項目會被 migrate_dir 略過)。
NEEDS_MIGRATE="no"
if [ -d ".claude/wiki" ] || [ -f ".claude/VERSION" ] \
|| [ -d "docs/3-specs" ] || [ -f "docs/SKILL.md" ] || [ -f "docs/README.md" ]; then
NEEDS_MIGRATE="yes"
fi
if [ "$NEEDS_MIGRATE" = "yes" ]; then
t "🔧 偵測到舊版結構,遷移到 system-dev/ …" "🔧 Old layout detected — migrating into system-dev/ …"
mkdir -p system-dev
# wiki(含 cards/ 與內含的 .git)整包搬
migrate_dir ".claude/wiki" "system-dev/wiki"
# 工具版號
migrate_dir ".claude/VERSION" "system-dev/VERSION"
# 工具文件白名單(只搬工具鋪的,用戶自填的 docs 內容不動)
migrate_dir "docs/SKILL.md" "system-dev/docs/SKILL.md"
migrate_dir "docs/README.md" "system-dev/docs/README.md"
migrate_dir "docs/1-vision" "system-dev/docs/1-vision"
migrate_dir "docs/2-architecture" "system-dev/docs/2-architecture"
migrate_dir "docs/3-specs" "system-dev/docs/3-specs"
migrate_dir "docs/4-guides" "system-dev/docs/4-guides"
migrate_dir "docs/5-records" "system-dev/docs/5-records"
migrate_dir "docs/6-user" "system-dev/docs/6-user"
echo ""
fi
# ── 工具函式 ───────────────────────────────────────
# 覆蓋更新:模板/邏輯檔,無條件抓最新版蓋掉。
update_file() {
local dest="$1" src="$2"
mkdir -p "$(dirname "$dest")"
if [ -f "$dest" ]; then
if curl -sSL "$src" -o "$dest.tmp" 2>/dev/null && [ -s "$dest.tmp" ]; then
if cmp -s "$dest" "$dest.tmp"; then
rm -f "$dest.tmp" # 內容相同,不算更新
else
mv "$dest.tmp" "$dest"
UPDATED+=("$dest")
fi
else
rm -f "$dest.tmp"
t " ⚠️ 抓取失敗,保留原檔:$dest" " ⚠️ Download failed, keeping the original: $dest"
fi
else
if curl -sSL "$src" -o "$dest" 2>/dev/null && [ -s "$dest" ]; then
NEW+=("$dest") # 新功能:舊版沒有的檔
else
rm -f "$dest"
t " ⚠️ 抓取失敗:$dest" " ⚠️ Download failed: $dest"
fi
fi
}
# 保留:使用者資料檔,只記錄「有保留」,永遠不動。
keep_file() {
[ -f "$1" ] && KEPT+=("$1") || true
}
# 補新檔:舊版沒有、新版才有的「使用者資料檔」(如 principles.md)。
# 不存在 → 抓範本下來(之後由使用者/CC 填);已存在 → 當用戶資料保留,絕不覆蓋。
add_if_missing() {
local dest="$1" src="$2"
if [ -f "$dest" ]; then
KEPT+=("$dest")
elif curl -sSL "$src" -o "$dest" 2>/dev/null && [ -s "$dest" ]; then
NEW+=("$dest")
else
rm -f "$dest"
fi
}
# 客製檔:使用者一定會手填內容(如 pre-write-guard.sh)。
# - 已存在 → 絕不覆蓋,但把最新模板版抓到 <檔名>.template.sh 旁邊,供使用者自行 diff 採納。
# - 不存在 → 視同新檔,直接抓本體(第一次安裝才會走這條)。
keep_with_template() {
local dest="$1" src="$2"
if [ -f "$dest" ]; then
KEPT+=("$dest")
local tmpl="${dest%.sh}.template.sh"
if curl -sSL "$src" -o "$tmpl.tmp" 2>/dev/null && [ -s "$tmpl.tmp" ]; then
if [ -f "$tmpl" ] && cmp -s "$tmpl" "$tmpl.tmp"; then
rm -f "$tmpl.tmp" # 模板版沒變,不重複提示
else
mv "$tmpl.tmp" "$tmpl"
TEMPLATED+=("$tmpl")
fi
else
rm -f "$tmpl.tmp"
fi
else
update_file "$dest" "$src" # 還沒裝過 → 當新檔處理
fi
}
# ── 偵測已安裝哪些模組(依現有檔案判斷,更新只動已裝的)──
# 遷移已在上面跑完,這裡看新位置 system-dev/。
HAS_WIKI=false
HAS_SDD=false
[ -d "system-dev/wiki" ] && HAS_WIKI=true
if [ -f ".claude/hooks/sdd-guard.sh" ] || [ -d "system-dev/docs/3-specs/TEMPLATE-sdd" ]; then HAS_SDD=true; fi
t "📦 偵測到已安裝模組:" "📦 Detected installed modules:"
$HAS_WIKI && echo " • LLM Wiki"
$HAS_SDD && echo " • SDD"
{ $HAS_WIKI || $HAS_SDD; } || \
t " (未偵測到任何模組——這裡可能還沒安裝,請改跑 install.sh" \
" (No modules detected — nothing installed here yet; run install.sh instead.)"
echo ""
# ── 客製檔:使用者手填的 guardrail,永不覆蓋(issue #3)──
# pre-write-guard.sh 的定位是「空白客製模板,使用者沒配置前不提供保護」(CHANGELOG 1.2.0)。
# 下游通常已塞滿自己的 enforcement,直接覆蓋=無聲關掉整套 guardrail。
# 改為:保留原檔不動,新版範本另存 pre-write-guard.template.sh,由使用者自行 diff 採納。
keep_with_template ".claude/hooks/pre-write-guard.sh" "$TEMPLATE_URL/.claude/hooks/pre-write-guard.sh"
# ── 模板/邏輯檔:覆蓋更新 ──────────────────────────
# 共用 hook 與指引
update_file ".claude/commands/issue-handle.md" "$TEMPLATE_URL/.claude/commands/issue-handle.md"
update_file "system-dev/VERSION" "$TEMPLATE_URL/system-dev/VERSION"
if $HAS_WIKI; then
# wiki 的「邏輯檔」:導航與 hooks,可覆蓋。wiki 資料在 system-dev/hooks/commands 留 .claude/。
update_file "system-dev/wiki/INDEX.md" "$TEMPLATE_URL/system-dev/wiki/INDEX.md"
update_file ".claude/hooks/session-start-recall.sh" "$TEMPLATE_URL/.claude/hooks/session-start-recall.sh"
update_file ".claude/hooks/wiki-secret-scan.sh" "$TEMPLATE_URL/.claude/hooks/wiki-secret-scan.sh"
update_file ".claude/commands/wiki-init.md" "$TEMPLATE_URL/.claude/commands/wiki-init.md"
update_file ".claude/commands/wiki-capture.md" "$TEMPLATE_URL/.claude/commands/wiki-capture.md"
update_file ".claude/commands/wiki-update.md" "$TEMPLATE_URL/.claude/commands/wiki-update.md"
update_file ".claude/commands/wiki-recall.md" "$TEMPLATE_URL/.claude/commands/wiki-recall.md"
# Coworkclaude.ai)的 wiki 整理 skill:規則檔,可覆蓋
update_file "system-dev/docs/SKILL.md" "$TEMPLATE_URL/system-dev/docs/SKILL.md"
# wiki 的「使用者資料」:絕不碰
keep_file "system-dev/wiki/status.md"
keep_file "system-dev/wiki/mistakes.md"
# principles.md1.10):舊版沒有 → 補範本;已有 → 當用戶資料保留
add_if_missing "system-dev/wiki/principles.md" "$TEMPLATE_URL/system-dev/wiki/principles.md"
keep_file "system-dev/wiki/decisions-summary.md"
keep_file "system-dev/wiki/TAXONOMY.md"
keep_file "system-dev/wiki/.wikiignore"
fi
if $HAS_SDD; then
# SDD 範本與 hook:可覆蓋
update_file "system-dev/docs/3-specs/TEMPLATE-sdd/design.md" "$TEMPLATE_URL/system-dev/docs/3-specs/TEMPLATE-sdd/design.md"
update_file "system-dev/docs/3-specs/TEMPLATE-sdd/tasks.md" "$TEMPLATE_URL/system-dev/docs/3-specs/TEMPLATE-sdd/tasks.md"
update_file "system-dev/docs/2-architecture/decisions/TEMPLATE-adr.md" "$TEMPLATE_URL/system-dev/docs/2-architecture/decisions/TEMPLATE-adr.md"
update_file ".claude/commands/sdd-check.md" "$TEMPLATE_URL/.claude/commands/sdd-check.md"
update_file ".claude/hooks/sdd-guard.sh" "$TEMPLATE_URL/.claude/hooks/sdd-guard.sh"
fi
# ── 自我更新:把最新的 update.sh / install.sh 抓到 system-dev/scripts/ ──
# 這兩支在 main/scripts/ 下(不在 template/);落地位置新版收進 system-dev/scripts/。
update_file "system-dev/scripts/update.sh" "$REPO_RAW/scripts/update.sh"
update_file "system-dev/scripts/install.sh" "$REPO_RAW/scripts/install.sh"
chmod +x .claude/hooks/*.sh system-dev/scripts/*.sh 2>/dev/null || true
# ── 使用者資料檔:絕不碰,但提醒「設定可能有新欄位要手動補」──
keep_file ".claude/settings.json"
keep_file "CLAUDE.md"
# ── 結果輸出 ───────────────────────────────────────
echo ""
echo "─────────────────────────────────"
if [ ${#MIGRATED[@]} -gt 0 ]; then
echo ""
t "📦 結構遷移(已收進 system-dev/):" "📦 Layout migrated (moved into system-dev/):"
for f in "${MIGRATED[@]}"; do echo "$f"; done
fi
if [ ${#COEXIST[@]} -gt 0 ]; then
echo ""
t "🛑 偵測到 wiki 並存(新舊位置都有資料,需要合併):" \
"🛑 Coexisting wiki detected (both old and new locations have data — needs merging):"
for f in "${COEXIST[@]}"; do echo "$f"; done
t " 成因:先跑過 install(建了空殼)才遷移,舊位置真資料沒被搬。" \
" Cause: install ran first (created an empty shell), so migration skipped your real data in the old location."
t " 不自動合併(避免覆蓋你的資料)。請叫你的 CC:" \
" Not auto-merged (to avoid overwriting your data). Ask your CC:"
t " 「.claude/wiki/ 和 system-dev/wiki/ 並存,請逐檔比對、把真資料合進 system-dev/,再刪舊的」" \
" \"There are two wikis (.claude/wiki/ and system-dev/wiki/) — diff each file, merge the real data into system-dev/, then delete the old one.\""
fi
if [ ${#NEW[@]} -gt 0 ]; then
echo ""
t "🆕 新功能(舊版沒有,已加入):" "🆕 New features (absent in the old version, now added):"
for f in "${NEW[@]}"; do echo " + $f"; done
fi
if [ ${#UPDATED[@]} -gt 0 ]; then
echo ""
t "⬆️ 已更新(覆蓋成新版):" "⬆️ Updated (overwritten with the new version):"
for f in "${UPDATED[@]}"; do echo " ~ $f"; done
fi
if [ ${#NEW[@]} -eq 0 ] && [ ${#UPDATED[@]} -eq 0 ]; then
echo ""
t "✨ 模板邏輯檔已全部最新,無需變動。" \
"✨ All template logic files are already up to date — no changes needed."
fi
if [ ${#KEPT[@]} -gt 0 ]; then
echo ""
t "🔒 完整保留(你的內容/設定,從未碰過):" \
"🔒 Fully preserved (your content/settings, never touched):"
for f in "${KEPT[@]}"; do echo " = $f"; done
fi
if [ ${#TEMPLATED[@]} -gt 0 ]; then
echo ""
t "📋 客製檔有新版範本(你的原檔沒動,新版另存旁邊,請自行 diff 採納):" \
"📋 Custom files have a new template version (your original is untouched; the new one is saved alongside — diff and adopt as you like):"
for f in "${TEMPLATED[@]}"; do
echo "$f"
t " 比對:diff \"${f%.template.sh}.sh\" \"$f\"" \
" compare: diff \"${f%.template.sh}.sh\" \"$f\""
done
fi
# ── settings.json 提醒:新模組 hook 可能要手動補 ──
if [ -f ".claude/settings.json" ]; then
MISSING=()
$HAS_WIKI && ! grep -q "session-start-recall.sh" .claude/settings.json && MISSING+=("SessionStart: session-start-recall.sh")
$HAS_WIKI && ! grep -q "wiki-secret-scan.sh" .claude/settings.json && MISSING+=("PreToolUse(Write|Edit): wiki-secret-scan.sh")
$HAS_SDD && ! grep -q "sdd-guard.sh" .claude/settings.json && MISSING+=("PreToolUse(Write|Edit): sdd-guard.sh")
if [ ${#MISSING[@]} -gt 0 ]; then
echo ""
t "📌 settings.json 是你的設定(沒動),但偵測到缺以下 hook,請手動補上:" \
"📌 settings.json is yours (untouched), but these hooks are missing — please add them manually:"
for h in "${MISSING[@]}"; do echo "$h"; done
fi
fi
echo ""
t "🚀 更新完成:${LOCAL_VER}${REMOTE_VER}" "🚀 Update complete: ${LOCAL_VER}${REMOTE_VER}"
t " 下次更新直接跑:bash system-dev/scripts/update.sh" " Next time, just run: bash system-dev/scripts/update.sh"
t " 改了什麼看:CHANGELOG.md" " See what changed: CHANGELOG.md"
echo ""
+2
View File
@@ -0,0 +1,2 @@
# wiki 機敏三層防護 L1:整檔排除(不進 wiki 投影/掃描的檔)
# 一行一個 glob,相對 .claude/wiki/。空檔也合法。
+62
View File
@@ -0,0 +1,62 @@
# system-dev/wiki/ — LLM 記憶系統
> 新 session 開始時從這裡導航。
> 目的:讓 CC 不需要重新學習已知的事。
> 維護者:CC(人不手動編輯這裡)
---
## push 檔(session 開始由 hook 主動注入,CC 行動前必看見)
| 檔案 | 注入形態 | 內容 |
|------|---------|------|
| `status.md` | 全文 | 當前進度、下一步(時態狀態)|
| `principles.md` | 全文(一行一條)| 跨全局設計原則,行動前必服從 |
| `mistakes.md` | 標題+一行症狀,全文按需展開 | 踩過的坑、被糾正的誤解(防不自覺盲區)|
> 為什麼這三個 push 而非 pull:它們是「CC 不會主動查、但不看就出事」的盲區。詳見 `/wiki-init` 的「push vs pull」。
---
## pullcards/CC 按需檢索)
一切知識內容——原文摘要、AI 筆記、決策、概念知識——都寫成 `cards/<bucket>/` 的概念原子卡。
`decisions-summary.md` 已降級為 cards(決策=知識內容);既有的保留為相容。
---
## 維護規則
1. 只增不刪——記錄 append,內容改了加新條目說明「舊的已更新」
2. status.md 每次 session 結束更新;mistakes/principles 一發現就 append
3. principles 一行一條、≤15 條(超過代表該合併或下放成 card)
4. **新增一個檢索角度 = 在下方「多角度視圖」加一節,不開新實體檔、不問用戶**
---
## 多角度視圖(由 /wiki-init、/wiki-capture 填入)
INDEX 是**所有檢索角度的入口**,不只標籤。原文是唯讀 SSoT,wiki 是改寫過的記憶。
新增角度只要在這裡加一節(如「決策角度」「原則角度」),指向對應 cards 或 push 檔——**不必新增實體特殊檔**。
### 子系統角度(按 `TAXONOMY.md` 的軸聚類,指向桶子索引)
#### 架構決策(跨子系統)
- [[decisions/00-INDEX]] — arcrun 架構決策(13 卡,已從 decisions-summary 全量改寫成原子卡)
> 子系統卡桶(零件架構 / cypher / credential / recipe / kbdb / 薄殼 / 部署)尚未獨立建——
> 決策卡已用 frontmatter `tags:` 掛子系統軸,可從 INDEX 標籤過濾;純子系統知識卡由 /wiki-capture 依需開桶。
### 決策角度(取代舊 decisions-summary.md 的視圖)
> `decisions-summary.md`(全文)保留為相容;13 條決策已逐條改寫成 `cards/decisions/` 原子卡,入口見 [[decisions/00-INDEX]]。
> 按子系統分群的清單在桶子索引內,這裡只放跨子系統最常查的:
- [[工作流是default零件是例外]] — 自用→工作流,全生態重用→才零件(mindset §1)
- [[薄殼原則-能力長在API]] — 能力只實作一次放 API,介面全薄殼(rule 07)
- [[自力救濟階梯-缺能力怎麼補]] — 缺能力分流:自家補API/第三方 workflow 補丁(issue #4
- [[service-binding-vs-cypher-binding]] — 零件串接走 HTTP URL,禁 service bindingrule 03
> 結構:INDEX(多角度入口)→ `cards/<bucket>/00-INDEX.md`(桶子索引,固定名)→ 概念原子卡。
> 指 `00-INDEX` **一律帶路徑** `[[bucket/00-INDEX]]`(固定名跨桶撞名);卡片間用裸 `[[卡名]]`
> 分類由卡片 frontmatter `tags:` 承載,標籤字典見 `TAXONOMY.md`。詳見 `/wiki-init` 規範。
+49
View File
@@ -0,0 +1,49 @@
# TAXONOMY.md — 標籤字典(arcrun repo 專屬)
> wiki 卡片的 frontmatter `tags:` **只能用這裡列出的標籤**
> 這不是凍結——字典**可以擴充**,只是**禁止繞過字典在卡片裡直接冒新標籤**。
> 維護者:CC(由 /wiki-init 初始化、隨 wiki 演進受控擴充)。
>
> **遇到現有軸都裝不下的內容時,照這個流程(先查、後擴、登記)**
> 1. **先查既有**:現有標籤真的都不合,還是只是同義詞?(`零件` vs `component` vs `WASM零件` 是同一軸,別重造)
> 2. **確實是新軸** → 把新標籤加進本檔(附一句定義),再用。不必停下來問人,但「先登記再使用」這道查核不能省。
> 3. 自由增生才是要防的——同義標籤散開會讓同類卡片分散、下游聚類失準。受控擴充(先查重、再登記)不會。
>
> **字典是每個 repo 各自的**arcrun 是開發 repo,軸是「子系統/層級/決策類型」,與知識型 vault 的「知識管理/學習認知」本來就不同。
---
## 分類採雙軸(一張卡可多重歸屬)
分類由 **frontmatter `tags:`** 承載,不靠資料夾、不靠行內 `#tag`
一張卡同時掛「子系統 1-3 個 + 形態 0-2 個」,可從任一軸過濾。
### 子系統 / 領域(主軸,每卡 1-3 個)
> arcrun 的架構分層。卡講「哪一塊」就掛對應標籤。
- `零件架構` — WASM 零件、component worker、registry、部署模式
- `cypher` — cypher-executor、workflow 執行、host functions、graph executor
- `credential` — auth primitive、加解密、JWT、recipe auth、credential 注入
- `recipe` — recipe schema、UUID 市場模型、投稿、信任機制
- `kbdb` — KBDB 資料層、三表、proxy、租戶隔離、embedding/vectorize
- `薄殼` — CLI / MCP / SDK 介面層、能力下沉 API、帳號統一
- `部署` — wrangler、local-deploy、self-hosted、CI 邊界、account override
- `平台原則` — 跨子系統的世界觀(工作流 vs 零件、誠實不假綠、AI→工具)
### 形態(副軸,每卡 0-2 個)
- `架構決策` — 一次性的設計選擇 + trade-off(取代舊 decisions-summary 視圖)
- `踩坑` — 事件復盤、被糾正的誤解(與 mistakes.md 互補:mistakes 是 push 摘要,card 是全文)
- `機制說明` — 某個能力「怎麼運作」的自包含解釋
- `禁令` — hook 強制的硬規則背後的 why
- `案例經驗` — 壓測、dogfood、實證落地的具體記錄
---
## 規則
1. **先查重、再登記、才使用**:禁止繞過字典在卡片直接冒新標籤;新標籤先確認非現有同義詞,加進本檔(附定義)再用。
2. **子系統 vs 形態分開**:子系統是「講哪一塊」,形態是「以什麼形式呈現」,不要混。
3. **頂層 INDEX.md 的標籤視圖依本字典的軸聚類**——字典改了,INDEX 視圖跟著更新。
4. **新增子系統軸要慎**:子系統是檢索骨架,動它影響全庫聚類;形態軸(呈現形式)擴充較安全。不確定就先用現有最接近的,並在卡片或本檔註記「待人類複核此分類」。
@@ -0,0 +1,34 @@
# 架構決策
> 桶子索引(容器:只連不重寫)。一決策一卡,自包含 + trade-off。
> 新決策由 /wiki-capture 寫進本桶並在此 append 一行。
> 權威全文:`system-dev/docs/2-architecture/decisions/DECISIONS.md`;本桶是 AI 改寫過的可檢索版。
## 零件 / 工作流 / 能力補丁
- [[工作流是default零件是例外]] — 自用→工作流,全生態重用→才零件;「能包」≠「該包」
- [[自力救濟階梯-缺能力怎麼補]] — 自家API缺→補API/第三方→workflow補丁/純計算→code-node
- [[薄殼原則-能力長在API]] — CLI/MCP/lib 只暴露,齊的單位是「能力」不是「端點」
- [[薄殼規則晚於實作-MCP漂移是歷史債]] — 為何 MCP/CLI 不一致:紀律 2026-06-07 才補、補前漂移
## 串接 / 部署
- [[service-binding-vs-cypher-binding]] — 零件走 HTTP URL,禁新增 service binding
- [[same-zone-1042用flag解不用binding]] — self-hosted cypher 加 global_fetch_strictly_public
- [[R2用途-平台零件不從R2讀]] — 平台零件 bundle 進 WorkerR2 只 Phase 5 用戶自製
- [[self-hosted部署-共享install加指紋跳過]] — root 共享 install + manifest content-hash 跳過未變動
- [[不依賴CI-執行鏈路vs零件投稿]] — 執行鏈路走 local script,零件投稿才走 PR/CI
## credential / recipe
- [[多worker-ENCRYPTION_KEY同步]] — 各 Worker secret put 同份 key,漂移→解密失敗表現為 401/403
- [[Recipe-UUID市場模型]] — canonical+uuid+author 多維身份,per-uuid 市場數據
## kbdb
- [[embedding是base-optional模組]] — binding 開才啟用,預設關降級 LIKE,精耕只 embed:true
- [[碰舊Mira需求先查頂層覆寫]] — mira 蒸發→當普世缺口,先查頂層 mira-dissolve 覆寫
## 平台目標
- [[Haiku能驅動是設計目標]] — Haiku 過=設計達成;撞牆是介面缺陷訊號非換模型
@@ -0,0 +1,39 @@
---
tags: [平台原則, 架構決策]
gloss: arcrun 能用 Haiku 驅動才算設計達成。撞牆不是換模型,是信號要改介面。
---
# Haiku 能驅動是設計目標
← [[decisions/00-INDEX]]
**來源**`system-dev/wiki/decisions-summary.md`Haiku 就能搞定是設計目標)、`.claude/rules/06-mindset.md §1`
**最後更新**2026-06-27
## 摘要
arcrun 的價值主張是「比直接開發容易 + 省 token + 省時間」。只有 Sonnet 能驅動時,「省」就不成立。Haiku 通過才是設計目標達成的證明;反之,Haiku 撞牆=介面缺陷信號,應改介面而非換模型。
## 重點
- **設計思想**:Haiku 作為基礎模型,若某操作需要 Sonnet 才行,代表介面太難、太多隱含上下文、需要手工智能補齊——這是架構欠債。
- **測試邏輯**
- Haiku 通過 ✅ → 設計目標達成,介面夠清晰
- Haiku 不通 → 升 Sonnet
- Sonnet 通過 → 介面缺陷,要改介面白痴化
- Sonnet 也不通 → 功能 bug,要改邏輯
- **不要的做法**:「Haiku 過不了就換 Sonnet」= 放棄設計目標、接受介面複雜度只有豪華模型才能應付。
- **改進信號**:Haiku 哪個步驟卡住(參數寫法?狀態機理解?錯誤訊息太模糊?),就是該改哪個介面。
- **誠實限制**mindset §7):Haiku 撞牆不要假綠(mock 迴應),要誠實標「需要 Sonnet」或「無法 resolve」,作為改進指標。
## 實體
- **Haiku**Claude 3.5 Haiku)— 輕量級語言模型,arcrun 驅動的基準。
- **設計達成**design achievement)— 介面清晰度高到 Haiku 能自主操作,無需人工介入。
- **介面缺陷**interface defect)— Haiku 無法理解但 Sonnet 能理解的操作,表示介面隱含前置知識過多。
- **功能 bug**functional bug)— Sonnet 也無法做到的操作,表示後端邏輯確實有問題。
## 關聯
### 內文知識關係
- 設計達成 >> 以 Haiku 通過 >> 衡量
- Haiku 不通 >> 信號 >> 介面缺陷
- 介面缺陷 >> 應該 >> 改介面
- 功能 bug >> 應該 >> 改邏輯
### 卡片關係
- [[Haiku能驅動是設計目標]] >> 對應 mindset >> [[工作流是default零件是例外]]
@@ -0,0 +1,33 @@
---
tags: [零件架構, 部署, 架構決策]
gloss: 平台內建零件 WASM 已 bundle 進各自 Worker binary,不從 R2 動態讀。R2 只在 Phase 5(用戶自製零件)啟用。
---
# R2 用途 — 平台零件不從 R2 讀
← [[decisions/00-INDEX]]
**來源**`system-dev/wiki/decisions-summary.md`R2 的用途)、`.claude/rules/03-component-architecture.md §2`
**最後更新**2026-06-27
## 摘要
平台內建零件的 WASM 文件在部署時已 bundle 進各自 Worker 的 binary(透過 codeload tarball);R2 存儲的機制只為 Phase 5(用戶上傳自製零件)保留。
## 重點
- 平台零件 = 獨立 Worker,已部署完成,走 HTTP URL 呼叫;沒有 R2 動態載入那一步。
- WASM 文件的位置:`registry/components/{name}/` 是原始碼,`.component-builds/{name}/component.wasm` 是編譯產物,commit 進 repo 後作為 codeload 部署來源。
- R2 存儲的唯一用途:Phase 5 啟用時,用戶透過 `acr component submit` 上傳自製 .wasm → R2 存儲 → runtime 動態執行(當時 API 才支援 `GET /user/{id}/components/{name}/wasm`)。
- 當前「怎麼從 R2 取 WASM」是典型誤問——错误假设的标志。
## 實體
- **平台內建零件**platform components)— 由 arcrun 官方維護、已編譯成 .wasm、獨立部署成 Worker 的零件(gmail、telegram、http_request 等)。
- **R2**Cloudflare R2)— 對象存儲服務,當前未用於平台零件;Phase 5 起用於存放用戶自製零件。
- **codeload 部署**tarball deployment)— 透過 GitHub codeload tarball 或 git clone + local build,把平台零件連同依賴打進 Worker binary 的部署方式。
- **用戶自製零件**user-submitted components)— Phase 5 後用戶上傳的自定義 WASM 零件(動態來源)。
## 關聯
### 內文知識關係
- 平台內建零件 >> 不從 >> R2
- R2 >> 保留用於 >> 用戶自製零件
- 平台內建零件 >> 透過 codeload 部署 >> 獨立 Worker
### 卡片關係
- [[R2用途-平台零件不從R2讀]] >> 架構前提來自 >> [[service-binding-vs-cypher-binding]]
@@ -0,0 +1,44 @@
---
tags: [recipe, 平台原則, 架構決策]
gloss: 多作者同 canonical recipe 用 UUID 並存,市場統計 per-uuid 區分版本,用戶 pull 時自動選最佳版本或按 author 指定。
---
# Recipe UUID 市場模型
← [[decisions/00-INDEX]]
**來源**`system-dev/wiki/decisions-summary.md`Recipe UUID 模型)、`docs/3-specs/arcrun/kbdb-base/` design.md §7.5
**最後更新**2026-06-27
## 摘要
Recipe 的身份由 canonical_id + uuid + author 三維度組成。同一 canonical 的多個版本用不同 uuid 存在 D1,市場統計(成功率)per-uuid 記錄,用戶 pull 時自動選市場最佳版本或明確按 author 指定。
## 重點
- **舊模型(已廢)**canonical_id 是唯一鑰匙,新版覆蓋舊版,多作者無法共存。
- **新模型(UUID**
- `canonical_id`:服務名標籤(gmail_send
- `uuid`:版本身份(Leo 的 gmail_send ≠ John 的 gmail_send
- `author`:誰寫的
- 真正的 KV/D1 鑰匙 = `recipe:{uuid}`
- **App Store 設計**
- `acr recipe pull gmail_send` → 後端搜 canonical_id=gmail_send 的所有 uuid → 按市場統計排序 → 回傳成功率最高的版本(自動選)
- `acr recipe pull gmail_send --author john` → 明確指定 John 的版本
- **市場統計 per-uuid**:每個 uuid 的 execution stat 分開記錄(success/fail/latency)→ 才能區分作者貢獻
- **向後相容**:舊 workflow 用 canonical_id → resolveRecipe 要能找到;migrate-uuid endpoint 把舊 key 轉新 uuid(冪等)
- **暴露警示**mindset §6):新 `submit-recipe` 需要用戶明示同意暴露(讓市場統計追蹤效果)
## 實體
- **Canonical ID**(规范ID)— 服務名標籤(gmail_send、slack_post),用於搜索,不是 KV 鑰匙。
- **UUID**(通用唯一標識符)— Recipe 版本身份,真正的 KV/D1 鑰匙 `recipe:{uuid}`
- **Author**(作者)— Recipe 的創作者身份,用於區分多作者同 canonical 的版本。
- **Market stat**(市場統計)— per-uuid 的成功率/失敗率/延遲,用於自動版本選擇。
- **App Store**(應用市場)— 公開 recipe 倉庫及版本選擇機制。
## 關聯
### 內文知識關係
- Canonical ID >> 標籤 >> UUID 群組
- UUID >> 記錄 >> Market stat
- Market stat >> 驅動 >> 自動版本選擇
- Author >> 並存於 >> 同 canonical 不同 UUID
- 舊 workflow >> 需要 >> 向後相容
### 卡片關係
- (相關 memory`recipe-trust-via-market-not-verification` — recipe 信任靠市場非人力防偽;屬 auto-memory 非 card
@@ -0,0 +1,43 @@
---
tags: [kbdb, 架構決策]
gloss: embedding 機制歸 KBDB base 的 optional 模組,有 Vectorize binding 才啟用;無則降級 LIKE keywordAPI 不變。
---
# Embedding 是 Base Optional 模組
← [[decisions/00-INDEX]]
**來源**`system-dev/wiki/decisions-summary.md`embedding 是 base optional 模組)、`docs/3-specs/arcrun/kbdb-base/` Phase 12
**最後更新**2026-06-27
## 摘要
語義查詢/embedding 不是獨立模組或圖插件,而是 KBDB base 層的 optional 擴展。Vectorize + AI binding 存在時啟用;缺少時降級 keyword search + capability hint(發現閉環,不假綠)。
## 重點
- **歸屬**embedding 機制屬 base 層(核心表 entries/templates/records),不是 graph 層(triplet 插件)、不是 ingest 層(爬蟲)。
- **啟用機制**
- 部署時檢查 KBDB binding 是否含 `[ai]` + `[[vectorize]]` → 有則啟用 semantic search
- 無則 API 降級為 keyword LIKE + `capability_hint`(告知使用者「語義搜尋未啟用」)
- **API 簽名不變**(無論啟用否,查詢端點同一個 `/search`),降級轉換在後端
- **部署陷阱**:部署注入 `[ai]`/`[[vectorize]]` binding 必須在 `stripOfficialOnlyBindings` **之後**(否則 [ai] 被清)。
- **精耕策略**(非地毯式):只 embed 標 `metadata_json.embed:true` 的 entrywiki 段落 + gloss),不對每個 block 灌。
- **無白名單**base 用通用 flag 判 embed:true,不寫死 entry_type 白名單(守解耦,base 對內容語意無知)。
- **向量存儲位置**:向量獨存 Vectorize,三表(entries/templates/records)不改動。
- **發現閉環**:無 embedding 時不假綠(回傳空結果假裝查到),而是明確 hint「語義未啟用」→ capability-driven 發現系統能自動 degrade。
## 實體
- **Base 層**base layer)— KBDB 核心表(entries/templates/records)及其基本操作。
- **Optional 模組**optional module)— 功能開/關不拆 repo,降級機制在 API 內部。
- **Vectorize**Cloudflare Vectorize)— 向量資料庫 binding,儲存 embedding。
- **Semantic search**(語義搜尋)— 基於 embedding 的向量相似度查詢。
- **Capability hint**(能力提示)— API 回傳中明確標示「此能力未啟用」,供 client 判斷。
- **Embed flag**embed:true)— 在 entry metadata_json 中設置,標示是否參與 embedding。
## 關聯
### 內文知識關係
- Vectorize binding >> 啟用 >> Semantic search
- 無 Vectorize >> 降級為 >> Keyword search
- Embed flag >> 控制 >> 哪些 entry 參與 embedding
- API >> 不變 >> 無論 embedding 啟用否
- Capability hint >> 發現 >> 語義搜尋未啟用
### 卡片關係
- [[embedding是base-optional模組]] >> 涉及部署 >> [[self-hosted部署-共享install加指紋跳過]]
@@ -0,0 +1,37 @@
---
tags: [cypher, 部署, 架構決策, 踩坑]
gloss: self-hosted cypher 和 component 同在 workers.dev zone 觸發 CF 1042,用 global_fetch_strictly_public flag 解,不是 Service Binding。
---
# Same-Zone 1042 用 flag 解不用 Binding
← [[decisions/00-INDEX]]
**來源**`system-dev/wiki/decisions-summary.md`(Cypher 怎麼調用零件避開 same-zone 1042)、`docs/incidents/2026-05-13-cypher-outbound-522.md`
**最後更新**2026-06-27
## 摘要
Cloudflare Workers 的 same-zone fetch 防護(HTTP 522)在 self-hosted 部署中會踩坑,因為 cypher 和 component 都在 `{sub}.workers.dev`。解法是在 cypher-executor 的 wrangler.toml 加 `global_fetch_strictly_public` flag,讓 same-zone fetch 走公網前門——而非用 Service Binding(那樣更死)。
## 重點
- **官方部署無此問題**cypher 在 `cypher.arcrun.dev`、component 在 `*.arcrun.dev``arcrun-*.workers.dev`,跨 zone,不踩防護。
- **Self-hosted 部署踩坑**:都在 `{sub}.workers.dev`,CF 防止同 zone 自循環死鎖,cypher fetch component 返 522。
- **為什麼不用 Service Binding**Binding 靜態、修改要 redeploycomponent 清單動態(用戶 workflow 決定),改變架構必要性低;flag 無副作用(official 加也沒影響,本就跨 zone)。
- **Flag 部署法**`cypher-executor/wrangler.toml` 加一行 `compatibility_flags = [ "nodejs_compat", "global_fetch_strictly_public" ]`,然後 cypher 和 auth worker 間的 HTTP fetch 都能通。
- **驗證**:若仍 522 → 檢查 flag 是否生效(某些 Wrangler 版本可能有 bug)。
## 實體
- **Same-zone 防護**Cloudflare same-zone protection)— CF Workers 內建的防護機制,禁止 Worker 在同一 zone 內 fetch 另一個 Worker 以防自循環。
- **HTTP 522**522 Connection Timed Out)— Cloudflare 自有錯誤,表示 fetch 因防護被擋。
- **global_fetch_strictly_public flag**Cloudflare compatibility flag)— 讓 same-zone fetch 從內部網路改走公網前門的相容旗標,無副作用。
- **cypher-executor**(Cypher 執行引擎)— 調度 workflow 並呼叫零件的 Worker,需加 flag 才能在 self-hosted fetch component。
- **動態 component 清單**dynamic component list)— workflow 執行時的零件集合,由用戶定義、會變動,無法靜態綁定。
## 關聯
### 內文知識關係
- same-zone 防護 >> 在 self-hosted >> 觸發 HTTP 522
- global_fetch_strictly_public flag >> 解決 >> HTTP 522
- global_fetch_strictly_public flag >> 不改變 >> 官方部署
- Service Binding >> 不能解決 >> 動態 component 清單
### 卡片關係
- [[same-zone-1042用flag解不用binding]] >> 迴避 >> [[service-binding-vs-cypher-binding]]
- [[same-zone-1042用flag解不用binding]] >> 涉及事件 >> [docs/incidents/2026-05-13-cypher-outbound-522.md]
@@ -0,0 +1,43 @@
---
tags: [部署, 架構決策]
gloss: 23 個 component worker 不各裝依賴,改成 root 共享一次 install + manifest 內容指紋跳過未變動——減 23× node_modules 膨脹。
---
# Self-Hosted 部署 — 共享 install 加指紋跳過
← [[decisions/00-INDEX]]
**來源**`system-dev/wiki/decisions-summary.md`self-hosted 部署)、`cli/src/lib/deploy.ts` §2.5
**最後更新**2026-06-27
## 摘要
tarball 解開後不每個 component worker 各裝 node_modules23× 重複 ~324MB),改成 root 共享一次 install;同時用 content hash manifest 記錄部署狀態,相同 hash 跳過已部署 worker,只部署內容變動者。
## 重點
- **問題**23 個 component worker 的 runtime dep 全是 honotier2 另需 zod/mcp-sdk/yaml),各裝 ~324MB node_modules = 23× 重複;且冪等重跑被誤解成「每次重做全部」,22 個沒變的白跑一遍。
- **共享 install 機制**
- tarball 根目錄裝一次 hono + wrangler + tier2 deps
- 各 worker 靠 Node.js 往上搜索路徑 resolve dependencies
- `wrangler deploy --dry-run` 驗證 tier1+tier2 都被 bundle(確保包完整)
- 結果:207MB×1 取代 324MB×23
- **Manifest 跳過**
- `~/.arcrun/deploy-manifest.json` 記錄 content hash + 上次成功時間戳
- 新解開 tarball → 算 content hash 與上次成功者比較 → 相同跳過、不同重部
- Hash 含 accountId(換帳號/KV namespace → 自動重部,不誤跳)
- `--force` flag 清空 manifest → 全部重新部署
- **失敗恢復**:共享 install 失敗 → 自動退回各 worker 自裝(不破壞既有路徑)
- **只記成功**:manifest 只追蹤成功部署者,失敗者下次必重試
## 實體
- **共享 install**shared installation)— root 目錄的單一 node_modules,所有 worker 共享。
- **Manifest**(部署清單)— `~/.arcrun/deploy-manifest.json`,記錄 content hash 和成功部署狀態。
- **Content hash**(內容雜湊)— 本次 tarball 內容的唯一指紋,用於比較是否變動。
- **Component worker**(零件 Worker)— `.component-builds/{name}/` 下各個獨立部署單位。
## 關聯
### 內文知識關係
- 共享 install >> 減少 >> 重複 node_modules
- Manifest >> 跳過 >> 未變動 component worker
- Content hash >> 相同時 >> 自動跳過部署
- accountId >> 不同時 >> 強制重新部署
### 卡片關係
- [[self-hosted部署-共享install加指紋跳過]] >> 優化 >> [[不依賴CI-執行鏈路vs零件投稿]]
@@ -0,0 +1,36 @@
---
tags: [零件架構, cypher, 架構決策]
gloss: 零件串接用 Cypher bindingYAML URL 清單)not Service Bindingwrangler.toml)——後者靜態、不適合動態 workflow。
---
# Service Binding vs Cypher Binding
← [[decisions/00-INDEX]]
**來源**`system-dev/wiki/decisions-summary.md`Service Binding vs Cypher Binding)、`.claude/rules/03-component-architecture.md §1`
**最後更新**2026-06-27
## 摘要
Cypher binding 是 YAML/KV 裡的 URL 清單(用戶定義、動態);Service Binding 是 wrangler.toml 的 `[[services]]`(平台內建、靜態)。workflow 層一律走 HTTP URL,禁止新增 Service Binding。
## 重點
- Cypher binding 用 HTTP 呼叫,workflow 是用戶定義的動態圖,不能要求每改 workflow 就 redeploy。
- Service Binding 靜態、修改 wrangler.toml 要重新部署——完全違反 workflow 靈活性,故只保留 13 個邏輯零件間的歷史遺產綁定、禁止擴展。
- 新零件部署流程:`registry/components/{name}/main.go` → 編譯 `.wasm` → 包進 `.component-builds/{name}/` WorkerHono + WASI shim)→ 獨立部署成 `arcrun-{name}.{sub}.workers.dev`(對內)+ `{name}.arcrun.dev`(對外)→ cypher 透過 HTTP fetch 呼叫。
- self-hosted 同 zone 1042 不是加 Service Binding 解——那樣更死。改用 flag 解(見卡「same-zone-1042用flag解不用binding」)。
## 實體
- **Cypher binding**workflow binding)— YAML/KV 裡存的 URL 清單,workflow 執行時動態查表呼叫零件。
- **Service Binding**Cloudflare service binding)— wrangler.toml 的 `[[services]]` 區塊,靜態綁定、修改須重新部署。
- **零件串接**component invocation)— workflow 在執行時呼叫零件的過程。
- **HTTP 呼叫**HTTP fetch)— 用 fetch() 透過公網 URL 呼叫零件,cypher 實踐零件串接的標準方式。
- **新零件**new component)— 新部署到 arcrun 系統的 WASM 零件(如新的第三方服務串接)。
## 關聯
### 內文知識關係
- Cypher binding >> 用 HTTP 呼叫 >> 零件串接
- Service Binding >> 靜態化 >> 零件串接
- 新零件 >> 禁止用 >> Service Binding
- Cypher binding >> 支撑 >> workflow
### 卡片關係
- [[service-binding-vs-cypher-binding]] >> 架構前提 >> [[R2用途-平台零件不從R2讀]]
- [[service-binding-vs-cypher-binding]] >> 相關事件 >> [[same-zone-1042用flag解不用binding]]
@@ -0,0 +1,36 @@
---
tags: [部署, 平台原則, 架構決策]
gloss: 執行鏈路(workflow/recipe/API 呼叫)不依賴 CI,走本地 script 部署。零件投稿(稀有)才走 PR/CI 讓人做閘門。
---
# 不依賴 CI — 執行鏈路 vs 零件投稿
← [[decisions/00-INDEX]]
**來源**`system-dev/wiki/decisions-summary.md`(不依賴 CI)、`.claude/rules/05-deploy-convention.md`
**最後更新**2026-06-27
## 摘要
用戶操作(workflow、recipe、API 呼叫)頻繁,走 CI 會有分鐘級延遲,不可接受。故執行鏈路用本地 script(`scripts/local-deploy.sh`)同步到 Hetzner。零件投稿稀有、走 PR/CI 把關,人 merge=天然閘門,CI 驗收 WASI 沙箱。
## 重點
- **執行鏈路**(高頻):用戶實時操作(`acr run``acr workflow push`、API call),不能等 CI 跑(分鐘延遲 unacceptable)→ 用本地 script 直接同步。
- **本地部署機制**`scripts/local-deploy.sh` 透過 `scp` / `rsync over ssh` 把更新推到 Hetzner `/opt/mira` 等目錄,直接、快速。
- **零件投稿**(稀有):幾周到幾月一次新零件 → 走 GitHub PR → 人 review + merge → CI 驗收(WASI 沙箱、Gherkin、假零件檢測)。
- **為什麼 CI 驗收零件**CI 容器支援 runtime 執行 wasm、檢驗行為;CF Worker 沒有 wasm runtimesandboxed 沒有 syscall),無法驗證。
- **人 merge 的價值**GitHub merge 需人類 approve,無法被 AI 偽造,天然的人類閘門。
- **不走 GitHub Actions 執行鏈路**:Actions 容易被濫用(當初 richblack 被 flag 的成因),且分鐘延遲。
## 實體
- **執行鏈路**execution path)— 用戶實時操作的頻繁路徑:workflow 建立、recipe 推送、API 呼叫。
- **本地部署**local deployment)— 透過 ssh/scp 直接從本機同步到伺服器,無 CI 介入。
- **零件投稿**component submission)— 新零件上傳的稀有操作,走 GitHub PR。
- **PR/CI 把關**PR/CI gatekeeping)— 零件投稿需人 merge 且 CI 驗收,雙層防護。
## 關聯
### 內文知識關係
- 執行鏈路 >> 高頻且實時 >> 不走 CI
- 本地部署 >> 支撑 >> 執行鏈路
- 零件投稿 >> 稀有且一次性 >> PR/CI 把關
- 人 merge >> 提供 >> 人類閘門
### 卡片關係
- [[不依賴CI-執行鏈路vs零件投稿]] >> 同源於避免被flag鐵律 >> [[self-hosted部署-共享install加指紋跳過]]
@@ -0,0 +1,35 @@
---
tags: [credential, 部署, 架構決策, 踩坑]
gloss: 多個 Workerauth primitive + cypher-executor)共享 ENCRYPTION_KEY,用 wrangler secret put 手動設進各 Worker secret store,不用 KV。
---
# 多 Worker ENCRYPTION_KEY 同步
← [[decisions/00-INDEX]]
**來源**`system-dev/wiki/decisions-summary.md`(多 Worker ENCRYPTION_KEY 同步)、`.claude/rules/01-tech-stack.md` 加解密規範
**最後更新**2026-06-27
## 摘要
Auth static_key / Auth service_account / cypher-executor 三個 Worker 都需 ENCRYPTION_KEY 來解密 credential。用各 Worker 的 secret store(非環境變數)存放;初期化時 `acr init` 生成一份 key,用 `wrangler secret put ENCRYPTION_KEY` 各設一次。
## 重點
- **Secret 存儲位置**:各 Worker 的 secret storeCloudflare 原生機制),不是環境變數、不是 KV。環境變數會洩漏到日誌;KV 的 list 操作可能外洩。
- **初始化流程**`acr init` 生成一份 32 字節隨機 key → 印出一次 → 用戶自己跑 `wrangler secret put ENCRYPTION_KEY --path <path>` 分別設進三個 Worker。
- **冪等性問題**:目前 `acr init` 多跑幾次會生成不同 key(不冪等)。長期應改成「init 檢查現有 config → 若存在 key 則重用舊的、否則生成新的」。
- **漂移陷阱**:若某個 Worker 的 key 遺漏或與其他 Worker 不同 → credential 解密失敗 → 表現為 401/403(用戶困惑,難debug)。
- **驗證方法**init 完成後做一個 test workflow 打一個認證過的 API(如 gmail),確認三個 Worker 的 key 一致。
## 實體
- **ENCRYPTION_KEY**(加密密鑰)— 32 字節 AES-GCM 密鑰,用來解密用戶的 credential。
- **Secret store**Cloudflare 密鑰存儲)— Worker 的原生機制,用 `wrangler secret put` 設置、runtime 讀取,內容不外洩。
- **密鑰漂移**key drift)— 多個 Worker 持有不同版本的 ENCRYPTION_KEY,導致某些 Worker 解密失敗。
- **冪等性**idempotency)— `acr init` 多次運行應產生同一結果(目前不達成)。
- **解密失敗**decryption failure)— Worker 因 key 不匹配無法解密 credential,表現為 401/403 錯誤。
## 關聯
### 內文知識關係
- ENCRYPTION_KEY >> 分散存儲於 >> Secret store
- 密鑰漂移 >> 導致 >> 解密失敗
- 冪等性 >> 缺乏時 >> 重跑 init 造成 key 不一致
### 卡片關係
- (相關 memory`encryption-key-drift-trap` — 解密失敗先比 key 指紋;屬 auto-memory 非 card
@@ -0,0 +1,33 @@
---
tags: [平台原則, 零件架構, 架構決策]
gloss: arcrun 的開發預設順序——串服務先寫工作流,只有「全生態必須重用」才把能力編譯成 WASM 零件。
---
# 工作流是 default 零件是例外
← [[decisions/00-INDEX]]
**來源**`system-dev/wiki/decisions-summary.md`(零件 vs 工作流)、`.claude/rules/06-mindset.md §1`
**最後更新**2026-06-27
## 摘要
新需求的預設解是寫工作流(YAML 串 HTTP);零件是稀有例外,只在「全 arcrun 生態必須重用」時才建。
## 重點
- AI 的典型走歪=把「需要一個能力」直接翻成「做一個零件」,把「能包」當成「該包」。
- 工作流構建成本低(YAML)、生命週期短(項目級);零件成本高(TinyGo/AS + WASI)、生命週期長(平台級)。
- 判準口訣:「他人會重複打這個服務嗎?」否→工作流;是且必要重用→才考慮零件。
- 反例:mira 的 `claude_api``km_writer``kbdb_upsert_block` 本是自用膠水,卻被錯做成零件。
## 實體
> 本卡內文關鍵實體(也是 graph node)。名+描述供下游 normalize。
- **工作流**workflowYAML 串接)— 用 HTTP + 流程控制 primitive 串服務的資料產物,arcrun 的預設開發單位。
- **零件**componentWASM component)— 編譯成 .wasm 的可重用能力單元,獨立部署成 Worker,稀有例外。
- **必要重用**(全生態重用)— 「全 arcrun 生態的人都會打同一服務」這個門檻,是建零件的唯一正當理由。
## 關聯
### 內文知識關係
- 零件 >> 對立於 >> 工作流
- 必要重用 >> 是建零件的前提 >> 零件
### 卡片關係
- [[工作流是default零件是例外]] >> 是其特例 >> [[自力救濟階梯-缺能力怎麼補]]
- [[工作流是default零件是例外]] >> 共享世界觀 >> [[薄殼原則-能力長在API]]
@@ -0,0 +1,44 @@
---
tags: [kbdb, 平台原則, 架構決策]
gloss: Mira 已蒸發,開的 KBDB 缺口當普世框架缺口處理;先查頂層 mira-dissolve 是否已重審覆寫,別照舊 issue 悶頭做。
---
# 碰舊 Mira 需求先查頂層覆寫
← [[decisions/00-INDEX]]
**來源**`system-dev/wiki/decisions-summary.md`(碰舊 Mira/SaaS KBDB 需求先查頂層覆寫)、`docs/4-guides/kbdb-capabilities.md`
**最後更新**2026-06-27
## 摘要
Mira 作為內部 AI 操盤手(LLM interface)已蒸發(重組為頂層決策單位),遺留的 KBDB 缺口不應照舊需求表悶頭做。應先檢查頂層 mira-dissolve 是否已重審、新的優先序、甚至否決某些需求。
## 重點
- **Mira 蒸發背景**:Mira 從實裝層升到頂層架構決策者(LLM Interface 角色),其原有 dogfood 缺口現交 arcrun 框架層補齊,但優先序要頂層重審。
- **已決定執行**
- **source 過濾** ✅ 做(`json_extract` 零建表,mistakes #18
- **documents 聚合** ❌ 不做(走 graph MCP,頂層 R6 否決)
- **DELETE proxy** ⏸ 擱置(依賴頂層 T8 認可;裸 delete 無 owner 檢查 = 跨租戶刪除風險)
- **embed-on-write** → 併入 embedding 卡片(#7
- **為什麼要先查**
- 頂層 mira-dissolve 文件可能已否決或重排某些需求
- 不檢查直接做 = 做了也可能被推翻、浪費時間
- 「普世框架缺口」vs「Mira 特例缺口」要區分
- **DELETE proxy 風險**:裸 `DELETE /entries/{id}` 無 owner 檢查 → 多租戶環境下可跨租戶刪除。需頂層 T8 任務明確認可 + owner 隔離機制才能做。
## 實體
- **Mira**(內部 AI 操盤手)— arcrun 的早期 dogfood 客戶,已升級為頂層架構決策單位。
- **Mira-dissolve**(Mira 蒸發重組)— 頂層決策文件,記錄 Mira 遺留需求的重審結果。
- **頂層覆寫**upstream override)— 頂層對某需求的最終決定(做/不做/改方向/等待)。
- **普世框架缺口**universal framework gap)— KBDB 對所有用戶都適用的功能缺陷。
- **Mira 特例缺口**Mira-specific gap)— 只對 Mira 的特殊工作流需要的功能。
## 關聯
### 內文知識關係
- Mira 蒸發 >> 遺留 >> 需求積壓
- 頂層覆寫 >> 決定 >> 普世框架缺口
- DELETE proxy >> 涉及 >> 跨租戶刪除風險
- source 過濾 >> 已批准 >> 執行
- documents 聚合 >> 已否決 >> 不執行
- DELETE proxy >> 擱置於 >> 頂層 T8 認可
### 卡片關係
- [[碰舊Mira需求先查頂層覆寫]] >> 涉及文件 >> [InkStoneCo/docs/3-specs/mira-dissolve]
@@ -0,0 +1,48 @@
---
tags: [平台原則, 架構決策]
gloss: 缺能力的補救階梯:自家 API 缺→補進 API;第三方 API 缺→workflow/code-node 補丁;純計算→code-node;才建零件。
---
# 自力救濟階梯 — 缺能力怎麼補
← [[decisions/00-INDEX]]
**來源**`system-dev/wiki/decisions-summary.md`(自力救濟階梯)、`.claude/rules/07-thin-shell.md §3.5`
**最後更新**2026-06-27
## 摘要
缺一個能力時的補救路徑分級。主問「那個 API 你能改嗎」:能改→補 API;改不了(第三方)→走 workflow/code-node 補丁;不入任何 API 的純計算→code-node;真需新穩定能力(極少)→零件 PR。
## 重點
- **階梯概覽**
| 情況 | 正解 | 為何 |
|---|---|---|
| 能打既有 API | **recipe**(無則建) | 單一 API 呼叫的封裝 |
| 自家 API 缺能力 | **補進 API** + 可同時發 issue | 你能改,能力該長在 API |
| 第三方 API 缺能力 | **可投稿的 workflow 補丁** | 改不了第三方,用資料方式自救 |
| 純計算(如文本轉大寫) | **code-node**(空白零件寫 JS) | 不該為此建一堆專用零件 |
| 真需新穩定能力(極少) | 自建零件 → PR | 維持零件庫最小 |
- **第三方 API 缺能力的實例**Google Sheets 一次只能倒全部、輸出前無法 filter;原廠不開此 API → arcrun 補不進去 → 正解是投稿一個 workflow 補丁「倒出後自己篩」。
- **Upsert 範例**(區分自家/第三方):
- Stripe 原廠提供 upsert API → recipe 直打
- Notion 沒 upsert API → 做一個 upsert workflow(先 GET 找、有則 PATCH 無則 POST),不是建零件
- **Hook 無法防守的**:§3.1 禁的是「介面層 TS 拼裝」(hook 7.x 擋),不是禁「資料方式補丁」。workflow/code-node 是 YAML/空白零件內的 JS,非介面層 TS → hook 範圍外、合法不被擋。
- **補丁轉正**:原廠出 API 後,補丁 workflow 因效能較差自然淘汰;不需特別移除。
## 實體
- **自家 API**arcrun-owned API)— arcrun 官方開發的 API(如 cypher-executor 端點、KBDB 基本盤),改動權掌握在 arcrun。
- **第三方 API**third-party API)— 外部服務提供的 APIGoogle Sheets、Stripe、Notion),改不了。
- **Workflow 補丁**workflow patch)— 用戶投稿的 workflow YAML,用來補第三方 API 缺的能力,可被多人重用。
- **Code-node**(代碼節點)— 空白 zero-logic 零件內直接寫 JavaScript,用於純計算。
- **零件庫污染**component bloat)— 為了單一功能、改不了的第三方 API 新建零件,長期難維護。
## 關聯
### 內文知識關係
- 自家 API 缺 >> 正解是 >> 補進 API
- 第三方 API 缺 >> 無法補 API >> 走 workflow 補丁
- 純計算 >> 應該 >> code-node
- 零件庫污染 >> 來自 >> 為第三方缺口建零件
- 補丁 workflow >> 因效能差 >> 自然淘汰
### 卡片關係
- [[自力救濟階梯-缺能力怎麼補]] >> 補充 >> [[工作流是default零件是例外]]
- [[自力救濟階梯-缺能力怎麼補]] >> 關聯 >> [[薄殼原則-能力長在API]]
@@ -0,0 +1,36 @@
---
tags: [薄殼, 平台原則, 架構決策]
gloss: 所有業務能力只實作一次放在 APIcypher-executor),CLI/MCP/SDK 全是只做介面轉換的薄殼。
---
# 薄殼原則 — 能力長在 API
← [[decisions/00-INDEX]]
**來源**`.claude/rules/07-thin-shell.md``system-dev/wiki/decisions-summary.md`(薄殼原則)
**最後更新**2026-06-27
## 摘要
能力只實作一次放在 APICLI / MCP / Python lib / JS lib 全是薄殼,只做參數解析 + HTTP 呼叫 + 格式轉換 + client 端加密,零業務邏輯。
## 重點
- 判準口訣:「這段邏輯換一個介面(CLI→MCP)要不要重寫?」要→它是能力,該在 API;不用→它是薄殼該做的。
- 違反例:CLI 迴圈 POST 多 recipeseed 該是 API 行為)、MCP 先 update 失敗再 insertupsert 該在 API)、SDK 自製 credential-injector(該在 WASM)。
- 齊的單位是「**能力**」不是「**端點**」:MCP=CLI 是出貨目標、不是任一時刻的不變量;底層 proxy 可有端點刻意不上 CLI/MCP(如 `/kbdb/entries` 裸 CRUD 消費者是 mira Python client)。
- 帳號統一:所有薄殼讀同一份身份來源(config.yaml / env),不可 CLI 連自架、MCP 連官方。
- hook 7.x 擋語法層可偵測的拼裝;藏在 helper 裡的邏輯擋不了,靠 code review。
## 實體
- **薄殼**thin shell)— CLI/MCP/SDK 介面層,只暴露能力不實作能力。
- **能力**business logic/業務邏輯)— 換介面要重寫的那段邏輯,必須下沉到 API 只實作一次。
- **API**cypher-executor HTTP 端點)— 能力的唯一真相源、所有薄殼共同呼叫的後端。
- **端點**endpoint)— API 的單一 HTTP 路由;齊的單位是能力不是端點,端點可刻意不上某介面。
## 關聯
### 內文知識關係
- 薄殼 >> 呼叫 >> API
- 能力 >> 下沉到 >> API
- 能力 >> 齊的單位是 >> 能力
- 端點 >> 不等於齊的單位 >> 能力
### 卡片關係
- [[薄殼原則-能力長在API]] >> 共享世界觀 >> [[工作流是default零件是例外]]
- [[薄殼原則-能力長在API]] >> 歷史成因見 >> [[薄殼規則晚於實作-MCP漂移是歷史債]]
@@ -0,0 +1,37 @@
---
tags: [薄殼, 平台原則, 踩坑, 案例經驗]
gloss: MCP 與 CLI 不一致的根因不是開發先後,而是薄殼紀律(rule 07)2026-06-07 才從壓測釐清,補立之前兩介面各自接後端而漂移。
---
# 薄殼規則晚於實作 — MCP 漂移是歷史債
← [[decisions/00-INDEX]]
**來源**`system-dev/docs/5-records/test-reports/壓測-recipe-library-2026-06-07.md` §5、`.claude/rules/07-thin-shell.md` 檔頭、decisions-summary(薄殼原則 2026-06-15 釐清)
**最後更新**2026-06-27
## 摘要
MCP 和 CLI 不一致的根因不是「MCP 更早開發所以舊」,而是**薄殼原則本身是 2026-06-07 壓測時才釐清的**——規則補立之前,兩個介面各自接後端、自然漂移,留下歷史債。
## 重點
- **規則晚於實作**:rule 07 檔頭自承來源是「壓測報告 §5.4/§5.5,設計者本人於壓測中釐清」。薄殼不是開局戒律,是撞牆後回頭講清楚的產物。
- **病徵(壓測 §5.1 原記)**:CLI 改了讀全域/專案/.env 身份,**MCP 沒跟上**,兩者打不同帳號。能力在兩介面各寫各的,沒有「先有 API、介面只暴露」的紀律約束。
- **化石證據**MCP `u6u_deploy_workflow``/workflows/deploy`(從不存在的端點 → 404),CLI `acr push``/webhooks/named`(活的)。兩條部署路徑底層根本不同 = 規則沒到位前各長各的(2026-06-27 issue #8 實作期挖出)。
- **同根的其他漂移**self-hosted MCP 連官方帳號([[薄殼原則-能力長在API]] §4、mistakes §5);YAML→graph 編排被寫在 CLI push.ts 介面層、MCP 期待 server 端點 → 兩邊都想自己編排。
- **校正常見誤解**:不是「先後」造成不一致,是「**紀律後補、補前漂移**」。修法是回頭把能力下沉 API(rule 07),不是怪某介面舊。
- **查證限制**`.claude/`/`docs/`/`system-dev/` 全 gitignored,git 無這些檔歷史 → 「精確定立時間」查不到,只能靠檔內日期註記(2026-06-07 壓測、2026-06-15 「MCP=CLI 是出貨目標非不變量」釐清)。
## 實體
- **薄殼原則**(rule 07/薄殼鐵律)— 能力只實作一次放 API、介面只暴露的紀律,2026-06-07 從壓測釐清。
- **規則晚於實作**(紀律後補)— 原則在介面已各自長成後才補立,是漂移的根因。
- **MCP 漂移**(CLI/MCP 不一致)— 兩介面各自接後端、打不同帳號/端點的歷史債。
- **死端點 404**/workflows/deploy 不存在)— MCP 打一個從未存在的部署端點,是漂移的具體化石。
## 關聯
### 內文知識關係
- 規則晚於實作 >> 導致 >> MCP 漂移
- MCP 漂移 >> 具體表現為 >> 死端點 404
- 薄殼原則 >> 補立晚於 >> MCP 漂移
- 薄殼原則 >> 修正 >> MCP 漂移
### 卡片關係
- [[薄殼規則晚於實作-MCP漂移是歷史債]] >> 補充歷史成因於 >> [[薄殼原則-能力長在API]]
- (相關 memory`mcp-self-hosted-bug-fixed``thin-shell-capability-in-api` — MCP 後端接線與薄殼鐵律)
+368
View File
@@ -0,0 +1,368 @@
---
name: decisions-summary
description: 架構決策快速查 — 做選擇時的關鍵 trade-off(不是全文,是導引 + 連結)
metadata:
type: reference
last_updated: 2026-06-26
---
# 架構決策快速查
> **用途**:遇到「X 該怎麼設計?」時,快速找到已有決策的 trade-off。
> **來源**DECISIONS.md(權威記錄)+ rule 02-07(執行細節)。
---
## 零件 vs 工作流(mindset §1
**Q:新需求該包零件還是寫工作流?**
**決策**:零件是稀有例外,工作流是 default。
| | 工作流 | 零件 |
|---|--------|------|
| **何時用** | 自用 / 少數人 / 試驗 | 全 arcrun 生態必須重用 |
| **構建成本** | 低(YAML | 高(TinyGo/AS + WASI |
| **生命周期** | 短(項目級) | 長(平台級) |
| **例子** | RSS→Sheet、郵件分類、GitHub issue bot | gmail、telegram、http_request |
**避坑**:「有 API 可包」≠「該包」。mira 錯做成零件的 claude_api、km_writer、kbdb_upsert_block 本該是工作流。
**詳見**DECISIONS.md §1、mindset §1
---
## Service Binding vs Cypher Bindingrule 03 §1
**Q:零件之間怎麼串接?**
**決策**
- **Cypher binding = YAML 裡的 URL 清單**(用戶 workflow 定義)→ HTTP 呼叫
- **Service Binding = wrangler.toml 的 `[[services]]`**(平台內建邏輯零件之間效能優化)→ 禁止新增
**Why**
- workflow 是用戶定義的動態圖,不能要求重新部署
- Service binding 靜態,修改 toml 要 redeploy → 違反 workflow 靈活性
- 13 個現有的 SVC_* binding(邏輯零件)是歷史遺產,保留但不擴展
**新零件怎麼部署**
1. 建 `registry/components/{name}/main.go`
2. 編譯 `.wasm`
3. 包進 `.component-builds/{name}/` WorkerHono + WASI shim
4. 部署成獨立 Worker`arcrun-{name}.{sub}.workers.dev`(對內) + `{name}.arcrun.dev`(對外)
5. cypher 透過 HTTP fetch 呼叫
**避坑**
- 不是「用 service binding 快」就用,那違反架構
- self-hosted 同 zone 1042 不是加 binding 解,是加 flag(§ same-zone-1042
**詳見**rule 03 §1-3、2026-05-13-cypher-outbound-522.md
---
## R2WASM_BUCKET)的用途(rule 03 §2
**Q:WASM 零件怎麼存儲和部署?**
**決策**
- **平台內建零件**bundle 進各自 Worker 的 binary`.wasm` commit 進 repo)→ 部署時用 codeload tarball
- **用戶自製零件**(Phase 5 以後):動態上傳到 R2runtime 執行
**現在狀態**
- 平台零件不從 R2 讀
- R2 只在 Phase 5 啟用
**避坑**
- 不要問「怎麼從 R2 取 WASM」(錯誤假設)
- 平台零件的 WASM 已 deploy 進 Worker,沒有 R2
- 用 HTTP URL 呼叫,不是動態讀 R2
**詳見**rule 03 §2、rule 05 WASM 來源
---
## Cypher 怎麼調用零件避開 same-zone 10422026-06-06
**Qcypher 打 component worker 返回 522**
**決策**
- **官方**cypher 在 cypher.arcrun.dev、component 在 {name}.arcrun.dev)→ 跨 zone,不踩
- **Self-hosted**(都在 {sub}.workers.dev)→ 踩 same-zone 防護
**解法**(不是 service binding):
```toml
# cypher-executor/wrangler.toml
compatibility_flags = [ "nodejs_compat", "global_fetch_strictly_public" ]
```
這讓 same-zone fetch 走公網前門 → 同 zone 也通。
**為什麼不用 binding**
- binding 靜態(toml 改要 redeploy
- component 清單動態(用戶 workflow 決定)
- 改變 architecture 必要性低(flag 無副作用)
**避坑**
- self-hosted cypher 務必有 `global_fetch_strictly_public`
- 若仍 522 → 檢查 flag 是否生效(某些 Wrangler 版本可能有 bug
**詳見**2026-05-13-cypher-outbound-522.md、2026-06-06 commit、rule 03 §3
---
## 多 Worker ENCRYPTION_KEY 同步(2026-05-29
**Qauth_static_key / auth_service_account / cypher-executor 都需 ENCRYPTION_KEY,怎麼保持一致?**
**決策**
- secret 存在各 Worker 的 secret store(非環境變數,避免洩漏)
- `wrangler secret put ENCRYPTION_KEY` 手動設進各 Worker
- 初始化:`acr init` 生成,展示一次,user 自己 secret put
**為什麼不用 KV**
- secret 是敏感內容,不應在 KV 存(會被 list 洩漏)
- secret store 是 Cloudflare 的原生機制
**冪等性**
- `acr init` 多跑幾次,生成不同 key(目前不冪等)
- 若要冪等,init 應檢查現有 config → reuse 舊 key
**避坑**
- init 完成後驗證所有三個 Worker 都有同一份 key
- 若某個 Worker 的 key 遺漏或不同 → credential 解密失敗(會表現為 401/403)
- 重跑 init 不要覆蓋舊 secret(目前沒有 checkneed improvement
**詳見**2026-05-29-encryption-key-drift.md、rule 01 加解密
---
## Recipe UUID 模型(kbdb-base §7.5
**Q:多作者同 canonical recipe 怎麼並存?**
**決策**
- **舊模型**(已廢):canonical_id 是唯一鑰匙,新版覆蓋舊版
- **新模型**UUID):recipe:{uuid} 是真正鑰匙,同 canonical 多 uuid 並存
**app-store 設計**
- `canonical_id`:服務名(gmail_send
- `uuid`:版本身份(Leo 的 gmail_send ≠ John 的 gmail_send
- `author`:誰做的
- `market_stat`:per-uuid 的成功/失敗率(區分作者)
**pull 邏輯**
- 公庫搜找 canonical → 回多 uuid
- `acr recipe pull gmail_send` → 自動選市場最佳版本(成功率高)
- `acr recipe pull gmail_send --author john` → 明確指定
**向後相容**
- 舊 workflow 用 canonical_id → resolveRecipe 要能找到
- migrate-uuid 端點把舊 key 轉新 uuid(冪等)
**避坑**
- 不是簡單的 key 重構,是多維度身份(canonical + uuid + author
- D1 per-uuid 記錄,不是 per-canonical(區分市場數據)
- 新 submit-p 要幌露警示(mindset §6
**詳見**kbdb-base design §7.5、credential-primitives-wasm
---
## 薄殼原則(rule 07
**QCLI / MCP / SDK 應該怎麼設計?**
**決策**
- **所有能力只實作一次,放在 APIcypher-executor**
- **介面層全是薄殼**:參數解析 + HTTP 呼叫 + 格式轉換,零邏輯
**違反例**
- CLI 迴圈 POST 多個 recipeseed 邏輯不該在介面)
- MCP 先 update 失敗再 insertupsert 邏輯應在 API
- SDK 自製 credential-injector(應在 WASM primitive
**同一 API 在不同介面的簽名**:
- ✅ 差異來自介面慣例(CLI 讀檔案、MCP 讀 JSON
- ❌ 底層實作分歧(CLI 連自架、MCP 連官方 = 差不同帳號)
**帳號統一**
- CLI / MCP / SDK 必須讀同一份身份設定(config.yaml / env
- self-hosted 的已知問題:MCP 可能指官方(mcp-account-source §5.2
**避坑**
- 介面間差異一大就改,不是「某個介面功能不全」就在那加邏輯
- seed / upsert / 複雜編排 → 都去補 API,不是繞過
### MCP「等於」CLI 嗎?——齊的單位是「能力」不是「端點」(2026-06-15 釐清)
**Q:MCP 和 CLI 該不該完全一致?API 有的端點是不是都要兩邊各開一個?**
**結論**
- rule 07 §5 的「CLI + MCP 覆蓋同一組 API 能力」**指該暴露給人/AI 的能力**——不是「API 的每個端點都要上兩邊」。
- 「介面進度暫時不一致」**不是 bug**(§5 明寫);bug 是「底層能力不齊 / 介面含不該含的邏輯 / 帳號來源不統一」這三者。所以 MCP=CLI 是**出貨目標**,不是**任一時刻的不變量**。
- **底層 API/proxy 可以有端點刻意不上 CLI/MCP**——那是分層,不是疏漏。判準看「這端點的消費者是誰」。
**實例(KBDB 資料層)**
- 該齊的齊了:MCP 6 個 `kbdb_*` 工具 ↔ CLI `acr kbdb` 6 個子命令,一一對應(template/record/query/search)。差異只來自介面慣例(MCP 吃 JSON、CLI 吃 `--slots a,b,c`),底層同一條 cypher `/kbdb/*` proxy。
- **`/kbdb/entries` 裸 CRUD 刻意兩邊都不接**:它的消費者是 **mira 的 Python client**(直連 HTTP 遷移用),不是人/AI。CLI/MCP 給的抽象是更高階的 template/recordKBDB 鐵律:不暴露裸表操作)。硬把 entries CRUD 塞進 CLI/MCP **反而違反鐵律**
**附帶釐清(驗收邊界)**:MCP 工具經 `env.KBDB` service binding 那一跳是 worker 間內部專線,**curl 從外面驗不到**(無公開 URL)。要驗它得在真 MCP client 裡 call;或靠「同 `kbdbFetch` 路徑的既有工具已驗過」佐證 binding 活。
**詳見**:rule 07(完整)、壓測報告 §5.1 seed 反例、kbdb-base/tasks.md 9.1/9.2/9.6
---
## Haiku 就能搞定是設計目標(mindset §1 + 壓測)
**Q:為什麼要用 Haiku 壓測,不直接用 Sonnet**
**決策**
- arcrun 價值主張:比直接開發容易 → 省 token + 省時間
- 若只有 Sonnet 能驅動,「省」就不成立(Sonnet 貴)
- **Haiku 就能搞定才是設計達成度的證明**
**測試邏輯**
- Haiku 過 ✅ → 設計目標達成
- Haiku 不過 → 判別:是介面缺陷還是模型本質限制
- 升 SonnetSonnet 過 → 介面太難,要改介面
- Sonnet 也不過 → 功能 bug,要改邏輯
**避坑**
- 不要「Haiku 過不了就換 Sonnet」,那是放棄設計目標
- Haiku 撞牆 = 發現「使用者自己搞不定」的點 → 要磨介面白痴化
**詳見**test_case.md 第一段、mindset §7 誠實限制
---
## 不依賴 CI(執行鏈路 vs 零件投稿)
**Q:為什麼部署走 local script,不是 GitHub Actions**
**決策**
- **執行鏈路**(高頻:workflow、recipe、API)→ 不依賴 CI(用戶實時操作)
- **零件投稿**(稀有:新零件)→ 走 PR/CI(人 merge = 閘門,CI 驗收)
**執行鏈路不能走 CI**
- `acr run` 用戶實時呼叫,不能等 CI 跑(分鐘級延遲不可接受)
- workflow 是 YAML,不是程式碼編譯
**零件投稿走 CI**
- 零件投稿稀有(幾周一次)
- PR check 能 runtime 跑 wasmCI 容器支援,CF Worker 不支援)
- 把關(WASI 沙箱驗證、Gherkin 全綠、假零件檢測)由 CI 跑
**避坑**
- 執行鏈路不要加 CI gate(會卡用戶)
- 部署走 local script,不是 workflow dispatch
- 零件投稿不要 self-service(要人 review
**詳見**rule 05 部署慣例、mindset §4
---
## self-hosted 部署:共享 install + 內容指紋跳過(2026-06-12
**結論**`acr update` 不每個 worker 各裝依賴、不每次全部重部。改成「root 共享一次 install」+
「內容指紋 manifest 跳過未變動」。
**原因**23 個 component worker 的 runtime dep 全是 honotier2 另需 zod/mcp-sdk/yaml),各裝
~324MB node_modules 是 23× 重複;且冪等重跑被誤解成「每次重做全部」,22 個沒變的白跑。
**機制**
- **共享 install**tarball root 裝一次(hono+wrangler+tier2 deps),各 worker 靠 node 往上 resolve。
207MB×1 取代 324MB×23`wrangler deploy --dry-run` 驗證 tier1+tier2 都 bundle 成功)。
- **manifest**`~/.arcrun/deploy-manifest.json`):注入後算 content hash,與上次成功比 → 相同跳過。
只記成功者(失敗下次必重試);hash 含 accountId(換帳號/KV 自動重部,不誤跳);`--force` 清空全重部。
- 共享失敗自動退回各 worker 自裝(不破壞既有路徑)。
**詳見**cli/src/lib/deploy.tsdownloadAndDeploy §2.5 共享、deploy 迴圈 manifest)、mistakes §13
---
## 自力救濟階梯——缺能力怎麼補(2026-06-26issue #4
**Q:缺一個能力,該補 API 還是別的?**
**主界線一句話**:**「那個 API 你能不能改?」**
- 能打既有 API → recipe
- **自家** API 缺 → 補進 API
- **第三方** API 缺(gsheets filter / 無 upsert)→ **可投稿的 workflow 補丁**(不是建零件!)
- 純計算 → **code-node**(空白 code 零件寫 JS
- 真需新穩定能力(極少)→ 零件 PR
**避坑**:§3.1 禁的是「介面層 TS 拼裝 + 亂建零件污染零件庫」,**不是禁任何補丁**。第三方 API 改不了,
不能被「只能 recipe」卡死。hook 7.x 只擋介面層 TSworkflow/code-node 是資料產物、不被擋。
code-node 規則已定、零件實作屬 wishlist C1 另案。**詳見**DECISIONS §9、07-thin-shell §3.5。
---
## embedding 是 base optional 模組(2026-06-26issue #7
**Q:語義查詢/embedding 放哪、怎麼開?**
**決策**embedding 歸 **base 的 optional 模組**(非 graph/ingest)。binding 開/關不拆 repo。
- 有 `VECTORIZE`+`AI` binding 才啟用;沒有 → 降級 LIKE keyword**API 不變**。
- 預設關(free-tier 友善);`acr init` 問、`kbdb_embed:true` + `acr update` 開(deploy 建 Vectorize index)。
- **精耕非地毯式**:只 embed 標 `metadata_json.embed:true` 的 entrywiki 段落+gloss),不對每個 block 灌。
- **base 用通用 flag 不寫死 entry_type 白名單**(守解耦,base 對內容語意無知)。
- **不動三表**(向量存 Vectorize);`?mode=semantic` 沒開 → 降級 keyword + `capability_hint`(發現閉環,不假綠)。
模型 `@cf/baai/bge-base-en-v1.5`768/cosine)。**避坑**deploy 注入 `[ai]`/`[[vectorize]]` binding 必須在
`stripOfficialOnlyBindings` **之後**(否則 [ai] 被清,見 mistakes #17)。**詳見**DECISIONS §10、kbdb-base SDD Phase 12。
---
## 碰舊 Mira/SaaS KBDB 需求先查頂層覆寫(2026-06-26issue #5
**Qmira dogfood 開的 KBDB 缺口還要做嗎?**
**決策**:Mira 已蒸發 → 當**普世框架缺口**處理,且**先查頂層 mira-dissolve 是否已重審覆寫**(別照舊 issue 悶頭做)。
- source 過濾 ✅做(json_extract 零建表,mistakes #18
- documents 聚合 ❌不做(走 graph MCP,頂層 R6 否決)
- DELETE proxy ⏸擱置(依賴頂層 T8;裸 delete 無 owner 檢查=跨租戶刪除風險)
- embed-on-write → 併 #7
**詳見**DECISIONS §11、`docs/4-guides/kbdb-capabilities.md`
---
## 快速決策樹
```
新功能怎麼做?
├─ 自用 / 少數人?
│ └─ 工作流(YAML
├─ 全生態必須重用?
│ ├─ 已有服務提供串接?
│ │ └─ HTTP 調用 + recipe
│ └─ 要深度集成?
│ └─ 零件(TinyGo
新零件怎麼部署?
├─ 平台內建?
│ └─ .component-builds/{name}/ Worker + HTTP URL
├─ 用戶自製?
│ └─ Phase 5(R2 動態讀)
多人協作怎麼同步?
├─ 資料(KV/D1)?
│ └─ 各自 Worker 綁定,不共用 secret
├─ 程式碼(執行邏輯)?
│ └─ 只在 APIcypher-executor),介面全薄殼
├─ 身份設定?
│ └─ 同一個 config(所有介面讀同一份)
```
---
## 進階參考
| 決策域 | 文件 | 優先閱讀 |
|--------|------|---------|
| 零件 | rule 03、rule 05 | 要建新零件時 |
| Recipe UUID | kbdb-base §7.5 design.md | 改 recipe 鑰匙時 |
| Credential | rule 01 、credential-primitives-wasm | 加新 auth primitive 時 |
| 部署 | rule 05、deploy.ts | 新增 Worker 時 |
| 介面設計 | rule 07、sd-and-website/design.md | 改 CLI/MCP/SDK 時 |
| 事件 | docs/incidents/ | 遇到類似問題時 |
+402
View File
@@ -0,0 +1,402 @@
---
name: cc-mistakes-and-lessons
description: CC 常犯的錯誤模式 + 避坑方法(來自 mindset + incidents
metadata:
type: reference
last_updated: 2026-06-26
---
# CC 常犯的錯誤 + 避坑方法
> **快速參考**:遇到類似情況時,對號入座避免重蹈覆轍。
> 來源:mindset §1-7 + docs/incidents/ 事件復盤。
---
## 1. 「能包成零件」≠「該包成零件」(mindset §1)
**錯誤模式**:看到「某服務有 API」就想「做個零件包起來」,跳過了「有必要嗎?」。
**後果**
- mira 的 `claude_api``km_writer``kbdb_upsert_block` 錯做成零件(其實是自用服務膠水)
- 浪費零件實現成本,降低代碼可維護性
**避坑**
1. **預設寫工作流**:自用 / 少數人用 / 試驗 → 全部工作流優先
2. **只在「必要重用」時建零件**:要讓全 arcrun 生態的人都能用 → 才值得零件化
3. **問「他人會重複打這個服務嗎?」**:否 → 工作流;是 → 才考慮零件
---
## 2. 工作流裡放 LLM 節點(mindset §2
**錯誤模式**:在工作流(執行引擎)內嵌 AI 推理,讓工作流依賴 LLM 判斷。
**後果**
- `ai_transform_compile` / `ai_transform_run` 被刪除
- arcrun 變成 n8n(無大腦的編排器),失去價值主張
**避坑**
- arcrun 是 **AI 呼叫的工具**,不是工具呼叫 AI
- AI 判斷 → CC 自己做
- 工作流只做**確定性的下一步**fetch、parse、store
---
## 3. 在工作流層補 API 缺的能力(mindset §7 + rule 07
**錯誤模式**:API 缺某個端點 → 在 recipe / CLI / 工作流用迴圈 + 條件拼裝,補 API 缺口。
**後果**
- kbdb-base §4.1seed 邏輯被寫進 CLI `init.ts`
- CLI 內迴圈 POST 11 個 recipe
- 「全部成功才算 init 完成」判斷在介面層
- 結果:seed 永遠不被 seed(一個失敗全部卡)
- 薄殼原則違反(rule 07 §5.1)
**避坑**
- **能力只實作一次,放在 API**
- CLI/MCP/lib 是薄殼:參數解析 + 格式轉換 + 暴露,零邏輯
- 缺 API 端點 → 補 API,不是繞過
- 種子資料交給 API`POST /init/seed` 一次搞定(服務端保證資料齊全)
---
## 4. 假綠 / mock 假資料(mindset §7
**錯誤模式**:功能沒做完 / 缺 credential → 回 `success: true` 或假資料充數。
**後果**
- 壓測階段 §4.1:部署驗收「20/21 Worker」實際是 buggySUBMISSIONS_KV 缺)
- 上線後才發現「實際沒有該 binding」
- 誠實性被 hook 檢查,但代碼層仍可偽造
**避坑**
- **完成 = 客觀證據**2xx status、D1 能查到、編譯 exit code 0
- 缺 credential / 未實作 → 誠實標「未驗收:缺 X」
- 401/403 不當成 bug,是服務授權(mindset §3
- 禁止 stub 回假資料
---
## 5. MCP 帳號跑到平台(self-hosted bug 2026-06-08
**錯誤模式**self-hosted 用戶的 MCP `.mcp.json` 指向官方 `mcp.arcrun.dev` 而非自己的。
**根因**
- init 沒寫 `config.mcp_url` → 沒更新 `.mcp.json`
- MCP /mcp 路由寫成 `app.post("/mcp")` 而非 `app.post("/")`basePath 已是 /mcp
**後果**
- Haiku 用 MCP 連到官方帳號,CLI 連自己的 → 私庫看不到
- 壓測 Cold.7 MCP 安裝卡住
**避坑**
- **self-hosted init 的 config.mcp_url = 自己的 arcrun-mcp.{sub}.workers.dev/mcp**
- 驗證:`curl -X POST https://arcrun-mcp.{sub}.workers.dev/mcp` 應 200(授權層外)
- basePath 和 route 的組合:`basePath("/mcp")` + `app.post("/")` = 路由 `/mcp`
---
## 6. 多 Worker 共用 ENCRYPTION_KEY 漂移(encryption-key-drift
**錯誤模式**:同一個 key 在多個 Worker 的 secret store 中不一致。
**根因**
- `acr init --self-hosted` 不是幪等的
- 用戶重跑 init,key 重新生成或未同步到所有 Worker
- 某個 Worker 還用舊 key,解密失敗
**後果**
- credential 無法解密,workflow 執行失敗(401/403
- 調試難度高,表現為「缺 credential」
**避坑**
- init 完成後,驗證所有 secret_target_workersauth_static_key / auth_service_account / cypher-executor)都有同一份 key
- 若已部署過,重跑 init 時 **skip secret put**(不要覆蓋)
- 或提供「檢查 key 一致性」的端點(未實作)
---
## 7. 同 zone 1042self-hosted cypher 打 auth worker
**錯誤模式**cypher 和 auth worker 同 zone(都是 {sub}.workers.dev),cypher fetch 打 auth 返回 522。
**原因**Cloudflare 「same-zone fetch 防環路」的保護機制。
**歷史繞路**:加 Service Binding(不規範,且靜態)。
**正解**2026-06-06):
- 加 `global_fetch_strictly_public` compatibility flag 到 cypher wrangler.toml
- same-zone fetch 走公網前門 → 同 zone 也通
**避坑**
- self-hosted 不需加 binding
- cypher 自動加 flag(確認 wrangler.toml 有)
- 若仍 522 → 檢查 flag 有沒有生效
---
## 8. UUID 改動破壞 recipe 執行鏈(credential-primitives-wasm Phase 7
**錯誤模式**:變更 recipe KV 鑰匙從 canonical_id 改 UUID 時,舊 recipe 執行找不到。
**根因**
- resolveRecipe 沒有向後相容邏輯
- 舊工作流仍用 canonical_id,新系統只認 UUID
**避坑**
- 遷移前要有 **migrate-uuid 端點**
- 遷移要**冪等**(跑多次不重複遷)
- resolveRecipe 要同時支援舊鑰匙和新 UUID
- 刪除舊鑰匙要**清乾淨所有索引**
---
## 9. 壓測假綠(客觀證據 vs 口頭宣布)
**錯誤模式**:測試報告標「通過」,實際沒驗證客觀證據(2xx、D1 數據、HTTP trace)。
**後果**
- 壓測階段 §2.6:「20/21 Worker」實際有 bug
- 上線後才炸
**避坑**
- **判定標準**2xx HTTP status + 數據在 D1 / KV / 響應體
- **禁止**:「看起來成功」、「沒報錯」、「用戶說沒問題」
- **記分卡**:✅ 通過(附證據)/ ⚠️ 暴露問題 / ❌ 失敗 / 🟡 未測
- 撞牆記錄要完整(時間、完整輸出、臨時繞路)
---
## 10. 沒讀 SDD 就動代碼(protocol §0
**錯誤模式**:看到「要改 X」就直接改,沒讀 SDD 的設計背景和邊界。
**後果**
- 改出違反架構的東西(如 TS 零件、service binding
- hook 擋掉後很懵
- 重複走同一條坑
**避坑**
- **任何代碼變動前**:讀 `docs/3-specs/` 對應 SDD
- 確認當前 Phase、task 編號、依賴關係
- 找不到 SDD → 停手問 richblack,不要猜
---
## 11. Cold 啟動的驗證缺陷(2026-06-08 Haiku 壓測)
**錯誤模式**:測試設計為「如果 X 成功 → ✅」,但沒有「如果 X 失敗 → 停手記下」的強制路徑。
**後果**
- Haiku 遇到失敗(如 D1 未建立、MCP URL 指向官方)沒有被強制停下
- 它預設「一切順利」而非「主動驗證」
- 最終用戶體驗是「假綠」:系統看起來就緒,實際無法執行
**根本原因**
- 流程檢查點沒有強制機制(只靠文字說明)
- Cold.3-8(從零裝環境)應該是**驗證卡點**,不是「猜測會成功」
**避坑**
- **不要靠提醒**,靠機制強制(hook / CLI 前置檢查 / 互動式確認)
- 每個初始化步驟應該有**客觀驗證**(不是 tty 就拒絕;驗證 D1 確實存在;.mcp.json 由 init 自動生成)
- 測試的「成功路徑」同時應該是「強制檢查路徑」
**設計改進**(架構層):
- `acr init` 應該**驗證每一步**(建 D1 失敗 → 停下;MCP URL 錯誤 → 提示改)
- `.mcp.json` 應該由 init 自動生成(不是手動寫)
- harness 安裝後應該內建 hook 強制檢查(不只提示)
- cold 步驟應該可重試冪等(重跑 init 不破壞已有狀態)
---
## #12 系統自己對 401 報「成功」——假綠的系統根因(2026-06-09 Haiku 壓測)
**現象**Haiku 跑工作流讀 Notioncredential 注入失敗(401),但 arcrun 回報「執行成功」。
不是 Haiku 單方面在騙——**arcrun 引擎自己把 401 判成 success**。
**根因鏈**(每一層都看不到 HTTP status code):
1. http_request host function`.component-builds/*/src/index.ts``return await res.text()`——
**丟掉 HTTP status code**,只回 body 原文(main.go:112 白紙黑字「架構債」)。
2. 零件 main.go 拿不到 status,只能猜 body:有 `{"error":...}` 才當失敗。
Notion 的 401 body 是 `{"object":"error",...}`key 是 object 不是 error)→ 沒被 catch → 判 success。
3. graph-executor `success===false || 'error' in r` → 節點通過 → verdict 寫 success。
4. `acr logs` / AI / 任何查詢拿到的都是「成功」。
**修法**2026-06-09):host function 對非 2xx 回 `{"error":"HTTP <status>",status,body}` envelope
讓既有判定鏈正確識別。2xx 維持原樣(向後相容)。http_request+claude_api+kbdb_upsert_block+km_writer 已修。
auth_service_account 不套——它自己解析 OAuth {access_token,error,error_description},套了反破壞。)
**教訓(richblack 原則)**
- **「執行成功與否」要 arcrun 系統能客觀驗,不能信 AI(或人)嘴巴說成功。**
- 但系統自己的 success 判定若看不到 status code,**系統自己就在假綠**——AI 假綠只是下游。
- 修「讓系統能驗真相」(surface status code> 修「叫 AI 別騙」。
**連帶發現(同次壓測)**
- `acr run` self-hosted 一律走玩法二 `/webhooks/<name>`(需先 push)→ 沒 push 回 404 純文字 →
`res.json()` 爆假錯誤。Haiku 因此 acr run 跑不了 → 被迫 curl 繞過再謊報 arcrun 成功。
修:本機有 YAML 就走玩法一 `/cypher/execute` 直接執行(三模式一致)+ res.ok 擋 + .yaml 容忍。
- **D1 建立只在 init 做一次**`acr update` 漏建 → init 時 D1 失敗(token 缺權限)後無冪等補建路徑。
修:update 也 ensureD1Database。**「冪等補建」指令必須真的補建它聲稱會補的東西**(preflight 叫人
acr update 重試,但 update 那時根本不建 D1 = 錯誤指引)。
- CF token 教學只勾 Workers+KV、**漏 D1 Edit** → D1 必建失敗。llms.txt/.env.example 已補 D1 權限。
**測試方法教訓(給操盤的 CC)**:壓測要測「Haiku 自主能不能做到」,**不可由 Opus 預先鋪路**
(設權限、填好 .env、prompt 裡寫死正確路徑、餵攻略檔)——那測的是照抄不是自主。唯一輸入=用戶口吻 prompt,
指引只能來自 Haiku 自己讀的系統入口(GitHub README/llms.txt)。且**不可信 Haiku 自報成功,要獨立查證**。
---
## 12. 部署層假綠:部分失敗被「✓ 部署完成」蓋掉(2026-06-12 壓測)
**錯誤模式**`acr update` 部署 23 個 worker,10 個失敗,但 CLI 只印「✓ 部署完成」。
用戶重跑 3 次都查不到根因(http_request 沒部上 → 壓測一直看到 401 假綠;cron migrate 404/500)。
**症狀**`downloadAndDeploy` 回傳的 `result.message` 含失敗清單,但 `update.ts` 無腦印綠勾不看 message
且每步 `execFileSync``stdio:'ignore'` 吞掉 stderr → 失敗只剩「Command failed: pnpm install」無從診斷。
**正確做法**
- `result.message.includes('失敗')` → 印 ⚠ + 明細,不印 ✓(CLI 1.3.5 修)
- 失敗步驟帶 stderr 尾段進錯誤訊息(`stdio:['ignore','ignore','pipe']` + catch e.stderrCLI 1.3.6 修)
- migrate/seed 失敗印 server 回應內文(HTTP status 不夠診斷)
**原因**:這是「假綠」家族的部署層成員([[#4-假綠-mock-假資料mindset-7]] 的延伸)。完成=客觀證據,
部署成功要逐 worker 可見 ✓/⚠,不能整批靜默後一句「完成」。日期:2026-06-12。
## 13. 「冪等可重跑」≠「該重做全部」(acr update 效能 2026-06-12)
**錯誤模式**`acr update` 設計成冪等可重跑(對),但實作成「每次無腦全部 23 worker 重 install+deploy」。
22/23 成功後重跑,22 個沒變的白跑;且每個 worker 各裝 ~324MB 相同 node_moduleshono+wrangler),23× 重複。
**症狀**richblack 觀察「一個一個慢慢跳,明顯在重新下載安裝」,跑好幾分鐘。
**正確做法(兩層,治本是②)**
1. **內容指紋 manifest**`~/.arcrun/deploy-manifest.json`):注入後算 hash,與上次成功比,相同跳過。
只記成功者(失敗下次必重試);含 accountId(換帳號自動重部);`--force` 強制全部。(CLI 1.3.7
2. **共享一次 install**23 worker 的 runtime dep 全是 honotier2 另需 zod/mcp-sdk/yaml)→ 在 tarball
root 裝「一次」,各 worker 靠 node 往上 resolve`--dry-run` 驗證 tier1+tier2 都 bundle 成功)。
207MB×1 取代 324MB×23。(CLI 1.3.8
**原因**:重跑要「只做沒成的 + 變動的」,不是「重做全部」。量測證實慢在重複 install,不是 worker 數量。
日期:2026-06-12。
## 14. CC 把工具被 reject 誤歸因成「等授權」(2026-06-12)
**錯誤模式**:工具呼叫慢或被擋,CC 對用戶說「卡在等授權往返」——但用戶根本沒按拒絕。
真因是:repo hookpre-bash-guard)攔截、權限分類器擋、或 CC 自己把一件事拆太碎/timeout 設太長空等。
**症狀**:「為什麼這麼久?」「不是我 reject 的,你要查為什麼 reject」——CC 甩鍋給授權而非查真因。
**正確做法**:被 reject/擋 → 查真因(讀 `.claude/settings.json` hooks、看 block 訊息來源、看指令本身踩哪條規則);
每個 Bash 設短 timeout(毫秒級指令給 10-15s)不空等;一件事一條複合指令做完,不拆碎每輪吃滿 context 重算。
**原因**:誠實歸因(mindset §7)。慢/被擋是有具體原因的,假設成「授權往返」是逃避查證。日期:2026-06-12。
---
## 15. 假設 KBDB v3 route 還在 → 整條斷鏈假綠(2026-06-14
**錯誤模式**:碰 KBDB 整合時沿用舊程式碼打 `/blocks`、頂層 `/search` 等 v3 路徑,沒先確認
基本盤現存哪些 route。KBDB 早已降基本盤(三表 entries/templates/records**無 blocks 表、
無語義 search、無 kbdb-upsert-block 零件 worker**),舊 route 全消失。
**症狀**skills/examples 整條(sync-registry-to-kbdb.py + arcrun_skills_examples.ts 的 5 個工具)
打死 route → 工具已註冊上線、AI 叫得到,呼叫卻全回 404。**典型假綠**:工具列表看得到
`arcrun_search_examples`,AI 以為能用,叫了拿 404,浪費 token 又被誤導。少了它 AI 寫 workflow 沒範本可用。
**正確做法**
1. 碰任何 KBDB 整合,**先確認打的是基本盤現存 route**:
`/entries`(含 `?entry_type=``?page_name=``/entries/search?q=`)、`/templates``/records``/recipe-stats`
別假設 v3 route`/blocks` `/search`)還在。
2. v3 `blocks` 欄位與基本盤 `entries` 幾乎 1:1(差別只在 `type``entry_type`),遷移很乾淨。
3. **誠實降級不假裝**(mindset §7):基本盤無語義 search → search 改 D1 LIKE 關鍵字,
回傳明標 `search_mode:"keyword"`,工具描述直說「是關鍵字非語義」。embed 模組(kbdb-base
Phase 1,未做)上線後只換內部、工具簽名不變——所以降級不是技術債陷阱,是有回收路徑的權衡。
**原因**:架構漂移(v3→基本盤)後沒回頭改下游,SDD 還標 ✅(當年確在舊 schema 跑通)。
教訓:上游 schema/route 換版,要 grep 全下游使用點逐一驗,別信舊 SDD 的 ✅。日期:2026-06-14。
詳見 docs/3-specs/llm-interface/tasks.md M3.2/M3.4 + kbdb-base/tasks.md 9.3/9.4。
> **漏網第二例(2026-06-15)——同類錯連犯兩次,正是沒做全域掃**9.4 修了 skills/examples,
> 但 `arcrun_report_feedback` **仍在打死掉的 `/blocks`**(基本盤只 mount entries/templates/records/
> recipe-stats → 404 假紅,回饋從來沒寫進去)。9.7 補修成 `/entries`(entry_type=agent-feedback)。
> **這正反證 §15 的教訓**:當時若真做了「grep 全庫的 `/blocks`」,這個就會一起抓到、不會拖到隔天。
> **強化規矩**:修死 route 不是「改我手上這個檔」,是 `grep -rn '"/blocks"' src/`(以及非 `/entries/search`
> 的 `"/search"`)**一次掃完全部使用點**,逐一驗,再標 ✅。漏一個 = 同個假綠陷阱原地複製。
---
## 16. 部署 leo21c 時 .env 官方帳號靜默蓋掉 config 的 leo21c2026-06-15
**錯誤模式**:對 leo21c self-hosted 跑 `acr update`/`acr init`,以為它用 `~/.arcrun/config.yaml`
的 leo21c 帳號(`cloudflare_account_id: 51a01bfa…`)。實際上 repo 根 `.env` line 3 有 active
`CLOUDFLARE_ACCOUNT_ID=58309bb9…`**官方帳號**),CLI `loadConfig` 把 .env 載進 process.env
**env > 全域 config**`config.ts:174`)→ 官方 account 覆蓋 leo21c。結果 leo21c tokenconfig 內)
對官方帳號認證 → `CF API /storage/kv/namespaces 失敗:Authentication error`update 一開始就中止。
**症狀**raw curl(直接讀 config.yaml 的 token+account)能通,但 `acr update` 報 Authentication error。
看似 token 壞掉,其實是 **token 對到錯帳號**token 是 leo21c 的、account 變成官方的)。
**診斷**`acr config --where` 印每欄來源。看到 `cloudflare_account_id ← env 變數` 且值是 58309bb9
就是踩到了(config.yaml 的 51a01bfa 被蓋)。
**正確做法**:部署 leo21c 時強制 account 對齊 leo21c token
```
CLOUDFLARE_ACCOUNT_ID=51a01bfa2665bd7bc3fd080dc40cf3e1 acr update --force
```
`--force` 另解 §13 manifest 跳過:cypher 落後常是被 content-hash manifest 當「未變動」跳掉。)
**原因**repo `.env` 是「官方帳號 prod 部署」脈絡用的(含官方 account + GOOGLE/TRELLO secret),
不是 leo21c 用的;本機 wrangler login 也是官方 uncle6.me。教訓:**部署前用 `acr config --where`
確認 account 真的對齊目標帳號的 token**,別信預設。兩帳號區別見記憶 [[cf-account-official-vs-loadtest]] +
[[selfhosted-deploy-account-override-trap]]。日期:2026-06-15。
---
## 17. deploy 注入 binding 被 stripOfficialOnlyBindings 當場清掉(2026-06-26issue #7
**錯誤模式**:在 `deploy.ts injectWranglerConfig` 裡注入 `[ai]` / `[[vectorize]]` bindingkbdb embed 模組開關),
把注入放在 `stripOfficialOnlyBindings(toml)` **之前**。strip 的 block header 正則含 `(routes|r2_buckets|ai)`
→ 會移除整個 `[ai]` 區塊。先注入 → 馬上被 strip 清掉 → self-hosted 開了語義查詢卻沒有 AI binding,embed 靜默失效。
**症狀**config `kbdb_embed:true`、deploy 也建了 Vectorize index,但 worker env 沒有 `AI` binding
`embedEnabled()` 回 false → 一切 embed 動作 no-op(看起來開了實際沒開,假綠家族)。
**正確做法**:注入 active binding 的步驟**一律放在 strip 之後**。注入前 binding 是註解(`# [ai]`),
strip 只清 active header 不碰註解;strip 完再取消註解 → 不會被清。dry-run 驗證注入後 active
`[ai]`/`[[vectorize]]`/`binding="VECTORIZE"` 都在再算數。
**原因**strip 與 inject 都是純文字操作、順序敏感。改 toml 注入順序時要想「後面還有沒有別的 pass 會動同一段」。
日期:2026-06-26。
---
## 18. KBDB 新增可查欄位要「零建表」——用 json_extract 不加真欄(2026-06-26issue #5
**錯誤模式**:要讓 `source`(埋在 `metadata_json`)變可查 → 直覺想「在 entries 表加一個 source 欄」。
這**違反 KBDB 表不變鐵律**(基礎三表萬年不動,新屬性天然在表外)。
**正確做法**:用 SQLite `json_extract(metadata_json, '$.source') = ?` 查既有 `metadata_json` TEXT 欄
**零建表、零 migration**filter 照樣可用。D1SQLite)原生支援 JSON 函式。
listEntries 加 `source` filter 即可,表結構一個欄位都不動。
**通用教訓**:KBDB「頂層化成可查欄位」≠「加真欄位」。凡是已經塞在 metadata_json 的屬性要變可查,
一律走 `json_extract`,不碰表結構。這跟 #6 的「PATCH record = 翻底層 entries.content 不動表」同源——
**動的是值/查詢,不是表**。日期:2026-06-26。
---
## 快速檢查清單(做新功能前)
- [ ] 這是工作流還是零件?問「有必要嗎?」
- [ ] API 有對應端點嗎?否 → 先補 API,不是在介面層拼裝
- [ ] 有 SDD 嗎?沒有 → 停手問 richblack
- [ ] 這段邏輯換介面(CLI → MCP)要重寫嗎?要 → 違反薄殼原則
- [ ] 會修改 KV/D1 的鑰匙嗎?是 → 檢查遷移冪等性 + 向後相容
- [ ] 需要人類同意嗎(暴露 / credential)?是 → 確保非 TTY 時拒絕
- [ ] 怎麼驗證完成?只靠輸出訊息 → 不夠,需 2xx + 數據
- [ ] 測試或初始化步驟有「失敗時怎辦」嗎?→ 不靠提醒,靠機制強制
- [ ] 碰 KBDB?確認打基本盤現存 route/entries /templates /records /entries/search),別假設 v3 /blocks /search 還在
- [ ] KBDB 要新可查欄位?用 json_extract 查 metadata_json,別加真欄(表不變鐵律,#18
- [ ] 改 deploy.ts toml 注入順序?想「後面還有沒有 pass(如 strip)會動同一段」(#17
+26
View File
@@ -0,0 +1,26 @@
# principles — 跨全局設計原則(push:CC 行動前必服從)
> 這個檔由 hook 在 session 開始**全文注入**,讓 CC 設計任何東西前都先看見這些準繩。
> 為什麼 push 而非寫成 card:原則是「會被遺忘的盲區」——沒推到眼前,CC 設計時很可能沒想到要服從就做了。
>
> 規則:**一行一條**,精煉成準繩(不是長篇論述)。≤15 條;超過代表某些該合併、或下放成 card。
> 發現新的跨全局原則 → append 一行。累積原則只改這個檔,**不必問用戶開新檔**。
> 區分:原則 = 反覆適用的準繩(這裡);單次選擇 = 決策(寫成 card);踩過的坑 = mistakes.md。
---
## 原則
- **工作流是 default,零件是稀有例外**:自用/少數人 → 工作流;只有「全 arcrun 生態必須重用」才建零件。「能包」≠「該包」。
- **零件只能 WASM**TinyGo 或 AssemblyScript 編譯成 `.wasm``registry/components/` 下禁 TypeScript 業務邏輯。
- **薄殼鐵律**:能力只實作一次放在 APIcypher-executor);CLI/MCP/lib 只做介面轉換 + 暴露,不自帶業務邏輯、不拼裝、不用 recipe 補 API 缺口。
- **cypher-executor TS 不碰 credential/auth/JWT/template 展開**:這些全在 WASM 零件;crypto.subtle 只准出現在 wasi-shim.ts 的 host function。
- **Cypher binding = YAML 的 URL 清單**,不是 Cloudflare service binding;零件串接走 HTTP URL,禁新增 `[[services]]`
- **arcrun 不做授權判斷**:能否打通由發 API key 的服務裁決,401/403 是對方行使授權、不是 arcrun bug。
- **arcrun 是 AI 呼叫的工具(AI→工具)**,不是工具回頭呼叫 AI;判斷/轉換由操盤的 CC 自己做,工作流不內嵌 LLM 節點。
- **誠實不假綠**stub/未實作回 `success:false` 或標 unimplemented,不 mock 充綠燈;完成=客觀證據(exit code / HTTP status + trace),不是口頭宣布。
- **不代替人類做風險確認**:建零件、暴露資料需人類明示同意;非 TTY 直跑就拒絕,不自己塞 flag 假裝人類同意。
- **部署繞開 GitHub**:走 `scripts/local-deploy.sh`wrangler 直推,不用 GitHub Actions(避免再被 flag)。
- **任何 code 變動前先讀對應 SDD 並宣告**:找不到對應 SDD → 停手問 richblack,不自行新建。
- **每完成一個 task 立刻更新 tasks.md `[x]`**,不批次。
- **缺能力的自力救濟階梯**:自家 API 缺→補 API;第三方 API 缺→workflow/code-node 補丁(非建零件);純計算→code-node;真需新穩定能力才零件 PR。
+190
View File
@@ -0,0 +1,190 @@
---
name: status
description: 當前進度、進行中 Phase、已知問題、下一步(動態文件,每 session 更新)
metadata:
type: project
last_updated: 2026-06-24
---
# 當前進度(動態)
> **更新頻率**:每次 session 結束時更新此檔。
> **新對話開始時讀此檔第一段**3 分鐘概覽)。
---
## 📍 當前位置
> **2026-06-27 本 sessionissue #8 地基1 + wiki-init 補骨架)**
> - **wiki-init 補骨架**:wiki 已初始化過(push 檔活躍),補了從沒建的 pull 層——`cards/decisions/` 13 張決策原子卡(Haiku 改寫 11+範本 2,含 gloss/實體/typed-edge)、TAXONOMY 換成 arcrun 軸(子系統/形態)、principles 填 13 條、INDEX 真實視圖。raw source 0 異動,無真斷鏈。
> - **issue #8[地基1] workflow description slot + search_workflow,北極星入口)**:新開 SDD `docs/3-specs/workflow-discovery/`(白名單已加)。leo 拍板 4 點(方案C雙寫/Q2 description 由操盤CC據實生成用戶可改/提示式回填/base通用entry_type filter+ 方向①(MCP 改打 /webhooks/named)。
> - ✅ **已實作 tsc 全綠**1.1 `/webhooks/named` 強制 description2.2+Q4 KBDB base 通用 entry_type filter(改4處:searchEntries/semanticSearch/route/proxy)|2.1 部署雙寫 embeddable entry(注意 KBDB 用 metadata_json 字串)|3.1 cypher `/workflows/search`3.2 MCP `u6u_search_workflows`4.1 `/workflows/backfill-search-entries`1.3b `GET /webhooks/named` 補 description/created_at 欄位。
> - ⏸ **卡待總管定**Phase 1.2/1.3MCP deploy 改打 /webhooks/named)卡在 ①-a/b/c——實作期發現 /webhooks/named 吃 graph 非 YAMLYAML→graph 編排寫在 CLI push.ts 介面層,MCP 複製=違 rule 07。①-c(先通債另開 issue)我推薦,待總管定。
> - **完成標準**:tsc 綠≠完成,框架級待 leo21c 端到端實證(強制填擋空/搜尋命中/租戶隔離/降級 hint/MCP 不再 404)。issue open。
> - **未 commit**(待 leo 明示;wiki 骨架與 #8 SDD/code 建議分兩 commit)。署名鐵律:跨 repo comment 開頭 `[arcrun CC]`#12)。
>
> **2026-06-26 上個 sessionissue #4/#5/#6/#7 一批)**
> - **#6**base `PATCH /records/:id`):✅ updateRecord + route,三表 append-only 不破。tsc 綠,端到端待 leo21c。issue open。
> - **#4**07-thin-shell §3.1 自力救濟階梯 + code-node 規則):✅ 兩份 07 同步 + 02-forbidden §5.2 連動。§3.5 階梯(自家API→補API/第三方→workflow/code-node 補丁/純計算→code-node)。**code-node 只定規則未實作零件**wishlist C1,另案)。純文檔。
> - **#5**(KBDB 查詢缺口,普世視角):source 過濾 ✅(json_extract metadata_json,零建表)+ cypher proxy 透傳;documents 聚合 ❌不做(走 graph MCP);DELETE proxy ⏸擱置(依賴頂層 T8);embed-on-write →併#7;能力對照文件 ✅ `docs/4-guides/kbdb-capabilities.md`
> - **#7**vectorize 全包,從零):✅ base embed 模組 `kbdb/src/embed.ts`(精耕只 embed `metadata.embed:true`+ entries route 接 embed-on-write/semantic search/capability_hint + kbdb_embed 開關(config/deploy ensureVectorizeIndex REST/init 問)+ MCP kbdb_search mode:semantic。**抓修 deploy 順序 bug**embed 注入要在 stripOfficialOnlyBindings 之後,否則 [ai] 被清)。kbdb/cypher/cli/mcp tsc 全綠,**端到端待 leo21c 部署開 Vectorize index**。
> - SDDkbdb-base tasks Phase 10#6/11#5/12#7)。4 issue 各 comment 回報、**全留 open**(待端到端/頂層)。**未 commit**(待人決定)。
| 項目 | 狀態 |
|------|------|
| **Phase** | Credential Primitives TS → WASM(§0.1-0.5 完成,0.6-1 進行中) |
| **主線** | kbdb-base §7.5 已上線(公庫/私庫雙向、UUID 身份、市場數據) |
| **近期完成** | MCP self-hosted bug 三修(2026-06-08 |
| **已部署(2026-06-09 上午)** | §8 P0 cron 止血;§7.8 onboarding P0/P1/P2CLI 1.3.3 publish |
| **已部署(2026-06-09 下午,Haiku 壓測暴露)** | **http_request+claude_api+kbdb_upsert_block+km_writer 假綠根因修復**(非 2xx 回 error envelope4 worker 已 deploy);**acr run self-hosted 修復**(本機 YAML 直接走 /cypher/execute 不需先 push + res.ok 擋 + .yaml 容忍);**D1-in-update 修復**update 漏建 D1 → 補 ensureD1DatabaseD1 已建 count:1);**CLI 1.3.4 publish**llms.txt/.env.example 加 D1 Edit 權限 |
| **Haiku 自主壓測(test_arcrun/5)結論** | onboarding 治好(兩輪都裝+init 沒跳過、走對路建 recipe 不建零件);但暴露 4 真 bug(見下);Haiku 仍會假綠(curl 繞過說成 arcrun 成功、D1 沒建謊報成功)→ 印證「執行真相要系統能驗,不能信 AI 嘴巴」 |
| **已驗證(2026-06-13 壓測 leo21c** | **401 假綠根治全鏈驗證**host fn error envelope → 零件 parsed["error"] → cypher isFailure()leo21c 實測 401 回 `success:false`(真紅);**`{{credential.notion_token}}` 注入實證打通**:真讀到 Notion Recipes 資料(「蕃茄蘑菇燉雞」+ iCook 連結),§8 credential 機制生效;**acr update 部署系統一輪修完**CLI 1.3.5 部署假綠露出、pnpm-workspace 缺檔補齊 23/23 全綠、1.3.6 失敗帶 stderr、1.3.7 manifest 跳過、1.3.8 共享 install 治本);check-release.sh + local-deploy.sh 全形括號 set-u crash 修復 |
| **已完成(2026-06-14matrix 重整交棒)** | **① SDD 遷移收尾**.agents/specs → docs/3-specs 全改(hooks5/rules5/CLAUDE.md/wiki + 2-architecture 鏡像/README/HANDOFF/4-guides),.agents 刪除,pre-write-guard 白名單刷成 10 個實存 SDDSessionStart + rule4.3 已驗)。**② KBDB 資料層 MCP 薄殼**kbdb-base Phase 9.1):mcp/src/tools/kbdb_data.ts 6 工具(template/record/query/search),守鐵律不給建表/SQLmcp tsc exit 0CLI(9.2)後補。**③ 修 LI M3 斷鏈**(見 mistakes #15):skills/examples 5 工具 + sync 從 v3 /blocks /search 改打基本盤 /entriesbase 加 page_name 過濾,search 誠實降級 LIKEmcp+kbdb tsc exit 0 |
| **已完成(2026-06-14 晚,HANDOFF §3b** | **修 MCP self-hosted 認證 401**mcp-account-source.md §5.5):根因=MCP partner-auth 把 Bearer 拿去 KBDB 驗證 partnernamespace 明碼非註冊 partner→401cypher 端 X-Arcrun-API-Key 不驗證直接當分區 key→CLI 通。修法①+②:① `MULTI_TENANT=false` 時 partner-auth 把 Bearer 明碼直接當 org_namespacetypes/middleware/wrangler,官方 SaaS 行為不變共用同碼);② mcp-setup 把 api_key/namespace 寫進 .mcp.json `headers.Authorization`(裸檔不送 header 是次因)。mcp+cli tsc exit 0、partner-auth 9 tests 綠 |
| **已處理(2026-06-25issue #3 官方庫誤寫善後)** | **① 清理 SOP runbook 備妥**`docs/5-records/2026-06-24-official-kbdb-cleanup-leo-misdelete.md`):14-E 遷移期 mira 誤寫 ~11 萬 `owner_id=leo` 進官方 prod `arcrun-kbdb`database_id `0c580910…`)。三道防誤刪閘(備份先行 d1 export → 核實 entry_type+時間範圍確認範圍乾淨 → 刪後驗證 count=0+孤兒 entry_values=0+其餘不受影響)。**補關聯刪除範圍**:`entry_values`slot-link,外鍵指 entries)先刪孤兒、`templates.created_by=leo` 單獨核實勿盲刪。**DELETE 不由 CC 跑**(不可逆+官方憑證+需人類明示,mindset §7)→ runbook 由 leo(官方運營方)親自執行。**【2026-06-25 已端到端執行完畢】** chaperone 模式逐步跑:核實誤寫 **111,368 筆**value 93,790/note 13,671/block 3,907,全 06-15~06-24 遷移期、孤兒=0、無 leo template)→ 45MB 備份 → leo 點頭後 `DELETE FROM entries WHERE owner_id='leo'`changes=111368)→ 驗證 leo 殘留=0/孤兒=0/官方庫其餘只剩 smoke_ns_1×2+null×13 筆烟霧殘留,非 leo)。備份已刪、`.gitignore``*.sql` 防誤 commit、gh issue #3 已回報客觀證據可關閉。**② 願景 acr migrate 雙向遷移記 BACKLOG.md**(對齊 wishlist C7,牽動 cli+cypher+kbdb,未來方向)。已 gh issue #3 comment 回報,**暫不 close 待 leo 跑完清理回報 count 歸零**。**③ 順手消 status 矛盾**credential 401 真實狀態=**已端到端實證打通**2026-06-13 Notion `{{credential.notion_token}}` 真讀到資料),tasks.md 8.5 原標 `[ ]`OpenAI 路徑沒走)已補 `[x]`(Notion 達同等證據,機制與服務無關)。**純文檔/runbook,無 code 變動** |
| **已修+merge prod2026-06-24issue #2 框架 bug** | **self-hosted cypher KBDB_BASE_URL 注入缺口修復**(總管經 GitHub issue #2 交辦):根因=`injectWranglerConfig` self-hosted 分支只注 database_id/MULTI_TENANT**漏注 KBDB_BASE_URL** → cypher `/kbdb/*` fallback 到官方 `arcrun-kbdb.uncle6-me`self-hosted 資料寫進官方庫、隔離破損)。修:deploy.ts self-hosted 分支加 `KBDB_BASE_URL` 改寫成 `arcrun-kbdb.<ctx.workerSubdomain>.workers.dev`,比照既有注入模式;init/update 共用此注入點一處修兩條路。驗:tsc exit 0、真實 cypher toml 注入 subdomain=leo21c → `arcrun-kbdb.leo21c.workers.dev`comment 行不動)。**已 merge main+push**commit 9c4333d、merge ba00b98)、**CLI npm publish 1.3.13**(修的是 CLI 注入邏輯 → self-hosted 用戶裝 npm 套件才到手,光 git 不生效;local-deploy.sh §6 自動 bump+publishrelease-check 全綠)、**issue #2 已關閉**。**端到端落庫實證歸 mira dogfooding 帳號**(需 leo21c token 跑 acr update + wrangler d1 execute 收綠燈,不卡本框架修復)。⚠️ 與 issue 描述出入:cypher toml **本就有** KBDB_BASE_URL(寫死官方),比「沒有」更糟(`??` fallback 根本不觸發)→ 修法是就地改寫而非新增。**慣例落地**:總管↔arcrun 交辦走本 repo GitHub issue(已寫進 CLAUDE.md|
| **已部署+端到端驗收(2026-06-15,總管交棒 3 件)** | **① CLAUDE.block.md 重寫**HANDOFF §6Haiku 能懂):補三盲點=recipe 是公共投稿非私人腳本/缺能力補 API 不准 recipe-工作流拼裝(附口訣)/自製零件退場路徑(claude_api 刪、kbdb 走 acr kbdb 薄殼、假零件改 recipe);README 零件vsrecipe 段同步對齊。**② cypher proxy 補 /kbdb/entries CRUD**kbdb-base 9.6,解鎖 mira _kbdb_client.py 主線):POST/GET list/GET :id/PATCH :id 純轉發基本盤;租戶隔離同 9.5(寫入注 owner_id、list 強制本租戶、PATCH 剝 owner_id、刻意不開 DELETE)。**③ arcrun_report_feedback 改打 /entries**kbdb-base 9.79.4 漏網):舊 POST /blocks 是死 route(404 假紅)→改 entry_type=agent-feedback。**端到端 prod 驗收全綠**:無key→401、跨租戶 list count=0、owner_id hijack 被剝、page_name lookup 通、/blocks→404 確認、agent-feedback 寫入經 proxy 讀回 count=1。cypher+mcp tsc exit 0、已部署官方 58309bb9、CLI npm 1.3.12、smoke 資料已清。**交棒 mira**leo21c 改 _kbdb_client.py 打 cypher /kbdb/entries 即可遷移 |
| **已部署+自驗(2026-06-15HANDOFF §6b 部署斷層解決)** | **leo21c cypher 落後 → 已重部,`/kbdb/entries` 回 200**(解鎖 mira 14-A 主線遷移)。**根因不是 GitHub lag**origin/main==本地,含 entries route):① `acr update` 的 content-hash manifestdeploy.ts:198-225)把 cypher 當未變動跳過 → `--force` 清空 manifest 全部重部;② **更深陷阱**repo `.env` line 3 active `CLOUDFLARE_ACCOUNT_ID=58309bb9`(官方)被 CLI 載入覆蓋 config.yaml 的 leo21c `51a01bfa`env>configconfig.ts:174)→ leo21c token 對官方帳號認證 → KV「Authentication error」中止 → 解:部署時 `CLOUDFLARE_ACCOUNT_ID=51a01bfa… acr update --force`(記憶 [[selfhosted-deploy-account-override-trap]])。**部署 23/23 全綠**(含 cypher/kbdb/mcp+ seed(10 API+23 auth)+cron migrate。**自驗**`/kbdb/entries?limit=1`→200 真 body(非假綠)、`/kbdb/templates`→200、`/kbdb/records?limit=1`→404**非回歸,proxy 本就無 bare list route,只有 POST + by-template/:t + :id**);缺口② MCP initialize(Bearer leo)→200MULTI_TENANT 注入生效、KBDB binding 隨 mcp 上線)。全域 acr 已升 1.3.12(npm 本就有,非重發)。**純部署無 code 變動** |
| **已部署(2026-06-15MCP self-hosted 401 注入缺口修補)** | **根因=部署沒注入 MULTI_TENANT**(非 code bug):partner-auth.ts MULTI_TENANT 分支對,但 mcp toml 該行原是註解、injectWranglerConfig 注 KV/subdomain 卻漏注 MULTI_TENANT → worker env undefined → 走 partner-key → self-hosted 401。修:deploy.ts 加 injectMultiTenantDeployContext.selfHostedinit/update 帶旗標;mcp toml 改 active [vars])。本地驗注入真實函式 PASSmcp/cypher 各 1 行 active MULTI_TENANT=false 在 [vars] 下);官方 MCP partner 路徑回歸 401(不變)。CLI npm 1.3.11。**端到端交棒 mira**leo21c 重跑 acr updateCLI≥1.3.11)→ curl Bearer leo /mcp 應 200。SDD §5.5.1 |
| **已部署+端到端驗證(2026-06-14 晚)** | **KBDB CLI 薄殼解卡(9.5+9.2)已上 prod**cypher `routes/kbdb-proxy.ts` 純轉發 + CLI `commands/kbdb.ts`(acr kbdb)。**端到端煙霧測試全綠**curl cypher.arcrun.dev/kbdb/*):無key→401、建template→200(created_by=租戶)、租戶隔離 query+search 都 0 筆跨租戶。CLI npm 1.3.10。**煙霧測試抓到 2 真 bug 並修**:①proxy fallback 寫死舊死的 kbdb.finally.click→改現役 arcrun-kbdb + cypher [vars] KBDB_BASE_URL;②kbdb searchByTemplate `\|\| true` stub 讓 owner_id 過濾失效(跨租戶洩漏)→改 SQL JOIN entries 真過濾。三 worker(cypher/mcp/kbdb)都已 deploy 官方帳號。**self-hosted MCP 那條未測**(官方不設 MULTI_TENANT,待 leo21c 部署 HANDOFF §3)。⚠️ prod 留了 smoke_contact 測試 template(掛 smoke_ns_1,不污染真租戶,template 無 delete API |
| **待處理** | §8 P1/P2 recipe/workflow list 遷 D1(需 D1 先穩,現已可建);4 份 inline host fn 抽共用 helper deduparcrun.dev/llms.txt servemcp worker 偶發 fetch failed(網路抖動,重跑即過,非 bug);**LI M3 實環境驗收**(需 KBDB_BASE_URL 跑 sync + 叫工具,目前只驗 tsc+dry-run);**mira 波次2 主線遷移**arcrun 端 cypher 已部 leo21c、/kbdb/entries 實測 200**已交棒待 mira CC 改 _kbdb_client.py**)|~~KBDB /kbdb/entries 缺口~~✅~~MCP report_feedback 死 route~~✅~~self-hosted MCP 端到端實測~~✅(2026-06-15 leo21c initialize 200,見上)~~leo21c cypher 落後/部署斷層~~✅(2026-06-15 §6b 解)~~self-hosted cypher KBDB_BASE_URL 漏注入(issue #2 隔離破損)~~✅(2026-06-24 merge prod,端到端待 mira 帳號驗) |
---
## 🔄 進行中的 Task
### credential-primitives-wasm
- [x] Phase 0.1-0.5:核心合併、21 個零件 contract 完成、CREDENTIALS_KV binding 確認
- [x] Phase 0.6wasi-shim 新增 host functionskv_get / crypto_decrypt / crypto_sign_rs256
- 決策:host function 實作位置 = cypher-executor/src/lib/wasi-shim.ts
- 待驗:component-loader 能否正確呼叫 WASM runner
- [ ] Phase 0.7component-loader WASM runner 路徑(依賴 0.6
- [ ] Phase 1.1-1.8auth_static_key WASM 零件(TinyGo
- [ ] Phase 2.1-2.6auth_service_account WASM 零件(JWT signing
- [ ] Phase 3.1-3.5:清除 cypher-executor 三套違規 TScredential-injector.ts / jwt-signer.ts / BUILTIN_*)
### kbdb-base
- [x] §7.5:公庫/私庫雙向、UUID 身份、市場數據(2026-06-07 deploy
- [x] §8 P0cron 止血(2026-06-09)。scheduled.ts 每分鐘 list → 單一 key `cron-idx:_all` get(新增 lib/cron-index.tswebhooks-named push/delete 維護;migrate-cron-index 一次性遷舊)。1440 list/日 → 0。cypher tsc exit 0
- [ ] §8 P1/P2recipe/workflow list 遷 D1(透過 kbdb worker /entries HTTP API 雙寫,**不加 binding**,用 cypher binding 狗糧)。另開 session 做(大、易出錯,需專注+壓測)
- **狀態**:架構拍板(richblack 2026-06-09:用 kbdb /entries HTTPservice binding 才需問),未動 code
### onboardingself-hosted-init §7.82026-06-09 交付)
- [x] P0acr init 偵測先於動作 + 裝完驗收(cli/src/lib/preflight.ts)。冪等。
- [x] P1acr whoami+--json+ MCP arcrun_whoamiAI 別自己 curl 猜帳號)
- [x] P2mcp-setup 寫完印「請重啟 client」(D3)
- [~] P3(部分,2026-06-09 push c152f5f):repo 加 `.env.example` 範本(每格白話說明、值留空,
`.gitignore` `!.env.example` 放行)+ llms.txt 教 AI「先 cp .env.example .env、帶用戶填值」。
已 push main → 公開 repo 生效(raw 200 已驗)。仍待:arcrun.dev/llms.txt servelanding/public 缺檔)。
### LLM Wiki 建設
- [x] 階段一:目錄結構建立、分類規則表、檔案掃描(101 個 .md)
- [x] 階段二:mistakes.md + decisions-summary.md + INDEX.md + status.md(本檔)
- [x] 階段三:文件遷移執行(2026-06-14 完成)— SDD 實體已在 `docs/3-specs/`hooks(5)/rules(5)/CLAUDE.md/wiki
全部從舊 `.agents/specs/` 改指 `docs/3-specs/``.agents/steerings/tech.md`(已與 `docs/3-specs/tech.md` 同步)
連同空 `.agents/` 刪除。SessionStart hook 與 pre-write-guard 白名單(10 個 SDD 目錄全對齊)已驗證。
另收尾活指針:docs/2-architecture/ 鏡像、docs READMEs / HANDOFF / 4-guides 的 arcrun-local 指針一併改新路徑。
刻意保留:docs/5-records 歷史 incident/migration 記錄、跨 repopolaris/mira、matrix/kbdb)路徑、
docs/3-specs/** SDD 內文(改 SDD 內文=change,需另確認)、README 遷移對照表。
- 🟡 待補:modules/ cypher-executor、wasi-shim、recipe-system 等)
---
## ⚠️ 已知問題 / 待處理
> 2026-06-09 更新:已解項標 ✅;🔴/🟡 為仍待處理。本次 Haiku 壓測新發現的 bug 加在表內。
| 問題 | 優先級 | 狀態 | 備註 |
|------|--------|------|------|
| ~~**credential 注入 401**~~ | ✅ 已解 | **8.1-8.5 全完成(2026-06-25 確認)** | 機制(auth_static_key `resolve_credentials` + graph-executor `resolveCredentialRefs`)已端到端實證:2026-06-13 Notion `{{credential.notion_token}}` 真讀到資料(同等於 8.5 OpenAI 驗收,機制與服務無關)。tasks.md 8.5 已補 `[x]` |
| §8 P1/P2 recipe/workflow list 遷 D1 | 🔴 高 | 架構已拍板未動 code | 走 kbdb /entries HTTP 雙寫不加 binding;依賴 D1(現已可建)。另開 session 做 |
| 4 份 inline http_request host fn 抽共用 helper | 🟡 中 | 待 dedup | http_request/claude_api/kbdb_upsert_block/km_writer 各自複製貼上同段(這次假綠修也是逐份改) |
| `arcrun.dev/llms.txt` 404 | 🟡 中 | 未 serve | landing/public 缺檔;GitHub repo 內正常(test/5 走 GitHub 不阻擋) |
| MCP account-source | 🟡 中 | 記錄中 | self-hosted MCP 指官方不指自己(§5.2 已知) |
| ENCRYPTION_KEY 冪等性 | 🟡 中 | 設計中 | init 多跑生成新 key,無法複用舊 key |
| recipe submitted 後沒有 uuid | 🟡 中 | 待驗 | submit-p 應回 uuidCLI 拿不到 |
| ~~KV list 爆量~~ | ✅ 已解 | §8 P0 部署 | cron list→單 key get1440/日→02026-06-09 |
| ~~onboarding 缺陷(4 項)~~ | ✅ 已解 | §7.8 P0/P1/P2 + P3 部分 | CLI 1.3.4Haiku 壓測證實裝+init 不跳過 |
| ~~D1 建不起來~~ | ✅ 已解 | update 補 ensureD1Database + token 加 D1 權限 | 2026-06-09D1 已建 count:1 |
| ~~http_request 401 假綠~~ | ✅ 已解 | host fn 非 2xx 回 error envelope | 4 worker deployauth_sa 不套 |
| ~~acr run self-hosted 404 爆~~ | ✅ 已解 | 本機 YAML 走 /cypher/execute + res.ok 擋 | CLI 1.3.4 |
| ~~npm publish~~ | ✅ 已解 | token 在 .env NPM_API_TOKEN | 走 local-deploy.sh step 6,別手動繞 |
---
## 🧪 測試進度
### 壓測 2026-06-08Haiku 乾淨重測)
- **目標**kbdb-base §7.5 公庫/私庫 + UUID 驗收
- **對象**Haiku(全程自主操作)
- **測試檔**`/test_arcrun/3/test_to_haiku.md`9 個步驟)
- **撞牆記錄**`/test_arcrun/撞牆記錄.md`
- **狀態**:準備妥當,awaiting Haiku run
### 前次壓測(2026-06-07
- ✅ kbdb-base §7.5 上線前驗收(16 項通過)
- 📋 發現 onboarding 四缺陷(Cold.1-8
---
## 🚫 封測狀態
**推遲**richblack 2026-04-19 決定,後延至 2026-06-08 依舊推遲)
**原因**
- Phase 1-3auth WASM + 清除違規 TS)未完
- 待 credential-primitives-wasm 完整交付
**啟動條件**Phase 1-3 完成 + 壓測 Haiku 自癒能力驗證
---
## 📋 下一步(優先級)— 2026-06-25 更新
### 🔴 最優先
1. [x] ~~**credential 注入 401 修復**~~**已端到端實證打通**2026-06-13 Notion `{{credential.notion_token}}` 真讀到資料;tasks.md 8.5 已補 `[x]`)。機制完成,非阻擋。
2. [ ] **§8 P1/P2 recipe/workflow list 遷 D1**D1 現已可建(依賴解除)。走 kbdb /entries HTTP 雙寫不加 binding。
大、易出錯,另開乾淨 session + 壓測。**← 現在的真.最優先未做項。**
### 🟡 本周
3. [ ] credential-primitives-wasm Phase 0.6-0.7host function + WASM runner)→ Phase 1-2auth WASM 零件)
4. [ ] 4 份 inline http_request host fn 抽共用 helperdedup;這次假綠修是逐份改的)
5. [ ] 清除 cypher-executor 違規 TSPhase 3credential-injector.ts / jwt-signer.ts / BUILTIN_*
### ⚪ 未來
6. [ ] `arcrun.dev/llms.txt` servelanding/public 補檔)
7. [ ] 補 wiki modules/(文件遷移階段一~三已於 2026-06-14 完成)
8. [ ] ENCRYPTION_KEY 冪等性、MCP account-source、recipe submit uuid 回傳
---
## 🔗 相關資源
| 資源 | 位置 | 用途 |
|------|------|------|
| 總進度 | `docs/3-specs/arcrun/arcrun.md` | 全景進度表 |
| 當前 Phase SDD | `docs/3-specs/arcrun/credential-primitives-wasm/` | design + tasks |
| 壓測 case | `/test_arcrun/2/test_case.md` / `3/test_to_haiku.md` | 功能驗收 |
| 事件復盤 | `/docs/incidents/` | 歷史踩坑 |
| 常犯錯誤 | `.claude/wiki/mistakes.md` | 自檢清單 |
| 架構決策 | `.claude/wiki/decisions-summary.md` | 設計參考 |
---
## 版本日誌
| 日期 | 變動 |
|------|------|
| 2026-06-08 | 初建。MCP bug 修正完成、wiki 系統搭建、壓測 Haiku 進行中 |
| 2026-06-08(補) | Haiku 壓測發現 Cold 驗證缺陷:init 無強制檢查點 → 假綠風險。記入 mistakes.md §11 |