@faapi/faapi 6.19.0 → 6.21.0

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 = [];
@@ -2617,6 +2645,50 @@ function collectRouteSchemaSources(routes, rootDir) {
2617
2645
  typeInfo: resolver.resolve(paramsArg.typeName) ?? null
2618
2646
  });
2619
2647
  }
2648
+ if (inputType === "body") {
2649
+ const queryArg = meta.params.find((p) => p.type === "query");
2650
+ if (queryArg?.typeName) {
2651
+ sources.push({
2652
+ urlPath: entry.urlPath,
2653
+ filePath,
2654
+ schemaName: getSchemaName(method, "query"),
2655
+ typeInfo: resolver.resolve(queryArg.typeName) ?? null
2656
+ });
2657
+ }
2658
+ } else if (hasBody(method)) {
2659
+ const bodyArg = meta.params.find((p) => p.type === "body") ?? meta.params.find((p) => p.type === "form");
2660
+ if (bodyArg?.typeName) {
2661
+ sources.push({
2662
+ urlPath: entry.urlPath,
2663
+ filePath,
2664
+ schemaName: getSchemaName(method, "body"),
2665
+ typeInfo: resolver.resolve(bodyArg.typeName) ?? null,
2666
+ coerce: bodyArg.type === "form" || void 0
2667
+ });
2668
+ }
2669
+ }
2670
+ }
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
+ });
2620
2692
  }
2621
2693
  }
2622
2694
  return { sources, resolversByFile };
@@ -2705,9 +2777,9 @@ function generateSchemaFileSource(sources, resolveType, helpersImportPath) {
2705
2777
  lines.push(...schemaBlocks);
2706
2778
  return lines.join("\n").replace(/\n+$/, "\n");
2707
2779
  }
