Compare commits
2 Commits
747dc3f843
...
7e87a3336b
| Author | SHA1 | Date | |
|---|---|---|---|
| 7e87a3336b | |||
| 1794072179 |
@@ -1,20 +1,38 @@
|
||||
import type { ParsedTriplets, NodeRole } from './triplet-parser';
|
||||
import { resolveNodeRole } from './triplet-parser';
|
||||
import { resolveNodeRole, isVirtualIoName } from './triplet-parser';
|
||||
import { wasmWorkerUrl } from '../lib/component-loader';
|
||||
import { resolveRecipe } from '../routes/recipes';
|
||||
import type { RecipeDefinition } from '../routes/recipes';
|
||||
|
||||
export type NodeStatus = 'found' | 'missing' | 'unknown';
|
||||
/**
|
||||
* `not_found` 而非 `missing`:欄位契約以頂層機械考
|
||||
* `system-dev/docs/3-specs/arcrun-usable/verify.sh` 為準(01 組 grep `not_found`)。
|
||||
*/
|
||||
export type NodeStatus = 'found' | 'not_found' | 'unknown';
|
||||
|
||||
export type NodeInfo = {
|
||||
status: NodeStatus;
|
||||
componentId?: string;
|
||||
type: NodeRole;
|
||||
/** found 時標來源庫:零件 registry(component)或 recipe 庫(recipe)。 */
|
||||
source?: 'component' | 'recipe';
|
||||
/** 零件契約(found 時附上,讓 AI 知道怎麼填 payload)。 */
|
||||
input_schema?: unknown;
|
||||
/** 成功率(found 時附上,讓「被測過幾次」看得見)。 */
|
||||
success_rate?: number;
|
||||
stability?: string;
|
||||
/** missing 時給的相近零件建議(避免 AI 只知道「沒有」卻不知道該用什麼)。 */
|
||||
suggestions?: string[];
|
||||
/** recipe found 時附上(AI 看得懂這個 recipe 在打哪個 API)。 */
|
||||
description?: string;
|
||||
endpoint?: string;
|
||||
/**
|
||||
* not_found 時的分型指路(task 3.7):兩庫(零件 registry+recipe 庫)都查過才點名,
|
||||
* 並告訴 AI 該走哪條補件路+去哪裡看做法。欄位名 `suggestion`(單數字串)=verify.sh 03 組契約。
|
||||
*/
|
||||
suggestion?: string;
|
||||
/** not_found 時的相近零件候選(自然語言節點名 → 既有零件的媒合)。 */
|
||||
similar_components?: string[];
|
||||
/** not_found 時的相近 recipe 候選。 */
|
||||
similar_recipes?: string[];
|
||||
};
|
||||
|
||||
export type SearchResult = {
|
||||
@@ -22,13 +40,26 @@ export type SearchResult = {
|
||||
missingNodes: string[];
|
||||
};
|
||||
|
||||
/** searchNodes 需要的環境子集(cypher-handlers 傳整份 Bindings 進來也相容)。 */
|
||||
export type SearchNodesEnv = {
|
||||
WORKER_SUBDOMAIN?: string;
|
||||
/**
|
||||
* registry 位置覆蓋(可選,非機密)。未設 → 用 wasmWorkerUrl('registry', WORKER_SUBDOMAIN)
|
||||
* 現算(比照 KBDB_GRAPH_URL 慣例)。本地 wrangler dev / self-hosted 把 registry 掛別處時用。
|
||||
*/
|
||||
REGISTRY_BASE_URL?: string;
|
||||
/** recipe 庫(本 worker 自己的 KV;task 3.6 兩庫都查的第二庫)。 */
|
||||
RECIPES?: KVNamespace;
|
||||
};
|
||||
|
||||
/**
|
||||
* 對所有節點進行解析,確認每個節點對應的零件 ID 與**是否真的存在**。
|
||||
* 對所有節點進行解析,確認每個節點對應的零件/recipe 是否**真的存在**。
|
||||
*
|
||||
* ⚠️ 2026-07-30 改為會查 registry(workflow-discovery task 3.x)。
|
||||
* ⚠️ 2026-07-30 改為會查 registry(workflow-discovery task 3.x);
|
||||
* 2026-07-31 再加 recipe 庫查詢+缺件分型指路(task 3.6/3.7)。
|
||||
*
|
||||
* 改之前的行為(病灶):無條件回 `status: 'found'`、`missingNodes` 永遠是空陣列——
|
||||
* 型別雖宣告了 `'missing'` 但程式碼從不使用。實測「完全不存在的東西xyz」也回 found。
|
||||
* 實測「完全不存在的東西xyz」也回 found。
|
||||
*
|
||||
* 為什麼這是嚴重問題(leo 2026-07-30 定性「腹語術」):
|
||||
* AI 寫意圖 → 查詢回「都 found」(假信號)→ 實際零件不存在
|
||||
@@ -36,23 +67,32 @@ export type SearchResult = {
|
||||
* → 於是正式 workflow 只用 2 個零件、8 個 code 節點含 if×61
|
||||
* ⇒ 「零件被測過 1000 次所以 AI 只要填 payload」的價值完全落空。
|
||||
*
|
||||
* 誠實限制:查不到 registry(未部署/網路失敗)時回 `'unknown'` 而不是 `'missing'`——
|
||||
* 設計基調(leo 2026-07-31 二次定調):**回覆的重點是「缺哪些」不是「有哪些」**——
|
||||
* 有的照常編圖不必報告;缺的要兩庫(零件 registry+recipe 庫)都搜過後點名+給正確指示:
|
||||
* 缺外部 API → 自己寫 recipe(skill `write_recipe`);
|
||||
* 缺計算原語 → 投稿零件 PR(skill `add_new_wasm_component`)。
|
||||
*
|
||||
* 誠實限制:查不到 registry(未部署/網路失敗)時回 `'unknown'` 而不是 `'not_found'`——
|
||||
* 不能因為查詢失敗就宣告零件不存在(那會讓 AI 誤判而重寫 code,正是要避免的事)。
|
||||
*/
|
||||
export async function searchNodes(
|
||||
parsed: ParsedTriplets,
|
||||
config?: Record<string, Record<string, unknown>>,
|
||||
env?: { WORKER_SUBDOMAIN?: string },
|
||||
env?: SearchNodesEnv,
|
||||
): Promise<SearchResult> {
|
||||
const nodeResults: Record<string, NodeInfo> = {};
|
||||
const missingNodes: string[] = [];
|
||||
|
||||
const sub = env?.WORKER_SUBDOMAIN;
|
||||
const registryBase = env?.REGISTRY_BASE_URL ?? (sub ? wasmWorkerUrl('registry', sub) : undefined);
|
||||
|
||||
for (const nodeName of parsed.nodeNames) {
|
||||
const role = resolveNodeRole(nodeName, parsed);
|
||||
|
||||
if (role === 'Input' || role === 'Output') {
|
||||
// 只有**字面上的虛擬 IO 名**(input/trigger/…/output/done)才免查——
|
||||
// 位置上是頭節點但名字是真零件(`aes_encrypt >> … >> code` 的頭,role 也是 Input)
|
||||
// 仍要照常查兩庫,否則缺件被角色掩蓋、又回到「假 found」。
|
||||
if ((role === 'Input' || role === 'Output') && isVirtualIoName(nodeName)) {
|
||||
nodeResults[nodeName] = { status: 'found', componentId: nodeName.toLowerCase(), type: role };
|
||||
continue;
|
||||
}
|
||||
@@ -61,21 +101,22 @@ export async function searchNodes(
|
||||
const componentId = configComponent ?? nodeName;
|
||||
|
||||
// config 明確給了 component(多半是安裝器代入的 worker URL 或既有 workflow)
|
||||
// → 不判 missing。這條路徑的存在性由 component-loader 在執行時決定(原行為)。
|
||||
// → 不判 not_found。這條路徑的存在性由 component-loader 在執行時決定(原行為)。
|
||||
if (configComponent) {
|
||||
nodeResults[nodeName] = { status: 'found', componentId, type: role };
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!sub) {
|
||||
if (!registryBase) {
|
||||
nodeResults[nodeName] = { status: 'unknown', componentId, type: role };
|
||||
continue;
|
||||
}
|
||||
|
||||
const q = await fetchComponent(sub, componentId);
|
||||
// ── 第一庫:零件 registry ────────────────────────────────────────────────
|
||||
const q = await fetchComponent(registryBase, componentId);
|
||||
if (!q.ok) {
|
||||
// registry 查不通(未部署/網路失敗)⇒ 誠實回 unknown。
|
||||
// **不能誤判 missing**——那會讓 AI 以為零件不存在而重寫 code,正是要避免的事。
|
||||
// **不能誤判 not_found**——那會讓 AI 以為零件不存在而重寫 code,正是要避免的事。
|
||||
nodeResults[nodeName] = { status: 'unknown', componentId, type: role };
|
||||
continue;
|
||||
}
|
||||
@@ -84,6 +125,7 @@ export async function searchNodes(
|
||||
status: 'found',
|
||||
componentId,
|
||||
type: role,
|
||||
source: 'component',
|
||||
input_schema: q.entry.input_schema,
|
||||
success_rate: q.entry.success_rate,
|
||||
stability: q.entry.stability,
|
||||
@@ -91,13 +133,99 @@ export async function searchNodes(
|
||||
continue;
|
||||
}
|
||||
|
||||
nodeResults[nodeName] = { status: 'missing', componentId, type: role };
|
||||
// ── 第二庫:recipe 庫(task 3.6——「說不出某 recipe 沒有」的病灶就在漏了這步)──
|
||||
const recipe = env?.RECIPES ? await resolveRecipe(componentId, env.RECIPES) : null;
|
||||
if (recipe) {
|
||||
nodeResults[nodeName] = {
|
||||
status: 'found',
|
||||
componentId: recipe.canonical_id,
|
||||
type: role,
|
||||
source: 'recipe',
|
||||
description: recipe.description,
|
||||
endpoint: recipe.endpoint,
|
||||
};
|
||||
continue;
|
||||
}
|
||||
|
||||
// ── 兩庫都沒有 ⇒ not_found + 分型指路(task 3.7)+ 相近候選 ────────────
|
||||
const [similarComponents, similarRecipes] = await Promise.all([
|
||||
searchSimilarComponents(registryBase, nodeName),
|
||||
env?.RECIPES ? searchSimilarRecipes(env.RECIPES, nodeName) : Promise.resolve([]),
|
||||
]);
|
||||
|
||||
nodeResults[nodeName] = {
|
||||
status: 'not_found',
|
||||
componentId,
|
||||
type: role,
|
||||
suggestion: buildSuggestion(componentId),
|
||||
...(similarComponents.length > 0 ? { similar_components: similarComponents } : {}),
|
||||
...(similarRecipes.length > 0 ? { similar_recipes: similarRecipes } : {}),
|
||||
};
|
||||
missingNodes.push(nodeName);
|
||||
}
|
||||
|
||||
return { nodeResults, missingNodes };
|
||||
}
|
||||
|
||||
// ── 缺件分型(task 3.7)────────────────────────────────────────────────────────
|
||||
//
|
||||
// 分型判準(刻意用簡單可解釋的規則,不接 LLM——查詢端點要快、要可預測):
|
||||
// 1) 名字含**外部服務詞**(google/telegram/slack…)→「外部 API 樣貌」
|
||||
// → recipe 路:recipe 是 http_request+參數模板的具名封裝,用戶自己就能寫,不用改平台。
|
||||
// 2) 否則名字含**計算原語詞**(encrypt/hash/encode…)→「計算原語樣貌」
|
||||
// → 零件路:純計算得進 WASM 沙箱跑,要走 GitHub PR 投稿(人 merge=人類閘門,mindset §4)。
|
||||
// 3) 都不含 → 判不出型,誠實說判不出,兩條路都給(不硬猜——猜錯會把人指去錯的路)。
|
||||
// 判斷順序:服務詞優先於計算詞——「google_sheets_parse」雖含 parse,本質仍是打外部 API。
|
||||
|
||||
const SERVICE_HINTS = [
|
||||
'google', 'gmail', 'sheets', 'slides', 'gdocs', 'drive', 'calendar', 'youtube',
|
||||
'slack', 'telegram', 'discord', 'line', 'whatsapp', 'twilio',
|
||||
'notion', 'airtable', 'trello', 'jira', 'asana', 'linear',
|
||||
'github', 'gitea', 'gitlab', 'bitbucket',
|
||||
'stripe', 'paypal', 'shopify', 'hubspot', 'salesforce',
|
||||
'openai', 'anthropic', 'claude', 'gemini', 'groq',
|
||||
'twitter', 'facebook', 'instagram', 'linkedin', 'dropbox', 'zoom',
|
||||
'sendgrid', 'mailgun', 'kbdb',
|
||||
];
|
||||
|
||||
const COMPUTE_HINTS = [
|
||||
'encrypt', 'decrypt', 'cipher', 'aes', 'rsa', 'sha', 'md5', 'hmac', 'hash',
|
||||
'sign', 'verify', 'encode', 'decode', 'base64', 'hex',
|
||||
'compress', 'decompress', 'zip', 'gzip',
|
||||
'uuid', 'random', 'regex', 'math', 'calc',
|
||||
'sort', 'dedup', 'diff', 'template', 'render', 'convert', 'transform',
|
||||
'parse', 'format', 'csv', 'xml',
|
||||
];
|
||||
|
||||
function buildSuggestion(componentId: string): string {
|
||||
const lower = componentId.toLowerCase();
|
||||
const serviceHit = SERVICE_HINTS.find(w => lower.includes(w));
|
||||
const computeHit = COMPUTE_HINTS.find(w => lower.includes(w));
|
||||
|
||||
if (serviceHit) {
|
||||
return (
|
||||
`兩庫都查過,零件 registry 與 recipe 庫皆無「${componentId}」。` +
|
||||
`名字含服務詞「${serviceHit}」=外部 API 樣貌 → 沒有此 recipe,可自己寫:` +
|
||||
`寫法看 skill「write_recipe」(arcrun_get_skill('write_recipe')),` +
|
||||
`寫好用 acr recipe push 或 POST /recipes 裝上即可用,不用改平台。`
|
||||
);
|
||||
}
|
||||
if (computeHit) {
|
||||
return (
|
||||
`兩庫都查過,零件 registry 與 recipe 庫皆無「${componentId}」。` +
|
||||
`名字含計算詞「${computeHit}」=計算原語樣貌 → 沒有此零件,可投稿 PR 新增 WASM component:` +
|
||||
`做法看 skill「add_new_wasm_component」(arcrun_get_skill('add_new_wasm_component'))。`
|
||||
);
|
||||
}
|
||||
return (
|
||||
`兩庫都查過,零件 registry 與 recipe 庫皆無「${componentId}」,且名字判不出型。` +
|
||||
`缺外部 API → 自己寫 recipe(skill「write_recipe」);` +
|
||||
`缺計算能力 → 投稿零件 PR(skill「add_new_wasm_component」,component 進 WASM 沙箱)。`
|
||||
);
|
||||
}
|
||||
|
||||
// ── registry 查詢 ─────────────────────────────────────────────────────────────
|
||||
|
||||
type CatalogEntry = {
|
||||
input_schema?: unknown;
|
||||
success_rate?: number;
|
||||
@@ -112,16 +240,15 @@ type CatalogEntry = {
|
||||
* 這是 CP2-B 記載的缺口(「修 /components 404」)——補了列表端點後可改為抓一次。
|
||||
* 現階段逐個查:節點數通常 <10,且有 5s timeout,可接受。
|
||||
*
|
||||
* 回傳 `null` 代表「查不到 registry 或該零件不存在」,由呼叫端區分:
|
||||
* 整體查不通 → `unknown`;查得通但這顆沒有 → `missing`。
|
||||
* 回傳 `ok:false` 代表「查不到 registry」,由呼叫端區分:
|
||||
* 整體查不通 → `unknown`;查得通但這顆沒有 → 繼續查 recipe 庫。
|
||||
*/
|
||||
async function fetchComponent(
|
||||
subdomain: string,
|
||||
registryBase: string,
|
||||
id: string,
|
||||
): Promise<{ ok: boolean; entry?: CatalogEntry }> {
|
||||
try {
|
||||
const base = wasmWorkerUrl('registry', subdomain);
|
||||
const res = await fetch(`${base}/components/${encodeURIComponent(id)}`, {
|
||||
const res = await fetch(`${registryBase}/components/${encodeURIComponent(id)}`, {
|
||||
signal: AbortSignal.timeout(5000),
|
||||
});
|
||||
if (res.status === 404) return { ok: true }; // registry 活著,但沒這顆
|
||||
@@ -141,3 +268,76 @@ async function fetchComponent(
|
||||
return { ok: false };
|
||||
}
|
||||
}
|
||||
|
||||
// ── 相近候選(自然語言節點名 → 既有零件/recipe 的媒合)──────────────────────────
|
||||
//
|
||||
// 節點名常是自然語言(例「判斷有沒有新資料」)。leo:「AI 不用知道零件存在」——
|
||||
// 所以 not_found 時要主動給相近候選,讓 AI 看回覆就知道「其實有 if_control 可用」。
|
||||
// 做法:先拿全名打 registry `/components/search`;沒中再斷詞重試——
|
||||
// ASCII 取 3 字以上的詞、中日韓取 2-gram(registry search 是子字串比對,整句中文必落空,
|
||||
// 2-gram 才撈得到「判斷」→ if_control(display_name「條件判斷」)這種命中)。
|
||||
|
||||
function extractTokens(name: string): string[] {
|
||||
const tokens: string[] = [];
|
||||
const ascii = name.toLowerCase().match(/[a-z0-9]{3,}/g) ?? [];
|
||||
tokens.push(...ascii);
|
||||
const cjkRuns = name.match(/[一-鿿]+/g) ?? [];
|
||||
for (const run of cjkRuns) {
|
||||
for (let i = 0; i + 2 <= run.length; i++) tokens.push(run.slice(i, i + 2));
|
||||
}
|
||||
return [...new Set(tokens)].slice(0, 8); // 上限 8 個 token,避免對 registry 掃太多輪
|
||||
}
|
||||
|
||||
async function searchRegistryIds(registryBase: string, q: string): Promise<string[]> {
|
||||
try {
|
||||
const res = await fetch(`${registryBase}/components/search?q=${encodeURIComponent(q)}`, {
|
||||
signal: AbortSignal.timeout(5000),
|
||||
});
|
||||
if (!res.ok) return [];
|
||||
const body = (await res.json()) as { data?: { results?: Array<{ canonical_id?: string }> } };
|
||||
return (body.data?.results ?? []).map(r => r.canonical_id).filter((s): s is string => !!s);
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
async function searchSimilarComponents(registryBase: string, nodeName: string): Promise<string[]> {
|
||||
// 1) 全名直接搜
|
||||
const direct = await searchRegistryIds(registryBase, nodeName);
|
||||
if (direct.length > 0) return direct.slice(0, 3);
|
||||
|
||||
// 2) 斷詞搜,依命中次數排序
|
||||
const tokens = extractTokens(nodeName);
|
||||
if (tokens.length === 0) return [];
|
||||
const hits = await Promise.all(tokens.map(t => searchRegistryIds(registryBase, t)));
|
||||
const count = new Map<string, number>();
|
||||
for (const ids of hits) {
|
||||
for (const id of ids) count.set(id, (count.get(id) ?? 0) + 1);
|
||||
}
|
||||
return [...count.entries()].sort((a, b) => b[1] - a[1]).slice(0, 3).map(([id]) => id);
|
||||
}
|
||||
|
||||
/** recipe 庫的相近候選:KV 全列(本部署 recipe 數量小)後子字串比對。 */
|
||||
async function searchSimilarRecipes(kv: KVNamespace, nodeName: string): Promise<string[]> {
|
||||
try {
|
||||
const list = await kv.list({ prefix: 'recipe:' });
|
||||
const all = (await Promise.all(
|
||||
list.keys.map(k => kv.get(k.name, 'json') as Promise<RecipeDefinition | null>),
|
||||
)).filter(Boolean) as RecipeDefinition[];
|
||||
|
||||
const tokens = [nodeName.toLowerCase(), ...extractTokens(nodeName)];
|
||||
const seen = new Set<string>();
|
||||
const matched: string[] = [];
|
||||
for (const r of all) {
|
||||
if (seen.has(r.canonical_id)) continue;
|
||||
const hay = `${r.canonical_id} ${r.display_name ?? ''} ${r.description ?? ''}`.toLowerCase();
|
||||
if (tokens.some(t => hay.includes(t))) {
|
||||
seen.add(r.canonical_id);
|
||||
matched.push(r.canonical_id);
|
||||
}
|
||||
}
|
||||
return matched.slice(0, 3);
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -105,6 +105,17 @@ export function parseTriplets(rawTriplets: unknown[]): ParsedTriplets | null {
|
||||
const INPUT_NAMES = new Set(['input', 'trigger', 'webhook', 'start']);
|
||||
const OUTPUT_NAMES = new Set(['output', 'result', 'end', 'done']);
|
||||
|
||||
/**
|
||||
* 是否為「虛擬 IO 節點名」(input/output 這類非零件的佔位節點)。
|
||||
* searchNodes 用它決定存在性查詢的短路:**只有字面上是虛擬 IO 名**才免查——
|
||||
* 位置上是頭節點但名字是真零件(例 `aes_encrypt >> ON_SUCCESS >> code` 的頭)
|
||||
* 仍要查兩庫,否則缺件被角色掩蓋、又回到「假 found」(task 3.7 實測踩到)。
|
||||
*/
|
||||
export function isVirtualIoName(name: string): boolean {
|
||||
const lower = name.toLowerCase();
|
||||
return INPUT_NAMES.has(lower) || OUTPUT_NAMES.has(lower);
|
||||
}
|
||||
|
||||
/** 根據節點在圖中的位置決定其 type
|
||||
*
|
||||
* 規則:
|
||||
|
||||
@@ -124,6 +124,10 @@ export type Bindings = {
|
||||
// 未設 → 純文字顯示,行為與現狀一字不變。知識庫 repo 是 private 時點了會要登入——要不要
|
||||
// 設由實例自己決定(demo 知識庫是 public,適用)。
|
||||
PORTAL_SOURCE_WEB_BASE?: string;
|
||||
// 零件 registry worker base URL(可選,非機密)。未設 → 用 WORKER_SUBDOMAIN 現算
|
||||
// https://arcrun-registry.<subdomain>.workers.dev(wasmWorkerUrl 慣例)。
|
||||
// 本地 wrangler dev/self-hosted 把 registry 掛別處時覆蓋(/cypher/search 存在性查詢用)。
|
||||
REGISTRY_BASE_URL?: string;
|
||||
// kbdb-graph-plugin worker base URL(可選)。未設 → 用 WORKER_SUBDOMAIN 現算
|
||||
// https://kbdb-graph-plugin.<subdomain>.workers.dev(該 repo wrangler.toml name 固定)。
|
||||
// console 卡片詳頁「關聯視圖」經 cypher proxy 打它(kbdb-proxy.ts /kbdb/graph/neighbors/:name)。
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
| 要做檢索問答(RAG) | `rag_with_arcrun` | `arcrun_get_skill('rag_with_arcrun')` |
|
||||
| workflow 卡住不動/paused | `debug_paused_workflow` | `arcrun_get_skill('debug_paused_workflow')` |
|
||||
| 想把 http 呼叫改成觸發別的 workflow | `migrate_http_to_trigger_workflow` | 同上 |
|
||||
| **缺某個外部 API 的 recipe**(查詢回 not_found 指 recipe 路)| `write_recipe` | `arcrun_get_skill('write_recipe')` |
|
||||
| **真的需要新零件**(罕見)| `add_new_wasm_component` | 同上 ⚠️ 先確認工作流做不到 |
|
||||
|
||||
## 二、我要查「有沒有現成的東西」
|
||||
@@ -39,7 +40,7 @@
|
||||
|
||||
| 坑 | 現況 | 怎麼避 |
|
||||
|---|---|---|
|
||||
| **`/cypher/search` 回假 `found`** | 對**任何**節點名都回 found(不查 registry)| status 目前不可信,改用 `list_components` 自己確認。修復中(CP `arcrun-usable` 步驟 3)|
|
||||
| **`/cypher/search` 曾回假 `found`** | 2026-07-31 已修:兩庫(零件+recipe)都查,缺件回 `not_found`+`suggestion` 指路。舊實例(未更新部署)仍是假 found | 拿到 `not_found` 照 `suggestion` 走;拿到 `unknown`=查不到 registry ≠ 不存在 |
|
||||
| **引擎沒有條件分支** | `grep ON_TRUE\|ON_FALSE` = 0;`if_control` 只回 boolean | 判斷寫成獨立節點接 `ON_SUCCESS`。見 Gitea Arcrun#5 |
|
||||
| **`registry/examples/` 8/13 是壞的** | 引用不存在的零件(把 recipe 當零件寫)| **別照抄 examples**,改用 `arcrun_get_workflow` 拿實跑過的 |
|
||||
| **registry 可能是空的** | 安裝器無註冊步驟 ⇒ 新實例查不到零件 | 查不到 ≠ 不存在,別據此改寫成 code |
|
||||
|
||||
@@ -121,13 +121,12 @@ curl -s -X POST https://arcrun-cypher-executor.<subdomain>.workers.dev/cypher/se
|
||||
|
||||
| status | 意思 | 你該做什麼 |
|
||||
|---|---|---|
|
||||
| `found` | 有零件,附 `input_schema`(怎麼填 payload)與 `success_rate` | **只填 payload** |
|
||||
| `missing` | 沒有這個零件 | 缺 API → 寫 recipe;缺能力 → 投稿零件 PR |
|
||||
| `found` | 有這個節點。`source: component` 附 `input_schema`(怎麼填 payload)與 `success_rate`;`source: recipe` 附 description/endpoint | **只填 payload** |
|
||||
| `not_found` | **兩庫(零件 registry+recipe 庫)都查過,確定沒有** | 照 `suggestion` 欄走:缺 API → 寫 recipe(skill `write_recipe`);缺計算能力 → 投稿零件 PR(skill `add_new_wasm_component`)。`similar_components`/`similar_recipes` 是相近候選——先看有沒有現成的能直接用 |
|
||||
| `unknown` | 查不到 registry | **不代表不存在**,別據此改寫成 code |
|
||||
|
||||
⚠️ **2026-07-30 已知限制**:`/cypher/search` 目前對**任何**節點名都回 `found`
|
||||
(不查 registry)⇒ **這個 status 現在不可信**。修復中(CP `arcrun-usable` 步驟 3)。
|
||||
在它修好前:用 `arcrun_list_components` / `arcrun_search_components` 自己確認零件是否存在。
|
||||
> 註(2026-07-31):`/cypher/search` 曾對任何節點名都回假 `found`,已修為真查兩庫。
|
||||
> 舊實例(未更新部署)仍可能假 found——status 可信度以該實例部署版本為準。
|
||||
|
||||
---
|
||||
|
||||
@@ -137,7 +136,7 @@ curl -s -X POST https://arcrun-cypher-executor.<subdomain>.workers.dev/cypher/se
|
||||
2. **第一個節點不是 `input`**
|
||||
3. **把 recipe 當零件寫**——`telegram_send`/`gmail`/`kbdb_get` 是 **recipe** 不是零件
|
||||
→ 寫成 `http_request` + 該 recipe
|
||||
4. 🔴 **查詢回 `missing` 就改寫成 `code` 節點**
|
||||
4. 🔴 **查詢回 `not_found` 就改寫成 `code` 節點**
|
||||
→ 那叫「腹語術」(表面用 Arcrun、實際全寫 JS)。正解:缺 API 寫 recipe、缺能力投稿零件。
|
||||
`code` 只用在**局部整形**(例:剝掉 LLM 回應的雜訊),不用來取代零件與流程控制。
|
||||
|
||||
|
||||
@@ -0,0 +1,137 @@
|
||||
# Skill: Write Recipe(寫 recipe)
|
||||
|
||||
## 何時用這個 skill
|
||||
|
||||
**`/cypher/search` 回 `not_found` 且 `suggestion` 指「寫 recipe」時,讀這支。**
|
||||
|
||||
- 要打某個外部 API(Google Slides / Slack / 任何有 HTTP API 的服務),但 `arcrun_recipe_list()` 沒有
|
||||
- `acr recipe search <關鍵字>` 落空,回「公庫無符合的 recipe」
|
||||
- 你想把一條常用的 API 呼叫封裝成可重用、可投稿的配方
|
||||
|
||||
> 🔴 **不要因為沒有 recipe 就改寫成 `code` 節點**——那是「腹語術」。
|
||||
> recipe 不用改平台、不用部署 Worker、不用寫程式,**幾行 YAML 就能自己補上**。
|
||||
|
||||
## recipe 是什麼
|
||||
|
||||
**「http_request + 參數模板」的具名封裝**(真身存在 cypher-executor 的 RECIPES KV)。
|
||||
執行時 cypher-executor 直接 fetch 該 endpoint——不 deploy Worker、不寫 WASM。
|
||||
|
||||
| 你缺的是… | 走哪條路 |
|
||||
|---|---|
|
||||
| 打**外部 API**(服務有 HTTP API) | **recipe(本 skill)** |
|
||||
| **計算能力**(加解密/壓縮這類純運算) | 零件 PR → `arcrun_get_skill('add_new_wasm_component')` |
|
||||
|
||||
## 1. Recipe 的欄位(schema)
|
||||
|
||||
```yaml
|
||||
canonical_id: google_slides_create # 必填。小寫底線,全庫唯一的可讀名
|
||||
endpoint: https://slides.googleapis.com/v1/presentations # 必填。要打的 URL
|
||||
method: POST # 選填,預設 POST(GET/PUT/PATCH/DELETE 皆可)
|
||||
display_name: Google Slides Create # 選填,人看的名字
|
||||
description: 建立一份新簡報。POST presentations,body 帶 title。auth: google service_account。
|
||||
auth_service: google_slides_sa # 選填。指向 auth recipe(見第 3 節)
|
||||
headers: {} # 選填。額外固定 header
|
||||
body: {} # 選填。固定 body 欄位(會與節點 payload 合併)
|
||||
```
|
||||
|
||||
- `hash_id`(`rec_xxxxxxxx`)與 `uuid` 由系統自動生成,不用寫。
|
||||
- **description 認真寫**:AI(包括未來的你)靠它決定要不要用這個 recipe。
|
||||
照庫裡的慣例寫「做什麼。怎麼打。auth 用什麼。」三段。
|
||||
|
||||
## 2. 真範例(從實際 seed 且實跑過的 recipe 照抄)
|
||||
|
||||
### A. token 在 URL path(`telegram_send`)
|
||||
|
||||
```yaml
|
||||
canonical_id: telegram_send
|
||||
display_name: Telegram Send
|
||||
description: Telegram sendMessage。token 在 URL path({{auth.bot_token}}),body 帶 chat_id+text。auth: static_key path 注入。
|
||||
endpoint: https://api.telegram.org/bot{{auth.bot_token}}/sendMessage
|
||||
method: POST
|
||||
auth_service: telegram
|
||||
```
|
||||
|
||||
### B. Bearer/service account(`gmail_send`)
|
||||
|
||||
```yaml
|
||||
canonical_id: gmail_send
|
||||
display_name: Gmail Send
|
||||
description: 寄 Gmail。POST messages/send,body 帶 raw(base64url MIME)。auth: google service_account。
|
||||
endpoint: https://gmail.googleapis.com/gmail/v1/users/me/messages/send
|
||||
method: POST
|
||||
auth_service: google_gmail_sa
|
||||
```
|
||||
|
||||
### 模板變數(endpoint 裡可用)
|
||||
|
||||
| 變數 | 來源 | 例 |
|
||||
|---|---|---|
|
||||
| `{{auth.X}}` | auth recipe 的 `inject.path` 注入 | `bot{{auth.bot_token}}/sendMessage` |
|
||||
| `{{_path}}` | 節點 payload 的 `_path` 欄位(路徑由工作流決定時用) | `https://sheets.googleapis.com{{_path}}` |
|
||||
|
||||
🔴 **金鑰只准名字,不准真身**:endpoint/headers 裡**絕不**寫死 token。
|
||||
真值走 credential 中心(`acr creds push`),recipe 只留 `{{auth.X}}` 這種名字引用。
|
||||
|
||||
## 3. auth 怎麼接(多數 recipe 需要)
|
||||
|
||||
`auth_service: telegram` 表示執行時去拿 `auth_recipe:telegram` 做認證注入。
|
||||
先查有沒有:`acr auth-recipe list` 或 `GET /auth-recipes/<service>`。
|
||||
|
||||
**沒有就要一併建**(`POST /auth-recipes`,缺這步 recipe 會注入空值打不通):
|
||||
|
||||
```yaml
|
||||
service: telegram # 必填
|
||||
primitive: static_key # 必填:static_key | oauth2 | service_account
|
||||
base_url: https://api.telegram.org # 必填
|
||||
required_secrets: # 必填,每個 secret 的 help_url 也必填(官方文件連結)
|
||||
- key: telegram_bot_token
|
||||
label: Bot Token(從 @BotFather 取得)
|
||||
help_url: https://core.telegram.org/bots/features#botfather
|
||||
inject: # 必填:secret 注入到哪(header / query / body / path)
|
||||
path:
|
||||
bot_token: "{{secret.telegram_bot_token}}"
|
||||
```
|
||||
|
||||
然後用戶端上傳真值:`acr creds push`(存進 credential 中心,AI 拿不到真身)。
|
||||
|
||||
## 4. 裝上(三個介面同一個 API)
|
||||
|
||||
```bash
|
||||
acr recipe push my_recipe.yaml # CLI:push 時會實打 endpoint 做打通檢查
|
||||
```
|
||||
|
||||
- MCP:`arcrun_recipe_push(...)`
|
||||
- HTTP:`POST /recipes`(JSON,欄位同上)
|
||||
|
||||
裝好後 workflow 直接引用(節點名=canonical_id,或 config 指定):
|
||||
|
||||
```yaml
|
||||
config:
|
||||
notify:
|
||||
component: telegram_send # 或穩定引用 rec_xxxxxxxx
|
||||
```
|
||||
|
||||
payload(如 `chat_id`、`text`)由上游節點或 context 給——這正是「AI 只填 payload」。
|
||||
|
||||
## 5. 驗收(誠實原則)
|
||||
|
||||
1. push 時的**打通檢查**只是提醒級——真驗收=**跑一次 workflow、`verdict=success`(2xx)**
|
||||
2. 缺 credential 打不到 2xx → 誠實標「未驗收:缺 X」,**不 mock 充綠燈**
|
||||
3. 打通後可投稿公庫讓別人用:`acr recipe submit-p <canonical_id>`(成為該 recipe 的作者版本)
|
||||
|
||||
## 6. 常犯的錯
|
||||
|
||||
1. **token 寫死在 endpoint/headers** → 金鑰鐵律違規。只准 `{{auth.X}}` 名字引用
|
||||
2. **method 用猜的** → 查官方文件。實錄:Sheets append 被猜成 PUT(官方是 POST `:append`),
|
||||
seed 到壞 recipe 每個新用戶都打 400
|
||||
3. **只建 recipe 忘了 auth recipe** → `{{auth.X}}` 注入空值打不通。實錄:telegram auth recipe
|
||||
漏進種子,所有新實例的 telegram 發訊全斷
|
||||
4. **落空就寫 code 節點** → 腹語術。recipe 是正路,且寫好可投稿讓全生態重用
|
||||
5. **canonical_id 用大寫/空白** → 一律小寫底線(系統會 trim+lowercase,但別靠它救)
|
||||
|
||||
## 7. 相關
|
||||
|
||||
- 寫意圖工作流(上游,先寫意圖再查缺什麼):`arcrun_get_skill('write_intent_workflow')`
|
||||
- 缺的是計算零件不是 API:`arcrun_get_skill('add_new_wasm_component')`
|
||||
- 查公庫現貨:`acr recipe search <關鍵字>` / `arcrun_recipe_search(...)`
|
||||
- 拉別人寫好的:`acr recipe pull <canonical_id> [--author=<name>]`
|
||||
@@ -22,6 +22,10 @@ export interface ComponentRecord {
|
||||
call_count: number;
|
||||
wasm_r2_key?: string;
|
||||
score: number;
|
||||
// 零件合約的 I/O schema。KV 記錄一直有存(indexOnlyComponent 寫入),
|
||||
// 過去查詢層把它丟掉 ⇒ /cypher/search 給不出「怎麼填 payload」——2026-07-31 補透傳。
|
||||
input_schema?: Record<string, unknown>;
|
||||
output_schema?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
// ── id 解析:支援 hash_id 和 canonical_id 兩種格式 ──────────────────────────
|
||||
@@ -158,5 +162,11 @@ function toComponentRecord(v: Record<string, unknown>): ComponentRecord {
|
||||
call_count: parseInt(String(v.call_count ?? '0'), 10),
|
||||
wasm_r2_key: v.wasm_r2_key ? String(v.wasm_r2_key) : undefined,
|
||||
score: computeScore(v),
|
||||
input_schema: isPlainObject(v.input_schema) ? v.input_schema : undefined,
|
||||
output_schema: isPlainObject(v.output_schema) ? v.output_schema : undefined,
|
||||
};
|
||||
}
|
||||
|
||||
function isPlainObject(x: unknown): x is Record<string, unknown> {
|
||||
return typeof x === 'object' && x !== null && !Array.isArray(x);
|
||||
}
|
||||
|
||||
@@ -61,13 +61,24 @@
|
||||
> 機械考題已在頂層 `arcrun-usable/verify.sh` 03 組(aes_encrypt/google_slides_create 兩題,
|
||||
> 現對實例跑全紅=正確標記)。**行為綠過才准佔用 arm 部署**(頂層 mistakes 07-31 條)。
|
||||
|
||||
- [ ] 3.6 recipe 納入 `/cypher/search` 存在性查詢(現只查零件 registry ⇒ 說不出「某 recipe 沒有」)
|
||||
- [ ] 3.7 缺件回應分型指路:`suggestion` 欄——計算原語型→「投稿零件 PR」;外部 API 型→「自己寫 recipe」
|
||||
- [x] 3.6 recipe 納入 `/cypher/search` 存在性查詢(現只查零件 registry ⇒ 說不出「某 recipe 沒有」)
|
||||
— 2026-07-31 search-nodes.ts:registry 落空後查 RECIPES KV(resolveRecipe),
|
||||
recipe found 附 source:'recipe'+description+endpoint;本地實測 telegram_send found ✓
|
||||
- [x] 3.7 缺件回應分型指路:`suggestion` 欄——計算原語型→「投稿零件 PR」;外部 API 型→「自己寫 recipe」
|
||||
(欄位契約定案時同步頂層 verify.sh 03 組的 grep)
|
||||
**且每筆要帶「去哪裡看」**(leo 07-31 三次定調):component 路→skill `add_new_wasm_component`
|
||||
(已存在、安裝器現會 seed 進新實例);recipe 路→skill `write_recipe`(**不存在,見 3.8**)
|
||||
- [ ] 3.8 寫 `write_recipe` skill(registry/skills/)——「怎麼寫 recipe」的 playbook 目前是空地,
|
||||
— 2026-07-31 完成:status 契約定 `not_found`(同 verify.sh 01/03 grep);分型=服務詞/計算詞
|
||||
簡單規則(不接 LLM,規則在 code 註解);附 similar_components/similar_recipes 相近候選
|
||||
(自然語言「判斷有沒有新資料」媒合到 if_control)。順手修二病灶:①頭節點被 resolveNodeRole
|
||||
判 Input 而免查(`aes_encrypt >> … >> code` 假 found)→ 只有字面 input/output 名才短路;
|
||||
② registry 查詢層把 KV 裡的 input_schema 丟掉 → 補透傳。本地 wrangler dev 跑頂層
|
||||
verify.sh 01+03 組 9/9 全綠(部署後要對真實例重驗)
|
||||
- [x] 3.8 寫 `write_recipe` skill(registry/skills/)——「怎麼寫 recipe」的 playbook 目前是空地,
|
||||
3.7 的 recipe 指路沒有目的地;寫完納入安裝器 seed 清單(compile-skills.mjs 自動收)
|
||||
— 2026-07-31 完成:內容從真 code 反推(RecipeDefinition schema/api-recipe-seeds 的
|
||||
telegram_send+gmail_send 真範例/auth-recipes 必填欄位/acr recipe push 打通檢查),
|
||||
比照 write_intent_workflow 風格;INDEX.md+write_intent_workflow.md 同步指路 not_found
|
||||
- 設計基調(leo 07-31 二次定調):**回覆的重點是「缺哪些」不是「有哪些」**——
|
||||
有的照常編圖不必報告;缺的要兩庫(零件+recipe)都搜過後點名+給正確指示。
|
||||
驗收兩層:機械(verify 01/03)綠 → haiku 真考(只讀回覆就能說出缺什麼、該做什麼)。
|
||||
|
||||
Reference in New Issue
Block a user