Files
data-sync-service/clients/userscript-template.js
T
K-hermes edf4dfcd52 初始提交: 通用数据同步服务
- 基于OpenResty的RESTful API服务
- 支持Cookie/Token等数据类型的存储和管理
- 登录认证保护
- Web管理界面
- 数据过期管理
2026-08-14 09:53:32 +08:00

406 lines
12 KiB
JavaScript

// ==UserScript==
// @name 通用数据同步脚本
// @namespace http://tampermonkey.net/
// @version 2.0
// @description 通用数据同步服务客户端,支持自动检测项目和账号
// @author Hermes
// @match *://*/*
// @grant GM_xmlhttpRequest
// @grant GM_setClipboard
// @grant GM_notification
// @connect 47.122.126.244
// ==/UserScript==
(function() {
'use strict';
// ==================== 配置区 ====================
const CONFIG = {
serverUrl: 'http://47.122.126.244:5001',
password: 'admin123123123',
// 自动检测配置
autoDetect: true, // 是否自动检测项目名和账号
tag: 'auto', // 'auto' 表示自动检测,或手动指定如 'tingwu:ykaayk@qq.com'
type: 'cookie', // cookie/token/credential/session/custom
// 过期配置
expiresIn: 604800, // 7天(秒)
// 其他配置
autoSync: false, // 是否自动同步(页面加载后自动上传)
syncInterval: 0, // 定时同步间隔(秒,0 表示不定时同步)
};
// ==================== 项目检测规则 ====================
const PROJECT_RULES = [
{
name: 'tingwu',
match: (url) => url.includes('tingwu.aliyun.com'),
getAccount: () => {
// 从页面提取账号信息
const emailElement = document.querySelector('[data-account]');
if (emailElement) return emailElement.getAttribute('data-account');
// 从 localStorage 提取
try {
const userData = JSON.parse(localStorage.getItem('user_data') || '{}');
return userData.email || userData.username || 'unknown';
} catch {}
return 'unknown';
},
loginUrl: 'https://tingwu.aliyun.com/'
},
{
name: 'wechat',
match: (url) => url.includes('mp.weixin.qq.com'),
getAccount: () => {
const accountElement = document.querySelector('.account_info_text');
return accountElement ? accountElement.textContent.trim() : 'admin';
},
loginUrl: 'https://mp.weixin.qq.com/'
},
// 添加更多项目...
];
// ==================== 核心功能 ====================
/**
* 自动检测项目和账号
*/
function autoDetectTag() {
const url = window.location.href;
for (const rule of PROJECT_RULES) {
if (rule.match(url)) {
const account = rule.getAccount();
return {
tag: `${rule.name}:${account}`,
loginUrl: rule.loginUrl,
project: rule.name,
account: account
};
}
}
return {
tag: 'unknown:unknown',
loginUrl: window.location.origin,
project: 'unknown',
account: 'unknown'
};
}
/**
* 获取当前页面的 Cookie 数据
*/
function getCookieData() {
const cookies = document.cookie.split(';').map(c => {
const [name, ...valueParts] = c.trim().split('=');
return {
name: name,
value: valueParts.join('='),
domain: window.location.hostname,
path: '/'
};
}).filter(c => c.name);
return {
cookies: cookies,
raw_cookie: document.cookie,
user_agent: navigator.userAgent,
url: window.location.href
};
}
/**
* 上传数据到服务器
*/
function uploadData(tag, type, data, metadata) {
const payload = {
password: CONFIG.password,
tag: tag,
type: type,
data: data,
metadata: metadata
};
GM_xmlhttpRequest({
method: 'POST',
url: `${CONFIG.serverUrl}/api/data`,
headers: {
'Content-Type': 'application/json'
},
data: JSON.stringify(payload),
onload: function(response) {
if (response.status === 200) {
const result = JSON.parse(response.responseText);
console.log('✅ 数据同步成功:', result);
GM_notification({
title: '数据同步成功',
text: `Tag: ${tag}\n时间: ${result.timestamp}`,
timeout: 3000
});
} else {
console.error('❌ 数据同步失败:', response.responseText);
GM_notification({
title: '数据同步失败',
text: `状态码: ${response.status}`,
timeout: 5000
});
}
},
onerror: function(error) {
console.error('❌ 网络错误:', error);
GM_notification({
title: '网络错误',
text: '无法连接到同步服务器',
timeout: 5000
});
}
});
}
/**
* 同步当前页面数据
*/
function syncData() {
let tag = CONFIG.tag;
let loginUrl = window.location.origin;
// 自动检测
if (CONFIG.autoDetect || tag === 'auto') {
const detected = autoDetectTag();
tag = detected.tag;
loginUrl = detected.loginUrl;
console.log('🔍 自动检测结果:', detected);
}
// 获取数据
let data;
if (CONFIG.type === 'cookie') {
data = getCookieData();
} else {
console.error('❌ 不支持的数据类型:', CONFIG.type);
return;
}
// 构建元数据
const metadata = {
expires_in: CONFIG.expiresIn,
login_url: loginUrl,
note: `自动同步 - ${new Date().toLocaleString()}`
};
// 上传
uploadData(tag, CONFIG.type, data, metadata);
}
/**
* 复制配置到剪贴板
*/
function copyConfig() {
let tag = CONFIG.tag;
if (CONFIG.autoDetect || tag === 'auto') {
const detected = autoDetectTag();
tag = detected.tag;
}
const config = {
serverUrl: CONFIG.serverUrl,
password: CONFIG.password,
tag: tag,
type: CONFIG.type
};
const configText = JSON.stringify(config, null, 2);
GM_setClipboard(configText);
console.log('📋 配置已复制到剪贴板:', config);
GM_notification({
title: '配置已复制',
text: `Tag: ${tag}`,
timeout: 2000
});
}
// ==================== UI 界面 ====================
/**
* 创建浮动按钮
*/
function createFloatingButton() {
const button = document.createElement('div');
button.id = 'data-sync-button';
button.innerHTML = '🔄';
button.style.cssText = `
position: fixed;
bottom: 20px;
right: 20px;
width: 50px;
height: 50px;
background: #4CAF50;
color: white;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
font-size: 24px;
box-shadow: 0 4px 8px rgba(0,0,0,0.3);
z-index: 10000;
transition: all 0.3s;
`;
button.addEventListener('mouseenter', () => {
button.style.transform = 'scale(1.1)';
});
button.addEventListener('mouseleave', () => {
button.style.transform = 'scale(1)';
});
button.addEventListener('click', () => {
showSyncPanel();
});
document.body.appendChild(button);
}
/**
* 显示同步面板
*/
function showSyncPanel() {
const detected = autoDetectTag();
const panel = document.createElement('div');
panel.id = 'data-sync-panel';
panel.innerHTML = `
<div style="background: white; padding: 20px; border-radius: 8px; box-shadow: 0 4px 16px rgba(0,0,0,0.2); max-width: 400px;">
<h3 style="margin: 0 0 15px 0;">数据同步</h3>
<div style="margin-bottom: 10px;">
<strong>项目:</strong> ${detected.project}
</div>
<div style="margin-bottom: 10px;">
<strong>账号:</strong> ${detected.account}
</div>
<div style="margin-bottom: 10px;">
<strong>Tag:</strong> ${detected.tag}
</div>
<div style="margin-bottom: 15px;">
<strong>类型:</strong> ${CONFIG.type}
</div>
<div style="display: flex; gap: 10px;">
<button id="sync-now-btn" style="flex: 1; padding: 10px; background: #4CAF50; color: white; border: none; border-radius: 4px; cursor: pointer;">
立即同步
</button>
<button id="copy-config-btn" style="flex: 1; padding: 10px; background: #2196F3; color: white; border: none; border-radius: 4px; cursor: pointer;">
复制配置
</button>
<button id="close-panel-btn" style="padding: 10px; background: #f44336; color: white; border: none; border-radius: 4px; cursor: pointer;">
关闭
</button>
</div>
</div>
`;
panel.style.cssText = `
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
z-index: 10001;
`;
// 添加背景遮罩
const overlay = document.createElement('div');
overlay.id = 'data-sync-overlay';
overlay.style.cssText = `
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.5);
z-index: 10000;
`;
document.body.appendChild(overlay);
document.body.appendChild(panel);
// 绑定事件
document.getElementById('sync-now-btn').addEventListener('click', () => {
syncData();
closePanel();
});
document.getElementById('copy-config-btn').addEventListener('click', () => {
copyConfig();
});
document.getElementById('close-panel-btn').addEventListener('click', closePanel);
overlay.addEventListener('click', closePanel);
}
/**
* 关闭面板
*/
function closePanel() {
const panel = document.getElementById('data-sync-panel');
const overlay = document.getElementById('data-sync-overlay');
if (panel) panel.remove();
if (overlay) overlay.remove();
}
// ==================== 初始化 ====================
function init() {
console.log('🚀 通用数据同步脚本已加载');
// 创建浮动按钮
createFloatingButton();
// 自动同步
if (CONFIG.autoSync) {
setTimeout(() => {
console.log('⏰ 执行自动同步...');
syncData();
}, 3000);
}
// 定时同步
if (CONFIG.syncInterval > 0) {
setInterval(() => {
console.log('⏰ 执行定时同步...');
syncData();
}, CONFIG.syncInterval * 1000);
}
// 快捷键 Ctrl+Shift+S
document.addEventListener('keydown', (e) => {
if (e.ctrlKey && e.shiftKey && e.key === 'S') {
e.preventDefault();
syncData();
}
});
// 快捷键 Ctrl+Shift+C
document.addEventListener('keydown', (e) => {
if (e.ctrlKey && e.shiftKey && e.key === 'C') {
e.preventDefault();
copyConfig();
}
});
}
// 等待 DOM 加载完成
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
} else {
init();
}
})();