mirror of
https://github.com/vercel/workflow.git
synced 2026-09-14 19:59:43 +08:00
ae51f45166
* Add workflow analytics world APIs * web: read observability list views from world.analytics when available Route the runs/steps/events/hooks list server actions through the optional world.analytics namespace when the backend provides one, falling back to the runtime storage APIs otherwise. Events listing only uses the analytics path when no payload data is requested. Detail/get actions, streams, and mutations are unchanged. * web: keep events and hooks list reads on the runtime storage API The Events tab and trace viewer derive step names and wait resumeAt from resolved event payloads, and the hooks table needs the secret token and ownerId for its resume/copy-token actions. The metadata-only analytics rows do not carry these, so only the runs and steps list views use world.analytics. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * web: read events list from world.analytics with a runtime-shape remap The events list/trace consumers read only top-level eventType, correlationId, and createdAt; event payloads are loaded lazily per event via fetchEvent(..., 'all') on the runtime path. Map the flat analytics event rows into the runtime Event shape (reconstructing eventData.stepName) so fetchEvents and fetchEventsByCorrelationId can use the analytics read path when available. Hooks remain on the runtime path (they need the secret token + ownerId). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * web: list hooks from world.analytics, fetch token on demand The hooks list now reads from the metadata-only world.analytics namespace when the backend provides one (falling back to the runtime storage APIs otherwise). A hook's secret token is no longer shipped in list rows — it is fetched one hook at a time via world.hooks.get only when the user copies the token or resumes the hook, keeping the secret out of bulk list responses. Adds a fetchHookToken server action + RPC, a HookListItem type (Hook without token), and a lazy HookTokenCell for the copy-token affordance. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * Handle analytics access metadata in web --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
136 lines
4.2 KiB
TypeScript
136 lines
4.2 KiB
TypeScript
/**
|
|
* Resource route that exposes server functions as an RPC-style API.
|
|
*
|
|
* Client code calls these via fetch POST to /api/rpc with a CBOR-encoded
|
|
* body containing { method, params }. Responses are CBOR-encoded, which
|
|
* preserves Uint8Array values for client-side hydration/deserialization.
|
|
*/
|
|
|
|
import { decode, encode } from 'cbor-x';
|
|
import {
|
|
cancelRun,
|
|
fetchEvent,
|
|
fetchEvents,
|
|
fetchEventsByCorrelationId,
|
|
fetchHook,
|
|
fetchHooks,
|
|
fetchHookToken,
|
|
fetchRun,
|
|
fetchRuns,
|
|
fetchStep,
|
|
fetchSteps,
|
|
fetchStreams,
|
|
fetchWorkflowsManifest,
|
|
getEncryptionKeyForRun,
|
|
getPublicServerConfig,
|
|
recreateRun,
|
|
reenqueueRun,
|
|
resumeHook,
|
|
runHealthCheck,
|
|
wakeUpRun,
|
|
} from '~/server/workflow-server-actions.server';
|
|
import type { Route } from './+types/api.rpc';
|
|
|
|
type RpcMethod = keyof typeof handlers;
|
|
|
|
const handlers = {
|
|
fetchRuns: (p: any) => fetchRuns(p.worldEnv ?? {}, p.params ?? {}),
|
|
fetchRun: (p: any) => fetchRun(p.worldEnv ?? {}, p.runId, p.resolveData),
|
|
fetchSteps: (p: any) => fetchSteps(p.worldEnv ?? {}, p.runId, p.params ?? {}),
|
|
fetchStep: (p: any) =>
|
|
fetchStep(p.worldEnv ?? {}, p.runId, p.stepId, p.resolveData),
|
|
fetchEvents: (p: any) =>
|
|
fetchEvents(p.worldEnv ?? {}, p.runId, p.params ?? {}),
|
|
fetchEvent: (p: any) =>
|
|
fetchEvent(p.worldEnv ?? {}, p.runId, p.eventId, p.resolveData),
|
|
fetchEventsByCorrelationId: (p: any) =>
|
|
fetchEventsByCorrelationId(
|
|
p.worldEnv ?? {},
|
|
p.correlationId,
|
|
p.params ?? {}
|
|
),
|
|
fetchHooks: (p: any) => fetchHooks(p.worldEnv ?? {}, p.params ?? {}),
|
|
fetchHookToken: (p: any) =>
|
|
fetchHookToken(p.worldEnv ?? {}, p.runId, p.hookId),
|
|
fetchHook: (p: any) => fetchHook(p.worldEnv ?? {}, p.hookId, p.resolveData),
|
|
cancelRun: (p: any) => cancelRun(p.worldEnv ?? {}, p.runId),
|
|
recreateRun: (p: any) =>
|
|
recreateRun(p.worldEnv ?? {}, p.runId, p.deploymentId),
|
|
reenqueueRun: (p: any) => reenqueueRun(p.worldEnv ?? {}, p.runId),
|
|
wakeUpRun: (p: any) => wakeUpRun(p.worldEnv ?? {}, p.runId, p.options),
|
|
resumeHook: (p: any) => resumeHook(p.worldEnv ?? {}, p.token, p.payload),
|
|
fetchStreams: (p: any) => fetchStreams(p.worldEnv ?? {}, p.runId),
|
|
fetchWorkflowsManifest: (p: any) => fetchWorkflowsManifest(p.worldEnv ?? {}),
|
|
runHealthCheck: (p: any) =>
|
|
runHealthCheck(p.worldEnv ?? {}, p.endpoint, p.options),
|
|
getEncryptionKeyForRun: (p: any) =>
|
|
getEncryptionKeyForRun(p.worldEnv ?? {}, p.runId),
|
|
getPublicServerConfig: () => getPublicServerConfig(),
|
|
} as const;
|
|
|
|
function cborResponse(data: unknown, status = 200): Response {
|
|
const encoded = encode(data);
|
|
return new Response(new Uint8Array(encoded), {
|
|
status,
|
|
headers: { 'Content-Type': 'application/cbor' },
|
|
});
|
|
}
|
|
|
|
export async function action({ request }: Route.ActionArgs) {
|
|
// Decode request body — accept both CBOR and JSON
|
|
let body: { method: string; params: any };
|
|
const contentType = request.headers.get('content-type') || '';
|
|
try {
|
|
if (contentType.includes('application/cbor')) {
|
|
const buffer = await request.arrayBuffer();
|
|
body = decode(new Uint8Array(buffer));
|
|
} else {
|
|
body = await request.json();
|
|
}
|
|
} catch {
|
|
return cborResponse(
|
|
{
|
|
success: false,
|
|
error: { message: 'Malformed request body', layer: 'server' },
|
|
},
|
|
400
|
|
);
|
|
}
|
|
|
|
const { method, params } = body;
|
|
|
|
if (!method || !(method in handlers)) {
|
|
return cborResponse(
|
|
{
|
|
success: false,
|
|
error: { message: `Unknown method: ${method}`, layer: 'server' },
|
|
},
|
|
400
|
|
);
|
|
}
|
|
|
|
try {
|
|
const result = await handlers[method as RpcMethod](params ?? {});
|
|
return cborResponse(result);
|
|
} catch (error) {
|
|
const message = error instanceof Error ? error.message : String(error);
|
|
return cborResponse(
|
|
{ success: false, error: { message, layer: 'server' } },
|
|
500
|
|
);
|
|
}
|
|
}
|
|
|
|
// Also support GET for read operations (JSON — no binary data needed)
|
|
export async function loader({ request }: Route.LoaderArgs) {
|
|
const url = new URL(request.url);
|
|
const method = url.searchParams.get('method');
|
|
|
|
if (method === 'getPublicServerConfig') {
|
|
const result = await getPublicServerConfig();
|
|
return Response.json(result);
|
|
}
|
|
|
|
return Response.json({ error: 'Use POST for RPC calls' }, { status: 405 });
|
|
}
|