@donggui/core 1.5.13 → 1.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/es/agent/agent.mjs +187 -10
- package/dist/es/agent/agent.mjs.map +1 -1
- package/dist/es/agent/utils.mjs +1 -1
- package/dist/es/ai-model/assert.mjs +415 -0
- package/dist/es/ai-model/assert.mjs.map +1 -0
- package/dist/es/ai-model/index.mjs +2 -1
- package/dist/es/device/index.mjs +1 -1
- package/dist/es/device/index.mjs.map +1 -1
- package/dist/es/service/index.mjs +4 -6
- package/dist/es/service/index.mjs.map +1 -1
- package/dist/es/types.mjs.map +1 -1
- package/dist/es/utils.mjs +2 -2
- package/dist/lib/agent/agent.js +185 -8
- package/dist/lib/agent/agent.js.map +1 -1
- package/dist/lib/agent/utils.js +1 -1
- package/dist/lib/ai-model/assert.js +455 -0
- package/dist/lib/ai-model/assert.js.map +1 -0
- package/dist/lib/ai-model/index.js +18 -11
- package/dist/lib/device/index.js +1 -1
- package/dist/lib/device/index.js.map +1 -1
- package/dist/lib/service/index.js +4 -6
- package/dist/lib/service/index.js.map +1 -1
- package/dist/lib/types.js +3 -3
- package/dist/lib/types.js.map +1 -1
- package/dist/lib/utils.js +2 -2
- package/dist/types/agent/agent.d.ts +38 -2
- package/dist/types/ai-model/assert.d.ts +66 -0
- package/dist/types/ai-model/index.d.ts +2 -0
- package/dist/types/types.d.ts +156 -0
- package/package.json +25 -44
|
@@ -0,0 +1,415 @@
|
|
|
1
|
+
import { getDebug } from "@midscene/shared/logger";
|
|
2
|
+
import { callAIWithObjectResponse } from "./service-caller/index.mjs";
|
|
3
|
+
const debug = getDebug('ai:assert');
|
|
4
|
+
const DEFAULT_SYSTEM_CHECK_PROMPT = `请检查以下系统级问题:
|
|
5
|
+
|
|
6
|
+
1. **白屏检测**:页面是否完全白屏或大面积空白
|
|
7
|
+
2. **布局遮挡**:重要元素是否存在组件间重叠遮挡
|
|
8
|
+
3. **未加载内容**:页面是否有加载中的占位符、骨架屏
|
|
9
|
+
4. **错误提示**:页面是否显示错误信息、异常提示
|
|
10
|
+
5. **后端错误**:是否有后端请求失败的提示,如"活动太火爆了"、报错 Toast、错误码 等
|
|
11
|
+
|
|
12
|
+
如果发现以上问题,请在 systemCheckResults 中标注对应字段为 true。`;
|
|
13
|
+
function buildAssertionPrompt(options) {
|
|
14
|
+
const { assertion, businessContext, systemCheckPrompt } = options;
|
|
15
|
+
let prompt = `## 用户断言描述
|
|
16
|
+
${assertion}`;
|
|
17
|
+
if (businessContext) prompt += `
|
|
18
|
+
|
|
19
|
+
## 业务知识上下文
|
|
20
|
+
${businessContext}`;
|
|
21
|
+
if (systemCheckPrompt) prompt += `
|
|
22
|
+
|
|
23
|
+
## 系统校验规则
|
|
24
|
+
${systemCheckPrompt}`;
|
|
25
|
+
prompt += `
|
|
26
|
+
|
|
27
|
+
## 输出要求
|
|
28
|
+
请以 JSON 格式输出断言结果:
|
|
29
|
+
{
|
|
30
|
+
"pass": boolean, // 断言是否通过
|
|
31
|
+
"thought": string, // 思考过程
|
|
32
|
+
"reason": string, // 失败原因(如果失败)
|
|
33
|
+
"systemCheckResults": { // 系统校验结果(可选)
|
|
34
|
+
"whiteScreen": boolean,
|
|
35
|
+
"layoutBlocked": boolean,
|
|
36
|
+
"loadingContent": boolean,
|
|
37
|
+
"errorPrompt": boolean,
|
|
38
|
+
"backendError": boolean
|
|
39
|
+
}
|
|
40
|
+
}`;
|
|
41
|
+
return prompt;
|
|
42
|
+
}
|
|
43
|
+
async function AiAssertElement(options) {
|
|
44
|
+
const { beforeScreenshot, afterScreenshot, assertion, businessContext, enableSystemCheck = true, customSystemCheckRules, modelConfig, abortSignal } = options;
|
|
45
|
+
const systemPrompt = `你是一个自动化测试断言专家。请根据以下信息判断断言是否通过。
|
|
46
|
+
|
|
47
|
+
你需要:
|
|
48
|
+
1. 分析操作前后的截图变化(如果提供了操作前截图)
|
|
49
|
+
2. 验证用户的断言描述是否成立
|
|
50
|
+
3. 检查是否存在系统级问题(如果启用)
|
|
51
|
+
4. 给出详细的思考过程和判断结果`;
|
|
52
|
+
const systemCheckPrompt = enableSystemCheck ? customSystemCheckRules || DEFAULT_SYSTEM_CHECK_PROMPT : '';
|
|
53
|
+
const userContent = [];
|
|
54
|
+
if (beforeScreenshot) {
|
|
55
|
+
userContent.push({
|
|
56
|
+
type: 'text',
|
|
57
|
+
text: '## 操作前截图(执行操作前的页面状态)'
|
|
58
|
+
});
|
|
59
|
+
userContent.push({
|
|
60
|
+
type: 'image_url',
|
|
61
|
+
image_url: {
|
|
62
|
+
url: beforeScreenshot,
|
|
63
|
+
detail: 'high'
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
userContent.push({
|
|
67
|
+
type: 'text',
|
|
68
|
+
text: '## 操作后截图(执行操作后的页面状态)'
|
|
69
|
+
});
|
|
70
|
+
} else userContent.push({
|
|
71
|
+
type: 'text',
|
|
72
|
+
text: '## 当前页面截图'
|
|
73
|
+
});
|
|
74
|
+
userContent.push({
|
|
75
|
+
type: 'image_url',
|
|
76
|
+
image_url: {
|
|
77
|
+
url: afterScreenshot,
|
|
78
|
+
detail: 'high'
|
|
79
|
+
}
|
|
80
|
+
});
|
|
81
|
+
userContent.push({
|
|
82
|
+
type: 'text',
|
|
83
|
+
text: buildAssertionPrompt({
|
|
84
|
+
assertion,
|
|
85
|
+
businessContext,
|
|
86
|
+
systemCheckPrompt
|
|
87
|
+
})
|
|
88
|
+
});
|
|
89
|
+
const msgs = [
|
|
90
|
+
{
|
|
91
|
+
role: 'system',
|
|
92
|
+
content: systemPrompt
|
|
93
|
+
},
|
|
94
|
+
{
|
|
95
|
+
role: 'user',
|
|
96
|
+
content: userContent
|
|
97
|
+
}
|
|
98
|
+
];
|
|
99
|
+
debug('calling AI for assertion:', assertion);
|
|
100
|
+
try {
|
|
101
|
+
const result = await callAIWithObjectResponse(msgs, modelConfig, {
|
|
102
|
+
abortSignal
|
|
103
|
+
});
|
|
104
|
+
debug('assertion result:', result.content);
|
|
105
|
+
return {
|
|
106
|
+
pass: result.content.pass,
|
|
107
|
+
thought: result.content.thought || '',
|
|
108
|
+
reason: result.content.reason,
|
|
109
|
+
usage: result.usage,
|
|
110
|
+
systemCheckResults: result.content.systemCheckResults,
|
|
111
|
+
rawResponse: JSON.stringify(result.content)
|
|
112
|
+
};
|
|
113
|
+
} catch (error) {
|
|
114
|
+
debug('assertion error:', error);
|
|
115
|
+
throw error;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
const DIFF_SYSTEM_PROMPT = `你是一个自动化测试的图像对比专家。你的任务是对比当前页面截图与基准图片,判断页面样式是否符合预期。
|
|
119
|
+
|
|
120
|
+
你需要:
|
|
121
|
+
1. 仔细对比两张图片的布局、颜色、元素位置
|
|
122
|
+
2. 分析差异是否在可接受范围内
|
|
123
|
+
3. 识别动态内容(如时间、日期)导致的差异
|
|
124
|
+
4. 给出详细的对比分析和判断结果`;
|
|
125
|
+
function buildDiffPrompt(options) {
|
|
126
|
+
const { assertion, businessContext, diffThreshold = 0.1, ignoreRegions, ignoreDynamicContent, strictMode } = options;
|
|
127
|
+
let prompt = `## 图像对比任务
|
|
128
|
+
|
|
129
|
+
请对比基准图片(预期样式)和当前截图(实际样式),判断页面是否符合预期。
|
|
130
|
+
|
|
131
|
+
## 用户断言描述
|
|
132
|
+
${assertion}`;
|
|
133
|
+
if (businessContext) prompt += `
|
|
134
|
+
|
|
135
|
+
## 业务知识上下文
|
|
136
|
+
${businessContext}`;
|
|
137
|
+
prompt += `
|
|
138
|
+
|
|
139
|
+
## 对比要求
|
|
140
|
+
|
|
141
|
+
请从以下维度进行对比分析:
|
|
142
|
+
|
|
143
|
+
1. **布局一致性**:页面布局是否与基准图片一致
|
|
144
|
+
2. **颜色一致性**:主要颜色是否与基准图片一致
|
|
145
|
+
3. **元素位置**:关键元素的位置是否与基准图片一致
|
|
146
|
+
4. **文字内容**:文字内容是否与基准图片一致
|
|
147
|
+
5. **图片资源**:图片、图标等资源是否正确加载
|
|
148
|
+
|
|
149
|
+
## 差异阈值
|
|
150
|
+
允许的差异阈值:${100 * diffThreshold}%`;
|
|
151
|
+
if (ignoreRegions && ignoreRegions.length > 0) {
|
|
152
|
+
prompt += `
|
|
153
|
+
|
|
154
|
+
## 忽略对比的区域
|
|
155
|
+
以下区域不参与对比:`;
|
|
156
|
+
ignoreRegions.forEach((region, index)=>{
|
|
157
|
+
prompt += `
|
|
158
|
+
${index + 1}. 位置 (${region.x}, ${region.y}),尺寸 ${region.width}x${region.height}`;
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
if (ignoreDynamicContent) prompt += `
|
|
162
|
+
|
|
163
|
+
## 动态内容处理
|
|
164
|
+
请忽略以下动态内容导致的差异:
|
|
165
|
+
- 时间显示
|
|
166
|
+
- 日期显示
|
|
167
|
+
- 随机验证码
|
|
168
|
+
- 动画效果`;
|
|
169
|
+
if (strictMode) prompt += `
|
|
170
|
+
|
|
171
|
+
## 严格模式
|
|
172
|
+
当前为严格模式,任何差异都视为失败。`;
|
|
173
|
+
prompt += `
|
|
174
|
+
|
|
175
|
+
## 输出要求
|
|
176
|
+
请以 JSON 格式输出对比结果:
|
|
177
|
+
{
|
|
178
|
+
"pass": boolean, // 断言是否通过
|
|
179
|
+
"thought": string, // 对比分析过程
|
|
180
|
+
"reason": string, // 失败原因(如果失败)
|
|
181
|
+
"diffDetails": { // 差异详情
|
|
182
|
+
"layoutMatch": boolean, // 布局是否匹配
|
|
183
|
+
"colorMatch": boolean, // 颜色是否匹配
|
|
184
|
+
"elementPositionMatch": boolean, // 元素位置是否匹配
|
|
185
|
+
"textContentMatch": boolean, // 文字内容是否匹配
|
|
186
|
+
"resourceMatch": boolean, // 资源是否匹配
|
|
187
|
+
"acceptableDifferences": string[], // 可接受的差异列表
|
|
188
|
+
"unacceptableDifferences": string[] // 不可接受的差异列表
|
|
189
|
+
}
|
|
190
|
+
}`;
|
|
191
|
+
return prompt;
|
|
192
|
+
}
|
|
193
|
+
async function AiAssertDiff(options) {
|
|
194
|
+
const { currentScreenshot, referenceImages, assertion, businessContext, diffThreshold, ignoreRegions, ignoreDynamicContent, strictMode, modelConfig, abortSignal } = options;
|
|
195
|
+
const userContent = [];
|
|
196
|
+
for (const refImage of referenceImages){
|
|
197
|
+
userContent.push({
|
|
198
|
+
type: 'text',
|
|
199
|
+
text: `## 基准图片:${refImage.name}`
|
|
200
|
+
});
|
|
201
|
+
userContent.push({
|
|
202
|
+
type: 'image_url',
|
|
203
|
+
image_url: {
|
|
204
|
+
url: refImage.url,
|
|
205
|
+
detail: 'high'
|
|
206
|
+
}
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
userContent.push({
|
|
210
|
+
type: 'text',
|
|
211
|
+
text: '## 当前截图(实际样式)'
|
|
212
|
+
});
|
|
213
|
+
userContent.push({
|
|
214
|
+
type: 'image_url',
|
|
215
|
+
image_url: {
|
|
216
|
+
url: currentScreenshot,
|
|
217
|
+
detail: 'high'
|
|
218
|
+
}
|
|
219
|
+
});
|
|
220
|
+
userContent.push({
|
|
221
|
+
type: 'text',
|
|
222
|
+
text: buildDiffPrompt({
|
|
223
|
+
assertion,
|
|
224
|
+
businessContext,
|
|
225
|
+
diffThreshold,
|
|
226
|
+
ignoreRegions,
|
|
227
|
+
ignoreDynamicContent,
|
|
228
|
+
strictMode
|
|
229
|
+
})
|
|
230
|
+
});
|
|
231
|
+
const msgs = [
|
|
232
|
+
{
|
|
233
|
+
role: 'system',
|
|
234
|
+
content: DIFF_SYSTEM_PROMPT
|
|
235
|
+
},
|
|
236
|
+
{
|
|
237
|
+
role: 'user',
|
|
238
|
+
content: userContent
|
|
239
|
+
}
|
|
240
|
+
];
|
|
241
|
+
debug('calling AI for diff assertion:', assertion);
|
|
242
|
+
try {
|
|
243
|
+
const result = await callAIWithObjectResponse(msgs, modelConfig, {
|
|
244
|
+
abortSignal
|
|
245
|
+
});
|
|
246
|
+
debug('diff assertion result:', result.content);
|
|
247
|
+
return {
|
|
248
|
+
pass: result.content.pass,
|
|
249
|
+
thought: result.content.thought || '',
|
|
250
|
+
reason: result.content.reason,
|
|
251
|
+
diffDetails: result.content.diffDetails,
|
|
252
|
+
usage: result.usage,
|
|
253
|
+
rawResponse: JSON.stringify(result.content)
|
|
254
|
+
};
|
|
255
|
+
} catch (error) {
|
|
256
|
+
debug('diff assertion error:', error);
|
|
257
|
+
throw error;
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
const VIDEO_SYSTEM_PROMPT = `你是一个自动化测试的视频/动画分析专家。你的任务是对比当前录制的视频与基准视频,判断动画效果是否符合预期。
|
|
261
|
+
|
|
262
|
+
你需要:
|
|
263
|
+
1. 分析视频中的动画流畅度、时长、完整性
|
|
264
|
+
2. 对比基准视频与当前视频的差异
|
|
265
|
+
3. 识别动画类型(过渡动画、加载动画、交互动画等)
|
|
266
|
+
4. 检测动画质量问题(卡顿、跳帧等)
|
|
267
|
+
5. 给出详细的分析过程和判断结果`;
|
|
268
|
+
function buildVideoPrompt(options) {
|
|
269
|
+
const { assertion, businessContext, videoOptions } = options;
|
|
270
|
+
let prompt = `## 视频/动画对比任务
|
|
271
|
+
|
|
272
|
+
请对比基准视频(预期动画效果)和当前录制的视频(实际动画效果),判断动画是否符合预期。
|
|
273
|
+
|
|
274
|
+
## 用户断言描述
|
|
275
|
+
${assertion}`;
|
|
276
|
+
if (businessContext) prompt += `
|
|
277
|
+
|
|
278
|
+
## 业务知识上下文
|
|
279
|
+
${businessContext}`;
|
|
280
|
+
prompt += `
|
|
281
|
+
|
|
282
|
+
## 分析要求
|
|
283
|
+
|
|
284
|
+
请从以下维度进行动画分析:
|
|
285
|
+
|
|
286
|
+
1. **动画流畅度**:动画是否流畅,有无卡顿、跳帧
|
|
287
|
+
2. **动画时长**:动画时长是否在预期范围内
|
|
288
|
+
3. **动画完整性**:动画是否完整执行,有无中断
|
|
289
|
+
4. **动画类型**:识别动画类型(过渡、加载、交互等)
|
|
290
|
+
5. **关键帧匹配**:关键时间点的画面是否符合预期`;
|
|
291
|
+
if (videoOptions) {
|
|
292
|
+
if (videoOptions.checkSmoothness) prompt += `
|
|
293
|
+
|
|
294
|
+
## 流畅度检测
|
|
295
|
+
流畅度阈值:${videoOptions.smoothnessThreshold || 60}/100
|
|
296
|
+
请评估动画流畅度并给出评分。`;
|
|
297
|
+
if (videoOptions.checkDuration && videoOptions.expectedDuration) prompt += `
|
|
298
|
+
|
|
299
|
+
## 时长检测
|
|
300
|
+
预期时长范围:${videoOptions.expectedDuration.min || 0}秒 - ${videoOptions.expectedDuration.max || '无限制'}秒`;
|
|
301
|
+
if (videoOptions.keyframes && videoOptions.keyframes.timestamps.length > 0) {
|
|
302
|
+
prompt += `
|
|
303
|
+
|
|
304
|
+
## 关键帧验证
|
|
305
|
+
需要在以下时间点验证画面:`;
|
|
306
|
+
videoOptions.keyframes.timestamps.forEach((ts, index)=>{
|
|
307
|
+
const desc = videoOptions.keyframes?.descriptions?.[index] || '未描述';
|
|
308
|
+
prompt += `
|
|
309
|
+
- ${ts}秒:${desc}`;
|
|
310
|
+
});
|
|
311
|
+
}
|
|
312
|
+
if (videoOptions.animationType && 'auto' !== videoOptions.animationType) prompt += `
|
|
313
|
+
|
|
314
|
+
## 动画类型
|
|
315
|
+
预期动画类型:${videoOptions.animationType}`;
|
|
316
|
+
}
|
|
317
|
+
prompt += `
|
|
318
|
+
|
|
319
|
+
## 输出要求
|
|
320
|
+
请以 JSON 格式输出对比结果:
|
|
321
|
+
{
|
|
322
|
+
"pass": boolean, // 断言是否通过
|
|
323
|
+
"thought": string, // 分析过程
|
|
324
|
+
"reason": string, // 失败原因(如果失败)
|
|
325
|
+
"videoDetails": { // 视频详情
|
|
326
|
+
"smoothnessScore": number, // 流畅度评分(0-100)
|
|
327
|
+
"duration": number, // 动画时长(秒)
|
|
328
|
+
"isComplete": boolean, // 动画是否完整
|
|
329
|
+
"keyframeMatches": [ // 关键帧匹配结果
|
|
330
|
+
{
|
|
331
|
+
"timestamp": number,
|
|
332
|
+
"matched": boolean,
|
|
333
|
+
"description": string
|
|
334
|
+
}
|
|
335
|
+
],
|
|
336
|
+
"detectedAnimationType": string, // 检测到的动画类型
|
|
337
|
+
"qualityAssessment": { // 质量评估
|
|
338
|
+
"hasStuttering": boolean, // 是否有卡顿
|
|
339
|
+
"hasFrameDropping": boolean, // 是否有跳帧
|
|
340
|
+
"averageFrameInterval": number, // 平均帧间隔(ms)
|
|
341
|
+
"frameIntervalStdDev": number // 帧间隔标准差
|
|
342
|
+
},
|
|
343
|
+
"acceptableDifferences": string[], // 可接受的差异列表
|
|
344
|
+
"unacceptableDifferences": string[] // 不可接受的差异列表
|
|
345
|
+
}
|
|
346
|
+
}`;
|
|
347
|
+
return prompt;
|
|
348
|
+
}
|
|
349
|
+
async function AiAssertVideo(options) {
|
|
350
|
+
const { currentVideoFrames, assertion, businessContext, videoOptions, modelConfig, abortSignal } = options;
|
|
351
|
+
const MAX_DURATION = 5;
|
|
352
|
+
const DEFAULT_FPS = 30;
|
|
353
|
+
const MAX_FRAMES = MAX_DURATION * DEFAULT_FPS;
|
|
354
|
+
if (currentVideoFrames.length > MAX_FRAMES) throw new Error(`Video frames exceed maximum limit. Maximum allowed: ${MAX_FRAMES} frames (${MAX_DURATION}s at ${DEFAULT_FPS}fps), got: ${currentVideoFrames.length} frames`);
|
|
355
|
+
const userContent = [];
|
|
356
|
+
userContent.push({
|
|
357
|
+
type: 'text',
|
|
358
|
+
text: `## 当前视频(共 ${currentVideoFrames.length} 帧,最大限制 ${MAX_FRAMES} 帧)
|
|
359
|
+
|
|
360
|
+
以下为当前录制视频的关键帧截图:`
|
|
361
|
+
});
|
|
362
|
+
const frameInterval = Math.max(1, Math.floor(currentVideoFrames.length / 10));
|
|
363
|
+
for(let i = 0; i < currentVideoFrames.length; i += frameInterval){
|
|
364
|
+
userContent.push({
|
|
365
|
+
type: 'text',
|
|
366
|
+
text: `帧 ${i + 1}/${currentVideoFrames.length}`
|
|
367
|
+
});
|
|
368
|
+
userContent.push({
|
|
369
|
+
type: 'image_url',
|
|
370
|
+
image_url: {
|
|
371
|
+
url: currentVideoFrames[i],
|
|
372
|
+
detail: 'low'
|
|
373
|
+
}
|
|
374
|
+
});
|
|
375
|
+
}
|
|
376
|
+
userContent.push({
|
|
377
|
+
type: 'text',
|
|
378
|
+
text: buildVideoPrompt({
|
|
379
|
+
assertion,
|
|
380
|
+
businessContext,
|
|
381
|
+
videoOptions
|
|
382
|
+
})
|
|
383
|
+
});
|
|
384
|
+
const msgs = [
|
|
385
|
+
{
|
|
386
|
+
role: 'system',
|
|
387
|
+
content: VIDEO_SYSTEM_PROMPT
|
|
388
|
+
},
|
|
389
|
+
{
|
|
390
|
+
role: 'user',
|
|
391
|
+
content: userContent
|
|
392
|
+
}
|
|
393
|
+
];
|
|
394
|
+
debug('calling AI for video assertion:', assertion);
|
|
395
|
+
try {
|
|
396
|
+
const result = await callAIWithObjectResponse(msgs, modelConfig, {
|
|
397
|
+
abortSignal
|
|
398
|
+
});
|
|
399
|
+
debug('video assertion result:', result.content);
|
|
400
|
+
return {
|
|
401
|
+
pass: result.content.pass,
|
|
402
|
+
thought: result.content.thought || '',
|
|
403
|
+
reason: result.content.reason,
|
|
404
|
+
videoDetails: result.content.videoDetails,
|
|
405
|
+
usage: result.usage,
|
|
406
|
+
rawResponse: JSON.stringify(result.content)
|
|
407
|
+
};
|
|
408
|
+
} catch (error) {
|
|
409
|
+
debug('video assertion error:', error);
|
|
410
|
+
throw error;
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
export { AiAssertDiff, AiAssertElement, AiAssertVideo };
|
|
414
|
+
|
|
415
|
+
//# sourceMappingURL=assert.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ai-model/assert.mjs","sources":["../../../src/ai-model/assert.ts"],"sourcesContent":["import type {\n AIUsageInfo,\n DiffDetails,\n IgnoreRegion,\n ReferenceImage,\n SystemCheckResults,\n VideoAssertionOptions,\n VideoDetails,\n} from '@/types';\nimport type { IModelConfig } from '@midscene/shared/env';\nimport { getDebug } from '@midscene/shared/logger';\nimport type { ChatCompletionUserMessageParam } from 'openai/resources/index';\nimport { callAIWithObjectResponse } from './service-caller/index';\n\nconst debug = getDebug('ai:assert');\n\nexport interface AiAssertOptions {\n beforeScreenshot?: string;\n afterScreenshot: string;\n assertion: string;\n businessContext?: string;\n enableSystemCheck?: boolean;\n customSystemCheckRules?: string;\n modelConfig: IModelConfig;\n abortSignal?: AbortSignal;\n referenceImages?: ReferenceImage[];\n diffThreshold?: number;\n ignoreRegions?: IgnoreRegion[];\n ignoreDynamicContent?: boolean;\n strictMode?: boolean;\n}\n\nexport interface AIAssertionResponse {\n pass: boolean;\n thought: string;\n reason?: string;\n systemCheckResults?: SystemCheckResults;\n diffDetails?: DiffDetails;\n}\n\nconst DEFAULT_SYSTEM_CHECK_PROMPT = `请检查以下系统级问题:\n\n1. **白屏检测**:页面是否完全白屏或大面积空白\n2. **布局遮挡**:重要元素是否存在组件间重叠遮挡\n3. **未加载内容**:页面是否有加载中的占位符、骨架屏\n4. **错误提示**:页面是否显示错误信息、异常提示\n5. **后端错误**:是否有后端请求失败的提示,如\"活动太火爆了\"、报错 Toast、错误码 等\n\n如果发现以上问题,请在 systemCheckResults 中标注对应字段为 true。`;\n\nfunction buildAssertionPrompt(options: {\n assertion: string;\n businessContext?: string;\n systemCheckPrompt?: string;\n}): string {\n const { assertion, businessContext, systemCheckPrompt } = options;\n\n let prompt = `## 用户断言描述\n${assertion}`;\n\n if (businessContext) {\n prompt += `\n\n## 业务知识上下文\n${businessContext}`;\n }\n\n if (systemCheckPrompt) {\n prompt += `\n\n## 系统校验规则\n${systemCheckPrompt}`;\n }\n\n prompt += `\n\n## 输出要求\n请以 JSON 格式输出断言结果:\n{\n \"pass\": boolean, // 断言是否通过\n \"thought\": string, // 思考过程\n \"reason\": string, // 失败原因(如果失败)\n \"systemCheckResults\": { // 系统校验结果(可选)\n \"whiteScreen\": boolean,\n \"layoutBlocked\": boolean,\n \"loadingContent\": boolean,\n \"errorPrompt\": boolean,\n \"backendError\": boolean\n }\n}`;\n\n return prompt;\n}\n\nexport async function AiAssertElement(options: AiAssertOptions): Promise<{\n pass: boolean;\n thought: string;\n reason?: string;\n usage?: AIUsageInfo;\n systemCheckResults?: SystemCheckResults;\n rawResponse?: string;\n}> {\n const {\n beforeScreenshot,\n afterScreenshot,\n assertion,\n businessContext,\n enableSystemCheck = true,\n customSystemCheckRules,\n modelConfig,\n abortSignal,\n } = options;\n\n const systemPrompt = `你是一个自动化测试断言专家。请根据以下信息判断断言是否通过。\n\n你需要:\n1. 分析操作前后的截图变化(如果提供了操作前截图)\n2. 验证用户的断言描述是否成立\n3. 检查是否存在系统级问题(如果启用)\n4. 给出详细的思考过程和判断结果`;\n\n const systemCheckPrompt = enableSystemCheck\n ? customSystemCheckRules || DEFAULT_SYSTEM_CHECK_PROMPT\n : '';\n\n const userContent: ChatCompletionUserMessageParam['content'] = [];\n\n if (beforeScreenshot) {\n userContent.push({\n type: 'text',\n text: '## 操作前截图(执行操作前的页面状态)',\n });\n userContent.push({\n type: 'image_url',\n image_url: { url: beforeScreenshot, detail: 'high' },\n });\n userContent.push({\n type: 'text',\n text: '## 操作后截图(执行操作后的页面状态)',\n });\n } else {\n userContent.push({\n type: 'text',\n text: '## 当前页面截图',\n });\n }\n\n userContent.push({\n type: 'image_url',\n image_url: { url: afterScreenshot, detail: 'high' },\n });\n\n userContent.push({\n type: 'text',\n text: buildAssertionPrompt({\n assertion,\n businessContext,\n systemCheckPrompt,\n }),\n });\n\n const msgs = [\n { role: 'system' as const, content: systemPrompt },\n { role: 'user' as const, content: userContent },\n ];\n\n debug('calling AI for assertion:', assertion);\n\n try {\n const result = await callAIWithObjectResponse<AIAssertionResponse>(\n msgs,\n modelConfig,\n { abortSignal },\n );\n\n debug('assertion result:', result.content);\n\n return {\n pass: result.content.pass,\n thought: result.content.thought || '',\n reason: result.content.reason,\n usage: result.usage,\n systemCheckResults: result.content.systemCheckResults,\n rawResponse: JSON.stringify(result.content),\n };\n } catch (error) {\n debug('assertion error:', error);\n throw error;\n }\n}\n\nconst DIFF_SYSTEM_PROMPT = `你是一个自动化测试的图像对比专家。你的任务是对比当前页面截图与基准图片,判断页面样式是否符合预期。\n\n你需要:\n1. 仔细对比两张图片的布局、颜色、元素位置\n2. 分析差异是否在可接受范围内\n3. 识别动态内容(如时间、日期)导致的差异\n4. 给出详细的对比分析和判断结果`;\n\nfunction buildDiffPrompt(options: {\n assertion: string;\n businessContext?: string;\n diffThreshold?: number;\n ignoreRegions?: IgnoreRegion[];\n ignoreDynamicContent?: boolean;\n strictMode?: boolean;\n}): string {\n const {\n assertion,\n businessContext,\n diffThreshold = 0.1,\n ignoreRegions,\n ignoreDynamicContent,\n strictMode,\n } = options;\n\n let prompt = `## 图像对比任务\n\n请对比基准图片(预期样式)和当前截图(实际样式),判断页面是否符合预期。\n\n## 用户断言描述\n${assertion}`;\n\n if (businessContext) {\n prompt += `\n\n## 业务知识上下文\n${businessContext}`;\n }\n\n prompt += `\n\n## 对比要求\n\n请从以下维度进行对比分析:\n\n1. **布局一致性**:页面布局是否与基准图片一致\n2. **颜色一致性**:主要颜色是否与基准图片一致\n3. **元素位置**:关键元素的位置是否与基准图片一致\n4. **文字内容**:文字内容是否与基准图片一致\n5. **图片资源**:图片、图标等资源是否正确加载\n\n## 差异阈值\n允许的差异阈值:${diffThreshold * 100}%`;\n\n if (ignoreRegions && ignoreRegions.length > 0) {\n prompt += `\n\n## 忽略对比的区域\n以下区域不参与对比:`;\n ignoreRegions.forEach((region, index) => {\n prompt += `\n${index + 1}. 位置 (${region.x}, ${region.y}),尺寸 ${region.width}x${region.height}`;\n });\n }\n\n if (ignoreDynamicContent) {\n prompt += `\n\n## 动态内容处理\n请忽略以下动态内容导致的差异:\n- 时间显示\n- 日期显示\n- 随机验证码\n- 动画效果`;\n }\n\n if (strictMode) {\n prompt += `\n\n## 严格模式\n当前为严格模式,任何差异都视为失败。`;\n }\n\n prompt += `\n\n## 输出要求\n请以 JSON 格式输出对比结果:\n{\n \"pass\": boolean, // 断言是否通过\n \"thought\": string, // 对比分析过程\n \"reason\": string, // 失败原因(如果失败)\n \"diffDetails\": { // 差异详情\n \"layoutMatch\": boolean, // 布局是否匹配\n \"colorMatch\": boolean, // 颜色是否匹配\n \"elementPositionMatch\": boolean, // 元素位置是否匹配\n \"textContentMatch\": boolean, // 文字内容是否匹配\n \"resourceMatch\": boolean, // 资源是否匹配\n \"acceptableDifferences\": string[], // 可接受的差异列表\n \"unacceptableDifferences\": string[] // 不可接受的差异列表\n }\n}`;\n\n return prompt;\n}\n\nexport async function AiAssertDiff(options: {\n currentScreenshot: string;\n referenceImages: ReferenceImage[];\n assertion: string;\n businessContext?: string;\n diffThreshold?: number;\n ignoreRegions?: IgnoreRegion[];\n ignoreDynamicContent?: boolean;\n strictMode?: boolean;\n modelConfig: IModelConfig;\n abortSignal?: AbortSignal;\n}): Promise<{\n pass: boolean;\n thought: string;\n reason?: string;\n diffDetails?: DiffDetails;\n usage?: AIUsageInfo;\n rawResponse?: string;\n}> {\n const {\n currentScreenshot,\n referenceImages,\n assertion,\n businessContext,\n diffThreshold,\n ignoreRegions,\n ignoreDynamicContent,\n strictMode,\n modelConfig,\n abortSignal,\n } = options;\n\n const userContent: ChatCompletionUserMessageParam['content'] = [];\n\n // 添加基准图片\n for (const refImage of referenceImages) {\n userContent.push({\n type: 'text',\n text: `## 基准图片:${refImage.name}`,\n });\n userContent.push({\n type: 'image_url',\n image_url: { url: refImage.url, detail: 'high' },\n });\n }\n\n // 添加当前截图\n userContent.push({\n type: 'text',\n text: '## 当前截图(实际样式)',\n });\n userContent.push({\n type: 'image_url',\n image_url: { url: currentScreenshot, detail: 'high' },\n });\n\n // 添加对比提示\n userContent.push({\n type: 'text',\n text: buildDiffPrompt({\n assertion,\n businessContext,\n diffThreshold,\n ignoreRegions,\n ignoreDynamicContent,\n strictMode,\n }),\n });\n\n const msgs = [\n { role: 'system' as const, content: DIFF_SYSTEM_PROMPT },\n { role: 'user' as const, content: userContent },\n ];\n\n debug('calling AI for diff assertion:', assertion);\n\n try {\n const result = await callAIWithObjectResponse<AIAssertionResponse>(\n msgs,\n modelConfig,\n { abortSignal },\n );\n\n debug('diff assertion result:', result.content);\n\n return {\n pass: result.content.pass,\n thought: result.content.thought || '',\n reason: result.content.reason,\n diffDetails: result.content.diffDetails,\n usage: result.usage,\n rawResponse: JSON.stringify(result.content),\n };\n } catch (error) {\n debug('diff assertion error:', error);\n throw error;\n }\n}\n\nconst VIDEO_SYSTEM_PROMPT = `你是一个自动化测试的视频/动画分析专家。你的任务是对比当前录制的视频与基准视频,判断动画效果是否符合预期。\n\n你需要:\n1. 分析视频中的动画流畅度、时长、完整性\n2. 对比基准视频与当前视频的差异\n3. 识别动画类型(过渡动画、加载动画、交互动画等)\n4. 检测动画质量问题(卡顿、跳帧等)\n5. 给出详细的分析过程和判断结果`;\n\nfunction buildVideoPrompt(options: {\n assertion: string;\n businessContext?: string;\n videoOptions?: VideoAssertionOptions;\n}): string {\n const { assertion, businessContext, videoOptions } = options;\n\n let prompt = `## 视频/动画对比任务\n\n请对比基准视频(预期动画效果)和当前录制的视频(实际动画效果),判断动画是否符合预期。\n\n## 用户断言描述\n${assertion}`;\n\n if (businessContext) {\n prompt += `\n\n## 业务知识上下文\n${businessContext}`;\n }\n\n prompt += `\n\n## 分析要求\n\n请从以下维度进行动画分析:\n\n1. **动画流畅度**:动画是否流畅,有无卡顿、跳帧\n2. **动画时长**:动画时长是否在预期范围内\n3. **动画完整性**:动画是否完整执行,有无中断\n4. **动画类型**:识别动画类型(过渡、加载、交互等)\n5. **关键帧匹配**:关键时间点的画面是否符合预期`;\n\n if (videoOptions) {\n if (videoOptions.checkSmoothness) {\n prompt += `\n\n## 流畅度检测\n流畅度阈值:${videoOptions.smoothnessThreshold || 60}/100\n请评估动画流畅度并给出评分。`;\n }\n\n if (videoOptions.checkDuration && videoOptions.expectedDuration) {\n prompt += `\n\n## 时长检测\n预期时长范围:${videoOptions.expectedDuration.min || 0}秒 - ${videoOptions.expectedDuration.max || '无限制'}秒`;\n }\n\n if (\n videoOptions.keyframes &&\n videoOptions.keyframes.timestamps.length > 0\n ) {\n prompt += `\n\n## 关键帧验证\n需要在以下时间点验证画面:`;\n videoOptions.keyframes.timestamps.forEach((ts, index) => {\n const desc = videoOptions.keyframes?.descriptions?.[index] || '未描述';\n prompt += `\n- ${ts}秒:${desc}`;\n });\n }\n\n if (videoOptions.animationType && videoOptions.animationType !== 'auto') {\n prompt += `\n\n## 动画类型\n预期动画类型:${videoOptions.animationType}`;\n }\n }\n\n prompt += `\n\n## 输出要求\n请以 JSON 格式输出对比结果:\n{\n \"pass\": boolean, // 断言是否通过\n \"thought\": string, // 分析过程\n \"reason\": string, // 失败原因(如果失败)\n \"videoDetails\": { // 视频详情\n \"smoothnessScore\": number, // 流畅度评分(0-100)\n \"duration\": number, // 动画时长(秒)\n \"isComplete\": boolean, // 动画是否完整\n \"keyframeMatches\": [ // 关键帧匹配结果\n {\n \"timestamp\": number,\n \"matched\": boolean,\n \"description\": string\n }\n ],\n \"detectedAnimationType\": string, // 检测到的动画类型\n \"qualityAssessment\": { // 质量评估\n \"hasStuttering\": boolean, // 是否有卡顿\n \"hasFrameDropping\": boolean, // 是否有跳帧\n \"averageFrameInterval\": number, // 平均帧间隔(ms)\n \"frameIntervalStdDev\": number // 帧间隔标准差\n },\n \"acceptableDifferences\": string[], // 可接受的差异列表\n \"unacceptableDifferences\": string[] // 不可接受的差异列表\n }\n}`;\n\n return prompt;\n}\n\nexport async function AiAssertVideo(options: {\n currentVideoFrames: string[];\n assertion: string;\n businessContext?: string;\n videoOptions?: VideoAssertionOptions;\n modelConfig: IModelConfig;\n abortSignal?: AbortSignal;\n}): Promise<{\n pass: boolean;\n thought: string;\n reason?: string;\n videoDetails?: VideoDetails;\n usage?: AIUsageInfo;\n rawResponse?: string;\n}> {\n const {\n currentVideoFrames,\n assertion,\n businessContext,\n videoOptions,\n modelConfig,\n abortSignal,\n } = options;\n\n const MAX_DURATION = 5;\n const DEFAULT_FPS = 30;\n const MAX_FRAMES = MAX_DURATION * DEFAULT_FPS;\n\n if (currentVideoFrames.length > MAX_FRAMES) {\n throw new Error(\n `Video frames exceed maximum limit. Maximum allowed: ${MAX_FRAMES} frames (${MAX_DURATION}s at ${DEFAULT_FPS}fps), got: ${currentVideoFrames.length} frames`,\n );\n }\n\n const userContent: ChatCompletionUserMessageParam['content'] = [];\n\n userContent.push({\n type: 'text',\n text: `## 当前视频(共 ${currentVideoFrames.length} 帧,最大限制 ${MAX_FRAMES} 帧)\n\n以下为当前录制视频的关键帧截图:`,\n });\n\n const frameInterval = Math.max(1, Math.floor(currentVideoFrames.length / 10));\n for (let i = 0; i < currentVideoFrames.length; i += frameInterval) {\n userContent.push({\n type: 'text',\n text: `帧 ${i + 1}/${currentVideoFrames.length}`,\n });\n userContent.push({\n type: 'image_url',\n image_url: { url: currentVideoFrames[i], detail: 'low' },\n });\n }\n\n userContent.push({\n type: 'text',\n text: buildVideoPrompt({\n assertion,\n businessContext,\n videoOptions,\n }),\n });\n\n const msgs = [\n { role: 'system' as const, content: VIDEO_SYSTEM_PROMPT },\n { role: 'user' as const, content: userContent },\n ];\n\n debug('calling AI for video assertion:', assertion);\n\n try {\n const result = await callAIWithObjectResponse<{\n pass: boolean;\n thought: string;\n reason?: string;\n videoDetails?: VideoDetails;\n }>(msgs, modelConfig, { abortSignal });\n\n debug('video assertion result:', result.content);\n\n return {\n pass: result.content.pass,\n thought: result.content.thought || '',\n reason: result.content.reason,\n videoDetails: result.content.videoDetails,\n usage: result.usage,\n rawResponse: JSON.stringify(result.content),\n };\n } catch (error) {\n debug('video assertion error:', error);\n throw error;\n }\n}\n"],"names":["debug","getDebug","DEFAULT_SYSTEM_CHECK_PROMPT","buildAssertionPrompt","options","assertion","businessContext","systemCheckPrompt","prompt","AiAssertElement","beforeScreenshot","afterScreenshot","enableSystemCheck","customSystemCheckRules","modelConfig","abortSignal","systemPrompt","userContent","msgs","result","callAIWithObjectResponse","JSON","error","DIFF_SYSTEM_PROMPT","buildDiffPrompt","diffThreshold","ignoreRegions","ignoreDynamicContent","strictMode","region","index","AiAssertDiff","currentScreenshot","referenceImages","refImage","VIDEO_SYSTEM_PROMPT","buildVideoPrompt","videoOptions","ts","desc","AiAssertVideo","currentVideoFrames","MAX_DURATION","DEFAULT_FPS","MAX_FRAMES","Error","frameInterval","Math","i"],"mappings":"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"}
|
|
@@ -3,9 +3,10 @@ import { systemPromptToLocateElement } from "./prompt/llm-locator.mjs";
|
|
|
3
3
|
import { generatePlaywrightTest, generatePlaywrightTestStream } from "./prompt/playwright-generator.mjs";
|
|
4
4
|
import { generateYamlTest, generateYamlTestStream } from "./prompt/yaml-generator.mjs";
|
|
5
5
|
import { AiExtractElementInfo, AiJudgeOrderSensitive, AiLocateElement, AiLocateSection } from "./inspect.mjs";
|
|
6
|
+
import { AiAssertDiff, AiAssertElement } from "./assert.mjs";
|
|
6
7
|
import { plan } from "./llm-planning.mjs";
|
|
7
8
|
import { autoGLMPlanning } from "./auto-glm/planning.mjs";
|
|
8
9
|
import { PointSchema, RectSchema, SizeSchema, TMultimodalPromptSchema, TUserPromptSchema, adaptBboxToRect, dumpActionParam, findAllMidsceneLocatorField, getMidsceneLocationSchema, parseActionParam } from "../common.mjs";
|
|
9
10
|
import { uiTarsPlanning } from "./ui-tars-planning.mjs";
|
|
10
11
|
import { ConversationHistory } from "./conversation-history.mjs";
|
|
11
|
-
export { AIResponseParseError, AiExtractElementInfo, AiJudgeOrderSensitive, AiLocateElement, AiLocateSection, ConversationHistory, PointSchema, RectSchema, SizeSchema, TMultimodalPromptSchema, TUserPromptSchema, adaptBboxToRect, autoGLMPlanning, callAI, callAIWithObjectResponse, callAIWithStringResponse, dumpActionParam, findAllMidsceneLocatorField, generatePlaywrightTest, generatePlaywrightTestStream, generateYamlTest, generateYamlTestStream, getMidsceneLocationSchema, parseActionParam, plan, systemPromptToLocateElement, uiTarsPlanning };
|
|
12
|
+
export { AIResponseParseError, AiAssertDiff, AiAssertElement, AiExtractElementInfo, AiJudgeOrderSensitive, AiLocateElement, AiLocateSection, ConversationHistory, PointSchema, RectSchema, SizeSchema, TMultimodalPromptSchema, TUserPromptSchema, adaptBboxToRect, autoGLMPlanning, callAI, callAIWithObjectResponse, callAIWithStringResponse, dumpActionParam, findAllMidsceneLocatorField, generatePlaywrightTest, generatePlaywrightTestStream, generateYamlTest, generateYamlTestStream, getMidsceneLocationSchema, parseActionParam, plan, systemPromptToLocateElement, uiTarsPlanning };
|
package/dist/es/device/index.mjs
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"device/index.mjs","sources":["../../../src/device/index.ts"],"sourcesContent":["import { getMidsceneLocationSchema } from '@/common';\nimport type {\n ActionScrollParam,\n DeviceAction,\n LocateResultElement,\n} from '@/types';\nimport type { IModelConfig } from '@midscene/shared/env';\nimport type { ElementNode } from '@midscene/shared/extractor';\nimport { getDebug } from '@midscene/shared/logger';\nimport { _keyDefinitions } from '@midscene/shared/us-keyboard-layout';\nimport { z } from 'zod';\nimport type { ElementCacheFeature, Rect, Size, UIContext } from '../types';\n\nexport interface FileChooserHandler {\n accept(files: string[]): Promise<void>;\n}\n\nexport abstract class AbstractInterface {\n abstract interfaceType: string;\n\n abstract screenshotBase64(): Promise<string>;\n abstract size(): Promise<Size>;\n abstract actionSpace(): DeviceAction[];\n\n abstract cacheFeatureForPoint?(\n center: [number, number],\n options?: {\n targetDescription?: string;\n modelConfig?: IModelConfig;\n },\n ): Promise<ElementCacheFeature>;\n abstract rectMatchesCacheFeature?(\n feature: ElementCacheFeature,\n ): Promise<Rect>;\n\n abstract destroy?(): Promise<void>;\n\n abstract describe?(): string;\n abstract beforeInvokeAction?(actionName: string, param: any): Promise<void>;\n abstract afterInvokeAction?(actionName: string, param: any): Promise<void>;\n\n // for web only\n registerFileChooserListener?(\n handler: (chooser: FileChooserHandler) => Promise<void>,\n ): Promise<{ dispose: () => void; getError: () => Error | undefined }>;\n\n // @deprecated do NOT extend this method\n abstract getElementsNodeTree?: () => Promise<ElementNode>;\n\n // @deprecated do NOT extend this method\n abstract url?: () => string | Promise<string>;\n\n // @deprecated do NOT extend this method\n abstract evaluateJavaScript?<T = any>(script: string): Promise<T>;\n\n /**\n * Get the current time from the device.\n * Returns the device's current timestamp in milliseconds.\n * This is useful when the system time and device time are not synchronized.\n */\n getTimestamp?(): Promise<number>;\n\n /** URL of native MJPEG stream for real-time screen preview (e.g. WDA MJPEG server) */\n mjpegStreamUrl?: string;\n}\n\n// Generic function to define actions with proper type inference\n// TRuntime allows specifying a different type for the runtime parameter (after location resolution)\n// TReturn allows specifying the return type of the action\nexport const defineAction = <\n TSchema extends z.ZodType | undefined = undefined,\n TRuntime = TSchema extends z.ZodType ? z.infer<TSchema> : undefined,\n TReturn = any,\n>(\n config: {\n name: string;\n description: string;\n interfaceAlias?: string;\n paramSchema?: TSchema;\n call: (param: TRuntime) => Promise<TReturn> | TReturn;\n } & Partial<\n Omit<\n DeviceAction<TRuntime, TReturn>,\n 'name' | 'description' | 'interfaceAlias' | 'paramSchema' | 'call'\n >\n >,\n): DeviceAction<TRuntime, TReturn> => {\n return config as any; // Type assertion needed because schema validation type differs from runtime type\n};\n\n// Tap\nexport const actionTapParamSchema = z.object({\n locate: getMidsceneLocationSchema().describe('The element to be tapped'),\n});\nexport type ActionTapParam = {\n locate: LocateResultElement;\n};\n\nexport const defineActionTap = (\n call: (param: ActionTapParam) => Promise<void>,\n): DeviceAction<ActionTapParam> => {\n return defineAction<typeof actionTapParamSchema, ActionTapParam>({\n name: 'Tap',\n description: 'Tap the element',\n interfaceAlias: 'aiTap',\n paramSchema: actionTapParamSchema,\n sample: {\n locate: { prompt: 'the \"Submit\" button' },\n },\n call,\n });\n};\n\n// RightClick\nexport const actionRightClickParamSchema = z.object({\n locate: getMidsceneLocationSchema().describe(\n 'The element to be right clicked',\n ),\n});\nexport type ActionRightClickParam = {\n locate: LocateResultElement;\n};\n\nexport const defineActionRightClick = (\n call: (param: ActionRightClickParam) => Promise<void>,\n): DeviceAction<ActionRightClickParam> => {\n return defineAction<\n typeof actionRightClickParamSchema,\n ActionRightClickParam\n >({\n name: 'RightClick',\n description: 'Right click the element',\n interfaceAlias: 'aiRightClick',\n paramSchema: actionRightClickParamSchema,\n sample: {\n locate: { prompt: 'the file icon on the desktop' },\n },\n call,\n });\n};\n\n// DoubleClick\nexport const actionDoubleClickParamSchema = z.object({\n locate: getMidsceneLocationSchema().describe(\n 'The element to be double clicked',\n ),\n});\nexport type ActionDoubleClickParam = {\n locate: LocateResultElement;\n};\n\nexport const defineActionDoubleClick = (\n call: (param: ActionDoubleClickParam) => Promise<void>,\n): DeviceAction<ActionDoubleClickParam> => {\n return defineAction<\n typeof actionDoubleClickParamSchema,\n ActionDoubleClickParam\n >({\n name: 'DoubleClick',\n description: 'Double click the element',\n interfaceAlias: 'aiDoubleClick',\n paramSchema: actionDoubleClickParamSchema,\n sample: {\n locate: { prompt: 'the folder icon' },\n },\n call,\n });\n};\n\n// Hover\nexport const actionHoverParamSchema = z.object({\n locate: getMidsceneLocationSchema().describe('The element to be hovered'),\n});\nexport type ActionHoverParam = {\n locate: LocateResultElement;\n};\n\nexport const defineActionHover = (\n call: (param: ActionHoverParam) => Promise<void>,\n): DeviceAction<ActionHoverParam> => {\n return defineAction<typeof actionHoverParamSchema, ActionHoverParam>({\n name: 'Hover',\n description: 'Move the mouse to the element',\n interfaceAlias: 'aiHover',\n paramSchema: actionHoverParamSchema,\n sample: {\n locate: { prompt: 'the navigation menu item \"Products\"' },\n },\n call,\n });\n};\n\n// Input\nconst inputLocateDescription =\n 'the position of the placeholder or text content in the target input field. If there is no content, locate the center of the input field.';\nexport const actionInputParamSchema = z.object({\n value: z\n .union([z.string(), z.number()])\n .transform((val) => String(val))\n .describe(\n 'The text to input. Provide the final content for replace/append modes, or an empty string when using clear mode to remove existing text.',\n ),\n locate: getMidsceneLocationSchema()\n .describe(inputLocateDescription)\n .optional(),\n mode: z\n .enum(['replace', 'clear', 'typeOnly'])\n .default('replace')\n .describe(\n 'Input mode: \"replace\" (default) - clear the field and input the value; \"typeOnly\" - type the value directly without clearing the field first; \"clear\" - clear the field without inputting new text.',\n ),\n});\nexport type ActionInputParam = {\n value: string;\n locate?: LocateResultElement;\n mode?: 'replace' | 'clear' | 'typeOnly' | 'append';\n};\n\nexport const defineActionInput = (\n call: (param: ActionInputParam) => Promise<void>,\n): DeviceAction<ActionInputParam> => {\n return defineAction<typeof actionInputParamSchema, ActionInputParam>({\n name: 'Input',\n description: 'Input the value into the element',\n interfaceAlias: 'aiInput',\n paramSchema: actionInputParamSchema,\n sample: {\n value: 'test@example.com',\n locate: { prompt: 'the email input field' },\n },\n call: (param) => {\n // backward compat: convert deprecated 'append' to 'typeOnly'\n if ((param.mode as string) === 'append') {\n param.mode = 'typeOnly';\n }\n return call(param);\n },\n });\n};\n\n// KeyboardPress\nexport const actionKeyboardPressParamSchema = z.object({\n locate: getMidsceneLocationSchema()\n .describe('The element to be clicked before pressing the key')\n .optional(),\n keyName: z\n .string()\n .describe(\n \"The key to be pressed. Use '+' for key combinations, e.g., 'Control+A', 'Shift+Enter'\",\n ),\n});\nexport type ActionKeyboardPressParam = {\n locate?: LocateResultElement;\n keyName: string;\n};\n\nexport const defineActionKeyboardPress = (\n call: (param: ActionKeyboardPressParam) => Promise<void>,\n): DeviceAction<ActionKeyboardPressParam> => {\n return defineAction<\n typeof actionKeyboardPressParamSchema,\n ActionKeyboardPressParam\n >({\n name: 'KeyboardPress',\n description:\n 'Press a key or key combination, like \"Enter\", \"Tab\", \"Escape\", or \"Control+A\", \"Shift+Enter\". Do not use this to type text.',\n interfaceAlias: 'aiKeyboardPress',\n paramSchema: actionKeyboardPressParamSchema,\n sample: {\n keyName: 'Enter',\n },\n call,\n });\n};\n\n// Scroll\nexport const actionScrollParamSchema = z.object({\n scrollType: z\n .enum([\n 'singleAction',\n 'scrollToBottom',\n 'scrollToTop',\n 'scrollToRight',\n 'scrollToLeft',\n ])\n .default('singleAction')\n .describe(\n 'The scroll behavior: \"singleAction\" for a single scroll action, \"scrollToBottom\" for scrolling all the way to the bottom by rapidly scrolling 5-10 times (skipping intermediate content until reaching the bottom), \"scrollToTop\" for scrolling all the way to the top by rapidly scrolling 5-10 times (skipping intermediate content until reaching the top), \"scrollToRight\" for scrolling all the way to the right by rapidly scrolling multiple times, \"scrollToLeft\" for scrolling all the way to the left by rapidly scrolling multiple times',\n ),\n direction: z\n .enum(['down', 'up', 'right', 'left'])\n .default('down')\n .describe(\n 'The direction to scroll. Only effective when scrollType is \"singleAction\".',\n ),\n distance: z\n .number()\n .nullable()\n .optional()\n .describe('The distance in pixels to scroll'),\n locate: getMidsceneLocationSchema()\n .optional()\n .describe(\n 'Describe the target element to be scrolled on, like \"the table\" or \"the list\" or \"the content area\" or \"the scrollable area\". Do NOT provide a general intent like \"scroll to find some element\"',\n ),\n});\n\nexport const defineActionScroll = (\n call: (param: ActionScrollParam) => Promise<void>,\n): DeviceAction<ActionScrollParam> => {\n return defineAction<typeof actionScrollParamSchema, ActionScrollParam>({\n name: 'Scroll',\n description:\n 'Scroll the page or a scrollable element to browse content. This is the preferred way to scroll on all platforms, including mobile. Supports scrollToBottom/scrollToTop for boundary navigation. Default: direction `down`, scrollType `singleAction`, distance `null`.',\n interfaceAlias: 'aiScroll',\n paramSchema: actionScrollParamSchema,\n sample: {\n direction: 'down',\n scrollType: 'singleAction',\n locate: { prompt: 'the center of the product list area' },\n },\n call,\n });\n};\n\n// DragAndDrop\nexport const actionDragAndDropParamSchema = z.object({\n from: getMidsceneLocationSchema().describe('The position to be dragged'),\n to: getMidsceneLocationSchema().describe('The position to be dropped'),\n});\nexport type ActionDragAndDropParam = {\n from: LocateResultElement;\n to: LocateResultElement;\n};\n\nexport const defineActionDragAndDrop = (\n call: (param: ActionDragAndDropParam) => Promise<void>,\n): DeviceAction<ActionDragAndDropParam> => {\n return defineAction<\n typeof actionDragAndDropParamSchema,\n ActionDragAndDropParam\n >({\n name: 'DragAndDrop',\n description:\n 'Pick up a specific UI element and move it to a new position (e.g., reorder a card, move a file into a folder, sort list items). The element itself moves with your finger/mouse.',\n interfaceAlias: 'aiDragAndDrop',\n paramSchema: actionDragAndDropParamSchema,\n sample: {\n from: { prompt: 'the \"report.pdf\" file icon' },\n to: { prompt: 'the upload drop zone' },\n },\n call,\n });\n};\n\nexport const ActionLongPressParamSchema = z.object({\n locate: getMidsceneLocationSchema().describe(\n 'The element to be long pressed',\n ),\n duration: z\n .number()\n .default(500)\n .optional()\n .describe('Long press duration in milliseconds'),\n});\n\nexport type ActionLongPressParam = {\n locate: LocateResultElement;\n duration?: number;\n};\nexport const defineActionLongPress = (\n call: (param: ActionLongPressParam) => Promise<void>,\n): DeviceAction<ActionLongPressParam> => {\n return defineAction<typeof ActionLongPressParamSchema, ActionLongPressParam>({\n name: 'LongPress',\n description: 'Long press the element',\n paramSchema: ActionLongPressParamSchema,\n sample: {\n locate: { prompt: 'the message bubble' },\n },\n call,\n });\n};\n\nexport const ActionSwipeParamSchema = z.object({\n start: getMidsceneLocationSchema()\n .optional()\n .describe(\n 'Starting point of the swipe gesture, if not specified, the center of the page will be used',\n ),\n direction: z\n .enum(['up', 'down', 'left', 'right'])\n .optional()\n .describe(\n 'The direction to swipe (required when using distance). The direction means the direction of the finger swipe.',\n ),\n distance: z\n .number()\n .optional()\n .describe('The distance in pixels to swipe (mutually exclusive with end)'),\n end: getMidsceneLocationSchema()\n .optional()\n .describe(\n 'Ending point of the swipe gesture (mutually exclusive with distance)',\n ),\n duration: z\n .number()\n .default(300)\n .describe('Duration of the swipe gesture in milliseconds'),\n repeat: z\n .number()\n .optional()\n .describe(\n 'The number of times to repeat the swipe gesture. 1 for default, 0 for infinite (e.g. endless swipe until the end of the page)',\n ),\n});\n\nexport type ActionSwipeParam = {\n start?: LocateResultElement;\n direction?: 'up' | 'down' | 'left' | 'right';\n distance?: number;\n end?: LocateResultElement;\n duration?: number;\n repeat?: number;\n};\n\nexport function normalizeMobileSwipeParam(\n param: ActionSwipeParam,\n screenSize: { width: number; height: number },\n): {\n startPoint: { x: number; y: number };\n endPoint: { x: number; y: number };\n duration: number;\n repeatCount: number;\n} {\n const { width, height } = screenSize;\n const { start, end } = param;\n\n const startPoint = start\n ? { x: start.center[0], y: start.center[1] }\n : { x: width / 2, y: height / 2 };\n\n let endPoint: { x: number; y: number };\n\n if (end) {\n endPoint = { x: end.center[0], y: end.center[1] };\n } else if (param.distance) {\n const direction = param.direction;\n if (!direction) {\n throw new Error('direction is required for swipe gesture');\n }\n endPoint = {\n x:\n startPoint.x +\n (direction === 'right'\n ? param.distance\n : direction === 'left'\n ? -param.distance\n : 0),\n y:\n startPoint.y +\n (direction === 'down'\n ? param.distance\n : direction === 'up'\n ? -param.distance\n : 0),\n };\n } else {\n throw new Error(\n 'Either end or distance must be specified for swipe gesture',\n );\n }\n\n endPoint.x = Math.max(0, Math.min(endPoint.x, width));\n endPoint.y = Math.max(0, Math.min(endPoint.y, height));\n\n const duration = param.duration ?? 300;\n\n let repeatCount = typeof param.repeat === 'number' ? param.repeat : 1;\n if (repeatCount === 0) {\n repeatCount = 10;\n }\n\n return { startPoint, endPoint, duration, repeatCount };\n}\n\nexport const defineActionSwipe = (\n call: (param: ActionSwipeParam) => Promise<void>,\n): DeviceAction<ActionSwipeParam> => {\n return defineAction<typeof ActionSwipeParamSchema, ActionSwipeParam>({\n name: 'Swipe',\n description:\n 'Perform a touch gesture for interactions beyond regular scrolling (e.g., flip pages in a carousel, dismiss a notification, swipe-to-delete a list item). For regular content scrolling, use Scroll instead. Use \"distance\" + \"direction\" for relative movement, or \"end\" for precise endpoint.',\n paramSchema: ActionSwipeParamSchema,\n sample: {\n start: { prompt: 'center of the notification' },\n end: { prompt: 'upper edge of the screen' },\n },\n call,\n });\n};\n\n// ClearInput\nexport const actionClearInputParamSchema = z.object({\n locate: getMidsceneLocationSchema()\n .describe('The input field to be cleared')\n .optional(),\n});\nexport type ActionClearInputParam = {\n locate?: LocateResultElement;\n};\n\nexport const defineActionClearInput = (\n call: (param: ActionClearInputParam) => Promise<void>,\n): DeviceAction<ActionClearInputParam> => {\n return defineAction<\n typeof actionClearInputParamSchema,\n ActionClearInputParam\n >({\n name: 'ClearInput',\n description: inputLocateDescription,\n interfaceAlias: 'aiClearInput',\n paramSchema: actionClearInputParamSchema,\n sample: {\n locate: { prompt: 'the search input field' },\n },\n call,\n });\n};\n\n// CursorMove\nexport const actionCursorMoveParamSchema = z.object({\n direction: z\n .enum(['left', 'right'])\n .describe('The direction to move the cursor'),\n times: z\n .number()\n .int()\n .min(1)\n .default(1)\n .describe(\n 'The number of times to move the cursor in the specified direction',\n ),\n});\nexport type ActionCursorMoveParam = {\n direction: 'left' | 'right';\n times?: number;\n};\n\nexport const defineActionCursorMove = (\n call: (param: ActionCursorMoveParam) => Promise<void>,\n): DeviceAction<ActionCursorMoveParam> => {\n return defineAction<\n typeof actionCursorMoveParamSchema,\n ActionCursorMoveParam\n >({\n name: 'CursorMove',\n description:\n 'Move the text cursor (caret) left or right within an input field or text area. Use this to reposition the cursor without selecting text.',\n paramSchema: actionCursorMoveParamSchema,\n sample: {\n direction: 'left',\n times: 3,\n },\n call,\n });\n};\n// Sleep\nexport const ActionSleepParamSchema = z.object({\n timeMs: z\n .number()\n .default(1000)\n .optional()\n .describe('Sleep duration in milliseconds, defaults to 1000ms (1 second)'),\n});\n\nexport type ActionSleepParam = {\n timeMs?: number;\n};\n\nexport const defineActionSleep = (): DeviceAction<ActionSleepParam> => {\n return defineAction<typeof ActionSleepParamSchema, ActionSleepParam>({\n name: 'Sleep',\n description:\n 'Wait for a specified duration before continuing. Defaults to 1 second (1000ms) if not specified.',\n paramSchema: ActionSleepParamSchema,\n sample: {\n timeMs: 2000,\n },\n call: async (param) => {\n const duration = param?.timeMs ?? 1000;\n getDebug('device:common-action')(`Sleeping for ${duration}ms`);\n await new Promise((resolve) => setTimeout(resolve, duration));\n },\n });\n};\n\nexport type { DeviceAction } from '../types';\nexport type {\n AndroidDeviceOpt,\n AndroidDeviceInputOpt,\n IOSDeviceOpt,\n IOSDeviceInputOpt,\n HarmonyDeviceOpt,\n HarmonyDeviceInputOpt,\n} from './device-options';\n"],"names":["AbstractInterface","defineAction","config","actionTapParamSchema","z","getMidsceneLocationSchema","defineActionTap","call","actionRightClickParamSchema","defineActionRightClick","actionDoubleClickParamSchema","defineActionDoubleClick","actionHoverParamSchema","defineActionHover","inputLocateDescription","actionInputParamSchema","val","String","defineActionInput","param","actionKeyboardPressParamSchema","defineActionKeyboardPress","actionScrollParamSchema","defineActionScroll","actionDragAndDropParamSchema","defineActionDragAndDrop","ActionLongPressParamSchema","defineActionLongPress","ActionSwipeParamSchema","normalizeMobileSwipeParam","screenSize","width","height","start","end","startPoint","endPoint","direction","Error","Math","duration","repeatCount","defineActionSwipe","actionClearInputParamSchema","defineActionClearInput","actionCursorMoveParamSchema","defineActionCursorMove","ActionSleepParamSchema","defineActionSleep","getDebug","Promise","resolve","setTimeout"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"device/index.mjs","sources":["../../../src/device/index.ts"],"sourcesContent":["import { getMidsceneLocationSchema } from '@/common';\nimport type {\n ActionScrollParam,\n DeviceAction,\n LocateResultElement,\n} from '@/types';\nimport type { IModelConfig } from '@midscene/shared/env';\nimport type { ElementNode } from '@midscene/shared/extractor';\nimport { getDebug } from '@midscene/shared/logger';\nimport { _keyDefinitions } from '@midscene/shared/us-keyboard-layout';\nimport { z } from 'zod';\nimport type { ElementCacheFeature, Rect, Size, UIContext } from '../types';\n\nexport interface FileChooserHandler {\n accept(files: string[]): Promise<void>;\n}\n\nexport abstract class AbstractInterface {\n abstract interfaceType: string;\n\n abstract screenshotBase64(): Promise<string>;\n abstract size(): Promise<Size>;\n abstract actionSpace(): DeviceAction[];\n\n abstract cacheFeatureForPoint?(\n center: [number, number],\n options?: {\n targetDescription?: string;\n modelConfig?: IModelConfig;\n },\n ): Promise<ElementCacheFeature>;\n abstract rectMatchesCacheFeature?(\n feature: ElementCacheFeature,\n ): Promise<Rect>;\n\n abstract destroy?(): Promise<void>;\n\n abstract describe?(): string;\n abstract beforeInvokeAction?(actionName: string, param: any): Promise<void>;\n abstract afterInvokeAction?(actionName: string, param: any): Promise<void>;\n\n // for web only\n registerFileChooserListener?(\n handler: (chooser: FileChooserHandler) => Promise<void>,\n ): Promise<{ dispose: () => void; getError: () => Error | undefined }>;\n\n // @deprecated do NOT extend this method\n abstract getElementsNodeTree?: () => Promise<ElementNode>;\n\n // @deprecated do NOT extend this method\n abstract url?: () => string | Promise<string>;\n\n // @deprecated do NOT extend this method\n abstract evaluateJavaScript?<T = any>(script: string): Promise<T>;\n\n /**\n * Get the current time from the device.\n * Returns the device's current timestamp in milliseconds.\n * This is useful when the system time and device time are not synchronized.\n */\n getTimestamp?(): Promise<number>;\n\n /** URL of native MJPEG stream for real-time screen preview (e.g. WDA MJPEG server) */\n mjpegStreamUrl?: string;\n}\n\n// Generic function to define actions with proper type inference\n// TRuntime allows specifying a different type for the runtime parameter (after location resolution)\n// TReturn allows specifying the return type of the action\nexport const defineAction = <\n TSchema extends z.ZodType | undefined = undefined,\n TRuntime = TSchema extends z.ZodType ? z.infer<TSchema> : undefined,\n TReturn = any,\n>(\n config: {\n name: string;\n description: string;\n interfaceAlias?: string;\n paramSchema?: TSchema;\n call: (param: TRuntime) => Promise<TReturn> | TReturn;\n } & Partial<\n Omit<\n DeviceAction<TRuntime, TReturn>,\n 'name' | 'description' | 'interfaceAlias' | 'paramSchema' | 'call'\n >\n >,\n): DeviceAction<TRuntime, TReturn> => {\n return config as any; // Type assertion needed because schema validation type differs from runtime type\n};\n\n// Tap\nexport const actionTapParamSchema = z.object({\n locate: getMidsceneLocationSchema().describe('The element to be tapped'),\n});\nexport type ActionTapParam = {\n locate: LocateResultElement;\n};\n\nexport const defineActionTap = (\n call: (param: ActionTapParam) => Promise<void>,\n): DeviceAction<ActionTapParam> => {\n return defineAction<typeof actionTapParamSchema, ActionTapParam>({\n name: 'Tap',\n description: 'Tap the element',\n interfaceAlias: 'aiTap',\n paramSchema: actionTapParamSchema,\n sample: {\n locate: { prompt: 'the \"Submit\" button' },\n },\n call,\n });\n};\n\n// RightClick\nexport const actionRightClickParamSchema = z.object({\n locate: getMidsceneLocationSchema().describe(\n 'The element to be right clicked',\n ),\n});\nexport type ActionRightClickParam = {\n locate: LocateResultElement;\n};\n\nexport const defineActionRightClick = (\n call: (param: ActionRightClickParam) => Promise<void>,\n): DeviceAction<ActionRightClickParam> => {\n return defineAction<\n typeof actionRightClickParamSchema,\n ActionRightClickParam\n >({\n name: 'RightClick',\n description: 'Right click the element',\n interfaceAlias: 'aiRightClick',\n paramSchema: actionRightClickParamSchema,\n sample: {\n locate: { prompt: 'the file icon on the desktop' },\n },\n call,\n });\n};\n\n// DoubleClick\nexport const actionDoubleClickParamSchema = z.object({\n locate: getMidsceneLocationSchema().describe(\n 'The element to be double clicked',\n ),\n});\nexport type ActionDoubleClickParam = {\n locate: LocateResultElement;\n};\n\nexport const defineActionDoubleClick = (\n call: (param: ActionDoubleClickParam) => Promise<void>,\n): DeviceAction<ActionDoubleClickParam> => {\n return defineAction<\n typeof actionDoubleClickParamSchema,\n ActionDoubleClickParam\n >({\n name: 'DoubleClick',\n description: 'Double click the element',\n interfaceAlias: 'aiDoubleClick',\n paramSchema: actionDoubleClickParamSchema,\n sample: {\n locate: { prompt: 'the folder icon' },\n },\n call,\n });\n};\n\n// Hover\nexport const actionHoverParamSchema = z.object({\n locate: getMidsceneLocationSchema().describe('The element to be hovered'),\n});\nexport type ActionHoverParam = {\n locate: LocateResultElement;\n};\n\nexport const defineActionHover = (\n call: (param: ActionHoverParam) => Promise<void>,\n): DeviceAction<ActionHoverParam> => {\n return defineAction<typeof actionHoverParamSchema, ActionHoverParam>({\n name: 'Hover',\n description: 'Move the mouse to the element',\n interfaceAlias: 'aiHover',\n paramSchema: actionHoverParamSchema,\n sample: {\n locate: { prompt: 'the navigation menu item \"Products\"' },\n },\n call,\n });\n};\n\n// Input\nconst inputLocateDescription =\n 'the position of the placeholder or text content in the target input field. If there is no content, locate the center of the input field.';\nexport const actionInputParamSchema = z.object({\n value: z\n .union([z.string(), z.number()])\n .transform((val) => String(val))\n .describe(\n 'The text to input. Provide the final content for replace/append modes, or an empty string when using clear mode to remove existing text.',\n ),\n locate: getMidsceneLocationSchema()\n .describe(inputLocateDescription)\n .optional(),\n mode: z\n .enum(['replace', 'clear', 'typeOnly'])\n .default('replace')\n .describe(\n 'Input mode: \"replace\" (default) - clear the field and input the value; \"typeOnly\" - type the value directly without clearing the field first; \"clear\" - clear the field without inputting new text.',\n ),\n});\nexport type ActionInputParam = {\n value: string;\n locate?: LocateResultElement;\n mode?: 'replace' | 'clear' | 'typeOnly' | 'append';\n};\n\nexport const defineActionInput = (\n call: (param: ActionInputParam) => Promise<void>,\n): DeviceAction<ActionInputParam> => {\n return defineAction<typeof actionInputParamSchema, ActionInputParam>({\n name: 'Input',\n description: 'Input the value into the element',\n interfaceAlias: 'aiInput',\n paramSchema: actionInputParamSchema,\n sample: {\n value: 'testexample.com',\n locate: { prompt: 'the email input field' },\n },\n call: (param) => {\n // backward compat: convert deprecated 'append' to 'typeOnly'\n if ((param.mode as string) === 'append') {\n param.mode = 'typeOnly';\n }\n return call(param);\n },\n });\n};\n\n// KeyboardPress\nexport const actionKeyboardPressParamSchema = z.object({\n locate: getMidsceneLocationSchema()\n .describe('The element to be clicked before pressing the key')\n .optional(),\n keyName: z\n .string()\n .describe(\n \"The key to be pressed. Use '+' for key combinations, e.g., 'Control+A', 'Shift+Enter'\",\n ),\n});\nexport type ActionKeyboardPressParam = {\n locate?: LocateResultElement;\n keyName: string;\n};\n\nexport const defineActionKeyboardPress = (\n call: (param: ActionKeyboardPressParam) => Promise<void>,\n): DeviceAction<ActionKeyboardPressParam> => {\n return defineAction<\n typeof actionKeyboardPressParamSchema,\n ActionKeyboardPressParam\n >({\n name: 'KeyboardPress',\n description:\n 'Press a key or key combination, like \"Enter\", \"Tab\", \"Escape\", or \"Control+A\", \"Shift+Enter\". Do not use this to type text.',\n interfaceAlias: 'aiKeyboardPress',\n paramSchema: actionKeyboardPressParamSchema,\n sample: {\n keyName: 'Enter',\n },\n call,\n });\n};\n\n// Scroll\nexport const actionScrollParamSchema = z.object({\n scrollType: z\n .enum([\n 'singleAction',\n 'scrollToBottom',\n 'scrollToTop',\n 'scrollToRight',\n 'scrollToLeft',\n ])\n .default('singleAction')\n .describe(\n 'The scroll behavior: \"singleAction\" for a single scroll action, \"scrollToBottom\" for scrolling all the way to the bottom by rapidly scrolling 5-10 times (skipping intermediate content until reaching the bottom), \"scrollToTop\" for scrolling all the way to the top by rapidly scrolling 5-10 times (skipping intermediate content until reaching the top), \"scrollToRight\" for scrolling all the way to the right by rapidly scrolling multiple times, \"scrollToLeft\" for scrolling all the way to the left by rapidly scrolling multiple times',\n ),\n direction: z\n .enum(['down', 'up', 'right', 'left'])\n .default('down')\n .describe(\n 'The direction to scroll. Only effective when scrollType is \"singleAction\".',\n ),\n distance: z\n .number()\n .nullable()\n .optional()\n .describe('The distance in pixels to scroll'),\n locate: getMidsceneLocationSchema()\n .optional()\n .describe(\n 'Describe the target element to be scrolled on, like \"the table\" or \"the list\" or \"the content area\" or \"the scrollable area\". Do NOT provide a general intent like \"scroll to find some element\"',\n ),\n});\n\nexport const defineActionScroll = (\n call: (param: ActionScrollParam) => Promise<void>,\n): DeviceAction<ActionScrollParam> => {\n return defineAction<typeof actionScrollParamSchema, ActionScrollParam>({\n name: 'Scroll',\n description:\n 'Scroll the page or a scrollable element to browse content. This is the preferred way to scroll on all platforms, including mobile. Supports scrollToBottom/scrollToTop for boundary navigation. Default: direction `down`, scrollType `singleAction`, distance `null`.',\n interfaceAlias: 'aiScroll',\n paramSchema: actionScrollParamSchema,\n sample: {\n direction: 'down',\n scrollType: 'singleAction',\n locate: { prompt: 'the center of the product list area' },\n },\n call,\n });\n};\n\n// DragAndDrop\nexport const actionDragAndDropParamSchema = z.object({\n from: getMidsceneLocationSchema().describe('The position to be dragged'),\n to: getMidsceneLocationSchema().describe('The position to be dropped'),\n});\nexport type ActionDragAndDropParam = {\n from: LocateResultElement;\n to: LocateResultElement;\n};\n\nexport const defineActionDragAndDrop = (\n call: (param: ActionDragAndDropParam) => Promise<void>,\n): DeviceAction<ActionDragAndDropParam> => {\n return defineAction<\n typeof actionDragAndDropParamSchema,\n ActionDragAndDropParam\n >({\n name: 'DragAndDrop',\n description:\n 'Pick up a specific UI element and move it to a new position (e.g., reorder a card, move a file into a folder, sort list items). The element itself moves with your finger/mouse.',\n interfaceAlias: 'aiDragAndDrop',\n paramSchema: actionDragAndDropParamSchema,\n sample: {\n from: { prompt: 'the \"report.pdf\" file icon' },\n to: { prompt: 'the upload drop zone' },\n },\n call,\n });\n};\n\nexport const ActionLongPressParamSchema = z.object({\n locate: getMidsceneLocationSchema().describe(\n 'The element to be long pressed',\n ),\n duration: z\n .number()\n .default(500)\n .optional()\n .describe('Long press duration in milliseconds'),\n});\n\nexport type ActionLongPressParam = {\n locate: LocateResultElement;\n duration?: number;\n};\nexport const defineActionLongPress = (\n call: (param: ActionLongPressParam) => Promise<void>,\n): DeviceAction<ActionLongPressParam> => {\n return defineAction<typeof ActionLongPressParamSchema, ActionLongPressParam>({\n name: 'LongPress',\n description: 'Long press the element',\n paramSchema: ActionLongPressParamSchema,\n sample: {\n locate: { prompt: 'the message bubble' },\n },\n call,\n });\n};\n\nexport const ActionSwipeParamSchema = z.object({\n start: getMidsceneLocationSchema()\n .optional()\n .describe(\n 'Starting point of the swipe gesture, if not specified, the center of the page will be used',\n ),\n direction: z\n .enum(['up', 'down', 'left', 'right'])\n .optional()\n .describe(\n 'The direction to swipe (required when using distance). The direction means the direction of the finger swipe.',\n ),\n distance: z\n .number()\n .optional()\n .describe('The distance in pixels to swipe (mutually exclusive with end)'),\n end: getMidsceneLocationSchema()\n .optional()\n .describe(\n 'Ending point of the swipe gesture (mutually exclusive with distance)',\n ),\n duration: z\n .number()\n .default(300)\n .describe('Duration of the swipe gesture in milliseconds'),\n repeat: z\n .number()\n .optional()\n .describe(\n 'The number of times to repeat the swipe gesture. 1 for default, 0 for infinite (e.g. endless swipe until the end of the page)',\n ),\n});\n\nexport type ActionSwipeParam = {\n start?: LocateResultElement;\n direction?: 'up' | 'down' | 'left' | 'right';\n distance?: number;\n end?: LocateResultElement;\n duration?: number;\n repeat?: number;\n};\n\nexport function normalizeMobileSwipeParam(\n param: ActionSwipeParam,\n screenSize: { width: number; height: number },\n): {\n startPoint: { x: number; y: number };\n endPoint: { x: number; y: number };\n duration: number;\n repeatCount: number;\n} {\n const { width, height } = screenSize;\n const { start, end } = param;\n\n const startPoint = start\n ? { x: start.center[0], y: start.center[1] }\n : { x: width / 2, y: height / 2 };\n\n let endPoint: { x: number; y: number };\n\n if (end) {\n endPoint = { x: end.center[0], y: end.center[1] };\n } else if (param.distance) {\n const direction = param.direction;\n if (!direction) {\n throw new Error('direction is required for swipe gesture');\n }\n endPoint = {\n x:\n startPoint.x +\n (direction === 'right'\n ? param.distance\n : direction === 'left'\n ? -param.distance\n : 0),\n y:\n startPoint.y +\n (direction === 'down'\n ? param.distance\n : direction === 'up'\n ? -param.distance\n : 0),\n };\n } else {\n throw new Error(\n 'Either end or distance must be specified for swipe gesture',\n );\n }\n\n endPoint.x = Math.max(0, Math.min(endPoint.x, width));\n endPoint.y = Math.max(0, Math.min(endPoint.y, height));\n\n const duration = param.duration ?? 300;\n\n let repeatCount = typeof param.repeat === 'number' ? param.repeat : 1;\n if (repeatCount === 0) {\n repeatCount = 10;\n }\n\n return { startPoint, endPoint, duration, repeatCount };\n}\n\nexport const defineActionSwipe = (\n call: (param: ActionSwipeParam) => Promise<void>,\n): DeviceAction<ActionSwipeParam> => {\n return defineAction<typeof ActionSwipeParamSchema, ActionSwipeParam>({\n name: 'Swipe',\n description:\n 'Perform a touch gesture for interactions beyond regular scrolling (e.g., flip pages in a carousel, dismiss a notification, swipe-to-delete a list item). For regular content scrolling, use Scroll instead. Use \"distance\" + \"direction\" for relative movement, or \"end\" for precise endpoint.',\n paramSchema: ActionSwipeParamSchema,\n sample: {\n start: { prompt: 'center of the notification' },\n end: { prompt: 'upper edge of the screen' },\n },\n call,\n });\n};\n\n// ClearInput\nexport const actionClearInputParamSchema = z.object({\n locate: getMidsceneLocationSchema()\n .describe('The input field to be cleared')\n .optional(),\n});\nexport type ActionClearInputParam = {\n locate?: LocateResultElement;\n};\n\nexport const defineActionClearInput = (\n call: (param: ActionClearInputParam) => Promise<void>,\n): DeviceAction<ActionClearInputParam> => {\n return defineAction<\n typeof actionClearInputParamSchema,\n ActionClearInputParam\n >({\n name: 'ClearInput',\n description: inputLocateDescription,\n interfaceAlias: 'aiClearInput',\n paramSchema: actionClearInputParamSchema,\n sample: {\n locate: { prompt: 'the search input field' },\n },\n call,\n });\n};\n\n// CursorMove\nexport const actionCursorMoveParamSchema = z.object({\n direction: z\n .enum(['left', 'right'])\n .describe('The direction to move the cursor'),\n times: z\n .number()\n .int()\n .min(1)\n .default(1)\n .describe(\n 'The number of times to move the cursor in the specified direction',\n ),\n});\nexport type ActionCursorMoveParam = {\n direction: 'left' | 'right';\n times?: number;\n};\n\nexport const defineActionCursorMove = (\n call: (param: ActionCursorMoveParam) => Promise<void>,\n): DeviceAction<ActionCursorMoveParam> => {\n return defineAction<\n typeof actionCursorMoveParamSchema,\n ActionCursorMoveParam\n >({\n name: 'CursorMove',\n description:\n 'Move the text cursor (caret) left or right within an input field or text area. Use this to reposition the cursor without selecting text.',\n paramSchema: actionCursorMoveParamSchema,\n sample: {\n direction: 'left',\n times: 3,\n },\n call,\n });\n};\n// Sleep\nexport const ActionSleepParamSchema = z.object({\n timeMs: z\n .number()\n .default(1000)\n .optional()\n .describe('Sleep duration in milliseconds, defaults to 1000ms (1 second)'),\n});\n\nexport type ActionSleepParam = {\n timeMs?: number;\n};\n\nexport const defineActionSleep = (): DeviceAction<ActionSleepParam> => {\n return defineAction<typeof ActionSleepParamSchema, ActionSleepParam>({\n name: 'Sleep',\n description:\n 'Wait for a specified duration before continuing. Defaults to 1 second (1000ms) if not specified.',\n paramSchema: ActionSleepParamSchema,\n sample: {\n timeMs: 2000,\n },\n call: async (param) => {\n const duration = param?.timeMs ?? 1000;\n getDebug('device:common-action')(`Sleeping for ${duration}ms`);\n await new Promise((resolve) => setTimeout(resolve, duration));\n },\n });\n};\n\nexport type { DeviceAction } from '../types';\nexport type {\n AndroidDeviceOpt,\n AndroidDeviceInputOpt,\n IOSDeviceOpt,\n IOSDeviceInputOpt,\n HarmonyDeviceOpt,\n HarmonyDeviceInputOpt,\n} from './device-options';\n"],"names":["AbstractInterface","defineAction","config","actionTapParamSchema","z","getMidsceneLocationSchema","defineActionTap","call","actionRightClickParamSchema","defineActionRightClick","actionDoubleClickParamSchema","defineActionDoubleClick","actionHoverParamSchema","defineActionHover","inputLocateDescription","actionInputParamSchema","val","String","defineActionInput","param","actionKeyboardPressParamSchema","defineActionKeyboardPress","actionScrollParamSchema","defineActionScroll","actionDragAndDropParamSchema","defineActionDragAndDrop","ActionLongPressParamSchema","defineActionLongPress","ActionSwipeParamSchema","normalizeMobileSwipeParam","screenSize","width","height","start","end","startPoint","endPoint","direction","Error","Math","duration","repeatCount","defineActionSwipe","actionClearInputParamSchema","defineActionClearInput","actionCursorMoveParamSchema","defineActionCursorMove","ActionSleepParamSchema","defineActionSleep","getDebug","Promise","resolve","setTimeout"],"mappings":"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"}
|