-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
init:完全使用cursor问答开发的chrome AI极简网页翻译扩展
- Loading branch information
0 parents
commit 5f8e636
Showing
29 changed files
with
3,032 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
*bak** |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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进行翻译。 | ||
- 如果需要强制重新翻译,可以点击对应的清除缓存按钮后,重新翻译。 | ||
|
||
## 先看看效果 | ||
|
||
- 安装插件后,点击插件图标,右上角会显示出功能弹窗: | ||
|
||
 | ||
|
||
- 点击“设置”按钮,配置大模型平台地址、模型名、和 AK,**记得首次使用要保存设置才生效**。 | ||
|
||
 | ||
|
||
- 整页翻译:对比翻译的效果 | ||
|
||
 | ||
|
||
- 整页翻译:替换翻译的效果 | ||
|
||
 | ||
|
||
- 划词翻译:对只需要翻译网页中部分文本,在选中文本(划词)后,会出现一个小的“翻译”按钮,点击之后就会弹窗显示翻译结果,目标语言在右上角的配置面板中指定。 | ||
|
||
 | ||
|
||
- 如果是阅读 pdf 文件,或者也是一般网页,右键选择“AI 翻译助手-翻译选中文本”,会弹出独立翻译窗口。 | ||
|
||
 | ||
|
||
- 这个独立窗口可以当成个简单的翻译工具,复制需要翻译的内容,选择目标语言,然后随意翻译即可。 | ||
|
||
 | ||
|
||
## 其他补充 | ||
|
||
- 翻译效果和大模型质量相关 | ||
- 网页内容过大,可能翻译比较慢,只会翻译点击翻译时已经加载的内容 | ||
- 嵌入式(对比翻译)效果不一定好看 | ||
|
||
## 安装使用 | ||
|
||
下载这个项目,解压后,打开 chrome 或 edge浏览器,进入 `chrome://extensions/` 或`edge://extensions/`页面,点击“加载已解压的扩展程序”,选择解压后的文件夹即可。 | ||
|
||
首次使用一定点击“设置”按钮或者插件图标右键选“选项”,去配置 API 地址、AK 和模型名称,点击“保存设置”。 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} | ||
}); |
Oops, something went wrong.