diff --git a/scripts/dev-api.js b/scripts/dev-api.js index f05c801..8f8c2f8 100644 --- a/scripts/dev-api.js +++ b/scripts/dev-api.js @@ -6931,7 +6931,7 @@ const handlers = { : 'hermes-agent @ git+https://github.com/NousResearch/hermes-agent.git' const installArgs = method === 'uv-pip' ? ['pip', 'install', pkg] - : ['tool', 'install', '--force', pkg, '--python', '3.11', '--with', 'croniter'] + : ['tool', 'install', '--force', pkg, '--python', '3.11', '--with', 'croniter', '--with', 'httpx', '--with', 'openai', '--with', 'aiohttp', '--with', 'websockets'] const result = spawnSync(uv, installArgs, { env: { ...process.env, PATH: hermesEnhancedPath(), GIT_TERMINAL_PROMPT: '0', ...gitMirrorEnv() }, timeout: 600000, @@ -6963,7 +6963,7 @@ const handlers = { if (!modelStr) throw new Error(`Provider '${providerId || 'custom'}' has no default model; please pass an explicit model name`) const baseUrlValue = baseUrl && baseUrl.trim() ? baseUrl.trim() : '' const baseUrlLine = baseUrlValue ? ` base_url: ${baseUrlValue}\n` : '' - const providerLine = providerId && providerId !== 'custom' ? ` provider: ${providerId}\n` : '' + const providerLine = providerId ? ` provider: ${providerId}\n` : '' const configPath = path.join(home, 'config.yaml') let configContent if (fs.existsSync(configPath)) { @@ -8783,29 +8783,37 @@ function _sanitizeHermesOpenrouterCustomMismatch() { const expected = _normalizeProviderUrl('https://openrouter.ai/api/v1') const usesCustomEndpoint = base && base !== expected const aliasChanged = (!provider || provider === 'custom' || usesCustomEndpoint) ? _ensureCustomOpenAIKeyAlias() : false - if (provider !== 'openrouter') return aliasChanged - if (!base || base === expected) return aliasChanged - let envRaw = '' - try { envRaw = fs.readFileSync(path.join(home, '.env'), 'utf8') } catch {} - const hasOpenrouterKey = _envHasValue(envRaw, 'OPENROUTER_API_KEY') - const hasCustomKey = _envHasValue(envRaw, 'CUSTOM_API_KEY') || _envHasValue(envRaw, 'OPENAI_API_KEY') - if (hasOpenrouterKey && !hasCustomKey) return aliasChanged + if (!usesCustomEndpoint) return aliasChanged + if (provider === 'custom') return aliasChanged const out = [] inModel = false + let providerWritten = false for (const line of raw.split('\n')) { const t = line.trim() if (t.startsWith('model:')) { inModel = true + providerWritten = false out.push(line) continue } if (inModel) { const indented = line.startsWith(' ') || line.startsWith('\t') - if (!indented && t && !t.startsWith('#')) inModel = false - else if (t.startsWith('provider:')) continue + if (!indented && t && !t.startsWith('#')) { + inModel = false + if (!providerWritten) { + out.push(' provider: custom') + providerWritten = true + } + } + else if (t.startsWith('provider:')) { + out.push(' provider: custom') + providerWritten = true + continue + } } out.push(line) } + if (inModel && !providerWritten) out.push(' provider: custom') let fixed = out.join('\n') if (!fixed.endsWith('\n')) fixed += '\n' fs.writeFileSync(configPath, fixed) diff --git a/src-tauri/src/commands/hermes.rs b/src-tauri/src/commands/hermes.rs index df91a95..ae0d81e 100644 --- a/src-tauri/src/commands/hermes.rs +++ b/src-tauri/src/commands/hermes.rs @@ -441,27 +441,21 @@ fn sanitize_hermes_openrouter_custom_mismatch() -> Result { } else { false }; - if provider != "openrouter" { + if !uses_custom_endpoint { return Ok(alias_changed); } - if base.is_empty() || base == expected { - return Ok(alias_changed); - } - - let env_raw = std::fs::read_to_string(home.join(".env")).unwrap_or_default(); - let has_openrouter_key = env_file_has_value(&env_raw, "OPENROUTER_API_KEY"); - let has_custom_key = env_file_has_value(&env_raw, "CUSTOM_API_KEY") - || env_file_has_value(&env_raw, "OPENAI_API_KEY"); - if has_openrouter_key && !has_custom_key { + if provider == "custom" { return Ok(alias_changed); } let mut out = Vec::new(); let mut in_model = false; + let mut provider_written = false; for line in raw.lines() { let trimmed = line.trim(); if trimmed.starts_with("model:") { in_model = true; + provider_written = false; out.push(line.to_string()); continue; } @@ -469,12 +463,21 @@ fn sanitize_hermes_openrouter_custom_mismatch() -> Result { let indented = line.starts_with(' ') || line.starts_with('\t'); if !indented && !trimmed.is_empty() && !trimmed.starts_with('#') { in_model = false; + if !provider_written { + out.push(" provider: custom".to_string()); + provider_written = true; + } } else if trimmed.starts_with("provider:") { + out.push(" provider: custom".to_string()); + provider_written = true; continue; } } out.push(line.to_string()); } + if in_model && !provider_written { + out.push(" provider: custom".to_string()); + } let mut fixed = out.join("\n"); if !fixed.ends_with('\n') { fixed.push('\n'); @@ -1653,7 +1656,22 @@ async fn install_via_uv_tool( let mut cmd = tokio::process::Command::new(uv_path); cmd.args([ - "tool", "install", "--force", &pkg, "--python", "3.11", "--with", "croniter", + "tool", + "install", + "--force", + &pkg, + "--python", + "3.11", + "--with", + "croniter", + "--with", + "httpx", + "--with", + "openai", + "--with", + "aiohttp", + "--with", + "websockets", ]); // 配置 PyPI 镜像(extras 的依赖仍从 PyPI 下载) @@ -1678,7 +1696,7 @@ async fn install_via_uv_tool( let _ = app.emit( "hermes-install-log", - "uv tool install hermes-agent --python 3.11", + "uv tool install hermes-agent --python 3.11 --with croniter --with httpx --with openai --with aiohttp --with websockets", ); let child = cmd.spawn().map_err(|e| format!("启动安装进程失败: {e}"))?; @@ -1904,8 +1922,8 @@ pub async fn configure_hermes( _ => String::new(), }; // Provider 字段用于稳定选择凭证来源。 - // `custom` 不写 provider 行,让 Hermes Agent 从 base_url 自动推断。 - let provider_line = if provider == "custom" || provider.is_empty() { + // `custom` 也需要显式写入,避免自定义端点被默认路由接管。 + let provider_line = if provider.is_empty() { String::new() } else { format!(" provider: {provider}\n") @@ -2026,7 +2044,7 @@ fn merge_hermes_config_yaml( // base_url_line 已包含 " base_url: xxx\n" 格式 result.push(base_url_line.trim_end().to_string()); } - // provider_line 仅在非空时写入(Hermes 不需要 provider 字段) + // provider_line 仅在非空时写入,确保模型路由稳定。 if !provider_line.is_empty() { result.push(provider_line.trim_end().to_string()); }