本帖使用社区开源推广,符合推广要求。我申明并遵循社区要求的以下内容: 我的帖子已经打上 开源推广 标签: 是 我的开源项目完整开源,无未开源部分: 是 我的开源项目已链接认可 LINUX DO 社区: 是 我帖子内的项目介绍,AI生成、润色内容部分已截图发出: 是 以上选择我承诺是永久有效的,接受社区和佬友监督: 是 以下为项目介绍正文内容,AI生成、润色内容已使用截图方式发出 可以快速搜索/启动应用,添加代办,记录临时文字片段,也支持选中文字后直接调用 AI 处理,比如解释、翻译、总结、改写之类的操作。 项目地址: https://github.com/YoungHQ/easy_launcher 产品文档: https://younghq.github.io/easy_launcher/ 项目截图: 现在项目还在持续完善中,想发出来听听大家的真实反馈:欢迎试用、提 issue。 非常期待获得佬友的支持啊。 以前都是写的一些框架,第一次写工具开源,若有不足,还望多多指出,感谢感谢 1 个帖子 - 1 位参与者 阅读完整话题
天天都用来标记 openai,anthropic,x 上的英文单词,还是挺好用的。 地址: https://github.com/hapiman/vocab-plugin 贴一些图:
本帖使用社区开源推广,符合推广要求。我申明并遵循社区要求的以下内容: 我的帖子已经打上 开源推广 标签: 是 我的开源项目完整开源,无未开源部分: 是 我的开源项目已链接认可 LINUX DO 社区: 是 我帖子内的项目介绍,AI生成、润色内容部分已截图发出: 是 以上选择我承诺是永久有效的,接受社区和佬友监督: 是 以下为项目介绍正文内容,AI生成、润色内容已使用截图方式发出 BG 研究 karpathy 大神的知识库,第一次接触到了 obsidian 之前都是使用的 typora 来记 md 笔记,用 zotero 看 pdf 文档和批注,现在靠 obsidian 一个软件全部替代了 而且搭配自部署插件 obsidian-fast-note-sync ,能让我 linux , windows ,手机端都同步好 由于我用惯了 zotero 的 pdf translate 插件 于是基于obsidian插件开发指南,做了个差不多功能的插件: 项目地址: https://github.com/Zhruoshui/obsidian-selection-translator 功能说明: 划词翻译 通过命令面板、快捷键、左侧功能区按钮或 Markdown 编辑器右键菜单翻译选中的 Markdown 或可选择 PDF 文本(** 为了不跟 pdf 插件产生冲突,pdf 可以点击插件按钮打开悬浮窗来进行 **)。 悬浮窗保持打开时,继续选择其他 Markdown 或 PDF 文本会自动翻译新的选择。 可以在悬浮窗中编辑源文本后重新翻译。 只选择一个英文单词时会自动使用配置的词典服务商查询,并在可用时播放英音 / 美音发音。(** 推荐使用默认的有道词典,其他两个还需要做适配 **) 悬浮窗工作流 在可拖动、可调整大小的悬浮窗中显示流式翻译状态、错误和结果。 顶部使用紧凑图标按钮复制完整译文、重新翻译或关闭悬浮窗。 译文结果支持自由选择,可以只复制其中任意片段。 顶部布局兼顾桌面端和较窄的移动端屏幕。 服务商支持 用户自行选择文本翻译服务商。OpenAI 兼容服务商、Bing 翻译(Microsoft Translator)、Google 翻译、DeepL、百度翻译和有道翻译都是可选项。 根据当前选择的服务商配置所需凭据。 OpenAI 兼容服务商支持提示词、温度和流式输出。传统翻译 API 会在服务商请求完成后返回译文。 只选择一个英文单词时会自动使用配置的词典服务商。有道词典、必应词典和剑桥词典均可选择,且不需要 API 密钥。 翻译前可以测试服务商配置是否可用。 插件界面跟随 Obsidian 应用语言,目前支持 English 和简体中文。 安装 通过 BRAT 安装 本插件通过 GitHub release 作为 beta 插件分发。可以使用 Obsidian 的 BRAT 插件安装: 在 Obsidian 社区插件中安装并启用 BRAT 。 打开 Settings → BRAT → Beta Plugin List 。 选择 Add Beta plugin 。 输入这个仓库地址: https://github.com/Zhruoshui/obsidian-selection-translator 后续思考 偶然看到 wps 中的翻译功能,可以借鉴优化 基于上下文更加精准的翻译 基于划词所选的片段,能够基于内容追问,想当于关键词 AI 搜索学习 界面字体大小等的调整,更方便手机上使用 如果您喜欢可以加个 star 1 个帖子 - 1 位参与者 阅读完整话题
Cursor 订阅快到期了,额度还没用完。 于是本着不浪费,榨干它的最后一点剩余价值,开启「每天 vibe coding 一个小项目」模式——全程不敲一行代码,全靠对话让 AI 写。今天的成果:AI 页面总结,一款 Chrome 扩展。 欢迎大家来使用,star ,收藏 https://github.com/ljp-777/chrome-ai-summary
本帖使用社区开源推广,符合推广要求。我申明并遵循社区要求的以下内容: 我的帖子已经打上 开源推广 标签: 是 我的开源项目完整开源,无未开源部分: 是 我的开源项目已链接认可 LINUX DO 社区: 是 我帖子内的项目介绍,AI生成、润色内容部分已截图发出: 是 以上选择我承诺是永久有效的,接受社区和佬友监督: 是 以下为项目介绍正文内容,AI生成、润色内容已使用截图方式发出 纯vibe coding出来的一个全局划词助手工具。之前用cherry studio,很喜欢划词助手功能,但是cs又有点太重了,有时候划词快捷键按半天没反应。所以自己写了个单独轻量的划词助手工具。 以下为更新日志: 更新了截图功能,可以进行ai识图,钉在桌面上,调节大小和透明度,以及标注图片。(当成一个纯截图工具个人感觉也不错) 更新了追问功能,划词或者截图识别后,可以在结果窗口里继续追问ai。 github.com GitHub - zcx960/AIA-selection-assistan: 轻量级系统全局ai划词助手,支持自定义大模型API,多种划词模式 轻量级系统全局ai划词助手,支持自定义大模型API,多种划词模式 截图: 1 个帖子 - 1 位参与者 阅读完整话题
本帖使用社区开源推广,符合推广要求。我申明并遵循社区要求的以下内容: 我的帖子已经打上 开源推广 标签: 是 我的开源项目完整开源,无未开源部分: 是 我的开源项目已链接认可 LINUX DO 社区: 是 我帖子内的项目介绍,AI生成、润色内容部分已截图发出: 是 以上选择我承诺是永久有效的,接受社区和佬友监督: 是 以下为项目介绍正文内容,AI生成、润色内容已使用截图方式发出 项目地址: GitHub - yeahhe365/Gemini-Nexus: Gemini Nexus 是一款面向浏览器场景的 AI 助手扩展,集成 Gemini Web、Gemini API 与 OpenAI 兼容接口,支持网页上下文、图像处理、工具调用和 MCP 浏览器控制。 · GitHub YouTube 视频总结,点击视频时间戳一键跳转 图片快捷处理,生成图片自动去除 Gemini 水印 划词弹窗 支持独立聊天界面 网页总结 浏览器控制 更多功能等你发现 求 star、 支持 6 个帖子 - 6 位参与者 阅读完整话题
我最近自己搓了个很小的 Chrome 扩展,叫 Say It 。 起因挺简单的,我平时看英文网页、文档的时候,经常会碰到一些单词,只想听他的发音。 以往的做法是,用 google 或百度翻译,输入单词,再听发音。方便一些的就是用翻译插件,划词,然后弹出一个小窗口,再点击哪个小喇叭,当然也有些工具是翻译的时候同步发音的,但我使用发现有 1~2 秒的延迟,本来只是想确定一下发音,结果每次都被弹窗打断了阅读节奏。 所以我就做了一个只干这件事的小插件: 选中单词,直接读出来。 没有翻译弹窗,没有按钮,也没有其他乱七八糟的功能。 划一下,它就读;读完就安静,等待下一次划词。 现在我自己看英文内容的时候基本一直开着它,尤其是读文档、博客、论文的时候,遇到拿不准读音的词,顺手一划就行,对我来说,体验还比较好,图个省事。 功能很简单: 选中单词后立刻发音,不用再点小喇叭 没有弹窗,不挡内容 正在发音时,再选新词会马上切过去 用的是浏览器本地的 Speech Synthesis API 不需要注册,不联网,也不会上传文本 插件是免费的,已经上架 Chrome 商店了,获取方式: 谷歌商店直接搜:Say It 直接访问: https://chromewebstore.google.com/detail/say-it/bdnkdfdcoohbfkaagjebpapoclbfifba 如果你也经常看英文网页,只是想快速听一下单词怎么读,可以试试看。 欢迎反馈问题
我最近自己搓了个很小的 Chrome 扩展,叫 Say It 。 起因挺简单的,我平时看英文网页、文档的时候,经常会碰到一些单词,只想听他的发音。 以往的做法是,用 google 或百度翻译,输入单词,再听发音。方便一些的就是用翻译插件,划词,然后弹出一个小窗口,再点击哪个小喇叭,当然也有些工具是翻译的时候同步发音的,但我使用发现有 1~2 秒的延迟,本来只是想确定一下发音,结果每次都被弹窗打断了阅读节奏。 所以我就做了一个只干这件事的小插件: 选中单词,直接读出来。 没有翻译弹窗,没有按钮,也没有其他乱七八糟的功能。 划一下,它就读;读完就安静,等待下一次划词。 现在我自己看英文内容的时候基本一直开着它,尤其是读文档、博客、论文的时候,遇到拿不准读音的词,顺手一划就行,对我来说,体验还比较好,图个省事。 功能很简单: 选中单词后立刻发音,不用再点小喇叭 没有弹窗,不挡内容 正在发音时,再选新词会马上切过去 用的是浏览器本地的 Speech Synthesis API 不需要注册,不联网,也不会上传文本 插件是免费的,已经上架 Chrome 商店了,获取方式: 谷歌商店直接搜:Say It 直接访问: https://chromewebstore.google.com/detail/say-it/bdnkdfdcoohbfkaagjebpapoclbfifba 如果你也经常看英文网页,只是想快速听一下单词怎么读,可以试试看。 欢迎反馈问题
我最近自己搓了个很小的 Chrome 扩展,叫 Say It 。 起因挺简单的,我平时看英文网页、文档的时候,经常会碰到一些单词,只想听他的发音。 以往的做法是,用 google 或百度翻译,输入单词,再听发音。方便一些的就是用翻译插件,划词,然后弹出一个小窗口,再点击哪个小喇叭,当然也有些工具是翻译的时候同步发音的,但我使用发现有 1~2 秒的延迟,本来只是想确定一下发音,结果每次都被弹窗打断了阅读节奏。 所以我就做了一个只干这件事的小插件: 选中单词,直接读出来。 没有翻译弹窗,没有按钮,也没有其他乱七八糟的功能。 划一下,它就读;读完就安静,等待下一次划词。 现在我自己看英文内容的时候基本一直开着它,尤其是读文档、博客、论文的时候,遇到拿不准读音的词,顺手一划就行,对我来说,体验还比较好,图个省事。 功能很简单: 选中单词后立刻发音,不用再点小喇叭 没有弹窗,不挡内容 正在发音时,再选新词会马上切过去 用的是浏览器本地的 Speech Synthesis API 不需要注册,不联网,也不会上传文本 插件是免费的,已经上架 Chrome 商店了,获取方式: 谷歌商店直接搜:Say It 直接访问: https://chromewebstore.google.com/detail/say-it/bdnkdfdcoohbfkaagjebpapoclbfifba 如果你也经常看英文网页,只是想快速听一下单词怎么读,可以试试看。 欢迎反馈问题
这个功能是最近更新的2.1.1版本才开始有的 1 个帖子 - 1 位参与者 阅读完整话题
痛点来自日常用 mac 碰到不熟悉的英文,总要复制粘贴到翻译软件里,就想做一个类似浏览器划词插件的菜单栏小工具。 https://github.com/itaober/atst 功能 ⌥D 划词翻译:选中英文按一下,结果浮窗弹出 ⌥S 截图翻译:框选屏幕区域,识别 + 翻译 开箱即用:内置 Google + Microsoft ,免 API key 可选 AI:OpenAI 兼容格式,自配置 Base URL / Key / Model ,支持智能解析(多义词、IPA 音标、技术名词解释) macOS 26 Liquid Glass 适配 纯 Swift 零依赖,DMG 5MB 用你自己的 key ,免费、无订阅,Apache 2.0 开源 首次使用 绕过未知开发者警告(自签名应用):右键 app → 打开 → 弹窗里再点一次「打开」。或在 Terminal 跑一句: xattr -d com.apple.quarantine /Applications/atst.app 权限:⌥D 需要辅助功能权限(读选区),⌥S 需要屏幕录制权限。在「系统设置 → 隐私与安全性」里放行即可。 截图
划词翻译的自定义功能,升级之后忽然没掉了,是觉得影响收入了么 这理由,无法反驳 原文 (点击了解更多详细信息) 9 个帖子 - 6 位参与者 阅读完整话题
支持类似图形化的鼠标点击,文字显示也有不同的高亮 划词自动复制 但 opencode 的Desktop UI 很差 16 个帖子 - 13 位参与者 阅读完整话题
有佬友遇到同样的情况吗?之前好像还能正常翻译的。 7 个帖子 - 6 位参与者 阅读完整话题
自己折腾了一个 Tampermonkey 脚本,功能比较实用: 划词后出现 红点 ,点击直接翻译选中文字 旁边有 蓝点 ,点击可 全文翻译 全文翻译是 直接替换页面文本节点 支持 恢复原文 支持 多个 API 自动故障回退 点页面其它地方,红蓝点会自动消失 适合拿来自定义接第三方翻译/LLM API,用来看英文网页挺方便。 希望对大家有用,不用折腾沉浸式翻译了,这个就是沉浸式,简单方便好用。好用点赞啊。 // ==UserScript== // @name 划词翻译 + 全文翻译(红点/蓝点/恢复原文/自动回退) // @name:en Selection Translate + Full Page Translate // @namespace http://tampermonkey.net/ // @version 3.2 // @description 红点翻译划词,蓝点全文翻译。直接替换页面文本节点,支持恢复原文,支持多API自动故障回退。 // @author AI Assistant // @match *://*/* // @grant GM_xmlhttpRequest // @grant GM_addStyle // @connect * // @license MIT // ==/UserScript== (function () { 'use strict'; if (window.__AI_TRANSLATOR_FULLPAGE_RUNNING__) { console.log('翻译脚本已在运行,本次加载被阻止。'); return; } window.__AI_TRANSLATOR_FULLPAGE_RUNNING__ = true; const API_PROVIDERS = [ { endpoint: 'https://YOUR_API_ENDPOINT_1/chat/completions', key: 'YOUR_API_KEY_1', model: 'gemini-translate-pro', prompt: '{text}', batchPrompt: `Translate the following multiple text segments into simplified Chinese. Rules: 1. Keep the numbering markers EXACTLY as they are. 2. Do not add explanations. 3. Do not omit any item. 4. Preserve formatting as much as possible. 5. Output ONLY the translated result in the same marker format. {text}` }, { endpoint: 'https://YOUR_API_ENDPOINT_2/chat/completions', key: 'YOUR_API_KEY_2', model: 'gemini-2.5-flash', prompt: `Translate the following text into simplified Chinese, keeping the original formatting as much as possible. Provide only the translated content, without any extra explanations or introductory phrases.\n\nText to translate:\n"""\n{text}\n"""`, batchPrompt: `Translate the following multiple text segments into simplified Chinese. Rules: 1. Keep the numbering markers EXACTLY as they are. 2. Do not add explanations. 3. Do not omit any item. 4. Preserve formatting as much as possible. 5. Output ONLY the translated result in the same marker format. {text}` } ]; const REQUEST_TIMEOUT = 45000; const BATCH_MAX_ITEMS = 12; const BATCH_MAX_CHARS = 3200; const MIN_TEXT_LENGTH = 2; const MAIN_CONTENT_SELECTORS = [ 'article', 'main', '[role="main"]', '.article', '.post', '.entry-content', '.content', '.markdown-body', '.doc-content', '.main-content' ]; const EXCLUDED_TAGS = new Set([ 'SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'OPTION', 'CODE', 'PRE', 'KBD', 'SAMP', 'SVG', 'CANVAS' ]); const INLINE_UI_EXCLUDE_SELECTORS = [ '#ai-translator-btn', '#ai-fullpage-btn', '#ai-translator-popup', '#ai-translate-toolbar' ]; const isConfigIncomplete = API_PROVIDERS.some(p => p.endpoint.includes('YOUR_API_ENDPOINT') || p.key.includes('YOUR_API_KEY') ); if (isConfigIncomplete) { alert('【划词/全文翻译脚本】\n\n请先在脚本顶部“用户配置区”填入你的 API endpoint 和 API key。'); return; } GM_addStyle(` #ai-translator-btn, #ai-fullpage-btn { position: absolute; width: 10px; height: 10px; border-radius: 50%; cursor: pointer; z-index: 999999; box-shadow: 0 2px 6px rgba(0,0,0,0.25); border: 2px solid white; transform: translate(-50%, -50%); opacity: 0.72; transition: transform 0.2s ease, opacity 0.2s ease; } #ai-translator-btn:hover, #ai-fullpage-btn:hover { transform: translate(-50%, -50%) scale(1.2); opacity: 1; } #ai-translator-btn { background-color: #ff4757; } #ai-fullpage-btn { background-color: #1e90ff; } #ai-translator-popup { position: absolute; z-index: 999998; background-color: #FFFBEF; border: 1px solid #EAEAEA; border-radius: 8px; box-shadow: 0 5px 16px rgba(0, 0, 0, 0.14); padding: 14px 16px; max-width: 460px; min-width: 220px; font-size: 14px; line-height: 1.65; color: #333; text-align: left; white-space: pre-wrap; word-wrap: break-word; transform: translate(-50%, -50%); box-sizing: border-box; } #ai-translate-toolbar { position: fixed; right: 16px; bottom: 18px; z-index: 999999; display: flex; align-items: center; gap: 8px; padding: 8px 10px; background: rgba(28, 28, 32, 0.92); color: #fff; border-radius: 10px; box-shadow: 0 6px 24px rgba(0,0,0,0.28); font-size: 12px; user-select: none; backdrop-filter: blur(6px); } #ai-translate-toolbar button { border: none; border-radius: 8px; padding: 6px 10px; cursor: pointer; color: #fff; font-size: 12px; line-height: 1; } #ai-translate-toolbar button:hover { opacity: 0.9; } #ai-restore-btn { background: #ff6b6b; } #ai-retranslate-btn { background: #4dabf7; } #ai-toolbar-close-btn { background: #666; } #ai-translate-status { max-width: 220px; color: #f1f3f5; opacity: 0.95; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } `); let translateBtn = null; let fullPageBtn = null; let translatePopup = null; let toolbar = null; let statusTextEl = null; let isFullPageTranslating = false; let pageTranslated = false; const originalTextMap = new WeakMap(); function cleanupDotsAndPopup() { if (translateBtn) { translateBtn.remove(); translateBtn = null; } if (fullPageBtn) { fullPageBtn.remove(); fullPageBtn = null; } if (translatePopup) { translatePopup.remove(); translatePopup = null; } } function clearSelection() { const selection = window.getSelection(); if (selection) { selection.removeAllRanges(); } } function ensureToolbar() { if (toolbar) return; toolbar = document.createElement('div'); toolbar.id = 'ai-translate-toolbar'; toolbar.innerHTML = ` <button id="ai-restore-btn" title="恢复原文">恢复原文</button> <button id="ai-retranslate-btn" title="翻译新增内容">翻译新增</button> <span id="ai-translate-status">未翻译</span> <button id="ai-toolbar-close-btn" title="关闭">×</button> `; document.body.appendChild(toolbar); statusTextEl = toolbar.querySelector('#ai-translate-status'); toolbar.querySelector('#ai-restore-btn').addEventListener('click', () => { restoreOriginalPage(); }); toolbar.querySelector('#ai-retranslate-btn').addEventListener('click', async () => { if (isFullPageTranslating) return; try { await translateEntirePage({ onlyUntranslated: true }); } catch (e) { setStatus('翻译新增失败:' + e.message); } }); toolbar.querySelector('#ai-toolbar-close-btn').addEventListener('click', () => { restoreOriginalPage(); toolbar.remove(); toolbar = null; statusTextEl = null; }); } function setStatus(text) { ensureToolbar(); if (statusTextEl) statusTextEl.textContent = text; console.log('[AI翻译]', text); } function showPopup(text, top, left) { if (translatePopup) translatePopup.remove(); translatePopup = document.createElement('div'); translatePopup.id = 'ai-translator-popup'; translatePopup.textContent = text; document.body.appendChild(translatePopup); translatePopup.style.top = `${top}px`; translatePopup.style.left = `${left}px`; } function sleep(ms) { return new Promise(resolve => setTimeout(resolve, ms)); } function chunkByRules(items, maxItems, maxChars) { const batches = []; let current = []; let currentChars = 0; for (const item of items) { const len = item.text.length; if ( current.length >= maxItems || (current.length > 0 && currentChars + len > maxChars) ) { batches.push(current); current = []; currentChars = 0; } current.push(item); currentChars += len; } if (current.length) batches.push(current); return batches; } function isElementVisible(el) { if (!el || !el.isConnected) return false; const style = window.getComputedStyle(el); if (style.display === 'none' || style.visibility === 'hidden') return false; return true; } function isInsideExcludedUI(node) { let el = node.parentElement; while (el) { for (const selector of INLINE_UI_EXCLUDE_SELECTORS) { if (el.matches && el.matches(selector)) return true; } el = el.parentElement; } return false; } function hasMeaningfulLatinText(text) { return /[A-Za-z\u00C0-\u024F]/.test(text); } function getMainContentRoot() { for (const selector of MAIN_CONTENT_SELECTORS) { const el = document.querySelector(selector); if (el && el.innerText && el.innerText.trim().length > 100) { return el; } } return document.body; } function extractCoreText(raw) { const leading = raw.match(/^\s*/)?.[0] || ''; const trailing = raw.match(/\s*$/)?.[0] || ''; const core = raw.trim(); return { leading, core, trailing }; } function getTranslatableTextNodes(root, onlyUntranslated = true) { const walker = document.createTreeWalker( root, NodeFilter.SHOW_TEXT, { acceptNode(node) { if (!node || !node.nodeValue) return NodeFilter.FILTER_REJECT; if (onlyUntranslated && originalTextMap.has(node)) return NodeFilter.FILTER_REJECT; const raw = node.nodeValue; const trimmed = raw.trim(); if (!trimmed || trimmed.length < MIN_TEXT_LENGTH) { return NodeFilter.FILTER_REJECT; } const parent = node.parentElement; if (!parent) return NodeFilter.FILTER_REJECT; if (EXCLUDED_TAGS.has(parent.tagName)) { return NodeFilter.FILTER_REJECT; } if (isInsideExcludedUI(node)) { return NodeFilter.FILTER_REJECT; } if (!isElementVisible(parent)) { return NodeFilter.FILTER_REJECT; } if (!/[^\d\s\p{P}]/u.test(trimmed) && !hasMeaningfulLatinText(trimmed)) { return NodeFilter.FILTER_REJECT; } if (trimmed.length < MIN_TEXT_LENGTH) { return NodeFilter.FILTER_REJECT; } if (!hasMeaningfulLatinText(trimmed)) { return NodeFilter.FILTER_REJECT; } return NodeFilter.FILTER_ACCEPT; } } ); const nodes = []; let current; while ((current = walker.nextNode())) { nodes.push(current); } return nodes; } function requestChatCompletion(provider, promptText) { return new Promise((resolve, reject) => { GM_xmlhttpRequest({ method: 'POST', url: provider.endpoint, timeout: REQUEST_TIMEOUT, headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${provider.key}` }, data: JSON.stringify({ model: provider.model, messages: [{ role: 'user', content: promptText }], temperature: 0.1, stream: false }), onload: function (response) { try { const data = JSON.parse(response.responseText); const content = data?.choices?.[0]?.message?.content?.trim(); if (content) { resolve(content); } else { reject(new Error(`无有效返回内容,HTTP ${response.status}`)); } } catch (e) { reject(new Error('响应解析失败')); } }, onerror: function (response) { reject(new Error(`网络错误 ${response?.status || ''}`)); }, ontimeout: function () { reject(new Error('请求超时')); } }); }); } function fillTemplate(template, text) { return (template || '{text}').replace('{text}', text); } async function translateSingleText(text) { let lastErr = null; for (let i = 0; i < API_PROVIDERS.length; i++) { const provider = API_PROVIDERS[i]; const promptText = fillTemplate(provider.prompt, text); try { return await requestChatCompletion(provider, promptText); } catch (e) { console.warn(`单条翻译 API[${i}] 失败:`, e.message); lastErr = e; } } throw lastErr || new Error('单条翻译失败:所有API均不可用'); } function buildBatchPayload(items) { return items.map((item, idx) => { return `<<<ITEM_${idx + 1}>>>\n${item.text}`; }).join('\n\n'); } function parseBatchResponse(responseText, expectedCount) { const results = new Array(expectedCount).fill(''); const regex = /<<<ITEM_(\d+)>>>\s*([\s\S]*?)(?=\n\s*<<<ITEM_\d+>>>|$)/g; let match; while ((match = regex.exec(responseText)) !== null) { const index = parseInt(match[1], 10) - 1; if (index >= 0 && index < expectedCount) { results[index] = (match[2] || '').trim(); } } return results; } async function translateBatch(batchItems) { const batchText = buildBatchPayload(batchItems); let lastErr = null; for (let i = 0; i < API_PROVIDERS.length; i++) { const provider = API_PROVIDERS[i]; const prompt = fillTemplate(provider.batchPrompt, batchText); try { const response = await requestChatCompletion(provider, prompt); const parsed = parseBatchResponse(response, batchItems.length); const validCount = parsed.filter(Boolean).length; if (validCount < Math.ceil(batchItems.length * 0.7)) { throw new Error('批量解析质量过低,转下一个API或单条翻译'); } return parsed.map((t, idx) => t || batchItems[idx].text); } catch (e) { console.warn(`批量翻译 API[${i}] 失败:`, e.message); lastErr = e; } } console.warn('批量翻译全部失败,降级为单条翻译:', lastErr?.message || ''); const results = []; for (const item of batchItems) { try { const one = await translateSingleText(item.text); results.push(one || item.text); await sleep(120); } catch { results.push(item.text); } } return results; } async function handleSelectionTranslate(text, top, left) { showPopup('翻译中...', top, left); try { const translated = await translateSingleText(text); if (translatePopup) translatePopup.textContent = translated || '翻译失败'; } catch (e) { if (translatePopup) translatePopup.textContent = '翻译失败:' + e.message; } } async function translateEntirePage({ onlyUntranslated = true } = {}) { if (isFullPageTranslating) { setStatus('正在翻译中,请稍候...'); return; } isFullPageTranslating = true; ensureToolbar(); try { const root = getMainContentRoot(); const nodes = getTranslatableTextNodes(root, onlyUntranslated); if (!nodes.length) { setStatus(onlyUntranslated ? '没有发现新的可翻译内容' : '未找到可翻译文本'); return; } const items = nodes.map(node => { const raw = node.nodeValue; const { leading, core, trailing } = extractCoreText(raw); return { node, raw, leading, core, trailing, text: core }; }).filter(item => item.text && item.text.length >= MIN_TEXT_LENGTH); if (!items.length) { setStatus('未找到有效文本'); return; } const batches = chunkByRules(items, BATCH_MAX_ITEMS, BATCH_MAX_CHARS); setStatus(`准备翻译:${items.length} 段文本,${batches.length} 批`); let done = 0; for (let i = 0; i < batches.length; i++) { const batch = batches[i]; setStatus(`翻译中:第 ${i + 1}/${batches.length} 批`); const translatedTexts = await translateBatch(batch); batch.forEach((item, idx) => { const translated = translatedTexts[idx]; if (!translated) return; if (!originalTextMap.has(item.node)) { originalTextMap.set(item.node, item.raw); } item.node.nodeValue = item.leading + translated + item.trailing; done++; }); await sleep(150); } pageTranslated = true; setStatus(`全文翻译完成:已替换 ${done} 段文本`); } catch (e) { setStatus('全文翻译失败:' + e.message); throw e; } finally { isFullPageTranslating = false; } } function restoreOriginalPage() { const root = getMainContentRoot(); const walker = document.createTreeWalker( root, NodeFilter.SHOW_TEXT, null ); let count = 0; let node; while ((node = walker.nextNode())) { if (originalTextMap.has(node)) { node.nodeValue = originalTextMap.get(node); count++; } } pageTranslated = false; setStatus(`已恢复原文:${count} 处`); } document.addEventListener('mouseup', function (e) { if ( e.target?.id === 'ai-translator-btn' || e.target?.id === 'ai-fullpage-btn' || (translatePopup && translatePopup.contains(e.target)) || (toolbar && toolbar.contains(e.target)) ) { return; } cleanupDotsAndPopup(); const selection = window.getSelection(); if (!selection || selection.isCollapsed || !selection.toString().trim()) { return; } const selectedText = selection.toString().trim(); let range; try { range = selection.getRangeAt(0); } catch { return; } const rect = range.getBoundingClientRect(); if (!rect) return; const centerX = window.scrollX + rect.left + rect.width / 2; const centerY = window.scrollY + rect.top + rect.height / 2; translateBtn = document.createElement('div'); translateBtn.id = 'ai-translator-btn'; document.body.appendChild(translateBtn); translateBtn.style.top = `${centerY}px`; translateBtn.style.left = `${centerX}px`; fullPageBtn = document.createElement('div'); fullPageBtn.id = 'ai-fullpage-btn'; document.body.appendChild(fullPageBtn); fullPageBtn.style.top = `${centerY}px`; fullPageBtn.style.left = `${centerX + 18}px`; translateBtn.addEventListener('click', function (event) { event.stopPropagation(); handleSelectionTranslate(selectedText, centerY + 24, centerX); if (translateBtn) translateBtn.style.display = 'none'; if (fullPageBtn) fullPageBtn.style.display = 'none'; }); fullPageBtn.addEventListener('click', async function (event) { event.stopPropagation(); showPopup('全文翻译中,请稍候...', centerY + 24, centerX + 60); if (translateBtn) translateBtn.style.display = 'none'; if (fullPageBtn) fullPageBtn.style.display = 'none'; try { await translateEntirePage({ onlyUntranslated: true }); if (translatePopup) { translatePopup.textContent = '全文翻译完成'; setTimeout(() => { if (translatePopup) { translatePopup.remove(); translatePopup = null; } }, 1200); } } catch (e) { if (translatePopup) translatePopup.textContent = '全文翻译失败:' + e.message; } }); }); document.addEventListener('mousedown', function (e) { if ( (translatePopup && translatePopup.contains(e.target)) || (toolbar && toolbar.contains(e.target)) || e.target?.id === 'ai-translator-btn' || e.target?.id === 'ai-fullpage-btn' ) { return; } cleanupDotsAndPopup(); clearSelection(); }); })(); 2 个帖子 - 2 位参与者 阅读完整话题
发现微软电脑管家更新了几个新功能:智能截图还有智能划词 看起来是调用系统的截图功能再加上一些ai功能,有截图翻译功能,可以用来平替pixpin,用起来还算凑合 缺点是还没有长截图 [!note]windows抛开系统不谈,做的系统小工具倒是越来越好用了,powertoys、软件管家都挺不错的,希望以后不要像edge一样越来越臃肿 6 个帖子 - 4 位参与者 阅读完整话题
snipdo现在一直在用这个软件,感觉挺好用的,但是毕竟是要高级版的才能够继续添加插件。有没有其他类似的开源软件可以用呀? 划词后可以复制、粘贴、打开链接、翻译等等。 1 个帖子 - 1 位参与者 阅读完整话题