flowmind 1.4.8 → 1.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -36,6 +36,7 @@ class LearningEngine {
36
36
  this.writeQueue = new WriteQueue();
37
37
  this.records = {};
38
38
  this.skillBindings = {};
39
+ this.resourceBindings = {};
39
40
  this.stats = {};
40
41
  this.initialized = false;
41
42
  }
@@ -55,6 +56,7 @@ class LearningEngine {
55
56
 
56
57
  // Load existing data
57
58
  await this.loadSkillBindings();
59
+ await this.loadResourceBindings();
58
60
  await this.loadStats();
59
61
 
60
62
  this.initialized = true;
@@ -309,6 +311,172 @@ class LearningEngine {
309
311
  return {};
310
312
  }
311
313
 
314
+ /**
315
+ * Save a business/resource binding for later reuse.
316
+ */
317
+ async saveResourceBinding(binding, context = {}) {
318
+ const normalized = {
319
+ id: binding.id || `bind-${Date.now()}-${uuidv4().slice(0, 8)}`,
320
+ timestamp: binding.timestamp || new Date().toISOString(),
321
+ business: binding.business || 'default',
322
+ aliases: [...new Set([binding.business, ...(binding.aliases || [])].filter(Boolean))],
323
+ componentType: binding.componentType || null,
324
+ provider: binding.provider || null,
325
+ mcpServer: binding.mcpServer || null,
326
+ skill: binding.skill || context.currentSkill || 'resource-bind',
327
+ source: binding.source || 'manual',
328
+ keywords: [...new Set(binding.keywords || this.extractKeywords(binding.business || ''))],
329
+ connection: { ...(binding.connection || {}) },
330
+ metadata: { ...(binding.metadata || {}) },
331
+ stats: {
332
+ useCount: binding.stats?.useCount || 0,
333
+ lastUsed: binding.stats?.lastUsed || null
334
+ }
335
+ };
336
+
337
+ await this.writeQueue.run('resource-bindings', async () => {
338
+ this.resourceBindings.bindings = this.resourceBindings.bindings || [];
339
+ const index = this.resourceBindings.bindings.findIndex((item) => item.id === normalized.id
340
+ || (item.business === normalized.business
341
+ && item.componentType === normalized.componentType
342
+ && item.provider === normalized.provider));
343
+
344
+ if (index >= 0) {
345
+ const previous = this.resourceBindings.bindings[index];
346
+ this.resourceBindings.bindings[index] = {
347
+ ...previous,
348
+ ...normalized,
349
+ aliases: [...new Set([...(previous.aliases || []), ...normalized.aliases])],
350
+ keywords: [...new Set([...(previous.keywords || []), ...normalized.keywords])],
351
+ connection: { ...(previous.connection || {}), ...(normalized.connection || {}) },
352
+ metadata: { ...(previous.metadata || {}), ...(normalized.metadata || {}) },
353
+ timestamp: normalized.timestamp
354
+ };
355
+ normalized.id = this.resourceBindings.bindings[index].id;
356
+ } else {
357
+ this.resourceBindings.bindings.push(normalized);
358
+ }
359
+
360
+ this.resourceBindings.lastUpdated = new Date().toISOString();
361
+ await this.saveResourceBindings();
362
+ });
363
+
364
+ await this.updateStats('resource_binding', normalized.skill);
365
+
366
+ eventBus.emit('learning:recorded', {
367
+ type: 'resource_binding',
368
+ skill: normalized.skill,
369
+ recordId: normalized.id,
370
+ business: normalized.business,
371
+ componentType: normalized.componentType,
372
+ provider: normalized.provider
373
+ });
374
+
375
+ return normalized;
376
+ }
377
+
378
+ /**
379
+ * Return all stored resource bindings.
380
+ */
381
+ async listResourceBindings() {
382
+ return this.resourceBindings.bindings || [];
383
+ }
384
+
385
+ /**
386
+ * Find the best-matching binding for an input and skill context.
387
+ */
388
+ async matchResourceBinding(input, skillName = null) {
389
+ const bindings = this.resourceBindings.bindings || [];
390
+ if (!bindings.length) return null;
391
+
392
+ const componentType = this.inferComponentType(input, skillName);
393
+ const normalized = String(input || '').toLowerCase();
394
+ const preferredBindingId = componentType
395
+ ? this.skillBindings.bindings?.[skillName]?.preferredResourceBindings?.[componentType] || null
396
+ : null;
397
+
398
+ let best = null;
399
+ let bestScore = 0;
400
+
401
+ for (const binding of bindings) {
402
+ let score = 0;
403
+
404
+ if (componentType && binding.componentType === componentType) {
405
+ score += 4;
406
+ }
407
+
408
+ if (binding.provider && normalized.includes(String(binding.provider).toLowerCase())) {
409
+ score += 3;
410
+ }
411
+
412
+ if (binding.mcpServer && normalized.includes(String(binding.mcpServer).toLowerCase())) {
413
+ score += 3;
414
+ }
415
+
416
+ if (preferredBindingId && binding.id === preferredBindingId) {
417
+ score += 8;
418
+ }
419
+
420
+ for (const alias of binding.aliases || []) {
421
+ if (alias && normalized.includes(String(alias).toLowerCase())) {
422
+ score += 6;
423
+ }
424
+ }
425
+
426
+ for (const keyword of binding.keywords || []) {
427
+ if (keyword && normalized.includes(String(keyword).toLowerCase())) {
428
+ score += 1;
429
+ }
430
+ }
431
+
432
+ if (!best || score > bestScore) {
433
+ best = binding;
434
+ bestScore = score;
435
+ }
436
+ }
437
+
438
+ if (!best) return null;
439
+
440
+ if (bestScore === 0) {
441
+ const sameComponent = componentType
442
+ ? bindings.filter((binding) => binding.componentType === componentType)
443
+ : [];
444
+ return sameComponent.length === 1 ? sameComponent[0] : null;
445
+ }
446
+
447
+ return best;
448
+ }
449
+
450
+ /**
451
+ * Mark a binding as used.
452
+ */
453
+ async markResourceBindingUsed(bindingId, context = {}) {
454
+ if (!bindingId) return;
455
+
456
+ let updatedBinding = null;
457
+ await this.writeQueue.run('resource-bindings', async () => {
458
+ const bindings = this.resourceBindings.bindings || [];
459
+ const index = bindings.findIndex((binding) => binding.id === bindingId);
460
+ if (index < 0) return;
461
+
462
+ const current = bindings[index];
463
+ updatedBinding = {
464
+ ...current,
465
+ stats: {
466
+ useCount: (current.stats?.useCount || 0) + 1,
467
+ lastUsed: new Date().toISOString()
468
+ }
469
+ };
470
+ bindings[index] = updatedBinding;
471
+ this.resourceBindings.lastUpdated = new Date().toISOString();
472
+ await this.saveResourceBindings();
473
+ });
474
+
475
+ if (updatedBinding) {
476
+ await this.promoteResourceBindingPreference(updatedBinding, context);
477
+ }
478
+ }
479
+
312
480
  /**
313
481
  * Save learning record
314
482
  */
@@ -373,6 +541,191 @@ class LearningEngine {
373
541
  });
