t158 迴歸修復「部署≠發現」:複製路徑回毫秒級純編圖,誠實化只留在 discover
leo 07-31 定調:「這裡只是複製一些工作流的 data 過去,沒有要在這裡驗證,難怪這麼慢。
就算是我自己寫了錯的工作流,也可以跑跑看,如果錯誤就修改,
沒有說有錯誤還要一個個驗證這回事。」
定性=迴歸非新設計:部署路徑純複製是既有設計(arcrun-rag installer/src/index.js:13
「workflows.json 是既有 workflows/*.local.yaml 的搬運(打包期抽 flow/config)」+
arcrun-rag wiki「workflow 打包期預編成 workflows.json(worker 免帶 parser)」)。
5cadc60 起誠實化漏進 /cypher/search ⇒ 複製路徑也逐節點跑兩庫查詢+相似搜尋
(每 missing 節點 1+9 次 HTTP+recipe KV 掃)⇒ 冷實例 8 節點實測 25.7s、
安裝器 15s timeout 必炸(leo stage 實走 rag_takedown_direct aborted)。
改動:
- /cypher/search 加 mode:compile=純編圖零查詢(安裝器/acr push 複製路徑);
discover=誠實查詢預設(AI 問「有沒有」的既有契約,not_found+分型指路全保留)
- /cypher/execute 一律 compile(存在性由 component-loader 執行時決定=原權威)
- compile 的節點 status 標 unchecked(誠實「沒查」,不回假 found)
- discover 批次化:registry 新增 GET /components/catalog(一次回全目錄含
input_schema,補 CP2-B「沒有列表端點」缺口)+recipe 清單一次抓,
存在判定與相似度全記憶體比對;舊 registry 無 catalog 端點 → 退回逐顆(相容);
registry 整個查不通 → unknown 照舊(不誤判 not_found)
- cli push 帶 mode:compile+拔 missing 擋(push 不看 missing;要問有沒有走 validate)
驗(本地 wrangler dev 誠實環境,registry 種 20 合約):
- compile 編圖:graph_neighbors 39ms/rag_chat(11節點) 3ms/rag_ingest_card 2ms/
rag_takedown_direct(8節點) 3ms——回迴歸前毫秒級
- 安裝器 pushWorkflowTo(mode:compile)4/4 ok(44/7/7/5ms)
- 故意引用不存在零件的 workflow:部署 ok=true,trigger 執行時誠實報
「找不到零件…」+可用零件清單(部署≠發現實證)
- discover 契約:邏輯名 missing=2+not_found+suggestion(21ms);
頂層 verify.sh 01 組 5/5+03 組 4/4 全綠
- cypher+registry tsc 全綠;vitest 9 failed/179 passed=5cadc60 基線完全相同
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -55,10 +55,12 @@ export async function cmdPush(filePath: string): Promise<void> {
|
||||
const searchSpinner = ora('取得執行圖').start();
|
||||
let graph: unknown;
|
||||
try {
|
||||
// t158「部署≠發現」(leo:「這裡只是複製工作流的 data 過去,沒有要在這裡驗證」):
|
||||
// push=複製路徑,帶 mode:compile 純編圖——寫錯的 workflow 照樣部署,錯在執行時現形。
|
||||
const res = await fetch(`${executorUrl}/cypher/search`, {
|
||||
method: 'POST',
|
||||
headers,
|
||||
body: JSON.stringify({ triplets: workflow.flow }),
|
||||
body: JSON.stringify({ triplets: workflow.flow, mode: 'compile' }),
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
@@ -68,10 +70,8 @@ export async function cmdPush(filePath: string): Promise<void> {
|
||||
}
|
||||
|
||||
const data = await res.json() as { cypher: { nodes: unknown[]; edges: unknown[] }; missing: string[] };
|
||||
if (data.missing?.length > 0) {
|
||||
searchSpinner.fail(chalk.red(`以下零件不存在:${data.missing.join(', ')}\n執行 acr parts 查看可用零件。`));
|
||||
process.exit(1);
|
||||
}
|
||||
// t158:push 不看 missing(compile 模式亦恆空)——存在性由執行時 component-loader 決定;
|
||||
// 要「先問有沒有」用 acr validate/MCP 查詢(discover 路徑)。
|
||||
|
||||
// 附上 id / name,並將 workflow.config 套入節點(componentId + data)
|
||||
const rawGraph = data.cypher as { nodes: Array<{ id: string; componentId?: string; data?: Record<string, unknown> }>; edges: unknown[] };
|
||||
|
||||
@@ -5,12 +5,13 @@ import { graphSchema } from '../lib/schemas';
|
||||
import { createComponentLoader } from '../lib/component-loader';
|
||||
import { writeEvaluation, updateComponentStats } from './execution-evaluator';
|
||||
import { parseTriplets } from './triplet-parser';
|
||||
import { searchNodes } from './search-nodes';
|
||||
import { searchNodes, type SearchMode } from './search-nodes';
|
||||
import { buildExecutionGraph } from './graph-builder';
|
||||
|
||||
export async function handleCypherSearch(
|
||||
triplets: unknown[],
|
||||
env: Bindings,
|
||||
mode: SearchMode = 'discover',
|
||||
): Promise<{ nodes: Record<string, unknown>; cypher: unknown; missing: string[] }> {
|
||||
const parsed = parseTriplets(triplets);
|
||||
if (!parsed) {
|
||||
@@ -19,7 +20,12 @@ export async function handleCypherSearch(
|
||||
|
||||
// 2026-07-30:查 registry 判真實存在(workflow-discovery)。
|
||||
// `missing` 以前寫死 [],等於告訴 AI「什麼都有」——那是「腹語術」的入口。
|
||||
const { nodeResults, missingNodes } = await searchNodes(parsed, undefined, env);
|
||||
//
|
||||
// t158(07-31 迴歸修復,leo:「這裡只是複製一些工作流的 data 過去,沒有要在這裡驗證」):
|
||||
// 誠實化只屬於 **discover**(AI 問「有沒有」);**compile**(部署/推送的複製路徑)
|
||||
// 純編圖零查詢——那本來就是既有設計(workflows.json=打包期預編的搬運),
|
||||
// 5cadc60 起誠實化漏進複製路徑=迴歸(冷實例 8 節點 25.7s、安裝器 timeout 炸)。
|
||||
const { nodeResults, missingNodes } = await searchNodes(parsed, undefined, env, mode);
|
||||
|
||||
const graph = buildExecutionGraph(parsed, nodeResults, 'cypher-search-result', 'Cypher Search Result');
|
||||
return { nodes: nodeResults, cypher: { nodes: graph.nodes, edges: graph.edges }, missing: missingNodes };
|
||||
@@ -52,7 +58,9 @@ export async function handleCypherExecute(
|
||||
throw new Error('無法解析任何節點');
|
||||
}
|
||||
|
||||
const { nodeResults } = await searchNodes(parsed, config, env);
|
||||
// t158:執行路徑=compile(零 discovery round-trip)——存在性由 component-loader
|
||||
// 在載入該節點時決定(原本的權威),查詢層不重複驗。
|
||||
const { nodeResults } = await searchNodes(parsed, config, env, 'compile');
|
||||
|
||||
const graph = buildExecutionGraph(parsed, nodeResults, graphId, graphName, config);
|
||||
const parseResult = graphSchema.safeParse(graph);
|
||||
|
||||
@@ -8,7 +8,8 @@ import type { RecipeDefinition } from '../routes/recipes';
|
||||
* `not_found` 而非 `missing`:欄位契約以頂層機械考
|
||||
* `system-dev/docs/3-specs/arcrun-usable/verify.sh` 為準(01 組 grep `not_found`)。
|
||||
*/
|
||||
export type NodeStatus = 'found' | 'not_found' | 'unknown';
|
||||
/** `unchecked`=compile 模式的誠實標記:沒查、不知道有沒有(≠found 的假信號)。 */
|
||||
export type NodeStatus = 'found' | 'not_found' | 'unknown' | 'unchecked';
|
||||
|
||||
export type NodeInfo = {
|
||||
status: NodeStatus;
|
||||
@@ -40,6 +41,18 @@ export type SearchResult = {
|
||||
missingNodes: string[];
|
||||
};
|
||||
|
||||
/**
|
||||
* t158(leo 07-31 定調「部署≠發現」):
|
||||
* 「這裡只是複製一些工作流的 data 過去,沒有要在這裡驗證,難怪這麼慢。
|
||||
* 就算是我自己寫了錯的工作流,也可以跑跑看,如果錯誤就修改,
|
||||
* 沒有說有錯誤還要一個個驗證這回事。」
|
||||
* - `compile`=純編圖:**零外部查詢**(不打 registry、不掃 recipe、不算相似度、不擋 missing)。
|
||||
* 部署/推送/執行路徑用——寫錯的 workflow 照樣部署,錯在執行時現形。
|
||||
* - `discover`=誠實查詢(預設,`/cypher/search` 的既有契約):AI 問「有沒有」時用,
|
||||
* not_found+分型指路+相似候選全保留。
|
||||
*/
|
||||
export type SearchMode = 'discover' | 'compile';
|
||||
|
||||
/** searchNodes 需要的環境子集(cypher-handlers 傳整份 Bindings 進來也相容)。 */
|
||||
export type SearchNodesEnv = {
|
||||
WORKER_SUBDOMAIN?: string;
|
||||
@@ -79,13 +92,46 @@ export async function searchNodes(
|
||||
parsed: ParsedTriplets,
|
||||
config?: Record<string, Record<string, unknown>>,
|
||||
env?: SearchNodesEnv,
|
||||
mode: SearchMode = 'discover',
|
||||
): Promise<SearchResult> {
|
||||
const nodeResults: Record<string, NodeInfo> = {};
|
||||
const missingNodes: string[] = [];
|
||||
|
||||
// ── compile:純編圖,零外部查詢(t158,部署≠發現)─────────────────────────
|
||||
if (mode === 'compile') {
|
||||
for (const nodeName of parsed.nodeNames) {
|
||||
const role = resolveNodeRole(nodeName, parsed);
|
||||
if ((role === 'Input' || role === 'Output') && isVirtualIoName(nodeName)) {
|
||||
nodeResults[nodeName] = { status: 'found', componentId: nodeName.toLowerCase(), type: role };
|
||||
continue;
|
||||
}
|
||||
const configComponent = config?.[nodeName]?.component as string | undefined;
|
||||
// unchecked=誠實「沒查」;存在性由 component-loader 在執行時決定
|
||||
nodeResults[nodeName] = {
|
||||
status: configComponent ? 'found' : 'unchecked',
|
||||
componentId: configComponent ?? nodeName,
|
||||
type: role,
|
||||
};
|
||||
}
|
||||
return { nodeResults, missingNodes };
|
||||
}
|
||||
|
||||
const sub = env?.WORKER_SUBDOMAIN;
|
||||
const registryBase = env?.REGISTRY_BASE_URL ?? (sub ? wasmWorkerUrl('registry', sub) : undefined);
|
||||
|
||||
// ── discover 批次化(t158):兩庫各抓**一次**,之後全在記憶體內比對。────────
|
||||
// 病史(07-31 stage 實測):舊版對每個 missing 節點各打「1 次逐顆查+最多 9 次
|
||||
// 相似搜尋+一輪 recipe KV 掃描」⇒ 冷實例 8 節點 /cypher/search 25.7s,
|
||||
// 安裝器 15s timeout 必炸。批次化後每 request 固定 1 次 catalog+1 次 recipe 清單。
|
||||
const catalog = registryBase ? await fetchCatalog(registryBase) : { status: 'unreachable' as const, entries: [] };
|
||||
const recipes = env?.RECIPES ? await listAllRecipes(env.RECIPES) : [];
|
||||
const byId = new Map<string, CatalogFullRecord>();
|
||||
for (const e of catalog.entries) {
|
||||
const prev = byId.get(e.canonical_id);
|
||||
if (!prev || (e.score ?? 0) > (prev.score ?? 0)) byId.set(e.canonical_id, e);
|
||||
for (const a of e.aliases ?? []) if (!byId.has(a)) byId.set(a, e);
|
||||
}
|
||||
|
||||
for (const nodeName of parsed.nodeNames) {
|
||||
const role = resolveNodeRole(nodeName, parsed);
|
||||
|
||||
@@ -107,34 +153,37 @@ export async function searchNodes(
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!registryBase) {
|
||||
// registry 完全查不通(未部署/網路失敗)⇒ 誠實回 unknown。
|
||||
// **不能誤判 not_found**——那會讓 AI 以為零件不存在而重寫 code,正是要避免的事。
|
||||
// 舊 registry 沒有 /catalog 端點(no_endpoint)→ 退回逐顆查(相容路徑)。
|
||||
if (catalog.status === 'unreachable') {
|
||||
nodeResults[nodeName] = { status: 'unknown', componentId, type: role };
|
||||
continue;
|
||||
}
|
||||
if (catalog.status === 'no_endpoint') {
|
||||
const legacy = await legacyPerNodeLookup(registryBase!, componentId, nodeName, role, env, recipes);
|
||||
nodeResults[nodeName] = legacy.info;
|
||||
if (legacy.missing) missingNodes.push(nodeName);
|
||||
continue;
|
||||
}
|
||||
|
||||
// ── 第一庫:零件 registry ────────────────────────────────────────────────
|
||||
const q = await fetchComponent(registryBase, componentId);
|
||||
if (!q.ok) {
|
||||
// registry 查不通(未部署/網路失敗)⇒ 誠實回 unknown。
|
||||
// **不能誤判 not_found**——那會讓 AI 以為零件不存在而重寫 code,正是要避免的事。
|
||||
nodeResults[nodeName] = { status: 'unknown', componentId, type: role };
|
||||
continue;
|
||||
}
|
||||
if (q.entry) {
|
||||
// ── 第一庫:零件 catalog(記憶體)────────────────────────────────────────
|
||||
const hit = byId.get(componentId);
|
||||
if (hit) {
|
||||
nodeResults[nodeName] = {
|
||||
status: 'found',
|
||||
componentId,
|
||||
type: role,
|
||||
source: 'component',
|
||||
input_schema: q.entry.input_schema,
|
||||
success_rate: q.entry.success_rate,
|
||||
stability: q.entry.stability,
|
||||
input_schema: hit.input_schema,
|
||||
success_rate: typeof hit.success_rate === 'number' ? hit.success_rate : undefined,
|
||||
stability: typeof hit.stability === 'string' ? hit.stability : undefined,
|
||||
};
|
||||
continue;
|
||||
}
|
||||
|
||||
// ── 第二庫:recipe 庫(task 3.6——「說不出某 recipe 沒有」的病灶就在漏了這步)──
|
||||
const recipe = env?.RECIPES ? await resolveRecipe(componentId, env.RECIPES) : null;
|
||||
// ── 第二庫:recipe 清單(記憶體;canonical_id 精確比對)──────────────────
|
||||
const recipe = recipes.find(r => r.canonical_id === componentId);
|
||||
if (recipe) {
|
||||
nodeResults[nodeName] = {
|
||||
status: 'found',
|
||||
@@ -147,11 +196,9 @@ export async function searchNodes(
|
||||
continue;
|
||||
}
|
||||
|
||||
// ── 兩庫都沒有 ⇒ not_found + 分型指路(task 3.7)+ 相近候選 ────────────
|
||||
const [similarComponents, similarRecipes] = await Promise.all([
|
||||
searchSimilarComponents(registryBase, nodeName),
|
||||
env?.RECIPES ? searchSimilarRecipes(env.RECIPES, nodeName) : Promise.resolve([]),
|
||||
]);
|
||||
// ── 兩庫都沒有 ⇒ not_found + 分型指路(task 3.7)+ 相近候選(全記憶體)──
|
||||
const similarComponents = similarFromCatalog(catalog.entries, nodeName);
|
||||
const similarRecipes = similarFromRecipes(recipes, nodeName);
|
||||
|
||||
nodeResults[nodeName] = {
|
||||
status: 'not_found',
|
||||
@@ -167,6 +214,126 @@ export async function searchNodes(
|
||||
return { nodeResults, missingNodes };
|
||||
}
|
||||
|
||||
// ── t158 批次化 helpers ────────────────────────────────────────────────────────
|
||||
|
||||
type CatalogFullRecord = {
|
||||
canonical_id: string;
|
||||
display_name?: string;
|
||||
description?: string;
|
||||
aliases?: string[];
|
||||
tags?: string[];
|
||||
score?: number;
|
||||
input_schema?: unknown;
|
||||
success_rate?: number;
|
||||
stability?: string;
|
||||
};
|
||||
|
||||
type CatalogFetch = { status: 'ok' | 'no_endpoint' | 'unreachable'; entries: CatalogFullRecord[] };
|
||||
|
||||
/** 一次抓 registry 全目錄。404=舊版 registry 沒這端點 → 呼叫端退回逐顆查。 */
|
||||
async function fetchCatalog(registryBase: string): Promise<CatalogFetch> {
|
||||
try {
|
||||
const res = await fetch(`${registryBase}/components/catalog`, { signal: AbortSignal.timeout(10000) });
|
||||
if (res.status === 404) return { status: 'no_endpoint', entries: [] };
|
||||
if (!res.ok) return { status: 'unreachable', entries: [] };
|
||||
const body = (await res.json()) as { data?: { components?: CatalogFullRecord[] } };
|
||||
return { status: 'ok', entries: body.data?.components ?? [] };
|
||||
} catch {
|
||||
return { status: 'unreachable', entries: [] };
|
||||
}
|
||||
}
|
||||
|
||||
/** 一次抓 recipe 全清單(本部署 recipe 數量小;exact 與相似度共用同一份)。 */
|
||||
async function listAllRecipes(kv: KVNamespace): Promise<RecipeDefinition[]> {
|
||||
try {
|
||||
const list = await kv.list({ prefix: 'recipe:' });
|
||||
return (await Promise.all(
|
||||
list.keys.map(k => kv.get(k.name, 'json') as Promise<RecipeDefinition | null>),
|
||||
)).filter(Boolean) as RecipeDefinition[];
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/** 相似零件(記憶體版):全名 substring 優先,否則斷詞計數 top3——判準與舊 HTTP 版一致。 */
|
||||
function similarFromCatalog(entries: CatalogFullRecord[], nodeName: string): string[] {
|
||||
const searchableOf = (e: CatalogFullRecord) =>
|
||||
[e.canonical_id, e.display_name ?? '', e.description ?? '', ...(e.aliases ?? []), ...(e.tags ?? [])]
|
||||
.join(' ').toLowerCase();
|
||||
const full = nodeName.toLowerCase();
|
||||
const direct = entries.filter(e => searchableOf(e).includes(full)).map(e => e.canonical_id);
|
||||
if (direct.length > 0) return [...new Set(direct)].slice(0, 3);
|
||||
|
||||
const tokens = extractTokens(nodeName);
|
||||
if (tokens.length === 0) return [];
|
||||
const count = new Map<string, number>();
|
||||
for (const e of entries) {
|
||||
const hay = searchableOf(e);
|
||||
const hits = tokens.filter(t => hay.includes(t)).length;
|
||||
if (hits > 0) count.set(e.canonical_id, Math.max(count.get(e.canonical_id) ?? 0, hits));
|
||||
}
|
||||
return [...count.entries()].sort((a, b) => b[1] - a[1]).slice(0, 3).map(([id]) => id);
|
||||
}
|
||||
|
||||
/** 相似 recipe(記憶體版;判準沿用 searchSimilarRecipes)。 */
|
||||
function similarFromRecipes(recipes: RecipeDefinition[], nodeName: string): string[] {
|
||||
const tokens = [nodeName.toLowerCase(), ...extractTokens(nodeName)];
|
||||
const seen = new Set<string>();
|
||||
const matched: string[] = [];
|
||||
for (const r of recipes) {
|
||||
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);
|
||||
}
|
||||
|
||||
/** 舊 registry(無 /catalog 端點)的相容路徑:維持逐顆查語義。 */
|
||||
async function legacyPerNodeLookup(
|
||||
registryBase: string,
|
||||
componentId: string,
|
||||
nodeName: string,
|
||||
role: NodeRole,
|
||||
env: SearchNodesEnv | undefined,
|
||||
recipes: RecipeDefinition[],
|
||||
): Promise<{ info: NodeInfo; missing: boolean }> {
|
||||
const q = await fetchComponent(registryBase, componentId);
|
||||
if (!q.ok) return { info: { status: 'unknown', componentId, type: role }, missing: false };
|
||||
if (q.entry) {
|
||||
return {
|
||||
info: {
|
||||
status: 'found', componentId, type: role, source: 'component',
|
||||
input_schema: q.entry.input_schema, success_rate: q.entry.success_rate, stability: q.entry.stability,
|
||||
},
|
||||
missing: false,
|
||||
};
|
||||
}
|
||||
const recipe = recipes.find(r => r.canonical_id === componentId)
|
||||
?? (env?.RECIPES ? await resolveRecipe(componentId, env.RECIPES) : null);
|
||||
if (recipe) {
|
||||
return {
|
||||
info: {
|
||||
status: 'found', componentId: recipe.canonical_id, type: role, source: 'recipe',
|
||||
description: recipe.description, endpoint: recipe.endpoint,
|
||||
},
|
||||
missing: false,
|
||||
};
|
||||
}
|
||||
const similarComponents = await searchSimilarComponents(registryBase, nodeName);
|
||||
const similarRecipes = similarFromRecipes(recipes, nodeName);
|
||||
return {
|
||||
info: {
|
||||
status: 'not_found', componentId, type: role, suggestion: buildSuggestion(componentId),
|
||||
...(similarComponents.length > 0 ? { similar_components: similarComponents } : {}),
|
||||
...(similarRecipes.length > 0 ? { similar_recipes: similarRecipes } : {}),
|
||||
},
|
||||
missing: true,
|
||||
};
|
||||
}
|
||||
|
||||
// ── 缺件分型(task 3.7)────────────────────────────────────────────────────────
|
||||
//
|
||||
// 分型判準(刻意用簡單可解釋的規則,不接 LLM——查詢端點要快、要可預測):
|
||||
|
||||
@@ -6,19 +6,23 @@ export const cypherRouter = new Hono<{ Bindings: Bindings }>();
|
||||
|
||||
// POST /cypher/search — 三元組 → 解析節點 → 語意搜尋零件 → 回傳 Cypher JSON (開發友善格式)
|
||||
cypherRouter.post('/cypher/search', async (c) => {
|
||||
const body = await c.req.json() as { triplets?: unknown };
|
||||
const body = await c.req.json() as { triplets?: unknown; mode?: unknown };
|
||||
const rawTriplets = body?.triplets;
|
||||
|
||||
if (!Array.isArray(rawTriplets) || rawTriplets.length === 0) {
|
||||
return c.json({ error: 'triplets 必須為非空字串陣列' }, 400);
|
||||
}
|
||||
|
||||
// t158「部署≠發現」:mode=compile=純編圖(安裝器/acr push 的複製路徑,零存在性查詢);
|
||||
// 預設 discover=誠實查詢(AI 問「有沒有」的既有契約,not_found+指路照舊)。
|
||||
const mode = body?.mode === 'compile' ? 'compile' : 'discover';
|
||||
|
||||
try {
|
||||
const now = new Date();
|
||||
const timestamp = now.toISOString();
|
||||
const versionId = `search-v1-${now.getFullYear()}${String(now.getMonth() + 1).padStart(2, '0')}${String(now.getDate()).padStart(2, '0')}-${String(now.getHours()).padStart(2, '0')}${String(now.getMinutes()).padStart(2, '0')}${String(now.getSeconds()).padStart(2, '0')}`;
|
||||
|
||||
const result = await handleCypherSearch(rawTriplets, c.env);
|
||||
const result = await handleCypherSearch(rawTriplets, c.env, mode);
|
||||
|
||||
const response = {
|
||||
version: versionId,
|
||||
|
||||
@@ -145,7 +145,7 @@ function computeScore(v: Record<string, unknown>): number {
|
||||
return successRate * speedScore * Math.log(callCount + 2);
|
||||
}
|
||||
|
||||
function toComponentRecord(v: Record<string, unknown>): ComponentRecord {
|
||||
export function toComponentRecord(v: Record<string, unknown>): ComponentRecord {
|
||||
return {
|
||||
component_hash_id: String(v.component_hash_id ?? ''),
|
||||
canonical_id: String(v.canonical_id ?? ''),
|
||||
|
||||
@@ -5,10 +5,34 @@
|
||||
|
||||
import { Hono } from 'hono';
|
||||
import type { Bindings } from '../types';
|
||||
import { getComponent, getComponentVersions, searchComponents } from '../actions/queryComponents';
|
||||
import { getComponent, getComponentVersions, searchComponents, toComponentRecord } from '../actions/queryComponents';
|
||||
import type { ComponentRecord } from '../actions/queryComponents';
|
||||
|
||||
const app = new Hono<{ Bindings: Bindings }>();
|
||||
|
||||
// 全清單(t158 批次化):/cypher/search discover 一次抓走整份目錄,
|
||||
// 節點存在判定+相似度全在 cypher 記憶體內比對——取代「每個 missing 節點
|
||||
// 各打 1+8 次查詢」的疊爆模式(冷實例 8 節點實測 25.7s 的病根)。
|
||||
// 也補上 CP2-B 記載的「registry 沒有列表端點」缺口。
|
||||
// 必須在 /:id 之前,避免 "catalog" 被當作 id。
|
||||
app.get('/catalog', async c => {
|
||||
const list = await c.env.SUBMISSIONS_KV.list({ prefix: 'comp:' });
|
||||
const seen = new Set<string>();
|
||||
const components: ComponentRecord[] = [];
|
||||
for (const key of list.keys) {
|
||||
const raw = await c.env.SUBMISSIONS_KV.get(key.name);
|
||||
if (!raw) continue;
|
||||
let v: Record<string, unknown>;
|
||||
try { v = JSON.parse(raw) as Record<string, unknown>; } catch { continue; }
|
||||
if (v.status === 'tombstone' || v.visibility !== 'public') continue;
|
||||
const dedup = `${String(v.component_hash_id ?? '')}:${String(v.version ?? '')}`;
|
||||
if (seen.has(dedup)) continue;
|
||||
seen.add(dedup);
|
||||
components.push(toComponentRecord(v));
|
||||
}
|
||||
return c.json({ success: true, data: { components, count: components.length } });
|
||||
});
|
||||
|
||||
// 語意搜尋(必須在 /:id 之前,避免 "search" 被當作 id)
|
||||
app.get('/search', async c => {
|
||||
const q = c.req.query('q');
|
||||
|
||||
Reference in New Issue
Block a user