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:
晴天
2026-03-02 13:00:16 +08:00
parent 5352337eaa
commit 53f46d8ef2
64 changed files with 4260 additions and 451 deletions

81
src/lib/app-state.js Normal file
View File

@@ -0,0 +1,81 @@
/**
* 全局应用状态
* 管理 openclaw 安装状态,供各组件查询
*/
import { api } from './tauri-api.js'
let _openclawReady = false
let _gatewayRunning = false
let _listeners = []
let _gwListeners = []
/** openclaw 是否就绪CLI 已安装 + 配置文件存在) */
export function isOpenclawReady() {
return _openclawReady
}
/** Gateway 是否正在运行 */
export function isGatewayRunning() {
return _gatewayRunning
}
/** 监听 Gateway 状态变化 */
export function onGatewayChange(fn) {
_gwListeners.push(fn)
return () => { _gwListeners = _gwListeners.filter(cb => cb !== fn) }
}
/** 检测 openclaw 安装状态 */
export async function detectOpenclawStatus() {
try {
const [installation, services] = await Promise.allSettled([
api.checkInstallation(),
api.getServicesStatus(),
])
const configExists = installation.status === 'fulfilled' && installation.value?.installed
const cliInstalled = services.status === 'fulfilled'
&& services.value?.length > 0
&& services.value[0]?.cli_installed !== false
_openclawReady = configExists && cliInstalled
// 顺便检测 Gateway 运行状态
if (services.status === 'fulfilled' && services.value?.length > 0) {
_setGatewayRunning(services.value[0]?.running === true)
}
} catch {
_openclawReady = false
}
_listeners.forEach(fn => { try { fn(_openclawReady) } catch {} })
return _openclawReady
}
function _setGatewayRunning(val) {
const changed = _gatewayRunning !== val
_gatewayRunning = val
if (changed) _gwListeners.forEach(fn => { try { fn(val) } catch {} })
}
/** 刷新 Gateway 运行状态(轻量,仅查服务状态) */
export async function refreshGatewayStatus() {
try {
const services = await api.getServicesStatus()
if (services?.length > 0) _setGatewayRunning(services[0]?.running === true)
} catch {}
return _gatewayRunning
}
let _pollTimer = null
/** 启动 Gateway 状态轮询(每 5 秒) */
export function startGatewayPoll() {
if (_pollTimer) return
_pollTimer = setInterval(() => refreshGatewayStatus(), 5000)
}
export function stopGatewayPoll() {
if (_pollTimer) { clearInterval(_pollTimer); _pollTimer = null }
}
/** 监听状态变化 */
export function onReadyChange(fn) {
_listeners.push(fn)
return () => { _listeners = _listeners.filter(cb => cb !== fn) }
}

View File

@@ -5,9 +5,14 @@
const isTauri = !!window.__TAURI_INTERNALS__
// 预加载 Tauri invoke避免每次 API 调用都做动态 import
const _invokeReady = isTauri
? import('@tauri-apps/api/core').then(m => m.invoke)
: null
async function invoke(cmd, args = {}) {
if (isTauri) {
const { invoke: tauriInvoke } = await import('@tauri-apps/api/core')
if (_invokeReady) {
const tauriInvoke = await _invokeReady
return tauriInvoke(cmd, args)
}
return mockInvoke(cmd, args)
@@ -17,7 +22,7 @@ async function invoke(cmd, args = {}) {
function mockInvoke(cmd, args) {
const mocks = {
get_services_status: () => [
{ label: 'ai.openclaw.gateway', pid: null, running: false, description: 'OpenClaw Gateway' },
{ label: 'ai.openclaw.gateway', pid: null, running: false, description: 'OpenClaw Gateway', cli_installed: true },
],
get_version_info: () => ({
current: '2026.2.23',
@@ -83,6 +88,7 @@ function mockInvoke(cmd, args) {
delete_memory_file: () => true,
export_memory_zip: ({ category }) => `/tmp/openclaw-${category}-20260226-160000.zip`,
check_installation: () => ({ installed: true, path: '/usr/local/bin/openclaw', version: '2026.2.23' }),
check_node: () => ({ installed: true, version: 'v20.11.0' }),
get_deploy_config: () => ({ gatewayUrl: 'http://127.0.0.1:18789', authToken: '', version: '2026.2.23' }),
read_mcp_config: () => ({
mcpServers: {
@@ -171,6 +177,7 @@ export const api = {
// 安装/部署
checkInstallation: () => invoke('check_installation'),
checkNode: () => invoke('check_node'),
getDeployConfig: () => invoke('get_deploy_config'),
writeEnvFile: (path, config) => invoke('write_env_file', { path, config }),