374
542
  }
375
543
 
544
+ async saveAutoGeneratedRecord(record, skill = record.skill || '_global') {
545
+ const recordPath = path.join(
546
+ this.expandPath(this.learningPath),
547
+ 'records',
548
+ skill,
549
+ `${record.id}.json`
550
+ );
551
+
552
+ const existed = await fs.pathExists(recordPath);
553
+ await fs.ensureDir(path.dirname(recordPath));
554
+ await fs.writeJson(recordPath, record, { spaces: 2 });
555
+
556
+ if (!this.records[skill]) {
557
+ this.records[skill] = [];
558
+ }
559
+ const index = this.records[skill].findIndex((item) => item.id === record.id);
560
+ if (index >= 0) {
561
+ this.records[skill][index] = record;
562
+ } else {
563
+ this.records[skill].push(record);
564
+ }
565
+
566
+ return { created: !existed, path: recordPath };
567
+ }
568
+
569
+ async savePassivePreferences(skillName, updates) {
570
+ const prefsPath = path.join(
571
+ this.expandPath(this.learningPath),
572
+ 'records',
573
+ skillName,
574
+ 'preferences.json'
575
+ );
576
+ const queueKey = `prefs:${skillName}`;
577
+ await this.writeQueue.run(queueKey, async () => {
578
+ let prefs = {};
579
+ if (await fs.pathExists(prefsPath)) {
580
+ prefs = await fs.readJson(prefsPath);
581
+ }
582
+
583
+ const next = typeof updates === 'function' ? updates(prefs) : { ...prefs, ...updates };
584
+ next.lastUpdated = new Date().toISOString();
585
+
586
+ await fs.ensureDir(path.dirname(prefsPath));
587
+ await fs.writeJson(prefsPath, next, { spaces: 2 });
588
+ });
589
+ }
590
+
591
+ getAutoPromoteConfig() {
592
+ return this.config.get('learning.autoPromote', {
593
+ enabled: true,
594
+ resourceBindingMinUses: 3,
595
+ sceneMinUses: 2
596
+ });
597
+ }
598
+
599
+ async promoteResourceBindingPreference(binding, context = {}) {
600
+ const config = this.getAutoPromoteConfig();
601
+ const skillName = context.skillName || context.currentSkill || binding.skill || null;
602
+ if (!config.enabled || !skillName || !binding.componentType) {
603
+ return false;
604
+ }
605
+
606
+ if ((binding.stats?.useCount || 0) < config.resourceBindingMinUses) {
607
+ return false;
608
+ }
609
+
610
+ this.skillBindings.bindings = this.skillBindings.bindings || {};
611
+ const skillBinding = this.skillBindings.bindings[skillName] || {
612
+ learningCount: 0,
613
+ records: [],
614
+ rules: [],
615
+ preferredResourceBindings: {}
616
+ };
617
+ skillBinding.preferredResourceBindings = skillBinding.preferredResourceBindings || {};
618
+
619
+ const existingPreferred = skillBinding.preferredResourceBindings[binding.componentType];
620
+ const changed = existingPreferred !== binding.id;
621
+ if (changed) {
622
+ skillBinding.preferredResourceBindings[binding.componentType] = binding.id;
623
+ skillBinding.lastLearning = new Date().toISOString();
624
+ this.skillBindings.bindings[skillName] = skillBinding;
625
+ await this.saveSkillBindings();
626
+ }
627
+
628
+ await this.savePassivePreferences(skillName, (prefs) => ({
629
+ ...prefs,
630
+ preferredResourceBindings: {
631
+ ...(prefs.preferredResourceBindings || {}),
632
+ [binding.componentType]: {
633
+ bindingId: binding.id,
634
+ business: binding.business,
635
+ provider: binding.provider,
636
+ componentType: binding.componentType,
637
+ useCount: binding.stats?.useCount || 0,
638
+ lastUsed: binding.stats?.lastUsed || null
639
+ }
640
+ }
641
+ }));
642
+
643
+ const record = {
644
+ id: `auto-resource-${skillName}-${binding.componentType}`,
645
+ timestamp: new Date().toISOString(),
646
+ type: 'resource_preference',
647
+ source: 'passive',
648
+ autoGenerated: true,
649
+ input: context.input || binding.metadata?.input || binding.business,
650
+ skill: skillName,
651
+ componentType: binding.componentType,
652
+ business: binding.business,
653
+ provider: binding.provider,
654
+ bindingId: binding.id,
655
+ useCount: binding.stats?.useCount || 0,
656
+ lastUsed: binding.stats?.lastUsed || null,
657
+ application: {
658
+ condition: `When ${skillName} resolves ${binding.componentType}`,
659
+ action: `Prefer resource binding ${binding.business}`,
660
+ priority: 'medium'
661
+ }
662
+ };
663
+ const saved = await this.saveAutoGeneratedRecord(record, skillName);
664
+ if (saved.created) {
665
+ await this.updateStats('resource_preference', skillName);
666
+ }
667
+
668
+ return changed || saved.created;
669
+ }
670
+
671
+ async promoteSceneUsage(scene, context = {}) {
672
+ const config = this.getAutoPromoteConfig();
673
+ if (!config.enabled || !scene?.id) {
674
+ return false;
675
+ }
676
+
677
+ if ((scene.stats?.useCount || 0) < config.sceneMinUses) {
678
+ return false;
679
+ }
680
+
681
+ const primarySkill = scene.workflow?.skills?.[0]?.skill || 'global';
682
+ const record = {
683
+ id: `auto-scene-${scene.id}`,
684
+ timestamp: new Date().toISOString(),
685
+ type: 'scene_preference',
686
+ source: 'passive',
687
+ autoGenerated: true,
688
+ input: context.input || scene.name,
689
+ skill: '_global',
690
+ sceneId: scene.id,
691
+ sceneName: scene.name,
692
+ primarySkill,
693
+ keywords: scene.keywords || [],
694
+ workflow: scene.workflow || {},
695
+ preferences: scene.preferences || {},
696
+ stats: {
697
+ useCount: scene.stats?.useCount || 0,
698
+ lastUsed: scene.stats?.lastUsed || null,
699
+ successRate: scene.stats?.successRate || 1.0
700
+ },
701
+ application: {
702
+ condition: `When request matches scene ${scene.name}`,
703
+ action: `Apply workflow starting with ${primarySkill}`,
704
+ priority: 'medium'
705
+ }
706
+ };
707
+
708
+ const saved = await this.saveAutoGeneratedRecord(record, '_global');
709
+ if (saved.created) {
710
+ await this.updateStats('scene_preference', 'global');
711
+ }
712
+
713
+ await this.savePassivePreferences('_global', (prefs) => ({
714
+ ...prefs,
715
+ preferredScenes: {
716
+ ...(prefs.preferredScenes || {}),
717
+ [scene.id]: {
718
+ name: scene.name,
719
+ primarySkill,
720
+ useCount: scene.stats?.useCount || 0,
721
+ successRate: scene.stats?.successRate || 1.0
722
+ }
723
+ }
724
+ }));
725
+
726
+ return saved.created;
727
+ }
728
+
376
729
  /**
377
730
  * Update skill bindings
378
731
  */
