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.
- package/bin/yymaxapi.js +477 -90
- 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
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
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
|
-
|
|
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
|
|
3161
|
-
const
|
|
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:
|
|
5585
|
+
error: readableError,
|
|
5586
|
+
rawError: (cleanStderr || fallbackOutput || plainCombined || error.message || 'CLI 执行失败').trim()
|
|
5200
5587
|
});
|
|
5201
5588
|
return;
|
|
5202
5589
|
}
|