Skip to content

Commit

Permalink
init:完全使用cursor问答开发的chrome AI极简网页翻译扩展
Browse files Browse the repository at this point in the history
  • Loading branch information
Sanotsu committed Nov 30, 2024
0 parents commit 5f8e636
Show file tree
Hide file tree
Showing 29 changed files with 3,032 additions and 0 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
*bak**
54 changes: 54 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
# 30 分钟,使用 cursor 开发的一个极简低配版"沉浸式翻译"chrome 插件

## 说明

- 这个插件是使用 cursor 的 composer 功能开发的,我本人是完全不懂 chrome 插件开发的。
- 翻译功能是基于调用在线服务大模型 API 实现的,所以需要使用者有可用的平台地址和 ak。
- 因为是调用大模型API进行翻译,可能完全翻译完会比较慢。
- 可以试用下我写在代码里的是硅基流动中免费的模型 `Qwen/Qwen2.5-7B-Instruct`
- 当然,如果对硅基流动平台感兴趣,还能用下我的邀请码注册,那就更好了:
- https://cloud.siliconflow.cn/i/tRIcST68
- 整页翻译的内容会保存在缓存中,1小时内同一个网站不会重复调用API进行翻译。
- 如果需要强制重新翻译,可以点击对应的清除缓存按钮后,重新翻译。

## 先看看效果

- 安装插件后,点击插件图标,右上角会显示出功能弹窗:

![点击插件按钮出现的嵌入式页面](./screenshots/点击插件按钮出现的嵌入式页面.png)

- 点击“设置”按钮,配置大模型平台地址、模型名、和 AK,**记得首次使用要保存设置才生效**

![自定义大模型API地址和ak](./screenshots/自定义大模型API地址和ak.png)

- 整页翻译:对比翻译的效果

![对比翻译示例页面](./screenshots/对比翻译示例页面.png)

- 整页翻译:替换翻译的效果

![替换翻译示例页面](./screenshots/替换翻译示例页面.png)

- 划词翻译:对只需要翻译网页中部分文本,在选中文本(划词)后,会出现一个小的“翻译”按钮,点击之后就会弹窗显示翻译结果,目标语言在右上角的配置面板中指定。

![划词翻译](./screenshots/划词翻译.png)

- 如果是阅读 pdf 文件,或者也是一般网页,右键选择“AI 翻译助手-翻译选中文本”,会弹出独立翻译窗口。

![pdf右键](./screenshots/pdf右键.png)

- 这个独立窗口可以当成个简单的翻译工具,复制需要翻译的内容,选择目标语言,然后随意翻译即可。

![独立翻译弹窗](./screenshots/独立翻译弹窗.png)

## 其他补充

- 翻译效果和大模型质量相关
- 网页内容过大,可能翻译比较慢,只会翻译点击翻译时已经加载的内容
- 嵌入式(对比翻译)效果不一定好看

## 安装使用

下载这个项目,解压后,打开 chrome 或 edge浏览器,进入 `chrome://extensions/``edge://extensions/`页面,点击“加载已解压的扩展程序”,选择解压后的文件夹即可。

首次使用一定点击“设置”按钮或者插件图标右键选“选项”,去配置 API 地址、AK 和模型名称,点击“保存设置”。
194 changes: 194 additions & 0 deletions background/background.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,194 @@
// 获取特定标签页的设置
const getTabSettings = async (tabId) => {
return new Promise((resolve) => {
chrome.storage.local.get(
{
[`targetLang_${tabId}`]: 'zh' // 获取特定标签页的目标语言
},
async (tabItems) => {
// 获取全局API设置
const globalSettings = await chrome.storage.sync.get({
apiEndpoint: '', // 移除默认值
apiKey: '',
model: ''
});

resolve({
...globalSettings,
targetLang: tabItems[`targetLang_${tabId}`]
});
}
);
});
};

