diff --git a/dashboard/src/components/shared/ExtensionCard.vue b/dashboard/src/components/shared/ExtensionCard.vue index 8335c47a53..afcc07e50f 100644 --- a/dashboard/src/components/shared/ExtensionCard.vue +++ b/dashboard/src/components/shared/ExtensionCard.vue @@ -268,6 +268,20 @@ const openWebui = () => { {{ extension.online_version }} + + + {{ + tm("card.status.localVersionAhead", { + version: extension.online_version, + }) + }} + { if (matchedPlugin) { extension.online_version = matchedPlugin.version; + const onlineVersion = String(matchedPlugin.version || "").trim(); + const versionComparison = comparePluginVersions( + extension.version, + onlineVersion, + ); + + extension.version_source_ahead = + typeof versionComparison === "number" && versionComparison > 0; extension.has_update = - extension.version !== matchedPlugin.version && - matchedPlugin.version !== tm("status.unknown"); + typeof versionComparison === "number" && versionComparison < 0; } else { extension.online_version = ""; extension.has_update = false; + extension.version_source_ahead = false; } }); }; @@ -1284,6 +1292,73 @@ export const useExtensionPage = () => { const normalizeAstrBotVersionSpec = (value) => String(value || "").trim(); + const parsePluginVersion = (value) => { + const version = String(value || "").trim().replace(/^v/i, ""); + const match = version.match( + /^([0-9]+(?:\.[0-9]+)*)(?:-([0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*))?(?:\+(.+))?$/, + ); + if (!match) return null; + + return { + parts: match[1].split(".").map((part) => Number.parseInt(part, 10)), + prerelease: match[2] + ? match[2].split(".").map((part) => { + if (/^\d+$/.test(part)) { + return Number.parseInt(part, 10); + } + return part; + }) + : null, + }; + }; + + const comparePluginVersions = (left, right) => { + const leftVersion = parsePluginVersion(left); + const rightVersion = parsePluginVersion(right); + if (!rightVersion) return null; + if (!leftVersion) return -1; + + const length = Math.max(leftVersion.parts.length, rightVersion.parts.length); + for (let i = 0; i < length; i += 1) { + const leftPart = leftVersion.parts[i] || 0; + const rightPart = rightVersion.parts[i] || 0; + if (leftPart > rightPart) return 1; + if (leftPart < rightPart) return -1; + } + + if (leftVersion.prerelease === null && rightVersion.prerelease !== null) { + return 1; + } + if (leftVersion.prerelease !== null && rightVersion.prerelease === null) { + return -1; + } + if (leftVersion.prerelease === null && rightVersion.prerelease === null) { + return 0; + } + + const prereleaseLength = Math.max( + leftVersion.prerelease.length, + rightVersion.prerelease.length, + ); + for (let i = 0; i < prereleaseLength; i += 1) { + const leftPart = leftVersion.prerelease[i]; + const rightPart = rightVersion.prerelease[i]; + + if (leftPart === undefined && rightPart !== undefined) return -1; + if (leftPart !== undefined && rightPart === undefined) return 1; + if (typeof leftPart === "number" && typeof rightPart === "string") { + return -1; + } + if (typeof leftPart === "string" && typeof rightPart === "number") { + return 1; + } + if (leftPart > rightPart) return 1; + if (leftPart < rightPart) return -1; + } + + return 0; + }; + const normalizeVersionParts = (value) => { const version = String(value || "") .trim()