@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/index.js CHANGED
@@ -1124,8 +1124,16 @@ var init_resolveInjection = __esm({
1124
1124
  // Phase 2.3
1125
1125
  tasks: "tasks",
1126
1126
  // 任务子系统:TaskClient(入队/查询)
1127
- log: "log"
1127
+ log: "log",
1128
1128
  // 请求级日志器(ctx.log 同一实例:scope http,自动带 requestId 等字段)
1129
+ // raw 系:恒原始输入(无 schema、无校验、管线永不转换),与 query/params/body
1130
+ // 的「校验转换后」口径二分互补
1131
+ rawQuery: "rawQuery",
1132
+ // URLSearchParams(原始查询参数)
1133
+ rawParams: "rawParams",
1134
+ // Record<string, string>(URL 原始路径段)
1135
+ rawBody: "rawBody"
1136
+ // string | undefined(请求体原始文本)
1129
1137
  };
1130
1138
  injectionCache = /* @__PURE__ */ new WeakMap();
1131
1139
  }
@@ -1183,7 +1191,19 @@ var init_analyzeInjection = __esm({
1183
1191
 
1184
1192
  // src/cli/collectRouteSchemaSources.ts
1185
1193
  import path2 from "path";
1186
- function collectRouteSchemaSources(routes, rootDir) {
1194
+ import ts6 from "typescript";
1195
+ function findExportedTypeName(sourceFile, name) {
1196
+ for (const statement of sourceFile.statements) {
1197
+ const modifiers = ts6.canHaveModifiers(statement) ? ts6.getModifiers(statement) : void 0;
1198
+ const isExported = modifiers?.some((m) => m.kind === ts6.SyntaxKind.ExportKeyword);
1199
+ if (!isExported) continue;
1200
+ if ((ts6.isInterfaceDeclaration(statement) || ts6.isTypeAliasDeclaration(statement)) && statement.name?.text === name) {
1201
+ return true;
1202
+ }
1203
+ }
1204
+ return false;
1205
+ }
1206
+ function collectRouteSchemaSources(routes, rootDir, wsRoutes) {
1187
1207
  const methodsByFile = /* @__PURE__ */ new Map();
1188
1208
  for (const route of routes) {
1189
1209
  const filePath = rootDir ? path2.resolve(rootDir, route.filePath) : route.filePath;
@@ -1194,9 +1214,17 @@ function collectRouteSchemaSources(routes, rootDir) {
1194
1214
  }
1195
1215
  entry.methods.add(route.method);
1196
1216
  }
1197
- const programByFile = createPrograms([...methodsByFile.keys()]);
1217
+ const wsFiles = [];
1218
+ for (const wsRoute of wsRoutes ?? []) {
1219
+ wsFiles.push({
1220
+ filePath: rootDir ? path2.resolve(rootDir, wsRoute.filePath) : wsRoute.filePath,
1221
+ urlPath: wsRoute.urlPath
1222
+ });
1223
+ }
1224
+ const allFiles = [.../* @__PURE__ */ new Set([...methodsByFile.keys(), ...wsFiles.map((f) => f.filePath)])];
1225
+ const programByFile = createPrograms(allFiles);
1198
1226
  const resolversByFile = /* @__PURE__ */ new Map();
1199
- for (const filePath of methodsByFile.keys()) {
1227
+ for (const filePath of allFiles) {
1200
1228
  resolversByFile.set(filePath, createLazyTypeResolver(programByFile.get(filePath), filePath));
1201
1229
  }
1202
1230
  const sources = [];
@@ -1228,6 +1256,50 @@ function collectRouteSchemaSources(routes, rootDir) {
1228
1256
  typeInfo: resolver.resolve(paramsArg.typeName) ?? null
1229
1257
  });
1230
1258
  }
1259
+ if (inputType === "body") {
1260
+ const queryArg = meta.params.find((p) => p.type === "query");
1261
+ if (queryArg?.typeName) {
1262
+ sources.push({
1263
+ urlPath: entry.urlPath,
1264
+ filePath,
1265
+ schemaName: getSchemaName(method, "query"),
1266
+ typeInfo: resolver.resolve(queryArg.typeName) ?? null
1267
+ });
1268
+ }
1269
+ } else if (hasBody(method)) {
1270
+ const bodyArg = meta.params.find((p) => p.type === "body") ?? meta.params.find((p) => p.type === "form");
1271
+ if (bodyArg?.typeName) {
1272
+ sources.push({
1273
+ urlPath: entry.urlPath,
1274
+ filePath,
1275
+ schemaName: getSchemaName(method, "body"),
1276
+ typeInfo: resolver.resolve(bodyArg.typeName) ?? null,
1277
+ coerce: bodyArg.type === "form" || void 0
1278
+ });
1279
+ }
1280
+ }
1281
+ }
1282
+ }
1283
+ for (const wsFile of wsFiles) {
1284
+ const program = programByFile.get(wsFile.filePath);
1285
+ const sourceFile = program?.getSourceFile(wsFile.filePath);
1286
+ if (!sourceFile) continue;
1287
+ const resolver = resolversByFile.get(wsFile.filePath);
1288
+ if (findExportedTypeName(sourceFile, "Query")) {
1289
+ sources.push({
1290
+ urlPath: wsFile.urlPath,
1291
+ filePath: wsFile.filePath,
1292
+ schemaName: getSchemaName("WS", "query"),
1293
+ typeInfo: resolver.resolve("Query") ?? null
1294
+ });
1295
+ }
1296
+ if (findExportedTypeName(sourceFile, "Params")) {
1297
+ sources.push({
1298
+ urlPath: wsFile.urlPath,
1299
+ filePath: wsFile.filePath,
1300
+ schemaName: getSchemaName("WS", "params"),
1301
+ typeInfo: resolver.resolve("Params") ?? null
1302
+ });
1231
1303
  }
1232
1304
  }
1233
1305
  return { sources, resolversByFile };
@@ -1714,9 +1786,9 @@ function generateSchemaFileSource(sources, resolveType, helpersImportPath) {
1714
1786
  lines.push(...schemaBlocks);
1715
1787
  return lines.join("\n").replace(/\n+$/, "\n");
1716
1788
  }
1717
- async function generateSchemaFiles(routes, rootDir, dist) {
1718
- if (routes.length === 0) return;
1719
- const { sources, resolversByFile } = collectRouteSchemaSources(routes, rootDir);
1789
+ async function generateSchemaFiles(routes, rootDir, dist, wsRoutes) {
1790
+ if (routes.length === 0 && (wsRoutes?.length ?? 0) === 0) return;
1791
+ const { sources, resolversByFile } = collectRouteSchemaSources(routes, rootDir, wsRoutes);
1720
1792
  await generateZodArtifacts(sources, {
1721
1793
  generateFileSource: generateSchemaFileSource,
1722
1794
  resolversByFile,
@@ -3036,8 +3108,8 @@ function resolveExport(module, exportName) {
3036
3108
  import fs4 from "fs";
3037
3109
  import { pathToFileURL as pathToFileURL2 } from "url";
3038
3110
  var loadTs;
3039
- function setLoadTimestamp(ts11) {
3040
- loadTs = ts11;
3111
+ function setLoadTimestamp(ts12) {
3112
+ loadTs = ts12;
3041
3113
  }
3042
3114
  function getVitestImportActual() {
3043
3115
  const vi = globalThis.vi;
@@ -3084,7 +3156,7 @@ import fs8 from "fs";
3084
3156
  import fg2 from "fast-glob";
3085
3157
 
3086
3158
  // src/cli/aliasPlugin.ts
3087
- import ts7 from "typescript";
3159
+ import ts8 from "typescript";
3088
3160
  import path8 from "path";
3089
3161
  import fs7 from "fs";
3090
3162
 
@@ -3143,7 +3215,7 @@ function isInsideDir(filePath, dir) {
3143
3215
  }
3144
3216
 
3145
3217
  // src/utils/readTsconfig.ts
3146
- import ts6 from "typescript";
3218
+ import ts7 from "typescript";
3147
3219
  import path7 from "path";
3148
3220
  import fs6 from "fs";
3149
3221
  var tsconfigCache = /* @__PURE__ */ new Map();
@@ -3160,9 +3232,9 @@ function readTsconfig(rootDir) {
3160
3232
  if (cached && cached.mtimeMs === mtimeMs) {
3161
3233
  return cached.config;
3162
3234
  }
3163
- const configFile = ts6.readConfigFile(tsconfigPath, ts6.sys.readFile);
3235
+ const configFile = ts7.readConfigFile(tsconfigPath, ts7.sys.readFile);
3164
3236
  if (configFile.error || !configFile.config) return null;
3165
- const parsed = ts6.parseJsonConfigFileContent(configFile.config, ts6.sys, rootDir);
3237
+ const parsed = ts7.parseJsonConfigFileContent(configFile.config, ts7.sys, rootDir);
3166
3238
  const baseUrl = parsed.options.baseUrl ?? rootDir;
3167
3239
  const rawPaths = parsed.options.paths;
3168
3240
  const config = rawPaths ? (() => {
@@ -3262,23 +3334,23 @@ function createAliasPlugin(config, options) {
3262
3334
  }
3263
3335
  const importer = args.path;
3264
3336
  const importerOutsideAppDir = appDirAbs ? !isInsideDir(importer, appDirAbs) : false;
3265
- const sourceFile = ts7.createSourceFile(importer, source, ts7.ScriptTarget.Latest, false);
3337
+ const sourceFile = ts8.createSourceFile(importer, source, ts8.ScriptTarget.Latest, false);
3266
3338
  const literals = [];
3267
3339
  const visit = (node) => {
3268
- if (ts7.isImportDeclaration(node)) {
3340
+ if (ts8.isImportDeclaration(node)) {
3269
3341
  if (!node.importClause?.isTypeOnly && node.moduleSpecifier) {
3270
- if (ts7.isStringLiteral(node.moduleSpecifier)) literals.push(node.moduleSpecifier);
3342
+ if (ts8.isStringLiteral(node.moduleSpecifier)) literals.push(node.moduleSpecifier);
3271
3343
  }
3272
- } else if (ts7.isExportDeclaration(node)) {
3344
+ } else if (ts8.isExportDeclaration(node)) {
3273
3345
  if (!node.isTypeOnly && node.moduleSpecifier) {
3274
- if (ts7.isStringLiteral(node.moduleSpecifier)) literals.push(node.moduleSpecifier);
3346
+ if (ts8.isStringLiteral(node.moduleSpecifier)) literals.push(node.moduleSpecifier);
3275
3347
  }
3276
- } else if (ts7.isCallExpression(node) && node.expression.kind === ts7.SyntaxKind.ImportKeyword && node.arguments[0] && ts7.isStringLiteral(node.arguments[0])) {
3348
+ } else if (ts8.isCallExpression(node) && node.expression.kind === ts8.SyntaxKind.ImportKeyword && node.arguments[0] && ts8.isStringLiteral(node.arguments[0])) {
3277
3349
  literals.push(node.arguments[0]);
3278
3350
  }
3279
- ts7.forEachChild(node, visit);
3351
+ ts8.forEachChild(node, visit);
3280
3352
  };
3281
- ts7.forEachChild(sourceFile, visit);
3353
+ ts8.forEachChild(sourceFile, visit);
3282
3354
  const replacements = [];
3283
3355
  const unresolvable = [];
3284
3356
  for (const lit of literals) {
@@ -3586,7 +3658,7 @@ function clearGeneratedSchemas() {
3586
3658
  state.generatedSchemas.clear();
3587
3659
  state.inFlightSchemaGenerations.clear();
3588
3660
  }
3589
- async function ensureSchemaGenerated(schemaPath, routeFilePath, routes, rootDir, dist) {
3661
+ async function ensureSchemaGenerated(schemaPath, routeFilePath, routes, rootDir, dist, wsRoutes) {
3590
3662
  const inFlight2 = state.inFlightSchemaGenerations.get(schemaPath);
3591
3663
  if (inFlight2) {
3592
3664
  await inFlight2;
@@ -3605,13 +3677,15 @@ async function ensureSchemaGenerated(schemaPath, routeFilePath, routes, rootDir,
3605
3677
  return false;
3606
3678
  }
3607
3679
  const fileRoutes = routes.filter((r) => r.filePath === routeFilePath);
3608
- if (fileRoutes.length === 0) {
3680
+ const fileWsRoutes = (wsRoutes ?? []).filter((r) => r.filePath === routeFilePath);
3681
+ if (fileRoutes.length === 0 && fileWsRoutes.length === 0) {
3609
3682
  return false;
3610
3683
  }
3611
3684
  const sourceRelPath = path14.relative(rootDir, sourceAbsPath).replace(/\\/g, "/");
3612
3685
  const sourceRoutes = fileRoutes.map((r) => ({ ...r, filePath: sourceRelPath }));
3686
+ const sourceWsRoutes = fileWsRoutes.map((r) => ({ ...r, filePath: sourceRelPath }));
3613
3687
  const generatePromise = (async () => {
3614
- await generateSchemaFiles(sourceRoutes, rootDir, dist);
3688
+ await generateSchemaFiles(sourceRoutes, rootDir, dist, sourceWsRoutes);
3615
3689
  state.generatedSchemas.add(schemaPath);
3616
3690
  })();
3617
3691
  state.inFlightSchemaGenerations.set(schemaPath, generatePromise);
@@ -3743,17 +3817,17 @@ init_generateZodArtifacts();
3743
3817
  import path15 from "path";
3744
3818
 
3745
3819
  // src/ast/extractToolMetadata.ts
3746
- import ts9 from "typescript";
3820
+ import ts10 from "typescript";
3747
3821
 
3748
3822
  // src/ast/jsDocMetadata.ts
3749
- import ts8 from "typescript";
3823
+ import ts9 from "typescript";
3750
3824
  function hasExportModifier(node) {
3751
- if (!ts8.canHaveModifiers(node)) return false;
3752
- const modifiers = ts8.getModifiers(node);
3753
- return !!modifiers?.some((m) => m.kind === ts8.SyntaxKind.ExportKeyword);
3825
+ if (!ts9.canHaveModifiers(node)) return false;
3826
+ const modifiers = ts9.getModifiers(node);
3827
+ return !!modifiers?.some((m) => m.kind === ts9.SyntaxKind.ExportKeyword);
3754
3828
  }
3755
3829
  function getJSDocFromNode(node) {
3756
- const apiDocs = ts8.getJSDocCommentsAndTags(node).filter((entry) => ts8.isJSDoc(entry));
3830
+ const apiDocs = ts9.getJSDocCommentsAndTags(node).filter((entry) => ts9.isJSDoc(entry));
3757
3831
  if (apiDocs.length > 0) return apiDocs[0];
3758
3832
  const directDocs = node.jsDoc;
3759
3833
  if (directDocs && directDocs.length > 0) return directDocs[0];
@@ -3799,18 +3873,18 @@ function extractToolMetadata(program, filePath, functionName, pathMeta) {
3799
3873
  }
3800
3874
  function findExportedFunction(sourceFile, functionName) {
3801
3875
  let result = null;
3802
- ts9.forEachChild(sourceFile, (node) => {
3876
+ ts10.forEachChild(sourceFile, (node) => {
3803
3877
  if (result) return;
3804
- if (ts9.isFunctionDeclaration(node) && hasExportModifier(node) && node.name?.text === functionName) {
3878
+ if (ts10.isFunctionDeclaration(node) && hasExportModifier(node) && node.name?.text === functionName) {
3805
3879
  result = { fn: node, jsDocOwner: node };
3806
3880
  return;
3807
3881
  }
3808
- if (ts9.isVariableStatement(node) && hasExportModifier(node)) {
3882
+ if (ts10.isVariableStatement(node) && hasExportModifier(node)) {
3809
3883
  for (const decl of node.declarationList.declarations) {
3810
3884
  if (result) break;
3811
- const nameText = ts9.isIdentifier(decl.name) ? decl.name.text : decl.name.getText(sourceFile);
3885
+ const nameText = ts10.isIdentifier(decl.name) ? decl.name.text : decl.name.getText(sourceFile);
3812
3886
  if (nameText !== functionName || !decl.initializer) continue;
3813
- if (ts9.isArrowFunction(decl.initializer) || ts9.isFunctionExpression(decl.initializer)) {
3887
+ if (ts10.isArrowFunction(decl.initializer) || ts10.isFunctionExpression(decl.initializer)) {
3814
3888
  result = { fn: decl.initializer, jsDocOwner: node };
3815
3889
  }
3816
3890
  }
@@ -3822,7 +3896,7 @@ function getFirstParamTypeName(fn, sourceFile) {
3822
3896
  const firstParam = fn.parameters[0];
3823
3897
  if (!firstParam) return void 0;
3824
3898
  if (!firstParam.type) return void 0;
3825
- if (!ts9.isTypeReferenceNode(firstParam.type)) return void 0;
3899
+ if (!ts10.isTypeReferenceNode(firstParam.type)) return void 0;
3826
3900
  return firstParam.type.typeName.getText(sourceFile);
3827
3901
  }
3828
3902
 
@@ -4707,17 +4781,24 @@ function encodeSseEvent(event) {
4707
4781
  out += "\n";
4708
4782
  return out;
4709
4783
  }
4710
- function createSseWriter() {
4784
+ function createSseWriter(options = {}) {
4785
+ const { onFirstWrite } = options;
4711
4786
  const encoder = new TextEncoder();
4712
4787
  let controller = null;
4713
4788
  let closed = false;
4714
4789
  let aborted = false;
4790
+ let firstWriteTriggered = false;
4715
4791
  let drainWaiters = [];
4716
4792
  const resolveDrainWaiters = () => {
4717
4793
  const waiters = drainWaiters;
4718
4794
  drainWaiters = [];
4719
4795
  for (const resolve of waiters) resolve();
4720
4796
  };
4797
+ const triggerFirstWrite = () => {
4798
+ if (firstWriteTriggered) return;
4799
+ firstWriteTriggered = true;
4800
+ onFirstWrite?.(response);
4801
+ };
4721
4802
  const stream = new ReadableStream(
4722
4803
  {
4723
4804
  start(c) {
@@ -4751,11 +4832,13 @@ function createSseWriter() {
4751
4832
  if (closed || !controller) return;
4752
4833
  const text = encodeSseEvent(event);
4753
4834
  controller.enqueue(encoder.encode(text));
4835
+ triggerFirstWrite();
4754
4836
  },
4755
4837
  sendRaw(chunk) {
4756
4838
  if (closed || !controller) return;
4757
4839
  const bytes = typeof chunk === "string" ? encoder.encode(chunk) : chunk;
4758
4840
  controller.enqueue(bytes);
4841
+ triggerFirstWrite();
4759
4842
  },
4760
4843
  sendError(error) {
4761
4844
  if (closed || !controller) return;
@@ -4763,6 +4846,7 @@ function createSseWriter() {
4763
4846
  const text = encodeSseEvent({ event: "error", data: message });
4764
4847
  try {
4765
4848
  controller.enqueue(encoder.encode(text));
4849
+ triggerFirstWrite();
4766
4850
  } finally {
4767
4851
  writer.close();
4768
4852
  }
@@ -4878,6 +4962,26 @@ function formatErrorResponse(error, config) {
4878
4962
  return jsonOk(body, 500);
4879
4963
  }
4880
4964
 
4965
+ // src/utils/queryToObject.ts
4966
+ var queryCache = /* @__PURE__ */ new WeakMap();
4967
+ function queryToObject(params) {
4968
+ const cached = queryCache.get(params);
4969
+ if (cached) return cached;
4970
+ const result = {};
4971
+ for (const [key, value] of params) {
4972
+ const existing = result[key];
4973
+ if (existing === void 0) {
4974
+ result[key] = value;
4975
+ } else if (Array.isArray(existing)) {
4976
+ existing.push(value);
4977
+ } else {
4978
+ result[key] = [existing, value];
4979
+ }
4980
+ }
4981
+ queryCache.set(params, result);
4982
+ return result;
4983
+ }
4984
+
4881
4985
  // src/runtime/createContext.ts
4882
4986
  function resolveRequestId(request) {
4883
4987
  const header = request.headers.get("x-request-id");
@@ -4927,7 +5031,11 @@ function createContextFromUrl(request, url, params, config = {}, ip = "", regist
4927
5031
  log,
4928
5032
  request,
4929
5033
  params,
4930
- query: url.searchParams,
5034
+ rawParams: params,
5035
+ // ctx.query 为对象口径(createContext 挂原始 string 值对象;handler 声明
5036
+ // query 类型时管线校验后替换为转换值)。原始 URLSearchParams 恒在 rawQuery。
5037
+ query: queryToObject(url.searchParams),
5038
+ rawQuery: url.searchParams,
4931
5039
  headers: request.headers,
4932
5040
  method: request.method,
4933
5041
  path: url.pathname,
@@ -4975,13 +5083,19 @@ function createContextFromUrl(request, url, params, config = {}, ip = "", regist
4975
5083
  /**
4976
5084
  * 创建 SSE writer,用于流式推送事件
4977
5085
  *
4978
- * handler 调用此方法后,通过返回的 writer 推送事件,框架自动把 writer.response
4979
- * 作为 HTTP 响应(Content-Type: text/event-stream)。
5086
+ * handler 调用此方法后,通过返回的 writer 推送事件。HTTP 服务场景下首次
5087
+ * 写入(send/sendRaw/sendError)即触发 server 层提前接管钩子,把 writer.response
5088
+ * 接入底层连接——流式期间字节即产即达(见 createServer.md「SSE 提前接管」)。
5089
+ * 无钩子场景(app.inject / 测试直调 invokeHandler)回落为 handler 返回后交出。
4980
5090
  *
4981
5091
  * 与 ctx.json / ctx.html 互斥:一个 handler 只能用一种响应方式。
4982
5092
  */
4983
5093
  sse() {
4984
- const writer = createSseWriter();
5094
+ const writer = createSseWriter({
5095
+ onFirstWrite: (response) => {
5096
+ ctx.__earlyRespond?.(response);
5097
+ }
5098
+ });
4985
5099
  const ctxWithSse = ctx;
4986
5100
  ctxWithSse.__sseResponse = writer.response;
4987
5101
  ctxWithSse.__sseWriter = writer;
@@ -5027,26 +5141,6 @@ function createContextFromUrl(request, url, params, config = {}, ip = "", regist
5027
5141
  return ctx;
5028
5142
  }
5029
5143
 
5030
- // src/utils/queryToObject.ts
5031
- var queryCache = /* @__PURE__ */ new WeakMap();
5032
- function queryToObject(params) {
5033
- const cached = queryCache.get(params);
5034
- if (cached) return cached;
5035
- const result = {};
5036
- for (const [key, value] of params) {
5037
- const existing = result[key];
5038
- if (existing === void 0) {
5039
- result[key] = value;
5040
- } else if (Array.isArray(existing)) {
5041
- existing.push(value);
5042
- } else {
5043
- result[key] = [existing, value];
5044
- }
5045
- }
5046
- queryCache.set(params, result);
5047
- return result;
5048
- }
5049
-
5050
5144
  // src/utils/parseJsonBody.ts
5051
5145
  function parseJsonBody(text) {
5052
5146
  try {
@@ -5092,46 +5186,29 @@ init_inputType();
5092
5186
  async function resolveInputFromUrl(method, request, url) {
5093
5187
  const inputType = getInputTypeForMethod(method);
5094
5188
  if (inputType === "body") {
5095
- const contentType = request.headers.get("content-type") ?? "";
5096
- if (contentType.includes("multipart/form-data")) {
5097
- return parseMultipart(request);
5098
- }
5099
- if (contentType.includes("application/x-www-form-urlencoded")) {
5100
- const text2 = await request.text();
5101
- if (isBlankText(text2)) return null;
5102
- const params = new URLSearchParams(text2);
5103
- const obj = {};
5104
- for (const [key, value] of params) {
5105
- obj[key] = value;
5106
- }
5107
- return obj;
5108
- }
5109
- const text = await request.text();
5110
- if (isBlankText(text)) {
5111
- return null;
5112
- }
5113
- const result = parseJsonBody(text);
5114
- if (!result.success) {
5115
- throw new ValidationError("\u8BF7\u6C42\u4F53\u4E0D\u662F\u5408\u6CD5\u7684 JSON", [
5116
- {
5117
- path: "body",
5118
- code: "INVALID_FORMAT",
5119
- expected: "JSON",
5120
- received: "text",
5121
- message: "\u8BF7\u6C42\u4F53\u4E0D\u662F\u5408\u6CD5\u7684 JSON"
5122
- }
5123
- ]);
5124
- }
5125
- return result.data;
5189
+ return parseBodyByContentType(request);
5126
5190
  }
5127
- return queryToObject(url.searchParams);
5128
- }
5129
- function isBlankText(text) {
5130
- return text.length === 0 || !/\S/.test(text);
5191
+ return { input: queryToObject(url.searchParams) };
5131
5192
  }
5132
- async function resolveBodyForQueryMethod(request) {
5193
+ async function parseBodyByContentType(request) {
5194
+ const contentType = request.headers.get("content-type") ?? "";
5195
+ if (contentType.includes("multipart/form-data")) {
5196
+ return { input: await parseMultipart(request) };
5197
+ }
5198
+ if (contentType.includes("application/x-www-form-urlencoded")) {
5199
+ const text2 = await request.text();
5200
+ if (isBlankText(text2)) return { input: null };
5201
+ const params = new URLSearchParams(text2);
5202
+ const obj = {};
5203
+ for (const [key, value] of params) {
5204
+ obj[key] = value;
5205
+ }
5206
+ return { input: obj, rawBody: text2 };
5207
+ }
5133
5208
  const text = await request.text();
5134
- if (isBlankText(text)) return void 0;
5209
+ if (isBlankText(text)) {
5210
+ return { input: null };
5211
+ }
5135
5212
  const result = parseJsonBody(text);
5136
5213
  if (!result.success) {
5137
5214
  throw new ValidationError("\u8BF7\u6C42\u4F53\u4E0D\u662F\u5408\u6CD5\u7684 JSON", [
@@ -5144,7 +5221,13 @@ async function resolveBodyForQueryMethod(request) {
5144
5221
  }
5145
5222
  ]);
5146
5223
  }
5147
- return result.data;
5224
+ return { input: result.data, rawBody: text };
5225
+ }
5226
+ function isBlankText(text) {
5227
+ return text.length === 0 || !/\S/.test(text);
5228
+ }
5229
+ async function resolveBodyForQueryMethod(request) {
5230
+ return parseBodyByContentType(request);
5148
5231
  }
5149
5232
 
5150
5233
  // src/response/pendingMeta.ts
@@ -5279,12 +5362,17 @@ function markBuffered(response) {
5279
5362
  init_resolveInjection();
5280
5363
  function getBuiltinInjectionValue(type, ctx, body) {
5281
5364
  switch (type) {
5282
- case "query": {
5283
- const validated = ctx.__validatedQuery;
5284
- return validated ?? queryToObject(ctx.query);
5285
- }
5365
+ case "query":
5366
+ return ctx.query;
5286
5367
  case "params":
5287
5368
  return ctx.params;
5369
+ // raw 系:恒原始,管线永不覆盖(ctx 字段即注入值)
5370
+ case "rawQuery":
5371
+ return ctx.rawQuery;
5372
+ case "rawParams":
5373
+ return ctx.rawParams;
5374
+ case "rawBody":
5375
+ return ctx.rawBody;
5288
5376
  case "headers":
5289
5377
  return ctx.headers;
5290
5378
  case "context":
@@ -5800,6 +5888,9 @@ function buildErrorResponse(err, config) {
5800
5888
  }
5801
5889
  }
5802
5890
 
5891
+ // src/server/handleWsUpgrade.ts
5892
+ init_generateSchemaFiles();
5893
+
5803
5894
  // src/middleware/loadMiddlewares.ts
5804
5895
  var middlewareCache = /* @__PURE__ */ new Map();
5805
5896
  var inFlight = /* @__PURE__ */ new Map();
@@ -5998,6 +6089,7 @@ function attachWebSocket(options) {
5998
6089
  server,
5999
6090
  routesRef,
6000
6091
  rootDir,
6092
+ dist,
6001
6093
  config,
6002
6094
  globalMiddlewares,
6003
6095
  trustedProxy = false,
@@ -6032,12 +6124,50 @@ function attachWebSocket(options) {
6032
6124
  const { route, params } = match;
6033
6125
  const headers = nodeHttpToWebHeaders(req);
6034
6126
  const host = req.headers.host ?? "localhost";
6035
- const url = `http://${host}${req.url ?? "/"}`;
6036
- const request = new Request(url, { method: "GET", headers });
6127
+ const fullUrl = new URL(`http://${host}${req.url ?? "/"}`);
6128
+ const request = new Request(fullUrl.toString(), { method: "GET", headers });
6037
6129
  const ctx = createContext(request, params, config, getClientIp(req, trustedProxy), registries);
6038
6130
  const meta = ctx.meta;
6039
6131
  let upgraded = false;
6040
6132
  const finalHandler = async () => {
6133
+ const schemaPath = getRuntimeSchemaPath(route.filePath, dist, rootDir);
6134
+ if (fs16.existsSync(schemaPath)) {
6135
+ if (isDevOnDemandEnabled()) {
6136
+ const devDist = getDevDist();
6137
+ if (devDist) {
6138
+ await ensureSchemaGenerated(
6139
+ schemaPath,
6140
+ route.filePath,
6141
+ routesRef.current,
6142
+ rootDir,
6143
+ dist,
6144
+ routesRef.wsCurrent
6145
+ );
6146
+ }
6147
+ }
6148
+ const searchParams = fullUrl.searchParams;
6149
+ const rawQueryObject = queryToObject(searchParams);
6150
+ const paramsResult = await validateInput(schemaPath, "WS", "params", params);
6151
+ if (!paramsResult.valid) {
6152
+ throw new ValidationError("\u53C2\u6570\u6821\u9A8C\u5931\u8D25", paramsResult.issues);
6153
+ }
6154
+ if (paramsResult.data !== params && typeof paramsResult.data === "object" && paramsResult.data !== null) {
6155
+ ctx.params = {
6156
+ ...params,
6157
+ ...paramsResult.data
6158
+ };
6159
+ }
6160
+ const queryResult = await validateInput(schemaPath, "WS", "query", rawQueryObject);
6161
+ if (!queryResult.valid) {
6162
+ throw new ValidationError("\u53C2\u6570\u6821\u9A8C\u5931\u8D25", queryResult.issues);
6163
+ }
6164
+ if (queryResult.data !== rawQueryObject && typeof queryResult.data === "object" && queryResult.data !== null) {
6165
+ ctx.query = {
6166
+ ...rawQueryObject,
6167
+ ...queryResult.data
6168
+ };
6169
+ }
6170
+ }
6041
6171
  let handlers;
6042
6172
  try {
6043
6173
  const absoluteFilePath = path18.resolve(rootDir, route.filePath);
@@ -6245,7 +6375,8 @@ function createServer(options) {
6245
6375
  globalInjectors,
6246
6376
  bodyLimit,
6247
6377
  trustedProxy,
6248
- registries
6378
+ registries,
6379
+ routesRef.wsCurrent
6249
6380
  ).catch((err) => {
6250
6381
  console.error("[faapi] Request pipeline failed after error response attempt:", err);
6251
6382
  try {
@@ -6263,6 +6394,7 @@ function createServer(options) {
6263
6394
  server,
6264
6395
  routesRef,
6265
6396
  rootDir,
6397
+ dist,
6266
6398
  config,
6267
6399
  globalMiddlewares,
6268
6400
  trustedProxy,
@@ -6270,14 +6402,13 @@ function createServer(options) {
6270
6402
  });
6271
6403
  return { server, routesRef };
6272
6404
  }
6273
- function prepareRequest(req, config, bodyLimit, trustedProxy, registries, requestSignal) {
6274
- const { request, url } = toWebRequest(req, bodyLimit, requestSignal);
6405
+ function prepareRequest(req, request, url, config, trustedProxy, registries, preMatched) {
6275
6406
  const method = request.method.toUpperCase();
6276
6407
  const urlPath = url.pathname;
6277
6408
  const ctx = createContextFromUrl(
6278
6409
  request,
6279
6410
  url,
6280
- {},
6411
+ preMatched?.params ?? {},
6281
6412
  config,
6282
6413
  getClientIp(req, trustedProxy),
6283
6414
  registries
@@ -6308,19 +6439,33 @@ function getRoutePaths(route, rootDir, dist) {
6308
6439
  }
6309
6440
  var mergedInjectorsCache = /* @__PURE__ */ new WeakMap();
6310
6441
  function createRoutePipeline(opts) {
6311
- const { routes, method, urlPath, url, ctx, request, rootDir, dist, globalInjectors } = opts;
6442
+ const {
6443
+ routes,
6444
+ method,
6445
+ urlPath,
6446
+ url,
6447
+ ctx,
6448
+ request,
6449
+ rootDir,
6450
+ dist,
6451
+ globalInjectors,
6452
+ preMatched,
6453
+ wsRoutes
6454
+ } = opts;
6312
6455
  return async () => {
6313
- const match = resolveRouteOrThrow(routes, method, urlPath);
6314
- ctx.params = match.params;
6456
+ const match = preMatched ?? resolveRouteOrThrow(routes, method, urlPath);
6315
6457
  const { route } = match;
6316
6458
  const { absFilePath, schemaPath } = getRoutePaths(route, rootDir, dist);
6317
6459
  const routeModule = await loadRouteModule(absFilePath, route.method, rootDir);
6318
- const input = await resolveInputFromUrl(route.method, request, url);
6460
+ const { input, rawBody } = await resolveInputFromUrl(route.method, request, url);
6461
+ if (rawBody !== void 0) {
6462
+ ctx.rawBody = rawBody;
6463
+ }
6319
6464
  const inputType = getInputTypeForMethod(route.method);
6320
6465
  if (isDevOnDemandEnabled()) {
6321
6466
  const devDist = getDevDist();
6322
6467
  if (devDist) {
6323
- await ensureSchemaGenerated(schemaPath, route.filePath, routes, rootDir, dist);
6468
+ await ensureSchemaGenerated(schemaPath, route.filePath, routes, rootDir, dist, wsRoutes);
6324
6469
  }
6325
6470
  }
6326
6471
  const result = await validateInput(schemaPath, route.method, inputType, input);
@@ -6328,7 +6473,7 @@ function createRoutePipeline(opts) {
6328
6473
  throw new ValidationError("\u53C2\u6570\u6821\u9A8C\u5931\u8D25", result.issues);
6329
6474
  }
6330
6475
  if (inputType === "query" && result.data !== input && typeof result.data === "object" && result.data !== null) {
6331
- ctx.__validatedQuery = {
6476
+ ctx.query = {
6332
6477
  ...input,
6333
6478
  ...result.data
6334
6479
  };
@@ -6343,7 +6488,36 @@ function createRoutePipeline(opts) {
6343
6488
  ...paramsResult.data
6344
6489
  } : match.params;
6345
6490
  }
6346
- const body = inputType === "query" && hasBody(route.method) ? await resolveBodyForQueryMethod(request) : hasBody(route.method) ? result.data : void 0;
6491
+ if (inputType === "body") {
6492
+ const rawQuery = queryToObject(url.searchParams);
6493
+ const queryResult = await validateInput(schemaPath, route.method, "query", rawQuery);
6494
+ if (!queryResult.valid) {
6495
+ throw new ValidationError("\u53C2\u6570\u6821\u9A8C\u5931\u8D25", queryResult.issues);
6496
+ }
6497
+ if (queryResult.data !== rawQuery && typeof queryResult.data === "object" && queryResult.data !== null) {
6498
+ ctx.query = {
6499
+ ...rawQuery,
6500
+ ...queryResult.data
6501
+ };
6502
+ }
6503
+ }
6504
+ let body;
6505
+ if (inputType === "query" && hasBody(route.method)) {
6506
+ const parsed = await resolveBodyForQueryMethod(request);
6507
+ if (parsed.rawBody !== void 0) {
6508
+ ctx.rawBody = parsed.rawBody;
6509
+ }
6510
+ const bodyResult = await validateInput(schemaPath, route.method, "body", parsed.input);
6511
+ if (!bodyResult.valid) {
6512
+ throw new ValidationError("\u53C2\u6570\u6821\u9A8C\u5931\u8D25", bodyResult.issues);
6513
+ }
6514
+ body = bodyResult.data;
6515
+ } else if (hasBody(route.method)) {
6516
+ body = result.data;
6517
+ }
6518
+ if (body !== void 0 && body !== null) {
6519
+ ctx.body = body;
6520
+ }
6347
6521
  if (route.middlewares === void 0 && route.injectors === void 0 && route.middlewarePaths) {
6348
6522
  await ensureMiddlewaresCompiled(route.middlewarePaths, rootDir);
6349
6523
  const bundle = await loadMergedMiddlewares(route.middlewarePaths);
@@ -6379,25 +6553,37 @@ async function sendErrorResponse(err, meta, res, onError, ctx) {
6379
6553
  }
6380
6554
  }
6381
6555
  }
6382
- async function handleRequest(routes, rootDir, dist, req, res, outerMiddlewares, onError, config, globalInjectors, bodyLimit, trustedProxy, registries) {
6556
+ async function handleRequest(routes, rootDir, dist, req, res, outerMiddlewares, onError, config, globalInjectors, bodyLimit, trustedProxy, registries, wsRoutes) {
6383
6557
  let meta = { headers: {}, setCookies: [] };
6384
6558
  let ctx;
6559
+ let sseEarlySent = false;
6385
6560
  const abortController = new AbortController();
6386
6561
  res.on("close", () => {
6387
6562
  if (!res.writableEnded) abortController.abort();
6388
6563
  });
6389
6564
  try {
6565
+ const { request, url } = toWebRequest(req, bodyLimit, abortController.signal);
6566
+ const method = request.method.toUpperCase();
6567
+ const urlPath = url.pathname;
6568
+ const preMatched = matchRoute(routes, method, urlPath);
6390
6569
  const prepared = prepareRequest(
6391
6570
  req,
6571
+ request,
6572
+ url,
6392
6573
  config,
6393
- bodyLimit,
6394
6574
  trustedProxy,
6395
6575
  registries,
6396
- abortController.signal
6576
+ preMatched
6397
6577
  );
6398
6578
  ctx = prepared.ctx;
6399
6579
  meta = prepared.meta;
6400
- const { request, url, method, urlPath } = prepared;
6580
+ ctx.__earlyRespond = (response2) => {
6581
+ if (sseEarlySent) return;
6582
+ sseEarlySent = true;
6583
+ void sendNodeResponse(mergeMeta(response2, meta), res).catch(() => {
6584
+ if (!res.writableEnded) res.destroy();
6585
+ });
6586
+ };
6401
6587
  const routePipeline = createRoutePipeline({
6402
6588
  routes,
6403
6589
  method,
@@ -6407,12 +6593,25 @@ async function handleRequest(routes, rootDir, dist, req, res, outerMiddlewares,
6407
6593
  request,
6408
6594
  rootDir,
6409
6595
  dist,
6410
- globalInjectors
6596
+ globalInjectors,
6597
+ preMatched,
6598
+ wsRoutes
6411
6599
  });
6412
6600
  const response = outerMiddlewares.length > 0 ? await compose(outerMiddlewares, ctx, routePipeline) : await routePipeline();
6413
- await sendSuccessResponse(response, res);
6601
+ if (!sseEarlySent) {
6602
+ await sendSuccessResponse(response, res);
6603
+ }
6414
6604
  } catch (err) {
6415
6605
  if (res.destroyed || res.writableEnded) return;
6606
+ if (sseEarlySent) {
6607
+ if (onError && ctx) {
6608
+ try {
6609
+ await onError(err, ctx);
6610
+ } catch {
6611
+ }
6612
+ }
6613
+ return;
6614
+ }
6416
6615
  if (err instanceof PayloadTooLargeError) {
6417
6616
  res.setHeader("Connection", "close");
6418
6617
  res.once("finish", () => req.destroy());
@@ -6683,7 +6882,7 @@ import path25 from "path";
6683
6882
  import path23 from "path";
6684
6883
 
6685
6884
  // src/ast/extractAgentMetadata.ts
6686
- import ts10 from "typescript";
6885
+ import ts11 from "typescript";
6687
6886
  init_resolveTypeNode();
6688
6887
  function extractAgentMetadata(program, filePath, pathMeta) {
6689
6888
  const sourceFile = program.getSourceFile(filePath);
@@ -6728,23 +6927,23 @@ function extractAgentMetadata(program, filePath, pathMeta) {
6728
6927
  }
6729
6928
  function findConfigExport(sourceFile) {
6730
6929
  let result = null;
6731
- ts10.forEachChild(sourceFile, (node) => {
6930
+ ts11.forEachChild(sourceFile, (node) => {
6732
6931
  if (result) return;
6733
- if (ts10.isVariableStatement(node) && hasExportModifier(node)) {
6932
+ if (ts11.isVariableStatement(node) && hasExportModifier(node)) {
6734
6933
  for (const decl of node.declarationList.declarations) {
6735
6934
  if (result) break;
6736
- const nameText = ts10.isIdentifier(decl.name) ? decl.name.text : "";
6935
+ const nameText = ts11.isIdentifier(decl.name) ? decl.name.text : "";
6737
6936
  if (nameText !== "config" || !decl.initializer) continue;
6738
- if (ts10.isObjectLiteralExpression(decl.initializer)) {
6937
+ if (ts11.isObjectLiteralExpression(decl.initializer)) {
6739
6938
  result = { jsDocOwner: node, objectLiteral: decl.initializer };
6740
- } else if (ts10.isArrowFunction(decl.initializer)) {
6939
+ } else if (ts11.isArrowFunction(decl.initializer)) {
6741
6940
  result = { jsDocOwner: node, objectLiteral: getReturnObjectLiteral(decl.initializer) };
6742
6941
  } else {
6743
6942
  result = { jsDocOwner: node, objectLiteral: null };
6744
6943
  }
6745
6944
  }
6746
6945
  }
6747
- if (ts10.isFunctionDeclaration(node) && hasExportModifier(node) && node.name?.text === "config") {
6946
+ if (ts11.isFunctionDeclaration(node) && hasExportModifier(node) && node.name?.text === "config") {
6748
6947
  const returnObj = getReturnObjectLiteral(node);
6749
6948
  result = { jsDocOwner: node, objectLiteral: returnObj };
6750
6949
  }
@@ -6754,12 +6953,12 @@ function findConfigExport(sourceFile) {
6754
6953
  function getReturnObjectLiteral(fn) {
6755
6954
  const body = fn.body;
6756
6955
  if (!body) return null;
6757
- if (ts10.isObjectLiteralExpression(body)) {
6956
+ if (ts11.isObjectLiteralExpression(body)) {
6758
6957
  return body;
6759
6958
  }
6760
- if (ts10.isBlock(body)) {
6959
+ if (ts11.isBlock(body)) {
6761
6960
  for (const stmt of body.statements) {
6762
- if (ts10.isReturnStatement(stmt) && stmt.expression && ts10.isObjectLiteralExpression(stmt.expression)) {
6961
+ if (ts11.isReturnStatement(stmt) && stmt.expression && ts11.isObjectLiteralExpression(stmt.expression)) {
6763
6962
  return stmt.expression;
6764
6963
  }
6765
6964
  }
@@ -6774,8 +6973,8 @@ function extractConfigFields(objLit, sourceFile) {
6774
6973
  let maxTurns;
6775
6974
  let inputDescription;
6776
6975
  for (const prop of objLit.properties) {
6777
- if (ts10.isSpreadAssignment(prop)) continue;
6778
- if (!ts10.isPropertyAssignment(prop)) {
6976
+ if (ts11.isSpreadAssignment(prop)) continue;
6977
+ if (!ts11.isPropertyAssignment(prop)) {
6779
6978
  throw SchemaExtractionError.at(
6780
6979
  prop,
6781
6980
  "config",
@@ -6826,13 +7025,13 @@ function extractConfigFields(objLit, sourceFile) {
6826
7025
  return { systemPrompt, tools, agents, model, maxTurns, inputDescription };
6827
7026
  }
6828
7027
  function getPropertyName(name) {
6829
- if (ts10.isIdentifier(name)) return name.text;
6830
- if (ts10.isStringLiteral(name)) return name.text;
7028
+ if (ts11.isIdentifier(name)) return name.text;
7029
+ if (ts11.isStringLiteral(name)) return name.text;
6831
7030
  return null;
6832
7031
  }
6833
7032
  function extractStringValue(expr) {
6834
7033
  if (isStringLikeLiteral(expr)) return expr.text;
6835
- if (ts10.isBinaryExpression(expr) && expr.operatorToken.kind === ts10.SyntaxKind.PlusToken) {
7034
+ if (ts11.isBinaryExpression(expr) && expr.operatorToken.kind === ts11.SyntaxKind.PlusToken) {
6836
7035
  const left = extractStringValue(expr.left);
6837
7036
  if (left === void 0) return void 0;
6838
7037
  const right = extractStringValue(expr.right);
@@ -6878,17 +7077,17 @@ function requireNumberValue(prop, fieldName, sourceFile) {
6878
7077
  return value;
6879
7078
  }
6880
7079
  function extractNumberValue(expr) {
6881
- if (ts10.isNumericLiteral(expr)) {
7080
+ if (ts11.isNumericLiteral(expr)) {
6882
7081
  const num = Number(expr.text);
6883
7082
  return Number.isNaN(num) ? void 0 : num;
6884
7083
  }
6885
7084
  return void 0;
6886
7085
  }
6887
7086
  function isStringLikeLiteral(node) {
6888
- return ts10.isStringLiteral(node) || ts10.isNoSubstitutionTemplateLiteral(node);
7087
+ return ts11.isStringLiteral(node) || ts11.isNoSubstitutionTemplateLiteral(node);
6889
7088
  }
6890
7089
  function extractStringArrayValue(expr) {
6891
- if (!ts10.isArrayLiteralExpression(expr)) return void 0;
7090
+ if (!ts11.isArrayLiteralExpression(expr)) return void 0;
6892
7091
  const values = [];
6893
7092
  for (const element of expr.elements) {
6894
7093
  const value = extractStringValue(element);
@@ -7865,14 +8064,14 @@ async function createDevApp(options) {
7865
8064
  void (async () => {
7866
8065
  try {
7867
8066
  const { generateSchemaFiles: generateSchemaFiles2 } = await Promise.resolve().then(() => (init_generateSchemaFiles(), generateSchemaFiles_exports));
7868
- await generateSchemaFiles2(sorted, ctx.rootDir, ctx.dist);
8067
+ await generateSchemaFiles2(sorted, ctx.rootDir, ctx.dist, reScanned.wsRoutes);
7869
8068
  } catch (err) {
7870
8069
  console.error("[faapi] Background schema regeneration failed:", err);
7871
8070
  }
7872
8071
  })();
7873
8072
  } else {
7874
8073
  const { generateSchemaFiles: generateSchemaFiles2 } = await Promise.resolve().then(() => (init_generateSchemaFiles(), generateSchemaFiles_exports));
7875
- await generateSchemaFiles2(sorted, ctx.rootDir, ctx.dist);
8074
+ await generateSchemaFiles2(sorted, ctx.rootDir, ctx.dist, reScanned.wsRoutes);
7876
8075
  }
7877
8076
  ctx.updateRoutes(sorted, reScanned.wsRoutes);
7878
8077
  };