mirror of
https://github.com/qingchencloud/clawpanel.git
synced 2026-05-29 04:10:00 +08:00
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:
@@ -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)
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user