plugin-agent-orchestrator 1.0.14 → 1.0.15

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.
Files changed (28) hide show
  1. package/dist/externalVersion.js +6 -6
  2. package/dist/server/collections/agent-execution-spans.d.ts +1 -1
  3. package/dist/server/collections/orchestrator-config.d.ts +1 -1
  4. package/dist/server/collections/orchestrator-logs.d.ts +1 -1
  5. package/dist/server/collections/skill-definitions.d.ts +0 -1
  6. package/dist/server/collections/skill-executions.d.ts +0 -1
  7. package/dist/server/collections/skill-worker-configs.d.ts +0 -1
  8. package/dist/server/services/CodeValidator.js +1 -0
  9. package/dist/server/skill-hub/tasks/SkillExecutionTask.d.ts +2 -0
  10. package/dist/server/skill-hub/tasks/SkillExecutionTask.js +122 -0
  11. package/package.json +1 -1
  12. package/src/client/index.tsx +1 -1
  13. package/src/client/plugin.tsx +54 -54
  14. package/src/client/skill-hub/index.tsx +75 -75
  15. package/src/server/migrations/20260423000000-add-progress-fields.ts +5 -5
  16. package/src/server/migrations/20260425000000-add-interaction-schema.ts +5 -5
  17. package/src/server/migrations/20260427000000-add-tracing-detail-fields.ts +5 -5
  18. package/src/server/migrations/20260427000000-change-packages-to-text.ts +7 -7
  19. package/src/server/migrations/20260427000001-change-other-json-to-text.ts +10 -10
  20. package/src/server/migrations/20260429000000-add-llm-fields.ts +2 -2
  21. package/src/server/migrations/20260429000000-fix-inputargs-json-to-text.ts +2 -2
  22. package/src/server/migrations/20260503000000-add-orchestrator-trace-fields.ts +2 -2
  23. package/src/server/plugin.ts +94 -94
  24. package/src/server/services/CodeValidator.ts +5 -5
  25. package/src/server/services/SkillManager.ts +1 -1
  26. package/src/server/services/WorkerEnvManager.ts +5 -5
  27. package/src/server/skill-hub/plugin.ts +58 -58
  28. package/src/server/skill-hub/tasks/SkillExecutionTask.ts +162 -16