// 调用API进行翻译
const translateWithAPI = async (text, targetLang, apiKey, model, apiEndpoint) => {
// 检查必要的API设置
if (!apiEndpoint || !apiKey || !model) {
throw new Error('请先在设置页面配置API信息');
}

const response = await fetch(apiEndpoint, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${apiKey}`
},
body: JSON.stringify({
model: model,
messages: [
{
role: "system",
content: `你是一个翻译助手。请将用户输入的文本翻译成${targetLang},只返回翻译结果,不需要解释。`
},
{
role: "user",
content: text
}
],
temperature: 0.3
})
});

if (!response.ok) {
throw new Error('翻译请求失败');
}

const data = await response.json();
return data.choices[0].message.content.trim();
};

// 监听来自content script的消息
chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
if (request.action === 'translate') {
(async () => {
try {
let settings;
if (request.isPopupWindow) {
// 如果是独立窗口的请求,获取全局API设置
settings = await chrome.storage.sync.get({
apiEndpoint: '',
apiKey: '',
model: ''
});
settings.targetLang = request.targetLang;
} else {
// 否则使用标签页的设置
settings = await getTabSettings(request.tabId);
}

const translation = await translateWithAPI(
request.text,
settings.targetLang,
settings.apiKey,
settings.model,
settings.apiEndpoint
);
sendResponse({ translation });
} catch (error) {
sendResponse({ error: error.message });
}
})();
return true;
} else if (request.action === 'getCurrentTabId') {
sendResponse({ tabId: sender.tab?.id });
return false;
}
});

// 存储面板状态(使用对象而不是 Map)
let panelStates = {};

// 添加新的消息处理
chrome.action.onClicked.addListener(async (tab) => {
const tabId = tab.id;

try {
// 检查当前标签页是否已有面板
const [result] = await chrome.scripting.executeScript({
target: { tabId: tabId },
func: togglePanel,
args: [tabId, !!panelStates[tabId]] // 转换为布尔值
});

// 更新面板状态
panelStates[tabId] = result.result;
} catch (error) {
console.error('面板切换失败:', error);
}
});

// 监听标签页关闭事件
chrome.tabs.onRemoved.addListener((tabId) => {
delete panelStates[tabId];
});

// 面板切换函数
function togglePanel(tabId, isVisible) {
let panel = document.querySelector(`.translator-panel[data-tab-id="${tabId}"]`);

if (panel) {
panel.style.display = panel.style.display === 'none' ? 'block' : 'none';
return panel.style.display === 'block';
} else {
const iframe = document.createElement('iframe');
iframe.className = 'translator-panel';
iframe.setAttribute('data-tab-id', tabId);
iframe.src = chrome.runtime.getURL('content/panel.html');
iframe.style.display = 'block';
document.body.appendChild(iframe);
return true;
}
}

let translateWindow = null;

// 创建右键菜单
chrome.runtime.onInstalled.addListener(() => {
chrome.contextMenus.create({
id: 'translateSelection',
title: 'AI翻译助手 - 翻译选中文本',
contexts: ['selection']
});
});

// 处理右键菜单点击
chrome.contextMenus.onClicked.addListener(async (info, tab) => {
if (info.menuItemId === 'translateSelection') {
const selectedText = info.selectionText;

if (!translateWindow) {
// 创建新窗口
const window = await chrome.windows.create({
url: chrome.runtime.getURL('translate/translate.html'),
type: 'popup',
width: 800,
height: 600
});
translateWindow = window.id;

// 等待窗口加载完成
setTimeout(() => {
chrome.runtime.sendMessage({
action: 'updateText',
text: selectedText
});
}, 1000);
} else {
// 更新已存在的窗口
chrome.windows.update(translateWindow, { focused: true });
chrome.runtime.sendMessage({
action: 'updateText',
text: selectedText
});
}
}
});

// 监听窗口关闭
chrome.windows.onRemoved.addListener((windowId) => {
if (windowId === translateWindow) {
translateWindow = null;
}
});
Loading

0 comments on commit 5f8e636

Please sign in to comment.