yymaxapi 1.0.49 → 1.0.51

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 +118 -95
  2. package/package.json +1 -1
package/bin/yymaxapi.js CHANGED
@@ -138,6 +138,8 @@ const DEFAULT_API_CONFIG = {
138
138
  }
139
139
  };
140
140
 
141
+ const SKIP_API_VALIDATION = false;
142
+
141
143
  function normalizeEndpoints(raw, fallback) {
142
144
  if (!Array.isArray(raw)) return fallback;
143
145
  const normalized = raw
@@ -371,6 +373,9 @@ function httpGetJson(url, headers = {}, timeout = 10000) {
371
373
  }
372
374
 
373
375
  async function validateApiKey(nodeUrl, apiKey) {
376
+ if (SKIP_API_VALIDATION) {
377
+ return { valid: true, skipped: true };
378
+ }
374
379
  const verifyUrl = `${nodeUrl.replace(/\/+$/, '')}/user/api/v1/me`;
375
380
  const maxRetries = 3;
376
381
  const spinner = ora({ text: '正在验证 API Key...', spinner: 'dots' }).start();
@@ -1177,8 +1182,23 @@ function coerceModelsRecord(value) {
1177
1182
  return record;
1178
1183
  }
1179
1184
 
1185
+ const OPENCLAW_KNOWN_ROOT_KEYS = new Set([
1186
+ 'models', 'agents', 'auth', 'gateway', 'tools',
1187
+ 'channels', 'mcpServers', 'globalShortcut', 'customModes',
1188
+ 'permissions', 'proxy', 'env', 'hooks', 'storage',
1189
+ ]);
1190
+
1191
+ function sanitizeRootKeys(config) {
1192
+ for (const key of Object.keys(config)) {
1193
+ if (!OPENCLAW_KNOWN_ROOT_KEYS.has(key)) {
1194
+ delete config[key];
1195
+ }
1196
+ }
1197
+ }
1198
+
1180
1199
  function ensureConfigStructure(config) {
1181
1200
  const next = config || {};
1201
+ sanitizeRootKeys(next);
1182
1202
  if (!next.models) next.models = {};
1183
1203
  if (!next.models.providers) next.models.providers = {};
1184
1204
  if (!next.agents) next.agents = {};
@@ -2661,25 +2681,54 @@ async function autoActivate(paths, args = {}) {
2661
2681
  // ---- 构建配置 ----
2662
2682
  const config = ensureConfigStructure(readConfig(paths.openclawConfig) || {});
2663
2683
 
2664
- // 增量写入: 只添加/更新云翼 provider,保留其他已有配置
2665
-
2666
- // Claude 侧 — 让用户选模型
2667
2684
  const claudeBaseUrl = buildFullUrl(selectedEndpoint.url, 'claude');
2668
- let claudeModelId = (args['claude-model'] || '').toString().trim();
2669
- if (!claudeModelId && CLAUDE_MODELS.length > 1) {
2670
- const { picked } = await inquirer.prompt([{
2671
- type: 'list',
2672
- name: 'picked',
2673
- message: '选择 Claude 模型:',
2674
- choices: CLAUDE_MODELS.map(m => ({ name: m.name, value: m.id })),
2675
- default: CLAUDE_MODELS[0].id
2676
- }]);
2677
- claudeModelId = picked;
2685
+ const codexBaseUrl = buildFullUrl(selectedEndpoint.url, 'codex');
2686
+
2687
+ // 检测单一模式:Claude Codex 只有一个模型且 id 相同(如 maxapi 的 heibai)
2688
+ const isSingleMode = CLAUDE_MODELS.length === 1 && CODEX_MODELS.length === 1
2689
+ && CLAUDE_MODELS[0].id === CODEX_MODELS[0].id;
2690
+
2691
+ let claudeModelId, codexModelId;
2692
+
2693
+ if (isSingleMode) {
2694
+ // 单一模式:固定模型,不选择,不分 Claude/Codex
2695
+ claudeModelId = CLAUDE_MODELS[0].id;
2696
+ codexModelId = CODEX_MODELS[0].id;
2697
+ } else {
2698
+ // 多模型模式:让用户选模型
2699
+ claudeModelId = (args['claude-model'] || '').toString().trim();
2700
+ if (!claudeModelId && CLAUDE_MODELS.length > 1) {
2701
+ const { picked } = await inquirer.prompt([{
2702
+ type: 'list',
2703
+ name: 'picked',
2704
+ message: '选择 Claude 模型:',
2705
+ choices: CLAUDE_MODELS.map(m => ({ name: m.name, value: m.id })),
2706
+ default: CLAUDE_MODELS[0].id
2707
+ }]);
2708
+ claudeModelId = picked;
2709
+ }
2710
+ if (!claudeModelId) claudeModelId = CLAUDE_MODELS[0]?.id || 'claude-sonnet-4-6';
2711
+
2712
+ codexModelId = (args['codex-model'] || '').toString().trim();
2713
+ if (!codexModelId && CODEX_MODELS.length > 1) {
2714
+ const { pickedCodex } = await inquirer.prompt([{
2715
+ type: 'list',
2716
+ name: 'pickedCodex',
2717
+ message: '选择 Codex 模型:',
2718
+ choices: CODEX_MODELS.map(m => ({ name: m.name, value: m.id })),
2719
+ default: CODEX_MODELS[0].id
2720
+ }]);
2721
+ codexModelId = pickedCodex;
2722
+ }
2723
+ if (!codexModelId) codexModelId = CODEX_MODELS[0]?.id || 'gpt-5.3-codex';
2678
2724
  }
2679
- if (!claudeModelId) claudeModelId = CLAUDE_MODELS[0]?.id || 'claude-sonnet-4-6';
2725
+
2680
2726
  const claudeModel = CLAUDE_MODELS.find(m => m.id === claudeModelId) || { id: claudeModelId, name: claudeModelId };
2727
+ const codexModel = CODEX_MODELS.find(m => m.id === codexModelId) || { id: codexModelId, name: codexModelId };
2681
2728
  const claudeModelKey = `${claudeProviderName}/${claudeModelId}`;
2729
+ const codexModelKey = `${codexProviderName}/${codexModelId}`;
2682
2730
 
2731
+ // 写入 Claude provider
2683
2732
  config.models.providers[claudeProviderName] = {
2684
2733
  baseUrl: claudeBaseUrl,
2685
2734
  auth: DEFAULT_AUTH_MODE,
@@ -2697,23 +2746,7 @@ async function autoActivate(paths, args = {}) {
2697
2746
  config.auth.profiles[`${claudeProviderName}:default`] = { provider: claudeProviderName, mode: 'api_key' };
2698
2747
  config.agents.defaults.models[claudeModelKey] = { alias: claudeProviderName };
2699
2748
 
2700
- // Codex 侧 — 让用户选模型
2701
- const codexBaseUrl = buildFullUrl(selectedEndpoint.url, 'codex');
2702
- let codexModelId = (args['codex-model'] || '').toString().trim();
2703
- if (!codexModelId && CODEX_MODELS.length > 1) {
2704
- const { pickedCodex } = await inquirer.prompt([{
2705
- type: 'list',
2706
- name: 'pickedCodex',
2707
- message: '选择 Codex 模型:',
2708
- choices: CODEX_MODELS.map(m => ({ name: m.name, value: m.id })),
2709
- default: CODEX_MODELS[0].id
2710
- }]);
2711
- codexModelId = pickedCodex;
2712
- }
2713
- if (!codexModelId) codexModelId = CODEX_MODELS[0]?.id || 'gpt-5.3-codex';
2714
- const codexModel = CODEX_MODELS.find(m => m.id === codexModelId) || { id: codexModelId, name: codexModelId };
2715
- const codexModelKey = `${codexProviderName}/${codexModelId}`;
2716
-
2749
+ // 写入 Codex provider(单一模式下仍写入,保证配置完整)
2717
2750
  config.models.providers[codexProviderName] = {
2718
2751
  baseUrl: codexBaseUrl,
2719
2752
  auth: DEFAULT_AUTH_MODE,
@@ -2732,27 +2765,34 @@ async function autoActivate(paths, args = {}) {
2732
2765
  config.agents.defaults.models[codexModelKey] = { alias: codexProviderName };
2733
2766
 
2734
2767
  // ---- 选择主力 ----
2735
- let primaryType = 'claude';
2736
- if (args.primary === 'claude') {
2737
- primaryType = 'claude';
2738
- } else if (args.primary === 'codex') {
2739
- primaryType = 'codex';
2768
+ let primaryModelKey, fallbackModelKey;
2769
+ if (isSingleMode) {
2770
+ // 单一模式:直接用 Claude provider 作为 primary,不需要用户选
2771
+ primaryModelKey = claudeModelKey;
2772
+ fallbackModelKey = codexModelKey;
2740
2773
  } else {
2741
- const { picked } = await inquirer.prompt([{
2742
- type: 'list',
2743
- name: 'picked',
2744
- message: '选择主力工具(默认启动哪个):',
2745
- choices: [
2746
- { name: 'Claude Code', value: 'claude' },
2747
- { name: 'Codex (GPT)', value: 'codex' }
2748
- ]
2749
- }]);
2750
- primaryType = picked;
2774
+ let primaryType = 'claude';
2775
+ if (args.primary === 'claude') {
2776
+ primaryType = 'claude';
2777
+ } else if (args.primary === 'codex') {
2778
+ primaryType = 'codex';
2779
+ } else {
2780
+ const { picked } = await inquirer.prompt([{
2781
+ type: 'list',
2782
+ name: 'picked',
2783
+ message: '选择主力工具(默认启动哪个):',
2784
+ choices: [
2785
+ { name: 'Claude Code', value: 'claude' },
2786
+ { name: 'Codex (GPT)', value: 'codex' }
2787
+ ]
2788
+ }]);
2789
+ primaryType = picked;
2790
+ }
2791
+ primaryModelKey = primaryType === 'claude' ? claudeModelKey : codexModelKey;
2792
+ fallbackModelKey = primaryType === 'claude' ? codexModelKey : claudeModelKey;
2751
2793
  }
2752
2794
 
2753
- const primaryModelKey = primaryType === 'claude' ? claudeModelKey : codexModelKey;
2754
2795
  config.agents.defaults.model.primary = primaryModelKey;
2755
- const fallbackModelKey = primaryType === 'claude' ? codexModelKey : claudeModelKey;
2756
2796
  config.agents.defaults.model.fallbacks = [fallbackModelKey];
2757
2797
 
2758
2798
  // ---- 写入配置 ----
@@ -2768,13 +2808,19 @@ async function autoActivate(paths, args = {}) {
2768
2808
  writeSpinner.succeed('配置写入完成');
2769
2809
 
2770
2810
  // ---- 输出结果 ----
2771
- const primaryTag = primaryType === 'claude' ? ' (主)' : '';
2772
- const codexTag = primaryType === 'codex' ? ' (主)' : '';
2773
2811
  console.log(chalk.green('\n✅ 一键激活完成!'));
2774
- console.log(chalk.cyan(` Claude Code${primaryTag}: ${claudeBaseUrl}`));
2775
- console.log(chalk.gray(` 模型: ${claudeModel.name}`));
2776
- console.log(chalk.cyan(` Codex${codexTag}: ${codexBaseUrl}`));
2777
- console.log(chalk.gray(` 模型: ${codexModel.name}`));
2812
+ if (isSingleMode) {
2813
+ console.log(chalk.cyan(` 节点: ${selectedEndpoint.url}`));
2814
+ console.log(chalk.gray(` 模型: ${claudeModel.name}`));
2815
+ } else {
2816
+ const primaryType = primaryModelKey === claudeModelKey ? 'claude' : 'codex';
2817
+ const primaryTag = primaryType === 'claude' ? ' (主)' : '';
2818
+ const codexTag = primaryType === 'codex' ? ' (主)' : '';
2819
+ console.log(chalk.cyan(` Claude Code${primaryTag}: ${claudeBaseUrl}`));
2820
+ console.log(chalk.gray(` 模型: ${claudeModel.name}`));
2821
+ console.log(chalk.cyan(` Codex${codexTag}: ${codexBaseUrl}`));
2822
+ console.log(chalk.gray(` 模型: ${codexModel.name}`));
2823
+ }
2778
2824
  console.log(chalk.gray(' API Key: 已设置'));
2779
2825
  if (extSynced.length > 0) console.log(chalk.gray(` 同步: ${extSynced.join(', ')}`));
2780
2826
 
@@ -2782,7 +2828,7 @@ async function autoActivate(paths, args = {}) {
2782
2828
  const gwToken = config.gateway?.auth?.token;
2783
2829
  if (gwToken) {
2784
2830
  console.log(chalk.green(`\n🌐 Web Dashboard:`));
2785
- console.log(chalk.cyan(` http://localhost:${gwPort}/?token=${gwToken}`));
2831
+ console.log(chalk.cyan(` http://127.0.0.1:${gwPort}/?token=${gwToken}`));
2786
2832
  }
2787
2833
 
2788
2834
  // ---- 测试连接 ----
@@ -3567,7 +3613,7 @@ async function switchModel(paths) {
3567
3613
 
3568
3614
  if (gwToken) {
3569
3615
  console.log(chalk.green(`\n🌐 Web Dashboard:`));
3570
- console.log(chalk.cyan(` http://localhost:${gwPort}/?token=${gwToken}`));
3616
+ console.log(chalk.cyan(` http://127.0.0.1:${gwPort}/?token=${gwToken}`));
3571
3617
  }
3572
3618
  }
3573
3619
  // ============ 权限管理 (tools.profile) ============
@@ -3658,8 +3704,7 @@ async function testConnection(paths, args = {}) {
3658
3704
  console.log(chalk.yellow('⚠️ 请先设置主模型'));
3659
3705
  return;
3660
3706
  }
3661
- // 优先选云翼/maxapi provider
3662
- const preferOrder = ['claude-yunyi', 'yunyi', 'heibai'];
3707
+ const preferOrder = ['maxapi', 'heibai', 'claude-yunyi', 'yunyi', 'maxapi-codex'];
3663
3708
  const preferred = preferOrder.find(n => providerNames.includes(n));
3664
3709
  const firstP = preferred || providerNames.find(n => n.includes('claude')) || providerNames[0];
3665
3710
  const firstModels = providers[firstP]?.models || [];
@@ -3695,7 +3740,7 @@ async function testConnection(paths, args = {}) {
3695
3740
  console.log(chalk.gray(`当前激活: ${typeLabel}`));
3696
3741
  console.log(chalk.gray(`中转节点: ${provider.baseUrl}`));
3697
3742
  console.log(chalk.gray(`模型: ${primary}`));
3698
- console.log(chalk.gray(`Gateway: http://localhost:${gatewayPort}\n`));
3743
+ console.log(chalk.gray(`Gateway: http://127.0.0.1:${gatewayPort}\n`));
3699
3744
  // 获取 Gateway token
3700
3745
  const gatewayToken = config.gateway?.auth?.token;
3701
3746
  if (!gatewayToken) {
@@ -3819,7 +3864,7 @@ async function testConnection(paths, args = {}) {
3819
3864
  }
3820
3865
  }
3821
3866
 
3822
- console.log(chalk.gray(` 端点: http://localhost:${gatewayPort}/v1/responses`));
3867
+ console.log(chalk.gray(` 端点: http://127.0.0.1:${gatewayPort}/v1/responses`));
3823
3868
  const startTime = Date.now();
3824
3869
  let result = await testGatewayApi(gatewayPort, gatewayToken, primary);
3825
3870
  const latency = Date.now() - startTime;
@@ -4017,15 +4062,11 @@ async function restartGatewayNative(silent = false) {
4017
4062
  const gwConfig = readConfig(configPaths.openclawConfig);
4018
4063
  const gatewayPort = gwConfig?.gateway?.port || 18789;
4019
4064
 
4020
- // 策略 A:先尝试 exec "gateway restart"(给 8s 超时——足够杀旧进程)
4021
- // 注意:该命令通常会启动前台 daemon 不退出,超时是正常的
4065
+ // 策略 A:尝试 exec "gateway restart"(短超时),然后端口探测
4022
4066
  const restartCmds = buildGatewayCommands(resolved, nodeInfo, useNode, 'restart');
4023
-
4024
- let execAttempted = false;
4025
- for (const cmd of restartCmds) {
4026
- execAttempted = true;
4027
- const result = safeExec(cmd, { timeout: 8000, env });
4028
- // 不管是否 "成功"(通常会超时),检查端口是否可达
4067
+ if (restartCmds.length > 0) {
4068
+ // 只尝试第一条(最精确的路径),避免逐条超时累积
4069
+ safeExec(restartCmds[0], { timeout: 8000, env });
4029
4070
  if (await waitForGateway(gatewayPort, '127.0.0.1', 5000)) {
4030
4071
  if (!silent) console.log(chalk.green(`✅ Gateway 已重启`));
4031
4072
  return true;
@@ -4035,29 +4076,34 @@ async function restartGatewayNative(silent = false) {
4035
4076
  // 策略 B:杀旧进程 → spawn 后台启动新 Gateway → 端口探测
4036
4077
  if (!silent) console.log(chalk.gray(' 常规重启未生效,尝试杀进程后重新启动...'));
4037
4078
  await killGatewayProcesses(gatewayPort);
4038
- await new Promise(resolve => setTimeout(resolve, 1500));
4079
+ await new Promise(resolve => setTimeout(resolve, 1000));
4039
4080
 
4040
4081
  const startCmds = buildGatewayCommands(resolved, nodeInfo, useNode, 'start');
4041
4082
  for (const cmd of startCmds) {
4042
4083
  if (spawnDetached(cmd, env)) {
4043
- if (await waitForGateway(gatewayPort, '127.0.0.1', 12000)) {
4084
+ if (await waitForGateway(gatewayPort, '127.0.0.1', 10000)) {
4044
4085
  if (!silent) console.log(chalk.green('✅ Gateway 已重启'));
4045
4086
  return true;
4046
4087
  }
4088
+ break; // spawn 成功但端口未通,不再逐条重试
4047
4089
  }
4048
4090
  }
4049
4091
 
4050
4092
  // 策略 C:用 login shell 启动(加载 nvm/fnm 等 PATH)
4051
4093
  if (process.platform !== 'win32') {
4052
4094
  if (!silent) console.log(chalk.gray(' 尝试通过 login shell 启动...'));
4095
+ let launched = false;
4053
4096
  for (const name of ['openclaw', 'clawdbot', 'moltbot']) {
4097
+ if (launched) break;
4054
4098
  for (const sh of ['/bin/zsh', '/bin/bash']) {
4055
4099
  if (!fs.existsSync(sh)) continue;
4056
4100
  if (spawnDetached(`${sh} -lc '${name} gateway'`, env)) {
4057
- if (await waitForGateway(gatewayPort, '127.0.0.1', 12000)) {
4101
+ launched = true;
4102
+ if (await waitForGateway(gatewayPort, '127.0.0.1', 10000)) {
4058
4103
  if (!silent) console.log(chalk.green('✅ Gateway 已重启 (login shell)'));
4059
4104
  return true;
4060
4105
  }
4106
+ break;
4061
4107
  }
4062
4108
  }
4063
4109
  }
@@ -4087,30 +4133,7 @@ function buildGatewayCommands(resolved, nodeInfo, useNode, action) {
4087
4133
  }
4088
4134
 
4089
4135
  const names = ['openclaw', 'clawdbot', 'moltbot'];
4090
- if (process.platform === 'win32') {
4091
- for (const name of names) commands.push(`${name} ${verb}`);
4092
- if (isWslAvailable()) {
4093
- const wslCli = getWslCliBinary();
4094
- if (wslCli) commands.push(`wsl -- bash -lc "${wslCli} ${verb}"`);
4095
- for (const name of names) commands.push(`wsl -- bash -lc "${name} ${verb}"`);
4096
- }
4097
- } else {
4098
- for (const name of names) commands.push(`${name} ${verb}`);
4099
- }
4100
-
4101
- if (isDockerAvailable()) {
4102
- const containers = findOpenclawDockerContainers();
4103
- for (const c of containers) {
4104
- if (c.cli === 'node') {
4105
- commands.push(dockerCmd(`exec ${c.id} bash -lc "node ${c.cliPath} ${verb}"`));
4106
- } else if (c.cli !== 'unknown') {
4107
- commands.push(dockerCmd(`exec ${c.id} bash -lc "${c.cli} ${verb}"`));
4108
- }
4109
- for (const name of names) {
4110
- commands.push(dockerCmd(`exec ${c.id} bash -lc "${name} ${verb}"`));
4111
- }
4112
- }
4113
- }
4136
+ for (const name of names) commands.push(`${name} ${verb}`);
4114
4137
 
4115
4138
  return [...new Set(commands)].filter(Boolean);
4116
4139
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "yymaxapi",
3
- "version": "1.0.49",
3
+ "version": "1.0.51",
4
4
  "description": "跨平台 OpenClaw/Clawdbot 配置管理工具 - 管理中转地址、模型切换、API Keys、测速优化",
5
5
  "main": "bin/yymaxapi.js",
6
6
  "bin": {