@lark-apaas/devtool-kits 1.2.24 → 1.2.25-alpha.20260819082505

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.d.cts CHANGED
@@ -274,6 +274,133 @@ declare function getQueryParam(req: AnyRequest, key: string): string | undefined
274
274
  */
275
275
  declare function registerMiddlewares(server: ExpressApp | ViteMiddleware, middlewares: Middleware[], options?: Partial<MiddlewareContext>): void;
276
276
 
277
+ /**
278
+ * 妙搭应用 Health Check 接口类型定义。
279
+ *
280
+ * 协议(飞书文档 - 妙搭应用 Health Check 接口规范):
281
+ * - HTTP connection refused / timeout → dev server 没起来 → 外壳重启沙箱
282
+ * - 200 + { ready: true } → 应用已就绪,放行
283
+ * - 200 + { ready: false, failed: false } → 还在启动,继续轮询
284
+ * - 200 + { ready: false, failed: true } → 终态失败,停止轮询并把 message 呈现给用户
285
+ *
286
+ * 约束:`ready: true` 时 `failed` 必须为 false。
287
+ */
288
+ interface HealthPayload {
289
+ /** 应用是否已就绪。client-only 栈等价于 dev server 自身就绪。 */
290
+ ready: boolean;
291
+ /** 终态失败:等下去也不会好。缺省 false。 */
292
+ failed: boolean;
293
+ /** 失败时的人类可读说明,仅用于展示与排查,外壳不得用于流程决策。 */
294
+ message?: string;
295
+ }
296
+ /** 单个信号的判定结果,仅内部使用。 */
297
+ interface ServerLiveness {
298
+ /** Nest 服务当前是否存活 */
299
+ alive: boolean;
300
+ /** 最近一次成功 listen 的进程 pid(无则 null) */
301
+ pid: number | null;
302
+ /** 最近一次成功 listen 的时间(ISO,无则 null) */
303
+ listenAt: string | null;
304
+ }
305
+ interface ResolveHealthOptions {
306
+ /** 项目根目录 */
307
+ rootDir: string;
308
+ /** 日志目录,默认 `<rootDir>/logs` */
309
+ logDir?: string;
310
+ /**
311
+ * 本栈是否包含 Node 服务端。
312
+ * false(vite-react / html 等 client-only 栈)时直接返回 dev server 自身状态,
313
+ * 行为与改造前完全一致。
314
+ */
315
+ hasServer: boolean;
316
+ /** dev server 自身是否已 listen */
317
+ clientReady: boolean;
318
+ /** 本次 dev 会话首次拉起 server 的时间戳(ms)。缺省时不做超时判定。 */
319
+ firstStartedAt?: number | null;
320
+ /** 编译产物入口,默认 `<rootDir>/dist/server/main.js` */
321
+ serverEntry?: string;
322
+ /** 超时上限(ms),超过则判 failed。默认 300000。 */
323
+ timeoutMs?: number;
324
+ /**
325
+ * 本次 dev 会话的启动时间(ms),用于排除 pid 复用导致的陈旧 marker。
326
+ * 与 `firstStartedAt` 分开:后者是「超时从何时起算」的锚点,崩溃后会前移。
327
+ */
328
+ sessionStartedAt?: number | null;
329
+ /** 探测到 server 存活时回调,供宿主记录「最后一次存活时刻」 */
330
+ onServerAlive?: () => void;
331
+ /** 注入点,便于测试 */
332
+ now?: () => number;
333
+ }
334
+
335
+ /**
336
+ * 判定应用健康状态。
337
+ *
338
+ * 设计原则:**`failed: true` 只能由肯定性事实触发,绝不能由否定性推断触发。**
339
+ * 「读不到日志」「匹配不到 marker」「产物看不见」都属于否定性推断,
340
+ * 一律降级为「还在启动」(`ready:false, failed:false`),让外壳继续等。
341
+ * 只有「超过 timeoutMs 仍未就绪」这一个时间事实可以判 failed。
342
+ */
343
+ declare function resolveHealth(options: ResolveHealthOptions): HealthPayload;
344
+
345
+ /**
346
+ * `process.kill(pid, 0)` 不发信号,只做存在性检查。
347
+ * EPERM 表示进程存在但当前用户无权发信号 —— 仍算存活。
348
+ */
349
+ declare function isPidAlive(pid: number): boolean;
350
+ /**
351
+ * 判定 Nest 服务当前是否存活。
352
+ *
353
+ * 两步都是「肯定性事实」:
354
+ * 1. 日志里存在 listen marker → 曾经成功启动过(匹配不到就不下结论)
355
+ * 2. 该 marker 的 pid 仍然存活 → 现在确实还活着
356
+ *
357
+ * 只有第 1 步会导致误判:`dev.js` 以 append 模式写日志,历史成功记录会长期留存,
358
+ * 因此第 2 步的 pid 验活不可省略。
359
+ *
360
+ * `sessionStartedAt` 用于排除 pid 复用:marker 时间早于本次会话启动时间的,一律不采信。
361
+ */
362
+ declare function resolveServerLiveness(logDir: string, sessionStartedAt?: number | null): ServerLiveness;
363
+
364
+ /** 沙箱外壳探测的是固定根路径,不跟 clientBasePath 走 */
365
+ declare const HEALTH_ENDPOINT = "/dev/health";
366
+ interface HealthHandlerOptions {
367
+ /** 项目根目录 */
368
+ rootDir: string;
369
+ /** 日志目录,默认 `<rootDir>/logs` */
370
+ logDir?: string;
371
+ /** 本栈是否包含 Node 服务端 */
372
+ hasServer: boolean;
373
+ /** dev server 自身是否已 listen(由各 preset 注入) */
374
+ isClientReady: () => boolean;
375
+ /** 超时上限(ms);未传时读环境变量 MIAODA_HEALTH_TIMEOUT_MS,再兜底 5min */
376
+ timeoutMs?: number;
377
+ }
378
+ /**
379
+ * 构造一个有状态的健康探测函数。宿主自行决定怎么把结果写进响应
380
+ * (Express 用 `res.status().json()`,Connect/Vite 用 `res.end()`)。
381
+ *
382
+ * 之所以有状态:超时窗口必须从「最后一次确认存活」起算,而不是从会话启动起算。
383
+ * 否则一个健康跑了 10 分钟的服务一旦崩溃,会因为 `now - firstStartedAt` 早已超限
384
+ * 而被**立刻**判成终态失败,supervisor 连重启的机会都没有。
385
+ *
386
+ * 判定本身出任何错都降级为「还在启动」——health 是旁路能力,
387
+ * 绝不能因为它自己的异常而让外壳误判失败、阻塞用户启动。
388
+ */
389
+ declare function createHealthProbe(options: HealthHandlerOptions): () => HealthPayload;
390
+ /**
391
+ * 单次计算健康状态(无状态版本)。
392
+ * 长驻宿主请用 {@link createHealthProbe},它会正确处理「崩溃后重新计时」。
393
+ */
394
+ declare function getHealthPayload(options: HealthHandlerOptions): HealthPayload;
395
+ /**
396
+ * 构造 `/dev/health` 的 Connect / Vite 中间件。
397
+ *
398
+ * 响应码恒 200:`connection refused` / `timeout` 已被外壳用来表示
399
+ * 「dev server 自身没起来 → 重启沙箱」,应用级失败必须走 body,
400
+ * 否则会和传输层失败混淆。
401
+ */
402
+ declare function createHealthHandler(options: HealthHandlerOptions): (req: IncomingMessage, res: ServerResponse, next?: () => void) => void;
403
+
277
404
  interface PageRouteInfo {
278
405
  path?: string;
279
406
  index?: boolean;
@@ -300,4 +427,4 @@ declare function parseRoutesFromFile(appPath: string, basePath: string, options?
300
427
  path: string;
301
428
  }>;
302
429
 
303
- export { type GlobalMiddleware, type Middleware, type MiddlewareContext, type Options, type PageRouteInfo, type ParseRoutesOptions, type RouteInfo, type RouteMiddleware, buildFullPath, calculateFileHash, createCollectLogsMiddleware, createDevLogsMiddleware, createOpenapiMiddleware, evaluateTemplateLiteral, extractPageRouteInfo, getQuery, getQueryParam, handleDevProxyError, isRouteComponent, normalizeBasePath, parseAndGenerateNestResourceTemplate, parseApiRoutes, parseRoutesFromFile, postprocessDrizzleSchema, registerMiddlewares, routeParserLog, sendError, sendJson, sendSuccess };
430
+ export { type GlobalMiddleware, HEALTH_ENDPOINT, type HealthHandlerOptions, type HealthPayload, type Middleware, type MiddlewareContext, type Options, type PageRouteInfo, type ParseRoutesOptions, type ResolveHealthOptions, type RouteInfo, type RouteMiddleware, type ServerLiveness, buildFullPath, calculateFileHash, createCollectLogsMiddleware, createDevLogsMiddleware, createHealthHandler, createHealthProbe, createOpenapiMiddleware, evaluateTemplateLiteral, extractPageRouteInfo, getHealthPayload, getQuery, getQueryParam, handleDevProxyError, isPidAlive, isRouteComponent, normalizeBasePath, parseAndGenerateNestResourceTemplate, parseApiRoutes, parseRoutesFromFile, postprocessDrizzleSchema, registerMiddlewares, resolveHealth, resolveServerLiveness, routeParserLog, sendError, sendJson, sendSuccess };
package/dist/index.d.ts CHANGED
@@ -274,6 +274,133 @@ declare function getQueryParam(req: AnyRequest, key: string): string | undefined
274
274
  */
275
275
  declare function registerMiddlewares(server: ExpressApp | ViteMiddleware, middlewares: Middleware[], options?: Partial<MiddlewareContext>): void;
276
276
 
277
+ /**
278
+ * 妙搭应用 Health Check 接口类型定义。
279
+ *
280
+ * 协议(飞书文档 - 妙搭应用 Health Check 接口规范):
281
+ * - HTTP connection refused / timeout → dev server 没起来 → 外壳重启沙箱
282
+ * - 200 + { ready: true } → 应用已就绪,放行
283
+ * - 200 + { ready: false, failed: false } → 还在启动,继续轮询
284
+ * - 200 + { ready: false, failed: true } → 终态失败,停止轮询并把 message 呈现给用户
285
+ *
286
+ * 约束:`ready: true` 时 `failed` 必须为 false。
287
+ */
288
+ interface HealthPayload {
289
+ /** 应用是否已就绪。client-only 栈等价于 dev server 自身就绪。 */
290
+ ready: boolean;
291
+ /** 终态失败:等下去也不会好。缺省 false。 */
292
+ failed: boolean;
293
+ /** 失败时的人类可读说明,仅用于展示与排查,外壳不得用于流程决策。 */
294
+ message?: string;
295
+ }
296
+ /** 单个信号的判定结果,仅内部使用。 */
297
+ interface ServerLiveness {
298
+ /** Nest 服务当前是否存活 */
299
+ alive: boolean;
300
+ /** 最近一次成功 listen 的进程 pid(无则 null) */
301
+ pid: number | null;
302
+ /** 最近一次成功 listen 的时间(ISO,无则 null) */
303
+ listenAt: string | null;
304
+ }
305
+ interface ResolveHealthOptions {
306
+ /** 项目根目录 */
307
+ rootDir: string;
308
+ /** 日志目录,默认 `<rootDir>/logs` */
309
+ logDir?: string;
310
+ /**
311
+ * 本栈是否包含 Node 服务端。
312
+ * false(vite-react / html 等 client-only 栈)时直接返回 dev server 自身状态,
313
+ * 行为与改造前完全一致。
314
+ */
315
+ hasServer: boolean;
316
+ /** dev server 自身是否已 listen */
317
+ clientReady: boolean;
318
+ /** 本次 dev 会话首次拉起 server 的时间戳(ms)。缺省时不做超时判定。 */
319
+ firstStartedAt?: number | null;
320
+ /** 编译产物入口,默认 `<rootDir>/dist/server/main.js` */
321
+ serverEntry?: string;
322
+ /** 超时上限(ms),超过则判 failed。默认 300000。 */
323
+ timeoutMs?: number;
324
+ /**
325
+ * 本次 dev 会话的启动时间(ms),用于排除 pid 复用导致的陈旧 marker。
326
+ * 与 `firstStartedAt` 分开:后者是「超时从何时起算」的锚点,崩溃后会前移。
327
+ */
328
+ sessionStartedAt?: number | null;
329
+ /** 探测到 server 存活时回调,供宿主记录「最后一次存活时刻」 */
330
+ onServerAlive?: () => void;
331
+ /** 注入点,便于测试 */
332
+ now?: () => number;
333
+ }
334
+
335
+ /**
336
+ * 判定应用健康状态。
337
+ *
338
+ * 设计原则:**`failed: true` 只能由肯定性事实触发,绝不能由否定性推断触发。**
339
+ * 「读不到日志」「匹配不到 marker」「产物看不见」都属于否定性推断,
340
+ * 一律降级为「还在启动」(`ready:false, failed:false`),让外壳继续等。
341
+ * 只有「超过 timeoutMs 仍未就绪」这一个时间事实可以判 failed。
342
+ */
343
+ declare function resolveHealth(options: ResolveHealthOptions): HealthPayload;
344
+
345
+ /**
346
+ * `process.kill(pid, 0)` 不发信号,只做存在性检查。
347
+ * EPERM 表示进程存在但当前用户无权发信号 —— 仍算存活。
348
+ */
349
+ declare function isPidAlive(pid: number): boolean;
350
+ /**
351
+ * 判定 Nest 服务当前是否存活。
352
+ *
353
+ * 两步都是「肯定性事实」:
354
+ * 1. 日志里存在 listen marker → 曾经成功启动过(匹配不到就不下结论)
355
+ * 2. 该 marker 的 pid 仍然存活 → 现在确实还活着
356
+ *
357
+ * 只有第 1 步会导致误判:`dev.js` 以 append 模式写日志,历史成功记录会长期留存,
358
+ * 因此第 2 步的 pid 验活不可省略。
359
+ *
360
+ * `sessionStartedAt` 用于排除 pid 复用:marker 时间早于本次会话启动时间的,一律不采信。
361
+ */
362
+ declare function resolveServerLiveness(logDir: string, sessionStartedAt?: number | null): ServerLiveness;
363
+
364
+ /** 沙箱外壳探测的是固定根路径,不跟 clientBasePath 走 */
365
+ declare const HEALTH_ENDPOINT = "/dev/health";
366
+ interface HealthHandlerOptions {
367
+ /** 项目根目录 */
368
+ rootDir: string;
369
+ /** 日志目录,默认 `<rootDir>/logs` */
370
+ logDir?: string;
371
+ /** 本栈是否包含 Node 服务端 */
372
+ hasServer: boolean;
373
+ /** dev server 自身是否已 listen(由各 preset 注入) */
374
+ isClientReady: () => boolean;
375
+ /** 超时上限(ms);未传时读环境变量 MIAODA_HEALTH_TIMEOUT_MS,再兜底 5min */
376
+ timeoutMs?: number;
377
+ }
378
+ /**
379
+ * 构造一个有状态的健康探测函数。宿主自行决定怎么把结果写进响应
380
+ * (Express 用 `res.status().json()`,Connect/Vite 用 `res.end()`)。
381
+ *
382
+ * 之所以有状态:超时窗口必须从「最后一次确认存活」起算,而不是从会话启动起算。
383
+ * 否则一个健康跑了 10 分钟的服务一旦崩溃,会因为 `now - firstStartedAt` 早已超限
384
+ * 而被**立刻**判成终态失败,supervisor 连重启的机会都没有。
385
+ *
386
+ * 判定本身出任何错都降级为「还在启动」——health 是旁路能力,
387
+ * 绝不能因为它自己的异常而让外壳误判失败、阻塞用户启动。
388
+ */
389
+ declare function createHealthProbe(options: HealthHandlerOptions): () => HealthPayload;
390
+ /**
391
+ * 单次计算健康状态(无状态版本)。
392
+ * 长驻宿主请用 {@link createHealthProbe},它会正确处理「崩溃后重新计时」。
393
+ */
394
+ declare function getHealthPayload(options: HealthHandlerOptions): HealthPayload;
395
+ /**
396
+ * 构造 `/dev/health` 的 Connect / Vite 中间件。
397
+ *
398
+ * 响应码恒 200:`connection refused` / `timeout` 已被外壳用来表示
399
+ * 「dev server 自身没起来 → 重启沙箱」,应用级失败必须走 body,
400
+ * 否则会和传输层失败混淆。
401
+ */
402
+ declare function createHealthHandler(options: HealthHandlerOptions): (req: IncomingMessage, res: ServerResponse, next?: () => void) => void;
403
+
277
404
  interface PageRouteInfo {
278
405
  path?: string;
279
406
  index?: boolean;
@@ -300,4 +427,4 @@ declare function parseRoutesFromFile(appPath: string, basePath: string, options?
300
427
  path: string;
301
428
  }>;
302
429
 
303
- export { type GlobalMiddleware, type Middleware, type MiddlewareContext, type Options, type PageRouteInfo, type ParseRoutesOptions, type RouteInfo, type RouteMiddleware, buildFullPath, calculateFileHash, createCollectLogsMiddleware, createDevLogsMiddleware, createOpenapiMiddleware, evaluateTemplateLiteral, extractPageRouteInfo, getQuery, getQueryParam, handleDevProxyError, isRouteComponent, normalizeBasePath, parseAndGenerateNestResourceTemplate, parseApiRoutes, parseRoutesFromFile, postprocessDrizzleSchema, registerMiddlewares, routeParserLog, sendError, sendJson, sendSuccess };
430
+ export { type GlobalMiddleware, HEALTH_ENDPOINT, type HealthHandlerOptions, type HealthPayload, type Middleware, type MiddlewareContext, type Options, type PageRouteInfo, type ParseRoutesOptions, type ResolveHealthOptions, type RouteInfo, type RouteMiddleware, type ServerLiveness, buildFullPath, calculateFileHash, createCollectLogsMiddleware, createDevLogsMiddleware, createHealthHandler, createHealthProbe, createOpenapiMiddleware, evaluateTemplateLiteral, extractPageRouteInfo, getHealthPayload, getQuery, getQueryParam, handleDevProxyError, isPidAlive, isRouteComponent, normalizeBasePath, parseAndGenerateNestResourceTemplate, parseApiRoutes, parseRoutesFromFile, postprocessDrizzleSchema, registerMiddlewares, resolveHealth, resolveServerLiveness, routeParserLog, sendError, sendJson, sendSuccess };
package/dist/index.js CHANGED
@@ -1610,8 +1610,8 @@ function hasSpecialPatterns(pattern) {
1610
1610
  return /[{*]/.test(pattern);
1611
1611
  }
1612
1612
  __name(hasSpecialPatterns, "hasSpecialPatterns");
1613
- function normalizePathForMatching(path5) {
1614
- return path5.replace(/\/+/g, "/").replace(/\/+$/, "");
1613
+ function normalizePathForMatching(path8) {
1614
+ return path8.replace(/\/+/g, "/").replace(/\/+$/, "");
1615
1615
  }
1616
1616
  __name(normalizePathForMatching, "normalizePathForMatching");
1617
1617
 
@@ -2160,7 +2160,7 @@ __name(readLogsBySource, "readLogsBySource");
2160
2160
  // src/middlewares/dev-logs/services/trigger.service.ts
2161
2161
  import { createReadStream as createReadStream3 } from "fs";
2162
2162
  import { createInterface as createInterface3 } from "readline";
2163
- async function readTriggerList(filePath, trigger, path5, limit, triggerID) {
2163
+ async function readTriggerList(filePath, trigger, path8, limit, triggerID) {
2164
2164
  if (!await fileExists(filePath)) {
2165
2165
  return void 0;
2166
2166
  }
@@ -2186,7 +2186,7 @@ async function readTriggerList(filePath, trigger, path5, limit, triggerID) {
2186
2186
  if (alreadyAdded) {
2187
2187
  return false;
2188
2188
  }
2189
- const isAutomationTrigger = builder.path?.endsWith(path5);
2189
+ const isAutomationTrigger = builder.path?.endsWith(path8);
2190
2190
  if (!isAutomationTrigger) {
2191
2191
  return false;
2192
2192
  }
@@ -2269,7 +2269,7 @@ async function readTriggerList(filePath, trigger, path5, limit, triggerID) {
2269
2269
  };
2270
2270
  }
2271
2271
  __name(readTriggerList, "readTriggerList");
2272
- async function readTriggerDetail(filePath, path5, instanceID) {
2272
+ async function readTriggerDetail(filePath, path8, instanceID) {
2273
2273
  const exists = await fileExists(filePath);
2274
2274
  if (!exists) {
2275
2275
  return void 0;
@@ -2285,7 +2285,7 @@ async function readTriggerDetail(filePath, path5, instanceID) {
2285
2285
  for await (const line of rl) {
2286
2286
  const entry = parseLogLine(line);
2287
2287
  if (!entry) continue;
2288
- const isAutomationTrigger = entry.path?.endsWith(path5);
2288
+ const isAutomationTrigger = entry.path?.endsWith(path8);
2289
2289
  const hasInstanceID = entry.instance_id === instanceID && entry.trigger;
2290
2290
  if (!isAutomationTrigger || !hasInstanceID) continue;
2291
2291
  matches.push(entry);
@@ -2539,16 +2539,16 @@ function createGetTriggerListHandler(logDir) {
2539
2539
  });
2540
2540
  }
2541
2541
  const triggerID = typeof req.query.triggerID === "string" ? req.query.triggerID.trim() : void 0;
2542
- const path5 = typeof req.query.path === "string" ? req.query.path.trim() : "/__innerapi__/automation/invoke";
2542
+ const path8 = typeof req.query.path === "string" ? req.query.path.trim() : "/__innerapi__/automation/invoke";
2543
2543
  const limit = parseLimit(req.query.limit, 10, 200);
2544
2544
  try {
2545
- const result = await readTriggerList(traceLogPath, trigger, path5, limit, triggerID);
2545
+ const result = await readTriggerList(traceLogPath, trigger, path8, limit, triggerID);
2546
2546
  if (!result) {
2547
2547
  return handleNotFound(res, traceLogPath);
2548
2548
  }
2549
2549
  res.json({
2550
2550
  file: getRelativePath(traceLogPath),
2551
- path: path5,
2551
+ path: path8,
2552
2552
  ...result
2553
2553
  });
2554
2554
  } catch (error) {
@@ -2566,9 +2566,9 @@ function createGetTriggerDetailHandler(logDir) {
2566
2566
  message: "instanceID is required"
2567
2567
  });
2568
2568
  }
2569
- const path5 = typeof req.query.path === "string" ? req.query.path.trim() : "/__innerapi__/automation/invoke";
2569
+ const path8 = typeof req.query.path === "string" ? req.query.path.trim() : "/__innerapi__/automation/invoke";
2570
2570
  try {
2571
- const result = await readTriggerDetail(traceLogPath, path5, instanceID);
2571
+ const result = await readTriggerDetail(traceLogPath, path8, instanceID);
2572
2572
  if (!result) {
2573
2573
  return handleNotFound(res, traceLogPath);
2574
2574
  }
@@ -3194,8 +3194,8 @@ __name(createSSEHandler, "createSSEHandler");
3194
3194
 
3195
3195
  // src/middlewares/dev-logs/api-list-handler.ts
3196
3196
  var SERVER_PORT = process.env.SERVER_PORT || "3000";
3197
- function extractModuleFromPath(path5) {
3198
- const segments = path5.split("/").filter(Boolean);
3197
+ function extractModuleFromPath(path8) {
3198
+ const segments = path8.split("/").filter(Boolean);
3199
3199
  let startIndex = 0;
3200
3200
  if (segments[0] === "api") {
3201
3201
  startIndex = 1;
@@ -3210,8 +3210,8 @@ function extractModuleFromPath(path5) {
3210
3210
  return moduleName;
3211
3211
  }
3212
3212
  __name(extractModuleFromPath, "extractModuleFromPath");
3213
- function generateRouteId(method, path5) {
3214
- const cleanPath = path5.replace(/[/:]/g, "_").replace(/^_+|_+$/g, "");
3213
+ function generateRouteId(method, path8) {
3214
+ const cleanPath = path8.replace(/[/:]/g, "_").replace(/^_+|_+$/g, "");
3215
3215
  return `${method.toLowerCase()}_${cleanPath}`;
3216
3216
  }
3217
3217
  __name(generateRouteId, "generateRouteId");
@@ -3685,17 +3685,217 @@ function registerMiddlewares(server, middlewares, options) {
3685
3685
  }
3686
3686
  }
3687
3687
  __name(registerMiddlewares, "registerMiddlewares");
3688
+
3689
+ // src/health/resolve.ts
3690
+ import fs11 from "fs";
3691
+ import path6 from "path";
3692
+
3693
+ // src/health/server-liveness.ts
3694
+ import fs10 from "fs";
3695
+ import path5 from "path";
3696
+ var TAIL_BYTES = 64 * 1024;
3697
+ function isListenMarker(line) {
3698
+ const message = String(line.message ?? "");
3699
+ if (line.context === "Bootstrap" && message.startsWith("Server running on")) {
3700
+ return true;
3701
+ }
3702
+ return line.context === "PlatformHealth" && message.startsWith("listening");
3703
+ }
3704
+ __name(isListenMarker, "isListenMarker");
3705
+ function readLastListenMarker(logFile) {
3706
+ let fd;
3707
+ try {
3708
+ const size = fs10.statSync(logFile).size;
3709
+ if (size === 0) return null;
3710
+ const length = Math.min(size, TAIL_BYTES);
3711
+ const buf = Buffer.alloc(length);
3712
+ fd = fs10.openSync(logFile, "r");
3713
+ fs10.readSync(fd, buf, 0, length, size - length);
3714
+ const lines = buf.toString("utf8").split("\n");
3715
+ for (let i = lines.length - 1; i >= 0; i--) {
3716
+ const raw = lines[i].trim();
3717
+ if (!raw.startsWith("{")) continue;
3718
+ try {
3719
+ const parsed = JSON.parse(raw);
3720
+ if (isListenMarker(parsed)) return parsed;
3721
+ } catch {
3722
+ }
3723
+ }
3724
+ } catch {
3725
+ } finally {
3726
+ if (fd !== void 0) {
3727
+ try {
3728
+ fs10.closeSync(fd);
3729
+ } catch {
3730
+ }
3731
+ }
3732
+ }
3733
+ return null;
3734
+ }
3735
+ __name(readLastListenMarker, "readLastListenMarker");
3736
+ function isPidAlive(pid) {
3737
+ if (!Number.isInteger(pid) || pid <= 0) return false;
3738
+ try {
3739
+ process.kill(pid, 0);
3740
+ return true;
3741
+ } catch (err) {
3742
+ return err?.code === "EPERM";
3743
+ }
3744
+ }
3745
+ __name(isPidAlive, "isPidAlive");
3746
+ function resolveServerLiveness(logDir, sessionStartedAt) {
3747
+ const marker = readLastListenMarker(path5.join(logDir, "server.log"));
3748
+ if (!marker || typeof marker.pid !== "number") {
3749
+ return {
3750
+ alive: false,
3751
+ pid: null,
3752
+ listenAt: null
3753
+ };
3754
+ }
3755
+ const listenAt = typeof marker.time === "string" ? marker.time : null;
3756
+ if (sessionStartedAt && listenAt) {
3757
+ const markerMs = Date.parse(listenAt);
3758
+ if (Number.isFinite(markerMs) && markerMs < sessionStartedAt - 5e3) {
3759
+ return {
3760
+ alive: false,
3761
+ pid: marker.pid,
3762
+ listenAt
3763
+ };
3764
+ }
3765
+ }
3766
+ return {
3767
+ alive: isPidAlive(marker.pid),
3768
+ pid: marker.pid,
3769
+ listenAt
3770
+ };
3771
+ }
3772
+ __name(resolveServerLiveness, "resolveServerLiveness");
3773
+
3774
+ // src/health/resolve.ts
3775
+ var DEFAULT_TIMEOUT_MS = 5 * 60 * 1e3;
3776
+ function resolveHealth(options) {
3777
+ const { rootDir, hasServer, clientReady, firstStartedAt, logDir = path6.join(rootDir, "logs"), serverEntry = path6.join(rootDir, "dist", "server", "main.js"), timeoutMs = DEFAULT_TIMEOUT_MS, now = Date.now, sessionStartedAt = null, onServerAlive } = options;
3778
+ if (!hasServer) {
3779
+ return {
3780
+ ready: clientReady,
3781
+ failed: false
3782
+ };
3783
+ }
3784
+ const liveness = resolveServerLiveness(logDir, sessionStartedAt);
3785
+ if (liveness.alive) {
3786
+ onServerAlive?.();
3787
+ return {
3788
+ ready: clientReady,
3789
+ failed: false
3790
+ };
3791
+ }
3792
+ const elapsed = firstStartedAt ? now() - firstStartedAt : 0;
3793
+ if (!firstStartedAt || elapsed <= timeoutMs) {
3794
+ return {
3795
+ ready: false,
3796
+ failed: false
3797
+ };
3798
+ }
3799
+ const seconds = Math.round(elapsed / 1e3);
3800
+ const compiled = safeExists(serverEntry);
3801
+ const message = compiled ? `\u540E\u7AEF\u670D\u52A1\u542F\u52A8\u8D85\u65F6\uFF08\u5DF2\u7B49\u5F85 ${seconds}s\uFF09\uFF1A\u7F16\u8BD1\u4EA7\u7269\u5DF2\u751F\u6210\u4F46\u670D\u52A1\u672A\u76D1\u542C\uFF0C\u8BF7\u67E5\u770B ${path6.join(logDir, "server.std.log")}` : `\u540E\u7AEF\u670D\u52A1\u7F16\u8BD1\u672A\u901A\u8FC7\uFF08\u5DF2\u7B49\u5F85 ${seconds}s\uFF09\uFF1A\u672A\u751F\u6210 ${path6.relative(rootDir, serverEntry)}\uFF0C\u8BF7\u67E5\u770B ${path6.join(logDir, "server.std.log")}`;
3802
+ return {
3803
+ ready: false,
3804
+ failed: true,
3805
+ message
3806
+ };
3807
+ }
3808
+ __name(resolveHealth, "resolveHealth");
3809
+ function safeExists(file) {
3810
+ try {
3811
+ return fs11.existsSync(file);
3812
+ } catch {
3813
+ return false;
3814
+ }
3815
+ }
3816
+ __name(safeExists, "safeExists");
3817
+
3818
+ // src/health/handler.ts
3819
+ import fs12 from "fs";
3820
+ import path7 from "path";
3821
+ var HEALTH_ENDPOINT = "/dev/health";
3822
+ function resolveTimeoutMs(explicit) {
3823
+ if (typeof explicit === "number") return explicit;
3824
+ const raw = Number(process.env.MIAODA_HEALTH_TIMEOUT_MS);
3825
+ return Number.isFinite(raw) && raw > 0 ? raw : void 0;
3826
+ }
3827
+ __name(resolveTimeoutMs, "resolveTimeoutMs");
3828
+ function readSessionStartedAt(logDir) {
3829
+ try {
3830
+ const raw = fs12.readFileSync(path7.join(logDir, "dev-status.json"), "utf8");
3831
+ const parsed = JSON.parse(raw);
3832
+ return typeof parsed.firstStartedAt === "number" ? parsed.firstStartedAt : null;
3833
+ } catch {
3834
+ return null;
3835
+ }
3836
+ }
3837
+ __name(readSessionStartedAt, "readSessionStartedAt");
3838
+ function createHealthProbe(options) {
3839
+ const logDir = options.logDir ?? path7.join(options.rootDir, "logs");
3840
+ let lastAliveAt = null;
3841
+ return /* @__PURE__ */ __name(function probe() {
3842
+ try {
3843
+ const sessionStartedAt = readSessionStartedAt(logDir);
3844
+ const since = lastAliveAt !== null && (sessionStartedAt === null || lastAliveAt > sessionStartedAt) ? lastAliveAt : sessionStartedAt;
3845
+ const payload = resolveHealth({
3846
+ rootDir: options.rootDir,
3847
+ logDir,
3848
+ hasServer: options.hasServer,
3849
+ clientReady: options.isClientReady(),
3850
+ firstStartedAt: since,
3851
+ timeoutMs: resolveTimeoutMs(options.timeoutMs),
3852
+ onServerAlive: /* @__PURE__ */ __name(() => {
3853
+ lastAliveAt = Date.now();
3854
+ }, "onServerAlive")
3855
+ });
3856
+ return payload;
3857
+ } catch {
3858
+ return {
3859
+ ready: false,
3860
+ failed: false
3861
+ };
3862
+ }
3863
+ }, "probe");
3864
+ }
3865
+ __name(createHealthProbe, "createHealthProbe");
3866
+ function getHealthPayload(options) {
3867
+ return createHealthProbe(options)();
3868
+ }
3869
+ __name(getHealthPayload, "getHealthPayload");
3870
+ function createHealthHandler(options) {
3871
+ const probe = createHealthProbe(options);
3872
+ return /* @__PURE__ */ __name(function healthHandler(req, res, next) {
3873
+ if (req.method && req.method !== "GET" && next) {
3874
+ return next();
3875
+ }
3876
+ res.setHeader("Content-Type", "application/json");
3877
+ res.setHeader("Cache-Control", "no-store");
3878
+ res.statusCode = 200;
3879
+ res.end(JSON.stringify(probe()));
3880
+ }, "healthHandler");
3881
+ }
3882
+ __name(createHealthHandler, "createHealthHandler");
3688
3883
  export {
3884
+ HEALTH_ENDPOINT,
3689
3885
  buildFullPath,
3690
3886
  calculateFileHash,
3691
3887
  createCollectLogsMiddleware,
3692
3888
  createDevLogsMiddleware,
3889
+ createHealthHandler,
3890
+ createHealthProbe,
3693
3891
  createOpenapiMiddleware,
3694
3892
  evaluateTemplateLiteral,
3695
3893
  extractPageRouteInfo,
3894
+ getHealthPayload,
3696
3895
  getQuery,
3697
3896
  getQueryParam,
3698
3897
  handleDevProxyError,
3898
+ isPidAlive,
3699
3899
  isRouteComponent,
3700
3900
  normalizeBasePath,
3701
3901
  parseAndGenerateNestResourceTemplate,
@@ -3703,6 +3903,8 @@ export {
3703
3903
  parseRoutesFromFile,
3704
3904
  postprocessDrizzleSchema,
3705
3905
  registerMiddlewares,
3906
+ resolveHealth,
3907
+ resolveServerLiveness,
3706
3908
  routeParserLog,
3707
3909
  sendError,
3708
3910
  sendJson,