@faapi/faapi 6.6.0 → 6.8.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.
@@ -249,6 +249,70 @@ interface TaskDriver {
249
249
  retry?(name: string, id: string): Promise<void>;
250
250
  }
251
251
 
252
+ /**
253
+ * 日志子系统类型契约(logger.md)
254
+ *
255
+ * Logger 跨越 handler(ctx.log / 参数 log)、任务(taskCtx.log)与任意业务代码
256
+ * (createLogger)多条路径,类型是各路径形状一致的单一来源;
257
+ * LogEntry 为纯数据(可结构化克隆),是隔离任务日志跨线程回传的契约。
258
+ */
259
+ /** 日志级别(阈值序:debug < info < warn < error) */
260
+ type LogLevel = 'debug' | 'info' | 'warn' | 'error';
261
+ /**
262
+ * 单条日志(sink 的输入,纯数据可结构化克隆)
263
+ *
264
+ * time 为 ISO 字符串(写入时刻生成);scope/fields 可省略。
265
+ */
266
+ interface LogEntry {
267
+ level: LogLevel;
268
+ message: string;
269
+ time: string;
270
+ scope?: string;
271
+ fields?: Record<string, unknown>;
272
+ }
273
+ /**
274
+ * 日志输出目标(默认 console 文本,可整体接管接 pino/winston/文件)
275
+ *
276
+ * sink 内抛错会被吞掉(日志永不影响业务流程)。
277
+ */
278
+ type LogSink = (entry: LogEntry) => void;
279
+ /**
280
+ * 日志器接口
281
+ *
282
+ * 方法签名统一为 `message` 在前、结构化字段可选在后;
283
+ * `child(scope)` 返回新 Logger(scope 以 `:` 合并、fields 浅合并,父子互不影响)。
284
+ */
285
+ interface Logger {
286
+ debug(message: string, fields?: Record<string, unknown>): void;
287
+ info(message: string, fields?: Record<string, unknown>): void;
288
+ warn(message: string, fields?: Record<string, unknown>): void;
289
+ error(message: string, fields?: Record<string, unknown>): void;
290
+ child(scope: string): Logger;
291
+ }
292
+ /**
293
+ * 日志器构造选项(实例级覆盖全局)
294
+ *
295
+ * 命名带 CreateLogger 前缀:公开导出避免与请求日志中间件的 `LoggerOptions`
296
+ * (middleware/logger.ts,先于本模块存在)冲突。
297
+ */
298
+ interface CreateLoggerOptions {
299
+ /** 实例级级别(覆盖全局 level) */
300
+ level?: LogLevel;
301
+ /** 实例级 sink(覆盖全局 sink;显式接管不受全局 `log: false` 影响) */
302
+ sink?: LogSink;
303
+ /** 构造字段(随每条日志携带,调用处 fields 同名键覆盖) */
304
+ fields?: Record<string, unknown>;
305
+ }
306
+ /**
307
+ * `config.log` 的对象形状(config 侧另接受 boolean:false 全静默、true 默认)
308
+ *
309
+ * level 解析顺序:显式值 > `LOG_LEVEL` 环境变量 > `'info'`(非法值启动报错)。
310
+ */
311
+ interface LogConfig {
312
+ level?: LogLevel;
313
+ sink?: LogSink;
314
+ }
315
+
252
316
  /**
253
317
  * 隔离执行器签名(taskQueue 按任务 meta.timeoutMs 调用;测试可注入 spy)
254
318
  */
@@ -268,15 +332,41 @@ interface TaskWorkerOptions {
268
332
  };
269
333
  /** 单次执行超时(毫秒) */
270
334
  timeoutMs: number;
335
+ /**
336
+ * 取消宽限期(毫秒)——两段式取消第一段发出 abort 信号后等待任务自行退出的
337
+ * 最长时间,超时未退出 `terminate()` 硬杀。来自 task meta `graceMs`,
338
+ * 未声明用 `KILL_GRACE_MS`(5s);`0` 表示不留宽限期(判定取消即硬杀)
339
+ */
340
+ graceMs?: number;
271
341
  /**
272
342
  * 注册表快照(纯数据,postMessage 结构化克隆传入,worker 内重建只读视图注入
273
343
  * taskCtx.registries)——语义层从 `TaskRegistriesView` 生成,缺省为空视图
274
344
  */
275
345
  registries?: TaskRegistriesSnapshot;
