yymaxapi 1.0.99 → 1.0.101

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 +306 -100
  2. package/package.json +1 -1
package/bin/yymaxapi.js CHANGED
@@ -254,6 +254,34 @@ function getDefaultCodexModel() {
254
254
  return CODEX_MODELS[0] || { id: 'gpt-5.4', name: 'GPT 5.4' };
255
255
  }
256
256
 
257
+ function getExternalClaudeProviderKey() {
258
+ return String(API_CONFIG?.claude?.providerName || 'yunyi-claude').trim() || 'yunyi-claude';
259
+ }
260
+
261
+ function getExternalCodexProviderKey() {
262
+ const providerKey = String(API_CONFIG?.codex?.providerName || '').trim();
263
+ if (!providerKey) return 'yunyi-codex';
264
+ return providerKey === 'yunyi' ? 'yunyi-codex' : providerKey;
265
+ }
266
+
267
+ function getExternalModelKey(type, modelId) {
268
+ const providerKey = type === 'codex' ? getExternalCodexProviderKey() : getExternalClaudeProviderKey();
269
+ return `${providerKey}/${modelId}`;
270
+ }
271
+
272
+ function getProviderBrandPrefix() {
273
+ const providerKeys = [getExternalClaudeProviderKey(), getExternalCodexProviderKey()];
274
+ if (providerKeys.some(key => key.startsWith('maxapi'))) return 'MAXAPI';
275
+ if (providerKeys.some(key => key.startsWith('yunyi'))) return '云翼';
276
+ return 'OpenClaw';
277
+ }
278
+
279
+ function isUnifiedSingleModelMode() {
280
+ return CLAUDE_MODELS.length === 1
281
+ && CODEX_MODELS.length === 1
282
+ && CLAUDE_MODELS[0].id === CODEX_MODELS[0].id;
283
+ }
284
+
257
285
  function buildProviderModelMap(models) {
258
286
  const mapped = {};
259
287
  for (const model of models || []) {
@@ -268,7 +296,7 @@ function getClaudeSwitchHint() {
268
296
  }
269
297
 
270
298
  function getOpencodeSwitchHint() {
271
- return [...CLAUDE_MODELS, ...CODEX_MODELS].map(model => model.name).join(' / ');
299
+ return [...new Set([...CLAUDE_MODELS, ...CODEX_MODELS].map(model => model.name))].join(' / ');
272
300
  }
273
301
 
274
302
  async function promptClaudeModelSelection(args = {}, message = '选择 Claude 模型:') {
@@ -297,16 +325,29 @@ async function promptOpencodeDefaultModelSelection(args = {}, message = '选择
297
325
  const fallbackClaude = getDefaultClaudeModel();
298
326
  const fallbackCodex = getDefaultCodexModel();
299
327
  const requested = (args['default-model'] || args.model || args['claude-model'] || args['codex-model'] || '').toString().trim();
328
+ const claudeProviderKey = getExternalClaudeProviderKey();
329
+ const codexProviderKey = getExternalCodexProviderKey();
330
+
331
+ if (isUnifiedSingleModelMode()) {
332
+ const unifiedModel = CLAUDE_MODELS[0] || CODEX_MODELS[0] || fallbackClaude;
333
+ return {
334
+ type: 'claude',
335
+ providerKey: claudeProviderKey,
336
+ modelId: unifiedModel.id,
337
+ modelName: unifiedModel.name,
338
+ modelKey: getExternalModelKey('claude', unifiedModel.id)
339
+ };
340
+ }
300
341
 
301
342
  if (requested) {
302
343
  const inClaude = CLAUDE_MODELS.find(model => model.id === requested);
303
344
  if (inClaude) {
304
345
  return {
305
346
  type: 'claude',
306
- providerKey: 'yunyi-claude',
347
+ providerKey: claudeProviderKey,
307
348
  modelId: inClaude.id,
308
349
  modelName: inClaude.name,
309
- modelKey: `yunyi-claude/${inClaude.id}`
350
+ modelKey: getExternalModelKey('claude', inClaude.id)
310
351
  };
311
352
  }
312
353
 
@@ -314,10 +355,10 @@ async function promptOpencodeDefaultModelSelection(args = {}, message = '选择
314
355
  if (inCodex) {
315
356
  return {
316
357
  type: 'codex',
317
- providerKey: 'yunyi-codex',
358
+ providerKey: codexProviderKey,
318
359
  modelId: inCodex.id,
319
360
  modelName: inCodex.name,
320
- modelKey: `yunyi-codex/${inCodex.id}`
361
+ modelKey: getExternalModelKey('codex', inCodex.id)
321
362
  };
322
363
  }
323
364
  }
@@ -350,20 +391,20 @@ async function promptOpencodeDefaultModelSelection(args = {}, message = '选择
350
391
  const model = CODEX_MODELS.find(item => item.id === pickedId) || fallbackCodex;
351
392
  return {
352
393
  type: 'codex',
353
- providerKey: 'yunyi-codex',
394
+ providerKey: codexProviderKey,
354
395
  modelId: model.id,
355
396
  modelName: model.name,
356
- modelKey: `yunyi-codex/${model.id}`
397
+ modelKey: getExternalModelKey('codex', model.id)
357
398
  };
358
399
  }
359
400
 
360
401
  const model = CLAUDE_MODELS.find(item => item.id === pickedId) || fallbackClaude;
361
402
  return {
362
403
  type: 'claude',
363
- providerKey: 'yunyi-claude',
404
+ providerKey: claudeProviderKey,
364
405
  modelId: model.id,
365
406
  modelName: model.name,
366
- modelKey: `yunyi-claude/${model.id}`
407
+ modelKey: getExternalModelKey('claude', model.id)
367
408
  };
368
409
  }
369
410
 
@@ -864,13 +905,14 @@ function writeCodexConfig(baseUrl, apiKey, modelId = 'gpt-5.4') {
864
905
  const markerEnd = '# <<< maxapi codex <<<';
865
906
  const topMarker = '# >>> maxapi codex top >>>';
866
907
  const topMarkerEnd = '# <<< maxapi codex top <<<';
867
- const providerKey = 'yunyi-codex';
908
+ const providerKey = getExternalCodexProviderKey();
909
+ const providerLabel = `${getProviderBrandPrefix()} Codex`;
910
+ const removableProviderKeys = [...new Set([providerKey, 'yunyi-codex', 'maxapi-codex'])];
868
911
  // 确保 base_url 以 /v1 结尾(Codex CLI 要求)
869
912
  let normalizedUrl = baseUrl.replace(/\/+$/, '');
870
913
  if (!normalizedUrl.endsWith('/v1')) normalizedUrl += '/v1';
871
914
  try {
872
915
  let existing = '';
873
- const escapedProviderKey = providerKey.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
874
916
  if (fs.existsSync(configPath)) {
875
917
  existing = fs.readFileSync(configPath, 'utf8');
876
918
  // 移除旧的 maxapi section(provider block)
@@ -879,8 +921,11 @@ function writeCodexConfig(baseUrl, apiKey, modelId = 'gpt-5.4') {
879
921
  // 移除旧的 maxapi top-level block
880
922
  const topRe = new RegExp(`${topMarker.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}[\\s\\S]*?${topMarkerEnd.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\n?`, 'g');
881
923
  existing = existing.replace(topRe, '');
882
- // 兼容旧版:移除历史遗留的未标记 yunyi-codex provider、旧 openclaw-relay、旧 yunyi opencode 标记块
883
- existing = existing.replace(new RegExp(`\\[model_providers\\.${escapedProviderKey}\\]\\n(?:(?!\\[)[^\\n]*\\n?)*`, 'g'), '');
924
+ // 兼容旧版:移除历史遗留的 provider block、旧 openclaw-relay、旧 yunyi opencode 标记块
925
+ for (const removableProviderKey of removableProviderKeys) {
926
+ const escapedProviderKey = removableProviderKey.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
927
+ existing = existing.replace(new RegExp(`\\[model_providers\\.${escapedProviderKey}\\]\\n(?:(?!\\[)[^\\n]*\\n?)*`, 'g'), '');
928
+ }
884
929
  existing = existing.replace(/\[model_providers\.openclaw-relay\]\n(?:(?!\[)[^\n]*\n?)*/g, '');
885
930
  existing = existing.replace(/# >>> yunyi opencode >>>[\s\S]*?# <<< yunyi opencode <<<\n?/g, '');
886
931
  existing = existing.replace(/\n{3,}/g, '\n\n').trim();
@@ -900,7 +945,7 @@ function writeCodexConfig(baseUrl, apiKey, modelId = 'gpt-5.4') {
900
945
  const providerBlock = [
901
946
  marker,
902
947
  `[model_providers.${providerKey}]`,
903
- `name = "云翼 Codex"`,
948
+ `name = "${providerLabel}"`,
904
949
  `base_url = "${normalizedUrl}"`,
905
950
  `wire_api = "responses"`,
906
951
  `experimental_bearer_token = "${apiKey}"`,
@@ -931,10 +976,13 @@ function writeCodexConfig(baseUrl, apiKey, modelId = 'gpt-5.4') {
931
976
  } catch { /* 非关键,静默失败 */ }
932
977
  }
933
978
 
934
- function writeOpencodeConfig(claudeBaseUrl, codexBaseUrl, apiKey, defaultModelKey = `yunyi-claude/${getDefaultClaudeModel().id}`) {
979
+ function writeOpencodeConfig(claudeBaseUrl, codexBaseUrl, apiKey, defaultModelKey = getExternalModelKey('claude', getDefaultClaudeModel().id)) {
935
980
  const home = os.homedir();
936
981
  const claudeUrl = claudeBaseUrl.replace(/\/+$/, '');
937
982
  const codexUrl = (codexBaseUrl || '').replace(/\/+$/, '');
983
+ const claudeProviderKey = getExternalClaudeProviderKey();
984
+ const codexProviderKey = getExternalCodexProviderKey();
985
+ const brandPrefix = getProviderBrandPrefix();
938
986
 
939
987
  // ---- 1. opencode.json (CLI + 桌面版) ----
940
988
  const configDir = process.platform === 'win32'
@@ -950,8 +998,8 @@ function writeOpencodeConfig(claudeBaseUrl, codexBaseUrl, apiKey, defaultModelKe
950
998
  if (!existing.provider) existing.provider = {};
951
999
 
952
1000
  // Claude provider (@ai-sdk/anthropic)
953
- existing.provider['yunyi-claude'] = {
954
- name: '云翼 Claude',
1001
+ existing.provider[claudeProviderKey] = {
1002
+ name: `${brandPrefix} Claude`,
955
1003
  npm: '@ai-sdk/anthropic',
956
1004
  models: buildProviderModelMap(CLAUDE_MODELS),
957
1005
  options: { apiKey, baseURL: `${claudeUrl}/v1` }
@@ -959,8 +1007,8 @@ function writeOpencodeConfig(claudeBaseUrl, codexBaseUrl, apiKey, defaultModelKe
959
1007
 
960
1008
  // Codex provider (@ai-sdk/openai)
961
1009
  if (codexUrl) {
962
- existing.provider['yunyi-codex'] = {
963
- name: '云翼 Codex',
1010
+ existing.provider[codexProviderKey] = {
1011
+ name: `${brandPrefix} Codex`,
964
1012
  npm: '@ai-sdk/openai',
965
1013
  models: buildProviderModelMap(CODEX_MODELS),
966
1014
  options: { apiKey, baseURL: codexUrl }
@@ -978,7 +1026,8 @@ function writeOpencodeConfig(claudeBaseUrl, codexBaseUrl, apiKey, defaultModelKe
978
1026
 
979
1027
  // 从 disabled_providers 中移除我们的 provider
980
1028
  if (Array.isArray(existing.disabled_providers)) {
981
- existing.disabled_providers = existing.disabled_providers.filter(p => p !== 'yunyi-claude' && p !== 'yunyi-codex');
1029
+ const enabledProviders = new Set([claudeProviderKey, codexProviderKey, 'yunyi-claude', 'yunyi-codex', 'maxapi', 'maxapi-codex']);
1030
+ existing.disabled_providers = existing.disabled_providers.filter(p => !enabledProviders.has(p));
982
1031
  if (existing.disabled_providers.length === 0) delete existing.disabled_providers;
983
1032
  }
984
1033
 
@@ -1036,7 +1085,7 @@ function syncExternalTools(type, baseUrl, apiKey, extra = {}) {
1036
1085
  synced.push('Codex CLI config');
1037
1086
  }
1038
1087
  if (type === 'claude' && extra.codexBaseUrl) {
1039
- writeOpencodeConfig(baseUrl, extra.codexBaseUrl, apiKey, extra.opencodeDefaultModelKey || `yunyi-claude/${extra.claudeModelId || getDefaultClaudeModel().id}`);
1088
+ writeOpencodeConfig(baseUrl, extra.codexBaseUrl, apiKey, extra.opencodeDefaultModelKey || getExternalModelKey('claude', extra.claudeModelId || getDefaultClaudeModel().id));
1040
1089
  synced.push('Opencode config');
1041
1090
  }
1042
1091
  } catch { /* ignore */ }
@@ -2115,9 +2164,146 @@ function getManagedClaudeAgentId(config) {
2115
2164
  return mainAgent ? YYMAXAPI_OPENCLAW_MAIN_AGENT_ID : (sideAgent ? YYMAXAPI_OPENCLAW_ALT_CLAUDE_AGENT_ID : null);
2116
2165
  }
2117
2166
 
2167
+ function getManagedYunyiActiveType(config) {
2168
+ const defaultsPrimary = canonicalizeManagedYunyiModelKey(String(config?.agents?.defaults?.model?.primary || '').trim());
2169
+ if (isManagedYunyiClaudeModelKey(defaultsPrimary)) return 'claude';
2170
+ if (isManagedYunyiGptModelKey(defaultsPrimary)) return 'codex';
2171
+
2172
+ const mainState = getAgentModelState(findAgentById(config, YYMAXAPI_OPENCLAW_MAIN_AGENT_ID));
2173
+ const mainPrimary = canonicalizeManagedYunyiModelKey(mainState.primary || '');
2174
+ if (isManagedYunyiClaudeModelKey(mainPrimary)) return 'claude';
2175
+ if (isManagedYunyiGptModelKey(mainPrimary)) return 'codex';
2176
+
2177
+ return 'claude';
2178
+ }
2179
+
2180
+ function findManagedYunyiAgentState(config, type) {
2181
+ const candidateIds = type === 'claude'
2182
+ ? [YYMAXAPI_OPENCLAW_MAIN_AGENT_ID, YYMAXAPI_OPENCLAW_ALT_CLAUDE_AGENT_ID]
2183
+ : [YYMAXAPI_OPENCLAW_MAIN_AGENT_ID, YYMAXAPI_OPENCLAW_GPT_AGENT_ID, ...YYMAXAPI_OPENCLAW_LEGACY_GPT_AGENT_IDS];
2184
+
2185
+ const matcher = type === 'claude' ? isManagedYunyiClaudeModelKey : isManagedYunyiGptModelKey;
2186
+
2187
+ for (const agentId of candidateIds) {
2188
+ const state = getAgentModelState(findAgentById(config, agentId));
2189
+ const primary = canonicalizeManagedYunyiModelKey(state.primary || '', type);
2190
+ if (matcher(primary)) return state;
2191
+ }
2192
+
2193
+ return {};
2194
+ }
2195
+
2196
+ function getManagedYunyiDesiredPrimaryModelKey(config, type, explicitModelKey = '') {
2197
+ const explicitPrimary = canonicalizeManagedYunyiModelKey(explicitModelKey, type);
2198
+ if (type === 'claude' ? isManagedYunyiClaudeModelKey(explicitPrimary) : isManagedYunyiGptModelKey(explicitPrimary)) {
2199
+ return explicitPrimary;
2200
+ }
2201
+
2202
+ const defaultsPrimary = canonicalizeManagedYunyiModelKey(String(config?.agents?.defaults?.model?.primary || '').trim(), type);
2203
+ if (type === 'claude' ? isManagedYunyiClaudeModelKey(defaultsPrimary) : isManagedYunyiGptModelKey(defaultsPrimary)) {
2204
+ return defaultsPrimary;
2205
+ }
2206
+
2207
+ return '';
2208
+ }
2209
+
2210
+ function resolveManagedYunyiAgentAssignments(config, preferredType = '') {
2211
+ const mainAgent = findAgentById(config, YYMAXAPI_OPENCLAW_MAIN_AGENT_ID);
2212
+ const preservedMain = Boolean(mainAgent) && !isManagedMainAgent(mainAgent);
2213
+ const activeType = preferredType || getManagedYunyiActiveType(config);
2214
+
2215
+ if (preservedMain) {
2216
+ return {
2217
+ preservedMain: true,
2218
+ activeType,
2219
+ mainAgentType: '',
2220
+ claudeAgentId: YYMAXAPI_OPENCLAW_ALT_CLAUDE_AGENT_ID,
2221
+ gptAgentId: YYMAXAPI_OPENCLAW_GPT_AGENT_ID
2222
+ };
2223
+ }
2224
+
2225
+ return activeType === 'codex'
2226
+ ? {
2227
+ preservedMain: false,
2228
+ activeType,
2229
+ mainAgentType: 'codex',
2230
+ claudeAgentId: YYMAXAPI_OPENCLAW_ALT_CLAUDE_AGENT_ID,
2231
+ gptAgentId: YYMAXAPI_OPENCLAW_MAIN_AGENT_ID
2232
+ }
2233
+ : {
2234
+ preservedMain: false,
2235
+ activeType: 'claude',
2236
+ mainAgentType: 'claude',
2237
+ claudeAgentId: YYMAXAPI_OPENCLAW_MAIN_AGENT_ID,
2238
+ gptAgentId: YYMAXAPI_OPENCLAW_GPT_AGENT_ID
2239
+ };
2240
+ }
2241
+
2242
+ function syncManagedYunyiAgents(config, options = {}) {
2243
+ ensureConfigStructure(config);
2244
+
2245
+ const assignments = resolveManagedYunyiAgentAssignments(config, options.preferredType);
2246
+ const agentList = ensureAgentList(config);
2247
+ const claudeDesiredPrimary = getManagedYunyiDesiredPrimaryModelKey(config, 'claude', options.selectedModelKey);
2248
+ const gptDesiredPrimary = getManagedYunyiDesiredPrimaryModelKey(config, 'codex', options.selectedModelKey);
2249
+ const claudeState = normalizeManagedYunyiModelState('claude', {
2250
+ ...findManagedYunyiAgentState(config, 'claude'),
2251
+ ...(claudeDesiredPrimary ? { primary: claudeDesiredPrimary } : {})
2252
+ }, options);
2253
+ const gptState = normalizeManagedYunyiModelState('codex', {
2254
+ ...findManagedYunyiAgentState(config, 'codex'),
2255
+ ...(gptDesiredPrimary ? { primary: gptDesiredPrimary } : {})
2256
+ }, options);
2257
+ let changed = false;
2258
+
2259
+ if (!assignments.preservedMain) {
2260
+ const mainType = assignments.mainAgentType === 'codex' ? 'codex' : 'claude';
2261
+ const mainState = mainType === 'codex' ? gptState : claudeState;
2262
+ const mainResult = upsertManagedAgent(agentList, {
2263
+ id: YYMAXAPI_OPENCLAW_MAIN_AGENT_ID,
2264
+ default: true,
2265
+ name: mainType === 'codex' ? 'yunyi-gpt' : 'yunyi-claude',
2266
+ model: mainState
2267
+ }, isManagedMainAgent);
2268
+ if (mainResult.changed) changed = true;
2269
+ }
2270
+
2271
+ if (assignments.claudeAgentId === YYMAXAPI_OPENCLAW_ALT_CLAUDE_AGENT_ID) {
2272
+ const sideClaudeResult = upsertManagedAgent(agentList, {
2273
+ id: YYMAXAPI_OPENCLAW_ALT_CLAUDE_AGENT_ID,
2274
+ default: false,
2275
+ name: 'yunyi-claude',
2276
+ model: claudeState
2277
+ }, isManagedClaudeSideAgent);
2278
+ if (sideClaudeResult.changed) changed = true;
2279
+ } else if (removeManagedAgent(agentList, YYMAXAPI_OPENCLAW_ALT_CLAUDE_AGENT_ID, isManagedClaudeSideAgent)) {
2280
+ changed = true;
2281
+ }
2282
+
2283
+ if (assignments.gptAgentId === YYMAXAPI_OPENCLAW_GPT_AGENT_ID) {
2284
+ const gptResult = upsertManagedAgent(agentList, {
2285
+ id: YYMAXAPI_OPENCLAW_GPT_AGENT_ID,
2286
+ default: false,
2287
+ name: 'yunyi-gpt',
2288
+ model: gptState
2289
+ }, isManagedGptAgent);
2290
+ if (gptResult.changed) changed = true;
2291
+ } else if (removeManagedAgent(agentList, YYMAXAPI_OPENCLAW_GPT_AGENT_ID, isManagedGptAgent)) {
2292
+ changed = true;
2293
+ }
2294
+
2295
+ for (const legacyAgentId of YYMAXAPI_OPENCLAW_LEGACY_GPT_AGENT_IDS) {
2296
+ if (legacyAgentId === YYMAXAPI_OPENCLAW_GPT_AGENT_ID) continue;
2297
+ if (removeManagedAgent(agentList, legacyAgentId, isManagedGptAgent)) changed = true;
2298
+ }
2299
+
2300
+ return { changed, ...assignments };
2301
+ }
2302
+
2118
2303
  function inferManagedYunyiAgentIdForModelKey(config, modelKey) {
2119
- if (isManagedYunyiGptModelKey(modelKey)) return YYMAXAPI_OPENCLAW_GPT_AGENT_ID;
2120
- if (isManagedYunyiClaudeModelKey(modelKey)) return getManagedClaudeAgentId(config);
2304
+ const assignments = resolveManagedYunyiAgentAssignments(config);
2305
+ if (isManagedYunyiGptModelKey(modelKey)) return assignments.gptAgentId;
2306
+ if (isManagedYunyiClaudeModelKey(modelKey)) return assignments.claudeAgentId || getManagedClaudeAgentId(config);
2121
2307
  return null;
2122
2308
  }
2123
2309
 
@@ -2157,20 +2343,15 @@ function applyManagedYunyiModelSelection(config, selectedModelKey) {
2157
2343
  }
2158
2344
  }
2159
2345
 
2160
- const agentId = inferManagedYunyiAgentIdForModelKey(config, normalizedSelected)
2161
- || (selectedType === 'codex' ? YYMAXAPI_OPENCLAW_GPT_AGENT_ID : YYMAXAPI_OPENCLAW_ALT_CLAUDE_AGENT_ID);
2162
- const agentList = ensureAgentList(config);
2163
- const nextAgent = {
2164
- id: agentId,
2165
- default: agentId === YYMAXAPI_OPENCLAW_MAIN_AGENT_ID,
2166
- name: selectedType === 'codex' ? 'yunyi-gpt' : 'yunyi-claude',
2167
- model: normalizeManagedYunyiModelState(selectedType, { primary: normalizedSelected })
2168
- };
2169
- const shouldManage = selectedType === 'codex'
2170
- ? isManagedGptAgent
2171
- : (agentId === YYMAXAPI_OPENCLAW_MAIN_AGENT_ID ? isManagedMainAgent : isManagedClaudeSideAgent);
2172
- const updateResult = upsertManagedAgent(agentList, nextAgent, shouldManage);
2173
- if (updateResult.changed) changed = true;
2346
+ const syncResult = syncManagedYunyiAgents(config, {
2347
+ preferredType: selectedType,
2348
+ selectedModelKey: normalizedSelected
2349
+ });
2350
+ if (syncResult.changed) changed = true;
2351
+
2352
+ const agentId = selectedType === 'codex'
2353
+ ? syncResult.gptAgentId
2354
+ : syncResult.claudeAgentId;
2174
2355
 
2175
2356
  return { changed, selectedModelKey: normalizedSelected, agentId };
2176
2357
  }
@@ -2544,45 +2725,6 @@ function applyManagedYunyiOpenClawLayout(config, options = {}) {
2544
2725
  }
2545
2726
  }
2546
2727
 
2547
- const agentList = ensureAgentList(config);
2548
- const currentMainAgentState = getAgentModelState(findAgentById(config, YYMAXAPI_OPENCLAW_MAIN_AGENT_ID));
2549
- const mainAgentResult = upsertManagedAgent(agentList, {
2550
- id: YYMAXAPI_OPENCLAW_MAIN_AGENT_ID,
2551
- default: true,
2552
- name: 'yunyi-claude',
2553
- model: normalizeManagedYunyiModelState('claude', currentMainAgentState, options)
2554
- }, isManagedMainAgent);
2555
- if (mainAgentResult.changed) changed = true;
2556
-
2557
- let claudeAgentId = YYMAXAPI_OPENCLAW_MAIN_AGENT_ID;
2558
- let preservedMain = !mainAgentResult.managed;
2559
- if (!mainAgentResult.managed) {
2560
- const currentClaudeSideState = getAgentModelState(findAgentById(config, YYMAXAPI_OPENCLAW_ALT_CLAUDE_AGENT_ID));
2561
- const fallbackClaudeAgentResult = upsertManagedAgent(agentList, {
2562
- id: YYMAXAPI_OPENCLAW_ALT_CLAUDE_AGENT_ID,
2563
- default: false,
2564
- name: 'yunyi-claude',
2565
- model: normalizeManagedYunyiModelState('claude', currentClaudeSideState, options)
2566
- }, isManagedClaudeSideAgent);
2567
- if (fallbackClaudeAgentResult.changed) changed = true;
2568
- claudeAgentId = fallbackClaudeAgentResult.managed ? YYMAXAPI_OPENCLAW_ALT_CLAUDE_AGENT_ID : null;
2569
- } else if (removeManagedAgent(agentList, YYMAXAPI_OPENCLAW_ALT_CLAUDE_AGENT_ID, isManagedClaudeSideAgent)) {
2570
- changed = true;
2571
- }
2572
-
2573
- const currentGptAgentState = getAgentModelState(findAgentById(config, YYMAXAPI_OPENCLAW_GPT_AGENT_ID));
2574
- const gptAgentResult = upsertManagedAgent(agentList, {
2575
- id: YYMAXAPI_OPENCLAW_GPT_AGENT_ID,
2576
- default: false,
2577
- name: 'yunyi-gpt',
2578
- model: normalizeManagedYunyiModelState('codex', currentGptAgentState, options)
2579
- }, isManagedGptAgent);
2580
- if (gptAgentResult.changed) changed = true;
2581
- for (const legacyAgentId of YYMAXAPI_OPENCLAW_LEGACY_GPT_AGENT_IDS) {
2582
- if (legacyAgentId === YYMAXAPI_OPENCLAW_GPT_AGENT_ID) continue;
2583
- if (removeManagedAgent(agentList, legacyAgentId, isManagedGptAgent)) changed = true;
2584
- }
2585
-
2586
2728
  if (shouldManageYunyiDefaults(config)) {
2587
2729
  const nextDefaultsModel = normalizeManagedYunyiDefaultsModel(config.agents.defaults.model, options);
2588
2730
  if (JSON.stringify(config.agents.defaults.model) !== JSON.stringify(nextDefaultsModel)) {
@@ -2604,14 +2746,26 @@ function applyManagedYunyiOpenClawLayout(config, options = {}) {
2604
2746
  changed = true;
2605
2747
  }
2606
2748
 
2607
- return { changed, applied: true, claudeAgentId, preservedMain };
2749
+ const syncResult = syncManagedYunyiAgents(config, options);
2750
+ if (syncResult.changed) changed = true;
2751
+
2752
+ return {
2753
+ changed,
2754
+ applied: true,
2755
+ claudeAgentId: syncResult.claudeAgentId,
2756
+ gptAgentId: syncResult.gptAgentId,
2757
+ preservedMain: syncResult.preservedMain,
2758
+ mainAgentType: syncResult.mainAgentType
2759
+ };
2608
2760
  }
2609
2761
 
2610
2762
  function printYunyiOpenClawSwitchHint(result = {}) {
2611
2763
  if (!result?.applied) return;
2612
2764
  const summary = result.preservedMain && result.claudeAgentId === YYMAXAPI_OPENCLAW_ALT_CLAUDE_AGENT_ID
2613
2765
  ? 'OpenClaw: main 已保留, Claude=yunyi-claude, GPT=yunyi-gpt'
2614
- : 'OpenClaw: main=yunyi-claude, GPT=yunyi-gpt';
2766
+ : (result.mainAgentType === 'codex'
2767
+ ? 'OpenClaw: main=yunyi-gpt, Claude=yunyi-claude'
2768
+ : 'OpenClaw: main=yunyi-claude, GPT=yunyi-gpt');
2615
2769
  console.log(chalk.cyan(` ${summary}`));
2616
2770
  }
2617
2771
 
@@ -3294,7 +3448,7 @@ function readOpencodeCliConfig() {
3294
3448
  const config = readJsonIfExists(configPath) || {};
3295
3449
  return {
3296
3450
  configPath,
3297
- modelKey: config.model || `yunyi-claude/${getDefaultClaudeModel().id}`,
3451
+ modelKey: config.model || getExternalModelKey('claude', getDefaultClaudeModel().id),
3298
3452
  configured: fs.existsSync(configPath),
3299
3453
  config
3300
3454
  };
@@ -3305,7 +3459,7 @@ function readCodexCliConfig() {
3305
3459
  const configRaw = fs.existsSync(configPath) ? fs.readFileSync(configPath, 'utf8') : '';
3306
3460
  const auth = readJsonIfExists(authPath) || {};
3307
3461
  const model = (configRaw.match(/^model\s*=\s*"([^"]+)"\s*$/m) || [])[1] || getDefaultCodexModel().id;
3308
- const provider = (configRaw.match(/^model_provider\s*=\s*"([^"]+)"\s*$/m) || [])[1] || 'yunyi-codex';
3462
+ const provider = (configRaw.match(/^model_provider\s*=\s*"([^"]+)"\s*$/m) || [])[1] || getExternalCodexProviderKey();
3309
3463
  const providerBlockRegex = new RegExp(`\\[model_providers\\.${escapeRegExp(provider)}\\]([\\s\\S]*?)(?=\\n\\[|$)`, 'm');
3310
3464
  const providerBlock = (configRaw.match(providerBlockRegex) || [])[1] || '';
3311
3465
  const baseUrl = (providerBlock.match(/base_url\s*=\s*"([^"]+)"/) || [])[1] || '';
@@ -3815,6 +3969,22 @@ async function tryAutoStartGateway(port, allowAutoDaemon) {
3815
3969
  }
3816
3970
  }
3817
3971
 
3972
+ if (process.platform !== 'win32') {
3973
+ for (const sh of ['/bin/zsh', '/bin/bash']) {
3974
+ if (!fs.existsSync(sh)) continue;
3975
+ for (const name of ['openclaw', 'clawdbot', 'moltbot']) {
3976
+ const loginShellCmd = `${sh} -lc '${name} gateway'`;
3977
+ console.log(chalk.yellow(`⚠️ 尝试启动 Gateway: ${loginShellCmd}`));
3978
+ if (spawnDetached(loginShellCmd, env)) {
3979
+ if (await waitForGateway(port, '127.0.0.1', 15000)) {
3980
+ console.log(chalk.green('✅ Gateway 已通过 login shell 启动'));
3981
+ return { started: true, method: 'cli-login-shell', cmd: loginShellCmd };
3982
+ }
3983
+ }
3984
+ }
3985
+ }
3986
+ }
3987
+
3818
3988
  return { started: false };
3819
3989
  }
3820
3990
 
@@ -4264,7 +4434,7 @@ async function presetClaude(paths, args = {}) {
4264
4434
  if (yunyiLayoutResult.applied) {
4265
4435
  syncManagedYunyiAuthProfiles(paths, config);
4266
4436
  }
4267
- syncExternalTools('claude', baseUrl, apiKey, { claudeModelId: modelId, opencodeDefaultModelKey: `yunyi-claude/${modelId}` });
4437
+ syncExternalTools('claude', baseUrl, apiKey, { claudeModelId: modelId, opencodeDefaultModelKey: getExternalModelKey('claude', modelId) });
4268
4438
  writeSpinner.succeed('配置写入完成');
4269
4439
 
4270
4440
  console.log(chalk.green('\n✅ Claude 节点配置完成!'));
@@ -4516,15 +4686,14 @@ async function autoActivate(paths, args = {}) {
4516
4686
  }
4517
4687
 
4518
4688
  // ---- 选模型(Claude + GPT 合并展示) ----
4519
- const isSingleMode = CLAUDE_MODELS.length === 1 && CODEX_MODELS.length === 1
4520
- && CLAUDE_MODELS[0].id === CODEX_MODELS[0].id;
4689
+ const isSingleMode = isUnifiedSingleModelMode();
4521
4690
 
4522
4691
  let selectedModelId;
4523
4692
  let selectedType; // 'claude' or 'codex'
4524
4693
 
4525
4694
  if (isSingleMode) {
4526
4695
  selectedModelId = CLAUDE_MODELS[0].id;
4527
- selectedType = 'claude';
4696
+ selectedType = String(args.primary || '').trim() === 'codex' ? 'codex' : 'claude';
4528
4697
  } else {
4529
4698
  const modelArg = (args.model || args['claude-model'] || args['codex-model'] || '').toString().trim();
4530
4699
  if (modelArg) {
@@ -4620,15 +4789,18 @@ async function autoActivate(paths, args = {}) {
4620
4789
 
4621
4790
  // 主模型 = 用户选的,备选 = 另一个
4622
4791
  const primaryModelKey = isClaudePrimary ? claudeModelKey : codexModelKey;
4623
- const fallbackModelKey = isClaudePrimary ? codexModelKey : claudeModelKey;
4792
+ const fallbackModelKey = isSingleMode ? '' : (isClaudePrimary ? codexModelKey : claudeModelKey);
4624
4793
  config.agents.defaults.model.primary = primaryModelKey;
4625
- config.agents.defaults.model.fallbacks = [fallbackModelKey];
4794
+ config.agents.defaults.model.fallbacks = fallbackModelKey ? [fallbackModelKey] : [];
4626
4795
  const yunyiLayoutResult = applyManagedYunyiOpenClawLayout(config, {
4627
4796
  force: true,
4628
4797
  endpointUrl: selectedEndpoint.url,
4629
4798
  apiKey
4630
4799
  });
4631
- applyManagedYunyiModelSelection(config, primaryModelKey);
4800
+ const selectionResult = applyManagedYunyiModelSelection(config, primaryModelKey);
4801
+ const finalYunyiLayoutResult = yunyiLayoutResult.applied
4802
+ ? { ...yunyiLayoutResult, ...resolveManagedYunyiAgentAssignments(config, selectedType), applied: true }
4803
+ : yunyiLayoutResult;
4632
4804
 
4633
4805
  // ---- 写入配置 ----
4634
4806
  const writeSpinner = ora({ text: '正在写入配置...', spinner: 'dots' }).start();
@@ -4641,13 +4813,13 @@ async function autoActivate(paths, args = {}) {
4641
4813
  if (yunyiLayoutResult.applied) {
4642
4814
  syncManagedYunyiAuthProfiles(paths, config);
4643
4815
  }
4644
- if (!isClaudePrimary) {
4645
- const resetResult = resetManagedAgentSessionsWithSync(paths, YYMAXAPI_OPENCLAW_GPT_AGENT_ID);
4816
+ if (selectionResult.agentId) {
4817
+ const resetResult = resetManagedAgentSessionsWithSync(paths, selectionResult.agentId);
4646
4818
  if (resetResult.changed) {
4647
- console.log(chalk.gray(` 已重置 ${YYMAXAPI_OPENCLAW_GPT_AGENT_ID} 的活动会话映射`));
4819
+ console.log(chalk.gray(` 已重置 ${selectionResult.agentId} 的活动会话映射`));
4648
4820
  }
4649
4821
  }
4650
- const opencodeDefaultModelKey = isClaudePrimary ? `yunyi-claude/${claudeModelId}` : `yunyi-codex/${codexModelId}`;
4822
+ const opencodeDefaultModelKey = isClaudePrimary ? getExternalModelKey('claude', claudeModelId) : getExternalModelKey('codex', codexModelId);
4651
4823
  try { syncExternalTools('claude', claudeBaseUrl, apiKey, { codexBaseUrl, claudeModelId, opencodeDefaultModelKey }); } catch { /* ignore */ }
4652
4824
  try { syncExternalTools('codex', codexBaseUrl, apiKey, { modelId: codexModelId }); } catch { /* ignore */ }
4653
4825
  writeSpinner.succeed('配置写入完成');
@@ -4656,7 +4828,7 @@ async function autoActivate(paths, args = {}) {
4656
4828
  const selectedModel = isClaudePrimary ? claudeModel : codexModel;
4657
4829
  console.log(chalk.green('\n✅ 配置完成!'));
4658
4830
  console.log(chalk.cyan(` 外部工具默认: ${selectedModel.name}`));
4659
- printYunyiOpenClawSwitchHint(yunyiLayoutResult);
4831
+ printYunyiOpenClawSwitchHint(finalYunyiLayoutResult);
4660
4832
 
4661
4833
  const gwPort = config.gateway?.port || 18789;
4662
4834
  const gwToken = config.gateway?.auth?.token;
@@ -4677,9 +4849,10 @@ async function autoActivate(paths, args = {}) {
4677
4849
  }]);
4678
4850
 
4679
4851
  if (nextAction === 'test') {
4680
- const selectedOpenClawAgentId = isClaudePrimary
4681
- ? (yunyiLayoutResult.claudeAgentId || YYMAXAPI_OPENCLAW_MAIN_AGENT_ID)
4682
- : YYMAXAPI_OPENCLAW_GPT_AGENT_ID;
4852
+ const selectedOpenClawAgentId = selectionResult.agentId
4853
+ || (isClaudePrimary
4854
+ ? (finalYunyiLayoutResult.claudeAgentId || YYMAXAPI_OPENCLAW_MAIN_AGENT_ID)
4855
+ : (finalYunyiLayoutResult.gptAgentId || YYMAXAPI_OPENCLAW_GPT_AGENT_ID));
4683
4856
  await testConnection(paths, { ...args, agent: selectedOpenClawAgentId });
4684
4857
  } else {
4685
4858
  console.log(chalk.cyan('👋 再见!\n'));
@@ -5096,7 +5269,7 @@ async function yycodeQuickSetup(paths) {
5096
5269
  if (yunyiLayoutResult.applied) {
5097
5270
  syncManagedYunyiAuthProfiles(paths, config);
5098
5271
  }
5099
- try { syncExternalTools('claude', claudeBaseUrl, apiKey, { codexBaseUrl, claudeModelId, opencodeDefaultModelKey: `yunyi-codex/${codexModelId}` }); } catch { /* ignore */ }
5272
+ try { syncExternalTools('claude', claudeBaseUrl, apiKey, { codexBaseUrl, claudeModelId, opencodeDefaultModelKey: getExternalModelKey('codex', codexModelId) }); } catch { /* ignore */ }
5100
5273
  try { syncExternalTools('codex', codexBaseUrl, apiKey, { modelId: codexModelId }); } catch { /* ignore */ }
5101
5274
  writeSpinner.succeed('配置写入完成');
5102
5275
 
@@ -5250,6 +5423,8 @@ function getConfigStatusLine(paths) {
5250
5423
  'claude-yunyi': 'Claude(包月)',
5251
5424
  'yunyi': 'Codex(包月)',
5252
5425
  'heibai': 'MAXAPI(按量)',
5426
+ 'maxapi': 'MAXAPI(按量)',
5427
+ 'maxapi-codex': 'MAXAPI(按量)',
5253
5428
  };
5254
5429
 
5255
5430
  const parts = [];
@@ -5460,6 +5635,8 @@ async function switchModel(paths) {
5460
5635
  'claude-yunyi': '云翼 Claude (包月)',
5461
5636
  'yunyi': '云翼 Codex (包月)',
5462
5637
  'heibai': 'MAXAPI (按量)',
5638
+ 'maxapi': 'MAXAPI (按量)',
5639
+ 'maxapi-codex': 'MAXAPI (按量)',
5463
5640
  };
5464
5641
 
5465
5642
  const claudeProviderName = API_CONFIG.claude.providerName;
@@ -5468,8 +5645,25 @@ async function switchModel(paths) {
5468
5645
  // 预设模型列表
5469
5646
  const choices = [];
5470
5647
  const presetKeys = new Set();
5471
-
5472
- if (CLAUDE_MODELS.length > 0) {
5648
+ const unifiedSingleMode = isUnifiedSingleModelMode();
5649
+ const unifiedModel = unifiedSingleMode ? (CLAUDE_MODELS[0] || CODEX_MODELS[0] || null) : null;
5650
+
5651
+ if (unifiedSingleMode && unifiedModel) {
5652
+ const preferredProviders = [
5653
+ primary.split('/')[0],
5654
+ providers[claudeProviderName] ? claudeProviderName : '',
5655
+ providers[codexProviderName] ? codexProviderName : '',
5656
+ Object.keys(providers)[0] || ''
5657
+ ].filter(Boolean);
5658
+ const unifiedProviderName = preferredProviders[0];
5659
+ const modelKey = `${unifiedProviderName}/${unifiedModel.id}`;
5660
+ const isCurrent = modelKey === primary;
5661
+ choices.push({
5662
+ name: isCurrent ? `${unifiedModel.name} (当前)` : unifiedModel.name,
5663
+ value: modelKey,
5664
+ });
5665
+ presetKeys.add(modelKey);
5666
+ } else if (CLAUDE_MODELS.length > 0) {
5473
5667
  choices.push(new inquirer.Separator(' -- Claude --'));
5474
5668
  for (const m of CLAUDE_MODELS) {
5475
5669
  const pName = providers[claudeProviderName] ? claudeProviderName : Object.keys(providers)[0];
@@ -5483,7 +5677,7 @@ async function switchModel(paths) {
5483
5677
  }
5484
5678
  }
5485
5679
 
5486
- if (CODEX_MODELS.length > 0) {
5680
+ if (!unifiedSingleMode && CODEX_MODELS.length > 0) {
5487
5681
  choices.push(new inquirer.Separator(' -- GPT --'));
5488
5682
  for (const m of CODEX_MODELS) {
5489
5683
  const pName = providers[codexProviderName] ? codexProviderName : Object.keys(providers)[0];
@@ -5501,6 +5695,12 @@ async function switchModel(paths) {
5501
5695
  const otherModels = [];
5502
5696
  for (const [providerName, providerConfig] of Object.entries(providers)) {
5503
5697
  for (const m of (providerConfig.models || [])) {
5698
+ if (unifiedSingleMode
5699
+ && unifiedModel
5700
+ && m.id === unifiedModel.id
5701
+ && (providerName === claudeProviderName || providerName === codexProviderName)) {
5702
+ continue;
5703
+ }
5504
5704
  const modelKey = `${providerName}/${m.id}`;
5505
5705
  if (!presetKeys.has(modelKey)) {
5506
5706
  otherModels.push({ modelKey, name: m.name || m.id, providerName });
@@ -5509,6 +5709,12 @@ async function switchModel(paths) {
5509
5709
  }
5510
5710
  const registeredKeys = Object.keys(config.agents?.defaults?.models || {});
5511
5711
  for (const modelKey of registeredKeys) {
5712
+ if (unifiedSingleMode && unifiedModel) {
5713
+ const [providerName, modelId] = modelKey.split('/');
5714
+ if (modelId === unifiedModel.id && (providerName === claudeProviderName || providerName === codexProviderName)) {
5715
+ continue;
5716
+ }
5717
+ }
5512
5718
  if (presetKeys.has(modelKey) || otherModels.some(o => o.modelKey === modelKey)) continue;
5513
5719
  const [pName, mId] = modelKey.split('/');
5514
5720
  if (!pName || !mId) continue;
@@ -5619,7 +5825,7 @@ async function switchModel(paths) {
5619
5825
  primary = finalSelected;
5620
5826
  ensureGatewaySettings(config);
5621
5827
  writeConfigWithSync(paths, config);
5622
- if (selectedAgentId === YYMAXAPI_OPENCLAW_GPT_AGENT_ID) {
5828
+ if (selectedAgentId && [YYMAXAPI_OPENCLAW_MAIN_AGENT_ID, YYMAXAPI_OPENCLAW_ALT_CLAUDE_AGENT_ID, YYMAXAPI_OPENCLAW_GPT_AGENT_ID].includes(selectedAgentId)) {
5623
5829
  const resetResult = resetManagedAgentSessionsWithSync(paths, selectedAgentId);
5624
5830
  if (resetResult.changed) {
5625
5831
  console.log(chalk.gray(` 已重置 ${selectedAgentId} 的活动会话映射`));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "yymaxapi",
3
- "version": "1.0.99",
3
+ "version": "1.0.101",
4
4
  "description": "跨平台 OpenClaw/Clawdbot 配置管理工具 - 管理中转地址、模型切换、API Keys、测速优化",
5
5
  "main": "bin/yymaxapi.js",
6
6
  "bin": {