neoagent 2.1.10 → 2.1.12
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/README.md +3 -0
- package/docs/configuration.md +1 -1
- 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 +53915 -53242
- package/server/routes/android.js +87 -0
- package/server/services/ai/engine.js +52 -14
- package/server/services/ai/outputSanitizer.js +67 -0
- package/server/services/ai/providers/anthropic.js +130 -1
- package/server/services/ai/systemPrompt.js +1 -0
- package/server/services/ai/toolCallSalvage.js +142 -0
- package/server/services/ai/toolResult.js +10 -0
- package/server/services/ai/tools.js +43 -0
- package/server/services/android/controller.js +161 -2
|
@@ -213,6 +213,24 @@ function getAvailableTools(app, options = {}) {
|
|
|
213
213
|
}
|
|
214
214
|
}
|
|
215
215
|
},
|
|
216
|
+
{
|
|
217
|
+
name: 'android_long_press',
|
|
218
|
+
description: 'Long-press an Android UI element or screen coordinate. Useful for context menus, drag handles, rearranging icons, and long-click actions.',
|
|
219
|
+
parameters: {
|
|
220
|
+
type: 'object',
|
|
221
|
+
properties: {
|
|
222
|
+
x: { type: 'number', description: 'Absolute X coordinate' },
|
|
223
|
+
y: { type: 'number', description: 'Absolute Y coordinate' },
|
|
224
|
+
text: { type: 'string', description: 'Visible text to match in the UI dump' },
|
|
225
|
+
resourceId: { type: 'string', description: 'Android resource-id to match' },
|
|
226
|
+
description: { type: 'string', description: 'content-desc / accessibility label to match' },
|
|
227
|
+
className: { type: 'string', description: 'Optional class name filter' },
|
|
228
|
+
packageName: { type: 'string', description: 'Optional package filter' },
|
|
229
|
+
clickable: { type: 'boolean', description: 'Prefer clickable elements' },
|
|
230
|
+
durationMs: { type: 'number', description: 'Press duration in milliseconds (default 650)' }
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
},
|
|
216
234
|
{
|
|
217
235
|
name: 'android_type',
|
|
218
236
|
description: 'Type text into the focused Android field, optionally tapping a matched element first.',
|
|
@@ -313,6 +331,19 @@ function getAvailableTools(app, options = {}) {
|
|
|
313
331
|
required: ['apkPath']
|
|
314
332
|
}
|
|
315
333
|
},
|
|
334
|
+
{
|
|
335
|
+
name: 'android_shell',
|
|
336
|
+
description: 'Run an adb shell command on the active Android device or emulator. Use this when a needed phone action is not covered by a higher-level Android tool.',
|
|
337
|
+
parameters: {
|
|
338
|
+
type: 'object',
|
|
339
|
+
properties: {
|
|
340
|
+
command: { type: 'string', description: 'Shell command to run on-device, without the leading "adb shell"' },
|
|
341
|
+
timeoutMs: { type: 'number', description: 'Timeout in milliseconds (default 20000)' },
|
|
342
|
+
screenshot: { type: 'boolean', description: 'Capture a screenshot after the command if it changes the UI (default false)' }
|
|
343
|
+
},
|
|
344
|
+
required: ['command']
|
|
345
|
+
}
|
|
346
|
+
},
|
|
316
347
|
{
|
|
317
348
|
name: 'web_search',
|
|
318
349
|
description: 'Search the public web without opening the browser. Uses Brave Search API for fast result retrieval.',
|
|
@@ -879,6 +910,12 @@ async function executeTool(toolName, args, context, engine) {
|
|
|
879
910
|
return await controller.tap(args || {});
|
|
880
911
|
}
|
|
881
912
|
|
|
913
|
+
case 'android_long_press': {
|
|
914
|
+
const controller = ac();
|
|
915
|
+
if (!controller) return { error: 'Android controller not available' };
|
|
916
|
+
return await controller.longPress(args || {});
|
|
917
|
+
}
|
|
918
|
+
|
|
882
919
|
case 'android_type': {
|
|
883
920
|
const controller = ac();
|
|
884
921
|
if (!controller) return { error: 'Android controller not available' };
|
|
@@ -927,6 +964,12 @@ async function executeTool(toolName, args, context, engine) {
|
|
|
927
964
|
return await controller.installApk(args || {});
|
|
928
965
|
}
|
|
929
966
|
|
|
967
|
+
case 'android_shell': {
|
|
968
|
+
const controller = ac();
|
|
969
|
+
if (!controller) return { error: 'Android controller not available' };
|
|
970
|
+
return await controller.shell(args || {});
|
|
971
|
+
}
|
|
972
|
+
|
|
930
973
|
case 'web_search': {
|
|
931
974
|
const apiKey = process.env.BRAVE_SEARCH_API_KEY;
|
|
932
975
|
if (!apiKey) return { error: 'BRAVE_SEARCH_API_KEY is not configured' };
|
|
@@ -64,6 +64,25 @@ function commandExists(command) {
|
|
|
64
64
|
return probe.status === 0;
|
|
65
65
|
}
|
|
66
66
|
|
|
67
|
+
function parseResolvedLaunchComponent(output, packageName) {
|
|
68
|
+
const lines = String(output || '')
|
|
69
|
+
.split('\n')
|
|
70
|
+
.map((line) => line.trim())
|
|
71
|
+
.filter(Boolean);
|
|
72
|
+
const normalizedPackage = String(packageName || '').trim();
|
|
73
|
+
const componentPattern = /^[A-Za-z0-9._$]+\/[A-Za-z0-9._$]+$/;
|
|
74
|
+
const relativePattern = /^[A-Za-z0-9._$]+\/\.[A-Za-z0-9._$]+$/;
|
|
75
|
+
|
|
76
|
+
const exact = lines.find((line) =>
|
|
77
|
+
normalizedPackage
|
|
78
|
+
? line.startsWith(`${normalizedPackage}/`)
|
|
79
|
+
: componentPattern.test(line) || relativePattern.test(line)
|
|
80
|
+
);
|
|
81
|
+
if (exact) return exact;
|
|
82
|
+
|
|
83
|
+
return lines.find((line) => componentPattern.test(line) || relativePattern.test(line)) || null;
|
|
84
|
+
}
|
|
85
|
+
|
|
67
86
|
function appendState(patch) {
|
|
68
87
|
const current = readState();
|
|
69
88
|
const next = {
|
|
@@ -470,6 +489,39 @@ function systemImagePackageToRelativeDir(packageName) {
|
|
|
470
489
|
return `${parts.join('/')}/`;
|
|
471
490
|
}
|
|
472
491
|
|
|
492
|
+
function systemImagePackageToAbi(packageName) {
|
|
493
|
+
const parts = String(packageName || '').split(';').filter(Boolean);
|
|
494
|
+
if (parts.length !== 4 || parts[0] !== 'system-images') {
|
|
495
|
+
return null;
|
|
496
|
+
}
|
|
497
|
+
return parts[3] || null;
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
function abiToCpuArch(abi) {
|
|
501
|
+
const value = String(abi || '').trim().toLowerCase();
|
|
502
|
+
if (value === 'arm64-v8a') return 'arm64';
|
|
503
|
+
if (value === 'armeabi-v7a' || value === 'armeabi') return 'arm';
|
|
504
|
+
if (value === 'x86_64') return 'x86_64';
|
|
505
|
+
if (value === 'x86') return 'x86';
|
|
506
|
+
return null;
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
function systemImagePackageToCpuArch(packageName) {
|
|
510
|
+
return abiToCpuArch(systemImagePackageToAbi(packageName));
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
function describeAutoFixChanges(current, next, fields = []) {
|
|
514
|
+
return fields
|
|
515
|
+
.map((field) => {
|
|
516
|
+
const before = current?.[field] ?? null;
|
|
517
|
+
const after = next?.[field] ?? null;
|
|
518
|
+
if (before === after) return null;
|
|
519
|
+
return `${field}: ${before ?? 'null'} -> ${after ?? 'null'}`;
|
|
520
|
+
})
|
|
521
|
+
.filter(Boolean)
|
|
522
|
+
.join(', ');
|
|
523
|
+
}
|
|
524
|
+
|
|
473
525
|
function sanitizeUiXml(raw) {
|
|
474
526
|
const text = String(raw || '');
|
|
475
527
|
const start = text.indexOf('<?xml');
|
|
@@ -581,7 +633,28 @@ class AndroidController {
|
|
|
581
633
|
if (!preferredInstalled) {
|
|
582
634
|
throw new Error(formatSystemImageError(installedImages));
|
|
583
635
|
}
|
|
584
|
-
|
|
636
|
+
const stateApiLevel = Number(state.apiLevel || 0) || 0;
|
|
637
|
+
const stateNeedsRefresh =
|
|
638
|
+
preferredInstalled.packageName !== state.systemImage ||
|
|
639
|
+
preferredInstalled.apiLevel !== stateApiLevel ||
|
|
640
|
+
preferredInstalled.arch !== state.systemImageArch;
|
|
641
|
+
if (stateNeedsRefresh) {
|
|
642
|
+
const changeSummary = describeAutoFixChanges(
|
|
643
|
+
{
|
|
644
|
+
systemImage: state.systemImage || null,
|
|
645
|
+
apiLevel: stateApiLevel || null,
|
|
646
|
+
systemImageArch: state.systemImageArch || null,
|
|
647
|
+
},
|
|
648
|
+
{
|
|
649
|
+
systemImage: preferredInstalled.packageName,
|
|
650
|
+
apiLevel: preferredInstalled.apiLevel,
|
|
651
|
+
systemImageArch: preferredInstalled.arch,
|
|
652
|
+
},
|
|
653
|
+
['systemImage', 'apiLevel', 'systemImageArch']
|
|
654
|
+
);
|
|
655
|
+
if (changeSummary) {
|
|
656
|
+
console.log(`[Android] Auto-fixed preferred system image (${changeSummary})`);
|
|
657
|
+
}
|
|
585
658
|
appendState({
|
|
586
659
|
bootstrapped: true,
|
|
587
660
|
systemImage: preferredInstalled.packageName,
|
|
@@ -670,19 +743,39 @@ class AndroidController {
|
|
|
670
743
|
if (!pkg) throw new Error('Android system image not installed');
|
|
671
744
|
const avdExists = list.includes(`Name: ${this.avdName}`);
|
|
672
745
|
let avdNeedsRecreate = avdExists && (!state.avdSystemImage || state.avdSystemImage !== pkg);
|
|
746
|
+
const avdRecreateReasons = [];
|
|
747
|
+
if (avdNeedsRecreate && state.avdSystemImage !== pkg) {
|
|
748
|
+
avdRecreateReasons.push(`systemImage: ${state.avdSystemImage || 'null'} -> ${pkg}`);
|
|
749
|
+
}
|
|
673
750
|
const configPath = path.join(AVD_HOME, `${this.avdName}.avd`, 'config.ini');
|
|
674
751
|
if (avdExists && fs.existsSync(configPath)) {
|
|
675
752
|
try {
|
|
676
753
|
const config = fs.readFileSync(configPath, 'utf8');
|
|
677
754
|
const currentImageDir = readIniValue(config, 'image.sysdir.1');
|
|
678
755
|
const expectedImageDir = systemImagePackageToRelativeDir(pkg);
|
|
756
|
+
const currentAbi = readIniValue(config, 'abi.type');
|
|
757
|
+
const expectedAbi = systemImagePackageToAbi(pkg);
|
|
758
|
+
const currentCpuArch = readIniValue(config, 'hw.cpu.arch');
|
|
759
|
+
const expectedCpuArch = systemImagePackageToCpuArch(pkg);
|
|
679
760
|
if (expectedImageDir && currentImageDir && currentImageDir !== expectedImageDir) {
|
|
680
761
|
avdNeedsRecreate = true;
|
|
762
|
+
avdRecreateReasons.push(`image.sysdir.1: ${currentImageDir} -> ${expectedImageDir}`);
|
|
763
|
+
}
|
|
764
|
+
if (expectedAbi && currentAbi && currentAbi !== expectedAbi) {
|
|
765
|
+
avdNeedsRecreate = true;
|
|
766
|
+
avdRecreateReasons.push(`abi.type: ${currentAbi} -> ${expectedAbi}`);
|
|
767
|
+
}
|
|
768
|
+
if (expectedCpuArch && currentCpuArch && currentCpuArch !== expectedCpuArch) {
|
|
769
|
+
avdNeedsRecreate = true;
|
|
770
|
+
avdRecreateReasons.push(`hw.cpu.arch: ${currentCpuArch} -> ${expectedCpuArch}`);
|
|
681
771
|
}
|
|
682
772
|
} catch {}
|
|
683
773
|
}
|
|
684
774
|
|
|
685
775
|
if (avdNeedsRecreate) {
|
|
776
|
+
if (avdRecreateReasons.length > 0) {
|
|
777
|
+
console.log(`[Android] Recreating AVD to repair config mismatch (${avdRecreateReasons.join(', ')})`);
|
|
778
|
+
}
|
|
686
779
|
await this.stopEmulator().catch(() => {});
|
|
687
780
|
await this.#run(`${quoteShell(avdManagerBinary())} delete avd -n ${quoteShell(this.avdName)}`, {
|
|
688
781
|
timeout: 120000,
|
|
@@ -1095,6 +1188,41 @@ class AndroidController {
|
|
|
1095
1188
|
};
|
|
1096
1189
|
}
|
|
1097
1190
|
|
|
1191
|
+
async longPress(args = {}) {
|
|
1192
|
+
let x = Number(args.x);
|
|
1193
|
+
let y = Number(args.y);
|
|
1194
|
+
let node = null;
|
|
1195
|
+
let serial = await this.ensureDevice();
|
|
1196
|
+
let uiDumpPath = null;
|
|
1197
|
+
|
|
1198
|
+
if (!Number.isFinite(x) || !Number.isFinite(y)) {
|
|
1199
|
+
const resolved = await this.#resolveSelector(args);
|
|
1200
|
+
serial = resolved.serial;
|
|
1201
|
+
node = resolved.node;
|
|
1202
|
+
uiDumpPath = resolved.uiDumpPath;
|
|
1203
|
+
x = node.bounds.centerX;
|
|
1204
|
+
y = node.bounds.centerY;
|
|
1205
|
+
}
|
|
1206
|
+
|
|
1207
|
+
const durationMs = Math.max(250, Number(args.durationMs) || 650);
|
|
1208
|
+
await this.#adb(
|
|
1209
|
+
serial,
|
|
1210
|
+
`shell input swipe ${Math.round(x)} ${Math.round(y)} ${Math.round(x)} ${Math.round(y)} ${Math.round(durationMs)}`,
|
|
1211
|
+
{ timeout: Math.max(15000, durationMs + 5000) },
|
|
1212
|
+
);
|
|
1213
|
+
const shot = await this.screenshot();
|
|
1214
|
+
return {
|
|
1215
|
+
success: true,
|
|
1216
|
+
serial,
|
|
1217
|
+
x: Math.round(x),
|
|
1218
|
+
y: Math.round(y),
|
|
1219
|
+
durationMs,
|
|
1220
|
+
target: summarizeNode(node),
|
|
1221
|
+
uiDumpPath,
|
|
1222
|
+
screenshotPath: shot.screenshotPath,
|
|
1223
|
+
};
|
|
1224
|
+
}
|
|
1225
|
+
|
|
1098
1226
|
async type(args = {}) {
|
|
1099
1227
|
const serial = await this.ensureDevice();
|
|
1100
1228
|
if (args.clear === true) {
|
|
@@ -1196,7 +1324,20 @@ class AndroidController {
|
|
|
1196
1324
|
if (args.activity) {
|
|
1197
1325
|
await this.#adb(serial, `shell am start -n ${quoteShell(`${args.packageName}/${args.activity}`)}`, { timeout: 20000 });
|
|
1198
1326
|
} else if (args.packageName) {
|
|
1199
|
-
await this.#
|
|
1327
|
+
const resolved = await this.#runAllowFailure(
|
|
1328
|
+
`${quoteShell(adbBinary())} -s ${quoteShell(serial)} shell cmd package resolve-activity --brief -c android.intent.category.LAUNCHER ${quoteShell(args.packageName)}`,
|
|
1329
|
+
{ timeout: 15000 },
|
|
1330
|
+
);
|
|
1331
|
+
const component = parseResolvedLaunchComponent(
|
|
1332
|
+
`${resolved.stdout || ''}\n${resolved.stderr || ''}`,
|
|
1333
|
+
args.packageName,
|
|
1334
|
+
);
|
|
1335
|
+
|
|
1336
|
+
if (component) {
|
|
1337
|
+
await this.#adb(serial, `shell am start -n ${quoteShell(component)}`, { timeout: 20000 });
|
|
1338
|
+
} else {
|
|
1339
|
+
await this.#adb(serial, `shell monkey -p ${quoteShell(args.packageName)} -c android.intent.category.LAUNCHER 1`, { timeout: 30000 });
|
|
1340
|
+
}
|
|
1200
1341
|
} else {
|
|
1201
1342
|
throw new Error('packageName is required for android_open_app');
|
|
1202
1343
|
}
|
|
@@ -1264,6 +1405,23 @@ class AndroidController {
|
|
|
1264
1405
|
};
|
|
1265
1406
|
}
|
|
1266
1407
|
|
|
1408
|
+
async shell(args = {}) {
|
|
1409
|
+
const serial = await this.ensureDevice();
|
|
1410
|
+
const command = String(args.command || '').trim();
|
|
1411
|
+
if (!command) throw new Error('command is required for android_shell');
|
|
1412
|
+
|
|
1413
|
+
const timeout = Math.max(1000, Number(args.timeoutMs) || 20000);
|
|
1414
|
+
const stdout = await this.#adb(serial, `shell ${quoteShell(command)}`, { timeout });
|
|
1415
|
+
const shot = args.screenshot === true ? await this.screenshot() : null;
|
|
1416
|
+
return {
|
|
1417
|
+
success: true,
|
|
1418
|
+
serial,
|
|
1419
|
+
command,
|
|
1420
|
+
stdout,
|
|
1421
|
+
screenshotPath: shot?.screenshotPath || null,
|
|
1422
|
+
};
|
|
1423
|
+
}
|
|
1424
|
+
|
|
1267
1425
|
async getStatus() {
|
|
1268
1426
|
const devices = isExecutable(adbBinary())
|
|
1269
1427
|
? await this.listDevices({ ensureBootstrapped: false }).catch(() => [])
|
|
@@ -1332,6 +1490,7 @@ module.exports = {
|
|
|
1332
1490
|
configuredSystemImagePackage,
|
|
1333
1491
|
configuredSystemImagePlatform,
|
|
1334
1492
|
formatSystemImageError,
|
|
1493
|
+
parseResolvedLaunchComponent,
|
|
1335
1494
|
parseLatestCmdlineToolsUrl,
|
|
1336
1495
|
parseSystemImages,
|
|
1337
1496
|
sanitizeUiXml,
|