@@ -415,6 +768,20 @@ class LearningEngine {
415
768
  }
416
769
  }
417
770
 
771
+ /**
772
+ * Load resource bindings
773
+ */
774
+ async loadResourceBindings() {
775
+ const bindingsPath = path.join(this.expandPath(this.learningPath), 'resource-bindings.json');
776
+
777
+ if (await fs.pathExists(bindingsPath)) {
778
+ this.resourceBindings = await fs.readJson(bindingsPath);
779
+ this.resourceBindings.bindings = this.resourceBindings.bindings || [];
780
+ } else {
781
+ this.resourceBindings = { version: '1.0', bindings: [] };
782
+ }
783
+ }
784
+
418
785
  /**
419
786
  * Save skill bindings
420
787
  */
@@ -426,6 +793,15 @@ class LearningEngine {
426
793
  });
427
794
  }
428
795
 
796
+ /**
797
+ * Save resource bindings
798
+ */
799
+ async saveResourceBindings() {
800
+ const bindingsPath = path.join(this.expandPath(this.learningPath), 'resource-bindings.json');
801
+ this.resourceBindings.lastUpdated = new Date().toISOString();
802
+ await fs.writeJson(bindingsPath, this.resourceBindings, { spaces: 2 });
803
+ }
804
+
429
805
  /**
430
806
  * Load stats
431
807
  */
