yymaxapi 1.0.112 → 1.0.117

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,20 +73,16 @@ 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"
76
+ "name": "默认主节点",
77
+ "url": "https://yunyi.yun"
78
78
  },
79
79
  {
80
- "name": "CF国外节点1",
81
- "url": "https://yunyi.cfd"
80
+ "name": "CDN节点1",
81
+ "url": "https://cdn1.yunyi.yun"
82
82
  },
83
83
  {
84
- "name": "CF国外节点2",
85
- "url": "https://cdn1.yunyi.cfd"
86
- },
87
- {
88
- "name": "CF国外节点3",
89
- "url": "https://cdn2.yunyi.cfd"
84
+ "name": "CDN节点2",
85
+ "url": "https://cdn2.yunyi.yun"
90
86
  }
91
87
  ];
92
88
 
@@ -616,9 +612,73 @@ function shouldRunEndpointSpeedTest(args = {}) {
616
612
  return !!raw;
617
613
  }
618
614
 
615
+ function getEndpointOverrideInput(args = {}) {
616
+ const raw = args['endpoint-url']
617
+ ?? args.endpointUrl
618
+ ?? args.endpoint
619
+ ?? args.domain
620
+ ?? args['base-url']
621
+ ?? args.baseUrl
622
+ ?? '';
623
+ return String(raw || '').trim();
624
+ }
625
+
626
+ function normalizeEndpointBaseUrl(baseUrl) {
627
+ let trimmed = String(baseUrl || '').trim();
628
+ if (!trimmed) return '';
629
+
630
+ trimmed = trimClaudeMessagesSuffix(trimmed);
631
+ trimmed = trimOpenAiEndpointSuffix(trimmed);
632
+ trimmed = stripManagedYunyiSuffix(trimmed);
633
+
634
+ try {
635
+ const urlObj = new URL(trimmed);
636
+ urlObj.search = '';
637
+ urlObj.hash = '';
638
+ const normalizedPath = urlObj.pathname.replace(/\/+$/, '');
639
+ return `${urlObj.origin}${normalizedPath && normalizedPath !== '/' ? normalizedPath : ''}`;
640
+ } catch {
641
+ return trimmed.replace(/\/+$/, '');
642
+ }
643
+ }
644
+
645
+ function resolveEndpointOverride(args = {}) {
646
+ const raw = getEndpointOverrideInput(args);
647
+ if (!raw) return null;
648
+
649
+ const normalized = normalizeEndpointBaseUrl(raw);
650
+ if (!normalized || !isValidUrl(normalized)) {
651
+ throw new Error('自定义节点 URL 无效,请使用 http(s):// 域名,支持填写根域名、/claude 或 /codex 地址');
652
+ }
653
+
654
+ return { name: '自定义节点', url: normalized };
655
+ }
656
+
657
+ async function promptCustomEndpointSelection(args = {}, options = {}) {
658
+ const defaultInput = getEndpointOverrideInput(args);
659
+ const promptMessage = options.customEndpointMessage || '请输入节点域名或 Base URL(支持根域名、/claude、/codex):';
660
+ const { customEndpointInput } = await inquirer.prompt([{
661
+ type: 'input',
662
+ name: 'customEndpointInput',
663
+ message: promptMessage,
664
+ default: defaultInput,
665
+ validate: input => {
666
+ const normalized = normalizeEndpointBaseUrl(input);
667
+ return Boolean(normalized && isValidUrl(normalized)) || '请输入有效的 URL(http:// 或 https://)';
668
+ }
669
+ }]);
670
+
671
+ return {
672
+ name: options.customEndpointName || '自定义节点',
673
+ url: normalizeEndpointBaseUrl(customEndpointInput)
674
+ };
675
+ }
676
+
619
677
  async function resolveEndpointSelection(args = {}, options = {}) {
620
678
  const defaultEndpoint = options.defaultEndpoint || ENDPOINTS[0];
621
679
  if (!defaultEndpoint) return null;
680
+ const directOverride = resolveEndpointOverride(args);
681
+ if (directOverride) return directOverride;
622
682
  if (!shouldRunEndpointSpeedTest(args)) return defaultEndpoint;
623
683
 
624
684
  const speedIntro = options.speedIntro || '📡 开始测速节点...\n';
@@ -636,6 +696,7 @@ async function resolveEndpointSelection(args = {}, options = {}) {
636
696
  message: chooseMessage,
637
697
  choices: [
638
698
  { name: `* 使用默认节点 (${defaultEndpoint.name})`, value: -1 },
699
+ { name: '* 手动输入节点域名', value: '__custom__' },
639
700
  new inquirer.Separator(' ---- 或按测速结果选择 ----'),
640
701
  ...sorted.map((endpoint, index) => ({
641
702
  name: `${endpoint.name} - ${endpoint.latency}ms (评分:${endpoint.score})`,
@@ -644,7 +705,9 @@ async function resolveEndpointSelection(args = {}, options = {}) {
644
705
  ]
645
706
  }]);
646
707
 
647
- const selectedEndpoint = selectedIndex === -1 ? defaultEndpoint : sorted[selectedIndex];
708
+ const selectedEndpoint = selectedIndex === '__custom__'
709
+ ? await promptCustomEndpointSelection(args, options)
710
+ : (selectedIndex === -1 ? defaultEndpoint : sorted[selectedIndex]);
648
711
  if (speedResult.usedFallback) {
649
712
  console.log(chalk.yellow('\n⚠ 当前使用备用节点\n'));
650
713
  }
@@ -652,13 +715,21 @@ async function resolveEndpointSelection(args = {}, options = {}) {
652
715
  }
653
716
 
654
717
  console.log(chalk.red('\n⚠️ 所有节点(含备用)均不可达'));
655
- const { proceed } = await inquirer.prompt([{
656
- type: 'confirm',
657
- name: 'proceed',
658
- message: proceedMessage,
659
- default: false
718
+ const proceedChoiceLabel = options.proceedChoiceLabel || `${proceedMessage.replace(/[??]\s*$/, '')} (${defaultEndpoint.name})`;
719
+ const { nextAction } = await inquirer.prompt([{
720
+ type: 'list',
721
+ name: 'nextAction',
722
+ message: '测速未找到可用节点,接下来怎么做?',
723
+ choices: [
724
+ { name: proceedChoiceLabel, value: 'default' },
725
+ { name: '手动输入节点域名', value: 'custom' },
726
+ { name: '取消', value: 'cancel' }
727
+ ]
660
728
  }]);
661
- if (!proceed) {
729
+ if (nextAction === 'custom') {
730
+ return promptCustomEndpointSelection(args, options);
731
+ }
732
+ if (nextAction !== 'default') {
662
733
  console.log(chalk.gray('已取消'));
663
734
  return null;
664
735
  }
@@ -2671,17 +2742,17 @@ function hasManagedYunyiFootprint(config) {
2671
2742
  }
2672
2743
 
2673
2744
  function inferManagedYunyiEndpointUrl(config, explicitEndpointUrl = '') {
2674
- const direct = stripManagedYunyiSuffix(explicitEndpointUrl);
2675
- if (direct && isLikelyYunyiBaseUrl(direct)) return direct;
2745
+ const direct = normalizeEndpointBaseUrl(explicitEndpointUrl);
2746
+ if (direct && isValidUrl(direct)) return direct;
2676
2747
 
2677
2748
  const providers = config?.models?.providers || {};
2678
2749
  for (const [name, provider] of Object.entries(providers)) {
2679
2750
  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;
2751
+ const candidate = normalizeEndpointBaseUrl(provider?.baseUrl || provider?.base_url || '');
2752
+ if (candidate && isValidUrl(candidate)) return candidate;
2682
2753
  }
2683
2754
 
2684
- return ENDPOINTS.find(item => isLikelyYunyiBaseUrl(item.url))?.url || ENDPOINTS[0]?.url || '';
2755
+ return ENDPOINTS.find(item => isValidUrl(item.url))?.url || ENDPOINTS[0]?.url || '';
2685
2756
  }
2686
2757
 
2687
2758
  function inferManagedYunyiApiKey(config, explicitApiKey = '') {
@@ -3450,6 +3521,10 @@ function shellQuote(value) {
3450
3521
  return `"${str.replace(/(["\\$`])/g, '\\$1')}"`;
3451
3522
  }
3452
3523
 
3524
+ function getShellNullInputRedirect() {
3525
+ return process.platform === 'win32' ? ' < NUL' : ' < /dev/null';
3526
+ }
3527
+
3453
3528
  function escapeSingleQuotedShell(value) {
3454
3529
  return String(value || '').replace(/'/g, "'\\''");
3455
3530
  }
@@ -4021,6 +4096,41 @@ function buildReadableAgentError(text, fallback = '') {
4021
4096
  return (preferred || lines[0] || fallback).slice(0, 300);
4022
4097
  }
4023
4098
 
4099
+ function scoreCliErrorDetail(text) {
4100
+ const lower = String(text || '').toLowerCase();
4101
+ if (!lower) return -1;
4102
+ let score = 0;
4103
+ if ([
4104
+ 'forbidden',
4105
+ 'unauthorized',
4106
+ 'permission to access',
4107
+ 'authentication_error',
4108
+ 'invalid bearer token',
4109
+ 'invalid api key',
4110
+ 'all models failed',
4111
+ 'auth issue',
4112
+ 'auth_permanent',
4113
+ 'unexpected status',
4114
+ 'gateway agent failed'
4115
+ ].some(pattern => lower.includes(pattern))) {
4116
+ score += 5;
4117
+ }
4118
+ if ([
4119
+ 'failed',
4120
+ 'error:',
4121
+ 'missing environment variable'
4122
+ ].some(pattern => lower.includes(pattern))) {
4123
+ score += 2;
4124
+ }
4125
+ if ([
4126
+ 'reading additional input from stdin',
4127
+ 'reconnecting'
4128
+ ].some(pattern => lower.includes(pattern))) {
4129
+ score -= 2;
4130
+ }
4131
+ return score;
4132
+ }
4133
+
4024
4134
  function looksLikeCliTestError(text) {
4025
4135
  const lower = cleanCliTestOutput(text).toLowerCase();
4026
4136
  if (!lower) return false;
@@ -4043,13 +4153,19 @@ function looksLikeCliTestError(text) {
4043
4153
 
4044
4154
  function runCliTestCandidates(name, commands, env) {
4045
4155
  let lastError = '命令执行失败';
4156
+ let lastErrorScore = scoreCliErrorDetail(lastError);
4046
4157
  for (const command of commands) {
4047
4158
  const result = safeExec(command, { timeout: 120000, env, maxBuffer: 1024 * 1024 });
4048
4159
  const combined = cleanCliTestOutput(`${result.output || ''}\n${result.stdout || ''}\n${result.stderr || ''}`);
4049
4160
  if (result.ok && !looksLikeCliTestError(combined)) {
4050
4161
  return { name, status: 'success', detail: summarizeCliTestOutput(combined) || '连接成功' };
4051
4162
  }
4052
- lastError = summarizeCliTestOutput(combined) || result.error || lastError;
4163
+ const candidateError = buildReadableAgentError(combined, result.error || lastError);
4164
+ const candidateScore = scoreCliErrorDetail(candidateError);
4165
+ if (candidateScore >= lastErrorScore) {
4166
+ lastError = candidateError;
4167
+ lastErrorScore = candidateScore;
4168
+ }
4053
4169
  }
4054
4170
  return { name, status: 'failed', detail: lastError };
4055
4171
  }
@@ -4119,10 +4235,11 @@ function testCodexCliConnection() {
4119
4235
  NODE_TLS_REJECT_UNAUTHORIZED: '0',
4120
4236
  NODE_NO_WARNINGS: '1'
4121
4237
  };
4238
+ const nullRedirect = getShellNullInputRedirect();
4122
4239
 
4123
4240
  return runCliTestCandidates('Codex CLI', [
4124
- `${shellQuote(cliBinary)} exec --skip-git-repo-check ${shellQuote('请只回复 OK')}`,
4125
- `${shellQuote(cliBinary)} exec ${shellQuote('请只回复 OK')}`
4241
+ `${shellQuote(cliBinary)} exec --skip-git-repo-check ${shellQuote('请只回复 OK')}${nullRedirect}`,
4242
+ `${shellQuote(cliBinary)} exec ${shellQuote('请只回复 OK')}${nullRedirect}`
4126
4243
  ], env);
4127
4244
  }
4128
4245
 
@@ -5178,7 +5295,11 @@ async function autoActivate(paths, args = {}) {
5178
5295
  const codexApiConfig = API_CONFIG.codex;
5179
5296
  const claudeProviderName = claudeApiConfig.providerName;
5180
5297
  const codexProviderName = codexApiConfig.providerName;
5181
- const selectedEndpoint = ENDPOINTS[0];
5298
+ const selectedEndpoint = await resolveEndpointSelection(args, {
5299
+ speedIntro: '📡 开始测速云翼节点...\n',
5300
+ proceedMessage: '仍要写入默认节点配置吗?'
5301
+ });
5302
+ if (!selectedEndpoint) return;
5182
5303
 
5183
5304
  // ---- API Key ----
5184
5305
  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.117",
4
4
  "description": "跨平台 OpenClaw/Clawdbot 配置管理工具 - 管理中转地址、模型切换、API Keys、测速优化",
5
5
  "main": "bin/yymaxapi.js",
6
6
  "bin": {