feat: prepare v0.18.0 release

This commit is contained in:
晴天
2026-06-06 18:32:37 +08:00
parent f340b64028
commit de1531d111
57 changed files with 1591 additions and 453 deletions

View File

@@ -9,12 +9,16 @@ import { toast } from './toast.js'
const APP_VERSION = typeof __APP_VERSION__ !== 'undefined' ? __APP_VERSION__ : '0.0.0'
import { t, getLang, setLang, getAvailableLangs } from '../lib/i18n.js'
import { isFeatureAvailable } from '../lib/feature-gates.js'
import { getKernelSnapshot } from '../lib/kernel.js'
import { getKernelSnapshot, recommendedIsNewer } from '../lib/kernel.js'
import { triggerKernelUpgrade } from '../lib/kernel-upgrade.js'
import { getActiveEngine, getActiveEngineId, listEngines, needsInitialEngineChoice, isEngineSetupDeferred, switchEngine, onEngineChange } from '../lib/engine-manager.js'
// 当用户点 "暂时不升级" 时,本地会话内不再显示升级提示
const SS_DISMISSED_KERNEL_UPGRADE = 'clawpanel_kernel_upgrade_dismissed'
const KERNEL_POLICY_TTL = 5 * 60 * 1000
let _kernelPolicyInfo = null
let _kernelPolicyFetchedAt = 0
let _kernelPolicyLoading = false
function NAV_ITEMS_FULL() { return [
{
@@ -282,6 +286,7 @@ export function renderSidebar(el) {
el.innerHTML = html
window.dispatchEvent(new CustomEvent('clawpanel:site-message-launcher-mounted'))
_ensureKernelPolicyInfo(el)
// 应用折叠态(桌面端)
_setDesktopSidebarCollapsed(collapsed)
@@ -422,6 +427,34 @@ export function renderSidebar(el) {
function _escSidebar(s) { return String(s || '').replace(/</g, '&lt;').replace(/>/g, '&gt;') }
function _kernelPolicyTarget(snap) {
return _kernelPolicyInfo?.recommended || snap?.target || ''
}
function _isRunningGatewayBelowTarget(snap) {
if (!snap?.version) return false
const target = _kernelPolicyTarget(snap)
return target ? recommendedIsNewer(target, snap.version) : !snap.isLatest
}
function _ensureKernelPolicyInfo(el) {
const snap = getKernelSnapshot()
if (getActiveEngineId() !== 'openclaw' || !snap?.version) return
const now = Date.now()
if (_kernelPolicyLoading) return
if (_kernelPolicyInfo && now - _kernelPolicyFetchedAt < KERNEL_POLICY_TTL) return
_kernelPolicyLoading = true
api.getVersionInfo()
.then(info => {
_kernelPolicyInfo = info || null
_kernelPolicyFetchedAt = Date.now()
if (el?.isConnected) renderSidebar(el)
})
.catch(() => {})
.finally(() => { _kernelPolicyLoading = false })
}
/**
* 渲染"内核可升级"卡片。
*
@@ -429,7 +462,7 @@ function _escSidebar(s) { return String(s || '').replace(/</g, '&lt;').replace(/
* - 当前引擎是 openclaw
* - 已成功握手 Gatewaysnapshot 有 version
* - 高于硬地板(< floor 由 floor-blocker 接管)
* - 低于推荐目标!isLatest
* - 运行中的 Gateway 低于推荐目标
* - 用户未在本会话中点击过 "暂不升级"
*
* 不满足任何一条返回空串。
@@ -441,13 +474,13 @@ function _renderKernelUpgradeHint() {
const snap = getKernelSnapshot()
if (!snap || !snap.version) return ''
if (!snap.aboveFloor) return '' // floor-blocker 处理
if (snap.isLatest) return '' // 已经是推荐目标
if (!_isRunningGatewayBelowTarget(snap)) return '' // 运行中的 Gateway 已经达到推荐目标
const arrowIcon = '<svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2"><polyline points="6 9 12 15 18 9"/></svg>'
const sparkIcon = '<svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 2L13.5 8.5 20 10l-6.5 1.5L12 18l-1.5-6.5L4 10l6.5-1.5z"/></svg>'
const fromLabel = snap.versionLabel || snap.version
const toLabel = snap.target || ''
const toLabel = _kernelPolicyTarget(snap)
return `
<div class="kernel-upgrade-hint" id="kernel-upgrade-hint" role="button" tabindex="0">

View File

@@ -65,7 +65,7 @@ export function normalizeSiteMessagePayload(payload = {}) {
export function openSiteMessageCenter({ force = false, tab = null } = {}) {
const visible = getVisibleMessages()
if (!force && !visible.notifications.length && !visible.announcements.length) return
_activeTab = tab || (visible.notifications.length ? 'notifications' : 'announcements')
_activeTab = tab || getPreferredTab()
renderModal()
}
@@ -81,9 +81,11 @@ function bindLaunchers() {
}
function updateLauncherBadge() {
const count = getVisibleMessages().notifications.length + getVisibleMessages().announcements.length
const visible = getVisibleMessages()
const count = isClosedToday() ? 0 : visible.notifications.length + visible.announcements.length
document.querySelectorAll(LAUNCHER_SELECTOR).forEach((launcher) => {
launcher.classList.toggle('has-unread', count > 0)
launcher.classList.toggle('is-muted-today', isClosedToday())
const badge = launcher.querySelector('.site-message-tool-badge, .site-message-fab-badge')
if (badge) badge.textContent = count > 9 ? '9+' : String(count || '')
})
@@ -94,6 +96,12 @@ function renderModal() {
if (old) old.remove()
const visible = getVisibleMessages()
const dismissed = getDismissedMessages()
const displayCounts = {
notifications: visible.notifications.length + dismissed.notifications.length,
announcements: visible.announcements.length + dismissed.announcements.length,
}
const activeVisibleCount = visible[_activeTab]?.length || 0
const overlay = document.createElement('div')
overlay.id = 'site-message-overlay'
@@ -107,21 +115,25 @@ function renderModal() {
<span class="site-message-title-icon" aria-hidden="true">${ICON_BELL}</span>
<div>
<h2>${t('siteMessages.title')}</h2>
<p>${formatSummary(visible)}</p>
<p>${formatSummary(displayCounts)}</p>
</div>
</div>
<div class="site-message-tabs" role="tablist">
${renderTab('notifications', t('siteMessages.notifications'), ICON_BELL, visible.notifications.length)}
${renderTab('announcements', t('siteMessages.announcements'), ICON_SEND, visible.announcements.length)}
${renderTab('notifications', t('siteMessages.notifications'), ICON_BELL, displayCounts.notifications)}
${renderTab('announcements', t('siteMessages.announcements'), ICON_SEND, displayCounts.announcements)}
</div>
<button class="site-message-close" type="button" title="${t('common.close')}">${ICON_X}</button>
</header>
<div class="site-message-body">
${_activeTab === 'notifications' ? renderNotifications(visible.notifications) : renderAnnouncements(visible.announcements)}
${_activeTab === 'notifications'
? renderNotifications(visible.notifications, dismissed.notifications)
: renderAnnouncements(visible.announcements, dismissed.announcements)}
</div>
<footer class="site-message-footer">
<button class="btn btn-secondary btn-sm" type="button" data-site-message-today>${t('siteMessages.closeToday')}</button>
<button class="btn btn-primary btn-sm" type="button" data-site-message-dismiss>${t('siteMessages.closeCurrent')}</button>
${activeVisibleCount
? `<button class="btn btn-primary btn-sm" type="button" data-site-message-dismiss>${getDismissButtonLabel()}</button>`
: `<button class="btn btn-primary btn-sm" type="button" data-site-message-close-current>${t('common.close')}</button>`}
</footer>
</section>
`
@@ -142,8 +154,9 @@ function renderTab(tab, label, icon, count) {
`
}
function renderNotifications(items) {
function renderNotifications(items, dismissedItems = []) {
if (!items.length) {
if (dismissedItems.length) return renderDismissedState('notifications', dismissedItems.length)
return `
<div class="site-message-empty">
<span class="site-message-empty-icon" aria-hidden="true">${ICON_BELL}</span>
@@ -177,8 +190,9 @@ function renderNotifications(items) {
`
}
function renderAnnouncements(items) {
function renderAnnouncements(items, dismissedItems = []) {
if (!items.length) {
if (dismissedItems.length) return renderDismissedState('announcements', dismissedItems.length)
return `
<div class="site-message-empty">
<span class="site-message-empty-icon" aria-hidden="true">${ICON_SEND}</span>
@@ -213,6 +227,18 @@ function renderAnnouncements(items) {
`
}
function renderDismissedState(tab, count) {
const icon = tab === 'notifications' ? ICON_BELL : ICON_SEND
return `
<div class="site-message-empty site-message-dismissed-empty">
<span class="site-message-empty-icon" aria-hidden="true">${icon}</span>
<strong>${t('siteMessages.dismissedTitle')}</strong>
<p>${t('siteMessages.dismissedHint', { count })}</p>
<button class="btn btn-secondary btn-sm" type="button" data-site-message-restore>${t('siteMessages.restoreDismissed')}</button>
</div>
`
}
function renderMessageCta(item, prominent = false) {
if (!item.ctaText || !item.ctaUrl) return ''
return `<a class="${prominent ? 'site-message-card-cta' : 'site-message-inline-cta'}" href="${escapeAttr(item.ctaUrl)}" target="_blank" rel="noopener">${escapeHtml(item.ctaText)}</a>`
@@ -223,12 +249,19 @@ function bindModalEvents(overlay) {
overlay.querySelector('[data-site-message-today]')?.addEventListener('click', () => {
localStorage.setItem(TODAY_CLOSE_KEY, todayKey())
closeModal()
updateLauncherBadge()
})
overlay.querySelector('[data-site-message-dismiss]')?.addEventListener('click', () => {
dismissItems(getVisibleMessages()[_activeTab])
closeModal()
updateLauncherBadge()
})
overlay.querySelector('[data-site-message-close-current]')?.addEventListener('click', closeModal)
overlay.querySelector('[data-site-message-restore]')?.addEventListener('click', () => {
restoreItems(_messages[_activeTab])
renderModal()
updateLauncherBadge()
})
overlay.querySelectorAll('[data-site-message-tab]').forEach((btn) => {
btn.addEventListener('click', () => {
_activeTab = btn.dataset.siteMessageTab || 'notifications'
@@ -255,12 +288,23 @@ function dismissItems(items) {
}
}
function restoreItems(items) {
for (const item of items || []) {
const key = dismissStorageKey(item)
if (key) localStorage.removeItem(key)
}
}
function shouldAutoOpen() {
if (localStorage.getItem(TODAY_CLOSE_KEY) === todayKey()) return false
if (isClosedToday()) return false
const visible = getVisibleMessages()
return visible.notifications.length > 0 || visible.announcements.length > 0
}
function isClosedToday() {
return localStorage.getItem(TODAY_CLOSE_KEY) === todayKey()
}
function getVisibleMessages() {
return {
notifications: _messages.notifications.filter(item => !isDismissed(item)),
@@ -268,6 +312,27 @@ function getVisibleMessages() {
}
}
function getDismissedMessages() {
return {
notifications: _messages.notifications.filter(isDismissed),
announcements: _messages.announcements.filter(isDismissed),
}
}
function getPreferredTab() {
const visible = getVisibleMessages()
if (visible.notifications.length) return 'notifications'
if (visible.announcements.length) return 'announcements'
const dismissed = getDismissedMessages()
if (dismissed.notifications.length) return 'notifications'
return 'announcements'
}
function getDismissButtonLabel() {
if (_activeTab === 'notifications') return t('siteMessages.dismissCurrentNotifications')
return t('siteMessages.dismissCurrentAnnouncements')
}
function normalizePayload(payload = {}) {
const notifications = []
const announcements = []