@faapi/faapi 6.20.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 = [];
@@ -1252,6 +1280,28 @@ function collectRouteSchemaSources(routes, rootDir) {
1252
1280
  }
1253
1281
  }
1254
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
+ });
1303
+ }
1304
+ }
1255
1305
  return { sources, resolversByFile };
1256
1306
  }
1257
1307
  var init_collectRouteSchemaSources = __esm({
@@ -1736,9 +1786,9 @@ function generateSchemaFileSource(sources, resolveType, helpersImportPath) {
1736
1786
  lines.push(...schemaBlocks);
1737
1787
  return lines.join("\n").replace(/\n+$/, "\n");
1738
1788
  }
1739
- async function generateSchemaFiles(routes, rootDir, dist) {
1740
- if (routes.length === 0) return;
1741
- 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);
1742
1792
  await generateZodArtifacts(sources, {
1743
1793
  generateFileSource: generateSchemaFileSource,
1744
1794
  resolversByFile,
@@ -3058,8 +3108,8 @@ function resolveExport(module, exportName) {
3058
3108
  import fs4 from "fs";
3059
3109
  import { pathToFileURL as pathToFileURL2 } from "url";
3060
3110
  var loadTs;
3061
- function setLoadTimestamp(ts11) {
3062
- loadTs = ts11;
3111
+ function setLoadTimestamp(ts12) {
3112
+ loadTs = ts12;
3063
3113
  }
3064
3114
  function getVitestImportActual() {
3065
3115
  const vi = globalThis.vi;
@@ -3106,7 +3156,7 @@ import fs8 from "fs";
3106
3156
  import fg2 from "fast-glob";
3107
3157
 
3108
3158
  // src/cli/aliasPlugin.ts
3109
- import ts7 from "typescript";
3159
+ import ts8 from "typescript";
3110
3160
  import path8 from "path";
3111
3161
  import fs7 from "fs";
3112
3162
 
@@ -3165,7 +3215,7 @@ function isInsideDir(filePath, dir) {
3165
3215
  }
3166
3216
 
3167
3217
  // src/utils/readTsconfig.ts
3168
- import ts6 from "typescript";
3218
+ import ts7 from "typescript";
3169
3219
  import path7 from "path";
3170
3220
  import fs6 from "fs";
3171
3221
  var tsconfigCache = /* @__PURE__ */ new Map();
@@ -3182,9 +3232,9 @@ function readTsconfig(rootDir) {
3182
3232
  if (cached && cached.mtimeMs === mtimeMs) {
3183
3233
  return cached.config;
3184
3234
  }
3185
- const configFile = ts6.readConfigFile(tsconfigPath, ts6.sys.readFile);
3235
+ const configFile = ts7.readConfigFile(tsconfigPath, ts7.sys.readFile);
3186
3236
  if (configFile.error || !configFile.config) return null;
3187
- const parsed = ts6.parseJsonConfigFileContent(configFile.config, ts6.sys, rootDir);
3237
+ const parsed = ts7.parseJsonConfigFileContent(configFile.config, ts7.sys, rootDir);
3188
3238
  const baseUrl = parsed.options.baseUrl ?? rootDir;
3189
3239
  const rawPaths = parsed.options.paths;
3190
3240
  const config = rawPaths ? (() => {
@@ -3284,23 +3334,23 @@ function createAliasPlugin(config, options) {
3284
3334
  }
3285
3335
  const importer = args.path;
3286
3336
  const importerOutsideAppDir = appDirAbs ? !isInsideDir(importer, appDirAbs) : false;
3287
- const sourceFile = ts7.createSourceFile(importer, source, ts7.ScriptTarget.Latest, false);
3337
+ const sourceFile = ts8.createSourceFile(importer, source, ts8.ScriptTarget.Latest, false);
3288
3338
  const literals = [];
3289
3339
  const visit = (node) => {
3290
- if (ts7.isImportDeclaration(node)) {
3340
+ if (ts8.isImportDeclaration(node)) {
3291
3341
  if (!node.importClause?.isTypeOnly && node.moduleSpecifier) {
3292
- if (ts7.isStringLiteral(node.moduleSpecifier)) literals.push(node.moduleSpecifier);
3342
+ if (ts8.isStringLiteral(node.moduleSpecifier)) literals.push(node.moduleSpecifier);
3293
3343
  }
3294
- } else if (ts7.isExportDeclaration(node)) {
3344
+ } else if (ts8.isExportDeclaration(node)) {
3295
3345
  if (!node.isTypeOnly && node.moduleSpecifier) {
3296
- if (ts7.isStringLiteral(node.moduleSpecifier)) literals.push(node.moduleSpecifier);
3346
+ if (ts8.isStringLiteral(node.moduleSpecifier)) literals.push(node.moduleSpecifier);
3297
3347
  }
3298
- } 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])) {
3299
3349
  literals.push(node.arguments[0]);
3300
3350
  }
3301
- ts7.forEachChild(node, visit);
3351
+ ts8.forEachChild(node, visit);
3302
3352
  };
3303
- ts7.forEachChild(sourceFile, visit);
3353
+ ts8.forEachChild(sourceFile, visit);
3304
3354
  const replacements = [];
3305
3355
  const unresolvable = [];
3306
3356
  for (const lit of literals) {
@@ -3608,7 +3658,7 @@ function clearGeneratedSchemas() {
3608
3658
  state.generatedSchemas.clear();
3609
3659
  state.inFlightSchemaGenerations.clear();
3610
3660
  }
3611
- async function ensureSchemaGenerated(schemaPath, routeFilePath, routes, rootDir, dist) {
3661
+ async function ensureSchemaGenerated(schemaPath, routeFilePath, routes, rootDir, dist, wsRoutes) {
3612
3662
  const inFlight2 = state.inFlightSchemaGenerations.get(schemaPath);
3613
3663
  if (inFlight2) {
3614
3664
  await inFlight2;
@@ -3627,13 +3677,15 @@ async function ensureSchemaGenerated(schemaPath, routeFilePath, routes, rootDir,
3627
3677
  return false;
3628
3678
  }
3629
3679
  const fileRoutes = routes.filter((r) => r.filePath === routeFilePath);
3630
- if (fileRoutes.length === 0) {
3680
+ const fileWsRoutes = (wsRoutes ?? []).filter((r) => r.filePath === routeFilePath);
3681
+ if (fileRoutes.length === 0 && fileWsRoutes.length === 0) {
3631
3682
  return false;
3632
3683
  }
3633
3684
  const sourceRelPath = path14.relative(rootDir, sourceAbsPath).replace(/\\/g, "/");
3634
3685
  const sourceRoutes = fileRoutes.map((r) => ({ ...r, filePath: sourceRelPath }));
3686
+ const sourceWsRoutes = fileWsRoutes.map((r) => ({ ...r, filePath: sourceRelPath }));
3635
3687
  const generatePromise = (async () => {
3636
- await generateSchemaFiles(sourceRoutes, rootDir, dist);
3688
+ await generateSchemaFiles(sourceRoutes, rootDir, dist, sourceWsRoutes);
3637
3689
  state.generatedSchemas.add(schemaPath);
3638
3690
  })();
3639
3691
  state.inFlightSchemaGenerations.set(schemaPath, generatePromise);
@@ -3765,17 +3817,17 @@ init_generateZodArtifacts();
3765
3817
  import path15 from "path";
3766
3818
 
3767
3819
  // src/ast/extractToolMetadata.ts
3768
- import ts9 from "typescript";
3820
+ import ts10 from "typescript";
3769
3821
 
3770
3822
  // src/ast/jsDocMetadata.ts
3771
- import ts8 from "typescript";
3823
+ import ts9 from "typescript";
3772
3824
  function hasExportModifier(node) {
3773
- if (!ts8.canHaveModifiers(node)) return false;
3774
- const modifiers = ts8.getModifiers(node);
3775
- 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);
3776
3828
  }
3777
3829
  function getJSDocFromNode(node) {
3778
- const apiDocs = ts8.getJSDocCommentsAndTags(node).filter((entry) => ts8.isJSDoc(entry));
3830
+ const apiDocs = ts9.getJSDocCommentsAndTags(node).filter((entry) => ts9.isJSDoc(entry));
3779
3831
  if (apiDocs.length > 0) return apiDocs[0];
3780
3832
  const directDocs = node.jsDoc;
3781
3833
  if (directDocs && directDocs.length > 0) return directDocs[0];
@@ -3821,18 +3873,18 @@ function extractToolMetadata(program, filePath, functionName, pathMeta) {
3821
3873
  }
3822
3874
  function findExportedFunction(sourceFile, functionName) {
3823
3875
  let result = null;
3824
- ts9.forEachChild(sourceFile, (node) => {
3876
+ ts10.forEachChild(sourceFile, (node) => {
3825
3877
  if (result) return;
3826
- if (ts9.isFunctionDeclaration(node) && hasExportModifier(node) && node.name?.text === functionName) {
3878
+ if (ts10.isFunctionDeclaration(node) && hasExportModifier(node) && node.name?.text === functionName) {
3827
3879
  result = { fn: node, jsDocOwner: node };
3828
3880
  return;
3829
3881
  }
3830
- if (ts9.isVariableStatement(node) && hasExportModifier(node)) {
3882
+ if (ts10.isVariableStatement(node) && hasExportModifier(node)) {
3831
3883
  for (const decl of node.declarationList.declarations) {
3832
3884
  if (result) break;
3833
- 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);
3834
3886
  if (nameText !== functionName || !decl.initializer) continue;
3835
- if (ts9.isArrowFunction(decl.initializer) || ts9.isFunctionExpression(decl.initializer)) {
3887
+ if (ts10.isArrowFunction(decl.initializer) || ts10.isFunctionExpression(decl.initializer)) {
3836
3888
  result = { fn: decl.initializer, jsDocOwner: node };
3837
3889
  }
3838
3890
  }
@@ -3844,7 +3896,7 @@ function getFirstParamTypeName(fn, sourceFile) {
3844
3896
  const firstParam = fn.parameters[0];
3845
3897
  if (!firstParam) return void 0;
3846
3898
  if (!firstParam.type) return void 0;
3847
- if (!ts9.isTypeReferenceNode(firstParam.type)) return void 0;
3899
+ if (!ts10.isTypeReferenceNode(firstParam.type)) return void 0;
3848
3900
  return firstParam.type.typeName.getText(sourceFile);
3849
3901
  }
3850
3902
 
@@ -4729,17 +4781,24 @@ function encodeSseEvent(event) {
4729
4781
  out += "\n";
4730
4782
  return out;
4731
4783
  }
4732
- function createSseWriter() {
4784
+ function createSseWriter(options = {}) {
4785
+ const { onFirstWrite } = options;
4733
4786
  const encoder = new TextEncoder();
4734
4787
  let controller = null;
4735
4788
  let closed = false;
4736
4789
  let aborted = false;
4790
+ let firstWriteTriggered = false;
4737
4791
  let drainWaiters = [];
4738
4792
  const resolveDrainWaiters = () => {
4739
4793
  const waiters = drainWaiters;
4740
4794
  drainWaiters = [];
4741
4795
  for (const resolve of waiters) resolve();
4742
4796
  };
4797
+ const triggerFirstWrite = () => {
4798
+ if (firstWriteTriggered) return;
4799
+ firstWriteTriggered = true;
4800
+ onFirstWrite?.(response);
4801
+ };
4743
4802
  const stream = new ReadableStream(
4744
4803
  {
4745
4804
  start(c) {
@@ -4773,11 +4832,13 @@ function createSseWriter() {
4773
4832
  if (closed || !controller) return;
4774
4833
  const text = encodeSseEvent(event);
4775
4834
  controller.enqueue(encoder.encode(text));
4835
+ triggerFirstWrite();
4776
4836
  },
4777
4837
  sendRaw(chunk) {
4778
4838
  if (closed || !controller) return;
4779
4839
  const bytes = typeof chunk === "string" ? encoder.encode(chunk) : chunk;
4780
4840
  controller.enqueue(bytes);
4841
+ triggerFirstWrite();
4781
4842
  },
4782
4843
  sendError(error) {
4783
4844
  if (closed || !controller) return;
@@ -4785,6 +4846,7 @@ function createSseWriter() {
4785
4846
  const text = encodeSseEvent({ event: "error", data: message });
4786
4847
  try {
4787
4848
  controller.enqueue(encoder.encode(text));
4849
+ triggerFirstWrite();
4788
4850
  } finally {
4789
4851
  writer.close();
4790
4852
  }
@@ -4900,6 +4962,26 @@ function formatErrorResponse(error, config) {
4900
4962
  return jsonOk(body, 500);
4901
4963
  }
4902
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
+
4903
4985
  // src/runtime/createContext.ts
4904
4986
  function resolveRequestId(request) {
4905
4987
  const header = request.headers.get("x-request-id");
@@ -4949,7 +5031,11 @@ function createContextFromUrl(request, url, params, config = {}, ip = "", regist
4949
5031
  log,
4950
5032
  request,
4951
5033
  params,
4952
- 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,
4953
5039
  headers: request.headers,
4954
5040
  method: request.method,
4955
5041
  path: url.pathname,
@@ -4997,13 +5083,19 @@ function createContextFromUrl(request, url, params, config = {}, ip = "", regist
4997
5083
  /**
4998
5084
  * 创建 SSE writer,用于流式推送事件
4999
5085
  *
5000
- * handler 调用此方法后,通过返回的 writer 推送事件,框架自动把 writer.response
5001
- * 作为 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 返回后交出。
5002
5090
  *
5003
5091
  * 与 ctx.json / ctx.html 互斥:一个 handler 只能用一种响应方式。
5004
5092
  */
5005
5093
  sse() {
5006
- const writer = createSseWriter();
5094
+ const writer = createSseWriter({
5095
+ onFirstWrite: (response) => {
5096
+ ctx.__earlyRespond?.(response);
5097
+ }
5098
+ });
5007
5099
  const ctxWithSse = ctx;
5008
5100
  ctxWithSse.__sseResponse = writer.response;
5009
5101
  ctxWithSse.__sseWriter = writer;
@@ -5049,26 +5141,6 @@ function createContextFromUrl(request, url, params, config = {}, ip = "", regist
5049
5141
  return ctx;
5050
5142
  }
5051
5143
 
5052
- // src/utils/queryToObject.ts
5053
- var queryCache = /* @__PURE__ */ new WeakMap();
5054
- function queryToObject(params) {
5055
- const cached = queryCache.get(params);
5056
- if (cached) return cached;
5057
- const result = {};
5058
- for (const [key, value] of params) {
5059
- const existing = result[key];
5060
- if (existing === void 0) {
5061
- result[key] = value;
5062
- } else if (Array.isArray(existing)) {
5063
- existing.push(value);
5064
- } else {
5065
- result[key] = [existing, value];
5066
- }
5067
- }
5068
- queryCache.set(params, result);
5069
- return result;
5070
- }
5071
-
5072
5144
  // src/utils/parseJsonBody.ts
5073
5145
  function parseJsonBody(text) {
5074
5146
  try {
@@ -5114,46 +5186,29 @@ init_inputType();
5114
5186
  async function resolveInputFromUrl(method, request, url) {
5115
5187
  const inputType = getInputTypeForMethod(method);
5116
5188
  if (inputType === "body") {
5117
- const contentType = request.headers.get("content-type") ?? "";
5118
- if (contentType.includes("multipart/form-data")) {
5119
- return parseMultipart(request);
5120
- }
5121
- if (contentType.includes("application/x-www-form-urlencoded")) {
5122
- const text2 = await request.text();
5123
- if (isBlankText(text2)) return null;
5124
- const params = new URLSearchParams(text2);
5125
- const obj = {};
5126
- for (const [key, value] of params) {
5127
- obj[key] = value;
5128
- }
5129
- return obj;
5130
- }
5131
- const text = await request.text();
5132
- if (isBlankText(text)) {
5133
- return null;
5134
- }
5135
- const result = parseJsonBody(text);
5136
- if (!result.success) {
5137
- throw new ValidationError("\u8BF7\u6C42\u4F53\u4E0D\u662F\u5408\u6CD5\u7684 JSON", [
5138
- {
5139
- path: "body",
5140
- code: "INVALID_FORMAT",
5141
- expected: "JSON",
5142
- received: "text",
5143
- message: "\u8BF7\u6C42\u4F53\u4E0D\u662F\u5408\u6CD5\u7684 JSON"
5144
- }
5145
- ]);
5146
- }
5147
- return result.data;
5189
+ return parseBodyByContentType(request);
5148
5190
  }
5149
- return queryToObject(url.searchParams);
5191
+ return { input: queryToObject(url.searchParams) };
5150
5192
  }
5151
- function isBlankText(text) {
5152
- return text.length === 0 || !/\S/.test(text);
5153
- }
5154
- 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
+ }
5155
5208
  const text = await request.text();
5156
- if (isBlankText(text)) return void 0;
5209
+ if (isBlankText(text)) {
5210
+ return { input: null };
5211
+ }
5157
5212
  const result = parseJsonBody(text);
5158
5213
  if (!result.success) {
5159
5214
  throw new ValidationError("\u8BF7\u6C42\u4F53\u4E0D\u662F\u5408\u6CD5\u7684 JSON", [
@@ -5166,7 +5221,13 @@ async function resolveBodyForQueryMethod(request) {
5166
5221
  }
5167
5222
  ]);
5168
5223
  }
5169
- 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);
5170
5231
  }
5171
5232
 
5172
5233
  // src/response/pendingMeta.ts
@@ -5301,12 +5362,17 @@ function markBuffered(response) {
5301
5362
  init_resolveInjection();
5302
5363
  function getBuiltinInjectionValue(type, ctx, body) {
5303
5364
  switch (type) {
5304
- case "query": {
5305
- const validated = ctx.__validatedQuery;
5306
- return validated ?? queryToObject(ctx.query);
5307
- }
5365
+ case "query":
5366
+ return ctx.query;
5308
5367
  case "params":
5309
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;
5310
5376
  case "headers":
5311
5377
  return ctx.headers;
5312
5378
  case "context":
@@ -5822,6 +5888,9 @@ function buildErrorResponse(err, config) {
5822
5888
  }
5823
5889
  }
5824
5890
 
5891
+ // src/server/handleWsUpgrade.ts
5892
+ init_generateSchemaFiles();
5893
+
5825
5894
  // src/middleware/loadMiddlewares.ts
5826
5895
  var middlewareCache = /* @__PURE__ */ new Map();
5827
5896
  var inFlight = /* @__PURE__ */ new Map();
@@ -6020,6 +6089,7 @@ function attachWebSocket(options) {
6020
6089
  server,
6021
6090
  routesRef,
6022
6091
  rootDir,
6092
+ dist,
6023
6093
  config,
6024
6094
  globalMiddlewares,
6025
6095
  trustedProxy = false,
@@ -6054,12 +6124,50 @@ function attachWebSocket(options) {
6054
6124
  const { route, params } = match;
6055
6125
  const headers = nodeHttpToWebHeaders(req);
6056
6126
  const host = req.headers.host ?? "localhost";
6057
- const url = `http://${host}${req.url ?? "/"}`;
6058
- 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 });
6059
6129
  const ctx = createContext(request, params, config, getClientIp(req, trustedProxy), registries);
6060
6130
  const meta = ctx.meta;
6061
6131
  let upgraded = false;
6062
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
+ }
6063
6171
  let handlers;
6064
6172
  try {
6065
6173
  const absoluteFilePath = path18.resolve(rootDir, route.filePath);
@@ -6267,7 +6375,8 @@ function createServer(options) {
6267
6375
  globalInjectors,
6268
6376
  bodyLimit,
6269
6377
  trustedProxy,
6270
- registries
6378
+ registries,
6379
+ routesRef.wsCurrent
6271
6380
  ).catch((err) => {
6272
6381
  console.error("[faapi] Request pipeline failed after error response attempt:", err);
6273
6382
  try {
@@ -6285,6 +6394,7 @@ function createServer(options) {
6285
6394
  server,
6286
6395
  routesRef,
6287
6396
  rootDir,
6397
+ dist,
6288
6398
  config,
6289
6399
  globalMiddlewares,
6290
6400
  trustedProxy,
@@ -6292,14 +6402,13 @@ function createServer(options) {
6292
6402
  });
6293
6403
  return { server, routesRef };
6294
6404
  }
6295
- function prepareRequest(req, config, bodyLimit, trustedProxy, registries, requestSignal) {
6296
- const { request, url } = toWebRequest(req, bodyLimit, requestSignal);
6405
+ function prepareRequest(req, request, url, config, trustedProxy, registries, preMatched) {
6297
6406
  const method = request.method.toUpperCase();
6298
6407
  const urlPath = url.pathname;
6299
6408
  const ctx = createContextFromUrl(
6300
6409
  request,
6301
6410
  url,
6302
- {},
6411
+ preMatched?.params ?? {},
6303
6412
  config,
6304
6413
  getClientIp(req, trustedProxy),
6305
6414
  registries
@@ -6330,19 +6439,33 @@ function getRoutePaths(route, rootDir, dist) {
6330
6439
  }
6331
6440
  var mergedInjectorsCache = /* @__PURE__ */ new WeakMap();
6332
6441
  function createRoutePipeline(opts) {
6333
- 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;
6334
6455
  return async () => {
6335
- const match = resolveRouteOrThrow(routes, method, urlPath);
6336
- ctx.params = match.params;
6456
+ const match = preMatched ?? resolveRouteOrThrow(routes, method, urlPath);
6337
6457
  const { route } = match;
6338
6458
  const { absFilePath, schemaPath } = getRoutePaths(route, rootDir, dist);
6339
6459
  const routeModule = await loadRouteModule(absFilePath, route.method, rootDir);
6340
- 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
+ }
6341
6464
  const inputType = getInputTypeForMethod(route.method);
6342
6465
  if (isDevOnDemandEnabled()) {
6343
6466
  const devDist = getDevDist();
6344
6467
  if (devDist) {
6345
- await ensureSchemaGenerated(schemaPath, route.filePath, routes, rootDir, dist);
6468
+ await ensureSchemaGenerated(schemaPath, route.filePath, routes, rootDir, dist, wsRoutes);
6346
6469
  }
6347
6470
  }
6348
6471
  const result = await validateInput(schemaPath, route.method, inputType, input);
@@ -6350,7 +6473,7 @@ function createRoutePipeline(opts) {
6350
6473
  throw new ValidationError("\u53C2\u6570\u6821\u9A8C\u5931\u8D25", result.issues);
6351
6474
  }
6352
6475
  if (inputType === "query" && result.data !== input && typeof result.data === "object" && result.data !== null) {
6353
- ctx.__validatedQuery = {
6476
+ ctx.query = {
6354
6477
  ...input,
6355
6478
  ...result.data
6356
6479
  };
@@ -6372,7 +6495,7 @@ function createRoutePipeline(opts) {
6372
6495
  throw new ValidationError("\u53C2\u6570\u6821\u9A8C\u5931\u8D25", queryResult.issues);
6373
6496
  }
6374
6497
  if (queryResult.data !== rawQuery && typeof queryResult.data === "object" && queryResult.data !== null) {
6375
- ctx.__validatedQuery = {
6498
+ ctx.query = {
6376
6499
  ...rawQuery,
6377
6500
  ...queryResult.data
6378
6501
  };
@@ -6381,16 +6504,20 @@ function createRoutePipeline(opts) {
6381
6504
  let body;
6382
6505
  if (inputType === "query" && hasBody(route.method)) {
6383
6506
  const parsed = await resolveBodyForQueryMethod(request);
6384
- if (parsed !== void 0) {
6385
- const bodyResult = await validateInput(schemaPath, route.method, "body", parsed);
6386
- if (!bodyResult.valid) {
6387
- throw new ValidationError("\u53C2\u6570\u6821\u9A8C\u5931\u8D25", bodyResult.issues);
6388
- }
6389
- body = bodyResult.data;
6507
+ if (parsed.rawBody !== void 0) {
6508
+ ctx.rawBody = parsed.rawBody;
6390
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;
6391
6515
  } else if (hasBody(route.method)) {
6392
6516
  body = result.data;
6393
6517
  }
6518
+ if (body !== void 0 && body !== null) {
6519
+ ctx.body = body;
6520
+ }
6394
6521
  if (route.middlewares === void 0 && route.injectors === void 0 && route.middlewarePaths) {
6395
6522
  await ensureMiddlewaresCompiled(route.middlewarePaths, rootDir);
6396
6523
  const bundle = await loadMergedMiddlewares(route.middlewarePaths);
@@ -6426,25 +6553,37 @@ async function sendErrorResponse(err, meta, res, onError, ctx) {
6426
6553
  }
6427
6554
  }
6428
6555
  }
6429
- 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) {
6430
6557
  let meta = { headers: {}, setCookies: [] };
6431
6558
  let ctx;
6559
+ let sseEarlySent = false;
6432
6560
  const abortController = new AbortController();
6433
6561
  res.on("close", () => {
6434
6562
  if (!res.writableEnded) abortController.abort();
6435
6563
  });
6436
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);
6437
6569
  const prepared = prepareRequest(
6438
6570
  req,
6571
+ request,
6572
+ url,
6439
6573
  config,
6440
- bodyLimit,
6441
6574
  trustedProxy,
6442
6575
  registries,
6443
- abortController.signal
6576
+ preMatched
6444
6577
  );
6445
6578
  ctx = prepared.ctx;
6446
6579
  meta = prepared.meta;
6447
- 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
+ };
6448
6587
  const routePipeline = createRoutePipeline({
6449
6588
  routes,
6450
6589
  method,
@@ -6454,12 +6593,25 @@ async function handleRequest(routes, rootDir, dist, req, res, outerMiddlewares,
6454
6593
  request,
6455
6594
  rootDir,
6456
6595
  dist,
6457
- globalInjectors
6596
+ globalInjectors,
6597
+ preMatched,
6598
+ wsRoutes
6458
6599
  });
6459
6600
  const response = outerMiddlewares.length > 0 ? await compose(outerMiddlewares, ctx, routePipeline) : await routePipeline();
6460
- await sendSuccessResponse(response, res);
6601
+ if (!sseEarlySent) {
6602
+ await sendSuccessResponse(response, res);
6603
+ }
6461
6604
  } catch (err) {
6462
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
+ }
6463
6615
  if (err instanceof PayloadTooLargeError) {
6464
6616
  res.setHeader("Connection", "close");
6465
6617
  res.once("finish", () => req.destroy());
@@ -6730,7 +6882,7 @@ import path25 from "path";
6730
6882
  import path23 from "path";
6731
6883
 
6732
6884
  // src/ast/extractAgentMetadata.ts
6733
- import ts10 from "typescript";
6885
+ import ts11 from "typescript";
6734
6886
  init_resolveTypeNode();
6735
6887
  function extractAgentMetadata(program, filePath, pathMeta) {
6736
6888
  const sourceFile = program.getSourceFile(filePath);
@@ -6775,23 +6927,23 @@ function extractAgentMetadata(program, filePath, pathMeta) {
6775
6927
  }
6776
6928
  function findConfigExport(sourceFile) {
6777
6929
  let result = null;
6778
- ts10.forEachChild(sourceFile, (node) => {
6930
+ ts11.forEachChild(sourceFile, (node) => {
6779
6931
  if (result) return;
6780
- if (ts10.isVariableStatement(node) && hasExportModifier(node)) {
6932
+ if (ts11.isVariableStatement(node) && hasExportModifier(node)) {
6781
6933
  for (const decl of node.declarationList.declarations) {
6782
6934
  if (result) break;
6783
- const nameText = ts10.isIdentifier(decl.name) ? decl.name.text : "";
6935
+ const nameText = ts11.isIdentifier(decl.name) ? decl.name.text : "";
6784
6936
  if (nameText !== "config" || !decl.initializer) continue;
6785
- if (ts10.isObjectLiteralExpression(decl.initializer)) {
6937
+ if (ts11.isObjectLiteralExpression(decl.initializer)) {
6786
6938
  result = { jsDocOwner: node, objectLiteral: decl.initializer };
6787
- } else if (ts10.isArrowFunction(decl.initializer)) {
6939
+ } else if (ts11.isArrowFunction(decl.initializer)) {
6788
6940
  result = { jsDocOwner: node, objectLiteral: getReturnObjectLiteral(decl.initializer) };
6789
6941
  } else {
6790
6942
  result = { jsDocOwner: node, objectLiteral: null };
6791
6943
  }
6792
6944
  }
6793
6945
  }
6794
- if (ts10.isFunctionDeclaration(node) && hasExportModifier(node) && node.name?.text === "config") {
6946
+ if (ts11.isFunctionDeclaration(node) && hasExportModifier(node) && node.name?.text === "config") {
6795
6947
  const returnObj = getReturnObjectLiteral(node);
6796
6948
  result = { jsDocOwner: node, objectLiteral: returnObj };
6797
6949
  }
@@ -6801,12 +6953,12 @@ function findConfigExport(sourceFile) {
6801
6953
  function getReturnObjectLiteral(fn) {
6802
6954
  const body = fn.body;
6803
6955
  if (!body) return null;
6804
- if (ts10.isObjectLiteralExpression(body)) {
6956
+ if (ts11.isObjectLiteralExpression(body)) {
6805
6957
  return body;
6806
6958
  }
6807
- if (ts10.isBlock(body)) {
6959
+ if (ts11.isBlock(body)) {
6808
6960
  for (const stmt of body.statements) {
6809
- if (ts10.isReturnStatement(stmt) && stmt.expression && ts10.isObjectLiteralExpression(stmt.expression)) {
6961
+ if (ts11.isReturnStatement(stmt) && stmt.expression && ts11.isObjectLiteralExpression(stmt.expression)) {
6810
6962
  return stmt.expression;
6811
6963
  }
6812
6964
  }
@@ -6821,8 +6973,8 @@ function extractConfigFields(objLit, sourceFile) {
6821
6973
  let maxTurns;
6822
6974
  let inputDescription;
6823
6975
  for (const prop of objLit.properties) {
6824
- if (ts10.isSpreadAssignment(prop)) continue;
6825
- if (!ts10.isPropertyAssignment(prop)) {
6976
+ if (ts11.isSpreadAssignment(prop)) continue;
6977
+ if (!ts11.isPropertyAssignment(prop)) {
6826
6978
  throw SchemaExtractionError.at(
6827
6979
  prop,
6828
6980
  "config",
@@ -6873,13 +7025,13 @@ function extractConfigFields(objLit, sourceFile) {
6873
7025
  return { systemPrompt, tools, agents, model, maxTurns, inputDescription };
6874
7026
  }
6875
7027
  function getPropertyName(name) {
6876
- if (ts10.isIdentifier(name)) return name.text;
6877
- if (ts10.isStringLiteral(name)) return name.text;
7028
+ if (ts11.isIdentifier(name)) return name.text;
7029
+ if (ts11.isStringLiteral(name)) return name.text;
6878
7030
  return null;
6879
7031
  }
6880
7032
  function extractStringValue(expr) {
6881
7033
  if (isStringLikeLiteral(expr)) return expr.text;
6882
- if (ts10.isBinaryExpression(expr) && expr.operatorToken.kind === ts10.SyntaxKind.PlusToken) {
7034
+ if (ts11.isBinaryExpression(expr) && expr.operatorToken.kind === ts11.SyntaxKind.PlusToken) {
6883
7035
  const left = extractStringValue(expr.left);
6884
7036
  if (left === void 0) return void 0;
6885
7037
  const right = extractStringValue(expr.right);
@@ -6925,17 +7077,17 @@ function requireNumberValue(prop, fieldName, sourceFile) {
6925
7077
  return value;
6926
7078
  }
6927
7079
  function extractNumberValue(expr) {
6928
- if (ts10.isNumericLiteral(expr)) {
7080
+ if (ts11.isNumericLiteral(expr)) {
6929
7081
  const num = Number(expr.text);
6930
7082
  return Number.isNaN(num) ? void 0 : num;
6931
7083
  }
6932
7084
  return void 0;
6933
7085
  }
6934
7086
  function isStringLikeLiteral(node) {
6935
- return ts10.isStringLiteral(node) || ts10.isNoSubstitutionTemplateLiteral(node);
7087
+ return ts11.isStringLiteral(node) || ts11.isNoSubstitutionTemplateLiteral(node);
6936
7088
  }
6937
7089
  function extractStringArrayValue(expr) {
6938
- if (!ts10.isArrayLiteralExpression(expr)) return void 0;
7090
+ if (!ts11.isArrayLiteralExpression(expr)) return void 0;
6939
7091
  const values = [];
6940
7092
  for (const element of expr.elements) {
6941
7093
  const value = extractStringValue(element);
@@ -7912,14 +8064,14 @@ async function createDevApp(options) {
7912
8064
  void (async () => {
7913
8065
  try {
7914
8066
  const { generateSchemaFiles: generateSchemaFiles2 } = await Promise.resolve().then(() => (init_generateSchemaFiles(), generateSchemaFiles_exports));
7915
- await generateSchemaFiles2(sorted, ctx.rootDir, ctx.dist);
8067
+ await generateSchemaFiles2(sorted, ctx.rootDir, ctx.dist, reScanned.wsRoutes);
7916
8068
  } catch (err) {
7917
8069
  console.error("[faapi] Background schema regeneration failed:", err);
7918
8070
  }
7919
8071
  })();
7920
8072
  } else {
7921
8073
  const { generateSchemaFiles: generateSchemaFiles2 } = await Promise.resolve().then(() => (init_generateSchemaFiles(), generateSchemaFiles_exports));
7922
- await generateSchemaFiles2(sorted, ctx.rootDir, ctx.dist);
8074
+ await generateSchemaFiles2(sorted, ctx.rootDir, ctx.dist, reScanned.wsRoutes);
7923
8075
  }
7924
8076
  ctx.updateRoutes(sorted, reScanned.wsRoutes);
7925
8077
  };