android-midscene-automation 0.1.42 → 0.2.1
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/AI/350/257/206/345/210/253/346/223/215/344/275/234/350/257/264/346/230/216.md +442 -0
- package/CHANGELOG.md +12 -2
- package/FAQ.md +8 -8
- package/README.md +156 -19
- package/USAGE.md +134 -9
- package/package.json +3 -2
- package/server/appium-auto-save.test.ts +2 -1
- package/server/appium-config.test.ts +48 -1
- package/server/appium-recorder/ai-recognition.test.ts +27 -0
- package/server/appium-recorder/ai-recognition.ts +16 -4
- package/server/appium-recorder/appium-runner.ts +32 -2
- package/server/appium-recorder/bounded-loop.test.ts +55 -0
- package/server/appium-recorder/bounded-loop.ts +12 -1
- package/server/appium-recorder/prompt-optimizer.ts +57 -0
- package/server/appium-recorder/report-summary.test.ts +78 -0
- package/server/appium-recorder/report-summary.ts +60 -0
- package/server/appium-recorder/report.ts +31 -2
- package/server/appium-recorder/repository.ts +8 -2
- package/server/appium-recorder/routes.ts +53 -3
- package/server/config.ts +17 -2
- package/server/http-api.ts +8 -4
- package/src/App.vue +34 -5
- package/src/api.ts +2 -2
- package/src/appium-recorder/AppiumPage.vue +126 -62
- package/src/appium-recorder/action-descriptions.ts +1 -0
- package/src/appium-recorder/ai-recognition.ts +7 -0
- package/src/appium-recorder/api.ts +22 -2
- package/src/appium-recorder/bounded-loop.ts +9 -1
- package/src/appium-recorder/components/AiRecognitionSettings.vue +57 -6
- package/src/appium-recorder/components/BreakLoopSettings.vue +19 -3
- package/src/appium-recorder/components/ComponentTree.vue +5 -5
- package/src/appium-recorder/components/CoordinatePointPicker.vue +23 -0
- package/src/appium-recorder/components/FlowActionMenu.vue +50 -20
- package/src/appium-recorder/components/FlowStepEditor.vue +10 -2
- package/src/appium-recorder/components/NestedConditionBranches.vue +18 -49
- package/src/appium-recorder/components/RecordedSteps.vue +16 -5
- package/src/appium-recorder/coordinate-preview.ts +37 -0
- package/src/appium-recorder/flow-copy.ts +1 -0
- package/src/appium-recorder/flow-graph.ts +5 -2
- package/src/appium-recorder/flow-labels.ts +4 -2
- package/src/appium-recorder/flow-merge.ts +1 -1
- package/src/appium-recorder/flow-unmerge.ts +2 -2
- package/src/appium-recorder/report-summary.ts +60 -0
- package/src/appium-recorder/types.ts +4 -1
- package/src/components/config/AiDeduplicationSettings.vue +5 -4
- package/src/components/config/AiPromptPresetsSettings.vue +64 -17
- package/src/components/config/AppiumConfigPanel.vue +66 -38
- package/src/components/config/AppiumModelSettings.vue +45 -0
- package/src/components/config/FlowAppearanceSettings.vue +0 -1
- package/src/components/config/ReplayReportSummarySettings.vue +118 -0
- package/src/components/device/DevicePreviewPanel.vue +18 -3
- package/src/components/help/HelpCenterDialog.vue +160 -0
- package/src/components/help/MarkdownDocument.vue +139 -0
- package/src/config/use-appium-auto-save.ts +12 -4
- package/src/pages/ConfigPage.vue +4 -2
- package/src/style.css +37 -45
- package/src/types.ts +2 -1
- package//346/265/201/347/250/213/350/212/202/347/202/271/346/223/215/344/275/234/350/257/264/346/230/216.md +408 -0
package/server/http-api.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { resolveReportSummary } from '../src/appium-recorder/report-summary';
|
|
1
2
|
// The Midscene Admin HTTP API — extracted from vite.config.ts so the exact
|
|
2
3
|
// same middleware serves the standalone dev/preview flow AND the DSH plugin.
|
|
3
4
|
// It is a Connect-style handler: (req, res, next). Callers that have no next
|
|
@@ -724,7 +725,8 @@ export function createApiMiddleware() {
|
|
|
724
725
|
}
|
|
725
726
|
const current = loadConfig();
|
|
726
727
|
const config = req.url === '/api/config/appium'
|
|
727
|
-
? { ...current, appium: { ...current.appium, ...parsed.appium, model: current.appium.model
|
|
728
|
+
? { ...current, appium: { ...current.appium, ...parsed.appium, model: current.appium.model, promptOptimizer: current.appium.promptOptimizer,
|
|
729
|
+
reportSummary: { ...resolveReportSummary(current.appium.reportSummary), ...parsed.appium?.reportSummary } } }
|
|
728
730
|
: { ...current, runtime: parsed.runtime ?? current.runtime };
|
|
729
731
|
saveConfig(config);
|
|
730
732
|
res.setHeader('Content-Type', 'application/json; charset=utf-8');
|
|
@@ -1097,7 +1099,7 @@ export function createApiMiddleware() {
|
|
|
1097
1099
|
try {
|
|
1098
1100
|
const parsed = await readBody<{
|
|
1099
1101
|
save?: boolean;
|
|
1100
|
-
modelKey?: 'midscene' | 'scriptOptimizer' | 'appium';
|
|
1102
|
+
modelKey?: 'midscene' | 'scriptOptimizer' | 'appium' | 'promptOptimizer';
|
|
1101
1103
|
model?: {
|
|
1102
1104
|
provider?: import('../src/config/midscene-model-presets').MidsceneModelProvider;
|
|
1103
1105
|
baseUrl?: string;
|
|
@@ -1108,7 +1110,7 @@ export function createApiMiddleware() {
|
|
|
1108
1110
|
}>(req);
|
|
1109
1111
|
const model = parsed.model || {};
|
|
1110
1112
|
|
|
1111
|
-
if (parsed.save && !['midscene', 'scriptOptimizer', 'appium'].includes(parsed.modelKey || '')) {
|
|
1113
|
+
if (parsed.save && !['midscene', 'scriptOptimizer', 'appium', 'promptOptimizer'].includes(parsed.modelKey || '')) {
|
|
1112
1114
|
throw new ConfigValidationError('请选择有效的模型配置');
|
|
1113
1115
|
}
|
|
1114
1116
|
if (parsed.save && parsed.modelKey === 'midscene' && !model.family?.trim()) {
|
|
@@ -1127,7 +1129,9 @@ export function createApiMiddleware() {
|
|
|
1127
1129
|
const key = parsed.modelKey!;
|
|
1128
1130
|
// 测试期间其他配置可能已保存,此时重新读取,只更新已通过测试的模型。
|
|
1129
1131
|
const current = loadConfig();
|
|
1130
|
-
|
|
1132
|
+
if (key === 'promptOptimizer') {
|
|
1133
|
+
saveConfig({ ...current, appium: { ...current.appium, promptOptimizer: { model: { baseUrl: model.baseUrl || '', apiKey: model.apiKey || '', name: model.name || '' } } } });
|
|
1134
|
+
} else saveConfig({ ...current, [key]: { ...current[key], model: { ...current[key].model, ...model } } });
|
|
1131
1135
|
}
|
|
1132
1136
|
res.setHeader('Content-Type', 'application/json; charset=utf-8');
|
|
1133
1137
|
res.end(JSON.stringify({ ...result, saved: parsed.save === true }));
|
package/src/App.vue
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
+
import { resolveReportSummary } from './appium-recorder/report-summary';
|
|
2
3
|
import { isAiRecognitionModelConfigured } from './appium-recorder/ai-recognition';
|
|
3
4
|
import { resolveAiDeduplication } from './appium-recorder/ai-deduplication';
|
|
4
5
|
import { resolveAiPromptPresets } from './appium-recorder/ai-prompt-presets';
|
|
@@ -11,6 +12,7 @@ import {
|
|
|
11
12
|
Loading,
|
|
12
13
|
Monitor,
|
|
13
14
|
Operation,
|
|
15
|
+
QuestionFilled,
|
|
14
16
|
RefreshLeft,
|
|
15
17
|
Setting,
|
|
16
18
|
VideoPause,
|
|
@@ -39,6 +41,7 @@ import AppiumPage from './appium-recorder/AppiumPage.vue';
|
|
|
39
41
|
import { normalizeFlowBackground, normalizeFlowLineColor } from './appium-recorder/flow-appearance';
|
|
40
42
|
import ConfigPage from './pages/ConfigPage.vue';
|
|
41
43
|
import GeneratorPage from './pages/GeneratorPage.vue';
|
|
44
|
+
import HelpCenterDialog from './components/help/HelpCenterDialog.vue';
|
|
42
45
|
import type {
|
|
43
46
|
AndroidDevice,
|
|
44
47
|
ConfigForm,
|
|
@@ -66,6 +69,7 @@ const isSavingAppPreset = ref(false);
|
|
|
66
69
|
const isRunningScript = ref(false);
|
|
67
70
|
const isStoppingScript = ref(false);
|
|
68
71
|
const showGeneratedCode = ref(false);
|
|
72
|
+
const helpCenterVisible = shallowRef(false);
|
|
69
73
|
const generatedCodeOverride = shallowRef<string | null>(null);
|
|
70
74
|
const generatedCodeDraft = shallowRef('');
|
|
71
75
|
const generatedCodeEditing = shallowRef(false);
|
|
@@ -105,6 +109,7 @@ const modelTestStatus = reactive({
|
|
|
105
109
|
midscene: '',
|
|
106
110
|
scriptOptimizer: '',
|
|
107
111
|
appium: '',
|
|
112
|
+
promptOptimizer: '',
|
|
108
113
|
});
|
|
109
114
|
const aiRecognitionModelConfigured = shallowRef(false);
|
|
110
115
|
const actionDialog = reactive({
|
|
@@ -156,7 +161,7 @@ const appPresetForm = reactive({
|
|
|
156
161
|
});
|
|
157
162
|
|
|
158
163
|
const configForm = reactive<ConfigForm>({
|
|
159
|
-
appium: { model: { baseUrl: '', apiKey: '', name: '' }, aiDeduplication: resolveAiDeduplication(), screenshotReport: false },
|
|
164
|
+
appium: { reportSummary: resolveReportSummary(), model: { baseUrl: '', apiKey: '', name: '' }, promptOptimizer: { model: { baseUrl: '', apiKey: '', name: '' } }, aiDeduplication: resolveAiDeduplication(), screenshotReport: false },
|
|
160
165
|
runtime: {
|
|
161
166
|
androidSdkPath: '',
|
|
162
167
|
reportOutputPath: '',
|
|
@@ -671,7 +676,9 @@ const loadConfig = async () => {
|
|
|
671
676
|
Object.assign(configForm.midscene.env, payload.midscene.env || {});
|
|
672
677
|
Object.assign(configForm.scriptOptimizer.model, payload.scriptOptimizer.model);
|
|
673
678
|
Object.assign(configForm.appium.model, payload.appium?.model || { baseUrl: '', apiKey: '', name: '' });
|
|
679
|
+
Object.assign(configForm.appium.promptOptimizer!.model, payload.appium?.promptOptimizer?.model || { baseUrl: '', apiKey: '', name: '' });
|
|
674
680
|
configForm.appium.screenshotReport = payload.appium?.screenshotReport === true;
|
|
681
|
+
configForm.appium.reportSummary = resolveReportSummary(payload.appium?.reportSummary);
|
|
675
682
|
configForm.appium.aiDeduplication = resolveAiDeduplication(payload.appium?.aiDeduplication);
|
|
676
683
|
configForm.appium.aiPromptPresets = resolveAiPromptPresets(payload.appium?.aiPromptPresets);
|
|
677
684
|
configForm.appium.flowBackgroundColor = normalizeFlowBackground(payload.appium?.flowBackgroundColor);
|
|
@@ -794,19 +801,25 @@ const removeAppPreset = async (id: string) => {
|
|
|
794
801
|
}
|
|
795
802
|
};
|
|
796
803
|
|
|
797
|
-
const testModel = async (key: 'midscene' | 'scriptOptimizer' | 'appium') => {
|
|
804
|
+
const testModel = async (key: 'midscene' | 'scriptOptimizer' | 'appium' | 'promptOptimizer') => {
|
|
798
805
|
if (testingModelKey.value) return;
|
|
799
806
|
testingModelKey.value = key;
|
|
800
807
|
errorMessage.value = '';
|
|
801
808
|
modelTestStatus[key] = '';
|
|
802
|
-
const model = { ...
|
|
809
|
+
const model = { ...(key === 'promptOptimizer'
|
|
810
|
+
? configForm.appium.promptOptimizer!.model
|
|
811
|
+
: configForm[key].model) };
|
|
803
812
|
|
|
804
813
|
try {
|
|
805
814
|
const payload = await api.testModel({ modelKey: key, model, save: true });
|
|
806
815
|
if (!payload.saved) throw new Error('模型未保存,请重试');
|
|
807
|
-
|
|
816
|
+
const successMessage = `测试通过,配置已保存${payload.content ? `:${payload.content}` : ''}`;
|
|
817
|
+
modelTestStatus[key] = '';
|
|
808
818
|
if (key === 'appium') aiRecognitionModelConfigured.value = isAiRecognitionModelConfigured(model);
|
|
809
|
-
|
|
819
|
+
void ElMessageBox.alert(successMessage, '模型测试成功', {
|
|
820
|
+
type: 'success',
|
|
821
|
+
confirmButtonText: '确定',
|
|
822
|
+
}).catch(() => {});
|
|
810
823
|
} catch (error) {
|
|
811
824
|
modelTestStatus[key] = `未保存:${error instanceof Error ? error.message : '测试失败'}`;
|
|
812
825
|
} finally {
|
|
@@ -1441,6 +1454,18 @@ onUnmounted(() => {
|
|
|
1441
1454
|
<span>参数配置</span>
|
|
1442
1455
|
</el-menu-item>
|
|
1443
1456
|
</el-menu>
|
|
1457
|
+
<div class="layout-sidebar__footer">
|
|
1458
|
+
<el-tooltip content="帮助与版本" placement="right">
|
|
1459
|
+
<button
|
|
1460
|
+
type="button"
|
|
1461
|
+
class="layout-sidebar__help"
|
|
1462
|
+
aria-label="打开帮助与版本信息"
|
|
1463
|
+
@click="helpCenterVisible = true"
|
|
1464
|
+
>
|
|
1465
|
+
<el-icon><QuestionFilled /></el-icon>
|
|
1466
|
+
</button>
|
|
1467
|
+
</el-tooltip>
|
|
1468
|
+
</div>
|
|
1444
1469
|
</aside>
|
|
1445
1470
|
|
|
1446
1471
|
<div class="layout-main">
|
|
@@ -1580,6 +1605,7 @@ onUnmounted(() => {
|
|
|
1580
1605
|
:app-presets="appPresets"
|
|
1581
1606
|
:ai-recognition-model-configured="aiRecognitionModelConfigured"
|
|
1582
1607
|
:ai-prompt-presets="configForm.appium.aiPromptPresets"
|
|
1608
|
+
:report-summary="configForm.appium.reportSummary"
|
|
1583
1609
|
:flow-background-color="configForm.appium.flowBackgroundColor"
|
|
1584
1610
|
:flow-line-color="configForm.appium.flowLineColor"
|
|
1585
1611
|
:device-actions="deviceActions"
|
|
@@ -1596,7 +1622,10 @@ onUnmounted(() => {
|
|
|
1596
1622
|
:refresh-device-preview="refreshDevicePreview"
|
|
1597
1623
|
:swipe-device="swipeDevice"
|
|
1598
1624
|
/>
|
|
1625
|
+
|
|
1599
1626
|
</main>
|
|
1600
1627
|
</div>
|
|
1628
|
+
|
|
1629
|
+
<HelpCenterDialog v-model="helpCenterVisible" />
|
|
1601
1630
|
</div>
|
|
1602
1631
|
</template>
|
package/src/api.ts
CHANGED
|
@@ -115,14 +115,14 @@ export function saveConfig(input: Pick<ConfigForm, 'runtime'>) {
|
|
|
115
115
|
return postJson<{ success?: boolean } | ConfigForm>(`${APP_BASE}/api/config`, input);
|
|
116
116
|
}
|
|
117
117
|
|
|
118
|
-
export function saveAppiumConfig(appium: Omit<ConfigForm['appium'], 'model'>) {
|
|
118
|
+
export function saveAppiumConfig(appium: Omit<ConfigForm['appium'], 'model' | 'promptOptimizer'>) {
|
|
119
119
|
return postJson<{ success: boolean }>(`${APP_BASE}/api/config/appium`, { appium });
|
|
120
120
|
}
|
|
121
121
|
|
|
122
122
|
// 测试指定模型配置是否可用,通常用于保存前验证 baseUrl/apiKey/model。
|
|
123
123
|
export function testModel(input: {
|
|
124
124
|
save?: boolean;
|
|
125
|
-
modelKey: 'midscene' | 'scriptOptimizer' | 'appium';
|
|
125
|
+
modelKey: 'midscene' | 'scriptOptimizer' | 'appium' | 'promptOptimizer';
|
|
126
126
|
model: {
|
|
127
127
|
provider?: MidsceneModelProvider;
|
|
128
128
|
baseUrl: string;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
import { computed, h, onMounted, onUnmounted, provide, reactive, shallowRef, watch } from 'vue';
|
|
3
3
|
import { flowBackgroundKey, normalizeFlowBackground, flowLineColorKey, normalizeFlowLineColor } from './flow-appearance';
|
|
4
|
+
import { useCoordinatePicker } from './coordinate-preview';
|
|
4
5
|
import { DEFAULT_NODE_TIMEOUT_MS } from './node-timeout';
|
|
5
6
|
import { aiPromptPresetsKey, DEFAULT_AI_PROMPT_PRESETS, type AiPromptPreset } from './ai-prompt-presets';
|
|
6
7
|
import AiRecognitionPromptInput from './components/AiRecognitionPromptInput.vue';
|
|
@@ -17,9 +18,10 @@ import { insertBeforeSharedStep } from './flow-insert';
|
|
|
17
18
|
import VariableExtractionSettings from './components/VariableExtractionSettings.vue';
|
|
18
19
|
import ScriptParameterSettings from './components/ScriptParameterSettings.vue';
|
|
19
20
|
import { validateVariables, validateExtraction, validateReturns, type TestVariable } from './variables';
|
|
20
|
-
import { defaultLoopConfig, enclosingLoops,
|
|
21
|
+
import { breakLoopTarget, continueLoopTarget, defaultLoopConfig, enclosingLoops, validateLoop, validateLoopSteps } from './bounded-loop';
|
|
21
22
|
import {
|
|
22
23
|
clearAppiumDeviceAppData,
|
|
24
|
+
checkReportSummaryModel,
|
|
23
25
|
deleteAppiumScript,
|
|
24
26
|
downloadAppiumScript,
|
|
25
27
|
getAppiumScripts,
|
|
@@ -46,6 +48,7 @@ import VisualChangeDialog from './components/VisualChangeDialog.vue';
|
|
|
46
48
|
import ImageCheckDialog from './components/ImageCheckDialog.vue';
|
|
47
49
|
import AiRecognitionTestDialog from './components/AiRecognitionTestDialog.vue';
|
|
48
50
|
import { validateAiRecognitionPrompt } from './ai-recognition';
|
|
51
|
+
import { DEFAULT_REPORT_SUMMARY_PROMPT, REPORT_SUMMARY_PROMPT_PRESETS, type ReportSummaryConfig } from './report-summary';
|
|
49
52
|
import { createImageCheckConfig, validateImageCheck, type ImageCheckConfig } from './image-check';
|
|
50
53
|
import {
|
|
51
54
|
createFlowClipboard,
|
|
@@ -101,10 +104,12 @@ const AUTO_TREE_REFRESH_INTERVAL_MS = 1800;
|
|
|
101
104
|
const readonlyFlowActionGroups: FlowActionGroup[] = [];
|
|
102
105
|
const readonlyFlowSelectedIndexes: number[] = [];
|
|
103
106
|
const recordReplayVideo = shallowRef(false);
|
|
107
|
+
const selectedReportTemplateName = shallowRef('');
|
|
104
108
|
|
|
105
109
|
const props = defineProps<{
|
|
106
110
|
aiRecognitionModelConfigured?: boolean;
|
|
107
111
|
aiPromptPresets?: AiPromptPreset[];
|
|
112
|
+
reportSummary?: ReportSummaryConfig;
|
|
108
113
|
flowBackgroundColor?: string;
|
|
109
114
|
flowLineColor?: string;
|
|
110
115
|
active: boolean;
|
|
@@ -134,6 +139,11 @@ const props = defineProps<{
|
|
|
134
139
|
provide(flowBackgroundKey, computed(() => normalizeFlowBackground(props.flowBackgroundColor)));
|
|
135
140
|
provide(flowLineColorKey, computed(() => normalizeFlowLineColor(props.flowLineColor)));
|
|
136
141
|
const availableAiPromptPresets = computed(() => props.aiPromptPresets ?? DEFAULT_AI_PROMPT_PRESETS);
|
|
142
|
+
const availableReportTemplates = computed(() => [
|
|
143
|
+
...REPORT_SUMMARY_PROMPT_PRESETS,
|
|
144
|
+
...(props.reportSummary?.customPresets || []),
|
|
145
|
+
]);
|
|
146
|
+
const selectedReportTemplate = computed(() => availableReportTemplates.value.find(item => item.name === selectedReportTemplateName.value));
|
|
137
147
|
provide(aiPromptPresetsKey, availableAiPromptPresets);
|
|
138
148
|
const selectedDeviceId = computed(() => props.playgroundDeviceId);
|
|
139
149
|
const tree = shallowRef<AppiumNode | null>(null);
|
|
@@ -180,6 +190,7 @@ const duplicatingScriptId = shallowRef('');
|
|
|
180
190
|
const renamingScriptId = shallowRef('');
|
|
181
191
|
const downloadingScriptId = shallowRef('');
|
|
182
192
|
const replaying = shallowRef(false);
|
|
193
|
+
const preparingReplay = shallowRef(false);
|
|
183
194
|
const stoppingReplay = shallowRef(false);
|
|
184
195
|
const activeReplayDeviceId = shallowRef('');
|
|
185
196
|
const recordingTap = shallowRef(false);
|
|
@@ -241,16 +252,9 @@ const linkedScriptTarget = computed(() => scripts.value.find((script) => script.
|
|
|
241
252
|
const linkedScriptPreviewScript = computed(() => (
|
|
242
253
|
scripts.value.find((script) => script.id === linkedScriptPreviewScriptId.value) || null
|
|
243
254
|
));
|
|
244
|
-
const compatibleLinkableScripts = computed(() => linkableScripts.value.filter(
|
|
245
|
-
script.appPackage === form.appPackage
|
|
246
|
-
|
|
247
|
-
Boolean(linkedScriptBranchTarget.value)
|
|
248
|
-
|| (
|
|
249
|
-
Boolean(linkedScriptExpectedActivity.value)
|
|
250
|
-
&& script.appActivity === linkedScriptExpectedActivity.value
|
|
251
|
-
)
|
|
252
|
-
)
|
|
253
|
-
)));
|
|
255
|
+
const compatibleLinkableScripts = computed(() => linkableScripts.value.filter(
|
|
256
|
+
(script) => script.appPackage === form.appPackage,
|
|
257
|
+
));
|
|
254
258
|
const hasSelectedAppPackage = computed(() => props.appPresets.some((app) => app.packageName === form.appPackage));
|
|
255
259
|
const scriptActivityMismatch = computed(() => {
|
|
256
260
|
const expectedActivity = selectedScript.value?.appActivity?.trim();
|
|
@@ -262,8 +266,8 @@ const recordingBusy = computed(() => (
|
|
|
262
266
|
|| resolvingNavigation.value
|
|
263
267
|
|| Boolean(pendingNavigation.value)
|
|
264
268
|
));
|
|
265
|
-
const recordingLocked = computed(() => recordingBusy.value || replaying.value);
|
|
266
|
-
const newScriptDisabled = computed(() => saving.value || replaying.value || recordingBusy.value || launchingApp.value || importingScript.value);
|
|
269
|
+
const recordingLocked = computed(() => recordingBusy.value || replaying.value || preparingReplay.value);
|
|
270
|
+
const newScriptDisabled = computed(() => saving.value || replaying.value || preparingReplay.value || recordingBusy.value || launchingApp.value || importingScript.value);
|
|
267
271
|
const overlayBounds = computed(() => flattenNodes(tree.value).flatMap((node) => (
|
|
268
272
|
node.bounds ? [{ id: node.id, ...node.bounds }] : []
|
|
269
273
|
)));
|
|
@@ -280,6 +284,13 @@ const visualChangeRegionSelectionEnabled = computed(() => (
|
|
|
280
284
|
(visualChangeDialogVisible.value && visualChangeConfig.value.mode === 'region') || imageCheckDraft.value?.imageCheck?.target === 'region'
|
|
281
285
|
));
|
|
282
286
|
|
|
287
|
+
const coordinatePicker = useCoordinatePicker(
|
|
288
|
+
computed(() => Boolean(selectedDeviceId.value) && !recordingLocked.value && !visualChangeRegionSelectionEnabled.value),
|
|
289
|
+
() => workspaceRef.value?.showPreview(),
|
|
290
|
+
);
|
|
291
|
+
const pickingCoordinate = coordinatePicker.picking;
|
|
292
|
+
watch([selectedDeviceId, () => props.active], () => coordinatePicker.cancel());
|
|
293
|
+
|
|
283
294
|
function stepWithBranchTarget(step: AppiumRecordedStep, branchTarget: BranchTarget) {
|
|
284
295
|
return {
|
|
285
296
|
...step,
|
|
@@ -955,6 +966,10 @@ async function executeFlowStep(index: number) {
|
|
|
955
966
|
}
|
|
956
967
|
|
|
957
968
|
function selectNodeFromPoint(point: { x: number; y: number }) {
|
|
969
|
+
if (pickingCoordinate.value) {
|
|
970
|
+
coordinatePicker.select(point);
|
|
971
|
+
return;
|
|
972
|
+
}
|
|
958
973
|
const node = findSmallestNodeAtPoint(tree.value, point.x, point.y);
|
|
959
974
|
if (node) {
|
|
960
975
|
selectedNode.value = node;
|
|
@@ -1144,16 +1159,8 @@ function openLinkedScriptDialog(index?: number, branchTarget?: BranchTarget) {
|
|
|
1144
1159
|
}
|
|
1145
1160
|
linkedScriptBranchTarget.value = branchTarget || null;
|
|
1146
1161
|
linkedScriptExpectedActivity.value = expectedActivityAfterStep(index);
|
|
1147
|
-
if (!linkedScriptBranchTarget.value && !linkedScriptExpectedActivity.value) {
|
|
1148
|
-
ElMessage.warning('无法确定当前插入点的 Activity,请先刷新组件树');
|
|
1149
|
-
return;
|
|
1150
|
-
}
|
|
1151
1162
|
if (!compatibleLinkableScripts.value.length) {
|
|
1152
|
-
ElMessage.warning(
|
|
1153
|
-
linkedScriptBranchTarget.value
|
|
1154
|
-
? '没有属于当前 App 的可连接脚本'
|
|
1155
|
-
: `没有入口 Activity 为 ${linkedScriptExpectedActivity.value} 的可连接脚本`,
|
|
1156
|
-
);
|
|
1163
|
+
ElMessage.warning('没有属于当前 App 的可连接脚本');
|
|
1157
1164
|
linkedScriptExpectedActivity.value = '';
|
|
1158
1165
|
linkedScriptBranchTarget.value = null;
|
|
1159
1166
|
return;
|
|
@@ -1177,16 +1184,6 @@ function addLinkedScriptStep() {
|
|
|
1177
1184
|
ElMessage.error('连接脚本必须属于当前 App');
|
|
1178
1185
|
return;
|
|
1179
1186
|
}
|
|
1180
|
-
if (
|
|
1181
|
-
!linkedScriptBranchTarget.value
|
|
1182
|
-
&& (!linkedScriptExpectedActivity.value || script.appActivity !== linkedScriptExpectedActivity.value)
|
|
1183
|
-
) {
|
|
1184
|
-
ElMessage.error(
|
|
1185
|
-
`无法连接:插入点 Activity 为 ${linkedScriptExpectedActivity.value || '-'},`
|
|
1186
|
-
+ `目标脚本入口 Activity 为 ${script.appActivity || '-'}`,
|
|
1187
|
-
);
|
|
1188
|
-
return;
|
|
1189
|
-
}
|
|
1190
1187
|
const editingIndex = linkedScriptEditingId.value ? steps.value.findIndex(step => step.id === linkedScriptEditingId.value) : -1;
|
|
1191
1188
|
if (linkedScriptEditingId.value && editingIndex < 0) { ElMessage.warning('原节点已删除,请重新打开'); return; }
|
|
1192
1189
|
if (editingIndex >= 0) {
|
|
@@ -1388,6 +1385,31 @@ async function addAction(
|
|
|
1388
1385
|
if (result) return insertStep({ ...step, breakLoopTargetId: scoped.breakLoopTargetId }, index, branchTarget);
|
|
1389
1386
|
return;
|
|
1390
1387
|
}
|
|
1388
|
+
if (action === 'continueLoop') {
|
|
1389
|
+
const step: AppiumRecordedStep = { id: createStepId(), type: 'continueLoop', label: '继续下一次循环', flow: { nodeKind: 'action' } };
|
|
1390
|
+
const scoped = reactive(branchTarget ? stepWithBranchTarget(step, branchTarget) : step);
|
|
1391
|
+
const loops = enclosingLoops(steps.value, scoped);
|
|
1392
|
+
if (!loops.length) { ElMessage.warning('继续下一次循环只能添加在循环体内'); return; }
|
|
1393
|
+
scoped.continueLoopTargetId = loops[0]!.id;
|
|
1394
|
+
const result = await ElMessageBox({
|
|
1395
|
+
title: '添加继续下一次循环',
|
|
1396
|
+
message: h(ElForm, { labelPosition: 'top' }, () => h(BreakLoopSettings, {
|
|
1397
|
+
step: scoped,
|
|
1398
|
+
steps: steps.value,
|
|
1399
|
+
mode: 'continue',
|
|
1400
|
+
onUpdate: (patch) => Object.assign(scoped, patch),
|
|
1401
|
+
})),
|
|
1402
|
+
showCancelButton: true, confirmButtonText: '添加', cancelButtonText: '取消',
|
|
1403
|
+
beforeClose: (action, _instance, done) => {
|
|
1404
|
+
if (action === 'confirm') {
|
|
1405
|
+
try { continueLoopTarget(steps.value, scoped); } catch (error) { ElMessage.warning((error as Error).message); return; }
|
|
1406
|
+
}
|
|
1407
|
+
done();
|
|
1408
|
+
},
|
|
1409
|
+
}).catch(() => null);
|
|
1410
|
+
if (result) return insertStep({ ...step, continueLoopTargetId: scoped.continueLoopTargetId }, index, branchTarget);
|
|
1411
|
+
return;
|
|
1412
|
+
}
|
|
1391
1413
|
if (action === 'imageCheck') {
|
|
1392
1414
|
imageCheckIndex = index;
|
|
1393
1415
|
imageCheckBranch = branchTarget;
|
|
@@ -1714,6 +1736,14 @@ async function addBranchAction(index: number, branch: BranchName, action: Record
|
|
|
1714
1736
|
applyBranchTargetToInsertedStep(inserted, branchTarget);
|
|
1715
1737
|
}
|
|
1716
1738
|
|
|
1739
|
+
function continueOnActivityChange() {
|
|
1740
|
+
if (!pendingNavigation.value || resolvingNavigation.value) return;
|
|
1741
|
+
// 跨页面继续录制时,入口仍是首次录制页面,不能改成跳转后的页面。
|
|
1742
|
+
form.appActivity ||= pendingNavigation.value.beforeActivity;
|
|
1743
|
+
pendingNavigation.value = null;
|
|
1744
|
+
ElMessage.success('已继续录制,后续操作将加入当前脚本');
|
|
1745
|
+
}
|
|
1746
|
+
|
|
1717
1747
|
async function saveOnActivityChange() {
|
|
1718
1748
|
const pending = pendingNavigation.value;
|
|
1719
1749
|
if (!pending || resolvingNavigation.value) return;
|
|
@@ -1726,7 +1756,7 @@ async function saveOnActivityChange() {
|
|
|
1726
1756
|
id: selectedScriptId.value || undefined,
|
|
1727
1757
|
name: scriptName,
|
|
1728
1758
|
appPackage: form.appPackage,
|
|
1729
|
-
appActivity:
|
|
1759
|
+
appActivity: form.appActivity || pending.beforeActivity,
|
|
1730
1760
|
deviceId: selectedDeviceId.value,
|
|
1731
1761
|
steps: steps.value,
|
|
1732
1762
|
variables: scriptVariables.value,
|
|
@@ -2075,6 +2105,7 @@ async function saveScript() {
|
|
|
2075
2105
|
}
|
|
2076
2106
|
|
|
2077
2107
|
async function replayScript() {
|
|
2108
|
+
if (preparingReplay.value || replaying.value) return;
|
|
2078
2109
|
if (!selectedScript.value) {
|
|
2079
2110
|
ElMessage.warning('请选择已保存脚本');
|
|
2080
2111
|
return;
|
|
@@ -2083,6 +2114,32 @@ async function replayScript() {
|
|
|
2083
2114
|
const saved = await saveScript();
|
|
2084
2115
|
if (!saved || !selectedScript.value) return;
|
|
2085
2116
|
}
|
|
2117
|
+
let reportSummaryEnabled = false;
|
|
2118
|
+
let reportSummaryPrompt: string | undefined;
|
|
2119
|
+
if (props.reportSummary?.enabled) {
|
|
2120
|
+
preparingReplay.value = true;
|
|
2121
|
+
try {
|
|
2122
|
+
const status = await checkReportSummaryModel();
|
|
2123
|
+
if (status.available) {
|
|
2124
|
+
reportSummaryEnabled = true;
|
|
2125
|
+
reportSummaryPrompt = selectedReportTemplate.value?.prompt || DEFAULT_REPORT_SUMMARY_PROMPT;
|
|
2126
|
+
} else {
|
|
2127
|
+
await ElMessageBox.alert(
|
|
2128
|
+
`当前提示词优化模型不可用${status.message ? `:${status.message}` : ''}。本次将跳过 AI 总结并生成默认格式的基础报告。`,
|
|
2129
|
+
'回放报告总结不可用',
|
|
2130
|
+
{ type: 'warning', confirmButtonText: '继续回放' },
|
|
2131
|
+
).catch(() => {});
|
|
2132
|
+
}
|
|
2133
|
+
} catch (error) {
|
|
2134
|
+
await ElMessageBox.alert(
|
|
2135
|
+
`无法检测提示词优化模型:${error instanceof Error ? error.message : '未知错误'}。本次将跳过 AI 总结并生成默认格式的基础报告。`,
|
|
2136
|
+
'回放报告总结不可用',
|
|
2137
|
+
{ type: 'warning', confirmButtonText: '继续回放' },
|
|
2138
|
+
).catch(() => {});
|
|
2139
|
+
} finally {
|
|
2140
|
+
preparingReplay.value = false;
|
|
2141
|
+
}
|
|
2142
|
+
}
|
|
2086
2143
|
replaying.value = true;
|
|
2087
2144
|
stoppingReplay.value = false;
|
|
2088
2145
|
activeReplayDeviceId.value = selectedDeviceId.value;
|
|
@@ -2090,7 +2147,13 @@ async function replayScript() {
|
|
|
2090
2147
|
try {
|
|
2091
2148
|
await pauseAutoTreeRefresh();
|
|
2092
2149
|
const result = await replayAppiumScript(
|
|
2093
|
-
{
|
|
2150
|
+
{
|
|
2151
|
+
id: selectedScript.value.id,
|
|
2152
|
+
deviceId: activeReplayDeviceId.value,
|
|
2153
|
+
recordVideo: recordReplayVideo.value,
|
|
2154
|
+
reportSummaryEnabled,
|
|
2155
|
+
reportSummaryPrompt,
|
|
2156
|
+
},
|
|
2094
2157
|
(line) => {
|
|
2095
2158
|
replayOutput.value += `${replayOutput.value ? '\n' : ''}${line}`;
|
|
2096
2159
|
},
|
|
@@ -2185,17 +2248,26 @@ watch(
|
|
|
2185
2248
|
<el-button
|
|
2186
2249
|
type="primary"
|
|
2187
2250
|
:icon="VideoPlay"
|
|
2188
|
-
:loading="replaying"
|
|
2251
|
+
:loading="replaying || preparingReplay"
|
|
2189
2252
|
:disabled="!selectedScript"
|
|
2190
2253
|
@click="replayScript"
|
|
2191
2254
|
>
|
|
2192
2255
|
回放
|
|
2193
2256
|
</el-button>
|
|
2194
|
-
<el-popover trigger="click" placement="bottom" :width="
|
|
2195
|
-
<template #reference><el-button type="primary" :icon="ArrowDown" :disabled="replaying" aria-label="回放设置" title="回放设置" class="replay-settings-toggle" /></template>
|
|
2196
|
-
<
|
|
2197
|
-
<el-
|
|
2198
|
-
|
|
2257
|
+
<el-popover trigger="click" placement="bottom" :width="320">
|
|
2258
|
+
<template #reference><el-button type="primary" :icon="ArrowDown" :disabled="replaying || preparingReplay" aria-label="回放设置" title="回放设置" class="replay-settings-toggle" /></template>
|
|
2259
|
+
<div class="replay-settings-panel">
|
|
2260
|
+
<el-tooltip content="使用内置服务端后台录制,无需安装 scrcpy 或 FFmpeg。含敏感变量的运行不录制;分享 HTML 报告时需同时携带 MP4。" placement="bottom" :show-after="300">
|
|
2261
|
+
<el-checkbox v-model="recordReplayVideo" :disabled="replaying || preparingReplay">录制回放视频</el-checkbox>
|
|
2262
|
+
</el-tooltip>
|
|
2263
|
+
<div class="replay-report-template">
|
|
2264
|
+
<span>报告模板</span>
|
|
2265
|
+
<el-select v-model="selectedReportTemplateName" clearable :disabled="replaying || preparingReplay || !reportSummary?.enabled" placeholder="默认测试报告(未选择时使用)">
|
|
2266
|
+
<el-option v-for="template in availableReportTemplates" :key="template.name" :label="template.name" :value="template.name" />
|
|
2267
|
+
</el-select>
|
|
2268
|
+
<small v-if="!reportSummary?.enabled">需先在参数配置中开启回放报告总结。</small>
|
|
2269
|
+
</div>
|
|
2270
|
+
</div>
|
|
2199
2271
|
</el-popover>
|
|
2200
2272
|
</el-button-group>
|
|
2201
2273
|
<el-button
|
|
@@ -2225,6 +2297,8 @@ watch(
|
|
|
2225
2297
|
:selected-bounds="selectedBounds"
|
|
2226
2298
|
:selected-region="visualChangeSelectedRegion"
|
|
2227
2299
|
:region-selection="visualChangeRegionSelectionEnabled"
|
|
2300
|
+
:point-selection="pickingCoordinate"
|
|
2301
|
+
@cancel-point-selection="coordinatePicker.cancel()"
|
|
2228
2302
|
:region-draw-mode="visualChangePicking || imageCheckPicking"
|
|
2229
2303
|
:device-width="deviceWidth"
|
|
2230
2304
|
:device-height="deviceHeight"
|
|
@@ -2265,23 +2339,6 @@ watch(
|
|
|
2265
2339
|
/>
|
|
2266
2340
|
</el-select>
|
|
2267
2341
|
</div>
|
|
2268
|
-
<details
|
|
2269
|
-
v-if="scriptActivityMismatch"
|
|
2270
|
-
class="appium-activity-summary"
|
|
2271
|
-
>
|
|
2272
|
-
<summary>当前页面与脚本绑定的 Activity 不一致</summary>
|
|
2273
|
-
<div class="appium-activity-lock-alert__details">
|
|
2274
|
-
<div>
|
|
2275
|
-
<strong>脚本绑定</strong>
|
|
2276
|
-
<code>{{ selectedScript?.appActivity || '-' }}</code>
|
|
2277
|
-
</div>
|
|
2278
|
-
<div>
|
|
2279
|
-
<strong>当前 Activity</strong>
|
|
2280
|
-
<code>{{ currentActivity || '正在获取' }}</code>
|
|
2281
|
-
</div>
|
|
2282
|
-
<p>不影响脚本编辑;如需录制当前页面的组件操作,请确认设备页面及所选组件。</p>
|
|
2283
|
-
</div>
|
|
2284
|
-
</details>
|
|
2285
2342
|
</section>
|
|
2286
2343
|
|
|
2287
2344
|
<section class="appium-workbench__section appium-workbench__flow">
|
|
@@ -2437,7 +2494,7 @@ watch(
|
|
|
2437
2494
|
:show-close="false"
|
|
2438
2495
|
>
|
|
2439
2496
|
<div class="appium-navigation-dialog">
|
|
2440
|
-
<p
|
|
2497
|
+
<p>检测到 Activity 已跳转。可以继续在当前脚本中录制后续操作,或保存当前脚本并开始录制新页面。</p>
|
|
2441
2498
|
<dl>
|
|
2442
2499
|
<div>
|
|
2443
2500
|
<dt>点击前</dt>
|
|
@@ -2451,6 +2508,7 @@ watch(
|
|
|
2451
2508
|
</div>
|
|
2452
2509
|
<template #footer>
|
|
2453
2510
|
<div class="appium-navigation-footer">
|
|
2511
|
+
<el-button :disabled="resolvingNavigation" @click="continueOnActivityChange">继续录制</el-button>
|
|
2454
2512
|
<el-button type="primary" :loading="resolvingNavigation" @click="saveOnActivityChange">保存脚本</el-button>
|
|
2455
2513
|
</div>
|
|
2456
2514
|
</template>
|
|
@@ -2467,7 +2525,7 @@ watch(
|
|
|
2467
2525
|
<el-form-item :label="linkedScriptBranchTarget ? '分支录制 Activity' : '插入点 Activity'">
|
|
2468
2526
|
<code class="appium-linked-script-activity">{{ linkedScriptExpectedActivity || '-' }}</code>
|
|
2469
2527
|
</el-form-item>
|
|
2470
|
-
<el-form-item
|
|
2528
|
+
<el-form-item label="选择同一 App 的脚本">
|
|
2471
2529
|
<div class="appium-linked-script-picker">
|
|
2472
2530
|
<el-select
|
|
2473
2531
|
v-model="linkedScriptTargetId"
|
|
@@ -2491,6 +2549,11 @@ watch(
|
|
|
2491
2549
|
</el-button>
|
|
2492
2550
|
</div>
|
|
2493
2551
|
</el-form-item>
|
|
2552
|
+
<el-alert v-if="linkedScriptTarget" :closable="false" type="info" show-icon
|
|
2553
|
+
:title="linkedScriptTarget.appActivity
|
|
2554
|
+
? `回放时等待进入 ${linkedScriptTarget.appActivity},请在前序步骤中完成页面跳转。`
|
|
2555
|
+
: '此脚本未绑定入口 Activity,将直接从当前画面执行。请确保前序步骤已进入目标页面。'"
|
|
2556
|
+
/>
|
|
2494
2557
|
<ScriptParameterSettings :step="linkedParameters" @update="Object.assign(linkedParameters, $event)" />
|
|
2495
2558
|
</el-form>
|
|
2496
2559
|
<template #footer>
|
|
@@ -2589,9 +2652,6 @@ watch(
|
|
|
2589
2652
|
<style scoped>
|
|
2590
2653
|
.appium-script-icon { display: inline-flex; }
|
|
2591
2654
|
.appium-script-icon .el-button { width: 30px; height: 30px; min-height: 30px; padding: 0; margin: 0; }
|
|
2592
|
-
.appium-activity-summary { margin: 8px 0; color: #946000; font-size: 12px; }
|
|
2593
|
-
.appium-activity-summary summary { cursor: pointer; line-height: 20px; }
|
|
2594
|
-
.appium-activity-summary[open] .appium-activity-lock-alert__details { max-height: 110px; overflow: auto; margin-top: 6px; }
|
|
2595
2655
|
:deep(.appium-recorder-card--workbench > .el-card__body) { overflow: hidden; }
|
|
2596
2656
|
:deep(.appium-workbench-tabs > .el-tabs__content) { overflow: hidden; }
|
|
2597
2657
|
:deep(.appium-workbench-tabs .el-tab-pane) { height: 100%; overflow: auto; }
|
|
@@ -2603,4 +2663,8 @@ watch(
|
|
|
2603
2663
|
.appium-workbench__flow :deep(.appium-flow-canvas--vue:not(.appium-flow-canvas--dialog)) { flex: 1; height: auto; min-height: 120px; }
|
|
2604
2664
|
.replay-button-group { display: inline-flex; flex-shrink: 0; }
|
|
2605
2665
|
.replay-settings-toggle { padding-inline: 8px; }
|
|
2666
|
+
.replay-settings-panel { display: grid; gap: 14px; }
|
|
2667
|
+
.replay-report-template { display: grid; gap: 6px; }
|
|
2668
|
+
.replay-report-template > span { color: var(--el-text-color-regular); font-size: 13px; }
|
|
2669
|
+
.replay-report-template small { color: var(--el-text-color-secondary); line-height: 1.5; }
|
|
2606
2670
|
</style>
|
|
@@ -21,6 +21,7 @@ export const actionDescriptions: Record<InsertAction, string> = {
|
|
|
21
21
|
endFlow: '立即结束当前回放流程,后续节点不再执行。',
|
|
22
22
|
loop: '重复执行循环体,支持固定次数或条件退出,必须设置最大次数。',
|
|
23
23
|
breakLoop: '退出指定的当前或外层循环,继续执行该循环的结束路径。',
|
|
24
|
+
continueLoop: '跳过当前轮剩余节点,进入指定的当前或外层循环的下一轮;达到最大次数后执行循环结束路径。',
|
|
24
25
|
clearAppData: '清除目标应用数据并重置应用状态,可能删除登录信息和本地设置,仅允许在流程开始处添加。',
|
|
25
26
|
popupCondition: '判断目标组件是否存在,可进一步配置指定文本及精确或模糊匹配,按结果进入是或否分支。',
|
|
26
27
|
checkboxState: '读取原生 Checkbox 的 checked 状态,按 true/false 分支执行。',
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { AiDeduplicationConfig } from './ai-deduplication';
|
|
2
2
|
|
|
3
3
|
export type AiRecognitionModel = { baseUrl: string; apiKey: string; name: string };
|
|
4
|
+
export type AiInvalidResultBranch = 'yes' | 'no';
|
|
4
5
|
export type AiObservationConfig = { durationMs: number; intervalMs: number; mode?: 'batch' | 'untilMatch' };
|
|
5
6
|
export type AiObservationFrame = { elapsedMs: number; imageDataUrl: string };
|
|
6
7
|
export type AiRecognitionResult = {
|
|
@@ -21,6 +22,12 @@ export function validateAiObservation(value: unknown): AiObservationConfig | und
|
|
|
21
22
|
return { durationMs, intervalMs, ...(mode ? { mode } : {}) };
|
|
22
23
|
}
|
|
23
24
|
|
|
25
|
+
export function validateAiInvalidResultBranch(value: unknown): AiInvalidResultBranch | undefined {
|
|
26
|
+
if (value === undefined || value === null || value === '') return undefined;
|
|
27
|
+
if (value !== 'yes' && value !== 'no') throw new Error('AI 无有效结果兜底分支无效');
|
|
28
|
+
return value;
|
|
29
|
+
}
|
|
30
|
+
|
|
24
31
|
// 未保存此字段的是旧版判断节点,保留已有分支;新节点显式保存 false。
|
|
25
32
|
export function isAiBranchEnabled(step: { aiBranchEnabled?: boolean }) {
|
|
26
33
|
return step.aiBranchEnabled !== false;
|
|
@@ -17,6 +17,16 @@ export async function validateAiBranchQuestion(prompt: string, signal?: AbortSig
|
|
|
17
17
|
}));
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
+
export async function optimizeAppiumPrompt(input: {
|
|
21
|
+
kind: 'aiRecognition' | 'reportSummary';
|
|
22
|
+
prompt: string;
|
|
23
|
+
condition?: boolean;
|
|
24
|
+
}, signal?: AbortSignal) {
|
|
25
|
+
return readJson<{ prompt: string }>(await fetch(`${APP_BASE}/api/appium-recorder/prompts/optimize`, {
|
|
26
|
+
method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(input), signal,
|
|
27
|
+
}));
|
|
28
|
+
}
|
|
29
|
+
|
|
20
30
|
export function captureImageCheckRegion(deviceId: string, region: AppiumVisualChangeRegion, screenWidth: number, screenHeight: number) {
|
|
21
31
|
return postJson<{ base64: string; screenWidth: number; screenHeight: number }>(`${APP_BASE}/api/appium-recorder/image-check/capture`, { deviceId, region, screenWidth, screenHeight });
|
|
22
32
|
}
|
|
@@ -138,7 +148,7 @@ type ReplayStreamEvent =
|
|
|
138
148
|
| { type: 'error'; message: string };
|
|
139
149
|
|
|
140
150
|
export async function replayAppiumScript(
|
|
141
|
-
input: { id: string; deviceId?: string; recordVideo?: boolean; parameters?: import('./variables').TestVariable[] },
|
|
151
|
+
input: { id: string; deviceId?: string; recordVideo?: boolean; reportSummaryEnabled?: boolean; reportSummaryPrompt?: string; parameters?: import('./variables').TestVariable[] },
|
|
142
152
|
onOutput?: (line: string) => void,
|
|
143
153
|
) {
|
|
144
154
|
const response = await fetch(`${APP_BASE}/api/appium-recorder/scripts/${encodeURIComponent(input.id)}/replay`, {
|
|
@@ -147,7 +157,13 @@ export async function replayAppiumScript(
|
|
|
147
157
|
'Content-Type': 'application/json',
|
|
148
158
|
Accept: 'application/x-ndjson',
|
|
149
159
|
},
|
|
150
|
-
body: JSON.stringify({
|
|
160
|
+
body: JSON.stringify({
|
|
161
|
+
deviceId: input.deviceId,
|
|
162
|
+
parameters: input.parameters,
|
|
163
|
+
recordVideo: input.recordVideo,
|
|
164
|
+
reportSummaryEnabled: input.reportSummaryEnabled,
|
|
165
|
+
reportSummaryPrompt: input.reportSummaryPrompt,
|
|
166
|
+
}),
|
|
151
167
|
});
|
|
152
168
|
if (!response.ok || !response.body) return readJson<ReplayResult>(response);
|
|
153
169
|
|
|
@@ -177,6 +193,10 @@ export async function replayAppiumScript(
|
|
|
177
193
|
return result;
|
|
178
194
|
}
|
|
179
195
|
|
|
196
|
+
export function checkReportSummaryModel() {
|
|
197
|
+
return postJson<{ enabled: boolean; available: boolean; message?: string }>(`${APP_BASE}/api/appium-recorder/report-summary/check`);
|
|
198
|
+
}
|
|
199
|
+
|
|
180
200
|
export function stopAppiumReplay(deviceId?: string) {
|
|
181
201
|
return postJson<{ success: boolean; stopped: boolean }>(
|
|
182
202
|
`${APP_BASE}/api/appium-recorder/replay/stop`,
|