yymaxapi 1.0.111 → 1.0.116

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -16,6 +16,7 @@ npx yymaxapi@latest
16
16
  npx yymaxapi@latest speed-test
17
17
  ```
18
18
  或在具体配置命令后追加 `--speed-test`,按测速结果手动选点。
19
+ 如需直接切换到其他云翼域名,可在预设/一键激活命令后追加 `--endpoint-url https://yunyi.cfd`,也兼容 `--base-url https://你的域名/claude` 这种写法。
19
20
 
20
21
  **方式二:一键配置 Claude**
21
22
  ```bash
package/bin/yymaxapi.js CHANGED
@@ -73,19 +73,11 @@ if (!process.__yymaxapiWarningFilterInstalled && typeof process.emitWarning ===
73
73
  // ============ 预设 (由 build.js 从 provider config 生成) ============
74
74
  const DEFAULT_ENDPOINTS = [
75
75
  {
76
- "name": "国内主节点",
77
- "url": "https://yunyi.rdzhvip.com"
78
- },
79
- {
80
- "name": "CF国外节点1",
76
+ "name": "默认主节点",
81
77
  "url": "https://yunyi.cfd"
82
78
  },
83
79
  {
84
- "name": "CF国外节点2",
85
- "url": "https://cdn1.yunyi.cfd"
86
- },
87
- {
88
- "name": "CF国外节点3",
80
+ "name": "CF国外节点1",
89
81
  "url": "https://cdn2.yunyi.cfd"
90
82
  }
91
83
  ];
@@ -616,9 +608,73 @@ function shouldRunEndpointSpeedTest(args = {}) {
616
608
  return !!raw;
617
609
  }
618
610
 
611
+ function getEndpointOverrideInput(args = {}) {
612
+ const raw = args['endpoint-url']
613
+ ?? args.endpointUrl
614
+ ?? args.endpoint
615
+ ?? args.domain
616
+ ?? args['base-url']
617
+ ?? args.baseUrl
618
+ ?? '';
619
+ return String(raw || '').trim();
620
+ }
621
+
622
+ function normalizeEndpointBaseUrl(baseUrl) {
623
+ let trimmed = String(baseUrl || '').trim();
624
+ if (!trimmed) return '';
625
+
626
+ trimmed = trimClaudeMessagesSuffix(trimmed);
627
+ trimmed = trimOpenAiEndpointSuffix(trimmed);
628
+ trimmed = stripManagedYunyiSuffix(trimmed);
629
+
630
+ try {
631
+ const urlObj = new URL(trimmed);
632
+ urlObj.search = '';
633
+ urlObj.hash = '';
634
+ const normalizedPath = urlObj.pathname.replace(/\/+$/, '');
635
+ return `${urlObj.origin}${normalizedPath && normalizedPath !== '/' ? normalizedPath : ''}`;
636
+ } catch {
637
+ return trimmed.replace(/\/+$/, '');
638
+ }
639
+ }
640
+
641
+ function resolveEndpointOverride(args = {}) {
642
+ const raw = getEndpointOverrideInput(args);
643
+ if (!raw) return null;
644
+
645
+ const normalized = normalizeEndpointBaseUrl(raw);
646
+ if (!normalized || !isValidUrl(normalized)) {
647
+ throw new Error('自定义节点 URL 无效,请使用 http(s):// 域名,支持填写根域名、/claude 或 /codex 地址');
648
+ }
649
+
650
+ return { name: '自定义节点', url: normalized };
651
+ }
652
+
653
+ async function promptCustomEndpointSelection(args = {}, options = {}) {
654
+ const defaultInput = getEndpointOverrideInput(args);
655
+ const promptMessage = options.customEndpointMessage || '请输入节点域名或 Base URL(支持根域名、/claude、/codex):';
656
+ const { customEndpointInput } = await inquirer.prompt([{
657
+ type: 'input',
658
+ name: 'customEndpointInput',
659
+ message: promptMessage,
660
+ default: defaultInput,
661
+ validate: input => {
662
+ const normalized = normalizeEndpointBaseUrl(input);
663
+ return Boolean(normalized && isValidUrl(normalized)) || '请输入有效的 URL(http:// 或 https://)';
664
+ }
665
+ }]);
666
+
667
+ return {
668
+ name: options.customEndpointName || '自定义节点',
669
+ url: normalizeEndpointBaseUrl(customEndpointInput)
670
+ };
671
+ }
672
+
619
673
  async function resolveEndpointSelection(args = {}, options = {}) {
620
674
  const defaultEndpoint = options.defaultEndpoint || ENDPOINTS[0];
621
675
  if (!defaultEndpoint) return null;
676
+ const directOverride = resolveEndpointOverride(args);
677
+ if (directOverride) return directOverride;
622
678
  if (!shouldRunEndpointSpeedTest(args)) return defaultEndpoint;
623
679
 
624
680
  const speedIntro = options.speedIntro || '📡 开始测速节点...\n';
@@ -636,6 +692,7 @@ async function resolveEndpointSelection(args = {}, options = {}) {
636
692
  message: chooseMessage,
637
693
  choices: [
638
694
  { name: `* 使用默认节点 (${defaultEndpoint.name})`, value: -1 },
695
+ { name: '* 手动输入节点域名', value: '__custom__' },
639
696
  new inquirer.Separator(' ---- 或按测速结果选择 ----'),
640
697
  ...sorted.map((endpoint, index) => ({
641
698
  name: `${endpoint.name} - ${endpoint.latency}ms (评分:${endpoint.score})`,
@@ -644,7 +701,9 @@ async function resolveEndpointSelection(args = {}, options = {}) {
644
701
  ]
645
702
  }]);
646
703
 
647
- const selectedEndpoint = selectedIndex === -1 ? defaultEndpoint : sorted[selectedIndex];
704
+ const selectedEndpoint = selectedIndex === '__custom__'
705
+ ? await promptCustomEndpointSelection(args, options)
706
+ : (selectedIndex === -1 ? defaultEndpoint : sorted[selectedIndex]);
648
707
  if (speedResult.usedFallback) {
649
708
  console.log(chalk.yellow('\n⚠ 当前使用备用节点\n'));
650
709
  }
@@ -652,13 +711,21 @@ async function resolveEndpointSelection(args = {}, options = {}) {
652
711
  }
653
712
 
654
713
  console.log(chalk.red('\n⚠️ 所有节点(含备用)均不可达'));
655
- const { proceed } = await inquirer.prompt([{
656
- type: 'confirm',
657
- name: 'proceed',
658
- message: proceedMessage,
659
- default: false
714
+ const proceedChoiceLabel = options.proceedChoiceLabel || `${proceedMessage.replace(/[??]\s*$/, '')} (${defaultEndpoint.name})`;
715
+ const { nextAction } = await inquirer.prompt([{
716
+ type: 'list',
717
+ name: 'nextAction',
718
+ message: '测速未找到可用节点,接下来怎么做?',
719
+ choices: [
720
+ { name: proceedChoiceLabel, value: 'default' },
721
+ { name: '手动输入节点域名', value: 'custom' },
722
+ { name: '取消', value: 'cancel' }
723
+ ]
660
724
  }]);
661
- if (!proceed) {
725
+ if (nextAction === 'custom') {
726
+ return promptCustomEndpointSelection(args, options);
727
+ }
728
+ if (nextAction !== 'default') {
662
729
  console.log(chalk.gray('已取消'));
663
730
  return null;
664
731
  }
@@ -1046,12 +1113,12 @@ function writeClaudeCodeSettings(baseUrl, apiKey, modelId = getDefaultClaudeMode
1046
1113
  settings.apiBaseUrl = normalizedBaseUrl;
1047
1114
  settings.model = normalizedModelId;
1048
1115
  if (!settings.env) settings.env = {};
1049
- settings.env.ANTHROPIC_API_KEY = apiKey;
1116
+ settings.env.ANTHROPIC_AUTH_TOKEN = apiKey;
1050
1117
  settings.env.ANTHROPIC_BASE_URL = normalizedBaseUrl;
1051
1118
  settings.env.ANTHROPIC_DEFAULT_OPUS_MODEL = pinnedOpusModel;
1052
1119
  settings.env.ANTHROPIC_DEFAULT_SONNET_MODEL = pinnedSonnetModel;
1053
1120
  settings.env.ANTHROPIC_DEFAULT_HAIKU_MODEL = pinnedHaikuModel;
1054
- delete settings.env.ANTHROPIC_AUTH_TOKEN;
1121
+ delete settings.env.ANTHROPIC_API_KEY;
1055
1122
  delete settings.availableModels;
1056
1123
  delete settings.env.ANTHROPIC_MODEL;
1057
1124
  if (!fs.existsSync(claudeDir)) fs.mkdirSync(claudeDir, { recursive: true });
@@ -2671,17 +2738,17 @@ function hasManagedYunyiFootprint(config) {
2671
2738
  }
2672
2739
 
2673
2740
  function inferManagedYunyiEndpointUrl(config, explicitEndpointUrl = '') {
2674
- const direct = stripManagedYunyiSuffix(explicitEndpointUrl);
2675
- if (direct && isLikelyYunyiBaseUrl(direct)) return direct;
2741
+ const direct = normalizeEndpointBaseUrl(explicitEndpointUrl);
2742
+ if (direct && isValidUrl(direct)) return direct;
2676
2743
 
2677
2744
  const providers = config?.models?.providers || {};
2678
2745
  for (const [name, provider] of Object.entries(providers)) {
2679
2746
  if (!isYunyiProviderEntry(name, provider, 'claude') && !isYunyiProviderEntry(name, provider, 'codex')) continue;
2680
- const candidate = stripManagedYunyiSuffix(provider?.baseUrl || provider?.base_url || '');
2681
- if (candidate && isLikelyYunyiBaseUrl(candidate)) return candidate;
2747
+ const candidate = normalizeEndpointBaseUrl(provider?.baseUrl || provider?.base_url || '');
2748
+ if (candidate && isValidUrl(candidate)) return candidate;
2682
2749
  }
2683
2750
 
2684
- return ENDPOINTS.find(item => isLikelyYunyiBaseUrl(item.url))?.url || ENDPOINTS[0]?.url || '';
2751
+ return ENDPOINTS.find(item => isValidUrl(item.url))?.url || ENDPOINTS[0]?.url || '';
2685
2752
  }
2686
2753
 
2687
2754
  function inferManagedYunyiApiKey(config, explicitApiKey = '') {
@@ -3450,6 +3517,10 @@ function shellQuote(value) {
3450
3517
  return `"${str.replace(/(["\\$`])/g, '\\$1')}"`;
