feat: 薄殼原則落地 + seed 下沉 API + MCP 進主庫 + 部署一致性
壓測四橫向問題修正(docs 壓測報告):
① 薄殼原則成鐵律:能力長在 API,CLI/MCP/lib 只暴露
- seed 下沉成 API 行為:cypher-executor POST /init/seed(一次灌 API+auth recipe),
種子資料移到 server src/lib/api-recipe-seeds.ts,CLI 改薄殼一次呼叫
- 解除 deployFullyOk 連坐 + init 補 seed auth recipe + update 補 seed/全 KV
- registry SUBMISSIONS_KV 補進 REQUIRED_KV_NAMESPACES(修 20/21)
② MCP 統一帳號來源(單一 remote MCP + .env 切 MCP URL)
- MCP 從 sibling repo 搬進 arcrun/mcp/(remote Worker,route 改 mcp.arcrun.dev)
- config 加 mcp_url 三層解析 + getMcpUrl + DEFAULT_MCP_URL
- 新增 acr mcp-setup:依 config 寫專案 .mcp.json(接案切資料夾自動切 MCP)
- acr --version 改動態讀 package.json(根治漂移)
③ Deploy 一致性
- tests/release.feature + scripts/check-release.sh
- local-deploy.sh:CLI npm publish + auto patch bump + CHANGELOG
- local-deploy.sh bash 3.2 相容修正(mapfile / 空陣列 set -u)
- builtins/pnpm-lock.yaml
④ README self-hosted 同步現況(移除 R2 殘留、加 flag/env、多帳號)
CLI bump → 1.3.0
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -2,8 +2,9 @@
|
||||
* seed-api-recipes.ts
|
||||
*
|
||||
* 將現役 API recipe 種子上傳至目標 cypher-executor(prod 或 self-host)。
|
||||
* 種子資料的單一來源在 CLI 端(cli/src/lib/api-recipe-seeds.ts),此腳本 import 它,
|
||||
* 避免兩份種子定義漂移。
|
||||
* 種子資料的單一來源在 server 端(src/lib/api-recipe-seeds.ts,薄殼原則 rule 07),此腳本 import 它。
|
||||
* 注意:self-host init 與 prod 補灌建議改打 POST /init/seed(API 行為,一次灌 API+auth recipe);
|
||||
* 本腳本保留作為 KV 直寫的備援路徑。
|
||||
*
|
||||
* 執行:
|
||||
* npx tsx scripts/seed-api-recipes.ts
|
||||
@@ -18,7 +19,9 @@
|
||||
* 對應 SDD:.agents/specs/arcrun/sdk-and-website/self-hosted-init.md §5
|
||||
*/
|
||||
|
||||
import { API_RECIPE_SEEDS } from '../../cli/src/lib/api-recipe-seeds.js';
|
||||
// 種子資料的單一來源已移到 server 端(src/lib/api-recipe-seeds.ts,薄殼原則 rule 07)。
|
||||
// 注意:現在 prod 補灌建議直接打 POST /init/seed(API 行為);本腳本保留作為 KV 直寫的備援。
|
||||
import { API_RECIPE_SEEDS } from '../src/lib/api-recipe-seeds';
|
||||
|
||||
const BASE_URL = process.env.ARCRUN_API_URL ?? 'https://cypher.arcrun.dev';
|
||||
const API_KEY = process.env.ARCRUN_API_KEY ?? '';
|
||||
|
||||
@@ -19,6 +19,7 @@ import { webhooksNamedRouter } from './routes/webhooks-named';
|
||||
import { authRouter } from './routes/auth';
|
||||
import { resumeRouter } from './routes/resume';
|
||||
import { executionsRouter } from './routes/executions';
|
||||
import { initSeedRouter } from './routes/init-seed';
|
||||
|
||||
const app = new Hono<{ Bindings: Bindings }>();
|
||||
|
||||
@@ -46,6 +47,7 @@ app.route('/', credentialsRouter);
|
||||
app.route('/', authRouter);
|
||||
app.route('/', resumeRouter);
|
||||
app.route('/', executionsRouter); // LI SDD M2.1: /executions/* + /workflows/:name/executions
|
||||
app.route('/', initSeedRouter); // 薄殼原則:seed recipe 是 API 行為(rule 07,壓測 §4.1)
|
||||
|
||||
// Worker 導出(fetch + scheduled)
|
||||
// scheduled handler 對應 wrangler.toml [triggers].crons,每分鐘 tick;
|
||||
|
||||
@@ -0,0 +1,120 @@
|
||||
/**
|
||||
* api-recipe-seeds.ts(server 端,唯一真相)
|
||||
*
|
||||
* 現役 API recipe 的種子定義。self-host 新帳號裝好後,POST /init/seed 端點把這些灌進空的 RECIPES KV。
|
||||
*
|
||||
* API recipe = http_request + 固定設定(endpoint/method 模板)。
|
||||
* 不需 deploy Worker,cypher-executor 執行時直接 fetch(見 routes/recipes.ts)。
|
||||
*
|
||||
* 為何放在 cypher-executor/src(薄殼原則 rule 07 + 壓測 §5.5):
|
||||
* - 「裝好之後預設有哪些 recipe」是 API 的能力,不是 CLI 的。seed 應由 API 端點完成
|
||||
* (POST /init/seed),CLI/MCP 只呼叫一次。種子資料是這個能力的一部分,故放 server。
|
||||
* - 種子的 endpoint 字串(sheets.googleapis.com 等)是 recipe 的「資料欄位」(宣告打哪),
|
||||
* 非 TS 裡的呼叫實作;真正呼叫仍走零件 / http_request。rule 02 §2.2 hook 已對本檔加例外
|
||||
* (richblack 2026-06-06 授權,與 auth-recipe-seeds.ts 同理由)。
|
||||
*
|
||||
* 來源:2026-06-01 從 prod cypher.arcrun.dev/recipes 逐一查得的現役定義。
|
||||
* 對應 SDD:.agents/specs/arcrun/sdk-and-website/self-hosted-init.md §5
|
||||
*
|
||||
* KBDB recipe(kbdb_*)採 Supabase 模式(richblack 2026-06-02):
|
||||
* 進 seed = 展示能力(引子)。使用者要用 → 去 arcrun 取統一 API Key 當 credential。
|
||||
* FOLLOW-UP(KBDB 端):endpoint 現為 kbdb.finally.click,KBDB 應改用統一對外網址;
|
||||
* KBDB 改網址後同步更新此處。seed 先照現況進。
|
||||
*/
|
||||
|
||||
export interface ApiRecipeSeed {
|
||||
canonical_id: string;
|
||||
display_name: string;
|
||||
description?: string;
|
||||
endpoint: string;
|
||||
method: string;
|
||||
auth_service?: string;
|
||||
}
|
||||
|
||||
export const API_RECIPE_SEEDS: ApiRecipeSeed[] = [
|
||||
// ── KBDB(Supabase 模式,auth_service=kbdb static_key)──
|
||||
{
|
||||
canonical_id: 'kbdb_get',
|
||||
display_name: 'KBDB Get',
|
||||
description: 'GET 讀取 block / 查詢。_path 帶查詢路徑。auth: kbdb static_key。',
|
||||
endpoint: 'https://kbdb.finally.click{{_path}}',
|
||||
method: 'GET',
|
||||
auth_service: 'kbdb',
|
||||
},
|
||||
{
|
||||
canonical_id: 'kbdb_create_block',
|
||||
display_name: 'KBDB Create Block',
|
||||
description: 'POST /blocks 建立 block。body 帶 block 欄位(content/type/page_name/source/user_id 等)。auth: kbdb static_key。',
|
||||
endpoint: 'https://kbdb.finally.click/blocks',
|
||||
method: 'POST',
|
||||
auth_service: 'kbdb',
|
||||
},
|
||||
{
|
||||
canonical_id: 'kbdb_patch_block',
|
||||
display_name: 'KBDB Patch Block',
|
||||
description: 'PATCH /blocks/:id 局部更新。_path 帶 /blocks/{id},body 帶要改的欄位。auth: kbdb static_key。',
|
||||
endpoint: 'https://kbdb.finally.click{{_path}}',
|
||||
method: 'PATCH',
|
||||
auth_service: 'kbdb',
|
||||
},
|
||||
{
|
||||
canonical_id: 'kbdb_delete',
|
||||
display_name: 'KBDB Delete',
|
||||
description: 'DELETE /blocks/:id 刪除 block。_path 帶 /blocks/{id}。auth: kbdb static_key。',
|
||||
endpoint: 'https://kbdb.finally.click{{_path}}',
|
||||
method: 'DELETE',
|
||||
auth_service: 'kbdb',
|
||||
},
|
||||
{
|
||||
canonical_id: 'kbdb_ingest',
|
||||
display_name: 'KBDB Ingest',
|
||||
description: 'POST /blocks/ingest 批次寫入。body 帶 input。auth: kbdb static_key。',
|
||||
endpoint: 'https://kbdb.finally.click/blocks/ingest',
|
||||
method: 'POST',
|
||||
auth_service: 'kbdb',
|
||||
},
|
||||
|
||||
// ── Google(service_account)──
|
||||
{
|
||||
canonical_id: 'gmail_send',
|
||||
display_name: 'Gmail Send',
|
||||
description: '寄 Gmail。POST messages/send,body 帶 raw(base64url MIME)。auth: google service_account。',
|
||||
endpoint: 'https://gmail.googleapis.com/gmail/v1/users/me/messages/send',
|
||||
method: 'POST',
|
||||
auth_service: 'google_gmail_sa',
|
||||
},
|
||||
{
|
||||
canonical_id: 'google_sheets_append',
|
||||
display_name: 'Google Sheets Append',
|
||||
description: '寫 Sheets。PUT values?valueInputOption=RAW,body 帶 values。auth: google service_account。',
|
||||
endpoint: 'https://sheets.googleapis.com{{_path}}',
|
||||
method: 'PUT',
|
||||
auth_service: 'google_sheets_sa',
|
||||
},
|
||||
{
|
||||
canonical_id: 'google_sheets_read',
|
||||
display_name: 'Google Sheets Read',
|
||||
description: '讀 Sheets。GET values。_path 帶完整路徑。auth: google service_account。',
|
||||
endpoint: 'https://sheets.googleapis.com{{_path}}',
|
||||
method: 'GET',
|
||||
auth_service: 'google_sheets_sa',
|
||||
},
|
||||
|
||||
// ── 訊息(static_key)──
|
||||
{
|
||||
canonical_id: 'telegram_send',
|
||||
display_name: 'Telegram Send',
|
||||
description: 'Telegram sendMessage。token 在 URL path({{auth.bot_token}}),body 帶 chat_id+text。auth: static_key path 注入。',
|
||||
endpoint: 'https://api.telegram.org/bot{{auth.bot_token}}/sendMessage',
|
||||
method: 'POST',
|
||||
auth_service: 'telegram',
|
||||
},
|
||||
{
|
||||
canonical_id: 'line_notify_send',
|
||||
display_name: 'LINE Notify',
|
||||
description: 'LINE Notify 推訊息。POST notify,body 帶 message(form-urlencoded)。auth: static_key Bearer line token。',
|
||||
endpoint: 'https://notify-api.line.me/api/notify',
|
||||
method: 'POST',
|
||||
auth_service: 'line_notify',
|
||||
},
|
||||
];
|
||||
@@ -0,0 +1,102 @@
|
||||
/**
|
||||
* /init/seed — 一鍵把平台預建的 recipe 種子灌進 RECIPES KV(API 行為,非介面層職責)
|
||||
*
|
||||
* 薄殼原則(rule 07 + 壓測 §4.1/§5.5):
|
||||
* 「裝好後預設有哪些 recipe」是 API 的能力。seed 由本端點完成,CLI/MCP 等薄殼只呼叫一次。
|
||||
* 之前 seed 寫在 CLI init.ts(迴圈 POST + deployFullyOk gate),導致 registry 20/21 連坐 →
|
||||
* seed 永遠被跳過、auth recipe 從不被 seed(壓測 §4.1)。本端點把 seed 下沉到 API,根除連坐。
|
||||
*
|
||||
* 行為:
|
||||
* - 冪等:已存在的 recipe 直接覆寫(重跑安全)。
|
||||
* - 一次灌「API recipe(API_RECIPE_SEEDS)+ auth recipe(AUTH_RECIPE_SEEDS)」兩者。
|
||||
* - 直接寫 KV(不走 POST /recipes 的 exposure_consent gate):種子是平台預建、非用戶互動 push,
|
||||
* 帶 seed 層級的 consent 憑證(誠實標來源,軌跡可審;mindset §7:機制價值是歸責+可審非防偽)。
|
||||
* - 誠實回報:逐筆 ok/fail 計數,不假綠。
|
||||
*
|
||||
* 對應 SDD:.agents/specs/arcrun/sdk-and-website/self-hosted-init.md §5
|
||||
*/
|
||||
|
||||
import { Hono } from 'hono';
|
||||
import type { Bindings } from '../types';
|
||||
import { deriveRecipeHash } from '../lib/hash';
|
||||
import type { RecipeDefinition, AuthRecipeDefinition } from './recipes';
|
||||
import { API_RECIPE_SEEDS } from '../lib/api-recipe-seeds';
|
||||
import { AUTH_RECIPE_SEEDS } from '../lib/auth-recipe-seeds';
|
||||
|
||||
export const initSeedRouter = new Hono<{ Bindings: Bindings }>();
|
||||
|
||||
initSeedRouter.post('/init/seed', async (c) => {
|
||||
const now = Date.now();
|
||||
const seedConsent = {
|
||||
confirmed_by_human: true as const,
|
||||
understood: 'platform seed (init/seed): 平台預建 recipe,非用戶互動 push',
|
||||
confirmed_at: new Date(now).toISOString(),
|
||||
};
|
||||
|
||||
let apiOk = 0;
|
||||
let apiFail = 0;
|
||||
const apiErrors: string[] = [];
|
||||
|
||||
for (const seed of API_RECIPE_SEEDS) {
|
||||
try {
|
||||
const canonicalId = seed.canonical_id.trim().toLowerCase();
|
||||
const hashId = await deriveRecipeHash(canonicalId);
|
||||
const existing = await c.env.RECIPES.get(`recipe:${canonicalId}`, 'json') as RecipeDefinition | null;
|
||||
const recipe: RecipeDefinition = {
|
||||
canonical_id: canonicalId,
|
||||
hash_id: hashId,
|
||||
display_name: seed.display_name,
|
||||
description: seed.description,
|
||||
endpoint: seed.endpoint,
|
||||
method: (seed.method ?? 'POST').toUpperCase(),
|
||||
auth_service: seed.auth_service,
|
||||
exposure_consent: existing?.exposure_consent ?? seedConsent,
|
||||
created_at: existing?.created_at ?? now,
|
||||
updated_at: now,
|
||||
};
|
||||
await Promise.all([
|
||||
c.env.RECIPES.put(`recipe:${canonicalId}`, JSON.stringify(recipe)),
|
||||
c.env.RECIPES.put(`idx:${hashId}`, canonicalId),
|
||||
]);
|
||||
apiOk++;
|
||||
} catch (e) {
|
||||
apiFail++;
|
||||
apiErrors.push(`${seed.canonical_id}: ${e instanceof Error ? e.message : String(e)}`);
|
||||
}
|
||||
}
|
||||
|
||||
let authOk = 0;
|
||||
let authFail = 0;
|
||||
const authErrors: string[] = [];
|
||||
|
||||
for (const seed of AUTH_RECIPE_SEEDS) {
|
||||
try {
|
||||
const service = seed.service.trim().toLowerCase();
|
||||
const existing = await c.env.RECIPES.get(`auth_recipe:${service}`, 'json') as AuthRecipeDefinition | null;
|
||||
const recipe: AuthRecipeDefinition = {
|
||||
...seed,
|
||||
service,
|
||||
created_at: existing?.created_at ?? now,
|
||||
updated_at: now,
|
||||
};
|
||||
await c.env.RECIPES.put(`auth_recipe:${service}`, JSON.stringify(recipe));
|
||||
authOk++;
|
||||
} catch (e) {
|
||||
authFail++;
|
||||
authErrors.push(`${seed.service}: ${e instanceof Error ? e.message : String(e)}`);
|
||||
}
|
||||
}
|
||||
|
||||
const allOk = apiFail === 0 && authFail === 0;
|
||||
return c.json(
|
||||
{
|
||||
success: allOk,
|
||||
api_recipes: { seeded: apiOk, failed: apiFail, errors: apiErrors },
|
||||
auth_recipes: { seeded: authOk, failed: authFail, errors: authErrors },
|
||||
message: allOk
|
||||
? `seed 完成:${apiOk} 個 API recipe + ${authOk} 個 auth recipe`
|
||||
: `seed 部分失敗(誠實回報,未假綠):API ${apiOk}✓/${apiFail}✗,auth ${authOk}✓/${authFail}✗`,
|
||||
},
|
||||
allOk ? 200 : 207,
|
||||
);
|
||||
});
|
||||
Reference in New Issue
Block a user