346
+ /**
347
+ * 任务日志配置(纯数据,postMessage 传入,worker 内联重建日志器注入 taskCtx.log;
348
+ * 缺省时 taskCtx.log 为 undefined):scope/fields 与进程内路径一致(`task:<name>`
349
+ * + jobId/task/attempt),level 为宿主侧生效的全局级别(worker 侧预过滤,
350
+ * 宿主 writeLogEntry 再次过滤)
351
+ */
352
+ log?: {
353
+ level: LogLevel;
354
+ scope?: string;
355
+ fields?: Record<string, unknown>;
356
+ };
276
357
  /** 外部取消信号(驱动停机超时 abort)——abort 同样触发两段式取消 */
277
358
  externalSignal?: AbortSignal;
278
- /** 宽限期覆盖(默认 KILL_GRACE_MS)——测试注入短值用,业务不配置 */
279
- killGraceMs?: number;
359
+ /**
360
+ * 进度回调:worker 内 `taskCtx.progress(value)` 的值经 `{ type: 'progress' }`
361
+ * 消息回传宿主(语义层记入 `TaskJob.progress`);不传则进度消息被忽略
362
+ */
363
+ onProgress?: (value: unknown) => void;
364
+ /**
365
+ * 日志回调:worker 内 taskCtx.log 的条目经 `{ type: 'log' }` 消息回传宿主,
366
+ * 由语义层接 writeLogEntry 走统一管道(自定义 sink 同样覆盖隔离任务);
367
+ * 不传则日志条目被忽略。宽限期(取消判定后)到达的条目不采纳(超时判定即终局)
368
+ */
369
+ onLog?: (entry: LogEntry) => void;
280
370
  }
281
371
  declare function runTaskInWorker(options: TaskWorkerOptions): Promise<unknown>;
282
372
 
@@ -397,11 +487,23 @@ interface FaapiTaskMeta {
397
487
  /** 失败重试次数(默认 0——失败即 failed,不重试) */
398
488
  retries?: number;
399
489
  /**
400
- * 单次执行超时(毫秒)。声明后该任务在独立 worker 线程执行,超时两段式取消
401
- * (先 abort 信号宽限 5s,未退出 terminate 硬杀)——判定超时即执行真正终止。
402
- * 未声明走进程内执行(零开销,但卡住时框架只能不再等待)。
490
+ * 单次执行超时(毫秒,最小 60000 即 1 分钟——低于阈值在扫描期报错)。声明后该
491
+ * 任务在独立 worker 线程执行,超时两段式取消(先 abort 信号宽限,未退出
492
+ * terminate 硬杀)——判定超时即执行真正终止。未声明走进程内执行(零开销)。
493
+ *
494
+ * 最小值的理由:声明 timeoutMs 的语义是"这是需要真取消的长任务",一分钟内能
495
+ * 跑完的任务没必要声明超时(走进程内,需要 deadline 自行用 Promise.race 实现);
496
+ * 且超时从派发起算、包含 worker 冷启动(线程创建 + 模块加载),过小的超时会在
497
+ * 任务做任何事之前就被取消。
403
498
  */
404
499
  timeoutMs?: number;
500
+ /**
501
+ * 取消宽限期(毫秒,仅声明 `timeoutMs` 的隔离任务生效):两段式取消第一段
502
+ * 发出 abort 信号后等待任务自行退出的最长时间,超时未退出 `terminate()` 硬杀。
503
+ * 默认 5000(5s);`0` 表示不留宽限期(判定取消即硬杀)。
504
+ * 未声明 `timeoutMs` 的任务无取消流程,本字段被忽略。
505
+ */
506
+ graceMs?: number;
405
507
  /** cron 表达式(croner 语法,支持秒级)——到点自动入队空 payload */
406
508
  cron?: string;
407
509
  }
@@ -417,6 +519,7 @@ interface TaskManifest {
417
519
  concurrency?: number;
418
520
  retries?: number;
419
521
  timeoutMs?: number;
522
+ graceMs?: number;
420
523
  }
421
524
  /**
422
525
  * 运行时任务元数据(faapi-tasks.js 水合到 TaskRegistry 后的形态,产物路径形式)
@@ -429,6 +532,8 @@ interface TaskMetadata {
429
532
  concurrency?: number;
430
533
  retries?: number;
431
534
  timeoutMs?: number;
535
+ /** 取消宽限期(毫秒,仅隔离任务生效),未声明用框架默认 5s */
536
+ graceMs?: number;
432
537
  }
