@faapi/faapi 6.20.0 → 6.21.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli/index.js CHANGED
@@ -711,8 +711,8 @@ var init_parseRouteFile = __esm({
711
711
  // src/utils/importWithCacheBust.ts
712
712
  import fs8 from "fs";
713
713
  import { pathToFileURL } from "url";
714
- function setLoadTimestamp(ts11) {
715
- loadTs = ts11;
714
+ function setLoadTimestamp(ts12) {
715
+ loadTs = ts12;
716
716
  }
717
717
  function getVitestImportActual() {
718
718
  const vi = globalThis.vi;
@@ -2513,8 +2513,16 @@ var init_resolveInjection = __esm({
2513
2513
  // Phase 2.3
2514
2514
  tasks: "tasks",
2515
2515
  // 任务子系统:TaskClient(入队/查询)
2516
- log: "log"
2516
+ log: "log",
2517
2517
  // 请求级日志器(ctx.log 同一实例:scope http,自动带 requestId 等字段)
2518
+ // raw 系:恒原始输入(无 schema、无校验、管线永不转换),与 query/params/body
2519
+ // 的「校验转换后」口径二分互补
2520
+ rawQuery: "rawQuery",
2521
+ // URLSearchParams(原始查询参数)
2522
+ rawParams: "rawParams",
2523
+ // Record<string, string>(URL 原始路径段)
2524
+ rawBody: "rawBody"
2525
+ // string | undefined(请求体原始文本)
2518
2526
  };
2519
2527
  injectionCache = /* @__PURE__ */ new WeakMap();
2520
2528
  }
@@ -2572,7 +2580,19 @@ var init_analyzeInjection = __esm({
2572
2580
 
2573
2581
  // src/cli/collectRouteSchemaSources.ts
2574
2582
  import path11 from "path";
2575
- function collectRouteSchemaSources(routes, rootDir) {
2583
+ import ts8 from "typescript";
2584
+ function findExportedTypeName(sourceFile, name) {
2585
+ for (const statement of sourceFile.statements) {
2586
+ const modifiers = ts8.canHaveModifiers(statement) ? ts8.getModifiers(statement) : void 0;
2587
+ const isExported = modifiers?.some((m) => m.kind === ts8.SyntaxKind.ExportKeyword);
2588
+ if (!isExported) continue;
2589
+ if ((ts8.isInterfaceDeclaration(statement) || ts8.isTypeAliasDeclaration(statement)) && statement.name?.text === name) {
2590
+ return true;
2591
+ }
2592
+ }
2593
+ return false;
2594
+ }
2595
+ function collectRouteSchemaSources(routes, rootDir, wsRoutes) {
2576
2596
  const methodsByFile = /* @__PURE__ */ new Map();
2577
2597
  for (const route of routes) {
2578
2598
  const filePath = rootDir ? path11.resolve(rootDir, route.filePath) : route.filePath;
@@ -2583,9 +2603,17 @@ function collectRouteSchemaSources(routes, rootDir) {
2583
2603
  }
2584
2604
  entry.methods.add(route.method);
2585
2605
  }
2586
- const programByFile = createPrograms([...methodsByFile.keys()]);
2606
+ const wsFiles = [];
2607
+ for (const wsRoute of wsRoutes ?? []) {
2608
+ wsFiles.push({
2609
+ filePath: rootDir ? path11.resolve(rootDir, wsRoute.filePath) : wsRoute.filePath,
2610
+ urlPath: wsRoute.urlPath
2611
+ });
2612
+ }
2613
+ const allFiles = [.../* @__PURE__ */ new Set([...methodsByFile.keys(), ...wsFiles.map((f) => f.filePath)])];
2614
+ const programByFile = createPrograms(allFiles);
2587
2615
  const resolversByFile = /* @__PURE__ */ new Map();
2588
- for (const filePath of methodsByFile.keys()) {
2616
+ for (const filePath of allFiles) {
2589
2617
  resolversByFile.set(filePath, createLazyTypeResolver(programByFile.get(filePath), filePath));
2590
2618
  }
2591
2619
  const sources = [];
@@ -2641,6 +2669,28 @@ function collectRouteSchemaSources(routes, rootDir) {
2641
2669
  }
2642
2670
  }
2643
2671
  }
2672
+ for (const wsFile of wsFiles) {
2673
+ const program = programByFile.get(wsFile.filePath);
2674
+ const sourceFile = program?.getSourceFile(wsFile.filePath);
2675
+ if (!sourceFile) continue;
2676
+ const resolver = resolversByFile.get(wsFile.filePath);
2677
+ if (findExportedTypeName(sourceFile, "Query")) {
2678
+ sources.push({
2679
+ urlPath: wsFile.urlPath,
2680
+ filePath: wsFile.filePath,
2681
+ schemaName: getSchemaName("WS", "query"),
2682
+ typeInfo: resolver.resolve("Query") ?? null
2683
+ });
2684
+ }
2685
+ if (findExportedTypeName(sourceFile, "Params")) {
2686
+ sources.push({
2687
+ urlPath: wsFile.urlPath,
2688
+ filePath: wsFile.filePath,
2689
+ schemaName: getSchemaName("WS", "params"),
2690
+ typeInfo: resolver.resolve("Params") ?? null
2691
+ });
2692
+ }
2693
+ }
2644
2694
  return { sources, resolversByFile };
2645
2695
  }
2646
2696
  var init_collectRouteSchemaSources = __esm({
@@ -2727,9 +2777,9 @@ function generateSchemaFileSource(sources, resolveType, helpersImportPath) {
2727
2777
  lines.push(...schemaBlocks);
2728
2778
  return lines.join("\n").replace(/\n+$/, "\n");
2729
2779
  }
2730
- async function generateSchemaFiles(routes, rootDir, dist) {
2731
- if (routes.length === 0) return;
2732
- const { sources, resolversByFile } = collectRouteSchemaSources(routes, rootDir);
2780
+ async function generateSchemaFiles(routes, rootDir, dist, wsRoutes) {
2781
+ if (routes.length === 0 && (wsRoutes?.length ?? 0) === 0) return;
2782
+ const { sources, resolversByFile } = collectRouteSchemaSources(routes, rootDir, wsRoutes);
2733
2783
  await generateZodArtifacts(sources, {
2734
2784
  generateFileSource: generateSchemaFileSource,
2735
2785
  resolversByFile,
@@ -2800,14 +2850,14 @@ var init_generateZodArtifacts = __esm({
2800
2850
  });
2801
2851
 
2802
2852
  // src/ast/jsDocMetadata.ts
2803
- import ts8 from "typescript";
2853
+ import ts9 from "typescript";
2804
2854
  function hasExportModifier(node) {
2805
- if (!ts8.canHaveModifiers(node)) return false;
2806
- const modifiers = ts8.getModifiers(node);
2807
- return !!modifiers?.some((m) => m.kind === ts8.SyntaxKind.ExportKeyword);
2855
+ if (!ts9.canHaveModifiers(node)) return false;
2856
+ const modifiers = ts9.getModifiers(node);
2857
+ return !!modifiers?.some((m) => m.kind === ts9.SyntaxKind.ExportKeyword);
2808
2858
  }
2809
2859
  function getJSDocFromNode(node) {
2810
- const apiDocs = ts8.getJSDocCommentsAndTags(node).filter((entry) => ts8.isJSDoc(entry));
2860
+ const apiDocs = ts9.getJSDocCommentsAndTags(node).filter((entry) => ts9.isJSDoc(entry));
2811
2861
  if (apiDocs.length > 0) return apiDocs[0];
2812
2862
  const directDocs = node.jsDoc;
2813
2863
  if (directDocs && directDocs.length > 0) return directDocs[0];
@@ -2838,7 +2888,7 @@ var init_jsDocMetadata = __esm({
2838
2888
  });
2839
2889
 
2840
2890
  // src/ast/extractToolMetadata.ts
2841
- import ts9 from "typescript";
2891
+ import ts10 from "typescript";
2842
2892
  function extractToolMetadata(program, filePath, functionName, pathMeta) {
2843
2893
  const sourceFile = program.getSourceFile(filePath);
2844
2894
  if (!sourceFile) return null;
@@ -2859,18 +2909,18 @@ function extractToolMetadata(program, filePath, functionName, pathMeta) {
2859
2909
  }
2860
2910
  function findExportedFunction(sourceFile, functionName) {
2861
2911
  let result = null;
2862
- ts9.forEachChild(sourceFile, (node) => {
2912
+ ts10.forEachChild(sourceFile, (node) => {
2863
2913
  if (result) return;
2864
- if (ts9.isFunctionDeclaration(node) && hasExportModifier(node) && node.name?.text === functionName) {
2914
+ if (ts10.isFunctionDeclaration(node) && hasExportModifier(node) && node.name?.text === functionName) {
2865
2915
  result = { fn: node, jsDocOwner: node };
2866
2916
  return;
2867
2917
  }
2868
- if (ts9.isVariableStatement(node) && hasExportModifier(node)) {
2918
+ if (ts10.isVariableStatement(node) && hasExportModifier(node)) {
2869
2919
  for (const decl of node.declarationList.declarations) {
2870
2920
  if (result) break;
2871
- const nameText = ts9.isIdentifier(decl.name) ? decl.name.text : decl.name.getText(sourceFile);
2921
+ const nameText = ts10.isIdentifier(decl.name) ? decl.name.text : decl.name.getText(sourceFile);
2872
2922
  if (nameText !== functionName || !decl.initializer) continue;
2873
- if (ts9.isArrowFunction(decl.initializer) || ts9.isFunctionExpression(decl.initializer)) {
2923
+ if (ts10.isArrowFunction(decl.initializer) || ts10.isFunctionExpression(decl.initializer)) {
2874
2924
  result = { fn: decl.initializer, jsDocOwner: node };
2875
2925
  }
2876
2926
  }
@@ -2882,7 +2932,7 @@ function getFirstParamTypeName(fn, sourceFile) {
2882
2932
  const firstParam = fn.parameters[0];
2883
2933
  if (!firstParam) return void 0;
2884
2934
  if (!firstParam.type) return void 0;
2885
- if (!ts9.isTypeReferenceNode(firstParam.type)) return void 0;
2935
+ if (!ts10.isTypeReferenceNode(firstParam.type)) return void 0;
2886
2936
  return firstParam.type.typeName.getText(sourceFile);
2887
2937
  }
2888
2938
  var init_extractToolMetadata = __esm({
@@ -3092,7 +3142,7 @@ var init_scanAgents = __esm({
3092
3142
  });
3093
3143
 
3094
3144
  // src/ast/extractAgentMetadata.ts
3095
- import ts10 from "typescript";
3145
+ import ts11 from "typescript";
3096
3146
  function extractAgentMetadata(program, filePath, pathMeta) {
3097
3147
  const sourceFile = program.getSourceFile(filePath);
3098
3148
  if (!sourceFile) return null;
@@ -3136,23 +3186,23 @@ function extractAgentMetadata(program, filePath, pathMeta) {
3136
3186
  }
3137
3187
  function findConfigExport(sourceFile) {
3138
3188
  let result = null;
3139
- ts10.forEachChild(sourceFile, (node) => {
3189
+ ts11.forEachChild(sourceFile, (node) => {
3140
3190
  if (result) return;
3141
- if (ts10.isVariableStatement(node) && hasExportModifier(node)) {
3191
+ if (ts11.isVariableStatement(node) && hasExportModifier(node)) {
3142
3192
  for (const decl of node.declarationList.declarations) {
3143
3193
  if (result) break;
3144
- const nameText = ts10.isIdentifier(decl.name) ? decl.name.text : "";
3194
+ const nameText = ts11.isIdentifier(decl.name) ? decl.name.text : "";
3145
3195
  if (nameText !== "config" || !decl.initializer) continue;
3146
- if (ts10.isObjectLiteralExpression(decl.initializer)) {
3196
+ if (ts11.isObjectLiteralExpression(decl.initializer)) {
3147
3197
  result = { jsDocOwner: node, objectLiteral: decl.initializer };
3148
- } else if (ts10.isArrowFunction(decl.initializer)) {
3198
+ } else if (ts11.isArrowFunction(decl.initializer)) {
3149
3199
  result = { jsDocOwner: node, objectLiteral: getReturnObjectLiteral(decl.initializer) };
3150
3200
  } else {
3151
3201
  result = { jsDocOwner: node, objectLiteral: null };
3152
3202
  }
3153
3203
  }
3154
3204
  }
3155
- if (ts10.isFunctionDeclaration(node) && hasExportModifier(node) && node.name?.text === "config") {
3205
+ if (ts11.isFunctionDeclaration(node) && hasExportModifier(node) && node.name?.text === "config") {
3156
3206
  const returnObj = getReturnObjectLiteral(node);
3157
3207
  result = { jsDocOwner: node, objectLiteral: returnObj };
3158
3208
  }
@@ -3162,12 +3212,12 @@ function findConfigExport(sourceFile) {
3162
3212
  function getReturnObjectLiteral(fn) {
3163
3213
  const body = fn.body;
3164
3214
  if (!body) return null;
3165
- if (ts10.isObjectLiteralExpression(body)) {
3215
+ if (ts11.isObjectLiteralExpression(body)) {
3166
3216
  return body;
3167
3217
  }
3168
- if (ts10.isBlock(body)) {
3218
+ if (ts11.isBlock(body)) {
3169
3219
  for (const stmt of body.statements) {
3170
- if (ts10.isReturnStatement(stmt) && stmt.expression && ts10.isObjectLiteralExpression(stmt.expression)) {
3220
+ if (ts11.isReturnStatement(stmt) && stmt.expression && ts11.isObjectLiteralExpression(stmt.expression)) {
3171
3221
  return stmt.expression;
3172
3222
  }
3173
3223
  }
@@ -3182,8 +3232,8 @@ function extractConfigFields(objLit, sourceFile) {
3182
3232
  let maxTurns;
3183
3233
  let inputDescription;
3184
3234
  for (const prop of objLit.properties) {
3185
- if (ts10.isSpreadAssignment(prop)) continue;
3186
- if (!ts10.isPropertyAssignment(prop)) {
3235
+ if (ts11.isSpreadAssignment(prop)) continue;
3236
+ if (!ts11.isPropertyAssignment(prop)) {
3187
3237
  throw SchemaExtractionError.at(
3188
3238
  prop,
3189
3239
  "config",
@@ -3234,13 +3284,13 @@ function extractConfigFields(objLit, sourceFile) {
3234
3284
  return { systemPrompt, tools, agents, model, maxTurns, inputDescription };
3235
3285
  }
3236
3286
  function getPropertyName(name) {
3237
- if (ts10.isIdentifier(name)) return name.text;
3238
- if (ts10.isStringLiteral(name)) return name.text;
3287
+ if (ts11.isIdentifier(name)) return name.text;
3288
+ if (ts11.isStringLiteral(name)) return name.text;
3239
3289
  return null;
3240
3290
  }
3241
3291
  function extractStringValue(expr) {
3242
3292
  if (isStringLikeLiteral(expr)) return expr.text;
3243
- if (ts10.isBinaryExpression(expr) && expr.operatorToken.kind === ts10.SyntaxKind.PlusToken) {
3293
+ if (ts11.isBinaryExpression(expr) && expr.operatorToken.kind === ts11.SyntaxKind.PlusToken) {
3244
3294
  const left = extractStringValue(expr.left);
3245
3295
  if (left === void 0) return void 0;
3246
3296
  const right = extractStringValue(expr.right);
@@ -3286,17 +3336,17 @@ function requireNumberValue(prop, fieldName, sourceFile) {
3286
3336
  return value;
3287
3337
  }
3288
3338
  function extractNumberValue(expr) {
3289
- if (ts10.isNumericLiteral(expr)) {
3339
+ if (ts11.isNumericLiteral(expr)) {
3290
3340
  const num = Number(expr.text);
3291
3341
  return Number.isNaN(num) ? void 0 : num;
3292
3342
  }
3293
3343
  return void 0;
3294
3344
  }
3295
3345
  function isStringLikeLiteral(node) {
3296
- return ts10.isStringLiteral(node) || ts10.isNoSubstitutionTemplateLiteral(node);
3346
+ return ts11.isStringLiteral(node) || ts11.isNoSubstitutionTemplateLiteral(node);
3297
3347
  }
3298
3348
  function extractStringArrayValue(expr) {
3299
- if (!ts10.isArrayLiteralExpression(expr)) return void 0;
3349
+ if (!ts11.isArrayLiteralExpression(expr)) return void 0;
3300
3350
  const values = [];
3301
3351
  for (const element of expr.elements) {
3302
3352
  const value = extractStringValue(element);
@@ -3784,7 +3834,7 @@ function clearGeneratedSchemas() {
3784
3834
  state.generatedSchemas.clear();
3785
3835
  state.inFlightSchemaGenerations.clear();
3786
3836
  }
3787
- async function ensureSchemaGenerated(schemaPath, routeFilePath, routes, rootDir, dist) {
3837
+ async function ensureSchemaGenerated(schemaPath, routeFilePath, routes, rootDir, dist, wsRoutes) {
3788
3838
  const inFlight2 = state.inFlightSchemaGenerations.get(schemaPath);
3789
3839
  if (inFlight2) {
3790
3840
  await inFlight2;
@@ -3803,13 +3853,15 @@ async function ensureSchemaGenerated(schemaPath, routeFilePath, routes, rootDir,
3803
3853
  return false;
3804
3854
  }
3805
3855
  const fileRoutes = routes.filter((r) => r.filePath === routeFilePath);
3806
- if (fileRoutes.length === 0) {
3856
+ const fileWsRoutes = (wsRoutes ?? []).filter((r) => r.filePath === routeFilePath);
3857
+ if (fileRoutes.length === 0 && fileWsRoutes.length === 0) {
3807
3858
  return false;
3808
3859
  }
3809
3860
  const sourceRelPath = path20.relative(rootDir, sourceAbsPath).replace(/\\/g, "/");
3810
3861
  const sourceRoutes = fileRoutes.map((r) => ({ ...r, filePath: sourceRelPath }));
3862
+ const sourceWsRoutes = fileWsRoutes.map((r) => ({ ...r, filePath: sourceRelPath }));
3811
3863
  const generatePromise = (async () => {
3812
- await generateSchemaFiles(sourceRoutes, rootDir, dist);
3864
+ await generateSchemaFiles(sourceRoutes, rootDir, dist, sourceWsRoutes);
3813
3865
  state.generatedSchemas.add(schemaPath);
3814
3866
  })();
3815
3867
  state.inFlightSchemaGenerations.set(schemaPath, generatePromise);
@@ -6314,17 +6366,24 @@ function encodeSseEvent(event) {
6314
6366
  out += "\n";
6315
6367
  return out;
6316
6368
  }
6317
- function createSseWriter() {
6369
+ function createSseWriter(options = {}) {
6370
+ const { onFirstWrite } = options;
6318
6371
  const encoder = new TextEncoder();
6319
6372
  let controller = null;
6320
6373
  let closed = false;
6321
6374
  let aborted = false;
6375
+ let firstWriteTriggered = false;
6322
6376
  let drainWaiters = [];
6323
6377
  const resolveDrainWaiters = () => {
6324
6378
  const waiters = drainWaiters;
6325
6379
  drainWaiters = [];
6326
6380
  for (const resolve3 of waiters) resolve3();
6327
6381
  };
6382
+ const triggerFirstWrite = () => {
6383
+ if (firstWriteTriggered) return;
6384
+ firstWriteTriggered = true;
6385
+ onFirstWrite?.(response);
6386
+ };
6328
6387
  const stream = new ReadableStream(
6329
6388
  {
6330
6389
  start(c) {
@@ -6358,11 +6417,13 @@ function createSseWriter() {
6358
6417
  if (closed || !controller) return;
6359
6418
  const text = encodeSseEvent(event);
6360
6419
  controller.enqueue(encoder.encode(text));
6420
+ triggerFirstWrite();
6361
6421
  },
6362
6422
  sendRaw(chunk) {
6363
6423
  if (closed || !controller) return;
6364
6424
  const bytes = typeof chunk === "string" ? encoder.encode(chunk) : chunk;
6365
6425
  controller.enqueue(bytes);
6426
+ triggerFirstWrite();
6366
6427
  },
6367
6428
  sendError(error) {
6368
6429
  if (closed || !controller) return;
@@ -6370,6 +6431,7 @@ function createSseWriter() {
6370
6431
  const text = encodeSseEvent({ event: "error", data: message });
6371
6432
  try {
6372
6433
  controller.enqueue(encoder.encode(text));
6434
+ triggerFirstWrite();
6373
6435
  } finally {
6374
6436
  writer.close();
6375
6437
  }
@@ -6817,6 +6879,32 @@ var init_logger = __esm({
6817
6879
  }
6818
6880
  });
6819
6881
 
6882
+ // src/utils/queryToObject.ts
6883
+ function queryToObject(params) {
6884
+ const cached = queryCache.get(params);
6885
+ if (cached) return cached;
6886
+ const result = {};
6887
+ for (const [key, value] of params) {
6888
+ const existing = result[key];
6889
+ if (existing === void 0) {
6890
+ result[key] = value;
6891
+ } else if (Array.isArray(existing)) {
6892
+ existing.push(value);
6893
+ } else {
6894
+ result[key] = [existing, value];
6895
+ }
6896
+ }
6897
+ queryCache.set(params, result);
6898
+ return result;
6899
+ }
6900
+ var queryCache;
6901
+ var init_queryToObject = __esm({
6902
+ "src/utils/queryToObject.ts"() {
6903
+ "use strict";
6904
+ queryCache = /* @__PURE__ */ new WeakMap();
6905
+ }
6906
+ });
6907
+
6820
6908
  // src/runtime/createContext.ts
6821
6909
  function resolveRequestId(request) {
6822
6910
  const header = request.headers.get("x-request-id");
@@ -6866,7 +6954,11 @@ function createContextFromUrl(request, url, params, config = {}, ip = "", regist
6866
6954
  log,
6867
6955
  request,
6868
6956
  params,
6869
- query: url.searchParams,
6957
+ rawParams: params,
6958
+ // ctx.query 为对象口径(createContext 挂原始 string 值对象;handler 声明
6959
+ // query 类型时管线校验后替换为转换值)。原始 URLSearchParams 恒在 rawQuery。
6960
+ query: queryToObject(url.searchParams),
6961
+ rawQuery: url.searchParams,
6870
6962
  headers: request.headers,
6871
6963
  method: request.method,
6872
6964
  path: url.pathname,
@@ -6914,13 +7006,19 @@ function createContextFromUrl(request, url, params, config = {}, ip = "", regist
6914
7006
  /**
6915
7007
  * 创建 SSE writer,用于流式推送事件
6916
7008
  *
6917
- * handler 调用此方法后,通过返回的 writer 推送事件,框架自动把 writer.response
6918
- * 作为 HTTP 响应(Content-Type: text/event-stream)。
7009
+ * handler 调用此方法后,通过返回的 writer 推送事件。HTTP 服务场景下首次
7010
+ * 写入(send/sendRaw/sendError)即触发 server 层提前接管钩子,把 writer.response
7011
+ * 接入底层连接——流式期间字节即产即达(见 createServer.md「SSE 提前接管」)。
7012
+ * 无钩子场景(app.inject / 测试直调 invokeHandler)回落为 handler 返回后交出。
6919
7013
  *
6920
7014
  * 与 ctx.json / ctx.html 互斥:一个 handler 只能用一种响应方式。
6921
7015
  */
6922
7016
  sse() {
6923
- const writer = createSseWriter();
7017
+ const writer = createSseWriter({
7018
+ onFirstWrite: (response) => {
7019
+ ctx.__earlyRespond?.(response);
7020
+ }
7021
+ });
6924
7022
  const ctxWithSse = ctx;
6925
7023
  ctxWithSse.__sseResponse = writer.response;
6926
7024
  ctxWithSse.__sseWriter = writer;
@@ -6972,32 +7070,7 @@ var init_createContext = __esm({
6972
7070
  init_sse();
6973
7071
  init_responseFormatter();
6974
7072
  init_logger();
6975
- }
6976
- });
6977
-
6978
- // src/utils/queryToObject.ts
6979
- function queryToObject(params) {
6980
- const cached = queryCache.get(params);
6981
- if (cached) return cached;
6982
- const result = {};
6983
- for (const [key, value] of params) {
6984
- const existing = result[key];
6985
- if (existing === void 0) {
6986
- result[key] = value;
6987
- } else if (Array.isArray(existing)) {
6988
- existing.push(value);
6989
- } else {
6990
- result[key] = [existing, value];
6991
- }
6992
- }
6993
- queryCache.set(params, result);
6994
- return result;
6995
- }
6996
- var queryCache;
6997
- var init_queryToObject = __esm({
6998
- "src/utils/queryToObject.ts"() {
6999
- "use strict";
7000
- queryCache = /* @__PURE__ */ new WeakMap();
7073
+ init_queryToObject();
7001
7074
  }
7002
7075
  });
7003
7076
 
@@ -7055,46 +7128,29 @@ var init_parseMultipart = __esm({
7055
7128
  async function resolveInputFromUrl(method, request, url) {
7056
7129
  const inputType = getInputTypeForMethod(method);
7057
7130
  if (inputType === "body") {
7058
- const contentType = request.headers.get("content-type") ?? "";
7059
- if (contentType.includes("multipart/form-data")) {
7060
- return parseMultipart(request);
7061
- }
7062
- if (contentType.includes("application/x-www-form-urlencoded")) {
7063
- const text2 = await request.text();
7064
- if (isBlankText(text2)) return null;
7065
- const params = new URLSearchParams(text2);
7066
- const obj = {};
7067
- for (const [key, value] of params) {
7068
- obj[key] = value;
7069
- }
7070
- return obj;
7071
- }
7072
- const text = await request.text();
7073
- if (isBlankText(text)) {
7074
- return null;
7075
- }
7076
- const result = parseJsonBody(text);
7077
- if (!result.success) {
7078
- throw new ValidationError("\u8BF7\u6C42\u4F53\u4E0D\u662F\u5408\u6CD5\u7684 JSON", [
7079
- {
7080
- path: "body",
7081
- code: "INVALID_FORMAT",
7082
- expected: "JSON",
7083
- received: "text",
7084
- message: "\u8BF7\u6C42\u4F53\u4E0D\u662F\u5408\u6CD5\u7684 JSON"
7085
- }
7086
- ]);
7087
- }
7088
- return result.data;
7131
+ return parseBodyByContentType(request);
7089
7132
  }
7090
- return queryToObject(url.searchParams);
7091
- }
7092
- function isBlankText(text) {
7093
- return text.length === 0 || !/\S/.test(text);
7133
+ return { input: queryToObject(url.searchParams) };
7094
7134
  }
7095
- async function resolveBodyForQueryMethod(request) {
7135
+ async function parseBodyByContentType(request) {
7136
+ const contentType = request.headers.get("content-type") ?? "";
7137
+ if (contentType.includes("multipart/form-data")) {
7138
+ return { input: await parseMultipart(request) };
7139
+ }
7140
+ if (contentType.includes("application/x-www-form-urlencoded")) {
7141
+ const text2 = await request.text();
7142
+ if (isBlankText(text2)) return { input: null };
7143
+ const params = new URLSearchParams(text2);
7144
+ const obj = {};
7145
+ for (const [key, value] of params) {
7146
+ obj[key] = value;
7147
+ }
7148
+ return { input: obj, rawBody: text2 };
7149
+ }
7096
7150
  const text = await request.text();
7097
- if (isBlankText(text)) return void 0;
7151
+ if (isBlankText(text)) {
7152
+ return { input: null };
7153
+ }
7098
7154
  const result = parseJsonBody(text);
7099
7155
  if (!result.success) {
7100
7156
  throw new ValidationError("\u8BF7\u6C42\u4F53\u4E0D\u662F\u5408\u6CD5\u7684 JSON", [
@@ -7107,7 +7163,13 @@ async function resolveBodyForQueryMethod(request) {
7107
7163
  }
7108
7164
  ]);
7109
7165
  }
7110
- return result.data;
7166
+ return { input: result.data, rawBody: text };
7167
+ }
7168
+ function isBlankText(text) {
7169
+ return text.length === 0 || !/\S/.test(text);
7170
+ }
7171
+ async function resolveBodyForQueryMethod(request) {
7172
+ return parseBodyByContentType(request);
7111
7173
  }
7112
7174
  var init_resolveInput = __esm({
7113
7175
  "src/runtime/resolveInput.ts"() {
@@ -7505,12 +7567,17 @@ var init_agentHandle = __esm({
7505
7567
  // src/injection/injectParams.ts
7506
7568
  function getBuiltinInjectionValue(type, ctx, body) {
7507
7569
  switch (type) {
7508
- case "query": {
7509
- const validated = ctx.__validatedQuery;
7510
- return validated ?? queryToObject(ctx.query);
7511
- }
7570
+ case "query":
7571
+ return ctx.query;
7512
7572
  case "params":
7513
7573
  return ctx.params;
7574
+ // raw 系:恒原始,管线永不覆盖(ctx 字段即注入值)
7575
+ case "rawQuery":
7576
+ return ctx.rawQuery;
7577
+ case "rawParams":
7578
+ return ctx.rawParams;
7579
+ case "rawBody":
7580
+ return ctx.rawBody;
7514
7581
  case "headers":
7515
7582
  return ctx.headers;
7516
7583
  case "context":
@@ -7576,7 +7643,6 @@ var init_injectParams = __esm({
7576
7643
  "src/injection/injectParams.ts"() {
7577
7644
  "use strict";
7578
7645
  init_resolveInjection();
7579
- init_queryToObject();
7580
7646
  init_agentRegistry();
7581
7647
  init_agentHandle();
7582
7648
  }
@@ -8424,6 +8490,7 @@ function attachWebSocket(options) {
8424
8490
  server,
8425
8491
  routesRef,
8426
8492
  rootDir,
8493
+ dist,
8427
8494
  config,
8428
8495
  globalMiddlewares,
8429
8496
  trustedProxy = false,
@@ -8458,12 +8525,50 @@ function attachWebSocket(options) {
8458
8525
  const { route, params } = match;
8459
8526
  const headers = nodeHttpToWebHeaders(req);
8460
8527
  const host = req.headers.host ?? "localhost";
8461
- const url = `http://${host}${req.url ?? "/"}`;
8462
- const request = new Request(url, { method: "GET", headers });
8528
+ const fullUrl = new URL(`http://${host}${req.url ?? "/"}`);
8529
+ const request = new Request(fullUrl.toString(), { method: "GET", headers });
8463
8530
  const ctx = createContext(request, params, config, getClientIp(req, trustedProxy), registries);
8464
8531
  const meta = ctx.meta;
8465
8532
  let upgraded = false;
8466
8533
  const finalHandler = async () => {
8534
+ const schemaPath = getRuntimeSchemaPath(route.filePath, dist, rootDir);
8535
+ if (fs19.existsSync(schemaPath)) {
8536
+ if (isDevOnDemandEnabled()) {
8537
+ const devDist = getDevDist();
8538
+ if (devDist) {
8539
+ await ensureSchemaGenerated(
8540
+ schemaPath,
8541
+ route.filePath,
8542
+ routesRef.current,
8543
+ rootDir,
8544
+ dist,
8545
+ routesRef.wsCurrent
8546
+ );
8547
+ }
8548
+ }
8549
+ const searchParams = fullUrl.searchParams;
8550
+ const rawQueryObject = queryToObject(searchParams);
8551
+ const paramsResult = await validateInput(schemaPath, "WS", "params", params);
8552
+ if (!paramsResult.valid) {
8553
+ throw new ValidationError("\u53C2\u6570\u6821\u9A8C\u5931\u8D25", paramsResult.issues);
8554
+ }
8555
+ if (paramsResult.data !== params && typeof paramsResult.data === "object" && paramsResult.data !== null) {
8556
+ ctx.params = {
8557
+ ...params,
8558
+ ...paramsResult.data
8559
+ };
8560
+ }
8561
+ const queryResult = await validateInput(schemaPath, "WS", "query", rawQueryObject);
8562
+ if (!queryResult.valid) {
8563
+ throw new ValidationError("\u53C2\u6570\u6821\u9A8C\u5931\u8D25", queryResult.issues);
8564
+ }
8565
+ if (queryResult.data !== rawQueryObject && typeof queryResult.data === "object" && queryResult.data !== null) {
8566
+ ctx.query = {
8567
+ ...rawQueryObject,
8568
+ ...queryResult.data
8569
+ };
8570
+ }
8571
+ }
8467
8572
  let handlers;
8468
8573
  try {
8469
8574
  const absoluteFilePath = path25.resolve(rootDir, route.filePath);
@@ -8527,7 +8632,11 @@ var init_handleWsUpgrade = __esm({
8527
8632
  init_invokeHandler();
8528
8633
  init_importWithCacheBust();
8529
8634
  init_getClientIp();
8635
+ init_queryToObject();
8530
8636
  init_serverUtils();
8637
+ init_validateInput();
8638
+ init_httpErrors();
8639
+ init_generateSchemaFiles();
8531
8640
  init_compileOnDemand();
8532
8641
  init_loadMiddlewares();
8533
8642
  init_pendingMeta();
@@ -8691,7 +8800,8 @@ function createServer(options) {
8691
8800
  globalInjectors,
8692
8801
  bodyLimit,
8693
8802
  trustedProxy,
8694
- registries
8803
+ registries,
8804
+ routesRef.wsCurrent
8695
8805
  ).catch((err) => {
8696
8806
  console.error("[faapi] Request pipeline failed after error response attempt:", err);
8697
8807
  try {
@@ -8709,6 +8819,7 @@ function createServer(options) {
8709
8819
  server,
8710
8820
  routesRef,
8711
8821
  rootDir,
8822
+ dist,
8712
8823
  config,
8713
8824
  globalMiddlewares,
8714
8825
  trustedProxy,
@@ -8716,14 +8827,13 @@ function createServer(options) {
8716
8827
  });
8717
8828
  return { server, routesRef };
8718
8829
  }
8719
- function prepareRequest(req, config, bodyLimit, trustedProxy, registries, requestSignal) {
8720
- const { request, url } = toWebRequest(req, bodyLimit, requestSignal);
8830
+ function prepareRequest(req, request, url, config, trustedProxy, registries, preMatched) {
8721
8831
  const method = request.method.toUpperCase();
8722
8832
  const urlPath = url.pathname;
8723
8833
  const ctx = createContextFromUrl(
8724
8834
  request,
8725
8835
  url,
8726
- {},
8836
+ preMatched?.params ?? {},
8727
8837
  config,
8728
8838
  getClientIp(req, trustedProxy),
8729
8839
  registries
@@ -8752,19 +8862,33 @@ function getRoutePaths(route, rootDir, dist) {
8752
8862
  return cached;
8753
8863
  }
8754
8864
  function createRoutePipeline(opts) {
8755
- const { routes, method, urlPath, url, ctx, request, rootDir, dist, globalInjectors } = opts;
8865
+ const {
8866
+ routes,
8867
+ method,
8868
+ urlPath,
8869
+ url,
8870
+ ctx,
8871
+ request,
8872
+ rootDir,
8873
+ dist,
8874
+ globalInjectors,
8875
+ preMatched,
8876
+ wsRoutes
8877
+ } = opts;
8756
8878
  return async () => {
8757
- const match = resolveRouteOrThrow(routes, method, urlPath);
8758
- ctx.params = match.params;
8879
+ const match = preMatched ?? resolveRouteOrThrow(routes, method, urlPath);
8759
8880
  const { route } = match;
8760
8881
  const { absFilePath, schemaPath } = getRoutePaths(route, rootDir, dist);
8761
8882
  const routeModule = await loadRouteModule(absFilePath, route.method, rootDir);
8762
- const input = await resolveInputFromUrl(route.method, request, url);
8883
+ const { input, rawBody } = await resolveInputFromUrl(route.method, request, url);
8884
+ if (rawBody !== void 0) {
8885
+ ctx.rawBody = rawBody;
8886
+ }
8763
8887
  const inputType = getInputTypeForMethod(route.method);
8764
8888
  if (isDevOnDemandEnabled()) {
8765
8889
  const devDist = getDevDist();
8766
8890
  if (devDist) {
8767
- await ensureSchemaGenerated(schemaPath, route.filePath, routes, rootDir, dist);
8891
+ await ensureSchemaGenerated(schemaPath, route.filePath, routes, rootDir, dist, wsRoutes);
8768
8892
  }
8769
8893
  }
8770
8894
  const result = await validateInput(schemaPath, route.method, inputType, input);
@@ -8772,7 +8896,7 @@ function createRoutePipeline(opts) {
8772
8896
  throw new ValidationError("\u53C2\u6570\u6821\u9A8C\u5931\u8D25", result.issues);
8773
8897
  }
8774
8898
  if (inputType === "query" && result.data !== input && typeof result.data === "object" && result.data !== null) {
8775
- ctx.__validatedQuery = {
8899
+ ctx.query = {
8776
8900
  ...input,
8777
8901
  ...result.data
8778
8902
  };
@@ -8794,7 +8918,7 @@ function createRoutePipeline(opts) {
8794
8918
  throw new ValidationError("\u53C2\u6570\u6821\u9A8C\u5931\u8D25", queryResult.issues);
8795
8919
  }
8796
8920
  if (queryResult.data !== rawQuery && typeof queryResult.data === "object" && queryResult.data !== null) {
8797
- ctx.__validatedQuery = {
8921
+ ctx.query = {
8798
8922
  ...rawQuery,
8799
8923
  ...queryResult.data
8800
8924
  };
@@ -8803,16 +8927,20 @@ function createRoutePipeline(opts) {
8803
8927
  let body;
8804
8928
  if (inputType === "query" && hasBody(route.method)) {
8805
8929
  const parsed = await resolveBodyForQueryMethod(request);
8806
- if (parsed !== void 0) {
8807
- const bodyResult = await validateInput(schemaPath, route.method, "body", parsed);
8808
- if (!bodyResult.valid) {
8809
- throw new ValidationError("\u53C2\u6570\u6821\u9A8C\u5931\u8D25", bodyResult.issues);
8810
- }
8811
- body = bodyResult.data;
8930
+ if (parsed.rawBody !== void 0) {
8931
+ ctx.rawBody = parsed.rawBody;
8932
+ }
8933
+ const bodyResult = await validateInput(schemaPath, route.method, "body", parsed.input);
8934
+ if (!bodyResult.valid) {
8935
+ throw new ValidationError("\u53C2\u6570\u6821\u9A8C\u5931\u8D25", bodyResult.issues);
8812
8936
  }
8937
+ body = bodyResult.data;
8813
8938
  } else if (hasBody(route.method)) {
8814
8939
  body = result.data;
8815
8940
  }
8941
+ if (body !== void 0 && body !== null) {
8942
+ ctx.body = body;
8943
+ }
8816
8944
  if (route.middlewares === void 0 && route.injectors === void 0 && route.middlewarePaths) {
8817
8945
  await ensureMiddlewaresCompiled(route.middlewarePaths, rootDir);
8818
8946
  const bundle = await loadMergedMiddlewares(route.middlewarePaths);
@@ -8848,25 +8976,37 @@ async function sendErrorResponse(err, meta, res, onError, ctx) {
8848
8976
  }
8849
8977
  }
8850
8978
  }
8851
- async function handleRequest(routes, rootDir, dist, req, res, outerMiddlewares, onError, config, globalInjectors, bodyLimit, trustedProxy, registries) {
8979
+ async function handleRequest(routes, rootDir, dist, req, res, outerMiddlewares, onError, config, globalInjectors, bodyLimit, trustedProxy, registries, wsRoutes) {
8852
8980
  let meta = { headers: {}, setCookies: [] };
8853
8981
  let ctx;
8982
+ let sseEarlySent = false;
8854
8983
  const abortController = new AbortController();
8855
8984
  res.on("close", () => {
8856
8985
  if (!res.writableEnded) abortController.abort();
8857
8986
  });
8858
8987
  try {
8988
+ const { request, url } = toWebRequest(req, bodyLimit, abortController.signal);
8989
+ const method = request.method.toUpperCase();
8990
+ const urlPath = url.pathname;
8991
+ const preMatched = matchRoute(routes, method, urlPath);
8859
8992
  const prepared = prepareRequest(
8860
8993
  req,
8994
+ request,
8995
+ url,
8861
8996
  config,
8862
- bodyLimit,
8863
8997
  trustedProxy,
8864
8998
  registries,
8865
- abortController.signal
8999
+ preMatched
8866
9000
  );
8867
9001
  ctx = prepared.ctx;
8868
9002
  meta = prepared.meta;
8869
- const { request, url, method, urlPath } = prepared;
9003
+ ctx.__earlyRespond = (response2) => {
9004
+ if (sseEarlySent) return;
9005
+ sseEarlySent = true;
9006
+ void sendNodeResponse(mergeMeta(response2, meta), res).catch(() => {
9007
+ if (!res.writableEnded) res.destroy();
9008
+ });
9009
+ };
8870
9010
  const routePipeline = createRoutePipeline({
8871
9011
  routes,
8872
9012
  method,
@@ -8876,12 +9016,25 @@ async function handleRequest(routes, rootDir, dist, req, res, outerMiddlewares,
8876
9016
  request,
8877
9017
  rootDir,
8878
9018
  dist,
8879
- globalInjectors
9019
+ globalInjectors,
9020
+ preMatched,
9021
+ wsRoutes
8880
9022
  });
8881
9023
  const response = outerMiddlewares.length > 0 ? await compose(outerMiddlewares, ctx, routePipeline) : await routePipeline();
8882
- await sendSuccessResponse(response, res);
9024
+ if (!sseEarlySent) {
9025
+ await sendSuccessResponse(response, res);
9026
+ }
8883
9027
  } catch (err) {
8884
9028
  if (res.destroyed || res.writableEnded) return;
9029
+ if (sseEarlySent) {
9030
+ if (onError && ctx) {
9031
+ try {
9032
+ await onError(err, ctx);
9033
+ } catch {
9034
+ }
9035
+ }
9036
+ return;
9037
+ }
8885
9038
  if (err instanceof PayloadTooLargeError) {
8886
9039
  res.setHeader("Connection", "close");
8887
9040
  res.once("finish", () => req.destroy());
@@ -10309,14 +10462,14 @@ async function createDevApp(options) {
10309
10462
  void (async () => {
10310
10463
  try {
10311
10464
  const { generateSchemaFiles: generateSchemaFiles2 } = await Promise.resolve().then(() => (init_generateSchemaFiles(), generateSchemaFiles_exports));
10312
- await generateSchemaFiles2(sorted, ctx.rootDir, ctx.dist);
10465
+ await generateSchemaFiles2(sorted, ctx.rootDir, ctx.dist, reScanned.wsRoutes);
10313
10466
  } catch (err) {
10314
10467
  console.error("[faapi] Background schema regeneration failed:", err);
10315
10468
  }
10316
10469
  })();
10317
10470
  } else {
10318
10471
  const { generateSchemaFiles: generateSchemaFiles2 } = await Promise.resolve().then(() => (init_generateSchemaFiles(), generateSchemaFiles_exports));
10319
- await generateSchemaFiles2(sorted, ctx.rootDir, ctx.dist);
10472
+ await generateSchemaFiles2(sorted, ctx.rootDir, ctx.dist, reScanned.wsRoutes);
10320
10473
  }
10321
10474
  ctx.updateRoutes(sorted, reScanned.wsRoutes);
10322
10475
  };
@@ -10551,7 +10704,7 @@ async function buildCommand(options) {
10551
10704
  console.log(` Found ${sorted.length} routes, ${wsRoutes.length} WS routes`);
10552
10705
  reportRouteConflicts(sorted);
10553
10706
  console.log("\n[4/8] Generating schema...");
10554
- await generateSchemaFiles(sorted, rootDir, outdir);
10707
+ await generateSchemaFiles(sorted, rootDir, outdir, wsRoutes);
10555
10708
  console.log(` Schema: zod.js files under ${path33.resolve(rootDir, outdir)}`);
10556
10709
  console.log("\n[5/8] Generating routes manifest...");
10557
10710
  const routesPath = path33.resolve(rootDir, outdir, "faapi-routes.js");