@faapi/faapi 6.7.0 → 6.9.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/cli/index.js +539 -233
- package/dist/cli/index.js.map +1 -1
- package/dist/index.d.ts +50 -3
- package/dist/index.js +494 -210
- package/dist/index.js.map +1 -1
- package/dist/{routeTypes-CsL7xtqu.d.ts → routeTypes-CuGMDCGB.d.ts} +121 -2
- package/dist/testing.d.ts +1 -1
- package/dist/testing.js +136 -2
- package/dist/testing.js.map +1 -1
- package/package.json +1 -1
|
@@ -249,6 +249,84 @@ 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` 环境变量 > 模式默认(`dir` 文件模式不过滤,
|
|
310
|
+
* 其余模式 `'info'`;非法值启动报错)。`sink` 与 `dir` 互斥,同时配置启动报错。
|
|
311
|
+
*/
|
|
312
|
+
interface LogConfig {
|
|
313
|
+
/** 实例级级别(覆盖全局 level) */
|
|
314
|
+
level?: LogLevel;
|
|
315
|
+
/** 自定义输出管道(整体接管,与 `dir` 互斥) */
|
|
316
|
+
sink?: LogSink;
|
|
317
|
+
/**
|
|
318
|
+
* 文件日志目录(egg 风格):配置即启用内置文件管道——默认 `app.log` 全量 +
|
|
319
|
+
* `error.log` 仅 error(dup 语义);请求日志自动并入(`accessLog` 缺省 true)
|
|
320
|
+
*/
|
|
321
|
+
dir?: string;
|
|
322
|
+
/** true 时按级别四文件 `debug.log`/`info.log`/`warn.log`/`error.log`(各只含对应级别) */
|
|
323
|
+
splitByLevel?: boolean;
|
|
324
|
+
/** 文件模式下是否保留 console 双写(默认 true;false 纯文件) */
|
|
325
|
+
stdout?: boolean;
|
|
326
|
+
/** 请求日志是否并入统一管道;缺省 = `dir` 配置时 true(见 middleware/logger.md) */
|
|
327
|
+
accessLog?: boolean;
|
|
328
|
+
}
|
|
329
|
+
|
|
252
330
|
/**
|
|
253
331
|
* 隔离执行器签名(taskQueue 按任务 meta.timeoutMs 调用;测试可注入 spy)
|
|
254
332
|
*/
|
|
@@ -279,6 +357,18 @@ interface TaskWorkerOptions {
|
|
|
279
357
|
* taskCtx.registries)——语义层从 `TaskRegistriesView` 生成,缺省为空视图
|
|
280
358
|
*/
|
|
281
359
|
registries?: TaskRegistriesSnapshot;
|
|
360
|
+
/**
|
|
361
|
+
* 任务日志配置(纯数据,postMessage 传入,worker 内联重建日志器注入 taskCtx.log;
|
|
362
|
+
* 缺省时 taskCtx.log 为 undefined):scope/fields 与进程内路径一致(`task:<name>`
|
|
363
|
+
* + jobId/task/attempt),level 为宿主侧生效的全局级别(worker 侧预过滤,
|
|
364
|
+
* 宿主 writeLogEntry 再次过滤;undefined = 宿主未配置阈值——文件管道默认全量,
|
|
365
|
+
* worker 侧同样不过滤)
|
|
366
|
+
*/
|
|
367
|
+
log?: {
|
|
368
|
+
level?: LogLevel;
|
|
369
|
+
scope?: string;
|
|
370
|
+
fields?: Record<string, unknown>;
|
|
371
|
+
};
|
|
282
372
|
/** 外部取消信号(驱动停机超时 abort)——abort 同样触发两段式取消 */
|
|
283
373
|
externalSignal?: AbortSignal;
|
|
284
374
|
/**
|
|
@@ -286,6 +376,12 @@ interface TaskWorkerOptions {
|
|
|
286
376
|
* 消息回传宿主(语义层记入 `TaskJob.progress`);不传则进度消息被忽略
|
|
287
377
|
*/
|
|
288
378
|
onProgress?: (value: unknown) => void;
|
|
379
|
+
/**
|
|
380
|
+
* 日志回调:worker 内 taskCtx.log 的条目经 `{ type: 'log' }` 消息回传宿主,
|
|
381
|
+
* 由语义层接 writeLogEntry 走统一管道(自定义 sink 同样覆盖隔离任务);
|
|
382
|
+
* 不传则日志条目被忽略。宽限期(取消判定后)到达的条目不采纳(超时判定即终局)
|
|
383
|
+
*/
|
|
384
|
+
onLog?: (entry: LogEntry) => void;
|
|
289
385
|
}
|
|
290
386
|
declare function runTaskInWorker(options: TaskWorkerOptions): Promise<unknown>;
|
|
291
387
|
|
|
@@ -521,6 +617,13 @@ interface TaskContext {
|
|
|
521
617
|
* 不可克隆按执行错误处理)。仅 running 状态生效,终态后调用被忽略。
|
|
522
618
|
*/
|
|
523
619
|
progress?: (value: unknown) => void;
|
|
620
|
+
/**
|
|
621
|
+
* 任务级日志器(可选字段;框架两条执行路径均注入,直接构造 TaskContext 的
|
|
622
|
+
* 测试/自定义执行器可不传)——scope `task:<name>`,字段自动携带
|
|
623
|
+
* jobId/task/attempt,输出走 `config.log` 统一管道(详见 logger/logger.md)。
|
|
624
|
+
* 隔离执行时条目经 postMessage 回传宿主输出,fields 需可结构化克隆。
|
|
625
|
+
*/
|
|
626
|
+
log?: Logger;
|
|
524
627
|
}
|
|
525
628
|
/**
|
|
526
629
|
* 任务模块形态(task.ts 编译产物中与执行相关的导出)
|
|
@@ -773,6 +876,21 @@ interface FaapiContext {
|
|
|
773
876
|
* 无 app 编排(编程式直调 ctx)时为 undefined
|
|
774
877
|
*/
|
|
775
878
|
tasks?: TaskClient;
|
|
879
|
+
/**
|
|
880
|
+
* 请求 ID:请求头 `x-request-id` 第一段(网关透传场景跨服务串联),无则 crypto.randomUUID() 生成。
|
|
881
|
+
* ctx.log 与请求日志中间件的条目均携带该字段,业务日志与请求日志可经此关联
|
|
882
|
+
*/
|
|
883
|
+
requestId: string;
|
|
884
|
+
/**
|
|
885
|
+
* 请求级日志器(scope `http`,自动携带 requestId/method/path 字段,详见 logger/logger.md)
|
|
886
|
+
*
|
|
887
|
+
* ```ts
|
|
888
|
+
* export function GET(ctx) {
|
|
889
|
+
* ctx.log.info('listing users');
|
|
890
|
+
* }
|
|
891
|
+
* ```
|
|
892
|
+
*/
|
|
893
|
+
log: Logger;
|
|
776
894
|
request: Request;
|
|
777
895
|
params: Record<string, string>;
|
|
778
896
|
query: URLSearchParams;
|
|
@@ -1030,7 +1148,8 @@ interface LoggerOptions {
|
|
|
1030
1148
|
* 错误用 try/catch 捕获,记录后重新抛出(让上层处理)。
|
|
1031
1149
|
* 成功时从 next() 返回的 Response 读取状态码。
|
|
1032
1150
|
*
|
|
1033
|
-
* log 函数每次请求时读取(options.log ??
|
|
1151
|
+
* log 函数每次请求时读取(options.log ?? defaultLog),运行时替换 console.log 或
|
|
1152
|
+
* 切换全局日志配置均会生效。
|
|
1034
1153
|
*/
|
|
1035
1154
|
declare function logger(options?: LoggerOptions): FaapiMiddleware;
|
|
1036
1155
|
|
|
@@ -1137,4 +1256,4 @@ interface RouteInfo {
|
|
|
1137
1256
|
output: RouteOutputSchema | null;
|
|
1138
1257
|
}
|
|
1139
1258
|
|
|
1140
|
-
export {
|
|
1259
|
+
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,
|
|
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-CuGMDCGB.js';
|
|
2
2
|
import { Server } from 'node:http';
|
|
3
3
|
import { WebSocket } from 'ws';
|
|
4
4
|
|
package/dist/testing.js
CHANGED
|
@@ -245,7 +245,109 @@ function formatErrorResponse(error, config) {
|
|
|
245
245
|
return jsonOk(body, 500);
|
|
246
246
|
}
|
|
247
247
|
|
|
248
|
+
// src/logger/formatEntry.ts
|
|
249
|
+
function serializeFieldValue(value) {
|
|
250
|
+
if (value instanceof Error) {
|
|
251
|
+
return { name: value.name, message: value.message, stack: value.stack };
|
|
252
|
+
}
|
|
253
|
+
return value;
|
|
254
|
+
}
|
|
255
|
+
function formatEntry(entry) {
|
|
256
|
+
let line = `[${entry.time}] ${entry.level.toUpperCase()} `;
|
|
257
|
+
if (entry.scope) line += `[${entry.scope}] `;
|
|
258
|
+
line += entry.message;
|
|
259
|
+
if (entry.fields !== void 0) {
|
|
260
|
+
const plain = Object.fromEntries(
|
|
261
|
+
Object.entries(entry.fields).map(([k, v]) => [k, serializeFieldValue(v)])
|
|
262
|
+
);
|
|
263
|
+
try {
|
|
264
|
+
line += ` ${JSON.stringify(plain)}`;
|
|
265
|
+
} catch (err) {
|
|
266
|
+
line += ` [unserializable fields: ${err instanceof Error ? err.message : String(err)}]`;
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
return line;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// src/logger/logger.ts
|
|
273
|
+
var LEVEL_RANK = { debug: 0, info: 1, warn: 2, error: 3 };
|
|
274
|
+
var globalState = { level: null, sink: null, fileSink: null, stdout: true, accessLog: false, disabled: false };
|
|
275
|
+
function globalThreshold() {
|
|
276
|
+
if (globalState.level) return globalState.level;
|
|
277
|
+
if (globalState.fileSink) return void 0;
|
|
278
|
+
return "info";
|
|
279
|
+
}
|
|
280
|
+
function belowThreshold(level, threshold) {
|
|
281
|
+
return threshold !== void 0 && LEVEL_RANK[level] < LEVEL_RANK[threshold];
|
|
282
|
+
}
|
|
283
|
+
var consoleSink = (entry) => {
|
|
284
|
+
console[CONSOLE_METHOD[entry.level]](formatEntry(entry));
|
|
285
|
+
};
|
|
286
|
+
var CONSOLE_METHOD = {
|
|
287
|
+
debug: "debug",
|
|
288
|
+
info: "info",
|
|
289
|
+
warn: "warn",
|
|
290
|
+
error: "error"
|
|
291
|
+
};
|
|
292
|
+
function globalDispatch(entry) {
|
|
293
|
+
if (globalState.disabled) return;
|
|
294
|
+
if (globalState.sink) {
|
|
295
|
+
globalState.sink(entry);
|
|
296
|
+
return;
|
|
297
|
+
}
|
|
298
|
+
if (globalState.fileSink) {
|
|
299
|
+
globalState.fileSink.write(entry);
|
|
300
|
+
if (globalState.stdout) consoleSink(entry);
|
|
301
|
+
return;
|
|
302
|
+
}
|
|
303
|
+
consoleSink(entry);
|
|
304
|
+
}
|
|
305
|
+
function writeLogEntry(entry) {
|
|
306
|
+
if (belowThreshold(entry.level, globalThreshold())) return;
|
|
307
|
+
globalDispatch(entry);
|
|
308
|
+
}
|
|
309
|
+
function isAccessLogPiped() {
|
|
310
|
+
return !globalState.disabled && globalState.accessLog;
|
|
311
|
+
}
|
|
312
|
+
function createLogger(scope, options) {
|
|
313
|
+
const baseScope = scope;
|
|
314
|
+
const baseFields = options?.fields;
|
|
315
|
+
const write = (level, message, callFields, scopeSuffix) => {
|
|
316
|
+
const threshold = options?.level ?? globalThreshold();
|
|
317
|
+
if (belowThreshold(level, threshold)) return;
|
|
318
|
+
const entry = {
|
|
319
|
+
level,
|
|
320
|
+
message,
|
|
321
|
+
time: (/* @__PURE__ */ new Date()).toISOString()
|
|
322
|
+
};
|
|
323
|
+
const fullScope = scopeSuffix === void 0 ? baseScope : baseScope === void 0 ? scopeSuffix : `${baseScope}:${scopeSuffix}`;
|
|
324
|
+
if (fullScope !== void 0) entry.scope = fullScope;
|
|
325
|
+
if (baseFields !== void 0 || callFields !== void 0) {
|
|
326
|
+
entry.fields = { ...baseFields, ...callFields };
|
|
327
|
+
}
|
|
328
|
+
if (options?.sink) {
|
|
329
|
+
options.sink(entry);
|
|
330
|
+
} else {
|
|
331
|
+
globalDispatch(entry);
|
|
332
|
+
}
|
|
333
|
+
};
|
|
334
|
+
const makeLogger = (scopeSuffix) => ({
|
|
335
|
+
debug: (message, fields) => write("debug", message, fields, scopeSuffix),
|
|
336
|
+
info: (message, fields) => write("info", message, fields, scopeSuffix),
|
|
337
|
+
warn: (message, fields) => write("warn", message, fields, scopeSuffix),
|
|
338
|
+
error: (message, fields) => write("error", message, fields, scopeSuffix),
|
|
339
|
+
child: (childScope) => makeLogger(scopeSuffix === void 0 ? childScope : `${scopeSuffix}:${childScope}`)
|
|
340
|
+
});
|
|
341
|
+
return makeLogger(void 0);
|
|
342
|
+
}
|
|
343
|
+
|
|
248
344
|
// src/runtime/createContext.ts
|
|
345
|
+
function resolveRequestId(request) {
|
|
346
|
+
const header = request.headers.get("x-request-id");
|
|
347
|
+
const first = header?.split(",")[0]?.trim();
|
|
348
|
+
if (first) return first;
|
|
349
|
+
return crypto.randomUUID();
|
|
350
|
+
}
|
|
249
351
|
function parseCookies(cookieHeader) {
|
|
250
352
|
const cookies = /* @__PURE__ */ new Map();
|
|
251
353
|
if (!cookieHeader) return cookies;
|
|
@@ -279,7 +381,13 @@ function createContextFromUrl(request, url, params, config = {}, ip = "", regist
|
|
|
279
381
|
for (const [key, val] of parsedCookies) {
|
|
280
382
|
cookiesObj[key] = val;
|
|
281
383
|
}
|
|
384
|
+
const requestId = resolveRequestId(request);
|
|
385
|
+
const log = createLogger("http", {
|
|
386
|
+
fields: { requestId, method: request.method, path: url.pathname }
|
|
387
|
+
});
|
|
282
388
|
const ctx = {
|
|
389
|
+
requestId,
|
|
390
|
+
log,
|
|
283
391
|
request,
|
|
284
392
|
params,
|
|
285
393
|
query: url.searchParams,
|
|
@@ -553,8 +661,10 @@ var PARAM_TYPE_MAP = {
|
|
|
553
661
|
// Phase 2.3
|
|
554
662
|
agents: "agents",
|
|
555
663
|
// Phase 2.3
|
|
556
|
-
tasks: "tasks"
|
|
664
|
+
tasks: "tasks",
|
|
557
665
|
// 任务子系统:TaskClient(入队/查询)
|
|
666
|
+
log: "log"
|
|
667
|
+
// 请求级日志器(ctx.log 同一实例:scope http,自动带 requestId 等字段)
|
|
558
668
|
};
|
|
559
669
|
var injectionCache = /* @__PURE__ */ new WeakMap();
|
|
560
670
|
function resolveInjection(fn) {
|
|
@@ -872,6 +982,9 @@ function getBuiltinInjectionValue(type, ctx, body) {
|
|
|
872
982
|
// 任务子系统:注入 TaskClient(入队/查询);未注册工厂(无 app 编排)时 undefined
|
|
873
983
|
case "tasks":
|
|
874
984
|
return ctx.registries ? ctx.registries.taskHandle.get(ctx) : void 0;
|
|
985
|
+
// 请求级日志器:与 ctx.log 同一实例(scope http,自动带 requestId/method/path 字段)
|
|
986
|
+
case "log":
|
|
987
|
+
return ctx.log;
|
|
875
988
|
default:
|
|
876
989
|
return void 0;
|
|
877
990
|
}
|
|
@@ -4093,14 +4206,34 @@ function etag(options = {}) {
|
|
|
4093
4206
|
}
|
|
4094
4207
|
|
|
4095
4208
|
// src/middleware/logger.ts
|
|
4209
|
+
function levelForStatus(status) {
|
|
4210
|
+
if (status >= 500) return "error";
|
|
4211
|
+
if (status >= 400) return "warn";
|
|
4212
|
+
return "info";
|
|
4213
|
+
}
|
|
4214
|
+
function defaultLog(entry, text) {
|
|
4215
|
+
if (isAccessLogPiped()) {
|
|
4216
|
+
const logEntry = {
|
|
4217
|
+
level: levelForStatus(entry.status),
|
|
4218
|
+
message: text,
|
|
4219
|
+
time: (/* @__PURE__ */ new Date()).toISOString(),
|
|
4220
|
+
scope: "access",
|
|
4221
|
+
fields: entry
|
|
4222
|
+
};
|
|
4223
|
+
writeLogEntry(logEntry);
|
|
4224
|
+
return;
|
|
4225
|
+
}
|
|
4226
|
+
console.log(entry);
|
|
4227
|
+
}
|
|
4096
4228
|
function logger(options = {}) {
|
|
4097
4229
|
return async (ctx, next) => {
|
|
4098
|
-
const log = options.log ??
|
|
4230
|
+
const log = options.log ?? defaultLog;
|
|
4099
4231
|
const start = Date.now();
|
|
4100
4232
|
try {
|
|
4101
4233
|
const response = await next();
|
|
4102
4234
|
const duration = Date.now() - start;
|
|
4103
4235
|
const entry = {
|
|
4236
|
+
requestId: ctx.requestId,
|
|
4104
4237
|
method: ctx.method,
|
|
4105
4238
|
path: ctx.path,
|
|
4106
4239
|
status: response.status,
|
|
@@ -4113,6 +4246,7 @@ function logger(options = {}) {
|
|
|
4113
4246
|
const message = err instanceof Error ? err.message : String(err);
|
|
4114
4247
|
const status = err?.statusCode ?? 500;
|
|
4115
4248
|
const entry = {
|
|
4249
|
+
requestId: ctx.requestId,
|
|
4116
4250
|
method: ctx.method,
|
|
4117
4251
|
path: ctx.path,
|
|
4118
4252
|
status,
|