ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

基于本地大模型与浏览器自动化实现智能邮箱助手

基于本地大模型与浏览器自动化实现智能邮箱助手 1. 项目概述当AI助手学会“动手”操作你的邮箱最近在折腾一个挺有意思的项目我把它叫做“WorkBuddy对接本地模型实现网页邮箱操作”。简单来说就是让一个AI助手WorkBuddy不再只是动嘴皮子回答问题而是能真正“动手”去帮你处理网页邮箱里的那些琐事比如自动归类邮件、智能回复、甚至帮你起草周报。这听起来是不是有点像科幻片里的场景但实现它的核心其实是我们手头就能玩转的两项技术一个能理解你意图的本地大语言模型和一个能让程序控制浏览器的“遥控器”——Chrome DevTools Protocol。为什么要把这两样东西凑在一起作为一个每天被海量邮件淹没的从业者我太清楚那种重复性劳动带来的疲惫感了。市面上虽然有一些邮箱助手但它们要么是云端服务数据隐私让人顾虑要么功能僵化无法理解我个性化的处理逻辑。而“本地模型CDP”这个组合恰好完美地解决了这两个痛点。本地模型保证了所有邮件内容、你的操作习惯都只在你的电脑上流转隐私安全可控而CDP则赋予了程序在浏览器里“为所欲为”的能力从点击按钮到填写表单无所不能。这个项目的本质就是给AI装上了一双能在浏览器里精准操作的“手”和“眼睛”。这个项目非常适合两类朋友一是对AI应用开发感兴趣的开发者想探索大模型如何与真实世界交互二是像我一样受困于效率工具渴望打造一个完全属于自己、高度定制化智能工作流的人。整个过程会涉及到本地模型的选型与部署、CDP的编程控制以及如何让两者协同工作的逻辑设计。不用担心复杂我会把每一步的“为什么”和“怎么做”都掰开揉碎了讲清楚让你不仅能复现更能理解背后的设计哲学。2. 核心思路拆解从“思考”到“执行”的完整链路要实现让AI操作邮箱我们不能把它想象成一个黑箱魔法。相反我们需要清晰地拆解出从“用户指令”到“浏览器动作”的完整逻辑链条。这个链条可以概括为三个核心环节感知、决策与执行。2.1 感知层如何让AI“看到”网页内容AI要操作邮箱首先得知道邮箱页面长什么样、上面有什么。这就是“感知”要解决的问题。我们不可能直接把浏览器的像素图丢给大模型那样效率太低且信息杂乱。正确的方法是获取页面的结构化信息主要是DOM文档对象模型。这里Chrome DevTools Protocol就派上用场了。我们可以通过CDP命令让浏览器将其当前页面的DOM树状态、元素的CSS选择器、文本内容等信息“吐”出来。例如获取收件箱列表里所有邮件的标题和发件人。获取到的这些结构化数据就构成了AI感知当前环境的“眼睛”。一个常见的技巧是我们不会抓取整个页面的DOM那样太大。而是通过分析邮箱页面的布局精准抓取关键区域如邮件列表区、阅读区、撰写区的DOM这能极大减少后续处理的数据量并提高AI理解的准确性。2.2 决策层本地大模型的“大脑”如何运转当AI“看到”了页面信息比如收件箱里有5封未读邮件其中3封来自“项目组”标题包含“紧急”结合你给它的指令如“帮我处理一下未读邮件”就需要它来“思考”并做出决策了。这就是本地大模型的核心工作。我们将“页面状态”和“用户指令”组合成一个精心设计的提示词Prompt发送给本地部署的模型。这个Prompt的编写质量直接决定了AI的表现。它需要清晰地定义任务你是一个邮箱助手提供上下文当前页面有哪些元素并约束输出格式必须输出一个可被程序解析的、具体的操作序列。例如一个简化版的Prompt可能是你是一个智能邮箱助手。当前页面是邮箱收件箱包含以下邮件 1. [发件人项目组A 标题关于下周会议安排 状态未读] 2. [发件人广告商B 标题年度大促 状态未读] 3. [发件人老板 标题Q3报告反馈 状态已读] 用户指令“将项目组的邮件标记为星标并删除广告邮件。” 请根据以上信息生成一个JSON格式的操作序列。每个操作必须包含 action操作类型如 click, input, select和 selectorCSS选择器用于定位元素。操作类型仅限于click, input, double_click, hover, select_option。 请仅输出JSON不要有其他解释。一个理想的模型回复应该像这样[ {action: click, selector: div[data-email-id1] .star-icon}, {action: click, selector: div[data-email-id2] .checkbox}, {action: click, selector: #delete-button} ]这个环节的关键在于模型的选择和Prompt工程。轻量级的本地模型如通过Ollama部署的Llama 3.2、Qwen2.5或DeepSeek Coder在理解这类结构化任务上已经表现不错且响应速度快隐私零泄露。2.3 执行层CDP如何将“想法”变为“动作”当决策层输出一个标准的操作序列JSON数组后执行层就要登场了。它的任务就是充当一个“机器人手臂”严格地按照序列执行每一个操作。我们再次请出CDP。通过CDP我们可以向浏览器发送精确的命令来模拟用户操作。例如click操作对应CDP的Input.dispatchMouseEvent命令可以模拟鼠标在特定坐标或元素上的点击。input操作对应Input.insertText或先click聚焦输入框再Input.insertText。select_option操作可能需要先click下拉框再click某个选项。执行层需要解析JSON中的selector通过CDP的DOM.querySelector或DOM.getBoxModel等命令来获取目标元素在页面中的精确位置或节点ID然后触发相应的事件。这里有一个非常重要的注意事项网络延迟与元素加载状态。浏览器操作是异步的点击一个按钮可能会触发页面跳转或局部刷新。因此在执行下一个操作前必须加入等待机制如等待某个特定元素出现、等待网络空闲否则程序会因找不到元素而报错。这通常是自动化脚本最容易“翻车”的地方。3. 技术栈选型与工具准备工欲善其事必先利其器。在开始编码之前我们需要选定一套稳定、高效且易于上手的技术组合。这个项目主要涉及三大部分浏览器控制、本地模型服务、以及中间的粘合逻辑。3.1 浏览器自动化核心为什么是Puppeteer/Playwright CDP直接裸调CDP API是非常繁琐的我们需要一个高级封装库。Puppeteer谷歌官方和Playwright微软出品是目前最主流的选择。它们都基于CDP但提供了更友好、更稳定的API。我最终选择了Playwright。原因有三点第一Playwright支持Chromium、Firefox和WebKit三大浏览器引擎兼容性更好第二它的API设计更现代例如自动等待机制auto-waiting做得更完善能大大减少我们手动处理等待状态的代码量第三它的社区活跃文档清晰。对于我们的项目我们将主要使用Playwright的Node.js库来启动和控制浏览器实例并通过其提供的底层接口直接调用一些CDP命令以实现更精细的控制。安装准备# 初始化项目 mkdir workbuddy-email-assistant cd workbuddy-email-assistant npm init -y # 安装Playwright及相关浏览器 npm install playwright npx playwright install chromium # 安装用于HTTP请求的axios用于与本地模型API通信 npm install axios3.2 本地大模型服务Ollama vs. LM Studio让AI跑在本地你需要一个模型“容器”和服务接口。两个最流行的选择是Ollama和LM Studio。Ollama 更像一个命令行工具和后台服务。它专注于无缝地拉取、运行和管理大语言模型。部署极其简单一条命令就能启动一个模型并提供类OpenAI的API接口。它非常适合作为后端服务集成到其他应用中。对于本项目Ollama是首选因为它轻量、无图形界面开销且API标准化。LM Studio 提供了一个漂亮的图形化界面方便你下载、尝试和配置模型。它更适合研究、测试和模型评估。虽然它也提供本地API但Ollama在作为纯后台服务集成时更简洁。选择Ollama的实操步骤访问Ollama官网根据你的操作系统Windows/macOS/Linux下载安装。打开终端拉取一个适合的轻量模型。例如7B参数的模型在消费级显卡上就能流畅运行ollama pull llama3.2:1b # 拉取一个非常小的模型用于测试 ollama pull qwen2.5:7b # 或拉取一个能力更强的7B模型运行模型服务ollama run qwen2.5:7b默认情况下Ollama会在http://localhost:11434提供一个API服务。我们可以通过/api/generate端点来调用它。3.3 粘合层Node.js脚本的逻辑骨架我们的主程序将是一个Node.js脚本它需要完成以下循环使用Playwright打开浏览器导航到网页邮箱如Gmail、Outlook网页版。登录邮箱首次可能需要手动处理或使用安全的方式存储会话。进入目标页面如收件箱通过Playwright/CDP抓取页面关键信息。将页面信息和用户指令构造成Prompt发送给本地Ollama API。解析模型返回的JSON操作序列。使用Playwright根据操作序列一步步执行浏览器操作。等待操作完成根据需求决定是否循环如处理下一批邮件。这个循环构成了我们WorkBuddy的“中枢神经系统”。4. 实战构建从零搭建你的邮箱AI助手理论说得再多不如一行代码。让我们开始动手搭建一个最小可行产品MVP实现“将未读邮件标记为已读”这个基础功能。4.1 第一步初始化浏览器环境与登录态管理首先我们创建一个index.js文件初始化Playwright和浏览器上下文。为了省去每次输入密码的麻烦我们可以利用Playwright的“存储认证状态”功能。const { chromium } require(playwright); const axios require(axios); const fs require(fs).promises; const path require(path); // 常量定义 const OLLAMA_API_URL http://localhost:11434/api/generate; const MODEL_NAME qwen2.5:7b; // 与你本地运行的模型名一致 const EMAIL_URL https://mail.google.com; // 以Gmail为例 const STATE_PATH path.join(__dirname, auth-state.json); async function initBrowser() { const browser await chromium.launch({ headless: false, // 首次运行设为false方便观察和登录 slowMo: 100, // 操作延迟100毫秒方便调试时看清 }); const context await browser.newContext({ viewport: { width: 1280, height: 720 }, userAgent: Mozilla/5.0 ... // 可设置一个常见的UA }); // 尝试加载之前保存的登录状态 try { const state JSON.parse(await fs.readFile(STATE_PATH, utf8)); await context.addCookies(state.cookies); await context.addInitScript(state.storageState); console.log(已加载历史登录状态。); } catch (err) { console.log(未找到历史登录状态需手动登录。); } const page await context.newPage(); await page.goto(EMAIL_URL); // 检查是否已登录如果未登录暂停让用户手动登录 // 这里以检查是否存在“登录”按钮或特定登录后元素为例需根据实际邮箱页面调整 const isLoggedIn await page.locator(a[href*SignOut]).count() 0 || await page.locator(div[data-tooltip账户信息]).count() 0; if (!isLoggedIn) { console.log(请在弹出的浏览器窗口中手动登录邮箱登录完成后回到控制台按回车继续...); await page.pause(); // Playwright的暂停功能等待用户操作 // 登录后保存状态 const cookies await context.cookies(); const storageState await context.storageState(); await fs.writeFile(STATE_PATH, JSON.stringify({ cookies, storageState }, null, 2)); console.log(登录状态已保存。); } else { console.log(已自动登录。); } return { browser, page }; }注意page.pause()是一个强大的调试工具它会暂停脚本执行并打开Playwright Inspector让你可以实时查看页面、执行命令。对于处理登录这类复杂且可能有多重验证的流程手动处理一次并保存状态是最稳妥的方式。4.2 第二步实现页面信息感知与提取登录成功后我们需要让AI“看到”收件箱。我们编写一个函数来提取未读邮件的关键信息。async function fetchUnreadEmails(page) { console.log(开始抓取未读邮件信息...); // 等待收件箱或邮件列表加载完成。这里的选择器需要根据目标邮箱页面实际调整。 // 以Gmail为例可能需要等待 div[rolemain] 或特定的邮件列表容器。 await page.waitForSelector(div[rolelist] div[rolelistitem], { timeout: 10000 }); // 通过CDP获取更丰富的DOM信息。这里使用Playwright封装的evaluate方法执行页面内JavaScript。 const emailData await page.evaluate(() { const emailItems document.querySelectorAll(div[rolelist] div[rolelistitem]); const emails []; emailItems.forEach((item, index) { // 这里的选择器逻辑需要根据具体邮箱页面结构进行适配和调整 // 目标是提取发件人、标题、是否未读、邮件ID用于后续操作定位 const senderEl item.querySelector([email]) || item.querySelector(.sender); const titleEl item.querySelector(.subject) || item.querySelector([data-thread-id]); const isUnread item.classList.contains(unread) || item.getAttribute(aria-read) false; if (senderEl titleEl) { emails.push({ id: index, // 临时ID实际应用中最好用data-attribute sender: senderEl.innerText.trim(), title: titleEl.innerText.trim(), isUnread: isUnread, // 获取一个相对稳定的选择器用于后续操作定位 selector: div[rolelist] div[rolelistitem]:nth-child(${index 1}) }); } }); // 过滤出未读邮件 return emails.filter(email email.isUnread); }); console.log(共发现 ${emailData.length} 封未读邮件。); return emailData; }实操心得页面信息提取是整个流程中最脆弱的一环因为网页结构可能随时变化。这里的CSS选择器只是示例你必须使用浏览器的开发者工具F12仔细分析目标邮箱页面的实际DOM结构找到稳定、唯一的元素选择器。一个技巧是优先选择带有>async function askModelForActions(emailList, userInstruction) { const prompt 你是一个智能邮箱助手。当前收件箱的未读邮件列表如下格式序号. [发件人] 标题 ${emailList.map((e, i) ${i1}. [${e.sender}] ${e.title}).join(\n)} 用户指令“${userInstruction}” 请根据以上信息生成一个JSON数组格式的操作序列来帮助用户完成指令。每个操作对象必须包含 - \action\: 操作类型只能是以下之一click, double_click, right_click, hover, input, select_option。 - \selector\: 一个有效的CSS选择器字符串用于在页面上定位目标元素。请基于我提供的邮件项中的selector字段进行组合或微调。 - \value\: 仅当action为input或select_option时需要输入的值或选择的选项。 当前任务仅限于处理上述列表中的邮件。请确保操作逻辑合理、顺序正确。 请只输出JSON数组不要有任何其他解释、标记或代码块包裹。 示例输出标记所有未读邮件为已读 [ {action: click, selector: div[role\list\] div[role\listitem\]:nth-child(1) .mail-read-checkbox}, {action: click, selector: div[role\list\] div[role\listitem\]:nth-child(2) .mail-read-checkbox} ] ; try { const response await axios.post(OLLAMA_API_URL, { model: MODEL_NAME, prompt: prompt, stream: false, options: { temperature: 0.1, // 低温度让输出更确定、更遵循格式 num_predict: 500 // 最大生成token数 } }); const modelResponse response.data.response; console.log(模型原始回复, modelResponse); // 清洗回复尝试提取JSON部分 const jsonMatch modelResponse.match(/\[[\s\S]*\]/); if (!jsonMatch) { throw new Error(模型未返回有效的JSON数组); } const actions JSON.parse(jsonMatch[0]); console.log(解析出的操作序列, JSON.stringify(actions, null, 2)); return actions; } catch (error) { console.error(调用模型失败, error.message); // 可以在这里实现一个降级策略比如使用一个预定义的简单规则 return []; } }注意事项Prompt工程是成败的关键。我们通过示例输出Few-shot Learning明确告诉了模型我们期望的格式。设置较低的temperature值可以减少模型的随机性让输出更稳定。务必在Prompt中强调“只输出JSON”并做好错误处理因为模型有时可能会在JSON前后添加无关文本。4.4 第四步执行模型生成的浏览器操作最后我们编写一个执行器来运行模型给出的操作列表。async function executeActions(page, actions) { console.log(开始执行 ${actions.length} 个操作...); for (const [index, act] of actions.entries()) { console.log(执行操作 ${index 1}: ${act.action} - ${act.selector}); try { const element await page.locator(act.selector).first(); await element.waitFor({ state: visible, timeout: 5000 }); switch (act.action) { case click: await element.click(); break; case double_click: await element.doubleClick(); break; case input: if (act.value) { await element.fill(act.value); } break; case select_option: if (act.value) { await element.selectOption(act.value); } break; // 可以扩展更多操作类型如 hover, right_click 等 default: console.warn(未知操作类型: ${act.action}); } // 每个操作后等待一小段时间模拟人类操作间隔也让页面有反应时间 await page.waitForTimeout(500); } catch (error) { console.error(执行操作 ${index 1} 时出错, error.message); // 可以选择记录错误并继续或中断整个流程 // break; } } console.log(所有操作执行完毕。); }4.5 第五步整合主流程现在我们把所有部分串联起来。async function main() { const userInstruction 将所有未读邮件标记为已读; // 可以改为从命令行参数读取 let browser; let page; try { ({ browser, page } await initBrowser()); const unreadEmails await fetchUnreadEmails(page); if (unreadEmails.length 0) { console.log(没有未读邮件任务结束。); return; } const actions await askModelForActions(unreadEmails, userInstruction); if (actions actions.length 0) { await executeActions(page, actions); console.log(指令执行完成); } else { console.log(模型未生成有效操作。); } // 执行完成后可以等待一会儿观察结果或进行下一步操作 await page.waitForTimeout(3000); } catch (error) { console.error(主流程发生错误, error); } finally { if (browser) { await browser.close(); } } } // 启动程序 main();运行这个脚本node index.js首次会弹出浏览器让你登录。登录后脚本会自动抓取未读邮件列表发送给本地模型模型会生成点击“未读标记”的操作序列最后由Playwright自动执行。你就完成了一次AI驱动的邮箱操作5. 进阶优化与实战技巧基础版本跑通后你会发现很多可以优化和深挖的地方。这里分享几个从实战中总结的进阶技巧。5.1 提升模型指令遵循能力的Prompt技巧最初的Prompt可能效果不稳定。我们可以通过以下方式优化角色定义更清晰在Prompt开头强模型化角色。“你是一个严格遵守指令、且输出格式必须精确的邮箱自动化助手。”提供更详细的上下文除了邮件列表还可以告诉模型当前页面有哪些可操作的元素如“顶部有‘归档’、‘删除’、‘标记为已读’按钮”。结构化输入将邮件信息以更结构化的方式如JSON放在Prompt中有助于模型解析。输出格式强制约束使用类似JSON Schema的描述来约束输出。例如“你的输出必须是一个JSON数组每个对象必须有action和selector字段...”。后处理校验在解析模型输出后增加一个校验步骤检查action是否在允许列表中selector是否基本合法例如是否以.或#或[开头可以过滤掉明显的格式错误。5.2 处理动态页面与等待策略网页是动态的一个操作如点击“删除”可能触发异步请求、页面刷新或元素状态改变。鲁棒的自动化脚本必须有完善的等待策略。Playwright内置等待尽量使用locator.waitFor()或page.waitForSelector()而不是固定的page.waitForTimeout()。前者会在条件满足时立即继续效率更高。等待网络请求对于会触发网络请求的操作可以使用page.waitForResponse()或page.waitForEvent(requestfinished)来等待关键请求完成。自定义等待函数对于复杂的交互可以编写自定义等待函数轮询检查某个特定条件是否满足。async function waitForEmailListUpdate(page, initialCount) { for (let i 0; i 10; i) { // 最多尝试10次每次间隔1秒 await page.waitForTimeout(1000); const currentCount await page.locator(div[rolelistitem]).count(); if (currentCount ! initialCount) { return true; // 列表已更新 } } throw new Error(邮件列表未在预期时间内更新); }5.3 扩展技能实现更复杂的邮箱操作我们的框架是通用的可以轻松扩展支持更多指令。智能归档指令“将来自‘订阅通知’且标题包含‘月度’的邮件归档。” 这需要模型能理解更复杂的条件逻辑。自动回复指令“给所有未读邮件中来自‘客户支持’的发件人回复‘已收到我们将尽快处理’。” 这需要模型能生成点击“回复”按钮、定位输入框、填写内容、点击发送等一系列复杂操作序列。Prompt需要更详细地描述回复界面的结构。会议邀请提取与创建日历指令“找出邮件中的会议邀请并提取时间、地点添加到我的日历。” 这需要结合模型的信息提取能力从邮件正文提取结构化数据和CDP操作日历网站的能力。实现这些复杂技能的关键在于为每个技能设计专属的、上下文更丰富的Prompt并确保页面信息提取函数能提供足够的数据如邮件正文片段。5.4 错误处理与日志记录一个健壮的生产级助手必须有完善的错误处理和日志。操作失败重试对于点击等操作如果因为元素临时未准备好而失败可以加入重试机制。模型响应降级当模型返回非预期格式或内容时应有降级方案。例如可以准备一些基于规则的硬编码操作作为后备。详细日志记录每个阶段的关键信息抓取的邮件、发送的Prompt、模型回复、执行的操作、遇到的错误便于事后排查。可以使用winston或pino等日志库。截图功能在关键步骤或发生错误时使用page.screenshot()保存页面截图这是最直观的调试工具。6. 常见问题与排查实录在实际搭建和运行过程中你几乎一定会遇到下面这些问题。这里是我踩过坑后的解决方案实录。6.1 模型响应格式不正确或包含多余文本问题模型返回的内容除了JSON外还多了“json”代码块标记或解释性文字导致JSON.parse失败。排查首先打印出模型的原始回复 (modelResponse) 查看。解决强化Prompt约束在Prompt中明确强调“请只输出JSON数组不要有任何其他解释、标记或代码块包裹。”后处理清洗像我们代码中那样使用正则表达式modelResponse.match(/\[[\s\S]*\]/)来提取可能的JSON部分。这是一个非常实用的技巧。使用支持JSON模式的模型有些模型如DeepSeek Coder在指定format: json参数后会强制以JSON格式输出。6.2 Playwright无法定位元素Selector失效问题脚本报错TimeoutError: locator.waitFor: Timeout 5000ms exceeded找不到元素。排查确认页面是否加载正确在执行操作前先手动检查页面是否处于预期状态是否跳转了是否弹出了模态框。验证选择器在浏览器的开发者工具Console中输入document.querySelector(你的选择器)看是否能找到元素。网页结构可能已更新。检查iframe目标元素是否在iframe内如果是需要使用page.frameLocator()先定位到iframe。解决使用更稳健的选择器优先使用>
返回列表