yymaxapi 1.0.103 → 1.0.108
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/README.md
CHANGED
|
@@ -8,8 +8,14 @@
|
|
|
8
8
|
```bash
|
|
9
9
|
npx yymaxapi@latest
|
|
10
10
|
```
|
|
11
|
-
|
|
12
|
-
|
|
11
|
+
按提示选择模型并输入 Key 即可,默认会直接使用预设默认节点。
|
|
12
|
+
主菜单已简化为:一键配置 / 配置外部工具 / 测速节点 / 测试连接 / 查看配置 / 恢复 / 退出。
|
|
13
|
+
|
|
14
|
+
**需要手动测速时**
|
|
15
|
+
```bash
|
|
16
|
+
npx yymaxapi@latest speed-test
|
|
17
|
+
```
|
|
18
|
+
或在具体配置命令后追加 `--speed-test`,按测速结果手动选点。
|
|
13
19
|
|
|
14
20
|
**方式二:一键配置 Claude**
|
|
15
21
|
```bash
|
package/bin/yymaxapi.js
CHANGED
|
@@ -321,6 +321,90 @@ async function promptClaudeModelSelection(args = {}, message = '选择 Claude
|
|
|
321
321
|
return CLAUDE_MODELS.find(model => model.id === selected) || fallback;
|
|
322
322
|
}
|
|
323
323
|
|
|
324
|
+
function normalizeHermesModelType(value) {
|
|
325
|
+
const normalized = String(value || '').trim().toLowerCase();
|
|
326
|
+
if (!normalized) return '';
|
|
327
|
+
if (['gpt', 'codex', 'openai'].includes(normalized)) return 'codex';
|
|
328
|
+
if (['claude', 'anthropic'].includes(normalized)) return 'claude';
|
|
329
|
+
return '';
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
function resolveHermesModelType(modelId, preferredType = '') {
|
|
333
|
+
const requestedType = normalizeHermesModelType(preferredType);
|
|
334
|
+
const inClaude = CLAUDE_MODELS.some(model => model.id === modelId);
|
|
335
|
+
const inCodex = CODEX_MODELS.some(model => model.id === modelId);
|
|
336
|
+
|
|
337
|
+
if (requestedType === 'claude' && (inClaude || !inCodex)) return 'claude';
|
|
338
|
+
if (requestedType === 'codex' && (inCodex || !inClaude)) return 'codex';
|
|
339
|
+
if (inCodex && !inClaude) return 'codex';
|
|
340
|
+
if (inClaude && !inCodex) return 'claude';
|
|
341
|
+
return requestedType || 'claude';
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
async function promptHermesModelSelection(args = {}, message = '选择 Hermes 默认模型:') {
|
|
345
|
+
const requestedModel = (args['model-id'] || args.model || '').toString().trim();
|
|
346
|
+
const requestedType = normalizeHermesModelType(
|
|
347
|
+
args.primary || args.type || args.protocol || args.provider || args.runtime
|
|
348
|
+
);
|
|
349
|
+
const explicitClaudeModel = (args['claude-model'] || '').toString().trim();
|
|
350
|
+
const explicitCodexModel = (args['codex-model'] || '').toString().trim();
|
|
351
|
+
|
|
352
|
+
let selectedType = requestedType;
|
|
353
|
+
if (!selectedType && explicitClaudeModel) selectedType = 'claude';
|
|
354
|
+
if (!selectedType && explicitCodexModel) selectedType = 'codex';
|
|
355
|
+
if (!selectedType && requestedModel) {
|
|
356
|
+
selectedType = resolveHermesModelType(requestedModel);
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
if (!selectedType) {
|
|
360
|
+
if (CLAUDE_MODELS.length > 0 && CODEX_MODELS.length === 0) {
|
|
361
|
+
selectedType = 'claude';
|
|
362
|
+
} else if (CODEX_MODELS.length > 0 && CLAUDE_MODELS.length === 0) {
|
|
363
|
+
selectedType = 'codex';
|
|
364
|
+
} else {
|
|
365
|
+
const { selectedType: pickedType } = await inquirer.prompt([{
|
|
366
|
+
type: 'list',
|
|
367
|
+
name: 'selectedType',
|
|
368
|
+
message: '选择 Hermes 模型类型:',
|
|
369
|
+
choices: [
|
|
370
|
+
{ name: 'Claude', value: 'claude' },
|
|
371
|
+
{ name: 'GPT', value: 'codex' }
|
|
372
|
+
],
|
|
373
|
+
default: 'claude'
|
|
374
|
+
}]);
|
|
375
|
+
selectedType = pickedType;
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
const modelList = selectedType === 'codex' ? CODEX_MODELS : CLAUDE_MODELS;
|
|
380
|
+
const fallbackModel = modelList[0] || (selectedType === 'codex' ? getDefaultCodexModel() : getDefaultClaudeModel());
|
|
381
|
+
const explicitModelId = selectedType === 'codex'
|
|
382
|
+
? (explicitCodexModel || requestedModel)
|
|
383
|
+
: (explicitClaudeModel || requestedModel);
|
|
384
|
+
|
|
385
|
+
if (explicitModelId) {
|
|
386
|
+
const model = modelList.find(item => item.id === explicitModelId) || { id: explicitModelId, name: explicitModelId };
|
|
387
|
+
return { type: selectedType, model };
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
if (modelList.length <= 1) {
|
|
391
|
+
return { type: selectedType, model: fallbackModel };
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
const { selected } = await inquirer.prompt([{
|
|
395
|
+
type: 'list',
|
|
396
|
+
name: 'selected',
|
|
397
|
+
message,
|
|
398
|
+
choices: modelList.map(model => ({ name: model.name, value: model.id })),
|
|
399
|
+
default: fallbackModel.id
|
|
400
|
+
}]);
|
|
401
|
+
|
|
402
|
+
return {
|
|
403
|
+
type: selectedType,
|
|
404
|
+
model: modelList.find(model => model.id === selected) || fallbackModel
|
|
405
|
+
};
|
|
406
|
+
}
|
|
407
|
+
|
|
324
408
|
async function promptOpencodeDefaultModelSelection(args = {}, message = '选择 Opencode 默认模型:') {
|
|
325
409
|
const fallbackClaude = getDefaultClaudeModel();
|
|
326
410
|
const fallbackCodex = getDefaultCodexModel();
|
|
@@ -523,6 +607,77 @@ async function testFallbackEndpoints() {
|
|
|
523
607
|
return testAllEndpoints(FALLBACK_EPS, { label: '检测备用节点', autoFallback: false });
|
|
524
608
|
}
|
|
525
609
|
|
|
610
|
+
function shouldRunEndpointSpeedTest(args = {}) {
|
|
611
|
+
const raw = args['speed-test'] !== undefined ? args['speed-test'] : args.speedTest;
|
|
612
|
+
if (raw === undefined) return false;
|
|
613
|
+
if (typeof raw === 'string') {
|
|
614
|
+
return !['false', '0', 'no'].includes(raw.toLowerCase());
|
|
615
|
+
}
|
|
616
|
+
return !!raw;
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
async function resolveEndpointSelection(args = {}, options = {}) {
|
|
620
|
+
const defaultEndpoint = options.defaultEndpoint || ENDPOINTS[0];
|
|
621
|
+
if (!defaultEndpoint) return null;
|
|
622
|
+
if (!shouldRunEndpointSpeedTest(args)) return defaultEndpoint;
|
|
623
|
+
|
|
624
|
+
const speedIntro = options.speedIntro || '📡 开始测速节点...\n';
|
|
625
|
+
const proceedMessage = options.proceedMessage || '仍要写入默认节点配置吗?';
|
|
626
|
+
const chooseMessage = options.chooseMessage || '选择节点:';
|
|
627
|
+
|
|
628
|
+
console.log(chalk.cyan(`${speedIntro}`));
|
|
629
|
+
const speedResult = await testAllEndpoints(ENDPOINTS, { autoFallback: true });
|
|
630
|
+
const sorted = speedResult.ranked || [];
|
|
631
|
+
|
|
632
|
+
if (sorted.length > 0) {
|
|
633
|
+
const { selectedIndex } = await inquirer.prompt([{
|
|
634
|
+
type: 'list',
|
|
635
|
+
name: 'selectedIndex',
|
|
636
|
+
message: chooseMessage,
|
|
637
|
+
choices: [
|
|
638
|
+
{ name: `* 使用默认节点 (${defaultEndpoint.name})`, value: -1 },
|
|
639
|
+
new inquirer.Separator(' ---- 或按测速结果选择 ----'),
|
|
640
|
+
...sorted.map((endpoint, index) => ({
|
|
641
|
+
name: `${endpoint.name} - ${endpoint.latency}ms (评分:${endpoint.score})`,
|
|
642
|
+
value: index
|
|
643
|
+
}))
|
|
644
|
+
]
|
|
645
|
+
}]);
|
|
646
|
+
|
|
647
|
+
const selectedEndpoint = selectedIndex === -1 ? defaultEndpoint : sorted[selectedIndex];
|
|
648
|
+
if (speedResult.usedFallback) {
|
|
649
|
+
console.log(chalk.yellow('\n⚠ 当前使用备用节点\n'));
|
|
650
|
+
}
|
|
651
|
+
return selectedEndpoint;
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
console.log(chalk.red('\n⚠️ 所有节点(含备用)均不可达'));
|
|
655
|
+
const { proceed } = await inquirer.prompt([{
|
|
656
|
+
type: 'confirm',
|
|
657
|
+
name: 'proceed',
|
|
658
|
+
message: proceedMessage,
|
|
659
|
+
default: false
|
|
660
|
+
}]);
|
|
661
|
+
if (!proceed) {
|
|
662
|
+
console.log(chalk.gray('已取消'));
|
|
663
|
+
return null;
|
|
664
|
+
}
|
|
665
|
+
return defaultEndpoint;
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
async function speedTestNodes() {
|
|
669
|
+
console.log(chalk.cyan.bold('\n📡 节点测速\n'));
|
|
670
|
+
const speedResult = await testAllEndpoints(ENDPOINTS, { autoFallback: true });
|
|
671
|
+
const defaultEndpoint = ENDPOINTS[0];
|
|
672
|
+
|
|
673
|
+
if (defaultEndpoint) {
|
|
674
|
+
console.log(chalk.gray(`\n默认配置当前直连: ${defaultEndpoint.name} (${defaultEndpoint.url})`));
|
|
675
|
+
}
|
|
676
|
+
if (speedResult.best && defaultEndpoint && speedResult.best.url !== defaultEndpoint.url) {
|
|
677
|
+
console.log(chalk.yellow('如需按测速结果选点,可在对应配置命令后追加 --speed-test'));
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
|
|
526
681
|
// ============ API Key 验证 ============
|
|
527
682
|
function httpGetJson(url, headers = {}, timeout = 10000) {
|
|
528
683
|
return new Promise((resolve, reject) => {
|
|
@@ -883,11 +1038,13 @@ function writeClaudeCodeSettings(baseUrl, apiKey, modelId = getDefaultClaudeMode
|
|
|
883
1038
|
if (fs.existsSync(settingsPath)) {
|
|
884
1039
|
try { settings = JSON.parse(fs.readFileSync(settingsPath, 'utf8')); } catch { settings = {}; }
|
|
885
1040
|
}
|
|
886
|
-
|
|
1041
|
+
const normalizedBaseUrl = baseUrl.replace(/\/+$/, '');
|
|
1042
|
+
settings.apiBaseUrl = normalizedBaseUrl;
|
|
887
1043
|
if (!settings.env) settings.env = {};
|
|
888
|
-
settings.env.
|
|
1044
|
+
settings.env.ANTHROPIC_API_KEY = apiKey;
|
|
1045
|
+
settings.env.ANTHROPIC_BASE_URL = normalizedBaseUrl;
|
|
1046
|
+
delete settings.env.ANTHROPIC_AUTH_TOKEN;
|
|
889
1047
|
delete settings.availableModels;
|
|
890
|
-
delete settings.env.ANTHROPIC_BASE_URL;
|
|
891
1048
|
delete settings.env.ANTHROPIC_MODEL;
|
|
892
1049
|
if (!fs.existsSync(claudeDir)) fs.mkdirSync(claudeDir, { recursive: true });
|
|
893
1050
|
fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2), 'utf8');
|
|
@@ -1075,12 +1232,16 @@ function writeOpencodeConfig(claudeBaseUrl, codexBaseUrl, apiKey, defaultModelKe
|
|
|
1075
1232
|
return configPath;
|
|
1076
1233
|
}
|
|
1077
1234
|
|
|
1078
|
-
function writeHermesConfig(baseUrl, apiKey, modelId = getDefaultClaudeModel().id) {
|
|
1235
|
+
function writeHermesConfig(baseUrl, apiKey, modelId = getDefaultClaudeModel().id, options = {}) {
|
|
1079
1236
|
const dataDir = getHermesDataDir();
|
|
1080
1237
|
const configPath = path.join(dataDir, 'config.yaml');
|
|
1081
1238
|
const envPath = getHermesEnvPath();
|
|
1082
1239
|
const existingConfigPath = getHermesConfigPath();
|
|
1083
1240
|
const wslMirror = getHermesWslMirrorInfo();
|
|
1241
|
+
const modelType = options.type || options.provider || '';
|
|
1242
|
+
const resolvedType = resolveHermesModelType(modelId, modelType);
|
|
1243
|
+
const runtimeProvider = resolvedType === 'codex' ? 'custom' : 'anthropic';
|
|
1244
|
+
const apiMode = resolvedType === 'codex' ? 'codex_responses' : 'anthropic_messages';
|
|
1084
1245
|
|
|
1085
1246
|
let existingConfigRaw = readTextIfExists(existingConfigPath);
|
|
1086
1247
|
if (!existingConfigRaw && wslMirror.sourceConfigPath) {
|
|
@@ -1092,15 +1253,25 @@ function writeHermesConfig(baseUrl, apiKey, modelId = getDefaultClaudeModel().id
|
|
|
1092
1253
|
existingEnvRaw = readWslTextFile(wslMirror.envPath);
|
|
1093
1254
|
}
|
|
1094
1255
|
|
|
1095
|
-
const normalizedBaseUrl =
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
const
|
|
1102
|
-
|
|
1256
|
+
const normalizedBaseUrl = (
|
|
1257
|
+
resolvedType === 'codex'
|
|
1258
|
+
? trimOpenAiEndpointSuffix(String(baseUrl || '').trim())
|
|
1259
|
+
: trimClaudeMessagesSuffix(String(baseUrl || '').trim())
|
|
1260
|
+
).replace(/\/+$/, '');
|
|
1261
|
+
const existingConfig = parseSimpleYamlConfig(existingConfigRaw);
|
|
1262
|
+
const existingModelConfig = existingConfig.model && !Array.isArray(existingConfig.model) && typeof existingConfig.model === 'object'
|
|
1263
|
+
? existingConfig.model
|
|
1264
|
+
: {};
|
|
1265
|
+
const nextConfigRaw = upsertHermesModelConfig(existingConfigRaw, {
|
|
1266
|
+
...existingModelConfig,
|
|
1267
|
+
default: modelId,
|
|
1268
|
+
provider: runtimeProvider,
|
|
1269
|
+
base_url: normalizedBaseUrl,
|
|
1270
|
+
api_mode: apiMode
|
|
1103
1271
|
});
|
|
1272
|
+
const envEntries = {};
|
|
1273
|
+
envEntries[resolvedType === 'codex' ? 'OPENAI_API_KEY' : 'ANTHROPIC_API_KEY'] = String(apiKey || '').trim();
|
|
1274
|
+
const nextEnvRaw = upsertEnvFile(existingEnvRaw, envEntries);
|
|
1104
1275
|
|
|
1105
1276
|
if (!fs.existsSync(dataDir)) fs.mkdirSync(dataDir, { recursive: true });
|
|
1106
1277
|
fs.writeFileSync(configPath, nextConfigRaw, 'utf8');
|
|
@@ -1120,6 +1291,9 @@ function writeHermesConfig(baseUrl, apiKey, modelId = getDefaultClaudeModel().id
|
|
|
1120
1291
|
wslConfigPath: wslMirror.configPath,
|
|
1121
1292
|
wslEnvPath: wslMirror.envPath,
|
|
1122
1293
|
modelId,
|
|
1294
|
+
modelType: resolvedType,
|
|
1295
|
+
provider: runtimeProvider,
|
|
1296
|
+
apiMode,
|
|
1123
1297
|
baseUrl: normalizedBaseUrl
|
|
1124
1298
|
};
|
|
1125
1299
|
}
|
|
@@ -3508,24 +3682,58 @@ function readWslTextFile(filePath) {
|
|
|
3508
3682
|
return result.ok ? (result.output || result.stdout || '') : '';
|
|
3509
3683
|
}
|
|
3510
3684
|
|
|
3685
|
+
function parseYamlScalar(value) {
|
|
3686
|
+
let normalized = String(value ?? '').trim();
|
|
3687
|
+
if (
|
|
3688
|
+
(normalized.startsWith('"') && normalized.endsWith('"'))
|
|
3689
|
+
|| (normalized.startsWith('\'') && normalized.endsWith('\''))
|
|
3690
|
+
) {
|
|
3691
|
+
try {
|
|
3692
|
+
normalized = JSON.parse(normalized);
|
|
3693
|
+
} catch {
|
|
3694
|
+
normalized = normalized.slice(1, -1);
|
|
3695
|
+
}
|
|
3696
|
+
}
|
|
3697
|
+
return normalized;
|
|
3698
|
+
}
|
|
3699
|
+
|
|
3511
3700
|
function parseSimpleYamlConfig(text) {
|
|
3512
3701
|
const config = {};
|
|
3702
|
+
let currentObjectKey = '';
|
|
3703
|
+
|
|
3513
3704
|
for (const line of String(text || '').split(/\r?\n/)) {
|
|
3514
|
-
|
|
3515
|
-
|
|
3516
|
-
|
|
3517
|
-
if (
|
|
3518
|
-
|
|
3519
|
-
|
|
3520
|
-
|
|
3521
|
-
|
|
3522
|
-
|
|
3523
|
-
|
|
3524
|
-
|
|
3705
|
+
if (!line.trim() || line.trim().startsWith('#')) continue;
|
|
3706
|
+
|
|
3707
|
+
const topLevelMatch = line.match(/^([A-Za-z0-9_-]+)\s*:\s*(.*?)\s*$/);
|
|
3708
|
+
if (topLevelMatch && !line.startsWith(' ')) {
|
|
3709
|
+
const key = topLevelMatch[1];
|
|
3710
|
+
const rawValue = topLevelMatch[2];
|
|
3711
|
+
if (rawValue === '') {
|
|
3712
|
+
if (!config[key] || typeof config[key] !== 'object' || Array.isArray(config[key])) {
|
|
3713
|
+
config[key] = {};
|
|
3714
|
+
}
|
|
3715
|
+
currentObjectKey = key;
|
|
3716
|
+
} else {
|
|
3717
|
+
config[key] = parseYamlScalar(rawValue);
|
|
3718
|
+
currentObjectKey = '';
|
|
3719
|
+
}
|
|
3720
|
+
continue;
|
|
3721
|
+
}
|
|
3722
|
+
|
|
3723
|
+
if (currentObjectKey) {
|
|
3724
|
+
const nestedMatch = line.match(/^\s{2}([A-Za-z0-9_-]+)\s*:\s*(.*?)\s*$/);
|
|
3725
|
+
if (nestedMatch) {
|
|
3726
|
+
if (!config[currentObjectKey] || typeof config[currentObjectKey] !== 'object' || Array.isArray(config[currentObjectKey])) {
|
|
3727
|
+
config[currentObjectKey] = {};
|
|
3728
|
+
}
|
|
3729
|
+
config[currentObjectKey][nestedMatch[1]] = parseYamlScalar(nestedMatch[2]);
|
|
3730
|
+
continue;
|
|
3525
3731
|
}
|
|
3526
3732
|
}
|
|
3527
|
-
|
|
3733
|
+
|
|
3734
|
+
currentObjectKey = '';
|
|
3528
3735
|
}
|
|
3736
|
+
|
|
3529
3737
|
return config;
|
|
3530
3738
|
}
|
|
3531
3739
|
|
|
@@ -3533,31 +3741,71 @@ function serializeYamlScalar(value) {
|
|
|
3533
3741
|
return JSON.stringify(String(value ?? ''));
|
|
3534
3742
|
}
|
|
3535
3743
|
|
|
3536
|
-
function
|
|
3537
|
-
const
|
|
3538
|
-
const
|
|
3539
|
-
|
|
3744
|
+
function buildHermesModelConfigBlock(entries) {
|
|
3745
|
+
const ordered = {};
|
|
3746
|
+
for (const key of ['default', 'provider', 'base_url', 'api_mode']) {
|
|
3747
|
+
if (Object.prototype.hasOwnProperty.call(entries, key)) {
|
|
3748
|
+
ordered[key] = entries[key];
|
|
3749
|
+
}
|
|
3750
|
+
}
|
|
3751
|
+
for (const [key, value] of Object.entries(entries)) {
|
|
3752
|
+
if (!Object.prototype.hasOwnProperty.call(ordered, key)) {
|
|
3753
|
+
ordered[key] = value;
|
|
3754
|
+
}
|
|
3755
|
+
}
|
|
3540
3756
|
|
|
3541
|
-
|
|
3542
|
-
|
|
3543
|
-
if (
|
|
3544
|
-
|
|
3545
|
-
if (!Object.prototype.hasOwnProperty.call(entries, key)) continue;
|
|
3546
|
-
normalized[i] = `${key}: ${serializeYamlScalar(entries[key])}`;
|
|
3547
|
-
seen.add(key);
|
|
3757
|
+
const lines = ['model:'];
|
|
3758
|
+
for (const [key, value] of Object.entries(ordered)) {
|
|
3759
|
+
if (value === undefined || value === null || value === '') continue;
|
|
3760
|
+
lines.push(` ${key}: ${serializeYamlScalar(value)}`);
|
|
3548
3761
|
}
|
|
3762
|
+
return lines.join('\n');
|
|
3763
|
+
}
|
|
3549
3764
|
|
|
3550
|
-
|
|
3551
|
-
|
|
3552
|
-
|
|
3765
|
+
function upsertHermesModelConfig(text, modelEntries) {
|
|
3766
|
+
let normalized = String(text || '').replace(/\r\n/g, '\n');
|
|
3767
|
+
normalized = normalized.replace(/^(provider|base_url|api_mode):\s*.*\n?/gm, '');
|
|
3768
|
+
|
|
3769
|
+
const lines = normalized.split('\n');
|
|
3770
|
+
const output = [];
|
|
3771
|
+
let replaced = false;
|
|
3772
|
+
|
|
3773
|
+
for (let i = 0; i < lines.length; i += 1) {
|
|
3774
|
+
const line = lines[i];
|
|
3775
|
+
if (/^model:\s*(.*)$/.test(line)) {
|
|
3776
|
+
replaced = true;
|
|
3777
|
+
if (output.length > 0 && output[output.length - 1] !== '') {
|
|
3778
|
+
output.push('');
|
|
3779
|
+
}
|
|
3780
|
+
output.push(buildHermesModelConfigBlock(modelEntries));
|
|
3781
|
+
|
|
3782
|
+
i += 1;
|
|
3783
|
+
while (i < lines.length) {
|
|
3784
|
+
const nextLine = lines[i];
|
|
3785
|
+
if (/^\s{2,}/.test(nextLine)) {
|
|
3786
|
+
i += 1;
|
|
3787
|
+
continue;
|
|
3788
|
+
}
|
|
3789
|
+
if (nextLine === '') {
|
|
3790
|
+
break;
|
|
3791
|
+
}
|
|
3792
|
+
i -= 1;
|
|
3793
|
+
break;
|
|
3794
|
+
}
|
|
3795
|
+
continue;
|
|
3553
3796
|
}
|
|
3797
|
+
output.push(line);
|
|
3554
3798
|
}
|
|
3555
3799
|
|
|
3556
|
-
while (
|
|
3557
|
-
|
|
3800
|
+
while (output.length > 0 && output[output.length - 1] === '') {
|
|
3801
|
+
output.pop();
|
|
3802
|
+
}
|
|
3803
|
+
if (!replaced) {
|
|
3804
|
+
if (output.length > 0) output.push('');
|
|
3805
|
+
output.push(buildHermesModelConfigBlock(modelEntries));
|
|
3558
3806
|
}
|
|
3559
3807
|
|
|
3560
|
-
return
|
|
3808
|
+
return output.join('\n').replace(/\n{3,}/g, '\n\n').trim() + '\n';
|
|
3561
3809
|
}
|
|
3562
3810
|
|
|
3563
3811
|
function parseEnvFile(text) {
|
|
@@ -3620,6 +3868,32 @@ function readHermesYamlConfig(configPath = getHermesConfigPath()) {
|
|
|
3620
3868
|
};
|
|
3621
3869
|
}
|
|
3622
3870
|
|
|
3871
|
+
function readHermesCliConfig() {
|
|
3872
|
+
const yamlConfig = readHermesYamlConfig();
|
|
3873
|
+
const envPath = getHermesEnvPath();
|
|
3874
|
+
const envEntries = parseEnvFile(readTextIfExists(envPath));
|
|
3875
|
+
const modelConfig = yamlConfig.config?.model && typeof yamlConfig.config.model === 'object'
|
|
3876
|
+
? yamlConfig.config.model
|
|
3877
|
+
: {};
|
|
3878
|
+
const modelId = String(modelConfig.default || '').trim() || getDefaultClaudeModel().id;
|
|
3879
|
+
const provider = String(modelConfig.provider || '').trim().toLowerCase();
|
|
3880
|
+
const type = resolveHermesModelType(modelId, provider);
|
|
3881
|
+
const apiKey = type === 'codex'
|
|
3882
|
+
? (envEntries.OPENAI_API_KEY || process.env.OPENAI_API_KEY || process.env.OPENAI_AUTH_TOKEN || '')
|
|
3883
|
+
: (envEntries.ANTHROPIC_API_KEY || envEntries.ANTHROPIC_TOKEN || process.env.ANTHROPIC_API_KEY || process.env.ANTHROPIC_TOKEN || process.env.CLAUDE_CODE_OAUTH_TOKEN || '');
|
|
3884
|
+
|
|
3885
|
+
return {
|
|
3886
|
+
configPath: yamlConfig.configPath,
|
|
3887
|
+
envPath,
|
|
3888
|
+
configured: yamlConfig.configured || fs.existsSync(envPath),
|
|
3889
|
+
modelId,
|
|
3890
|
+
provider,
|
|
3891
|
+
type,
|
|
3892
|
+
baseUrl: String(modelConfig.base_url || '').trim(),
|
|
3893
|
+
apiKey
|
|
3894
|
+
};
|
|
3895
|
+
}
|
|
3896
|
+
|
|
3623
3897
|
function getOpencodeConfigPath() {
|
|
3624
3898
|
const home = os.homedir();
|
|
3625
3899
|
return process.platform === 'win32'
|
|
@@ -3638,11 +3912,20 @@ function getCodexCliPaths() {
|
|
|
3638
3912
|
function readClaudeCodeCliConfig() {
|
|
3639
3913
|
const settingsPath = getClaudeCodeSettingsPath();
|
|
3640
3914
|
const settings = readJsonIfExists(settingsPath) || {};
|
|
3915
|
+
const settingsEnv = settings.env && typeof settings.env === 'object' ? settings.env : {};
|
|
3916
|
+
const settingsApiKey = String(settingsEnv.ANTHROPIC_API_KEY || '').trim();
|
|
3917
|
+
const settingsLegacyAuthToken = String(settingsEnv.ANTHROPIC_AUTH_TOKEN || '').trim();
|
|
3918
|
+
const settingsBaseUrl = String(settingsEnv.ANTHROPIC_BASE_URL || settings.apiBaseUrl || '').trim();
|
|
3641
3919
|
return {
|
|
3642
3920
|
settingsPath,
|
|
3643
|
-
modelId: settings.model ||
|
|
3644
|
-
baseUrl:
|
|
3645
|
-
apiKey:
|
|
3921
|
+
modelId: settings.model || settingsEnv.ANTHROPIC_MODEL || process.env.ANTHROPIC_MODEL || getDefaultClaudeModel().id,
|
|
3922
|
+
baseUrl: settingsBaseUrl || process.env.ANTHROPIC_BASE_URL || '',
|
|
3923
|
+
apiKey: settingsApiKey || settingsLegacyAuthToken || process.env.ANTHROPIC_API_KEY || process.env.ANTHROPIC_AUTH_TOKEN || process.env.CLAUDE_API_KEY || '',
|
|
3924
|
+
settingsBaseUrl,
|
|
3925
|
+
settingsApiKey,
|
|
3926
|
+
settingsLegacyAuthToken,
|
|
3927
|
+
hasLegacyAuthTokenInSettings: !!settingsLegacyAuthToken,
|
|
3928
|
+
usesLegacyAuthTokenOnly: !settingsApiKey && !!settingsLegacyAuthToken,
|
|
3646
3929
|
configured: fs.existsSync(settingsPath)
|
|
3647
3930
|
};
|
|
3648
3931
|
}
|
|
@@ -3697,6 +3980,23 @@ function summarizeCliTestOutput(text) {
|
|
|
3697
3980
|
return lines[0].slice(0, 160);
|
|
3698
3981
|
}
|
|
3699
3982
|
|
|
3983
|
+
function extractHermesCliReply(text) {
|
|
3984
|
+
const lines = cleanCliTestOutput(text)
|
|
3985
|
+
.split('\n')
|
|
3986
|
+
.map(line => line.trim())
|
|
3987
|
+
.filter(Boolean)
|
|
3988
|
+
.filter(line => !/^session_id:/i.test(line))
|
|
3989
|
+
.filter(line => !/^resume this session/i.test(line))
|
|
3990
|
+
.filter(line => !/^session:/i.test(line))
|
|
3991
|
+
.filter(line => !/^duration:/i.test(line))
|
|
3992
|
+
.filter(line => !/^messages:/i.test(line))
|
|
3993
|
+
.filter(line => !/^[╭╰│─]+/.test(line));
|
|
3994
|
+
|
|
3995
|
+
const okLine = lines.find(line => /^OK$/i.test(line));
|
|
3996
|
+
if (okLine) return okLine;
|
|
3997
|
+
return lines[0] || '';
|
|
3998
|
+
}
|
|
3999
|
+
|
|
3700
4000
|
function buildReadableAgentError(text, fallback = '') {
|
|
3701
4001
|
const cleaned = cleanCliTestOutput(text);
|
|
3702
4002
|
if (!cleaned) return fallback;
|
|
@@ -3712,6 +4012,11 @@ function looksLikeCliTestError(text) {
|
|
|
3712
4012
|
'all models failed',
|
|
3713
4013
|
'auth_permanent',
|
|
3714
4014
|
'auth issue',
|
|
4015
|
+
'authentication_error',
|
|
4016
|
+
'failed to authenticate',
|
|
4017
|
+
'invalid bearer token',
|
|
4018
|
+
'invalid api key',
|
|
4019
|
+
'fix external api key',
|
|
3715
4020
|
'unauthorized',
|
|
3716
4021
|
'forbidden',
|
|
3717
4022
|
'missing environment variable',
|
|
@@ -3739,17 +4044,28 @@ function testClaudeCodeCliConnection() {
|
|
|
3739
4044
|
|
|
3740
4045
|
const config = readClaudeCodeCliConfig();
|
|
3741
4046
|
if (!config.configured) return { name: 'Claude Code CLI', status: 'skipped', detail: '未检测到 ~/.claude/settings.json' };
|
|
3742
|
-
if (
|
|
4047
|
+
if (config.usesLegacyAuthTokenOnly) {
|
|
4048
|
+
return {
|
|
4049
|
+
name: 'Claude Code CLI',
|
|
4050
|
+
status: 'failed',
|
|
4051
|
+
detail: '检测到旧版 Claude Code 配置字段 ANTHROPIC_AUTH_TOKEN,请重新运行 yymaxapi 以改写为 ANTHROPIC_API_KEY'
|
|
4052
|
+
};
|
|
4053
|
+
}
|
|
4054
|
+
if (!config.settingsBaseUrl || !config.settingsApiKey) {
|
|
4055
|
+
return { name: 'Claude Code CLI', status: 'failed', detail: 'Claude Code 配置缺少 settings.json 中的 Base URL 或 API Key' };
|
|
4056
|
+
}
|
|
3743
4057
|
|
|
3744
4058
|
const env = {
|
|
3745
4059
|
...process.env,
|
|
3746
4060
|
PATH: extendPathEnv(null),
|
|
3747
|
-
ANTHROPIC_BASE_URL: config.baseUrl,
|
|
3748
|
-
ANTHROPIC_AUTH_TOKEN: config.apiKey,
|
|
3749
|
-
ANTHROPIC_MODEL: config.modelId,
|
|
3750
4061
|
NODE_TLS_REJECT_UNAUTHORIZED: '0',
|
|
3751
4062
|
NODE_NO_WARNINGS: '1'
|
|
3752
4063
|
};
|
|
4064
|
+
delete env.ANTHROPIC_BASE_URL;
|
|
4065
|
+
delete env.ANTHROPIC_API_KEY;
|
|
4066
|
+
delete env.ANTHROPIC_AUTH_TOKEN;
|
|
4067
|
+
delete env.ANTHROPIC_MODEL;
|
|
4068
|
+
delete env.CLAUDE_API_KEY;
|
|
3753
4069
|
|
|
3754
4070
|
return runCliTestCandidates('Claude Code CLI', [
|
|
3755
4071
|
`${shellQuote(cliBinary)} -p --model ${shellQuote(config.modelId)} --output-format text ${shellQuote('请只回复 OK')}`,
|
|
@@ -3801,14 +4117,62 @@ function testCodexCliConnection() {
|
|
|
3801
4117
|
], env);
|
|
3802
4118
|
}
|
|
3803
4119
|
|
|
4120
|
+
function testHermesCliConnection() {
|
|
4121
|
+
const cliBinary = resolveCommandBinary('hermes');
|
|
4122
|
+
if (!cliBinary) return { name: 'Hermes CLI', status: 'skipped', detail: '未安装 hermes 命令' };
|
|
4123
|
+
|
|
4124
|
+
const config = readHermesCliConfig();
|
|
4125
|
+
if (!config.configured) return { name: 'Hermes CLI', status: 'skipped', detail: '未检测到 ~/.hermes/config.yaml 或 ~/.hermes/.env' };
|
|
4126
|
+
if (!config.apiKey) return { name: 'Hermes CLI', status: 'failed', detail: 'Hermes 配置缺少 API Key' };
|
|
4127
|
+
|
|
4128
|
+
const env = {
|
|
4129
|
+
...process.env,
|
|
4130
|
+
PATH: extendPathEnv(null),
|
|
4131
|
+
NODE_TLS_REJECT_UNAUTHORIZED: '0',
|
|
4132
|
+
NODE_NO_WARNINGS: '1'
|
|
4133
|
+
};
|
|
4134
|
+
|
|
4135
|
+
if (config.type === 'codex') {
|
|
4136
|
+
env.OPENAI_API_KEY = config.apiKey;
|
|
4137
|
+
delete env.ANTHROPIC_API_KEY;
|
|
4138
|
+
delete env.ANTHROPIC_TOKEN;
|
|
4139
|
+
delete env.CLAUDE_CODE_OAUTH_TOKEN;
|
|
4140
|
+
} else {
|
|
4141
|
+
env.ANTHROPIC_API_KEY = config.apiKey;
|
|
4142
|
+
if (config.baseUrl) env.ANTHROPIC_BASE_URL = config.baseUrl;
|
|
4143
|
+
delete env.ANTHROPIC_TOKEN;
|
|
4144
|
+
delete env.CLAUDE_CODE_OAUTH_TOKEN;
|
|
4145
|
+
}
|
|
4146
|
+
|
|
4147
|
+
const providerArg = config.provider === 'anthropic' ? ' --provider anthropic' : '';
|
|
4148
|
+
const result = safeExec(`${shellQuote(cliBinary)} chat -Q -q ${shellQuote('请只回复 OK')}${providerArg}`, {
|
|
4149
|
+
timeout: 120000,
|
|
4150
|
+
env,
|
|
4151
|
+
maxBuffer: 1024 * 1024
|
|
4152
|
+
});
|
|
4153
|
+
const combined = cleanCliTestOutput(`${result.output || ''}\n${result.stdout || ''}\n${result.stderr || ''}`);
|
|
4154
|
+
const reply = extractHermesCliReply(combined);
|
|
4155
|
+
|
|
4156
|
+
if (result.ok && /^OK$/i.test(reply) && !looksLikeCliTestError(combined)) {
|
|
4157
|
+
return { name: 'Hermes CLI', status: 'success', detail: 'OK' };
|
|
4158
|
+
}
|
|
4159
|
+
|
|
4160
|
+
return {
|
|
4161
|
+
name: 'Hermes CLI',
|
|
4162
|
+
status: 'failed',
|
|
4163
|
+
detail: reply || summarizeCliTestOutput(combined) || result.error || '命令执行失败'
|
|
4164
|
+
};
|
|
4165
|
+
}
|
|
4166
|
+
|
|
3804
4167
|
async function testAdditionalCliConnections(args = {}, options = {}) {
|
|
3805
4168
|
if (args['no-app-test'] || args.noAppTest) return;
|
|
3806
4169
|
|
|
3807
|
-
const requested = new Set((options.only || ['claude', 'opencode', 'codex']).map(item => String(item)));
|
|
4170
|
+
const requested = new Set((options.only || ['claude', 'opencode', 'codex', 'hermes']).map(item => String(item)));
|
|
3808
4171
|
const results = [];
|
|
3809
4172
|
if (requested.has('claude')) results.push(testClaudeCodeCliConnection());
|
|
3810
4173
|
if (requested.has('opencode')) results.push(testOpencodeCliConnection());
|
|
3811
4174
|
if (requested.has('codex')) results.push(testCodexCliConnection());
|
|
4175
|
+
if (requested.has('hermes')) results.push(testHermesCliConnection());
|
|
3812
4176
|
if (results.length === 0) return;
|
|
3813
4177
|
|
|
3814
4178
|
console.log(chalk.cyan('\n附加测试: 其他 CLI 连接...'));
|
|
@@ -4332,6 +4696,27 @@ function trimClaudeMessagesSuffix(baseUrl) {
|
|
|
4332
4696
|
return trimmed;
|
|
4333
4697
|
}
|
|
4334
4698
|
|
|
4699
|
+
function trimOpenAiEndpointSuffix(baseUrl) {
|
|
4700
|
+
const trimmed = baseUrl.trim();
|
|
4701
|
+
const suffixes = [
|
|
4702
|
+
'/v1/responses',
|
|
4703
|
+
'/responses',
|
|
4704
|
+
'/v1/chat/completions',
|
|
4705
|
+
'/chat/completions',
|
|
4706
|
+
'/v1/models',
|
|
4707
|
+
'/models',
|
|
4708
|
+
'/v1'
|
|
4709
|
+
];
|
|
4710
|
+
|
|
4711
|
+
for (const suffix of suffixes) {
|
|
4712
|
+
if (trimmed.endsWith(suffix)) {
|
|
4713
|
+
return trimmed.slice(0, -suffix.length);
|
|
4714
|
+
}
|
|
4715
|
+
}
|
|
4716
|
+
|
|
4717
|
+
return trimmed;
|
|
4718
|
+
}
|
|
4719
|
+
|
|
4335
4720
|
async function quickSetup(paths, args = {}) {
|
|
4336
4721
|
console.log(chalk.cyan.bold('\n🚀 快速配置向导\n'));
|
|
4337
4722
|
|
|
@@ -4474,47 +4859,15 @@ async function quickSetup(paths, args = {}) {
|
|
|
4474
4859
|
}
|
|
4475
4860
|
|
|
4476
4861
|
async function presetClaude(paths, args = {}) {
|
|
4477
|
-
console.log(chalk.cyan.bold('\n🚀 Claude
|
|
4862
|
+
console.log(chalk.cyan.bold('\n🚀 Claude 快速配置(默认节点直连)\n'));
|
|
4478
4863
|
|
|
4479
4864
|
const apiConfig = API_CONFIG.claude;
|
|
4480
4865
|
const providerPrefix = (args['provider-prefix'] || args.prefix || apiConfig.providerName).toString().trim() || apiConfig.providerName;
|
|
4481
|
-
|
|
4482
|
-
|
|
4483
|
-
|
|
4484
|
-
|
|
4485
|
-
if (
|
|
4486
|
-
console.log(chalk.cyan('📡 开始测速 Claude 节点...\n'));
|
|
4487
|
-
const speedResult = await testAllEndpoints(ENDPOINTS, { autoFallback: true });
|
|
4488
|
-
|
|
4489
|
-
const sorted = speedResult.ranked || [];
|
|
4490
|
-
if (sorted.length > 0) {
|
|
4491
|
-
const defaultEp = ENDPOINTS[0];
|
|
4492
|
-
const { selectedIndex } = await inquirer.prompt([{
|
|
4493
|
-
type: 'list',
|
|
4494
|
-
name: 'selectedIndex',
|
|
4495
|
-
message: '选择节点:',
|
|
4496
|
-
choices: [
|
|
4497
|
-
{ name: `* 使用默认节点 (${defaultEp.name})`, value: -1 },
|
|
4498
|
-
new inquirer.Separator(' ---- 或按测速结果选择 ----'),
|
|
4499
|
-
...sorted.map((e, i) => ({
|
|
4500
|
-
name: `${e.name} - ${e.latency}ms (评分:${e.score})`,
|
|
4501
|
-
value: i
|
|
4502
|
-
}))
|
|
4503
|
-
]
|
|
4504
|
-
}]);
|
|
4505
|
-
selectedEndpoint = selectedIndex === -1 ? defaultEp : sorted[selectedIndex];
|
|
4506
|
-
if (speedResult.usedFallback) {
|
|
4507
|
-
console.log(chalk.yellow(`\n⚠ 当前使用备用节点\n`));
|
|
4508
|
-
}
|
|
4509
|
-
} else {
|
|
4510
|
-
console.log(chalk.red('\n⚠️ 所有节点(含备用)均不可达'));
|
|
4511
|
-
const { proceed } = await inquirer.prompt([{
|
|
4512
|
-
type: 'confirm', name: 'proceed',
|
|
4513
|
-
message: '仍要写入默认节点配置吗?', default: false
|
|
4514
|
-
}]);
|
|
4515
|
-
if (!proceed) { console.log(chalk.gray('已取消')); return; }
|
|
4516
|
-
}
|
|
4517
|
-
}
|
|
4866
|
+
const selectedEndpoint = await resolveEndpointSelection(args, {
|
|
4867
|
+
speedIntro: '📡 开始测速 Claude 节点...\n',
|
|
4868
|
+
proceedMessage: '仍要写入默认节点配置吗?'
|
|
4869
|
+
});
|
|
4870
|
+
if (!selectedEndpoint) return;
|
|
4518
4871
|
|
|
4519
4872
|
const config = ensureConfigStructure(readConfig(paths.openclawConfig) || {});
|
|
4520
4873
|
const repairResult = applyConfigRepairsWithSync(config, paths);
|
|
@@ -4526,6 +4879,8 @@ async function presetClaude(paths, args = {}) {
|
|
|
4526
4879
|
|
|
4527
4880
|
const apiKeyEnvFallbacks = [
|
|
4528
4881
|
'OPENCLAW_CLAUDE_KEY',
|
|
4882
|
+
'ANTHROPIC_API_KEY',
|
|
4883
|
+
'ANTHROPIC_AUTH_TOKEN',
|
|
4529
4884
|
'CLAUDE_API_KEY',
|
|
4530
4885
|
'OPENCLAW_API_KEY'
|
|
4531
4886
|
];
|
|
@@ -4657,48 +5012,16 @@ async function presetClaude(paths, args = {}) {
|
|
|
4657
5012
|
}
|
|
4658
5013
|
|
|
4659
5014
|
async function presetCodex(paths, args = {}) {
|
|
4660
|
-
console.log(chalk.cyan.bold('\n🚀 Codex
|
|
5015
|
+
console.log(chalk.cyan.bold('\n🚀 Codex 快速配置(默认节点直连)\n'));
|
|
4661
5016
|
|
|
4662
5017
|
const apiConfig = API_CONFIG.codex;
|
|
4663
5018
|
const providerPrefix = (args['provider-prefix'] || args.prefix || apiConfig.providerName).toString().trim() || apiConfig.providerName;
|
|
4664
5019
|
const providerName = (args['provider-name'] || args.provider || providerPrefix).toString().trim() || apiConfig.providerName;
|
|
4665
|
-
|
|
4666
|
-
|
|
4667
|
-
|
|
4668
|
-
|
|
4669
|
-
if (
|
|
4670
|
-
console.log(chalk.cyan('📡 开始测速 Codex 节点...\n'));
|
|
4671
|
-
const speedResult = await testAllEndpoints(ENDPOINTS, { autoFallback: true });
|
|
4672
|
-
|
|
4673
|
-
const sorted = speedResult.ranked || [];
|
|
4674
|
-
if (sorted.length > 0) {
|
|
4675
|
-
const defaultEp = ENDPOINTS[0];
|
|
4676
|
-
const { selectedIndex } = await inquirer.prompt([{
|
|
4677
|
-
type: 'list',
|
|
4678
|
-
name: 'selectedIndex',
|
|
4679
|
-
message: '选择节点:',
|
|
4680
|
-
choices: [
|
|
4681
|
-
{ name: `* 使用默认节点 (${defaultEp.name})`, value: -1 },
|
|
4682
|
-
new inquirer.Separator(' ---- 或按测速结果选择 ----'),
|
|
4683
|
-
...sorted.map((e, i) => ({
|
|
4684
|
-
name: `${e.name} - ${e.latency}ms (评分:${e.score})`,
|
|
4685
|
-
value: i
|
|
4686
|
-
}))
|
|
4687
|
-
]
|
|
4688
|
-
}]);
|
|
4689
|
-
selectedEndpoint = selectedIndex === -1 ? defaultEp : sorted[selectedIndex];
|
|
4690
|
-
if (speedResult.usedFallback) {
|
|
4691
|
-
console.log(chalk.yellow(`\n⚠ 当前使用备用节点\n`));
|
|
4692
|
-
}
|
|
4693
|
-
} else {
|
|
4694
|
-
console.log(chalk.red('\n⚠️ 所有节点(含备用)均不可达'));
|
|
4695
|
-
const { proceed } = await inquirer.prompt([{
|
|
4696
|
-
type: 'confirm', name: 'proceed',
|
|
4697
|
-
message: '仍要写入默认节点配置吗?', default: false
|
|
4698
|
-
}]);
|
|
4699
|
-
if (!proceed) { console.log(chalk.gray('已取消')); return; }
|
|
4700
|
-
}
|
|
4701
|
-
}
|
|
5020
|
+
const selectedEndpoint = await resolveEndpointSelection(args, {
|
|
5021
|
+
speedIntro: '📡 开始测速 Codex 节点...\n',
|
|
5022
|
+
proceedMessage: '仍要写入默认节点配置吗?'
|
|
5023
|
+
});
|
|
5024
|
+
if (!selectedEndpoint) return;
|
|
4702
5025
|
|
|
4703
5026
|
const config = ensureConfigStructure(readConfig(paths.openclawConfig) || {});
|
|
4704
5027
|
const repairResult = applyConfigRepairsWithSync(config, paths);
|
|
@@ -4852,6 +5175,8 @@ async function autoActivate(paths, args = {}) {
|
|
|
4852
5175
|
const apiKeyEnvFallbacks = [
|
|
4853
5176
|
'OPENCLAW_CLAUDE_KEY',
|
|
4854
5177
|
'OPENCLAW_CODEX_KEY',
|
|
5178
|
+
'ANTHROPIC_API_KEY',
|
|
5179
|
+
'ANTHROPIC_AUTH_TOKEN',
|
|
4855
5180
|
'CLAUDE_API_KEY',
|
|
4856
5181
|
'OPENAI_API_KEY',
|
|
4857
5182
|
'OPENCLAW_API_KEY'
|
|
@@ -5062,40 +5387,10 @@ async function autoActivate(paths, args = {}) {
|
|
|
5062
5387
|
async function activateClaudeCode(paths, args = {}) {
|
|
5063
5388
|
console.log(chalk.cyan.bold('\n🔧 配置 Claude Code CLI\n'));
|
|
5064
5389
|
const selectedModel = await promptClaudeModelSelection(args);
|
|
5065
|
-
|
|
5066
|
-
|
|
5067
|
-
|
|
5068
|
-
|
|
5069
|
-
|
|
5070
|
-
if (shouldTest) {
|
|
5071
|
-
console.log(chalk.cyan('📡 开始测速节点...\n'));
|
|
5072
|
-
const speedResult = await testAllEndpoints(ENDPOINTS, { autoFallback: true });
|
|
5073
|
-
const sorted = speedResult.ranked || [];
|
|
5074
|
-
if (sorted.length > 0) {
|
|
5075
|
-
const defaultEp = ENDPOINTS[0];
|
|
5076
|
-
const { selectedIndex } = await inquirer.prompt([{
|
|
5077
|
-
type: 'list',
|
|
5078
|
-
name: 'selectedIndex',
|
|
5079
|
-
message: '选择节点:',
|
|
5080
|
-
choices: [
|
|
5081
|
-
{ name: `* 使用默认节点 (${defaultEp.name})`, value: -1 },
|
|
5082
|
-
new inquirer.Separator(' ---- 或按测速结果选择 ----'),
|
|
5083
|
-
...sorted.map((e, i) => ({
|
|
5084
|
-
name: `${e.name} - ${e.latency}ms (评分:${e.score})`,
|
|
5085
|
-
value: i
|
|
5086
|
-
}))
|
|
5087
|
-
]
|
|
5088
|
-
}]);
|
|
5089
|
-
selectedEndpoint = selectedIndex === -1 ? defaultEp : sorted[selectedIndex];
|
|
5090
|
-
} else {
|
|
5091
|
-
console.log(chalk.red('\n⚠️ 所有节点均不可达'));
|
|
5092
|
-
const { proceed } = await inquirer.prompt([{
|
|
5093
|
-
type: 'confirm', name: 'proceed',
|
|
5094
|
-
message: '仍要使用默认节点配置吗?', default: false
|
|
5095
|
-
}]);
|
|
5096
|
-
if (!proceed) { console.log(chalk.gray('已取消')); return; }
|
|
5097
|
-
}
|
|
5098
|
-
}
|
|
5390
|
+
const selectedEndpoint = await resolveEndpointSelection(args, {
|
|
5391
|
+
proceedMessage: '仍要使用默认节点配置吗?'
|
|
5392
|
+
});
|
|
5393
|
+
if (!selectedEndpoint) return;
|
|
5099
5394
|
|
|
5100
5395
|
// ---- API Key ----
|
|
5101
5396
|
const directKey = (args['api-key'] || args.apiKey || args.key || '').toString().trim();
|
|
@@ -5103,7 +5398,7 @@ async function activateClaudeCode(paths, args = {}) {
|
|
|
5103
5398
|
if (directKey) {
|
|
5104
5399
|
apiKey = directKey;
|
|
5105
5400
|
} else {
|
|
5106
|
-
const envKey = process.env.ANTHROPIC_AUTH_TOKEN || process.env.CLAUDE_API_KEY || '';
|
|
5401
|
+
const envKey = process.env.ANTHROPIC_API_KEY || process.env.ANTHROPIC_AUTH_TOKEN || process.env.CLAUDE_API_KEY || '';
|
|
5107
5402
|
apiKey = await promptApiKey('请输入 API Key:', envKey);
|
|
5108
5403
|
}
|
|
5109
5404
|
if (!apiKey) { console.log(chalk.gray('已取消')); return; }
|
|
@@ -5131,7 +5426,7 @@ async function activateClaudeCode(paths, args = {}) {
|
|
|
5131
5426
|
console.log(chalk.gray(' API Key: 已设置'));
|
|
5132
5427
|
console.log(chalk.gray('\n 已写入:'));
|
|
5133
5428
|
console.log(chalk.gray(' • ~/.claude/settings.json'));
|
|
5134
|
-
console.log(chalk.gray(' • 最小字段: apiBaseUrl + env.
|
|
5429
|
+
console.log(chalk.gray(' • 最小字段: apiBaseUrl + env.ANTHROPIC_API_KEY + env.ANTHROPIC_BASE_URL'));
|
|
5135
5430
|
console.log(chalk.yellow('\n 提示: 如果 Claude Code 已在运行,重新打开一个会话即可读取新配置'));
|
|
5136
5431
|
|
|
5137
5432
|
if (await confirmImmediateTest(args, '是否立即测试 Claude Code CLI 连接?')) {
|
|
@@ -5143,40 +5438,10 @@ async function activateClaudeCode(paths, args = {}) {
|
|
|
5143
5438
|
async function activateOpencode(paths, args = {}) {
|
|
5144
5439
|
console.log(chalk.cyan.bold('\n🔧 配置 Opencode\n'));
|
|
5145
5440
|
const defaultModel = await promptOpencodeDefaultModelSelection(args);
|
|
5146
|
-
|
|
5147
|
-
|
|
5148
|
-
|
|
5149
|
-
|
|
5150
|
-
|
|
5151
|
-
if (shouldTest) {
|
|
5152
|
-
console.log(chalk.cyan('📡 开始测速节点...\n'));
|
|
5153
|
-
const speedResult = await testAllEndpoints(ENDPOINTS, { autoFallback: true });
|
|
5154
|
-
const sorted = speedResult.ranked || [];
|
|
5155
|
-
if (sorted.length > 0) {
|
|
5156
|
-
const defaultEp = ENDPOINTS[0];
|
|
5157
|
-
const { selectedIndex } = await inquirer.prompt([{
|
|
5158
|
-
type: 'list',
|
|
5159
|
-
name: 'selectedIndex',
|
|
5160
|
-
message: '选择节点:',
|
|
5161
|
-
choices: [
|
|
5162
|
-
{ name: `* 使用默认节点 (${defaultEp.name})`, value: -1 },
|
|
5163
|
-
new inquirer.Separator(' ---- 或按测速结果选择 ----'),
|
|
5164
|
-
...sorted.map((e, i) => ({
|
|
5165
|
-
name: `${e.name} - ${e.latency}ms (评分:${e.score})`,
|
|
5166
|
-
value: i
|
|
5167
|
-
}))
|
|
5168
|
-
]
|
|
5169
|
-
}]);
|
|
5170
|
-
selectedEndpoint = selectedIndex === -1 ? defaultEp : sorted[selectedIndex];
|
|
5171
|
-
} else {
|
|
5172
|
-
console.log(chalk.red('\n⚠️ 所有节点均不可达'));
|
|
5173
|
-
const { proceed } = await inquirer.prompt([{
|
|
5174
|
-
type: 'confirm', name: 'proceed',
|
|
5175
|
-
message: '仍要使用默认节点配置吗?', default: false
|
|
5176
|
-
}]);
|
|
5177
|
-
if (!proceed) { console.log(chalk.gray('已取消')); return; }
|
|
5178
|
-
}
|
|
5179
|
-
}
|
|
5441
|
+
const selectedEndpoint = await resolveEndpointSelection(args, {
|
|
5442
|
+
proceedMessage: '仍要使用默认节点配置吗?'
|
|
5443
|
+
});
|
|
5444
|
+
if (!selectedEndpoint) return;
|
|
5180
5445
|
|
|
5181
5446
|
// ---- API Key ----
|
|
5182
5447
|
const directKey = (args['api-key'] || args.apiKey || args.key || '').toString().trim();
|
|
@@ -5184,7 +5449,7 @@ async function activateOpencode(paths, args = {}) {
|
|
|
5184
5449
|
if (directKey) {
|
|
5185
5450
|
apiKey = directKey;
|
|
5186
5451
|
} else {
|
|
5187
|
-
const envKey = process.env.ANTHROPIC_AUTH_TOKEN || process.env.CLAUDE_API_KEY || '';
|
|
5452
|
+
const envKey = process.env.ANTHROPIC_API_KEY || process.env.ANTHROPIC_AUTH_TOKEN || process.env.CLAUDE_API_KEY || '';
|
|
5188
5453
|
apiKey = await promptApiKey('请输入 API Key:', envKey);
|
|
5189
5454
|
}
|
|
5190
5455
|
if (!apiKey) { console.log(chalk.gray('已取消')); return; }
|
|
@@ -5240,39 +5505,10 @@ async function activateCodex(paths, args = {}) {
|
|
|
5240
5505
|
}
|
|
5241
5506
|
const modelConfig = CODEX_MODELS.find(m => m.id === modelId) || { id: modelId, name: modelId };
|
|
5242
5507
|
|
|
5243
|
-
|
|
5244
|
-
|
|
5245
|
-
|
|
5246
|
-
|
|
5247
|
-
if (shouldTest) {
|
|
5248
|
-
console.log(chalk.cyan('📡 开始测速节点...\n'));
|
|
5249
|
-
const speedResult = await testAllEndpoints(ENDPOINTS, { autoFallback: true });
|
|
5250
|
-
const sorted = speedResult.ranked || [];
|
|
5251
|
-
if (sorted.length > 0) {
|
|
5252
|
-
const defaultEp = ENDPOINTS[0];
|
|
5253
|
-
const { selectedIndex } = await inquirer.prompt([{
|
|
5254
|
-
type: 'list',
|
|
5255
|
-
name: 'selectedIndex',
|
|
5256
|
-
message: '选择节点:',
|
|
5257
|
-
choices: [
|
|
5258
|
-
{ name: `* 使用默认节点 (${defaultEp.name})`, value: -1 },
|
|
5259
|
-
new inquirer.Separator(' ---- 或按测速结果选择 ----'),
|
|
5260
|
-
...sorted.map((e, i) => ({
|
|
5261
|
-
name: `${e.name} - ${e.latency}ms (评分:${e.score})`,
|
|
5262
|
-
value: i
|
|
5263
|
-
}))
|
|
5264
|
-
]
|
|
5265
|
-
}]);
|
|
5266
|
-
selectedEndpoint = selectedIndex === -1 ? defaultEp : sorted[selectedIndex];
|
|
5267
|
-
} else {
|
|
5268
|
-
console.log(chalk.red('\n⚠️ 所有节点均不可达'));
|
|
5269
|
-
const { proceed } = await inquirer.prompt([{
|
|
5270
|
-
type: 'confirm', name: 'proceed',
|
|
5271
|
-
message: '仍要使用默认节点配置吗?', default: false
|
|
5272
|
-
}]);
|
|
5273
|
-
if (!proceed) { console.log(chalk.gray('已取消')); return; }
|
|
5274
|
-
}
|
|
5275
|
-
}
|
|
5508
|
+
const selectedEndpoint = await resolveEndpointSelection(args, {
|
|
5509
|
+
proceedMessage: '仍要使用默认节点配置吗?'
|
|
5510
|
+
});
|
|
5511
|
+
if (!selectedEndpoint) return;
|
|
5276
5512
|
|
|
5277
5513
|
// ---- API Key ----
|
|
5278
5514
|
const directKey = (args['api-key'] || args.apiKey || args.key || '').toString().trim();
|
|
@@ -5319,47 +5555,22 @@ async function activateCodex(paths, args = {}) {
|
|
|
5319
5555
|
// ============ 单独配置 Hermes ============
|
|
5320
5556
|
async function activateHermes(paths, args = {}) {
|
|
5321
5557
|
console.log(chalk.cyan.bold('\n🔧 配置 Hermes\n'));
|
|
5322
|
-
const
|
|
5323
|
-
|
|
5324
|
-
const
|
|
5325
|
-
|
|
5326
|
-
|
|
5327
|
-
|
|
5328
|
-
|
|
5329
|
-
const speedResult = await testAllEndpoints(ENDPOINTS, { autoFallback: true });
|
|
5330
|
-
const sorted = speedResult.ranked || [];
|
|
5331
|
-
if (sorted.length > 0) {
|
|
5332
|
-
const defaultEp = ENDPOINTS[0];
|
|
5333
|
-
const { selectedIndex } = await inquirer.prompt([{
|
|
5334
|
-
type: 'list',
|
|
5335
|
-
name: 'selectedIndex',
|
|
5336
|
-
message: '选择节点:',
|
|
5337
|
-
choices: [
|
|
5338
|
-
{ name: `* 使用默认节点 (${defaultEp.name})`, value: -1 },
|
|
5339
|
-
new inquirer.Separator(' ---- 或按测速结果选择 ----'),
|
|
5340
|
-
...sorted.map((e, i) => ({
|
|
5341
|
-
name: `${e.name} - ${e.latency}ms (评分:${e.score})`,
|
|
5342
|
-
value: i
|
|
5343
|
-
}))
|
|
5344
|
-
]
|
|
5345
|
-
}]);
|
|
5346
|
-
selectedEndpoint = selectedIndex === -1 ? defaultEp : sorted[selectedIndex];
|
|
5347
|
-
} else {
|
|
5348
|
-
console.log(chalk.red('\n⚠️ 所有节点均不可达'));
|
|
5349
|
-
const { proceed } = await inquirer.prompt([{
|
|
5350
|
-
type: 'confirm', name: 'proceed',
|
|
5351
|
-
message: '仍要使用默认节点配置吗?', default: false
|
|
5352
|
-
}]);
|
|
5353
|
-
if (!proceed) { console.log(chalk.gray('已取消')); return; }
|
|
5354
|
-
}
|
|
5355
|
-
}
|
|
5558
|
+
const selection = await promptHermesModelSelection(args, '选择 Hermes 默认模型:');
|
|
5559
|
+
const selectedModel = selection.model;
|
|
5560
|
+
const selectedType = selection.type;
|
|
5561
|
+
const selectedEndpoint = await resolveEndpointSelection(args, {
|
|
5562
|
+
proceedMessage: '仍要使用默认节点配置吗?'
|
|
5563
|
+
});
|
|
5564
|
+
if (!selectedEndpoint) return;
|
|
5356
5565
|
|
|
5357
5566
|
const directKey = (args['api-key'] || args.apiKey || args.key || '').toString().trim();
|
|
5358
5567
|
let apiKey;
|
|
5359
5568
|
if (directKey) {
|
|
5360
5569
|
apiKey = directKey;
|
|
5361
5570
|
} else {
|
|
5362
|
-
const envKey =
|
|
5571
|
+
const envKey = selectedType === 'codex'
|
|
5572
|
+
? (process.env.OPENAI_API_KEY || process.env.OPENAI_AUTH_TOKEN || '')
|
|
5573
|
+
: (process.env.ANTHROPIC_API_KEY || process.env.ANTHROPIC_AUTH_TOKEN || process.env.CLAUDE_API_KEY || '');
|
|
5363
5574
|
apiKey = await promptApiKey('请输入 API Key:', envKey);
|
|
5364
5575
|
}
|
|
5365
5576
|
if (!apiKey) { console.log(chalk.gray('已取消')); return; }
|
|
@@ -5374,15 +5585,17 @@ async function activateHermes(paths, args = {}) {
|
|
|
5374
5585
|
if (!continueAnyway) { console.log(chalk.gray('已取消')); return; }
|
|
5375
5586
|
}
|
|
5376
5587
|
|
|
5377
|
-
const
|
|
5588
|
+
const typeLabel = selectedType === 'codex' ? 'GPT' : 'Claude';
|
|
5589
|
+
const hermesBaseUrl = buildFullUrl(selectedEndpoint.url, selectedType === 'codex' ? 'codex' : 'claude');
|
|
5378
5590
|
const writeSpinner = ora({ text: '正在写入 Hermes 配置...', spinner: 'dots' }).start();
|
|
5379
|
-
const hermesPaths = writeHermesConfig(
|
|
5591
|
+
const hermesPaths = writeHermesConfig(hermesBaseUrl, apiKey, selectedModel.id, { type: selectedType });
|
|
5380
5592
|
writeSpinner.succeed('Hermes 配置写入完成');
|
|
5381
5593
|
|
|
5382
5594
|
console.log(chalk.green('\n✅ Hermes 配置完成!'));
|
|
5383
5595
|
console.log(chalk.cyan(` Base URL: ${hermesPaths.baseUrl}`));
|
|
5384
5596
|
console.log(chalk.gray(` 模型: ${selectedModel.name} (${selectedModel.id})`));
|
|
5385
|
-
console.log(chalk.gray(
|
|
5597
|
+
console.log(chalk.gray(` 协议: ${typeLabel}`));
|
|
5598
|
+
console.log(chalk.gray(` Provider: ${hermesPaths.provider}`));
|
|
5386
5599
|
console.log(chalk.gray(' API Key: 已设置'));
|
|
5387
5600
|
console.log(chalk.gray('\n 已写入:'));
|
|
5388
5601
|
console.log(chalk.gray(` • ${hermesPaths.configPath}`));
|
|
@@ -5390,8 +5603,12 @@ async function activateHermes(paths, args = {}) {
|
|
|
5390
5603
|
if (hermesPaths.wslConfigPath && hermesPaths.wslEnvPath) {
|
|
5391
5604
|
console.log(chalk.gray(' • 已额外同步到 WSL ~/.hermes'));
|
|
5392
5605
|
}
|
|
5393
|
-
console.log(chalk.yellow('\n 提示: Hermes
|
|
5394
|
-
console.log(chalk.yellow(' 建议:
|
|
5606
|
+
console.log(chalk.yellow('\n 提示: Hermes doctor 当前会固定检查官方 Anthropic /v1/models,不会读取 model.base_url;第三方 Anthropic 中转可能被误报为 invalid API key'));
|
|
5607
|
+
console.log(chalk.yellow(' 建议: 以 yymaxapi 的 Hermes CLI 运行时测试,或 `hermes chat -Q -q "请只回复 OK"` 的结果为准'));
|
|
5608
|
+
|
|
5609
|
+
if (await confirmImmediateTest(args, '是否立即测试 Hermes CLI 连接?')) {
|
|
5610
|
+
await testAdditionalCliConnections(args, { only: ['hermes'] });
|
|
5611
|
+
}
|
|
5395
5612
|
}
|
|
5396
5613
|
|
|
5397
5614
|
// ============ 主程序 ============
|
|
@@ -5433,6 +5650,10 @@ async function main() {
|
|
|
5433
5650
|
await testConnection(paths, args);
|
|
5434
5651
|
return;
|
|
5435
5652
|
}
|
|
5653
|
+
if (args._.includes('speed-test') || args._.includes('speedtest')) {
|
|
5654
|
+
await speedTestNodes();
|
|
5655
|
+
return;
|
|
5656
|
+
}
|
|
5436
5657
|
|
|
5437
5658
|
while (true) {
|
|
5438
5659
|
// 显示当前状态
|
|
@@ -5459,6 +5680,7 @@ async function main() {
|
|
|
5459
5680
|
new inquirer.Separator(' -- 工具 --'),
|
|
5460
5681
|
{ name: ' 切换 OpenClaw 模型', value: 'switch_model' },
|
|
5461
5682
|
{ name: ` 权限管理${getToolsProfileTag(paths)}`, value: 'tools_profile' },
|
|
5683
|
+
{ name: ' 测速节点', value: 'speed_test_nodes' },
|
|
5462
5684
|
{ name: ' 测试连接', value: 'test_connection' },
|
|
5463
5685
|
{ name: ' 查看配置', value: 'view_config' },
|
|
5464
5686
|
{ name: ' 恢复默认', value: 'restore' },
|
|
@@ -5482,6 +5704,9 @@ async function main() {
|
|
|
5482
5704
|
case 'test_connection':
|
|
5483
5705
|
await testConnection(paths, {});
|
|
5484
5706
|
break;
|
|
5707
|
+
case 'speed_test_nodes':
|
|
5708
|
+
await speedTestNodes();
|
|
5709
|
+
break;
|
|
5485
5710
|
case 'switch_model':
|
|
5486
5711
|
await switchModel(paths);
|
|
5487
5712
|
break;
|
|
@@ -6686,6 +6911,8 @@ function testGatewayViaAgent(model, agentId = '') {
|
|
|
6686
6911
|
}
|
|
6687
6912
|
const nodeInfo = findCompatibleNode(nodeMajor);
|
|
6688
6913
|
const env = { ...process.env, PATH: extendPathEnv(nodeInfo ? nodeInfo.path : null), NODE_NO_WARNINGS: '1' };
|
|
6914
|
+
delete env.ANTHROPIC_API_KEY;
|
|
6915
|
+
delete env.ANTHROPIC_AUTH_TOKEN;
|
|
6689
6916
|
delete env.CLAUDE_API_KEY;
|
|
6690
6917
|
delete env.OPENCLAW_CLAUDE_KEY;
|
|
6691
6918
|
delete env.OPENCLAW_API_KEY;
|
|
@@ -155,7 +155,8 @@ npx yymaxapi@latest
|
|
|
155
155
|
{
|
|
156
156
|
"apiBaseUrl": "https://yunyi.rdzhvip.com/claude",
|
|
157
157
|
"env": {
|
|
158
|
-
"
|
|
158
|
+
"ANTHROPIC_API_KEY": "<你的云翼 API Key>",
|
|
159
|
+
"ANTHROPIC_BASE_URL": "https://yunyi.rdzhvip.com/claude"
|
|
159
160
|
}
|
|
160
161
|
}
|
|
161
162
|
```
|