yymaxapi 1.0.112 → 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
  }
@@ -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
  }
@@ -4021,6 +4092,41 @@ function buildReadableAgentError(text, fallback = '') {
4021
4092
  return (preferred || lines[0] || fallback).slice(0, 300);
4022
4093
  }
4023
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
+
4024
4130
  function looksLikeCliTestError(text) {
4025
4131
  const lower = cleanCliTestOutput(text).toLowerCase();
4026
4132
  if (!lower) return false;
@@ -4043,13 +4149,19 @@ function looksLikeCliTestError(text) {
4043
4149
 
4044
4150
  function runCliTestCandidates(name, commands, env) {
4045
4151
  let lastError = '命令执行失败';
4152
+ let lastErrorScore = scoreCliErrorDetail(lastError);
4046
4153
  for (const command of commands) {
4047
4154
  const result = safeExec(command, { timeout: 120000, env, maxBuffer: 1024 * 1024 });
4048
4155
  const combined = cleanCliTestOutput(`${result.output || ''}\n${result.stdout || ''}\n${result.stderr || ''}`);
4049
4156
  if (result.ok && !looksLikeCliTestError(combined)) {
4050
4157
  return { name, status: 'success', detail: summarizeCliTestOutput(combined) || '连接成功' };
4051
4158
  }
4052
- 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
+ }
4053
4165
  }
4054
4166
  return { name, status: 'failed', detail: lastError };
4055
4167
  }
@@ -4119,10 +4231,11 @@ function testCodexCliConnection() {
4119
4231
  NODE_TLS_REJECT_UNAUTHORIZED: '0',
4120
4232
  NODE_NO_WARNINGS: '1'
4121
4233
  };
4234
+ const nullRedirect = getShellNullInputRedirect();
4122
4235
 
4123
4236
  return runCliTestCandidates('Codex CLI', [
4124
- `${shellQuote(cliBinary)} exec --skip-git-repo-check ${shellQuote('请只回复 OK')}`,
4125
- `${shellQuote(cliBinary)} exec ${shellQuote('请只回复 OK')}`
4237
+ `${shellQuote(cliBinary)} exec --skip-git-repo-check ${shellQuote('请只回复 OK')}${nullRedirect}`,
4238
+ `${shellQuote(cliBinary)} exec ${shellQuote('请只回复 OK')}${nullRedirect}`
4126
4239
  ], env);
4127
4240
  }
4128
4241
 
@@ -5178,7 +5291,11 @@ async function autoActivate(paths, args = {}) {
5178
5291
  const codexApiConfig = API_CONFIG.codex;
5179
5292
  const claudeProviderName = claudeApiConfig.providerName;
5180
5293
  const codexProviderName = codexApiConfig.providerName;
5181
- const selectedEndpoint = ENDPOINTS[0];
5294
+ const selectedEndpoint = await resolveEndpointSelection(args, {
5295
+ speedIntro: '📡 开始测速云翼节点...\n',
5296
+ proceedMessage: '仍要写入默认节点配置吗?'
5297
+ });
5298
+ if (!selectedEndpoint) return;
5182
5299
 
5183
5300
  // ---- API Key ----
5184
5301
  const apiKeyEnvFallbacks = [
@@ -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.112",
3
+ "version": "1.0.116",
4
4
  "description": "跨平台 OpenClaw/Clawdbot 配置管理工具 - 管理中转地址、模型切换、API Keys、测速优化",
5
5
  "main": "bin/yymaxapi.js",
6
6
  "bin": {