@lark-apaas/nestjs-capability 0.0.1-alpha.1 → 0.0.1-alpha.3

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
@@ -15,6 +15,7 @@ npm install @lark-apaas/nestjs-capability
15
15
  - 从 `server/capabilities/` 目录加载能力配置
16
16
  - 加载并实例化关联的插件
17
17
  - 多种调用方式(Node 调用、Debug 调用、前端调用)
18
+ - 流式调用支持(SSE)
18
19
  - 模板参数解析
19
20
 
20
21
  ## 快速开始
@@ -59,6 +60,36 @@ export class TaskService {
59
60
  }
60
61
  ```
61
62
 
63
+ ### 流式调用
64
+
65
+ 用于 LLM 对话等流式输出场景:
66
+
67
+ ```typescript
68
+ @Injectable()
69
+ export class ChatService {
70
+ constructor(private readonly capabilityService: CapabilityService) {}
71
+
72
+ async *chat(message: string): AsyncIterable<{ content: string }> {
73
+ const stream = this.capabilityService
74
+ .load('ai_chat')
75
+ .callStream('chat', { message });
76
+
77
+ for await (const chunk of stream) {
78
+ yield chunk as { content: string };
79
+ }
80
+ }
81
+ }
82
+ ```
83
+
84
+ 普通调用会自动聚合流式结果:
85
+
86
+ ```typescript
87
+ // 即使是流式 action,call() 也会返回聚合后的完整结果
88
+ const result = await this.capabilityService
89
+ .load('ai_chat')
90
+ .call('chat', { message: 'hello' });
91
+ ```
92
+
62
93
  ### 上下文覆盖
63
94
 
64
95
  ```typescript
@@ -128,16 +159,42 @@ POST /api/capability/:capability_id
128
159
  }
129
160
  ```
130
161
 
131
- ### Debug 调用(仅开发环境)
162
+ ### 前端流式调用
132
163
 
133
164
  ```
134
- POST /__innerapi__/capability/debug/:capability_id
165
+ POST /api/capability/:capability_id/stream
135
166
 
136
167
  请求体:
137
168
  {
138
- "action": "run",
169
+ "action": "chat",
139
170
  "params": {
171
+ "message": "hello"
172
+ }
173
+ }
174
+
175
+ 响应(SSE 格式):
176
+ data: {"content":"Hello"}
177
+ data: {"content":" World"}
178
+ data: [DONE]
179
+ ```
180
+
181
+ ### Debug 调用(仅开发环境)
182
+
183
+ ```
184
+ POST /__innerapi__/capability/debug/:capability_id
185
+
186
+ 请求体(所有字段可选):
187
+ {
188
+ "action": "run", // 可选,默认使用插件第一个 action
189
+ "params": { // 可选,用户输入参数
140
190
  "group_name": "测试群"
191
+ },
192
+ "capability": { // 可选,完整的 capability 配置(优先使用)
193
+ "id": "test",
194
+ "pluginID": "@test/plugin",
195
+ "pluginVersion": "1.0.0",
196
+ "name": "测试",
197
+ "formValue": {}
141
198
  }
142
199
  }
143
200
 
@@ -150,11 +207,30 @@ POST /__innerapi__/capability/debug/:capability_id
150
207
  "capabilityConfig": { ... },
151
208
  "resolvedParams": { ... },
152
209
  "duration": 123,
153
- "pluginID": "@xxx/plugin"
210
+ "pluginID": "@xxx/plugin",
211
+ "action": "run"
154
212
  }
155
213
  }
156
214
  ```
157
215
 
216
+ ### Debug 流式调用(仅开发环境)
217
+
218
+ ```
219
+ POST /__innerapi__/capability/debug/:capability_id/stream
220
+
221
+ 请求体(所有字段可选):
222
+ {
223
+ "action": "chat",
224
+ "params": { "message": "hello" },
225
+ "capability": { ... }
226
+ }
227
+
228
+ 响应(SSE 格式):
229
+ data: {"content":"Hello"}
230
+ data: {"content":" World"}
231
+ data: [DONE]
232
+ ```
233
+
158
234
  ### 列出所有能力(仅开发环境)
159
235
 
160
236
  ```