@@ -95,14 +95,14 @@ export class SkillHubSubFeature {
95
95
  // 2. Init services
96
96
  const storagePath = resolve(process.cwd(), 'storage', 'plugin-skill-hub'); // Keep old storage path for backwards compatibility
97
97
  this.fileManager = new FileManager(storagePath);
98
- this.sandboxRunner = new SandboxRunner(this.fileManager, this.app.logger, storagePath);
99
- this.skillManager = new SkillManager(this.db);
100
- this.workerEnvManager = new WorkerEnvManager(this.app, this.db, storagePath);
98
+ this.sandboxRunner = new SandboxRunner(this.fileManager, (this as any).app.logger, storagePath);
99
+ this.skillManager = new SkillManager((this as any).db);
100
+ this.workerEnvManager = new WorkerEnvManager((this as any).app, (this as any).db, storagePath);
101
101
  this.skillRepoService = new SkillRepositoryService(storagePath);
102
102
  this.mcpController = new McpController(this);
103
103
 
104
104
  // 3. Register REST actions
105
- this.app.resourceManager.define({
105
+ (this as any).app.resourceManager.define({
106
106
  name: 'skillHub',
107
107
  actions: {
108
108
  download: this.handleDownload.bind(this),
@@ -119,7 +119,7 @@ export class SkillHubSubFeature {
119
119
 
120
120
 
121
121
  // 4.5. Register DB hooks for automatic storage physical cleanup
122
- this.db.on('skillExecutions.afterDestroy', async (model, options) => {
122
+ (this as any).db.on('skillExecutions.afterDestroy', async (model, options) => {
123
123
  const execId = model.get('id');
124
124
  try {
125
125
  const dir = this.fileManager.getExecDir(String(execId));
@@ -127,29 +127,29 @@ export class SkillHubSubFeature {
127
127
  require('fs').rmSync(dir, { recursive: true, force: true });
128
128
  }
129
129
  } catch (err) {
130
- this.app.logger.error(`[skill-hub] Failed to cleanup physical storage for execId ${execId}`, { error: err });
130
+ (this as any).app.logger.error(`[skill-hub] Failed to cleanup physical storage for execId ${execId}`, { error: err });
131
131
  }
132
132
  });
133
133
 
134
- this.db.on('skillDefinitions.afterSave', async (model, options) => {
134
+ (this as any).db.on('skillDefinitions.afterSave', async (model, options) => {
135
135
  // If a zip file was uploaded, extract it and update the skill record
136
136
  if (model.changed('fileId') && model.get('fileId')) {
137
137
  try {
138
- const attachment = await this.db.getRepository('attachments').findOne({
138
+ const attachment = await (this as any).db.getRepository('attachments').findOne({
139
139
  filter: { id: model.get('fileId') },
140
140
  transaction: options.transaction,
141
141
  });
142
142
 
143
143
  if (attachment) {
144
- const fileManager = this.app.pm.get('@nocobase/plugin-file-manager') as any;
144
+ const fileManager = (this as any).app.pm.get('@nocobase/plugin-file-manager') as any;
145
145
  if (!fileManager) {
146
- this.app.logger.warn('[skill-hub] plugin-file-manager not found, cannot extract skill package');
146
+ (this as any).app.logger.warn('[skill-hub] plugin-file-manager not found, cannot extract skill package');
147
147
  return;
148
148
  }
149
149
 
150
150
  const streamData = await fileManager.getFileStream(attachment);
151
151
  if (!streamData || !streamData.stream) {
152
- this.app.logger.warn(`[skill-hub] Could not get file stream for attachment ${attachment.get('id')}`);
152
+ (this as any).app.logger.warn(`[skill-hub] Could not get file stream for attachment ${attachment.get('id')}`);
153
153
  return;
154
154
  }
155
155
 
@@ -179,54 +179,54 @@ export class SkillHubSubFeature {
179
179
  if (metadata.timeoutSeconds) updateValues.timeoutSeconds = metadata.timeoutSeconds;
180
180
  if (instructions) updateValues.instructions = instructions;
181
181
 
182
- await this.db.getRepository('skillDefinitions').update({
182
+ await (this as any).db.getRepository('skillDefinitions').update({
183
183
  filter: { id: model.get('id') },
184
184
  values: updateValues,
185
185
  transaction: options.transaction,
186
186
  });
187
187
 
188
188
  unlinkSync(tempZipPath);
189
- this.app.logger.info(`[skill-hub] Successfully extracted zip and updated skill: ${skillName}`);
189
+ (this as any).app.logger.info(`[skill-hub] Successfully extracted zip and updated skill: ${skillName}`);
190
190
  }
191
191
  }
192
192
  } catch (err) {
193
- this.app.logger.error(`[skill-hub] Failed to unpack skill zip`, { error: err });
193
+ (this as any).app.logger.error(`[skill-hub] Failed to unpack skill zip`, { error: err });
194
194
  }
195
195
  }
196
196
  });
197
197
 
198
198
  // 5. Subscribe PubSub — worker processes skill execution tasks
199
- this.app.pubSubManager.subscribe('skill-hub.task', async (payload: any) => {
199
+ (this as any).app.pubSubManager.subscribe('skill-hub.task', async (payload: any) => {
200
200
  if (process.env.SKILL_HUB_SANDBOX === 'false') return;
201
201
  await this.onQueueTask(payload);
202
202
  });
203
203
 
204
204
  // 5b. Subscribe PubSub — worker processes init-env tasks
205
- this.app.pubSubManager.subscribe('skill-hub.init-env', async (payload: any) => {
205
+ (this as any).app.pubSubManager.subscribe('skill-hub.init-env', async (payload: any) => {
206
206
  if (process.env.SKILL_HUB_SANDBOX === 'false') return;
207
207
  await this.workerEnvManager.executeInit(payload);
208
208
  });
209
209
 
210
210
  // 6. Register AI tools + subscriptions (deferred — after all plugins loaded)
211
- this.app.on('afterStart', async () => {
211
+ (this as any).app.on('afterStart', async () => {
212
212
  this.registerAITools();
213
213
  this.startCleanupInterval();
214
214
  await this.subscribeInitEnvDone();
215
215
  // Ensure any newly added built-in skills are seeded automatically on upgrade/restart
216
216
  await this.skillManager.seedDefaults().catch((e) => {
217
- this.app.logger.error(`[skill-hub] Failed to seed default skills: ${e.message}`);
217
+ (this as any).app.logger.error(`[skill-hub] Failed to seed default skills: ${e.message}`);
218
218
  });
219
219
  });
220
220
  }
221
221
 
222
222
  private async onQueueTask(message: { id: string }) {
223
- this.app.logger.info(`[skill-hub] Worker received queue task: ${message.id}`);
224
- const execution = await this.db.getRepository('skillExecutions').findOne({
223
+ (this as any).app.logger.info(`[skill-hub] Worker received queue task: ${message.id}`);
224
+ const execution = await (this as any).db.getRepository('skillExecutions').findOne({
225
225
  filter: { id: message.id },
226
226
  appends: ['skill'],
227
227
  });
228
228
  if (!execution) {
229
- this.app.logger.warn(`[skill-hub] Task ${message.id} ignored: execution record not found.`);
229
+ (this as any).app.logger.warn(`[skill-hub] Task ${message.id} ignored: execution record not found.`);
230
230
  return;
231
231
  }
232
232
 
@@ -235,7 +235,7 @@ export class SkillHubSubFeature {
235
235
  this.sandboxRunner,
236
236
  this.fileManager,
237
237
  this.skillRepoService,
238
- this.app,
238
+ (this as any).app,
239
239
  );
240
240
  await task.run();
241
241
  }
@@ -259,7 +259,7 @@ export class SkillHubSubFeature {
259
259
  }
260
260
  }
261
261
 
262
- const execution = await this.db.getRepository('skillExecutions').create({
262
+ const execution = await (this as any).db.getRepository('skillExecutions').create({
263
263
  values: {
264
264
  skillId: skill.id,
265
265
  status: 'pending',
@@ -271,7 +271,7 @@ export class SkillHubSubFeature {
271
271
 
272
272
  const execId = String(execution.id);
273
273
 
274
- this.app.logger.info(
274
+ (this as any).app.logger.info(
275
275
  `[skill-hub] Queued execution ${execId}: skill=${skill.get ? skill.get('name') : skill.name}, ` +
276
276
  `user=${userId || 'system'}`,
277
277
  );
@@ -317,10 +317,10 @@ export class SkillHubSubFeature {
317
317
  };
318
318
 
319
319
  // Subscribe progress and completion FIRST (before dispatching)
320
- await this.app.pubSubManager.subscribe(progressChannel, progressCallback);
320
+ await (this as any).app.pubSubManager.subscribe(progressChannel, progressCallback);
321
321
  cleanups.push({ channel: progressChannel, callback: progressCallback });
322
322
 
323
- await this.app.pubSubManager.subscribe(doneChannel, doneCallback);
323
+ await (this as any).app.pubSubManager.subscribe(doneChannel, doneCallback);
324
324
  cleanups.push({ channel: doneChannel, callback: doneCallback });
325
325
 
326
326
  // Handle user abort (cancel chat) → propagate to worker
@@ -329,9 +329,9 @@ export class SkillHubSubFeature {
329
329
  signal.addEventListener?.('abort', () => {
330
330
  clearTimeout(timeout);
331
331
  // Publish abort to worker via PubSub
332
- this.app.pubSubManager.publish(abortChannel, { reason: 'user_cancel' }).catch(() => {});
332
+ (this as any).app.pubSubManager.publish(abortChannel, { reason: 'user_cancel' }).catch(() => {});
333
333
  // Also update the execution status
334
- this.db.getRepository('skillExecutions').update({
334
+ (this as any).db.getRepository('skillExecutions').update({
335
335
  filter: { id: execId },
336
336
  values: { status: 'canceled' },
337
337
  }).catch(() => {});
@@ -340,7 +340,7 @@ export class SkillHubSubFeature {
340
340
  }
341
341
 
342
342
  // NOW Dispatch to worker via EventQueue
343
- await this.app.pubSubManager.publish('skill-hub.task', { id: execId });
343
+ await (this as any).app.pubSubManager.publish('skill-hub.task', { id: execId });
344
344
 
345
345
  // Wait for completion
346
346
  result = await resultPromise;
@@ -348,7 +348,7 @@ export class SkillHubSubFeature {
348
348
  // Cleanup all PubSub subscriptions
349
349
  for (const { channel, callback } of cleanups) {
350
350
  try {
351
- await this.app.pubSubManager.unsubscribe(channel, callback);
351
+ await (this as any).app.pubSubManager.unsubscribe(channel, callback);
352
352
  } catch {
353
353
  // ignore cleanup errors
354
354
  }
@@ -383,7 +383,7 @@ export class SkillHubSubFeature {
383
383
  ctx.throw(401, 'Unauthorized');
384
384
  }
385
385
 
386
- const execution = await this.db.getRepository('skillExecutions').findOne({
386
+ const execution = await (this as any).db.getRepository('skillExecutions').findOne({
387
387
  filter: { id: execId },
388
388
  });
389
389
 
@@ -414,7 +414,7 @@ export class SkillHubSubFeature {
414
414
  ctx.throw(400, 'Missing skillId');
415
415
  }
416
416
 
417
- const skill = await this.db.getRepository('skillDefinitions').findOne({
417
+ const skill = await (this as any).db.getRepository('skillDefinitions').findOne({
418
418
  filter: { id: skillId },
419
419
  });
420
420
  if (!skill) {
@@ -465,20 +465,20 @@ export class SkillHubSubFeature {
465
465
  if (data.status === 'succeeded' && data.whitelist) {
466
466
  values.packageWhitelist = stringifyJsonText(data.whitelist, { python: [], node: [], apt: [] });
467
467
  }
468
- await this.db.getRepository('skillWorkerConfigs').update({
468
+ await (this as any).db.getRepository('skillWorkerConfigs').update({
469
469
  filter: {},
470
470
  values,
471
471
  forceUpdate: true,
472
472
  });
473
- this.app.logger.info(`[skill-hub] Init env ${data.status}`);
473
+ (this as any).app.logger.info(`[skill-hub] Init env ${data.status}`);
474
474
  } catch (err) {
475
- this.app.logger.warn('[skill-hub] Failed to update init env status:', err);
475
+ (this as any).app.logger.warn('[skill-hub] Failed to update init env status:', err);
476
476
  }
477
477
  };
478
478
 
479
479
  this.initEnvProgressCallback = async (data: any) => {
480
480
  try {
481
- await this.db.getRepository('skillWorkerConfigs').update({
481
+ await (this as any).db.getRepository('skillWorkerConfigs').update({
482
482
  filter: {},
483
483
  values: {
484
484
  initProgressPercent: data.percent,
@@ -491,15 +491,15 @@ export class SkillHubSubFeature {
491
491
  }
492
492
  };
493
493
 
494
- await this.app.pubSubManager.subscribe('skill-hub.init-env.done', this.initEnvDoneCallback);
495
- await this.app.pubSubManager.subscribe('skill-hub.init-env.progress', this.initEnvProgressCallback);
494
+ await (this as any).app.pubSubManager.subscribe('skill-hub.init-env.done', this.initEnvDoneCallback);
495
+ await (this as any).app.pubSubManager.subscribe('skill-hub.init-env.progress', this.initEnvProgressCallback);
496
496
  }
497
497
 
498
498
  private registerAITools() {
499
499
  try {
500
- const aiPlugin = this.app.pm.get('@nocobase/plugin-ai') as any;
500
+ const aiPlugin = (this as any).app.pm.get('@nocobase/plugin-ai') as any;
501
501
  if (!aiPlugin?.ai?.toolsManager) {
502
- this.app.logger.warn('[skill-hub] plugin-ai not available, skip AI tool registration.');
502
+ (this as any).app.logger.warn('[skill-hub] plugin-ai not available, skip AI tool registration.');
503
503
  return;
504
504
  }
505
505
 
@@ -509,7 +509,7 @@ export class SkillHubSubFeature {
509
509
  // 2. Dynamic tools — each enabled skill becomes a separate AI tool.
510
510
  aiPlugin.ai.toolsManager.registerDynamicTools(async (register: { registerTools: (options: any) => void }) => {
511
511
  try {
512
- const skills = await this.db.getRepository('skillDefinitions').find({
512
+ const skills = await (this as any).db.getRepository('skillDefinitions').find({
513
513
  filter: { enabled: true },
514
514
  });
515
515
 
@@ -539,7 +539,7 @@ export class SkillHubSubFeature {
539
539
  },
540
540
  invoke: async (toolCtx: any, args: any) => {
541
541
  // Re-fetch skill to get latest version (hot-reload support)
542
- const latestSkill = await this.db.getRepository('skillDefinitions').findOne({
542
+ const latestSkill = await (this as any).db.getRepository('skillDefinitions').findOne({
543
543
  filter: { id: skill.get('id'), enabled: true },
544
544
  });
545
545
  if (!latestSkill) {
@@ -556,13 +556,13 @@ export class SkillHubSubFeature {
556
556
 
557
557
  register.registerTools(tools);
558
558
  } catch (err) {
559
- this.app.logger.warn('[skill-hub] Failed to provide dynamic tools', err);
559
+ (this as any).app.logger.warn('[skill-hub] Failed to provide dynamic tools', err);
560
560
  }
561
561
  });
562
562
 
563
- this.app.logger.info('[skill-hub] AI tools registered (dynamic provider + general tool).');
563
+ (this as any).app.logger.info('[skill-hub] AI tools registered (dynamic provider + general tool).');
564
564
  } catch (error) {
565
- this.app.logger.warn('[skill-hub] Failed to register AI tools:', error);
565
+ (this as any).app.logger.warn('[skill-hub] Failed to register AI tools:', error);
566
566
  }
567
567
  }
568
568
 
@@ -573,13 +573,13 @@ export class SkillHubSubFeature {
573
573
  this.cleanupInterval = setInterval(async () => {
574
574
  // 1. Storage Retention Cleanup
575
575
  try {
576
- const config = await this.db.getRepository('skillWorkerConfigs').findOne();
576
+ const config = await (this as any).db.getRepository('skillWorkerConfigs').findOne();
577
577
  const hours = config ? config.get('retentionHours') : 24;
578
578
 
579
579
  if (hours && hours > 0) {
580
580
  const MAX_AGE_MS = hours * 60 * 60 * 1000;
581
581
  const cutoff = new Date(Date.now() - MAX_AGE_MS);
582
- const repo = this.db.getRepository('skillExecutions');
582
+ const repo = (this as any).db.getRepository('skillExecutions');
583
583
 
584
584
  const outdated = await repo.find({
585
585
  where: { createdAt: { $lt: cutoff } }
@@ -589,11 +589,11 @@ export class SkillHubSubFeature {
589
589
  for (const record of outdated) {
590
590
  await record.destroy(); // Fires afterDestroy hook which removes physical folder
591
591
  }
592
- this.app.logger.info(`[skill-hub] Auto-cleaned up ${outdated.length} expired execution records`);
592
+ (this as any).app.logger.info(`[skill-hub] Auto-cleaned up ${outdated.length} expired execution records`);
593
593
  }
594
594
  }
595
595
  } catch (err) {
596
- this.app.logger.warn('[skill-hub] Auto Cleanup error:', err);
596
+ (this as any).app.logger.warn('[skill-hub] Auto Cleanup error:', err);
597
597
  }
598
598
 
599
599
  // 2. Cleanup rate limiter stale entries
@@ -605,12 +605,12 @@ export class SkillHubSubFeature {
605
605
  // Unsubscribe PubSub
606
606
  if (this.initEnvDoneCallback) {
607
607
  try {
608
- await this.app.pubSubManager.unsubscribe('skill-hub.init-env.done', this.initEnvDoneCallback);
608
+ await (this as any).app.pubSubManager.unsubscribe('skill-hub.init-env.done', this.initEnvDoneCallback);
609
609
  } catch { /* ignore */ }
610
610
  }
611
611
  if (this.initEnvProgressCallback) {
612
612
  try {
613
- await this.app.pubSubManager.unsubscribe('skill-hub.init-env.progress', this.initEnvProgressCallback);
613
+ await (this as any).app.pubSubManager.unsubscribe('skill-hub.init-env.progress', this.initEnvProgressCallback);
614
614
  } catch { /* ignore */ }
615
615
  }
616
616
 
@@ -624,7 +624,7 @@ export class SkillHubSubFeature {
624
624
  // --- Handlers ---
625
625
  private async handleClearStorage(ctx: any, next: () => Promise<any>) {
626
626
  const { type } = ctx.request.body || ctx.action.params.values;
627
- const repo = this.db.getRepository('skillExecutions');
627
+ const repo = (this as any).db.getRepository('skillExecutions');
628
628
  let count = 0;
629
629
 
630
630
  if (type === 'all') {
@@ -634,7 +634,7 @@ export class SkillHubSubFeature {
634
634
  }
635
635
  count = results.length;
636
636
  } else if (type === 'expired') {
637
- const config = await this.db.getRepository('skillWorkerConfigs').findOne();
637
+ const config = await (this as any).db.getRepository('skillWorkerConfigs').findOne();
638
638
  const hours = config ? config.get('retentionHours') : 24;
639
639
  if (hours > 0) {
640
640
  const cutoff = new Date(Date.now() - hours * 60 * 60 * 1000);
@@ -653,7 +653,7 @@ export class SkillHubSubFeature {
653
653
  private async handleListTemplates(ctx: any, next: () => Promise<any>) {
654
654
  // Dynamic Pull: discover templates from all active plugins in the system
655
655
  try {
656
- const allPlugins = this.app.pm.getPlugins();
656
+ const allPlugins = (this as any).app.pm.getPlugins();
657
657
  for (const [, pluginInstance] of allPlugins) {
658
658
  if (typeof (pluginInstance as any).getSkillTemplates === 'function') {
659
659
  const pluginSkills = (pluginInstance as any).getSkillTemplates();
@@ -667,7 +667,7 @@ export class SkillHubSubFeature {
667
667
  }
668
668
  }
669
669
  } catch (e) {
670
- this.app.logger.warn(`[skill-hub] Failed to discover some plugin skills: ${e.message}`);
670
+ (this as any).app.logger.warn(`[skill-hub] Failed to discover some plugin skills: ${e.message}`);
671
671
  }
672
672
 
673
673
  ctx.body = { data: Array.from(this.skillTemplates.values()) };
@@ -683,7 +683,7 @@ export class SkillHubSubFeature {
683
683
  */
684
684
  registerSkillTemplate(pluginName: string, skillDef: any) {
685
685
  this.skillTemplates.set(skillDef.name, this.hydrateSkillTemplate(pluginName, skillDef));
686
- this.app.logger.info(`[skill-hub] Registered skill template "${skillDef.name}" from plugin "${pluginName}"`);
686
+ (this as any).app.logger.info(`[skill-hub] Registered skill template "${skillDef.name}" from plugin "${pluginName}"`);
687
687
  }
688
688
 
689
689
  resolveSkillTemplate(templateName: string) {
@@ -692,7 +692,7 @@ export class SkillHubSubFeature {
692
692
  if (cached) return cached;
693
693
 
694
694
  try {
695
- const allPlugins = this.app.pm.getPlugins();
695
+ const allPlugins = (this as any).app.pm.getPlugins();
696
696
  for (const [, pluginInstance] of allPlugins) {
697
697
  if (typeof (pluginInstance as any).getSkillTemplates !== 'function') continue;
698
698
  const pluginSkills = (pluginInstance as any).getSkillTemplates();
@@ -705,7 +705,7 @@ export class SkillHubSubFeature {
705
705
  }
706
706
  }
707
707
  } catch (e: any) {
708
- this.app.logger.warn(`[skill-hub] Failed to resolve plugin skill "${templateName}": ${e.message}`);
708
+ (this as any).app.logger.warn(`[skill-hub] Failed to resolve plugin skill "${templateName}": ${e.message}`);
709
709
  }
710
710
 
711
711
  return null;
@@ -1,8 +1,10 @@
1
1
  import Application from '@nocobase/server';
2
- import { resolve } from 'path';
2
+ import { cpSync, existsSync, readFileSync, rmSync } from 'fs';
3
+ import { resolve, sep } from 'path';
3
4
  import { SandboxRunner } from '../../services/SandboxRunner';
4
5
  import { FileManager } from '../../services/FileManager';
5
6
  import { SkillRepositoryService } from '../../services/SkillRepositoryService';
7
+ import { CodeValidator } from '../../services/CodeValidator';
6
8
  import { parseJsonText, stringifyJsonText } from '../utils/json-fields';
7
9
 
8
10
  /**
@@ -62,13 +64,13 @@ export class SkillExecutionTask {
62
64
  const abortController = new TaskAbortController();
63
65
  const abortChannel = `skill-hub.abort.${execId}`;
64
66
  const abortCallback = async () => {
65
- this.app.logger.info(`[skill-hub] Task ${execId}: received abort signal`);
67
+ (this as any).app.logger.info(`[skill-hub] Task ${execId}: received abort signal`);
66
68
  abortController.abort();
67
69
  };
68
70
 
69
71
  try {
70
72
  // Subscribe to abort channel before starting execution
71
- await this.app.pubSubManager.subscribe(abortChannel, abortCallback);
73
+ await (this as any).app.pubSubManager.subscribe(abortChannel, abortCallback);
72
74
 
73
75
  // Render code template with input args
74
76
  const inputArgs = parseJsonText(this.execution.get('inputArgs'), {});
@@ -84,7 +86,7 @@ export class SkillExecutionTask {
84
86
 
85
87
  if (storageType === 'plugin') {
86
88
  const pluginSkillName = (skill.get ? skill.get('pluginSource') : skill.pluginSource) || skillName;
87
- const orchestratorPlugin = this.app.pm.get('plugin-agent-orchestrator') as any;
89
+ const orchestratorPlugin = (this as any).app.pm.get('plugin-agent-orchestrator') as any;
88
90
  const skillHub = orchestratorPlugin?.skillHub;
89
91
  let pluginTemplate = typeof skillHub?.resolveSkillTemplate === 'function'
90
92
  ? skillHub.resolveSkillTemplate(pluginSkillName)
@@ -92,7 +94,7 @@ export class SkillExecutionTask {
92
94
 
93
95
  // Fallback: discover dynamically if not cached (e.g. executed in worker before UI was loaded)
94
96
  if (!pluginTemplate && skillHub) {
95
- const allPlugins = this.app.pm.getPlugins();
97
+ const allPlugins = (this as any).app.pm.getPlugins();
96
98
  for (const [, pInstance] of allPlugins) {
97
99
  if (typeof (pInstance as any).getSkillTemplates === 'function') {
98
100
  const pluginSkills = (pInstance as any).getSkillTemplates();
@@ -148,7 +150,7 @@ export class SkillExecutionTask {
148
150
  // Load package whitelist for import validation
149
151
  let packageWhitelist: string[] = [];
150
152
  try {
151
- const workerConfig = await this.app.db.getRepository('skillWorkerConfigs').findOne();
153
+ const workerConfig = await (this as any).app.db.getRepository('skillWorkerConfigs').findOne();
152
154
  if (workerConfig) {
153
155
  const wl = parseJsonText(
154
156
  workerConfig.get ? workerConfig.get('packageWhitelist') : workerConfig.packageWhitelist,
@@ -167,8 +169,8 @@ export class SkillExecutionTask {
167
169
 
168
170
  // In multi-node setups, local cache might be missing on this specific worker node. Re-download from S3 if needed.
169
171
  if (!require('fs').existsSync(this.skillRepoService.getSkillPath(skillName)) && fileId) {
170
- const fmPlugin = this.app.pm.get('@nocobase/plugin-file-manager') as any;
171
- const attachment = await this.app.db.getRepository('attachments').findOne({ filter: { id: fileId } });
172
+ const fmPlugin = (this as any).app.pm.get('@nocobase/plugin-file-manager') as any;
173
+ const attachment = await (this as any).app.db.getRepository('attachments').findOne({ filter: { id: fileId } });
172
174
  if (fmPlugin && attachment) {
173
175
  try {
174
176
  const streamData = await fmPlugin.getFileStream(attachment);
@@ -183,12 +185,12 @@ export class SkillExecutionTask {
183
185
  });
184
186
  await this.skillRepoService.extractSkillPackage(skillName, tempZipPath);
185
187
  require('fs').unlinkSync(tempZipPath);
186
- this.app.logger.info(
188
+ (this as any).app.logger.info(
187
189
  `[skill-hub] Task ${execId}: Auto-restored skill package ${skillName} from S3/Storage`,
188
190
  );
189
191
  }
190
192
  } catch (fetchErr) {
191
- this.app.logger.warn(
193
+ (this as any).app.logger.warn(
192
194
  `[skill-hub] Task ${execId}: Failed to fetch skill package ${skillName} from storage`,
193
195
  { error: fetchErr },
194
196
  );
@@ -212,10 +214,17 @@ export class SkillExecutionTask {
212
214
  packageWhitelist,
213
215
  onProgress: (progress) => {
214
216
  // Worker → PubSub → Main Server → runtime.writer → SSE → Client
215
- this.app.pubSubManager.publish(`skill-hub.progress.${execId}`, progress);
217
+ (this as any).app.pubSubManager.publish(`skill-hub.progress.${execId}`, progress);
216
218
  },
217
219
  });
218
220
 
221
+ if (result.success) {
222
+ const installMessage = await this.installGeneratedSkillIfRequested(execId);
223
+ if (installMessage) {
224
+ result.stdout = [result.stdout, installMessage].filter(Boolean).join('\n');
225
+ }
226
+ }
227
+
219
228
  // Determine final status
220
229
  let status: string;
221
230
  if (result.canceled) {
@@ -235,7 +244,7 @@ export class SkillExecutionTask {
235
244
  });
236
245
 
237
246
  // Notify main server: task completed
238
- await this.app.pubSubManager.publish(`skill-hub.done.${execId}`, {
247
+ await (this as any).app.pubSubManager.publish(`skill-hub.done.${execId}`, {
239
248
  status,
240
249
  stdout: result.stdout?.slice(0, 3000),
241
250
  stderr: result.stderr?.slice(0, 1000),
@@ -244,7 +253,7 @@ export class SkillExecutionTask {
244
253
  });
245
254
 
246
255
  // Log execution metrics
247
- this.app.logger.info(
256
+ (this as any).app.logger.info(
248
257
  `[skill-hub] Execution ${execId} ${status}: ` +
249
258
  `skill=${skill.get ? skill.get('name') : skill.name}, ` +
250
259
  `language=${language}, ` +
@@ -260,18 +269,18 @@ export class SkillExecutionTask {
260
269
  stderr: errorMessage,
261
270
  });
262
271
 
263
- await this.app.pubSubManager.publish(`skill-hub.done.${execId}`, {
272
+ await (this as any).app.pubSubManager.publish(`skill-hub.done.${execId}`, {
264
273
  status: 'failed',
265
274
  stderr: errorMessage,
266
275
  files: [],
267
276
  durationMs: 0,
268
277
  });
269
278
 
270
- this.app.logger.error(`[skill-hub] Execution ${execId} error: ${errorMessage}`);
279
+ (this as any).app.logger.error(`[skill-hub] Execution ${execId} error: ${errorMessage}`);
271
280
  } finally {
272
281
  // Always cleanup abort subscription
273
282
  try {
274
- await this.app.pubSubManager.unsubscribe(abortChannel, abortCallback);
283
+ await (this as any).app.pubSubManager.unsubscribe(abortChannel, abortCallback);
275
284
  } catch {
276
285
  // ignore cleanup errors
277
286
  }
@@ -294,4 +303,141 @@ export class SkillExecutionTask {
294
303
  code = code.replaceAll('{{skillDir}}', (skillDir || '').replace(/\\/g, '/'));
295
304
  return code;
296
305
  }
306
+
307
+ private async installGeneratedSkillIfRequested(execId: string): Promise<string | null> {
308
+ const manifestPath = this.fileManager.getOutputFilePath(execId, 'skill-hub-install.json');
309
+ if (!manifestPath) return null;
310
+
311
+ let manifest: any;
312
+ try {
313
+ manifest = JSON.parse(readFileSync(manifestPath, 'utf8'));
314
+ } catch (error) {
315
+ throw new Error(`Generated skill install manifest is invalid JSON: ${error instanceof Error ? error.message : String(error)}`);
316
+ }
317
+
318
+ if (!manifest?.autoInstall) return null;
319
+
320
+ const skill = manifest.skill || {};
321
+ const name = String(skill.name || '').trim();
322
+ if (!/^[a-z0-9][a-z0-9-]{0,63}$/.test(name)) {
323
+ throw new Error(`Generated skill name "${name}" is invalid. Use lowercase letters, numbers, and hyphens.`);
324
+ }
325
+
326
+ const language = skill.language;
327
+ if (language !== 'python' && language !== 'node') {
328
+ throw new Error(`Generated skill "${name}" has unsupported language "${language}".`);
329
+ }
330
+
331
+ if (!skill.codeTemplate) {
332
+ throw new Error(`Generated skill "${name}" is missing codeTemplate.`);
333
+ }
334
+
335
+ const validator = new CodeValidator();
336
+ validator.validate(skill.codeTemplate, language);
337
+ await this.validateGeneratedSkillPackages(name, language, skill.packages);
338
+
339
+ const outputDir = this.fileManager.getOutputDir(execId);
340
+ const outputRoot = resolve(outputDir);
341
+ const packageDirName = String(manifest.packageDir || name);
342
+ const packageDir = resolve(outputRoot, packageDirName);
343
+ if ((packageDir !== outputRoot && !packageDir.startsWith(outputRoot + sep)) || !existsSync(packageDir)) {
344
+ throw new Error(`Generated skill package directory "${packageDirName}" was not found in output.`);
345
+ }
346
+
347
+ if (manifest.testInput && Object.keys(manifest.testInput).length > 0) {
348
+ const verifyExecId = `${execId}-verify-${name}`;
349
+ const verifyCode = this.renderTemplate(skill.codeTemplate, manifest.testInput, verifyExecId, packageDir);
350
+ const verifyResult = await this.sandboxRunner.execute({
351
+ language,
352
+ code: verifyCode,
353
+ execId: verifyExecId,
354
+ timeoutSeconds: Math.min(Number(skill.timeoutSeconds || 60), 30),
355
+ maxOutputSizeMb: Math.min(Number(skill.maxOutputSizeMb || 50), 10),
356
+ skillDir: packageDir,
357
+ });
358
+
359
+ if (!verifyResult.success) {
360
+ throw new Error(
361
+ `Generated skill "${name}" failed smoke verification: ${verifyResult.stderr || verifyResult.stdout || 'unknown error'}`,
362
+ );
363
+ }
364
+ }
365
+
366
+ const skillRepoDir = this.skillRepoService.getSkillPath(name);
367
+ if (existsSync(skillRepoDir)) {
368
+ rmSync(skillRepoDir, { recursive: true, force: true });
369
+ }
370
+ cpSync(packageDir, skillRepoDir, {
371
+ recursive: true,
372
+ force: true,
373
+ filter: (src) => {
374
+ const leaf = src.split(/[\\/]/).pop();
375
+ return !['node_modules', '.git', '__pycache__'].includes(leaf || '') && !src.endsWith('.pyc');
376
+ },
377
+ });
378
+
379
+ const values: any = {
380
+ name,
381
+ title: skill.title || name,
382
+ description: skill.description || '',
383
+ instructions: skill.instructions || '',
384
+ language,
385
+ codeTemplate: skill.codeTemplate,
386
+ inputSchema: stringifyJsonText(skill.inputSchema || { type: 'object', properties: {} }),
387
+ packages: stringifyJsonText(skill.packages || [], []),
388
+ timeoutSeconds: skill.timeoutSeconds || 60,
389
+ maxOutputSizeMb: skill.maxOutputSizeMb || 50,
390
+ enabled: skill.enabled !== false,
391
+ toolScope: skill.toolScope || 'CUSTOM',
392
+ autoCall: !!skill.autoCall,
393
+ storageType: 'local',
394
+ storageUrl: `local://generated/${name}`,
395
+ };
396
+
397
+ if (skill.interactionSchema) {
398
+ values.interactionSchema = stringifyJsonText(skill.interactionSchema);
399
+ }
400
+
401
+ const repo = (this as any).app.db.getRepository('skillDefinitions');
402
+ const existing = await repo.findOne({ filter: { name } });
403
+ if (existing) {
404
+ if (manifest.overwrite === false) {
405
+ throw new Error(`Skill "${name}" already exists and overwrite=false.`);
406
+ }
407
+ await repo.update({ filter: { name }, values });
408
+ return `[skill-hub] Updated generated skill "${name}" in Skill Hub.`;
409
+ }
410
+
411
+ await repo.create({ values });
412
+ return `[skill-hub] Installed generated skill "${name}" in Skill Hub.`;
413
+ }
414
+
415
+ private async validateGeneratedSkillPackages(name: string, language: 'python' | 'node', packages: any) {
416
+ if (!Array.isArray(packages) || packages.length === 0) return;
417
+ if (!packages.every((pkg) => typeof pkg === 'string' && pkg.trim())) {
418
+ throw new Error(`Generated skill "${name}" has invalid packages. Use an array of package names.`);
419
+ }
420
+
421
+ const workerConfig = await (this as any).app.db.getRepository('skillWorkerConfigs').findOne();
422
+ if (!workerConfig) return;
423
+
424
+ const whitelist = parseJsonText(
425
+ workerConfig.get ? workerConfig.get('packageWhitelist') : workerConfig.packageWhitelist,
426
+ null,
427
+ );
428
+ const allowed = whitelist?.[language];
429
+ if (!Array.isArray(allowed) || allowed.length === 0) return;
430
+
431
+ const allowedSet = new Set(allowed.map((pkg: string) => pkg.toLowerCase()));
432
+ const missing = packages
433
+ .map((pkg: string) => pkg.trim())
434
+ .filter((pkg: string) => !allowedSet.has(pkg.toLowerCase()));
435
+
436
+ if (missing.length > 0) {
437
+ throw new Error(
438
+ `Generated skill "${name}" requires ${language} package(s) not available in the Skill Hub worker environment: ` +
439
+ `${missing.join(', ')}. Add them to the worker environment and refresh/init Skill Hub before installing this skill.`,
440
+ );
441
+ }
442
+ }
297
443
  }