47c6aaea03
SDD: workflow-discovery 3.12/3.13(不是新規格;3.12 已 confirmed 並實作完成) ## 1) workers_ai_chat 種子(新) Cloudflare Workers AI 走 env.AI binding ⇒ 用戶不必填任何 API 金鑰就能問答。 放種子表而非產品安裝器:「裝好後預設有哪些 recipe」是平台能力(rule 07 薄殼原則)。 換模型/換供應商=改這一筆 recipe,workflow 不動。 選型實測(1.4.4 實例,真實長度 RAG prompt,每個模型連跑 2 次): llama-4-scout-17b 2373/2173 ms ✅ 答案最完整、引用正確 ← 選它 llama-3.3-70b-fp8-fast 3261/2147 ms ✅ 可用但波動較大 mistral-small-3.1-24b 3560/3631 ms qwen2.5-coder-32b 3572/3353 ms gpt-oss-120b 1971/2295 ms ❌ 回應形狀不同,response 取不到文字 gemma-3-12b-it ❌ 5018 帳號無權限 對照舊路徑 Gemini gemma-4-31b-it:同型提問 16.87 s,且吐整段英文思考草稿。 ## 2) 修 /init/seed 靜默吃掉 3.12 欄位 3.12 給 RecipeDefinition 加了 body_template/response_map/auth/binding_name, 但 /init/seed 是**列舉欄位重建** recipe record ⇒ 不在名單上的欄位被丟掉。 最惡劣的地方是「哪裡都不會紅」:recipe 查得到、endpoint 對,只有跑起來像沒設定過。 與 08-02 syncManifest 吃掉 manifest.daemon 欄同型(教訓:東西還在不在也要進機械閘)。 加 tests/init-seed-recipe-fields.test.ts:拿掉修復會紅、補回會綠(已實測會擋)。 ## 3) 修 D1 LIKE pattern 50 bytes 上限造成的 500 /entries/search?q=… 只要 q 超過 48 bytes 就回 HTTP 500,沒有錯誤訊息。 逐 byte 二分:48→200/49→500;中文 16 字→200/17 字→500。 判別實驗:q 固定 48 bytes、其他 filter 全塞滿讓 SQL 變很長 → 仍 200 ⇒ 爆的是 LIKE 的 pattern('%'+q+'%' = 50),不是 statement 長度。 中文問句超過 16 字是常態,而 rag_chat 用整句問題當 q ⇒ 聊天對正常問句等於不能用。 (=InkStoneCo status.md 待辦第 1 條「KBDB keyword 長查詢會炸」的根因。) 修法:q ≤ 48 bytes 走原路(行為逐字不變),超過才拆詞/切 UTF-8 邊界片段。 kbdb 全套 83 測全綠(含新增 8 項)。 ## 4) 順手 - 移除被 commit 進 repo 的 node_modules 壞 symlink(指向 leo Mac 的絕對路徑, 害任何 fresh clone 裝不起來、切分支還會把裝好的蓋掉——本次撞了兩次)。 - pending-changes.md 加 P2 提案(fan-out 並行執行)+等裁決,未動引擎。 驗證:cypher-executor 新增測試 17/17 綠;tsc 與基線逐字相同; 全套測試失敗集合與基線**逐字相同**(基線 14 個失敗,本分支 t173 既有,非本次引入)。
111 lines
4.9 KiB
TypeScript
111 lines
4.9 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,
|
||
// ③ payload/回應/binding 三層(3.12):不列進來的欄位會被**靜默吃掉**——
|
||
// 種子帶了 body_template/response_map/auth 卻沒進 KV,症狀是 recipe 存在但跑起來
|
||
// 「像沒設定過」,且哪裡都不會紅(08-02 manifest.daemon 欄被列舉式重建吃掉的同型)。
|
||
body_template: seed.body_template,
|
||
response_map: seed.response_map,
|
||
auth: seed.auth,
|
||
binding_name: seed.binding_name,
|
||
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,
|
||
);
|
||
});
|