wu-framework 1.1.8 → 1.1.9
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/LICENSE +19 -1
- package/README.md +227 -626
- package/dist/wu-framework.cjs.js +1 -1
- package/dist/wu-framework.cjs.js.map +1 -1
- package/dist/wu-framework.dev.js +2988 -1076
- package/dist/wu-framework.dev.js.map +1 -1
- package/dist/wu-framework.esm.js +1 -1
- package/dist/wu-framework.esm.js.map +1 -1
- package/dist/wu-framework.umd.js +1 -1
- package/dist/wu-framework.umd.js.map +1 -1
- package/package.json +10 -4
- package/src/adapters/react/index.js +51 -46
- package/src/ai/wu-ai-agent.js +546 -0
- package/src/ai/wu-ai-browser-primitives.js +354 -0
- package/src/ai/wu-ai-browser.js +29 -312
- package/src/ai/wu-ai-conversation.js +143 -84
- package/src/ai/wu-ai-orchestrate.js +1021 -0
- package/src/ai/wu-ai-provider.js +105 -10
- package/src/ai/wu-ai.js +338 -8
- package/src/core/wu-cache.js +1 -2
- package/src/core/wu-core.js +3 -4
- package/src/core/wu-mcp-bridge.js +198 -414
- package/src/core/wu-plugin.js +4 -1
- package/src/core/wu-style-bridge.js +23 -21
- package/src/index.js +25 -2
package/dist/wu-framework.dev.js
CHANGED
|
@@ -271,6 +271,142 @@ class WuLoader {
|
|
|
271
271
|
}
|
|
272
272
|
}
|
|
273
273
|
|
|
274
|
+
/**
|
|
275
|
+
* 📝 WU-LOGGER: Sistema de logging inteligente para entornos
|
|
276
|
+
* Controla los logs automáticamente según el entorno
|
|
277
|
+
*/
|
|
278
|
+
|
|
279
|
+
class WuLogger {
|
|
280
|
+
constructor() {
|
|
281
|
+
// Detectar entorno automáticamente
|
|
282
|
+
this.isDevelopment = this.detectEnvironment();
|
|
283
|
+
// En desarrollo: warn (menos ruido), en producción: error
|
|
284
|
+
this.logLevel = this.isDevelopment ? 'warn' : 'error';
|
|
285
|
+
|
|
286
|
+
this.levels = {
|
|
287
|
+
debug: 0,
|
|
288
|
+
info: 1,
|
|
289
|
+
warn: 2,
|
|
290
|
+
error: 3,
|
|
291
|
+
silent: 4
|
|
292
|
+
};
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/**
|
|
296
|
+
* Detectar si estamos en desarrollo
|
|
297
|
+
*/
|
|
298
|
+
detectEnvironment() {
|
|
299
|
+
// Múltiples formas de detectar desarrollo
|
|
300
|
+
return (
|
|
301
|
+
// Vite development
|
|
302
|
+
window.location.hostname === 'localhost' ||
|
|
303
|
+
window.location.hostname === '127.0.0.1' ||
|
|
304
|
+
window.location.port !== '' ||
|
|
305
|
+
// NODE_ENV si está disponible
|
|
306
|
+
(typeof process !== 'undefined' && process.env?.NODE_ENV === 'development') ||
|
|
307
|
+
// URL params para forzar debug
|
|
308
|
+
new URLSearchParams(window.location.search).has('wu-debug') ||
|
|
309
|
+
// Manual override
|
|
310
|
+
window.WU_DEBUG === true
|
|
311
|
+
);
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
/**
|
|
315
|
+
* Configurar nivel de logging
|
|
316
|
+
*/
|
|
317
|
+
setLevel(level) {
|
|
318
|
+
this.logLevel = level;
|
|
319
|
+
return this;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
/**
|
|
323
|
+
* Habilitar/deshabilitar development mode
|
|
324
|
+
*/
|
|
325
|
+
setDevelopment(isDev) {
|
|
326
|
+
this.isDevelopment = isDev;
|
|
327
|
+
this.logLevel = isDev ? 'debug' : 'error';
|
|
328
|
+
return this;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
/**
|
|
332
|
+
* Verificar si debemos mostrar el log
|
|
333
|
+
*/
|
|
334
|
+
shouldLog(level) {
|
|
335
|
+
return this.levels[level] >= this.levels[this.logLevel];
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
/**
|
|
339
|
+
* Logging methods
|
|
340
|
+
*/
|
|
341
|
+
debug(...args) {
|
|
342
|
+
if (this.shouldLog('debug')) {
|
|
343
|
+
console.log(...args);
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
info(...args) {
|
|
348
|
+
if (this.shouldLog('info')) {
|
|
349
|
+
console.info(...args);
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
warn(...args) {
|
|
354
|
+
if (this.shouldLog('warn')) {
|
|
355
|
+
console.warn(...args);
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
error(...args) {
|
|
360
|
+
if (this.shouldLog('error')) {
|
|
361
|
+
console.error(...args);
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
/**
|
|
366
|
+
* Logging con contexto Wu
|
|
367
|
+
*/
|
|
368
|
+
wu(level, ...args) {
|
|
369
|
+
if (this.shouldLog(level)) {
|
|
370
|
+
const method = level === 'debug' ? 'log' : level;
|
|
371
|
+
console[method]('[Wu]', ...args);
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
/**
|
|
376
|
+
* Helper methods específicos para Wu
|
|
377
|
+
*/
|
|
378
|
+
wuDebug(...args) { this.wu('debug', ...args); }
|
|
379
|
+
wuInfo(...args) { this.wu('info', ...args); }
|
|
380
|
+
wuWarn(...args) { this.wu('warn', ...args); }
|
|
381
|
+
wuError(...args) { this.wu('error', ...args); }
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
// Singleton instance
|
|
385
|
+
const logger = new WuLogger();
|
|
386
|
+
|
|
387
|
+
/**
|
|
388
|
+
* 🔇 Silenciar todos los logs de Wu Framework
|
|
389
|
+
* Útil en producción para eliminar todo el ruido
|
|
390
|
+
*/
|
|
391
|
+
function silenceAllLogs() {
|
|
392
|
+
logger.setLevel('silent');
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
/**
|
|
396
|
+
* 🔊 Restaurar logs (nivel debug)
|
|
397
|
+
*/
|
|
398
|
+
function enableAllLogs() {
|
|
399
|
+
logger.setLevel('debug');
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
var wuLogger = /*#__PURE__*/Object.freeze({
|
|
403
|
+
__proto__: null,
|
|
404
|
+
WuLogger: WuLogger,
|
|
405
|
+
enableAllLogs: enableAllLogs,
|
|
406
|
+
logger: logger,
|
|
407
|
+
silenceAllLogs: silenceAllLogs
|
|
408
|
+
});
|
|
409
|
+
|
|
274
410
|
/**
|
|
275
411
|
* 🎨 WU-STYLE-BRIDGE: SHADOW DOM STYLE SHARING SYSTEM
|
|
276
412
|
*
|
|
@@ -301,6 +437,7 @@ class WuLoader {
|
|
|
301
437
|
* - Riesgo de colisiones: NINGUNO
|
|
302
438
|
*/
|
|
303
439
|
|
|
440
|
+
|
|
304
441
|
class WuStyleBridge {
|
|
305
442
|
constructor() {
|
|
306
443
|
this.styleObserver = null;
|
|
@@ -329,7 +466,7 @@ class WuStyleBridge {
|
|
|
329
466
|
cacheEnabled: true
|
|
330
467
|
};
|
|
331
468
|
|
|
332
|
-
|
|
469
|
+
logger.debug('[WuStyleBridge] 🎨 Style sharing system initialized');
|
|
333
470
|
}
|
|
334
471
|
|
|
335
472
|
/**
|
|
@@ -339,7 +476,7 @@ class WuStyleBridge {
|
|
|
339
476
|
*/
|
|
340
477
|
registerFullyIsolatedApp(appName, appUrl) {
|
|
341
478
|
this.fullyIsolatedApps.set(appName, appUrl);
|
|
342
|
-
|
|
479
|
+
logger.debug(`[WuStyleBridge] 🛡️ Registered fully-isolated app: ${appName} (${appUrl})`);
|
|
343
480
|
}
|
|
344
481
|
|
|
345
482
|
/**
|
|
@@ -437,7 +574,7 @@ class WuStyleBridge {
|
|
|
437
574
|
|
|
438
575
|
// Filtrar estilos de apps con fully-isolated (después de obtener viteId para mejor detección)
|
|
439
576
|
if (this.isStyleFromFullyIsolatedApp(style) || (viteId && this.isStyleFromFullyIsolatedApp(viteId))) {
|
|
440
|
-
|
|
577
|
+
logger.debug(`[WuStyleBridge] 🛡️ Filtered out style from fully-isolated app: ${viteId || 'unknown'}`);
|
|
441
578
|
return;
|
|
442
579
|
}
|
|
443
580
|
|
|
@@ -465,7 +602,7 @@ class WuStyleBridge {
|
|
|
465
602
|
});
|
|
466
603
|
}
|
|
467
604
|
|
|
468
|
-
|
|
605
|
+
logger.debug(`[WuStyleBridge] 🔍 Detected ${styles.length} shareable styles`);
|
|
469
606
|
return styles;
|
|
470
607
|
}
|
|
471
608
|
|
|
@@ -521,26 +658,26 @@ class WuStyleBridge {
|
|
|
521
658
|
*/
|
|
522
659
|
async injectStylesIntoShadow(shadowRoot, appName, styleMode) {
|
|
523
660
|
if (!shadowRoot) {
|
|
524
|
-
|
|
661
|
+
logger.warn('[WuStyleBridge] ⚠️ No shadow root provided');
|
|
525
662
|
return 0;
|
|
526
663
|
}
|
|
527
664
|
|
|
528
665
|
// 🛡️ MODO FULLY-ISOLATED: No inyectar ningún estilo compartido
|
|
529
666
|
// Los estilos propios se manejan en wu-sandbox.js con injectOwnStylesToShadow
|
|
530
667
|
if (styleMode === 'fully-isolated') {
|
|
531
|
-
|
|
668
|
+
logger.debug(`[WuStyleBridge] 🛡️ Style mode "fully-isolated" for ${appName}, skipping shared style injection`);
|
|
532
669
|
return 0;
|
|
533
670
|
}
|
|
534
671
|
|
|
535
672
|
// 🔒 MODO ISOLATED: No inyectar estilos externos - usar encapsulamiento nativo de Shadow DOM
|
|
536
673
|
// La app debe manejar sus propios estilos (CSS-in-JS, scoped styles, imports directos)
|
|
537
674
|
if (styleMode === 'isolated') {
|
|
538
|
-
|
|
675
|
+
logger.debug(`[WuStyleBridge] 🔒 Style mode "isolated" for ${appName}, using native Shadow DOM encapsulation (no external styles)`);
|
|
539
676
|
return 0;
|
|
540
677
|
}
|
|
541
678
|
|
|
542
679
|
// 🌐 MODO SHARED (default): Inyectar todos los estilos compartidos del documento
|
|
543
|
-
|
|
680
|
+
logger.debug(`[WuStyleBridge] 🌐 Style mode "shared" for ${appName}, injecting all shared styles...`);
|
|
544
681
|
|
|
545
682
|
// Detectar estilos del documento
|
|
546
683
|
const styles = this.detectDocumentStyles();
|
|
@@ -566,11 +703,11 @@ class WuStyleBridge {
|
|
|
566
703
|
break;
|
|
567
704
|
}
|
|
568
705
|
} catch (error) {
|
|
569
|
-
|
|
706
|
+
logger.warn(`[WuStyleBridge] ⚠️ Failed to inject style:`, error);
|
|
570
707
|
}
|
|
571
708
|
}
|
|
572
709
|
|
|
573
|
-
|
|
710
|
+
logger.debug(`[WuStyleBridge] ✅ Injected ${injectedCount} shared styles into ${appName}`);
|
|
574
711
|
return injectedCount;
|
|
575
712
|
}
|
|
576
713
|
|
|
@@ -583,7 +720,7 @@ class WuStyleBridge {
|
|
|
583
720
|
// Verificar si ya existe
|
|
584
721
|
const existing = shadowRoot.querySelector(`link[href="${style.href}"]`);
|
|
585
722
|
if (existing) {
|
|
586
|
-
|
|
723
|
+
logger.debug(`[WuStyleBridge] ⏭️ Style already exists: ${style.library || style.href}`);
|
|
587
724
|
return;
|
|
588
725
|
}
|
|
589
726
|
|
|
@@ -597,7 +734,7 @@ class WuStyleBridge {
|
|
|
597
734
|
// Insertar al principio del shadow root (antes de otros estilos)
|
|
598
735
|
shadowRoot.insertBefore(link, shadowRoot.firstChild);
|
|
599
736
|
|
|
600
|
-
|
|
737
|
+
logger.debug(`[WuStyleBridge] 🔗 Injected link: ${style.library || style.href}`);
|
|
601
738
|
}
|
|
602
739
|
|
|
603
740
|
/**
|
|
@@ -611,7 +748,7 @@ class WuStyleBridge {
|
|
|
611
748
|
if (viteId) {
|
|
612
749
|
const existing = shadowRoot.querySelector(`style[data-wu-vite-id="${viteId}"]`);
|
|
613
750
|
if (existing) {
|
|
614
|
-
|
|
751
|
+
logger.debug(`[WuStyleBridge] ⏭️ Inline style already exists: ${viteId}`);
|
|
615
752
|
return;
|
|
616
753
|
}
|
|
617
754
|
}
|
|
@@ -628,7 +765,7 @@ class WuStyleBridge {
|
|
|
628
765
|
// Insertar al principio del shadow root
|
|
629
766
|
shadowRoot.insertBefore(styleTag, shadowRoot.firstChild);
|
|
630
767
|
|
|
631
|
-
|
|
768
|
+
logger.debug(`[WuStyleBridge] 📝 Injected inline style: ${style.library || viteId}`);
|
|
632
769
|
}
|
|
633
770
|
|
|
634
771
|
/**
|
|
@@ -645,7 +782,7 @@ class WuStyleBridge {
|
|
|
645
782
|
|
|
646
783
|
// Verificar si ya existe
|
|
647
784
|
if (shadowRoot.adoptedStyleSheets.includes(style.sheet)) {
|
|
648
|
-
|
|
785
|
+
logger.debug(`[WuStyleBridge] ⏭️ Adopted stylesheet already exists`);
|
|
649
786
|
return;
|
|
650
787
|
}
|
|
651
788
|
|
|
@@ -654,9 +791,9 @@ class WuStyleBridge {
|
|
|
654
791
|
style.sheet
|
|
655
792
|
];
|
|
656
793
|
|
|
657
|
-
|
|
794
|
+
logger.debug(`[WuStyleBridge] 📋 Injected adopted stylesheet`);
|
|
658
795
|
} catch (error) {
|
|
659
|
-
|
|
796
|
+
logger.warn(`[WuStyleBridge] ⚠️ Failed to inject adopted stylesheet:`, error);
|
|
660
797
|
}
|
|
661
798
|
}
|
|
662
799
|
|
|
@@ -690,7 +827,7 @@ class WuStyleBridge {
|
|
|
690
827
|
}
|
|
691
828
|
|
|
692
829
|
if (hasStyleChanges && callback) {
|
|
693
|
-
|
|
830
|
+
logger.debug('[WuStyleBridge] 🔄 Style changes detected');
|
|
694
831
|
callback();
|
|
695
832
|
}
|
|
696
833
|
});
|
|
@@ -701,7 +838,7 @@ class WuStyleBridge {
|
|
|
701
838
|
subtree: true
|
|
702
839
|
});
|
|
703
840
|
|
|
704
|
-
|
|
841
|
+
logger.debug('[WuStyleBridge] 👀 Observing style changes');
|
|
705
842
|
}
|
|
706
843
|
|
|
707
844
|
/**
|
|
@@ -714,7 +851,7 @@ class WuStyleBridge {
|
|
|
714
851
|
...config
|
|
715
852
|
};
|
|
716
853
|
|
|
717
|
-
|
|
854
|
+
logger.debug('[WuStyleBridge] ⚙️ Configuration updated:', this.config);
|
|
718
855
|
}
|
|
719
856
|
|
|
720
857
|
/**
|
|
@@ -726,7 +863,7 @@ class WuStyleBridge {
|
|
|
726
863
|
this.styleObserver = null;
|
|
727
864
|
}
|
|
728
865
|
|
|
729
|
-
|
|
866
|
+
logger.debug('[WuStyleBridge] 🧹 StyleBridge cleaned up');
|
|
730
867
|
}
|
|
731
868
|
|
|
732
869
|
/**
|
|
@@ -748,206 +885,70 @@ class WuStyleBridge {
|
|
|
748
885
|
}
|
|
749
886
|
|
|
750
887
|
/**
|
|
751
|
-
*
|
|
752
|
-
*
|
|
888
|
+
* WU-PROXY-SANDBOX: Hardened JavaScript Isolation
|
|
889
|
+
*
|
|
890
|
+
* ES6 Proxy-based sandbox with side-effect tracking:
|
|
891
|
+
* - Timer hijacking (setTimeout, setInterval, requestAnimationFrame)
|
|
892
|
+
* - Event listener tracking (window + document addEventListener)
|
|
893
|
+
* - DOM scoping (querySelector/querySelectorAll → shadow root)
|
|
894
|
+
* - Storage scoping (localStorage/sessionStorage → prefixed keys)
|
|
895
|
+
*
|
|
896
|
+
* All tracked side effects are automatically cleaned up on deactivate().
|
|
753
897
|
*/
|
|
754
898
|
|
|
755
|
-
class WuLogger {
|
|
756
|
-
constructor() {
|
|
757
|
-
// Detectar entorno automáticamente
|
|
758
|
-
this.isDevelopment = this.detectEnvironment();
|
|
759
|
-
// En desarrollo: warn (menos ruido), en producción: error
|
|
760
|
-
this.logLevel = this.isDevelopment ? 'warn' : 'error';
|
|
761
899
|
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
}
|
|
900
|
+
class WuProxySandbox {
|
|
901
|
+
constructor(appName) {
|
|
902
|
+
this.appName = appName;
|
|
903
|
+
this.proxy = null;
|
|
904
|
+
this.fakeWindow = Object.create(null);
|
|
905
|
+
this.active = false;
|
|
906
|
+
this.modifiedKeys = new Set();
|
|
770
907
|
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
//
|
|
776
|
-
return (
|
|
777
|
-
// Vite development
|
|
778
|
-
window.location.hostname === 'localhost' ||
|
|
779
|
-
window.location.hostname === '127.0.0.1' ||
|
|
780
|
-
window.location.port !== '' ||
|
|
781
|
-
// NODE_ENV si está disponible
|
|
782
|
-
(typeof process !== 'undefined' && process.env?.NODE_ENV === 'development') ||
|
|
783
|
-
// URL params para forzar debug
|
|
784
|
-
new URLSearchParams(window.location.search).has('wu-debug') ||
|
|
785
|
-
// Manual override
|
|
786
|
-
window.WU_DEBUG === true
|
|
787
|
-
);
|
|
788
|
-
}
|
|
908
|
+
// --- Side-effect tracking ---
|
|
909
|
+
this._timers = new Set();
|
|
910
|
+
this._intervals = new Set();
|
|
911
|
+
this._rafs = new Set();
|
|
912
|
+
this._eventListeners = []; // [{target, event, handler, options}]
|
|
789
913
|
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
this.
|
|
795
|
-
|
|
796
|
-
}
|
|
914
|
+
// --- DOM & Storage scoping ---
|
|
915
|
+
this._container = null;
|
|
916
|
+
this._shadowRoot = null;
|
|
917
|
+
this._scopedDocument = null;
|
|
918
|
+
this._scopedLocalStorage = null;
|
|
919
|
+
this._scopedSessionStorage = null;
|
|
797
920
|
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
setDevelopment(isDev) {
|
|
802
|
-
this.isDevelopment = isDev;
|
|
803
|
-
this.logLevel = isDev ? 'debug' : 'error';
|
|
804
|
-
return this;
|
|
921
|
+
// --- Window patching state ---
|
|
922
|
+
this._patched = false;
|
|
923
|
+
this._originals = null;
|
|
805
924
|
}
|
|
806
925
|
|
|
807
926
|
/**
|
|
808
|
-
*
|
|
927
|
+
* Set the DOM scope for this sandbox.
|
|
928
|
+
* Must be called before activate() for DOM scoping to work.
|
|
929
|
+
* @param {HTMLElement} container - App container element
|
|
930
|
+
* @param {ShadowRoot} shadowRoot - Shadow root containing the container
|
|
809
931
|
*/
|
|
810
|
-
|
|
811
|
-
|
|
932
|
+
setContainer(container, shadowRoot) {
|
|
933
|
+
this._container = container;
|
|
934
|
+
this._shadowRoot = shadowRoot;
|
|
812
935
|
}
|
|
813
936
|
|
|
814
937
|
/**
|
|
815
|
-
*
|
|
938
|
+
* Activate the sandbox. Creates the Proxy and starts tracking.
|
|
939
|
+
* @returns {Proxy} The sandboxed window proxy
|
|
816
940
|
*/
|
|
817
|
-
|
|
818
|
-
if (this.
|
|
819
|
-
console.log(...args);
|
|
820
|
-
}
|
|
821
|
-
}
|
|
941
|
+
activate() {
|
|
942
|
+
if (this.active) return this.proxy;
|
|
822
943
|
|
|
823
|
-
|
|
824
|
-
if (this.shouldLog('info')) {
|
|
825
|
-
console.info(...args);
|
|
826
|
-
}
|
|
827
|
-
}
|
|
944
|
+
const self = this;
|
|
828
945
|
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
error(...args) {
|
|
836
|
-
if (this.shouldLog('error')) {
|
|
837
|
-
console.error(...args);
|
|
838
|
-
}
|
|
839
|
-
}
|
|
840
|
-
|
|
841
|
-
/**
|
|
842
|
-
* Logging con contexto Wu
|
|
843
|
-
*/
|
|
844
|
-
wu(level, ...args) {
|
|
845
|
-
if (this.shouldLog(level)) {
|
|
846
|
-
const method = level === 'debug' ? 'log' : level;
|
|
847
|
-
console[method]('[Wu]', ...args);
|
|
848
|
-
}
|
|
849
|
-
}
|
|
850
|
-
|
|
851
|
-
/**
|
|
852
|
-
* Helper methods específicos para Wu
|
|
853
|
-
*/
|
|
854
|
-
wuDebug(...args) { this.wu('debug', ...args); }
|
|
855
|
-
wuInfo(...args) { this.wu('info', ...args); }
|
|
856
|
-
wuWarn(...args) { this.wu('warn', ...args); }
|
|
857
|
-
wuError(...args) { this.wu('error', ...args); }
|
|
858
|
-
}
|
|
859
|
-
|
|
860
|
-
// Singleton instance
|
|
861
|
-
const logger = new WuLogger();
|
|
862
|
-
|
|
863
|
-
/**
|
|
864
|
-
* 🔇 Silenciar todos los logs de Wu Framework
|
|
865
|
-
* Útil en producción para eliminar todo el ruido
|
|
866
|
-
*/
|
|
867
|
-
function silenceAllLogs() {
|
|
868
|
-
logger.setLevel('silent');
|
|
869
|
-
}
|
|
870
|
-
|
|
871
|
-
/**
|
|
872
|
-
* 🔊 Restaurar logs (nivel debug)
|
|
873
|
-
*/
|
|
874
|
-
function enableAllLogs() {
|
|
875
|
-
logger.setLevel('debug');
|
|
876
|
-
}
|
|
877
|
-
|
|
878
|
-
var wuLogger = /*#__PURE__*/Object.freeze({
|
|
879
|
-
__proto__: null,
|
|
880
|
-
WuLogger: WuLogger,
|
|
881
|
-
enableAllLogs: enableAllLogs,
|
|
882
|
-
logger: logger,
|
|
883
|
-
silenceAllLogs: silenceAllLogs
|
|
884
|
-
});
|
|
885
|
-
|
|
886
|
-
/**
|
|
887
|
-
* WU-PROXY-SANDBOX: Hardened JavaScript Isolation
|
|
888
|
-
*
|
|
889
|
-
* ES6 Proxy-based sandbox with side-effect tracking:
|
|
890
|
-
* - Timer hijacking (setTimeout, setInterval, requestAnimationFrame)
|
|
891
|
-
* - Event listener tracking (window + document addEventListener)
|
|
892
|
-
* - DOM scoping (querySelector/querySelectorAll → shadow root)
|
|
893
|
-
* - Storage scoping (localStorage/sessionStorage → prefixed keys)
|
|
894
|
-
*
|
|
895
|
-
* All tracked side effects are automatically cleaned up on deactivate().
|
|
896
|
-
*/
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
class WuProxySandbox {
|
|
900
|
-
constructor(appName) {
|
|
901
|
-
this.appName = appName;
|
|
902
|
-
this.proxy = null;
|
|
903
|
-
this.fakeWindow = Object.create(null);
|
|
904
|
-
this.active = false;
|
|
905
|
-
this.modifiedKeys = new Set();
|
|
906
|
-
|
|
907
|
-
// --- Side-effect tracking ---
|
|
908
|
-
this._timers = new Set();
|
|
909
|
-
this._intervals = new Set();
|
|
910
|
-
this._rafs = new Set();
|
|
911
|
-
this._eventListeners = []; // [{target, event, handler, options}]
|
|
912
|
-
|
|
913
|
-
// --- DOM & Storage scoping ---
|
|
914
|
-
this._container = null;
|
|
915
|
-
this._shadowRoot = null;
|
|
916
|
-
this._scopedDocument = null;
|
|
917
|
-
this._scopedLocalStorage = null;
|
|
918
|
-
this._scopedSessionStorage = null;
|
|
919
|
-
|
|
920
|
-
// --- Window patching state ---
|
|
921
|
-
this._patched = false;
|
|
922
|
-
this._originals = null;
|
|
923
|
-
}
|
|
924
|
-
|
|
925
|
-
/**
|
|
926
|
-
* Set the DOM scope for this sandbox.
|
|
927
|
-
* Must be called before activate() for DOM scoping to work.
|
|
928
|
-
* @param {HTMLElement} container - App container element
|
|
929
|
-
* @param {ShadowRoot} shadowRoot - Shadow root containing the container
|
|
930
|
-
*/
|
|
931
|
-
setContainer(container, shadowRoot) {
|
|
932
|
-
this._container = container;
|
|
933
|
-
this._shadowRoot = shadowRoot;
|
|
934
|
-
}
|
|
935
|
-
|
|
936
|
-
/**
|
|
937
|
-
* Activate the sandbox. Creates the Proxy and starts tracking.
|
|
938
|
-
* @returns {Proxy} The sandboxed window proxy
|
|
939
|
-
*/
|
|
940
|
-
activate() {
|
|
941
|
-
if (this.active) return this.proxy;
|
|
942
|
-
|
|
943
|
-
const self = this;
|
|
944
|
-
|
|
945
|
-
this.proxy = new Proxy(window, {
|
|
946
|
-
get(target, prop) {
|
|
947
|
-
// 1. App's own isolated globals
|
|
948
|
-
if (prop in self.fakeWindow) {
|
|
949
|
-
return self.fakeWindow[prop];
|
|
950
|
-
}
|
|
946
|
+
this.proxy = new Proxy(window, {
|
|
947
|
+
get(target, prop) {
|
|
948
|
+
// 1. App's own isolated globals
|
|
949
|
+
if (prop in self.fakeWindow) {
|
|
950
|
+
return self.fakeWindow[prop];
|
|
951
|
+
}
|
|
951
952
|
|
|
952
953
|
// 2. Intercepted APIs
|
|
953
954
|
const intercepted = self._intercept(prop, target);
|
|
@@ -3419,8 +3420,7 @@ class WuCache {
|
|
|
3419
3420
|
maxItems: options.maxItems || 100,
|
|
3420
3421
|
defaultTTL: options.defaultTTL || 3600000, // 1 hour
|
|
3421
3422
|
persistent: options.persistent !== false,
|
|
3422
|
-
storage: options.storage || 'memory'
|
|
3423
|
-
compression: options.compression || false
|
|
3423
|
+
storage: options.storage || 'memory'
|
|
3424
3424
|
};
|
|
3425
3425
|
|
|
3426
3426
|
// 🔐 Rate limiting configuration
|
|
@@ -4788,11 +4788,14 @@ const createPlugin = (config) => {
|
|
|
4788
4788
|
permissions: config.permissions || ['events'],
|
|
4789
4789
|
install: config.install,
|
|
4790
4790
|
uninstall: config.uninstall,
|
|
4791
|
+
beforeInit: config.beforeInit,
|
|
4792
|
+
afterInit: config.afterInit,
|
|
4791
4793
|
beforeMount: config.beforeMount,
|
|
4792
4794
|
afterMount: config.afterMount,
|
|
4793
4795
|
beforeUnmount: config.beforeUnmount,
|
|
4794
4796
|
afterUnmount: config.afterUnmount,
|
|
4795
|
-
onError: config.onError
|
|
4797
|
+
onError: config.onError,
|
|
4798
|
+
onDestroy: config.onDestroy
|
|
4796
4799
|
};
|
|
4797
4800
|
};
|
|
4798
4801
|
|
|
@@ -7443,7 +7446,7 @@ class WuCore {
|
|
|
7443
7446
|
|
|
7444
7447
|
/**
|
|
7445
7448
|
* Registrar una aplicacion
|
|
7446
|
-
* @param {Object} appConfig - { name, url }
|
|
7449
|
+
* @param {Object} appConfig - { name, url, keepAlive, sandbox, container, ... }
|
|
7447
7450
|
*/
|
|
7448
7451
|
async registerApp(appConfig) {
|
|
7449
7452
|
const { name, url } = appConfig;
|
|
@@ -7455,10 +7458,9 @@ class WuCore {
|
|
|
7455
7458
|
const manifestData = await this.manifest.load(url);
|
|
7456
7459
|
this.manifests.set(name, manifestData);
|
|
7457
7460
|
|
|
7458
|
-
// Registrar la app
|
|
7461
|
+
// Registrar la app — preserve all config fields (keepAlive, sandbox, container, etc.)
|
|
7459
7462
|
this.apps.set(name, {
|
|
7460
|
-
|
|
7461
|
-
url,
|
|
7463
|
+
...appConfig,
|
|
7462
7464
|
manifest: manifestData,
|
|
7463
7465
|
status: 'registered'
|
|
7464
7466
|
});
|
|
@@ -8733,6 +8735,24 @@ class OpenAIAdapter extends BaseAdapter {
|
|
|
8733
8735
|
if (options.temperature !== undefined) body.temperature = options.temperature;
|
|
8734
8736
|
if (options.maxTokens) body.max_tokens = options.maxTokens;
|
|
8735
8737
|
if (options.stream) body.stream = true;
|
|
8738
|
+
|
|
8739
|
+
// Structured output / JSON mode
|
|
8740
|
+
if (options.responseFormat) {
|
|
8741
|
+
const rf = options.responseFormat;
|
|
8742
|
+
if (rf === 'json' || rf?.type === 'json_object') {
|
|
8743
|
+
body.response_format = { type: 'json_object' };
|
|
8744
|
+
} else if (rf?.type === 'json_schema') {
|
|
8745
|
+
body.response_format = {
|
|
8746
|
+
type: 'json_schema',
|
|
8747
|
+
json_schema: {
|
|
8748
|
+
name: rf.name || 'response',
|
|
8749
|
+
schema: rf.schema,
|
|
8750
|
+
strict: rf.strict !== false,
|
|
8751
|
+
},
|
|
8752
|
+
};
|
|
8753
|
+
}
|
|
8754
|
+
}
|
|
8755
|
+
|
|
8736
8756
|
return body;
|
|
8737
8757
|
}
|
|
8738
8758
|
|
|
@@ -8852,6 +8872,26 @@ class AnthropicAdapter extends BaseAdapter {
|
|
|
8852
8872
|
if (systemMsgs.length) {
|
|
8853
8873
|
body.system = systemMsgs.map(m => m.content).join('\n\n');
|
|
8854
8874
|
}
|
|
8875
|
+
|
|
8876
|
+
// Structured output / JSON mode (Anthropic has no native support)
|
|
8877
|
+
// Strategy: augment system prompt + prefill assistant turn with '{'
|
|
8878
|
+
if (options.responseFormat) {
|
|
8879
|
+
const rf = options.responseFormat;
|
|
8880
|
+
const jsonInstruction = '\n\nYou MUST respond with valid JSON only. No markdown, no explanation.';
|
|
8881
|
+
|
|
8882
|
+
if (rf === 'json' || rf?.type === 'json_object') {
|
|
8883
|
+
body.system = (body.system || '') + jsonInstruction;
|
|
8884
|
+
} else if (rf?.type === 'json_schema') {
|
|
8885
|
+
const schemaStr = JSON.stringify(rf.schema, null, 2);
|
|
8886
|
+
body.system = (body.system || '') +
|
|
8887
|
+
jsonInstruction +
|
|
8888
|
+
`\n\nYour response MUST conform to this JSON schema:\n${schemaStr}`;
|
|
8889
|
+
}
|
|
8890
|
+
|
|
8891
|
+
// Prefill assistant message with '{' to force JSON output
|
|
8892
|
+
body.messages.push({ role: 'assistant', content: '{' });
|
|
8893
|
+
}
|
|
8894
|
+
|
|
8855
8895
|
if (options.tools?.length) {
|
|
8856
8896
|
body.tools = options.tools.map(t => ({
|
|
8857
8897
|
name: t.name,
|
|
@@ -8952,6 +8992,17 @@ class OllamaAdapter extends BaseAdapter {
|
|
|
8952
8992
|
}
|
|
8953
8993
|
if (options.temperature !== undefined) body.options = { temperature: options.temperature };
|
|
8954
8994
|
if (options.stream !== undefined) body.stream = options.stream;
|
|
8995
|
+
|
|
8996
|
+
// Structured output / JSON mode
|
|
8997
|
+
if (options.responseFormat) {
|
|
8998
|
+
const rf = options.responseFormat;
|
|
8999
|
+
if (rf === 'json' || rf?.type === 'json_object') {
|
|
9000
|
+
body.format = 'json';
|
|
9001
|
+
} else if (rf?.type === 'json_schema') {
|
|
9002
|
+
body.format = rf.schema;
|
|
9003
|
+
}
|
|
9004
|
+
}
|
|
9005
|
+
|
|
8955
9006
|
return body;
|
|
8956
9007
|
}
|
|
8957
9008
|
|
|
@@ -9076,9 +9127,7 @@ class WuAIProvider {
|
|
|
9076
9127
|
* @returns {Promise<{ content: string, tool_calls?: Array, usage?: object }>}
|
|
9077
9128
|
*/
|
|
9078
9129
|
async send(messages, options = {}) {
|
|
9079
|
-
this.
|
|
9080
|
-
const adapter = this._active;
|
|
9081
|
-
const config = this._activeConfig;
|
|
9130
|
+
const { adapter, config } = this._resolveProvider(options.provider);
|
|
9082
9131
|
|
|
9083
9132
|
// Custom adapter: call user function directly
|
|
9084
9133
|
if (adapter.isCustom && adapter._sendFn) {
|
|
@@ -9102,7 +9151,25 @@ class WuAIProvider {
|
|
|
9102
9151
|
});
|
|
9103
9152
|
|
|
9104
9153
|
const data = await response.json();
|
|
9105
|
-
|
|
9154
|
+
const result = adapter.parseResponse(data);
|
|
9155
|
+
|
|
9156
|
+
// Anthropic prefill compensation: we prepended '{' to force JSON,
|
|
9157
|
+
// so the response content is the continuation — restore the full JSON
|
|
9158
|
+
if (adapter instanceof AnthropicAdapter && options.responseFormat && result.content) {
|
|
9159
|
+
result.content = '{' + result.content;
|
|
9160
|
+
}
|
|
9161
|
+
|
|
9162
|
+
// Validate JSON when responseFormat was requested
|
|
9163
|
+
if (options.responseFormat && result.content) {
|
|
9164
|
+
try {
|
|
9165
|
+
result.parsed = JSON.parse(result.content);
|
|
9166
|
+
} catch {
|
|
9167
|
+
result.parseError = 'Response is not valid JSON';
|
|
9168
|
+
logger.wuDebug('[wu-ai] responseFormat requested but LLM returned invalid JSON');
|
|
9169
|
+
}
|
|
9170
|
+
}
|
|
9171
|
+
|
|
9172
|
+
return result;
|
|
9106
9173
|
}
|
|
9107
9174
|
|
|
9108
9175
|
/**
|
|
@@ -9113,9 +9180,7 @@ class WuAIProvider {
|
|
|
9113
9180
|
* @yields {StreamChunk}
|
|
9114
9181
|
*/
|
|
9115
9182
|
async *stream(messages, options = {}) {
|
|
9116
|
-
this.
|
|
9117
|
-
const adapter = this._active;
|
|
9118
|
-
const config = this._activeConfig;
|
|
9183
|
+
const { adapter, config } = this._resolveProvider(options.provider);
|
|
9119
9184
|
|
|
9120
9185
|
// Custom adapter: call user generator directly
|
|
9121
9186
|
if (adapter.isCustom && adapter._streamFn) {
|
|
@@ -9147,6 +9212,10 @@ class WuAIProvider {
|
|
|
9147
9212
|
const decoder = new TextDecoder();
|
|
9148
9213
|
let buffer = '';
|
|
9149
9214
|
|
|
9215
|
+
// Anthropic prefill compensation for streaming:
|
|
9216
|
+
// emit the '{' we used as prefill before the first real chunk
|
|
9217
|
+
let needsPrefill = adapter instanceof AnthropicAdapter && !!options.responseFormat;
|
|
9218
|
+
|
|
9150
9219
|
try {
|
|
9151
9220
|
while (true) {
|
|
9152
9221
|
const { done, value } = await reader.read();
|
|
@@ -9161,7 +9230,13 @@ class WuAIProvider {
|
|
|
9161
9230
|
if (!trimmed) continue;
|
|
9162
9231
|
|
|
9163
9232
|
const chunk = adapter.parseStreamChunk(trimmed);
|
|
9164
|
-
if (chunk)
|
|
9233
|
+
if (chunk) {
|
|
9234
|
+
if (needsPrefill && chunk.type === 'text') {
|
|
9235
|
+
chunk.content = '{' + chunk.content;
|
|
9236
|
+
needsPrefill = false;
|
|
9237
|
+
}
|
|
9238
|
+
yield chunk;
|
|
9239
|
+
}
|
|
9165
9240
|
if (chunk?.type === 'done') return;
|
|
9166
9241
|
}
|
|
9167
9242
|
}
|
|
@@ -9197,10 +9272,13 @@ class WuAIProvider {
|
|
|
9197
9272
|
}
|
|
9198
9273
|
}
|
|
9199
9274
|
|
|
9200
|
-
// 4xx (except 429) — don't retry
|
|
9201
|
-
|
|
9275
|
+
// 4xx (except 429) — don't retry, fail immediately
|
|
9276
|
+
const clientError = new Error(`[wu-ai] Request failed: ${response.status} ${response.statusText}`);
|
|
9277
|
+
clientError._noRetry = true;
|
|
9278
|
+
throw clientError;
|
|
9202
9279
|
} catch (err) {
|
|
9203
9280
|
if (err.name === 'AbortError') throw err;
|
|
9281
|
+
if (err._noRetry) throw err; // 4xx — don't retry
|
|
9204
9282
|
lastError = err;
|
|
9205
9283
|
if (attempt < this._retryConfig.maxRetries) {
|
|
9206
9284
|
const delay = this._retryConfig.baseDelayMs * Math.pow(2, attempt);
|
|
@@ -9224,6 +9302,25 @@ class WuAIProvider {
|
|
|
9224
9302
|
return endpoint;
|
|
9225
9303
|
}
|
|
9226
9304
|
|
|
9305
|
+
/**
|
|
9306
|
+
* Resolve which provider/adapter to use for a request.
|
|
9307
|
+
* Supports per-call selection: options.provider = 'anthropic'
|
|
9308
|
+
*
|
|
9309
|
+
* @param {string} [providerName] - Optional provider name override
|
|
9310
|
+
* @returns {{ adapter: BaseAdapter, config: object }}
|
|
9311
|
+
*/
|
|
9312
|
+
_resolveProvider(providerName) {
|
|
9313
|
+
if (providerName) {
|
|
9314
|
+
const entry = this._providers.get(providerName);
|
|
9315
|
+
if (!entry) {
|
|
9316
|
+
throw new Error(`[wu-ai] Provider '${providerName}' not registered. Available: ${[...this._providers.keys()].join(', ')}`);
|
|
9317
|
+
}
|
|
9318
|
+
return { adapter: entry.adapter, config: entry.config };
|
|
9319
|
+
}
|
|
9320
|
+
this._ensureActive();
|
|
9321
|
+
return { adapter: this._active, config: this._activeConfig };
|
|
9322
|
+
}
|
|
9323
|
+
|
|
9227
9324
|
_ensureActive() {
|
|
9228
9325
|
if (!this._active) {
|
|
9229
9326
|
throw new Error(
|
|
@@ -9828,6 +9925,34 @@ function validateParams(params, schema) {
|
|
|
9828
9925
|
return { valid: errors.length === 0, errors };
|
|
9829
9926
|
}
|
|
9830
9927
|
|
|
9928
|
+
// ─── Context Budget ──────────────────────────────────────────────
|
|
9929
|
+
|
|
9930
|
+
/**
|
|
9931
|
+
* Estimate token count from character count.
|
|
9932
|
+
* Rough heuristic: 1 token ≈ 4 chars for English, ≈ 2 chars for CJK.
|
|
9933
|
+
*
|
|
9934
|
+
* @param {string} text
|
|
9935
|
+
* @param {number} [charRatio=4]
|
|
9936
|
+
* @returns {number}
|
|
9937
|
+
*/
|
|
9938
|
+
function estimateTokens(text, charRatio = 4) {
|
|
9939
|
+
return Math.ceil(text.length / charRatio);
|
|
9940
|
+
}
|
|
9941
|
+
|
|
9942
|
+
/**
|
|
9943
|
+
* Truncate text to fit within a token budget.
|
|
9944
|
+
*
|
|
9945
|
+
* @param {string} text
|
|
9946
|
+
* @param {number} maxTokens
|
|
9947
|
+
* @param {number} [charRatio=4]
|
|
9948
|
+
* @returns {string}
|
|
9949
|
+
*/
|
|
9950
|
+
function truncateToTokenBudget(text, maxTokens, charRatio = 4) {
|
|
9951
|
+
const maxChars = maxTokens * charRatio;
|
|
9952
|
+
if (text.length <= maxChars) return text;
|
|
9953
|
+
return text.slice(0, maxChars) + '\n...[truncated to fit token budget]';
|
|
9954
|
+
}
|
|
9955
|
+
|
|
9831
9956
|
/**
|
|
9832
9957
|
* WU-AI-CONTEXT: Automatic context collector for LLMs
|
|
9833
9958
|
*
|
|
@@ -10445,6 +10570,8 @@ const DEFAULT_CONFIG = {
|
|
|
10445
10570
|
systemPrompt: null, // string or function returning string
|
|
10446
10571
|
temperature: undefined,
|
|
10447
10572
|
maxTokens: undefined,
|
|
10573
|
+
namespaceTTL: 30 * 60_000, // 30 min — auto-expire inactive namespaces (0 = disabled)
|
|
10574
|
+
gcInterval: 5 * 60_000, // 5 min — how often to sweep for expired namespaces
|
|
10448
10575
|
};
|
|
10449
10576
|
|
|
10450
10577
|
// ─── Conversation Namespace ──────────────────────────────────────
|
|
@@ -10509,6 +10636,7 @@ class WuAIConversation {
|
|
|
10509
10636
|
this._config = { ...DEFAULT_CONFIG };
|
|
10510
10637
|
this._namespaces = new Map();
|
|
10511
10638
|
this._activeRequests = new Map(); // namespace → promise
|
|
10639
|
+
this._lastGcRun = Date.now();
|
|
10512
10640
|
}
|
|
10513
10641
|
|
|
10514
10642
|
/**
|
|
@@ -10529,6 +10657,7 @@ class WuAIConversation {
|
|
|
10529
10657
|
* @param {object} [options.templateVars] - Variables for template interpolation
|
|
10530
10658
|
* @param {number} [options.temperature] - Override temperature
|
|
10531
10659
|
* @param {number} [options.maxTokens] - Override max tokens
|
|
10660
|
+
* @param {string|object} [options.responseFormat] - Request JSON output ('json' or { type: 'json_schema', schema, name? })
|
|
10532
10661
|
* @param {AbortSignal} [options.signal] - External abort signal
|
|
10533
10662
|
* @returns {Promise<{ content: string, tool_results?: Array, usage?: object, namespace: string }>}
|
|
10534
10663
|
*/
|
|
@@ -10577,6 +10706,8 @@ class WuAIConversation {
|
|
|
10577
10706
|
tools: tools.length > 0 ? tools : undefined,
|
|
10578
10707
|
temperature: options.temperature ?? this._config.temperature,
|
|
10579
10708
|
maxTokens: options.maxTokens ?? this._config.maxTokens,
|
|
10709
|
+
responseFormat: options.responseFormat,
|
|
10710
|
+
provider: options.provider,
|
|
10580
10711
|
signal,
|
|
10581
10712
|
});
|
|
10582
10713
|
|
|
@@ -10701,102 +10832,113 @@ class WuAIConversation {
|
|
|
10701
10832
|
this._permissions.rateLimiter.recordStart(ns.name);
|
|
10702
10833
|
|
|
10703
10834
|
try {
|
|
10704
|
-
|
|
10705
|
-
let
|
|
10706
|
-
|
|
10707
|
-
// Accumulator for streaming tool call deltas
|
|
10708
|
-
const toolCallAccumulator = new Map(); // index → { id, name, args }
|
|
10709
|
-
|
|
10710
|
-
for await (const chunk of this._provider.stream(ns.getMessages(), {
|
|
10711
|
-
tools: tools.length > 0 ? tools : undefined,
|
|
10712
|
-
temperature: options.temperature ?? this._config.temperature,
|
|
10713
|
-
maxTokens: options.maxTokens ?? this._config.maxTokens,
|
|
10714
|
-
signal,
|
|
10715
|
-
})) {
|
|
10716
|
-
if (chunk.type === 'text') {
|
|
10717
|
-
fullContent += chunk.content;
|
|
10718
|
-
yield chunk;
|
|
10719
|
-
} else if (chunk.type === 'tool_call_start') {
|
|
10720
|
-
// Start accumulating a new tool call
|
|
10721
|
-
toolCallAccumulator.set(toolCallAccumulator.size, {
|
|
10722
|
-
id: chunk.id,
|
|
10723
|
-
name: chunk.name,
|
|
10724
|
-
args: '',
|
|
10725
|
-
});
|
|
10726
|
-
} else if (chunk.type === 'tool_call_delta') {
|
|
10727
|
-
// Accumulate arguments delta
|
|
10728
|
-
const idx = chunk.index ?? (toolCallAccumulator.size - 1);
|
|
10729
|
-
const acc = toolCallAccumulator.get(idx);
|
|
10730
|
-
if (acc) {
|
|
10731
|
-
if (chunk.id) acc.id = chunk.id;
|
|
10732
|
-
if (chunk.name) acc.name = chunk.name;
|
|
10733
|
-
acc.args += chunk.argumentsDelta || '';
|
|
10734
|
-
} else {
|
|
10735
|
-
// New tool call via OpenAI format
|
|
10736
|
-
toolCallAccumulator.set(idx, {
|
|
10737
|
-
id: chunk.id || `tc_${idx}`,
|
|
10738
|
-
name: chunk.name || '',
|
|
10739
|
-
args: chunk.argumentsDelta || '',
|
|
10740
|
-
});
|
|
10741
|
-
}
|
|
10742
|
-
} else if (chunk.type === 'done') {
|
|
10743
|
-
// If we accumulated tool calls, execute them
|
|
10744
|
-
if (toolCallAccumulator.size > 0) {
|
|
10745
|
-
const toolCalls = [];
|
|
10746
|
-
for (const [, acc] of toolCallAccumulator) {
|
|
10747
|
-
let parsedArgs = {};
|
|
10748
|
-
try { parsedArgs = JSON.parse(acc.args); } catch { /* empty args */ }
|
|
10749
|
-
toolCalls.push({ id: acc.id, name: acc.name, arguments: parsedArgs });
|
|
10750
|
-
}
|
|
10751
|
-
|
|
10752
|
-
// Add assistant message with tool calls
|
|
10753
|
-
ns.addMessage({ role: 'assistant', content: fullContent, tool_calls: toolCalls });
|
|
10835
|
+
// Tool call loop — mirrors send() behavior (up to maxToolRounds)
|
|
10836
|
+
let rounds = 0;
|
|
10837
|
+
const maxRounds = this._config.maxToolRounds;
|
|
10754
10838
|
|
|
10755
|
-
|
|
10756
|
-
|
|
10757
|
-
|
|
10758
|
-
|
|
10759
|
-
|
|
10760
|
-
});
|
|
10839
|
+
while (rounds <= maxRounds) {
|
|
10840
|
+
const tools = this._actions.getToolSchemas();
|
|
10841
|
+
let fullContent = '';
|
|
10842
|
+
const toolCallAccumulator = new Map(); // index → { id, name, args }
|
|
10843
|
+
let streamEnded = false;
|
|
10761
10844
|
|
|
10762
|
-
|
|
10763
|
-
|
|
10764
|
-
|
|
10765
|
-
|
|
10766
|
-
|
|
10767
|
-
|
|
10768
|
-
|
|
10769
|
-
|
|
10770
|
-
|
|
10771
|
-
|
|
10772
|
-
|
|
10845
|
+
for await (const chunk of this._provider.stream(ns.getMessages(), {
|
|
10846
|
+
tools: tools.length > 0 ? tools : undefined,
|
|
10847
|
+
temperature: options.temperature ?? this._config.temperature,
|
|
10848
|
+
maxTokens: options.maxTokens ?? this._config.maxTokens,
|
|
10849
|
+
responseFormat: options.responseFormat,
|
|
10850
|
+
provider: options.provider,
|
|
10851
|
+
signal,
|
|
10852
|
+
})) {
|
|
10853
|
+
if (chunk.type === 'text') {
|
|
10854
|
+
fullContent += chunk.content;
|
|
10855
|
+
yield chunk;
|
|
10856
|
+
} else if (chunk.type === 'tool_call_start') {
|
|
10857
|
+
toolCallAccumulator.set(toolCallAccumulator.size, {
|
|
10858
|
+
id: chunk.id,
|
|
10859
|
+
name: chunk.name,
|
|
10860
|
+
args: '',
|
|
10861
|
+
});
|
|
10862
|
+
} else if (chunk.type === 'tool_call_delta') {
|
|
10863
|
+
const idx = chunk.index ?? (toolCallAccumulator.size - 1);
|
|
10864
|
+
const acc = toolCallAccumulator.get(idx);
|
|
10865
|
+
if (acc) {
|
|
10866
|
+
if (chunk.id) acc.id = chunk.id;
|
|
10867
|
+
if (chunk.name) acc.name = chunk.name;
|
|
10868
|
+
acc.args += chunk.argumentsDelta || '';
|
|
10869
|
+
} else {
|
|
10870
|
+
toolCallAccumulator.set(idx, {
|
|
10871
|
+
id: chunk.id || `tc_${idx}`,
|
|
10872
|
+
name: chunk.name || '',
|
|
10873
|
+
args: chunk.argumentsDelta || '',
|
|
10773
10874
|
});
|
|
10774
10875
|
}
|
|
10775
|
-
|
|
10876
|
+
} else if (chunk.type === 'done') {
|
|
10877
|
+
streamEnded = true;
|
|
10878
|
+
break; // exit inner loop, handle tool calls below
|
|
10879
|
+
} else if (chunk.type === 'usage') {
|
|
10880
|
+
yield chunk;
|
|
10881
|
+
} else if (chunk.type === 'error') {
|
|
10882
|
+
yield chunk;
|
|
10883
|
+
}
|
|
10884
|
+
}
|
|
10776
10885
|
|
|
10777
|
-
|
|
10778
|
-
|
|
10779
|
-
|
|
10780
|
-
// No tool calls — add assistant message and finish
|
|
10886
|
+
// No tool calls → final response, we're done
|
|
10887
|
+
if (toolCallAccumulator.size === 0) {
|
|
10888
|
+
if (fullContent) {
|
|
10781
10889
|
ns.addMessage({ role: 'assistant', content: fullContent });
|
|
10782
10890
|
}
|
|
10783
|
-
|
|
10784
10891
|
this._permissions.circuitBreaker.recordSuccess();
|
|
10785
10892
|
yield { type: 'done' };
|
|
10786
10893
|
return;
|
|
10787
|
-
} else if (chunk.type === 'usage') {
|
|
10788
|
-
yield chunk;
|
|
10789
|
-
} else if (chunk.type === 'error') {
|
|
10790
|
-
yield chunk;
|
|
10791
10894
|
}
|
|
10792
|
-
}
|
|
10793
10895
|
|
|
10794
|
-
|
|
10795
|
-
|
|
10796
|
-
|
|
10896
|
+
// Tool calls detected — execute them
|
|
10897
|
+
rounds++;
|
|
10898
|
+
if (rounds > maxRounds) {
|
|
10899
|
+
const msg = `[wu-ai] Tool call loop limit (${maxRounds}) reached in streaming namespace '${ns.name}'`;
|
|
10900
|
+
logger.wuWarn(msg);
|
|
10901
|
+
yield { type: 'error', error: msg };
|
|
10902
|
+
yield { type: 'done' };
|
|
10903
|
+
return;
|
|
10904
|
+
}
|
|
10905
|
+
|
|
10906
|
+
// Parse accumulated tool calls
|
|
10907
|
+
const toolCalls = [];
|
|
10908
|
+
for (const [, acc] of toolCallAccumulator) {
|
|
10909
|
+
let parsedArgs = {};
|
|
10910
|
+
try { parsedArgs = JSON.parse(acc.args); } catch { /* empty args */ }
|
|
10911
|
+
toolCalls.push({ id: acc.id, name: acc.name, arguments: parsedArgs });
|
|
10912
|
+
}
|
|
10913
|
+
|
|
10914
|
+
// Add assistant message with tool calls to history
|
|
10915
|
+
ns.addMessage({ role: 'assistant', content: fullContent, tool_calls: toolCalls });
|
|
10916
|
+
|
|
10917
|
+
// Execute each tool call
|
|
10918
|
+
this._permissions.loopProtection.enter(traceId);
|
|
10919
|
+
for (const tc of toolCalls) {
|
|
10920
|
+
const result = await this._actions.execute(tc.name, tc.arguments, {
|
|
10921
|
+
traceId, depth: rounds, callId: tc.id,
|
|
10922
|
+
});
|
|
10923
|
+
|
|
10924
|
+
yield {
|
|
10925
|
+
type: 'tool_result',
|
|
10926
|
+
tool: tc.name,
|
|
10927
|
+
result: result.success ? result.result : { error: result.reason },
|
|
10928
|
+
success: result.success,
|
|
10929
|
+
};
|
|
10930
|
+
|
|
10931
|
+
ns.addMessage({
|
|
10932
|
+
role: 'tool',
|
|
10933
|
+
content: JSON.stringify(result.success ? result.result : { error: result.reason }),
|
|
10934
|
+
tool_call_id: tc.id,
|
|
10935
|
+
});
|
|
10936
|
+
}
|
|
10937
|
+
this._permissions.loopProtection.exit(traceId);
|
|
10938
|
+
|
|
10939
|
+
yield { type: 'tool_calls_done', count: toolCalls.length };
|
|
10940
|
+
// Loop continues → re-stream with tool results in history
|
|
10797
10941
|
}
|
|
10798
|
-
this._permissions.circuitBreaker.recordSuccess();
|
|
10799
|
-
yield { type: 'done' };
|
|
10800
10942
|
|
|
10801
10943
|
} catch (err) {
|
|
10802
10944
|
this._permissions.circuitBreaker.recordFailure();
|
|
@@ -10892,10 +11034,52 @@ class WuAIConversation {
|
|
|
10892
11034
|
|
|
10893
11035
|
_getOrCreateNamespace(name) {
|
|
10894
11036
|
const nsName = name || this._config.defaultNamespace;
|
|
11037
|
+
|
|
11038
|
+
// Lazy GC sweep — only runs periodically, not on every access
|
|
11039
|
+
this._maybeGcSweep();
|
|
11040
|
+
|
|
10895
11041
|
if (!this._namespaces.has(nsName)) {
|
|
10896
11042
|
this._namespaces.set(nsName, new ConversationNamespace(nsName));
|
|
10897
11043
|
}
|
|
10898
|
-
|
|
11044
|
+
const ns = this._namespaces.get(nsName);
|
|
11045
|
+
ns.lastActivity = Date.now();
|
|
11046
|
+
return ns;
|
|
11047
|
+
}
|
|
11048
|
+
|
|
11049
|
+
/**
|
|
11050
|
+
* Sweep expired namespaces. Called lazily on access, throttled by gcInterval.
|
|
11051
|
+
* Never deletes the default namespace or namespaces with active requests.
|
|
11052
|
+
*/
|
|
11053
|
+
_maybeGcSweep() {
|
|
11054
|
+
const ttl = this._config.namespaceTTL;
|
|
11055
|
+
const interval = this._config.gcInterval;
|
|
11056
|
+
if (!ttl || ttl <= 0) return; // GC disabled
|
|
11057
|
+
|
|
11058
|
+
const now = Date.now();
|
|
11059
|
+
if (now - this._lastGcRun < interval) return; // Not time yet
|
|
11060
|
+
this._lastGcRun = now;
|
|
11061
|
+
|
|
11062
|
+
const cutoff = now - ttl;
|
|
11063
|
+
const toDelete = [];
|
|
11064
|
+
|
|
11065
|
+
for (const [name, ns] of this._namespaces) {
|
|
11066
|
+
// Never GC the default namespace
|
|
11067
|
+
if (name === this._config.defaultNamespace) continue;
|
|
11068
|
+
// Don't GC namespaces with active abort controllers (in-flight request)
|
|
11069
|
+
if (ns._abortController) continue;
|
|
11070
|
+
// Expired?
|
|
11071
|
+
if (ns.lastActivity < cutoff) {
|
|
11072
|
+
toDelete.push(name);
|
|
11073
|
+
}
|
|
11074
|
+
}
|
|
11075
|
+
|
|
11076
|
+
for (const name of toDelete) {
|
|
11077
|
+
this._namespaces.delete(name);
|
|
11078
|
+
}
|
|
11079
|
+
|
|
11080
|
+
if (toDelete.length > 0) {
|
|
11081
|
+
logger.wuDebug(`[wu-ai] GC sweep: removed ${toDelete.length} expired namespace(s): ${toDelete.join(', ')}`);
|
|
11082
|
+
}
|
|
10899
11083
|
}
|
|
10900
11084
|
|
|
10901
11085
|
async _buildSystemPrompt(options) {
|
|
@@ -11368,51 +11552,1961 @@ class WuAITriggers {
|
|
|
11368
11552
|
}
|
|
11369
11553
|
|
|
11370
11554
|
/**
|
|
11371
|
-
* WU-AI
|
|
11555
|
+
* WU-AI-AGENT: Autonomous agent loop (Paradigm 3)
|
|
11372
11556
|
*
|
|
11373
|
-
*
|
|
11374
|
-
*
|
|
11375
|
-
*
|
|
11557
|
+
* The agent is the third paradigm of wu.ai:
|
|
11558
|
+
* Paradigm 1 — App sends messages to LLM (conversation)
|
|
11559
|
+
* Paradigm 2 — External LLM calls into app (tools/WebMCP)
|
|
11560
|
+
* Paradigm 3 — AI as autonomous director (this file)
|
|
11376
11561
|
*
|
|
11377
|
-
*
|
|
11378
|
-
*
|
|
11379
|
-
*
|
|
11380
|
-
*
|
|
11381
|
-
* browser_snapshot — Get accessibility tree of the DOM
|
|
11382
|
-
* browser_navigate — Navigate SPA routes
|
|
11383
|
-
* browser_network — View captured HTTP requests (fetch + XHR)
|
|
11384
|
-
* browser_console — View captured console messages
|
|
11385
|
-
* browser_info — Get page state: apps, store, URL, viewport
|
|
11386
|
-
* browser_select — Select option in dropdowns
|
|
11387
|
-
* browser_scroll — Scroll page or element
|
|
11562
|
+
* The agent receives a goal and iterates: send to LLM, execute tool calls,
|
|
11563
|
+
* observe results, repeat — until the goal is achieved or limits are hit.
|
|
11564
|
+
* It is an async generator, yielding each step so the caller can observe,
|
|
11565
|
+
* log, render, or intervene at any point.
|
|
11388
11566
|
*
|
|
11389
|
-
*
|
|
11390
|
-
*
|
|
11391
|
-
*
|
|
11392
|
-
*
|
|
11393
|
-
*
|
|
11567
|
+
* This is a foundation, not a planner. It does not decompose goals into
|
|
11568
|
+
* sub-tasks or maintain a world model. It trusts the LLM to drive the
|
|
11569
|
+
* loop and uses the [DONE] marker or tool-call cessation as termination
|
|
11570
|
+
* signals. More sophisticated planning belongs in userland, composed
|
|
11571
|
+
* on top of this primitive.
|
|
11572
|
+
*
|
|
11573
|
+
* Key features:
|
|
11574
|
+
* - Async generator yielding step results (observable, composable)
|
|
11575
|
+
* - Human-in-the-loop via shouldContinue callback
|
|
11576
|
+
* - Abort support via AbortController
|
|
11577
|
+
* - Permission preflight before every step
|
|
11578
|
+
* - Event emission on start, step, done, error
|
|
11579
|
+
* - Auto-generated system prompt describing agent role and tools
|
|
11580
|
+
* - Configurable step limit (default 10)
|
|
11394
11581
|
*/
|
|
11395
11582
|
|
|
11396
|
-
// ── Shared capture buffers ──
|
|
11397
|
-
const networkLog = [];
|
|
11398
|
-
const MAX_NETWORK_LOG = 300;
|
|
11399
|
-
const consoleLog = [];
|
|
11400
|
-
const MAX_CONSOLE_LOG = 500;
|
|
11401
|
-
let interceptorsInstalled = false;
|
|
11402
11583
|
|
|
11403
|
-
|
|
11404
|
-
|
|
11405
|
-
|
|
11584
|
+
// ─── Constants ──────────────────────────────────────────────────
|
|
11585
|
+
|
|
11586
|
+
const DONE_MARKER = '[DONE]';
|
|
11587
|
+
|
|
11588
|
+
const DEFAULT_MAX_STEPS = 10;
|
|
11589
|
+
|
|
11590
|
+
const AGENT_NAMESPACE_PREFIX = 'agent:';
|
|
11591
|
+
|
|
11592
|
+
// ─── Step Result Types ──────────────────────────────────────────
|
|
11593
|
+
|
|
11594
|
+
/**
|
|
11595
|
+
* @typedef {object} AgentStepResult
|
|
11596
|
+
* @property {number} step - Step number (1-indexed)
|
|
11597
|
+
* @property {'thinking'|'tool_call'|'done'|'blocked'|'aborted'|'interrupted'} type
|
|
11598
|
+
* @property {string} [content] - LLM text response for this step
|
|
11599
|
+
* @property {Array} [toolResults] - Tool execution results (if tools were called)
|
|
11600
|
+
* @property {object} [usage] - Token usage for this step
|
|
11601
|
+
* @property {string} [reason] - Reason for termination (done/blocked/aborted)
|
|
11602
|
+
* @property {number} elapsed - Time in ms for this step
|
|
11603
|
+
*/
|
|
11604
|
+
|
|
11605
|
+
// ─── Agent Class ────────────────────────────────────────────────
|
|
11606
|
+
|
|
11607
|
+
class WuAIAgent {
|
|
11608
|
+
/**
|
|
11609
|
+
* @param {object} deps - Injected dependencies (same pattern as other wu-ai modules)
|
|
11610
|
+
* @param {import('./wu-ai-conversation.js').WuAIConversation} deps.conversation - Conversation manager
|
|
11611
|
+
* @param {import('./wu-ai-actions.js').WuAIActions} deps.actions - Action registry
|
|
11612
|
+
* @param {import('./wu-ai-context.js').WuAIContext} deps.context - Context collector
|
|
11613
|
+
* @param {import('./wu-ai-permissions.js').WuAIPermissions} deps.permissions - Permission system
|
|
11614
|
+
* @param {object} deps.eventBus - WuEventBus instance
|
|
11615
|
+
*/
|
|
11616
|
+
constructor({ conversation, actions, context, permissions, eventBus }) {
|
|
11617
|
+
this._conversation = conversation;
|
|
11618
|
+
this._actions = actions;
|
|
11619
|
+
this._context = context;
|
|
11620
|
+
this._permissions = permissions;
|
|
11621
|
+
this._eventBus = eventBus;
|
|
11622
|
+
|
|
11623
|
+
this._config = {
|
|
11624
|
+
maxSteps: DEFAULT_MAX_STEPS,
|
|
11625
|
+
systemPrompt: null,
|
|
11626
|
+
};
|
|
11627
|
+
|
|
11628
|
+
this._activeRuns = new Map(); // runId -> AbortController
|
|
11629
|
+
this._stats = {
|
|
11630
|
+
totalRuns: 0,
|
|
11631
|
+
totalSteps: 0,
|
|
11632
|
+
completedRuns: 0,
|
|
11633
|
+
abortedRuns: 0,
|
|
11634
|
+
errorRuns: 0,
|
|
11635
|
+
};
|
|
11636
|
+
}
|
|
11637
|
+
|
|
11638
|
+
/**
|
|
11639
|
+
* Post-init configuration.
|
|
11640
|
+
*
|
|
11641
|
+
* @param {object} config
|
|
11642
|
+
* @param {number} [config.maxSteps] - Default max steps for all runs
|
|
11643
|
+
* @param {string|Function} [config.systemPrompt] - Default agent system prompt
|
|
11644
|
+
*/
|
|
11645
|
+
configure(config) {
|
|
11646
|
+
if (config.maxSteps !== undefined) this._config.maxSteps = config.maxSteps;
|
|
11647
|
+
if (config.systemPrompt !== undefined) this._config.systemPrompt = config.systemPrompt;
|
|
11648
|
+
}
|
|
11649
|
+
|
|
11650
|
+
/**
|
|
11651
|
+
* Run the agent loop toward a goal.
|
|
11652
|
+
*
|
|
11653
|
+
* The generator yields after each LLM round. The caller can inspect
|
|
11654
|
+
* the result, update UI, or break out of the loop to abort early.
|
|
11655
|
+
*
|
|
11656
|
+
* Termination conditions (checked in order):
|
|
11657
|
+
* 1. AbortController signal fired
|
|
11658
|
+
* 2. shouldContinue() returns false (human-in-the-loop gate)
|
|
11659
|
+
* 3. Permission preflight denied
|
|
11660
|
+
* 4. LLM response contains [DONE] marker
|
|
11661
|
+
* 5. LLM stops requesting tool calls (after previously requesting them)
|
|
11662
|
+
* 6. maxSteps reached
|
|
11663
|
+
*
|
|
11664
|
+
* @param {string} goal - Natural language description of what to achieve
|
|
11665
|
+
* @param {object} [options]
|
|
11666
|
+
* @param {number} [options.maxSteps] - Override max steps for this run
|
|
11667
|
+
* @param {string} [options.provider] - Use a specific registered provider
|
|
11668
|
+
* @param {string} [options.namespace] - Conversation namespace (auto-generated if omitted)
|
|
11669
|
+
* @param {string|Function} [options.systemPrompt] - Override system prompt
|
|
11670
|
+
* @param {Function} [options.onStep] - Callback invoked after each step: (stepResult) => void
|
|
11671
|
+
* @param {Function} [options.shouldContinue] - Async gate: (stepResult) => boolean. Return false to stop.
|
|
11672
|
+
* @param {AbortSignal} [options.signal] - External abort signal
|
|
11673
|
+
* @param {number} [options.temperature] - LLM temperature
|
|
11674
|
+
* @param {number} [options.maxTokens] - LLM max tokens per step
|
|
11675
|
+
* @yields {AgentStepResult}
|
|
11676
|
+
*/
|
|
11677
|
+
async *run(goal, options = {}) {
|
|
11678
|
+
const maxSteps = options.maxSteps ?? this._config.maxSteps;
|
|
11679
|
+
const namespace = options.namespace || this._generateNamespace();
|
|
11680
|
+
const runId = this._generateRunId();
|
|
11681
|
+
|
|
11682
|
+
// Abort controller: merge external signal with our own
|
|
11683
|
+
const controller = new AbortController();
|
|
11684
|
+
this._activeRuns.set(runId, controller);
|
|
11685
|
+
|
|
11686
|
+
if (options.signal) {
|
|
11687
|
+
if (options.signal.aborted) {
|
|
11688
|
+
controller.abort();
|
|
11689
|
+
} else {
|
|
11690
|
+
options.signal.addEventListener('abort', () => controller.abort(), { once: true });
|
|
11691
|
+
}
|
|
11692
|
+
}
|
|
11693
|
+
|
|
11694
|
+
this._stats.totalRuns++;
|
|
11695
|
+
|
|
11696
|
+
// Build the agent system prompt
|
|
11697
|
+
const systemPrompt = await this._buildAgentSystemPrompt(goal, options);
|
|
11698
|
+
|
|
11699
|
+
// Emit start event
|
|
11700
|
+
this._eventBus.emit('ai:agent:start', {
|
|
11701
|
+
runId,
|
|
11702
|
+
goal,
|
|
11703
|
+
namespace,
|
|
11704
|
+
maxSteps,
|
|
11705
|
+
}, { appName: 'wu-ai' });
|
|
11706
|
+
|
|
11707
|
+
logger.wuInfo(`[wu-ai] Agent run started: "${goal.slice(0, 80)}${goal.length > 80 ? '...' : ''}" (max ${maxSteps} steps)`);
|
|
11708
|
+
|
|
11709
|
+
let step = 0;
|
|
11710
|
+
let previousHadToolCalls = false;
|
|
11711
|
+
let finalReason = 'max_steps';
|
|
11712
|
+
|
|
11713
|
+
try {
|
|
11714
|
+
// Inject the goal as the first user message via conversation.send
|
|
11715
|
+
// The loop sends the goal on step 1, then follow-up prompts on subsequent steps.
|
|
11716
|
+
|
|
11717
|
+
while (step < maxSteps) {
|
|
11718
|
+
step++;
|
|
11719
|
+
const stepStart = Date.now();
|
|
11720
|
+
|
|
11721
|
+
// ── 1. Check abort ──
|
|
11722
|
+
if (controller.signal.aborted) {
|
|
11723
|
+
const result = this._buildStepResult(step, 'aborted', {
|
|
11724
|
+
reason: 'Aborted by caller',
|
|
11725
|
+
elapsed: Date.now() - stepStart,
|
|
11726
|
+
});
|
|
11727
|
+
this._stats.abortedRuns++;
|
|
11728
|
+
finalReason = 'aborted';
|
|
11729
|
+
this._emitStep(runId, result);
|
|
11730
|
+
yield result;
|
|
11731
|
+
return;
|
|
11732
|
+
}
|
|
11733
|
+
|
|
11734
|
+
// ── 2. Permission preflight ──
|
|
11735
|
+
const traceId = this._permissions.loopProtection.createTraceId();
|
|
11736
|
+
const preflight = this._permissions.preflight({
|
|
11737
|
+
namespace,
|
|
11738
|
+
depth: step,
|
|
11739
|
+
traceId,
|
|
11740
|
+
});
|
|
11741
|
+
|
|
11742
|
+
if (!preflight.allowed) {
|
|
11743
|
+
const result = this._buildStepResult(step, 'blocked', {
|
|
11744
|
+
reason: preflight.reason,
|
|
11745
|
+
elapsed: Date.now() - stepStart,
|
|
11746
|
+
});
|
|
11747
|
+
finalReason = 'blocked';
|
|
11748
|
+
this._emitStep(runId, result);
|
|
11749
|
+
yield result;
|
|
11750
|
+
return;
|
|
11751
|
+
}
|
|
11752
|
+
|
|
11753
|
+
// ── 3. Compose the message for this step ──
|
|
11754
|
+
const message = step === 1
|
|
11755
|
+
? goal
|
|
11756
|
+
: 'Continue working toward the goal. If you are done, include [DONE] in your response.';
|
|
11757
|
+
|
|
11758
|
+
// ── 4. Send to conversation (handles tool call loops internally) ──
|
|
11759
|
+
let response;
|
|
11760
|
+
try {
|
|
11761
|
+
response = await this._conversation.send(message, {
|
|
11762
|
+
namespace,
|
|
11763
|
+
systemPrompt,
|
|
11764
|
+
provider: options.provider,
|
|
11765
|
+
temperature: options.temperature,
|
|
11766
|
+
maxTokens: options.maxTokens,
|
|
11767
|
+
signal: controller.signal,
|
|
11768
|
+
});
|
|
11769
|
+
} catch (err) {
|
|
11770
|
+
if (err.name === 'AbortError' || controller.signal.aborted) {
|
|
11771
|
+
const result = this._buildStepResult(step, 'aborted', {
|
|
11772
|
+
reason: 'Aborted during LLM call',
|
|
11773
|
+
elapsed: Date.now() - stepStart,
|
|
11774
|
+
});
|
|
11775
|
+
this._stats.abortedRuns++;
|
|
11776
|
+
finalReason = 'aborted';
|
|
11777
|
+
this._emitStep(runId, result);
|
|
11778
|
+
yield result;
|
|
11779
|
+
return;
|
|
11780
|
+
}
|
|
11781
|
+
throw err;
|
|
11782
|
+
}
|
|
11783
|
+
|
|
11784
|
+
const elapsed = Date.now() - stepStart;
|
|
11785
|
+
this._stats.totalSteps++;
|
|
11786
|
+
|
|
11787
|
+
const hasToolResults = response.tool_results && response.tool_results.length > 0;
|
|
11788
|
+
const content = response.content || '';
|
|
11789
|
+
|
|
11790
|
+
// ── 5. Check for DONE marker ──
|
|
11791
|
+
if (content.includes(DONE_MARKER)) {
|
|
11792
|
+
const result = this._buildStepResult(step, 'done', {
|
|
11793
|
+
content: content.replace(DONE_MARKER, '').trim(),
|
|
11794
|
+
toolResults: response.tool_results,
|
|
11795
|
+
usage: response.usage,
|
|
11796
|
+
reason: 'Goal completed (DONE marker)',
|
|
11797
|
+
elapsed,
|
|
11798
|
+
});
|
|
11799
|
+
finalReason = 'done';
|
|
11800
|
+
this._stats.completedRuns++;
|
|
11801
|
+
this._emitStep(runId, result);
|
|
11802
|
+
if (options.onStep) await this._safeCallback(options.onStep, result);
|
|
11803
|
+
yield result;
|
|
11804
|
+
return;
|
|
11805
|
+
}
|
|
11806
|
+
|
|
11807
|
+
// ── 6. Check for tool-call cessation ──
|
|
11808
|
+
// If the LLM was previously calling tools but stopped, it has
|
|
11809
|
+
// settled on a final answer. This is the implicit completion signal.
|
|
11810
|
+
const currentHasToolCalls = hasToolResults;
|
|
11811
|
+
if (previousHadToolCalls && !currentHasToolCalls) {
|
|
11812
|
+
const result = this._buildStepResult(step, 'done', {
|
|
11813
|
+
content,
|
|
11814
|
+
toolResults: response.tool_results,
|
|
11815
|
+
usage: response.usage,
|
|
11816
|
+
reason: 'Goal completed (no further tool calls)',
|
|
11817
|
+
elapsed,
|
|
11818
|
+
});
|
|
11819
|
+
finalReason = 'done';
|
|
11820
|
+
this._stats.completedRuns++;
|
|
11821
|
+
this._emitStep(runId, result);
|
|
11822
|
+
if (options.onStep) await this._safeCallback(options.onStep, result);
|
|
11823
|
+
yield result;
|
|
11824
|
+
return;
|
|
11825
|
+
}
|
|
11826
|
+
|
|
11827
|
+
previousHadToolCalls = currentHasToolCalls;
|
|
11828
|
+
|
|
11829
|
+
// ── 7. Yield the step result ──
|
|
11830
|
+
const stepType = hasToolResults ? 'tool_call' : 'thinking';
|
|
11831
|
+
const result = this._buildStepResult(step, stepType, {
|
|
11832
|
+
content,
|
|
11833
|
+
toolResults: response.tool_results,
|
|
11834
|
+
usage: response.usage,
|
|
11835
|
+
elapsed,
|
|
11836
|
+
});
|
|
11837
|
+
|
|
11838
|
+
this._emitStep(runId, result);
|
|
11839
|
+
if (options.onStep) await this._safeCallback(options.onStep, result);
|
|
11840
|
+
yield result;
|
|
11841
|
+
|
|
11842
|
+
// ── 8. Human-in-the-loop gate ──
|
|
11843
|
+
if (options.shouldContinue) {
|
|
11844
|
+
let shouldGo;
|
|
11845
|
+
try {
|
|
11846
|
+
shouldGo = await options.shouldContinue(result);
|
|
11847
|
+
} catch {
|
|
11848
|
+
shouldGo = false;
|
|
11849
|
+
}
|
|
11850
|
+
|
|
11851
|
+
if (!shouldGo) {
|
|
11852
|
+
const interrupted = this._buildStepResult(step, 'interrupted', {
|
|
11853
|
+
content,
|
|
11854
|
+
reason: 'Stopped by shouldContinue callback',
|
|
11855
|
+
elapsed: 0,
|
|
11856
|
+
});
|
|
11857
|
+
finalReason = 'interrupted';
|
|
11858
|
+
this._emitStep(runId, interrupted);
|
|
11859
|
+
yield interrupted;
|
|
11860
|
+
return;
|
|
11861
|
+
}
|
|
11862
|
+
}
|
|
11863
|
+
}
|
|
11864
|
+
|
|
11865
|
+
// ── Max steps reached ──
|
|
11866
|
+
const maxStepResult = this._buildStepResult(step, 'done', {
|
|
11867
|
+
reason: `Max steps (${maxSteps}) reached`,
|
|
11868
|
+
elapsed: 0,
|
|
11869
|
+
});
|
|
11870
|
+
finalReason = 'max_steps';
|
|
11871
|
+
yield maxStepResult;
|
|
11872
|
+
|
|
11873
|
+
} catch (err) {
|
|
11874
|
+
this._stats.errorRuns++;
|
|
11875
|
+
finalReason = 'error';
|
|
11876
|
+
|
|
11877
|
+
logger.wuWarn(`[wu-ai] Agent run error: ${err.message}`);
|
|
11878
|
+
|
|
11879
|
+
this._eventBus.emit('ai:agent:error', {
|
|
11880
|
+
runId,
|
|
11881
|
+
goal,
|
|
11882
|
+
namespace,
|
|
11883
|
+
step,
|
|
11884
|
+
error: err.message,
|
|
11885
|
+
}, { appName: 'wu-ai' });
|
|
11886
|
+
|
|
11887
|
+
throw err;
|
|
11888
|
+
} finally {
|
|
11889
|
+
// Clean up
|
|
11890
|
+
this._activeRuns.delete(runId);
|
|
11891
|
+
|
|
11892
|
+
// Delete auto-generated agent namespaces to prevent memory leaks
|
|
11893
|
+
// User-provided namespaces are preserved (the user owns their lifecycle)
|
|
11894
|
+
if (!options.namespace && namespace.startsWith(AGENT_NAMESPACE_PREFIX)) {
|
|
11895
|
+
this._conversation.deleteNamespace(namespace);
|
|
11896
|
+
}
|
|
11897
|
+
|
|
11898
|
+
this._eventBus.emit('ai:agent:done', {
|
|
11899
|
+
runId,
|
|
11900
|
+
goal,
|
|
11901
|
+
namespace,
|
|
11902
|
+
totalSteps: step,
|
|
11903
|
+
reason: finalReason,
|
|
11904
|
+
}, { appName: 'wu-ai' });
|
|
11905
|
+
|
|
11906
|
+
logger.wuDebug(`[wu-ai] Agent run finished: ${finalReason} after ${step} step(s)`);
|
|
11907
|
+
}
|
|
11908
|
+
}
|
|
11909
|
+
|
|
11910
|
+
/**
|
|
11911
|
+
* Abort an active agent run by runId.
|
|
11912
|
+
*
|
|
11913
|
+
* @param {string} runId - The run ID to abort
|
|
11914
|
+
*/
|
|
11915
|
+
abort(runId) {
|
|
11916
|
+
const controller = this._activeRuns.get(runId);
|
|
11917
|
+
if (controller) {
|
|
11918
|
+
controller.abort();
|
|
11919
|
+
logger.wuDebug(`[wu-ai] Agent run aborted: ${runId}`);
|
|
11920
|
+
}
|
|
11921
|
+
}
|
|
11922
|
+
|
|
11923
|
+
/**
|
|
11924
|
+
* Abort all active agent runs.
|
|
11925
|
+
*/
|
|
11926
|
+
abortAll() {
|
|
11927
|
+
for (const [runId, controller] of this._activeRuns) {
|
|
11928
|
+
controller.abort();
|
|
11929
|
+
}
|
|
11930
|
+
this._activeRuns.clear();
|
|
11931
|
+
}
|
|
11932
|
+
|
|
11933
|
+
/**
|
|
11934
|
+
* Get IDs of currently active runs.
|
|
11935
|
+
*
|
|
11936
|
+
* @returns {string[]}
|
|
11937
|
+
*/
|
|
11938
|
+
getActiveRuns() {
|
|
11939
|
+
return [...this._activeRuns.keys()];
|
|
11940
|
+
}
|
|
11941
|
+
|
|
11942
|
+
/**
|
|
11943
|
+
* Get agent statistics.
|
|
11944
|
+
*
|
|
11945
|
+
* @returns {object}
|
|
11946
|
+
*/
|
|
11947
|
+
getStats() {
|
|
11948
|
+
return {
|
|
11949
|
+
...this._stats,
|
|
11950
|
+
activeRuns: this._activeRuns.size,
|
|
11951
|
+
config: { ...this._config },
|
|
11952
|
+
};
|
|
11953
|
+
}
|
|
11954
|
+
|
|
11955
|
+
/**
|
|
11956
|
+
* Destroy the agent, aborting all active runs.
|
|
11957
|
+
*/
|
|
11958
|
+
destroy() {
|
|
11959
|
+
this.abortAll();
|
|
11960
|
+
this._stats = {
|
|
11961
|
+
totalRuns: 0,
|
|
11962
|
+
totalSteps: 0,
|
|
11963
|
+
completedRuns: 0,
|
|
11964
|
+
abortedRuns: 0,
|
|
11965
|
+
errorRuns: 0,
|
|
11966
|
+
};
|
|
11967
|
+
}
|
|
11968
|
+
|
|
11969
|
+
// ─── Private ──────────────────────────────────────────────────
|
|
11970
|
+
|
|
11971
|
+
/**
|
|
11972
|
+
* Build the agent-specific system prompt. This tells the LLM it is
|
|
11973
|
+
* operating as an autonomous agent with a goal, available tools, and
|
|
11974
|
+
* the [DONE] completion protocol.
|
|
11975
|
+
*/
|
|
11976
|
+
async _buildAgentSystemPrompt(goal, options) {
|
|
11977
|
+
// Explicit override takes precedence
|
|
11978
|
+
const basePrompt = options.systemPrompt
|
|
11979
|
+
?? this._config.systemPrompt
|
|
11980
|
+
?? null;
|
|
11981
|
+
|
|
11982
|
+
if (basePrompt) {
|
|
11983
|
+
const resolved = typeof basePrompt === 'function'
|
|
11984
|
+
? await basePrompt(goal)
|
|
11985
|
+
: basePrompt;
|
|
11986
|
+
return resolved;
|
|
11987
|
+
}
|
|
11988
|
+
|
|
11989
|
+
// Auto-generate from context and tools
|
|
11990
|
+
const parts = [];
|
|
11991
|
+
|
|
11992
|
+
parts.push(
|
|
11993
|
+
'You are an autonomous AI agent connected to a live web application via Wu Framework.',
|
|
11994
|
+
'You have been given a goal and must work step-by-step to achieve it.',
|
|
11995
|
+
'',
|
|
11996
|
+
'PROTOCOL:',
|
|
11997
|
+
'- Each message you send is one "step" in your execution.',
|
|
11998
|
+
'- You may call tools to read or modify application state.',
|
|
11999
|
+
'- After each step, you will be prompted to continue.',
|
|
12000
|
+
'- When the goal is fully achieved, include the marker [DONE] in your response.',
|
|
12001
|
+
'- If you determine the goal cannot be achieved, include [DONE] and explain why.',
|
|
12002
|
+
'- Be concise. Each step should make meaningful progress.',
|
|
12003
|
+
'',
|
|
12004
|
+
);
|
|
12005
|
+
|
|
12006
|
+
// Collect context if available
|
|
12007
|
+
if (this._context) {
|
|
12008
|
+
try {
|
|
12009
|
+
await this._context.collect();
|
|
12010
|
+
const snapshot = this._context.getSnapshot();
|
|
12011
|
+
if (snapshot?._mountedApps?.length) {
|
|
12012
|
+
parts.push(`MOUNTED APPS: ${snapshot._mountedApps.join(', ')}`, '');
|
|
12013
|
+
}
|
|
12014
|
+
if (snapshot?._store && Object.keys(snapshot._store).length > 0) {
|
|
12015
|
+
parts.push(`APPLICATION STATE:\n${JSON.stringify(snapshot._store, null, 2)}`, '');
|
|
12016
|
+
}
|
|
12017
|
+
} catch {
|
|
12018
|
+
// Context collection is best-effort
|
|
12019
|
+
}
|
|
12020
|
+
}
|
|
12021
|
+
|
|
12022
|
+
// List available tools
|
|
12023
|
+
const tools = this._actions.getToolSchemas();
|
|
12024
|
+
if (tools.length > 0) {
|
|
12025
|
+
parts.push('AVAILABLE TOOLS:');
|
|
12026
|
+
for (const tool of tools) {
|
|
12027
|
+
const paramKeys = tool.parameters?.properties
|
|
12028
|
+
? Object.keys(tool.parameters.properties).join(', ')
|
|
12029
|
+
: 'none';
|
|
12030
|
+
parts.push(`- ${tool.name}(${paramKeys}): ${tool.description}`);
|
|
12031
|
+
}
|
|
12032
|
+
parts.push('');
|
|
12033
|
+
}
|
|
12034
|
+
|
|
12035
|
+
parts.push(`GOAL: ${goal}`);
|
|
12036
|
+
|
|
12037
|
+
return parts.join('\n');
|
|
12038
|
+
}
|
|
12039
|
+
|
|
12040
|
+
/**
|
|
12041
|
+
* Build a normalized step result object.
|
|
12042
|
+
*
|
|
12043
|
+
* @param {number} step
|
|
12044
|
+
* @param {string} type
|
|
12045
|
+
* @param {object} data
|
|
12046
|
+
* @returns {AgentStepResult}
|
|
12047
|
+
*/
|
|
12048
|
+
_buildStepResult(step, type, data = {}) {
|
|
12049
|
+
return {
|
|
12050
|
+
step,
|
|
12051
|
+
type,
|
|
12052
|
+
content: data.content ?? null,
|
|
12053
|
+
toolResults: data.toolResults ?? null,
|
|
12054
|
+
usage: data.usage ?? null,
|
|
12055
|
+
reason: data.reason ?? null,
|
|
12056
|
+
elapsed: data.elapsed ?? 0,
|
|
12057
|
+
};
|
|
12058
|
+
}
|
|
12059
|
+
|
|
12060
|
+
/**
|
|
12061
|
+
* Emit an ai:agent:step event.
|
|
12062
|
+
*/
|
|
12063
|
+
_emitStep(runId, result) {
|
|
12064
|
+
this._eventBus.emit('ai:agent:step', {
|
|
12065
|
+
runId,
|
|
12066
|
+
...result,
|
|
12067
|
+
}, { appName: 'wu-ai' });
|
|
12068
|
+
}
|
|
12069
|
+
|
|
12070
|
+
/**
|
|
12071
|
+
* Safely invoke a callback, swallowing errors so the agent loop
|
|
12072
|
+
* is never broken by a faulty onStep handler.
|
|
12073
|
+
*/
|
|
12074
|
+
async _safeCallback(fn, ...args) {
|
|
12075
|
+
try {
|
|
12076
|
+
const result = fn(...args);
|
|
12077
|
+
if (result && typeof result.then === 'function') {
|
|
12078
|
+
await result;
|
|
12079
|
+
}
|
|
12080
|
+
} catch (err) {
|
|
12081
|
+
logger.wuDebug(`[wu-ai] Agent callback error: ${err.message}`);
|
|
12082
|
+
}
|
|
12083
|
+
}
|
|
12084
|
+
|
|
12085
|
+
/**
|
|
12086
|
+
* Generate a unique namespace for an agent run.
|
|
12087
|
+
*/
|
|
12088
|
+
_generateNamespace() {
|
|
12089
|
+
return AGENT_NAMESPACE_PREFIX + Date.now().toString(36) + '_' + Math.random().toString(36).slice(2, 6);
|
|
12090
|
+
}
|
|
12091
|
+
|
|
12092
|
+
/**
|
|
12093
|
+
* Generate a unique run ID.
|
|
12094
|
+
*/
|
|
12095
|
+
_generateRunId() {
|
|
12096
|
+
return 'run_' + Date.now().toString(36) + '_' + Math.random().toString(36).slice(2, 8);
|
|
12097
|
+
}
|
|
12098
|
+
}
|
|
12099
|
+
|
|
12100
|
+
/**
|
|
12101
|
+
* WU-AI Browser Primitives
|
|
12102
|
+
*
|
|
12103
|
+
* Shared browser capabilities used by both wu-ai-browser.js (Paradigm 1/2)
|
|
12104
|
+
* and wu-mcp-bridge.js (Paradigm 3). Single source of truth to avoid
|
|
12105
|
+
* duplicating interceptors, DOM traversal, and Canvas rendering.
|
|
12106
|
+
*/
|
|
12107
|
+
|
|
12108
|
+
// ── Shared capture buffers (singleton) ──
|
|
12109
|
+
|
|
12110
|
+
const networkLog = [];
|
|
12111
|
+
const MAX_NETWORK_LOG = 300;
|
|
12112
|
+
const consoleLog = [];
|
|
12113
|
+
const MAX_CONSOLE_LOG = 500;
|
|
12114
|
+
|
|
12115
|
+
let _interceptorsInstalled = false;
|
|
12116
|
+
|
|
12117
|
+
/**
|
|
12118
|
+
* Install network + console interceptors (idempotent — only runs once).
|
|
12119
|
+
*/
|
|
12120
|
+
function ensureInterceptors() {
|
|
12121
|
+
if (_interceptorsInstalled) return;
|
|
12122
|
+
_installNetworkInterceptor();
|
|
12123
|
+
_installConsoleInterceptor();
|
|
12124
|
+
_interceptorsInstalled = true;
|
|
12125
|
+
}
|
|
12126
|
+
|
|
12127
|
+
/**
|
|
12128
|
+
* Build an accessibility tree representation of a DOM element.
|
|
12129
|
+
* Traverses into Shadow DOM if present.
|
|
12130
|
+
*/
|
|
12131
|
+
function buildA11yTree(el, depth = 0, maxDepth = 5) {
|
|
12132
|
+
if (depth > maxDepth || !el) return '';
|
|
12133
|
+
|
|
12134
|
+
const indent = ' '.repeat(depth);
|
|
12135
|
+
const tag = el.tagName?.toLowerCase() || '';
|
|
12136
|
+
const role = el.getAttribute?.('role') || '';
|
|
12137
|
+
const ariaLabel = el.getAttribute?.('aria-label') || '';
|
|
12138
|
+
const text = el.childNodes?.length === 1 && el.childNodes[0].nodeType === 3
|
|
12139
|
+
? el.textContent?.trim().slice(0, 80) : '';
|
|
12140
|
+
|
|
12141
|
+
let line = `${indent}<${tag}`;
|
|
12142
|
+
if (el.id) line += ` id="${el.id}"`;
|
|
12143
|
+
if (role) line += ` role="${role}"`;
|
|
12144
|
+
if (ariaLabel) line += ` aria-label="${ariaLabel}"`;
|
|
12145
|
+
if (el.className && typeof el.className === 'string') {
|
|
12146
|
+
const cls = el.className.trim().slice(0, 60);
|
|
12147
|
+
if (cls) line += ` class="${cls}"`;
|
|
12148
|
+
}
|
|
12149
|
+
line += '>';
|
|
12150
|
+
if (text) line += ` "${text}"`;
|
|
12151
|
+
|
|
12152
|
+
let result = line + '\n';
|
|
12153
|
+
const root = el.shadowRoot || el;
|
|
12154
|
+
const children = root.children || [];
|
|
12155
|
+
|
|
12156
|
+
for (let i = 0; i < children.length && i < 50; i++) {
|
|
12157
|
+
result += buildA11yTree(children[i], depth + 1, maxDepth);
|
|
12158
|
+
}
|
|
12159
|
+
return result;
|
|
12160
|
+
}
|
|
12161
|
+
|
|
12162
|
+
/**
|
|
12163
|
+
* Recursively inline computed styles from source to clone for Canvas rendering.
|
|
12164
|
+
*/
|
|
12165
|
+
function inlineComputedStyles(source, clone) {
|
|
12166
|
+
const props = ['color', 'background', 'background-color', 'font-family',
|
|
12167
|
+
'font-size', 'font-weight', 'border', 'border-radius', 'padding', 'margin',
|
|
12168
|
+
'display', 'flex-direction', 'align-items', 'justify-content', 'gap',
|
|
12169
|
+
'width', 'height', 'max-width', 'max-height', 'overflow', 'opacity',
|
|
12170
|
+
'box-shadow', 'text-align', 'line-height', 'position', 'top', 'left',
|
|
12171
|
+
'right', 'bottom', 'z-index', 'transform', 'visibility'];
|
|
12172
|
+
|
|
12173
|
+
try {
|
|
12174
|
+
const style = window.getComputedStyle(source);
|
|
12175
|
+
for (const prop of props) {
|
|
12176
|
+
const val = style.getPropertyValue(prop);
|
|
12177
|
+
if (val) clone.style?.setProperty(prop, val);
|
|
12178
|
+
}
|
|
12179
|
+
} catch (_) { /* skip */ }
|
|
12180
|
+
|
|
12181
|
+
const srcKids = source.children || [];
|
|
12182
|
+
const cloneKids = clone.children || [];
|
|
12183
|
+
const max = Math.min(srcKids.length, cloneKids.length, 200);
|
|
12184
|
+
for (let i = 0; i < max; i++) {
|
|
12185
|
+
inlineComputedStyles(srcKids[i], cloneKids[i]);
|
|
12186
|
+
}
|
|
12187
|
+
}
|
|
12188
|
+
|
|
12189
|
+
/**
|
|
12190
|
+
* Capture a screenshot of a DOM element via Canvas API (SVG foreignObject).
|
|
12191
|
+
* @returns {Promise<{ width, height, format, base64, sizeKB } | { error: string }>}
|
|
12192
|
+
*/
|
|
12193
|
+
async function captureScreenshot(selector, quality = 0.8) {
|
|
12194
|
+
const target = selector
|
|
12195
|
+
? document.querySelector(selector)
|
|
12196
|
+
: document.documentElement;
|
|
12197
|
+
|
|
12198
|
+
if (!target) return { error: `Element not found: ${selector}` };
|
|
12199
|
+
|
|
12200
|
+
const rect = target.getBoundingClientRect();
|
|
12201
|
+
const w = Math.ceil(Math.min(rect.width || window.innerWidth, 1920));
|
|
12202
|
+
const h = Math.ceil(Math.min(rect.height || window.innerHeight, 1080));
|
|
12203
|
+
|
|
12204
|
+
const clone = target.cloneNode(true);
|
|
12205
|
+
inlineComputedStyles(target, clone);
|
|
12206
|
+
|
|
12207
|
+
const serializer = new XMLSerializer();
|
|
12208
|
+
const xhtml = serializer.serializeToString(clone);
|
|
12209
|
+
|
|
12210
|
+
const svgStr = [
|
|
12211
|
+
`<svg xmlns="http://www.w3.org/2000/svg" width="${w}" height="${h}">`,
|
|
12212
|
+
'<foreignObject width="100%" height="100%">',
|
|
12213
|
+
`<div xmlns="http://www.w3.org/1999/xhtml" style="width:${w}px;height:${h}px;overflow:hidden;">`,
|
|
12214
|
+
xhtml,
|
|
12215
|
+
'</div>',
|
|
12216
|
+
'</foreignObject>',
|
|
12217
|
+
'</svg>',
|
|
12218
|
+
].join('');
|
|
12219
|
+
|
|
12220
|
+
const svgBlob = new Blob([svgStr], { type: 'image/svg+xml;charset=utf-8' });
|
|
12221
|
+
const url = URL.createObjectURL(svgBlob);
|
|
12222
|
+
|
|
12223
|
+
const dataUrl = await new Promise((resolve) => {
|
|
12224
|
+
const img = new Image();
|
|
12225
|
+
img.onload = () => {
|
|
12226
|
+
const canvas = document.createElement('canvas');
|
|
12227
|
+
canvas.width = w;
|
|
12228
|
+
canvas.height = h;
|
|
12229
|
+
const ctx = canvas.getContext('2d');
|
|
12230
|
+
ctx.drawImage(img, 0, 0);
|
|
12231
|
+
URL.revokeObjectURL(url);
|
|
12232
|
+
resolve(canvas.toDataURL('image/png', quality));
|
|
12233
|
+
};
|
|
12234
|
+
img.onerror = () => {
|
|
12235
|
+
URL.revokeObjectURL(url);
|
|
12236
|
+
resolve(null);
|
|
12237
|
+
};
|
|
12238
|
+
img.src = url;
|
|
12239
|
+
});
|
|
12240
|
+
|
|
12241
|
+
if (!dataUrl) return { error: 'Canvas rendering failed' };
|
|
12242
|
+
|
|
12243
|
+
const base64 = dataUrl.split(',')[1];
|
|
12244
|
+
return {
|
|
12245
|
+
width: w,
|
|
12246
|
+
height: h,
|
|
12247
|
+
format: 'png',
|
|
12248
|
+
base64,
|
|
12249
|
+
sizeKB: Math.round((base64.length * 3) / 4 / 1024),
|
|
12250
|
+
};
|
|
12251
|
+
}
|
|
12252
|
+
|
|
12253
|
+
/**
|
|
12254
|
+
* Click an element by CSS selector or visible text content.
|
|
12255
|
+
* @returns {{ clicked, text } | { error }}
|
|
12256
|
+
*/
|
|
12257
|
+
function clickElement(selector, text) {
|
|
12258
|
+
let el = null;
|
|
12259
|
+
|
|
12260
|
+
if (selector) {
|
|
12261
|
+
el = document.querySelector(selector);
|
|
12262
|
+
}
|
|
12263
|
+
|
|
12264
|
+
if (!el && text) {
|
|
12265
|
+
const candidates = document.querySelectorAll(
|
|
12266
|
+
'button, a, [role="button"], input[type="submit"], input[type="button"], [data-click], label, [onclick]'
|
|
12267
|
+
);
|
|
12268
|
+
const searchText = text.toLowerCase();
|
|
12269
|
+
for (const candidate of candidates) {
|
|
12270
|
+
if (candidate.textContent?.trim().toLowerCase().includes(searchText)) {
|
|
12271
|
+
el = candidate;
|
|
12272
|
+
break;
|
|
12273
|
+
}
|
|
12274
|
+
}
|
|
12275
|
+
}
|
|
12276
|
+
|
|
12277
|
+
if (!el) return { error: `Element not found: ${selector || `text="${text}"`}` };
|
|
12278
|
+
|
|
12279
|
+
el.scrollIntoView({ behavior: 'instant', block: 'center' });
|
|
12280
|
+
el.click();
|
|
12281
|
+
|
|
12282
|
+
const tag = el.tagName?.toLowerCase();
|
|
12283
|
+
const id = el.id ? `#${el.id}` : '';
|
|
12284
|
+
return {
|
|
12285
|
+
clicked: `${tag}${id}`,
|
|
12286
|
+
text: el.textContent?.trim().slice(0, 100) || '',
|
|
12287
|
+
rect: el.getBoundingClientRect().toJSON?.() || null,
|
|
12288
|
+
};
|
|
12289
|
+
}
|
|
12290
|
+
|
|
12291
|
+
/**
|
|
12292
|
+
* Type into an input, textarea, or contenteditable element.
|
|
12293
|
+
* Works with React, Vue, Angular, and other frameworks.
|
|
12294
|
+
* @returns {{ selector, typed, currentValue, submitted } | { error }}
|
|
12295
|
+
*/
|
|
12296
|
+
function typeIntoElement(selector, text, { clear = false, submit = false } = {}) {
|
|
12297
|
+
if (!selector) return { error: 'selector is required' };
|
|
12298
|
+
if (text === undefined) return { error: 'text is required' };
|
|
12299
|
+
|
|
12300
|
+
const el = document.querySelector(selector);
|
|
12301
|
+
if (!el) return { error: `Element not found: ${selector}` };
|
|
12302
|
+
|
|
12303
|
+
el.focus();
|
|
12304
|
+
|
|
12305
|
+
if (clear) {
|
|
12306
|
+
el.value = '';
|
|
12307
|
+
el.dispatchEvent(new Event('input', { bubbles: true }));
|
|
12308
|
+
}
|
|
12309
|
+
|
|
12310
|
+
// Use native setter to trigger framework reactivity (React, Vue, etc.)
|
|
12311
|
+
const nativeSetter = Object.getOwnPropertyDescriptor(
|
|
12312
|
+
window.HTMLInputElement.prototype, 'value'
|
|
12313
|
+
)?.set || Object.getOwnPropertyDescriptor(
|
|
12314
|
+
window.HTMLTextAreaElement.prototype, 'value'
|
|
12315
|
+
)?.set;
|
|
12316
|
+
|
|
12317
|
+
const newValue = clear ? text : (el.value || '') + text;
|
|
12318
|
+
if (nativeSetter) {
|
|
12319
|
+
nativeSetter.call(el, newValue);
|
|
12320
|
+
} else {
|
|
12321
|
+
el.value = newValue;
|
|
12322
|
+
}
|
|
12323
|
+
|
|
12324
|
+
el.dispatchEvent(new Event('input', { bubbles: true }));
|
|
12325
|
+
el.dispatchEvent(new Event('change', { bubbles: true }));
|
|
12326
|
+
|
|
12327
|
+
if (submit) {
|
|
12328
|
+
const form = el.closest('form');
|
|
12329
|
+
if (form) {
|
|
12330
|
+
form.dispatchEvent(new Event('submit', { bubbles: true, cancelable: true }));
|
|
12331
|
+
} else {
|
|
12332
|
+
el.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter', code: 'Enter', bubbles: true }));
|
|
12333
|
+
}
|
|
12334
|
+
}
|
|
12335
|
+
|
|
12336
|
+
return {
|
|
12337
|
+
selector,
|
|
12338
|
+
typed: text,
|
|
12339
|
+
currentValue: el.value?.slice(0, 200),
|
|
12340
|
+
submitted: !!submit,
|
|
12341
|
+
};
|
|
12342
|
+
}
|
|
12343
|
+
|
|
12344
|
+
/**
|
|
12345
|
+
* Filter and return network log entries.
|
|
12346
|
+
*/
|
|
12347
|
+
function getFilteredNetwork(method, status, limit = 30) {
|
|
12348
|
+
let filtered = networkLog;
|
|
12349
|
+
if (method) {
|
|
12350
|
+
filtered = filtered.filter((r) => r.method === method.toUpperCase());
|
|
12351
|
+
}
|
|
12352
|
+
if (status) {
|
|
12353
|
+
if (status === 'error') {
|
|
12354
|
+
filtered = filtered.filter((r) => r.status === 0 || r.status >= 400);
|
|
12355
|
+
} else {
|
|
12356
|
+
filtered = filtered.filter((r) => String(r.status).startsWith(String(status)));
|
|
12357
|
+
}
|
|
12358
|
+
}
|
|
12359
|
+
return {
|
|
12360
|
+
requests: filtered.slice(-limit),
|
|
12361
|
+
total: networkLog.length,
|
|
12362
|
+
showing: Math.min(filtered.length, limit),
|
|
12363
|
+
};
|
|
12364
|
+
}
|
|
12365
|
+
|
|
12366
|
+
/**
|
|
12367
|
+
* Filter and return console log entries.
|
|
12368
|
+
*/
|
|
12369
|
+
function getFilteredConsole(level, limit = 30) {
|
|
12370
|
+
const filtered = level && level !== 'all'
|
|
12371
|
+
? consoleLog.filter((m) => m.level === level)
|
|
12372
|
+
: consoleLog;
|
|
12373
|
+
return {
|
|
12374
|
+
messages: filtered.slice(-limit),
|
|
12375
|
+
total: consoleLog.length,
|
|
12376
|
+
showing: Math.min(filtered.length, limit),
|
|
12377
|
+
};
|
|
12378
|
+
}
|
|
12379
|
+
|
|
12380
|
+
// ── Private: Interceptors ──
|
|
12381
|
+
|
|
12382
|
+
function _installNetworkInterceptor() {
|
|
12383
|
+
// Intercept fetch
|
|
12384
|
+
const originalFetch = window.fetch;
|
|
12385
|
+
window.fetch = async function (...args) {
|
|
12386
|
+
const start = Date.now();
|
|
12387
|
+
const req = args[0];
|
|
12388
|
+
const url = typeof req === 'string' ? req : req?.url || '';
|
|
12389
|
+
const method = (args[1]?.method || req?.method || 'GET').toUpperCase();
|
|
12390
|
+
|
|
12391
|
+
try {
|
|
12392
|
+
const response = await originalFetch.apply(window, args);
|
|
12393
|
+
const size = parseInt(response.headers?.get('content-length') || '0', 10);
|
|
12394
|
+
networkLog.push({
|
|
12395
|
+
type: 'fetch', method, url,
|
|
12396
|
+
status: response.status, statusText: response.statusText,
|
|
12397
|
+
duration: Date.now() - start, size, timestamp: start,
|
|
12398
|
+
});
|
|
12399
|
+
if (networkLog.length > MAX_NETWORK_LOG) networkLog.shift();
|
|
12400
|
+
return response;
|
|
12401
|
+
} catch (err) {
|
|
12402
|
+
networkLog.push({
|
|
12403
|
+
type: 'fetch', method, url,
|
|
12404
|
+
status: 0, error: err.message,
|
|
12405
|
+
duration: Date.now() - start, timestamp: start,
|
|
12406
|
+
});
|
|
12407
|
+
if (networkLog.length > MAX_NETWORK_LOG) networkLog.shift();
|
|
12408
|
+
throw err;
|
|
12409
|
+
}
|
|
12410
|
+
};
|
|
12411
|
+
|
|
12412
|
+
// Intercept XMLHttpRequest
|
|
12413
|
+
const origOpen = XMLHttpRequest.prototype.open;
|
|
12414
|
+
const origSend = XMLHttpRequest.prototype.send;
|
|
12415
|
+
|
|
12416
|
+
XMLHttpRequest.prototype.open = function (method, url, ...rest) {
|
|
12417
|
+
this._wuAi = { method: (method || 'GET').toUpperCase(), url: String(url) };
|
|
12418
|
+
return origOpen.call(this, method, url, ...rest);
|
|
12419
|
+
};
|
|
12420
|
+
|
|
12421
|
+
XMLHttpRequest.prototype.send = function (...args) {
|
|
12422
|
+
if (this._wuAi) {
|
|
12423
|
+
this._wuAi.start = Date.now();
|
|
12424
|
+
this.addEventListener('loadend', () => {
|
|
12425
|
+
networkLog.push({
|
|
12426
|
+
type: 'xhr', method: this._wuAi.method, url: this._wuAi.url,
|
|
12427
|
+
status: this.status, statusText: this.statusText,
|
|
12428
|
+
duration: Date.now() - this._wuAi.start,
|
|
12429
|
+
size: parseInt(this.getResponseHeader('content-length') || '0', 10),
|
|
12430
|
+
timestamp: this._wuAi.start,
|
|
12431
|
+
});
|
|
12432
|
+
if (networkLog.length > MAX_NETWORK_LOG) networkLog.shift();
|
|
12433
|
+
});
|
|
12434
|
+
}
|
|
12435
|
+
return origSend.apply(this, args);
|
|
12436
|
+
};
|
|
12437
|
+
}
|
|
12438
|
+
|
|
12439
|
+
function _installConsoleInterceptor() {
|
|
12440
|
+
const levels = ['log', 'warn', 'error'];
|
|
12441
|
+
for (const level of levels) {
|
|
12442
|
+
const original = console[level];
|
|
12443
|
+
console[level] = (...args) => {
|
|
12444
|
+
consoleLog.push({
|
|
12445
|
+
level,
|
|
12446
|
+
message: args.map((a) => (typeof a === 'object' ? JSON.stringify(a) : String(a))).join(' '),
|
|
12447
|
+
timestamp: Date.now(),
|
|
12448
|
+
});
|
|
12449
|
+
if (consoleLog.length > MAX_CONSOLE_LOG) consoleLog.shift();
|
|
12450
|
+
original.apply(console, args);
|
|
12451
|
+
};
|
|
12452
|
+
}
|
|
12453
|
+
}
|
|
12454
|
+
|
|
12455
|
+
/**
|
|
12456
|
+
* WU-AI-ORCHESTRATE: Cross-Micro-App AI Coordination (Paradigm 4)
|
|
12457
|
+
*
|
|
12458
|
+
* The fourth paradigm of wu.ai:
|
|
12459
|
+
* Paradigm 1 — App sends messages to LLM (conversation)
|
|
12460
|
+
* Paradigm 2 — External LLM calls into app (tools/WebMCP)
|
|
12461
|
+
* Paradigm 3 — AI as autonomous director (agent loop)
|
|
12462
|
+
* Paradigm 4 — AI as microfrontend glue (this file)
|
|
12463
|
+
*
|
|
12464
|
+
* The Problem:
|
|
12465
|
+
* In microfrontend architectures, apps are isolated by design.
|
|
12466
|
+
* Cross-app coordination requires manual wiring through events
|
|
12467
|
+
* and shared state. As apps grow, wiring becomes n² complexity.
|
|
12468
|
+
*
|
|
12469
|
+
* The Solution:
|
|
12470
|
+
* Each micro-app declares its capabilities to the AI layer.
|
|
12471
|
+
* The AI understands the semantic meaning of each capability
|
|
12472
|
+
* and can resolve natural-language intents by calling the right
|
|
12473
|
+
* actions across the right apps — without tight coupling.
|
|
12474
|
+
*
|
|
12475
|
+
* Key Concepts:
|
|
12476
|
+
* - Capability: An action scoped to a specific micro-app
|
|
12477
|
+
* Registered as 'appName:actionName', cleaned up on unmount.
|
|
12478
|
+
* - Intent: A natural-language cross-app request resolved in
|
|
12479
|
+
* a single conversation turn with an orchestrator system prompt.
|
|
12480
|
+
* - Capability Map: The AI's understanding of system topology —
|
|
12481
|
+
* which apps exist and what each can do.
|
|
12482
|
+
*
|
|
12483
|
+
* This module does NOT replace actions, triggers, or agents.
|
|
12484
|
+
* It enriches them with cross-app topology awareness.
|
|
12485
|
+
*
|
|
12486
|
+
* API (accessible via wu.ai):
|
|
12487
|
+
* wu.ai.capability(app, name, config) → Register app-scoped capability
|
|
12488
|
+
* wu.ai.intent(description, options) → Resolve cross-app intent
|
|
12489
|
+
* wu.ai.removeApp(appName) → Cleanup on unmount
|
|
12490
|
+
* wu.ai.workflow(name, config) → Register reusable AI workflow
|
|
12491
|
+
* wu.ai.runWorkflow(name, params, opts) → Execute a registered workflow
|
|
12492
|
+
*/
|
|
12493
|
+
|
|
12494
|
+
|
|
12495
|
+
// ─── Constants ──────────────────────────────────────────────────
|
|
12496
|
+
|
|
12497
|
+
const INTENT_NAMESPACE_PREFIX = 'intent:';
|
|
12498
|
+
|
|
12499
|
+
// ─── Deterministic Step Actions ─────────────────────────────────
|
|
12500
|
+
|
|
12501
|
+
/**
|
|
12502
|
+
* Execute a single deterministic workflow step.
|
|
12503
|
+
* No AI needed — directly calls browser primitives.
|
|
12504
|
+
*
|
|
12505
|
+
* @param {object} step - Step definition
|
|
12506
|
+
* @param {string} step.action - 'click' | 'type' | 'navigate' | 'wait' | 'emit' | 'setState'
|
|
12507
|
+
* @param {object} params - Interpolated params
|
|
12508
|
+
* @returns {{ success: boolean, detail?: string, error?: string }}
|
|
12509
|
+
*/
|
|
12510
|
+
function executeDeterministicStep(step, eventBus, store) {
|
|
12511
|
+
switch (step.action) {
|
|
12512
|
+
case 'click': {
|
|
12513
|
+
const result = clickElement(step.selector, step.text);
|
|
12514
|
+
if (result.error) return { success: false, error: result.error };
|
|
12515
|
+
return { success: true, detail: `Clicked: ${step.selector || step.text}` };
|
|
12516
|
+
}
|
|
12517
|
+
|
|
12518
|
+
case 'type': {
|
|
12519
|
+
const result = typeIntoElement(step.selector, step.value, {
|
|
12520
|
+
clear: step.clear ?? true,
|
|
12521
|
+
submit: step.submit ?? false,
|
|
12522
|
+
});
|
|
12523
|
+
if (result.error) return { success: false, error: result.error };
|
|
12524
|
+
return { success: true, detail: `Typed "${step.value}" into ${step.selector}` };
|
|
12525
|
+
}
|
|
12526
|
+
|
|
12527
|
+
case 'navigate': {
|
|
12528
|
+
if (eventBus && step.section) {
|
|
12529
|
+
eventBus.emit('nav:section', { section: step.section }, { appName: 'wu-ai' });
|
|
12530
|
+
return { success: true, detail: `Navigated to section: ${step.section}` };
|
|
12531
|
+
}
|
|
12532
|
+
if (step.selector) {
|
|
12533
|
+
const result = clickElement(step.selector, step.text);
|
|
12534
|
+
if (result.error) return { success: false, error: result.error };
|
|
12535
|
+
return { success: true, detail: `Navigated via click: ${step.selector}` };
|
|
12536
|
+
}
|
|
12537
|
+
return { success: false, error: 'navigate requires "section" or "selector"' };
|
|
12538
|
+
}
|
|
12539
|
+
|
|
12540
|
+
case 'wait': {
|
|
12541
|
+
// Wait is handled by the runner (delay + optional selector poll)
|
|
12542
|
+
return { success: true, detail: `Wait: ${step.ms || 0}ms` };
|
|
12543
|
+
}
|
|
12544
|
+
|
|
12545
|
+
case 'emit': {
|
|
12546
|
+
if (!eventBus) return { success: false, error: 'eventBus not available' };
|
|
12547
|
+
eventBus.emit(step.event, step.data || {}, { appName: 'wu-ai' });
|
|
12548
|
+
return { success: true, detail: `Emitted: ${step.event}` };
|
|
12549
|
+
}
|
|
12550
|
+
|
|
12551
|
+
case 'setState': {
|
|
12552
|
+
if (!store) return { success: false, error: 'store not available' };
|
|
12553
|
+
store.set(step.path, step.value);
|
|
12554
|
+
return { success: true, detail: `Set state: ${step.path}` };
|
|
12555
|
+
}
|
|
12556
|
+
|
|
12557
|
+
default:
|
|
12558
|
+
return { success: false, error: `Unknown action: ${step.action}` };
|
|
12559
|
+
}
|
|
12560
|
+
}
|
|
12561
|
+
|
|
12562
|
+
/**
|
|
12563
|
+
* Wait for a selector to appear in the DOM (with timeout).
|
|
12564
|
+
*
|
|
12565
|
+
* @param {string} selector
|
|
12566
|
+
* @param {number} timeout - ms
|
|
12567
|
+
* @returns {Promise<boolean>}
|
|
12568
|
+
*/
|
|
12569
|
+
function waitForSelector(selector, timeout = 5000) {
|
|
12570
|
+
return new Promise((resolve) => {
|
|
12571
|
+
if (document.querySelector(selector)) {
|
|
12572
|
+
resolve(true);
|
|
12573
|
+
return;
|
|
12574
|
+
}
|
|
12575
|
+
|
|
12576
|
+
const interval = 100;
|
|
12577
|
+
let elapsed = 0;
|
|
12578
|
+
const timer = setInterval(() => {
|
|
12579
|
+
elapsed += interval;
|
|
12580
|
+
if (document.querySelector(selector)) {
|
|
12581
|
+
clearInterval(timer);
|
|
12582
|
+
resolve(true);
|
|
12583
|
+
} else if (elapsed >= timeout) {
|
|
12584
|
+
clearInterval(timer);
|
|
12585
|
+
resolve(false);
|
|
12586
|
+
}
|
|
12587
|
+
}, interval);
|
|
12588
|
+
});
|
|
12589
|
+
}
|
|
12590
|
+
|
|
12591
|
+
/**
|
|
12592
|
+
* Simple delay.
|
|
12593
|
+
*/
|
|
12594
|
+
function delay(ms) {
|
|
12595
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
12596
|
+
}
|
|
12597
|
+
|
|
12598
|
+
// ─── WuAIOrchestrate ────────────────────────────────────────────
|
|
12599
|
+
|
|
12600
|
+
class WuAIOrchestrate {
|
|
12601
|
+
/**
|
|
12602
|
+
* @param {object} deps
|
|
12603
|
+
* @param {import('./wu-ai-actions.js').WuAIActions} deps.actions
|
|
12604
|
+
* @param {import('./wu-ai-conversation.js').WuAIConversation} deps.conversation
|
|
12605
|
+
* @param {import('./wu-ai-context.js').WuAIContext} deps.context
|
|
12606
|
+
* @param {import('./wu-ai-permissions.js').WuAIPermissions} deps.permissions
|
|
12607
|
+
* @param {object} deps.eventBus
|
|
12608
|
+
*/
|
|
12609
|
+
constructor({ actions, conversation, context, permissions, eventBus, agent, store }) {
|
|
12610
|
+
this._actions = actions;
|
|
12611
|
+
this._conversation = conversation;
|
|
12612
|
+
this._context = context;
|
|
12613
|
+
this._permissions = permissions;
|
|
12614
|
+
this._eventBus = eventBus;
|
|
12615
|
+
this._agent = agent; // WuAIAgent — for workflow execution
|
|
12616
|
+
this._store = store; // WuStore — for deterministic setState steps
|
|
12617
|
+
|
|
12618
|
+
// appName → Map<actionName, { description, qualifiedName }>
|
|
12619
|
+
this._capabilities = new Map();
|
|
12620
|
+
|
|
12621
|
+
// name → { goal, steps, parameters, provider, ... }
|
|
12622
|
+
this._workflows = new Map();
|
|
12623
|
+
|
|
12624
|
+
this._config = {
|
|
12625
|
+
defaultProvider: null,
|
|
12626
|
+
defaultTemperature: 0.3, // lower temp for orchestration
|
|
12627
|
+
};
|
|
12628
|
+
|
|
12629
|
+
this._stats = {
|
|
12630
|
+
totalIntents: 0,
|
|
12631
|
+
resolvedIntents: 0,
|
|
12632
|
+
failedIntents: 0,
|
|
12633
|
+
workflowsRegistered: 0,
|
|
12634
|
+
workflowsExecuted: 0,
|
|
12635
|
+
};
|
|
12636
|
+
}
|
|
12637
|
+
|
|
12638
|
+
/**
|
|
12639
|
+
* Post-init configuration.
|
|
12640
|
+
*
|
|
12641
|
+
* @param {object} config
|
|
12642
|
+
* @param {string} [config.defaultProvider] - Default provider for intents
|
|
12643
|
+
* @param {number} [config.defaultTemperature] - Default temperature for intents
|
|
12644
|
+
*/
|
|
12645
|
+
configure(config) {
|
|
12646
|
+
if (config.defaultProvider !== undefined) this._config.defaultProvider = config.defaultProvider;
|
|
12647
|
+
if (config.defaultTemperature !== undefined) this._config.defaultTemperature = config.defaultTemperature;
|
|
12648
|
+
}
|
|
12649
|
+
|
|
12650
|
+
// ─── Capability Registration ────────────────────────────────────
|
|
12651
|
+
|
|
12652
|
+
/**
|
|
12653
|
+
* Register a capability scoped to a micro-app.
|
|
12654
|
+
*
|
|
12655
|
+
* Under the hood this registers a normal action with the qualified
|
|
12656
|
+
* name 'appName:actionName' so the LLM can call it directly.
|
|
12657
|
+
* The capability map is tracked separately for lifecycle management
|
|
12658
|
+
* (removeApp) and system prompt enrichment.
|
|
12659
|
+
*
|
|
12660
|
+
* @param {string} appName - The micro-app name (e.g., 'orders', 'dashboard')
|
|
12661
|
+
* @param {string} actionName - The capability name (e.g., 'getRecent', 'updateKPIs')
|
|
12662
|
+
* @param {object} config - Same as wu.ai.action() config:
|
|
12663
|
+
* { description, parameters, handler, confirm?, permissions?, dangerous? }
|
|
12664
|
+
*/
|
|
12665
|
+
register(appName, actionName, config) {
|
|
12666
|
+
if (!appName || !actionName) {
|
|
12667
|
+
throw new Error('[wu-ai] capability() requires both appName and actionName');
|
|
12668
|
+
}
|
|
12669
|
+
if (!config || typeof config.handler !== 'function') {
|
|
12670
|
+
throw new Error(`[wu-ai] capability '${appName}:${actionName}' must have a handler function`);
|
|
12671
|
+
}
|
|
12672
|
+
|
|
12673
|
+
const qualifiedName = `${appName}:${actionName}`;
|
|
12674
|
+
|
|
12675
|
+
// Track in capability map
|
|
12676
|
+
if (!this._capabilities.has(appName)) {
|
|
12677
|
+
this._capabilities.set(appName, new Map());
|
|
12678
|
+
}
|
|
12679
|
+
this._capabilities.get(appName).set(actionName, {
|
|
12680
|
+
description: config.description || actionName,
|
|
12681
|
+
qualifiedName,
|
|
12682
|
+
});
|
|
12683
|
+
|
|
12684
|
+
// Register as a normal action with enriched description
|
|
12685
|
+
this._actions.register(qualifiedName, {
|
|
12686
|
+
...config,
|
|
12687
|
+
description: `[${appName}] ${config.description || actionName}`,
|
|
12688
|
+
});
|
|
12689
|
+
|
|
12690
|
+
logger.wuDebug(`[wu-ai] Capability registered: ${qualifiedName}`);
|
|
12691
|
+
}
|
|
12692
|
+
|
|
12693
|
+
/**
|
|
12694
|
+
* Remove a single capability.
|
|
12695
|
+
*
|
|
12696
|
+
* @param {string} appName
|
|
12697
|
+
* @param {string} actionName
|
|
12698
|
+
*/
|
|
12699
|
+
unregister(appName, actionName) {
|
|
12700
|
+
const appCaps = this._capabilities.get(appName);
|
|
12701
|
+
if (!appCaps) return;
|
|
12702
|
+
|
|
12703
|
+
const qualifiedName = `${appName}:${actionName}`;
|
|
12704
|
+
appCaps.delete(actionName);
|
|
12705
|
+
this._actions.unregister(qualifiedName);
|
|
12706
|
+
|
|
12707
|
+
// Clean up empty app entry
|
|
12708
|
+
if (appCaps.size === 0) {
|
|
12709
|
+
this._capabilities.delete(appName);
|
|
12710
|
+
}
|
|
12711
|
+
}
|
|
12712
|
+
|
|
12713
|
+
/**
|
|
12714
|
+
* Remove all capabilities for a micro-app (unmount cleanup).
|
|
12715
|
+
*
|
|
12716
|
+
* Call this when a micro-app is unmounted to prevent stale
|
|
12717
|
+
* capabilities from appearing in the AI's capability map.
|
|
12718
|
+
*
|
|
12719
|
+
* @param {string} appName
|
|
12720
|
+
* @returns {number} Number of capabilities removed
|
|
12721
|
+
*/
|
|
12722
|
+
removeApp(appName) {
|
|
12723
|
+
const appCaps = this._capabilities.get(appName);
|
|
12724
|
+
if (!appCaps) return 0;
|
|
12725
|
+
|
|
12726
|
+
let removed = 0;
|
|
12727
|
+
for (const [actionName] of appCaps) {
|
|
12728
|
+
const qualifiedName = `${appName}:${actionName}`;
|
|
12729
|
+
this._actions.unregister(qualifiedName);
|
|
12730
|
+
removed++;
|
|
12731
|
+
}
|
|
12732
|
+
|
|
12733
|
+
this._capabilities.delete(appName);
|
|
12734
|
+
|
|
12735
|
+
logger.wuDebug(`[wu-ai] All capabilities removed for app '${appName}' (${removed})`);
|
|
12736
|
+
|
|
12737
|
+
this._eventBus.emit('ai:app:removed', {
|
|
12738
|
+
appName,
|
|
12739
|
+
capabilitiesRemoved: removed,
|
|
12740
|
+
}, { appName: 'wu-ai' });
|
|
12741
|
+
|
|
12742
|
+
return removed;
|
|
12743
|
+
}
|
|
12744
|
+
|
|
12745
|
+
// ─── Capability Map ─────────────────────────────────────────────
|
|
12746
|
+
|
|
12747
|
+
/**
|
|
12748
|
+
* Get the full capability map grouped by app.
|
|
12749
|
+
*
|
|
12750
|
+
* Used for system prompt enrichment and debugging.
|
|
12751
|
+
*
|
|
12752
|
+
* @returns {object} { appName: [{ action, description }], ... }
|
|
12753
|
+
*/
|
|
12754
|
+
getCapabilityMap() {
|
|
12755
|
+
const map = {};
|
|
12756
|
+
for (const [appName, actions] of this._capabilities) {
|
|
12757
|
+
map[appName] = [];
|
|
12758
|
+
for (const [, meta] of actions) {
|
|
12759
|
+
map[appName].push({
|
|
12760
|
+
action: meta.qualifiedName,
|
|
12761
|
+
description: meta.description,
|
|
12762
|
+
});
|
|
12763
|
+
}
|
|
12764
|
+
}
|
|
12765
|
+
return map;
|
|
12766
|
+
}
|
|
12767
|
+
|
|
12768
|
+
/**
|
|
12769
|
+
* Get app names that have registered capabilities.
|
|
12770
|
+
*
|
|
12771
|
+
* @returns {string[]}
|
|
12772
|
+
*/
|
|
12773
|
+
getRegisteredApps() {
|
|
12774
|
+
return [...this._capabilities.keys()];
|
|
12775
|
+
}
|
|
12776
|
+
|
|
12777
|
+
/**
|
|
12778
|
+
* Check if an app has registered capabilities.
|
|
12779
|
+
*
|
|
12780
|
+
* @param {string} appName
|
|
12781
|
+
* @returns {boolean}
|
|
12782
|
+
*/
|
|
12783
|
+
hasApp(appName) {
|
|
12784
|
+
return this._capabilities.has(appName);
|
|
12785
|
+
}
|
|
12786
|
+
|
|
12787
|
+
/**
|
|
12788
|
+
* Get the total number of registered capabilities across all apps.
|
|
12789
|
+
*
|
|
12790
|
+
* @returns {number}
|
|
12791
|
+
*/
|
|
12792
|
+
getTotalCapabilities() {
|
|
12793
|
+
let count = 0;
|
|
12794
|
+
for (const actions of this._capabilities.values()) {
|
|
12795
|
+
count += actions.size;
|
|
12796
|
+
}
|
|
12797
|
+
return count;
|
|
12798
|
+
}
|
|
12799
|
+
|
|
12800
|
+
// ─── Intent Resolution ──────────────────────────────────────────
|
|
12801
|
+
|
|
12802
|
+
/**
|
|
12803
|
+
* Resolve a cross-app intent in a single conversation turn.
|
|
12804
|
+
*
|
|
12805
|
+
* The AI receives:
|
|
12806
|
+
* - The full capability map (what each app can do)
|
|
12807
|
+
* - Current application state (via context)
|
|
12808
|
+
* - Mounted apps list
|
|
12809
|
+
* - All registered tools (capabilities are tools)
|
|
12810
|
+
*
|
|
12811
|
+
* Unlike agent(), this is NOT a multi-step autonomous loop.
|
|
12812
|
+
* The LLM resolves the intent in one logical request (which may
|
|
12813
|
+
* include multiple tool calls within the conversation's tool-call
|
|
12814
|
+
* loop, but conceptually is a single turn).
|
|
12815
|
+
*
|
|
12816
|
+
* Unlike send(), the namespace is ephemeral and auto-cleaned,
|
|
12817
|
+
* and the system prompt is auto-built with the capability map.
|
|
12818
|
+
*
|
|
12819
|
+
* @param {string} description - Natural language intent
|
|
12820
|
+
* e.g., "Show me the top customer by order count"
|
|
12821
|
+
* e.g., "Update dashboard stats and notify the topbar"
|
|
12822
|
+
* @param {object} [options]
|
|
12823
|
+
* @param {string[]} [options.plan] - Optional action sequence hint.
|
|
12824
|
+
* The AI uses this as guidance but can deviate if needed.
|
|
12825
|
+
* e.g., ['orders:getRecent', 'customers:lookup']
|
|
12826
|
+
* @param {string} [options.provider] - LLM provider override
|
|
12827
|
+
* @param {number} [options.temperature] - Temperature override
|
|
12828
|
+
* @param {number} [options.maxTokens] - Max tokens override
|
|
12829
|
+
* @param {AbortSignal} [options.signal] - Abort signal
|
|
12830
|
+
* @param {string|object} [options.responseFormat] - Response format
|
|
12831
|
+
* @returns {Promise<{
|
|
12832
|
+
* content: string,
|
|
12833
|
+
* tool_results: Array,
|
|
12834
|
+
* usage: object|null,
|
|
12835
|
+
* resolved: boolean,
|
|
12836
|
+
* appsInvolved: string[]
|
|
12837
|
+
* }>}
|
|
12838
|
+
*/
|
|
12839
|
+
async resolve(description, options = {}) {
|
|
12840
|
+
if (!description || typeof description !== 'string') {
|
|
12841
|
+
throw new Error('[wu-ai] intent() requires a description string');
|
|
12842
|
+
}
|
|
12843
|
+
|
|
12844
|
+
this._stats.totalIntents++;
|
|
12845
|
+
const namespace = this._generateNamespace();
|
|
12846
|
+
|
|
12847
|
+
// Collect fresh context before building the prompt
|
|
12848
|
+
if (this._context) {
|
|
12849
|
+
try {
|
|
12850
|
+
await this._context.collect();
|
|
12851
|
+
} catch {
|
|
12852
|
+
// Context collection is best-effort
|
|
12853
|
+
}
|
|
12854
|
+
}
|
|
12855
|
+
|
|
12856
|
+
const systemPrompt = this._buildOrchestratorPrompt(options);
|
|
12857
|
+
|
|
12858
|
+
this._eventBus.emit('ai:intent:start', {
|
|
12859
|
+
description: description.slice(0, 200),
|
|
12860
|
+
namespace,
|
|
12861
|
+
capabilities: this.getTotalCapabilities(),
|
|
12862
|
+
}, { appName: 'wu-ai' });
|
|
12863
|
+
|
|
12864
|
+
try {
|
|
12865
|
+
const response = await this._conversation.send(description, {
|
|
12866
|
+
namespace,
|
|
12867
|
+
systemPrompt,
|
|
12868
|
+
provider: options.provider || this._config.defaultProvider,
|
|
12869
|
+
temperature: options.temperature ?? this._config.defaultTemperature,
|
|
12870
|
+
maxTokens: options.maxTokens,
|
|
12871
|
+
signal: options.signal,
|
|
12872
|
+
responseFormat: options.responseFormat,
|
|
12873
|
+
});
|
|
12874
|
+
|
|
12875
|
+
const toolResults = response.tool_results || [];
|
|
12876
|
+
const appsInvolved = this._extractInvolvedApps(toolResults);
|
|
12877
|
+
const resolved = !!(response.content);
|
|
12878
|
+
|
|
12879
|
+
if (resolved) {
|
|
12880
|
+
this._stats.resolvedIntents++;
|
|
12881
|
+
} else {
|
|
12882
|
+
this._stats.failedIntents++;
|
|
12883
|
+
}
|
|
12884
|
+
|
|
12885
|
+
const result = {
|
|
12886
|
+
content: response.content || '',
|
|
12887
|
+
tool_results: toolResults,
|
|
12888
|
+
usage: response.usage || null,
|
|
12889
|
+
resolved,
|
|
12890
|
+
appsInvolved,
|
|
12891
|
+
};
|
|
12892
|
+
|
|
12893
|
+
this._eventBus.emit('ai:intent:resolved', {
|
|
12894
|
+
description: description.slice(0, 200),
|
|
12895
|
+
resolved,
|
|
12896
|
+
appsInvolved,
|
|
12897
|
+
}, { appName: 'wu-ai' });
|
|
12898
|
+
|
|
12899
|
+
return result;
|
|
12900
|
+
} catch (err) {
|
|
12901
|
+
this._stats.failedIntents++;
|
|
12902
|
+
|
|
12903
|
+
this._eventBus.emit('ai:intent:error', {
|
|
12904
|
+
description: description.slice(0, 200),
|
|
12905
|
+
error: err.message,
|
|
12906
|
+
}, { appName: 'wu-ai' });
|
|
12907
|
+
|
|
12908
|
+
throw err;
|
|
12909
|
+
} finally {
|
|
12910
|
+
// Always clean up the ephemeral namespace
|
|
12911
|
+
this._conversation.deleteNamespace(namespace);
|
|
12912
|
+
}
|
|
12913
|
+
}
|
|
12914
|
+
|
|
12915
|
+
// ─── System Prompt Builder ──────────────────────────────────────
|
|
12916
|
+
|
|
12917
|
+
/**
|
|
12918
|
+
* Build the orchestrator system prompt with full capability map.
|
|
12919
|
+
*
|
|
12920
|
+
* This method is also available to other modules (triggers, agents)
|
|
12921
|
+
* that want capability-aware system prompts.
|
|
12922
|
+
*
|
|
12923
|
+
* @param {object} [options]
|
|
12924
|
+
* @param {string[]} [options.plan] - Optional action sequence hint
|
|
12925
|
+
* @returns {string}
|
|
12926
|
+
*/
|
|
12927
|
+
buildOrchestratorPrompt(options = {}) {
|
|
12928
|
+
return this._buildOrchestratorPrompt(options);
|
|
12929
|
+
}
|
|
12930
|
+
|
|
12931
|
+
/** @private */
|
|
12932
|
+
_buildOrchestratorPrompt(options = {}) {
|
|
12933
|
+
const parts = [];
|
|
12934
|
+
|
|
12935
|
+
parts.push(
|
|
12936
|
+
'You are an AI orchestrator for a microfrontend application.',
|
|
12937
|
+
'Multiple independent apps are mounted, each with specific capabilities.',
|
|
12938
|
+
'Resolve cross-app requests by calling the right capabilities in the right order.',
|
|
12939
|
+
'',
|
|
12940
|
+
'RULES:',
|
|
12941
|
+
'- Call capabilities (tools) to gather data or trigger actions.',
|
|
12942
|
+
'- You may call multiple capabilities from different apps if needed.',
|
|
12943
|
+
'- Synthesize results into a clear, actionable response.',
|
|
12944
|
+
'- If a required app is not available or lacks a capability, explain what is missing.',
|
|
12945
|
+
'',
|
|
12946
|
+
);
|
|
12947
|
+
|
|
12948
|
+
// Capability map
|
|
12949
|
+
const capMap = this.getCapabilityMap();
|
|
12950
|
+
const appNames = Object.keys(capMap);
|
|
12951
|
+
|
|
12952
|
+
if (appNames.length > 0) {
|
|
12953
|
+
parts.push('CAPABILITY MAP:');
|
|
12954
|
+
for (const appName of appNames) {
|
|
12955
|
+
parts.push(` ${appName}:`);
|
|
12956
|
+
for (const cap of capMap[appName]) {
|
|
12957
|
+
parts.push(` - ${cap.action}: ${cap.description}`);
|
|
12958
|
+
}
|
|
12959
|
+
}
|
|
12960
|
+
parts.push('');
|
|
12961
|
+
} else {
|
|
12962
|
+
parts.push(
|
|
12963
|
+
'NOTE: No app capabilities are registered. Answer based on available context only.',
|
|
12964
|
+
'',
|
|
12965
|
+
);
|
|
12966
|
+
}
|
|
12967
|
+
|
|
12968
|
+
// Optional plan hint
|
|
12969
|
+
if (options.plan && options.plan.length > 0) {
|
|
12970
|
+
parts.push(
|
|
12971
|
+
'SUGGESTED PLAN (follow this unless a better approach is evident):',
|
|
12972
|
+
);
|
|
12973
|
+
for (let i = 0; i < options.plan.length; i++) {
|
|
12974
|
+
parts.push(` ${i + 1}. ${options.plan[i]}`);
|
|
12975
|
+
}
|
|
12976
|
+
parts.push('');
|
|
12977
|
+
}
|
|
12978
|
+
|
|
12979
|
+
// Context snapshot (state, mounted apps)
|
|
12980
|
+
const snapshot = this._context?.getSnapshot();
|
|
12981
|
+
if (snapshot?._mountedApps?.length) {
|
|
12982
|
+
parts.push(`MOUNTED APPS: ${snapshot._mountedApps.join(', ')}`, '');
|
|
12983
|
+
}
|
|
12984
|
+
if (snapshot?._store && Object.keys(snapshot._store).length > 0) {
|
|
12985
|
+
parts.push(
|
|
12986
|
+
'CURRENT STATE:',
|
|
12987
|
+
JSON.stringify(snapshot._store, null, 2),
|
|
12988
|
+
'',
|
|
12989
|
+
);
|
|
12990
|
+
}
|
|
12991
|
+
|
|
12992
|
+
return parts.join('\n');
|
|
12993
|
+
}
|
|
12994
|
+
|
|
12995
|
+
// ─── Workflows ─────────────────────────────────────────────────
|
|
12996
|
+
|
|
12997
|
+
/**
|
|
12998
|
+
* Register a reusable AI workflow.
|
|
12999
|
+
*
|
|
13000
|
+
* A workflow is a named, parameterized recipe that the AI agent
|
|
13001
|
+
* follows step by step. Think of it as a macro: you define it once,
|
|
13002
|
+
* then run it whenever you need with different parameters.
|
|
13003
|
+
*
|
|
13004
|
+
* The AI receives the steps as instructions and uses browser actions
|
|
13005
|
+
* (screenshot, click, type) plus any registered capabilities/actions
|
|
13006
|
+
* to execute them. You can watch every step in real time.
|
|
13007
|
+
*
|
|
13008
|
+
* @param {string} name - Workflow name (e.g., 'register-user')
|
|
13009
|
+
* @param {object} config
|
|
13010
|
+
* @param {string} config.description - What this workflow does
|
|
13011
|
+
* @param {string[]} config.steps - Step-by-step instructions for the AI
|
|
13012
|
+
* @param {object} [config.parameters] - Parameter definitions for interpolation
|
|
13013
|
+
* e.g., { name: { type: 'string', required: true }, email: { type: 'string' } }
|
|
13014
|
+
* @param {number} [config.maxSteps=15] - Max agent steps allowed
|
|
13015
|
+
* @param {string} [config.provider] - LLM provider to use
|
|
13016
|
+
* @param {number} [config.temperature] - Temperature (default: 0.2 for precision)
|
|
13017
|
+
*
|
|
13018
|
+
* @example
|
|
13019
|
+
* // ── AI Mode (default): steps are natural language ──
|
|
13020
|
+
* wu.ai.workflow('register-user', {
|
|
13021
|
+
* description: 'Register a new user in the system',
|
|
13022
|
+
* steps: [
|
|
13023
|
+
* 'Navigate to the Customers section',
|
|
13024
|
+
* 'Click the "Add Customer" button',
|
|
13025
|
+
* 'Fill in the name field with {{name}}',
|
|
13026
|
+
* 'Click Submit',
|
|
13027
|
+
* ],
|
|
13028
|
+
* parameters: { name: { type: 'string', required: true } },
|
|
13029
|
+
* });
|
|
13030
|
+
*
|
|
13031
|
+
* // ── Deterministic Mode: steps are exact actions, NO AI NEEDED ──
|
|
13032
|
+
* wu.ai.workflow('register-user', {
|
|
13033
|
+
* mode: 'deterministic',
|
|
13034
|
+
* description: 'Register a new user',
|
|
13035
|
+
* steps: [
|
|
13036
|
+
* { action: 'navigate', section: 'customers' },
|
|
13037
|
+
* { action: 'click', selector: '#add-customer-btn' },
|
|
13038
|
+
* { action: 'type', selector: '#name', value: '{{name}}' },
|
|
13039
|
+
* { action: 'type', selector: '#email', value: '{{email}}' },
|
|
13040
|
+
* { action: 'click', selector: '#submit-btn' },
|
|
13041
|
+
* { action: 'wait', selector: '.success-message', timeout: 5000 },
|
|
13042
|
+
* ],
|
|
13043
|
+
* parameters: {
|
|
13044
|
+
* name: { type: 'string', required: true },
|
|
13045
|
+
* email: { type: 'string', required: true },
|
|
13046
|
+
* },
|
|
13047
|
+
* });
|
|
13048
|
+
*/
|
|
13049
|
+
registerWorkflow(name, config) {
|
|
13050
|
+
if (!name) {
|
|
13051
|
+
throw new Error('[wu-ai] workflow() requires a name');
|
|
13052
|
+
}
|
|
13053
|
+
if (!config || !config.steps || !Array.isArray(config.steps) || config.steps.length === 0) {
|
|
13054
|
+
throw new Error(`[wu-ai] workflow '${name}' must have a non-empty steps array`);
|
|
13055
|
+
}
|
|
13056
|
+
|
|
13057
|
+
// Detect mode: if steps are objects with 'action', it's deterministic
|
|
13058
|
+
const mode = config.mode || (
|
|
13059
|
+
config.steps.length > 0 && typeof config.steps[0] === 'object' && config.steps[0].action
|
|
13060
|
+
? 'deterministic'
|
|
13061
|
+
: 'ai'
|
|
13062
|
+
);
|
|
13063
|
+
|
|
13064
|
+
this._workflows.set(name, {
|
|
13065
|
+
description: config.description || name,
|
|
13066
|
+
steps: config.steps,
|
|
13067
|
+
mode,
|
|
13068
|
+
parameters: config.parameters || {},
|
|
13069
|
+
maxSteps: config.maxSteps ?? 15,
|
|
13070
|
+
provider: config.provider || null,
|
|
13071
|
+
temperature: config.temperature ?? 0.2,
|
|
13072
|
+
});
|
|
13073
|
+
|
|
13074
|
+
this._stats.workflowsRegistered++;
|
|
13075
|
+
|
|
13076
|
+
logger.wuDebug(`[wu-ai] Workflow registered: '${name}' (${config.steps.length} steps)`);
|
|
13077
|
+
}
|
|
13078
|
+
|
|
13079
|
+
/**
|
|
13080
|
+
* Execute a registered workflow with parameters.
|
|
13081
|
+
*
|
|
13082
|
+
* Returns an async generator (like agent) — you iterate over it
|
|
13083
|
+
* to observe each step in real time.
|
|
13084
|
+
*
|
|
13085
|
+
* @param {string} name - Workflow name
|
|
13086
|
+
* @param {object} [params={}] - Parameters to interpolate into steps
|
|
13087
|
+
* e.g., { name: 'Juan Pérez', email: 'juan@test.com' }
|
|
13088
|
+
* @param {object} [options={}]
|
|
13089
|
+
* @param {Function} [options.onStep] - Callback per step
|
|
13090
|
+
* @param {Function} [options.shouldContinue] - Human-in-the-loop gate
|
|
13091
|
+
* @param {AbortSignal} [options.signal] - Abort signal
|
|
13092
|
+
* @returns {AsyncGenerator<AgentStepResult>}
|
|
13093
|
+
*
|
|
13094
|
+
* @example
|
|
13095
|
+
* for await (const step of wu.ai.runWorkflow('register-user', {
|
|
13096
|
+
* name: 'Juan Pérez',
|
|
13097
|
+
* email: 'juan@test.com',
|
|
13098
|
+
* })) {
|
|
13099
|
+
* console.log(`Paso ${step.step}: ${step.content}`);
|
|
13100
|
+
* if (step.type === 'done') console.log('Workflow completado!');
|
|
13101
|
+
* }
|
|
13102
|
+
*
|
|
13103
|
+
* // With human approval per step:
|
|
13104
|
+
* for await (const step of wu.ai.runWorkflow('register-user', params, {
|
|
13105
|
+
* shouldContinue: (step) => confirm(`¿Continuar? ${step.content?.slice(0, 60)}`),
|
|
13106
|
+
* })) {
|
|
13107
|
+
* renderStep(step);
|
|
13108
|
+
* }
|
|
13109
|
+
*/
|
|
13110
|
+
async *executeWorkflow(name, params = {}, options = {}) {
|
|
13111
|
+
const workflow = this._workflows.get(name);
|
|
13112
|
+
if (!workflow) {
|
|
13113
|
+
throw new Error(`[wu-ai] Workflow '${name}' is not registered`);
|
|
13114
|
+
}
|
|
13115
|
+
|
|
13116
|
+
// Validate required parameters
|
|
13117
|
+
for (const [paramName, paramConfig] of Object.entries(workflow.parameters)) {
|
|
13118
|
+
if (paramConfig.required && (params[paramName] === undefined || params[paramName] === null)) {
|
|
13119
|
+
throw new Error(`[wu-ai] Workflow '${name}' requires parameter '${paramName}'`);
|
|
13120
|
+
}
|
|
13121
|
+
}
|
|
13122
|
+
|
|
13123
|
+
this._stats.workflowsExecuted++;
|
|
13124
|
+
|
|
13125
|
+
// Branch on mode
|
|
13126
|
+
if (workflow.mode === 'deterministic') {
|
|
13127
|
+
yield* this._executeDeterministic(name, workflow, params, options);
|
|
13128
|
+
} else {
|
|
13129
|
+
yield* this._executeWithAgent(name, workflow, params, options);
|
|
13130
|
+
}
|
|
13131
|
+
}
|
|
13132
|
+
|
|
13133
|
+
/**
|
|
13134
|
+
* Execute workflow using the AI agent (natural language steps).
|
|
13135
|
+
* @private
|
|
13136
|
+
*/
|
|
13137
|
+
async *_executeWithAgent(name, workflow, params, options) {
|
|
13138
|
+
if (!this._agent) {
|
|
13139
|
+
throw new Error('[wu-ai] Agent module not available for workflow execution');
|
|
13140
|
+
}
|
|
13141
|
+
|
|
13142
|
+
// Interpolate parameters into string steps
|
|
13143
|
+
const interpolatedSteps = workflow.steps.map(step => {
|
|
13144
|
+
let result = step;
|
|
13145
|
+
for (const [key, value] of Object.entries(params)) {
|
|
13146
|
+
result = result.replace(new RegExp(`\\{\\{${key}\\}\\}`, 'g'), String(value));
|
|
13147
|
+
}
|
|
13148
|
+
return result;
|
|
13149
|
+
});
|
|
13150
|
+
|
|
13151
|
+
const goal = this._buildWorkflowGoal(workflow, interpolatedSteps, params);
|
|
13152
|
+
|
|
13153
|
+
this._eventBus.emit('ai:workflow:start', {
|
|
13154
|
+
workflow: name,
|
|
13155
|
+
mode: 'ai',
|
|
13156
|
+
params,
|
|
13157
|
+
steps: interpolatedSteps.length,
|
|
13158
|
+
}, { appName: 'wu-ai' });
|
|
13159
|
+
|
|
13160
|
+
let finalStep = null;
|
|
13161
|
+
|
|
13162
|
+
try {
|
|
13163
|
+
yield* this._agent.run(goal, {
|
|
13164
|
+
maxSteps: workflow.maxSteps,
|
|
13165
|
+
provider: options.provider || workflow.provider || this._config.defaultProvider,
|
|
13166
|
+
temperature: workflow.temperature ?? 0.2,
|
|
13167
|
+
onStep: (step) => {
|
|
13168
|
+
finalStep = step;
|
|
13169
|
+
if (options.onStep) options.onStep(step);
|
|
13170
|
+
},
|
|
13171
|
+
shouldContinue: options.shouldContinue,
|
|
13172
|
+
signal: options.signal,
|
|
13173
|
+
});
|
|
13174
|
+
} finally {
|
|
13175
|
+
this._eventBus.emit('ai:workflow:done', {
|
|
13176
|
+
workflow: name,
|
|
13177
|
+
params,
|
|
13178
|
+
totalSteps: finalStep?.step || 0,
|
|
13179
|
+
result: finalStep?.type || 'unknown',
|
|
13180
|
+
}, { appName: 'wu-ai' });
|
|
13181
|
+
}
|
|
13182
|
+
}
|
|
13183
|
+
|
|
13184
|
+
/**
|
|
13185
|
+
* Execute workflow deterministically — NO AI NEEDED.
|
|
13186
|
+
* Steps are exact actions: { action: 'click', selector: '#btn' }
|
|
13187
|
+
* @private
|
|
13188
|
+
*/
|
|
13189
|
+
async *_executeDeterministic(name, workflow, params, options) {
|
|
13190
|
+
// Interpolate parameters into step values
|
|
13191
|
+
const steps = workflow.steps.map(step => {
|
|
13192
|
+
const interpolated = { ...step };
|
|
13193
|
+
for (const [key, value] of Object.entries(params)) {
|
|
13194
|
+
const pattern = new RegExp(`\\{\\{${key}\\}\\}`, 'g');
|
|
13195
|
+
for (const field of ['value', 'selector', 'text', 'section', 'event', 'path']) {
|
|
13196
|
+
if (typeof interpolated[field] === 'string') {
|
|
13197
|
+
interpolated[field] = interpolated[field].replace(pattern, String(value));
|
|
13198
|
+
}
|
|
13199
|
+
}
|
|
13200
|
+
}
|
|
13201
|
+
return interpolated;
|
|
13202
|
+
});
|
|
13203
|
+
|
|
13204
|
+
this._eventBus?.emit('ai:workflow:start', {
|
|
13205
|
+
workflow: name,
|
|
13206
|
+
mode: 'deterministic',
|
|
13207
|
+
params,
|
|
13208
|
+
steps: steps.length,
|
|
13209
|
+
}, { appName: 'wu-ai' });
|
|
13210
|
+
|
|
13211
|
+
let lastStep = null;
|
|
13212
|
+
|
|
13213
|
+
try {
|
|
13214
|
+
for (let i = 0; i < steps.length; i++) {
|
|
13215
|
+
const step = steps[i];
|
|
13216
|
+
const stepNum = i + 1;
|
|
13217
|
+
const startTime = Date.now();
|
|
13218
|
+
|
|
13219
|
+
// Check abort
|
|
13220
|
+
if (options.signal?.aborted) {
|
|
13221
|
+
const result = {
|
|
13222
|
+
step: stepNum,
|
|
13223
|
+
type: 'aborted',
|
|
13224
|
+
content: 'Workflow aborted',
|
|
13225
|
+
reason: 'Aborted by caller',
|
|
13226
|
+
elapsed: 0,
|
|
13227
|
+
};
|
|
13228
|
+
lastStep = result;
|
|
13229
|
+
yield result;
|
|
13230
|
+
return;
|
|
13231
|
+
}
|
|
13232
|
+
|
|
13233
|
+
// Handle 'wait' specially — it's async
|
|
13234
|
+
if (step.action === 'wait') {
|
|
13235
|
+
if (step.selector) {
|
|
13236
|
+
const found = await waitForSelector(step.selector, step.timeout || 5000);
|
|
13237
|
+
const elapsed = Date.now() - startTime;
|
|
13238
|
+
const result = {
|
|
13239
|
+
step: stepNum,
|
|
13240
|
+
type: found ? 'action' : 'error',
|
|
13241
|
+
content: found
|
|
13242
|
+
? `Waited for "${step.selector}" — found`
|
|
13243
|
+
: `Timeout waiting for "${step.selector}"`,
|
|
13244
|
+
elapsed,
|
|
13245
|
+
};
|
|
13246
|
+
lastStep = result;
|
|
13247
|
+
if (options.onStep) options.onStep(result);
|
|
13248
|
+
yield result;
|
|
13249
|
+
if (!found) return; // stop on timeout
|
|
13250
|
+
} else if (step.ms) {
|
|
13251
|
+
await delay(step.ms);
|
|
13252
|
+
const result = {
|
|
13253
|
+
step: stepNum,
|
|
13254
|
+
type: 'action',
|
|
13255
|
+
content: `Waited ${step.ms}ms`,
|
|
13256
|
+
elapsed: step.ms,
|
|
13257
|
+
};
|
|
13258
|
+
lastStep = result;
|
|
13259
|
+
if (options.onStep) options.onStep(result);
|
|
13260
|
+
yield result;
|
|
13261
|
+
}
|
|
13262
|
+
continue;
|
|
13263
|
+
}
|
|
13264
|
+
|
|
13265
|
+
// Execute the step
|
|
13266
|
+
const execResult = executeDeterministicStep(step, this._eventBus, this._store);
|
|
13267
|
+
const elapsed = Date.now() - startTime;
|
|
13268
|
+
|
|
13269
|
+
const stepResult = {
|
|
13270
|
+
step: stepNum,
|
|
13271
|
+
type: execResult.success ? 'action' : 'error',
|
|
13272
|
+
content: execResult.success ? execResult.detail : execResult.error,
|
|
13273
|
+
elapsed,
|
|
13274
|
+
};
|
|
13275
|
+
|
|
13276
|
+
lastStep = stepResult;
|
|
13277
|
+
if (options.onStep) options.onStep(stepResult);
|
|
13278
|
+
yield stepResult;
|
|
13279
|
+
|
|
13280
|
+
// Human-in-the-loop gate
|
|
13281
|
+
if (options.shouldContinue) {
|
|
13282
|
+
let shouldGo;
|
|
13283
|
+
try {
|
|
13284
|
+
shouldGo = await options.shouldContinue(stepResult);
|
|
13285
|
+
} catch {
|
|
13286
|
+
shouldGo = false;
|
|
13287
|
+
}
|
|
13288
|
+
if (!shouldGo) {
|
|
13289
|
+
const interrupted = {
|
|
13290
|
+
step: stepNum,
|
|
13291
|
+
type: 'interrupted',
|
|
13292
|
+
content: 'Stopped by user',
|
|
13293
|
+
reason: 'shouldContinue returned false',
|
|
13294
|
+
elapsed: 0,
|
|
13295
|
+
};
|
|
13296
|
+
lastStep = interrupted;
|
|
13297
|
+
yield interrupted;
|
|
13298
|
+
return;
|
|
13299
|
+
}
|
|
13300
|
+
}
|
|
13301
|
+
|
|
13302
|
+
// Stop on error
|
|
13303
|
+
if (!execResult.success) return;
|
|
13304
|
+
|
|
13305
|
+
// Small delay between steps for UI to update
|
|
13306
|
+
if (i < steps.length - 1) {
|
|
13307
|
+
await delay(step.delay ?? 200);
|
|
13308
|
+
}
|
|
13309
|
+
}
|
|
13310
|
+
|
|
13311
|
+
// All steps completed
|
|
13312
|
+
const done = {
|
|
13313
|
+
step: steps.length,
|
|
13314
|
+
type: 'done',
|
|
13315
|
+
content: `Workflow "${name}" completed (${steps.length} steps)`,
|
|
13316
|
+
reason: 'All steps executed',
|
|
13317
|
+
elapsed: 0,
|
|
13318
|
+
};
|
|
13319
|
+
lastStep = done;
|
|
13320
|
+
if (options.onStep) options.onStep(done);
|
|
13321
|
+
yield done;
|
|
13322
|
+
|
|
13323
|
+
} finally {
|
|
13324
|
+
this._eventBus?.emit('ai:workflow:done', {
|
|
13325
|
+
workflow: name,
|
|
13326
|
+
mode: 'deterministic',
|
|
13327
|
+
params,
|
|
13328
|
+
totalSteps: lastStep?.step || 0,
|
|
13329
|
+
result: lastStep?.type || 'unknown',
|
|
13330
|
+
}, { appName: 'wu-ai' });
|
|
13331
|
+
}
|
|
13332
|
+
}
|
|
13333
|
+
|
|
13334
|
+
/**
|
|
13335
|
+
* Check if a workflow is registered.
|
|
13336
|
+
*
|
|
13337
|
+
* @param {string} name
|
|
13338
|
+
* @returns {boolean}
|
|
13339
|
+
*/
|
|
13340
|
+
hasWorkflow(name) {
|
|
13341
|
+
return this._workflows.has(name);
|
|
13342
|
+
}
|
|
13343
|
+
|
|
13344
|
+
/**
|
|
13345
|
+
* Get a workflow definition.
|
|
13346
|
+
*
|
|
13347
|
+
* @param {string} name
|
|
13348
|
+
* @returns {object|null}
|
|
13349
|
+
*/
|
|
13350
|
+
getWorkflow(name) {
|
|
13351
|
+
const w = this._workflows.get(name);
|
|
13352
|
+
if (!w) return null;
|
|
13353
|
+
return {
|
|
13354
|
+
description: w.description,
|
|
13355
|
+
steps: [...w.steps],
|
|
13356
|
+
mode: w.mode,
|
|
13357
|
+
parameters: { ...w.parameters },
|
|
13358
|
+
maxSteps: w.maxSteps,
|
|
13359
|
+
};
|
|
13360
|
+
}
|
|
13361
|
+
|
|
13362
|
+
/**
|
|
13363
|
+
* Remove a registered workflow.
|
|
13364
|
+
*
|
|
13365
|
+
* @param {string} name
|
|
13366
|
+
*/
|
|
13367
|
+
removeWorkflow(name) {
|
|
13368
|
+
this._workflows.delete(name);
|
|
13369
|
+
}
|
|
13370
|
+
|
|
13371
|
+
/**
|
|
13372
|
+
* Get all workflow names.
|
|
13373
|
+
*
|
|
13374
|
+
* @returns {string[]}
|
|
13375
|
+
*/
|
|
13376
|
+
getWorkflowNames() {
|
|
13377
|
+
return [...this._workflows.keys()];
|
|
13378
|
+
}
|
|
13379
|
+
|
|
13380
|
+
/** @private */
|
|
13381
|
+
_buildWorkflowGoal(workflow, steps, params) {
|
|
13382
|
+
const parts = [];
|
|
13383
|
+
|
|
13384
|
+
parts.push(
|
|
13385
|
+
`WORKFLOW: ${workflow.description}`,
|
|
13386
|
+
'',
|
|
13387
|
+
'You must follow these steps IN ORDER. Use browser tools (screenshot, click, type)',
|
|
13388
|
+
'to interact with the application. After each step, take a screenshot to verify.',
|
|
13389
|
+
'',
|
|
13390
|
+
'STEPS:',
|
|
13391
|
+
);
|
|
13392
|
+
|
|
13393
|
+
for (let i = 0; i < steps.length; i++) {
|
|
13394
|
+
parts.push(` ${i + 1}. ${steps[i]}`);
|
|
13395
|
+
}
|
|
13396
|
+
|
|
13397
|
+
parts.push(
|
|
13398
|
+
'',
|
|
13399
|
+
'After completing all steps successfully, respond with [DONE].',
|
|
13400
|
+
'If a step fails, explain what went wrong.',
|
|
13401
|
+
);
|
|
13402
|
+
|
|
13403
|
+
// Add capability context if available
|
|
13404
|
+
const capMap = this.getCapabilityMap();
|
|
13405
|
+
const appNames = Object.keys(capMap);
|
|
13406
|
+
if (appNames.length > 0) {
|
|
13407
|
+
parts.push('', 'AVAILABLE APP CAPABILITIES:');
|
|
13408
|
+
for (const appName of appNames) {
|
|
13409
|
+
for (const cap of capMap[appName]) {
|
|
13410
|
+
parts.push(` - ${cap.action}: ${cap.description}`);
|
|
13411
|
+
}
|
|
13412
|
+
}
|
|
13413
|
+
}
|
|
13414
|
+
|
|
13415
|
+
return parts.join('\n');
|
|
13416
|
+
}
|
|
13417
|
+
|
|
13418
|
+
// ─── Stats & Lifecycle ──────────────────────────────────────────
|
|
13419
|
+
|
|
13420
|
+
getStats() {
|
|
13421
|
+
return {
|
|
13422
|
+
...this._stats,
|
|
13423
|
+
registeredApps: this.getRegisteredApps(),
|
|
13424
|
+
totalCapabilities: this.getTotalCapabilities(),
|
|
13425
|
+
capabilityMap: this.getCapabilityMap(),
|
|
13426
|
+
workflows: this.getWorkflowNames(),
|
|
13427
|
+
config: { ...this._config },
|
|
13428
|
+
};
|
|
13429
|
+
}
|
|
13430
|
+
|
|
13431
|
+
destroy() {
|
|
13432
|
+
// Remove all capabilities from the action registry
|
|
13433
|
+
for (const [appName, actions] of this._capabilities) {
|
|
13434
|
+
for (const [actionName] of actions) {
|
|
13435
|
+
this._actions.unregister(`${appName}:${actionName}`);
|
|
13436
|
+
}
|
|
13437
|
+
}
|
|
13438
|
+
this._capabilities.clear();
|
|
13439
|
+
this._workflows.clear();
|
|
13440
|
+
|
|
13441
|
+
this._stats = {
|
|
13442
|
+
totalIntents: 0,
|
|
13443
|
+
resolvedIntents: 0,
|
|
13444
|
+
failedIntents: 0,
|
|
13445
|
+
workflowsRegistered: 0,
|
|
13446
|
+
workflowsExecuted: 0,
|
|
13447
|
+
};
|
|
13448
|
+
}
|
|
13449
|
+
|
|
13450
|
+
// ─── Private Helpers ────────────────────────────────────────────
|
|
13451
|
+
|
|
13452
|
+
/**
|
|
13453
|
+
* Extract app names from tool results based on qualified action names.
|
|
13454
|
+
* e.g., tool name 'orders:getRecent' → app 'orders'
|
|
13455
|
+
*/
|
|
13456
|
+
_extractInvolvedApps(toolResults) {
|
|
13457
|
+
if (!toolResults || !Array.isArray(toolResults)) return [];
|
|
13458
|
+
const apps = new Set();
|
|
13459
|
+
for (const result of toolResults) {
|
|
13460
|
+
const name = result.name || result.tool || '';
|
|
13461
|
+
const colonIdx = name.indexOf(':');
|
|
13462
|
+
if (colonIdx > 0) {
|
|
13463
|
+
apps.add(name.slice(0, colonIdx));
|
|
13464
|
+
}
|
|
13465
|
+
}
|
|
13466
|
+
return [...apps];
|
|
13467
|
+
}
|
|
13468
|
+
|
|
13469
|
+
_generateNamespace() {
|
|
13470
|
+
return INTENT_NAMESPACE_PREFIX + Date.now().toString(36) +
|
|
13471
|
+
'_' + Math.random().toString(36).slice(2, 6);
|
|
13472
|
+
}
|
|
13473
|
+
}
|
|
13474
|
+
|
|
13475
|
+
/**
|
|
13476
|
+
* WU-AI Browser Actions
|
|
13477
|
+
*
|
|
13478
|
+
* Registers browser automation tools into wu.ai so any LLM provider
|
|
13479
|
+
* (OpenAI, Claude, Gemini, Ollama, etc.) can autonomously see and
|
|
13480
|
+
* control the page — no human intervention required.
|
|
13481
|
+
*
|
|
13482
|
+
* Tools registered:
|
|
13483
|
+
* browser_screenshot — Capture page/element as PNG (Canvas API)
|
|
13484
|
+
* browser_click — Click element by selector or visible text
|
|
13485
|
+
* browser_type — Type into inputs (React/Vue/framework compatible)
|
|
13486
|
+
* browser_snapshot — Get accessibility tree of the DOM
|
|
13487
|
+
* browser_navigate — Navigate SPA routes
|
|
13488
|
+
* browser_network — View captured HTTP requests (fetch + XHR)
|
|
13489
|
+
* browser_console — View captured console messages
|
|
13490
|
+
* browser_info — Get page state: apps, store, URL, viewport
|
|
13491
|
+
* browser_select — Select option in dropdowns
|
|
13492
|
+
* browser_scroll — Scroll page or element
|
|
13493
|
+
*
|
|
13494
|
+
* @example
|
|
13495
|
+
* // Auto-registered when wu.ai initializes
|
|
13496
|
+
* // Any LLM connected via wu.ai.provider can now use these tools:
|
|
13497
|
+
* const tools = wu.ai.tools();
|
|
13498
|
+
* // → includes browser_screenshot, browser_click, etc.
|
|
13499
|
+
*/
|
|
13500
|
+
|
|
13501
|
+
|
|
13502
|
+
/**
|
|
13503
|
+
* Register all browser automation actions into a WuAI instance.
|
|
13504
|
+
*
|
|
11406
13505
|
* @param {object} ai - The WuAI instance (wu.ai)
|
|
11407
13506
|
* @param {object} wu - The Wu Framework instance (window.wu)
|
|
11408
13507
|
*/
|
|
11409
13508
|
function registerBrowserActions(ai, wu) {
|
|
11410
|
-
|
|
11411
|
-
if (!interceptorsInstalled) {
|
|
11412
|
-
_installNetworkInterceptor();
|
|
11413
|
-
_installConsoleInterceptor();
|
|
11414
|
-
interceptorsInstalled = true;
|
|
11415
|
-
}
|
|
13509
|
+
ensureInterceptors();
|
|
11416
13510
|
|
|
11417
13511
|
// ════════════════════════════════════════════
|
|
11418
13512
|
// SCREENSHOT — Canvas API (SVG foreignObject)
|
|
@@ -11427,66 +13521,7 @@ function registerBrowserActions(ai, wu) {
|
|
|
11427
13521
|
required: false,
|
|
11428
13522
|
},
|
|
11429
13523
|
},
|
|
11430
|
-
handler: async (params) =>
|
|
11431
|
-
const target = params.selector
|
|
11432
|
-
? document.querySelector(params.selector)
|
|
11433
|
-
: document.documentElement;
|
|
11434
|
-
|
|
11435
|
-
if (!target) return { error: `Element not found: ${params.selector}` };
|
|
11436
|
-
|
|
11437
|
-
const rect = target.getBoundingClientRect();
|
|
11438
|
-
const w = Math.ceil(Math.min(rect.width || window.innerWidth, 1920));
|
|
11439
|
-
const h = Math.ceil(Math.min(rect.height || window.innerHeight, 1080));
|
|
11440
|
-
|
|
11441
|
-
// Clone and inline styles for accurate rendering
|
|
11442
|
-
const clone = target.cloneNode(true);
|
|
11443
|
-
_inlineComputedStyles(target, clone);
|
|
11444
|
-
|
|
11445
|
-
const serializer = new XMLSerializer();
|
|
11446
|
-
const xhtml = serializer.serializeToString(clone);
|
|
11447
|
-
|
|
11448
|
-
const svgStr = [
|
|
11449
|
-
`<svg xmlns="http://www.w3.org/2000/svg" width="${w}" height="${h}">`,
|
|
11450
|
-
'<foreignObject width="100%" height="100%">',
|
|
11451
|
-
`<div xmlns="http://www.w3.org/1999/xhtml" style="width:${w}px;height:${h}px;overflow:hidden;">`,
|
|
11452
|
-
xhtml,
|
|
11453
|
-
'</div>',
|
|
11454
|
-
'</foreignObject>',
|
|
11455
|
-
'</svg>',
|
|
11456
|
-
].join('');
|
|
11457
|
-
|
|
11458
|
-
const svgBlob = new Blob([svgStr], { type: 'image/svg+xml;charset=utf-8' });
|
|
11459
|
-
const url = URL.createObjectURL(svgBlob);
|
|
11460
|
-
|
|
11461
|
-
const dataUrl = await new Promise((resolve) => {
|
|
11462
|
-
const img = new Image();
|
|
11463
|
-
img.onload = () => {
|
|
11464
|
-
const canvas = document.createElement('canvas');
|
|
11465
|
-
canvas.width = w;
|
|
11466
|
-
canvas.height = h;
|
|
11467
|
-
const ctx = canvas.getContext('2d');
|
|
11468
|
-
ctx.drawImage(img, 0, 0);
|
|
11469
|
-
URL.revokeObjectURL(url);
|
|
11470
|
-
resolve(canvas.toDataURL('image/png', 0.8));
|
|
11471
|
-
};
|
|
11472
|
-
img.onerror = () => {
|
|
11473
|
-
URL.revokeObjectURL(url);
|
|
11474
|
-
resolve(null);
|
|
11475
|
-
};
|
|
11476
|
-
img.src = url;
|
|
11477
|
-
});
|
|
11478
|
-
|
|
11479
|
-
if (!dataUrl) return { error: 'Canvas rendering failed' };
|
|
11480
|
-
|
|
11481
|
-
const base64 = dataUrl.split(',')[1];
|
|
11482
|
-
return {
|
|
11483
|
-
width: w,
|
|
11484
|
-
height: h,
|
|
11485
|
-
format: 'png',
|
|
11486
|
-
base64,
|
|
11487
|
-
sizeKB: Math.round((base64.length * 3) / 4 / 1024),
|
|
11488
|
-
};
|
|
11489
|
-
},
|
|
13524
|
+
handler: async (params) => captureScreenshot(params.selector),
|
|
11490
13525
|
permissions: [],
|
|
11491
13526
|
});
|
|
11492
13527
|
|
|
@@ -11509,38 +13544,11 @@ function registerBrowserActions(ai, wu) {
|
|
|
11509
13544
|
},
|
|
11510
13545
|
},
|
|
11511
13546
|
handler: async (params, api) => {
|
|
11512
|
-
|
|
11513
|
-
|
|
11514
|
-
|
|
11515
|
-
el = document.querySelector(params.selector);
|
|
11516
|
-
}
|
|
11517
|
-
|
|
11518
|
-
if (!el && params.text) {
|
|
11519
|
-
const candidates = document.querySelectorAll(
|
|
11520
|
-
'button, a, [role="button"], input[type="submit"], input[type="button"], [data-click], label, [onclick]'
|
|
11521
|
-
);
|
|
11522
|
-
const searchText = params.text.toLowerCase();
|
|
11523
|
-
for (const candidate of candidates) {
|
|
11524
|
-
if (candidate.textContent?.trim().toLowerCase().includes(searchText)) {
|
|
11525
|
-
el = candidate;
|
|
11526
|
-
break;
|
|
11527
|
-
}
|
|
11528
|
-
}
|
|
13547
|
+
const result = clickElement(params.selector, params.text);
|
|
13548
|
+
if (!result.error) {
|
|
13549
|
+
api.emit?.('browser:clicked', { selector: params.selector, text: params.text });
|
|
11529
13550
|
}
|
|
11530
|
-
|
|
11531
|
-
if (!el) return { error: `Element not found: ${params.selector || `text="${params.text}"`}` };
|
|
11532
|
-
|
|
11533
|
-
el.scrollIntoView({ behavior: 'instant', block: 'center' });
|
|
11534
|
-
el.click();
|
|
11535
|
-
|
|
11536
|
-
const tag = el.tagName?.toLowerCase();
|
|
11537
|
-
const id = el.id ? `#${el.id}` : '';
|
|
11538
|
-
api.emit?.('browser:clicked', { selector: params.selector, text: params.text });
|
|
11539
|
-
|
|
11540
|
-
return {
|
|
11541
|
-
clicked: `${tag}${id}`,
|
|
11542
|
-
text: el.textContent?.trim().slice(0, 100) || '',
|
|
11543
|
-
};
|
|
13551
|
+
return result;
|
|
11544
13552
|
},
|
|
11545
13553
|
permissions: ['emitEvents'],
|
|
11546
13554
|
});
|
|
@@ -11574,50 +13582,14 @@ function registerBrowserActions(ai, wu) {
|
|
|
11574
13582
|
},
|
|
11575
13583
|
},
|
|
11576
13584
|
handler: async (params, api) => {
|
|
11577
|
-
const
|
|
11578
|
-
|
|
11579
|
-
|
|
11580
|
-
|
|
11581
|
-
|
|
11582
|
-
|
|
11583
|
-
el.value = '';
|
|
11584
|
-
el.dispatchEvent(new Event('input', { bubbles: true }));
|
|
11585
|
-
}
|
|
11586
|
-
|
|
11587
|
-
// Use native setter to trigger framework reactivity (React, Vue, etc.)
|
|
11588
|
-
const nativeSetter = Object.getOwnPropertyDescriptor(
|
|
11589
|
-
window.HTMLInputElement.prototype, 'value'
|
|
11590
|
-
)?.set || Object.getOwnPropertyDescriptor(
|
|
11591
|
-
window.HTMLTextAreaElement.prototype, 'value'
|
|
11592
|
-
)?.set;
|
|
11593
|
-
|
|
11594
|
-
const newValue = params.clear ? params.text : (el.value || '') + params.text;
|
|
11595
|
-
if (nativeSetter) {
|
|
11596
|
-
nativeSetter.call(el, newValue);
|
|
11597
|
-
} else {
|
|
11598
|
-
el.value = newValue;
|
|
11599
|
-
}
|
|
11600
|
-
|
|
11601
|
-
el.dispatchEvent(new Event('input', { bubbles: true }));
|
|
11602
|
-
el.dispatchEvent(new Event('change', { bubbles: true }));
|
|
11603
|
-
|
|
11604
|
-
if (params.submit) {
|
|
11605
|
-
const form = el.closest('form');
|
|
11606
|
-
if (form) {
|
|
11607
|
-
form.dispatchEvent(new Event('submit', { bubbles: true, cancelable: true }));
|
|
11608
|
-
} else {
|
|
11609
|
-
el.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter', code: 'Enter', bubbles: true }));
|
|
11610
|
-
}
|
|
13585
|
+
const result = typeIntoElement(params.selector, params.text, {
|
|
13586
|
+
clear: params.clear,
|
|
13587
|
+
submit: params.submit,
|
|
13588
|
+
});
|
|
13589
|
+
if (!result.error) {
|
|
13590
|
+
api.emit?.('browser:typed', { selector: params.selector, length: params.text.length });
|
|
11611
13591
|
}
|
|
11612
|
-
|
|
11613
|
-
api.emit?.('browser:typed', { selector: params.selector, length: params.text.length });
|
|
11614
|
-
|
|
11615
|
-
return {
|
|
11616
|
-
selector: params.selector,
|
|
11617
|
-
typed: params.text,
|
|
11618
|
-
currentValue: el.value?.slice(0, 200),
|
|
11619
|
-
submitted: !!params.submit,
|
|
11620
|
-
};
|
|
13592
|
+
return result;
|
|
11621
13593
|
},
|
|
11622
13594
|
permissions: ['emitEvents'],
|
|
11623
13595
|
});
|
|
@@ -11747,7 +13719,7 @@ function registerBrowserActions(ai, wu) {
|
|
|
11747
13719
|
|
|
11748
13720
|
if (!target) return { error: `Element not found: ${params.selector}` };
|
|
11749
13721
|
|
|
11750
|
-
const tree =
|
|
13722
|
+
const tree = buildA11yTree(target, 0, params.depth || 5);
|
|
11751
13723
|
return { snapshot: tree };
|
|
11752
13724
|
},
|
|
11753
13725
|
permissions: [],
|
|
@@ -11791,31 +13763,13 @@ function registerBrowserActions(ai, wu) {
|
|
|
11791
13763
|
description: 'Filter: "2" (2xx success), "4" (4xx errors), "5" (5xx errors), "error" (all failures)',
|
|
11792
13764
|
required: false,
|
|
11793
13765
|
},
|
|
11794
|
-
limit: {
|
|
11795
|
-
type: 'number',
|
|
11796
|
-
description: 'Max requests to return (default: 30)',
|
|
11797
|
-
required: false,
|
|
11798
|
-
},
|
|
11799
|
-
},
|
|
11800
|
-
handler: async (params) => {
|
|
11801
|
-
let filtered = networkLog;
|
|
11802
|
-
if (params.method) {
|
|
11803
|
-
filtered = filtered.filter((r) => r.method === params.method.toUpperCase());
|
|
11804
|
-
}
|
|
11805
|
-
if (params.status) {
|
|
11806
|
-
if (params.status === 'error') {
|
|
11807
|
-
filtered = filtered.filter((r) => r.status === 0 || r.status >= 400);
|
|
11808
|
-
} else {
|
|
11809
|
-
filtered = filtered.filter((r) => String(r.status).startsWith(params.status));
|
|
11810
|
-
}
|
|
11811
|
-
}
|
|
11812
|
-
const limit = params.limit || 30;
|
|
11813
|
-
return {
|
|
11814
|
-
requests: filtered.slice(-limit),
|
|
11815
|
-
total: networkLog.length,
|
|
11816
|
-
showing: Math.min(filtered.length, limit),
|
|
11817
|
-
};
|
|
13766
|
+
limit: {
|
|
13767
|
+
type: 'number',
|
|
13768
|
+
description: 'Max requests to return (default: 30)',
|
|
13769
|
+
required: false,
|
|
13770
|
+
},
|
|
11818
13771
|
},
|
|
13772
|
+
handler: async (params) => getFilteredNetwork(params.method, params.status, params.limit),
|
|
11819
13773
|
permissions: [],
|
|
11820
13774
|
});
|
|
11821
13775
|
|
|
@@ -11837,17 +13791,7 @@ function registerBrowserActions(ai, wu) {
|
|
|
11837
13791
|
required: false,
|
|
11838
13792
|
},
|
|
11839
13793
|
},
|
|
11840
|
-
handler: async (params) =>
|
|
11841
|
-
const filtered = params.level
|
|
11842
|
-
? consoleLog.filter((m) => m.level === params.level)
|
|
11843
|
-
: consoleLog;
|
|
11844
|
-
const limit = params.limit || 30;
|
|
11845
|
-
return {
|
|
11846
|
-
messages: filtered.slice(-limit),
|
|
11847
|
-
total: consoleLog.length,
|
|
11848
|
-
showing: Math.min(filtered.length, limit),
|
|
11849
|
-
};
|
|
11850
|
-
},
|
|
13794
|
+
handler: async (params) => getFilteredConsole(params.level, params.limit),
|
|
11851
13795
|
permissions: [],
|
|
11852
13796
|
});
|
|
11853
13797
|
|
|
@@ -11895,141 +13839,8 @@ function registerBrowserActions(ai, wu) {
|
|
|
11895
13839
|
});
|
|
11896
13840
|
}
|
|
11897
13841
|
|
|
11898
|
-
//
|
|
11899
|
-
|
|
11900
|
-
function _inlineComputedStyles(source, clone) {
|
|
11901
|
-
const props = ['color', 'background', 'background-color', 'font-family',
|
|
11902
|
-
'font-size', 'font-weight', 'border', 'border-radius', 'padding', 'margin',
|
|
11903
|
-
'display', 'flex-direction', 'align-items', 'justify-content', 'gap',
|
|
11904
|
-
'width', 'height', 'max-width', 'max-height', 'overflow', 'opacity',
|
|
11905
|
-
'box-shadow', 'text-align', 'line-height', 'position', 'top', 'left',
|
|
11906
|
-
'right', 'bottom', 'z-index', 'transform', 'visibility'];
|
|
11907
|
-
|
|
11908
|
-
try {
|
|
11909
|
-
const style = window.getComputedStyle(source);
|
|
11910
|
-
for (const prop of props) {
|
|
11911
|
-
const val = style.getPropertyValue(prop);
|
|
11912
|
-
if (val) clone.style?.setProperty(prop, val);
|
|
11913
|
-
}
|
|
11914
|
-
} catch (_) { /* skip */ }
|
|
11915
|
-
|
|
11916
|
-
const srcKids = source.children || [];
|
|
11917
|
-
const cloneKids = clone.children || [];
|
|
11918
|
-
const max = Math.min(srcKids.length, cloneKids.length, 200);
|
|
11919
|
-
for (let i = 0; i < max; i++) {
|
|
11920
|
-
_inlineComputedStyles(srcKids[i], cloneKids[i]);
|
|
11921
|
-
}
|
|
11922
|
-
}
|
|
11923
|
-
|
|
11924
|
-
// ── Private: Build accessibility tree ──
|
|
11925
|
-
|
|
11926
|
-
function _buildA11yTree(el, depth, maxDepth) {
|
|
11927
|
-
if (depth > maxDepth || !el) return '';
|
|
11928
|
-
|
|
11929
|
-
const indent = ' '.repeat(depth);
|
|
11930
|
-
const tag = el.tagName?.toLowerCase() || '';
|
|
11931
|
-
const role = el.getAttribute?.('role') || '';
|
|
11932
|
-
const ariaLabel = el.getAttribute?.('aria-label') || '';
|
|
11933
|
-
const text = el.childNodes?.length === 1 && el.childNodes[0].nodeType === 3
|
|
11934
|
-
? el.textContent?.trim().slice(0, 80) : '';
|
|
11935
|
-
|
|
11936
|
-
let line = `${indent}<${tag}`;
|
|
11937
|
-
if (el.id) line += ` id="${el.id}"`;
|
|
11938
|
-
if (role) line += ` role="${role}"`;
|
|
11939
|
-
if (ariaLabel) line += ` aria-label="${ariaLabel}"`;
|
|
11940
|
-
if (el.className && typeof el.className === 'string') {
|
|
11941
|
-
const cls = el.className.trim().slice(0, 60);
|
|
11942
|
-
if (cls) line += ` class="${cls}"`;
|
|
11943
|
-
}
|
|
11944
|
-
line += '>';
|
|
11945
|
-
if (text) line += ` "${text}"`;
|
|
11946
|
-
|
|
11947
|
-
let result = line + '\n';
|
|
11948
|
-
const root = el.shadowRoot || el;
|
|
11949
|
-
const children = root.children || [];
|
|
11950
|
-
|
|
11951
|
-
for (let i = 0; i < children.length && i < 50; i++) {
|
|
11952
|
-
result += _buildA11yTree(children[i], depth + 1, maxDepth);
|
|
11953
|
-
}
|
|
11954
|
-
return result;
|
|
11955
|
-
}
|
|
11956
|
-
|
|
11957
|
-
// ── Private: Network interceptor ──
|
|
11958
|
-
|
|
11959
|
-
function _installNetworkInterceptor() {
|
|
11960
|
-
// Intercept fetch
|
|
11961
|
-
const originalFetch = window.fetch;
|
|
11962
|
-
window.fetch = async function (...args) {
|
|
11963
|
-
const start = Date.now();
|
|
11964
|
-
const req = args[0];
|
|
11965
|
-
const url = typeof req === 'string' ? req : req?.url || '';
|
|
11966
|
-
const method = (args[1]?.method || req?.method || 'GET').toUpperCase();
|
|
11967
|
-
|
|
11968
|
-
try {
|
|
11969
|
-
const response = await originalFetch.apply(window, args);
|
|
11970
|
-
const size = parseInt(response.headers?.get('content-length') || '0', 10);
|
|
11971
|
-
networkLog.push({
|
|
11972
|
-
type: 'fetch', method, url,
|
|
11973
|
-
status: response.status, statusText: response.statusText,
|
|
11974
|
-
duration: Date.now() - start, size, timestamp: start,
|
|
11975
|
-
});
|
|
11976
|
-
if (networkLog.length > MAX_NETWORK_LOG) networkLog.shift();
|
|
11977
|
-
return response;
|
|
11978
|
-
} catch (err) {
|
|
11979
|
-
networkLog.push({
|
|
11980
|
-
type: 'fetch', method, url,
|
|
11981
|
-
status: 0, error: err.message,
|
|
11982
|
-
duration: Date.now() - start, timestamp: start,
|
|
11983
|
-
});
|
|
11984
|
-
if (networkLog.length > MAX_NETWORK_LOG) networkLog.shift();
|
|
11985
|
-
throw err;
|
|
11986
|
-
}
|
|
11987
|
-
};
|
|
11988
|
-
|
|
11989
|
-
// Intercept XMLHttpRequest
|
|
11990
|
-
const origOpen = XMLHttpRequest.prototype.open;
|
|
11991
|
-
const origSend = XMLHttpRequest.prototype.send;
|
|
11992
|
-
|
|
11993
|
-
XMLHttpRequest.prototype.open = function (method, url, ...rest) {
|
|
11994
|
-
this._wuAi = { method: (method || 'GET').toUpperCase(), url: String(url) };
|
|
11995
|
-
return origOpen.call(this, method, url, ...rest);
|
|
11996
|
-
};
|
|
11997
|
-
|
|
11998
|
-
XMLHttpRequest.prototype.send = function (...args) {
|
|
11999
|
-
if (this._wuAi) {
|
|
12000
|
-
this._wuAi.start = Date.now();
|
|
12001
|
-
this.addEventListener('loadend', () => {
|
|
12002
|
-
networkLog.push({
|
|
12003
|
-
type: 'xhr', method: this._wuAi.method, url: this._wuAi.url,
|
|
12004
|
-
status: this.status, statusText: this.statusText,
|
|
12005
|
-
duration: Date.now() - this._wuAi.start,
|
|
12006
|
-
size: parseInt(this.getResponseHeader('content-length') || '0', 10),
|
|
12007
|
-
timestamp: this._wuAi.start,
|
|
12008
|
-
});
|
|
12009
|
-
if (networkLog.length > MAX_NETWORK_LOG) networkLog.shift();
|
|
12010
|
-
});
|
|
12011
|
-
}
|
|
12012
|
-
return origSend.apply(this, args);
|
|
12013
|
-
};
|
|
12014
|
-
}
|
|
12015
|
-
|
|
12016
|
-
// ── Private: Console interceptor ──
|
|
12017
|
-
|
|
12018
|
-
function _installConsoleInterceptor() {
|
|
12019
|
-
const levels = ['log', 'warn', 'error'];
|
|
12020
|
-
for (const level of levels) {
|
|
12021
|
-
const original = console[level];
|
|
12022
|
-
console[level] = (...args) => {
|
|
12023
|
-
consoleLog.push({
|
|
12024
|
-
level,
|
|
12025
|
-
message: args.map((a) => (typeof a === 'object' ? JSON.stringify(a) : String(a))).join(' '),
|
|
12026
|
-
timestamp: Date.now(),
|
|
12027
|
-
});
|
|
12028
|
-
if (consoleLog.length > MAX_CONSOLE_LOG) consoleLog.shift();
|
|
12029
|
-
original.apply(console, args);
|
|
12030
|
-
};
|
|
12031
|
-
}
|
|
12032
|
-
}
|
|
13842
|
+
// All private helpers (buildA11yTree, inlineComputedStyles, interceptors)
|
|
13843
|
+
// are now in wu-ai-browser-primitives.js — single source of truth.
|
|
12033
13844
|
|
|
12034
13845
|
/**
|
|
12035
13846
|
* WU-AI: Central orchestrator for AI integration
|
|
@@ -12039,19 +13850,35 @@ function _installConsoleInterceptor() {
|
|
|
12039
13850
|
*
|
|
12040
13851
|
* Architecture:
|
|
12041
13852
|
* WuAI (this file)
|
|
12042
|
-
* ├── WuAIProvider
|
|
12043
|
-
* ├── WuAIPermissions
|
|
12044
|
-
* ├── WuAIContext
|
|
12045
|
-
* ├── WuAIActions
|
|
12046
|
-
* ├── WuAIConversation
|
|
12047
|
-
*
|
|
13853
|
+
* ├── WuAIProvider → BYOL provider management (OpenAI, Anthropic, Ollama, Custom)
|
|
13854
|
+
* ├── WuAIPermissions → 4-layer security (perms, rate limit, circuit breaker, loop guard)
|
|
13855
|
+
* ├── WuAIContext → Auto context collection with token budget
|
|
13856
|
+
* ├── WuAIActions → Tool/action registry and sandboxed execution
|
|
13857
|
+
* ├── WuAIConversation → Multi-turn conversation manager with namespaces
|
|
13858
|
+
* ├── WuAITriggers → Event-to-AI reactive bridge
|
|
13859
|
+
* ├── WuAIAgent → Autonomous agent loop (goal → steps → done)
|
|
13860
|
+
* ├── WuAIOrchestrate → Cross-micro-app AI coordination (capabilities + intents)
|
|
13861
|
+
* └── BrowserPrimitives → Shared screenshot, click, type, a11y tree, interceptors
|
|
13862
|
+
*
|
|
13863
|
+
* Four Paradigms:
|
|
13864
|
+
* 1. App → LLM send/stream/json → conversation with tool loops
|
|
13865
|
+
* 2. LLM → App tools/execute/expose → external agents call into the app
|
|
13866
|
+
* 3. AI Director agent(goal) → autonomous multi-step loop
|
|
13867
|
+
* 4. MF Glue capability/intent → cross-app coordination via AI
|
|
12048
13868
|
*
|
|
12049
13869
|
* Public API (accessible via wu.ai):
|
|
12050
13870
|
* wu.ai.provider(name, config) → Register LLM provider
|
|
12051
13871
|
* wu.ai.send(message, opts) → Send message (non-streaming)
|
|
12052
13872
|
* wu.ai.stream(message, opts) → Send message (streaming)
|
|
13873
|
+
* wu.ai.json(message, schema?) → Send and get parsed JSON back
|
|
13874
|
+
* wu.ai.agent(goal, opts) → Run autonomous agent loop
|
|
12053
13875
|
* wu.ai.action(name, config) → Register an action/tool
|
|
12054
13876
|
* wu.ai.trigger(name, config) → Register an event trigger
|
|
13877
|
+
* wu.ai.capability(app, name, c) → Register app-scoped capability
|
|
13878
|
+
* wu.ai.intent(desc, opts) → Resolve cross-app intent
|
|
13879
|
+
* wu.ai.removeApp(appName) → Remove app capabilities (unmount)
|
|
13880
|
+
* wu.ai.workflow(name, config) → Register reusable AI workflow
|
|
13881
|
+
* wu.ai.runWorkflow(name, params)→ Execute workflow (async generator)
|
|
12055
13882
|
* wu.ai.context.configure(...) → Configure context collection
|
|
12056
13883
|
* wu.ai.abort(namespace?) → Abort active request
|
|
12057
13884
|
*
|
|
@@ -12150,10 +13977,38 @@ class WuAI {
|
|
|
12150
13977
|
this._modules.triggers.configure(config.triggers);
|
|
12151
13978
|
}
|
|
12152
13979
|
|
|
13980
|
+
// 7. Agent (depends on conversation, actions, context, permissions, eventBus)
|
|
13981
|
+
this._modules.agent = new WuAIAgent({
|
|
13982
|
+
conversation: this._modules.conversation,
|
|
13983
|
+
actions: this._modules.actions,
|
|
13984
|
+
context: this._modules.context,
|
|
13985
|
+
permissions: this._modules.permissions,
|
|
13986
|
+
eventBus: this._eventBus,
|
|
13987
|
+
});
|
|
13988
|
+
if (config.agent) {
|
|
13989
|
+
this._modules.agent.configure(config.agent);
|
|
13990
|
+
}
|
|
13991
|
+
|
|
13992
|
+
// 8. Orchestrate — Paradigm 4: AI as microfrontend glue
|
|
13993
|
+
// Agent ref is passed so workflows can delegate to the agent loop
|
|
13994
|
+
// Store ref is passed for deterministic setState steps
|
|
13995
|
+
this._modules.orchestrate = new WuAIOrchestrate({
|
|
13996
|
+
actions: this._modules.actions,
|
|
13997
|
+
conversation: this._modules.conversation,
|
|
13998
|
+
context: this._modules.context,
|
|
13999
|
+
permissions: this._modules.permissions,
|
|
14000
|
+
eventBus: this._eventBus,
|
|
14001
|
+
agent: this._modules.agent,
|
|
14002
|
+
store: this._store,
|
|
14003
|
+
});
|
|
14004
|
+
if (config.orchestrate) {
|
|
14005
|
+
this._modules.orchestrate.configure(config.orchestrate);
|
|
14006
|
+
}
|
|
14007
|
+
|
|
12153
14008
|
this._initialized = true;
|
|
12154
14009
|
logger.wuInfo('[wu-ai] Initialized');
|
|
12155
14010
|
|
|
12156
|
-
//
|
|
14011
|
+
// 9. Browser automation actions (screenshot, click, type, network, etc.)
|
|
12157
14012
|
// Must be AFTER _initialized = true to prevent recursive init loop
|
|
12158
14013
|
if (typeof window !== 'undefined') {
|
|
12159
14014
|
registerBrowserActions(this, this._core);
|
|
@@ -12202,7 +14057,12 @@ class WuAI {
|
|
|
12202
14057
|
* Send a message to the LLM and get a complete response.
|
|
12203
14058
|
*
|
|
12204
14059
|
* @param {string} message - User message
|
|
12205
|
-
* @param {object} [options] - { namespace, systemPrompt, templateVars, temperature, maxTokens, signal }
|
|
14060
|
+
* @param {object} [options] - { namespace, systemPrompt, templateVars, temperature, maxTokens, provider, responseFormat, signal }
|
|
14061
|
+
* @param {string} [options.provider] - Use a specific registered provider (e.g., 'anthropic', 'openai')
|
|
14062
|
+
* @param {string|object} [options.responseFormat] - Request JSON output.
|
|
14063
|
+
* - `'json'` — simple JSON mode (OpenAI: json_object, Ollama: format:"json", Anthropic: prompt injection)
|
|
14064
|
+
* - `{ type: 'json_schema', schema: {...}, name?: string }` — structured output with JSON Schema
|
|
14065
|
+
* (OpenAI: native json_schema mode, Ollama: schema in format, Anthropic: schema in system prompt)
|
|
12206
14066
|
* @returns {Promise<{ content: string, tool_results?: Array, usage?: object, namespace: string }>}
|
|
12207
14067
|
*
|
|
12208
14068
|
* @example
|
|
@@ -12211,6 +14071,21 @@ class WuAI {
|
|
|
12211
14071
|
*
|
|
12212
14072
|
* // With namespace for separate conversation
|
|
12213
14073
|
* const response = await wu.ai.send('Analyze this chart', { namespace: 'analytics' });
|
|
14074
|
+
*
|
|
14075
|
+
* // Use a specific provider for this message
|
|
14076
|
+
* const response = await wu.ai.send('Translate this', { provider: 'anthropic' });
|
|
14077
|
+
*
|
|
14078
|
+
* // Simple JSON mode
|
|
14079
|
+
* const response = await wu.ai.send('List 5 colors', { responseFormat: 'json' });
|
|
14080
|
+
*
|
|
14081
|
+
* // Structured output with JSON Schema
|
|
14082
|
+
* const response = await wu.ai.send('List 5 colors', {
|
|
14083
|
+
* responseFormat: {
|
|
14084
|
+
* type: 'json_schema',
|
|
14085
|
+
* schema: { type: 'object', properties: { colors: { type: 'array', items: { type: 'string' } } } },
|
|
14086
|
+
* name: 'color_list',
|
|
14087
|
+
* },
|
|
14088
|
+
* });
|
|
12214
14089
|
*/
|
|
12215
14090
|
async send(message, options = {}) {
|
|
12216
14091
|
this._ensureInit();
|
|
@@ -12235,6 +14110,71 @@ class WuAI {
|
|
|
12235
14110
|
yield* this._modules.conversation.stream(message, options);
|
|
12236
14111
|
}
|
|
12237
14112
|
|
|
14113
|
+
/**
|
|
14114
|
+
* Send a message and get a parsed JSON response.
|
|
14115
|
+
* Shortcut for send() with responseFormat + automatic JSON.parse().
|
|
14116
|
+
*
|
|
14117
|
+
* @param {string} message - User message
|
|
14118
|
+
* @param {object} [options] - All send() options plus:
|
|
14119
|
+
* @param {object} [options.schema] - JSON Schema for structured output
|
|
14120
|
+
* @param {string} [options.schemaName='response'] - Schema name (required by OpenAI)
|
|
14121
|
+
* @returns {Promise<{ data: object|null, raw: string, error?: string, usage?: object, namespace: string }>}
|
|
14122
|
+
*
|
|
14123
|
+
* @example
|
|
14124
|
+
* // Simple JSON (no schema)
|
|
14125
|
+
* const { data } = await wu.ai.json('List 5 colors as a JSON array');
|
|
14126
|
+
* // data = ["red", "blue", ...]
|
|
14127
|
+
*
|
|
14128
|
+
* // With schema
|
|
14129
|
+
* const { data } = await wu.ai.json('List 5 colors', {
|
|
14130
|
+
* schema: { type: 'object', properties: { colors: { type: 'array', items: { type: 'string' } } } },
|
|
14131
|
+
* });
|
|
14132
|
+
* // data = { colors: ["red", "blue", ...] }
|
|
14133
|
+
*
|
|
14134
|
+
* // With schema + provider
|
|
14135
|
+
* const { data } = await wu.ai.json('List 5 colors', {
|
|
14136
|
+
* schema: mySchema,
|
|
14137
|
+
* provider: 'openai',
|
|
14138
|
+
* temperature: 0,
|
|
14139
|
+
* });
|
|
14140
|
+
*/
|
|
14141
|
+
async json(message, options = {}) {
|
|
14142
|
+
this._ensureInit();
|
|
14143
|
+
|
|
14144
|
+
const { schema, schemaName, ...rest } = options;
|
|
14145
|
+
|
|
14146
|
+
let responseFormat;
|
|
14147
|
+
if (schema) {
|
|
14148
|
+
responseFormat = { type: 'json_schema', schema, name: schemaName || 'response' };
|
|
14149
|
+
} else {
|
|
14150
|
+
responseFormat = options.responseFormat || 'json';
|
|
14151
|
+
}
|
|
14152
|
+
|
|
14153
|
+
const response = await this._modules.conversation.send(message, { ...rest, responseFormat });
|
|
14154
|
+
|
|
14155
|
+
// The provider already attempts parse and sets response.parsed / response.parseError
|
|
14156
|
+
let data = null;
|
|
14157
|
+
let error;
|
|
14158
|
+
|
|
14159
|
+
if (response.parsed !== undefined) {
|
|
14160
|
+
data = response.parsed;
|
|
14161
|
+
} else if (response.content) {
|
|
14162
|
+
try {
|
|
14163
|
+
data = JSON.parse(response.content);
|
|
14164
|
+
} catch {
|
|
14165
|
+
error = 'LLM response is not valid JSON';
|
|
14166
|
+
}
|
|
14167
|
+
}
|
|
14168
|
+
|
|
14169
|
+
return {
|
|
14170
|
+
data,
|
|
14171
|
+
raw: response.content || '',
|
|
14172
|
+
error: error || response.parseError,
|
|
14173
|
+
usage: response.usage,
|
|
14174
|
+
namespace: response.namespace,
|
|
14175
|
+
};
|
|
14176
|
+
}
|
|
14177
|
+
|
|
12238
14178
|
/**
|
|
12239
14179
|
* Abort active request(s).
|
|
12240
14180
|
*
|
|
@@ -12311,18 +14251,211 @@ class WuAI {
|
|
|
12311
14251
|
* },
|
|
12312
14252
|
* });
|
|
12313
14253
|
*/
|
|
12314
|
-
trigger(name, config) {
|
|
14254
|
+
trigger(name, config) {
|
|
14255
|
+
this._ensureInit();
|
|
14256
|
+
this._modules.triggers.register(name, config);
|
|
14257
|
+
return this;
|
|
14258
|
+
}
|
|
14259
|
+
|
|
14260
|
+
/**
|
|
14261
|
+
* Fire a trigger manually.
|
|
14262
|
+
*/
|
|
14263
|
+
async fireTrigger(name, eventData) {
|
|
14264
|
+
this._ensureInit();
|
|
14265
|
+
return this._modules.triggers.fire(name, eventData);
|
|
14266
|
+
}
|
|
14267
|
+
|
|
14268
|
+
// ─── Agent (Paradigm 3: Autonomous AI) ─────────────────────────
|
|
14269
|
+
|
|
14270
|
+
/**
|
|
14271
|
+
* Run an autonomous agent that pursues a goal using available tools.
|
|
14272
|
+
* Returns an async generator that yields step-by-step results.
|
|
14273
|
+
*
|
|
14274
|
+
* @param {string} goal - What the agent should accomplish
|
|
14275
|
+
* @param {object} [options]
|
|
14276
|
+
* @param {number} [options.maxSteps=10] - Maximum autonomous steps
|
|
14277
|
+
* @param {string} [options.provider] - Which LLM provider to use
|
|
14278
|
+
* @param {string} [options.namespace] - Conversation namespace
|
|
14279
|
+
* @param {string} [options.systemPrompt] - Override system prompt
|
|
14280
|
+
* @param {Function} [options.onStep] - Callback per step: (stepResult) => void
|
|
14281
|
+
* @param {Function} [options.shouldContinue] - Human-in-the-loop: (stepResult) => boolean|Promise<boolean>
|
|
14282
|
+
* @param {AbortSignal} [options.signal] - Abort signal
|
|
14283
|
+
* @returns {AsyncGenerator<AgentStepResult>}
|
|
14284
|
+
*
|
|
14285
|
+
* @example
|
|
14286
|
+
* // Basic usage
|
|
14287
|
+
* for await (const step of wu.ai.agent('Find all orders above $100 and summarize them')) {
|
|
14288
|
+
* console.log(`Step ${step.step}: ${step.content?.slice(0, 100)}`);
|
|
14289
|
+
* if (step.done) console.log('Agent finished!');
|
|
14290
|
+
* }
|
|
14291
|
+
*
|
|
14292
|
+
* // With human-in-the-loop
|
|
14293
|
+
* for await (const step of wu.ai.agent('Reorganize the product catalog', {
|
|
14294
|
+
* shouldContinue: (step) => confirm(`Continue? Step ${step.step}: ${step.content?.slice(0, 50)}`),
|
|
14295
|
+
* })) {
|
|
14296
|
+
* updateUI(step);
|
|
14297
|
+
* }
|
|
14298
|
+
*/
|
|
14299
|
+
async *agent(goal, options = {}) {
|
|
14300
|
+
this._ensureInit();
|
|
14301
|
+
yield* this._modules.agent.run(goal, options);
|
|
14302
|
+
}
|
|
14303
|
+
|
|
14304
|
+
// ─── Paradigm 4: AI as Microfrontend Glue ─────────────────────
|
|
14305
|
+
|
|
14306
|
+
/**
|
|
14307
|
+
* Register a capability scoped to a specific micro-app.
|
|
14308
|
+
*
|
|
14309
|
+
* Each micro-app calls this to declare what it can do. The AI uses
|
|
14310
|
+
* the capability map to resolve cross-app intents.
|
|
14311
|
+
*
|
|
14312
|
+
* @param {string} appName - The micro-app name (e.g., 'orders', 'dashboard')
|
|
14313
|
+
* @param {string} actionName - The capability name (e.g., 'getRecent', 'updateKPIs')
|
|
14314
|
+
* @param {object} config - Same as wu.ai.action() config:
|
|
14315
|
+
* { description, parameters, handler, confirm?, permissions?, dangerous? }
|
|
14316
|
+
*
|
|
14317
|
+
* @example
|
|
14318
|
+
* // In orders micro-app (React):
|
|
14319
|
+
* wu.ai.capability('orders', 'getRecent', {
|
|
14320
|
+
* description: 'Get the N most recent orders',
|
|
14321
|
+
* parameters: { limit: { type: 'number' } },
|
|
14322
|
+
* handler: async (params) => fetchOrders({ limit: params.limit || 10 }),
|
|
14323
|
+
* });
|
|
14324
|
+
*
|
|
14325
|
+
* // In dashboard micro-app (Svelte):
|
|
14326
|
+
* wu.ai.capability('dashboard', 'updateKPIs', {
|
|
14327
|
+
* description: 'Refresh the KPI cards with latest data',
|
|
14328
|
+
* handler: async () => { refreshKPIs(); return { updated: true }; },
|
|
14329
|
+
* });
|
|
14330
|
+
*/
|
|
14331
|
+
capability(appName, actionName, config) {
|
|
14332
|
+
this._ensureInit();
|
|
14333
|
+
this._modules.orchestrate.register(appName, actionName, config);
|
|
14334
|
+
return this;
|
|
14335
|
+
}
|
|
14336
|
+
|
|
14337
|
+
/**
|
|
14338
|
+
* Resolve a cross-app intent in a single conversation turn.
|
|
14339
|
+
*
|
|
14340
|
+
* The AI receives the full capability map (what each app can do),
|
|
14341
|
+
* current application state, and mounted apps. It resolves the
|
|
14342
|
+
* intent by calling the right capabilities across app boundaries.
|
|
14343
|
+
*
|
|
14344
|
+
* @param {string} description - Natural language intent
|
|
14345
|
+
* @param {object} [options]
|
|
14346
|
+
* @param {string[]} [options.plan] - Optional action sequence hint
|
|
14347
|
+
* @param {string} [options.provider] - LLM provider override
|
|
14348
|
+
* @param {number} [options.temperature] - Temperature override
|
|
14349
|
+
* @param {number} [options.maxTokens] - Max tokens override
|
|
14350
|
+
* @param {AbortSignal} [options.signal] - Abort signal
|
|
14351
|
+
* @param {string|object} [options.responseFormat] - Response format
|
|
14352
|
+
* @returns {Promise<{ content: string, tool_results: Array, usage: object|null, resolved: boolean, appsInvolved: string[] }>}
|
|
14353
|
+
*
|
|
14354
|
+
* @example
|
|
14355
|
+
* // Simple cross-app query
|
|
14356
|
+
* const result = await wu.ai.intent('Show me the top customer by order count');
|
|
14357
|
+
* // AI calls orders:getRecent → aggregates → returns answer
|
|
14358
|
+
*
|
|
14359
|
+
* // With plan hint
|
|
14360
|
+
* const result = await wu.ai.intent('Update all views after a new order', {
|
|
14361
|
+
* plan: ['orders:getRecent', 'dashboard:updateKPIs', 'analytics:refresh'],
|
|
14362
|
+
* });
|
|
14363
|
+
*
|
|
14364
|
+
* // With JSON response
|
|
14365
|
+
* const result = await wu.ai.intent('Get order stats by status', {
|
|
14366
|
+
* responseFormat: 'json',
|
|
14367
|
+
* });
|
|
14368
|
+
*/
|
|
14369
|
+
async intent(description, options = {}) {
|
|
14370
|
+
this._ensureInit();
|
|
14371
|
+
return this._modules.orchestrate.resolve(description, options);
|
|
14372
|
+
}
|
|
14373
|
+
|
|
14374
|
+
/**
|
|
14375
|
+
* Remove all capabilities for a micro-app.
|
|
14376
|
+
* Call this when a micro-app is unmounted to prevent stale
|
|
14377
|
+
* capabilities from appearing in the AI's capability map.
|
|
14378
|
+
*
|
|
14379
|
+
* @param {string} appName - The micro-app name
|
|
14380
|
+
*
|
|
14381
|
+
* @example
|
|
14382
|
+
* // In unmount lifecycle:
|
|
14383
|
+
* wu.ai.removeApp('orders');
|
|
14384
|
+
*/
|
|
14385
|
+
removeApp(appName) {
|
|
14386
|
+
this._ensureInit();
|
|
14387
|
+
this._modules.orchestrate.removeApp(appName);
|
|
14388
|
+
return this;
|
|
14389
|
+
}
|
|
14390
|
+
|
|
14391
|
+
/**
|
|
14392
|
+
* Register a reusable AI workflow — a named, step-by-step recipe
|
|
14393
|
+
* that the AI agent follows using browser automation.
|
|
14394
|
+
*
|
|
14395
|
+
* @param {string} name - Workflow name (e.g., 'register-user')
|
|
14396
|
+
* @param {object} config
|
|
14397
|
+
* @param {string} config.description - What this workflow does
|
|
14398
|
+
* @param {string[]} config.steps - Step-by-step instructions
|
|
14399
|
+
* Use {{paramName}} for parameter interpolation.
|
|
14400
|
+
* @param {object} [config.parameters] - Parameter definitions
|
|
14401
|
+
* @param {number} [config.maxSteps=15] - Max agent steps
|
|
14402
|
+
* @param {string} [config.provider] - LLM provider
|
|
14403
|
+
*
|
|
14404
|
+
* @example
|
|
14405
|
+
* wu.ai.workflow('register-user', {
|
|
14406
|
+
* description: 'Register a new user in the system',
|
|
14407
|
+
* steps: [
|
|
14408
|
+
* 'Navigate to the Customers section',
|
|
14409
|
+
* 'Click the "Add Customer" button',
|
|
14410
|
+
* 'Type "{{name}}" into the name field',
|
|
14411
|
+
* 'Type "{{email}}" into the email field',
|
|
14412
|
+
* 'Click Submit',
|
|
14413
|
+
* 'Verify the success message appears',
|
|
14414
|
+
* ],
|
|
14415
|
+
* parameters: {
|
|
14416
|
+
* name: { type: 'string', required: true },
|
|
14417
|
+
* email: { type: 'string', required: true },
|
|
14418
|
+
* },
|
|
14419
|
+
* });
|
|
14420
|
+
*/
|
|
14421
|
+
workflow(name, config) {
|
|
12315
14422
|
this._ensureInit();
|
|
12316
|
-
this._modules.
|
|
14423
|
+
this._modules.orchestrate.registerWorkflow(name, config);
|
|
12317
14424
|
return this;
|
|
12318
14425
|
}
|
|
12319
14426
|
|
|
12320
14427
|
/**
|
|
12321
|
-
*
|
|
14428
|
+
* Execute a registered workflow. Returns an async generator
|
|
14429
|
+
* so you can observe each step in real time.
|
|
14430
|
+
*
|
|
14431
|
+
* @param {string} name - Workflow name
|
|
14432
|
+
* @param {object} [params={}] - Parameters to fill into the steps
|
|
14433
|
+
* @param {object} [options={}]
|
|
14434
|
+
* @param {Function} [options.onStep] - Callback per step
|
|
14435
|
+
* @param {Function} [options.shouldContinue] - Human-in-the-loop gate
|
|
14436
|
+
* @param {AbortSignal} [options.signal] - Abort signal
|
|
14437
|
+
* @returns {AsyncGenerator<AgentStepResult>}
|
|
14438
|
+
*
|
|
14439
|
+
* @example
|
|
14440
|
+
* // Run and watch every step
|
|
14441
|
+
* for await (const step of wu.ai.runWorkflow('register-user', {
|
|
14442
|
+
* name: 'Juan Pérez',
|
|
14443
|
+
* email: 'juan@test.com',
|
|
14444
|
+
* })) {
|
|
14445
|
+
* console.log(`Step ${step.step}: ${step.content}`);
|
|
14446
|
+
* if (step.type === 'done') console.log('Workflow complete!');
|
|
14447
|
+
* }
|
|
14448
|
+
*
|
|
14449
|
+
* // With human approval per step
|
|
14450
|
+
* for await (const step of wu.ai.runWorkflow('register-user', params, {
|
|
14451
|
+
* shouldContinue: (s) => confirm(`Continue? ${s.content?.slice(0, 60)}`),
|
|
14452
|
+
* })) {
|
|
14453
|
+
* renderStep(step);
|
|
14454
|
+
* }
|
|
12322
14455
|
*/
|
|
12323
|
-
async
|
|
14456
|
+
async *runWorkflow(name, params = {}, options = {}) {
|
|
12324
14457
|
this._ensureInit();
|
|
12325
|
-
|
|
14458
|
+
yield* this._modules.orchestrate.executeWorkflow(name, params, options);
|
|
12326
14459
|
}
|
|
12327
14460
|
|
|
12328
14461
|
// ─── Context ───────────────────────────────────────────────────
|
|
@@ -12461,6 +14594,8 @@ class WuAI {
|
|
|
12461
14594
|
actions: this._modules.actions.getStats(),
|
|
12462
14595
|
conversation: this._modules.conversation.getStats(),
|
|
12463
14596
|
triggers: this._modules.triggers.getStats(),
|
|
14597
|
+
agent: this._modules.agent.getStats(),
|
|
14598
|
+
orchestrate: this._modules.orchestrate.getStats(),
|
|
12464
14599
|
};
|
|
12465
14600
|
}
|
|
12466
14601
|
|
|
@@ -12470,6 +14605,8 @@ class WuAI {
|
|
|
12470
14605
|
destroy() {
|
|
12471
14606
|
if (!this._initialized) return;
|
|
12472
14607
|
|
|
14608
|
+
this._modules.orchestrate.destroy();
|
|
14609
|
+
this._modules.agent.destroy();
|
|
12473
14610
|
this._modules.conversation.abortAll();
|
|
12474
14611
|
this._modules.triggers.destroy();
|
|
12475
14612
|
this._modules = {};
|
|
@@ -12495,6 +14632,8 @@ class WuAI {
|
|
|
12495
14632
|
if (config.context) this._modules.context.configure(config.context);
|
|
12496
14633
|
if (config.conversation) this._modules.conversation.configure(config.conversation);
|
|
12497
14634
|
if (config.triggers) this._modules.triggers.configure(config.triggers);
|
|
14635
|
+
if (config.agent) this._modules.agent.configure(config.agent);
|
|
14636
|
+
if (config.orchestrate) this._modules.orchestrate.configure(config.orchestrate);
|
|
12498
14637
|
}
|
|
12499
14638
|
}
|
|
12500
14639
|
|
|
@@ -12505,14 +14644,21 @@ class WuAI {
|
|
|
12505
14644
|
* commands using wu.* APIs. This is the "eyes and hands" of
|
|
12506
14645
|
* the MCP server inside the browser.
|
|
12507
14646
|
*
|
|
14647
|
+
* Security:
|
|
14648
|
+
* - Optional auth token sent on first message (handshake)
|
|
14649
|
+
* - All state/event/mount operations check wu.ai permissions
|
|
14650
|
+
* - Mutating operations emit audit events
|
|
14651
|
+
* - Read-only operations (status, list_apps, snapshot, console, network) are unrestricted
|
|
14652
|
+
*
|
|
12508
14653
|
* @example
|
|
12509
|
-
* //
|
|
12510
|
-
*
|
|
14654
|
+
* // Connect with auth token
|
|
14655
|
+
* wu.mcp.connect('ws://localhost:19100', { token: 'my-secret' });
|
|
12511
14656
|
*
|
|
12512
|
-
*
|
|
12513
|
-
*
|
|
14657
|
+
* // Connect without auth (development only)
|
|
14658
|
+
* wu.mcp.connect();
|
|
12514
14659
|
*/
|
|
12515
14660
|
|
|
14661
|
+
|
|
12516
14662
|
/**
|
|
12517
14663
|
* Create the MCP bridge for a Wu instance.
|
|
12518
14664
|
*
|
|
@@ -12523,6 +14669,8 @@ function createMcpBridge(wu) {
|
|
|
12523
14669
|
let ws = null;
|
|
12524
14670
|
let reconnectTimer = null;
|
|
12525
14671
|
let reconnectAttempts = 0;
|
|
14672
|
+
let authenticated = false;
|
|
14673
|
+
let authToken = null;
|
|
12526
14674
|
const MAX_RECONNECT_ATTEMPTS = 10;
|
|
12527
14675
|
const RECONNECT_DELAY = 2000;
|
|
12528
14676
|
|
|
@@ -12530,14 +14678,6 @@ function createMcpBridge(wu) {
|
|
|
12530
14678
|
const eventLog = [];
|
|
12531
14679
|
const MAX_EVENT_LOG = 200;
|
|
12532
14680
|
|
|
12533
|
-
// Console log capture
|
|
12534
|
-
const consoleLog = [];
|
|
12535
|
-
const MAX_CONSOLE_LOG = 500;
|
|
12536
|
-
|
|
12537
|
-
// Network log capture
|
|
12538
|
-
const networkLog = [];
|
|
12539
|
-
const MAX_NETWORK_LOG = 300;
|
|
12540
|
-
|
|
12541
14681
|
// Capture events for history
|
|
12542
14682
|
if (wu.eventBus) {
|
|
12543
14683
|
wu.eventBus.on('*', (event) => {
|
|
@@ -12551,22 +14691,50 @@ function createMcpBridge(wu) {
|
|
|
12551
14691
|
});
|
|
12552
14692
|
}
|
|
12553
14693
|
|
|
12554
|
-
//
|
|
12555
|
-
|
|
14694
|
+
// Install shared interceptors (idempotent — safe if wu-ai-browser already did it)
|
|
14695
|
+
ensureInterceptors();
|
|
14696
|
+
|
|
14697
|
+
// ── Permission helpers ──
|
|
14698
|
+
|
|
14699
|
+
/**
|
|
14700
|
+
* Check a permission flag via wu.ai.permissions if available.
|
|
14701
|
+
* Falls back to deny if wu.ai is not initialized.
|
|
14702
|
+
*/
|
|
14703
|
+
function _checkPermission(perm) {
|
|
14704
|
+
if (wu.ai && wu.ai.permissions) {
|
|
14705
|
+
return wu.ai.permissions.check(perm);
|
|
14706
|
+
}
|
|
14707
|
+
// If AI module not initialized, deny write operations, allow reads
|
|
14708
|
+
const readPerms = ['readStore', 'executeActions'];
|
|
14709
|
+
return readPerms.includes(perm);
|
|
14710
|
+
}
|
|
12556
14711
|
|
|
12557
|
-
|
|
12558
|
-
|
|
14712
|
+
/**
|
|
14713
|
+
* Emit an audit event for bridge operations.
|
|
14714
|
+
*/
|
|
14715
|
+
function _audit(operation, params, result) {
|
|
14716
|
+
if (wu.eventBus) {
|
|
14717
|
+
wu.eventBus.emit('mcp:bridge:operation', {
|
|
14718
|
+
operation,
|
|
14719
|
+
params,
|
|
14720
|
+
result: result?.error ? { error: result.error } : { success: true },
|
|
14721
|
+
timestamp: Date.now(),
|
|
14722
|
+
}, { appName: 'wu-mcp-bridge' });
|
|
14723
|
+
}
|
|
14724
|
+
}
|
|
12559
14725
|
|
|
12560
14726
|
// ── Command handlers ──
|
|
12561
14727
|
|
|
12562
14728
|
const handlers = {
|
|
14729
|
+
// ── Read-only operations (no permission gates) ──
|
|
14730
|
+
|
|
12563
14731
|
status() {
|
|
12564
14732
|
return {
|
|
12565
14733
|
connected: true,
|
|
12566
14734
|
framework: 'wu-framework',
|
|
12567
14735
|
apps: _getAppList(),
|
|
12568
14736
|
storeKeys: wu.store ? Object.keys(wu.store.get('') || {}) : [],
|
|
12569
|
-
actionsCount: wu.ai
|
|
14737
|
+
actionsCount: wu.ai ? wu.ai.tools().length : 0,
|
|
12570
14738
|
eventLogSize: eventLog.length,
|
|
12571
14739
|
};
|
|
12572
14740
|
},
|
|
@@ -12575,45 +14743,55 @@ function createMcpBridge(wu) {
|
|
|
12575
14743
|
return _getAppList();
|
|
12576
14744
|
},
|
|
12577
14745
|
|
|
12578
|
-
|
|
12579
|
-
|
|
12580
|
-
if (wu.eventBus) {
|
|
12581
|
-
wu.eventBus.emit('shell:navigate', { route });
|
|
12582
|
-
}
|
|
12583
|
-
if (wu.store) {
|
|
12584
|
-
wu.store.set('currentPath', route);
|
|
12585
|
-
}
|
|
12586
|
-
return { navigated: route };
|
|
14746
|
+
list_events({ limit = 20 }) {
|
|
14747
|
+
return eventLog.slice(-limit);
|
|
12587
14748
|
},
|
|
12588
14749
|
|
|
12589
|
-
|
|
12590
|
-
if (!
|
|
12591
|
-
|
|
12592
|
-
|
|
12593
|
-
wu.mount(appName, container);
|
|
12594
|
-
return { mounted: appName, container };
|
|
12595
|
-
}
|
|
12596
|
-
return { error: 'wu.mount not available' };
|
|
12597
|
-
} catch (err) {
|
|
12598
|
-
return { error: err.message };
|
|
12599
|
-
}
|
|
14750
|
+
list_actions() {
|
|
14751
|
+
if (!wu.ai) return { actions: [], note: 'wu.ai not initialized' };
|
|
14752
|
+
const tools = wu.ai.tools();
|
|
14753
|
+
return { actions: tools, count: tools.length };
|
|
12600
14754
|
},
|
|
12601
14755
|
|
|
12602
|
-
|
|
12603
|
-
if (!appName) return { error: 'appName is required' };
|
|
14756
|
+
snapshot({ appName }) {
|
|
12604
14757
|
try {
|
|
12605
|
-
|
|
12606
|
-
wu.
|
|
12607
|
-
|
|
12608
|
-
|
|
12609
|
-
return { error:
|
|
14758
|
+
const target = appName
|
|
14759
|
+
? document.querySelector(`[data-wu-app="${appName}"]`) || document.querySelector(`#wu-app-${appName}`)
|
|
14760
|
+
: document.body;
|
|
14761
|
+
|
|
14762
|
+
if (!target) return { error: `App "${appName}" not found in DOM` };
|
|
14763
|
+
|
|
14764
|
+
return {
|
|
14765
|
+
app: appName || '(page)',
|
|
14766
|
+
snapshot: buildA11yTree(target, 0, 5),
|
|
14767
|
+
timestamp: Date.now(),
|
|
14768
|
+
};
|
|
12610
14769
|
} catch (err) {
|
|
12611
14770
|
return { error: err.message };
|
|
12612
14771
|
}
|
|
12613
14772
|
},
|
|
12614
14773
|
|
|
14774
|
+
console({ level = 'all', limit = 50 }) {
|
|
14775
|
+
return getFilteredConsole(level, limit);
|
|
14776
|
+
},
|
|
14777
|
+
|
|
14778
|
+
async screenshot({ selector, quality = 0.8 }) {
|
|
14779
|
+
const result = await captureScreenshot(selector, quality);
|
|
14780
|
+
if (!result.error) result.timestamp = Date.now();
|
|
14781
|
+
return result;
|
|
14782
|
+
},
|
|
14783
|
+
|
|
14784
|
+
network({ method, status, limit = 50 }) {
|
|
14785
|
+
return getFilteredNetwork(method, status, limit);
|
|
14786
|
+
},
|
|
14787
|
+
|
|
14788
|
+
// ── Permission-gated operations ──
|
|
14789
|
+
|
|
12615
14790
|
get_state({ path }) {
|
|
12616
14791
|
if (!wu.store) return { error: 'wu.store not available' };
|
|
14792
|
+
if (!_checkPermission('readStore')) {
|
|
14793
|
+
return { error: 'Permission denied: readStore is disabled' };
|
|
14794
|
+
}
|
|
12617
14795
|
const value = wu.store.get(path || '');
|
|
12618
14796
|
return { path: path || '(root)', value };
|
|
12619
14797
|
},
|
|
@@ -12621,293 +14799,162 @@ function createMcpBridge(wu) {
|
|
|
12621
14799
|
set_state({ path, value }) {
|
|
12622
14800
|
if (!wu.store) return { error: 'wu.store not available' };
|
|
12623
14801
|
if (!path) return { error: 'path is required' };
|
|
14802
|
+
if (!_checkPermission('writeStore')) {
|
|
14803
|
+
_audit('set_state', { path }, { error: 'Permission denied' });
|
|
14804
|
+
return { error: 'Permission denied: writeStore is disabled' };
|
|
14805
|
+
}
|
|
12624
14806
|
wu.store.set(path, value);
|
|
14807
|
+
_audit('set_state', { path, value }, { });
|
|
12625
14808
|
return { path, value, updated: true };
|
|
12626
14809
|
},
|
|
12627
14810
|
|
|
12628
14811
|
emit_event({ event, data }) {
|
|
12629
14812
|
if (!wu.eventBus) return { error: 'wu.eventBus not available' };
|
|
12630
14813
|
if (!event) return { error: 'event name is required' };
|
|
12631
|
-
|
|
14814
|
+
if (!_checkPermission('emitEvents')) {
|
|
14815
|
+
_audit('emit_event', { event }, { error: 'Permission denied' });
|
|
14816
|
+
return { error: 'Permission denied: emitEvents is disabled' };
|
|
14817
|
+
}
|
|
14818
|
+
wu.eventBus.emit(event, data, { appName: 'wu-mcp-bridge' });
|
|
14819
|
+
_audit('emit_event', { event, data }, { });
|
|
12632
14820
|
return { emitted: event, data };
|
|
12633
14821
|
},
|
|
12634
14822
|
|
|
12635
|
-
|
|
12636
|
-
|
|
12637
|
-
|
|
12638
|
-
|
|
12639
|
-
|
|
12640
|
-
|
|
12641
|
-
|
|
12642
|
-
|
|
12643
|
-
|
|
12644
|
-
|
|
12645
|
-
|
|
12646
|
-
|
|
14823
|
+
navigate({ route }) {
|
|
14824
|
+
if (!route) return { error: 'Route is required' };
|
|
14825
|
+
if (!_checkPermission('emitEvents')) {
|
|
14826
|
+
_audit('navigate', { route }, { error: 'Permission denied: emitEvents' });
|
|
14827
|
+
return { error: 'Permission denied: emitEvents is disabled' };
|
|
14828
|
+
}
|
|
14829
|
+
if (wu.eventBus) {
|
|
14830
|
+
wu.eventBus.emit('shell:navigate', { route }, { appName: 'wu-mcp-bridge' });
|
|
14831
|
+
}
|
|
14832
|
+
if (wu.store && _checkPermission('writeStore')) {
|
|
14833
|
+
wu.store.set('currentPath', route);
|
|
14834
|
+
}
|
|
14835
|
+
_audit('navigate', { route }, { });
|
|
14836
|
+
return { navigated: route };
|
|
12647
14837
|
},
|
|
12648
14838
|
|
|
12649
|
-
|
|
12650
|
-
if (!
|
|
12651
|
-
if (!
|
|
12652
|
-
|
|
14839
|
+
mount_app({ appName, container }) {
|
|
14840
|
+
if (!appName) return { error: 'appName is required' };
|
|
14841
|
+
if (!_checkPermission('modifyDOM')) {
|
|
14842
|
+
_audit('mount_app', { appName }, { error: 'Permission denied' });
|
|
14843
|
+
return { error: 'Permission denied: modifyDOM is disabled' };
|
|
14844
|
+
}
|
|
12653
14845
|
try {
|
|
12654
|
-
|
|
12655
|
-
|
|
12656
|
-
|
|
12657
|
-
|
|
12658
|
-
emit: (e, d) => wu.eventBus?.emit(e, d),
|
|
12659
|
-
setState: (p, v) => wu.store?.set(p, v),
|
|
12660
|
-
getState: (p) => wu.store?.get(p),
|
|
12661
|
-
});
|
|
12662
|
-
return { action, result };
|
|
14846
|
+
if (wu.mount) {
|
|
14847
|
+
wu.mount(appName, container);
|
|
14848
|
+
_audit('mount_app', { appName, container }, { success: true });
|
|
14849
|
+
return { mounted: appName, container };
|
|
12663
14850
|
}
|
|
12664
|
-
return { error:
|
|
14851
|
+
return { error: 'wu.mount not available' };
|
|
12665
14852
|
} catch (err) {
|
|
12666
14853
|
return { error: err.message };
|
|
12667
14854
|
}
|
|
12668
14855
|
},
|
|
12669
14856
|
|
|
12670
|
-
|
|
12671
|
-
|
|
12672
|
-
|
|
12673
|
-
|
|
12674
|
-
|
|
12675
|
-
|
|
12676
|
-
if (!target) return { error: `App "${appName}" not found in DOM` };
|
|
12677
|
-
|
|
12678
|
-
const tree = _buildA11yTree(target, 0, 5);
|
|
12679
|
-
return {
|
|
12680
|
-
app: appName || '(page)',
|
|
12681
|
-
snapshot: tree,
|
|
12682
|
-
timestamp: Date.now(),
|
|
12683
|
-
};
|
|
12684
|
-
} catch (err) {
|
|
12685
|
-
return { error: err.message };
|
|
14857
|
+
unmount_app({ appName }) {
|
|
14858
|
+
if (!appName) return { error: 'appName is required' };
|
|
14859
|
+
if (!_checkPermission('modifyDOM')) {
|
|
14860
|
+
_audit('unmount_app', { appName }, { error: 'Permission denied' });
|
|
14861
|
+
return { error: 'Permission denied: modifyDOM is disabled' };
|
|
12686
14862
|
}
|
|
12687
|
-
},
|
|
12688
|
-
|
|
12689
|
-
console({ level = 'all', limit = 50 }) {
|
|
12690
|
-
const filtered = level === 'all'
|
|
12691
|
-
? consoleLog
|
|
12692
|
-
: consoleLog.filter((m) => m.level === level);
|
|
12693
|
-
return filtered.slice(-limit);
|
|
12694
|
-
},
|
|
12695
|
-
|
|
12696
|
-
async screenshot({ selector, quality = 0.8 }) {
|
|
12697
14863
|
try {
|
|
12698
|
-
|
|
12699
|
-
|
|
12700
|
-
:
|
|
12701
|
-
|
|
12702
|
-
|
|
12703
|
-
|
|
12704
|
-
const rect = target.getBoundingClientRect();
|
|
12705
|
-
const w = Math.ceil(Math.min(rect.width || window.innerWidth, 1920));
|
|
12706
|
-
const h = Math.ceil(Math.min(rect.height || window.innerHeight, 1080));
|
|
12707
|
-
|
|
12708
|
-
// Clone target and inline all computed styles for accurate rendering
|
|
12709
|
-
const clone = target.cloneNode(true);
|
|
12710
|
-
_inlineComputedStyles(target, clone);
|
|
12711
|
-
|
|
12712
|
-
// Serialize to XHTML (required for SVG foreignObject)
|
|
12713
|
-
const serializer = new XMLSerializer();
|
|
12714
|
-
const xhtml = serializer.serializeToString(clone);
|
|
12715
|
-
|
|
12716
|
-
// Build SVG with foreignObject containing the styled DOM
|
|
12717
|
-
const svgStr = [
|
|
12718
|
-
`<svg xmlns="http://www.w3.org/2000/svg" width="${w}" height="${h}">`,
|
|
12719
|
-
'<foreignObject width="100%" height="100%">',
|
|
12720
|
-
`<div xmlns="http://www.w3.org/1999/xhtml" style="width:${w}px;height:${h}px;overflow:hidden;">`,
|
|
12721
|
-
xhtml,
|
|
12722
|
-
'</div>',
|
|
12723
|
-
'</foreignObject>',
|
|
12724
|
-
'</svg>',
|
|
12725
|
-
].join('');
|
|
12726
|
-
|
|
12727
|
-
const svgBlob = new Blob([svgStr], { type: 'image/svg+xml;charset=utf-8' });
|
|
12728
|
-
const url = URL.createObjectURL(svgBlob);
|
|
12729
|
-
|
|
12730
|
-
// Render SVG to Canvas
|
|
12731
|
-
const dataUrl = await new Promise((resolve) => {
|
|
12732
|
-
const img = new Image();
|
|
12733
|
-
img.onload = () => {
|
|
12734
|
-
const canvas = document.createElement('canvas');
|
|
12735
|
-
canvas.width = w;
|
|
12736
|
-
canvas.height = h;
|
|
12737
|
-
const ctx = canvas.getContext('2d');
|
|
12738
|
-
ctx.drawImage(img, 0, 0);
|
|
12739
|
-
URL.revokeObjectURL(url);
|
|
12740
|
-
resolve(canvas.toDataURL('image/png', quality));
|
|
12741
|
-
};
|
|
12742
|
-
img.onerror = () => {
|
|
12743
|
-
URL.revokeObjectURL(url);
|
|
12744
|
-
resolve(null);
|
|
12745
|
-
};
|
|
12746
|
-
img.src = url;
|
|
12747
|
-
});
|
|
12748
|
-
|
|
12749
|
-
if (!dataUrl) return { error: 'Canvas rendering failed' };
|
|
12750
|
-
|
|
12751
|
-
// Return base64 without the data:image/png;base64, prefix
|
|
12752
|
-
const base64 = dataUrl.split(',')[1];
|
|
12753
|
-
return {
|
|
12754
|
-
selector: selector || '(page)',
|
|
12755
|
-
width: w,
|
|
12756
|
-
height: h,
|
|
12757
|
-
format: 'png',
|
|
12758
|
-
base64,
|
|
12759
|
-
sizeKB: Math.round((base64.length * 3) / 4 / 1024),
|
|
12760
|
-
timestamp: Date.now(),
|
|
12761
|
-
};
|
|
14864
|
+
if (wu.unmount) {
|
|
14865
|
+
wu.unmount(appName);
|
|
14866
|
+
_audit('unmount_app', { appName }, { success: true });
|
|
14867
|
+
return { unmounted: appName };
|
|
14868
|
+
}
|
|
14869
|
+
return { error: 'wu.unmount not available' };
|
|
12762
14870
|
} catch (err) {
|
|
12763
14871
|
return { error: err.message };
|
|
12764
14872
|
}
|
|
12765
14873
|
},
|
|
12766
14874
|
|
|
12767
14875
|
click({ selector, text }) {
|
|
12768
|
-
|
|
12769
|
-
|
|
12770
|
-
|
|
12771
|
-
if (selector) {
|
|
12772
|
-
el = document.querySelector(selector);
|
|
12773
|
-
}
|
|
12774
|
-
|
|
12775
|
-
// Fallback: find by visible text content
|
|
12776
|
-
if (!el && text) {
|
|
12777
|
-
const candidates = document.querySelectorAll('button, a, [role="button"], input[type="submit"], [data-click], label');
|
|
12778
|
-
for (const candidate of candidates) {
|
|
12779
|
-
if (candidate.textContent?.trim().toLowerCase().includes(text.toLowerCase())) {
|
|
12780
|
-
el = candidate;
|
|
12781
|
-
break;
|
|
12782
|
-
}
|
|
12783
|
-
}
|
|
12784
|
-
}
|
|
12785
|
-
|
|
12786
|
-
if (!el) return { error: `Element not found: ${selector || `text="${text}"`}` };
|
|
12787
|
-
|
|
12788
|
-
// Scroll into view and click
|
|
12789
|
-
el.scrollIntoView({ behavior: 'instant', block: 'center' });
|
|
12790
|
-
el.click();
|
|
12791
|
-
|
|
12792
|
-
const tag = el.tagName?.toLowerCase();
|
|
12793
|
-
const id = el.id ? `#${el.id}` : '';
|
|
12794
|
-
const cls = el.className && typeof el.className === 'string' ? `.${el.className.split(' ')[0]}` : '';
|
|
12795
|
-
return {
|
|
12796
|
-
clicked: `${tag}${id}${cls}`,
|
|
12797
|
-
text: el.textContent?.trim().slice(0, 80) || '',
|
|
12798
|
-
rect: el.getBoundingClientRect().toJSON(),
|
|
12799
|
-
};
|
|
12800
|
-
} catch (err) {
|
|
12801
|
-
return { error: err.message };
|
|
14876
|
+
if (!_checkPermission('modifyDOM')) {
|
|
14877
|
+
return { error: 'Permission denied: modifyDOM is disabled' };
|
|
12802
14878
|
}
|
|
14879
|
+
const result = clickElement(selector, text);
|
|
14880
|
+
_audit('click', { selector, text }, result);
|
|
14881
|
+
return result;
|
|
12803
14882
|
},
|
|
12804
14883
|
|
|
12805
14884
|
type({ selector, text, clear = false, submit = false }) {
|
|
12806
|
-
|
|
12807
|
-
|
|
12808
|
-
if (text === undefined) return { error: 'text is required' };
|
|
12809
|
-
|
|
12810
|
-
const el = document.querySelector(selector);
|
|
12811
|
-
if (!el) return { error: `Element not found: ${selector}` };
|
|
12812
|
-
|
|
12813
|
-
// Focus the element
|
|
12814
|
-
el.focus();
|
|
12815
|
-
|
|
12816
|
-
// Clear existing value if requested
|
|
12817
|
-
if (clear) {
|
|
12818
|
-
el.value = '';
|
|
12819
|
-
el.dispatchEvent(new Event('input', { bubbles: true }));
|
|
12820
|
-
}
|
|
12821
|
-
|
|
12822
|
-
// Set value and fire events (works with React, Vue, etc.)
|
|
12823
|
-
const nativeInputValueSetter = Object.getOwnPropertyDescriptor(
|
|
12824
|
-
window.HTMLInputElement.prototype, 'value'
|
|
12825
|
-
)?.set || Object.getOwnPropertyDescriptor(
|
|
12826
|
-
window.HTMLTextAreaElement.prototype, 'value'
|
|
12827
|
-
)?.set;
|
|
12828
|
-
|
|
12829
|
-
if (nativeInputValueSetter) {
|
|
12830
|
-
nativeInputValueSetter.call(el, clear ? text : el.value + text);
|
|
12831
|
-
} else {
|
|
12832
|
-
el.value = clear ? text : el.value + text;
|
|
12833
|
-
}
|
|
12834
|
-
|
|
12835
|
-
// Dispatch events that frameworks listen to
|
|
12836
|
-
el.dispatchEvent(new Event('input', { bubbles: true }));
|
|
12837
|
-
el.dispatchEvent(new Event('change', { bubbles: true }));
|
|
12838
|
-
|
|
12839
|
-
// Submit form if requested
|
|
12840
|
-
if (submit) {
|
|
12841
|
-
const form = el.closest('form');
|
|
12842
|
-
if (form) {
|
|
12843
|
-
form.dispatchEvent(new Event('submit', { bubbles: true, cancelable: true }));
|
|
12844
|
-
} else {
|
|
12845
|
-
el.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter', code: 'Enter', bubbles: true }));
|
|
12846
|
-
}
|
|
12847
|
-
}
|
|
12848
|
-
|
|
12849
|
-
return {
|
|
12850
|
-
selector,
|
|
12851
|
-
typed: text,
|
|
12852
|
-
currentValue: el.value?.slice(0, 200),
|
|
12853
|
-
submitted: submit,
|
|
12854
|
-
};
|
|
12855
|
-
} catch (err) {
|
|
12856
|
-
return { error: err.message };
|
|
14885
|
+
if (!_checkPermission('modifyDOM')) {
|
|
14886
|
+
return { error: 'Permission denied: modifyDOM is disabled' };
|
|
12857
14887
|
}
|
|
14888
|
+
const result = typeIntoElement(selector, text, { clear, submit });
|
|
14889
|
+
_audit('type', { selector, textLength: text?.length }, result);
|
|
14890
|
+
return result;
|
|
12858
14891
|
},
|
|
12859
14892
|
|
|
12860
|
-
|
|
12861
|
-
|
|
12862
|
-
if (
|
|
12863
|
-
if (status) {
|
|
12864
|
-
if (status === 'error') {
|
|
12865
|
-
filtered = filtered.filter((r) => r.status === 0 || r.status >= 400);
|
|
12866
|
-
} else {
|
|
12867
|
-
filtered = filtered.filter((r) => String(r.status).startsWith(String(status)));
|
|
12868
|
-
}
|
|
12869
|
-
}
|
|
12870
|
-
return {
|
|
12871
|
-
requests: filtered.slice(-limit),
|
|
12872
|
-
total: networkLog.length,
|
|
12873
|
-
filtered: filtered.length,
|
|
12874
|
-
};
|
|
12875
|
-
},
|
|
14893
|
+
async execute_action({ action, params }) {
|
|
14894
|
+
if (!wu.ai) return { error: 'wu.ai not available' };
|
|
14895
|
+
if (!action) return { error: 'action name is required' };
|
|
12876
14896
|
|
|
12877
|
-
eval({ expression }) {
|
|
12878
14897
|
try {
|
|
12879
|
-
//
|
|
12880
|
-
const result =
|
|
12881
|
-
return {
|
|
12882
|
-
expression,
|
|
12883
|
-
result: typeof result === 'object' ? JSON.stringify(result, null, 2) : String(result),
|
|
12884
|
-
type: typeof result,
|
|
12885
|
-
};
|
|
14898
|
+
// Execute through public API (respects permissions, validation, audit)
|
|
14899
|
+
const result = await wu.ai.execute(action, params || {});
|
|
14900
|
+
return { action, ...result };
|
|
12886
14901
|
} catch (err) {
|
|
12887
|
-
return { error: err.message
|
|
14902
|
+
return { error: err.message };
|
|
12888
14903
|
}
|
|
12889
14904
|
},
|
|
12890
14905
|
};
|
|
12891
14906
|
|
|
12892
14907
|
// ── WebSocket connection ──
|
|
12893
14908
|
|
|
12894
|
-
function connect(url = 'ws://localhost:19100') {
|
|
14909
|
+
function connect(url = 'ws://localhost:19100', options = {}) {
|
|
12895
14910
|
if (ws && ws.readyState <= 1) {
|
|
12896
14911
|
console.warn('[wu-mcp-bridge] Already connected or connecting');
|
|
12897
14912
|
return;
|
|
12898
14913
|
}
|
|
12899
14914
|
|
|
14915
|
+
authToken = options.token || null;
|
|
14916
|
+
authenticated = !authToken; // No token = auto-authenticated (dev mode)
|
|
14917
|
+
|
|
12900
14918
|
try {
|
|
12901
14919
|
ws = new WebSocket(url);
|
|
12902
14920
|
|
|
12903
14921
|
ws.onopen = () => {
|
|
12904
14922
|
console.log('[wu-mcp-bridge] Connected to wu-mcp-server');
|
|
12905
14923
|
reconnectAttempts = 0;
|
|
14924
|
+
|
|
14925
|
+
// Send auth handshake if token provided
|
|
14926
|
+
if (authToken) {
|
|
14927
|
+
ws.send(JSON.stringify({
|
|
14928
|
+
type: 'auth',
|
|
14929
|
+
token: authToken,
|
|
14930
|
+
}));
|
|
14931
|
+
}
|
|
12906
14932
|
};
|
|
12907
14933
|
|
|
12908
14934
|
ws.onmessage = async (event) => {
|
|
12909
14935
|
try {
|
|
12910
14936
|
const msg = JSON.parse(event.data);
|
|
14937
|
+
|
|
14938
|
+
// Handle auth response
|
|
14939
|
+
if (msg.type === 'auth_result') {
|
|
14940
|
+
authenticated = msg.success === true;
|
|
14941
|
+
if (!authenticated) {
|
|
14942
|
+
console.error('[wu-mcp-bridge] Authentication failed:', msg.reason || 'Invalid token');
|
|
14943
|
+
disconnect();
|
|
14944
|
+
} else {
|
|
14945
|
+
console.log('[wu-mcp-bridge] Authenticated successfully');
|
|
14946
|
+
}
|
|
14947
|
+
return;
|
|
14948
|
+
}
|
|
14949
|
+
|
|
14950
|
+
// Reject commands if not authenticated
|
|
14951
|
+
if (!authenticated) {
|
|
14952
|
+
if (msg.id) {
|
|
14953
|
+
_respond(msg.id, null, 'Not authenticated. Send auth token first.');
|
|
14954
|
+
}
|
|
14955
|
+
return;
|
|
14956
|
+
}
|
|
14957
|
+
|
|
12911
14958
|
const { id, command, params } = msg;
|
|
12912
14959
|
|
|
12913
14960
|
if (!id || !command) {
|
|
@@ -12935,7 +14982,8 @@ function createMcpBridge(wu) {
|
|
|
12935
14982
|
ws.onclose = () => {
|
|
12936
14983
|
console.log('[wu-mcp-bridge] Disconnected');
|
|
12937
14984
|
ws = null;
|
|
12938
|
-
|
|
14985
|
+
authenticated = false;
|
|
14986
|
+
_scheduleReconnect(url, options);
|
|
12939
14987
|
};
|
|
12940
14988
|
|
|
12941
14989
|
ws.onerror = () => {
|
|
@@ -12943,7 +14991,7 @@ function createMcpBridge(wu) {
|
|
|
12943
14991
|
};
|
|
12944
14992
|
} catch (err) {
|
|
12945
14993
|
console.error('[wu-mcp-bridge] Connection failed:', err.message);
|
|
12946
|
-
_scheduleReconnect(url);
|
|
14994
|
+
_scheduleReconnect(url, options);
|
|
12947
14995
|
}
|
|
12948
14996
|
}
|
|
12949
14997
|
|
|
@@ -12957,10 +15005,11 @@ function createMcpBridge(wu) {
|
|
|
12957
15005
|
ws.close();
|
|
12958
15006
|
ws = null;
|
|
12959
15007
|
}
|
|
15008
|
+
authenticated = false;
|
|
12960
15009
|
}
|
|
12961
15010
|
|
|
12962
15011
|
function isConnected() {
|
|
12963
|
-
return ws !== null && ws.readyState === 1;
|
|
15012
|
+
return ws !== null && ws.readyState === 1 && authenticated;
|
|
12964
15013
|
}
|
|
12965
15014
|
|
|
12966
15015
|
// ── Private helpers ──
|
|
@@ -12971,15 +15020,14 @@ function createMcpBridge(wu) {
|
|
|
12971
15020
|
ws.send(JSON.stringify(msg));
|
|
12972
15021
|
}
|
|
12973
15022
|
|
|
12974
|
-
function _scheduleReconnect(url) {
|
|
15023
|
+
function _scheduleReconnect(url, options) {
|
|
12975
15024
|
if (reconnectAttempts >= MAX_RECONNECT_ATTEMPTS) return;
|
|
12976
15025
|
reconnectAttempts++;
|
|
12977
15026
|
const delay = RECONNECT_DELAY * Math.min(reconnectAttempts, 5);
|
|
12978
|
-
reconnectTimer = setTimeout(() => connect(url), delay);
|
|
15027
|
+
reconnectTimer = setTimeout(() => connect(url, options), delay);
|
|
12979
15028
|
}
|
|
12980
15029
|
|
|
12981
15030
|
function _getAppList() {
|
|
12982
|
-
// Try to get app info from wu internals
|
|
12983
15031
|
const apps = [];
|
|
12984
15032
|
|
|
12985
15033
|
if (wu._apps) {
|
|
@@ -13007,142 +15055,6 @@ function createMcpBridge(wu) {
|
|
|
13007
15055
|
return apps;
|
|
13008
15056
|
}
|
|
13009
15057
|
|
|
13010
|
-
function _buildA11yTree(el, depth, maxDepth) {
|
|
13011
|
-
if (depth > maxDepth || !el) return '';
|
|
13012
|
-
|
|
13013
|
-
const indent = ' '.repeat(depth);
|
|
13014
|
-
const tag = el.tagName?.toLowerCase() || '';
|
|
13015
|
-
const role = el.getAttribute?.('role') || '';
|
|
13016
|
-
const ariaLabel = el.getAttribute?.('aria-label') || '';
|
|
13017
|
-
const text = el.childNodes?.length === 1 && el.childNodes[0].nodeType === 3
|
|
13018
|
-
? el.textContent?.trim().slice(0, 80) : '';
|
|
13019
|
-
|
|
13020
|
-
let line = `${indent}<${tag}`;
|
|
13021
|
-
if (el.id) line += ` id="${el.id}"`;
|
|
13022
|
-
if (role) line += ` role="${role}"`;
|
|
13023
|
-
if (ariaLabel) line += ` aria-label="${ariaLabel}"`;
|
|
13024
|
-
if (el.className && typeof el.className === 'string') {
|
|
13025
|
-
const cls = el.className.trim().slice(0, 60);
|
|
13026
|
-
if (cls) line += ` class="${cls}"`;
|
|
13027
|
-
}
|
|
13028
|
-
line += '>';
|
|
13029
|
-
if (text) line += ` "${text}"`;
|
|
13030
|
-
|
|
13031
|
-
let result = line + '\n';
|
|
13032
|
-
|
|
13033
|
-
// Traverse into shadow DOM if present
|
|
13034
|
-
const root = el.shadowRoot || el;
|
|
13035
|
-
const children = root.children || [];
|
|
13036
|
-
|
|
13037
|
-
for (let i = 0; i < children.length && i < 50; i++) {
|
|
13038
|
-
result += _buildA11yTree(children[i], depth + 1, maxDepth);
|
|
13039
|
-
}
|
|
13040
|
-
|
|
13041
|
-
return result;
|
|
13042
|
-
}
|
|
13043
|
-
|
|
13044
|
-
function _interceptConsole() {
|
|
13045
|
-
const levels = ['log', 'warn', 'error'];
|
|
13046
|
-
for (const level of levels) {
|
|
13047
|
-
const original = console[level];
|
|
13048
|
-
console[level] = (...args) => {
|
|
13049
|
-
consoleLog.push({
|
|
13050
|
-
level,
|
|
13051
|
-
message: args.map((a) => (typeof a === 'object' ? JSON.stringify(a) : String(a))).join(' '),
|
|
13052
|
-
timestamp: Date.now(),
|
|
13053
|
-
});
|
|
13054
|
-
if (consoleLog.length > MAX_CONSOLE_LOG) consoleLog.shift();
|
|
13055
|
-
original.apply(console, args);
|
|
13056
|
-
};
|
|
13057
|
-
}
|
|
13058
|
-
}
|
|
13059
|
-
|
|
13060
|
-
function _interceptNetwork() {
|
|
13061
|
-
// ── Intercept fetch() ──
|
|
13062
|
-
const originalFetch = window.fetch;
|
|
13063
|
-
window.fetch = async function (...args) {
|
|
13064
|
-
const start = Date.now();
|
|
13065
|
-
const req = args[0];
|
|
13066
|
-
const url = typeof req === 'string' ? req : req?.url || '';
|
|
13067
|
-
const method = args[1]?.method || (req?.method) || 'GET';
|
|
13068
|
-
|
|
13069
|
-
try {
|
|
13070
|
-
const response = await originalFetch.apply(window, args);
|
|
13071
|
-
const size = parseInt(response.headers?.get('content-length') || '0', 10);
|
|
13072
|
-
networkLog.push({
|
|
13073
|
-
type: 'fetch', method: method.toUpperCase(), url,
|
|
13074
|
-
status: response.status, statusText: response.statusText,
|
|
13075
|
-
duration: Date.now() - start, size, timestamp: start,
|
|
13076
|
-
});
|
|
13077
|
-
if (networkLog.length > MAX_NETWORK_LOG) networkLog.shift();
|
|
13078
|
-
return response;
|
|
13079
|
-
} catch (err) {
|
|
13080
|
-
networkLog.push({
|
|
13081
|
-
type: 'fetch', method: method.toUpperCase(), url,
|
|
13082
|
-
status: 0, error: err.message,
|
|
13083
|
-
duration: Date.now() - start, timestamp: start,
|
|
13084
|
-
});
|
|
13085
|
-
if (networkLog.length > MAX_NETWORK_LOG) networkLog.shift();
|
|
13086
|
-
throw err;
|
|
13087
|
-
}
|
|
13088
|
-
};
|
|
13089
|
-
|
|
13090
|
-
// ── Intercept XMLHttpRequest ──
|
|
13091
|
-
const origOpen = XMLHttpRequest.prototype.open;
|
|
13092
|
-
const origSend = XMLHttpRequest.prototype.send;
|
|
13093
|
-
|
|
13094
|
-
XMLHttpRequest.prototype.open = function (method, url, ...rest) {
|
|
13095
|
-
this._wuMcp = { method: (method || 'GET').toUpperCase(), url: String(url), start: null };
|
|
13096
|
-
return origOpen.call(this, method, url, ...rest);
|
|
13097
|
-
};
|
|
13098
|
-
|
|
13099
|
-
XMLHttpRequest.prototype.send = function (...args) {
|
|
13100
|
-
if (this._wuMcp) {
|
|
13101
|
-
this._wuMcp.start = Date.now();
|
|
13102
|
-
this.addEventListener('loadend', () => {
|
|
13103
|
-
networkLog.push({
|
|
13104
|
-
type: 'xhr',
|
|
13105
|
-
method: this._wuMcp.method,
|
|
13106
|
-
url: this._wuMcp.url,
|
|
13107
|
-
status: this.status,
|
|
13108
|
-
statusText: this.statusText,
|
|
13109
|
-
duration: Date.now() - this._wuMcp.start,
|
|
13110
|
-
size: parseInt(this.getResponseHeader('content-length') || '0', 10),
|
|
13111
|
-
timestamp: this._wuMcp.start,
|
|
13112
|
-
});
|
|
13113
|
-
if (networkLog.length > MAX_NETWORK_LOG) networkLog.shift();
|
|
13114
|
-
});
|
|
13115
|
-
}
|
|
13116
|
-
return origSend.apply(this, args);
|
|
13117
|
-
};
|
|
13118
|
-
}
|
|
13119
|
-
|
|
13120
|
-
function _inlineComputedStyles(source, clone) {
|
|
13121
|
-
// Copy computed styles from source to clone for accurate Canvas rendering
|
|
13122
|
-
const sourceStyle = window.getComputedStyle(source);
|
|
13123
|
-
const important = ['color', 'background', 'background-color', 'font-family',
|
|
13124
|
-
'font-size', 'font-weight', 'border', 'border-radius', 'padding', 'margin',
|
|
13125
|
-
'display', 'flex-direction', 'align-items', 'justify-content', 'gap',
|
|
13126
|
-
'width', 'height', 'max-width', 'max-height', 'overflow', 'opacity',
|
|
13127
|
-
'box-shadow', 'text-align', 'line-height', 'position', 'top', 'left',
|
|
13128
|
-
'right', 'bottom', 'z-index', 'transform', 'visibility'];
|
|
13129
|
-
|
|
13130
|
-
for (const prop of important) {
|
|
13131
|
-
try {
|
|
13132
|
-
const val = sourceStyle.getPropertyValue(prop);
|
|
13133
|
-
if (val) clone.style?.setProperty(prop, val);
|
|
13134
|
-
} catch (_) { /* skip */ }
|
|
13135
|
-
}
|
|
13136
|
-
|
|
13137
|
-
// Recurse into children (limit depth for performance)
|
|
13138
|
-
const sourceChildren = source.children || [];
|
|
13139
|
-
const cloneChildren = clone.children || [];
|
|
13140
|
-
const max = Math.min(sourceChildren.length, cloneChildren.length, 200);
|
|
13141
|
-
for (let i = 0; i < max; i++) {
|
|
13142
|
-
_inlineComputedStyles(sourceChildren[i], cloneChildren[i]);
|
|
13143
|
-
}
|
|
13144
|
-
}
|
|
13145
|
-
|
|
13146
15058
|
return { connect, disconnect, isConnected };
|
|
13147
15059
|
}
|
|
13148
15060
|
|
|
@@ -13241,12 +15153,12 @@ if (typeof window !== 'undefined') {
|
|
|
13241
15153
|
if (!wu.mcp) {
|
|
13242
15154
|
let _mcpBridge = null;
|
|
13243
15155
|
wu.mcp = {
|
|
13244
|
-
async connect(url = 'ws://localhost:19100') {
|
|
15156
|
+
async connect(url = 'ws://localhost:19100', options = {}) {
|
|
13245
15157
|
if (!_mcpBridge) {
|
|
13246
15158
|
const { createMcpBridge } = await Promise.resolve().then(function () { return wuMcpBridge; });
|
|
13247
15159
|
_mcpBridge = createMcpBridge(wu);
|
|
13248
15160
|
}
|
|
13249
|
-
_mcpBridge.connect(url);
|
|
15161
|
+
_mcpBridge.connect(url, options);
|
|
13250
15162
|
},
|
|
13251
15163
|
disconnect() {
|
|
13252
15164
|
_mcpBridge?.disconnect();
|
|
@@ -13302,5 +15214,5 @@ const clearOverrides = () => wu.clearOverrides();
|
|
|
13302
15214
|
const usePlugin = (plugin, opts) => wu.pluginSystem.use(plugin, opts);
|
|
13303
15215
|
const useHook = (phase, middleware, opts) => wu.hooks.use(phase, middleware, opts);
|
|
13304
15216
|
|
|
13305
|
-
export { WuAI, WuAIActions, WuAIContext, WuAIConversation, WuAIPermissions, WuAIProvider, WuAITriggers, WuApp, WuCache, WuCore, WuErrorBoundary, WuEventBus, WuHtmlParser, WuIframeSandbox, WuLifecycleHooks, WuLoader, WuLoadingStrategy, WuManifest, WuOverrides, WuPerformance, WuPluginSystem, WuPrefetch, WuProxySandbox, WuSandbox, WuScriptExecutor, WuSnapshotSandbox, WuStore, WuStyleBridge, app, clearOverrides, createConditionalHook, createGuardHook, createMcpBridge, createPlugin, createSimpleHook, createTimedHook, createTransformHook, wu_default as default, define, destroy, emit, enableAllLogs, endMeasure, generatePerformanceReport, getOverrides, getState, hide, init, isHidden, mount, off, on, onStateChange, once, override, prefetch, prefetchAll, registerBrowserActions, removeOverride, setState, show, silenceAllLogs, startMeasure, store, unmount, useHook, usePlugin, wu };
|
|
15217
|
+
export { WuAI, WuAIActions, WuAIAgent, WuAIContext, WuAIConversation, WuAIOrchestrate, WuAIPermissions, WuAIProvider, WuAITriggers, WuApp, WuCache, WuCore, WuErrorBoundary, WuEventBus, WuHtmlParser, WuIframeSandbox, WuLifecycleHooks, WuLoader, WuLoadingStrategy, WuManifest, WuOverrides, WuPerformance, WuPluginSystem, WuPrefetch, WuProxySandbox, WuSandbox, WuScriptExecutor, WuSnapshotSandbox, WuStore, WuStyleBridge, app, buildA11yTree, buildToolSchemas, captureScreenshot, clearOverrides, clickElement, createConditionalHook, createGuardHook, createMcpBridge, createPlugin, createSimpleHook, createTimedHook, createTransformHook, wu_default as default, define, destroy, emit, enableAllLogs, endMeasure, ensureInterceptors, estimateTokens, generatePerformanceReport, getFilteredConsole, getFilteredNetwork, getOverrides, getState, hide, init, interpolate, isHidden, mount, normalizeParameters, off, on, onStateChange, once, override, prefetch, prefetchAll, redactSensitive, registerBrowserActions, removeOverride, sanitizeForPrompt, setState, show, silenceAllLogs, startMeasure, store, truncateToTokenBudget, typeIntoElement, unmount, useHook, usePlugin, validateParams, wu };
|
|
13306
15218
|
//# sourceMappingURL=wu-framework.dev.js.map
|