android-midscene-automation 0.1.2 → 0.1.5
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/bin/android-midscene-automation.js +6 -1
- package/package.json +1 -2
- package/server/appium-recorder/appium-runner.ts +2 -1
- package/server/config-store.ts +1 -31
- package/server/config.ts +3 -3
- package/server/http-api.ts +9 -1
- package/server/model-call-usage-importer.ts +2 -2
- package/server/paths.ts +10 -1
- package/server/script-db.ts +4 -30
- package/server/script-runner.ts +9 -6
- package/server/storage/sqlite.ts +75 -12
|
@@ -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:
|
|
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.
|
|
3
|
+
"version": "0.1.5",
|
|
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 =
|
|
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'));
|
package/server/config-store.ts
CHANGED
|
@@ -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 {
|
|
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 =
|
|
29
|
-
const legacyConfigPath =
|
|
28
|
+
const configPath = appDataPath('config.json');
|
|
29
|
+
const legacyConfigPath = appDataPath('config.yaml');
|
|
30
30
|
|
|
31
31
|
function defaultConfig(): AppConfig {
|
|
32
32
|
return {
|
package/server/http-api.ts
CHANGED
|
@@ -94,6 +94,10 @@ function writeNdjson(res: ServerResponse, event: RunGeneratedScriptEvent | { typ
|
|
|
94
94
|
res.write(`${JSON.stringify(event)}\n`);
|
|
95
95
|
}
|
|
96
96
|
|
|
97
|
+
function npxCommand() {
|
|
98
|
+
return process.platform === 'win32' ? 'npx.cmd' : 'npx';
|
|
99
|
+
}
|
|
100
|
+
|
|
97
101
|
type AndroidDevice = {
|
|
98
102
|
id: string;
|
|
99
103
|
status: string;
|
|
@@ -506,7 +510,7 @@ export function createApiMiddleware() {
|
|
|
506
510
|
};
|
|
507
511
|
}
|
|
508
512
|
|
|
509
|
-
playgroundProcess = spawn(
|
|
513
|
+
playgroundProcess = spawn(npxCommand(), ['--yes', '@midscene/android-playground'], {
|
|
510
514
|
cwd: appPath(),
|
|
511
515
|
env: {
|
|
512
516
|
...process.env,
|
|
@@ -515,6 +519,10 @@ export function createApiMiddleware() {
|
|
|
515
519
|
stdio: 'ignore',
|
|
516
520
|
detached: false,
|
|
517
521
|
});
|
|
522
|
+
playgroundProcess.once('error', (error) => {
|
|
523
|
+
console.warn(`Android Playground 启动失败:${error.message}`);
|
|
524
|
+
playgroundProcess = null;
|
|
525
|
+
});
|
|
518
526
|
playgroundProcess.once('exit', () => {
|
|
519
527
|
playgroundProcess = null;
|
|
520
528
|
});
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
|
-
import {
|
|
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
|
|
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
|
+
}
|
package/server/script-db.ts
CHANGED
|
@@ -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 =
|
|
43
|
-
const legacyJsonPath =
|
|
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,
|
package/server/script-runner.ts
CHANGED
|
@@ -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 =
|
|
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 =
|
|
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 =
|
|
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(
|
|
436
|
-
cwd:
|
|
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',
|
package/server/storage/sqlite.ts
CHANGED
|
@@ -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 {
|
|
5
|
+
import { appDataPath } from '../paths';
|
|
5
6
|
|
|
6
|
-
const dbPath =
|
|
7
|
-
const
|
|
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
|
-
|
|
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
|
-
|
|
37
|
-
|
|
38
|
-
|
|
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
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
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
|
}
|