@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.
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
- import { A as AppRegistries, R as RouteManifest, F as FaapiContext, C as CorsOptions, T as TaskClient, H as HelmetOptions, L as LoggerOptions, a as FaapiMiddleware, I as InjectorMap, b as TaskFailedHandler, c as TaskQueueDeps, d as TaskQueue, e as TaskDriver, f as TaskRegistry, g as TaskManifest, h as ToolMetadata, i as AgentCore, j as AgentMetadata, W as WsRouteManifest } from './routeTypes-q9OXB--o.js';
2
- export { k as AgentHandleFactory, l as AgentHandleStore, m as AgentPathMeta, n as AgentRegistry, o as AgentToolDescriptor, p as FaapiContextConfig, q as FaapiTaskMeta, r as FailOptions, s as Injector, t as RouteInfo, u as RouteInputSchema, v as RouteOutputSchema, w as RouteParamSchema, S as SkillRegistry, x as SseEvent, y as SseWriter, z as TaskContext, B as TaskDriverJob, D as TaskDriverProcess, E as TaskDriverRecord, G as TaskFailedInfo, J as TaskJob, K as TaskJobStatus, M as TaskMetadata, N as TaskModule, O as TaskRegistriesSnapshot, P as TaskRegistriesView, Q as ToolCore, U as ToolPathMeta, V as ToolRegistry, X as cors, Y as createAppRegistries, Z as createTaskRegistriesView, _ as createTaskRegistry, $ as helmet, a0 as logger } from './routeTypes-q9OXB--o.js';
1
+ import { A as AppRegistries, R as RouteManifest, F as FaapiContext, C as CorsOptions, T as TaskClient, H as HelmetOptions, L as LoggerOptions, a as LogConfig, b as FaapiMiddleware, I as InjectorMap, c as TaskFailedHandler, d as TaskQueueDeps, e as TaskQueue, f as TaskDriver, g as TaskRegistry, h as TaskManifest, i as ToolMetadata, j as AgentCore, k as AgentMetadata, l as CreateLoggerOptions, m as Logger, W as WsRouteManifest } from './routeTypes-J5pPbjwR.js';
2
+ export { n as AgentHandleFactory, o as AgentHandleStore, p as AgentPathMeta, q as AgentRegistry, r as AgentToolDescriptor, s as FaapiContextConfig, t as FaapiTaskMeta, u as FailOptions, v as Injector, w as LogEntry, x as LogLevel, y as LogSink, z as RouteInfo, B as RouteInputSchema, D as RouteOutputSchema, E as RouteParamSchema, S as SkillRegistry, G as SseEvent, J as SseWriter, K as TaskContext, M as TaskDriverJob, N as TaskDriverProcess, O as TaskDriverRecord, P as TaskFailedInfo, Q as TaskJob, U as TaskJobStatus, V as TaskMetadata, X as TaskModule, Y as TaskRegistriesSnapshot, Z as TaskRegistriesView, _ as ToolCore, $ as ToolPathMeta, a0 as ToolRegistry, a1 as cors, a2 as createAppRegistries, a3 as createTaskRegistriesView, a4 as createTaskRegistry, a5 as helmet, a6 as logger } from './routeTypes-J5pPbjwR.js';
3
3
  import * as node_http from 'node:http';
4
4
  import { Server, IncomingMessage, ServerResponse } from 'node:http';
5
5
  import { Socket } from 'node:net';