2708
- async function generateSchemaFiles(routes, rootDir, dist) {
2709
- if (routes.length === 0) return;
2710
- 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);
2711
2783
  await generateZodArtifacts(sources, {
2712
2784
  generateFileSource: generateSchemaFileSource,
2713
2785
  resolversByFile,
@@ -2778,14 +2850,14 @@ var init_generateZodArtifacts = __esm({
2778
2850
  });
2779
2851
 
2780
2852
  // src/ast/jsDocMetadata.ts
2781
- import ts8 from "typescript";
2853
+ import ts9 from "typescript";
2782
2854
  function hasExportModifier(node) {
2783
- if (!ts8.canHaveModifiers(node)) return false;
2784
- const modifiers = ts8.getModifiers(node);
2785
- 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);
2786
2858
  }
2787
2859
  function getJSDocFromNode(node) {
2788
- const apiDocs = ts8.getJSDocCommentsAndTags(node).filter((entry) => ts8.isJSDoc(entry));
2860
+ const apiDocs = ts9.getJSDocCommentsAndTags(node).filter((entry) => ts9.isJSDoc(entry));
2789
2861
  if (apiDocs.length > 0) return apiDocs[0];
2790
2862
  const directDocs = node.jsDoc;
2791
2863
  if (directDocs && directDocs.length > 0) return directDocs[0];
@@ -2816,7 +2888,7 @@ var init_jsDocMetadata = __esm({
2816
2888
  });
2817
2889
 
2818
2890
  // src/ast/extractToolMetadata.ts
2819
- import ts9 from "typescript";
2891
+ import ts10 from "typescript";
2820
2892
  function extractToolMetadata(program, filePath, functionName, pathMeta) {
2821
2893
  const sourceFile = program.getSourceFile(filePath);
2822
2894
  if (!sourceFile) return null;
@@ -2837,18 +2909,18 @@ function extractToolMetadata(program, filePath, functionName, pathMeta) {
2837
2909
  }
2838
2910
  function findExportedFunction(sourceFile, functionName) {
2839
2911
  let result = null;
2840
- ts9.forEachChild(sourceFile, (node) => {
2912
+ ts10.forEachChild(sourceFile, (node) => {
2841
2913
  if (result) return;
2842
- if (ts9.isFunctionDeclaration(node) && hasExportModifier(node) && node.name?.text === functionName) {
2914
+ if (ts10.isFunctionDeclaration(node) && hasExportModifier(node) && node.name?.text === functionName) {
2843
2915
  result = { fn: node, jsDocOwner: node };
2844
2916
  return;
2845
2917
  }
2846
- if (ts9.isVariableStatement(node) && hasExportModifier(node)) {
2918
+ if (ts10.isVariableStatement(node) && hasExportModifier(node)) {
2847
2919
  for (const decl of node.declarationList.declarations) {
2848
2920
  if (result) break;
2849
- 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);
2850
2922
  if (nameText !== functionName || !decl.initializer) continue;
2851
- if (ts9.isArrowFunction(decl.initializer) || ts9.isFunctionExpression(decl.initializer)) {
2923
+ if (ts10.isArrowFunction(decl.initializer) || ts10.isFunctionExpression(decl.initializer)) {
2852
2924
  result = { fn: decl.initializer, jsDocOwner: node };
2853
2925
  }
2854
2926
  }
@@ -2860,7 +2932,7 @@ function getFirstParamTypeName(fn, sourceFile) {
2860
2932
  const firstParam = fn.parameters[0];
2861
2933
  if (!firstParam) return void 0;
2862
2934
  if (!firstParam.type) return void 0;
2863
- if (!ts9.isTypeReferenceNode(firstParam.type)) return void 0;
2935
+ if (!ts10.isTypeReferenceNode(firstParam.type)) return void 0;
2864
2936
  return firstParam.type.typeName.getText(sourceFile);
2865
2937
  }
2866
2938
  var init_extractToolMetadata = __esm({
@@ -3070,7 +3142,7 @@ var init_scanAgents = __esm({
3070
3142
  });
3071
3143
 
3072
3144
  // src/ast/extractAgentMetadata.ts
3073
- import ts10 from "typescript";
3145
+ import ts11 from "typescript";
3074
3146
  function extractAgentMetadata(program, filePath, pathMeta) {
3075
3147
  const sourceFile = program.getSourceFile(filePath);
3076
3148
  if (!sourceFile) return null;
@@ -3114,23 +3186,23 @@ function extractAgentMetadata(program, filePath, pathMeta) {
3114
3186
  }
3115
3187
  function findConfigExport(sourceFile) {
3116
3188
  let result = null;
3117
- ts10.forEachChild(sourceFile, (node) => {
3189
+ ts11.forEachChild(sourceFile, (node) => {
3118
3190
  if (result) return;
3119
- if (ts10.isVariableStatement(node) && hasExportModifier(node)) {
3191
+ if (ts11.isVariableStatement(node) && hasExportModifier(node)) {
3120
3192
  for (const decl of node.declarationList.declarations) {
3121
3193
  if (result) break;
3122
- const nameText = ts10.isIdentifier(decl.name) ? decl.name.text : "";
3194
+ const nameText = ts11.isIdentifier(decl.name) ? decl.name.text : "";
3123
3195
  if (nameText !== "config" || !decl.initializer) continue;
3124
- if (ts10.isObjectLiteralExpression(decl.initializer)) {
3196
+ if (ts11.isObjectLiteralExpression(decl.initializer)) {
3125
3197
  result = { jsDocOwner: node, objectLiteral: decl.initializer };
3126
- } else if (ts10.isArrowFunction(decl.initializer)) {
3198
+ } else if (ts11.isArrowFunction(decl.initializer)) {
3127
3199
  result = { jsDocOwner: node, objectLiteral: getReturnObjectLiteral(decl.initializer) };
3128
3200
  } else {
3129
3201
  result = { jsDocOwner: node, objectLiteral: null };
3130
3202
  }
3131
3203
  }
3132
3204
  }
3133
- if (ts10.isFunctionDeclaration(node) && hasExportModifier(node) && node.name?.text === "config") {
3205
+ if (ts11.isFunctionDeclaration(node) && hasExportModifier(node) && node.name?.text === "config") {
3134
3206
  const returnObj = getReturnObjectLiteral(node);
3135
3207
  result = { jsDocOwner: node, objectLiteral: returnObj };
3136
3208
  }
@@ -3140,12 +3212,12 @@ function findConfigExport(sourceFile) {
3140
3212
  function getReturnObjectLiteral(fn) {
3141
3213
  const body = fn.body;
3142
3214
  if (!body) return null;
3143
- if (ts10.isObjectLiteralExpression(body)) {
3215
+ if (ts11.isObjectLiteralExpression(body)) {
3144
3216
  return body;
3145
3217
  }
3146
- if (ts10.isBlock(body)) {
3218
+ if (ts11.isBlock(body)) {
3147
3219
  for (const stmt of body.statements) {
3148
- if (ts10.isReturnStatement(stmt) && stmt.expression && ts10.isObjectLiteralExpression(stmt.expression)) {
3220
+ if (ts11.isReturnStatement(stmt) && stmt.expression && ts11.isObjectLiteralExpression(stmt.expression)) {
3149
3221
  return stmt.expression;
3150
3222
  }
3151
3223
  }
@@ -3160,8 +3232,8 @@ function extractConfigFields(objLit, sourceFile) {
3160
3232
  let maxTurns;
3161
3233
  let inputDescription;
3162
3234
  for (const prop of objLit.properties) {
3163
- if (ts10.isSpreadAssignment(prop)) continue;
3164
- if (!ts10.isPropertyAssignment(prop)) {
3235
+ if (ts11.isSpreadAssignment(prop)) continue;
3236
+ if (!ts11.isPropertyAssignment(prop)) {
3165
3237
  throw SchemaExtractionError.at(
3166
3238
  prop,
3167
3239
  "config",
@@ -3212,13 +3284,13 @@ function extractConfigFields(objLit, sourceFile) {
3212
3284
  return { systemPrompt, tools, agents, model, maxTurns, inputDescription };
3213
3285
  }
3214
3286
  function getPropertyName(name) {
3215
- if (ts10.isIdentifier(name)) return name.text;
3216
- if (ts10.isStringLiteral(name)) return name.text;
3287
+ if (ts11.isIdentifier(name)) return name.text;
3288
+ if (ts11.isStringLiteral(name)) return name.text;
3217
3289
  return null;
3218
3290
  }
3219
3291
  function extractStringValue(expr) {
3220
3292
  if (isStringLikeLiteral(expr)) return expr.text;
3221
- if (ts10.isBinaryExpression(expr) && expr.operatorToken.kind === ts10.SyntaxKind.PlusToken) {
3293
+ if (ts11.isBinaryExpression(expr) && expr.operatorToken.kind === ts11.SyntaxKind.PlusToken) {
3222
3294
  const left = extractStringValue(expr.left);
3223
3295
  if (left === void 0) return void 0;
3224
3296
  const right = extractStringValue(expr.right);
@@ -3264,17 +3336,17 @@ function requireNumberValue(prop, fieldName, sourceFile) {
3264
3336
  return value;
3265
3337
  }
3266
3338
  function extractNumberValue(expr) {
3267
- if (ts10.isNumericLiteral(expr)) {
3339
+ if (ts11.isNumericLiteral(expr)) {
3268
3340
  const num = Number(expr.text);
3269
3341
  return Number.isNaN(num) ? void 0 : num;
3270
3342
  }
3271
3343
  return void 0;
3272
3344
  }
3273
3345
  function isStringLikeLiteral(node) {
3274
- return ts10.isStringLiteral(node) || ts10.isNoSubstitutionTemplateLiteral(node);
3346
+ return ts11.isStringLiteral(node) || ts11.isNoSubstitutionTemplateLiteral(node);
3275
3347
  }
3276
3348
  function extractStringArrayValue(expr) {
3277
- if (!ts10.isArrayLiteralExpression(expr)) return void 0;
3349
+ if (!ts11.isArrayLiteralExpression(expr)) return void 0;
3278
3350
  const values = [];
3279
3351
  for (const element of expr.elements) {
3280
3352
  const value = extractStringValue(element);
@@ -3762,7 +3834,7 @@ function clearGeneratedSchemas() {
3762
3834
  state.generatedSchemas.clear();
3763
3835
  state.inFlightSchemaGenerations.clear();
3764
3836
  }
3765
- async function ensureSchemaGenerated(schemaPath, routeFilePath, routes, rootDir, dist) {
3837
+ async function ensureSchemaGenerated(schemaPath, routeFilePath, routes, rootDir, dist, wsRoutes) {
3766
3838
  const inFlight2 = state.inFlightSchemaGenerations.get(schemaPath);
3767
3839
  if (inFlight2) {
3768
3840
  await inFlight2;
@@ -3781,13 +3853,15 @@ async function ensureSchemaGenerated(schemaPath, routeFilePath, routes, rootDir,
3781
3853
  return false;
3782
3854
  }
3783
3855
  const fileRoutes = routes.filter((r) => r.filePath === routeFilePath);
3784
- if (fileRoutes.length === 0) {
3856
+ const fileWsRoutes = (wsRoutes ?? []).filter((r) => r.filePath === routeFilePath);
3857
+ if (fileRoutes.length === 0 && fileWsRoutes.length === 0) {
3785
3858
  return false;
3786
3859
  }
3787
3860
  const sourceRelPath = path20.relative(rootDir, sourceAbsPath).replace(/\\/g, "/");
3788
3861
  const sourceRoutes = fileRoutes.map((r) => ({ ...r, filePath: sourceRelPath }));
3862
+ const sourceWsRoutes = fileWsRoutes.map((r) => ({ ...r, filePath: sourceRelPath }));
3789
3863
  const generatePromise = (async () => {
3790
- await generateSchemaFiles(sourceRoutes, rootDir, dist);
3864
+ await generateSchemaFiles(sourceRoutes, rootDir, dist, sourceWsRoutes);
3791
3865
  state.generatedSchemas.add(schemaPath);
3792
3866
  })();
3793
3867
  state.inFlightSchemaGenerations.set(schemaPath, generatePromise);
@@ -6292,17 +6366,24 @@ function encodeSseEvent(event) {
6292
6366
  out += "\n";
6293
6367
  return out;
6294
6368
  }
6295
- function createSseWriter() {
6369
+ function createSseWriter(options = {}) {
6370
+ const { onFirstWrite } = options;
6296
6371
  const encoder = new TextEncoder();
6297
6372
  let controller = null;
6298
6373
  let closed = false;
6299
6374
  let aborted = false;
6375
+ let firstWriteTriggered = false;
6300
6376
  let drainWaiters = [];
6301
6377
  const resolveDrainWaiters = () => {
6302
6378
  const waiters = drainWaiters;
6303
6379
  drainWaiters = [];
6304
6380
  for (const resolve3 of waiters) resolve3();
6305
6381
  };
6382
+ const triggerFirstWrite = () => {
6383
+ if (firstWriteTriggered) return;
6384
+ firstWriteTriggered = true;
6385
+ onFirstWrite?.(response);
6386
+ };
6306
6387
  const stream = new ReadableStream(
6307
6388
  {
6308
6389
  start(c) {
@@ -6336,11 +6417,13 @@ function createSseWriter() {
6336
6417
  if (closed || !controller) return;
6337
6418
  const text = encodeSseEvent(event);
6338
6419
  controller.enqueue(encoder.encode(text));
6420
+ triggerFirstWrite();
6339
6421
  },
6340
6422
  sendRaw(chunk) {
6341
6423
  if (closed || !controller) return;
6342
6424
  const bytes = typeof chunk === "string" ? encoder.encode(chunk) : chunk;
6343
6425
  controller.enqueue(bytes);
6426
+ triggerFirstWrite();
6344
6427
  },
6345
6428
  sendError(error) {
6346
6429
  if (closed || !controller) return;
@@ -6348,6 +6431,7 @@ function createSseWriter() {
6348
6431
  const text = encodeSseEvent({ event: "error", data: message });
6349
6432
  try {
6350
6433
  controller.enqueue(encoder.encode(text));
6434
+ triggerFirstWrite();
6351
6435
  } finally {
6352
6436
  writer.close();
6353
6437
  }
@@ -6795,6 +6879,32 @@ var init_logger = __esm({
6795
6879
  }
6796
6880
  });
6797
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
+
6798
6908
  // src/runtime/createContext.ts
6799
6909
  function resolveRequestId(request) {
6800
6910
  const header = request.headers.get("x-request-id");
@@ -6844,7 +6954,11 @@ function createContextFromUrl(request, url, params, config = {}, ip = "", regist
6844
6954
  log,
6845
6955
  request,
6846
6956
  params,
6847
- 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,
6848
6962
  headers: request.headers,
6849
6963
  method: request.method,
6850
6964
  path: url.pathname,
@@ -6892,13 +7006,19 @@ function createContextFromUrl(request, url, params, config = {}, ip = "", regist
6892
7006
  /**
6893
7007
  * 创建 SSE writer,用于流式推送事件
6894
7008
  *
6895
- * handler 调用此方法后,通过返回的 writer 推送事件,框架自动把 writer.response
6896
- * 作为 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 返回后交出。
6897
7013
  *
6898
7014
  * 与 ctx.json / ctx.html 互斥:一个 handler 只能用一种响应方式。
6899
7015
  */
6900
7016
  sse() {
6901
- const writer = createSseWriter();
7017
+ const writer = createSseWriter({
7018
+ onFirstWrite: (response) => {
7019
+ ctx.__earlyRespond?.(response);
7020
+ }
7021
+ });
6902
7022
  const ctxWithSse = ctx;
6903
7023
  ctxWithSse.__sseResponse = writer.response;
6904
7024
  ctxWithSse.__sseWriter = writer;
@@ -6950,32 +7070,7 @@ var init_createContext = __esm({
6950
7070
  init_sse();
6951
7071
  init_responseFormatter();
6952
7072
  init_logger();
6953
- }
6954
- });
6955
-
6956
- // src/utils/queryToObject.ts
6957
- function queryToObject(params) {
6958
- const cached = queryCache.get(params);
6959
- if (cached) return cached;
6960
- const result = {};
6961
- for (const [key, value] of params) {
6962
- const existing = result[key];
6963
- if (existing === void 0) {
6964
- result[key] = value;
6965
- } else if (Array.isArray(existing)) {
6966
- existing.push(value);
6967
- } else {
6968
- result[key] = [existing, value];
6969
- }
6970
- }
6971
- queryCache.set(params, result);
6972
- return result;
6973
- }
6974
- var queryCache;
6975
- var init_queryToObject = __esm({
6976
- "src/utils/queryToObject.ts"() {
6977
- "use strict";
6978
- queryCache = /* @__PURE__ */ new WeakMap();
7073
+ init_queryToObject();
6979
7074
  }
6980
7075
  });
6981
7076
 
@@ -7033,46 +7128,29 @@ var init_parseMultipart = __esm({
7033
7128
  async function resolveInputFromUrl(method, request, url) {
7034
7129
  const inputType = getInputTypeForMethod(method);
7035
7130
  if (inputType === "body") {
7036
- const contentType = request.headers.get("content-type") ?? "";
7037
- if (contentType.includes("multipart/form-data")) {
7038
- return parseMultipart(request);
7039
- }
7040
- if (contentType.includes("application/x-www-form-urlencoded")) {
7041
- const text2 = await request.text();
7042
- if (isBlankText(text2)) return null;
7043
- const params = new URLSearchParams(text2);
7044
- const obj = {};
7045
- for (const [key, value] of params) {
7046
- obj[key] = value;
7047
- }
7048
- return obj;
7049
- }
7050
- const text = await request.text();
7051
- if (isBlankText(text)) {
7052
- return null;
7053
- }
7054
- const result = parseJsonBody(text);
7055
- if (!result.success) {
7056
- throw new ValidationError("\u8BF7\u6C42\u4F53\u4E0D\u662F\u5408\u6CD5\u7684 JSON", [
7057
- {
7058
- path: "body",
7059
- code: "INVALID_FORMAT",
7060
- expected: "JSON",
7061
- received: "text",
7062
- message: "\u8BF7\u6C42\u4F53\u4E0D\u662F\u5408\u6CD5\u7684 JSON"
7063
- }
7064
- ]);
7065
- }
7066
- return result.data;
7131
+ return parseBodyByContentType(request);
7067
7132
  }
7068
- return queryToObject(url.searchParams);
7133
+ return { input: queryToObject(url.searchParams) };
7069
7134
  }
7070
- function isBlankText(text) {
7071
- return text.length === 0 || !/\S/.test(text);
7072
- }
7073
- 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
+ }
7074
7150
  const text = await request.text();
7075
- if (isBlankText(text)) return void 0;
7151
+ if (isBlankText(text)) {
7152
+ return { input: null };
7153
+ }
7076
7154
  const result = parseJsonBody(text);
7077
7155
  if (!result.success) {
7078
7156
  throw new ValidationError("\u8BF7\u6C42\u4F53\u4E0D\u662F\u5408\u6CD5\u7684 JSON", [
@@ -7085,7 +7163,13 @@ async function resolveBodyForQueryMethod(request) {
7085
7163
  }
7086
7164
  ]);
7087
7165
  }
7088
- 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);
7089
7173
  }
7090
7174
  var init_resolveInput = __esm({
7091
7175
  "src/runtime/resolveInput.ts"() {
@@ -7483,12 +7567,17 @@ var init_agentHandle = __esm({
7483
7567
  // src/injection/injectParams.ts
7484
7568
  function getBuiltinInjectionValue(type, ctx, body) {
7485
7569
  switch (type) {
7486
- case "query": {
7487
- const validated = ctx.__validatedQuery;
7488
- return validated ?? queryToObject(ctx.query);
7489
- }
7570
+ case "query":
7571
+ return ctx.query;
7490
7572
  case "params":
7491
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;
7492
7581
  case "headers":
7493
7582
  return ctx.headers;
7494
7583
  case "context":
@@ -7554,7 +7643,6 @@ var init_injectParams = __esm({
7554
7643
  "src/injection/injectParams.ts"() {
7555
7644
  "use strict";
7556
7645
  init_resolveInjection();
7557
- init_queryToObject();
7558
7646
  init_agentRegistry();
7559
7647
  init_agentHandle();
7560
7648
  }
@@ -8402,6 +8490,7 @@ function attachWebSocket(options) {
8402
8490
  server,
8403
8491
  routesRef,
8404
8492
  rootDir,
8493
+ dist,
8405
8494
  config,
8406
8495
  globalMiddlewares,
8407
8496
  trustedProxy = false,
@@ -8436,12 +8525,50 @@ function attachWebSocket(options) {
8436
8525
  const { route, params } = match;
8437
8526
  const headers = nodeHttpToWebHeaders(req);
8438
8527
  const host = req.headers.host ?? "localhost";
8439
- const url = `http://${host}${req.url ?? "/"}`;
8440
- 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 });
8441
8530
  const ctx = createContext(request, params, config, getClientIp(req, trustedProxy), registries);
8442
8531
  const meta = ctx.meta;
8443
8532
  let upgraded = false;
8444
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
+ }
8445
8572
  let handlers;
8446
8573
  try {
8447
8574
  const absoluteFilePath = path25.resolve(rootDir, route.filePath);
@@ -8505,7 +8632,11 @@ var init_handleWsUpgrade = __esm({
8505
8632
  init_invokeHandler();
8506
8633
  init_importWithCacheBust();
8507
8634
  init_getClientIp();
8635
+ init_queryToObject();
8508
8636
  init_serverUtils();
8637
+ init_validateInput();
8638
+ init_httpErrors();
8639
+ init_generateSchemaFiles();
8509
8640
  init_compileOnDemand();
8510
8641
  init_loadMiddlewares();
8511
8642
  init_pendingMeta();
@@ -8669,7 +8800,8 @@ function createServer(options) {
8669
8800
  globalInjectors,
8670
8801
  bodyLimit,
8671
8802
  trustedProxy,
8672
- registries
8803
+ registries,
8804
+ routesRef.wsCurrent
8673
8805
  ).catch((err) => {
8674
8806
  console.error("[faapi] Request pipeline failed after error response attempt:", err);
8675
8807
  try {
@@ -8687,6 +8819,7 @@ function createServer(options) {
8687
8819
  server,
8688
8820
  routesRef,
8689
8821
  rootDir,
8822
+ dist,
8690
8823
  config,
8691
8824
  globalMiddlewares,
8692
8825
  trustedProxy,
@@ -8694,14 +8827,13 @@ function createServer(options) {
8694
8827
  });
8695
8828
  return { server, routesRef };
8696
8829
  }
8697
- function prepareRequest(req, config, bodyLimit, trustedProxy, registries, requestSignal) {
8698
- const { request, url } = toWebRequest(req, bodyLimit, requestSignal);
8830
+ function prepareRequest(req, request, url, config, trustedProxy, registries, preMatched) {
8699
8831
  const method = request.method.toUpperCase();
8700
8832
  const urlPath = url.pathname;
8701
8833
  const ctx = createContextFromUrl(
8702
8834
  request,
8703
8835
  url,
8704
- {},
8836
+ preMatched?.params ?? {},
8705
8837
  config,
8706
8838
  getClientIp(req, trustedProxy),
8707
8839
  registries
@@ -8730,19 +8862,33 @@ function getRoutePaths(route, rootDir, dist) {
8730
8862
  return cached;
8731
8863
  }
8732
8864
  function createRoutePipeline(opts) {
8733
- 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;
8734
8878
  return async () => {
8735
- const match = resolveRouteOrThrow(routes, method, urlPath);
8736
- ctx.params = match.params;
8879
+ const match = preMatched ?? resolveRouteOrThrow(routes, method, urlPath);
8737
8880
  const { route } = match;
8738
8881
  const { absFilePath, schemaPath } = getRoutePaths(route, rootDir, dist);
8739
8882
  const routeModule = await loadRouteModule(absFilePath, route.method, rootDir);
8740
- 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
+ }
8741
8887
  const inputType = getInputTypeForMethod(route.method);
8742
8888
  if (isDevOnDemandEnabled()) {
8743
8889
  const devDist = getDevDist();
8744
8890
  if (devDist) {
8745
- await ensureSchemaGenerated(schemaPath, route.filePath, routes, rootDir, dist);
8891
+ await ensureSchemaGenerated(schemaPath, route.filePath, routes, rootDir, dist, wsRoutes);
8746
8892
  }
8747
8893
  }
8748
8894
  const result = await validateInput(schemaPath, route.method, inputType, input);
@@ -8750,7 +8896,7 @@ function createRoutePipeline(opts) {
8750
8896
  throw new ValidationError("\u53C2\u6570\u6821\u9A8C\u5931\u8D25", result.issues);
8751
8897
  }
8752
8898
  if (inputType === "query" && result.data !== input && typeof result.data === "object" && result.data !== null) {
8753
- ctx.__validatedQuery = {
8899
+ ctx.query = {
8754
8900
  ...input,
8755
8901
  ...result.data
8756
8902
  };
@@ -8765,7 +8911,36 @@ function createRoutePipeline(opts) {
8765
8911
  ...paramsResult.data
8766
8912
  } : match.params;
8767
8913
  }
8768
- const body = inputType === "query" && hasBody(route.method) ? await resolveBodyForQueryMethod(request) : hasBody(route.method) ? result.data : void 0;
8914
+ if (inputType === "body") {
8915
+ const rawQuery = queryToObject(url.searchParams);
8916
+ const queryResult = await validateInput(schemaPath, route.method, "query", rawQuery);
8917
+ if (!queryResult.valid) {
8918
+ throw new ValidationError("\u53C2\u6570\u6821\u9A8C\u5931\u8D25", queryResult.issues);
8919
+ }
8920
+ if (queryResult.data !== rawQuery && typeof queryResult.data === "object" && queryResult.data !== null) {
8921
+ ctx.query = {
8922
+ ...rawQuery,
8923
+ ...queryResult.data
8924
+ };
8925
+ }
8926
+ }
8927
+ let body;
8928
+ if (inputType === "query" && hasBody(route.method)) {
8929
+ const parsed = await resolveBodyForQueryMethod(request);
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);
8936
+ }
8937
+ body = bodyResult.data;
8938
+ } else if (hasBody(route.method)) {
8939
+ body = result.data;
8940
+ }
8941
+ if (body !== void 0 && body !== null) {
8942
+ ctx.body = body;
8943
+ }
8769
8944
  if (route.middlewares === void 0 && route.injectors === void 0 && route.middlewarePaths) {
8770
8945
  await ensureMiddlewaresCompiled(route.middlewarePaths, rootDir);
8771
8946
  const bundle = await loadMergedMiddlewares(route.middlewarePaths);
@@ -8801,25 +8976,37 @@ async function sendErrorResponse(err, meta, res, onError, ctx) {
8801
8976
  }
8802
8977
  }
8803
8978
  }
8804
- 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) {
8805
8980
  let meta = { headers: {}, setCookies: [] };
8806
8981
  let ctx;
8982
+ let sseEarlySent = false;
8807
8983
  const abortController = new AbortController();
8808
8984
  res.on("close", () => {
8809
8985
  if (!res.writableEnded) abortController.abort();
8810
8986
  });
8811
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);
8812
8992
  const prepared = prepareRequest(
8813
8993
  req,
8994
+ request,
8995
+ url,
8814
8996
  config,
8815
- bodyLimit,
8816
8997
  trustedProxy,
8817
8998
  registries,
8818
- abortController.signal
8999
+ preMatched
8819
9000
  );
8820
9001
  ctx = prepared.ctx;
8821
9002
  meta = prepared.meta;
8822
- 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
+ };
8823
9010
  const routePipeline = createRoutePipeline({
8824
9011
  routes,
8825
9012
  method,
@@ -8829,12 +9016,25 @@ async function handleRequest(routes, rootDir, dist, req, res, outerMiddlewares,
8829
9016
  request,
8830
9017
  rootDir,
8831
9018
  dist,
8832
- globalInjectors
9019
+ globalInjectors,
9020
+ preMatched,
9021
+ wsRoutes
8833
9022
  });
8834
9023
  const response = outerMiddlewares.length > 0 ? await compose(outerMiddlewares, ctx, routePipeline) : await routePipeline();
8835
- await sendSuccessResponse(response, res);
9024
+ if (!sseEarlySent) {
9025
+ await sendSuccessResponse(response, res);
9026
+ }
8836
9027
  } catch (err) {
8837
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
+ }
8838
9038
  if (err instanceof PayloadTooLargeError) {
8839
9039
  res.setHeader("Connection", "close");
8840
9040
  res.once("finish", () => req.destroy());
@@ -8855,6 +9055,7 @@ var init_createServer = __esm({
8855
9055
  init_httpErrors();
8856
9056
  init_validateInput();
8857
9057
  init_inputType();
9058
+ init_queryToObject();
8858
9059
  init_getClientIp();
8859
9060
  init_cors();
8860
9061
  init_helmet();
@@ -10261,14 +10462,14 @@ async function createDevApp(options) {
10261
10462
  void (async () => {
10262
10463
  try {
10263
10464
  const { generateSchemaFiles: generateSchemaFiles2 } = await Promise.resolve().then(() => (init_generateSchemaFiles(), generateSchemaFiles_exports));
10264
- await generateSchemaFiles2(sorted, ctx.rootDir, ctx.dist);
10465
+ await generateSchemaFiles2(sorted, ctx.rootDir, ctx.dist, reScanned.wsRoutes);
10265
10466
  } catch (err) {
10266
10467
  console.error("[faapi] Background schema regeneration failed:", err);
10267
10468
  }
10268
10469
  })();
10269
10470
  } else {
10270
10471
  const { generateSchemaFiles: generateSchemaFiles2 } = await Promise.resolve().then(() => (init_generateSchemaFiles(), generateSchemaFiles_exports));
10271
- await generateSchemaFiles2(sorted, ctx.rootDir, ctx.dist);
10472
+ await generateSchemaFiles2(sorted, ctx.rootDir, ctx.dist, reScanned.wsRoutes);
10272
10473
  }
10273
10474
  ctx.updateRoutes(sorted, reScanned.wsRoutes);
10274
10475
  };
@@ -10503,7 +10704,7 @@ async function buildCommand(options) {
10503
10704
  console.log(` Found ${sorted.length} routes, ${wsRoutes.length} WS routes`);
10504
10705
  reportRouteConflicts(sorted);
10505
10706
  console.log("\n[4/8] Generating schema...");
10506
- await generateSchemaFiles(sorted, rootDir, outdir);
10707
+ await generateSchemaFiles(sorted, rootDir, outdir, wsRoutes);
10507
10708
  console.log(` Schema: zod.js files under ${path33.resolve(rootDir, outdir)}`);
10508
10709
  console.log("\n[5/8] Generating routes manifest...");
10509
10710
  const routesPath = path33.resolve(rootDir, outdir, "faapi-routes.js");