yymaxapi 1.0.80 → 1.0.82
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/bin/yymaxapi.js
CHANGED
|
@@ -94,7 +94,7 @@ const DEFAULT_CLAUDE_MODELS = [
|
|
|
94
94
|
},
|
|
95
95
|
{
|
|
96
96
|
"id": "claude-opus-4-6",
|
|
97
|
-
"name": "Claude Opus 4.6 (
|
|
97
|
+
"name": "Claude Opus 4.6 (待稳定)"
|
|
98
98
|
}
|
|
99
99
|
];
|
|
100
100
|
|
|
@@ -228,6 +228,127 @@ const CLAUDE_MODELS = PRESETS.models.claude;
|
|
|
228
228
|
const CODEX_MODELS = PRESETS.models.codex;
|
|
229
229
|
const API_CONFIG = PRESETS.apiConfig;
|
|
230
230
|
|
|
231
|
+
function getDefaultClaudeModel() {
|
|
232
|
+
return CLAUDE_MODELS[0] || { id: 'claude-sonnet-4-6', name: 'Claude Sonnet 4.6' };
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function getDefaultCodexModel() {
|
|
236
|
+
return CODEX_MODELS[0] || { id: 'gpt-5.4', name: 'GPT 5.4' };
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
function buildProviderModelMap(models) {
|
|
240
|
+
const mapped = {};
|
|
241
|
+
for (const model of models || []) {
|
|
242
|
+
if (!model?.id) continue;
|
|
243
|
+
mapped[model.id] = { name: model.name || model.id };
|
|
244
|
+
}
|
|
245
|
+
return mapped;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
function getClaudeSwitchHint() {
|
|
249
|
+
return CLAUDE_MODELS.map(model => model.name).join(' / ');
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
function getOpencodeSwitchHint() {
|
|
253
|
+
return [...CLAUDE_MODELS, ...CODEX_MODELS].map(model => model.name).join(' / ');
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
async function promptClaudeModelSelection(args = {}, message = '选择 Claude 模型:') {
|
|
257
|
+
const requested = (args['claude-model'] || args.model || args['model-id'] || '').toString().trim();
|
|
258
|
+
const fallback = getDefaultClaudeModel();
|
|
259
|
+
if (requested) {
|
|
260
|
+
return CLAUDE_MODELS.find(model => model.id === requested) || { id: requested, name: requested };
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
if (CLAUDE_MODELS.length <= 1) {
|
|
264
|
+
return fallback;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
const { selected } = await inquirer.prompt([{
|
|
268
|
+
type: 'list',
|
|
269
|
+
name: 'selected',
|
|
270
|
+
message,
|
|
271
|
+
choices: CLAUDE_MODELS.map(model => ({ name: model.name, value: model.id })),
|
|
272
|
+
default: fallback.id
|
|
273
|
+
}]);
|
|
274
|
+
|
|
275
|
+
return CLAUDE_MODELS.find(model => model.id === selected) || fallback;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
async function promptOpencodeDefaultModelSelection(args = {}, message = '选择 Opencode 默认模型:') {
|
|
279
|
+
const fallbackClaude = getDefaultClaudeModel();
|
|
280
|
+
const fallbackCodex = getDefaultCodexModel();
|
|
281
|
+
const requested = (args['default-model'] || args.model || args['claude-model'] || args['codex-model'] || '').toString().trim();
|
|
282
|
+
|
|
283
|
+
if (requested) {
|
|
284
|
+
const inClaude = CLAUDE_MODELS.find(model => model.id === requested);
|
|
285
|
+
if (inClaude) {
|
|
286
|
+
return {
|
|
287
|
+
type: 'claude',
|
|
288
|
+
providerKey: 'yunyi-claude',
|
|
289
|
+
modelId: inClaude.id,
|
|
290
|
+
modelName: inClaude.name,
|
|
291
|
+
modelKey: `yunyi-claude/${inClaude.id}`
|
|
292
|
+
};
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
const inCodex = CODEX_MODELS.find(model => model.id === requested);
|
|
296
|
+
if (inCodex) {
|
|
297
|
+
return {
|
|
298
|
+
type: 'codex',
|
|
299
|
+
providerKey: 'yunyi-codex',
|
|
300
|
+
modelId: inCodex.id,
|
|
301
|
+
modelName: inCodex.name,
|
|
302
|
+
modelKey: `yunyi-codex/${inCodex.id}`
|
|
303
|
+
};
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
const defaultValue = `claude:${fallbackClaude.id}`;
|
|
308
|
+
const choices = [];
|
|
309
|
+
if (CLAUDE_MODELS.length > 0) {
|
|
310
|
+
choices.push(new inquirer.Separator(' -- Claude --'));
|
|
311
|
+
for (const model of CLAUDE_MODELS) {
|
|
312
|
+
choices.push({ name: model.name, value: `claude:${model.id}` });
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
if (CODEX_MODELS.length > 0) {
|
|
316
|
+
choices.push(new inquirer.Separator(' -- GPT --'));
|
|
317
|
+
for (const model of CODEX_MODELS) {
|
|
318
|
+
choices.push({ name: model.name, value: `codex:${model.id}` });
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
const { picked } = await inquirer.prompt([{
|
|
323
|
+
type: 'list',
|
|
324
|
+
name: 'picked',
|
|
325
|
+
message,
|
|
326
|
+
choices,
|
|
327
|
+
default: defaultValue
|
|
328
|
+
}]);
|
|
329
|
+
|
|
330
|
+
const [pickedType, pickedId] = picked.split(':');
|
|
331
|
+
if (pickedType === 'codex') {
|
|
332
|
+
const model = CODEX_MODELS.find(item => item.id === pickedId) || fallbackCodex;
|
|
333
|
+
return {
|
|
334
|
+
type: 'codex',
|
|
335
|
+
providerKey: 'yunyi-codex',
|
|
336
|
+
modelId: model.id,
|
|
337
|
+
modelName: model.name,
|
|
338
|
+
modelKey: `yunyi-codex/${model.id}`
|
|
339
|
+
};
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
const model = CLAUDE_MODELS.find(item => item.id === pickedId) || fallbackClaude;
|
|
343
|
+
return {
|
|
344
|
+
type: 'claude',
|
|
345
|
+
providerKey: 'yunyi-claude',
|
|
346
|
+
modelId: model.id,
|
|
347
|
+
modelName: model.name,
|
|
348
|
+
modelKey: `yunyi-claude/${model.id}`
|
|
349
|
+
};
|
|
350
|
+
}
|
|
351
|
+
|
|
231
352
|
// 备份文件名(兼容旧版单文件备份)
|
|
232
353
|
const BACKUP_FILENAME = 'openclaw-default.json.bak';
|
|
233
354
|
const BACKUP_DIR_NAME = 'backups';
|
|
@@ -636,6 +757,7 @@ function readConfig(configPath) {
|
|
|
636
757
|
}
|
|
637
758
|
|
|
638
759
|
function writeConfig(configPath, config) {
|
|
760
|
+
sanitizeDefaultModelSelection(config);
|
|
639
761
|
const dir = path.dirname(configPath);
|
|
640
762
|
if (!fs.existsSync(dir)) {
|
|
641
763
|
fs.mkdirSync(dir, { recursive: true });
|
|
@@ -645,7 +767,7 @@ function writeConfig(configPath, config) {
|
|
|
645
767
|
|
|
646
768
|
// ============ 多工具配置同步 ============
|
|
647
769
|
|
|
648
|
-
function writeClaudeCodeSettings(baseUrl, apiKey) {
|
|
770
|
+
function writeClaudeCodeSettings(baseUrl, apiKey, modelId = getDefaultClaudeModel().id) {
|
|
649
771
|
const home = os.homedir();
|
|
650
772
|
// ~/.claude/settings.json
|
|
651
773
|
const claudeDir = path.join(home, '.claude');
|
|
@@ -656,9 +778,12 @@ function writeClaudeCodeSettings(baseUrl, apiKey) {
|
|
|
656
778
|
try { settings = JSON.parse(fs.readFileSync(settingsPath, 'utf8')); } catch { settings = {}; }
|
|
657
779
|
}
|
|
658
780
|
settings.apiBaseUrl = baseUrl.replace(/\/+$/, '');
|
|
781
|
+
settings.model = modelId;
|
|
782
|
+
settings.availableModels = CLAUDE_MODELS.map(model => model.id);
|
|
659
783
|
if (!settings.env) settings.env = {};
|
|
660
784
|
settings.env.ANTHROPIC_BASE_URL = baseUrl.replace(/\/+$/, '');
|
|
661
785
|
settings.env.ANTHROPIC_AUTH_TOKEN = apiKey;
|
|
786
|
+
settings.env.ANTHROPIC_MODEL = modelId;
|
|
662
787
|
if (!fs.existsSync(claudeDir)) fs.mkdirSync(claudeDir, { recursive: true });
|
|
663
788
|
fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2), 'utf8');
|
|
664
789
|
} catch { /* 非关键,静默失败 */ }
|
|
@@ -681,7 +806,7 @@ function writeClaudeCodeSettings(baseUrl, apiKey) {
|
|
|
681
806
|
try {
|
|
682
807
|
const cleanUrl = baseUrl.replace(/\/+$/, '');
|
|
683
808
|
execSync(
|
|
684
|
-
`powershell -NoProfile -Command "[Environment]::SetEnvironmentVariable('ANTHROPIC_BASE_URL','${cleanUrl}','User'); [Environment]::SetEnvironmentVariable('ANTHROPIC_AUTH_TOKEN','${apiKey}','User'); [Environment]::SetEnvironmentVariable('NODE_TLS_REJECT_UNAUTHORIZED','0','User')"`,
|
|
809
|
+
`powershell -NoProfile -Command "[Environment]::SetEnvironmentVariable('ANTHROPIC_BASE_URL','${cleanUrl}','User'); [Environment]::SetEnvironmentVariable('ANTHROPIC_AUTH_TOKEN','${apiKey}','User'); [Environment]::SetEnvironmentVariable('ANTHROPIC_MODEL','${modelId}','User'); [Environment]::SetEnvironmentVariable('NODE_TLS_REJECT_UNAUTHORIZED','0','User')"`,
|
|
685
810
|
{ stdio: 'pipe' }
|
|
686
811
|
);
|
|
687
812
|
} catch { /* best-effort */ }
|
|
@@ -693,6 +818,7 @@ function writeClaudeCodeSettings(baseUrl, apiKey) {
|
|
|
693
818
|
marker,
|
|
694
819
|
`export ANTHROPIC_BASE_URL="${cleanUrl}"`,
|
|
695
820
|
`export ANTHROPIC_AUTH_TOKEN="${apiKey}"`,
|
|
821
|
+
`export ANTHROPIC_MODEL="${modelId}"`,
|
|
696
822
|
'# 中转站证书校验放宽,避免 unknown certificate verification error',
|
|
697
823
|
'export NODE_TLS_REJECT_UNAUTHORIZED=0',
|
|
698
824
|
markerEnd
|
|
@@ -796,7 +922,7 @@ function writeCodexConfig(baseUrl, apiKey, modelId = 'gpt-5.4') {
|
|
|
796
922
|
} catch { /* 非关键,静默失败 */ }
|
|
797
923
|
}
|
|
798
924
|
|
|
799
|
-
function writeOpencodeConfig(claudeBaseUrl, codexBaseUrl, apiKey,
|
|
925
|
+
function writeOpencodeConfig(claudeBaseUrl, codexBaseUrl, apiKey, defaultModelKey = `yunyi-claude/${getDefaultClaudeModel().id}`) {
|
|
800
926
|
const home = os.homedir();
|
|
801
927
|
const claudeUrl = claudeBaseUrl.replace(/\/+$/, '');
|
|
802
928
|
const codexUrl = (codexBaseUrl || '').replace(/\/+$/, '');
|
|
@@ -818,7 +944,7 @@ function writeOpencodeConfig(claudeBaseUrl, codexBaseUrl, apiKey, modelId) {
|
|
|
818
944
|
existing.provider['yunyi-claude'] = {
|
|
819
945
|
name: '云翼 Claude',
|
|
820
946
|
npm: '@ai-sdk/anthropic',
|
|
821
|
-
models:
|
|
947
|
+
models: buildProviderModelMap(CLAUDE_MODELS),
|
|
822
948
|
options: { apiKey, baseURL: `${claudeUrl}/v1` }
|
|
823
949
|
};
|
|
824
950
|
|
|
@@ -827,7 +953,7 @@ function writeOpencodeConfig(claudeBaseUrl, codexBaseUrl, apiKey, modelId) {
|
|
|
827
953
|
existing.provider['yunyi-codex'] = {
|
|
828
954
|
name: '云翼 Codex',
|
|
829
955
|
npm: '@ai-sdk/openai',
|
|
830
|
-
models:
|
|
956
|
+
models: buildProviderModelMap(CODEX_MODELS),
|
|
831
957
|
options: { apiKey, baseURL: codexUrl }
|
|
832
958
|
};
|
|
833
959
|
}
|
|
@@ -839,8 +965,7 @@ function writeOpencodeConfig(claudeBaseUrl, codexBaseUrl, apiKey, modelId) {
|
|
|
839
965
|
}
|
|
840
966
|
|
|
841
967
|
// 设置默认模型
|
|
842
|
-
|
|
843
|
-
existing.model = `yunyi-claude/${rawModelId}`;
|
|
968
|
+
existing.model = defaultModelKey;
|
|
844
969
|
|
|
845
970
|
// 从 disabled_providers 中移除我们的 provider
|
|
846
971
|
if (Array.isArray(existing.disabled_providers)) {
|
|
@@ -895,14 +1020,14 @@ function syncExternalTools(type, baseUrl, apiKey, extra = {}) {
|
|
|
895
1020
|
const synced = [];
|
|
896
1021
|
try {
|
|
897
1022
|
if (type === 'claude') {
|
|
898
|
-
writeClaudeCodeSettings(baseUrl, apiKey);
|
|
1023
|
+
writeClaudeCodeSettings(baseUrl, apiKey, extra.claudeModelId || getDefaultClaudeModel().id);
|
|
899
1024
|
synced.push('Claude Code settings');
|
|
900
1025
|
} else if (type === 'codex') {
|
|
901
|
-
writeCodexConfig(baseUrl, apiKey);
|
|
1026
|
+
writeCodexConfig(baseUrl, apiKey, extra.modelId || getDefaultCodexModel().id);
|
|
902
1027
|
synced.push('Codex CLI config');
|
|
903
1028
|
}
|
|
904
1029
|
if (type === 'claude' && extra.codexBaseUrl) {
|
|
905
|
-
writeOpencodeConfig(baseUrl, extra.codexBaseUrl, apiKey);
|
|
1030
|
+
writeOpencodeConfig(baseUrl, extra.codexBaseUrl, apiKey, extra.opencodeDefaultModelKey || `yunyi-claude/${extra.claudeModelId || getDefaultClaudeModel().id}`);
|
|
906
1031
|
synced.push('Opencode config');
|
|
907
1032
|
}
|
|
908
1033
|
} catch { /* ignore */ }
|
|
@@ -1329,6 +1454,413 @@ function coerceModelsRecord(value) {
|
|
|
1329
1454
|
return record;
|
|
1330
1455
|
}
|
|
1331
1456
|
|
|
1457
|
+
const YUNYI_PROVIDER_ALIASES = {
|
|
1458
|
+
claude: new Set(['claude-yunyi', 'yunyi-claude', 'claude_yunyi']),
|
|
1459
|
+
codex: new Set(['yunyi', 'yunyi-codex', 'codex-yunyi', 'codex_yunyi'])
|
|
1460
|
+
};
|
|
1461
|
+
|
|
1462
|
+
function looksLikeApiKey(value) {
|
|
1463
|
+
const text = String(value || '').trim();
|
|
1464
|
+
if (!text || text.includes('/') || text.includes(' ')) return false;
|
|
1465
|
+
if (/^(sk-|STP|YK)/i.test(text)) return true;
|
|
1466
|
+
return /^[A-Za-z0-9._-]{16,}$/.test(text);
|
|
1467
|
+
}
|
|
1468
|
+
|
|
1469
|
+
function isLikelyYunyiBaseUrl(baseUrl) {
|
|
1470
|
+
const urlText = String(baseUrl || '').trim();
|
|
1471
|
+
if (!urlText) return false;
|
|
1472
|
+
try {
|
|
1473
|
+
const hostname = new URL(urlText).hostname.toLowerCase();
|
|
1474
|
+
return hostname.includes('yunyi')
|
|
1475
|
+
|| hostname.includes('rdzhvip.com')
|
|
1476
|
+
|| hostname === '47.99.42.193'
|
|
1477
|
+
|| hostname === '47.97.100.10';
|
|
1478
|
+
} catch {
|
|
1479
|
+
return /yunyi|rdzhvip\.com|47\.99\.42\.193|47\.97\.100\.10/i.test(urlText);
|
|
1480
|
+
}
|
|
1481
|
+
}
|
|
1482
|
+
|
|
1483
|
+
function isYunyiProviderEntry(name, providerConfig, type) {
|
|
1484
|
+
const cfg = providerConfig || {};
|
|
1485
|
+
const providerName = String(name || '').trim();
|
|
1486
|
+
if (YUNYI_PROVIDER_ALIASES[type]?.has(providerName)) return true;
|
|
1487
|
+
|
|
1488
|
+
const baseUrl = String(cfg.baseUrl || cfg.base_url || '').trim();
|
|
1489
|
+
const api = String(cfg.api || '').trim();
|
|
1490
|
+
if (!isLikelyYunyiBaseUrl(baseUrl)) return false;
|
|
1491
|
+
|
|
1492
|
+
if (type === 'claude') {
|
|
1493
|
+
return api.startsWith('anthropic') || /\/claude(?:\/|$)/.test(baseUrl);
|
|
1494
|
+
}
|
|
1495
|
+
return api.startsWith('openai') || /\/codex(?:\/|$)/.test(baseUrl);
|
|
1496
|
+
}
|
|
1497
|
+
|
|
1498
|
+
function normalizeProviderModelEntry(model, fallbackContext = {}) {
|
|
1499
|
+
if (!model) return null;
|
|
1500
|
+
if (typeof model === 'string') {
|
|
1501
|
+
return {
|
|
1502
|
+
id: model,
|
|
1503
|
+
name: model,
|
|
1504
|
+
contextWindow: fallbackContext.contextWindow,
|
|
1505
|
+
maxTokens: fallbackContext.maxTokens
|
|
1506
|
+
};
|
|
1507
|
+
}
|
|
1508
|
+
if (typeof model !== 'object') return null;
|
|
1509
|
+
|
|
1510
|
+
const id = String(model.id || model.model || '').trim();
|
|
1511
|
+
if (!id) return null;
|
|
1512
|
+
return {
|
|
1513
|
+
...model,
|
|
1514
|
+
id,
|
|
1515
|
+
name: model.name || id,
|
|
1516
|
+
contextWindow: model.contextWindow || fallbackContext.contextWindow,
|
|
1517
|
+
maxTokens: model.maxTokens || fallbackContext.maxTokens
|
|
1518
|
+
};
|
|
1519
|
+
}
|
|
1520
|
+
|
|
1521
|
+
function mergeProviderModels(existingModels = [], incomingModels = [], fallbackContext = {}) {
|
|
1522
|
+
const merged = [];
|
|
1523
|
+
const seen = new Set();
|
|
1524
|
+
|
|
1525
|
+
for (const item of [...existingModels, ...incomingModels]) {
|
|
1526
|
+
const normalized = normalizeProviderModelEntry(item, fallbackContext);
|
|
1527
|
+
if (!normalized || seen.has(normalized.id)) continue;
|
|
1528
|
+
seen.add(normalized.id);
|
|
1529
|
+
merged.push(normalized);
|
|
1530
|
+
}
|
|
1531
|
+
|
|
1532
|
+
return merged;
|
|
1533
|
+
}
|
|
1534
|
+
|
|
1535
|
+
function renameProviderReferencesInConfig(config, oldName, newName) {
|
|
1536
|
+
if (!oldName || !newName || oldName === newName) return false;
|
|
1537
|
+
let changed = false;
|
|
1538
|
+
|
|
1539
|
+
if (config?.agents?.defaults?.models) {
|
|
1540
|
+
for (const key of Object.keys(config.agents.defaults.models)) {
|
|
1541
|
+
if (!key.startsWith(`${oldName}/`)) continue;
|
|
1542
|
+
const nextKey = `${newName}/${key.slice(oldName.length + 1)}`;
|
|
1543
|
+
if (!config.agents.defaults.models[nextKey]) {
|
|
1544
|
+
const entry = { ...(config.agents.defaults.models[key] || {}) };
|
|
1545
|
+
if (!entry.alias || entry.alias === oldName) entry.alias = newName;
|
|
1546
|
+
config.agents.defaults.models[nextKey] = entry;
|
|
1547
|
+
}
|
|
1548
|
+
delete config.agents.defaults.models[key];
|
|
1549
|
+
changed = true;
|
|
1550
|
+
}
|
|
1551
|
+
}
|
|
1552
|
+
|
|
1553
|
+
if (config?.agents?.defaults?.model) {
|
|
1554
|
+
const modelState = config.agents.defaults.model;
|
|
1555
|
+
if (typeof modelState.primary === 'string' && modelState.primary.startsWith(`${oldName}/`)) {
|
|
1556
|
+
modelState.primary = `${newName}/${modelState.primary.slice(oldName.length + 1)}`;
|
|
1557
|
+
changed = true;
|
|
1558
|
+
}
|
|
1559
|
+
if (Array.isArray(modelState.fallbacks)) {
|
|
1560
|
+
const nextFallbacks = modelState.fallbacks.map(modelKey => (
|
|
1561
|
+
typeof modelKey === 'string' && modelKey.startsWith(`${oldName}/`)
|
|
1562
|
+
? `${newName}/${modelKey.slice(oldName.length + 1)}`
|
|
1563
|
+
: modelKey
|
|
1564
|
+
));
|
|
1565
|
+
if (JSON.stringify(nextFallbacks) !== JSON.stringify(modelState.fallbacks)) {
|
|
1566
|
+
modelState.fallbacks = nextFallbacks;
|
|
1567
|
+
changed = true;
|
|
1568
|
+
}
|
|
1569
|
+
}
|
|
1570
|
+
}
|
|
1571
|
+
|
|
1572
|
+
if (config?.auth?.profiles) {
|
|
1573
|
+
for (const key of Object.keys(config.auth.profiles)) {
|
|
1574
|
+
if (!key.startsWith(`${oldName}:`)) continue;
|
|
1575
|
+
const suffix = key.slice(oldName.length + 1);
|
|
1576
|
+
const nextKey = `${newName}:${suffix}`;
|
|
1577
|
+
const profile = { ...(config.auth.profiles[key] || {}) };
|
|
1578
|
+
profile.provider = newName;
|
|
1579
|
+
config.auth.profiles[nextKey] = profile;
|
|
1580
|
+
delete config.auth.profiles[key];
|
|
1581
|
+
changed = true;
|
|
1582
|
+
}
|
|
1583
|
+
}
|
|
1584
|
+
|
|
1585
|
+
return changed;
|
|
1586
|
+
}
|
|
1587
|
+
|
|
1588
|
+
function suggestConflictProviderName(config, baseName) {
|
|
1589
|
+
const providers = config?.models?.providers || {};
|
|
1590
|
+
let candidate = `${baseName}-legacy`;
|
|
1591
|
+
let index = 2;
|
|
1592
|
+
while (providers[candidate]) {
|
|
1593
|
+
candidate = `${baseName}-legacy-${index}`;
|
|
1594
|
+
index += 1;
|
|
1595
|
+
}
|
|
1596
|
+
return candidate;
|
|
1597
|
+
}
|
|
1598
|
+
|
|
1599
|
+
function sanitizeProviderConfig(name, providerConfig, typeHint = null) {
|
|
1600
|
+
if (!providerConfig || typeof providerConfig !== 'object') return false;
|
|
1601
|
+
let changed = false;
|
|
1602
|
+
|
|
1603
|
+
if (providerConfig.base_url && !providerConfig.baseUrl) {
|
|
1604
|
+
providerConfig.baseUrl = providerConfig.base_url;
|
|
1605
|
+
delete providerConfig.base_url;
|
|
1606
|
+
changed = true;
|
|
1607
|
+
}
|
|
1608
|
+
|
|
1609
|
+
if (providerConfig.model && !providerConfig.models) {
|
|
1610
|
+
providerConfig.models = [providerConfig.model];
|
|
1611
|
+
delete providerConfig.model;
|
|
1612
|
+
changed = true;
|
|
1613
|
+
}
|
|
1614
|
+
|
|
1615
|
+
const inferredType = typeHint
|
|
1616
|
+
|| (String(providerConfig.api || '').startsWith('anthropic') ? 'claude'
|
|
1617
|
+
: String(providerConfig.api || '').startsWith('openai') ? 'codex'
|
|
1618
|
+
: null);
|
|
1619
|
+
const fallbackContext = inferredType === 'claude'
|
|
1620
|
+
? { contextWindow: API_CONFIG.claude?.contextWindow, maxTokens: API_CONFIG.claude?.maxTokens }
|
|
1621
|
+
: inferredType === 'codex'
|
|
1622
|
+
? { contextWindow: API_CONFIG.codex?.contextWindow, maxTokens: API_CONFIG.codex?.maxTokens }
|
|
1623
|
+
: {};
|
|
1624
|
+
|
|
1625
|
+
if (providerConfig.models) {
|
|
1626
|
+
const nextModels = mergeProviderModels(providerConfig.models, [], fallbackContext);
|
|
1627
|
+
if (JSON.stringify(nextModels) !== JSON.stringify(providerConfig.models)) {
|
|
1628
|
+
providerConfig.models = nextModels;
|
|
1629
|
+
changed = true;
|
|
1630
|
+
}
|
|
1631
|
+
}
|
|
1632
|
+
|
|
1633
|
+
if (!providerConfig.auth && providerConfig.apiKey) {
|
|
1634
|
+
providerConfig.auth = DEFAULT_AUTH_MODE;
|
|
1635
|
+
changed = true;
|
|
1636
|
+
} else if (typeof providerConfig.auth === 'string') {
|
|
1637
|
+
const authValue = providerConfig.auth.trim();
|
|
1638
|
+
if (authValue === 'api_key') {
|
|
1639
|
+
providerConfig.auth = DEFAULT_AUTH_MODE;
|
|
1640
|
+
changed = true;
|
|
1641
|
+
} else if (!['api-key', 'token', 'none'].includes(authValue)) {
|
|
1642
|
+
if (!providerConfig.apiKey && looksLikeApiKey(authValue)) {
|
|
1643
|
+
providerConfig.apiKey = authValue;
|
|
1644
|
+
providerConfig.auth = DEFAULT_AUTH_MODE;
|
|
1645
|
+
changed = true;
|
|
1646
|
+
} else if (providerConfig.apiKey) {
|
|
1647
|
+
providerConfig.auth = DEFAULT_AUTH_MODE;
|
|
1648
|
+
changed = true;
|
|
1649
|
+
}
|
|
1650
|
+
}
|
|
1651
|
+
}
|
|
1652
|
+
|
|
1653
|
+
if (!providerConfig.headers || Array.isArray(providerConfig.headers) || typeof providerConfig.headers !== 'object') {
|
|
1654
|
+
providerConfig.headers = {};
|
|
1655
|
+
changed = true;
|
|
1656
|
+
}
|
|
1657
|
+
|
|
1658
|
+
if (providerConfig.authHeader === undefined) {
|
|
1659
|
+
if (String(providerConfig.api || '').startsWith('openai')) {
|
|
1660
|
+
providerConfig.authHeader = true;
|
|
1661
|
+
changed = true;
|
|
1662
|
+
} else if (String(providerConfig.api || '').startsWith('anthropic')) {
|
|
1663
|
+
providerConfig.authHeader = false;
|
|
1664
|
+
changed = true;
|
|
1665
|
+
}
|
|
1666
|
+
}
|
|
1667
|
+
|
|
1668
|
+
return changed;
|
|
1669
|
+
}
|
|
1670
|
+
|
|
1671
|
+
function sanitizeConfigAuthProfiles(config) {
|
|
1672
|
+
if (!config.auth) config.auth = {};
|
|
1673
|
+
if (!config.auth.profiles || typeof config.auth.profiles !== 'object' || Array.isArray(config.auth.profiles)) {
|
|
1674
|
+
config.auth.profiles = {};
|
|
1675
|
+
return true;
|
|
1676
|
+
}
|
|
1677
|
+
|
|
1678
|
+
let changed = false;
|
|
1679
|
+
for (const [key, profile] of Object.entries(config.auth.profiles)) {
|
|
1680
|
+
const provider = (profile && typeof profile === 'object' && profile.provider) || key.split(':')[0];
|
|
1681
|
+
if (!provider) {
|
|
1682
|
+
delete config.auth.profiles[key];
|
|
1683
|
+
changed = true;
|
|
1684
|
+
continue;
|
|
1685
|
+
}
|
|
1686
|
+
const nextProfile = {
|
|
1687
|
+
provider,
|
|
1688
|
+
mode: (profile && typeof profile === 'object' && (profile.mode === 'api-key' ? 'api_key' : profile.mode)) || 'api_key'
|
|
1689
|
+
};
|
|
1690
|
+
if (JSON.stringify(profile) !== JSON.stringify(nextProfile)) {
|
|
1691
|
+
config.auth.profiles[key] = nextProfile;
|
|
1692
|
+
changed = true;
|
|
1693
|
+
}
|
|
1694
|
+
}
|
|
1695
|
+
return changed;
|
|
1696
|
+
}
|
|
1697
|
+
|
|
1698
|
+
function repairYunyiProviderAliases(config) {
|
|
1699
|
+
if (!config?.models?.providers) return { changed: false, renamedProviders: [] };
|
|
1700
|
+
|
|
1701
|
+
let changed = false;
|
|
1702
|
+
const renamedProviders = [];
|
|
1703
|
+
const providers = config.models.providers;
|
|
1704
|
+
|
|
1705
|
+
for (const type of ['claude', 'codex']) {
|
|
1706
|
+
const canonicalName = API_CONFIG[type]?.providerName;
|
|
1707
|
+
if (!canonicalName) continue;
|
|
1708
|
+
|
|
1709
|
+
const candidateEntries = Object.entries(providers).filter(([name, providerConfig]) => isYunyiProviderEntry(name, providerConfig, type));
|
|
1710
|
+
if (candidateEntries.length === 0) continue;
|
|
1711
|
+
|
|
1712
|
+
if (!providers[canonicalName]) {
|
|
1713
|
+
providers[canonicalName] = {};
|
|
1714
|
+
changed = true;
|
|
1715
|
+
}
|
|
1716
|
+
|
|
1717
|
+
sanitizeProviderConfig(canonicalName, providers[canonicalName], type);
|
|
1718
|
+
|
|
1719
|
+
for (const [name, providerConfig] of candidateEntries) {
|
|
1720
|
+
sanitizeProviderConfig(name, providerConfig, type);
|
|
1721
|
+
if (name === canonicalName) continue;
|
|
1722
|
+
|
|
1723
|
+
const target = providers[canonicalName];
|
|
1724
|
+
if (!target.baseUrl && providerConfig.baseUrl) target.baseUrl = providerConfig.baseUrl;
|
|
1725
|
+
if (!target.api && providerConfig.api) target.api = providerConfig.api;
|
|
1726
|
+
if (!target.apiKey && providerConfig.apiKey) target.apiKey = providerConfig.apiKey;
|
|
1727
|
+
if (!target.auth && providerConfig.auth) target.auth = providerConfig.auth;
|
|
1728
|
+
if (target.authHeader === undefined && providerConfig.authHeader !== undefined) target.authHeader = providerConfig.authHeader;
|
|
1729
|
+
if ((!target.headers || Object.keys(target.headers).length === 0) && providerConfig.headers && typeof providerConfig.headers === 'object') {
|
|
1730
|
+
target.headers = { ...providerConfig.headers };
|
|
1731
|
+
}
|
|
1732
|
+
target.models = mergeProviderModels(target.models, providerConfig.models, type === 'claude'
|
|
1733
|
+
? { contextWindow: API_CONFIG.claude?.contextWindow, maxTokens: API_CONFIG.claude?.maxTokens }
|
|
1734
|
+
: { contextWindow: API_CONFIG.codex?.contextWindow, maxTokens: API_CONFIG.codex?.maxTokens });
|
|
1735
|
+
|
|
1736
|
+
delete providers[name];
|
|
1737
|
+
renamedProviders.push({ from: name, to: canonicalName });
|
|
1738
|
+
renameProviderReferencesInConfig(config, name, canonicalName);
|
|
1739
|
+
changed = true;
|
|
1740
|
+
}
|
|
1741
|
+
|
|
1742
|
+
sanitizeProviderConfig(canonicalName, providers[canonicalName], type);
|
|
1743
|
+
}
|
|
1744
|
+
|
|
1745
|
+
return { changed, renamedProviders };
|
|
1746
|
+
}
|
|
1747
|
+
|
|
1748
|
+
function reserveProviderName(config, desiredName, expectedType) {
|
|
1749
|
+
const providers = config?.models?.providers || {};
|
|
1750
|
+
const existing = providers[desiredName];
|
|
1751
|
+
if (!existing) return { changed: false, renamedProviders: [] };
|
|
1752
|
+
if (isYunyiProviderEntry(desiredName, existing, expectedType)) {
|
|
1753
|
+
return { changed: false, renamedProviders: [] };
|
|
1754
|
+
}
|
|
1755
|
+
|
|
1756
|
+
const nextName = suggestConflictProviderName(config, desiredName);
|
|
1757
|
+
providers[nextName] = existing;
|
|
1758
|
+
delete providers[desiredName];
|
|
1759
|
+
sanitizeProviderConfig(nextName, providers[nextName]);
|
|
1760
|
+
renameProviderReferencesInConfig(config, desiredName, nextName);
|
|
1761
|
+
return { changed: true, renamedProviders: [{ from: desiredName, to: nextName }] };
|
|
1762
|
+
}
|
|
1763
|
+
|
|
1764
|
+
function repairConfigProviders(config, options = {}) {
|
|
1765
|
+
if (!config.models) config.models = {};
|
|
1766
|
+
if (!config.models.providers || typeof config.models.providers !== 'object' || Array.isArray(config.models.providers)) {
|
|
1767
|
+
config.models.providers = {};
|
|
1768
|
+
}
|
|
1769
|
+
|
|
1770
|
+
let changed = false;
|
|
1771
|
+
const renamedProviders = [];
|
|
1772
|
+
|
|
1773
|
+
for (const type of ['claude', 'codex']) {
|
|
1774
|
+
const desiredName = API_CONFIG[type]?.providerName;
|
|
1775
|
+
if (!desiredName) continue;
|
|
1776
|
+
const reserved = reserveProviderName(config, desiredName, type);
|
|
1777
|
+
if (reserved.changed) {
|
|
1778
|
+
changed = true;
|
|
1779
|
+
renamedProviders.push(...reserved.renamedProviders);
|
|
1780
|
+
}
|
|
1781
|
+
}
|
|
1782
|
+
|
|
1783
|
+
const repairedAliases = repairYunyiProviderAliases(config);
|
|
1784
|
+
if (repairedAliases.changed) changed = true;
|
|
1785
|
+
|
|
1786
|
+
for (const [name, providerConfig] of Object.entries(config.models.providers)) {
|
|
1787
|
+
if (sanitizeProviderConfig(name, providerConfig)) {
|
|
1788
|
+
changed = true;
|
|
1789
|
+
}
|
|
1790
|
+
}
|
|
1791
|
+
|
|
1792
|
+
if (sanitizeConfigAuthProfiles(config)) {
|
|
1793
|
+
changed = true;
|
|
1794
|
+
}
|
|
1795
|
+
|
|
1796
|
+
return {
|
|
1797
|
+
changed,
|
|
1798
|
+
renamedProviders: [...renamedProviders, ...repairedAliases.renamedProviders]
|
|
1799
|
+
};
|
|
1800
|
+
}
|
|
1801
|
+
|
|
1802
|
+
function isValidModelRef(value) {
|
|
1803
|
+
return typeof value === 'string' && value.trim().includes('/');
|
|
1804
|
+
}
|
|
1805
|
+
|
|
1806
|
+
function collectConfiguredModelKeys(config) {
|
|
1807
|
+
const keys = [];
|
|
1808
|
+
|
|
1809
|
+
const registered = Object.keys(config?.agents?.defaults?.models || {}).filter(isValidModelRef);
|
|
1810
|
+
keys.push(...registered);
|
|
1811
|
+
|
|
1812
|
+
const providers = config?.models?.providers || {};
|
|
1813
|
+
for (const [providerName, providerConfig] of Object.entries(providers)) {
|
|
1814
|
+
for (const model of providerConfig.models || []) {
|
|
1815
|
+
if (!model?.id) continue;
|
|
1816
|
+
const modelKey = `${providerName}/${model.id}`;
|
|
1817
|
+
if (isValidModelRef(modelKey)) keys.push(modelKey);
|
|
1818
|
+
}
|
|
1819
|
+
}
|
|
1820
|
+
|
|
1821
|
+
return [...new Set(keys)];
|
|
1822
|
+
}
|
|
1823
|
+
|
|
1824
|
+
function inferPrimaryModelKey(config) {
|
|
1825
|
+
return collectConfiguredModelKeys(config)[0] || '';
|
|
1826
|
+
}
|
|
1827
|
+
|
|
1828
|
+
function normalizeDefaultModelSelection(value, config) {
|
|
1829
|
+
if (typeof value === 'string') {
|
|
1830
|
+
const primary = value.trim();
|
|
1831
|
+
return primary ? { primary, fallbacks: [] } : { fallbacks: [] };
|
|
1832
|
+
}
|
|
1833
|
+
|
|
1834
|
+
if (value && typeof value === 'object' && !Array.isArray(value)) {
|
|
1835
|
+
let primary = typeof value.primary === 'string' ? value.primary.trim() : '';
|
|
1836
|
+
const fallbacks = Array.isArray(value.fallbacks)
|
|
1837
|
+
? [...new Set(value.fallbacks.map(item => String(item || '').trim()).filter(isValidModelRef))]
|
|
1838
|
+
: [];
|
|
1839
|
+
|
|
1840
|
+
if (!primary) {
|
|
1841
|
+
primary = inferPrimaryModelKey(config) || fallbacks[0] || '';
|
|
1842
|
+
}
|
|
1843
|
+
|
|
1844
|
+
const normalizedFallbacks = fallbacks.filter(modelKey => modelKey !== primary);
|
|
1845
|
+
const normalized = { fallbacks: normalizedFallbacks };
|
|
1846
|
+
if (primary) normalized.primary = primary;
|
|
1847
|
+
return normalized;
|
|
1848
|
+
}
|
|
1849
|
+
|
|
1850
|
+
const inferred = inferPrimaryModelKey(config);
|
|
1851
|
+
return inferred ? { primary: inferred, fallbacks: [] } : { fallbacks: [] };
|
|
1852
|
+
}
|
|
1853
|
+
|
|
1854
|
+
function sanitizeDefaultModelSelection(config) {
|
|
1855
|
+
if (!config?.agents?.defaults) return;
|
|
1856
|
+
const normalized = normalizeDefaultModelSelection(config.agents.defaults.model, config);
|
|
1857
|
+
if (normalized.primary) {
|
|
1858
|
+
config.agents.defaults.model = normalized;
|
|
1859
|
+
} else {
|
|
1860
|
+
delete config.agents.defaults.model;
|
|
1861
|
+
}
|
|
1862
|
+
}
|
|
1863
|
+
|
|
1332
1864
|
const OPENCLAW_INVALID_ROOT_KEYS = ['model'];
|
|
1333
1865
|
|
|
1334
1866
|
function sanitizeRootKeys(config) {
|
|
@@ -1344,7 +1876,7 @@ function ensureConfigStructure(config) {
|
|
|
1344
1876
|
if (!next.models.providers) next.models.providers = {};
|
|
1345
1877
|
if (!next.agents) next.agents = {};
|
|
1346
1878
|
if (!next.agents.defaults) next.agents.defaults = {};
|
|
1347
|
-
|
|
1879
|
+
next.agents.defaults.model = normalizeDefaultModelSelection(next.agents.defaults.model, next);
|
|
1348
1880
|
if (!next.agents.defaults.models || Array.isArray(next.agents.defaults.models) || typeof next.agents.defaults.models !== 'object') {
|
|
1349
1881
|
next.agents.defaults.models = coerceModelsRecord(next.agents.defaults.models);
|
|
1350
1882
|
}
|
|
@@ -1515,6 +2047,32 @@ function syncMirroredAuthStores(paths) {
|
|
|
1515
2047
|
}
|
|
1516
2048
|
}
|
|
1517
2049
|
|
|
2050
|
+
function renameProviderInAuthStore(paths, oldProvider, newProvider) {
|
|
2051
|
+
if (!paths?.authProfiles || !oldProvider || !newProvider || oldProvider === newProvider) return false;
|
|
2052
|
+
const store = readAuthStore(paths.authProfiles);
|
|
2053
|
+
let changed = false;
|
|
2054
|
+
|
|
2055
|
+
for (const key of Object.keys(store.profiles)) {
|
|
2056
|
+
if (!key.startsWith(`${oldProvider}:`)) continue;
|
|
2057
|
+
const suffix = key.slice(oldProvider.length + 1);
|
|
2058
|
+
const nextKey = `${newProvider}:${suffix}`;
|
|
2059
|
+
const profile = { ...(store.profiles[key] || {}) };
|
|
2060
|
+
if (!store.profiles[nextKey]) {
|
|
2061
|
+
profile.provider = newProvider;
|
|
2062
|
+
store.profiles[nextKey] = profile;
|
|
2063
|
+
}
|
|
2064
|
+
delete store.profiles[key];
|
|
2065
|
+
changed = true;
|
|
2066
|
+
}
|
|
2067
|
+
|
|
2068
|
+
if (changed) {
|
|
2069
|
+
writeAuthStore(paths.authProfiles, store);
|
|
2070
|
+
syncMirroredAuthStores(paths);
|
|
2071
|
+
}
|
|
2072
|
+
|
|
2073
|
+
return changed;
|
|
2074
|
+
}
|
|
2075
|
+
|
|
1518
2076
|
function pruneAuthProfilesExceptWithSync(paths, keepProviders = []) {
|
|
1519
2077
|
const removed = pruneAuthProfilesExcept(paths.authProfiles, keepProviders);
|
|
1520
2078
|
syncMirroredAuthStores(paths);
|
|
@@ -1526,6 +2084,16 @@ function updateAuthProfilesWithSync(paths, providerName, apiKey) {
|
|
|
1526
2084
|
syncMirroredAuthStores(paths);
|
|
1527
2085
|
}
|
|
1528
2086
|
|
|
2087
|
+
function applyConfigRepairsWithSync(config, paths) {
|
|
2088
|
+
const repairResult = repairConfigProviders(config);
|
|
2089
|
+
if (paths && repairResult.renamedProviders.length > 0) {
|
|
2090
|
+
for (const item of repairResult.renamedProviders) {
|
|
2091
|
+
renameProviderInAuthStore(paths, item.from, item.to);
|
|
2092
|
+
}
|
|
2093
|
+
}
|
|
2094
|
+
return repairResult;
|
|
2095
|
+
}
|
|
2096
|
+
|
|
1529
2097
|
function pruneAuthProfilesByPrefix(authProfilesPath, prefixBase, keepProviders = []) {
|
|
1530
2098
|
const keepSet = new Set(keepProviders);
|
|
1531
2099
|
const store = readAuthStore(authProfilesPath);
|
|
@@ -1619,6 +2187,20 @@ async function promptApiKey(message, defaultValue) {
|
|
|
1619
2187
|
return '';
|
|
1620
2188
|
}
|
|
1621
2189
|
|
|
2190
|
+
async function confirmImmediateTest(args = {}, message = '是否立即测试连接?', defaultValue = true) {
|
|
2191
|
+
if (args.test !== undefined) {
|
|
2192
|
+
return !['false', '0', 'no'].includes(String(args.test).toLowerCase());
|
|
2193
|
+
}
|
|
2194
|
+
|
|
2195
|
+
const { shouldTest } = await inquirer.prompt([{
|
|
2196
|
+
type: 'confirm',
|
|
2197
|
+
name: 'shouldTest',
|
|
2198
|
+
message,
|
|
2199
|
+
default: defaultValue
|
|
2200
|
+
}]);
|
|
2201
|
+
return shouldTest;
|
|
2202
|
+
}
|
|
2203
|
+
|
|
1622
2204
|
function extendPathEnv(preferredNodePath) {
|
|
1623
2205
|
const current = process.env.PATH || '';
|
|
1624
2206
|
const parts = current.split(path.delimiter).filter(Boolean);
|
|
@@ -1816,6 +2398,259 @@ function getCliMeta() {
|
|
|
1816
2398
|
return { cliBinary, cliName, nodeMajor };
|
|
1817
2399
|
}
|
|
1818
2400
|
|
|
2401
|
+
function resolveCommandBinary(names) {
|
|
2402
|
+
const candidates = Array.isArray(names) ? names : [names];
|
|
2403
|
+
const isWin = process.platform === 'win32';
|
|
2404
|
+
const searchDirs = (process.env.PATH || '').split(path.delimiter).concat(EXTRA_BIN_DIRS).filter(Boolean);
|
|
2405
|
+
|
|
2406
|
+
for (const name of candidates) {
|
|
2407
|
+
const variants = isWin ? [`${name}.cmd`, `${name}.exe`, name] : [name];
|
|
2408
|
+
for (const variant of variants) {
|
|
2409
|
+
for (const dir of searchDirs) {
|
|
2410
|
+
const full = path.join(dir, variant);
|
|
2411
|
+
try {
|
|
2412
|
+
if (fs.existsSync(full) && fs.statSync(full).isFile()) {
|
|
2413
|
+
return full;
|
|
2414
|
+
}
|
|
2415
|
+
} catch { }
|
|
2416
|
+
}
|
|
2417
|
+
}
|
|
2418
|
+
|
|
2419
|
+
if (isWin) {
|
|
2420
|
+
const result = safeExec(`where ${name}`);
|
|
2421
|
+
if (result.ok && result.output) {
|
|
2422
|
+
return result.output.split('\n')[0].trim();
|
|
2423
|
+
}
|
|
2424
|
+
} else {
|
|
2425
|
+
for (const sh of ['/bin/zsh', '/bin/bash', '/bin/sh']) {
|
|
2426
|
+
if (!fs.existsSync(sh)) continue;
|
|
2427
|
+
const result = safeExec(`${sh} -lc "command -v ${name}"`);
|
|
2428
|
+
if (result.ok && result.output) {
|
|
2429
|
+
return result.output.split('\n')[0].trim();
|
|
2430
|
+
}
|
|
2431
|
+
}
|
|
2432
|
+
}
|
|
2433
|
+
}
|
|
2434
|
+
|
|
2435
|
+
return null;
|
|
2436
|
+
}
|
|
2437
|
+
|
|
2438
|
+
function readJsonIfExists(filePath) {
|
|
2439
|
+
if (!filePath || !fs.existsSync(filePath)) return null;
|
|
2440
|
+
try {
|
|
2441
|
+
return JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
|
2442
|
+
} catch {
|
|
2443
|
+
return null;
|
|
2444
|
+
}
|
|
2445
|
+
}
|
|
2446
|
+
|
|
2447
|
+
function getClaudeCodeSettingsPath() {
|
|
2448
|
+
return path.join(os.homedir(), '.claude', 'settings.json');
|
|
2449
|
+
}
|
|
2450
|
+
|
|
2451
|
+
function getOpencodeConfigPath() {
|
|
2452
|
+
const home = os.homedir();
|
|
2453
|
+
return process.platform === 'win32'
|
|
2454
|
+
? path.join(process.env.APPDATA || path.join(home, 'AppData', 'Roaming'), 'opencode', 'opencode.json')
|
|
2455
|
+
: path.join(home, '.config', 'opencode', 'opencode.json');
|
|
2456
|
+
}
|
|
2457
|
+
|
|
2458
|
+
function getCodexCliPaths() {
|
|
2459
|
+
const codexDir = path.join(os.homedir(), '.codex');
|
|
2460
|
+
return {
|
|
2461
|
+
configPath: path.join(codexDir, 'config.toml'),
|
|
2462
|
+
authPath: path.join(codexDir, 'auth.json')
|
|
2463
|
+
};
|
|
2464
|
+
}
|
|
2465
|
+
|
|
2466
|
+
function readClaudeCodeCliConfig() {
|
|
2467
|
+
const settingsPath = getClaudeCodeSettingsPath();
|
|
2468
|
+
const settings = readJsonIfExists(settingsPath) || {};
|
|
2469
|
+
return {
|
|
2470
|
+
settingsPath,
|
|
2471
|
+
modelId: settings.model || settings.env?.ANTHROPIC_MODEL || process.env.ANTHROPIC_MODEL || getDefaultClaudeModel().id,
|
|
2472
|
+
baseUrl: settings.env?.ANTHROPIC_BASE_URL || settings.apiBaseUrl || process.env.ANTHROPIC_BASE_URL || '',
|
|
2473
|
+
apiKey: settings.env?.ANTHROPIC_AUTH_TOKEN || process.env.ANTHROPIC_AUTH_TOKEN || '',
|
|
2474
|
+
configured: fs.existsSync(settingsPath)
|
|
2475
|
+
};
|
|
2476
|
+
}
|
|
2477
|
+
|
|
2478
|
+
function readOpencodeCliConfig() {
|
|
2479
|
+
const configPath = getOpencodeConfigPath();
|
|
2480
|
+
const config = readJsonIfExists(configPath) || {};
|
|
2481
|
+
return {
|
|
2482
|
+
configPath,
|
|
2483
|
+
modelKey: config.model || `yunyi-claude/${getDefaultClaudeModel().id}`,
|
|
2484
|
+
configured: fs.existsSync(configPath),
|
|
2485
|
+
config
|
|
2486
|
+
};
|
|
2487
|
+
}
|
|
2488
|
+
|
|
2489
|
+
function readCodexCliConfig() {
|
|
2490
|
+
const { configPath, authPath } = getCodexCliPaths();
|
|
2491
|
+
const configRaw = fs.existsSync(configPath) ? fs.readFileSync(configPath, 'utf8') : '';
|
|
2492
|
+
const auth = readJsonIfExists(authPath) || {};
|
|
2493
|
+
const model = (configRaw.match(/^model\s*=\s*"([^"]+)"\s*$/m) || [])[1] || getDefaultCodexModel().id;
|
|
2494
|
+
const provider = (configRaw.match(/^model_provider\s*=\s*"([^"]+)"\s*$/m) || [])[1] || 'yunyi-codex';
|
|
2495
|
+
const providerBlockRegex = new RegExp(`\\[model_providers\\.${escapeRegExp(provider)}\\]([\\s\\S]*?)(?=\\n\\[|$)`, 'm');
|
|
2496
|
+
const providerBlock = (configRaw.match(providerBlockRegex) || [])[1] || '';
|
|
2497
|
+
const baseUrl = (providerBlock.match(/base_url\s*=\s*"([^"]+)"/) || [])[1] || '';
|
|
2498
|
+
const apiKey = (providerBlock.match(/experimental_bearer_token\s*=\s*"([^"]+)"/) || [])[1] || auth.OPENAI_API_KEY || '';
|
|
2499
|
+
return {
|
|
2500
|
+
configPath,
|
|
2501
|
+
authPath,
|
|
2502
|
+
modelId: model,
|
|
2503
|
+
provider,
|
|
2504
|
+
baseUrl,
|
|
2505
|
+
apiKey,
|
|
2506
|
+
configured: fs.existsSync(configPath)
|
|
2507
|
+
};
|
|
2508
|
+
}
|
|
2509
|
+
|
|
2510
|
+
function cleanCliTestOutput(text) {
|
|
2511
|
+
return String(text || '')
|
|
2512
|
+
.replace(/\u001b\[[0-9;]*m/g, '')
|
|
2513
|
+
.replace(/\(node:\d+\) Warning: Setting the NODE_TLS_REJECT_UNAUTHORIZED[\s\S]*?(?=\n\S|$)/g, '')
|
|
2514
|
+
.replace(/\s+$/g, '')
|
|
2515
|
+
.trim();
|
|
2516
|
+
}
|
|
2517
|
+
|
|
2518
|
+
function summarizeCliTestOutput(text) {
|
|
2519
|
+
const lines = cleanCliTestOutput(text)
|
|
2520
|
+
.split('\n')
|
|
2521
|
+
.map(line => line.trim())
|
|
2522
|
+
.filter(Boolean)
|
|
2523
|
+
.filter(line => !/^logs?:/i.test(line));
|
|
2524
|
+
if (lines.length === 0) return '';
|
|
2525
|
+
return lines[0].slice(0, 160);
|
|
2526
|
+
}
|
|
2527
|
+
|
|
2528
|
+
function buildReadableAgentError(text, fallback = '') {
|
|
2529
|
+
const cleaned = cleanCliTestOutput(text);
|
|
2530
|
+
if (!cleaned) return fallback;
|
|
2531
|
+
const lines = cleaned.split('\n').map(line => line.trim()).filter(Boolean);
|
|
2532
|
+
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));
|
|
2533
|
+
return (preferred || lines[0] || fallback).slice(0, 300);
|
|
2534
|
+
}
|
|
2535
|
+
|
|
2536
|
+
function looksLikeCliTestError(text) {
|
|
2537
|
+
const lower = cleanCliTestOutput(text).toLowerCase();
|
|
2538
|
+
if (!lower) return false;
|
|
2539
|
+
return [
|
|
2540
|
+
'all models failed',
|
|
2541
|
+
'auth_permanent',
|
|
2542
|
+
'auth issue',
|
|
2543
|
+
'unauthorized',
|
|
2544
|
+
'forbidden',
|
|
2545
|
+
'missing environment variable',
|
|
2546
|
+
'gateway agent failed',
|
|
2547
|
+
'error:'
|
|
2548
|
+
].some(pattern => lower.includes(pattern));
|
|
2549
|
+
}
|
|
2550
|
+
|
|
2551
|
+
function runCliTestCandidates(name, commands, env) {
|
|
2552
|
+
let lastError = '命令执行失败';
|
|
2553
|
+
for (const command of commands) {
|
|
2554
|
+
const result = safeExec(command, { timeout: 120000, env, maxBuffer: 1024 * 1024 });
|
|
2555
|
+
const combined = cleanCliTestOutput(`${result.output || ''}\n${result.stdout || ''}\n${result.stderr || ''}`);
|
|
2556
|
+
if (result.ok && !looksLikeCliTestError(combined)) {
|
|
2557
|
+
return { name, status: 'success', detail: summarizeCliTestOutput(combined) || '连接成功' };
|
|
2558
|
+
}
|
|
2559
|
+
lastError = summarizeCliTestOutput(combined) || result.error || lastError;
|
|
2560
|
+
}
|
|
2561
|
+
return { name, status: 'failed', detail: lastError };
|
|
2562
|
+
}
|
|
2563
|
+
|
|
2564
|
+
function testClaudeCodeCliConnection() {
|
|
2565
|
+
const cliBinary = resolveCommandBinary('claude');
|
|
2566
|
+
if (!cliBinary) return { name: 'Claude Code CLI', status: 'skipped', detail: '未安装 claude 命令' };
|
|
2567
|
+
|
|
2568
|
+
const config = readClaudeCodeCliConfig();
|
|
2569
|
+
if (!config.configured) return { name: 'Claude Code CLI', status: 'skipped', detail: '未检测到 ~/.claude/settings.json' };
|
|
2570
|
+
if (!config.baseUrl || !config.apiKey) return { name: 'Claude Code CLI', status: 'failed', detail: 'Claude Code 配置缺少 Base URL 或 API Key' };
|
|
2571
|
+
|
|
2572
|
+
const env = {
|
|
2573
|
+
...process.env,
|
|
2574
|
+
PATH: extendPathEnv(null),
|
|
2575
|
+
ANTHROPIC_BASE_URL: config.baseUrl,
|
|
2576
|
+
ANTHROPIC_AUTH_TOKEN: config.apiKey,
|
|
2577
|
+
ANTHROPIC_MODEL: config.modelId,
|
|
2578
|
+
NODE_TLS_REJECT_UNAUTHORIZED: '0',
|
|
2579
|
+
NODE_NO_WARNINGS: '1'
|
|
2580
|
+
};
|
|
2581
|
+
|
|
2582
|
+
return runCliTestCandidates('Claude Code CLI', [
|
|
2583
|
+
`${shellQuote(cliBinary)} -p --model ${shellQuote(config.modelId)} --output-format text ${shellQuote('请只回复 OK')}`,
|
|
2584
|
+
`${shellQuote(cliBinary)} -p --model ${shellQuote(config.modelId)} ${shellQuote('请只回复 OK')}`,
|
|
2585
|
+
`${shellQuote(cliBinary)} -p ${shellQuote('请只回复 OK')}`
|
|
2586
|
+
], env);
|
|
2587
|
+
}
|
|
2588
|
+
|
|
2589
|
+
function testOpencodeCliConnection() {
|
|
2590
|
+
const cliBinary = resolveCommandBinary('opencode');
|
|
2591
|
+
if (!cliBinary) return { name: 'Opencode CLI', status: 'skipped', detail: '未安装 opencode 命令' };
|
|
2592
|
+
|
|
2593
|
+
const config = readOpencodeCliConfig();
|
|
2594
|
+
if (!config.configured) return { name: 'Opencode CLI', status: 'skipped', detail: '未检测到 opencode 配置文件' };
|
|
2595
|
+
|
|
2596
|
+
const env = {
|
|
2597
|
+
...process.env,
|
|
2598
|
+
PATH: extendPathEnv(null),
|
|
2599
|
+
NODE_TLS_REJECT_UNAUTHORIZED: '0',
|
|
2600
|
+
NODE_NO_WARNINGS: '1'
|
|
2601
|
+
};
|
|
2602
|
+
|
|
2603
|
+
return runCliTestCandidates('Opencode CLI', [
|
|
2604
|
+
`${shellQuote(cliBinary)} run --model ${shellQuote(config.modelKey)} ${shellQuote('请只回复 OK')}`,
|
|
2605
|
+
`${shellQuote(cliBinary)} run ${shellQuote('请只回复 OK')} --model ${shellQuote(config.modelKey)}`,
|
|
2606
|
+
`${shellQuote(cliBinary)} -m ${shellQuote(config.modelKey)} run ${shellQuote('请只回复 OK')}`
|
|
2607
|
+
], env);
|
|
2608
|
+
}
|
|
2609
|
+
|
|
2610
|
+
function testCodexCliConnection() {
|
|
2611
|
+
const cliBinary = resolveCommandBinary('codex');
|
|
2612
|
+
if (!cliBinary) return { name: 'Codex CLI', status: 'skipped', detail: '未安装 codex 命令' };
|
|
2613
|
+
|
|
2614
|
+
const config = readCodexCliConfig();
|
|
2615
|
+
if (!config.configured) return { name: 'Codex CLI', status: 'skipped', detail: '未检测到 ~/.codex/config.toml' };
|
|
2616
|
+
if (!config.apiKey) return { name: 'Codex CLI', status: 'failed', detail: 'Codex CLI 配置缺少 API Key' };
|
|
2617
|
+
|
|
2618
|
+
const env = {
|
|
2619
|
+
...process.env,
|
|
2620
|
+
PATH: extendPathEnv(null),
|
|
2621
|
+
OPENAI_API_KEY: config.apiKey,
|
|
2622
|
+
NODE_TLS_REJECT_UNAUTHORIZED: '0',
|
|
2623
|
+
NODE_NO_WARNINGS: '1'
|
|
2624
|
+
};
|
|
2625
|
+
|
|
2626
|
+
return runCliTestCandidates('Codex CLI', [
|
|
2627
|
+
`${shellQuote(cliBinary)} exec --skip-git-repo-check ${shellQuote('请只回复 OK')}`,
|
|
2628
|
+
`${shellQuote(cliBinary)} exec ${shellQuote('请只回复 OK')}`
|
|
2629
|
+
], env);
|
|
2630
|
+
}
|
|
2631
|
+
|
|
2632
|
+
async function testAdditionalCliConnections(args = {}, options = {}) {
|
|
2633
|
+
if (args['no-app-test'] || args.noAppTest) return;
|
|
2634
|
+
|
|
2635
|
+
const requested = new Set((options.only || ['claude', 'opencode', 'codex']).map(item => String(item)));
|
|
2636
|
+
const results = [];
|
|
2637
|
+
if (requested.has('claude')) results.push(testClaudeCodeCliConnection());
|
|
2638
|
+
if (requested.has('opencode')) results.push(testOpencodeCliConnection());
|
|
2639
|
+
if (requested.has('codex')) results.push(testCodexCliConnection());
|
|
2640
|
+
if (results.length === 0) return;
|
|
2641
|
+
|
|
2642
|
+
console.log(chalk.cyan('\n附加测试: 其他 CLI 连接...'));
|
|
2643
|
+
for (const result of results) {
|
|
2644
|
+
if (result.status === 'success') {
|
|
2645
|
+
console.log(chalk.green(`✅ ${result.name}: ${result.detail}`));
|
|
2646
|
+
} else if (result.status === 'skipped') {
|
|
2647
|
+
console.log(chalk.gray(`○ ${result.name}: ${result.detail}`));
|
|
2648
|
+
} else {
|
|
2649
|
+
console.log(chalk.red(`❌ ${result.name}: ${result.detail}`));
|
|
2650
|
+
}
|
|
2651
|
+
}
|
|
2652
|
+
}
|
|
2653
|
+
|
|
1819
2654
|
function getNodeMajor(versionOutput) {
|
|
1820
2655
|
const match = String(versionOutput || '').trim().match(/^v?(\d+)/);
|
|
1821
2656
|
return match ? Number(match[1]) : null;
|
|
@@ -1868,11 +2703,20 @@ function findCompatibleNode(minMajor = 22) {
|
|
|
1868
2703
|
function autoFixConfig(paths) {
|
|
1869
2704
|
try {
|
|
1870
2705
|
const config = readConfig(paths.openclawConfig);
|
|
1871
|
-
if (!config
|
|
2706
|
+
if (!config) return;
|
|
1872
2707
|
|
|
1873
2708
|
let changed = false;
|
|
1874
2709
|
const codexProviderName = API_CONFIG.codex?.providerName;
|
|
1875
|
-
const
|
|
2710
|
+
const originalModelJson = JSON.stringify(config.agents?.defaults?.model ?? null);
|
|
2711
|
+
ensureConfigStructure(config);
|
|
2712
|
+
sanitizeDefaultModelSelection(config);
|
|
2713
|
+
const repairResult = applyConfigRepairsWithSync(config, paths);
|
|
2714
|
+
if (repairResult.changed) changed = true;
|
|
2715
|
+
const codexProvider = codexProviderName && config.models.providers?.[codexProviderName];
|
|
2716
|
+
const nextModelJson = JSON.stringify(config.agents?.defaults?.model ?? null);
|
|
2717
|
+
if (originalModelJson !== nextModelJson) {
|
|
2718
|
+
changed = true;
|
|
2719
|
+
}
|
|
1876
2720
|
|
|
1877
2721
|
// 修复: openai-responses → openai-completions(云翼服务器不支持 /v1/responses)
|
|
1878
2722
|
if (codexProvider && codexProvider.api === 'openai-responses') {
|
|
@@ -1888,7 +2732,7 @@ function autoFixConfig(paths) {
|
|
|
1888
2732
|
|
|
1889
2733
|
if (changed) {
|
|
1890
2734
|
writeConfig(paths.openclawConfig, config);
|
|
1891
|
-
console.log(chalk.green('✓
|
|
2735
|
+
console.log(chalk.green('✓ 已自动修复配置(模型结构 / API 协议)'));
|
|
1892
2736
|
}
|
|
1893
2737
|
} catch { /* ignore */ }
|
|
1894
2738
|
}
|
|
@@ -2391,26 +3235,7 @@ async function quickSetup(paths, args = {}) {
|
|
|
2391
3235
|
|
|
2392
3236
|
let config = readConfig(paths.openclawConfig) || {};
|
|
2393
3237
|
config = ensureConfigStructure(config);
|
|
2394
|
-
|
|
2395
|
-
const existingProviders = Object.keys(config.models.providers || {});
|
|
2396
|
-
const toRemove = existingProviders.filter(name => name !== providerName);
|
|
2397
|
-
if (toRemove.length > 0 && !args.force) {
|
|
2398
|
-
const { overwrite } = await inquirer.prompt([{
|
|
2399
|
-
type: 'confirm',
|
|
2400
|
-
name: 'overwrite',
|
|
2401
|
-
message: `检测到已有中转配置: ${existingProviders.join(', ')},将仅保留 ${providerName}。是否继续?`,
|
|
2402
|
-
default: false
|
|
2403
|
-
}]);
|
|
2404
|
-
if (!overwrite) {
|
|
2405
|
-
console.log(chalk.gray('已取消'));
|
|
2406
|
-
return;
|
|
2407
|
-
}
|
|
2408
|
-
}
|
|
2409
|
-
|
|
2410
|
-
if (toRemove.length > 0) {
|
|
2411
|
-
pruneProvidersExcept(config, [providerName]);
|
|
2412
|
-
pruneAuthProfilesExceptWithSync(paths, [providerName]);
|
|
2413
|
-
}
|
|
3238
|
+
const repairResult = applyConfigRepairsWithSync(config, paths);
|
|
2414
3239
|
|
|
2415
3240
|
config.models.providers[providerName] = {
|
|
2416
3241
|
baseUrl: normalizedBaseUrl,
|
|
@@ -2458,6 +3283,9 @@ async function quickSetup(paths, args = {}) {
|
|
|
2458
3283
|
if (setPrimary) {
|
|
2459
3284
|
console.log(chalk.yellow(` 主模型: ${modelKey}`));
|
|
2460
3285
|
}
|
|
3286
|
+
if (repairResult.renamedProviders.length > 0) {
|
|
3287
|
+
console.log(chalk.gray(` 已保留并修复冲突 provider: ${repairResult.renamedProviders.map(item => `${item.from}→${item.to}`).join(', ')}`));
|
|
3288
|
+
}
|
|
2461
3289
|
}
|
|
2462
3290
|
|
|
2463
3291
|
async function presetClaude(paths, args = {}) {
|
|
@@ -2504,31 +3332,10 @@ async function presetClaude(paths, args = {}) {
|
|
|
2504
3332
|
}
|
|
2505
3333
|
|
|
2506
3334
|
const config = ensureConfigStructure(readConfig(paths.openclawConfig) || {});
|
|
3335
|
+
const repairResult = applyConfigRepairsWithSync(config, paths);
|
|
2507
3336
|
|
|
2508
3337
|
const providerName = (args['provider-name'] || args.provider || providerPrefix).toString().trim() || apiConfig.providerName;
|
|
2509
|
-
|
|
2510
|
-
const existingProviders = Object.keys(config.models.providers || {});
|
|
2511
|
-
const toRemove = existingProviders.filter(name => name !== providerName);
|
|
2512
|
-
|
|
2513
|
-
if (toRemove.length > 0 && !args.force) {
|
|
2514
|
-
const { overwrite } = await inquirer.prompt([{
|
|
2515
|
-
type: 'confirm',
|
|
2516
|
-
name: 'overwrite',
|
|
2517
|
-
message: `检测到已有中转配置: ${existingProviders.join(', ')},将仅保留 ${providerName}。是否继续?`,
|
|
2518
|
-
default: false
|
|
2519
|
-
}]);
|
|
2520
|
-
if (!overwrite) {
|
|
2521
|
-
console.log(chalk.gray('已取消'));
|
|
2522
|
-
return;
|
|
2523
|
-
}
|
|
2524
|
-
}
|
|
2525
|
-
|
|
2526
|
-
const removedProviders = toRemove.length > 0
|
|
2527
|
-
? pruneProvidersExcept(config, [providerName])
|
|
2528
|
-
: [];
|
|
2529
|
-
if (removedProviders.length > 0) {
|
|
2530
|
-
pruneAuthProfilesExceptWithSync(paths, [providerName]);
|
|
2531
|
-
}
|
|
3338
|
+
const removedProviders = repairResult.renamedProviders.map(item => item.from);
|
|
2532
3339
|
|
|
2533
3340
|
const baseUrl = buildFullUrl(selectedEndpoint.url, 'claude');
|
|
2534
3341
|
|
|
@@ -2632,7 +3439,7 @@ async function presetClaude(paths, args = {}) {
|
|
|
2632
3439
|
ensureGatewaySettings(config);
|
|
2633
3440
|
writeConfigWithSync(paths, config);
|
|
2634
3441
|
updateAuthProfilesWithSync(paths, providerName, apiKey);
|
|
2635
|
-
const extSynced = syncExternalTools('claude', baseUrl, apiKey);
|
|
3442
|
+
const extSynced = syncExternalTools('claude', baseUrl, apiKey, { claudeModelId: modelId, opencodeDefaultModelKey: `yunyi-claude/${modelId}` });
|
|
2636
3443
|
writeSpinner.succeed('配置写入完成');
|
|
2637
3444
|
|
|
2638
3445
|
console.log(chalk.green('\n✅ Claude 节点配置完成!'));
|
|
@@ -2641,6 +3448,9 @@ async function presetClaude(paths, args = {}) {
|
|
|
2641
3448
|
console.log(chalk.gray(` 模型: ${modelName}`));
|
|
2642
3449
|
console.log(chalk.gray(' API Key: 已设置'));
|
|
2643
3450
|
if (extSynced.length > 0) console.log(chalk.gray(` 同步: ${extSynced.join(', ')}`));
|
|
3451
|
+
if (repairResult.renamedProviders.length > 0) {
|
|
3452
|
+
console.log(chalk.gray(` 已保留并修复冲突 provider: ${repairResult.renamedProviders.map(item => `${item.from}→${item.to}`).join(', ')}`));
|
|
3453
|
+
}
|
|
2644
3454
|
|
|
2645
3455
|
const shouldTestGateway = args.test !== undefined
|
|
2646
3456
|
? !['false', '0', 'no'].includes(String(args.test).toLowerCase())
|
|
@@ -2701,28 +3511,8 @@ async function presetCodex(paths, args = {}) {
|
|
|
2701
3511
|
}
|
|
2702
3512
|
|
|
2703
3513
|
const config = ensureConfigStructure(readConfig(paths.openclawConfig) || {});
|
|
2704
|
-
const
|
|
2705
|
-
const
|
|
2706
|
-
|
|
2707
|
-
if (toRemove.length > 0 && !args.force) {
|
|
2708
|
-
const { overwrite } = await inquirer.prompt([{
|
|
2709
|
-
type: 'confirm',
|
|
2710
|
-
name: 'overwrite',
|
|
2711
|
-
message: `检测到已有中转配置: ${existingProviders.join(', ')},将仅保留 ${providerName}。是否继续?`,
|
|
2712
|
-
default: false
|
|
2713
|
-
}]);
|
|
2714
|
-
if (!overwrite) {
|
|
2715
|
-
console.log(chalk.gray('已取消'));
|
|
2716
|
-
return;
|
|
2717
|
-
}
|
|
2718
|
-
}
|
|
2719
|
-
|
|
2720
|
-
const removedProviders = toRemove.length > 0
|
|
2721
|
-
? pruneProvidersExcept(config, [providerName])
|
|
2722
|
-
: [];
|
|
2723
|
-
if (removedProviders.length > 0) {
|
|
2724
|
-
pruneAuthProfilesExceptWithSync(paths, [providerName]);
|
|
2725
|
-
}
|
|
3514
|
+
const repairResult = applyConfigRepairsWithSync(config, paths);
|
|
3515
|
+
const removedProviders = repairResult.renamedProviders.map(item => item.from);
|
|
2726
3516
|
|
|
2727
3517
|
const baseUrl = buildFullUrl(selectedEndpoint.url, 'codex');
|
|
2728
3518
|
|
|
@@ -2826,7 +3616,7 @@ async function presetCodex(paths, args = {}) {
|
|
|
2826
3616
|
cleanupConflictingEnvVars(config, baseUrl, apiKey);
|
|
2827
3617
|
writeConfigWithSync(paths, config);
|
|
2828
3618
|
updateAuthProfilesWithSync(paths, providerName, apiKey);
|
|
2829
|
-
const extSynced2 = syncExternalTools('codex', baseUrl, apiKey);
|
|
3619
|
+
const extSynced2 = syncExternalTools('codex', baseUrl, apiKey, { modelId });
|
|
2830
3620
|
writeSpinner2.succeed('配置写入完成');
|
|
2831
3621
|
|
|
2832
3622
|
console.log(chalk.green('\n✅ Codex 节点配置完成!'));
|
|
@@ -2835,6 +3625,9 @@ async function presetCodex(paths, args = {}) {
|
|
|
2835
3625
|
console.log(chalk.gray(` 模型: ${modelName}`));
|
|
2836
3626
|
console.log(chalk.gray(' API Key: 已设置'));
|
|
2837
3627
|
if (extSynced2.length > 0) console.log(chalk.gray(` 同步: ${extSynced2.join(', ')}`));
|
|
3628
|
+
if (repairResult.renamedProviders.length > 0) {
|
|
3629
|
+
console.log(chalk.gray(` 已保留并修复冲突 provider: ${repairResult.renamedProviders.map(item => `${item.from}→${item.to}`).join(', ')}`));
|
|
3630
|
+
}
|
|
2838
3631
|
|
|
2839
3632
|
const shouldTestGateway = args.test !== undefined
|
|
2840
3633
|
? !['false', '0', 'no'].includes(String(args.test).toLowerCase())
|
|
@@ -2943,6 +3736,7 @@ async function autoActivate(paths, args = {}) {
|
|
|
2943
3736
|
|
|
2944
3737
|
// ---- 构建配置 ----
|
|
2945
3738
|
const config = ensureConfigStructure(readConfig(paths.openclawConfig) || {});
|
|
3739
|
+
applyConfigRepairsWithSync(config, paths);
|
|
2946
3740
|
|
|
2947
3741
|
const claudeBaseUrl = buildFullUrl(selectedEndpoint.url, 'claude');
|
|
2948
3742
|
const codexBaseUrl = buildFullUrl(selectedEndpoint.url, 'codex');
|
|
@@ -3012,8 +3806,9 @@ async function autoActivate(paths, args = {}) {
|
|
|
3012
3806
|
updateAuthProfilesWithSync(paths, claudeProviderName, apiKey);
|
|
3013
3807
|
updateAuthProfilesWithSync(paths, codexProviderName, apiKey);
|
|
3014
3808
|
const extSynced = [];
|
|
3015
|
-
|
|
3016
|
-
try { syncExternalTools('
|
|
3809
|
+
const opencodeDefaultModelKey = isClaudePrimary ? `yunyi-claude/${claudeModelId}` : `yunyi-codex/${codexModelId}`;
|
|
3810
|
+
try { extSynced.push(...syncExternalTools('claude', claudeBaseUrl, apiKey, { codexBaseUrl, claudeModelId, opencodeDefaultModelKey })); } catch { /* ignore */ }
|
|
3811
|
+
try { extSynced.push(...syncExternalTools('codex', codexBaseUrl, apiKey, { modelId: codexModelId })); } catch { /* ignore */ }
|
|
3017
3812
|
writeSpinner.succeed('配置写入完成');
|
|
3018
3813
|
|
|
3019
3814
|
// ---- 输出结果 ----
|
|
@@ -3025,7 +3820,7 @@ async function autoActivate(paths, args = {}) {
|
|
|
3025
3820
|
console.log(chalk.gray(' API Key: 已设置'));
|
|
3026
3821
|
if (extSynced.length > 0) console.log(chalk.gray(` 同步: ${extSynced.join(', ')}`));
|
|
3027
3822
|
console.log(chalk.gray(' 若遇 certificate 报错,请新开终端或执行 source ~/.zshrc 后重试(已放宽 TLS 校验)'));
|
|
3028
|
-
console.log(chalk.gray(
|
|
3823
|
+
console.log(chalk.gray(` 使用 OpenCode 时可在界面中切换 ${getOpencodeSwitchHint()};Codex 仅支持 GPT`));
|
|
3029
3824
|
|
|
3030
3825
|
const gwPort = config.gateway?.port || 18789;
|
|
3031
3826
|
const gwToken = config.gateway?.auth?.token;
|
|
@@ -3056,8 +3851,7 @@ async function autoActivate(paths, args = {}) {
|
|
|
3056
3851
|
// ============ 单独配置 Claude Code CLI ============
|
|
3057
3852
|
async function activateClaudeCode(paths, args = {}) {
|
|
3058
3853
|
console.log(chalk.cyan.bold('\n🔧 配置 Claude Code CLI\n'));
|
|
3059
|
-
|
|
3060
|
-
const claudeApiConfig = API_CONFIG.claude;
|
|
3854
|
+
const selectedModel = await promptClaudeModelSelection(args);
|
|
3061
3855
|
|
|
3062
3856
|
// ---- 测速选节点 ----
|
|
3063
3857
|
const shouldTest = !(args['no-test'] || args.noTest);
|
|
@@ -3118,25 +3912,28 @@ async function activateClaudeCode(paths, args = {}) {
|
|
|
3118
3912
|
// ---- 写入配置 ----
|
|
3119
3913
|
const claudeBaseUrl = buildFullUrl(selectedEndpoint.url, 'claude');
|
|
3120
3914
|
const writeSpinner = ora({ text: '正在写入 Claude Code 配置...', spinner: 'dots' }).start();
|
|
3121
|
-
writeClaudeCodeSettings(claudeBaseUrl, apiKey);
|
|
3915
|
+
writeClaudeCodeSettings(claudeBaseUrl, apiKey, selectedModel.id);
|
|
3122
3916
|
writeSpinner.succeed('Claude Code 配置写入完成');
|
|
3123
3917
|
|
|
3124
3918
|
console.log(chalk.green('\n✅ Claude Code CLI 配置完成!'));
|
|
3125
3919
|
console.log(chalk.cyan(` Base URL: ${claudeBaseUrl}`));
|
|
3126
|
-
console.log(chalk.gray(` 模型:
|
|
3920
|
+
console.log(chalk.gray(` 模型: ${selectedModel.name} (${selectedModel.id})`));
|
|
3127
3921
|
console.log(chalk.gray(' API Key: 已设置'));
|
|
3128
3922
|
console.log(chalk.gray('\n 已写入:'));
|
|
3129
3923
|
console.log(chalk.gray(' • ~/.claude/settings.json'));
|
|
3130
3924
|
console.log(chalk.gray(' • ~/.claude.json (跳过 onboarding)'));
|
|
3131
|
-
console.log(chalk.gray(' • shell 环境变量 (ANTHROPIC_BASE_URL, ANTHROPIC_AUTH_TOKEN)'));
|
|
3925
|
+
console.log(chalk.gray(' • shell 环境变量 (ANTHROPIC_BASE_URL, ANTHROPIC_AUTH_TOKEN, ANTHROPIC_MODEL)'));
|
|
3132
3926
|
console.log(chalk.yellow('\n 提示: 请重新打开终端或执行 source ~/.zshrc 使环境变量生效'));
|
|
3927
|
+
|
|
3928
|
+
if (await confirmImmediateTest(args, '是否立即测试 Claude Code CLI 连接?')) {
|
|
3929
|
+
await testAdditionalCliConnections(args, { only: ['claude'] });
|
|
3930
|
+
}
|
|
3133
3931
|
}
|
|
3134
3932
|
|
|
3135
3933
|
// ============ 单独配置 Opencode ============
|
|
3136
3934
|
async function activateOpencode(paths, args = {}) {
|
|
3137
3935
|
console.log(chalk.cyan.bold('\n🔧 配置 Opencode\n'));
|
|
3138
|
-
|
|
3139
|
-
const claudeApiConfig = API_CONFIG.claude;
|
|
3936
|
+
const defaultModel = await promptOpencodeDefaultModelSelection(args);
|
|
3140
3937
|
|
|
3141
3938
|
// ---- 测速选节点 ----
|
|
3142
3939
|
const shouldTest = !(args['no-test'] || args.noTest);
|
|
@@ -3197,21 +3994,24 @@ async function activateOpencode(paths, args = {}) {
|
|
|
3197
3994
|
// ---- 写入配置 ----
|
|
3198
3995
|
const claudeBaseUrl = buildFullUrl(selectedEndpoint.url, 'claude');
|
|
3199
3996
|
const codexBaseUrl = buildFullUrl(selectedEndpoint.url, 'codex');
|
|
3200
|
-
const modelId = 'claude-sonnet-4-6';
|
|
3201
3997
|
const writeSpinner = ora({ text: '正在写入 Opencode 配置...', spinner: 'dots' }).start();
|
|
3202
|
-
|
|
3998
|
+
writeOpencodeConfig(claudeBaseUrl, codexBaseUrl, apiKey, defaultModel.modelKey);
|
|
3203
3999
|
writeSpinner.succeed('Opencode 配置写入完成');
|
|
3204
4000
|
|
|
3205
4001
|
console.log(chalk.green('\n✅ Opencode 配置完成!'));
|
|
3206
|
-
console.log(chalk.cyan(` Claude: ${claudeBaseUrl} →
|
|
3207
|
-
console.log(chalk.cyan(` Codex: ${codexBaseUrl} →
|
|
3208
|
-
console.log(chalk.gray(` 默认模型:
|
|
4002
|
+
console.log(chalk.cyan(` Claude: ${claudeBaseUrl} → ${getClaudeSwitchHint()}`));
|
|
4003
|
+
console.log(chalk.cyan(` Codex: ${codexBaseUrl} → ${CODEX_MODELS.map(model => model.name).join(' / ')}`));
|
|
4004
|
+
console.log(chalk.gray(` 默认模型: ${defaultModel.modelKey}`));
|
|
3209
4005
|
console.log(chalk.gray(' API Key: 已设置'));
|
|
3210
4006
|
console.log(chalk.gray('\n 已写入:'));
|
|
3211
4007
|
console.log(chalk.gray(' • ~/.config/opencode/opencode.json (CLI + 桌面版)'));
|
|
3212
4008
|
console.log(chalk.gray(' • ~/.codex/config.toml (model_providers)'));
|
|
3213
4009
|
console.log(chalk.gray(' • ~/.codex/auth.json (API Keys)'));
|
|
3214
|
-
console.log(chalk.yellow(
|
|
4010
|
+
console.log(chalk.yellow(`\n 切换模型: 在 opencode 内使用 /model 命令切换 (${getOpencodeSwitchHint()})`));
|
|
4011
|
+
|
|
4012
|
+
if (await confirmImmediateTest(args, '是否立即测试 Opencode CLI 连接?')) {
|
|
4013
|
+
await testAdditionalCliConnections(args, { only: ['opencode'] });
|
|
4014
|
+
}
|
|
3215
4015
|
}
|
|
3216
4016
|
|
|
3217
4017
|
// ============ 单独配置 Codex CLI ============
|
|
@@ -3219,7 +4019,7 @@ async function activateCodex(paths, args = {}) {
|
|
|
3219
4019
|
console.log(chalk.cyan.bold('\n🔧 配置 Codex CLI\n'));
|
|
3220
4020
|
|
|
3221
4021
|
// ---- 选模型 ----
|
|
3222
|
-
let modelId =
|
|
4022
|
+
let modelId = getDefaultCodexModel().id;
|
|
3223
4023
|
if (CODEX_MODELS.length > 1) {
|
|
3224
4024
|
const { selected } = await inquirer.prompt([{
|
|
3225
4025
|
type: 'list',
|
|
@@ -3301,6 +4101,10 @@ async function activateCodex(paths, args = {}) {
|
|
|
3301
4101
|
console.log(chalk.gray(' • ~/.codex/config.toml (model + model_providers)'));
|
|
3302
4102
|
console.log(chalk.gray(' • ~/.codex/auth.json (OPENAI_API_KEY)'));
|
|
3303
4103
|
console.log(chalk.yellow('\n 提示: 请重新打开终端使配置生效'));
|
|
4104
|
+
|
|
4105
|
+
if (await confirmImmediateTest(args, '是否立即测试 Codex CLI 连接?')) {
|
|
4106
|
+
await testAdditionalCliConnections(args, { only: ['codex'] });
|
|
4107
|
+
}
|
|
3304
4108
|
}
|
|
3305
4109
|
|
|
3306
4110
|
// ============ yycode 精简模式(零交互一键配置) ============
|
|
@@ -3446,8 +4250,8 @@ async function yycodeQuickSetup(paths) {
|
|
|
3446
4250
|
writeConfigWithSync(paths, config);
|
|
3447
4251
|
updateAuthProfilesWithSync(paths, claudeProviderName, apiKey);
|
|
3448
4252
|
updateAuthProfilesWithSync(paths, codexProviderName, apiKey);
|
|
3449
|
-
try { syncExternalTools('claude', claudeBaseUrl, apiKey, { codexBaseUrl }); } catch { /* ignore */ }
|
|
3450
|
-
try { syncExternalTools('codex', codexBaseUrl, apiKey); } catch { /* ignore */ }
|
|
4253
|
+
try { syncExternalTools('claude', claudeBaseUrl, apiKey, { codexBaseUrl, claudeModelId, opencodeDefaultModelKey: `yunyi-codex/${codexModelId}` }); } catch { /* ignore */ }
|
|
4254
|
+
try { syncExternalTools('codex', codexBaseUrl, apiKey, { modelId: codexModelId }); } catch { /* ignore */ }
|
|
3451
4255
|
writeSpinner.succeed('配置写入完成');
|
|
3452
4256
|
|
|
3453
4257
|
// ---- 结果 ----
|
|
@@ -3457,7 +4261,7 @@ async function yycodeQuickSetup(paths) {
|
|
|
3457
4261
|
console.log(chalk.cyan(` Codex (主): ${codexBaseUrl}`));
|
|
3458
4262
|
console.log(chalk.gray(` 模型: ${codexModel.name}`));
|
|
3459
4263
|
console.log(chalk.gray(' API Key: 已设置'));
|
|
3460
|
-
console.log(chalk.gray(' 同步: Claude Code settings, Codex CLI config'));
|
|
4264
|
+
console.log(chalk.gray(' 同步: Claude Code settings, Opencode config, Codex CLI config'));
|
|
3461
4265
|
console.log('');
|
|
3462
4266
|
}
|
|
3463
4267
|
|
|
@@ -3529,7 +4333,7 @@ async function main() {
|
|
|
3529
4333
|
{ name: ' 配置 Opencode', value: 'activate_opencode' },
|
|
3530
4334
|
{ name: ' 配置 Codex CLI', value: 'activate_codex' },
|
|
3531
4335
|
new inquirer.Separator(' -- 工具 --'),
|
|
3532
|
-
{ name: '
|
|
4336
|
+
{ name: ' 切换 OpenClaw 模型', value: 'switch_model' },
|
|
3533
4337
|
{ name: ` 权限管理${getToolsProfileTag(paths)}`, value: 'tools_profile' },
|
|
3534
4338
|
{ name: ' 测试连接', value: 'test_connection' },
|
|
3535
4339
|
{ name: ' 查看配置', value: 'view_config' },
|
|
@@ -3774,11 +4578,12 @@ async function activate(paths, type) {
|
|
|
3774
4578
|
}
|
|
3775
4579
|
|
|
3776
4580
|
|
|
3777
|
-
// ============
|
|
4581
|
+
// ============ 切换 OpenClaw 模型 ============
|
|
3778
4582
|
async function switchModel(paths) {
|
|
3779
|
-
console.log(chalk.cyan('🔄
|
|
4583
|
+
console.log(chalk.cyan('🔄 切换 OpenClaw 模型\n'));
|
|
3780
4584
|
|
|
3781
4585
|
const config = ensureConfigStructure(readConfig(paths.openclawConfig) || {});
|
|
4586
|
+
applyConfigRepairsWithSync(config, paths);
|
|
3782
4587
|
const primary = config.agents?.defaults?.model?.primary || '';
|
|
3783
4588
|
const providers = config.models?.providers || {};
|
|
3784
4589
|
|
|
@@ -3869,7 +4674,7 @@ async function switchModel(paths) {
|
|
|
3869
4674
|
const { selected } = await inquirer.prompt([{
|
|
3870
4675
|
type: 'list',
|
|
3871
4676
|
name: 'selected',
|
|
3872
|
-
message: '
|
|
4677
|
+
message: '选择 OpenClaw 模型:',
|
|
3873
4678
|
default: primaryInPreset ? primary : undefined,
|
|
3874
4679
|
pageSize: 15,
|
|
3875
4680
|
choices,
|
|
@@ -3879,7 +4684,7 @@ async function switchModel(paths) {
|
|
|
3879
4684
|
|
|
3880
4685
|
if (selected === '__other__') {
|
|
3881
4686
|
if (otherModels.length === 0) {
|
|
3882
|
-
|
|
4687
|
+
console.log(chalk.gray('\n当前 OpenClaw 配置中没有其他模型,仅有预设模型可用'));
|
|
3883
4688
|
return;
|
|
3884
4689
|
}
|
|
3885
4690
|
const otherChoices = otherModels.map(o => {
|
|
@@ -4038,12 +4843,13 @@ async function manageToolsProfile(paths) {
|
|
|
4038
4843
|
|
|
4039
4844
|
// ============ 测试连接 ============
|
|
4040
4845
|
async function testConnection(paths, args = {}) {
|
|
4041
|
-
console.log(chalk.cyan('🧪 测试 OpenClaw
|
|
4846
|
+
console.log(chalk.cyan('🧪 测试 OpenClaw / 各 CLI 连接\n'));
|
|
4042
4847
|
invalidateGatewayEnvCache();
|
|
4043
4848
|
|
|
4044
|
-
const config = readConfig(paths.openclawConfig);
|
|
4849
|
+
const config = ensureConfigStructure(readConfig(paths.openclawConfig) || {});
|
|
4850
|
+
applyConfigRepairsWithSync(config, paths);
|
|
4045
4851
|
|
|
4046
|
-
if (!config) {
|
|
4852
|
+
if (!config || !config.models) {
|
|
4047
4853
|
console.log(chalk.yellow('配置文件不存在,请先选择节点'));
|
|
4048
4854
|
return;
|
|
4049
4855
|
}
|
|
@@ -4051,7 +4857,7 @@ async function testConnection(paths, args = {}) {
|
|
|
4051
4857
|
// 检查当前激活的是哪个
|
|
4052
4858
|
let primary = config.agents?.defaults?.model?.primary || '';
|
|
4053
4859
|
if (!primary.includes('/')) {
|
|
4054
|
-
console.log(chalk.yellow('⚠️
|
|
4860
|
+
console.log(chalk.yellow('⚠️ 主模型未设置,请先通过「切换 OpenClaw 模型」或「一键配置」设置主模型'));
|
|
4055
4861
|
return;
|
|
4056
4862
|
}
|
|
4057
4863
|
|
|
@@ -4222,6 +5028,10 @@ async function testConnection(paths, args = {}) {
|
|
|
4222
5028
|
if (!cliResult.success) {
|
|
4223
5029
|
console.log(chalk.red(`\n❌ Gateway CLI 测试失败`));
|
|
4224
5030
|
console.log(chalk.red(` 错误: ${cliResult.error || '未知错误'}`));
|
|
5031
|
+
if (cliResult.rawError && cliResult.rawError !== cliResult.error) {
|
|
5032
|
+
console.log(chalk.gray(` 原始输出: ${cliResult.rawError.substring(0, 300)}`));
|
|
5033
|
+
}
|
|
5034
|
+
console.log(chalk.gray(' 可继续执行: openclaw logs --follow'));
|
|
4225
5035
|
console.log(chalk.gray(` 将尝试使用 HTTP 端点测试...`));
|
|
4226
5036
|
}
|
|
4227
5037
|
}
|
|
@@ -4300,6 +5110,8 @@ async function testConnection(paths, args = {}) {
|
|
|
4300
5110
|
}
|
|
4301
5111
|
} catch (error) {
|
|
4302
5112
|
console.log(chalk.red(`❌ 测试失败: ${error.message}`));
|
|
5113
|
+
} finally {
|
|
5114
|
+
await testAdditionalCliConnections(args);
|
|
4303
5115
|
}
|
|
4304
5116
|
}
|
|
4305
5117
|
|
|
@@ -4725,10 +5537,18 @@ function testGatewayViaAgent(model) {
|
|
|
4725
5537
|
// stdout 有 JSON,走正常解析流程而非直接报错
|
|
4726
5538
|
stdout = fallbackOutput;
|
|
4727
5539
|
} else {
|
|
5540
|
+
const plainCmd = cmd.replace(/\s--json\b/, '');
|
|
5541
|
+
const plainResult = safeExec(plainCmd, execOpts);
|
|
5542
|
+
const plainCombined = `${plainResult.output || ''}\n${plainResult.stdout || ''}\n${plainResult.stderr || ''}`.trim();
|
|
5543
|
+
const readableError = buildReadableAgentError(
|
|
5544
|
+
`${cleanStderr}\n${fallbackOutput}\n${plainCombined}`,
|
|
5545
|
+
(error.message || 'CLI 执行失败').trim()
|
|
5546
|
+
);
|
|
4728
5547
|
resolve({
|
|
4729
5548
|
success: false,
|
|
4730
5549
|
usedCli: true,
|
|
4731
|
-
error:
|
|
5550
|
+
error: readableError,
|
|
5551
|
+
rawError: (cleanStderr || fallbackOutput || plainCombined || error.message || 'CLI 执行失败').trim()
|
|
4732
5552
|
});
|
|
4733
5553
|
return;
|
|
4734
5554
|
}
|