104 lines
4.4 KiB
TypeScript
104 lines
4.4 KiB
TypeScript
/**
|
||
* /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';
|
||
import { ensurePortalTemplates } from './portal';
|
||
|
||
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)}`);
|
||
}
|
||
}
|
||
|
||
// portal-auth P2(#24/#25):portal_user / portal_library template 也是「裝好後預設就緒」
|
||
// 的種子(KBDB 萬用表 template,零新表),冪等 ensure(已存在跳過)。seed 是 API 行為(rule 07)。
|
||
const portalTemplates = await ensurePortalTemplates(c.env);
|
||
|
||
const allOk = apiFail === 0 && authFail === 0 && portalTemplates.errors.length === 0;
|
||
return c.json(
|
||
{
|
||
success: allOk,
|
||
api_recipes: { seeded: apiOk, failed: apiFail, errors: apiErrors },
|
||
auth_recipes: { seeded: authOk, failed: authFail, errors: authErrors },
|
||
portal_templates: portalTemplates,
|
||
message: allOk
|
||
? `seed 完成:${apiOk} 個 API recipe + ${authOk} 個 auth recipe + portal templates(新建 ${portalTemplates.created.length}/已存在 ${portalTemplates.existing.length})`
|
||
: `seed 部分失敗(誠實回報,未假綠):API ${apiOk}✓/${apiFail}✗,auth ${authOk}✓/${authFail}✗,portal templates 錯誤 ${portalTemplates.errors.length}`,
|
||
},
|
||
allOk ? 200 : 207,
|
||
);
|
||
});
|