您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Export Expo EAS build logs and rename app files by version
// ==UserScript== // @name Expo EAS Build Log Export // @namespace Violentmonkey Scripts // @match https://expo.dev/accounts/*/projects/*/builds/* // @grant GM_xmlhttpRequest // @version 0.5 // @author likaci // @description Export Expo EAS build logs and rename app files by version // @license MIT // ==/UserScript== (function () { "use strict"; const TAG = "EAS-LOG-EXPORT"; console.log(TAG, "loaded"); const buildId = window.location.pathname.split("/").pop(); let installButton; const observer = new MutationObserver((mutations, observer) => { installButton = document.querySelector( '[data-testid="artifact-download-button"]' ); if (buildId && installButton) { console.log(TAG, "Found build ID:", buildId); const getExpoSession = () => { try { const sessionCookie = document.cookie .split(";") .find((c) => c.trim().startsWith("io.expo.auth.sessionSecret=")); if (!sessionCookie) return null; return decodeURIComponent(sessionCookie.split("=")[1]); } catch (e) { console.log(TAG, "Failed to parse session cookie:", e); return null; } }; const session = getExpoSession(); if (!session) { console.error(TAG, "No expo session found"); return; } const graphqlQuery = { query: ` query BuildById($buildId: ID!) { builds { byId(buildId: $buildId) { id app { slug } platform status artifacts { applicationArchiveUrl xcodeBuildLogsUrl } logFiles appVersion appBuildVersion buildProfile } } } `, variables: { buildId: buildId, }, }; fetch("https://api.expo.dev/graphql", { method: "POST", headers: { "Content-Type": "application/json", Accept: "application/json", "expo-session": session, }, body: JSON.stringify(graphqlQuery), }) .then((response) => response.json()) .then((data) => { console.log(TAG, "Fetched build data:", data); if (data?.data?.builds?.byId) { handleBuildData(data.data.builds.byId); } }) .catch((error) => console.error(TAG, "Error fetching build data:", error) ); observer.disconnect(); } }); observer.observe(document.body, { childList: true, subtree: true, }); function handleBuildData(buildData) { const { app, logFiles, artifacts, appVersion, appBuildVersion, buildProfile, platform, } = buildData; const {slug} = app; const {applicationArchiveUrl, xcodeBuildLogsUrl} = artifacts; const filePrefix = `${appVersion}-${appBuildVersion}_${buildProfile}`; if (!installButton) { console.error(TAG, "Install button not found"); } else { const targetDiv = installButton.parentNode; // Logs if (logFiles?.length > 0) { createDownloadButton(targetDiv, "Logs", () => downloadLogs(logFiles, `logs_${platform.toLowerCase()}_${filePrefix}`) ); } // Xcode logs if (xcodeBuildLogsUrl) { createDownloadButton(targetDiv, "Xcode Logs", () => downloadFile(xcodeBuildLogsUrl, `logs_xcode_${filePrefix}.log`) ); } // App if (applicationArchiveUrl) { createDownloadButton(targetDiv, "App", () => { const extension = applicationArchiveUrl.split(".").pop(); downloadFile( applicationArchiveUrl, `${slug}_${filePrefix}.${extension}` ); }); } } } function createDownloadButton(targetDiv, text, onclick) { const btn = document.createElement("button"); btn.textContent = text; btn.className = "border-solid rounded-md font-medium h-9 px-4 text-xs bg-button-primary text-button-primary hocus:bg-button-primary-hover"; btn.onclick = onclick; targetDiv.appendChild(btn); } async function downloadLogs(logFiles, filePrefix) { const logsByPhase = {}; for (const logFileUrl of logFiles) { try { const response = await fetch(logFileUrl); const text = await response.text(); const lines = text.split("\n"); lines.forEach((line) => { try { const log = JSON.parse(line); const phase = log.phase; if (!logsByPhase[phase]) { logsByPhase[phase] = []; } logsByPhase[phase].push(`[${log.time}] ${log.msg}`); } catch (e) { console.warn(TAG, "Error parsing log line:", line, e); } }); } catch (error) { console.error(TAG, "Error fetching log file:", logFileUrl, error); } } let formattedLogs = ""; for (const phase in logsByPhase) { formattedLogs += `=== ${phase} ===\n`; formattedLogs += logsByPhase[phase].join("\n"); formattedLogs += "\n\n"; } downloadBlob( new Blob([formattedLogs], {type: "text/plain"}), `${filePrefix}.log` ); } async function downloadFile(url, filename) { return new Promise((resolve, reject) => { GM_xmlhttpRequest({ method: "GET", url: url, responseType: "blob", onload: function (response) { resolve(response.response); }, onerror: function (error) { reject(error); } }); }).then(blob => { downloadBlob(blob, filename); }); } function downloadBlob(blob, filename) { const downloadLink = document.createElement('a'); downloadLink.href = URL.createObjectURL(blob); downloadLink.download = filename; document.body.appendChild(downloadLink); downloadLink.click(); document.body.removeChild(downloadLink); } })();