Files
FlowPilot/sidepanel/update-service.js
T
2026-05-16 06:56:38 +08:00

460 lines
13 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
(() => {
const GITHUB_OWNER = 'QLHazyCoder';
const GITHUB_REPO = 'FlowPilot';
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_FLOWPILOT = 'flowpilot';
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 (/^flowpilot/i.test(trimmed)) {
return VERSION_FAMILY_FLOWPILOT;
}
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(/^(?: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_FLOWPILOT) {
return 'FlowPilot';
}
if (family === VERSION_FAMILY_ULTRA) {
return 'Ultra';
}
if (family === VERSION_FAMILY_PRO) {
return 'Pro';
}
return 'v';
}
function getVersionFamilyRank(family) {
if (family === VERSION_FAMILY_FLOWPILOT) {
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_FLOWPILOT, 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_FLOWPILOT) || 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_FLOWPILOT);
if (versionName) {
return versionName;
}
const versionCore = extractVersionCore(manifest?.version || '');
return versionCore ? formatDisplayVersion(`FlowPilot${versionCore}`, VERSION_FAMILY_FLOWPILOT) : '';
}
async function getReleaseSnapshot(options = {}) {
const localVersion = getLocalVersionLabel(chrome.runtime.getManifest()) || 'FlowPilot0.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,
};
})();