android-midscene-automation 0.1.21 → 0.1.23

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "android-midscene-automation",
3
- "version": "0.1.21",
3
+ "version": "0.1.23",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "git+https://github.com/oooooooko/android-midscene-automation.git"
@@ -11,7 +11,7 @@
11
11
  "homepage": "https://github.com/oooooooko/android-midscene-automation#readme",
12
12
  "type": "module",
13
13
  "bin": {
14
- "android-midscene-automation": "./bin/android-midscene-automation.js"
14
+ "android-midscene-automation": "bin/android-midscene-automation.js"
15
15
  },
16
16
  "files": [
17
17
  "bin",
@@ -23,6 +23,7 @@
23
23
  "vite.config.ts",
24
24
  "README.md",
25
25
  "USAGE.md",
26
+ "FAQ.md",
26
27
  "CHANGELOG.md"
27
28
  ],
28
29
  "scripts": {
@@ -35,25 +36,40 @@
35
36
  "test:e2e": "playwright test"
36
37
  },
37
38
  "dependencies": {
38
- "@element-plus/icons-vue": "^2.3.2",
39
- "@midscene/android": "1.10.12",
40
- "@midscene/web": "1.10.12",
41
- "@vitejs/plugin-vue": "^6.0.1",
42
- "element-plus": "^2.11.1",
43
- "openai": "^5.12.2",
44
- "tsx": "^4.20.3",
45
- "typescript": "^5.8.3",
46
- "vite": "^7.0.4",
47
- "vue": "^3.5.18",
48
- "word-extractor": "^1.0.4",
49
- "xlsx": "^0.18.5",
50
- "yaml": "^2.8.1"
39
+ "@dagrejs/dagre": "1.1.5",
40
+ "@element-plus/icons-vue": "2.3.2",
41
+ "@midscene/android": "https://registry.npmjs.org/@midscene/android/-/android-1.12.0.tgz",
42
+ "@midscene/android-playground": "1.12.0",
43
+ "@midscene/core": "https://registry.npmjs.org/@midscene/core/-/core-1.12.0.tgz",
44
+ "@midscene/playground": "https://registry.npmjs.org/@midscene/playground/-/playground-1.12.0.tgz",
45
+ "@midscene/shared": "https://registry.npmjs.org/@midscene/shared/-/shared-1.12.0.tgz",
46
+ "@midscene/web": "1.12.0",
47
+ "@vitejs/plugin-vue": "6.0.7",
48
+ "@vue-flow/core": "1.48.2",
49
+ "element-plus": "2.11.1",
50
+ "openai": "5.23.2",
51
+ "tsx": "4.23.0",
52
+ "typescript": "5.9.3",
53
+ "vite": "7.3.6",
54
+ "vue": "3.5.39",
55
+ "word-extractor": "1.0.4",
56
+ "xlsx": "0.18.5",
57
+ "yaml": "2.9.0"
58
+ },
59
+ "overrides": {
60
+ "@floating-ui/core": "1.7.5",
61
+ "@floating-ui/dom": "1.7.6",
62
+ "nanoid": "3.3.15",
63
+ "playwright": "1.61.1",
64
+ "playwright-core": "1.61.1",
65
+ "postcss": "8.5.16",
66
+ "rollup": "4.62.2"
51
67
  },
52
68
  "devDependencies": {
53
- "@playwright/test": "^1.54.1",
54
- "@types/node": "^24.0.13",
55
- "@types/word-extractor": "^1.0.6",
56
- "@vue/tsconfig": "^0.7.0",
57
- "vue-tsc": "^3.0.3"
69
+ "@playwright/test": "1.61.1",
70
+ "@types/node": "24.13.2",
71
+ "@types/word-extractor": "1.0.6",
72
+ "@vue/tsconfig": "0.7.0",
73
+ "vue-tsc": "3.3.6"
58
74
  }
59
75
  }
@@ -122,6 +122,27 @@ export function launchAppOnDevice(deviceId: string, packageName: string) {
122
122
  });
123
123
  }
124
124
 
