当前位置: 首页 > news >正文

用TRAE编程助手编写一个浏览器插件

        我经常在浏览器中打开很多网页却没有时间看(实际是懒:-)),如果关掉又怕未来有用时找不到,而如果加入收藏夹,那就基本代表着遗忘了。在关掉可惜,开着又占标签栏空间的两难之间,我准备搞一个“待浏览“插件,将所有已经打开的网页一次性全放进去。然后,就可以把除当前页面外的所有网页标签关闭,这下世界清静了。

        chrome浏览器插件编写我只是知道大概,要写出一个完整能用的插件,按以往的经验,没有两天的时间编码和调试是搞不出来的。所以我决定尝试用trae编程助手来完成开发工作。而让我惊喜的是,当我用提示词分步提出要求后,trae完成了一个完整可用,且一次跑通的插件。而我除了readme.md文件之外,其他html、js、css、josn文件,全部由trae AI自动生成,代码人工“零修改”!

      一、使用trae开发这个叫unview插件的过程

        首先在用trae打一个空文件夹,然后在trae AI侧栏输入提示词。项目分几步完成,每一步的提示词如下:

        第一步提示词:

        我想编写一个Chrome插件,将浏览器当前打开的网面全部放入一个“待浏览”列表,并按日期分组,同时要去除重复的网址。

        第二步提提示词

        现在这个插件已经能够加入待浏览的网址,并按日期分组和去重。我需要在此基础上进一步完善:

  1. 待浏览的网址要能够保存在本地,并在打开插件时,自动加载到插件中。
  2. 加入按网址标题搜索定位的功能。

        第三步提示词

        数据存储在chrome.storage.local中可能会被清理,应该加入保存在本地系统文件中的功能。

        第四步提示词

        现在进一步完善:

  1. 日期分组应该可以折叠,以方便在多个日期间导航
  2. 增加一个关闭除当前而外其他所有页面的功能
  3. 优化功能按钮布局和大小,让功能按钮与列表区有明确的界限

        第五步提示词

        重新生成readme.md

通过上述五步与trae AI助手的交互,trae完整生成了插件框架和相应代码,甚至readme.md都帮助写好了。

二、项目结构

d:\MyProg\browerExt\
├── README.md
└── un_viwe\
    ├── README.md
    ├── images\
    │   ├── icon128.png
    │   ├── icon16.png
    │   └── icon48.png
    ├── manifest.json
    ├── popup.html
    ├── popup.js
    ├── styles.css
    └── unview.png

三、相关代码

1、popup.html

<!DOCTYPE html>
<html lang="zh-CN">
<head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>待浏览列表</title><link rel="stylesheet" href="styles.css">
</head>
<body><div class="container"><h1>待浏览列表</h1><div class="search-container"><input type="text" id="searchInput" placeholder="搜索网址标题..."></div><div class="button-group"><div class="buttons"><button id="addCurrentPage">添加当前页面</button><button id="addAllTabs">添加所有标签页</button><button id="removeDuplicates">去重</button></div><div class="buttons"><button id="closeOtherTabs">关闭其他标签页</button><button id="exportData">导出数据</button><button id="importData">导入数据</button></div><input type="file" id="fileInput" accept=".json" style="display: none;"></div><div id="listContainer" class="list-container"><!-- 待浏览列表将在这里显示 --></div></div><script src="popup.js"></script>
</body>
</html>

2.popup.js

