Files
data-sync-service/clients/userscript-template.js
T
K-hermes a3c23970d7 功能: 优化油猴脚本 v3.0
主要改进:
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完成。
2026-08-14 10:15:31 +08:00

609 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: '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) {
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();
}
})();