@lark-apaas/nestjs-capability 0.1.3 → 0.1.4-alpha.2

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/README.md CHANGED
@@ -310,15 +310,53 @@ interface CapabilityExecutor {
310
310
 
311
311
  ```typescript
312
312
  interface PluginLoaderService {
313
- // 加载插件并创建实例(带缓存)
314
- loadPlugin(pluginID: string): PluginInstance;
313
+ // 加载插件并创建实例
314
+ // - 对于 refType: "config" 的插件,每次调用都创建新实例
315
+ // - 对于 refType: "action" 的插件,使用缓存
316
+ loadPlugin(pluginID: string, config?: unknown): PluginInstance;
315
317
 
316
318
  // 检查插件是否已安装
317
319
  isPluginInstalled(pluginID: string): boolean;
318
320
 
319
- // 清除插件缓存
321
+ // 读取插件的 manifest.json
322
+ getManifest(pluginID: string): PluginManifest | null;
323
+
324
+ // 检查插件是否需要 config(form.refType === 'config')
325
+ isConfigRequired(pluginID: string): boolean;
326
+
327
+ // 清除插件缓存(包括 manifest 缓存)
320
328
  clearCache(pluginID?: string): void;
321
329
  }
330
+
331
+ interface PluginManifest {
332
+ name: string;
333
+ form?: {
334
+ refType?: 'config' | 'action';
335
+ schema?: unknown;
336
+ };
337
+ [key: string]: unknown;
338
+ }
339
+ ```
340
+
341
+ #### form.refType 说明
342
+
343
+ 插件的 `manifest.json` 中可以通过 `form.refType` 字段指定 `formValue` 的消费方式:
344
+
345
+ | refType | 行为 | 缓存策略 |
346
+ |---------|------|----------|
347
+ | `"config"` | `formValue` 作为 config 传给 `create()` 函数 | 不缓存实例,每次重新创建 |
348
+ | `"action"` 或不设置 | `formValue` 通过模板引擎解析后作为 action 参数 | 缓存实例 |
349
+
350
+ **manifest.json 示例:**
351
+
352
+ ```json
353
+ {
354
+ "name": "@test/feishu-bitable",
355
+ "form": {
356
+ "refType": "config",
357
+ "schema": { ... }
358
+ }
359
+ }
322
360
  ```
323
361
 
324
362
  ### TemplateEngineService
package/dist/index.cjs CHANGED
@@ -114,13 +114,13 @@ var TemplateEngineService = class {
114
114
  resolveString(template, input, paramsSchema) {
115
115
  const wholeMatch = template.match(this.WHOLE_STRING_EXPR_REGEX);
116
116
  if (wholeMatch) {
117
- const path2 = wholeMatch[1];
118
- const value = this.getValueByPath(input, path2);
117
+ const path3 = wholeMatch[1];
118
+ const value = this.getValueByPath(input, path3);
119
119
  if (value !== void 0) {
120
120
  return value;
121
121
  }
122
122
  if (paramsSchema) {
123
- const defaultValue = this.getDefaultValueForPath(path2, paramsSchema);
123
+ const defaultValue = this.getDefaultValueForPath(path3, paramsSchema);
124
124
  if (defaultValue !== void 0) {
125
125
  return defaultValue;
126
126
  }
@@ -132,8 +132,8 @@ var TemplateEngineService = class {
132
132
  return template;
133
133
  }
134
134
  this.EXPR_REGEX.lastIndex = 0;
135
- const result = template.replace(this.EXPR_REGEX, (match, path2) => {
136
- const value = this.getValueByPath(input, path2);
135
+ const result = template.replace(this.EXPR_REGEX, (match, path3) => {
136
+ const value = this.getValueByPath(input, path3);
137
137
  if (value === void 0) {
138
138
  return match;
139
139
  }
@@ -148,8 +148,8 @@ var TemplateEngineService = class {
148
148
  }
149
149
  return result;
150
150
  }
151
- getValueByPath(obj, path2) {
152
- const keys = path2.split(".");
151
+ getValueByPath(obj, path3) {
152
+ const keys = path3.split(".");
153
153
  let current = obj;
154
154
  for (const key of keys) {
155
155
  if (current === null || current === void 0) {
@@ -165,8 +165,8 @@ var TemplateEngineService = class {
165
165
  * @param schema - JSON Schema
166
166
  * @returns 默认值,如果无法确定则返回 undefined
167
167
  */
168
- getDefaultValueForPath(path2, schema) {
169
- const fieldSchema = this.getSchemaForPath(path2, schema);
168
+ getDefaultValueForPath(path3, schema) {
169
+ const fieldSchema = this.getSchemaForPath(path3, schema);
170
170
  if (!fieldSchema) {
171
171
  return void 0;
172
172
  }
@@ -178,8 +178,8 @@ var TemplateEngineService = class {
178
178
  * @param schema - 根 JSON Schema
179
179
  * @returns 路径对应的 schema,如果不存在则返回 undefined
180
180
  */
181
- getSchemaForPath(path2, schema) {
182
- const keys = path2.split(".");
181
+ getSchemaForPath(path3, schema) {
182
+ const keys = path3.split(".");
183
183
  let currentSchema = schema;
184
184
  for (const key of keys) {
185
185
  if (!currentSchema?.properties) {
@@ -228,6 +228,8 @@ TemplateEngineService = _ts_decorate([
228
228
  var import_common2 = require("@nestjs/common");
229
229
  var import_node_module = require("module");
230
230
  var import_node_url = require("url");
231
+ var fs = __toESM(require("fs"), 1);
232
+ var path = __toESM(require("path"), 1);
231
233
  function _ts_decorate2(decorators, target, key, desc) {
232
234
  var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
233
235
  if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
@@ -264,13 +266,75 @@ var PluginLoaderService = class _PluginLoaderService {
264
266
  pluginInstances = /* @__PURE__ */ new Map();
265
267
  /** 记录每个插件的加载版本(时间戳),用于 ESM 缓存绕过 */
266
268
  pluginVersions = /* @__PURE__ */ new Map();
267
- async loadPlugin(pluginKey) {
269
+ /** 缓存插件的 manifest.json */
270
+ manifestCache = /* @__PURE__ */ new Map();
271
+ /**
272
+ * 从入口路径向上查找包根目录(包含 package.json 的目录)
273
+ */
274
+ findPackageRoot(entryPath) {
275
+ let dir = path.dirname(entryPath);
276
+ while (dir !== path.dirname(dir)) {
277
+ if (fs.existsSync(path.join(dir, "package.json"))) {
278
+ return dir;
279
+ }
280
+ dir = path.dirname(dir);
281
+ }
282
+ return null;
283
+ }
284
+ /**
285
+ * 读取并缓存 manifest
286
+ */
287
+ getManifest(pluginKey) {
288
+ if (this.manifestCache.has(pluginKey)) {
289
+ return this.manifestCache.get(pluginKey) ?? null;
290
+ }
291
+ try {
292
+ const entryPath = require2.resolve(pluginKey);
293
+ const packageRoot = this.findPackageRoot(entryPath);
294
+ if (!packageRoot) {
295
+ this.manifestCache.set(pluginKey, null);
296
+ return null;
297
+ }
298
+ const manifestPath = path.join(packageRoot, "manifest.json");
299
+ if (!fs.existsSync(manifestPath)) {
300
+ this.manifestCache.set(pluginKey, null);
301
+ return null;
302
+ }
303
+ const content = fs.readFileSync(manifestPath, "utf-8");
304
+ const manifest = JSON.parse(content);
305
+ this.manifestCache.set(pluginKey, manifest);
306
+ return manifest;
307
+ } catch {
308
+ this.manifestCache.set(pluginKey, null);
309
+ return null;
310
+ }
311
+ }
312
+ /**
313
+ * 检查插件是否需要 config(form.refType === 'config')
314
+ */
315
+ isConfigRequired(pluginKey) {
316
+ const manifest = this.getManifest(pluginKey);
317
+ return manifest?.form?.refType === "config";
318
+ }
319
+ async loadPlugin(pluginKey, config) {
320
+ const isConfigRequired = this.isConfigRequired(pluginKey);
321
+ if (isConfigRequired) {
322
+ return this.createPluginInstance(pluginKey, config);
323
+ }
268
324
  const cached = this.pluginInstances.get(pluginKey);
269
325
  if (cached) {
270
326
  this.logger.debug(`Using cached plugin instance: ${pluginKey}`);
271
327
  return cached;
272
328
  }
273
- this.logger.log(`Loading plugin: ${pluginKey}`);
329
+ const instance = await this.createPluginInstance(pluginKey);
330
+ this.pluginInstances.set(pluginKey, instance);
331
+ return instance;
332
+ }
333
+ /**
334
+ * 创建插件实例(内部方法)
335
+ */
336
+ async createPluginInstance(pluginKey, config) {
337
+ this.logger.log(`Loading plugin: ${pluginKey}${config !== void 0 ? " (with config)" : ""}`);
274
338
  try {
275
339
  const resolvedPath = require2.resolve(pluginKey);
276
340
  const version = this.pluginVersions.get(pluginKey) ?? 0;
@@ -281,8 +345,7 @@ var PluginLoaderService = class _PluginLoaderService {
281
345
  if (typeof pluginPackage.create !== "function") {
282
346
  throw new PluginLoadError(pluginKey, "Plugin does not export create() function");
283
347
  }
284
- const instance = pluginPackage.create();
285
- this.pluginInstances.set(pluginKey, instance);
348
+ const instance = pluginPackage.create(config);
286
349
  this.logger.log(`Plugin loaded successfully: ${pluginKey}`);
287
350
  return instance;
288
351
  } catch (error) {
@@ -303,6 +366,7 @@ var PluginLoaderService = class _PluginLoaderService {
303
366
  /**
304
367
  * 清除插件缓存
305
368
  * - 清除应用层 pluginInstances 缓存
369
+ * - 清除 manifest 缓存
306
370
  * - 清除 Node.js CJS 模块缓存(require.cache)
307
371
  * - 更新版本号,下次 import 时绕过 ESM 缓存
308
372
  * @param pluginKey - 插件标识,不传则清除所有
@@ -310,6 +374,7 @@ var PluginLoaderService = class _PluginLoaderService {
310
374
  clearCache(pluginKey) {
311
375
  if (pluginKey) {
312
376
  this.pluginInstances.delete(pluginKey);
377
+ this.manifestCache.delete(pluginKey);
313
378
  this.clearNodeModuleCache(pluginKey);
314
379
  this.pluginVersions.set(pluginKey, Date.now());
315
380
  this.logger.log(`Cleared cache for plugin: ${pluginKey}`);
@@ -319,6 +384,7 @@ var PluginLoaderService = class _PluginLoaderService {
319
384
  this.pluginVersions.set(key, Date.now());
320
385
  }
321
386
  this.pluginInstances.clear();
387
+ this.manifestCache.clear();
322
388
  this.logger.log("Cleared all plugin caches");
323
389
  }
324
390
  }
@@ -364,8 +430,8 @@ PluginLoaderService = _ts_decorate2([
364
430
  // src/services/capability.service.ts
365
431
  var import_common3 = require("@nestjs/common");
366
432
  var import_nestjs_common = require("@lark-apaas/nestjs-common");
367
- var fs = __toESM(require("fs"), 1);
368
- var path = __toESM(require("path"), 1);
433
+ var fs2 = __toESM(require("fs"), 1);
434
+ var path2 = __toESM(require("path"), 1);
369
435
  var chokidar = __toESM(require("chokidar"), 1);
370
436
 
371
437
  // src/utils/log-utils.ts
@@ -488,7 +554,7 @@ var CapabilityService = class _CapabilityService {
488
554
  this.pluginLoaderService = pluginLoaderService;
489
555
  this.templateEngineService = templateEngineService;
490
556
  const isDev = process.env.NODE_ENV === "development";
491
- this.capabilitiesDir = path.join(process.cwd(), isDev ? "server/capabilities" : "capabilities");
557
+ this.capabilitiesDir = path2.join(process.cwd(), isDev ? "server/capabilities" : "capabilities");
492
558
  }
493
559
  /**
494
560
  * 设置模块配置
@@ -519,11 +585,11 @@ var CapabilityService = class _CapabilityService {
519
585
  if (this.fileWatcher) {
520
586
  return;
521
587
  }
522
- if (!fs.existsSync(this.capabilitiesDir)) {
588
+ if (!fs2.existsSync(this.capabilitiesDir)) {
523
589
  this.logger.warn(`Cannot start watching: directory not found: ${this.capabilitiesDir}`);
524
590
  return;
525
591
  }
526
- const pattern = path.join(this.capabilitiesDir, "*.json");
592
+ const pattern = path2.join(this.capabilitiesDir, "*.json");
527
593
  const debounce = this.options.watchDebounce ?? 300;
528
594
  this.logger.log(`Starting file watcher: ${pattern}`);
529
595
  this.fileWatcher = chokidar.watch(pattern, {
@@ -588,7 +654,7 @@ var CapabilityService = class _CapabilityService {
588
654
  * 从文件加载单个能力配置
589
655
  */
590
656
  loadCapabilityFromFile(filePath) {
591
- const content = fs.readFileSync(filePath, "utf-8");
657
+ const content = fs2.readFileSync(filePath, "utf-8");
592
658
  const config = JSON.parse(content);
593
659
  if (!config.id) {
594
660
  this.logger.warn(`Skipping capability without id: ${filePath}`);
@@ -626,14 +692,14 @@ var CapabilityService = class _CapabilityService {
626
692
  // ==================== 原有方法 ====================
627
693
  async loadCapabilities() {
628
694
  this.logger.log(`Loading capabilities from ${this.capabilitiesDir}`);
629
- if (!fs.existsSync(this.capabilitiesDir)) {
695
+ if (!fs2.existsSync(this.capabilitiesDir)) {
630
696
  this.logger.warn(`Capabilities directory not found: ${this.capabilitiesDir}`);
631
697
  return;
632
698
  }
633
- const files = fs.readdirSync(this.capabilitiesDir).filter((f) => f.endsWith(".json"));
699
+ const files = fs2.readdirSync(this.capabilitiesDir).filter((f) => f.endsWith(".json"));
634
700
  for (const file of files) {
635
701
  try {
636
- const filePath = path.join(this.capabilitiesDir, file);
702
+ const filePath = path2.join(this.capabilitiesDir, file);
637
703
  this.loadCapabilityFromFile(filePath);
638
704
  } catch (error) {
639
705
  this.logger.error(`Failed to load capability from ${file}:`, error);
@@ -678,10 +744,30 @@ var CapabilityService = class _CapabilityService {
678
744
  };
679
745
  }
680
746
  /**
747
+ * 加载插件实例并解析参数
748
+ * 根据 manifest 的 form.refType 决定 formValue 的消费方式
749
+ */
750
+ async loadPluginAndResolveParams(config, input) {
751
+ const isConfigRequired = this.pluginLoaderService.isConfigRequired(config.pluginKey);
752
+ if (isConfigRequired) {
753
+ const pluginInstance2 = await this.pluginLoaderService.loadPlugin(config.pluginKey, config.formValue);
754
+ return {
755
+ pluginInstance: pluginInstance2,
756
+ resolvedParams: input
757
+ };
758
+ }
759
+ const pluginInstance = await this.pluginLoaderService.loadPlugin(config.pluginKey);
760
+ const resolvedParams = config.formValue ? this.templateEngineService.resolve(config.formValue, input, config.paramsSchema) : input;
761
+ return {
762
+ pluginInstance,
763
+ resolvedParams
764
+ };
765
+ }
766
+ /**
681
767
  * 检查 action 是否为流式
682
768
  */
683
769
  async checkIsStream(config, actionName) {
684
- const pluginInstance = await this.pluginLoaderService.loadPlugin(config.pluginKey);
770
+ const { pluginInstance } = await this.loadPluginAndResolveParams(config, {});
685
771
  if (!pluginInstance.hasAction(actionName)) {
686
772
  throw new ActionNotFoundError(config.pluginKey, actionName);
687
773
  }
@@ -700,11 +786,10 @@ var CapabilityService = class _CapabilityService {
700
786
  action: actionName
701
787
  };
702
788
  try {
703
- const pluginInstance = await this.pluginLoaderService.loadPlugin(config.pluginKey);
789
+ const { pluginInstance, resolvedParams } = await this.loadPluginAndResolveParams(config, input);
704
790
  if (!pluginInstance.hasAction(actionName)) {
705
791
  throw new ActionNotFoundError(config.pluginKey, actionName);
706
792
  }
707
- const resolvedParams = config.formValue ? this.templateEngineService.resolve(config.formValue, input, config.paramsSchema) : input;
708
793
  const context = this.buildActionContext(contextOverride);
709
794
  const isStream = pluginInstance.isStreamAction?.(actionName) ?? false;
710
795
  this.logger.log("Executing capability (call)", {
@@ -751,11 +836,10 @@ var CapabilityService = class _CapabilityService {
751
836
  };
752
837
  const chunks = [];
753
838
  try {
754
- const pluginInstance = await this.pluginLoaderService.loadPlugin(config.pluginKey);
839
+ const { pluginInstance, resolvedParams } = await this.loadPluginAndResolveParams(config, input);
755
840
  if (!pluginInstance.hasAction(actionName)) {
756
841
  throw new ActionNotFoundError(config.pluginKey, actionName);
757
842
  }
758
- const resolvedParams = config.formValue ? this.templateEngineService.resolve(config.formValue, input, config.paramsSchema) : input;
759
843
  const context = this.buildActionContext(contextOverride);
760
844
  const isStream = pluginInstance.isStreamAction?.(actionName) ?? false;
761
845
  this.logger.log("Executing capability (stream)", {
@@ -802,11 +886,10 @@ var CapabilityService = class _CapabilityService {
802
886
  };
803
887
  const chunks = [];
804
888
  try {
805
- const pluginInstance = await this.pluginLoaderService.loadPlugin(config.pluginKey);
889
+ const { pluginInstance, resolvedParams } = await this.loadPluginAndResolveParams(config, input);
806
890
  if (!pluginInstance.hasAction(actionName)) {
807
891
  throw new ActionNotFoundError(config.pluginKey, actionName);
808
892
  }
809
- const resolvedParams = config.formValue ? this.templateEngineService.resolve(config.formValue, input, config.paramsSchema) : input;
810
893
  const context = this.buildActionContext(contextOverride);
811
894
  const isStream = pluginInstance.isStreamAction?.(actionName) ?? false;
812
895
  this.logger.log("Executing capability (streamWithEvents)", {
@@ -982,11 +1065,12 @@ var DebugController = class _DebugController {
982
1065
  * 获取 action 名称
983
1066
  * 优先使用传入的 action,否则使用插件第一个 action
984
1067
  */
985
- async getActionName(pluginKey, bodyAction) {
1068
+ async getActionName(pluginKey, formValue, bodyAction) {
986
1069
  if (bodyAction) {
987
1070
  return bodyAction;
988
1071
  }
989
- const pluginInstance = await this.pluginLoaderService.loadPlugin(pluginKey);
1072
+ const isConfigRequired = this.pluginLoaderService.isConfigRequired(pluginKey);
1073
+ const pluginInstance = isConfigRequired ? await this.pluginLoaderService.loadPlugin(pluginKey, formValue) : await this.pluginLoaderService.loadPlugin(pluginKey);
990
1074
  const actions = pluginInstance.listActions();
991
1075
  if (actions.length === 0) {
992
1076
  throw new Error(`Plugin ${pluginKey} has no actions`);
@@ -998,7 +1082,7 @@ var DebugController = class _DebugController {
998
1082
  const params = body.params ?? {};
999
1083
  try {
1000
1084
  const config = this.getCapabilityConfig(capabilityId, body.capability);
1001
- const action = await this.getActionName(config.pluginKey, body.action);
1085
+ const action = await this.getActionName(config.pluginKey, config.formValue, body.action);
1002
1086
  const resolvedParams = this.templateEngineService.resolve(config.formValue, params, config.paramsSchema);
1003
1087
  const result = await this.capabilityService.loadWithConfig(config).call(action, params, {
1004
1088
  isDebug: true
@@ -1055,7 +1139,7 @@ var DebugController = class _DebugController {
1055
1139
  res.setHeader("Connection", "keep-alive");
1056
1140
  try {
1057
1141
  const config = this.getCapabilityConfig(capabilityId, body.capability);
1058
- const action = await this.getActionName(config.pluginKey, body.action);
1142
+ const action = await this.getActionName(config.pluginKey, config.formValue, body.action);
1059
1143
  const loggerContext = {
1060
1144
  capability_id: config.id,
1061
1145
  plugin_key: config.pluginKey,