import { ConfigStore } from "./ConfigStore"; import { exec, execSync } from "child_process"; import { promisify } from "util"; import { existsSync, readFileSync, writeFileSync, readdirSync, unlinkSync, rmdirSync, mkdirSync, } from "fs"; import { join } from "path"; import https from "https"; import http from "http"; import { createWriteStream } from "fs"; import { sendDownloadProgress } from "../main"; const execAsync = promisify(exec); interface PhpVersion { version: string; path: string; isActive: boolean; } interface PhpExtension { name: string; enabled: boolean; installed: boolean; } interface AvailablePeclExtension { name: string; version: string; downloadUrl: string; description?: string; packageName?: string; // Packagist 包名,用于 PIE 安装 } interface AvailablePhpVersion { version: string; downloadUrl: string; type: "nts" | "ts"; arch: "x64" | "x86"; } export class PhpManager { private configStore: ConfigStore; constructor(configStore: ConfigStore) { this.configStore = configStore; } /** * 获取已安装的 PHP 版本列表 */ async getInstalledVersions(): Promise { const versions: PhpVersion[] = []; const activeVersion = this.configStore.get("activePhpVersion"); const phpDir = join(this.configStore.getBasePath(), "php"); if (!existsSync(phpDir)) { return versions; } const dirs = readdirSync(phpDir, { withFileTypes: true }); for (const dir of dirs) { if (dir.isDirectory() && dir.name.startsWith("php-")) { const version = dir.name.replace("php-", ""); const phpPath = join(phpDir, dir.name); // 验证 PHP 是否真的存在 if (existsSync(join(phpPath, "php.exe"))) { versions.push({ version, path: phpPath, isActive: version === activeVersion, }); } } } return versions.sort((a, b) => b.version.localeCompare(a.version)); } /** * 从 windows.php.net 自动获取可用的 PHP 版本列表 */ async getAvailableVersions(): Promise { let versions: AvailablePhpVersion[] = []; try { // 尝试从 windows.php.net/downloads/releases/ 获取版本列表 versions = await this.fetchPhpVersionsFromWeb(); } catch (error) { console.error("获取 PHP 版本列表失败:", error); } // 如果网络获取失败或为空,使用备用列表 if (versions.length === 0) { console.log("使用备用版本列表"); versions = this.getFallbackVersions(); } // 过滤掉已安装的版本 const installed = await this.getInstalledVersions(); const installedVersions = installed.map((v) => v.version); return versions.filter((v) => !installedVersions.includes(v.version)); } /** * 从网页获取 PHP 版本列表 */ private async fetchPhpVersionsFromWeb(): Promise { return new Promise((resolve, reject) => { const url = "https://windows.php.net/downloads/releases/"; https .get( url, { headers: { "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36", }, }, (response) => { if (response.statusCode !== 200) { reject(new Error(`HTTP ${response.statusCode}`)); return; } let html = ""; response.on("data", (chunk) => (html += chunk)); response.on("end", () => { try { const versions = this.parsePhpVersionsFromHtml(html); resolve(versions); } catch (e) { reject(e); } }); } ) .on("error", reject) .setTimeout(10000, () => { reject(new Error("请求超时")); }); }); } /** * 解析 HTML 获取 PHP 版本信息 */ private parsePhpVersionsFromHtml(html: string): AvailablePhpVersion[] { const versions: AvailablePhpVersion[] = []; const seenVersions = new Set(); // 多种正则模式匹配不同格式 const patterns = [ // 标准格式: php-8.4.3-nts-Win32-vs17-x64.zip /php-(\d+\.\d+\.\d+)-nts-Win32-vs(\d+)-x64\.zip/g, // TS格式: php-8.4.3-Win32-vs17-x64.zip /php-(\d+\.\d+\.\d+)-Win32-vs(\d+)-x64\.zip/g, ]; // 匹配 NTS 版本 let match; const ntsRegex = /php-(\d+\.\d+\.\d+)-nts-Win32-vs(\d+)-x64\.zip/g; while ((match = ntsRegex.exec(html)) !== null) { const version = match[1]; const vsVersion = match[2]; const versionKey = `${version}-nts`; if (!seenVersions.has(versionKey)) { seenVersions.add(versionKey); versions.push({ version: version, downloadUrl: `https://windows.php.net/downloads/releases/php-${version}-nts-Win32-vs${vsVersion}-x64.zip`, type: "nts", arch: "x64", }); } } // 匹配 TS 版本 (不含 nts 的) const tsRegex = /php-(\d+\.\d+\.\d+)-Win32-vs(\d+)-x64\.zip/g; while ((match = tsRegex.exec(html)) !== null) { const fullMatch = match[0]; // 跳过 nts 版本(已经处理过) if (fullMatch.includes("-nts-")) continue; const version = match[1]; const vsVersion = match[2]; const versionKey = `${version}-ts`; if (!seenVersions.has(versionKey)) { seenVersions.add(versionKey); versions.push({ version: `${version}-ts`, downloadUrl: `https://windows.php.net/downloads/releases/php-${version}-Win32-vs${vsVersion}-x64.zip`, type: "ts", arch: "x64", }); } } // 按版本号排序(降序) versions.sort((a, b) => { const vA = a.version.replace("-ts", ""); const vB = b.version.replace("-ts", ""); return vB.localeCompare(vA, undefined, { numeric: true }); }); console.log(`从 windows.php.net 获取到 ${versions.length} 个 PHP 版本`); if (versions.length > 0) { console.log("版本列表:", versions.map((v) => v.version).join(", ")); } return versions; } /** * 备用版本列表(当网络请求失败时使用) * 基于 https://windows.php.net/download/ (2025-12-25) */ private getFallbackVersions(): AvailablePhpVersion[] { return [ // PHP 8.4 (VS17) - 最新稳定版 { version: "8.4.3", downloadUrl: "https://windows.php.net/downloads/releases/php-8.4.3-nts-Win32-vs17-x64.zip", type: "nts", arch: "x64", }, { version: "8.4.3-ts", downloadUrl: "https://windows.php.net/downloads/releases/php-8.4.3-Win32-vs17-x64.zip", type: "ts", arch: "x64", }, // PHP 8.3 (VS16) { version: "8.3.15", downloadUrl: "https://windows.php.net/downloads/releases/php-8.3.15-nts-Win32-vs16-x64.zip", type: "nts", arch: "x64", }, { version: "8.3.15-ts", downloadUrl: "https://windows.php.net/downloads/releases/php-8.3.15-Win32-vs16-x64.zip", type: "ts", arch: "x64", }, // PHP 8.2 (VS16) { version: "8.2.27", downloadUrl: "https://windows.php.net/downloads/releases/php-8.2.27-nts-Win32-vs16-x64.zip", type: "nts", arch: "x64", }, { version: "8.2.27-ts", downloadUrl: "https://windows.php.net/downloads/releases/php-8.2.27-Win32-vs16-x64.zip", type: "ts", arch: "x64", }, // PHP 8.1 (VS16) { version: "8.1.31", downloadUrl: "https://windows.php.net/downloads/releases/php-8.1.31-nts-Win32-vs16-x64.zip", type: "nts", arch: "x64", }, { version: "8.1.31-ts", downloadUrl: "https://windows.php.net/downloads/releases/php-8.1.31-Win32-vs16-x64.zip", type: "ts", arch: "x64", }, ]; } /** * 安装 PHP 版本 */ async install( version: string ): Promise<{ success: boolean; message: string }> { try { const available = await this.getAvailableVersions(); const versionInfo = available.find((v) => v.version === version); if (!versionInfo) { return { success: false, message: `未找到 PHP ${version} 版本` }; } const phpPath = this.configStore.getPhpPath(version); const tempPath = this.configStore.getTempPath(); const zipPath = join(tempPath, `php-${version}.zip`); // 确保目录存在 if (!existsSync(tempPath)) { mkdirSync(tempPath, { recursive: true }); } if (!existsSync(phpPath)) { mkdirSync(phpPath, { recursive: true }); } console.log(`开始下载 PHP ${version} 从 ${versionInfo.downloadUrl}`); // 下载 PHP await this.downloadFile(versionInfo.downloadUrl, zipPath); console.log(`下载完成,开始解压到 ${phpPath}`); // 解压 await this.unzip(zipPath, phpPath); console.log("解压完成"); // 删除临时文件 if (existsSync(zipPath)) { unlinkSync(zipPath); } // 创建默认 php.ini await this.createDefaultPhpIni(phpPath); // 添加到配置 this.configStore.addPhpVersion(version); return { success: true, message: `PHP ${version} 安装成功` }; } catch (error: any) { console.error("PHP 安装失败:", error); return { success: false, message: `安装失败: ${error.message}` }; } } /** * 卸载 PHP 版本 */ async uninstall( version: string ): Promise<{ success: boolean; message: string }> { try { const phpPath = this.configStore.getPhpPath(version); if (!existsSync(phpPath)) { return { success: false, message: `PHP ${version} 未安装` }; } // 如果是当前活动版本,先清除环境变量 const activeVersion = this.configStore.get("activePhpVersion"); if (activeVersion === version) { await this.removeFromPath(phpPath); this.configStore.set("activePhpVersion", ""); } // 递归删除目录 this.removeDirectory(phpPath); // 从配置中移除 this.configStore.removePhpVersion(version); return { success: true, message: `PHP ${version} 已卸载` }; } catch (error: any) { return { success: false, message: `卸载失败: ${error.message}` }; } } /** * 设置活动的 PHP 版本(添加到环境变量) */ async setActive( version: string ): Promise<{ success: boolean; message: string }> { try { const phpPath = this.configStore.getPhpPath(version); if (!existsSync(phpPath)) { return { success: false, message: `PHP ${version} 未安装` }; } // 检查 php.exe 是否存在 if (!existsSync(join(phpPath, "php.exe"))) { return { success: false, message: `PHP ${version} 安装不完整,找不到 php.exe`, }; } console.log(`设置 PHP ${version} 为默认版本,路径: ${phpPath}`); // 添加新的 PHP 路径到环境变量(会自动移除旧的 PHP 路径) await this.addToPath(phpPath); // 更新配置 this.configStore.set("activePhpVersion", version); return { success: true, message: `PHP ${version} 已设置为默认版本\n\n环境变量已更新,新开的终端窗口中将生效。\n路径: ${phpPath}`, }; } catch (error: any) { console.error("设置默认 PHP 版本失败:", error); return { success: false, message: `设置失败: ${error.message}` }; } } /** * 打开扩展目录(用于手动安装扩展) */ async openExtensionDir( version: string ): Promise<{ success: boolean; message: string; path?: string }> { try { const phpPath = this.configStore.getPhpPath(version); const extDir = join(phpPath, "ext"); if (!existsSync(extDir)) { mkdirSync(extDir, { recursive: true }); } // 使用 Electron shell 打开文件夹(更可靠) const { shell } = await import("electron"); const result = await shell.openPath(extDir); if (result) { // result 非空表示有错误 return { success: false, message: `打开失败: ${result}` }; } return { success: true, message: `已打开扩展目录: ${extDir}`, path: extDir, }; } catch (error: any) { return { success: false, message: `打开失败: ${error.message}` }; } } /** * 获取 PHP 扩展列表 */ async getExtensions(version: string): Promise { const phpPath = this.configStore.getPhpPath(version); const extDir = join(phpPath, "ext"); const iniPath = join(phpPath, "php.ini"); if (!existsSync(extDir)) { return []; } const extensions: PhpExtension[] = []; const iniContent = existsSync(iniPath) ? readFileSync(iniPath, "utf-8") : ""; // 将 ini 内容按行分割,用于精确匹配 const iniLines = iniContent.split("\n"); const files = readdirSync(extDir); for (const file of files) { if (file.startsWith("php_") && file.endsWith(".dll")) { const extName = file.replace("php_", "").replace(".dll", ""); // 检查是否有未被注释的 extension= 行 const isEnabled = iniLines.some((line) => { const trimmedLine = line.trim(); // 跳过注释行 if (trimmedLine.startsWith(";")) { return false; } // 检查各种可能的格式 return ( trimmedLine === `extension=${extName}` || trimmedLine === `extension=php_${extName}.dll` || trimmedLine === `extension=${extName}.dll` || trimmedLine.startsWith(`extension=${extName} `) || trimmedLine.startsWith(`extension=php_${extName}.dll `) || trimmedLine.startsWith(`extension=${extName}.dll `) ); }); extensions.push({ name: extName, enabled: isEnabled, installed: true, }); } } return extensions.sort((a, b) => a.name.localeCompare(b.name)); } /** * 启用扩展 */ async enableExtension( version: string, ext: string ): Promise<{ success: boolean; message: string }> { try { const phpPath = this.configStore.getPhpPath(version); const iniPath = join(phpPath, "php.ini"); if (!existsSync(iniPath)) { return { success: false, message: "php.ini 文件不存在" }; } let content = readFileSync(iniPath, "utf-8"); const lines = content.split("\n"); let found = false; let alreadyEnabled = false; // 扩展可能的格式 const patterns = [ new RegExp(`^;?\\s*extension\\s*=\\s*${ext}\\s*$`, "i"), new RegExp(`^;?\\s*extension\\s*=\\s*php_${ext}\\.dll\\s*$`, "i"), new RegExp(`^;?\\s*extension\\s*=\\s*${ext}\\.dll\\s*$`, "i"), ]; for (let i = 0; i < lines.length; i++) { for (const pattern of patterns) { if (pattern.test(lines[i])) { found = true; if (lines[i].trim().startsWith(";")) { // 取消注释 lines[i] = lines[i].replace(/^(\s*);/, "$1"); } else { alreadyEnabled = true; } break; } } } if (alreadyEnabled) { return { success: true, message: `扩展 ${ext} 已经启用` }; } if (!found) { // 添加新的扩展配置 // 查找 Dynamic Extensions 区域或文件末尾 let insertIndex = lines.findIndex( (l) => l.includes("[PHP]") || l.includes("Dynamic Extensions") ); if (insertIndex === -1) { insertIndex = lines.length; } else { // 在该区域后找到合适位置 for (let i = insertIndex + 1; i < lines.length; i++) { if (lines[i].startsWith("[") && !lines[i].includes("Dynamic")) { insertIndex = i; break; } if (lines[i].includes("extension=")) { insertIndex = i + 1; } } } lines.splice(insertIndex, 0, `extension=${ext}`); } writeFileSync(iniPath, lines.join("\n")); return { success: true, message: `扩展 ${ext} 已启用,重启 PHP 后生效` }; } catch (error: any) { return { success: false, message: `启用扩展失败: ${error.message}` }; } } /** * 禁用扩展 */ async disableExtension( version: string, ext: string ): Promise<{ success: boolean; message: string }> { try { const phpPath = this.configStore.getPhpPath(version); const iniPath = join(phpPath, "php.ini"); if (!existsSync(iniPath)) { return { success: false, message: "php.ini 文件不存在" }; } let content = readFileSync(iniPath, "utf-8"); const lines = content.split("\n"); let found = false; // 扩展可能的格式 const patterns = [ new RegExp(`^\\s*extension\\s*=\\s*${ext}\\s*$`, "i"), new RegExp(`^\\s*extension\\s*=\\s*php_${ext}\\.dll\\s*$`, "i"), new RegExp(`^\\s*extension\\s*=\\s*${ext}\\.dll\\s*$`, "i"), ]; for (let i = 0; i < lines.length; i++) { for (const pattern of patterns) { if (pattern.test(lines[i])) { found = true; if (!lines[i].trim().startsWith(";")) { // 注释掉 lines[i] = ";" + lines[i]; } break; } } } if (!found) { return { success: true, message: `扩展 ${ext} 未找到或已禁用` }; } writeFileSync(iniPath, lines.join("\n")); return { success: true, message: `扩展 ${ext} 已禁用,重启 PHP 后生效` }; } catch (error: any) { return { success: false, message: `禁用扩展失败: ${error.message}` }; } } /** * 获取可安装的 PECL 扩展列表(从 pecl.php.net 搜索) */ async getAvailableExtensions( version: string, searchKeyword?: string ): Promise { const extensions: AvailablePeclExtension[] = []; try { // 获取 PHP 版本信息 const phpPath = this.configStore.getPhpPath(version); const phpInfo = await this.getPhpInfo(phpPath); if (!phpInfo) { console.error("无法获取 PHP 信息"); return this.getDefaultExtensionList(version); } const { majorMinor, isNts } = phpInfo; console.log(`PHP Info: ${majorMinor}, NTS: ${isNts}`); // 使用 Packagist API 获取扩展列表(PIE 推荐方式) // https://packagist.org/extensions const keyword = searchKeyword || ""; const searchUrl = keyword ? `https://packagist.org/search.json?q=${encodeURIComponent( keyword )}&type=php-ext` : `https://packagist.org/search.json?type=php-ext`; console.log(`从 Packagist 搜索扩展: ${keyword || "(全部)"}`); let foundPackages: { name: string; description: string; packageName: string; }[] = []; try { const jsonStr = await this.fetchHtmlContent(searchUrl); const data = JSON.parse(jsonStr); if (data.results && Array.isArray(data.results)) { for (const pkg of data.results) { // Packagist 包名格式:vendor/package const packageName = pkg.name || ""; // 扩展名通常是包名的最后一部分,或从 description 提取 let extName = packageName.split("/").pop() || ""; // 移除常见前缀 extName = extName .replace(/^php[-_]?/, "") .replace(/[-_]?extension$/, ""); foundPackages.push({ name: extName, description: pkg.description || "", packageName: packageName, }); } } console.log(`从 Packagist 找到 ${foundPackages.length} 个扩展包`); } catch (e: any) { console.log(`Packagist API 请求失败: ${e.message},尝试使用预定义列表`); } // 如果 Packagist 无结果,使用预定义的常用扩展列表 if (foundPackages.length === 0) { foundPackages = this.getPopularExtensionsList(keyword); console.log(`使用预定义扩展列表: ${foundPackages.length} 个`); } // 获取已安装的扩展 const installedExts = await this.getExtensions(version); const installedNames = installedExts.map((e) => e.name.toLowerCase()); // 过滤已安装的扩展 const availablePackages = foundPackages.filter( (pkg) => !installedNames.includes(pkg.name.toLowerCase()) ); // 限制数量 const checkPackages = availablePackages.slice(0, searchKeyword ? 50 : 20); for (const pkg of checkPackages) { extensions.push({ name: pkg.name, version: "latest", downloadUrl: "", // PIE 会自动处理 description: pkg.description, packageName: pkg.packageName, } as AvailablePeclExtension & { packageName?: string }); } console.log(`找到 ${extensions.length} 个可安装的扩展`); return extensions.sort((a, b) => a.name.localeCompare(b.name)); } catch (error: any) { console.error("获取可用扩展列表失败:", error); return this.getDefaultExtensionList(version); } } /** * 获取常用 PHP 扩展列表(带 Packagist 包名) * PIE 包名格式参考: https://packagist.org/extensions */ private getPopularExtensionsList( keyword?: string ): { name: string; description: string; packageName: string }[] { // PIE 兼容的扩展包名(vendor/package 格式) const popularExtensions = [ { name: "redis", description: "PHP extension for Redis", packageName: "phpredis/phpredis", // 正确的 PIE 包名 }, { name: "mongodb", description: "MongoDB driver for PHP", packageName: "mongodb/mongodb", // MongoDB 官方扩展 }, { name: "memcached", description: "PHP extension for Memcached", packageName: "php-memcached-dev/php-memcached", }, { name: "imagick", description: "ImageMagick for PHP", packageName: "imagick/imagick", }, { name: "xdebug", description: "Debugging and profiling for PHP", packageName: "xdebug/xdebug", }, { name: "swoole", description: "High-performance coroutine framework", packageName: "openswoole/swoole", // OpenSwoole(PIE 兼容) }, { name: "yaml", description: "YAML parser and emitter", packageName: "php/pecl-file_formats-yaml", }, { name: "apcu", description: "APCu - APC User Cache", packageName: "apcu/apcu", // 正确的 PIE 包名 }, { name: "grpc", description: "gRPC for PHP", packageName: "grpc/grpc" }, { name: "protobuf", description: "Protocol Buffers", packageName: "google/protobuf", }, { name: "igbinary", description: "Binary serialization", packageName: "igbinary/igbinary", }, { name: "msgpack", description: "MessagePack serialization", packageName: "msgpack/msgpack-php", }, { name: "sodium", description: "Modern cryptography library", packageName: "php/pecl-crypto-sodium", }, { name: "zip", description: "ZIP file support", packageName: "php/pecl-file_formats-zip", }, { name: "rar", description: "RAR archive support", packageName: "php/pecl-file_formats-rar", }, { name: "amqp", description: "AMQP messaging library", packageName: "php-amqp/php-amqp", }, { name: "oauth", description: "OAuth consumer extension", packageName: "php/pecl-web_services-oauth", }, { name: "ssh2", description: "SSH2 bindings", packageName: "php/pecl-networking-ssh2", }, { name: "event", description: "Event-based I/O", packageName: "php/pecl-event", }, { name: "uv", description: "libuv bindings", packageName: "amphp/ext-uv", }, ]; if (!keyword) { return popularExtensions; } const lowerKeyword = keyword.toLowerCase(); return popularExtensions.filter( (ext) => ext.name.toLowerCase().includes(lowerKeyword) || ext.description.toLowerCase().includes(lowerKeyword) || ext.packageName.toLowerCase().includes(lowerKeyword) ); } /** * 解码 HTML 实体 */ private decodeHtmlEntities(html: string): string { return html .replace(/./g, ".") .replace(///g, "/") .replace(/&/g, "&") .replace(/</g, "<") .replace(/>/g, ">") .replace(/"/g, '"') .replace(/&#(\d+);/g, (_, code) => String.fromCharCode(parseInt(code, 10)) ) .replace(/ /g, " "); } /** * 从 PECL 详情页获取扩展信息 * 流程: * 1. 访问 https://pecl.php.net/package/扩展名 获取最新稳定版本 * 2. 访问 https://pecl.php.net/package/扩展名/版本/windows 获取 Windows DLL 链接 */ private async getExtensionFromPecl( extName: string, phpInfo: { majorMinor: string; isNts: boolean; compiler: string } ): Promise { const { majorMinor, isNts } = phpInfo; try { // 1. 获取扩展详情页,找到最新稳定版本 const packageUrl = `https://pecl.php.net/package/${extName}`; console.log(`获取扩展详情: ${packageUrl}`); let packageHtml = await this.fetchHtmlContent(packageUrl); console.log(`获取到 HTML 长度: ${packageHtml.length}`); if (packageHtml.length < 1000) { console.log( `HTML 内容过短,可能获取失败: ${packageHtml.substring(0, 500)}` ); } // 解码 HTML 实体(如 . -> . , / -> /) packageHtml = this.decodeHtmlEntities(packageHtml); console.log(`解码后 HTML 长度: ${packageHtml.length}`); // 解析版本列表,找到最新的稳定版本(state=stable 且有 DLL 链接) // 分步解析: // 1. 找到所有表格行 ... // 2. 检查每行是否包含 DLL 链接和版本信息 let latestStableVersion: string | null = null; let latestBetaVersion: string | null = null; // 方法1:直接搜索带 DLL 链接的版本 // 格式: /package/xxx/VERSION/windows">...DLL const dllVersionRegex = /\/package\/[^/]+\/([\d.]+(?:RC\d+)?)\/windows/gi; const versionsWithDll: string[] = []; let dllMatch; // 调试:打印部分 HTML 内容查看格式 const windowsLinkIndex = packageHtml.indexOf("/windows"); if (windowsLinkIndex > 0) { console.log( `HTML 样本 (解码后): ${packageHtml.substring( Math.max(0, windowsLinkIndex - 100), windowsLinkIndex + 50 )}` ); } else { // 可能解码不完整,检查是否还有编码的 windows console.log("解码后未找到 /windows,检查原始 HTML..."); // 尝试多种可能的编码形式 const patterns = ['windows"', "windows<", "DLL", "/windows"]; for (const pattern of patterns) { const idx = packageHtml.indexOf(pattern); if (idx > 0) { console.log( `找到 "${pattern}" 在位置 ${idx}: ${packageHtml.substring( Math.max(0, idx - 80), idx + 40 )}` ); break; } } } while ((dllMatch = dllVersionRegex.exec(packageHtml)) !== null) { const ver = dllMatch[1]; if (!versionsWithDll.includes(ver)) { versionsWithDll.push(ver); console.log(`找到带 DLL 的版本: ${ver}`); } } // 对每个有 DLL 的版本,检查其状态 for (const ver of versionsWithDll) { // 在 HTML 中找到这个版本对应的行,检查是 stable 还是 beta // 格式: >VERSION...stable 或 >VERSION...beta const stateRegex = new RegExp( `>${ver.replace( /\./g, "\\." )}[\\s\\S]*?]*>\\s*(stable|beta|alpha)\\s*`, "i" ); const stateMatch = stateRegex.exec(packageHtml); if (stateMatch) { const state = stateMatch[1].toLowerCase(); console.log(`版本 ${ver} 状态: ${state}`); if (state === "stable" && !latestStableVersion) { latestStableVersion = ver; break; // 找到第一个稳定版本就停止 } else if ( (state === "beta" || state === "alpha") && !latestBetaVersion ) { latestBetaVersion = ver; } } } let targetVersion = latestStableVersion || latestBetaVersion; // 如果正则匹配失败,直接使用第一个有 DLL 的版本 if (!targetVersion && versionsWithDll.length > 0) { targetVersion = versionsWithDll[0]; console.log(`未能确定状态,使用第一个有 DLL 的版本: ${targetVersion}`); } if (!targetVersion) { console.log(`扩展 ${extName} 没有 Windows DLL`); return null; } console.log(`扩展 ${extName} 最新版本: ${targetVersion}`); // 2. 获取 Windows DLL 页面 const windowsUrl = `https://pecl.php.net/package/${extName}/${targetVersion}/windows`; console.log(`获取 Windows DLL 列表: ${windowsUrl}`); const windowsHtml = await this.fetchHtmlContent(windowsUrl); // 3. 查找匹配当前 PHP 版本的 DLL 链接 // 实际 URL 格式:https://downloads.php.net/~windows/pecl/releases/redis/6.3.0/php_redis-6.3.0-8.3-nts-vs16-x64.zip // 注意:URL 中的下划线可能被编码为 %5F,如 php%5Fredis // 链接文本有换行和大量空格 const tsType = isNts ? "nts" : "ts"; // 提取所有 pecl releases 的 zip 链接 const allLinksRegex = / x86 let matchedUrl: string | null = null; for (const url of allLinks) { // 解码 URL(%5F -> _) const decodedUrl = decodeURIComponent(url).toLowerCase(); // 检查是否匹配 PHP 版本和 NTS/TS // 格式: -8.3-nts- 或 -8.3-ts- const versionPattern = `-${majorMinor}-${tsType}-`; if (decodedUrl.includes(versionPattern)) { // 优先选择 x64 if (decodedUrl.includes("x64")) { matchedUrl = url; console.log(`匹配到 x64: ${url}`); break; } else if (!matchedUrl && decodedUrl.includes("x86")) { matchedUrl = url; console.log(`匹配到 x86: ${url}`); // 继续查找,看有没有 x64 } } } if (matchedUrl) { console.log(`找到 ${extName} ${targetVersion} 的 DLL: ${matchedUrl}`); return { name: extName, version: targetVersion, downloadUrl: matchedUrl, description: await this.getExtensionDescription(extName), }; } console.log( `扩展 ${extName} 没有适用于 PHP ${majorMinor} ${ isNts ? "NTS" : "TS" } 的 DLL` ); console.log(`可用链接: ${allLinks.slice(0, 5).join(", ")}...`); return null; } catch (error: any) { console.error(`获取扩展 ${extName} 失败:`, error.message); return null; } } /** * 从 windows.php.net 获取扩展列表(备用方法) */ private async getExtensionsFromWindowsPhp( version: string, phpInfo: { majorMinor: string; isNts: boolean; compiler: string }, searchKeyword?: string ): Promise { const extensions: AvailablePeclExtension[] = []; const { majorMinor, isNts, compiler } = phpInfo; try { const peclUrl = "https://windows.php.net/downloads/pecl/releases/"; const html = await this.fetchHtmlContent(peclUrl); // 解析扩展目录 const extDirRegex = //g; let match; const extNames: string[] = []; while ((match = extDirRegex.exec(html)) !== null) { const extName = match[1]; if (extName && !extName.startsWith(".") && extName !== "snaps") { extNames.push(extName); } } // 获取已安装的扩展 const installedExts = await this.getExtensions(version); const installedNames = installedExts.map((e) => e.name.toLowerCase()); // 过滤搜索关键词 let filteredNames = extNames; if (searchKeyword) { const keyword = searchKeyword.toLowerCase(); filteredNames = extNames.filter((name) => name.toLowerCase().includes(keyword) ); } // 限制检查数量 const checkNames = filteredNames.slice(0, searchKeyword ? 100 : 30); for (const extName of checkNames) { if (installedNames.includes(extName.toLowerCase())) continue; try { const extUrl = `${peclUrl}${extName}/`; const extHtml = await this.fetchHtmlContent(extUrl); const versionDirRegex = //g; const versions: string[] = []; let vMatch; while ((vMatch = versionDirRegex.exec(extHtml)) !== null) { versions.push(vMatch[1]); } if (versions.length > 0) { versions.sort((a, b) => b.localeCompare(a, undefined, { numeric: true }) ); const latestVersion = versions[0]; const tsType = isNts ? "nts" : "ts"; const dllPattern = `php_${extName}-${latestVersion}-${majorMinor}-${tsType}-${compiler}-x64.zip`; const dllUrl = `${extUrl}${latestVersion}/${dllPattern}`; const exists = await this.checkUrlExists(dllUrl); if (exists) { extensions.push({ name: extName, version: latestVersion, downloadUrl: dllUrl, }); } } } catch (e) { continue; } } return extensions.sort((a, b) => a.name.localeCompare(b.name)); } catch (error) { console.error("从 windows.php.net 获取扩展失败:", error); return this.getDefaultExtensionList(version); } } /** * 获取扩展描述(简化版) */ private async getExtensionDescription( extName: string ): Promise { const descriptions: Record = { redis: "PHP Redis 客户端扩展", memcached: "Memcached 缓存客户端", mongodb: "MongoDB 数据库驱动", imagick: "ImageMagick 图像处理", xdebug: "调试和性能分析工具", apcu: "用户数据缓存", yaml: "YAML 数据格式支持", swoole: "高性能异步网络框架", igbinary: "高效二进制序列化", ssh2: "SSH2 协议支持", grpc: "gRPC 远程调用支持", protobuf: "Protocol Buffers 支持", rar: "RAR 压缩文件支持", zip: "ZIP 压缩文件支持", oauth: "OAuth 认证支持", mailparse: "邮件解析扩展", uuid: "UUID 生成支持", xlswriter: "Excel 文件写入", event: "事件驱动扩展", ev: "libev 事件循环", }; return descriptions[extName.toLowerCase()]; } /** * 构建 PECL DLL 直接下载链接 */ private async buildPeclDownloadUrl( extName: string, phpInfo: { majorMinor: string; isNts: boolean; compiler: string } ): Promise { const { majorMinor, isNts, compiler } = phpInfo; const tsType = isNts ? "nts" : "ts"; // 常用扩展的 PECL 包名映射 const extNameMapping: { [key: string]: string } = { redis: "redis", mongodb: "mongodb", memcached: "memcached", imagick: "imagick", xdebug: "xdebug", swoole: "swoole", yaml: "yaml", apcu: "apcu", igbinary: "igbinary", msgpack: "msgpack", grpc: "grpc", protobuf: "protobuf", amqp: "amqp", ssh2: "ssh2", event: "event", oauth: "oauth", rar: "rar", zip: "zip", }; const peclExtName = extNameMapping[extName.toLowerCase()] || extName.toLowerCase(); try { // 先获取最新版本 const packageUrl = `https://pecl.php.net/package/${peclExtName}`; console.log(`从 PECL 获取 ${peclExtName} 版本列表: ${packageUrl}`); let html = await this.fetchHtmlContent(packageUrl); console.log(`获取到 HTML 长度: ${html.length}`); // 解码 HTML 实体 html = this.decodeHtmlEntities(html); // 查找有 DLL 的版本 - 格式: /package/redis/6.3.0/windows const dllVersionRegex = /\/package\/[^\/]+\/([\d.]+(?:RC\d+)?)\/windows/g; const matches = html.match(dllVersionRegex); console.log(`找到 DLL 链接: ${matches ? matches.length : 0} 个`); let latestVersion: string | null = null; if (matches && matches.length > 0) { // 从第一个匹配中提取版本号 const versionMatch = matches[0].match(/\/([\d.]+(?:RC\d+)?)\/windows/); if (versionMatch) { latestVersion = versionMatch[1]; } } if (!latestVersion) { console.log(`未找到 ${peclExtName} 的 Windows DLL 版本`); // 尝试直接用最新版本号 const anyVersionMatch = html.match(/\/package\/[^\/]+\/([\d.]+)["'>]/); if (anyVersionMatch) { latestVersion = anyVersionMatch[1]; console.log(`尝试使用版本: ${latestVersion}`); } else { return null; } } console.log(`找到 ${peclExtName} 版本: ${latestVersion}`); // 构建下载链接 // 格式: https://downloads.php.net/~windows/pecl/releases/redis/6.3.0/php_redis-6.3.0-8.4-nts-vs17-x64.zip const possibleUrls = [ `https://downloads.php.net/~windows/pecl/releases/${peclExtName}/${latestVersion}/php_${peclExtName}-${latestVersion}-${majorMinor}-${tsType}-${compiler}-x64.zip`, `https://downloads.php.net/~windows/pecl/releases/${peclExtName}/${latestVersion}/php_${peclExtName}-${latestVersion}-${majorMinor}-${tsType}-${compiler}-x86.zip`, // 备选格式(不同编译器版本) `https://downloads.php.net/~windows/pecl/releases/${peclExtName}/${latestVersion}/php_${peclExtName}-${latestVersion}-${majorMinor}-${tsType}-vs16-x64.zip`, `https://downloads.php.net/~windows/pecl/releases/${peclExtName}/${latestVersion}/php_${peclExtName}-${latestVersion}-${majorMinor}-${tsType}-vc15-x64.zip`, ]; // 检查哪个 URL 有效 for (const url of possibleUrls) { console.log(`检查 URL: ${url}`); const exists = await this.checkUrlExists(url); if (exists) { console.log(`找到有效的 PECL DLL: ${url}`); return url; } } // 如果精确匹配失败,尝试从 Windows 页面解析 const windowsUrl = `https://pecl.php.net/package/${peclExtName}/${latestVersion}/windows`; console.log(`从 Windows 页面查找: ${windowsUrl}`); const windowsHtml = await this.fetchHtmlContent(windowsUrl); // 查找匹配的 DLL 链接 const allLinksRegex = / { try { const phpExe = join(phpPath, "php.exe"); if (!existsSync(phpExe)) return null; const { stdout } = await execAsync(`"${phpExe}" -i`, { windowsHide: true, timeout: 10000, }); // 解析 PHP 版本 const versionMatch = stdout.match(/PHP Version => (\d+\.\d+)/); const majorMinor = versionMatch ? versionMatch[1] : "8.3"; // 检查是否是 NTS const isNts = stdout.includes("Thread Safety => disabled"); // 解析编译器版本 const compilerMatch = stdout.match(/Compiler => MSVC(\d+)/) || stdout.match(/Visual C\+\+ (\d{4})/); let compiler = "vc15"; // 默认值 if (compilerMatch) { const msvcVersion = parseInt(compilerMatch[1]); if (msvcVersion >= 1930 || compilerMatch[1] === "2022") { compiler = "vs17"; } else if (msvcVersion >= 1920 || compilerMatch[1] === "2019") { compiler = "vs16"; } else if (msvcVersion >= 1910 || compilerMatch[1] === "2017") { compiler = "vc15"; } } // 也从目录名获取信息 const pathParts = phpPath.toLowerCase(); if (pathParts.includes("vs17")) compiler = "vs17"; else if (pathParts.includes("vs16")) compiler = "vs16"; console.log( `PHP Info: ${majorMinor}, NTS: ${isNts}, Compiler: ${compiler}` ); return { majorMinor, isNts, compiler }; } catch (error) { console.error("获取 PHP 信息失败:", error); return null; } } /** * 获取默认扩展列表(当在线获取失败时使用) */ private getDefaultExtensionList(version: string): AvailablePeclExtension[] { // 常用的 PECL 扩展 const commonExtensions = [ { name: "redis", description: "Redis 缓存扩展" }, { name: "memcached", description: "Memcached 缓存扩展" }, { name: "mongodb", description: "MongoDB 数据库扩展" }, { name: "imagick", description: "图像处理扩展" }, { name: "xdebug", description: "调试和分析扩展" }, { name: "apcu", description: "APCu 用户缓存扩展" }, { name: "yaml", description: "YAML 解析扩展" }, { name: "swoole", description: "高性能网络框架扩展" }, { name: "igbinary", description: "高效序列化扩展" }, { name: "ssh2", description: "SSH2 连接扩展" }, ]; return commonExtensions.map((ext) => ({ name: ext.name, version: "latest", downloadUrl: `https://windows.php.net/downloads/pecl/releases/${ext.name}/`, description: ext.description, })); } /** * 确保 PIE (PHP Installer for Extensions) 已安装 * 优先使用 Windows 可执行文件版本(实验性),备用 phar 版本 * 参考: https://github.com/php/pie/blob/1.4.x/docs/usage.md */ private async ensurePieInstalled(): Promise<{ path: string; isExe: boolean; } | null> { const pieDir = join(this.configStore.getBasePath(), "tools"); const pieExePath = join(pieDir, "pie.exe"); const piePharPath = join(pieDir, "pie.phar"); // 优先检查 exe 版本 if (existsSync(pieExePath)) { return { path: pieExePath, isExe: true }; } // 检查 phar 版本 if (existsSync(piePharPath)) { return { path: piePharPath, isExe: false }; } // 下载 PIE console.log("正在下载 PIE (PHP Installer for Extensions)..."); mkdirSync(pieDir, { recursive: true }); // 尝试下载 Windows 可执行文件版本(实验性但更可靠) try { const pieExeUrl = "https://php.github.io/pie/pie-Windows-X64.exe"; console.log(`尝试下载 PIE Windows 可执行文件: ${pieExeUrl}`); await this.downloadFile(pieExeUrl, pieExePath); console.log("PIE Windows 可执行文件下载完成"); return { path: pieExePath, isExe: true }; } catch (error: any) { console.error("下载 PIE exe 失败,尝试 phar 版本:", error.message); } // 备用:下载 phar 版本 try { const piePharUrl = "https://github.com/php/pie/releases/latest/download/pie.phar"; console.log(`下载 PIE phar: ${piePharUrl}`); await this.downloadFile(piePharUrl, piePharPath); console.log("PIE phar 下载完成"); return { path: piePharPath, isExe: false }; } catch (error: any) { console.error("下载 PIE phar 失败:", error.message); return null; } } /** * 使用 PIE 安装扩展 * 参考: https://github.com/php/pie/blob/1.4.x/docs/usage.md * * Windows 上使用 --with-php-path 指定目标 PHP 版本: * pie install --with-php-path=C:\php-8.3.6\php.exe vendor/package */ private async installWithPie( phpPath: string, extName: string, packageName?: string ): Promise<{ success: boolean; message: string }> { const pieInfo = await this.ensurePieInstalled(); if (!pieInfo) { return { success: false, message: "PIE 下载失败,无法使用 PIE 安装" }; } const phpExe = join(phpPath, "php.exe"); // 使用 package name(如 phpredis/phpredis)或扩展名 const pkg = packageName || extName; try { console.log(`使用 PIE 安装扩展: ${pkg}`); let cmd: string; if (pieInfo.isExe) { // 使用 PIE Windows 可执行文件,通过 --with-php-path 指定目标 PHP cmd = `"${pieInfo.path}" install --with-php-path="${phpExe}" ${pkg}`; } else { // 使用 phar 版本,需要 PHP 来运行 cmd = `"${phpExe}" "${pieInfo.path}" install ${pkg}`; } console.log(`执行命令: ${cmd}`); const { stdout, stderr } = await execAsync(cmd, { timeout: 300000, // 5 分钟超时 windowsHide: true, env: { ...process.env, // 跳过 Box Requirements Checker(如果有问题) BOX_REQUIREMENT_CHECKER: "0", }, }); console.log("PIE 输出:", stdout); if (stderr) console.log("PIE stderr:", stderr); // 检查是否安装成功 // 成功信息示例: "Install complete", "Already installed", "Extension is enabled and loaded" if ( stdout.includes("Install complete") || stdout.includes("Already installed") || stdout.includes("Extension is enabled") ) { return { success: true, message: `${extName} 扩展通过 PIE 安装成功` }; } else if (stdout.includes("extension=")) { return { success: true, message: `${extName} 扩展安装成功,请检查 php.ini 配置`, }; } // 如果有任何输出但没有明显错误,可能也是成功的 if ( stdout && !stdout.includes("Error") && !stdout.includes("Exception") ) { return { success: true, message: `${extName} 扩展安装完成\n\n${stdout}`, }; } return { success: false, message: `PIE 安装输出: ${stdout}\n${stderr || ""}`, }; } catch (error: any) { console.error("PIE 安装失败:", error.message); // 提取有用的错误信息 let errorMsg = error.message; if (error.stdout) errorMsg += `\n输出: ${error.stdout}`; if (error.stderr) errorMsg += `\n错误: ${error.stderr}`; return { success: false, message: `PIE 安装失败: ${errorMsg}` }; } } /** * 安装扩展(使用 PIE) */ async installExtension( version: string, extName: string, downloadUrl?: string, packageName?: string ): Promise<{ success: boolean; message: string }> { try { const phpPath = this.configStore.getPhpPath(version); const extDir = join(phpPath, "ext"); if (!existsSync(extDir)) { return { success: false, message: "PHP 扩展目录不存在" }; } // 获取 PHP 信息 const phpInfo = await this.getPhpInfo(phpPath); if (!phpInfo) { return { success: false, message: "无法获取 PHP 版本信息" }; } // 使用 PIE 安装 console.log(`使用 PIE 安装扩展 ${extName}...`); const pieResult = await this.installWithPie( phpPath, extName, packageName ); return pieResult; } catch (error: any) { console.error(`安装扩展 ${extName} 失败:`, error); return { success: false, message: `安装失败: ${error.message}` }; } } /** * 下载扩展文件 */ private async downloadExtension(url: string, dest: string): Promise { return new Promise((resolve, reject) => { const file = createWriteStream(dest); const protocol = url.startsWith("https") ? https : http; const request = protocol.get( url, { headers: { "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36", }, }, (response) => { if ( response.statusCode === 301 || response.statusCode === 302 || response.statusCode === 307 ) { const redirectUrl = response.headers.location; if (redirectUrl) { file.close(); if (existsSync(dest)) unlinkSync(dest); this.downloadExtension(redirectUrl, dest) .then(resolve) .catch(reject); return; } } if (response.statusCode !== 200) { reject(new Error(`下载失败,状态码: ${response.statusCode}`)); return; } const totalSize = parseInt( response.headers["content-length"] || "0", 10 ); let downloadedSize = 0; let lastProgressTime = Date.now(); response.on("data", (chunk) => { downloadedSize += chunk.length; const now = Date.now(); if (now - lastProgressTime > 300) { const progress = totalSize > 0 ? Math.round((downloadedSize / totalSize) * 100) : 0; sendDownloadProgress( "php-ext", progress, downloadedSize, totalSize ); lastProgressTime = now; } }); response.pipe(file); file.on("finish", () => { file.close(); sendDownloadProgress("php-ext", 100, totalSize, totalSize); resolve(); }); } ); request.on("error", (err) => { file.close(); if (existsSync(dest)) unlinkSync(dest); reject(err); }); request.setTimeout(120000, () => { request.destroy(); reject(new Error("下载超时")); }); }); } /** * 解压 ZIP 文件 */ private async unzipFile(zipPath: string, destPath: string): Promise { const { createReadStream } = await import("fs"); const unzipper = await import("unzipper"); const { pipeline } = await import("stream/promises"); await pipeline( createReadStream(zipPath), unzipper.Extract({ path: destPath }) ); } /** * 检查 URL 是否存在 */ private async checkUrlExists(url: string): Promise { return new Promise((resolve) => { const protocol = url.startsWith("https") ? https : http; const request = protocol.request( url, { method: "HEAD", timeout: 5000 }, (response) => { resolve(response.statusCode === 200); } ); request.on("error", () => resolve(false)); request.on("timeout", () => { request.destroy(); resolve(false); }); request.end(); }); } /** * 获取 HTML 内容 */ private async fetchHtmlContent(url: string): Promise { return new Promise((resolve, reject) => { const protocol = url.startsWith("https") ? https : http; const request = protocol.get( url, { headers: { "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36", }, timeout: 15000, }, (response) => { if (response.statusCode === 301 || response.statusCode === 302) { const redirectUrl = response.headers.location; if (redirectUrl) { this.fetchHtmlContent(redirectUrl).then(resolve).catch(reject); return; } } if (response.statusCode !== 200) { reject(new Error(`HTTP ${response.statusCode}`)); return; } let html = ""; response.on("data", (chunk) => (html += chunk)); response.on("end", () => resolve(html)); } ); request.on("error", reject); request.on("timeout", () => { request.destroy(); reject(new Error("请求超时")); }); }); } /** * 获取 php.ini 配置内容 */ async getConfig(version: string): Promise { const phpPath = this.configStore.getPhpPath(version); const iniPath = join(phpPath, "php.ini"); if (!existsSync(iniPath)) { return ""; } return readFileSync(iniPath, "utf-8"); } /** * 保存 php.ini 配置 */ async saveConfig( version: string, config: string ): Promise<{ success: boolean; message: string }> { try { const phpPath = this.configStore.getPhpPath(version); const iniPath = join(phpPath, "php.ini"); writeFileSync(iniPath, config); return { success: true, message: "php.ini 保存成功" }; } catch (error: any) { return { success: false, message: `保存失败: ${error.message}` }; } } // ==================== 私有方法 ==================== private async downloadFile(url: string, dest: string): Promise { return new Promise((resolve, reject) => { // 确保目标目录存在 const destDir = require("path").dirname(dest); if (!existsSync(destDir)) { mkdirSync(destDir, { recursive: true }); } const file = createWriteStream(dest); const protocol = url.startsWith("https") ? https : http; console.log(`开始下载: ${url}`); const request = protocol.get( url, { headers: { "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36", }, }, (response) => { // 处理重定向 if (response.statusCode === 301 || response.statusCode === 302) { const redirectUrl = response.headers.location; if (redirectUrl) { file.close(); if (existsSync(dest)) unlinkSync(dest); console.log(`重定向到: ${redirectUrl}`); this.downloadFile(redirectUrl, dest).then(resolve).catch(reject); return; } } if (response.statusCode !== 200) { file.close(); if (existsSync(dest)) unlinkSync(dest); reject( new Error(`下载失败,状态码: ${response.statusCode},URL: ${url}`) ); return; } const totalSize = parseInt( response.headers["content-length"] || "0", 10 ); let downloadedSize = 0; let lastProgressTime = Date.now(); response.on("data", (chunk) => { downloadedSize += chunk.length; const now = Date.now(); // 每500ms发送一次进度 if (now - lastProgressTime > 500) { const progress = totalSize > 0 ? Math.round((downloadedSize / totalSize) * 100) : 0; sendDownloadProgress("php", progress, downloadedSize, totalSize); lastProgressTime = now; } }); response.pipe(file); file.on("finish", () => { file.close(); sendDownloadProgress("php", 100, totalSize, totalSize); console.log("下载完成"); resolve(); }); file.on("error", (err) => { file.close(); if (existsSync(dest)) unlinkSync(dest); reject(err); }); } ); request.on("error", (err) => { file.close(); if (existsSync(dest)) unlinkSync(dest); reject(new Error(`网络错误: ${err.message}`)); }); // 设置超时 15 分钟(PHP包较大,国际网络可能较慢) request.setTimeout(900000, () => { request.destroy(); file.close(); if (existsSync(dest)) unlinkSync(dest); reject(new Error("下载超时(15分钟)")); }); }); } private async unzip(zipPath: string, destPath: string): Promise { // 确保目标目录存在 if (!existsSync(destPath)) { mkdirSync(destPath, { recursive: true }); } const { createReadStream } = await import("fs"); const unzipper = await import("unzipper"); return new Promise((resolve, reject) => { const stream = createReadStream(zipPath).pipe( unzipper.Extract({ path: destPath }) ); stream.on("close", () => { console.log("解压完成:", destPath); resolve(); }); stream.on("error", (err: Error) => { console.error("解压错误:", err); reject(new Error(`解压失败: ${err.message}`)); }); }); } private async createDefaultPhpIni(phpPath: string): Promise { const devIniPath = join(phpPath, "php.ini-development"); const iniPath = join(phpPath, "php.ini"); if (existsSync(devIniPath) && !existsSync(iniPath)) { let content = readFileSync(devIniPath, "utf-8"); // 设置常用配置 content = content.replace( /;extension_dir = "ext"/, 'extension_dir = "ext"' ); content = content.replace(/;extension=curl/, "extension=curl"); content = content.replace(/;extension=fileinfo/, "extension=fileinfo"); content = content.replace(/;extension=gd/, "extension=gd"); content = content.replace(/;extension=mbstring/, "extension=mbstring"); content = content.replace(/;extension=mysqli/, "extension=mysqli"); content = content.replace(/;extension=openssl/, "extension=openssl"); content = content.replace(/;extension=pdo_mysql/, "extension=pdo_mysql"); content = content.replace(/;extension=zip/, "extension=zip"); // 设置时区 content = content.replace( /;date.timezone =/, "date.timezone = Asia/Shanghai" ); writeFileSync(iniPath, content); } } private async addToPath(phpPath: string): Promise { try { // 先移除所有 PHP 相关路径,再添加新路径 const tempScriptPath = join( this.configStore.getTempPath(), "update_path.ps1" ); mkdirSync(this.configStore.getTempPath(), { recursive: true }); // 将路径作为参数传递给脚本,避免转义问题 // 使用纯英文避免编码问题 const psScript = ` param([string]$NewPhpPath) $userPath = [Environment]::GetEnvironmentVariable('PATH', 'User') if ($userPath -eq $null) { $userPath = '' } Write-Host "Original PATH length: $($userPath.Length)" Write-Host "New PHP path: $NewPhpPath" $paths = $userPath -split ';' | Where-Object { $_ -ne '' -and $_.Trim() -ne '' } Write-Host "Original path count: $($paths.Count)" $filteredPaths = @() foreach ($p in $paths) { $pathLower = $p.ToLower() $isPhpPath = $false if ($pathLower -like '*\\php\\php-*' -or $pathLower -like '*\\php-*\\*' -or $pathLower -like '*phpenv*php*' -or $pathLower -like '*phper*php*' -or $pathLower -like '*xampp*php*' -or $pathLower -like '*wamp*php*' -or $pathLower -like '*laragon*php*' -or $pathLower -match '\\\\php\\\\?$' -or $pathLower -match '\\\\php-\\d') { $isPhpPath = $true Write-Host "Removing PHP path: $p" } if (-not $isPhpPath -and (Test-Path (Join-Path $p 'php.exe') -ErrorAction SilentlyContinue)) { $isPhpPath = $true Write-Host "Removing path with php.exe: $p" } if (-not $isPhpPath) { $filteredPaths += $p } } Write-Host "Filtered path count: $($filteredPaths.Count)" $allPaths = @($NewPhpPath) + $filteredPaths $newPath = $allPaths -join ';' Write-Host "New PATH starts with: $($newPath.Substring(0, [Math]::Min(150, $newPath.Length)))..." [Environment]::SetEnvironmentVariable('PATH', $newPath, 'User') Start-Sleep -Milliseconds 500 $verifyPath = [Environment]::GetEnvironmentVariable('PATH', 'User') if ($verifyPath -and $verifyPath.Contains($NewPhpPath)) { Write-Host "SUCCESS: PHP path added to user PATH" } else { Write-Host "WARNING: PATH update may not have taken effect" } `; writeFileSync(tempScriptPath, psScript, "utf-8"); // 使用参数传递路径 const { stdout, stderr } = await execAsync( `powershell -ExecutionPolicy Bypass -File "${tempScriptPath}" -NewPhpPath "${phpPath}"`, { windowsHide: true, timeout: 30000 } ); console.log("PATH 更新输出:", stdout); if (stderr) console.error("PATH stderr:", stderr); // 检查是否成功 if (stdout.includes("SUCCESS")) { console.log("PATH 更新成功"); } else { console.warn("PATH 更新可能未完全成功"); } // 清理临时脚本 if (existsSync(tempScriptPath)) { unlinkSync(tempScriptPath); } } catch (error: any) { console.error("添加 PATH 失败:", error); throw new Error(`设置环境变量失败: ${error.message}`); } } private async removeFromPath(phpPath: string): Promise { try { const escapedPhpPath = phpPath.replace(/\\/g, "\\\\"); const psCommand = ` $userPath = [Environment]::GetEnvironmentVariable('PATH', 'User') if ($userPath -eq $null) { $userPath = '' } $paths = $userPath -split ';' | Where-Object { $_ -ne '' -and $_ -ne '${escapedPhpPath}' } $newPath = $paths -join ';' [Environment]::SetEnvironmentVariable('PATH', $newPath, 'User') Write-Host "PATH removed successfully" `; const { stdout, stderr } = await execAsync( `powershell -Command "${psCommand}"`, { windowsHide: true } ); console.log("移除 PATH 成功:", stdout); if (stderr) console.error("PATH stderr:", stderr); } catch (error: any) { console.error("移除 PATH 失败:", error); } } private removeDirectory(dir: string): void { if (existsSync(dir)) { const files = readdirSync(dir, { withFileTypes: true }); for (const file of files) { const fullPath = join(dir, file.name); if (file.isDirectory()) { this.removeDirectory(fullPath); } else { unlinkSync(fullPath); } } rmdirSync(dir); } } }