feat(hermes-install): diagnose network failures and add optional Git mirror (#273)

- Detect git/network failure patterns (failed to connect, could not resolve host,
  unable to access, etc.) in install/update output and append a clear hint
  pointing users to the proxy or mirror settings instead of leaving them with
  raw multi-line git stderr.
- Add optional 'Hermes Install Mirror' setting (panelConfig.gitMirror): when set,
  install/upgrade injects GIT_CONFIG_COUNT/KEY_0/VALUE_0 to rewrite
  https://github.com/ via the mirror prefix at process scope only — the user's
  global ~/.gitconfig is never touched.
- Surface the new mirror field in Settings (works for both engines), with
  zh-CN/en/zh-TW copy and a hint explaining how it interacts with the install
  flow.
This commit is contained in:
晴天
2026-05-14 01:46:55 +08:00
parent e9cd2c6059
commit 7d4a423df0
4 changed files with 214 additions and 10 deletions

View File

@@ -76,6 +76,25 @@ export default {
proxyCleared: _('网络代理已关闭', 'Network proxy disabled', '網路代理已關閉', 'ネットワークプロキシ無効化', '네트워크 프록시 비활성화됨'),
modelProxyOn: _('模型请求将走代理', 'Model requests will use proxy', '模型請求將走代理', 'モデルリクエストはプロキシを使用します', '모델 요청이 프록시를 사용합니다'),
modelProxyOff: _('模型请求已关闭代理', 'Model requests proxy disabled', '模型請求已關閉代理', 'モデルリクエストプロキシ無効化', '모델 요청 프록시 비활성화됨'),
hermesMirror: _(
'Hermes 安装镜像',
'Hermes Install Mirror',
'Hermes 安裝鏡像',
'Hermes インストールミラー',
'Hermes 설치 미러'
),
hermesMirrorHint: _(
'当 Hermes Agent 安装/升级因 GitHub 网络不通失败时,在这里填入可用的 Git 镜像前缀(如 https://ghproxy.com/)。仅作为环境变量临时生效,不会修改全局 git 配置。留空 = 直接连 GitHub。',
'When installing/upgrading Hermes Agent fails because GitHub is unreachable, set a Git mirror prefix here (e.g. https://ghproxy.com/). Applied only as a per-process environment override; your global git config is untouched. Leave empty to connect GitHub directly.',
'當 Hermes Agent 安裝/升級因 GitHub 網路不通失敗時,在這裡填入可用的 Git 鏡像前綴(如 https://ghproxy.com/)。僅作為環境變數臨時生效,不會修改全局 git 配置。留空 = 直接連 GitHub。'
),
hermesMirrorPlaceholder: _(
'https://ghproxy.com/ (留空 = 不使用镜像)',
'https://ghproxy.com/ (leave empty to disable)',
'https://ghproxy.com/ (留空 = 不使用鏡像)'
),
hermesMirrorSaved: _('Hermes 安装镜像已保存', 'Hermes install mirror saved', 'Hermes 安裝鏡像已儲存'),
hermesMirrorCleared: _('已关闭 Hermes 安装镜像', 'Hermes install mirror disabled', '已關閉 Hermes 安裝鏡像'),
gitPath: _('Git 可执行文件路径', 'Git Executable Path', 'Git 可執行檔路徑', 'Git 実行ファイルパス', 'Git 실행 파일 경로'),
gitPathHint: _('自定义 Git 可执行文件路径。留空则自动从系统 PATH 中查找。当系统找不到 Git 时,可在此手动指定完整路径。', 'Custom Git executable path. Leave empty to auto-detect from system PATH. Specify the full path here if the system cannot find Git.', '自定義 Git 可執行檔路徑。留空則自動從系統 PATH 中尋找。當系統找不到 Git 時,可在此手動指定完整路徑。', 'カスタム Git 実行ファイルパス。空欄にするとシステム PATH から自動検出します。システムが Git を見つけられない場合はここにフルパスを指定してください。'),
gitPathPlaceholder: _('留空自动检测,例如 C:\\Program Files\\Git\\cmd\\git.exe', 'Leave empty for auto-detect, e.g. C:\\Program Files\\Git\\cmd\\git.exe', '留空自動檢測,例如 /usr/local/bin/git', '空欄で自動検出、例: C:\\Program Files\\Git\\cmd\\git.exe'),

View File

@@ -113,6 +113,11 @@ export async function render() {
<div id="cli-binding-bar"><div class="stat-card loading-placeholder" style="height:48px"></div></div>
</div>`}
<div class="config-section" id="hermes-mirror-section">
<div class="config-section-title">${t('settings.hermesMirror')}</div>
<div id="hermes-mirror-bar"><div class="stat-card loading-placeholder" style="height:48px"></div></div>
</div>
<div class="config-section" id="language-section">
<div class="config-section-title">${t('settings.language')}</div>
<div id="language-bar"></div>
@@ -132,7 +137,7 @@ export async function render() {
async function loadAll(page) {
const isHermes = getActiveEngineId() === 'hermes'
const tasks = [loadProxyConfig(page), loadModelProxyConfig(page)]
const tasks = [loadProxyConfig(page), loadModelProxyConfig(page), loadHermesMirror(page)]
if (!isHermes) {
tasks.push(loadOpenclawDir(page), loadOpenclawSearchPaths(page), loadDockerDefaults(page), loadGitPath(page), loadCliBinding(page), loadRegistry(page))
}
@@ -477,6 +482,12 @@ function bindEvents(page) {
case 'reset-git-path':
await handleResetGitPath(page)
break
case 'save-hermes-mirror':
await handleSaveHermesMirror(page)
break
case 'reset-hermes-mirror':
await handleResetHermesMirror(page)
break
case 'scan-git-paths':
await handleScanGitPaths(page)
break
@@ -659,6 +670,50 @@ async function handleResetGitPath(page) {
await loadGitPath(page)
}
// ===== Hermes 安装镜像 =====
async function loadHermesMirror(page) {
const bar = page.querySelector('#hermes-mirror-bar')
if (!bar) return
try {
const cfg = await api.readPanelConfig()
const value = cfg?.gitMirror || ''
bar.innerHTML = `
<div class="stat-card" style="padding:16px">
<p style="font-size:var(--font-size-xs);color:var(--text-tertiary);margin-bottom:12px;line-height:1.5">${t('settings.hermesMirrorHint')}</p>
<div style="display:flex;gap:8px;align-items:center;flex-wrap:wrap">
<input class="input" data-name="hermes-mirror" value="${escapeHtml(value)}" placeholder="${t('settings.hermesMirrorPlaceholder')}" style="flex:1;min-width:240px">
<button class="btn btn-primary btn-sm" data-action="save-hermes-mirror">${t('common.save')}</button>
${value ? `<button class="btn btn-secondary btn-sm" data-action="reset-hermes-mirror">${t('settings.resetDefault')}</button>` : ''}
</div>
</div>`
} catch (e) {
bar.innerHTML = `<div class="stat-card" style="padding:16px;color:var(--error)">${e}</div>`
}
}
async function handleSaveHermesMirror(page) {
const input = page.querySelector('[data-name="hermes-mirror"]')
const value = (input?.value || '').trim()
const cfg = await api.readPanelConfig()
if (value) {
cfg.gitMirror = value
} else {
delete cfg.gitMirror
}
await api.writePanelConfig(cfg)
toast(value ? t('settings.hermesMirrorSaved') : t('settings.hermesMirrorCleared'), 'success')
await loadHermesMirror(page)
}
async function handleResetHermesMirror(page) {
const cfg = await api.readPanelConfig()
delete cfg.gitMirror
await api.writePanelConfig(cfg)
toast(t('settings.hermesMirrorCleared'), 'success')
await loadHermesMirror(page)
}
// ===== CLI 绑定 =====
async function loadCliBinding(page) {