feat(hermes): 安装向导体验重做——实时日志、镜像配置、可操作错误与断点续行

依据上游 Hermes 源码(官方 install.sh/ps1 → uv → setup → doctor 流程)
与面板安装链路审计(18 项问题清单,见 .tmp 调研文档)实施:

- 安装输出实时流式:新增 run_install_command_streaming,uv tool/pip
  安装的 stdout/stderr 逐行实时 emit(原 wait_with_output 等进程结束
  才发日志,用户盯 3-10 分钟转圈);stderr 保留尾部 8KB 供失败诊断
- 网络与镜像 UI:安装步新增折叠区,PyPI 镜像(清华/阿里云/自定义)
  与 Git 镜像前缀直接在向导内配置并持久化(panelConfig,后端已读取)
- 失败诊断可操作:诊断关键词覆盖 PyPI 超时/SSL/代理/解析失败,
  给出镜像与代理建议;uv pip 备选安装路径同样接入诊断
- 检测门控:环境有提示项(Python 版本/Git 缺失)时停留检测页等确认,
  不再 800ms 自动跳走;全部正常仍自动进入下一步
- 断点续行:向导阶段写入 sessionStorage,重开回到上次步骤
  (以检测结果为前提校验,complete 不恢复)
- 配置步错误改内联面板,API Key 旁提示「获取模型列表」可验证 Key;
  自定义网关健康检查对 ok=false 响应视为失败

另修复模型渠道页 API 类型下拉显示 [object Object](API_TYPES 为
{value,label} 对象数组),卡片芯片同步显示可读类型名。

验证:npm build / node --test 427 通过 / cargo fmt+clippy -D warnings /
cargo test --lib 314 通过

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
晴天
2026-07-05 12:07:40 -07:00
parent dba69ee0fc
commit 1dc9151b63
5 changed files with 297 additions and 71 deletions

View File

@@ -32,6 +32,11 @@ function newChannelId() {
return `ch-${Date.now().toString(36)}-${Math.floor(Math.random() * 1e6).toString(36)}`
}
// apiType 值 → 用户可读标签(卡片与芯片展示用)
function apiTypeLabel(value) {
return API_TYPES.find(item => item.value === value)?.label || value
}
export async function render() {
const page = document.createElement('div')
page.className = 'page channels-hub-page'
@@ -176,7 +181,7 @@ function renderChannelCard(state, channel) {
</div>
<div class="mch-url" title="${attr(channel.baseUrl)}">${esc(channel.baseUrl || '-')}</div>
<div class="mch-meta">
<span class="mch-chip">${esc(channel.apiType)}</span>
<span class="mch-chip">${esc(apiTypeLabel(channel.apiType))}</span>
<span class="mch-chip">${icon('box', 11)} ${t('modelChannels.modelCount', { count: (channel.models || []).length })}</span>
<span class="mch-chip ${channel.apiKeySaved ? '' : 'mch-chip-warn'}">${icon('key', 11)} ${esc(keyInfo)}</span>
${channel.defaultModel ? `<span class="mch-chip">${icon('check', 11)} ${esc(channel.defaultModel)}</span>` : ''}
@@ -223,7 +228,7 @@ function renderEditor(state) {
<div class="form-group">
<label class="form-label" for="mch-api-type">${t('modelChannels.apiType')}</label>
<select class="form-input" id="mch-api-type">
${API_TYPES.map(v => `<option value="${attr(v)}" ${v === draft.apiType ? 'selected' : ''}>${esc(v)}</option>`).join('')}
${API_TYPES.map(item => `<option value="${attr(item.value)}" ${item.value === draft.apiType ? 'selected' : ''}>${esc(item.label)}</option>`).join('')}
</select>
</div>
<div class="form-group">