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
|
@@ -5,14 +5,32 @@
|
|
|
5
5
|
* commands using wu.* APIs. This is the "eyes and hands" of
|
|
6
6
|
* the MCP server inside the browser.
|
|
7
7
|
*
|
|
8
|
+
* Security:
|
|
9
|
+
* - Optional auth token sent on first message (handshake)
|
|
10
|
+
* - All state/event/mount operations check wu.ai permissions
|
|
11
|
+
* - Mutating operations emit audit events
|
|
12
|
+
* - Read-only operations (status, list_apps, snapshot, console, network) are unrestricted
|
|
13
|
+
*
|
|
8
14
|
* @example
|
|
9
|
-
* //
|
|
10
|
-
*
|
|
15
|
+
* // Connect with auth token
|
|
16
|
+
* wu.mcp.connect('ws://localhost:19100', { token: 'my-secret' });
|
|
11
17
|
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
18
|
+
* // Connect without auth (development only)
|
|
19
|
+
* wu.mcp.connect();
|
|
14
20
|
*/
|
|
15
21
|
|
|
22
|
+
import {
|
|
23
|
+
ensureInterceptors,
|
|
24
|
+
networkLog,
|
|
25
|
+
consoleLog,
|
|
26
|
+
captureScreenshot,
|
|
27
|
+
buildA11yTree,
|
|
28
|
+
clickElement,
|
|
29
|
+
typeIntoElement,
|
|
30
|
+
getFilteredNetwork,
|
|
31
|
+
getFilteredConsole,
|
|
32
|
+
} from '../ai/wu-ai-browser-primitives.js';
|
|
33
|
+
|
|
16
34
|
/**
|
|
17
35
|
* Create the MCP bridge for a Wu instance.
|
|
18
36
|
*
|
|
@@ -23,6 +41,8 @@ export function createMcpBridge(wu) {
|
|
|
23
41
|
let ws = null;
|
|
24
42
|
let reconnectTimer = null;
|
|
25
43
|
let reconnectAttempts = 0;
|
|
44
|
+
let authenticated = false;
|
|
45
|
+
let authToken = null;
|
|
26
46
|
const MAX_RECONNECT_ATTEMPTS = 10;
|
|
27
47
|
const RECONNECT_DELAY = 2000;
|
|
28
48
|
|
|
@@ -30,14 +50,6 @@ export function createMcpBridge(wu) {
|
|
|
30
50
|
const eventLog = [];
|
|
31
51
|
const MAX_EVENT_LOG = 200;
|
|
32
52
|
|
|
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
53
|
// Capture events for history
|
|
42
54
|
if (wu.eventBus) {
|
|
43
55
|
wu.eventBus.on('*', (event) => {
|
|
@@ -51,22 +63,50 @@ export function createMcpBridge(wu) {
|
|
|
51
63
|
});
|
|
52
64
|
}
|
|
53
65
|
|
|
54
|
-
//
|
|
55
|
-
|
|
66
|
+
// Install shared interceptors (idempotent — safe if wu-ai-browser already did it)
|
|
67
|
+
ensureInterceptors();
|
|
56
68
|
|
|
57
|
-
//
|
|
58
|
-
|
|
69
|
+
// ── Permission helpers ──
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Check a permission flag via wu.ai.permissions if available.
|
|
73
|
+
* Falls back to deny if wu.ai is not initialized.
|
|
74
|
+
*/
|
|
75
|
+
function _checkPermission(perm) {
|
|
76
|
+
if (wu.ai && wu.ai.permissions) {
|
|
77
|
+
return wu.ai.permissions.check(perm);
|
|
78
|
+
}
|
|
79
|
+
// If AI module not initialized, deny write operations, allow reads
|
|
80
|
+
const readPerms = ['readStore', 'executeActions'];
|
|
81
|
+
return readPerms.includes(perm);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Emit an audit event for bridge operations.
|
|
86
|
+
*/
|
|
87
|
+
function _audit(operation, params, result) {
|
|
88
|
+
if (wu.eventBus) {
|
|
89
|
+
wu.eventBus.emit('mcp:bridge:operation', {
|
|
90
|
+
operation,
|
|
91
|
+
params,
|
|
92
|
+
result: result?.error ? { error: result.error } : { success: true },
|
|
93
|
+
timestamp: Date.now(),
|
|
94
|
+
}, { appName: 'wu-mcp-bridge' });
|
|
95
|
+
}
|
|
96
|
+
}
|
|
59
97
|
|
|
60
98
|
// ── Command handlers ──
|
|
61
99
|
|
|
62
100
|
const handlers = {
|
|
101
|
+
// ── Read-only operations (no permission gates) ──
|
|
102
|
+
|
|
63
103
|
status() {
|
|
64
104
|
return {
|
|
65
105
|
connected: true,
|
|
66
106
|
framework: 'wu-framework',
|
|
67
107
|
apps: _getAppList(),
|
|
68
108
|
storeKeys: wu.store ? Object.keys(wu.store.get('') || {}) : [],
|
|
69
|
-
actionsCount: wu.ai
|
|
109
|
+
actionsCount: wu.ai ? wu.ai.tools().length : 0,
|
|
70
110
|
eventLogSize: eventLog.length,
|
|
71
111
|
};
|
|
72
112
|
},
|
|
@@ -75,45 +115,55 @@ export function createMcpBridge(wu) {
|
|
|
75
115
|
return _getAppList();
|
|
76
116
|
},
|
|
77
117
|
|
|
78
|
-
|
|
79
|
-
|
|
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 };
|
|
118
|
+
list_events({ limit = 20 }) {
|
|
119
|
+
return eventLog.slice(-limit);
|
|
87
120
|
},
|
|
88
121
|
|
|
89
|
-
|
|
90
|
-
if (!
|
|
91
|
-
|
|
92
|
-
|
|
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
|
-
}
|
|
122
|
+
list_actions() {
|
|
123
|
+
if (!wu.ai) return { actions: [], note: 'wu.ai not initialized' };
|
|
124
|
+
const tools = wu.ai.tools();
|
|
125
|
+
return { actions: tools, count: tools.length };
|
|
100
126
|
},
|
|
101
127
|
|
|
102
|
-
|
|
103
|
-
if (!appName) return { error: 'appName is required' };
|
|
128
|
+
snapshot({ appName }) {
|
|
104
129
|
try {
|
|
105
|
-
|
|
106
|
-
wu.
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
return { error:
|
|
130
|
+
const target = appName
|
|
131
|
+
? document.querySelector(`[data-wu-app="${appName}"]`) || document.querySelector(`#wu-app-${appName}`)
|
|
132
|
+
: document.body;
|
|
133
|
+
|
|
134
|
+
if (!target) return { error: `App "${appName}" not found in DOM` };
|
|
135
|
+
|
|
136
|
+
return {
|
|
137
|
+
app: appName || '(page)',
|
|
138
|
+
snapshot: buildA11yTree(target, 0, 5),
|
|
139
|
+
timestamp: Date.now(),
|
|
140
|
+
};
|
|
110
141
|
} catch (err) {
|
|
111
142
|
return { error: err.message };
|
|
112
143
|
}
|
|
113
144
|
},
|
|
114
145
|
|
|
146
|
+
console({ level = 'all', limit = 50 }) {
|
|
147
|
+
return getFilteredConsole(level, limit);
|
|
148
|
+
},
|
|
149
|
+
|
|
150
|
+
async screenshot({ selector, quality = 0.8 }) {
|
|
151
|
+
const result = await captureScreenshot(selector, quality);
|
|
152
|
+
if (!result.error) result.timestamp = Date.now();
|
|
153
|
+
return result;
|
|
154
|
+
},
|
|
155
|
+
|
|
156
|
+
network({ method, status, limit = 50 }) {
|
|
157
|
+
return getFilteredNetwork(method, status, limit);
|
|
158
|
+
},
|
|
159
|
+
|
|
160
|
+
// ── Permission-gated operations ──
|
|
161
|
+
|
|
115
162
|
get_state({ path }) {
|
|
116
163
|
if (!wu.store) return { error: 'wu.store not available' };
|
|
164
|
+
if (!_checkPermission('readStore')) {
|
|
165
|
+
return { error: 'Permission denied: readStore is disabled' };
|
|
166
|
+
}
|
|
117
167
|
const value = wu.store.get(path || '');
|
|
118
168
|
return { path: path || '(root)', value };
|
|
119
169
|
},
|
|
@@ -121,293 +171,162 @@ export function createMcpBridge(wu) {
|
|
|
121
171
|
set_state({ path, value }) {
|
|
122
172
|
if (!wu.store) return { error: 'wu.store not available' };
|
|
123
173
|
if (!path) return { error: 'path is required' };
|
|
174
|
+
if (!_checkPermission('writeStore')) {
|
|
175
|
+
_audit('set_state', { path }, { error: 'Permission denied' });
|
|
176
|
+
return { error: 'Permission denied: writeStore is disabled' };
|
|
177
|
+
}
|
|
124
178
|
wu.store.set(path, value);
|
|
179
|
+
_audit('set_state', { path, value }, { success: true });
|
|
125
180
|
return { path, value, updated: true };
|
|
126
181
|
},
|
|
127
182
|
|
|
128
183
|
emit_event({ event, data }) {
|
|
129
184
|
if (!wu.eventBus) return { error: 'wu.eventBus not available' };
|
|
130
185
|
if (!event) return { error: 'event name is required' };
|
|
131
|
-
|
|
186
|
+
if (!_checkPermission('emitEvents')) {
|
|
187
|
+
_audit('emit_event', { event }, { error: 'Permission denied' });
|
|
188
|
+
return { error: 'Permission denied: emitEvents is disabled' };
|
|
189
|
+
}
|
|
190
|
+
wu.eventBus.emit(event, data, { appName: 'wu-mcp-bridge' });
|
|
191
|
+
_audit('emit_event', { event, data }, { success: true });
|
|
132
192
|
return { emitted: event, data };
|
|
133
193
|
},
|
|
134
194
|
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
195
|
+
navigate({ route }) {
|
|
196
|
+
if (!route) return { error: 'Route is required' };
|
|
197
|
+
if (!_checkPermission('emitEvents')) {
|
|
198
|
+
_audit('navigate', { route }, { error: 'Permission denied: emitEvents' });
|
|
199
|
+
return { error: 'Permission denied: emitEvents is disabled' };
|
|
200
|
+
}
|
|
201
|
+
if (wu.eventBus) {
|
|
202
|
+
wu.eventBus.emit('shell:navigate', { route }, { appName: 'wu-mcp-bridge' });
|
|
203
|
+
}
|
|
204
|
+
if (wu.store && _checkPermission('writeStore')) {
|
|
205
|
+
wu.store.set('currentPath', route);
|
|
206
|
+
}
|
|
207
|
+
_audit('navigate', { route }, { success: true });
|
|
208
|
+
return { navigated: route };
|
|
147
209
|
},
|
|
148
210
|
|
|
149
|
-
|
|
150
|
-
if (!
|
|
151
|
-
if (!
|
|
152
|
-
|
|
211
|
+
mount_app({ appName, container }) {
|
|
212
|
+
if (!appName) return { error: 'appName is required' };
|
|
213
|
+
if (!_checkPermission('modifyDOM')) {
|
|
214
|
+
_audit('mount_app', { appName }, { error: 'Permission denied' });
|
|
215
|
+
return { error: 'Permission denied: modifyDOM is disabled' };
|
|
216
|
+
}
|
|
153
217
|
try {
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
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 };
|
|
218
|
+
if (wu.mount) {
|
|
219
|
+
wu.mount(appName, container);
|
|
220
|
+
_audit('mount_app', { appName, container }, { success: true });
|
|
221
|
+
return { mounted: appName, container };
|
|
163
222
|
}
|
|
164
|
-
return { error:
|
|
223
|
+
return { error: 'wu.mount not available' };
|
|
165
224
|
} catch (err) {
|
|
166
225
|
return { error: err.message };
|
|
167
226
|
}
|
|
168
227
|
},
|
|
169
228
|
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
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 };
|
|
229
|
+
unmount_app({ appName }) {
|
|
230
|
+
if (!appName) return { error: 'appName is required' };
|
|
231
|
+
if (!_checkPermission('modifyDOM')) {
|
|
232
|
+
_audit('unmount_app', { appName }, { error: 'Permission denied' });
|
|
233
|
+
return { error: 'Permission denied: modifyDOM is disabled' };
|
|
186
234
|
}
|
|
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
235
|
try {
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
:
|
|
201
|
-
|
|
202
|
-
|
|
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
|
-
};
|
|
236
|
+
if (wu.unmount) {
|
|
237
|
+
wu.unmount(appName);
|
|
238
|
+
_audit('unmount_app', { appName }, { success: true });
|
|
239
|
+
return { unmounted: appName };
|
|
240
|
+
}
|
|
241
|
+
return { error: 'wu.unmount not available' };
|
|
262
242
|
} catch (err) {
|
|
263
243
|
return { error: err.message };
|
|
264
244
|
}
|
|
265
245
|
},
|
|
266
246
|
|
|
267
247
|
click({ selector, text }) {
|
|
268
|
-
|
|
269
|
-
|
|
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 };
|
|
248
|
+
if (!_checkPermission('modifyDOM')) {
|
|
249
|
+
return { error: 'Permission denied: modifyDOM is disabled' };
|
|
302
250
|
}
|
|
251
|
+
const result = clickElement(selector, text);
|
|
252
|
+
_audit('click', { selector, text }, result);
|
|
253
|
+
return result;
|
|
303
254
|
},
|
|
304
255
|
|
|
305
256
|
type({ selector, text, clear = false, submit = false }) {
|
|
306
|
-
|
|
307
|
-
|
|
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 };
|
|
257
|
+
if (!_checkPermission('modifyDOM')) {
|
|
258
|
+
return { error: 'Permission denied: modifyDOM is disabled' };
|
|
357
259
|
}
|
|
260
|
+
const result = typeIntoElement(selector, text, { clear, submit });
|
|
261
|
+
_audit('type', { selector, textLength: text?.length }, result);
|
|
262
|
+
return result;
|
|
358
263
|
},
|
|
359
264
|
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
if (
|
|
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
|
-
},
|
|
265
|
+
async execute_action({ action, params }) {
|
|
266
|
+
if (!wu.ai) return { error: 'wu.ai not available' };
|
|
267
|
+
if (!action) return { error: 'action name is required' };
|
|
376
268
|
|
|
377
|
-
eval({ expression }) {
|
|
378
269
|
try {
|
|
379
|
-
//
|
|
380
|
-
const result =
|
|
381
|
-
return {
|
|
382
|
-
expression,
|
|
383
|
-
result: typeof result === 'object' ? JSON.stringify(result, null, 2) : String(result),
|
|
384
|
-
type: typeof result,
|
|
385
|
-
};
|
|
270
|
+
// Execute through public API (respects permissions, validation, audit)
|
|
271
|
+
const result = await wu.ai.execute(action, params || {});
|
|
272
|
+
return { action, ...result };
|
|
386
273
|
} catch (err) {
|
|
387
|
-
return { error: err.message
|
|
274
|
+
return { error: err.message };
|
|
388
275
|
}
|
|
389
276
|
},
|
|
390
277
|
};
|
|
391
278
|
|
|
392
279
|
// ── WebSocket connection ──
|
|
393
280
|
|
|
394
|
-
function connect(url = 'ws://localhost:19100') {
|
|
281
|
+
function connect(url = 'ws://localhost:19100', options = {}) {
|
|
395
282
|
if (ws && ws.readyState <= 1) {
|
|
396
283
|
console.warn('[wu-mcp-bridge] Already connected or connecting');
|
|
397
284
|
return;
|
|
398
285
|
}
|
|
399
286
|
|
|
287
|
+
authToken = options.token || null;
|
|
288
|
+
authenticated = !authToken; // No token = auto-authenticated (dev mode)
|
|
289
|
+
|
|
400
290
|
try {
|
|
401
291
|
ws = new WebSocket(url);
|
|
402
292
|
|
|
403
293
|
ws.onopen = () => {
|
|
404
294
|
console.log('[wu-mcp-bridge] Connected to wu-mcp-server');
|
|
405
295
|
reconnectAttempts = 0;
|
|
296
|
+
|
|
297
|
+
// Send auth handshake if token provided
|
|
298
|
+
if (authToken) {
|
|
299
|
+
ws.send(JSON.stringify({
|
|
300
|
+
type: 'auth',
|
|
301
|
+
token: authToken,
|
|
302
|
+
}));
|
|
303
|
+
}
|
|
406
304
|
};
|
|
407
305
|
|
|
408
306
|
ws.onmessage = async (event) => {
|
|
409
307
|
try {
|
|
410
308
|
const msg = JSON.parse(event.data);
|
|
309
|
+
|
|
310
|
+
// Handle auth response
|
|
311
|
+
if (msg.type === 'auth_result') {
|
|
312
|
+
authenticated = msg.success === true;
|
|
313
|
+
if (!authenticated) {
|
|
314
|
+
console.error('[wu-mcp-bridge] Authentication failed:', msg.reason || 'Invalid token');
|
|
315
|
+
disconnect();
|
|
316
|
+
} else {
|
|
317
|
+
console.log('[wu-mcp-bridge] Authenticated successfully');
|
|
318
|
+
}
|
|
319
|
+
return;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
// Reject commands if not authenticated
|
|
323
|
+
if (!authenticated) {
|
|
324
|
+
if (msg.id) {
|
|
325
|
+
_respond(msg.id, null, 'Not authenticated. Send auth token first.');
|
|
326
|
+
}
|
|
327
|
+
return;
|
|
328
|
+
}
|
|
329
|
+
|
|
411
330
|
const { id, command, params } = msg;
|
|
412
331
|
|
|
413
332
|
if (!id || !command) {
|
|
@@ -435,7 +354,8 @@ export function createMcpBridge(wu) {
|
|
|
435
354
|
ws.onclose = () => {
|
|
436
355
|
console.log('[wu-mcp-bridge] Disconnected');
|
|
437
356
|
ws = null;
|
|
438
|
-
|
|
357
|
+
authenticated = false;
|
|
358
|
+
_scheduleReconnect(url, options);
|
|
439
359
|
};
|
|
440
360
|
|
|
441
361
|
ws.onerror = () => {
|
|
@@ -443,7 +363,7 @@ export function createMcpBridge(wu) {
|
|
|
443
363
|
};
|
|
444
364
|
} catch (err) {
|
|
445
365
|
console.error('[wu-mcp-bridge] Connection failed:', err.message);
|
|
446
|
-
_scheduleReconnect(url);
|
|
366
|
+
_scheduleReconnect(url, options);
|
|
447
367
|
}
|
|
448
368
|
}
|
|
449
369
|
|
|
@@ -457,10 +377,11 @@ export function createMcpBridge(wu) {
|
|
|
457
377
|
ws.close();
|
|
458
378
|
ws = null;
|
|
459
379
|
}
|
|
380
|
+
authenticated = false;
|
|
460
381
|
}
|
|
461
382
|
|
|
462
383
|
function isConnected() {
|
|
463
|
-
return ws !== null && ws.readyState === 1;
|
|
384
|
+
return ws !== null && ws.readyState === 1 && authenticated;
|
|
464
385
|
}
|
|
465
386
|
|
|
466
387
|
// ── Private helpers ──
|
|
@@ -471,15 +392,14 @@ export function createMcpBridge(wu) {
|
|
|
471
392
|
ws.send(JSON.stringify(msg));
|
|
472
393
|
}
|
|
473
394
|
|
|
474
|
-
function _scheduleReconnect(url) {
|
|
395
|
+
function _scheduleReconnect(url, options) {
|
|
475
396
|
if (reconnectAttempts >= MAX_RECONNECT_ATTEMPTS) return;
|
|
476
397
|
reconnectAttempts++;
|
|
477
398
|
const delay = RECONNECT_DELAY * Math.min(reconnectAttempts, 5);
|
|
478
|
-
reconnectTimer = setTimeout(() => connect(url), delay);
|
|
399
|
+
reconnectTimer = setTimeout(() => connect(url, options), delay);
|
|
479
400
|
}
|
|
480
401
|
|
|
481
402
|
function _getAppList() {
|
|
482
|
-
// Try to get app info from wu internals
|
|
483
403
|
const apps = [];
|
|
484
404
|
|
|
485
405
|
if (wu._apps) {
|
|
@@ -507,141 +427,5 @@ export function createMcpBridge(wu) {
|
|
|
507
427
|
return apps;
|
|
508
428
|
}
|
|
509
429
|
|
|
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
430
|
return { connect, disconnect, isConnected };
|
|
647
431
|
}
|