尚未检测到更新。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