// ==UserScript== // @name 划词翻译 - DeepSeek 悬浮卡片 // @namespace http://tampermonkey.net/ // @version 19.9.4 // @description 划词后在当前页面弹出暗色悬浮卡片(高度完全自适应,最大480),后台标签页中的 DeepSeek 静默翻译并实时回传,无多余窗口 // @author YourName // @match *://*/* // @match https://chat.deepseek.com/* // @grant GM_addStyle // @grant GM_getValue // @grant GM_setValue // @grant GM_deleteValue // @grant GM_addValueChangeListener // ==/UserScript== /* * ⚠️ 使用说明: * 1. 本脚本是「窗口版」的替代方案,采用页面内悬浮卡片显示译文。 * 安装/启用本脚本时,请先停用旧的「划词翻译 - DeepSeek 自动发送」, * 否则划词时会出现两个🌐按钮。 * * 2. 架构演进史(踩坑记录): * v19.8.1 工作窗口完全移出屏幕 → Chrome"遮挡节流"冻结页面,回传卡死; * v19.8.2 工作窗口留 50px 小角 → 依旧被节流(用户点回页面看卡片时 * 主窗口盖住工作窗口,照样触发遮挡); * v19.9 改用【后台标签页】当工作页——浏览器对后台标签从不冻结 * JS(只节流计时器),这是所有网页自动化依赖的可靠行为。 * 另外:曾想用隐藏 iframe 当工作页,但 DeepSeek 响应头带 * CSP: frame-ancestors 'none',官方封死了 iframe 嵌入。 * * 3. 工作方式: * - 点击🌐 → 当前页面弹出悬浮卡片; * - 首次使用会打开一个 DeepSeek 标签页(标题显示"翻译助手"), * 它会常驻后台复用:之后的翻译直接走后台,零弹窗零闪烁; * - 工作页把流式译文通过 GM 存储跨标签通知 + postMessage 双通道 * 实时回传给卡片; * - 工作页空闲 3 分钟自动关闭,下次划词自动重开; * - 卡片左上角 ⠿ 可拖动,拖后位置固定;✕ 关闭卡片。 */ (function() { 'use strict'; const CONFIG = { targetLang: '中文', deepseekUrl: 'https://chat.deepseek.com/', buttonDelay: 300, cardWidth: 320, cardMaxHeight: 480, maxTextLength: 999999, offsetX: 20, // 卡片水平偏移量(右侧空间不够则向左) offsetY: 20, // 卡片垂直偏移量(始终向下) doneIdleMs: 1500, // 输出停止多久判定为"完成" workerName: 'ds_translate_worker', workerIdleCloseMs: 180000, // 工作页空闲多久后自动关闭(3分钟) }; const isDeepSeek = location.href.startsWith('https://chat.deepseek.com/'); const RELAY_SOURCE = 'ds-translate-card-relay'; const TASK_KEY = 'ds_card_task'; const RELAY_KEY = 'ds_card_relay'; const HEARTBEAT_KEY = 'ds_worker_heartbeat'; const HEARTBEAT_INTERVAL = 5000; // 工作页心跳间隔 const HEARTBEAT_STALE = 12000; // 超过此时长视为工作页已死亡 function log(msg, data) { console.log(`[划词翻译-卡片] ${msg}`, data || ''); } // ============================================================ // 普通网页部分(划词按钮 + 悬浮卡片) // ============================================================ if (!isDeepSeek) { GM_addStyle(` #translate-popup-btn { position: fixed; z-index: 2147483000; background: #4D6BFE; color: white; border: none; border-radius: 50%; padding: 6px 6px; font-size: 18px; cursor: pointer; box-shadow: 0 4px 12px rgba(77, 107, 254, 0.4); transition: all 0.2s ease; display: none; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; user-select: none; pointer-events: auto; line-height: 1; width: 32px; height: 32px; align-items: center; justify-content: center; } #translate-popup-btn:hover { background: #3B5DE7; transform: scale(1.1); box-shadow: 0 6px 20px rgba(77, 107, 254, 0.5); } /* ---------- 悬浮卡片(固定暗色主题:纯黑底白字) ---------- */ #ds-translate-card { position: fixed; z-index: 2147483000; width: ${CONFIG.cardWidth}px; max-height: ${CONFIG.cardMaxHeight}px; background: #000000; border: 1px solid #2a2d33; border-radius: 10px; box-shadow: 0 8px 30px rgba(0, 0, 0, 0.55); box-sizing: border-box; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Microsoft YaHei", sans-serif; font-size: 14px; line-height: 1.7; } #ds-translate-card-scroll { max-height: ${CONFIG.cardMaxHeight - 2}px; overflow-y: auto; padding: 14px 16px 16px; box-sizing: border-box; border-radius: 10px; } #ds-translate-card-scroll, #ds-translate-card-scroll * { color: #f3f5f7; } #ds-translate-card-scroll a { color: #6ea8ff !important; text-decoration: underline; } #ds-translate-card-scroll code, #ds-translate-card-scroll pre { background: rgba(255, 255, 255, 0.08) !important; } #ds-translate-card-scroll table, #ds-translate-card-scroll th, #ds-translate-card-scroll td { border-color: rgba(255, 255, 255, 0.25) !important; } #ds-translate-card-close { position: absolute; top: 6px; right: 8px; width: 24px; height: 24px; line-height: 24px; text-align: center; border-radius: 50%; background: rgba(255, 255, 255, 0.12); color: #c9ccd4; cursor: pointer; font-size: 13px; user-select: none; z-index: 1; } #ds-translate-card-close:hover { background: rgba(255, 255, 255, 0.25); } #ds-translate-card-drag { position: absolute; top: 6px; left: 8px; width: 24px; height: 24px; line-height: 24px; text-align: center; border-radius: 50%; background: rgba(255, 255, 255, 0.12); color: #c9ccd4; cursor: move; font-size: 12px; user-select: none; z-index: 1; } #ds-translate-card-drag:hover { background: rgba(255, 255, 255, 0.25); } #ds-translate-card-status { color: #8a8f99 !important; font-size: 13px; } #ds-translate-card-status .dots::after { content: ''; animation: ds-card-dots 1.2s steps(4, end) infinite; } @keyframes ds-card-dots { 0% { content: ''; } 25% { content: '.'; } 50% { content: '..'; } 75% { content: '...'; } } `); let selectedText = ''; let popupBtn = null; let hideTimer = null; let currentMouseX = 0; let currentMouseY = 0; let card = null; let cardScroll = null; let cardStatus = null; let cardBody = null; let cardAnchorX = 0; let cardAnchorY = 0; let cardTimeoutTimer = null; let relayPollTimer = null; // GM 存储轮询定时器(卡片存在期间运行) let cardPinned = false; // 用户拖动后位置固定,不再自动跟随 let workerWin = null; // 后台工作标签页引用 function createPopupButton() { if (popupBtn) return; popupBtn = document.createElement('button'); popupBtn.id = 'translate-popup-btn'; popupBtn.textContent = '🌐'; document.body.appendChild(popupBtn); popupBtn.addEventListener('click', function(e) { e.stopPropagation(); triggerTranslation(selectedText, currentMouseX, currentMouseY); hidePopupButton(); }); popupBtn.addEventListener('mouseleave', function() { hideTimer = setTimeout(hidePopupButton, 300); }); popupBtn.addEventListener('mouseenter', function() { clearTimeout(hideTimer); }); } function showPopupButton(x, y) { if (!popupBtn) createPopupButton(); popupBtn.style.display = 'flex'; popupBtn.style.left = (x + 8) + 'px'; popupBtn.style.top = (y - 16) + 'px'; clearTimeout(hideTimer); } function hidePopupButton() { if (popupBtn) popupBtn.style.display = 'none'; } // ---------- 悬浮卡片 ---------- function removeCard() { if (cardTimeoutTimer) { clearTimeout(cardTimeoutTimer); cardTimeoutTimer = null; } if (relayPollTimer) { clearInterval(relayPollTimer); relayPollTimer = null; } if (card) { card.remove(); card = null; } cardScroll = null; cardStatus = null; cardBody = null; cardPinned = false; } function showFloatingCard(x, y) { removeCard(); cardAnchorX = x; cardAnchorY = y; card = document.createElement('div'); card.id = 'ds-translate-card'; cardScroll = document.createElement('div'); cardScroll.id = 'ds-translate-card-scroll'; card.appendChild(cardScroll); cardStatus = document.createElement('div'); cardStatus.id = 'ds-translate-card-status'; cardStatus.innerHTML = '翻译中'; cardScroll.appendChild(cardStatus); cardBody = document.createElement('div'); cardBody.id = 'ds-translate-card-body'; cardScroll.appendChild(cardBody); const closeBtn = document.createElement('div'); closeBtn.id = 'ds-translate-card-close'; closeBtn.textContent = '✕'; closeBtn.title = '关闭卡片'; closeBtn.addEventListener('click', removeCard); card.appendChild(closeBtn); // 左上角拖动手柄:按住可把卡片拖到任意位置,拖后位置固定 const dragHandle = document.createElement('div'); dragHandle.id = 'ds-translate-card-drag'; dragHandle.textContent = '⠿'; dragHandle.title = '拖动卡片'; makeCardDraggable(dragHandle); card.appendChild(dragHandle); document.body.appendChild(card); positionCard(); // 超时提示:60s 没有任何译文则提示检查登录状态 cardTimeoutTimer = setTimeout(function() { if (cardStatus && cardBody && !cardBody.innerHTML) { cardStatus.textContent = '等待超时:请确认 DeepSeek 已登录'; } }, 60000); } function makeCardDraggable(handle) { handle.addEventListener('mousedown', function(e) { e.preventDefault(); e.stopPropagation(); if (!card) return; cardPinned = true; const rect = card.getBoundingClientRect(); const dx = e.clientX - rect.left; const dy = e.clientY - rect.top; function onMove(ev) { if (!card) return cleanup(); // 拖动时允许大部分移出屏幕,但保留可抓回来的边角 let left = Math.min(Math.max(ev.clientX - dx, -rect.width + 48), window.innerWidth - 48); let top = Math.min(Math.max(ev.clientY - dy, 0), window.innerHeight - 48); card.style.left = left + 'px'; card.style.top = top + 'px'; } function cleanup() { window.removeEventListener('mousemove', onMove); window.removeEventListener('mouseup', cleanup); } window.addEventListener('mousemove', onMove); window.addEventListener('mouseup', cleanup); }); } function positionCard() { if (!card || cardPinned) return; const vw = window.innerWidth; const vh = window.innerHeight; const rect = card.getBoundingClientRect(); let left = cardAnchorX + CONFIG.offsetX; if (left + rect.width > vw) { left = cardAnchorX - CONFIG.offsetX - rect.width; } if (left < 0) left = 10; let top = cardAnchorY + CONFIG.offsetY; if (top + rect.height > vh) { top = vh - rect.height - 10; } if (top < 0) top = 10; card.style.left = left + 'px'; card.style.top = top + 'px'; } // 接收工作标签页回传的流式译文 // 双通道:GM 存储跨标签通知(主) + postMessage(备) let lastRelayTs = 0; function handleRelay(d) { if (!card) return; if (d.ts) { if (d.ts <= lastRelayTs) return; // 丢弃重复/乱序消息 lastRelayTs = d.ts; } if (d.error) { if (cardStatus) cardStatus.textContent = '出错了:' + d.error; return; } if (typeof d.html === 'string' && cardBody) { cardBody.innerHTML = d.html; if (cardStatus) { cardStatus.remove(); cardStatus = null; } } if (d.done) { if (cardTimeoutTimer) { clearTimeout(cardTimeoutTimer); cardTimeoutTimer = null; } log('译文接收完成'); } positionCard(); if (cardScroll) cardScroll.scrollTop = cardScroll.scrollHeight; } window.addEventListener('message', function(e) { if (!e.origin || !e.origin.startsWith('https://chat.deepseek.com')) return; const d = e.data; if (!d || d.source !== RELAY_SOURCE) return; handleRelay(d); }); if (typeof GM_addValueChangeListener === 'function') { GM_addValueChangeListener(RELAY_KEY, function(name, oldValue, newValue, remote) { if (!remote) return; try { const d = JSON.parse(newValue); if (d && d.source === RELAY_SOURCE) handleRelay(d); } catch(e) {} }); } // 第三条通道:主动轮询 GM 存储。 // Tampermonkey 的跨标签值变更通知有同步延迟(后台任务多时可达数秒), // 非 opener 页面又收不到 postMessage,只靠通知会卡顿。 // 卡片存在期间每 60ms 直接读一次最新值(与工作页 60ms 写入限频对齐), // 前台定时器不受节流,配合 handleRelay 的时间戳去重, // 与另两条通道互不冲突、谁先到用谁。 function startRelayPolling() { if (relayPollTimer || typeof GM_getValue !== 'function') return; relayPollTimer = setInterval(() => { if (!card) return; try { const raw = GM_getValue(RELAY_KEY, null); if (!raw) return; const d = JSON.parse(raw); if (d && d.source === RELAY_SOURCE) handleRelay(d); } catch(e) {} }, 60); } // ---------- 触发翻译 ---------- function triggerTranslation(text, mouseX, mouseY) { if (!text || text.trim().length === 0) return; if (text.trim().length > CONFIG.maxTextLength) { alert(`选中的文本过长(超过 ${CONFIG.maxTextLength} 字符),请减少选择。`); return; } const seq = Date.now(); // 1. 先把任务写进 GM 存储(工作页无论新旧都能拿到) GM_setValue(TASK_KEY, JSON.stringify({ seq: seq, text: text.trim(), targetLang: CONFIG.targetLang })); showFloatingCard(mouseX, mouseY); startRelayPolling(); ensureWorkerTab(seq); } // 后台工作标签页:常驻复用,绝不以"窗口"形式出现。 // 浏览器对后台标签页从不冻结 JS(这是所有网页自动化依赖的 // 可靠行为),所以回传永远不会像离屏窗口那样卡死。 function isWorkerAlive() { // 工作页每 5 秒广播一次心跳(GM 存储是所有标签页共享的), // 心跳新鲜 = 某个标签页(哪怕是别的页面开的)里工作页还活着 try { const hb = GM_getValue(HEARTBEAT_KEY, 0); return hb && (Date.now() - hb < HEARTBEAT_STALE); } catch(e) { return false; } } function ensureWorkerTab() { if (workerWin && !workerWin.closed) { // 本页面自己开的工作页已存活,任务会通过 GM 存储通知它 log('复用本页工作标签页'); return; } if (isWorkerAlive()) { // 别的页面打开的工作页还活着:任务照样通过 GM 存储下发, // 它监听到后会翻译并回传,本页无需(也不能)重复开新页。 // (Chrome 的命名窗口只对同一个 opener 复用,跨标签页 // window.open 同名会开新标签,所以这里绝不能再 open) log('检测到其他页面的工作标签页存活,直接复用'); return; } // 心跳已死(从未打开/被手动关闭/空闲自动关闭):开新工作页。 // 首次会短暂出现在前台加载,之后常驻后台。 log('打开新的工作标签页'); workerWin = window.open(CONFIG.deepseekUrl, CONFIG.workerName); } let mouseX = 0, mouseY = 0; let showButtonTimer = null; document.addEventListener('mouseup', function(e) { const selection = window.getSelection(); const text = selection.toString().trim(); if (text.length > 0 && text.length <= CONFIG.maxTextLength) { selectedText = text; mouseX = e.clientX; mouseY = e.clientY; currentMouseX = mouseX; currentMouseY = mouseY; clearTimeout(showButtonTimer); showButtonTimer = setTimeout(() => { if (window.getSelection().toString().trim() === text) { showPopupButton(mouseX, mouseY); } }, CONFIG.buttonDelay); } else { hidePopupButton(); } }); document.addEventListener('mousemove', function(e) { currentMouseX = e.clientX; currentMouseY = e.clientY; }); document.addEventListener('mousedown', function(e) { if (popupBtn && !popupBtn.contains(e.target)) hidePopupButton(); }); document.addEventListener('scroll', hidePopupButton); createPopupButton(); log('普通网页部分已加载'); // ============================================================ // DeepSeek 页面部分(后台工作标签页:自动发送 + 回传译文) // ============================================================ } else { // 标签页标题改成"翻译助手",方便在标签栏里辨认、和普通 DS 页面区分 document.title = '🈯 翻译助手'; log('DeepSeek 工作标签页已加载'); let lastTaskSeq = 0; // 已处理/已接收的最大任务序号 let busy = false; // 当前是否有任务在跑 let queuedTask = null; // 忙碌时到达的最新任务(只保留最新) let lastActivity = Date.now(); // 最后活动时间(用于空闲自动关闭) let relayStarted = false; let lastHTML = ''; let lastChangeTime = 0; let doneSent = false; let tsSeq = Date.now(); let lastStorageWrite = 0; // ---------- 任务接收 ---------- function acceptTask(task) { if (!task || !task.seq || task.seq <= lastTaskSeq) return; // 重复/过期任务 lastTaskSeq = task.seq; lastActivity = Date.now(); if (busy) { queuedTask = task; // 忙碌时只保留最新任务 log('忙碌中,任务已排队', task.seq); } else { runTask(task); } } function runTask(task) { busy = true; doneSent = false; lastHTML = ''; lastChangeTime = Date.now(); lastActivity = Date.now(); log('开始翻译任务', task.seq); startRelay(); autoTranslate(task.text, task.targetLang); } function finishTask() { busy = false; lastActivity = Date.now(); if (queuedTask) { const t = queuedTask; queuedTask = null; setTimeout(() => acceptTask(t), 800); } } // 页面加载时:读取可能已经写入的任务(首次划词后标签页才打开的场景) try { const raw = GM_getValue(TASK_KEY, null); if (raw) { const task = JSON.parse(raw); // 60 秒内的任务才算数,更早的视为残渣 if (task && task.seq && Date.now() - task.seq < 60000) { acceptTask(task); } } } catch(e) { log('读取初始任务失败', e); } // 标签页常驻:监听后续任务(划词页直接写入,无需重开标签页) if (typeof GM_addValueChangeListener === 'function') { GM_addValueChangeListener(TASK_KEY, function(name, oldValue, newValue, remote) { if (!remote) return; try { acceptTask(JSON.parse(newValue)); } catch(e) { log('任务解析失败', e); } }); } // 心跳广播:让所有划词页知道"工作页还活着",从而跨标签页复用, // 避免每个页面第一次划词都重复开一个新的 DS 标签页。 // 标签页关闭(手动或空闲自动关闭)后 setInterval 随之消亡,心跳自然停止。 try { GM_setValue(HEARTBEAT_KEY, Date.now()); } catch(e) {} setInterval(() => { try { GM_setValue(HEARTBEAT_KEY, Date.now()); } catch(e) {} }, HEARTBEAT_INTERVAL); // 空闲自动关闭:3 分钟没有任务就自己关掉,保持标签栏干净 setInterval(() => { if (!busy && lastTaskSeq && Date.now() - lastActivity > CONFIG.workerIdleCloseMs) { log('空闲超时,自动关闭工作标签页'); try { window.close(); } catch(e) {} } }, 15000); // ---------- 译文回传 ---------- function nextTs() { tsSeq = Math.max(tsSeq + 1, Date.now()); return tsSeq; } // 双通道回传:1) GM 存储跨标签通知(主);2) postMessage 给 opener(备)。 // 卡片端按时间戳去重,两条路谁先到用谁。 function relayMessage(payload) { const full = Object.assign({ source: RELAY_SOURCE, ts: nextTs() }, payload); // 通道1:GM 存储通知(流式期间限频 60ms,对齐 postMessage 的 // 实时感;完成/出错立即写)。60ms 是 A 页逐字与 B 页颗粒度 // 一致的关键——限频太松会让非 opener 页面"一段一段"出字。 try { const now = Date.now(); if (payload.done || payload.error || now - lastStorageWrite > 60) { lastStorageWrite = now; GM_setValue(RELAY_KEY, JSON.stringify(full)); } } catch(e) { log('GM 存储回传失败', e); } // 通道2:postMessage try { if (window.opener && !window.opener.closed) { window.opener.postMessage(full, '*'); } } catch(e) { log('postMessage 回传失败', e); } } function relayError(msg) { log('错误:' + msg); relayMessage({ error: msg }); } function startRelay() { if (relayStarted) return; relayStarted = true; // 从所有 .ds-markdown 里挑出"最终译文",排除思考过程: // DeepSeek 的"已深度思考(用时 X 秒)"折叠条和思考内容共用 // 一个容器,最终译文在这个容器外面。找到该容器的最内层包裹, // 排除其中的 markdown,取剩下的最后一条。 // 若结构对不上(官网改版)则退回旧行为:取最后一条。 function pickAnswerMarkdown() { const outputs = document.querySelectorAll('.ds-markdown'); if (!outputs.length) return null; let thinkWrap = null; const hints = document.querySelectorAll('div,button,span'); for (const el of hints) { const t = (el.textContent || '').trim(); // 提示条都是短文本,限制长度防止误伤正文里提到"思考"的段落 if (t.length === 0 || t.length > 30) continue; if (!/已深度思考|思考过程|思考中|思考完毕|已搜索到|联网搜索/.test(t)) continue; // 从提示条向上找"最内层的、直接包含 .ds-markdown 的容器" let p = el.parentElement; while (p && p !== document.body) { if (p.querySelector('.ds-markdown')) { // 容器本身不能是 markdown(防止误伤正文), // 思考折叠条是独立于 markdown 的元素 if (!p.matches('.ds-markdown')) thinkWrap = p; break; } p = p.parentElement; } } if (thinkWrap) { for (let i = outputs.length - 1; i >= 0; i--) { if (!thinkWrap.contains(outputs[i])) return outputs[i]; } return null; // 目前只有思考内容 → 不回传 } return outputs[outputs.length - 1]; } const mo = new MutationObserver(() => { const answer = pickAnswerMarkdown(); if (!answer) return; const html = answer.innerHTML; if (html && html !== lastHTML) { lastHTML = html; lastChangeTime = Date.now(); relayMessage({ html: lastHTML, done: false }); } }); mo.observe(document.body, { childList: true, subtree: true, characterData: true }); // 完成检测:内容停止变化超过 doneIdleMs 视为输出完毕。 // 注意:后台标签页的 setInterval 最多被钳到 1 秒一次, // 只影响"完成"信号的延迟,不影响流式内容(靠 MutationObserver)。 setInterval(() => { if (doneSent || !lastHTML) return; if (Date.now() - lastChangeTime > CONFIG.doneIdleMs) { doneSent = true; relayMessage({ html: lastHTML, done: true }); log('译文回传完毕'); finishTask(); } }, 300); } // ---------- 自动填充与发送 ---------- function autoTranslate(text, targetLang) { const prompt = `仅输出${targetLang}译文: """ ${text} """ 输出示例: The translated text itself`; log('提示词已构建'); function findInput() { let input = document.querySelector('textarea[name="search"]'); if (input) return input; input = document.querySelector('textarea.d96f2d2a'); if (input) return input; input = document.querySelector('textarea'); return input; } function findSendButton() { const candidates = document.querySelectorAll('div[role="button"]'); for (let btn of candidates) { const svg = btn.querySelector('svg'); if (svg) { const path = svg.querySelector('path[d^="M8.3125"]'); if (path) return btn; } } for (let btn of candidates) { if (!btn.classList.contains('ds-button--disabled')) return btn; } return null; } function fillInput(input, value) { input.focus(); const nativeSetter = Object.getOwnPropertyDescriptor( HTMLTextAreaElement.prototype, 'value' ).set; nativeSetter.call(input, value); ['input', 'change', 'keydown', 'keyup'].forEach(type => { input.dispatchEvent(new Event(type, { bubbles: true, cancelable: true })); }); input.dispatchEvent(new CompositionEvent('compositionstart', { bubbles: true })); input.dispatchEvent(new CompositionEvent('compositionend', { bubbles: true, data: value })); log('输入框已填充'); } function clickSendButton(btn, input) { return new Promise((resolve) => { if (btn.classList.contains('ds-button--disabled')) { log('发送按钮禁用,等待启用...'); const observer = new MutationObserver(() => { if (!btn.classList.contains('ds-button--disabled')) { observer.disconnect(); btn.click(); log('点击发送按钮'); resolve(); } }); observer.observe(btn, { attributes: true, attributeFilter: ['class'] }); setTimeout(() => { observer.disconnect(); if (btn.classList.contains('ds-button--disabled')) { log('发送按钮超时未启用'); relayError('DeepSeek 发送按钮未就绪,请重试'); finishTask(); resolve(); } }, 5000); } else { btn.click(); log('点击发送按钮'); resolve(); } }); } (async function() { const newBtn = document.querySelector('button[aria-label="新对话"], button[aria-label="新建对话"], .new-chat-btn'); if (newBtn) newBtn.click(); await sleep(500); const input = findInput(); if (!input) { // 不自动关闭:用户可能需要在这个标签页里登录 DeepSeek relayError('DeepSeek 页面未找到输入框,请点击"翻译助手"标签页确认已登录'); finishTask(); return; } fillInput(input, prompt); await sleep(400); const sendBtn = findSendButton(); if (sendBtn) { await clickSendButton(sendBtn, input); } else { relayError('未找到发送按钮,请重试'); finishTask(); } })(); } function sleep(ms) { return new Promise(r => setTimeout(r, ms)); } } })();