perf: ARM设备性能优化 — in-flight请求去重+后端缓存+仪表盘轮询降频+R2 CDN加速

This commit is contained in:
晴天
2026-03-16 13:55:41 +08:00
parent b11c9533ef
commit 61434137d7
8 changed files with 645 additions and 52 deletions

View File

@@ -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`,