mirror of
https://github.com/qingchencloud/clawpanel.git
synced 2026-08-09 15:43:44 +08:00
feat: Windows 兼容性全面改进
- Windows Gateway 启动改为前台 spawn 模式(绕过 schtasks 管理员权限) - 添加全局 Gateway 未启动引导横幅(黄色提示条 + 一键启动按钮) - 所有页面加载动画改为脉冲效果 - 统一 Windows cmd /c 调用加 CREATE_NO_WINDOW 标志 - 托盘菜单复用 service.rs 逻辑 - 新增 utils.rs 封装 openclaw_command - 修复 config 文件 UI 字段污染问题 - 添加 dev.ps1 启动脚本
This commit is contained in:
@@ -12,7 +12,7 @@ export async function render() {
|
||||
|
||||
page.innerHTML = `
|
||||
<div class="page-header" style="display:flex;align-items:center;gap:16px">
|
||||
<img src="/images/logo.svg" alt="ClawPanel" style="width:48px;height:48px;border-radius:var(--radius-md)">
|
||||
<img src="/images/logo-brand.png" alt="ClawPanel" style="height:48px;width:auto">
|
||||
<div>
|
||||
<h1 class="page-title" style="margin:0">ClawPanel</h1>
|
||||
<p class="page-desc" style="margin:0">OpenClaw 可视化管理面板</p>
|
||||
|
||||
@@ -21,12 +21,13 @@ export async function render() {
|
||||
</div>
|
||||
</div>
|
||||
<div class="page-content">
|
||||
<div id="agents-list"></div>
|
||||
<div id="agents-list" class="loading-text">加载中...</div>
|
||||
</div>
|
||||
`
|
||||
|
||||
const state = { agents: [] }
|
||||
await loadAgents(page, state)
|
||||
// 非阻塞:先返回 DOM,后台加载数据
|
||||
loadAgents(page, state)
|
||||
|
||||
page.querySelector('#btn-add-agent').addEventListener('click', () => showAddAgentDialog(page, state))
|
||||
|
||||
|
||||
@@ -109,7 +109,8 @@ export async function render() {
|
||||
_cmdPanelEl = page.querySelector('#chat-cmd-panel')
|
||||
|
||||
bindEvents(page)
|
||||
await connectGateway()
|
||||
// 非阻塞:先返回 DOM,后台连接 Gateway
|
||||
connectGateway()
|
||||
return page
|
||||
}
|
||||
|
||||
|
||||
@@ -29,7 +29,7 @@ export async function render() {
|
||||
</div>
|
||||
<div class="config-section">
|
||||
<div class="config-section-title">最近日志</div>
|
||||
<div class="log-viewer" id="recent-logs" style="max-height:300px">加载中...</div>
|
||||
<div class="log-viewer" id="recent-logs" style="max-height:300px"><div class="loading-text">加载中...</div></div>
|
||||
</div>
|
||||
`
|
||||
|
||||
|
||||
@@ -27,12 +27,12 @@ export async function render() {
|
||||
<div id="cftunnel-card" class="config-section">
|
||||
<div class="config-section-title">cftunnel 内网穿透</div>
|
||||
<div class="form-hint" style="margin-bottom:var(--space-md)">通过 Cloudflare Tunnel 将本地服务暴露到公网,无需公网 IP 和端口映射。</div>
|
||||
<div id="cftunnel-content">加载中...</div>
|
||||
<div id="cftunnel-content" class="loading-text">加载中...</div>
|
||||
</div>
|
||||
<div id="clawapp-card" class="config-section">
|
||||
<div class="config-section-title">ClawApp 移动客户端</div>
|
||||
<div class="form-hint" style="margin-bottom:var(--space-md)">基于 LobeChat 的 AI 对话客户端,通过 Gateway 连接模型服务。支持本地和外网访问。</div>
|
||||
<div id="clawapp-content">加载中...</div>
|
||||
<div id="clawapp-content" class="loading-text">加载中...</div>
|
||||
</div>
|
||||
`
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ export async function render() {
|
||||
<h1 class="page-title">Gateway 配置</h1>
|
||||
<p class="page-desc">Gateway 是 AI 模型的统一入口,所有应用通过它来调用模型服务</p>
|
||||
</div>
|
||||
<div id="gateway-config">加载中...</div>
|
||||
<div id="gateway-config" class="loading-text">加载中...</div>
|
||||
<div class="gw-save-bar">
|
||||
<button class="btn btn-primary" id="btn-save-gw">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="16" height="16"><path d="M19 21H5a2 2 0 01-2-2V5a2 2 0 012-2h11l5 5v11a2 2 0 01-2 2z"/><path d="M17 21v-8H7v8"/><path d="M7 3v5h8"/></svg>
|
||||
@@ -24,7 +24,8 @@ export async function render() {
|
||||
`
|
||||
|
||||
const state = { config: null }
|
||||
await loadConfig(page, state)
|
||||
// 非阻塞:先返回 DOM,后台加载数据
|
||||
loadConfig(page, state)
|
||||
page.querySelector('#btn-save-gw').onclick = async () => {
|
||||
const btn = page.querySelector('#btn-save-gw')
|
||||
btn.disabled = true
|
||||
|
||||
@@ -33,7 +33,7 @@ export async function render() {
|
||||
<input type="checkbox" id="log-autoscroll" checked> 自动滚动
|
||||
</label>
|
||||
</div>
|
||||
<div class="log-viewer" id="log-content" style="height:calc(100vh - 280px)">加载中...</div>
|
||||
<div class="log-viewer" id="log-content" style="height:calc(100vh - 280px)"><div class="loading-text">加载中...</div></div>
|
||||
`
|
||||
|
||||
let currentTab = 'gateway'
|
||||
|
||||
@@ -15,23 +15,12 @@ export async function render() {
|
||||
const page = document.createElement('div')
|
||||
page.className = 'page'
|
||||
|
||||
// 先获取 agent 列表
|
||||
let agents = []
|
||||
try {
|
||||
agents = await api.listAgents()
|
||||
} catch { agents = [{ id: 'main', identityName: '默认' }] }
|
||||
|
||||
const agentOptions = agents.map(a => {
|
||||
const label = a.identityName ? a.identityName.split(',')[0].trim() : a.id
|
||||
return `<option value="${a.id}">${a.id}${a.id !== label ? ' — ' + label : ''}</option>`
|
||||
}).join('')
|
||||
|
||||
page.innerHTML = `
|
||||
<div class="page-header">
|
||||
<h1 class="page-title">记忆文件</h1>
|
||||
<div class="page-actions" style="display:flex;align-items:center;gap:var(--space-sm)">
|
||||
<label style="font-size:var(--font-size-sm);color:var(--text-tertiary)">Agent:</label>
|
||||
<select class="form-input" id="agent-select" style="width:auto;min-width:140px">${agentOptions}</select>
|
||||
<select class="form-input" id="agent-select" style="width:auto;min-width:140px"><option value="main">main</option></select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="tab-bar">
|
||||
@@ -47,7 +36,7 @@ export async function render() {
|
||||
<div style="padding:0 var(--space-sm) var(--space-sm)">
|
||||
<button class="btn btn-sm btn-secondary" id="btn-export-zip" style="width:100%">打包下载全部</button>
|
||||
</div>
|
||||
<div id="file-tree">加载中...</div>
|
||||
<div id="file-tree" class="loading-text">加载中...</div>
|
||||
</div>
|
||||
<div class="memory-editor">
|
||||
<div class="editor-toolbar">
|
||||
@@ -65,6 +54,17 @@ export async function render() {
|
||||
|
||||
const state = { category: 'memory', currentPath: null, agentId: 'main' }
|
||||
|
||||
// 非阻塞加载 agent 列表,然后填充下拉框
|
||||
api.listAgents().then(agents => {
|
||||
const select = page.querySelector('#agent-select')
|
||||
if (!select) return
|
||||
const options = agents.map(a => {
|
||||
const label = a.identityName ? a.identityName.split(',')[0].trim() : a.id
|
||||
return `<option value="${a.id}">${a.id}${a.id !== label ? ' — ' + label : ''}</option>`
|
||||
}).join('')
|
||||
select.innerHTML = options
|
||||
}).catch(() => {})
|
||||
|
||||
// Agent 切换
|
||||
page.querySelector('#agent-select').onchange = (e) => {
|
||||
state.agentId = e.target.value
|
||||
|
||||
@@ -64,11 +64,12 @@ export async function render() {
|
||||
<div style="margin-bottom:var(--space-md)">
|
||||
<input class="form-input" id="model-search" placeholder="搜索模型(按 ID 或名称过滤)" style="max-width:360px">
|
||||
</div>
|
||||
<div id="providers-list">加载中...</div>
|
||||
<div id="providers-list" class="loading-text">加载中...</div>
|
||||
`
|
||||
|
||||
const state = { config: null, search: '', undoStack: [] }
|
||||
await loadConfig(page, state)
|
||||
// 非阻塞:先返回 DOM,后台加载数据
|
||||
loadConfig(page, state)
|
||||
bindTopActions(page, state)
|
||||
|
||||
// 搜索框实时过滤
|
||||
|
||||
@@ -26,10 +26,10 @@ export async function render() {
|
||||
<p class="page-desc">管理 OpenClaw 服务、检查更新、配置备份</p>
|
||||
</div>
|
||||
<div id="version-bar"></div>
|
||||
<div id="services-list">加载中...</div>
|
||||
<div id="services-list" class="loading-text">加载中...</div>
|
||||
<div class="config-section" id="registry-section">
|
||||
<div class="config-section-title">npm 源设置</div>
|
||||
<div id="registry-bar">加载中...</div>
|
||||
<div id="registry-bar" class="loading-text">加载中...</div>
|
||||
</div>
|
||||
<div class="config-section" id="backup-section">
|
||||
<div class="config-section-title">配置备份</div>
|
||||
@@ -37,7 +37,7 @@ export async function render() {
|
||||
<div id="backup-actions" style="margin-bottom:var(--space-md)">
|
||||
<button class="btn btn-primary btn-sm" data-action="create-backup">创建备份</button>
|
||||
</div>
|
||||
<div id="backup-list">加载中...</div>
|
||||
<div id="backup-list" class="loading-text">加载中...</div>
|
||||
</div>
|
||||
`
|
||||
|
||||
@@ -141,25 +141,37 @@ async function loadServices(page) {
|
||||
function renderServices(container, services) {
|
||||
const gw = services.find(s => s.label === 'ai.openclaw.gateway')
|
||||
|
||||
// Gateway 专属卡片(带安装/卸载)
|
||||
let html = ''
|
||||
if (gw) {
|
||||
// 检测 CLI 是否安装
|
||||
const cliMissing = gw.cli_installed === false
|
||||
|
||||
html += `
|
||||
<div class="service-card" data-label="${gw.label}">
|
||||
<div class="service-info">
|
||||
<span class="status-dot ${gw.running ? 'running' : 'stopped'}"></span>
|
||||
<span class="status-dot ${cliMissing ? 'stopped' : gw.running ? 'running' : 'stopped'}"></span>
|
||||
<div>
|
||||
<div class="service-name">${gw.label}</div>
|
||||
<div class="service-desc">${gw.description || ''}${gw.pid ? ' (PID: ' + gw.pid + ')' : ''}</div>
|
||||
<div class="service-desc">${cliMissing
|
||||
? 'OpenClaw CLI 未安装'
|
||||
: (gw.description || '') + (gw.pid ? ' (PID: ' + gw.pid + ')' : '')
|
||||
}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="service-actions">
|
||||
${gw.running
|
||||
? `<button class="btn btn-secondary btn-sm" data-action="restart" data-label="${gw.label}">重启</button>
|
||||
<button class="btn btn-danger btn-sm" data-action="stop" data-label="${gw.label}">停止</button>
|
||||
<button class="btn btn-danger btn-sm" data-action="uninstall-gateway">卸载</button>`
|
||||
: `<button class="btn btn-primary btn-sm" data-action="start" data-label="${gw.label}">启动</button>
|
||||
<button class="btn btn-danger btn-sm" data-action="uninstall-gateway">卸载</button>`
|
||||
${cliMissing
|
||||
? `<div style="display:flex;flex-direction:column;gap:var(--space-xs);align-items:flex-end">
|
||||
<div style="color:var(--text-tertiary);font-size:var(--font-size-xs)">请先安装 OpenClaw CLI:</div>
|
||||
<code style="font-size:var(--font-size-xs);background:var(--bg-tertiary);padding:2px 8px;border-radius:4px;user-select:all">npm install -g @qingchencloud/openclaw-zh</code>
|
||||
<button class="btn btn-secondary btn-sm" data-action="refresh-services" style="margin-top:4px">刷新状态</button>
|
||||
</div>`
|
||||
: gw.running
|
||||
? `<button class="btn btn-secondary btn-sm" data-action="restart" data-label="${gw.label}">重启</button>
|
||||
<button class="btn btn-danger btn-sm" data-action="stop" data-label="${gw.label}">停止</button>
|
||||
<button class="btn btn-danger btn-sm" data-action="uninstall-gateway">卸载</button>`
|
||||
: `<button class="btn btn-primary btn-sm" data-action="start" data-label="${gw.label}">启动</button>
|
||||
<button class="btn btn-primary btn-sm" data-action="install-gateway">安装</button>
|
||||
<button class="btn btn-danger btn-sm" data-action="uninstall-gateway">卸载</button>`
|
||||
}
|
||||
</div>
|
||||
</div>`
|
||||
@@ -255,6 +267,9 @@ function bindEvents(page) {
|
||||
case 'uninstall-gateway':
|
||||
await handleUninstallGateway(btn, page)
|
||||
break
|
||||
case 'refresh-services':
|
||||
await loadServices(page)
|
||||
break
|
||||
case 'save-registry':
|
||||
await handleSaveRegistry(btn, page)
|
||||
break
|
||||
|
||||
215
src/pages/setup.js
Normal file
215
src/pages/setup.js
Normal file
@@ -0,0 +1,215 @@
|
||||
/**
|
||||
* 初始设置页面 — openclaw 未安装时的引导
|
||||
* 自动检测环境 → 版本选择 → 一键安装 → 自动跳转
|
||||
*/
|
||||
import { api } from '../lib/tauri-api.js'
|
||||
import { showUpgradeModal } from '../components/modal.js'
|
||||
import { toast } from '../components/toast.js'
|
||||
|
||||
export async function render() {
|
||||
const page = document.createElement('div')
|
||||
page.className = 'page'
|
||||
|
||||
page.innerHTML = `
|
||||
<div style="max-width:560px;margin:48px auto;text-align:center">
|
||||
<div style="margin-bottom:var(--space-lg)">
|
||||
<img src="/images/logo-brand.png" alt="ClawPanel" style="max-width:160px;width:100%;height:auto">
|
||||
</div>
|
||||
<h1 style="font-size:var(--font-size-xl);margin-bottom:var(--space-xs)">欢迎使用 ClawPanel</h1>
|
||||
<p style="color:var(--text-secondary);margin-bottom:var(--space-xl);line-height:1.6">
|
||||
OpenClaw AI Agent 框架的桌面管理面板
|
||||
</p>
|
||||
|
||||
<div id="setup-steps"></div>
|
||||
|
||||
<div style="margin-top:var(--space-lg)">
|
||||
<button class="btn btn-secondary btn-sm" id="btn-recheck" style="min-width:120px">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="14" height="14" style="margin-right:4px"><polyline points="23 4 23 10 17 10"/><path d="M20.49 15a9 9 0 11-2.12-9.36L23 10"/></svg>
|
||||
重新检测
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
|
||||
page.querySelector('#btn-recheck').addEventListener('click', () => runDetect(page))
|
||||
runDetect(page)
|
||||
return page
|
||||
}
|
||||
|
||||
async function runDetect(page) {
|
||||
const stepsEl = page.querySelector('#setup-steps')
|
||||
stepsEl.innerHTML = `
|
||||
<div class="stat-card loading-placeholder" style="height:48px"></div>
|
||||
<div class="stat-card loading-placeholder" style="height:48px;margin-top:8px"></div>
|
||||
<div class="stat-card loading-placeholder" style="height:48px;margin-top:8px"></div>
|
||||
`
|
||||
// 并行检测 Node.js、OpenClaw CLI、配置文件
|
||||
const [nodeRes, clawRes, configRes] = await Promise.allSettled([
|
||||
api.checkNode(),
|
||||
api.getServicesStatus(),
|
||||
api.checkInstallation(),
|
||||
])
|
||||
|
||||
const node = nodeRes.status === 'fulfilled' ? nodeRes.value : { installed: false }
|
||||
const cliOk = clawRes.status === 'fulfilled'
|
||||
&& clawRes.value?.length > 0
|
||||
&& clawRes.value[0]?.cli_installed !== false
|
||||
const config = configRes.status === 'fulfilled' ? configRes.value : { installed: false }
|
||||
|
||||
renderSteps(page, { node, cliOk, config })
|
||||
}
|
||||
|
||||
function stepIcon(ok) {
|
||||
const color = ok ? 'var(--success)' : 'var(--text-tertiary)'
|
||||
return `<span style="color:${color};font-weight:700;width:18px;display:inline-block">${ok ? '✓' : '✗'}</span>`
|
||||
}
|
||||
|
||||
function renderSteps(page, { node, cliOk, config }) {
|
||||
const stepsEl = page.querySelector('#setup-steps')
|
||||
const nodeOk = node.installed
|
||||
const allOk = nodeOk && cliOk && config.installed
|
||||
|
||||
let html = ''
|
||||
|
||||
// 第一步:Node.js
|
||||
html += `
|
||||
<div class="config-section" style="text-align:left">
|
||||
<div class="config-section-title" style="display:flex;align-items:center;gap:4px">
|
||||
${stepIcon(nodeOk)} Node.js 环境
|
||||
</div>
|
||||
${nodeOk
|
||||
? `<p style="color:var(--success);font-size:var(--font-size-sm)">已安装 ${node.version || ''}</p>`
|
||||
: `<p style="color:var(--text-secondary);font-size:var(--font-size-sm);margin-bottom:var(--space-sm)">
|
||||
OpenClaw 基于 Node.js 运行,请先安装。
|
||||
</p>
|
||||
<a class="btn btn-primary btn-sm" href="https://nodejs.org/" target="_blank" rel="noopener">下载 Node.js</a>
|
||||
<span class="form-hint" style="margin-left:8px">安装后点击「重新检测」</span>`
|
||||
}
|
||||
</div>
|
||||
`
|
||||
|
||||
// 第二步:OpenClaw CLI
|
||||
html += `
|
||||
<div class="config-section" style="text-align:left;${nodeOk ? '' : 'opacity:0.4;pointer-events:none'}">
|
||||
<div class="config-section-title" style="display:flex;align-items:center;gap:4px">
|
||||
${stepIcon(cliOk)} OpenClaw CLI
|
||||
</div>
|
||||
${cliOk
|
||||
? `<p style="color:var(--success);font-size:var(--font-size-sm)">CLI 可用</p>`
|
||||
: renderInstallSection()
|
||||
}
|
||||
</div>
|
||||
`
|
||||
// 第三步:配置文件
|
||||
html += `
|
||||
<div class="config-section" style="text-align:left;${cliOk ? '' : 'opacity:0.4;pointer-events:none'}">
|
||||
<div class="config-section-title" style="display:flex;align-items:center;gap:4px">
|
||||
${stepIcon(config.installed)} 配置文件
|
||||
</div>
|
||||
${config.installed
|
||||
? `<p style="color:var(--success);font-size:var(--font-size-sm)">配置文件位于 ${config.path || ''}</p>`
|
||||
: `<p style="color:var(--text-secondary);font-size:var(--font-size-sm);margin-bottom:var(--space-sm)">
|
||||
安装 CLI 后会自动生成配置,也可手动执行 <code>openclaw configure</code>
|
||||
</p>`
|
||||
}
|
||||
</div>
|
||||
`
|
||||
|
||||
// 全部就绪 → 进入面板
|
||||
if (allOk) {
|
||||
html += `
|
||||
<div style="margin-top:var(--space-lg)">
|
||||
<button class="btn btn-primary" id="btn-enter" style="min-width:200px">进入面板</button>
|
||||
</div>
|
||||
`
|
||||
}
|
||||
|
||||
stepsEl.innerHTML = html
|
||||
bindEvents(page, nodeOk)
|
||||
}
|
||||
|
||||
function renderInstallSection() {
|
||||
return `
|
||||
<p style="color:var(--text-secondary);font-size:var(--font-size-sm);margin-bottom:var(--space-sm)">
|
||||
选择版本后点击安装,将自动执行 npm 全局安装。
|
||||
</p>
|
||||
<div style="display:flex;gap:var(--space-sm);margin-bottom:var(--space-sm)">
|
||||
<label class="setup-source-option" style="flex:1;cursor:pointer">
|
||||
<input type="radio" name="install-source" value="chinese" checked style="margin-right:6px">
|
||||
<div>
|
||||
<div style="font-weight:600;font-size:var(--font-size-sm)">汉化优化版(推荐)</div>
|
||||
<div style="font-size:var(--font-size-xs);color:var(--text-tertiary)">@qingchencloud/openclaw-zh</div>
|
||||
</div>
|
||||
</label>
|
||||
<label class="setup-source-option" style="flex:1;cursor:pointer">
|
||||
<input type="radio" name="install-source" value="official" style="margin-right:6px">
|
||||
<div>
|
||||
<div style="font-weight:600;font-size:var(--font-size-sm)">官方原版</div>
|
||||
<div style="font-size:var(--font-size-xs);color:var(--text-tertiary)">openclaw</div>
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
<div style="margin-bottom:var(--space-sm)">
|
||||
<label style="font-size:var(--font-size-xs);color:var(--text-tertiary);display:block;margin-bottom:4px">npm 镜像源</label>
|
||||
<select id="registry-select" style="width:100%;padding:6px 8px;border-radius:var(--radius-sm);border:1px solid var(--border-primary);background:var(--bg-secondary);color:var(--text-primary);font-size:var(--font-size-sm)">
|
||||
<option value="https://registry.npmmirror.com">淘宝镜像(推荐国内用户)</option>
|
||||
<option value="https://registry.npmjs.org">npm 官方源</option>
|
||||
<option value="https://repo.huaweicloud.com/repository/npm/">华为云镜像</option>
|
||||
</select>
|
||||
</div>
|
||||
<button class="btn btn-primary btn-sm" id="btn-install">一键安装</button>
|
||||
`
|
||||
}
|
||||
|
||||
function bindEvents(page, nodeOk) {
|
||||
// 进入面板
|
||||
page.querySelector('#btn-enter')?.addEventListener('click', () => {
|
||||
window.location.reload()
|
||||
})
|
||||
|
||||
// 一键安装
|
||||
const installBtn = page.querySelector('#btn-install')
|
||||
if (!installBtn || !nodeOk) return
|
||||
|
||||
installBtn.addEventListener('click', async () => {
|
||||
const source = page.querySelector('input[name="install-source"]:checked')?.value || 'chinese'
|
||||
const registry = page.querySelector('#registry-select')?.value
|
||||
const modal = showUpgradeModal()
|
||||
let unlistenLog, unlistenProgress
|
||||
|
||||
try {
|
||||
const { listen } = await import('@tauri-apps/api/event')
|
||||
unlistenLog = await listen('upgrade-log', (e) => modal.appendLog(e.payload))
|
||||
unlistenProgress = await listen('upgrade-progress', (e) => modal.setProgress(e.payload))
|
||||
|
||||
// 先设置镜像源
|
||||
if (registry) {
|
||||
modal.appendLog(`设置 npm 镜像源: ${registry}`)
|
||||
try { await api.setNpmRegistry(registry) } catch {}
|
||||
}
|
||||
|
||||
const msg = await api.upgradeOpenclaw(source)
|
||||
modal.setDone(msg)
|
||||
|
||||
// 安装成功后自动安装 Gateway
|
||||
modal.appendLog('正在安装 Gateway 服务...')
|
||||
try {
|
||||
await api.installGateway()
|
||||
modal.appendLog('✅ Gateway 服务已安装')
|
||||
} catch (e) {
|
||||
modal.appendLog('⚠️ Gateway 安装失败: ' + e)
|
||||
}
|
||||
|
||||
toast('OpenClaw 安装成功', 'success')
|
||||
setTimeout(() => window.location.reload(), 1500)
|
||||
} catch (e) {
|
||||
modal.appendLog(String(e))
|
||||
modal.setError('安装失败')
|
||||
} finally {
|
||||
unlistenLog?.()
|
||||
unlistenProgress?.()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user