1. 项目概述:一个浏览器书签,搞定所有主流AI对话导出
作为一名长期和各类AI助手打交道的博主,我深知一个痛点:和ChatGPT、Claude、Gemini这些工具聊了半天,产出了一堆有价值的代码、方案或者灵感,最后想整理归档或者分享给同事时,却异常麻烦。要么得一段段手动复制粘贴,格式全乱;要么想保存成PDF,却发现网页自带的“打印”功能会把侧边栏、导航栏这些无关内容全打进去,还得手动调整CSS,非常不优雅。
今天分享的这个叫“give-me/bookmarklets”的小工具,完美解决了这个问题。它本质上是一个浏览器书签工具,我习惯叫它“一键导出小助手”。你只需要把这个工具保存为浏览器书签,之后在任何支持的AI对话页面(目前完美支持ChatGPT、Claude、Gemini和Grok)点一下这个书签,就能把整段对话,连同可能存在的代码块、文件预览等“附加内容”,一键导出为干净的PDF或格式清晰的纯文本文件。整个过程完全在本地浏览器中运行,不经过任何第三方服务器,你的对话数据百分百不会泄露。
它特别适合需要频繁整理AI对话内容的朋友,比如程序员存档技术讨论、学生保存学习记录、内容创作者收集灵感素材。接下来,我会详细拆解它的工作原理、手把手教你如何部署使用,并分享一些我深度使用后总结的独家技巧和避坑指南。
2. 核心原理与设计思路拆解
这个工具虽然用起来简单,但背后的设计思路非常巧妙,充分考虑了不同场景下的用户体验和安全性。它不是一个大而全的浏览器插件,而是一个轻量级的“书签小程序”,这个选择本身就很有讲究。
2.1 为何选择Bookmarklet而非浏览器插件?
首先,我们得明白Bookmarklet是什么。它是一段以javascript:开头的代码,保存在浏览器书签栏里。点击时,这段代码会在当前页面的上下文中执行。相比于浏览器插件,它有三大优势:
- 极致的轻量与便捷:无需安装,不占用浏览器后台资源,没有复杂的权限申请。就是一个书签,点一下就用。
- 绝对的安全与隐私:所有代码逻辑和数据处理都发生在你本地浏览器的内存中,执行完毕即消失。它不会像插件一样常驻后台,也无法访问你浏览器标签页之外的数据,从根本上杜绝了数据被上传到开发者服务器的风险。
- 无平台依赖与更新灵活:不受Chrome应用商店或Firefox插件中心的审核限制。一旦AI对话页面的HTML结构发生变化导致工具失效,开发者可以快速更新GitHub上的代码,用户只需替换书签中的URL即可更新,响应速度远快于插件审核上架。
当然,缺点也有,比如功能复杂度受限于单次执行的代码量,且无法进行复杂的后台交互。但对于“导出页面内容”这个单一、明确的需求,Bookmarklet是近乎完美的解决方案。
2.2 双引擎PDF导出策略的精妙之处
这个工具最核心的智慧体现在PDF导出上,它提供了“可搜索PDF”和“不可搜索PDF”两种模式,其底层采用了两种完全不同的技术方案,以适配不同AI平台的技术限制。
方案一:利用浏览器原生打印功能生成“可搜索PDF”这是默认且兼容性最好的方案。它的原理是:
- 工具通过CSS选择器,精准定位到网页中的对话主区域和可能的附加内容区域(如代码预览窗)。
- 在内存中创建一个临时的
<div>容器,并将找到的所有内容克隆一份放入其中。 - 动态插入一段仅针对打印生效的CSS样式,其核心规则是:在打印时,隐藏页面上所有其他元素,只显示我们创建的那个临时容器。
- 调用浏览器的
window.print()方法。此时,浏览器弹出的打印预览窗口里,就只有我们想要的纯净对话内容了。 - 用户选择“另存为PDF”,即可得到一个文本可被选中、搜索的PDF文件。之后,工具会自动清理掉临时创建的样式和容器。
这个方案的优点是生成的PDF质量高、文字可搜索、完全依赖浏览器自身能力,无需加载外部资源。但它的缺点是,生成的PDF样式(字体、布局)受限于浏览器打印引擎和用户打印设置。
方案二:引入html2pdf.js库生成“不可搜索PDF”这个方案主要作为备选,用于应对一些平台的安全策略。它的流程是:
- 当用户选择“生成不可搜索PDF”时,工具会从Cloudflare的公共CDN动态加载一个叫
html2pdf.js的第三方开源库。 - 该库会在浏览器内,将我们找到的HTML内容先渲染到Canvas画布上,再将Canvas转换为PDF文件。
- 由于本质上是将文字变成了图片,所以生成的PDF内的文字无法被直接选中和搜索。
这个方案看似是退而求其次,实则解决了关键问题:内容安全策略。像ChatGPT这样的网站,可能会设置严格的CSP,禁止页面执行eval或new Function等,这有时会干扰方案一的打印流程。而方案二通过加载外部库,绕开了这些限制,保证了功能的可用性。开发者通过一个csp标志位来智能判断当前网站是否需要启用此方案,非常贴心。
2.3 内容定位机制:CSS选择器的艺术
工具能否准确抓取内容,完全依赖于其对目标网站HTML结构的理解,即那一串串的CSS选择器。例如,对于Claude,它用div[data-test-render-count]来找到对话容器,用div[data-testid="user-message"]来定位用户消息。这要求开发者必须持续跟进这些AI产品的界面更新。
注意:这是此类工具最脆弱的环节。一旦ChatGPT或Claude的前端工程师改了某个
div的class名或>javascript:(function () { /* v. 0.12, github.com/give-me/bookmarklets */ let dialog, events = [], extras = [], csp = false; switch (location.hostname) { case 'claude.ai': dialog = document.querySelector('div[data-test-render-count]').parentElement; events = dialog.querySelectorAll('div[data-testid="user-message"], div[data-test-render-count]>div>div>div.font-claude-response'); extras.push(document.querySelector('div.h-full.top-0 div.font-mono')); extras.push(document.querySelector('div.h-full.top-0 div#wiggle-file-content')); extras.push(document.querySelector('div.h-full.top-0 div#markdown-artifact')); break; case 'chatgpt.com': dialog = document.querySelector('article').parentElement; events = dialog.querySelectorAll('div[data-message-author-role]'); extras.push(document.querySelector('section.popover>section')); csp = true; break; case 'grok.com': dialog = document.querySelector('div#last-reply-container').parentElement; events = dialog.querySelectorAll('div.message-bubble'); extras.push(document.querySelector('aside')); csp = true; break; case 'gemini.google.com': dialog = document.querySelector('#chat-history'); events = dialog.querySelectorAll('user-query-content, message-content'); extras.push(document.querySelector('code-immersive-panel>div.container')); extras.push(document.querySelector('deep-research-immersive-panel>div.container')); extras.push(document.querySelector('extended-response-panel response-container')); csp = true; break; default: return alert(location.hostname + ' is not supported'); } events = [...events].filter(Boolean); extras = [...extras].filter(Boolean); console.group(`Found elements at ${location.hostname}:`); console.debug('dialog', dialog); console.debug('events', events); console.debug('extras', extras); console.groupEnd(); let blocks = [dialog, ...extras]; let ts = new Date().toISOString().replace(/[-:T.]/g, '').slice(0, 14); if (confirm('Confirm if you prefer to export PDF instead of text')) { if (csp || confirm('Confirm if the PDF should be searchable')) { let temp = document.createElement('div'); temp.id = 'id-' + Math.random().toString(36).slice(2, 9); blocks.forEach(el => temp.appendChild(el.cloneNode(true))); let style = document.createElement('style'); style.textContent = `@media print{body>*{display:none!important}#${temp.id}{display:flex!important;flex-direction:column}}`; document.head.appendChild(style); document.body.appendChild(temp); print(); setTimeout(() => { document.head.removeChild(style); document.body.removeChild(temp); }, 1000); } else { let script = document.createElement('script'); script.src = 'https://cdnjs.cloudflare.com/ajax/libs/html2pdf.js/0.12.1/html2pdf.bundle.min.js'; script.onload = function () { let pdf = html2pdf().set({ margin: 5, filename: `${ts}.pdf`, html2canvas: {scale: 2, logging: false} }).from(blocks.shift()); blocks.forEach(el => pdf = pdf.toPdf().get('pdf').then(pdfObj => pdfObj.addPage()).from(el).toContainer().toCanvas().toPdf()); pdf.save(); }; document.body.appendChild(script); } } else { let txt = events.map((e, i) => `# ${i % 2 ? 'AI' : 'Me'}:\n\n${e.innerText.trim()}\n\n`).join(''); txt += extras.map((e, i) => `# Extra ${i + 1}:\n\n${e.innerText.trim()}\n\n`).join(''); let href = URL.createObjectURL(new Blob(['\uFEFF', txt], {type: 'text/plain;charset=utf-8'})); let link = Object.assign(document.createElement('a'), {href: href, download: `${ts}.txt`}); link.click(); URL.revokeObjectURL(link.href); } })();
- 保存:点击“保存”按钮。此时,你的书签栏里应该出现了一个以你刚才命名的新书签。
实操心得:建议将这个书签保存在书签栏的显眼位置,或者将其添加到浏览器工具栏(如果浏览器支持)。因为代码较长,在书签管理器里直接编辑容易出错,如果未来需要更新,更推荐的做法是删除旧书签,然后重新创建。
3.2 在不同AI平台上的使用流程
书签创建好后,使用就非常简单了,基本是“哪里需要点哪里”。
- 打开目标对话:首先,在浏览器中打开你想要导出的AI对话页面。确保页面已经完全加载完毕,对话历史滚动到了你需要的位置。
- 点击书签:点击你刚刚创建的那个书签。
- 跟随提示操作:
- 首先会弹出一个对话框,询问“Confirm if you prefer to export PDF instead of text”。意思是“确认是否要导出PDF而非文本”。点击“确定”则进入PDF导出流程,点击“取消”则直接导出为文本文件。
- 如果选择导出文本:工具会立即生成一个
.txt文件并触发下载。文件内容会清晰地用# Me:和# AI:来区分对话角色,附加内容也会单独标注,格式非常整洁。- 如果选择导出PDF:会弹出第二个对话框,询问“Confirm if the PDF should be searchable”。意思是“确认PDF是否需要可搜索”。
- 点击“确定”:工具会尝试使用方案一(浏览器打印)生成可搜索的PDF。此时会立即调起浏览器的打印预览窗口。你需要在这个打印窗口的“目标打印机”处,选择“另存为PDF”,然后点击保存。这是最关键的一步,很多新手会在这里愣住,以为出错了。
- 点击“取消”:工具会使用方案二(html2pdf.js)生成不可搜索的PDF。这个过程可能需要几秒钟来加载库和生成文件,完成后会自动下载。
在不同平台上的细微差别:
- Claude.ai: 体验最流畅,通常直接使用可搜索PDF方案,且能很好地捕获侧边栏的代码文件等内容。
- ChatGPT.com: 由于CSP限制,通常会触发不可搜索PDF方案。如果你在点击书签后没有立即弹出打印窗口,而是浏览器看起来“卡”了一下,稍等片刻就会开始下载PDF,这是正常现象。
- Gemini.google.com: 行为与ChatGPT类似。
- Grok.com: 根据其页面安全策略,也可能触发不可搜索PDF方案。
4. 高级技巧与深度定制解析
如果你不满足于基本使用,想更深入地掌控这个工具,或者解决一些特定问题,下面这些技巧会很有帮助。
4.1 理解并验证内容抓取
工具执行时,会在浏览器的开发者控制台输出调试信息。你可以按
F12打开开发者工具,切换到Console标签页,然后再点击书签。你会看到类似这样的日志:Found elements at chatgpt.com: dialog: <div>...</div> events: NodeList(20) [div, div, div, ...] extras: [section.popover>section]这非常有用!
events的数量代表了它找到了多少条对话消息。如果这里显示为0或数量远少于实际对话条数,说明CSS选择器可能已经失效,工具需要更新了。extras数组则显示了它找到了哪些额外的内容面板(比如上传的文件预览区)。4.2 自定义文件名与导出内容
工具生成的PDF或文本文件,默认以时间戳命名,格式如
20240415123045.pdf。如果你希望文件名能体现对话主题,可以在浏览器打印预览窗口(可搜索PDF方案)中手动修改“文件名”字段再保存。对于文本导出,工具会导出它所能抓取到的所有对话历史。如果你只想导出部分内容,一个变通的方法是:先手动将网页滚动到你想要开始导出的那条消息附近,确保这些消息已经被加载到DOM中,然后再点击书签。不过,它无法智能地只导出“最后10条”或“选中部分”,这是Bookmarklet这种轻量级形式的局限。
4.3 处理导出PDF的样式问题
使用“可搜索PDF”方案时,最终的样式取决于你的浏览器打印设置。如果你对默认的PDF样式不满意(比如字体太小、背景色被打印出来),可以在打印预览窗口中进行调整:
- 布局:通常选择“纵向”即可。
- 纸张大小:A4是通用选择。
- 边距:可以选择“无”以获得最大内容区域,或者“最小值”。
- 选项:务必勾选“背景图形”。如果不勾选,Claude、ChatGPT等深色背景下的文字可能会变成白底白字而无法显示。勾选后,才能正确打印出文字颜色。
- 缩放:保持100%即可。
这些设置会被浏览器记住,下次调用打印时一般会沿用,所以通常只需配置一次。
4.4 如何手动更新失效的书签
正如前面原理部分提到的,当AI网站改版,工具可能会失效。表现通常是点击书签后弹窗提示“xxx is not supported”或者导出的内容为空。
这时,你需要手动更新书签中的代码:
- 访问该工具的GitHub源码页面:
https://github.com/give-me/bookmarklets/blob/main/bookmarklets/export.js- 找到页面上最新的、完整的
javascript:代码(通常以javascript:(function(){...})();的形式包裹)。- 完全复制这段新代码。
- 回到浏览器书签管理器,找到原来的书签,编辑它,用新代码完全替换掉“网址”字段里的旧代码。
- 保存。这样就完成了更新。
个人经验:建议关注一下这个GitHub仓库,甚至可以点个Star。这样当工具失效时,你能第一时间想到可能是网站改版了,并知道去哪里获取更新。这是使用这类开源小工具的必备素养。
5. 常见问题排查与解决方案实录
在实际使用中,你可能会遇到一些问题。下面是我总结的一些常见情况及解决方法。
5.1 点击书签没有任何反应
这是最常见的问题,通常原因和解决方法如下:
问题现象 可能原因 解决方案 点击书签,页面毫无反应,无弹窗。 1. 书签代码复制不完整,首尾缺失或中间有换行。
2. 浏览器安全策略禁止了书签栏执行大量JS代码。1.重新复制粘贴:确保从 javascript:开始,到最后的})();结束,完整且中间无换行。最好在纯文本编辑器里检查一遍。
2.尝试在地址栏执行:将书签代码完整复制,粘贴到浏览器的地址栏中,然后按回车。如果这时能运行,说明书签本身有问题,删除后重新创建。地址栏执行后报语法错误。 代码在传输过程中可能被意外修改(如邮件、聊天软件自动格式化)。 始终从项目的官方GitHub页面或可信的文章中直接复制代码源。 仅在某些网站上无反应。 该网站可能使用了严格的CSP,阻止了某些内联脚本执行方式。 尝试使用“不可搜索PDF”选项(如果弹窗能出现的话)。或者,检查控制台是否有CSP报错。 5.2 导出内容不完整或错乱
问题现象 可能原因 解决方案 导出的PDF/文本只包含最近几条消息。 AI聊天界面是“无限滚动”加载的,未滚动查看的历史消息并未被加载到DOM中。 在点击书签前,手动向上滚动页面,直到所有你需要导出的历史对话都出现在屏幕上。确保它们被加载出来。 导出的文本中“Me”和“AI”角色标记错乱。 工具通过消息在列表中的奇偶索引位置来判断角色,如果页面结构复杂,可能有干扰元素。 检查控制台输出的 events数量是否正确。如果错乱严重,可能是网站结构已大变,需等待工具更新。临时方案是导出后手动校对。附加内容(如代码文件)没有被导出。 该附加内容面板未被工具内置的CSS选择器捕获,或者面板处于隐藏/折叠状态。 在点击书签前,确保相关的附加内容面板是展开可见的。例如,在Claude中点击了上传的文件,让预览窗显示出来。 5.3 PDF相关的问题
问题现象 可能原因 解决方案 选择“可搜索PDF”后,打印预览窗口内容空白。 临时创建的容器样式可能未生效,或被页面更高优先级的样式覆盖。 1. 在打印预览窗口的“更多设置”中,确认已勾选“背景图形”。
2. 尝试使用“不可搜索PDF”方案。“不可搜索PDF”生成时间很长,或浏览器卡死。 对话历史非常长, html2pdf.js在将大量HTML渲染到Canvas时消耗了大量资源。1. 耐心等待,长对话可能需要数十秒。
2. 考虑分批导出,或先导出为文本。
3. 检查控制台是否有JS错误。PDF文件很大。 “不可搜索PDF”本质是图片,分辨率高(scale: 2)会导致文件体积大。 如果对文件大小敏感,优先使用“可搜索PDF”方案。对于不可搜索方案,可以尝试修改代码中的 html2canvas: {scale: 2},将2改为1,但会降低清晰度。5.4 浏览器兼容性与安全警告
问题现象 说明与解决方案 在Safari上可能有限制。 Safari对书签执行JS代码有时限制更严。确保在Safari的“偏好设置”->“高级”中,勾选了“在菜单栏中显示开发菜单”,然后在“开发”菜单中确保“允许JavaScript来自智能搜索字段”是启用的。 浏览器提示“此网页正在尝试加载不安全的脚本”。 当使用“不可搜索PDF”方案时,会从Cloudflare CDN加载 html2pdf.js库。浏览器可能会弹出警告。Cloudflare CDN是广泛使用的可信源,可以放心点击“加载”或“允许”。这是实现本地转换功能的必要步骤,代码本身不会外传你的数据。这个工具是我目前用过最优雅、最轻量的AI对话导出方案。它把复杂的功能封装成了一个简单的书签,真正做到了“开箱即用,用完即走”。它的存在提醒我们,很多时候解决问题不需要重型的软件或插件,一段精心设计的脚本就能极大提升效率。当然,保持对工具更新机制的关注,理解其工作原理以应对偶尔的失效,是享受这种轻量化便利的同时,需要承担的一点小责任。如果你也经常需要整理和保存与AI的对话,强烈建议花五分钟设置一下,它会成为你浏览器里一个低调但无比实用的效率神器。