mirror of
https://github.com/qingchencloud/clawpanel.git
synced 2026-05-31 05:10:14 +08:00
feat: OpenClaw 4.9 全面适配 (v0.12.0)
- 推荐内核统一升级至 2026.4.9 / 2026.4.9-zh.2 - standalone 安装兼容 edition 格式 latest.json + openclaw-zh- 文件名前缀 - standalone 三级降级: R2 CDN → GitHub Releases → npm - pre_install_cleanup 所有命令加 10s 超时保护(修复安装卡死) - npm uninstall 加 30s 超时保护 - wmic 全部迁移到 PowerShell(兼容 Windows 11) - standalone 下载增加文字进度显示
This commit is contained in:
@@ -1289,10 +1289,14 @@ function renderSessionList(sessions) {
|
||||
const msgCount = s.messageCount || s.messages || 0
|
||||
const agentId = parseSessionAgent(key)
|
||||
const displayLabel = getDisplayLabel(key) || label
|
||||
const cpCount = s.compactionCheckpointCount || 0
|
||||
return `<div class="chat-session-card${active}" data-key="${escapeAttr(key)}">
|
||||
<div class="chat-session-card-header">
|
||||
<span class="chat-session-label" title="${t('chat.doubleClickRename')}">${escapeAttr(displayLabel)}</span>
|
||||
<button class="chat-session-del" data-del="${escapeAttr(key)}" title="${t('common.delete')}">×</button>
|
||||
<div style="display:flex;gap:2px;align-items:center">
|
||||
${cpCount > 0 ? `<button class="chat-session-del" data-compaction="${escapeAttr(key)}" title="${t('chat.compactionHistory')}" style="color:var(--text-tertiary);font-size:11px">⟳${cpCount}</button>` : ''}
|
||||
<button class="chat-session-del" data-del="${escapeAttr(key)}" title="${t('common.delete')}">×</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="chat-session-card-meta">
|
||||
${agentId && agentId !== 'main' ? `<span class="chat-session-agent">${escapeAttr(agentId)}</span>` : ''}
|
||||
@@ -1303,6 +1307,8 @@ function renderSessionList(sessions) {
|
||||
}).join('')
|
||||
|
||||
_sessionListEl.onclick = (e) => {
|
||||
const cpBtn = e.target.closest('[data-compaction]')
|
||||
if (cpBtn) { e.stopPropagation(); showCompactionHistory(cpBtn.dataset.compaction); return }
|
||||
const delBtn = e.target.closest('[data-del]')
|
||||
if (delBtn) { e.stopPropagation(); deleteSession(delBtn.dataset.del); return }
|
||||
const item = e.target.closest('[data-key]')
|
||||
@@ -1433,6 +1439,96 @@ async function deleteSession(key) {
|
||||
}
|
||||
}
|
||||
|
||||
// ===== 4.9: Sessions Compaction History =====
|
||||
async function showCompactionHistory(key) {
|
||||
if (!key || !wsClient.gatewayReady) return
|
||||
const label = getDisplayLabel(key)
|
||||
toast(t('chat.compactionLoading'), 'info')
|
||||
try {
|
||||
const result = await wsClient.sessionsCompactionList(key)
|
||||
const checkpoints = result?.checkpoints || []
|
||||
if (!checkpoints.length) {
|
||||
toast(t('chat.compactionEmpty'), 'info')
|
||||
return
|
||||
}
|
||||
const listHtml = checkpoints.map((cp, idx) => {
|
||||
const id = cp.id || cp.checkpointId || `cp-${idx}`
|
||||
const ts = cp.timestamp || cp.createdAt || 0
|
||||
const timeStr = ts ? new Date(typeof ts === 'number' && ts < 1e12 ? ts * 1000 : ts).toLocaleString() : '—'
|
||||
const tokensBefore = cp.tokensBefore ?? '—'
|
||||
const tokensAfter = cp.tokensAfter ?? '—'
|
||||
return `<div style="padding:10px 0;border-bottom:1px solid var(--border-primary);display:flex;justify-content:space-between;align-items:center;gap:8px">
|
||||
<div style="min-width:0;flex:1">
|
||||
<div style="font-size:13px;font-weight:500">#${idx + 1} · ${escapeAttr(timeStr)}</div>
|
||||
<div style="font-size:12px;color:var(--text-tertiary);margin-top:2px">${tokensBefore} → ${tokensAfter} tokens</div>
|
||||
</div>
|
||||
<div style="display:flex;gap:4px;flex-shrink:0">
|
||||
<button class="btn btn-sm btn-secondary" data-cp-branch="${escapeAttr(id)}">${t('chat.compactionBranch')}</button>
|
||||
<button class="btn btn-sm btn-warning" data-cp-restore="${escapeAttr(id)}">${t('chat.compactionRestore')}</button>
|
||||
</div>
|
||||
</div>`
|
||||
}).join('')
|
||||
|
||||
const overlay = document.createElement('div')
|
||||
overlay.className = 'modal-overlay'
|
||||
overlay.innerHTML = `<div class="modal" style="max-width:520px;max-height:80vh;overflow:auto">
|
||||
<div class="modal-header"><h3>${escapeAttr(t('chat.compactionHistory'))}: ${escapeAttr(label)}</h3></div>
|
||||
<div class="modal-body" style="padding:0 var(--space-md)">${listHtml}</div>
|
||||
<div class="modal-footer"><button class="btn btn-secondary" data-cp-close>${t('common.close')}</button></div>
|
||||
</div>`
|
||||
document.body.appendChild(overlay)
|
||||
|
||||
overlay.addEventListener('click', async (e) => {
|
||||
if (e.target === overlay || e.target.closest('[data-cp-close]')) {
|
||||
overlay.remove()
|
||||
return
|
||||
}
|
||||
const branchBtn = e.target.closest('[data-cp-branch]')
|
||||
if (branchBtn) {
|
||||
branchBtn.disabled = true
|
||||
try {
|
||||
const res = await wsClient.sessionsCompactionBranch(key, branchBtn.dataset.cpBranch)
|
||||
toast(t('chat.compactionBranchDone'), 'success')
|
||||
overlay.remove()
|
||||
if (res?.key) void switchSession(res.key)
|
||||
else refreshSessionList()
|
||||
} catch (err) {
|
||||
toast(`${t('common.operationFailed')}: ${err.message}`, 'error')
|
||||
branchBtn.disabled = false
|
||||
}
|
||||
return
|
||||
}
|
||||
const restoreBtn = e.target.closest('[data-cp-restore]')
|
||||
if (restoreBtn) {
|
||||
const yes = await showConfirm(t('chat.compactionConfirmRestore'))
|
||||
if (!yes) return
|
||||
restoreBtn.disabled = true
|
||||
try {
|
||||
await wsClient.sessionsCompactionRestore(key, restoreBtn.dataset.cpRestore)
|
||||
toast(t('chat.compactionRestoreDone'), 'success')
|
||||
overlay.remove()
|
||||
if (key === _sessionKey) {
|
||||
clearMessages()
|
||||
_lastHistoryHash = ''
|
||||
loadHistory()
|
||||
}
|
||||
refreshSessionList()
|
||||
} catch (err) {
|
||||
toast(`${t('common.operationFailed')}: ${err.message}`, 'error')
|
||||
restoreBtn.disabled = false
|
||||
}
|
||||
}
|
||||
})
|
||||
} catch (e) {
|
||||
const msg = String(e?.message || e || '').toLowerCase()
|
||||
if (msg.includes('unknown method') || msg.includes('not found') || msg.includes('unsupported')) {
|
||||
toast(t('chat.compactionUnsupported'), 'warning')
|
||||
} else {
|
||||
toast(`${t('common.operationFailed')}: ${e.message}`, 'error')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function resetCurrentSession() {
|
||||
if (!_sessionKey) return
|
||||
const label = getDisplayLabel(_sessionKey)
|
||||
|
||||
@@ -6,6 +6,7 @@ import { api } from '../lib/tauri-api.js'
|
||||
import { toast } from '../components/toast.js'
|
||||
import { icon } from '../lib/icons.js'
|
||||
import { t } from '../lib/i18n.js'
|
||||
import { wsClient } from '../lib/ws-client.js'
|
||||
|
||||
let _page = null, _config = null, _dirty = false
|
||||
|
||||
@@ -369,10 +370,74 @@ function renderApprovals(el) {
|
||||
</div>
|
||||
${toggleRow('approvals-forwardExec', t('communication.approvalsForwardExec'), t('communication.approvalsForwardExecHint'), !!a.enabled)}
|
||||
</div>
|
||||
<div class="config-section" style="margin-top:var(--space-lg)">
|
||||
<div class="config-section-title" style="display:flex;justify-content:space-between;align-items:center">
|
||||
<span>${t('communication.pendingApprovals')}</span>
|
||||
<button class="btn btn-sm btn-secondary" id="btn-refresh-approvals">${t('communication.refreshApprovals')}</button>
|
||||
</div>
|
||||
<div id="approval-queue" style="margin-top:var(--space-sm)">
|
||||
<div class="form-hint">${t('communication.approvalsLoadingQueue')}</div>
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
el.querySelectorAll('input, select').forEach(inp => {
|
||||
inp.addEventListener('change', markDirty)
|
||||
})
|
||||
el.querySelector('#btn-refresh-approvals')?.addEventListener('click', () => loadApprovalQueue(el))
|
||||
loadApprovalQueue(el)
|
||||
}
|
||||
|
||||
async function loadApprovalQueue(el) {
|
||||
const container = (el || _page)?.querySelector('#approval-queue')
|
||||
if (!container) return
|
||||
if (!wsClient.connected || !wsClient.gatewayReady) {
|
||||
container.innerHTML = `<div class="form-hint">${esc(t('communication.approvalsGwNotReady'))}</div>`
|
||||
return
|
||||
}
|
||||
container.innerHTML = `<div class="form-hint">${esc(t('communication.approvalsLoadingQueue'))}</div>`
|
||||
let execItems = [], pluginItems = [], unsupported = false
|
||||
try {
|
||||
const [execRes, pluginRes] = await Promise.allSettled([
|
||||
wsClient.execApprovalList(),
|
||||
wsClient.pluginApprovalList(),
|
||||
])
|
||||
if (execRes.status === 'fulfilled') execItems = execRes.value?.approvals || execRes.value?.items || []
|
||||
else {
|
||||
const msg = String(execRes.reason?.message || '').toLowerCase()
|
||||
if (msg.includes('unknown method') || msg.includes('not found')) unsupported = true
|
||||
}
|
||||
if (pluginRes.status === 'fulfilled') pluginItems = pluginRes.value?.approvals || pluginRes.value?.items || []
|
||||
} catch {}
|
||||
|
||||
if (unsupported) {
|
||||
container.innerHTML = `<div class="form-hint" style="color:var(--text-tertiary)">${esc(t('communication.approvalsUnsupported'))}</div>`
|
||||
return
|
||||
}
|
||||
|
||||
const allItems = [
|
||||
...execItems.map(i => ({ ...i, _type: 'exec' })),
|
||||
...pluginItems.map(i => ({ ...i, _type: 'plugin' })),
|
||||
]
|
||||
|
||||
if (!allItems.length) {
|
||||
container.innerHTML = `<div class="form-hint">${esc(t('communication.approvalsQueueEmpty'))}</div>`
|
||||
return
|
||||
}
|
||||
|
||||
container.innerHTML = allItems.map(item => {
|
||||
const id = item.id || item.approvalId || ''
|
||||
const type = item._type === 'plugin' ? 'Plugin' : 'Exec'
|
||||
const cmd = item.command || item.description || item.name || id
|
||||
const status = item.status || 'pending'
|
||||
const ts = item.createdAt || item.timestamp || 0
|
||||
const timeStr = ts ? new Date(typeof ts === 'number' && ts < 1e12 ? ts * 1000 : ts).toLocaleString() : ''
|
||||
return `<div style="padding:10px 0;border-bottom:1px solid var(--border-primary);display:flex;justify-content:space-between;align-items:center;gap:8px">
|
||||
<div style="min-width:0;flex:1">
|
||||
<div style="font-size:13px"><span class="badge" style="font-size:11px;margin-right:4px">${esc(type)}</span>${esc(cmd)}</div>
|
||||
<div style="font-size:12px;color:var(--text-tertiary);margin-top:2px">${esc(status)}${timeStr ? ' · ' + timeStr : ''}</div>
|
||||
</div>
|
||||
</div>`
|
||||
}).join('')
|
||||
}
|
||||
|
||||
function collectApprovals() {
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
import { api } from '../lib/tauri-api.js'
|
||||
import { toast } from '../components/toast.js'
|
||||
import { t } from '../lib/i18n.js'
|
||||
import { wsClient } from '../lib/ws-client.js'
|
||||
|
||||
let _loadSeq = 0
|
||||
let _selectedAgentId = null // null = default (main)
|
||||
@@ -243,7 +244,12 @@ async function handleInfo(page, name) {
|
||||
detail.innerHTML = `<div class="form-hint" style="margin-top:var(--space-md)">${t('skills.loadingDetail')}</div>`
|
||||
detail.scrollIntoView({ behavior: 'smooth', block: 'nearest' })
|
||||
try {
|
||||
const skill = await api.skillsInfo(name, _selectedAgentId)
|
||||
let skill = null
|
||||
// 优先 Gateway RPC(可获取 ClawHub 远程详情),回退 Tauri 本地
|
||||
if (wsClient.connected && wsClient.gatewayReady) {
|
||||
try { skill = await wsClient.skillsDetail(name) } catch {}
|
||||
}
|
||||
if (!skill) skill = await api.skillsInfo(name, _selectedAgentId)
|
||||
const s = skill || {}
|
||||
const reqs = s.requirements || {}
|
||||
const miss = s.missing || {}
|
||||
@@ -368,10 +374,20 @@ async function handleStoreSearch(page) {
|
||||
renderStoreItems(results, filtered)
|
||||
return
|
||||
}
|
||||
// 没有索引时走服务端搜索
|
||||
// 没有索引时走服务端搜索(优先 Gateway RPC,回退 Tauri)
|
||||
results.innerHTML = `<div class="form-hint" style="padding:var(--space-sm)">${t('skills.searching')}</div>`
|
||||
try {
|
||||
const items = await api.skillhubSearch(input.value.trim())
|
||||
let items
|
||||
if (wsClient.connected && wsClient.gatewayReady) {
|
||||
try {
|
||||
const res = await wsClient.skillsSearch(input.value.trim(), 30)
|
||||
items = res?.results || []
|
||||
} catch {
|
||||
items = await api.skillhubSearch(input.value.trim())
|
||||
}
|
||||
} else {
|
||||
items = await api.skillhubSearch(input.value.trim())
|
||||
}
|
||||
renderStoreItems(results, items)
|
||||
} catch (e) {
|
||||
results.innerHTML = `<div style="color:var(--error);padding:var(--space-sm)">${t('skills.searchFailed')}: ${esc(e?.message || e)}</div>`
|
||||
|
||||
Reference in New Issue
Block a user