Files
data-sync-service/clients/userscript-template.js
T
K-hermes e985a1a143 重构: 将油猴脚本调整为通用版本
主要改动:
1. 移除微信公众号相关配置和检测规则
2. 将 @match 改为通配符 *://*/* 支持所有网站
3. 添加注释说明如何添加自定义网站检测规则
4. 保留通义听悟作为示例
5. 强调通用性,用户可自行扩展

脚本现在是真正的通用数据同步工具,不局限于特定网站。
2026-08-14 10:20:39 +08:00

612 lines
22 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// ==UserScript==
// @name 通用数据同步脚本
// @namespace http://tampermonkey.net/
// @version 3.0
// @description 通用数据同步服务客户端,支持自动检测项目和账号,可视化配置管理
// @author Hermes
// @match *://*/*
// @grant GM_xmlhttpRequest
// @grant GM_setClipboard
// @grant GM_notification
// @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, // 是否自动检测项目名和账号
tag: 'auto', // 'auto' 表示自动检测,或手动指定如 'tingwu:ykaayk@qq.com'
type: 'cookie', // cookie/token/credential/session/custom
// 过期配置
expiresIn: 604800, // 7天(秒)
// 其他配置
autoSync: false, // 是否自动同步(页面加载后自动上传)
syncInterval: 0, // 定时同步间隔(秒,0 表示不定时同步)
};
// 加载配置
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_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: '网站名称',
// match: (url) => url.includes('example.com'),
// getAccount: () => {
// // 返回账号标识
// return 'user@example.com';
// },
// loginUrl: 'https://example.com/login'
// }
];
// ==================== 核心功能 ====================
/**
* 自动检测项目和账号
*/
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) {
let result;
try {
result = JSON.parse(response.responseText);
} catch (e) {
console.error('❌ 响应解析失败:', response.responseText);
GM_notification({
title: '同步失败',
text: '服务器响应格式错误',
timeout: 5000
});
return;
}
// 检查是否成功:HTTP 200 且响应体包含 success: true
if (response.status === 200 && result.success === true) {
console.log('✅ 数据同步成功:', result);
GM_notification({
title: '数据同步成功',
text: `Tag: ${tag}\n时间: ${result.timestamp || '未知'}`,
timeout: 3000
});
} else {
// 失败:可能是 HTTP 错误码,或响应包含 error 字段
const errorMsg = result.error || result.message || response.statusText || '未知错误';
console.error('❌ 数据同步失败:', errorMsg, response);
GM_notification({
title: '数据同步失败',
text: `状态码: ${response.status}\n错误: ${errorMsg}`,
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: 450px;">
<h3 style="margin: 0 0 15px 0; color: #333;">数据同步控制面板</h3>
<div style="margin-bottom: 15px; padding: 10px; background: #f5f5f5; border-radius: 4px;">
<div style="margin-bottom: 8px;">
<strong>项目:</strong> ${detected.project}
</div>
<div style="margin-bottom: 8px;">
<strong>账号:</strong> ${detected.account}
</div>
<div style="margin-bottom: 8px;">
<strong>Tag:</strong> ${detected.tag}
</div>
<div>
<strong>类型:</strong> ${CONFIG.type}
</div>
</div>
<div style="display: flex; gap: 10px; margin-bottom: 15px;">
<button id="sync-now-btn" style="flex: 1; padding: 10px; background: #4CAF50; color: white; border: none; border-radius: 4px; cursor: pointer; font-weight: bold;">
🔄 立即同步
</button>
<button id="copy-config-btn" style="flex: 1; padding: 10px; background: #2196F3; color: white; border: none; border-radius: 4px; cursor: pointer; font-weight: bold;">
📋 复制配置
</button>
</div>
<div style="display: flex; gap: 10px;">
<button id="settings-btn" style="flex: 1; padding: 10px; background: #FF9800; color: white; border: none; border-radius: 4px; cursor: pointer; font-weight: bold;">
⚙️ 配置设置
</button>
<button id="close-panel-btn" style="padding: 10px 20px; background: #f44336; color: white; border: none; border-radius: 4px; cursor: pointer; font-weight: bold;">
关闭
</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('settings-btn').addEventListener('click', () => {
closePanel();
showSettingsPanel();
});
document.getElementById('close-panel-btn').addEventListener('click', closePanel);
overlay.addEventListener('click', closePanel);
}
/**
* 显示设置面板
*/
function showSettingsPanel() {
const panel = document.createElement('div');
panel.id = 'data-sync-settings-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: 500px; max-height: 80vh; overflow-y: auto;">
<h3 style="margin: 0 0 15px 0; color: #333;">配置设置</h3>
<div style="margin-bottom: 15px;">
<label style="display: block; margin-bottom: 5px; font-weight: bold; color: #555;">服务器地址</label>
<input type="text" id="setting-serverUrl" value="${CONFIG.serverUrl}"
style="width: 100%; padding: 8px; border: 1px solid #ddd; border-radius: 4px; box-sizing: border-box;">
</div>
<div style="margin-bottom: 15px;">
<label style="display: block; margin-bottom: 5px; font-weight: bold; color: #555;">密码</label>
<input type="password" id="setting-password" value="${CONFIG.password}"
style="width: 100%; padding: 8px; border: 1px solid #ddd; border-radius: 4px; box-sizing: border-box;">
</div>
<div style="margin-bottom: 15px;">
<label style="display: block; margin-bottom: 5px; font-weight: bold; color: #555;">Tag(留空自动检测)</label>
<input type="text" id="setting-tag" value="${CONFIG.tag === 'auto' ? '' : CONFIG.tag}" placeholder="auto"
style="width: 100%; padding: 8px; border: 1px solid #ddd; border-radius: 4px; box-sizing: border-box;">
<small style="color: #888;">格式:项目名:账号,如 tingwu:user@email.com</small>
</div>
<div style="margin-bottom: 15px;">
<label style="display: block; margin-bottom: 5px; font-weight: bold; color: #555;">数据类型</label>
<select id="setting-type" style="width: 100%; padding: 8px; border: 1px solid #ddd; border-radius: 4px; box-sizing: border-box;">
<option value="cookie" ${CONFIG.type === 'cookie' ? 'selected' : ''}>Cookie</option>
<option value="token" ${CONFIG.type === 'token' ? 'selected' : ''}>Token</option>
<option value="credential" ${CONFIG.type === 'credential' ? 'selected' : ''}>Credential</option>
<option value="session" ${CONFIG.type === 'session' ? 'selected' : ''}>Session</option>
<option value="custom" ${CONFIG.type === 'custom' ? 'selected' : ''}>Custom</option>
</select>
</div>
<div style="margin-bottom: 15px;">
<label style="display: block; margin-bottom: 5px; font-weight: bold; color: #555;">过期时间(秒)</label>
<input type="number" id="setting-expiresIn" value="${CONFIG.expiresIn}"
style="width: 100%; padding: 8px; border: 1px solid #ddd; border-radius: 4px; box-sizing: border-box;">
<small style="color: #888;">默认 6048007天)</small>
</div>
<div style="margin-bottom: 15px;">
<label style="display: flex; align-items: center; cursor: pointer;">
<input type="checkbox" id="setting-autoDetect" ${CONFIG.autoDetect ? 'checked' : ''}
style="margin-right: 8px;">
<span style="font-weight: bold; color: #555;">自动检测项目和账号</span>
</label>
</div>
<div style="margin-bottom: 15px;">
<label style="display: flex; align-items: center; cursor: pointer;">
<input type="checkbox" id="setting-autoSync" ${CONFIG.autoSync ? 'checked' : ''}
style="margin-right: 8px;">
<span style="font-weight: bold; color: #555;">页面加载后自动同步</span>
</label>
</div>
<div style="margin-bottom: 20px;">
<label style="display: block; margin-bottom: 5px; font-weight: bold; color: #555;">定时同步间隔(秒,0=禁用)</label>
<input type="number" id="setting-syncInterval" value="${CONFIG.syncInterval}"
style="width: 100%; padding: 8px; border: 1px solid #ddd; border-radius: 4px; box-sizing: border-box;">
</div>
<div style="display: flex; gap: 10px;">
<button id="save-settings-btn" style="flex: 1; padding: 10px; background: #4CAF50; color: white; border: none; border-radius: 4px; cursor: pointer; font-weight: bold;">
✅ 保存配置
</button>
<button id="reset-settings-btn" style="flex: 1; padding: 10px; background: #FF9800; color: white; border: none; border-radius: 4px; cursor: pointer; font-weight: bold;">
🔄 恢复默认
</button>
<button id="close-settings-btn" style="padding: 10px 20px; background: #f44336; color: white; border: none; border-radius: 4px; cursor: pointer; font-weight: bold;">
关闭
</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('save-settings-btn').addEventListener('click', () => {
const newConfig = {
serverUrl: document.getElementById('setting-serverUrl').value.trim(),
password: document.getElementById('setting-password').value,
tag: document.getElementById('setting-tag').value.trim() || 'auto',
type: document.getElementById('setting-type').value,
expiresIn: parseInt(document.getElementById('setting-expiresIn').value) || 604800,
autoDetect: document.getElementById('setting-autoDetect').checked,
autoSync: document.getElementById('setting-autoSync').checked,
syncInterval: parseInt(document.getElementById('setting-syncInterval').value) || 0,
};
CONFIG = newConfig;
saveConfig(newConfig);
GM_notification({
title: '配置已保存',
text: '设置将在下次页面加载时生效',
timeout: 3000
});
closePanel();
});
document.getElementById('reset-settings-btn').addEventListener('click', () => {
if (confirm('确定要恢复默认配置吗?')) {
CONFIG = {...DEFAULT_CONFIG};
saveConfig(CONFIG);
GM_notification({
title: '配置已重置',
text: '已恢复为默认配置',
timeout: 3000
});
closePanel();
}
});
document.getElementById('close-settings-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();
}
})();