@@ -192,16 +268,30 @@ interface CapabilityService {
192
268
  // 加载能力并返回执行器
193
269
  load(capabilityId: string): CapabilityExecutor;
194
270
 
271
+ // 使用传入的配置加载能力执行器(用于 debug 场景)
272
+ loadWithConfig(config: CapabilityConfig): CapabilityExecutor;
273
+
195
274
  // 设置自定义能力目录
196
275
  setCapabilitiesDir(dir: string): void;
197
276
  }
198
277
 
199
278
  interface CapabilityExecutor {
279
+ // 调用能力(流式 action 会自动聚合结果)
200
280
  call(
201
281
  actionName: string,
202
282
  input: unknown,
203
283
  context?: Partial<PluginActionContext>
204
284
  ): Promise<unknown>;
285
+
286
+ // 流式调用能力(返回 AsyncIterable)
287
+ callStream(
288
+ actionName: string,
289
+ input: unknown,
290
+ context?: Partial<PluginActionContext>
291
+ ): AsyncIterable<unknown>;
292
+
293
+ // 检查 action 是否为流式
294
+ isStream(actionName: string): Promise<boolean>;
205
295
  }
206
296
  ```
207
297
 
@@ -257,10 +347,22 @@ interface CapabilityConfig {
257
347
 
258
348
  ```typescript
259
349
  interface PluginInstance {
350
+ // 执行 action(必需)
260
351
  run(actionName: string, context: PluginActionContext, input: unknown): Promise<unknown>;
352
+
353
+ // 流式执行 action(可选)
354
+ runStream?(actionName: string, context: PluginActionContext, input: unknown): AsyncIterable<unknown>;
355
+
356
+ // 检查是否为流式 action(可选)
357
+ isStreamAction?(actionName: string): boolean;
358
+
359
+ // 聚合流式结果(可选,默认返回 chunks 数组)
360
+ aggregate?(actionName: string, chunks: unknown[]): unknown;
361
+
362
+ // Action 相关
261
363
  hasAction(actionName: string): boolean;
262
- getActionSchema(actionName: string): ActionSchema | null;
263
364
  listActions(): string[];
365
+ getActionSchema(actionName: string): ActionSchema | null;
264
366
  getInputSchema(actionName: string, config?: unknown): ZodSchema | undefined;
265
367
  getOutputSchema(actionName: string, input: unknown, config?: unknown): ZodSchema | undefined;
266
368
  }
