wu-framework 1.1.6 → 1.1.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +511 -977
- package/dist/wu-framework.cjs.js +3 -1
- package/dist/wu-framework.cjs.js.map +1 -0
- package/dist/wu-framework.dev.js +7533 -2761
- package/dist/wu-framework.dev.js.map +1 -1
- package/dist/wu-framework.esm.js +3 -0
- package/dist/wu-framework.esm.js.map +1 -0
- package/dist/wu-framework.umd.js +3 -1
- package/dist/wu-framework.umd.js.map +1 -0
- package/integrations/astro/README.md +127 -0
- package/integrations/astro/WuApp.astro +63 -0
- package/integrations/astro/WuShell.astro +39 -0
- package/integrations/astro/index.js +68 -0
- package/integrations/astro/package.json +38 -0
- package/integrations/astro/types.d.ts +53 -0
- package/package.json +94 -74
- package/src/adapters/angular/ai.js +30 -0
- package/src/adapters/angular/index.d.ts +154 -0
- package/src/adapters/angular/index.js +932 -0
- package/src/adapters/angular.d.ts +3 -154
- package/src/adapters/angular.js +3 -813
- package/src/adapters/index.js +35 -24
- package/src/adapters/lit/ai.js +20 -0
- package/src/adapters/lit/index.d.ts +120 -0
- package/src/adapters/lit/index.js +721 -0
- package/src/adapters/lit.d.ts +3 -120
- package/src/adapters/lit.js +3 -726
- package/src/adapters/preact/ai.js +33 -0
- package/src/adapters/preact/index.d.ts +108 -0
- package/src/adapters/preact/index.js +661 -0
- package/src/adapters/preact.d.ts +3 -108
- package/src/adapters/preact.js +3 -665
- package/src/adapters/react/ai.js +135 -0
- package/src/adapters/react/index.d.ts +246 -0
- package/src/adapters/react/index.js +689 -0
- package/src/adapters/react.d.ts +3 -212
- package/src/adapters/react.js +3 -513
- package/src/adapters/shared.js +64 -0
- package/src/adapters/solid/ai.js +32 -0
- package/src/adapters/solid/index.d.ts +101 -0
- package/src/adapters/solid/index.js +586 -0
- package/src/adapters/solid.d.ts +3 -101
- package/src/adapters/solid.js +3 -591
- package/src/adapters/svelte/ai.js +31 -0
- package/src/adapters/svelte/index.d.ts +166 -0
- package/src/adapters/svelte/index.js +798 -0
- package/src/adapters/svelte.d.ts +3 -166
- package/src/adapters/svelte.js +3 -803
- package/src/adapters/vanilla/ai.js +30 -0
- package/src/adapters/vanilla/index.d.ts +179 -0
- package/src/adapters/vanilla/index.js +785 -0
- package/src/adapters/vanilla.d.ts +3 -179
- package/src/adapters/vanilla.js +3 -791
- package/src/adapters/vue/ai.js +52 -0
- package/src/adapters/vue/index.d.ts +299 -0
- package/src/adapters/vue/index.js +608 -0
- package/src/adapters/vue.d.ts +3 -299
- package/src/adapters/vue.js +3 -611
- package/src/ai/wu-ai-actions.js +261 -0
- package/src/ai/wu-ai-browser.js +663 -0
- package/src/ai/wu-ai-context.js +332 -0
- package/src/ai/wu-ai-conversation.js +554 -0
- package/src/ai/wu-ai-permissions.js +381 -0
- package/src/ai/wu-ai-provider.js +605 -0
- package/src/ai/wu-ai-schema.js +225 -0
- package/src/ai/wu-ai-triggers.js +396 -0
- package/src/ai/wu-ai.js +474 -0
- package/src/core/wu-app.js +50 -8
- package/src/core/wu-cache.js +1 -1
- package/src/core/wu-core.js +645 -677
- package/src/core/wu-html-parser.js +121 -211
- package/src/core/wu-iframe-sandbox.js +328 -0
- package/src/core/wu-mcp-bridge.js +647 -0
- package/src/core/wu-overrides.js +510 -0
- package/src/core/wu-prefetch.js +414 -0
- package/src/core/wu-proxy-sandbox.js +398 -75
- package/src/core/wu-sandbox.js +86 -268
- package/src/core/wu-script-executor.js +79 -182
- package/src/core/wu-snapshot-sandbox.js +149 -106
- package/src/core/wu-strategies.js +13 -0
- package/src/core/wu-style-bridge.js +0 -2
- package/src/index.js +139 -665
- package/dist/wu-framework.hex.js +0 -23
- package/dist/wu-framework.min.js +0 -1
- package/dist/wu-framework.obf.js +0 -1
- package/scripts/build-protected.js +0 -366
- package/scripts/build.js +0 -212
- package/scripts/rollup-plugin-hex.js +0 -143
- package/src/core/wu-registry.js +0 -60
- package/src/core/wu-sandbox-pool.js +0 -390
|
@@ -0,0 +1,647 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WU-MCP Bridge (Browser Side)
|
|
3
|
+
*
|
|
4
|
+
* Connects to the wu-mcp-server via WebSocket and executes
|
|
5
|
+
* commands using wu.* APIs. This is the "eyes and hands" of
|
|
6
|
+
* the MCP server inside the browser.
|
|
7
|
+
*
|
|
8
|
+
* @example
|
|
9
|
+
* // Auto-connect (called by wu.mcp.connect())
|
|
10
|
+
* import { createMcpBridge } from './wu-mcp-bridge.js';
|
|
11
|
+
*
|
|
12
|
+
* const bridge = createMcpBridge(wuInstance);
|
|
13
|
+
* bridge.connect('ws://localhost:3100');
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Create the MCP bridge for a Wu instance.
|
|
18
|
+
*
|
|
19
|
+
* @param {object} wu - The Wu Framework instance (window.wu)
|
|
20
|
+
* @returns {object} Bridge API: { connect, disconnect, isConnected }
|
|
21
|
+
*/
|
|
22
|
+
export function createMcpBridge(wu) {
|
|
23
|
+
let ws = null;
|
|
24
|
+
let reconnectTimer = null;
|
|
25
|
+
let reconnectAttempts = 0;
|
|
26
|
+
const MAX_RECONNECT_ATTEMPTS = 10;
|
|
27
|
+
const RECONNECT_DELAY = 2000;
|
|
28
|
+
|
|
29
|
+
// Event log for wu_list_events
|
|
30
|
+
const eventLog = [];
|
|
31
|
+
const MAX_EVENT_LOG = 200;
|
|
32
|
+
|
|
33
|
+
// Console log capture
|
|
34
|
+
const consoleLog = [];
|
|
35
|
+
const MAX_CONSOLE_LOG = 500;
|
|
36
|
+
|
|
37
|
+
// Network log capture
|
|
38
|
+
const networkLog = [];
|
|
39
|
+
const MAX_NETWORK_LOG = 300;
|
|
40
|
+
|
|
41
|
+
// Capture events for history
|
|
42
|
+
if (wu.eventBus) {
|
|
43
|
+
wu.eventBus.on('*', (event) => {
|
|
44
|
+
eventLog.push({
|
|
45
|
+
name: event.name,
|
|
46
|
+
data: event.data,
|
|
47
|
+
timestamp: event.timestamp || Date.now(),
|
|
48
|
+
source: event.source || 'unknown',
|
|
49
|
+
});
|
|
50
|
+
if (eventLog.length > MAX_EVENT_LOG) eventLog.shift();
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Capture console messages
|
|
55
|
+
_interceptConsole();
|
|
56
|
+
|
|
57
|
+
// Capture network requests (fetch + XMLHttpRequest)
|
|
58
|
+
_interceptNetwork();
|
|
59
|
+
|
|
60
|
+
// ── Command handlers ──
|
|
61
|
+
|
|
62
|
+
const handlers = {
|
|
63
|
+
status() {
|
|
64
|
+
return {
|
|
65
|
+
connected: true,
|
|
66
|
+
framework: 'wu-framework',
|
|
67
|
+
apps: _getAppList(),
|
|
68
|
+
storeKeys: wu.store ? Object.keys(wu.store.get('') || {}) : [],
|
|
69
|
+
actionsCount: wu.ai?._actions ? Object.keys(wu.ai._actions).length : 0,
|
|
70
|
+
eventLogSize: eventLog.length,
|
|
71
|
+
};
|
|
72
|
+
},
|
|
73
|
+
|
|
74
|
+
list_apps() {
|
|
75
|
+
return _getAppList();
|
|
76
|
+
},
|
|
77
|
+
|
|
78
|
+
navigate({ route }) {
|
|
79
|
+
if (!route) return { error: 'Route is required' };
|
|
80
|
+
if (wu.eventBus) {
|
|
81
|
+
wu.eventBus.emit('shell:navigate', { route });
|
|
82
|
+
}
|
|
83
|
+
if (wu.store) {
|
|
84
|
+
wu.store.set('currentPath', route);
|
|
85
|
+
}
|
|
86
|
+
return { navigated: route };
|
|
87
|
+
},
|
|
88
|
+
|
|
89
|
+
mount_app({ appName, container }) {
|
|
90
|
+
if (!appName) return { error: 'appName is required' };
|
|
91
|
+
try {
|
|
92
|
+
if (wu.mount) {
|
|
93
|
+
wu.mount(appName, container);
|
|
94
|
+
return { mounted: appName, container };
|
|
95
|
+
}
|
|
96
|
+
return { error: 'wu.mount not available' };
|
|
97
|
+
} catch (err) {
|
|
98
|
+
return { error: err.message };
|
|
99
|
+
}
|
|
100
|
+
},
|
|
101
|
+
|
|
102
|
+
unmount_app({ appName }) {
|
|
103
|
+
if (!appName) return { error: 'appName is required' };
|
|
104
|
+
try {
|
|
105
|
+
if (wu.unmount) {
|
|
106
|
+
wu.unmount(appName);
|
|
107
|
+
return { unmounted: appName };
|
|
108
|
+
}
|
|
109
|
+
return { error: 'wu.unmount not available' };
|
|
110
|
+
} catch (err) {
|
|
111
|
+
return { error: err.message };
|
|
112
|
+
}
|
|
113
|
+
},
|
|
114
|
+
|
|
115
|
+
get_state({ path }) {
|
|
116
|
+
if (!wu.store) return { error: 'wu.store not available' };
|
|
117
|
+
const value = wu.store.get(path || '');
|
|
118
|
+
return { path: path || '(root)', value };
|
|
119
|
+
},
|
|
120
|
+
|
|
121
|
+
set_state({ path, value }) {
|
|
122
|
+
if (!wu.store) return { error: 'wu.store not available' };
|
|
123
|
+
if (!path) return { error: 'path is required' };
|
|
124
|
+
wu.store.set(path, value);
|
|
125
|
+
return { path, value, updated: true };
|
|
126
|
+
},
|
|
127
|
+
|
|
128
|
+
emit_event({ event, data }) {
|
|
129
|
+
if (!wu.eventBus) return { error: 'wu.eventBus not available' };
|
|
130
|
+
if (!event) return { error: 'event name is required' };
|
|
131
|
+
wu.eventBus.emit(event, data);
|
|
132
|
+
return { emitted: event, data };
|
|
133
|
+
},
|
|
134
|
+
|
|
135
|
+
list_events({ limit = 20 }) {
|
|
136
|
+
return eventLog.slice(-limit);
|
|
137
|
+
},
|
|
138
|
+
|
|
139
|
+
list_actions() {
|
|
140
|
+
if (!wu.ai?._actions) return { actions: [], note: 'wu.ai not initialized or no actions registered' };
|
|
141
|
+
const actions = Object.entries(wu.ai._actions).map(([name, def]) => ({
|
|
142
|
+
name,
|
|
143
|
+
description: def.description || '',
|
|
144
|
+
parameters: def.parameters ? Object.keys(def.parameters) : [],
|
|
145
|
+
}));
|
|
146
|
+
return { actions, count: actions.length };
|
|
147
|
+
},
|
|
148
|
+
|
|
149
|
+
async execute_action({ action, params }) {
|
|
150
|
+
if (!wu.ai) return { error: 'wu.ai not available' };
|
|
151
|
+
if (!action) return { error: 'action name is required' };
|
|
152
|
+
|
|
153
|
+
try {
|
|
154
|
+
// Try to execute via wu.ai action system
|
|
155
|
+
if (wu.ai._actions && wu.ai._actions[action]) {
|
|
156
|
+
const handler = wu.ai._actions[action].handler;
|
|
157
|
+
const result = await handler(params || {}, {
|
|
158
|
+
emit: (e, d) => wu.eventBus?.emit(e, d),
|
|
159
|
+
setState: (p, v) => wu.store?.set(p, v),
|
|
160
|
+
getState: (p) => wu.store?.get(p),
|
|
161
|
+
});
|
|
162
|
+
return { action, result };
|
|
163
|
+
}
|
|
164
|
+
return { error: `Action "${action}" not found` };
|
|
165
|
+
} catch (err) {
|
|
166
|
+
return { error: err.message };
|
|
167
|
+
}
|
|
168
|
+
},
|
|
169
|
+
|
|
170
|
+
snapshot({ appName }) {
|
|
171
|
+
try {
|
|
172
|
+
const target = appName
|
|
173
|
+
? document.querySelector(`[data-wu-app="${appName}"]`) || document.querySelector(`#wu-app-${appName}`)
|
|
174
|
+
: document.body;
|
|
175
|
+
|
|
176
|
+
if (!target) return { error: `App "${appName}" not found in DOM` };
|
|
177
|
+
|
|
178
|
+
const tree = _buildA11yTree(target, 0, 5);
|
|
179
|
+
return {
|
|
180
|
+
app: appName || '(page)',
|
|
181
|
+
snapshot: tree,
|
|
182
|
+
timestamp: Date.now(),
|
|
183
|
+
};
|
|
184
|
+
} catch (err) {
|
|
185
|
+
return { error: err.message };
|
|
186
|
+
}
|
|
187
|
+
},
|
|
188
|
+
|
|
189
|
+
console({ level = 'all', limit = 50 }) {
|
|
190
|
+
const filtered = level === 'all'
|
|
191
|
+
? consoleLog
|
|
192
|
+
: consoleLog.filter((m) => m.level === level);
|
|
193
|
+
return filtered.slice(-limit);
|
|
194
|
+
},
|
|
195
|
+
|
|
196
|
+
async screenshot({ selector, quality = 0.8 }) {
|
|
197
|
+
try {
|
|
198
|
+
const target = selector
|
|
199
|
+
? document.querySelector(selector)
|
|
200
|
+
: document.documentElement;
|
|
201
|
+
|
|
202
|
+
if (!target) return { error: `Element not found: ${selector}` };
|
|
203
|
+
|
|
204
|
+
const rect = target.getBoundingClientRect();
|
|
205
|
+
const w = Math.ceil(Math.min(rect.width || window.innerWidth, 1920));
|
|
206
|
+
const h = Math.ceil(Math.min(rect.height || window.innerHeight, 1080));
|
|
207
|
+
|
|
208
|
+
// Clone target and inline all computed styles for accurate rendering
|
|
209
|
+
const clone = target.cloneNode(true);
|
|
210
|
+
_inlineComputedStyles(target, clone);
|
|
211
|
+
|
|
212
|
+
// Serialize to XHTML (required for SVG foreignObject)
|
|
213
|
+
const serializer = new XMLSerializer();
|
|
214
|
+
const xhtml = serializer.serializeToString(clone);
|
|
215
|
+
|
|
216
|
+
// Build SVG with foreignObject containing the styled DOM
|
|
217
|
+
const svgStr = [
|
|
218
|
+
`<svg xmlns="http://www.w3.org/2000/svg" width="${w}" height="${h}">`,
|
|
219
|
+
'<foreignObject width="100%" height="100%">',
|
|
220
|
+
`<div xmlns="http://www.w3.org/1999/xhtml" style="width:${w}px;height:${h}px;overflow:hidden;">`,
|
|
221
|
+
xhtml,
|
|
222
|
+
'</div>',
|
|
223
|
+
'</foreignObject>',
|
|
224
|
+
'</svg>',
|
|
225
|
+
].join('');
|
|
226
|
+
|
|
227
|
+
const svgBlob = new Blob([svgStr], { type: 'image/svg+xml;charset=utf-8' });
|
|
228
|
+
const url = URL.createObjectURL(svgBlob);
|
|
229
|
+
|
|
230
|
+
// Render SVG to Canvas
|
|
231
|
+
const dataUrl = await new Promise((resolve) => {
|
|
232
|
+
const img = new Image();
|
|
233
|
+
img.onload = () => {
|
|
234
|
+
const canvas = document.createElement('canvas');
|
|
235
|
+
canvas.width = w;
|
|
236
|
+
canvas.height = h;
|
|
237
|
+
const ctx = canvas.getContext('2d');
|
|
238
|
+
ctx.drawImage(img, 0, 0);
|
|
239
|
+
URL.revokeObjectURL(url);
|
|
240
|
+
resolve(canvas.toDataURL('image/png', quality));
|
|
241
|
+
};
|
|
242
|
+
img.onerror = () => {
|
|
243
|
+
URL.revokeObjectURL(url);
|
|
244
|
+
resolve(null);
|
|
245
|
+
};
|
|
246
|
+
img.src = url;
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
if (!dataUrl) return { error: 'Canvas rendering failed' };
|
|
250
|
+
|
|
251
|
+
// Return base64 without the data:image/png;base64, prefix
|
|
252
|
+
const base64 = dataUrl.split(',')[1];
|
|
253
|
+
return {
|
|
254
|
+
selector: selector || '(page)',
|
|
255
|
+
width: w,
|
|
256
|
+
height: h,
|
|
257
|
+
format: 'png',
|
|
258
|
+
base64,
|
|
259
|
+
sizeKB: Math.round((base64.length * 3) / 4 / 1024),
|
|
260
|
+
timestamp: Date.now(),
|
|
261
|
+
};
|
|
262
|
+
} catch (err) {
|
|
263
|
+
return { error: err.message };
|
|
264
|
+
}
|
|
265
|
+
},
|
|
266
|
+
|
|
267
|
+
click({ selector, text }) {
|
|
268
|
+
try {
|
|
269
|
+
let el = null;
|
|
270
|
+
|
|
271
|
+
if (selector) {
|
|
272
|
+
el = document.querySelector(selector);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// Fallback: find by visible text content
|
|
276
|
+
if (!el && text) {
|
|
277
|
+
const candidates = document.querySelectorAll('button, a, [role="button"], input[type="submit"], [data-click], label');
|
|
278
|
+
for (const candidate of candidates) {
|
|
279
|
+
if (candidate.textContent?.trim().toLowerCase().includes(text.toLowerCase())) {
|
|
280
|
+
el = candidate;
|
|
281
|
+
break;
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
if (!el) return { error: `Element not found: ${selector || `text="${text}"`}` };
|
|
287
|
+
|
|
288
|
+
// Scroll into view and click
|
|
289
|
+
el.scrollIntoView({ behavior: 'instant', block: 'center' });
|
|
290
|
+
el.click();
|
|
291
|
+
|
|
292
|
+
const tag = el.tagName?.toLowerCase();
|
|
293
|
+
const id = el.id ? `#${el.id}` : '';
|
|
294
|
+
const cls = el.className && typeof el.className === 'string' ? `.${el.className.split(' ')[0]}` : '';
|
|
295
|
+
return {
|
|
296
|
+
clicked: `${tag}${id}${cls}`,
|
|
297
|
+
text: el.textContent?.trim().slice(0, 80) || '',
|
|
298
|
+
rect: el.getBoundingClientRect().toJSON(),
|
|
299
|
+
};
|
|
300
|
+
} catch (err) {
|
|
301
|
+
return { error: err.message };
|
|
302
|
+
}
|
|
303
|
+
},
|
|
304
|
+
|
|
305
|
+
type({ selector, text, clear = false, submit = false }) {
|
|
306
|
+
try {
|
|
307
|
+
if (!selector) return { error: 'selector is required' };
|
|
308
|
+
if (text === undefined) return { error: 'text is required' };
|
|
309
|
+
|
|
310
|
+
const el = document.querySelector(selector);
|
|
311
|
+
if (!el) return { error: `Element not found: ${selector}` };
|
|
312
|
+
|
|
313
|
+
// Focus the element
|
|
314
|
+
el.focus();
|
|
315
|
+
|
|
316
|
+
// Clear existing value if requested
|
|
317
|
+
if (clear) {
|
|
318
|
+
el.value = '';
|
|
319
|
+
el.dispatchEvent(new Event('input', { bubbles: true }));
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
// Set value and fire events (works with React, Vue, etc.)
|
|
323
|
+
const nativeInputValueSetter = Object.getOwnPropertyDescriptor(
|
|
324
|
+
window.HTMLInputElement.prototype, 'value'
|
|
325
|
+
)?.set || Object.getOwnPropertyDescriptor(
|
|
326
|
+
window.HTMLTextAreaElement.prototype, 'value'
|
|
327
|
+
)?.set;
|
|
328
|
+
|
|
329
|
+
if (nativeInputValueSetter) {
|
|
330
|
+
nativeInputValueSetter.call(el, clear ? text : el.value + text);
|
|
331
|
+
} else {
|
|
332
|
+
el.value = clear ? text : el.value + text;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
// Dispatch events that frameworks listen to
|
|
336
|
+
el.dispatchEvent(new Event('input', { bubbles: true }));
|
|
337
|
+
el.dispatchEvent(new Event('change', { bubbles: true }));
|
|
338
|
+
|
|
339
|
+
// Submit form if requested
|
|
340
|
+
if (submit) {
|
|
341
|
+
const form = el.closest('form');
|
|
342
|
+
if (form) {
|
|
343
|
+
form.dispatchEvent(new Event('submit', { bubbles: true, cancelable: true }));
|
|
344
|
+
} else {
|
|
345
|
+
el.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter', code: 'Enter', bubbles: true }));
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
return {
|
|
350
|
+
selector,
|
|
351
|
+
typed: text,
|
|
352
|
+
currentValue: el.value?.slice(0, 200),
|
|
353
|
+
submitted: submit,
|
|
354
|
+
};
|
|
355
|
+
} catch (err) {
|
|
356
|
+
return { error: err.message };
|
|
357
|
+
}
|
|
358
|
+
},
|
|
359
|
+
|
|
360
|
+
network({ method, status, limit = 50 }) {
|
|
361
|
+
let filtered = networkLog;
|
|
362
|
+
if (method) filtered = filtered.filter((r) => r.method.toUpperCase() === method.toUpperCase());
|
|
363
|
+
if (status) {
|
|
364
|
+
if (status === 'error') {
|
|
365
|
+
filtered = filtered.filter((r) => r.status === 0 || r.status >= 400);
|
|
366
|
+
} else {
|
|
367
|
+
filtered = filtered.filter((r) => String(r.status).startsWith(String(status)));
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
return {
|
|
371
|
+
requests: filtered.slice(-limit),
|
|
372
|
+
total: networkLog.length,
|
|
373
|
+
filtered: filtered.length,
|
|
374
|
+
};
|
|
375
|
+
},
|
|
376
|
+
|
|
377
|
+
eval({ expression }) {
|
|
378
|
+
try {
|
|
379
|
+
// eslint-disable-next-line no-eval
|
|
380
|
+
const result = eval(expression);
|
|
381
|
+
return {
|
|
382
|
+
expression,
|
|
383
|
+
result: typeof result === 'object' ? JSON.stringify(result, null, 2) : String(result),
|
|
384
|
+
type: typeof result,
|
|
385
|
+
};
|
|
386
|
+
} catch (err) {
|
|
387
|
+
return { error: err.message, expression };
|
|
388
|
+
}
|
|
389
|
+
},
|
|
390
|
+
};
|
|
391
|
+
|
|
392
|
+
// ── WebSocket connection ──
|
|
393
|
+
|
|
394
|
+
function connect(url = 'ws://localhost:19100') {
|
|
395
|
+
if (ws && ws.readyState <= 1) {
|
|
396
|
+
console.warn('[wu-mcp-bridge] Already connected or connecting');
|
|
397
|
+
return;
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
try {
|
|
401
|
+
ws = new WebSocket(url);
|
|
402
|
+
|
|
403
|
+
ws.onopen = () => {
|
|
404
|
+
console.log('[wu-mcp-bridge] Connected to wu-mcp-server');
|
|
405
|
+
reconnectAttempts = 0;
|
|
406
|
+
};
|
|
407
|
+
|
|
408
|
+
ws.onmessage = async (event) => {
|
|
409
|
+
try {
|
|
410
|
+
const msg = JSON.parse(event.data);
|
|
411
|
+
const { id, command, params } = msg;
|
|
412
|
+
|
|
413
|
+
if (!id || !command) {
|
|
414
|
+
console.warn('[wu-mcp-bridge] Invalid message:', msg);
|
|
415
|
+
return;
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
const handler = handlers[command];
|
|
419
|
+
if (!handler) {
|
|
420
|
+
_respond(id, null, `Unknown command: ${command}`);
|
|
421
|
+
return;
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
try {
|
|
425
|
+
const result = await handler(params || {});
|
|
426
|
+
_respond(id, result);
|
|
427
|
+
} catch (err) {
|
|
428
|
+
_respond(id, null, err.message);
|
|
429
|
+
}
|
|
430
|
+
} catch (err) {
|
|
431
|
+
console.error('[wu-mcp-bridge] Failed to handle message:', err);
|
|
432
|
+
}
|
|
433
|
+
};
|
|
434
|
+
|
|
435
|
+
ws.onclose = () => {
|
|
436
|
+
console.log('[wu-mcp-bridge] Disconnected');
|
|
437
|
+
ws = null;
|
|
438
|
+
_scheduleReconnect(url);
|
|
439
|
+
};
|
|
440
|
+
|
|
441
|
+
ws.onerror = () => {
|
|
442
|
+
// onclose will fire after this
|
|
443
|
+
};
|
|
444
|
+
} catch (err) {
|
|
445
|
+
console.error('[wu-mcp-bridge] Connection failed:', err.message);
|
|
446
|
+
_scheduleReconnect(url);
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
function disconnect() {
|
|
451
|
+
if (reconnectTimer) {
|
|
452
|
+
clearTimeout(reconnectTimer);
|
|
453
|
+
reconnectTimer = null;
|
|
454
|
+
}
|
|
455
|
+
reconnectAttempts = MAX_RECONNECT_ATTEMPTS; // prevent reconnect
|
|
456
|
+
if (ws) {
|
|
457
|
+
ws.close();
|
|
458
|
+
ws = null;
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
function isConnected() {
|
|
463
|
+
return ws !== null && ws.readyState === 1;
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
// ── Private helpers ──
|
|
467
|
+
|
|
468
|
+
function _respond(id, result, error) {
|
|
469
|
+
if (!ws || ws.readyState !== 1) return;
|
|
470
|
+
const msg = error ? { id, error } : { id, result };
|
|
471
|
+
ws.send(JSON.stringify(msg));
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
function _scheduleReconnect(url) {
|
|
475
|
+
if (reconnectAttempts >= MAX_RECONNECT_ATTEMPTS) return;
|
|
476
|
+
reconnectAttempts++;
|
|
477
|
+
const delay = RECONNECT_DELAY * Math.min(reconnectAttempts, 5);
|
|
478
|
+
reconnectTimer = setTimeout(() => connect(url), delay);
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
function _getAppList() {
|
|
482
|
+
// Try to get app info from wu internals
|
|
483
|
+
const apps = [];
|
|
484
|
+
|
|
485
|
+
if (wu._apps) {
|
|
486
|
+
for (const [name, app] of Object.entries(wu._apps)) {
|
|
487
|
+
apps.push({
|
|
488
|
+
name,
|
|
489
|
+
mounted: app.mounted || app.isMounted || false,
|
|
490
|
+
url: app.url || app.info?.url || '',
|
|
491
|
+
status: app.status || app.info?.status || 'unknown',
|
|
492
|
+
});
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
// Fallback: scan DOM for wu-app elements
|
|
497
|
+
if (apps.length === 0) {
|
|
498
|
+
document.querySelectorAll('[data-wu-app]').forEach((el) => {
|
|
499
|
+
apps.push({
|
|
500
|
+
name: el.getAttribute('data-wu-app'),
|
|
501
|
+
mounted: true,
|
|
502
|
+
container: `#${el.id || '(no-id)'}`,
|
|
503
|
+
});
|
|
504
|
+
});
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
return apps;
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
function _buildA11yTree(el, depth, maxDepth) {
|
|
511
|
+
if (depth > maxDepth || !el) return '';
|
|
512
|
+
|
|
513
|
+
const indent = ' '.repeat(depth);
|
|
514
|
+
const tag = el.tagName?.toLowerCase() || '';
|
|
515
|
+
const role = el.getAttribute?.('role') || '';
|
|
516
|
+
const ariaLabel = el.getAttribute?.('aria-label') || '';
|
|
517
|
+
const text = el.childNodes?.length === 1 && el.childNodes[0].nodeType === 3
|
|
518
|
+
? el.textContent?.trim().slice(0, 80) : '';
|
|
519
|
+
|
|
520
|
+
let line = `${indent}<${tag}`;
|
|
521
|
+
if (el.id) line += ` id="${el.id}"`;
|
|
522
|
+
if (role) line += ` role="${role}"`;
|
|
523
|
+
if (ariaLabel) line += ` aria-label="${ariaLabel}"`;
|
|
524
|
+
if (el.className && typeof el.className === 'string') {
|
|
525
|
+
const cls = el.className.trim().slice(0, 60);
|
|
526
|
+
if (cls) line += ` class="${cls}"`;
|
|
527
|
+
}
|
|
528
|
+
line += '>';
|
|
529
|
+
if (text) line += ` "${text}"`;
|
|
530
|
+
|
|
531
|
+
let result = line + '\n';
|
|
532
|
+
|
|
533
|
+
// Traverse into shadow DOM if present
|
|
534
|
+
const root = el.shadowRoot || el;
|
|
535
|
+
const children = root.children || [];
|
|
536
|
+
|
|
537
|
+
for (let i = 0; i < children.length && i < 50; i++) {
|
|
538
|
+
result += _buildA11yTree(children[i], depth + 1, maxDepth);
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
return result;
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
function _interceptConsole() {
|
|
545
|
+
const levels = ['log', 'warn', 'error'];
|
|
546
|
+
for (const level of levels) {
|
|
547
|
+
const original = console[level];
|
|
548
|
+
console[level] = (...args) => {
|
|
549
|
+
consoleLog.push({
|
|
550
|
+
level,
|
|
551
|
+
message: args.map((a) => (typeof a === 'object' ? JSON.stringify(a) : String(a))).join(' '),
|
|
552
|
+
timestamp: Date.now(),
|
|
553
|
+
});
|
|
554
|
+
if (consoleLog.length > MAX_CONSOLE_LOG) consoleLog.shift();
|
|
555
|
+
original.apply(console, args);
|
|
556
|
+
};
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
function _interceptNetwork() {
|
|
561
|
+
// ── Intercept fetch() ──
|
|
562
|
+
const originalFetch = window.fetch;
|
|
563
|
+
window.fetch = async function (...args) {
|
|
564
|
+
const start = Date.now();
|
|
565
|
+
const req = args[0];
|
|
566
|
+
const url = typeof req === 'string' ? req : req?.url || '';
|
|
567
|
+
const method = args[1]?.method || (req?.method) || 'GET';
|
|
568
|
+
|
|
569
|
+
try {
|
|
570
|
+
const response = await originalFetch.apply(window, args);
|
|
571
|
+
const size = parseInt(response.headers?.get('content-length') || '0', 10);
|
|
572
|
+
networkLog.push({
|
|
573
|
+
type: 'fetch', method: method.toUpperCase(), url,
|
|
574
|
+
status: response.status, statusText: response.statusText,
|
|
575
|
+
duration: Date.now() - start, size, timestamp: start,
|
|
576
|
+
});
|
|
577
|
+
if (networkLog.length > MAX_NETWORK_LOG) networkLog.shift();
|
|
578
|
+
return response;
|
|
579
|
+
} catch (err) {
|
|
580
|
+
networkLog.push({
|
|
581
|
+
type: 'fetch', method: method.toUpperCase(), url,
|
|
582
|
+
status: 0, error: err.message,
|
|
583
|
+
duration: Date.now() - start, timestamp: start,
|
|
584
|
+
});
|
|
585
|
+
if (networkLog.length > MAX_NETWORK_LOG) networkLog.shift();
|
|
586
|
+
throw err;
|
|
587
|
+
}
|
|
588
|
+
};
|
|
589
|
+
|
|
590
|
+
// ── Intercept XMLHttpRequest ──
|
|
591
|
+
const origOpen = XMLHttpRequest.prototype.open;
|
|
592
|
+
const origSend = XMLHttpRequest.prototype.send;
|
|
593
|
+
|
|
594
|
+
XMLHttpRequest.prototype.open = function (method, url, ...rest) {
|
|
595
|
+
this._wuMcp = { method: (method || 'GET').toUpperCase(), url: String(url), start: null };
|
|
596
|
+
return origOpen.call(this, method, url, ...rest);
|
|
597
|
+
};
|
|
598
|
+
|
|
599
|
+
XMLHttpRequest.prototype.send = function (...args) {
|
|
600
|
+
if (this._wuMcp) {
|
|
601
|
+
this._wuMcp.start = Date.now();
|
|
602
|
+
this.addEventListener('loadend', () => {
|
|
603
|
+
networkLog.push({
|
|
604
|
+
type: 'xhr',
|
|
605
|
+
method: this._wuMcp.method,
|
|
606
|
+
url: this._wuMcp.url,
|
|
607
|
+
status: this.status,
|
|
608
|
+
statusText: this.statusText,
|
|
609
|
+
duration: Date.now() - this._wuMcp.start,
|
|
610
|
+
size: parseInt(this.getResponseHeader('content-length') || '0', 10),
|
|
611
|
+
timestamp: this._wuMcp.start,
|
|
612
|
+
});
|
|
613
|
+
if (networkLog.length > MAX_NETWORK_LOG) networkLog.shift();
|
|
614
|
+
});
|
|
615
|
+
}
|
|
616
|
+
return origSend.apply(this, args);
|
|
617
|
+
};
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
function _inlineComputedStyles(source, clone) {
|
|
621
|
+
// Copy computed styles from source to clone for accurate Canvas rendering
|
|
622
|
+
const sourceStyle = window.getComputedStyle(source);
|
|
623
|
+
const important = ['color', 'background', 'background-color', 'font-family',
|
|
624
|
+
'font-size', 'font-weight', 'border', 'border-radius', 'padding', 'margin',
|
|
625
|
+
'display', 'flex-direction', 'align-items', 'justify-content', 'gap',
|
|
626
|
+
'width', 'height', 'max-width', 'max-height', 'overflow', 'opacity',
|
|
627
|
+
'box-shadow', 'text-align', 'line-height', 'position', 'top', 'left',
|
|
628
|
+
'right', 'bottom', 'z-index', 'transform', 'visibility'];
|
|
629
|
+
|
|
630
|
+
for (const prop of important) {
|
|
631
|
+
try {
|
|
632
|
+
const val = sourceStyle.getPropertyValue(prop);
|
|
633
|
+
if (val) clone.style?.setProperty(prop, val);
|
|
634
|
+
} catch (_) { /* skip */ }
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
// Recurse into children (limit depth for performance)
|
|
638
|
+
const sourceChildren = source.children || [];
|
|
639
|
+
const cloneChildren = clone.children || [];
|
|
640
|
+
const max = Math.min(sourceChildren.length, cloneChildren.length, 200);
|
|
641
|
+
for (let i = 0; i < max; i++) {
|
|
642
|
+
_inlineComputedStyles(sourceChildren[i], cloneChildren[i]);
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
return { connect, disconnect, isConnected };
|
|
647
|
+
}
|