feat: implement custom node.js sync server and secure extension client with login/register UI
This commit is contained in:
@@ -139,6 +139,78 @@ function autoFillLogin(username, password) {
|
||||
passwordEl.focus();
|
||||
setNativeValue(passwordEl, password);
|
||||
triggerEvents(passwordEl);
|
||||
|
||||
// --- New: Auto-click Login Button ---
|
||||
setTimeout(() => {
|
||||
// 常规登录按钮选择器
|
||||
const loginButtonSelectors = [
|
||||
'button[type="submit"]',
|
||||
'input[type="submit"]',
|
||||
'button.login-btn',
|
||||
'button.submit-btn',
|
||||
'#login-btn',
|
||||
'.login-button',
|
||||
'button.ant-btn-primary' // 适配 Ant Design
|
||||
];
|
||||
|
||||
let loginBtn = null;
|
||||
for (const selector of loginButtonSelectors) {
|
||||
const btn = document.querySelector(selector);
|
||||
if (btn && btn.offsetWidth > 0 && btn.offsetHeight > 0) {
|
||||
loginBtn = btn;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// 如果没有按选择器找到,尝试按文本内容查找
|
||||
if (!loginBtn) {
|
||||
const allBtns = document.querySelectorAll('button, div[role="button"], span');
|
||||
for (const btn of allBtns) {
|
||||
const text = btn.innerText || btn.textContent;
|
||||
if (btn.offsetWidth > 0 && btn.offsetHeight > 0 &&
|
||||
(text.includes('登录') || text.includes('Log In') || text.includes('Login')) &&
|
||||
btn.tagName !== 'BODY') {
|
||||
loginBtn = btn;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (loginBtn) {
|
||||
console.log("[QuickPurge] Login button found, clicking...");
|
||||
loginBtn.click();
|
||||
|
||||
// --- New: Secondary Confirmation Auto-click ---
|
||||
// 登录后开启定时检测,识别“账号已登录”确认弹窗
|
||||
let confirmTries = 0;
|
||||
const confirmInterval = setInterval(() => {
|
||||
confirmTries++;
|
||||
const allBtns = document.querySelectorAll('button, div[role="button"], span');
|
||||
let confirmBtn = null;
|
||||
|
||||
for (const btn of allBtns) {
|
||||
const text = (btn.innerText || btn.textContent).trim();
|
||||
// 匹配“确认”、“继续登录”等文字,且由于是弹窗按钮,通常会有特定的类名
|
||||
if (btn.offsetWidth > 0 && btn.offsetHeight > 0 &&
|
||||
(text === '确认' || text === '确定' || text === '继续登录' || text === 'Confirm') &&
|
||||
btn.tagName !== 'BODY') {
|
||||
confirmBtn = btn;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (confirmBtn) {
|
||||
console.log("[QuickPurge] Secondary confirmation button found, clicking...");
|
||||
confirmBtn.click();
|
||||
clearInterval(confirmInterval);
|
||||
}
|
||||
|
||||
if (confirmTries > 20) { // 最多检测 5 秒 (250mx * 20)
|
||||
clearInterval(confirmInterval);
|
||||
}
|
||||
}, 250);
|
||||
}
|
||||
}, 300); // 填充完后等待 300ms 点击,确保框架已处理输入
|
||||
}, 50);
|
||||
|
||||
return true;
|
||||
|
||||
64
popup.css
64
popup.css
@@ -238,8 +238,25 @@ button {
|
||||
flex-grow: 1;
|
||||
overflow: hidden;
|
||||
margin-right: 8px;
|
||||
pointer-events: none;
|
||||
/* easier dragging */
|
||||
cursor: pointer;
|
||||
padding: 6px 8px;
|
||||
border-radius: 6px;
|
||||
transition: all 0.2s ease;
|
||||
/* Ensure child clicks are captured */
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
.rule-info:hover {
|
||||
background: rgba(59, 130, 246, 0.08);
|
||||
}
|
||||
|
||||
.rule-info:active {
|
||||
background: rgba(59, 130, 246, 0.15);
|
||||
transform: scale(0.995);
|
||||
}
|
||||
|
||||
.rule-info:active {
|
||||
background: rgba(59, 130, 246, 0.1);
|
||||
}
|
||||
|
||||
.rule-domain {
|
||||
@@ -254,15 +271,38 @@ button {
|
||||
|
||||
.rule-env {
|
||||
display: inline-block;
|
||||
background: rgba(59, 130, 246, 0.1);
|
||||
color: var(--accent-color);
|
||||
padding: 2px 6px;
|
||||
border-radius: 4px;
|
||||
font-size: 10px;
|
||||
font-weight: 500;
|
||||
margin-left: 6px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.env-dd-pp {
|
||||
background: #E0E7FF;
|
||||
color: #4338CA;
|
||||
border: 1px solid #C7D2FE;
|
||||
}
|
||||
|
||||
.env-dd-ats {
|
||||
background: #DCFCE7;
|
||||
color: #15803D;
|
||||
border: 1px solid #BBF7D0;
|
||||
}
|
||||
|
||||
.env-std-pp {
|
||||
background: #FEF3C7;
|
||||
color: #B45309;
|
||||
border: 1px solid #FDE68A;
|
||||
}
|
||||
|
||||
.env-std-ats {
|
||||
background: #FCE7F3;
|
||||
color: #BE185D;
|
||||
border: 1px solid #FBCFE8;
|
||||
}
|
||||
|
||||
.rule-acc {
|
||||
font-size: 11px;
|
||||
color: var(--text-secondary);
|
||||
@@ -276,24 +316,14 @@ button {
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.btn-execute {
|
||||
color: var(--success-color);
|
||||
background: rgba(16, 185, 129, 0.1);
|
||||
}
|
||||
|
||||
.btn-execute:hover {
|
||||
background: rgba(16, 185, 129, 0.2);
|
||||
color: #059669;
|
||||
}
|
||||
|
||||
.btn-delete {
|
||||
color: var(--danger-color);
|
||||
background: rgba(239, 68, 68, 0.1);
|
||||
background: rgba(239, 68, 68, 0.05);
|
||||
}
|
||||
|
||||
.btn-delete:hover {
|
||||
background: rgba(239, 68, 68, 0.2);
|
||||
color: #b91c1c;
|
||||
background: rgba(239, 68, 68, 0.15);
|
||||
color: var(--danger-hover);
|
||||
}
|
||||
|
||||
.loading {
|
||||
|
||||
63
popup.html
63
popup.html
@@ -3,26 +3,34 @@
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>QuickPurge</title>
|
||||
<title>清速登</title>
|
||||
<link rel="stylesheet" href="popup.css">
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&display=swap" rel="stylesheet">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="app-view">
|
||||
<div class="header">
|
||||
<h1>QuickPurge & AutoLogin</h1>
|
||||
<button id="add-btn" class="icon-btn" title="添加新账号配置">
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M12 5v14m-7-7h14" />
|
||||
</svg>
|
||||
<h1>清速登</h1>
|
||||
<div>
|
||||
<a href="#" id="logout-btn"
|
||||
style="font-size: 11px; color: var(--text-secondary); text-decoration: none; margin-right: 8px;">切换账号</a>
|
||||
<button id="add-btn" class="primary-btn btn-sm" style="padding: 4px 10px; font-size: 12px;"
|
||||
title="添加新账号配置">
|
||||
添加账号
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="add-form" class="hidden">
|
||||
<h2 id="form-title">添加配置</h2>
|
||||
<input type="hidden" id="editing-id" />
|
||||
<div class="form-group">
|
||||
<select id="env-select">
|
||||
<option value="PP">PP环境 (app135148)</option>
|
||||
<option value="ATS">ATS环境 (app135149)</option>
|
||||
<option value="DD_PP">DD PP</option>
|
||||
<option value="DD_ATS">DD ATS</option>
|
||||
<option value="STD_PP">STD PP</option>
|
||||
<option value="STD_ATS">STD ATS</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
@@ -45,6 +53,45 @@
|
||||
<!-- Rules will be dynamically inserted here by JS -->
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 登录视图 -->
|
||||
<div id="login-view" class="view-container">
|
||||
<div class="auth-box">
|
||||
<h2 style="font-size: 16px; margin-bottom: 20px;">账户登录</h2>
|
||||
<div class="form-group">
|
||||
<input type="text" id="auth-login-username" placeholder="请输入同步账号">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<input type="password" id="auth-login-password" placeholder="请输入密码">
|
||||
</div>
|
||||
<button id="auth-login-btn" class="primary-btn" style="width: 100%; margin-top: 10px;">登录同步</button>
|
||||
<div style="margin-top: 15px; font-size: 12px; text-align: center;">
|
||||
还没有账号? <a href="#" id="go-to-register"
|
||||
style="color: var(--accent-color); text-decoration: none;">立即注册</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 注册视图 -->
|
||||
<div id="register-view" class="view-container hidden">
|
||||
<div class="auth-box">
|
||||
<h2 style="font-size: 16px; margin-bottom: 20px;">新用户注册</h2>
|
||||
<div class="form-group">
|
||||
<input type="text" id="auth-reg-username" placeholder="账号">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<input type="password" id="auth-reg-password" placeholder="密码">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<input type="text" id="auth-reg-invite" placeholder="邀请码">
|
||||
</div>
|
||||
<button id="auth-reg-btn" class="primary-btn" style="width: 100%; margin-top: 10px;">注册</button>
|
||||
<div style="margin-top: 15px; font-size: 12px; text-align: center;">
|
||||
已有账号? <a href="#" id="go-to-login" style="color: var(--accent-color); text-decoration: none;">返回登录</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="popup.js"></script>
|
||||
</body>
|
||||
|
||||
291
popup.js
291
popup.js
@@ -9,59 +9,242 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
const usernameInput = document.getElementById('username-input');
|
||||
const passwordInput = document.getElementById('password-input');
|
||||
const rulesList = document.getElementById('rules-list');
|
||||
const rulesContainer = document.querySelector('.rules-container');
|
||||
const formTitle = document.getElementById('form-title');
|
||||
const editingIdInput = document.getElementById('editing-id');
|
||||
|
||||
const loginView = document.getElementById('login-view');
|
||||
const registerView = document.getElementById('register-view');
|
||||
const appView = document.getElementById('app-view');
|
||||
const authLoginBtn = document.getElementById('auth-login-btn');
|
||||
const authRegBtn = document.getElementById('auth-reg-btn');
|
||||
const goToRegister = document.getElementById('go-to-register');
|
||||
const goToLogin = document.getElementById('go-to-login');
|
||||
const logoutBtn = document.getElementById('logout-btn');
|
||||
|
||||
if (logoutBtn) {
|
||||
logoutBtn.addEventListener('click', (e) => {
|
||||
e.preventDefault();
|
||||
if (confirm('确定要退出当前同步账号吗?')) {
|
||||
chrome.storage.local.remove(['quickPurgeUser'], () => {
|
||||
currentUser = null;
|
||||
rules = [];
|
||||
renderRules(); // 立即清空界面显示的 rules
|
||||
showView('login');
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const ENV_CONFIG = {
|
||||
PP: {
|
||||
DD_PP: {
|
||||
domain: 'app135148.dingtalkoxm.com',
|
||||
url: 'https://app135148.dingtalkoxm.com/user/login'
|
||||
},
|
||||
ATS: {
|
||||
DD_ATS: {
|
||||
domain: 'app135149.dingtalkoxm.com',
|
||||
url: 'https://app135149.dingtalkoxm.com/login'
|
||||
},
|
||||
STD_PP: {
|
||||
domain: 'core.mokahr.com',
|
||||
url: 'https://core.mokahr.com'
|
||||
},
|
||||
STD_ATS: {
|
||||
domain: 'app.mokahr.com',
|
||||
url: 'https://app.mokahr.com'
|
||||
}
|
||||
};
|
||||
|
||||
const API_BASE = 'https://sqd.zhouzishen.cn/api'; // 已更新为您的生产服务器地址
|
||||
let currentUser = null;
|
||||
let rules = [];
|
||||
let dragStartIndex = -1;
|
||||
|
||||
// Load rules from chrome.storage
|
||||
function loadRules() {
|
||||
if (typeof chrome !== 'undefined' && chrome.storage) {
|
||||
chrome.storage.local.get(['quickPurgeAccounts'], (result) => {
|
||||
rules = result.quickPurgeAccounts || [];
|
||||
// --- 工具函数 ---
|
||||
function hasChinese(str) {
|
||||
return /[\u4E00-\u9FA5]/.test(str);
|
||||
}
|
||||
|
||||
// --- Auth View Controllers ---
|
||||
function showView(viewName) {
|
||||
if (loginView) loginView.classList.add('hidden');
|
||||
if (registerView) registerView.classList.add('hidden');
|
||||
if (appView) appView.classList.add('hidden');
|
||||
|
||||
if (viewName === 'app') {
|
||||
if (appView) appView.classList.remove('hidden');
|
||||
} else {
|
||||
if (viewName === 'login' && loginView) loginView.classList.remove('hidden');
|
||||
else if (viewName === 'register' && registerView) registerView.classList.remove('hidden');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if (goToRegister) goToRegister.addEventListener('click', (e) => { e.preventDefault(); showView('register'); });
|
||||
if (goToLogin) goToLogin.addEventListener('click', (e) => { e.preventDefault(); showView('login'); });
|
||||
|
||||
if (authRegBtn) {
|
||||
authRegBtn.addEventListener('click', async () => {
|
||||
const username = document.getElementById('auth-reg-username').value.trim();
|
||||
const password = document.getElementById('auth-reg-password').value;
|
||||
const inviteCode = document.getElementById('auth-reg-invite').value.trim();
|
||||
|
||||
if (!username || !password || !inviteCode) return alert('请填写完整信息');
|
||||
if (hasChinese(username) || hasChinese(password) || hasChinese(inviteCode)) {
|
||||
return alert('账号、密码和邀请码不允许包含中文字符');
|
||||
}
|
||||
authRegBtn.textContent = '注册中...';
|
||||
try {
|
||||
const res = await fetch(`${API_BASE}/register`, {
|
||||
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ username, password, inviteCode })
|
||||
});
|
||||
const data = await res.json();
|
||||
if (data.code === 0) {
|
||||
alert('注册成功,请使用该账号登录');
|
||||
document.getElementById('auth-login-username').value = username;
|
||||
document.getElementById('auth-login-password').value = password;
|
||||
showView('login');
|
||||
} else alert(data.msg);
|
||||
} catch (e) { alert('请求失败,请确保本地服务器已启动运行。' + e); }
|
||||
finally { authRegBtn.textContent = '注册'; }
|
||||
});
|
||||
}
|
||||
|
||||
if (authLoginBtn) {
|
||||
authLoginBtn.addEventListener('click', async () => {
|
||||
const username = document.getElementById('auth-login-username').value.trim();
|
||||
const password = document.getElementById('auth-login-password').value;
|
||||
|
||||
if (!username || !password) return alert('请输入账号和密码');
|
||||
if (hasChinese(username) || hasChinese(password)) {
|
||||
return alert('账号和密码格式不正确(不能包含中文)');
|
||||
}
|
||||
authLoginBtn.textContent = '登录中...';
|
||||
try {
|
||||
const res = await fetch(`${API_BASE}/login`, {
|
||||
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ username, password })
|
||||
});
|
||||
const data = await res.json();
|
||||
if (data.code === 0) {
|
||||
currentUser = { username, password };
|
||||
rules = processLoadedRules(data.data.rules || []);
|
||||
chrome.storage.local.set({ quickPurgeUser: { username, password, rules } }, () => {
|
||||
showView('app');
|
||||
renderRules();
|
||||
});
|
||||
} else {
|
||||
} else alert(data.msg);
|
||||
} catch (e) { alert('请求失败,请确保本地服务器已启动运行。' + e); }
|
||||
finally { authLoginBtn.textContent = '登录'; }
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
// Load Init from local cache
|
||||
function loadAndInitBaseApp() {
|
||||
if (typeof chrome !== 'undefined' && chrome.storage) {
|
||||
chrome.storage.local.get(['quickPurgeUser'], async (result) => {
|
||||
const user = result.quickPurgeUser;
|
||||
if (user && user.username && user.password) {
|
||||
currentUser = user;
|
||||
rules = processLoadedRules(user.rules || []);
|
||||
showView('app'); // Proceed to app UI
|
||||
renderRules();
|
||||
|
||||
// 静默发起登录获取云端最新数据
|
||||
try {
|
||||
const res = await fetch(`${API_BASE}/login`, {
|
||||
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ username: user.username, password: user.password })
|
||||
});
|
||||
const data = await res.json();
|
||||
if (data.code === 0) {
|
||||
rules = processLoadedRules(data.data.rules || []);
|
||||
chrome.storage.local.set({ quickPurgeUser: { ...currentUser, rules } });
|
||||
renderRules();
|
||||
}
|
||||
} catch (e) {
|
||||
console.log("Offline mode, using local storage cache fallback.");
|
||||
}
|
||||
} else {
|
||||
showView('login'); // Not logged in
|
||||
}
|
||||
});
|
||||
} else {
|
||||
showView('login');
|
||||
}
|
||||
}
|
||||
|
||||
function processLoadedRules(rawRules) {
|
||||
return rawRules.map(rule => {
|
||||
if (rule.env === 'PP') rule.env = 'DD_PP';
|
||||
if (rule.env === 'ATS') rule.env = 'DD_ATS';
|
||||
return rule;
|
||||
});
|
||||
}
|
||||
|
||||
function saveRules() {
|
||||
if (typeof chrome !== 'undefined' && chrome.storage) {
|
||||
chrome.storage.local.set({ quickPurgeAccounts: rules }, () => {
|
||||
if (typeof chrome !== 'undefined' && chrome.storage && currentUser) {
|
||||
chrome.storage.local.set({ quickPurgeUser: { ...currentUser, rules } }, () => {
|
||||
renderRules();
|
||||
// 异步防抖提交到自建服务云端
|
||||
fetch(`${API_BASE}/sync`, {
|
||||
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ username: currentUser.username, rules })
|
||||
}).catch(e => console.error("Sync error:", e));
|
||||
});
|
||||
} else {
|
||||
renderRules();
|
||||
}
|
||||
}
|
||||
|
||||
function toggleAddForm() {
|
||||
function toggleAddForm(mode = 'add', ruleId = null) {
|
||||
if (mode === 'edit') {
|
||||
const rule = rules.find(r => r.id === ruleId);
|
||||
if (rule) {
|
||||
formTitle.textContent = '编辑配置';
|
||||
editingIdInput.value = rule.id;
|
||||
envSelect.value = rule.env;
|
||||
clientInput.value = rule.clientName;
|
||||
usernameInput.value = rule.username;
|
||||
passwordInput.value = rule.password;
|
||||
addForm.classList.remove('hidden');
|
||||
rulesContainer.classList.add('hidden');
|
||||
addBtn.classList.add('hidden'); // Optional: hide add btn while editing
|
||||
}
|
||||
} else {
|
||||
formTitle.textContent = '添加配置';
|
||||
editingIdInput.value = '';
|
||||
resetForm();
|
||||
addForm.classList.toggle('hidden');
|
||||
|
||||
if (addForm.classList.contains('hidden')) {
|
||||
rulesContainer.classList.remove('hidden');
|
||||
addBtn.classList.remove('hidden');
|
||||
} else {
|
||||
rulesContainer.classList.add('hidden');
|
||||
addBtn.classList.add('hidden');
|
||||
}
|
||||
}
|
||||
|
||||
if (!addForm.classList.contains('hidden')) {
|
||||
clientInput.focus();
|
||||
}
|
||||
}
|
||||
|
||||
addBtn.addEventListener('click', toggleAddForm);
|
||||
addBtn.addEventListener('click', () => toggleAddForm('add'));
|
||||
|
||||
cancelBtn.addEventListener('click', () => {
|
||||
addForm.classList.add('hidden');
|
||||
rulesContainer.classList.remove('hidden');
|
||||
addBtn.classList.remove('hidden');
|
||||
resetForm();
|
||||
});
|
||||
|
||||
function resetForm() {
|
||||
envSelect.value = 'PP';
|
||||
editingIdInput.value = '';
|
||||
envSelect.value = 'DD_PP';
|
||||
clientInput.value = '';
|
||||
usernameInput.value = '';
|
||||
passwordInput.value = '';
|
||||
@@ -72,12 +255,27 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
const clientName = clientInput.value.trim();
|
||||
const username = usernameInput.value.trim();
|
||||
const password = passwordInput.value;
|
||||
const editingId = editingIdInput.value;
|
||||
|
||||
if (!clientName || !username || !password) {
|
||||
alert('请填写完整的客户名称、账号和密码!');
|
||||
return;
|
||||
}
|
||||
|
||||
if (editingId) {
|
||||
// Update existing rule
|
||||
const index = rules.findIndex(r => r.id === editingId);
|
||||
if (index !== -1) {
|
||||
rules[index] = {
|
||||
...rules[index],
|
||||
env: env,
|
||||
clientName: clientName,
|
||||
username: username,
|
||||
password: password
|
||||
};
|
||||
}
|
||||
} else {
|
||||
// Add new rule
|
||||
rules.push({
|
||||
id: Date.now().toString(),
|
||||
env: env,
|
||||
@@ -85,9 +283,12 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
username: username,
|
||||
password: password
|
||||
});
|
||||
}
|
||||
|
||||
saveRules();
|
||||
addForm.classList.add('hidden');
|
||||
rulesContainer.classList.remove('hidden');
|
||||
addBtn.classList.remove('hidden');
|
||||
resetForm();
|
||||
});
|
||||
|
||||
@@ -117,16 +318,18 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
<div class="drag-handle" title="拖拽排序">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M8 9h8M8 15h8" /></svg>
|
||||
</div>
|
||||
<div class="rule-info">
|
||||
<div class="rule-domain" title="${rule.clientName}">
|
||||
<div class="rule-info" data-id="${rule.id}" title="点击执行一键登录">
|
||||
<div class="rule-domain">
|
||||
${rule.clientName}
|
||||
<span class="rule-env">${rule.env}</span>
|
||||
<span class="rule-env env-${rule.env.toLowerCase().replace('_', '-')}">${rule.env.replace('_', ' ')}</span>
|
||||
</div>
|
||||
<div class="rule-acc" title="${rule.username}">帐号: ${rule.username}</div>
|
||||
<div class="rule-acc">帐号: ${rule.username}</div>
|
||||
</div>
|
||||
<div class="rule-actions">
|
||||
<button class="icon-btn btn-execute" data-id="${rule.id}" title="清理数据并自动登录">
|
||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M13 10V3L4 14h7v7l9-11h-7z"/></svg>
|
||||
<button class="icon-btn btn-edit" data-id="${rule.id}" title="编辑此配置">
|
||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z" />
|
||||
</svg>
|
||||
</button>
|
||||
<button class="icon-btn btn-delete" data-id="${rule.id}" title="删除此配置">
|
||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"/></svg>
|
||||
@@ -138,21 +341,31 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
rulesList.appendChild(li);
|
||||
});
|
||||
|
||||
document.querySelectorAll('.btn-delete').forEach(btn => {
|
||||
btn.addEventListener('click', (e) => {
|
||||
document.querySelectorAll('.rule-info').forEach(infoArea => {
|
||||
infoArea.addEventListener('click', (e) => {
|
||||
const id = e.currentTarget.getAttribute('data-id');
|
||||
rules = rules.filter(r => r.id !== id);
|
||||
saveRules();
|
||||
const rule = rules.find(r => r.id === id);
|
||||
if (rule) {
|
||||
executePurgeAndLogin(rule, e.currentTarget);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
document.querySelectorAll('.btn-execute').forEach(btn => {
|
||||
document.querySelectorAll('.btn-edit').forEach(btn => {
|
||||
btn.addEventListener('click', (e) => {
|
||||
const button = e.currentTarget;
|
||||
const id = button.getAttribute('data-id');
|
||||
const rule = rules.find(r => r.id === id);
|
||||
if (rule) {
|
||||
executePurgeAndLogin(rule, button);
|
||||
e.stopPropagation();
|
||||
const id = e.currentTarget.getAttribute('data-id');
|
||||
toggleAddForm('edit', id);
|
||||
});
|
||||
});
|
||||
|
||||
document.querySelectorAll('.btn-delete').forEach(btn => {
|
||||
btn.addEventListener('click', (e) => {
|
||||
e.stopPropagation();
|
||||
const id = e.currentTarget.getAttribute('data-id');
|
||||
if (confirm('确定要删除此配置吗?')) {
|
||||
rules = rules.filter(r => r.id !== id);
|
||||
saveRules();
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -204,10 +417,9 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
saveRules();
|
||||
}
|
||||
|
||||
function executePurgeAndLogin(rule, buttonElement) {
|
||||
const svg = buttonElement.querySelector('svg');
|
||||
const originalSvg = svg.outerHTML;
|
||||
buttonElement.innerHTML = `<svg class="loading" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M12 2v4m0 12v4M4.93 4.93l2.83 2.83m8.48 8.48l2.83 2.83M2 12h4m12 0h4M4.93 19.07l2.83-2.83m8.48-8.48l2.83-2.83"/></svg>`;
|
||||
function executePurgeAndLogin(rule, element) {
|
||||
const originalContent = element.innerHTML;
|
||||
element.innerHTML = `<div class="loading-state"><svg class="loading" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M12 2v4m0 12v4M4.93 4.93l2.83 2.83m8.48 8.48l2.83 2.83M2 12h4m12 0h4M4.93 19.07l2.83-2.83m8.48-8.48l2.83-2.83"/></svg><span>清理中...</span></div>`;
|
||||
|
||||
if (typeof chrome !== 'undefined' && chrome.runtime) {
|
||||
const config = ENV_CONFIG[rule.env];
|
||||
@@ -218,16 +430,13 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
username: rule.username,
|
||||
password: rule.password
|
||||
}, (response) => {
|
||||
// Will visually reset only if popup is somehow still open after redirect (unlikely as active tab changes)
|
||||
buttonElement.innerHTML = originalSvg;
|
||||
});
|
||||
} else {
|
||||
// Return text after a short delay
|
||||
setTimeout(() => {
|
||||
console.log(`Mock: Purging ${rule.env} and filling ${rule.username}`);
|
||||
buttonElement.innerHTML = originalSvg;
|
||||
}, 1000);
|
||||
if (element) element.innerHTML = originalContent;
|
||||
}, 1500);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
loadRules();
|
||||
loadAndInitBaseApp();
|
||||
});
|
||||
|
||||
17
server/Dockerfile
Normal file
17
server/Dockerfile
Normal file
@@ -0,0 +1,17 @@
|
||||
FROM node:18-slim
|
||||
|
||||
# 安装基本运行环境(Debian slim 版通常有预编译的 sqlite3 二进制包,速度极快)
|
||||
WORKDIR /app
|
||||
RUN mkdir -p /app/data
|
||||
|
||||
# 强制安装不从源码编译的 sqlite3 以节省时间
|
||||
RUN npm install express cors sqlite3 --build-from-source=false
|
||||
|
||||
# 复制服务端源码
|
||||
COPY server.js .
|
||||
|
||||
# 暴露 3000 端口
|
||||
EXPOSE 3000
|
||||
|
||||
# 启动服务
|
||||
CMD ["node", "server.js"]
|
||||
12
server/docker-compose.yml
Normal file
12
server/docker-compose.yml
Normal file
@@ -0,0 +1,12 @@
|
||||
version: '3.8'
|
||||
|
||||
services:
|
||||
quickpurge-sync:
|
||||
build: .
|
||||
container_name: quickpurge-sync
|
||||
restart: always
|
||||
ports:
|
||||
- "3001:3001"
|
||||
volumes:
|
||||
# 将数据目录整体挂载,防止被 Docker 误识别为文件夹
|
||||
- ./data:/app/data
|
||||
14
server/package.json
Normal file
14
server/package.json
Normal file
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"name": "quickpurge-sync",
|
||||
"version": "1.0.0",
|
||||
"description": "QuickPurge Sync Server",
|
||||
"main": "server.js",
|
||||
"dependencies": {
|
||||
"cors": "^2.8.5",
|
||||
"express": "^4.18.2",
|
||||
"sqlite3": "^5.1.6"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "node server.js"
|
||||
}
|
||||
}
|
||||
129
server/server.js
Normal file
129
server/server.js
Normal file
@@ -0,0 +1,129 @@
|
||||
const express = require('express');
|
||||
const cors = require('cors');
|
||||
const sqlite3 = require('sqlite3').verbose();
|
||||
const path = require('path');
|
||||
|
||||
const app = express();
|
||||
const PORT = process.env.PORT || 3001;
|
||||
const INVITE_CODE = 'Moka'; // 注册需校验此邀请码
|
||||
|
||||
// 启用中间件
|
||||
app.use(cors()); // 允许 Chrome 扩展跨域请求
|
||||
app.use(express.json()); // 解析 application/json
|
||||
|
||||
// 初始化 SQLite 数据库 (建议使用绝对路径并指向挂载的目录)
|
||||
const dbPath = process.env.DB_PATH || path.join(__dirname, 'data', 'data.db');
|
||||
const dbDir = path.dirname(dbPath);
|
||||
|
||||
// 确保目录存在
|
||||
const fs = require('fs');
|
||||
if (!fs.existsSync(dbDir)) {
|
||||
fs.mkdirSync(dbDir, { recursive: true });
|
||||
}
|
||||
|
||||
const db = new sqlite3.Database(dbPath, (err) => {
|
||||
if (err) {
|
||||
console.error('Could not connect to database', err);
|
||||
} else {
|
||||
console.log('Connected to sqlite3 database at:', dbPath);
|
||||
// 初始化用户表
|
||||
db.run(`
|
||||
CREATE TABLE IF NOT EXISTS users (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
username TEXT UNIQUE NOT NULL,
|
||||
password TEXT NOT NULL,
|
||||
rules TEXT DEFAULT '[]',
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||
)
|
||||
`);
|
||||
}
|
||||
});
|
||||
|
||||
// === 接口 1: 注册 ===
|
||||
app.post('/api/register', (req, res) => {
|
||||
const { username, password, inviteCode } = req.body;
|
||||
|
||||
if (inviteCode !== INVITE_CODE) {
|
||||
return res.json({ code: 1, msg: '注册码错误' });
|
||||
}
|
||||
if (!username || !password) {
|
||||
return res.json({ code: 1, msg: '用户名和密码不能为空' });
|
||||
}
|
||||
|
||||
// 写入数据库 (直接使用 db.run 更简洁)
|
||||
db.run('INSERT INTO users (username, password) VALUES (?, ?)', [username, password], function (err) {
|
||||
if (err) {
|
||||
if (err.message.includes('UNIQUE constraint failed')) {
|
||||
return res.json({ code: 1, msg: '用户名太火爆,已被注册' });
|
||||
}
|
||||
console.error('Register error:', err);
|
||||
return res.json({ code: 1, msg: '服务器内部错误' });
|
||||
}
|
||||
res.json({ code: 0, msg: '注册成功' });
|
||||
});
|
||||
});
|
||||
|
||||
// === 接口 2: 登录 ===
|
||||
app.post('/api/login', (req, res) => {
|
||||
const { username, password } = req.body;
|
||||
|
||||
if (!username || !password) {
|
||||
return res.json({ code: 1, msg: '请输入用户名和密码' });
|
||||
}
|
||||
|
||||
db.get('SELECT * FROM users WHERE username = ? AND password = ?', [username, password], (err, row) => {
|
||||
if (err) {
|
||||
console.error('Login error:', err);
|
||||
return res.json({ code: 1, msg: '服务器内部错误' });
|
||||
}
|
||||
if (!row) {
|
||||
return res.json({ code: 1, msg: '账号或密码错误' });
|
||||
}
|
||||
|
||||
let parsedRules = [];
|
||||
try {
|
||||
parsedRules = row.rules ? JSON.parse(row.rules) : [];
|
||||
} catch (e) {
|
||||
console.error('Failed to parse rules JSON', e);
|
||||
}
|
||||
|
||||
res.json({
|
||||
code: 0,
|
||||
msg: '登录成功',
|
||||
data: { rules: parsedRules }
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// === 接口 3: 同步配置数据 (保存更新) ===
|
||||
app.post('/api/sync', (req, res) => {
|
||||
const { username, rules } = req.body;
|
||||
|
||||
if (!username || !Array.isArray(rules)) {
|
||||
return res.json({ code: 1, msg: '参数不合法' });
|
||||
}
|
||||
|
||||
const rulesJsonStr = JSON.stringify(rules);
|
||||
|
||||
// 覆盖更新该用户的 rules 列
|
||||
// (简化起见,此处信任前端传来的 username。真实生产在这一步应当检验 token/session,不过做个人小工具够用即可防君子)
|
||||
db.run('UPDATE users SET rules = ? WHERE username = ?', [rulesJsonStr, username], function (err) {
|
||||
if (err) {
|
||||
console.error('Sync error:', err);
|
||||
return res.json({ code: 1, msg: '同步失败:服务器内部错误' });
|
||||
}
|
||||
if (this.changes === 0) {
|
||||
return res.json({ code: 1, msg: '同步失败:未找到该用户' });
|
||||
}
|
||||
res.json({ code: 0, msg: '同步成功' });
|
||||
});
|
||||
});
|
||||
|
||||
// 启动服务器
|
||||
app.listen(PORT, '0.0.0.0', () => {
|
||||
console.log(`Server is running on http://0.0.0.0:${PORT}`);
|
||||
console.log(`API endpoints available:`);
|
||||
console.log(`- POST /api/register`);
|
||||
console.log(`- POST /api/login`);
|
||||
console.log(`- POST /api/sync`);
|
||||
});
|
||||
Reference in New Issue
Block a user