// ==UserScript==
// @name NS 简单图床快捷上传
// @namespace http://tampermonkey.net/
// @version 1.1
// @description 在 Nodeseek.com 的发送评论按钮旁添加图片上传功能,支持粘贴/拖拽上传,提示在右下角
// @author BreezeZhang
// @match https://*.nodeseek.com/*
// @grant GM_setValue
// @grant GM_getValue
// @grant unsafeWindow
// @connect nodeseek.com
// @connect *
// @license GPL-3.0
// ==/UserScript==
(function() {
'use strict';
let EASYIMAGE_API_URL = GM_getValue('EASYIMAGE_API_URL', '');
let EASYIMAGE_TOKEN = GM_getValue('EASYIMAGE_TOKEN', '');
let LANKONG_API_URL = GM_getValue('LANKONG_API_URL', '');
let LANKONG_EMAIL = GM_getValue('LANKONG_EMAIL', '');
let LANKONG_PASSWORD = GM_getValue('LANKONG_PASSWORD', '');
let UPLOAD_TYPE = GM_getValue('UPLOAD_TYPE', 'easyimage');
if (!EASYIMAGE_API_URL || !EASYIMAGE_TOKEN) {
showConfigModal();
return;
}
initScript();
// 提取域名(仅用于日志和提示)
function extractDomain(url) {
try {
const urlObj = new URL(url);
return urlObj.hostname;
} catch (e) {
console.error('无效的 API URL:', url, e);
return '';
}
}
function showConfigModal(isUpdate = false) {
const modal = document.createElement('div');
modal.style.cssText = `
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.5);
display: flex;
justify-content: center;
align-items: center;
z-index: 1000;
`;
const modalContent = document.createElement('div');
modalContent.style.cssText = `
background-color: #ffffff;
padding: 20px;
border-radius: 8px;
width: 350px;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
display: flex;
flex-direction: column;
`;
const title = document.createElement('h3');
title.textContent = isUpdate ? '更新配置' : '初始配置';
title.style.cssText = `
margin-bottom: 20px;
text-align: center;
color: #333;
font-size: 1.2em;
`;
const uploadTypeLabel = document.createElement('label');
uploadTypeLabel.textContent = '选择图床类型:';
uploadTypeLabel.style.cssText = `
display: block;
margin-bottom: 5px;
color: #666;
font-size: 0.9em;
`;
const uploadTypeSelect = document.createElement('select');
uploadTypeSelect.style.cssText = `
width: 100%;
padding: 8px;
margin-bottom: 15px;
border: 1px solid #ddd;
border-radius: 4px;
font-size: 1em;
`;
const easyImageOption = document.createElement('option');
easyImageOption.value = 'easyimage';
easyImageOption.textContent = '简单图床';
const lankongOption = document.createElement('option');
lankongOption.value = 'lankong';
lankongOption.textContent = '兰空图床';
uploadTypeSelect.appendChild(easyImageOption);
uploadTypeSelect.appendChild(lankongOption);
uploadTypeSelect.value = UPLOAD_TYPE;
const apiLabel = document.createElement('label');
apiLabel.textContent = 'API URL:';
apiLabel.style.cssText = `
display: block;
margin-bottom: 5px;
color: #666;
font-size: 0.9em;
`;
const apiInput = document.createElement('input');
apiInput.type = 'text';
apiInput.value = UPLOAD_TYPE === 'easyimage' ? EASYIMAGE_API_URL : LANKONG_API_URL;
apiInput.style.cssText = `
width: 100%;
padding: 8px;
margin-bottom: 15px;
border: 1px solid #ddd;
border-radius: 4px;
font-size: 1em;
`;
const tokenLabel = document.createElement('label');
tokenLabel.textContent = 'Token:';
tokenLabel.style.cssText = apiLabel.style.cssText;
const tokenInput = document.createElement('input');
tokenInput.type = 'text';
tokenInput.value = UPLOAD_TYPE === 'easyimage' ? EASYIMAGE_TOKEN : '';
tokenInput.style.cssText = apiInput.style.cssText;
const emailLabel = document.createElement('label');
emailLabel.textContent = '邮箱:';
emailLabel.style.cssText = apiLabel.style.cssText;
const emailInput = document.createElement('input');
emailInput.type = 'text';
emailInput.value = UPLOAD_TYPE === 'lankong' ? LANKONG_EMAIL : '';
emailInput.style.cssText = apiInput.style.cssText;
const passwordLabel = document.createElement('label');
passwordLabel.textContent = '密码:';
passwordLabel.style.cssText = apiLabel.style.cssText;
const passwordInput = document.createElement('input');
passwordInput.type = 'password';
passwordInput.value = UPLOAD_TYPE === 'lankong' ? LANKONG_PASSWORD : '';
passwordInput.style.cssText = apiInput.style.cssText;
const buttonContainer = document.createElement('div');
buttonContainer.style.cssText = `
display: flex;
justify-content: flex-end;
margin-top: 20px;
`;
const confirmBtn = document.createElement('button');
confirmBtn.textContent = '保存';
confirmBtn.style.cssText = `
padding: 10px 20px;
background-color: #4CAF50;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 1em;
margin-right: 10px;
transition: background-color 0.3s ease;
`;
confirmBtn.addEventListener('click', () => {
const apiUrl = apiInput.value.trim();
const token = tokenInput.value.trim();
const email = emailInput.value.trim();
const password = passwordInput.value.trim();
const uploadType = uploadTypeSelect.value;
if (!apiUrl || (uploadType === 'easyimage' && !token) || (uploadType === 'lankong' && (!email || !password))) {
alert('请填写所有必填字段!');
return;
}
UPLOAD_TYPE = uploadType;
GM_setValue('UPLOAD_TYPE', UPLOAD_TYPE);
if (uploadType === 'easyimage') {
EASYIMAGE_API_URL = apiUrl;
EASYIMAGE_TOKEN = token;
GM_setValue('EASYIMAGE_API_URL', EASYIMAGE_API_URL);
GM_setValue('EASYIMAGE_TOKEN', EASYIMAGE_TOKEN);
} else if (uploadType === 'lankong') {
LANKONG_API_URL = apiUrl;
LANKONG_EMAIL = email;
LANKONG_PASSWORD = password;
GM_setValue('LANKONG_API_URL', LANKONG_API_URL);
GM_setValue('LANKONG_EMAIL', LANKONG_EMAIL);
GM_setValue('LANKONG_PASSWORD', LANKONG_PASSWORD);
}
document.body.removeChild(modal);
if (isUpdate) alert('配置更新成功!');
initScript();
});
const cancelBtn = document.createElement('button');
cancelBtn.textContent = '取消';
cancelBtn.style.cssText = `
padding: 10px 20px;
background-color: #f44336;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 1em;
transition: background-color 0.3s ease;
`;
cancelBtn.addEventListener('click', () => {
document.body.removeChild(modal);
if (!isUpdate) alert('配置未完成,脚本将无法运行!');
});
buttonContainer.appendChild(confirmBtn);
buttonContainer.appendChild(cancelBtn);
modalContent.appendChild(title);
modalContent.appendChild(uploadTypeLabel);
modalContent.appendChild(uploadTypeSelect);
modalContent.appendChild(apiLabel);
modalContent.appendChild(apiInput);
modalContent.appendChild(tokenLabel);
modalContent.appendChild(tokenInput);
modalContent.appendChild(emailLabel);
modalContent.appendChild(emailInput);
modalContent.appendChild(passwordLabel);
modalContent.appendChild(passwordInput);
modalContent.appendChild(buttonContainer);
modal.appendChild(modalContent);
document.body.appendChild(modal);
modal.addEventListener('click', (e) => {
if (e.target === modal) document.body.removeChild(modal);
});
confirmBtn.addEventListener('mouseover', () => {
confirmBtn.style.backgroundColor = '#45a049';
});
confirmBtn.addEventListener('mouseout', () => {
confirmBtn.style.backgroundColor = '#4CAF50';
});
cancelBtn.addEventListener('mouseover', () => {
cancelBtn.style.backgroundColor = '#da190b';
});
cancelBtn.addEventListener('mouseout', () => {
cancelBtn.style.backgroundColor = '#f44336';
});
uploadTypeSelect.addEventListener('change', () => {
const uploadType = uploadTypeSelect.value;
if (uploadType === 'easyimage') {
apiLabel.textContent = 'API URL:';
tokenLabel.textContent = 'Token:';
emailLabel.style.display = 'none';
emailInput.style.display = 'none';
passwordLabel.style.display = 'none';
passwordInput.style.display = 'none';
apiInput.value = EASYIMAGE_API_URL || 'https://example.com/api/index.php';
tokenInput.value = EASYIMAGE_TOKEN || '';
} else if (uploadType === 'lankong') {
apiLabel.textContent = '兰空图床域名:';
tokenLabel.textContent = 'Token:';
emailLabel.style.display = 'block';
emailInput.style.display = 'block';
passwordLabel.style.display = 'block';
passwordInput.style.display = 'block';
apiInput.value = LANKONG_API_URL || 'https://你的兰空图床域名';
tokenInput.value = '';
emailInput.value = LANKONG_EMAIL || 'YOUR_EMAIL_HERE';
passwordInput.value = LANKONG_PASSWORD || 'YOUR_PASSWORD_HERE';
}
});
uploadTypeSelect.dispatchEvent(new Event('change'));
}
function initScript() {
createUploadButton();
setupPasteAndDrop();
addPlaceholder();
const observer = new MutationObserver(() => {
createUploadButton();
setupPasteAndDrop();
addPlaceholder();
});
observer.observe(document.body, { childList: true, subtree: true });
}
function createUploadButton() {
const topicSelect = document.querySelector('.topic-select');
if (!topicSelect || topicSelect.querySelector('.easyimage-upload-container')) return;
const submitBtn = topicSelect.querySelector('.submit.btn');
if (!submitBtn) return;
const container = document.createElement('div');
container.className = 'easyimage-upload-container';
container.style.cssText = 'margin:5px 0;display:inline-block';
const uploadButton = document.createElement('button');
uploadButton.textContent = '📷 上传图片';
uploadButton.style.cssText = 'padding:5px 10px;background-color:#4CAF50;color:white;border:none;border-radius:3px;cursor:pointer;margin-right:10px';
const updateButton = document.createElement('button');
updateButton.textContent = '⚙️ 更新配置';
updateButton.style.cssText = 'padding:5px 10px;background-color:#2196F3;color:white;border:none;border-radius:3px;cursor:pointer;margin-right:10px';
const fileInput = document.createElement('input');
fileInput.type = 'file';
fileInput.accept = 'image/*';
fileInput.style.display = 'none';
container.appendChild(uploadButton);
container.appendChild(updateButton);
container.appendChild(fileInput);
submitBtn.parentElement.insertBefore(container, submitBtn);
uploadButton.addEventListener('click', () => fileInput.click());
fileInput.addEventListener('change', () => {
if (fileInput.files.length === 0) return;
const file = fileInput.files[0];
uploadButton.textContent = '正在上传...';
uploadButton.disabled = true;
uploadImage(file, (success, result) => {
uploadButton.textContent = '📷 上传图片';
uploadButton.disabled = false;
if (success) insertMarkdown(result);
else alert('上传失败:' + result);
});
});
updateButton.addEventListener('click', () => showConfigModal(true));
}
function setupPasteAndDrop() {
const editor = document.querySelector('.CodeMirror');
if (!editor) return;
const cm = editor.CodeMirror;
if (!cm) return;
if (editor.dataset.pasteDropSetup) return;
editor.dataset.pasteDropSetup = 'true';
cm.on('paste', (cmInstance, event) => {
const items = (event.clipboardData || window.clipboardData).items;
for (let i = 0; i < items.length; i++) {
if (items[i].type.indexOf('image') === 0) {
event.preventDefault();
const file = items[i].getAsFile();
uploadImage(file, (success, result) => {
if (success) insertMarkdown(result);
else alert('上传失败:' + result);
});
break;
}
}
});
let uploadInProgress = false;
editor.addEventListener('dragover', (event) => {
event.preventDefault();
event.dataTransfer.dropEffect = 'copy';
});
editor.addEventListener('drop', (event) => {
event.preventDefault();
event.stopPropagation();
if (uploadInProgress) {
console.log('Drop event skipped: upload in progress');
return;
}
const files = event.dataTransfer.files;
if (files.length > 0 && files[0].type.indexOf('image') === 0) {
const file = files[0];
console.log('Starting upload for file:', file.name);
uploadInProgress = true;
uploadImage(file, (success, result) => {
uploadInProgress = false;
if (success) {
insertMarkdown(result);
console.log('Upload completed for:', file.name);
} else {
alert('上传失败:' + result);
console.log('Upload failed for:', file.name, result);
}
});
} else {
console.log('No image file dropped');
}
});
}
function addPlaceholder() {
const editor = document.querySelector('.CodeMirror');
if (!editor || editor.querySelector('.easyimage-placeholder')) return;
const placeholder = document.createElement('div');
placeholder.className = 'easyimage-placeholder';
placeholder.textContent = '拖拽或粘贴图片可以上传图片';
placeholder.style.cssText = 'position:absolute;bottom:10px;right:10px;color:#aaa;font-size:12px;pointer-events:none;z-index:1';
editor.appendChild(placeholder);
const cm = editor.CodeMirror;
if (cm) {
cm.on('change', () => {
placeholder.style.display = cm.getValue().trim() ? 'none' : 'block';
});
}
}
function uploadImage(file, callback) {
const fileKey = `${file.name}-${file.size}-${file.lastModified}`;
if (window.uploadedFiles && window.uploadedFiles[fileKey]) {
console.log('File already uploaded:', fileKey);
return callback(true, window.uploadedFiles[fileKey]);
}
if (UPLOAD_TYPE === 'easyimage') {
const apiDomain = extractDomain(EASYIMAGE_API_URL);
console.log('Uploading to:', EASYIMAGE_API_URL, 'Domain:', apiDomain, 'with token:', EASYIMAGE_TOKEN);
const formData = new FormData();
formData.append('image', file);
formData.append('token', EASYIMAGE_TOKEN);
// 使用 unsafeWindow.fetch 发起请求,绕过 Tampermonkey 的 @connect 限制
unsafeWindow.fetch(EASYIMAGE_API_URL, {
method: 'POST',
body: formData,
timeout: 10000 // 10 秒超时(fetch 本身不支持 timeout,需手动实现)
})
.then(response => {
if (!response.ok) {
throw new Error(`HTTP error! Status: ${response.status}`);
}
return response.json();
})
.then(result => {
console.log('Response:', result);
if (result.result === 'success' && result.code === 200) {
if (!window.uploadedFiles) window.uploadedFiles = {};
window.uploadedFiles[fileKey] = result.url;
callback(true, result.url);
} else {
callback(false, result.message || '服务器返回错误');
}
})
.catch(error => {
console.error('Fetch error:', error);
callback(false, '网络错误:' + error.message);
});
} else if (UPLOAD_TYPE === 'lankong') {
const apiDomain = extractDomain(LANKONG_API_URL);
console.log('Fetching token from:', LANKONG_API_URL, 'Domain:', apiDomain);
unsafeWindow.fetch(`${LANKONG_API_URL}/api/v1/tokens`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
email: LANKONG_EMAIL,
password: LANKONG_PASSWORD
})
})
.then(response => response.json())
.then(data => {
if (data.status) {
const token = data.data.token;
console.log('Token: ', token);
const formData = new FormData();
formData.append('image', file);
unsafeWindow.fetch(`${LANKONG_API_URL}/api/v1/upload`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${token}`
},
body: formData,
timeout: 10000 // 10 秒超时(fetch 本身不支持 timeout,需手动实现)
})
.then(response => response.json())
.then(result => {
if (result.status) {
if (!window.uploadedFiles) window.uploadedFiles = {};
window.uploadedFiles[fileKey] = result.data.url;
callback(true, result.data.url);
} else {
callback(false, result.message || '上传失败');
}
})
.catch(error => {
console.error('Upload error:', error);
callback(false, '网络错误:' + error.message);
});
} else {
callback(false, data.message || '获取Token失败');
}
})
.catch(error => {
console.error('Fetch token error:', error);
callback(false, '网络错误:' + error.message);
});
}
}
function insertMarkdown(url) {
const markdown = ``;
const editor = document.querySelector('.CodeMirror');
if (editor) {
const cm = editor.CodeMirror;
if (cm) {
cm.replaceRange(markdown, cm.getCursor());
cm.focus();
showSuccessMessage();
}
}
}
function showSuccessMessage() {
const topicSelect = document.querySelector('.topic-select');
const successMsg = document.createElement('div');
successMsg.textContent = '🎉 图片上传成功!';
successMsg.style.cssText = 'color:#4CAF50;margin:5px 0';
topicSelect.insertBefore(successMsg, topicSelect.firstChild);
setTimeout(() => successMsg.remove(), 2000);
}
})();