尚未偵測到更新。GitHub 發布版本後會顯示在這裡。
個人 GitHub 帳號下公開儲存庫的擁有者可以登入並驗證所有權,自行管理介紹,無需管理員核准。
登入並產生驗證檔案,在 30 分鐘內將其提交至此儲存庫的預設分支。其他分支或分叉儲存庫中的檔案無效。然後在下方檢查檔案,即可無需管理員核准直接管理專案。請保留檔案以維持管理權限。
如需登入,將前往登入頁面。登入後請回到此處,再次點擊所選的驗證按鈕。
在專案資料夾中執行 /nomorevibe verify,即可驗證所有權並自行管理介紹。
一分鐘完成驗證製作者的介紹僅有上面的一句話。 更多詳情請閱讀儲存庫 README。 儲存庫 README ↗
正在載入完整 README。
檢視完整 README ↗Build me a clean, modern 3-stage web app called "InboxAI" — a Gmail AI Agent setup wizard. The app lives on one page and walks users through 3 stages. Only one stage is visible at a time.
=== BRAND & STYLE ===
=== STAGE 1 — LANDING / HERO === Full screen hero section with:
=== STAGE 2 — SETTINGS FORM === Header: "Tell us about your inbox" with subtext "Takes 2 minutes. We'll build your custom script with your settings already filled in."
Back button at top left that goes back to Stage 1.
Form fields:
Big button at bottom: "Build my custom script →" — clicking it generates the script and goes to Stage 3.
=== STAGE 3 — GENERATED SCRIPT + INSTALL STEPS === Back button at top left that goes back to Stage 2.
Header: "Your script is ready" Subtext: "Follow the 4 steps below. Takes about 5 minutes. You'll never need to touch code again."
Green success banner: "✓ Your settings have been baked into the script below."
SCRIPT OUTPUT BOX:
INSTALL STEPS — numbered list of cards: Step 1: "Go to script.google.com and sign in with the same Google account as your Gmail. Click New project." Step 2: "Delete everything in the editor. Paste your script above using Ctrl+V (Cmd+V on Mac). Save with Ctrl+S." Step 3: "Click the dropdown at the top of the editor — it says myFunction by default. Change it to testGemini. Press ▶ Run. Accept any permissions Google asks for." Step 4: "Once that works, change the dropdown to setupTrigger and press ▶ Run. Your agent is now live and runs every hour automatically." Step 5: "To stop the agent any time, run stopAgent from the same dropdown. To fully remove access go to myaccount.google.com/permissions."
=== THE SCRIPT TEMPLATE === When the user clicks "Build my custom script", generate this script with their values filled in. Replace all placeholder values with what the user entered in the form:
// ============================================================ // GMAIL AI AGENT — your custom build // Paste into script.google.com → New project → Run setupTrigger // ============================================================
const SETTINGS = { geminiApiKey: "PASTE_YOUR_GEMINI_API_KEY_HERE", trustedSenders: [ [LIST OF EMAILS FROM FORM, each on its own line in quotes] ], deleteSpam: [TRUE OR FALSE FROM TOGGLE], unsubscribeAfterDays: [NUMBER FROM SLIDER], notificationEmail: Session.getActiveUser().getEmail(), useTelegram: [TRUE OR FALSE FROM TOGGLE], telegramBotToken: "[TOKEN FROM FORM OR PASTE_YOUR_BOT_TOKEN_HERE]", telegramChatId: "[CHAT ID FROM FORM OR PASTE_YOUR_CHAT_ID_HERE]", batchSize: 20, lookbackDays: 3 };
const Memory = {
load() { const raw = PropertiesService.getScriptProperties().getProperty("agentMemory"); return raw ? JSON.parse(raw) : {}; },
save(memory) { PropertiesService.getScriptProperties().setProperty("agentMemory", JSON.stringify(memory)); },
remember(email, action, reason) { const m = this.load(); m[email.toLowerCase()] = { action, reason, learnedOn: new Date().toLocaleDateString() }; this.save(m); },
recall(email) { return this.load()[email.toLowerCase()] || null; },
summarize() { const m = this.load(); const e = Object.entries(m); if (!e.length) return "No senders learned yet."; return e.map(([em, d]) => ${em}: ${d.action} — "${d.reason}").join("\n"); },
forget(email) { const m = this.load(); delete m[email.toLowerCase()]; this.save(m); },
reset() { PropertiesService.getScriptProperties().deleteProperty("agentMemory"); }
};
function askGemini(senderEmail, senderName, subject, snippet) {
const prompt = You are a Gmail assistant. Analyze this email and decide what to do.\nFrom: ${senderName} <${senderEmail}>\nSubject: ${subject}\nPreview: ${snippet}\nRules: STAR=personal/important, TRASH=junk/scam, UNSUB=newsletter/marketing, FLAG=unsure\nReply ONLY:\nDECISION: [STAR|TRASH|UNSUB|FLAG]\nREASON: [one sentence];
const url = https://generativelanguage.googleapis.com/v1beta/models/gemini-1.5-flash:generateContent?key=${SETTINGS.geminiApiKey};
try {
const res = UrlFetchApp.fetch(url, { method: "post", contentType: "application/json", payload: JSON.stringify({ contents: [{ parts: [{ text: prompt }] }], generationConfig: { temperature: 0.1, maxOutputTokens: 60 } }), muteHttpExceptions: true });
const text = JSON.parse(res.getContentText())?.candidates?.[0]?.content?.parts?.[0]?.text?.trim();
if (!text) return { action: "FLAG", reason: "No response from Gemini" };
const action = (text.match(/DECISION:\s*(STAR|TRASH|UNSUB|FLAG)/i) || [])[1]?.toUpperCase() || "FLAG";
const reason = (text.match(/REASON:\s*(.+)/i) || [])[1]?.trim() || "No reason";
return { action, reason };
} catch (e) { return { action: "FLAG", reason: "API error: " + e.message }; }
}
function setupTrigger() { ScriptApp.getProjectTriggers().forEach(t => ScriptApp.deleteTrigger(t)); ScriptApp.newTrigger("runAgent").timeBased().everyHours(1).create(); sendNotification("✅ Gmail Agent active!", "Your Gmail AI Agent is running.", "✅ Gmail AI Agent is live!"); Logger.log("Agent activated."); }
function runAgent() {
const flagged = [], actioned = [];
processTrustedSenders(actioned);
if (SETTINGS.deleteSpam) clearSpam(actioned);
const threads = GmailApp.search(is:unread newer_than:${SETTINGS.lookbackDays}d, 0, SETTINGS.batchSize);
for (const thread of threads) {
const msgs = thread.getMessages();
if (!msgs.length) continue;
const msg = msgs[msgs.length - 1];
const senderFull = msg.getFrom();
const senderEmail = extractEmail(senderFull);
if (isTrustedSender(senderEmail)) continue;
let decision = Memory.recall(senderEmail);
if (!decision) { decision = askGemini(senderEmail, extractName(senderFull), msg.getSubject() || "(no subject)", msg.getPlainBody().substring(0, 300)); Memory.remember(senderEmail, decision.action, decision.reason); }
switch (decision.action) {
case "STAR": msg.star(); actioned.push(⭐ Starred: "${msg.getSubject()}"); break;
case "TRASH": thread.moveToTrash(); actioned.push(🗑️ Trashed: "${msg.getSubject()}"); break;
case "UNSUB":
const url = getUnsubscribeUrl(msg);
if (url && attemptUnsubscribe(url)) { thread.moveToTrash(); actioned.push(📧 Unsubscribed: ${senderEmail}); }
else flagged.push({ from: senderFull, subject: msg.getSubject(), reason: "Unsubscribe failed — manual action needed" });
break;
default: flagged.push({ from: senderFull, subject: msg.getSubject(), reason: decision.reason });
}
}
if (flagged.length) sendFlagNotification(flagged, actioned);
}
function processTrustedSenders(actioned) {
GmailApp.search(is:unread newer_than:${SETTINGS.lookbackDays}d, 0, SETTINGS.batchSize).forEach(thread => {
thread.getMessages().forEach(msg => { if (msg.isUnread() && isTrustedSender(extractEmail(msg.getFrom()))) { msg.star(); actioned.push(⭐ Starred (trusted): "${msg.getSubject()}"); } });
});
}
function isTrustedSender(email) { return SETTINGS.trustedSenders.some(t => email.toLowerCase().includes(t.toLowerCase())); }
function clearSpam(actioned) { const s = GmailApp.search("in:spam", 0, SETTINGS.batchSize); if (s.length) { GmailApp.moveThreadsToTrash(s); actioned.push(🗑️ Trashed ${s.length} spam threads); } }
function sendFlagNotification(flagged, actioned) {
const lines = flagged.map((e, i) => ${i + 1}. From: ${e.from}\n "${e.subject}"\n Reason: ${e.reason}).join("\n\n");
sendNotification(🚩 ${flagged.length} email(s) need your attention, ${flagged.length} email(s) were flagged:\n\n${lines}\n\nThese were NOT touched., 🚩 ${flagged.length} email(s) flagged:\n${lines});
}
function sendNotification(subject, emailBody, telegramText) {
try { GmailApp.sendEmail(SETTINGS.notificationEmail, subject, emailBody); } catch (e) {}
if (SETTINGS.useTelegram && SETTINGS.telegramBotToken !== "PASTE_YOUR_BOT_TOKEN_HERE") {
try { UrlFetchApp.fetch(https://api.telegram.org/bot${SETTINGS.telegramBotToken}/sendMessage, { method: "post", contentType: "application/json", payload: JSON.stringify({ chat_id: SETTINGS.telegramChatId, text: telegramText || subject }), muteHttpExceptions: true }); } catch (e) {}
}
}
function extractEmail(f) { const m = f.match(/<(.+?)>/); return m ? m[1].toLowerCase() : f.toLowerCase().trim(); } function extractName(f) { const m = f.match(/^([^<]+)</); return m ? m[1].trim() : f; } function getUnsubscribeUrl(msg) { try { const m = msg.getRawContent().match(/List-Unsubscribe:\s*<(https?://[^>]+)>/i); return m ? m[1] : null; } catch (e) { return null; } } function attemptUnsubscribe(url) { try { const r = UrlFetchApp.fetch(url, { method: "get", followRedirects: true, muteHttpExceptions: true }); return r.getResponseCode() < 400; } catch (e) { return false; } }
function testGemini() { Logger.log(JSON.stringify(askGemini("[email protected]", "Example", "Weekly digest", "Top stories this week..."))); } function testTelegram() { SETTINGS.useTelegram = true; sendNotification("🤖 Test", "Test email.", "🤖 Telegram connected!"); } function showMemory() { Logger.log(Memory.summarize()); } function forgetSender() { Memory.forget("[email protected]"); } function resetMemory() { Memory.reset(); } function stopAgent() { ScriptApp.getProjectTriggers().forEach(t => ScriptApp.deleteTrigger(t)); Logger.log("Agent stopped."); }
=== IMPORTANT NOTES FOR THE BUILDER ===
This project was built with Lovable.
Live app: https://inboxaidfw.lovable.app
Continue developing this project in the Lovable editor.
main on GitHub and your changes sync back into Lovable, ready for your next prompt.Prefer working locally? You need Node.js and npm — install with nvm.
git clone <this-repository-url>
cd <repository-name>
npm i
npm run dev