neoagent 2.1.12 → 2.1.14
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 +1 -1
- package/server/public/assets/fonts/MaterialIcons-Regular.otf +0 -0
- package/server/public/flutter_bootstrap.js +1 -1
- package/server/public/main.dart.js +31420 -31340
- package/server/routes/android.js +15 -4
- package/server/services/ai/engine.js +119 -7
- package/server/services/ai/history.js +63 -1
- package/server/services/ai/providers/base.js +17 -0
- package/server/services/ai/providers/grok.js +34 -0
- package/server/services/ai/providers/openai.js +34 -0
- package/server/services/ai/systemPrompt.js +1 -0
- package/server/services/ai/toolResult.js +2 -0
- package/server/services/ai/tools.js +76 -16
- package/server/services/android/controller.js +168 -27
- package/server/services/websocket.js +19 -0
|
@@ -276,6 +276,49 @@ function extractZip(zipPath, destDir) {
|
|
|
276
276
|
throw new Error('Neither unzip nor ditto is available to extract Android SDK archives');
|
|
277
277
|
}
|
|
278
278
|
|
|
279
|
+
function listFilesRecursive(rootDir, predicate, bucket = []) {
|
|
280
|
+
for (const entry of fs.readdirSync(rootDir, { withFileTypes: true })) {
|
|
281
|
+
const fullPath = path.join(rootDir, entry.name);
|
|
282
|
+
if (entry.isDirectory()) {
|
|
283
|
+
listFilesRecursive(fullPath, predicate, bucket);
|
|
284
|
+
continue;
|
|
285
|
+
}
|
|
286
|
+
if (!predicate || predicate(fullPath, entry)) {
|
|
287
|
+
bucket.push(fullPath);
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
return bucket;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
function resolveBundleInstallTargets(bundleDir) {
|
|
294
|
+
const apkFiles = listFilesRecursive(bundleDir, (filePath) => path.extname(filePath).toLowerCase() === '.apk')
|
|
295
|
+
.sort((a, b) => a.localeCompare(b));
|
|
296
|
+
if (apkFiles.length === 0) {
|
|
297
|
+
throw new Error('APK bundle did not contain any installable .apk files.');
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
const universalApk = apkFiles.find((filePath) => path.basename(filePath).toLowerCase() === 'universal.apk');
|
|
301
|
+
if (universalApk) {
|
|
302
|
+
return {
|
|
303
|
+
mode: 'single',
|
|
304
|
+
installPaths: [universalApk],
|
|
305
|
+
layout: 'universal',
|
|
306
|
+
};
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
if (apkFiles.length === 1) {
|
|
310
|
+
return {
|
|
311
|
+
mode: 'single',
|
|
312
|
+
installPaths: apkFiles,
|
|
313
|
+
layout: 'single-apk',
|
|
314
|
+
};
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
throw new Error(
|
|
318
|
+
'APK bundles must include a universal APK. Export a universal .apks bundle or upload a single .apk instead.'
|
|
319
|
+
);
|
|
320
|
+
}
|
|
321
|
+
|
|
279
322
|
function parseLatestCmdlineToolsUrl(xml) {
|
|
280
323
|
const tag = platformTag() === 'mac' ? 'macosx' : 'linux';
|
|
281
324
|
const packageMatch = xml.match(new RegExp(`<remotePackage\\s+path="cmdline-tools;latest">([\\s\\S]*?)<\\/remotePackage>`));
|
|
@@ -1104,7 +1147,7 @@ class AndroidController {
|
|
|
1104
1147
|
}
|
|
1105
1148
|
|
|
1106
1149
|
async screenshot(options = {}) {
|
|
1107
|
-
const serial = await this.ensureDevice();
|
|
1150
|
+
const serial = options.serial || await this.ensureDevice();
|
|
1108
1151
|
const filename = `android_${Date.now()}.png`;
|
|
1109
1152
|
const fullPath = path.join(SCREENSHOTS_DIR, filename);
|
|
1110
1153
|
await this.#run(`${quoteShell(adbBinary())} -s ${quoteShell(serial)} exec-out screencap -p > ${quoteShell(fullPath)}`, { timeout: 30000 });
|
|
@@ -1117,7 +1160,7 @@ class AndroidController {
|
|
|
1117
1160
|
}
|
|
1118
1161
|
|
|
1119
1162
|
async dumpUi(options = {}) {
|
|
1120
|
-
const serial = await this.ensureDevice();
|
|
1163
|
+
const serial = options.serial || await this.ensureDevice();
|
|
1121
1164
|
let xml = await this.#adb(serial, 'shell uiautomator dump --compressed /dev/tty', { timeout: 30000 });
|
|
1122
1165
|
if (!String(xml || '').includes('<hierarchy')) {
|
|
1123
1166
|
const remote = '/sdcard/neoagent-ui.xml';
|
|
@@ -1140,6 +1183,65 @@ class AndroidController {
|
|
|
1140
1183
|
};
|
|
1141
1184
|
}
|
|
1142
1185
|
|
|
1186
|
+
async #captureObservation(serial, options = {}) {
|
|
1187
|
+
const resolvedSerial = serial || await this.ensureDevice();
|
|
1188
|
+
const observation = {
|
|
1189
|
+
serial: resolvedSerial,
|
|
1190
|
+
screenshotPath: null,
|
|
1191
|
+
fullPath: null,
|
|
1192
|
+
uiDumpPath: null,
|
|
1193
|
+
nodeCount: null,
|
|
1194
|
+
preview: undefined,
|
|
1195
|
+
observationWarnings: [],
|
|
1196
|
+
};
|
|
1197
|
+
|
|
1198
|
+
if (options.screenshot !== false) {
|
|
1199
|
+
try {
|
|
1200
|
+
const shot = await this.screenshot({ serial: resolvedSerial });
|
|
1201
|
+
observation.screenshotPath = shot?.screenshotPath || null;
|
|
1202
|
+
observation.fullPath = shot?.fullPath || null;
|
|
1203
|
+
} catch (err) {
|
|
1204
|
+
observation.observationWarnings.push(`screenshot: ${err.message}`);
|
|
1205
|
+
}
|
|
1206
|
+
}
|
|
1207
|
+
|
|
1208
|
+
if (options.uiDump !== false) {
|
|
1209
|
+
try {
|
|
1210
|
+
const dump = await this.dumpUi({
|
|
1211
|
+
serial: resolvedSerial,
|
|
1212
|
+
includeNodes: options.includeNodes !== false,
|
|
1213
|
+
});
|
|
1214
|
+
observation.uiDumpPath = dump.uiDumpPath;
|
|
1215
|
+
observation.nodeCount = dump.nodeCount;
|
|
1216
|
+
observation.preview = dump.preview;
|
|
1217
|
+
} catch (err) {
|
|
1218
|
+
observation.observationWarnings.push(`ui_dump: ${err.message}`);
|
|
1219
|
+
}
|
|
1220
|
+
}
|
|
1221
|
+
|
|
1222
|
+
if (observation.observationWarnings.length === 0) {
|
|
1223
|
+
delete observation.observationWarnings;
|
|
1224
|
+
}
|
|
1225
|
+
|
|
1226
|
+
return observation;
|
|
1227
|
+
}
|
|
1228
|
+
|
|
1229
|
+
async observe(options = {}) {
|
|
1230
|
+
const serial = options.serial || await this.ensureDevice();
|
|
1231
|
+
const observation = await this.#captureObservation(serial, options);
|
|
1232
|
+
if (!observation.screenshotPath && !observation.uiDumpPath) {
|
|
1233
|
+
throw new Error(
|
|
1234
|
+
Array.isArray(observation.observationWarnings) && observation.observationWarnings.length > 0
|
|
1235
|
+
? observation.observationWarnings.join(' | ')
|
|
1236
|
+
: 'Unable to capture Android observation',
|
|
1237
|
+
);
|
|
1238
|
+
}
|
|
1239
|
+
return {
|
|
1240
|
+
success: true,
|
|
1241
|
+
...observation,
|
|
1242
|
+
};
|
|
1243
|
+
}
|
|
1244
|
+
|
|
1143
1245
|
async #resolveSelector(args = {}) {
|
|
1144
1246
|
const dump = await this.dumpUi({ includeNodes: false });
|
|
1145
1247
|
const selector = {
|
|
@@ -1164,27 +1266,27 @@ class AndroidController {
|
|
|
1164
1266
|
let y = Number(args.y);
|
|
1165
1267
|
let node = null;
|
|
1166
1268
|
let serial = await this.ensureDevice();
|
|
1167
|
-
let
|
|
1269
|
+
let resolvedFromUiDumpPath = null;
|
|
1168
1270
|
|
|
1169
1271
|
if (!Number.isFinite(x) || !Number.isFinite(y)) {
|
|
1170
1272
|
const resolved = await this.#resolveSelector(args);
|
|
1171
1273
|
serial = resolved.serial;
|
|
1172
1274
|
node = resolved.node;
|
|
1173
|
-
|
|
1275
|
+
resolvedFromUiDumpPath = resolved.uiDumpPath;
|
|
1174
1276
|
x = node.bounds.centerX;
|
|
1175
1277
|
y = node.bounds.centerY;
|
|
1176
1278
|
}
|
|
1177
1279
|
|
|
1178
1280
|
await this.#adb(serial, `shell input tap ${Math.round(x)} ${Math.round(y)}`, { timeout: 15000 });
|
|
1179
|
-
const
|
|
1281
|
+
const observation = await this.#captureObservation(serial, args);
|
|
1180
1282
|
return {
|
|
1181
1283
|
success: true,
|
|
1182
1284
|
serial,
|
|
1183
1285
|
x: Math.round(x),
|
|
1184
1286
|
y: Math.round(y),
|
|
1185
1287
|
target: summarizeNode(node),
|
|
1186
|
-
|
|
1187
|
-
|
|
1288
|
+
resolvedFromUiDumpPath,
|
|
1289
|
+
...observation,
|
|
1188
1290
|
};
|
|
1189
1291
|
}
|
|
1190
1292
|
|
|
@@ -1193,13 +1295,13 @@ class AndroidController {
|
|
|
1193
1295
|
let y = Number(args.y);
|
|
1194
1296
|
let node = null;
|
|
1195
1297
|
let serial = await this.ensureDevice();
|
|
1196
|
-
let
|
|
1298
|
+
let resolvedFromUiDumpPath = null;
|
|
1197
1299
|
|
|
1198
1300
|
if (!Number.isFinite(x) || !Number.isFinite(y)) {
|
|
1199
1301
|
const resolved = await this.#resolveSelector(args);
|
|
1200
1302
|
serial = resolved.serial;
|
|
1201
1303
|
node = resolved.node;
|
|
1202
|
-
|
|
1304
|
+
resolvedFromUiDumpPath = resolved.uiDumpPath;
|
|
1203
1305
|
x = node.bounds.centerX;
|
|
1204
1306
|
y = node.bounds.centerY;
|
|
1205
1307
|
}
|
|
@@ -1210,7 +1312,7 @@ class AndroidController {
|
|
|
1210
1312
|
`shell input swipe ${Math.round(x)} ${Math.round(y)} ${Math.round(x)} ${Math.round(y)} ${Math.round(durationMs)}`,
|
|
1211
1313
|
{ timeout: Math.max(15000, durationMs + 5000) },
|
|
1212
1314
|
);
|
|
1213
|
-
const
|
|
1315
|
+
const observation = await this.#captureObservation(serial, args);
|
|
1214
1316
|
return {
|
|
1215
1317
|
success: true,
|
|
1216
1318
|
serial,
|
|
@@ -1218,8 +1320,8 @@ class AndroidController {
|
|
|
1218
1320
|
y: Math.round(y),
|
|
1219
1321
|
durationMs,
|
|
1220
1322
|
target: summarizeNode(node),
|
|
1221
|
-
|
|
1222
|
-
|
|
1323
|
+
resolvedFromUiDumpPath,
|
|
1324
|
+
...observation,
|
|
1223
1325
|
};
|
|
1224
1326
|
}
|
|
1225
1327
|
|
|
@@ -1237,6 +1339,8 @@ class AndroidController {
|
|
|
1237
1339
|
description: args.description,
|
|
1238
1340
|
className: args.className,
|
|
1239
1341
|
clickable: true,
|
|
1342
|
+
screenshot: false,
|
|
1343
|
+
uiDump: false,
|
|
1240
1344
|
}).catch(() => {});
|
|
1241
1345
|
}
|
|
1242
1346
|
|
|
@@ -1244,12 +1348,12 @@ class AndroidController {
|
|
|
1244
1348
|
if (args.pressEnter) {
|
|
1245
1349
|
await this.#adb(serial, 'shell input keyevent 66', { timeout: 10000 });
|
|
1246
1350
|
}
|
|
1247
|
-
const
|
|
1351
|
+
const observation = await this.#captureObservation(serial, args);
|
|
1248
1352
|
return {
|
|
1249
1353
|
success: true,
|
|
1250
1354
|
serial,
|
|
1251
1355
|
typed: args.text || '',
|
|
1252
|
-
|
|
1356
|
+
...observation,
|
|
1253
1357
|
};
|
|
1254
1358
|
}
|
|
1255
1359
|
|
|
@@ -1264,11 +1368,11 @@ class AndroidController {
|
|
|
1264
1368
|
throw new Error('x1, y1, x2, and y2 are required for android_swipe');
|
|
1265
1369
|
}
|
|
1266
1370
|
await this.#adb(serial, `shell input swipe ${Math.round(x1)} ${Math.round(y1)} ${Math.round(x2)} ${Math.round(y2)} ${Math.round(duration)}`, { timeout: 15000 });
|
|
1267
|
-
const
|
|
1371
|
+
const observation = await this.#captureObservation(serial, args);
|
|
1268
1372
|
return {
|
|
1269
1373
|
success: true,
|
|
1270
1374
|
serial,
|
|
1271
|
-
|
|
1375
|
+
...observation,
|
|
1272
1376
|
};
|
|
1273
1377
|
}
|
|
1274
1378
|
|
|
@@ -1278,13 +1382,13 @@ class AndroidController {
|
|
|
1278
1382
|
const keyCode = Number.isFinite(Number(raw)) ? Number(raw) : (DEFAULT_KEYEVENTS[raw] || null);
|
|
1279
1383
|
if (!keyCode) throw new Error(`Unsupported Android key: ${args.key}`);
|
|
1280
1384
|
await this.#adb(serial, `shell input keyevent ${keyCode}`, { timeout: 10000 });
|
|
1281
|
-
const
|
|
1385
|
+
const observation = await this.#captureObservation(serial, args);
|
|
1282
1386
|
return {
|
|
1283
1387
|
success: true,
|
|
1284
1388
|
serial,
|
|
1285
1389
|
key: args.key,
|
|
1286
1390
|
keyCode,
|
|
1287
|
-
|
|
1391
|
+
...observation,
|
|
1288
1392
|
};
|
|
1289
1393
|
}
|
|
1290
1394
|
|
|
@@ -1304,13 +1408,17 @@ class AndroidController {
|
|
|
1304
1408
|
clickable: args.clickable,
|
|
1305
1409
|
});
|
|
1306
1410
|
if (node) {
|
|
1307
|
-
const
|
|
1411
|
+
const observation = await this.#captureObservation(dump.serial, {
|
|
1412
|
+
screenshot: args.screenshot !== false,
|
|
1413
|
+
uiDump: args.uiDump !== false,
|
|
1414
|
+
includeNodes: args.includeNodes,
|
|
1415
|
+
});
|
|
1308
1416
|
return {
|
|
1309
1417
|
success: true,
|
|
1310
1418
|
serial: dump.serial,
|
|
1311
1419
|
matched: summarizeNode(node),
|
|
1312
|
-
|
|
1313
|
-
|
|
1420
|
+
matchedFromUiDumpPath: dump.uiDumpPath,
|
|
1421
|
+
...observation,
|
|
1314
1422
|
};
|
|
1315
1423
|
}
|
|
1316
1424
|
await sleep(intervalMs);
|
|
@@ -1341,13 +1449,13 @@ class AndroidController {
|
|
|
1341
1449
|
} else {
|
|
1342
1450
|
throw new Error('packageName is required for android_open_app');
|
|
1343
1451
|
}
|
|
1344
|
-
const
|
|
1452
|
+
const observation = await this.#captureObservation(serial, args);
|
|
1345
1453
|
return {
|
|
1346
1454
|
success: true,
|
|
1347
1455
|
serial,
|
|
1348
1456
|
packageName: args.packageName,
|
|
1349
1457
|
activity: args.activity || null,
|
|
1350
|
-
|
|
1458
|
+
...observation,
|
|
1351
1459
|
};
|
|
1352
1460
|
}
|
|
1353
1461
|
|
|
@@ -1367,11 +1475,11 @@ class AndroidController {
|
|
|
1367
1475
|
}
|
|
1368
1476
|
|
|
1369
1477
|
await this.#adb(serial, parts.join(' '), { timeout: 20000 });
|
|
1370
|
-
const
|
|
1478
|
+
const observation = await this.#captureObservation(serial, args);
|
|
1371
1479
|
return {
|
|
1372
1480
|
success: true,
|
|
1373
1481
|
serial,
|
|
1374
|
-
|
|
1482
|
+
...observation,
|
|
1375
1483
|
};
|
|
1376
1484
|
}
|
|
1377
1485
|
|
|
@@ -1397,11 +1505,38 @@ class AndroidController {
|
|
|
1397
1505
|
const apkPath = path.resolve(String(args.apkPath || ''));
|
|
1398
1506
|
if (!apkPath || !fs.existsSync(apkPath)) throw new Error(`APK not found: ${apkPath}`);
|
|
1399
1507
|
const serial = await this.ensureDevice();
|
|
1508
|
+
const extension = path.extname(apkPath).toLowerCase();
|
|
1509
|
+
|
|
1510
|
+
if (extension === '.aab') {
|
|
1511
|
+
throw new Error('.aab app bundles are not directly installable. Export a .apks bundle or .apk first.');
|
|
1512
|
+
}
|
|
1513
|
+
|
|
1514
|
+
if (extension === '.apks') {
|
|
1515
|
+
const extractDir = fs.mkdtempSync(path.join(TMP_DIR, 'apk-bundle-'));
|
|
1516
|
+
try {
|
|
1517
|
+
extractZip(apkPath, extractDir);
|
|
1518
|
+
const bundle = resolveBundleInstallTargets(extractDir);
|
|
1519
|
+
await this.#adb(serial, `install -r ${quoteShell(bundle.installPaths[0])}`, { timeout: 300000 });
|
|
1520
|
+
return {
|
|
1521
|
+
success: true,
|
|
1522
|
+
serial,
|
|
1523
|
+
apkPath,
|
|
1524
|
+
artifactType: 'apks',
|
|
1525
|
+
installedPaths: bundle.installPaths,
|
|
1526
|
+
bundleLayout: bundle.layout,
|
|
1527
|
+
};
|
|
1528
|
+
} finally {
|
|
1529
|
+
fs.rmSync(extractDir, { recursive: true, force: true });
|
|
1530
|
+
}
|
|
1531
|
+
}
|
|
1532
|
+
|
|
1400
1533
|
await this.#adb(serial, `install -r ${quoteShell(apkPath)}`, { timeout: 300000 });
|
|
1401
1534
|
return {
|
|
1402
1535
|
success: true,
|
|
1403
1536
|
serial,
|
|
1404
1537
|
apkPath,
|
|
1538
|
+
artifactType: 'apk',
|
|
1539
|
+
installedPaths: [apkPath],
|
|
1405
1540
|
};
|
|
1406
1541
|
}
|
|
1407
1542
|
|
|
@@ -1412,13 +1547,19 @@ class AndroidController {
|
|
|
1412
1547
|
|
|
1413
1548
|
const timeout = Math.max(1000, Number(args.timeoutMs) || 20000);
|
|
1414
1549
|
const stdout = await this.#adb(serial, `shell ${quoteShell(command)}`, { timeout });
|
|
1415
|
-
const
|
|
1550
|
+
const observation = args.screenshot === true
|
|
1551
|
+
? await this.#captureObservation(serial)
|
|
1552
|
+
: null;
|
|
1416
1553
|
return {
|
|
1417
1554
|
success: true,
|
|
1418
1555
|
serial,
|
|
1419
1556
|
command,
|
|
1420
1557
|
stdout,
|
|
1421
|
-
screenshotPath:
|
|
1558
|
+
screenshotPath: observation?.screenshotPath || null,
|
|
1559
|
+
fullPath: observation?.fullPath || null,
|
|
1560
|
+
uiDumpPath: observation?.uiDumpPath || null,
|
|
1561
|
+
nodeCount: observation?.nodeCount,
|
|
1562
|
+
preview: observation?.preview,
|
|
1422
1563
|
};
|
|
1423
1564
|
}
|
|
1424
1565
|
|
|
@@ -76,6 +76,25 @@ function setupWebSocket(io, services) {
|
|
|
76
76
|
}
|
|
77
77
|
}
|
|
78
78
|
|
|
79
|
+
const activeRun = agentEngine.findSteerableRunForUser(userId, 'web');
|
|
80
|
+
if (activeRun) {
|
|
81
|
+
const queued = agentEngine.enqueueSteering(activeRun.runId, task, {
|
|
82
|
+
platform: 'web',
|
|
83
|
+
socketId: socket.id
|
|
84
|
+
});
|
|
85
|
+
if (queued) {
|
|
86
|
+
db.prepare('INSERT INTO conversation_history (user_id, agent_run_id, role, content, metadata) VALUES (?, ?, ?, ?, ?)')
|
|
87
|
+
.run(
|
|
88
|
+
userId,
|
|
89
|
+
activeRun.runId,
|
|
90
|
+
'user',
|
|
91
|
+
task,
|
|
92
|
+
JSON.stringify({ platform: 'web', steering: true })
|
|
93
|
+
);
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
79
98
|
db.prepare('INSERT INTO conversation_history (user_id, role, content, metadata) VALUES (?, ?, ?, ?)')
|
|
80
99
|
.run(userId, 'user', task, JSON.stringify({ platform: 'web' }));
|
|
81
100
|
|