Repository: AtmoOmen/DalamudPlugins Branch: master Commit: 639b99827591 Files: 5 Total size: 18.6 KB Directory structure: gitextract_bm_bag9t/ ├── .gitattributes ├── .github/ │ ├── plugin-release-rules.json │ └── workflows/ │ └── update-pluginmaster.yml ├── README.md └── scripts/ └── Update-PluginMaster.ps1 ================================================ FILE CONTENTS ================================================ ================================================ FILE: .gitattributes ================================================ # Auto detect text files and perform LF normalization * text=auto ================================================ FILE: .github/plugin-release-rules.json ================================================ { "plugins": [] } ================================================ FILE: .github/workflows/update-pluginmaster.yml ================================================ name: 自动更新 pluginmaster on: workflow_dispatch: schedule: - cron: '0 * * * *' release: types: - published permissions: contents: write concurrency: group: update-pluginmaster cancel-in-progress: true env: TARGET_BRANCH: main TARGET_WORKTREE: ../pluginmaster-main jobs: update-pluginmaster: runs-on: ubuntu-latest steps: - name: 检出仓库 uses: actions/checkout@v5 with: fetch-depth: 0 - name: 准备 main 分支工作区 shell: bash run: | rm -rf "${TARGET_WORKTREE}" rm -f "${RUNNER_TEMP}/pluginmaster-main-before.json" git fetch origin "${TARGET_BRANCH}" || true if git show "origin/${TARGET_BRANCH}:pluginmaster.json" > "${RUNNER_TEMP}/pluginmaster-main-before.json" 2>/dev/null; then echo "已读取 main 分支当前的 pluginmaster.json" else cp pluginmaster.json "${RUNNER_TEMP}/pluginmaster-main-before.json" echo "main 分支当前不存在 pluginmaster.json,先使用当前分支文件初始化" fi git worktree add --detach "${TARGET_WORKTREE}" git -C "${TARGET_WORKTREE}" checkout --orphan "${TARGET_BRANCH}" git -C "${TARGET_WORKTREE}" rm -rf --ignore-unmatch . git -C "${TARGET_WORKTREE}" clean -fdx cp "${RUNNER_TEMP}/pluginmaster-main-before.json" "${TARGET_WORKTREE}/pluginmaster.json" - name: 更新 pluginmaster.json shell: pwsh env: GITHUB_TOKEN: ${{ github.token }} run: ./scripts/Update-PluginMaster.ps1 -PluginMasterPath "${{ github.workspace }}/../pluginmaster-main/pluginmaster.json" - name: 提交 main 分支变更 shell: bash run: | if cmp -s "${TARGET_WORKTREE}/pluginmaster.json" "${RUNNER_TEMP}/pluginmaster-main-before.json"; then echo "pluginmaster.json 没有变化" exit 0 fi git -C "${TARGET_WORKTREE}" config user.name "github-actions[bot]" git -C "${TARGET_WORKTREE}" config user.email "41898282+github-actions[bot]@users.noreply.github.com" git -C "${TARGET_WORKTREE}" add pluginmaster.json git -C "${TARGET_WORKTREE}" commit -m "chore: 自动更新 pluginmaster 版本与下载链接" git -C "${TARGET_WORKTREE}" push --force origin "HEAD:${TARGET_BRANCH}" ================================================ FILE: README.md ================================================ # DalamudPlugins 由 **AtmoOmen** 个人维护的 Dalamud 插件仓库 Dalamud Plugin Repo Maintained by AtmoOmen ## 链接 / Link **加速链接 / Link For Chinese Mainland:** ``` https://gh.atmoomen.top/DalamudPlugins/main/pluginmaster.json ``` **原始链接 / Original Link:** ``` https://raw.githubusercontent.com/AtmoOmen/DalamudPlugins/main/pluginmaster.json ``` **Daily Routines Discord:** [![image](https://discordapp.com/api/guilds/1258981591124938762/embed.png?style=banner2)](https://discord.gg/dailyroutines) ## 仓库插件列表 | 名称 | 原作者 | 游戏版本 | 描述 | 备注 | |----------|----------|----------|----------|----------| | [Daily Routines](https://github.com/Dalamud-DailyRoutines) | [AtmoOmen](https://github.com/AtmoOmen) | 7.4 | 一些自动化小工具合集 | **个人编写** | | [Untarnished Heart](https://github.com/AtmoOmen/UntarnishedHeart) | [AtmoOmen](https://github.com/AtmoOmen) | 7.4 | 自动化 T 职副本场次成就刷取工具 | **个人编写** | | [vnavmesh](https://github.com/AtmoOmen/ffxiv_navmesh-cn) | [veyn](https://github.com/awgil) | 7.4 | 自动寻路 | 适配国服, 部分汉化 | | [GatherBuddyReborn](https://github.com/AtmoOmen/GatherBuddyReborn) | [Ottermandias](https://github.com/Ottermandias), [CombatReborn](https://github.com/FFXIV-CombatReborn) | 7.4 | Gather Buddy 自动采集版 | 适配国服, 部分汉化 | | [Currency Tracker](https://github.com/AtmoOmen/CurrencyTracker) | [AtmoOmen](https://github.com/AtmoOmen) | 7.4 | 记录你各种的货币收支情况 | **个人编写** | | [VFXEditorCN](https://github.com/AtmoOmen/VFXEditor-CN) | [ocealot](https://github.com/0ceal0t) | 7.4 | 游戏内视觉效果、动画与音频编辑插件 | 适配国服, 基本完全汉化 | | [Character Data Sync X](https://github.com/AtmoOmen/Dalamud.CharacterSync) | [goat](https://github.com/goaaats), [AtmoOmen](https://github.com/AtmoOmen) | 7.4 | 将你的主角色数据安全地同步到其他角色上 | 基于主库完全重写, 完全汉化 | | [HaselDebug](https://github.com/AtmoOmen/HaselDebug) | [Haselnussbomber](https://github.com/Haselnussbomber) | 7.4 | 多功能开发用工具箱 | 适配国服 | | [A Realm Recorded Lite](https://github.com/AtmoOmen/ARealmRecordedLite) | [UnknownX](https://github.com/UnknownX7), [AtmoOmen](https://github.com/AtmoOmen) | 7.4 | A Realm Recorded 精简版 | 适配国服, 完全汉化, 完全重写 | [Raphael.Dalamud](https://github.com/Dalamud-DailyRoutines/Raphael.Dalamud) | - | 7.4 | 生产求解器 Raphael 的 Dalamud 包装 | - | [DCTravelerX](https://github.com/Dalamud-DailyRoutines/DCTraveler) | [Loskh](https://github.com/Loskh) | 7.4 | 国服游戏内跨大区插件 | 代码清理, 兼容 DR ================================================ FILE: scripts/Update-PluginMaster.ps1 ================================================ param( [string]$PluginMasterPath = (Join-Path $PSScriptRoot "..\pluginmaster.json"), [string]$ConfigPath = (Join-Path $PSScriptRoot "..\.github\plugin-release-rules.json"), [string]$GitHubApiBase = "https://api.github.com" ) Set-StrictMode -Version Latest $ErrorActionPreference = "Stop" $script:GitHubHeaders = @{ Accept = "application/vnd.github+json" "User-Agent" = "DalamudPlugins-PluginMaster-Updater" } if (-not [string]::IsNullOrWhiteSpace($env:GITHUB_TOKEN)) { $script:GitHubHeaders.Authorization = "Bearer $($env:GITHUB_TOKEN)" } $script:ReleaseCache = @{} function Get-ReadablePath { param( [Parameter(Mandatory)] [string]$Path, [Parameter(Mandatory)] [string]$Description ) if ([string]::IsNullOrWhiteSpace($Path)) { throw "$Description 路径不能为空" } try { return [string](Resolve-Path -LiteralPath $Path) } catch { throw "$Description 不存在: $Path" } } function Get-ObjectPropertyValue { param( [Parameter(Mandatory)] [AllowNull()] [object]$InputObject, [Parameter(Mandatory)] [string]$PropertyName ) if ($null -eq $InputObject) { return $null } if ($InputObject -is [System.Collections.IDictionary]) { if ($InputObject.Contains($PropertyName)) { return $InputObject[$PropertyName] } return $null } $property = $InputObject.PSObject.Properties[$PropertyName] if ($null -eq $property) { return $null } return $property.Value } function Read-JsonFile { param( [Parameter(Mandatory)] [string]$Path, [Parameter(Mandatory)] [string]$Description ) $resolvedPath = Get-ReadablePath -Path $Path -Description $Description $rawContent = Get-Content -Raw -Encoding utf8 -LiteralPath $resolvedPath if ([string]::IsNullOrWhiteSpace($rawContent)) { throw "$Description 内容为空: $resolvedPath" } try { return @{ Path = $resolvedPath RawContent = $rawContent Json = $rawContent | ConvertFrom-Json } } catch { throw "$Description 不是合法的 JSON: $resolvedPath。$($_.Exception.Message)" } } function Normalize-Version { param( [Parameter(Mandatory)] [string]$VersionText ) $segments = $VersionText.Split(".", [StringSplitOptions]::RemoveEmptyEntries) if ($segments.Count -lt 3 -or $segments.Count -gt 4) { throw "不支持的版本号格式: $VersionText" } while ($segments.Count -lt 4) { $segments += "0" } return [string]::Join(".", $segments) } function ConvertTo-VersionObject { param( [Parameter(Mandatory)] [string]$VersionText ) return [Version](Normalize-Version -VersionText $VersionText) } function Get-RepoSlugFromUrl { param( [Parameter(Mandatory)] [string]$RepoUrl ) $match = [regex]::Match($RepoUrl, "^https://github\.com/(?[^/]+)/(?[^/?#]+?)(?:\.git)?/?$") if (-not $match.Success) { throw "无法从 RepoUrl 解析 GitHub 仓库: $RepoUrl" } return "$($match.Groups["owner"].Value)/$($match.Groups["repo"].Value)" } function Get-RuleLookup { param( [AllowNull()] [array]$Rules ) $lookup = @{} if ($null -eq $Rules) { return $lookup } for ($index = 0; $index -lt $Rules.Count; $index++) { $rule = $Rules[$index] if ($null -eq $rule) { Write-Warning "已跳过空规则,索引为 $index" continue } $internalName = [string](Get-ObjectPropertyValue -InputObject $rule -PropertyName "internalName") if ([string]::IsNullOrWhiteSpace($internalName)) { Write-Warning "已跳过缺少 internalName 的规则,索引为 $index" continue } if ($lookup.ContainsKey($internalName)) { Write-Warning "已跳过重复规则 internalName: $internalName" continue } $lookup[$internalName] = $rule } return $lookup } function Get-Releases { param( [Parameter(Mandatory)] [string]$RepoSlug ) if ($script:ReleaseCache.ContainsKey($RepoSlug)) { return $script:ReleaseCache[$RepoSlug] } $uri = "$GitHubApiBase/repos/$RepoSlug/releases?per_page=100" Write-Host "正在读取仓库 $RepoSlug 的 release 列表..." $releases = Invoke-RestMethod -Method Get -Headers $script:GitHubHeaders -Uri $uri $script:ReleaseCache[$RepoSlug] = @($releases) return $script:ReleaseCache[$RepoSlug] } function Get-LatestReleaseInfo { param( [Parameter(Mandatory)] [string]$RepoSlug, [string]$TagPrefix ) if ([string]::IsNullOrWhiteSpace($TagPrefix)) { $normalizedPrefix = $null } else { $normalizedPrefix = $TagPrefix.Trim() } $tagPattern = if ($null -eq $normalizedPrefix) { "^(?\d+\.\d+\.\d+(?:\.\d+)?)$" } else { "^{0}-(?\d+\.\d+\.\d+(?:\.\d+)?)$" -f [regex]::Escape($normalizedPrefix) } $releases = Get-Releases -RepoSlug $RepoSlug $orderedReleases = $releases | Where-Object { -not $_.draft -and -not $_.prerelease } | Sort-Object { if ($_.published_at) { [DateTimeOffset]$_.published_at } elseif ($_.created_at) { [DateTimeOffset]$_.created_at } else { [DateTimeOffset]::MinValue } } -Descending foreach ($release in $orderedReleases) { $match = [regex]::Match([string]$release.tag_name, $tagPattern) if (-not $match.Success) { continue } return @{ TagName = [string]$release.tag_name AssemblyVersion = Normalize-Version -VersionText $match.Groups["version"].Value PublishedAt = [string]$release.published_at } } if ($null -eq $normalizedPrefix) { $modeDescription = "纯数字 Tag" } else { $modeDescription = "前缀为 $normalizedPrefix 的 Tag" } throw "仓库 $RepoSlug 中未找到符合条件的最新正式版 release: $modeDescription" } function Get-SelectedPluginIndexes { param( [Parameter(Mandatory)] [AllowEmptyCollection()] [array]$Plugins ) $selected = @{} for ($index = 0; $index -lt $Plugins.Count; $index++) { $plugin = $Plugins[$index] if ($null -eq $plugin) { Write-Warning "已跳过空插件对象,索引为 $index" continue } $internalName = [string](Get-ObjectPropertyValue -InputObject $plugin -PropertyName "InternalName") if ([string]::IsNullOrWhiteSpace($internalName)) { Write-Warning "已跳过缺少 InternalName 的插件,索引为 $index" continue } $assemblyVersion = [string](Get-ObjectPropertyValue -InputObject $plugin -PropertyName "AssemblyVersion") if ([string]::IsNullOrWhiteSpace($assemblyVersion)) { Write-Warning "已跳过缺少 AssemblyVersion 的插件 $internalName" continue } try { $versionObject = ConvertTo-VersionObject -VersionText $assemblyVersion } catch { Write-Warning "已跳过版本号无效的插件 ${internalName}: $assemblyVersion" continue } if (-not $selected.ContainsKey($internalName) -or $versionObject -gt $selected[$internalName].Version) { $selected[$internalName] = @{ Index = $index Version = $versionObject } } } return $selected } function Get-DownloadUrl { param( [Parameter(Mandatory)] [string]$RepoSlug, [Parameter(Mandatory)] [string]$TagName ) return "https://github.com/$RepoSlug/releases/download/$TagName/latest.zip" } function Update-PluginBlock { param( [Parameter(Mandatory)] [string]$Content, [Parameter(Mandatory)] [string]$InternalName, [Parameter(Mandatory)] [string]$CurrentAssemblyVersion, [Parameter(Mandatory)] [string]$NewAssemblyVersion, [Parameter(Mandatory)] [string]$DownloadUrl ) $blockPattern = '(?s)\{[^{}]*"InternalName"\s*:\s*"' + [regex]::Escape($InternalName) + '"[^{}]*"AssemblyVersion"\s*:\s*"' + [regex]::Escape($CurrentAssemblyVersion) + '"[^{}]*\}' $blockMatch = [regex]::Match($Content, $blockPattern) if (-not $blockMatch.Success) { throw "未找到需要回写的插件对象: InternalName=$InternalName, AssemblyVersion=$CurrentAssemblyVersion" } $updatedBlock = $blockMatch.Value $assemblyVersionPattern = New-Object System.Text.RegularExpressions.Regex '("AssemblyVersion"\s*:\s*")[^"]+(")' $downloadInstallPattern = New-Object System.Text.RegularExpressions.Regex '("DownloadLinkInstall"\s*:\s*")[^"]+(")' $downloadUpdatePattern = New-Object System.Text.RegularExpressions.Regex '("DownloadLinkUpdate"\s*:\s*")[^"]+(")' $updatedBlock = $assemblyVersionPattern.Replace( $updatedBlock, [System.Text.RegularExpressions.MatchEvaluator]{ param($match) return $match.Groups[1].Value + $NewAssemblyVersion + $match.Groups[2].Value }, 1 ) $updatedBlock = $downloadInstallPattern.Replace( $updatedBlock, [System.Text.RegularExpressions.MatchEvaluator]{ param($match) return $match.Groups[1].Value + $DownloadUrl + $match.Groups[2].Value }, 1 ) $updatedBlock = $downloadUpdatePattern.Replace( $updatedBlock, [System.Text.RegularExpressions.MatchEvaluator]{ param($match) return $match.Groups[1].Value + $DownloadUrl + $match.Groups[2].Value }, 1 ) return $Content.Substring(0, $blockMatch.Index) + $updatedBlock + $Content.Substring($blockMatch.Index + $blockMatch.Length) } $pluginMasterFile = Read-JsonFile -Path $PluginMasterPath -Description "pluginmaster.json" $pluginMasterPath = $pluginMasterFile.Path $pluginMasterContent = $pluginMasterFile.RawContent $pluginMaster = @($pluginMasterFile.Json) $configFile = Read-JsonFile -Path $ConfigPath -Description "规则配置" $config = $configFile.Json $configPlugins = @(Get-ObjectPropertyValue -InputObject $config -PropertyName "plugins") $ruleLookup = Get-RuleLookup -Rules $configPlugins $selectedIndexes = Get-SelectedPluginIndexes -Plugins $pluginMaster $updatedCount = 0 $skippedCount = 0 $selectedItems = $selectedIndexes.GetEnumerator() | Sort-Object Key $pendingUpdates = @() foreach ($item in $selectedItems) { try { $pluginIndex = [int]$item.Value.Index $plugin = $pluginMaster[$pluginIndex] $internalName = [string](Get-ObjectPropertyValue -InputObject $plugin -PropertyName "InternalName") $rule = $null if ($ruleLookup.ContainsKey($internalName)) { $rule = $ruleLookup[$internalName] } $repoUrl = [string](Get-ObjectPropertyValue -InputObject $plugin -PropertyName "RepoUrl") $repoSlug = Get-RepoSlugFromUrl -RepoUrl $repoUrl $tagPrefix = $null if ($null -ne $rule) { $sourceRepo = [string](Get-ObjectPropertyValue -InputObject $rule -PropertyName "sourceRepo") if (-not [string]::IsNullOrWhiteSpace($sourceRepo)) { $repoSlug = $sourceRepo.Trim() } $configuredTagPrefix = [string](Get-ObjectPropertyValue -InputObject $rule -PropertyName "tagPrefix") if (-not [string]::IsNullOrWhiteSpace($configuredTagPrefix)) { $tagPrefix = $configuredTagPrefix.Trim() } } $latestRelease = Get-LatestReleaseInfo -RepoSlug $repoSlug -TagPrefix $tagPrefix $downloadUrl = Get-DownloadUrl -RepoSlug $repoSlug -TagName $latestRelease.TagName $beforeVersion = [string](Get-ObjectPropertyValue -InputObject $plugin -PropertyName "AssemblyVersion") $beforeInstall = [string](Get-ObjectPropertyValue -InputObject $plugin -PropertyName "DownloadLinkInstall") $beforeUpdate = [string](Get-ObjectPropertyValue -InputObject $plugin -PropertyName "DownloadLinkUpdate") $plugin.AssemblyVersion = $latestRelease.AssemblyVersion $plugin.DownloadLinkInstall = $downloadUrl $plugin.DownloadLinkUpdate = $downloadUrl $hasChanged = $false if ($beforeVersion -ne [string]$plugin.AssemblyVersion) { $hasChanged = $true } if ($beforeInstall -ne [string]$plugin.DownloadLinkInstall) { $hasChanged = $true } if ($beforeUpdate -ne [string]$plugin.DownloadLinkUpdate) { $hasChanged = $true } if ($hasChanged) { $updatedCount++ $pendingUpdates += [pscustomobject]@{ InternalName = $internalName CurrentAssemblyVersion = $beforeVersion NewAssemblyVersion = [string]$plugin.AssemblyVersion DownloadUrl = $downloadUrl } Write-Host "已更新 $internalName -> 版本 $($plugin.AssemblyVersion),Tag 为 $($latestRelease.TagName)" } else { Write-Host "$internalName 已是最新状态" } } catch { $skippedCount++ Write-Warning "已跳过插件 $($item.Key): $($_.Exception.Message)" } } if ($updatedCount -eq 0) { if ($skippedCount -gt 0) { Write-Warning "本次有 $skippedCount 个插件被跳过" } Write-Host "pluginmaster.json 无需更新" exit 0 } $updatedContent = $pluginMasterContent foreach ($pendingUpdate in $pendingUpdates) { $updatedContent = Update-PluginBlock ` -Content $updatedContent ` -InternalName $pendingUpdate.InternalName ` -CurrentAssemblyVersion $pendingUpdate.CurrentAssemblyVersion ` -NewAssemblyVersion $pendingUpdate.NewAssemblyVersion ` -DownloadUrl $pendingUpdate.DownloadUrl } $utf8NoBom = New-Object System.Text.UTF8Encoding($false) [System.IO.File]::WriteAllText((Resolve-Path $PluginMasterPath), $updatedContent, $utf8NoBom) if ($skippedCount -gt 0) { Write-Warning "本次有 $skippedCount 个插件被跳过" } Write-Host "pluginmaster.json 已写回,共更新 $updatedCount 个插件条目"