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.
- package/dist/externalVersion.js +6 -6
- package/dist/server/collections/agent-execution-spans.d.ts +1 -1
- package/dist/server/collections/orchestrator-config.d.ts +1 -1
- package/dist/server/collections/orchestrator-logs.d.ts +1 -1
- package/dist/server/collections/skill-definitions.d.ts +0 -1
- package/dist/server/collections/skill-executions.d.ts +0 -1
- package/dist/server/collections/skill-worker-configs.d.ts +0 -1
- package/dist/server/services/CodeValidator.js +1 -0
- package/dist/server/skill-hub/tasks/SkillExecutionTask.d.ts +2 -0
- package/dist/server/skill-hub/tasks/SkillExecutionTask.js +122 -0
- package/package.json +1 -1
- package/src/client/index.tsx +1 -1
- package/src/client/plugin.tsx +54 -54
- package/src/client/skill-hub/index.tsx +75 -75
- package/src/server/migrations/20260423000000-add-progress-fields.ts +5 -5
- package/src/server/migrations/20260425000000-add-interaction-schema.ts +5 -5
- package/src/server/migrations/20260427000000-add-tracing-detail-fields.ts +5 -5
- package/src/server/migrations/20260427000000-change-packages-to-text.ts +7 -7
- package/src/server/migrations/20260427000001-change-other-json-to-text.ts +10 -10
- package/src/server/migrations/20260429000000-add-llm-fields.ts +2 -2
- package/src/server/migrations/20260429000000-fix-inputargs-json-to-text.ts +2 -2
- package/src/server/migrations/20260503000000-add-orchestrator-trace-fields.ts +2 -2
- package/src/server/plugin.ts +94 -94
- package/src/server/services/CodeValidator.ts +5 -5
- package/src/server/services/SkillManager.ts +1 -1
- package/src/server/services/WorkerEnvManager.ts +5 -5
- package/src/server/skill-hub/plugin.ts +58 -58
- 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 {
|
|
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
|
}
|