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 既有,非本次引入)。
68 lines
3.8 KiB
TypeScript
68 lines
3.8 KiB
TypeScript
/**
|
||
* /init/seed 必須把種子的 3.12 三層欄位原樣寫進 KV —— SDD: workflow-discovery task 3.12/3.13
|
||
*
|
||
* 為什麼要有這個測試(別刪):
|
||
* 3.12 給 `RecipeDefinition` 加了 body_template / response_map / auth / binding_name,
|
||
* 但 `/init/seed` 當時是**列舉欄位重建** recipe record ⇒ 不在名單上的欄位被靜默吃掉。
|
||
* 症狀最惡劣的地方在於「哪裡都不會紅」:recipe 查得到、canonical_id 對、endpoint 對,
|
||
* 只有跑起來像沒設定過(auth 掉了 ⇒ 走 HTTP 路徑去 fetch「@cf/…」這種不是網址的字串)。
|
||
* 這與 2026-08-02 `syncManifest()` 列舉式重建吃掉 `manifest.daemon` 欄是同一型事故——
|
||
* 當時的教訓寫著:「**東西還在不在**也要進機械閘」,本檔就是那道閘。
|
||
*
|
||
* 範圍:只驗「種子 → KV」這段(純資料搬運)。真的呼叫 Workers AI 由實例端到端驗。
|
||
*/
|
||
import { describe, it, expect } from 'vitest';
|
||
import { env, SELF } from 'cloudflare:test';
|
||
import { API_RECIPE_SEEDS } from '../src/lib/api-recipe-seeds';
|
||
|
||
type StoredRecipe = {
|
||
canonical_id: string;
|
||
endpoint: string;
|
||
auth?: string;
|
||
binding_name?: string;
|
||
body_template?: Record<string, unknown>;
|
||
response_map?: { text_path?: string; answer_marker?: string; strip_prefixes?: string[] };
|
||
};
|
||
|
||
async function seedThenRead(canonicalId: string): Promise<StoredRecipe> {
|
||
const res = await SELF.fetch('https://example.com/init/seed', { method: 'POST' });
|
||
// 測試環境沒有 KBDB binding ⇒ portal template 那段必然失敗、整體回 207(誠實回報,非本測目標)。
|
||
// 本檔只管 API recipe 那半,所以驗它自己的計數,不驗整體 status。
|
||
const body = await res.json<{ api_recipes: { seeded: number; failed: number; errors: string[] } }>();
|
||
expect(body.api_recipes.errors).toEqual([]);
|
||
expect(body.api_recipes.failed).toBe(0);
|
||
const uuid = await env.RECIPES.get(`idx:installed:${canonicalId}`);
|
||
expect(uuid, `${canonicalId} 沒有被 seed 進 KV`).toBeTruthy();
|
||
return JSON.parse((await env.RECIPES.get(`recipe:${uuid}`))!) as StoredRecipe;
|
||
}
|
||
|
||
describe('/init/seed 不得靜默吃掉 recipe 的 3.12 欄位', () => {
|
||
it('workers_ai_chat 種子本身宣告齊四個欄位(種子端)', () => {
|
||
const seed = API_RECIPE_SEEDS.find(s => s.canonical_id === 'workers_ai_chat');
|
||
expect(seed, 'workers_ai_chat 種子不存在=裝完不會有免金鑰問答').toBeDefined();
|
||
expect(seed!.auth).toBe('binding');
|
||
expect(seed!.binding_name).toBe('AI');
|
||
expect(seed!.endpoint.startsWith('@cf/'), 'binding 型的 endpoint=模型 id').toBe(true);
|
||
expect(seed!.body_template).toBeDefined();
|
||
expect(seed!.response_map?.text_path).toBe('response');
|
||
});
|
||
|
||
it('seed 之後 KV 裡讀回來的仍帶 auth/binding_name/body_template/response_map(KV 端)', async () => {
|
||
const stored = await seedThenRead('workers_ai_chat');
|
||
expect(stored.auth, 'auth 掉了 ⇒ 會被當成 HTTP recipe 去 fetch 一個不是網址的字串').toBe('binding');
|
||
expect(stored.binding_name).toBe('AI');
|
||
expect(stored.body_template, 'body_template 掉了 ⇒ 整包 ctx 被當 payload 送給模型').toBeDefined();
|
||
expect(stored.response_map?.text_path, 'response_map 掉了 ⇒ 下游拿不到 text').toBe('response');
|
||
expect(stored.response_map?.answer_marker).toBe('【答】');
|
||
});
|
||
|
||
it('既有 HTTP 種子不受影響:沒宣告新欄位就是 undefined,不憑空長出來', async () => {
|
||
const stored = await seedThenRead('telegram_send');
|
||
expect(stored.auth).toBeUndefined();
|
||
expect(stored.binding_name).toBeUndefined();
|
||
expect(stored.body_template).toBeUndefined();
|
||
expect(stored.response_map).toBeUndefined();
|
||
expect(stored.endpoint).toContain('api.telegram.org');
|
||
});
|
||
});
|