mirror of
https://github.com/qingchencloud/clawpanel.git
synced 2026-06-05 07:40:16 +08:00
chore: release v0.14.0
集中发版: 新功能(10) - 心甜Claw 引擎入口(第 3 个引擎模式) - Hermes 22 个 Provider 注册表 + 安装/仪表盘动态加载 - Hermes .env 高级编辑(拒绝触碰托管 Provider 密钥) - Hermes 会话与用量分析增强 - Hermes Dashboard 自动拉起 + Windows POSIX-only 兼容模态 - Hermes Skills 工具集面板 - 官网 Hermes Agent 黑金特色区 + 图文指南 - Boot Manifest 启动页(双语 + 错峰动画) - 官网 Markdown 阅读器图片 lightbox - Hermes Memory 概览卡 改进(9) - Hermes 仪表盘/扩展页全面本地化 - 记忆编辑大尺寸模态 - 日志下载 Web/桌面分流 - 侧边栏导航补全 - 模型备选管理 UI(PR #232) - 模型加载错误 UX 重做(错误卡 + 详情 + 重试) - .page 布局 clamp + .page-narrow - Memory 单列断点提早到 1100px - Web 模式跳过前端热更新检查 修复(12) - Gateway 启动 platforms.api_server.enabled 自修复(含 7 unit test) - Memory 页 overview 卡穿模(旧 flex 列约束 → 自然块流) - Skills 页 hero/toolsets 被压缩(flex-shrink:0) - Web 模式 Skills ReferenceError(补 _readHermesDisabledSkills) - 日志/记忆下载行为分流 - src/pages/models.js 5 处 typo - 删除 56 行 .hm-memory-* 死代码 + line-clamp 标准属性 - Dependabot rustls-webpki / postcss / rand
This commit is contained in:
@@ -10,6 +10,12 @@ import { API_TYPES, PROVIDER_PRESETS, QTCOOL, MODEL_PRESETS, fetchQtcoolModels }
|
||||
import { t } from '../lib/i18n.js'
|
||||
import { scheduleGatewayRestart, fireRestartNow, cancelPendingRestart, onRestartState } from '../lib/gateway-restart-queue.js'
|
||||
|
||||
// HTML 转义,防止错误信息中的特殊字符破坏页面或被注入
|
||||
function escapeHtml(str) {
|
||||
if (str == null) return ''
|
||||
return String(str).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"')
|
||||
}
|
||||
|
||||
export async function render() {
|
||||
const page = document.createElement('div')
|
||||
page.className = 'page'
|
||||
@@ -88,8 +94,29 @@ async function loadConfig(page, state) {
|
||||
renderDefaultBar(page, state)
|
||||
renderProviders(page, state)
|
||||
} catch (e) {
|
||||
listEl.innerHTML = '<div style="color:var(--error);padding:20px">' + t('models.configLoadFailed') + ': ' + e + '</div>'
|
||||
toast(t('models.configLoadFailed') + ': ' + e, 'error')
|
||||
console.error('[models] loadConfig failed:', e)
|
||||
const detail = escapeHtml(e?.stack || e?.message || String(e))
|
||||
const shortMsg = escapeHtml(e?.message || String(e))
|
||||
listEl.innerHTML = `
|
||||
<div class="models-load-error" style="padding:36px 20px;text-align:center;max-width:560px;margin:0 auto">
|
||||
<div style="display:inline-flex;align-items:center;justify-content:center;width:48px;height:48px;border-radius:50%;background:rgba(239,68,68,0.10);color:var(--error);margin-bottom:14px">
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round">
|
||||
<circle cx="12" cy="12" r="10"/>
|
||||
<line x1="12" y1="8" x2="12" y2="12"/>
|
||||
<line x1="12" y1="16" x2="12.01" y2="16"/>
|
||||
</svg>
|
||||
</div>
|
||||
<div style="color:var(--text-primary);font-weight:600;font-size:15px;margin-bottom:6px">${t('models.configLoadFailed')}</div>
|
||||
<div style="color:var(--text-secondary);font-size:13px;line-height:1.65;margin-bottom:18px">${t('models.configLoadFailedHint')}</div>
|
||||
<details style="text-align:left;margin-bottom:18px">
|
||||
<summary style="cursor:pointer;color:var(--text-tertiary);font-size:12px;padding:4px 0;user-select:none">${t('models.configLoadDetails')}</summary>
|
||||
<pre style="margin-top:8px;padding:10px 12px;background:var(--bg-secondary);border:1px solid var(--border-primary);border-radius:6px;font-size:11px;color:var(--text-secondary);white-space:pre-wrap;word-break:break-all;max-height:220px;overflow:auto;text-align:left">${detail}</pre>
|
||||
</details>
|
||||
<button class="btn btn-primary btn-sm" id="models-retry-load">${t('models.retryRestart')}</button>
|
||||
</div>
|
||||
`
|
||||
listEl.querySelector('#models-retry-load')?.addEventListener('click', () => loadConfig(page, state))
|
||||
toast(`${t('models.configLoadFailed')}: ${shortMsg}`, 'error')
|
||||
}
|
||||
}
|
||||
|
||||
@@ -116,7 +143,7 @@ function collectAllModels(config) {
|
||||
if (id) result.push({ provider: pk, modelId: id, full: `${pk}/${id}` })
|
||||
}
|
||||
}
|
||||
return resul
|
||||
return result
|
||||
}
|
||||
|
||||
function getApiTypeLabel(apiType) {
|
||||
@@ -1513,7 +1540,7 @@ async function handleBatchTest(section, state, providerKey) {
|
||||
const start = Date.now()
|
||||
try {
|
||||
await api.testModel(provider.baseUrl, provider.apiKey || '', modelId, provider.api || 'openai-completions')
|
||||
const elapsed = Date.now() - star
|
||||
const elapsed = Date.now() - start
|
||||
if (model && typeof model === 'object') {
|
||||
model.latency = elapsed
|
||||
model.lastTestAt = Date.now()
|
||||
@@ -1522,7 +1549,7 @@ async function handleBatchTest(section, state, providerKey) {
|
||||
}
|
||||
ok++
|
||||
} catch (e) {
|
||||
const elapsed = Date.now() - star
|
||||
const elapsed = Date.now() - start
|
||||
if (model && typeof model === 'object') {
|
||||
model.latency = null
|
||||
model.lastTestAt = Date.now()
|
||||
@@ -1554,7 +1581,7 @@ async function handleBatchTest(section, state, providerKey) {
|
||||
newBtn.classList.add('btn-secondary')
|
||||
}
|
||||
|
||||
const aborted = ctrl.abor
|
||||
const aborted = ctrl.abort
|
||||
autoSave(state)
|
||||
if (aborted) {
|
||||
toast(t('models.batchTestAborted', { ok, fail, skip: ids.length - ok - fail }), 'warning')
|
||||
@@ -1678,13 +1705,13 @@ async function testModel(btn, state, providerKey, idx) {
|
||||
const modelId = typeof model === 'string' ? model : model.id
|
||||
|
||||
btn.disabled = true
|
||||
const origText = btn.textConten
|
||||
const origText = btn.textContent
|
||||
btn.textContent = t('models.testing')
|
||||
|
||||
const start = Date.now()
|
||||
try {
|
||||
const reply = await api.testModel(provider.baseUrl, provider.apiKey || '', modelId, provider.api || 'openai-completions')
|
||||
const elapsed = Date.now() - star
|
||||
const elapsed = Date.now() - start
|
||||
// 记录到模型对象
|
||||
if (typeof model === 'object') {
|
||||
model.latency = elapsed
|
||||
@@ -1707,7 +1734,7 @@ async function testModel(btn, state, providerKey, idx) {
|
||||
toast(t('models.testOk', { model: modelId, time: (elapsed / 1000).toFixed(1), reply: reply.slice(0, 50) }), 'success')
|
||||
}
|
||||
} catch (e) {
|
||||
const elapsed = Date.now() - star
|
||||
const elapsed = Date.now() - start
|
||||
if (typeof model === 'object') {
|
||||
model.latency = null
|
||||
model.lastTestAt = Date.now()
|
||||
@@ -1717,7 +1744,7 @@ async function testModel(btn, state, providerKey, idx) {
|
||||
toast(t('models.testFail', { model: modelId, time: (elapsed / 1000).toFixed(1), error: e }), 'error', { duration: 8000 })
|
||||
} finally {
|
||||
btn.disabled = false
|
||||
btn.textContent = origTex
|
||||
btn.textContent = origText
|
||||
// 刷新卡片显示最新状态
|
||||
const page = btn.closest('.page')
|
||||
if (page) {
|
||||
|
||||
Reference in New Issue
Block a user