433
538
  /**
434
539
  * 任务记录状态
@@ -451,6 +556,8 @@ interface TaskJob {
451
556
  result?: unknown;
452
557
  /** 错误消息(failed 时) */
453
558
  error?: string;
559
+ /** 最近一次进度上报值(run 内 `taskCtx.progress(value)` 写入;派发时清空上一轮) */
560
+ progress?: unknown;
454
561
  createdAt: number;
455
562
  /** 计划执行时间戳(重试/延迟任务与 createdAt 不同) */
456
563
  runAt?: number;
@@ -488,6 +595,20 @@ interface TaskContext {
488
595
  * 任务内组装 agent 用 `registries.agent.getAgentEntry(name)`(含 filePath/hasRun)。
489
596
  */
490
597
  registries: TaskRegistriesView;
598
+ /**
599
+ * 进度上报(可选):执行中主动上报进度,记入 `TaskJob.progress`(`list()` 可见)
600
+ *
601
+ * 进程内直写记录;隔离路径经 postMessage 回传宿主(值必须可结构化克隆,
602
+ * 不可克隆按执行错误处理)。仅 running 状态生效,终态后调用被忽略。
603
+ */
604
+ progress?: (value: unknown) => void;
605
+ /**
606
+ * 任务级日志器(可选字段;框架两条执行路径均注入,直接构造 TaskContext 的
607
+ * 测试/自定义执行器可不传)——scope `task:<name>`,字段自动携带
608
+ * jobId/task/attempt,输出走 `config.log` 统一管道(详见 logger/logger.md)。
609
+ * 隔离执行时条目经 postMessage 回传宿主输出,fields 需可结构化克隆。
610
+ */
611
+ log?: Logger;
491
612
  }
492
613
  /**
493
614
  * 任务模块形态(task.ts 编译产物中与执行相关的导出)
@@ -740,6 +861,21 @@ interface FaapiContext {
740
861
  * 无 app 编排(编程式直调 ctx)时为 undefined
741
862
  */
742
863
  tasks?: TaskClient;
864
+ /**
865
+ * 请求 ID:请求头 `x-request-id` 第一段(网关透传场景跨服务串联),无则 crypto.randomUUID() 生成。
866
+ * ctx.log 与请求日志中间件的条目均携带该字段,业务日志与请求日志可经此关联
867
+ */
868
+ requestId: string;
869
+ /**
870
+ * 请求级日志器(scope `http`,自动携带 requestId/method/path 字段,详见 logger/logger.md)
871
+ *
872
+ * ```ts
873
+ * export function GET(ctx) {
874
+ * ctx.log.info('listing users');
875
+ * }
876
+ * ```
877
+ */
878
+ log: Logger;
743
879
  request: Request;
744
880
  params: Record<string, string>;
745
881
  query: URLSearchParams;
@@ -1104,4 +1240,4 @@ interface RouteInfo {
1104
1240
  output: RouteOutputSchema | null;
1105
1241
  }
1106
1242
 
