@faapi/faapi 6.31.0 → 6.33.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 +125 -88
- package/dist/cli/index.js.map +1 -1
- package/dist/{contextTypes-BKQsSVMv.d.ts → contextTypes-D1ORxB1P.d.ts} +1204 -65
- package/dist/index.d.ts +5 -760
- package/dist/index.js +234 -198
- package/dist/index.js.map +1 -1
- package/dist/testing.d.ts +2 -2
- package/dist/testing.js +49 -37
- package/dist/testing.js.map +1 -1
- package/dist/workerEntry.d.ts +20 -5
- package/dist/workerEntry.js +32 -5
- package/dist/workerEntry.js.map +1 -1
- package/package.json +1 -1
- package/dist/routeTypes-Cc0P3CcU.d.ts +0 -204
package/dist/index.js
CHANGED
|
@@ -1126,6 +1126,8 @@ var init_resolveInjection = __esm({
|
|
|
1126
1126
|
// 任务子系统:TaskClient(入队/查询)
|
|
1127
1127
|
log: "log",
|
|
1128
1128
|
// 请求级日志器(ctx.log 同一实例:scope http,自动带 requestId 等字段)
|
|
1129
|
+
llm: "llm",
|
|
1130
|
+
// 轻量 LLM 补全通道(@faapi/agent 插件注册到 registries.llm;未加载时 undefined)
|
|
1129
1131
|
// raw 系:恒原始输入(无 schema、无校验、管线永不转换),与 query/params/body
|
|
1130
1132
|
// 的「校验转换后」口径二分互补
|
|
1131
1133
|
rawQuery: "rawQuery",
|
|
@@ -1970,6 +1972,20 @@ function createAgentHandleStore() {
|
|
|
1970
1972
|
}
|
|
1971
1973
|
};
|
|
1972
1974
|
}
|
|
1975
|
+
function createLlmChannelStore() {
|
|
1976
|
+
let current = null;
|
|
1977
|
+
return {
|
|
1978
|
+
register(channel) {
|
|
1979
|
+
current = channel;
|
|
1980
|
+
},
|
|
1981
|
+
get() {
|
|
1982
|
+
return current ?? void 0;
|
|
1983
|
+
},
|
|
1984
|
+
clear() {
|
|
1985
|
+
current = null;
|
|
1986
|
+
}
|
|
1987
|
+
};
|
|
1988
|
+
}
|
|
1973
1989
|
function createTaskHandleStore() {
|
|
1974
1990
|
let currentFactory = null;
|
|
1975
1991
|
return {
|
|
@@ -2026,7 +2042,8 @@ function createAppRegistries() {
|
|
|
2026
2042
|
const task = createTaskRegistry();
|
|
2027
2043
|
const agentHandle = createAgentHandleStore();
|
|
2028
2044
|
const taskHandle = createTaskHandleStore();
|
|
2029
|
-
|
|
2045
|
+
const llm = createLlmChannelStore();
|
|
2046
|
+
return { tool, agent, skill, task, agentHandle, taskHandle, llm };
|
|
2030
2047
|
}
|
|
2031
2048
|
var defaultRegistries = createAppRegistries();
|
|
2032
2049
|
|
|
@@ -2109,19 +2126,6 @@ var TaskCancelledError = class extends Error {
|
|
|
2109
2126
|
this.name = "TaskCancelledError";
|
|
2110
2127
|
}
|
|
2111
2128
|
};
|
|
2112
|
-
function safeConfig(config) {
|
|
2113
|
-
if (config === void 0 || config === null) return config;
|
|
2114
|
-
try {
|
|
2115
|
-
structuredClone(config);
|
|
2116
|
-
return config;
|
|
2117
|
-
} catch {
|
|
2118
|
-
}
|
|
2119
|
-
try {
|
|
2120
|
-
return JSON.parse(JSON.stringify(config));
|
|
2121
|
-
} catch {
|
|
2122
|
-
return void 0;
|
|
2123
|
-
}
|
|
2124
|
-
}
|
|
2125
2129
|
function reviveError(payload) {
|
|
2126
2130
|
const err = new Error(payload?.message ?? "task worker error");
|
|
2127
2131
|
if (payload?.name !== void 0) err.name = payload.name;
|
|
@@ -2236,22 +2240,130 @@ async function runTaskInWorker(options) {
|
|
|
2236
2240
|
type: "run",
|
|
2237
2241
|
payload,
|
|
2238
2242
|
taskCtx: {
|
|
2239
|
-
config: safeConfig(taskCtx.config),
|
|
2240
2243
|
job: taskCtx.job
|
|
2241
2244
|
},
|
|
2242
2245
|
registries: options.registries,
|
|
2246
|
+
llms: options.llms,
|
|
2243
2247
|
log: options.log
|
|
2244
2248
|
});
|
|
2245
2249
|
} catch (err) {
|
|
2246
2250
|
finish(
|
|
2247
|
-
() => reject(
|
|
2248
|
-
new Error(`Task "${taskCtx.job.name}" payload/config is not cloneable: ${String(err)}`)
|
|
2249
|
-
)
|
|
2251
|
+
() => reject(new Error(`Task "${taskCtx.job.name}" payload is not cloneable: ${String(err)}`))
|
|
2250
2252
|
);
|
|
2251
2253
|
}
|
|
2252
2254
|
});
|
|
2253
2255
|
}
|
|
2254
2256
|
|
|
2257
|
+
// src/utils/isPlainObject.ts
|
|
2258
|
+
function isPlainObject(value) {
|
|
2259
|
+
if (value === null || typeof value !== "object") {
|
|
2260
|
+
return false;
|
|
2261
|
+
}
|
|
2262
|
+
if (Array.isArray(value)) {
|
|
2263
|
+
return false;
|
|
2264
|
+
}
|
|
2265
|
+
const proto = Object.getPrototypeOf(value);
|
|
2266
|
+
return proto === null || proto === Object.prototype;
|
|
2267
|
+
}
|
|
2268
|
+
|
|
2269
|
+
// src/utils/stringifyJson.ts
|
|
2270
|
+
function stringifyJson(value) {
|
|
2271
|
+
if (!needsConversion(value, /* @__PURE__ */ new Set())) {
|
|
2272
|
+
return JSON.stringify(value);
|
|
2273
|
+
}
|
|
2274
|
+
return JSON.stringify(convertValue(value, /* @__PURE__ */ new Set()));
|
|
2275
|
+
}
|
|
2276
|
+
function needsConversion(value, ancestors) {
|
|
2277
|
+
if (value === null) return false;
|
|
2278
|
+
const type = typeof value;
|
|
2279
|
+
if (type === "bigint") return true;
|
|
2280
|
+
if (type === "number") return !Number.isFinite(value);
|
|
2281
|
+
if (type !== "object") return false;
|
|
2282
|
+
const obj = value;
|
|
2283
|
+
if (ancestors.has(obj)) {
|
|
2284
|
+
throw new TypeError("[faapi] Converting circular structure to JSON");
|
|
2285
|
+
}
|
|
2286
|
+
if (obj instanceof Date || obj instanceof Map || obj instanceof Set || obj instanceof RegExp) {
|
|
2287
|
+
return true;
|
|
2288
|
+
}
|
|
2289
|
+
if (typeof obj.toJSON === "function") return true;
|
|
2290
|
+
if (Array.isArray(obj)) {
|
|
2291
|
+
ancestors.add(obj);
|
|
2292
|
+
for (let i = 0; i < obj.length; i++) {
|
|
2293
|
+
if (needsConversion(obj[i], ancestors)) {
|
|
2294
|
+
ancestors.delete(obj);
|
|
2295
|
+
return true;
|
|
2296
|
+
}
|
|
2297
|
+
}
|
|
2298
|
+
ancestors.delete(obj);
|
|
2299
|
+
return false;
|
|
2300
|
+
}
|
|
2301
|
+
if (isPlainObject(obj)) {
|
|
2302
|
+
ancestors.add(obj);
|
|
2303
|
+
for (const key in obj) {
|
|
2304
|
+
if (!Object.prototype.hasOwnProperty.call(obj, key)) continue;
|
|
2305
|
+
if (needsConversion(obj[key], ancestors)) {
|
|
2306
|
+
ancestors.delete(obj);
|
|
2307
|
+
return true;
|
|
2308
|
+
}
|
|
2309
|
+
}
|
|
2310
|
+
ancestors.delete(obj);
|
|
2311
|
+
return false;
|
|
2312
|
+
}
|
|
2313
|
+
return false;
|
|
2314
|
+
}
|
|
2315
|
+
function convertValue(value, ancestors) {
|
|
2316
|
+
if (value === null) return value;
|
|
2317
|
+
const type = typeof value;
|
|
2318
|
+
if (type === "bigint") return value.toString();
|
|
2319
|
+
if (type === "number") return Number.isFinite(value) ? value : String(value);
|
|
2320
|
+
if (type !== "object") return value;
|
|
2321
|
+
const obj = value;
|
|
2322
|
+
if (ancestors.has(obj)) {
|
|
2323
|
+
throw new TypeError("[faapi] Converting circular structure to JSON");
|
|
2324
|
+
}
|
|
2325
|
+
if (obj instanceof Date) return obj.getTime();
|
|
2326
|
+
if (obj instanceof Map) {
|
|
2327
|
+
ancestors.add(obj);
|
|
2328
|
+
const entries = Array.from(obj, ([k, v]) => [
|
|
2329
|
+
convertValue(k, ancestors),
|
|
2330
|
+
convertValue(v, ancestors)
|
|
2331
|
+
]);
|
|
2332
|
+
ancestors.delete(obj);
|
|
2333
|
+
return entries;
|
|
2334
|
+
}
|
|
2335
|
+
if (obj instanceof Set) {
|
|
2336
|
+
ancestors.add(obj);
|
|
2337
|
+
const values = Array.from(obj, (v) => convertValue(v, ancestors));
|
|
2338
|
+
ancestors.delete(obj);
|
|
2339
|
+
return values;
|
|
2340
|
+
}
|
|
2341
|
+
if (obj instanceof RegExp) return obj.toString();
|
|
2342
|
+
const toJSON = obj.toJSON;
|
|
2343
|
+
if (typeof toJSON === "function") {
|
|
2344
|
+
ancestors.add(obj);
|
|
2345
|
+
const result = convertValue(toJSON.call(obj), ancestors);
|
|
2346
|
+
ancestors.delete(obj);
|
|
2347
|
+
return result;
|
|
2348
|
+
}
|
|
2349
|
+
if (Array.isArray(obj)) {
|
|
2350
|
+
ancestors.add(obj);
|
|
2351
|
+
const arr = obj.map((item) => convertValue(item, ancestors));
|
|
2352
|
+
ancestors.delete(obj);
|
|
2353
|
+
return arr;
|
|
2354
|
+
}
|
|
2355
|
+
if (isPlainObject(obj)) {
|
|
2356
|
+
ancestors.add(obj);
|
|
2357
|
+
const out = {};
|
|
2358
|
+
for (const [key, val] of Object.entries(obj)) {
|
|
2359
|
+
out[key] = convertValue(val, ancestors);
|
|
2360
|
+
}
|
|
2361
|
+
ancestors.delete(obj);
|
|
2362
|
+
return out;
|
|
2363
|
+
}
|
|
2364
|
+
return obj;
|
|
2365
|
+
}
|
|
2366
|
+
|
|
2255
2367
|
// src/logger/formatEntry.ts
|
|
2256
2368
|
function serializeFieldValue(value) {
|
|
2257
2369
|
if (value instanceof Error) {
|
|
@@ -2267,11 +2379,7 @@ function formatEntry(entry) {
|
|
|
2267
2379
|
const plain = Object.fromEntries(
|
|
2268
2380
|
Object.entries(entry.fields).map(([k, v]) => [k, serializeFieldValue(v)])
|
|
2269
2381
|
);
|
|
2270
|
-
|
|
2271
|
-
line += ` ${JSON.stringify(plain)}`;
|
|
2272
|
-
} catch (err) {
|
|
2273
|
-
line += ` [unserializable fields: ${err instanceof Error ? err.message : String(err)}]`;
|
|
2274
|
-
}
|
|
2382
|
+
line += ` ${stringifyJson(plain)}`;
|
|
2275
2383
|
}
|
|
2276
2384
|
return line;
|
|
2277
2385
|
}
|
|
@@ -2289,7 +2397,14 @@ function createFileLogSink(options) {
|
|
|
2289
2397
|
};
|
|
2290
2398
|
const openStream = (name) => {
|
|
2291
2399
|
const stream = fs2.createWriteStream(path3.join(options.dir, `${name}.log`), { flags: "a" });
|
|
2292
|
-
|
|
2400
|
+
let streamErrorLogged = false;
|
|
2401
|
+
stream.on("error", (err) => {
|
|
2402
|
+
if (streamErrorLogged) return;
|
|
2403
|
+
streamErrorLogged = true;
|
|
2404
|
+
console.error(
|
|
2405
|
+
`[faapi] log file stream "${name}.log" failed \u2014 file logging on this stream stopped (subsequent errors suppressed). Check disk space / permissions for ${options.dir}:`,
|
|
2406
|
+
err
|
|
2407
|
+
);
|
|
2293
2408
|
});
|
|
2294
2409
|
streams.set(name, stream);
|
|
2295
2410
|
};
|
|
@@ -2521,13 +2636,14 @@ function createTaskQueue(deps) {
|
|
|
2521
2636
|
const loadTaskModule = deps.loadTaskModule ?? (async (filePath) => await import(filePath));
|
|
2522
2637
|
const loadPayloadSchema = deps.loadPayloadSchema ?? (async (filePath) => {
|
|
2523
2638
|
const zodPath = path4.join(path4.dirname(filePath), "zod.js");
|
|
2524
|
-
|
|
2525
|
-
|
|
2526
|
-
|
|
2527
|
-
|
|
2528
|
-
|
|
2529
|
-
|
|
2639
|
+
const mod = await import(zodPath);
|
|
2640
|
+
const schemaKey = Object.keys(mod).find((k) => k.endsWith("Schema"));
|
|
2641
|
+
if (!schemaKey) {
|
|
2642
|
+
throw new Error(
|
|
2643
|
+
`[faapi] Task payload schema artifact has no *Schema export: ${zodPath} \u2014 re-run "faapi dev" / "faapi build" to regenerate task artifacts.`
|
|
2644
|
+
);
|
|
2530
2645
|
}
|
|
2646
|
+
return mod[schemaKey];
|
|
2531
2647
|
});
|
|
2532
2648
|
function assertKnownTask(name) {
|
|
2533
2649
|
const meta = registry.get(name);
|
|
@@ -2546,14 +2662,13 @@ function createTaskQueue(deps) {
|
|
|
2546
2662
|
async function validatePayload(name, payload) {
|
|
2547
2663
|
if (!schemaCache.has(name)) {
|
|
2548
2664
|
const meta = registry.get(name);
|
|
2549
|
-
schemaCache.set(
|
|
2550
|
-
name,
|
|
2551
|
-
await loadPayloadSchema(path4.resolve(rootDir, meta.filePath)).catch(() => void 0)
|
|
2552
|
-
);
|
|
2665
|
+
schemaCache.set(name, await loadPayloadSchema(path4.resolve(rootDir, meta.filePath)));
|
|
2553
2666
|
}
|
|
2554
2667
|
const schema = schemaCache.get(name);
|
|
2555
2668
|
if (!schema || typeof schema.safeParse !== "function") {
|
|
2556
|
-
|
|
2669
|
+
throw new Error(
|
|
2670
|
+
`[faapi] Task "${name}" has no valid payload schema (zod.js missing or no *Schema export). Payload declarations are mandatory: re-run "faapi dev" / "faapi build" to regenerate task artifacts.`
|
|
2671
|
+
);
|
|
2557
2672
|
}
|
|
2558
2673
|
const result = schema.safeParse(payload);
|
|
2559
2674
|
if (!result.success) {
|
|
@@ -2587,11 +2702,15 @@ function createTaskQueue(deps) {
|
|
|
2587
2702
|
taskModulePath: path4.resolve(rootDir, meta.filePath),
|
|
2588
2703
|
payload: job.payload,
|
|
2589
2704
|
taskCtx: {
|
|
2590
|
-
config
|
|
2705
|
+
// 隔离任务不传 config:worker 线程不接收进程配置(config 含函数字段
|
|
2706
|
+
// 不可结构化克隆,框架不做降级传递)——任务数据经 payload 显式传入
|
|
2591
2707
|
resourcesDir: deps.resourcesDir,
|
|
2592
2708
|
job: { id: job.id, name: job.name, attempt: job.attempt }
|
|
2593
2709
|
},
|
|
2594
2710
|
registries: snapshotRegistries(),
|
|
2711
|
+
// agent.llms 纯数据快照随 workerData 下发,worker 内动态加载 @faapi/agent
|
|
2712
|
+
// 重建轻量补全通道(taskCtx.llm)——llms 未配置时不传
|
|
2713
|
+
llms: deps.llms,
|
|
2595
2714
|
// 日志配置随 workerData 下发(纯数据),worker 内联重建 taskCtx.log,
|
|
2596
2715
|
// 条目回传宿主走统一管道(scope/fields 与进程内路径一致)
|
|
2597
2716
|
log: {
|
|
@@ -2621,6 +2740,8 @@ function createTaskQueue(deps) {
|
|
|
2621
2740
|
config: deps.config,
|
|
2622
2741
|
job: { id: job.id, name: job.name, attempt: job.attempt },
|
|
2623
2742
|
registries: registriesView,
|
|
2743
|
+
// 轻量补全通道惰性读取(插件晚于队列构造注册,执行时刻取值才可见)
|
|
2744
|
+
llm: deps.llm?.get(),
|
|
2624
2745
|
log: createLogger(`task:${job.name}`, {
|
|
2625
2746
|
fields: { jobId: job.id, task: job.name, attempt: job.attempt }
|
|
2626
2747
|
}),
|
|
@@ -2649,7 +2770,8 @@ function createTaskQueue(deps) {
|
|
|
2649
2770
|
cancelled,
|
|
2650
2771
|
error: record.error
|
|
2651
2772
|
})
|
|
2652
|
-
).catch(() => {
|
|
2773
|
+
).catch((hookErr) => {
|
|
2774
|
+
console.error(`[faapi] task onFailed hook threw for "${job.name}":`, hookErr);
|
|
2653
2775
|
});
|
|
2654
2776
|
}
|
|
2655
2777
|
throw err;
|
|
@@ -2872,25 +2994,27 @@ function detectIgnoredMetaFields(source, strictHit) {
|
|
|
2872
2994
|
for (const line of source.split("\n")) {
|
|
2873
2995
|
const trimmed = line.trim();
|
|
2874
2996
|
if (trimmed.startsWith("//")) continue;
|
|
2997
|
+
if (trimmed.startsWith("*") || trimmed.startsWith("/*") || trimmed.endsWith("*/")) continue;
|
|
2875
2998
|
for (const field of ALL_META_FIELDS) {
|
|
2876
2999
|
if (new RegExp(`\\b${field}\\s*:`).test(trimmed)) declared.add(field);
|
|
2877
3000
|
}
|
|
2878
3001
|
}
|
|
2879
3002
|
return ALL_META_FIELDS.filter((field) => declared.has(field) && !strictHit[field]);
|
|
2880
3003
|
}
|
|
2881
|
-
function
|
|
2882
|
-
|
|
3004
|
+
function throwIgnoredMetaFields(name, fields) {
|
|
3005
|
+
if (fields.length === 0) return;
|
|
3006
|
+
const details = fields.map((field) => {
|
|
2883
3007
|
if (STRING_META_FIELDS.includes(field)) {
|
|
2884
|
-
|
|
2885
|
-
`[faapi] Task "${name}" declares "${field}" but its value is not a string literal \u2014 the declaration was ignored and the task will not be cron-scheduled. Meta values are extracted from source code without importing it, so only literal values are recognized: use a quoted literal (cron: '0 3 * * *'), not an expression or a dynamic value.`
|
|
2886
|
-
);
|
|
2887
|
-
} else {
|
|
2888
|
-
const consequence = field === "timeoutMs" ? " and the task will run in-process with no timeout" : "";
|
|
2889
|
-
console.warn(
|
|
2890
|
-
`[faapi] Task "${name}" declares "${field}" but its value is not a numeric literal \u2014 the declaration was ignored${consequence}. Meta values are extracted from source code without importing it, so only literal values are recognized (underscores allowed, e.g. 1_800_000); expressions like 30 * 60_000 or dynamic values are not supported.`
|
|
2891
|
-
);
|
|
3008
|
+
return `"${field}": value is not a string literal \u2014 the declaration would be ignored and the task would not be cron-scheduled. Use a quoted literal (cron: '0 3 * * *'), not an expression or a dynamic value.`;
|
|
2892
3009
|
}
|
|
2893
|
-
|
|
3010
|
+
const consequence = field === "timeoutMs" ? " \u2014 the declaration would be ignored and the task would silently run in-process with no timeout" : " \u2014 the declaration would be ignored";
|
|
3011
|
+
return `"${field}": value is not a numeric literal${consequence}. Only literal values are recognized (underscores allowed, e.g. 1_800_000); expressions like 30 * 60_000 or dynamic values are not supported.`;
|
|
3012
|
+
});
|
|
3013
|
+
throw new Error(
|
|
3014
|
+
`[faapi] Task "${name}" declares meta field(s) with non-literal values:
|
|
3015
|
+
${details.join("\n ")}
|
|
3016
|
+
Meta values are extracted from source code without importing it, so only literals are recognized. If a matched line is inside a block comment (e.g. a single-line /* ... */ comment containing a meta-like declaration), rewrite the comment so it does not look like a meta declaration.`
|
|
3017
|
+
);
|
|
2894
3018
|
}
|
|
2895
3019
|
function parseNumericLiteral(raw) {
|
|
2896
3020
|
return Number(raw.replace(/_/g, ""));
|
|
@@ -2960,7 +3084,7 @@ async function scanTasks(rootDir, patterns) {
|
|
|
2960
3084
|
timeoutMs: timeoutMs !== void 0,
|
|
2961
3085
|
graceMs: graceMs !== void 0
|
|
2962
3086
|
};
|
|
2963
|
-
|
|
3087
|
+
throwIgnoredMetaFields(name, detectIgnoredMetaFields(source, strictHit));
|
|
2964
3088
|
tasks.push(manifest);
|
|
2965
3089
|
}
|
|
2966
3090
|
return tasks.sort((a, b) => a.name.localeCompare(b.name));
|
|
@@ -3560,11 +3684,7 @@ async function ensureMiddlewaresCompiled(middlewarePaths, rootDir) {
|
|
|
3560
3684
|
if (!dist) return;
|
|
3561
3685
|
for (const mwPath of middlewarePaths) {
|
|
3562
3686
|
const sourcePath = prodPathToSourcePath(mwPath, rootDir, dist);
|
|
3563
|
-
|
|
3564
|
-
await ensureCompiled(sourcePath, rootDir, dist);
|
|
3565
|
-
} catch (err) {
|
|
3566
|
-
console.error(`[faapi] Failed to compile middleware source ${sourcePath}:`, err);
|
|
3567
|
-
}
|
|
3687
|
+
await ensureCompiled(sourcePath, rootDir, dist);
|
|
3568
3688
|
}
|
|
3569
3689
|
}
|
|
3570
3690
|
function prodSourcePathToProductPath(sourceAbsPath, rootDir, dist) {
|
|
@@ -4568,116 +4688,6 @@ function hasBufferedBody(response) {
|
|
|
4568
4688
|
return bufferedBodyResponses.has(response);
|
|
4569
4689
|
}
|
|
4570
4690
|
|
|
4571
|
-
// src/utils/isPlainObject.ts
|
|
4572
|
-
function isPlainObject(value) {
|
|
4573
|
-
if (value === null || typeof value !== "object") {
|
|
4574
|
-
return false;
|
|
4575
|
-
}
|
|
4576
|
-
if (Array.isArray(value)) {
|
|
4577
|
-
return false;
|
|
4578
|
-
}
|
|
4579
|
-
const proto = Object.getPrototypeOf(value);
|
|
4580
|
-
return proto === null || proto === Object.prototype;
|
|
4581
|
-
}
|
|
4582
|
-
|
|
4583
|
-
// src/utils/stringifyJson.ts
|
|
4584
|
-
function stringifyJson(value) {
|
|
4585
|
-
if (!needsConversion(value, /* @__PURE__ */ new Set())) {
|
|
4586
|
-
return JSON.stringify(value);
|
|
4587
|
-
}
|
|
4588
|
-
return JSON.stringify(convertValue(value, /* @__PURE__ */ new Set()));
|
|
4589
|
-
}
|
|
4590
|
-
function needsConversion(value, ancestors) {
|
|
4591
|
-
if (value === null) return false;
|
|
4592
|
-
const type = typeof value;
|
|
4593
|
-
if (type === "bigint") return true;
|
|
4594
|
-
if (type === "number") return !Number.isFinite(value);
|
|
4595
|
-
if (type !== "object") return false;
|
|
4596
|
-
const obj = value;
|
|
4597
|
-
if (ancestors.has(obj)) {
|
|
4598
|
-
throw new TypeError("[faapi] Converting circular structure to JSON");
|
|
4599
|
-
}
|
|
4600
|
-
if (obj instanceof Date || obj instanceof Map || obj instanceof Set || obj instanceof RegExp) {
|
|
4601
|
-
return true;
|
|
4602
|
-
}
|
|
4603
|
-
if (typeof obj.toJSON === "function") return true;
|
|
4604
|
-
if (Array.isArray(obj)) {
|
|
4605
|
-
ancestors.add(obj);
|
|
4606
|
-
for (let i = 0; i < obj.length; i++) {
|
|
4607
|
-
if (needsConversion(obj[i], ancestors)) {
|
|
4608
|
-
ancestors.delete(obj);
|
|
4609
|
-
return true;
|
|
4610
|
-
}
|
|
4611
|
-
}
|
|
4612
|
-
ancestors.delete(obj);
|
|
4613
|
-
return false;
|
|
4614
|
-
}
|
|
4615
|
-
if (isPlainObject(obj)) {
|
|
4616
|
-
ancestors.add(obj);
|
|
4617
|
-
for (const key in obj) {
|
|
4618
|
-
if (!Object.prototype.hasOwnProperty.call(obj, key)) continue;
|
|
4619
|
-
if (needsConversion(obj[key], ancestors)) {
|
|
4620
|
-
ancestors.delete(obj);
|
|
4621
|
-
return true;
|
|
4622
|
-
}
|
|
4623
|
-
}
|
|
4624
|
-
ancestors.delete(obj);
|
|
4625
|
-
return false;
|
|
4626
|
-
}
|
|
4627
|
-
return false;
|
|
4628
|
-
}
|
|
4629
|
-
function convertValue(value, ancestors) {
|
|
4630
|
-
if (value === null) return value;
|
|
4631
|
-
const type = typeof value;
|
|
4632
|
-
if (type === "bigint") return value.toString();
|
|
4633
|
-
if (type === "number") return Number.isFinite(value) ? value : String(value);
|
|
4634
|
-
if (type !== "object") return value;
|
|
4635
|
-
const obj = value;
|
|
4636
|
-
if (ancestors.has(obj)) {
|
|
4637
|
-
throw new TypeError("[faapi] Converting circular structure to JSON");
|
|
4638
|
-
}
|
|
4639
|
-
if (obj instanceof Date) return obj.getTime();
|
|
4640
|
-
if (obj instanceof Map) {
|
|
4641
|
-
ancestors.add(obj);
|
|
4642
|
-
const entries = Array.from(obj, ([k, v]) => [
|
|
4643
|
-
convertValue(k, ancestors),
|
|
4644
|
-
convertValue(v, ancestors)
|
|
4645
|
-
]);
|
|
4646
|
-
ancestors.delete(obj);
|
|
4647
|
-
return entries;
|
|
4648
|
-
}
|
|
4649
|
-
if (obj instanceof Set) {
|
|
4650
|
-
ancestors.add(obj);
|
|
4651
|
-
const values = Array.from(obj, (v) => convertValue(v, ancestors));
|
|
4652
|
-
ancestors.delete(obj);
|
|
4653
|
-
return values;
|
|
4654
|
-
}
|
|
4655
|
-
if (obj instanceof RegExp) return obj.toString();
|
|
4656
|
-
const toJSON = obj.toJSON;
|
|
4657
|
-
if (typeof toJSON === "function") {
|
|
4658
|
-
ancestors.add(obj);
|
|
4659
|
-
const result = convertValue(toJSON.call(obj), ancestors);
|
|
4660
|
-
ancestors.delete(obj);
|
|
4661
|
-
return result;
|
|
4662
|
-
}
|
|
4663
|
-
if (Array.isArray(obj)) {
|
|
4664
|
-
ancestors.add(obj);
|
|
4665
|
-
const arr = obj.map((item) => convertValue(item, ancestors));
|
|
4666
|
-
ancestors.delete(obj);
|
|
4667
|
-
return arr;
|
|
4668
|
-
}
|
|
4669
|
-
if (isPlainObject(obj)) {
|
|
4670
|
-
ancestors.add(obj);
|
|
4671
|
-
const out = {};
|
|
4672
|
-
for (const [key, val] of Object.entries(obj)) {
|
|
4673
|
-
out[key] = convertValue(val, ancestors);
|
|
4674
|
-
}
|
|
4675
|
-
ancestors.delete(obj);
|
|
4676
|
-
return out;
|
|
4677
|
-
}
|
|
4678
|
-
return obj;
|
|
4679
|
-
}
|
|
4680
|
-
|
|
4681
4691
|
// src/runtime/sse.ts
|
|
4682
4692
|
function encodeSseEvent(event) {
|
|
4683
4693
|
let out = "";
|
|
@@ -5375,6 +5385,10 @@ function getBuiltinInjectionValue(type, ctx, body) {
|
|
|
5375
5385
|
// 任务子系统:注入 TaskClient(入队/查询);未注册工厂(无 app 编排)时 undefined
|
|
5376
5386
|
case "tasks":
|
|
5377
5387
|
return ctx.registries ? ctx.registries.taskHandle.get(ctx) : void 0;
|
|
5388
|
+
// 轻量 LLM 补全通道:@faapi/agent 插件注册到 registries.llm;与请求上下文无关,
|
|
5389
|
+
// get 无参。插件未加载(或编程式直调 ctx 无 registries)时 undefined
|
|
5390
|
+
case "llm":
|
|
5391
|
+
return ctx.registries ? ctx.registries.llm.get() : void 0;
|
|
5378
5392
|
// 请求级日志器:与 ctx.log 同一实例(scope http,自动带 requestId/method/path 字段)
|
|
5379
5393
|
case "log":
|
|
5380
5394
|
return ctx.log;
|
|
@@ -5871,38 +5885,30 @@ function setCachedMiddlewares(absPath, bundle) {
|
|
|
5871
5885
|
middlewareCache.set(absPath, bundle);
|
|
5872
5886
|
}
|
|
5873
5887
|
async function loadMiddlewaresFile(filePath) {
|
|
5874
|
-
|
|
5875
|
-
try {
|
|
5876
|
-
module = await importWithCacheBust(filePath);
|
|
5877
|
-
} catch (err) {
|
|
5878
|
-
console.error(
|
|
5879
|
-
`[faapi] Failed to load middlewares from ${filePath}:`,
|
|
5880
|
-
err instanceof Error ? err.stack ?? err.message : err
|
|
5881
|
-
);
|
|
5882
|
-
return { middlewares: [], injectors: {} };
|
|
5883
|
-
}
|
|
5888
|
+
const module = await importWithCacheBust(filePath);
|
|
5884
5889
|
const middlewares = module.default ?? module.middlewares ?? [];
|
|
5885
5890
|
if (!Array.isArray(middlewares)) {
|
|
5886
|
-
|
|
5887
|
-
|
|
5891
|
+
throw new TypeError(
|
|
5892
|
+
`[faapi] middlewares.ts must export an array (default or named "middlewares"): ${filePath}`
|
|
5893
|
+
);
|
|
5888
5894
|
}
|
|
5889
|
-
const validMiddlewares =
|
|
5895
|
+
const validMiddlewares = [];
|
|
5896
|
+
for (const m of middlewares) {
|
|
5890
5897
|
if (typeof m !== "function") {
|
|
5891
|
-
|
|
5892
|
-
|
|
5898
|
+
throw new TypeError(
|
|
5899
|
+
`[faapi] every middleware must be a function, got ${typeof m}: ${filePath}`
|
|
5900
|
+
);
|
|
5893
5901
|
}
|
|
5894
|
-
|
|
5895
|
-
}
|
|
5902
|
+
validMiddlewares.push(m);
|
|
5903
|
+
}
|
|
5896
5904
|
const injectors = module.injectors ?? {};
|
|
5897
5905
|
if (typeof injectors !== "object" || injectors === null) {
|
|
5898
|
-
|
|
5899
|
-
return { middlewares: validMiddlewares, injectors: {} };
|
|
5906
|
+
throw new TypeError(`[faapi] "injectors" export must be an object: ${filePath}`);
|
|
5900
5907
|
}
|
|
5901
5908
|
const validInjectors = {};
|
|
5902
5909
|
for (const [name, injector] of Object.entries(injectors)) {
|
|
5903
5910
|
if (typeof injector !== "function") {
|
|
5904
|
-
|
|
5905
|
-
continue;
|
|
5911
|
+
throw new TypeError(`[faapi] injector "${name}" must be a function: ${filePath}`);
|
|
5906
5912
|
}
|
|
5907
5913
|
validInjectors[name] = injector;
|
|
5908
5914
|
}
|
|
@@ -5917,10 +5923,16 @@ async function loadMergedMiddlewares(middlewarePaths) {
|
|
|
5917
5923
|
if (bundle === void 0) {
|
|
5918
5924
|
let loading = inFlight.get(absMwPath);
|
|
5919
5925
|
if (!loading) {
|
|
5920
|
-
loading = loadMiddlewaresFile(absMwPath).then(
|
|
5921
|
-
|
|
5922
|
-
|
|
5923
|
-
|
|
5926
|
+
loading = loadMiddlewaresFile(absMwPath).then(
|
|
5927
|
+
(result) => {
|
|
5928
|
+
setCachedMiddlewares(absMwPath, result);
|
|
5929
|
+
return result;
|
|
5930
|
+
},
|
|
5931
|
+
(err) => {
|
|
5932
|
+
inFlight.delete(absMwPath);
|
|
5933
|
+
throw err;
|
|
5934
|
+
}
|
|
5935
|
+
);
|
|
5924
5936
|
inFlight.set(absMwPath, loading);
|
|
5925
5937
|
}
|
|
5926
5938
|
bundle = await loading;
|
|
@@ -6515,7 +6527,8 @@ async function sendErrorResponse(err, meta, res, onError, ctx) {
|
|
|
6515
6527
|
if (onError && ctx) {
|
|
6516
6528
|
try {
|
|
6517
6529
|
await onError(err, ctx);
|
|
6518
|
-
} catch {
|
|
6530
|
+
} catch (hookErr) {
|
|
6531
|
+
console.error("[faapi] lifecycle onError hook threw:", hookErr);
|
|
6519
6532
|
}
|
|
6520
6533
|
}
|
|
6521
6534
|
}
|
|
@@ -6708,7 +6721,7 @@ async function loadPlugins(declarations, ctx, rootDir, dist) {
|
|
|
6708
6721
|
const upgradeWrappers = [];
|
|
6709
6722
|
const failures = [];
|
|
6710
6723
|
if (!declarations || declarations.length === 0) {
|
|
6711
|
-
return { handlerWrappers, upgradeWrappers
|
|
6724
|
+
return { handlerWrappers, upgradeWrappers };
|
|
6712
6725
|
}
|
|
6713
6726
|
const fullCtx = {
|
|
6714
6727
|
...ctx,
|
|
@@ -6735,7 +6748,7 @@ async function loadPlugins(declarations, ctx, rootDir, dist) {
|
|
|
6735
6748
|
}
|
|
6736
6749
|
if (enable === false) continue;
|
|
6737
6750
|
if (loaded.has(specifier)) {
|
|
6738
|
-
|
|
6751
|
+
failures.push({ specifier, reason: "duplicate plugin declaration" });
|
|
6739
6752
|
continue;
|
|
6740
6753
|
}
|
|
6741
6754
|
loaded.add(specifier);
|
|
@@ -6756,12 +6769,12 @@ async function loadPlugins(declarations, ctx, rootDir, dist) {
|
|
|
6756
6769
|
}
|
|
6757
6770
|
}
|
|
6758
6771
|
if (failures.length > 0) {
|
|
6759
|
-
|
|
6772
|
+
throw new Error(
|
|
6760
6773
|
`[faapi] ${failures.length} plugin(s) failed to load:
|
|
6761
6774
|
` + failures.map((f) => ` - ${f.specifier}: ${f.reason}`).join("\n")
|
|
6762
6775
|
);
|
|
6763
6776
|
}
|
|
6764
|
-
return { handlerWrappers, upgradeWrappers
|
|
6777
|
+
return { handlerWrappers, upgradeWrappers };
|
|
6765
6778
|
}
|
|
6766
6779
|
var TS_EXTS = /\.(ts|tsx|mts|cts)$/;
|
|
6767
6780
|
var JS_EXTS = /\.(js|mjs|cjs|jsx)$/;
|
|
@@ -7362,6 +7375,7 @@ function generateTaskSchemaFileSource(sources, resolveType, helpersImportPath) {
|
|
|
7362
7375
|
}
|
|
7363
7376
|
async function generateTaskArtifacts(manifests, rootDir, dist) {
|
|
7364
7377
|
const metadata = [];
|
|
7378
|
+
const declarationErrors = [];
|
|
7365
7379
|
if (manifests.length > 0) {
|
|
7366
7380
|
const programByFile = createPrograms(manifests.map((m) => path25.resolve(rootDir, m.filePath)));
|
|
7367
7381
|
for (const manifest of manifests) {
|
|
@@ -7370,11 +7384,28 @@ async function generateTaskArtifacts(manifests, rootDir, dist) {
|
|
|
7370
7384
|
name: manifest.name,
|
|
7371
7385
|
filePath: manifest.filePath
|
|
7372
7386
|
});
|
|
7373
|
-
if (result) {
|
|
7374
|
-
|
|
7387
|
+
if (!result) {
|
|
7388
|
+
declarationErrors.push(
|
|
7389
|
+
`"${manifest.name}" (${manifest.filePath}): no exported run function found`
|
|
7390
|
+
);
|
|
7391
|
+
continue;
|
|
7375
7392
|
}
|
|
7393
|
+
if (!result.inputTypeName) {
|
|
7394
|
+
declarationErrors.push(
|
|
7395
|
+
`"${manifest.name}" (${manifest.filePath}): run's first parameter has no type name \u2014 declare a Payload type (an empty interface for parameterless tasks), or explicitly opt out of validation with "type Payload = unknown"`
|
|
7396
|
+
);
|
|
7397
|
+
continue;
|
|
7398
|
+
}
|
|
7399
|
+
metadata.push({ ...result, taskName: manifest.name });
|
|
7376
7400
|
}
|
|
7377
7401
|
}
|
|
7402
|
+
if (declarationErrors.length > 0) {
|
|
7403
|
+
throw new Error(
|
|
7404
|
+
`[faapi] ${declarationErrors.length} task(s) missing a Payload type declaration \u2014 payload validation must not be silently skipped:
|
|
7405
|
+
${declarationErrors.join("\n ")}
|
|
7406
|
+
Every task must declare a payload type on run's first parameter; "type Payload = unknown" generates an always-passing schema (explicit no-validation opt-out).`
|
|
7407
|
+
);
|
|
7408
|
+
}
|
|
7378
7409
|
const serialized = serializeTasks(manifests, dist);
|
|
7379
7410
|
const tasksPath = path25.resolve(rootDir, dist, TASKS_FILE);
|
|
7380
7411
|
await writeTasksModule(serialized, tasksPath);
|
|
@@ -7652,9 +7683,13 @@ async function createAppBase(options) {
|
|
|
7652
7683
|
const taskQueue = createTaskQueue({
|
|
7653
7684
|
registry: registries.task,
|
|
7654
7685
|
rootDir,
|
|
7655
|
-
config,
|
|
7686
|
+
config: config ?? {},
|
|
7656
7687
|
resourcesDir,
|
|
7657
7688
|
registries: createTaskRegistriesView(registries),
|
|
7689
|
+
// 轻量补全通道:进程内路径经 store 惰性读取(插件注册后的活引用);
|
|
7690
|
+
// 隔离路径传 agent.llms 纯数据快照(worker 内重建)
|
|
7691
|
+
llm: registries.llm,
|
|
7692
|
+
llms: config?.agent?.llms,
|
|
7658
7693
|
driver: taskDriver,
|
|
7659
7694
|
onFailed: config?.task?.onFailed
|
|
7660
7695
|
});
|
|
@@ -7679,7 +7714,7 @@ async function createAppBase(options) {
|
|
|
7679
7714
|
dist,
|
|
7680
7715
|
cors: config?.cors ?? true,
|
|
7681
7716
|
onError: config?.lifecycle?.onError,
|
|
7682
|
-
config
|
|
7717
|
+
config,
|
|
7683
7718
|
wsRoutes,
|
|
7684
7719
|
middlewares: config?.middlewares,
|
|
7685
7720
|
injectors: config?.injectors,
|
|
@@ -7792,6 +7827,7 @@ async function createAppBase(options) {
|
|
|
7792
7827
|
registries.task.clear();
|
|
7793
7828
|
registries.agentHandle.clear();
|
|
7794
7829
|
registries.taskHandle.clear();
|
|
7830
|
+
registries.llm.clear();
|
|
7795
7831
|
setActiveResourcesDir(null);
|
|
7796
7832
|
if (!server.listening) {
|
|
7797
7833
|
app.server = null;
|