// ==UserScript==
// @name 水源图床上传工具
// @namespace http://tampermonkey.net/
// @version 1.1
// @description 向水源图床上传图片,自动携带cookie
// @author Labyrinth
// @icon https://shuiyuan.s3.jcloud.sjtu.edu.cn/original/4X/f/2/3/f23eba8f728e684ad7c9fc3529083e03cc054fc2.svg
// @match https://shuiyuan.sjtu.edu.cn/*
// @match https://notes.sjtu.edu.cn/*
// @grant GM_xmlhttpRequest
// @grant GM_setValue
// @grant GM_getValue
// @license MIT
// @connect notes.sjtu.edu.cn
// ==/UserScript==
(function() {
'use strict';
// Cookie处理函数
function getCookiesForDomain(domain) {
const cookies = document.cookie.split(';');
const domainCookies = [];
cookies.forEach(cookie => {
const trimmedCookie = cookie.trim();
if (trimmedCookie) {
domainCookies.push(trimmedCookie);
}
});
return domainCookies.join('; ');
}
// 检查是否在notes.sjtu.edu.cn域名下,如果是则保存cookie
function saveCookiesIfNeeded() {
if (window.location.hostname === 'notes.sjtu.edu.cn') {
const cookies = getCookiesForDomain('notes.sjtu.edu.cn');
if (cookies) {
GM_setValue('notes_sjtu_cookies', cookies);
console.log('已保存水源笔记Cookie');
}
}
}
// 获取保存的cookie
function getSavedCookies() {
return GM_getValue('notes_sjtu_cookies', '');
}
// 创建上传按钮和界面
function createUploadInterface() {
// 创建上传容器
const uploadContainer = document.createElement('div');
uploadContainer.id = 'shuiyuan-upload-container';
uploadContainer.style.cssText = `
position: fixed;
top: 20px;
right: 20px;
width: 300px;
background: #fff;
border: 2px solid #007bff;
border-radius: 8px;
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
z-index: 10000;
font-family: Arial, sans-serif;
display: none;
`;
// 创建标题栏
const titleBar = document.createElement('div');
titleBar.style.cssText = `
background: #007bff;
color: white;
padding: 10px;
border-radius: 6px 6px 0 0;
font-weight: bold;
cursor: move;
display: flex;
justify-content: space-between;
align-items: center;
`;
titleBar.innerHTML = `
<span>水源图床上传</span>
<span id="close-upload-panel" style="cursor: pointer; font-size: 18px;">×</span>
`;
// 创建内容区域
const content = document.createElement('div');
content.style.padding = '15px';
const savedCookies = getSavedCookies();
const isNotesPage = window.location.hostname === 'notes.sjtu.edu.cn';
content.innerHTML = `
<div style="margin-bottom: 15px; padding: 8px; border-radius: 4px; ${savedCookies ? 'background: #d4edda; border: 1px solid #c3e6cb; color: #155724;' : 'background: #f8d7da; border: 1px solid #f5c6cb; color: #721c24;'}">
<div style="font-weight: bold; margin-bottom: 5px;">登录状态:</div>
<div style="font-size: 12px;">
${savedCookies ? '✅ 已检测到登录Cookie' : '❌ 未检测到登录Cookie'}
</div>
${!isNotesPage && !savedCookies ? '<div style="font-size: 11px; margin-top: 5px;">请先访问 <a href="https://notes.sjtu.edu.cn" target="_blank" style="color: #721c24;">notes.sjtu.edu.cn</a> 登录</div>' : ''}
${isNotesPage ? '<button id="refresh-cookies-btn" style="width: 100%; margin-top: 5px; padding: 5px; background: #007bff; color: white; border: none; border-radius: 3px; cursor: pointer; font-size: 12px;">刷新登录状态</button>' : ''}
</div>
<div style="margin-bottom: 15px;">
<label style="display: block; margin-bottom: 5px; font-weight: bold;">选择图片:</label>
<input type="file" id="image-input" accept="image/*" style="width: 100%; padding: 5px; border: 1px solid #ddd; border-radius: 4px; margin-bottom: 8px;">
<div style="text-align: center; color: #666; font-size: 12px; margin-bottom: 8px;">或</div>
<div id="paste-area" style="width: 100%; height: 80px; border: 2px dashed #ddd; border-radius: 4px; display: flex; align-items: center; justify-content: center; cursor: pointer; background: #f8f9fa; transition: all 0.3s ease;">
<div style="text-align: center; color: #666; font-size: 13px;">
<div>📋 点击此处并按 Ctrl+V 粘贴图片</div>
<div style="font-size: 11px; margin-top: 3px;">或右键选择"粘贴"</div>
</div>
</div>
<div id="paste-preview" style="margin-top: 8px; display: none;">
<div style="font-size: 12px; color: #28a745; margin-bottom: 5px;">✅ 已粘贴图片:</div>
<img id="preview-image" style="max-width: 100%; max-height: 100px; border-radius: 4px; border: 1px solid #ddd;">
</div>
</div>
<div style="margin-bottom: 15px;">
<button id="upload-btn" style="width: 100%; padding: 10px; background: #28a745; color: white; border: none; border-radius: 4px; cursor: pointer; font-size: 14px;" ${!savedCookies ? 'disabled' : ''}>上传图片</button>
</div>
<div id="upload-status" style="padding: 8px; border-radius: 4px; display: none;"></div>
<div id="upload-result" style="margin-top: 10px; display: none;">
<label style="display: block; margin-bottom: 5px; font-weight: bold;">图片链接:</label>
<textarea id="result-url" readonly style="width: 100%; height: 40px; padding: 5px; border: 1px solid #ddd; border-radius: 4px; resize: none; margin-bottom: 5px;"></textarea>
<label style="display: block; margin-bottom: 5px; font-weight: bold;">HTML格式:</label>
<textarea id="result-html" readonly style="width: 100%; height: 40px; padding: 5px; border: 1px solid #ddd; border-radius: 4px; resize: none; margin-bottom: 5px;"></textarea>
<label style="display: block; margin-bottom: 5px; font-weight: bold;">Markdown格式:</label>
<textarea id="result-markdown" readonly style="width: 100%; height: 40px; padding: 5px; border: 1px solid #ddd; border-radius: 4px; resize: none; margin-bottom: 10px;"></textarea>
<div style="display: flex; gap: 5px;">
<button id="copy-url-btn" style="flex: 1; padding: 8px; background: #17a2b8; color: white; border: none; border-radius: 4px; cursor: pointer; font-size: 12px;">复制链接</button>
<button id="copy-html-btn" style="flex: 1; padding: 8px; background: #28a745; color: white; border: none; border-radius: 4px; cursor: pointer; font-size: 12px;">复制HTML</button>
<button id="copy-markdown-btn" style="flex: 1; padding: 8px; background: #fd7e14; color: white; border: none; border-radius: 4px; cursor: pointer; font-size: 12px;">复制MD</button>
</div>
</div>
`;
uploadContainer.appendChild(titleBar);
uploadContainer.appendChild(content);
document.body.appendChild(uploadContainer);
// 创建触发按钮
const triggerBtn = document.createElement('button');
triggerBtn.id = 'show-upload-panel';
triggerBtn.innerHTML = '📷 上传图片';
triggerBtn.style.cssText = `
position: fixed;
top: 20px;
right: 20px;
padding: 10px 15px;
background: #007bff;
color: white;
border: none;
border-radius: 5px;
cursor: pointer;
z-index: 9998;
font-size: 14px;
box-shadow: 0 2px 8px rgba(0,0,0,0.2);
`;
document.body.appendChild(triggerBtn);
return { uploadContainer, triggerBtn };
}
// 粘贴处理相关函数
let pastedFile = null;
// 处理粘贴事件
function handlePaste(e) {
e.preventDefault();
const items = e.clipboardData.items;
for (let i = 0; i < items.length; i++) {
const item = items[i];
if (item.type.indexOf('image') !== -1) {
const file = item.getAsFile();
if (file) {
setPastedImage(file);
break;
}
}
}
}
// 设置粘贴的图片
function setPastedImage(file) {
pastedFile = file;
// 创建预览
const reader = new FileReader();
reader.onload = function(e) {
const previewDiv = document.getElementById('paste-preview');
const previewImg = document.getElementById('preview-image');
previewImg.src = e.target.result;
previewDiv.style.display = 'block';
// 更新粘贴区域样式
const pasteArea = document.getElementById('paste-area');
pasteArea.style.borderColor = '#28a745';
pasteArea.style.background = '#d4edda';
};
reader.readAsDataURL(file);
// 清空文件选择器
const fileInput = document.getElementById('image-input');
fileInput.value = '';
}
// 获取当前选择的文件(文件选择器或粘贴)
function getCurrentFile() {
if (pastedFile) {
return pastedFile;
}
const fileInput = document.getElementById('image-input');
return fileInput.files[0] || null;
}
// 清除粘贴的图片
function clearPastedImage() {
pastedFile = null;
const previewDiv = document.getElementById('paste-preview');
const pasteArea = document.getElementById('paste-area');
if (previewDiv) previewDiv.style.display = 'none';
if (pasteArea) {
pasteArea.style.borderColor = '#ddd';
pasteArea.style.background = '#f8f9fa';
// 确保粘贴区域失去焦点
if (document.activeElement === pasteArea) {
pasteArea.blur();
}
}
}
// 上传图片到水源图床
async function uploadToShuiyuan(file) {
return new Promise((resolve) => {
try {
// 创建FormData
const formData = new FormData();
formData.append('image', file);
// 获取保存的Cookie
const savedCookies = getSavedCookies();
// 构建请求头
const headers = {
"accept": "*/*",
"accept-language": "zh-CN,zh;q=0.9,en;q=0.8",
"cache-control": "no-cache",
"pragma": "no-cache",
"sec-ch-ua": '"Not)A;Brand";v="8", "Chromium";v="138", "Google Chrome";v="138"',
"sec-ch-ua-mobile": "?0",
"sec-ch-ua-platform": '"Windows"',
"sec-fetch-dest": "empty",
"sec-fetch-mode": "cors",
"sec-fetch-site": "cross-site"
};
// 如果有保存的Cookie,则添加到请求头
if (savedCookies) {
headers["Cookie"] = savedCookies;
}
// 使用GM_xmlhttpRequest来绕过CORS限制
GM_xmlhttpRequest({
method: "POST",
url: "https://notes.sjtu.edu.cn/uploadimage",
headers: headers,
data: formData,
onload: function(response) {
if (response.status >= 200 && response.status < 300) {
try {
// 尝试解析JSON响应
const responseText = response.responseText.trim();
console.log('服务器响应:', responseText); // 调试日志
const jsonResponse = JSON.parse(responseText);
let imageUrl;
// 检查不同可能的响应格式
if (jsonResponse.link) {
imageUrl = jsonResponse.link;
} else if (jsonResponse.url) {
imageUrl = jsonResponse.url;
} else if (jsonResponse.data && jsonResponse.data.link) {
imageUrl = jsonResponse.data.link;
} else if (jsonResponse.data && jsonResponse.data.url) {
imageUrl = jsonResponse.data.url;
} else {
// 如果找不到预期的字段,使用整个响应作为URL
imageUrl = responseText;
}
console.log('提取的图片链接:', imageUrl); // 调试日志
resolve({
success: true,
url: imageUrl,
message: '上传成功!'
});
} catch (e) {
console.log('JSON解析失败,使用原始响应:', e);
// 如果不是JSON格式,直接使用原始响应
resolve({
success: true,
url: response.responseText.trim(),
message: '上传成功!'
});
}
} else {
resolve({
success: false,
message: `上传失败: ${response.status} ${response.statusText}`
});
}
},
onerror: function(error) {
resolve({
success: false,
message: `上传失败: 网络错误`
});
},
ontimeout: function() {
resolve({
success: false,
message: `上传失败: 请求超时`
});
},
timeout: 30000 // 30秒超时
});
} catch (error) {
resolve({
success: false,
message: `上传失败: ${error.message}`
});
}
});
}
// 显示状态信息
function showStatus(message, isError = false) {
const statusDiv = document.getElementById('upload-status');
statusDiv.style.display = 'block';
statusDiv.style.background = isError ? '#f8d7da' : '#d4edda';
statusDiv.style.color = isError ? '#721c24' : '#155724';
statusDiv.style.border = `1px solid ${isError ? '#f5c6cb' : '#c3e6cb'}`;
statusDiv.textContent = message;
}
// 隐藏状态信息
function hideStatus() {
const statusDiv = document.getElementById('upload-status');
statusDiv.style.display = 'none';
}
// 复制到剪贴板
async function copyToClipboard(text, type = '链接') {
try {
await navigator.clipboard.writeText(text);
showStatus(`${type}已复制到剪贴板!`);
setTimeout(hideStatus, 2000);
} catch (err) {
// 降级方案
const textArea = document.createElement('textarea');
textArea.value = text;
document.body.appendChild(textArea);
textArea.select();
document.execCommand('copy');
document.body.removeChild(textArea);
showStatus(`${type}已复制到剪贴板!`);
setTimeout(hideStatus, 2000);
}
}
// 使面板可拖拽
function makeDraggable(element) {
let pos1 = 0, pos2 = 0, pos3 = 0, pos4 = 0;
const titleBar = element.querySelector('div');
titleBar.onmousedown = dragMouseDown;
function dragMouseDown(e) {
e = e || window.event;
e.preventDefault();
pos3 = e.clientX;
pos4 = e.clientY;
document.onmouseup = closeDragElement;
document.onmousemove = elementDrag;
}
function elementDrag(e) {
e = e || window.event;
e.preventDefault();
pos1 = pos3 - e.clientX;
pos2 = pos4 - e.clientY;
pos3 = e.clientX;
pos4 = e.clientY;
element.style.top = (element.offsetTop - pos2) + "px";
element.style.left = (element.offsetLeft - pos1) + "px";
}
function closeDragElement() {
document.onmouseup = null;
document.onmousemove = null;
}
}
// 初始化插件
function init() {
// 如果在notes.sjtu.edu.cn域名下,保存cookie
saveCookiesIfNeeded();
// 如果不在notes.sjtu.edu.cn域名下且没有保存的cookie,显示提示
if (window.location.hostname !== 'notes.sjtu.edu.cn' && !getSavedCookies()) {
console.warn('未找到水源笔记的Cookie,请先访问 https://notes.sjtu.edu.cn 登录后再使用上传功能');
}
const { uploadContainer, triggerBtn } = createUploadInterface();
// 使面板可拖拽
makeDraggable(uploadContainer);
// 绑定事件
triggerBtn.addEventListener('click', () => {
uploadContainer.style.display = 'block';
triggerBtn.style.display = 'none';
});
document.getElementById('close-upload-panel').addEventListener('click', () => {
uploadContainer.style.display = 'none';
triggerBtn.style.display = 'block';
// 关闭面板时将焦点回到页面主体
document.body.focus();
const mainContent = document.querySelector('main, #main, .main-content, body');
if (mainContent) {
mainContent.focus();
}
});
document.getElementById('upload-btn').addEventListener('click', async () => {
// 检查是否有登录Cookie
const savedCookies = getSavedCookies();
if (!savedCookies) {
showStatus('请先登录水源笔记!访问 https://notes.sjtu.edu.cn 登录后再使用上传功能。', true);
return;
}
const file = getCurrentFile();
if (!file) {
showStatus('请先选择或粘贴图片文件!', true);
return;
}
// 验证文件类型
if (!file.type.startsWith('image/')) {
showStatus('请选择有效的图片文件!', true);
return;
}
// 验证文件大小(限制为10MB)
if (file.size > 10 * 1024 * 1024) {
showStatus('图片文件不能超过10MB!', true);
return;
}
showStatus('正在上传,请稍候...');
const result = await uploadToShuiyuan(file);
if (result.success) {
showStatus(result.message);
const imageUrl = result.url;
// 填充不同格式的文本框
document.getElementById('result-url').value = imageUrl;
document.getElementById('result-html').value = `<img src="${imageUrl}" alt="uploaded image" />`;
document.getElementById('result-markdown').value = ``;
document.getElementById('upload-result').style.display = 'block';
// 清除粘贴的图片和文件选择器
clearPastedImage();
document.getElementById('image-input').value = '';
// 修复焦点问题:将焦点移回页面主体
document.body.focus();
// 如果页面有主要内容区域,也可以尝试聚焦到那里
const mainContent = document.querySelector('main, #main, .main-content, body');
if (mainContent) {
mainContent.focus();
}
} else {
showStatus(result.message, true);
document.getElementById('upload-result').style.display = 'none';
}
});
document.getElementById('copy-url-btn').addEventListener('click', () => {
const url = document.getElementById('result-url').value;
if (url) {
copyToClipboard(url, '链接');
}
});
document.getElementById('copy-html-btn').addEventListener('click', () => {
const html = document.getElementById('result-html').value;
if (html) {
copyToClipboard(html, 'HTML代码');
}
});
document.getElementById('copy-markdown-btn').addEventListener('click', () => {
const markdown = document.getElementById('result-markdown').value;
if (markdown) {
copyToClipboard(markdown, 'Markdown代码');
}
});
// 刷新Cookie按钮事件(仅在notes.sjtu.edu.cn页面显示)
const refreshCookiesBtn = document.getElementById('refresh-cookies-btn');
if (refreshCookiesBtn) {
refreshCookiesBtn.addEventListener('click', () => {
saveCookiesIfNeeded();
showStatus('已刷新登录状态');
setTimeout(() => {
location.reload(); // 重新加载页面以更新界面
}, 1000);
});
}
// 粘贴区域相关事件
const pasteArea = document.getElementById('paste-area');
const fileInput = document.getElementById('image-input');
// 粘贴区域点击聚焦(为了接收粘贴事件)
pasteArea.addEventListener('click', () => {
pasteArea.focus();
// 短暂聚焦后自动失焦,避免长时间持有焦点
setTimeout(() => {
if (document.activeElement === pasteArea) {
pasteArea.blur();
}
}, 100);
});
// 设置粘贴区域可聚焦
pasteArea.tabIndex = 0;
// 粘贴成功后自动失焦
const originalHandlePaste = handlePaste;
const enhancedHandlePaste = function(e) {
originalHandlePaste(e);
// 粘贴完成后失焦
setTimeout(() => {
if (document.activeElement === pasteArea) {
pasteArea.blur();
}
}, 50);
};
// 监听粘贴事件
pasteArea.addEventListener('paste', enhancedHandlePaste);
document.addEventListener('paste', (e) => {
// 如果上传面板是可见的,处理全局粘贴
if (uploadContainer.style.display !== 'none') {
enhancedHandlePaste(e);
}
});
// 文件选择器变化时清除粘贴的图片
fileInput.addEventListener('change', () => {
if (fileInput.files.length > 0) {
clearPastedImage();
}
});
// 拖拽支持
pasteArea.addEventListener('dragover', (e) => {
e.preventDefault();
pasteArea.style.borderColor = '#007bff';
pasteArea.style.background = '#e3f2fd';
});
pasteArea.addEventListener('dragleave', (e) => {
e.preventDefault();
if (!pastedFile) {
pasteArea.style.borderColor = '#ddd';
pasteArea.style.background = '#f8f9fa';
}
});
pasteArea.addEventListener('drop', (e) => {
e.preventDefault();
const files = e.dataTransfer.files;
if (files.length > 0 && files[0].type.startsWith('image/')) {
setPastedImage(files[0]);
} else {
pasteArea.style.borderColor = '#ddd';
pasteArea.style.background = '#f8f9fa';
}
});
// 添加键盘快捷键支持
document.addEventListener('keydown', (e) => {
if (e.ctrlKey && e.shiftKey && e.key === 'U') {
e.preventDefault();
if (uploadContainer.style.display === 'none') {
uploadContainer.style.display = 'block';
triggerBtn.style.display = 'none';
} else {
uploadContainer.style.display = 'none';
triggerBtn.style.display = 'block';
}
}
});
}
// 等待页面加载完成后初始化
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
} else {
init();
}
})();