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:
@@ -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