yymaxapi 1.0.81 → 1.0.83

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 +477 -90
  2. package/package.json +1 -1
package/bin/yymaxapi.js CHANGED
@@ -636,6 +636,28 @@ function getWslMirrorInfo() {
636
636
  return { configPath, authProfiles };
637
637
  }
638
638
 
639
+ function getGatewayListenOwner(port = 18789) {
640
+ if (process.platform === 'win32') return null;
641
+
642
+ const lsofResult = safeExec(`lsof -nP -iTCP:${port} -sTCP:LISTEN -F u 2>/dev/null`, { timeout: 3000 });
643
+ const lsofOutput = [lsofResult.output, lsofResult.stdout, lsofResult.stderr].filter(Boolean).join('\n');
644
+ const userLine = lsofOutput
645
+ .split('\n')
646
+ .map(line => line.trim())
647
+ .find(line => line.startsWith('u'));
648
+ if (userLine && userLine.length > 1) {
649
+ return userLine.slice(1).trim() || null;
650
+ }
651
+
652
+ const pidResult = safeExec(`lsof -ti :${port} 2>/dev/null | head -n1`, { timeout: 3000 });
653
+ const pid = pidResult.ok ? pidResult.output.trim().split('\n')[0] : '';
654
+ if (!/^\d+$/.test(pid)) return null;
655
+
656
+ const ownerResult = safeExec(`ps -o user= -p ${pid} 2>/dev/null`, { timeout: 3000 });
657
+ if (!ownerResult.ok || !ownerResult.output) return null;
658
+ return ownerResult.output.trim().split(/\s+/)[0] || null;
659
+ }
660
+
639
661
  function getConfigPath() {
640
662
  const homeDir = os.homedir();
641
663
  const openclawStateDir = process.env.OPENCLAW_STATE_DIR || path.join(homeDir, '.openclaw');
@@ -664,23 +686,34 @@ function getConfigPath() {
664
686
 
665
687
  const candidates = [];
666
688
  if (envConfig) candidates.push(envConfig);
667
- if (preferMoltbot) {
668
- candidates.push(...moltbotCandidates, ...openclawCandidates);
669
- } else {
670
- candidates.push(...openclawCandidates, ...moltbotCandidates);
671
- }
689
+ const rootCandidates = [];
690
+ let preferRootConfig = false;
691
+
692
+ const preferredCandidates = preferMoltbot
693
+ ? [...moltbotCandidates, ...openclawCandidates]
694
+ : [...openclawCandidates, ...moltbotCandidates];
672
695
 
673
696
  // Fallback: 当前用户非 root 时,也检查 /root 下的配置(OpenClaw 常以 root 安装)
674
697
  if (process.platform !== 'win32' && homeDir !== '/root') {
675
698
  const rootOpenclawDir = '/root/.openclaw';
676
699
  const rootClawdbotDir = '/root/.clawdbot';
677
- candidates.push(
700
+ rootCandidates.push(
678
701
  path.join(rootOpenclawDir, 'openclaw.json'),
679
702
  path.join(rootOpenclawDir, 'moltbot.json'),
680
703
  path.join(rootClawdbotDir, 'openclaw.json'),
681
704
  path.join(rootClawdbotDir, 'clawdbot.json'),
682
705
  path.join(rootClawdbotDir, 'moltbot.json')
683
706
  );
707
+
708
+ const rootConfigExists = rootCandidates.some(p => p && fs.existsSync(p));
709
+ const gatewayOwner = getGatewayListenOwner();
710
+ preferRootConfig = rootConfigExists && gatewayOwner === 'root';
711
+ }
712
+
713
+ if (preferRootConfig) {
714
+ candidates.push(...rootCandidates, ...preferredCandidates);
715
+ } else {
716
+ candidates.push(...preferredCandidates, ...rootCandidates);
684
717
  }
685
718
 
686
719
  // Windows + WSL: 尝试读取 WSL 内的配置文件
@@ -714,18 +747,6 @@ function getConfigPath() {
714
747
 
715
748
  const configDir = path.dirname(openclawConfig);
716
749
 
717
- const baseAuthCandidates = buildAuthCandidates([openclawStateDir, clawdbotStateDir]);
718
- const moltbotAuthCandidates = buildAuthCandidates(moltbotStateDirs);
719
-
720
- const authCandidates = preferMoltbot
721
- ? [...moltbotAuthCandidates, ...baseAuthCandidates]
722
- : [...baseAuthCandidates, ...moltbotAuthCandidates];
723
-
724
- const authProfiles = authCandidates.find(p => fs.existsSync(p)) || authCandidates[0];
725
- const authSyncTargets = process.platform === 'win32'
726
- ? [...new Set(buildAuthCandidates([openclawStateDir, clawdbotStateDir]))].filter(p => p !== authProfiles)
727
- : [];
728
-
729
750
  const syncTargets = [];
730
751
  if (openclawConfig.startsWith(openclawStateDir) && fs.existsSync(clawdbotStateDir)) {
731
752
  syncTargets.push(
@@ -734,6 +755,20 @@ function getConfigPath() {
734
755
  );
735
756
  }
736
757
 
758
+ const primaryAuthBases = [configDir];
759
+ const secondaryAuthBases = preferMoltbot
760
+ ? [...moltbotStateDirs, openclawStateDir, clawdbotStateDir]
761
+ : [openclawStateDir, clawdbotStateDir, ...moltbotStateDirs];
762
+ const authCandidates = [...new Set(buildAuthCandidates([...primaryAuthBases, ...secondaryAuthBases]))];
763
+
764
+ const authProfiles = authCandidates.find(p => fs.existsSync(p)) || authCandidates[0];
765
+
766
+ const authSyncBaseDirs = syncTargets.map(target => path.dirname(target));
767
+ if (process.platform === 'win32') {
768
+ authSyncBaseDirs.push(openclawStateDir, clawdbotStateDir);
769
+ }
770
+ const authSyncTargets = [...new Set(buildAuthCandidates(authSyncBaseDirs))].filter(p => p !== authProfiles);
771
+
737
772
  return { openclawConfig, authProfiles, configDir, syncTargets, authSyncTargets, wslConfigPath, wslAuthProfiles };
738
773
  }
739
774
 
@@ -1454,6 +1489,351 @@ function coerceModelsRecord(value) {
1454
1489
  return record;
1455
1490
  }
1456
1491
 
1492
+ const YUNYI_PROVIDER_ALIASES = {
1493
+ claude: new Set(['claude-yunyi', 'yunyi-claude', 'claude_yunyi']),
1494
+ codex: new Set(['yunyi', 'yunyi-codex', 'codex-yunyi', 'codex_yunyi'])
1495
+ };
1496
+
1497
+ function looksLikeApiKey(value) {
1498
+ const text = String(value || '').trim();
1499
+ if (!text || text.includes('/') || text.includes(' ')) return false;
1500
+ if (/^(sk-|STP|YK)/i.test(text)) return true;
1501
+ return /^[A-Za-z0-9._-]{16,}$/.test(text);
1502
+ }
1503
+
1504
+ function isLikelyYunyiBaseUrl(baseUrl) {
1505
+ const urlText = String(baseUrl || '').trim();
1506
+ if (!urlText) return false;
1507
+ try {
1508
+ const hostname = new URL(urlText).hostname.toLowerCase();
1509
+ return hostname.includes('yunyi')
1510
+ || hostname.includes('rdzhvip.com')
1511
+ || hostname === '47.99.42.193'
1512
+ || hostname === '47.97.100.10';
1513
+ } catch {
1514
+ return /yunyi|rdzhvip\.com|47\.99\.42\.193|47\.97\.100\.10/i.test(urlText);
1515
+ }
1516
+ }
1517
+
1518
+ function isYunyiProviderEntry(name, providerConfig, type) {
1519
+ const cfg = providerConfig || {};
1520
+ const providerName = String(name || '').trim();
1521
+ if (YUNYI_PROVIDER_ALIASES[type]?.has(providerName)) return true;
1522
+
1523
+ const baseUrl = String(cfg.baseUrl || cfg.base_url || '').trim();
1524
+ const api = String(cfg.api || '').trim();
1525
+ if (!isLikelyYunyiBaseUrl(baseUrl)) return false;
1526
+
1527
+ if (type === 'claude') {
1528
+ return api.startsWith('anthropic') || /\/claude(?:\/|$)/.test(baseUrl);
1529
+ }
1530
+ return api.startsWith('openai') || /\/codex(?:\/|$)/.test(baseUrl);
1531
+ }
1532
+
1533
+ function normalizeProviderModelEntry(model, fallbackContext = {}) {
1534
+ if (!model) return null;
1535
+ if (typeof model === 'string') {
1536
+ return {
1537
+ id: model,
1538
+ name: model,
1539
+ contextWindow: fallbackContext.contextWindow,
1540
+ maxTokens: fallbackContext.maxTokens
1541
+ };
1542
+ }
1543
+ if (typeof model !== 'object') return null;
1544
+
1545
+ const id = String(model.id || model.model || '').trim();
1546
+ if (!id) return null;
1547
+ return {
1548
+ ...model,
1549
+ id,
1550
+ name: model.name || id,
1551
+ contextWindow: model.contextWindow || fallbackContext.contextWindow,
1552
+ maxTokens: model.maxTokens || fallbackContext.maxTokens
1553
+ };
1554
+ }
1555
+
1556
+ function mergeProviderModels(existingModels = [], incomingModels = [], fallbackContext = {}) {
1557
+ const merged = [];
1558
+ const seen = new Set();
1559
+
1560
+ for (const item of [...existingModels, ...incomingModels]) {
1561
+ const normalized = normalizeProviderModelEntry(item, fallbackContext);
1562
+ if (!normalized || seen.has(normalized.id)) continue;
1563
+ seen.add(normalized.id);
1564
+ merged.push(normalized);
1565
+ }
1566
+
1567
+ return merged;
1568
+ }
1569
+
1570
+ function renameProviderReferencesInConfig(config, oldName, newName) {
1571
+ if (!oldName || !newName || oldName === newName) return false;
1572
+ let changed = false;
1573
+
1574
+ if (config?.agents?.defaults?.models) {
1575
+ for (const key of Object.keys(config.agents.defaults.models)) {
1576
+ if (!key.startsWith(`${oldName}/`)) continue;
1577
+ const nextKey = `${newName}/${key.slice(oldName.length + 1)}`;
1578
+ if (!config.agents.defaults.models[nextKey]) {
1579
+ const entry = { ...(config.agents.defaults.models[key] || {}) };
1580
+ if (!entry.alias || entry.alias === oldName) entry.alias = newName;
1581
+ config.agents.defaults.models[nextKey] = entry;
1582
+ }
1583
+ delete config.agents.defaults.models[key];
1584
+ changed = true;
1585
+ }
1586
+ }
1587
+
1588
+ if (config?.agents?.defaults?.model) {
1589
+ const modelState = config.agents.defaults.model;
1590
+ if (typeof modelState.primary === 'string' && modelState.primary.startsWith(`${oldName}/`)) {
1591
+ modelState.primary = `${newName}/${modelState.primary.slice(oldName.length + 1)}`;
1592
+ changed = true;
1593
+ }
1594
+ if (Array.isArray(modelState.fallbacks)) {
1595
+ const nextFallbacks = modelState.fallbacks.map(modelKey => (
1596
+ typeof modelKey === 'string' && modelKey.startsWith(`${oldName}/`)
1597
+ ? `${newName}/${modelKey.slice(oldName.length + 1)}`
1598
+ : modelKey
1599
+ ));
1600
+ if (JSON.stringify(nextFallbacks) !== JSON.stringify(modelState.fallbacks)) {
1601
+ modelState.fallbacks = nextFallbacks;
1602
+ changed = true;
1603
+ }
1604
+ }
1605
+ }
1606
+
1607
+ if (config?.auth?.profiles) {
1608
+ for (const key of Object.keys(config.auth.profiles)) {
1609
+ if (!key.startsWith(`${oldName}:`)) continue;
1610
+ const suffix = key.slice(oldName.length + 1);
1611
+ const nextKey = `${newName}:${suffix}`;
1612
+ const profile = { ...(config.auth.profiles[key] || {}) };
1613
+ profile.provider = newName;
1614
+ config.auth.profiles[nextKey] = profile;
1615
+ delete config.auth.profiles[key];
1616
+ changed = true;
1617
+ }
1618
+ }
1619
+
1620
+ return changed;
1621
+ }
1622
+
1623
+ function suggestConflictProviderName(config, baseName) {
1624
+ const providers = config?.models?.providers || {};
1625
+ let candidate = `${baseName}-legacy`;
1626
+ let index = 2;
1627
+ while (providers[candidate]) {
1628
+ candidate = `${baseName}-legacy-${index}`;
1629
+ index += 1;
1630
+ }
1631
+ return candidate;
1632
+ }
1633
+
1634
+ function sanitizeProviderConfig(name, providerConfig, typeHint = null) {
1635
+ if (!providerConfig || typeof providerConfig !== 'object') return false;
1636
+ let changed = false;
1637
+
1638
+ if (providerConfig.base_url && !providerConfig.baseUrl) {
1639
+ providerConfig.baseUrl = providerConfig.base_url;
1640
+ delete providerConfig.base_url;
1641
+ changed = true;
1642
+ }
1643
+
1644
+ if (providerConfig.model && !providerConfig.models) {
1645
+ providerConfig.models = [providerConfig.model];
1646
+ delete providerConfig.model;
1647
+ changed = true;
1648
+ }
1649
+
1650
+ const inferredType = typeHint
1651
+ || (String(providerConfig.api || '').startsWith('anthropic') ? 'claude'
1652
+ : String(providerConfig.api || '').startsWith('openai') ? 'codex'
1653
+ : null);
1654
+ const fallbackContext = inferredType === 'claude'
1655
+ ? { contextWindow: API_CONFIG.claude?.contextWindow, maxTokens: API_CONFIG.claude?.maxTokens }
1656
+ : inferredType === 'codex'
1657
+ ? { contextWindow: API_CONFIG.codex?.contextWindow, maxTokens: API_CONFIG.codex?.maxTokens }
1658
+ : {};
1659
+
1660
+ if (providerConfig.models) {
1661
+ const nextModels = mergeProviderModels(providerConfig.models, [], fallbackContext);
1662
+ if (JSON.stringify(nextModels) !== JSON.stringify(providerConfig.models)) {
1663
+ providerConfig.models = nextModels;
1664
+ changed = true;
1665
+ }
1666
+ }
1667
+
1668
+ if (!providerConfig.auth && providerConfig.apiKey) {
1669
+ providerConfig.auth = DEFAULT_AUTH_MODE;
1670
+ changed = true;
1671
+ } else if (typeof providerConfig.auth === 'string') {
1672
+ const authValue = providerConfig.auth.trim();
1673
+ if (authValue === 'api_key') {
1674
+ providerConfig.auth = DEFAULT_AUTH_MODE;
1675
+ changed = true;
1676
+ } else if (!['api-key', 'token', 'none'].includes(authValue)) {
1677
+ if (!providerConfig.apiKey && looksLikeApiKey(authValue)) {
1678
+ providerConfig.apiKey = authValue;
1679
+ providerConfig.auth = DEFAULT_AUTH_MODE;
1680
+ changed = true;
1681
+ } else if (providerConfig.apiKey) {
1682
+ providerConfig.auth = DEFAULT_AUTH_MODE;
1683
+ changed = true;
1684
+ }
1685
+ }
1686
+ }
1687
+
1688
+ if (!providerConfig.headers || Array.isArray(providerConfig.headers) || typeof providerConfig.headers !== 'object') {
1689
+ providerConfig.headers = {};
1690
+ changed = true;
1691
+ }
1692
+
1693
+ if (providerConfig.authHeader === undefined) {
1694
+ if (String(providerConfig.api || '').startsWith('openai')) {
1695
+ providerConfig.authHeader = true;
1696
+ changed = true;
1697
+ } else if (String(providerConfig.api || '').startsWith('anthropic')) {
1698
+ providerConfig.authHeader = false;
1699
+ changed = true;
1700
+ }
1701
+ }
1702
+
1703
+ return changed;
1704
+ }
1705
+
1706
+ function sanitizeConfigAuthProfiles(config) {
1707
+ if (!config.auth) config.auth = {};
1708
+ if (!config.auth.profiles || typeof config.auth.profiles !== 'object' || Array.isArray(config.auth.profiles)) {
1709
+ config.auth.profiles = {};
1710
+ return true;
1711
+ }
1712
+
1713
+ let changed = false;
1714
+ for (const [key, profile] of Object.entries(config.auth.profiles)) {
1715
+ const provider = (profile && typeof profile === 'object' && profile.provider) || key.split(':')[0];
1716
+ if (!provider) {
1717
+ delete config.auth.profiles[key];
1718
+ changed = true;
1719
+ continue;
1720
+ }
1721
+ const nextProfile = {
1722
+ provider,
1723
+ mode: (profile && typeof profile === 'object' && (profile.mode === 'api-key' ? 'api_key' : profile.mode)) || 'api_key'
1724
+ };
1725
+ if (JSON.stringify(profile) !== JSON.stringify(nextProfile)) {
1726
+ config.auth.profiles[key] = nextProfile;
1727
+ changed = true;
1728
+ }
1729
+ }
1730
+ return changed;
1731
+ }
1732
+
1733
+ function repairYunyiProviderAliases(config) {
1734
+ if (!config?.models?.providers) return { changed: false, renamedProviders: [] };
1735
+
1736
+ let changed = false;
1737
+ const renamedProviders = [];
1738
+ const providers = config.models.providers;
1739
+
1740
+ for (const type of ['claude', 'codex']) {
1741
+ const canonicalName = API_CONFIG[type]?.providerName;
1742
+ if (!canonicalName) continue;
1743
+
1744
+ const candidateEntries = Object.entries(providers).filter(([name, providerConfig]) => isYunyiProviderEntry(name, providerConfig, type));
1745
+ if (candidateEntries.length === 0) continue;
1746
+
1747
+ if (!providers[canonicalName]) {
1748
+ providers[canonicalName] = {};
1749
+ changed = true;
1750
+ }
1751
+
1752
+ sanitizeProviderConfig(canonicalName, providers[canonicalName], type);
1753
+
1754
+ for (const [name, providerConfig] of candidateEntries) {
1755
+ sanitizeProviderConfig(name, providerConfig, type);
1756
+ if (name === canonicalName) continue;
1757
+
1758
+ const target = providers[canonicalName];
1759
+ if (!target.baseUrl && providerConfig.baseUrl) target.baseUrl = providerConfig.baseUrl;
1760
+ if (!target.api && providerConfig.api) target.api = providerConfig.api;
1761
+ if (!target.apiKey && providerConfig.apiKey) target.apiKey = providerConfig.apiKey;
1762
+ if (!target.auth && providerConfig.auth) target.auth = providerConfig.auth;
1763
+ if (target.authHeader === undefined && providerConfig.authHeader !== undefined) target.authHeader = providerConfig.authHeader;
1764
+ if ((!target.headers || Object.keys(target.headers).length === 0) && providerConfig.headers && typeof providerConfig.headers === 'object') {
1765
+ target.headers = { ...providerConfig.headers };
1766
+ }
1767
+ target.models = mergeProviderModels(target.models, providerConfig.models, type === 'claude'
1768
+ ? { contextWindow: API_CONFIG.claude?.contextWindow, maxTokens: API_CONFIG.claude?.maxTokens }
1769
+ : { contextWindow: API_CONFIG.codex?.contextWindow, maxTokens: API_CONFIG.codex?.maxTokens });
1770
+
1771
+ delete providers[name];
1772
+ renamedProviders.push({ from: name, to: canonicalName });
1773
+ renameProviderReferencesInConfig(config, name, canonicalName);
1774
+ changed = true;
1775
+ }
1776
+
1777
+ sanitizeProviderConfig(canonicalName, providers[canonicalName], type);
1778
+ }
1779
+
1780
+ return { changed, renamedProviders };
1781
+ }
1782
+
1783
+ function reserveProviderName(config, desiredName, expectedType) {
1784
+ const providers = config?.models?.providers || {};
1785
+ const existing = providers[desiredName];
1786
+ if (!existing) return { changed: false, renamedProviders: [] };
1787
+ if (isYunyiProviderEntry(desiredName, existing, expectedType)) {
1788
+ return { changed: false, renamedProviders: [] };
1789
+ }
1790
+
1791
+ const nextName = suggestConflictProviderName(config, desiredName);
1792
+ providers[nextName] = existing;
1793
+ delete providers[desiredName];
1794
+ sanitizeProviderConfig(nextName, providers[nextName]);
1795
+ renameProviderReferencesInConfig(config, desiredName, nextName);
1796
+ return { changed: true, renamedProviders: [{ from: desiredName, to: nextName }] };
1797
+ }
1798
+
1799
+ function repairConfigProviders(config, options = {}) {
1800
+ if (!config.models) config.models = {};
1801
+ if (!config.models.providers || typeof config.models.providers !== 'object' || Array.isArray(config.models.providers)) {
1802
+ config.models.providers = {};
1803
+ }
1804
+
1805
+ let changed = false;
1806
+ const renamedProviders = [];
1807
+
1808
+ for (const type of ['claude', 'codex']) {
1809
+ const desiredName = API_CONFIG[type]?.providerName;
1810
+ if (!desiredName) continue;
1811
+ const reserved = reserveProviderName(config, desiredName, type);
1812
+ if (reserved.changed) {
1813
+ changed = true;
1814
+ renamedProviders.push(...reserved.renamedProviders);
1815
+ }
1816
+ }
1817
+
1818
+ const repairedAliases = repairYunyiProviderAliases(config);
1819
+ if (repairedAliases.changed) changed = true;
1820
+
1821
+ for (const [name, providerConfig] of Object.entries(config.models.providers)) {
1822
+ if (sanitizeProviderConfig(name, providerConfig)) {
1823
+ changed = true;
1824
+ }
1825
+ }
1826
+
1827
+ if (sanitizeConfigAuthProfiles(config)) {
1828
+ changed = true;
1829
+ }
1830
+
1831
+ return {
1832
+ changed,
1833
+ renamedProviders: [...renamedProviders, ...repairedAliases.renamedProviders]
1834
+ };
1835
+ }
1836
+
1457
1837
  function isValidModelRef(value) {
1458
1838
  return typeof value === 'string' && value.trim().includes('/');
1459
1839
  }
@@ -1702,6 +2082,32 @@ function syncMirroredAuthStores(paths) {
1702
2082
  }
1703
2083
  }
1704
2084
 
2085
+ function renameProviderInAuthStore(paths, oldProvider, newProvider) {
2086
+ if (!paths?.authProfiles || !oldProvider || !newProvider || oldProvider === newProvider) return false;
2087
+ const store = readAuthStore(paths.authProfiles);
2088
+ let changed = false;
2089
+
2090
+ for (const key of Object.keys(store.profiles)) {
2091
+ if (!key.startsWith(`${oldProvider}:`)) continue;
2092
+ const suffix = key.slice(oldProvider.length + 1);
2093
+ const nextKey = `${newProvider}:${suffix}`;
2094
+ const profile = { ...(store.profiles[key] || {}) };
2095
+ if (!store.profiles[nextKey]) {
2096
+ profile.provider = newProvider;
2097
+ store.profiles[nextKey] = profile;
2098
+ }
2099
+ delete store.profiles[key];
2100
+ changed = true;
2101
+ }
2102
+
2103
+ if (changed) {
2104
+ writeAuthStore(paths.authProfiles, store);
2105
+ syncMirroredAuthStores(paths);
2106
+ }
2107
+
2108
+ return changed;
2109
+ }
2110
+
1705
2111
  function pruneAuthProfilesExceptWithSync(paths, keepProviders = []) {
1706
2112
  const removed = pruneAuthProfilesExcept(paths.authProfiles, keepProviders);
1707
2113
  syncMirroredAuthStores(paths);
@@ -1713,6 +2119,16 @@ function updateAuthProfilesWithSync(paths, providerName, apiKey) {
1713
2119
  syncMirroredAuthStores(paths);
1714
2120
  }
1715
2121
 
2122
+ function applyConfigRepairsWithSync(config, paths) {
2123
+ const repairResult = repairConfigProviders(config);
2124
+ if (paths && repairResult.renamedProviders.length > 0) {
2125
+ for (const item of repairResult.renamedProviders) {
2126
+ renameProviderInAuthStore(paths, item.from, item.to);
2127
+ }
2128
+ }
2129
+ return repairResult;
2130
+ }
2131
+
1716
2132
  function pruneAuthProfilesByPrefix(authProfilesPath, prefixBase, keepProviders = []) {
1717
2133
  const keepSet = new Set(keepProviders);
1718
2134
  const store = readAuthStore(authProfilesPath);
@@ -2144,6 +2560,14 @@ function summarizeCliTestOutput(text) {
2144
2560
  return lines[0].slice(0, 160);
2145
2561
  }
2146
2562
 
2563
+ function buildReadableAgentError(text, fallback = '') {
2564
+ const cleaned = cleanCliTestOutput(text);
2565
+ if (!cleaned) return fallback;
2566
+ const lines = cleaned.split('\n').map(line => line.trim()).filter(Boolean);
2567
+ const preferred = lines.find(line => /agent failed before reply|all models failed|auth_permanent|auth issue|unauthorized|forbidden|missing environment variable|provider .* issue|invalid config/i.test(line));
2568
+ return (preferred || lines[0] || fallback).slice(0, 300);
2569
+ }
2570
+
2147
2571
  function looksLikeCliTestError(text) {
2148
2572
  const lower = cleanCliTestOutput(text).toLowerCase();
2149
2573
  if (!lower) return false;
@@ -2316,15 +2740,14 @@ function autoFixConfig(paths) {
2316
2740
  const config = readConfig(paths.openclawConfig);
2317
2741
  if (!config) return;
2318
2742
 
2319
- config.models = config.models || {};
2320
- config.models.providers = config.models.providers || {};
2321
-
2322
2743
  let changed = false;
2323
2744
  const codexProviderName = API_CONFIG.codex?.providerName;
2324
- const codexProvider = codexProviderName && config.models.providers[codexProviderName];
2325
2745
  const originalModelJson = JSON.stringify(config.agents?.defaults?.model ?? null);
2326
2746
  ensureConfigStructure(config);
2327
2747
  sanitizeDefaultModelSelection(config);
2748
+ const repairResult = applyConfigRepairsWithSync(config, paths);
2749
+ if (repairResult.changed) changed = true;
2750
+ const codexProvider = codexProviderName && config.models.providers?.[codexProviderName];
2328
2751
  const nextModelJson = JSON.stringify(config.agents?.defaults?.model ?? null);
2329
2752
  if (originalModelJson !== nextModelJson) {
2330
2753
  changed = true;
@@ -2847,26 +3270,7 @@ async function quickSetup(paths, args = {}) {
2847
3270
 
2848
3271
  let config = readConfig(paths.openclawConfig) || {};
2849
3272
  config = ensureConfigStructure(config);
2850
-
2851
- const existingProviders = Object.keys(config.models.providers || {});
2852
- const toRemove = existingProviders.filter(name => name !== providerName);
2853
- if (toRemove.length > 0 && !args.force) {
2854
- const { overwrite } = await inquirer.prompt([{
2855
- type: 'confirm',
2856
- name: 'overwrite',
2857
- message: `检测到已有中转配置: ${existingProviders.join(', ')},将仅保留 ${providerName}。是否继续?`,
2858
- default: false
2859
- }]);
2860
- if (!overwrite) {
2861
- console.log(chalk.gray('已取消'));
2862
- return;
2863
- }
2864
- }
2865
-
2866
- if (toRemove.length > 0) {
2867
- pruneProvidersExcept(config, [providerName]);
2868
- pruneAuthProfilesExceptWithSync(paths, [providerName]);
2869
- }
3273
+ const repairResult = applyConfigRepairsWithSync(config, paths);
2870
3274
 
2871
3275
  config.models.providers[providerName] = {
2872
3276
  baseUrl: normalizedBaseUrl,
@@ -2914,6 +3318,9 @@ async function quickSetup(paths, args = {}) {
2914
3318
  if (setPrimary) {
2915
3319
  console.log(chalk.yellow(` 主模型: ${modelKey}`));
2916
3320
  }
3321
+ if (repairResult.renamedProviders.length > 0) {
3322
+ console.log(chalk.gray(` 已保留并修复冲突 provider: ${repairResult.renamedProviders.map(item => `${item.from}→${item.to}`).join(', ')}`));
3323
+ }
2917
3324
  }
2918
3325
 
2919
3326
  async function presetClaude(paths, args = {}) {
@@ -2960,31 +3367,10 @@ async function presetClaude(paths, args = {}) {
2960
3367
  }
2961
3368
 
2962
3369
  const config = ensureConfigStructure(readConfig(paths.openclawConfig) || {});
3370
+ const repairResult = applyConfigRepairsWithSync(config, paths);
2963
3371
 
2964
3372
  const providerName = (args['provider-name'] || args.provider || providerPrefix).toString().trim() || apiConfig.providerName;
2965
-
2966
- const existingProviders = Object.keys(config.models.providers || {});
2967
- const toRemove = existingProviders.filter(name => name !== providerName);
2968
-
2969
- if (toRemove.length > 0 && !args.force) {
2970
- const { overwrite } = await inquirer.prompt([{
2971
- type: 'confirm',
2972
- name: 'overwrite',
2973
- message: `检测到已有中转配置: ${existingProviders.join(', ')},将仅保留 ${providerName}。是否继续?`,
2974
- default: false
2975
- }]);
2976
- if (!overwrite) {
2977
- console.log(chalk.gray('已取消'));
2978
- return;
2979
- }
2980
- }
2981
-
2982
- const removedProviders = toRemove.length > 0
2983
- ? pruneProvidersExcept(config, [providerName])
2984
- : [];
2985
- if (removedProviders.length > 0) {
2986
- pruneAuthProfilesExceptWithSync(paths, [providerName]);
2987
- }
3373
+ const removedProviders = repairResult.renamedProviders.map(item => item.from);
2988
3374
 
2989
3375
  const baseUrl = buildFullUrl(selectedEndpoint.url, 'claude');
2990
3376
 
@@ -3097,6 +3483,9 @@ async function presetClaude(paths, args = {}) {
3097
3483
  console.log(chalk.gray(` 模型: ${modelName}`));
3098
3484
  console.log(chalk.gray(' API Key: 已设置'));
3099
3485
  if (extSynced.length > 0) console.log(chalk.gray(` 同步: ${extSynced.join(', ')}`));
3486
+ if (repairResult.renamedProviders.length > 0) {
3487
+ console.log(chalk.gray(` 已保留并修复冲突 provider: ${repairResult.renamedProviders.map(item => `${item.from}→${item.to}`).join(', ')}`));
3488
+ }
3100
3489
 
3101
3490
  const shouldTestGateway = args.test !== undefined
3102
3491
  ? !['false', '0', 'no'].includes(String(args.test).toLowerCase())
@@ -3157,28 +3546,8 @@ async function presetCodex(paths, args = {}) {
3157
3546
  }
3158
3547
 
3159
3548
  const config = ensureConfigStructure(readConfig(paths.openclawConfig) || {});
3160
- const existingProviders = Object.keys(config.models.providers || {});
3161
- const toRemove = existingProviders.filter(name => name !== providerName);
3162
-
3163
- if (toRemove.length > 0 && !args.force) {
3164
- const { overwrite } = await inquirer.prompt([{
3165
- type: 'confirm',
3166
- name: 'overwrite',
3167
- message: `检测到已有中转配置: ${existingProviders.join(', ')},将仅保留 ${providerName}。是否继续?`,
3168
- default: false
3169
- }]);
3170
- if (!overwrite) {
3171
- console.log(chalk.gray('已取消'));
3172
- return;
3173
- }
3174
- }
3175
-
3176
- const removedProviders = toRemove.length > 0
3177
- ? pruneProvidersExcept(config, [providerName])
3178
- : [];
3179
- if (removedProviders.length > 0) {
3180
- pruneAuthProfilesExceptWithSync(paths, [providerName]);
3181
- }
3549
+ const repairResult = applyConfigRepairsWithSync(config, paths);
3550
+ const removedProviders = repairResult.renamedProviders.map(item => item.from);
3182
3551
 
3183
3552
  const baseUrl = buildFullUrl(selectedEndpoint.url, 'codex');
3184
3553
 
@@ -3291,6 +3660,9 @@ async function presetCodex(paths, args = {}) {
3291
3660
  console.log(chalk.gray(` 模型: ${modelName}`));
3292
3661
  console.log(chalk.gray(' API Key: 已设置'));
3293
3662
  if (extSynced2.length > 0) console.log(chalk.gray(` 同步: ${extSynced2.join(', ')}`));
3663
+ if (repairResult.renamedProviders.length > 0) {
3664
+ console.log(chalk.gray(` 已保留并修复冲突 provider: ${repairResult.renamedProviders.map(item => `${item.from}→${item.to}`).join(', ')}`));
3665
+ }
3294
3666
 
3295
3667
  const shouldTestGateway = args.test !== undefined
3296
3668
  ? !['false', '0', 'no'].includes(String(args.test).toLowerCase())
@@ -3399,6 +3771,7 @@ async function autoActivate(paths, args = {}) {
3399
3771
 
3400
3772
  // ---- 构建配置 ----
3401
3773
  const config = ensureConfigStructure(readConfig(paths.openclawConfig) || {});
3774
+ applyConfigRepairsWithSync(config, paths);
3402
3775
 
3403
3776
  const claudeBaseUrl = buildFullUrl(selectedEndpoint.url, 'claude');
3404
3777
  const codexBaseUrl = buildFullUrl(selectedEndpoint.url, 'codex');
@@ -4245,6 +4618,7 @@ async function switchModel(paths) {
4245
4618
  console.log(chalk.cyan('🔄 切换 OpenClaw 模型\n'));
4246
4619
 
4247
4620
  const config = ensureConfigStructure(readConfig(paths.openclawConfig) || {});
4621
+ applyConfigRepairsWithSync(config, paths);
4248
4622
  const primary = config.agents?.defaults?.model?.primary || '';
4249
4623
  const providers = config.models?.providers || {};
4250
4624
 
@@ -4507,9 +4881,10 @@ async function testConnection(paths, args = {}) {
4507
4881
  console.log(chalk.cyan('🧪 测试 OpenClaw / 各 CLI 连接\n'));
4508
4882
  invalidateGatewayEnvCache();
4509
4883
 
4510
- const config = readConfig(paths.openclawConfig);
4884
+ const config = ensureConfigStructure(readConfig(paths.openclawConfig) || {});
4885
+ applyConfigRepairsWithSync(config, paths);
4511
4886
 
4512
- if (!config) {
4887
+ if (!config || !config.models) {
4513
4888
  console.log(chalk.yellow('配置文件不存在,请先选择节点'));
4514
4889
  return;
4515
4890
  }
@@ -4688,6 +5063,10 @@ async function testConnection(paths, args = {}) {
4688
5063
  if (!cliResult.success) {
4689
5064
  console.log(chalk.red(`\n❌ Gateway CLI 测试失败`));
4690
5065
  console.log(chalk.red(` 错误: ${cliResult.error || '未知错误'}`));
5066
+ if (cliResult.rawError && cliResult.rawError !== cliResult.error) {
5067
+ console.log(chalk.gray(` 原始输出: ${cliResult.rawError.substring(0, 300)}`));
5068
+ }
5069
+ console.log(chalk.gray(' 可继续执行: openclaw logs --follow'));
4691
5070
  console.log(chalk.gray(` 将尝试使用 HTTP 端点测试...`));
4692
5071
  }
4693
5072
  }
@@ -5193,10 +5572,18 @@ function testGatewayViaAgent(model) {
5193
5572
  // stdout 有 JSON,走正常解析流程而非直接报错
5194
5573
  stdout = fallbackOutput;
5195
5574
  } else {
5575
+ const plainCmd = cmd.replace(/\s--json\b/, '');
5576
+ const plainResult = safeExec(plainCmd, execOpts);
5577
+ const plainCombined = `${plainResult.output || ''}\n${plainResult.stdout || ''}\n${plainResult.stderr || ''}`.trim();
5578
+ const readableError = buildReadableAgentError(
5579
+ `${cleanStderr}\n${fallbackOutput}\n${plainCombined}`,
5580
+ (error.message || 'CLI 执行失败').trim()
5581
+ );
5196
5582
  resolve({
5197
5583
  success: false,
5198
5584
  usedCli: true,
5199
- error: (cleanStderr || fallbackOutput || error.message || 'CLI 执行失败').trim()
5585
+ error: readableError,
5586
+ rawError: (cleanStderr || fallbackOutput || plainCombined || error.message || 'CLI 执行失败').trim()
5200
5587
  });
5201
5588
  return;
5202
5589
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "yymaxapi",
3
- "version": "1.0.81",
3
+ "version": "1.0.83",
4
4
  "description": "跨平台 OpenClaw/Clawdbot 配置管理工具 - 管理中转地址、模型切换、API Keys、测速优化",
5
5
  "main": "bin/yymaxapi.js",
6
6
  "bin": {