android-midscene-automation 0.1.2 → 0.1.4

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.
@@ -9,6 +9,7 @@ const packageRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '
9
9
  const viteRoot = path.dirname(require.resolve('vite/package.json'));
10
10
  const viteBin = path.join(viteRoot, 'bin', 'vite.js');
11
11
  const args = process.argv.slice(2);
12
+ const userRoot = process.cwd();
12
13
 
13
14
  if (!args.includes('--host')) {
14
15
  args.unshift('127.0.0.1');
@@ -17,7 +18,11 @@ if (!args.includes('--host')) {
17
18
 
18
19
  const child = spawn(process.execPath, [viteBin, ...args], {
19
20
  cwd: packageRoot,
20
- env: process.env,
21
+ env: {
22
+ ...process.env,
23
+ ANDROID_MIDSCENE_PACKAGE_ROOT: process.env.ANDROID_MIDSCENE_PACKAGE_ROOT || packageRoot,
24
+ ANDROID_MIDSCENE_DATA_ROOT: process.env.ANDROID_MIDSCENE_DATA_ROOT || userRoot,
25
+ },
21
26
  stdio: 'inherit',
22
27
  });
23
28
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "android-midscene-automation",
3
- "version": "0.1.2",
3
+ "version": "0.1.4",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "android-midscene-automation": "./bin/android-midscene-automation.js"
@@ -30,7 +30,6 @@
30
30
  "@vitejs/plugin-vue": "^6.0.1",
31
31
  "element-plus": "^2.11.1",
32
32
  "openai": "^5.12.2",
33
- "playwright": "^1.54.1",
34
33
  "tsx": "^4.20.3",
35
34
  "typescript": "^5.8.3",
36
35
  "vite": "^7.0.4",
@@ -2,6 +2,7 @@ import { execFile } from 'node:child_process';
2
2
  import { mkdir, writeFile } from 'node:fs/promises';
3
3
  import { join } from 'node:path';
4
4
  import type { AppiumRecordedScriptRecord, AppiumRecordedStepRecord } from './repository';
5
+ import { appDataPath } from '../paths';
5
6
 
6
7
  type AppiumSessionResponse = {
7
8
  value?: {
@@ -240,7 +241,7 @@ async function waitForElementGone(sessionId: string, step: AppiumRecordedStepRec
240
241
  async function saveScreenshot(sessionId: string) {
241
242
  const payload = await appiumRequest<AppiumValueResponse<string>>(`/session/${sessionId}/screenshot`);
242
243
  if (!payload.value) throw new Error('Appium 未返回截图数据');
243
- const dir = join(process.cwd(), '.midscene-app', 'screenshots');
244
+ const dir = appDataPath('.midscene-app', 'screenshots');
244
245
  await mkdir(dir, { recursive: true });
245
246
  const file = join(dir, `appium-${Date.now()}.png`);
246
247
  await writeFile(file, Buffer.from(payload.value, 'base64'));
@@ -1,8 +1,5 @@
1
- import { execFileSync } from 'node:child_process';
2
- import fs from 'node:fs';
3
- import path from 'node:path';
4
- import { appPath } from './paths';
5
1
  import type { AppConfig } from './config';
2
+ import { querySql, runSql, sqlString } from './storage/sqlite';
6
3
 
7
4
  export type AppPresetRecord = {
8
5
  id: string;
@@ -20,39 +17,12 @@ type AppPresetRow = {
20
17
  updated_at: string;
21
18
  };
22
19
 
23
- const dbPath = appPath('.midscene-app', 'script-cache.sqlite');
24
- const sqliteBin = process.env.SQLITE3_BIN || '/usr/bin/sqlite3';
25
20
  let initialized = false;
26
21
 
27
22
  function createId() {
28
23
  return `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`;
29
24
  }
30
25
 
31
- function ensureDbDir() {
32
- fs.mkdirSync(path.dirname(dbPath), { recursive: true });
33
- }
34
-
35
- function sqlString(value: string) {
36
- return `'${value.replace(/'/g, "''")}'`;
37
- }
38
-
39
- function runSql(sql: string) {
40
- ensureDbDir();
41
- execFileSync(sqliteBin, [dbPath], {
42
- input: sql,
43
- maxBuffer: 20 * 1024 * 1024,
44
- });
45
- }
46
-
47
- function querySql<T>(sql: string) {
48
- ensureDbDir();
49
- const output = execFileSync(sqliteBin, ['-json', dbPath, sql], {
50
- encoding: 'utf8',
51
- maxBuffer: 20 * 1024 * 1024,
52
- });
53
- return JSON.parse(output || '[]') as T[];
54
- }
55
-
56
26
  function rowToAppPreset(row: AppPresetRow): AppPresetRecord {
57
27
  return {
58
28
  id: row.id,
package/server/config.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import fs from 'node:fs';
2
2
  import path from 'node:path';
3
3
  import YAML from 'yaml';
4
- import { appPath } from './paths';
4
+ import { appDataPath } from './paths';
5
5
  import { loadModelConfigFromDb, saveModelConfigToDb } from './config-store';
6
6
 
7
7
  export type AppConfig = {
@@ -25,8 +25,8 @@ export type AppConfig = {
25
25
  };
26
26
 
27
27
  let cachedConfig: AppConfig | null = null;
28
- const configPath = appPath('config.json');
29
- const legacyConfigPath = appPath('config.yaml');
28
+ const configPath = appDataPath('config.json');
29
+ const legacyConfigPath = appDataPath('config.yaml');
30
30
 
31
31
  function defaultConfig(): AppConfig {
32
32
  return {
@@ -1,6 +1,6 @@
1
1
  import fs from 'node:fs';
2
2
  import path from 'node:path';
3
- import { appPath } from './paths';
3
+ import { appDataPath } from './paths';
4
4
  import { saveModelUsageRecord } from './model-usage-repository';
5
5
 
6
6
  type MidsceneModelConfig = {
@@ -31,7 +31,7 @@ type ModelCallRecord = {
31
31
  };
32
32
 
33
33
  function modelRequestDir() {
34
- return appPath('midscene_run', 'model-requests');
34
+ return appDataPath('midscene_run', 'model-requests');
35
35
  }
36
36
 
37
37
  function numberValue(value: unknown) {
package/server/paths.ts CHANGED
@@ -9,11 +9,15 @@
9
9
  // the standalone flow behaves exactly as before.
10
10
  import path from 'node:path';
11
11
 
12
- let appRoot = process.cwd();
12
+ let appRoot = process.env.ANDROID_MIDSCENE_PACKAGE_ROOT || process.cwd();
13
+ let appDataRoot = process.env.ANDROID_MIDSCENE_DATA_ROOT || appRoot;
13
14
 
14
15
  /** Point all app runtime paths at an explicit root (used by the DSH plugin). */
15
16
  export function setAppRoot(root: string) {
16
17
  appRoot = root;
18
+ if (!process.env.ANDROID_MIDSCENE_DATA_ROOT) {
19
+ appDataRoot = root;
20
+ }
17
21
  }
18
22
 
19
23
  /** The current app root (defaults to process.cwd()). */
@@ -25,3 +29,8 @@ export function getAppRoot() {
25
29
  export function appPath(...segments: string[]) {
26
30
  return path.resolve(appRoot, ...segments);
27
31
  }
32
+
33
+ /** Resolve one or more segments under the writable user data root. */
34
+ export function appDataPath(...segments: string[]) {
35
+ return path.resolve(appDataRoot, ...segments);
36
+ }
@@ -1,7 +1,7 @@
1
- import { execFileSync } from 'node:child_process';
2
1
  import fs from 'node:fs';
3
- import path from 'node:path';
4
2
  import { appPath } from './paths';
3
+ import { appDataPath } from './paths';
4
+ import { querySql, runSql, sqlString, sqlJson } from './storage/sqlite';
5
5
 
6
6
  export type ScriptStepRecord = {
7
7
  id?: string;
@@ -39,40 +39,14 @@ type ScriptRow = {
39
39
  updated_at: string;
40
40
  };
41
41
 
42
- const dbPath = appPath('.midscene-app', 'script-cache.sqlite');
43
- const legacyJsonPath = appPath('midscene_run', 'script-db.json');
44
- const sqliteBin = process.env.SQLITE3_BIN || '/usr/bin/sqlite3';
42
+ const dbPath = appDataPath('.midscene-app', 'script-cache.sqlite');
43
+ const legacyJsonPath = appDataPath('midscene_run', 'script-db.json');
45
44
  let initialized = false;
46
45
 
47
46
  function createId() {
48
47
  return `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`;
49
48
  }
50
49
 
51
- function ensureDbDir() {
52
- fs.mkdirSync(path.dirname(dbPath), { recursive: true });
53
- }
54
-
55
- function sqlString(value: string) {
56
- return `'${value.replace(/'/g, "''")}'`;
57
- }
58
-
59
- function runSql(sql: string) {
60
- ensureDbDir();
61
- execFileSync(sqliteBin, [dbPath], {
62
- input: sql,
63
- maxBuffer: 20 * 1024 * 1024,
64
- });
65
- }
66
-
67
- function querySql<T>(sql: string) {
68
- ensureDbDir();
69
- const output = execFileSync(sqliteBin, ['-json', dbPath, sql], {
70
- encoding: 'utf8',
71
- maxBuffer: 20 * 1024 * 1024,
72
- });
73
- return JSON.parse(output || '[]') as T[];
74
- }
75
-
76
50
  function rowToRecord(row: ScriptRow): ScriptRecord {
77
51
  return {
78
52
  id: row.id,
@@ -1,9 +1,10 @@
1
1
  import fs from 'node:fs';
2
2
  import path from 'node:path';
3
3
  import { spawn } from 'node:child_process';
4
+ import { createRequire } from 'node:module';
4
5
  import { pathToFileURL } from 'node:url';
5
6
  import ts from 'typescript';
6
- import { appPath } from './paths';
7
+ import { appDataPath, appPath } from './paths';
7
8
  import {
8
9
  getScriptRecord,
9
10
  removeScriptRecord,
@@ -14,6 +15,8 @@ import {
14
15
 
15
16
  const STEP_EVENT_PREFIX = '__MIDSCENE_STEP_EVENT__';
16
17
  const SCRIPT_TIMEOUT_MS = Number(process.env.MIDSCENE_SCRIPT_TIMEOUT_MS || 600_000);
18
+ const require = createRequire(import.meta.url);
19
+ const tsxBin = path.join(path.dirname(require.resolve('tsx/package.json')), 'dist', 'cli.mjs');
17
20
 
18
21
  function injectMidsceneEnv(code: string) {
19
22
  const configModuleUrl = pathToFileURL(appPath('server', 'config.ts')).href;
@@ -27,7 +30,7 @@ function toScriptFileName(scriptName: string) {
27
30
  }
28
31
 
29
32
  export function checkGeneratedScriptExists(input: { scriptName: string }) {
30
- const outputDir = appPath('scripts-output');
33
+ const outputDir = appDataPath('scripts-output');
31
34
  const filePath = path.join(outputDir, toScriptFileName(input.scriptName));
32
35
  return {
33
36
  exists: fs.existsSync(filePath),
@@ -44,7 +47,7 @@ export function saveGeneratedScript(input: {
44
47
  }) {
45
48
  validateGeneratedScriptCode(input.code);
46
49
 
47
- const outputDir = appPath('scripts-output');
50
+ const outputDir = appDataPath('scripts-output');
48
51
  fs.mkdirSync(outputDir, { recursive: true });
49
52
 
50
53
  const filePath = path.join(outputDir, toScriptFileName(input.scriptName));
@@ -129,7 +132,7 @@ export function deleteGeneratedScript(input: { id: string }) {
129
132
  };
130
133
  }
131
134
 
132
- const outputDir = appPath('scripts-output');
135
+ const outputDir = appDataPath('scripts-output');
133
136
  const filePath = path.resolve(record.filePath);
134
137
  const canDeleteFile = filePath.startsWith(`${outputDir}${path.sep}`) && fs.existsSync(filePath);
135
138
  if (canDeleteFile) {
@@ -432,8 +435,8 @@ export async function runGeneratedScript(input: {
432
435
  fs.writeFileSync(filePath, injectMidsceneEnv(resolveExecutableCode(input)), 'utf8');
433
436
 
434
437
  return await new Promise<{ success: boolean; output: string; filePath: string }>((resolve) => {
435
- const child = spawn('npx', ['tsx', filePath], {
436
- cwd: appPath(),
438
+ const child = spawn(process.execPath, [tsxBin, filePath], {
439
+ cwd: appDataPath(),
437
440
  env: {
438
441
  ...process.env,
439
442
  MIDSCENE_RECORD_MODEL_CALL: 'true',
@@ -1,10 +1,12 @@
1
1
  import { execFileSync } from 'node:child_process';
2
2
  import fs from 'node:fs';
3
+ import { createRequire } from 'node:module';
3
4
  import path from 'node:path';
4
- import { appPath } from '../paths';
5
+ import { appDataPath } from '../paths';
5
6
 
6
- const dbPath = appPath('.midscene-app', 'script-cache.sqlite');
7
- const sqliteBin = process.env.SQLITE3_BIN || '/usr/bin/sqlite3';
7
+ const dbPath = appDataPath('.midscene-app', 'script-cache.sqlite');
8
+ const require = createRequire(import.meta.url);
9
+ let databaseSyncCtor: unknown;
8
10
 
9
11
  export function getRuntimeDbPath() {
10
12
  return dbPath;
@@ -30,20 +32,81 @@ function ensureDbDir() {
30
32
  fs.mkdirSync(path.dirname(dbPath), { recursive: true });
31
33
  }
32
34
 
33
- // 使用 sqlite3 CLI 保持项目零新增依赖;所有运行态功能共用同一个 SQLite 文件。
35
+ function getSqliteBin() {
36
+ return process.env.SQLITE3_BIN || (process.platform === 'win32' ? 'sqlite3.exe' : 'sqlite3');
37
+ }
38
+
39
+ function getDatabaseSync() {
40
+ if (databaseSyncCtor !== undefined) {
41
+ return databaseSyncCtor as null | { new (filename: string): { exec(sql: string): void; prepare(sql: string): { all(): unknown[] }; close(): void } };
42
+ }
43
+
44
+ try {
45
+ databaseSyncCtor = (require('node:sqlite') as {
46
+ DatabaseSync?: { new (filename: string): { exec(sql: string): void; prepare(sql: string): { all(): unknown[] }; close(): void } };
47
+ }).DatabaseSync || null;
48
+ } catch {
49
+ databaseSyncCtor = null;
50
+ }
51
+
52
+ return databaseSyncCtor as null | { new (filename: string): { exec(sql: string): void; prepare(sql: string): { all(): unknown[] }; close(): void } };
53
+ }
54
+
55
+ function runWithNodeSqlite<T>(callback: (db: InstanceType<NonNullable<ReturnType<typeof getDatabaseSync>>>) => T) {
56
+ const DatabaseSync = getDatabaseSync();
57
+ if (!DatabaseSync) {
58
+ return null;
59
+ }
60
+
61
+ const db = new DatabaseSync(dbPath);
62
+ try {
63
+ return callback(db as InstanceType<NonNullable<ReturnType<typeof getDatabaseSync>>>);
64
+ } finally {
65
+ db.close();
66
+ }
67
+ }
68
+
69
+ function explainSqliteCliError(error: unknown) {
70
+ if ((error as NodeJS.ErrnoException).code !== 'ENOENT') {
71
+ throw error;
72
+ }
73
+
74
+ throw new Error(
75
+ `未找到 sqlite3 命令。请安装 sqlite3 并加入 PATH,或通过 SQLITE3_BIN 指定 sqlite3 可执行文件路径。当前尝试执行:${getSqliteBin()}`,
76
+ );
77
+ }
78
+
79
+ // 优先使用 Node 24 内置 sqlite;旧 Node 再回退到 sqlite3 CLI。
34
80
  export function runSql(sql: string) {
35
81
  ensureDbDir();
36
- execFileSync(sqliteBin, [dbPath], {
37
- input: sql,
38
- maxBuffer: 40 * 1024 * 1024,
82
+ const result = runWithNodeSqlite((db) => {
83
+ db.exec(sql);
84
+ return true;
39
85
  });
86
+ if (result) return;
87
+
88
+ try {
89
+ execFileSync(getSqliteBin(), [dbPath], {
90
+ input: sql,
91
+ maxBuffer: 40 * 1024 * 1024,
92
+ });
93
+ } catch (error) {
94
+ explainSqliteCliError(error);
95
+ }
40
96
  }
41
97
 
42
98
  export function querySql<T>(sql: string) {
43
99
  ensureDbDir();
44
- const output = execFileSync(sqliteBin, ['-json', dbPath, sql], {
45
- encoding: 'utf8',
46
- maxBuffer: 40 * 1024 * 1024,
47
- });
48
- return JSON.parse(output || '[]') as T[];
100
+ const result = runWithNodeSqlite((db) => db.prepare(sql).all() as T[]);
101
+ if (result) return result;
102
+
103
+ try {
104
+ const output = execFileSync(getSqliteBin(), ['-json', dbPath, sql], {
105
+ encoding: 'utf8',
106
+ maxBuffer: 40 * 1024 * 1024,
107
+ });
108
+ return JSON.parse(output || '[]') as T[];
109
+ } catch (error) {
110
+ explainSqliteCliError(error);
111
+ }
49
112
  }