@aiscene/aiserver 1.9.6 → 1.9.8

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.
Files changed (49) hide show
  1. package/dist/core/device-proxy.d.ts +17 -0
  2. package/dist/core/device-proxy.d.ts.map +1 -1
  3. package/dist/core/device-proxy.js +95 -5
  4. package/dist/core/device-proxy.js.map +1 -1
  5. package/dist/debug/types.d.ts +56 -4
  6. package/dist/debug/types.d.ts.map +1 -1
  7. package/dist/debug/websocket-server.d.ts +8 -0
  8. package/dist/debug/websocket-server.d.ts.map +1 -1
  9. package/dist/debug/websocket-server.js +180 -17
  10. package/dist/debug/websocket-server.js.map +1 -1
  11. package/dist/executor/action-executor.d.ts +5 -5
  12. package/dist/executor/action-executor.d.ts.map +1 -1
  13. package/dist/executor/action-executor.js +32 -25
  14. package/dist/executor/action-executor.js.map +1 -1
  15. package/dist/executor/android-executor.d.ts +3 -14
  16. package/dist/executor/android-executor.d.ts.map +1 -1
  17. package/dist/executor/android-executor.js +21 -96
  18. package/dist/executor/android-executor.js.map +1 -1
  19. package/dist/index.d.ts.map +1 -1
  20. package/dist/index.js +22 -10
  21. package/dist/index.js.map +1 -1
  22. package/dist/scrcpy/server.d.ts +13 -0
  23. package/dist/scrcpy/server.d.ts.map +1 -1
  24. package/dist/scrcpy/server.js +333 -148
  25. package/dist/scrcpy/server.js.map +1 -1
  26. package/dist/task/guada-online-batch-poller.d.ts +21 -0
  27. package/dist/task/guada-online-batch-poller.d.ts.map +1 -0
  28. package/dist/task/guada-online-batch-poller.js +132 -0
  29. package/dist/task/guada-online-batch-poller.js.map +1 -0
  30. package/dist/task/online-loop-poller.d.ts +20 -0
  31. package/dist/task/online-loop-poller.d.ts.map +1 -0
  32. package/dist/task/online-loop-poller.js +152 -0
  33. package/dist/task/online-loop-poller.js.map +1 -0
  34. package/dist/task/scheduler.d.ts +1 -0
  35. package/dist/task/scheduler.d.ts.map +1 -1
  36. package/dist/task/scheduler.js +47 -0
  37. package/dist/task/scheduler.js.map +1 -1
  38. package/dist/task/test-case-reference-resolver.d.ts +16 -0
  39. package/dist/task/test-case-reference-resolver.d.ts.map +1 -0
  40. package/dist/task/test-case-reference-resolver.js +283 -0
  41. package/dist/task/test-case-reference-resolver.js.map +1 -0
  42. package/dist/web/debug-page.d.ts.map +1 -1
  43. package/dist/web/debug-page.js +49 -58
  44. package/dist/web/debug-page.js.map +1 -1
  45. package/dist/web/server.d.ts +5 -0
  46. package/dist/web/server.d.ts.map +1 -1
  47. package/dist/web/server.js +35 -0
  48. package/dist/web/server.js.map +1 -1
  49. package/package.json +1 -1
@@ -21,6 +21,7 @@ import { applyAiMemory } from '../core/ai-memory.js';
21
21
  import { whistleManager } from '../proxy/whistle-manager.js';
22
22
  import { executeAndroidLogin, executeWebLogin, } from '../login/android-login-context.js';
23
23
  import { webRecordingManager } from '../recorder/web-recording-manager.js';
24
+ import { resolveTestCaseReferences } from '../task/test-case-reference-resolver.js';
24
25
  const logger = createLogger('DebugWebSocket');
25
26
  /** 获取本机可用的非内部 IPv4 地址,供客户端连接投屏 WebSocket */