@@ -552,6 +552,26 @@ interface FaapiConfig {
552
552
  bodyLimit?: number;
553
553
  /** 日志中间件配置 */
554
554
  logger?: LoggerOptions | boolean;
555
+ /**
556
+ * 业务日志器全局配置(与 `logger` 请求日志中间件是两条独立管道,详见 logger/logger.md)
557
+ *
558
+ * - `false`:完全静默(含 error,测试降噪)
559
+ * - `true` / 缺省:默认级别(显式 level > LOG_LEVEL env > 'info')+ console 文本输出
560
+ * - `LogConfig`:精细配置 level 与 sink(sink 可整体接管接 pino/winston/文件)
561
+ *
562
+ * 生效范围:`ctx.log` / 参数注入 `log` / `createLogger` / `taskCtx.log`。
563
+ * 日志全局配置是进程级资源,多 app 同进程时后启动覆盖先启动。
564
+ *
565
+ * ```ts
566
+ * export default {
567
+ * log: {
568
+ * level: 'debug',
569
+ * sink: (entry) => pinoLogger[entry.level]({ scope: entry.scope, ...entry.fields }, entry.message),
570
+ * },
571
+ * } satisfies FaapiConfig;
572
+ * ```
573
+ */
574
+ log?: LogConfig | boolean;
555
575
  /** HTTP/2 配置,false 禁用(默认 http/1.1) */
556
576
  http2?: Http2Options | boolean;
557
577
  /**
@@ -1466,6 +1486,26 @@ declare function loadConfig(rootDir: string, dist: string): Promise<Partial<Faap
1466
1486
  */
1467
1487
  declare function loadEnv(rootDir: string): void;
1468
1488
 
1489
+ /**
1490
+ * 设置全局日志配置(进程级,`createAppBase` 启动时以 `config.log` 调用)
1491
+ *
1492
+ * - `false`:完全静默(含 error,测试降噪场景)
1493
+ * - `true` / `{}`:默认级别(显式 level > LOG_LEVEL env > 'info')+ 默认 console sink
1494
+ * - `{ level, sink }`:精细配置;非法 level 抛错(启动期 fail fast)
1495
+ * - `undefined`:重置为默认(编程式多 app 切换全局配置用)
1496
+ *
1497
+ * LOG_LEVEL env 仅在本函数读取——app 启动路径必经此处(env 非法在启动期报错);
1498
+ * 纯编程式不调本函数时 env 不生效(默认 info)。
1499
+ */
1500
+ declare function configureLogging(config?: LogConfig | boolean | undefined): void;
1501
+ /**
1502
+ * 创建日志器
1503
+ *
1504
+ * @param scope 分类标签(如 'db'、'http'),默认无分类
1505
+ * @param options 实例级覆盖(level/sink/构造 fields)
1506
+ */
1507
+ declare function createLogger(scope?: string, options?: CreateLoggerOptions): Logger;
1508
+
1469
1509
  declare const VALIDATION_ERROR = "VALIDATION_ERROR";
1470
1510
  declare const ROUTE_NOT_FOUND = "ROUTE_NOT_FOUND";
1471
1511
  declare const METHOD_NOT_ALLOWED = "METHOD_NOT_ALLOWED";
@@ -1666,4 +1706,4 @@ type ProdApp = AppBase;
1666
1706
  */
1667
1707
  declare function createProdApp(options?: CreateAppOptions): Promise<ProdApp>;
1668
1708
 
1669
- export { type AgentConfig, AgentCore, AgentMetadata, type AgentModule, type ProdApp as App, AppRegistries, CorsOptions, type CreateAppOptions, type CronScheduler, type DevApp, type FaapiConfig, FaapiContext, FaapiError, FaapiMiddleware, type FaapiPlugin, type HandlerTypeInfo, HelmetOptions, type InjectOptions, type InjectResponse, InjectorMap, InternalError, type LifecycleContext, type LifecycleHooks, type LlmConfig, type LlmModelConfig, LoggerOptions, MethodNotAllowedError, ModuleLoadError, type PluginContext, type PluginDeclaration, type ProdApp, type PropertyType, type RequestHandler, type ResponseConfig, RouteManifest, RouteNotFoundError, type RouteSchemaSource, type RuntimeType, SchemaExtractionError, TASK_PATTERNS, type TaskBullMqOptions, TaskClient, type TaskConfig, TaskDriver, TaskFailedHandler, TaskManifest, type TaskPgBossOptions, TaskQueue, TaskRegistry, ToolMetadata, type ToolModule, type ToolSchemaModule, type TypeConstraint, type UpgradeHandler, ValidationError, type ValidationErrorCode, type ValidationIssue, type WsContext, type WsEventHandlers, type WsHandler, type WsSocket, clearAgentHandleFactory, collectRouteSchemaSources, createProdApp as createApp, createCronScheduler, createDevApp, createProdApp, createProgram, createPrograms, createTaskQueue, extractTypeInfo, getAgent, getAgentEntry, getApp, getInputTypeForMethod, getSkill, getTool, getToolSchemaPath, hydrateSkillRegistry, invalidateProgramCache, listSkills, loadAgentModule, loadConfig, loadEnv, loadTaskDriver, loadToolModule, loadToolSchema, registerAgentHandleFactory, removeSkill, resolveAgentTools, resolveSubAgents, resolveTypeNode, scanTasks, upsertSkill };
1709
+ export { type AgentConfig, AgentCore, AgentMetadata, type AgentModule, type ProdApp as App, AppRegistries, CorsOptions, type CreateAppOptions, CreateLoggerOptions, type CronScheduler, type DevApp, type FaapiConfig, FaapiContext, FaapiError, FaapiMiddleware, type FaapiPlugin, type HandlerTypeInfo, HelmetOptions, type InjectOptions, type InjectResponse, InjectorMap, InternalError, type LifecycleContext, type LifecycleHooks, type LlmConfig, type LlmModelConfig, LogConfig, Logger, LoggerOptions, MethodNotAllowedError, ModuleLoadError, type PluginContext, type PluginDeclaration, type ProdApp, type PropertyType, type RequestHandler, type ResponseConfig, RouteManifest, RouteNotFoundError, type RouteSchemaSource, type RuntimeType, SchemaExtractionError, TASK_PATTERNS, type TaskBullMqOptions, TaskClient, type TaskConfig, TaskDriver, TaskFailedHandler, TaskManifest, type TaskPgBossOptions, TaskQueue, TaskRegistry, ToolMetadata, type ToolModule, type ToolSchemaModule, type TypeConstraint, type UpgradeHandler, ValidationError, type ValidationErrorCode, type ValidationIssue, type WsContext, type WsEventHandlers, type WsHandler, type WsSocket, clearAgentHandleFactory, collectRouteSchemaSources, configureLogging, createProdApp as createApp, createCronScheduler, createDevApp, createLogger, createProdApp, createProgram, createPrograms, createTaskQueue, extractTypeInfo, getAgent, getAgentEntry, getApp, getInputTypeForMethod, getSkill, getTool, getToolSchemaPath, hydrateSkillRegistry, invalidateProgramCache, listSkills, loadAgentModule, loadConfig, loadEnv, loadTaskDriver, loadToolModule, loadToolSchema, registerAgentHandleFactory, removeSkill, resolveAgentTools, resolveSubAgents, resolveTypeNode, scanTasks, upsertSkill };
package/dist/index.js CHANGED
@@ -1122,8 +1122,10 @@ var init_resolveInjection = __esm({
1122
1122
  // Phase 2.3
1123
1123
  agents: "agents",
1124
1124
  // Phase 2.3
1125
- tasks: "tasks"
1125
+ tasks: "tasks",
1126
1126
  // 任务子系统:TaskClient(入队/查询)
1127
+ log: "log"
1128
+ // 请求级日志器(ctx.log 同一实例:scope http,自动带 requestId 等字段)
1127
1129
  };
1128
1130
  injectionCache = /* @__PURE__ */ new WeakMap();
1129
1131
  }
@@ -2053,6 +2055,49 @@ function buildRegistriesView(snapshot) {
2053
2055
  };
2054
2056
  }
