// ==UserScript== // @name SwipeSense Plus // @namespace http://tampermonkey.net/ // @version 0.9 // @description 移动端右滑英文段落,AI自动分析。针对高版本安卓优化了请求稳定性。 // @author MoodHappy // @match *://*/* // @grant GM_xmlhttpRequest // @grant GM_addStyle // @grant GM_setValue // @grant GM_getValue // @grant GM_registerMenuCommand // @grant GM_notification // @connect * // @run-at document-end // @downloadURL https://update.greasyfork.icu/scripts/560773/SwipeSense%20Plus.user.js // @updateURL https://update.greasyfork.icu/scripts/560773/SwipeSense%20Plus.meta.js // ==/UserScript== (function() { 'use strict'; // ================= 默认配置 ================= const DEFAULT_PROMPT = `你是一个专业的英语学习助手。 请分析用户发送的文本,找出 3-5 个较难的单词或短语。 请务必严格按照以下 HTML 格式输出(不要输出 Markdown,只输出 HTML): 如果不包含难词,请简要总结段落大意。`; const DEFAULT_CONFIG_TEMPLATE = { name: "默认AI (ChatAnywhere)", url: "https://api.chatanywhere.tech/v1/chat/completions", key: "", model: "gpt-3.5-turbo", prompt: DEFAULT_PROMPT }; const KEYS = { CONFIG_LIST: 'ai_config_list_v3', SELECTED_INDEX: 'ai_selected_index_v3', CACHE: 'ai_annotation_cache' }; // ================= CSS 定义 ================= GM_addStyle(` #ai-config-modal { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.5); z-index: 100000; display: flex; justify-content: center; align-items: center; backdrop-filter: blur(3px); opacity: 0; pointer-events: none; transition: opacity 0.2s; } #ai-config-modal.show { opacity: 1; pointer-events: auto; } .ai-config-card { background: white; width: 90%; max-width: 420px; max-height: 90vh; border-radius: 12px; box-shadow: 0 4px 20px rgba(0,0,0,0.2); font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; display: flex; flex-direction: column; overflow: hidden; } .ai-header { padding: 15px; border-bottom: 1px solid #eee; display: flex; justify-content: space-between; align-items: center; background: #f8fafc; } .ai-header h3 { margin: 0; font-size: 16px; color: #333; } .ai-content-scroll { padding: 15px; overflow-y: auto; flex: 1; } .ai-list-item { display: flex; align-items: center; padding: 10px; border: 1px solid #e2e8f0; border-radius: 8px; margin-bottom: 8px; background: #fff; transition: all 0.2s; } .ai-list-item.active { border-color: #0ea5e9; background: #f0f9ff; } .ai-radio { width: 18px; height: 18px; border-radius: 50%; border: 2px solid #cbd5e1; margin-right: 12px; cursor: pointer; flex-shrink: 0; display: flex; justify-content: center; align-items: center; } .ai-list-item.active .ai-radio { border-color: #0ea5e9; } .ai-list-item.active .ai-radio::after { content: ''; width: 8px; height: 8px; background: #0ea5e9; border-radius: 50%; } .ai-info { flex: 1; overflow: hidden; cursor: pointer;} .ai-name { font-weight: 600; font-size: 14px; color: #334155; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } .ai-model { font-size: 11px; color: #94a3b8; } .ai-actions { display: flex; gap: 8px; } .ai-icon-btn { padding: 6px; border-radius: 4px; border: none; background: transparent; cursor: pointer; color: #64748b; font-size: 16px; display: flex; align-items: center; } .ai-icon-btn:hover { background: #f1f5f9; color: #0ea5e9; } .ai-icon-del:hover { color: #ef4444; } .ai-edit-form { display: none; padding-top: 10px; border-top: 1px solid #eee; margin-top: 10px;} .ai-edit-form.show { display: block; } .ai-form-group { margin-bottom: 12px; } .ai-form-group label { display: block; font-size: 12px; color: #666; margin-bottom: 4px; } .ai-form-group input, .ai-form-group textarea { width: 100%; padding: 8px; box-sizing: border-box; border: 1px solid #ddd; border-radius: 6px; font-size: 14px; background: #f9f9f9; font-family: inherit; } .ai-form-group textarea { resize: vertical; min-height: 80px; } .ai-footer { padding: 15px; border-top: 1px solid #eee; display: flex; gap: 10px; background: #fff; } .ai-btn { flex: 1; padding: 10px; border: none; border-radius: 6px; font-size: 14px; cursor: pointer; text-align: center;} .ai-btn-primary { background: #0ea5e9; color: white; } .ai-btn-secondary { background: #e2e8f0; color: #333; } .ai-btn-add { background: #10b981; color: white; margin-bottom: 15px; width: 100%; } .ai-btn-clear-cache { font-size: 11px; color: #ef4444; background: none; border: none; text-decoration: underline; cursor: pointer;} `); const SHADOW_CSS = ` :host { all: initial; display: block; font-family: sans-serif; font-size: 14px; margin: 8px 0 16px 0; } .ai-note-box { background-color: #f0f9ff; border-left: 4px solid #0ea5e9; border-radius: 6px; line-height: 1.6; color: #334155; box-shadow: 0 2px 6px rgba(0,0,0,0.08); overflow: hidden; animation: fadeIn 0.3s ease-in-out; } .ai-note-main { padding: 12px; } .ai-note-loading { padding: 12px; color: #64748b; font-size: 13px; display: flex; align-items: center; gap: 6px;} .ai-note-title { font-weight: bold; color: #0369a1; margin-bottom: 6px; font-size: 12px; text-transform: uppercase; display: flex; justify-content: space-between; align-items: center; } .ai-note-source { font-weight: normal; font-size: 10px; color: #94a3b8; background: rgba(255,255,255,0.8); padding: 2px 6px; border-radius: 4px; } .ai-note-content ul { margin: 0; padding-left: 18px; } .ai-note-content li { margin-bottom: 5px; } .ai-chat-section { background: #e0f2fe; border-top: 1px solid #bae6fd; padding: 10px; } .ai-chat-history { margin-bottom: 10px; font-size: 13px; display: flex; flex-direction: column; gap: 8px;} .ai-msg-user { align-self: flex-end; color: #555; font-size: 12px; max-width: 85%; } .ai-msg-user span { background: #fff; padding: 4px 8px; border-radius: 8px 8px 0 8px; display: inline-block; box-shadow: 0 1px 2px rgba(0,0,0,0.05); } .ai-msg-ai { align-self: flex-start; color: #333; max-width: 95%; } .ai-msg-ai span { display: block; background: rgba(255,255,255,0.6); padding: 6px 8px; border-radius: 0 8px 8px 8px; border-left: 2px solid #0ea5e9; } .ai-input-wrapper { display: flex; gap: 6px; } .ai-chat-input { flex: 1; padding: 8px; border: 1px solid #cbd5e1; border-radius: 4px; font-size: 13px; outline: none; background: white; color: #333; } .ai-chat-input:focus { border-color: #0ea5e9; } .ai-chat-btn { background: #0ea5e9; color: white; border: none; border-radius: 4px; padding: 0 12px; font-size: 13px; cursor: pointer; } .ai-chat-btn:disabled { background: #94a3b8; cursor: not-allowed; } @keyframes fadeIn { from { opacity: 0; transform: translateY(-5px); } to { opacity: 1; transform: translateY(0); } } `; // ================= 配置管理 ================= const ConfigManager = { getList: () => { let list = GM_getValue(KEYS.CONFIG_LIST, [DEFAULT_CONFIG_TEMPLATE]); return list.map(item => ({...item, prompt: item.prompt || DEFAULT_PROMPT})); }, setList: (list) => GM_setValue(KEYS.CONFIG_LIST, list), getSelectedIndex: () => { const idx = GM_getValue(KEYS.SELECTED_INDEX, 0); const list = ConfigManager.getList(); return (idx >= 0 && idx < list.length) ? idx : 0; }, setSelectedIndex: (idx) => GM_setValue(KEYS.SELECTED_INDEX, idx), add: (config) => { const list = ConfigManager.getList(); list.push(config); ConfigManager.setList(list); return list.length - 1; }, update: (index, config) => { const list = ConfigManager.getList(); if(list[index]) { list[index] = config; ConfigManager.setList(list); } }, remove: (index) => { const list = ConfigManager.getList(); if(list.length <= 1) return; list.splice(index, 1); ConfigManager.setList(list); let current = ConfigManager.getSelectedIndex(); if(current >= index) ConfigManager.setSelectedIndex(Math.max(0, current - 1)); } }; // ================= UI逻辑 (略,保持原有) ================= let currentEditIndex = -1; function createUI() { const modal = document.createElement('div'); modal.id = 'ai-config-modal'; modal.className = 'notranslate'; modal.innerHTML = `