1107
- export { helmet as $, type AppRegistries as A, type TaskDriverJob as B, type CorsOptions as C, type TaskDriverProcess as D, type TaskDriverRecord as E, type FaapiContext as F, type TaskFailedInfo as G, type HelmetOptions as H, type InjectorMap as I, type TaskJob as J, type TaskJobStatus as K, type LoggerOptions as L, type TaskMetadata as M, type TaskModule as N, type TaskRegistriesSnapshot as O, type TaskRegistriesView as P, type ToolCore as Q, type RouteManifest as R, type SkillRegistry as S, type TaskClient as T, type ToolPathMeta as U, type ToolRegistry as V, type WsRouteManifest as W, cors as X, createAppRegistries as Y, createTaskRegistriesView as Z, createTaskRegistry as _, type FaapiMiddleware as a, logger as a0, type TaskFailedHandler as b, type TaskQueueDeps as c, type TaskQueue as d, type TaskDriver as e, type TaskRegistry as f, type TaskManifest as g, type ToolMetadata as h, type AgentCore as i, type AgentMetadata as j, type AgentHandleFactory as k, type AgentHandleStore as l, type AgentPathMeta as m, type AgentRegistry as n, type AgentToolDescriptor as o, type FaapiContextConfig as p, type FaapiTaskMeta as q, type FailOptions as r, type Injector as s, type RouteInfo as t, type RouteInputSchema as u, type RouteOutputSchema as v, type RouteParamSchema as w, type SseEvent as x, type SseWriter as y, type TaskContext as z };
1243
+ export { type ToolPathMeta as $, type AppRegistries as A, type RouteInputSchema as B, type CorsOptions as C, type RouteOutputSchema as D, type RouteParamSchema as E, type FaapiContext as F, type SseEvent as G, type HelmetOptions as H, type InjectorMap as I, type SseWriter as J, type TaskContext as K, type LoggerOptions as L, type TaskDriverJob as M, type TaskDriverProcess as N, type TaskDriverRecord as O, type TaskFailedInfo as P, type TaskJob as Q, type RouteManifest as R, type SkillRegistry as S, type TaskClient as T, type TaskJobStatus as U, type TaskMetadata as V, type WsRouteManifest as W, type TaskModule as X, type TaskRegistriesSnapshot as Y, type TaskRegistriesView as Z, type ToolCore as _, type LogConfig as a, type ToolRegistry as a0, cors as a1, createAppRegistries as a2, createTaskRegistriesView as a3, createTaskRegistry as a4, helmet as a5, logger as a6, type FaapiMiddleware as b, type TaskFailedHandler as c, type TaskQueueDeps as d, type TaskQueue as e, type TaskDriver as f, type TaskRegistry as g, type TaskManifest as h, type ToolMetadata as i, type AgentCore as j, type AgentMetadata as k, type CreateLoggerOptions as l, type Logger as m, type AgentHandleFactory as n, type AgentHandleStore as o, type AgentPathMeta as p, type AgentRegistry as q, type AgentToolDescriptor as r, type FaapiContextConfig as s, type FaapiTaskMeta as t, type FailOptions as u, type Injector as v, type LogEntry as w, type LogLevel as x, type LogSink as y, type RouteInfo as z };
package/dist/testing.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { F as FaapiContext, a as FaapiMiddleware, I as InjectorMap, R as RouteManifest, W as WsRouteManifest, C as CorsOptions, H as HelmetOptions, L as LoggerOptions } from './routeTypes-q9OXB--o.js';
1
+ import { F as FaapiContext, b as FaapiMiddleware, I as InjectorMap, R as RouteManifest, W as WsRouteManifest, C as CorsOptions, H as HelmetOptions, L as LoggerOptions } from './routeTypes-J5pPbjwR.js';
2
2
  import { Server } from 'node:http';
3
3
  import { WebSocket } from 'ws';
4
4
 
package/dist/testing.js CHANGED
@@ -245,7 +245,90 @@ function formatErrorResponse(error, config) {
245
245
  return jsonOk(body, 500);
246
246
  }
247
247
 
248
+ // src/logger/logger.ts
249
+ var LEVEL_RANK = { debug: 0, info: 1, warn: 2, error: 3 };
250
+ var globalState = {
251
+ level: null,
252
+ sink: null,
253
+ disabled: false
254
+ };
255
+ function globalThreshold() {
256
+ return globalState.level ?? "info";
257
+ }
258
+ function serializeFieldValue(value) {
259
+ if (value instanceof Error) {
260
+ return { name: value.name, message: value.message, stack: value.stack };
261
+ }
262
+ return value;
263
+ }
264
+ function formatEntry(entry) {
265
+ let line = `[${entry.time}] ${entry.level.toUpperCase()} `;
266
+ if (entry.scope) line += `[${entry.scope}] `;
267
+ line += entry.message;
268
+ if (entry.fields !== void 0) {
269
+ const plain = Object.fromEntries(
270
+ Object.entries(entry.fields).map(([k, v]) => [k, serializeFieldValue(v)])
271
+ );
272
+ try {
273
+ line += ` ${JSON.stringify(plain)}`;
274
+ } catch (err) {
275
+ line += ` [unserializable fields: ${err instanceof Error ? err.message : String(err)}]`;
276
+ }
277
+ }
278
+ return line;
279
+ }
280
+ var CONSOLE_METHOD = {
281
+ debug: "debug",
282
+ info: "info",
283
+ warn: "warn",
284
+ error: "error"
285
+ };
286
+ var consoleSink = (entry) => {
287
+ console[CONSOLE_METHOD[entry.level]](formatEntry(entry));
288
+ };
289
+ function globalDispatch(entry) {
290
+ if (globalState.disabled) return;
291
+ (globalState.sink ?? consoleSink)(entry);
292
+ }
293
+ function createLogger(scope, options) {
294
+ const baseScope = scope;
295
+ const baseFields = options?.fields;
296
+ const write = (level, message, callFields, scopeSuffix) => {
297
+ const threshold = options?.level ?? globalThreshold();
298
+ if (LEVEL_RANK[level] < LEVEL_RANK[threshold]) return;
299
+ const entry = {
300
+ level,
301
+ message,
302
+ time: (/* @__PURE__ */ new Date()).toISOString()
303
+ };
304
+ const fullScope = scopeSuffix === void 0 ? baseScope : baseScope === void 0 ? scopeSuffix : `${baseScope}:${scopeSuffix}`;
305
+ if (fullScope !== void 0) entry.scope = fullScope;
306
+ if (baseFields !== void 0 || callFields !== void 0) {
307
+ entry.fields = { ...baseFields, ...callFields };
308
+ }
309
+ if (options?.sink) {
310
+ options.sink(entry);
311
+ } else {
312
+ globalDispatch(entry);
313
+ }
314
+ };
315
+ const makeLogger = (scopeSuffix) => ({
316
+ debug: (message, fields) => write("debug", message, fields, scopeSuffix),
317
+ info: (message, fields) => write("info", message, fields, scopeSuffix),
318
+ warn: (message, fields) => write("warn", message, fields, scopeSuffix),
319
+ error: (message, fields) => write("error", message, fields, scopeSuffix),
320
+ child: (childScope) => makeLogger(scopeSuffix === void 0 ? childScope : `${scopeSuffix}:${childScope}`)
321
+ });
322
+ return makeLogger(void 0);
323
+ }
324
+
248
325
  // src/runtime/createContext.ts