2055
2057
  `;
2058
+ var SERIALIZE_ERROR_SOURCE = `
2059
+ function serializeError(err) {
2060
+ if (err instanceof Error) {
2061
+ var props = {};
2062
+ var keys = Object.keys(err);
2063
+ for (var i = 0; i < keys.length; i++) {
2064
+ props[keys[i]] = err[keys[i]];
2065
+ }
2066
+ return { name: err.name, message: err.message, stack: err.stack, props: props };
2067
+ }
2068
+ return { name: 'Error', message: String(err), stack: undefined, props: {} };
2069
+ }
2070
+ `;
2071
+ var BUILD_LOGGER_SOURCE = `
2072
+ var LOG_RANK = { debug: 0, info: 1, warn: 2, error: 3 };
2073
+ function createTaskLogger(level, scope, fields) {
2074
+ function make(suffix) {
2075
+ function write(lvl, message, callFields) {
2076
+ if (LOG_RANK[lvl] < LOG_RANK[level]) return;
2077
+ var entry = { level: lvl, message: message, time: new Date().toISOString() };
2078
+ var fullScope = suffix === undefined ? scope : scope === undefined ? suffix : scope + ':' + suffix;
2079
+ if (fullScope !== undefined) entry.scope = fullScope;
2080
+ if (fields !== undefined || callFields !== undefined) {
2081
+ entry.fields = Object.assign({}, fields, callFields);
2082
+ }
2083
+ try {
2084
+ parentPort.postMessage({ type: 'log', entry: entry });
2085
+ } catch (_) {
2086
+ entry.fields = { warning: 'log fields not cloneable across worker boundary, dropped' };
2087
+ parentPort.postMessage({ type: 'log', entry: entry });
2088
+ }
2089
+ }
2090
+ return {
2091
+ debug: function (m, f) { write('debug', m, f); },
2092
+ info: function (m, f) { write('info', m, f); },
2093
+ warn: function (m, f) { write('warn', m, f); },
2094
+ error: function (m, f) { write('error', m, f); },
2095
+ child: function (cs) { return make(suffix === undefined ? cs : suffix + ':' + cs); },
2096
+ };
2097
+ }
2098
+ return make(undefined);
2099
+ }
2100
+ `;
2056
2101
  function buildWrapperSource(moduleUrl) {
2057
2102
  return `
2058
2103
  import { parentPort } from 'node:worker_threads';
@@ -2060,9 +2105,16 @@ import * as mod from '${moduleUrl}';
2060
2105
 
2061
2106
  ${BUILD_VIEW_SOURCE}
2062
2107
 
2108
+ ${SERIALIZE_ERROR_SOURCE}
2109
+
2110
+ ${BUILD_LOGGER_SOURCE}
2111
+
2063
2112
  const run = mod.run;