@@ -480,6 +856,7 @@ class LearningEngine {
480
856
  exportedAt: new Date().toISOString(),
481
857
  stats: this.stats,
482
858
  skillBindings: this.skillBindings,
859
+ resourceBindings: this.resourceBindings,
483
860
  records: this.records
484
861
  };
485
862
 
@@ -513,8 +890,23 @@ class LearningEngine {
513
890
  }
514
891
  }
515
892
 
893
+ // Merge resource bindings
894
+ const existing = this.resourceBindings.bindings || [];
895
+ const incoming = data.resourceBindings?.bindings || [];
896
+ for (const binding of incoming) {
897
+ const match = existing.find((item) => item.id === binding.id
898
+ || (item.business === binding.business
899
+ && item.componentType === binding.componentType
900
+ && item.provider === binding.provider));
901
+ if (!match) {
902
+ existing.push(binding);
903
+ }
904
+ }
905
+ this.resourceBindings.bindings = existing;
906
+
516
907
  // Save
517
908
  await this.saveSkillBindings();
909
+ await this.saveResourceBindings();
518
910
  await this.saveStats();
519
911
 
520
912
  return { success: true, imported: data.stats.totalRecords };
@@ -619,6 +1011,20 @@ class LearningEngine {
619
1011
  return [...new Set(words)].slice(0, 10);
620
1012
  }