// 获取DOM元素
const addCurrentPageBtn = document.getElementById('addCurrentPage');
const addAllTabsBtn = document.getElementById('addAllTabs');
const removeDuplicatesBtn = document.getElementById('removeDuplicates');
const listContainer = document.getElementById('listContainer');
const searchInput = document.getElementById('searchInput');
const exportDataBtn = document.getElementById('exportData');
const importDataBtn = document.getElementById('importData');
const fileInput = document.getElementById('fileInput');
const closeOtherTabsBtn = document.getElementById('closeOtherTabs');// 添加搜索功能事件监听
searchInput.addEventListener('input', () => {loadUrls(searchInput.value.toLowerCase());
});// 导出数据到本地文件
exportDataBtn.addEventListener('click', async () => {try {const data = await chrome.storage.local.get('urlsByDate');const urlsByDate = data.urlsByDate || {};// 将数据转换为JSON字符串const jsonData = JSON.stringify(urlsByDate, null, 2);// 创建Blob对象const blob = new Blob([jsonData], { type: 'application/json' });const url = URL.createObjectURL(blob);// 创建下载链接const a = document.createElement('a');a.href = url;// 以当前日期作为文件名const today = new Date().toISOString().split('T')[0];a.download = `待浏览列表备份_${today}.json`;document.body.appendChild(a);a.click();// 清理setTimeout(() => {document.body.removeChild(a);URL.revokeObjectURL(url);}, 100);showNotification('数据已成功导出到本地文件');} catch (error) {console.error('导出数据失败:', error);showNotification('导出数据失败,请重试');}
});// 关闭除当前页面外的其他所有标签页
closeOtherTabsBtn.addEventListener('click', async () => {try {// 获取当前窗口的所有标签页const tabs = await chrome.tabs.query({ currentWindow: true });// 获取当前活动标签页const activeTab = await chrome.tabs.query({ active: true, currentWindow: true });if (tabs.length <= 1) {showNotification('没有其他标签页可关闭');return;}// 显示确认对话框if (confirm(`确定要关闭除当前页外的 ${tabs.length - 1} 个标签页吗?`)) {// 收集所有非活动标签页的IDconst tabsToClose = tabs.filter(tab => tab.id !== activeTab[0].id).map(tab => tab.id);// 关闭这些标签页await chrome.tabs.remove(tabsToClose);showNotification(`已关闭 ${tabsToClose.length} 个标签页`);}} catch (error) {console.error('关闭其他标签页失败:', error);showNotification('关闭其他标签页失败,请重试');}
});// 导入数据从本地文件
importDataBtn.addEventListener('click', () => {fileInput.click();
});fileInput.addEventListener('change', async (e) => {try {const file = e.target.files[0];if (!file) return;// 检查文件类型if (!file.name.endsWith('.json')) {showNotification('请选择JSON格式的文件');fileInput.value = ''; // 重置文件输入return;}// 读取文件内容const reader = new FileReader();reader.onload = async (event) => {try {const jsonData = event.target.result;const urlsByDate = JSON.parse(jsonData);// 显示确认对话框,警告用户导入会覆盖现有数据if (confirm('导入数据将会覆盖现有列表,确定要继续吗?')) {// 保存导入的数据await chrome.storage.local.set({ urlsByDate });showNotification('数据已成功导入');loadUrls(); // 重新加载列表}} catch (parseError) {console.error('解析文件失败:', parseError);showNotification('文件格式错误,无法导入数据');}};reader.onerror = () => {console.error('读取文件失败');showNotification('读取文件失败,请重试');};reader.readAsText(file);} catch (error) {console.error('导入数据失败:', error);showNotification('导入数据失败,请重试');} finally {fileInput.value = ''; // 重置文件输入}
});// 初始化页面时加载待浏览列表
loadUrls();// 添加当前页面到待浏览列表
addCurrentPageBtn.addEventListener('click', async () => {try {const tabs = await chrome.tabs.query({ active: true, currentWindow: true });if (tabs && tabs.length > 0) {const currentTab = tabs[0];await addUrlToList(currentTab.url, currentTab.title);showNotification('当前页面已添加到待浏览列表');}} catch (error) {console.error('添加当前页面失败:', error);}
});// 添加所有标签页到待浏览列表
addAllTabsBtn.addEventListener('click', async () => {try {const tabs = await chrome.tabs.query({ currentWindow: true });if (tabs && tabs.length > 0) {const urlsAdded = [];for (const tab of tabs) {if (tab.url && !tab.url.startsWith('chrome://')) {await addUrlToList(tab.url, tab.title);urlsAdded.push(tab.url);}}showNotification(`已添加 ${urlsAdded.length} 个标签页到待浏览列表`);}} catch (error) {console.error('添加所有标签页失败:', error);}
});// 去重功能
removeDuplicatesBtn.addEventListener('click', async () => {try {const data = await chrome.storage.local.get('urlsByDate');const urlsByDate = data.urlsByDate || {};// 收集所有URL并检测重复const allUrls = new Map(); // key: url, value: {date, title}const duplicateUrls = new Set();for (const [date, urls] of Object.entries(urlsByDate)) {for (const urlInfo of urls) {if (allUrls.has(urlInfo.url)) {duplicateUrls.add(urlInfo.url);} else {allUrls.set(urlInfo.url, { date, title: urlInfo.title });}}}if (duplicateUrls.size === 0) {showNotification('没有找到重复的URL');return;}// 显示确认对话框const confirmMessage = `找到 ${duplicateUrls.size} 个重复的URL,确定要删除重复项吗?`;if (confirm(confirmMessage)) {// 创建新的去重后的URL数据结构const newUrlsByDate = {};// 遍历去重后的URL集合for (const [url, info] of allUrls.entries()) {if (!newUrlsByDate[info.date]) {newUrlsByDate[info.date] = [];}newUrlsByDate[info.date].push({ url, title: info.title });}// 保存去重后的数据await chrome.storage.local.set({ urlsByDate: newUrlsByDate });showNotification(`已删除 ${duplicateUrls.size} 个重复的URL`);loadUrls(); // 重新加载列表}} catch (error) {console.error('去重失败:', error);}
});// 添加URL到列表
async function addUrlToList(url, title) {try {const data = await chrome.storage.local.get('urlsByDate');const urlsByDate = data.urlsByDate || {};// 获取当前日期(YYYY-MM-DD格式)const today = new Date().toISOString().split('T')[0];// 初始化今天的数组(如果不存在)if (!urlsByDate[today]) {urlsByDate[today] = [];}// 检查URL是否已存在(在当前日期下)const urlExists = urlsByDate[today].some(urlInfo => urlInfo.url === url);if (!urlExists) {urlsByDate[today].push({ url, title: title || url });await chrome.storage.local.set({ urlsByDate });loadUrls(); // 重新加载列表}} catch (error) {console.error('添加URL失败:', error);}
}// 加载并显示待浏览列表
async function loadUrls(searchTerm = '') {try {const data = await chrome.storage.local.get('urlsByDate');const urlsByDate = data.urlsByDate || {};// 清空列表容器listContainer.innerHTML = '';// 如果没有URL,显示空消息if (Object.keys(urlsByDate).length === 0) {const emptyMessage = document.createElement('div');emptyMessage.className = 'empty-message';emptyMessage.textContent = '待浏览列表为空,点击上方按钮添加URL';listContainer.appendChild(emptyMessage);return;}// 获取所有日期并按降序排序const dates = Object.keys(urlsByDate).sort((a, b) => new Date(b) - new Date(a));let hasVisibleUrls = false;// 为每个日期创建分组for (const date of dates) {const urls = urlsByDate[date];// 创建日期分组容器const dateGroup = document.createElement('div');dateGroup.className = 'date-group';// 创建日期标题容器const dateHeader = document.createElement('div');dateHeader.className = 'date-header';// 创建折叠/展开按钮const toggleBtn = document.createElement('span');toggleBtn.className = 'toggle-btn';toggleBtn.textContent = '▼'; // 默认展开dateHeader.appendChild(toggleBtn);// 创建日期文本const dateText = document.createElement('span');dateText.className = 'date-text';dateText.textContent = formatDate(date);dateHeader.appendChild(dateText);dateGroup.appendChild(dateHeader);// 创建URL列表容器const urlsContainer = document.createElement('div');urlsContainer.className = 'urls-container';dateGroup.appendChild(urlsContainer);// 添加折叠/展开功能dateHeader.addEventListener('click', () => {const isExpanded = toggleBtn.textContent === '▼';toggleBtn.textContent = isExpanded ? '▶' : '▼';urlsContainer.style.display = isExpanded ? 'none' : 'block';});let hasVisibleUrlsInDate = false;// 为每个URL创建条目urls.forEach((urlInfo, index) => {// 根据搜索词过滤URLif (searchTerm && !urlInfo.title.toLowerCase().includes(searchTerm) && !urlInfo.url.toLowerCase().includes(searchTerm)) {return;}hasVisibleUrls = true;hasVisibleUrlsInDate = true;const urlItem = document.createElement('div');urlItem.className = 'url-item';// 创建URL链接const urlLink = document.createElement('a');urlLink.href = urlInfo.url;urlLink.target = '_blank';urlLink.textContent = urlInfo.title || urlInfo.url;urlItem.appendChild(urlLink);// 创建删除按钮const deleteBtn = document.createElement('button');deleteBtn.textContent = '删除';deleteBtn.addEventListener('click', (e) => {e.preventDefault(); // 阻止链接跳转deleteUrl(date, index);});urlItem.appendChild(deleteBtn);urlsContainer.appendChild(urlItem); // 将URL项添加到URL容器中});// 只有当该日期有可见的URL时,才添加到列表容器if (hasVisibleUrlsInDate) {listContainer.appendChild(dateGroup);}}// 如果搜索后没有匹配的URL,显示提示信息if (!hasVisibleUrls) {const noResultsMessage = document.createElement('div');noResultsMessage.className = 'empty-message';noResultsMessage.textContent = '没有找到匹配的URL';listContainer.appendChild(noResultsMessage);}} catch (error) {console.error('加载URL列表失败:', error);}
}// 删除指定的URL
async function deleteUrl(date, index) {try {const data = await chrome.storage.local.get('urlsByDate');const urlsByDate = data.urlsByDate || {};if (urlsByDate[date] && urlsByDate[date][index]) {// 从数组中删除指定索引的URLurlsByDate[date].splice(index, 1);// 如果该日期的数组为空,则删除该日期if (urlsByDate[date].length === 0) {delete urlsByDate[date];}// 保存更新后的数据await chrome.storage.local.set({ urlsByDate });loadUrls(); // 重新加载列表}} catch (error) {console.error('删除URL失败:', error);}
}// 格式化日期显示
function formatDate(dateString) {const date = new Date(dateString);const year = date.getFullYear();const month = (date.getMonth() + 1).toString().padStart(2, '0');const day = date.getDate().toString().padStart(2, '0');// 获取今天、昨天和明天的日期进行比较const today = new Date().toISOString().split('T')[0];const yesterday = new Date();yesterday.setDate(yesterday.getDate() - 1);const yesterdayString = yesterday.toISOString().split('T')[0];const tomorrow = new Date();tomorrow.setDate(tomorrow.getDate() + 1);const tomorrowString = tomorrow.toISOString().split('T')[0];if (dateString === today) {return `今天 ${year}-${month}-${day}`;} else if (dateString === yesterdayString) {return `昨天 ${year}-${month}-${day}`;} else if (dateString === tomorrowString) {return `明天 ${year}-${month}-${day}`;} else {return `${year}-${month}-${day}`;}
}// 显示通知
function showNotification(message) {// 创建一个简单的通知元素const notification = document.createElement('div');notification.style.position = 'fixed';notification.style.bottom = '10px';notification.style.right = '10px';notification.style.padding = '10px 15px';notification.style.backgroundColor = '#4285f4';notification.style.color = 'white';notification.style.borderRadius = '4px';notification.style.zIndex = '1000';notification.style.boxShadow = '0 2px 5px rgba(0,0,0,0.2)';notification.textContent = message;document.body.appendChild(notification);// 3秒后移除通知setTimeout(() => {notification.style.opacity = '0';notification.style.transition = 'opacity 0.5s';setTimeout(() => {document.body.removeChild(notification);}, 500);}, 3000);
}

3.styles.css

* {margin: 0;padding: 0;box-sizing: border-box;
}body {font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;width: 400px;max-height: 600px;background-color: #f5f5f5;
}.container {padding: 16px;
}h1 {font-size: 18px;margin-bottom: 16px;color: #333;text-align: center;
}.search-container {margin-bottom: 16px;padding-bottom: 16px;border-bottom: 2px solid #e0e0e0;
}#searchInput {width: 100%;padding: 8px 12px;border: 1px solid #ddd;border-radius: 4px;font-size: 14px;outline: none;transition: border-color 0.2s;margin-bottom: 12px;
}#searchInput:focus {border-color: #4285f4;
}.toggle-btn {margin-right: 8px;cursor: pointer;font-size: 12px;width: 16px;display: inline-block;text-align: center;
}date-text {flex: 1;
}.urls-container {display: block;
}.buttons {display: flex;gap: 8px;margin-bottom: 16px;
}button {flex: 1;padding: 8px 12px;border: none;border-radius: 4px;background-color: #4285f4;color: white;font-size: 14px;cursor: pointer;transition: background-color 0.2s;
}button:hover {background-color: #3367d6;
}button:active {transform: scale(0.98);
}.list-container {max-height: 450px;overflow-y: auto;
}.date-group {margin-bottom: 16px;background-color: white;border-radius: 8px;box-shadow: 0 1px 3px rgba(0,0,0,0.1);overflow: hidden;
}.date-header {background-color: #4285f4;color: white;padding: 8px 12px;font-weight: bold;font-size: 14px;display: flex;align-items: center;cursor: pointer;
}.date-text {flex: 1;
}.url-item {display: flex;align-items: center;padding: 8px 12px;border-bottom: 1px solid #eee;transition: background-color 0.2s;
}.url-item:last-child {border-bottom: none;
}.url-item:hover {background-color: #f9f9f9;
}.url-item a {flex: 1;color: #333;text-decoration: none;word-break: break-all;font-size: 13px;
}.url-item a:hover {text-decoration: underline;
}.url-item button {margin-left: 8px;padding: 4px 8px;background-color: #ea4335;font-size: 12px;
}.url-item button:hover {background-color: #d3392c;
}.empty-message {text-align: center;color: #999;padding: 20px;font-size: 14px;
}::-webkit-scrollbar {width: 6px;
}::-webkit-scrollbar-track {background: #f1f1f1;border-radius: 3px;
}::-webkit-scrollbar-thumb {background: #ccc;border-radius: 3px;
}::-webkit-scrollbar-thumb:hover {background: #aaa;
}

4.manifest.json

{"manifest_version": 3,"name": "待浏览列表","description": "将当前打开的网页地址归纳到待浏览列表,并按日期分组和去重功能","version": "1.0","action": {"default_popup": "popup.html","default_icon": {"16": "images/icon16.png","48": "images/icon48.png","128": "images/icon128.png"}},"permissions": ["tabs","storage"],"icons": {"16": "images/icon16.png","48": "images/icon48.png","128": "images/icon128.png"}
}

将上述代码放在一个单独的文件夹如d:\unview,并在其中创建一个images子目录,放上自己的3个icon文件,然后在chrome浏览器中打开扩展管理,打开”开发者柜式“,点击”加载已解压的扩展程序“(我用的360极速浏览器),在插件管理器上点选”常驻“,此插件就可以使用了。

项目的github地址:https://github.com/woxili880409/unview

四、TRAE自动生成firefox、edge同款插件

提示词如下:

“现在un_viwe在chrome浏览器中已经正常工作了,但是我想将这引插件也用在foxfire浏览器上,请在fox_unview文件夹中创建一个同样功能,在foxfire浏览器上工作的插件。”

提示词中甚至有错字,但trae用时8分钟,完成了代码平台转换,将Chrome API调用(chrome. )替换为Firefox兼容的browser. API调用。利用Firefox临时加载功能加载插件后,导入标签页等功能正常,但关闭其他标签和导入功能无法正常工作。

相同的提示词,替换成edge浏览器后,edge插件一次性生成,一次性通过。原因trae也给出了提示:“Edge浏览器支持Chrome的API“

AI,在个人项目开发上,已经势不可挡了......

http://www.dtcms.com/a/393389.html

相关文章:

  • 赋能工业未来:向成电子XC3576H工控主板多领域应用前景
  • Multi-Agent多智能体系统(三)
  • 【语法进阶】高级用法、贪婪与非贪婪
  • 15天见效的SEO优化方案
  • C语言基础【20】:指针7
  • IC 数字逻辑设计中的硬件算法 01 记
  • 《棒球运动联盟》国家级运动健将标准·棒球1号位
  • AAC 详解
  • 蚂蚁集团DIVER登顶BRIGHT榜首,开源多阶段推理检索范式
  • 2013/12 JLPT听力原文 问题四
  • 挑战与应对:轻量化 AI 算法的成长烦恼
  • FPGA基础 -- CDC(Clock Domain Crossing)实战教程
  • 低碳经济:碳汇——从生态固碳到金融资产的价值转化
  • QGC 通信模块架构梳理
  • Application接口拓展功能(三)
  • 【Python】错误和异常
  • 【状态机实现】初识——基于状态机实现的流程编排和Activiti、Camunda、Flowable等工作流的区别
  • SpringBoot自动配置核心原理
  • Python 中的 Builder 模式实践 —— 以 UserProfileBuilder 为例
  • 探秘陌讯AIGC检测算法优化:详解MPS加速与模型热重载的实现原理
  • 1.3 管道(Pipe)核心知识点总结
  • GLUE:自然语言理解评估的黄金基准
  • 第13章 智能监测-设备数据处理
  • GEO技术科普
  • B004基于三菱FX2NPLC智能自提柜控制系统仿真
  • MTK CPU温度调节一知半解
  • V90伺服驱动器“速度模式“双极性模拟量速度控制
  • 课前练习题-20250919
  • C++类与对象
  • 企业级Docker镜像仓库Harbor