922a57fe34
Self-hosted 開源:WASM 零件 + recipe + cypher-executor,跑在你自己的 Cloudflare。 此為重建的乾淨歷史起點(移除曾誤 commit 的 GCP SA 金鑰,舊歷史保留在 richblack/arcrun 與本地 backup 分支)。含: - acr init --self-hosted installer(建 KV/R2 + codeload 拉預編譯 wasm + wrangler deploy + seed recipe) - recipe push 把關(資料外流提醒 + 打通檢查) - 19 個正當零件預編譯 wasm(claude_api/km_writer/kbdb_upsert_block 排除:違反 DECISIONS §1) - CLI / cypher-executor / registry / 完整 SDD Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
163 lines
5.8 KiB
TypeScript
163 lines
5.8 KiB
TypeScript
// 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<string | null> {
|
||
// 已經是 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<ComponentRecord[]> {
|
||
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<ComponentRecord | null> {
|
||
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<ComponentRecord[]> {
|
||
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<ComponentRecord[]> {
|
||
const q = query.toLowerCase();
|
||
|
||
// 列出所有 comp: 前綴的 key(只取最新一頁,最多 1000 個)
|
||
const list = await env.SUBMISSIONS_KV.list({ prefix: 'comp:' });
|
||
|
||
const seen = new Set<string>(); // 每個 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<string, unknown>;
|
||
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<string, unknown>): 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<string, unknown>): 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),
|
||
};
|
||
}
|