@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 CHANGED
@@ -767,38 +767,30 @@ function setCachedMiddlewares(absPath, bundle) {
767
767
  middlewareCache.set(absPath, bundle);
768
768
  }
769
769
  async function loadMiddlewaresFile(filePath) {
770
- let module;
771
- try {
772
- module = await importWithCacheBust(filePath);
773
- } catch (err) {
774
- console.error(
775
- `[faapi] Failed to load middlewares from ${filePath}:`,
776
- err instanceof Error ? err.stack ?? err.message : err
777
- );
778
- return { middlewares: [], injectors: {} };
779
- }
770
+ const module = await importWithCacheBust(filePath);
780
771
  const middlewares = module.default ?? module.middlewares ?? [];
781
772
  if (!Array.isArray(middlewares)) {
782
- console.warn(`[faapi] middlewares.ts \u5E94\u5BFC\u51FA\u6570\u7EC4\uFF0C\u5DF2\u5FFD\u7565: ${filePath}`);
783
- return { middlewares: [], injectors: {} };
773
+ throw new TypeError(
774
+ `[faapi] middlewares.ts must export an array (default or named "middlewares"): ${filePath}`
775
+ );
784
776
  }
785
- const validMiddlewares = middlewares.filter((m) => {
777
+ const validMiddlewares = [];
778
+ for (const m of middlewares) {
786
779
  if (typeof m !== "function") {
787
- console.warn(`[faapi] \u65E0\u6548\u7684\u4E2D\u95F4\u4EF6\u9879\uFF08\u5E94\u4E3A\u51FD\u6570\uFF09\uFF0C\u5DF2\u5FFD\u7565: ${typeof m}`);
788
- return false;
780
+ throw new TypeError(
781
+ `[faapi] every middleware must be a function, got ${typeof m}: ${filePath}`
782
+ );
789
783
  }
790
- return true;
791
- });
784
+ validMiddlewares.push(m);
785
+ }
792
786
  const injectors = module.injectors ?? {};
793
787
  if (typeof injectors !== "object" || injectors === null) {
794
- console.warn(`[faapi] injectors \u5E94\u5BFC\u51FA\u5BF9\u8C61\uFF0C\u5DF2\u5FFD\u7565: ${filePath}`);
795
- return { middlewares: validMiddlewares, injectors: {} };
788
+ throw new TypeError(`[faapi] "injectors" export must be an object: ${filePath}`);
796
789
  }
797
790
  const validInjectors = {};
798
791
  for (const [name, injector] of Object.entries(injectors)) {
799
792
  if (typeof injector !== "function") {
800
- console.warn(`[faapi] \u6CE8\u5165\u5668 ${name} \u5E94\u4E3A\u51FD\u6570\uFF0C\u5DF2\u5FFD\u7565`);
801
- continue;
793
+ throw new TypeError(`[faapi] injector "${name}" must be a function: ${filePath}`);
802
794
  }
803
795
  validInjectors[name] = injector;
804
796
  }
@@ -813,10 +805,16 @@ async function loadMergedMiddlewares(middlewarePaths) {
813
805
  if (bundle === void 0) {
814
806
  let loading = inFlight.get(absMwPath);
815
807
  if (!loading) {
816
- loading = loadMiddlewaresFile(absMwPath).then((result) => {
817
- setCachedMiddlewares(absMwPath, result);
818
- return result;
819
- });
808
+ loading = loadMiddlewaresFile(absMwPath).then(
809
+ (result) => {
810
+ setCachedMiddlewares(absMwPath, result);
811
+ return result;
812
+ },
813
+ (err) => {
814
+ inFlight.delete(absMwPath);
815
+ throw err;
816
+ }
817
+ );
820
818
  inFlight.set(absMwPath, loading);
821
819
  }
822
820
  bundle = await loading;
@@ -2550,6 +2548,8 @@ var init_resolveInjection = __esm({
2550
2548
  // 任务子系统:TaskClient(入队/查询)
2551
2549
  log: "log",
2552
2550
  // 请求级日志器(ctx.log 同一实例:scope http,自动带 requestId 等字段)
2551
+ llm: "llm",
2552
+ // 轻量 LLM 补全通道(@faapi/agent 插件注册到 registries.llm;未加载时 undefined)
2553
2553
  // raw 系:恒原始输入(无 schema、无校验、管线永不转换),与 query/params/body
2554
2554
  // 的「校验转换后」口径二分互补
2555
2555
  rawQuery: "rawQuery",
@@ -3627,25 +3627,27 @@ function detectIgnoredMetaFields(source, strictHit) {
3627
3627
  for (const line of source.split("\n")) {
3628
3628
  const trimmed = line.trim();
3629
3629
  if (trimmed.startsWith("//")) continue;
3630
+ if (trimmed.startsWith("*") || trimmed.startsWith("/*") || trimmed.endsWith("*/")) continue;
3630
3631
  for (const field of ALL_META_FIELDS) {
3631
3632
  if (new RegExp(`\\b${field}\\s*:`).test(trimmed)) declared.add(field);
3632
3633
  }
3633
3634
  }
3634
3635
  return ALL_META_FIELDS.filter((field) => declared.has(field) && !strictHit[field]);
3635
3636
  }
3636
- function warnIgnoredMetaFields(name, fields) {
3637
- for (const field of fields) {
3637
+ function throwIgnoredMetaFields(name, fields) {
3638
+ if (fields.length === 0) return;
3639
+ const details = fields.map((field) => {
3638
3640
  if (STRING_META_FIELDS.includes(field)) {
3639
- console.warn(
3640
- `[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.`
3641
- );
3642
- } else {
3643
- const consequence = field === "timeoutMs" ? " and the task will run in-process with no timeout" : "";
3644
- console.warn(
3645
- `[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.`
3646
- );
3641
+ 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.`;
3647
3642
  }
3648
- }
3643
+ 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";
3644
+ 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.`;
3645
+ });
3646
+ throw new Error(
3647
+ `[faapi] Task "${name}" declares meta field(s) with non-literal values:
3648
+ ${details.join("\n ")}
3649
+ 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.`
3650
+ );
3649
3651
  }
3650
3652
  function parseNumericLiteral(raw) {
3651
3653
  return Number(raw.replace(/_/g, ""));
@@ -3713,7 +3715,7 @@ async function scanTasks(rootDir, patterns) {
3713
3715
  timeoutMs: timeoutMs !== void 0,
3714
3716
  graceMs: graceMs !== void 0
3715
3717
  };
3716
- warnIgnoredMetaFields(name, detectIgnoredMetaFields(source, strictHit));
3718
+ throwIgnoredMetaFields(name, detectIgnoredMetaFields(source, strictHit));
3717
3719
  tasks.push(manifest);
3718
3720
  }
3719
3721
  return tasks.sort((a, b) => a.name.localeCompare(b.name));
@@ -3828,6 +3830,7 @@ function generateTaskSchemaFileSource(sources, resolveType, helpersImportPath) {
3828
3830
  }
3829
3831
  async function generateTaskArtifacts(manifests, rootDir, dist) {
3830
3832
  const metadata = [];
3833
+ const declarationErrors = [];
3831
3834
  if (manifests.length > 0) {
3832
3835
  const programByFile = createPrograms(manifests.map((m) => path17.resolve(rootDir, m.filePath)));
3833
3836
  for (const manifest of manifests) {
@@ -3836,11 +3839,28 @@ async function generateTaskArtifacts(manifests, rootDir, dist) {
3836
3839
  name: manifest.name,
3837
3840
  filePath: manifest.filePath
3838
3841
  });
3839
- if (result) {
3840
- metadata.push({ ...result, taskName: manifest.name });
3842
+ if (!result) {
3843
+ declarationErrors.push(
3844
+ `"${manifest.name}" (${manifest.filePath}): no exported run function found`
3845
+ );
3846
+ continue;
3841
3847
  }
3848
+ if (!result.inputTypeName) {
3849
+ declarationErrors.push(
3850
+ `"${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"`
3851
+ );
3852
+ continue;
3853
+ }
3854
+ metadata.push({ ...result, taskName: manifest.name });
3842
3855
  }
3843
3856
  }
3857
+ if (declarationErrors.length > 0) {
3858
+ throw new Error(
3859
+ `[faapi] ${declarationErrors.length} task(s) missing a Payload type declaration \u2014 payload validation must not be silently skipped:
3860
+ ${declarationErrors.join("\n ")}
3861
+ 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).`
3862
+ );
3863
+ }
3844
3864
  const serialized = serializeTasks(manifests, dist);
3845
3865
  const tasksPath = path17.resolve(rootDir, dist, TASKS_FILE);
3846
3866
  await writeTasksModule(serialized, tasksPath);
@@ -4021,11 +4041,7 @@ async function ensureMiddlewaresCompiled(middlewarePaths, rootDir) {
4021
4041
  if (!dist) return;
4022
4042
  for (const mwPath of middlewarePaths) {
4023
4043
  const sourcePath = prodPathToSourcePath(mwPath, rootDir, dist);
4024
- try {
4025
- await ensureCompiled(sourcePath, rootDir, dist);
4026
- } catch (err) {
4027
- console.error(`[faapi] Failed to compile middleware source ${sourcePath}:`, err);
4028
- }
4044
+ await ensureCompiled(sourcePath, rootDir, dist);
4029
4045
  }
4030
4046
  }
4031
4047
  function prodSourcePathToProductPath(sourceAbsPath, rootDir, dist) {
@@ -6942,17 +6958,14 @@ function formatEntry(entry) {
6942
6958
  const plain = Object.fromEntries(
6943
6959
  Object.entries(entry.fields).map(([k, v]) => [k, serializeFieldValue(v)])
6944
6960
  );
6945
- try {
6946
- line += ` ${JSON.stringify(plain)}`;
6947
- } catch (err) {
6948
- line += ` [unserializable fields: ${err instanceof Error ? err.message : String(err)}]`;
6949
- }
6961
+ line += ` ${stringifyJson(plain)}`;
6950
6962
  }
6951
6963
  return line;
6952
6964
  }
6953
6965
  var init_formatEntry = __esm({
6954
6966
  "src/logger/formatEntry.ts"() {
6955
6967
  "use strict";
6968
+ init_stringifyJson();
6956
6969
  }
6957
6970
  });
6958
6971
 
@@ -6968,7 +6981,14 @@ function createFileLogSink(options) {
6968
6981
  };
6969
6982
  const openStream = (name) => {
6970
6983
  const stream = fs18.createWriteStream(path24.join(options.dir, `${name}.log`), { flags: "a" });
6971
- stream.on("error", () => {
6984
+ let streamErrorLogged = false;
6985
+ stream.on("error", (err) => {
6986
+ if (streamErrorLogged) return;
6987
+ streamErrorLogged = true;
6988
+ console.error(
6989
+ `[faapi] log file stream "${name}.log" failed \u2014 file logging on this stream stopped (subsequent errors suppressed). Check disk space / permissions for ${options.dir}:`,
6990
+ err
6991
+ );
6972
6992
  });
6973
6993
  streams.set(name, stream);
6974
6994
  };
@@ -7798,6 +7818,20 @@ function createAgentHandleStore() {
7798
7818
  }
7799
7819
  };
7800
7820
  }
7821
+ function createLlmChannelStore() {
7822
+ let current = null;
7823
+ return {
7824
+ register(channel) {
7825
+ current = channel;
7826
+ },
7827
+ get() {
7828
+ return current ?? void 0;
7829
+ },
7830
+ clear() {
7831
+ current = null;
7832
+ }
7833
+ };
7834
+ }
7801
7835
  function createTaskHandleStore() {
7802
7836
  let currentFactory = null;
7803
7837
  return {
@@ -7854,7 +7888,8 @@ function createAppRegistries() {
7854
7888
  const task = createTaskRegistry();
7855
7889
  const agentHandle = createAgentHandleStore();
7856
7890
  const taskHandle = createTaskHandleStore();
7857
- return { tool, agent, skill, task, agentHandle, taskHandle };
7891
+ const llm = createLlmChannelStore();
7892
+ return { tool, agent, skill, task, agentHandle, taskHandle, llm };
7858
7893
  }
7859
7894
  var defaultRegistries;
7860
7895
  var init_registries = __esm({
@@ -7938,6 +7973,10 @@ function getBuiltinInjectionValue(type, ctx, body) {
7938
7973
  // 任务子系统:注入 TaskClient(入队/查询);未注册工厂(无 app 编排)时 undefined
7939
7974
  case "tasks":
7940
7975
  return ctx.registries ? ctx.registries.taskHandle.get(ctx) : void 0;
7976
+ // 轻量 LLM 补全通道:@faapi/agent 插件注册到 registries.llm;与请求上下文无关,
7977
+ // get 无参。插件未加载(或编程式直调 ctx 无 registries)时 undefined
7978
+ case "llm":
7979
+ return ctx.registries ? ctx.registries.llm.get() : void 0;
7941
7980
  // 请求级日志器:与 ctx.log 同一实例(scope http,自动带 requestId/method/path 字段)
7942
7981
  case "log":
7943
7982
  return ctx.log;
@@ -9296,7 +9335,8 @@ async function sendErrorResponse(err, meta, res, onError, ctx) {
9296
9335
  if (onError && ctx) {
9297
9336
  try {
9298
9337
  await onError(err, ctx);
9299
- } catch {
9338
+ } catch (hookErr) {
9339
+ console.error("[faapi] lifecycle onError hook threw:", hookErr);
9300
9340
  }
9301
9341
  }
9302
9342
  }
@@ -9440,19 +9480,6 @@ function resolveWorkerEntryUrl() {
9440
9480
  workerEntryUrl = existsSync3(fileURLToPath(js)) ? js : new URL("./workerEntry.ts", import.meta.url);
9441
9481
  return workerEntryUrl;
9442
9482
  }
9443
- function safeConfig(config) {
9444
- if (config === void 0 || config === null) return config;
9445
- try {
9446
- structuredClone(config);
9447
- return config;
9448
- } catch {
9449
- }
9450
- try {
9451
- return JSON.parse(JSON.stringify(config));
9452
- } catch {
9453
- return void 0;
9454
- }
9455
- }
9456
9483
  function reviveError(payload) {
9457
9484
  const err = new Error(payload?.message ?? "task worker error");
9458
9485
  if (payload?.name !== void 0) err.name = payload.name;
@@ -9567,17 +9594,15 @@ async function runTaskInWorker(options) {
9567
9594
  type: "run",
9568
9595
  payload,
9569
9596
  taskCtx: {
9570
- config: safeConfig(taskCtx.config),
9571
9597
  job: taskCtx.job
9572
9598
  },
9573
9599
  registries: options.registries,
9600
+ llms: options.llms,
9574
9601
  log: options.log
9575
9602
  });
9576
9603
  } catch (err) {
9577
9604
  finish(
9578
- () => reject(
9579
- new Error(`Task "${taskCtx.job.name}" payload/config is not cloneable: ${String(err)}`)
9580
- )
9605
+ () => reject(new Error(`Task "${taskCtx.job.name}" payload is not cloneable: ${String(err)}`))
9581
9606
  );
9582
9607
  }
9583
9608
  });
@@ -9628,13 +9653,14 @@ function createTaskQueue(deps) {
9628
9653
  const loadTaskModule = deps.loadTaskModule ?? (async (filePath) => await import(filePath));
9629
9654
  const loadPayloadSchema = deps.loadPayloadSchema ?? (async (filePath) => {
9630
9655
  const zodPath = path28.join(path28.dirname(filePath), "zod.js");
9631
- try {
9632
- const mod = await import(zodPath);
9633
- const schemaKey = Object.keys(mod).find((k) => k.endsWith("Schema"));
9634
- return schemaKey ? mod[schemaKey] : void 0;
9635
- } catch {
9636
- return void 0;
9656
+ const mod = await import(zodPath);
9657
+ const schemaKey = Object.keys(mod).find((k) => k.endsWith("Schema"));
9658
+ if (!schemaKey) {
9659
+ throw new Error(
9660
+ `[faapi] Task payload schema artifact has no *Schema export: ${zodPath} \u2014 re-run "faapi dev" / "faapi build" to regenerate task artifacts.`
9661
+ );
9637
9662
  }
9663
+ return mod[schemaKey];
9638
9664
  });
9639
9665
  function assertKnownTask(name) {
9640
9666
  const meta = registry.get(name);
@@ -9653,14 +9679,13 @@ function createTaskQueue(deps) {
9653
9679
  async function validatePayload(name, payload) {
9654
9680
  if (!schemaCache.has(name)) {
9655
9681
  const meta = registry.get(name);
9656
- schemaCache.set(
9657
- name,
9658
- await loadPayloadSchema(path28.resolve(rootDir, meta.filePath)).catch(() => void 0)
9659
- );
9682
+ schemaCache.set(name, await loadPayloadSchema(path28.resolve(rootDir, meta.filePath)));
9660
9683
  }
9661
9684
  const schema = schemaCache.get(name);
9662
9685
  if (!schema || typeof schema.safeParse !== "function") {
9663
- return payload;
9686
+ throw new Error(
9687
+ `[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.`
9688
+ );
9664
9689
  }
9665
9690
  const result = schema.safeParse(payload);
9666
9691
  if (!result.success) {
@@ -9694,11 +9719,15 @@ function createTaskQueue(deps) {
9694
9719
  taskModulePath: path28.resolve(rootDir, meta.filePath),
9695
9720
  payload: job.payload,
9696
9721
  taskCtx: {
9697
- config: deps.config,
9722
+ // 隔离任务不传 config:worker 线程不接收进程配置(config 含函数字段
9723
+ // 不可结构化克隆,框架不做降级传递)——任务数据经 payload 显式传入
9698
9724
  resourcesDir: deps.resourcesDir,
9699
9725
  job: { id: job.id, name: job.name, attempt: job.attempt }
9700
9726
  },
9701
9727
  registries: snapshotRegistries(),
9728
+ // agent.llms 纯数据快照随 workerData 下发,worker 内动态加载 @faapi/agent
9729
+ // 重建轻量补全通道(taskCtx.llm)——llms 未配置时不传
9730
+ llms: deps.llms,
9702
9731
  // 日志配置随 workerData 下发(纯数据),worker 内联重建 taskCtx.log,
9703
9732
  // 条目回传宿主走统一管道(scope/fields 与进程内路径一致)
9704
9733
  log: {
@@ -9728,6 +9757,8 @@ function createTaskQueue(deps) {
9728
9757
  config: deps.config,
9729
9758
  job: { id: job.id, name: job.name, attempt: job.attempt },
9730
9759
  registries: registriesView,
9760
+ // 轻量补全通道惰性读取(插件晚于队列构造注册,执行时刻取值才可见)
9761
+ llm: deps.llm?.get(),
9731
9762
  log: createLogger(`task:${job.name}`, {
9732
9763
  fields: { jobId: job.id, task: job.name, attempt: job.attempt }
9733
9764
  }),
@@ -9756,7 +9787,8 @@ function createTaskQueue(deps) {
9756
9787
  cancelled,
9757
9788
  error: record.error
9758
9789
  })
9759
- ).catch(() => {
9790
+ ).catch((hookErr) => {
9791
+ console.error(`[faapi] task onFailed hook threw for "${job.name}":`, hookErr);
9760
9792
  });
9761
9793
  }
9762
9794
  throw err;
@@ -10014,7 +10046,7 @@ async function loadPlugins(declarations, ctx, rootDir, dist) {
10014
10046
  const upgradeWrappers = [];
10015
10047
  const failures = [];
10016
10048
  if (!declarations || declarations.length === 0) {
10017
- return { handlerWrappers, upgradeWrappers, failures };
10049
+ return { handlerWrappers, upgradeWrappers };
10018
10050
  }
10019
10051
  const fullCtx = {
10020
10052
  ...ctx,
@@ -10041,7 +10073,7 @@ async function loadPlugins(declarations, ctx, rootDir, dist) {
10041
10073
  }
10042
10074
  if (enable === false) continue;
10043
10075
  if (loaded.has(specifier)) {
10044
- console.warn(`! Plugin already loaded: ${specifier}, skipping`);
10076
+ failures.push({ specifier, reason: "duplicate plugin declaration" });
10045
10077
  continue;
10046
10078
  }
10047
10079
  loaded.add(specifier);
@@ -10062,12 +10094,12 @@ async function loadPlugins(declarations, ctx, rootDir, dist) {
10062
10094
  }
10063
10095
  }
10064
10096
  if (failures.length > 0) {
10065
- console.error(
10097
+ throw new Error(
10066
10098
  `[faapi] ${failures.length} plugin(s) failed to load:
10067
10099
  ` + failures.map((f) => ` - ${f.specifier}: ${f.reason}`).join("\n")
10068
10100
  );
10069
10101
  }
10070
- return { handlerWrappers, upgradeWrappers, failures };
10102
+ return { handlerWrappers, upgradeWrappers };
10071
10103
  }
10072
10104
  function resolveLocalPluginSource(specifier, baseDir) {
10073
10105
  const base = path29.isAbsolute(specifier) ? specifier : path29.resolve(baseDir, specifier);
@@ -10401,9 +10433,13 @@ async function createAppBase(options) {
10401
10433
  const taskQueue = createTaskQueue({
10402
10434
  registry: registries.task,
10403
10435
  rootDir,
10404
- config,
10436
+ config: config ?? {},
10405
10437
  resourcesDir,
10406
10438
  registries: createTaskRegistriesView(registries),
10439
+ // 轻量补全通道:进程内路径经 store 惰性读取(插件注册后的活引用);
10440
+ // 隔离路径传 agent.llms 纯数据快照(worker 内重建)
10441
+ llm: registries.llm,
10442
+ llms: config?.agent?.llms,
10407
10443
  driver: taskDriver,
10408
10444
  onFailed: config?.task?.onFailed
10409
10445
  });
@@ -10428,7 +10464,7 @@ async function createAppBase(options) {
10428
10464
  dist,
10429
10465
  cors: config?.cors ?? true,
10430
10466
  onError: config?.lifecycle?.onError,
10431
- config: config ?? void 0,
10467
+ config,
10432
10468
  wsRoutes,
10433
10469
  middlewares: config?.middlewares,
10434
10470
  injectors: config?.injectors,
@@ -10541,6 +10577,7 @@ async function createAppBase(options) {
10541
10577
  registries.task.clear();
10542
10578
  registries.agentHandle.clear();
10543
10579
  registries.taskHandle.clear();
10580
+ registries.llm.clear();
10544
10581
  setActiveResourcesDir(null);
10545
10582
  if (!server.listening) {
10546
10583
  app.server = null;