// ==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 = `
`;
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 = `
`;
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 = `
`;
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;
});
};
}
})();