fix(hermes): restore gateway message sending dependencies

Hermes Gateway 能启动但消息发送链路仍可能不可用:工具安装环境缺少运行时依赖,
同时自定义端点场景下 provider 字段可能被省略,导致消息路由继续落到错误 provider。

## 补齐运行时依赖
- uv tool install 固定带上 HTTP 客户端依赖
- 补齐 OpenAI SDK 依赖
- 补齐 Gateway HTTP server 所需 aiohttp
- 补齐 browser/dialog 等工具链会触发的 websocket 依赖
- 安装日志同步展示完整安装参数,便于用户排查环境问题

## 明确 custom provider
- 自定义 base_url 时显式写入 provider: custom
- 如果 model 段已有 provider,则覆盖为 custom
- 如果 model 段缺少 provider,则补写 provider: custom
- 继续保留 OPENAI_API_KEY alias 自愈,确保辅助客户端和主流程读取同一份凭证

## 范围
- 桌面 Tauri 安装与配置自愈逻辑
- Web dev API 安装与配置自愈逻辑
This commit is contained in:
晴天
2026-05-15 17:32:56 +08:00
parent 583f5401ac
commit 0b4ef11971
2 changed files with 52 additions and 26 deletions

View File

@@ -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)

View File

@@ -441,27 +441,21 @@ fn sanitize_hermes_openrouter_custom_mismatch() -> Result<bool, String> {
} 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<bool, String> {
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());
}