yymaxapi 1.0.79 → 1.0.81
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';
|
|
@@ -468,6 +589,53 @@ function buildAuthCandidates(baseDirs) {
|
|
|
468
589
|
return auths;
|
|
469
590
|
}
|
|
470
591
|
|
|
592
|
+
function buildPosixAuthCandidates(baseDirs) {
|
|
593
|
+
const auths = [];
|
|
594
|
+
for (const baseDir of baseDirs) {
|
|
595
|
+
auths.push(
|
|
596
|
+
path.posix.join(baseDir, 'agents', 'main', 'agent', 'auth-profiles.json'),
|
|
597
|
+
path.posix.join(baseDir, 'agent', 'auth-profiles.json')
|
|
598
|
+
);
|
|
599
|
+
}
|
|
600
|
+
return auths;
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
function findExistingWslFile(candidates = []) {
|
|
604
|
+
for (const candidate of candidates) {
|
|
605
|
+
const check = safeExec(`wsl -- bash -c "test -f '${candidate}' && echo yes"`, { timeout: 5000 });
|
|
606
|
+
if (check.ok && check.output.trim() === 'yes') {
|
|
607
|
+
return candidate;
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
return null;
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
function getWslMirrorInfo() {
|
|
614
|
+
if (process.platform !== 'win32' || !isWslAvailable()) {
|
|
615
|
+
return { configPath: null, authProfiles: null };
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
const wslHome = getWslHome() || '/root';
|
|
619
|
+
const configCandidates = [
|
|
620
|
+
`${wslHome}/.openclaw/openclaw.json`,
|
|
621
|
+
`${wslHome}/.openclaw/moltbot.json`,
|
|
622
|
+
`${wslHome}/.clawdbot/openclaw.json`,
|
|
623
|
+
`${wslHome}/.clawdbot/clawdbot.json`,
|
|
624
|
+
'/root/.openclaw/openclaw.json',
|
|
625
|
+
'/root/.openclaw/moltbot.json',
|
|
626
|
+
'/root/.clawdbot/openclaw.json',
|
|
627
|
+
'/root/.clawdbot/clawdbot.json'
|
|
628
|
+
];
|
|
629
|
+
|
|
630
|
+
const configPath = findExistingWslFile(configCandidates);
|
|
631
|
+
const authBases = configPath
|
|
632
|
+
? [path.posix.dirname(configPath)]
|
|
633
|
+
: [`${wslHome}/.openclaw`, `${wslHome}/.clawdbot`, '/root/.openclaw', '/root/.clawdbot'];
|
|
634
|
+
const authProfiles = findExistingWslFile(buildPosixAuthCandidates(authBases));
|
|
635
|
+
|
|
636
|
+
return { configPath, authProfiles };
|
|
637
|
+
}
|
|
638
|
+
|
|
471
639
|
function getConfigPath() {
|
|
472
640
|
const homeDir = os.homedir();
|
|
473
641
|
const openclawStateDir = process.env.OPENCLAW_STATE_DIR || path.join(homeDir, '.openclaw');
|
|
@@ -516,29 +684,24 @@ function getConfigPath() {
|
|
|
516
684
|
}
|
|
517
685
|
|
|
518
686
|
// Windows + WSL: 尝试读取 WSL 内的配置文件
|
|
687
|
+
let wslConfigPath = null;
|
|
688
|
+
let wslAuthProfiles = null;
|
|
519
689
|
if (process.platform === 'win32' && isWslAvailable()) {
|
|
520
690
|
try {
|
|
521
|
-
const
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
if (check.ok && check.output.trim() === 'yes') {
|
|
532
|
-
// 将 WSL 配置复制到 Windows 侧,保持同步
|
|
533
|
-
const winDest = path.join(openclawStateDir, path.basename(wp));
|
|
534
|
-
try {
|
|
535
|
-
const content = execFileSync('wsl', ['bash', '-c', `cat '${wp}'`], { encoding: 'utf8', timeout: 10000, stdio: 'pipe' });
|
|
536
|
-
if (!fs.existsSync(openclawStateDir)) fs.mkdirSync(openclawStateDir, { recursive: true });
|
|
691
|
+
const mirrorInfo = getWslMirrorInfo();
|
|
692
|
+
wslConfigPath = mirrorInfo.configPath;
|
|
693
|
+
wslAuthProfiles = mirrorInfo.authProfiles;
|
|
694
|
+
|
|
695
|
+
if (wslConfigPath) {
|
|
696
|
+
const winDest = path.join(openclawStateDir, path.basename(wslConfigPath));
|
|
697
|
+
try {
|
|
698
|
+
if (!fs.existsSync(openclawStateDir)) fs.mkdirSync(openclawStateDir, { recursive: true });
|
|
699
|
+
if (!fs.existsSync(winDest)) {
|
|
700
|
+
const content = execFileSync('wsl', ['bash', '-c', `cat '${wslConfigPath}'`], { encoding: 'utf8', timeout: 10000, stdio: 'pipe' });
|
|
537
701
|
fs.writeFileSync(winDest, content, 'utf8');
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
}
|
|
702
|
+
}
|
|
703
|
+
if (!candidates.includes(winDest)) candidates.unshift(winDest);
|
|
704
|
+
} catch { }
|
|
542
705
|
}
|
|
543
706
|
} catch { }
|
|
544
707
|
}
|
|
@@ -559,6 +722,9 @@ function getConfigPath() {
|
|
|
559
722
|
: [...baseAuthCandidates, ...moltbotAuthCandidates];
|
|
560
723
|
|
|
561
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
|
+
: [];
|
|
562
728
|
|
|
563
729
|
const syncTargets = [];
|
|
564
730
|
if (openclawConfig.startsWith(openclawStateDir) && fs.existsSync(clawdbotStateDir)) {
|
|
@@ -568,7 +734,7 @@ function getConfigPath() {
|
|
|
568
734
|
);
|
|
569
735
|
}
|
|
570
736
|
|
|
571
|
-
return { openclawConfig, authProfiles, configDir, syncTargets };
|
|
737
|
+
return { openclawConfig, authProfiles, configDir, syncTargets, authSyncTargets, wslConfigPath, wslAuthProfiles };
|
|
572
738
|
}
|
|
573
739
|
|
|
574
740
|
// ============ 配置读写 ============
|
|
@@ -591,6 +757,7 @@ function readConfig(configPath) {
|
|
|
591
757
|
}
|
|
592
758
|
|
|
593
759
|
function writeConfig(configPath, config) {
|
|
760
|
+
sanitizeDefaultModelSelection(config);
|
|
594
761
|
const dir = path.dirname(configPath);
|
|
595
762
|
if (!fs.existsSync(dir)) {
|
|
596
763
|
fs.mkdirSync(dir, { recursive: true });
|
|
@@ -600,7 +767,7 @@ function writeConfig(configPath, config) {
|
|
|
600
767
|
|
|
601
768
|
// ============ 多工具配置同步 ============
|
|
602
769
|
|
|
603
|
-
function writeClaudeCodeSettings(baseUrl, apiKey) {
|
|
770
|
+
function writeClaudeCodeSettings(baseUrl, apiKey, modelId = getDefaultClaudeModel().id) {
|
|
604
771
|
const home = os.homedir();
|
|
605
772
|
// ~/.claude/settings.json
|
|
606
773
|
const claudeDir = path.join(home, '.claude');
|
|
@@ -611,9 +778,12 @@ function writeClaudeCodeSettings(baseUrl, apiKey) {
|
|
|
611
778
|
try { settings = JSON.parse(fs.readFileSync(settingsPath, 'utf8')); } catch { settings = {}; }
|
|
612
779
|
}
|
|
613
780
|
settings.apiBaseUrl = baseUrl.replace(/\/+$/, '');
|
|
781
|
+
settings.model = modelId;
|
|
782
|
+
settings.availableModels = CLAUDE_MODELS.map(model => model.id);
|
|
614
783
|
if (!settings.env) settings.env = {};
|
|
615
784
|
settings.env.ANTHROPIC_BASE_URL = baseUrl.replace(/\/+$/, '');
|
|
616
785
|
settings.env.ANTHROPIC_AUTH_TOKEN = apiKey;
|
|
786
|
+
settings.env.ANTHROPIC_MODEL = modelId;
|
|
617
787
|
if (!fs.existsSync(claudeDir)) fs.mkdirSync(claudeDir, { recursive: true });
|
|
618
788
|
fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2), 'utf8');
|
|
619
789
|
} catch { /* 非关键,静默失败 */ }
|
|
@@ -636,7 +806,7 @@ function writeClaudeCodeSettings(baseUrl, apiKey) {
|
|
|
636
806
|
try {
|
|
637
807
|
const cleanUrl = baseUrl.replace(/\/+$/, '');
|
|
638
808
|
execSync(
|
|
639
|
-
`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')"`,
|
|
640
810
|
{ stdio: 'pipe' }
|
|
641
811
|
);
|
|
642
812
|
} catch { /* best-effort */ }
|
|
@@ -648,6 +818,7 @@ function writeClaudeCodeSettings(baseUrl, apiKey) {
|
|
|
648
818
|
marker,
|
|
649
819
|
`export ANTHROPIC_BASE_URL="${cleanUrl}"`,
|
|
650
820
|
`export ANTHROPIC_AUTH_TOKEN="${apiKey}"`,
|
|
821
|
+
`export ANTHROPIC_MODEL="${modelId}"`,
|
|
651
822
|
'# 中转站证书校验放宽,避免 unknown certificate verification error',
|
|
652
823
|
'export NODE_TLS_REJECT_UNAUTHORIZED=0',
|
|
653
824
|
markerEnd
|
|
@@ -751,7 +922,7 @@ function writeCodexConfig(baseUrl, apiKey, modelId = 'gpt-5.4') {
|
|
|
751
922
|
} catch { /* 非关键,静默失败 */ }
|
|
752
923
|
}
|
|
753
924
|
|
|
754
|
-
function writeOpencodeConfig(claudeBaseUrl, codexBaseUrl, apiKey,
|
|
925
|
+
function writeOpencodeConfig(claudeBaseUrl, codexBaseUrl, apiKey, defaultModelKey = `yunyi-claude/${getDefaultClaudeModel().id}`) {
|
|
755
926
|
const home = os.homedir();
|
|
756
927
|
const claudeUrl = claudeBaseUrl.replace(/\/+$/, '');
|
|
757
928
|
const codexUrl = (codexBaseUrl || '').replace(/\/+$/, '');
|
|
@@ -773,7 +944,7 @@ function writeOpencodeConfig(claudeBaseUrl, codexBaseUrl, apiKey, modelId) {
|
|
|
773
944
|
existing.provider['yunyi-claude'] = {
|
|
774
945
|
name: '云翼 Claude',
|
|
775
946
|
npm: '@ai-sdk/anthropic',
|
|
776
|
-
models:
|
|
947
|
+
models: buildProviderModelMap(CLAUDE_MODELS),
|
|
777
948
|
options: { apiKey, baseURL: `${claudeUrl}/v1` }
|
|
778
949
|
};
|
|
779
950
|
|
|
@@ -782,7 +953,7 @@ function writeOpencodeConfig(claudeBaseUrl, codexBaseUrl, apiKey, modelId) {
|
|
|
782
953
|
existing.provider['yunyi-codex'] = {
|
|
783
954
|
name: '云翼 Codex',
|
|
784
955
|
npm: '@ai-sdk/openai',
|
|
785
|
-
models:
|
|
956
|
+
models: buildProviderModelMap(CODEX_MODELS),
|
|
786
957
|
options: { apiKey, baseURL: codexUrl }
|
|
787
958
|
};
|
|
788
959
|
}
|
|
@@ -794,8 +965,7 @@ function writeOpencodeConfig(claudeBaseUrl, codexBaseUrl, apiKey, modelId) {
|
|
|
794
965
|
}
|
|
795
966
|
|
|
796
967
|
// 设置默认模型
|
|
797
|
-
|
|
798
|
-
existing.model = `yunyi-claude/${rawModelId}`;
|
|
968
|
+
existing.model = defaultModelKey;
|
|
799
969
|
|
|
800
970
|
// 从 disabled_providers 中移除我们的 provider
|
|
801
971
|
if (Array.isArray(existing.disabled_providers)) {
|
|
@@ -850,14 +1020,14 @@ function syncExternalTools(type, baseUrl, apiKey, extra = {}) {
|
|
|
850
1020
|
const synced = [];
|
|
851
1021
|
try {
|
|
852
1022
|
if (type === 'claude') {
|
|
853
|
-
writeClaudeCodeSettings(baseUrl, apiKey);
|
|
1023
|
+
writeClaudeCodeSettings(baseUrl, apiKey, extra.claudeModelId || getDefaultClaudeModel().id);
|
|
854
1024
|
synced.push('Claude Code settings');
|
|
855
1025
|
} else if (type === 'codex') {
|
|
856
|
-
writeCodexConfig(baseUrl, apiKey);
|
|
1026
|
+
writeCodexConfig(baseUrl, apiKey, extra.modelId || getDefaultCodexModel().id);
|
|
857
1027
|
synced.push('Codex CLI config');
|
|
858
1028
|
}
|
|
859
1029
|
if (type === 'claude' && extra.codexBaseUrl) {
|
|
860
|
-
writeOpencodeConfig(baseUrl, extra.codexBaseUrl, apiKey);
|
|
1030
|
+
writeOpencodeConfig(baseUrl, extra.codexBaseUrl, apiKey, extra.opencodeDefaultModelKey || `yunyi-claude/${extra.claudeModelId || getDefaultClaudeModel().id}`);
|
|
861
1031
|
synced.push('Opencode config');
|
|
862
1032
|
}
|
|
863
1033
|
} catch { /* ignore */ }
|
|
@@ -879,10 +1049,15 @@ function syncClawdbotConfigs(paths, config) {
|
|
|
879
1049
|
function writeConfigWithSync(paths, config) {
|
|
880
1050
|
writeConfig(paths.openclawConfig, config);
|
|
881
1051
|
syncClawdbotConfigs(paths, config);
|
|
1052
|
+
const hasWslMirror = process.platform === 'win32' && !!paths.wslConfigPath;
|
|
1053
|
+
if (hasWslMirror) {
|
|
1054
|
+
syncConfigToWsl(paths.openclawConfig, paths.wslConfigPath);
|
|
1055
|
+
}
|
|
1056
|
+
invalidateGatewayEnvCache();
|
|
882
1057
|
// 如果 Gateway 在 WSL,自动同步配置过去
|
|
883
1058
|
const gwEnv = detectGatewayEnv();
|
|
884
|
-
if (gwEnv === 'wsl') {
|
|
885
|
-
syncConfigToWsl(paths.openclawConfig);
|
|
1059
|
+
if (!hasWslMirror && gwEnv === 'wsl') {
|
|
1060
|
+
syncConfigToWsl(paths.openclawConfig, paths.wslConfigPath);
|
|
886
1061
|
}
|
|
887
1062
|
// 如果 Gateway 在 Docker 容器内,自动同步配置过去
|
|
888
1063
|
if (gwEnv === 'docker' && _selectedDockerContainer) {
|
|
@@ -1081,6 +1256,10 @@ let _gwEnvCache = null;
|
|
|
1081
1256
|
let _wslAvailCache = null;
|
|
1082
1257
|
let _wslHomeCache = undefined; // undefined = 未检测, null = 检测失败
|
|
1083
1258
|
|
|
1259
|
+
function invalidateGatewayEnvCache() {
|
|
1260
|
+
_gwEnvCache = null;
|
|
1261
|
+
}
|
|
1262
|
+
|
|
1084
1263
|
function isWslAvailable() {
|
|
1085
1264
|
if (process.platform !== 'win32') return false;
|
|
1086
1265
|
if (_wslAvailCache !== null) return _wslAvailCache;
|
|
@@ -1206,16 +1385,12 @@ function execAsyncInGatewayEnv(cmd, options = {}) {
|
|
|
1206
1385
|
}
|
|
1207
1386
|
|
|
1208
1387
|
// 同步配置到 WSL(仅在 Gateway 环境为 WSL 时调用)
|
|
1209
|
-
function syncConfigToWsl(windowsConfigPath) {
|
|
1388
|
+
function syncConfigToWsl(windowsConfigPath, wslDestPath) {
|
|
1210
1389
|
try {
|
|
1211
1390
|
const wslHome = getWslHome();
|
|
1212
|
-
if (!wslHome) return;
|
|
1213
|
-
const
|
|
1214
|
-
|
|
1215
|
-
if (!match) return;
|
|
1216
|
-
const wslSrc = `/mnt/${match[1].toLowerCase()}/${match[2]}`;
|
|
1217
|
-
const wslDest = `${wslHome}/.openclaw/openclaw.json`;
|
|
1218
|
-
execFileSync('wsl', ['bash', '-c', `mkdir -p "${wslHome}/.openclaw" && cp "${wslSrc}" "${wslDest}"`], { timeout: 10000, stdio: 'pipe' });
|
|
1391
|
+
if (!wslHome && !wslDestPath) return;
|
|
1392
|
+
const target = wslDestPath || `${wslHome}/.openclaw/openclaw.json`;
|
|
1393
|
+
syncFileToWsl(windowsConfigPath, target);
|
|
1219
1394
|
} catch { /* best-effort */ }
|
|
1220
1395
|
}
|
|
1221
1396
|
|
|
@@ -1279,6 +1454,68 @@ function coerceModelsRecord(value) {
|
|
|
1279
1454
|
return record;
|
|
1280
1455
|
}
|
|
1281
1456
|
|
|
1457
|
+
function isValidModelRef(value) {
|
|
1458
|
+
return typeof value === 'string' && value.trim().includes('/');
|
|
1459
|
+
}
|
|
1460
|
+
|
|
1461
|
+
function collectConfiguredModelKeys(config) {
|
|
1462
|
+
const keys = [];
|
|
1463
|
+
|
|
1464
|
+
const registered = Object.keys(config?.agents?.defaults?.models || {}).filter(isValidModelRef);
|
|
1465
|
+
keys.push(...registered);
|
|
1466
|
+
|
|
1467
|
+
const providers = config?.models?.providers || {};
|
|
1468
|
+
for (const [providerName, providerConfig] of Object.entries(providers)) {
|
|
1469
|
+
for (const model of providerConfig.models || []) {
|
|
1470
|
+
if (!model?.id) continue;
|
|
1471
|
+
const modelKey = `${providerName}/${model.id}`;
|
|
1472
|
+
if (isValidModelRef(modelKey)) keys.push(modelKey);
|
|
1473
|
+
}
|
|
1474
|
+
}
|
|
1475
|
+
|
|
1476
|
+
return [...new Set(keys)];
|
|
1477
|
+
}
|
|
1478
|
+
|
|
1479
|
+
function inferPrimaryModelKey(config) {
|
|
1480
|
+
return collectConfiguredModelKeys(config)[0] || '';
|
|
1481
|
+
}
|
|
1482
|
+
|
|
1483
|
+
function normalizeDefaultModelSelection(value, config) {
|
|
1484
|
+
if (typeof value === 'string') {
|
|
1485
|
+
const primary = value.trim();
|
|
1486
|
+
return primary ? { primary, fallbacks: [] } : { fallbacks: [] };
|
|
1487
|
+
}
|
|
1488
|
+
|
|
1489
|
+
if (value && typeof value === 'object' && !Array.isArray(value)) {
|
|
1490
|
+
let primary = typeof value.primary === 'string' ? value.primary.trim() : '';
|
|
1491
|
+
const fallbacks = Array.isArray(value.fallbacks)
|
|
1492
|
+
? [...new Set(value.fallbacks.map(item => String(item || '').trim()).filter(isValidModelRef))]
|
|
1493
|
+
: [];
|
|
1494
|
+
|
|
1495
|
+
if (!primary) {
|
|
1496
|
+
primary = inferPrimaryModelKey(config) || fallbacks[0] || '';
|
|
1497
|
+
}
|
|
1498
|
+
|
|
1499
|
+
const normalizedFallbacks = fallbacks.filter(modelKey => modelKey !== primary);
|
|
1500
|
+
const normalized = { fallbacks: normalizedFallbacks };
|
|
1501
|
+
if (primary) normalized.primary = primary;
|
|
1502
|
+
return normalized;
|
|
1503
|
+
}
|
|
1504
|
+
|
|
1505
|
+
const inferred = inferPrimaryModelKey(config);
|
|
1506
|
+
return inferred ? { primary: inferred, fallbacks: [] } : { fallbacks: [] };
|
|
1507
|
+
}
|
|
1508
|
+
|
|
1509
|
+
function sanitizeDefaultModelSelection(config) {
|
|
1510
|
+
if (!config?.agents?.defaults) return;
|
|
1511
|
+
const normalized = normalizeDefaultModelSelection(config.agents.defaults.model, config);
|
|
1512
|
+
if (normalized.primary) {
|
|
1513
|
+
config.agents.defaults.model = normalized;
|
|
1514
|
+
} else {
|
|
1515
|
+
delete config.agents.defaults.model;
|
|
1516
|
+
}
|
|
1517
|
+
}
|
|
1518
|
+
|
|
1282
1519
|
const OPENCLAW_INVALID_ROOT_KEYS = ['model'];
|
|
1283
1520
|
|
|
1284
1521
|
function sanitizeRootKeys(config) {
|
|
@@ -1294,7 +1531,7 @@ function ensureConfigStructure(config) {
|
|
|
1294
1531
|
if (!next.models.providers) next.models.providers = {};
|
|
1295
1532
|
if (!next.agents) next.agents = {};
|
|
1296
1533
|
if (!next.agents.defaults) next.agents.defaults = {};
|
|
1297
|
-
|
|
1534
|
+
next.agents.defaults.model = normalizeDefaultModelSelection(next.agents.defaults.model, next);
|
|
1298
1535
|
if (!next.agents.defaults.models || Array.isArray(next.agents.defaults.models) || typeof next.agents.defaults.models !== 'object') {
|
|
1299
1536
|
next.agents.defaults.models = coerceModelsRecord(next.agents.defaults.models);
|
|
1300
1537
|
}
|
|
@@ -1412,9 +1649,70 @@ function readAuthStore(authProfilesPath) {
|
|
|
1412
1649
|
}
|
|
1413
1650
|
|
|
1414
1651
|
function writeAuthStore(authProfilesPath, store) {
|
|
1652
|
+
const authDir = path.dirname(authProfilesPath);
|
|
1653
|
+
if (!fs.existsSync(authDir)) {
|
|
1654
|
+
fs.mkdirSync(authDir, { recursive: true });
|
|
1655
|
+
}
|
|
1415
1656
|
fs.writeFileSync(authProfilesPath, JSON.stringify(store, null, 2), 'utf8');
|
|
1416
1657
|
}
|
|
1417
1658
|
|
|
1659
|
+
function toWslMountPath(windowsPath) {
|
|
1660
|
+
const winNorm = String(windowsPath || '').replace(/\\/g, '/');
|
|
1661
|
+
const match = winNorm.match(/^([A-Za-z]):\/(.*)/);
|
|
1662
|
+
if (!match) return null;
|
|
1663
|
+
return `/mnt/${match[1].toLowerCase()}/${match[2]}`;
|
|
1664
|
+
}
|
|
1665
|
+
|
|
1666
|
+
function syncFileToWsl(windowsPath, wslDestPath) {
|
|
1667
|
+
if (process.platform !== 'win32' || !wslDestPath) return false;
|
|
1668
|
+
const wslSrc = toWslMountPath(windowsPath);
|
|
1669
|
+
if (!wslSrc) return false;
|
|
1670
|
+
|
|
1671
|
+
const wslDir = path.posix.dirname(wslDestPath);
|
|
1672
|
+
execFileSync('wsl', ['bash', '-lc', `mkdir -p ${shellQuote(wslDir)} && cp ${shellQuote(wslSrc)} ${shellQuote(wslDestPath)}`], {
|
|
1673
|
+
timeout: 10000,
|
|
1674
|
+
stdio: 'pipe'
|
|
1675
|
+
});
|
|
1676
|
+
return true;
|
|
1677
|
+
}
|
|
1678
|
+
|
|
1679
|
+
function syncAuthProfilesToWsl(windowsAuthProfilesPath, wslDestPath) {
|
|
1680
|
+
try {
|
|
1681
|
+
const wslHome = getWslHome();
|
|
1682
|
+
if (!wslHome && !wslDestPath) return;
|
|
1683
|
+
const target = wslDestPath || path.posix.join(wslHome, '.openclaw', 'agents', 'main', 'agent', 'auth-profiles.json');
|
|
1684
|
+
syncFileToWsl(windowsAuthProfilesPath, target);
|
|
1685
|
+
} catch { /* best-effort */ }
|
|
1686
|
+
}
|
|
1687
|
+
|
|
1688
|
+
function syncMirroredAuthStores(paths) {
|
|
1689
|
+
if (!paths?.authProfiles) return;
|
|
1690
|
+
|
|
1691
|
+
const store = readAuthStore(paths.authProfiles);
|
|
1692
|
+
for (const target of [...new Set(paths.authSyncTargets || [])]) {
|
|
1693
|
+
if (!target || target === paths.authProfiles) continue;
|
|
1694
|
+
try {
|
|
1695
|
+
writeAuthStore(target, store);
|
|
1696
|
+
} catch { /* best-effort */ }
|
|
1697
|
+
}
|
|
1698
|
+
|
|
1699
|
+
if (process.platform === 'win32' && (paths.wslAuthProfiles || paths.wslConfigPath)) {
|
|
1700
|
+
const derivedWslAuthPath = paths.wslAuthProfiles || path.posix.join(path.posix.dirname(paths.wslConfigPath), 'agents', 'main', 'agent', 'auth-profiles.json');
|
|
1701
|
+
syncAuthProfilesToWsl(paths.authProfiles, derivedWslAuthPath);
|
|
1702
|
+
}
|
|
1703
|
+
}
|
|
1704
|
+
|
|
1705
|
+
function pruneAuthProfilesExceptWithSync(paths, keepProviders = []) {
|
|
1706
|
+
const removed = pruneAuthProfilesExcept(paths.authProfiles, keepProviders);
|
|
1707
|
+
syncMirroredAuthStores(paths);
|
|
1708
|
+
return removed;
|
|
1709
|
+
}
|
|
1710
|
+
|
|
1711
|
+
function updateAuthProfilesWithSync(paths, providerName, apiKey) {
|
|
1712
|
+
updateAuthProfiles(paths.authProfiles, providerName, apiKey);
|
|
1713
|
+
syncMirroredAuthStores(paths);
|
|
1714
|
+
}
|
|
1715
|
+
|
|
1418
1716
|
function pruneAuthProfilesByPrefix(authProfilesPath, prefixBase, keepProviders = []) {
|
|
1419
1717
|
const keepSet = new Set(keepProviders);
|
|
1420
1718
|
const store = readAuthStore(authProfilesPath);
|
|
@@ -1508,6 +1806,20 @@ async function promptApiKey(message, defaultValue) {
|
|
|
1508
1806
|
return '';
|
|
1509
1807
|
}
|
|
1510
1808
|
|
|
1809
|
+
async function confirmImmediateTest(args = {}, message = '是否立即测试连接?', defaultValue = true) {
|
|
1810
|
+
if (args.test !== undefined) {
|
|
1811
|
+
return !['false', '0', 'no'].includes(String(args.test).toLowerCase());
|
|
1812
|
+
}
|
|
1813
|
+
|
|
1814
|
+
const { shouldTest } = await inquirer.prompt([{
|
|
1815
|
+
type: 'confirm',
|
|
1816
|
+
name: 'shouldTest',
|
|
1817
|
+
message,
|
|
1818
|
+
default: defaultValue
|
|
1819
|
+
}]);
|
|
1820
|
+
return shouldTest;
|
|
1821
|
+
}
|
|
1822
|
+
|
|
1511
1823
|
function extendPathEnv(preferredNodePath) {
|
|
1512
1824
|
const current = process.env.PATH || '';
|
|
1513
1825
|
const parts = current.split(path.delimiter).filter(Boolean);
|
|
@@ -1705,6 +2017,251 @@ function getCliMeta() {
|
|
|
1705
2017
|
return { cliBinary, cliName, nodeMajor };
|
|
1706
2018
|
}
|
|
1707
2019
|
|
|
2020
|
+
function resolveCommandBinary(names) {
|
|
2021
|
+
const candidates = Array.isArray(names) ? names : [names];
|
|
2022
|
+
const isWin = process.platform === 'win32';
|
|
2023
|
+
const searchDirs = (process.env.PATH || '').split(path.delimiter).concat(EXTRA_BIN_DIRS).filter(Boolean);
|
|
2024
|
+
|
|
2025
|
+
for (const name of candidates) {
|
|
2026
|
+
const variants = isWin ? [`${name}.cmd`, `${name}.exe`, name] : [name];
|
|
2027
|
+
for (const variant of variants) {
|
|
2028
|
+
for (const dir of searchDirs) {
|
|
2029
|
+
const full = path.join(dir, variant);
|
|
2030
|
+
try {
|
|
2031
|
+
if (fs.existsSync(full) && fs.statSync(full).isFile()) {
|
|
2032
|
+
return full;
|
|
2033
|
+
}
|
|
2034
|
+
} catch { }
|
|
2035
|
+
}
|
|
2036
|
+
}
|
|
2037
|
+
|
|
2038
|
+
if (isWin) {
|
|
2039
|
+
const result = safeExec(`where ${name}`);
|
|
2040
|
+
if (result.ok && result.output) {
|
|
2041
|
+
return result.output.split('\n')[0].trim();
|
|
2042
|
+
}
|
|
2043
|
+
} else {
|
|
2044
|
+
for (const sh of ['/bin/zsh', '/bin/bash', '/bin/sh']) {
|
|
2045
|
+
if (!fs.existsSync(sh)) continue;
|
|
2046
|
+
const result = safeExec(`${sh} -lc "command -v ${name}"`);
|
|
2047
|
+
if (result.ok && result.output) {
|
|
2048
|
+
return result.output.split('\n')[0].trim();
|
|
2049
|
+
}
|
|
2050
|
+
}
|
|
2051
|
+
}
|
|
2052
|
+
}
|
|
2053
|
+
|
|
2054
|
+
return null;
|
|
2055
|
+
}
|
|
2056
|
+
|
|
2057
|
+
function readJsonIfExists(filePath) {
|
|
2058
|
+
if (!filePath || !fs.existsSync(filePath)) return null;
|
|
2059
|
+
try {
|
|
2060
|
+
return JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
|
2061
|
+
} catch {
|
|
2062
|
+
return null;
|
|
2063
|
+
}
|
|
2064
|
+
}
|
|
2065
|
+
|
|
2066
|
+
function getClaudeCodeSettingsPath() {
|
|
2067
|
+
return path.join(os.homedir(), '.claude', 'settings.json');
|
|
2068
|
+
}
|
|
2069
|
+
|
|
2070
|
+
function getOpencodeConfigPath() {
|
|
2071
|
+
const home = os.homedir();
|
|
2072
|
+
return process.platform === 'win32'
|
|
2073
|
+
? path.join(process.env.APPDATA || path.join(home, 'AppData', 'Roaming'), 'opencode', 'opencode.json')
|
|
2074
|
+
: path.join(home, '.config', 'opencode', 'opencode.json');
|
|
2075
|
+
}
|
|
2076
|
+
|
|
2077
|
+
function getCodexCliPaths() {
|
|
2078
|
+
const codexDir = path.join(os.homedir(), '.codex');
|
|
2079
|
+
return {
|
|
2080
|
+
configPath: path.join(codexDir, 'config.toml'),
|
|
2081
|
+
authPath: path.join(codexDir, 'auth.json')
|
|
2082
|
+
};
|
|
2083
|
+
}
|
|
2084
|
+
|
|
2085
|
+
function readClaudeCodeCliConfig() {
|
|
2086
|
+
const settingsPath = getClaudeCodeSettingsPath();
|
|
2087
|
+
const settings = readJsonIfExists(settingsPath) || {};
|
|
2088
|
+
return {
|
|
2089
|
+
settingsPath,
|
|
2090
|
+
modelId: settings.model || settings.env?.ANTHROPIC_MODEL || process.env.ANTHROPIC_MODEL || getDefaultClaudeModel().id,
|
|
2091
|
+
baseUrl: settings.env?.ANTHROPIC_BASE_URL || settings.apiBaseUrl || process.env.ANTHROPIC_BASE_URL || '',
|
|
2092
|
+
apiKey: settings.env?.ANTHROPIC_AUTH_TOKEN || process.env.ANTHROPIC_AUTH_TOKEN || '',
|
|
2093
|
+
configured: fs.existsSync(settingsPath)
|
|
2094
|
+
};
|
|
2095
|
+
}
|
|
2096
|
+
|
|
2097
|
+
function readOpencodeCliConfig() {
|
|
2098
|
+
const configPath = getOpencodeConfigPath();
|
|
2099
|
+
const config = readJsonIfExists(configPath) || {};
|
|
2100
|
+
return {
|
|
2101
|
+
configPath,
|
|
2102
|
+
modelKey: config.model || `yunyi-claude/${getDefaultClaudeModel().id}`,
|
|
2103
|
+
configured: fs.existsSync(configPath),
|
|
2104
|
+
config
|
|
2105
|
+
};
|
|
2106
|
+
}
|
|
2107
|
+
|
|
2108
|
+
function readCodexCliConfig() {
|
|
2109
|
+
const { configPath, authPath } = getCodexCliPaths();
|
|
2110
|
+
const configRaw = fs.existsSync(configPath) ? fs.readFileSync(configPath, 'utf8') : '';
|
|
2111
|
+
const auth = readJsonIfExists(authPath) || {};
|
|
2112
|
+
const model = (configRaw.match(/^model\s*=\s*"([^"]+)"\s*$/m) || [])[1] || getDefaultCodexModel().id;
|
|
2113
|
+
const provider = (configRaw.match(/^model_provider\s*=\s*"([^"]+)"\s*$/m) || [])[1] || 'yunyi-codex';
|
|
2114
|
+
const providerBlockRegex = new RegExp(`\\[model_providers\\.${escapeRegExp(provider)}\\]([\\s\\S]*?)(?=\\n\\[|$)`, 'm');
|
|
2115
|
+
const providerBlock = (configRaw.match(providerBlockRegex) || [])[1] || '';
|
|
2116
|
+
const baseUrl = (providerBlock.match(/base_url\s*=\s*"([^"]+)"/) || [])[1] || '';
|
|
2117
|
+
const apiKey = (providerBlock.match(/experimental_bearer_token\s*=\s*"([^"]+)"/) || [])[1] || auth.OPENAI_API_KEY || '';
|
|
2118
|
+
return {
|
|
2119
|
+
configPath,
|
|
2120
|
+
authPath,
|
|
2121
|
+
modelId: model,
|
|
2122
|
+
provider,
|
|
2123
|
+
baseUrl,
|
|
2124
|
+
apiKey,
|
|
2125
|
+
configured: fs.existsSync(configPath)
|
|
2126
|
+
};
|
|
2127
|
+
}
|
|
2128
|
+
|
|
2129
|
+
function cleanCliTestOutput(text) {
|
|
2130
|
+
return String(text || '')
|
|
2131
|
+
.replace(/\u001b\[[0-9;]*m/g, '')
|
|
2132
|
+
.replace(/\(node:\d+\) Warning: Setting the NODE_TLS_REJECT_UNAUTHORIZED[\s\S]*?(?=\n\S|$)/g, '')
|
|
2133
|
+
.replace(/\s+$/g, '')
|
|
2134
|
+
.trim();
|
|
2135
|
+
}
|
|
2136
|
+
|
|
2137
|
+
function summarizeCliTestOutput(text) {
|
|
2138
|
+
const lines = cleanCliTestOutput(text)
|
|
2139
|
+
.split('\n')
|
|
2140
|
+
.map(line => line.trim())
|
|
2141
|
+
.filter(Boolean)
|
|
2142
|
+
.filter(line => !/^logs?:/i.test(line));
|
|
2143
|
+
if (lines.length === 0) return '';
|
|
2144
|
+
return lines[0].slice(0, 160);
|
|
2145
|
+
}
|
|
2146
|
+
|
|
2147
|
+
function looksLikeCliTestError(text) {
|
|
2148
|
+
const lower = cleanCliTestOutput(text).toLowerCase();
|
|
2149
|
+
if (!lower) return false;
|
|
2150
|
+
return [
|
|
2151
|
+
'all models failed',
|
|
2152
|
+
'auth_permanent',
|
|
2153
|
+
'auth issue',
|
|
2154
|
+
'unauthorized',
|
|
2155
|
+
'forbidden',
|
|
2156
|
+
'missing environment variable',
|
|
2157
|
+
'gateway agent failed',
|
|
2158
|
+
'error:'
|
|
2159
|
+
].some(pattern => lower.includes(pattern));
|
|
2160
|
+
}
|
|
2161
|
+
|
|
2162
|
+
function runCliTestCandidates(name, commands, env) {
|
|
2163
|
+
let lastError = '命令执行失败';
|
|
2164
|
+
for (const command of commands) {
|
|
2165
|
+
const result = safeExec(command, { timeout: 120000, env, maxBuffer: 1024 * 1024 });
|
|
2166
|
+
const combined = cleanCliTestOutput(`${result.output || ''}\n${result.stdout || ''}\n${result.stderr || ''}`);
|
|
2167
|
+
if (result.ok && !looksLikeCliTestError(combined)) {
|
|
2168
|
+
return { name, status: 'success', detail: summarizeCliTestOutput(combined) || '连接成功' };
|
|
2169
|
+
}
|
|
2170
|
+
lastError = summarizeCliTestOutput(combined) || result.error || lastError;
|
|
2171
|
+
}
|
|
2172
|
+
return { name, status: 'failed', detail: lastError };
|
|
2173
|
+
}
|
|
2174
|
+
|
|
2175
|
+
function testClaudeCodeCliConnection() {
|
|
2176
|
+
const cliBinary = resolveCommandBinary('claude');
|
|
2177
|
+
if (!cliBinary) return { name: 'Claude Code CLI', status: 'skipped', detail: '未安装 claude 命令' };
|
|
2178
|
+
|
|
2179
|
+
const config = readClaudeCodeCliConfig();
|
|
2180
|
+
if (!config.configured) return { name: 'Claude Code CLI', status: 'skipped', detail: '未检测到 ~/.claude/settings.json' };
|
|
2181
|
+
if (!config.baseUrl || !config.apiKey) return { name: 'Claude Code CLI', status: 'failed', detail: 'Claude Code 配置缺少 Base URL 或 API Key' };
|
|
2182
|
+
|
|
2183
|
+
const env = {
|
|
2184
|
+
...process.env,
|
|
2185
|
+
PATH: extendPathEnv(null),
|
|
2186
|
+
ANTHROPIC_BASE_URL: config.baseUrl,
|
|
2187
|
+
ANTHROPIC_AUTH_TOKEN: config.apiKey,
|
|
2188
|
+
ANTHROPIC_MODEL: config.modelId,
|
|
2189
|
+
NODE_TLS_REJECT_UNAUTHORIZED: '0',
|
|
2190
|
+
NODE_NO_WARNINGS: '1'
|
|
2191
|
+
};
|
|
2192
|
+
|
|
2193
|
+
return runCliTestCandidates('Claude Code CLI', [
|
|
2194
|
+
`${shellQuote(cliBinary)} -p --model ${shellQuote(config.modelId)} --output-format text ${shellQuote('请只回复 OK')}`,
|
|
2195
|
+
`${shellQuote(cliBinary)} -p --model ${shellQuote(config.modelId)} ${shellQuote('请只回复 OK')}`,
|
|
2196
|
+
`${shellQuote(cliBinary)} -p ${shellQuote('请只回复 OK')}`
|
|
2197
|
+
], env);
|
|
2198
|
+
}
|
|
2199
|
+
|
|
2200
|
+
function testOpencodeCliConnection() {
|
|
2201
|
+
const cliBinary = resolveCommandBinary('opencode');
|
|
2202
|
+
if (!cliBinary) return { name: 'Opencode CLI', status: 'skipped', detail: '未安装 opencode 命令' };
|
|
2203
|
+
|
|
2204
|
+
const config = readOpencodeCliConfig();
|
|
2205
|
+
if (!config.configured) return { name: 'Opencode CLI', status: 'skipped', detail: '未检测到 opencode 配置文件' };
|
|
2206
|
+
|
|
2207
|
+
const env = {
|
|
2208
|
+
...process.env,
|
|
2209
|
+
PATH: extendPathEnv(null),
|
|
2210
|
+
NODE_TLS_REJECT_UNAUTHORIZED: '0',
|
|
2211
|
+
NODE_NO_WARNINGS: '1'
|
|
2212
|
+
};
|
|
2213
|
+
|
|
2214
|
+
return runCliTestCandidates('Opencode CLI', [
|
|
2215
|
+
`${shellQuote(cliBinary)} run --model ${shellQuote(config.modelKey)} ${shellQuote('请只回复 OK')}`,
|
|
2216
|
+
`${shellQuote(cliBinary)} run ${shellQuote('请只回复 OK')} --model ${shellQuote(config.modelKey)}`,
|
|
2217
|
+
`${shellQuote(cliBinary)} -m ${shellQuote(config.modelKey)} run ${shellQuote('请只回复 OK')}`
|
|
2218
|
+
], env);
|
|
2219
|
+
}
|
|
2220
|
+
|
|
2221
|
+
function testCodexCliConnection() {
|
|
2222
|
+
const cliBinary = resolveCommandBinary('codex');
|
|
2223
|
+
if (!cliBinary) return { name: 'Codex CLI', status: 'skipped', detail: '未安装 codex 命令' };
|
|
2224
|
+
|
|
2225
|
+
const config = readCodexCliConfig();
|
|
2226
|
+
if (!config.configured) return { name: 'Codex CLI', status: 'skipped', detail: '未检测到 ~/.codex/config.toml' };
|
|
2227
|
+
if (!config.apiKey) return { name: 'Codex CLI', status: 'failed', detail: 'Codex CLI 配置缺少 API Key' };
|
|
2228
|
+
|
|
2229
|
+
const env = {
|
|
2230
|
+
...process.env,
|
|
2231
|
+
PATH: extendPathEnv(null),
|
|
2232
|
+
OPENAI_API_KEY: config.apiKey,
|
|
2233
|
+
NODE_TLS_REJECT_UNAUTHORIZED: '0',
|
|
2234
|
+
NODE_NO_WARNINGS: '1'
|
|
2235
|
+
};
|
|
2236
|
+
|
|
2237
|
+
return runCliTestCandidates('Codex CLI', [
|
|
2238
|
+
`${shellQuote(cliBinary)} exec --skip-git-repo-check ${shellQuote('请只回复 OK')}`,
|
|
2239
|
+
`${shellQuote(cliBinary)} exec ${shellQuote('请只回复 OK')}`
|
|
2240
|
+
], env);
|
|
2241
|
+
}
|
|
2242
|
+
|
|
2243
|
+
async function testAdditionalCliConnections(args = {}, options = {}) {
|
|
2244
|
+
if (args['no-app-test'] || args.noAppTest) return;
|
|
2245
|
+
|
|
2246
|
+
const requested = new Set((options.only || ['claude', 'opencode', 'codex']).map(item => String(item)));
|
|
2247
|
+
const results = [];
|
|
2248
|
+
if (requested.has('claude')) results.push(testClaudeCodeCliConnection());
|
|
2249
|
+
if (requested.has('opencode')) results.push(testOpencodeCliConnection());
|
|
2250
|
+
if (requested.has('codex')) results.push(testCodexCliConnection());
|
|
2251
|
+
if (results.length === 0) return;
|
|
2252
|
+
|
|
2253
|
+
console.log(chalk.cyan('\n附加测试: 其他 CLI 连接...'));
|
|
2254
|
+
for (const result of results) {
|
|
2255
|
+
if (result.status === 'success') {
|
|
2256
|
+
console.log(chalk.green(`✅ ${result.name}: ${result.detail}`));
|
|
2257
|
+
} else if (result.status === 'skipped') {
|
|
2258
|
+
console.log(chalk.gray(`○ ${result.name}: ${result.detail}`));
|
|
2259
|
+
} else {
|
|
2260
|
+
console.log(chalk.red(`❌ ${result.name}: ${result.detail}`));
|
|
2261
|
+
}
|
|
2262
|
+
}
|
|
2263
|
+
}
|
|
2264
|
+
|
|
1708
2265
|
function getNodeMajor(versionOutput) {
|
|
1709
2266
|
const match = String(versionOutput || '').trim().match(/^v?(\d+)/);
|
|
1710
2267
|
return match ? Number(match[1]) : null;
|
|
@@ -1757,11 +2314,21 @@ function findCompatibleNode(minMajor = 22) {
|
|
|
1757
2314
|
function autoFixConfig(paths) {
|
|
1758
2315
|
try {
|
|
1759
2316
|
const config = readConfig(paths.openclawConfig);
|
|
1760
|
-
if (!config
|
|
2317
|
+
if (!config) return;
|
|
2318
|
+
|
|
2319
|
+
config.models = config.models || {};
|
|
2320
|
+
config.models.providers = config.models.providers || {};
|
|
1761
2321
|
|
|
1762
2322
|
let changed = false;
|
|
1763
2323
|
const codexProviderName = API_CONFIG.codex?.providerName;
|
|
1764
2324
|
const codexProvider = codexProviderName && config.models.providers[codexProviderName];
|
|
2325
|
+
const originalModelJson = JSON.stringify(config.agents?.defaults?.model ?? null);
|
|
2326
|
+
ensureConfigStructure(config);
|
|
2327
|
+
sanitizeDefaultModelSelection(config);
|
|
2328
|
+
const nextModelJson = JSON.stringify(config.agents?.defaults?.model ?? null);
|
|
2329
|
+
if (originalModelJson !== nextModelJson) {
|
|
2330
|
+
changed = true;
|
|
2331
|
+
}
|
|
1765
2332
|
|
|
1766
2333
|
// 修复: openai-responses → openai-completions(云翼服务器不支持 /v1/responses)
|
|
1767
2334
|
if (codexProvider && codexProvider.api === 'openai-responses') {
|
|
@@ -1777,7 +2344,7 @@ function autoFixConfig(paths) {
|
|
|
1777
2344
|
|
|
1778
2345
|
if (changed) {
|
|
1779
2346
|
writeConfig(paths.openclawConfig, config);
|
|
1780
|
-
console.log(chalk.green('✓
|
|
2347
|
+
console.log(chalk.green('✓ 已自动修复配置(模型结构 / API 协议)'));
|
|
1781
2348
|
}
|
|
1782
2349
|
} catch { /* ignore */ }
|
|
1783
2350
|
}
|
|
@@ -2298,7 +2865,7 @@ async function quickSetup(paths, args = {}) {
|
|
|
2298
2865
|
|
|
2299
2866
|
if (toRemove.length > 0) {
|
|
2300
2867
|
pruneProvidersExcept(config, [providerName]);
|
|
2301
|
-
|
|
2868
|
+
pruneAuthProfilesExceptWithSync(paths, [providerName]);
|
|
2302
2869
|
}
|
|
2303
2870
|
|
|
2304
2871
|
config.models.providers[providerName] = {
|
|
@@ -2336,7 +2903,7 @@ async function quickSetup(paths, args = {}) {
|
|
|
2336
2903
|
ensureGatewaySettings(config);
|
|
2337
2904
|
if (apiConfig.api.startsWith('openai')) cleanupConflictingEnvVars(config, normalizedBaseUrl, apiKey);
|
|
2338
2905
|
writeConfigWithSync(paths, config);
|
|
2339
|
-
|
|
2906
|
+
updateAuthProfilesWithSync(paths, providerName, apiKey);
|
|
2340
2907
|
ws.succeed('配置写入完成');
|
|
2341
2908
|
|
|
2342
2909
|
console.log(chalk.green(`\n✅ ${typeLabel} 中转已配置完成!`));
|
|
@@ -2416,7 +2983,7 @@ async function presetClaude(paths, args = {}) {
|
|
|
2416
2983
|
? pruneProvidersExcept(config, [providerName])
|
|
2417
2984
|
: [];
|
|
2418
2985
|
if (removedProviders.length > 0) {
|
|
2419
|
-
|
|
2986
|
+
pruneAuthProfilesExceptWithSync(paths, [providerName]);
|
|
2420
2987
|
}
|
|
2421
2988
|
|
|
2422
2989
|
const baseUrl = buildFullUrl(selectedEndpoint.url, 'claude');
|
|
@@ -2520,8 +3087,8 @@ async function presetClaude(paths, args = {}) {
|
|
|
2520
3087
|
createTimestampedBackup(paths.openclawConfig, paths.configDir, 'claude');
|
|
2521
3088
|
ensureGatewaySettings(config);
|
|
2522
3089
|
writeConfigWithSync(paths, config);
|
|
2523
|
-
|
|
2524
|
-
const extSynced = syncExternalTools('claude', baseUrl, apiKey);
|
|
3090
|
+
updateAuthProfilesWithSync(paths, providerName, apiKey);
|
|
3091
|
+
const extSynced = syncExternalTools('claude', baseUrl, apiKey, { claudeModelId: modelId, opencodeDefaultModelKey: `yunyi-claude/${modelId}` });
|
|
2525
3092
|
writeSpinner.succeed('配置写入完成');
|
|
2526
3093
|
|
|
2527
3094
|
console.log(chalk.green('\n✅ Claude 节点配置完成!'));
|
|
@@ -2610,7 +3177,7 @@ async function presetCodex(paths, args = {}) {
|
|
|
2610
3177
|
? pruneProvidersExcept(config, [providerName])
|
|
2611
3178
|
: [];
|
|
2612
3179
|
if (removedProviders.length > 0) {
|
|
2613
|
-
|
|
3180
|
+
pruneAuthProfilesExceptWithSync(paths, [providerName]);
|
|
2614
3181
|
}
|
|
2615
3182
|
|
|
2616
3183
|
const baseUrl = buildFullUrl(selectedEndpoint.url, 'codex');
|
|
@@ -2714,8 +3281,8 @@ async function presetCodex(paths, args = {}) {
|
|
|
2714
3281
|
ensureGatewaySettings(config);
|
|
2715
3282
|
cleanupConflictingEnvVars(config, baseUrl, apiKey);
|
|
2716
3283
|
writeConfigWithSync(paths, config);
|
|
2717
|
-
|
|
2718
|
-
const extSynced2 = syncExternalTools('codex', baseUrl, apiKey);
|
|
3284
|
+
updateAuthProfilesWithSync(paths, providerName, apiKey);
|
|
3285
|
+
const extSynced2 = syncExternalTools('codex', baseUrl, apiKey, { modelId });
|
|
2719
3286
|
writeSpinner2.succeed('配置写入完成');
|
|
2720
3287
|
|
|
2721
3288
|
console.log(chalk.green('\n✅ Codex 节点配置完成!'));
|
|
@@ -2898,11 +3465,12 @@ async function autoActivate(paths, args = {}) {
|
|
|
2898
3465
|
ensureGatewaySettings(config);
|
|
2899
3466
|
cleanupConflictingEnvVars(config, codexBaseUrl, apiKey);
|
|
2900
3467
|
writeConfigWithSync(paths, config);
|
|
2901
|
-
|
|
2902
|
-
|
|
3468
|
+
updateAuthProfilesWithSync(paths, claudeProviderName, apiKey);
|
|
3469
|
+
updateAuthProfilesWithSync(paths, codexProviderName, apiKey);
|
|
2903
3470
|
const extSynced = [];
|
|
2904
|
-
|
|
2905
|
-
try { syncExternalTools('
|
|
3471
|
+
const opencodeDefaultModelKey = isClaudePrimary ? `yunyi-claude/${claudeModelId}` : `yunyi-codex/${codexModelId}`;
|
|
3472
|
+
try { extSynced.push(...syncExternalTools('claude', claudeBaseUrl, apiKey, { codexBaseUrl, claudeModelId, opencodeDefaultModelKey })); } catch { /* ignore */ }
|
|
3473
|
+
try { extSynced.push(...syncExternalTools('codex', codexBaseUrl, apiKey, { modelId: codexModelId })); } catch { /* ignore */ }
|
|
2906
3474
|
writeSpinner.succeed('配置写入完成');
|
|
2907
3475
|
|
|
2908
3476
|
// ---- 输出结果 ----
|
|
@@ -2914,7 +3482,7 @@ async function autoActivate(paths, args = {}) {
|
|
|
2914
3482
|
console.log(chalk.gray(' API Key: 已设置'));
|
|
2915
3483
|
if (extSynced.length > 0) console.log(chalk.gray(` 同步: ${extSynced.join(', ')}`));
|
|
2916
3484
|
console.log(chalk.gray(' 若遇 certificate 报错,请新开终端或执行 source ~/.zshrc 后重试(已放宽 TLS 校验)'));
|
|
2917
|
-
console.log(chalk.gray(
|
|
3485
|
+
console.log(chalk.gray(` 使用 OpenCode 时可在界面中切换 ${getOpencodeSwitchHint()};Codex 仅支持 GPT`));
|
|
2918
3486
|
|
|
2919
3487
|
const gwPort = config.gateway?.port || 18789;
|
|
2920
3488
|
const gwToken = config.gateway?.auth?.token;
|
|
@@ -2945,8 +3513,7 @@ async function autoActivate(paths, args = {}) {
|
|
|
2945
3513
|
// ============ 单独配置 Claude Code CLI ============
|
|
2946
3514
|
async function activateClaudeCode(paths, args = {}) {
|
|
2947
3515
|
console.log(chalk.cyan.bold('\n🔧 配置 Claude Code CLI\n'));
|
|
2948
|
-
|
|
2949
|
-
const claudeApiConfig = API_CONFIG.claude;
|
|
3516
|
+
const selectedModel = await promptClaudeModelSelection(args);
|
|
2950
3517
|
|
|
2951
3518
|
// ---- 测速选节点 ----
|
|
2952
3519
|
const shouldTest = !(args['no-test'] || args.noTest);
|
|
@@ -3007,25 +3574,28 @@ async function activateClaudeCode(paths, args = {}) {
|
|
|
3007
3574
|
// ---- 写入配置 ----
|
|
3008
3575
|
const claudeBaseUrl = buildFullUrl(selectedEndpoint.url, 'claude');
|
|
3009
3576
|
const writeSpinner = ora({ text: '正在写入 Claude Code 配置...', spinner: 'dots' }).start();
|
|
3010
|
-
writeClaudeCodeSettings(claudeBaseUrl, apiKey);
|
|
3577
|
+
writeClaudeCodeSettings(claudeBaseUrl, apiKey, selectedModel.id);
|
|
3011
3578
|
writeSpinner.succeed('Claude Code 配置写入完成');
|
|
3012
3579
|
|
|
3013
3580
|
console.log(chalk.green('\n✅ Claude Code CLI 配置完成!'));
|
|
3014
3581
|
console.log(chalk.cyan(` Base URL: ${claudeBaseUrl}`));
|
|
3015
|
-
console.log(chalk.gray(` 模型:
|
|
3582
|
+
console.log(chalk.gray(` 模型: ${selectedModel.name} (${selectedModel.id})`));
|
|
3016
3583
|
console.log(chalk.gray(' API Key: 已设置'));
|
|
3017
3584
|
console.log(chalk.gray('\n 已写入:'));
|
|
3018
3585
|
console.log(chalk.gray(' • ~/.claude/settings.json'));
|
|
3019
3586
|
console.log(chalk.gray(' • ~/.claude.json (跳过 onboarding)'));
|
|
3020
|
-
console.log(chalk.gray(' • shell 环境变量 (ANTHROPIC_BASE_URL, ANTHROPIC_AUTH_TOKEN)'));
|
|
3587
|
+
console.log(chalk.gray(' • shell 环境变量 (ANTHROPIC_BASE_URL, ANTHROPIC_AUTH_TOKEN, ANTHROPIC_MODEL)'));
|
|
3021
3588
|
console.log(chalk.yellow('\n 提示: 请重新打开终端或执行 source ~/.zshrc 使环境变量生效'));
|
|
3589
|
+
|
|
3590
|
+
if (await confirmImmediateTest(args, '是否立即测试 Claude Code CLI 连接?')) {
|
|
3591
|
+
await testAdditionalCliConnections(args, { only: ['claude'] });
|
|
3592
|
+
}
|
|
3022
3593
|
}
|
|
3023
3594
|
|
|
3024
3595
|
// ============ 单独配置 Opencode ============
|
|
3025
3596
|
async function activateOpencode(paths, args = {}) {
|
|
3026
3597
|
console.log(chalk.cyan.bold('\n🔧 配置 Opencode\n'));
|
|
3027
|
-
|
|
3028
|
-
const claudeApiConfig = API_CONFIG.claude;
|
|
3598
|
+
const defaultModel = await promptOpencodeDefaultModelSelection(args);
|
|
3029
3599
|
|
|
3030
3600
|
// ---- 测速选节点 ----
|
|
3031
3601
|
const shouldTest = !(args['no-test'] || args.noTest);
|
|
@@ -3086,21 +3656,24 @@ async function activateOpencode(paths, args = {}) {
|
|
|
3086
3656
|
// ---- 写入配置 ----
|
|
3087
3657
|
const claudeBaseUrl = buildFullUrl(selectedEndpoint.url, 'claude');
|
|
3088
3658
|
const codexBaseUrl = buildFullUrl(selectedEndpoint.url, 'codex');
|
|
3089
|
-
const modelId = 'claude-sonnet-4-6';
|
|
3090
3659
|
const writeSpinner = ora({ text: '正在写入 Opencode 配置...', spinner: 'dots' }).start();
|
|
3091
|
-
|
|
3660
|
+
writeOpencodeConfig(claudeBaseUrl, codexBaseUrl, apiKey, defaultModel.modelKey);
|
|
3092
3661
|
writeSpinner.succeed('Opencode 配置写入完成');
|
|
3093
3662
|
|
|
3094
3663
|
console.log(chalk.green('\n✅ Opencode 配置完成!'));
|
|
3095
|
-
console.log(chalk.cyan(` Claude: ${claudeBaseUrl} →
|
|
3096
|
-
console.log(chalk.cyan(` Codex: ${codexBaseUrl} →
|
|
3097
|
-
console.log(chalk.gray(` 默认模型:
|
|
3664
|
+
console.log(chalk.cyan(` Claude: ${claudeBaseUrl} → ${getClaudeSwitchHint()}`));
|
|
3665
|
+
console.log(chalk.cyan(` Codex: ${codexBaseUrl} → ${CODEX_MODELS.map(model => model.name).join(' / ')}`));
|
|
3666
|
+
console.log(chalk.gray(` 默认模型: ${defaultModel.modelKey}`));
|
|
3098
3667
|
console.log(chalk.gray(' API Key: 已设置'));
|
|
3099
3668
|
console.log(chalk.gray('\n 已写入:'));
|
|
3100
3669
|
console.log(chalk.gray(' • ~/.config/opencode/opencode.json (CLI + 桌面版)'));
|
|
3101
3670
|
console.log(chalk.gray(' • ~/.codex/config.toml (model_providers)'));
|
|
3102
3671
|
console.log(chalk.gray(' • ~/.codex/auth.json (API Keys)'));
|
|
3103
|
-
console.log(chalk.yellow(
|
|
3672
|
+
console.log(chalk.yellow(`\n 切换模型: 在 opencode 内使用 /model 命令切换 (${getOpencodeSwitchHint()})`));
|
|
3673
|
+
|
|
3674
|
+
if (await confirmImmediateTest(args, '是否立即测试 Opencode CLI 连接?')) {
|
|
3675
|
+
await testAdditionalCliConnections(args, { only: ['opencode'] });
|
|
3676
|
+
}
|
|
3104
3677
|
}
|
|
3105
3678
|
|
|
3106
3679
|
// ============ 单独配置 Codex CLI ============
|
|
@@ -3108,7 +3681,7 @@ async function activateCodex(paths, args = {}) {
|
|
|
3108
3681
|
console.log(chalk.cyan.bold('\n🔧 配置 Codex CLI\n'));
|
|
3109
3682
|
|
|
3110
3683
|
// ---- 选模型 ----
|
|
3111
|
-
let modelId =
|
|
3684
|
+
let modelId = getDefaultCodexModel().id;
|
|
3112
3685
|
if (CODEX_MODELS.length > 1) {
|
|
3113
3686
|
const { selected } = await inquirer.prompt([{
|
|
3114
3687
|
type: 'list',
|
|
@@ -3190,6 +3763,10 @@ async function activateCodex(paths, args = {}) {
|
|
|
3190
3763
|
console.log(chalk.gray(' • ~/.codex/config.toml (model + model_providers)'));
|
|
3191
3764
|
console.log(chalk.gray(' • ~/.codex/auth.json (OPENAI_API_KEY)'));
|
|
3192
3765
|
console.log(chalk.yellow('\n 提示: 请重新打开终端使配置生效'));
|
|
3766
|
+
|
|
3767
|
+
if (await confirmImmediateTest(args, '是否立即测试 Codex CLI 连接?')) {
|
|
3768
|
+
await testAdditionalCliConnections(args, { only: ['codex'] });
|
|
3769
|
+
}
|
|
3193
3770
|
}
|
|
3194
3771
|
|
|
3195
3772
|
// ============ yycode 精简模式(零交互一键配置) ============
|
|
@@ -3333,10 +3910,10 @@ async function yycodeQuickSetup(paths) {
|
|
|
3333
3910
|
ensureGatewaySettings(config);
|
|
3334
3911
|
cleanupConflictingEnvVars(config, codexBaseUrl, apiKey);
|
|
3335
3912
|
writeConfigWithSync(paths, config);
|
|
3336
|
-
|
|
3337
|
-
|
|
3338
|
-
try { syncExternalTools('claude', claudeBaseUrl, apiKey, { codexBaseUrl }); } catch { /* ignore */ }
|
|
3339
|
-
try { syncExternalTools('codex', codexBaseUrl, apiKey); } catch { /* ignore */ }
|
|
3913
|
+
updateAuthProfilesWithSync(paths, claudeProviderName, apiKey);
|
|
3914
|
+
updateAuthProfilesWithSync(paths, codexProviderName, apiKey);
|
|
3915
|
+
try { syncExternalTools('claude', claudeBaseUrl, apiKey, { codexBaseUrl, claudeModelId, opencodeDefaultModelKey: `yunyi-codex/${codexModelId}` }); } catch { /* ignore */ }
|
|
3916
|
+
try { syncExternalTools('codex', codexBaseUrl, apiKey, { modelId: codexModelId }); } catch { /* ignore */ }
|
|
3340
3917
|
writeSpinner.succeed('配置写入完成');
|
|
3341
3918
|
|
|
3342
3919
|
// ---- 结果 ----
|
|
@@ -3346,7 +3923,7 @@ async function yycodeQuickSetup(paths) {
|
|
|
3346
3923
|
console.log(chalk.cyan(` Codex (主): ${codexBaseUrl}`));
|
|
3347
3924
|
console.log(chalk.gray(` 模型: ${codexModel.name}`));
|
|
3348
3925
|
console.log(chalk.gray(' API Key: 已设置'));
|
|
3349
|
-
console.log(chalk.gray(' 同步: Claude Code settings, Codex CLI config'));
|
|
3926
|
+
console.log(chalk.gray(' 同步: Claude Code settings, Opencode config, Codex CLI config'));
|
|
3350
3927
|
console.log('');
|
|
3351
3928
|
}
|
|
3352
3929
|
|
|
@@ -3418,7 +3995,7 @@ async function main() {
|
|
|
3418
3995
|
{ name: ' 配置 Opencode', value: 'activate_opencode' },
|
|
3419
3996
|
{ name: ' 配置 Codex CLI', value: 'activate_codex' },
|
|
3420
3997
|
new inquirer.Separator(' -- 工具 --'),
|
|
3421
|
-
{ name: '
|
|
3998
|
+
{ name: ' 切换 OpenClaw 模型', value: 'switch_model' },
|
|
3422
3999
|
{ name: ` 权限管理${getToolsProfileTag(paths)}`, value: 'tools_profile' },
|
|
3423
4000
|
{ name: ' 测试连接', value: 'test_connection' },
|
|
3424
4001
|
{ name: ' 查看配置', value: 'view_config' },
|
|
@@ -3663,9 +4240,9 @@ async function activate(paths, type) {
|
|
|
3663
4240
|
}
|
|
3664
4241
|
|
|
3665
4242
|
|
|
3666
|
-
// ============
|
|
4243
|
+
// ============ 切换 OpenClaw 模型 ============
|
|
3667
4244
|
async function switchModel(paths) {
|
|
3668
|
-
console.log(chalk.cyan('🔄
|
|
4245
|
+
console.log(chalk.cyan('🔄 切换 OpenClaw 模型\n'));
|
|
3669
4246
|
|
|
3670
4247
|
const config = ensureConfigStructure(readConfig(paths.openclawConfig) || {});
|
|
3671
4248
|
const primary = config.agents?.defaults?.model?.primary || '';
|
|
@@ -3758,7 +4335,7 @@ async function switchModel(paths) {
|
|
|
3758
4335
|
const { selected } = await inquirer.prompt([{
|
|
3759
4336
|
type: 'list',
|
|
3760
4337
|
name: 'selected',
|
|
3761
|
-
message: '
|
|
4338
|
+
message: '选择 OpenClaw 模型:',
|
|
3762
4339
|
default: primaryInPreset ? primary : undefined,
|
|
3763
4340
|
pageSize: 15,
|
|
3764
4341
|
choices,
|
|
@@ -3768,7 +4345,7 @@ async function switchModel(paths) {
|
|
|
3768
4345
|
|
|
3769
4346
|
if (selected === '__other__') {
|
|
3770
4347
|
if (otherModels.length === 0) {
|
|
3771
|
-
|
|
4348
|
+
console.log(chalk.gray('\n当前 OpenClaw 配置中没有其他模型,仅有预设模型可用'));
|
|
3772
4349
|
return;
|
|
3773
4350
|
}
|
|
3774
4351
|
const otherChoices = otherModels.map(o => {
|
|
@@ -3927,7 +4504,8 @@ async function manageToolsProfile(paths) {
|
|
|
3927
4504
|
|
|
3928
4505
|
// ============ 测试连接 ============
|
|
3929
4506
|
async function testConnection(paths, args = {}) {
|
|
3930
|
-
console.log(chalk.cyan('🧪 测试 OpenClaw
|
|
4507
|
+
console.log(chalk.cyan('🧪 测试 OpenClaw / 各 CLI 连接\n'));
|
|
4508
|
+
invalidateGatewayEnvCache();
|
|
3931
4509
|
|
|
3932
4510
|
const config = readConfig(paths.openclawConfig);
|
|
3933
4511
|
|
|
@@ -3939,7 +4517,7 @@ async function testConnection(paths, args = {}) {
|
|
|
3939
4517
|
// 检查当前激活的是哪个
|
|
3940
4518
|
let primary = config.agents?.defaults?.model?.primary || '';
|
|
3941
4519
|
if (!primary.includes('/')) {
|
|
3942
|
-
console.log(chalk.yellow('⚠️
|
|
4520
|
+
console.log(chalk.yellow('⚠️ 主模型未设置,请先通过「切换 OpenClaw 模型」或「一键配置」设置主模型'));
|
|
3943
4521
|
return;
|
|
3944
4522
|
}
|
|
3945
4523
|
|
|
@@ -4064,6 +4642,15 @@ async function testConnection(paths, args = {}) {
|
|
|
4064
4642
|
console.log(chalk.red(` 期望: ${primary}`));
|
|
4065
4643
|
console.log(chalk.cyan(` 实际: ${actualModelKey}`));
|
|
4066
4644
|
console.log(chalk.gray(` 这意味着 ${primary} 无法正常工作,请检查该模型的中转配置`));
|
|
4645
|
+
const configuredFallbacks = config.agents?.defaults?.model?.fallbacks || [];
|
|
4646
|
+
if (configuredFallbacks.length > 0 && !configuredFallbacks.includes(actualModelKey)) {
|
|
4647
|
+
console.log(chalk.yellow(` ⚠️ 实际回退模型不在当前配置的 fallbacks 中,疑似读到了另一份配置`));
|
|
4648
|
+
console.log(chalk.gray(` 当前 fallbacks: ${configuredFallbacks.join(', ')}`));
|
|
4649
|
+
console.log(chalk.gray(` 当前配置文件: ${paths.openclawConfig}`));
|
|
4650
|
+
if (process.platform === 'win32') {
|
|
4651
|
+
console.log(chalk.gray(' Windows 请重点检查: %USERPROFILE%\\.openclaw、%USERPROFILE%\\.clawdbot、WSL ~/.openclaw'));
|
|
4652
|
+
}
|
|
4653
|
+
}
|
|
4067
4654
|
// 检查是否可能是 api 字段错误导致的 502
|
|
4068
4655
|
const expectedProvider = primary.split('/')[0];
|
|
4069
4656
|
const providerCfg = config.models?.providers?.[expectedProvider];
|
|
@@ -4179,12 +4766,15 @@ async function testConnection(paths, args = {}) {
|
|
|
4179
4766
|
}
|
|
4180
4767
|
} catch (error) {
|
|
4181
4768
|
console.log(chalk.red(`❌ 测试失败: ${error.message}`));
|
|
4769
|
+
} finally {
|
|
4770
|
+
await testAdditionalCliConnections(args);
|
|
4182
4771
|
}
|
|
4183
4772
|
}
|
|
4184
4773
|
|
|
4185
4774
|
// ============ 重启 Gateway ============
|
|
4186
4775
|
async function restartGateway({ silent = false } = {}) {
|
|
4187
4776
|
if (!silent) console.log(chalk.cyan('\n正在重启 OpenClaw Gateway...'));
|
|
4777
|
+
invalidateGatewayEnvCache();
|
|
4188
4778
|
|
|
4189
4779
|
const gwEnv = detectGatewayEnv();
|
|
4190
4780
|
const configPaths = getConfigPath();
|
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
"models": {
|
|
26
26
|
"claude": [
|
|
27
27
|
{ "id": "claude-sonnet-4-6", "name": "Claude Sonnet 4.6" },
|
|
28
|
-
{ "id": "claude-opus-4-6", "name": "Claude Opus 4.6 (
|
|
28
|
+
{ "id": "claude-opus-4-6", "name": "Claude Opus 4.6 (待稳定)" }
|
|
29
29
|
],
|
|
30
30
|
"codex": [
|
|
31
31
|
{ "id": "gpt-5.4", "name": "GPT 5.4" },
|