mirror of
https://github.com/qingchencloud/clawpanel.git
synced 2026-06-21 23:54:22 +08:00
fix(setup): 完善 Node 版本检测与升级引导
This commit is contained in:
@@ -23,6 +23,11 @@
|
||||
import { t } from './i18n.js'
|
||||
|
||||
const PATTERNS = [
|
||||
// Node.js 版本不满足当前 OpenClaw 要求
|
||||
{
|
||||
key: 'nodeVersion',
|
||||
re: /(node\.?js.*(版本过低|too old)|requires node|要求\s*[><=^~]*\d|node.*version.*(too old|unsupported))/i,
|
||||
},
|
||||
// 网络
|
||||
{
|
||||
key: 'network',
|
||||
@@ -90,6 +95,7 @@ function toRawString(e) {
|
||||
|
||||
// 不同错误类型默认对应的行动按钮(label 走 i18n,route 直接跳转)
|
||||
const DEFAULT_ACTIONS = {
|
||||
nodeVersion: { labelKey: 'common.errorAction.checkNode', route: '/setup' },
|
||||
gatewayDown: { labelKey: 'common.errorAction.startGateway', route: '/services' },
|
||||
cmdMissing: { labelKey: 'common.errorAction.openSettings', route: '/settings' },
|
||||
permission: { labelKey: 'common.errorAction.openSettings', route: '/settings' },
|
||||
|
||||
@@ -397,6 +397,7 @@ export const api = {
|
||||
checkGit: () => cachedInvoke('check_git', {}, 60000),
|
||||
scanGitPaths: () => invoke('scan_git_paths'),
|
||||
autoInstallGit: () => invoke('auto_install_git'),
|
||||
autoInstallNode: () => invoke('auto_install_node').then(r => { invalidate('check_node', 'get_services_status'); invoke('invalidate_path_cache').catch(() => {}); return r }),
|
||||
configureGitHttps: () => invoke('configure_git_https'),
|
||||
getDeployConfig: () => cachedInvoke('get_deploy_config'),
|
||||
patchModelVision: () => invoke('patch_model_vision'),
|
||||
|
||||
@@ -75,6 +75,7 @@ export default {
|
||||
// 每个错误同时有 短文案 (error.*) + 行动建议 (errorHint.*)
|
||||
// ---------------------------------------------------------------------
|
||||
error: {
|
||||
nodeVersion: _('Node.js 版本过低', 'Node.js version is too old', 'Node.js 版本過低', 'Node.js のバージョンが古すぎます', 'Node.js 버전이 너무 낮습니다', 'Phiên bản Node.js quá cũ', 'La versión de Node.js es demasiado antigua', 'A versão do Node.js é muito antiga', 'Версия Node.js слишком старая', 'La version de Node.js est trop ancienne', 'Node.js-Version ist zu alt'),
|
||||
network: _('网络不通', 'Network unreachable', '網路不通', 'ネットワークに接続できません', '네트워크에 연결할 수 없습니다', 'Không có kết nối mạng', 'Sin conexión', 'Sem conexão', 'Нет сети', 'Pas de réseau', 'Keine Verbindung'),
|
||||
gatewayDown: _('Gateway 未启动', 'Gateway is not running', 'Gateway 未啟動', 'Gateway が起動していません', 'Gateway가 실행되고 있지 않습니다', 'Gateway chưa khởi động', 'Gateway no está en ejecución', 'Gateway não está em execução', 'Gateway не запущен', 'Gateway non démarré', 'Gateway läuft nicht'),
|
||||
cmdMissing: _('命令未找到', 'Command not found', '命令未找到', 'コマンドが見つかりません', '명령을 찾을 수 없습니다', 'Không tìm thấy lệnh', 'Comando no encontrado', 'Comando não encontrado', 'Команда не найдена', 'Commande introuvable', 'Befehl nicht gefunden'),
|
||||
@@ -87,6 +88,7 @@ export default {
|
||||
generic: _('操作未完成', 'Action failed', '操作未完成', '操作が完了しませんでした', '작업이 완료되지 않았습니다', 'Thao tác chưa hoàn thành', 'Acción fallida', 'Ação falhou', 'Действие не выполнено', 'Action échouée', 'Aktion fehlgeschlagen'),
|
||||
},
|
||||
errorHint: {
|
||||
nodeVersion: _('请升级 Node.js 后点击重新检测,再启动 Gateway', 'Upgrade Node.js, click re-detect, then start Gateway again', '請升級 Node.js 後點擊重新檢測,再啟動 Gateway', 'Node.js をアップグレードして再検出し、Gateway を再起動してください', 'Node.js를 업그레이드한 뒤 다시 감지하고 Gateway를 시작하세요', 'Hãy nâng cấp Node.js, kiểm tra lại, rồi khởi động Gateway', 'Actualiza Node.js, vuelve a detectar e inicia Gateway', 'Atualize o Node.js, detecte novamente e inicie o Gateway', 'Обновите Node.js, повторите проверку и запустите Gateway', 'Mettez Node.js à jour, relancez la détection, puis démarrez Gateway', 'Aktualisieren Sie Node.js, erkennen Sie erneut und starten Sie Gateway'),
|
||||
network: _('请检查网络连接,或确认你正在访问的服务器是否可达', 'Check your network connection, or confirm the target server is reachable', '請檢查網路連線,或確認目標伺服器是否可達', 'ネットワーク接続を確認するか、対象サーバーに到達できるか確認してください', '네트워크 연결을 확인하거나 대상 서버에 도달할 수 있는지 확인하세요', 'Hãy kiểm tra kết nối mạng hoặc xác nhận máy chủ đích có thể truy cập', 'Verifica tu conexión de red o confirma que el servidor destino sea accesible', 'Verifique sua conexão ou se o servidor de destino está acessível', 'Проверьте сеть или доступность целевого сервера', 'Vérifiez votre réseau ou la disponibilité du serveur cible', 'Prüfen Sie Ihre Netzwerkverbindung oder die Erreichbarkeit des Zielservers'),
|
||||
gatewayDown: _('请前往仪表盘启动 Gateway 后重试', 'Open the dashboard to start the Gateway, then retry', '請前往儀表板啟動 Gateway 後重試', 'ダッシュボードで Gateway を起動してから再試行してください', '대시보드에서 Gateway를 시작한 후 다시 시도하세요', 'Vui lòng vào bảng điều khiển để khởi động Gateway rồi thử lại', 'Abre el panel para iniciar Gateway y reintenta', 'Abra o painel para iniciar o Gateway e tente novamente', 'Откройте панель и запустите Gateway, затем повторите', 'Ouvrez le tableau de bord pour démarrer Gateway, puis réessayez', 'Öffnen Sie das Dashboard, um das Gateway zu starten, und versuchen Sie es erneut'),
|
||||
cmdMissing: _('请确认依赖已正确安装;可在设置页指定可执行文件路径', 'Make sure the dependency is installed; you can specify the executable path in Settings', '請確認相依套件已安裝;可在設定頁指定可執行檔案路徑', '依存関係がインストールされていることを確認してください。設定ページで実行ファイルのパスを指定できます', '의존성이 설치되어 있는지 확인하세요; 설정 페이지에서 실행 파일 경로를 지정할 수 있습니다', 'Hãy bảo đảm phụ thuộc đã được cài đặt; bạn có thể chỉ định đường dẫn tệp thực thi trong Cài đặt', 'Asegúrate de que la dependencia esté instalada; puedes especificar la ruta del ejecutable en Configuración', 'Verifique se a dependência está instalada; você pode especificar o caminho do executável em Configurações', 'Убедитесь, что зависимость установлена; путь к исполняемому файлу можно указать в настройках', 'Vérifiez que la dépendance est installée ; vous pouvez préciser le chemin de l\u2019exécutable dans Paramètres', 'Stellen Sie sicher, dass die Abhängigkeit installiert ist; den Pfad zur ausführbaren Datei können Sie in den Einstellungen festlegen'),
|
||||
@@ -108,6 +110,7 @@ export default {
|
||||
errorRawLabel: _('技术详情', 'Technical details', '技術詳情', '技術的詳細', '기술 정보', 'Chi tiết kỹ thuật', 'Detalles técnicos', 'Detalhes técnicos', 'Технические подробности', 'Détails techniques', 'Technische Details'),
|
||||
// 智能行动按钮(toast 副标题旁的快捷跳转)
|
||||
errorAction: {
|
||||
checkNode: _('检查 Node.js', 'Check Node.js', '檢查 Node.js', 'Node.js を確認', 'Node.js 확인', 'Kiểm tra Node.js', 'Comprobar Node.js', 'Verificar Node.js', 'Проверить Node.js', 'Vérifier Node.js', 'Node.js prüfen'),
|
||||
startGateway: _('去启动 Gateway', 'Start Gateway', '去啟動 Gateway', 'Gateway を起動', 'Gateway 시작', 'Khởi động Gateway', 'Iniciar Gateway', 'Iniciar Gateway', 'Запустить Gateway', 'Démarrer Gateway', 'Gateway starten'),
|
||||
openSettings: _('打开设置', 'Open Settings', '打開設定', '設定を開く', '설정 열기', 'Mở cài đặt', 'Abrir configuración', 'Abrir configurações', 'Открыть настройки', 'Ouvrir les paramètres', 'Einstellungen öffnen'),
|
||||
checkApiKey: _('检查 API Key', 'Check API Key', '檢查 API Key', 'API Key を確認', 'API Key 확인', 'Kiểm tra API Key', 'Comprobar API Key', 'Verificar API Key', 'Проверить API Key', 'Vérifier la clé API', 'API-Key prüfen'),
|
||||
|
||||
@@ -124,6 +124,31 @@ export default {
|
||||
domesticMirrorShort: _('国内镜像:', 'China mirror:', '國內鏡像:'),
|
||||
promptNodeMissing: _('Node.js 未安装或未检测到', 'Node.js not installed or not detected', 'Node.js 未安裝或未檢測到'),
|
||||
promptNodeOk: _('Node.js 已安装: {version}', 'Node.js installed: {version}', 'Node.js 已安裝: {version}'),
|
||||
nodeVersionUnsupported: _('Node.js {version} 版本过低,当前 OpenClaw 要求 {required}', 'Node.js {version} is too old; current OpenClaw requires {required}', 'Node.js {version} 版本過低,目前 OpenClaw 要求 {required}'),
|
||||
promptNodeUnsupported: _('Node.js 版本过低: 当前 {version},要求 {required}', 'Node.js version is too old: current {version}, required {required}', 'Node.js 版本過低:目前 {version},要求 {required}'),
|
||||
nodeUpgradeHint: _('当前 Node.js {version} 版本过低,OpenClaw 要求 {required}。请先升级 Node.js,否则 Gateway 无法启动。', 'Current Node.js {version} is too old; OpenClaw requires {required}. Upgrade Node.js first or Gateway cannot start.', '目前 Node.js {version} 版本過低,OpenClaw 要求 {required}。請先升級 Node.js,否則 Gateway 無法啟動。'),
|
||||
nodeUnsupportedTitle: _('已检测到 Node.js,但版本不满足要求', 'Node.js was detected, but the version is not supported', '已檢測到 Node.js,但版本不符合要求'),
|
||||
winNodeUpgradeHint: _('Windows 可尝试一键升级;如果失败,请手动安装 Node.js 22.19.0 或更高版本。升级后点击「重新检测」。', 'On Windows, try one-click upgrade. If it fails, manually install Node.js 22.19.0 or newer. Click "Re-detect" after upgrading.', 'Windows 可嘗試一鍵升級;如果失敗,請手動安裝 Node.js 22.19.0 或更高版本。升級後點擊「重新檢測」。'),
|
||||
autoUpgradeNodeBtn: _('一键升级 Node.js', 'Upgrade Node.js', '一鍵升級 Node.js'),
|
||||
upgradingNode: _('升级中...', 'Upgrading...', '升級中...'),
|
||||
downloadLatestNode: _('下载新版 Node.js', 'Download latest Node.js', '下載新版 Node.js'),
|
||||
nodeUpgradeSuccess: _('Node.js 已升级,请重新检测', 'Node.js upgraded, please re-detect', 'Node.js 已升級,請重新檢測'),
|
||||
nodeAutoUpgradeFailed: _('Node.js 自动升级失败: {err}', 'Node.js auto upgrade failed: {err}', 'Node.js 自動升級失敗: {err}'),
|
||||
nodeAutoUpgradeFailedTitle: _('Node.js 自动升级失败', 'Node.js auto upgrade failed', 'Node.js 自動升級失敗'),
|
||||
nodeUpgradeTitle: _('升级 Node.js', 'Upgrade Node.js', '升級 Node.js'),
|
||||
nodeUpgradeConfirmTitle: _('确认升级 Node.js?', 'Upgrade Node.js?', '確認升級 Node.js?'),
|
||||
nodeUpgradeConfirmMessage: _('ClawPanel 将调用 Windows winget 安装或升级 Node.js LTS。', 'ClawPanel will use Windows winget to install or upgrade Node.js LTS.', 'ClawPanel 將呼叫 Windows winget 安裝或升級 Node.js LTS。'),
|
||||
nodeUpgradeConfirmImpactWinget: _('会执行 winget upgrade/install OpenJS.NodeJS.LTS。', 'Runs winget upgrade/install OpenJS.NodeJS.LTS.', '會執行 winget upgrade/install OpenJS.NodeJS.LTS。'),
|
||||
nodeUpgradeConfirmImpactPermission: _('Windows 可能弹出安装确认或权限提示。', 'Windows may show an installer confirmation or permission prompt.', 'Windows 可能彈出安裝確認或權限提示。'),
|
||||
nodeUpgradeConfirmImpactRedetect: _('完成后会刷新 PATH 并自动重新检测环境。', 'After completion, PATH is refreshed and the environment is re-detected.', '完成後會刷新 PATH 並自動重新檢測環境。'),
|
||||
nodeUpgradePreparing: _('准备升级 Node.js...', 'Preparing Node.js upgrade...', '準備升級 Node.js...'),
|
||||
nodeUpgradeInstalling: _('正在安装或升级 Node.js...', 'Installing or upgrading Node.js...', '正在安裝或升級 Node.js...'),
|
||||
nodeUpgradeVerifying: _('正在重新检测 Node.js...', 'Re-detecting Node.js...', '正在重新檢測 Node.js...'),
|
||||
nodeUpgradeDone: _('Node.js 检测通过', 'Node.js check passed', 'Node.js 檢測通過'),
|
||||
nodeUpgradeStarting: _('开始检查 Windows winget 和 Node.js LTS 安装状态...', 'Checking Windows winget and Node.js LTS install state...', '開始檢查 Windows winget 和 Node.js LTS 安裝狀態...'),
|
||||
nodeUpgradeRedetecting: _('已触发重新检测;如果仍显示旧版本,请重启 ClawPanel 或检查 PATH 优先级。', 'Re-detecting now. If the old version still appears, restart ClawPanel or check PATH priority.', '已觸發重新檢測;如果仍顯示舊版本,請重啟 ClawPanel 或檢查 PATH 優先級。'),
|
||||
nodeManualInstallHint: _('请手动安装 Node.js 22.19.0 或更高版本:', 'Please manually install Node.js 22.19.0 or newer:', '請手動安裝 Node.js 22.19.0 或更高版本:'),
|
||||
nodeUpgradeRestartHint: _('安装后回到 ClawPanel 点击「重新检测」。如果仍检测到旧版本,重启 ClawPanel 或把新版 Node.js 路径放到 PATH 更靠前的位置。', 'After installing, return to ClawPanel and click "Re-detect". If the old version is still detected, restart ClawPanel or move the newer Node.js path earlier in PATH.', '安裝後回到 ClawPanel 點擊「重新檢測」。如果仍檢測到舊版本,請重啟 ClawPanel 或把新版 Node.js 路徑放到 PATH 更靠前的位置。'),
|
||||
promptGitMissing: _('Git 未安装', 'Git not installed', 'Git 未安裝'),
|
||||
promptGitOk: _('Git 已安装: {version}', 'Git installed: {version}', 'Git 已安裝: {version}'),
|
||||
promptCliMissing: _('OpenClaw CLI 未安装', 'OpenClaw CLI not installed', 'OpenClaw CLI 未安裝'),
|
||||
@@ -157,8 +182,11 @@ export default {
|
||||
scanningPaths: _('正在扫描常见安装路径...', 'Scanning common install paths...', '正在掃描常见安裝路徑...'),
|
||||
scanNotFound: _('未找到 Node.js 安装,请手动指定路径或下载安装。', 'Node.js not found, please specify path manually or download.', '未找到 Node.js 安裝,請手動指定路徑或下載安裝。'),
|
||||
scanUseBtn: _('使用', 'Use'),
|
||||
nodeUnavailableBtn: _('不可用', 'Unavailable', '不可用'),
|
||||
nodeVersionTooLowShort: _('要求 {required}', 'Requires {required}', '要求 {required}'),
|
||||
scanFailed: _('扫描失败: {err}', 'Scan failed: {err}', '掃描失敗: {err}'),
|
||||
nodeSaved: _('Node.js 路径已保存,正在重新检测...', 'Node.js path saved, re-detecting...', 'Node.js 路徑已儲存,正在重新檢測...'),
|
||||
nodePathSaveFailed: _('Node.js 路径保存失败: {err}', 'Failed to save Node.js path: {err}', 'Node.js 路徑儲存失敗: {err}'),
|
||||
detecting2: _('检测中...', 'Detecting...', '檢測中...'),
|
||||
nodeFoundSaved: _('找到 Node.js {version},路径已保存', 'Found Node.js {version}, path saved', '找到 Node.js {version},路徑已儲存'),
|
||||
nodeNotFoundAtPath: _('该目录下未找到 node 可执行文件,请确认路径正确。', 'Node executable not found in this directory, please verify the path.', '該目錄下未找到 node 可執行檔案,請確認路徑正確。'),
|
||||
|
||||
@@ -721,7 +721,7 @@ async function handleServiceAction(action, label, page) {
|
||||
// 而不是丢一个 toast 让小白对着 stderr 干瞪眼。
|
||||
await _promptDoctorFix(page, actionLabel, e)
|
||||
} else {
|
||||
toast(t('services.actionCmdFailed', { action: actionLabel, error: e.message || e }), 'error')
|
||||
toast(humanizeError(e, t('services.actionCmdFailed', { action: actionLabel, error: '' }).trim()), 'error')
|
||||
}
|
||||
if (actionsEl) actionsEl.innerHTML = origHtml
|
||||
if (dot) dot.className = 'status-dot stopped'
|
||||
@@ -1136,7 +1136,7 @@ async function _promptDoctorFix(page, actionLabel, originalErr) {
|
||||
}
|
||||
)
|
||||
if (!yes) {
|
||||
toast(t('services.actionCmdFailed', { action: actionLabel, error: errMsg }), 'error')
|
||||
toast(humanizeError(errMsg, t('services.actionCmdFailed', { action: actionLabel, error: '' }).trim()), 'error')
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
* 初始设置页面 — openclaw 未安装时的引导
|
||||
* 自动检测环境 → 版本选择 → 一键安装 → 自动跳转
|
||||
*/
|
||||
import { api, invalidate } from '../lib/tauri-api.js'
|
||||
import { api, invalidate, isTauriRuntime, safeTauriListen } from '../lib/tauri-api.js'
|
||||
import { showConfirm, showUpgradeModal } from '../components/modal.js'
|
||||
import { toast } from '../components/toast.js'
|
||||
import { setUpgrading, isMacPlatform } from '../lib/app-state.js'
|
||||
@@ -50,6 +50,14 @@ function buildStatusMeta(...parts) {
|
||||
.join(' · ')
|
||||
}
|
||||
|
||||
function isWindowsPlatform() {
|
||||
return navigator.platform?.startsWith('Win') || navigator.userAgent?.includes('Windows')
|
||||
}
|
||||
|
||||
function canAutoUpgradeNode() {
|
||||
return isTauriRuntime() && isWindowsPlatform()
|
||||
}
|
||||
|
||||
function renderDetectionHint(pathValue, sourceLabel = '') {
|
||||
const normalizedPath = String(pathValue || '').trim()
|
||||
const normalizedSource = String(sourceLabel || '').trim()
|
||||
@@ -215,7 +223,7 @@ async function runDetect(page) {
|
||||
api.configureGitHttps().catch(() => {})
|
||||
}
|
||||
|
||||
const nodeOk = node.installed
|
||||
const nodeOk = node.installed && node.compatible !== false
|
||||
const allOk = nodeOk && cliOk && config.installed
|
||||
|
||||
// 全部通过 → 自动跳转到仪表盘
|
||||
@@ -236,10 +244,12 @@ function stepIcon(ok) {
|
||||
|
||||
function renderSteps(page, { node, git, cliOk, config, version }) {
|
||||
const stepsEl = page.querySelector('#setup-steps')
|
||||
const nodeOk = node.installed
|
||||
const nodeOk = node.installed && node.compatible !== false
|
||||
const gitOk = git?.installed || false
|
||||
const allOk = nodeOk && cliOk && config.installed
|
||||
const nodeStatusMeta = nodeOk
|
||||
const nodeStatusMeta = node.installed && node.compatible === false
|
||||
? t('setup.nodeVersionUnsupported', { version: node.version || t('common.unknown'), required: node.requiredVersion || t('common.unknown') })
|
||||
: nodeOk
|
||||
? buildStatusMeta(node.version || t('setup.statusReady'), node.path)
|
||||
: t('setup.statusActionNeeded')
|
||||
const gitStatusMeta = gitOk
|
||||
@@ -270,22 +280,26 @@ function renderSteps(page, { node, git, cliOk, config, version }) {
|
||||
|
||||
// 第一步:Node.js
|
||||
if (!nodeOk) {
|
||||
const nodeTooOld = node.installed && node.compatible === false
|
||||
html += `
|
||||
<div class="config-section" style="text-align:left">
|
||||
<div class="config-section-title" style="display:flex;align-items:center;gap:4px">
|
||||
${stepIcon(nodeOk)} ${t('setup.stepNode')}
|
||||
</div>
|
||||
<p style="color:var(--text-secondary);font-size:var(--font-size-sm);margin-bottom:var(--space-sm)">
|
||||
${t('setup.stepNodeHint')}
|
||||
${nodeTooOld ? t('setup.nodeUpgradeHint', { version: node.version || t('common.unknown'), required: node.requiredVersion || t('common.unknown') }) : t('setup.stepNodeHint')}
|
||||
</p>
|
||||
<a class="btn btn-primary btn-sm" href="https://nodejs.org/" target="_blank" rel="noopener">${t('setup.downloadNode')}</a>
|
||||
${nodeTooOld && canAutoUpgradeNode()
|
||||
? `<button class="btn btn-primary btn-sm" id="btn-auto-install-node">${t('setup.autoUpgradeNodeBtn')}</button>`
|
||||
: ''}
|
||||
<a class="btn ${nodeTooOld && canAutoUpgradeNode() ? 'btn-secondary' : 'btn-primary'} btn-sm" href="https://nodejs.org/" target="_blank" rel="noopener">${nodeTooOld ? t('setup.downloadLatestNode') : t('setup.downloadNode')}</a>
|
||||
<span class="form-hint" style="margin-left:8px">${t('setup.recheckAfterInstall')}</span>
|
||||
<div style="margin-top:var(--space-sm);padding:10px 12px;background:var(--bg-tertiary);border-radius:var(--radius-sm);font-size:var(--font-size-xs);color:var(--text-secondary);line-height:1.6">
|
||||
<strong>${t('setup.nodeInstalledButNotDetected')}</strong>
|
||||
<strong>${nodeTooOld ? t('setup.nodeUnsupportedTitle') : t('setup.nodeInstalledButNotDetected')}</strong>
|
||||
${isMacPlatform()
|
||||
? `${t('setup.macNodeHint')}<br>
|
||||
<code style="background:var(--bg-secondary);padding:2px 6px;border-radius:3px;user-select:all">open /Applications/ClawPanel.app</code>`
|
||||
: `${t('setup.winNodeHint')}`
|
||||
: `${nodeTooOld ? t('setup.winNodeUpgradeHint') : t('setup.winNodeHint')}`
|
||||
}
|
||||
<div style="margin-top:8px;display:flex;gap:6px;align-items:center;flex-wrap:wrap">
|
||||
<button class="btn btn-secondary btn-sm" id="btn-scan-node" style="font-size:11px;padding:3px 10px">${icon('search', 12)} ${t('setup.scanNodeBtn')}</button>
|
||||
@@ -590,6 +604,7 @@ function renderEnvironmentHint() {
|
||||
function buildSetupProblemPrompt({ node, git, cliOk, config }) {
|
||||
const problems = []
|
||||
if (!node.installed) problems.push(`- ${t('setup.promptNodeMissing')}`)
|
||||
else if (node.compatible === false) problems.push(`- ${t('setup.promptNodeUnsupported', { version: node.version || t('common.unknown'), required: node.requiredVersion || t('common.unknown') })}`)
|
||||
else problems.push(`- ${t('setup.promptNodeOk', { version: node.version || t('common.unknown') })}`)
|
||||
if (!git?.installed) problems.push(`- ${t('setup.promptGitMissing')}`)
|
||||
else problems.push(`- ${t('setup.promptGitOk', { version: git.version || t('common.unknown') })}`)
|
||||
@@ -666,6 +681,68 @@ function bindEvents(page, nodeOk, detectState) {
|
||||
}
|
||||
})
|
||||
|
||||
// 一键安装 / 升级 Node.js
|
||||
page.querySelector('#btn-auto-install-node')?.addEventListener('click', async () => {
|
||||
const btn = page.querySelector('#btn-auto-install-node')
|
||||
const yes = await showConfirm({
|
||||
title: t('setup.nodeUpgradeConfirmTitle'),
|
||||
message: t('setup.nodeUpgradeConfirmMessage'),
|
||||
impact: [
|
||||
t('setup.nodeUpgradeConfirmImpactWinget'),
|
||||
t('setup.nodeUpgradeConfirmImpactPermission'),
|
||||
t('setup.nodeUpgradeConfirmImpactRedetect'),
|
||||
],
|
||||
confirmText: t('setup.autoUpgradeNodeBtn'),
|
||||
cancelText: t('common.cancel'),
|
||||
variant: 'primary',
|
||||
})
|
||||
if (!yes) return
|
||||
|
||||
const modal = showUpgradeModal(t('setup.nodeUpgradeTitle'))
|
||||
modal.setProgressLabels({
|
||||
preparing: t('setup.nodeUpgradePreparing'),
|
||||
downloading: t('setup.nodeUpgradeInstalling'),
|
||||
installing: t('setup.nodeUpgradeVerifying'),
|
||||
done: t('setup.nodeUpgradeDone'),
|
||||
})
|
||||
modal.setProgress(15)
|
||||
modal.appendLog(t('setup.nodeUpgradeStarting'))
|
||||
|
||||
let unlistenLog, unlistenProgress
|
||||
const cleanup = () => {
|
||||
setUpgrading(false)
|
||||
unlistenLog?.()
|
||||
unlistenProgress?.()
|
||||
}
|
||||
setUpgrading(true)
|
||||
btn.disabled = true
|
||||
btn.textContent = t('setup.upgradingNode')
|
||||
try {
|
||||
unlistenLog = await safeTauriListen('upgrade-log', (e) => modal.appendLog(e.payload))
|
||||
unlistenProgress = await safeTauriListen('upgrade-progress', (e) => modal.setProgress(e.payload))
|
||||
const msg = await api.autoInstallNode()
|
||||
modal.setProgress(100)
|
||||
modal.setDone(msg || t('setup.nodeUpgradeSuccess'))
|
||||
modal.appendLog(t('setup.nodeUpgradeRedetecting'))
|
||||
toast(msg || t('setup.nodeUpgradeSuccess'), 'success')
|
||||
await api.invalidatePathCache().catch(() => {})
|
||||
setTimeout(() => runDetect(page), 800)
|
||||
} catch (e) {
|
||||
const errMsg = String(e?.message || e)
|
||||
modal.setError(t('setup.nodeAutoUpgradeFailedTitle'))
|
||||
modal.appendLog(errMsg)
|
||||
modal.appendLog('')
|
||||
modal.appendLog(t('setup.nodeManualInstallHint'))
|
||||
modal.appendLog('https://nodejs.org/')
|
||||
modal.appendLog(t('setup.nodeUpgradeRestartHint'))
|
||||
toast(t('setup.nodeAutoUpgradeFailed', { err: errMsg }), 'error')
|
||||
btn.disabled = false
|
||||
btn.textContent = t('setup.autoUpgradeNodeBtn')
|
||||
} finally {
|
||||
cleanup()
|
||||
}
|
||||
})
|
||||
|
||||
// 自定义 OpenClaw 安装路径
|
||||
const dirInput = page.querySelector('#input-openclaw-dir')
|
||||
const dirResultEl = page.querySelector('#openclaw-dir-result')
|
||||
@@ -803,19 +880,30 @@ function bindEvents(page, nodeOk, detectState) {
|
||||
if (results.length === 0) {
|
||||
resultEl.innerHTML = `<span style="color:var(--warning)">${t('setup.scanNotFound')}</span>`
|
||||
} else {
|
||||
resultEl.innerHTML = results.map(r =>
|
||||
`<div style="display:flex;align-items:center;gap:6px;margin-top:4px">
|
||||
<span style="color:var(--success)">✓</span>
|
||||
<code style="flex:1;background:var(--bg-secondary);padding:2px 6px;border-radius:3px;font-size:11px">${r.path}</code>
|
||||
<span style="font-size:11px;color:var(--text-tertiary)">${r.version}</span>
|
||||
<button class="btn btn-primary btn-sm btn-use-path" data-path="${r.path}" style="font-size:10px;padding:2px 8px">${t('setup.scanUseBtn')}</button>
|
||||
resultEl.innerHTML = results.map(r => {
|
||||
const compatible = r.compatible !== false
|
||||
const color = compatible ? 'var(--success)' : 'var(--danger)'
|
||||
const status = compatible
|
||||
? ''
|
||||
: `<span style="font-size:11px;color:var(--danger)">${t('setup.nodeVersionTooLowShort', { required: r.requiredVersion || t('common.unknown') })}</span>`
|
||||
return `<div style="display:flex;align-items:center;gap:6px;margin-top:4px">
|
||||
<span style="color:${color}">${compatible ? '✓' : '✗'}</span>
|
||||
<code style="flex:1;background:var(--bg-secondary);padding:2px 6px;border-radius:3px;font-size:11px">${escapeHtml(r.path)}</code>
|
||||
<span style="font-size:11px;color:var(--text-tertiary)">${escapeHtml(r.version)}</span>
|
||||
${status}
|
||||
<button class="btn btn-primary btn-sm btn-use-path" data-path="${escapeHtml(r.dir || r.path)}" style="font-size:10px;padding:2px 8px" ${compatible ? '' : 'disabled'}>${compatible ? t('setup.scanUseBtn') : t('setup.nodeUnavailableBtn')}</button>
|
||||
</div>`
|
||||
}
|
||||
).join('')
|
||||
resultEl.querySelectorAll('.btn-use-path').forEach(b => {
|
||||
b.addEventListener('click', async () => {
|
||||
await api.saveCustomNodePath(b.dataset.path)
|
||||
toast(t('setup.nodeSaved'), 'success')
|
||||
setTimeout(() => runDetect(page), 300)
|
||||
try {
|
||||
await api.saveCustomNodePath(b.dataset.path)
|
||||
toast(t('setup.nodeSaved'), 'success')
|
||||
setTimeout(() => runDetect(page), 300)
|
||||
} catch (e) {
|
||||
toast(t('setup.nodePathSaveFailed', { err: e?.message || e }), 'error')
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
@@ -838,6 +926,10 @@ function bindEvents(page, nodeOk, detectState) {
|
||||
try {
|
||||
const result = await api.checkNodeAtPath(dir)
|
||||
if (result.installed) {
|
||||
if (result.compatible === false) {
|
||||
resultEl.innerHTML = `<span style="color:var(--danger)">✗ ${t('setup.nodeVersionUnsupported', { version: result.version || t('common.unknown'), required: result.requiredVersion || t('common.unknown') })}</span>`
|
||||
return
|
||||
}
|
||||
await api.saveCustomNodePath(dir)
|
||||
resultEl.innerHTML = `<span style="color:var(--success)">✓ ${t('setup.nodeFoundSaved', { version: result.version })}</span>`
|
||||
toast(t('setup.nodeSaved'), 'success')
|
||||
@@ -1103,4 +1195,3 @@ function bindEvents(page, nodeOk, detectState) {
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user