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>
124 lines
4.0 KiB
TypeScript
124 lines
4.0 KiB
TypeScript
/**
|
|
* Implicit telemetry — 對應 SDD .agents/specs/llm-interface/ M1.2
|
|
*
|
|
* 每次 deploy / run / validate 失敗,cypher-executor 自動寫 KBDB block
|
|
* type=agent-telemetry,含 event_type / workflow_name / error_code /
|
|
* duration_ms / api_key_hash / agent_user_agent。
|
|
*
|
|
* 隱私:api_key SHA-256 截 16 字元(不可逆,可聚合),workflow 內容不 log。
|
|
*
|
|
* 設計:不阻擋主流程,fetch fire-and-forget;錯誤只 console.warn 不 throw。
|
|
*
|
|
* 注意:本 module 屬 orchestrator 自身能力(觀測自己),不違反「業務邏輯走 WASM」鐵律。
|
|
* 跟 trigger_workflow / scheduled() 同類,是 cypher-executor 自我管理的一部分。
|
|
*/
|
|
|
|
import type { Bindings, ExecutionContext } from '../types';
|
|
|
|
export type TelemetryEvent =
|
|
| 'deploy_success'
|
|
| 'deploy_fail'
|
|
| 'run_success'
|
|
| 'run_fail'
|
|
| 'validation_error'
|
|
| 'mcp_tool_call'
|
|
| 'node_success' // 單一 node 跑完(給 step-level 效能分析用)
|
|
| 'node_failure'; // 單一 node 失敗
|
|
|
|
export interface TelemetryRecord {
|
|
event_type: TelemetryEvent;
|
|
workflow_name?: string;
|
|
component_id?: string;
|
|
error_code?: string;
|
|
duration_ms: number;
|
|
api_key_hash: string;
|
|
agent_user_agent?: string;
|
|
}
|
|
|
|
/**
|
|
* api_key → SHA-256 hex 截前 16 字元
|
|
* 不可逆,可用來聚合(同一用戶不同 event 統計),不會洩漏原 key
|
|
*/
|
|
export async function hashApiKey(apiKey: string): Promise<string> {
|
|
if (!apiKey) return 'anon';
|
|
const encoder = new TextEncoder();
|
|
const data = encoder.encode(apiKey);
|
|
const hashBuffer = await crypto.subtle.digest('SHA-256', data);
|
|
const hashArray = Array.from(new Uint8Array(hashBuffer));
|
|
return hashArray
|
|
.slice(0, 8) // 8 bytes = 16 hex chars
|
|
.map(b => b.toString(16).padStart(2, '0'))
|
|
.join('');
|
|
}
|
|
|
|
/**
|
|
* KBDB upsert URL(內部走 workers.dev 避同 zone 自循環)
|
|
* 對應 .claude/rules/03-component-architecture.md
|
|
*/
|
|
function kbdbCreateBlockUrl(env: Bindings): string {
|
|
const subdomain = env.WORKER_SUBDOMAIN || 'uncle6-me';
|
|
return `https://arcrun-kbdb-create-block.${subdomain}.workers.dev`;
|
|
}
|
|
|
|
/**
|
|
* 寫一筆 telemetry block 到 KBDB。fire-and-forget。
|
|
*
|
|
* 寫不進去也不擋主流程 —— 平台自己的觀測絕不能讓 user-facing 流程失敗。
|
|
*
|
|
* 用 ctx.waitUntil 確保即使主 request 已回,背景仍會跑完。
|
|
*/
|
|
export function recordTelemetry(
|
|
env: Bindings,
|
|
apiKey: string | undefined,
|
|
record: Omit<TelemetryRecord, 'api_key_hash'>,
|
|
ctx?: ExecutionContext,
|
|
): void {
|
|
const promise = (async () => {
|
|
try {
|
|
const api_key_hash = await hashApiKey(apiKey ?? '');
|
|
// platform telemetry 用一個系統 ak(讀 env.PLATFORM_API_KEY),所有 telemetry
|
|
// 都聚集在 platform user_id 下,避免污染用戶自己的 KBDB namespace
|
|
const platformKey = env.PLATFORM_API_KEY || apiKey || '';
|
|
if (!platformKey) {
|
|
// 沒 platform key + 沒用戶 key → 無處可寫,skip
|
|
console.warn('[telemetry] no api_key, skipping');
|
|
return;
|
|
}
|
|
|
|
const body = {
|
|
api_key: platformKey,
|
|
type: 'agent-telemetry',
|
|
source: 'cypher-executor',
|
|
user_id: 'platform_telemetry',
|
|
content: JSON.stringify(record),
|
|
metadata_json: JSON.stringify({ ...record, api_key_hash }),
|
|
tags_json: JSON.stringify([
|
|
'agent-telemetry',
|
|
`event:${record.event_type}`,
|
|
]),
|
|
};
|
|
|
|
const res = await fetch(kbdbCreateBlockUrl(env), {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify(body),
|
|
});
|
|
|
|
if (!res.ok) {
|
|
console.warn(
|
|
'[telemetry] write failed',
|
|
res.status,
|
|
await res.text().catch(() => 'no body'),
|
|
);
|
|
}
|
|
} catch (e) {
|
|
console.warn('[telemetry] exception', e);
|
|
}
|
|
})();
|
|
|
|
if (ctx?.waitUntil) {
|
|
ctx.waitUntil(promise);
|
|
}
|
|
// 沒 ctx.waitUntil 的情況(直接從 host function call)也讓 promise 自己跑,可能被 cancel 也接受
|
|
}
|