// queryComponents — 查詢零件合約 // 支援兩種查詢 id: // component_hash_id(cmp_xxxxxxxx)— 永久穩定,workflow 引用用 // canonical_id(小寫底線) — 可讀名稱,透過 idx: 反查索引解析 // Requirements: 12.2, 12.3 import type { Bindings } from '../types'; export interface ComponentRecord { component_hash_id: string; canonical_id: string; display_name: string; version: string; category: string; stability: string; status: string; description: string; aliases: string[]; tags: string[]; success_rate: number; avg_duration_ms: number; call_count: number; wasm_r2_key?: string; score: number; } // ── id 解析:支援 hash_id 和 canonical_id 兩種格式 ────────────────────────── async function resolveHashId(id: string, env: Bindings): Promise { // 已經是 hash_id 格式 if (id.startsWith('cmp_')) return id; // canonical_id → 透過 idx: 反查索引 const hashId = await env.SUBMISSIONS_KV.get(`idx:${id}`); return hashId; } // ── 取得零件的所有版本 ──────────────────────────────────────────────────────── async function listVersions(hashId: string, env: Bindings): Promise { const prefix = `comp:${hashId}:`; const list = await env.SUBMISSIONS_KV.list({ prefix }); const records: ComponentRecord[] = []; for (const key of list.keys) { const raw = await env.SUBMISSIONS_KV.get(key.name); if (!raw) continue; try { const v = JSON.parse(raw); if (v.status === 'tombstone') continue; records.push(toComponentRecord(v)); } catch { continue; } } return records; } // ── 公開 API ────────────────────────────────────────────────────────────────── /** 取得零件最優版本(floating 策略:成功率 × 速度 × log(使用次數)) */ export async function getComponent( id: string, env: Bindings, ): Promise { const hashId = await resolveHashId(id, env); if (!hashId) return null; const versions = await listVersions(hashId, env); if (versions.length === 0) return null; versions.sort((a, b) => b.score - a.score); return versions[0]; } /** 取得零件所有版本清單(含評分排序) */ export async function getComponentVersions( id: string, env: Bindings, ): Promise { const hashId = await resolveHashId(id, env); if (!hashId) return []; const versions = await listVersions(hashId, env); versions.sort((a, b) => b.score - a.score); return versions.slice(0, 10); } /** 關鍵字搜尋(掃描 KV prefix comp:,比對 canonical_id / display_name / description / aliases) * * 注意:這是 Phase 0 的純文字比對版本。 * Phase 2 接入 Cloudflare Vectorize 後改為語意搜尋,API 介面不變。 */ export async function searchComponents( query: string, env: Bindings, ): Promise { const q = query.toLowerCase(); // 列出所有 comp: 前綴的 key(只取最新一頁,最多 1000 個) const list = await env.SUBMISSIONS_KV.list({ prefix: 'comp:' }); const seen = new Set(); // 每個 hash_id 只取最優版本 const candidates: ComponentRecord[] = []; for (const key of list.keys) { const raw = await env.SUBMISSIONS_KV.get(key.name); if (!raw) continue; let v: Record; try { v = JSON.parse(raw); } catch { continue; } if (v.status === 'tombstone' || v.visibility !== 'public') continue; // 比對:canonical_id / display_name / description / aliases const searchable = [ String(v.canonical_id ?? ''), String(v.display_name ?? ''), String(v.description ?? ''), ...(Array.isArray(v.aliases) ? v.aliases.map(String) : []), ...(Array.isArray(v.tags) ? v.tags.map(String) : []), ].join(' ').toLowerCase(); if (!searchable.includes(q)) continue; const hashId = String(v.component_hash_id ?? ''); if (seen.has(`${hashId}:${v.version}`)) continue; seen.add(`${hashId}:${v.version}`); candidates.push(toComponentRecord(v)); } candidates.sort((a, b) => b.score - a.score); return candidates.slice(0, 10); } // ── 內部工具函數 ────────────────────────────────────────────────────────────── function computeScore(v: Record): number { const successRate = parseFloat(String(v.success_rate ?? '1')); const avgDuration = parseFloat(String(v.avg_duration_ms ?? '10')); const callCount = parseInt(String(v.call_count ?? '0'), 10); const speedScore = Math.max(0, 1 - avgDuration / 1000); return successRate * speedScore * Math.log(callCount + 2); } function toComponentRecord(v: Record): ComponentRecord { return { component_hash_id: String(v.component_hash_id ?? ''), canonical_id: String(v.canonical_id ?? ''), display_name: String(v.display_name ?? ''), version: String(v.version ?? 'v1'), category: String(v.category ?? 'logic'), stability: String(v.stability ?? 'floating'), status: String(v.status ?? 'active'), description: String(v.description ?? ''), aliases: Array.isArray(v.aliases) ? v.aliases.map(String) : [], tags: Array.isArray(v.tags) ? v.tags.map(String) : [], success_rate: parseFloat(String(v.success_rate ?? '1')), avg_duration_ms: parseFloat(String(v.avg_duration_ms ?? '0')), 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), }; }