3451
3518
  }
3452
3519
 
3520
+ function getShellNullInputRedirect() {
3521
+ return process.platform === 'win32' ? ' < NUL' : ' < /dev/null';
3522
+ }
3523
+
3453
3524
  function escapeSingleQuotedShell(value) {
3454
3525
  return String(value || '').replace(/'/g, "'\\''");
3455
3526
  }
@@ -3931,18 +4002,17 @@ function readClaudeCodeCliConfig() {
3931
4002
  const settings = readJsonIfExists(settingsPath) || {};
3932
4003
  const settingsEnv = settings.env && typeof settings.env === 'object' ? settings.env : {};
3933
4004
  const settingsApiKey = String(settingsEnv.ANTHROPIC_API_KEY || '').trim();
3934
- const settingsLegacyAuthToken = String(settingsEnv.ANTHROPIC_AUTH_TOKEN || '').trim();
4005
+ const settingsAuthToken = String(settingsEnv.ANTHROPIC_AUTH_TOKEN || '').trim();
3935
4006
  const settingsBaseUrl = String(settingsEnv.ANTHROPIC_BASE_URL || settings.apiBaseUrl || '').trim();
3936
4007
  return {
3937
4008
  settingsPath,
3938
4009
  modelId: settings.model || settingsEnv.ANTHROPIC_MODEL || process.env.ANTHROPIC_MODEL || getDefaultClaudeModel().id,
3939
4010
  baseUrl: settingsBaseUrl || process.env.ANTHROPIC_BASE_URL || '',
3940
- apiKey: settingsApiKey || settingsLegacyAuthToken || process.env.ANTHROPIC_API_KEY || process.env.ANTHROPIC_AUTH_TOKEN || process.env.CLAUDE_API_KEY || '',
4011
+ apiKey: settingsAuthToken || settingsApiKey || process.env.ANTHROPIC_AUTH_TOKEN || process.env.ANTHROPIC_API_KEY || process.env.CLAUDE_API_KEY || '',
3941
4012
  settingsBaseUrl,
3942
4013
  settingsApiKey,
3943
- settingsLegacyAuthToken,
3944
- hasLegacyAuthTokenInSettings: !!settingsLegacyAuthToken,
3945
- usesLegacyAuthTokenOnly: !settingsApiKey && !!settingsLegacyAuthToken,
4014
+ settingsAuthToken,
4015
+ settingsAuthMode: settingsAuthToken ? 'auth_token' : (settingsApiKey ? 'api_key' : ''),
3946
4016
  configured: fs.existsSync(settingsPath)
3947
4017
  };
3948
4018
  }
@@ -4022,6 +4092,41 @@ function buildReadableAgentError(text, fallback = '') {
4022
4092
  return (preferred || lines[0] || fallback).slice(0, 300);
4023
4093
  }
4024
4094
 
4095
+ function scoreCliErrorDetail(text) {
4096
+ const lower = String(text || '').toLowerCase();
4097
+ if (!lower) return -1;
4098
+ let score = 0;
4099
+ if ([
4100
+ 'forbidden',
4101
+ 'unauthorized',
4102
+ 'permission to access',
4103
+ 'authentication_error',
4104
+ 'invalid bearer token',
4105
+ 'invalid api key',
4106
+ 'all models failed',
4107
+ 'auth issue',
4108
+ 'auth_permanent',
4109
+ 'unexpected status',
4110
+ 'gateway agent failed'
4111
+ ].some(pattern => lower.includes(pattern))) {
4112
+ score += 5;
4113
+ }
4114
+ if ([
4115
+ 'failed',
4116
+ 'error:',
4117
+ 'missing environment variable'
4118
+ ].some(pattern => lower.includes(pattern))) {
4119
+ score += 2;
4120
+ }
4121
+ if ([
4122
+ 'reading additional input from stdin',
4123
+ 'reconnecting'
4124
+ ].some(pattern => lower.includes(pattern))) {
4125
+ score -= 2;
4126
+ }
4127
+ return score;
4128
+ }
4129
+
4025
4130
  function looksLikeCliTestError(text) {
4026
4131
  const lower = cleanCliTestOutput(text).toLowerCase();
4027
4132
  if (!lower) return false;
@@ -4044,13 +4149,19 @@ function looksLikeCliTestError(text) {
4044
4149
 
4045
4150
  function runCliTestCandidates(name, commands, env) {
4046
4151
  let lastError = '命令执行失败';
4152
+ let lastErrorScore = scoreCliErrorDetail(lastError);
4047
4153
  for (const command of commands) {
4048
4154
  const result = safeExec(command, { timeout: 120000, env, maxBuffer: 1024 * 1024 });
4049
4155
  const combined = cleanCliTestOutput(`${result.output || ''}\n${result.stdout || ''}\n${result.stderr || ''}`);
4050
4156
  if (result.ok && !looksLikeCliTestError(combined)) {
4051
4157
  return { name, status: 'success', detail: summarizeCliTestOutput(combined) || '连接成功' };
4052
4158
  }
4053
- lastError = summarizeCliTestOutput(combined) || result.error || lastError;
4159
+ const candidateError = buildReadableAgentError(combined, result.error || lastError);
4160
+ const candidateScore = scoreCliErrorDetail(candidateError);
4161
+ if (candidateScore >= lastErrorScore) {
4162
+ lastError = candidateError;
4163
+ lastErrorScore = candidateScore;
4164
+ }
4054
4165
  }
4055
4166
  return { name, status: 'failed', detail: lastError };
4056
4167
  }
@@ -4061,15 +4172,8 @@ function testClaudeCodeCliConnection() {
4061
4172
 
4062
4173
  const config = readClaudeCodeCliConfig();
4063
4174
  if (!config.configured) return { name: 'Claude Code CLI', status: 'skipped', detail: '未检测到 ~/.claude/settings.json' };
4064
- if (config.usesLegacyAuthTokenOnly) {
4065
- return {
4066
- name: 'Claude Code CLI',
4067
- status: 'failed',
4068
- detail: '检测到旧版 Claude Code 配置字段 ANTHROPIC_AUTH_TOKEN,请重新运行 yymaxapi 以改写为 ANTHROPIC_API_KEY'
4069
- };
4070
- }
4071
- if (!config.settingsBaseUrl || !config.settingsApiKey) {
4072
- return { name: 'Claude Code CLI', status: 'failed', detail: 'Claude Code 配置缺少 settings.json 中的 Base URL 或 API Key' };
4175
+ if (!config.settingsBaseUrl || !config.apiKey) {
4176
+ return { name: 'Claude Code CLI', status: 'failed', detail: 'Claude Code 配置缺少 settings.json 中的 Base URL 或鉴权字段' };
4073
4177
  }
4074
4178
 
4075
4179
  const env = {
@@ -4127,10 +4231,11 @@ function testCodexCliConnection() {
4127
4231
  NODE_TLS_REJECT_UNAUTHORIZED: '0',
4128
4232
  NODE_NO_WARNINGS: '1'
4129
4233
  };
4234
+ const nullRedirect = getShellNullInputRedirect();
4130
4235
 
4131
4236
  return runCliTestCandidates('Codex CLI', [
4132
- `${shellQuote(cliBinary)} exec --skip-git-repo-check ${shellQuote('请只回复 OK')}`,
4133
- `${shellQuote(cliBinary)} exec ${shellQuote('请只回复 OK')}`
4237
+ `${shellQuote(cliBinary)} exec --skip-git-repo-check ${shellQuote('请只回复 OK')}${nullRedirect}`,
4238
+ `${shellQuote(cliBinary)} exec ${shellQuote('请只回复 OK')}${nullRedirect}`
4134
4239
  ], env);
4135
4240
  }
4136
4241
 
@@ -5186,7 +5291,11 @@ async function autoActivate(paths, args = {}) {
5186
5291
  const codexApiConfig = API_CONFIG.codex;
5187
5292
  const claudeProviderName = claudeApiConfig.providerName;
5188
5293
  const codexProviderName = codexApiConfig.providerName;
5189
- const selectedEndpoint = ENDPOINTS[0];
5294
+ const selectedEndpoint = await resolveEndpointSelection(args, {
5295
+ speedIntro: '📡 开始测速云翼节点...\n',
5296
+ proceedMessage: '仍要写入默认节点配置吗?'
5297
+ });
5298
+ if (!selectedEndpoint) return;
5190
5299
 
5191
5300
  // ---- API Key ----
5192
5301
  const apiKeyEnvFallbacks = [
@@ -5443,7 +5552,8 @@ async function activateClaudeCode(paths, args = {}) {
5443
5552
  console.log(chalk.gray(' API Key: 已设置'));
5444
5553
  console.log(chalk.gray('\n 已写入:'));
5445
5554
  console.log(chalk.gray(' • ~/.claude/settings.json'));
5446
- console.log(chalk.gray(' • 关键字段: apiBaseUrl + model + env.ANTHROPIC_API_KEY + env.ANTHROPIC_BASE_URL'));
5555
+ console.log(chalk.gray(' • 关键字段: apiBaseUrl + model + env.ANTHROPIC_AUTH_TOKEN + env.ANTHROPIC_BASE_URL'));
5556
+ console.log(chalk.gray(' • Claude Code 走 Bearer Token 网关鉴权,避免落入官方登录 / 批准自定义 API Key 流程'));
5447
5557
  console.log(chalk.gray(' • 已钉住 Claude alias 默认版本,避免第三方中转回落到官方旧别名'));
5448
5558
  console.log(chalk.yellow('\n 提示: 如果 Claude Code 已在运行,重新打开一个会话即可读取新配置'));
5449
5559
 
@@ -13,10 +13,8 @@
13
13
  ```json5
14
14
  {
15
15
  "endpoints": [
16
- { "name": "国内主节点", "url": "https://yunyi.rdzhvip.com" },
17
- { "name": "CF国外节点1", "url": "https://yunyi.cfd" },
18
- { "name": "CF国外节点2", "url": "https://cdn1.yunyi.cfd" },
19
- { "name": "CF国外节点3", "url": "https://cdn2.yunyi.cfd" }
16
+ { "name": "默认主节点", "url": "https://yunyi.cfd" },
17
+ { "name": "CF国外节点1", "url": "https://cdn2.yunyi.cfd" }
20
18
  ],
21
19
  "fallbackEndpoints": [
22
20
  { "name": "备用节点1", "url": "http://47.99.42.193" },
@@ -77,7 +75,7 @@ npx yymaxapi@latest
77
75
  ```json
78
76
  {
79
77
  "provider": "anthropic",
80
- "base_url": "https://yunyi.rdzhvip.com/claude",
78
+ "base_url": "https://yunyi.cfd/claude",
81
79
  "api": "anthropic-messages",
82
80
  "api_key": "<你的云翼 API Key>",
83
81
  "model": {
@@ -92,7 +90,7 @@ npx yymaxapi@latest
92
90
  ```json
93
91
  {
94
92
  "provider": "openai",
95
- "base_url": "https://yunyi.rdzhvip.com/codex",
93
+ "base_url": "https://yunyi.cfd/codex",
96
94
  "api": "openai-completions",
97
95
  "api_key": "<你的云翼 API Key>",
98
96
  "model": {
@@ -104,6 +102,7 @@ npx yymaxapi@latest
104
102
 
105
103
  **注意事项:**
106
104
  - `base_url` 不要加 `/v1`,平台会自动拼接路径
105
+ - 如需临时切到其他云翼域名,可直接把域名部分替换掉;`yymaxapi` 也支持在预设/一键激活命令中传 `--endpoint-url https://你的域名` 或 `--base-url https://你的域名/claude`
107
106
  - Claude 可用模型:`claude-sonnet-4-6`、`claude-opus-4-7`、`claude-haiku-4-5`
108
107
  - GPT 可用模型:`gpt-5.4`
109
108
  - 已验证环境:腾讯云 OpenCloudOS,OpenClaw `2026.2.3-1`
@@ -122,7 +121,7 @@ npx yymaxapi@latest
122
121
  ```json
123
122
  {
124
123
  "provider": "anthropic",
125
- "base_url": "https://yunyi.rdzhvip.com/claude",
124
+ "base_url": "https://yunyi.cfd/claude",
126
125
  "api": "anthropic-messages",
127
126
  "api_key": "<你的云翼 API Key>",
128
127
  "model": {
@@ -137,7 +136,7 @@ npx yymaxapi@latest
137
136
  ```json
138
137
  {
139
138
  "provider": "openai",
140
- "base_url": "https://yunyi.rdzhvip.com/codex",
139
+ "base_url": "https://yunyi.cfd/codex",
141
140
  "api": "openai-completions",
142
141
  "api_key": "<你的云翼 API Key>",
143
142
  "model": {
@@ -153,10 +152,10 @@ npx yymaxapi@latest
153
152
 
154
153
  ```json
155
154
  {
156
- "apiBaseUrl": "https://yunyi.rdzhvip.com/claude",
155
+ "apiBaseUrl": "https://yunyi.cfd/claude",
157
156
  "env": {
158
157
  "ANTHROPIC_API_KEY": "<你的云翼 API Key>",
159
- "ANTHROPIC_BASE_URL": "https://yunyi.rdzhvip.com/claude"
158
+ "ANTHROPIC_BASE_URL": "https://yunyi.cfd/claude"
160
159
  }
161
160
  }
162
161
  ```
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "yymaxapi",
3
- "version": "1.0.111",
3
+ "version": "1.0.116",
4
4
  "description": "跨平台 OpenClaw/Clawdbot 配置管理工具 - 管理中转地址、模型切换、API Keys、测速优化",
5
5
  "main": "bin/yymaxapi.js",
6
6
  "bin": {