Files
Arcrun/registry/src/actions/queryComponents.ts
T
uncle6me-web 922a57fe34 arcrun — AI workflow execution engine (clean history)
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>
2026-06-03 15:52:38 +08:00

163 lines
5.8 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// queryComponents — 查詢零件合約
// 支援兩種查詢 id
// component_hash_idcmp_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),
};
}