package/dist/index.cjs CHANGED
@@ -57,7 +57,10 @@ var TemplateEngineService = class {
57
57
  static {
58
58
  __name(this, "TemplateEngineService");
59
59
  }
60
- TEMPLATE_REGEX = /^\{\{input\.(.+)\}\}$/;
60
+ // 匹配 {{input.xxx}} 或 {{input.xxx.yyy}} 表达式
61
+ EXPR_REGEX = /\{\{input\.([a-zA-Z_][a-zA-Z_0-9]*(?:\.[a-zA-Z_][a-zA-Z_0-9]*)*)\}\}/g;
62
+ // 匹配整串单个表达式
63
+ WHOLE_STRING_EXPR_REGEX = /^\{\{input\.([a-zA-Z_][a-zA-Z_0-9]*(?:\.[a-zA-Z_][a-zA-Z_0-9]*)*)\}\}$/;
61
64
  resolve(template, input) {
62
65
  if (typeof template === "string") {
63
66
  return this.resolveString(template, input);
@@ -71,12 +74,25 @@ var TemplateEngineService = class {
71
74
  return template;
72
75
  }
73
76
  resolveString(template, input) {
74
- const match = template.match(this.TEMPLATE_REGEX);
75
- if (!match) {
77
+ const wholeMatch = template.match(this.WHOLE_STRING_EXPR_REGEX);
78
+ if (wholeMatch) {
79
+ const path2 = wholeMatch[1];
80
+ const value = this.getValueByPath(input, path2);
81
+ return value !== void 0 ? value : template;
82
+ }
83
+ this.EXPR_REGEX.lastIndex = 0;
84
+ if (!this.EXPR_REGEX.test(template)) {
76
85
  return template;
77
86
  }
78
- const path2 = match[1];
79
- return this.getValueByPath(input, path2);
87
+ this.EXPR_REGEX.lastIndex = 0;
88
+ const result = template.replace(this.EXPR_REGEX, (match, path2) => {
89
+ const value = this.getValueByPath(input, path2);
90
+ if (value === void 0) {
91
+ return match;
92
+ }
93
+ return String(value);
94
+ });
95
+ return result;
80
96
  }
81
97
  resolveObject(template, input) {
82
98
  const result = {};
@@ -278,13 +294,44 @@ var CapabilityService = class _CapabilityService {
278
294
  if (!config) {
279
295
  throw new CapabilityNotFoundError(capabilityId);
280
296
  }
297
+ return this.createExecutor(config);
298
+ }
299
+ /**
300
+ * 使用传入的配置加载能力执行器
301
+ * 用于 debug 场景,支持用户传入自定义配置
302
+ */
303
+ loadWithConfig(config) {
304
+ return this.createExecutor(config);
305
+ }
306
+ createExecutor(config) {
281
307
  return {
282
308
  call: /* @__PURE__ */ __name(async (actionName, input, contextOverride) => {
283
- return this.execute(config, actionName, input, contextOverride);
284
- }, "call")
309
+ return this.executeCall(config, actionName, input, contextOverride);
310
+ }, "call"),
311
+ callStream: /* @__PURE__ */ __name((actionName, input, contextOverride) => {
312
+ return this.executeCallStream(config, actionName, input, contextOverride);
313
+ }, "callStream"),
314
+ isStream: /* @__PURE__ */ __name(async (actionName) => {
315
+ return this.checkIsStream(config, actionName);
316
+ }, "isStream")
285
317
  };
286
318
  }
287
- async execute(config, actionName, input, contextOverride) {
319
+ /**
320
+ * 检查 action 是否为流式
321
+ */
322
+ async checkIsStream(config, actionName) {
323
+ const pluginInstance = await this.pluginLoaderService.loadPlugin(config.pluginID);
324
+ if (!pluginInstance.hasAction(actionName)) {
325
+ throw new ActionNotFoundError(config.pluginID, actionName);
326
+ }
327
+ return pluginInstance.isStreamAction?.(actionName) ?? false;
328
+ }
329
+ /**
330
+ * 执行 capability(始终返回 Promise)
331
+ * - unary action: 直接返回结果
332
+ * - stream action: 内部聚合所有 chunk 后返回
333
+ */
334
+ async executeCall(config, actionName, input, contextOverride) {
288
335
  const startTime = Date.now();
289
336
  try {
290
337
  const pluginInstance = await this.pluginLoaderService.loadPlugin(config.pluginID);
@@ -293,13 +340,24 @@ var CapabilityService = class _CapabilityService {
293
340
  }
294
341
  const resolvedParams = this.templateEngineService.resolve(config.formValue, input);
295
342
  const context = this.buildActionContext(contextOverride);
343
+ const isStream = pluginInstance.isStreamAction?.(actionName) ?? false;
296
344
  this.logger.log({
297
345
  message: "Executing capability",
298
346
  capabilityId: config.id,
299
347
  action: actionName,
300
- pluginID: config.pluginID
348
+ pluginID: config.pluginID,
349
+ isStream
301
350
  });
302
- const result = await pluginInstance.run(actionName, context, resolvedParams);
351
+ let result;
352
+ if (isStream && pluginInstance.runStream) {
353
+ const chunks = [];
354
+ for await (const chunk of pluginInstance.runStream(actionName, context, resolvedParams)) {
355
+ chunks.push(chunk);
356
+ }
357
+ result = pluginInstance.aggregate ? pluginInstance.aggregate(actionName, chunks) : chunks;
358
+ } else {
359
+ result = await pluginInstance.run(actionName, context, resolvedParams);
360
+ }
303
361
  this.logger.log({
304
362
  message: "Capability executed successfully",
305
363
  capabilityId: config.id,
@@ -318,6 +376,51 @@ var CapabilityService = class _CapabilityService {
318
376
  throw error;
319
377
  }
320
378
  }
379
+ /**
380
+ * 流式执行 capability
381
+ * - stream action: 返回原始 AsyncIterable
382
+ * - unary action: 包装为单次 yield
383
+ */
384
+ async *executeCallStream(config, actionName, input, contextOverride) {
385
+ const startTime = Date.now();
386
+ try {
387
+ const pluginInstance = await this.pluginLoaderService.loadPlugin(config.pluginID);
388
+ if (!pluginInstance.hasAction(actionName)) {
389
+ throw new ActionNotFoundError(config.pluginID, actionName);
390
+ }
391
+ const resolvedParams = this.templateEngineService.resolve(config.formValue, input);
392
+ const context = this.buildActionContext(contextOverride);
393
+ const isStream = pluginInstance.isStreamAction?.(actionName) ?? false;
394
+ this.logger.log({
395
+ message: "Executing capability (stream)",
396
+ capabilityId: config.id,
397
+ action: actionName,
398
+ pluginID: config.pluginID,
399
+ isStream
400
+ });
401
+ if (isStream && pluginInstance.runStream) {
402
+ yield* pluginInstance.runStream(actionName, context, resolvedParams);
403
+ } else {
404
+ const result = await pluginInstance.run(actionName, context, resolvedParams);
405
+ yield result;
406
+ }
407
+ this.logger.log({
408
+ message: "Capability stream completed",
409
+ capabilityId: config.id,
410
+ action: actionName,
411
+ duration: Date.now() - startTime
412
+ });
413
+ } catch (error) {
414
+ this.logger.error({
415
+ message: "Capability stream execution failed",
416
+ capabilityId: config.id,
417
+ action: actionName,
418
+ error: error instanceof Error ? error.message : String(error),
419
+ duration: Date.now() - startTime
420
+ });
421
+ throw error;
422
+ }
423
+ }
321
424
  buildActionContext(override) {
322
425
  return {
323
426
  logger: this.logger,
@@ -369,9 +472,11 @@ var DebugController = class {
369
472
  __name(this, "DebugController");
370
473
  }
371
474
  capabilityService;
475
+ pluginLoaderService;
372
476
  templateEngineService;
373
- constructor(capabilityService, templateEngineService) {
477
+ constructor(capabilityService, pluginLoaderService, templateEngineService) {
374
478
  this.capabilityService = capabilityService;
479
+ this.pluginLoaderService = pluginLoaderService;
375
480
  this.templateEngineService = templateEngineService;
376
481
  }
377
482
  list() {
@@ -387,8 +492,14 @@ var DebugController = class {
387
492
  }))
388
493
  };
389
494
  }
390
- async debug(capabilityId, body) {
391
- const startTime = Date.now();
495
+ /**
496
+ * 获取 capability 配置
497
+ * 优先使用 body.capability,否则从服务获取
498
+ */
499
+ getCapabilityConfig(capabilityId, bodyCapability) {
500
+ if (bodyCapability) {
501
+ return bodyCapability;
502
+ }
392
503
  const config = this.capabilityService.getCapability(capabilityId);
393
504
  if (!config) {
394
505
  throw new import_common4.HttpException({
@@ -397,9 +508,35 @@ var DebugController = class {
397
508
  error: "CAPABILITY_NOT_FOUND"
398
509
  }, import_common4.HttpStatus.NOT_FOUND);
399
510
  }
400
- const resolvedParams = this.templateEngineService.resolve(config.formValue, body.params);
511
+ return config;
512
+ }
513
+ /**
514
+ * 获取 action 名称
515
+ * 优先使用传入的 action,否则使用插件第一个 action
516
+ */
517
+ async getActionName(pluginID, bodyAction) {
518
+ if (bodyAction) {
519
+ return bodyAction;
520
+ }
521
+ const pluginInstance = await this.pluginLoaderService.loadPlugin(pluginID);
522
+ const actions = pluginInstance.listActions();
523
+ if (actions.length === 0) {
524
+ throw new import_common4.HttpException({
525
+ code: 1,
526
+ message: `Plugin ${pluginID} has no actions`,
527
+ error: "NO_ACTIONS"
528
+ }, import_common4.HttpStatus.BAD_REQUEST);
529
+ }
530
+ return actions[0];
531
+ }
532
+ async debug(capabilityId, body) {
533
+ const startTime = Date.now();
534
+ const params = body.params ?? {};
535
+ const config = this.getCapabilityConfig(capabilityId, body.capability);
536
+ const action = await this.getActionName(config.pluginID, body.action);
537
+ const resolvedParams = this.templateEngineService.resolve(config.formValue, params);
401
538
  try {
402
- const result = await this.capabilityService.load(capabilityId).call(body.action, body.params);
539
+ const result = await this.capabilityService.loadWithConfig(config).call(action, params);
403
540
  return {
404
541
  code: 0,
405
542
  message: "success",
@@ -408,7 +545,8 @@ var DebugController = class {
408
545
  capabilityConfig: config,
409
546
  resolvedParams,
410
547
  duration: Date.now() - startTime,
411
- pluginID: config.pluginID
548
+ pluginID: config.pluginID,
549
+ action
412
550
  }
413
551
  };
414
552
  } catch (error) {
@@ -430,7 +568,8 @@ var DebugController = class {
430
568
  error: "PLUGIN_NOT_FOUND",
431
569
  debug: {
432
570
  duration,
433
- pluginID: config.pluginID
571
+ pluginID: config.pluginID,
572
+ action
434
573
  }
435
574
  }, import_common4.HttpStatus.INTERNAL_SERVER_ERROR);
436
575
  }
@@ -441,7 +580,8 @@ var DebugController = class {
441
580
  error: "ACTION_NOT_FOUND",
442
581
  debug: {
443
582
  duration,
444
- pluginID: config.pluginID
583
+ pluginID: config.pluginID,
584
+ action
445
585
  }
446
586
  }, import_common4.HttpStatus.BAD_REQUEST);
447
587
  }
@@ -452,11 +592,51 @@ var DebugController = class {
452
592
  debug: {
453
593
  duration,
454
594
  pluginID: config.pluginID,
595
+ action,
455
596
  resolvedParams
456
597
  }
457
598
  }, import_common4.HttpStatus.INTERNAL_SERVER_ERROR);
458
599
  }
459
600
  }
601
+ async debugStream(capabilityId, body, res) {
602
+ const params = body.params ?? {};
603
+ res.setHeader("Content-Type", "text/event-stream");
604
+ res.setHeader("Cache-Control", "no-cache");
605
+ res.setHeader("Connection", "keep-alive");
606
+ try {
607
+ const config = this.getCapabilityConfig(capabilityId, body.capability);
608
+ const action = await this.getActionName(config.pluginID, body.action);
609
+ const capability = this.capabilityService.loadWithConfig(config);
610
+ const stream = capability.callStream(action, params);
611
+ for await (const chunk of stream) {
612
+ res.write(`data: ${JSON.stringify(chunk)}
613
+
614
+ `);
615
+ }
616
+ res.write("data: [DONE]\n\n");
617
+ } catch (error) {
618
+ const message = error instanceof Error ? error.message : String(error);
619
+ let errorCode = "EXECUTION_ERROR";
620
+ if (error instanceof CapabilityNotFoundError) {
621
+ errorCode = "CAPABILITY_NOT_FOUND";
622
+ } else if (error instanceof PluginNotFoundError) {
623
+ errorCode = "PLUGIN_NOT_FOUND";
624
+ } else if (error instanceof ActionNotFoundError) {
625
+ errorCode = "ACTION_NOT_FOUND";
626
+ } else if (error instanceof import_common4.HttpException) {
627
+ const response = error.getResponse();
628
+ errorCode = response.error ?? "EXECUTION_ERROR";
629
+ }
630
+ res.write(`data: ${JSON.stringify({
631
+ error: message,
632
+ code: errorCode
633
+ })}
634
+
635
+ `);
636
+ } finally {
637
+ res.end();
638
+ }
639
+ }
460
640
  };
461
641
  _ts_decorate4([
462
642
  (0, import_common4.Get)("list"),
@@ -471,15 +651,29 @@ _ts_decorate4([
471
651
  _ts_metadata2("design:type", Function),
472
652
  _ts_metadata2("design:paramtypes", [
473
653
  String,
474
- typeof ExecuteRequestBody === "undefined" ? Object : ExecuteRequestBody
654
+ typeof DebugRequestBody === "undefined" ? Object : DebugRequestBody
475
655
  ]),
476
656
  _ts_metadata2("design:returntype", Promise)
477
657
  ], DebugController.prototype, "debug", null);
658
+ _ts_decorate4([
659
+ (0, import_common4.Post)("debug/:capability_id/stream"),
660
+ _ts_param2(0, (0, import_common4.Param)("capability_id")),
661
+ _ts_param2(1, (0, import_common4.Body)()),
662
+ _ts_param2(2, (0, import_common4.Res)()),
663
+ _ts_metadata2("design:type", Function),
664
+ _ts_metadata2("design:paramtypes", [
665
+ String,
666
+ typeof DebugRequestBody === "undefined" ? Object : DebugRequestBody,
667
+ typeof Response === "undefined" ? Object : Response
668
+ ]),
669
+ _ts_metadata2("design:returntype", Promise)
670
+ ], DebugController.prototype, "debugStream", null);
478
671
  DebugController = _ts_decorate4([
479
672
  (0, import_common4.Controller)("__innerapi__/capability"),
480
673
  _ts_metadata2("design:type", Function),
481
674
  _ts_metadata2("design:paramtypes", [
482
675
  typeof CapabilityService === "undefined" ? Object : CapabilityService,
676
+ typeof PluginLoaderService === "undefined" ? Object : PluginLoaderService,
483
677
  typeof TemplateEngineService === "undefined" ? Object : TemplateEngineService
484
678
  ])
485
679
  ], DebugController);
@@ -562,6 +756,39 @@ var WebhookController = class {
562
756
  }, import_common5.HttpStatus.INTERNAL_SERVER_ERROR);
563
757
  }
564
758
  }
759
+ async executeStream(capabilityId, body, res) {
760
+ res.setHeader("Content-Type", "text/event-stream");
761
+ res.setHeader("Cache-Control", "no-cache");
762
+ res.setHeader("Connection", "keep-alive");
763
+ try {
764
+ const capability = this.capabilityService.load(capabilityId);
765
+ const stream = capability.callStream(body.action, body.params);
766
+ for await (const chunk of stream) {
767
+ res.write(`data: ${JSON.stringify(chunk)}
768
+
769
+ `);
770
+ }
771
+ res.write("data: [DONE]\n\n");
772
+ } catch (error) {
773
+ const message = error instanceof Error ? error.message : String(error);
774
+ let errorCode = "EXECUTION_ERROR";
775
+ if (error instanceof CapabilityNotFoundError) {
776
+ errorCode = "CAPABILITY_NOT_FOUND";
777
+ } else if (error instanceof PluginNotFoundError) {
778
+ errorCode = "PLUGIN_NOT_FOUND";
779
+ } else if (error instanceof ActionNotFoundError) {
780
+ errorCode = "ACTION_NOT_FOUND";
781
+ }
782
+ res.write(`data: ${JSON.stringify({
783
+ error: message,
784
+ code: errorCode
785
+ })}
786
+
787
+ `);
788
+ } finally {
789
+ res.end();
790
+ }
791
+ }
565
792
  };
566
793
  _ts_decorate5([
567
794
  (0, import_common5.Get)("list"),
@@ -580,6 +807,19 @@ _ts_decorate5([
580
807
  ]),
581
808
  _ts_metadata3("design:returntype", Promise)
582
809
  ], WebhookController.prototype, "execute", null);
810
+ _ts_decorate5([
811
+ (0, import_common5.Post)(":capability_id/stream"),
812
+ _ts_param3(0, (0, import_common5.Param)("capability_id")),
813
+ _ts_param3(1, (0, import_common5.Body)()),
814
+ _ts_param3(2, (0, import_common5.Res)()),
815
+ _ts_metadata3("design:type", Function),
816
+ _ts_metadata3("design:paramtypes", [
817
+ String,
818
+ typeof ExecuteRequestBody === "undefined" ? Object : ExecuteRequestBody,
819
+ typeof Response === "undefined" ? Object : Response
820
+ ]),
821
+ _ts_metadata3("design:returntype", Promise)
822
+ ], WebhookController.prototype, "executeStream", null);
583
823
  WebhookController = _ts_decorate5([
584
824
  (0, import_common5.Controller)("api/capability"),
585
825
  _ts_metadata3("design:type", Function),