@aiscene/aiserver 2.0.6 → 2.0.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.
@@ -18,11 +18,20 @@ import { ExecutorFactory } from '../executor/executor-factory.js';
18
18
  import { instrumentCode } from '../executor/code-instrument.js';
19
19
  import { playwrightSwipe } from '../core/native-swipe.js';
20
20
  import { applyAiMemory } from '../core/ai-memory.js';
21
+ import { FIXED_MODEL_API_KEY } from '../config/index.js';
21
22
  import { whistleManager } from '../proxy/whistle-manager.js';
22
23
  import { executeAndroidLogin, executeWebLogin, } from '../login/android-login-context.js';
23
24
  import { webRecordingManager } from '../recorder/web-recording-manager.js';
24
25
  import { resolveTestCaseReferences } from '../task/test-case-reference-resolver.js';
25
26
  const logger = createLogger('DebugWebSocket');
27
+ const MODEL_API_KEY_ENV_KEYS = new Set([
28
+ 'MIDSCENE_MODEL_API_KEY',
29
+ 'MIDSCENE_PLANNING_MODEL_API_KEY',
30
+ 'MIDSCENE_INSIGHT_MODEL_API_KEY',
31
+ ]);
32
+ function isApiKeyConfigKey(key) {
33
+ return MODEL_API_KEY_ENV_KEYS.has(key) || /api[_-]?key/i.test(key);
34
+ }
26
35
  /** 获取本机可用的非内部 IPv4 地址,供客户端连接投屏 WebSocket */
27
36
  function getLocalIP() {
28
37
  const interfaces = os.networkInterfaces();
@@ -532,6 +541,150 @@ export class DebugWebSocketServer {
532
541
  creator: session?.captureDataPersistence?.creator || '',
533
542
  };
534
543
  }
