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>
148 lines
5.6 KiB
TypeScript
148 lines
5.6 KiB
TypeScript
// POST /components — 零件提交端點(沙盒驗收流程)
|
||
// POST /components/index-only — metadata-only 索引(無 wasm、無沙盒,給 backfill 用)
|
||
// Requirements: 2.1, 2.2, 2.3
|
||
// SDD: matrix/arcrun/.agents/specs/component-registry-canon/design.md
|
||
|
||
import { Hono } from 'hono';
|
||
import type { Bindings } from '../types';
|
||
import { validateContract } from '../actions/validateContract';
|
||
import { submitComponent } from '../actions/submitComponent';
|
||
import type { SubmitOptions } from '../actions/submitComponent';
|
||
import { indexOnlyComponent } from '../actions/indexOnlyComponent';
|
||
|
||
const app = new Hono<{ Bindings: Bindings }>();
|
||
|
||
app.post('/', async c => {
|
||
// 接受 multipart/form-data:contract(JSON 字串)+ wasm(binary)
|
||
let contract: unknown;
|
||
let wasmBytes: Uint8Array;
|
||
// G0/G4:人類確認 + 舉證 + 本地 Gherkin evidence(multipart 或 JSON 皆可帶)
|
||
const submitOptions: SubmitOptions = {};
|
||
|
||
const contentType = c.req.header('content-type') ?? '';
|
||
|
||
if (contentType.includes('multipart/form-data')) {
|
||
const formData = await c.req.formData();
|
||
const contractStr = formData.get('contract');
|
||
const wasmFile = formData.get('wasm');
|
||
|
||
if (!contractStr || typeof contractStr !== 'string') {
|
||
return c.json({ success: false, error: '缺少 contract 欄位' }, 400);
|
||
}
|
||
if (!wasmFile || !(wasmFile instanceof File)) {
|
||
return c.json({ success: false, error: '缺少 wasm 欄位' }, 400);
|
||
}
|
||
|
||
try {
|
||
contract = JSON.parse(contractStr);
|
||
} catch {
|
||
return c.json({ success: false, error: 'contract 必須為合法 JSON' }, 400);
|
||
}
|
||
|
||
wasmBytes = new Uint8Array(await wasmFile.arrayBuffer());
|
||
|
||
const hc = formData.get('human_confirmation');
|
||
if (typeof hc === 'string') { try { submitOptions.human_confirmation = JSON.parse(hc); } catch { /* ignore */ } }
|
||
const ge = formData.get('gherkin_evidence');
|
||
if (typeof ge === 'string') { try { submitOptions.gherkin_evidence = JSON.parse(ge); } catch { /* ignore */ } }
|
||
if (formData.get('skip_acceptance') === 'true') submitOptions.skip_acceptance = true;
|
||
} else {
|
||
// 也支援純 JSON(用於測試,wasm 以 base64 傳入)
|
||
let body: Record<string, unknown>;
|
||
try {
|
||
body = await c.req.json();
|
||
} catch {
|
||
return c.json({ success: false, error: 'request body 必須為 multipart/form-data 或 JSON' }, 400);
|
||
}
|
||
|
||
contract = body.contract;
|
||
const wasmBase64 = body.wasm_base64;
|
||
|
||
if (!contract) {
|
||
return c.json({ success: false, error: '缺少 contract 欄位' }, 400);
|
||
}
|
||
if (!wasmBase64 || typeof wasmBase64 !== 'string') {
|
||
return c.json({ success: false, error: '缺少 wasm_base64 欄位' }, 400);
|
||
}
|
||
|
||
// base64 decode
|
||
const binaryStr = atob(wasmBase64);
|
||
wasmBytes = new Uint8Array(binaryStr.length);
|
||
for (let i = 0; i < binaryStr.length; i++) {
|
||
wasmBytes[i] = binaryStr.charCodeAt(i);
|
||
}
|
||
|
||
if (body.human_confirmation) submitOptions.human_confirmation = body.human_confirmation as SubmitOptions['human_confirmation'];
|
||
if (body.gherkin_evidence) submitOptions.gherkin_evidence = body.gherkin_evidence as SubmitOptions['gherkin_evidence'];
|
||
if (body.skip_acceptance === true) submitOptions.skip_acceptance = true;
|
||
}
|
||
|
||
// 驗證 contract 格式
|
||
const validation = validateContract(contract);
|
||
if (!validation.valid) {
|
||
return c.json({
|
||
success: false,
|
||
failed_step: 'contract_validation',
|
||
reason: `合約格式驗證失敗:${validation.errors.join(', ')}`,
|
||
missing_fields: validation.missing_fields,
|
||
guide_anchor: '#contract-example',
|
||
}, 422);
|
||
}
|
||
|
||
// 執行沙盒驗收(含 G0 人類閘門)+ 寫入 metadata
|
||
const result = await submitComponent(wasmBytes, validation.contract!, c.env, submitOptions);
|
||
|
||
if (!result.success) {
|
||
return c.json(result, 422);
|
||
}
|
||
|
||
return c.json(result, 201);
|
||
});
|
||
|
||
// POST /components/index-only — metadata-only 索引(給 backfill 用)
|
||
// 只接 contract(JSON),不收 wasm bytes、不沙盒驗收
|
||
// 用途:cypher-executor 已不從 R2 動態載 wasm(零件用獨立 Worker URL),
|
||
// 故已部署但未索引的零件,只要把 metadata 寫進 KV 讓 search 找得到即可。
|
||
app.post('/index-only', async c => {
|
||
let body: Record<string, unknown>;
|
||
try {
|
||
body = await c.req.json();
|
||
} catch {
|
||
return c.json({ success: false, error: 'request body 必須為 JSON' }, 400);
|
||
}
|
||
|
||
const contract = body.contract;
|
||
if (!contract) {
|
||
return c.json({ success: false, error: '缺少 contract 欄位' }, 400);
|
||
}
|
||
|
||
// index-only 是 backfill 用,比 submit 寬鬆:
|
||
// 既有零件可能沒 gherkin_tests / 沒 description / aliases — 補預設讓索引能進
|
||
if (typeof contract === 'object' && contract !== null) {
|
||
const c2 = contract as Record<string, unknown>;
|
||
if (!c2.gherkin_tests || (Array.isArray(c2.gherkin_tests) && c2.gherkin_tests.length < 2)) {
|
||
c2.gherkin_tests = [
|
||
{ scenario: 'placeholder happy', given: '{}', then_contains: '{' },
|
||
{ scenario: 'placeholder fail', given: '{}', then_contains: '}' },
|
||
];
|
||
}
|
||
if (!c2.description) c2.description = '';
|
||
if (!c2.tags) c2.tags = [];
|
||
}
|
||
|
||
const validation = validateContract(contract);
|
||
if (!validation.valid) {
|
||
return c.json({
|
||
success: false,
|
||
failed_step: 'contract_validation',
|
||
reason: `合約格式驗證失敗:${validation.errors.join(', ')}`,
|
||
missing_fields: validation.missing_fields,
|
||
}, 422);
|
||
}
|
||
|
||
const result = await indexOnlyComponent(validation.contract!, c.env);
|
||
return c.json(result, result.already_indexed ? 200 : 201);
|
||
});
|
||
|
||
export default app;
|