Files
2026-07-13 12:44:08 +08:00

785 lines
20 KiB
JavaScript

import fs from 'node:fs'
import os from 'node:os'
import path from 'node:path'
import { spawn } from 'node:child_process'
import dotenv from 'dotenv'
import execa from 'execa'
import semver from 'semver'
import kill from 'tree-kill'
import { LogHelper } from '@/helpers/log-helper'
import { RuntimeHelper } from '@/helpers/runtime-helper'
import { SystemHelper } from '@/helpers/system-helper'
import { shouldIgnoreTCPServerError } from '@/utilities'
import {
MINIMUM_REQUIRED_RAM,
LEON_VERSION,
INSTANCE_ID,
NODE_VERSION,
PNPM_VERSION,
PYTHON_VERSION,
UV_VERSION,
NODE_RUNTIME_BIN_PATH,
PNPM_RUNTIME_BIN_PATH,
PYTHON_RUNTIME_BIN_PATH,
UV_RUNTIME_BIN_PATH,
NODEJS_BRIDGE_ENTRY_PATH,
NODEJS_BRIDGE_VERSION,
PYTHON_BRIDGE_ENTRY_PATH,
PYTHON_BRIDGE_RUNTIME_BIN_PATH,
PYTHON_BRIDGE_VERSION,
PYTHON_TCP_SERVER_ENTRY_PATH,
PYTHON_TCP_SERVER_RUNTIME_BIN_PATH,
PYTHON_TCP_SERVER_VERSION,
PYTHON_TCP_SERVER_SETTINGS,
PYTHON_TCP_SERVER_TTS_MODEL_DIR_PATH,
PYTHON_TCP_SERVER_TTS_MODEL_PATH,
PYTHON_TCP_SERVER_TTS_BERT_BASE_DIR_PATH,
PYTHON_TCP_SERVER_ASR_MODEL_DIR_PATH,
AUDIO_MODELS_PATH,
PROFILE_DOT_ENV_PATH,
TSX_CLI_PATH,
LANG,
HAS_ASR,
HAS_TTS,
HAS_WAKE_WORD,
SHOULD_START_PYTHON_TCP_SERVER,
LEON_ROUTING_MODE,
WORKFLOW_LLM_TARGET,
AGENT_LLM_TARGET,
NATIVE_SKILLS_PATH,
PYTORCH_TORCH_PATH,
NVIDIA_CUBLAS_PATH,
NVIDIA_CUDNN_PATH,
NVIDIA_CUSPARSE_PATH,
NVIDIA_CUSPARSE_FULL_PATH,
NVIDIA_NCCL_PATH,
NVIDIA_NVJITLINK_PATH,
NVIDIA_NVSHMEM_PATH
} from '@/constants'
dotenv.config({ path: PROFILE_DOT_ENV_PATH })
const CHECK_TMP_DIR_PATH = path.join(process.cwd(), 'scripts', 'tmp')
const PYTHON_TCP_SERVER_TTS_MODEL_CONFIG_PATH = path.join(
PYTHON_TCP_SERVER_TTS_MODEL_DIR_PATH,
'config.json'
)
const PYTHON_TCP_SERVER_WAKE_WORD_MODEL_DIR_PATH = path.join(
AUDIO_MODELS_PATH,
'wake_word'
)
const PYTHON_TCP_SERVER_WAKE_WORD_MODEL_PATH = path.join(
PYTHON_TCP_SERVER_WAKE_WORD_MODEL_DIR_PATH,
PYTHON_TCP_SERVER_SETTINGS.wake_word.model_file_name
)
const PYTHON_TCP_SERVER_WAKE_WORD_MELSPEC_PATH = path.join(
PYTHON_TCP_SERVER_WAKE_WORD_MODEL_DIR_PATH,
'melspectrogram.onnx'
)
const PYTHON_TCP_SERVER_WAKE_WORD_EMBEDDING_PATH = path.join(
PYTHON_TCP_SERVER_WAKE_WORD_MODEL_DIR_PATH,
'embedding.onnx'
)
function createCheckResult(title, severity = 'error') {
return {
title,
severity,
ok: true,
details: []
}
}
function addFailure(result, detail) {
result.ok = false
result.details.push(detail)
}
function addDetail(result, detail) {
result.details.push(detail)
}
function getFormattedCheckStatus(check) {
if (check.ok) {
return 'ok'
}
return check.severity === 'warning' ? 'warning' : 'error'
}
function extractFirstVersion(rawOutput) {
const match = rawOutput.match(/\d+\.\d+\.\d+/)
return match ? match[0] : null
}
async function getCommandVersion(executablePath, args = []) {
const result = await execa(executablePath, args)
return {
raw: result.stdout.trim(),
version: extractFirstVersion(result.stdout.trim())
}
}
async function writeCheckIntentObject(fileName, intentObject) {
await fs.promises.mkdir(CHECK_TMP_DIR_PATH, { recursive: true })
const filePath = path.join(CHECK_TMP_DIR_PATH, fileName)
await fs.promises.writeFile(filePath, JSON.stringify(intentObject, null, 2))
return filePath
}
function buildExtraContext(lang) {
const now = new Date()
return {
lang,
date: now.toISOString().slice(0, 10),
time: now.toTimeString().slice(0, 8),
timestamp: now.getTime(),
date_time: now.toISOString(),
week_day: now.toLocaleDateString('en-US', { weekday: 'long' })
}
}
async function createNodejsBridgeIntentObject() {
const skillConfigPath = path.join(
NATIVE_SKILLS_PATH,
'date_time_skill',
'skill.json'
)
const skillConfig = JSON.parse(await fs.promises.readFile(skillConfigPath, 'utf8'))
return writeCheckIntentObject('check-nodejs-bridge-intent-object.json', {
id: 'check-nodejs-bridge',
lang: 'en',
context_name: 'date_time',
skill_name: 'date_time_skill',
action_name: 'current_time',
skill_config: skillConfig,
skill_config_path: skillConfigPath,
utterance: 'What time is it?',
entities: [],
action_arguments: {},
sentiment: {
vote: 'neutral',
score: 0
},
context: {
utterances: ['What time is it?'],
action_arguments: [{}],
entities: [],
sentiments: [{ vote: 'neutral', score: 0 }],
data: {}
},
extra_context: buildExtraContext('en')
})
}
async function createPythonBridgeIntentObject() {
const skillConfigPath = path.join(
NATIVE_SKILLS_PATH,
'color_skill',
'skill.json'
)
const skillConfig = JSON.parse(await fs.promises.readFile(skillConfigPath, 'utf8'))
return writeCheckIntentObject('check-python-bridge-intent-object.json', {
id: 'check-python-bridge',
lang: 'en',
context_name: 'color',
skill_name: 'color_skill',
action_name: 'tell_hexadecimal_color',
skill_config: skillConfig,
skill_config_path: skillConfigPath,
utterance: 'What is the hexadecimal color for red?',
entities: [],
action_arguments: {
color_name: 'red'
},
sentiment: {
vote: 'neutral',
score: 0
},
context: {
utterances: ['What is the hexadecimal color for red?'],
action_arguments: [{ color_name: 'red' }],
entities: [],
sentiments: [{ vote: 'neutral', score: 0 }],
data: {}
},
extra_context: buildExtraContext('en')
})
}
async function runNodejsBridgeCheck(intentObjectPath) {
return execa(NODE_RUNTIME_BIN_PATH, [
TSX_CLI_PATH,
NODEJS_BRIDGE_ENTRY_PATH,
'--runtime',
'skill',
intentObjectPath
])
}
async function runPythonBridgeCheck(intentObjectPath) {
return execa(PYTHON_BRIDGE_RUNTIME_BIN_PATH, [
PYTHON_BRIDGE_ENTRY_PATH,
intentObjectPath
])
}
function buildTCPServerEnv() {
const env = { ...process.env }
if (SystemHelper.isLinux()) {
const torchLibPath = `${PYTORCH_TORCH_PATH}/lib`
const nvidiaLibPaths = [
`${NVIDIA_CUBLAS_PATH}/lib`,
`${NVIDIA_CUDNN_PATH}/lib`,
`${NVIDIA_CUSPARSE_PATH}/lib`,
`${NVIDIA_CUSPARSE_FULL_PATH}/lib`,
`${NVIDIA_NCCL_PATH}/lib`,
`${NVIDIA_NVSHMEM_PATH}/lib`,
`${NVIDIA_NVJITLINK_PATH}/lib`
]
env['LD_LIBRARY_PATH'] = [torchLibPath, ...nvidiaLibPaths, env['LD_LIBRARY_PATH']]
.filter(Boolean)
.join(':')
}
return env
}
function startTCPServerCheck() {
const args = [PYTHON_TCP_SERVER_ENTRY_PATH, LANG || 'en']
const env = buildTCPServerEnv()
const commandString = RuntimeHelper.buildShellCommand(
PYTHON_TCP_SERVER_RUNTIME_BIN_PATH,
args
)
return new Promise((resolve) => {
const child = spawn(PYTHON_TCP_SERVER_RUNTIME_BIN_PATH, args, {
env,
stdio: ['ignore', 'pipe', 'pipe'],
windowsHide: true
})
const timeoutMs = 3 * 60_000
let output = ''
let resolved = false
const finish = (result) => {
if (resolved) {
return
}
resolved = true
clearTimeout(timeoutId)
if (child.pid) {
kill(child.pid)
}
resolve({
command: commandString,
...result
})
}
const timeoutId = setTimeout(() => {
finish({
ok: false,
output,
error: `The Python TCP server timed out after ${timeoutMs}ms`
})
}, timeoutMs)
child.stdout.on('data', (data) => {
const text = data.toString()
output += text
if (text.toLowerCase().includes('waiting for connection')) {
finish({
ok: true,
output,
error: null
})
}
})
child.stderr.on('data', (data) => {
const text = data.toString()
if (shouldIgnoreTCPServerError(text)) {
output += text
return
}
output += text
finish({
ok: false,
output,
error: text.trim() || 'Unknown TCP server startup error'
})
})
child.on('error', (error) => {
finish({
ok: false,
output,
error: String(error)
})
})
child.on('exit', (code) => {
if (!resolved) {
finish({
ok: false,
output,
error: `The Python TCP server exited before becoming ready (code: ${code ?? 'unknown'})`
})
}
})
})
}
function validateLocalLLMTarget(target, label, result) {
if (!target.isEnabled) {
addDetail(result, `${label}: disabled`)
return
}
if (!target.isLocal) {
addDetail(result, `${label}: ${target.label}`)
return
}
if (!target.isResolved) {
addFailure(
result,
`${label}: ${target.resolutionError || 'local model is not configured or installed yet'}`
)
return
}
if (!fs.existsSync(target.model)) {
addFailure(
result,
`${label}: local model not found at ${target.model}`
)
return
}
addDetail(result, `${label}: ${target.label}`)
}
function checkTCPServerAssets() {
const result = createCheckResult('Python TCP server assets')
if (HAS_TTS) {
if (!fs.existsSync(PYTHON_TCP_SERVER_TTS_MODEL_CONFIG_PATH)) {
addFailure(
result,
`Missing TTS config at ${PYTHON_TCP_SERVER_TTS_MODEL_CONFIG_PATH}`
)
}
if (!fs.existsSync(PYTHON_TCP_SERVER_TTS_MODEL_PATH)) {
addFailure(
result,
`Missing TTS model at ${PYTHON_TCP_SERVER_TTS_MODEL_PATH}`
)
}
if (!fs.existsSync(PYTHON_TCP_SERVER_TTS_BERT_BASE_DIR_PATH)) {
addFailure(
result,
`Missing TTS BERT base directory at ${PYTHON_TCP_SERVER_TTS_BERT_BASE_DIR_PATH}`
)
}
}
if (HAS_ASR && !fs.existsSync(PYTHON_TCP_SERVER_ASR_MODEL_DIR_PATH)) {
addFailure(
result,
`Missing ASR model directory at ${PYTHON_TCP_SERVER_ASR_MODEL_DIR_PATH}`
)
}
if (HAS_WAKE_WORD) {
if (!fs.existsSync(PYTHON_TCP_SERVER_WAKE_WORD_MODEL_PATH)) {
addFailure(
result,
`Missing wake word model at ${PYTHON_TCP_SERVER_WAKE_WORD_MODEL_PATH}`
)
}
if (!fs.existsSync(PYTHON_TCP_SERVER_WAKE_WORD_MELSPEC_PATH)) {
addFailure(
result,
`Missing wake word melspectrogram model at ${PYTHON_TCP_SERVER_WAKE_WORD_MELSPEC_PATH}`
)
}
if (!fs.existsSync(PYTHON_TCP_SERVER_WAKE_WORD_EMBEDDING_PATH)) {
addFailure(
result,
`Missing wake word embedding model at ${PYTHON_TCP_SERVER_WAKE_WORD_EMBEDDING_PATH}`
)
}
}
if (result.ok) {
addDetail(
result,
`ASR=${HAS_ASR ? 'enabled' : 'disabled'}, TTS=${HAS_TTS ? 'enabled' : 'disabled'}, wake_word=${HAS_WAKE_WORD ? 'enabled' : 'disabled'}`
)
}
return result
}
function printCheckResult(result) {
const status = getFormattedCheckStatus(result)
if (status === 'ok') {
LogHelper.success(result.title)
} else if (status === 'warning') {
LogHelper.warning(result.title)
} else {
LogHelper.error(result.title)
}
result.details.forEach((detail) => {
LogHelper.default(`- ${detail}`)
})
}
;(async () => {
const checks = {
canRun: createCheckResult('Run Leon'),
hardware: createCheckResult('Hardware'),
managedNode: createCheckResult('Managed Node.js runtime'),
managedPNPM: createCheckResult('Managed pnpm runtime'),
managedPython: createCheckResult('Managed Python runtime'),
managedUV: createCheckResult('Managed uv runtime'),
llmTargets: createCheckResult('LLM target configuration'),
nodeJSBridge: createCheckResult('Node.js bridge smoke test'),
pythonBridge: createCheckResult('Python bridge smoke test'),
pythonTCPServerAssets: createCheckResult('Python TCP server assets'),
pythonTCPServer: createCheckResult('Python TCP server boot')
}
const summary = {
leonVersion: LEON_VERSION || null,
instanceID: INSTANCE_ID || null,
environment: {
os: SystemHelper.getInformation(),
release: os.release(),
cpus: os.cpus().length,
totalRAMInGB: SystemHelper.getTotalRAM(),
freeRAMInGB: SystemHelper.getFreeRAM()
},
hardware: {},
runtimes: {},
llm: {
routingMode: LEON_ROUTING_MODE,
workflow: WORKFLOW_LLM_TARGET.label,
agent: AGENT_LLM_TARGET.label
},
nodeJSBridge: {},
pythonBridge: {},
pythonTCPServer: {}
}
LogHelper.title('Checking')
LogHelper.info('Leon version')
LogHelper.success(String(LEON_VERSION))
LogHelper.info('Environment')
LogHelper.success(JSON.stringify(summary.environment))
try {
const [
gpuDeviceNames,
graphicsComputeAPI,
totalVRAM,
freeVRAM,
usedVRAM,
canSupportLocalLLM
] = await Promise.all([
SystemHelper.getGPUDeviceNames(),
SystemHelper.getGraphicsComputeAPI(),
SystemHelper.getTotalVRAM(),
SystemHelper.getFreeVRAM(),
SystemHelper.getUsedVRAM(),
SystemHelper.canSupportLocalLLM()
])
summary.hardware = {
gpuDeviceNames,
graphicsComputeAPI,
totalVRAMInGB: totalVRAM,
freeVRAMInGB: freeVRAM,
usedVRAMInGB: usedVRAM,
canSupportLocalLLM
}
addDetail(
checks.hardware,
`GPU: ${gpuDeviceNames.length > 0 ? gpuDeviceNames.join(', ') : 'none'}`
)
addDetail(checks.hardware, `Compute API: ${graphicsComputeAPI}`)
addDetail(
checks.hardware,
`VRAM: total=${totalVRAM} GB | free=${freeVRAM} GB | used=${usedVRAM} GB`
)
addDetail(
checks.hardware,
`Local LLM support: ${canSupportLocalLLM ? 'yes' : 'no'}`
)
} catch (error) {
checks.hardware.severity = 'warning'
addFailure(
checks.hardware,
`Unable to inspect GPU/VRAM information: ${String(error)}`
)
}
if (Math.round(summary.environment.freeRAMInGB) < MINIMUM_REQUIRED_RAM) {
addFailure(
checks.canRun,
`Free RAM is ${summary.environment.freeRAMInGB} GB but Leon needs at least ${MINIMUM_REQUIRED_RAM} GB`
)
} else {
addDetail(
checks.canRun,
`Free RAM: ${summary.environment.freeRAMInGB} GB | Total RAM: ${summary.environment.totalRAMInGB} GB`
)
}
const runtimeChecks = [
{
key: 'managedNode',
label: 'node',
executablePath: NODE_RUNTIME_BIN_PATH,
expectedVersion: NODE_VERSION
},
{
key: 'managedPNPM',
label: 'pnpm',
executablePath: PNPM_RUNTIME_BIN_PATH,
expectedVersion: PNPM_VERSION
},
{
key: 'managedPython',
label: 'python',
executablePath: PYTHON_RUNTIME_BIN_PATH,
expectedVersion: PYTHON_VERSION
},
{
key: 'managedUV',
label: 'uv',
executablePath: UV_RUNTIME_BIN_PATH,
expectedVersion: UV_VERSION
}
]
for (const runtimeCheck of runtimeChecks) {
const result = checks[runtimeCheck.key]
if (!fs.existsSync(runtimeCheck.executablePath)) {
addFailure(
result,
`Runtime not found at ${runtimeCheck.executablePath}`
)
continue
}
try {
const { raw, version } = await getCommandVersion(
runtimeCheck.executablePath,
['--version']
)
summary.runtimes[runtimeCheck.label] = {
path: runtimeCheck.executablePath,
version: raw
}
addDetail(result, `Path: ${runtimeCheck.executablePath}`)
addDetail(result, `Version: ${raw}`)
const expectedVersion = extractFirstVersion(runtimeCheck.expectedVersion)
if (
expectedVersion &&
version &&
semver.valid(semver.coerce(version)) &&
semver.valid(semver.coerce(expectedVersion)) &&
semver.neq(
semver.coerce(version),
semver.coerce(expectedVersion)
)
) {
addFailure(
result,
`Expected ${expectedVersion} but got ${version}`
)
}
} catch (error) {
addFailure(result, String(error))
}
}
if (!fs.existsSync(TSX_CLI_PATH)) {
addFailure(
checks.nodeJSBridge,
`Missing tsx CLI at ${TSX_CLI_PATH}`
)
}
validateLocalLLMTarget(WORKFLOW_LLM_TARGET, 'Workflow', checks.llmTargets)
validateLocalLLMTarget(AGENT_LLM_TARGET, 'Agent', checks.llmTargets)
const pythonTCPServerAssetsResult = checkTCPServerAssets()
checks.pythonTCPServerAssets = pythonTCPServerAssetsResult
let nodeIntentObjectPath = null
let pythonIntentObjectPath = null
try {
nodeIntentObjectPath = await createNodejsBridgeIntentObject()
const executionStart = Date.now()
const result = await runNodejsBridgeCheck(nodeIntentObjectPath)
const executionTime = Date.now() - executionStart
const combinedOutput = `${result.stdout}\n${result.stderr}`.trim()
summary.nodeJSBridge = {
version: NODEJS_BRIDGE_VERSION,
command: result.command,
executionTimeMs: executionTime,
output: combinedOutput
}
addDetail(checks.nodeJSBridge, `Version: ${NODEJS_BRIDGE_VERSION}`)
addDetail(checks.nodeJSBridge, `Execution time: ${executionTime}ms`)
if (
combinedOutput.includes('Error while running') ||
combinedOutput.trim() === ''
) {
addFailure(
checks.nodeJSBridge,
combinedOutput || 'The Node.js bridge did not produce any output'
)
}
} catch (error) {
addFailure(checks.nodeJSBridge, String(error))
}
try {
pythonIntentObjectPath = await createPythonBridgeIntentObject()
const executionStart = Date.now()
const result = await runPythonBridgeCheck(pythonIntentObjectPath)
const executionTime = Date.now() - executionStart
const combinedOutput = `${result.stdout}\n${result.stderr}`.trim()
summary.pythonBridge = {
version: PYTHON_BRIDGE_VERSION,
command: result.command,
executionTimeMs: executionTime,
output: combinedOutput
}
addDetail(checks.pythonBridge, `Version: ${PYTHON_BRIDGE_VERSION}`)
addDetail(checks.pythonBridge, `Execution time: ${executionTime}ms`)
if (
combinedOutput.includes('Error while running') ||
combinedOutput.includes('Traceback') ||
combinedOutput.trim() === ''
) {
addFailure(
checks.pythonBridge,
combinedOutput || 'The Python bridge did not produce any output'
)
}
} catch (error) {
addFailure(checks.pythonBridge, String(error))
}
if (SHOULD_START_PYTHON_TCP_SERVER) {
try {
const startupStart = Date.now()
const result = await startTCPServerCheck()
const startupTime = Date.now() - startupStart
summary.pythonTCPServer = {
version: PYTHON_TCP_SERVER_VERSION,
command: result.command,
startupTimeMs: startupTime,
output: result.output
}
addDetail(checks.pythonTCPServer, `Version: ${PYTHON_TCP_SERVER_VERSION}`)
addDetail(checks.pythonTCPServer, `Startup time: ${startupTime}ms`)
if (!result.ok) {
addFailure(
checks.pythonTCPServer,
result.error || 'The Python TCP server did not become ready'
)
}
} catch (error) {
addFailure(checks.pythonTCPServer, String(error))
}
} else {
checks.pythonTCPServer.severity = 'warning'
addDetail(
checks.pythonTCPServer,
'Skipped because neither ASR nor TTS is enabled'
)
}
for (const check of Object.values(checks)) {
printCheckResult(check)
}
const criticalFailures = Object.values(checks).some(
(check) => check.severity === 'error' && !check.ok
)
LogHelper.title('Summary')
if (criticalFailures || !checks.canRun.ok) {
LogHelper.error('Please fix the errors above')
} else {
LogHelper.success('Hooray! Leon can run correctly')
}
try {
if (nodeIntentObjectPath) {
await fs.promises.rm(nodeIntentObjectPath, { force: true })
}
if (pythonIntentObjectPath) {
await fs.promises.rm(pythonIntentObjectPath, { force: true })
}
} catch {
// Ignore cleanup failures for temporary check fixtures.
}
process.exit(criticalFailures || !checks.canRun.ok ? 1 : 0)
})().catch((error) => {
LogHelper.title('Checking')
LogHelper.error(`Unexpected error: ${error}`)
process.exit(1)
})