26
27
  function getLocalIP() {
@@ -43,6 +44,30 @@ export class DebugWebSocketServer {
43
44
  port;
44
45
  isStarted = false;
45
46
  activeBatchSessionIds = new Set();
47
+ createHeadlessBatchClient(batchId) {
48
+ return {
49
+ OPEN: 1,
50
+ readyState: 1,
51
+ bufferedAmount: 0,
52
+ send: (_payload, callback) => {
53
+ if (typeof callback === 'function')
54
+ callback();
55
+ },
56
+ close: () => undefined,
57
+ terminate: () => undefined,
58
+ };
59
+ }
60
+ runBatchActionInBackground(request) {
61
+ if (!this.isStarted) {
62
+ throw new Error('Debug WebSocket server is not started');
63
+ }
64
+ const batchId = request?.batchId || 'unknown';
65
+ const ws = this.createHeadlessBatchClient(batchId);
66
+ return this.handleBatchAction(ws, request).catch((error) => {
67
+ logger.error(`[BatchAction] Background batch failed: batchId=${batchId}, error=${error.message}`);
68
+ throw error;
69
+ });
70
+ }
46
71
  // 任务日志订阅: taskId -> Set of WebSocket clients
47
72
  taskLogSubscribers = new Map();
48
73
  // 记录客户端订阅的所有任务(用于断开时清理)
@@ -449,6 +474,24 @@ export class DebugWebSocketServer {
449
474
  // 包含 await/this.agent/this.sleep 等 JS 代码特征则视为代码
450
475
  return /\bawait\b/.test(text) || /\bthis\.(agent|sleep|page)\b/.test(text);
451
476
  }
477
+ async resolveScriptReferences(script) {
478
+ if (!script)
479
+ return script;
480
+ return resolveTestCaseReferences(script, {
481
+ apiUrl: this.config.task.apiUrl,
482
+ logger,
483
+ });
484
+ }
485
+ async resolveActionParamsReferences(actionParams) {
486
+ if (!actionParams || typeof actionParams.code !== 'string') {
487
+ return actionParams;
488
+ }
489
+ const resolvedCode = await this.resolveScriptReferences(actionParams.code);
490
+ return {
491
+ ...actionParams,
492
+ code: resolvedCode || actionParams.code,
493
+ };
494
+ }
452
495
  async handleStartDebug(ws, request) {
453
496
  // Web 分支:支持 sessionId 复用 + 主进程内嵌执行 + 投屏
454
497
  if (request.platform === 'web') {
@@ -460,6 +503,7 @@ export class DebugWebSocketServer {
460
503
  const session = sessionManager.create(sessionId, ws, request.deviceId);
461
504
  this.sendMessage(ws, { type: 'session_created', sessionId }, request.deviceId);
462
505
  try {
506
+ request.naturalLanguage = await this.resolveScriptReferences(request.naturalLanguage) || request.naturalLanguage;
463
507
  sessionManager.updateStatus(sessionId, 'running');
464
508
  this.sendMessage(ws, { type: 'debug_started', sessionId }, request.deviceId);
465
509
  await executeAndroidLogin({
@@ -536,6 +580,7 @@ export class DebugWebSocketServer {
536
580
  }
537
581
  this.sendMessage(ws, { type: 'session_created', sessionId });
538
582
  try {
583
+ request.naturalLanguage = await this.resolveScriptReferences(request.naturalLanguage) || request.naturalLanguage;
539
584
  sessionManager.updateStatus(sessionId, 'running');
540
585
  this.sendMessage(ws, { type: 'debug_started', sessionId, reused: isReuse });
541
586
  // 3) 首次启动:申请浏览器 + 创建 agent + 自动投屏
@@ -1098,6 +1143,7 @@ export class DebugWebSocketServer {
1098
1143
  try {
1099
1144
  sessionManager.updateStatus(sessionId, 'running');
1100
1145
  this.sendMessage(ws, { type: 'action_started', sessionId, actionType: request.actionType }, request.deviceId);
1146
+ const resolvedActionParams = await this.resolveActionParamsReferences(request.actionParams);
1101
1147
  await executeAndroidLogin({
1102
1148
  source: request,
1103
1149
  deviceId: request.deviceId,
@@ -1124,10 +1170,11 @@ export class DebugWebSocketServer {
1124
1170
  testName: createDebugReportGroupName(request),
1125
1171
  actionType: request.actionType,
1126
1172
  locatePrompt: request.locatePrompt,
1127
- actionParams: request.actionParams,
1173
+ actionParams: resolvedActionParams,
1128
1174
  udid: request.deviceId,
1129
1175
  packageName: request.packageName,
1130
- testUrl: request.testUrl,
1176
+ testUrl: this.pickWebTargetUrl(request),
1177
+ url: this.pickWebTargetUrl(request),
1131
1178
  modelConfig: fullModelConfig,
1132
1179
  aiMemory: request.aiMemory,
1133
1180
  memory: request.memory,
@@ -1291,7 +1338,7 @@ export class DebugWebSocketServer {
1291
1338
  const fullModelConfig = this.applyModelConfig(request.modelConfig);
1292
1339
  // 始终启动请求抓包
1293
1340
  try {
1294
- await whistleManager.startCapture(sessionId);
1341
+ await whistleManager.startCapture(sessionId, undefined, 3000, request.proxyAccount);
1295
1342
  logger.info('[AiAct] Whistle request capture started: sessionId=' + sessionId);
1296
1343
  }
1297
1344
  catch (captureErr) {
@@ -1371,6 +1418,7 @@ export class DebugWebSocketServer {
1371
1418
  try {
1372
1419
  sessionManager.updateStatus(sessionId, 'running');
1373
1420
  this.sendMessage(ws, { type: 'ai_act_started', sessionId }, request.deviceId);
1421
+ const resolvedNaturalLanguage = await this.resolveScriptReferences(request.naturalLanguage);
1374
1422
  await executeAndroidLogin({
1375
1423
  source: request,
1376
1424
  deviceId: request.deviceId,
@@ -1393,14 +1441,18 @@ export class DebugWebSocketServer {
1393
1441
  type: request.platform || 'android',
1394
1442
  testName: createDebugReportGroupName(request),
1395
1443
  actionType: 'aiAct',
1396
- naturalLanguage: request.naturalLanguage,
1444
+ naturalLanguage: resolvedNaturalLanguage,
1397
1445
  udid: request.deviceId,
1398
1446
  packageName: request.packageName,
1447
+ testUrl: this.pickWebTargetUrl(request),
1448
+ url: this.pickWebTargetUrl(request),
1399
1449
  modelConfig: fullModelConfig,
1400
1450
  aiMemory: request.aiMemory,
1401
1451
  memory: request.memory,
1402
1452
  memoryScope: request.memoryScope,
1403
1453
  skipAppRestart: request.skipAppRestart,
1454
+ proxyPort: request.proxyPort,
1455
+ proxyAccount: request.proxyAccount,
1404
1456
  actionParams: {
1405
1457
  deepLocate: request.deepLocate,
1406
1458
  deepThink: request.deepThink,
@@ -1547,9 +1599,92 @@ export class DebugWebSocketServer {
1547
1599
  return String(value).split('#')[0].replace(/\/$/, '');
1548
1600
  }
1549
1601
  }
1602
+ nonEmptyText(value) {
1603
+ if (typeof value !== 'string')
1604
+ return '';
1605
+ const trimmed = value.trim();
1606
+ if (!trimmed)
1607
+ return '';
1608
+ if (trimmed.length >= 2 &&
1609
+ ((trimmed[0] === '"' && trimmed[trimmed.length - 1] === '"') ||
1610
+ (trimmed[0] === "'" && trimmed[trimmed.length - 1] === "'"))) {
1611
+ return trimmed.slice(1, -1).trim();
1612
+ }
1613
+ return trimmed;
1614
+ }
1615
+ pickWebTargetUrlWithSource(source, label = 'source', seen = new WeakSet()) {
1616
+ if (!source || typeof source !== 'object')
1617
+ return { value: '', path: '' };
1618
+ if (seen.has(source))
1619
+ return { value: '', path: '' };
1620
+ seen.add(source);
1621
+ const record = source;
1622
+ const urlKeys = [
1623
+ 'testUrl',
1624
+ 'test_url',
1625
+ 'url',
1626
+ 'targetUrl',
1627
+ 'target_url',
1628
+ 'pageUrl',
1629
+ 'page_url',
1630
+ 'openUrl',
1631
+ 'open_url',
1632
+ 'jumpUrl',
1633
+ 'jump_url',
1634
+ 'deeplink',
1635
+ 'deepLink',
1636
+ 'deep_link',
1637
+ 'schemeUrl',
1638
+ 'scheme_url',
1639
+ 'schemaUrl',
1640
+ 'schema_url',
1641
+ 'h5Url',
1642
+ 'h5_url',
1643
+ 'webUrl',
1644
+ 'web_url',
1645
+ 'landingUrl',
1646
+ 'landing_url',
1647
+ ];
1648
+ for (const key of urlKeys) {
1649
+ const value = this.nonEmptyText(record[key]);
1650
+ if (value)
1651
+ return { value, path: `${label}.${key}` };
1652
+ }
1653
+ const nestedKeys = [
1654
+ 'actionParams',
1655
+ 'params',
1656
+ 'config',
1657
+ 'data',
1658
+ 'payload',
1659
+ 'extra',
1660
+ 'case',
1661
+ 'testCase',
1662
+ 'request',
1663
+ 'context',
1664
+ ];
1665
+ for (const key of nestedKeys) {
1666
+ const nested = record[key];
1667
+ if (nested && typeof nested === 'object') {
1668
+ const picked = this.pickWebTargetUrlWithSource(nested, `${label}.${key}`, seen);
1669
+ if (picked.value)
1670
+ return picked;
1671
+ }
1672
+ }
1673
+ return { value: '', path: '' };
1674
+ }
1550
1675
  pickWebTargetUrl(source) {
1551
- const value = source?.testUrl || source?.test_url || source?.url;
1552
- return value && String(value).trim() ? String(value).trim() : '';
1676
+ return this.pickWebTargetUrlWithSource(source).value;
1677
+ }
1678
+ summarizeUrlInputs(source) {
1679
+ if (!source || typeof source !== 'object')
1680
+ return 'none';
1681
+ const record = source;
1682
+ const topLevelKeys = Object.keys(record).filter((key) => /url|link|scheme|schema|deep/i.test(key));
1683
+ const actionParams = record.actionParams;
1684
+ const actionParamKeys = actionParams && typeof actionParams === 'object'
1685
+ ? Object.keys(actionParams).filter((key) => /url|link|scheme|schema|deep/i.test(key))
1686
+ : [];
1687
+ return `top=[${topLevelKeys.join(',') || '-'}], actionParams=[${actionParamKeys.join(',') || '-'}]`;
1553
1688
  }
1554
1689
  pickBatchAiMemorySource(request, tc) {
1555
1690
  return {
@@ -1633,6 +1768,8 @@ export class DebugWebSocketServer {
1633
1768
  }
1634
1769
  try {
1635
1770
  this.sendMessage(ws, { type: 'web_action_started', sessionId, actionType: request.actionType });
1771
+ const resolvedNaturalLanguage = await this.resolveScriptReferences(request.naturalLanguage);
1772
+ const resolvedActionParams = await this.resolveActionParamsReferences(request.actionParams);
1636
1773
  // 始终启动请求抓包
1637
1774
  try {
1638
1775
  await whistleManager.startCapture(sessionId);
@@ -1860,7 +1997,7 @@ export class DebugWebSocketServer {
1860
1997
  break;
1861
1998
  }
1862
1999
  case 'aiAction': {
1863
- const text = request.naturalLanguage || request.locatePrompt || '';
2000
+ const text = resolvedNaturalLanguage || request.locatePrompt || '';
1864
2001
  if (!text)
1865
2002
  throw new Error('aiAction requires naturalLanguage or locatePrompt');
1866
2003
  result = await agent.aiAct(text);
@@ -1899,7 +2036,7 @@ export class DebugWebSocketServer {
1899
2036
  }
1900
2037
  case 'runCode': {
1901
2038
  // 支持两种方式传递代码:request.code 或 request.actionParams.code
1902
- const code = request.actionParams?.code || request.code || '';
2039
+ const code = resolvedActionParams?.code || request.code || '';
1903
2040
  if (!code)
1904
2041
  throw new Error('runCode requires actionParams.code');
1905
2042
  // 行号探针:与真机 runCode 一致,通过 instrumentCode 在每条语句前插入 __lineCallback(line),
@@ -2286,7 +2423,12 @@ export class DebugWebSocketServer {
2286
2423
  callbackOk: callbackFailures.length === 0,
2287
2424
  callbackFailures: callbackFailures.slice(-10),
2288
2425
  });
2289
- await this.continueNextLoopBatches(ws, request, finalizeCallback.data);
2426
+ try {
2427
+ await this.continueNextLoopBatches(ws, request, finalizeCallback.data);
2428
+ }
2429
+ catch (error) {
2430
+ logger.error(`[BatchAction] Continue next loop batch failed after finalize: batchId=${batchId}, error=${error.message}`);
2431
+ }
2290
2432
  }
2291
2433
  }
2292
2434
  getNextBatchRequests(callbackData) {
@@ -2342,14 +2484,16 @@ export class DebugWebSocketServer {
2342
2484
  if (!targetUrl) {
2343
2485
  throw new Error('Web 用例缺少测试地址,请先在用例中补充测试地址后再执行');
2344
2486
  }
2487
+ const resolvedNaturalLanguage = await this.resolveScriptReferences(tc.naturalLanguage);
2488
+ const resolvedActionParams = await this.resolveActionParamsReferences(tc.actionParams);
2345
2489
  // 构造单条 WebActionExecuteRequest
2346
2490
  const webActionRequest = {
2347
2491
  type: 'execute_web_action',
2348
2492
  actionType: tc.actionType || 'aiAction',
2349
2493
  url: targetUrl,
2350
2494
  testUrl: targetUrl,
2351
- naturalLanguage: tc.naturalLanguage,
2352
- actionParams: tc.actionParams,
2495
+ naturalLanguage: resolvedNaturalLanguage,
2496
+ actionParams: resolvedActionParams,
2353
2497
  sessionId: sharedSessionId || undefined,
2354
2498
  mobileMode: request.mobileMode,
2355
2499
  deviceName: request.deviceName,
@@ -2525,7 +2669,7 @@ export class DebugWebSocketServer {
2525
2669
  // 执行动作
2526
2670
  const actionType = tc.actionType || 'aiAction';
2527
2671
  if (actionType === 'aiAction' || actionType === 'aiAct') {
2528
- const text = tc.naturalLanguage || '';
2672
+ const text = resolvedNaturalLanguage || '';
2529
2673
  if (!text)
2530
2674
  throw new Error('aiAction requires naturalLanguage');
2531
2675
  await agent.aiAct(text);
@@ -2536,7 +2680,7 @@ export class DebugWebSocketServer {
2536
2680
  await this.safeWebGoto(page, targetUrl, `${caseLabel} goto`);
2537
2681
  }
2538
2682
  else if (actionType === 'runCode') {
2539
- const code = tc.actionParams?.code || '';
2683
+ const code = resolvedActionParams?.code || '';
2540
2684
  if (!code)
2541
2685
  throw new Error('runCode requires actionParams.code');
2542
2686
  const AsyncFunctionCtor = async function () { }.constructor;
@@ -2584,6 +2728,11 @@ export class DebugWebSocketServer {
2584
2728
  const effectiveDeviceId = tc.deviceId || request.deviceId;
2585
2729
  const effectivePackageName = tc.packageName || request.packageName;
2586
2730
  const effectiveBundleId = tc.bundleId || request.bundleId;
2731
+ let effectiveTestUrlInfo = this.pickWebTargetUrlWithSource(tc, 'case');
2732
+ if (!effectiveTestUrlInfo.value) {
2733
+ effectiveTestUrlInfo = this.pickWebTargetUrlWithSource(request, 'request');
2734
+ }
2735
+ let effectiveTestUrl = effectiveTestUrlInfo.value;
2587
2736
  const effectiveLoginContext = tc.loginContext || batchLoginContext;
2588
2737
  const caseMemorySource = this.pickBatchAiMemorySource(request, tc);
2589
2738
  const caseSessionId = `batch-${batchId}-case-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
@@ -2631,18 +2780,32 @@ export class DebugWebSocketServer {
2631
2780
  catch (captureErr) {
2632
2781
  logger.warn(`[BatchAction] Failed to start capture: ${captureErr.message}`);
2633
2782
  }
2783
+ const resolvedNaturalLanguage = await this.resolveScriptReferences(tc.naturalLanguage);
2784
+ const resolvedActionParams = await this.resolveActionParamsReferences(tc.actionParams);
2785
+ if (!effectiveTestUrl && resolvedActionParams && resolvedActionParams !== tc.actionParams) {
2786
+ effectiveTestUrlInfo = this.pickWebTargetUrlWithSource({ actionParams: resolvedActionParams }, 'case');
2787
+ effectiveTestUrl = effectiveTestUrlInfo.value;
2788
+ }
2789
+ if (effectiveTestUrl) {
2790
+ logger.info(`${caseLabel}: resolved test URL from ${effectiveTestUrlInfo.path}`);
2791
+ }
2792
+ else {
2793
+ logger.warn(`${caseLabel}: no test URL resolved; checked case ${this.summarizeUrlInputs(tc)}; request ${this.summarizeUrlInputs(request)}`);
2794
+ }
2634
2795
  // 构造 ExecuteConfig
2635
2796
  const actionType = tc.actionType || 'aiAct';
2636
2797
  const config = {
2637
2798
  type: platform,
2638
2799
  testName: tc.title || tc.id || `batch-${batchId}-case-${caseIndex + 1}`,
2639
2800
  actionType,
2640
- naturalLanguage: tc.naturalLanguage,
2641
- locatePrompt: tc.actionParams?.locatePrompt,
2642
- actionParams: tc.actionParams,
2801
+ naturalLanguage: resolvedNaturalLanguage,
2802
+ locatePrompt: resolvedActionParams?.locatePrompt,
2803
+ actionParams: resolvedActionParams,
2643
2804
  udid: effectiveDeviceId,
2644
2805
  packageName: effectivePackageName,
2645
2806
  bundleId: effectiveBundleId,
2807
+ testUrl: effectiveTestUrl || undefined,
2808
+ url: effectiveTestUrl || undefined,
2646
2809
  modelConfig: fullModelConfig,
2647
2810
  aiMemory: caseMemorySource.aiMemory,
2648
2811
  memory: caseMemorySource.memory,
@@ -3083,7 +3246,7 @@ export class DebugWebSocketServer {
3083
3246
  const token = request.callback?.token?.trim();
3084
3247
  if (token)
3085
3248
  headers.Authorization = token.startsWith('Bearer ') ? token : `Bearer ${token}`;
3086
- const timeoutMs = Math.max(1000, Math.min(Number(request.callback?.timeoutMs || 10000) || 10000, 60000));
3249
+ const timeoutMs = Math.max(1000, Math.min(Number(request.callback?.timeoutMs || 10000) || 10000, 600000));
3087
3250
  const maxAttempts = 2;
3088
3251
  for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
3089
3252
  const controller = new AbortController();