mirror of
https://github.com/qingchencloud/clawpanel.git
synced 2026-07-14 08:54:30 +08:00
发布 0.15.0: - 新增内核版本兼容层、特性门控、低版本阻断和升级提示 - 新增 PATH 中 OpenClaw CLI 冲突检测、隔离与恢复 - 修复 Hermes Gateway loopback 自动拉起与 /v1/runs 诊断 - 修复 standalone 一键安装包在 About/仪表盘显示未知版本 - 同步 OpenClaw 2026.5.6 推荐版本和热更新 minAppVersion - 补齐本地 JS/Rust 测试与发布前检查说明 验证: - npm run build - node --test tests/*.test.js - node --check src/scripts JS 文件 - cargo fmt --all -- --check - cargo check - cargo clippy --all-targets -- -D warnings - cargo test
145 lines
4.7 KiB
JavaScript
145 lines
4.7 KiB
JavaScript
/**
|
||
* 内核版本徽章组件
|
||
*
|
||
* 在 sidebar 底部 / 设置页 / About 页显示当前对接的内核版本以及已启用特性数。
|
||
* 用法:
|
||
* import { mountKernelBadge } from './components/kernel-badge.js'
|
||
* const el = mountKernelBadge(container, { compact: true })
|
||
*
|
||
* 自动订阅 onKernelChange,状态变化时刷新;container 销毁时调用 el._unsub() 清理。
|
||
*/
|
||
import { onKernelChange, getKernelSnapshot } from '../lib/kernel.js'
|
||
import { FEATURE_CATALOG } from '../lib/feature-catalog.js'
|
||
import { t } from '../lib/i18n.js'
|
||
|
||
/**
|
||
* 在指定容器中挂载内核徽章。
|
||
*
|
||
* @param {HTMLElement} container 父容器
|
||
* @param {Object} [opts]
|
||
* @param {boolean} [opts.compact=false] 紧凑模式:仅显示版本号 + 角标
|
||
* @returns {HTMLElement & { _unsub?: () => void }}
|
||
*/
|
||
export function mountKernelBadge(container, opts = {}) {
|
||
const compact = !!opts.compact
|
||
const el = document.createElement('div')
|
||
el.className = compact ? 'kernel-badge kernel-badge-compact' : 'kernel-badge'
|
||
|
||
function update(snap) {
|
||
if (!snap || !snap.version) {
|
||
el.innerHTML = compact
|
||
? `<span class="kernel-badge-version">${escapeHtml(t('kernel.badge.unknown'))}</span>`
|
||
: `<div class="kernel-badge-row"><span class="kernel-badge-label">${escapeHtml(t('kernel.badge.currentKernel'))}</span><span class="kernel-badge-version">${escapeHtml(t('kernel.badge.unknown'))}</span></div>`
|
||
return
|
||
}
|
||
|
||
const total = Object.values(FEATURE_CATALOG).filter(f => f.engine === snap.engine).length
|
||
const enabled = snap.features.size
|
||
|
||
let stateBadge = ''
|
||
if (!snap.aboveFloor) {
|
||
stateBadge = `<span class="kernel-badge-state warn">${escapeHtml(t('kernel.badge.belowTarget'))}</span>`
|
||
} else if (snap.isLatest) {
|
||
stateBadge = `<span class="kernel-badge-state ok">${escapeHtml(t('kernel.badge.latest'))}</span>`
|
||
} else if (!snap.isLatest && snap.target) {
|
||
stateBadge = `<span class="kernel-badge-state info">${escapeHtml(t('kernel.badge.belowTarget'))}</span>`
|
||
}
|
||
|
||
if (compact) {
|
||
el.innerHTML = `
|
||
<span class="kernel-badge-version">${escapeHtml(snap.versionLabel || snap.version)}</span>
|
||
${stateBadge}
|
||
`
|
||
el.title = t('kernel.badge.featuresEnabled', { enabled, total })
|
||
} else {
|
||
el.innerHTML = `
|
||
<div class="kernel-badge-row">
|
||
<span class="kernel-badge-label">${escapeHtml(t('kernel.badge.currentKernel'))}</span>
|
||
<span class="kernel-badge-version">${escapeHtml(snap.versionLabel || snap.version)}</span>
|
||
${stateBadge}
|
||
</div>
|
||
<div class="kernel-badge-row kernel-badge-features">
|
||
${escapeHtml(t('kernel.badge.featuresEnabled', { enabled, total }))}
|
||
</div>
|
||
`
|
||
}
|
||
}
|
||
|
||
// 立即渲染当前快照(可能为 null)
|
||
update(getKernelSnapshot())
|
||
|
||
// 订阅变化
|
||
const unsub = onKernelChange(update)
|
||
el._unsub = unsub
|
||
|
||
injectStyles()
|
||
container.appendChild(el)
|
||
return el
|
||
}
|
||
|
||
function escapeHtml(s) {
|
||
return String(s ?? '')
|
||
.replace(/&/g, '&')
|
||
.replace(/</g, '<')
|
||
.replace(/>/g, '>')
|
||
.replace(/"/g, '"')
|
||
}
|
||
|
||
let _stylesInjected = false
|
||
function injectStyles() {
|
||
if (_stylesInjected) return
|
||
_stylesInjected = true
|
||
const css = `
|
||
.kernel-badge {
|
||
display: flex; flex-direction: column; gap: 4px;
|
||
padding: 8px 12px;
|
||
border-radius: 8px;
|
||
background: var(--bg-tertiary, #f3f4f6);
|
||
border: 1px solid var(--border-color, #e5e7eb);
|
||
font-size: 12px;
|
||
line-height: 1.4;
|
||
}
|
||
.kernel-badge-compact {
|
||
flex-direction: row;
|
||
align-items: center;
|
||
gap: 6px;
|
||
padding: 4px 10px;
|
||
background: transparent;
|
||
border: 1px solid var(--border-color, #e5e7eb);
|
||
cursor: default;
|
||
}
|
||
.kernel-badge-row {
|
||
display: flex; align-items: center; gap: 8px;
|
||
flex-wrap: wrap;
|
||
}
|
||
.kernel-badge-label {
|
||
color: var(--text-tertiary, #888);
|
||
font-size: 11px;
|
||
}
|
||
.kernel-badge-version {
|
||
font-family: var(--font-mono, monospace);
|
||
font-weight: 600;
|
||
color: var(--text-primary, #111);
|
||
}
|
||
.kernel-badge-features {
|
||
color: var(--text-secondary, #555);
|
||
font-size: 11px;
|
||
}
|
||
.kernel-badge-state {
|
||
font-size: 10px;
|
||
font-weight: 600;
|
||
padding: 1px 6px;
|
||
border-radius: 10px;
|
||
text-transform: uppercase;
|
||
letter-spacing: 0.04em;
|
||
}
|
||
.kernel-badge-state.ok { color: var(--success, #16a34a); background: rgba(34,197,94,.12); }
|
||
.kernel-badge-state.info { color: var(--info, #2563eb); background: rgba(59,130,246,.12); }
|
||
.kernel-badge-state.warn { color: var(--warning, #d97706); background: rgba(245,158,11,.16); }
|
||
`
|
||
const style = document.createElement('style')
|
||
style.setAttribute('data-origin', 'kernel-badge')
|
||
style.textContent = css
|
||
document.head.appendChild(style)
|
||
}
|