Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
81 changes: 78 additions & 3 deletions electron/main/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3385,6 +3385,17 @@

if (isWin) {
if (!officialStateCleanupSucceeded) {
// Windows: first try to gracefully stop any lingering gateway/node processes
// that may hold file locks on the OpenClaw home directory.
await runShell('taskkill', ['/f', '/im', 'openclaw.exe', '/t'], 5_000, 'upgrade').catch(() => {
// openclaw.exe may not be running, ignore
})
await runShell('taskkill', ['/f', '/im', 'node.exe', '/fi', 'WINDOWTITLE eq openclaw*'], 5_000, 'upgrade').catch(() => {
// No matching node process, ignore
})
// Short delay to let file handles release
await new Promise((resolve) => setTimeout(resolve, 1000))

// 旧版 OpenClaw 无官方 uninstall 时,回退到本地目录删除
const rmOpenclaw = await runShell(
'cmd',
Expand All @@ -3393,7 +3404,7 @@
'upgrade'
)
if (!rmOpenclaw.ok) {
errors.push(`删除 ${targetDisplayHomeDir} 失败: ${rmOpenclaw.stderr}`)
errors.push(`删除 ${targetDisplayHomeDir} 失败。可能存在文件被其他进程占用,请关闭所有 OpenClaw 相关程序后重试。`)
}
}
} else {
Expand Down Expand Up @@ -3619,6 +3630,56 @@
/**
* 刷新环境变量,让新安装的程序可以被检测到
*/
/**
* Broadcast WM_SETTINGCHANGE to all top-level windows so other processes
* (explorer, new terminal sessions, etc.) pick up environment variable
* changes (especially PATH) without requiring a reboot or log-out.
*
* Equivalent to: SendMessageTimeout(HWND_BROADCAST, WM_SETTINGCHANGE, 0, "Environment", ...)
*/
async function broadcastWindowsEnvironmentChange(): Promise<void> {
if (process.platform !== 'win32') return
const childProcess = await getSpawnFn()
return new Promise((resolve, reject) => {
const proc = childProcess.spawn(

Check failure on line 3644 in electron/main/cli.ts

View workflow job for this annotation

GitHub Actions / verify

Property 'spawn' does not exist on type '{ (command: string, options?: SpawnOptionsWithoutStdio | undefined): ChildProcessWithoutNullStreams; (command: string, options: SpawnOptionsWithStdioTuple<...>): ChildProcessByStdio<...>; (command: string, options: SpawnOptionsWithStdioTuple<...>): ChildProcessByStdio<...>; (command: string, options: SpawnOptionsWit...'.
'powershell',
[
'-NoProfile',
'-Command',
[
'Add-Type -TypeDefinition @"',
'using System;',
'using System.Runtime.InteropServices;',
'public static class EnvNotify {',
' [DllImport("user32.dll", SetLastError = true, CharSet = CharSet.Auto)]',
' public static extern IntPtr SendMessageTimeout(',
' IntPtr hWnd, uint Msg, UIntPtr wParam, string lParam,',
' uint fuFlags, uint uTimeout, out UIntPtr lpdwResult);',
' public static readonly IntPtr HWND_BROADCAST = (IntPtr)0xffff;',
' public static readonly uint WM_SETTINGCHANGE = 0x001A;',
' public static readonly uint SMTO_ABORTIFHUNG = 0x0002;',
' public static void Notify() {',
' UIntPtr result;',
' SendMessageTimeout(HWND_BROADCAST, WM_SETTINGCHANGE, UIntPtr.Zero, "Environment", SMTO_ABORTIFHUNG, 5000, out result);',
' }',
'}',
'"@',
'[EnvNotify]::Notify()',
].join('\n'),
],
{
stdio: ['ignore', 'ignore', 'pipe'],
timeout: 15000,
}
)
proc.on('error', reject)
proc.on('close', (code) => {

Check failure on line 3676 in electron/main/cli.ts

View workflow job for this annotation

GitHub Actions / verify

Parameter 'code' implicitly has an 'any' type.
if (code === 0) resolve()
else reject(new Error(`broadcastEnvironment failed with exit code ${code}`))
})
})
}

export async function refreshEnvironment(): Promise<{ ok: boolean; newPath?: string }> {
const platform = process.platform

Expand All @@ -3645,12 +3706,26 @@
}

// Windows: 从注册表读取最新 PATH 并更新当前进程的环境变量
const result = await runShell('powershell', [
// Prefer pwsh (PowerShell 7+) for speed, fall back to Windows PowerShell 5.1
const psCmd = '[System.Environment]::GetEnvironmentVariable("Path","Machine") + ";" + [System.Environment]::GetEnvironmentVariable("Path","User")'
const pwshResult = await runShell('pwsh', [
'-NoProfile',
'-Command',
'[System.Environment]::GetEnvironmentVariable("Path","Machine") + ";" + [System.Environment]::GetEnvironmentVariable("Path","User")'
psCmd,
], MAIN_RUNTIME_POLICY.cli.lightweightProbeTimeoutMs, 'env')

const result = pwshResult.ok ? pwshResult : await runShell('powershell', [
'-NoProfile',
'-Command',
psCmd,
], MAIN_RUNTIME_POLICY.cli.lightweightProbeTimeoutMs, 'env')

if (result.ok && result.stdout.trim()) {
// Broadcast WM_SETTINGCHANGE so other processes (e.g. explorer, new terminals)
// pick up the PATH change without requiring a reboot.
await broadcastWindowsEnvironmentChange().catch(() => {
// Best-effort: if broadcast fails, PATH refresh still works for the current process.
})
return commitPath(result.stdout.trim())
}
return commitPath(buildCliPathWithCandidates({
Expand Down
6 changes: 4 additions & 2 deletions electron/main/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,9 @@ function createWindow() {
})

browserWindow.on('close', (event) => {
if (process.platform !== 'darwin' || isQuitting) return
if (isQuitting) return
// macOS: standard behavior — hide to Dock
// Windows: minimize to system tray instead of quitting
event.preventDefault()
browserWindow.hide()
})
Expand Down Expand Up @@ -272,7 +274,7 @@ app.whenReady().then(() => {

app.on('window-all-closed', () => {
win = null
if (process.platform !== 'darwin') app.quit()
// Keep the app running in the system tray on all platforms
})

app.on('before-quit', (event) => {
Expand Down
45 changes: 44 additions & 1 deletion electron/main/ipc-handlers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1024,6 +1024,16 @@ export function registerIpcHandlers() {
return usrLocalProbe.ok
}

const isWingetAvailable = async (): Promise<boolean> => {
const probe = await runShell('winget', ['--version'], 15_000, 'env-setup')
return probe.ok
}

const isChocoAvailable = async (): Promise<boolean> => {
const probe = await runShell('choco', ['--version'], 15_000, 'env-setup')
return probe.ok
}

ipcMain.handle('deps:installBin', async (_e, bin: string) => {
return withManagedOperationLock('runtime-install', async () => {
const safePackage = normalizeSafeInstallPackageName(bin)
Expand Down Expand Up @@ -1121,7 +1131,7 @@ export function registerIpcHandlers() {
return { ok: true, stdout: '', stderr: '', code: 0 }
}

// 2. 尝试 brew install
// 2. Try platform-native package manager (brew on macOS, winget/choco on Windows)
if (process.platform === 'darwin') {
if (await isBrewAvailable()) {
for (const bin of missingBins) {
Expand All @@ -1146,6 +1156,39 @@ export function registerIpcHandlers() {
log('/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"')
return { ok: false, stdout: '', stderr: `需要 Homebrew 来安装依赖,请先安装 Homebrew`, code: 1 }
}
} else if (process.platform === 'win32') {
// Windows: try winget first, then chocolatey as fallback
const wingetAvailable = await isWingetAvailable()
const chocoAvailable = !wingetAvailable && await isChocoAvailable()

if (wingetAvailable) {
for (const bin of missingBins) {
if (await verifyBin(bin)) continue
log(`通过 winget 安装 ${bin} ...`)
const installResult = await runShell('winget', ['install', '--id', bin, '-e', '--accept-source-agreements', '--accept-package-agreements'], 120_000, 'env-setup')
if (installResult.ok || await verifyBin(bin)) {
log(`${bin} 已通过 winget 安装`)
}
}
} else if (chocoAvailable) {
for (const bin of missingBins) {
if (await verifyBin(bin)) continue
log(`通过 Chocolatey 安装 ${bin} ...`)
await runShell('choco', ['install', bin, '-y'], 120_000, 'env-setup')
if (await verifyBin(bin)) {
log(`${bin} 已通过 Chocolatey 安装`)
}
}
} else {
log('未检测到 winget 或 Chocolatey,部分依赖无法自动安装')
log('建议安装 winget (Windows 10+ 自带) 或 Chocolatey:')
log('https://winget.run 或 https://chocolatey.org/install')
}

if (await checkAllResolved(missingBins)) {
log('所有依赖安装完成')
return { ok: true, stdout: '', stderr: '', code: 0 }
}
}

const stillMissing = []
Expand Down
5 changes: 5 additions & 0 deletions electron/main/openclaw-paths.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,11 @@ export function formatDisplayPath(
normalizedPath.startsWith(`${normalizedHomeDir}${platform === 'win32' ? '\\' : '/'}`)

if (!isMatch) return normalizedPath

// Windows: show full path — users are more familiar with C:\Users\xxx\.openclaw
// macOS/Linux: use ~ shorthand — standard Unix convention
if (platform === 'win32') return normalizedPath

return `~${normalizedPath.slice(normalizedHomeDir.length)}`
}

Expand Down
62 changes: 62 additions & 0 deletions electron/main/openclaw-permission-auto-repair.ts
Original file line number Diff line number Diff line change
Expand Up @@ -360,6 +360,68 @@ async function maybeAttemptPermissionRepair(
}
}

if (resolvedDependencies.platform === 'win32') {
// Windows: use icacls to grant current user full control on blocked directories
const currentUser = resolvedDependencies.currentUser.username || '%USERNAME%'
const icaclsCommands = repairRoots
.map((repairRoot) => `icacls "${repairRoot}" /grant:r "${currentUser}:(OI)(CI)F" /T /C /Q`)
.join(' && ')

const repairResult = await resolvedDependencies.runPrivilegedRepair({
command: icaclsCommands,
prompt: [
'Qclaw 检测到 OpenClaw 配置或运行目录权限异常。',
'',
'Qclaw 需要修复这些目录的访问权限,才能继续当前操作。',
'',
'点击"是"以管理员权限继续。',
].join('\n'),
controlDomain: context.controlDomain || 'global',
})

if (!repairResult.ok) {
return {
attempted: true,
repaired: false,
message: buildRepairFailureMessage({
blockedProbes: blockedEntries.map((entry) => entry.probe),
repairRoots,
reason: [
'Windows 权限修复失败。',
'如需手动修复,请以管理员身份运行 PowerShell 执行:',
...repairRoots.map((root) => `icacls "${root}" /grant:r "${currentUser}:(OI)(CI)F" /T /C /Q`),
].join('\n'),
}),
}
}

const verificationProbes = await Promise.all(
blockedEntries.map(async ({ path }) => ({
path,
probe: await resolvedDependencies.probePath(path),
}))
)
const remainingBlocked = verificationProbes.filter(
({ probe }) => !probe.writable || probe.ownerMatchesCurrentUser === false
)
if (remainingBlocked.length > 0) {
return {
attempted: true,
repaired: false,
message: buildRepairFailureMessage({
blockedProbes: remainingBlocked.map((entry) => entry.probe),
repairRoots,
reason: 'Qclaw 已尝试自动修复,但仍有目录权限异常。',
}),
}
}

return {
attempted: true,
repaired: true,
}
}

if (resolvedDependencies.platform !== 'darwin') {
return {
attempted: false,
Expand Down
Loading