yymaxapi 1.0.102 → 1.0.104
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 +491 -156
- package/package.json +1 -1
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();
|
|
@@ -1075,6 +1159,72 @@ function writeOpencodeConfig(claudeBaseUrl, codexBaseUrl, apiKey, defaultModelKe
|
|
|
1075
1159
|
return configPath;
|
|
1076
1160
|
}
|
|
1077
1161
|
|
|
1162
|
+
function writeHermesConfig(baseUrl, apiKey, modelId = getDefaultClaudeModel().id, options = {}) {
|
|
1163
|
+
const dataDir = getHermesDataDir();
|
|
1164
|
+
const configPath = path.join(dataDir, 'config.yaml');
|
|
1165
|
+
const envPath = getHermesEnvPath();
|
|
1166
|
+
const existingConfigPath = getHermesConfigPath();
|
|
1167
|
+
const wslMirror = getHermesWslMirrorInfo();
|
|
1168
|
+
const modelType = options.type || options.provider || '';
|
|
1169
|
+
const resolvedType = resolveHermesModelType(modelId, modelType);
|
|
1170
|
+
const runtimeProvider = resolvedType === 'codex' ? 'custom' : 'anthropic';
|
|
1171
|
+
const apiMode = resolvedType === 'codex' ? 'codex_responses' : 'anthropic_messages';
|
|
1172
|
+
|
|
1173
|
+
let existingConfigRaw = readTextIfExists(existingConfigPath);
|
|
1174
|
+
if (!existingConfigRaw && wslMirror.sourceConfigPath) {
|
|
1175
|
+
existingConfigRaw = readWslTextFile(wslMirror.sourceConfigPath);
|
|
1176
|
+
}
|
|
1177
|
+
|
|
1178
|
+
let existingEnvRaw = readTextIfExists(envPath);
|
|
1179
|
+
if (!existingEnvRaw && wslMirror.envPath) {
|
|
1180
|
+
existingEnvRaw = readWslTextFile(wslMirror.envPath);
|
|
1181
|
+
}
|
|
1182
|
+
|
|
1183
|
+
const normalizedBaseUrl = (
|
|
1184
|
+
resolvedType === 'codex'
|
|
1185
|
+
? trimOpenAiEndpointSuffix(String(baseUrl || '').trim())
|
|
1186
|
+
: trimClaudeMessagesSuffix(String(baseUrl || '').trim())
|
|
1187
|
+
).replace(/\/+$/, '');
|
|
1188
|
+
const existingConfig = parseSimpleYamlConfig(existingConfigRaw);
|
|
1189
|
+
const existingModelConfig = existingConfig.model && !Array.isArray(existingConfig.model) && typeof existingConfig.model === 'object'
|
|
1190
|
+
? existingConfig.model
|
|
1191
|
+
: {};
|
|
1192
|
+
const nextConfigRaw = upsertHermesModelConfig(existingConfigRaw, {
|
|
1193
|
+
...existingModelConfig,
|
|
1194
|
+
default: modelId,
|
|
1195
|
+
provider: runtimeProvider,
|
|
1196
|
+
base_url: normalizedBaseUrl,
|
|
1197
|
+
api_mode: apiMode
|
|
1198
|
+
});
|
|
1199
|
+
const envEntries = {};
|
|
1200
|
+
envEntries[resolvedType === 'codex' ? 'OPENAI_API_KEY' : 'ANTHROPIC_API_KEY'] = String(apiKey || '').trim();
|
|
1201
|
+
const nextEnvRaw = upsertEnvFile(existingEnvRaw, envEntries);
|
|
1202
|
+
|
|
1203
|
+
if (!fs.existsSync(dataDir)) fs.mkdirSync(dataDir, { recursive: true });
|
|
1204
|
+
fs.writeFileSync(configPath, nextConfigRaw, 'utf8');
|
|
1205
|
+
fs.writeFileSync(envPath, nextEnvRaw, 'utf8');
|
|
1206
|
+
|
|
1207
|
+
if (wslMirror.configPath) {
|
|
1208
|
+
try { syncFileToWsl(configPath, wslMirror.configPath); } catch { /* best-effort */ }
|
|
1209
|
+
}
|
|
1210
|
+
if (wslMirror.envPath) {
|
|
1211
|
+
try { syncFileToWsl(envPath, wslMirror.envPath); } catch { /* best-effort */ }
|
|
1212
|
+
}
|
|
1213
|
+
|
|
1214
|
+
return {
|
|
1215
|
+
dataDir,
|
|
1216
|
+
configPath,
|
|
1217
|
+
envPath,
|
|
1218
|
+
wslConfigPath: wslMirror.configPath,
|
|
1219
|
+
wslEnvPath: wslMirror.envPath,
|
|
1220
|
+
modelId,
|
|
1221
|
+
modelType: resolvedType,
|
|
1222
|
+
provider: runtimeProvider,
|
|
1223
|
+
apiMode,
|
|
1224
|
+
baseUrl: normalizedBaseUrl
|
|
1225
|
+
};
|
|
1226
|
+
}
|
|
1227
|
+
|
|
1078
1228
|
function syncExternalTools(type, baseUrl, apiKey, extra = {}) {
|
|
1079
1229
|
const synced = [];
|
|
1080
1230
|
try {
|
|
@@ -3400,6 +3550,251 @@ function getClaudeCodeSettingsPath() {
|
|
|
3400
3550
|
return path.join(os.homedir(), '.claude', 'settings.json');
|
|
3401
3551
|
}
|
|
3402
3552
|
|
|
3553
|
+
function readTextIfExists(filePath) {
|
|
3554
|
+
if (!filePath || !fs.existsSync(filePath)) return '';
|
|
3555
|
+
try {
|
|
3556
|
+
return fs.readFileSync(filePath, 'utf8');
|
|
3557
|
+
} catch {
|
|
3558
|
+
return '';
|
|
3559
|
+
}
|
|
3560
|
+
}
|
|
3561
|
+
|
|
3562
|
+
function getHermesDataDir() {
|
|
3563
|
+
const envDir = String(process.env.HERMES_DATA_DIR || '').trim();
|
|
3564
|
+
return envDir || path.join(os.homedir(), '.hermes');
|
|
3565
|
+
}
|
|
3566
|
+
|
|
3567
|
+
function getHermesConfigPath() {
|
|
3568
|
+
const dataDir = getHermesDataDir();
|
|
3569
|
+
const candidates = [
|
|
3570
|
+
path.join(dataDir, 'config.yaml'),
|
|
3571
|
+
path.join(dataDir, 'config.yml')
|
|
3572
|
+
];
|
|
3573
|
+
return candidates.find(filePath => fs.existsSync(filePath)) || candidates[0];
|
|
3574
|
+
}
|
|
3575
|
+
|
|
3576
|
+
function getHermesEnvPath() {
|
|
3577
|
+
return path.join(getHermesDataDir(), '.env');
|
|
3578
|
+
}
|
|
3579
|
+
|
|
3580
|
+
function getHermesWslMirrorInfo() {
|
|
3581
|
+
if (process.platform !== 'win32' || !isWslAvailable()) {
|
|
3582
|
+
return {
|
|
3583
|
+
dataDir: null,
|
|
3584
|
+
sourceConfigPath: null,
|
|
3585
|
+
configPath: null,
|
|
3586
|
+
envPath: null
|
|
3587
|
+
};
|
|
3588
|
+
}
|
|
3589
|
+
|
|
3590
|
+
const wslHome = getWslHome() || '/root';
|
|
3591
|
+
const dataDir = path.posix.join(wslHome, '.hermes');
|
|
3592
|
+
const sourceConfigPath = findExistingWslFile([
|
|
3593
|
+
path.posix.join(dataDir, 'config.yaml'),
|
|
3594
|
+
path.posix.join(dataDir, 'config.yml')
|
|
3595
|
+
]);
|
|
3596
|
+
|
|
3597
|
+
return {
|
|
3598
|
+
dataDir,
|
|
3599
|
+
sourceConfigPath,
|
|
3600
|
+
configPath: path.posix.join(dataDir, 'config.yaml'),
|
|
3601
|
+
envPath: path.posix.join(dataDir, '.env')
|
|
3602
|
+
};
|
|
3603
|
+
}
|
|
3604
|
+
|
|
3605
|
+
function readWslTextFile(filePath) {
|
|
3606
|
+
if (process.platform !== 'win32' || !filePath) return '';
|
|
3607
|
+
const quoted = shellQuote(filePath);
|
|
3608
|
+
const result = safeExec(`wsl -- bash -lc "cat ${quoted} 2>/dev/null"`, { timeout: 10000 });
|
|
3609
|
+
return result.ok ? (result.output || result.stdout || '') : '';
|
|
3610
|
+
}
|
|
3611
|
+
|
|
3612
|
+
function parseYamlScalar(value) {
|
|
3613
|
+
let normalized = String(value ?? '').trim();
|
|
3614
|
+
if (
|
|
3615
|
+
(normalized.startsWith('"') && normalized.endsWith('"'))
|
|
3616
|
+
|| (normalized.startsWith('\'') && normalized.endsWith('\''))
|
|
3617
|
+
) {
|
|
3618
|
+
try {
|
|
3619
|
+
normalized = JSON.parse(normalized);
|
|
3620
|
+
} catch {
|
|
3621
|
+
normalized = normalized.slice(1, -1);
|
|
3622
|
+
}
|
|
3623
|
+
}
|
|
3624
|
+
return normalized;
|
|
3625
|
+
}
|
|
3626
|
+
|
|
3627
|
+
function parseSimpleYamlConfig(text) {
|
|
3628
|
+
const config = {};
|
|
3629
|
+
let currentObjectKey = '';
|
|
3630
|
+
|
|
3631
|
+
for (const line of String(text || '').split(/\r?\n/)) {
|
|
3632
|
+
if (!line.trim() || line.trim().startsWith('#')) continue;
|
|
3633
|
+
|
|
3634
|
+
const topLevelMatch = line.match(/^([A-Za-z0-9_-]+)\s*:\s*(.*?)\s*$/);
|
|
3635
|
+
if (topLevelMatch && !line.startsWith(' ')) {
|
|
3636
|
+
const key = topLevelMatch[1];
|
|
3637
|
+
const rawValue = topLevelMatch[2];
|
|
3638
|
+
if (rawValue === '') {
|
|
3639
|
+
if (!config[key] || typeof config[key] !== 'object' || Array.isArray(config[key])) {
|
|
3640
|
+
config[key] = {};
|
|
3641
|
+
}
|
|
3642
|
+
currentObjectKey = key;
|
|
3643
|
+
} else {
|
|
3644
|
+
config[key] = parseYamlScalar(rawValue);
|
|
3645
|
+
currentObjectKey = '';
|
|
3646
|
+
}
|
|
3647
|
+
continue;
|
|
3648
|
+
}
|
|
3649
|
+
|
|
3650
|
+
if (currentObjectKey) {
|
|
3651
|
+
const nestedMatch = line.match(/^\s{2}([A-Za-z0-9_-]+)\s*:\s*(.*?)\s*$/);
|
|
3652
|
+
if (nestedMatch) {
|
|
3653
|
+
if (!config[currentObjectKey] || typeof config[currentObjectKey] !== 'object' || Array.isArray(config[currentObjectKey])) {
|
|
3654
|
+
config[currentObjectKey] = {};
|
|
3655
|
+
}
|
|
3656
|
+
config[currentObjectKey][nestedMatch[1]] = parseYamlScalar(nestedMatch[2]);
|
|
3657
|
+
continue;
|
|
3658
|
+
}
|
|
3659
|
+
}
|
|
3660
|
+
|
|
3661
|
+
currentObjectKey = '';
|
|
3662
|
+
}
|
|
3663
|
+
|
|
3664
|
+
return config;
|
|
3665
|
+
}
|
|
3666
|
+
|
|
3667
|
+
function serializeYamlScalar(value) {
|
|
3668
|
+
return JSON.stringify(String(value ?? ''));
|
|
3669
|
+
}
|
|
3670
|
+
|
|
3671
|
+
function buildHermesModelConfigBlock(entries) {
|
|
3672
|
+
const ordered = {};
|
|
3673
|
+
for (const key of ['default', 'provider', 'base_url', 'api_mode']) {
|
|
3674
|
+
if (Object.prototype.hasOwnProperty.call(entries, key)) {
|
|
3675
|
+
ordered[key] = entries[key];
|
|
3676
|
+
}
|
|
3677
|
+
}
|
|
3678
|
+
for (const [key, value] of Object.entries(entries)) {
|
|
3679
|
+
if (!Object.prototype.hasOwnProperty.call(ordered, key)) {
|
|
3680
|
+
ordered[key] = value;
|
|
3681
|
+
}
|
|
3682
|
+
}
|
|
3683
|
+
|
|
3684
|
+
const lines = ['model:'];
|
|
3685
|
+
for (const [key, value] of Object.entries(ordered)) {
|
|
3686
|
+
if (value === undefined || value === null || value === '') continue;
|
|
3687
|
+
lines.push(` ${key}: ${serializeYamlScalar(value)}`);
|
|
3688
|
+
}
|
|
3689
|
+
return lines.join('\n');
|
|
3690
|
+
}
|
|
3691
|
+
|
|
3692
|
+
function upsertHermesModelConfig(text, modelEntries) {
|
|
3693
|
+
let normalized = String(text || '').replace(/\r\n/g, '\n');
|
|
3694
|
+
normalized = normalized.replace(/^(provider|base_url|api_mode):\s*.*\n?/gm, '');
|
|
3695
|
+
|
|
3696
|
+
const lines = normalized.split('\n');
|
|
3697
|
+
const output = [];
|
|
3698
|
+
let replaced = false;
|
|
3699
|
+
|
|
3700
|
+
for (let i = 0; i < lines.length; i += 1) {
|
|
3701
|
+
const line = lines[i];
|
|
3702
|
+
if (/^model:\s*(.*)$/.test(line)) {
|
|
3703
|
+
replaced = true;
|
|
3704
|
+
if (output.length > 0 && output[output.length - 1] !== '') {
|
|
3705
|
+
output.push('');
|
|
3706
|
+
}
|
|
3707
|
+
output.push(buildHermesModelConfigBlock(modelEntries));
|
|
3708
|
+
|
|
3709
|
+
i += 1;
|
|
3710
|
+
while (i < lines.length) {
|
|
3711
|
+
const nextLine = lines[i];
|
|
3712
|
+
if (/^\s{2,}/.test(nextLine)) {
|
|
3713
|
+
i += 1;
|
|
3714
|
+
continue;
|
|
3715
|
+
}
|
|
3716
|
+
if (nextLine === '') {
|
|
3717
|
+
break;
|
|
3718
|
+
}
|
|
3719
|
+
i -= 1;
|
|
3720
|
+
break;
|
|
3721
|
+
}
|
|
3722
|
+
continue;
|
|
3723
|
+
}
|
|
3724
|
+
output.push(line);
|
|
3725
|
+
}
|
|
3726
|
+
|
|
3727
|
+
while (output.length > 0 && output[output.length - 1] === '') {
|
|
3728
|
+
output.pop();
|
|
3729
|
+
}
|
|
3730
|
+
if (!replaced) {
|
|
3731
|
+
if (output.length > 0) output.push('');
|
|
3732
|
+
output.push(buildHermesModelConfigBlock(modelEntries));
|
|
3733
|
+
}
|
|
3734
|
+
|
|
3735
|
+
return output.join('\n').replace(/\n{3,}/g, '\n\n').trim() + '\n';
|
|
3736
|
+
}
|
|
3737
|
+
|
|
3738
|
+
function parseEnvFile(text) {
|
|
3739
|
+
const entries = {};
|
|
3740
|
+
for (const line of String(text || '').split(/\r?\n/)) {
|
|
3741
|
+
const trimmed = line.trim();
|
|
3742
|
+
if (!trimmed || trimmed.startsWith('#')) continue;
|
|
3743
|
+
const match = trimmed.match(/^([A-Za-z_][A-Za-z0-9_]*)\s*=\s*(.*)$/);
|
|
3744
|
+
if (!match) continue;
|
|
3745
|
+
let value = match[2].trim();
|
|
3746
|
+
if (
|
|
3747
|
+
(value.startsWith('"') && value.endsWith('"'))
|
|
3748
|
+
|| (value.startsWith('\'') && value.endsWith('\''))
|
|
3749
|
+
) {
|
|
3750
|
+
try {
|
|
3751
|
+
value = JSON.parse(value);
|
|
3752
|
+
} catch {
|
|
3753
|
+
value = value.slice(1, -1);
|
|
3754
|
+
}
|
|
3755
|
+
}
|
|
3756
|
+
entries[match[1]] = value;
|
|
3757
|
+
}
|
|
3758
|
+
return entries;
|
|
3759
|
+
}
|
|
3760
|
+
|
|
3761
|
+
function upsertEnvFile(text, entries) {
|
|
3762
|
+
const lines = String(text || '').replace(/\r\n/g, '\n').split('\n');
|
|
3763
|
+
const normalized = lines.length === 1 && lines[0] === '' ? [] : [...lines];
|
|
3764
|
+
const seen = new Set();
|
|
3765
|
+
|
|
3766
|
+
for (let i = 0; i < normalized.length; i += 1) {
|
|
3767
|
+
const match = normalized[i].match(/^\s*([A-Za-z_][A-Za-z0-9_]*)\s*=/);
|
|
3768
|
+
if (!match) continue;
|
|
3769
|
+
const key = match[1];
|
|
3770
|
+
if (!Object.prototype.hasOwnProperty.call(entries, key)) continue;
|
|
3771
|
+
normalized[i] = `${key}=${JSON.stringify(String(entries[key] ?? ''))}`;
|
|
3772
|
+
seen.add(key);
|
|
3773
|
+
}
|
|
3774
|
+
|
|
3775
|
+
for (const key of Object.keys(entries)) {
|
|
3776
|
+
if (!seen.has(key)) {
|
|
3777
|
+
normalized.push(`${key}=${JSON.stringify(String(entries[key] ?? ''))}`);
|
|
3778
|
+
}
|
|
3779
|
+
}
|
|
3780
|
+
|
|
3781
|
+
while (normalized.length > 0 && normalized[normalized.length - 1] === '') {
|
|
3782
|
+
normalized.pop();
|
|
3783
|
+
}
|
|
3784
|
+
|
|
3785
|
+
return normalized.join('\n') + '\n';
|
|
3786
|
+
}
|
|
3787
|
+
|
|
3788
|
+
function readHermesYamlConfig(configPath = getHermesConfigPath()) {
|
|
3789
|
+
const raw = readTextIfExists(configPath);
|
|
3790
|
+
return {
|
|
3791
|
+
configPath,
|
|
3792
|
+
configured: !!raw || fs.existsSync(configPath),
|
|
3793
|
+
config: parseSimpleYamlConfig(raw),
|
|
3794
|
+
raw
|
|
3795
|
+
};
|
|
3796
|
+
}
|
|
3797
|
+
|
|
3403
3798
|
function getOpencodeConfigPath() {
|
|
3404
3799
|
const home = os.homedir();
|
|
3405
3800
|
return process.platform === 'win32'
|
|
@@ -4112,6 +4507,27 @@ function trimClaudeMessagesSuffix(baseUrl) {
|
|
|
4112
4507
|
return trimmed;
|
|
4113
4508
|
}
|
|
4114
4509
|
|
|
4510
|
+
function trimOpenAiEndpointSuffix(baseUrl) {
|
|
4511
|
+
const trimmed = baseUrl.trim();
|
|
4512
|
+
const suffixes = [
|
|
4513
|
+
'/v1/responses',
|
|
4514
|
+
'/responses',
|
|
4515
|
+
'/v1/chat/completions',
|
|
4516
|
+
'/chat/completions',
|
|
4517
|
+
'/v1/models',
|
|
4518
|
+
'/models',
|
|
4519
|
+
'/v1'
|
|
4520
|
+
];
|
|
4521
|
+
|
|
4522
|
+
for (const suffix of suffixes) {
|
|
4523
|
+
if (trimmed.endsWith(suffix)) {
|
|
4524
|
+
return trimmed.slice(0, -suffix.length);
|
|
4525
|
+
}
|
|
4526
|
+
}
|
|
4527
|
+
|
|
4528
|
+
return trimmed;
|
|
4529
|
+
}
|
|
4530
|
+
|
|
4115
4531
|
async function quickSetup(paths, args = {}) {
|
|
4116
4532
|
console.log(chalk.cyan.bold('\n🚀 快速配置向导\n'));
|
|
4117
4533
|
|
|
@@ -5096,183 +5512,94 @@ async function activateCodex(paths, args = {}) {
|
|
|
5096
5512
|
}
|
|
5097
5513
|
}
|
|
5098
5514
|
|
|
5099
|
-
// ============
|
|
5100
|
-
async function
|
|
5101
|
-
console.log(chalk.cyan.bold('\n
|
|
5515
|
+
// ============ 单独配置 Hermes ============
|
|
5516
|
+
async function activateHermes(paths, args = {}) {
|
|
5517
|
+
console.log(chalk.cyan.bold('\n🔧 配置 Hermes\n'));
|
|
5518
|
+
const selection = await promptHermesModelSelection(args, '选择 Hermes 默认模型:');
|
|
5519
|
+
const selectedModel = selection.model;
|
|
5520
|
+
const selectedType = selection.type;
|
|
5102
5521
|
|
|
5103
|
-
const
|
|
5104
|
-
|
|
5105
|
-
const claudeProviderName = claudeApiConfig.providerName;
|
|
5106
|
-
const codexProviderName = codexApiConfig.providerName;
|
|
5522
|
+
const shouldTest = !(args['no-test'] || args.noTest);
|
|
5523
|
+
let selectedEndpoint = ENDPOINTS[0];
|
|
5107
5524
|
|
|
5108
|
-
|
|
5109
|
-
|
|
5110
|
-
|
|
5525
|
+
if (shouldTest) {
|
|
5526
|
+
console.log(chalk.cyan('📡 开始测速节点...\n'));
|
|
5527
|
+
const speedResult = await testAllEndpoints(ENDPOINTS, { autoFallback: true });
|
|
5528
|
+
const sorted = speedResult.ranked || [];
|
|
5529
|
+
if (sorted.length > 0) {
|
|
5530
|
+
const defaultEp = ENDPOINTS[0];
|
|
5531
|
+
const { selectedIndex } = await inquirer.prompt([{
|
|
5532
|
+
type: 'list',
|
|
5533
|
+
name: 'selectedIndex',
|
|
5534
|
+
message: '选择节点:',
|
|
5535
|
+
choices: [
|
|
5536
|
+
{ name: `* 使用默认节点 (${defaultEp.name})`, value: -1 },
|
|
5537
|
+
new inquirer.Separator(' ---- 或按测速结果选择 ----'),
|
|
5538
|
+
...sorted.map((e, i) => ({
|
|
5539
|
+
name: `${e.name} - ${e.latency}ms (评分:${e.score})`,
|
|
5540
|
+
value: i
|
|
5541
|
+
}))
|
|
5542
|
+
]
|
|
5543
|
+
}]);
|
|
5544
|
+
selectedEndpoint = selectedIndex === -1 ? defaultEp : sorted[selectedIndex];
|
|
5545
|
+
} else {
|
|
5546
|
+
console.log(chalk.red('\n⚠️ 所有节点均不可达'));
|
|
5547
|
+
const { proceed } = await inquirer.prompt([{
|
|
5548
|
+
type: 'confirm', name: 'proceed',
|
|
5549
|
+
message: '仍要使用默认节点配置吗?', default: false
|
|
5550
|
+
}]);
|
|
5551
|
+
if (!proceed) { console.log(chalk.gray('已取消')); return; }
|
|
5552
|
+
}
|
|
5553
|
+
}
|
|
5111
5554
|
|
|
5112
|
-
// 1. CLI 参数
|
|
5113
|
-
const args = parseArgs(process.argv.slice(2));
|
|
5114
5555
|
const directKey = (args['api-key'] || args.apiKey || args.key || '').toString().trim();
|
|
5556
|
+
let apiKey;
|
|
5115
5557
|
if (directKey) {
|
|
5116
5558
|
apiKey = directKey;
|
|
5117
|
-
keySource = '命令行参数';
|
|
5118
|
-
}
|
|
5119
|
-
|
|
5120
|
-
// 2. 环境变量
|
|
5121
|
-
if (!apiKey) {
|
|
5122
|
-
const envKeys = ['OPENCLAW_CLAUDE_KEY', 'OPENCLAW_CODEX_KEY', 'CLAUDE_API_KEY', 'OPENAI_API_KEY', 'OPENCLAW_API_KEY'];
|
|
5123
|
-
for (const k of envKeys) {
|
|
5124
|
-
if (process.env[k] && process.env[k].trim()) {
|
|
5125
|
-
apiKey = process.env[k].trim();
|
|
5126
|
-
keySource = `环境变量 ${k}`;
|
|
5127
|
-
break;
|
|
5128
|
-
}
|
|
5129
|
-
}
|
|
5130
|
-
}
|
|
5131
|
-
|
|
5132
|
-
// 3. 已有 OpenClaw 配置(云翼 Claude Code 密钥)
|
|
5133
|
-
if (!apiKey) {
|
|
5134
|
-
try {
|
|
5135
|
-
const config = readConfig(paths.openclawConfig);
|
|
5136
|
-
if (config && config.models && config.models.providers) {
|
|
5137
|
-
// 优先取 claude-yunyi 的 key
|
|
5138
|
-
const preferredOrder = [claudeProviderName, codexProviderName];
|
|
5139
|
-
for (const name of preferredOrder) {
|
|
5140
|
-
const p = config.models.providers[name];
|
|
5141
|
-
if (p && p.apiKey && p.apiKey.trim()) {
|
|
5142
|
-
apiKey = p.apiKey.trim();
|
|
5143
|
-
keySource = `已有配置 (${name})`;
|
|
5144
|
-
break;
|
|
5145
|
-
}
|
|
5146
|
-
}
|
|
5147
|
-
// 其他 provider 的 key
|
|
5148
|
-
if (!apiKey) {
|
|
5149
|
-
for (const [name, p] of Object.entries(config.models.providers)) {
|
|
5150
|
-
if (p.apiKey && p.apiKey.trim()) {
|
|
5151
|
-
apiKey = p.apiKey.trim();
|
|
5152
|
-
keySource = `已有配置 (${name})`;
|
|
5153
|
-
break;
|
|
5154
|
-
}
|
|
5155
|
-
}
|
|
5156
|
-
}
|
|
5157
|
-
}
|
|
5158
|
-
} catch { /* ignore */ }
|
|
5159
|
-
}
|
|
5160
|
-
|
|
5161
|
-
// 4. 都没有,提示输入
|
|
5162
|
-
if (apiKey) {
|
|
5163
|
-
const masked = apiKey.length > 8 ? apiKey.slice(0, 5) + '***' + apiKey.slice(-3) : '***';
|
|
5164
|
-
console.log(chalk.green(`✓ 已检测到 API Key: ${masked} (来源: ${keySource})`));
|
|
5165
5559
|
} else {
|
|
5166
|
-
|
|
5167
|
-
|
|
5168
|
-
|
|
5169
|
-
|
|
5170
|
-
// ---- 静默测速选最快节点 ----
|
|
5171
|
-
const speedSpinner = ora({ text: '正在测速选择最快节点...', spinner: 'dots' }).start();
|
|
5172
|
-
let selectedEndpoint = ENDPOINTS[0];
|
|
5173
|
-
try {
|
|
5174
|
-
const speedResult = await testAllEndpoints(ENDPOINTS, { autoFallback: true });
|
|
5175
|
-
if (speedResult.ranked && speedResult.ranked.length > 0) {
|
|
5176
|
-
selectedEndpoint = speedResult.ranked[0];
|
|
5177
|
-
}
|
|
5178
|
-
speedSpinner.succeed(`节点: ${selectedEndpoint.name}`);
|
|
5179
|
-
} catch {
|
|
5180
|
-
speedSpinner.succeed(`节点: ${selectedEndpoint.name} (默认)`);
|
|
5560
|
+
const envKey = selectedType === 'codex'
|
|
5561
|
+
? (process.env.OPENAI_API_KEY || process.env.OPENAI_AUTH_TOKEN || '')
|
|
5562
|
+
: (process.env.ANTHROPIC_API_KEY || process.env.ANTHROPIC_AUTH_TOKEN || process.env.CLAUDE_API_KEY || '');
|
|
5563
|
+
apiKey = await promptApiKey('请输入 API Key:', envKey);
|
|
5181
5564
|
}
|
|
5565
|
+
if (!apiKey) { console.log(chalk.gray('已取消')); return; }
|
|
5182
5566
|
|
|
5183
|
-
|
|
5567
|
+
console.log('');
|
|
5184
5568
|
const validation = await validateApiKey(selectedEndpoint.url, apiKey);
|
|
5185
5569
|
if (!validation.valid) {
|
|
5186
|
-
|
|
5570
|
+
const { continueAnyway } = await inquirer.prompt([{
|
|
5571
|
+
type: 'confirm', name: 'continueAnyway',
|
|
5572
|
+
message: 'API Key 验证失败,是否仍然继续写入配置?', default: false
|
|
5573
|
+
}]);
|
|
5574
|
+
if (!continueAnyway) { console.log(chalk.gray('已取消')); return; }
|
|
5187
5575
|
}
|
|
5188
5576
|
|
|
5189
|
-
|
|
5190
|
-
const
|
|
5191
|
-
|
|
5192
|
-
|
|
5577
|
+
const typeLabel = selectedType === 'codex' ? 'GPT' : 'Claude';
|
|
5578
|
+
const hermesBaseUrl = buildFullUrl(selectedEndpoint.url, selectedType === 'codex' ? 'codex' : 'claude');
|
|
5579
|
+
const writeSpinner = ora({ text: '正在写入 Hermes 配置...', spinner: 'dots' }).start();
|
|
5580
|
+
const hermesPaths = writeHermesConfig(hermesBaseUrl, apiKey, selectedModel.id, { type: selectedType });
|
|
5581
|
+
writeSpinner.succeed('Hermes 配置写入完成');
|
|
5193
5582
|
|
|
5194
|
-
|
|
5195
|
-
|
|
5196
|
-
|
|
5197
|
-
|
|
5198
|
-
|
|
5199
|
-
|
|
5200
|
-
|
|
5201
|
-
|
|
5202
|
-
|
|
5203
|
-
|
|
5204
|
-
|
|
5205
|
-
authHeader: false,
|
|
5206
|
-
apiKey: apiKey.trim(),
|
|
5207
|
-
models: [{ id: claudeModel.id, name: claudeModel.name, contextWindow: claudeApiConfig.contextWindow, maxTokens: claudeApiConfig.maxTokens }]
|
|
5208
|
-
};
|
|
5209
|
-
config.auth.profiles[`${claudeProviderName}:default`] = { provider: claudeProviderName, mode: 'api_key' };
|
|
5210
|
-
config.agents.defaults.models[claudeModelKey] = { alias: claudeProviderName };
|
|
5211
|
-
|
|
5212
|
-
// Codex 侧
|
|
5213
|
-
const codexBaseUrl = buildFullUrl(selectedEndpoint.url, 'codex');
|
|
5214
|
-
const codexModelId = CODEX_MODELS[0]?.id || 'gpt-5.4';
|
|
5215
|
-
const codexModel = CODEX_MODELS.find(m => m.id === codexModelId) || { id: codexModelId, name: 'GPT 5.4' };
|
|
5216
|
-
const codexModelKey = `${codexProviderName}/${codexModelId}`;
|
|
5217
|
-
|
|
5218
|
-
config.models.providers[codexProviderName] = {
|
|
5219
|
-
baseUrl: codexBaseUrl,
|
|
5220
|
-
auth: DEFAULT_AUTH_MODE,
|
|
5221
|
-
api: codexApiConfig.api,
|
|
5222
|
-
headers: {},
|
|
5223
|
-
authHeader: codexApiConfig.api.startsWith('openai'),
|
|
5224
|
-
apiKey: apiKey.trim(),
|
|
5225
|
-
models: [{ id: codexModel.id, name: codexModel.name, contextWindow: codexApiConfig.contextWindow, maxTokens: codexApiConfig.maxTokens }]
|
|
5226
|
-
};
|
|
5227
|
-
config.auth.profiles[`${codexProviderName}:default`] = { provider: codexProviderName, mode: 'api_key' };
|
|
5228
|
-
config.agents.defaults.models[codexModelKey] = { alias: codexProviderName };
|
|
5229
|
-
|
|
5230
|
-
// 默认主力: Codex, 备用: Claude
|
|
5231
|
-
config.agents.defaults.model.primary = codexModelKey;
|
|
5232
|
-
config.agents.defaults.model.fallbacks = [claudeModelKey];
|
|
5233
|
-
const yunyiLayoutResult = applyManagedYunyiOpenClawLayout(config, {
|
|
5234
|
-
force: true,
|
|
5235
|
-
endpointUrl: selectedEndpoint.url,
|
|
5236
|
-
apiKey
|
|
5237
|
-
});
|
|
5238
|
-
|
|
5239
|
-
// ---- 写入 ----
|
|
5240
|
-
const writeSpinner = ora({ text: '正在写入配置...', spinner: 'dots' }).start();
|
|
5241
|
-
createTimestampedBackup(paths.openclawConfig, paths.configDir, 'yycode');
|
|
5242
|
-
ensureGatewaySettings(config);
|
|
5243
|
-
cleanupConflictingEnvVars(config, codexBaseUrl, apiKey);
|
|
5244
|
-
writeConfigWithSync(paths, config);
|
|
5245
|
-
updateAuthProfilesWithSync(paths, claudeProviderName, apiKey);
|
|
5246
|
-
updateAuthProfilesWithSync(paths, codexProviderName, apiKey);
|
|
5247
|
-
if (yunyiLayoutResult.applied) {
|
|
5248
|
-
syncManagedYunyiAuthProfiles(paths, config);
|
|
5583
|
+
console.log(chalk.green('\n✅ Hermes 配置完成!'));
|
|
5584
|
+
console.log(chalk.cyan(` Base URL: ${hermesPaths.baseUrl}`));
|
|
5585
|
+
console.log(chalk.gray(` 模型: ${selectedModel.name} (${selectedModel.id})`));
|
|
5586
|
+
console.log(chalk.gray(` 协议: ${typeLabel}`));
|
|
5587
|
+
console.log(chalk.gray(` Provider: ${hermesPaths.provider}`));
|
|
5588
|
+
console.log(chalk.gray(' API Key: 已设置'));
|
|
5589
|
+
console.log(chalk.gray('\n 已写入:'));
|
|
5590
|
+
console.log(chalk.gray(` • ${hermesPaths.configPath}`));
|
|
5591
|
+
console.log(chalk.gray(` • ${hermesPaths.envPath}`));
|
|
5592
|
+
if (hermesPaths.wslConfigPath && hermesPaths.wslEnvPath) {
|
|
5593
|
+
console.log(chalk.gray(' • 已额外同步到 WSL ~/.hermes'));
|
|
5249
5594
|
}
|
|
5250
|
-
|
|
5251
|
-
|
|
5252
|
-
writeSpinner.succeed('配置写入完成');
|
|
5253
|
-
|
|
5254
|
-
// ---- 结果 ----
|
|
5255
|
-
console.log(chalk.green('\n✅ 配置完成!'));
|
|
5256
|
-
console.log(chalk.cyan(` Claude Code: ${claudeModel.name}`));
|
|
5257
|
-
console.log(chalk.cyan(` Codex CLI: ${codexModel.name}`));
|
|
5258
|
-
printYunyiOpenClawSwitchHint(yunyiLayoutResult);
|
|
5259
|
-
console.log('');
|
|
5595
|
+
console.log(chalk.yellow('\n 提示: Hermes Desktop 的“测试连接”按钮当前可能仍按 /v1/models 检测;按钮不绿,不代表 runtime 不能正常对话'));
|
|
5596
|
+
console.log(chalk.yellow(' 建议: 重启 Hermes Desktop,或手动执行 hermes server --port 8787 验证 Hermes runtime'));
|
|
5260
5597
|
}
|
|
5261
5598
|
|
|
5262
|
-
|
|
5263
5599
|
// ============ 主程序 ============
|
|
5264
5600
|
async function main() {
|
|
5265
5601
|
console.clear();
|
|
5266
5602
|
|
|
5267
|
-
// yycode 精简模式:检测到 yycode CLI 时直接走零交互流程
|
|
5268
|
-
const isYYCode = path.basename(process.argv[1] || '').replace(/\.js$/, '') === 'yycode';
|
|
5269
|
-
if (isYYCode) {
|
|
5270
|
-
const paths = getConfigPath();
|
|
5271
|
-
backupOriginalConfig(paths.openclawConfig, paths.configDir);
|
|
5272
|
-
await yycodeQuickSetup(paths);
|
|
5273
|
-
return;
|
|
5274
|
-
}
|
|
5275
|
-
|
|
5276
5603
|
console.log(chalk.cyan.bold('\n🔧 OpenClaw API 配置工具\n'));
|
|
5277
5604
|
|
|
5278
5605
|
const paths = getConfigPath();
|
|
@@ -5296,6 +5623,10 @@ async function main() {
|
|
|
5296
5623
|
await activateClaudeCode(paths, args);
|
|
5297
5624
|
return;
|
|
5298
5625
|
}
|
|
5626
|
+
if (args.preset === 'hermes' || args._.includes('preset-hermes') || args._.includes('hermes-preset')) {
|
|
5627
|
+
await activateHermes(paths, args);
|
|
5628
|
+
return;
|
|
5629
|
+
}
|
|
5299
5630
|
if (args.preset === 'codex' || args._.includes('preset-codex') || args._.includes('codex-preset')) {
|
|
5300
5631
|
await autoActivate(paths, { ...args, primary: 'codex' });
|
|
5301
5632
|
return;
|
|
@@ -5324,6 +5655,7 @@ async function main() {
|
|
|
5324
5655
|
new inquirer.Separator(' -- 一键配置 --'),
|
|
5325
5656
|
{ name: ' 配置 OpenClaw(Claude + Codex)', value: 'auto_activate' },
|
|
5326
5657
|
{ name: ' 配置 Claude Code', value: 'activate_claude_code' },
|
|
5658
|
+
{ name: ' 配置 Hermes', value: 'activate_hermes' },
|
|
5327
5659
|
{ name: ' 配置 Opencode', value: 'activate_opencode' },
|
|
5328
5660
|
{ name: ' 配置 Codex CLI', value: 'activate_codex' },
|
|
5329
5661
|
new inquirer.Separator(' -- 工具 --'),
|
|
@@ -5367,6 +5699,9 @@ async function main() {
|
|
|
5367
5699
|
case 'activate_claude_code':
|
|
5368
5700
|
await activateClaudeCode(paths);
|
|
5369
5701
|
break;
|
|
5702
|
+
case 'activate_hermes':
|
|
5703
|
+
await activateHermes(paths);
|
|
5704
|
+
break;
|
|
5370
5705
|
case 'activate_opencode':
|
|
5371
5706
|
await activateOpencode(paths);
|
|
5372
5707
|
break;
|