mirror of
https://github.com/qingchencloud/clawpanel.git
synced 2026-05-06 20:02:49 +08:00
perf: ARM设备性能优化 — in-flight请求去重+后端缓存+仪表盘轮询降频+R2 CDN加速
This commit is contained in:
@@ -122,6 +122,126 @@ function loadVersionPolicy() {
|
||||
}
|
||||
}
|
||||
|
||||
function r2Config() {
|
||||
const policy = loadVersionPolicy()
|
||||
return policy?.r2 || { enabled: false }
|
||||
}
|
||||
|
||||
function r2PlatformKey() {
|
||||
const arch = process.arch // x64, arm64, etc.
|
||||
const plat = process.platform // linux, darwin, win32
|
||||
if (plat === 'win32' && arch === 'x64') return 'win-x64'
|
||||
if (plat === 'darwin' && arch === 'arm64') return 'darwin-arm64'
|
||||
if (plat === 'darwin' && arch === 'x64') return 'darwin-x64'
|
||||
if (plat === 'linux' && arch === 'x64') return 'linux-x64'
|
||||
if (plat === 'linux' && arch === 'arm64') return 'linux-arm64'
|
||||
return 'unknown'
|
||||
}
|
||||
|
||||
async function _tryR2Install(version, source, logs) {
|
||||
const r2 = r2Config()
|
||||
if (!r2.enabled || !r2.baseUrl) return false
|
||||
const platform = r2PlatformKey()
|
||||
if (platform === 'unknown') return false
|
||||
|
||||
logs.push('尝试从 CDN 加速下载...')
|
||||
const manifestUrl = `${r2.baseUrl}/latest.json`
|
||||
const resp = await globalThis.fetch(manifestUrl, { signal: AbortSignal.timeout(10000) })
|
||||
if (!resp.ok) throw new Error(`CDN 清单不可用 (HTTP ${resp.status})`)
|
||||
const manifest = await resp.json()
|
||||
|
||||
const sourceKey = source === 'official' ? 'official' : 'chinese'
|
||||
const asset = manifest?.[sourceKey]?.assets?.[platform]
|
||||
if (!asset?.url) throw new Error(`CDN 无 ${sourceKey}/${platform} 归档`)
|
||||
|
||||
const cdnVersion = manifest?.[sourceKey]?.version || version
|
||||
if (version !== 'latest' && !versionsMatch(cdnVersion, version)) {
|
||||
throw new Error(`CDN 版本 ${cdnVersion} 与请求版本 ${version} 不匹配`)
|
||||
}
|
||||
|
||||
const sizeMb = asset.size ? `${(asset.size / 1048576).toFixed(0)}MB` : '未知大小'
|
||||
logs.push(`CDN 下载: ${cdnVersion} (${platform}, ${sizeMb})`)
|
||||
|
||||
// 下载到临时文件
|
||||
const tmpPath = path.join(os.tmpdir(), `openclaw-${platform}.tgz`)
|
||||
const dlResp = await globalThis.fetch(asset.url, { signal: AbortSignal.timeout(300000) })
|
||||
if (!dlResp.ok) throw new Error(`CDN 下载失败 (HTTP ${dlResp.status})`)
|
||||
const buffer = Buffer.from(await dlResp.arrayBuffer())
|
||||
fs.writeFileSync(tmpPath, buffer)
|
||||
|
||||
// SHA256 校验
|
||||
if (asset.sha256) {
|
||||
const crypto = require('crypto')
|
||||
const hash = crypto.createHash('sha256').update(buffer).digest('hex')
|
||||
if (hash !== asset.sha256) {
|
||||
fs.unlinkSync(tmpPath)
|
||||
throw new Error(`SHA256 校验失败: 期望 ${asset.sha256}, 实际 ${hash}`)
|
||||
}
|
||||
logs.push('SHA256 校验通过 ✓')
|
||||
}
|
||||
|
||||
// 确定 npm 全局 node_modules 目录
|
||||
let modulesDir
|
||||
if (isWindows) {
|
||||
modulesDir = path.join(process.env.APPDATA || '', 'npm', 'node_modules')
|
||||
} else if (isMac) {
|
||||
modulesDir = fs.existsSync('/opt/homebrew/lib/node_modules')
|
||||
? '/opt/homebrew/lib/node_modules'
|
||||
: '/usr/local/lib/node_modules'
|
||||
} else {
|
||||
try {
|
||||
const prefix = execSync('npm config get prefix', { encoding: 'utf8', timeout: 5000 }).trim()
|
||||
modulesDir = path.join(prefix, 'lib', 'node_modules')
|
||||
} catch {
|
||||
modulesDir = '/usr/local/lib/node_modules'
|
||||
}
|
||||
}
|
||||
if (!fs.existsSync(modulesDir)) fs.mkdirSync(modulesDir, { recursive: true })
|
||||
|
||||
// 清理旧目录
|
||||
const qcDir = path.join(modulesDir, '@qingchencloud')
|
||||
if (fs.existsSync(qcDir)) fs.rmSync(qcDir, { recursive: true, force: true })
|
||||
|
||||
// 解压
|
||||
logs.push(`解压到 ${modulesDir}`)
|
||||
execSync(`tar -xzf "${tmpPath}" -C "${modulesDir}"`, { timeout: 60000, windowsHide: true })
|
||||
|
||||
// 创建 bin 链接
|
||||
let binDir
|
||||
if (isWindows) {
|
||||
binDir = path.join(process.env.APPDATA || '', 'npm')
|
||||
} else if (isMac) {
|
||||
binDir = fs.existsSync('/opt/homebrew/bin') ? '/opt/homebrew/bin' : '/usr/local/bin'
|
||||
} else {
|
||||
try {
|
||||
const prefix = execSync('npm config get prefix', { encoding: 'utf8', timeout: 5000 }).trim()
|
||||
binDir = path.join(prefix, 'bin')
|
||||
} catch {
|
||||
binDir = '/usr/local/bin'
|
||||
}
|
||||
}
|
||||
const openclawJs = path.join(modulesDir, '@qingchencloud', 'openclaw-zh', 'bin', 'openclaw.js')
|
||||
if (fs.existsSync(openclawJs)) {
|
||||
if (isWindows) {
|
||||
const cmdContent = `@ECHO off\r\nGOTO start\r\n:find_dp0\r\nSET dp0=%~dp0\r\nEXIT /b\r\n:start\r\nSETLOCAL\r\nCALL :find_dp0\r\n\r\nIF EXIST "%dp0%\\node.exe" (\r\n SET "_prog=%dp0%\\node.exe"\r\n) ELSE (\r\n SET "_prog=node"\r\n SET PATHEXT=%PATHEXT:;.JS;=;%\r\n)\r\n\r\nendLocal & goto #_undefined_# 2>NUL || title %COMSPEC% & "%_prog%" "${openclawJs}" %*\r\n`
|
||||
fs.writeFileSync(path.join(binDir, 'openclaw.cmd'), cmdContent)
|
||||
} else {
|
||||
const linkPath = path.join(binDir, 'openclaw')
|
||||
try { fs.unlinkSync(linkPath) } catch {}
|
||||
fs.symlinkSync(openclawJs, linkPath)
|
||||
try { fs.chmodSync(openclawJs, 0o755) } catch {}
|
||||
try { fs.chmodSync(linkPath, 0o755) } catch {}
|
||||
}
|
||||
logs.push('bin 链接已创建 ✓')
|
||||
}
|
||||
|
||||
// 清理临时文件
|
||||
try { fs.unlinkSync(tmpPath) } catch {}
|
||||
|
||||
logs.push(`✅ CDN 加速安装完成,当前版本: ${cdnVersion}`)
|
||||
return true
|
||||
}
|
||||
|
||||
function recommendedVersionFor(source = 'chinese') {
|
||||
const policy = loadVersionPolicy()
|
||||
return policy?.panels?.[PANEL_VERSION]?.[source]?.recommended
|
||||
@@ -1202,6 +1322,32 @@ function _normalizeBaseUrl(raw) {
|
||||
return base
|
||||
}
|
||||
|
||||
// === 后端内存缓存(ARM 设备性能优化)===
|
||||
// 防止短时间内重复 spawn CLI 进程,显著降低 CPU 占用
|
||||
const _serverCache = new Map()
|
||||
function serverCached(key, ttlMs, fn) {
|
||||
const entry = _serverCache.get(key)
|
||||
if (entry && Date.now() - entry.ts < ttlMs) return entry.val
|
||||
// in-flight 去重:同一 key 正在执行中,复用 Promise
|
||||
if (entry && entry.pending) return entry.pending
|
||||
const result = fn()
|
||||
if (result && typeof result.then === 'function') {
|
||||
// async
|
||||
const pending = result.then(val => {
|
||||
_serverCache.set(key, { val, ts: Date.now() })
|
||||
return val
|
||||
}).catch(err => {
|
||||
_serverCache.delete(key)
|
||||
throw err
|
||||
})
|
||||
_serverCache.set(key, { ...(entry || {}), pending })
|
||||
return pending
|
||||
}
|
||||
// sync
|
||||
_serverCache.set(key, { val: result, ts: Date.now() })
|
||||
return result
|
||||
}
|
||||
|
||||
// === API Handlers ===
|
||||
|
||||
const handlers = {
|
||||
@@ -1230,33 +1376,35 @@ const handlers = {
|
||||
return true
|
||||
},
|
||||
|
||||
// 服务管理
|
||||
async get_services_status() {
|
||||
const label = 'ai.openclaw.gateway'
|
||||
let { running, pid } = isMac ? macCheckService(label) : isLinux ? linuxCheckGateway() : await winCheckGateway()
|
||||
// 服务管理(10s 服务端缓存 + in-flight 去重,ARM 设备关键优化)
|
||||
get_services_status() {
|
||||
return serverCached('svc_status', 10000, async () => {
|
||||
const label = 'ai.openclaw.gateway'
|
||||
let { running, pid } = isMac ? macCheckService(label) : isLinux ? linuxCheckGateway() : await winCheckGateway()
|
||||
|
||||
// 通用兜底:进程检测说没运行,但端口实际在监听 → Gateway 已在运行
|
||||
if (!running) {
|
||||
const port = readGatewayPort()
|
||||
const portOpen = await new Promise(resolve => {
|
||||
const sock = net.createConnection(port, '127.0.0.1', () => { sock.destroy(); resolve(true) })
|
||||
sock.on('error', () => resolve(false))
|
||||
sock.setTimeout(2000, () => { sock.destroy(); resolve(false) })
|
||||
})
|
||||
if (portOpen) { running = true }
|
||||
}
|
||||
// 通用兜底:进程检测说没运行,但端口实际在监听 → Gateway 已在运行
|
||||
if (!running) {
|
||||
const port = readGatewayPort()
|
||||
const portOpen = await new Promise(resolve => {
|
||||
const sock = net.createConnection(port, '127.0.0.1', () => { sock.destroy(); resolve(true) })
|
||||
sock.on('error', () => resolve(false))
|
||||
sock.setTimeout(2000, () => { sock.destroy(); resolve(false) })
|
||||
})
|
||||
if (portOpen) { running = true }
|
||||
}
|
||||
|
||||
let cliInstalled = false
|
||||
if (isMac) {
|
||||
cliInstalled = fs.existsSync('/opt/homebrew/bin/openclaw') || fs.existsSync('/usr/local/bin/openclaw')
|
||||
} else if (isWindows) {
|
||||
try { cliInstalled = fs.existsSync(path.join(process.env.APPDATA || '', 'npm', 'openclaw.cmd')) }
|
||||
catch { cliInstalled = false }
|
||||
} else {
|
||||
cliInstalled = !!findOpenclawBin()
|
||||
}
|
||||
let cliInstalled = false
|
||||
if (isMac) {
|
||||
cliInstalled = fs.existsSync('/opt/homebrew/bin/openclaw') || fs.existsSync('/usr/local/bin/openclaw')
|
||||
} else if (isWindows) {
|
||||
try { cliInstalled = fs.existsSync(path.join(process.env.APPDATA || '', 'npm', 'openclaw.cmd')) }
|
||||
catch { cliInstalled = false }
|
||||
} else {
|
||||
cliInstalled = !!findOpenclawBin()
|
||||
}
|
||||
|
||||
return [{ label, running, pid, description: 'OpenClaw Gateway', cli_installed: cliInstalled }]
|
||||
return [{ label, running, pid, description: 'OpenClaw Gateway', cli_installed: cliInstalled }]
|
||||
})
|
||||
},
|
||||
|
||||
start_service({ label }) {
|
||||
@@ -2589,25 +2737,27 @@ const handlers = {
|
||||
}
|
||||
},
|
||||
|
||||
// 运行时状态摘要(openclaw status --json)
|
||||
// 运行时状态摘要(60s 服务端缓存,ARM 设备上此调用是最大 CPU 消耗源)
|
||||
get_status_summary() {
|
||||
try {
|
||||
const raw = execSync('openclaw status --json 2>&1', { windowsHide: true, timeout: 10000 }).toString()
|
||||
// 提取第一个 JSON 对象
|
||||
const idx = raw.indexOf('{')
|
||||
if (idx >= 0) {
|
||||
try { return JSON.parse(raw.slice(idx)) } catch {}
|
||||
// 流式解析:找到匹配的 } 结束
|
||||
let depth = 0
|
||||
for (let i = idx; i < raw.length; i++) {
|
||||
if (raw[i] === '{') depth++
|
||||
else if (raw[i] === '}') { depth--; if (depth === 0) { try { return JSON.parse(raw.slice(idx, i + 1)) } catch { break } } }
|
||||
return serverCached('status_summary', 60000, () => {
|
||||
try {
|
||||
const raw = execSync('openclaw status --json 2>&1', { windowsHide: true, timeout: 10000 }).toString()
|
||||
// 提取第一个 JSON 对象
|
||||
const idx = raw.indexOf('{')
|
||||
if (idx >= 0) {
|
||||
try { return JSON.parse(raw.slice(idx)) } catch {}
|
||||
// 流式解析:找到匹配的 } 结束
|
||||
let depth = 0
|
||||
for (let i = idx; i < raw.length; i++) {
|
||||
if (raw[i] === '{') depth++
|
||||
else if (raw[i] === '}') { depth--; if (depth === 0) { try { return JSON.parse(raw.slice(idx, i + 1)) } catch { break } } }
|
||||
}
|
||||
}
|
||||
return { error: '解析失败' }
|
||||
} catch (e) {
|
||||
return { error: e.message || String(e) }
|
||||
}
|
||||
return { error: '解析失败' }
|
||||
} catch (e) {
|
||||
return { error: e.message || String(e) }
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
// 版本信息
|
||||
@@ -2935,7 +3085,7 @@ const handlers = {
|
||||
throw new Error('查询版本失败: ' + (lastError?.message || lastError || 'unknown error'))
|
||||
},
|
||||
|
||||
upgrade_openclaw({ source = 'chinese', version } = {}) {
|
||||
async upgrade_openclaw({ source = 'chinese', version } = {}) {
|
||||
const currentSource = detectInstalledSource()
|
||||
const pkg = npmPackageName(source)
|
||||
const recommended = recommendedVersionFor(source)
|
||||
@@ -2944,12 +3094,23 @@ const handlers = {
|
||||
const needUninstallOld = currentSource !== source
|
||||
const npmBin = isWindows ? 'npm.cmd' : 'npm'
|
||||
const registry = pickRegistryForPackage(pkg)
|
||||
const gitConfigured = configureGitHttpsRules()
|
||||
const gitEnv = buildGitInstallEnv()
|
||||
const logs = []
|
||||
|
||||
// ── R2 CDN 加速:优先尝试从 CDN 下载预装归档 ──
|
||||
if (source !== 'official') {
|
||||
try {
|
||||
const r2Result = await _tryR2Install(ver, source, logs)
|
||||
if (r2Result) return logs.join('\n')
|
||||
} catch (e) {
|
||||
logs.push(`CDN 加速不可用(${e.message}),降级到 npm 安装...`)
|
||||
}
|
||||
}
|
||||
|
||||
if (!version && recommended) {
|
||||
logs.push(`ClawPanel ${PANEL_VERSION} 默认绑定 OpenClaw 稳定版: ${recommended}`)
|
||||
}
|
||||
const gitConfigured = configureGitHttpsRules()
|
||||
const gitEnv = buildGitInstallEnv()
|
||||
logs.push(`Git HTTPS 规则已就绪 (${gitConfigured}/${GIT_HTTPS_REWRITES.length})`)
|
||||
const runInstall = (targetRegistry) => execSync(
|
||||
`${npmBin} install -g ${pkg}@${ver} --force --registry ${targetRegistry} --verbose 2>&1`,
|
||||
|
||||
Reference in New Issue
Block a user