yymaxapi 1.0.67 → 1.0.69

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/bin/yymaxapi.js CHANGED
@@ -676,7 +676,7 @@ function writeClaudeCodeSettings(baseUrl, apiKey) {
676
676
  }
677
677
  }
678
678
 
679
- function writeCodexConfig(baseUrl, apiKey) {
679
+ function writeCodexConfig(baseUrl, apiKey, modelId = 'gpt-5.4') {
680
680
  const codexDir = path.join(os.homedir(), '.codex');
681
681
  if (!fs.existsSync(codexDir)) fs.mkdirSync(codexDir, { recursive: true });
682
682
 
@@ -698,7 +698,7 @@ function writeCodexConfig(baseUrl, apiKey) {
698
698
  }
699
699
  const section = [
700
700
  marker,
701
- `model = "gpt-5.4"`,
701
+ `model = "${modelId}"`,
702
702
  `model_provider = "${providerKey}"`,
703
703
  ``,
704
704
  `[model_providers.${providerKey}]`,
@@ -726,6 +726,8 @@ function writeCodexConfig(baseUrl, apiKey) {
726
726
 
727
727
  function writeOpencodeConfig(claudeBaseUrl, codexBaseUrl, apiKey, modelId) {
728
728
  const home = os.homedir();
729
+ const claudeUrl = claudeBaseUrl.replace(/\/+$/, '');
730
+ const codexUrl = (codexBaseUrl || '').replace(/\/+$/, '');
729
731
 
730
732
  // ---- 1. opencode.json (CLI + 桌面版) ----
731
733
  const configDir = process.platform === 'win32'
@@ -738,21 +740,42 @@ function writeOpencodeConfig(claudeBaseUrl, codexBaseUrl, apiKey, modelId) {
738
740
  if (fs.existsSync(configPath)) {
739
741
  try { existing = JSON.parse(fs.readFileSync(configPath, 'utf8')); } catch { existing = {}; }
740
742
  }
741
- const cleanClaudeUrl = claudeBaseUrl.replace(/\/+$/, '');
742
- const cleanCodexUrl = (codexBaseUrl || '').replace(/\/+$/, '');
743
743
  if (!existing.provider) existing.provider = {};
744
- existing.provider.anthropic = {
745
- options: { apiKey: apiKey, baseURL: cleanClaudeUrl }
744
+
745
+ // Claude provider (@ai-sdk/anthropic)
746
+ existing.provider['yunyi-claude'] = {
747
+ name: '云翼 Claude',
748
+ npm: '@ai-sdk/anthropic',
749
+ models: { 'claude-sonnet-4-6': { name: 'Claude Sonnet 4.6' } },
750
+ options: { apiKey, baseURL: `${claudeUrl}/v1` }
746
751
  };
747
- if (cleanCodexUrl) {
748
- existing.provider.openai = {
749
- options: { apiKey: apiKey, baseURL: cleanCodexUrl }
752
+
753
+ // Codex provider (@ai-sdk/openai)
754
+ if (codexUrl) {
755
+ existing.provider['yunyi-codex'] = {
756
+ name: '云翼 Codex',
757
+ npm: '@ai-sdk/openai',
758
+ models: { 'gpt-5.4': { name: 'GPT 5.4' } },
759
+ options: { apiKey, baseURL: codexUrl }
750
760
  };
751
761
  }
762
+
763
+ // 清理旧版写入的通用 provider 名(之前的 bug)
764
+ if (existing.provider.anthropic && existing.provider.anthropic.options
765
+ && existing.provider.anthropic.options.baseURL && existing.provider.anthropic.options.baseURL.includes('yunyi')) {
766
+ delete existing.provider.anthropic;
767
+ }
768
+
769
+ // 设置默认模型
752
770
  const rawModelId = modelId || 'claude-sonnet-4-6';
753
- existing.model = rawModelId.startsWith('anthropic/') || rawModelId.startsWith('openai/')
754
- ? rawModelId
755
- : `anthropic/${rawModelId}`;
771
+ existing.model = `yunyi-claude/${rawModelId}`;
772
+
773
+ // 从 disabled_providers 中移除我们的 provider
774
+ if (Array.isArray(existing.disabled_providers)) {
775
+ existing.disabled_providers = existing.disabled_providers.filter(p => p !== 'yunyi-claude' && p !== 'yunyi-codex');
776
+ if (existing.disabled_providers.length === 0) delete existing.disabled_providers;
777
+ }
778
+
756
779
  if (!existing.$schema) existing.$schema = 'https://opencode.ai/config.json';
757
780
  fs.writeFileSync(configPath, JSON.stringify(existing, null, 2), 'utf8');
758
781
  } catch { /* 非关键,静默失败 */ }
@@ -763,49 +786,74 @@ function writeOpencodeConfig(claudeBaseUrl, codexBaseUrl, apiKey, modelId) {
763
786
  try {
764
787
  if (!fs.existsSync(codexDir)) fs.mkdirSync(codexDir, { recursive: true });
765
788
 
766
- const claudeUrl = claudeBaseUrl.replace(/\/+$/, '');
767
- const codexUrl = (codexBaseUrl || '').replace(/\/+$/, '');
768
-
769
- const marker = '# >>> yunyi opencode >>>';
770
- const markerEnd = '# <<< yunyi opencode <<<';
771
- let existing = '';
789
+ let content = '';
772
790
  if (fs.existsSync(codexConfigPath)) {
773
- existing = fs.readFileSync(codexConfigPath, 'utf8');
774
- const re = new RegExp(`${marker.replace(/[.*+?^${}()|[\\]\\]/g, '\\$&')}[\\s\\S]*?${markerEnd.replace(/[.*+?^${}()|[\\]\\]/g, '\\$&')}\\n?`, 'g');
775
- existing = existing.replace(re, '').trim();
791
+ content = fs.readFileSync(codexConfigPath, 'utf8');
776
792
  }
777
793
 
778
- const section = [
779
- marker,
780
- `model = "yunyi-claude/claude-sonnet-4-6"`,
781
- `model_provider = "yunyi-claude"`,
782
- `model_context_window = 1000000`,
783
- `model_auto_compact_token_limit = 900000`,
784
- `model_reasoning_effort = "xhigh"`,
794
+ // 移除旧标记块(yunyi opencode / maxapi codex)
795
+ content = content.replace(/# >>> yunyi opencode >>>[\s\S]*?# <<< yunyi opencode <<<\n?/g, '');
796
+ content = content.replace(/# >>> maxapi codex >>>[\s\S]*?# <<< maxapi codex <<<\n?/g, '');
797
+
798
+ // 移除旧的 yunyi model_providers 段落(包括 yunyi-cli 写入的 [model_providers.yunyi])
799
+ content = content.replace(/\[model_providers\.yunyi[^\]]*\]\n(?:(?!\[)[^\n]*\n?)*/g, '');
800
+
801
+ // 替换或插入顶层设置
802
+ const topSettings = {
803
+ model_provider: '"yunyi-claude"',
804
+ model: '"claude-sonnet-4-6"',
805
+ model_context_window: '1000000',
806
+ model_auto_compact_token_limit: '900000',
807
+ model_reasoning_effort: '"xhigh"',
808
+ disable_response_storage: 'true',
809
+ };
810
+ for (const [key, value] of Object.entries(topSettings)) {
811
+ const re = new RegExp(`^${key}\\s*=\\s*[^\\n]*`, 'm');
812
+ if (re.test(content)) {
813
+ content = content.replace(re, `${key} = ${value}`);
814
+ } else {
815
+ // 插入到第一个 section 之前
816
+ const firstSection = content.search(/^\[/m);
817
+ if (firstSection > 0) {
818
+ content = content.slice(0, firstSection) + `${key} = ${value}\n` + content.slice(firstSection);
819
+ } else if (firstSection === 0) {
820
+ content = `${key} = ${value}\n` + content;
821
+ } else {
822
+ content += `${key} = ${value}\n`;
823
+ }
824
+ }
825
+ }
826
+
827
+ // 清理多余空行
828
+ content = content.replace(/\n{3,}/g, '\n\n').trim();
829
+
830
+ // 追加 model_providers 段落
831
+ const providers = [
785
832
  '',
786
- `[model_providers.yunyi-claude]`,
787
- `name = "云翼 Claude"`,
833
+ '# >>> yunyi opencode >>>',
834
+ '[model_providers.yunyi-claude]',
835
+ 'name = "云翼 Claude"',
788
836
  `base_url = "${claudeUrl}"`,
789
- `wire_api = "anthropic"`,
790
- `env_key = "ANTHROPIC_API_KEY"`,
837
+ 'wire_api = "anthropic"',
838
+ `experimental_bearer_token = "${apiKey}"`,
791
839
  ];
792
-
793
840
  if (codexUrl) {
794
- section.push(
841
+ providers.push(
795
842
  '',
796
- `[model_providers.yunyi-codex]`,
797
- `name = "云翼 Codex"`,
843
+ '[model_providers.yunyi-codex]',
844
+ 'name = "云翼 Codex"',
798
845
  `base_url = "${codexUrl}"`,
799
- `wire_api = "responses"`,
800
- `requires_openai_auth = true`,
801
- `env_key = "OPENAI_API_KEY"`,
846
+ 'wire_api = "responses"',
847
+ `experimental_bearer_token = "${apiKey}"`,
848
+ 'requires_openai_auth = true',
802
849
  );
803
850
  }
851
+ providers.push('# <<< yunyi opencode <<<');
852
+ content += '\n\n' + providers.join('\n') + '\n';
804
853
 
805
- section.push(markerEnd);
806
- fs.writeFileSync(codexConfigPath, existing ? `${existing}\n\n${section.join('\n')}\n` : `${section.join('\n')}\n`, 'utf8');
854
+ fs.writeFileSync(codexConfigPath, content, 'utf8');
807
855
 
808
- // ~/.codex/auth.json
856
+ // ---- 3. ~/.codex/auth.json ----
809
857
  const authPath = path.join(codexDir, 'auth.json');
810
858
  let auth = {};
811
859
  if (fs.existsSync(authPath)) {
@@ -3018,15 +3066,104 @@ async function activateOpencode(paths, args = {}) {
3018
3066
  writeSpinner.succeed('Opencode 配置写入完成');
3019
3067
 
3020
3068
  console.log(chalk.green('\n✅ Opencode 配置完成!'));
3021
- console.log(chalk.cyan(` Claude: ${claudeBaseUrl}`));
3022
- console.log(chalk.cyan(` Codex: ${codexBaseUrl}`));
3023
- console.log(chalk.gray(` 默认模型: anthropic/${modelId}`));
3069
+ console.log(chalk.cyan(` Claude: ${claudeBaseUrl} → claude-sonnet-4-6`));
3070
+ console.log(chalk.cyan(` Codex: ${codexBaseUrl} → gpt-5.4`));
3071
+ console.log(chalk.gray(` 默认模型: yunyi-claude/${modelId}`));
3024
3072
  console.log(chalk.gray(' API Key: 已设置'));
3025
3073
  console.log(chalk.gray('\n 已写入:'));
3026
3074
  console.log(chalk.gray(' • ~/.config/opencode/opencode.json (CLI + 桌面版)'));
3027
3075
  console.log(chalk.gray(' • ~/.codex/config.toml (model_providers)'));
3028
3076
  console.log(chalk.gray(' • ~/.codex/auth.json (API Keys)'));
3029
- console.log(chalk.yellow('\n 切换模型: 在 opencode 内使用 /model 命令切换 claude/gpt'));
3077
+ console.log(chalk.yellow('\n 切换模型: 在 opencode 内使用 /model 命令切换 yunyi-claude / yunyi-codex'));
3078
+ }
3079
+
3080
+ // ============ 单独配置 Codex CLI ============
3081
+ async function activateCodex(paths, args = {}) {
3082
+ console.log(chalk.cyan.bold('\n🔧 配置 Codex CLI\n'));
3083
+
3084
+ // ---- 选模型 ----
3085
+ let modelId = CODEX_MODELS[0]?.id || 'gpt-5.4';
3086
+ if (CODEX_MODELS.length > 1) {
3087
+ const { selected } = await inquirer.prompt([{
3088
+ type: 'list',
3089
+ name: 'selected',
3090
+ message: '选择模型:',
3091
+ choices: CODEX_MODELS.map(m => ({ name: m.name, value: m.id }))
3092
+ }]);
3093
+ modelId = selected;
3094
+ }
3095
+ const modelConfig = CODEX_MODELS.find(m => m.id === modelId) || { id: modelId, name: modelId };
3096
+
3097
+ // ---- 测速选节点 ----
3098
+ const shouldTest = !(args['no-test'] || args.noTest);
3099
+ let selectedEndpoint = ENDPOINTS[0];
3100
+
3101
+ if (shouldTest) {
3102
+ console.log(chalk.cyan('📡 开始测速节点...\n'));
3103
+ const speedResult = await testAllEndpoints(ENDPOINTS, { autoFallback: true });
3104
+ const sorted = speedResult.ranked || [];
3105
+ if (sorted.length > 0) {
3106
+ const defaultEp = ENDPOINTS[0];
3107
+ const { selectedIndex } = await inquirer.prompt([{
3108
+ type: 'list',
3109
+ name: 'selectedIndex',
3110
+ message: '选择节点:',
3111
+ choices: [
3112
+ { name: `* 使用默认节点 (${defaultEp.name})`, value: -1 },
3113
+ new inquirer.Separator(' ---- 或按测速结果选择 ----'),
3114
+ ...sorted.map((e, i) => ({
3115
+ name: `${e.name} - ${e.latency}ms (评分:${e.score})`,
3116
+ value: i
3117
+ }))
3118
+ ]
3119
+ }]);
3120
+ selectedEndpoint = selectedIndex === -1 ? defaultEp : sorted[selectedIndex];
3121
+ } else {
3122
+ console.log(chalk.red('\n⚠️ 所有节点均不可达'));
3123
+ const { proceed } = await inquirer.prompt([{
3124
+ type: 'confirm', name: 'proceed',
3125
+ message: '仍要使用默认节点配置吗?', default: false
3126
+ }]);
3127
+ if (!proceed) { console.log(chalk.gray('已取消')); return; }
3128
+ }
3129
+ }
3130
+
3131
+ // ---- API Key ----
3132
+ const directKey = (args['api-key'] || args.apiKey || args.key || '').toString().trim();
3133
+ let apiKey;
3134
+ if (directKey) {
3135
+ apiKey = directKey;
3136
+ } else {
3137
+ const envKey = process.env.OPENAI_API_KEY || '';
3138
+ apiKey = await promptApiKey('请输入 API Key:', envKey);
3139
+ }
3140
+ if (!apiKey) { console.log(chalk.gray('已取消')); return; }
3141
+
3142
+ // ---- 验证 ----
3143
+ console.log('');
3144
+ const validation = await validateApiKey(selectedEndpoint.url, apiKey);
3145
+ if (!validation.valid) {
3146
+ const { continueAnyway } = await inquirer.prompt([{
3147
+ type: 'confirm', name: 'continueAnyway',
3148
+ message: 'API Key 验证失败,是否仍然继续写入配置?', default: false
3149
+ }]);
3150
+ if (!continueAnyway) { console.log(chalk.gray('已取消')); return; }
3151
+ }
3152
+
3153
+ // ---- 写入配置 ----
3154
+ const codexBaseUrl = buildFullUrl(selectedEndpoint.url, 'codex');
3155
+ const writeSpinner = ora({ text: '正在写入 Codex 配置...', spinner: 'dots' }).start();
3156
+ writeCodexConfig(codexBaseUrl, apiKey, modelId);
3157
+ writeSpinner.succeed('Codex 配置写入完成');
3158
+
3159
+ console.log(chalk.green('\n✅ Codex CLI 配置完成!'));
3160
+ console.log(chalk.cyan(` Base URL: ${codexBaseUrl}`));
3161
+ console.log(chalk.gray(` 模型: ${modelConfig.name} (${modelId})`));
3162
+ console.log(chalk.gray(' API Key: 已设置'));
3163
+ console.log(chalk.gray('\n 已写入:'));
3164
+ console.log(chalk.gray(' • ~/.codex/config.toml (model + model_providers)'));
3165
+ console.log(chalk.gray(' • ~/.codex/auth.json (OPENAI_API_KEY)'));
3166
+ console.log(chalk.yellow('\n 提示: 请重新打开终端使配置生效'));
3030
3167
  }
3031
3168
 
3032
3169
  // ============ yycode 精简模式(零交互一键配置) ============
@@ -3248,6 +3385,7 @@ async function main() {
3248
3385
  { name: ' 配置 OpenClaw(Claude + Codex)', value: 'auto_activate' },
3249
3386
  { name: ' 配置 Claude Code', value: 'activate_claude_code' },
3250
3387
  { name: ' 配置 Opencode', value: 'activate_opencode' },
3388
+ { name: ' 配置 Codex CLI', value: 'activate_codex' },
3251
3389
  new inquirer.Separator(' -- 工具 --'),
3252
3390
  { name: ' 切换模型', value: 'switch_model' },
3253
3391
  { name: ` 权限管理${getToolsProfileTag(paths)}`, value: 'tools_profile' },
@@ -3292,6 +3430,9 @@ async function main() {
3292
3430
  case 'activate_opencode':
3293
3431
  await activateOpencode(paths);
3294
3432
  break;
3433
+ case 'activate_codex':
3434
+ await activateCodex(paths);
3435
+ break;
3295
3436
  }
3296
3437
  } catch (error) {
3297
3438
  console.log(chalk.red(`\n错误: ${error.message}\n`));
@@ -107,3 +107,62 @@ npx yymaxapi@latest
107
107
  - GPT 可用模型:`gpt-5.4`
108
108
  - 已验证环境:腾讯云 OpenCloudOS,OpenClaw `2026.2.3-1`
109
109
  - 参考文档:https://cloud.tencent.com/developer/article/2624003
110
+
111
+ ### 腾讯云 + Claude Code CLI 一起写入(同一 Key)
112
+
113
+ 云翼一张卡可同时用于 Claude 与 GPT。按下面两步做即可同时生效。
114
+
115
+ **1. 腾讯云 OpenClaw 自定义模型(添加两条)**
116
+
117
+ 在腾讯云 OpenClaw 的「自定义模型」里添加**两条**,`api_key` 用同一云翼 Key 即可。
118
+
119
+ 第一条 — Claude Sonnet 4.6:
120
+
121
+ ```json
122
+ {
123
+ "provider": "anthropic",
124
+ "base_url": "https://yunyi.rdzhvip.com/claude",
125
+ "api": "anthropic-messages",
126
+ "api_key": "<你的云翼 API Key>",
127
+ "model": {
128
+ "id": "claude-sonnet-4-6",
129
+ "name": "Claude Sonnet 4.6"
130
+ }
131
+ }
132
+ ```
133
+
134
+ 第二条 — GPT 5.4:
135
+
136
+ ```json
137
+ {
138
+ "provider": "openai",
139
+ "base_url": "https://yunyi.rdzhvip.com/codex",
140
+ "api": "openai-responses",
141
+ "api_key": "<你的云翼 API Key>",
142
+ "model": {
143
+ "id": "gpt-5.4",
144
+ "name": "GPT 5.4"
145
+ }
146
+ }
147
+ ```
148
+
149
+ **2. Claude Code CLI(本机终端)**
150
+
151
+ 在终端执行一次,或写入 `~/.zshrc` / `~/.bashrc` 后 `source`,Claude Code 即走云翼 Claude:
152
+
153
+ ```bash
154
+ export ANTHROPIC_BASE_URL="https://yunyi.rdzhvip.com/claude"
155
+ export ANTHROPIC_AUTH_TOKEN="<你的云翼 API Key>"
156
+ # 若遇证书报错可加:
157
+ export NODE_TLS_REJECT_UNAUTHORIZED=0
158
+ ```
159
+
160
+ Windows(PowerShell,用户级环境变量):
161
+
162
+ ```powershell
163
+ [Environment]::SetEnvironmentVariable('ANTHROPIC_BASE_URL', 'https://yunyi.rdzhvip.com/claude', 'User')
164
+ [Environment]::SetEnvironmentVariable('ANTHROPIC_AUTH_TOKEN', '<你的云翼 API Key>', 'User')
165
+ [Environment]::SetEnvironmentVariable('NODE_TLS_REJECT_UNAUTHORIZED', '0', 'User')
166
+ ```
167
+
168
+ 完成后:腾讯云 OpenClaw 里可选 Claude 或 GPT;本机 Claude Code CLI 使用同一 Key 走云翼 Claude。
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "yymaxapi",
3
- "version": "1.0.67",
3
+ "version": "1.0.69",
4
4
  "description": "跨平台 OpenClaw/Clawdbot 配置管理工具 - 管理中转地址、模型切换、API Keys、测速优化",
5
5
  "main": "bin/yymaxapi.js",
6
6
  "bin": {