yymaxapi 1.0.100 → 1.0.102

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 (2) hide show
  1. package/bin/yymaxapi.js +174 -105
  2. package/package.json +1 -1
package/bin/yymaxapi.js CHANGED
@@ -254,6 +254,34 @@ function getDefaultCodexModel() {
254
254
  return CODEX_MODELS[0] || { id: 'gpt-5.4', name: 'GPT 5.4' };
255
255
  }
256
256
 
257
+ function getExternalClaudeProviderKey() {
258
+ return String(API_CONFIG?.claude?.providerName || 'yunyi-claude').trim() || 'yunyi-claude';
259
+ }
260
+
261
+ function getExternalCodexProviderKey() {
262
+ const providerKey = String(API_CONFIG?.codex?.providerName || '').trim();
263
+ if (!providerKey) return 'yunyi-codex';
264
+ return providerKey === 'yunyi' ? 'yunyi-codex' : providerKey;
265
+ }
266
+
267
+ function getExternalModelKey(type, modelId) {
268
+ const providerKey = type === 'codex' ? getExternalCodexProviderKey() : getExternalClaudeProviderKey();
269
+ return `${providerKey}/${modelId}`;
270
+ }
271
+
272
+ function getProviderBrandPrefix() {
273
+ const providerKeys = [getExternalClaudeProviderKey(), getExternalCodexProviderKey()];
274
+ if (providerKeys.some(key => key.startsWith('maxapi'))) return 'MAXAPI';
275
+ if (providerKeys.some(key => key.startsWith('yunyi'))) return '云翼';
276
+ return 'OpenClaw';
277
+ }
278
+
279
+ function isUnifiedSingleModelMode() {
280
+ return CLAUDE_MODELS.length === 1
281
+ && CODEX_MODELS.length === 1
282
+ && CLAUDE_MODELS[0].id === CODEX_MODELS[0].id;
283
+ }
284
+
257
285
  function buildProviderModelMap(models) {
258
286
  const mapped = {};
259
287
  for (const model of models || []) {
@@ -268,7 +296,7 @@ function getClaudeSwitchHint() {
268
296
  }
269
297
 
270
298
  function getOpencodeSwitchHint() {
271
- return [...CLAUDE_MODELS, ...CODEX_MODELS].map(model => model.name).join(' / ');
299
+ return [...new Set([...CLAUDE_MODELS, ...CODEX_MODELS].map(model => model.name))].join(' / ');
272
300
  }
273
301
 
274
302
  async function promptClaudeModelSelection(args = {}, message = '选择 Claude 模型:') {
@@ -297,16 +325,29 @@ async function promptOpencodeDefaultModelSelection(args = {}, message = '选择
297
325
  const fallbackClaude = getDefaultClaudeModel();
298
326
  const fallbackCodex = getDefaultCodexModel();
299
327
  const requested = (args['default-model'] || args.model || args['claude-model'] || args['codex-model'] || '').toString().trim();
328
+ const claudeProviderKey = getExternalClaudeProviderKey();
329
+ const codexProviderKey = getExternalCodexProviderKey();
330
+
331
+ if (isUnifiedSingleModelMode()) {
332
+ const unifiedModel = CLAUDE_MODELS[0] || CODEX_MODELS[0] || fallbackClaude;
333
+ return {
334
+ type: 'claude',
335
+ providerKey: claudeProviderKey,
336
+ modelId: unifiedModel.id,
337
+ modelName: unifiedModel.name,
338
+ modelKey: getExternalModelKey('claude', unifiedModel.id)
339
+ };
340
+ }
300
341
 
301
342
  if (requested) {
302
343
  const inClaude = CLAUDE_MODELS.find(model => model.id === requested);
303
344
  if (inClaude) {
304
345
  return {
305
346
  type: 'claude',
306
- providerKey: 'yunyi-claude',
347
+ providerKey: claudeProviderKey,
307
348
  modelId: inClaude.id,
308
349
  modelName: inClaude.name,
309
- modelKey: `yunyi-claude/${inClaude.id}`
350
+ modelKey: getExternalModelKey('claude', inClaude.id)
310
351
  };
311
352
  }
312
353
 
@@ -314,10 +355,10 @@ async function promptOpencodeDefaultModelSelection(args = {}, message = '选择
314
355
  if (inCodex) {
315
356
  return {
316
357
  type: 'codex',
317
- providerKey: 'yunyi-codex',
358
+ providerKey: codexProviderKey,
318
359
  modelId: inCodex.id,
319
360
  modelName: inCodex.name,
320
- modelKey: `yunyi-codex/${inCodex.id}`
361
+ modelKey: getExternalModelKey('codex', inCodex.id)
321
362
  };
322
363
  }
323
364
  }
@@ -350,20 +391,20 @@ async function promptOpencodeDefaultModelSelection(args = {}, message = '选择
350
391
  const model = CODEX_MODELS.find(item => item.id === pickedId) || fallbackCodex;
351
392
  return {
352
393
  type: 'codex',
353
- providerKey: 'yunyi-codex',
394
+ providerKey: codexProviderKey,
354
395
  modelId: model.id,
355
396
  modelName: model.name,
356
- modelKey: `yunyi-codex/${model.id}`
397
+ modelKey: getExternalModelKey('codex', model.id)
357
398
  };
358
399
  }
359
400
 
360
401
  const model = CLAUDE_MODELS.find(item => item.id === pickedId) || fallbackClaude;
361
402
  return {
362
403
  type: 'claude',
363
- providerKey: 'yunyi-claude',
404
+ providerKey: claudeProviderKey,
364
405
  modelId: model.id,
365
406
  modelName: model.name,
366
- modelKey: `yunyi-claude/${model.id}`
407
+ modelKey: getExternalModelKey('claude', model.id)
367
408
  };
368
409
  }
369
410
 
@@ -371,6 +412,7 @@ async function promptOpencodeDefaultModelSelection(args = {}, message = '选择
371
412
  const BACKUP_FILENAME = 'openclaw-default.json.bak';
372
413
  const BACKUP_DIR_NAME = 'backups';
373
414
  const MAX_BACKUPS = 10;
415
+ const GATEWAY_CLI_NAMES = ['openclaw'];
374
416
  const EXTRA_BIN_DIRS = [
375
417
  path.join(os.homedir(), '.npm-global', 'bin'),
376
418
  path.join(os.homedir(), '.local', 'bin'),
@@ -864,13 +906,14 @@ function writeCodexConfig(baseUrl, apiKey, modelId = 'gpt-5.4') {
864
906
  const markerEnd = '# <<< maxapi codex <<<';
865
907
  const topMarker = '# >>> maxapi codex top >>>';
866
908
  const topMarkerEnd = '# <<< maxapi codex top <<<';
867
- const providerKey = 'yunyi-codex';
909
+ const providerKey = getExternalCodexProviderKey();
910
+ const providerLabel = `${getProviderBrandPrefix()} Codex`;
911
+ const removableProviderKeys = [...new Set([providerKey, 'yunyi-codex', 'maxapi-codex'])];
868
912
  // 确保 base_url 以 /v1 结尾(Codex CLI 要求)
869
913
  let normalizedUrl = baseUrl.replace(/\/+$/, '');
870
914
  if (!normalizedUrl.endsWith('/v1')) normalizedUrl += '/v1';
871
915
  try {
872
916
  let existing = '';
873
- const escapedProviderKey = providerKey.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
874
917
  if (fs.existsSync(configPath)) {
875
918
  existing = fs.readFileSync(configPath, 'utf8');
876
919
  // 移除旧的 maxapi section(provider block)
@@ -879,8 +922,11 @@ function writeCodexConfig(baseUrl, apiKey, modelId = 'gpt-5.4') {
879
922
  // 移除旧的 maxapi top-level block
880
923
  const topRe = new RegExp(`${topMarker.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}[\\s\\S]*?${topMarkerEnd.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\n?`, 'g');
881
924
  existing = existing.replace(topRe, '');
882
- // 兼容旧版:移除历史遗留的未标记 yunyi-codex provider、旧 openclaw-relay、旧 yunyi opencode 标记块
883
- existing = existing.replace(new RegExp(`\\[model_providers\\.${escapedProviderKey}\\]\\n(?:(?!\\[)[^\\n]*\\n?)*`, 'g'), '');
925
+ // 兼容旧版:移除历史遗留的 provider block、旧 openclaw-relay、旧 yunyi opencode 标记块
926
+ for (const removableProviderKey of removableProviderKeys) {
927
+ const escapedProviderKey = removableProviderKey.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
928
+ existing = existing.replace(new RegExp(`\\[model_providers\\.${escapedProviderKey}\\]\\n(?:(?!\\[)[^\\n]*\\n?)*`, 'g'), '');
929
+ }
884
930
  existing = existing.replace(/\[model_providers\.openclaw-relay\]\n(?:(?!\[)[^\n]*\n?)*/g, '');
885
931
  existing = existing.replace(/# >>> yunyi opencode >>>[\s\S]*?# <<< yunyi opencode <<<\n?/g, '');
886
932
  existing = existing.replace(/\n{3,}/g, '\n\n').trim();
@@ -900,7 +946,7 @@ function writeCodexConfig(baseUrl, apiKey, modelId = 'gpt-5.4') {
900
946
  const providerBlock = [
901
947
  marker,
902
948
  `[model_providers.${providerKey}]`,
903
- `name = "云翼 Codex"`,
949
+ `name = "${providerLabel}"`,
904
950
  `base_url = "${normalizedUrl}"`,
905
951
  `wire_api = "responses"`,
906
952
  `experimental_bearer_token = "${apiKey}"`,
@@ -931,10 +977,13 @@ function writeCodexConfig(baseUrl, apiKey, modelId = 'gpt-5.4') {
931
977
  } catch { /* 非关键,静默失败 */ }
932
978
  }
933
979
 
934
- function writeOpencodeConfig(claudeBaseUrl, codexBaseUrl, apiKey, defaultModelKey = `yunyi-claude/${getDefaultClaudeModel().id}`) {
980
+ function writeOpencodeConfig(claudeBaseUrl, codexBaseUrl, apiKey, defaultModelKey = getExternalModelKey('claude', getDefaultClaudeModel().id)) {
935
981
  const home = os.homedir();
936
982
  const claudeUrl = claudeBaseUrl.replace(/\/+$/, '');
937
983
  const codexUrl = (codexBaseUrl || '').replace(/\/+$/, '');
984
+ const claudeProviderKey = getExternalClaudeProviderKey();
985
+ const codexProviderKey = getExternalCodexProviderKey();
986
+ const brandPrefix = getProviderBrandPrefix();
938
987
 
939
988
  // ---- 1. opencode.json (CLI + 桌面版) ----
940
989
  const configDir = process.platform === 'win32'
@@ -950,8 +999,8 @@ function writeOpencodeConfig(claudeBaseUrl, codexBaseUrl, apiKey, defaultModelKe
950
999
  if (!existing.provider) existing.provider = {};
951
1000
 
952
1001
  // Claude provider (@ai-sdk/anthropic)
953
- existing.provider['yunyi-claude'] = {
954
- name: '云翼 Claude',
1002
+ existing.provider[claudeProviderKey] = {
1003
+ name: `${brandPrefix} Claude`,
955
1004
  npm: '@ai-sdk/anthropic',
956
1005
  models: buildProviderModelMap(CLAUDE_MODELS),
957
1006
  options: { apiKey, baseURL: `${claudeUrl}/v1` }
@@ -959,8 +1008,8 @@ function writeOpencodeConfig(claudeBaseUrl, codexBaseUrl, apiKey, defaultModelKe
959
1008
 
960
1009
  // Codex provider (@ai-sdk/openai)
961
1010
  if (codexUrl) {
962
- existing.provider['yunyi-codex'] = {
963
- name: '云翼 Codex',
1011
+ existing.provider[codexProviderKey] = {
1012
+ name: `${brandPrefix} Codex`,
964
1013
  npm: '@ai-sdk/openai',
965
1014
  models: buildProviderModelMap(CODEX_MODELS),
966
1015
  options: { apiKey, baseURL: codexUrl }
@@ -978,7 +1027,8 @@ function writeOpencodeConfig(claudeBaseUrl, codexBaseUrl, apiKey, defaultModelKe
978
1027
 
979
1028
  // 从 disabled_providers 中移除我们的 provider
980
1029
  if (Array.isArray(existing.disabled_providers)) {
981
- existing.disabled_providers = existing.disabled_providers.filter(p => p !== 'yunyi-claude' && p !== 'yunyi-codex');
1030
+ const enabledProviders = new Set([claudeProviderKey, codexProviderKey, 'yunyi-claude', 'yunyi-codex', 'maxapi', 'maxapi-codex']);
1031
+ existing.disabled_providers = existing.disabled_providers.filter(p => !enabledProviders.has(p));
982
1032
  if (existing.disabled_providers.length === 0) delete existing.disabled_providers;
983
1033
  }
984
1034
 
@@ -1036,7 +1086,7 @@ function syncExternalTools(type, baseUrl, apiKey, extra = {}) {
1036
1086
  synced.push('Codex CLI config');
1037
1087
  }
1038
1088
  if (type === 'claude' && extra.codexBaseUrl) {
1039
- writeOpencodeConfig(baseUrl, extra.codexBaseUrl, apiKey, extra.opencodeDefaultModelKey || `yunyi-claude/${extra.claudeModelId || getDefaultClaudeModel().id}`);
1089
+ writeOpencodeConfig(baseUrl, extra.codexBaseUrl, apiKey, extra.opencodeDefaultModelKey || getExternalModelKey('claude', extra.claudeModelId || getDefaultClaudeModel().id));
1040
1090
  synced.push('Opencode config');
1041
1091
  }
1042
1092
  } catch { /* ignore */ }
@@ -1102,7 +1152,7 @@ function dockerCmd(cmd) {
1102
1152
  return _dockerNeedsSudo ? `sudo -n docker ${cmd}` : `docker ${cmd}`;
1103
1153
  }
1104
1154
 
1105
- // 查找包含 openclaw/clawdbot/moltbot 的运行中容器
1155
+ // 查找包含 openclaw 的运行中容器
1106
1156
  function findOpenclawDockerContainers() {
1107
1157
  if (_dockerContainerCache !== null) return _dockerContainerCache;
1108
1158
  if (!isDockerAvailable()) { _dockerContainerCache = []; return []; }
@@ -1120,7 +1170,7 @@ function findOpenclawDockerContainers() {
1120
1170
  const status = statusParts.join(' ');
1121
1171
  if (!id) continue;
1122
1172
 
1123
- // 检查容器内是否有 openclaw/clawdbot/moltbot(用 command -v 代替 which,兼容精简镜像)
1173
+ // 检查容器内是否有 openclaw(用 command -v 代替 which,兼容精简镜像)
1124
1174
  for (const cli of ['openclaw', 'clawdbot', 'moltbot']) {
1125
1175
  const check = safeExec(dockerCmd(`exec ${id} sh -c "command -v ${cli} 2>/dev/null || which ${cli} 2>/dev/null"`), { timeout: 8000 });
1126
1176
  if (check.ok && check.output && check.output.trim()) {
@@ -1294,7 +1344,7 @@ let _wslCliBinaryCache = undefined;
1294
1344
 
1295
1345
  function getWslCliBinary() {
1296
1346
  if (_wslCliBinaryCache !== undefined) return _wslCliBinaryCache;
1297
- for (const name of ['openclaw', 'clawdbot', 'moltbot']) {
1347
+ for (const name of GATEWAY_CLI_NAMES) {
1298
1348
  try {
1299
1349
  const result = execFileSync('wsl', ['bash', '-lc', `which ${name} 2>/dev/null`], { encoding: 'utf8', timeout: 5000, stdio: 'pipe' }).trim();
1300
1350
  if (result && result.startsWith('/')) {
@@ -1408,7 +1458,7 @@ function cleanupAgentProcesses() {
1408
1458
  try {
1409
1459
  const { execSync } = require('child_process');
1410
1460
  if (process.platform === 'win32') {
1411
- for (const name of ['openclaw', 'clawdbot', 'moltbot']) {
1461
+ for (const name of GATEWAY_CLI_NAMES) {
1412
1462
  try {
1413
1463
  execSync(`wmic process where "commandline like '%${name}%agent%' and not commandline like '%gateway%'" call terminate 2>nul`, { stdio: 'ignore', timeout: 10000 });
1414
1464
  } catch { /* ignore */ }
@@ -1419,18 +1469,18 @@ function cleanupAgentProcesses() {
1419
1469
  // WSL 内的 agent 也要清理
1420
1470
  if (detectGatewayEnv() === 'wsl') {
1421
1471
  try {
1422
- execSync('wsl -- bash -lc "pkill -f \'openclaw.*agent\' 2>/dev/null; pkill -f \'clawdbot.*agent\' 2>/dev/null; pkill -f \'moltbot.*agent\' 2>/dev/null; true"', { stdio: 'ignore', timeout: 10000 });
1472
+ execSync('wsl -- bash -lc "pkill -f \'openclaw.*agent\' 2>/dev/null || true"', { stdio: 'ignore', timeout: 10000 });
1423
1473
  } catch { /* ignore */ }
1424
1474
  }
1425
1475
  } else {
1426
- for (const name of ['openclaw', 'clawdbot', 'moltbot']) {
1476
+ for (const name of GATEWAY_CLI_NAMES) {
1427
1477
  execSync(`pkill -f '${name}.*agent' 2>/dev/null || true`, { stdio: 'ignore' });
1428
1478
  }
1429
1479
  }
1430
1480
  // Docker 容器内的 agent 也要清理
1431
1481
  if (detectGatewayEnv() === 'docker' && _selectedDockerContainer) {
1432
1482
  try {
1433
- for (const name of ['openclaw', 'clawdbot', 'moltbot']) {
1483
+ for (const name of GATEWAY_CLI_NAMES) {
1434
1484
  safeExec(dockerCmd(`exec ${_selectedDockerContainer.id} sh -c "pkill -f '${name}.*agent' 2>/dev/null || true"`), { timeout: 10000 });
1435
1485
  }
1436
1486
  } catch { /* ignore */ }
@@ -3160,6 +3210,14 @@ function shellQuote(value) {
3160
3210
  return `"${str.replace(/(["\\$`])/g, '\\$1')}"`;
3161
3211
  }
3162
3212
 
3213
+ function escapeSingleQuotedShell(value) {
3214
+ return String(value || '').replace(/'/g, "'\\''");
3215
+ }
3216
+
3217
+ function buildLoginShellCommand(shellPath, command) {
3218
+ return `${shellPath} -lc '${escapeSingleQuotedShell(command)}'`;
3219
+ }
3220
+
3163
3221
  function escapeXml(value) {
3164
3222
  return String(value)
3165
3223
  .replace(/&/g, '&amp;')
@@ -3203,7 +3261,7 @@ function resolveCliBinary() {
3203
3261
  }
3204
3262
  }
3205
3263
 
3206
- const candidates = ['openclaw', 'clawdbot', 'moltbot'];
3264
+ const candidates = GATEWAY_CLI_NAMES;
3207
3265
  const isWin = process.platform === 'win32';
3208
3266
  const searchDirs = (process.env.PATH || '').split(path.delimiter).concat(EXTRA_BIN_DIRS);
3209
3267
  for (const name of candidates) {
@@ -3222,30 +3280,6 @@ function resolveCliBinary() {
3222
3280
  }
3223
3281
  }
3224
3282
 
3225
- const moltbotRoots = [];
3226
- if (process.env.MOLTBOT_ROOT) moltbotRoots.push(process.env.MOLTBOT_ROOT);
3227
- moltbotRoots.push('/opt/moltbot');
3228
- moltbotRoots.push('/opt/moltbot/app');
3229
-
3230
- const scriptCandidates = [];
3231
- for (const root of moltbotRoots) {
3232
- scriptCandidates.push(
3233
- path.join(root, 'moltbot.mjs'),
3234
- path.join(root, 'bin', 'moltbot.mjs'),
3235
- path.join(root, 'bin', 'moltbot.js'),
3236
- path.join(root, 'src', 'moltbot.mjs'),
3237
- path.join(root, 'src', 'moltbot.js')
3238
- );
3239
- }
3240
-
3241
- for (const candidate of scriptCandidates) {
3242
- try {
3243
- if (fs.existsSync(candidate) && fs.statSync(candidate).isFile()) {
3244
- return candidate;
3245
- }
3246
- } catch { }
3247
- }
3248
-
3249
3283
  // Fallback: use login shell to find the binary (loads .zshrc/.bashrc PATH)
3250
3284
  for (const name of candidates) {
3251
3285
  if (process.platform === 'win32') {
@@ -3312,8 +3346,7 @@ function getCliMeta() {
3312
3346
  const cliBinary = resolveCliBinary();
3313
3347
  const cliName = cliBinary ? path.basename(cliBinary) : '';
3314
3348
  const cliLower = cliName.toLowerCase();
3315
- const isMoltbot = cliLower.startsWith('moltbot') || cliLower.includes('moltbot');
3316
- const nodeMajor = isMoltbot ? 24 : 22;
3349
+ const nodeMajor = 22;
3317
3350
  return { cliBinary, cliName, nodeMajor };
3318
3351
  }
3319
3352
 
@@ -3399,7 +3432,7 @@ function readOpencodeCliConfig() {
3399
3432
  const config = readJsonIfExists(configPath) || {};
3400
3433
  return {
3401
3434
  configPath,
3402
- modelKey: config.model || `yunyi-claude/${getDefaultClaudeModel().id}`,
3435
+ modelKey: config.model || getExternalModelKey('claude', getDefaultClaudeModel().id),
3403
3436
  configured: fs.existsSync(configPath),
3404
3437
  config
3405
3438
  };
@@ -3410,7 +3443,7 @@ function readCodexCliConfig() {
3410
3443
  const configRaw = fs.existsSync(configPath) ? fs.readFileSync(configPath, 'utf8') : '';
3411
3444
  const auth = readJsonIfExists(authPath) || {};
3412
3445
  const model = (configRaw.match(/^model\s*=\s*"([^"]+)"\s*$/m) || [])[1] || getDefaultCodexModel().id;
3413
- const provider = (configRaw.match(/^model_provider\s*=\s*"([^"]+)"\s*$/m) || [])[1] || 'yunyi-codex';
3446
+ const provider = (configRaw.match(/^model_provider\s*=\s*"([^"]+)"\s*$/m) || [])[1] || getExternalCodexProviderKey();
3414
3447
  const providerBlockRegex = new RegExp(`\\[model_providers\\.${escapeRegExp(provider)}\\]([\\s\\S]*?)(?=\\n\\[|$)`, 'm');
3415
3448
  const providerBlock = (configRaw.match(providerBlockRegex) || [])[1] || '';
3416
3449
  const baseUrl = (providerBlock.match(/base_url\s*=\s*"([^"]+)"/) || [])[1] || '';
@@ -3826,7 +3859,7 @@ async function tryAutoStartGateway(port, allowAutoDaemon) {
3826
3859
  if (container.cliPath) dockerCmds.push(`${container.cliPath} gateway`);
3827
3860
  dockerCmds.push(`${container.cli} gateway`);
3828
3861
  }
3829
- for (const name of ['openclaw', 'clawdbot', 'moltbot']) {
3862
+ for (const name of GATEWAY_CLI_NAMES) {
3830
3863
  dockerCmds.push(`${name} gateway`);
3831
3864
  }
3832
3865
 
@@ -3858,7 +3891,7 @@ async function tryAutoStartGateway(port, allowAutoDaemon) {
3858
3891
  }
3859
3892
  } else {
3860
3893
  if (daemonResult.reason === 'cli-not-found') {
3861
- console.log(chalk.red('❌ 未找到 openclaw/clawdbot/moltbot 命令,无法自动启动 Gateway'));
3894
+ console.log(chalk.red('❌ 未找到 openclaw 命令,无法自动启动 Gateway'));
3862
3895
  } else {
3863
3896
  console.log(chalk.red(`❌ 自动启动失败: ${daemonResult.reason}`));
3864
3897
  }
@@ -3902,15 +3935,9 @@ async function tryAutoStartGateway(port, allowAutoDaemon) {
3902
3935
  const nodeInfo = findCompatibleNode(nodeMajor);
3903
3936
  const env = { ...process.env, PATH: extendPathEnv(nodeInfo ? nodeInfo.path : null) };
3904
3937
  const useNode = cliBinary && nodeInfo && isNodeShebang(cliBinary);
3905
- const cliCmd = cliBinary
3906
- ? (useNode ? `"${nodeInfo.path}" "${cliBinary}" gateway` : `"${cliBinary}" gateway`)
3907
- : null;
3938
+ const candidates = buildGatewayCommands(cliBinary, nodeInfo, useNode, 'start');
3908
3939
 
3909
- const candidates = [];
3910
- if (cliCmd) candidates.push(cliCmd);
3911
- candidates.push('openclaw gateway', 'clawdbot gateway', 'moltbot gateway');
3912
-
3913
- for (const cmd of [...new Set(candidates)].filter(Boolean)) {
3940
+ for (const cmd of candidates) {
3914
3941
  console.log(chalk.yellow(`⚠️ 尝试启动 Gateway: ${cmd}`));
3915
3942
  if (spawnDetached(cmd, env)) {
3916
3943
  if (await waitForGateway(port, '127.0.0.1', 10000)) {
@@ -3920,6 +3947,22 @@ async function tryAutoStartGateway(port, allowAutoDaemon) {
3920
3947
  }
3921
3948
  }
3922
3949
 
3950
+ if (process.platform !== 'win32') {
3951
+ for (const sh of ['/bin/zsh', '/bin/bash']) {
3952
+ if (!fs.existsSync(sh)) continue;
3953
+ for (const cmd of candidates) {
3954
+ const loginShellCmd = buildLoginShellCommand(sh, cmd);
3955
+ console.log(chalk.yellow(`⚠️ 尝试启动 Gateway: ${loginShellCmd}`));
3956
+ if (spawnDetached(loginShellCmd, env)) {
3957
+ if (await waitForGateway(port, '127.0.0.1', 15000)) {
3958
+ console.log(chalk.green('✅ Gateway 已通过 login shell 启动'));
3959
+ return { started: true, method: 'cli-login-shell', cmd: loginShellCmd };
3960
+ }
3961
+ }
3962
+ }
3963
+ }
3964
+ }
3965
+
3923
3966
  return { started: false };
3924
3967
  }
3925
3968
 
@@ -4369,7 +4412,7 @@ async function presetClaude(paths, args = {}) {
4369
4412
  if (yunyiLayoutResult.applied) {
4370
4413
  syncManagedYunyiAuthProfiles(paths, config);
4371
4414
  }
4372
- syncExternalTools('claude', baseUrl, apiKey, { claudeModelId: modelId, opencodeDefaultModelKey: `yunyi-claude/${modelId}` });
4415
+ syncExternalTools('claude', baseUrl, apiKey, { claudeModelId: modelId, opencodeDefaultModelKey: getExternalModelKey('claude', modelId) });
4373
4416
  writeSpinner.succeed('配置写入完成');
4374
4417
 
4375
4418
  console.log(chalk.green('\n✅ Claude 节点配置完成!'));
@@ -4621,15 +4664,14 @@ async function autoActivate(paths, args = {}) {
4621
4664
  }
4622
4665
 
4623
4666
  // ---- 选模型(Claude + GPT 合并展示) ----
4624
- const isSingleMode = CLAUDE_MODELS.length === 1 && CODEX_MODELS.length === 1
4625
- && CLAUDE_MODELS[0].id === CODEX_MODELS[0].id;
4667
+ const isSingleMode = isUnifiedSingleModelMode();
4626
4668
 
4627
4669
  let selectedModelId;
4628
4670
  let selectedType; // 'claude' or 'codex'
4629
4671
 
4630
4672
  if (isSingleMode) {
4631
4673
  selectedModelId = CLAUDE_MODELS[0].id;
4632
- selectedType = 'claude';
4674
+ selectedType = String(args.primary || '').trim() === 'codex' ? 'codex' : 'claude';
4633
4675
  } else {
4634
4676
  const modelArg = (args.model || args['claude-model'] || args['codex-model'] || '').toString().trim();
4635
4677
  if (modelArg) {
@@ -4725,9 +4767,9 @@ async function autoActivate(paths, args = {}) {
4725
4767
 
4726
4768
  // 主模型 = 用户选的,备选 = 另一个
4727
4769
  const primaryModelKey = isClaudePrimary ? claudeModelKey : codexModelKey;
4728
- const fallbackModelKey = isClaudePrimary ? codexModelKey : claudeModelKey;
4770
+ const fallbackModelKey = isSingleMode ? '' : (isClaudePrimary ? codexModelKey : claudeModelKey);
4729
4771
  config.agents.defaults.model.primary = primaryModelKey;
4730
- config.agents.defaults.model.fallbacks = [fallbackModelKey];
4772
+ config.agents.defaults.model.fallbacks = fallbackModelKey ? [fallbackModelKey] : [];
4731
4773
  const yunyiLayoutResult = applyManagedYunyiOpenClawLayout(config, {
4732
4774
  force: true,
4733
4775
  endpointUrl: selectedEndpoint.url,
@@ -4755,7 +4797,7 @@ async function autoActivate(paths, args = {}) {
4755
4797
  console.log(chalk.gray(` 已重置 ${selectionResult.agentId} 的活动会话映射`));
4756
4798
  }
4757
4799
  }
4758
- const opencodeDefaultModelKey = isClaudePrimary ? `yunyi-claude/${claudeModelId}` : `yunyi-codex/${codexModelId}`;
4800
+ const opencodeDefaultModelKey = isClaudePrimary ? getExternalModelKey('claude', claudeModelId) : getExternalModelKey('codex', codexModelId);
4759
4801
  try { syncExternalTools('claude', claudeBaseUrl, apiKey, { codexBaseUrl, claudeModelId, opencodeDefaultModelKey }); } catch { /* ignore */ }
4760
4802
  try { syncExternalTools('codex', codexBaseUrl, apiKey, { modelId: codexModelId }); } catch { /* ignore */ }
4761
4803
  writeSpinner.succeed('配置写入完成');
@@ -5205,7 +5247,7 @@ async function yycodeQuickSetup(paths) {
5205
5247
  if (yunyiLayoutResult.applied) {
5206
5248
  syncManagedYunyiAuthProfiles(paths, config);
5207
5249
  }
5208
- try { syncExternalTools('claude', claudeBaseUrl, apiKey, { codexBaseUrl, claudeModelId, opencodeDefaultModelKey: `yunyi-codex/${codexModelId}` }); } catch { /* ignore */ }
5250
+ try { syncExternalTools('claude', claudeBaseUrl, apiKey, { codexBaseUrl, claudeModelId, opencodeDefaultModelKey: getExternalModelKey('codex', codexModelId) }); } catch { /* ignore */ }
5209
5251
  try { syncExternalTools('codex', codexBaseUrl, apiKey, { modelId: codexModelId }); } catch { /* ignore */ }
5210
5252
  writeSpinner.succeed('配置写入完成');
5211
5253
 
@@ -5359,6 +5401,8 @@ function getConfigStatusLine(paths) {
5359
5401
  'claude-yunyi': 'Claude(包月)',
5360
5402
  'yunyi': 'Codex(包月)',
5361
5403
  'heibai': 'MAXAPI(按量)',
5404
+ 'maxapi': 'MAXAPI(按量)',
5405
+ 'maxapi-codex': 'MAXAPI(按量)',
5362
5406
  };
5363
5407
 
5364
5408
  const parts = [];
@@ -5569,6 +5613,8 @@ async function switchModel(paths) {
5569
5613
  'claude-yunyi': '云翼 Claude (包月)',
5570
5614
  'yunyi': '云翼 Codex (包月)',
5571
5615
  'heibai': 'MAXAPI (按量)',
5616
+ 'maxapi': 'MAXAPI (按量)',
5617
+ 'maxapi-codex': 'MAXAPI (按量)',
5572
5618
  };
5573
5619
 
5574
5620
  const claudeProviderName = API_CONFIG.claude.providerName;
@@ -5577,8 +5623,25 @@ async function switchModel(paths) {
5577
5623
  // 预设模型列表
5578
5624
  const choices = [];
5579
5625
  const presetKeys = new Set();
5580
-
5581
- if (CLAUDE_MODELS.length > 0) {
5626
+ const unifiedSingleMode = isUnifiedSingleModelMode();
5627
+ const unifiedModel = unifiedSingleMode ? (CLAUDE_MODELS[0] || CODEX_MODELS[0] || null) : null;
5628
+
5629
+ if (unifiedSingleMode && unifiedModel) {
5630
+ const preferredProviders = [
5631
+ primary.split('/')[0],
5632
+ providers[claudeProviderName] ? claudeProviderName : '',
5633
+ providers[codexProviderName] ? codexProviderName : '',
5634
+ Object.keys(providers)[0] || ''
5635
+ ].filter(Boolean);
5636
+ const unifiedProviderName = preferredProviders[0];
5637
+ const modelKey = `${unifiedProviderName}/${unifiedModel.id}`;
5638
+ const isCurrent = modelKey === primary;
5639
+ choices.push({
5640
+ name: isCurrent ? `${unifiedModel.name} (当前)` : unifiedModel.name,
5641
+ value: modelKey,
5642
+ });
5643
+ presetKeys.add(modelKey);
5644
+ } else if (CLAUDE_MODELS.length > 0) {
5582
5645
  choices.push(new inquirer.Separator(' -- Claude --'));
5583
5646
  for (const m of CLAUDE_MODELS) {
5584
5647
  const pName = providers[claudeProviderName] ? claudeProviderName : Object.keys(providers)[0];
@@ -5592,7 +5655,7 @@ async function switchModel(paths) {
5592
5655
  }
5593
5656
  }
5594
5657
 
5595
- if (CODEX_MODELS.length > 0) {
5658
+ if (!unifiedSingleMode && CODEX_MODELS.length > 0) {
5596
5659
  choices.push(new inquirer.Separator(' -- GPT --'));
5597
5660
  for (const m of CODEX_MODELS) {
5598
5661
  const pName = providers[codexProviderName] ? codexProviderName : Object.keys(providers)[0];
@@ -5610,6 +5673,12 @@ async function switchModel(paths) {
5610
5673
  const otherModels = [];
5611
5674
  for (const [providerName, providerConfig] of Object.entries(providers)) {
5612
5675
  for (const m of (providerConfig.models || [])) {
5676
+ if (unifiedSingleMode
5677
+ && unifiedModel
5678
+ && m.id === unifiedModel.id
5679
+ && (providerName === claudeProviderName || providerName === codexProviderName)) {
5680
+ continue;
5681
+ }
5613
5682
  const modelKey = `${providerName}/${m.id}`;
5614
5683
  if (!presetKeys.has(modelKey)) {
5615
5684
  otherModels.push({ modelKey, name: m.name || m.id, providerName });
@@ -5618,6 +5687,12 @@ async function switchModel(paths) {
5618
5687
  }
5619
5688
  const registeredKeys = Object.keys(config.agents?.defaults?.models || {});
5620
5689
  for (const modelKey of registeredKeys) {
5690
+ if (unifiedSingleMode && unifiedModel) {
5691
+ const [providerName, modelId] = modelKey.split('/');
5692
+ if (modelId === unifiedModel.id && (providerName === claudeProviderName || providerName === codexProviderName)) {
5693
+ continue;
5694
+ }
5695
+ }
5621
5696
  if (presetKeys.has(modelKey) || otherModels.some(o => o.modelKey === modelKey)) continue;
5622
5697
  const [pName, mId] = modelKey.split('/');
5623
5698
  if (!pName || !mId) continue;
@@ -5887,7 +5962,7 @@ async function testConnection(paths, args = {}) {
5887
5962
  const container = await selectDockerContainer();
5888
5963
  if (!container) {
5889
5964
  console.log(chalk.red('❌ 未找到包含 OpenClaw/Clawdbot/Moltbot 的 Docker 容器'));
5890
- console.log(chalk.gray(' 请确保容器正在运行且已安装 openclaw/clawdbot/moltbot'));
5965
+ console.log(chalk.gray(' 请确保容器正在运行且已安装 openclaw'));
5891
5966
  return;
5892
5967
  }
5893
5968
  }
@@ -5927,8 +6002,6 @@ async function testConnection(paths, args = {}) {
5927
6002
  console.log(chalk.gray(` 然后执行: ${_selectedDockerContainer.cli === 'node' ? `node ${_selectedDockerContainer.cliPath}` : _selectedDockerContainer.cli} gateway`));
5928
6003
  } else {
5929
6004
  console.log(chalk.gray(' 请在新的终端执行: openclaw gateway'));
5930
- console.log(chalk.gray(' 或: clawdbot gateway'));
5931
- console.log(chalk.gray(' 或: moltbot gateway'));
5932
6005
  }
5933
6006
  return;
5934
6007
  }
@@ -5938,7 +6011,7 @@ async function testConnection(paths, args = {}) {
5938
6011
  if (!restartOk) {
5939
6012
  console.log(chalk.yellow('⚠️ Gateway 未能通过常规方式重启,当前使用的可能是之前的 Gateway 进程'));
5940
6013
  console.log(chalk.yellow(' 新配置可能未生效。如 bot 不回复,请手动重启 Gateway:'));
5941
- console.log(chalk.gray(' openclaw gateway restart 或 clawdbot gateway restart'));
6014
+ console.log(chalk.gray(' openclaw gateway restart'));
5942
6015
  }
5943
6016
 
5944
6017
  // 步骤2: 通过 Gateway 端点测试(优先使用 CLI agent)
@@ -6063,7 +6136,7 @@ async function testConnection(paths, args = {}) {
6063
6136
  console.log(chalk.gray(`\n 建议操作:`));
6064
6137
  console.log(chalk.gray(` 1) 复制最新地址并重新打开浏览器(不要用旧书签)`));
6065
6138
  console.log(chalk.cyan(` http://127.0.0.1:${gatewayPort}/#token=${gatewayToken}`));
6066
- console.log(chalk.gray(` 2) 执行 Gateway 重启:openclaw gateway restart / clawdbot gateway restart`));
6139
+ console.log(chalk.gray(` 2) 执行 Gateway 重启:openclaw gateway restart`));
6067
6140
  console.log(chalk.gray(` 3) 若仍 401,检查是否存在多个配置目录(.openclaw 与 .clawdbot)`));
6068
6141
  }
6069
6142
 
@@ -6073,7 +6146,7 @@ async function testConnection(paths, args = {}) {
6073
6146
  console.log(chalk.gray(` 如遇问题,尝试更新 Gateway: npm install -g openclaw@latest && openclaw gateway restart`));
6074
6147
  }
6075
6148
 
6076
- console.log(chalk.gray(`\n 提示: 如果 Gateway 未运行,请执行: openclaw gateway / clawdbot gateway / moltbot gateway`));
6149
+ console.log(chalk.gray(`\n 提示: 如果 Gateway 未运行,请执行: openclaw gateway`));
6077
6150
  }
6078
6151
  } catch (error) {
6079
6152
  console.log(chalk.red(`❌ 测试失败: ${error.message}`));
@@ -6117,7 +6190,7 @@ function buildDockerInnerCmds(container, verb) {
6117
6190
  if (container.cliPath) cmds.push(`${container.cliPath} ${verb}`);
6118
6191
  cmds.push(`${container.cli} ${verb}`);
6119
6192
  }
6120
- for (const name of ['openclaw', 'clawdbot', 'moltbot']) {
6193
+ for (const name of GATEWAY_CLI_NAMES) {
6121
6194
  cmds.push(`${name} ${verb}`);
6122
6195
  }
6123
6196
  return [...new Set(cmds)].filter(Boolean);
@@ -6150,7 +6223,7 @@ async function restartGatewayDocker(gatewayPort, silent = false) {
6150
6223
  // 策略 B:杀容器内旧进程 → spawn 启动新 Gateway → 端口探测
6151
6224
  if (!silent) console.log(chalk.gray(' Docker 内常规重启未生效,尝试杀进程后重新启动...'));
6152
6225
  try {
6153
- for (const name of ['openclaw', 'clawdbot', 'moltbot']) {
6226
+ for (const name of GATEWAY_CLI_NAMES) {
6154
6227
  safeExec(dockerCmd(`exec ${cid} sh -c "pkill -f '${name}.*gateway' 2>/dev/null || true"`), { timeout: 5000 });
6155
6228
  }
6156
6229
  safeExec(dockerCmd(`exec ${cid} sh -c "lsof -ti :${gatewayPort} 2>/dev/null | xargs -r kill -9 2>/dev/null || true"`), { timeout: 5000 });
@@ -6174,7 +6247,7 @@ async function restartGatewayDocker(gatewayPort, silent = false) {
6174
6247
  async function restartGatewayWsl(gatewayPort, silent = false) {
6175
6248
  if (!silent) console.log(chalk.gray(' [检测] Gateway 运行在 WSL 中'));
6176
6249
  const wslCli = getWslCliBinary();
6177
- const names = ['openclaw', 'clawdbot', 'moltbot'];
6250
+ const names = GATEWAY_CLI_NAMES;
6178
6251
  const portWasOpenBefore = await isPortOpen(gatewayPort, '127.0.0.1', 500);
6179
6252
  const beforeSignature = portWasOpenBefore ? getGatewayProcessSignature(gatewayPort, 'wsl') : '';
6180
6253
 
@@ -6267,13 +6340,11 @@ async function restartGatewayNative(silent = false) {
6267
6340
  // 策略 C:用 login shell 启动(加载 nvm/fnm 等 PATH)
6268
6341
  if (process.platform !== 'win32') {
6269
6342
  if (!silent) console.log(chalk.gray(' 尝试通过 login shell 启动...'));
6270
- let launched = false;
6271
- for (const name of ['openclaw', 'clawdbot', 'moltbot']) {
6272
- if (launched) break;
6273
- for (const sh of ['/bin/zsh', '/bin/bash']) {
6274
- if (!fs.existsSync(sh)) continue;
6275
- if (spawnDetached(`${sh} -lc '${name} gateway'`, env)) {
6276
- launched = true;
6343
+ for (const sh of ['/bin/zsh', '/bin/bash']) {
6344
+ if (!fs.existsSync(sh)) continue;
6345
+ for (const cmd of startCmds) {
6346
+ const loginShellCmd = buildLoginShellCommand(sh, cmd);
6347
+ if (spawnDetached(loginShellCmd, env)) {
6277
6348
  if (await waitForGateway(gatewayPort, '127.0.0.1', 10000)) {
6278
6349
  if (!silent) console.log(chalk.green('✅ Gateway 已重启 (login shell)'));
6279
6350
  return true;
@@ -6286,10 +6357,8 @@ async function restartGatewayNative(silent = false) {
6286
6357
 
6287
6358
  // 全部失败,输出诊断
6288
6359
  if (!silent) {
6289
- console.log(chalk.red(`❌ 重启失败: 找不到 openclaw/clawdbot/moltbot 命令`));
6360
+ console.log(chalk.red('❌ 重启失败: 找不到 openclaw 命令'));
6290
6361
  console.log(chalk.gray(` 请手动运行: openclaw gateway restart`));
6291
- console.log(chalk.gray(` 或: clawdbot gateway restart`));
6292
- console.log(chalk.gray(` 或: moltbot gateway restart`));
6293
6362
  printGatewayDiagnostics(resolved);
6294
6363
  }
6295
6364
  return false;
@@ -6307,7 +6376,7 @@ function buildGatewayCommands(resolved, nodeInfo, useNode, action) {
6307
6376
  }
6308
6377
  }
6309
6378
 
6310
- const names = ['openclaw', 'clawdbot', 'moltbot'];
6379
+ const names = GATEWAY_CLI_NAMES;
6311
6380
  for (const name of names) commands.push(`${name} ${verb}`);
6312
6381
 
6313
6382
  return [...new Set(commands)].filter(Boolean);
@@ -6324,13 +6393,13 @@ async function killGatewayProcesses(gatewayPort = 18789) {
6324
6393
  }
6325
6394
  }
6326
6395
  if (isWslAvailable()) {
6327
- for (const name of ['openclaw', 'clawdbot', 'moltbot']) {
6396
+ for (const name of GATEWAY_CLI_NAMES) {
6328
6397
  safeExec(`wsl -- bash -c "pkill -f '${name}.*gateway' 2>/dev/null || true"`, { timeout: 5000 });
6329
6398
  }
6330
6399
  safeExec(`wsl -- bash -c "lsof -ti :${gatewayPort} 2>/dev/null | xargs -r kill -9 2>/dev/null || true"`, { timeout: 5000 });
6331
6400
  }
6332
6401
  } else {
6333
- for (const name of ['openclaw', 'clawdbot', 'moltbot']) {
6402
+ for (const name of GATEWAY_CLI_NAMES) {
6334
6403
  safeExec(`pkill -f '${name}.*gateway' 2>/dev/null || true`);
6335
6404
  }
6336
6405
  const lsof = safeExec(`lsof -ti :${gatewayPort} 2>/dev/null`);
@@ -6347,14 +6416,14 @@ function printGatewayDiagnostics(resolved) {
6347
6416
  console.log(chalk.gray(`\n [诊断] resolveCliBinary = ${resolved || 'null'}`));
6348
6417
  const npmPrefix = safeExec('npm prefix -g');
6349
6418
  if (npmPrefix.ok) console.log(chalk.gray(` [诊断] npm prefix -g = ${npmPrefix.output}`));
6350
- for (const name of ['openclaw', 'clawdbot', 'moltbot']) {
6419
+ for (const name of GATEWAY_CLI_NAMES) {
6351
6420
  const which = safeExec(process.platform === 'win32' ? `where ${name} 2>nul` : `/bin/zsh -lc "command -v ${name}" 2>/dev/null || /bin/bash -lc "command -v ${name}" 2>/dev/null`);
6352
6421
  if (which.ok && which.output) console.log(chalk.gray(` [诊断] ${name} -> ${which.output.split('\n')[0].trim()}`));
6353
6422
  }
6354
6423
  if (isDockerAvailable()) {
6355
6424
  const dContainers = findOpenclawDockerContainers();
6356
6425
  if (dContainers.length > 0) {
6357
- console.log(chalk.gray(` [诊断] Docker 容器 (含 openclaw/clawdbot/moltbot):`));
6426
+ console.log(chalk.gray(' [诊断] Docker 容器 (含 openclaw):'));
6358
6427
  for (const c of dContainers) {
6359
6428
  console.log(chalk.gray(` - ${c.name} (${c.image}) [${c.cli}: ${c.cliPath}]`));
6360
6429
  }
@@ -6445,7 +6514,7 @@ function testGatewayApi(port, token, model, endpoint = '/v1/chat/completions') {
6445
6514
 
6446
6515
  req.on('error', (e) => {
6447
6516
  if (e.code === 'ECONNREFUSED') {
6448
- resolve({ success: false, error: 'Gateway 未运行,请先启动: openclaw gateway / clawdbot gateway / moltbot gateway' });
6517
+ resolve({ success: false, error: 'Gateway 未运行,请先启动: openclaw gateway' });
6449
6518
  } else {
6450
6519
  resolve({ success: false, error: e.message });
6451
6520
  }
@@ -6472,14 +6541,14 @@ function testGatewayViaAgent(model, agentId = '') {
6472
6541
  const agentCmd = `${wslCli} ${subcommand}`;
6473
6542
  cmd = `wsl -- bash -c '${agentCmd.replace(/'/g, "'\\''")}'`;
6474
6543
  } else {
6475
- const agentCmd = `openclaw ${subcommand} 2>/dev/null || clawdbot ${subcommand} 2>/dev/null || moltbot ${subcommand}`;
6544
+ const agentCmd = `openclaw ${subcommand}`;
6476
6545
  cmd = `wsl -- bash -lc '${agentCmd.replace(/'/g, "'\\''")}'`;
6477
6546
  }
6478
6547
  execOpts = { timeout: 120000 };
6479
6548
  } else {
6480
6549
  const { cliBinary, nodeMajor } = getCliMeta();
6481
6550
  if (!cliBinary) {
6482
- resolve({ success: false, usedCli: false, error: '未找到 openclaw/clawdbot/moltbot 命令' });
6551
+ resolve({ success: false, usedCli: false, error: '未找到 openclaw 命令' });
6483
6552
  return;
6484
6553
  }
6485
6554
  const nodeInfo = findCompatibleNode(nodeMajor);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "yymaxapi",
3
- "version": "1.0.100",
3
+ "version": "1.0.102",
4
4
  "description": "跨平台 OpenClaw/Clawdbot 配置管理工具 - 管理中转地址、模型切换、API Keys、测速优化",
5
5
  "main": "bin/yymaxapi.js",
6
6
  "bin": {