@dimina-kit/devtools 0.4.0-dev.20260702175315 → 0.4.0-dev.20260703051110
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/main/app/app.js +5 -25
- package/dist/main/app/native-overview.d.ts +27 -0
- package/dist/main/app/native-overview.js +44 -0
- package/dist/main/index.bundle.js +826 -690
- package/dist/main/ipc/bridge-router.js +180 -141
- package/dist/main/ipc/project-fs.js +39 -21
- package/dist/main/services/automation/handlers/app.js +143 -109
- package/dist/main/services/mcp/tools/simulator-tools.js +118 -97
- package/dist/main/services/network-forward/dispatch-batch.d.ts +22 -0
- package/dist/main/services/network-forward/dispatch-batch.js +21 -0
- package/dist/main/services/network-forward/index.js +5 -20
- package/dist/main/services/projects/create-project-service.js +54 -34
- package/dist/main/services/projects/types.d.ts +1 -32
- package/dist/main/services/service-console/console-api.js +36 -27
- package/dist/main/services/simulator-temp-files/disk.js +109 -86
- package/dist/main/services/workbench-coi-server.js +58 -45
- package/dist/main/utils/logger.d.ts +5 -12
- package/dist/main/utils/logger.js +5 -45
- package/dist/preload/instrumentation/wxml-extract.js +94 -62
- package/dist/preload/runtime/main-api-runner.d.ts +1 -1
- package/dist/preload/windows/main.cjs +284 -565
- package/dist/preload/windows/main.cjs.map +2 -2
- package/dist/preload/windows/simulator.cjs +37 -39
- package/dist/preload/windows/simulator.cjs.map +2 -2
- package/dist/preload/windows/simulator.js +37 -39
- package/dist/render-host/render-inspect.js +63 -52
- package/dist/renderer/assets/index-D-ksyN1p.js +49 -0
- package/dist/renderer/assets/workbenchSettings-COhuFF-i.js +8 -0
- package/dist/renderer/entries/main/index.html +1 -1
- package/dist/renderer/entries/workbench-settings/index.html +1 -1
- package/dist/shared/appdata-accumulator.js +53 -48
- package/dist/shared/open-in-editor-resource-path.d.ts +56 -0
- package/dist/shared/open-in-editor-resource-path.js +133 -0
- package/dist/shared/open-in-editor.d.ts +2 -9
- package/dist/shared/open-in-editor.js +8 -93
- package/dist/shared/types.d.ts +13 -0
- package/dist/simulator/assets/{device-shell-CUl0ILfn.js → device-shell-CiLAPa0I.js} +2 -2
- package/dist/simulator/assets/simulator-Dvnxry3y.js +10 -0
- package/dist/simulator/simulator.html +1 -1
- package/dist/vscode-workbench/assets/__vite-browser-external-CXi6Kz9W.js +1 -0
- package/dist/vscode-workbench/assets/{dist-CS7SQP3K.js → dist-TpGpmMGi.js} +3 -3
- package/dist/vscode-workbench/assets/{iconv-lite-umd-D3q-1-o6.js → iconv-lite-umd-C9tiCAJa.js} +1 -1
- package/dist/vscode-workbench/assets/{index-zjigpZ25.js → index-DJ1HyMZ7.js} +20 -22
- package/dist/vscode-workbench/assets/{jschardet-Cu4ufeHq.js → jschardet-DE1jV513.js} +1 -1
- package/dist/vscode-workbench/index.html +1 -1
- package/package.json +5 -5
- package/dist/renderer/assets/index-B-Dqb7G0.js +0 -49
- package/dist/renderer/assets/workbenchSettings-Bim9ol9R.js +0 -8
- package/dist/simulator/assets/simulator-Dz8iGcgy.js +0 -10
- package/dist/vscode-workbench/assets/__vite-browser-external-B0DTNerG.js +0 -1
|
@@ -76,122 +76,141 @@ appHandlers['App.getPageStack'] = async (ctx) => {
|
|
|
76
76
|
}
|
|
77
77
|
return { pageStack };
|
|
78
78
|
};
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
const
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
//
|
|
85
|
-
//
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
79
|
+
/** Nav (incl. navigateBack) on the native-host service-host `wx`, waiting for the page to actually change. */
|
|
80
|
+
async function runNativeHostNav(bridge, serviceWc, method, args) {
|
|
81
|
+
const arg = method === 'navigateBack'
|
|
82
|
+
? (args[0] ?? { delta: 1 })
|
|
83
|
+
: (args[0] ?? {});
|
|
84
|
+
// Capture the active page BEFORE navigating so we can wait for it to
|
|
85
|
+
// actually change, instead of blindly sleeping a fixed duration.
|
|
86
|
+
const since = bridge.getActiveBridgeId();
|
|
87
|
+
await serviceWc.executeJavaScript(`wx.${method}(${JSON.stringify(arg)})`);
|
|
88
|
+
// Wait for the bridge's `activePage` signal (the new page mounted), with a
|
|
89
|
+
// timeout floor = the old blind-wait duration. Resolves as soon as the new
|
|
90
|
+
// page is active — far faster than the fixed sleep for the common case —
|
|
91
|
+
// and still bounded for edges (e.g. switchTab back to an already-mounted
|
|
92
|
+
// tab keeps the same bridgeId, so it falls through to the floor, same as
|
|
93
|
+
// the old behaviour). See wait-active-page.ts.
|
|
94
|
+
const timeoutMs = method === 'navigateBack' ? 1500 : 2000;
|
|
95
|
+
await waitForActivePage(bridge, {
|
|
96
|
+
since,
|
|
97
|
+
timeoutMs,
|
|
98
|
+
onTimeout: () => console.warn(`[automation] ${method}: activePage signal not seen within ${timeoutMs}ms — proceeding on timeout floor`),
|
|
99
|
+
});
|
|
100
|
+
return { result: undefined };
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Non-nav `wx.*` on the native-host service-host, returning its (sync) value.
|
|
104
|
+
* Async methods that report via a success callback return undefined here,
|
|
105
|
+
* same as the default-arch generic path (`runSimulatorGeneric`).
|
|
106
|
+
*/
|
|
107
|
+
async function runNativeHostGeneric(serviceWc, method, args) {
|
|
108
|
+
const argsStr = args.map((a) => JSON.stringify(a)).join(', ');
|
|
109
|
+
const result = await serviceWc.executeJavaScript(`
|
|
110
|
+
new Promise((resolve, reject) => {
|
|
111
|
+
try {
|
|
112
|
+
if (typeof wx === 'undefined' || typeof wx[${JSON.stringify(method)}] !== 'function') {
|
|
113
|
+
reject(new Error('wx.' + ${JSON.stringify(method)} + ' is not a function in service host'))
|
|
114
|
+
return
|
|
113
115
|
}
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
116
|
+
resolve(wx.${method}(${argsStr}))
|
|
117
|
+
} catch (e) { reject(e && e.message ? e.message : String(e)) }
|
|
118
|
+
})
|
|
119
|
+
`);
|
|
120
|
+
return { result };
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* NATIVE-HOST: the authoritative `wx.*` runs in the hidden service-host
|
|
124
|
+
* window (the simulator / render-guest context has no `wx`). Run EVERY method
|
|
125
|
+
* there — nav so the page stack goes through the real runtime path, and
|
|
126
|
+
* non-nav (setNavigationBarTitle / getSystemInfoSync / tabBar APIs / …) so
|
|
127
|
+
* they don't fall through to the default-arch eval-in-simulator path, which
|
|
128
|
+
* throws "wx is not defined" under native-host.
|
|
129
|
+
*/
|
|
130
|
+
async function runNativeHostWxMethod(bridge, method, args) {
|
|
131
|
+
const serviceWc = bridge.getServiceWc();
|
|
132
|
+
if (!serviceWc)
|
|
133
|
+
throw new Error('Service host not connected');
|
|
134
|
+
if (NAV_METHODS.has(method) || method === 'navigateBack') {
|
|
135
|
+
return runNativeHostNav(bridge, serviceWc, method, args);
|
|
136
|
+
}
|
|
137
|
+
return runNativeHostGeneric(serviceWc, method, args);
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* Try clicking a matching DOM element for a nav target. We can't use
|
|
141
|
+
* `inIframe` here — tabBar mini-apps keep prior tabs' iframes around
|
|
142
|
+
* (display:none) and `iframes[length-1]` ends up being the hidden,
|
|
143
|
+
* freshly-cached tab rather than the visible one. Pick the visible iframe
|
|
144
|
+
* explicitly.
|
|
145
|
+
*/
|
|
146
|
+
async function clickNavTarget(ctx, cleanUrl) {
|
|
147
|
+
return evalInSim(ctx, `(() => {
|
|
148
|
+
const iframes = Array.from(document.querySelectorAll('iframe'))
|
|
149
|
+
const visible = iframes.filter((f) => {
|
|
150
|
+
const cs = window.getComputedStyle(f)
|
|
151
|
+
if (cs.display === 'none' || cs.visibility === 'hidden') return false
|
|
152
|
+
const rect = f.getBoundingClientRect()
|
|
153
|
+
return rect.width > 0 && rect.height > 0
|
|
154
|
+
})
|
|
155
|
+
const target = visible[visible.length - 1] || iframes[iframes.length - 1]
|
|
156
|
+
if (!target || !target.contentDocument) return false
|
|
157
|
+
const path = ${JSON.stringify(cleanUrl)}
|
|
158
|
+
const el = Array.from(target.contentDocument.querySelectorAll('[data-path]'))
|
|
159
|
+
.find((e) => e.getAttribute('data-path') === path)
|
|
160
|
+
if (el) { el.click(); return true }
|
|
161
|
+
return false
|
|
162
|
+
})()`).catch(() => false);
|
|
163
|
+
}
|
|
164
|
+
/**
|
|
165
|
+
* Fallback cascade for navigation methods when no matching DOM element was
|
|
166
|
+
* found to click:
|
|
167
|
+
* 1. iframe wx[method] — page-iframe surface (older arch, where jdimina
|
|
168
|
+
* installed `window.wx` on each page frame).
|
|
169
|
+
* 2. top-window wx[method] — set up by simulator/main.tsx, binds
|
|
170
|
+
* MiniApp.switchTab / navigateTo / etc. Required for switchTab since
|
|
171
|
+
* page-iframe wx (jdimina) doesn't expose it.
|
|
172
|
+
*/
|
|
173
|
+
async function invokeNavViaWx(ctx, method, cleanUrl) {
|
|
174
|
+
const apiName = JSON.stringify(method);
|
|
175
|
+
const urlJson = JSON.stringify(cleanUrl);
|
|
176
|
+
await evalInSim(ctx, `(() => {
|
|
177
|
+
const iframes = Array.from(document.querySelectorAll('iframe'))
|
|
178
|
+
for (const f of iframes) {
|
|
120
179
|
try {
|
|
121
|
-
if (
|
|
122
|
-
|
|
180
|
+
if (f.contentWindow && f.contentWindow.wx && typeof f.contentWindow.wx[${apiName}] === 'function') {
|
|
181
|
+
f.contentWindow.wx[${apiName}]({ url: ${urlJson} })
|
|
123
182
|
return
|
|
124
183
|
}
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
})
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
// Try clicking a matching DOM element first. We can't use `inIframe`
|
|
138
|
-
// here — tabBar mini-apps keep prior tabs' iframes around (display:none)
|
|
139
|
-
// and `iframes[length-1]` ends up being the hidden, freshly-cached tab
|
|
140
|
-
// rather than the visible one. Pick the visible iframe explicitly.
|
|
141
|
-
const clicked = await evalInSim(ctx, `(() => {
|
|
142
|
-
const iframes = Array.from(document.querySelectorAll('iframe'))
|
|
143
|
-
const visible = iframes.filter((f) => {
|
|
144
|
-
const cs = window.getComputedStyle(f)
|
|
145
|
-
if (cs.display === 'none' || cs.visibility === 'hidden') return false
|
|
146
|
-
const rect = f.getBoundingClientRect()
|
|
147
|
-
return rect.width > 0 && rect.height > 0
|
|
148
|
-
})
|
|
149
|
-
const target = visible[visible.length - 1] || iframes[iframes.length - 1]
|
|
150
|
-
if (!target || !target.contentDocument) return false
|
|
151
|
-
const path = ${JSON.stringify(cleanUrl)}
|
|
152
|
-
const el = Array.from(target.contentDocument.querySelectorAll('[data-path]'))
|
|
153
|
-
.find((e) => e.getAttribute('data-path') === path)
|
|
154
|
-
if (el) { el.click(); return true }
|
|
155
|
-
return false
|
|
156
|
-
})()`).catch(() => false);
|
|
157
|
-
if (!clicked) {
|
|
158
|
-
// Fallback cascade for navigation methods:
|
|
159
|
-
// 1. iframe wx[method] — page-iframe surface (older arch, where
|
|
160
|
-
// jdimina installed `window.wx` on each page frame).
|
|
161
|
-
// 2. top-window wx[method] — set up by simulator/main.tsx, binds
|
|
162
|
-
// MiniApp.switchTab / navigateTo / etc. Required for switchTab
|
|
163
|
-
// since page-iframe wx (jdimina) doesn't expose it.
|
|
164
|
-
const apiName = JSON.stringify(method);
|
|
165
|
-
const urlJson = JSON.stringify(cleanUrl);
|
|
166
|
-
await evalInSim(ctx, `(() => {
|
|
167
|
-
const iframes = Array.from(document.querySelectorAll('iframe'))
|
|
168
|
-
for (const f of iframes) {
|
|
169
|
-
try {
|
|
170
|
-
if (f.contentWindow && f.contentWindow.wx && typeof f.contentWindow.wx[${apiName}] === 'function') {
|
|
171
|
-
f.contentWindow.wx[${apiName}]({ url: ${urlJson} })
|
|
172
|
-
return
|
|
173
|
-
}
|
|
174
|
-
} catch (_) {}
|
|
175
|
-
}
|
|
176
|
-
if (typeof wx !== 'undefined' && wx && typeof wx[${apiName}] === 'function') {
|
|
177
|
-
wx[${apiName}]({ url: ${urlJson} })
|
|
178
|
-
}
|
|
179
|
-
})()`);
|
|
180
|
-
}
|
|
181
|
-
// Wait for navigation
|
|
182
|
-
await new Promise((r) => setTimeout(r, 2000));
|
|
183
|
-
}
|
|
184
|
-
return { result: undefined };
|
|
185
|
-
}
|
|
186
|
-
// For navigateBack
|
|
187
|
-
if (method === 'navigateBack') {
|
|
188
|
-
await evalInSim(ctx, 'history.back()');
|
|
189
|
-
await new Promise((r) => setTimeout(r, 1500));
|
|
184
|
+
} catch (_) {}
|
|
185
|
+
}
|
|
186
|
+
if (typeof wx !== 'undefined' && wx && typeof wx[${apiName}] === 'function') {
|
|
187
|
+
wx[${apiName}]({ url: ${urlJson} })
|
|
188
|
+
}
|
|
189
|
+
})()`);
|
|
190
|
+
}
|
|
191
|
+
/** Default-arch nav: handled via DOM click / wx.* on the iframe. */
|
|
192
|
+
async function runSimulatorNav(ctx, method, args) {
|
|
193
|
+
const opts = args[0];
|
|
194
|
+
const url = opts?.url;
|
|
195
|
+
if (!url)
|
|
190
196
|
return { result: undefined };
|
|
197
|
+
const cleanUrl = url.startsWith('/') ? url : `/${url}`;
|
|
198
|
+
const clicked = await clickNavTarget(ctx, cleanUrl);
|
|
199
|
+
if (!clicked) {
|
|
200
|
+
await invokeNavViaWx(ctx, method, cleanUrl);
|
|
191
201
|
}
|
|
192
|
-
//
|
|
202
|
+
// Wait for navigation
|
|
203
|
+
await new Promise((r) => setTimeout(r, 2000));
|
|
204
|
+
return { result: undefined };
|
|
205
|
+
}
|
|
206
|
+
async function runSimulatorNavigateBack(ctx) {
|
|
207
|
+
await evalInSim(ctx, 'history.back()');
|
|
208
|
+
await new Promise((r) => setTimeout(r, 1500));
|
|
209
|
+
return { result: undefined };
|
|
210
|
+
}
|
|
211
|
+
/** General wx method call — async wx methods with success/fail callbacks. */
|
|
212
|
+
async function runSimulatorGeneric(ctx, method, args) {
|
|
193
213
|
const argsStr = args.map((a) => JSON.stringify(a)).join(', ');
|
|
194
|
-
// Async wx methods with success/fail callbacks
|
|
195
214
|
const result = await evalInSim(ctx, `
|
|
196
215
|
new Promise((resolve, reject) => {
|
|
197
216
|
try {
|
|
@@ -203,6 +222,21 @@ appHandlers['App.callWxMethod'] = async (ctx, params) => {
|
|
|
203
222
|
})
|
|
204
223
|
`);
|
|
205
224
|
return { result };
|
|
225
|
+
}
|
|
226
|
+
appHandlers['App.callWxMethod'] = async (ctx, params) => {
|
|
227
|
+
const method = params.method;
|
|
228
|
+
const args = params.args || [];
|
|
229
|
+
const bridge = ctx.bridge;
|
|
230
|
+
if (bridge?.isNativeHost()) {
|
|
231
|
+
return runNativeHostWxMethod(bridge, method, args);
|
|
232
|
+
}
|
|
233
|
+
if (NAV_METHODS.has(method)) {
|
|
234
|
+
return runSimulatorNav(ctx, method, args);
|
|
235
|
+
}
|
|
236
|
+
if (method === 'navigateBack') {
|
|
237
|
+
return runSimulatorNavigateBack(ctx);
|
|
238
|
+
}
|
|
239
|
+
return runSimulatorGeneric(ctx, method, args);
|
|
206
240
|
};
|
|
207
241
|
appHandlers['App.callFunction'] = async (ctx, params) => {
|
|
208
242
|
const fnDecl = params.functionDeclaration;
|
|
@@ -11,6 +11,111 @@
|
|
|
11
11
|
*/
|
|
12
12
|
import { z } from 'zod';
|
|
13
13
|
import { getClient } from '../target-manager.js';
|
|
14
|
+
function ok(text) {
|
|
15
|
+
return { content: [{ type: 'text', text }] };
|
|
16
|
+
}
|
|
17
|
+
function err(msg) {
|
|
18
|
+
return { content: [{ type: 'text', text: `Error: ${msg}` }], isError: true };
|
|
19
|
+
}
|
|
20
|
+
async function clickAt(c, x, y) {
|
|
21
|
+
await c.Input.dispatchMouseEvent({ type: 'mousePressed', x, y, button: 'left', clickCount: 1 });
|
|
22
|
+
await c.Input.dispatchMouseEvent({ type: 'mouseReleased', x, y, button: 'left', clickCount: 1 });
|
|
23
|
+
}
|
|
24
|
+
async function tapCoord(c, x, y) {
|
|
25
|
+
if (typeof x !== 'number' || typeof y !== 'number')
|
|
26
|
+
return err('tap_coord requires x and y');
|
|
27
|
+
await clickAt(c, x, y);
|
|
28
|
+
return ok(`Tapped at (${x}, ${y})`);
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* In-page expression evaluated by `tap_selector`: locates the nth match,
|
|
32
|
+
* scrolls it into view, and reports its center point (or a reason it can't be
|
|
33
|
+
* tapped) so the caller can dispatch a real mouse click at that point.
|
|
34
|
+
*/
|
|
35
|
+
function buildTapSelectorExpression(selector, index) {
|
|
36
|
+
return `(() => {
|
|
37
|
+
const payload = ${JSON.stringify({ selector, index })}
|
|
38
|
+
try {
|
|
39
|
+
const matches = Array.from(document.querySelectorAll(payload.selector))
|
|
40
|
+
if (matches.length === 0) {
|
|
41
|
+
return { ok: false, reason: 'no_match', message: \`selector matched no elements: \${payload.selector}\` }
|
|
42
|
+
}
|
|
43
|
+
if (payload.index < 0 || payload.index >= matches.length) {
|
|
44
|
+
return { ok: false, reason: 'out_of_range', message: \`nth \${payload.index} out of range (matches: \${matches.length})\` }
|
|
45
|
+
}
|
|
46
|
+
const element = matches[payload.index]
|
|
47
|
+
element.scrollIntoView({ block: 'center', inline: 'center' })
|
|
48
|
+
const rect = element.getBoundingClientRect()
|
|
49
|
+
if (rect.width === 0 || rect.height === 0) {
|
|
50
|
+
return { ok: false, reason: 'not_visible', message: \`selector matched an element, but it is not visible or rendered (zero rect): \${payload.selector}[\${payload.index}]\` }
|
|
51
|
+
}
|
|
52
|
+
return {
|
|
53
|
+
ok: true,
|
|
54
|
+
selector: payload.selector,
|
|
55
|
+
index: payload.index,
|
|
56
|
+
x: rect.left + rect.width / 2,
|
|
57
|
+
y: rect.top + rect.height / 2,
|
|
58
|
+
rect: { left: rect.left, top: rect.top, width: rect.width, height: rect.height },
|
|
59
|
+
}
|
|
60
|
+
} catch (error) {
|
|
61
|
+
const message = error instanceof Error ? error.message : String(error)
|
|
62
|
+
return { ok: false, reason: 'selector_error', message: \`invalid selector: \${payload.selector} (\${message})\` }
|
|
63
|
+
}
|
|
64
|
+
})()`;
|
|
65
|
+
}
|
|
66
|
+
async function tapSelector(c, selector, nth) {
|
|
67
|
+
if (!selector)
|
|
68
|
+
return err('tap_selector requires selector');
|
|
69
|
+
const index = nth ?? 0;
|
|
70
|
+
const expression = buildTapSelectorExpression(selector, index);
|
|
71
|
+
const result = await c.Runtime.evaluate({ expression, returnByValue: true, awaitPromise: true });
|
|
72
|
+
const value = result.result?.value;
|
|
73
|
+
if (!value)
|
|
74
|
+
return err(`selector evaluation returned no result: ${selector}[${index}]`);
|
|
75
|
+
if (!value.ok)
|
|
76
|
+
return err(value.message);
|
|
77
|
+
await clickAt(c, value.x, value.y);
|
|
78
|
+
return ok(`Tapped selector ${value.selector}[${value.index}] at (${value.x}, ${value.y}) within rect (${value.rect.left}, ${value.rect.top}, ${value.rect.width} x ${value.rect.height})`);
|
|
79
|
+
}
|
|
80
|
+
async function typeText(c, selector, nth, text) {
|
|
81
|
+
if (!selector || typeof text !== 'string')
|
|
82
|
+
return err('type requires selector and text');
|
|
83
|
+
const { root } = await c.DOM.getDocument({ depth: 0 });
|
|
84
|
+
const { nodeIds } = await c.DOM.querySelectorAll({ nodeId: root.nodeId, selector });
|
|
85
|
+
if (!nodeIds || nodeIds.length === 0)
|
|
86
|
+
return err(`selector matched no elements: ${selector}`);
|
|
87
|
+
const index = nth ?? 0;
|
|
88
|
+
if (index < 0 || index >= nodeIds.length)
|
|
89
|
+
return err(`nth ${index} out of range (matches: ${nodeIds.length})`);
|
|
90
|
+
await c.DOM.focus({ nodeId: nodeIds[index] });
|
|
91
|
+
try {
|
|
92
|
+
await c.Input.insertText({ text });
|
|
93
|
+
}
|
|
94
|
+
catch {
|
|
95
|
+
for (const ch of text) {
|
|
96
|
+
await c.Input.dispatchKeyEvent({ type: 'char', text: ch });
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
return ok(`Typed ${text.length} char(s) into ${selector}[${index}]`);
|
|
100
|
+
}
|
|
101
|
+
async function scrollAt(c, x, y, deltaX, deltaY) {
|
|
102
|
+
if (typeof x !== 'number' || typeof y !== 'number' || typeof deltaX !== 'number' || typeof deltaY !== 'number') {
|
|
103
|
+
return err('scroll requires x, y, deltaX, deltaY');
|
|
104
|
+
}
|
|
105
|
+
await c.Input.dispatchMouseEvent({ type: 'mouseWheel', x, y, deltaX, deltaY });
|
|
106
|
+
return ok(`Scrolled at (${x}, ${y}) by (${deltaX}, ${deltaY})`);
|
|
107
|
+
}
|
|
108
|
+
async function dispatchKey(c, key) {
|
|
109
|
+
if (!key)
|
|
110
|
+
return err('key requires key');
|
|
111
|
+
const isSingleChar = key.length === 1;
|
|
112
|
+
const down = { type: 'keyDown', key, code: key };
|
|
113
|
+
if (isSingleChar)
|
|
114
|
+
down.text = key;
|
|
115
|
+
await c.Input.dispatchKeyEvent(down);
|
|
116
|
+
await c.Input.dispatchKeyEvent({ type: 'keyUp', key, code: key });
|
|
117
|
+
return ok(`Dispatched key ${key}`);
|
|
118
|
+
}
|
|
14
119
|
export function registerSimulatorTools(server) {
|
|
15
120
|
server.tool('simulator_navigate', 'Navigate the simulator to a URL, or reload the current page', {
|
|
16
121
|
url: z.string().optional().describe('URL to navigate to'),
|
|
@@ -40,104 +145,20 @@ export function registerSimulatorTools(server) {
|
|
|
40
145
|
deltaY: z.number().optional().describe('Vertical wheel delta for `scroll`'),
|
|
41
146
|
}, async ({ action, x, y, selector, nth, text, key, deltaX, deltaY }) => {
|
|
42
147
|
const c = getClient('simulator');
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
return err('tap_selector requires selector');
|
|
57
|
-
const index = nth ?? 0;
|
|
58
|
-
const expression = `(() => {
|
|
59
|
-
const payload = ${JSON.stringify({ selector, index })}
|
|
60
|
-
try {
|
|
61
|
-
const matches = Array.from(document.querySelectorAll(payload.selector))
|
|
62
|
-
if (matches.length === 0) {
|
|
63
|
-
return { ok: false, reason: 'no_match', message: \`selector matched no elements: \${payload.selector}\` }
|
|
64
|
-
}
|
|
65
|
-
if (payload.index < 0 || payload.index >= matches.length) {
|
|
66
|
-
return { ok: false, reason: 'out_of_range', message: \`nth \${payload.index} out of range (matches: \${matches.length})\` }
|
|
67
|
-
}
|
|
68
|
-
const element = matches[payload.index]
|
|
69
|
-
element.scrollIntoView({ block: 'center', inline: 'center' })
|
|
70
|
-
const rect = element.getBoundingClientRect()
|
|
71
|
-
if (rect.width === 0 || rect.height === 0) {
|
|
72
|
-
return { ok: false, reason: 'not_visible', message: \`selector matched an element, but it is not visible or rendered (zero rect): \${payload.selector}[\${payload.index}]\` }
|
|
73
|
-
}
|
|
74
|
-
return {
|
|
75
|
-
ok: true,
|
|
76
|
-
selector: payload.selector,
|
|
77
|
-
index: payload.index,
|
|
78
|
-
x: rect.left + rect.width / 2,
|
|
79
|
-
y: rect.top + rect.height / 2,
|
|
80
|
-
rect: { left: rect.left, top: rect.top, width: rect.width, height: rect.height },
|
|
81
|
-
}
|
|
82
|
-
} catch (error) {
|
|
83
|
-
const message = error instanceof Error ? error.message : String(error)
|
|
84
|
-
return { ok: false, reason: 'selector_error', message: \`invalid selector: \${payload.selector} (\${message})\` }
|
|
85
|
-
}
|
|
86
|
-
})()`;
|
|
87
|
-
const result = await c.Runtime.evaluate({ expression, returnByValue: true, awaitPromise: true });
|
|
88
|
-
const value = result.result?.value;
|
|
89
|
-
if (!value)
|
|
90
|
-
return err(`selector evaluation returned no result: ${selector}[${index}]`);
|
|
91
|
-
if (!value.ok)
|
|
92
|
-
return err(value.message);
|
|
93
|
-
await clickAt(value.x, value.y);
|
|
94
|
-
return {
|
|
95
|
-
content: [{
|
|
96
|
-
type: 'text',
|
|
97
|
-
text: `Tapped selector ${value.selector}[${value.index}] at (${value.x}, ${value.y}) within rect (${value.rect.left}, ${value.rect.top}, ${value.rect.width} x ${value.rect.height})`,
|
|
98
|
-
}],
|
|
99
|
-
};
|
|
100
|
-
}
|
|
101
|
-
if (action === 'type') {
|
|
102
|
-
if (!selector || typeof text !== 'string')
|
|
103
|
-
return err('type requires selector and text');
|
|
104
|
-
const { root } = await c.DOM.getDocument({ depth: 0 });
|
|
105
|
-
const { nodeIds } = await c.DOM.querySelectorAll({ nodeId: root.nodeId, selector });
|
|
106
|
-
if (!nodeIds || nodeIds.length === 0)
|
|
107
|
-
return err(`selector matched no elements: ${selector}`);
|
|
108
|
-
const index = nth ?? 0;
|
|
109
|
-
if (index < 0 || index >= nodeIds.length)
|
|
110
|
-
return err(`nth ${index} out of range (matches: ${nodeIds.length})`);
|
|
111
|
-
await c.DOM.focus({ nodeId: nodeIds[index] });
|
|
112
|
-
try {
|
|
113
|
-
await c.Input.insertText({ text });
|
|
114
|
-
}
|
|
115
|
-
catch {
|
|
116
|
-
for (const ch of text) {
|
|
117
|
-
await c.Input.dispatchKeyEvent({ type: 'char', text: ch });
|
|
118
|
-
}
|
|
119
|
-
}
|
|
120
|
-
return { content: [{ type: 'text', text: `Typed ${text.length} char(s) into ${selector}[${index}]` }] };
|
|
121
|
-
}
|
|
122
|
-
if (action === 'scroll') {
|
|
123
|
-
if (typeof x !== 'number' || typeof y !== 'number' || typeof deltaX !== 'number' || typeof deltaY !== 'number') {
|
|
124
|
-
return err('scroll requires x, y, deltaX, deltaY');
|
|
125
|
-
}
|
|
126
|
-
await c.Input.dispatchMouseEvent({ type: 'mouseWheel', x, y, deltaX, deltaY });
|
|
127
|
-
return { content: [{ type: 'text', text: `Scrolled at (${x}, ${y}) by (${deltaX}, ${deltaY})` }] };
|
|
128
|
-
}
|
|
129
|
-
if (action === 'key') {
|
|
130
|
-
if (!key)
|
|
131
|
-
return err('key requires key');
|
|
132
|
-
const isSingleChar = key.length === 1;
|
|
133
|
-
const down = { type: 'keyDown', key, code: key };
|
|
134
|
-
if (isSingleChar)
|
|
135
|
-
down.text = key;
|
|
136
|
-
await c.Input.dispatchKeyEvent(down);
|
|
137
|
-
await c.Input.dispatchKeyEvent({ type: 'keyUp', key, code: key });
|
|
138
|
-
return { content: [{ type: 'text', text: `Dispatched key ${key}` }] };
|
|
148
|
+
switch (action) {
|
|
149
|
+
case 'tap_coord':
|
|
150
|
+
return tapCoord(c, x, y);
|
|
151
|
+
case 'tap_selector':
|
|
152
|
+
return tapSelector(c, selector, nth);
|
|
153
|
+
case 'type':
|
|
154
|
+
return typeText(c, selector, nth, text);
|
|
155
|
+
case 'scroll':
|
|
156
|
+
return scrollAt(c, x, y, deltaX, deltaY);
|
|
157
|
+
case 'key':
|
|
158
|
+
return dispatchKey(c, key);
|
|
159
|
+
default:
|
|
160
|
+
return err(`unknown action: ${String(action)}`);
|
|
139
161
|
}
|
|
140
|
-
return err(`unknown action: ${action}`);
|
|
141
162
|
});
|
|
142
163
|
}
|
|
143
164
|
//# sourceMappingURL=simulator-tools.js.map
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure batch-packing for the native dispatch queue: greedily pack queued CDP
|
|
3
|
+
* messages up to `maxBatchChars` so one `executeJavaScript` call stays sized,
|
|
4
|
+
* pulling any oversized message (over `maxSingleChars`) into `chunked` for the
|
|
5
|
+
* separate chunked transport instead.
|
|
6
|
+
*
|
|
7
|
+
* An oversized message reached while `batch` is still empty is queued into
|
|
8
|
+
* `chunked` immediately and packing continues past it, so several oversized
|
|
9
|
+
* messages in a row are all pulled out in one pass. One reached once `batch`
|
|
10
|
+
* already has items stops packing there, leaving it (and everything after) in
|
|
11
|
+
* `remaining` for the next flush — this preserves delivery order across passes.
|
|
12
|
+
*/
|
|
13
|
+
export interface PackedDispatchBatch {
|
|
14
|
+
/** Messages to send in one `executeJavaScript` batch dispatch. */
|
|
15
|
+
batch: string[];
|
|
16
|
+
/** Oversized messages to dispatch individually via the chunked transport. */
|
|
17
|
+
chunked: string[];
|
|
18
|
+
/** Whatever the queue had left after this pass. */
|
|
19
|
+
remaining: string[];
|
|
20
|
+
}
|
|
21
|
+
export declare function packDispatchBatch(queue: readonly string[], maxSingleChars: number, maxBatchChars: number): PackedDispatchBatch;
|
|
22
|
+
//# sourceMappingURL=dispatch-batch.d.ts.map
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export function packDispatchBatch(queue, maxSingleChars, maxBatchChars) {
|
|
2
|
+
const batch = [];
|
|
3
|
+
const chunked = [];
|
|
4
|
+
let batchChars = 0;
|
|
5
|
+
let i = 0;
|
|
6
|
+
for (; i < queue.length; i++) {
|
|
7
|
+
const msg = queue[i];
|
|
8
|
+
if (msg.length > maxSingleChars) {
|
|
9
|
+
if (batch.length > 0)
|
|
10
|
+
break;
|
|
11
|
+
chunked.push(msg);
|
|
12
|
+
continue;
|
|
13
|
+
}
|
|
14
|
+
if (batch.length > 0 && batchChars + msg.length > maxBatchChars)
|
|
15
|
+
break;
|
|
16
|
+
batch.push(msg);
|
|
17
|
+
batchChars += msg.length;
|
|
18
|
+
}
|
|
19
|
+
return { batch, chunked, remaining: queue.slice(i) };
|
|
20
|
+
}
|
|
21
|
+
//# sourceMappingURL=dispatch-batch.js.map
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { DisposableRegistry, toDisposable } from '@dimina-kit/electron-deck/main';
|
|
2
2
|
import { isFrontendSettled } from '../views/inject-when-ready.js';
|
|
3
|
+
import { packDispatchBatch } from './dispatch-batch.js';
|
|
3
4
|
// ── requestId namespacing (pure, testable) ──────────────────────────────────
|
|
4
5
|
/**
|
|
5
6
|
* CDP events whose `params.requestId` we rewrite into the virtual namespace.
|
|
@@ -375,26 +376,10 @@ export function createNetworkForwarder(bridge) {
|
|
|
375
376
|
if (sink === 'idle')
|
|
376
377
|
beginProbing();
|
|
377
378
|
// Pack greedily up to MAX_BATCH_CHARS so one executeJavaScript stays sized;
|
|
378
|
-
// oversized single messages go via the chunked transport.
|
|
379
|
-
const batch =
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
for (; i < dispatchQueue.length; i++) {
|
|
383
|
-
const msg = dispatchQueue[i];
|
|
384
|
-
if (msg.length > MAX_SINGLE_DISPATCH_CHARS) {
|
|
385
|
-
// Flush whatever's accumulated first to preserve ordering, then chunk.
|
|
386
|
-
if (batch.length > 0)
|
|
387
|
-
break;
|
|
388
|
-
dispatchChunked(wc, msg);
|
|
389
|
-
continue;
|
|
390
|
-
}
|
|
391
|
-
if (batch.length > 0 && batchChars + msg.length > MAX_BATCH_CHARS)
|
|
392
|
-
break;
|
|
393
|
-
batch.push(msg);
|
|
394
|
-
batchChars += msg.length;
|
|
395
|
-
}
|
|
396
|
-
// Everything up to i was either batched or chunk-dispatched; keep the rest.
|
|
397
|
-
const remaining = dispatchQueue.slice(i);
|
|
379
|
+
// oversized single messages go via the chunked transport (see packDispatchBatch).
|
|
380
|
+
const { batch, chunked, remaining } = packDispatchBatch(dispatchQueue, MAX_SINGLE_DISPATCH_CHARS, MAX_BATCH_CHARS);
|
|
381
|
+
for (const msg of chunked)
|
|
382
|
+
dispatchChunked(wc, msg);
|
|
398
383
|
if (batch.length === 0) {
|
|
399
384
|
// Only chunked messages were processed this turn; continue with the rest.
|
|
400
385
|
dispatchQueue = remaining;
|