/** * /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:種子是平台預建、非用戶互動 push(暴露 consent 閘已於 Arcrun#13 移除)。 * - 誠實回報:逐筆 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 { installRecipeRecord, resolveRecipe } 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(); // 暴露 consent 閘已移除(leo 2026-06-29,Arcrun#13):種子不再帶 exposure_consent。 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); // UUID 模型(§7.5.5):種子 author='system'。冪等:已安裝沿用其 uuid,否則新領。 const existing = await resolveRecipe(canonicalId, c.env.RECIPES); const recipe: RecipeDefinition = { uuid: existing?.uuid ?? crypto.randomUUID(), author: existing?.author ?? 'system', 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, created_at: existing?.created_at ?? now, updated_at: now, }; await installRecipeRecord(c.env.RECIPES, recipe); 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, ); });