您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Automatically download QFX statements from Amazon
// ==UserScript== // @name Amazon QFX Statement Exporter // @namespace Violentmonkey Scripts // @license MIT // @version 1.1.12 // @description Automatically download QFX statements from Amazon // @match https://*.amazon.com/* // @grant GM_download // @grant GM_xmlhttpRequest // @grant GM_setValue // @grant GM_getValue // @require https://cdn.jsdelivr.net/npm/[email protected]/build/md5.min.js // ==/UserScript== (function () { 'use strict'; // src/common.ts async function getElement(selector, POLL_INTERVAL) { return new Promise((resolve) => { const check = setInterval(() => { let el; if (typeof selector === "string") { el = document.querySelector(selector); } else { el = Array.from(selector).map((s) => document.querySelector(s)).find(Boolean); } if (el) { clearInterval(check); resolve(el); } }, POLL_INTERVAL); }); } async function easyDownload({ content, name, saveAs = true }) { let type; if (name.endsWith(".qfx")) type = "application/x-qfx"; else if (name.endsWith(".csv")) type = "text/csv"; else type = "application/octet-stream"; await GM_download_promise({ url: URL.createObjectURL(new Blob([content.trim()], { type })), name, saveAs }); } function GM_download_promise(option) { const { url, name, saveAs } = option; return new Promise((resolve, reject) => { GM_download({ url, name, saveAs, onload: resolve, onerror: reject }); }); } function easySetValue(key, value) { GM_setValue(key, value); } function easyGetValue(key) { return GM_getValue(key); } // src/amazon/lib.ts var BANK_ID = "amazon"; var LOGGER_prefix = `[${BANK_ID} Downloader]`; var origLog = console.log; var origError = console.error; console.log = (...args) => origLog(LOGGER_prefix, ...args); console.error = (...args) => origError(LOGGER_prefix, ...args); var POLL_INTERVAL = 500; class SingleTransaction { id; hint; date; amount; url; description; constructor(id, hint, date, amount, url, description) { this.id = id; this.hint = hint; this.date = date; this.amount = amount; this.url = url; this.description = description; } } var matchedTransactions = {}; var cachedCardTextAreas = {}; var shouldCollectMode = false; var history_length = 3; async function addGotoTransactionButton() { const container = await getElement("#nav-al-your-account", POLL_INTERVAL); const CLASS = "my-download-btn"; if (container?.querySelector(`.${CLASS}`)) return; const btn = document.createElement("button"); btn.textContent = "Download CSV"; btn.className = CLASS; btn.style.cssText = ` padding: 4px 4px; margin: 0px 0px 4px 0px; background-color: #007bff; color: white; border: none; border-radius: 4px; cursor: pointer; width: fit-content; `; btn.addEventListener("click", async () => { window.location.href = "https://www.amazon.com/cpe/yourpayments/transactions"; }); container?.insertBefore(btn, container.firstChild); } async function collectTransactionData() { if (!shouldCollectMode) return; const form = document.querySelector("form.a-spacing-none"); if (!form) return; const transactionElements = form.querySelectorAll(".a-section.a-spacing-base"); let currentDate = undefined; for (const transactionElement of transactionElements) { if (transactionElement.querySelector(".a-row") === null) { currentDate = transactionElement.querySelector("span")?.textContent; continue; } const hint_amount = Array.from(transactionElement.querySelectorAll(".a-column span")).map((el) => el.textContent); const url_element = transactionElement.querySelector(".a-column a"); if (hint_amount.length < 2 || !hint_amount[0] || !hint_amount[1]) { console.error("Failed to extract hint and amount:", { hint_amount, url_element }); continue; } if (hint_amount.length == 4) { const [hint, amount, order_text, description] = hint_amount; let id = order_text.trim() || ""; if (id.startsWith("Order #")) { id = id.slice("Order #".length).trim(); } matchedTransactions[`${id}---${hint}`] = new SingleTransaction(id, hint, currentDate ? new Date(currentDate) : new Date("2000-01-01"), parseFloat(hint_amount[1].replace(/[$,]/g, "")) || 0, "", description || ""); } else { const [hint, amount, description] = hint_amount; if (!url_element) { console.error("Failed to extract URL element:", { hint_amount }); continue; } let id = url_element.textContent?.trim() || ""; if (id.startsWith("Order #")) { id = id.slice("Order #".length).trim(); } matchedTransactions[`${id}---${hint}`] = new SingleTransaction(id, hint, currentDate ? new Date(currentDate) : new Date("2000-01-01"), parseFloat(hint_amount[1].replace(/[$,]/g, "")) || 0, url_element.href || "", description || ""); } } if (checkForOldTransactions(history_length)) { console.log("Found old transactions, stopped"); shouldCollectMode = false; attachCardTextAreas(); } else { console.log("No old transactions found"); pressNextButton(); } } async function sleep(ms) { return new Promise((resolve) => setTimeout(resolve, ms)); } async function pressNextButton() { const nextButton = Array.from(document.querySelectorAll("input[type='submit']")).find((input) => { const span = input.nextElementSibling; return span && span.textContent === "Next Page"; }); if (nextButton) { await sleep(1000); nextButton.click(); await sleep(1000); collectTransactionData(); } } function checkForOldTransactions(ago) { const oldDate = new Date; oldDate.setMonth(oldDate.getMonth() - ago); for (const [_, t] of Object.entries(matchedTransactions)) { if (t.date < oldDate) { return true; } } return false; } function generateQFX(cardnumber, matchedTransactions2, mapping) { const earliest = Object.values(matchedTransactions2).reduce((min, t) => t.date < min ? t.date : min, new Date); const latest = Object.values(matchedTransactions2).reduce((max, t) => t.date > max ? t.date : max, new Date(0)); const transactionCount = {}; for (const [_, t] of Object.entries(matchedTransactions2)) { transactionCount[t.id] = (transactionCount[t.id] || 0) + 1; } const formatQFXDate = (date) => { const yyyy = date.getFullYear(); const mm = String(date.getMonth() + 1).padStart(2, "0"); const dd = String(date.getDate()).padStart(2, "0"); return `${yyyy}${mm}${dd}120000`; }; const qfxHeader = ` OFXHEADER:100 DATA:OFXSGML VERSION:102 SECURITY:NONE ENCODING:USASCII CHARSET:1252 COMPRESSION:NONE OLDFILEUID:NONE NEWFILEUID:NONE <OFX> <SIGNONMSGSRSV1> <SONRS> <STATUS> <CODE>0 <SEVERITY>INFO </STATUS> <DTSERVER>20250420120000[0:GMT] <LANGUAGE>ENG <FI> <ORG>B1 <FID>10898 </FI> <INTU.BID>10898 </SONRS> </SIGNONMSGSRSV1> <CREDITCARDMSGSRSV1> <CCSTMTTRNRS> <TRNUID>1 <STATUS> <CODE>0 <SEVERITY>INFO <MESSAGE>Success </STATUS> <CCSTMTRS> <CURDEF>USD <CCACCTFROM> <ACCTID>${cardnumber} </CCACCTFROM> <BANKTRANLIST> <DTSTART>${formatQFXDate(earliest)}[0:GMT] <DTEND>${formatQFXDate(latest)}[0:GMT] `; const qfxTransactions = Object.values(matchedTransactions2).map((t) => { if (cardnumber != mapping[t.hint]) return ""; const hash = md5(t.id).substring(0, 6); return ` <STMTTRN> <TRNTYPE>DEBIT <DTPOSTED>${formatQFXDate(t.date)}[0:GMT] <TRNAMT>${t.amount} <FITID>${formatQFXDate(t.date)}${hash} <NAME>${t.description} <MEMO>ID${t.id}-${(transactionCount[t.id] ?? 0) > 1 ? "mixed" : ""} </STMTTRN> `; }); const qfxFooter = ` </BANKTRANLIST> <LEDGERBAL> <BALAMT>0 <DTASOF>${formatQFXDate(latest)}[0:GMT] </LEDGERBAL> <AVAILBAL> <BALAMT>0 <DTASOF>${formatQFXDate(latest)}[0:GMT] </AVAILBAL> </CCSTMTRS> </CCSTMTTRNRS> </CREDITCARDMSGSRSV1> </OFX> `; return qfxHeader + qfxTransactions.join(` `) + qfxFooter; } async function attachCardTextAreas() { const cardListArea = await getElement("#my-card-list-area", POLL_INTERVAL); if (cardListArea) { cardListArea.innerHTML = ""; } for (const [_, t] of Object.entries(matchedTransactions)) { if (!cachedCardTextAreas[t.hint]) { const cardTextArea = document.createElement("textarea"); cardTextArea.textContent = easyGetValue(`amazon-card-details-${t.hint}`) || ""; cardTextArea.className = "my-card-text-area"; cardTextArea.placeholder = `Enter card details to ${t.hint} here...`; cardTextArea.onchange = (e) => { const value = e.target.value; easySetValue(`amazon-card-details-${t.hint}`, value); }; cachedCardTextAreas[t.hint] = cardTextArea; } } for (const [_, t] of Object.entries(matchedTransactions)) { const cardTextArea = cachedCardTextAreas[t.hint]; if (cardTextArea) { cardListArea?.appendChild(cardTextArea); } } const downloadButton = document.createElement("button"); downloadButton.textContent = "Download QFX"; downloadButton.onclick = async () => { const mapping = {}; for (const [_, t] of Object.entries(matchedTransactions)) { const cardTextArea = cachedCardTextAreas[t.hint]; if (cardTextArea) { mapping[t.hint] = cardTextArea.value; } } const uniqueCardNumbers = Array.from(new Set(Object.values(mapping).filter(Boolean))); for (const cardnumber of uniqueCardNumbers) { let strip = function(s) { return s.replace(/[^\w\s]/gi, "").replace(/\s+/g, "_").replace("*", "").replace(" ", ""); }; const qfx_content = generateQFX(cardnumber, matchedTransactions, mapping); await easyDownload({ content: qfx_content, name: `${BANK_ID}_${strip(cardnumber)}_${new Date().toISOString().slice(0, 10)}_${history_length}mo.qfx`, saveAs: true }); } const widgetArea = await getElement(".my-widget-area", POLL_INTERVAL); widgetArea.appendChild(downloadButton); }; cardListArea?.appendChild(downloadButton); } async function addWidget() { const container = await getElement(".amazonPayWalletBreadcrumbsContainer ", POLL_INTERVAL); if (!container) return; const widgetArea = document.createElement("div"); widgetArea.className = "my-widget-area a-container payWalletContentContainer"; widgetArea.style.border = "1px solid #333"; container?.parentNode?.insertBefore(widgetArea, container.nextSibling); const title = document.createElement("h3"); title.textContent = "QFX Download Plugin"; widgetArea.appendChild(title); const collectButton = document.createElement("button"); collectButton.textContent = "Collect Transactions"; collectButton.onclick = () => { shouldCollectMode = true; collectButton.style.display = "none"; collectTransactionData(); }; widgetArea.appendChild(collectButton); const cardArea = document.createElement("div"); const cardLabel = document.createElement("label"); cardLabel.textContent = "Card Details"; cardArea.id = "my-card-list-area"; widgetArea.appendChild(cardLabel); widgetArea.appendChild(cardArea); } // src/amazon/index.ts try { addGotoTransactionButton(); addWidget(); } catch (err) { console.error("[Amazon Downloader] Error:", err); } })();