AI 通道配置

编辑配置

`; document.body.appendChild(modal); bindEvents(); } function bindEvents() { document.getElementById('ai-btn-close').onclick = () => document.getElementById('ai-config-modal').classList.remove('show'); document.getElementById('ai-btn-clear-cache').onclick = clearCache; document.getElementById('ai-btn-add-view').onclick = () => showEditForm(-1); document.getElementById('ai-btn-cancel-edit').onclick = hideEditForm; document.getElementById('ai-btn-save-edit').onclick = saveConfigFromForm; } function renderList() { const container = document.getElementById('ai-config-list-container'); const list = ConfigManager.getList(); const selectedIdx = ConfigManager.getSelectedIndex(); container.innerHTML = ''; list.forEach((cfg, index) => { const el = document.createElement('div'); el.className = `ai-list-item ${index === selectedIdx ? 'active' : ''}`; el.innerHTML = `
${cfg.name}
${cfg.model}
`; el.onclick = (e) => { const action = e.target.dataset.action || e.target.parentElement.dataset.action; const idx = parseInt(e.target.dataset.idx || e.target.parentElement.dataset.idx); if (action === 'select') { ConfigManager.setSelectedIndex(idx); renderList(); } else if (action === 'edit') showEditForm(idx); else if (action === 'del') { if(confirm("删除?")) { ConfigManager.remove(idx); renderList(); } } }; container.appendChild(el); }); } function showEditForm(index) { currentEditIndex = index; const list = ConfigManager.getList(); const data = index === -1 ? { name: "", url: "", key: "", model: "", prompt: DEFAULT_PROMPT } : list[index]; document.getElementById('cfg-name').value = data.name; document.getElementById('cfg-url').value = data.url; document.getElementById('cfg-key').value = data.key; document.getElementById('cfg-model').value = data.model; document.getElementById('cfg-prompt').value = data.prompt; document.getElementById('ai-config-list-container').style.display = 'none'; document.getElementById('ai-btn-add-view').style.display = 'none'; document.getElementById('ai-edit-area').classList.add('show'); } function hideEditForm() { document.getElementById('ai-edit-area').classList.remove('show'); document.getElementById('ai-config-list-container').style.display = 'block'; document.getElementById('ai-btn-add-view').style.display = 'block'; } function saveConfigFromForm() { const cfg = { name: document.getElementById('cfg-name').value.trim() || '未命名', url: document.getElementById('cfg-url').value.trim(), key: document.getElementById('cfg-key').value.trim(), model: document.getElementById('cfg-model').value.trim(), prompt: document.getElementById('cfg-prompt').value.trim() }; if(!cfg.url || !cfg.key) return alert("必填URL和Key"); if(currentEditIndex === -1) ConfigManager.add(cfg); else ConfigManager.update(currentEditIndex, cfg); hideEditForm(); renderList(); } function clearCache() { if(confirm('清除缓存?')) GM_setValue(KEYS.CACHE, {}); } GM_registerMenuCommand("⚙️ AI 多源配置", () => { if(!document.getElementById('ai-config-modal')) createUI(); renderList(); document.getElementById('ai-config-modal').classList.add('show'); }); // ================= 滑动交互 ================= let touchStartX = 0, touchStartY = 0; document.addEventListener('touchstart', (e) => { touchStartX = e.changedTouches[0].screenX; touchStartY = e.changedTouches[0].screenY; }, { passive: true }); document.addEventListener('touchend', (e) => { const endX = e.changedTouches[0].screenX; const endY = e.changedTouches[0].screenY; if ((endX - touchStartX) > 80 && Math.abs(endY - touchStartY) < 60) { const p = e.target.closest('p'); if (p && p.textContent.trim().length > 15) toggleAnnotation(p); } }, { passive: true }); // ================= 核心 AI 请求修复逻辑 ================= function toggleAnnotation(p) { const existing = p.nextElementSibling; if (existing && existing.tagName === 'AI-ANNOTATION-HOST') { existing.remove(); return; } const host = document.createElement('ai-annotation-host'); p.parentNode.insertBefore(host, p.nextSibling); const shadow = host.attachShadow({ mode: 'open' }); shadow.innerHTML = `
`; const noteBox = shadow.querySelector('.ai-note-box'); const text = p.textContent.trim(); const hash = "h" + Math.abs(text.split("").reduce((a,b)=>{a=((a<<5)-a)+b.charCodeAt(0);return a&a},0)); const cache = GM_getValue(KEYS.CACHE, {}); if (cache[hash]) { renderContent(shadow, noteBox, cache[hash].content, cache[hash].source, text); } else { noteBox.innerHTML = `
⚡ 正在分析 (连接中)...
`; const configList = ConfigManager.getList(); const currentIdx = ConfigManager.getSelectedIndex(); const messages = [ { role: "system", content: configList[currentIdx].prompt }, { role: "user", content: `Text: "${text}"` } ]; callAIWithFailover(messages, (content, name) => { cache[hash] = { content, source: name }; GM_setValue(KEYS.CACHE, cache); renderContent(shadow, noteBox, content, name, text); }, (err) => { noteBox.innerHTML = `
分析失败
${err}
`; }); } } function callAIWithFailover(messages, onSuccess, onError) { const configList = ConfigManager.getList(); const startIndex = ConfigManager.getSelectedIndex(); const tryOrder = configList.map((_, i) => (startIndex + i) % configList.length); let attempt = 0; let lastErr = ""; function next() { if (attempt >= tryOrder.length) return onError(lastErr); const cfg = configList[tryOrder[attempt++]]; // 针对高版本安卓优化的请求头 const headers = { "Content-Type": "application/json", "Authorization": `Bearer ${cfg.key.trim()}`, "User-Agent": navigator.userAgent, // 必须携带 UA "Accept": "application/json" }; GM_xmlhttpRequest({ method: "POST", url: cfg.url.trim(), headers: headers, data: JSON.stringify({ model: cfg.model.trim(), messages: messages, temperature: 0.3 }), timeout: 30000, // 增加到30秒 onload: (res) => { if (res.status === 200) { try { const data = JSON.parse(res.responseText); const content = data.choices[0].message.content; onSuccess(content.replace(/```html|```/g, '').trim(), cfg.name); } catch(e) { lastErr = "解析数据失败"; next(); } } else { lastErr = `错误码:${res.status} - ${res.statusText || '无响应'}`; next(); } }, onerror: (res) => { lastErr = `网络拦截或域解析失败(Status: ${res.status})`; next(); }, ontimeout: () => { lastErr = `线路[${cfg.name}]连接超时(30s)`; next(); } }); } next(); } function renderContent(shadow, container, html, source, originalText) { container.innerHTML = `
📝 重点解析${source}
${html}
`; const input = container.querySelector('.ai-chat-input'); const btn = container.querySelector('.ai-chat-btn'); const history = container.querySelector('.ai-chat-history'); btn.onclick = () => { const q = input.value.trim(); if(!q) return; const userMsg = document.createElement('div'); userMsg.className = 'ai-msg-user'; userMsg.innerHTML = `${q}`; history.appendChild(userMsg); input.value = 'AI 思考中...'; input.disabled = true; const msgs = [ { role: "system", content: "You are a helpful English tutor." }, { role: "user", content: `Context: ${originalText}\nQuestion: ${q}` } ]; callAIWithFailover(msgs, (res) => { const aiMsg = document.createElement('div'); aiMsg.className = 'ai-msg-ai'; aiMsg.innerHTML = `${res}`; history.appendChild(aiMsg); input.value = ''; input.disabled = false; }, (err) => { alert(err); input.disabled = false; }); }; } })();