From f6947100aafa64bfd24f0bc8c1ab18f1e49afb0f Mon Sep 17 00:00:00 2001 From: Vanisper <273266469@qq.com> Date: Sat, 22 Nov 2025 21:34:38 +0800 Subject: [PATCH 1/4] =?UTF-8?q?feat:=20devtools=20=E6=96=B0=E5=A2=9E=20Win?= =?UTF-8?q?dows=20=E5=B9=B3=E5=8F=B0=E6=94=AF=E6=8C=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/logics/devtools.ts | 15 ++++- src/utils/index.ts | 126 ++++++++++++++++++++++++++++++++++++++++- 2 files changed, 136 insertions(+), 5 deletions(-) diff --git a/src/logics/devtools.ts b/src/logics/devtools.ts index 10c63ef..7c12be4 100644 --- a/src/logics/devtools.ts +++ b/src/logics/devtools.ts @@ -6,7 +6,7 @@ import spawn from 'cross-spawn' import { bold, gray, white } from 'kolorist' import { getCliConfig, getGlobalConfig } from '@/cli/config' import { MP_PLATFORMS } from '@/constants' -import { ensureJsonSync, isMac, isWindows, stripAnsiColors } from '@/utils' +import { ensureJsonSync, findSoftwareInstallLocation, isMac, isWindows, stripAnsiColors } from '@/utils' import { logger } from '@/utils/log' const DEVTOOLS_BUNDLE_ID = { @@ -14,7 +14,7 @@ const DEVTOOLS_BUNDLE_ID = { 'mp-weixin': 'com.tencent.webplusdevtools', }, windows: { - 'mp-weixin': 'webplusdevtools.exe', + 'mp-weixin': '微信开发者工具', }, } @@ -52,7 +52,16 @@ function getDevtoolsPath() { } } if (isWindows()) { - // Windows 平台的实现 + const devtoolsBundleId = DEVTOOLS_BUNDLE_ID.windows[platform as keyof typeof DEVTOOLS_BUNDLE_ID.windows] + if (!devtoolsBundleId) { + return '' + } + + const installLocation = findSoftwareInstallLocation(devtoolsBundleId, devtoolsBundleId) + + if (installLocation) { + cliPath = path.join(installLocation, 'cli.bat') + } } return cliPath diff --git a/src/utils/index.ts b/src/utils/index.ts index 6caf027..c0e3505 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -1,6 +1,6 @@ +import { execSync } from 'node:child_process' import os from 'node:os' - -import { parse } from 'node:path' +import { dirname, join, parse } from 'node:path' import fs from 'fs-extra' export function isMac() { @@ -38,3 +38,125 @@ export async function ensureJson(path: string, object: any = {}) { await fs.writeJSON(path, object) } } + +export function decodeGbk(input?: NonSharedBuffer) { + const decoder = new TextDecoder('gbk') + return decoder.decode(input) +} + +/** + * 查找软件安装目录 + * @param executableName 可执行文件名 (不带可执行后缀,例如 "wechatwebdevtools") + * @param displayName 软件在控制面板显示的名称 (例如 "微信开发者工具"),当前仅作用于 windows 下 + * @returns 软件安装 + */ +export function findSoftwareInstallLocation(executableName: string, displayName?: string) { + if (isWindows()) { + // 查询标准卸载注册表 (Uninstall Keys),兼容 32位 & 64位 + if (displayName) { + try { + const installPath = findInUninstallRegistry(displayName) + if (installPath) { + return installPath[0] + } + } + catch { /** nothing */ } + } + + // 查询兼容性助手 (AppCompatFlags) + // Borrowed from https://github.com/uni-helper/hbuilderx-cli/blob/9e39cbf2f1986ee1f2a3f105b927ec4ee6cf7112/src/utils.ts#L5-L24 + try { + const exeFullName = `${executableName}.exe` + const cmd = `reg query "HKEY_CURRENT_USER\\Software\\Microsoft\\Windows NT\\CurrentVersion\\AppCompatFlags\\Compatibility Assistant\\Store" /f "${exeFullName}" /c` + + const resultBuffer = execSync(cmd) + const result = decodeGbk(resultBuffer) + + // match: "C:\Folder\app.exe" REG_BINARY + // eslint-disable-next-line regexp/no-super-linear-backtracking, regexp/no-useless-flag + const regex = /^.*?(\w:\\.*?)\s*REG_BINARY/gm + const matches = regex.exec(result) + + if (matches && matches[1]) { + return dirname(matches[1]) + } + } + catch { /** nothing */ } + } + + if (isMac()) { + const candidates = [ + `/Applications/${executableName}.app`, // 系统应用目录 + join(os.homedir(), 'Applications', `${executableName}.app`), // 用户应用目录 (~/Applications) + ] + + for (const candidate of candidates) { + if (fs.existsSync(candidate)) { + return candidate + } + } + } + + return null +} + +/** + * 使用 PowerShell 深度遍历 Uninstall 注册表 + * - 兼容 64位 和 32位 + * - 返回 InstallLocation 或 DisplayIcon + * @platform Only `windows` + */ +function findInUninstallRegistry(appName: string): [string, 'INSTALL_LOCATION' | 'DISPLAY_ICON'] | null { + const psCommand = ` + $paths = @( + "HKLM:\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\*", + "HKLM:\\SOFTWARE\\WOW6432Node\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\*" + ) + $target = "*${appName}*" + + foreach ($path in $paths) { + Get-ItemProperty $path -ErrorAction SilentlyContinue | Where-Object { $_.DisplayName -like $target } | ForEach-Object { + if ($_.InstallLocation) { + # 使用 INSTALL_LOCATION| 标识前缀 + Write-Output "INSTALL_LOCATION|" + $_.InstallLocation + exit + } + if ($_.DisplayIcon) { + # 使用 DISPLAY_ICON| 标识前缀 + # 如果没有 InstallLocation,尝试从 DisplayIcon (通常是 exe 路径) 获取目录 + Write-Output "DISPLAY_ICON|" + $_.DisplayIcon + exit + } + } + } + ` + + try { + // exec PowerShell, use UTF8 + const output = execSync(`powershell -NoProfile -ExecutionPolicy Bypass -Command "${psCommand}"`, { + encoding: 'utf8', + stdio: ['ignore', 'pipe', 'ignore'], // ignore error + }).toString().trim() + + if (!output) + return null + + const parts = output.split('|', 2) + if (parts.length !== 2) { + return null + } + + const [sourceType, rawPath] = parts + const cleanPath = rawPath.replace(/"/g, '').trim() // 去掉可能的引号并修剪空格 + + if (sourceType === 'INSTALL_LOCATION') { + return [cleanPath, sourceType] + } + else if (sourceType === 'DISPLAY_ICON') { + return [dirname(cleanPath), sourceType] + } + } + catch { /** nothing */ } + + return null +} From f09af1d33e035da1d44ca017395df4fb20a2559d Mon Sep 17 00:00:00 2001 From: Vanisper <273266469@qq.com> Date: Sat, 22 Nov 2025 23:16:55 +0800 Subject: [PATCH 2/4] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D=20PowerShell=20?= =?UTF-8?q?=E5=91=BD=E4=BB=A4=E7=9A=84=E5=AD=97=E7=AC=A6=E4=B8=B2=E5=A4=84?= =?UTF-8?q?=E7=90=86=EF=BC=8C=E7=A1=AE=E4=BF=9D=E6=AD=A3=E7=A1=AE=E7=BC=96?= =?UTF-8?q?=E7=A0=81=E5=92=8C=E8=BE=93=E5=87=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/utils/index.ts | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/src/utils/index.ts b/src/utils/index.ts index c0e3505..5037389 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -107,7 +107,7 @@ export function findSoftwareInstallLocation(executableName: string, displayName? * @platform Only `windows` */ function findInUninstallRegistry(appName: string): [string, 'INSTALL_LOCATION' | 'DISPLAY_ICON'] | null { - const psCommand = ` + const psScript = ` $paths = @( "HKLM:\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\*", "HKLM:\\SOFTWARE\\WOW6432Node\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\*" @@ -118,33 +118,36 @@ function findInUninstallRegistry(appName: string): [string, 'INSTALL_LOCATION' | Get-ItemProperty $path -ErrorAction SilentlyContinue | Where-Object { $_.DisplayName -like $target } | ForEach-Object { if ($_.InstallLocation) { # 使用 INSTALL_LOCATION| 标识前缀 - Write-Output "INSTALL_LOCATION|" + $_.InstallLocation + Write-Output "INSTALL_LOCATION|$($_.InstallLocation)" exit } if ($_.DisplayIcon) { # 使用 DISPLAY_ICON| 标识前缀 # 如果没有 InstallLocation,尝试从 DisplayIcon (通常是 exe 路径) 获取目录 - Write-Output "DISPLAY_ICON|" + $_.DisplayIcon + Write-Output "DISPLAY_ICON|$($_.DisplayIcon)" exit } } } ` + // 编码,防止转义、乱码问题 + // eslint-disable-next-line node/prefer-global/buffer + const encodedCommand = Buffer.from(psScript, 'utf16le').toString('base64') + try { - // exec PowerShell, use UTF8 - const output = execSync(`powershell -NoProfile -ExecutionPolicy Bypass -Command "${psCommand}"`, { - encoding: 'utf8', + const resultBuffer = execSync(`powershell -NoProfile -ExecutionPolicy Bypass -EncodedCommand "${encodedCommand}"`, { stdio: ['ignore', 'pipe', 'ignore'], // ignore error - }).toString().trim() + windowsHide: true, + }) + const output = decodeGbk(resultBuffer) if (!output) return null const parts = output.split('|', 2) - if (parts.length !== 2) { + if (parts.length !== 2) return null - } const [sourceType, rawPath] = parts const cleanPath = rawPath.replace(/"/g, '').trim() // 去掉可能的引号并修剪空格 From ac742809fca2a48f41ee26b9804136ed133fda44 Mon Sep 17 00:00:00 2001 From: Vanisper <273266469@qq.com> Date: Sat, 22 Nov 2025 23:55:40 +0800 Subject: [PATCH 3/4] =?UTF-8?q?feat:=20=E5=A2=9E=E5=BC=BA=E5=AF=B9?= =?UTF-8?q?=E8=BD=AF=E4=BB=B6=E8=B7=AF=E5=BE=84=E5=8C=B9=E9=85=8D=E7=9A=84?= =?UTF-8?q?=E5=A4=84=E7=90=86=EF=BC=8C=E7=A1=AE=E4=BF=9D=E6=AD=A3=E7=A1=AE?= =?UTF-8?q?=E8=8E=B7=E5=8F=96=E5=8F=AF=E6=89=A7=E8=A1=8C=E6=96=87=E4=BB=B6?= =?UTF-8?q?=E7=9B=AE=E5=BD=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/utils/index.ts | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/src/utils/index.ts b/src/utils/index.ts index 5037389..ad079df 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -1,6 +1,6 @@ import { execSync } from 'node:child_process' import os from 'node:os' -import { dirname, join, parse } from 'node:path' +import { basename, dirname, join, parse } from 'node:path' import fs from 'fs-extra' export function isMac() { @@ -73,8 +73,23 @@ export function findSoftwareInstallLocation(executableName: string, displayName? const result = decodeGbk(resultBuffer) // match: "C:\Folder\app.exe" REG_BINARY - // eslint-disable-next-line regexp/no-super-linear-backtracking, regexp/no-useless-flag + // eslint-disable-next-line regexp/no-super-linear-backtracking const regex = /^.*?(\w:\\.*?)\s*REG_BINARY/gm + + let match + // 遍历所有匹配结果,防止只取到第一个错误的包含匹配 + // eslint-disable-next-line no-cond-assign + while ((match = regex.exec(result)) !== null) { + if (match && match[1]) { + const fullExePath = match[1].trim() + const fileName = basename(fullExePath) + + if (fileName.toLowerCase() === exeFullName.toLowerCase()) { + return dirname(fullExePath) + } + } + } + const matches = regex.exec(result) if (matches && matches[1]) { From 59468f360c3fab5d23dc1083c54bbfbaa633d4e3 Mon Sep 17 00:00:00 2001 From: Vanisper <273266469@qq.com> Date: Sun, 23 Nov 2025 00:17:50 +0800 Subject: [PATCH 4/4] =?UTF-8?q?fix:=20=E7=A7=BB=E9=99=A4=E9=81=97=E5=BF=98?= =?UTF-8?q?=E7=9A=84=E5=BE=85=E7=A7=BB=E9=99=A4=E7=9A=84=E5=86=97=E4=BD=99?= =?UTF-8?q?=E4=BB=A3=E7=A0=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/utils/index.ts | 6 ------ 1 file changed, 6 deletions(-) diff --git a/src/utils/index.ts b/src/utils/index.ts index ad079df..c1e75e5 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -89,12 +89,6 @@ export function findSoftwareInstallLocation(executableName: string, displayName? } } } - - const matches = regex.exec(result) - - if (matches && matches[1]) { - return dirname(matches[1]) - } } catch { /** nothing */ } }