544
+ getModelConfigString(modelConfig, key, fallback) {
545
+ const value = modelConfig?.[key];
546
+ if (typeof value === 'string' && value.trim())
547
+ return value.trim();
548
+ if (typeof value === 'number' && Number.isFinite(value))
549
+ return String(value);
550
+ return fallback || process.env[key] || '';
551
+ }
552
+ resolveDebugFallbackModelConfig(primaryModelConfig) {
553
+ const ai = this.config.ai;
554
+ const hasFallback = [
555
+ ai.fallbackModelBaseUrl,
556
+ ai.fallbackModelApiKey,
557
+ ai.fallbackModelName,
558
+ ai.fallbackModelFamily,
559
+ ai.fallbackPlanningModelBaseUrl,
560
+ ai.fallbackPlanningModelApiKey,
561
+ ai.fallbackPlanningModelName,
562
+ ai.fallbackPlanningModelFamily,
563
+ ai.fallbackInsightModelBaseUrl,
564
+ ai.fallbackInsightModelApiKey,
565
+ ai.fallbackInsightModelName,
566
+ ai.fallbackInsightModelFamily,
567
+ ].some(value => typeof value === 'string' && value.trim());
568
+ if (!hasFallback)
569
+ return undefined;
570
+ const primary = {
571
+ MIDSCENE_MODEL_BASE_URL: this.getModelConfigString(primaryModelConfig, 'MIDSCENE_MODEL_BASE_URL', ai.baseUrl),
572
+ MIDSCENE_MODEL_API_KEY: FIXED_MODEL_API_KEY,
573
+ MIDSCENE_MODEL_NAME: this.getModelConfigString(primaryModelConfig, 'MIDSCENE_MODEL_NAME', ai.modelName),
574
+ MIDSCENE_MODEL_FAMILY: this.getModelConfigString(primaryModelConfig, 'MIDSCENE_MODEL_FAMILY', ai.modelFamily),
575
+ MIDSCENE_PLANNING_MODEL_BASE_URL: this.getModelConfigString(primaryModelConfig, 'MIDSCENE_PLANNING_MODEL_BASE_URL', ai.planningModelBaseUrl || ai.baseUrl),
576
+ MIDSCENE_PLANNING_MODEL_API_KEY: FIXED_MODEL_API_KEY,
577
+ MIDSCENE_PLANNING_MODEL_NAME: this.getModelConfigString(primaryModelConfig, 'MIDSCENE_PLANNING_MODEL_NAME', ai.planningModelName || ai.modelName),
578
+ MIDSCENE_PLANNING_MODEL_FAMILY: this.getModelConfigString(primaryModelConfig, 'MIDSCENE_PLANNING_MODEL_FAMILY', ai.planningModelFamily || ai.modelFamily),
579
+ MIDSCENE_INSIGHT_MODEL_BASE_URL: this.getModelConfigString(primaryModelConfig, 'MIDSCENE_INSIGHT_MODEL_BASE_URL', ai.insightModelBaseUrl || ai.baseUrl),
580
+ MIDSCENE_INSIGHT_MODEL_API_KEY: FIXED_MODEL_API_KEY,
581
+ MIDSCENE_INSIGHT_MODEL_NAME: this.getModelConfigString(primaryModelConfig, 'MIDSCENE_INSIGHT_MODEL_NAME', ai.insightModelName || ai.modelName),
582
+ MIDSCENE_INSIGHT_MODEL_FAMILY: this.getModelConfigString(primaryModelConfig, 'MIDSCENE_INSIGHT_MODEL_FAMILY', ai.insightModelFamily || ai.modelFamily),
583
+ };
584
+ const fallback = {
585
+ MIDSCENE_MODEL_BASE_URL: ai.fallbackModelBaseUrl || primary.MIDSCENE_MODEL_BASE_URL,
586
+ MIDSCENE_MODEL_API_KEY: FIXED_MODEL_API_KEY,
587
+ MIDSCENE_MODEL_NAME: ai.fallbackModelName || primary.MIDSCENE_MODEL_NAME,
588
+ MIDSCENE_MODEL_FAMILY: ai.fallbackModelFamily || primary.MIDSCENE_MODEL_FAMILY,
589
+ MIDSCENE_PLANNING_MODEL_BASE_URL: ai.fallbackPlanningModelBaseUrl || ai.fallbackModelBaseUrl || primary.MIDSCENE_PLANNING_MODEL_BASE_URL,
590
+ MIDSCENE_PLANNING_MODEL_API_KEY: FIXED_MODEL_API_KEY,
591
+ MIDSCENE_PLANNING_MODEL_NAME: ai.fallbackPlanningModelName || ai.fallbackModelName || primary.MIDSCENE_PLANNING_MODEL_NAME,
592
+ MIDSCENE_PLANNING_MODEL_FAMILY: ai.fallbackPlanningModelFamily || ai.fallbackModelFamily || primary.MIDSCENE_PLANNING_MODEL_FAMILY,
593
+ MIDSCENE_INSIGHT_MODEL_BASE_URL: ai.fallbackInsightModelBaseUrl || ai.fallbackModelBaseUrl || primary.MIDSCENE_INSIGHT_MODEL_BASE_URL,
594
+ MIDSCENE_INSIGHT_MODEL_API_KEY: FIXED_MODEL_API_KEY,
595
+ MIDSCENE_INSIGHT_MODEL_NAME: ai.fallbackInsightModelName || ai.fallbackModelName || primary.MIDSCENE_INSIGHT_MODEL_NAME,
596
+ MIDSCENE_INSIGHT_MODEL_FAMILY: ai.fallbackInsightModelFamily || ai.fallbackModelFamily || primary.MIDSCENE_INSIGHT_MODEL_FAMILY,
597
+ };
598
+ const changed = Object.keys(primary).some(key => primary[key] !== fallback[key]);
599
+ return changed ? fallback : undefined;
600
+ }
601
+ applyResolvedModelConfigToEnv(modelConfig, label) {
602
+ for (const [key, rawValue] of Object.entries(modelConfig)) {
603
+ const value = MODEL_API_KEY_ENV_KEYS.has(key) || isApiKeyConfigKey(key) ? FIXED_MODEL_API_KEY : rawValue;
604
+ if (value) {
605
+ process.env[key] = value;
606
+ logger.info(`[ModelConfig] Applied ${key} = ${value} (${label})`);
607
+ }
608
+ }
609
+ }
610
+ isModelRateLimitError(message) {
611
+ if (!message)
612
+ return false;
613
+ return /\b429\b|too many requests|rate.?limit|quota exceeded|insufficient.?quota|requests per/i.test(message);
614
+ }
615
+ async executeExecutorWithAbort(executor, config, abortController) {
616
+ if (abortController.signal.aborted) {
617
+ return { success: false, errorMessage: 'Action stopped by user' };
618
+ }
619
+ let abortHandler;
620
+ const executePromise = executor.execute(config).catch((error) => ({
621
+ success: false,
622
+ errorMessage: error.message,
623
+ }));
624
+ const abortPromise = new Promise((resolve) => {
625
+ abortHandler = () => resolve({ success: false, errorMessage: 'Action stopped by user' });
626
+ abortController.signal.addEventListener('abort', abortHandler, { once: true });
627
+ });
628
+ try {
629
+ return await Promise.race([executePromise, abortPromise]);
630
+ }
631
+ finally {
632
+ if (abortHandler) {
633
+ abortController.signal.removeEventListener('abort', abortHandler);
634
+ }
635
+ }
636
+ }
637
+ async destroyExecutorForRetry(session, executor, logPrefix) {
638
+ if (!executor || typeof executor.destroy !== 'function')
639
+ return;
640
+ try {
641
+ await executor.destroy();
642
+ }
643
+ catch (destroyErr) {
644
+ logger.warn(`[${logPrefix}] Executor destroy error: ${destroyErr.message}`);
645
+ }
646
+ finally {
647
+ if (session?.executor === executor) {
648
+ session.executor = undefined;
649
+ }
650
+ }
651
+ }
652
+ async destroyCurrentExecutor(session, logPrefix) {
653
+ const executor = session?.executor;
654
+ await this.destroyExecutorForRetry(session, executor, logPrefix);
655
+ }
656
+ async executeWithDebugModelFallback(options) {
657
+ let executor = options.createExecutor();
658
+ if (options.session)
659
+ options.session.executor = executor;
660
+ let result = await this.executeExecutorWithAbort(executor, options.config, options.abortController);
661
+ if (options.abortController.signal.aborted)
662
+ return result;
663
+ const fallbackModelConfig = this.resolveDebugFallbackModelConfig(options.config.modelConfig);
664
+ if (!result.success && fallbackModelConfig && this.isModelRateLimitError(result.errorMessage)) {
665
+ const fallbackLog = '[model-fallback] 429/rate limit detected, retrying once with fallback model config';
666
+ logger.warn(`[${options.logPrefix}] Model rate limit detected, retrying once with fallback model config`);
667
+ sessionManager.addLog(options.sessionId, fallbackLog);
668
+ this.sendMessage(options.ws, {
669
+ type: 'log_output',
670
+ sessionId: options.sessionId,
671
+ content: fallbackLog,
672
+ }, options.deviceId);
673
+ await this.destroyExecutorForRetry(options.session, executor, options.logPrefix);
674
+ if (options.abortController.signal.aborted) {
675
+ return { success: false, errorMessage: 'Action stopped by user' };
676
+ }
677
+ this.applyResolvedModelConfigToEnv(fallbackModelConfig, 'debug fallback model');
678
+ executor = options.createExecutor();
679
+ if (options.session)
680
+ options.session.executor = executor;
681
+ result = await this.executeExecutorWithAbort(executor, { ...options.config, modelConfig: fallbackModelConfig }, options.abortController);
682
+ if (!options.abortController.signal.aborted && !result.success && result.errorMessage) {
683
+ result.errorMessage = `Fallback retry failed: ${result.errorMessage}`;
684
+ }
685
+ }
686
+ return result;
687
+ }
535
688
  async handleStartDebug(ws, request) {
536
689
  // Web 分支:支持 sessionId 复用 + 主进程内嵌执行 + 投屏
537
690
  if (request.platform === 'web') {
@@ -878,7 +1031,7 @@ export class DebugWebSocketServer {
878
1031
  session.abortController = abortController;
879
1032
  // 将前端传来的 modelConfig 应用到环境变量,并获取补全后的 modelConfig(含 Planning/Insight)
880
1033
  const fullModelConfig = this.applyModelConfig(request.modelConfig);
881
- const executor = ExecutorFactory.create('action', {
1034
+ const createExecutor = () => ExecutorFactory.create('action', {
882
1035
  sessionId,
883
1036
  onLog: (msg) => {
884
1037
  sessionManager.addLog(sessionId, msg);
@@ -891,9 +1044,6 @@ export class DebugWebSocketServer {
891
1044
  this.sendMessage(ws, { type: 'execution_line', sessionId, line }, request.deviceId);
892
1045
  },
893
1046
  });
894
- // 保存 executor 引用到 session,以便 stop_debug 时可以中断
895
- if (session)
896
- session.executor = executor;
897
1047
  const config = {
898
1048
  type: (request.platform === 'ios' ? 'ios' : 'android'),
899
1049
  testName: createDebugReportGroupName(request),
@@ -925,39 +1075,23 @@ export class DebugWebSocketServer {
925
1075
  catch (captureErr) {
926
1076
  logger.warn('[Debug] Failed to start request capture: ' + (captureErr instanceof Error ? captureErr.message : String(captureErr)));
927
1077
  }
928
- // 使用 AbortSignal 监听停止请求
929
- const executePromise = executor.execute(config);
930
- const abortPromise = new Promise((resolve) => {
931
- abortController.signal.addEventListener('abort', () => {
932
- resolve({ success: false, errorMessage: 'Action stopped by user' });
933
- }, { once: true });
1078
+ const result = await this.executeWithDebugModelFallback({
1079
+ ws,
1080
+ session,
1081
+ sessionId,
1082
+ deviceId: request.deviceId,
1083
+ config,
1084
+ abortController,
1085
+ createExecutor,
1086
+ logPrefix: 'executeRunCodeInProcess',
934
1087
  });
935
- let result;
936
- let aborted = false;
937
- try {
938
- result = await Promise.race([executePromise, abortPromise]);
939
- // 检测是否由用户主动停止(handleStopDebug 调用 abortController.abort())
940
- aborted = abortController.signal.aborted;
941
- }
942
- catch (error) {
943
- result = { success: false, errorMessage: error.message };
944
- }
945
1088
  // 如果是用户主动停止,由 handleStopDebug 统一发送结果消息,此处不再重复发送
946
- if (aborted) {
1089
+ if (abortController.signal.aborted) {
947
1090
  logger.info(`[executeRunCodeInProcess] Execution aborted by user, skipping result messages (handleStopDebug will send them)`);
948
1091
  return;
949
1092
  }
950
1093
  // 清理 executor(释放 device/agent 资源 + 清除设备代理)
951
- if (executor && typeof executor.destroy === 'function') {
952
- try {
953
- await executor.destroy();
954
- }
955
- catch (destroyErr) {
956
- logger.warn(`[executeRunCodeInProcess] Executor destroy error: ${destroyErr.message}`);
957
- }
958
- }
959
- if (session)
960
- session.executor = undefined;
1094
+ await this.destroyCurrentExecutor(session, 'executeRunCodeInProcess');
961
1095
  // 停止请求抓包,采集调试期间的所有请求
962
1096
  let capturedRequests = [];
963
1097
  let captureDataUrl;
@@ -1055,15 +1189,15 @@ export class DebugWebSocketServer {
1055
1189
  ...process.env,
1056
1190
  WORKER_CONFIG: JSON.stringify(execConfig),
1057
1191
  MIDSCENE_MODEL_BASE_URL: process.env.MIDSCENE_MODEL_BASE_URL || this.config.ai.baseUrl,
1058
- MIDSCENE_MODEL_API_KEY: process.env.MIDSCENE_MODEL_API_KEY || this.config.ai.apiKey,
1192
+ MIDSCENE_MODEL_API_KEY: FIXED_MODEL_API_KEY,
1059
1193
  MIDSCENE_MODEL_NAME: process.env.MIDSCENE_MODEL_NAME || this.config.ai.modelName,
1060
1194
  MIDSCENE_MODEL_FAMILY: process.env.MIDSCENE_MODEL_FAMILY || this.config.ai.modelFamily,
1061
1195
  MIDSCENE_PLANNING_MODEL_BASE_URL: process.env.MIDSCENE_PLANNING_MODEL_BASE_URL || this.config.ai.planningModelBaseUrl,
1062
- MIDSCENE_PLANNING_MODEL_API_KEY: process.env.MIDSCENE_PLANNING_MODEL_API_KEY || this.config.ai.planningModelApiKey,
1196
+ MIDSCENE_PLANNING_MODEL_API_KEY: FIXED_MODEL_API_KEY,
1063
1197
  MIDSCENE_PLANNING_MODEL_NAME: process.env.MIDSCENE_PLANNING_MODEL_NAME || this.config.ai.planningModelName,
1064
1198
  MIDSCENE_PLANNING_MODEL_FAMILY: process.env.MIDSCENE_PLANNING_MODEL_FAMILY || this.config.ai.planningModelFamily,
1065
1199
  MIDSCENE_INSIGHT_MODEL_BASE_URL: process.env.MIDSCENE_INSIGHT_MODEL_BASE_URL || this.config.ai.insightModelBaseUrl,
1066
- MIDSCENE_INSIGHT_MODEL_API_KEY: process.env.MIDSCENE_INSIGHT_MODEL_API_KEY || this.config.ai.insightModelApiKey,
1200
+ MIDSCENE_INSIGHT_MODEL_API_KEY: FIXED_MODEL_API_KEY,
1067
1201
  MIDSCENE_INSIGHT_MODEL_NAME: process.env.MIDSCENE_INSIGHT_MODEL_NAME || this.config.ai.insightModelName,
1068
1202
  MIDSCENE_INSIGHT_MODEL_FAMILY: process.env.MIDSCENE_INSIGHT_MODEL_FAMILY || this.config.ai.insightModelFamily,
1069
1203
  };
@@ -1176,7 +1310,7 @@ export class DebugWebSocketServer {
1176
1310
  // 调试日志:打印收到的模型配置
1177
1311
  logger.info(`>>> [WS] Received execute_action request:`);
1178
1312
  logger.info(`>>> [WS] request.modelName = ${request.modelName}`);
1179
- logger.info(`>>> [WS] request.modelConfig = ${JSON.stringify(request.modelConfig)}`);
1313
+ logger.info(`>>> [WS] request.modelConfig = ${JSON.stringify(this.forceFixedModelApiKeys(request.modelConfig))}`);
1180
1314
  // 将前端传来的 modelConfig 应用到环境变量,并获取补全后的 modelConfig(含 Planning/Insight)
1181
1315
  const fullModelConfig = this.applyModelConfig(request.modelConfig);
1182
1316
  // 同时处理顶层的 modelName 字段
@@ -1194,7 +1328,7 @@ export class DebugWebSocketServer {
1194
1328
  platform: request.platform,
1195
1329
  logPrefix: `[Action ${sessionId}]`,
1196
1330
  });
1197
- const executor = ExecutorFactory.create('action', {
1331
+ const createExecutor = () => ExecutorFactory.create('action', {
1198
1332
  sessionId,
1199
1333
  onLog: (msg) => {
1200
1334
  sessionManager.addLog(sessionId, msg);
@@ -1207,8 +1341,6 @@ export class DebugWebSocketServer {
1207
1341
  this.sendMessage(ws, { type: 'execution_line', sessionId, line }, request.deviceId);
1208
1342
  },
1209
1343
  });
1210
- // 保存 executor 引用到 session,以便 stop_debug 时可以中断
1211
- session.executor = executor;
1212
1344
  const config = {
1213
1345
  type: request.platform || 'android',
1214
1346
  testName: createDebugReportGroupName(request),
@@ -1239,29 +1371,23 @@ export class DebugWebSocketServer {
1239
1371
  catch (captureErr) {
1240
1372
  logger.warn('[Action] Failed to start request capture: ' + (captureErr instanceof Error ? captureErr.message : String(captureErr)));
1241
1373
  }
1242
- // 使用 AbortSignal 监听停止请求
1243
- const executePromise = executor.execute(config);
1244
- const abortPromise = new Promise((resolve) => {
1245
- abortController.signal.addEventListener('abort', () => {
1246
- resolve({ success: false, errorMessage: 'Action stopped by user' });
1247
- }, { once: true });
1374
+ const result = await this.executeWithDebugModelFallback({
1375
+ ws,
1376
+ session,
1377
+ sessionId,
1378
+ deviceId: request.deviceId,
1379
+ config,
1380
+ abortController,
1381
+ createExecutor,
1382
+ logPrefix: 'Action',
1248
1383
  });
1249
- const result = await Promise.race([executePromise, abortPromise]);
1250
1384
  // 如果是用户主动停止,由 handleStopDebug 统一发送结果消息,此处不再重复发送
1251
1385
  if (abortController.signal.aborted) {
1252
1386
  logger.info(`[Action] Execution aborted by user, skipping result messages (handleStopDebug will send them)`);
1253
1387
  return;
1254
1388
  }
1255
1389
  // 清理 executor(释放 device/agent 资源 + 清除设备代理)
1256
- if (executor && typeof executor.destroy === 'function') {
1257
- try {
1258
- await executor.destroy();
1259
- }
1260
- catch (destroyErr) {
1261
- logger.warn(`[Action] Executor destroy error: ${destroyErr.message}`);
1262
- }
1263
- }
1264
- session.executor = undefined;
1390
+ await this.destroyCurrentExecutor(session, 'Action');
1265
1391
  // 停止请求抓包,采集执行期间的所有请求
1266
1392
  let capturedRequests = [];
1267
1393
  let captureDataUrl;
@@ -1470,7 +1596,7 @@ export class DebugWebSocketServer {
1470
1596
  platform: request.platform,
1471
1597
  logPrefix: `[AiAct ${sessionId}]`,
1472
1598
  });
1473
- const executor = ExecutorFactory.create('action', {
1599
+ const createExecutor = () => ExecutorFactory.create('action', {
1474
1600
  sessionId,
1475
1601
  onLog: (msg) => {
1476
1602
  sessionManager.addLog(sessionId, msg);
@@ -1480,8 +1606,6 @@ export class DebugWebSocketServer {
1480
1606
  this.sendMessage(ws, { type: 'action_dump', sessionId, dump }, request.deviceId);
1481
1607
  },
1482
1608
  });
1483
- // 保存 executor 引用到 session,以便 stop_debug 时可以中断
1484
- session.executor = executor;
1485
1609
  const config = {
1486
1610
  type: request.platform || 'android',
1487
1611
  testName: createDebugReportGroupName(request),
@@ -1512,14 +1636,16 @@ export class DebugWebSocketServer {
1512
1636
  catch (captureErr) {
1513
1637
  logger.warn('[Action] Failed to start request capture: ' + (captureErr instanceof Error ? captureErr.message : String(captureErr)));
1514
1638
  }
1515
- // 使用 AbortSignal 监听停止请求
1516
- const executePromise = executor.execute(config);
1517
- const abortPromise = new Promise((resolve) => {
1518
- abortController.signal.addEventListener('abort', () => {
1519
- resolve({ success: false, errorMessage: 'Action stopped by user' });
1520
- }, { once: true });
1639
+ const result = await this.executeWithDebugModelFallback({
1640
+ ws,
1641
+ session,
1642
+ sessionId,
1643
+ deviceId: request.deviceId,
1644
+ config,
1645
+ abortController,
1646
+ createExecutor,
1647
+ logPrefix: 'AiAct',
1521
1648
  });
1522
- const result = await Promise.race([executePromise, abortPromise]);
1523
1649
  // 用户主动停止由 handleStopDebug 统一发送结果消息,这里不重复发;同时把 finalSent 置位防止 finally 兜底再发
1524
1650
  if (abortController.signal.aborted) {
1525
1651
  finalSent = true;
@@ -1527,15 +1653,7 @@ export class DebugWebSocketServer {
1527
1653
  return;
1528
1654
  }
1529
1655
  // 清理 executor(释放 device/agent 资源 + 清除设备代理)
1530
- if (executor && typeof executor.destroy === 'function') {
1531
- try {
1532
- await executor.destroy();
1533
- }
1534
- catch (destroyErr) {
1535
- logger.warn(`[AiAct] Executor destroy error: ${destroyErr.message}`);
1536
- }
1537
- }
1538
- session.executor = undefined;
1656
+ await this.destroyCurrentExecutor(session, 'AiAct');
1539
1657
  // 停止请求抓包,采集执行期间的所有请求
1540
1658
  let captureDataUrl;
1541
1659
  try {
@@ -3209,13 +3327,26 @@ export class DebugWebSocketServer {
3209
3327
  * 将前端传来的 modelConfig 应用到环境变量
3210
3328
  * ActionExecutor 通过环境变量读取模型配置,所以需要同步更新
3211
3329
  */
3212
- applyModelConfig(modelConfig) {
3330
+ forceFixedModelApiKeys(modelConfig) {
3213
3331
  const result = { ...(modelConfig || {}) };
3332
+ for (const key of MODEL_API_KEY_ENV_KEYS) {
3333
+ result[key] = FIXED_MODEL_API_KEY;
3334
+ }
3335
+ for (const key of Object.keys(result)) {
3336
+ if (isApiKeyConfigKey(key)) {
3337
+ result[key] = FIXED_MODEL_API_KEY;
3338
+ }
3339
+ }
3340
+ return result;
3341
+ }
3342
+ applyModelConfig(modelConfig) {
3343
+ const sanitizedModelConfig = this.forceFixedModelApiKeys(modelConfig);
3344
+ const result = { ...sanitizedModelConfig };
3214
3345
  // 1. If no modelConfig provided, use server config ai defaults for main model
3215
3346
  if (!modelConfig || typeof modelConfig !== 'object' || Object.keys(modelConfig).length === 0) {
3216
3347
  const mainModelDefaults = {
3217
3348
  MIDSCENE_MODEL_BASE_URL: this.config.ai.baseUrl,
3218
- MIDSCENE_MODEL_API_KEY: this.config.ai.apiKey,
3349
+ MIDSCENE_MODEL_API_KEY: FIXED_MODEL_API_KEY,
3219
3350
  MIDSCENE_MODEL_NAME: this.config.ai.modelName,
3220
3351
  MIDSCENE_MODEL_FAMILY: this.config.ai.modelFamily,
3221
3352
  };
@@ -3235,9 +3366,16 @@ export class DebugWebSocketServer {
3235
3366
  'MIDSCENE_INSIGHT_MODEL_BASE_URL', 'MIDSCENE_INSIGHT_MODEL_API_KEY', 'MIDSCENE_INSIGHT_MODEL_NAME', 'MIDSCENE_INSIGHT_MODEL_FAMILY',
3236
3367
  ];
3237
3368
  for (const key of envKeys) {
3238
- const value = modelConfig[key];
3369
+ if (MODEL_API_KEY_ENV_KEYS.has(key)) {
3370
+ process.env[key] = FIXED_MODEL_API_KEY;
3371
+ result[key] = FIXED_MODEL_API_KEY;
3372
+ logger.info(`[ModelConfig] Applied ${key} = ${FIXED_MODEL_API_KEY} (fixed)`);
3373
+ continue;
3374
+ }
3375
+ const value = sanitizedModelConfig[key];
3239
3376
  if (value && typeof value === 'string') {
3240
3377
  process.env[key] = value;
3378
+ result[key] = value;
3241
3379
  logger.info(`[ModelConfig] Applied ${key} = ${value}`);
3242
3380
  }
3243
3381
  }
@@ -3257,7 +3395,7 @@ export class DebugWebSocketServer {
3257
3395
  // 2. Always force-apply Planning model config from server config
3258
3396
  const planningDefaults = {
3259
3397
  MIDSCENE_PLANNING_MODEL_BASE_URL: this.config.ai.planningModelBaseUrl,
3260
- MIDSCENE_PLANNING_MODEL_API_KEY: this.config.ai.planningModelApiKey,
3398
+ MIDSCENE_PLANNING_MODEL_API_KEY: FIXED_MODEL_API_KEY,
3261
3399
  MIDSCENE_PLANNING_MODEL_NAME: this.config.ai.planningModelName,
3262
3400
  MIDSCENE_PLANNING_MODEL_FAMILY: this.config.ai.planningModelFamily,
3263
3401
  };
@@ -3271,7 +3409,7 @@ export class DebugWebSocketServer {
3271
3409
  // 3. Always force-apply Insight model config from server config
3272
3410
  const insightDefaults = {
3273
3411
  MIDSCENE_INSIGHT_MODEL_BASE_URL: this.config.ai.insightModelBaseUrl,
3274
- MIDSCENE_INSIGHT_MODEL_API_KEY: this.config.ai.insightModelApiKey,
3412
+ MIDSCENE_INSIGHT_MODEL_API_KEY: FIXED_MODEL_API_KEY,
3275
3413
  MIDSCENE_INSIGHT_MODEL_NAME: this.config.ai.insightModelName,
3276
3414
  MIDSCENE_INSIGHT_MODEL_FAMILY: this.config.ai.insightModelFamily,
3277
3415
  };