2064
2113
  if (typeof run !== 'function') {
2065
- parentPort.postMessage({ type: 'error', message: 'Task module has no run export' });
2114
+ parentPort.postMessage({
2115
+ type: 'error',
2116
+ error: { name: 'Error', message: 'Task module has no run export', stack: undefined, props: {} },
2117
+ });
2066
2118
  } else {
2067
2119
  let controller = null;
2068
2120
  parentPort.on('message', async (msg) => {
@@ -2075,21 +2127,45 @@ if (typeof run !== 'function') {
2075
2127
  const { payload, taskCtx } = msg;
2076
2128
  try {
2077
2129
  const registries = buildRegistriesView(msg.registries);
2078
- const result = await run(payload, { ...taskCtx, signal: controller.signal, registries });
2130
+ const progress = (value) => parentPort.postMessage({ type: 'progress', value });
2131
+ const taskLog = msg.log ? createTaskLogger(msg.log.level, msg.log.scope, msg.log.fields) : undefined;
2132
+ const result = await run(payload, { ...taskCtx, signal: controller.signal, registries, progress, log: taskLog });
2079
2133
  parentPort.postMessage({ type: 'done', result });
2080
2134
  } catch (err) {
2081
- parentPort.postMessage({
2082
- type: 'error',
2083
- message: err instanceof Error ? err.message : String(err),
2084
- });
2135
+ const serialized = serializeError(err);
2136
+ try {
2137
+ parentPort.postMessage({ type: 'error', error: serialized });
2138
+ } catch (_) {
2139
+ // props \u542B\u4E0D\u53EF\u514B\u9686\u503C\uFF08\u51FD\u6570\u7B49\uFF09\u65F6\u4E22\u5F03 props\uFF0C\u4FDD\u5E95 name/message/stack
2140
+ parentPort.postMessage({
2141
+ type: 'error',
2142
+ error: { name: serialized.name, message: serialized.message, stack: serialized.stack, props: {} },
2143
+ });
2144
+ }
2085
2145
  }
2086
2146
  });
2087
2147
  }
2088
2148
  `;
2089
2149
  }
2150
+ function reviveError(payload) {
2151
+ const err = new Error(payload?.message ?? "task worker error");
2152
+ if (payload?.name !== void 0) err.name = payload.name;
2153
+ if (payload?.stack !== void 0) err.stack = payload.stack;
2154
+ if (payload?.props) {
2155
+ for (const [key, value] of Object.entries(payload.props)) {
2156
+ err[key] = value;
2157
+ }
2158
+ }
2159
+ return err;
2160
+ }
2090
2161
  async function runTaskInWorker(options) {
2091
2162
  const { taskModulePath, payload, taskCtx, timeoutMs, externalSignal } = options;
2092
- const killGraceMs = options.killGraceMs ?? KILL_GRACE_MS;
2163
+ const killGraceMs = options.graceMs ?? KILL_GRACE_MS;
2164
+ if (externalSignal?.aborted) {
2165
+ throw new TaskCancelledError(
2166
+ `Task "${taskCtx.job.name}" cancelled before start (driver stop signal already aborted)`
2167
+ );
2168
+ }
2093
2169
  const moduleUrl = pathToFileURL(taskModulePath).href;
2094
2170
  const wrapperUrl = new URL(
2095
2171
  `data:text/javascript,${encodeURIComponent(buildWrapperSource(moduleUrl))}`
@@ -2137,27 +2213,39 @@ async function runTaskInWorker(options) {
2137
2213
  () => startCancel(`timed out after ${timeoutMs}ms`),
2138
2214
  timeoutMs
2139
2215
  );
2140
- worker.on("message", (msg) => {
2141
- if (phase === "grace") {
2142
- if (msg?.type === "done") {
2143
- finish(
2144
- () => reject(
2145
- new TaskCancelledError(
2146
- `Task "${taskCtx.job.name}" ${cancelReason} (task completed after the timeout)`
2216
+ worker.on(
2217
+ "message",
2218
+ (msg) => {
2219
+ if (phase === "grace") {
2220
+ if (msg?.type === "done") {
2221
+ finish(
2222
+ () => reject(
2223
+ new TaskCancelledError(
2224
+ `Task "${taskCtx.job.name}" ${cancelReason} (task completed after the timeout)`
2225
+ )
2147
2226
  )
2148
- )
2149
- );
2227
+ );
2228
+ } else if (msg?.type === "error") {
2229
+ finish(() => reject(new TaskCancelledError(msg.error?.message ?? cancelReason)));
2230
+ }
2231
+ return;
2232
+ }
2233
+ if (phase !== "running") return;
2234
+ if (msg?.type === "done") {
2235
+ finish(() => resolve(msg.result));
2150
2236
  } else if (msg?.type === "error") {
2151
- finish(() => reject(new TaskCancelledError(msg.message ?? cancelReason)));
2237
+ finish(() => reject(reviveError(msg.error)));
2238
+ } else if (msg?.type === "progress") {
2239
+ options.onProgress?.(msg.value);
2240
+ } else if (msg?.type === "log") {
2241
+ options.onLog?.(msg.entry);
2152
2242
  }
2153
- return;
2154
- }
2155
- if (phase !== "running") return;
2156
- if (msg?.type === "done") {
2157
- finish(() => resolve(msg.result));
2158
- } else if (msg?.type === "error") {
2159
- finish(() => reject(new Error(msg.message ?? "task worker error")));
2160
2243
  }
2244
+ );
2245
+ worker.on("messageerror", () => {
2246
+ finish(
2247
+ () => reject(new Error(`Task "${taskCtx.job.name}" worker message could not be deserialized`))
2248
+ );
2161
2249
  });
2162
2250
  worker.on("error", (err) => {
2163
2251
  finish(() => reject(err));
@@ -2174,7 +2262,8 @@ async function runTaskInWorker(options) {
2174
2262
  type: "run",
2175
2263
  payload,
2176
2264
  taskCtx: { config: safeConfig(taskCtx.config), job: taskCtx.job },
2177
- registries: options.registries
2265
+ registries: options.registries,
2266
+ log: options.log
2178
2267
  });
2179
2268
  } catch (err) {
2180
2269
  finish(
@@ -2186,7 +2275,121 @@ async function runTaskInWorker(options) {
2186
2275
  });
2187
2276
  }
2188
2277
 
2278
+ // src/logger/logger.ts
2279
+ var LEVEL_RANK = { debug: 0, info: 1, warn: 2, error: 3 };
2280
+ var VALID_LEVELS = ["debug", "info", "warn", "error"];
2281
+ function assertLogLevel(value, source) {
2282
+ if (typeof value !== "string" || !VALID_LEVELS.includes(value)) {
2283
+ throw new Error(
2284
+ `[faapi] Invalid log level from ${source}: ${String(value)}. Expected one of: ${VALID_LEVELS.join(", ")}.`
2285
+ );
2286
+ }
2287
+ }
2288
+ var globalState = {
2289
+ level: null,
2290
+ sink: null,
2291
+ disabled: false
2292
+ };
2293
+ function configureLogging(config) {
2294
+ if (config === void 0) {
2295
+ globalState.level = null;
2296
+ globalState.sink = null;
2297
+ globalState.disabled = false;
2298
+ return;
2299
+ }
2300
+ const objectConfig = typeof config === "object" ? config : {};
2301
+ globalState.disabled = config === false;
2302
+ globalState.sink = objectConfig.sink ?? null;
2303
+ if (objectConfig.level !== void 0) {
2304
+ assertLogLevel(objectConfig.level, "config.log.level");
2305
+ globalState.level = objectConfig.level;
2306
+ } else if (process.env.LOG_LEVEL !== void 0) {
2307
+ assertLogLevel(process.env.LOG_LEVEL, "LOG_LEVEL env");
2308
+ globalState.level = process.env.LOG_LEVEL;
2309
+ } else {
2310
+ globalState.level = null;
2311
+ }
2312
+ }
2313
+ function globalThreshold() {
2314
+ return globalState.level ?? "info";
2315
+ }
2316
+ function getEffectiveLogLevel() {
2317
+ return globalThreshold();
2318
+ }
2319
+ function serializeFieldValue(value) {
2320
+ if (value instanceof Error) {
2321
+ return { name: value.name, message: value.message, stack: value.stack };
2322
+ }
2323
+ return value;
2324
+ }
2325
+ function formatEntry(entry) {
2326
+ let line = `[${entry.time}] ${entry.level.toUpperCase()} `;
2327
+ if (entry.scope) line += `[${entry.scope}] `;
2328
+ line += entry.message;
2329
+ if (entry.fields !== void 0) {
2330
+ const plain = Object.fromEntries(
2331
+ Object.entries(entry.fields).map(([k, v]) => [k, serializeFieldValue(v)])
2332
+ );
2333
+ try {
2334
+ line += ` ${JSON.stringify(plain)}`;
2335
+ } catch (err) {
2336
+ line += ` [unserializable fields: ${err instanceof Error ? err.message : String(err)}]`;
2337
+ }
2338
+ }
2339
+ return line;
2340
+ }
2341
+ var CONSOLE_METHOD = {
2342
+ debug: "debug",
2343
+ info: "info",
2344
+ warn: "warn",
2345
+ error: "error"
2346
+ };
2347
+ var consoleSink = (entry) => {
2348
+ console[CONSOLE_METHOD[entry.level]](formatEntry(entry));
2349
+ };
2350
+ function globalDispatch(entry) {
2351
+ if (globalState.disabled) return;
2352
+ (globalState.sink ?? consoleSink)(entry);
2353
+ }
2354
+ function writeLogEntry(entry) {
2355
+ if (LEVEL_RANK[entry.level] < LEVEL_RANK[globalThreshold()]) return;
2356
+ globalDispatch(entry);
2357
+ }
2358
+ function createLogger(scope, options) {
2359
+ const baseScope = scope;
2360
+ const baseFields = options?.fields;
2361
+ const write = (level, message, callFields, scopeSuffix) => {
2362
+ const threshold = options?.level ?? globalThreshold();
2363
+ if (LEVEL_RANK[level] < LEVEL_RANK[threshold]) return;
2364
+ const entry = {
2365
+ level,
2366
+ message,
2367
+ time: (/* @__PURE__ */ new Date()).toISOString()
2368
+ };
2369
+ const fullScope = scopeSuffix === void 0 ? baseScope : baseScope === void 0 ? scopeSuffix : `${baseScope}:${scopeSuffix}`;
2370
+ if (fullScope !== void 0) entry.scope = fullScope;
2371
+ if (baseFields !== void 0 || callFields !== void 0) {
2372
+ entry.fields = { ...baseFields, ...callFields };
2373
+ }
2374
+ if (options?.sink) {
2375
+ options.sink(entry);
2376
+ } else {
2377
+ globalDispatch(entry);
2378
+ }
2379
+ };
2380
+ const makeLogger = (scopeSuffix) => ({
2381
+ debug: (message, fields) => write("debug", message, fields, scopeSuffix),
2382
+ info: (message, fields) => write("info", message, fields, scopeSuffix),
2383
+ warn: (message, fields) => write("warn", message, fields, scopeSuffix),
2384
+ error: (message, fields) => write("error", message, fields, scopeSuffix),
2385
+ child: (childScope) => makeLogger(scopeSuffix === void 0 ? childScope : `${scopeSuffix}:${childScope}`)
2386
+ });
2387
+ return makeLogger(void 0);
2388
+ }
2389
+
2189
2390
  // src/task/taskQueue.ts
2391
+ var TERMINAL_STATUSES = /* @__PURE__ */ new Set(["done", "failed", "cancelled"]);
2392
+ var MAX_FINISHED_RECORDS = 1e3;
2190
2393
  function createTaskQueue(deps) {
2191
2394
  const { registry, rootDir, driver } = deps;
2192
2395
  const registriesView = deps.registries ?? createEmptyTaskRegistriesView();
@@ -2195,6 +2398,25 @@ function createTaskQueue(deps) {
2195
2398
  const schemaCache = /* @__PURE__ */ new Map();
2196
2399
  let started = false;
2197
2400
  let stopped = false;
2401
+ let finishedCount = 0;
2402
+ function transitionStatus(record, status) {
2403
+ const wasTerminal = TERMINAL_STATUSES.has(record.status);
2404
+ const nowTerminal = TERMINAL_STATUSES.has(status);
2405
+ if (!wasTerminal && nowTerminal) finishedCount++;
2406
+ else if (wasTerminal && !nowTerminal) finishedCount--;
2407
+ record.status = status;
2408
+ }
2409
+ function evictFinishedRecords() {
2410
+ let toEvict = finishedCount - MAX_FINISHED_RECORDS;
2411
+ for (const [id, job] of records) {
2412
+ if (toEvict <= 0) break;
2413
+ if (TERMINAL_STATUSES.has(job.status)) {
2414
+ records.delete(id);
2415
+ finishedCount--;
2416
+ toEvict--;
2417
+ }
2418
+ }
2419
+ }
2198
2420
  const loadTaskModule = deps.loadTaskModule ?? (async (filePath) => await import(filePath));
2199
2421
  const loadPayloadSchema = deps.loadPayloadSchema ?? (async (filePath) => {
2200
2422
  const zodPath = path3.join(path3.dirname(filePath), "zod.js");
@@ -2253,7 +2475,8 @@ function createTaskQueue(deps) {
2253
2475
  createdAt: Date.now()
2254
2476
  };
2255
2477
  record.attempts = job.attempt;
2256
- record.status = "running";
2478
+ transitionStatus(record, "running");
2479
+ delete record.progress;
2257
2480
  record.error = void 0;
2258
2481
  records.set(job.id, record);
2259
2482
  try {
@@ -2267,8 +2490,20 @@ function createTaskQueue(deps) {
2267
2490
  job: { id: job.id, name: job.name, attempt: job.attempt }
2268
2491
  },
2269
2492
  registries: snapshotRegistries(),
2493
+ // 日志配置随 workerData 下发(纯数据),worker 内联重建 taskCtx.log,
2494
+ // 条目回传宿主走统一管道(scope/fields 与进程内路径一致)
2495
+ log: {
2496
+ level: getEffectiveLogLevel(),
2497
+ scope: `task:${job.name}`,
2498
+ fields: { jobId: job.id, task: job.name, attempt: job.attempt }
2499
+ },
2270
2500
  timeoutMs: meta.timeoutMs,
2271
- externalSignal: job.signal
2501
+ graceMs: meta.graceMs,
2502
+ externalSignal: job.signal,
2503
+ onProgress: (value) => {
2504
+ if (record.status === "running") record.progress = value;
2505
+ },
2506
+ onLog: writeLogEntry
2272
2507
  });
2273
2508
  } else {
2274
2509
  let mod = moduleCache2.get(job.name);
@@ -2283,17 +2518,25 @@ function createTaskQueue(deps) {
2283
2518
  signal: job.signal,
2284
2519
  config: deps.config,
2285
2520
  job: { id: job.id, name: job.name, attempt: job.attempt },
2286
- registries: registriesView
2521
+ registries: registriesView,
2522
+ log: createLogger(`task:${job.name}`, {
2523
+ fields: { jobId: job.id, task: job.name, attempt: job.attempt }
2524
+ }),
2525
+ progress: (value) => {
2526
+ if (record.status === "running") record.progress = value;
2527
+ }
2287
2528
  };
2288
2529
  result = await mod.run(job.payload, taskCtx);
2289
2530
  }
2290
- record.status = "done";
2531
+ transitionStatus(record, "done");
2291
2532
  record.result = result;
2533
+ evictFinishedRecords();
2292
2534
  return result;
2293
2535
  } catch (err) {
2294
2536
  const cancelled = err instanceof TaskCancelledError || job.signal.aborted;
2295
- record.status = cancelled ? "cancelled" : "failed";
2537
+ transitionStatus(record, cancelled ? "cancelled" : "failed");
2296
2538
  record.error = err instanceof Error ? err.message : String(err);
2539
+ evictFinishedRecords();
2297
2540
  if (deps.onFailed) {
2298
2541
  void Promise.resolve().then(
2299
2542
  () => deps.onFailed({
@@ -2388,7 +2631,8 @@ function createTaskQueue(deps) {
2388
2631
  }
2389
2632
  await driver.cancel(name, id);
2390
2633
  const record = records.get(id);
2391
- if (record) record.status = "cancelled";
2634
+ if (record) transitionStatus(record, "cancelled");
2635
+ evictFinishedRecords();
2392
2636
  },
2393
2637
  async retry(name, id) {
2394
2638
  if (!driver.retry) {
@@ -2398,7 +2642,7 @@ function createTaskQueue(deps) {
2398
2642
  }
2399
2643
  await driver.retry(name, id);
2400
2644
  const record = records.get(id);
2401
- if (record) record.status = "pending";
2645
+ if (record) transitionStatus(record, "pending");
2402
2646
  },
2403
2647
  async start() {
2404
2648
  if (stopped) {
@@ -2506,9 +2750,15 @@ import fs2 from "fs";
2506
2750
  var TASK_PATTERNS = ["src/tasks/**/task.ts"];
2507
2751
  var TASK_FILENAME = "task.ts";
2508
2752
  var CRON_RE = /(?:^|\n)\s*cron:\s*['"`]([^'"`\n]+)['"`]/;
2509
- var CONCURRENCY_RE = /(?:^|\n)\s*concurrency:\s*(\d+)/;
2510
- var RETRIES_RE = /(?:^|\n)\s*retries:\s*(\d+)/;
2511
- var TIMEOUT_MS_RE = /(?:^|\n)\s*timeoutMs:\s*(\d+)/;
2753
+ var NUM_LITERAL = "(\\d+(?:_\\d+)*)";
2754
+ var CONCURRENCY_RE = new RegExp(`(?:^|\\n)\\s*concurrency:\\s*${NUM_LITERAL}`);
2755
+ var RETRIES_RE = new RegExp(`(?:^|\\n)\\s*retries:\\s*${NUM_LITERAL}`);
2756
+ var TIMEOUT_MS_RE = new RegExp(`(?:^|\\n)\\s*timeoutMs:\\s*${NUM_LITERAL}`);
2757
+ var GRACE_MS_RE = new RegExp(`(?:^|\\n)\\s*graceMs:\\s*${NUM_LITERAL}`);
2758
+ function parseNumericLiteral(raw) {
2759
+ return Number(raw.replace(/_/g, ""));
2760
+ }
2761
+ var MIN_ISOLATED_TIMEOUT_MS = 6e4;
2512
2762
  function filePathToTaskName(filePath) {
2513
2763
  const normalized = filePath.replace(/\\/g, "/");
2514
2764
  const match = normalized.match(/(?:^|\/)tasks\/(.+)\/task\.ts$/);
@@ -2546,10 +2796,20 @@ async function scanTasks(rootDir, patterns) {
2546
2796
  const concurrency = CONCURRENCY_RE.exec(source)?.[1];
2547
2797
  const retries = RETRIES_RE.exec(source)?.[1];
2548
2798
  const timeoutMs = TIMEOUT_MS_RE.exec(source)?.[1];
2799
+ const graceMs = GRACE_MS_RE.exec(source)?.[1];
2549
2800
  if (cron !== void 0) manifest.cron = cron;
2550
- if (concurrency !== void 0) manifest.concurrency = Number(concurrency);
2551
- if (retries !== void 0) manifest.retries = Number(retries);
2552
- if (timeoutMs !== void 0) manifest.timeoutMs = Number(timeoutMs);
2801
+ if (concurrency !== void 0) manifest.concurrency = parseNumericLiteral(concurrency);
2802
+ if (retries !== void 0) manifest.retries = parseNumericLiteral(retries);
2803
+ if (timeoutMs !== void 0) {
2804
+ const value = parseNumericLiteral(timeoutMs);
2805
+ if (value < MIN_ISOLATED_TIMEOUT_MS) {
2806
+ throw new Error(
2807
+ `[faapi] Task "${name}" timeoutMs ${value} is below the minimum ${MIN_ISOLATED_TIMEOUT_MS}ms (1 minute). Declaring a timeout means this is a long-running task that needs real cancellation \u2014 tasks finishing within a minute do not need one: remove timeoutMs to run in-process (implement your own deadline with Promise.race if needed), or raise it.`
2808
+ );
2809
+ }
2810
+ manifest.timeoutMs = value;
2811
+ }
2812
+ if (graceMs !== void 0) manifest.graceMs = parseNumericLiteral(graceMs);
2553
2813
  tasks.push(manifest);
2554
2814
  }
2555
2815
  return tasks.sort((a, b) => a.name.localeCompare(b.name));
@@ -3673,6 +3933,7 @@ function logger(options = {}) {
3673
3933
  const response = await next();
3674
3934
  const duration = Date.now() - start;
3675
3935
  const entry = {
3936
+ requestId: ctx.requestId,
3676
3937
  method: ctx.method,
3677
3938
  path: ctx.path,
3678
3939
  status: response.status,
@@ -3685,6 +3946,7 @@ function logger(options = {}) {
3685
3946
  const message = err instanceof Error ? err.message : String(err);
3686
3947
  const status = err?.statusCode ?? 500;
3687
3948
  const entry = {
3949
+ requestId: ctx.requestId,
3688
3950
  method: ctx.method,
3689
3951
  path: ctx.path,
3690
3952
  status,
@@ -4307,6 +4569,12 @@ function formatErrorResponse(error, config) {
4307
4569
  }
4308
4570
 
4309
4571
  // src/runtime/createContext.ts
4572
+ function resolveRequestId(request) {
4573
+ const header = request.headers.get("x-request-id");
4574
+ const first = header?.split(",")[0]?.trim();
4575
+ if (first) return first;
4576
+ return crypto.randomUUID();
4577
+ }
4310
4578
  function parseCookies(cookieHeader) {
4311
4579
  const cookies = /* @__PURE__ */ new Map();
4312
4580
  if (!cookieHeader) return cookies;
@@ -4340,7 +4608,13 @@ function createContextFromUrl(request, url, params, config = {}, ip = "", regist
4340
4608
  for (const [key, val] of parsedCookies) {
4341
4609
  cookiesObj[key] = val;
4342
4610
  }
4611
+ const requestId = resolveRequestId(request);
4612
+ const log = createLogger("http", {
4613
+ fields: { requestId, method: request.method, path: url.pathname }
4614
+ });
4343
4615
  const ctx = {
4616
+ requestId,
4617
+ log,
4344
4618
  request,
4345
4619
  params,
4346
4620
  query: url.searchParams,
@@ -4725,6 +4999,9 @@ function getBuiltinInjectionValue(type, ctx, body) {
4725
4999
  // 任务子系统:注入 TaskClient(入队/查询);未注册工厂(无 app 编排)时 undefined
4726
5000
  case "tasks":
4727
5001
  return ctx.registries ? ctx.registries.taskHandle.get(ctx) : void 0;
5002
+ // 请求级日志器:与 ctx.log 同一实例(scope http,自动带 requestId/method/path 字段)
5003
+ case "log":
5004
+ return ctx.log;
4728
5005
  default:
4729
5006
  return void 0;
4730
5007
  }
@@ -6116,7 +6393,8 @@ function serializeTasks(tasks, dist = "dist") {
6116
6393
  ...t.cron !== void 0 ? { cron: t.cron } : {},
6117
6394
  ...t.concurrency !== void 0 ? { concurrency: t.concurrency } : {},
6118
6395
  ...t.retries !== void 0 ? { retries: t.retries } : {},
6119
- ...t.timeoutMs !== void 0 ? { timeoutMs: t.timeoutMs } : {}
6396
+ ...t.timeoutMs !== void 0 ? { timeoutMs: t.timeoutMs } : {},
6397
+ ...t.graceMs !== void 0 ? { graceMs: t.graceMs } : {}
6120
6398
  }));
6121
6399
  }
6122
6400
  async function writeTasksModule(manifest, outputPath) {
@@ -6132,7 +6410,8 @@ function hydrateTasks(manifest) {
6132
6410
  cron: t.cron ?? void 0,
6133
6411
  concurrency: t.concurrency ?? void 0,
6134
6412
  retries: t.retries ?? void 0,
6135
- timeoutMs: t.timeoutMs ?? void 0
6413
+ timeoutMs: t.timeoutMs ?? void 0,
6414
+ graceMs: t.graceMs ?? void 0
6136
6415
  }));
6137
6416
  }
6138
6417
  function collectTaskSchemaSources(tasks, rootDir) {
@@ -6558,6 +6837,7 @@ var FAAPI_CONFIG_KEYS = /* @__PURE__ */ new Set([
6558
6837
  "etag",
6559
6838
  "bodyLimit",
6560
6839
  "logger",
6840
+ "log",
6561
6841
  "http2",
6562
6842
  "trustedProxy",
6563
6843
  "response",
@@ -6576,6 +6856,7 @@ async function createAppBase(options) {
6576
6856
  );
6577
6857
  }
6578
6858
  const config = await loadConfig(rootDir, dist);
6859
+ configureLogging(config?.log);
6579
6860
  const serialized = await importWithCacheBust(routesPath);
6580
6861
  const hydrated = await hydrateRoutes(serialized);
6581
6862
  let sorted = sortRoutes(hydrated.routes);
@@ -7274,11 +7555,13 @@ export {
7274
7555
  ValidationError,
7275
7556
  clearAgentHandleFactory,
7276
7557
  collectRouteSchemaSources,
7558
+ configureLogging,
7277
7559
  cors,
7278
7560
  createProdApp as createApp,
7279
7561
  createAppRegistries,
7280
7562
  createCronScheduler,
7281
7563
  createDevApp,
7564
+ createLogger,
7282
7565
  createProdApp,
7283
7566
  createProgram,
7284
7567
  createPrograms,