Greasy Fork

来自缓存

Greasy Fork is available in English.

SwipeSense Plus

移动端右滑英文段落,AI自动分析。针对高版本安卓优化了请求稳定性。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==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
// ==/UserScript==

(function() {
    'use strict';

    // ================= 默认配置 =================
    const DEFAULT_PROMPT = `你是一个专业的英语学习助手。
请分析用户发送的文本,找出 3-5 个较难的单词或短语。
请务必严格按照以下 HTML 格式输出(不要输出 Markdown,只输出 HTML):
<ul>
  <li><b>单词/短语</b>: 中文释义</li>
</ul>
如果不包含难词,请简要总结段落大意。`;

    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 = `
            <div class="ai-config-card">
                <div class="ai-header"><h3>AI 通道配置</h3><button id="ai-btn-clear-cache" class="ai-btn-clear-cache">清除缓存</button></div>
                <div class="ai-content-scroll">
                    <div id="ai-config-list-container"></div>
                    <button class="ai-btn ai-btn-add" id="ai-btn-add-view">+ 添加新AI</button>
                    <div id="ai-edit-area" class="ai-edit-form">
                        <h4 style="margin:0 0 10px 0; color:#333;" id="ai-edit-title">编辑配置</h4>
                        <div class="ai-form-group"><label>配置名称</label><input type="text" id="cfg-name"></div>
                        <div class="ai-form-group"><label>API URL</label><input type="text" id="cfg-url"></div>
                        <div class="ai-form-group"><label>API Key</label><input type="password" id="cfg-key"></div>
                        <div class="ai-form-group"><label>模型名称</label><input type="text" id="cfg-model"></div>
                        <div class="ai-form-group"><label>自定义提示词</label><textarea id="cfg-prompt"></textarea></div>
                        <div style="display:flex; gap:10px;"><button class="ai-btn ai-btn-secondary" id="ai-btn-cancel-edit">取消</button><button class="ai-btn ai-btn-primary" id="ai-btn-save-edit">保存</button></div>
                    </div>
                </div>
                <div class="ai-footer"><button class="ai-btn ai-btn-secondary" id="ai-btn-close">关闭</button></div>
            </div>`;
        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 = `
                <div class="ai-radio" data-action="select" data-idx="${index}"></div>
                <div class="ai-info" data-action="select" data-idx="${index}">
                    <div class="ai-name">${cfg.name}</div>
                    <div class="ai-model">${cfg.model}</div>
                </div>
                <div class="ai-actions">
                    <button class="ai-icon-btn" data-action="edit" data-idx="${index}">✎</button>
                    <button class="ai-icon-btn ai-icon-del" data-action="del" data-idx="${index}">✕</button>
                </div>`;
            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 = `<style>${SHADOW_CSS}</style><div class="ai-note-box"></div>`;
        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 = `<div class="ai-note-loading">⚡ 正在分析 (连接中)...</div>`;
            
            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 = `<div class="ai-note-main" style="color:#ef4444;"><b>分析失败</b><br/><small>${err}</small></div>`;
            });
        }
    }

    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 = `
            <div class="ai-note-main">
                <div class="ai-note-title"><span>📝 重点解析</span><span class="ai-note-source">${source}</span></div>
                <div class="ai-note-content">${html}</div>
            </div>
            <div class="ai-chat-section">
                <div class="ai-chat-history"></div>
                <div class="ai-input-wrapper">
                    <input type="text" class="ai-chat-input" placeholder="针对段落提问...">
                    <button class="ai-chat-btn">发送</button>
                </div>
            </div>`;
        
        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 = `<span>${q}</span>`;
            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 = `<span>${res}</span>`;
                history.appendChild(aiMsg);
                input.value = ''; input.disabled = false;
            }, (err) => {
                alert(err); input.disabled = false;
            });
        };
    }
})();