326
+ function resolveRequestId(request) {
327
+ const header = request.headers.get("x-request-id");
328
+ const first = header?.split(",")[0]?.trim();
329
+ if (first) return first;
330
+ return crypto.randomUUID();
331
+ }
249
332
  function parseCookies(cookieHeader) {
250
333
  const cookies = /* @__PURE__ */ new Map();
251
334
  if (!cookieHeader) return cookies;
@@ -279,7 +362,13 @@ function createContextFromUrl(request, url, params, config = {}, ip = "", regist
279
362
  for (const [key, val] of parsedCookies) {
280
363
  cookiesObj[key] = val;
281
364
  }
365
+ const requestId = resolveRequestId(request);
366
+ const log = createLogger("http", {
367
+ fields: { requestId, method: request.method, path: url.pathname }
368
+ });
282
369
  const ctx = {
370
+ requestId,
371
+ log,
283
372
  request,
284
373
  params,
285
374
  query: url.searchParams,
@@ -553,8 +642,10 @@ var PARAM_TYPE_MAP = {
553
642
  // Phase 2.3
554
643
  agents: "agents",
555
644
  // Phase 2.3
556
- tasks: "tasks"
645
+ tasks: "tasks",
557
646
  // 任务子系统:TaskClient(入队/查询)
647
+ log: "log"
648
+ // 请求级日志器(ctx.log 同一实例:scope http,自动带 requestId 等字段)
558
649
  };
559
650
  var injectionCache = /* @__PURE__ */ new WeakMap();
560
651
  function resolveInjection(fn) {
@@ -872,6 +963,9 @@ function getBuiltinInjectionValue(type, ctx, body) {
872
963
  // 任务子系统:注入 TaskClient(入队/查询);未注册工厂(无 app 编排)时 undefined
873
964
  case "tasks":
874
965
  return ctx.registries ? ctx.registries.taskHandle.get(ctx) : void 0;
966
+ // 请求级日志器:与 ctx.log 同一实例(scope http,自动带 requestId/method/path 字段)
967
+ case "log":
968
+ return ctx.log;
875
969
  default:
876
970
  return void 0;
877
971
  }
@@ -4101,6 +4195,7 @@ function logger(options = {}) {
4101
4195
  const response = await next();
4102
4196
  const duration = Date.now() - start;
4103
4197
  const entry = {
4198
+ requestId: ctx.requestId,
4104
4199
  method: ctx.method,
4105
4200
  path: ctx.path,
4106
4201
  status: response.status,
@@ -4113,6 +4208,7 @@ function logger(options = {}) {
4113
4208
  const message = err instanceof Error ? err.message : String(err);
4114
4209
  const status = err?.statusCode ?? 500;
4115
4210
  const entry = {
4211
+ requestId: ctx.requestId,
4116
4212
  method: ctx.method,
4117
4213
  path: ctx.path,
4118
4214
  status,