neoagent 3.0.1-beta.8 → 3.1.0
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/.env.example +2 -10
- package/README.md +12 -3
- package/docs/automation.md +37 -0
- package/docs/benchmarking.md +102 -0
- package/docs/billing.md +34 -8
- package/docs/configuration.md +11 -2
- package/docs/getting-started.md +10 -8
- package/docs/operations.md +1 -1
- package/flutter_app/lib/main.dart +3 -0
- package/flutter_app/lib/main_account_settings.dart +138 -0
- package/flutter_app/lib/main_app_shell.dart +38 -13
- package/flutter_app/lib/main_billing.dart +1465 -0
- package/flutter_app/lib/main_chat.dart +1612 -214
- package/flutter_app/lib/main_controller.dart +398 -26
- package/flutter_app/lib/main_devices.dart +293 -207
- package/flutter_app/lib/main_models.dart +142 -0
- package/flutter_app/lib/main_navigation.dart +19 -1
- package/flutter_app/lib/main_operations.dart +288 -9
- package/flutter_app/lib/main_settings.dart +510 -300
- package/flutter_app/lib/main_shared.dart +2 -0
- package/flutter_app/lib/main_timeline.dart +1378 -0
- package/flutter_app/lib/src/backend_client.dart +149 -19
- package/flutter_app/lib/src/desktop_companion_actions.dart +132 -21
- package/flutter_app/lib/src/desktop_companion_io.dart +65 -1
- package/flutter_app/lib/src/desktop_companion_stub.dart +12 -0
- package/flutter_app/lib/src/desktop_ocr_bridge.dart +2 -0
- package/flutter_app/lib/src/desktop_ocr_bridge_io.dart +125 -0
- package/flutter_app/lib/src/desktop_ocr_bridge_stub.dart +30 -0
- package/flutter_app/lib/src/desktop_passive_history.dart +332 -0
- package/flutter_app/lib/src/recording_bridge_io.dart +80 -72
- package/flutter_app/lib/src/recording_bridge_web.dart +127 -114
- package/flutter_app/lib/src/recording_chunk_queue.dart +149 -0
- package/flutter_app/lib/src/recording_chunk_queue_io.dart +182 -0
- package/flutter_app/lib/src/recording_payloads.dart +9 -0
- package/flutter_app/macos/Runner/AppDelegate.swift +25 -0
- package/flutter_app/windows/runner/flutter_window.cpp +75 -0
- package/landing/index.html +3 -3
- package/lib/manager.js +184 -66
- package/package.json +4 -1
- package/server/admin/access.js +12 -7
- package/server/admin/admin.css +78 -0
- package/server/admin/admin.js +436 -10
- package/server/admin/billing.js +165 -42
- package/server/admin/index.html +80 -5
- package/server/admin/users.js +15 -15
- package/server/db/database.js +125 -20
- package/server/http/routes.js +1 -0
- package/server/http/static.js +4 -2
- package/server/public/.last_build_id +1 -1
- package/server/public/assets/fonts/MaterialIcons-Regular.otf +0 -0
- package/server/public/canvaskit/wimp.js.symbols +8475 -8467
- package/server/public/canvaskit/wimp.wasm +0 -0
- package/server/public/flutter_bootstrap.js +2 -2
- package/server/public/main.dart.js +91077 -87037
- package/server/routes/account.js +53 -0
- package/server/routes/admin.js +345 -64
- package/server/routes/agents.js +203 -21
- package/server/routes/billing.js +5 -0
- package/server/routes/browser.js +8 -1
- package/server/routes/recordings.js +96 -6
- package/server/routes/screenHistory.js +140 -2
- package/server/routes/timeline.js +43 -0
- package/server/services/account/erasure.js +263 -0
- package/server/services/ai/hooks.js +4 -1
- package/server/services/ai/loop/agent_engine_core.js +8 -1
- package/server/services/ai/loop/blank_recovery.js +36 -0
- package/server/services/ai/loop/conversation_loop.js +166 -33
- package/server/services/ai/messagingFallback.js +22 -0
- package/server/services/ai/rate_limits.js +28 -5
- package/server/services/ai/systemPrompt.js +6 -5
- package/server/services/ai/taskAnalysis.js +2 -0
- package/server/services/ai/toolEvidence.js +15 -0
- package/server/services/ai/toolResult.js +40 -0
- package/server/services/ai/tools.js +163 -4
- package/server/services/android/controller.js +6 -2
- package/server/services/billing/plans.js +2 -1
- package/server/services/browser/anti_detection.js +192 -0
- package/server/services/browser/controller.js +180 -54
- package/server/services/desktop/auth.js +3 -0
- package/server/services/desktop/registry.js +50 -2
- package/server/services/integrations/google/calendar.js +22 -14
- package/server/services/manager.js +12 -42
- package/server/services/memory/ingestion_chunking.js +268 -0
- package/server/services/messaging/telnyx.js +9 -8
- package/server/services/recordings/manager.js +60 -27
- package/server/services/runtime/backends/local-vm.js +40 -22
- package/server/services/runtime/docker-vm-manager.js +157 -266
- package/server/services/runtime/guest_bootstrap.js +17 -5
- package/server/services/runtime/guest_image.js +188 -0
- package/server/services/runtime/manager.js +0 -1
- package/server/services/runtime/validation.js +3 -8
- package/server/services/social_video/service.js +60 -10
- package/server/services/tasks/runtime.js +234 -9
- package/server/services/tasks/schedule_utils.js +5 -5
- package/server/services/tasks/task_repository.js +13 -0
- package/server/services/timeline/service.js +558 -0
- package/server/services/wearable/gateway.js +1 -1
- package/server/services/websocket.js +21 -3
- package/server/services/desktop/screenRecorder.js +0 -292
- package/server/services/desktop/screen_recorder_support.js +0 -46
|
@@ -240,7 +240,7 @@ function validateProactiveSendMessageArgs({ purpose, normalizedMessage }) {
|
|
|
240
240
|
}
|
|
241
241
|
|
|
242
242
|
if (normalizedPurpose === 'no_response') {
|
|
243
|
-
if (normalizedMessage !== '[NO RESPONSE]') {
|
|
243
|
+
if (normalizedMessage && normalizedMessage !== '[NO RESPONSE]') {
|
|
244
244
|
return {
|
|
245
245
|
ok: false,
|
|
246
246
|
error: 'purpose=no_response requires content "[NO RESPONSE]".',
|
|
@@ -276,7 +276,33 @@ function getRunState(engine, runId) {
|
|
|
276
276
|
|
|
277
277
|
function hasAlreadySentProactiveMessage({ triggerSource, runState, deliveryState, allowMultipleProactiveMessages }) {
|
|
278
278
|
if (!isProactiveTrigger(triggerSource) || allowMultipleProactiveMessages) return false;
|
|
279
|
-
return Boolean(
|
|
279
|
+
return Boolean(
|
|
280
|
+
runState?.messagingSent
|
|
281
|
+
|| deliveryState?.messagingSent
|
|
282
|
+
|| runState?.proactiveMessageStaged
|
|
283
|
+
|| deliveryState?.proactiveMessageStaged
|
|
284
|
+
);
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
function markProactiveMessageStaged({ runState, deliveryState, platform, to, content, purpose, mediaPath = null }) {
|
|
288
|
+
const staged = {
|
|
289
|
+
platform: String(platform || '').trim(),
|
|
290
|
+
to: String(to || '').trim(),
|
|
291
|
+
content: String(content || '').trim(),
|
|
292
|
+
purpose: String(purpose || '').trim().toLowerCase(),
|
|
293
|
+
mediaPath: mediaPath || null,
|
|
294
|
+
stagedAt: new Date().toISOString(),
|
|
295
|
+
};
|
|
296
|
+
|
|
297
|
+
if (runState) {
|
|
298
|
+
runState.proactiveMessageStaged = true;
|
|
299
|
+
runState.stagedProactiveMessage = staged;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
if (deliveryState) {
|
|
303
|
+
deliveryState.proactiveMessageStaged = true;
|
|
304
|
+
deliveryState.stagedProactiveMessage = staged;
|
|
305
|
+
}
|
|
280
306
|
}
|
|
281
307
|
|
|
282
308
|
function markProactiveMessageSent({ runState, deliveryState, content }) {
|
|
@@ -411,7 +437,9 @@ function getAvailableTools(app, options = {}) {
|
|
|
411
437
|
url: { type: 'string', description: 'URL to navigate to' },
|
|
412
438
|
screenshot: { type: 'boolean', description: 'Take a screenshot (default true)' },
|
|
413
439
|
waitFor: { type: 'string', description: 'CSS selector to wait for' },
|
|
414
|
-
fullPage: { type: 'boolean', description: 'Full page screenshot (default false)' }
|
|
440
|
+
fullPage: { type: 'boolean', description: 'Full page screenshot (default false)' },
|
|
441
|
+
referrerMode: { type: 'string', enum: ['direct', 'google', 'current'], description: 'Navigation referrer strategy for the VM browser (default direct). google sends a Google referrer; current navigates from the current page when possible.' },
|
|
442
|
+
challengeRetry: { type: 'boolean', description: 'Retry once with a Google referrer if a known bot challenge is detected (default true).' }
|
|
415
443
|
},
|
|
416
444
|
required: ['url']
|
|
417
445
|
}
|
|
@@ -1476,6 +1504,30 @@ function getAvailableTools(app, options = {}) {
|
|
|
1476
1504
|
required: ['query']
|
|
1477
1505
|
}
|
|
1478
1506
|
},
|
|
1507
|
+
{
|
|
1508
|
+
name: 'list_chats',
|
|
1509
|
+
description: 'List all known groups and conversations across connected messaging platforms. Use this when the user doesn\'t know the exact group name or chat ID — it returns every chat that has ever sent a message, with platform, chat ID, display name, and whether it\'s a group or DM.',
|
|
1510
|
+
parameters: {
|
|
1511
|
+
type: 'object',
|
|
1512
|
+
properties: {
|
|
1513
|
+
platform: { type: 'string', description: 'Filter to a specific platform (e.g. telegram, whatsapp, discord). Omit to list chats across all platforms.' },
|
|
1514
|
+
}
|
|
1515
|
+
}
|
|
1516
|
+
},
|
|
1517
|
+
{
|
|
1518
|
+
name: 'read_messages',
|
|
1519
|
+
description: 'Read recent messages from a connected messaging platform group or conversation. Use this to get recaps, search history, or summarize what\'s been going on in a Telegram group, WhatsApp chat, Discord channel, Slack channel, or any other connected platform. Messages are stored locally from inbound traffic.',
|
|
1520
|
+
parameters: {
|
|
1521
|
+
type: 'object',
|
|
1522
|
+
properties: {
|
|
1523
|
+
platform: { type: 'string', description: 'Platform name (e.g. telegram, whatsapp, discord, slack, signal, teams). Omit to search across all connected platforms.' },
|
|
1524
|
+
chat_id: { type: 'string', description: 'Exact platform chat or group ID. Use this when known.' },
|
|
1525
|
+
group_name: { type: 'string', description: 'Group, channel, or chat name to find (case-insensitive partial match). Use when you don\'t know the exact chat_id.' },
|
|
1526
|
+
limit: { type: 'number', description: 'Max messages to return (default 20, max 100).' },
|
|
1527
|
+
search: { type: 'string', description: 'Optional keyword to filter messages by content.' },
|
|
1528
|
+
}
|
|
1529
|
+
}
|
|
1530
|
+
},
|
|
1479
1531
|
{
|
|
1480
1532
|
name: 'social_video_extract',
|
|
1481
1533
|
description: 'Extract title, description, transcript, and one representative frame image from a public social video URL (YouTube, TikTok, Instagram, or X) without social API keys.',
|
|
@@ -1737,7 +1789,9 @@ async function executeTool(toolName, args, context, engine) {
|
|
|
1737
1789
|
return { ...await provider.navigate(args.url, {
|
|
1738
1790
|
screenshot: args.screenshot !== false,
|
|
1739
1791
|
waitFor: args.waitFor,
|
|
1740
|
-
fullPage: args.fullPage
|
|
1792
|
+
fullPage: args.fullPage,
|
|
1793
|
+
referrerMode: args.referrerMode,
|
|
1794
|
+
challengeRetry: args.challengeRetry
|
|
1741
1795
|
}), backend };
|
|
1742
1796
|
}
|
|
1743
1797
|
|
|
@@ -2245,6 +2299,91 @@ async function executeTool(toolName, args, context, engine) {
|
|
|
2245
2299
|
};
|
|
2246
2300
|
}
|
|
2247
2301
|
|
|
2302
|
+
case 'list_chats': {
|
|
2303
|
+
const listPlatform = typeof args.platform === 'string' ? args.platform.trim().toLowerCase() : null;
|
|
2304
|
+
|
|
2305
|
+
let listQuery = `SELECT platform, platform_chat_id, metadata
|
|
2306
|
+
FROM messages
|
|
2307
|
+
WHERE user_id = ? AND platform_chat_id IS NOT NULL AND platform != 'web'`;
|
|
2308
|
+
const listParams = [userId];
|
|
2309
|
+
|
|
2310
|
+
if (listPlatform) { listQuery += ' AND platform = ?'; listParams.push(listPlatform); }
|
|
2311
|
+
listQuery += ' ORDER BY id DESC LIMIT 2000';
|
|
2312
|
+
|
|
2313
|
+
const listRows = db.prepare(listQuery).all(...listParams);
|
|
2314
|
+
|
|
2315
|
+
const chatMap = new Map();
|
|
2316
|
+
for (const row of listRows) {
|
|
2317
|
+
const key = `${row.platform}:${row.platform_chat_id}`;
|
|
2318
|
+
if (chatMap.has(key)) continue;
|
|
2319
|
+
let meta = {};
|
|
2320
|
+
try { meta = row.metadata ? JSON.parse(row.metadata) : {}; } catch {}
|
|
2321
|
+
const isGroup = String(meta.isGroup || '').match(/^(true|1)$/i) != null && String(meta.isGroup || '') !== '';
|
|
2322
|
+
const name = (meta.groupName || meta.group_name || meta.guildName || meta.guild_name || meta.chatName || meta.chat_name || '').trim()
|
|
2323
|
+
|| (meta.senderName || meta.sender_name || '').trim()
|
|
2324
|
+
|| null;
|
|
2325
|
+
chatMap.set(key, {
|
|
2326
|
+
platform: row.platform,
|
|
2327
|
+
chat_id: row.platform_chat_id,
|
|
2328
|
+
name,
|
|
2329
|
+
type: isGroup ? 'group' : 'dm',
|
|
2330
|
+
});
|
|
2331
|
+
}
|
|
2332
|
+
|
|
2333
|
+
const chats = Array.from(chatMap.values());
|
|
2334
|
+
return {
|
|
2335
|
+
count: chats.length,
|
|
2336
|
+
chats,
|
|
2337
|
+
};
|
|
2338
|
+
}
|
|
2339
|
+
|
|
2340
|
+
case 'read_messages': {
|
|
2341
|
+
const msgLimit = Math.max(1, Math.min(Number(args.limit) || 20, 100));
|
|
2342
|
+
const msgPlatform = typeof args.platform === 'string' ? args.platform.trim().toLowerCase() : null;
|
|
2343
|
+
const msgChatId = typeof args.chat_id === 'string' ? args.chat_id.trim() : null;
|
|
2344
|
+
const msgGroupName = typeof args.group_name === 'string' ? args.group_name.trim().toLowerCase() : null;
|
|
2345
|
+
const msgSearch = typeof args.search === 'string' ? args.search.trim() : null;
|
|
2346
|
+
|
|
2347
|
+
let msgQuery = "SELECT role, content, platform, platform_chat_id, metadata, created_at FROM messages WHERE user_id = ? AND role = 'user'";
|
|
2348
|
+
const msgParams = [userId];
|
|
2349
|
+
|
|
2350
|
+
if (msgPlatform) { msgQuery += ' AND platform = ?'; msgParams.push(msgPlatform); }
|
|
2351
|
+
if (msgChatId) { msgQuery += ' AND platform_chat_id = ?'; msgParams.push(msgChatId); }
|
|
2352
|
+
if (msgSearch) { msgQuery += ' AND content LIKE ?'; msgParams.push(`%${msgSearch}%`); }
|
|
2353
|
+
|
|
2354
|
+
msgQuery += ' ORDER BY created_at DESC LIMIT ?';
|
|
2355
|
+
msgParams.push(msgGroupName ? Math.min(msgLimit * 10, 500) : msgLimit);
|
|
2356
|
+
|
|
2357
|
+
const msgRows = db.prepare(msgQuery).all(...msgParams);
|
|
2358
|
+
|
|
2359
|
+
let filteredRows = msgRows;
|
|
2360
|
+
if (msgGroupName) {
|
|
2361
|
+
filteredRows = msgRows.filter((row) => {
|
|
2362
|
+
try {
|
|
2363
|
+
const meta = row.metadata ? JSON.parse(row.metadata) : {};
|
|
2364
|
+
const name = (meta.groupName || meta.group_name || meta.guildName || meta.guild_name || meta.chatName || meta.chat_name || row.platform_chat_id || '').toLowerCase();
|
|
2365
|
+
return name.includes(msgGroupName);
|
|
2366
|
+
} catch { return false; }
|
|
2367
|
+
}).slice(0, msgLimit);
|
|
2368
|
+
}
|
|
2369
|
+
|
|
2370
|
+
return {
|
|
2371
|
+
count: filteredRows.length,
|
|
2372
|
+
messages: filteredRows.reverse().map((row) => {
|
|
2373
|
+
let meta = {};
|
|
2374
|
+
try { meta = row.metadata ? JSON.parse(row.metadata) : {}; } catch {}
|
|
2375
|
+
return {
|
|
2376
|
+
platform: row.platform,
|
|
2377
|
+
chat_id: row.platform_chat_id,
|
|
2378
|
+
group_name: meta.groupName || meta.group_name || meta.guildName || meta.guild_name || null,
|
|
2379
|
+
sender: meta.senderName || meta.sender_name || null,
|
|
2380
|
+
content: row.content,
|
|
2381
|
+
timestamp: row.created_at,
|
|
2382
|
+
};
|
|
2383
|
+
}),
|
|
2384
|
+
};
|
|
2385
|
+
}
|
|
2386
|
+
|
|
2248
2387
|
case 'social_video_extract': {
|
|
2249
2388
|
const service = socialVideo();
|
|
2250
2389
|
if (!service || typeof service.extractFromUrl !== 'function') {
|
|
@@ -2376,6 +2515,26 @@ async function executeTool(toolName, args, context, engine) {
|
|
|
2376
2515
|
};
|
|
2377
2516
|
}
|
|
2378
2517
|
|
|
2518
|
+
if (isProactiveTrigger(triggerSource) && context.stageProactiveMessages === true && !suppressReply) {
|
|
2519
|
+
markProactiveMessageStaged({
|
|
2520
|
+
runState,
|
|
2521
|
+
deliveryState,
|
|
2522
|
+
platform: args.platform,
|
|
2523
|
+
to: args.to,
|
|
2524
|
+
content: normalizedMessage,
|
|
2525
|
+
purpose: args.purpose,
|
|
2526
|
+
mediaPath: args.media_path,
|
|
2527
|
+
});
|
|
2528
|
+
return {
|
|
2529
|
+
success: true,
|
|
2530
|
+
staged: true,
|
|
2531
|
+
purpose: normalizeSendMessagePurpose(args.purpose),
|
|
2532
|
+
platform: args.platform,
|
|
2533
|
+
to: args.to,
|
|
2534
|
+
content: normalizedMessage,
|
|
2535
|
+
};
|
|
2536
|
+
}
|
|
2537
|
+
|
|
2379
2538
|
const sendResult = await manager.sendMessage(userId, args.platform, args.to, args.content, {
|
|
2380
2539
|
agentId,
|
|
2381
2540
|
mediaPath: args.media_path,
|
|
@@ -582,7 +582,8 @@ class AndroidController {
|
|
|
582
582
|
});
|
|
583
583
|
|
|
584
584
|
progress('Waiting for Android to boot (can take 2–5 min on first run)…');
|
|
585
|
-
|
|
585
|
+
// Abort early if the emulator process dies, instead of polling until timeout.
|
|
586
|
+
await this.#waitForBoot({ isAlive: () => this.#isPidAlive(proc.pid) });
|
|
586
587
|
|
|
587
588
|
writeState(this.userId, { bootstrapped: true, starting: false, startupPhase: null, lastStartError: null });
|
|
588
589
|
console.log(`[Android] Emulator ready on ${this.adbSerial}`);
|
|
@@ -593,10 +594,13 @@ class AndroidController {
|
|
|
593
594
|
});
|
|
594
595
|
}
|
|
595
596
|
|
|
596
|
-
async #waitForBoot(timeoutMs = 10 * 60 * 1000) {
|
|
597
|
+
async #waitForBoot({ timeoutMs = 10 * 60 * 1000, isAlive = () => true } = {}) {
|
|
597
598
|
const adb = adbBin(this.sdkDir);
|
|
598
599
|
const deadline = Date.now() + timeoutMs;
|
|
599
600
|
while (Date.now() < deadline) {
|
|
601
|
+
if (!isAlive()) {
|
|
602
|
+
throw new Error('Emulator process exited before Android finished booting (check virtualization/KVM support and the system image).');
|
|
603
|
+
}
|
|
600
604
|
try {
|
|
601
605
|
const r = spawnSync(adb, ['-s', this.adbSerial, 'shell', 'getprop', 'sys.boot_completed'], { encoding: 'utf8', timeout: 5000 });
|
|
602
606
|
if (r.stdout?.trim() === '1') return;
|
|
@@ -42,7 +42,8 @@ function validatePlanData(data, requireName = true) {
|
|
|
42
42
|
|
|
43
43
|
function createPlan(data) {
|
|
44
44
|
validatePlanData(data, true);
|
|
45
|
-
const id = data.id || `plan_${randomUUID().replace(/-/g, '')
|
|
45
|
+
const id = data.id || `plan_${randomUUID().replace(/-/g, '')}`;
|
|
46
|
+
// UTC timestamp without timezone marker — SQLite stores it as TEXT in ISO-like format.
|
|
46
47
|
const now = new Date().toISOString().replace('T', ' ').replace(/\.\d{3}Z$/, '');
|
|
47
48
|
db.prepare(`
|
|
48
49
|
INSERT INTO billing_plans
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const crypto = require('crypto');
|
|
4
|
+
|
|
5
|
+
const USER_AGENTS = Object.freeze([
|
|
6
|
+
{
|
|
7
|
+
ua: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/135.0.0.0 Safari/537.36',
|
|
8
|
+
platform: 'Win32',
|
|
9
|
+
webglVendor: 'Google Inc. (NVIDIA)',
|
|
10
|
+
webglRenderer: 'ANGLE (NVIDIA, NVIDIA GeForce RTX 3060 Direct3D11 vs_5_0 ps_5_0, D3D11)',
|
|
11
|
+
},
|
|
12
|
+
{
|
|
13
|
+
ua: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.0.0 Safari/537.36',
|
|
14
|
+
platform: 'Win32',
|
|
15
|
+
webglVendor: 'Google Inc. (Intel)',
|
|
16
|
+
webglRenderer: 'ANGLE (Intel, Intel(R) UHD Graphics 770 Direct3D11 vs_5_0 ps_5_0, D3D11)',
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
ua: 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/135.0.0.0 Safari/537.36',
|
|
20
|
+
platform: 'MacIntel',
|
|
21
|
+
webglVendor: 'Google Inc. (Apple)',
|
|
22
|
+
webglRenderer: 'ANGLE (Apple, Apple M2, OpenGL 4.1)',
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
ua: 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.0.0 Safari/537.36',
|
|
26
|
+
platform: 'MacIntel',
|
|
27
|
+
webglVendor: 'Google Inc. (Apple)',
|
|
28
|
+
webglRenderer: 'ANGLE (Apple, Apple M1, OpenGL 4.1)',
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
ua: 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/135.0.0.0 Safari/537.36',
|
|
32
|
+
platform: 'Linux x86_64',
|
|
33
|
+
webglVendor: 'Google Inc. (Intel)',
|
|
34
|
+
webglRenderer: 'ANGLE (Intel, Mesa Intel(R) UHD Graphics 620, OpenGL 4.6)',
|
|
35
|
+
},
|
|
36
|
+
]);
|
|
37
|
+
|
|
38
|
+
const VIEWPORTS = Object.freeze([
|
|
39
|
+
{ width: 1920, height: 1080, weight: 35 },
|
|
40
|
+
{ width: 1366, height: 768, weight: 26 },
|
|
41
|
+
{ width: 1536, height: 864, weight: 16 },
|
|
42
|
+
{ width: 1280, height: 720, weight: 9 },
|
|
43
|
+
{ width: 1440, height: 900, weight: 9 },
|
|
44
|
+
{ width: 1600, height: 900, weight: 5 },
|
|
45
|
+
]);
|
|
46
|
+
|
|
47
|
+
const REFERRER_MODES = new Set(['direct', 'google', 'current']);
|
|
48
|
+
|
|
49
|
+
function hashInt(value) {
|
|
50
|
+
const hash = crypto.createHash('sha256').update(String(value || 'default')).digest();
|
|
51
|
+
return hash.readUInt32BE(0);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function chooseWeighted(items, seed) {
|
|
55
|
+
const total = items.reduce((sum, item) => sum + Number(item.weight || 1), 0);
|
|
56
|
+
let cursor = Math.abs(Number(seed) || 0) % total;
|
|
57
|
+
for (const item of items) {
|
|
58
|
+
cursor -= Number(item.weight || 1);
|
|
59
|
+
if (cursor < 0) return item;
|
|
60
|
+
}
|
|
61
|
+
return items[0];
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function chooseBrowserIdentity(profileKey) {
|
|
65
|
+
const seed = hashInt(profileKey);
|
|
66
|
+
const profile = USER_AGENTS[seed % USER_AGENTS.length];
|
|
67
|
+
const viewport = chooseWeighted(VIEWPORTS, seed >>> 3);
|
|
68
|
+
return {
|
|
69
|
+
userAgent: profile.ua,
|
|
70
|
+
platform: profile.platform,
|
|
71
|
+
viewport: { width: viewport.width, height: viewport.height },
|
|
72
|
+
webglVendor: profile.webglVendor,
|
|
73
|
+
webglRenderer: profile.webglRenderer,
|
|
74
|
+
hardwareConcurrency: [4, 6, 8, 12, 16][seed % 5],
|
|
75
|
+
deviceMemory: [4, 8, 16][(seed >>> 5) % 3],
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function normalizeReferrerMode(value) {
|
|
80
|
+
const normalized = String(value || '').trim().toLowerCase();
|
|
81
|
+
return REFERRER_MODES.has(normalized) ? normalized : 'direct';
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function normalizeChallengeRetry(value) {
|
|
85
|
+
return value !== false;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function detectBotChallenge(input = {}) {
|
|
89
|
+
const title = String(input.title || '').trim();
|
|
90
|
+
const url = String(input.url || '').trim();
|
|
91
|
+
const html = String(input.html || '');
|
|
92
|
+
const text = String(input.text || input.pageContent || '');
|
|
93
|
+
const haystack = `${title}\n${url}\n${html.slice(0, 200000)}\n${text.slice(0, 50000)}`.toLowerCase();
|
|
94
|
+
|
|
95
|
+
if (title === 'Just a moment...' || haystack.includes('cf-turnstile-response') || haystack.includes('/cdn-cgi/challenge-platform/')) {
|
|
96
|
+
return { detected: true, provider: 'cloudflare' };
|
|
97
|
+
}
|
|
98
|
+
if (haystack.includes('please verify you are a human') || haystack.includes('press & hold to confirm')) {
|
|
99
|
+
return { detected: true, provider: 'perimeterx' };
|
|
100
|
+
}
|
|
101
|
+
if (haystack.includes('datadome') || haystack.includes('geo.captcha-delivery.com')) {
|
|
102
|
+
return { detected: true, provider: 'datadome' };
|
|
103
|
+
}
|
|
104
|
+
if (
|
|
105
|
+
haystack.includes('unusual traffic')
|
|
106
|
+
|| haystack.includes('are you a robot')
|
|
107
|
+
|| haystack.includes('verify that you are human')
|
|
108
|
+
|| haystack.includes('complete the security check')
|
|
109
|
+
) {
|
|
110
|
+
return { detected: true, provider: 'generic' };
|
|
111
|
+
}
|
|
112
|
+
return { detected: false, provider: null };
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function rand(min, max) {
|
|
116
|
+
return Math.floor(Math.random() * (max - min + 1)) + min;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function easeInOutQuad(t) {
|
|
120
|
+
return t < 0.5 ? 2 * t * t : 1 - Math.pow(-2 * t + 2, 2) / 2;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function easeOutQuad(t) {
|
|
124
|
+
return 1 - (1 - t) * (1 - t);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function easeInOutSine(t) {
|
|
128
|
+
return -(Math.cos(Math.PI * t) - 1) / 2;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function bezierPoint(points, t) {
|
|
132
|
+
let current = points.map((point) => ({ x: point.x, y: point.y }));
|
|
133
|
+
while (current.length > 1) {
|
|
134
|
+
current = current.slice(0, -1).map((point, index) => ({
|
|
135
|
+
x: point.x + (current[index + 1].x - point.x) * t,
|
|
136
|
+
y: point.y + (current[index + 1].y - point.y) * t,
|
|
137
|
+
}));
|
|
138
|
+
}
|
|
139
|
+
return current[0];
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function generateHumanMousePath(from, to, viewport = {}) {
|
|
143
|
+
const start = {
|
|
144
|
+
x: Number.isFinite(Number(from?.x)) ? Number(from.x) : 0,
|
|
145
|
+
y: Number.isFinite(Number(from?.y)) ? Number(from.y) : 0,
|
|
146
|
+
};
|
|
147
|
+
const end = {
|
|
148
|
+
x: Number.isFinite(Number(to?.x)) ? Number(to.x) : 0,
|
|
149
|
+
y: Number.isFinite(Number(to?.y)) ? Number(to.y) : 0,
|
|
150
|
+
};
|
|
151
|
+
const distance = Math.hypot(end.x - start.x, end.y - start.y);
|
|
152
|
+
if (distance < 2) return [start, end];
|
|
153
|
+
|
|
154
|
+
const width = Math.max(1, Number(viewport.width || 1440));
|
|
155
|
+
const height = Math.max(1, Number(viewport.height || 900));
|
|
156
|
+
const steps = Math.max(12, Math.min(90, Math.round(18 + distance / 18 + rand(-5, 8))));
|
|
157
|
+
const boundaryX = Math.min(90, Math.max(10, distance * 0.12));
|
|
158
|
+
const boundaryY = Math.min(90, Math.max(10, distance * 0.12));
|
|
159
|
+
const controlCount = rand(1, 3);
|
|
160
|
+
const controls = [];
|
|
161
|
+
for (let i = 1; i <= controlCount; i += 1) {
|
|
162
|
+
const ratio = i / (controlCount + 1);
|
|
163
|
+
controls.push({
|
|
164
|
+
x: Math.max(0, Math.min(width, start.x + (end.x - start.x) * ratio + rand(-boundaryX, boundaryX))),
|
|
165
|
+
y: Math.max(0, Math.min(height, start.y + (end.y - start.y) * ratio + rand(-boundaryY, boundaryY))),
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const tweens = [easeInOutQuad, easeOutQuad, easeInOutSine];
|
|
170
|
+
const tween = tweens[rand(0, tweens.length - 1)];
|
|
171
|
+
const curve = [start, ...controls, end];
|
|
172
|
+
const path = [];
|
|
173
|
+
for (let i = 0; i < steps; i += 1) {
|
|
174
|
+
const point = bezierPoint(curve, tween(i / (steps - 1)));
|
|
175
|
+
path.push({
|
|
176
|
+
x: Math.round(Math.max(0, Math.min(width, point.x))),
|
|
177
|
+
y: Math.round(Math.max(0, Math.min(height, point.y + (i > 0 && i < steps - 1 ? rand(-1, 1) : 0)))),
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
path[0] = { x: Math.round(start.x), y: Math.round(start.y) };
|
|
181
|
+
path[path.length - 1] = { x: Math.round(end.x), y: Math.round(end.y) };
|
|
182
|
+
return path;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
module.exports = {
|
|
186
|
+
chooseBrowserIdentity,
|
|
187
|
+
detectBotChallenge,
|
|
188
|
+
generateHumanMousePath,
|
|
189
|
+
normalizeChallengeRetry,
|
|
190
|
+
normalizeReferrerMode,
|
|
191
|
+
rand,
|
|
192
|
+
};
|