update-service.js 7.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288
  1. (() => {
  2. const GITHUB_OWNER = 'QLHazyCoder';
  3. const GITHUB_REPO = 'codex-oauth-automation-extension';
  4. const RELEASES_PAGE_URL = `https://github.com/${GITHUB_OWNER}/${GITHUB_REPO}/releases`;
  5. const RELEASES_API_URL = `https://api.github.com/repos/${GITHUB_OWNER}/${GITHUB_REPO}/releases?per_page=10`;
  6. const CACHE_KEY = 'multipage-release-snapshot-v1';
  7. const CACHE_TTL_MS = 60 * 60 * 1000;
  8. const FETCH_TIMEOUT_MS = 8000;
  9. const MAX_RELEASES = 10;
  10. const MAX_NOTES_PER_RELEASE = 5;
  11. function stripVersionPrefix(version) {
  12. return String(version || '').trim().replace(/^v/i, '');
  13. }
  14. function parseVersionParts(version) {
  15. const core = stripVersionPrefix(version).split('-')[0];
  16. if (!core) {
  17. return [0];
  18. }
  19. return core.split('.').map((part) => {
  20. const numeric = Number.parseInt(part, 10);
  21. return Number.isFinite(numeric) ? numeric : 0;
  22. });
  23. }
  24. function compareVersions(left, right) {
  25. const leftParts = parseVersionParts(left);
  26. const rightParts = parseVersionParts(right);
  27. const maxLength = Math.max(leftParts.length, rightParts.length, 3);
  28. for (let index = 0; index < maxLength; index += 1) {
  29. const leftPart = leftParts[index] || 0;
  30. const rightPart = rightParts[index] || 0;
  31. if (leftPart > rightPart) {
  32. return 1;
  33. }
  34. if (leftPart < rightPart) {
  35. return -1;
  36. }
  37. }
  38. return 0;
  39. }
  40. function sanitizeInlineMarkdown(text) {
  41. return String(text || '')
  42. .replace(/!\[[^\]]*]\(([^)]+)\)/g, '')
  43. .replace(/\[([^\]]+)]\(([^)]+)\)/g, '$1')
  44. .replace(/`([^`]+)`/g, '$1')
  45. .replace(/[*_~>#]/g, '')
  46. .replace(/\s+/g, ' ')
  47. .trim();
  48. }
  49. function parseReleaseNotes(body) {
  50. const lines = String(body || '')
  51. .replace(/\r\n/g, '\n')
  52. .split('\n')
  53. .map((line) => line.trim())
  54. .filter(Boolean);
  55. const bulletLines = [];
  56. const plainLines = [];
  57. for (const line of lines) {
  58. if (/^#{1,6}\s*/.test(line)) {
  59. continue;
  60. }
  61. if (/^```/.test(line) || /^---+$/.test(line)) {
  62. continue;
  63. }
  64. if (/^[-*+]\s+/.test(line)) {
  65. bulletLines.push(line.replace(/^[-*+]\s+/, ''));
  66. continue;
  67. }
  68. if (/^\d+\.\s+/.test(line)) {
  69. bulletLines.push(line.replace(/^\d+\.\s+/, ''));
  70. continue;
  71. }
  72. plainLines.push(line);
  73. }
  74. const noteLines = bulletLines.length > 0 ? bulletLines : plainLines;
  75. return noteLines
  76. .map(sanitizeInlineMarkdown)
  77. .filter(Boolean)
  78. .slice(0, MAX_NOTES_PER_RELEASE);
  79. }
  80. function normalizeReleaseVersion(release) {
  81. const candidates = [
  82. release?.tag_name,
  83. release?.name,
  84. ];
  85. for (const candidate of candidates) {
  86. const normalized = stripVersionPrefix(candidate);
  87. if (normalized) {
  88. return normalized;
  89. }
  90. }
  91. return '';
  92. }
  93. function sanitizeRelease(release) {
  94. const version = normalizeReleaseVersion(release);
  95. if (!version) {
  96. return null;
  97. }
  98. const rawTitle = sanitizeInlineMarkdown(release?.name || '');
  99. const normalizedTitle = stripVersionPrefix(rawTitle) === version ? '' : rawTitle;
  100. return {
  101. version,
  102. title: normalizedTitle,
  103. url: String(release?.html_url || RELEASES_PAGE_URL),
  104. publishedAt: String(release?.published_at || release?.created_at || ''),
  105. notes: parseReleaseNotes(release?.body || ''),
  106. };
  107. }
  108. function readCache() {
  109. try {
  110. const raw = localStorage.getItem(CACHE_KEY);
  111. if (!raw) {
  112. return null;
  113. }
  114. const parsed = JSON.parse(raw);
  115. if (!parsed || !Array.isArray(parsed.releases) || !Number.isFinite(parsed.fetchedAt)) {
  116. return null;
  117. }
  118. if ((Date.now() - parsed.fetchedAt) > CACHE_TTL_MS) {
  119. return null;
  120. }
  121. return parsed.releases;
  122. } catch (error) {
  123. return null;
  124. }
  125. }
  126. function writeCache(releases) {
  127. try {
  128. localStorage.setItem(CACHE_KEY, JSON.stringify({
  129. fetchedAt: Date.now(),
  130. releases,
  131. }));
  132. } catch (error) {
  133. // Ignore cache write failures.
  134. }
  135. }
  136. async function fetchReleases() {
  137. const controller = new AbortController();
  138. const timeoutId = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);
  139. try {
  140. const response = await fetch(RELEASES_API_URL, {
  141. method: 'GET',
  142. headers: {
  143. Accept: 'application/vnd.github+json',
  144. },
  145. cache: 'no-store',
  146. signal: controller.signal,
  147. });
  148. if (!response.ok) {
  149. throw new Error(`GitHub Releases 请求失败(${response.status})`);
  150. }
  151. const payload = await response.json();
  152. if (!Array.isArray(payload)) {
  153. throw new Error('GitHub Releases 返回格式异常');
  154. }
  155. const releases = payload
  156. .filter((release) => release && !release.draft && !release.prerelease)
  157. .map(sanitizeRelease)
  158. .filter(Boolean)
  159. .sort((left, right) => compareVersions(right.version, left.version))
  160. .slice(0, MAX_RELEASES);
  161. writeCache(releases);
  162. return releases;
  163. } catch (error) {
  164. if (error?.name === 'AbortError') {
  165. throw new Error('GitHub Releases 请求超时');
  166. }
  167. throw error;
  168. } finally {
  169. clearTimeout(timeoutId);
  170. }
  171. }
  172. async function loadReleases(options = {}) {
  173. if (!options.force) {
  174. const cached = readCache();
  175. if (cached) {
  176. return cached;
  177. }
  178. }
  179. return fetchReleases();
  180. }
  181. function buildReleaseSnapshot(releases, localVersion) {
  182. const latestRelease = releases[0] || null;
  183. if (!latestRelease) {
  184. return {
  185. status: 'empty',
  186. localVersion,
  187. latestVersion: null,
  188. latestRelease: null,
  189. newerReleases: [],
  190. logUrl: RELEASES_PAGE_URL,
  191. releasesPageUrl: RELEASES_PAGE_URL,
  192. checkedAt: Date.now(),
  193. };
  194. }
  195. const newerReleases = releases.filter((release) => compareVersions(release.version, localVersion) > 0);
  196. return {
  197. status: newerReleases.length > 0 ? 'update-available' : 'latest',
  198. localVersion,
  199. latestVersion: latestRelease.version,
  200. latestRelease,
  201. newerReleases,
  202. logUrl: latestRelease.url || RELEASES_PAGE_URL,
  203. releasesPageUrl: RELEASES_PAGE_URL,
  204. checkedAt: Date.now(),
  205. };
  206. }
  207. async function getReleaseSnapshot(options = {}) {
  208. const localVersion = stripVersionPrefix(chrome.runtime.getManifest()?.version || '0.0.0');
  209. try {
  210. const releases = await loadReleases(options);
  211. return buildReleaseSnapshot(releases, localVersion);
  212. } catch (error) {
  213. return {
  214. status: 'error',
  215. localVersion,
  216. latestVersion: null,
  217. latestRelease: null,
  218. newerReleases: [],
  219. logUrl: RELEASES_PAGE_URL,
  220. releasesPageUrl: RELEASES_PAGE_URL,
  221. checkedAt: Date.now(),
  222. errorMessage: error?.message || '更新检查失败',
  223. };
  224. }
  225. }
  226. function formatReleaseDate(value) {
  227. if (!value) {
  228. return '';
  229. }
  230. const date = new Date(value);
  231. if (Number.isNaN(date.getTime())) {
  232. return '';
  233. }
  234. const year = date.getFullYear();
  235. const month = String(date.getMonth() + 1).padStart(2, '0');
  236. const day = String(date.getDate()).padStart(2, '0');
  237. return `${year}-${month}-${day}`;
  238. }
  239. window.SidepanelUpdateService = {
  240. compareVersions,
  241. formatReleaseDate,
  242. getReleaseSnapshot,
  243. releasesPageUrl: RELEASES_PAGE_URL,
  244. stripVersionPrefix,
  245. };
  246. })();