import GuJumpgate snapshot from GitHub
This commit is contained in:
@@ -0,0 +1,459 @@
|
||||
(() => {
|
||||
const GITHUB_OWNER = 'FoundZiGu';
|
||||
const GITHUB_REPO = 'GuJumpgate';
|
||||
const RELEASES_PAGE_URL = `https://github.com/${GITHUB_OWNER}/${GITHUB_REPO}/releases`;
|
||||
const RELEASES_API_URL = `https://api.github.com/repos/${GITHUB_OWNER}/${GITHUB_REPO}/releases?per_page=10`;
|
||||
const CACHE_KEY = 'multipage-release-snapshot-v1';
|
||||
const IGNORED_UPDATE_VERSION_KEY = 'multipage-ignored-release-version-v1';
|
||||
const CACHE_TTL_MS = 60 * 60 * 1000;
|
||||
const FETCH_TIMEOUT_MS = 8000;
|
||||
const MAX_RELEASES = 10;
|
||||
const MAX_NOTES_PER_RELEASE = 5;
|
||||
const VERSION_FAMILY_GUJUMPGATE = 'gujumpgate';
|
||||
const VERSION_FAMILY_ULTRA = 'ultra';
|
||||
const VERSION_FAMILY_PRO = 'pro';
|
||||
const VERSION_FAMILY_LEGACY = 'legacy';
|
||||
|
||||
function getVersionFamily(version, fallbackFamily = VERSION_FAMILY_LEGACY) {
|
||||
const trimmed = String(version || '').trim();
|
||||
if (/^(?:gujumpgate|flowpilot)/i.test(trimmed)) {
|
||||
return VERSION_FAMILY_GUJUMPGATE;
|
||||
}
|
||||
if (/^ultra/i.test(trimmed)) {
|
||||
return VERSION_FAMILY_ULTRA;
|
||||
}
|
||||
if (/^pro/i.test(trimmed)) {
|
||||
return VERSION_FAMILY_PRO;
|
||||
}
|
||||
if (/^v/i.test(trimmed)) {
|
||||
return VERSION_FAMILY_LEGACY;
|
||||
}
|
||||
return fallbackFamily;
|
||||
}
|
||||
|
||||
function stripVersionPrefix(version) {
|
||||
return String(version || '').trim().replace(/^(?:gujumpgate|flowpilot|ultra|pro|v)\s*/i, '');
|
||||
}
|
||||
|
||||
function extractVersionCore(version) {
|
||||
const core = stripVersionPrefix(version).split('-')[0];
|
||||
return /^\d+(?:\.\d+){1,3}$/.test(core) ? core : '';
|
||||
}
|
||||
|
||||
function formatDisplayVersion(version, fallbackFamily = VERSION_FAMILY_LEGACY) {
|
||||
const core = extractVersionCore(version);
|
||||
if (!core) {
|
||||
return '';
|
||||
}
|
||||
|
||||
const family = getVersionFamily(version, fallbackFamily);
|
||||
return `${getVersionFamilyPrefix(family)}${core}`;
|
||||
}
|
||||
|
||||
function parseVersionMeta(version, fallbackFamily = VERSION_FAMILY_LEGACY) {
|
||||
const family = getVersionFamily(version, fallbackFamily);
|
||||
const core = extractVersionCore(version);
|
||||
return {
|
||||
family,
|
||||
core,
|
||||
displayVersion: core ? `${getVersionFamilyPrefix(family)}${core}` : '',
|
||||
parts: core
|
||||
? core.split('.').map((part) => {
|
||||
const numeric = Number.parseInt(part, 10);
|
||||
return Number.isFinite(numeric) ? numeric : 0;
|
||||
})
|
||||
: [0],
|
||||
};
|
||||
}
|
||||
|
||||
function getVersionFamilyPrefix(family) {
|
||||
if (family === VERSION_FAMILY_GUJUMPGATE) {
|
||||
return 'GuJumpgate';
|
||||
}
|
||||
if (family === VERSION_FAMILY_ULTRA) {
|
||||
return 'Ultra';
|
||||
}
|
||||
if (family === VERSION_FAMILY_PRO) {
|
||||
return 'Pro';
|
||||
}
|
||||
return 'v';
|
||||
}
|
||||
|
||||
function getVersionFamilyRank(family) {
|
||||
if (family === VERSION_FAMILY_GUJUMPGATE) {
|
||||
return 4;
|
||||
}
|
||||
if (family === VERSION_FAMILY_ULTRA) {
|
||||
return 3;
|
||||
}
|
||||
if (family === VERSION_FAMILY_PRO) {
|
||||
return 2;
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
function parseVersionParts(version, fallbackFamily = VERSION_FAMILY_LEGACY) {
|
||||
return parseVersionMeta(version, fallbackFamily).parts;
|
||||
}
|
||||
|
||||
function compareVersions(left, right, fallbackFamily = VERSION_FAMILY_LEGACY) {
|
||||
const leftMeta = parseVersionMeta(left, fallbackFamily);
|
||||
const rightMeta = parseVersionMeta(right, fallbackFamily);
|
||||
const leftFamilyRank = getVersionFamilyRank(leftMeta.family);
|
||||
const rightFamilyRank = getVersionFamilyRank(rightMeta.family);
|
||||
|
||||
if (leftFamilyRank > rightFamilyRank) {
|
||||
return 1;
|
||||
}
|
||||
if (leftFamilyRank < rightFamilyRank) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
const leftParts = leftMeta.parts;
|
||||
const rightParts = rightMeta.parts;
|
||||
const maxLength = Math.max(leftParts.length, rightParts.length, 3);
|
||||
|
||||
for (let index = 0; index < maxLength; index += 1) {
|
||||
const leftPart = leftParts[index] || 0;
|
||||
const rightPart = rightParts[index] || 0;
|
||||
if (leftPart > rightPart) {
|
||||
return 1;
|
||||
}
|
||||
if (leftPart < rightPart) {
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
function sanitizeInlineMarkdown(text) {
|
||||
return String(text || '')
|
||||
.replace(/!\[[^\]]*]\(([^)]+)\)/g, '')
|
||||
.replace(/\[([^\]]+)]\(([^)]+)\)/g, '$1')
|
||||
.replace(/`([^`]+)`/g, '$1')
|
||||
.replace(/[*_~>#]/g, '')
|
||||
.replace(/\s+/g, ' ')
|
||||
.trim();
|
||||
}
|
||||
|
||||
function parseReleaseNotes(body) {
|
||||
const lines = String(body || '')
|
||||
.replace(/\r\n/g, '\n')
|
||||
.split('\n')
|
||||
.map((line) => line.trim())
|
||||
.filter(Boolean);
|
||||
|
||||
const bulletLines = [];
|
||||
const plainLines = [];
|
||||
|
||||
for (const line of lines) {
|
||||
if (/^#{1,6}\s*/.test(line)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (/^```/.test(line) || /^---+$/.test(line)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (/^[-*+]\s+/.test(line)) {
|
||||
bulletLines.push(line.replace(/^[-*+]\s+/, ''));
|
||||
continue;
|
||||
}
|
||||
|
||||
if (/^\d+\.\s+/.test(line)) {
|
||||
bulletLines.push(line.replace(/^\d+\.\s+/, ''));
|
||||
continue;
|
||||
}
|
||||
|
||||
plainLines.push(line);
|
||||
}
|
||||
|
||||
const noteLines = bulletLines.length > 0 ? bulletLines : plainLines;
|
||||
return noteLines
|
||||
.map(sanitizeInlineMarkdown)
|
||||
.filter(Boolean)
|
||||
.slice(0, MAX_NOTES_PER_RELEASE);
|
||||
}
|
||||
|
||||
function normalizeReleaseVersion(release) {
|
||||
const candidates = [
|
||||
release?.tag_name,
|
||||
release?.name,
|
||||
];
|
||||
|
||||
for (const candidate of candidates) {
|
||||
const meta = parseVersionMeta(candidate);
|
||||
if (meta.core) {
|
||||
return meta;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function sanitizeRelease(release) {
|
||||
const versionMeta = normalizeReleaseVersion(release);
|
||||
if (!versionMeta?.core) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const rawTitle = sanitizeInlineMarkdown(release?.name || '');
|
||||
const titleMeta = parseVersionMeta(rawTitle, versionMeta.family);
|
||||
const normalizedTitle = titleMeta.core && titleMeta.displayVersion === versionMeta.displayVersion
|
||||
? ''
|
||||
: rawTitle;
|
||||
|
||||
return {
|
||||
version: versionMeta.core,
|
||||
displayVersion: versionMeta.displayVersion,
|
||||
family: versionMeta.family,
|
||||
title: normalizedTitle,
|
||||
url: String(release?.html_url || RELEASES_PAGE_URL),
|
||||
publishedAt: String(release?.published_at || release?.created_at || ''),
|
||||
notes: parseReleaseNotes(release?.body || ''),
|
||||
};
|
||||
}
|
||||
|
||||
function getComparableReleaseVersion(release) {
|
||||
const fallbackFamily = [VERSION_FAMILY_GUJUMPGATE, VERSION_FAMILY_ULTRA, VERSION_FAMILY_PRO].includes(release?.family)
|
||||
? release.family
|
||||
: VERSION_FAMILY_LEGACY;
|
||||
const displayVersion = String(release?.displayVersion || '').trim();
|
||||
if (displayVersion) {
|
||||
return displayVersion;
|
||||
}
|
||||
return formatDisplayVersion(release?.version || '', fallbackFamily);
|
||||
}
|
||||
|
||||
function sortReleasesByVersion(releases = []) {
|
||||
return [...releases].sort((left, right) => (
|
||||
compareVersions(
|
||||
getComparableReleaseVersion(right),
|
||||
getComparableReleaseVersion(left)
|
||||
)
|
||||
));
|
||||
}
|
||||
|
||||
function readCache() {
|
||||
try {
|
||||
const raw = localStorage.getItem(CACHE_KEY);
|
||||
if (!raw) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const parsed = JSON.parse(raw);
|
||||
if (!parsed || !Array.isArray(parsed.releases) || !Number.isFinite(parsed.fetchedAt)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if ((Date.now() - parsed.fetchedAt) > CACHE_TTL_MS) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return sortReleasesByVersion(parsed.releases).slice(0, MAX_RELEASES);
|
||||
} catch (error) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function writeCache(releases) {
|
||||
try {
|
||||
localStorage.setItem(CACHE_KEY, JSON.stringify({
|
||||
fetchedAt: Date.now(),
|
||||
releases,
|
||||
}));
|
||||
} catch (error) {
|
||||
// Ignore cache write failures.
|
||||
}
|
||||
}
|
||||
|
||||
function getIgnoredUpdateVersion() {
|
||||
try {
|
||||
return String(localStorage.getItem(IGNORED_UPDATE_VERSION_KEY) || '').trim();
|
||||
} catch (error) {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
function setIgnoredUpdateVersion(version) {
|
||||
const normalized = formatDisplayVersion(version, VERSION_FAMILY_GUJUMPGATE) || String(version || '').trim();
|
||||
try {
|
||||
if (normalized) {
|
||||
localStorage.setItem(IGNORED_UPDATE_VERSION_KEY, normalized);
|
||||
} else {
|
||||
localStorage.removeItem(IGNORED_UPDATE_VERSION_KEY);
|
||||
}
|
||||
} catch (error) {
|
||||
// Ignore localStorage failures.
|
||||
}
|
||||
return normalized;
|
||||
}
|
||||
|
||||
function clearIgnoredUpdateVersion() {
|
||||
return setIgnoredUpdateVersion('');
|
||||
}
|
||||
|
||||
async function fetchReleases() {
|
||||
const controller = new AbortController();
|
||||
const timeoutId = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);
|
||||
|
||||
try {
|
||||
const response = await fetch(RELEASES_API_URL, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
Accept: 'application/vnd.github+json',
|
||||
},
|
||||
cache: 'no-store',
|
||||
signal: controller.signal,
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`GitHub Releases 请求失败(${response.status})`);
|
||||
}
|
||||
|
||||
const payload = await response.json();
|
||||
if (!Array.isArray(payload)) {
|
||||
throw new Error('GitHub Releases 返回格式异常');
|
||||
}
|
||||
|
||||
const releases = payload
|
||||
.filter((release) => release && !release.draft && !release.prerelease)
|
||||
.map(sanitizeRelease)
|
||||
.filter(Boolean);
|
||||
|
||||
const sortedReleases = sortReleasesByVersion(releases).slice(0, MAX_RELEASES);
|
||||
|
||||
writeCache(sortedReleases);
|
||||
return sortedReleases;
|
||||
} catch (error) {
|
||||
if (error?.name === 'AbortError') {
|
||||
throw new Error('GitHub Releases 请求超时');
|
||||
}
|
||||
throw error;
|
||||
} finally {
|
||||
clearTimeout(timeoutId);
|
||||
}
|
||||
}
|
||||
|
||||
async function loadReleases(options = {}) {
|
||||
if (!options.force) {
|
||||
const cached = readCache();
|
||||
if (cached) {
|
||||
return cached;
|
||||
}
|
||||
}
|
||||
|
||||
return fetchReleases();
|
||||
}
|
||||
|
||||
function buildReleaseSnapshot(releases, localVersion) {
|
||||
const latestRelease = releases[0] || null;
|
||||
if (!latestRelease) {
|
||||
return {
|
||||
status: 'empty',
|
||||
localVersion,
|
||||
latestVersion: null,
|
||||
latestRelease: null,
|
||||
newerReleases: [],
|
||||
logUrl: RELEASES_PAGE_URL,
|
||||
releasesPageUrl: RELEASES_PAGE_URL,
|
||||
checkedAt: Date.now(),
|
||||
};
|
||||
}
|
||||
|
||||
const newerReleases = releases.filter((release) => compareVersions(release.displayVersion, localVersion) > 0);
|
||||
const ignoredVersion = getIgnoredUpdateVersion();
|
||||
const newestUpdateVersion = newerReleases[0]?.displayVersion || '';
|
||||
const updateIgnored = Boolean(newestUpdateVersion)
|
||||
&& Boolean(ignoredVersion)
|
||||
&& compareVersions(ignoredVersion, newestUpdateVersion) === 0;
|
||||
return {
|
||||
status: newerReleases.length > 0
|
||||
? (updateIgnored ? 'ignored' : 'update-available')
|
||||
: 'latest',
|
||||
localVersion,
|
||||
latestVersion: latestRelease.displayVersion,
|
||||
latestRelease,
|
||||
newerReleases,
|
||||
ignoredVersion: updateIgnored ? ignoredVersion : '',
|
||||
logUrl: latestRelease.url || RELEASES_PAGE_URL,
|
||||
releasesPageUrl: RELEASES_PAGE_URL,
|
||||
checkedAt: Date.now(),
|
||||
};
|
||||
}
|
||||
|
||||
function getSnapshotUpdateVersion(snapshot = {}) {
|
||||
const newerReleases = Array.isArray(snapshot?.newerReleases) ? snapshot.newerReleases : [];
|
||||
return newerReleases[0]?.displayVersion || snapshot?.latestVersion || '';
|
||||
}
|
||||
|
||||
function ignoreReleaseSnapshot(snapshot = {}) {
|
||||
const targetVersion = getSnapshotUpdateVersion(snapshot);
|
||||
if (!targetVersion) {
|
||||
return '';
|
||||
}
|
||||
return setIgnoredUpdateVersion(targetVersion);
|
||||
}
|
||||
|
||||
function getLocalVersionLabel(manifest = chrome.runtime.getManifest()) {
|
||||
const versionName = formatDisplayVersion(manifest?.version_name, VERSION_FAMILY_GUJUMPGATE);
|
||||
if (versionName) {
|
||||
return versionName;
|
||||
}
|
||||
|
||||
const versionCore = extractVersionCore(manifest?.version || '');
|
||||
return versionCore ? formatDisplayVersion(`GuJumpgate${versionCore}`, VERSION_FAMILY_GUJUMPGATE) : '';
|
||||
}
|
||||
|
||||
async function getReleaseSnapshot(options = {}) {
|
||||
const localVersion = getLocalVersionLabel(chrome.runtime.getManifest()) || 'GuJumpgate0.0';
|
||||
|
||||
try {
|
||||
const releases = await loadReleases(options);
|
||||
return buildReleaseSnapshot(releases, localVersion);
|
||||
} catch (error) {
|
||||
return {
|
||||
status: 'error',
|
||||
localVersion,
|
||||
latestVersion: null,
|
||||
latestRelease: null,
|
||||
newerReleases: [],
|
||||
logUrl: RELEASES_PAGE_URL,
|
||||
releasesPageUrl: RELEASES_PAGE_URL,
|
||||
checkedAt: Date.now(),
|
||||
errorMessage: error?.message || '更新检查失败',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function formatReleaseDate(value) {
|
||||
if (!value) {
|
||||
return '';
|
||||
}
|
||||
|
||||
const date = new Date(value);
|
||||
if (Number.isNaN(date.getTime())) {
|
||||
return '';
|
||||
}
|
||||
|
||||
const year = date.getFullYear();
|
||||
const month = String(date.getMonth() + 1).padStart(2, '0');
|
||||
const day = String(date.getDate()).padStart(2, '0');
|
||||
return `${year}-${month}-${day}`;
|
||||
}
|
||||
|
||||
window.SidepanelUpdateService = {
|
||||
compareVersions,
|
||||
formatDisplayVersion,
|
||||
formatReleaseDate,
|
||||
clearIgnoredUpdateVersion,
|
||||
getIgnoredUpdateVersion,
|
||||
getLocalVersionLabel,
|
||||
getReleaseSnapshot,
|
||||
ignoreReleaseSnapshot,
|
||||
repositoryUrl: `https://github.com/${GITHUB_OWNER}/${GITHUB_REPO}`,
|
||||
releasesPageUrl: RELEASES_PAGE_URL,
|
||||
stripVersionPrefix,
|
||||
};
|
||||
})();
|
||||
Reference in New Issue
Block a user