From 6b2f2543c4cc7796807fe199d151244661a7a5ec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=BA=81=E5=8A=A8=E7=9A=84=E6=B0=A8=E6=B0=94?= <131591012+zaodonganqi@users.noreply.github.com> Date: Thu, 5 Feb 2026 01:48:06 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=94=AF=E6=8C=81=E4=BB=93=E5=BA=93?= =?UTF-8?q?=E4=BD=BF=E7=94=A8=E5=85=AC=E5=85=B1=E8=B5=84=E6=BA=90=20(#2716?= =?UTF-8?q?)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Core/Script/PackageDocumentLoader.cs | 145 ++++++++++ .../Core/Script/Project/ScriptProject.cs | 37 ++- .../Core/Script/ScriptRepoUpdater.cs | 247 ++++++++++++++++++ 3 files changed, 424 insertions(+), 5 deletions(-) create mode 100644 BetterGenshinImpact/Core/Script/PackageDocumentLoader.cs diff --git a/BetterGenshinImpact/Core/Script/PackageDocumentLoader.cs b/BetterGenshinImpact/Core/Script/PackageDocumentLoader.cs new file mode 100644 index 00000000..20836631 --- /dev/null +++ b/BetterGenshinImpact/Core/Script/PackageDocumentLoader.cs @@ -0,0 +1,145 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text.RegularExpressions; +using System.Threading.Tasks; +using Microsoft.ClearScript; +using Microsoft.ClearScript.JavaScript; + +namespace BetterGenshinImpact.Core.Script +{ + public class PackageDocumentLoader : DocumentLoader + { + private readonly string _scriptRootPath; + + public PackageDocumentLoader(string scriptRootPath) + { + _scriptRootPath = Path.GetFullPath(scriptRootPath); + } + + public override async Task LoadDocumentAsync(DocumentSettings settings, DocumentInfo? sourceInfo, string specifier, DocumentCategory category, DocumentContextCallback contextCallback) + { + string? targetPath = ResolvePhysicalPath(settings, sourceInfo, specifier); + + if (targetPath == null || !File.Exists(targetPath)) + { + return await Default.LoadDocumentAsync(settings, sourceInfo, specifier, category, contextCallback); + } + + // 处理 JS 文件的重写 + if (Path.GetExtension(targetPath).ToLower() == ".js") + { + string content = await File.ReadAllTextAsync(targetPath); + string processedCode = RewriteScriptCode(content, targetPath); + return new StringDocument(new DocumentInfo(targetPath) { Category = ModuleCategory.Standard }, processedCode); + } + + return await Default.LoadDocumentAsync(settings, sourceInfo, specifier, category, contextCallback); + } + + /// + /// js重写 + /// + public string RewriteScriptCode(string code, string currentFilePath) + { + if (string.IsNullOrEmpty(code)) return code; + + string result = code.Replace("../../../packages", "packages"); + + // 拦截资源导入 + var resourceRegex = new Regex(@"import\s+([\w\d_*$]+|[\s\S]*?)\s+from\s+(['""])([^'""\n]+)(['""])"); + result = resourceRegex.Replace(result, match => + { + string importPart = match.Groups[1].Value.Trim(); + string quote = match.Groups[2].Value; + string rawPath = match.Groups[3].Value; + + string path = rawPath.Replace("../../../packages", "packages"); + string? resourceFullPath = ResolvePathInternal(null, currentFilePath, path); + + if (resourceFullPath != null && File.Exists(resourceFullPath)) + { + string normalizedPath = Path.GetRelativePath(_scriptRootPath, resourceFullPath).Replace("\\", "/"); + bool isJs = resourceFullPath.EndsWith(".js", StringComparison.OrdinalIgnoreCase); + bool isImage = IsImageFile(resourceFullPath); + + // 图片 -> Mat + if (isImage) + { + if (importPart.StartsWith("{")) return match.Value; + return $"const {importPart} = file.ReadImageMatSync({quote}{normalizedPath}{quote});"; + } + + // 非 JS 资源 -> 文本内容 + if (!isJs) + { + if (importPart.StartsWith("{")) return match.Value; + return $"const {importPart} = file.ReadTextSync({quote}{normalizedPath}{quote});"; + } + } + + return match.Value; + }); + + return result; + } + + private string? ResolvePhysicalPath(DocumentSettings settings, DocumentInfo? sourceInfo, string specifier) + { + return ResolvePathInternal(settings.SearchPath, sourceInfo?.Name, specifier); + } + + private string? ResolvePathInternal(string? searchPath, string? referrer, string specifier) + { + if (specifier.StartsWith("packages/", StringComparison.OrdinalIgnoreCase)) + { + return ProbeFile(Path.Combine(_scriptRootPath, specifier)); + } + + if (specifier.StartsWith(".")) + { + if (!string.IsNullOrEmpty(referrer)) + { + string? dir = Path.GetDirectoryName(referrer); + if (dir != null) + { + return ProbeFile(Path.GetFullPath(Path.Combine(dir, specifier))); + } + } + } + + if (!string.IsNullOrEmpty(searchPath)) + { + var paths = searchPath.Split(';', StringSplitOptions.RemoveEmptyEntries); + foreach (var p in paths) + { + string combined = Path.Combine(p, specifier); + string? found = ProbeFile(combined); + if (found != null) return found; + } + } + + return ProbeFile(Path.Combine(_scriptRootPath, specifier)); + } + + private string? ProbeFile(string path) + { + try + { + if (File.Exists(path)) return path; + if (File.Exists(path + ".js")) return path + ".js"; + } + catch { } + return null; + } + + private static bool IsImageFile(string path) + { + if (string.IsNullOrEmpty(path)) return false; + string ext = Path.GetExtension(path).ToLower(); + string[] imageExtensions = { ".png", ".jpg", ".jpeg", ".bmp", ".tiff", ".webp" }; + return imageExtensions.Contains(ext); + } + } +} diff --git a/BetterGenshinImpact/Core/Script/Project/ScriptProject.cs b/BetterGenshinImpact/Core/Script/Project/ScriptProject.cs index fb948772..f114f364 100644 --- a/BetterGenshinImpact/Core/Script/Project/ScriptProject.cs +++ b/BetterGenshinImpact/Core/Script/Project/ScriptProject.cs @@ -2,8 +2,10 @@ using BetterGenshinImpact.Core.Config; using Microsoft.ClearScript; using Microsoft.ClearScript.V8; using System; +using System.Collections.Generic; using System.Diagnostics; using System.IO; +using System.Linq; using System.Threading.Tasks; using System.Windows; using System.Windows.Controls; @@ -69,10 +71,24 @@ public class ScriptProject return scrollViewer; } - public IScriptEngine BuildScriptEngine(PathingPartyConfig? partyConfig = null) + private IScriptEngine BuildScriptEngine(PathingPartyConfig? partyConfig) { - IScriptEngine engine = new V8ScriptEngine(V8ScriptEngineFlags.UseCaseInsensitiveMemberBinding | V8ScriptEngineFlags.EnableTaskPromiseConversion); - EngineExtend.InitHost(engine, ProjectPath, Manifest.Library,partyConfig); + V8ScriptEngine engine = new V8ScriptEngine(V8ScriptEngineFlags.UseCaseInsensitiveMemberBinding | V8ScriptEngineFlags.EnableTaskPromiseConversion); + + // packages 依赖和资源重载 + var loader = new PackageDocumentLoader(ProjectPath); + engine.DocumentSettings.Loader = loader; + + // 添加 packages 到搜索路径 + var libraries = new HashSet(Manifest.Library ?? Array.Empty()) + { + ".", + "./packages" + }; + + var libraryList = libraries.ToList(); + + EngineExtend.InitHost(engine, ProjectPath, libraryList.ToArray(), partyConfig); return engine; } @@ -83,6 +99,10 @@ public class ScriptProject // 加载代码 var code = await LoadCode(); var engine = BuildScriptEngine(partyConfig); + + // 使用自定义加载器解析脚本文件 + var loader = (PackageDocumentLoader)engine.DocumentSettings.Loader; + if (context != null) { // 写入配置的内容 @@ -90,12 +110,19 @@ public class ScriptProject } try { - if (Manifest.Library.Length != 0) + bool useModule = Manifest.Library.Length != 0 || + code.Contains("import ", StringComparison.Ordinal) || + code.Contains("export ", StringComparison.Ordinal); + + if (useModule) { // 清除Document缓存 DocumentLoader.Default.DiscardCachedDocuments(); - var evaluation = engine.Evaluate(new DocumentInfo { Category = ModuleCategory.Standard }, code); + string mainScriptPath = Path.Combine(ProjectPath, Manifest.Main); + string runtimeCode = loader.RewriteScriptCode(code, mainScriptPath); + + var evaluation = engine.Evaluate(new DocumentInfo(mainScriptPath) { Category = ModuleCategory.Standard }, runtimeCode); if (evaluation is Task task) await task; } else diff --git a/BetterGenshinImpact/Core/Script/ScriptRepoUpdater.cs b/BetterGenshinImpact/Core/Script/ScriptRepoUpdater.cs index 52cc6583..4d411ff0 100644 --- a/BetterGenshinImpact/Core/Script/ScriptRepoUpdater.cs +++ b/BetterGenshinImpact/Core/Script/ScriptRepoUpdater.cs @@ -1338,6 +1338,12 @@ public class ScriptRepoUpdater : Singleton RestoreScriptFiles(path, repoPath); } + // Resolving dependencies for JS scripts + if (first == "js") + { + ResolveScriptDependencies(repoPath, destPath); + } + UpdateSubscribedScriptPaths(); Toast.Success("脚本订阅链接导入完成"); } @@ -1514,6 +1520,247 @@ public class ScriptRepoUpdater : Singleton } } + /// + /// 解析并下载脚本依赖 (packages) + /// + /// 仓库路径 + /// 本地脚本路径 + private void ResolveScriptDependencies(string repoPath, string localScriptPath) + { + try + { + var processedFiles = new HashSet(); + var processingQueue = new Queue(); + + // 确定根目录 + string baseDestDir; + if (File.Exists(localScriptPath)) + { + processingQueue.Enqueue(localScriptPath); + baseDestDir = Path.GetDirectoryName(localScriptPath) ?? localScriptPath; + } + else if (Directory.Exists(localScriptPath)) + { + baseDestDir = localScriptPath; + // 初始加入目录下的所有 JS 文件 + foreach (var f in Directory.GetFiles(localScriptPath, "*.js", SearchOption.AllDirectories)) + { + processingQueue.Enqueue(f); + } + } + else + { + return; + } + + // 清理 packages + var targetPackagesDir = Path.Combine(baseDestDir, "packages"); + if (Directory.Exists(targetPackagesDir)) + { + try + { + Directory.Delete(targetPackagesDir, true); + // _logger.LogInformation($"已清理旧依赖目录: {targetPackagesDir}"); + } + catch (Exception ex) + { + _logger.LogWarning($"清理依赖目录失败: {ex.Message}"); + } + } + + // 捕获导入的变量名 + var regex = new System.Text.RegularExpressions.Regex( + @"(import\s+([\w\d_$]+)\s+from\s+['""]|import\s+(?:[\w\s{},*]*?from\s+)?['""]|export\s+(?:[\w\s{},*]*?from\s+)?['""]|import\s+['""]|require\s*\(\s*['""])([^'""\n]+)(['""])"); + + while (processingQueue.Count > 0) + { + var currentFile = processingQueue.Dequeue(); + + // 避免重复处理 + if (processedFiles.Contains(currentFile)) continue; + processedFiles.Add(currentFile); + + try + { + if (!File.Exists(currentFile)) continue; + + var content = File.ReadAllText(currentFile); + var matches = regex.Matches(content); + + foreach (System.Text.RegularExpressions.Match match in matches) + { + var originalPath = match.Groups[3].Value; + string? packagePath = null; + + // 识别是否为 packages 引用 + int packageIndex = originalPath.IndexOf("packages/", StringComparison.OrdinalIgnoreCase); + if (packageIndex >= 0) + { + packagePath = originalPath.Substring(packageIndex).Replace('\\', '/'); + } + // 识别是否为 packages 内部的相对引用 + else if (originalPath.StartsWith(".")) + { + // 检查当前文件是否在 packages 目录下 + var localPackagesDir = Path.Combine(baseDestDir, "packages"); + if (currentFile.StartsWith(localPackagesDir, StringComparison.OrdinalIgnoreCase)) + { + // 计算当前文件对应的 repo 路径 + var relToScript = Path.GetRelativePath(baseDestDir, currentFile); + var relDir = Path.GetDirectoryName(relToScript); // e.g. packages/utils + + if (relDir != null) + { + var depPackagePath = Path.Combine(relDir, originalPath).Replace('\\', '/'); + // 规范化路径 + depPackagePath = Path.GetFullPath(Path.Combine(baseDestDir, depPackagePath)); + depPackagePath = Path.GetRelativePath(baseDestDir, depPackagePath).Replace('\\', '/'); + + if (depPackagePath.StartsWith("packages/", StringComparison.OrdinalIgnoreCase)) + { + packagePath = depPackagePath; + } + } + } + } + + if (packagePath != null) + { + var destPath = Path.Combine(baseDestDir, packagePath); + bool isCode = packagePath.EndsWith(".js", StringComparison.OrdinalIgnoreCase); + + // 如果文件不存在,下载 + if (!File.Exists(destPath)) + { + bool downloaded = false; + + // 尝试精确下载 + if (DownloadAndQueue(repoPath, packagePath, destPath, processingQueue)) + { + downloaded = true; + } + // 尝试 .js + else if (isCode || packagePath.IndexOf('.') == -1) + { + // 尝试补充 .js + if (DownloadAndQueue(repoPath, packagePath + ".js", destPath + ".js", processingQueue)) downloaded = true; + } + + if (!downloaded) + { + _logger.LogWarning($"依赖未找到: {packagePath} (in {Path.GetFileName(currentFile)})"); + } + } + else + { + // 文件已存在 + if (isCode) + { + if (!processedFiles.Contains(destPath) && !processingQueue.Contains(destPath)) processingQueue.Enqueue(destPath); + } + } + } + } + } + catch (Exception ex) + { + _logger.LogWarning($"分析文件依赖出错: {currentFile}, {ex.Message}"); + } + } + } + catch (Exception ex) + { + _logger.LogError(ex, "解析脚本依赖主流程失败"); + } + } + + private bool DownloadAndQueue(string repoPath, string sourcePath, string destPath, Queue queue) + { + if (CheckoutRepoRootPath(repoPath, sourcePath, destPath)) + { + queue.Enqueue(destPath); + return true; + } + return false; + } + + /// + /// 从仓库根目录检出指定路径 + /// + /// 是否成功检出 + private bool CheckoutRepoRootPath(string repoPath, string sourcePath, string destPath) + { + bool isGitRepo = IsGitRepository(repoPath); + + if (isGitRepo) + { + using var repo = new Repository(repoPath); + var commit = repo.Head.Tip; + + if (commit == null) throw new Exception("仓库HEAD未指向任何提交"); + + TreeEntry? entry = null; + var pathParts = sourcePath.Split(new[] { '/', '\\' }, StringSplitOptions.RemoveEmptyEntries); + Tree currentTree = commit.Tree; // Start from ROOT tree + + for (int i = 0; i < pathParts.Length; i++) + { + entry = currentTree[pathParts[i]]; + if (entry == null) return false; // Path not found + + if (i < pathParts.Length - 1) + { + if (entry.TargetType != TreeEntryTargetType.Tree) + // 路径中间部分不是目录,说明路径错误 + return false; + currentTree = (Tree)entry.Target; + } + } + + if (entry == null) return false; + + if (entry.TargetType == TreeEntryTargetType.Blob) + { + var blob = (Blob)entry.Target; + var dir = Path.GetDirectoryName(destPath); + if (!string.IsNullOrEmpty(dir) && !Directory.Exists(dir)) Directory.CreateDirectory(dir); + + using var contentStream = blob.GetContentStream(); + using var fileStream = File.Create(destPath); + contentStream.CopyTo(fileStream); + return true; + } + else if (entry.TargetType == TreeEntryTargetType.Tree) + { + var tree = (Tree)entry.Target; + CheckoutTree(tree, destPath, sourcePath); + return true; + } + return false; + } + else + { + var potentialRoot = Directory.GetParent(repoPath)?.FullName; + if (potentialRoot != null) + { + var scriptPath = Path.Combine(potentialRoot, sourcePath); + if (Directory.Exists(scriptPath)) + { + CopyDirectory(scriptPath, destPath); + return true; + } + else if (File.Exists(scriptPath)) + { + var dir = Path.GetDirectoryName(destPath); + if (!string.IsNullOrEmpty(dir) && !Directory.Exists(dir)) Directory.CreateDirectory(dir); + File.Copy(scriptPath, destPath, true); + return true; + } + } + return false; + } + } + private static (string firstFolder, string remainingPath) GetFirstFolderAndRemainingPath(string path) { // 检查路径是否为空或仅包含部分字符