125
+ export function clearAppDataOnDevice(deviceId: string, packageName: string) {
126
+ if (!packageName) return Promise.reject(new Error('清理 App 缓存缺少包名'));
127
+ return new Promise<void>((resolve, reject) => {
128
+ execFile(getAdbCommand(), [
129
+ '-s',
130
+ deviceId,
131
+ 'shell',
132
+ 'pm',
133
+ 'clear',
134
+ packageName,
135
+ ], { maxBuffer: 4 * 1024 * 1024 }, (error, stdout, stderr) => {
136
+ const output = `${stdout || ''}\n${stderr || ''}`.trim();
137
+ if (error || !/^success$/im.test(output)) {
138
+ reject(new Error(output || error?.message || `ADB 清理 ${packageName} 失败`));
139
+ return;
140
+ }
141
+ resolve();
142
+ });
143
+ });
144
+ }
145
+
125
146
  function adbSwipe(deviceId: string, step: Required<AppiumRecordedStepRecord>['swipe']) {
126
147
  return new Promise<void>((resolve, reject) => {
127
148
  execFile(getAdbCommand(), [
@@ -500,6 +521,12 @@ async function runStep(sessionId: string, deviceId: string, step: AppiumRecorded
500
521
  return `ADB 已启动 ${packageName}`;
501
522
  }
502
523
 
524
+ if (step.type === 'clearAppData') {
525
+ const packageName = step.value || '';
526
+ await clearAppDataOnDevice(deviceId, packageName);
527
+ return `ADB 已清除 ${packageName} 的应用数据与缓存`;
528
+ }
529
+
503
530
  if (step.type === 'screenshot') {
504
531
  await saveScreenshot(sessionId);
505
532
  return;
@@ -710,10 +737,20 @@ async function appendStepDiagnostics(lines: string[], deviceId: string, step: Ap
710
737
  }
711
738
 
712
739
  type ReplayStepOptions = {
713
- skipLaunchApp?: boolean;
740
+ skipAppInitialization?: boolean;
714
741
  scriptName?: string;
715
742
  };
716
743
 
744
+ function shouldSkipLinkedScriptInitStep(options: ReplayStepOptions, step: AppiumRecordedStepRecord) {
745
+ return Boolean(options.skipAppInitialization && (step.type === 'launchApp' || step.type === 'clearAppData'));
746
+ }
747
+
748
+ function linkedScriptInitSkipReason(step: AppiumRecordedStepRecord) {
749
+ return step.type === 'clearAppData'
750
+ ? '连接脚本不重复清理 App'
751
+ : '连接脚本不重复启动 App';
752
+ }
753
+
717
754
  async function replayLinkedScript(
718
755
  sessionId: string,
719
756
  deviceId: string,
@@ -754,7 +791,7 @@ async function replayLinkedScript(
754
791
  lines.push(`连接脚本开始:${linkedScript.name}`);
755
792
  const nextStack = [...stack, linkedScript.id];
756
793
  await replayScriptSteps(sessionId, deviceId, linkedScript.steps, lines, nextStack, {
757
- skipLaunchApp: true,
794
+ skipAppInitialization: true,
758
795
  scriptName: linkedScript.name,
759
796
  });
760
797
  lines.push(`连接脚本完成:${linkedScript.name}`);
@@ -770,8 +807,8 @@ async function replayLinearSteps(
770
807
  ) {
771
808
  for (const [index, step] of steps.entries()) {
772
809
  throwIfReplayStopped();
773
- if (options.skipLaunchApp && step.type === 'launchApp') {
774
- lines.push(`[节点 ${index + 1}] 跳过:${step.label}(连接脚本不重复启动 App)`);
810
+ if (shouldSkipLinkedScriptInitStep(options, step)) {
811
+ lines.push(`[节点 ${index + 1}] 跳过:${step.label}(${linkedScriptInitSkipReason(step)})`);
775
812
  continue;
776
813
  }
777
814
  lines.push(`[节点 ${index + 1}] 开始:${step.label}`);
@@ -841,8 +878,8 @@ async function replayFlowSteps(
841
878
  if (guard > maxVisits) throw new Error('流程图可能存在循环,已终止回放');
842
879
 
843
880
  const step = steps[index];
844
- if (options.skipLaunchApp && step.type === 'launchApp') {
845
- lines.push(`[节点 ${index + 1}] 跳过:${step.label}(连接脚本不重复启动 App)`);
881
+ if (shouldSkipLinkedScriptInitStep(options, step)) {
882
+ lines.push(`[节点 ${index + 1}] 跳过:${step.label}(${linkedScriptInitSkipReason(step)})`);
846
883
  index = nextIndexAfterStep(index, step);
847
884
  continue;
848
885
  }
@@ -26,6 +26,7 @@ export type AppiumRecordedStepRecord = {
26
26
  | 'swipe'
27
27
  | 'screenshot'
28
28
  | 'launchApp'
29
+ | 'clearAppData'
29
30
  | 'waitDisappear'
30
31
  | 'assertText'
31
32
  | 'longPress'
@@ -11,7 +11,7 @@ import {
11
11
  saveAppiumRecordedScript,
12
12
  type AppiumRecordedStepRecord,
13
13
  } from './repository';
14
- import { launchAppOnDevice, replayAppiumScript } from './appium-runner';
14
+ import { clearAppDataOnDevice, launchAppOnDevice, replayAppiumScript } from './appium-runner';
15
15
  import { isRemoteDeviceId, sendRemoteCommand } from '../remote-agents/registry';
16
16
  import { getAdbCommand } from '../android-sdk';
17
17
 
@@ -205,6 +205,19 @@ export async function handleAppiumRecorderRequest(
205
205
  return true;
206
206
  }
207
207
 
208
+ if (pathname === '/api/appium-recorder/clear-app-data' && req.method === 'POST') {
209
+ const parsed = await readBody<{ deviceId?: string; packageName?: string }>(req);
210
+ const deviceId = parsed.deviceId?.trim() || selectedDeviceId;
211
+ const packageName = parsed.packageName?.trim() || '';
212
+ if (!deviceId) throw new Error('未检测到可用设备');
213
+ if (!packageName) throw new Error('请选择预设 App');
214
+ assertDeviceAllowed(deviceId);
215
+ if (isRemoteDeviceId(deviceId)) throw new Error('远程设备暂不支持清理 App 缓存');
216
+ await clearAppDataOnDevice(deviceId, packageName);
217
+ sendJson(res, { success: true });
218
+ return true;
219
+ }
220
+
208
221
  if (pathname === '/api/appium-recorder/scripts' && req.method === 'GET') {
209
222
  sendJson(res, { scripts: listAppiumRecordedScripts() });
210
223
  return true;
@@ -95,9 +95,13 @@ function writeNdjson(res: ServerResponse, event: RunGeneratedScriptEvent | { typ
95
95
  res.write(`${JSON.stringify(event)}\n`);
96
96
  }
97
97
 
98
- function npxCommand() {
99
- return process.platform === 'win32' ? 'npx.cmd' : 'npx';
100
- }
98
+ const START_ANDROID_PLAYGROUND_WITHOUT_BROWSER = [
99
+ "import { androidPlaygroundPlatform, ScrcpyServer } from '@midscene/android-playground';",
100
+ "import { launchPreparedPlaygroundPlatform } from '@midscene/playground';",
101
+ 'const scrcpyServer = new ScrcpyServer();',
102
+ 'const prepared = await androidPlaygroundPlatform.prepare({ scrcpyServer });',
103
+ 'await launchPreparedPlaygroundPlatform(prepared);',
104
+ ].join('\n');
101
105
 
102
106
  type AndroidDevice = {
103
107
  id: string;
@@ -513,7 +517,7 @@ export function createApiMiddleware() {
513
517
  };
514
518
  }
515
519
 
516
- playgroundProcess = spawn(npxCommand(), ['--yes', '@midscene/android-playground'], {
520
+ playgroundProcess = spawn(process.execPath, ['--input-type=module', '--eval', START_ANDROID_PLAYGROUND_WITHOUT_BROWSER], {
517
521
  cwd: appPath(),
518
522
  env: {
519
523
  ...process.env,
@@ -30,6 +30,18 @@ type ModelTestResult = {
30
30
  usage?: ModelUsage;
31
31
  };
32
32
 
33
+ function normalizeBaseUrl(value: string) {
34
+ return value.trim().replace(/\/+$/, '');
35
+ }
36
+
37
+ function normalizeModelName(value: string) {
38
+ return value.trim().replace(/[‐‑‒–—―]/g, '-');
39
+ }
40
+
41
+ function visibleContent(value: string) {
42
+ return value.replace(/<think>[\s\S]*?<\/think>/g, '').trim() || value.trim();
43
+ }
44
+
33
45
  function execCodex(args: string[]) {
34
46
  return new Promise<string>((resolve, reject) => {
35
47
  execFile('codex', args, { timeout: 10_000 }, (error, stdout, stderr) => {
@@ -63,23 +75,27 @@ async function testCodexConnection(input: ModelInput): Promise<ModelTestResult>
63
75
  }
64
76
 
65
77
  export async function testModelConnection(input: ModelInput): Promise<ModelTestResult> {
66
- if (input.provider === 'codex' || input.baseUrl === 'codex://app-server') {
78
+ const baseUrl = normalizeBaseUrl(input.baseUrl);
79
+ const modelName = normalizeModelName(input.name);
80
+
81
+ if (input.provider === 'codex' || baseUrl === 'codex://app-server') {
67
82
  return testCodexConnection(input);
68
83
  }
69
84
 
70
- if (!input.baseUrl || !input.apiKey || !input.name) {
85
+ if (!baseUrl || !input.apiKey.trim() || !modelName) {
71
86
  throw new Error('模型配置不完整');
72
87
  }
73
88
 
74
89
  const startedAt = Date.now();
75
90
  const client = new OpenAI({
76
- apiKey: input.apiKey,
77
- baseURL: input.baseUrl,
91
+ apiKey: input.apiKey.trim(),
92
+ baseURL: baseUrl,
78
93
  });
79
94
 
80
95
  const result = await client.chat.completions.create({
81
- model: input.name,
96
+ model: modelName,
82
97
  temperature: 0,
98
+ max_completion_tokens: 128,
83
99
  messages: [
84
100
  {
85
101
  role: 'user',
@@ -88,7 +104,7 @@ export async function testModelConnection(input: ModelInput): Promise<ModelTestR
88
104
  ],
89
105
  });
90
106
 
91
- const content = result.choices[0]?.message?.content?.trim() || '';
107
+ const content = visibleContent(result.choices[0]?.message?.content || '');
92
108
  const usage: ModelUsage = {
93
109
  promptTokens: result.usage?.prompt_tokens,
94
110
  completionTokens: result.usage?.completion_tokens,
package/src/App.vue CHANGED
@@ -1391,7 +1391,10 @@ onUnmounted(() => {
1391
1391
 
1392
1392
  <main
1393
1393
  class="page-body"
1394
- :class="{ 'page-body--fixed': activeMenu === 'generator' && activeGeneratorMode === 'ai' }"
1394
+ :class="{
1395
+ 'page-body--fixed': activeMenu === 'generator' && activeGeneratorMode === 'ai',
1396
+ 'page-body--appium': activeMenu === 'appium',
1397
+ }"
1395
1398
  >
1396
1399
  <el-alert v-if="activeMenu !== 'appium' && errorMessage" type="error" :closable="false" show-icon class="page-alert">
1397
1400
  <template #title>{{ errorMessage }}</template>
package/src/api.ts CHANGED
@@ -32,6 +32,8 @@ async function postJson<T>(url: string, body?: unknown) {
32
32
  method: 'POST',
33
33
  headers: { 'Content-Type': 'application/json' },
34
34
  body: JSON.stringify(body || {}),
35
+ }).catch((error) => {
36
+ throw new Error(`无法连接本地后端 ${url}:${error instanceof Error ? error.message : '网络请求失败'}`);
35
37
  });
36
38
  return readJson<T>(response);
37
39
  }
@@ -287,4 +289,4 @@ export async function runScript(
287
289
  if (buffer.trim()) {
288
290
  onEvent(JSON.parse(buffer) as RunScriptStreamEvent);
289
291
  }
290
- }
292
+ }