| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170 |
- import { Router, type Request, type Response } from 'express';
- import { randomUUID } from 'node:crypto';
- import { ZodError, z } from 'zod';
- import { ApiError } from '../http/api-error.js';
- import { assertCloudAction, CLOUD_ROUTES, requireWorkspaceId } from './action-registry.js';
- const requestSchema = z.object({
- action: z.string().min(1).max(100),
- workspaceId: z.string().min(1).max(200).optional(),
- platform: z.string().min(1).max(30).optional(),
- payload: z.record(z.string(), z.unknown()).optional().default({}),
- idempotencyKey: z.string().min(8).max(200).optional(),
- }).strict();
- type Dispatcher = (request: Request, response: Response, actionPath: string, method: string, body: Record<string, unknown>, requestId: string) => Promise<void>;
- type SpecialActionHandler = (request: Request, response: Response, action: string, body: Record<string, unknown>, requestId: string) => Promise<boolean>;
- export function createCloudFunctionRouter(input: {
- defaultWorkspaceId: string;
- dispatch: Dispatcher;
- specialAction?: SpecialActionHandler;
- }): Router {
- const router = Router();
- router.get('/test', (_request, response) => response.json({ success: true, data: { service: 'saas-voc-cloud-functions' }, requestId: randomUUID() }));
- router.post('/', async (request, response) => {
- const requestId = randomUUID();
- try {
- const parsed = requestSchema.parse(unwrapManagedFunctionEnvelope(request.body));
- const action = assertCloudAction(parsed.action);
- const payload: Record<string, unknown> = { ...(parsed.payload as Record<string, unknown>), ...(parsed.platform ? { platform: parsed.platform } : {}) };
- const workspaceId = requireWorkspaceId(parsed.workspaceId ?? payload['workspaceId'], input.defaultWorkspaceId);
- // The authenticated workspace is authoritative; never let a nested
- // payload replace it after it has been validated.
- payload['workspaceId'] = workspaceId;
- const route = CLOUD_ROUTES[action];
- if (!route) {
- if (input.specialAction && await input.specialAction(request, response, action, { ...payload, workspaceId }, requestId)) return;
- throw new ApiError(501, 'cloud_action_not_implemented');
- }
- const actionPath = route.path(payload, workspaceId);
- const query = route.query?.(payload, workspaceId) ?? {};
- const queryString = new URLSearchParams(
- Object.entries(query).filter(([, value]) => value !== undefined && value !== null && value !== '')
- .map(([key, value]) => [key, Array.isArray(value) ? value.join(',') : String(value)]),
- ).toString();
- const fullPath = queryString ? `${actionPath}?${queryString}` : actionPath;
- const body = { ...payload, workspaceId, ...(parsed.idempotencyKey ? { idempotencyKey: parsed.idempotencyKey } : {}) };
- await input.dispatch(request, response, fullPath, route.method, body, requestId);
- } catch (error) {
- sendCloudError(response, requestId, error);
- }
- });
- return router;
- }
- export async function dispatchThroughRouter(
- businessRouter: Router,
- request: Request,
- response: Response,
- actionPath: string,
- method: string,
- body: Record<string, unknown>,
- requestId: string,
- ): Promise<void> {
- const originalUrl = request.url;
- const originalMethod = request.method;
- const originalBody = request.body;
- const originalJson = response.json.bind(response);
- const originalSend = response.send.bind(response);
- let settled = false;
- request.url = actionPath;
- request.method = method;
- request.body = body;
- await new Promise<void>((resolve, reject) => {
- const finish = (error?: unknown) => {
- if (settled) return;
- settled = true;
- if (error) reject(error);
- else resolve();
- };
- response.json = ((value: unknown) => {
- response.json = originalJson;
- if (response.statusCode >= 400 || (value && typeof value === 'object' && 'error' in value)) {
- finish(new ApiError(response.statusCode >= 400 ? response.statusCode : 500, extractErrorCode(value)));
- } else {
- const output = isCloudResponse(value) ? value : { success: true, data: value, requestId };
- response.status(200).json(output);
- finish();
- }
- return response;
- }) as Response['json'];
- response.send = ((value: unknown) => {
- response.send = originalSend;
- let parsed: unknown = value;
- if (Buffer.isBuffer(value) || value instanceof Uint8Array) {
- try { parsed = JSON.parse(Buffer.from(value).toString('utf8')); } catch { parsed = { value: '[non-json response]' }; }
- } else if (typeof value === 'string') {
- try { parsed = JSON.parse(value); } catch { parsed = { value: '[non-json response]' }; }
- }
- if (response.statusCode >= 400 || (parsed && typeof parsed === 'object' && 'error' in parsed)) {
- finish(new ApiError(response.statusCode >= 400 ? response.statusCode : 502, extractErrorCode(parsed)));
- } else {
- const output = isCloudResponse(parsed) ? parsed : { success: true, data: parsed, requestId };
- response.status(200).json(output);
- finish();
- }
- return response;
- }) as Response['send'];
- (businessRouter as unknown as { handle: (request: Request, response: Response, next: (error?: unknown) => void) => void })
- .handle(request, response, (error?: unknown) => finish(error ?? new ApiError(404, 'not_found')));
- }).finally(() => {
- request.url = originalUrl;
- request.method = originalMethod;
- request.body = originalBody;
- response.json = originalJson;
- response.send = originalSend;
- });
- }
- /**
- * FmodeCloud.function posts `{ path, params, _ApplicationId, _InstallationId }`.
- * The hosted evaluator unwraps `params` before invoking the Function, so the
- * local adapter mirrors that transport step for the same browser client.
- */
- function unwrapManagedFunctionEnvelope(body: unknown): unknown {
- if (!body || typeof body !== 'object' || Array.isArray(body)) return body;
- const value = body as Record<string, unknown>;
- if (value['path'] !== '/saas-voc-gateway') return body;
- const params = value['params'];
- return params && typeof params === 'object' && !Array.isArray(params) ? params : body;
- }
- function extractErrorCode(value: unknown): string {
- if (value && typeof value === 'object' && 'error' in value) {
- const error = (value as Record<string, unknown>).error;
- if (typeof error === 'string') return error;
- }
- return 'cloud_action_failed';
- }
- function isCloudResponse(value: unknown): value is { success: boolean; data: unknown; requestId: string } {
- return Boolean(value && typeof value === 'object' && 'success' in value && 'requestId' in value && 'data' in value);
- }
- function sendCloudError(response: Response, requestId: string, error: unknown): void {
- if (response.headersSent) return;
- const status = error instanceof ApiError ? error.status : error instanceof ZodError ? 400 : 500;
- const code = error instanceof ApiError
- ? error.code
- : error instanceof ZodError ? 'invalid_request' : 'internal_error';
- const details = error instanceof ZodError
- ? error.issues.map((issue) => ({ path: issue.path.join('.'), message: issue.message }))
- : undefined;
- if (status >= 500) console.error('[cloud-function]', requestId, code, error instanceof Error ? error.message : error);
- response.status(status).json({ success: false, code, message: publicMessage(code), requestId, ...(details ? { details } : {}) });
- }
- function publicMessage(code: string): string {
- const messages: Record<string, string> = {
- cloud_action_not_allowed: '不支持的业务操作',
- cloud_action_not_implemented: '业务操作暂未配置',
- invalid_request: '请求参数错误',
- internal_error: '服务暂不可用,请稍后重试',
- };
- return messages[code] ?? code;
- }
|