主要改进: 1. 修复核心Bug - 正确检测API响应状态(检查success字段) 2. 配置界面化 - 使用GM_setValue/GM_getValue持久化存储 3. 添加可视化配置面板 - 无需修改代码 4. 增强错误处理 - 显示详细的错误信息 5. API兼容性验证 - 与后端完全兼容 提供两个版本: - clients/userscript-template.js (v3.0增强版, 608行) - client/userscript-template.js (v2.0简化版, 431行) 新增文档: - USERSCRIPT_CHANGELOG.md - 更新日志 - USERSCRIPT_FIX_SUMMARY.md - 修复总结 - USERSCRIPT_USAGE.md - 使用指南 由Claude Code完成。
432 lines
16 KiB
JavaScript
432 lines
16 KiB
JavaScript
// ==UserScript==
|
|
// @name 通用数据同步助手
|
|
// @namespace http://tampermonkey.net/
|
|
// @version 2.0.0
|
|
// @description 一键同步 Cookie/Token 到服务器(通用版),支持可视化配置管理
|
|
// @author K-hermes
|
|
// @match https://tingwu.aliyun.com/*
|
|
// @match https://mp.weixin.qq.com/*
|
|
// @grant GM_xmlhttpRequest
|
|
// @grant GM_setClipboard
|
|
// @grant GM_setValue
|
|
// @grant GM_getValue
|
|
// @connect 47.122.126.244
|
|
// @connect *
|
|
// ==/UserScript==
|
|
|
|
(function() {
|
|
'use strict';
|
|
|
|
// ==================== 配置管理 ====================
|
|
const DEFAULT_CONFIG = {
|
|
serverUrl: 'http://47.122.126.244:5001',
|
|
password: 'admin123123123',
|
|
autoDetect: true
|
|
};
|
|
|
|
// 加载配置
|
|
function loadConfig() {
|
|
const saved = GM_getValue('sync_config', null);
|
|
if (saved) {
|
|
try {
|
|
return {...DEFAULT_CONFIG, ...JSON.parse(saved)};
|
|
} catch (e) {
|
|
console.error('[同步助手] 配置解析失败,使用默认配置', e);
|
|
}
|
|
}
|
|
return {...DEFAULT_CONFIG};
|
|
}
|
|
|
|
// 保存配置
|
|
function saveConfig(config) {
|
|
GM_setValue('sync_config', JSON.stringify(config));
|
|
console.log('[同步助手] 配置已保存');
|
|
}
|
|
|
|
// 全局配置对象
|
|
let CONFIG = loadConfig();
|
|
|
|
// ==================== 项目检测 ====================
|
|
const PROJECT_PATTERNS = {
|
|
'tingwu.aliyun.com': {
|
|
project: 'tingwu',
|
|
accountSelector: '.user-email, [class*="email"], [class*="account"]',
|
|
loginUrl: 'https://tingwu.aliyun.com/'
|
|
},
|
|
'mp.weixin.qq.com': {
|
|
project: 'wechat',
|
|
accountSelector: '.account__nickname, .weui-desktop-account__name',
|
|
loginUrl: 'https://mp.weixin.qq.com/'
|
|
}
|
|
};
|
|
|
|
function detectProject() {
|
|
const host = window.location.hostname;
|
|
for (const [pattern, config] of Object.entries(PROJECT_PATTERNS)) {
|
|
if (host.includes(pattern)) {
|
|
return config;
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
function detectAccount(selector) {
|
|
if (!selector) return 'auto';
|
|
|
|
const element = document.querySelector(selector);
|
|
if (element) {
|
|
return element.innerText.trim() || 'auto';
|
|
}
|
|
|
|
// 尝试从 localStorage/sessionStorage 读取
|
|
const storageKeys = ['userEmail', 'account', 'username', 'user_id'];
|
|
for (const key of storageKeys) {
|
|
const value = localStorage.getItem(key) || sessionStorage.getItem(key);
|
|
if (value) {
|
|
return value;
|
|
}
|
|
}
|
|
|
|
return 'auto';
|
|
}
|
|
|
|
// ==================== Cookie 提取 ====================
|
|
function getAllCookies() {
|
|
const cookies = document.cookie.split(';').map(cookie => {
|
|
const [name, value] = cookie.trim().split('=');
|
|
return {
|
|
name: name,
|
|
value: value || '',
|
|
domain: window.location.hostname,
|
|
path: '/',
|
|
secure: window.location.protocol === 'https:',
|
|
httpOnly: false
|
|
};
|
|
});
|
|
|
|
return cookies.filter(c => c.name);
|
|
}
|
|
|
|
// ==================== 同步功能 ====================
|
|
function syncData() {
|
|
const projectConfig = detectProject();
|
|
if (!projectConfig) {
|
|
showMessage('⚠️ 当前网站不支持自动同步', 'warning');
|
|
return;
|
|
}
|
|
|
|
const account = detectAccount(projectConfig.accountSelector);
|
|
const tag = `${projectConfig.project}:${account}`;
|
|
const cookies = getAllCookies();
|
|
|
|
if (cookies.length === 0) {
|
|
showMessage('⚠️ 未找到 Cookie', 'warning');
|
|
return;
|
|
}
|
|
|
|
const data = {
|
|
password: CONFIG.password,
|
|
tag: tag,
|
|
type: 'cookie',
|
|
data: {
|
|
cookies: cookies,
|
|
raw_cookie: document.cookie,
|
|
user_agent: navigator.userAgent
|
|
},
|
|
metadata: {
|
|
expires_in: 7 * 24 * 3600, // 7天
|
|
note: `${projectConfig.project} 账号同步`,
|
|
login_url: projectConfig.loginUrl
|
|
}
|
|
};
|
|
|
|
showMessage('🔄 正在同步...', 'info');
|
|
|
|
GM_xmlhttpRequest({
|
|
method: 'POST',
|
|
url: `${CONFIG.serverUrl}/api/data`,
|
|
headers: {
|
|
'Content-Type': 'application/json'
|
|
},
|
|
data: JSON.stringify(data),
|
|
onload: function(response) {
|
|
let result;
|
|
try {
|
|
result = JSON.parse(response.responseText);
|
|
} catch (e) {
|
|
showMessage('❌ 服务器响应格式错误', 'error');
|
|
return;
|
|
}
|
|
|
|
// 检查是否成功:HTTP 200 且响应体包含 success: true
|
|
if (response.status === 200 && result.success === true) {
|
|
showMessage(`✅ 同步成功!\n\nTag: ${tag}\n时间: ${result.timestamp || '未知'}`, 'success');
|
|
} else {
|
|
// 失败:可能是 HTTP 错误码,或响应包含 error 字段
|
|
const errorMsg = result.error || result.message || response.statusText || '未知错误';
|
|
showMessage(`❌ 同步失败\n\n状态码: ${response.status}\n错误: ${errorMsg}`, 'error');
|
|
}
|
|
},
|
|
onerror: function(error) {
|
|
showMessage(`❌ 网络错误: ${error.error || '无法连接到服务器'}`, 'error');
|
|
}
|
|
});
|
|
}
|
|
|
|
// ==================== 复制配置 ====================
|
|
function copyConfig() {
|
|
const projectConfig = detectProject();
|
|
if (!projectConfig) {
|
|
showMessage('⚠️ 当前网站不支持', 'warning');
|
|
return;
|
|
}
|
|
|
|
const account = detectAccount(projectConfig.accountSelector);
|
|
const tag = `${projectConfig.project}:${account}`;
|
|
|
|
const config = {
|
|
serverUrl: CONFIG.serverUrl,
|
|
password: CONFIG.password,
|
|
tag: tag,
|
|
type: 'cookie'
|
|
};
|
|
|
|
const configText = JSON.stringify(config, null, 2);
|
|
GM_setClipboard(configText);
|
|
showMessage('✅ 配置已复制到剪贴板', 'success');
|
|
}
|
|
|
|
// ==================== UI ====================
|
|
function createButton() {
|
|
const button = document.createElement('div');
|
|
button.innerHTML = `
|
|
<style>
|
|
#sync-helper-btn {
|
|
position: fixed;
|
|
top: 100px;
|
|
right: 20px;
|
|
z-index: 99999;
|
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
color: white;
|
|
padding: 12px 20px;
|
|
border-radius: 8px;
|
|
cursor: pointer;
|
|
box-shadow: 0 4px 15px rgba(0,0,0,0.2);
|
|
font-size: 14px;
|
|
font-weight: bold;
|
|
transition: all 0.3s;
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
}
|
|
#sync-helper-btn:hover {
|
|
transform: translateY(-2px);
|
|
box-shadow: 0 6px 20px rgba(0,0,0,0.3);
|
|
}
|
|
#sync-helper-menu {
|
|
display: none;
|
|
position: fixed;
|
|
top: 150px;
|
|
right: 20px;
|
|
z-index: 99998;
|
|
background: white;
|
|
border-radius: 8px;
|
|
box-shadow: 0 4px 15px rgba(0,0,0,0.2);
|
|
overflow: hidden;
|
|
}
|
|
.sync-menu-item {
|
|
padding: 12px 20px;
|
|
cursor: pointer;
|
|
transition: background 0.2s;
|
|
border-bottom: 1px solid #eee;
|
|
}
|
|
.sync-menu-item:last-child {
|
|
border-bottom: none;
|
|
}
|
|
.sync-menu-item:hover {
|
|
background: #f5f5f5;
|
|
}
|
|
</style>
|
|
<div id="sync-helper-btn">
|
|
🔄 同步数据
|
|
</div>
|
|
<div id="sync-helper-menu">
|
|
<div class="sync-menu-item" id="sync-now">🔄 立即同步</div>
|
|
<div class="sync-menu-item" id="copy-config">📋 复制配置</div>
|
|
<div class="sync-menu-item" id="open-settings">⚙️ 配置设置</div>
|
|
</div>
|
|
`;
|
|
document.body.appendChild(button);
|
|
|
|
// 事件绑定
|
|
const btn = document.getElementById('sync-helper-btn');
|
|
const menu = document.getElementById('sync-helper-menu');
|
|
|
|
btn.addEventListener('click', () => {
|
|
menu.style.display = menu.style.display === 'none' ? 'block' : 'none';
|
|
});
|
|
|
|
document.getElementById('sync-now').addEventListener('click', () => {
|
|
menu.style.display = 'none';
|
|
syncData();
|
|
});
|
|
|
|
document.getElementById('copy-config').addEventListener('click', () => {
|
|
menu.style.display = 'none';
|
|
copyConfig();
|
|
});
|
|
|
|
document.getElementById('open-settings').addEventListener('click', () => {
|
|
menu.style.display = 'none';
|
|
showSettingsPanel();
|
|
});
|
|
|
|
// 点击外部关闭菜单
|
|
document.addEventListener('click', (e) => {
|
|
if (!btn.contains(e.target) && !menu.contains(e.target)) {
|
|
menu.style.display = 'none';
|
|
}
|
|
});
|
|
}
|
|
|
|
function showSettingsPanel() {
|
|
const panel = document.createElement('div');
|
|
panel.id = 'sync-settings-panel';
|
|
panel.innerHTML = `
|
|
<div style="position: fixed; top: 0; left: 0; right: 0; bottom: 0; background: rgba(0,0,0,0.5); z-index: 999998; display: flex; align-items: center; justify-content: center;">
|
|
<div style="background: white; padding: 25px; border-radius: 12px; box-shadow: 0 8px 32px rgba(0,0,0,0.3); max-width: 450px; width: 90%;">
|
|
<h3 style="margin: 0 0 20px 0; color: #333; font-size: 18px;">配置设置</h3>
|
|
|
|
<div style="margin-bottom: 15px;">
|
|
<label style="display: block; margin-bottom: 5px; font-weight: bold; color: #555; font-size: 13px;">服务器地址</label>
|
|
<input type="text" id="settings-serverUrl" value="${CONFIG.serverUrl}"
|
|
style="width: 100%; padding: 10px; border: 1px solid #ddd; border-radius: 6px; box-sizing: border-box; font-size: 14px;">
|
|
</div>
|
|
|
|
<div style="margin-bottom: 15px;">
|
|
<label style="display: block; margin-bottom: 5px; font-weight: bold; color: #555; font-size: 13px;">密码</label>
|
|
<input type="password" id="settings-password" value="${CONFIG.password}"
|
|
style="width: 100%; padding: 10px; border: 1px solid #ddd; border-radius: 6px; box-sizing: border-box; font-size: 14px;">
|
|
</div>
|
|
|
|
<div style="margin-bottom: 20px;">
|
|
<label style="display: flex; align-items: center; cursor: pointer;">
|
|
<input type="checkbox" id="settings-autoDetect" ${CONFIG.autoDetect ? 'checked' : ''}
|
|
style="margin-right: 8px; width: 18px; height: 18px;">
|
|
<span style="font-weight: bold; color: #555; font-size: 13px;">自动检测项目和账号</span>
|
|
</label>
|
|
</div>
|
|
|
|
<div style="display: flex; gap: 10px;">
|
|
<button id="settings-save" style="flex: 1; padding: 12px; background: #4CAF50; color: white; border: none; border-radius: 6px; cursor: pointer; font-weight: bold; font-size: 14px;">
|
|
✅ 保存
|
|
</button>
|
|
<button id="settings-reset" style="flex: 1; padding: 12px; background: #FF9800; color: white; border: none; border-radius: 6px; cursor: pointer; font-weight: bold; font-size: 14px;">
|
|
🔄 重置
|
|
</button>
|
|
<button id="settings-close" style="padding: 12px 20px; background: #f44336; color: white; border: none; border-radius: 6px; cursor: pointer; font-weight: bold; font-size: 14px;">
|
|
关闭
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
`;
|
|
document.body.appendChild(panel);
|
|
|
|
// 绑定事件
|
|
document.getElementById('settings-save').addEventListener('click', () => {
|
|
const newConfig = {
|
|
serverUrl: document.getElementById('settings-serverUrl').value.trim(),
|
|
password: document.getElementById('settings-password').value,
|
|
autoDetect: document.getElementById('settings-autoDetect').checked
|
|
};
|
|
|
|
CONFIG = newConfig;
|
|
saveConfig(newConfig);
|
|
showMessage('✅ 配置已保存', 'success');
|
|
panel.remove();
|
|
});
|
|
|
|
document.getElementById('settings-reset').addEventListener('click', () => {
|
|
if (confirm('确定要恢复默认配置吗?')) {
|
|
CONFIG = {...DEFAULT_CONFIG};
|
|
saveConfig(CONFIG);
|
|
showMessage('✅ 配置已重置', 'success');
|
|
panel.remove();
|
|
}
|
|
});
|
|
|
|
document.getElementById('settings-close').addEventListener('click', () => {
|
|
panel.remove();
|
|
});
|
|
|
|
// 点击背景关闭
|
|
panel.addEventListener('click', (e) => {
|
|
if (e.target === panel) {
|
|
panel.remove();
|
|
}
|
|
});
|
|
}
|
|
|
|
function showMessage(text, type) {
|
|
const colors = {
|
|
success: '#4CAF50',
|
|
error: '#f44336',
|
|
warning: '#ff9800',
|
|
info: '#2196F3'
|
|
};
|
|
|
|
const msg = document.createElement('div');
|
|
msg.style.cssText = `
|
|
position: fixed;
|
|
top: 20px;
|
|
right: 20px;
|
|
z-index: 999999;
|
|
background: ${colors[type] || colors.info};
|
|
color: white;
|
|
padding: 15px 20px;
|
|
border-radius: 8px;
|
|
box-shadow: 0 4px 15px rgba(0,0,0,0.3);
|
|
font-size: 14px;
|
|
white-space: pre-line;
|
|
max-width: 400px;
|
|
animation: slideIn 0.3s ease-out;
|
|
`;
|
|
msg.textContent = text;
|
|
|
|
const style = document.createElement('style');
|
|
style.textContent = `
|
|
@keyframes slideIn {
|
|
from { transform: translateX(400px); opacity: 0; }
|
|
to { transform: translateX(0); opacity: 1; }
|
|
}
|
|
`;
|
|
document.head.appendChild(style);
|
|
|
|
document.body.appendChild(msg);
|
|
|
|
setTimeout(() => {
|
|
msg.style.animation = 'slideOut 0.3s ease-in';
|
|
setTimeout(() => msg.remove(), 300);
|
|
}, 3000);
|
|
|
|
const styleOut = document.createElement('style');
|
|
styleOut.textContent = `
|
|
@keyframes slideOut {
|
|
from { transform: translateX(0); opacity: 1; }
|
|
to { transform: translateX(400px); opacity: 0; }
|
|
}
|
|
`;
|
|
document.head.appendChild(styleOut);
|
|
}
|
|
|
|
// ==================== 初始化 ====================
|
|
window.addEventListener('load', () => {
|
|
setTimeout(() => {
|
|
createButton();
|
|
console.log('[同步助手] 已加载');
|
|
}, 1000);
|
|
});
|
|
|
|
})();
|