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)