// ==UserScript==
// @name Wikipedia AI Summary (维基百科AI总结)
// @namespace http://yhgzs-111.github.io/
// @version 1.0
// @description 维基百科 AI 摘要脚本,兼容适配OpenAI API格式的所有LLM。
// @author Deepseek, ChatGPT and NNNH
// @match https://*.wikipedia.org/*
// @icon https://www.google.com/s2/favicons?sz=64&domain=wikipedia.org
// @grant GM_registerMenuCommand
// @grant GM_setValue
// @grant GM_getValue
// @grant GM_xmlhttpRequest
// @connect *
// @license MIT
// ==/UserScript==
(function() {
'use strict';
// 默认配置
const defaultConfig = {
apiKey: '',
apiURL: '',
systemPrompt: '请用中文总结以下内容,保持关键信息的完整性:',
model: ''
};
// 初始化配置
let config = {
apiKey: GM_getValue('apiKey', defaultConfig.apiKey),
apiURL: GM_getValue('apiURL', defaultConfig.apiURL),
systemPrompt: GM_getValue('systemPrompt', defaultConfig.systemPrompt),
model: GM_getValue('model', defaultConfig.model)
};
// 注册(不可用)设置菜单
GM_registerMenuCommand("AI Summary Settings", showSettings);
// 创建浮动按钮
let floatingBtn = null;
document.addEventListener('mouseup', handleTextSelection);
function handleTextSelection(e) {
const selection = window.getSelection().toString().trim();
if (!selection) {
if (floatingBtn) {
floatingBtn.remove();
floatingBtn = null;
}
return;
}
if (!floatingBtn) {
createFloatingButton(e);
} else {
positionButton(e);
}
}
function createFloatingButton(e) {
floatingBtn = document.createElement('button');
floatingBtn.textContent = 'AI Summary';
Object.assign(floatingBtn.style, {
position: 'absolute',
zIndex: 9999,
background: '#4CAF50',
color: 'white',
border: 'none',
borderRadius: '4px',
padding: '8px 16px',
cursor: 'pointer',
boxShadow: '0 2px 5px rgba(0,0,0,0.2)'
});
positionButton(e);
floatingBtn.addEventListener('click', handleSummary);
document.body.appendChild(floatingBtn);
}
function positionButton(e) {
// 直接使用 e.pageX 与 e.pageY(包含滚动偏移),无需额外添加滚动量
floatingBtn.style.left = `${e.pageX + 15}px`;
floatingBtn.style.top = `${e.pageY - 50}px`;
}
async function handleSummary() {
const selection = window.getSelection().toString().trim();
if (!selection) return;
floatingBtn.textContent = '处理中...';
try {
const summary = await getAISummary(selection);
showSummaryPopup(summary);
} catch (error) {
alert(`错误: ${error.message}`);
} finally {
if (floatingBtn) {
floatingBtn.remove();
floatingBtn = null;
}
}
}
// 修改后的 getAISummary 函数,兼容 /v1/chat/completions 和 /v1/completions 两种接口格式
async function getAISummary(text) {
return new Promise((resolve, reject) => {
// 判断是否为 chat 接口(URL 中包含 "chat/completions")
const isChatEndpoint = config.apiURL.includes("chat/completions");
// 判断模型是否为 OpenAI 官方支持的LLM模型
const isChatModel = ["gpt-3.5-turbo", "gpt-4", "gpt-4o"].includes(config.model);
let payload;
if (isChatEndpoint && isChatModel) {
// 使用对话接口格式
payload = {
model: config.model,
messages: [
{ role: "system", content: config.systemPrompt },
{ role: "user", content: text }
],
temperature: 0.7
};
} else {
// 使用传统 completions 接口格式:将 systemPrompt 与用户输入拼接成 prompt
payload = {
model: config.model,
prompt: config.systemPrompt + "\n" + text,
temperature: 0.7,
max_tokens: 150 // 可根据需要调整
};
}
GM_xmlhttpRequest({
method: "POST",
url: config.apiURL,
headers: {
"Content-Type": "application/json",
"Authorization": `Bearer ${config.apiKey}`
},
data: JSON.stringify(payload),
onload: function(response) {
try {
const data = JSON.parse(response.responseText);
if (response.status === 200) {
let summary;
if (isChatEndpoint && isChatModel) {
summary = data.choices[0].message.content;
} else {
summary = data.choices[0].text;
}
resolve(summary);
} else {
reject(new Error(data.error?.message || 'Unknown error'));
}
} catch(e) {
reject(new Error("Response parse error: " + e.message));
}
},
onerror: function(error) {
reject(error);
}
});
});
}
function showSummaryPopup(content) {
const popup = document.createElement('div');
Object.assign(popup.style, {
position: 'fixed',
top: '50%',
left: '50%',
transform: 'translate(-50%, -50%)',
background: 'white',
padding: '20px',
borderRadius: '8px',
boxShadow: '0 4px 12px rgba(0,0,0,0.2)',
maxWidth: '600px',
width: '90%',
maxHeight: '80vh',
overflow: 'auto',
zIndex: 10000
});
const closeBtn = document.createElement('button');
closeBtn.textContent = '×';
Object.assign(closeBtn.style, {
position: 'absolute',
top: '10px',
right: '10px',
background: 'none',
border: 'none',
fontSize: '20px',
cursor: 'pointer'
});
closeBtn.onclick = () => popup.remove();
const contentDiv = document.createElement('div');
contentDiv.innerHTML = content.replace(/\n/g, '<br>');
popup.appendChild(closeBtn);
popup.appendChild(contentDiv);
document.body.appendChild(popup);
}
function showSettings() {
const settings = `
<div style="padding:20px;font-family:sans-serif">
<h2>AI Summary Settings</h2>
<form id="settingsForm">
<label>API Key:<br>
<input type="password" id="apiKey" value="${config.apiKey}" style="width:300px"></label><br><br>
<label>API URL:<br>
<input type="text" id="apiURL" value="${config.apiURL}" style="width:300px"></label><br><br>
<label>System Prompt:<br>
<textarea id="systemPrompt" rows="4" style="width:300px">${config.systemPrompt}</textarea></label><br><br>
<label>Model (手动输入):<br>
<input type="text" id="model" value="${config.model}" style="width:300px"></label><br><br>
<button type="submit">Save</button>
</form>
</div>
`;
const win = window.open('', 'AI Summary Settings', 'width=400,height=500');
win.document.write(settings);
win.document.getElementById('settingsForm').onsubmit = function(e) {
e.preventDefault();
config = {
apiKey: win.document.getElementById('apiKey').value,
apiURL: win.document.getElementById('apiURL').value,
systemPrompt: win.document.getElementById('systemPrompt').value,
model: win.document.getElementById('model').value
};
GM_setValue('apiKey', config.apiKey);
GM_setValue('apiURL', config.apiURL);
GM_setValue('systemPrompt', config.systemPrompt);
GM_setValue('model', config.model);
win.close();
};
}
})();