621
1013
 
1014
+ inferComponentType(input, skillName = null) {
1015
+ const normalized = `${skillName || ''} ${input || ''}`.toLowerCase();
1016
+
1017
+ if (/yapi|api 文档|api doc|swagger|接口/.test(normalized)) return 'apiDoc';
1018
+ if (/yuque|语雀|knowledge base|知识库|design/.test(normalized)) return 'knowledgeBase';
1019
+ if (/redis|缓存|cache/.test(normalized)) return 'redisMonitor';
1020
+ if (/sql|数据库|database|rds|dms/.test(normalized)) return 'databaseQuery';
1021
+ if (/log|日志|trace|sls/.test(normalized)) return 'logService';
1022
+ if (/pipeline|workflow|部署|流水线/.test(normalized)) return 'workflow';
1023
+ if (/report|coverage|报告/.test(normalized)) return 'report';
1024
+
1025
+ return null;
1026
+ }
1027
+
622
1028
  extractWorkflow(input) {
623
1029
  // Extract workflow steps
624
1030
  return {
@@ -65,6 +65,14 @@ class FridayFlowAdapter extends WorkflowAdapter {
65
65
  };
66
66
  }
67
67
 
68
+ async startBatchPipelineRun(params) {
69
+ return {
70
+ mcpServer: this.mcpServer,
71
+ tool: this.resolveTool('startBatchPipelineRun'),
72
+ params: params || {}
73
+ };
74
+ }
75
+
68
76
  async getPipelineRun(pipelineId, runId) {
69
77
  return {
70
78
  mcpServer: this.mcpServer,
@@ -239,7 +239,7 @@ class SceneMatcher {
239
239
  /**
240
240
  * Update scene usage stats
241
241
  */
242
- async updateSceneStats(sceneId, success) {
242
+ async updateSceneStats(sceneId, success, context = {}) {
243
243
  const scene = this.scenes.find(s => s.id === sceneId);
244
244
  if (!scene) return;
245
245
 
@@ -254,6 +254,10 @@ class SceneMatcher {
254
254
 
255
255
  // Save updated scenes
256
256
  await this.saveScenes();
257
+
258
+ if (success && this.learning?.promoteSceneUsage) {
259
+ await this.learning.promoteSceneUsage(scene, context);
260
+ }
257
261
  }
258
262
 
259
263
  /**