您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
多风格、多模型可选/添加 预定义角色可选/添加 + 可拖拽面板 + TTS + 用的硅基API,国内模型对某些风格并不支持,你们可以自行修改,而且其他浏览器你们也可以扩展下
// ==UserScript== // @name 谷歌搜索自定义风格总结 // @description 多风格、多模型可选/添加 预定义角色可选/添加 + 可拖拽面板 + TTS + 用的硅基API,国内模型对某些风格并不支持,你们可以自行修改,而且其他浏览器你们也可以扩展下 // @author songmile // @version 1.4 // @license MIT // @namespace http://tampermonkey.net/ // @match https://www.google.com/search* // @run-at document-end // @grant GM.setValue // @grant GM.getValue // ==/UserScript== (async () => { let defaultEndpoint = "https://api.siliconflow.cn/v1/chat/completions" let defaultToken = "<token>" let qwenEndpoint = await GM.getValue("QWEN_API_ENDPOINT", defaultEndpoint) let qwenToken = await GM.getValue("QWEN_BEARER_TOKEN", defaultToken) let modelList = [ "Qwen/Qwen2.5-7B-Instruct", "deepseek-ai/DeepSeek-V2.5", "Qwen/QVQ-72B-Preview" ] let modelMode = modelList[0] let rolePrompts = { "猫娘风格": "以猫娘口吻写作,大约 100 字,用中文。专业但可爱,常用“喵、嗯、好耶,好棒”一类语气词,对观点进行小短评。", "导师PUA风": "使用强烈的批评和打击,带一点激励,整体像导师对不满意的研究生。参考示例:“说实话,我对你最近的表现很失望。当初破格收你进实验室,是看中你的潜力的……你要是再发不出一作,明年的开题就别想了。”语言中带威胁、讽刺、贬低,且强调科研思维和独立思考的重要性。约100字,不要太温和。", "学术妲己风": "结合妲己的妩媚撩人腔调,但暗含一定学术严谨。说话时既能撒娇,也能引用文献式表达,不要低俗,可做出一些“嗯哼~ 这篇论文,姐姐觉得似乎欠了点火候”之类的表述。", "玩梗狂魔风": "大量使用互联网梗、鬼畜梗、弹幕梗。段落轻松幽默,可出现“哈哈哈”、“笑死我了”或表情包文字。适度融入段子,让人会心一笑。", "霸道总裁风": "模拟“霸道总裁”的语气,讲话自带命令和冷酷,但也偶尔流露关心。例如:“我不想再说第二遍,这个项目我势在必得。你要做的,就是搞定一切,否则后果自负。”注意保持精英口吻,带点强势,字里行间有一点甜宠也行。" } let roleMode = "猫娘风格" let allSummaries = [] let currentUtter = null let isPaused = false function createControlPanel() { const panel = document.createElement("div") panel.id = "qwen-panel" panel.style.cssText = ` position: fixed; top: 100px; left: 50%; transform: translateX(-50%); width: 420px; background: #fff; border: 2px solid #ccd; border-radius: 8px; box-shadow: 0 2px 8px rgba(0,0,0,0.2); z-index: 999999; user-select: none; font-family: sans-serif; ` const titleBar = document.createElement("div") titleBar.style.cssText = ` background: linear-gradient(45deg, #aaf, #eef); padding: 6px 10px; cursor: move; display: flex; justify-content: space-between; align-items: center; border-radius: 6px 6px 0 0; ` const titleText = document.createElement("span") titleText.innerText = "谷歌搜索自定义总结 || 公众号:Songmile" titleText.style.fontWeight = "bold" const toggleBtn = document.createElement("button") toggleBtn.innerText = "收起" toggleBtn.style.cursor = "pointer" toggleBtn.addEventListener("click", () => { if (panel.classList.contains("collapsed")) { panel.classList.remove("collapsed") content.style.display = "block" toggleBtn.innerText = "收起" } else { panel.classList.add("collapsed") content.style.display = "none" toggleBtn.innerText = "展开" } }) titleBar.appendChild(titleText) titleBar.appendChild(toggleBtn) panel.appendChild(titleBar) const content = document.createElement("div") content.style.cssText = "padding:8px; display:block;" const endpointLabel = document.createElement("label") endpointLabel.innerText = "API Endpoint: " const endpointInput = document.createElement("input") endpointInput.type = "text" endpointInput.style.width = "100%" endpointInput.value = qwenEndpoint endpointInput.addEventListener("change", async () => { qwenEndpoint = endpointInput.value.trim() await GM.setValue("QWEN_API_ENDPOINT", qwenEndpoint) }) const tokenLabel = document.createElement("label") tokenLabel.innerText = "Bearer Token: " const tokenInput = document.createElement("input") tokenInput.type = "text" tokenInput.style.width = "100%" tokenInput.value = qwenToken tokenInput.addEventListener("change", async () => { qwenToken = tokenInput.value.trim() await GM.setValue("QWEN_BEARER_TOKEN", qwenToken) }) const apiBox = document.createElement("div") apiBox.style.marginBottom = "6px" apiBox.appendChild(endpointLabel) apiBox.appendChild(document.createElement("br")) apiBox.appendChild(endpointInput) apiBox.appendChild(document.createElement("br")) apiBox.appendChild(tokenLabel) apiBox.appendChild(document.createElement("br")) apiBox.appendChild(tokenInput) const modelBox = document.createElement("div") modelBox.style.marginBottom = "8px" const modelLabel = document.createElement("label") modelLabel.innerText = "模型选择:" const modelSelect = document.createElement("select") modelList.forEach(m => { const opt = document.createElement("option") opt.value = m opt.textContent = m modelSelect.appendChild(opt) }) modelSelect.value = modelMode modelSelect.addEventListener("change", async () => { modelMode = modelSelect.value await reAnnotateAll() }) const addModelInput = document.createElement("input") addModelInput.type = "text" addModelInput.placeholder = "输入新模型名称" addModelInput.style.width = "66%" addModelInput.style.marginRight = "4px" const addModelBtn = document.createElement("button") addModelBtn.textContent = "添加模型" addModelBtn.addEventListener("click", () => { const newModel = addModelInput.value.trim() if (!newModel) return modelList.push(newModel) const opt = document.createElement("option") opt.value = newModel opt.textContent = newModel modelSelect.appendChild(opt) addModelInput.value = "" alert("模型已添加到列表") }) modelBox.appendChild(modelLabel) modelBox.appendChild(modelSelect) modelBox.appendChild(document.createElement("br")) modelBox.appendChild(addModelInput) modelBox.appendChild(addModelBtn) const roleBox = document.createElement("div") roleBox.style.marginBottom = "8px" const roleLabel = document.createElement("label") roleLabel.innerText = "角色风格:" const roleSelect = document.createElement("select") Object.keys(rolePrompts).forEach(r => { const opt = document.createElement("option") opt.value = r opt.textContent = r roleSelect.appendChild(opt) }) roleSelect.value = roleMode roleSelect.addEventListener("change", async () => { roleMode = roleSelect.value await reAnnotateAll() }) const addRoleNameInput = document.createElement("input") addRoleNameInput.type = "text" addRoleNameInput.placeholder = "角色名称" addRoleNameInput.style.width = "33%" const addRolePromptInput = document.createElement("input") addRolePromptInput.type = "text" addRolePromptInput.placeholder = "角色Prompt" addRolePromptInput.style.width = "33%" const addRoleBtn = document.createElement("button") addRoleBtn.textContent = "添加角色" addRoleBtn.addEventListener("click", async () => { const rName = addRoleNameInput.value.trim() const rPrompt = addRolePromptInput.value.trim() if (!rName || !rPrompt) return rolePrompts[rName] = rPrompt const opt = document.createElement("option") opt.value = rName opt.textContent = rName roleSelect.appendChild(opt) addRoleNameInput.value = "" addRolePromptInput.value = "" alert("角色已添加到列表") }) roleBox.appendChild(roleLabel) roleBox.appendChild(roleSelect) roleBox.appendChild(document.createElement("br")) roleBox.appendChild(addRoleNameInput) roleBox.appendChild(addRolePromptInput) roleBox.appendChild(addRoleBtn) const aggBtn = document.createElement("button") aggBtn.textContent = "生成搜索综合" aggBtn.style.marginRight = "6px" aggBtn.addEventListener("click", async () => { if (!allSummaries.length) { alert("暂无摘要可汇总") return } const combinedText = allSummaries .map((obj, i) => `(${i+1})【${obj.title}】:${obj.summary}`) .join("\n\n") const aggregatorPrompt = ` 我收集了以下搜索结果的简要内容,请你以角色【${roleMode}】,再用模型【${modelMode}】写1~2段综合, 以下是所有摘要: ${combinedText} ` const finalSummary = await callQwen(aggregatorPrompt) if (finalSummary) displayGlobalSummary(finalSummary) }) const mdBtn = document.createElement("button") mdBtn.textContent = "导出MD" mdBtn.style.marginRight = "6px" mdBtn.addEventListener("click", () => { if (!allSummaries.length) { alert("暂无可导出的摘要") return } const lines = allSummaries.map( (o, i) => `## ${i+1}. ${o.title}\n\n${o.summary}` ) alert("Markdown:\n\n" + lines.join("\n\n---\n\n")) }) const clearBtn = document.createElement("button") clearBtn.textContent = "清空数据" clearBtn.style.color = "red" clearBtn.addEventListener("click", () => { if (confirm("确定清空所有摘要吗")) { allSummaries = [] alert("已清空") } }) const btnBox = document.createElement("div") btnBox.style.marginBottom = "8px" btnBox.appendChild(aggBtn) btnBox.appendChild(mdBtn) btnBox.appendChild(clearBtn) const ttsControl = document.createElement("div") ttsControl.style.cssText = "background:#eef; padding:4px; border-radius:4px;" const ttsTitle = document.createElement("div") ttsTitle.innerHTML = "<b>语音控制:</b>" const playBtn = document.createElement("button") playBtn.textContent = "播放/继续" playBtn.addEventListener("click", () => { if (currentUtter && isPaused) { window.speechSynthesis.resume() isPaused = false } else { alert("请先点击某条摘要的语音播报按钮") } }) const pauseBtn = document.createElement("button") pauseBtn.textContent = "暂停" pauseBtn.addEventListener("click", () => { if (!currentUtter) { alert("暂无播放可暂停") return } window.speechSynthesis.pause() isPaused = true }) const stopBtn = document.createElement("button") stopBtn.textContent = "停止" stopBtn.addEventListener("click", () => { window.speechSynthesis.cancel() currentUtter = null isPaused = false }) ttsControl.appendChild(ttsTitle) ttsControl.appendChild(playBtn) ttsControl.appendChild(pauseBtn) ttsControl.appendChild(stopBtn) content.appendChild(apiBox) content.appendChild(modelBox) content.appendChild(roleBox) content.appendChild(btnBox) content.appendChild(ttsControl) panel.appendChild(content) document.body.appendChild(panel) let offsetX=0, offsetY=0 let isDragging=false titleBar.addEventListener("mousedown", (e) => { isDragging=true offsetX = e.clientX - panel.offsetLeft offsetY = e.clientY - panel.offsetTop document.addEventListener("mousemove", onMove) document.addEventListener("mouseup", onUp) }) function onMove(e) { if (!isDragging) return panel.style.left = e.clientX - offsetX + "px" panel.style.top = e.clientY - offsetY + "px" } function onUp() { isDragging=false document.removeEventListener("mousemove", onMove) document.removeEventListener("mouseup", onUp) } } function displayGlobalSummary(summaryText) { const old = document.querySelector("#qwen-global-summary") if (old) old.remove() const div = document.createElement("div") div.id = "qwen-global-summary" div.style.cssText = ` background: #fffbe6; border: 1px dashed #ccc; padding: 10px; margin: 10px auto; max-width: 800px; font-size: 15px; line-height: 1.6; color: #333; ` div.textContent = "【综合】 " + summaryText document.body.insertBefore(div, document.body.firstChild) window.scrollTo({ top: 0, behavior: "smooth" }) } async function callQwen(promptText, attempt=1) { try { const ticker = document.querySelector("#qwen-ticker") ticker.classList.add("working") ticker.style.opacity = "1" const requestBody = { model: modelMode, messages: [ { role: "user", content: promptText } ], stream: false, max_tokens: 512, stop: ["null"], temperature: 0.7, top_p: 0.7, top_k: 50, frequency_penalty: 0.5, n: 1 } const options = { method: "POST", headers: { Authorization: `Bearer ${qwenToken}`, "Content-Type": "application/json" }, body: JSON.stringify(requestBody) } const response = await fetch(qwenEndpoint, options) if (!response.ok) { if (response.status===429 && attempt===1) { await new Promise(r=>setTimeout(r,5000)) return callQwen(promptText,2) } throw new Error(`status=${response.status}`) } const data = await response.json() const finalText = parseQwenResponse(data) ticker.classList.remove("working") setTimeout(()=>{ticker.style.opacity="0"},1200) return finalText } catch(err) { const ticker = document.querySelector("#qwen-ticker") ticker.classList.remove("working") ticker.style.opacity="0" return null } } function parseQwenResponse(jsonData) { if (!jsonData) return "[返回为空]" if (!jsonData.choices || !jsonData.choices.length) return "[choices为空]" const c = jsonData.choices[0] if (!c.message || typeof c.message.content!=="string") return "[content不是字符串]" return c.message.content.trim() || "[空]" } async function processArticle(article, title, url) { try { const userQuery = document.querySelector("textarea")?.value || "" const rolePrompt = rolePrompts[roleMode] || rolePrompts["猫娘风格"] const promptText = ` 我正在搜索与【${userQuery}】相关的内容。 有一篇文章大约100字的概括,用角色【${roleMode}】: ${rolePrompt} 如果URL无法访问可不输出。 标题:${title} 链接:${url} ` const summary = await callQwen(promptText) if (!summary) { insertError(article, "生成失败") return } let targetElement = article.parentElement.parentElement.parentElement.parentElement .nextSibling?.querySelectorAll("div>span")[1] || article.parentElement.parentElement.parentElement.parentElement .nextSibling?.querySelectorAll("div>span")[0] if (!targetElement) return targetElement.parentElement.style.webkitLineClamp = "30" article.classList.add("qwen-annotated") targetElement.textContent="" const chunkSize=20 let displayText="✦ " targetElement.textContent=displayText for (let i=0;i<summary.length;i+=chunkSize) { const chunk = summary.slice(i,i+chunkSize) const span = document.createElement("span") span.style.opacity="0" span.textContent=chunk targetElement.appendChild(span) await new Promise(r=>setTimeout(r,100)) span.style.transition="opacity 1s ease-in-out" span.style.opacity="1" } allSummaries.push({ title, summary }) const ttsBtn = document.createElement("button") ttsBtn.textContent="语音播报" ttsBtn.style.cssText="margin-left:6px; font-size:12px; cursor:pointer;" ttsBtn.addEventListener("click",()=>{ if (!window.speechSynthesis) { alert("不支持TTS") return } window.speechSynthesis.cancel() isPaused=false currentUtter=new SpeechSynthesisUtterance(summary) window.speechSynthesis.speak(currentUtter) }) targetElement.appendChild(ttsBtn) } catch(e) { insertError(article,"生成失败:"+e?.message) } } function insertError(article, msg) { let targetElement = article.parentElement.parentElement.parentElement.parentElement .nextSibling?.querySelectorAll("div>span")[1] || article.parentElement.parentElement.parentElement.parentElement .nextSibling?.querySelectorAll("div>span")[0] if (targetElement) targetElement.textContent = "[Qwen失败] "+msg } async function throttledProcessArticle(article, title, url, interval) { await new Promise(res=>setTimeout(res,interval)) return processArticle(article, title, url) } function insertTickerElement() { const ticker = document.createElement("div") ticker.id = "qwen-ticker" ticker.style.cssText=` position: fixed; right:20px; bottom:10px; font-size:2em; color:#888; transition:opacity .3s; z-index:999999; pointer-events:none; ` ticker.innerHTML="✦" document.body.appendChild(ticker) const style=document.createElement("style") style.textContent=` @keyframes rotateGlow { 0% { transform: rotate(0deg) scale(1); color:#fc8; } 25%{ transform: rotate(90deg) scale(1.2); color:#ff8;} 50%{ transform: rotate(180deg) scale(1); color:#8cf;} 75%{ transform: rotate(270deg) scale(1.2); color:#f8f;} 100%{transform: rotate(360deg) scale(1); color:#fc8;} } #qwen-ticker.working { animation: rotateGlow 1.5s linear infinite; } ` document.head.appendChild(style) } async function runAnnotation() { for (let j=0;j<30;j++){ const articles = Array.from(document.querySelectorAll("#rso>div")) .map(div=>div.querySelector("span>a:not(.qwen-annotated)")) if (!articles.length) break const tasks=articles.map((link,i)=>{ if (!link) return Promise.resolve() const href=link.getAttribute("href") const title=link.querySelector("h3")?.textContent||"无标题" if(!href)return Promise.resolve() return throttledProcessArticle(link,title,href,i*2500) }) await Promise.all(tasks) document.querySelector("#qwen-ticker").style.opacity="0" } document.querySelector("#qwen-ticker").style.opacity="0" } async function reAnnotateAll() { document.querySelectorAll(".qwen-annotated").forEach(link=>{ const container=link.parentElement?.parentElement?.parentElement?.parentElement?.nextSibling if(!container)return container.querySelectorAll("div>span").forEach(s=>s.textContent="") link.classList.remove("qwen-annotated") }) allSummaries=[] await runAnnotation() } await new Promise(r=>setTimeout(r,1000)) createControlPanel() insertTickerElement() await runAnnotation() })()