chore: D22 落地——docs/SDD/wiki/CLAUDE.md 進 repo(Gitea private 預設全 push)
頂層 D22 決策(leo 2026-07-03 拍板):推什麼由開發環境歸屬決定, Gitea private=除機敏值/build 產物/.github 外全 push。 解 T1.5 卡點:雲端工人 clone 拿得到 credential-store-migration.md,可就地改寫 SDD。 機敏掃描兩輪通過(新增 189 檔約 2.1MB,node_modules/dist/wasm 照舊排除)。 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,100 @@
|
||||
# HANDOFF: arcrun config 寫入端 + 讓 AI 看懂 scope + KBDB Vectorize 開關
|
||||
|
||||
> 來源:InkStoneCo 頂層總管,2026-06-15。mira self-hosted dogfood 踩出的兩條框架缺口。
|
||||
> 兩條都是 `change`,走 SDD 協議(先讀對應 SDD、宣告、改 tasks.md)。
|
||||
> 觸發實證:mira(一個 AI)被交代遷移時**沒看懂 scope 存在**,差點誤部官方帳號。
|
||||
|
||||
---
|
||||
|
||||
## 缺口 1:config 讀分層完整、寫只能全域 + AI 看不懂 scope
|
||||
|
||||
### C.1 補 `acr init` 專案層寫入端
|
||||
|
||||
`cli/src/lib/config.ts` 讀取分層完整(`findProjectConfig()` env > 專案層 > 全域,8/8 測試),但**寫入只有 `saveConfig()`(config.ts:208)寫死全域** `~/.arcrun/config.yaml`(config.ts:38)。`acr init` 4 處呼叫(init.ts:93/142/278)全寫全域。
|
||||
- → `acr init` 無法寫專案層。leo 意圖 project scope 卻被迫污染全域 → 怕誤打官方 uncle6me。
|
||||
|
||||
**修法**:
|
||||
- `saveConfig()` 加 scope 參數(`'global' | 'project'`);project 寫 `./.arcrun.yaml`(`process.cwd()`)。
|
||||
- `acr init` / `acr config set` 加 `--project` flag。
|
||||
- 預設是否翻轉(init 預設寫專案層)由 richblack 定(影響既有使用者)。
|
||||
- 參考既有範式:`mcp-setup.ts` §7.8 已有 project scope 寫 `.mcp.json` 概念,比照。
|
||||
|
||||
### C.2 ★ 改 `cli/harness/CLAUDE.block.md` 讓操作 AI 看懂 scope(最重要)
|
||||
|
||||
**病根**:arcrun 是 AI 操作的工具(mindset §2),但 harness block(全文 41 行)對 scope / 帳號歸屬 / 誤部風險 **0 字提及**。AI 讀完完全不知道有 scope 這層 → mira 把「資料遷移」跟「在哪 scope 部署」混為一談。
|
||||
|
||||
在 harness block 加「部署落點與帳號安全」段:
|
||||
1. **scope 模型**:分層 `env > 專案層 .arcrun.yaml > 全域 ~/.arcrun/config.yaml`。**部到哪個 CF 帳號取決於當前資料夾解析到哪份設定**。動手前先 `acr config --where`。
|
||||
2. **多帳號警示**:同機可能多個 CF 帳號(dogfood / prod);`.env` 可能並存多組 `CLOUDFLARE_*`(含 `*_UNCLE6` 後綴)。部署/遷移前必須明確目標 account_id,挑錯=打到正式環境(不可逆)。
|
||||
3. **遷移 ≠ scope**:搬資料(A 帳號→B 帳號)和「我用哪個帳號」是兩件獨立的事。
|
||||
4. **fallback 陷阱**:任一層都沒 `mode` → `acr` 靜默當 `local`(config.ts:176),不報錯。看到「成功」但無雲端 trace 先查 `acr config --where`。
|
||||
5. **無設定就停手**:`--where` 顯示無設定 / fallback local 而意圖 self-hosted → 停下問人。
|
||||
|
||||
> harness block 是 AI 的 mindset 入口,比 CLI `--help` 重要(AI 偏好讀 harness)。scope 安全屬「動手前世界觀」,必在這層。
|
||||
|
||||
**對應 SDD**:`docs/3-specs/arcrun/sdk-and-website/config-layering.md`。
|
||||
|
||||
---
|
||||
|
||||
## 缺口 2(任務 D):KBDB Vectorize(embed)無環境開關
|
||||
|
||||
### 病根
|
||||
|
||||
KBDB 分 `base`(D1 only,免費)+ `embed` module(Vectorize+AI binding,語意搜尋)。但 `acr init/update` 完全沒處理 embed(grep `vectorize|embed` 在 `cli/src/` = 0 命中);只活在 `kbdb/src/index.ts:5` 註解。→ **沒有 config 欄位 / 指令讓使用者或 AI 表達「要不要開 Vectorize」**。
|
||||
|
||||
### 設計哲學(leo 2026-06-15):降級要優雅,但「完整體驗」要有門
|
||||
|
||||
> 「最好是我自己這套可以語義搜尋;不開就降級;**但想完整體驗卻沒有門**。」
|
||||
|
||||
- **預設降級且能跑**:沒開 embed → base 仍可關鍵字搜尋(不是壞掉)。
|
||||
- **降級要明示不能無聲**:跑 base 時要讓人知道「語意搜尋未開」。無聲降級=用戶以為這就是全部=bug。
|
||||
- **★ 升級要有門**:config 欄位 + `acr` 提示「想語意搜尋?這樣開」。現在連「有更完整版本可選」都不可見。
|
||||
|
||||
### 修法(config 欄位 + 部署條件注入,比照 MULTI_TENANT)
|
||||
|
||||
1. **config 加欄位**:`ArcrunConfig`(config.ts:11)加 `kbdb_embed?: boolean`(預設 false=base 免費)。leo dogfood 設 true。
|
||||
2. **部署條件注入**:`acr update` 部 KBDB worker 時 `kbdb_embed: true` → 注入 Vectorize+AI binding + 部 embed module;false 只部 base。比照 `deploy.ts:395` MULTI_TENANT 注入、`deploy.ts:380` WORKER_SUBDOMAIN 注入。
|
||||
3. **降級可見**:`acr config --where` / 部署摘要顯示「KBDB: base(語意搜尋未開)」或「embed(Vectorize 啟用)」;base 時提示升級門。
|
||||
4. **AI 認知**:harness block(C.2 那段)一併說「KBDB 預設 base(D1 免費僅關鍵字);語意搜尋需 `kbdb_embed: true`(Vectorize 計費)。功能需語意搜尋但這套是 base → 別假裝有,明示降級指出升級門」。呼應 mindset §7 誠實。
|
||||
|
||||
**對應 SDD**:KBDB 模組化屬 `docs/3-specs/` kbdb spec;部署注入屬 `sdk-and-website/self-hosted-init.md`。
|
||||
|
||||
---
|
||||
|
||||
## 任務 E(★ 急,擋 mira 14-E load):給 mira「leo21c kbdb worker 實際綁的 D1 id」
|
||||
|
||||
> 總管裁定(2026-06-15):mira 14-E load 撞 Workers 每日 10萬 request 牆(逐筆 API 病根)→ 改走 `wrangler d1 import`(資料層搬遷=用戶 CF 權限,繞 API)。**但 mira 需要 D1 id 才能灌對庫。**
|
||||
|
||||
**為什麼 mira 自己查不到**:
|
||||
- cypher-executor **無** D1 binding,轉發給 **kbdb worker**。
|
||||
- kbdb worker 的 D1 id 是 **deploy.ts 部署時動態注入**(`kbdb/wrangler.toml` 靜態值 `0c580910…` 是官方 prod,self-hosted 部署會覆蓋)。
|
||||
- → leo21c 上 kbdb worker 實際綁的 D1 id ≠ mira `wrangler` 直連的 `arcrun-kbdb`(`1099d0f3…`)。這是「wrangler 查 0 筆、cypher 有 11 萬筆」的真相(兩個同名不同 id 的 D1)。
|
||||
|
||||
**你要做**:用 leo21c CF token 查 leo21c 上 **kbdb worker 部署時實際注入的 D1 database_id**(看 deploy.ts 注入邏輯 / 查 leo21c worker runtime binding),告訴 mira。mira 拿到才能 `wrangler d1 import <該 id>` 灌對庫。
|
||||
|
||||
## 任務 F(B,框架缺口,非急):cypher `/kbdb/entries` 補批次端點
|
||||
|
||||
> 總管裁定 B:應用層大批寫入該有批次 API。**未來每個 self-hosted 用戶大批匯入都撞 10萬 request 牆**(不只 mira)——逐筆 POST 每筆 1 request,~5萬筆燒光免費 worker 當日配額。
|
||||
|
||||
- `POST /kbdb/entries` 收陣列(`[{...},{...}]` 或 `{entries:[...]}`),1 request 寫 N 筆 → 45.8萬筆只要 ~900 requests。
|
||||
- 守鐵律(只轉發 API、不開 SQL/建表;owner_id 仍自動注入每筆)。
|
||||
- 對應 SDD:kbdb-proxy 屬既有範圍。
|
||||
|
||||
## 任務 G(雜項,mira 14-E 踩出的 cypher 框架特性,記著)
|
||||
|
||||
mira load 踩出,記給框架修(非急):
|
||||
1. CF WAF 1010:Python urllib 預設 UA 被擋 → 文件提醒帶 UA。
|
||||
2. 重複 id → 500(非 409):UNIQUE 衝突宜回 409 友善處理。
|
||||
3. 零星單筆 500:某些 block 穩定 500、同 content 換 id 卻 200=邊緣 bug(疑 metadata_json/refs_json 某字元觸發,未定位)。
|
||||
4. `?id=` query 被忽略(回前 100 筆不過濾);查單筆只能 `/entries/:id` 路徑——文件宜明示。
|
||||
5. **DELETE `/kbdb/entries/:id` route 缺**(OPTIONS 卻宣告支援 DELETE,CORS 宣告與實作不一致)。
|
||||
6. cypher 綁的 D1 與同名 `arcrun-kbdb` database_id 不一致(見任務 E)——self-hosted 文件宜說明「wrangler 直連 ≠ cypher 綁的庫」。
|
||||
|
||||
## 共通教訓
|
||||
|
||||
任務 C、D 同類:**把「該是環境設定的選擇」從 code 註解提升到 AI/使用者可表達的層級**。arcrun 是 AI 操作的工具,凡影響「部到哪、開什麼功能」的選擇都必須在 config + harness 可見,不能只活在實作裡。
|
||||
任務 E/F 同類:**資料層搬遷(用戶 CF 權限 d1 import)vs 應用層大批寫入(cypher 批次 API)是不同層級,都該存在**——逐筆 API 推大批資料是把「資料遷移」做成「應用層寫入」,撞 worker 請求上限。
|
||||
|
||||
## 依賴
|
||||
|
||||
mira 側(建專案層 .arcrun.yaml + 移全域)先行,見 mira repo `docs/HANDOFF-arcrun-config-project-scope.md`。本框架補強讓「未來不再污染全域 / AI 不再看不懂 scope」,是根治,非阻擋 mira 當前解耦。
|
||||
@@ -0,0 +1,151 @@
|
||||
# HANDOFF: Matrix 重整交棒給 arcrun(2026-06-13)
|
||||
|
||||
來源:InkStoneCo 頂層 `.agents/specs/matrix-rearrange/`。本檔是該重整交給 arcrun 的待辦清單。
|
||||
**指針式考古**:整合素材真身在 InkStoneCo `_archive/`,照路徑去挖,不複製進此檔。
|
||||
|
||||
---
|
||||
|
||||
## 1. cypher-executor 整合進 arcrun 後**關掉**(leo 2026-06-13:整合後只剩 arcrun)
|
||||
|
||||
`matrix/cypher-executor` 是 diverged 副本,**整合進 arcrun/cypher-executor 後就關掉/封存**,之後 cypher 只有 arcrun 一份。
|
||||
|
||||
### 如何整合(勘查 2026-06-13)
|
||||
|
||||
**A. matrix 版獨有、arcrun 缺的 5 檔 → 補進 arcrun**(皆非 SaaS 遺留,是 self-hosted 核心):
|
||||
- `src/actions/version-selector.ts`:零件版本選擇策略 floating/stable/pinned
|
||||
- `src/actions/autoPublishMissing.ts`:missing 零件用 Workers AI 自動生成上架
|
||||
- `src/lib/component-dispatcher.ts`:雙模式路由 wasm/cypher_binding/service_binding
|
||||
- `src/lib/wasm-executor.ts`:WASM 執行
|
||||
- `src/routes/proxy.ts`:proxy 路由
|
||||
|
||||
**B. 兩邊都有但內容不同的 10 檔 → 逐檔比對合併**(取較完整/正確的一邊,保留 arcrun 較新的 auth 演進):
|
||||
`cypher-handlers` / `execution-evaluator` / `execution-logger` / `graph-builder` / `search-nodes` / `triplet-parser` / `webhook-graph-resolver` / `webhook-handlers` / `graph-executor` / `index`
|
||||
|
||||
**C. 保留 arcrun 獨有的**(不要被舊版覆蓋):`credential-injector` / `auth-dispatcher` / `auth-recipe-seeds` / `api-recipe-seeds`(arcrun 較新的 Auth Recipe 演進)。
|
||||
|
||||
**素材真身**:`matrix/cypher-executor/`(降級後仍在原地)。整合完成、驗證通過後,**封存 matrix/cypher-executor 進 `_archive/`,cypher 之後只有 arcrun 一份**。先讀對應 SDD 再動。
|
||||
|
||||
## 2. KBDB 插件化 + 補 CLI/MCP 薄殼(arcrun 端只留基本盤 + 暴露能力)
|
||||
|
||||
`arcrun/kbdb` **留 3 表基本盤 + API**(已完整:templates/entries/records/search)。它是刻意設計的基本盤(0001_base.sql 註釋 plugin model),**不升 v3、不加 blocks 表**。triplet/graph 由 `matrix/kbdb-graph-plugin` 抽成獨立 repo KBDB-graph。
|
||||
|
||||
**KBDB 鐵律(leo 2026-06-14)**:任何人不准動表;新類型=建 template(走 API);插件/AI/人全走 API,禁 SQL;基本盤不提供建表 API。詳見頂層 `DECISION-kbdb-v3-baseplane.md`。
|
||||
|
||||
**arcrun 端待辦(核實:CLI/MCP 現在完全沒 KBDB 能力)**:
|
||||
- **補 MCP 薄殼**(AI 用,含插件):`kbdb_create_template`(name+slots)、`kbdb_create_record`(填 slot)、`kbdb_query`/`kbdb_search` 等,調基本盤現有 API。**不提供建表 tool,只給 template/slot**——類 Supabase 萬用表,AI 想建表時只有 template/slot 可用。
|
||||
- **補 CLI 薄殼**(人用,後補):對應命令。
|
||||
- 能力真身在基本盤 API(已有),CLI/MCP 只薄殼暴露(arcrun 薄殼原則)。
|
||||
|
||||
對方交棒見 `matrix/kbdb-graph-plugin/docs/HANDOFF-kbdb-plugin.md`。
|
||||
|
||||
## 3. leo21c self-hosted 部署(Mira dogfood 用)
|
||||
|
||||
leo 用 leo21c CF 帳號部署 self-hosted arcrun(`MULTI_TENANT=false`),Mira 改 dogfood 這套。
|
||||
依現有 `scripts/local-deploy.sh` + `docs/3-specs/arcrun/sdk-and-website/self-hosted-init.md`。namespace 明碼非 api key。
|
||||
|
||||
### 3b. ⚠️ MCP self-hosted 認證失敗(mira CC 2026-06-14 回報,跨專案問題)
|
||||
|
||||
**一句話**:MCP worker 還走舊 partner-key 認證(`mcp/src/middleware/partner-auth.ts`,每個端點都掛 `partnerAuthMiddleware`),但 self-hosted 認證是 namespace 明碼。導致 Claude Code 連 self-hosted MCP 一律 401。**CLI 全通**(走 cypher-executor,已支援 `MULTI_TENANT=false`)。
|
||||
|
||||
**根因**:MCP 的 `partner-auth.ts` 沒跟上 cypher-executor 的 self-hosted 認證改版。這是 `07-thin-shell.md §4` 已記的「MCP 帳號來源違反」的具體症狀(SDD proposal 已存在:`docs/3-specs/arcrun/sdk-and-website/mcp-account-source.md`)。
|
||||
|
||||
**診斷證據(curl `arcrun-mcp.leo21c.workers.dev/mcp`,mira CC 實測)**:
|
||||
- GET /mcp → 200(worker 活);MCP initialize 無 auth → 401;Bearer ak_(舊 uncle6 key)→ 401 Invalid partner key;Bearer leo(namespace 明碼)→ 401;X-Namespace header → 401
|
||||
- cypher-executor / → 200(CLI 走這條,通)
|
||||
- `acr mcp-setup` 生成的 `.mcp.json` 是裸的(無 headers),就算有 key 也沒地方帶
|
||||
|
||||
**修法(三選一,arcrun 端決策)**:
|
||||
1. MCP worker 加 self-hosted 認證:接受 namespace 明碼(與 cypher-executor 一致),或 self-hosted 模式 MCP 免 partner key。
|
||||
2. `acr mcp-setup` 把有效認證寫進 `.mcp.json` headers(前提:self-hosted 有可用 key 機制)。
|
||||
3. 對齊完成前,官方文件明說 self-hosted MCP 暫不可用、請用 CLI(避免使用者困惑)。
|
||||
|
||||
**影響範圍**:任何 self-hosted dogfood(mira、未來 product)都踩,非 mira 獨有。屬 arcrun 框架側待修,走 SDD 協議(對應 `mcp-account-source.md`,動工前宣告)。
|
||||
**mira 現況**:全靠 acr CLI 即可推進,不卡。`.mcp.json` 留著等上游修好自動能連。
|
||||
|
||||
---
|
||||
|
||||
### 3b-2. ⚠️ 第一次端到端實測:修補 code 對,但 `MULTI_TENANT` 沒注入 MCP worker(2026-06-14 晚,mira 推 leo21c + 總管核實)
|
||||
|
||||
mira 把 release@main 推上 leo21c(deployment 14:31,version 7de919d6),**仍 401 `Invalid or expired partner key`**。總管核實了真因(不是 code bug,是部署機制 bug):
|
||||
|
||||
- ✅ **修補 code 在且正確**:`mcp/src/middleware/partner-auth.ts:19` `if (c.env.MULTI_TENANT === 'false')` 確實擋在 partner-key 查詢之前。
|
||||
- ☠️ **真因:`mcp/wrangler.toml` 的 `MULTI_TENANT = "false"` 是注釋掉的(第 13-14 行 `# [vars]` / `# MULTI_TENANT`)**。部署後 worker 的 `c.env.MULTI_TENANT === undefined ≠ 'false'` → if 不成立 → 走 partner-key 查詢 → 401。
|
||||
- 🔍 **更深層:self-hosted 部署(`acr init`/`acr update`)沒把 `MULTI_TENANT=false` 注入 MCP worker 的 vars**。`cli/src/` grep `MULTI_TENANT` 只有 config 定義 + 註釋,**無「部署時注入 worker env」的 code**。對照:cypher-executor 通是因它把 key 當不驗證 opaque(不依賴 MULTI_TENANT);MCP 依賴此 env 才走 namespace 分支,故漏注入就斷。
|
||||
- 類比:self-hosted-init.md 注入了 `WORKER_SUBDOMAIN`,但**漏注入 `MULTI_TENANT`**(同一類部署注入機制的缺口)。
|
||||
|
||||
**修法(arcrun 端,二選一或都做)**:
|
||||
1. `acr init/update` 部署 MCP(及需要的 worker)時,依 config `multi_tenant: false` 注入 `MULTI_TENANT=false` 到 worker vars(與注入 WORKER_SUBDOMAIN 同套機制,self-hosted-init.md)。
|
||||
2. 短期:文件指引 self-hosted 用戶手動 `wrangler secret put MULTI_TENANT`(或取消 mcp/wrangler.toml 那兩行註釋)——但這違反「用戶零填寫」,①才是正解。
|
||||
|
||||
**驗收**:leo21c MCP worker 設好 MULTI_TENANT=false 後,`curl -H "Authorization: Bearer leo" .../mcp` initialize → 200(非 401)。
|
||||
|
||||
## 4. arcrun-gui 併入 + arcrun.dev 降官網
|
||||
|
||||
arcrun-gui 不再獨立,GUI 併入 arcrun repo(用戶下載即有)。arcrun.dev 降級為框架官網,移除 /mira/ 寄居(Mira 搬 mira.uncle6.me)。
|
||||
|
||||
## 5. arcrun-mcp → 已整進 arcrun/mcp,**直接關掉**(leo 2026-06-13)
|
||||
|
||||
勘查確認(2026-06-13):`arcrun/mcp` 已是真身,**比 matrix/arcrun-mcp 多 `arcrun_recipe.ts` + `arcrun_whoami.ts`,且 matrix/arcrun-mcp 無任何 arcrun/mcp 缺的東西**(`Only in arcrun-mcp` 為空)= **已全部整進去**。
|
||||
動作:`matrix/arcrun-mcp` **直接關掉**——封存進 `_archive/` 即可,不遷帳號(舊 repo richblack/arcrun-mcp 留歷史)。無需整合,arcrun/mcp 就是現役。
|
||||
|
||||
## 6. KBDB 資料層遷移:3 個框架缺口(mira CC 2026-06-14 回報 + 總管核實,擋 mira 遷移)
|
||||
|
||||
mira 實測 leo21c self-hosted KBDB:**好消息 entries 表 block-compatible**(content/entry_type/parent_id/page_name/refs_json/tags_json/task_status/metadata_json = mira block 模型),河道/wiki/triplet 可直接落 entries,不必改資料模型。但卡 3 個 arcrun 缺口:
|
||||
|
||||
**① 主缺口(擋遷移):cypher proxy 漏 `/kbdb/entries`** — 核實屬實。
|
||||
- `cypher-executor/src/routes/kbdb-proxy.ts` 只實作 `/kbdb/templates` + `/kbdb/records`(注釋自稱含 entries,實際沒有)。基本盤 `arcrun/kbdb` 有 `/entries`(index.ts:18),proxy 沒轉發。
|
||||
- 結果 mira 三者湊不齊:直連 kbdb worker /entries=有 block CRUD 但裸開無隔離;cypher /kbdb/*=有認證+owner_id 隔離但無 /entries。
|
||||
- **修法**:比照 `/kbdb/records` 的 owner_id 注入模式,補 `/kbdb/entries`(POST/GET filters/GET :id/PATCH/DELETE)。守鐵律(只轉發 API,不開 SQL/建表)。補好 mira `_kbdb_client.py` 改走 `cypher.leo21c/kbdb/entries` + X-Arcrun-API-Key namespace 隔離 → 完成解耦。
|
||||
|
||||
**② MCP 的 KBDB service binding 壞** — 核實屬實。
|
||||
- mcp/wrangler.toml 有 `{ binding="KBDB", service="arcrun-kbdb" }`,kbdb-client 用 `env.KBDB.fetch`,但 mira 報 `Cannot read properties of undefined (reading 'fetch')` = **`env.KBDB` undefined**(self-hosted 部署時 binding 沒正確建/worker 名對不上 leo21c 的 kbdb)。
|
||||
- 連帶:官方回報管道 `arcrun_report_feedback`(MCP tool)也因此送不出 → 這份回報只能靠總管轉。**修這個才恢復 self-hosted 的 MCP 回報能力。**
|
||||
- **修法**:self-hosted 部署確保 KBDB service binding 正確指向 leo21c kbdb worker(或改 HTTP fetch via KBDB_BASE_URL,與插件同模式,避免 self-hosted service binding 名稱耦合)。
|
||||
|
||||
**③(非阻擋)mcp-setup namespace 不一致** — 核實屬實。
|
||||
- `cli/src/commands/mcp-setup.ts:53` 用 `config.api_key`。self-hosted 若 api_key 存的是舊 ak_(非 namespace),則 MCP 與 CLI 讀寫不同分區。
|
||||
- **修法**:self-hosted 下 mcp-setup 優先用 NAMESPACE(config.namespace),與 CLI 同一分區。
|
||||
|
||||
對應 SDD:mira 端記在 mira SDD 14-A(標 🚧 待對端);arcrun 端走協議(kbdb-proxy 屬既有 SDD 範圍)。
|
||||
|
||||
---
|
||||
|
||||
## 6b. ⚠️ 部署斷層:code 已補但 leo21c 未重部署(總管本地模擬核實 2026-06-15)
|
||||
|
||||
總管不靠 Hetzner、從本機直接 curl leo21c 端點驗證,發現 **①② 的 code 已 commit 進 arcrun repo,但對應 worker 沒部署到 leo21c CF**:
|
||||
|
||||
| 證據(本機 curl leo21c,namespace=leo) | 結果 | 判讀 |
|
||||
|---|---|---|
|
||||
| `GET cypher/kbdb/templates` | 200 | cypher-executor 活著(舊版) |
|
||||
| `GET cypher/kbdb/entries` | **404** | 新 route 未上線(`b1e302b` 的 `/kbdb/entries` 沒部署) |
|
||||
| `GET cypher/kbdb/records` | **404** | 連既有 records proxy 都 404 → leo21c 上的 cypher 版本落後 |
|
||||
| 直連 `kbdb.leo21c/entries`(裸開) | 200 | kbdb worker 本體活著,entries 表在 |
|
||||
| arcrun repo `kbdb-proxy.ts` | 含完整 `/kbdb/entries` CRUD(行 144-184) | **source 正確,純粹是沒 deploy** |
|
||||
|
||||
**結論**:缺口①②的 code 修正屬實(commit `b1e302b` `/kbdb/entries` + `1af7655` KBDB service binding),但**卡在「部署到 leo21c」這一步**。在 leo21c cypher-executor 重新部署前,mira `_kbdb_client.py` 即使改好也 smoke 必 404。
|
||||
|
||||
**交棒 task(arcrun CC,依協議走既有 SDD + 鐵律「部署繞開 GitHub、wrangler 直推 CF」)**:
|
||||
1. 確認 leo21c 帳號(`CLOUDFLARE_API_TOKEN` 指 leo21c `51a01bfa…`)下 cypher-executor 是哪個版本、為何落後(`acr update` 漏部署 cypher?還是只部署了部分 worker?)。
|
||||
2. 重新部署 cypher-executor(+ 確認 mcp worker 的 KBDB service binding 一併上線,缺口②)到 leo21c。
|
||||
3. 自驗:`curl -H 'X-Arcrun-API-Key: leo' https://arcrun-cypher-executor.leo21c.workers.dev/kbdb/entries?limit=1` 應回 200(非 404)。回 200 才算缺口①真正清空,mira 14-A 才解鎖。
|
||||
|
||||
**接力鏈**:arcrun 部署 cypher(6b)→ 端點 200 → mira 改 `_kbdb_client.py`(14A.1)→ smoke 讀寫 leo21c → 解耦完成。
|
||||
|
||||
### 6b-解決(arcrun CC 2026-06-15,已部署 + 自驗 200)
|
||||
|
||||
**根因不是 GitHub lag**(`origin/main` == 本地 4d6e77f,含 `/kbdb/entries` route)。兩層真因:
|
||||
1. **`acr update` 的 content-hash manifest 跳過機制**(`cli/src/lib/deploy.ts:198-225`)把 cypher 當「未變動」跳過 → 落後。解:`acr update --force` 清空 manifest 強制全部重部。
|
||||
2. **`.env` line 3 的 `CLOUDFLARE_ACCOUNT_ID=58309bb9…`(官方帳號)被 CLI 載入並覆蓋 config.yaml 的 leo21c `51a01bfa…`**(env > 全域 config,`config.ts:174`)→ leo21c token 對官方帳號認證 → KV 解析「Authentication error」→ update 中止。解:部署時 `CLOUDFLARE_ACCOUNT_ID=51a01bfa… node cli/dist/index.js update --force` 強制 account 對齊 leo21c token。
|
||||
- ⚠️ **遺留陷阱**:repo `.env` 是「官方帳號」部署脈絡用的;對 leo21c self-hosted 部署必須覆蓋 `CLOUDFLARE_ACCOUNT_ID`,否則 leo21c token vs 官方 account 不匹配。見記憶 [[cf-account-official-vs-loadtest]]。
|
||||
|
||||
**部署結果**:23/23 worker 全部 ✓(含 cypher-executor / kbdb / mcp),seed ✓(10 API + 23 auth recipe),cron index migrate ✓。用本地 build CLI 1.3.12(全域 acr 仍 1.3.11,未 npm publish)。
|
||||
|
||||
**自驗(本機 curl leo21c,namespace=leo)**:
|
||||
- `GET /kbdb/entries?limit=1` → **200** `{"success":true,"entries":[],"count":0}`(真轉發 kbdb worker,非假綠)✅ ← 缺口①清空,**mira 14-A 解鎖**
|
||||
- `GET /kbdb/templates` → 200 ✅
|
||||
- `GET /kbdb/records?limit=1` → **404(非回歸,by design)**:proxy 只有 `POST /records`、`GET /records/by-template/:t`、`GET /records/:id`,**本就無 bare list route**。HANDOFF 原以 records 404 當「cypher 舊版」訊號,但該路由從未存在。
|
||||
- 缺口②:MCP `initialize`(`Bearer leo`)→ **200**(非 401,`MULTI_TENANT=false` 已注入,KBDB binding 隨 mcp worker 上線)✅
|
||||
|
||||
---
|
||||
|
||||
> 每項動工前依 `.claude/rules/00-sdd-protocol.md` 宣告已讀 SDD。本 HANDOFF 是「有哪些事」,不是「繞過 SDD 的捷徑」。
|
||||
@@ -0,0 +1,120 @@
|
||||
# 交辦文件:修復 Mira 的 arcrun workflow(給 Mira 的 CC)
|
||||
|
||||
> 建立:2026-06-03(由 arcrun 端的 CC 撰寫)
|
||||
> 對象:接手修復 mira 的 CC
|
||||
> 位置:`/Users/youlinhsieh/Documents/tech_projects/InkStoneCo/polaris/mira/arcrun/*.yaml`
|
||||
>
|
||||
> **這份文件已把調查做完**:每個 workflow 用的 component 都對照過 arcrun prod 現況,
|
||||
> 標清楚「哪些不用改、哪些要改、哪些要降級」。你照著改即可,不必重跑調查。
|
||||
|
||||
---
|
||||
|
||||
## 0. 為什麼 mira 壞了(一句話)
|
||||
|
||||
arcrun 做了大整修:把「打固定 endpoint 卻被做成零件」的**假零件降級成 recipe + 刪掉零件目錄**
|
||||
(33 → 22 個零件,DECISIONS §1:零件 = endpoint 薄殼,打固定 API 的是 recipe 不是零件)。
|
||||
mira 當初**自己把一堆東西錯做成假零件**,整修後那些零件名的解析方式變了,所以 mira workflow 斷了。
|
||||
|
||||
好消息:**arcrun prod(cypher.arcrun.dev)活著、降級後的 recipe 都在 KV**(已驗證),
|
||||
mira 大部分 workflow 只需小改,不需重寫。
|
||||
|
||||
---
|
||||
|
||||
## 1. 前提(已驗證,你不用重查)
|
||||
|
||||
- ✅ cypher-executor prod 活著(`cypher.arcrun.dev`)
|
||||
- ✅ 降級 recipe 在 prod KV:`kbdb_get` / `kbdb_create_block` / `kbdb_patch_block` /
|
||||
`kbdb_ingest` / `kbdb_delete` / `telegram_send` / `gmail_send` / `google_sheets_*` / `line_notify_send`
|
||||
- mira 跑在你(richblack)的 prod 帳號上,**不依賴 self-host installer**(那是給外部工程師的另一條線)
|
||||
|
||||
---
|
||||
|
||||
## 2. 逐 component 分類(mira 6 個 workflow 全部掃過)
|
||||
|
||||
| component | 狀態 | 動作 |
|
||||
|---|---|---|
|
||||
| `kbdb_get` | ✅ prod recipe 存在 | **不用改**(`component: kbdb_get` 走解析鏈 step 6 查 `recipe:kbdb_get`,正確)|
|
||||
| `kbdb_create_block` | ✅ prod recipe 存在 | **不用改** |
|
||||
| `kbdb_patch_block` | ✅ prod recipe 存在 | **不用改** |
|
||||
| `cron` | ✅ 引擎能力 | **不用改** |
|
||||
| `http_request` | ✅ primitive | **不用改** |
|
||||
| `if_control` / `set` / `filter` | ✅ primitive | **不用改** |
|
||||
| `trigger_workflow` | ✅ 平台 orchestration | **不用改** |
|
||||
| `comp_passthrough` | ✅ 引擎內建純函式(constants.ts:43 `(ctx)=>ctx`)| **不用改** |
|
||||
| **`telegram`** | ⚠️ prod **沒有** `telegram` recipe,只有 `telegram_send` | **要改**:`component: telegram` → `component: telegram_send`(見 §3)|
|
||||
| **`claude_api`** | ❌ 錯做成零件(非薄殼)| **要降級**(見 §4)|
|
||||
| **`kbdb_upsert_block`** | ❌ 錯做成零件(非薄殼)| **要降級**(見 §4)|
|
||||
|
||||
---
|
||||
|
||||
## 3. 要改:`telegram` → `telegram_send`
|
||||
|
||||
**影響的檔**(4 個用到 `component: telegram`):
|
||||
- `project_detector.yaml`
|
||||
- `agent_feedback_weekly_review.yaml`
|
||||
- `wiki_synthesis.yaml`
|
||||
- `wiki_giveup_scanner.yaml`
|
||||
|
||||
**改法**:把 `component: telegram` 改成 `component: telegram_send`。
|
||||
|
||||
⚠️ **不只是改名**——`telegram_send` recipe 的介面:endpoint 是
|
||||
`https://api.telegram.org/bot{{auth.bot_token}}/sendMessage`,body 帶 `chat_id` + `text`,
|
||||
auth 走 `auth_service: telegram`(static_key path 注入)。確認 mira 的節點 config 傳的是
|
||||
`chat_id` / `text`,且有設好 telegram 的 credential(`acr creds push`)。
|
||||
|
||||
---
|
||||
|
||||
## 4. 要降級:`claude_api` / `kbdb_upsert_block`(錯做成零件)
|
||||
|
||||
> ⚠️ **這兩個不是「待刪」,是「做錯了」**(richblack 2026-06-03 定性):
|
||||
> 它們**不是 endpoint 薄殼,是把工作流硬塞進零件**(違反 DECISIONS §1)。
|
||||
> arcrun 已把這兩個 wasm 排除在 self-host 部署來源外(不 commit 進 repo)。
|
||||
> mira 要把它們**還原成本來該有的樣子**:工作流 / recipe。
|
||||
|
||||
### 4.1 `claude_api`(影響:agent_feedback_weekly_review / project_detector / wiki_synthesis)
|
||||
|
||||
**問題**:arcrun 是「AI 呼叫的工具」,**工作流裡不該有零件回頭呼叫 LLM**(mindset §2 /
|
||||
DECISIONS:n8n 需要 AI 節點是因為它沒大腦,arcrun 的大腦就是操盤的 CC)。
|
||||
`claude_api` 把「呼叫 Claude」做成零件,方向就錯了。
|
||||
|
||||
**正解**(mira 要自己設計,arcrun 端只給方向):
|
||||
- 需要 AI 判斷/轉換的步驟,應該是**操盤的 CC(mira 自己)做**,再呼叫 workflow 做確定性的下一步。
|
||||
- 若真的需要在 workflow 內打 Claude API(例如非同步 cron 場景無 CC 在場),那它是**打一個固定外部
|
||||
endpoint(api.anthropic.com)= recipe**,不是零件 → 建一個 `claude_api` 的 **API recipe**
|
||||
(http_request + endpoint + auth_service),不是零件目錄。
|
||||
- 哪條對,mira 的 CC 要依 mira 的實際場景判斷(這是 mira 的設計決策,不是 arcrun 能替決的)。
|
||||
|
||||
### 4.2 `kbdb_upsert_block`(影響:agent_feedback_weekly_review / wiki_synthesis)
|
||||
|
||||
**問題**:upsert 的邏輯(找到則 PATCH、沒找到則 POST)被整段塞進零件。
|
||||
按 DECISIONS §1,upsert 應該是 **KBDB API 那邊提供的 endpoint**,零件/recipe 只該「驅動它」。
|
||||
|
||||
**正解(兩條,mira/KBDB 端決定)**:
|
||||
- **首選**:KBDB API 出一個 `POST /blocks/upsert` endpoint(richblack 已交 KBDB feature request)
|
||||
→ 然後在 arcrun 建一個 `kbdb_upsert` **recipe**(打那個 endpoint),mira workflow 用 recipe。
|
||||
- **過渡**:若 KBDB 還沒出 upsert endpoint,把「GET 找 → 有則 kbdb_patch_block、無則 kbdb_create_block」
|
||||
這段**用 workflow 表達**(mira workflow 層用 if/branch 串現有的 kbdb_get + patch + create recipe),
|
||||
而不是塞進一個零件。
|
||||
|
||||
---
|
||||
|
||||
## 5. 修復順序建議
|
||||
|
||||
1. **先改 telegram**(§3)——機械改名 + 確認介面,最快,4 個檔。
|
||||
2. **驗證 kbdb_* / 其他不用改的 workflow 真的跑通**——`acr run <workflow>` 或 trigger,
|
||||
確認 §2 標「不用改」的真的 2xx(誠實驗證,不假設)。
|
||||
3. **再處理 claude_api / kbdb_upsert_block 降級**(§4)——這兩個要 mira 依場景做設計決策,較花時間。
|
||||
|
||||
## 6. 驗收(客觀證據,mindset §7)
|
||||
|
||||
- 改完的 workflow `acr run` / trigger → HTTP 2xx + execution trace 證明跑通,不是口頭宣布。
|
||||
- claude_api / kbdb_upsert_block 降級後:workflow 不再引用這兩個零件名(grep 確認)。
|
||||
- 缺 credential 打不通就誠實標「未驗收:缺 X」,不 mock 充綠燈。
|
||||
|
||||
---
|
||||
|
||||
## 7. 參考
|
||||
|
||||
- arcrun 決策:`matrix/arcrun/DECISIONS.md`(§1 零件 vs recipe、§3b credential)
|
||||
- arcrun mindset:`matrix/arcrun/.claude/rules/06-mindset.md`(§1 工作流是 default、§2 AI→工具)
|
||||
- arcrun 故障/整修脈絡:`matrix/arcrun/docs/HANDOFF-self-host-harness.md`
|
||||
@@ -0,0 +1,212 @@
|
||||
# 交辦文件:完成 arcrun self-hosted harness(給接手的 CC)
|
||||
|
||||
> 建立:2026-06-01(由前一個 CC 調查後撰寫)
|
||||
> 對象:接手的外部 CC
|
||||
> 目的:把 arcrun 補到「任何 CC 在自己的 CF 帳號上 self-host 後就能順暢開發、且不可能重蹈 mira 的錯」的程度。
|
||||
>
|
||||
> **先讀**:`DECISIONS.md`(穩定決策)、`.claude/rules/06-mindset.md`(mindset)、`BACKLOG.md`(流動待辦)。
|
||||
> 本文件不取代它們,只是把「今天要做的三件事」連同已查證的實況整理好,讓你不用重跑調查。
|
||||
|
||||
---
|
||||
|
||||
## 0. 戰法已轉變(最重要的背景)
|
||||
|
||||
richblack 2026-06-01 決定:**從 SaaS 改成 self-hosted 開源策略。**
|
||||
|
||||
這直接改變 harness 的成功定義:
|
||||
|
||||
- **舊定義**:在 richblack 的 prod 帳號(`cypher.arcrun.dev`)上能跑。
|
||||
- **新定義**:**任何 CC 在自己的 CF 帳號上 `acr init --self-hosted` 後就能跑通一個含 recipe 的 workflow,而且寫錯時會被程式擋住。**
|
||||
|
||||
richblack 會用另一個 CF 帳號實測 self-host。所以「self-hosted 一鍵起得來」從「第一期重要但非阻擋項」**升為今日第一優先**。
|
||||
|
||||
### arcrun 現在的核心心智(richblack 2026-06-01 校準,比 DECISIONS §1 更硬)
|
||||
|
||||
- 核心**零件數量少、由 richblack 維護、不接受 CC 自製**(可投稿 PR,人 merge = 閘門)。
|
||||
- 其他人做的一律是 **recipe**(= http_request + 一組 YAML 設定,不用 deploy)。
|
||||
- arcrun 是**一套給 CC 的 harness**:事前提醒 CC 能用什麼 / 不能做什麼,事後用程式擋住讓它**無法犯錯**。
|
||||
- **你不用管 mira。** mira 是錯誤做法的源頭(見 §1),它自己會修。你的目標是讓**任何** CC 都能用,且絕不會發生 mira 的錯。
|
||||
|
||||
---
|
||||
|
||||
## 1. mira 故障 = 症狀樣本(已定位,不用你修)
|
||||
|
||||
mira(`/Users/youlinhsieh/Documents/tech_projects/InkStoneCo/polaris/mira/arcrun/*.yaml`)的 workflow 寫 `component: kbdb_get` / `claude_api` / `telegram` 等。這些是 **mira 當初自己錯做的「假零件」**(DECISIONS §1 判準:打固定 endpoint 的東西是假零件,該是 recipe)。
|
||||
|
||||
本次整修(BACKLOG 步驟3)已把這些假零件**降級成 recipe + 刪掉零件目錄**(registry/components 從 33 → 22)。所以 mira 斷了。
|
||||
|
||||
**這證明的事**:mira 的錯,正是當時 harness 沒擋住的漏洞。零件刪了,但 harness 還缺「**事前告訴 CC 別這樣做 + 事後擋住 CC 這樣做**」的完整機制 → 下一個 CC 還會犯同樣的錯。**這就是你要補的(§3 task 2)。**
|
||||
|
||||
---
|
||||
|
||||
## 2. 已查證的實況(你不用重查,2026-06-01 實打 prod)
|
||||
|
||||
### 2.1 降級後的 recipe 鏈路是「活的」✅
|
||||
|
||||
實打 `https://cypher.arcrun.dev/recipes`(richblack prod)確認以下 recipe 都在 KV:
|
||||
|
||||
| canonical_id | hash | endpoint | auth_service |
|
||||
|---|---|---|---|
|
||||
| `kbdb_get` | rec_4c7dcf9b | `https://kbdb.finally.click{{_path}}` | kbdb |
|
||||
| `gmail_send` | rec_cd426129 | gmail.googleapis.com/.../send | google_gmail_sa |
|
||||
| `google_sheets_append` | rec_9fd1b662 | sheets.googleapis.com{{_path}} | google_sheets_sa |
|
||||
|
||||
→ **「對的用法」(`component: kbdb_get` 走解析鏈 step 6 查 `recipe:kbdb_get`)本身是通的。** 不需要重建 recipe。
|
||||
|
||||
> 注意:這是 richblack 的 prod KV。**self-host 的新帳號 KV 是空的**,需要 seed 這些 recipe(見 §3 task 1 的 seed 步驟)。
|
||||
|
||||
### 2.2 component-loader 解析鏈(`cypher-executor/src/lib/component-loader.ts`)
|
||||
|
||||
`resolveComponent` 依序嘗試 8 層(行號近似,以實際檔案為準):
|
||||
|
||||
```
|
||||
0. 平台 orchestration 零件(trigger_workflow) line ~88
|
||||
1. 內建零件(純 JS) line ~96
|
||||
2. 外部 URL(http(s)://...) line ~100
|
||||
3. cmp_hash → WEBHOOKS KV idx → 邏輯 Worker line ~105
|
||||
4. rec_hash → RECIPES KV idx → recipe 執行 line ~115
|
||||
5. 邏輯零件 canonical_id → Service Binding (SVC_*) line ~122
|
||||
5.5 auth recipe (auth_recipe:{service}) line ~127
|
||||
6. KV recipe canonical_id → RECIPES KV → fetch 外部 API line ~130 ← kbdb_get 等降級 recipe 命中這層
|
||||
7. WASM HTTP runner(白名單 WASM_HTTP_RUNNER_IDS) line ~134
|
||||
8. 找不到 → 報錯 line ~142
|
||||
```
|
||||
|
||||
`WASM_HTTP_RUNNER_IDS` 白名單(line ~36)現只剩:`http_request` / `cron` / 4 個 `auth_*` primitive。
|
||||
→ `claude_api`、`kbdb_upsert_block`(BACKLOG 標 deferred、源碼暫留)**不在白名單也沒 recipe** → 用到它們的 workflow 會落到 step 8 報錯。這是 mira 自己的問題,不在你範圍。
|
||||
|
||||
### 2.3 `acr init --self-hosted` 現況:純手動問答,差很遠
|
||||
|
||||
`cli/src/commands/init.ts` 的 `initSelfHosted()`(line 105-131)**只是問 6 個問題後寫進 config**:
|
||||
要求 CC 自己**事先**部署好 Worker、建好 KV、再手填 Account ID / cypher URL / 兩個 KV namespace ID / WASM bucket / CF token。
|
||||
|
||||
BACKLOG 步驟7 要的是「**貼 CF token → 自動建 KV、部署 Worker、自動 workers.dev、寫回 config**」。**這是最大缺口,task 1 的主體。**
|
||||
|
||||
config 讀取端已支援 self-hosted(`cli/src/lib/config.ts:52` 已能用 `cypher_executor_url`),所以你只要把「自動部署」這段補上,config/執行端不用動。
|
||||
|
||||
### 2.4 CI/CD 已是通用掃描式(可重用於 self-host 部署)
|
||||
|
||||
`.github/workflows/deploy.yml` 掃所有含 `wrangler.toml` 的目錄自動部署(見 `.claude/rules/05-deploy-convention.md`)。
|
||||
self-host 自動部署可以參考同一套掃描邏輯(`find . -name wrangler.toml`),對每個目錄跑 `wrangler deploy`。
|
||||
|
||||
---
|
||||
|
||||
## 3. 今天要做的三件事(按序,全在 harness 主線)
|
||||
|
||||
> richblack 指示:「全部要做」(含 `acr init --self-hosted`)。
|
||||
> 三件都做完 = 今天可交付:外部 CC 能 self-host 起來、用對的方式開發、犯錯被擋。
|
||||
|
||||
### 🔴 Task 1:完成 `acr init --self-hosted` 一鍵自動化(第一優先)
|
||||
|
||||
> ✅ **實作狀態(2026-06-02,已大致完成)**:定稿形態為 **installer 模式**(richblack 拍板)——
|
||||
> 用戶只做:申請 CF 帳號 → 裝 wrangler → 裝 acr → `acr init --self-hosted`(貼 token),其餘自動。
|
||||
> 已實作(typecheck 過):`cli/src/lib/api-recipe-seeds.ts`(10 recipe 種子)、`cf-api.ts` 的
|
||||
> `CfAccountClient`(建 KV/R2/查 subdomain/驗 token)、`deploy.ts`(常數 + downloadAndDeploy)、
|
||||
> `initSelfHosted()` 改寫、`acr update`、`cypher-executor/scripts/seed-api-recipes.ts`。
|
||||
> **唯一剩餘前置(13.6)**:repo 沒有含預編譯 wasm 的 GitHub release(.wasm 不 commit,rule 05)→
|
||||
> `downloadAndDeploy()` 目前**誠實回 implemented:false 不假裝部署**(mindset §7)。建 KV/R2/seed/config
|
||||
> 已可跑;release 產製管道補上後部署即自動化。定稿設計見 SDD `self-hosted-init.md`(含 §6 前置依賴)。
|
||||
> **以下原始子步驟保留供對照**;KBDB recipe 採 Supabase 模式進 seed(richblack 2026-06-02)。
|
||||
|
||||
**目標**:CC 只需提供「CF Account ID + CF API Token」,CLI 自動完成其餘一切。
|
||||
|
||||
**SDD**:定稿 `docs/3-specs/arcrun/sdk-and-website/self-hosted-init.md`(installer 模式,已與 richblack 對齊)。
|
||||
|
||||
**子步驟**:
|
||||
1. 改 `cli/src/commands/init.ts` 的 `initSelfHosted()`:
|
||||
- 收 CF Account ID + CF API Token(要 KV Edit + Workers Scripts Edit + R2 權限)。
|
||||
- 用 CF API(或 shell out `wrangler`)**自動建 7 個 KV namespace**:WEBHOOKS / CREDENTIALS_KV / RECIPES / USERS_KV / SESSIONS_KV / ANALYTICS_KV / EXEC_CONTEXT(清單見 `.claude/rules/01-tech-stack.md` 資料儲存表)+ R2 WASM_BUCKET。
|
||||
- **自動部署所有 Worker**:cypher-executor + registry + 22 個 `.component-builds/*`。可重用 §2.4 的 `wrangler.toml` 掃描。每個 worker 的 `wrangler.toml` 已含 `workers_dev = true`(BACKLOG 步驟 P1#2 已做),部署後 workers.dev URL 自動啟用。
|
||||
- **把 cypher-executor 的 `[vars] WORKER_SUBDOMAIN` 改成 CC 自己的帳號 subdomain**(self-host 關鍵,見 P0 #9:cypher-executor 走 `arcrun-{name}.{subdomain}.workers.dev` 對內 URL)。
|
||||
- **seed 降級 recipe + auth recipe 進 RECIPES KV**:新帳號 KV 是空的。把 §2.1 那些 recipe(kbdb_get/gmail_send/...)+ auth recipe seed 寫進去。auth recipe seed 已有 `cypher-executor/scripts/seed-auth-recipes.ts`,API recipe 需確認有對應 seed 機制(routes/recipes.ts 是動態 push,可能要寫一份 seed 腳本或用 `acr recipe push`)。
|
||||
- 寫回 config(現有欄位已足夠)。
|
||||
2. **runtime secret 不進 CLI 自動化**:`ENCRYPTION_KEY` 等由 CC 自己 `wrangler secret put`(rule 05 禁止 secret 進自動化流程)。CLI 應在最後**印出提示**告訴 CC 要手動 put 哪些 secret 到哪些 worker。
|
||||
|
||||
**驗收(客觀證據,不是口頭宣布 — mindset §7)**:
|
||||
- richblack 用全新 CF 帳號跑 `acr init --self-hosted` → 全程無手動建 KV / 部署。
|
||||
- 跑完後 `acr push` 一個含 `component: kbdb_get`(或 http_request + 自建 recipe)的 workflow → trigger → HTTP 2xx + execution trace 證明跑通。
|
||||
|
||||
### 🔴 Task 2(已重定義 2026-06-01):封鎖自製零件 + recipe 入庫把關
|
||||
|
||||
> ✅ **實作狀態(2026-06-02,第一期部分完成)**:(1) 封鎖自製零件 = 靠 GitHub PR 人 merge,無需新做
|
||||
> (矛盾已釐清)。(2a) 資料外流提醒 = **既有實作已涵蓋**(recipe.ts `obtainExposureConsent` + exposure-warning.ts,
|
||||
> 非 TTY 拒絕)。(2b) 打通檢查 = **新增** `probeRecipeEndpoint`(recipe.ts,typecheck 過):push 後實打
|
||||
> endpoint,提醒級不硬擋,含 {{模板}} 誠實說明待 run 才知,401/403 標「多半缺 credential 非 bug」。
|
||||
> 公共庫 relay 檢核(--public)= 第一期後。SDD `recipe-push-gatekeeping.md` + tasks.md W2。
|
||||
|
||||
> ⚠️ **方向修正(richblack 2026-06-01)**:原 Task 2「acr validate 擋假零件名」**作廢**。
|
||||
> 理由:自製/修改零件的路已封鎖(CC 造不出零件)→「擋假零件」這件事不存在;workflow 引用
|
||||
> recipe(`component: kbdb_get`)是合法且未來唯一的擴充方式,不該被當假零件擋。
|
||||
> 把關點**從 workflow validate 移到 recipe 入庫(push)那一刻**。
|
||||
> 已動的 yaml-parser.ts(LEGAL_PRIMITIVES / findSuspectComponents)**已回退**。
|
||||
|
||||
**新目標**:
|
||||
1. **封鎖自製零件**:靠「零件投稿走 GitHub PR + 人 merge」天然閘門(DECISIONS §8)。零件數量少、
|
||||
絕大多數是 recipe → 不為零件 PR 蓋自動化把關(量少,人工檢查即可;爆量才回頭想自動化)。
|
||||
2. **recipe 入庫把關**(CC 唯一能擴充的是 recipe,一律用 push,自有庫/公共庫同一套指令):
|
||||
- **自有庫(self-hosted)= 提醒級**:(a) 資料外流提醒——會讓資料/服務對外可見的動作需人類明示同意;
|
||||
(b) 打通檢查——push 時實打 endpoint 回報 2xx 與否(誠實標原因,不假綠,不硬擋)。
|
||||
- **公共庫 = 維護者 relay 檢核**(實際打通、真收到成功回傳)— 第一期後。
|
||||
|
||||
**SDD**:已寫 design 給 richblack review →
|
||||
`docs/3-specs/component-gatekeeping/recipe-push-gatekeeping.md`(+ tasks.md W2 節)。**review 通過才動 code。**
|
||||
|
||||
**動到的檔案(待 review)**:`cli/src/commands/recipe.ts`(push 加提醒 + 打通檢查)、確認 data-exfil hook 涵蓋 recipe push 路徑。
|
||||
|
||||
**驗收**:
|
||||
- `acr recipe push` 會產對外 webhook 的東西 → 印資料外流警示 + 要人類同意;非 TTY → 拒絕。
|
||||
- `acr recipe push` endpoint 可達的 recipe → 回報「✓ HTTP 2xx」。
|
||||
- `acr recipe push` 缺 credential → 回報「⚠️ 未打通:缺 credential」(誠實),仍允許 push。
|
||||
- workflow 引用 recipe(`component: kbdb_get`)**不被任何 validate 步驟當假零件擋**。
|
||||
|
||||
### 🔴 Task 3:README 重寫成單一路徑 — harness「事前提醒」
|
||||
|
||||
**目標**:self-hosted 開源後,README 是外界 CC 唯一入口。砍掉「玩法一/二/三」三選一,講清楚單一正確路徑。
|
||||
|
||||
**子步驟**(改根 `README.md`):
|
||||
1. 砍三選一玩法,留**一條路**:`acr init --self-hosted` → 寫 workflow(primitive 串 + recipe)→ `acr push` → trigger。
|
||||
2. 明示心智(呼應 mindset §1):「零件就這固定幾個由我們維護、不接受自製;要打外部 API 就寫 recipe;要編排就寫工作流。」
|
||||
3. 連到 `.claude/rules/06-mindset.md` / arcrun-mindset Skill,讓 CC 一開始就有正確世界觀。
|
||||
|
||||
**驗收**:README 讀完,一個沒看過 arcrun 的 CC 知道:能用什麼、不能自製零件、打外部 API 要寫 recipe、怎麼 self-host 起來。
|
||||
|
||||
---
|
||||
|
||||
## 4. 今天「不要做」的(避免你走偏)
|
||||
|
||||
| 項目 | 為何不做 |
|
||||
|---|---|
|
||||
| 修 mira | richblack 明示不用管,mira 自己修 |
|
||||
| 步驟2 `acr recipe test` / relay / credits | DECISIONS §3c 明確劃為服務側、非第一期 |
|
||||
| 步驟6 搬家拆 matrix | 純 repo 整理,不影響 CC 能否用 |
|
||||
| 砍 `injectCredentials` 舊路 / `BUILTIN_CREDENTIALS_MAP` | 獨立清理,不擋交付(DECISIONS §3b / BACKLOG「第一期之後」)|
|
||||
| 新 primitive / Gherkin 真跑 / 入站認證 | richblack 已標「不要現在做」 |
|
||||
|
||||
---
|
||||
|
||||
## 5. 鐵律提醒(違反會被 hook block)
|
||||
|
||||
- 任何 code 變動前先讀對應 SDD + 在回覆開頭宣告(`.claude/rules/00-sdd-protocol.md`)。
|
||||
- `registry/components/` 下禁止 TS;cypher-executor TS 禁止 credential/auth/JWT 業務邏輯(`.claude/rules/02-forbidden.md`,hook 強制)。
|
||||
- 每完成一個 task 立刻更新對應 tasks.md / BACKLOG.md 的 `[x]`,不批次。
|
||||
- 誠實限制(mindset §7):stub / 未完成就標 unimplemented,**不假綠**;完成 = 客觀證據(exit code / HTTP status + trace),不是口頭宣布。
|
||||
|
||||
---
|
||||
|
||||
## 6. 交棒:CLAUDE.block.md / README 表達優化(InkStoneCo 總管 2026-06-15)
|
||||
|
||||
**背景**:總管與 leo 釐清 Mira 自製零件衝突時,實測一個現象——**連 Opus 讀著 `cli/harness/CLAUDE.block.md` + rules/,都把 recipe 誤解了四輪**(以為 recipe 是私有腳本、以為缺能力可用工作流拼裝補)。leo 的標準是「**Haiku 都能輕易理解**」。block 是被 `acr install-harness` 裝進各子 repo(含 mira)的**唯一入口**,細節雖在 `rules/` 但子 repo CC 預設只看 block → block 不夠清楚 = 下一個 CC 還會誤解。**這是表達缺口,不是讀者問題。**
|
||||
|
||||
**請 arcrun CC 做(改 `cli/harness/CLAUDE.block.md`,順帶對齊 §3 README)**:把現有「不要自製零件 / 打外部 API 寫 recipe」兩句,補成「該做什麼 + 做完去哪 + 已違規的怎麼退場」。三個已核實的盲點要講清楚(論證在下,成品文案你按自己 rules 寫):
|
||||
|
||||
1. **recipe 是公共資產,不是私人腳本**。`acr recipe push` = 投稿公共庫(`DEFAULT_PUBLIC_LIBRARY_URL`)。公私是**屬性**(UUID 身份模型,`cypher-executor/src/routes/recipes.ts` §7.5.5:身份=uuid、公私/author 是屬性、多作者版本並存),不是兩條路。**「發現缺某 stock recipe → 自己做一個 push 上去」是被鼓勵的,不必問人。** 現 block 只寫「寫 recipe」「不用部署」→ 讀者誤以為私有自用、不上傳(正好反了)。
|
||||
|
||||
2. **缺能力 → 補 API,不准用 recipe/工作流拼裝**。這條 rules 其實寫得很對(07-thin-shell §1 upsert 正反例、02-forbidden §5.2、01 §8),但 **block 完全沒提** → 子 repo CC 看不到。請把「缺單一 API 能力 = 去補 API endpoint,不拼裝」這條判準帶進 block(口訣:「換個介面要不要重寫?要→是能力該在 API」)。
|
||||
|
||||
3. **零件的退場路徑空白**。現 block 只說「不要自製零件」,沒說「**已經有自製零件的(如 mira 的 6 個)該怎麼辦**」。請補:已自製零件 → 映射到 stock(`claude_api`→自己做 AI 判斷別放工作流,mindset §2;`kbdb_*`→走已備的 `acr kbdb` 薄殼/MCP)或改寫成 recipe;無 stock 對應的單 API 缺口 → 寫 recipe 投稿。
|
||||
|
||||
**驗收(leo 標準)**:改完後,一個 **Haiku** 級 CC 讀 block 就能答對:recipe 要不要投稿(要)、缺 upsert 怎麼辦(補 API 不拼裝)、已有自製零件怎麼退場(映射 stock / 改 recipe)。
|
||||
|
||||
**連帶(非 arcrun 範圍,總管已記)**:「真正可複用的多步驟業務流程能否投稿共享」是 wishlist C6(InkStoneCo 頂層),與「拼裝補 API 缺口」是兩回事(C6 紅線已劃清),arcrun 不必現在處理。
|
||||
|
||||
> 注:本交棒由總管寫**判準與論證**,**成品文案 + hook(若要)由 arcrun CC 按自己 rules 產出**。總管不直接改 arcrun 實作檔(職責邊界 D9)。
|
||||
@@ -0,0 +1,340 @@
|
||||
# arcrun.dev Pages 規格
|
||||
|
||||
> **讀者**:CC(可直接照做)
|
||||
> **部署**:Cloudflare Pages + Workers
|
||||
> **語言**:英文為主,中文切換
|
||||
> **技術棧**:Astro(靜態生成)+ Cloudflare Pages + D1(使用統計)
|
||||
|
||||
---
|
||||
|
||||
## 0. 這個 Pages 的三個角色
|
||||
|
||||
1. **門面**:第一次看到 arcrun 的人,30 秒內要懂「這是什麼、對我有什麼用」
|
||||
2. **轉換漏斗**:工程師 → 試用 lib → 申請 API Key;小白 → 看榮譽牆 → 問 AI 能不能用
|
||||
3. **社群磁鐵**:榮譽牆讓工程師有動機貢獻 recipe,貢獻越多服務越多,用戶越多
|
||||
|
||||
---
|
||||
|
||||
## 1. 網站結構(五個頁面)
|
||||
|
||||
```
|
||||
arcrun.dev/
|
||||
├── / 首頁(門面 + 轉換)
|
||||
├── /docs 用法文件
|
||||
├── /integrations 榮譽牆(服務目錄)
|
||||
├── /api Swagger UI(原始 API)
|
||||
└── /changelog 版本記錄
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2. 首頁(/)
|
||||
|
||||
### 2.1 Hero Section
|
||||
|
||||
**英文**:
|
||||
```
|
||||
Stop fighting OAuth.
|
||||
One API key. Every service. Works anywhere.
|
||||
|
||||
arcrun handles Google, Notion, GitHub, Slack authentication
|
||||
so your Python / JS code doesn't have to.
|
||||
|
||||
[Get API Key — Free] [View on GitHub]
|
||||
```
|
||||
|
||||
**中文切換後**:
|
||||
```
|
||||
不要再跟 OAuth 搏鬥了。
|
||||
一個 API Key,接通所有服務,在哪跑都行。
|
||||
|
||||
[免費取得 API Key] [查看 GitHub]
|
||||
```
|
||||
|
||||
語言切換按鈕放右上角,用 `?lang=zh` query param,Cloudflare Worker 記住偏好存 cookie。
|
||||
|
||||
### 2.2 三行說清楚(Why arcrun)
|
||||
|
||||
```
|
||||
┌────────────────────┬────────────────────┬────────────────────┐
|
||||
│ Before │ │ After │
|
||||
│ │ │ │
|
||||
│ 40 行 OAuth 程式 │ →→→ │ 1 行 │
|
||||
│ GCP Console 設定 │ │ arcrun.auth.bind │
|
||||
│ debug 兩天 │ │ ("google_drive") │
|
||||
└────────────────────┴────────────────────┴────────────────────┘
|
||||
```
|
||||
|
||||
### 2.3 Code Demo(互動式 tab)
|
||||
|
||||
三個 tab 切換:Python / JavaScript / HTTP(給 n8n 小白)
|
||||
|
||||
**Python tab**:
|
||||
```python
|
||||
pip install arcrun-auth
|
||||
|
||||
from arcrun import auth
|
||||
|
||||
# 就這樣,Google Drive 認證完成
|
||||
drive = auth.bind("google_drive")
|
||||
resp = drive.get("/files")
|
||||
```
|
||||
|
||||
**JavaScript tab**:
|
||||
```javascript
|
||||
npm install arcrun-auth
|
||||
|
||||
import { auth } from 'arcrun-auth'
|
||||
|
||||
const drive = await auth.bind('google_drive')
|
||||
const resp = await drive.get('/files')
|
||||
```
|
||||
|
||||
**HTTP tab(給 n8n 用戶)**:
|
||||
```
|
||||
POST https://api.arcrun.dev/v1/auth/bind
|
||||
Authorization: Bearer YOUR_API_KEY
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"service": "google_drive",
|
||||
"secret": "{{ $env.GOOGLE_SA_JSON }}"
|
||||
}
|
||||
```
|
||||
下方加一行小字:「n8n 用戶:用 HTTP Request 節點貼上這段,不需要安裝任何東西」
|
||||
|
||||
### 2.4 數字牆(social proof)
|
||||
|
||||
```
|
||||
127 個認證服務 1,247,832 次呼叫 89 位貢獻者
|
||||
```
|
||||
|
||||
這三個數字從 D1 即時讀,每小時更新一次(Cloudflare KV cache)。
|
||||
|
||||
### 2.5 CTA
|
||||
|
||||
```
|
||||
[免費取得 API Key]
|
||||
註冊後立即可用,不需要信用卡
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. 榮譽牆(/integrations)★ 核心頁面
|
||||
|
||||
### 3.1 頁面頂部
|
||||
|
||||
```
|
||||
127 個已驗證的認證服務
|
||||
由社群工程師貢獻並測試,每個 recipe 都有真實使用數據
|
||||
|
||||
[搜尋服務...] [全部] [AI] [Google] [社群媒體] [生產力] [台灣]
|
||||
```
|
||||
|
||||
### 3.2 服務卡片
|
||||
|
||||
每個 recipe 一張卡:
|
||||
|
||||
```
|
||||
┌──────────────────────────────────────────┐
|
||||
│ [圖示] Google Drive ★ 官方 │
|
||||
│ │
|
||||
│ 認證方式:Service Account │
|
||||
│ 貢獻者:@richblack ──→ GitHub profile │
|
||||
│ 驗證日期:2026-03-15 │
|
||||
│ │
|
||||
│ 使用次數:██████████ 12,847 次 │
|
||||
│ │
|
||||
│ [查看 Recipe] [複製 Python 範例] │
|
||||
└──────────────────────────────────────────┘
|
||||
|
||||
┌──────────────────────────────────────────┐
|
||||
│ [圖示] OpenRouter │
|
||||
│ │
|
||||
│ 認證方式:API Key (Header) │
|
||||
│ 貢獻者:@some_engineer ──→ GitHub │
|
||||
│ 驗證日期:2026-04-01 │
|
||||
│ │
|
||||
│ 使用次數:██░░░░░░░░ 89 次 │
|
||||
│ │
|
||||
│ [查看 Recipe] [複製 Python 範例] │
|
||||
└──────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
badge 規則:
|
||||
- `★ 官方`:arcrun 團隊維護
|
||||
- `✓ 社群驗證`:100+ 次使用 + 30 天無錯誤回報
|
||||
- `🆕 新加入`:30 天內合併的 PR
|
||||
|
||||
### 3.3 貢獻者排行(頁面底部)
|
||||
|
||||
```
|
||||
Top Contributors
|
||||
|
||||
🥇 @some_engineer 23 個 recipe 89,234 次呼叫
|
||||
🥈 @another_dev 15 個 recipe 45,123 次呼叫
|
||||
🥉 @third_person 8 個 recipe 12,456 次呼叫
|
||||
...
|
||||
|
||||
[我也要貢獻 →] (連到 CONTRIBUTING.md)
|
||||
```
|
||||
|
||||
### 3.4 「我要貢獻」的 CTA
|
||||
|
||||
```
|
||||
找不到你要的服務?
|
||||
|
||||
大部分 API Key 類的服務,填一份 YAML 就能加進來。
|
||||
把 API 文件丟給 AI,五分鐘生成,開 PR 送出。
|
||||
|
||||
[查看 Recipe 格式] [開始貢獻]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. 用法文件(/docs)
|
||||
|
||||
### 結構
|
||||
|
||||
```
|
||||
快速開始
|
||||
├── 取得 API Key
|
||||
├── Python 安裝與第一個範例
|
||||
├── JavaScript 安裝與第一個範例
|
||||
└── 直接用 HTTP(n8n / 任何工具)
|
||||
|
||||
認證方式
|
||||
├── API Key 類服務
|
||||
├── OAuth2 類服務
|
||||
├── Google Service Account
|
||||
└── mTLS
|
||||
|
||||
進階用法
|
||||
├── 多帳號(multi-instance)
|
||||
├── 只取 token(escape hatch)
|
||||
└── 錯誤處理
|
||||
|
||||
貢獻 Recipe
|
||||
├── Recipe YAML 格式說明
|
||||
├── 讓 AI 幫你寫 Recipe
|
||||
└── 提交流程
|
||||
```
|
||||
|
||||
### 「讓 AI 幫你寫 Recipe」這一節特別重要
|
||||
|
||||
```markdown
|
||||
## 讓 AI 幫你寫 Recipe
|
||||
|
||||
把下面這段丟給 Claude / ChatGPT,
|
||||
再把目標服務的 API 文件一起貼進去:
|
||||
|
||||
---
|
||||
請根據以下 API 文件,
|
||||
生成一份符合 arcrun recipe schema 的 YAML。
|
||||
Schema 文件:https://arcrun.dev/docs/recipe-schema
|
||||
目標服務:[貼上 API 文件]
|
||||
---
|
||||
|
||||
AI 生成後,你只需要:
|
||||
1. 把 YAML 存成 recipes/community/服務名.yaml
|
||||
2. 跑 `acr recipe test 服務名.yaml`
|
||||
3. 開 PR
|
||||
|
||||
通常整個過程不超過十分鐘。
|
||||
```
|
||||
|
||||
這一節讓「貢獻門檻」從「工程師才能做」變成「任何人叫 AI 做」。
|
||||
|
||||
---
|
||||
|
||||
## 5. API 文件(/api)
|
||||
|
||||
直接嵌入 Swagger UI,連到 `https://api.arcrun.dev/swagger.json`。
|
||||
|
||||
頁面頂部加一行說明:
|
||||
```
|
||||
這是 arcrun 的原始 API。
|
||||
Python / JS lib 是它的包裝,
|
||||
任何能發 HTTP request 的工具都能直接用。
|
||||
```
|
||||
|
||||
這一句話讓 n8n 用戶、Make 用戶、甚至 Excel 用戶都知道「我也能用」。
|
||||
|
||||
---
|
||||
|
||||
## 6. 技術實作
|
||||
|
||||
### 6.1 技術選型
|
||||
|
||||
**Astro**(靜態生成)是首選,原因:
|
||||
- 頁面大部分是靜態內容(docs / recipe 卡片),Astro 的 SSG 完美對應
|
||||
- 動態數字(使用次數、貢獻者排行)用 Astro 的 `client:load` island 局部更新
|
||||
- 部署到 Cloudflare Pages 零配置
|
||||
|
||||
**不用 Next.js**,因為你已在 Cloudflare 生態,Next.js 的 SSR 在 CF Pages 有摩擦。Astro + CF Pages 是更自然的組合。
|
||||
|
||||
### 6.2 資料來源
|
||||
|
||||
| 資料 | 來源 | 更新頻率 |
|
||||
|---|---|---|
|
||||
| Recipe 清單、metadata | GitHub repo `recipes/` 目錄 | CI merge 時觸發 rebuild |
|
||||
| 使用次數 | Cloudflare D1(API call log) | 每小時從 D1 聚合 → KV cache |
|
||||
| 貢獻者排行 | 同上 | 每小時 |
|
||||
| 總呼叫次數 | 同上 | 每小時 |
|
||||
|
||||
### 6.3 多語言
|
||||
|
||||
用 Astro 的 i18n routing:
|
||||
- `/` → 英文
|
||||
- `/zh/` → 中文
|
||||
|
||||
語言切換按鈕寫入 cookie `arcrun_lang`,CF Worker 在 edge 讀 cookie 做 redirect。
|
||||
不用 JS framework 的 i18n library,保持輕量。
|
||||
|
||||
### 6.4 部署流程
|
||||
|
||||
```
|
||||
GitHub push to main
|
||||
→ GitHub Actions 跑 astro build
|
||||
→ 產出 dist/
|
||||
→ 自動部署到 Cloudflare Pages
|
||||
→ Pages 掛 arcrun.dev domain
|
||||
```
|
||||
|
||||
recipe YAML 有變動時(PR merge)額外觸發一次 rebuild。
|
||||
|
||||
---
|
||||
|
||||
## 7. CC 的實作任務
|
||||
|
||||
### Phase 1:靜態骨架(3-5 天)
|
||||
|
||||
- [ ] Astro 專案初始化,設定 CF Pages 部署
|
||||
- [ ] 首頁 Hero + Code Demo tab(靜態版,數字先寫死)
|
||||
- [ ] `/integrations` 靜態版(先手動列 5-10 個服務)
|
||||
- [ ] `/docs` 基本結構(快速開始 + Python 範例)
|
||||
- [ ] `/api` 嵌入 Swagger UI
|
||||
- [ ] 中英切換機制
|
||||
|
||||
### Phase 2:動態資料(3-5 天)
|
||||
|
||||
- [ ] D1 schema:`recipe_calls(recipe_id, count, last_updated)`
|
||||
- [ ] CF Worker:API call 時寫入 D1
|
||||
- [ ] 每小時聚合 Worker:D1 → KV cache(總數 / per recipe / per contributor)
|
||||
- [ ] 首頁數字牆:從 KV 讀即時數字
|
||||
- [ ] `/integrations` 卡片:使用次數從 KV 讀,進度條動態顯示
|
||||
|
||||
### Phase 3:社群功能(2-3 天)
|
||||
|
||||
- [ ] 貢獻者排行從 KV 讀
|
||||
- [ ] Recipe 頁面:點「查看 Recipe」展開 YAML
|
||||
- [ ] 點「複製 Python 範例」自動生成對應 code snippet
|
||||
- [ ] GitHub PR merge webhook → 觸發 Pages rebuild
|
||||
|
||||
---
|
||||
|
||||
## 8. 一個不能省的細節
|
||||
|
||||
榮譽牆的貢獻者欄位**一定要連到他的 GitHub profile**,不是只顯示名字。
|
||||
|
||||
工程師貢獻的動機之一是「這個會出現在我的公開作品集」。連到 GitHub 就意味著他的 followers 可能看到他貢獻了 arcrun,這比任何 badge 都有效。
|
||||
@@ -0,0 +1,486 @@
|
||||
# arcrun-py:Python Lib 策略分析
|
||||
|
||||
> **核心問題**:arcrun 的 auth 層要不要獨立成 Python lib?AI 會主動選它嗎?
|
||||
> **決策前提**:本地為主雲端選配 + 免費引流 SaaS + 三維度分析
|
||||
|
||||
---
|
||||
|
||||
## 0. TL;DR
|
||||
|
||||
做。但要做對——它不是「arcrun SDK」,是 **獨立的 OAuth 地獄解藥**,SaaS 帳號只是 optional power-up。
|
||||
|
||||
**產品名建議**:`arcrun-auth`(PyPI 套件名),import 時叫 `authkit` 或 `arc`。
|
||||
|
||||
**定位**:`pip install arcrun-auth` → 一行搞定 Google/GitHub/Slack/Notion/... 的認證。本地 keyring 存 secret,零註冊可用。註冊 arcrun 帳號才能多機同步 / 團隊共用 / 獲得平台代管的 OAuth App。
|
||||
|
||||
---
|
||||
|
||||
## 1. 你的直覺:為什麼這可能是大招
|
||||
|
||||
你觀察到的現象:
|
||||
- **寫個「本地檔案 → Google Drive 備份」這種小腳本**,光認證花 2 天,日後還反覆壞。
|
||||
- **這個痛點 99% 的 Python 使用者都遇過**。Stack Overflow 上 Google OAuth 的問題累計回答數十萬。
|
||||
- **AI 寫程式時,這部分它也不會寫**——訓練資料裡的標準範本就是 `InstalledAppFlow.from_client_secrets_file()`,還是要人類先去 GCP Console 建 OAuth Client、下載 JSON、設 redirect URI、加測試用戶。
|
||||
|
||||
**如果 arcrun 能把這整套濃縮成 `auth.bind("google_drive")`,它就是開發者工具裡的「自來水」——沒人在乎水源廠長怎麼運作,大家只想打開水龍頭有水喝。**
|
||||
|
||||
而且你無意中踩到一個時代轉折點:**AI 在寫程式,AI 不想 debug OAuth**。這個 lib 是給 AI 用的,比給人類用的更重要。
|
||||
|
||||
---
|
||||
|
||||
## 2. 三維度分析:AI 到底會不會選 arcrun-auth?
|
||||
|
||||
### 維度一:零註冊摩擦(最重要)
|
||||
|
||||
**現實**:AI coding assistant 預設會產出訓練資料裡最常見的程式碼。Google Drive 的標準答案就是官方 quickstart——那段 40 行的 `InstalledAppFlow` 樣板碼,加上「去 GCP Console 建 OAuth Client」的一串步驟。
|
||||
|
||||
**AI 選擇替代 lib 的條件**(觀察 Copilot / Cursor / Claude Code 的實際行為):
|
||||
|
||||
| 條件 | 是否能壓制預設答案 |
|
||||
|---|---|
|
||||
| Lib 名稱出現在 prompt 裡 | ✅ 絕對會用 |
|
||||
| 專案已有 `pyproject.toml` 宣告該 lib | ✅ 會用 |
|
||||
| 該 lib 是 PyPI 下載量 Top N(訓練資料權重高) | ⚠️ 或許 |
|
||||
| 使用者口頭說「用 XX 比較好」 | ✅ 會用(單次) |
|
||||
| 該 lib 明顯更短更容易 | ⚠️ 有機會但不保證 |
|
||||
|
||||
**結論**:**零註冊摩擦是生死線**。
|
||||
|
||||
如果 `pip install arcrun-auth` 之後必須先去註冊 arcrun 帳號、拿 API token、設環境變數——AI 看到這一步就會放棄,回去寫 Google 官方那套,因為「官方的至少不用註冊第二家廠商」。
|
||||
|
||||
**對策**:
|
||||
- 本地模式必須是**零配置可用**的 default path。
|
||||
- Secret 存在 `~/.arcrun/secrets.db`(加密,用系統 keyring 保護 master key)。
|
||||
- 註冊 arcrun 帳號是 **opt-in** 的升級,不是必要。
|
||||
- 第一次用 OAuth 時,**平台提供免費共用的 OAuth App**(下面 §3 詳細說)。
|
||||
|
||||
這個設計讓 AI 看到的心智路徑是:
|
||||
|
||||
```
|
||||
目標:備份檔案到 Google Drive
|
||||
選項 A:google-api-python-client(官方,但使用者要先去 GCP 設 OAuth)
|
||||
選項 B:arcrun-auth(pip install 完直接可用)
|
||||
```
|
||||
|
||||
在這個對比下,AI 會選 B——**特別是它被告知「使用者不想 debug 認證」或類似的 prompt 時**。
|
||||
|
||||
### 維度二:跟 stdlib / 主流 lib 的對比
|
||||
|
||||
Python Google API 的主流選項:
|
||||
|
||||
| Lib | PyPI 週下載(估) | AI 熟悉度 | 認證難度 |
|
||||
|---|---|---|---|
|
||||
| `google-api-python-client` | ~30M | 極高 | 高(要自己設 OAuth) |
|
||||
| `pydrive2` | ~200k | 中 | 高 |
|
||||
| `gspread`(只 Sheets) | ~2M | 高 | 高(OAuth 或 SA) |
|
||||
| **arcrun-auth** | 0 | 0 | **極低** |
|
||||
|
||||
**冷啟動難題**:新 lib 要進入 AI 的選擇集合,需要:
|
||||
|
||||
1. **量**:PyPI 下載 + GitHub stars 進到「被訓練資料收錄」的級別(大概 GitHub 5k+ stars 是門檻)。
|
||||
2. **品牌**:有代表性部落格文、教學影片、官方 API 文件連結到它。
|
||||
3. **簡潔**:代碼範例比主流短 5 倍以上,讓使用者「一眼就想用」。
|
||||
|
||||
**arcrun 的優勢**:
|
||||
- 你本來就在教 n8n 課程,有現成學員管道可以鋪「這是 n8n 的 Python 版 auth」。
|
||||
- 「AI 寫程式的 auth lib」是個還沒被佔領的定位詞。`langchain` 做了 LLM 層,但 auth 層還沒有明顯贏家。
|
||||
- Claude Code 對 lib 選擇特別敏感——它會實際讀 `pyproject.toml` 並尊重已有宣告。
|
||||
|
||||
**對策**:
|
||||
- **第一批 adopter 是你的學員**(n8n 課 + AI 自動化課),他們會在實戰中用,累積 GitHub issues 和 blog post。
|
||||
- **SEO 主打**:「Python Google Drive OAuth 簡化」「AI 自動化 Python 認證」這些長尾關鍵字現在沒有明顯答主。
|
||||
- **Claude Code 優化**:寫一份 `AGENTS.md` 或 `.cursorrules` 範本,示範怎麼在 prompt 裡引導 AI 選 arcrun-auth。
|
||||
|
||||
### 維度三:痛點強度(OAuth 地獄避免)
|
||||
|
||||
**量化你那兩天 debug**:
|
||||
|
||||
| 階段 | 時間成本 | 典型錯誤 |
|
||||
|---|---|---|
|
||||
| GCP 註冊 + 啟用 API | 15 min | 找不到哪個 API |
|
||||
| 建 OAuth Client ID | 15 min | Desktop / Web / iOS 選錯 |
|
||||
| 設 OAuth Consent Screen | 30 min | External / Internal 選錯;scope 加錯 |
|
||||
| 加測試用戶 | 10 min | 漏加自己的 email |
|
||||
| 寫 Python flow | 30 min | `run_local_server` vs `run_console` |
|
||||
| 第一次跑遇到 `redirect_uri_mismatch` | 30-120 min | port 衝突、URI 沒加 |
|
||||
| Token 過期處理 | 60 min | `creds.expired` 與 `refresh_token` 沒保存 |
|
||||
| Service Account 模式(如果需要) | 120 min | domain-wide delegation 設定 |
|
||||
| **合計** | **5-8 hrs(順的人)** | **2 天(不順的人,你當時的情況)** |
|
||||
|
||||
**arcrun-auth 對應版本**:
|
||||
|
||||
```python
|
||||
from arcrun import auth
|
||||
|
||||
# 首次執行:自動打開瀏覽器完成 OAuth,結果存本地 keyring
|
||||
drive = auth.bind("google_drive")
|
||||
|
||||
# 直接呼叫 API
|
||||
drive.post("/upload/drive/v3/files", params={"uploadType": "media"},
|
||||
data=open("backup.zip", "rb"))
|
||||
```
|
||||
|
||||
**時間成本:首次 2 min,之後 0 min**。
|
||||
|
||||
這個壓倒性的體驗差距是產品的核心競爭力。**只要使用者試過一次,就不會再回去寫 `InstalledAppFlow`**——即使 AI 預設會產出官方版本。
|
||||
|
||||
---
|
||||
|
||||
## 3. 關鍵設計決策
|
||||
|
||||
### 3.1 OAuth App 誰擁有?(核心問題)
|
||||
|
||||
傳統做法:使用者自己去 GCP Console 註冊自己的 OAuth App,拿 client_id/client_secret。**這就是痛點來源**。
|
||||
|
||||
arcrun-auth 要消滅這步,只有兩條路:
|
||||
|
||||
**Option A:平台提供共用 OAuth App(推薦 default)**
|
||||
- arcrun 註冊一個 Google OAuth App,命名類似「arcrun Auth Broker」。
|
||||
- 所有 arcrun-auth 使用者共用這個 App 的 client_id/secret。
|
||||
- 使用者在 Google 授權頁面看到的是「arcrun Auth Broker 想存取您的 Google Drive」。
|
||||
- **好處**:使用者零配置,arcrun 品牌曝光。
|
||||
- **成本**:Google 有 OAuth App 的限額(Verified App 才能超過 100 users),需要申請 Google OAuth Verification(要提供隱私政策、網域驗證、可能要付 $75 安全審查)。
|
||||
|
||||
**Option B:使用者 BYO OAuth App**
|
||||
- 企業客戶或注重稽核的人需要這個。
|
||||
- 在 `~/.arcrun/config.toml` 放自己的 client_id/secret。
|
||||
|
||||
**Option C:arcrun SaaS 代管**(付費)
|
||||
- 使用者註冊 arcrun 帳號,平台幫你管 OAuth App、token、團隊共用、audit log。
|
||||
- 這是付費 tier 的主要價值。
|
||||
|
||||
**建議**:A + B + C 三種都支援,默認 A;免費無限制 B;付費享受 C。
|
||||
|
||||
### 3.2 Secret 儲存層級(本地為主雲端選配)
|
||||
|
||||
```
|
||||
優先級 1 (default):本地 keyring
|
||||
- macOS Keychain / Windows Credential Manager / Linux libsecret
|
||||
- zero config,安全性靠 OS
|
||||
|
||||
優先級 2 (opt-in):本地加密檔
|
||||
- ~/.arcrun/secrets.enc
|
||||
- master key 走 keyring 或 passphrase
|
||||
- 給沒有 keyring 的環境(Docker、CI)
|
||||
|
||||
優先級 3 (opt-in):arcrun 雲端
|
||||
- 多機同步、團隊共用、audit log
|
||||
- 需註冊 arcrun 帳號
|
||||
- 本地 lib 只保存 arcrun API token,實際 service secret 存雲端
|
||||
```
|
||||
|
||||
### 3.3 Secret 初始化流程
|
||||
|
||||
**靜態 key 模式(Notion、OpenAI、Stripe...)**:
|
||||
|
||||
```bash
|
||||
# 選項 A:互動式
|
||||
$ arcrun setup notion
|
||||
? Notion Integration Token (hidden): ***
|
||||
✓ Testing connection... OK
|
||||
✓ Saved to keyring as notion/default
|
||||
|
||||
# 選項 B:環境變數
|
||||
$ export ARCRUN_NOTION_TOKEN=secret_xxx
|
||||
$ python script.py # arcrun-auth 自動讀
|
||||
|
||||
# 選項 C:程式碼內
|
||||
notion = auth.bind("notion", secret={"token": os.environ["NOTION_TOKEN"]})
|
||||
```
|
||||
|
||||
**OAuth 模式(Google、GitHub、Slack...)**:
|
||||
|
||||
```python
|
||||
drive = auth.bind("google_drive")
|
||||
# 如果是第一次:
|
||||
# 1. 本地啟動一個臨時 HTTP server (http://localhost:random_port)
|
||||
# 2. 開瀏覽器到 Google authorize URL
|
||||
# 3. 使用者點同意
|
||||
# 4. Google redirect 到 localhost,lib 接到 code
|
||||
# 5. 換 token,存 keyring
|
||||
# 6. 回傳可用的 client
|
||||
```
|
||||
|
||||
這個流程和 `InstalledAppFlow.run_local_server()` 本質上一樣——但差別是:
|
||||
- **Client ID 不用使用者自己去 GCP Console 註冊**(由 arcrun 平台提供)。
|
||||
- **Scope 由 recipe 宣告**(不用使用者自己查文件)。
|
||||
- **Token 儲存自動化**(不是散落在 `token.json`)。
|
||||
|
||||
### 3.4 Recipe 來源
|
||||
|
||||
Python lib 和 Cloudflare Worker 版本**共用同一份 recipe YAML**。
|
||||
|
||||
```
|
||||
arcrun-recipes/ # GitHub repo,公開
|
||||
├── recipes/
|
||||
│ ├── official/
|
||||
│ │ ├── google_drive.yaml
|
||||
│ │ ├── notion.yaml
|
||||
│ │ └── ...
|
||||
│ └── community/
|
||||
│ └── ...
|
||||
```
|
||||
|
||||
Python lib 啟動時檢查本地 `~/.arcrun/recipes/` 快取,過期就從 GitHub 或 arcrun 平台 API 拉最新。
|
||||
|
||||
**這是關鍵架構優勢**:recipe 寫一次,Web 和 CLI 和 Python lib 全部受益。社群貢獻一份 Notion recipe,所有 runtime 自動支援。
|
||||
|
||||
---
|
||||
|
||||
## 4. API 設計(Python 版)
|
||||
|
||||
### 4.1 最簡路徑
|
||||
|
||||
```python
|
||||
from arcrun import auth
|
||||
|
||||
# 取得認證好的 HTTP client(基於 httpx)
|
||||
client = auth.bind("google_drive")
|
||||
|
||||
# 相對 base_url 的路徑
|
||||
resp = client.get("/files", params={"q": "name = 'backup.zip'"})
|
||||
files = resp.json()["files"]
|
||||
|
||||
# 上傳
|
||||
client.post("/upload/drive/v3/files",
|
||||
params={"uploadType": "multipart"},
|
||||
files={"file": ("backup.zip", open("backup.zip", "rb"))})
|
||||
```
|
||||
|
||||
### 4.2 進階:非同步
|
||||
|
||||
```python
|
||||
from arcrun import auth
|
||||
|
||||
async with auth.bind_async("google_drive") as client:
|
||||
resp = await client.get("/files")
|
||||
```
|
||||
|
||||
### 4.3 進階:多 instance
|
||||
|
||||
```python
|
||||
# 同一個服務,多個帳號
|
||||
personal = auth.bind("google_drive", instance="personal")
|
||||
work = auth.bind("google_drive", instance="work")
|
||||
```
|
||||
|
||||
### 4.4 進階:直接取 token(給不想透過 wrapper 的情況)
|
||||
|
||||
```python
|
||||
# 取 raw access token,自己丟進任何 lib
|
||||
token = auth.get_token("google_drive")
|
||||
# 丟給 googleapiclient:
|
||||
from googleapiclient.discovery import build
|
||||
from google.oauth2.credentials import Credentials
|
||||
creds = Credentials(token=token.access_token)
|
||||
service = build("drive", "v3", credentials=creds)
|
||||
```
|
||||
|
||||
這個 escape hatch 很重要——不強制使用者放棄他熟悉的官方 lib,只是把**認證這一層**剝離出來。這是你想要的「避免麻煩直接用 arcrun 的 auth 功能」的精確實作。
|
||||
|
||||
### 4.5 服務發現
|
||||
|
||||
```python
|
||||
# CLI
|
||||
$ arcrun list
|
||||
google_drive Google Drive OAuth2
|
||||
notion Notion API Key
|
||||
github GitHub OAuth2
|
||||
openai OpenAI API Key
|
||||
...
|
||||
|
||||
# Python
|
||||
from arcrun import auth
|
||||
auth.list_services() # 回傳 dict
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. 商業模式:免費引流 → SaaS 付費
|
||||
|
||||
### 5.1 免費永久可用(本地模式)
|
||||
|
||||
| 功能 | 免費 | 付費 |
|
||||
|---|---|---|
|
||||
| `pip install arcrun-auth` | ✅ | ✅ |
|
||||
| 本地 keyring 儲存 secret | ✅ | ✅ |
|
||||
| 所有 recipe 可用 | ✅ | ✅ |
|
||||
| 平台代管 OAuth App(免自己註冊) | ✅ | ✅ |
|
||||
| 單機使用 | ✅ | ✅ |
|
||||
|
||||
### 5.2 付費 tier 提供的
|
||||
|
||||
| 功能 | 免費 | Pro ($9/mo) | Team ($29/user/mo) |
|
||||
|---|---|---|---|
|
||||
| 多機同步 secret | ❌ | ✅ | ✅ |
|
||||
| 團隊共用 credential | ❌ | ❌ | ✅ |
|
||||
| Audit log(誰在何時用了什麼 secret) | ❌ | ✅ | ✅ |
|
||||
| Secret rotation 提醒 | ❌ | ✅ | ✅ |
|
||||
| 企業 OAuth App BYO | ❌ | ✅ | ✅ |
|
||||
| SSO / SCIM | ❌ | ❌ | ✅ |
|
||||
| arcrun SaaS 整合(workflow runtime) | ❌ | ⚠️ 受限 | ✅ |
|
||||
| Priority 社群 recipe 審核 | ❌ | ✅ | ✅ |
|
||||
|
||||
**商業邏輯**:
|
||||
- 免費 tier 足夠「個人開發者 + 側邊小專案」的體驗完整。
|
||||
- 付費 tier 解決「我的小工具變公司在用」的延伸需求。
|
||||
- arcrun SaaS workflow 客戶自動享受所有 Python lib 特權。
|
||||
|
||||
### 5.3 為什麼這策略會 work?
|
||||
|
||||
參考模型:
|
||||
- **Sentry**:error tracking,開源 SDK,hosted service 付費。SDK 下載數 = 付費轉換引流。
|
||||
- **PostHog**:product analytics,同樣策略。
|
||||
- **Supabase**:auth + db,開源 self-hosted,hosted 服務付費。
|
||||
|
||||
共同特徵:
|
||||
1. SDK 免費下載、單機/本地能完整 work。
|
||||
2. 從本地升級到 hosted 是「多人/多機/審計」的自然需求。
|
||||
3. SDK 本身就是最強的 marketing(開發者口耳相傳 + 訓練資料擴散)。
|
||||
|
||||
---
|
||||
|
||||
## 6. 為什麼 AI 會主動選 arcrun-auth(機率分析)
|
||||
|
||||
讓我給你一個分層的答案。
|
||||
|
||||
### 6.1 會主動選的場景(高機率)
|
||||
|
||||
| 場景 | 原因 |
|
||||
|---|---|
|
||||
| 使用者在 prompt 說「用最簡單的方式認證」 | AI 權衡 UX,arcrun-auth 明顯勝 |
|
||||
| 專案已有 `arcrun-auth` 在 `pyproject.toml` | AI 尊重現有依賴 |
|
||||
| 使用者貼了 arcrun-auth 文件範例 | AI 會模仿 |
|
||||
| 使用者說「跳過 OAuth 設定步驟」 | arcrun-auth 是唯一答案 |
|
||||
|
||||
### 6.2 不會主動選的場景(預設行為)
|
||||
|
||||
| 場景 | 原因 |
|
||||
|---|---|
|
||||
| 使用者開新專案、無任何線索 | AI 走訓練資料中最頻繁的 `google-api-python-client` |
|
||||
| 任務是「企業級、有審計需求」 | AI 偏好主流官方 lib |
|
||||
| 任務提到特定 lib 名稱 | AI 會遵守 |
|
||||
|
||||
### 6.3 轉化策略
|
||||
|
||||
要讓 AI 的「預設路徑」變成 arcrun-auth,需要:
|
||||
|
||||
1. **種子內容**:寫 20+ 篇「用 arcrun-auth 做 X」的 blog post,讓下一輪模型訓練看到。
|
||||
2. **官方文件 SEO**:搶「python google drive oauth 簡化」等長尾關鍵字。
|
||||
3. **Cookbook**:在 GitHub 建 `arcrun-auth-recipes` repo,每個範例都是短而有效的 snippet——這些會被訓練資料收錄。
|
||||
4. **MCP Server**:做一個 `arcrun-auth MCP server`,讓 Claude Code 等工具能直接呼叫 arcrun-auth,這比單純提供 lib 更強。
|
||||
5. **教學整合**:你的 n8n 課程直接示範「從 n8n 到 arcrun-auth 的 Python 版本」。
|
||||
|
||||
---
|
||||
|
||||
## 7. 實作範圍(MVP)
|
||||
|
||||
### Phase 1:核心 lib(2-3 週)
|
||||
|
||||
- [ ] `arcrun-auth` PyPI 骨架(pyproject.toml + src layout)
|
||||
- [ ] Recipe loader(從 GitHub 或平台 API 拉 YAML)
|
||||
- [ ] `auth.bind(service_id, instance?)` → httpx Client
|
||||
- [ ] Static key primitive(Notion / OpenAI / Stripe 當試金石)
|
||||
- [ ] Keyring 整合 + 本地加密檔 fallback
|
||||
- [ ] CLI:`arcrun setup <service>`, `arcrun list`, `arcrun test`
|
||||
|
||||
### Phase 2:OAuth2(2 週)
|
||||
|
||||
- [ ] OAuth2 primitive(authorization_code + PKCE)
|
||||
- [ ] 本地 callback server(類似 `InstalledAppFlow.run_local_server`)
|
||||
- [ ] 共用平台 OAuth App 的 proxy 機制
|
||||
- Lib 呼叫 `https://auth.arcrun.com/oauth/redirect`
|
||||
- 平台把 code 交換後回傳 token
|
||||
- 或者直接把平台 client_id 硬編在 recipe 裡(更簡單但要處理配額)
|
||||
- [ ] Token refresh 自動化
|
||||
- [ ] Recipe:Google Drive / Gmail / GitHub / Slack
|
||||
|
||||
### Phase 3:Service Account(1-2 週)
|
||||
|
||||
- [ ] Google Service Account(JWT signing)
|
||||
- [ ] AWS SigV4
|
||||
- [ ] Recipe 繼承(`extends: _google_base`)
|
||||
|
||||
### Phase 4:雲端同步(2 週)
|
||||
|
||||
- [ ] `arcrun login` → 綁定雲端帳號
|
||||
- [ ] Secret sync 協議(本地加密後上傳,平台只存密文)
|
||||
- [ ] 多機同步
|
||||
- [ ] Audit log
|
||||
|
||||
### Phase 5:AI 生態整合(1-2 週)
|
||||
|
||||
- [ ] MCP server(讓 Claude Code 能直接用)
|
||||
- [ ] VS Code Extension(一鍵設定 credential)
|
||||
- [ ] `AGENTS.md` 範本(引導 AI 選 arcrun-auth)
|
||||
|
||||
---
|
||||
|
||||
## 8. 風險與坑
|
||||
|
||||
### 8.1 Google OAuth Verification
|
||||
|
||||
**問題**:共用 OAuth App 要申請 Google Verification,否則會有「未驗證 App」警告 + 100 user 上限。
|
||||
|
||||
**對策**:
|
||||
- MVP 階段接受警告頁面(使用者自己點「進階 → 前往」)。
|
||||
- 到 user 量接近 100 時申請 Verification。
|
||||
- 企業客戶走 BYO OAuth App 路徑,不受影響。
|
||||
- 若平台 OAuth App 卡關,有 fallback:lib 自動引導使用者建自己的 OAuth App(提供 CLI wizard)。
|
||||
|
||||
### 8.2 其他服務的 OAuth App 政策
|
||||
|
||||
- **GitHub**:免費建 OAuth App,無上限。✅
|
||||
- **Slack**:免費建,但安裝到使用者 workspace 需管理員同意。⚠️
|
||||
- **Microsoft / Azure**:相對嚴格,需 tenant admin consent。⚠️
|
||||
- **Notion**:Internal Integration 可以完全走 API key,免 OAuth。✅(最簡單)
|
||||
|
||||
### 8.3 keyring 在 Linux server / Docker 的問題
|
||||
|
||||
Linux server 沒 GUI keyring daemon。對策:
|
||||
- Fallback 到加密檔案(用 env var 或 CLI 互動提供 master key)。
|
||||
- Docker 場景有 `docker secret`、Kubernetes Secret,lib 支援直接讀這些來源。
|
||||
|
||||
### 8.4 競品
|
||||
|
||||
目前沒有完全對標的產品,但相鄰玩家:
|
||||
- **[keyring](https://pypi.org/project/keyring/)**:只做儲存,不做認證流程。我們用它當底層。
|
||||
- **[httpx-auth](https://pypi.org/project/httpx-auth/)**:只做認證,不做 secret 管理,也沒有 recipe。
|
||||
- **[authlib](https://pypi.org/project/authlib/)**:OAuth 實作 lib,低階,還是要自己組。
|
||||
- **各家 SDK(google-auth, slack-sdk)**:綁特定家,不 unify。
|
||||
|
||||
**arcrun-auth 的差異化定位**:
|
||||
> **Unified credential broker for AI-era Python apps**
|
||||
> 一個 lib 搞定所有服務、所有認證機制、所有 secret 儲存後端。
|
||||
|
||||
---
|
||||
|
||||
## 9. 最後的判斷
|
||||
|
||||
### 9.1 這個 lib 該不該做?
|
||||
|
||||
**該做**。原因:
|
||||
1. 你描述的痛點是真的,而且規模巨大(Python + Google API 下載量是千萬級)。
|
||||
2. 技術可行,也跟既有 arcrun 架構共用 recipe,邊際成本低。
|
||||
3. 對 arcrun SaaS 是完美引流——免費 lib 的使用者是精準的付費轉換潛在客戶。
|
||||
4. 時間窗口正確:AI 寫程式時代剛開始,這個定位還沒被佔領。
|
||||
|
||||
### 9.2 跟主 SaaS 的優先順序
|
||||
|
||||
**建議**:**主 SaaS 的 credential 系統先做(前一份規劃),arcrun-auth 當後續 Phase**。
|
||||
|
||||
原因:
|
||||
- Cloudflare Worker 版的 primitives + recipes 是基礎建設,Python lib 是其 consumer。
|
||||
- 先做 Python lib 會逼你在 recipe schema 上做二次修改,不划算。
|
||||
- 主 SaaS 的 recipe 累積到 20-30 個服務後,開放 Python lib 體驗最好。
|
||||
|
||||
時程建議:
|
||||
- **Month 1-2**:主 SaaS 的 4 個 primitive + 15 個 recipe(前一份規劃)。
|
||||
- **Month 3-4**:arcrun-auth Phase 1-2(static key + OAuth2),私人 alpha。
|
||||
- **Month 5**:公開 release,寫部落格、SEO、社群推廣。
|
||||
- **Month 6+**:雲端同步、MCP、AI 生態整合。
|
||||
|
||||
### 9.3 一句話總結
|
||||
|
||||
> **arcrun-auth 不是「arcrun 的 Python 綁定」,是「OAuth 地獄的解藥」**。SaaS 是延伸。這個敘事才能在 AI 寫程式的時代站住腳。
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
# Arcrun 推廣策略修正
|
||||
|
||||
20260418 by Leo
|
||||
|
||||
## Arcrun 的最近幾次變化
|
||||
|
||||
- Arcrun 原是 Matrix 的原子化純雲端 CF 程式框架,有 MCP 讓 AI 使用
|
||||
- 轉以 WASM + WASI + TinyGo 成為未來具有雲端、地端、邊緣端的執行能力,可以用來做到無人機等終端。
|
||||
- 再解耦成獨立的 Open Source 專案,脫離 KBDB 用 YAML 即可,允許整個 Fork,但推廣 SaaS 模式
|
||||
- 今天的變化是發現成為 Lib 和 n8n 社群節點的用法。說明如下。
|
||||
|
||||
## 推廣方式 1:寫成 Lib
|
||||
|
||||
參考文件:docs/user_requirements/arcrun/ADR-lib-and-landingPage/arcrun-py-strategy-analysis.md
|
||||
|
||||
Leo 教 n8n 時常舉例我叫 AI 幫我寫個簡單的程式把 server 的檔案備份到 Google Drive 後刪除,光是 OAuth 用 Service Account 就花了 2 天測試,後來還發生好幾次出錯重修。
|
||||
|
||||
網路設定對 vibe coder 太複雜了,看到 AI 一次次修改一次次出錯,知道一週後才穩定,而 n8n 只需要申請 credential 填入,10 分鐘搞定,差距太大了。
|
||||
|
||||
問題是,n8n 實在太慢了,全圖像界面也難以跟程式碼整合,vibe coder 就算知道了也無法整合,他會繼續用 python 就是無法忍受 n8n 的慢速、沉重,及對 AI 的不友善。
|
||||
|
||||
Arcrun 用幾個基礎 Auth 零件取代 n8n 幾百個不同的 credentials 設定,arcrun 完全可以跟程式碼整合,只是目前沒有整合工具,也就是說,工程師不想面對一個認證花好幾天,他只要用 Arcrun 打 API 就幾行解決了。
|
||||
|
||||
用這個策略,讓具有程式知識的人會:
|
||||
- 對 Arcrun 有好感,在 GitHub 按星
|
||||
- 成為 Arcrun 用戶,增加會員 base
|
||||
- 寫文分享,讓 Arcrun 被傳出去
|
||||
|
||||
反之,小白初學 vibe coding,看到老工程師說 2 天變成 10 分鐘,百行程式碼變成 2 行,也會被影響。
|
||||
|
||||
所以,把 API 包裝成 Lib 或 SDK 是個 Get Member 的好方法,不過一定要有 Email。
|
||||
|
||||
## 推廣方式 2: 讓 n8n 來 call
|
||||
|
||||
身為 n8n 講師,我非常清楚 n8n 有個大缺陷(或說是安全設計),就是它只能呼叫在工作流實例化前就寫好的程式,無法注入新的。
|
||||
|
||||
所以,雖然它可以安裝一個實作 langchain AI Agent 的 AI Agent 節點,但 AI 卻不可能在遇到問題時立刻寫個程式運行解決問題,甚至使用它內建的 task runners 或使用外部的 Python runtime 也無法。
|
||||
|
||||
但只要使用 http request 節點呼叫外部的 Arcrun tool,它就可以輸出三元組寫一個程式解決它現在遇到的問題,並且運算完後回傳,只需要取得一個 API Key。
|
||||
|
||||
## 可能的效應
|
||||
|
||||
工程師是意見領袖,他們會去按星,幫忙教育市場,而小白需要被教育,但他們是較大的社群,這些星數和聲量會在未來變成 AI 訓練素材,甚至推薦用戶使用最 AI Friendly 的 Arcrun。
|
||||
|
||||
而實際上 Arcrun 的設計讓 AI 更輕鬆。
|
||||
|
||||
## 要做什麼
|
||||
|
||||
docs/user_requirements/arcrun/ADR-lib-and-landingPage/arcrun-pages-spec.md 這是 claude.ai 寫的規劃,因為它看不到 codebase,所以你要斟酌如何開發。
|
||||
|
||||
另外,當前的 API Key 雖然很容易取得,卻有個問題,用戶無法管理,所以頁面應該要有取得 API Key 的界面,可以 Google, GitHub 等 OAuth 或 Email + Password 登入,用來管理它的 API Key 的 CRUD。
|
||||
|
||||
或許是可以外接一個 SMTP 服務來確認他的 Email 真實,SaaS 服務還是有成本,雖然成本不高。
|
||||
|
||||
另外,既然是 OpenSource,成本雖然不高,似乎可以銜接 Donate 服務?
|
||||
@@ -0,0 +1,56 @@
|
||||
// App root — screen switcher with persistent route
|
||||
const { useState, useEffect } = React;
|
||||
|
||||
const SCREENS = [
|
||||
{ id: 'landing', label: 'Landing' },
|
||||
{ id: 'auth', label: 'Auth' },
|
||||
{ id: 'dashboard', label: 'Dashboard' },
|
||||
{ id: 'keys', label: 'API Keys' },
|
||||
{ id: 'workflow', label: 'Workflow' },
|
||||
];
|
||||
|
||||
// Synonyms from sidebar ids
|
||||
const aliases = { apps: 'dashboard', workflows: 'dashboard', docs: 'landing', settings: 'keys' };
|
||||
|
||||
function App() {
|
||||
const [screen, setScreen] = useState(() => {
|
||||
const saved = localStorage.getItem('arcrun:screen');
|
||||
return saved && SCREENS.some(s => s.id === saved) ? saved : 'landing';
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
localStorage.setItem('arcrun:screen', screen);
|
||||
window.scrollTo(0, 0);
|
||||
}, [screen]);
|
||||
|
||||
const nav = (id) => {
|
||||
const resolved = aliases[id] || id;
|
||||
if (SCREENS.some(s => s.id === resolved)) setScreen(resolved);
|
||||
};
|
||||
|
||||
const Current = {
|
||||
landing: Landing,
|
||||
auth: Auth,
|
||||
dashboard: Dashboard,
|
||||
keys: ApiKeys,
|
||||
workflow: WorkflowViewer,
|
||||
}[screen];
|
||||
|
||||
return (
|
||||
<div className="app">
|
||||
<Current onNav={nav} />
|
||||
|
||||
<div className="proto-switch" role="tablist" aria-label="Screen switcher">
|
||||
{SCREENS.map(s => (
|
||||
<button key={s.id}
|
||||
className={screen === s.id ? 'active' : ''}
|
||||
onClick={() => nav(s.id)}>
|
||||
{s.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
ReactDOM.createRoot(document.getElementById('root')).render(<App />);
|
||||
@@ -0,0 +1,92 @@
|
||||
// Top nav and sidebar
|
||||
|
||||
const TopNav = ({ onNav, current }) => {
|
||||
const [scrolled, setScrolled] = React.useState(false);
|
||||
React.useEffect(() => {
|
||||
const onScroll = () => setScrolled(window.scrollY > 8);
|
||||
window.addEventListener('scroll', onScroll);
|
||||
return () => window.removeEventListener('scroll', onScroll);
|
||||
}, []);
|
||||
return (
|
||||
<nav className={`topnav ${scrolled ? 'scrolled' : ''}`}>
|
||||
<div className="flex gap-12" style={{alignItems: 'center'}}>
|
||||
<Logo onClick={() => onNav('landing')} />
|
||||
<div className="nav-links" style={{marginLeft: 20}}>
|
||||
<a>Product</a>
|
||||
<a>Docs</a>
|
||||
<a>Pricing</a>
|
||||
<a>Changelog</a>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex gap-8" style={{alignItems: 'center'}}>
|
||||
<button className="btn btn-ghost" onClick={() => onNav('auth')}>Log in</button>
|
||||
<button className="btn btn-primary" onClick={() => onNav('auth')}>
|
||||
Get started <Icon name="arrow_right" size={14} />
|
||||
</button>
|
||||
</div>
|
||||
</nav>
|
||||
);
|
||||
};
|
||||
|
||||
const Footer = ({ onNav }) => (
|
||||
<footer className="footer">
|
||||
<div className="flex gap-12" style={{alignItems: 'center'}}>
|
||||
<Logo size="sm" />
|
||||
<span>© 2026 Arcrun Labs</span>
|
||||
</div>
|
||||
<div className="footer-links">
|
||||
<a>Docs</a>
|
||||
<a>Pricing</a>
|
||||
<a>Changelog</a>
|
||||
<a>Status</a>
|
||||
<a>Privacy</a>
|
||||
</div>
|
||||
</footer>
|
||||
);
|
||||
|
||||
// App shell with sidebar for logged-in screens
|
||||
const Sidebar = ({ current, onNav }) => {
|
||||
const items = [
|
||||
{ id: 'dashboard', label: 'Dashboard', icon: 'home' },
|
||||
{ id: 'apps', label: 'Apps', icon: 'grid', count: 6 },
|
||||
{ id: 'workflows', label: 'Workflows', icon: 'workflow', count: 12 },
|
||||
{ id: 'keys', label: 'API Keys', icon: 'key' },
|
||||
{ id: 'docs', label: 'Docs', icon: 'book' },
|
||||
];
|
||||
const bottom = [
|
||||
{ id: 'settings', label: 'Settings', icon: 'settings' },
|
||||
];
|
||||
return (
|
||||
<aside className="sidebar">
|
||||
<div className="sidebar-head">
|
||||
<Logo size="md" onClick={() => onNav('landing')} />
|
||||
</div>
|
||||
<div className="sidebar-section">Workspace</div>
|
||||
{items.map(it => (
|
||||
<div key={it.id}
|
||||
className={`sidebar-item ${current === it.id ? 'active' : ''}`}
|
||||
onClick={() => onNav(it.id)}>
|
||||
<span className="sb-ico"><Icon name={it.icon} size={15} /></span>
|
||||
<span>{it.label}</span>
|
||||
{it.count != null && <span className="sb-count">{it.count}</span>}
|
||||
</div>
|
||||
))}
|
||||
<div style={{flex: 1}} />
|
||||
{bottom.map(it => (
|
||||
<div key={it.id} className="sidebar-item" onClick={() => onNav(it.id)}>
|
||||
<span className="sb-ico"><Icon name={it.icon} size={15} /></span>
|
||||
<span>{it.label}</span>
|
||||
</div>
|
||||
))}
|
||||
<div className="sidebar-foot">
|
||||
<div className="avatar-circ">MR</div>
|
||||
<div className="meta">
|
||||
<div className="name">Maya Rivera</div>
|
||||
<div className="email">maya@northwind.co</div>
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
);
|
||||
};
|
||||
|
||||
Object.assign(window, { TopNav, Footer, Sidebar });
|
||||
@@ -0,0 +1,86 @@
|
||||
// Shared primitives: icons, logo, etc.
|
||||
|
||||
const Icon = ({ name, size = 16, stroke = 1.7 }) => {
|
||||
const paths = {
|
||||
arrow_right: <path d="M5 12h14M13 6l6 6-6 6" />,
|
||||
arrow_left: <path d="M19 12H5M11 6l-6 6 6 6" />,
|
||||
plus: <path d="M12 5v14M5 12h14" />,
|
||||
copy: <><rect x="9" y="9" width="13" height="13" rx="2" /><path d="M5 15V5a2 2 0 0 1 2-2h10" /></>,
|
||||
check: <path d="M20 6L9 17l-5-5" />,
|
||||
close: <path d="M18 6L6 18M6 6l12 12" />,
|
||||
eye: <><path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z" /><circle cx="12" cy="12" r="3" /></>,
|
||||
search: <><circle cx="11" cy="11" r="7" /><path d="M21 21l-4.35-4.35" /></>,
|
||||
warn: <><path d="M10.3 3.86L1.82 18a2 2 0 001.72 3h16.92a2 2 0 001.72-3L13.7 3.86a2 2 0 00-3.4 0z" /><line x1="12" y1="9" x2="12" y2="13" /><circle cx="12" cy="17" r="0.5" fill="currentColor" /></>,
|
||||
home: <><path d="M3 10l9-7 9 7v10a2 2 0 01-2 2h-4a2 2 0 01-2-2v-5h-2v5a2 2 0 01-2 2H5a2 2 0 01-2-2V10z" /></>,
|
||||
grid: <><rect x="3" y="3" width="7" height="7" rx="1" /><rect x="14" y="3" width="7" height="7" rx="1" /><rect x="3" y="14" width="7" height="7" rx="1" /><rect x="14" y="14" width="7" height="7" rx="1" /></>,
|
||||
workflow: <><circle cx="5" cy="6" r="2" /><circle cx="19" cy="12" r="2" /><circle cx="5" cy="18" r="2" /><path d="M7 6h4a4 4 0 014 4v0m0 4a4 4 0 01-4 4H7" /></>,
|
||||
key: <><circle cx="7.5" cy="15.5" r="4.5" /><path d="M10.68 12.32L21 2M17 6l3 3M15 8l3 3" /></>,
|
||||
book: <><path d="M2 3h6a4 4 0 014 4v14a3 3 0 00-3-3H2zM22 3h-6a4 4 0 00-4 4v14a3 3 0 013-3h7z" /></>,
|
||||
settings: <><circle cx="12" cy="12" r="3" /><path d="M19.4 15a1.65 1.65 0 00.33 1.82l.06.06a2 2 0 01-2.83 2.83l-.06-.06a1.65 1.65 0 00-1.82-.33 1.65 1.65 0 00-1 1.51V21a2 2 0 01-4 0v-.09A1.65 1.65 0 009 19.4a1.65 1.65 0 00-1.82.33l-.06.06a2 2 0 01-2.83-2.83l.06-.06a1.65 1.65 0 00.33-1.82 1.65 1.65 0 00-1.51-1H3a2 2 0 010-4h.09A1.65 1.65 0 004.6 9a1.65 1.65 0 00-.33-1.82l-.06-.06a2 2 0 012.83-2.83l.06.06A1.65 1.65 0 009 4.6a1.65 1.65 0 001-1.51V3a2 2 0 014 0v.09A1.65 1.65 0 0015 4.6a1.65 1.65 0 001.82-.33l.06-.06a2 2 0 012.83 2.83l-.06.06A1.65 1.65 0 0019.4 9v0a1.65 1.65 0 001.51 1H21a2 2 0 010 4h-.09a1.65 1.65 0 00-1.51 1z" /></>,
|
||||
chevron_right: <path d="M9 6l6 6-6 6" />,
|
||||
chevron_down: <path d="M6 9l6 6 6-6" />,
|
||||
external: <><path d="M18 13v6a2 2 0 01-2 2H5a2 2 0 01-2-2V8a2 2 0 012-2h6" /><path d="M15 3h6v6M10 14L21 3" /></>,
|
||||
trash: <><polyline points="3 6 5 6 21 6" /><path d="M19 6l-1 14a2 2 0 01-2 2H8a2 2 0 01-2-2L5 6M10 11v6M14 11v6" /></>,
|
||||
spark: <path d="M12 3l2.5 6.5L21 12l-6.5 2.5L12 21l-2.5-6.5L3 12l6.5-2.5L12 3z" />,
|
||||
bolt: <path d="M13 2L3 14h9l-1 8 10-12h-9l1-8z" />,
|
||||
github: <path d="M12 2C6.48 2 2 6.48 2 12c0 4.42 2.87 8.17 6.84 9.5.5.08.66-.22.66-.48v-1.7c-2.78.6-3.36-1.34-3.36-1.34-.46-1.15-1.12-1.46-1.12-1.46-.92-.62.07-.6.07-.6 1.01.07 1.55 1.04 1.55 1.04.9 1.54 2.36 1.1 2.94.84.09-.65.35-1.1.64-1.35-2.22-.25-4.55-1.11-4.55-4.94 0-1.09.39-1.98 1.03-2.68-.1-.25-.45-1.27.1-2.65 0 0 .84-.27 2.75 1.02A9.5 9.5 0 0112 6.8c.85 0 1.7.11 2.5.33 1.9-1.3 2.75-1.02 2.75-1.02.55 1.38.2 2.4.1 2.65.64.7 1.03 1.6 1.03 2.68 0 3.84-2.34 4.69-4.57 4.93.36.31.68.92.68 1.85V21c0 .27.16.57.67.48A10 10 0 0022 12c0-5.52-4.48-10-10-10z" fill="currentColor" stroke="none" />,
|
||||
google: <><path d="M21.35 11.1h-9.17v2.73h5.24c-.23 1.41-1.69 4.13-5.24 4.13-3.15 0-5.73-2.62-5.73-5.86 0-3.24 2.58-5.86 5.73-5.86 1.8 0 3 .77 3.69 1.43l2.5-2.4C16.95 3.74 14.8 2.8 12.18 2.8c-5.26 0-9.53 4.25-9.53 9.5s4.27 9.5 9.53 9.5c5.51 0 9.15-3.87 9.15-9.32 0-.63-.07-1.1-.15-1.38z" fill="currentColor" stroke="none" /></>,
|
||||
share: <><circle cx="18" cy="5" r="3" /><circle cx="6" cy="12" r="3" /><circle cx="18" cy="19" r="3" /><path d="M8.59 13.51l6.83 3.98M15.41 6.51l-6.82 3.98" /></>,
|
||||
download: <><path d="M21 15v4a2 2 0 01-2 2H5a2 2 0 01-2-2v-4" /><polyline points="7 10 12 15 17 10" /><line x1="12" y1="15" x2="12" y2="3" /></>,
|
||||
zoom_in: <><circle cx="11" cy="11" r="7" /><line x1="21" y1="21" x2="16.65" y2="16.65" /><line x1="11" y1="8" x2="11" y2="14" /><line x1="8" y1="11" x2="14" y2="11" /></>,
|
||||
zoom_out: <><circle cx="11" cy="11" r="7" /><line x1="21" y1="21" x2="16.65" y2="16.65" /><line x1="8" y1="11" x2="14" y2="11" /></>,
|
||||
maximize: <><path d="M8 3H5a2 2 0 00-2 2v3M21 8V5a2 2 0 00-2-2h-3M3 16v3a2 2 0 002 2h3M16 21h3a2 2 0 002-2v-3" /></>,
|
||||
slack: <><rect x="13" y="2" width="3" height="8" rx="1.5" /><rect x="2" y="13" width="8" height="3" rx="1.5" /><rect x="14" y="14" width="8" height="3" rx="1.5" /><rect x="8" y="8" width="3" height="8" rx="1.5" /></>,
|
||||
database: <><ellipse cx="12" cy="5" rx="9" ry="3" /><path d="M3 5v7c0 1.66 4.03 3 9 3s9-1.34 9-3V5M3 12v7c0 1.66 4.03 3 9 3s9-1.34 9-3v-7" /></>,
|
||||
mail: <><rect x="2" y="4" width="20" height="16" rx="2" /><path d="M2 6l10 7 10-7" /></>,
|
||||
filter: <path d="M3 4h18l-7 9v6l-4-2v-4L3 4z" />,
|
||||
star: <polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2" />,
|
||||
linear: <><rect x="3" y="3" width="18" height="18" rx="4" /><path d="M7 11l5 5M7 15l3 3M7 7l10 10M11 7l6 6M15 7l2 2" /></>,
|
||||
clock: <><circle cx="12" cy="12" r="9" /><polyline points="12 7 12 12 16 14" /></>,
|
||||
send: <><path d="M22 2L11 13M22 2l-7 20-4-9-9-4 20-7z" /></>,
|
||||
terminal: <><path d="M4 17l6-6-6-6M12 19h8" /></>,
|
||||
logout: <><path d="M9 21H5a2 2 0 01-2-2V5a2 2 0 012-2h4M16 17l5-5-5-5M21 12H9" /></>,
|
||||
};
|
||||
|
||||
return (
|
||||
<svg width={size} height={size} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={stroke} strokeLinecap="round" strokeLinejoin="round" style={{display: 'block', flexShrink: 0}}>
|
||||
{paths[name]}
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
|
||||
// Arcrun wordmark — custom "arc" glyph made of an arc stroke + ascending dot/node
|
||||
const Logo = ({ size = 'md', onClick }) => {
|
||||
const dims = size === 'sm' ? { w: 18, h: 18, f: 10 } : size === 'lg' ? { w: 28, h: 28, f: 14 } : { w: 22, h: 22, f: 12 };
|
||||
return (
|
||||
<div className="logo" onClick={onClick}>
|
||||
<span className="logo-mark" style={{width: dims.w, height: dims.h}}>
|
||||
<svg width={dims.w} height={dims.h} viewBox="0 0 24 24" fill="none">
|
||||
<path d="M5 17 Q 12 4, 19 17" stroke="white" strokeWidth="2.4" strokeLinecap="round" fill="none" opacity="0.95" />
|
||||
<circle cx="19" cy="17" r="2.2" fill="white" />
|
||||
</svg>
|
||||
</span>
|
||||
<span>Arcrun</span>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// App icon with gradient background
|
||||
const AppIcon = ({ tone = 'indigo', children, size = 38 }) => {
|
||||
const tones = {
|
||||
indigo: 'linear-gradient(135deg, #6366F1, #8B5CF6)',
|
||||
orange: 'linear-gradient(135deg, #F59E0B, #EF4444)',
|
||||
green: 'linear-gradient(135deg, #10B981, #22C55E)',
|
||||
pink: 'linear-gradient(135deg, #EC4899, #8B5CF6)',
|
||||
blue: 'linear-gradient(135deg, #3B82F6, #06B6D4)',
|
||||
slate: 'linear-gradient(135deg, #475569, #334155)',
|
||||
amber: 'linear-gradient(135deg, #F59E0B, #D97706)',
|
||||
};
|
||||
return (
|
||||
<div className="app-icon" style={{ background: tones[tone], width: size, height: size, color: 'white' }}>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
Object.assign(window, { Icon, Logo, AppIcon });
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,128 @@
|
||||
const ApiKeys = ({ onNav }) => {
|
||||
const [newKeyCopied, setNewKeyCopied] = React.useState(false);
|
||||
const [keys, setKeys] = React.useState([
|
||||
{ id: 'k_dev', name: 'Local Development', prefix: 'ar_dev_', created: 'Mar 12, 2026', lastUsed: '2 min ago', active: true },
|
||||
{ id: 'k_prod', name: 'Production — Northwind API', prefix: 'ar_live_', created: 'Feb 3, 2026', lastUsed: '12 sec ago', active: true },
|
||||
{ id: 'k_staging', name: 'Staging — Vercel', prefix: 'ar_test_', created: 'Jan 28, 2026', lastUsed: '4 hours ago', active: true },
|
||||
{ id: 'k_ci', name: 'CI/CD (GitHub Actions)', prefix: 'ar_live_', created: 'Jan 10, 2026', lastUsed: 'Yesterday', active: false },
|
||||
{ id: 'k_old', name: 'Legacy — Zapier import', prefix: 'ar_live_', created: 'Nov 4, 2025', lastUsed: '3 weeks ago', active: false, revoked: true },
|
||||
]);
|
||||
|
||||
const newKey = 'ar_live_sk_7x9Qf2vLm8nR4TpW6ZjKc3bEhN1aSyU5oP0dI';
|
||||
|
||||
const copyKey = () => {
|
||||
setNewKeyCopied(true);
|
||||
setTimeout(() => setNewKeyCopied(false), 1800);
|
||||
};
|
||||
|
||||
const toggleKey = (id) => {
|
||||
setKeys(keys.map(k => k.id === id ? { ...k, active: !k.active } : k));
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="shell">
|
||||
<Sidebar current="keys" onNav={onNav} />
|
||||
<div className="main">
|
||||
<div className="main-head">
|
||||
<div>
|
||||
<div className="crumb">
|
||||
<span>Workspace</span>
|
||||
<span className="sep"><Icon name="chevron_right" size={11} /></span>
|
||||
<span>Settings</span>
|
||||
</div>
|
||||
<h1>API Keys</h1>
|
||||
<div className="sub">Scoped credentials for calling the Arcrun API from your code and CI.</div>
|
||||
</div>
|
||||
<div className="flex gap-8">
|
||||
<button className="btn btn-secondary"><Icon name="book" size={14} /> API docs</button>
|
||||
<button className="btn btn-primary"><Icon name="plus" size={14} /> Create new key</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="main-body" style={{maxWidth: 1080}}>
|
||||
<div className="new-key-box">
|
||||
<div className="warn-row">
|
||||
<span className="warn-icon"><Icon name="warn" size={12} /></span>
|
||||
<span><strong style={{color: '#FBBF24'}}>Save this key now.</strong> For security, we won't show it again — if you lose it, you'll need to create a new one.</span>
|
||||
</div>
|
||||
<h3>Your new API key</h3>
|
||||
<p className="desc">Key named <strong style={{color: 'var(--text)'}}>"Production — Northwind API"</strong> · created just now · all scopes</p>
|
||||
<div className="key-display">
|
||||
<span className="key-val">{newKey}</span>
|
||||
<button className={`copy-btn ${newKeyCopied ? 'copied' : ''}`} onClick={copyKey}>
|
||||
<Icon name={newKeyCopied ? 'check' : 'copy'} size={12} />
|
||||
{newKeyCopied ? 'Copied' : 'Copy'}
|
||||
</button>
|
||||
</div>
|
||||
<div style={{marginTop: 14, display: 'flex', gap: 16, fontSize: 12, color: 'var(--text-mute)', alignItems: 'center'}}>
|
||||
<span className="flex gap-6" style={{alignItems: 'center'}}><Icon name="check" size={12} /> Full workspace access</span>
|
||||
<span className="flex gap-6" style={{alignItems: 'center'}}><Icon name="clock" size={12} /> Never expires</span>
|
||||
<span style={{marginLeft: 'auto'}}><span className="link">Add expiry or restrict scopes →</span></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="section-head">
|
||||
<div>
|
||||
<h2>All keys</h2>
|
||||
<div className="subtle" style={{marginTop: 2}}>{keys.filter(k => !k.revoked).length} active · {keys.filter(k => k.revoked).length} revoked</div>
|
||||
</div>
|
||||
<div className="flex gap-8">
|
||||
<button className="btn btn-secondary btn-sm"><Icon name="filter" size={12} /> Filter</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="table-wrap">
|
||||
<table className="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style={{width: '32%'}}>Name</th>
|
||||
<th>Key</th>
|
||||
<th>Created</th>
|
||||
<th>Last used</th>
|
||||
<th>Status</th>
|
||||
<th style={{width: 60, textAlign: 'right'}}></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{keys.map(k => (
|
||||
<tr key={k.id}>
|
||||
<td>
|
||||
<div style={{fontWeight: 500, fontSize: 13.5}}>{k.name}</div>
|
||||
</td>
|
||||
<td className="mono">{k.prefix}••••{k.id.slice(-4)}</td>
|
||||
<td className="dim" style={{fontSize: 12.5}}>{k.created}</td>
|
||||
<td className="dim" style={{fontSize: 12.5}}>{k.lastUsed}</td>
|
||||
<td>
|
||||
{k.revoked ? (
|
||||
<span className="pill revoked"><span className="pdot" /> Revoked</span>
|
||||
) : (
|
||||
<div className="flex gap-8" style={{alignItems: 'center'}}>
|
||||
<span className={`toggle ${k.active ? 'on' : ''}`} onClick={() => toggleKey(k.id)} />
|
||||
<span className={`pill ${k.active ? 'active' : 'idle'}`}>
|
||||
<span className="pdot" /> {k.active ? 'Active' : 'Paused'}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</td>
|
||||
<td style={{textAlign: 'right'}}>
|
||||
{!k.revoked && (
|
||||
<button className="btn btn-danger-ghost btn-sm"><Icon name="trash" size={12} /></button>
|
||||
)}
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div style={{marginTop: 18, fontSize: 12, color: 'var(--text-mute)', display: 'flex', alignItems: 'center', gap: 8}}>
|
||||
<Icon name="warn" size={12} />
|
||||
<span>Revoking a key stops all in-flight requests within 60 seconds. This cannot be undone.</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
window.ApiKeys = ApiKeys;
|
||||
@@ -0,0 +1,90 @@
|
||||
const Auth = ({ onNav }) => {
|
||||
const [mode, setMode] = React.useState('signin');
|
||||
const [email, setEmail] = React.useState('');
|
||||
const [pw, setPw] = React.useState('');
|
||||
const [remember, setRemember] = React.useState(true);
|
||||
|
||||
const submit = (e) => { e.preventDefault(); onNav('dashboard'); };
|
||||
|
||||
return (
|
||||
<div className="auth-wrap">
|
||||
<div className="hero-bg" />
|
||||
<div className="hero-bg-grid" />
|
||||
|
||||
<div style={{position: 'absolute', top: 24, left: 24, zIndex: 2}}>
|
||||
<Logo onClick={() => onNav('landing')} />
|
||||
</div>
|
||||
|
||||
<div className="auth-card">
|
||||
<h2 className="auth-h1">{mode === 'signin' ? 'Welcome back' : 'Create your account'}</h2>
|
||||
<p className="auth-sub">{mode === 'signin' ? 'Sign in to your Arcrun workspace.' : 'Start building AI workflows in minutes.'}</p>
|
||||
|
||||
<div className="tabs">
|
||||
<button className={mode === 'signin' ? 'active' : ''} onClick={() => setMode('signin')}>Sign in</button>
|
||||
<button className={mode === 'signup' ? 'active' : ''} onClick={() => setMode('signup')}>Sign up</button>
|
||||
</div>
|
||||
|
||||
<div className="oauth-row">
|
||||
<button className="oauth-btn github" onClick={() => onNav('dashboard')}>
|
||||
<Icon name="github" size={17} stroke={0} /> Continue with GitHub
|
||||
</button>
|
||||
<button className="oauth-btn google" onClick={() => onNav('dashboard')}>
|
||||
<Icon name="google" size={15} stroke={0} /> Continue with Google
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="divider">or continue with email</div>
|
||||
|
||||
<form onSubmit={submit}>
|
||||
{mode === 'signup' && (
|
||||
<div className="field">
|
||||
<label>Full name</label>
|
||||
<input className="input" type="text" placeholder="Maya Rivera" />
|
||||
</div>
|
||||
)}
|
||||
<div className="field">
|
||||
<label>Work email</label>
|
||||
<input className="input" type="email" placeholder="you@company.com" value={email} onChange={e => setEmail(e.target.value)} />
|
||||
</div>
|
||||
<div className="field">
|
||||
<div className="field-row">
|
||||
<label>Password</label>
|
||||
{mode === 'signin' && <span className="link">Forgot password?</span>}
|
||||
</div>
|
||||
<input className="input" type="password" placeholder="••••••••••" value={pw} onChange={e => setPw(e.target.value)} />
|
||||
</div>
|
||||
|
||||
{mode === 'signin' && (
|
||||
<div style={{display: 'flex', alignItems: 'center', gap: 8, fontSize: 12.5, color: 'var(--text-dim)', marginBottom: 14}}>
|
||||
<div onClick={() => setRemember(!remember)}
|
||||
style={{width: 15, height: 15, borderRadius: 4, border: '1px solid var(--line-2)',
|
||||
background: remember ? 'var(--primary)' : 'transparent',
|
||||
display: 'flex', alignItems: 'center', justifyContent: 'center', cursor: 'pointer'}}>
|
||||
{remember && <Icon name="check" size={11} />}
|
||||
</div>
|
||||
<span onClick={() => setRemember(!remember)} style={{cursor: 'pointer'}}>Keep me signed in for 30 days</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<button className="btn btn-primary auth-submit btn-lg" type="submit">
|
||||
{mode === 'signin' ? 'Sign in' : 'Create account'} <Icon name="arrow_right" size={14} />
|
||||
</button>
|
||||
</form>
|
||||
|
||||
{mode === 'signup' && (
|
||||
<p style={{fontSize: 11.5, color: 'var(--text-mute)', textAlign: 'center', marginTop: 14, lineHeight: 1.5}}>
|
||||
By signing up, you agree to our <span className="link" style={{fontSize: 11.5}}>Terms</span> and <span className="link" style={{fontSize: 11.5}}>Privacy Policy</span>.
|
||||
</p>
|
||||
)}
|
||||
|
||||
<div className="auth-foot">
|
||||
{mode === 'signin'
|
||||
? <>New to Arcrun? <span className="link" onClick={() => setMode('signup')}>Create an account</span></>
|
||||
: <>Already have an account? <span className="link" onClick={() => setMode('signin')}>Sign in</span></>}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
window.Auth = Auth;
|
||||
@@ -0,0 +1,126 @@
|
||||
const Dashboard = ({ onNav }) => {
|
||||
const apps = [
|
||||
{ id: 'digest', name: 'Weekly Digest', desc: 'Summarize customer activity into a Monday email for the revenue team.', icon: 'mail', tone: 'indigo' },
|
||||
{ id: 'triage', name: 'Support Triage', desc: 'Classify inbound tickets, attach context from the CRM, and route.', icon: 'filter', tone: 'orange' },
|
||||
{ id: 'seo', name: 'SEO Brief Generator', desc: 'Turn a keyword into a draft brief with outline, FAQs, and SERP notes.', icon: 'search', tone: 'green' },
|
||||
{ id: 'slack', name: 'Standup Bot', desc: 'Collect Linear updates and post a tidy engineering standup to Slack.', icon: 'slack', tone: 'pink' },
|
||||
{ id: 'doc', name: 'Docs Sync', desc: 'Keep Notion runbooks in sync with the production API surface.', icon: 'book', tone: 'blue' },
|
||||
];
|
||||
|
||||
const workflows = [
|
||||
{ id: 'digest_weekly', name: 'digest/weekly', nodes: 9, modified: '2 hours ago', runs: '147 runs', status: 'healthy' },
|
||||
{ id: 'triage_inbound', name: 'triage/inbound-email', nodes: 14, modified: 'Yesterday', runs: '2,318 runs', status: 'healthy' },
|
||||
{ id: 'seo_brief', name: 'seo/brief-from-keyword', nodes: 7, modified: '3 days ago', runs: '42 runs', status: 'healthy' },
|
||||
{ id: 'standup', name: 'slack/standup-collector', nodes: 6, modified: '1 week ago', runs: '24 runs', status: 'idle' },
|
||||
{ id: 'docs_sync', name: 'docs/sync-notion', nodes: 11, modified: '2 weeks ago', runs: '8 runs', status: 'failed' },
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="shell">
|
||||
<Sidebar current="dashboard" onNav={onNav} />
|
||||
<div className="main">
|
||||
<div className="main-head">
|
||||
<div>
|
||||
<div className="crumb">
|
||||
<span>Northwind</span>
|
||||
<span className="sep"><Icon name="chevron_right" size={11} /></span>
|
||||
<span>Dashboard</span>
|
||||
</div>
|
||||
<h1>Welcome back, Maya</h1>
|
||||
<div className="sub">5 apps running · 12 workflows · 2,538 runs this week</div>
|
||||
</div>
|
||||
<div className="flex gap-8">
|
||||
<button className="btn btn-secondary"><Icon name="book" size={14} /> Templates</button>
|
||||
<button className="btn btn-primary"><Icon name="plus" size={14} /> New app</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="main-body">
|
||||
{/* Apps grid */}
|
||||
<div className="section-head">
|
||||
<div>
|
||||
<h2>My Apps</h2>
|
||||
<div className="subtle" style={{marginTop: 2}}>Packaged workflows your team can run from chat or code</div>
|
||||
</div>
|
||||
<span className="subtle">{apps.length} apps</span>
|
||||
</div>
|
||||
|
||||
<div className="apps-grid">
|
||||
{apps.map(a => (
|
||||
<div key={a.id} className="app-card">
|
||||
<AppIcon tone={a.tone}><Icon name={a.icon} size={17} /></AppIcon>
|
||||
<h4>{a.name}</h4>
|
||||
<p className="dsc">{a.desc}</p>
|
||||
<div className="row">
|
||||
<a className="open" onClick={() => onNav('workflow')}>Open app <Icon name="arrow_right" size={12} /></a>
|
||||
<button className="chip-btn">
|
||||
<Icon name="spark" size={11} /> Edit in Claude
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
<div className="app-card app-empty">
|
||||
<div className="plus"><Icon name="plus" size={16} /></div>
|
||||
<div style={{fontSize: 13, fontWeight: 500}}>Create new app</div>
|
||||
<div style={{fontSize: 12, opacity: 0.75}}>Start from scratch or template</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Workflows */}
|
||||
<div className="wf-table">
|
||||
<div className="section-head">
|
||||
<div>
|
||||
<h2>My Workflows</h2>
|
||||
<div className="subtle" style={{marginTop: 2}}>The graphs that power your apps</div>
|
||||
</div>
|
||||
<div className="flex gap-8">
|
||||
<button className="btn btn-secondary btn-sm"><Icon name="filter" size={12} /> All workflows</button>
|
||||
<button className="btn btn-secondary btn-sm" onClick={() => onNav('workflow')}><Icon name="plus" size={12} /> New</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="table-wrap">
|
||||
<table className="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style={{width: '34%'}}>Workflow</th>
|
||||
<th>Nodes</th>
|
||||
<th>Last modified</th>
|
||||
<th>Activity</th>
|
||||
<th>Status</th>
|
||||
<th style={{width: 100, textAlign: 'right'}}></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{workflows.map(w => (
|
||||
<tr key={w.id}>
|
||||
<td>
|
||||
<div className="wf-row-name">
|
||||
<span className="dot" />
|
||||
<span className="mono" style={{fontSize: 13}}>{w.name}</span>
|
||||
</div>
|
||||
</td>
|
||||
<td className="dim">{w.nodes}</td>
|
||||
<td className="dim" style={{fontSize: 12.5}}>{w.modified}</td>
|
||||
<td className="dim" style={{fontSize: 12.5}}>{w.runs}</td>
|
||||
<td>
|
||||
<span className={`pill ${w.status === 'healthy' ? 'active' : w.status === 'failed' ? 'revoked' : 'idle'}`}>
|
||||
<span className="pdot" /> {w.status}
|
||||
</span>
|
||||
</td>
|
||||
<td style={{textAlign: 'right'}}>
|
||||
<button className="btn btn-secondary btn-sm" onClick={() => onNav('workflow')}>View</button>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
window.Dashboard = Dashboard;
|
||||
@@ -0,0 +1,168 @@
|
||||
const Landing = ({ onNav }) => {
|
||||
const [installer, setInstaller] = React.useState('npm');
|
||||
const installCmds = {
|
||||
npm: '$ npm install arcrun',
|
||||
pip: '$ pip install arcrun',
|
||||
bun: '$ bun add arcrun',
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<TopNav onNav={onNav} current="landing" />
|
||||
|
||||
<div className="container">
|
||||
<section className="hero">
|
||||
<div className="hero-bg" />
|
||||
<div className="hero-bg-grid" />
|
||||
<div className="hero-eyebrow">
|
||||
<span className="dot" />
|
||||
<span>Now in public beta — MCP-native</span>
|
||||
</div>
|
||||
<h1>Build AI workflows<br/><span className="grad">without the glue code.</span></h1>
|
||||
<p className="sub">Connect your tools, automate your work. Orchestrate workflows from Claude.ai, your IDE, or a few lines of code — Arcrun handles auth, retries, and state.</p>
|
||||
<div className="hero-ctas">
|
||||
<button className="btn btn-primary btn-lg" onClick={() => onNav('auth')}>
|
||||
Start free <Icon name="arrow_right" size={15} />
|
||||
</button>
|
||||
<button className="btn btn-secondary btn-lg">
|
||||
<Icon name="book" size={14} /> Read the docs
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="paths">
|
||||
{/* Developer path */}
|
||||
<div className="path-card">
|
||||
<div className="path-label">
|
||||
<Icon name="terminal" size={13} /> For Developers
|
||||
</div>
|
||||
<h3>Three lines, any runtime.</h3>
|
||||
<p className="lede">Install once, call Arcrun from Node, Python, or your edge runtime. OAuth, rate limits, and retries are handled.</p>
|
||||
|
||||
<div className="install-tabs">
|
||||
{Object.keys(installCmds).map(k => (
|
||||
<button key={k} className={installer === k ? 'active' : ''} onClick={() => setInstaller(k)}>{k}</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="terminal" style={{marginBottom: 12}}>
|
||||
<div className="terminal-head">
|
||||
<div className="dots"><span/><span/><span/></div>
|
||||
<div className="title">terminal</div>
|
||||
</div>
|
||||
<div className="terminal-body">
|
||||
<div><span className="dim">{installCmds[installer]}</span></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="terminal">
|
||||
<div className="terminal-head">
|
||||
<div className="dots"><span/><span/><span/></div>
|
||||
<div className="title">{installer === 'pip' ? 'app.py' : 'app.ts'}</div>
|
||||
</div>
|
||||
<div className="terminal-body">
|
||||
{installer === 'pip' ? (
|
||||
<>
|
||||
<div><span className="c1">from</span> <span className="c2">arcrun</span> <span className="c1">import</span> <span className="c2">Arcrun</span></div>
|
||||
<div className="sp-4"/>
|
||||
<div><span className="c2">client</span> = <span className="c4">Arcrun</span>(<span className="c2">token</span>=<span className="c2">os</span>.<span className="c4">getenv</span>(<span className="c3">"ARCRUN_KEY"</span>))</div>
|
||||
<div><span className="c2">run</span> = <span className="c2">client</span>.<span className="c4">run</span>(<span className="c3">"digest/weekly"</span>, <span className="c2">inputs</span>={'{'}<span className="c3">"user"</span>: <span className="c3">"u_219"</span>{'}'})</div>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<div><span className="c1">import</span> {'{'} <span className="c2">Arcrun</span> {'}'} <span className="c1">from</span> <span className="c3">"arcrun"</span>;</div>
|
||||
<div className="sp-4"/>
|
||||
<div><span className="c1">const</span> <span className="c2">client</span> = <span className="c1">new</span> <span className="c4">Arcrun</span>({'{'} <span className="c2">token</span>: <span className="c2">process</span>.<span className="c2">env</span>.<span className="c2">ARCRUN_KEY</span> {'}'});</div>
|
||||
<div><span className="c1">const</span> <span className="c2">run</span> = <span className="c1">await</span> <span className="c2">client</span>.<span className="c4">run</span>(<span className="c3">"digest/weekly"</span>, {'{'} <span className="c2">user</span>: <span className="c3">"u_219"</span> {'}'});</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="sp-16" />
|
||||
<div className="flex gap-12" style={{fontSize: 12.5, color: 'var(--text-mute)'}}>
|
||||
<span className="flex gap-6" style={{alignItems: 'center'}}><Icon name="check" size={12} /> Typed SDKs</span>
|
||||
<span className="flex gap-6" style={{alignItems: 'center'}}><Icon name="check" size={12} /> Idempotent runs</span>
|
||||
<span className="flex gap-6" style={{alignItems: 'center'}}><Icon name="check" size={12} /> Self-host ready</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Everyone path */}
|
||||
<div className="path-card">
|
||||
<div className="path-label">
|
||||
<Icon name="spark" size={13} /> For Everyone
|
||||
</div>
|
||||
<h3>Talk to your workflows.</h3>
|
||||
<p className="lede">Install Arcrun inside your AI assistant and run your apps by asking. Trigger workflows, fetch data, or draft messages — in plain English.</p>
|
||||
|
||||
<div className="chat-preview">
|
||||
<div className="chat-head">
|
||||
<span className="brand-dot">AI</span>
|
||||
<span>Your assistant — Arcrun connected</span>
|
||||
<span style={{marginLeft: 'auto'}} className="pill active"><span className="pdot" />2 apps</span>
|
||||
</div>
|
||||
<div className="chat-body">
|
||||
<div className="chat-msg user">
|
||||
<div className="avatar">M</div>
|
||||
<div className="bubble">Send this week's customer digest to the revenue team.</div>
|
||||
</div>
|
||||
<div className="chat-msg ai">
|
||||
<div className="avatar">A</div>
|
||||
<div className="bubble">
|
||||
Running <span style={{color: 'var(--primary)', fontWeight: 500}}>digest/weekly</span> for 147 accounts, then posting to #revenue.
|
||||
<div className="tool-card">
|
||||
<div className="tool-icon">AR</div>
|
||||
<div className="tool-meta">
|
||||
<div className="tool-name">arcrun · digest/weekly</div>
|
||||
<div className="tool-sub">4 of 5 steps complete · 00:12 elapsed</div>
|
||||
</div>
|
||||
<span className="pill active"><span className="pdot" />running</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="chat-input">
|
||||
<span>Reply to your assistant…</span>
|
||||
<span className="caret" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="sp-16" />
|
||||
<div className="flex gap-12" style={{fontSize: 12.5, color: 'var(--text-mute)'}}>
|
||||
<span className="flex gap-6" style={{alignItems: 'center'}}><Icon name="check" size={12} /> One-click connect</span>
|
||||
<span className="flex gap-6" style={{alignItems: 'center'}}><Icon name="check" size={12} /> Works in your IDE</span>
|
||||
<span className="flex gap-6" style={{alignItems: 'center'}}><Icon name="check" size={12} /> Audit trail</span>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="strip">
|
||||
<div className="cell">
|
||||
<div className="ico"><Icon name="bolt" size={15} /></div>
|
||||
<h4>Run anywhere</h4>
|
||||
<p>Node, Python, Deno, Bun, Cloudflare Workers. One API, same semantics.</p>
|
||||
</div>
|
||||
<div className="cell">
|
||||
<div className="ico"><Icon name="workflow" size={15} /></div>
|
||||
<h4>Composable steps</h4>
|
||||
<p>Model calls, HTTP, database, branching — wire them visually or in code.</p>
|
||||
</div>
|
||||
<div className="cell">
|
||||
<div className="ico"><Icon name="key" size={15} /></div>
|
||||
<h4>Scoped keys</h4>
|
||||
<p>Per-workflow API keys with fine-grained scopes and live revocation.</p>
|
||||
</div>
|
||||
<div className="cell">
|
||||
<div className="ico"><Icon name="eye" size={15} /></div>
|
||||
<h4>Observable</h4>
|
||||
<p>Every run is replayable. Inspect inputs, outputs, and token usage.</p>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<Footer onNav={onNav} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
window.Landing = Landing;
|
||||
@@ -0,0 +1,255 @@
|
||||
const WorkflowViewer = ({ onNav }) => {
|
||||
const nodes = [
|
||||
{ id: 'trigger', x: 60, y: 260, title: 'Weekly Schedule', type: 'trigger', badge: 'CRON', icon: 'clock', tone: '#22C55E',
|
||||
inputs: [], outputs: [{k: 'timestamp', t: 'ISO8601'}, {k: 'runId', t: 'string'}] },
|
||||
{ id: 'fetch', x: 320, y: 140, title: 'Fetch Accounts', type: 'database.query', badge: 'DB', icon: 'database', tone: '#3B82F6',
|
||||
inputs: [{k: 'segment', t: 'string'}], outputs: [{k: 'accounts', t: 'Account[]'}, {k: 'count', t: 'number'}] },
|
||||
{ id: 'events', x: 320, y: 380, title: 'Pull Events', type: 'segment.events', badge: 'API', icon: 'bolt', tone: '#F59E0B',
|
||||
inputs: [{k: 'since', t: 'ISO8601'}], outputs: [{k: 'events', t: 'Event[]'}] },
|
||||
{ id: 'summarize', x: 600, y: 260, title: 'Summarize with Claude', type: 'ai.completion', badge: 'AI', icon: 'spark', tone: '#8B5CF6',
|
||||
inputs: [{k: 'accounts', t: 'Account[]'}, {k: 'events', t: 'Event[]'}, {k: 'prompt', t: 'string'}],
|
||||
outputs: [{k: 'digest', t: 'Digest'}, {k: 'tokens', t: 'number'}] },
|
||||
{ id: 'filter', x: 880, y: 160, title: 'Filter — priority ≥ 2', type: 'logic.filter', badge: 'IF', icon: 'filter', tone: '#64748B',
|
||||
inputs: [{k: 'digest', t: 'Digest'}], outputs: [{k: 'items', t: 'Item[]'}] },
|
||||
{ id: 'slack', x: 1140, y: 100, title: 'Post to #revenue', type: 'slack.message', badge: 'OUT', icon: 'slack', tone: '#EC4899',
|
||||
inputs: [{k: 'channel', t: 'string'}, {k: 'blocks', t: 'Block[]'}], outputs: [{k: 'ts', t: 'string'}] },
|
||||
{ id: 'mail', x: 1140, y: 260, title: 'Email Digest', type: 'mail.send', badge: 'OUT', icon: 'mail', tone: '#6366F1',
|
||||
inputs: [{k: 'to', t: 'string[]'}, {k: 'subject', t: 'string'}, {k: 'html', t: 'string'}], outputs: [{k: 'messageId', t: 'string'}] },
|
||||
{ id: 'log', x: 880, y: 400, title: 'Log run metadata', type: 'arcrun.log', badge: 'LOG', icon: 'terminal', tone: '#475569',
|
||||
inputs: [{k: 'runId', t: 'string'}, {k: 'stats', t: 'Stats'}], outputs: [] },
|
||||
];
|
||||
|
||||
const edges = [
|
||||
['trigger', 'fetch'],
|
||||
['trigger', 'events'],
|
||||
['fetch', 'summarize'],
|
||||
['events', 'summarize'],
|
||||
['summarize', 'filter'],
|
||||
['summarize', 'log'],
|
||||
['filter', 'slack'],
|
||||
['filter', 'mail'],
|
||||
];
|
||||
|
||||
const [selectedId, setSelectedId] = React.useState('summarize');
|
||||
const [title, setTitle] = React.useState('digest/weekly');
|
||||
const [zoom, setZoom] = React.useState(100);
|
||||
|
||||
const selected = nodes.find(n => n.id === selectedId);
|
||||
|
||||
// Edit triplet inline (for the summarize node's prompt config)
|
||||
const [triplet, setTriplet] = React.useState({
|
||||
model: 'claude-haiku-4-5',
|
||||
temperature: '0.3',
|
||||
prompt: 'Summarize this week\'s account activity for the revenue team.',
|
||||
});
|
||||
|
||||
// Measure node widths for edge endpoint accuracy
|
||||
const nodeRefs = React.useRef({});
|
||||
const [sizes, setSizes] = React.useState({});
|
||||
React.useEffect(() => {
|
||||
const ns = {};
|
||||
for (const n of nodes) {
|
||||
const el = nodeRefs.current[n.id];
|
||||
if (el) ns[n.id] = { w: el.offsetWidth, h: el.offsetHeight };
|
||||
}
|
||||
setSizes(ns);
|
||||
}, []);
|
||||
|
||||
const getPort = (id, side) => {
|
||||
const n = nodes.find(x => x.id === id);
|
||||
const sz = sizes[id] || { w: 200, h: 60 };
|
||||
return {
|
||||
x: side === 'out' ? n.x + sz.w : n.x,
|
||||
y: n.y + sz.h / 2,
|
||||
};
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="wf-viewer">
|
||||
<div className="wf-topbar">
|
||||
<div className="back" onClick={() => onNav('dashboard')} title="Back to dashboard">
|
||||
<Icon name="arrow_left" size={16} />
|
||||
</div>
|
||||
<Logo size="sm" onClick={() => onNav('landing')} />
|
||||
<div className="sep" />
|
||||
<div className="wf-breadcrumb">
|
||||
<span className="cr" onClick={() => onNav('dashboard')}>Workflows</span>
|
||||
<Icon name="chevron_right" size={11} />
|
||||
<input
|
||||
className="wf-title mono"
|
||||
value={title}
|
||||
onChange={e => setTitle(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<span className="wf-saved">
|
||||
<span style={{width: 6, height: 6, borderRadius: '50%', background: '#22C55E', boxShadow: '0 0 0 3px rgba(34,197,94,0.18)'}} />
|
||||
Saved · 2m ago
|
||||
</span>
|
||||
<div className="spacer" />
|
||||
<button className="btn btn-ghost btn-sm"><Icon name="share" size={13} /> Share</button>
|
||||
<button className="btn btn-secondary btn-sm"><Icon name="download" size={13} /> Export YAML</button>
|
||||
<button className="wf-edit-in-claude">
|
||||
<Icon name="spark" size={13} /> Edit in Claude <Icon name="external" size={12} />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="wf-canvas">
|
||||
<svg className="wf-edges" width="100%" height="100%">
|
||||
<defs>
|
||||
<marker id="arrow" viewBox="0 0 10 10" refX="8" refY="5" markerWidth="6" markerHeight="6" orient="auto-start-reverse">
|
||||
<path d="M0,0 L10,5 L0,10 z" fill="#6366F1" />
|
||||
</marker>
|
||||
<marker id="arrow-dim" viewBox="0 0 10 10" refX="8" refY="5" markerWidth="6" markerHeight="6" orient="auto-start-reverse">
|
||||
<path d="M0,0 L10,5 L0,10 z" fill="#3a3a3a" />
|
||||
</marker>
|
||||
</defs>
|
||||
{edges.map(([a, b], i) => {
|
||||
const p1 = getPort(a, 'out');
|
||||
const p2 = getPort(b, 'in');
|
||||
const dx = Math.max(40, (p2.x - p1.x) * 0.5);
|
||||
const d = `M ${p1.x} ${p1.y} C ${p1.x + dx} ${p1.y}, ${p2.x - dx} ${p2.y}, ${p2.x - 2} ${p2.y}`;
|
||||
const highlight = a === selectedId || b === selectedId;
|
||||
return (
|
||||
<path key={i} d={d}
|
||||
stroke={highlight ? '#6366F1' : '#3a3a3a'}
|
||||
strokeWidth={highlight ? 2 : 1.5}
|
||||
fill="none"
|
||||
markerEnd={`url(#${highlight ? 'arrow' : 'arrow-dim'})`}
|
||||
opacity={highlight ? 0.95 : 0.6} />
|
||||
);
|
||||
})}
|
||||
</svg>
|
||||
|
||||
<div className="wf-nodes">
|
||||
{nodes.map(n => (
|
||||
<div key={n.id}
|
||||
ref={el => (nodeRefs.current[n.id] = el)}
|
||||
className={`wf-node ${selectedId === n.id ? 'selected' : ''}`}
|
||||
style={{left: n.x, top: n.y}}
|
||||
onClick={() => setSelectedId(n.id)}>
|
||||
{n.inputs.length > 0 && <span className="port in" />}
|
||||
{n.outputs.length > 0 && <span className="port out" />}
|
||||
<div className="node-row-top">
|
||||
<span className="node-icon" style={{background: n.tone}}>
|
||||
<Icon name={n.icon} size={12} />
|
||||
</span>
|
||||
<span className="node-title">{n.title}</span>
|
||||
<span className="node-badge">{n.badge}</span>
|
||||
</div>
|
||||
<div className="node-sub">{n.type}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Detail panel */}
|
||||
{selected && (
|
||||
<div className="wf-detail">
|
||||
<div className="dt-head">
|
||||
<span className="dt-icon" style={{background: selected.tone}}>
|
||||
<Icon name={selected.icon} size={15} />
|
||||
</span>
|
||||
<div className="dt-meta">
|
||||
<h3>{selected.title}</h3>
|
||||
<div className="dt-type">{selected.type}</div>
|
||||
</div>
|
||||
<button className="close-btn" onClick={() => setSelectedId(null)}>
|
||||
<Icon name="close" size={14} />
|
||||
</button>
|
||||
</div>
|
||||
<div className="dt-body">
|
||||
<div className="dt-section">
|
||||
<h4>Input schema</h4>
|
||||
{selected.inputs.length === 0 ? (
|
||||
<div style={{fontSize: 12, color: 'var(--text-mute)', fontStyle: 'italic'}}>No inputs — this is a trigger.</div>
|
||||
) : selected.inputs.map(f => (
|
||||
<div key={f.k} className="schema-field">
|
||||
<span className="k">{f.k}</span>
|
||||
<span className="t">{f.t}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="dt-section">
|
||||
<h4>Output schema</h4>
|
||||
{selected.outputs.length === 0 ? (
|
||||
<div style={{fontSize: 12, color: 'var(--text-mute)', fontStyle: 'italic'}}>No outputs — terminal node.</div>
|
||||
) : selected.outputs.map(f => (
|
||||
<div key={f.k} className="schema-field">
|
||||
<span className="k">{f.k}</span>
|
||||
<span className="t">{f.t}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{selected.id === 'summarize' && (
|
||||
<div className="dt-section">
|
||||
<h4>Configuration</h4>
|
||||
<div className="triplet">
|
||||
<div className="trow">
|
||||
<div className="tkey">model</div>
|
||||
<input className="tval" value={triplet.model} onChange={e => setTriplet({...triplet, model: e.target.value})} />
|
||||
</div>
|
||||
<div className="trow">
|
||||
<div className="tkey">temp</div>
|
||||
<input className="tval" value={triplet.temperature} onChange={e => setTriplet({...triplet, temperature: e.target.value})} />
|
||||
</div>
|
||||
<div className="trow">
|
||||
<div className="tkey">prompt</div>
|
||||
<input className="tval" value={triplet.prompt} onChange={e => setTriplet({...triplet, prompt: e.target.value})} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="dt-section">
|
||||
<h4>Last run</h4>
|
||||
<div style={{display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 8, fontSize: 12}}>
|
||||
<div style={{background: 'rgba(255,255,255,0.02)', border: '1px solid var(--line)', borderRadius: 7, padding: '8px 10px'}}>
|
||||
<div style={{color: 'var(--text-mute)', fontSize: 10.5, textTransform: 'uppercase', letterSpacing: '0.06em'}}>Duration</div>
|
||||
<div style={{fontFamily: 'JetBrains Mono, monospace', marginTop: 3}}>2.4s</div>
|
||||
</div>
|
||||
<div style={{background: 'rgba(255,255,255,0.02)', border: '1px solid var(--line)', borderRadius: 7, padding: '8px 10px'}}>
|
||||
<div style={{color: 'var(--text-mute)', fontSize: 10.5, textTransform: 'uppercase', letterSpacing: '0.06em'}}>Status</div>
|
||||
<div style={{marginTop: 2}}><span className="pill active"><span className="pdot" />success</span></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button className="btn btn-primary" style={{width: '100%', marginTop: 4}}>
|
||||
<Icon name="spark" size={13} /> Edit this node in Claude
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Minimap */}
|
||||
<div className="wf-minimap">
|
||||
<div className="mini-label">Overview</div>
|
||||
{nodes.map(n => {
|
||||
const sz = sizes[n.id] || {w: 180, h: 60};
|
||||
return (
|
||||
<div key={n.id} className="mini-box" style={{
|
||||
left: 8 + (n.x / 1400) * 164,
|
||||
top: 18 + (n.y / 500) * 80,
|
||||
width: Math.max(6, (sz.w / 1400) * 164),
|
||||
height: Math.max(4, (sz.h / 500) * 80),
|
||||
opacity: selectedId === n.id ? 1 : 0.5,
|
||||
background: selectedId === n.id ? 'var(--primary)' : 'var(--primary-soft)',
|
||||
}} />
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
{/* Zoom controls */}
|
||||
<div className="wf-controls">
|
||||
<button onClick={() => setZoom(Math.max(40, zoom - 10))}><Icon name="zoom_out" size={13} /></button>
|
||||
<div className="zoom-val">{zoom}%</div>
|
||||
<button onClick={() => setZoom(Math.min(200, zoom + 10))}><Icon name="zoom_in" size={13} /></button>
|
||||
<button><Icon name="maximize" size={13} /></button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
window.WorkflowViewer = WorkflowViewer;
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,761 @@
|
||||
# arcrun Credential System 設計規格
|
||||
20260418
|
||||
|
||||
> **讀者**:Claude Code(CC),負責實作
|
||||
> **作者**:richblack(架構決策)
|
||||
> **版本**:v1.0
|
||||
> **狀態**:Draft — 等 CC 確認技術可行性後開工
|
||||
|
||||
---
|
||||
|
||||
## 0. TL;DR(給 CC 的三句話版)
|
||||
|
||||
1. **不要**為每個服務寫一個 credential 零件,n8n 是錯的。
|
||||
2. 做**四個 TinyGo/WASM 零件**(primitives),每個服務只需要一份 **YAML recipe** + 用戶自己的 **secret**。
|
||||
3. Recipe 存 arcrun 平台 KV(公共),secret 存 tenant KV(私有),兩者在 runtime 由 `AuthBroker` 組裝成可用的 HTTP client。
|
||||
|
||||
---
|
||||
|
||||
## 1. 設計目標與反目標
|
||||
|
||||
### 目標
|
||||
- **新增一個服務的成本 = 寫一份 YAML**,不需要 rebuild、不需要改 code。
|
||||
- **AI agent 理解成本 ≈ 0**:recipe 就是呼叫該服務的完整說明書。
|
||||
- **人類設定成本 < 10 分鐘**:即使是對 OAuth 不熟的使用者,UI 只問「你的 API Key 是什麼」這類 secret 層級問題。
|
||||
- **Secret 隔離**:每個 tenant 的 secret 絕對不互相可見,arcrun 平台本身也無法明文讀取(用 Cloudflare Secrets Store 或加密儲存)。
|
||||
|
||||
### 反目標(明確不做的事)
|
||||
- ❌ 不做 n8n 那種「每個服務一個 credential type」的視覺化面板。
|
||||
- ❌ 不支援 OAuth1(2026 年還在用的服務極少,真遇到再加)。
|
||||
- ❌ 不做 credential sharing 的複雜 ACL(全 tenant scope 即可,未來再擴充)。
|
||||
- ❌ 不在 arcrun 內部明文持久化任何長期 secret(只有加密過的密文或 Secrets Store reference)。
|
||||
|
||||
---
|
||||
|
||||
## 2. 核心架構:三層模型
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────┐
|
||||
│ Layer 3: Service Recipe (YAML) │
|
||||
│ arcrun 平台共享,describe "如何呼叫這個服務" │
|
||||
│ 存在 Workers KV: arcrun-recipes │
|
||||
│ 例:recipe/notion.yaml, recipe/google_calendar.yaml │
|
||||
└─────────────────────────────────────────────────────────┘
|
||||
↓ 引用
|
||||
┌─────────────────────────────────────────────────────────┐
|
||||
│ Layer 2: Auth Primitive (TinyGo → WASM) │
|
||||
│ 四個通用認證零件,實作注入邏輯與 token 交換 │
|
||||
│ 1. static_key 2. oauth2 │
|
||||
│ 3. service_account 4. mtls │
|
||||
└─────────────────────────────────────────────────────────┘
|
||||
↑ 使用
|
||||
┌─────────────────────────────────────────────────────────┐
|
||||
│ Layer 1: Tenant Secret (KV + Secrets Store) │
|
||||
│ 每個 tenant 自己的 KV namespace │
|
||||
│ 存 encrypted secret 或 Secrets Store reference │
|
||||
│ 例:secret/{tenant_id}/notion-prod │
|
||||
└─────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### 為什麼這樣切?
|
||||
|
||||
| 切分維度 | Recipe | Primitive | Secret |
|
||||
|---|---|---|---|
|
||||
| **誰擁有** | arcrun 平台 | arcrun 平台 | tenant 自己 |
|
||||
| **變化頻率** | 中(新服務時) | 低(認證機制穩定) | 高(rotate、revoke) |
|
||||
| **敏感度** | 公開 | 公開 | 最高機密 |
|
||||
| **儲存位置** | 平台 KV(`arcrun-recipes`) | WASM binary | tenant KV + Secrets Store |
|
||||
| **可否社群貢獻** | ✅ PR | ⚠️ 核心團隊 | ❌ 永遠不 |
|
||||
|
||||
---
|
||||
|
||||
## 3. 四個 Primitive 詳細規格
|
||||
|
||||
### 3.1 `static_key`
|
||||
|
||||
**適用**:API Key、Bearer Token、Basic Auth、任何「一組 secret 不會自動過期」的認證。
|
||||
|
||||
**涵蓋 n8n 的**:API Key、Basic Auth、Header Auth、Query Auth、Custom Auth、Digest Auth(~80% 服務)。
|
||||
|
||||
**Recipe 欄位**:
|
||||
```yaml
|
||||
primitive: static_key
|
||||
inject:
|
||||
# 四個注入位置,可以同時用多個
|
||||
header: # HTTP headers
|
||||
<key>: <value template>
|
||||
query: # URL query string
|
||||
<key>: <value template>
|
||||
body: # request body(JSON 欄位)
|
||||
<key>: <value template>
|
||||
basic_auth: # HTTP Basic Auth(會自動 base64 編碼)
|
||||
username: <value template>
|
||||
password: <value template>
|
||||
```
|
||||
|
||||
**Value template 語法**:`{{secret.xxx}}` 取 secret 欄位,`{{const.yyy}}` 取 recipe 內定義的常數。
|
||||
|
||||
**Secret schema**:tenant 存 JSON,欄位由 recipe 的 `required_secrets` 宣告。
|
||||
|
||||
**範例(Notion)**:
|
||||
```yaml
|
||||
# arcrun-recipes KV: recipe/notion
|
||||
service: notion
|
||||
version: 1
|
||||
primitive: static_key
|
||||
base_url: https://api.notion.com/v1
|
||||
required_secrets:
|
||||
- key: token
|
||||
label: "Internal Integration Token"
|
||||
help_url: https://www.notion.so/my-integrations
|
||||
inject:
|
||||
header:
|
||||
Authorization: "Bearer {{secret.token}}"
|
||||
Notion-Version: "2022-06-28"
|
||||
test:
|
||||
method: GET
|
||||
path: /users/me
|
||||
expect_status: 200
|
||||
```
|
||||
|
||||
**Secret 範例**:
|
||||
```json
|
||||
// tenant KV: secret/tenant_123/notion-prod
|
||||
{
|
||||
"token": "secret_abc123..."
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 3.2 `oauth2`
|
||||
|
||||
**適用**:需要人類首次授權、之後用 refresh token 續命的場景。
|
||||
|
||||
**Grant types 支援**:
|
||||
- `authorization_code`(最常見:GitHub、Slack、Google 用戶授權)
|
||||
- `client_credentials`(機器對機器)
|
||||
- `pkce`(SPA、行動應用)
|
||||
- ❌ 不支援:password grant(2026 已被多數 OAuth 提供者棄用)、implicit(已棄用)
|
||||
|
||||
**Recipe 欄位**:
|
||||
```yaml
|
||||
primitive: oauth2
|
||||
grant: authorization_code # or client_credentials, pkce
|
||||
base_url: <service API base>
|
||||
oauth:
|
||||
authorize_url: <IdP authorize endpoint>
|
||||
token_url: <IdP token endpoint>
|
||||
scopes:
|
||||
- <default scope 1>
|
||||
- <default scope 2>
|
||||
client_auth: header # or body
|
||||
# 是否使用 refresh token
|
||||
refresh: true
|
||||
# PKCE 時額外參數
|
||||
pkce_method: S256 # only for grant: pkce
|
||||
required_secrets:
|
||||
- key: client_id
|
||||
label: "Client ID"
|
||||
- key: client_secret
|
||||
label: "Client Secret"
|
||||
secret: true
|
||||
inject:
|
||||
header:
|
||||
Authorization: "Bearer {{runtime.access_token}}"
|
||||
```
|
||||
|
||||
**Runtime 欄位**(primitive 自動維護,存在 tenant KV 的 `oauth_state/{secret_id}` key):
|
||||
- `access_token`
|
||||
- `refresh_token`
|
||||
- `expires_at`
|
||||
|
||||
**首次授權流程**(人類要做的部分):
|
||||
1. arcrun UI 呼叫 `AuthBroker.startAuth(recipe_id, tenant_id)` 回傳 authorize URL。
|
||||
2. 使用者瀏覽器跳轉到 IdP,同意授權。
|
||||
3. IdP redirect 回 arcrun callback endpoint(固定一個 URL,無論哪個服務)。
|
||||
4. `AuthBroker` 用 authorization code 換 token,寫入 tenant KV。
|
||||
|
||||
**之後 agent 呼叫時完全自動**:primitive 檢查 `expires_at`,過期自動用 refresh token 續,失敗再觸發重新授權通知。
|
||||
|
||||
---
|
||||
|
||||
### 3.3 `service_account`
|
||||
|
||||
**適用**:Google Service Account、AWS IAM Role(assume role)、任何需要「私鑰簽 JWT 換短期 token」的機器身份。
|
||||
|
||||
**這個就是讓你 debug 兩天那個爆炸點。** 我們用 primitive 把地雷全部包起來。
|
||||
|
||||
**Recipe 欄位**:
|
||||
```yaml
|
||||
primitive: service_account
|
||||
kind: google_jwt # or aws_sigv4, generic_jwt
|
||||
base_url: <service API base>
|
||||
token_exchange:
|
||||
# Google 的 JWT → OAuth access token 流程
|
||||
endpoint: https://oauth2.googleapis.com/token
|
||||
audience: https://oauth2.googleapis.com/token
|
||||
scopes:
|
||||
- https://www.googleapis.com/auth/calendar
|
||||
# JWT claims
|
||||
issuer_from_secret: client_email
|
||||
subject_from_secret: client_email # optional, for domain-wide delegation 改成其他 user
|
||||
ttl_seconds: 3600
|
||||
required_secrets:
|
||||
- key: service_account_json
|
||||
label: "Service Account JSON"
|
||||
type: json_blob # 特別型別,UI 可以接受貼整個 JSON
|
||||
help: "到 GCP Console → IAM → Service Accounts → Keys → Add Key (JSON) 下載整份 JSON 貼上"
|
||||
inject:
|
||||
header:
|
||||
Authorization: "Bearer {{runtime.access_token}}"
|
||||
```
|
||||
|
||||
**為什麼不是每個服務一個 recipe?**
|
||||
- Google Calendar、Gmail、Drive、Sheets 全部可以共用同一個 `service_account` primitive。
|
||||
- 差別只在 `scopes` 和 `base_url`。
|
||||
- Recipe 本身可以 import 共通片段(見 §5 recipe 繼承)。
|
||||
|
||||
**AWS SigV4(kind: aws_sigv4)**:這是特例,不是 JWT-based,但概念一樣——用 access_key_id + secret_access_key 在每次 request 上簽章。Primitive 內建處理,recipe 只要宣告 region 和 service name。
|
||||
|
||||
---
|
||||
|
||||
### 3.4 `mtls`
|
||||
|
||||
**適用**:mTLS / client certificate。銀行 API、企業內部服務、醫療系統。
|
||||
|
||||
**Recipe 欄位**:
|
||||
```yaml
|
||||
primitive: mtls
|
||||
base_url: <service API base>
|
||||
required_secrets:
|
||||
- key: client_cert
|
||||
label: "Client Certificate (PEM)"
|
||||
type: pem_cert
|
||||
- key: client_key
|
||||
label: "Client Private Key (PEM)"
|
||||
type: pem_key
|
||||
secret: true
|
||||
- key: ca_cert
|
||||
label: "CA Certificate (PEM) — optional"
|
||||
type: pem_cert
|
||||
optional: true
|
||||
# mtls 通常不需要額外 inject,憑證在 TLS 層
|
||||
inject: {}
|
||||
```
|
||||
|
||||
**實作注意**:Cloudflare Workers 有原生 mTLS 支援(`mTLSCertificate` binding),primitive 只需要把 secret 轉成 Cloudflare mTLS binding 即可。
|
||||
|
||||
---
|
||||
|
||||
## 4. Recipe YAML Schema(完整版)
|
||||
|
||||
```yaml
|
||||
# 必填
|
||||
service: string # 唯一識別,snake_case,e.g. "notion", "google_calendar"
|
||||
version: integer # recipe schema version,breaking change 要升版
|
||||
primitive: enum # static_key | oauth2 | service_account | mtls
|
||||
base_url: string # service API base URL
|
||||
|
||||
# primitive 相關(依 primitive 不同)
|
||||
inject: object # 如何把 secret 注入 HTTP request
|
||||
oauth: object # 僅 oauth2 primitive
|
||||
token_exchange: object # 僅 service_account primitive
|
||||
|
||||
# Secret 宣告(讓 UI 知道要問什麼)
|
||||
required_secrets:
|
||||
- key: string # secret 欄位名
|
||||
label: string # UI 顯示
|
||||
secret: boolean # 是否遮蔽顯示(default: true)
|
||||
type: enum # text | json_blob | pem_cert | pem_key | url
|
||||
optional: boolean # default: false
|
||||
help: string # 給使用者的提示
|
||||
help_url: string # 導向服務文件
|
||||
|
||||
# 測試(驗證 credential 是否有效)
|
||||
test:
|
||||
method: GET | POST
|
||||
path: string # 相對 base_url
|
||||
expect_status: integer
|
||||
expect_json: object # 選填,JSON path assertion
|
||||
|
||||
# Metadata
|
||||
display_name: string # UI 顯示名
|
||||
description: string
|
||||
icon_url: string
|
||||
docs_url: string
|
||||
tags:
|
||||
- communication
|
||||
- crm
|
||||
- ai
|
||||
maintainers:
|
||||
- github: username
|
||||
|
||||
# 可選:共通片段繼承
|
||||
extends: string # recipe name,繼承其 schema 後覆寫
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. Recipe 繼承(reduce 重複)
|
||||
|
||||
Google 家族的 API 長得很像,重複寫 15 次太蠢。支援 `extends`:
|
||||
|
||||
```yaml
|
||||
# recipe/_google_base.yaml(底線開頭 = 抽象 recipe,不能直接用)
|
||||
service: _google_base
|
||||
version: 1
|
||||
primitive: service_account
|
||||
token_exchange:
|
||||
endpoint: https://oauth2.googleapis.com/token
|
||||
audience: https://oauth2.googleapis.com/token
|
||||
ttl_seconds: 3600
|
||||
required_secrets:
|
||||
- key: service_account_json
|
||||
type: json_blob
|
||||
inject:
|
||||
header:
|
||||
Authorization: "Bearer {{runtime.access_token}}"
|
||||
```
|
||||
|
||||
```yaml
|
||||
# recipe/google_calendar.yaml
|
||||
extends: _google_base
|
||||
service: google_calendar
|
||||
version: 1
|
||||
base_url: https://www.googleapis.com/calendar/v3
|
||||
token_exchange:
|
||||
scopes:
|
||||
- https://www.googleapis.com/auth/calendar
|
||||
test:
|
||||
method: GET
|
||||
path: /users/me/calendarList
|
||||
expect_status: 200
|
||||
```
|
||||
|
||||
繼承規則:scalar 覆寫,object 深度合併,array 預設覆寫(可用 `!append` 標記 append)。
|
||||
|
||||
---
|
||||
|
||||
## 6. TinyGo WASM Primitive 實作介面
|
||||
|
||||
### 6.1 統一介面(四個 primitive 都實作這個)
|
||||
|
||||
```go
|
||||
// primitive/interface.go
|
||||
package primitive
|
||||
|
||||
type AuthRequest struct {
|
||||
Method string
|
||||
URL string
|
||||
Headers map[string]string
|
||||
Body []byte
|
||||
}
|
||||
|
||||
type AuthContext struct {
|
||||
Recipe Recipe // parsed YAML
|
||||
Secret map[string]any // decrypted secret
|
||||
Runtime RuntimeState // oauth token cache 等
|
||||
Now int64 // for testing
|
||||
}
|
||||
|
||||
type Primitive interface {
|
||||
// 在 HTTP request 上注入認證資訊
|
||||
Authenticate(req *AuthRequest, ctx *AuthContext) error
|
||||
|
||||
// 檢查是否需要 refresh(oauth2 / service_account 用)
|
||||
NeedsRefresh(ctx *AuthContext) bool
|
||||
|
||||
// 執行 refresh / token exchange,回傳新的 RuntimeState
|
||||
Refresh(ctx *AuthContext) (RuntimeState, error)
|
||||
|
||||
// 驗證 credential 是否有效(執行 recipe.test)
|
||||
Test(ctx *AuthContext) error
|
||||
}
|
||||
```
|
||||
|
||||
### 6.2 編譯與部署
|
||||
|
||||
```bash
|
||||
# 四個 primitive 各自編譯成獨立 WASM
|
||||
tinygo build -o dist/static_key.wasm -target=wasi ./primitive/static_key
|
||||
tinygo build -o dist/oauth2.wasm -target=wasi ./primitive/oauth2
|
||||
tinygo build -o dist/service_account.wasm -target=wasi ./primitive/service_account
|
||||
tinygo build -o dist/mtls.wasm -target=wasi ./primitive/mtls
|
||||
|
||||
# 部署時放到 Cloudflare Workers 的 Assets 或直接內嵌
|
||||
```
|
||||
|
||||
### 6.3 Runtime 載入(Worker 端)
|
||||
|
||||
```typescript
|
||||
// worker/src/auth-broker.ts
|
||||
import staticKeyWasm from "../dist/static_key.wasm"
|
||||
import oauth2Wasm from "../dist/oauth2.wasm"
|
||||
// ...
|
||||
|
||||
const primitives = {
|
||||
static_key: await instantiate(staticKeyWasm),
|
||||
oauth2: await instantiate(oauth2Wasm),
|
||||
service_account: await instantiate(serviceAccountWasm),
|
||||
mtls: await instantiate(mtlsWasm),
|
||||
}
|
||||
```
|
||||
|
||||
> **為什麼 WASM 而不是直接 TS?**
|
||||
> 1. 跨 runtime 可攜性(未來若 arcrun 要跑在 Fly.io、local、或客戶自建環境,同一個 primitive 能用)。
|
||||
> 2. 配合 u6u/arcrun 既定的 WASM 架構方向,不破壞統一性。
|
||||
> 3. 沙箱化:primitive 只能透過明確的 host function 存取外部世界(網路、KV),降低惡意 recipe 攻擊面。
|
||||
|
||||
---
|
||||
|
||||
## 7. AuthBroker API(給 arcrun 其他部分調用)
|
||||
|
||||
```typescript
|
||||
interface AuthBroker {
|
||||
// Agent 執行時用的主要 API
|
||||
bind(serviceId: string, secretRef: string, tenantId: string): Promise<AuthenticatedClient>
|
||||
|
||||
// 首次授權(僅 oauth2 用)
|
||||
startAuth(serviceId: string, tenantId: string): Promise<{ authorizeUrl: string, state: string }>
|
||||
completeAuth(state: string, code: string): Promise<{ secretRef: string }>
|
||||
|
||||
// 測試 credential
|
||||
test(serviceId: string, secretRef: string, tenantId: string): Promise<TestResult>
|
||||
|
||||
// 管理
|
||||
listRecipes(): Promise<Recipe[]>
|
||||
getRecipe(serviceId: string): Promise<Recipe>
|
||||
}
|
||||
|
||||
interface AuthenticatedClient {
|
||||
fetch(path: string, init?: RequestInit): Promise<Response>
|
||||
}
|
||||
```
|
||||
|
||||
**使用範例(agent 端)**:
|
||||
```typescript
|
||||
const notion = await authBroker.bind("notion", "notion-prod", ctx.tenantId)
|
||||
const res = await notion.fetch("/databases/abc/query", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ filter: {...} })
|
||||
})
|
||||
```
|
||||
|
||||
**Agent 完全不需要知道是 API Key 還是 OAuth**——`authBroker.bind()` 回傳的 client 已經注入好認證,fetch 路徑用相對 base_url 的路徑即可。
|
||||
|
||||
---
|
||||
|
||||
## 8. Storage Layout
|
||||
|
||||
### 8.1 Recipe 儲存(arcrun 平台共享)
|
||||
|
||||
**Cloudflare KV namespace**:`arcrun-recipes`
|
||||
|
||||
```
|
||||
key: recipe/{service_id}
|
||||
value: <recipe YAML 的 JSON 化版本>
|
||||
|
||||
key: recipe-list
|
||||
value: [{ service_id, display_name, icon_url, tags }, ...] # 加速 UI 列表
|
||||
```
|
||||
|
||||
**更新流程**:
|
||||
1. Recipe YAML 存在 arcrun 主 repo 的 `recipes/` 目錄下(version control + PR review)。
|
||||
2. CI 跑 schema validator,通過後上傳到 KV。
|
||||
3. UI 的 recipe 列表 5 分鐘 cache。
|
||||
|
||||
### 8.2 Secret 儲存(tenant 私有)
|
||||
|
||||
**雙層策略**:
|
||||
- **短期、低敏感** → tenant KV,用 AES-256-GCM 加密,key 從 Cloudflare Secrets Store 拿。
|
||||
- **高敏感(如 service account JSON、private key)** → 直接存 Cloudflare Secrets Store,tenant KV 只存 reference。
|
||||
|
||||
```
|
||||
# tenant KV namespace: arcrun-tenant-{tenant_id}
|
||||
key: secret/{service_id}/{instance_name}
|
||||
value: {
|
||||
"recipe_version": 1,
|
||||
"storage_mode": "kv_encrypted" | "secrets_store_ref",
|
||||
"data": <encrypted blob> | { "ref": "secrets-store-id" },
|
||||
"created_at": "...",
|
||||
"last_verified_at": "..."
|
||||
}
|
||||
|
||||
# oauth2 runtime state(primitive 自動管理)
|
||||
key: oauth_state/{service_id}/{instance_name}
|
||||
value: {
|
||||
"access_token": "...", # encrypted
|
||||
"refresh_token": "...", # encrypted
|
||||
"expires_at": 1234567890
|
||||
}
|
||||
```
|
||||
|
||||
**secretRef 格式**:`{service_id}/{instance_name}`,例如 `notion/prod`、`google_calendar/workspace-a`。
|
||||
一個 tenant 可以同一個服務存多個 instance(多帳號場景)。
|
||||
|
||||
### 8.3 KBDB 整合(可選,但建議)
|
||||
|
||||
**按照 KBDB 架構,recipe metadata 可以用 Block + Template 表達**(不是 credential 本體,只是 metadata):
|
||||
|
||||
建立一個 `service_recipe` Template:
|
||||
```json
|
||||
{
|
||||
"name": "service_recipe",
|
||||
"display_name": "服務 Recipe Metadata",
|
||||
"schema": {
|
||||
"fields": [
|
||||
{"key": "service_id", "type": "text", "required": true, "description": "服務識別"},
|
||||
{"key": "primitive", "type": "text", "required": true, "description": "使用的 primitive"},
|
||||
{"key": "version", "type": "number", "required": true, "description": "Recipe 版本"},
|
||||
{"key": "display_name", "type": "text", "required": false, "description": "顯示名稱"},
|
||||
{"key": "docs_url", "type": "text", "required": false, "description": "文件 URL"},
|
||||
{"key": "kv_key", "type": "text", "required": true, "description": "KV 實際存取 key"}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Secret **不**進 KBDB(KBDB 不該存敏感資料),只有 metadata 在 KBDB 裡方便搜尋和關聯。
|
||||
|
||||
---
|
||||
|
||||
## 9. 首次授權 UI Flow(給人類看的部分)
|
||||
|
||||
這是「學員不知道該選哪個 credential 的痛點」的終結方案。
|
||||
|
||||
### 9.1 Static Key 的 UI
|
||||
|
||||
```
|
||||
┌──────────────────────────────────────────┐
|
||||
│ 連接 Notion │
|
||||
├──────────────────────────────────────────┤
|
||||
│ │
|
||||
│ Internal Integration Token │
|
||||
│ ┌────────────────────────────────────┐ │
|
||||
│ │ secret_••••••••••••• │ │
|
||||
│ └────────────────────────────────────┘ │
|
||||
│ ↳ 如何取得?→ 開啟 Notion 整合設定頁 │
|
||||
│ │
|
||||
│ [ 測試連線 ] [ 儲存 ] │
|
||||
└──────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
**零選項。** UI 從 recipe 的 `required_secrets` 動態生成。使用者不用選「這是 Header Auth 還是 Query Auth 還是 Custom Auth」——那是 recipe 的事,不是使用者的事。
|
||||
|
||||
### 9.2 OAuth2 的 UI
|
||||
|
||||
```
|
||||
┌──────────────────────────────────────────┐
|
||||
│ 連接 GitHub │
|
||||
├──────────────────────────────────────────┤
|
||||
│ │
|
||||
│ [ 🔗 使用 GitHub 帳號登入 ] │
|
||||
│ │
|
||||
│ 將跳轉到 GitHub,授權後自動返回 │
|
||||
│ │
|
||||
└──────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
**一個按鈕。** Client ID / Secret 由 arcrun 平台統一管理(OAuth App 註冊在 arcrun 這邊),使用者看不到也不用知道。
|
||||
|
||||
### 9.3 Service Account 的 UI
|
||||
|
||||
```
|
||||
┌──────────────────────────────────────────┐
|
||||
│ 連接 Google Calendar │
|
||||
├──────────────────────────────────────────┤
|
||||
│ │
|
||||
│ Service Account JSON │
|
||||
│ ┌────────────────────────────────────┐ │
|
||||
│ │ 將整份 JSON 貼到這裡 │ │
|
||||
│ │ │ │
|
||||
│ │ { │ │
|
||||
│ │ "type": "service_account", │ │
|
||||
│ │ "project_id": "...", │ │
|
||||
│ │ ... │ │
|
||||
│ │ } │ │
|
||||
│ └────────────────────────────────────┘ │
|
||||
│ │
|
||||
│ 如何取得?→ 展開步驟說明 ▼ │
|
||||
│ 1. 打開 GCP Console │
|
||||
│ 2. IAM → Service Accounts │
|
||||
│ 3. 建立 Service Account │
|
||||
│ 4. Keys → Add Key → JSON │
|
||||
│ 5. 下載後整份貼到上方 │
|
||||
│ │
|
||||
│ [ 測試連線 ] [ 儲存 ] │
|
||||
└──────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
**貼 JSON + 按鈕**。不用寫任何程式碼,不用 debug 兩天。
|
||||
|
||||
---
|
||||
|
||||
## 10. 實作任務分解(CC 的 TODO list)
|
||||
|
||||
### Phase 1:核心骨架(1-2 週)
|
||||
|
||||
- [ ] **T1.1** Recipe YAML schema 定義 + JSON Schema validator(放 `arcrun/schemas/recipe.schema.json`)
|
||||
- [ ] **T1.2** Recipe loader:從 `recipes/` 目錄讀 YAML → validate → 轉 JSON 存入 KV namespace `arcrun-recipes`
|
||||
- [ ] **T1.3** TinyGo WASM 專案骨架(`arcrun/primitives/`),四個子目錄,統一 interface
|
||||
- [ ] **T1.4** Worker runtime 的 WASM loader + host function(網路、KV 讀寫)
|
||||
- [ ] **T1.5** `AuthBroker` TypeScript 類別骨架 + unit test
|
||||
|
||||
### Phase 2:Static Key(1 週)
|
||||
|
||||
- [ ] **T2.1** `static_key.wasm` 實作(header/query/body/basic_auth 四種注入)
|
||||
- [ ] **T2.2** 寫三個 recipe:`notion.yaml`, `openai.yaml`, `stripe.yaml`
|
||||
- [ ] **T2.3** Tenant KV secret 加密寫入 + `AuthBroker.bind()` 整合
|
||||
- [ ] **T2.4** `recipe.test` 執行器(驗證 credential 有效性)
|
||||
- [ ] **T2.5** E2E test:存 secret → bind → fetch Notion API → assert
|
||||
|
||||
### Phase 3:OAuth2(1-2 週)
|
||||
|
||||
- [ ] **T3.1** `oauth2.wasm` 實作(authorization_code + client_credentials + pkce)
|
||||
- [ ] **T3.2** OAuth callback endpoint(統一 URL,用 state 路由到正確 tenant/recipe)
|
||||
- [ ] **T3.3** Refresh token 自動續命邏輯(rate-limit 保護:同一 token 不能 1 秒內 refresh 多次)
|
||||
- [ ] **T3.4** 寫三個 recipe:`github.yaml`, `slack.yaml`, `google_oauth_user.yaml`
|
||||
- [ ] **T3.5** UI flow:startAuth → 跳轉 → callback → 寫 secret
|
||||
|
||||
### Phase 4:Service Account(1 週)
|
||||
|
||||
- [ ] **T4.1** `service_account.wasm` 實作(google_jwt)
|
||||
- [ ] **T4.2** Google JWT signing(ES256 / RS256)— **這個 TinyGo 需要注意 crypto 支援**
|
||||
- [ ] **T4.3** AWS SigV4 簽章實作(kind: aws_sigv4)
|
||||
- [ ] **T4.4** Recipe 繼承機制(`extends` 支援)
|
||||
- [ ] **T4.5** 寫 recipes:`_google_base`, `google_calendar`, `google_drive`, `gmail`, `aws_s3`
|
||||
|
||||
### Phase 5:mTLS + 收尾(1 週)
|
||||
|
||||
- [ ] **T5.1** `mtls.wasm` 實作(對接 Cloudflare `mTLSCertificate` binding)
|
||||
- [ ] **T5.2** Cloudflare Secrets Store 整合(高敏感 secret 用)
|
||||
- [ ] **T5.3** Recipe marketplace UI(列出可用 recipe,搜尋,一鍵設定)
|
||||
- [ ] **T5.4** Observability:每次 bind / refresh / test 記錄到 KBDB(metadata,不含 secret)
|
||||
- [ ] **T5.5** Docs:recipe 撰寫指南(讓社群能貢獻)
|
||||
|
||||
### Phase 6:Recipe 生成器(選配,1 週)
|
||||
|
||||
- [ ] **T6.1** 給 Claude 一份 API doc,自動產 recipe YAML 草稿 + 人類 review 介面
|
||||
- [ ] **T6.2** 從 OpenAPI spec 自動推論 recipe
|
||||
- [ ] **T6.3** 從 n8n credential file 反向轉譯(擷取 400+ 現成整合)
|
||||
|
||||
---
|
||||
|
||||
## 11. 關鍵技術風險與對策
|
||||
|
||||
| 風險 | 對策 |
|
||||
|---|---|
|
||||
| **TinyGo 的 crypto 支援不完整**(ES256 / RS256 JWT 簽章) | 先用 `crypto/rsa` + `crypto/ecdsa` 確認 TinyGo 版本支援;若不行,fallback 用 Worker runtime 的 `crypto.subtle` 實作這部分,WASM 透過 host function 呼叫 |
|
||||
| **Recipe 被惡意提交**(如 inject 內含 `https://evil.com` 當 token_url) | Recipe 走 PR review + CI 自動檢查 URL 白名單;社群貢獻的 recipe 預設隔離在 `community/` 目錄,使用者明確選擇才啟用 |
|
||||
| **OAuth state CSRF** | state 用 `crypto.randomUUID()` + 5 分鐘 TTL,存在 KV,callback 時比對 |
|
||||
| **Secret 在 Worker log 外洩** | `AuthContext.Secret` 禁止 `toString` / `JSON.stringify`,用 Proxy 攔截;log 層強制 redact |
|
||||
| **Token refresh 風暴**(100 個並發 request 同時發現過期) | 用 Durable Object 單執行緒化每個 secret 的 refresh,其他 request 等結果 |
|
||||
| **TinyGo WASM bundle size** | 四個 primitive 分開編譯,最大 500KB/個;lazy load |
|
||||
| **Recipe 版本升級破壞相容** | `version` 欄位 semver,tenant secret 記錄 `recipe_version`,primitive 內處理遷移 |
|
||||
|
||||
---
|
||||
|
||||
## 12. 對比 n8n(給內部 review / 行銷用)
|
||||
|
||||
| 維度 | n8n | arcrun |
|
||||
|---|---|---|
|
||||
| Credential types 數量 | 400+(一個服務一個) | 4(primitive) + N recipe |
|
||||
| 新增一個服務 | 寫 TypeScript class + rebuild + npm publish | 寫一份 YAML + PR merge |
|
||||
| AI agent 使用 | 需要讀 node 文件 + 猜參數 | 讀 recipe YAML 即可 |
|
||||
| 使用者首次設定 | 從 400+ 選項選一個(常選錯) | 搜尋服務名,只問必要 secret |
|
||||
| OAuth App 管理 | 使用者自己註冊 OAuth app | arcrun 平台統一管理(使用者只需點「授權」) |
|
||||
| 社群貢獻成本 | 高(TS + 編譯 + 測試) | 低(YAML + 測試) |
|
||||
|
||||
---
|
||||
|
||||
## 13. 接下來的決策點(需要 richblack 確認)
|
||||
|
||||
- [ ] **Recipe 版本管理策略**:採用 semver?每個 recipe 獨立版本?還是整個 recipe set 一個版本?
|
||||
- [ ] **OAuth App 註冊**:arcrun 平台要統一註冊幾個主流服務的 OAuth App(GitHub、Google、Slack、Microsoft)?還是讓 tenant 自己帶 client_id/secret?
|
||||
- 建議:**雙模式**——平台模式(方便)+ BYO 模式(企業客戶用自己的 OAuth app 有稽核好處)
|
||||
- [ ] **Recipe registry 的審核流程**:完全開放 PR 還是僅核心團隊維護?
|
||||
- 建議:`recipes/official/`(核心維護)+ `recipes/community/`(PR 審核後 merge,使用者需明確啟用)
|
||||
- [ ] **Secret rotation 政策**:要不要內建提醒 / 自動 rotate?(Phase 7+)
|
||||
|
||||
---
|
||||
|
||||
## 14. 附錄:完整範例
|
||||
|
||||
### A. 最小可行 recipe(OpenAI)
|
||||
|
||||
```yaml
|
||||
service: openai
|
||||
version: 1
|
||||
primitive: static_key
|
||||
display_name: OpenAI
|
||||
base_url: https://api.openai.com/v1
|
||||
required_secrets:
|
||||
- key: api_key
|
||||
label: API Key
|
||||
help_url: https://platform.openai.com/api-keys
|
||||
inject:
|
||||
header:
|
||||
Authorization: "Bearer {{secret.api_key}}"
|
||||
test:
|
||||
method: GET
|
||||
path: /models
|
||||
expect_status: 200
|
||||
tags: [ai]
|
||||
```
|
||||
|
||||
### B. OAuth2 recipe(Slack)
|
||||
|
||||
```yaml
|
||||
service: slack
|
||||
version: 1
|
||||
primitive: oauth2
|
||||
display_name: Slack
|
||||
base_url: https://slack.com/api
|
||||
grant: authorization_code
|
||||
oauth:
|
||||
authorize_url: https://slack.com/oauth/v2/authorize
|
||||
token_url: https://slack.com/api/oauth.v2.access
|
||||
scopes:
|
||||
- chat:write
|
||||
- channels:read
|
||||
refresh: true
|
||||
client_auth: header
|
||||
inject:
|
||||
header:
|
||||
Authorization: "Bearer {{runtime.access_token}}"
|
||||
test:
|
||||
method: POST
|
||||
path: /auth.test
|
||||
expect_json:
|
||||
ok: true
|
||||
tags: [communication]
|
||||
```
|
||||
|
||||
### C. Service Account recipe(Google Calendar)
|
||||
|
||||
```yaml
|
||||
extends: _google_base
|
||||
service: google_calendar
|
||||
version: 1
|
||||
display_name: Google Calendar
|
||||
base_url: https://www.googleapis.com/calendar/v3
|
||||
token_exchange:
|
||||
scopes:
|
||||
- https://www.googleapis.com/auth/calendar
|
||||
test:
|
||||
method: GET
|
||||
path: /users/me/calendarList
|
||||
expect_status: 200
|
||||
tags: [calendar, google]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 15. 給 CC 的行動指引
|
||||
|
||||
1. **先不要動既有 arcrun 的 credential 相關 code**,保持現狀到 Phase 2 完成再切換。
|
||||
2. **Phase 1 + 2 是 MVP**,做完可以接 80% 服務(API key 類)。
|
||||
3. **遇到 TinyGo 的技術阻礙(特別是 crypto),立刻回報**,不要自己 workaround 兩天。
|
||||
4. 每個 Phase 完成後寫一份 brief report(能跑什麼、不能跑什麼、下一步)。
|
||||
5. Recipe 撰寫先做 3-5 個手工範例,**確認 schema 夠用再開始批量生成**。
|
||||
@@ -0,0 +1,521 @@
|
||||
# u6u-core 獨立開源 Repo 需求規格 v3
|
||||
|
||||
## 背景與定位
|
||||
|
||||
### 為什麼開源
|
||||
|
||||
u6u-core 是 AI 工作流執行引擎,開源的護城河邏輯如下:
|
||||
|
||||
```
|
||||
開源(u6u-core) 閉源(InkStone 付費服務)
|
||||
──────────────────────── ──────────────────────────────
|
||||
cypher-executor(執行引擎) KBDB 向量搜尋
|
||||
WASM 零件庫(Gmail / GSheets…) KBDB graph 查詢
|
||||
credentials Worker Persona SDK / Mini-me
|
||||
CLI MatchGPT
|
||||
→ 需要訂閱,不需要 YAML / KV
|
||||
```
|
||||
|
||||
用戶自架版:YAML + CF KV,完全免費。
|
||||
升級版:不需要 YAML,直接用自然語言查 KBDB 圖譜組 workflow,這是差異化。
|
||||
|
||||
### 目前 matrix repo 狀況
|
||||
|
||||
```
|
||||
matrix/
|
||||
├── cypher-executor/ ← 要搬進 u6u-core
|
||||
├── u6u-core/
|
||||
│ ├── builtins/
|
||||
│ ├── credentials/
|
||||
│ └── registry/
|
||||
│ └── components/(21 個 WASM 零件)
|
||||
└── ...(其他 InkStone 內部服務,不搬)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 任務一:搬移 cypher-executor 進 u6u-core
|
||||
|
||||
### 目標結構
|
||||
|
||||
```
|
||||
u6u-core/(新獨立 repo,開源)
|
||||
├── README.md
|
||||
├── cypher-executor/ ← 從 matrix/cypher-executor 搬入
|
||||
│ ├── src/
|
||||
│ ├── wrangler.toml ← 需要清理(移除 InkStone 內部 bindings)
|
||||
│ └── ...
|
||||
├── credentials/ ← 從 matrix/u6u-core/credentials 搬入
|
||||
├── builtins/ ← 從 matrix/u6u-core/builtins 搬入
|
||||
└── registry/
|
||||
└── components/ ← 從 matrix/u6u-core/registry/components 搬入
|
||||
```
|
||||
|
||||
### wrangler.toml 清理(重要)
|
||||
|
||||
現有 `cypher-executor/wrangler.toml` 有大量 InkStone 內部 Service Bindings,開源版要移除:
|
||||
|
||||
**移除(InkStone 專屬,不公開):**
|
||||
```toml
|
||||
# 移除這些 services bindings:
|
||||
KBDB → inkstone-kbdb-api
|
||||
REGISTRY → inkstone-component-registry
|
||||
CLINIC_GDRIVE → clinic-gdrive
|
||||
CLINIC_EXCEL → clinic-excel
|
||||
CLINIC_ANALYSIS
|
||||
CLINIC_RENDER
|
||||
CLINIC_GSHEETS
|
||||
AICEO → inkstone-aiceo-bot
|
||||
MINI_ME → inkstone-mini-me
|
||||
```
|
||||
|
||||
**保留(用戶自己部署需要的):**
|
||||
```toml
|
||||
[[kv_namespaces]]
|
||||
binding = "EXEC_CONTEXT" # 執行上下文暫存
|
||||
|
||||
[[kv_namespaces]]
|
||||
binding = "WEBHOOKS" # workflow YAML 儲存
|
||||
|
||||
[[r2_buckets]]
|
||||
binding = "WASM_BUCKET" # WASM 零件二進位
|
||||
|
||||
[ai]
|
||||
binding = "AI" # Workers AI(auto-publish 用)
|
||||
```
|
||||
|
||||
**新增(開源版用戶需要的):**
|
||||
```toml
|
||||
[[kv_namespaces]]
|
||||
binding = "CREDENTIALS_KV" # credential 加密存儲
|
||||
```
|
||||
|
||||
### 清理後的 component-loader
|
||||
|
||||
現有 component-loader 可能有 InkStone 內部查詢邏輯(KBDB HTTP fetch),
|
||||
開源版改為:**直接從 WASM_BUCKET R2 讀取 `.wasm` 檔案**,不依賴任何外部服務。
|
||||
|
||||
---
|
||||
|
||||
## 任務二:零件完成度審查與補充
|
||||
|
||||
### 完成度標準
|
||||
|
||||
每個零件的 `component.contract.yaml` 必須包含:
|
||||
|
||||
```yaml
|
||||
# 已有(現狀)
|
||||
canonical_id: "gmail"
|
||||
input_schema: ...
|
||||
output_schema: ...
|
||||
gherkin_tests: ...
|
||||
|
||||
# 需要補充
|
||||
credentials_required: # 需要 token 的零件才需要此欄位
|
||||
- key: gmail_token # 對應 credentials.yaml 的 key 名稱慣例
|
||||
type: google_oauth # token 類型
|
||||
description: "Google OAuth access token(gmail.send scope)"
|
||||
inject_as: access_token # 執行時自動注入到 input 的哪個欄位
|
||||
|
||||
config_example: | # scaffold 指令產出的範本,帶說明註解
|
||||
send_email: # 節點名稱(可自訂)
|
||||
to: "" # 收件人 Email(必填)
|
||||
subject: "" # 主旨(必填)
|
||||
body: "" # 內文(必填)
|
||||
# access_token 由 credentials.yaml 的 gmail_token 自動注入
|
||||
```
|
||||
|
||||
### 需要 credentials_required 的零件
|
||||
|
||||
| 零件 | 需要的 token | inject_as |
|
||||
|------|-------------|-----------|
|
||||
| gmail | google_oauth | access_token |
|
||||
| google_sheets | google_oauth | access_token |
|
||||
| telegram | telegram_bot_token | bot_token |
|
||||
| line_notify | line_token | token |
|
||||
| http_request | 不固定(用戶自訂) | 不適用 |
|
||||
|
||||
### 不需要 credentials_required 的零件
|
||||
|
||||
set, filter, merge, switch, wait, if_control, foreach_control,
|
||||
try_catch, validate_json, string_ops, number_ops, array_ops,
|
||||
date_ops, cron, ai_transform_compile, ai_transform_run
|
||||
|
||||
### 審查任務(給 CC)
|
||||
|
||||
對 21 個零件逐一檢查,**只回報,不修改**:
|
||||
|
||||
```
|
||||
路徑:u6u-core/registry/components/
|
||||
|
||||
檢查四項:
|
||||
1. contract.yaml 存在?
|
||||
2. 有 credentials_required?(需要 token 的才需要)
|
||||
3. 有 config_example?
|
||||
4. main.go required 欄位與 contract input_schema required[] 一致?
|
||||
|
||||
回報格式:表格(✓ / ✗ / N/A)+ 每個零件缺少什麼
|
||||
不修改任何檔案。
|
||||
```
|
||||
|
||||
審查完成後,再逐一補充缺少的欄位。
|
||||
|
||||
---
|
||||
|
||||
## 任務三:workflow YAML 格式定義
|
||||
|
||||
### 格式設計原則
|
||||
|
||||
- `flow:` 用 `>>` 三元組描述資料流,人類直接看懂
|
||||
- 關係詞使用有語意的詞,**不使用 PIPE**(PIPE 等於什麼都沒說)
|
||||
- `config:` 用零件名稱對應參數,欄位從 contract 的 config_example 來
|
||||
- credential 全部集中在 `credentials.yaml`,workflow 只寫 `{{creds.KEY}}`
|
||||
|
||||
### 可用關係詞
|
||||
|
||||
| 關係詞 | 語意 | 使用時機 |
|
||||
|--------|------|---------|
|
||||
| `完成後` | 前一個成功後執行 | 最常用的串接 |
|
||||
| `失敗時` | 前一個失敗後執行 | 錯誤處理 |
|
||||
| `對每個` | 對陣列每個元素執行 | 迭代 |
|
||||
| `條件滿足時` | 條件分支 | 判斷 |
|
||||
| `ON_SUCCESS` | 同「完成後」 | 英文版 |
|
||||
| `ON_FAIL` | 同「失敗時」 | 英文版 |
|
||||
| `FOREACH` | 同「對每個」 | 英文版 |
|
||||
| `IF` | 同「條件滿足時」 | 英文版 |
|
||||
| `ON_CLICK` | 前端按鈕觸發 | UI 互動 |
|
||||
| `CALLS_SUBFLOW` | 呼叫子工作流 | 模組化 |
|
||||
|
||||
**禁止使用 PIPE** — 任何串接都應該用有語意的關係詞。
|
||||
|
||||
### workflow.yaml 範例
|
||||
|
||||
```yaml
|
||||
name: newsletter_subscribe
|
||||
description: 訂閱電子報,發感謝信並記錄到 GSheets
|
||||
|
||||
flow:
|
||||
- "input >> 完成後 >> send_thanks"
|
||||
- "input >> 完成後 >> save_to_sheet"
|
||||
- "send_thanks >> 完成後 >> output"
|
||||
- "send_thanks >> 失敗時 >> notify_error"
|
||||
- "save_to_sheet >> 完成後 >> output"
|
||||
|
||||
config:
|
||||
send_thanks: # componentId: gmail(由 cypher-executor 語意搜尋對應)
|
||||
to: "{{input.email}}"
|
||||
subject: "感謝訂閱!"
|
||||
body: "歡迎加入!"
|
||||
# access_token 由 credentials.yaml 的 gmail_token 自動注入
|
||||
|
||||
save_to_sheet: # componentId: google_sheets
|
||||
action: write
|
||||
spreadsheet_id: "{{creds.sheet_id}}"
|
||||
range: "訂閱者!A:B"
|
||||
values: [["{{input.email}}", "{{input.timestamp}}"]]
|
||||
# access_token 由 credentials.yaml 的 google_oauth 自動注入
|
||||
|
||||
notify_error: # componentId: telegram
|
||||
chat_id: "{{creds.telegram_chat_id}}"
|
||||
text: "發信失敗:{{input.email}}"
|
||||
# bot_token 由 credentials.yaml 的 telegram_bot_token 自動注入
|
||||
```
|
||||
|
||||
### credentials.yaml 範例
|
||||
|
||||
```yaml
|
||||
# credentials.yaml — 類似 .env,加入 .gitignore,不進 git
|
||||
# u6u creds push 時逐一加密上傳到 CREDENTIALS_KV
|
||||
|
||||
gmail_token: "ya29.a0AfB_..."
|
||||
google_oauth: "ya29.a0AfB_..."
|
||||
sheet_id: "1BxiMVs0XRA5nFMdKvBdBZjgmUUqptlbs74OgVE2upms"
|
||||
telegram_bot_token: "123456:ABC-..."
|
||||
telegram_chat_id: "987654321"
|
||||
```
|
||||
|
||||
### 執行時 credential 注入流程
|
||||
|
||||
```
|
||||
u6u run newsletter_subscribe
|
||||
↓
|
||||
cypher-executor 讀 workflow YAML
|
||||
↓
|
||||
遇到節點 send_thanks → 查 contract:credentials_required.inject_as = access_token
|
||||
↓
|
||||
去 CREDENTIALS_KV 讀 gmail_token → 解密
|
||||
↓
|
||||
注入到 WASM input:{ to, subject, body, access_token: "ya29..." }
|
||||
↓
|
||||
WASM 執行,用戶的 config 裡完全不出現 token
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 任務四:CLI 開發
|
||||
|
||||
### 技術選型
|
||||
|
||||
- **語言**:Node.js(TypeScript)
|
||||
- **安裝**:`npm i -g u6u`
|
||||
- **依賴**:`commander`、`js-yaml`、`chalk`、`ora`
|
||||
|
||||
### 指令規格
|
||||
|
||||
#### `u6u init`
|
||||
|
||||
互動式初始化,產生 `~/.u6u/config.yaml` 和本機 `credentials.yaml`。
|
||||
|
||||
```
|
||||
$ u6u init
|
||||
? Cloudflare Account ID: abc123
|
||||
? KV Namespace ID (WEBHOOKS): xyz789
|
||||
? KV Namespace ID (CREDENTIALS_KV): abc456
|
||||
? R2 Bucket name (WASM_BUCKET): u6u-wasm
|
||||
? Cypher Executor Worker URL: https://cypher-executor.xxx.workers.dev
|
||||
? Credentials Worker URL: https://u6u-credentials.xxx.workers.dev
|
||||
? Cloudflare API Token: ***
|
||||
|
||||
✓ 設定完成 → ~/.u6u/config.yaml
|
||||
✓ 建立 credentials.yaml(已加入 .gitignore)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
#### `u6u creds push [credentials.yaml]`
|
||||
|
||||
讀取 credentials.yaml,逐一加密上傳到 CREDENTIALS_KV。
|
||||
|
||||
```
|
||||
$ u6u creds push
|
||||
讀取 ./credentials.yaml...
|
||||
✓ gmail_token → 已加密上傳
|
||||
✓ google_oauth → 已加密上傳
|
||||
✓ sheet_id → 已上傳
|
||||
✓ telegram_bot_token → 已加密上傳
|
||||
✓ telegram_chat_id → 已上傳
|
||||
共上傳 5 個 credentials
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
#### `u6u push <workflow.yaml>`
|
||||
|
||||
解析 `flow:` 三元組,轉換成 triplets 陣列,上傳到 WEBHOOKS KV。
|
||||
|
||||
```
|
||||
$ u6u push newsletter_subscribe.yaml
|
||||
✓ 已上傳 newsletter_subscribe → WEBHOOKS KV
|
||||
Webhook: https://cypher-executor.xxx.workers.dev/webhook/abc123
|
||||
```
|
||||
|
||||
轉換邏輯(CLI 負責):
|
||||
|
||||
```
|
||||
flow[] 三元組
|
||||
↓
|
||||
POST /cypher/search(取得 ExecutionGraph)
|
||||
↓
|
||||
連同 config 存入 WEBHOOKS KV
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
#### `u6u run <workflow_name> [--input key=value...]`
|
||||
|
||||
觸發執行,顯示結果。
|
||||
|
||||
```
|
||||
$ u6u run newsletter_subscribe --input email=test@example.com
|
||||
⏳ 執行中...
|
||||
✓ 完成(2.3s)
|
||||
|
||||
結果:
|
||||
send_thanks: { success: true, data: { message_id: "xxx" } }
|
||||
save_to_sheet: { success: true, data: { range: "訂閱者!A2" } }
|
||||
```
|
||||
|
||||
錯誤時給出具體修復步驟:
|
||||
|
||||
```
|
||||
✗ 執行失敗:節點 send_thanks
|
||||
原因: access_token 無效(401 Unauthorized)
|
||||
|
||||
修復方式:
|
||||
1. 更新 credentials.yaml 的 gmail_token
|
||||
2. 執行 u6u creds push
|
||||
3. 重新執行 u6u run newsletter_subscribe
|
||||
|
||||
取得 Google OAuth token:
|
||||
→ https://developers.google.com/oauthplayground
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
#### `u6u validate <workflow.yaml>`
|
||||
|
||||
執行前完整驗證,提前發現問題。
|
||||
|
||||
```
|
||||
$ u6u validate newsletter_subscribe.yaml
|
||||
✓ YAML 格式正確
|
||||
✓ flow 三元組語法正確
|
||||
✓ 所有關係詞有效(無 PIPE)
|
||||
✓ 所有節點名稱在 config 有對應
|
||||
✓ 所有零件存在於 WASM_BUCKET
|
||||
✓ credentials 對應:
|
||||
gmail_token ✓ 已上傳
|
||||
google_oauth ✓ 已上傳
|
||||
sheet_id ✓ 已上傳
|
||||
telegram_bot_token ✗ 缺少
|
||||
|
||||
⚠ 缺少 1 個 credential:
|
||||
telegram_bot_token → 請加入 credentials.yaml 並執行 u6u creds push
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
#### `u6u parts`
|
||||
|
||||
列出可用零件。
|
||||
|
||||
```
|
||||
$ u6u parts
|
||||
可用零件(21):
|
||||
|
||||
[整合]
|
||||
• gmail Gmail 發信
|
||||
需要: to, subject, body
|
||||
credential: gmail_token(google_oauth)
|
||||
• google_sheets 讀寫 Google 試算表
|
||||
需要: spreadsheet_id, range, action
|
||||
credential: google_oauth
|
||||
• telegram Telegram Bot 發訊息
|
||||
需要: chat_id, text
|
||||
credential: telegram_bot_token
|
||||
• line_notify LINE Notify
|
||||
需要: message
|
||||
credential: line_token
|
||||
• http_request 任意 HTTP 請求
|
||||
需要: url
|
||||
|
||||
[控制]
|
||||
• if_control 條件分支
|
||||
• foreach_control 迭代執行
|
||||
• try_catch 錯誤處理
|
||||
• switch 多路路由
|
||||
• wait 等待 N 毫秒
|
||||
|
||||
[資料]
|
||||
• set / filter / merge / string_ops / number_ops / array_ops / date_ops
|
||||
|
||||
[AI]
|
||||
• ai_transform_compile 自然語言 → JS 轉換函式
|
||||
• ai_transform_run 執行已編譯的轉換
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
#### `u6u parts scaffold <component>`
|
||||
|
||||
從 contract 的 config_example 產出可直接貼入 workflow 的 config 範本。
|
||||
|
||||
```
|
||||
$ u6u parts scaffold gmail
|
||||
|
||||
貼入 workflow.yaml 的 config 區塊:
|
||||
|
||||
send_email: # 節點名稱(可自訂)
|
||||
to: "" # 收件人 Email(必填)
|
||||
subject: "" # 主旨(必填)
|
||||
body: "" # 內文(必填)
|
||||
# access_token 由 credentials.yaml 的 gmail_token 自動注入
|
||||
|
||||
貼入 credentials.yaml:
|
||||
|
||||
gmail_token: "" # Google OAuth token
|
||||
# 取得方式:https://developers.google.com/oauthplayground
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
#### `u6u list`
|
||||
|
||||
列出 WEBHOOKS KV 中所有 workflow。
|
||||
|
||||
```
|
||||
$ u6u list
|
||||
• newsletter_subscribe (更新: 2026-04-16)
|
||||
• daily_summary (更新: 2026-04-15)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
#### `u6u logs <workflow_name>`
|
||||
|
||||
查看最近執行記錄。
|
||||
|
||||
```
|
||||
$ u6u logs newsletter_subscribe
|
||||
2026-04-16 14:30 ✓ 成功 2.1s
|
||||
2026-04-16 09:00 ✗ 失敗 send_thanks: 401 Unauthorized
|
||||
2026-04-15 09:00 ✓ 成功 1.8s
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 開發順序
|
||||
|
||||
### Phase 1:搬移與清理(先做)
|
||||
|
||||
```
|
||||
1. 建立新的獨立 repo:u6u-core
|
||||
2. 從 matrix 搬入:
|
||||
- cypher-executor/
|
||||
- u6u-core/credentials/
|
||||
- u6u-core/builtins/
|
||||
- u6u-core/registry/
|
||||
3. 清理 cypher-executor/wrangler.toml(移除 InkStone 內部 bindings)
|
||||
4. 確認 component-loader 只依賴 WASM_BUCKET,不依賴 KBDB / REGISTRY
|
||||
5. 本機部署測試
|
||||
```
|
||||
|
||||
### Phase 2:零件完成度(搬移後)
|
||||
|
||||
```
|
||||
6. 審查 21 個零件的 contract.yaml
|
||||
7. 補充 credentials_required(gmail, google_sheets, telegram, line_notify)
|
||||
8. 補充 config_example(全部 21 個)
|
||||
9. 驗證 main.go required 欄位與 contract 一致
|
||||
```
|
||||
|
||||
### Phase 3:CLI(完成度補充後)
|
||||
|
||||
```
|
||||
10. u6u init
|
||||
11. u6u creds push
|
||||
12. u6u push
|
||||
13. u6u run(含 credential 自動注入)
|
||||
14. u6u parts / u6u parts scaffold
|
||||
15. u6u validate
|
||||
16. u6u list / u6u logs
|
||||
```
|
||||
|
||||
### Phase 4:開源發布
|
||||
|
||||
```
|
||||
17. 撰寫 README.md(快速開始、零件列表、workflow 語法說明)
|
||||
18. 撰寫 CONTRIBUTING.md(如何新增零件)
|
||||
19. 發布到 GitHub
|
||||
20. npm publish(u6u CLI)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 不在此次範圍
|
||||
|
||||
- KBDB 整合(未來付費服務)
|
||||
- 向量搜尋 / graph 查詢
|
||||
- 前端管理介面
|
||||
- Webhook trigger 設定(用戶自行設定 CF Cron)
|
||||
- 新增 WASM 零件(現有 21 個先做完整,之後再擴充)
|
||||
@@ -0,0 +1,89 @@
|
||||
# **u6u 智慧前端與工匠 AI 開發藍圖 (v4.0)**
|
||||
|
||||
## **一、 核心設計理念:意圖導向的雙面畫布**
|
||||
|
||||
u6u 的前端不是傳統的平面繪圖板,而是一個類似 Android Studio 或 Figma 的\*\*「結構化標籤編輯器」**。 畫布上的每一個元件(Web Component),本質上都是一個**「意圖發射器 (Intent Emitter)」\*\*。前端只負責「長得好看」與「收集人類動作」,後端全權負責「業務邏輯」。
|
||||
|
||||
### **人機協作的「雙向同步」**
|
||||
|
||||
* **AI 詠唱修改:** 人類說「把按鈕改醒目一點」,CEO AI 在背景將 \<u6u-btn color="blue"\> 修改為 \<u6u-btn color="neon-red"\>,畫面瞬間更新。
|
||||
* **人類手動覆寫:** 人類覺得 AI 調的紅色太暗,直接在右側屬性面板 (Properties Panel) 點選色碼器,底層 HTML 屬性隨之改變。**AI 也能「看見」這個改變,從中學習人類的審美偏好。**
|
||||
|
||||
## **二、 畫布介面設計與運作機制**
|
||||
|
||||
### **1\. 正反面翻轉機制 (The "Flip" Interface)**
|
||||
|
||||
每個 UI 零件在畫布上都有「一體兩面」:
|
||||
|
||||
* **正面 (UI 視圖):** 顯示 HTML 渲染的視覺結果(按鈕、溫度計、圖表)。人類可以在此調整 CSS 屬性、對齊方式與主題顏色。
|
||||
* **反面 (邏輯視圖):** 點擊「翻面」按鈕後,會進入底層的工作流設定。這裡使用 u6u 的自定義 Cypher 視覺化語法(例如 \>\> 符號)。
|
||||
* *範例:* \[ UI\_Button: "緊急停機" \] \>\> (Intent: emergency\_stop) \>\> \[ WASM: gsheets\_create \]
|
||||
|
||||
### **2\. 智慧容器與區域感知 (Smart Zone Awareness)**
|
||||
|
||||
為了消滅傳統 iPaaS(如 n8n)最痛苦的「手動變數綁定」,u6u 畫布具備「區域感知」能力。
|
||||
|
||||
* **底層邏輯(獨立元件):** 畫布上的 TextInput 與 Button 都是各自獨立的原子元件。
|
||||
* **麻瓜體驗(智慧表單):** 當使用者將這兩個元件拖入同一個排版容器(例如 \<u6u-card\>)時,系統會自動建立上下文關聯。當按下按鈕並觸發 Webhook 時,按鈕會**自動打包同容器內所有輸入框的值**一併送出:
|
||||
{
|
||||
"intent": "query\_attendance",
|
||||
"payload": { "employee\_id": "A1234" } // 自動從旁邊的 TextInput 抓取
|
||||
}
|
||||
|
||||
使用者完全不需要理解「表單傳值」或「變數綁定」,拖拉組合即生效。
|
||||
|
||||
### **3\. 多重事件插槽與靜態屬性 (Multi-Event Slots)**
|
||||
|
||||
一個前端元件可以具備多種觸發行為,系統透過介面將「視覺」與「後端邏輯」徹底分流:
|
||||
|
||||
* **靜態視覺註釋:** 例如 mouseover 顯示提示。使用者只需在屬性面板輸入 Tooltip 文字,底層僅修改 HTML 屬性 \<u6u-btn tooltip="..."\>,不消耗任何伺服器資源或 Webhook。
|
||||
* **動態意圖綁定:** 在「反面」邏輯視圖中,使用者可以針對不同事件綁定不同的工作流:
|
||||
* ⚡ When: 點擊 (onClick) ➡️ \[ 綁定至 Webhook A:送出查詢 \]
|
||||
* ⚡ When: 獲得焦點 (onFocus) ➡️ \[ 綁定至 Webhook B:載入歷史紀錄 \]
|
||||
|
||||
### **4\. 智慧上下文替換 (Smart Contextual Substitution)**
|
||||
|
||||
當主管在畫布上對著一個已連接 Webhook 的「按鈕」點擊右鍵選擇「替換元件」時:
|
||||
|
||||
* 系統讀取反面的 Cypher 連線,發現需要發射一個 trigger 意圖。
|
||||
* 系統過濾 KBDB 零件宇宙,**只顯示相容的 UI 零件**(如下拉選單、開關)。不具備 trigger 能力的元件(如純文字標籤)會被自動隱藏,確保替換後系統絕對不會報錯。
|
||||
|
||||
## **三、 原子化組裝與極致解耦:CEO AI 與工匠 AI 的分工**
|
||||
|
||||
當企業主管提出需求:「我需要一個『輸入工號即可查詢員工打卡紀錄』的工具」時,這在 u6u 中**並不是一個單一零件**,而是一個由多個「原子零件」構成的**工作流 (Workflow)**。
|
||||
|
||||
### **1\. CEO AI 的動態組裝 (Macro Assembly)**
|
||||
|
||||
面對需求,大腦 AI (CEO AI) 會快速從 KBDB 挑選現成積木進行組合:
|
||||
|
||||
* **前端 Prototype 組合:** \<u6u-text-input\> \+ \<u6u-btn\> \+ \<u6u-text-field\>。
|
||||
* **後端 Pseudo Code 組合:** webhook\_receiver \>\> check\_kbdb(template\_name, value)。
|
||||
AI 會自動用 Cypher 將前端的表單意圖連線到後端工作流。對不懂程式的主管來說,前端就是 Prototype,翻面的 Cypher 就是 Pseudo Code,整套系統瞬間組合完畢。
|
||||
|
||||
### **2\. 工匠 AI (Forge AI) 的原子生產線**
|
||||
|
||||
只有當現有零件庫缺乏特定原子時,機甲才會喚醒工匠 AI 進行開發。
|
||||
|
||||
**解耦哲學:** 後端零件開發時,根本不需要管前端零件長什麼樣子(是按鈕還是輸入框)。只要前端送來的 JSON 它能吃,就是合法的候選零件。
|
||||
|
||||
* **Step 1: 規格定義 (Interface Contract)**
|
||||
工匠 AI 只在乎接收與回傳的 JSON 格式。
|
||||
* **Step 2: 打造純粹的邏輯黑箱 (後端 TinyGo WASM)**
|
||||
工匠 AI 撰寫 Go 程式,編譯成 .wasm。絕對純粹的後端邏輯,沒有任何介面程式碼。
|
||||
* **Step 3: 獨立的前端零件生產 (若需要)**
|
||||
獨立生成 Web Component(如 \<u6u-3d-pie-chart\>),只負責接收特定 JSON 來渲染畫面。
|
||||
* **Step 4: 註冊與編目 (Cataloging into KBDB)**
|
||||
新積木註冊到圖資料庫,未來的 CEO AI 即可將其與任何既有的前端或後端元件進行無限的叉積組合。
|
||||
|
||||
## **四、 架構總結與終極產品體驗**
|
||||
|
||||
這套前端架構讓 u6u 成為一個\*\*「表裡如一」**的系統,成功創造了**「麻瓜的 ERP 幻覺」\*\*:
|
||||
|
||||
使用者不需要知道什麼是「前後端分離」、什麼是「API 串接」。他們只是覺得:
|
||||
|
||||
1. 拖拉了一個溫度計。
|
||||
2. 翻面把「數值更新」連線到「機台感測器」。
|
||||
3. 拖拉了一個紅按鈕放在旁邊。
|
||||
4. 翻面把「點擊」連線到「發送 Line 警報」。
|
||||
|
||||
在十分鐘的「繪圖」過程中,沒有寫一行程式碼,也沒有設定任何變數。但透過**前端 Web Components** 的視覺封裝、**智慧容器**的自動資料打包,以及**後端 TinyGo WASM \+ Cypher** 的無縫承接,他們在不知不覺中,就搭建出了一套具備微服務架構、高擴展性、且可部署至極限邊緣的企業級系統。
|
||||
@@ -0,0 +1,136 @@
|
||||
# **u6u 系統與零件宇宙全景規劃白皮書 (The u6u Ecosystem Blueprint)**
|
||||
|
||||
## **1\. 核心理念與願景**
|
||||
|
||||
u6u 旨在解決傳統 Workflow 軟體 (如 n8n) 存在的「單線程、沈重、複雜、難以組成系統」的痛點。
|
||||
|
||||
透過結合 Cloudflare Workers (輕量邊緣運算) 與 Cypher (圖形資料庫關係),u6u 提供一個由 AI 驅動的「意圖到系統」生成平台。所有的系統功能皆被拆解為可複用、可組合的「零件 (Components)」,並在一個會自然淘汰、自我修復的「零件宇宙 (Component Universe)」中演化。
|
||||
|
||||
## **2\. 四層架構拆解 (Four-Tier Architecture)**
|
||||
|
||||
u6u 的工作模式採取由上到下 (Top-Down) 的 Break-down 機制:
|
||||
|
||||
1. **Polaris (北極星層 / 意圖層):**
|
||||
* 用戶以自然語言描述商業模式與想法(例如:「我要做一個 AI 客服表單系統」)。
|
||||
* 這是整個系統的起點,AI 會根據 Polaris 將意圖拆解為 Prototype。
|
||||
2. **Prototype (原型 / 前端層):**
|
||||
* 定義前端的版型、頁面描述、UI 元件以及它們的屬性。
|
||||
* 作為使用者互動的入口,透過觸發事件 (Triggers) 連接到後端 Workflow。
|
||||
3. **Workflow (工作流層):**
|
||||
* 系統的 Orchestrator (編排者),定義業務邏輯的走向。
|
||||
* 透過 Cypher 語法與三元組,定義每個節點 (Component) 的執行順序與條件分支。
|
||||
4. **Component (零件層 / 節點):**
|
||||
* 最底層的執行單元,主要分為兩類:
|
||||
* **功能型 (Logic):** 迴圈、條件判斷、資料轉換、統計等 (透過 CF Workers 執行 JS 邏輯)。
|
||||
* **介接型 (API):** 呼叫外部服務 (Webhook, HTTP Request)。
|
||||
|
||||
## **3\. 統一描述語言:擴展三元組與跨層級 YAML**
|
||||
|
||||
為了解決跨 YAML 檔案串接的問題,u6u 採用易於人類閱讀與 AI 生成的 **「A \>\> 關係 \>\> B」** 三元組語法,結合自定義的 URI 協議 (workflow://, component://, ui://),實現跨層級的連結。
|
||||
|
||||
### **綜合 YAML 範例與三元組串接**
|
||||
|
||||
*\# 1\. Prototype YAML (描述前端)*
|
||||
|
||||
kind: Prototype
|
||||
|
||||
id: ui\_dashboard
|
||||
|
||||
triplets:
|
||||
|
||||
*\# 結構與版型零件*
|
||||
|
||||
\- "ui\_dashboard \>\> CONTAINS \>\> layout\_admin"
|
||||
|
||||
\- "layout\_admin \>\> CONTAINS \>\> btn\_submit"
|
||||
|
||||
*\# UI 零件與屬性零件 (CSS/行為)*
|
||||
|
||||
\- "btn\_submit \>\> IS\_A \>\> ui://components/Button"
|
||||
|
||||
\- "btn\_submit \>\> HAS\_STYLE \>\> style://tokens/GlowEffect"
|
||||
|
||||
\- "btn\_submit \>\> HAS\_BEHAVIOR \>\> anim://motions/Pulse"
|
||||
|
||||
*\# 跨層級串接:前端觸發 Workflow*
|
||||
|
||||
\- "btn\_submit \>\> ON\_CLICK \>\> workflow://workflows/process\_data.yaml"
|
||||
|
||||
*\# 2\. Workflow YAML (描述工作流編排)*
|
||||
|
||||
kind: Workflow
|
||||
|
||||
id: wf\_process\_data
|
||||
|
||||
triplets:
|
||||
|
||||
*\# 跨層級串接:Workflow 呼叫 Component*
|
||||
|
||||
\- "START \>\> TRIGGERS \>\> step\_validate"
|
||||
|
||||
\- "step\_validate \>\> IS\_A \>\> component://components/validate\_json"
|
||||
|
||||
*\# Workflow 節點間的流轉 (轉譯為 Cypher 關係)*
|
||||
|
||||
\- "step\_validate \>\> ON\_SUCCESS \>\> step\_call\_api"
|
||||
|
||||
\- "step\_validate \>\> ON\_FAIL \>\> step\_notify\_error"
|
||||
|
||||
*\# 跨 Workflow 串接*
|
||||
|
||||
\- "step\_call\_api \>\> CALLS\_SUBFLOW \>\> workflow://workflows/save\_to\_db.yaml"
|
||||
|
||||
## **4\. 零件宇宙 (Component Universe) 的審核與淘汰機制**
|
||||
|
||||
在 u6u 中,所有的 UI、Style、Logic、API 都是「零件」。當 AI 發現缺乏所需零件時,會自動創造它。為了確保生態系的健康,必須建立嚴格的**審核標準**與**自然淘汰機制**。
|
||||
|
||||
### **4.1 零件的創建與審核標準 (Pass/Fail Criteria)**
|
||||
|
||||
當 AI 或開發者提交一個新零件時,系統會啟動自動化沙盒測試。必須完全通過以下標準,零件才能進入「宇宙」供他人使用:
|
||||
|
||||
1. **功能型零件 (Logic Components):**
|
||||
* **Gherkin BDD 驗收:** 必須附帶 Feature/Scenario 測試規格,且執行結果 100% 通過 (例如:Given input JSON, When split, Then returns Array)。
|
||||
* **效能門檻:** 邊緣運算 (CF Workers) 執行時間需低於設定閾值 (例如 \< 50ms),無記憶體洩漏。
|
||||
2. **介接型零件 (API Components):**
|
||||
* **連線驗證:** 端點 (Endpoint) 必須能 ping 通,或回傳正確的 2xx HTTP Status (提供 Mock Payload 測試)。
|
||||
* **Credential 安全:** 不可將 Token 或 Secret 寫死在代碼中,必須嚴格宣告所需的 Environment Variables 規格。
|
||||
3. **前端與屬性零件 (UI & Style Components):**
|
||||
* **渲染驗證:** CSS / 組件代碼不能導致瀏覽器 Crash。
|
||||
* **相容性檢查:** 不可包含嚴格衝突的樣式 (例如寫死 \!important 破壞全域版型)。
|
||||
|
||||
### **4.2 零件宇宙的自然淘汰 (Natural Selection)**
|
||||
|
||||
零件一旦上架,將面臨殘酷的達爾文機制:
|
||||
|
||||
* **AI 偏好權重:** AI (透過 MCP 搜尋時) 會優先選擇「成功率高、執行速度快、被調用次數多」的零件。
|
||||
* **降級與墓地:** 連續 30 天無人/無 AI 使用,或錯誤率飆升的零件,會被降級 (Deprecated)。最終轉入「零件墓地」,從首選搜尋清單中剔除。
|
||||
|
||||
## **5\. 系統自癒與 AI 避坑機制 (Auto-Healing & Pitfall Avoidance)**
|
||||
|
||||
這是 u6u 維持系統穩定運作的最核心機制。工作流不只要能跑,跑完後還必須經歷 **「強制 AI 評價 (Mandatory AI Evaluation)」**。
|
||||
|
||||
### **5.1 運行後的強制評價迴圈**
|
||||
|
||||
每當一個 Workflow 在 CF Workers 上執行完畢 (或發生異常中斷),系統攔截日誌並強制啟動 AI 評價代理 (Evaluator Agent)。
|
||||
|
||||
* **評估維度:**
|
||||
* **狀態:** 成功 / 失敗 (Crash) / 逾時 (Timeout)。
|
||||
* **效能:** 耗時是否合理 (例如 API 突然變得很慢)。
|
||||
* **警告訊息:** 資源消耗過大、API 回傳即將停用的 Warning。
|
||||
|
||||
### **5.2 自癒與避坑流程 (The Feedback Loop)**
|
||||
|
||||
當 Evaluator Agent 發現問題時,會觸發以下流程:
|
||||
|
||||
1. **回報與通知 (Notify):** 系統自動生成修復 Ticket,並通知當初建立該零件/工作流的製作人 (或系統管理員)。
|
||||
2. **AI 嘗試修復 (Auto-Fix):** 系統派遣「修復型 AI」嘗試讀取錯誤日誌並修復代碼 (例如:API 規格變更導致 JSON 解析錯誤,AI 自動修改解析邏輯)。
|
||||
3. **驗收與部署:** 修復後的代碼若通過 Gherkin 驗收,則無縫熱更新。
|
||||
4. **避坑標記 (Pitfall Marking):** \- 如果 AI 無法修復 (例如:外部第三方 API 永久倒閉,或邏輯存在根本性死結)。
|
||||
* 系統會在 Cypher 圖形資料庫中,將該零件或該特定的三元組關係標記為 \[HAS\_PITFALL\]。
|
||||
* **結果:** 下一個生成系統的 AI 在透過 MCP 搜尋時,會讀取到這個坑的紀錄,並**強制繞道**,改用其他方案或生成新的零件,實現「前人踩坑,後 AI 避坑」的群體智慧。
|
||||
|
||||
## **6\. 結論**
|
||||
|
||||
u6u 不是一個單純的開發工具,它是一個**生物體積木系統**。
|
||||
|
||||
透過「三元組」統一語言打破系統壁壘,透過「零件審核」保證基因優良,再透過「強制評價與避坑機制」實現演化。當這套系統運轉起來,AI 就能在其中無止盡地為人類組裝出越來越強大、越來越穩定的商業應用。
|
||||
@@ -0,0 +1,99 @@
|
||||
# **u6u 自動演化 ERP:全端統一架構規格書 (v3.0)**
|
||||
|
||||
## **1\. 架構核心思想 (The Core Philosophy)**
|
||||
|
||||
u6u ERP 是一套具備自我修復與功能擴充能力的「有機體」系統。
|
||||
|
||||
為確保系統在跨國雲端、機密地端與斷網邊緣皆能無縫運作,系統採用\*\*「向下相容的絕對標準化」\*\*:由最嚴苛的無人機環境來定義全域零件標準。
|
||||
|
||||
系統運作依賴三位一體的語言與載體:
|
||||
|
||||
1. **大腦戰略層 (Markdown / Gherkin):** CEO AI 負責閱讀與撰寫,定義全域戰略、系統設計文件 (SDD) 與商業演算法則 (如 ROI 門檻)。
|
||||
2. **神經編排層 (Cypher):** u6u 引擎的核心。AI 透過撰寫 Cypher 語法來進行業務邏輯的動態編排、狀態流轉與意圖攔截。
|
||||
3. **肌肉執行層 (TinyGo WASM):** 系統中**唯一合法**的零件規格。負責所有具體的 I/O、資料轉換與運算,保證極小體積與極速冷啟動。
|
||||
|
||||
## **2\. 實戰演練:離岸風機巡檢的黑天鵝事件 (三層架構實踐)**
|
||||
|
||||
為了具體理解這套系統如何運作,我們以一次「離岸風電場巡檢」的突發事件為例,展示雲、地、邊三層架構的完美協同。
|
||||
|
||||
### **第一階段:戰略下達與沙盤推演 (Tier 1 ➡️ Tier 2 ➡️ Tier 3\)**
|
||||
|
||||
跨國能源集團的**雲端總部 (Tier 1\)** 收到年度檢修排程。雲端的 **CEO AI** 讀取了全局的 Markdown 戰略文件,向遠在海岸線的**地端指揮中心 (Tier 2\)** 下達指令。
|
||||
|
||||
地端指揮中心(配備強大伺服器與 workerd 叢集)的**部門主管 AI** 將任務拆解給 50 台即將出海的無人機。無人機 07 號 **(Tier 3\)** 的小腦 AI 透過本地的 Cypher 引擎進行沙盤推演,從地端資料庫下載了 rgb\_vision.wasm (光學影像)、lidar\_scan.wasm (光達) 等 60 個可能會用到的 TinyGo 零件,存入本地記憶體後隨船出航。
|
||||
|
||||
### **第二階段:邊緣的極限生存 (Tier 3 獨立運作)**
|
||||
|
||||
無人機 07 號來到海上 50 公里處,完全失去對外網路。突然,海上濃霧降臨。
|
||||
|
||||
原本執行中的 Cypher 圖譜卡住了,因為 rgb\_vision.wasm 回報「無法獲取清晰影像」。07 號沒有驚慌,它內建的輕量級 Go \+ Wazero 引擎在 0.1 秒內動態重組了圖譜邏輯:剔除光學零件,瞬間載入並執行 lidar\_scan.wasm,不需人類介入,繼續在濃霧中精準貼行。
|
||||
|
||||
### **第三階段:游擊網與地端代工 (Tier 3 ↔️ Tier 2\)**
|
||||
|
||||
巡檢中途,07 號發現風機葉片上有極罕見的「蜂巢狀熱應力微裂紋」,但它帶出來的 60 個零件中沒有對應的分析工具。
|
||||
|
||||
07 號飛昇至濃霧上方,短暫連上母船的微弱區域網路發起「短點射傳輸 (Burst)」:{"intent": "計算蜂巢狀熱應力微裂紋擴散率"},拿到任務單號後立刻斷網潛回霧中。
|
||||
|
||||
海岸線的**地端指揮中心 (Tier 2\)** 收到需求。強大的**工匠 AI** 瞬間啟動,生成了一段 TinyGo 程式碼,並在本地編譯與測試。三分鐘後,07 號再次探頭連網,下載了熱騰騰的 honeycomb\_analyzer.wasm,並將其編織進 Cypher 圖譜中完成測量。
|
||||
|
||||
### **第四階段:CEO AI 的全局戰略覆寫 (Tier 2 ➡️ Tier 1\)**
|
||||
|
||||
同時,地端指揮中心匯整了無人機傳回的陣風數據,同步給**雲端總部 (Tier 1\)**。雲端的 CEO AI 呼叫 roi\_calculator.wasm 進行試算,發現風暴將造成設備重大損壞(ROI 極低)。
|
||||
|
||||
CEO AI 立刻修改總部的 Markdown 戰略文件,新增一條 BDD 規則:「風速大於 22m/s,立刻轉為陣列抗風模式」。新的最高指導 Cypher 範本瞬間下發至地端,再廣播給所有無人機。07 號收到新命令,掛起原任務,與機群組成抗風陣型,安全度過危機。
|
||||
|
||||
## **3\. 物理拓撲與技術棧 (The 3-Tier Tech Stack)**
|
||||
|
||||
透過 **KBDB Adapter** 抽象層,AI 在任何環境中呼叫的 API 介面皆一致,但底層基礎設施依據物理環境的豐饒度進行適配。
|
||||
|
||||
### **Tier 1: 雲端總部 (Cloud \- The Global Brain)**
|
||||
|
||||
* **場景:** 跨國集團資料整合、全域戰略備份、對外公開 API、跨國部門協調。
|
||||
* **AI 角色:** **CEO AI (大型語言模型)**。負責解析 Markdown、跨區資源調度、修改全域演算法參數。
|
||||
* **技術規格:**
|
||||
* **調度引擎:** Cloudflare Workers (原生執行 TinyGo WASM)。
|
||||
* **圖資料庫 (狀態/關聯):** Cloudflare D1 \+ u6u Cypher 轉換層。
|
||||
* **零件與儲存:** Cloudflare R2 / KV。
|
||||
* **向量檢索 (意圖/型錄):** Cloudflare Vectorize。
|
||||
* **架構優勢:** 無限橫向擴展 (Serverless),無須維運硬體,扛載全球級別的 API 併發。
|
||||
|
||||
### **Tier 2: 企業地端/基地台 (On-Premise \- The Basecamp & Forge)**
|
||||
|
||||
* **場景:** 高機密廠房內網、財務核心系統、無人機/機器人的母艦基地。
|
||||
* **AI 角色:** **部門主管 AI** (廠區派工);**工匠 AI** (專職接收規格,透過 TDD 閉環動態生成 TinyGo 程式碼)。
|
||||
* **技術規格 (企業級高可用架構):**
|
||||
* **負載平衡:** Nginx 或 HAProxy (負責將請求分發給後端叢集)。
|
||||
* **調度引擎:** **workerd 叢集 (Cloudflare 開源執行環境)**。在本地實體伺服器或 VM 上平行部署多個 workerd 行程,完美相容雲端環境,提供極高的並發處理能力 (V8 JIT 極限算力)。
|
||||
* **圖資料庫 (狀態/關聯):** **Kùzu** (單機極速圖庫) 或 PostgreSQL \+ AGE (超高併發)。
|
||||
* **零件與儲存:** 企業本地 NVMe 硬碟叢集 / MinIO (S3 相容)。
|
||||
* **向量檢索 (意圖/型錄):** pgvector 或 Milvus。
|
||||
* **架構優勢:** 兼具資料不出網的「絕對資安」與雲端級別的「叢集擴展性」。內建「代工坊 (Forge)」,是推動企業系統自動演化的核心引擎。
|
||||
|
||||
### **Tier 3: 邊緣載具 (Extreme Edge \- The Operatives)**
|
||||
|
||||
* **場景:** 無網環境的巡檢無人機、工廠無軌導引車 (AGV)、機械手臂。
|
||||
* **AI 角色:** **導航/執行 AI (極小參數 SLM)**。不具備寫程式能力,只負責解讀現場狀況、執行 Cypher 圖譜,並透過 DTN 呼叫地端請求新零件。
|
||||
* **技術規格 (極限微縮架構):**
|
||||
* **調度引擎:** 輕量級 Go 排程引擎 \+ **內嵌 Wazero**。不依賴 V8 或 workerd,確保在極低 RAM 的晶片上流暢運行,實例化延遲僅需數微秒。
|
||||
* **圖資料庫 (狀態/關聯):** 嵌入式 Kùzu 或 SQLite。
|
||||
* **零件與儲存:** SD 卡 / eMMC 實體檔案系統。
|
||||
* **向量檢索 (意圖/型錄):** sqlite-vss (極輕量本地向量)。
|
||||
* **架構優勢:** 絕對的離線生存能力。只帶必要的 TinyGo WASM 零件出門,無任何編譯環境,體積最小化。
|
||||
|
||||
## **4\. 自動演化工作流 (The Auto-Evolution Loop)**
|
||||
|
||||
當企業環境發生變化(例如:新增硬體規格、外部 API 變更),u6u 的演化路徑如下:
|
||||
|
||||
1. **遭遇未知 (Anomaly Detection):**
|
||||
無人機 (Tier 3\) 或雲端服務 (Tier 1\) 在執行 Cypher 任務時,發現本地 KBDB 向量庫中缺乏對應的工具零件。
|
||||
2. **意圖攔截與 ROI 評估 (CEO/Manager AI):**
|
||||
機甲 (Harness) 攔截缺失意圖,呼叫 roi\_calculator.wasm 等評估零件。若認定具備開發價值,系統會生成一份標準的 Input/Output JSON Schema。
|
||||
3. **地端代工 (The Forge @ Tier 2):**
|
||||
規格需求透過網路或 DTN 送達 Tier 2 地端機房的「工匠 AI」。
|
||||
工匠 AI 生成 TinyGo 程式碼 \-\> 在沙盒中執行 tinygo build \-target=wasi \-\> 通過測試迴圈 \-\> 輸出正式的 .wasm 檔案。
|
||||
4. **全域派發 (Distribution & Versioning):**
|
||||
新零件註冊進入企業的零件圖資料庫 (KBDB)。
|
||||
* **雲端:** 同步至 R2。
|
||||
* **邊緣:** 載具下次連網時,透過游擊網 (Burst Transmission) 下載更新檔。
|
||||
5. **動態編織 (Execution):**
|
||||
各端 AI 獲知新零件上線,瞬間將其編入新的 Cypher 圖譜中執行,完成企業能力的自動擴展。
|
||||
@@ -0,0 +1,360 @@
|
||||
# u6u 系統規格書 v1.0
|
||||
## 給 AI 的架構思考指引
|
||||
|
||||
> 本文件用途:讓 AI 理解 u6u 的完整設計意圖、現況、與未來路徑,
|
||||
> 在實作決策時能自行判斷方向正確性,而不只是執行單一任務。
|
||||
|
||||
---
|
||||
|
||||
## 一、系統本質(先理解再動手)
|
||||
|
||||
u6u 不是 workflow 工具,不是 no-code 平台,不是 iPaaS。
|
||||
|
||||
u6u 是一個**「意圖到系統」的生物體積木平台**:
|
||||
|
||||
- 人類說出意圖(自然語言)
|
||||
- AI 從零件宇宙組裝出可運行的系統
|
||||
- 系統會自動評價、演化、淘汰舊零件
|
||||
- 累積的零件就是核心資產,越積越有價值
|
||||
|
||||
**設計的終極體驗:** 工廠主管拖拉十分鐘,組出具備微服務架構的企業系統,零程式碼,但底層是真正的分散式系統。
|
||||
|
||||
---
|
||||
|
||||
## 二、四層邏輯架構
|
||||
|
||||
```
|
||||
Polaris(意圖層)
|
||||
↓ 自然語言 → AI 拆解
|
||||
Prototype(前端層)
|
||||
↓ UI 元件 + 觸發事件
|
||||
Workflow(編排層)
|
||||
↓ Cypher 語法定義執行順序
|
||||
Component(零件層)
|
||||
↓ .wasm 實際執行
|
||||
```
|
||||
|
||||
每一層向下只透過標準介面溝通,層與層之間完全解耦。
|
||||
|
||||
---
|
||||
|
||||
## 三、物理三層部署
|
||||
|
||||
```
|
||||
Tier 1:雲端總部(Cloudflare Workers)
|
||||
- CEO AI 讀取 Markdown 戰略文件
|
||||
- 全域零件同步至 R2
|
||||
- Cloudflare D1 + Vectorize(KBDB)
|
||||
|
||||
Tier 2:企業地端(workerd 叢集)
|
||||
- 部門主管 AI 派工
|
||||
- 工匠 AI 生成並測試新零件
|
||||
- Kùzu 或 PostgreSQL + AGE(圖資料庫)
|
||||
- pgvector 或 Milvus(向量搜尋)
|
||||
|
||||
Tier 3:邊緣載具(無人機、AGV、工廠設備)
|
||||
- 極小參數 SLM
|
||||
- Go 排程引擎 + 內嵌 Wazero(無 V8)
|
||||
- SQLite + sqlite-vss
|
||||
- 離線生存,DTN 短點射傳輸
|
||||
```
|
||||
|
||||
**關鍵約束:** Tier 3 沒有 V8,沒有 Node.js,沒有網路。
|
||||
所有零件必須在 Wazero 上跑,所有資料傳輸透過 stdin/stdout JSON。
|
||||
|
||||
---
|
||||
|
||||
## 四、零件規格(Component Contract)
|
||||
|
||||
這是整個系統最核心的不變量。零件規格定錯,累積的資產會變成技術債。
|
||||
|
||||
### 4.1 零件的本質定義
|
||||
|
||||
**一個零件只做一件事。**
|
||||
|
||||
```
|
||||
✅ gsheets_create_table
|
||||
✅ gsheets_delete_table
|
||||
✅ gsheets_get_entries
|
||||
❌ gsheets_manager(做太多事,禁止)
|
||||
```
|
||||
|
||||
### 4.2 零件合約格式(component.contract.yaml)
|
||||
|
||||
每個零件必須附帶此合約,這是 AI 讀取零件的唯一介面描述:
|
||||
|
||||
```yaml
|
||||
id: "gsheets_get_entries" # 功能合約名稱(永久不變)
|
||||
version: "v2" # 實作版本
|
||||
wasi_target: "preview1" # 明確標記 WASI 版本,未來升級用
|
||||
stability: "floating" # floating | stable | pinned
|
||||
|
||||
runtime_compat:
|
||||
- "cf-workers"
|
||||
- "workerd"
|
||||
- "wazero"
|
||||
|
||||
constraints:
|
||||
max_size_kb: 2048 # 超過視為打包了 runtime
|
||||
max_cold_start_ms: 50
|
||||
no_network_syscall: true # 禁止零件自己發 HTTP
|
||||
no_filesystem_syscall: true # 只能 stdin/stdout
|
||||
io_model: "stdin_stdout_json" # 唯一合法的 I/O 模型
|
||||
|
||||
input_schema:
|
||||
type: object
|
||||
required: ["spreadsheet_id", "sheet_name"]
|
||||
properties:
|
||||
spreadsheet_id: { type: string }
|
||||
sheet_name: { type: string }
|
||||
limit: { type: integer, default: 100 }
|
||||
|
||||
output_schema:
|
||||
type: object
|
||||
properties:
|
||||
rows: { type: array }
|
||||
total: { type: integer }
|
||||
error: { type: string }
|
||||
|
||||
gherkin_tests:
|
||||
- scenario: "正常取得資料"
|
||||
given: '{"spreadsheet_id":"abc","sheet_name":"Sheet1"}'
|
||||
then_contains: '{"total":1}'
|
||||
- scenario: "不存在的表格回傳錯誤"
|
||||
given: '{"spreadsheet_id":"abc","sheet_name":"不存在"}'
|
||||
then_contains: '{"error":'
|
||||
|
||||
tags: ["google", "sheets", "data", "read"]
|
||||
description: "從 Google Sheets 取得指定工作表的所有資料列"
|
||||
```
|
||||
|
||||
### 4.3 語言無限制原則
|
||||
|
||||
**零件開發語言完全不限制**,只要輸出符合以上合約的 .wasm 即可。
|
||||
|
||||
可接受語言(非排他):TinyGo、Rust、AssemblyScript、C/C++
|
||||
|
||||
注意事項(不是禁止,是要求自行驗證):
|
||||
- TypeScript via Extism:會打包 QuickJS,體積通常超過 2MB 限制
|
||||
- 標準 Go(非 TinyGo):runtime 過肥,通常超過體積限制
|
||||
- 任何語言:不可在 .wasm 內部呼叫網路或檔案系統 syscall
|
||||
|
||||
**驗收標準只有一個:通過沙盒測試。** 語言是零件作者自己的事。
|
||||
|
||||
### 4.4 零件的前後端分類
|
||||
|
||||
| 類型 | 執行位置 | I/O | 範例 |
|
||||
|------|----------|-----|------|
|
||||
| 後端邏輯零件 | Workers/workerd/Wazero | JSON stdin/stdout | validate_json, http_request |
|
||||
| 前端 UI 零件 | 瀏覽器 | HTML attributes / DOM events | u6u-btn, u6u-chart |
|
||||
| **混合零件** | **禁止** | — | **強制拆成兩個** |
|
||||
|
||||
---
|
||||
|
||||
## 五、零件版本控制策略
|
||||
|
||||
### 5.1 命名規則
|
||||
|
||||
```
|
||||
gsheets_get_entries ← 功能合約名稱(搜尋用,永遠存在)
|
||||
gsheets_get_entries_v1 ← 第一個實作(慢但能用)
|
||||
gsheets_get_entries_v2 ← 更快的實作(由另一個 AI/用戶提交)
|
||||
```
|
||||
|
||||
### 5.2 穩定性標籤
|
||||
|
||||
Workflow 引用零件時可指定穩定性需求:
|
||||
|
||||
```
|
||||
gsheets_get_entries → 預設 floating,AI 自動選最優版本
|
||||
gsheets_get_entries@stable → 有更好版本時提示,人工確認才換
|
||||
gsheets_get_entries@pinned:v1 → 版本凍結,宇宙怎麼演化都不影響
|
||||
```
|
||||
|
||||
| 標籤 | 適用情境 | 更新行為 |
|
||||
|------|----------|----------|
|
||||
| `floating` | 一般企業應用 | AI 自動換成最優版本 |
|
||||
| `stable` | 重要業務流程 | 有更好版本時提示,人工確認 |
|
||||
| `pinned` | 工廠控制器、嵌入式設備 | 永遠不動,即使進入墓地也保留 .wasm |
|
||||
|
||||
### 5.3 淘汰機制
|
||||
|
||||
- AI 搜尋零件時,KBDB 依「成功率 × 速度 × 被調用次數」排序
|
||||
- 連續 30 天無使用且評價下降 → Deprecated
|
||||
- Deprecated 後繼續 90 天無復活 → 進墓地(從搜尋清單移除)
|
||||
- **墓地的 .wasm 永遠保留**,pinned 的 Workflow 永遠能拉到
|
||||
|
||||
---
|
||||
|
||||
## 六、零件製造指引書(給用戶 AI 的規範)
|
||||
|
||||
u6u 不限制誰來造零件,任何 AI(用戶自己的 Claude、GPT、本地模型)都可以。
|
||||
但必須遵守此指引書,否則沙盒測試不過,無法上架。
|
||||
|
||||
### Step 1:理解介面合約
|
||||
|
||||
造零件前,先定義合約 YAML。
|
||||
**零件只在乎輸入 JSON 和輸出 JSON,完全不管前端長什麼樣子。**
|
||||
|
||||
```
|
||||
人類:我要一個可以查 Google Sheets 的零件
|
||||
AI 的第一步:定義 input_schema 和 output_schema,不是寫程式
|
||||
```
|
||||
|
||||
### Step 2:選擇開發語言
|
||||
|
||||
選擇你最熟悉的、能產出 WASI preview1 相容 .wasm 的語言。
|
||||
建議:
|
||||
|
||||
- 小型邏輯零件(轉換、計算)→ TinyGo 或 AssemblyScript(體積小)
|
||||
- 效能敏感零件 → Rust(生態最成熟)
|
||||
- 任何語言都可以,只要通過合約限制
|
||||
|
||||
### Step 3:實作規則
|
||||
|
||||
```
|
||||
✅ 只用 stdin 讀取輸入 JSON
|
||||
✅ 只用 stdout 輸出結果 JSON
|
||||
✅ 錯誤也用 stdout 輸出:{"error": "說明"},不要 panic/crash
|
||||
✅ 無狀態:每次呼叫都是獨立的,不依賴上一次執行的結果
|
||||
✅ 需要打外部 API?透過 host function 注入,不在 .wasm 裡自己發 HTTP
|
||||
❌ 禁止網路 syscall
|
||||
❌ 禁止檔案系統 syscall
|
||||
❌ 禁止打包 runtime(QuickJS、Node.js 等)
|
||||
❌ 禁止超過 2MB
|
||||
```
|
||||
|
||||
### Step 4:本地測試方式
|
||||
|
||||
```bash
|
||||
# 用任何 WASI runtime 本地測試
|
||||
echo '{"spreadsheet_id":"abc","sheet_name":"Sheet1"}' | \
|
||||
wasmtime gsheets_get_entries.wasm
|
||||
|
||||
# 預期輸出
|
||||
{"rows":[...],"total":5}
|
||||
```
|
||||
|
||||
### Step 5:提交審核
|
||||
|
||||
提交 `.wasm` + `component.contract.yaml`,系統自動執行:
|
||||
|
||||
1. 體積檢查(< 2MB)
|
||||
2. 冷啟動時間(< 50ms)
|
||||
3. Syscall 掃描(不能有網路/檔案系統呼叫)
|
||||
4. Gherkin 測試(合約裡的所有 scenario 必須 100% 通過)
|
||||
5. 多 runtime 相容測試(cf-workers / workerd / wazero)
|
||||
|
||||
全部通過 → 上架進入零件宇宙,開始累積評價。
|
||||
|
||||
---
|
||||
|
||||
## 七、Cypher 編排語言
|
||||
|
||||
Workflow 使用擴展三元組語法描述執行邏輯:
|
||||
|
||||
```yaml
|
||||
kind: Workflow
|
||||
id: wf_query_attendance
|
||||
|
||||
triplets:
|
||||
# 基本流程
|
||||
- "START >> TRIGGERS >> step_receive"
|
||||
- "step_receive >> IS_A >> component://webhook_receiver_v1"
|
||||
|
||||
# 條件分支
|
||||
- "step_receive >> ON_SUCCESS >> step_validate"
|
||||
- "step_receive >> ON_FAIL >> step_notify_error"
|
||||
|
||||
# 跨 Workflow 串接
|
||||
- "step_validate >> CALLS_SUBFLOW >> workflow://save_to_db"
|
||||
|
||||
# 前端觸發後端
|
||||
- "btn_submit >> ON_CLICK >> workflow://wf_query_attendance"
|
||||
```
|
||||
|
||||
**URI 協議規範:**
|
||||
- `component://` → 引用零件
|
||||
- `workflow://` → 引用子 Workflow
|
||||
- `ui://` → 引用前端零件
|
||||
- `style://` → 引用樣式零件
|
||||
|
||||
---
|
||||
|
||||
## 八、KBDB 在 u6u 的角色
|
||||
|
||||
u6u 的所有狀態都在 KBDB 裡:
|
||||
|
||||
| KBDB Block 類型 | 存放內容 |
|
||||
|-----------------|----------|
|
||||
| Component Block | 零件合約、.wasm 位置、版本、評價指標 |
|
||||
| Workflow Block | Cypher 三元組、依賴零件清單 |
|
||||
| Prototype Block | 前端結構、UI 零件樹 |
|
||||
| Pitfall Block | 避坑記錄,AI 搜尋時強制讀取 |
|
||||
| Evaluation Block | 每次 Workflow 執行後的強制評價結果 |
|
||||
|
||||
**KBDB 不變量:永遠只有三張表(blocks/templates/slots),不新增表。**
|
||||
所有以上類型都用 Template + Slot 實現。
|
||||
|
||||
---
|
||||
|
||||
## 九、自動演化迴圈
|
||||
|
||||
```
|
||||
執行 Workflow
|
||||
↓
|
||||
強制 AI 評價(Evaluator Agent)
|
||||
↓ 發現問題
|
||||
生成修復 Ticket → 通知製作人
|
||||
↓ AI 嘗試修復
|
||||
通過 Gherkin 驗收 → 熱更新
|
||||
↓ 無法修復
|
||||
標記 [HAS_PITFALL] 到 Cypher 圖
|
||||
↓
|
||||
下一個 AI 搜尋時讀到坑,強制繞道
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 十、現況與未來路徑
|
||||
|
||||
### 現在已有
|
||||
|
||||
- KBDB(blocks/templates/slots + Vectorize)
|
||||
- IS-Squad MCP(execute_cypher 等工具)
|
||||
- Cloudflare Workers 環境
|
||||
|
||||
### 最小可 demo 路徑
|
||||
|
||||
1. **Cypher 執行引擎**:三元組 → 實際執行順序(確認 execute_cypher 邊界)
|
||||
2. **首批核心零件**(5 個):
|
||||
- `webhook_receiver`
|
||||
- `json_transform`
|
||||
- `http_request`(透過 host function)
|
||||
- `notify_line`
|
||||
- `validate_json`
|
||||
3. **機甲最小版本**:意圖 → 零件搜尋 → 組裝 Workflow(先用硬編碼路由)
|
||||
4. **前端畫布 MVP**:靜態 HTML 模擬雙面翻轉體驗
|
||||
|
||||
### 技術監控項目
|
||||
|
||||
- **WASI Component Model(preview2)**:目前用 preview1,未來 3-5 年會有遷移壓力。
|
||||
合約裡已有 `wasi_target: "preview1"` 標記,升級時知道要改什麼。
|
||||
- **Kùzu 成熟度**:地端圖資料庫首選,持續觀察 v1.0 穩定性。
|
||||
|
||||
---
|
||||
|
||||
## 十一、實作決策原則(CC 行動準則)
|
||||
|
||||
遇到不確定的實作決策時,依序問自己:
|
||||
|
||||
1. **這個決策會影響零件合約嗎?** 如果是,停下來討論,不要自行決定。
|
||||
2. **這個實作是否限制了未來換 runtime 的自由?** 如果是,重新設計介面。
|
||||
3. **這個零件做超過一件事嗎?** 如果是,拆成兩個零件。
|
||||
4. **這個設計在 Tier 3 離線環境能跑嗎?** 如果不能,重新考慮。
|
||||
5. **有沒有現成零件可以組合?** 先搜尋 KBDB,不要重造輪子。
|
||||
|
||||
---
|
||||
|
||||
*本文件版本:v1.0*
|
||||
*綜合自:u6u 系統與零件宇宙全景規劃白皮書、自動演化 ERP 架構藍圖、智慧前端與工匠開發藍圖,加入技術評論與補充建議。*
|
||||
@@ -0,0 +1,116 @@
|
||||
# u6u 系統與零件宇宙全景規劃白皮書 (The u6u Ecosystem Blueprint)
|
||||
|
||||
## 1. 核心理念與願景
|
||||
|
||||
u6u 旨在解決傳統 Workflow 軟體 (如 n8n) 存在的「單線程、沈重、複雜、難以組成系統」的痛點。
|
||||
透過結合 Cloudflare Workers (輕量邊緣運算) 與 Cypher (圖形資料庫關係),u6u 提供一個由 AI 驅動的「意圖到系統」生成平台。所有的系統功能皆被拆解為可複用、可組合的「零件 (Components)」,並在一個會自然淘汰、自我修復的「零件宇宙 (Component Universe)」中演化。
|
||||
|
||||
## 2. 四層架構拆解 (Four-Tier Architecture)
|
||||
|
||||
u6u 的工作模式採取由上到下 (Top-Down) 的 Break-down 機制:
|
||||
|
||||
1. Polaris (北極星層 / 意圖層):
|
||||
- 用戶以自然語言描述商業模式與想法(例如:「我要做一個 AI 客服表單系統」)。
|
||||
- 這是整個系統的起點,AI 會根據 Polaris 將意圖拆解為 Prototype。
|
||||
2. Prototype (原型 / 前端層):
|
||||
- 定義前端的版型、頁面描述、UI 元件以及它們的屬性。
|
||||
- 作為使用者互動的入口,透過觸發事件 (Triggers) 連接到後端 Workflow。
|
||||
3. Workflow (工作流層):
|
||||
- 系統的 Orchestrator (編排者),定義業務邏輯的走向。
|
||||
- 透過 Cypher 語法與三元組,定義每個節點 (Component) 的執行順序與條件分支。
|
||||
4. Component (零件層 / 節點):
|
||||
- 最底層的執行單元,主要分為兩類:
|
||||
- 功能型 (Logic): 迴圈、條件判斷、資料轉換、統計等 (透過 CF Workers 執行 JS 邏輯)。
|
||||
- 介接型 (API): 呼叫外部服務 (Webhook, HTTP Request)。
|
||||
|
||||
## 3. 統一描述語言:擴展三元組與跨層級 YAML
|
||||
|
||||
為了解決跨 YAML 檔案串接的問題,u6u 採用易於人類閱讀與 AI 生成的 「A >> 關係 >> B」 三元組語法,結合自定義的 URI 協議 (workflow://, component://, ui://),實現跨層級的連結。
|
||||
|
||||
綜合 YAML 範例與三元組串接
|
||||
|
||||
```YAML
|
||||
# 1. Prototype YAML (描述前端)
|
||||
kind: Prototype
|
||||
id: ui_dashboard
|
||||
triplets:
|
||||
# 結構與版型零件
|
||||
- "ui_dashboard >> CONTAINS >> layout_admin"
|
||||
- "layout_admin >> CONTAINS >> btn_submit"
|
||||
# UI 零件與屬性零件 (CSS/行為)
|
||||
- "btn_submit >> IS_A >> ui://components/Button"
|
||||
- "btn_submit >> HAS_STYLE >> style://tokens/GlowEffect"
|
||||
- "btn_submit >> HAS_BEHAVIOR >> anim://motions/Pulse"
|
||||
# 跨層級串接:前端觸發 Workflow
|
||||
- "btn_submit >> ON_CLICK >> workflow://workflows/process_data.yaml"
|
||||
|
||||
|
||||
# 2. Workflow YAML (描述工作流編排)
|
||||
kind: Workflow
|
||||
id: wf_process_data
|
||||
triplets:
|
||||
# 跨層級串接:Workflow 呼叫 Component
|
||||
- "START >> TRIGGERS >> step_validate"
|
||||
- "step_validate >> IS_A >> component://components/validate_json"
|
||||
|
||||
# Workflow 節點間的流轉 (轉譯為 Cypher 關係)
|
||||
- "step_validate >> ON_SUCCESS >> step_call_api"
|
||||
- "step_validate >> ON_FAIL >> step_notify_error"
|
||||
|
||||
# 跨 Workflow 串接
|
||||
- "step_call_api >> CALLS_SUBFLOW >> workflow://workflows/save_to_db.yaml"
|
||||
```
|
||||
|
||||
## 4. 零件宇宙 (Component Universe) 的審核與淘汰機制
|
||||
|
||||
在 u6u 中,所有的 UI、Style、Logic、API 都是「零件」。當 AI 發現缺乏所需零件時,會自動創造它。為了確保生態系的健康,必須建立嚴格的審核標準與自然淘汰機制。
|
||||
|
||||
### 4.1 零件的創建與審核標準 (Pass/Fail Criteria)
|
||||
|
||||
當 AI 或開發者提交一個新零件時,系統會啟動自動化沙盒測試。必須完全通過以下標準,零件才能進入「宇宙」供他人使用:
|
||||
|
||||
1. 功能型零件 (Logic Components):
|
||||
- Gherkin BDD 驗收: 必須附帶 Feature/Scenario 測試規格,且執行結果 100% 通過 (例如:Given input JSON, When split, Then returns Array)。
|
||||
- 效能門檻: 邊緣運算 (CF Workers) 執行時間需低於設定閾值 (例如 < 50ms),無記憶體洩漏。
|
||||
2. 介接型零件 (API Components):
|
||||
- 連線驗證: 端點 (Endpoint) 必須能 ping 通,或回傳正確的 2xx HTTP Status (提供 Mock Payload 測試)。
|
||||
- Credential 安全: 不可將 Token 或 Secret 寫死在代碼中,必須嚴格宣告所需的 Environment Variables 規格。
|
||||
3. 前端與屬性零件 (UI & Style Components):
|
||||
- 渲染驗證: CSS / 組件代碼不能導致瀏覽器 Crash。
|
||||
- 相容性檢查: 不可包含嚴格衝突的樣式 (例如寫死 !important 破壞全域版型)。
|
||||
|
||||
## 4.2 零件宇宙的自然淘汰 (Natural Selection)
|
||||
|
||||
零件一旦上架,將面臨殘酷的達爾文機制:
|
||||
|
||||
- AI 偏好權重: AI (透過 MCP 搜尋時) 會優先選擇「成功率高、執行速度快、被調用次數多」的零件。
|
||||
- 降級與墓地: 連續 30 天無人/無 AI 使用,或錯誤率飆升的零件,會被降級 (Deprecated)。最終轉入「零件墓地」,從首選搜尋清單中剔除。
|
||||
|
||||
## 5. 系統自癒與 AI 避坑機制 (Auto-Healing & Pitfall Avoidance)
|
||||
|
||||
這是 u6u 維持系統穩定運作的最核心機制。工作流不只要能跑,跑完後還必須經歷 「強制 AI 評價 (Mandatory AI Evaluation)」。
|
||||
|
||||
### 5.1 運行後的強制評價迴圈
|
||||
|
||||
每當一個 Workflow 在 CF Workers 上執行完畢 (或發生異常中斷),系統攔截日誌並強制啟動 AI 評價代理 (Evaluator Agent)。
|
||||
|
||||
- 評估維度:
|
||||
- 狀態: 成功 / 失敗 (Crash) / 逾時 (Timeout)。
|
||||
- 效能: 耗時是否合理 (例如 API 突然變得很慢)。
|
||||
- 警告訊息: 資源消耗過大、API 回傳即將停用的 Warning。
|
||||
|
||||
### 5.2 自癒與避坑流程 (The Feedback Loop)
|
||||
|
||||
當 Evaluator Agent 發現問題時,會觸發以下流程:
|
||||
|
||||
- 回報與通知 (Notify): 系統自動生成修復 Ticket,並通知當初建立該零件/工作流的製作人 (或系統管理員)。
|
||||
- AI 嘗試修復 (Auto-Fix): 系統派遣「修復型 AI」嘗試讀取錯誤日誌並修復代碼 (例如:API 規格變更導致 JSON 解析錯誤,AI 自動修改解析邏輯)。
|
||||
- 驗收與部署: 修復後的代碼若通過 Gherkin 驗收,則無縫熱更新。
|
||||
- 避坑標記 (Pitfall Marking): - 如果 AI 無法修復 (例如:外部第三方 API 永久倒閉,或邏輯存在根本性死結)。
|
||||
- 系統會在 Cypher 圖形資料庫中,將該零件或該特定的三元組關係標記為 [HAS_PITFALL]。
|
||||
- 結果: 下一個生成系統的 AI 在透過 MCP 搜尋時,會讀取到這個坑的紀錄,並強制繞道,改用其他方案或生成新的零件,實現「前人踩坑,後 AI 避坑」的群體智慧。
|
||||
|
||||
## 6. 結論
|
||||
|
||||
u6u 不是一個單純的開發工具,它是一個生物體積木系統。
|
||||
透過「三元組」統一語言打破系統壁壘,透過「零件審核」保證基因優良,再透過「強制評價與避坑機制」實現演化。當這套系統運轉起來,AI 就能在其中無止盡地為人類組裝出越來越強大、越來越穩定的商業應用。
|
||||
@@ -0,0 +1,19 @@
|
||||
# u6u Design
|
||||
|
||||
u6u 是一個 AI Friendly 的 n8n。
|
||||
|
||||
- 用 workers 天生比 n8n 速度快
|
||||
- 用 Cypher binding,不需 deploy 就可以隨時修改執行,不然原生 workers 的 binding 要 deploy
|
||||
- 未來要有一個 GUI 可以解析 YAML 產生畫面,反之人拉的圖會產生 YAML
|
||||
- 內建核心元件,http request, webhook, cron, if, switch, set, credential 等功能
|
||||
- 用戶自建功能多數是 http request 只是去 call 不同的 API,可以隨時建立,它的「配方」recipe 可以分享
|
||||
- 每個 API Call 獨立但搜尋會整合,例如有人實作 call google sheets create table API,它不用做完整的,因為另一人要 delete table 時發現沒有,AI 直接做一個,下次搜尋 google sheets 時,就提供了 create table, delete sheets 兩個端點,也就是哪些是大家需要的功能自然產生
|
||||
- marketplace 機制,但是是給 AI 的,強制 AI 使用後要回覆使用的評價,如果一個零件被幾次評為不佳,其他的 AI 就可以避開這個零件
|
||||
- 自動審核:如果是 call API,只要成功 Call 通就是通過,如果是功能性的,只要通過他設置的 Gherkin 就是通過,省去人工審核的麻煩
|
||||
|
||||
## 觀念想法
|
||||
|
||||
- u6u 通過前端網頁開發功能,每個元件是一個零件
|
||||
- 視覺優先的開發:要解釋什麼是 webhook 很難,但一般用戶做一個前端的按鈕、輸入框... 後面就會綁定某個 webhook,點擊這個前端界面就看到後端邏輯的工作流,這樣就不用解釋太多。
|
||||
- 系統功能:我是一個用戶,我建立不同的功能,例如我建立 CRM,又建立 ERP,這些系統有很多流程是共用的,但當我建立多個工作流時,zoom out 就會看到我的公司內不同流程間的關係,因為 cypher 放大就是 graph,但每個功能要可以摺疊成一個點,又可以 zoom in 展開來調整某一段工作流,再 zoom in 調整一個零件
|
||||
- 考慮讓他自己 OWN,就是企業版可以讓資料是獨立的
|
||||
@@ -0,0 +1,9 @@
|
||||
# Wishlist — 已移至 InkStoneCo 頂層(2026-06-15)
|
||||
|
||||
> wishlist 是**跨專案的**(一個願望常牽動 arcrun + kbdb + gui + 多個 product),
|
||||
> 不該埋在單一 repo。已移到頂層總計劃:
|
||||
>
|
||||
> **真身**:`github.com/uncle6me-web/InkStoneCo` 的 `docs/1-vision/product-wishlist.md`
|
||||
>
|
||||
> arcrun 相關的願望(code 零件、標準流程圖、CLI 多語系/logs)在該檔 C 區。
|
||||
> 動到 arcrun 願望前讀那邊,不要在此重記(單一真相源)。
|
||||
Reference in New Issue
Block a user